本篇博客的uboot是基于u-boot-1.16,平台基于s3c2440。
uboot的最终功能是启动内核,所以需要读出内核,从哪里读,读到哪里去?从flash上读到sdram,所以需要初始化sdram。sdram等硬件的工作是离不开时钟,所以需要初始化时钟,由于整个启动过程是不允许被中断(除了认为切断电源),所以需要关闭看门狗、屏蔽中断。为了让我们的开发更加方便,我们需要加入其他的功能,比如我们需要把内核烧写到flash上去,而内核二进制文件从哪里来的呢?所以我们需要初始化网卡或者USB。比如为输出打印信息,我们需要初始化串口
先来深入分析一下配置过程
输入的命令是make 100ask24x0_config,我们在Makefile中找到与100ask24x0_config相关的如下内容
100ask24x0_config : unconfig
@$(MKCONFIG) $(@:_config=) arm arm920t 100ask24x0 NULL s3c24x0
在Makefile中查到MKCONFIG的定义如下
MKCONFIG := $(SRCTREE)/mkconfig
export MKCONFIG
那 $(@:_config=)这个是什么意思呢? @代表着目标文件,所以意思就是要把目标100ask24x0_config中的config替换为空(因为等号后面是空)
所以make 100ask24x0_config命令实际执行的是 mkconfig 100ask24x0 arm arm920t 100ask24x0 NULL s3c24x0
mkconfig代码如下
APPEND=no # Default: Create new config file
BOARD_NAME="" # Name to print in make output
#mkconfig 100ask24x0 arm arm920t 100ask24x0 NULL s3c24x0
# $0 $1 $2 $3 $4 $5 $6
while [ $# -gt 0 ] ; do $#当前程式的参数的个数,gt是greater的缩写,所以就是说如果当前的参数的个数大于0就执行如下
case "$1" in #通过第一个参数来判断,我们的任何一个参数中都没有这个,所以如下都可以忽略掉
--) shift ; break ;;
-a) shift ; APPEND=yes ;;
-n) shift ; BOARD_NAME="${1%%_config}" ; shift ;;
*) break ;;
esac
done
#当方括号是条件测试,作用等同于test。||运算和C语言中类似
#如果BOARD_NAME定义就执行前者,如果没有定义就执行BOARD_NAME = "$1"
#在上文中,BOADR_NAME为空,没有定义,BOARD_NAME = 100ask24x0
[ "${BOARD_NAME}" ] || BOARD_NAME="$1"
#参数个数的比较
[ $# -lt 4 ] && exit 1
[ $# -gt 6 ] && exit 1
echo "Configuring for ${BOARD_NAME} board..."
#打印 Configuring for 100ask24x0 board...(经测试,确实有打印)
#
# Create link to architecture specific headers
# $SRCTREE和$OBJTREE的定义都在Makefile中
#OBJTREE := $(if $(BUILD_DIR),$(BUILD_DIR),$(CURDIR))
#SRCTREE := $(CURDIR)
#OBJTREE的定义是如果变量BUILD_DIR被定义了,则BUILD_DIR,否则就是CURDIR,我们可看到在Makefile中B没有被定义
#所以这个测试条件不成立,执行else语句
if [ "$SRCTREE" != "$OBJTREE" ] ; then
mkdir -p ${OBJTREE}/include
mkdir -p ${OBJTREE}/include2
cd ${OBJTREE}/include2
rm -f asm
ln -s ${SRCTREE}/include/asm-$2 asm
LNPREFIX="../../include2/asm/"
cd ../include
rm -rf asm-$2
rm -f asm
mkdir asm-$2
ln -s asm-$2 asm
#进入当前目前目录的下一层目录include
#删除掉
#建立一软链接(-s soft), ln -s 源文件 目标文件(链接文件)
#就相当于首先会删除名为asm的文件,然后再创建一个链接到asm-arm的链接文件(asm)
#include目录下确实有一个asm链接到asm-arm的文件
#这是由原因的,在源码中我们经常会用到asm,但是不同的架构的asm是不一样的,用这种替换和链接的方式可以让源码没有必要去修改
else
cd ./include
rm -f asm
ln -s asm-$2 asm
fi
rm -f asm-$2/arch
if [ -z "$6" -o "$6" = "NULL" ] ; then
ln -s ${LNPREFIX}arch-$3 asm-$2/arch
else
#没有找到LNPREFIX的定义是怎么回事啊
#ln -s arch-100ask24x0(源文件)asm-arm/arch(链接文件)
# 验证asm-arm/arch确实指向arch-100ask24x0
ln -s ${LNPREFIX}arch-$6 asm-$2/arch
fi
#和上面非常相似
if [ "$2" = "arm" ] ; then
rm -f asm-$2/proc
ln -s ${LNPREFIX}proc-armv asm-$2/proc
fi
#
# Create include file for Make
#
echo "ARCH = $2" > config.mk
echo "CPU = $3" >> config.mk
echo "BOARD = $4" >> config.mk
[ "$5" ] && [ "$5" != "NULL" ] && echo "VENDOR = $5" >> config.mk
[ "$6" ] && [ "$6" != "NULL" ] && echo "SOC = $6" >> config.mk
#include目录下的config.mk文件确实包含了上述的内容
#
# Create board specific header file
#
if [ "$APPEND" = "yes" ] # Append to existing config file
then
echo >> config.h
else
> config.h # Create new config file
#
fi
echo "/* Automatically generated - do not edit */" >>config.h
echo "#include <configs/$1.h>" >>config.h
#config.h文件确实包含了上述的内容
exit 0
接下来分析编译过程,分析Makefile
......
include $(OBJTREE)/include/config.mk #配置时生成的文件,需要在makefile中取出信息
......
ifeq ($(ARCH),arm)
CROSS_COMPILE = arm-linux- #选择交叉编译器
......
all: $(ALL)
$(obj)u-boot.hex: $(obj)u-boot
$(OBJCOPY) ${OBJCFLAGS} -O ihex $< $@
$(obj)u-boot.srec: $(obj)u-boot
$(OBJCOPY) ${OBJCFLAGS} -O srec $< $@
$(obj)u-boot.bin: $(obj)u-boot
$(OBJCOPY) ${OBJCFLAGS} -O binary $< $@
$(obj)u-boot.img: $(obj)u-boot.bin
./tools/mkimage -A $(ARCH) -T firmware -C none \
-a $(TEXT_BASE) -e 0 \
-n $(shell sed -n -e 's/.*U_BOOT_VERSION//p' $(VERSION_FILE) | \
sed -e 's/"[ ]*$$/ for $(BOARD) board"/') \
-d $< $@
$(obj)u-boot.dis: $(obj)u-boot
$(OBJDUMP) -d $< > $@
$(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
如下是编译后生成的结果(部分)
cd /home/book/u-boot-1.1.6 && arm-linux-ld -Bstatic -T /home/book/u-boot-1.1.6/board/100ask24x0/u-boot.lds -Ttext 0x33F80000 $UNDEF_SYM cpu/arm920t/start.o \
#注释:cd $(LNDIR) && $(LD) $(LDFLAGS) $$UNDEF_SYM $(__OBJS)
--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 \
#注释 --start-group $(__LIBS) --end-group $(PLATFORM_LIBS)
-Map u-boot.map -o u-boot
arm-linux-objcopy --gap-fill=0xff -O srec u-boot u-boot.srec
arm-linux-objcopy --gap-fill=0xff -O binary u-boot u-boot.bin
所以现在我们需要到链接脚本上看下,这些库文件是基于什么样的规则被链接起来的
24 OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm")
25 /*OUTPUT_FORMAT("elf32-arm", "elf32-arm", "elf32-arm")*/
26 OUTPUT_ARCH(arm)
27 ENTRY(_start) #入口是 _start
28 SECTIONS
29 {
30 . = 0x00000000;
31 #起始地址是 0x33F80000 + = 0x00000000
32 . = ALIGN(4);
33 .text :
34 {
35 cpu/arm920t/start.o (.text)
36 board/100ask24x0/boot_init.o (.text)
37 *(.text)
38 }
39
40 . = ALIGN(4);
41 .rodata : { *(.rodata) }
42
43 . = ALIGN(4);
44 .data : { *(.data) }
45
46 . = ALIGN(4);
47 .got : { *(.got) }
48
49 . = .;
50 __u_boot_cmd_start = .;
51 .u_boot_cmd : { *(.u_boot_cmd) }
52 __u_boot_cmd_end = .;
53
54 . = ALIGN(4);
55 __bss_start = .;
56 .bss : { *(.bss) }
57 _end = .;
58 }
可以从链接脚本中看到,文件链接是顺序和通常情况没有什么区别
需要指出的是-TEXT_BASE 代码段的基地址是可以修改的,这里我们指定0x33F80000,我们的bank是64MB空间
memory bank的end address为0X33FFFFFF,所以0x33F80000到end address之间有512kb的空间,这个空间就是给u-boot的空间
现在我们来看下uboot的启动过程
之前已经了解到了,uboot启动的入口是start.S文件
启动过程为:关闭看门狗,屏蔽所有的中断,初始化时钟,初始化SDRAM,如果程序比较大,还需要将程序从nand flash中拷贝到sdram中来。由于我们需要运行c程序来引导加载内核,而c程序中的局部变量是存放在栈中,所以我们必须要设置栈,为栈预留空间。设置栈的操作就是将sp寄存器(栈顶指针)指向内存中的合适的位置。
先看下start.S中和启动有关的内容
reset:
/*
* set the cpu to SVC32 mode #设置为管理者模式
*/
mrs r0,cpsr
bic r0,r0,#0x1f
orr r0,r0,#0xd3
msr cpsr,r0
/* turn off the watchdog */
#if defined(CONFIG_S3C2400)
# define pWTCON 0x15300000
# define INTMSK 0x14400008 /* Interupt-Controller base addresses */
# define CLKDIVN 0x14800014 /* clock divisor register */
#elif defined(CONFIG_S3C2410)
# define pWTCON 0x53000000
# define INTMOD 0X4A000004
# define INTMSK 0x4A000008 /* Interupt-Controller base addresses */
# define INTSUBMSK 0x4A00001C
# define CLKDIVN 0x4C000014 /* clock divisor register */
#endif
#if defined(CONFIG_S3C2400) || defined(CONFIG_S3C2410)
ldr r0, =pWTCON
mov r1, #0x0
str r1, [r0]
/*
* mask all IRQs by setting all bits in the INTMR - default
*/
mov r1, #0xffffffff
ldr r0, =INTMSK #将中断控制器的INTMSK寄存器总所有位都设置为1,使中断控制器屏蔽一切中断
str r1, [r0]
# if defined(CONFIG_S3C2410)
ldr r1, =0x3ff
ldr r0, =INTSUBMSK #在2410中还有INTSUBMSK,也需要屏蔽(我们之前在触摸屏中有涉及到这个寄存器)
str r1, [r0]
# endif
#if 0
/* FCLK:HCLK:PCLK = 1:2:4 */
/* default FCLK is 120 MHz ! */
ldr r0, =CLKDIVN
mov r1, #3
str r1, [r0]
#endif
#endif /* CONFIG_S3C2400 || CONFIG_S3C2410 */
#ifndef CONFIG_SKIP_LOWLEVEL_INIT
#if 0
/* 这些代码会使用SP,在NAND启动时会破坏片内内存的部分代码
* 导致NAND启动时无法使用休眠-唤醒功能
*/
/* 设置SP指向片内内存 */
ldr sp, =4092
ldr r0, =0x12345678
str r0, [sp]
ldr r1, [sp]
cmp r0, r1
ldrne sp, =0x40000000+4096
bl clock_init
#else
/* 设置时钟, 使用汇编 */
#define S3C2440_MPLL_400MHZ ((0x5c<<12)|(0x01<<4)|(0x01))
#define S3C2440_UPLL_48MHZ ((0x38<<12)|(0x02<<4)|(0x02))
#define S3C2440_CLKDIV (0x05) // | (1<<3)) /* FCLK:HCLK:PCLK = 1:4:8, UCLK = UPLL/2 */
ldr r1, =CLKDIVN
mov r2, #S3C2440_CLKDIV
str r2, [r1]
mrc p15, 0, r1, c1, c0, 0 // read ctrl register
orr r1, r1, #0xc0000000 // Asynchronous
mcr p15, 0, r1, c1, c0, 0 // write ctrl register
ldr r0,=LOCKTIME
ldr r1,=0xffffff
str r1,[r0]
// delay
mov r0, #0x200
1: subs r0, r0, #1
bne 1b
// Configure MPLL
ldr r0,=MPLLCON
ldr r1,=S3C2440_MPLL_400MHZ
str r1,[r0]
// delay
mov r0, #0x200
1: subs r0, r0, #1
bne 1b
//Configure UPLL
ldr r0, =UPLLCON
ldr r1, =S3C2440_UPLL_48MHZ
str r1, [r0]
// delay
mov r0, #0x200
1: subs r0, r0, #1
bne 1b
#endif
#endif
/* 2. 根据 GSTATUS2[1]判断是复位还是唤醒 */
ldr r0, =GSTATUS2
ldr r1, [r0]
tst r1, #(1<<1) /* r1 & (1<<1) */
bne wake_up
/*
* we do sys-critical inits only at reboot,
* not when booting from ram!
*/
#ifndef CONFIG_SKIP_LOWLEVEL_INIT
adr r0, _start /* r0 <- current position of code */
ldr r1, _TEXT_BASE /* test if we run from flash or RAM */
cmp r0, r1 /* don't reloc during debug */
blne cpu_init_crit /*start代表着current position of code, 如果run from sram那说明_start的位置为0*/
#endif /*需要cpu的初始化,如果run from flash,则不用cpu初始化*/
/*run from flash也就是之前所说的nand 启动,启动地址等于_start(makefile中指定的地址);run from ram, 就是nor 启动,nor前4k代码汇编整个代码重定位到内存中,然而cpu所看的地址依然是0地址,所以启动地址为0.*/
/* Set up the stack */
stack_setup:
ldr r0, _TEXT_BASE /* upper 128 KiB: relocated uboot */
sub r0, r0, #CFG_MALLOC_LEN /* malloc area */
sub r0, r0, #CFG_GBL_DATA_SIZE /* bdinfo */
#ifdef CONFIG_USE_IRQ
sub r0, r0, #(CONFIG_STACKSIZE_IRQ+CONFIG_STACKSIZE_FIQ)
#endif
sub sp, r0, #12 /* leave 3 words for abort-stack */
/*
* 首先将_TEXT_BASE的值赋给r0,从这个地址以下的区域,分别为
* | | _TEXT_BASE初始值
* | | CFG_MALLOC_LEN 这个应该是堆
* | | CFG_GBL_DATA_SIZE 全局变量区
* | |CONFIG_STACKSIZE_IRQ
* | | CONFIG_STACKSIZE_FIQ
* | | 栈顶指针sp(预留三个字节)
*/
/*把代码从flash中读到sdram中去*/
#ifndef CONFIG_SKIP_RELOCATE_UBOOT
relocate: /* relocate U-Boot to RAM */
adr r0, _start /* r0 <- current position of code */
ldr r1, _TEXT_BASE /* test if we run from flash or RAM */
cmp r0, r1 /* don't reloc during debug */
beq clear_bss
ldr r2, _armboot_start
ldr r3, _bss_start
sub r2, r3, r2 /* r2 <- size of armboot */
#if 1
bl CopyCode2Ram /* r0: source, r1: dest, r2: size */
#else
add r2, r0, r2 /* r2 <- source end address */
copy_loop:
ldmia r0!, {r3-r10} /* copy from source address [r0] */
stmia r1!, {r3-r10} /* copy to target address [r1] */
cmp r0, r2 /* until source end addreee [r2] */
ble copy_loop
#endif
#endif /* CONFIG_SKIP_RELOCATE_UBOOT */
/*清除bss段的操作是依靠一个循环,_bss_start到_bss_end,每位都填上0*/
clear_bss:
ldr r0, _bss_start /* find start of bss segment */
ldr r1, _bss_end /* stop here */
mov r2, #0x00000000 /* clear */
clbss_l:str r2, [r0] /* clear loop... */
add r0, r0, #4
cmp r0, r1
ble clbss_l
......
ldr pc, _start_armboot
_start_armboot: .word start_armboot
可以看到硬件的初始化完成后将会进入到_start_armboot 这个是一个c文件
汇编语言实现的阶段被称为第一阶段,设置CPU为SVC32模式,关闭看门狗,屏蔽中断,初始化时钟,设置堆栈,将整个代码从flash里重定位到内存,清除bss段(可以在链接脚本看到bss段的起始地址等于end address),然后跳转到_start_boot
在代码重定位中有一个重要的c函数 CopyCode2Ram
int CopyCode2Ram(unsigned long start_addr, unsigned char *buf, int size)
{
unsigned int *pdwDest;
unsigned int *pdwSrc;
int i;
if (bBootFrmNORFlash())
{
pdwDest = (unsigned int *)buf;
pdwSrc = (unsigned int *)start_addr;
/* 从 NOR Flash启动 */
for (i = 0; i < size / 4; i++)
{
pdwDest[i] = pdwSrc[i];
}
return 0;
}
else
{
/* 初始化NAND Flash */
nand_init_ll();
/* 从 NAND Flash启动 */
nand_read_ll_lp(buf, start_addr, (size + NAND_BLOCK_MASK_LP)&~(NAND_BLOCK_MASK_LP));
return 0;
}
}
重定位代码时,需要判断源码是从nor flash启动还是从nand flash启动的,不同的启动方式对应的处理方法也不同
判断启动方式很简单,在bBootFrmNORFlash函数中,nor flash不能像内存一些直接写,必须要输入一些指令后才可以写,这在之前的博客中有提到,所以可以通过直接对一个随机变量赋值(赋值前一定要保存变量的原值,赋值完成后要恢复随机变量的原值),如果赋值失败则为nor flash启动,否则为nand flash 启动。
回到前面所说的内核启动的第二段 _start_armboot
简单看下这个函数,如下贴出它的部分代码
void start_armboot (void)
{
...
/*通过init_sequence进行一系列的初始化,包括cpu_init,borad_init interrupt_init等等
* 其中需要重点关注一下board_init,它不仅初始化了gpio的串口,还执行 gd->bd->bi_arch_number = MACH_TYPE_SMDK2410;
* 和 gd->bd->bi_boot_params = 0x30000100;
*/
for (init_fnc_ptr = init_sequence; *init_fnc_ptr; ++init_fnc_ptr) {
if ((*init_fnc_ptr)() != 0) {
hang ();
}
...
size = flash_init ();//初始化flash,在这个函数中会自动实现对flash类别的识别区分。
...
nand_init();
/*初始了nor flash 和 nand flash 以后,就可以对flash进行读和写操作了*/
...
/*和环境变量初始化有关*/
env_relocate ();
...
/*串口的初始化*/
Port_Init();
...
for (;;) {
main_loop ();
}
}
因为需要从flash中读出内核,需要对flash进行读和写操作,对于nor flash而言,读可以像内存一样读,但是写却需要输入指令;对于nand flash而言,操作又不一样,所以需要在对flash进行初始化之前需要识别是什么flash。
然后初始化nand flash,接着初始化串口。到这里我们已经初始了flash等硬件,可以对flash进行读和写,因而也就可以从flash上读取内核。
那如何启动内核呢?答案就在for循环中的main_loop函数,列出这个函数的部分代码
...
s = getenv ("bootcmd");
...
if (bootdelay >= 0 && s && !abortboot (bootdelay)) {\
printf("Booting Linux ...\n");
run_command (s, 0);
...
}
先解释一下for循环main_loop,在linux系统中,通常都会有一个命令行解释器的shell,我们敲入一些命令,然后shell解析执行命令,我们已经知道,每敲入一个命令,shell这个相当于父进程的会创建一个子进程来解析执行命令;然而在uboot中是不存在进程这一概念,命令解析执行的工作就交给了main_loop来执行。
从上面的代码可以看到,当bootdelay跑完且s不为空时,就会run_command (s, 0),联系我们所熟悉的uboot界面,我们知道当倒计时没有被打断,将会自动启动内核。所以启动内核的关键就在于命令s和函数run_command。
run_command的部分代码
int run_command (const char *cmd, int flag)
{
...
/*有了这段代码,就允许我们在输入cmd时,可以输入多个命令,但是多个命令之间需要有“;”来隔开*/
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;
}
...
/* Extract arguments 从输入的命令中提出参数,首先输入命令数据中,在单个命令中第一个是cmd,后面的是参数
* 所以提出参数的函数执行后,argv[0]是命令名称,argv[1]...是参数。所以我们首先要在command table中找到argv[0]对应的命令
* 然后调用相应的函数,并把argv[1]等作为参数传递进去。提出参数很好理解,但是find_cmd和加载内核密切相关,需要对这个函数进行分析
*/
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;
}
}
可以看到遍历和输入命令同名的cmd是在一块连续的内存上进行的(也可以说是一张连续的表),表的起始是__u_boot_cmd_start,表的终止是__u_boot_cmd_end,有趣的是这两个变量的定义不是在u-boot的源码中,而是在uboot编译的链接脚本中。
find_cmd
/*command.c*/
cmd_tbl_t *find_cmd (const char *cmd)
{
...
for (cmdtp = &__u_boot_cmd_start;
cmdtp != &__u_boot_cmd_end;
cmdtp++) {
//匹配名称总是用strncmp或者strncmp,但是用strncmp还需要判断cmd的长度和匹配到的cmd的长度是不是一致的
if (strncmp (cmd, cmdtp->name, len) == 0) {
if (len == strlen (cmdtp->name))
return cmdtp; /* full match */
cmdtp_temp = cmdtp; /* abbreviated command ? */
n_found++;
}
}
...
}
跟着u_boot_cmd找到了如下的内容
/*command.h*/
/*command table中每个成员的属性,就是每个命令的属性*/
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
};
...
typedef struct cmd_tbl_s cmd_tbl_t;
extern cmd_tbl_t __u_boot_cmd_start;
extern cmd_tbl_t __u_boot_cmd_end;
#define Struct_Section __attribute__ ((unused,section (".u_boot_cmd")))
#ifdef CFG_LONGHELP
/*稍后将根据如下的代码中来解析启动内核的命令*/
#define U_BOOT_CMD(name,maxargs,rep,cmd,usage,help) \
cmd_tbl_t __u_boot_cmd_##name Struct_Section = {#name, maxargs, rep, cmd, usage, help}
打印开发板环境变量,和启动内核相关的命令为 bootm 0x30007FC0
在源码中找到bootm命令的定义
/*上面的头文件里已经对U_BOOT_CMD进行了定义,初始化的内容就可以一一对上*/
U_BOOT_CMD(
bootm, CFG_MAXARGS, 1, do_bootm,
"bootm - boot application image from memory\n",
"[addr [arg ...]]\n - boot application image stored in memory\n"
"\tpassing arguments 'arg ...'; when booting a Linux kernel,\n"
"\t'arg' can be the address of an initrd image\n"
);
根据command.h中的定义,可以解析出bootm命令含义是
cmd_tbl_t __u_boot_cmd_bootm __attribute__ ((unused,section (".u_boot_cmd"))) = {#name, maxargs, rep, cmd, usage, help}
/* cmd_tbl_t是cmd的类型
* Struct_Section在头文件中可以找到是__attribute__ ((unused,section (".u_boot_cmd")))
* attribute((section(“name”))) 其作用是将作用的函数或数据放入指定名为"section_name"对应的段中
* 所以可以解释上述的代码含义是将这个命令(实际上所有的命令都会这样执行)放入的.u_boot_cmd段中,而在链接脚本中也确实有定义这个段
*/
上述分析到cmd都会放入到.u_boot_cmd段中,所以find_cmd才可以从__u_boot_cmd_start开始,到__u_boot_cmd_end结束。
为了更加深入的理解u-boot是如何执行命令,我们自己定义一个command
可以借鉴bootm这个我们已经研究,而且将要继续研究的命令来写一个自定义的命令
思路:首先定义,然后写函数,然后要注册到数组(结果发现不需要注册,可能是在编译过程中会自动加到段中,所以不需要注册),修改Makefile,编译就可以在串口终端上运行这个命令,这篇博客已经够长了,所以我就不再详细描述了
装载和启动内核的命令是 bootcmd=nand read.jffs2 0x30007FC0 kernel; bootm 0x30007FC0
nand read 表示我们要从nand flash上读取内核并复制到另外一个地方,从哪里读?kernel;读到哪里去?0x30007FC0
kernel代表着分区的意思,在u-boot中的分区是通过代码把分区写定了,通过mtdpart命令可以知道kernel的起始地址(offset)和大小(size)
所以我们制动启动内核前是从kernel分区里读取内核,读到0x30007FC0
在讲启动内核之前,先讲一下uImage,uImage实际上一个头部加上真正的内核
如下就是uImage头部的定义,我所列出的两个参数是我们最需要关注的参数
ih_load表示内核的启动时的加载地址,在这里就是0x30007FC0;ih_ep表示入口地址,内核运行时,从哪个地址开始执行(入口地址)。
当bootm指令会从0x30007FC0开始,读到了uImage的头部,获得了加载地址和入口地址,如果uImage中的内核并不位于加载地址时,bootm指令对应的do_bootm函数就会把内核重定位到加载地址上去
typedef struct image_header {
...
uint32_t ih_load; /* Data Load Address */
uint32_t ih_ep; /* Entry Point Address */
...
} image_header_t;
do_bootm
/*从do_bootm中截取了部分代码,确实如上面说的*/
switch (hdr->ih_comp) {
case IH_COMP_NONE:
/*判断ih_load的地址是否等于内核实际所在地址,如果不是,就需要将内核重定位到ih_load*/
if(ntohl(hdr->ih_load) == data) {
printf (" XIP %s ... ", name);
} else {
#if defined(CONFIG_HW_WATCHDOG) || defined(CONFIG_WATCHDOG)
size_t l = len;
void *to = (void *)ntohl(hdr->ih_load);
void *from = (void *)data;
printf (" Loading %s ... ", name);
while (l > 0) {
size_t tail = (l > CHUNKSZ) ? CHUNKSZ : l;
WATCHDOG_RESET();
memmove (to, from, tail);
to += tail;
from += tail;
l -= tail;
}
#else /* !(CONFIG_HW_WATCHDOG || CONFIG_WATCHDOG) */
memmove ((void *) ntohl(hdr->ih_load), (uchar *)data, len);
do_bootm还会执行diable cache等准备工作,最后调用函数来启动内核
switch (hdr->ih_os) {
default: /* handled by (original) Linux case */
case IH_OS_LINUX:
#ifdef CONFIG_SILENT_CONSOLE
fixup_silent_linux();
#endif
do_bootm_linux (cmdtp, flag, argc, argv,
addr, len_ptr, verify);
break;
...
}
因为我们这里是启动linux,所以调用do_bootm_linux
有一个问题需要思考一下,当开始启动内核的时候,uboot实际上已经退出了,那它如何把信息传递给linux内核呢?
uboot会把内核需要的信息以约定好的格式放在一个约定好的位置,内核会在在约定的地址上取得信息。
do_bootm_linux 的中如下代码就是涉及到u-boot和linux内核之间的信息传递
#if defined (CONFIG_SETUP_MEMORY_TAGS) || \
defined (CONFIG_CMDLINE_TAG) || \
defined (CONFIG_INITRD_TAG) || \
defined (CONFIG_SERIAL_TAG) || \
defined (CONFIG_REVISION_TAG) || \
defined (CONFIG_LCD) || \
defined (CONFIG_VFD)
setup_start_tag (bd);
#ifdef CONFIG_SERIAL_TAG
setup_serial_tag (¶ms);
#endif
#ifdef CONFIG_REVISION_TAG
setup_revision_tag (¶ms);
#endif
#ifdef CONFIG_SETUP_MEMORY_TAGS
setup_memory_tags (bd);
#endif
#ifdef CONFIG_CMDLINE_TAG
setup_commandline_tag (bd, commandline);
#endif
#ifdef CONFIG_INITRD_TAG
if (initrd_start && initrd_end)
setup_initrd_tag (bd, initrd_start, initrd_end);
#endif
#if defined (CONFIG_VFD) || defined (CONFIG_LCD)
setup_videolfb_tag ((gd_t *) gd);
#endif
setup_end_tag (bd);
和内核启动的相关代码
do_bootm_linux
{
void (*theKernel)(int zero, int arch, uint params);
...
theKernel (0, bd->bi_arch_number, bd->bi_boot_params);
}
其中theKernel函数的第三个参数bd->bi_boot_params刚好是设置参数中的setup_start_tag的设置的第一个参数,这样内核在启动时就可以找到自己需要的参数了。
关于参数传递这部分内容我找了相关博客,如下内容来自博主“求知_swat”,在此向他/她表示感谢
https://blog.csdn.net/hailin0716/article/details/18553055
the kernel其实不是个函数,而是指向内核入口地址的指针,把它强行转化为带三个参数的函数指针,会把三个参数保存到通用寄存器中,实现了向kernel传递信息的功能,在这个例子里,会把R0赋值为0,R1赋值为机器号 R2赋值为启动参数数据结构的首地址。
为什么要把信息传递给寄存器?因为这个时刻非常的特殊,uboot的工作已经完成,所以我们不能用uboot作为软件基础,而linux内核还没有启动,也不能用linux,所以在这个特殊的时候,没有任何软件基础可以让我们实现参数的传递,只能直接通过寄存器
因此,要向内核传递参数很简单,只要把启动参数封装在linux预定好的数据结构里,拷贝到某个地址(一般约定俗成是内存首地址+0x100) 。在tx2440开发板,该地址为 0x3000 0000+0x100。
总结一下:这篇博客我写得比较长,包含了u-boot配置、编译和启动,然后uboot再对内核进行重定位和启动,其实这个过程非常的复杂,一篇文章根本就讲不完。目前只是做了一个框架式的说明,希望以后能够有更深入的认识,加油。