嵌入式系统 Boot Loader 技术内幕


嵌入式 linux 系统中,通常需要由 boot loader 设置的常见启动参数有:atag_core、atag_mem、atag_cmdline、atag_ramdisk、atag_initrd等。

比如,设置 atag_core 的代码如下:

params = (struct tag *)boot_params;params->hdr.tag = atag_core;params->hdr.size = tag_size(tag_core);params->u.core.flags = 0;params->u.core.pagesize = 0;params->u.core.rootdev = 0;params = tag_next(params);

其中,boot_params 表示内核启动参数在内存中的起始基地址,指针 params 是一个 struct tag 类型的指针。宏 tag_next() 将以指向当前标记的指针为参数,计算紧临当前标记的下一个标记的起始地址。注意,内核的根文件系统所在的设备id就是在这里设置的。

下面是设置内存映射情况的示例代码:

for(i = 0; i < num_mem_areas; i++) {if(memory_map[i].used) {params->hdr.tag = atag_mem;params->hdr.size = tag_size(tag_mem32);params->u.mem.start = memory_map[i].start;params->u.mem.size = memory_map[i].size;params = tag_next(params);}}

可以看出,在 memory_map[]数组中,每一个有效的内存段都对应一个 atag_mem 参数标记。

linux 内核在启动时可以以命令行参数的形式来接收信息,利用这一点我们可以向内核提供那些内核不能自己检测的硬件参数信息,或者重载(override)内核自己检测到的信息。比如,我们用这样一个命令行参数字符串"console=ttys0,115200n8"来通知内核以 ttys0 作为控制台,且串口采用 "115200bps、无奇偶校验、8位数据位"这样的设置。下面是一段设置调用内核命令行参数字符串的示例代码:

char *p;/* eat leading white space */for(p = commandline; *p == " "; p++);/* skip non-existent command lines so the kernel will still    * use its default command line. */if(*p == "\0")return;params->hdr.tag = atag_cmdline;params->hdr.size = (sizeof(struct tag_header) + strlen(p) + 1 + 4) >> 2;strcpy(params->u.cmdline.cmdline, p);params = tag_next(params);

请注意在上述代码中,设置 tag_header 的大小时,必须包括字符串的终止符"\0",此外还要将字节数向上圆整4个字节,因为 tag_header 结构中的size 成员表示的是字数。

下面是设置 atag_initrd 的示例代码,它告诉内核在 ram 中的什么地方可以找到 initrd 映象(压缩格式)以及它的大小:

params->hdr.tag = atag_initrd2;params->hdr.size = tag_size(tag_initrd);params->u.initrd.start = ramdisk_ram_base;params->u.initrd.size = initrd_len;params = tag_next(params);

下面是设置 atag_ramdisk 的示例代码,它告诉内核解压后的 ramdisk 有多大(单位是kb):

params->hdr.tag = atag_ramdisk;params->hdr.size = tag_size(tag_ramdisk);params->u.ramdisk.start = 0;params->u.ramdisk.size = ramdisk_size; /* 请注意,单位是kb */params->u.ramdisk.flags = 1; /* automatically load ramdisk */params = tag_next(params);

最后,设置 atag_none 标记,结束整个启动参数列表:

static void setup_end_tag(void){params->hdr.tag = atag_none;params->hdr.size = 0;}

3.2.5 调用内核

boot loader 调用 linux 内核的方法是直接跳转到内核的第一条指令处,也即直接跳转到 mem_start+0x8000 地址处。在跳转时,下列条件要满足:

1. cpu 寄存器的设置:

  • r0=0;

  • r1=机器类型 id;关于 machine type number,可以参见 linux/arch/arm/tools/mach-types。

  • r2=启动参数标记列表在 ram 中起始基地址;

2. cpu 模式:

  • 必须禁止中断(irqs和fiqs);

  • cpu 必须 svc 模式;

3. cache 和 mmu 的设置:

  • mmu 必须关闭;

  • 指令 cache 可以打开也可以关闭;

  • 数据 cache 必须关闭;

如果用 c 语言,可以像下列示例代码这样来调用内核:

void (*thekernel)(int zero, int arch, u32 params_addr) = (void (*)(int, int, u32))kernel_ram_base;……thekernel(0, arch_number, (u32) kernel_params_start);

注意,thekernel()函数调用应该永远不返回的。如果这个调用返回,则说明出错。


 

4. 关于串口终端

在 boot loader 程序的设计与实现中,没有什么能够比从串口终端正确地收到打印信息能更令人激动了。此外,向串口终端打印信息也是一个非常重要而又有效的调试手段。但是,我们经常会碰到串口终端显示乱码或根本没有显示的问题。造成这个问题主要有两种原因:(1) boot loader 对串口的初始化设置不正确。(2) 运行在 host 端的终端仿真程序对串口的设置不正确,这包括:波特率、奇偶校验、数据位和停止位等方面的设置。

此外,有时也会碰到这样的问题,那就是:在 boot loader 的运行过程中我们可以正确地向串口终端输出信息,但当 boot loader 启动内核后却无法看到内核的启动输出信息。对这一问题的原因可以从以下几个方面来考虑:

(1) 首先请确认你的内核在编译时配置了对串口终端的支持,并配置了正确的串口驱动程序。

(2) 你的 boot loader 对串口的初始化设置可能会和内核对串口的初始化设置不一致。此外,对于诸如 s3c44b0x 这样的 cpu,cpu 时钟频率的设置也会影响串口,因此如果 boot loader 和内核对其 cpu 时钟频率的设置不一致,也会使串口终端无法正确显示信息。

(3) 最后,还要确认 boot loader 所用的内核基地址必须和内核映像在编译时所用的运行基地址一致,尤其是对于 uclinux 而言。假设你的内核映像在编译时用的基地址是 0xc0008000,但你的 boot loader 却将它加载到 0xc0010000 处去执行,那么内核映像当然不能正确地执行了。

5. 结束语

boot loader 的设计与实现是一个非常复杂的过程。如果不能从串口收到那激动人心的"uncompressing linux.................. done, booting the kernel……"内核启动信息,恐怕谁也不能说:"嗨,我的 boot loader 已经成功地转起来了!"。



 

COPYRIGHT(C) 2011 厦门永宏亚得机电科技有限公司版权所有(闽ICP备05025945号) ALL RIGHTS RESERVED?

电话: 0592-5190891 传真: 0592-5190720 E-Mail: E-mail:yade8895@163.com
地址: 厦门市海沧区兴港六里17号2607室 邮编:361009 联系人:翟先生