Linux内核学习(四):Bootloader的特种兵-Uboot(二)
内容全部来自韦神《嵌入式Linux应用开发完全手册》
在上一篇的Linux内核学习(三):Bootloader的特种兵-Uboot(一)中我们学习了关于Uboot的介绍、源码结构以及配置和编译的过程,到这一章我们来看看U-Boot的启动过程源码的分析。
很多的时候会把系统文件那些放在flash,然后运行的时候加载到内存中,然后启动。这个加载的过程就是需要UBOOT来完成。那Uboot本身的位置放在哪里呢?这个其实也是不确定的,有可能就在chip on rom里面,也可能在flash里面,这个过程是可以做很多的自定义的。大多数时候是从flash中执行一段代码,然后将flash中储存的代码和数据搬移到ram中,然后跳转到ram中执行。
为什么要用flash来存储系统,这是从价格、还有内存是否掉电还在等各个方面综合的结果。我们常见的PC的bias就在flash里面,这是因为其掉电了还能保存。flash是更加强大的ROM,电脑的内存条是RAM,这样讲起来真的要不我再整一篇关于内存的东西来捋一下这个ROM、RAM、Flash吧。
扯远了,开始回到正题。本文使用的U-Boot 从NOR Flash启动,U-Boot属于两阶段的Bootloader,
1、UBoot的启动过程源码分析
1.1、U-Boot第一阶段代码分析
(1)硬件设备初始化。
依次完成如下设置:将CPU的工作模式设为管理模式(svc),关闭WATCHDOG,设置FCLK、HCLK、PCLK的比例(即设置CLKDIVN寄存器),关闭 MMU、CACHE。
(2)为加载Bootloader的第二阶段代码准备RAM空间。
所谓准备RAM 空间,就是初始化内存芯片,使它可用。
(3)复制Bootloader 的第二阶段代码到 RAM空间中。
( 4)设置好栈。
栈的设置灵活性很大,只要让 sp寄存器指向一段没有使用的内存即可。
韦神的这个图很完备,很多的地方都只有上半个部分
(5)跳转到第二阶段代码的C入口点。
在跳转之前,还要清除BSS段(初始值为0、无初始值的全局变量、静态变量放在BSS段)
1.2、U-Boot第二阶段代码分析
U-Boot在启动内核之前可以让用户决定是否进入下载模式,即进入U-Boot的控制界面。
(1)初始化本阶段要使用到的硬件设备。
最主要的是设置系统时钟、初始化串口,只要这两个设置好了,就可以从串口看到打印信息。
(2)检测系统内存映射( memory map)。
对于特定的开发板,其内存的分布是明确的,所以可以直接设置。
(3) U-Boot命令的格式。
内核的启动,是通过U-Boot命令来实现的。U-Boot中每个命令都通过U_BOOT_CMD宏来定义,格式如下:
U_BOOT_CMD (name,maxargs, repeatable, command, "usage" , "help")
各项参数的意义如下。
- name:命令的名字,注意,它不是一个字符串(不要用双引号括起来)。
- maxargs:最大的参数个数。
- ③repeatable:命令是否可重复,可重复是指运行一个命令后,下次敲回车即可再次运行。
- command:对应的函数指针,类型为(*cmd)(struct cmd_tbl_s , int,int, char[])。
- usage:简短的使用说明,这是个字符串。
- help:较详细的使用说明,这是个字符串。
Struct_Section也是在 include/command.h中定义,如下所示:
下面举个栗子:比如对于bootm命令,它如下定义:
宏U_BOOT_CMD扩展开后如下所示:
对于每个使用U_BOOT_CMD宏来定义的命令,其实都是在".u_boot_cmd"段中定义一个cmd_tbl_t结构。连接脚本U-Boot.lds 中有如下代码:
程序中就是根据命令的名字在内存段__u_boot_cmd_start~__u_boot_cmd_end 找到它的cmd_tbl_t结构,然后调用它的函数(请参考common/command.c中的 find_cmd函数)。
内核的复制和启动,可以通过如下命令来完成:
- bootm从内存、ROM、NOR Flash中启动内核,
- bootp则通过网络来启动,
- 而nboot 从NAND Flash启动内核。
它们都是先将内核映象从各种媒介中读出,存放在指定的位置;然后设置标记列表以给内核传递参数;最后跳到内核的入口点去执行。具体实现的细节不再描述,有兴趣的读者可以阅读common/cmd_boot.c、common/cmd_net.c、common/cmd_nand.c来了解它们的实现。
( 4)为内核设置启动参数。
U-Boot也是通过标记列表向内核传递参数。并且,在15.1.2小节中内存标记、命令行标记的示例代码就是取自U-Boot 中的 setup_memory_tags、setup_commandline_tag 函数,它们都是在 lib _arm/armlinux.c中定义。一般而言,设置这两个标记就可以了,在配置文件include/configs/smdk2410.h中增加如下两个配置项即可:
(这个我在这一篇的文章里写到了,那个ATAG_CORE标记着参数的开始还记得吗)
对于ARM架构的CPU,都是通过lib_arm/armlinux.c中的do_bootm_linux函数来启动内核。这个函数中,设置标记列表,最后通过
theKernel (0, bd→bi_arch_number, bd-bi_boot_params)
调用内核。其中,
- theKernel指向内核存放的地址(对于ARM架构的CPU,通常是Ox30008000),
- bd→bi_arch_number就是前面board_init函数设置的机器类型ID,
- bd→bi_boot_params就是标记列表的开始地址。
到了这里我们就把内核调用起来了,开始跑内核了,到这里经过韦神两篇文章的熏陶你肯定对这个UBOOT有了更深的体会。
最后再来看看UBOOT使用时的常用命令。
2、UBoot的常用命令
2.1、 U-Boot的常用命令的用法
进入U-Boot控制界面后,可以运行各种命令,比如下载文件到内存,擦除、读写Flash、运行内存、NOR Flash、NAND Flash中的程序,查看、修改、比较内存中的数据等。
使用各种命令时,可以使用其开头的若干个字母代替它。比如 tftpboot命令,可以使用t、tf、 tft、tftp 等字母代替,只要其他命令不以这些字母开头即可。
当运行一个命令之后,如果它是可重复执行的(代码中使用U_BOOT_CMD定义这个命令时,第3个参数是1),若想再次运行可以直接输入回车。
U-Boot接收的数据都是十六进制,输入时可以省略前缀0x、0X。下面介绍常用的命令。
( 1)帮助命令help。
运行help命令可以看到U-Boot中所有命令的作用,如果要查看某个命令的使用方法,运行“help命令名”,比如“help bootm”。
可以使用“?”来代替“help”,比如直接输入“?”、“? bootm”。
(2)下载命令。
U-Boot 支持串口下载、网络下载,相关命令有:loadb、loads、loadx、loady和 tftpboot、nfs。
前几个串口下载命令使用方法相似,以 loadx命令为例,它的用法为“loadx [ off ][ baud ]”。
- “[]”表示里面的参数可以省略,
- off表示文件下载后存放的内存地址,
- baud表示使用的波特率。
如果baud参数省略,则使用当前的波特率;如果off参数省略,存放的地址为配置文件中定义的宏CFG_LOAD_ADDR。
tftpboot命令使用TFTP协议从服务器下载文件,服务器的IP地址为环境变量 serverip。用法为
tftpboot [loadAddress] [bootfilename]
- loadAddress表示文件下载后存放的内存地址,
- bootfilename表示要下载的文件的名称。
如果loadAddress 省略,存放的地址为配置文件中定义的宏CFG_LOAD_ADDR;如果 bootfilename省略,则使用开发板的IP地址构造一个文件名,比如开发板IP为192.168.1.17,则默认的文件名为COA80711.img。
nfs命令使用NFS协议下载文件,用法为
nfs [loadAddress] [host ip addr:bootfilename]
loadAddress、bootfilename”的意义与 tftpboot命令一样,
- “host ip addr”表示服务器的IP地址,默认为环境变量serverip。
下载文件成功后,U-Boot 会自动创建或更新环境变量filesize,它表示下载的文件的长度,可以在后续命令中使用“$(filesize)”来引用它。
(3)内存操作命令。
常用的命令有:查看内存命令md、修改内存命令md、填充内存命令mw、复制命令cp。
这些命令都可以带上后缀“.b”、“.w”或“.1",表示以字节、字(2个字节)、双字(4个字节)为单位进行操作。
比如“cp.1 30000000 310000002”将从开始地址0x30000000处,复制2个双字到开始地址为0x31000000的地方。
md命令用法为“md[.b, .w,.1] address [count]",表示以字节、字或双字(默认为双字)为单位,显示从地址:address开始的内存数据,显示的数据个数为count。
mm命令用法为“mm[.b, .w,.l] address",表示以字节、字或双字(默认为双字)为单位,从地址: address开始修改内存数据。执行mm命令后,输入新数据后回车,地址会自动增加,按“Ctrl+C”键退出。
mw命令用法为“mw[.b,.w,.I] address value [count]”,表示以字节、字或双字(默认为双字)为单位,往开始地址为address 的内存中填充count个数据,数据值为value。
cp命令用法为“cp[.b,.w,.l] source target count",表示以字节、字或双字(默认为双字)为单位,从源地址source的内存复制count 个数据到目的地址的内存。
(4)NOR Flash操作命令。
常用的命令有查看Flash信息的 flinfo命令、加/解写保护命令protect、擦除命令erase。由于NOR Flash的接口与一般内存相似,所以一些内存命令可以在NOR Flash .上使用,比如读NOR Flash 时可以使用md、cp命令,写NOR Flash时可以使用cp命令( cp根据地址分辨出是NOR Flash,从而调用NOR Flash 驱动完成写操作)。
直接运行“flinfo”即可看到NOR Flash 的信息,有NOR Flash 的型号、容量、各扇区的开始地址、是否只读等信息。
其中的RO表示该扇区处于写保护状态,只读。
对于只读的扇区,在擦除、烧写它之前,要先解除写保护。最简单的命令为“protect offall”,解除所有NOR Flash的写保护。
erase命令常用的格式为“erase start end",擦除的地址范围为start~end;“erase start +len”,擦除的地址范围为 start~ ( star+tlen-1),“erase all”,表示擦除所有NOR Flash。
注意
其中的地址范围,刚好是一个扇区的开始地址到另一个(或同一个)扇区的结束地址。比如要擦除Amd29LV800BB的前5个扇区,执行的命令为“erase 0 0x2ffff,而非“erase 0 0x30000”.
(5)NAND Flash操作命令。
NAND Flash操作命令只有一个: nand,它根据不同的参数进行不同操作,比如擦除、读取、烧写等。
- “nand info”查看NAND Flash 信息。
- “nand erase [clean] [off size]”擦除NAND Flash。加上“clean”时,表示在每个块的第一个扇区的OOB区加写入清除标记; off、size表示要擦除的开始偏移地址的长度,如果省略off 和 size,表示要擦除整个NAND Flash。
- “nand read[.jffs2] addr off size”从NAND Flash偏移地址 off处读出size个字节的数据存放到开始地址为 addr的内存中。是否加后缀“.jffs”的差别只是读操作时的ECC校验方法不同。
- “nand write[.jffs2] addr off size”把开始地址为addr的内存中的size个字节数据写到NANDFlash 的偏移地址 off处。是否加后缀“.jffs”的差别只是写操作时的ECC校验方法不同。
- “nand read.yaffs addr off size”从 NAND Flash偏移地址off 处读出 size个字节的数据(包括OOB区域),存放到开始地址为addr 的内存中。
- “nand write.yaffs addr off size”把开始地址为addr 的内存中的size个字节数据(其中有要写入OOB区域的数据)写到NAND Flash 的偏移地址 off 处。
- “nand dump off”将NAND Flash偏移地址 off 的一个扇区的数据打印出来,包括OOB数据。
(6)环境变量命令。
- “printenv”命令打印全部环境变量,“printenv name1 name2…”打印名字为name1、name2、…的环境变量。
- “setenv name value”设置名字为name的环境变量的值为value。setenv name”删除名字为name的环境变量。
上面的设置、删除操作只是在内存中进行
- “ saveenv”将更改后的所有环境变量写入NORFlash 中。
(7)启动命令。
不带参数的“boot”、"bootm”命令都是执行环境变量bootcmd所指定的命令。
- “bootm [addr[arg…]]”命令启动存放在地址 addr处的 U-Boot格式的映象文件(使用U-Boot目录 tools下的 mkimage 工具制作得到),[arg…]表示参数。如果addr参数省略,映象文件所在地址为配置文件中定义的宏CFG_LOAD_ADDR。
- “go addr [arg…]”与bootm 命令类似,启动存放在地址 addr处的二进制文件,[arg…]表示参数。
- “nboot [[[loadAddr] dev] offset]”命令将NAND Flash设备dev 上偏移地址 off处的映象文件复制到内存 loadAddr处,然后,如果环境变量 autostart的值为“yes",就启动这个映象。
如果loadAddr参数省略,存放地址为配置文件中定义的宏CFG_LOAD.ADDR;
如果dev参数省略,则它的取值为环境变量 bootdevice的值;
如果 offset参数省略,则默认为0。
2.2、 U-Boot命令使用实例
到这里,差不多就是关于Uboot的全部了,知道了Uboot的流程,也对其源码、命令、操作使用都有了个粗放的认识,最后再整个栗子瞅瞅,当然更多地其实需要你自己打开编译器去看看这个源码的真实面目啦。
下面通过一个例子来演示如何使用各种命令烧写内核映象文件、yaffs映象文件,并启动系统。
(YAFFS是第一个在GPL协议下发布的、基于日志的、专门为NAND Flash存储器设计的、适用于大容量的存储设备的嵌入式文件系统。)
(1)制作内核映象文件。
对于本书使用的Linux 2.6.22.6版本,编译内核时可以直接生成U-Boot 格式的映象文件ulmage。
对于不能直接生成ulmage 的内核,制作方法在.U-Boot根目录下的README 文件中有说明,假设已经编译好的内核文件为vmlinux,它是ELF格式的。
mkimage是U-Boot目录tools下的工具,它在编译U-Boot 时自动生成。执行以下3个命令将内核文件 vmlinux制作为U-Boot格式的映象文件ulmage,它们首先将vmlinux转换为二进制格式,然后压缩,最后构造头部信息(里面包含有文件名称、大小、类型、CRC校验码等),如下所示。
(2)烧写内核映象文件ulmage。
首先将ulmage放在主机上的 tfp 或nfs目录下,确保已经开启tftp 或nfs服务。然后运行如下命令下载文件,擦除、烧写NAND Flash,如下所示。
(3)烧写yaffs 文件系统映象。
假设yaffs文件系统映象的文件名为yaffs.img,首先将它放在主机上的tftp或nfs目录下,确保已经开启 tftp或nfs服务;然后执行如下命令下载、擦除、烧写,如下所示。
这时,重启系统,在U-Boot 倒数3s之后,就会自动启动Linux系统。
(4)烧写jffs2文件系统映象。
(JFFS与YAFFS都是日志结构文件系统(LFS),保障了数据的可靠性与安全性,可以恢复数据。
一般来说,对于小于64MB的NAND Flash,可以选用JFFS2;如果超过64MB,用YAFFS2比较合适)
假设jffs2文件系统映象的文件名为jffs2.img,首先将它放在主机上的tftp或nfs目录下,确保已经开启tftp或nfs服务;然后执行如下命令下载、擦除、烧写,如下所示。
系统启动后,就可以使用“mount -t jffs2 /dev/mtdblock1 /mnt”挂接jffs2文件系统。
(在这里对于这个jffs和yaffs应该是不同用处,协同工作,这两个都是针对flash的文件系统,有jffs2,yaffs2,logfs,ubifs,那就改天和UU们再一起学习一下吧)