本课内容
- u-boot打补丁、编译、烧写、试验
- u-boot功能、结构,结合makefile分析
- u-boot命令分析
- u-boot的核心:命令
- 实现
- 添加一个命令
- 启动内核的过程分析
- 读出内核
- uImage头部
- 设置启动参数:TAG
u-boot打补丁、编译、烧写、试验
bootloarder与linux系统直接相关
bootloarder的最终目的是启动内核。
bootloarder的源码包在网上就能下载,但是你想根据具体需要修改bootloarder时需要打补丁。
在ubuntu体验一下u-boot是个什么东西:
- 将u-boot-1.1.6.tar和u-boot-1.1.6_jz2440.patch两个文件放置于ubuntu之下,补丁文件里面—表示原来的代码,+++表示修改后的代码。@@-34,6 +34,8@@表示原本34行开始六行代码转变成34行开始的8行代码。再在ubuntu中解压缩。
- 补丁怎么打?知道补丁是哪个,那么要打到哪里去?具体补丁打到哪里文件里面有涉及,如下图所示。在u-boot-1.1.6.tar解压后的文件夹目录下运行指令patch -p1< …/u-boot-1.1.6_jz2440.patch即可打补丁,-p1的原因是当前目录是u-boot-1.1.6,因此得把u-boot-1.1.6忽略掉,-p1的作用就是忽略掉第一个斜杠之前的东西。
- u-boot的目的就是要支持很多种单板,因此需要配置。配置方法:输入命令make 100ask24x0_config
- 编译 make
编译好后就会产生u-boot.bin编译文件,将其烧写到开发板上去(烧录过程省略),烧进去之后用串口观察启动数据,记得按空格来进入命令菜单。
按q便可以输入u-boot的命令,用help就能看到很多命令,看命令的用法可以用“?+命令”,回去菜单命令输入menu就可以了。
u-boot菜单可以用来通过usb下载程序、内核、文件系统。
用u-boot命令print可以打印环境变量,比如将倒计时时间改为10后保存。
u-bood是bootloarder中的其中一种,u-boot的目的就是启动内核。
windows中的BIOS是从硬盘上读入内核,而u-boot是从Flash上读出内核并放在SDRAM,接着启动内核。
u-boot为了达到这个终极目的要实现的功能
- 读Flash+写Flash(写Flash是为了开发方便,方便用其他的启动程序,而烧写的程序内容可以从网卡获得,也可以从USB获取),从串口输入命令。
- 初始化SDRAM
- 初始化时钟
- 初始化串口
- 启动内核
从写Flash到串口都是为了开发方便额外增加的功能,其他的都是实现启动内核的基本功能。
实际是u-boot可以理解成一个单片机程序,只是比较复杂而已。
结合makefile分析u-boot的功能、结构
分析文件时,想了解文件时怎么链接的和整体结构,需要用makefile
u-boot怎么编译:
- 先配置,用命令make ~~~config
- 用命令make
分析配置过程
有上图可知make 100ask24x0_config指令相当于运行图中第二句的指令
@$(MKCONFIG) $(@:_config=) arm arm920t 100ask24x0 NULL s3c24x0
在makefile中
MKCONFIG :=$(SRCTREE)/mkconfig
SRC源文件TREE树,大概理解为源文件所在的目录下面有个mkconfig
" @ " 表 示 目 标 , 就 是 100 a s k 24 x 0 c o n f i g 这 个 东 西 , “ @"表示目标,就是100ask24x0_config这个东西,“ @"表示目标,就是100ask24x0config这个东西,“(@:_config=)"相当于100ask24x0
因此,执行命令
@$(MKCONFIG) $(@:_config=) arm arm920t 100ask24x0 NULL s3c24x0
相当于执行mkconfig 100ask24x0 arm arm920t 100ask24x0 NULL s3c24x0
这个命令的含义:
mkconfig文件中有段程序如下,程序表示如果BOARD_NAME没有定义则运行BOARD_NAME="$1"
[ "${BOARD_NAME}" ] || BOARD_NAME="$1"
$1表示什么意思?
在linux脚本里面,$0表示第一个参数,后面以此类推
执行完上面的程序之后,相当于BOARD_NAME=100ask24x0
然后看下面的函数
[ $# -lt 4 ] && exit 1
[ $# -gt 6 ] && exit 1
$#表示 参数个数,-lt表示小于,-gt表示大于,因此4<=参数个数<=6时程序才正常运行,不然退出。
ln -s asm-asm asm 表示建立一个链接文件asm,该文件指向asm-asm
如何查看asm指向asm-asm,在对应的目录下面输入指令ls -l asm便可以看出来。现在的这些操作时针对运行mkconfig之后,分析里面的语句,mkconfig和makefile一样是指令集。根据里面的指令来观察make 100ask_config的运行结果。
include文件夹里面有各种格式的asm文件,如果源码里面写这么一行
#include <asm/type.h>
如果给ARM架构编译的时候arm应该为arm-arm,给i386架构编译的时候应该为arm-i386,不想不代码改来改去,直接用asm就可以了。asm是什么东西?它配置的时候临时生成,指向某个架构。
执行完下面语句(>表示创建config.mk文件,>>表示内容追加)
echo "ARCH = $2" > config.mk
echo "CPU = $3" >> config.mk
echo "BOARD = $4" >> config.mk
config.mk的内容为:
ARCH = arm
CPU = arm920t
BOARD = 100ask24x0
VENDOR = NULL即为空
SOC = s3c24x0
实验结果:
产生config.mk和config.h两个文件,书本267页有总结
分析编译过程
直接执行make命令,因此要观察makefile文件
里面有句话include $(OBJTREE)/include/config.mk
包含了刚才配置过程生成的东西,里面定义的东西主要是确定版本这些信息,编译时要用到
关键语句
OBJS = cpu/$(CPU)/start.o
LIBS = lib_generic/libgeneric.a
LIBS += board/$(BOARDDIR)/lib$(BOARD).a
LIBS += cpu/$(CPU)/lib$(CPU).a
$CPU=arm920
$(BOARDDIR)=100ask24x0
LIBS += A/A.a ,这种格式的语句在makefile里面有很多,其意思主要就是在A目录下的所有文件都打包,形成一个A.a这么一个库。
执行make的时候,如果不指定目标,那么all这个指令的目的就是想生成第一个这个目标。
all: $(ALL) ##all依赖于ALL
ALL = $(obj)u-boot.srec $(obj)u-boot.bin $(obj)System.map $(U_BOOT_NAND)
$(obj)u-boot.bin: $(obj)u-boot @@elf格式的u-boot
$(OBJCOPY) ${OBJCFLAGS} -O binary $< $@
$(obj)u-boot: depend version $(SUBDIRS) $(OBJS) $(LIBS) $(LDSCRIPT)
UNDEF_SYM=`$(OBJDUMP) -x $(LIBS) |sed -n -e 's/.*\(__u_boot_cmd_.*\)/-u\1/p'|sort|uniq`;\
cd $(LNDIR) && $(LD) $(LDFLAGS) $$UNDEF_SYM $(__OBJS) \
--start-group $(__LIBS) --end-group $(PLATFORM_LIBS) \
-Map u-boot.map -o u-boot
不用展开这些东西也能知道这是干嘛的,我们直接执行make命令,执行完在末尾我们可以看到这样的代码
UNDEF_SYM=`arm-linux-objdump -x lib_generic/libgeneric.a board/100ask24x0/lib100ask24x0.a cpu/arm920t/libarm920t.a cpu/arm920t/s3c24x0/libs3c24x0.a lib_arm/libarm.a fs/cramfs/libcramfs.a fs/fat/libfat.a fs/fdos/libfdos.a fs/jffs2/libjffs2.a fs/reiserfs/libreiserfs.a fs/ext2/libext2fs.a net/libnet.a disk/libdisk.a rtc/librtc.a dtt/libdtt.a drivers/libdrivers.a drivers/nand/libnand.a drivers/nand_legacy/libnand_legacy.a drivers/usb/libusb.a drivers/sk98lin/libsk98lin.a common/libcommon.a |sed -n -e 's/.*\(__u_boot_cmd_.*\)/-u\1/p'|sort|uniq`;\
cd /home/book/Desktop/u-boot-1.1.6 &&
arm-linux-ld -Bstatic -T /home/book/Desktop/u-boot-1.1.6/board/100ask24x0/u-boot.lds -Ttext 0x33F80000 $UNDEF_SYM cpu/arm920t/start.o \
--start-group lib_generic/libgeneric.a board/100ask24x0/lib100ask24x0.a cpu/arm920t/libarm920t.a cpu/arm920t/s3c24x0/libs3c24x0.a lib_arm/libarm.a fs/cramfs/libcramfs.a fs/fat/libfat.a fs/fdos/libfdos.a fs/jffs2/libjffs2.a fs/reiserfs/libreiserfs.a fs/ext2/libext2fs.a net/libnet.a disk/libdisk.a rtc/librtc.a dtt/libdtt.a drivers/libdrivers.a drivers/nand/libnand.a drivers/nand_legacy/libnand_legacy.a drivers/usb/libusb.a drivers/sk98lin/libsk98lin.a common/libcommon.a --end-group -L /work/tools/gcc-3.4.5-glibc-2.3.6/lib/gcc/arm-linux/3.4.5 -lgcc \
-Map u-boot.map -o u-boot
链接文件依赖于谁呢?
一个是链接脚本-T /home/book/Desktop/u-boot-1.1.6/board/100ask24x0/u-boot.lds -Ttext 0x33F80000 $UNDEF_SYM cpu/arm920t/start.o \
一个是后面成堆的材料,这些原材料是怎么组织成u-boot的?谁放在去前面?此时就要看链接脚本
u-boot.lds的内容如下所示:
SECTIONS
{
. = 0x00000000; //因为上面的提示,下面的内容将会从0x33F80000开始排放
. = ALIGN(4);
.text :
{
cpu/arm920t/start.o (.text)
board/100ask24x0/boot_init.o (.text)
*(.text)
}
. = ALIGN(4);
.rodata : { *(.rodata) }
. = ALIGN(4);
.data : { *(.data) }
. = ALIGN(4);
.got : { *(.got) }
. = .;
__u_boot_cmd_start = .;
.u_boot_cmd : { *(.u_boot_cmd) }
__u_boot_cmd_end = .;
. = ALIGN(4);
__bss_start = .;
.bss : { *(.bss) }
_end = .;
}
0x33F80000下面首先会排放cpu/arm920t/start.o、board/100ask24x0/boot_init.o还有其他所有文件的代码段
接下来是所有文件的只读数据段、所有文件的数据段还有u-boot自己定义的段.u_boot_cmd
分析u-boot就从cpu/arm920t/start.o这个文件开始入手
分析makefile所得:
- 第一个文件时CPU/ARM920t/start.s
- 链接地址怎么定的,是在board/100ask24x0/u-boot.lds这个链接脚本还有+0x33F80000,0x33F80000怎么来的?通过搜索指令grep “33FF8000" * -nR来查找
分析u-boot源码
回顾一下以前的实验
- 初始化(关看门狗、初始化时钟、初始化SDRAM)
- 程序很大,要把程序从NandFlash转移到SDRAM里面来
- 设置sp
u-boot只不过是一个比较复杂的单片机程序
硬件部分有一开始讲到的
关看门狗、初始化时钟和SDRAM、设置栈、还有网卡,USB和串口功能
设置栈是为了能够调用C函数,通过C函数来从Flash中读出内核和启动内核
现在开始分析第一个文件CPU/ARM920t/start.s,其代码内容多,根据重要程度来进行复制。
-
进入reset的内容
- 把CPU设为SVC32模式(管理模式)
- 关看门狗
- 屏蔽所有中断
- CPU的循环初始化
- 设置栈(栈的分配如下所示,u-boot代码放最顶层,其他各有各的用途)
- 初始化时钟
- relocate:把代码从Flash里面读到SDRAM的链接地址那里去,即这个程序运行时应该处于的链接地址.
- 清bss段,bss段指的是初始值为0或者没有定义值的静态变量和全局变量。
- start_armboot(这是一个C函数)
以上为硬件相关的初始化,我们称其为第一阶段,第二阶段从这个start_armboot开始。
第二阶段所涉及的功能有:开发功能里面的烧写Flash,设置网卡,USB,串口,从Flash读出内核,启动内核,都是在第二阶段实现的,一般都是用C语言写的。
接下来来讨论如何将代码从Flash里面拷到SDRAM
NOR Flash的访问可以直接用地址寻址,因此从NOR Flash拷到SDRAM就很简单了。
如果是NandFlash,就去看之前的实验。
如何判断是NORFlash还是NANDFlash
如果是NORFLAH启动的话,0地址就对应于NORFlash,NORFlash不能像内存一样去写。如果从Nand启动的话,0地址就对应于片内RAM,0地址是可以写的。
因此,把往0地址写值,然后再读出来,读出来的值和写进去的值一样则说明Nand启动,否则是NOR启动。
第二阶段代码讲解
第二段代码的讲解主要是对start.s文件代码的分析。
u-boot的唯一目标就是启动内核,要做的事情就是从Flash读出内核并启动它。
我们抱着这个目标去看源代码,其他省略掉。
看一下u-boot的内存分布,如下图所示:
怎么读出内核?
如果是NORFlash读内存很简单,但是要写就要识别这是哪种NorFlash。由于u-boot是单片机程序,因此那些malloc和free都得自己实现,用于malloc分配的内存堆对应于上图192k的那块区域,在这块空间上实现了堆的分配和释放。
调用完Flash_init()和nand_init()这两个函数之后,这个程序就有能力去写和读Flash。
有个函数env_relocate为环境变量的初始化,环境变量是什么?
进入u-boot的命令界面(在菜单界面输入q便可进入),输入print,打印出来的变量就是环境变量,等号后面的东西就是他们的值。
环境变量用来干什么?
比如说里面有一个环境变量时波特率,因此我们可以通过修改这个环境变量来改变串口的波特率。
环境变量从哪里来的?
- 代码写死的,就是默认的。
- Flash上保存的。
u-boot启动之后会现在Flash上看一下有没有环境变量,如果有的话就用Flash上的环境变量,如果没有就用默认的环境变量。
阶段汇总:
现在在start_armboot文件中通过Flash_init和nand_init使函数具有一定的Flash读写能力。然后进入main_loop主循环。而这个主循环的实际效果就是我们之前那个等待我们输数据进去的界面。
我们想知道怎么启动,如何启动内核,有个函数很重要。
s=getenv("bootcmd")
boot启动,cmd命令,启动命令
s = getenv ("bootcmd");
debug ("### main_loop: bootcmd=\"%s\"\n", s ? s : "<UNDEFINED>");
if (bootdelay >= 0 && s && !abortboot (bootdelay)) {
# ifdef CONFIG_AUTOBOOT_KEYED
int prev = disable_ctrlc(1); /* disable Control C checking */
# endif
# ifndef CFG_HUSH_PARSER
{
printf("Booting Linux ...\n");
run_command (s, 0);
}
# else
parse_string_outer(s, FLAG_PARSE_SEMICOLON |
FLAG_EXIT_FROM_LOOP);
# endif
if (bootdelay >= 0 && s && !abortboot (bootdelay)),如果倒数计时期间没有按下空格键的话,会跳到后面的Booting Linux, run_command (s, 0);运行命令,这个命令就是从前面的s=getenv(“bootcmd”)来的。因此怎么从Flash上读取内核,启动内核,getenv(“bootcmd”)函数很重要。
怎么读出内核?
nand read.jffs2 0x30007Fc0 kernel
解读:从kernel读到0x30007FC0,kernel是一个分区。nand read.jffs2是指令?
怎么启动?
bootm 0x3007FC0
内核的启动依赖于bootcmd这个环境变量,在查询环境变量中
bootcmd=nand read.jffs2 0x30007Fc0 kernel ; bootm 0x3007FC0
如果在倒计时时按入空格,就会运行run_command (“menu”, 0);并进入菜单,输入q就会进入另外一个死循环,此时便是等待我们输入东西的界面
阶段总结:
启动内核:
s = getenv ("bootcmd");
run_command (s, 0);
在u-boot控制界面
- readline读入串口的数据
- run_command
因此,只有分析run_command才能分析内核的启动流程。
u-boot命令的实现
在u-boot命令界面输入命令,便有对应的东西出来,那么命令怎么实现?
根据命令找到对应的函数,然后再执行函数,因此会有一个命令的结构体,是的命令能够对应不同的函数。
run_command函数的内容如下所示:
int run_command (const char *cmd, int flag)
{
cmd_tbl_t *cmdtp;
char cmdbuf[CFG_CBSIZE]; /* working copy of cmd */
char *token; /* start of token in cmdbuf */
char *sep; /* end of token (separator) in cmdbuf */
char finaltoken[CFG_CBSIZE];
char *str = cmdbuf;
char *argv[CFG_MAXARGS + 1]; /* NULL terminated */
int argc, inquotes;
int repeatable = 1;
int rc = 0;
#ifdef DEBUG_PARSER
printf ("[RUN_COMMAND] cmd[%p]=\"", cmd);
puts (cmd ? cmd : "NULL"); /* use puts - string may be loooong */
puts ("\"\n");
#endif
clear_ctrlc(); /* forget any previous Control C */
if (!cmd || !*cmd) {
return -1; /* empty command */
}
if (strlen(cmd) >= CFG_CBSIZE) {
puts ("## Command too long!\n");
return -1;
}
strcpy (cmdbuf, cmd);
/* Process separators and check for invalid
* repeatable commands
*/
#ifdef DEBUG_PARSER
printf ("[PROCESS_SEPARATORS] %s\n", cmd);
#endif
while (*str) {
/*
* Find separator, or string end
* Allow simple escape of ';' by writing "\;"
*/
for (inquotes = 0, sep = str; *sep; sep++) {
if ((*sep=='\'') &&
(*(sep-1) != '\\'))
inquotes=!inquotes;
if (!inquotes &&
(*sep == ';') && /* separator */
( sep != str) && /* past string start */
(*(sep-1) != '\\')) /* and NOT escaped */
break;
}
/*
* Limit the token to data between separators
*/
token = str;
if (*sep) {
str = sep + 1; /* start of command for next pass */
*sep = '\0';
}
else
str = sep; /* no more commands for next pass */
#ifdef DEBUG_PARSER
printf ("token: \"%s\"\n", token);
#endif
/* find macros in this token and replace them */
process_macros (token, finaltoken);
/* Extract arguments */
if ((argc = parse_line (finaltoken, argv)) == 0) {
rc = -1; /* no command at all */
continue;
}
/* Look up command in command table */
if ((cmdtp = find_cmd(argv[0])) == NULL) {
printf ("Unknown command '%s' - try 'help'\n", argv[0]);
rc = -1; /* give up after bad command */
continue;
}
/* found - check max args */
if (argc > cmdtp->maxargs) {
printf ("Usage:\n%s\n", cmdtp->usage);
rc = -1;
continue;
}
#if (CONFIG_COMMANDS & CFG_CMD_BOOTD)
/* avoid "bootd" recursion */
if (cmdtp->cmd == do_bootd) {
#ifdef DEBUG_PARSER
printf ("[%s]\n", finaltoken);
#endif
if (flag & CMD_FLAG_BOOTD) {
puts ("'bootd' recursion detected\n");
rc = -1;
continue;
} else {
flag |= CMD_FLAG_BOOTD;
}
}
#endif /* CFG_CMD_BOOTD */
/* OK - call function to do the command */
if ((cmdtp->cmd) (cmdtp, flag, argc, argv) != 0) {
rc = -1;
}
repeatable &= cmdtp->repeatable;
/* Did the user stop this? */
if (had_ctrlc ())
return 0; /* if stopped then not repeatable */
}
return rc ? rc : repeatable;
}
下面这段代码的目的是可以通过符号;来分隔两个不同的命令
if ((*sep=='\'') &&
(*(sep-1) != '\\'))
inquotes=!inquotes;
if (!inquotes &&
(*sep == ';') && /* separator */
( sep != str) && /* past string start */
(*(sep-1) != '\\')) /* and NOT escaped */
parse_line函数的作用是解析这些文字,比如说输入“md.w 0”,这个字符串要解析一下,怎么解析?就是提取参数并赋给字符串数组。
argv[0]=“md.w” argv[1]=“0”
argv[0]放的是命令,0是参数
if ((cmdtp = find_cmd(argv[0])) == NULL) {
printf ("Unknown command '%s' - try 'help'\n", argv[0]);
rc = -1; /* give up after bad command */
continue;
}
find_cmd(argv[0])为查找命令函数,cmdtp为一个结构体,其结构体申明如下所示:
struct cmd_tbl_s {
char *name; /* Command Name */
int maxargs; /* maximum number of arguments 最大参数*/
int repeatable; /* autorepeat allowed? 是否可重复 */
/* Implementation function */
int (*cmd)(struct cmd_tbl_s *, int, int, char *[]);
char *usage; /* Usage message (short) */
#ifdef CFG_LONGHELP
char *help; /* Help message (long) */
#endif
#ifdef CONFIG_AUTO_COMPLETE
/* do auto completion on the arguments */
int (*complete)(int argc, char *argv[], char last_char, int maxv, char *cmdv[]);
#endif
};
是否可重复的意思是输入命令之后,直接回车就可以执行刚才的命令
(*cmd)(struct cmd_tbl_s *, int, int, char *[])是函数指针
usage短的帮助信息,help为长的帮助信息。直接输入help出来的命令信息就是短的帮助。help+具体命令就为长的帮助信息。
cmd_tbl_t *find_cmd (const char *cmd)
{
cmd_tbl_t *cmdtp;
cmd_tbl_t *cmdtp_temp = &__u_boot_cmd_start; /*Init value */
const char *p;
int len;
int n_found = 0;
/*
* Some commands allow length modifiers (like "cp.b");
* compare command name only until first dot.
*/
len = ((p = strchr(cmd, '.')) == NULL) ? strlen (cmd) : (p - cmd);
for (cmdtp = &__u_boot_cmd_start;
cmdtp != &__u_boot_cmd_end;
cmdtp++) {
if (strncmp (cmd, cmdtp->name, len) == 0) {
if (len == strlen (cmdtp->name))
return cmdtp; /* full match */
cmdtp_temp = cmdtp; /* abbreviated command ? */
n_found++;
}
}
if (n_found == 1) { /* exactly one match */
return cmdtp_temp;
}
return NULL; /* not found or ambiguous command */
}
__ u_boot_cmd_start和__u_boot_cmd_end是在链接脚本里面定义的,该函数说明如果名字匹配成功就返回结构体,不成功则指向下一个。
现在来看一下start和end之间放的是什么东西。
一开始
变量代入之后
下面那句话的意思是定义 _u_boot_vmd_bootm这个结构体,这个结构体有个属性,强制将section转换为“.u_boot_cmd” ,跟链接文件中start和end之间的东西对应起来。里面的内容如图中最后一行的括号的内容。
代码里面所有用U_boot_CMD这个宏定义起来的东西最终都相当于定义这么一个结构体,这个结构体特别在它有个属性,强制地将段指定为.u_boot.cmd。所有的命令都会集中到链接文件中start和end中间那里来。
我们现在不急这分析内核怎么启动,我们现在增加一个hello命令打印“hello world”
建立一个新文件Hello_cmd.c,并将头文件复制下去,文件放在common下面,仿照别人怎么写。
首先写打印的函数
将U_BOOT_CMD的内容写到文件里
修改打印函数来打印参数
代码写完,怎么放到u_boot里面编译?
- 把代码放到Ubuntu中common文件的目录之下
- 修改common目录下面的makefile文件vi .common/Makefile,在里面加Hello_cmd.o
- 用make编译便可
通过输入Hello asd qwe还能将参数赋给hello函数的形参
u-boot启动内核
启动内核依赖的两条代码
s = getenv ("bootcmd"); //获取环境变量
run_command (s, 0); //运行命令
bootcmd=nand read.jffs2 0x30007Fc0 kernel ; bootm 0x3007FC0
//s就是上述的两条命令
s里面两条指令的意思就是在NandFlash上将内核读到0x30007FC0这个地址上去。第二条命令就是在这个地址上面启动内核。
现在来分析这两个命令
nand read.jffs2 0x30007Fc0 kernel
内核读取的地方是kernel分区
分区
在PC机上每个硬盘前都会有一个分区表,对于嵌入式linux来时Flash上面内有分区表,我们需要脑补分区。因为没有分区表,所以只能通过在源码写死来体现分区的概念。因此我们关心的不是分区的名字,而是分区的地址。代码写死的地方是在include\configs文件夹里面的100ask24x0文件
#define MTDPARTS_DEFAULT "mtdparts=nandflash0:256k@0(bootloader)," \
"128k(params)," \
"2m(kernel)," \
"-(root)"
该宏定义了mtd分区,该分区位于nandflash上面,前面的256k从0开始是bootloarder,接下来128k是params,用来存放环境变量的。接下来2M是kernel,剩下的东西是root分区。
u-boot对这些分区的东西,名字不重要,重要的是它的起始地址和大小。脑补好后要在代码里面写死它。
在u-boot命令行输入mtd即可查看代码分区,分区表示方法是地址大小和起始地址。
kernel分区
kernel分区的起始地址:600000,大小为2M=0x00200000
因此nand read.jffs2 0x30007Fc0 kernel这句话可以理解为
nand read.jffs2 0x30007Fc0 0x00600000 0x00200000
表示在0x00600000读取内核,读取的大小为2M。
因为命令bootm对应的函数为do_bootm,因此应该有一种约定俗称的习惯,这个习惯就是函数名称为do_+指令名
在 命令行输入nand read便可看这个命令后面应当加的参数
.jffs2的来由:这是个文件格式的东西,但是这里跟文件格式关系不大,这个用这个命令的时候,这个2M的长度不需要你页对齐,你可以随便写。如果用其他后缀命令的话是需要某些页对齐或者块对齐的。
分析bootm命令
u-boot启动内核是在Flash读,Flash上存的内核的格式:uImage
其格式为:头部+真正的内核
头部的结构体代码如下所示:
typedef struct image_header {
uint32_t ih_magic; /* Image Header Magic Number */
uint32_t ih_hcrc; /* Image Header CRC Checksum */
uint32_t ih_time; /* Image Creation Timestamp */
uint32_t ih_size; /* Image Data Size */
uint32_t ih_load; /* Data Load Address */
uint32_t ih_ep; /* Entry Point Address */
uint32_t ih_dcrc; /* Image Data CRC Checksum */
uint8_t ih_os; /* Operating System */
uint8_t ih_arch; /* CPU architecture */
uint8_t ih_type; /* Image Type */
uint8_t ih_comp; /* Compression Type */
uint8_t ih_name[IH_NMLEN]; /* Image Name */
} image_header_t;
关心其中的两个参数ih_load和ih_ep,load表示加载地址,表示内核运行时先把它放在那里,ep表示入口地址,要运行内核的时候直接跳到这个地址就可以了。
从kernel读出来的内核数据具体要放那里呢?上面是将其放在0x30007FC0,实际上我们是可以随便放的,只要地址不破坏下面图示的内存使用情况就可以了。SDRAM总共64M,而图示的已经分配的部分加起来都不到1M。
为什么可以随便放?
因为uImage里面有个头部,头部里面有加载地址和入口地址。nand read.jffs2 0x30007Fc0 kernel指令已经将uImage格式的内核代码拷进0x30007FC0,用bootm 0x30007FC0时便先读uImage文件的头部,知道它的加载地址和入口地址,如果发现真正的内核并不在于它的加载地址的话就要把真正的内核移到加载地址里面去。最后跳到入口地址去执行。
in_load的值为0x30008000,因此内核运行时应该放到这里来,现在读到0x30007FC0,为什么是0x30007FC0呢?因为0x30007FC0+64=0x30008000,因此这样写的目的就是不用去移动数据,加快启动速度。
总结bootm的作用:
- 根据头部移动内核到合适的地方。
- 启动用do_bootm_linux(PC机启动时BIOS会去检测内存和Flash并告诉内核),因为linux没有同样的东西,因此do_bootm_linux要做如下事情:
- u_boot告诉内核一些启动参数(设置启动参数)
- 跳到入口地址,启动内核。
void (*theKernel)(int zero, int arch, uint params);
//theKernel为函数指针
theKernel = (void (*)(int, int, uint))ntohl(hdr->ih_ep); //theKernel为头部的入口地址
theKernel (0, bd->bi_arch_number, bd->bi_boot_params);
//执行完这句就跳到入口地址去执行
怎么设置启动参数
u-boot把内核读进来之后要去启动它,启动它就要跳到内核里面去,然后u-boot就不存在了,那么他们之间怎么交互数据?
方法:在某个地址按某种格式保存数据,当内核启动的时候,就去改地址把参数数据读取出来,按照双方约定好的格式解析出来。这个格式称为TAG,这个地址对于开发板来说是0x30000100
setup_start_tag (bd);
setup_memory_tags (bd);
setup_commandline_tag (bd, commandline);
setup_end_tag (bd);
tag是一个结构体
struct tag {
struct tag_header hdr;
union {
struct tag_core core;
struct tag_mem32 mem;
struct tag_videotext videotext;
struct tag_ramdisk ramdisk;
struct tag_initrd initrd;
struct tag_serialnr serialnr;
struct tag_revision revision;
struct tag_videolfb videolfb;
struct tag_cmdline cmdline;
struct tag_acorn acorn;
struct tag_memclk memclk;
} u;
};
static void setup_start_tag (bd_t *bd)
{
params = (struct tag *) bd->bi_boot_params; //bi_boot_params=30000100,因此参数放在这个地址
params->hdr.tag = ATAG_CORE;
params->hdr.size = tag_size (tag_core);/tag_core也是一个结构体,里面的成员有flag/pagesize/rootdev,下面给这三个参数赋值
params->u.core.flags = 0;
params->u.core.pagesize = 0;
params->u.core.rootdev = 0;
params = tag_next (params);
}
setup_start_tag (bd);执行完,其内部数据分布如下图所示,tag结构体的tag_header定义的结构体hdr结构体有两个参数,分别为size和tag。hdr.tag = ATAG_CORE(0x54410001),hdr.size = tag_size (tag_core)
#define tag_size(type) ((sizeof(struct tag_header) + sizeof(struct type)) >> 2)
从这里可以看出,tag_size这个函数就是将tag_header结构体的字节大小和形参的结构体字节大小相加除以4,tag_header结构体内容为u32 size;u32 tag;,故为8个字节,程序中输入的形参结构体tag_core的内容为flag/pagesize/rootdev,都为32位,因此共有12个字节,(12+8)/4=5,因此hdr.siz的值为5.
tag_next (params);用于指向下一个参数。
下面来分析setup_memory_tags (bd);PC机在启动的时候BIOS会检测出内存出来并告诉windous系统,同样的,u-boot也应该能做到同样的东西。
static void setup_memory_tags (bd_t *bd)
{
int i;
for (i = 0; i < CONFIG_NR_DRAM_BANKS; i++) {
params->hdr.tag = ATAG_MEM;
params->hdr.size = tag_size (tag_mem32);
params->u.mem.start = bd->bi_dram[i].start;
params->u.mem.size = bd->bi_dram[i].size;
params = tag_next (params);
}
}
一样的,一开始头部那里是size,然后是tag(0x54410002),mem.size=64M,mem.start=0x30000000(这些都是之前设置好的了,其函数如下,对于linux来说,内存大小这样写就好了,没必要像PC机那么复杂)
运行完内存分布如下:
setup_commandline_tag (bd, commandline);其具体内容如下所示:
static void setup_commandline_tag (bd_t *bd, char *commandline)
{
char *p;
if (!commandline)
return;
/* 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);
}
commandline来源于函数 char *commandline = getenv (“bootargs”);也就是来源于环境变量bootargs,可以用linux命令界面输入print可得其具体指令。
指令运行完后内存分布如上所示。
最后一项,setup_end_tag (bd);其内容如下:
static void setup_end_tag (bd_t *bd)
{
params->hdr.tag = ATAG_NONE;
params->hdr.size = 0;
}
所有的东西保存好之后,启动内核,内核就会到这个地址来,从0x30000100来读取参数
u-boot的终极目标是启动内核
- 从Flash读出内核
- 启动内核
- 设参数(参数是用来告诉内核内存有多大,命令行参数有什么。
- 跳到入口去执行。
theKernel (0, bd->bi_arch_number, bd->bi_boot_params);
启动的时候带了三个参数,就是上面三个,bd->bi_arch_number是个什么东西?这个称为机器ID, gd->bd->bi_arch_number = MACH_TYPE_S3C2440;内核启动的时候可以支持很多种单板,能不能支持我们用的这个单板呢?由我们这个及其ID来进行比较得出结果。u-boot作为上电运行的第一个程序,有责任去确定这个单板的机器ID是多少,内核启动之后就会去比对这个机器ID,看能不能支持这个单板。因此这里要给内核传递一个机器ID。
运行完这一条语句后,控制权就由u-boot转移到内核,也就没有u-boot的什么事了。
这章u-boot的内容弄得有点繁杂错乱,不过中心有挺简单的,有必要在后续做一个u-boot内核启动流程。