Uboot代码结构详细分析

1. Bootloader功能分析

  • Bootloader(如Uboot、Redboot、Blob、vivi等)直接和CPU、外围硬件设备(存储器、网卡、LCD等)打交道,负责初始化硬件设备,以及负责拉起内核:建立内存空间映射图,为内核的启动运行做好一切准备,最后将Linux内核代码加载到RAM中运行。

  • 一般来说,bootload都会提供两种操作模式:

    • 正常启动模式:无需开发者和用户干涉,上电后自动开始运行,完成启动内核的任务;
    • 下载模式(开发者模式):需要用户干预,进入下载模式,使用uboot命令进行操作;
  • BootLoader程序是分级载入机制,通常分为 stage1 和 stage2 两大部分。

    • Stage1:
      BootLoader 的 stage1 依赖于CPU体系结构的代码,例如CPU相关初始化代码等, 通常都用汇编语言来实现,达到高效操作的目的。 包括以下步骤:

      • ①硬件设备初始化
        • 屏蔽所有的中断
        • 设置CPU的速度和时钟频率
        • 设置内存控制器
        • 关闭 CPU 内部指令/数据 cache
      • ②为加载 stage2 准备 RAM 空间
      • ③拷贝 stage2 到 RAM 空间中
      • ④设置好堆栈指针sp,为执行 C 语言代码作好准备;
      • ⑤跳转到 stage2 阶段的C程序入口处。
    • Stage2:

      BootLoader 的 stage2 通常用C语言来实现,可以实现更复杂的功能,而且代码会具有更好的可读性和可移植性。 包括以下步骤:

      • ①初始化本阶段要使用到的硬件设备;
        • 串口设备:以便输出信息
          ……
        • ②检测系统内存映射(memory map):准备识别在整个 4GB 物理地址空间中有哪些地址范围被分配用来寻址系统的 RAM单元;
        • ③将 kernel 映像和根文件系统映像从 flash 上读到 RAM 空间中;
        • ④为内核设置启动参数;
        • ⑤调用内核:跳至内核代码入口开始执行。

2. Uboot代码结构分析

开发环境:我使用的是JZ2440开发板,处理器是三星的S3C2440,CPU是ARM920T。

开发目标:修改Uboot源代码,使之适配我的S3C2440处理器。

开发方法:1、因为Uboot默认不支持S3C2440处理器,所以先研究Uboot支持的且与S3C2440相近的S3C2410的启动过程源代码,了解自己需要修改哪些文件。

Uboot代码结构详细分析_第1张图片

首先,请看smdk2410的启动过程思维导图:

Uboot代码结构详细分析_第2张图片

2.1 一看配置文件smdk2410.h

在查看uboot源码的时候,很多配置项都取决于单板配置文件中的宏定义,比如smdk2410这个单板的配置文件是include\configs\smdk2410.h文件,在查看源码时,可以在此文件中查看宏定义是否存在。

2.2 二看链接文件u-boot.ds

可以看到,0地址处存放的是arch/arm/cpu/arm920t/start.S文件编译产生的obj文件。

Uboot代码结构详细分析_第3张图片

2.3 三看启动文件start.S

打开arch/arm/cpu/arm920t/start.S

.globl _start                      // .globl定义一个全局符号"_start",表明_start这个符号要被链接器用到 
_start:                            //_start:系统复位设置,以下共8种不同的异常处理
b	   start_code                  //系统启动即跳转到start_code处执行
ldr    pc, _undefined_instruction                 //未定义的指令异常 0x4
ldr    pc, _software_interrupt                 // 软件中断异常 0x8 
ldr    pc, _prefetch_abort                 //内存操作异常 0xc
ldr    pc, _data_abort                 //数据异常 0x10
ldr    pc, _not_used                 //未使用 0x14
ldr    pc, _irq                   //中断IRQ异常 0x18
ldr    pc, _fiq                 //快速中断FIQ异常 0x1c

_undefined_instruction:    .word undefined_instruction                 //0x20
_software_interrupt:    .word software_interrupt                       //0x24
_prefetch_abort:    .word prefetch_abort             // 0x28
_data_abort:    .word data_abort                     //0x2c
_not_used:    .word not_used                        //0x30
_irq:    .word irq                                  //0x34
_fiq:    .word fiq                                  //0x38

.balignl 16,0xdeadbeef                              //0x3c

在第1行中".globl _start":使用.globol声明全局符号_start,在 board/100ask24x0/u-boot.lds中ENTRY(_start)这里用到。其中符号保存的地址都在顶层目录/system.map中列出来了

system.map文件开头部分如下:

33f80000 t $a 
33f80000 T _start                   //_start符号被链接在33f80000,其中33f80000是生成bin文件的运行启始地址.
33f80020 t $d 
33f80020 t _undefined_instruction   //_undefined_instruction符号被链接在33f80020
...
33f80160 t undefined_instruction //_undefined_instruction指向的undefined_instruction符号被链接在33f80160
33f801c0 t software_interrupt
33f80220 t prefetch_abort
33f80280 t data_abort
33f802e0 t not_used
33f80340 T Launch
33f803b0 t On_Steppingstone
33f80400 t irq
...

在第2行中_start之所以有8种不同的异常处理,是在2440芯片手册已经规定好了的,如下图1:

Uboot代码结构详细分析_第4张图片

从上图可以看出复位异常处理需要进入管理模式(0X00000000),所以start.S 中b start_code也可以写作b reset,都是跳转到start_code处执行,即设置CPU进入管理模式后进行核级初始化。

在linux中的异常向量地址是经过MMU(虚拟内存管理)产生的虚拟地址,比如中断地址:0x18映射到物理地址是0xc000 0018(映射地址由自己设定),所以linux把中断向量放在0xc000 0018就行了。

CPU一上电设置了入口地址"ENTRY(_start)“后,就会进入”_start"全局符号中执行上面第3行跳转到复位异常字段: b reseb start_code

1. 后面的异常处理为什么用ldr不用b指令?

之所以第一句使用b reset,是因为ldr指令属于绝对跳转,而b属于相对跳转,它的地址与代码位置无关。因为复位异常在CPU运行前是没有初始化SDRAM的(不能使用0X30000000以上地址)。

在正常工作后也可能触发复位,这时由于CPU已经对SDRAM、MMU(虚拟内存管理)等初始化了,此时的虚拟地址和物理地址完全不同,所以reset使用b指令相对跳转。

2. 后面的异常处理是怎么执行的?执行后异常处理又怎么退出?

(1)在2440芯片手册上给出,例如当处理一个中断IRQ异常时:

  • a. 保存当前PC现场(返回地址)到寄存器R14;
  • b. 把当前程序状态寄存器(CPSR)保存到备份程序状态寄存器(SPSR)中,从异常退出的时候,就可以由SPSR来恢复CPSR;
  • c. 根据中断IRQ异常处理,强制将 CPSR 模式位设为中断模式,如下图:Uboot代码结构详细分析_第5张图片
  • d. 强制 PC 执行相关异常向量处的语句(跳转到中断异常处理函数处)。

(2)当退出中断IRQ异常时:**

  • a. 将中断IRQ所对应的R14_irq寄存器中的返回地址减4(PC总指向当前执行语句的下两条语句地址)得到被中断语句的下条地址放入到 PC 中。

Uboot代码结构详细分析_第6张图片

  • b. 复制 SPSR 的内容给 CPSR 中。
  • c. 如果在异常进入时置位了中断禁止标志位异常,则清除中断禁止标志位。

3. 第12行中的.word类似于(unsigend long)

以第12行中 _undefined_instruction: .word undefined_instruction为例讲解:
_undefined_instructionundefined_instruction都是一个标号
,表示_undefined_instruction指向一个32位(4字节)地址,该地址用undefined_instruction符号变量代替。

用C语言表示就是: _undefined_instruction = &undefined_instruction

相当于PC从_undefined_instruction取值时将符号变量undefined_instruction的地址存到了PC中。

4. 第20行中 .balignl 16,0xdeadbeef:

它的意思就是在以当前地址开始,在地址为16的倍数的指令位置的上一个指令填入0xdeadbeef的内容。
此时当前地址刚好0x3c=60,由于ARM每个指令间隔4个字节,且64%16=0,所以在0x3c中填入0xdeadbeef。
它们的作用就是为内存做标记插在那里,表示以此为界,往前有特殊作用的内存,禁止用户访问。

接下来继续往下看start.s

/*
*实际启动代码
*/

start_code: 
/* 1、设置CPU为管理模式(SVC32 mode)*/                           
  mrs    r0,cpsr                   //MRS读出CPSR寄存器值到R0
  bic    r0,r0,#0x1f               //将R0低5位清空
  orr    r0,r0,#0xd3               //R0与b'110 10011按位或,禁止IRQ和FIQ中断,10011:复位需要设为管理模式
  msr    cpsr,r0                   //MSR写入CPSR寄存器

	/* S3C2410的内核是ARM920T,这段代码用不到 */
#if	defined(CONFIG_AT91RM9200DK) || defined(CONFIG_AT91RM9200EK)
	/*
	 * relocate exception table
	 */
	ldr	r0, =_start
	ldr	r1, =0x0
	mov	r2, #16
copyex:
	subs	r2, r2, #1
	ldr	r3, [r0], #4
	str	r3, [r1], #4
	bne	copyex
#endif

#ifdef CONFIG_S3C24X0
	/* turn off the watchdog */
	/* 2、关看门狗 */
	//经过查看S3C2410/S3C2440数据手册,寄存器地址一致,不用修改

# if defined(CONFIG_S3C2400)
	#define pWTCON	0x15300000
	#define INTMSK	0x14400008	/* Interrupt-Controller base addresses */
	#define CLKDIVN	0x14800014	/* clock divisor register */
#else
	#define pWTCON     0x53000000  //看门狗定时器寄存器(0表示关闭看门狗)
	#define INTMOD     0X4A000004  //(中断模式寄存器(0:IRQ模式,1:FRQ模式)
	#define INTMSK		0x4A000008	//中断屏蔽寄存器(0:开启中断服务,1:关闭中断服务)
	#define INTSUBMSK	0x4A00001C  //中断次级屏蔽寄存器(0:开启中断服务,1:关闭中断服务)
	#define CLKDIVN    0x4C000014  //时钟分频寄存器
#endif
      
/* 关看门狗 */
ldr     r0, =pWTCON                        //R0等于WTCON地址
mov     r1, #0x0                           //R1=0x0
str     r1, [r0]                           //关闭WTCON寄存器,pWTCON=0;

/* 3、关掉所有中断   */  
    mov    r1, #0xffffffff                     //R1=0XFFFFFFFF
    ldr    r0, =INTMSK                         //R0等于INTMSK地址
    str    r1, [r0]                            //*0x4A000008=0XFFFF FFFF(关闭所有中断)

# if defined(CONFIG_S3C2410)
    ldr    r1, =0x3ff                            //R1=0x3FF
    ldr    r0, =INTSUBMSK                        //R0等于INTSUBMSK地址
    str    r1, [r0]                              //*0x4A00001C=0x3FF(关闭次级所有中断)
# endif

	/* 4. 设置系统时钟分频系数 */
	/* FCLK:HCLK:PCLK = 1:2:4 */
	/* default FCLK is 120 MHz ! */
	ldr	r0, =CLKDIVN
	mov	r1, #3
	str	r1, [r0]


	/*
	 * we do sys-critical inits only at reboot,
	 * not when booting from ram!
	 * 5. CPU内部初始化
	 *判断系统是从nand启动还是直接将程序下载到SDRAM中运行,若系统从nand启动,这里得到r0和r1值
	 *是不一样的,r1=0x33f80000,而r0=0x00000000。说明没初始化SDRAM,ne(no equal)标识符为
	 *真,所以bl cpu_init_crit执行跳转.
	 */
#ifndef CONFIG_SKIP_LOWLEVEL_INIT
  adr    r0, _start     
  ldr    r1, _TEXT_BASE     
  cmp     r0, r1               
  blne    cpu_init_crit
#endif

/* Set stackpointer in internal RAM to call board_init_f */
/* 6. 设置好栈顶指针sp = 0x30000f80,然后调用C程序中的board_init_f 函数 */
call_board_init_f:
	ldr	sp, =(CONFIG_SYS_INIT_SP_ADDR)
	bic	sp, sp, #7 /* 8-byte alignment for ABI compliance */
	ldr	r0,=0x00000000
	bl	board_init_f


  • CPU复位后从这里开始执行bootloader的 stage1 阶段,这里初始化了:
    • 1.设置CPU为管理模式:SVC32
    • 2.关看门狗
    • 3.屏蔽所有中断
    • 4.设置系统时钟分频系数
    • 5.CPU内部(核级)初始化(进入cpu_init_crit函数关闭MMU,进入lowlevel_init初始化13个BANK寄存器来初始化SDRAM)
    • 6.设置好栈顶指针sp,调用C程序中的board_init_f 函数。

2.4 四看CPU核级初始化文件cpu_init_crit函数

  • cpu_init_crit函数的主要功能是:
    • 设置CPU中重要的寄存器
    • 设置内存控制器时序

2.4.1 设置CPU中的寄存器

#ifndef CONFIG_SKIP_LOWLEVEL_INIT
    cpu_init_crit:
	/*
	 *flush v4 I/D caches
	 */
    mov    r0, #0
    //关闭ICaches(指令缓存,关闭是为了降低MMU查表带来的开销)和DCaches(数据缓存,DCaches使用的是虚拟地址,开启MMU之前必须关闭)
    mcr    p15, 0, r0, c7, c7, 0
    //使无效整个数据和指令TLB(TLB就是负责将虚拟内存地址翻译成实际的物理内存地址)
    mcr    p15, 0, r0, c8, c7, 0
	
    /*
	 * disable MMU stuff and caches
	 */
    mrc    p15, 0, r0, c1, c0, 0
        
    //bit8:系统不保护,bit9:ROM不保护,bit13:设置正常异常模式0x0~0x1c,即异常模式基地址为0X0
    bic    r0, r0, #0x00002300    @ clear bits 13, 9:8 (--V- --RS)
        
    //bit0~2:禁止MMU,禁止地址对齐检查,禁止数据Cache.bit7:设为小端模式
    bic    r0, r0, #0x00000087    @ clear bits 7, 2:0 (B--- -CAM) 
    orr    r0, r0, #0x00000002    @ bit2:开启地址对齐
    orr    r0, r0, #0x00001000    @ bit12:开启ICache
    mcr    p15, 0, r0, c1, c0, 0
    /*
    *Caches:是一种高速缓存存储器,用于保存CPU频繁使用的数据。在使用Cache技术的处理器上,当一条
    *指令要访问内存的数据时,首先查询cache缓存中是否有数据以及数据是否过期,如果数未过期则从
    *cache读出数据。处理器会定期回写cache中的数据到内存。根据程序的局部性原理,使用cache后可以
    *大大加快处理器访问内存数据的速度。其中DCaches和ICaches分别用来存放数据和执行这些数据的指令。
    *TLB:就是负责将虚拟内存地址翻译成实际的物理内存地址,TLB中存放了一些页表文件,文件中记录了虚
    *拟地址和物理地址的映射关系。当应用程序访问一个虚拟地址的时候,会从TLB中查询出对应的物理地址,
    *然后访问物理地址。TLB通常是一个分层结构,使用与Cache类似的原理。处理器使用一定的算法把最常用
    *的转换表放在最先访问的层次。这里禁用MMU,是方便后面直接使用物理地址来设置控制寄存。
    */
        
    /*
	 * before relocating, we have to setup RAM timing
	 * because memory timing is board-dependend, you will
	 * find a lowlevel_init.S in your board directory.
	 */
    mov    ip, lr 		//临时保存当前子程序返回地址,因为接下来执行bl会覆盖当前返回地址.
    bl    lowlevel_init //跳转到lowlevel_init(/board/samsung/smdk2410/lowlevel_init.S)
    mov    lr, ip 		//恢复当前返回地址
    mov    pc, lr 		//退出
#endif /* CONFIG_SKIP_LOWLEVEL_INIT */

2.4.2 调用lowlevel_init函数 设置内存控制器

每个单板的内存控制器设置代码都是不一样的,所以 lowlevel_init 函数放在了单板目录中的lowlevel_init.S文件中,即 board\samsung\smdk2410\lowlevel_init.S

_TEXT_BASE:
	.word	CONFIG_SYS_TEXT_BASE

.globl lowlevel_init
lowlevel_init:
	/* memory control configuration */
	/* make r0 relative the current location so that it */
	/* reads SMRDATA out of FLASH rather than memory ! */

    ldr r0, =SMRDATA 	//将SMRDATA的首地址(0x33F806C8)存到r0中 
    ldr r1, _TEXT_BASE 	//r1等于_TEXT_BASE内容,也就是TEXT_BASE(0x33F80000)
    sub    r0, r0, r1 	//将0x33F806C8与0x33F80000相减,得到现在13个寄存器值在NOR Flash上存放的开始地址
    ldr    r1, =BWSCON 	//将BWSCON寄存器地址值存到r1中 (第一个存储器寄存器首地址)
    add r2, r0, #13*4 	//每个寄存器4字节,r2=r0+13*4=NOR Flash上13个寄存器值最后一个地址
0: 
    ldr r3, [r0], #4 	//将r0的内容存到r3的内容中(r3等于SMRDATA里面值), 同时r0地址+=4;
    str r3, [r1], #4 	//将r3的内容存到r1所指的地址中(向寄存器地址里写入r3值),同时r1地址+=4;
    cmp r2, r0 			// 判断r2和r0
    bne 0b 				//不等则跳转到第6行继续执行

    mov pc, lr 			//跳回到返回地址中继续执行

SMRDATA:
.word (0+(B1_BWSCON<<4)+(B2_BWSCON<<8)+(B3_BWSCON<<12)+(B4_BWSCON<<16)+(
B5_BWSCON<<20)+(B6_BWSCON<<24)+(B7_BWSCON<<28)) //设置每个BWSCON,注意BANK0由硬件连线决定了
.word ((B0_Tacs<<13)+(B0_Tcos<<11)+(B0_Tacc<<8)+(B0_Tcoh<<6)+(B0_Tah<<4)+(B0_Tacp<<2)+(B0_PMC))
.word ((B1_Tacs<<13)+(B1_Tcos<<11)+(B1_Tacc<<8)+(B1_Tcoh<<6)+(B1_Tah<<4)+(B1_Tacp<<2)+(B1_PMC))
.word ((B2_Tacs<<13)+(B2_Tcos<<11)+(B2_Tacc<<8)+(B2_Tcoh<<6)+(B2_Tah<<4)+(B2_Tacp<<2)+(B2_PMC))
.word ((B3_Tacs<<13)+(B3_Tcos<<11)+(B3_Tacc<<8)+(B3_Tcoh<<6)+(B3_Tah<<4)+(B3_Tacp<<2)+(B3_PMC))
.word ((B4_Tacs<<13)+(B4_Tcos<<11)+(B4_Tacc<<8)+(B4_Tcoh<<6)+(B4_Tah<<4)+(B4_Tacp<<2)+(B4_PMC))
.word ((B5_Tacs<<13)+(B5_Tcos<<11)+(B5_Tacc<<8)+(B5_Tcoh<<6)+(B5_Tah<<4)+(B5_Tacp<<2)+(B5_PMC))
//设置BANKCON0~BANKCON5 
.word ((B6_MT<<15)+(B6_Trcd<<2)+(B6_SCAN))
.word ((B7_MT<<15)+(B7_Trcd<<2)+(B7_SCAN))
    
//设置BANKCON6~BANKCON7
.word ((REFEN<<23)+(TREFMD<<22)+(Trp<<20)+(Trc<<18)+(Tchr<<16)+REFCNT)
    
//设置REFRESH,在S3C2440中11~17位是保留的,也即(Tchr<<16)无意义
.word 0xb1 //设置BANKSIZE,对于容量可以设置大些,多出来的空内存会被自动检测出来
.word 0x30 //设置MRSRB6
.word 0x30 //设置MRSRB7

2.5 五看CPU板级初始化文件board_init_f函数

该函数在arch/arm/lib/board.c中定义:

2.5.1 gd指针

/* Pointer is writable since we allocated a register for it */
	gd = (gd_t *) ((CONFIG_SYS_INIT_SP_ADDR) & ~0x07);

gd指针变量是一个寄存器变量,在arch/arm/include/asm/global_data.h文件中定义:#define DECLARE_GLOBAL_DATA_PTR register volatile gd_t *gd asm ("r8")。这个宏定义将gd定义为一个指向gd_t类型的寄存器变量(优势:读写效率高),并将这个寄存器指定为CPU寄存器组中的r8寄存器。

那么,gd变量指向内存中的哪个地址呢?

在stage1阶段跳转到 board_init_f 函数之前,使用汇编指令将 sp 设置为CONFIG_SYS_INIT_SP_ADDR,通过直接查看反汇编代码,得到该值为0x30000f80。

接下来具体研究一下CONFIG_SYS_INIT_SP_ADDR是如何计算出来的,在include/configs/smdk2410.h文件中可以看到计算公式:

/* additions for new relocation code, must be added to all boards */
//为了新的重定位代码添加
#define CONFIG_SYS_SDRAM_BASE	PHYS_SDRAM_1
#define CONFIG_SYS_INIT_SP_ADDR	(CONFIG_SYS_SDRAM_BASE + 0x1000 - GENERATED_GBL_DATA_SIZE)

同样在该配置文件中,定义了 PHYS_SDRAM_1 的大小:

 * Physical Memory Map
   */
   #define CONFIG_NR_DRAM_BANKS	1          /* we have 1 bank of DRAM */
   #define PHYS_SDRAM_1		0x30000000 /* SDRAM Bank #1 */
   #define PHYS_SDRAM_1_SIZE	0x04000000 /* 64 MB */

#define PHYS_FLASH_1		0x00000000 /* Flash Bank #0 */

#define CONFIG_SYS_FLASH_BASE	PHYS_FLASH_1

2.5.2 执行init_sequence(初始化序列)中的所有函数

接下来继续研读uboot源代码,在设置完gd指针之后,uboot调用执行了 init_sequence中的所有函数:

for (init_fnc_ptr = init_sequence; *init_fnc_ptr; ++init_fnc_ptr) {
	if ((*init_fnc_ptr)() != 0) {
		hang ();
	}
}

这个函数指针数组init_sequence具体的定义如下(方便起见,我将其中没有用到的代码标示了“未用到”):

init_fnc_t *init_sequence[] = {

/*未用到*/
#if defined(CONFIG_ARCH_CPU_INIT)
	arch_cpu_init,		/* basic arch cpu dependent setup */
#endif


#if defined(CONFIG_BOARD_EARLY_INIT_F)
	board_early_init_f,
#endif

/*未用到*/
#ifdef CONFIG_OF_CONTROL
	fdtdec_check_fdt,
#endif


	timer_init,		/* initialize timer */

/*未用到*/
#ifdef CONFIG_FSL_ESDHC
	get_clocks,
#endif


	env_init,		/* initialize environment */
	init_baudrate,		/* initialze baudrate settings */
	serial_init,		/* serial communications setup */
	console_init_f,		/* stage 1 init of console */
	display_banner,		/* say that we are here */

#if defined(CONFIG_DISPLAY_CPUINFO)
	print_cpuinfo,		/* display cpu info (and speed) */
#endif

/*未用到*/
#if defined(CONFIG_DISPLAY_BOARDINFO)
	checkboard,		/* display board info */
#endif

/*未用到*/
#if defined(CONFIG_HARD_I2C) || defined(CONFIG_SOFT_I2C)
	init_func_i2c,
#endif

	dram_init,		/* configure available RAM banks */
	NULL,
};

总结一下,初始化序列 init_sequence 主要是设备初始化工作

  • ① 硬件平台初始化:时钟系统初始化,GPIO初始化;
  • ② 定时器初始化;
  • ③ 外围设备初始化:串口、Flash等;
  • ④ 打印CPU信息;
  • ⑤ 初始化DRAM(SDRAM);

接下来挨个查看这些函数的源码:

  • board_early_init_f

这个函数在board/samsung/smdk2410/smdk2410.c文件中定义,是一些与硬件平台相关的初始化,包括时钟初始化、GPIO初始化

/*
 * Miscellaneous platform dependent initialisations
 * 各种各样的硬件平台相关初始化
 */

int board_early_init_f(void)
{
	struct s3c24x0_clock_power * const clk_power =
					s3c24x0_get_base_clock_power();
	struct s3c24x0_gpio * const gpio = s3c24x0_get_base_gpio();

	/* to reduce PLL lock time, adjust the LOCKTIME register */
	writel(0xFFFFFF, &clk_power->locktime);

	/* configure MPLL */
	writel((M_MDIV << 12) + (M_PDIV << 4) + M_SDIV,
	       &clk_power->mpllcon);

	/* some delay between MPLL and UPLL */
	pll_delay(4000);

	/* configure UPLL */
	writel((U_M_MDIV << 12) + (U_M_PDIV << 4) + U_M_SDIV,
	       &clk_power->upllcon);

	/* some delay between MPLL and UPLL */
	pll_delay(8000);

	/* set up the I/O ports */
	writel(0x007FFFFF, &gpio->gpacon);
	writel(0x00044555, &gpio->gpbcon);
	writel(0x000007FF, &gpio->gpbup);
	writel(0xAAAAAAAA, &gpio->gpccon);
	writel(0x0000FFFF, &gpio->gpcup);
	writel(0xAAAAAAAA, &gpio->gpdcon);
	writel(0x0000FFFF, &gpio->gpdup);
	writel(0xAAAAAAAA, &gpio->gpecon);
	writel(0x0000FFFF, &gpio->gpeup);
	writel(0x000055AA, &gpio->gpfcon);
	writel(0x000000FF, &gpio->gpfup);
	writel(0xFF95FFBA, &gpio->gpgcon);
	writel(0x0000FFFF, &gpio->gpgup);
	writel(0x002AFAAA, &gpio->gphcon);
	writel(0x000007FF, &gpio->gphup);

	return 0;
}
  • timer_init初始化

这个函数在arch/arm/cpu/arm920t/s3c24x0/timer.c中定义,用来初始化系统定时器:

int timer_init(void)
{
	struct s3c24x0_timers *timers = s3c24x0_get_base_timers();
	ulong tmr;

	/* use PWM Timer 4 because it has no output */
	/* prescaler for Timer 4 is 16 */
	writel(0x0f00, &timers->tcfg0);
	if (gd->tbu == 0) {
		/*
		 * for 10 ms clock period @ PCLK with 4 bit divider = 1/2
		 * (default) and prescaler = 16. Should be 10390
		 * @33.25MHz and 15625 @ 50 MHz
		 */
		gd->tbu = get_PCLK() / (2 * 16 * 100);
		gd->timer_rate_hz = get_PCLK() / (2 * 16);
	}
	/* load value for 10 ms timeout */
	writel(gd->tbu, &timers->tcntb4);
	/* auto load, manual update of timer 4 */
	tmr = (readl(&timers->tcon) & ~0x0700000) | 0x0600000;
	writel(tmr, &timers->tcon);
	/* auto load, start timer 4 */
	tmr = (tmr & ~0x0700000) | 0x0500000;
	writel(tmr, &timers->tcon);
	gd->lastinc = 0;
	gd->tbl = 0;

	return 0;
}
  • 设备初始化

这些函数用来初始化需要用到的设备,在通用设备驱动文件中定义:

	env_init,			/* initialize environment */
	init_baudrate,		/* initialze baudrate settings */
	serial_init,		/* serial communications setup */
	console_init_f,		/* stage 1 init of console */
	display_banner,		/* say that we are here */
  • print_cpuinfo

这个函数用来打印CPU信息,在arch/arm/cpu/arm920t/s3c24x0/cpu_info.c文件中:

int print_cpuinfo(void)
{
	int i;
	char buf[32];
/* the S3C2400 seems to be lacking a CHIP ID register */
#ifndef CONFIG_S3C2400
	ulong cpuid;
	struct s3c24x0_gpio * const gpio = s3c24x0_get_base_gpio();

	cpuid = readl(&gpio->gstatus1);
	printf("CPUID: %8lX\n", cpuid);
#endif
	for (i = 0; i < ARRAY_SIZE(freq_f); i++)
		printf("%cCLK: %8s MHz\n", freq_c[i], strmhz(buf, freq_f[i]()));

	return 0;
}
  • dram_init

这个用来初始化SDRAM,在board/samsung/smdk2410/smdk2410.c文件中:

int dram_init(void)
{
	/* dram_init must store complete ramsize in gd->ram_size */
	gd->ram_size = PHYS_SDRAM_1_SIZE;
	return 0;
}

PHYS_SDRAM_1_SIZE宏定义在上文中已经分析过了,为64MB:

#define PHYS_SDRAM_1_SIZE	0x04000000 /* 64 MB */

返回到start.S继续往下看

stack_setup: //设置栈,方便调用C函数 
ldr    r0, _TEXT_BASE    //代码段的初始地址:r0=0x33f80000
sub    r0, r0, #CFG_MALLOC_LEN      //留出一段内存以实现malloc:r0=0x33f50000
sub    r0, r0, #CFG_GBL_DATA_SIZE  //再留出一段存一些全局参数的变量:r0=0x33F4FF80

#ifdef CONFIG_USE_IRQ
sub    r0, r0, #(CONFIG_STACKSIZE_IRQ+CONFIG_STACKSIZE_FIQ) //中断与快中断的栈:r0=0x33F4DF7C
#endif
sub    sp, r0, #12                 //留出12字节内存给abort异常设置栈顶sp=r0-12;

#ifndef CONFIG_SKIP_LOWLEVEL_INIT
bl clock_init                  //进入clock_init函数
#endif 

这里初始化了:

​ 5.设置栈

​ 6.进入clock_init函数设置时钟

进入clock_init函数

void clock_init(void)
{
S3C24X0_CLOCK_POWER *clk_power = (S3C24X0_CLOCK_POWER *)0x4C000000; //定义一个S3C24X0_CLOCK_POWER型结构体指针,clk_power->LOCKTIME=0x4C000000

if (isS3C2410) //isS3C2410为0,执行else
{... ...}
else
{
/* FCLK:HCLK:PCLK = 1:4:8 */
clk_power->CLKDIVN = S3C2440_CLKDIV; //S3C2440_CLKDIV=0X05

/* change to asynchronous bus mod */
__asm__( "mrc p15, 0, r1, c1, c0, 0\n" /* read ctrl register */ 
"orr r1, r1, #0xc0000000\n" //使其从快总线模式改变为异步总线模式,在2440手册上看到
"mcr p15, 0, r1, c1, c0, 0\n" /* write ctrl register */ 
:::"r1" //:::"r1" 向GCC声明:我对r1作了改动
);

/* to reduce PLL lock time, adjust the LOCKTIME register */
clk_power->LOCKTIME = 0xFFFFFFFF; //PLL 锁定时间计数寄存器

/* configure UPLL */
clk_power->UPLLCON = S3C2440_UPLL_48MHZ; //UCLK=48Mhz

/* some delay between MPLL and UPLL */
delay (4000); //等待UCLK时钟波形稳定

/* configure MPLL */
clk_power->MPLLCON = S3C2440_MPLL_400MHZ; //FCLK=400Mhz

/* some delay between MPLL and UPLL */ 
delay (8000); //等待FCLK时钟波形稳定
}
}

2.5.3 准备内存空间

在 board_init_f 函数中,接下来的内容都是在准备内存空间,详细的代码和SDRAM中uboot准备的内存分布图如下:

Uboot代码结构详细分析_第7张图片

移植成功后,执行bdinfo命令查看信息,各地址可能的取值如下:

_start = 0x3000 0000

_size = 0x0400 0000

TLB addr = 0x33FF 0000

relocaddr = 0x33F9 D000

reloc off = 0x33F9 D000

irq_sp = 0x33B8 CF64

sp start = 0x33B8 CF58

FB base = 0x0000 0000

2.5.4 代码重定位

在准备完内存空间之后,就进入了这个函数的最末端,重定位代码,代码如下:

	gd->relocaddr = addr;
	gd->start_addr_sp = addr_sp;
	gd->reloc_off = addr - _TEXT_BASE;
	debug("relocation Offset is: %08lx\n", gd->reloc_off);
	memcpy(id, (void *)gd, sizeof(gd_t));

	//重定位代码
	relocate_code(addr_sp, id, addr);

2.6 六看重定位代码relocate_code

relocate_code的函数体是在start.S中用汇编语言编写的。

2.6.1. 复制Flash代码到SDRAM中

根据ARM子程序调用规则,C语言调用relocate_code函数时,传入的三个参数分别存放在R0、R1、R2寄存器中,所以在汇编代码被调用时,首先将这三个重要参数保存:

	.globl	relocate_code
relocate_code:
	mov	r4, r0	/* save addr_sp 保存栈顶指针的值到r4寄存器中 */
	mov	r5, r1	/* save addr of gd 保存gd指针的值到r5寄存器中 */
	mov	r6, r2	/* save addr of destination 保存addr的值到r6寄存器中*/

然后设置栈顶指针sp:

	/* Set up the stack						    */
stack_setup:
	mov	sp, r4

bss段中存放的都是初始值为0的全局变量或者静态变量,不会包含在二进制文件中,所以拷贝程序时只需要拷贝到bss端的起始地址结束即可

	adr	r0, _start		//_start是程序起始地址
	cmp	r0, r6			//和要拷贝的目的地址比较一下
	beq	clear_bss		//如果相同,跳过拷贝,直接跳到clear_bss处
	mov	r1, r6			//把目的地址加载到r1中
	ldr	r3, _bss_start_ofs	//加载
	add	r2, r0, r3		/* r2 <- source end address	    */

copy_loop:
	ldmia	r0!, {r9-r10}		/* copy from source address [r0]    */
	stmia	r1!, {r9-r10}		/* copy to   target address [r1]    */
	cmp	r0, r2					/* until source end address [r2]    */
	blo	copy_loop

2.6.2. 修改变量和函数的链接地址

存放在Flash中的程序中,函数和变量的调用地址是基于0地址,现在拷贝到了SDRAM中,而SDRAM的基地址是0x3000_0000,所以需要对拷贝程序中所有的调用链接地址进行修改。

修改程序如下:

#ifndef CONFIG_SPL_BUILD
	/*
	 * fix .rel.dyn relocations
	 */
	ldr	r0, _TEXT_BASE		/* r0 <- Text base */
	sub	r9, r6, r0		/* r9 <- relocation offset */
	ldr	r10, _dynsym_start_ofs	/* r10 <- sym table ofs */
	add	r10, r10, r0		/* r10 <- sym table in FLASH */
	ldr	r2, _rel_dyn_start_ofs	/* r2 <- rel dyn start ofs */
	add	r2, r2, r0		/* r2 <- rel dyn start in FLASH */
	ldr	r3, _rel_dyn_end_ofs	/* r3 <- rel dyn end ofs */
	add	r3, r3, r0		/* r3 <- rel dyn end in FLASH */
fixloop:
	ldr	r0, [r2]		/* r0 <- location to fix up, IN FLASH! */
	add	r0, r0, r9		/* r0 <- location to fix up in RAM */
	ldr	r1, [r2, #4]
	and	r7, r1, #0xff
	cmp	r7, #23			/* relative fixup? */
	beq	fixrel
	cmp	r7, #2			/* absolute fixup? */
	beq	fixabs
	/* ignore unknown type of fixup */
	b	fixnext
fixabs:
	/* absolute fix: set location to (offset) symbol value */
	mov	r1, r1, LSR #4		/* r1 <- symbol index in .dynsym */
	add	r1, r10, r1		/* r1 <- address of symbol in table */
	ldr	r1, [r1, #4]		/* r1 <- symbol value */
	add	r1, r1, r9		/* r1 <- relocated sym addr */
	b	fixnext
fixrel:
	/* relative fix: increase location by offset */
	ldr	r1, [r0]
	add	r1, r1, r9
fixnext:
	str	r1, [r0]
	add	r2, r2, #8		/* each rel.dyn entry is 8 bytes */
	cmp	r2, r3
	blo	fixloop
#endif

2.6.3 清除BSS

clear_bss:
#ifndef CONFIG_SPL_BUILD
	ldr	r0, _bss_start_ofs
	ldr	r1, _bss_end_ofs
	mov	r4, r6			/* reloc addr */
	add	r0, r0, r4
	add	r1, r1, r4
	mov	r2, #0x00000000		/* clear			    */

clbss_l:str	r2, [r0]		/* clear loop...		    */
	add	r0, r0, #4
	cmp	r0, r1
	bne	clbss_l

	bl coloured_LED_init
	bl red_led_on
#endif

2.7 七看board_init_r代码(调用C函数,进入stage2阶段)

首先计算调用地址,存放到lr寄存器中,然后设置向该函数传入的参数(gd_t地址和dest_addr地址),最后加载lr寄存器的值到pc中,成功调用board_init_r函数(这个函数在arch\arm\lib\board.c文件中):

	ldr	r0, _board_init_r_ofs
	adr	r1, _start
	add	lr, r0, r1
	add	lr, lr, r9
	/* setup parameters for board_init_r */
	mov	r0, r5		/* gd_t */
	mov	r1, r6		/* dest_addr */
	/* jump to it ... */
	mov	pc, lr

_board_init_r_ofs:
	.word board_init_r - _start

调用该函数开始uboot的stage2阶段——由C语言实现的复杂功能。该阶段的分析放到后面再分析,下面先看我们修改后的u-boot文件能否成功编译,再将其烧录到实际开发板中,看能否成功运行。

2.8、uboot的硬件初始化(板级)

在start.S初始化后跳转到start_armboot实现第2阶段硬件相关的初始化(烧写擦除flash,网卡驱动,usb驱动,串口驱动,从FLASH读内核,启动内核等)然后调用main_loop();实现u-boot环境参数设置(print),读内核,启动内核等.

uboot-第二阶段硬件初始化主要主要执行以下三个过程:

1.启动内核(开机不按空格)

s = getenv ("bootcmd"); //char指针变量s指向bootcmd(command)环境变量
run_command (s, 0); //执行bootcmd命令 run_command():执行命令函数

2.进入菜单(开机按空格)

run_command("menu", 0); //进入菜单界面,按q(queue)键退出

3.进入u-boot界面(退出菜单后)

len = readline (CFG_PROMPT); //一直扫描串口输入的命令(以回车结尾)
rc = run_command (lastcommand, flag); //执行串口输入的命令

上面三个过程都是执行了run_command()函数,所以u-boot核心在于执行命令。下面具体请看代码:

start_armboot函数代码如下(位于u-boot-1.1.6/lib_arm/borad.c)

void start_armboot (void)
{
...
for (init_fnc_ptr = init_sequence; *init_fnc_ptr; ++init_fnc_ptr) { //init_sequence:进入初始化序列,初始化CPU,GPIO,中断,环境,串口,设置SDRAM首地址和长度等
if ((*init_fnc_ptr)() != 0) {
hang ();
}
}

size = flash_init (); //初始化NOR FLASH
mem_malloc_init (_armboot_start - CFG_MALLOC_LEN); //初始化malloc(相当于C语言中的malloc()和free(),实现堆的分配和释放)
nand_init();    //初始化NAND FLASH
env_relocate (); //将环境参数[enviconment]迁移[relocate]到内存指定位置
cs8900_get_enetaddr (gd->bd->bi_enetaddr); //初始化网络设备
....
for (;;) {
main_loop (); //死循环调用main_loop();
}

main_loop()分析(位于u-boot-1.1.6/common/main.c):
环境参数设置,从flash读出内核,启动内核等
void main_loop (void)
{
...

s = getenv ("bootdelay"); //char指针变量s指向bootdelay(开机倒计时)环境变量
bootdelay = s ? (int)simple_strtol(s, NULL, 10) : CONFIG_BOOTDELAY; //这个CONFIG_BOOTDELAY全局变量等于3,表示bootdelay=3S、
...
...
s = getenv ("bootcmd"); //char指针变量s指向bootcmd(command)环境变量
...
...
if (bootdelay >= 0 && s && !abortboot (bootdelay)) { //当开机bootdelay秒内无串口输入时
...
# ifndef CFG_HUSH_PARSER
{
printf("Booting Linux ...\n"); //启动 Linux ...
run_command (s, 0); //执行bootcmd命令 s= getenv ("bootcmd");
}
...
}
/*bootcmd:启动命令 */
/*在uboot里输入print就会有这么一行"boodcmd=nand read .jffs2 0x30007FC0 kernel; bootm 0x30007FC0" */ 
/*当在开机bootdelay秒内未有串口输入,则会执行bootcmd命令: */ 
/*1.从NAND FLASH中读出内核到SDRAM的0x30007FC0中(nand read .jffs2 0x30007FC0 kernel) */ 
/* 2.从0x30007FC0中启动内核 */
...
...
run_command("menu", 0); //当开机bootdelay秒内有串口输入时,进入菜单界面,按q(queue)键退出
...
...
for (;;) { //进入u-boot界面
...
len = readline (CFG_PROMPT); //一直扫描串口输入的命令(以回车结尾)
...
rc = run_command (lastcommand, flag); //执行串口输入的命令
}
...
}

接下来开始分析u-boot怎么实现查找命令和制作命令。

2.9 run_command()命令查找过程和命令生成过程

2.9.1 run_command函数命令查找过程分析

在u-boot界面中(main_loop();位于u-boot-1.1.6/common/main.c ):
a 输入命令字符串
b 将命令字符串代入函数run_command()
c run_command():判断命令字符串,在argv[0]里保存命令名,并调用find_cmd(argv[0]))函数查找内存中该命令结构体,判断各个参数,执行命令等
d find_cmd(argv[0])):查找.u_boot_cmd命令段中所有命令是否与argv[0]这个命令名字相等,

2.9.1.1 首先查看run_command()函数分析,如何判断判断命令
int run_command (const char *cmd, int flag) //*cmd:入口字符串命令 flag:参数
{
cmd_tbl_t *cmdtp;
char cmdbuf[CFG_CBSIZE];    //cmdbuf:用来备份的
char *str = cmdbuf; //*str指向备份的入口命令
...
if (!cmd || !*cmd) { //先对字符串命令cmd的有效性进行检测,判断该命令是否为空
return -1;    /* empty command */
}
if (strlen(cmd) >= CFG_CBSIZE) { //判断字符串命令的长度是否在CFG_CBSIZE(256)范围之内
puts ("## Command too long!\n");
return -1;
}
strcpy (cmdbuf, cmd); //备份入口命令cmd到cmdbuf

while (*str) { //str指向cmdbuf备份命令,循环判断字符串命令有几个
/*因为uboot允许一次输入多个命令,以下就是分析 是否有多个命令,*/ 
/* 比如在uboot界面输入"print;md.w 0"回车后,先打印出环境参数后,后打印地址0里存放的数据。*/ 
for (inquotes = 0, sep = str; *sep; sep++) //sep指向当前命令str的开头,一直for寻找当前命令结尾处
{
if ((*sep=='\'') && //对"\"解析,分割成多个命令 
(*(sep-1) != '\\'))
inquotes=!inquotes;

if (!inquotes &&
(*sep == ';') &&    //判断当前等于";"    
( sep != str) &&    //且当前指向的位置不是命令的开头, 
(*(sep-1) != '\\'))    
break; //停止本次for循环,sep指向当前这个命令结尾处 
}
token= str; //token指向当前命令的开头 
if (*sep) { //将当前";"分割符处替换成'\0'空字符 
str = sep + 1;    //str命令指向下个命令,若下个命令为空,退出while (*str)循环. 
*sep = '\0'; //将当前命令结尾";"处替换成'\0'空字符 
}
else 
str = sep;    //如果没有命令了,就指向当前命令底部 

process_macros (token, finaltoken); //token=当前命令开头,将当前命令中的宏替换掉,
//例如命令"nand write .yaffs 30000000 0X00260000 $(kernelsize)":其中$(kernelsize)就是宏,这里将替换成文件大小长度

if ((argc = parse_line (finaltoken, argv)) == 0) 
//argc等于参数的个数。
//parse_line函数:解析当前命令用argv数组保存并返回当前命令参数个数,例如"md.w 0"->argv[0]=“md.w”(保存命令), argv[1]=“0”(保存参数)

{ 
rc = -1;    /* no command at all */
continue;
}

if ((cmdtp = find_cmd(argv[0])) == NULL) { 
/* find_cmd(argv[0])) :查找.u_boot_cmd段中是否有这个命令argv[0]名字,若有的话返回这个命令的结构体,否则返回NULL。*/
/* cmdtp: 指向argv[0]命令名字的结构体. */
printf ("Unknown command '%s' - try 'help'\n", argv[0]); //输出提示,未找到命令
rc = -1;    /* give up after bad command */
continue;
}
/*其中find_cmd()返回值和cmdtp都是一个cmd_tbl_s型结构体,其中成员如下所示:
struct cmd_tbl_s {
char    *name;    //命令的名字
int    maxargs;    //命令后带的最大参数个数
int    repeatable;    //定义命令是否可重复,例如:在uboot界面输入"md.w 0"打印后,再次敲回车键继续运行该命令.

int    (*cmd)(struct cmd_tbl_s *, int, int, char *[]); //函数指针,用于命令执行时需要调用什么函数
char    *usage;    // 该命令所对应得较短的使用说明,例如输入“help”,每行命令后面都跟着较短的使用说明 
#ifdef    CFG_LONGHELP
char    *help;    // 该命令所对应得较详细的使用说明,例如输入“help md”,会打印出该命令详细的使用说明 
#endif 
};
*/
if (argc > cmdtp->maxargs) { //检查当前命令的参数个数argc是否在最大参数个数范围内
printf ("Usage:\n%s\n", cmdtp->usage);
rc = -1;
continue;
}

if ((cmdtp->cmd) (cmdtp, flag, argc, argv) != 0) { //cmdtp:当前命令结构体,判断cmdtp->cmd执行命令有没有函数
rc = -1; //执行命令
}

repeatable &= cmdtp->repeatable; //设置命令重复执行标志

if (had_ctrlc ()) //检查是否有ctrl+c按键按下,如果有的话,取消当前命令执行。
return 0;    /* if stopped then not repeatable */
}

return rc ? rc : repeatable;
}
2.9.1.2 进入find_cmd()函数分析,如何查找命令
cmd_tbl_t *find_cmd (const char *cmd) //*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;
len = ((p = strchr(cmd, '.')) == NULL) ? strlen (cmd) : (p - cmd); //查找'.'这个字符,用来获得当前字符串命令长度len

for (cmdtp = &__u_boot_cmd_start; 
cmdtp != &__u_boot_cmd_end; 
cmdtp++) 
//上面的__u_boot_cmd_start和__u_boot_cmd_end在board/100ask24x0/u-boot.lds连接脚本里已定义.
//所以for循环是将*cmd入口参数从所有命令起始段找到命令结束段,直到找到为止。
{
if (strncmp (cmd, cmdtp->name, len) == 0) { //比较len长度的*cmd和cmdtp->name,若相等表示找到一样的实际命令。
if (len == strlen (cmdtp->name)) //再次获取实际命令的长度,判断是否和当前命令的长度一样。
return cmdtp;    //已找到,返回实际命令的结构体 
cmdtp_temp = cmdtp;    /* abbreviated command ? */
n_found++;
}
}

if (n_found == 1) {    /* exactly one match */
return cmdtp_temp;
}

return NULL;    /* not found or ambiguous command */
}

2.9.2 命令定义分析,分析命令是怎么定义出来的

例如:"boodcmd=nand read.jffs2 0x30007FC0 kernel;bootm 0x30007FC0 "中bootm命令(定义过程,如何定义的)分析:

2.9.2.1 “bootm 0x30007FC0” 是使用bootm命令,参数为0x30007FC0, 该命令位于Cmd_bootm.C

先搜索bootm命令,位于./common/Cmd_bootm.C (命令文件都存在common文件里,Cmd_bootm.C就是定义bootm命令的文件)
进入./common/Cmd_bootm.C:

其中执行bootm这个命令时所对应的函数就是:

int do_bootm (cmd_tbl_t *cmdtp, int flag, int argc, char *argv[]);

这个do_bootm函数等于bootm命令里的宏U_BOOT_CMD->cmd
do_bootm()成员:

cmd_tbl_t *cmdtp :  指向命令段里的bootm命令结构体
flag   :      参数
argc   :      参数个数,例如"bootm 0x30007FC0",那么argc=2。
argv   :      存参数的数组,共argc个.例如"bootm 0x30007FC0",那么argv[0]="bootm",argv[1]="0x30007FC0".

它在U_BOOT_CMD宏里,是因为每个命令都是通过U_BOOT_CMD宏定义调用的,如下:

U_BOOT_CMD(                            //U_BOOT_CMD宏里有bootm成员,CFG_MAXARGS成员等
bootm,    CFG_MAXARGS,    1,    do_bootm,                 // do_bootm是一个函数名
"bootm - boot application image from memory\n",
"[addr [arg ...]]\n - boot application image stored in memory\n" //usage成员,较短的帮助说明
"\tpassing arguments 'arg ...'; when booting a Linux kernel,\n"
"\t'arg' can be the address of an initrd image\n"
#ifdef CONFIG_OF_FLAT_TREE
"\tWhen booting a Linux kernel which requires a flat device-tree\n"
"\ta third argument is required which is the address of the of the\n"
"\tdevice-tree blob. To boot that kernel without an initrd image,\n"
"\tuse a '-' for the second argument. If you do not pass a third\n" 
"\ta bd_info struct will be passed instead\n" //help成员,详细的帮助说明
#endif 
);
2.9.2.2 再来看看U_BOOT_CMD宏是怎么定义的,宏U_BOOT_CMD在./include/command.h定义,如下所示:
#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}

这里定义了全局变量:U_BOOT_CMD(name,maxargs,rep,cmd,usage,help)等于"cmd_tbl_t _u_boot_cmd##name Struct_Section={#name, maxargs, rep, cmd, usage, help}"

2.9.2.3 其中U_BOOT_CMD宏各个参数意义如下:

cmd_tbl_t: 定义__u_boot_cmd_bootm的类型为cmd_tbl_t结构体
##name: 指向name,其中name指向第2.1节里面U_BOOT_CMD宏里的第一个成员 bootm.
Struct_Section: 保存命令的段位置参数,在./include/Command.h中定义, “#define Struct_Section attribute ((unused,section (".u_boot_cmd")))”,section表示强制将 段属性设为.u_boot_cmd命令段
#name: U_BOOT_CMD宏里第一个成员,命令名字,等于"bootm"
maxargs: 最大参数个数,等于CFG_MAXARGS
rep: [repeat]是否支持重复,等于1,表示支持
cmd: 执行命令后对应的函数指针,执行命令时就会使用该指针 , 在2.1节里,
usage:保存字符串,用于较短的帮助说明,等于上面代码里"bootm - boot application image from memory\n"
help:用于详细的帮助说明,等于U_BOOT_CMD宏里usage成员后剩下的几行字符串 (它们之间没有加逗号,所以那几行字符串都是连接在一起的).

2.9.2.4所以对于bootm命令,最终扩展开为: cmd_tbl_t __u_boot_cmd_bootm attribute ((unused,section (".u_boot_cmd"))) ={bootm, CFG_MAXARGS, 1, do_bootm, “字符串1”,"“字符串2”}

所有uboot中命令定义都是通过 U_BOOT_CMD宏 保存在.u_boot_cmd段中,通过run_command()函数调用.
接下来学习怎么仿照bootm命令来制作hello命令。

2.10 仿照bootm制作hello命令

仿照bootm命令生成来制作一个hello命令,功能:打印出hello,world!和参数值。

步骤如下:

  1. 点击New File,创建cmd_hello.c

将./common/cmd_bootm.c的头文件复制到 cmd_hello.c中. 因为cmd_bootm.c的头文件都是包括的命令相关的文件):

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
  1. 点击保存

保存在./common文件下,(命令文件都存在common文件里)

  1. 写执行命令需要调用的函数:

复制./common/cmd_bootm.c里的:

int do_bootm (cmd_tbl_t *cmdtp, int flag, int argc, char *argv[])
{
}

改成:

int do_hello (cmd_tbl_t *cmdtp, int flag, int argc, char *argv[]) //执行命令需要调用的函数
{
int i;
printf ("hello,world!,arg_numble=%d\n",argc); //打印"hello,world!"和参数个数arg_numble
for(i=0;i<argc;i++)
printf("argv[i]=%s",i,arg[i]); //打印命令名字和参数值

return 0;
}
  1. 添加U_BOOT_CMD宏(实现:通过U_BOOT_CMD宏来将命令保存在.u_boot_cmd段里):
U_BOOT_CMD(
	hello, 			//命令名
	CFG_MAXARGS, 	//参数最大值
	1, 				//支持重复使用命令
	do_hello, 		//函数指针,用于命令执行时需要调用什么函数,就是第2节的do_hello函数
	"hello - just for help...", 	//短的使用说明
	"hello - long help... ..." 		//长的使用说明,敲打"help hello"命令,就会出现这段字符串
);
  1. 将cmd_hello.c复制到虚拟机中u-boot-1.1.6/common目录下**

  2. 进入common目录,输入"vi mkfine" 修改conmon目录下mkfine,在mkefine第54行,COBJS里添加cmd_hello.o文件**

  3. 输入"make",生成u-boot.bin文件重新下载就可以使用hello命令了

  4. cmd_hello.c源码:

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

int do_hello (cmd_tbl_t *cmdtp, int flag, int argc, char *argv[]) //执行命令需要调用的函数
{
int i;
printf ("hello,world!,arg_numble=%d\n",argc); //打印"hello,world!"和参数个数arg_numble 
for(i=0;i<argc;i++)
printf("argv[i]=%s",i,arg[i]); //打印参数

return 0;
}
U_BOOT_CMD(
hello,    //命令名
CFG_MAXARGS, //参数最大值
1,    //支持重复使用命令
do_hello, //函数指针,用于命令执行时需要调用什么函数,就是第2节的do_hello函数
"hello - just for help...\n", //短的使用说明
"hello - long help... ...\n" //长的使用说明,敲打"help hello"命令,就会出现这段字符串 
#endif
);

2.11 通过nand命令读内核

本节主要讲解:详细分析UBOOT中bootcmd=nand read.jffs2 0x30007FC0 kernel;bootm 0x30007FC0怎么实现nand命令读内核.

命令行:bootcmd=nand read.jffs2 0x30007FC0 kernel的执行主要有两个步骤:

步骤a: 从NAND FILSHE中kernel分区读出
步骤b: 放到0x30007FC0去

  1. kernel分区: 是flash中的内核区

其中在flash中定义了4大分区:

bootloader 一开机直接运行u-boot
boot parameters 存放一些可以设置的参数,供u-boot使用
kernel 存放内核区
root filesystem 根文件系统,挂载(mount)后才能使用文件系统中的应用程序

这几个分区通过配置文件已在flash地址上是写好了,位于 u-boot-1.1.6/include/configs/100ask24x0.h

#define MTDIDS_DEFAULT "nand0=nandflash0"
#define MTDPARTS_DEFAULT "mtdparts=nandflash0:256k@0(bootloader)," \
"128k(params)," \
"2m(kernel)," \
"-(root)"

在100ask24x0.h里定义了一个MTDPARTS_DEFAULT宏定义,

  • “mtdparts=nandflash0:” 表示mtdparts分区位于nandflash上
  • “256k@0(bootloader),” 表示从0开始共256kb是bootloader分区
  • “128k(params),” 表示接下来128kb用来存放参数,是params分区
  • “2m(kernel),” 表示接下来2Mb用来存放内核,是kernel分区
  • “-(root)” 表示剩下的容量存放根文件系统,是root分区
  1. 可以通过在uboot界面输入"mtd"命令,查看4个分区的位置情况:
#: name 		size 		offset 		mask_flags
0:bootloader 0X00040000 	0X00000000 		0
1:params 	 0X00020000 	0X00040000 		0
2:kernel	 0X00200000 	0X00060000 		0
3:root		 0X0fda0000 	0X00260000 		0

从上面可以看出bootloader基地址是0x0000 0000,该分区大小为0x0004 000,所以结束地址为0X0003 FFFF。
为什么0X00040000等于256kb?
因为在ARM920t中,每隔4个地址保存了一个32位数据(4个字节), 所以0X00040000=0X00040000个字节=0x100(256)*0x400(1024)=256Kb

  1. 所以 nand read.jffs2 0x30007FC0 kernel 最终扩展开为:
    nand read.jffs2 0x30007FC0 0X00060000 0X00200000

  2. nand命令位于./common/cmd_nand.c(所有命令文件都是存在common中,以cmd_xx.c形式保存)

    其中nand命令执行时调用的是do_nand(cmd_tbl_t * cmdtp, int flag, int argc, char *argv[])函数
    进入do_nand()函数:

    int do_nand(cmd_tbl_t * cmdtp, int flag, int argc, char *argv[])
    { 
    int i, dev, ret;
    ulong addr, off, size;
    char *cmd, *s;
    nand_info_t *nand;
    int quiet = 0;
    const char *quiet_str = getenv("quiet"); //获取环境变量quiet
    
    
    if (argc < 2) //判断nand命令参数个数若小于2,将goto到usage,打印cmdtp->usage(nand命令短的帮助说明)
    goto usage;
    ...
    cmd = argv[1]; //cmd="read.jffs2"
    ...
    if (strcmp(cmd, "info") == 0) //cmd不等于"info",不执行
    {...}
    ...
    if (strcmp(cmd, "bad") != 0 && strcmp(cmd, "erase") != 0 &&
    strncmp(cmd, "dump", 4) != 0 &&
    strncmp(cmd, "read", 4) != 0 && strncmp(cmd, "write", 5) != 0 &&
    strcmp(cmd, "scrub") != 0 && strcmp(cmd, "markbad") != 0 &&
    strcmp(cmd, "biterr") != 0 &&
    strcmp(cmd, "lock") != 0 && strcmp(cmd, "unlock") != 0 )
    goto usage; //若argv[1]都不满足的话,表示使用命令在语法上有错误,打印短的帮助说明
    ....
    if (strncmp(cmd, "read", 4) == 0 || strncmp(cmd, "write", 5) == 0) //cmd==read为真
    {
    int read;
    
    if (argc < 4) // "nand read.jffs2 0x30007FC0 0X00060000 0X00200000"共5个参数,这里不执行
    goto usage;
    
    addr = (ulong)simple_strtoul(argv[2], NULL, 16); //将argv[2]的"0x30007FC0"字符型转换成数值型
    
    read = strncmp(cmd, "read", 4) == 0; 
    //strncmp():判断cmd和"read"前4个字节若相等返回0,不相等返回大于0的数
    //这里cmd与"read"相等,所以strncmp()返回0,read=(0==0)为真,所以read=1 
    printf("\nNAND %s: ", read ? "read" : "write"); //由于read=1,所以打印"\nNAND read:" 
    
    if (arg_off_size(argc - 3, argv + 3, nand, &off, &size) != 0)
    return 1;
    
    s = strchr(cmd, '.'); //strchr():查找'.'字符,若没找到返回NULL。
    if (s != NULL &&
    (!strcmp(s, ".jffs2") || !strcmp(s, ".e") || !strcmp(s, ".i"))) //if为真,argv[1]=read.jffs2
    { 
    if (read) { //read==1,执行if
    /* read */
    nand_read_options_t opts;
    memset(&opts, 0, sizeof(opts));
    opts.buffer = (u_char*) addr; //设置buffer=0x30007FC0
    opts.length = size; //设置size=0X00200000=2097152 byte
    opts.offset = off; //设置offset=0X00060000
    opts.quiet = quiet;
    ret = nand_read_opts(nand, &opts); 
    //nand_read_opts():读取nandflash的kernel分区到 buffer地址,如读取成功返回0
    } else {
    /* write */
    ...
    }
    }
    else if ( s != NULL && !strcmp(s, ".yaffs")){
    ...
    }else if ( s != NULL && !strcmp(s, ".raw")){
    ...
    } else {
    ...
    }
    
    printf(" %d bytes %s: %s\n", size, 
    read ? "read" : "written", ret ? "ERROR" : "OK"); //打印"2097152 bytes read : OK\n"
    
    return ret == 0 ? 0 : 1; //read读取kernel分区成功返回0,失败返回1
    }
    

2.12 启动函数bootm命令

本节主要讲解:
详细分析UBOOT中"bootcmd=nand read.jffs2 0x30007FC0 kernel;bootm 0x30007FC0"
中怎么实现bootm命令启动内核.

其中bootm要做的事情:
a 读取头部,把内核拷贝到合适的地方(0X30008000)
b 在do_boom_linux()中把参数给内核准备好,并告诉内核参数的首地址
c 在do_boom_linux()中最后使用theKernel () 引导内核.
{注意:当在cmd_bootm.C中没有定义宏CONFIG_PPC时,
系统使用./lib_arm/armlinux.C下的do_bootm_linux()函数(本uboot使用的是这个函数).
若定义了该宏,系统会使用./common/cmd_bootm.C下的do_bootm_linux()函数.}

bootm 0x30007FC0
为什么这里是从0x30007FC0启动?

因为Flash上存的内核格式是:uImage  
而uiamge由: 头部(header) + 真正的内核 组成
在下面1.1节中讲到头部占用了64B字节,用来存放各个参数变量,所以真正的内核加载地址是在:
真正的内核开始地址=0x30007FC0+64=0X30008000,所以bootm启动内核地址刚好位于nand命令加载的地址后面,不需要移动.

  1. uImage头部结构体分析

头部:由结构体image_header_t定义,该结构体大小为64B,位于./include/image.h

typedef struct image_header {
uint32_t    ih_magic;    /* Image Header Magic Number(镜像头部幻数,为#define IH_MAGIC    0x27051956    )    */ //幻数:用来标记文件的格式 
uint32_t    ih_hcrc;    /* Image Header CRC Checksum(镜像头部CRC校验码)    */
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(镜像数据CRC校验码)    */
uint8_t    ih_os;       /* Operating System(操作系统类型)    */
uint8_t    ih_arch;     /* CPU architecture(CPU架构)    */
uint8_t    ih_type;     /* Image Type(镜像类型)    */
uint8_t    ih_comp;     /* Compression Type(压缩类型)    */
uint8_t    ih_name[IH_NMLEN];    /* Image Name(镜像名字ih_name,共32字节 #define IH_NMLEN    32)    */
} image_header_t;
  1. bootm命令之do_bootm函数分析 (bootm命令位于./common/cmd_bootm.c,其中nand命令执行时调用的是do_bootm()函数)
int do_bootm (cmd_tbl_t *cmdtp, int flag, int argc, char *argv[])
{
ulong    iflag;
ulong    addr;
ulong    data, len, checksum;
ulong *len_ptr;
uint    unc_len = CFG_BOOTM_LEN; 
int    i, verify;
char    *name, *s;
int    (*appl)(int, char *[]);
image_header_t *hdr = &header; //定义头部结构体指针hdr等于header的地址.

s = getenv ("verify"); //读取uboot环境变量verify
verify = (s && (*s == 'n')) ? 0 : 1; //如果verify==n,局部变量verify=0,否则verify=1.

if (argc < 2) { //如果argc==1(只输入了bootm),则使用缺省加载地址load_addr 
addr = load_addr;
} else { //否则使用argv[1](0x30007FC0)为加载地址
addr = simple_strtoul(argv[1], NULL, 16);
}
SHOW_BOOT_PROGRESS (1);
printf ("## Booting image at %08lx ...\n", addr); //打印"## Booting image at 0x30007FC0 ...\n" 

#ifdef CONFIG_HAS_DATAFLASH
if (addr_dataflash(addr)){
read_dataflash(addr, sizeof(image_header_t), (char *)&header);
} else
#endif
memmove (&header, (char *)addr, sizeof(image_header_t)); 
//在加载地址中前64B大小的头部结构体提取到image_header_t结构变量header中,为下面的分析校验做准备

if (ntohl(hdr->ih_magic) != IH_MAGIC) //判断幻数Magic number 是否匹配,不匹配说明下载过程中错误.
{
...
} else
#endif    /* __I386__ */
{
puts ("Bad Magic Number\n");
SHOW_BOOT_PROGRESS (-1);
return 1;
}
}
SHOW_BOOT_PROGRESS (2);

data = (ulong)&header; 
len = sizeof(image_header_t);

checksum = ntohl(hdr->ih_hcrc);
hdr->ih_hcrc = 0;

if (crc32 (0, (uchar *)data    , len) != checksum) { //判断校验和
puts ("Bad Header Checksum\n");
SHOW_BOOT_PROGRESS (-2);
return 1;
}
SHOW_BOOT_PROGRESS (3);

....

#if defined(__PPC__) //判断体系结构,校验CPU类型是否正确
if (hdr->ih_arch != IH_CPU_PPC)
#elif defined(__ARM__)
if (hdr->ih_arch != IH_CPU_ARM)
#elif defined(__I386__)
if (hdr->ih_arch != IH_CPU_I386)
#elif defined(__mips__)
if (hdr->ih_arch != IH_CPU_MIPS)
#elif defined(__nios__)
if (hdr->ih_arch != IH_CPU_NIOS)
#elif defined(__M68K__)
if (hdr->ih_arch != IH_CPU_M68K)
#elif defined(__microblaze__)
if (hdr->ih_arch != IH_CPU_MICROBLAZE)
#elif defined(__nios2__)
if (hdr->ih_arch != IH_CPU_NIOS2)
#elif defined(__blackfin__)
if (hdr->ih_arch != IH_CPU_BLACKFIN)
#elif defined(__avr32__)
if (hdr->ih_arch != IH_CPU_AVR32)
#else
# error Unknown CPU type //没有找到CPU类型
#endif
...
switch (hdr->ih_type) //判断镜像image类型
{ ...}

switch (hdr->ih_comp) //根据镜像压缩(compression)类型把内核镜像解压到指定的地址
{
case IH_COMP_NONE: //使用的是没有压缩,执行该段case
if(ntohl(hdr->ih_load) == data) //该data内核地址刚好位于ih_load加载地址,不需要移动,直接运行
{ 
printf (" XIP %s ... ", name); //打印
} 
else //else执行内核移动,将内核data地址移到 hdr->ih_load (加载地址)中
{ ...
memmove ((void *) ntohl(hdr->ih_load), (uchar *)data, len); 
...}    
break;
case IH_COMP_GZIP:
....
}
...
switch (hdr->ih_os) //根据不同的操作系统类型来启动内核
{ 
case IH_OS_LINUX: //LINUX系统,执行该段case
#ifdef CONFIG_SILENT_CONSOLE
fixup_silent_linux();
#endif
do_bootm_linux(cmdtp, flag, argc, argv,addr, len_ptr, verify); //执行do_bootm_linux()函数启动内核
break;
case IH_OS_NETBSD: //NETBSD系统 
....
....
}

do_bootm()函数若执行无误,最终会执行do_bootm_linux()函数

  1. bootm命令之do_bootm_linux函数分析
    进入do_bootm_linux()函数(位于./lib_arm/armlinux.C) :
void do_bootm_linux (cmd_tbl_t *cmdtp, int flag, int argc, char *argv[],ulong addr, ulong *len_ptr, int verify) 
{
void (*theKernel)(int zero, int arch, uint params); //定义一个函数指针theKernel
... ...
theKernel = (void (*)(int, int, uint))ntohl(hdr->ih_ep); //1.设置theKernel地址=ih_ep镜像入口地址,用于后面启动内核
... ...
char *commandline = getenv ("bootargs"); 
//commandline指向"bootargs"命令环境参数. 用于后面setup_commandline_tag的形参
//在本uboot界面中输入print指令就能得到"bootargs=noinitrd root=/dev/mtdblock3 init=/linuxrc console=ttySAC0" 
//root=/dev/mtdblock3:表示根文件系统root位于第4个flsh分区(mtdblock3), mtdblock0=bootloader,mtd1=参数,mtd2=内核 
//init=/linuxrc:指定内核启动后运行的第一个脚本是当前目录下linuxrc脚本
//console=ttySAC0:指定选择串口0(ttySAC0)来打印信息、
... ...

/*2.设置tag 参数*/ 
setup_start_tag (bd);           //在0X30000100地址保存start_tag数据,tag:用于u-boot给Linux kernel传递参数数据,因为内核启动后不能使用uboot了.
setup_memory_tags (bd);                   //保存memory_tag数据,让LINUX知道内存多大
setup_commandline_tag (bd, commandline); //保存commandline_tag数据
setup_end_tag (bd);                      //初始化tag结构体结束
....
cleanup_before_linux ();                   //3.启动内核之前需要做一些清理工作,禁止中断,关闭cache

theKernel (0, bd->bi_arch_number, bd->bi_boot_params); 
//4.通过ih_ep镜像入口地址启动内核,然后从0X30000010处读取tag参数,
//其中"bd->bi_arch_number"参数是向内核传递的机器ID,用于内核确定机器ID是否正确,    bd->bi_arch_number是在start_armboot函数中board_init里赋了值

}

从以上代码中可以看出启动内核之前主要执行了两步骤:

A 通过setup_…_tag函数为内核准备参数,

B 进入cleanup_before_linux函数清除中断和cache

  1. tag参数函数分析
  • d setup_start_tag (bd)函数分析如下: (在上面的tag结构体的首地址为什么在0X30000100?)

通过搜索"setup_start_tag"得到该函数位于./lib_arm/armlinux.c中:

static void setup_start_tag (bd_t *bd)
{
params = (struct tag *) bd->bi_boot_params; 

//初始化(struct tag *)型全局变量params= bd->bi_boot_params=0x30000100,
// 之后的memory_tag和commandline_tag等tag数据都保存在params后面的偏移地址. 
params->hdr.tag = ATAG_CORE; //存放srat常量:params->hdr.tag = ATAG_CORE=0x54410001, tag表示tag类型的常量。 
params->hdr.size = tag_size (tag_core); //存放srat长度:params->hdr.size=5, size表示start_tag的结构大小。
//因为tag_size (tag_core)=((sizeof(struct tag_header) + sizeof(struct tag_core)) >> 2)
//其中tag_header结构体里有2个4字节成员(size,tag),
//tag_core结构体里有3个4字节成员(flags,pagesize,rootdev)
//所以tag_size (tag_core)=(2*4+3*4)>>2=5; 单位是4字节
params->u.core.flags = 0;    //存放params的(tag_core型)结构体成员u.core.flags=0
params->u.core.pagesize = 0;//存放params的(tag_core型)结构体成员u.core.pagesize=0
params->u.core.rootdev = 0;//存放params的(tag_core型)结构体成员u.core.rootdev=0

params = tag_next (params); //params指向下一个tag(setup_memory_tags),params=(0x30000100+size*4)=0x30000114 
}

通过上面代码,最终内存分布为:

Uboot代码结构详细分析_第8张图片

  • do_bootm_linux函数中setup_memory_tags(bd)函数分析如下:
static void setup_memory_tags (bd_t *bd) 
{
int i;

for (i = 0; i < CONFIG_NR_DRAM_BANKS; i++) {
params->hdr.tag = ATAG_MEM; //存放内存tag常量: params->hdr.tag =ATAG_MEM= 0x54410002 
params->hdr.size = tag_size (tag_mem32); //存放内存长度:params->hdr.size =4 (len+ATAG_MEM+u.mem.size+u.mem.start)

params->u.mem.start = bd->bi_dram[i].start; 
//存放内存(sdram)的的首地址,
// bd->bi_dram[i].start在start_armboot()函数中init_sequence->dram_init结构函数成员里被复制:
// gd->bd->bi_dram[0].start = PHYS_SDRAM_1;其中"PHYS_SDRAM_1"在./include/configs/100ask24x0.h中定义为0X30000000(bank6首地址)
//所以,这里存放内存(sdram)首地址:params->u.mem.start =0X30000000; 
params->u.mem.size = bd->bi_dram[i].size;
//同上,gd->bd->bi_dram[0].size = PHYS_SDRAM_1_SIZE;"PHYS_SDRAM_1_SIZE"被定义为0X04000000(64Mb)
//所以,这里存放内存(sdram)长度: params->u.mem.size=0X04000000; 
params = tag_next (params); //params指向下一个tag(setup_commandline_tag),params=(0x30000114+size*4)=0x30000124 
}
}

通过上面代码,最终内存分布为:

Uboot代码结构详细分析_第9张图片

  • do_bootm_linux函数中setup_commandline_tag(bd)函数分析如下:
static void setup_commandline_tag (bd_t *bd, char *commandline) //commandline:指向"bootargs"命令环境参数
{
char *p;

if (!commandline) // 判断bootargs是否为空,
return;


for (p = commandline; *p == ' '; p++); //去掉空格

if (*p == '\0') //判断*p是否为空
return;

params->hdr.tag = ATAG_CMDLINE; //存放命令行产量: params->hdr.tag =ATAG_MEM= 0x54410009 
params->hdr.size =
(sizeof (struct tag_header) + strlen (p) + 1 + 4) >> 2; //存放命令行长度 params->hdr.size 
/* 其中 strlen (p) + 1 + 4: +1表示添加结束符'/0' */
/* +4 表示向上取整,比如当len=(4,5,6,7)时,size=(len+4)>>2=2; 实现4字节对齐 */

strcpy (params->u.cmdline.cmdline, p); 
//存放命令行参数:params->u.cmdline.cmdline=boottargs=noinitrd root=/dev/mtdblock3 init=/linuxrc console=ttySAC0

params = tag_next (params); //params指向下一个tag(setup_end_tag)
}

通过上面代码,最终内存分布为:

Uboot代码结构详细分析_第10张图片

  • do_bootm_linux函数中setup_end_tag (bd)函数分析如下:
static void setup_end_tag (bd_t *bd)  
{
params->hdr.tag = ATAG_NONE; //params->hdr.tag =ATAG_NONE=0
params->hdr.size = 0; //size=0
}

通过上面代码,最终内存分布为:

Uboot代码结构详细分析_第11张图片

  • 进入cleanup_before_linux函数清除中断和cache(./arm920t/cpu/cpu.c):
int cleanup_before_linux (void)
{
unsigned long i;

disable_interrupts (); //禁止中断
/* turn off I/D-cache */ //关闭 指令Icache和数据Dcache
asm ("mrc p15, 0, %0, c1, c0, 0":"=r" (i));
i &= ~(C1_DC | C1_IC);
asm ("mcr p15, 0, %0, c1, c0, 0": :"r" (i));

/* flush I/D-cache */
i = 0;
asm ("mcr p15, 0, %0, c7, c7, 0": :"r" (i));

return (0);
}

2.13 八看编译测试

  • 1 编译smdk2440开发板的启动程序

    • make distclean
      make smdk2440_config
      make
      
  • 2 编译成功后,使用EasyOpen-JTAG将其烧录到NorFlash中,但由于这种方式非常耗时,所以建议使用开发板或芯片提供商官方提供的正常运行的U-BOOT程序的USB下载方式进行烧录。

    • 1 连接EasyOpenJTAG:EasyOpenJTAG 和电脑的 USB相连,将 EasyOpenJTAG 的 10pin 的 JTAG 线插入 开发板的有凸槽那端的 JTAG 口,接上开发板电源线,开启电源。

    • Uboot代码结构详细分析_第12张图片

    • 2 使用 oflash 命令烧写程序

      • (1) 把要烧写的文件 uboot.bin拷贝到 D 盘根目录。

      • (2) “Win + R”组合键打开 Windows 命令窗口并输入cmd, (2) 先在窗口输入“d:”切换到 D 盘根目录,然后输入“dir”,查看当前路径中的文件, 确认要烧录的uboot.bin文件已存在与当前目录。

      • (3) 输入“oflash uboot.bin”命令,即可启动 oflash 并选择烧写程序。

        • ① 选择 JTAG 类型,这里选择“0:OpenJTAG”,因此输入“0”并回车;
        • ② 选择 CPU 类型,这里选择“1:S3C2440”, 因此输入“1”并回车;
        • ③ 选择烧写位置,这里选择“0:Nand Flash prog”, 因此输入“0”并回车;
        • ④ 选择烧写内容,这里选择“0:Nand Flash Program”, 因此输入“0”并回车;
        • ⑤ 选择烧写起始地址,直接输入“0“并回车;

        Uboot代码结构详细分析_第13张图片

  • 3 利用USB下载方式烧录自己编译的uboot文件

    • ① 然后用 2 条 USB线连接 PC 和 开发板:一条是 USB串口、另一条是 DNW 设备(USB文件传输)。

    • ②打开串口工具,然后上电启动开发板,在串口工具中按住空格,使 UBOOT启动前进入菜单模式,并键入“q”退出菜单模式,进入命令模式。

    • ③ Windows下打开dnw程序,并在串口工具命令行中输入usb 1 30000000命令,准备下载自己编译的uboot.bin。

    • Uboot代码结构详细分析_第14张图片

    • 4)打开 dnw_100ask.exe,可以看到类似下图(标题栏中显示“USB:OK”时,才可以使用 USB下载):

    • Uboot代码结构详细分析_第15张图片

    • 5)依次选择菜单栏的USB Port => Transmit,在弹出的对话框中选择需要下载的我们自己编译的uboot.bin文件。

    • 6)观察串口工具,如果提示“RECEIVED FILE SIZE: …. ”表示文件已经下载到开发板指定的内存地址处(0x30000000)。

    • 7)依次执行如下指令,将文件从内存移动到Nor Flash中:

      • protect off all
        erase 0 7ffff
        cp.b 30000000 0 80000
        
    • 8)重启开发板(Nor Flash启动),观察我们自己编译的uboot运行情况:

Uboot代码结构详细分析_第16张图片

2.14 九看运行结果

可以看到自己移植的uboot运行起来了,但是串口仍然有乱码,说明波特率设置有问题,后续进行修改串口设置:

1 修改串口设置

  • 在文件drivers/serial/serial_s3c24x0.c中找到串口配置函数serial_init,进一步查找,同样在该文件中有serial_init_dev,该函数用来初始化串口设备,该函数末尾跳转到_serial_setbrg。

  • _serial_setbrg函数同样在该文件中调用get_PCLK函数来计算值,如下:

    	/* value is calculated so : (int)(PCLK/16./baudrate) -1 */
    	reg = get_PCLK() / (16 * gd->baudrate) - 1;
    

    查看get_PCLK函数,跳转到文件arch/arm/cpu/arm920t/s3c24x0/speed.c中,可以看到,该函数调用了get_HCLK:

    /* return PCLK frequency */
    ulong get_PCLK(void)
    {
    	struct s3c24x0_clock_power *clk_power = s3c24x0_get_base_clock_power();
    
    	return (readl(&clk_power->clkdivn) & 1) ? get_HCLK() / 2 : get_HCLK();
    }
    

    get_HCLK函数同样在该文件中,但是从源码可以看到,只有定义了宏CONFIG_S3C2440,该段代码才有效(整个文件还需要开启宏CONFIG_S3C24X0):

    /* return HCLK frequency */
    ulong get_HCLK(void)
    {
    	struct s3c24x0_clock_power *clk_power = s3c24x0_get_base_clock_power();
    #ifdef CONFIG_S3C2440
    	switch (readl(&clk_power->clkdivn) & 0x6) {
    	default:
    	case 0:
    		return get_FCLK();
    	case 2:
    		return get_FCLK() / 2;
    	case 4:
    		return (readl(&clk_power->camdivn) & (1 << 9)) ?
    			get_FCLK() / 8 : get_FCLK() / 4;
    	case 6:
    		return (readl(&clk_power->camdivn) & (1 << 8)) ?
    			get_FCLK() / 6 : get_FCLK() / 3;
    	}
    #else
    	return (readl(&clk_power->clkdivn) & 2) ? get_FCLK() / 2 : get_FCLK();
    #endif
    }
    

1、修改include/configs/smdk2440.h文件:注释掉原CONFIG_S3C2410,添加CONFIG_S3C2440

Uboot代码结构详细分析_第17张图片

2、修改arch/arm/cpu/arm920t/start.S中配置时钟分频系数的代码:

Uboot代码结构详细分析_第18张图片

3、编译之后发现nand文件和yaffs2文件系统有问题,这里暂且先不使用它们,在单板配置文件smdk2440.h中屏蔽掉相关宏定义:

Uboot代码结构详细分析_第19张图片

Uboot代码结构详细分析_第20张图片

4、再次编译,没有问题,下载到开发板的Nor Flash中,查看串口输出,可以正常打印CPU信息(比如时钟信息),但是接下来uboot程序提示flash出错,并且死机。

Uboot代码结构详细分析_第21张图片

2 修改Nor Flash设置

下面讲述如何定位问题,解决问题,使uboot支持nor flash读写。定位出错问题所在的方法很简单,定位到红色的日志信息 “Flash:” 在程序中的位置即可,这个内容搜索在VS Code全局搜索中很慢,所以沿着uboot启动过程寻找。

  • 在arch/arm/lib/board.c文件中找到,在函数board_init_r中,也就是uboot启动的第2阶段。在代码中可以看到:首先打印出日志信息“Flash:”,然后调用 flash_ini t获取 flash_size,接着对获取到的flash_size进行判断,如果flash_size等于0,则再打印“failed”日志,并且调用hang函数,挂起CPU,即死机。那么,初步定位,问题就在于 flash_init 函数有问题,进入该函数查看其源码。

image-20210619231152710

  • flash_init 函数的源码定义在drivers/mtd/cfi_flash.c文件中,在该函数中,重点代码(2184-2186)如图:

Uboot代码结构详细分析_第22张图片

uboot首先调用flash_detect_legacy函数获取Flash的CFI信息,如果获取失败,则调用flash_get_size 函数获取CFI信息,获取之后,将CFI信息中的Flash 大小赋值给size变量,然后返回。

所以,目前问题变为:这两种获取Flash中CFI信息的方法中出现了错误,没有获取到CFI信息。

  • 第一种方法:flash_detect_legacy

    • 通过跳转到其定义,可以看到源码是通过宏定义CONFIG_FLASH_CFI_LEGACY配置的,在smdk2440.h文件中搜索一下,可以看到被定义,所以flash_detect_legacy相关方法启用。
    • 在函数的源码中,有一句调试信息(1698-1701)通过debug语句输出,可以通过该条语句来查看读取出的三个ID值。默认情况下,debug函数是被关闭的, 在该文件最开始开启debug的宏定义,即添加#define DEBUG语句。
    • Uboot代码结构详细分析_第23张图片
  • 再次编译uboot,并下载u-boot.bin到Nor Flash去,启动,查看串口输出结果

Uboot代码结构详细分析_第24张图片

在输出的调试信息中可以看到,Flash的CFI被读出来了,但Flash仍然失败。在调试信息中可以看到,读取出的ID信息为c2 2249 0,查看Nor Flash芯片手册可以看到,0xc2为Manifacture ID,0x2249为Device ID,读出的信息是正确的,但是为什么系统仍然提示Flash出现错误呢?

问题就在于,JEDEC这种古老的标准,它在读取出 ID 信息之后,跑去和Flash库(一个数组)中的信息进行对比匹配,如果匹配到了,就取出预先已经定义好的信息,比如Flash大小之类的信息,如图中红框所示:

Uboot代码结构详细分析_第25张图片

jedec_flash_match函数定义在drivers/mtd/jedec_flash.c文件中,源码如图:

Uboot代码结构详细分析_第26张图片

翻译一下这个函数的注释:将jedec id 和table相比进行匹配,如果匹配到值,则填充整个flash_info结构体!

所以,读取到的ID值正确,但是系统仍然报错Flash的情况只有一个:预先设置的table中不存在目前的FlashID所匹配的信息。

  • 添加板载Nor Flash的jedec信息

    jedec_table同样定义在文件drivers/mtd/jedec_flash.c中。

    首先是类似于CONFIG_SYS_FLASH_LEGACY_256Kx8 这样的宏定义,对Flash进行大概的分类,JZ2440开发板板载Nor Flash型号是MX29LV160DBTI-70G,大小是16M-bit,换算是字节大小就是2MB

    在jedec_table的最后发给仿照其它信息,添加本款Flash的jedec信息:

    • mfr_id:该信息为Manifacture ID,在文件include/flash.h中Device IDs段落中定义,这里使用MX的:
    #define MX_MANUFACT 0x00C200C2 /* MXIC  manuf. ID in D23..D16, D7..D0 */
    
    • **dev_id(器件ID)*器件ID在文件drivers/mtd/jedec_flash.c中宏定义,添加本款Flash的:
    //JZ2440开发板板载Nor Flash
    #define MX29LV160DBTI 0x2249
    
    • name:随便填
    • uaddr:(unlock addr,解锁地址)使用16位的解锁地址宏定义 MTD_UADDR_0x0555_0x02AA 即可。
    • DevSize:(期间容量),容量大小也在drivers/mtd/jedec_flash.c文件中设置,这里选择SIZE_2MiB。
    • CmdSet:(使用的指令集)。使用默认的CFI_CMDSET_AMD_LEGACY即可。
    • NumEraseRegions:(擦除区域种类个数)本款Flash芯片的擦除扇区结构总共有4种,如图:

    Uboot代码结构详细分析_第27张图片

    • regions:使用ERASEINFO()宏定义填写上面擦除区域种类的详细信息,该宏定义的格式如下:

    • ERASEINFO(size,blocks)	//表示sIze大小(单位是字节)的扇区有blocks个
      
  • 综合以上分析,最后填充的本款Flash信息为(添加到文件drivers/mtd/jedec_flash.c中的jedec_table中):

/* JZ2440开发板板载NoR FLASH */
#ifdef CONFIG_SYS_FLASH_LEGACY_1024Kx16
	{
		.mfr_id		= (u8)MX_MANUFACT,
		.dev_id		= MX29LV160DBTI,
		.name		= "MX MX29LV160DBTI",
		.uaddr		= {
			[1] = MTD_UADDR_0x0555_0x02AA /* x16 */
		},
		.DevSize		= SIZE_2MiB,
		.CmdSet			= CFI_CMDSET_AMD_LEGACY,
		.NumEraseRegions	= 4,
		.regions		= {
			ERASEINFO(16*1024, 1),
			ERASEINFO(8*1024, 2),
			ERASEINFO(32*1024, 1),
			ERASEINFO(64*1024, 31),
		}
	},
#endif
  • 开启这款Flash的宏定义

Uboot代码结构详细分析_第28张图片

  • 编译下载看结果

Uboot代码结构详细分析_第29张图片

可以看到flash检测不会卡死,uboot正常启动到命令行界面,但是仍然有一个报错信息,根据报错信息在文件中搜索,找到该行日志的打印位置,可以看到,当扇区数量大于宏定义CONFIG_SYS_MAX_FLASH_SECT时,系统就会打印出这行日志信息:

Uboot代码结构详细分析_第30张图片

因为该款Flash实际扇区是35个,所以到该宏定义在单板配置文件(include/configs/smdk2440.h)中,修改该宏定义为36个。

Uboot代码结构详细分析_第31张图片

  • 关闭调试信息,修改前导符

因为,Flash检测没有任何问题,uboot正常启动到命令行界面,并且命令可以正常使用,调试完毕,不用输出调试信息,所以将drivers/mtd/cfi_flash.c文件中开头添加的宏定义(#define DEBUG)关掉。

在单板配置文件include/configs/smdk2440.h中130行左右修改宏定义,将#define CONFIG_SYS_PROMPT "SMDK2410"改为我们需要显示的信息:#define CONFIG_SYS_PROMPT "JZ2440"

  • 再编译看结果

    可以使用uboot命令flinfo查看当前flash存储器信息。

    Uboot代码结构详细分析_第32张图片

    扇区起始地址与数据手册相对应(RO是uboot软件中指定的,RO标示出的是当前程序存储区)

    Uboot代码结构详细分析_第33张图片

3 修改Nand Flash设置

  • 取消Nand Flash屏蔽

在之前初步移植uboot时,发现开启nand flash之后编译不通过,所以屏蔽了nand flash的使用,在单板配置文件include/configs/smdk2440.h中开启(取消注释#define CONFIG_CMD_NAND),然后编译,改正编译错误。

Uboot代码结构详细分析_第34张图片

  • 定位编译出错问题

    • 查看drivers/mtd/nand/s3c2410_nand.c文件的72行:

      Uboot代码结构详细分析_第35张图片

      这个指针有问题的话,就是nand这个结构体变量的定义问题,找到nand变量的定义:

      struct s3c2410_nand *nand = s3c2410_get_base_nand();
      

      接下来问题就变为struct s3c2410_nand这个结构体定义有问题,继续寻找该定义,果然,在文件arch/arm/include/asm/arch-s3c24x0/s3c24x0.h中,我们定义的是CONFIG_S3C2440,所以有struct s3c2440_nand的定义,没有struct s3c2410_nand的定义

      image-20210620161913213

  • 解决问题第一步:添加s3c2440_nand.c文件

    • 在当前目录下复制drivers/mtd/nand/s3c2410_nand.c文件,并改名为s3c2440_nand.c。将所有2410的宏定义,全部替换为2440。将所有函数名和变量名都改为2440版本的。

Uboot代码结构详细分析_第36张图片

对照数据手册,修改NFCONF寄存器和NFCONT寄存器中这些位的值!

  • 修改当前目录下的makefile文件,将s3c2440_nand.c文件加入编译。

Uboot代码结构详细分析_第37张图片

  • 在单板配置文件include/configs/smdk2440.h中配置宏定义CONFIG_NAND_S3C2440

Uboot代码结构详细分析_第38张图片

  • Uboot中Nand Flash操作框架分析

    在uboot中,这些对于nand_flash的操作是基于一套操作框架的,类似于Linux中的设备驱动,所以首先分析一下这套框架,然后修改代码。

    • nand_init函数

    uboot的第二阶段,在 board_init_r 函数中(文件arch/arm/lib/board.c第534行左右)调用了 nand_init() 初始化函数。下面从该函数(在drivers/mtd/nand/nand.c第100行处)入手分析nand flash操作框架。

    Uboot代码结构详细分析_第39张图片

    • nand_init_chip函数:(同样在该文件中),定义如下:

    Uboot代码结构详细分析_第40张图片

    在此处定义了 nand_chip 结构体和 mtd_info 结构体,并分别将指针传递给board_nand_init 函数和 nand_scan 函数,接下来分别分析这两个函数。

    • board_nand_init 函数

    board_nand_init 函数定义在上面我们自己添加的文件drivers/mtd/nand/s3c2440_nand.c中,该函数中主要**使能了nand flash内存控制器,初始化nand flash控制器中的时序参数,最重要的是:初始化传入的nand_chip结构体中的成员。**struct nand_chip结构体在include/linux/mtd/nand.h文件中定义,这个结构体中的成员非常多,其中有非常多的函数指针,这些函数指针在初始化时被设置为指向底层实现的函数,在调用时拉起底层函数。

Uboot代码结构详细分析_第41张图片

在board_nand_init 中初始化该结构体时,别的函数指针都被传入了相应的值,只有 select_chip 函数未被传入具体实现,而是NULL

Uboot代码结构详细分析_第42张图片

接下来继续分析这套框架,看一下 select_chip 函数的调用情况。

  • nand_scan 函数

该函数定义在drivers/mtd/nand/nand_base.c文件中:

Uboot代码结构详细分析_第43张图片

这个函数将mtd结构体指针继续传给了nand_scan_ident函数,接着分析,这个函数同样在该文件中定义,根据函数注释可知,nand_scan_ident 函数会读取Flash ID,并根据读出的ID设置MTD信息,存入mtd结构体中。

Uboot代码结构详细分析_第44张图片

至此,对nand_flash的整套操作框架已经讲解清楚。可以画出这样一个调用关系图:

Uboot代码结构详细分析_第45张图片

  • ​ 接下来分析nand_scan_ident函数中调用的两个函数:

    • nand_set_defaults函数

    该函数定义在文件drivers/mtd/nand/nand_base.c中,主要作用是设置 nand_chip结构体中的默认调用的函数,设置方法如下:

    如果nand_chip结构体中的函数指针已经有值了,则不进行任何操作;

    如果nand_chip结构体中的函数指针为NULL,则赋予默认值。

    之前的分析,select_chip 指针被设置为NULL,所以在该函数中会被设置默认值nand_select_chip:

    Uboot代码结构详细分析_第46张图片

    默认函数 nand_select_chip 也同样定义在该文件中,定义如下

    Uboot代码结构详细分析_第47张图片

    **结论:**从该函数中可以找到问题,**片选函数被调用时,什么都不干,导致nand Flash芯片根本不工作。**解决方法就是:自己重新实现一个正常工作的 nand_select_chip 函数并将函数指针传给nand_flash结构体中的对应成员

    • nand_get_flash_type函数

    该函数主要是读取ID值,根据根据读取到的ID值分析出nand flash类型,存储到 mtd_info 结构体中,该函数同样定义drivers/mtd/nand/nand_base.c文件中:

    Uboot代码结构详细分析_第48张图片

在该函数中,调用select_chip时传入的第二个参数为0,进一步验证了默认的片选函数什么都不干的分析结果。

  • 解决问题:自定义select_chip函数

drivers/mtd/nand/s3c2440_nand.c文件中自定义片选函数,函数定义如下:

static void s3c2440_nand_select_chip(struct mtd_info *mtd, int chipnr)
{
	struct s3c2440_nand *nand = s3c2440_get_base_nand();

	switch (chipnr)
	{
		case -1:
			/* 取消片选 */ 
			writel(readl(&nand->nfcont) | (1 << 1), &nand->nfcont);
			break;
		case 0:
			/* 开启片选 */
			writel(readl(&nand->nfcont) & (~(1 << 1)), &nand->nfcont);
			break;

	default:
		BUG();
	}
}

然后在 board_nand_init 函数中使用该函数指针初始化nand_chip结构体成员,这样就避免它使用不能正常工作的默认值:

Uboot代码结构详细分析_第49张图片

  • 修改board_nand_init()函数

    • 首先设置nand flash控制器时序参数tacls = 0; twrph0 = 1; twrph1 = 0;

    Uboot代码结构详细分析_第50张图片

    • 修改寄存器设置代码:

    Uboot代码结构详细分析_第51张图片

  • 修改s3c2440_hwcontrol()函数

    • 该函数是所有发命令、发地址、发数据都会回调的函数,修改实现如下:
    static void s3c2440_hwcontrol(struct mtd_info *mtd, int cmd, unsigned int ctrl)
    {
    	struct s3c2440_nand *nand = s3c2440_get_base_nand();
    
    	debug("hwcontrol(): 0x%02x 0x%02x\n", cmd, ctrl);
    	
    	if(ctrl & NAND_CLE)
    	{
    		//发命令
    		writeb(cmd, &(nand->nfcmd));
    	}
    	else if(ctrl & NAND_ALE)
    	{
    		//发地址
    		writeb(cmd, &(nand->nfaddr));
    	}
    }
    
  • 关硬件ECC,开软件ECC

    • 这部分代码是使用宏定义CONFIG_S3C2440_NAND_HWECC来控制的:

Uboot代码结构详细分析_第52张图片

  • 所以在单板配置文件include/configs/smdk2440.h中去掉该宏定义CONFIG_S3C2440_NAND_HWECC

Uboot代码结构详细分析_第53张图片

  • 编译下载后观察结果

至此,添加对Nand Flash的支持完成,编译,下载到开发板,在串口查看结果,成功检测出了Nand Flash,并分析出大小为256MB:

Uboot代码结构详细分析_第54张图片

4 添加网卡DM9000支持

JZ2440开发板上板载的是DM9000C网卡,所以我们需要添加对其的支持。因为UBOOT自带的驱动文件夹(drivers/net)中包含有DM9000的文件(dm9000x.c和dm9000x.h),所以只需要在net目录的Makefile文件中,将DM9000的这两个文件加入工程。打开Make file文件:

image-20210620221759494

从makefille中可以看到,只需要配置 CONFIG_DRIVER_DM9000 这个宏定义,dm9000的相关文件就会被加入到工程。但根据经验,需要配置的肯定不止一个宏定义这么简单,全局搜索一下此宏定义,参考别的单板文件davinci_dm355evm.h:

Uboot代码结构详细分析_第55张图片

果然,除了 CONFIG_DRIVER_DM9000 宏定义之外,还有三个地址相关的配置宏定义。

仿照这个配置,在自己的单板配置文件中添加include/configs/smdk2440.h,并将原有CS8900网卡驱动的配置去掉:

Uboot代码结构详细分析_第56张图片

  • 确定宏CONFIG_DM9000_BASE的值

三个地址相关宏定义是参考别的单板配置复制过来的,肯定不能用,查看S3C2440芯片手册和原理图,确定这三个地址。

Uboot代码结构详细分析_第57张图片

首先是基地址CONFIG_DM9000_BASE,在原理图中可以看到DM9000是接在内存控制器上的BANK4上的,由nGCS4控制,进而在S3C2440芯片手册查找BANK4对应的基地址为0x20000000。

DM9000_IO默认配置的是基地址CONFIG_DM9000_BASE,不用修改。

DM9000的地址线和数据线是分离的,但是数据线上可以传输命令或者数据,需要使用信号线CMD区分,查看原理图,方便起见将 LADDR2 信号线接到 CMD 引脚上,当作控制信号来用。

所以当发出的地址中 bit2 为0时,表示数据线上传输的命令,当发出地址中 bit 2 为1时,表示传输的是数据,DM9000_DATA宏定义就表示发数据时地址应该有什么变化,将bit 2置为1即可:

综合以上修改,最后修改结果如图:

image-20210620223334825

  • 设置内存控制器

    根据原理图,网卡DM9000是接到内存控制器的BANK4的,所以需要设置内存控制器中BANK4的位宽参数和时序参数。

    • 设置内存控制器BANK4

      • 设置数据总线位宽(BWSCON 寄存器):

      Uboot代码结构详细分析_第58张图片

      • 设置时序参数(BANKCON4 寄存器):

      Uboot代码结构详细分析_第59张图片

      根据DM9000的时序性能(Tcah = 1 clock),此寄存器的值可以设置为0x00000740

      内存控制器设置在board/samsung/smdk2440/lowlevel_init.S文件中的 lowlevel_init 函数中,接下来开始修改:

      默认B4_BWSCON寄存器的设置是DW16,和DM9000一致,不用修改。

      时序参数宏定义中只需要修改一处即可:

      Uboot代码结构详细分析_第60张图片

  • 修改网卡初始化函数

    在uboot启动的第二阶段board_init_r函数中,网卡初始化调用的是eth_initialize函数:

    Uboot代码结构详细分析_第61张图片

    eth_initialize 函数在net/eth.c文件中,其中又调用了 board_eth_init 函数:

    Uboot代码结构详细分析_第62张图片

    board_eth_init 函数在board/samsung/smdk2440/smdk2410.c文件中,实现如下:

    Uboot代码结构详细分析_第63张图片

    可以看到,当定义CONFIG_CS8900时,会调用其初始化函数,但是此处我们定义的是DM9000,所以无任何操作。修改board/samsung/smdk2440/smdk2410.c文件名为smdk2440.c,然后修改此目录下的Makefile,将smdk2440.c文件加入工程。最后进入文件后修改函数board_eth_init函数:

    Uboot代码结构详细分析_第64张图片

  • 编译下载观察结果

后续添加网络测试

3、uboot的裁剪

目前一步一步移植完成的uboot.bin文件有322KB(太大了),可以进行一定的裁剪,将用不到的代码不加入编译,减小uboot.bin文件的大小。

裁剪方法:修改单板配置文件include/configs/smdk2440.h,去除不必要的宏定义

3.1 去除USB、RTC、BOOTP支持

注释相关宏定义:

Uboot代码结构详细分析_第65张图片

3.2 去除DHCP、DATE、USB命令支持

Uboot代码结构详细分析_第66张图片

3.3 去除文件系统支持

Uboot代码结构详细分析_第67张图片

3.4 重新编译、下载、查看结果

Uboot代码结构详细分析_第68张图片

编译完成之后大小裁剪到221KB,比之前的322KB小了足足100KB:

下载到开发板中,在串口终端中看看是否可以正常启动:

4、设置uboot的mtd分区

uboot支持各种设备之后,接下来的工作就是烧写内核、烧写文件系统,所以需要对整块Nand Flash的空间作以规划,大致分为以下四个空间即可:

bootloader空间
内核参数空间
内核空间
文件系统空间
但是目前我们仅知道uboot.bin被裁剪到了217KB,而不知道其它三个空间的信息,而且是第一次移植,对内核参数占用空间、内核空间的大小没有概念,所以我们需要借助别人已经制作好的内核和文件系统来查看这个信息。

启动JZ2440官方提供的uImage_4.3内核之后可以看到对整块Nand Flash的MTD分区信息:

Uboot代码结构详细分析_第69张图片

那么,mtd这样规划后有什么作用呢?

mtd的作用仅仅是给地址起个别名,方便使用,比如要烧写内核时要写0x00060000这个地址,就可以用 mtd2 这个名字代替,使代码变得更加通用。下面就开启我们u-boot的mtdparts命令支持。

4.1 添加宏CONFIG_CMD_MTDPARTS

mtdparts命令的相关实现在common/cmd_mtdparts.c文件中,目前uboot命令中无此命令,所以查看其同目录的Makefile文件,将此文件加入工程中编译。同时在单板配置文件smdk2440.h中打开CONFIG_CMD_MTDPARTS宏定义:

Uboot代码结构详细分析_第70张图片

从Makefile文件129行可以看出,要想添加cmd_mtdparts.o目标,必须定义CONFIG_CMD_MTDPARTS宏。所以,我们去单板配置文件打开该宏定义:

Uboot代码结构详细分析_第71张图片

4.2 添加宏CONFIG_MTD_DEVICE

在上一步的修改后,进行编译发现报错,提示未定义的函数引用。说明该函数所在文件未添加进工程,解决办法就是到该函数文件所在目录的Makefile文件中查看添加办法(在单板配置文件中添加某个宏)。

image-20210621221241359
在mtdparts命令的实现文件中调用了函数 get_mtd_device_nm,然而此函数没有定义,在Source Insight中找到该函数定义在 drivers/mtd/mtdcore.c 文件中,说明此文件没有包含到工程中,继续查看mtdcore.c 文件目录中的Makefile,可以看到需要定义宏CONFIG_MTD_DEVICE,所以继续在单板配置文件中添加该宏定义:

Uboot代码结构详细分析_第72张图片

4.3 编译、下载、查看结果

再次编译,编译成功,烧写到开发板,在串口终端中查看是否已经支持mtdpars命令了:

image-20210621221803373

使用时出现错误,提示mtdids没有定义,当前没有默认值。

4.4 添加默认配置宏

搜索此mtdids,查找问题,果然在文件common/cmd_mtdparts.c中找到结果:

Uboot代码结构详细分析_第73张图片

在单板配置文件中,仿照其它单板文件的配置,添加这两个宏定义:

 #define MTDIDS_DEFAULT          "nand0=jz2440_nand.0"
 #define MTDPARTS_DEFAULT        "mtdparts=jz2440_nand.0:"   \
                                         "256k(uboot)ro,"    \
                                 "128k(params)ro,"   \
                                 "2m(kernel),"       \
                                 "-(filesystem)"

Uboot代码结构详细分析_第74张图片

再次编译,下载到开发板进行测试:

Uboot代码结构详细分析_第75张图片

看一看,这次mtd_parts正常打印出了刚刚设置的默认mtdids和mtdparts信息。

4.5 测试mtdparts分区表是否可以正常使用

  • 首先串口中断命令行输入mtdparts default

  • 接着使用nand擦除命令依次对各分区进行擦除

    • nand erase.part uboot
      nand erase.part params
      nand erase.part kernel
      nand erase.part filesystem
      

Uboot代码结构详细分析_第76张图片

  • 将事先下载到地址0x30000000处的内核映像,写入分区名kernel指定的nand内核分区中:

    • nand write 30000000 kernel
      

    Uboot代码结构详细分析_第77张图片

4.6 让uboot自动执行mtdparts default命令

在使用mtd分区名称之前手动执行了mtdparts default命令设置分区表,但是在实际使用时,不可能每次在uboot命令行中手动执行这条命令,然后启动内核,所以在内核未启动之前,就要在代码中执行此命令。

在文件arch/arm/lib/board.c中,在函数board_init_r的最后(在main_loop之前)添加run_command("mtdparts default", 0);这行代码,以达到自动执行命令的目的:

Uboot代码结构详细分析_第78张图片

接着编译,下载到开发板中,测试。

上电之后执行mtdparts查看分区表,分区表已经设置成功,正常打印

Uboot代码结构详细分析_第79张图片

5、设置默认环境变量参数

目前移植的uboot支持SDRAM、Nor Flash、Nand Flash、DM9000网卡,但是还有一行警告没有处理。这行警告的原因是没有设置环境变量参数,所以uboot启动时读取校验参数失败,使用默认的参数。

Uboot代码结构详细分析_第80张图片

通过在根目录下搜索这行日志内容grep “using default enviroment” * -nR找到所在位置:common/env_common.c文件:

Uboot代码结构详细分析_第81张图片

打开该文件,查找该字符串所在函数:

Uboot代码结构详细分析_第82张图片

该函数中主要使用的 default_environment 数组,接着查看该数组内容,同样定义在该文件中,该数组的功能是根据我们定义的宏定义来设置默认环境变量参数

Uboot代码结构详细分析_第83张图片

5.1 设置默认参数

在单板配置文件include/configs/smdk2440.h中配置这些相关宏定义:

  • 内核启动相关宏定义
/* 内核启动相关ENV */
#define CONFIG_BOOTARGS		"console=ttySAC0,115200 root=/dev/mtdblock3"
#define	CONFIG_BOOTCOMMAND	"nand read 30000000 kernel 0x200000;bootm 30000000"

因为配置了CONFIG_BOOTCOMMAND 宏定义,所以在uboot启动时会开始有倒数计时,必须要在规定的时间内(默认5s)按下任意一个键,才能进入命令行,否则直接传递配置的内核参数,使用配置的命令启动内核。

  • 网络相关宏定义:
/* 网络相关ENV */
#define CONFIG_NETMASK		255.255.255.0
#define CONFIG_IPADDR		192.168.1.50
#define CONFIG_SERVERIP		192.168.1.100
#define CONFIG_ETHADDR		52:54:00:7c:df:b7
#define CONFIG_GATEWAYIP	192.168.1.1

Uboot代码结构详细分析_第84张图片

5.2 设置saveenv命令

如果只修改了代码中的宏定义,而没有将这些环境变量参数save到Flash中的话,在uboot启动时,当在读取flash中的参数发生错误后,就会使用默认环境变量,也就是在第上节中配置的那些宏定义。

在uboot的命令列表中可以看到saveenv命令表示将环境变量值存储到当前存储器中:

Uboot代码结构详细分析_第85张图片

全局查找saveenv命令的实现,找到了两个定义:

Uboot代码结构详细分析_第86张图片

common/env_nand.c文件中:saveenv会将环境变量存入nand flash中;

common/env_flash.c文件中:saveenv会将环境变量存入nor flash中;

因为两个文件中的定义冲突,肯定不会同时加入工程中编译,所以查看这两个文件共同目录/common下的Makefile:

image-20210623215732194

可以看到,结果为:

  • 配置宏定义CONFIG_ENV_IS_IN_FLASH:加入common/env_flash.c文件;
  • 配置宏定义CONFIG_ENV_IS_IN_NAND:加入common/env_nand.c文件;

而我们需要将环境变量存入Nand Flash中,所以在单板配置文件smdk2440.h中配置宏定义CONFIG_ENV_IS_IN_NAND,注释宏定义CONFIG_ENV_IS_IN_FLASH

Uboot代码结构详细分析_第87张图片

然后进入common/env_nand.c文件中找到 saveenv 定义,看还有哪些宏需要配置:

Uboot代码结构详细分析_第88张图片

在定义中可以看到主要有三个宏定义,CONFIG_ENV_OFFSET表示存入Nand Flash中的地址(即params分区地址),CONFIG_ENV_SIZE表示内核启动参数params分区的大小,CONFIG_ENV_RANGE表示执行擦除操作时的区域范围(大小)。

所以,按照我们之前对Nand Flash(大小为256MB)的分区情况:

  • uboot分区: 起始地址0x00000000、大小256KB;
  • params分区: 起始地址0x00040000、大小128KB;
  • kernel分区: 起始地址0x00060000、大小2MB;
  • filesystem分区:起始地址0x00260000、大小(256MB - 256KB - 128KB - 2MB)

在单板文件中添加对相关宏的定义(注意去掉原有宏定义的冲突):

Uboot代码结构详细分析_第89张图片

接着编译,烧写到开发板中,在串口终端中进行测试:

  • 未执行saveenv命令之前,仍有警告提示:

Uboot代码结构详细分析_第90张图片

  • 执行saveenv命令,将默认参数存入Nand Flash的params分区后,再重启开发板,观察结果发现:saveenv成功,重启开发板,可以看到uboot从nand flash中默认读取出了环境变量,警告消失

Uboot代码结构详细分析_第91张图片

5.3 动态修改环境变量,并查看能否顺利保存

  • 首先使用print命令查看所有环境变量值:

Uboot代码结构详细分析_第92张图片

  • 使用uboot命令set来修改ip地址并保存:
set ipaddr 192.168.1.8
saveenv

Uboot代码结构详细分析_第93张图片

  • 重新上电时候,uboot自动读取flash中的环境变量,可以看到已经是新设置的值。

6、烧写内核及文件系统

在上一节中,单板配置文件中添加了如下宏定义:define CONFIG_BOOTCOMMAND "nand read 30000000 kernel 0x200000;bootm 30000000"。他的意义是:启动时内核先会进行倒数计时,如果没有操作,就会执行图中的命令,从nand flash中的kernel 分区处读取内核到内存中的0x30000000处,也就是SDRAM起始地址,然后从0x30000000处启动内核。

6.1 烧写内核

所以,我们只需要将JZ2440官方提供的内核文件复制到TFTP服务器目录,然后将该内核文件提前烧写到nand flash的kernel分区即可,烧写方法如下:

tftp 30000000 uImage
nand erase.part kernel
nand write 30000000 kernel

然后重启开发板即可看到内核成功启动:

Uboot代码结构详细分析_第94张图片

Uboot代码结构详细分析_第95张图片

但是因为nand flash中还没有烧写文件系统,所以系统会停止在挂载文件系统的地方,接下来烧写文件系统。

6.2 烧写yaffs2文件系统

将JZ2440官方提供的 yaffs2 文件系统文件拷贝到TFTP服务器目录,按照下面的过程进行烧写:

tftp 30000000 fs_mini_mdev.yaffs2
nand erase.part filesystem
nand write.yaffs 30000000 260000 889bc0 //烧写大小要填写十六进制具体大小
										//(可以从tftp 命令的执行结果看到),
										//不能直接填写mtd分区名称filesystem。
								//文件系统的的大小889bc0由tftp下载命令反馈结果获知

烧写时出现了yaffs命令不支持的问题:Unknown nand command suffix '.yaffs'.

接下来定位这个问题,在 common/cmd_nand.c 文件中查看该命令的代码,可以看到,只有开启了宏定义 CONFIG_CMD_NAND_YAFFS,这段代码才会加入工程中编译:

Uboot代码结构详细分析_第96张图片

在单板配置文件 include/configs/smdk2440.h 中开启该宏定义:

Uboot代码结构详细分析_第97张图片

接下来重新编译uboot,再次烧写到开发板中进行yaffs烧写测试,烧写成功后重启开发板:

Uboot代码结构详细分析_第98张图片

6.3 解决bug

经过对比原始文件系统文件内容和烧写到nand flash中的内容,发现烧写的 nand flash 第一页的oob数据不一致,导致往后全部没有烧写进去,根据 nand write.yaffs 命令执行过程,进入在文件drivers/mtd/nand/nand_util.c中的nand_write_skip_bad 函数,改正uboot中的bug:

第一个bug:

Uboot代码结构详细分析_第99张图片

第二个bug:

Uboot代码结构详细分析_第100张图片

重新编译,烧写uboot到开发板中,进行yaffs烧写测试,重启之后了可以看到文件系统成功挂载,进入到linux系统:

Uboot代码结构详细分析_第101张图片

7、给uboot制作补丁

补丁文件就是通过对比当前文件和源码文件之后,得到修改内容的文件。

有了补丁文件,就可以在uboot2012.04的源码之上,直接打补丁,就会变为移植好适配JZ2440的uboot,非常方便,

在分享或者发布的时候,因为Uboot源码是统一的,所以只需要分享或者发布这个补丁文件即可。

7.1 制作补丁

make distclean	//清除编译产生文件
mv u-boot-2012.04.01 u-boot-2012.04.01-jz2440	//对文件夹重命名
tar -jxvf u-boot-2012.04.01.tar.bz2	//重新解压uboot2012.04.01源码
//制作补丁 diff -urN <旧的文件> <新的文件> > <补丁文件名.patch>
diff -urN u-boot-2012.04.01 u-boot-2012.04.01-jz2440 > u-boot-2012.04.01-jz2440.patch

image-20210624203115272

7.2 如何打补丁

打补丁时使用命令patch,其格式为:patch -p<数字n> < <补丁文件路径和位置>

其中数字n表示,补丁文件中位置信息中,忽略前n项。

比如,这里我进入到uboot源码文件夹中,执行打补丁命令,因为已经在uboot-2012.04.01这个目录下了,所以 n = 1,表示第一级目录被忽略。

Uboot代码结构详细分析_第102张图片

7.3 测试补丁可用性

打上补丁之后,直接编译:

make distclean
make smdk2440_config
make

如果成功生成u-boot.bin文件,则编译成功。

参考文献:https://blog.csdn.net/Mculover666/article/details/104401102

你可能感兴趣的:(嵌入式开发,bootloader,uboot代码结构,uboot补丁制作,uboot裁剪,JZ2440)