uboot移植——深入认识u-boot的配置、编译和启动过程

本篇博客的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 (&params);
#endif
#ifdef CONFIG_REVISION_TAG
	setup_revision_tag (&params);
#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再对内核进行重定位和启动,其实这个过程非常的复杂,一篇文章根本就讲不完。目前只是做了一个框架式的说明,希望以后能够有更深入的认识,加油。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值