一、u-boot 目录

U-BOOT源码分析 - 图1

一般移植 U-BOOT 会修改绿色部分的代码,U-BOOT 中各目录间也是有层次结构的,虽然这种分法不是绝对的,但是在移植过程中可以提供一些指导意义。
U-BOOT源码分析 - 图2

可以通过 “内容一的举例” 来看看移植过程中需要更改哪些文件,我将其放在文末。

二、U-BOOT 功能

要学习 u-boot 之前,最好先了解一块板子的裸板程序启动的过程,因为 u-boot 其实就是裸板程序的集大成者。
U-boot 的主线目标功能是从 flash 中读出内核,放到内存中,启动内核。为了要实现这个功能,u-boot 分为两个阶段,主要在 start.s 文件中。

1、第一阶段

硬件设备初始化;
为加载 Bootloader 的第二阶段代码准备 RAM 空间;
复制 Bootloader 的第二阶段代码到 RAM 空间中;
设置好栈;
跳转到第二阶段代码的 C 入口点(start_armboot);
备注:在第一阶段进行的硬件初始化一般包括:关闭 WATCHDOG、关中断、设置 CPU 的速度和时钟频率、 RAM 初始化等。这些并不都是必须的,比如 S3C2410/S3C2440 的开发板所使用的 U-Boot 中,就将 CPU 的速度和时钟频率放在第二阶段进行设置。

2、第二阶段

初始化本阶段要使用到的硬件设备;
检测系统内存映射( Memory map );
将内核映像和根文件系统映像从 Flash 上读到 RAM 空间中;
为内核设置启动参数;
调用内核;
备注:为了方便开发,初始化一个串口以便程序员与 Bootloader 进行交互。部分内容的解释在文末,名为 “内容二的解释”。

三、u-boot 源码查看前置步骤

将 u-boot 源码放在 linux 系统下编译(需要配置交叉编译工具),设置为板子的配置(如 make smdk2410_config),再编译(make),最好的办法是拿一个开发板的 u-boot 源码进行编译,因为开发板厂商一般都会提供编译环境等(这些问题不该在初学时就遇到,否则会极大降低学习的积极性),编译成功后,将代码复制到 windows 下用 source insight 查看代码(这样看代码之间的互联性比较方便)。

以我的板子(jz2440 为例),将全部目录加进 SI 后,要去掉部分不是 2440 板子的板级文件(注:加入文件时,如果不能加入. S 文件,需要更改 SI 的设置)
board 目录只留 samsung/smdk2410;
arch 目录只留:(注意要加入各层的通用文件(未在文件夹内的))
a、arm/cpu/arm920t/s3c24x0 以及各层的通用文件(未在文件夹内的)
b、arm/cpu/dts
c、arm/include/asm/arch-s3c24x0 和 proc-armv 以及各层的通用文件(未在文件夹内的)
d、arm/lib
include/config 目录只加 smdk2410.h
再同步(project-synchronize files)

四、源码分析(第一阶段)

最传统的方法是从 makefile 开始分析,但是较为复杂,先直接从网上找到结论:从文件层面上看主要流程是在两个文件中:cpu/arm920t/start.s,lib_arm/board.c。
打开 cpu/arm920t/start.S

1、建立异常向量表

  1. .globl _start
  2. _start:
  3. b reset
  4. ldr pc, _undefined_instruction
  5. ldr pc, _software_interrupt
  6. ldr pc, _prefetch_abort
  7. ldr pc, _data_abort
  8. ldr pc, _not_used
  9. ldr pc, _irq
  10. ldr pc, _fiq
  11. _undefined_instruction: .word undefined_instruction
  12. _software_interrupt: .word software_interrupt
  13. _prefetch_abort: .word prefetch_abort
  14. _data_abort: .word data_abort
  15. _not_used: .word not_used
  16. _irq: .word irq
  17. _fiq: .word fiq
  18. .balignl 16,0xdeadbeef

在第 1 行中 “.globl start”: 使用. globol 声明全局符号_start, 在 board/100ask24x0/u-boot.lds 中 ENTRY(_start) 这里用到。
其中符号保存的地址都在顶层目录 / system.map 中列出来了
在第 2 行中_start 之所以有 8 种不同的异常处理,是在 2440 芯片手册中已经规定好了的。
问题 1:后面的异常处理为什么用 ldr 不用 b 指令?
ldr 是绝对跳转,b 是相对跳转,它的地址与代码位置无关。复位异常在 CPU 运行前,并未初始化 SDRAM(故不能使用 0x3000 0000 以上的地址),在正常工作后也可能触发复位,这时由于 CPU 已经对 SDRAM、MMU(虚拟内存管理) 等初始化了,
此时的虚拟地址和物理地址完全不同,所以 reset 使用 b 指令相对跳转。
2. 后面的异常处理是怎么执行的?执行后异常处理又怎么退出的?
在 2440 芯片手册上给出, 例如当处理一个中断IRQ 异常时:
a. 保存当前 PC 现场到寄存器 R14,
b. 把当前程序状态寄存器 (CPSR) 保存到备份程序状态寄存器 (SPSR) 中. 从异常退出的时候,就可以由 SPSR 来恢复 CPSR。
c. 根据中断 IRQ 异常处理, 强制将 CPSR 模式位设为中断模式
d. 强制 PC 从相关异常向量处取下条指令。跳转到 0x18 实现中断异常处理.
退出中断IRQ异常时:
  a). 将中断 IRQ 所对应的是 R14_irq 寄存器并放入到 PC 中, 如下图, 中断 IRQ 所对应的是 R14_irq 寄存器,执行 MOVS R14_svc .
b). 复制 SPSR 的内容返回给 CPSR 中。
  c). 如果在异常进入时置位了中断禁止标志位异常,清除中断禁止标志位
问题 3. 第 12 行中 .word: 类似于 (unsigend long)
以第 12 行中 _undefined_instruction: .word undefined_instruction 为例
_undefined_instruction 和_undefined_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。
它们的作用就是为内存做标记,插在那里,这个位置往前有特殊作用的内存,禁止访问。

2、设置管理模式、关看门狗、屏蔽中断

reset—->start.o

  1. reset:
  2. /* 设置CPSR程序程序状态寄存器为管理模式 */
  3. mrs r0,cpsr //MRS读出CPSR寄存器值到R0
  4. bic r0,r0,#0x1f //将R05位清空
  5. orr r0,r0,#0xd3 //R0b'110 10011按位或,禁止IRQ和FIQ中断,10011:复位需要设为管理模式(图1)
  6. msr cpsr,r0 //MSR写入CPSR寄存器
  7. /* 关看门狗 */
  8. # define pWTCON 0x53000000 //(WitchDog Timer)看门狗定时器寄存器WTCON,设为0X0表示关闭看门狗
  9. # define INTMOD 0X4A000004 //(Interrupt Mode)中断模式寄存器INTMOD,相应位=0:IRQ模式,相应位=1:IRQ模式,
  10. # define INTMSK 0x4A000008 //(Interrupt Mask)中断屏蔽寄存器INTMSK,相应位=0:开启中断服务,相应位=1:关闭中断服务
  11. # define INTSUBMSK 0x4A00001C //中断次级屏蔽寄存器,相应位=0:开启中断服务,相应位=1:关闭中断服务
  12. # define CLKDIVN 0x4C000014 //时钟分频寄存器
  13. #if defined(CONFIG_S3C2400) || defined(CONFIG_S3C2410) //宏定义CONFIG_S3C2410已定义
  14. ldr r0, =pWTCON //R0等于WTCON地址
  15. mov r1, #0x0 //R1=0x0
  16. str r1, [r0] //关闭WTCON寄存器,pWTCON=0;
  17. /* 关中断 */
  18. mov r1, #0xffffffff //R1=0XFFFF FFFF
  19. ldr r0, =INTMSK //R0等于INTMSK地址
  20. str r1, [r0] //*0x4A000008=0XFFFF FFFF(关闭所有中断)
  21. # if defined(CONFIG_S3C2410)
  22. ldr r1, =0x3ff //R1=0x3FF
  23. ldr r0, =INTSUBMSK //R0等于INTSUBMSK地址
  24. str r1, [r0] //*0x4A00001C=0x3FF(关闭次级所有中断)
  25. # endif
  26. /*
  27. 判断系统是从nand启动的还是直接将程序下载到SDRAM中运行,
  28. 若系统从nand启动,这里得到r0和r1值是不一样的,r1=0x33f80000,
  29. 而r0=0x00000000。说明没初始化SDRAM,ne(no equal)标识符为真,所以bl cpu_init_crit执行跳转.
  30. */
  31. adr r0, _start
  32. ldr r1, _TEXT_BASE
  33. cmp r0, r1
  34. blne cpu_init_crit

CPU 复位后是从这里开始执行,这里初始化了:
  1. 执行设置 CPSR 程序程序状态寄存器为管理模式
  2. 关看门狗
  3. 屏蔽中断
  4. 进入 cpu_init_crit 函数关闭 MMU, 进入 lowlevel_init 初始化 13 个 BANK 寄存器来初始化 SDR

3、进入 cpu_init_crit 函数 (关闭 MMU)

  1. cpu_init_crit:
  2. mov r0, #0
  3. mcr p15, 0, r0, c7, c7, 0 //关闭ICaches(指令缓存,关闭是为了降低MMU查表带来的开销)和DCaches(数据缓存,DCaches使用的是虚拟地址,开启MMU之前必须关闭)
  4. mcr p15, 0, r0, c8, c7, 0 //使无效整个数据TLB和指令TLB(TLB就是负责将虚拟内存地址翻译成实际的物理内存地址)
  5. mrc p15, 0, r0, c1, c0, 0
  6. bic r0, r0, #0x00002300 @ clear bits 13, 9:8 (--V- --RS) //bit8:系统不保护,bit9:ROM不保护,bit13:设置正常异常模式0x0~0x1c,即异常模式基地址为0X0
  7. bic r0, r0, #0x00000087 @ clear bits 7, 2:0 (B--- -CAM) //bit0~2:禁止MMU,禁止地址对齐检查,禁止数据Cache.bit7:设为小端模式
  8. orr r0, r0, #0x00000002 @ set bit 2 (A) Align //bit2:开启数据Cache
  9. orr r0, r0, #0x00001000 @ set bit 12 (I) I-Cache //bit12:开启指令Cache
  10. mcr p15, 0, r0, c1, c0, 0
  11. /*
  12. mcr/mrc:
  13. Caches:是一种高速缓存存储器,用于保存CPU频繁使用的数据。在使用Cache技术的处理器上,当一条指令要访问内存的数据时,
  14. 首先查询cache缓存中是否有数据以及数据是否过期,如果数据未过期则从cache读出数据。处理器会定期回写cache中的数据到内存。
  15. 根据程序的局部性原理,使用cache后可以大大加快处理器访问内存数据的速度。
  16. 其中DCaches和ICaches分别用来存放数据和执行这些数据的指令
  17. TLB:就是负责将虚拟内存地址翻译成实际的物理内存地址,TLB中存放了一些页表文件,文件中记录了虚拟地址和物理地址的映射关系。
  18. 当应用程序访问一个虚拟地址的时候,会从TLB中查询出对应的物理地址,然后访问物理地址。TLB通常是一个分层结构,
  19. 使用与Cache类似的原理。处理器使用一定的算法把最常用的页表放在最先访问的层次。
  20. 这里禁用MMU,是方便后面直接使用物理地址来设置控制寄存器
  21. */
  22. mov ip, lr //临时保存当前子程序返回地址,因为接下来执行bl会覆盖当前返回地址.
  23. bl lowlevel_init //跳转到lowlevel_init(位于u-boot-1.1.6/board/100ask24x0/lowlevel_init.S)
  24. mov lr, ip //恢复当前返回地址
  25. mov pc, lr //退出

4、进入 lowlevel_init 函数 (初始化各个 bank 和 SDRAM)

  1. lowlevel_init:
  2. ldr r0, =SMRDATA //将SMRDATA的首地址(0x33F806C8)存到r0中
  3. ldr r1, _TEXT_BASE //r1等于_TEXT_BASE内容,也就是TEXT_BASE(0x33F80000)
  4. sub r0, r0, r1 //将0x33F806C8与0x33F80000相减,得到现在13个寄存器值在NOR Flash上存放的开始地址
  5. ldr r1, =BWSCON //将BWSCON寄存器地址值存到r1中 (第一个存储器寄存器首地址)
  6. add r2, r0, #13*4 //每个寄存器4字节,r2=r0+13*4=NOR Flash上13个寄存器值最后一个地址
  7. 0:
  8. ldr r3, [r0], #4 //将r0的内容存到r3的内容中(r3等于SMRDATA里面值), 同时r0地址+=4;
  9. str r3, [r1], #4 //将r3的内容存到r1所指的地址中(向寄存器地址里写入r3值),同时r1地址+=4;
  10. cmp r2, r0 // 判断r2和r0
  11. bne 0b //不等则跳转到第6行继续执行
  12. mov pc, lr //跳回到返回地址中继续执行
  13. SMRDATA:
  14. .word (0+(B1_BWSCON<<4)+(B2_BWSCON<<8)+(B3_BWSCON<<12)+(B4_BWSCON<<16)+(B5_BWSCON<<20)+(B6_BWSCON<<24)+(B7_BWSCON<<28))
  15. //设置每个BWSCON,注意BANK0由硬件连线决定了
  16. .word ((B0_Tacs<<13)+(B0_Tcos<<11)+(B0_Tacc<<8)+(B0_Tcoh<<6)+(B0_Tah<<4)+(B0_Tacp<<2)+(B0_PMC))
  17. .word ((B1_Tacs<<13)+(B1_Tcos<<11)+(B1_Tacc<<8)+(B1_Tcoh<<6)+(B1_Tah<<4)+(B1_Tacp<<2)+(B1_PMC))
  18. .word ((B2_Tacs<<13)+(B2_Tcos<<11)+(B2_Tacc<<8)+(B2_Tcoh<<6)+(B2_Tah<<4)+(B2_Tacp<<2)+(B2_PMC))
  19. .word ((B3_Tacs<<13)+(B3_Tcos<<11)+(B3_Tacc<<8)+(B3_Tcoh<<6)+(B3_Tah<<4)+(B3_Tacp<<2)+(B3_PMC))
  20. .word ((B4_Tacs<<13)+(B4_Tcos<<11)+(B4_Tacc<<8)+(B4_Tcoh<<6)+(B4_Tah<<4)+(B4_Tacp<<2)+(B4_PMC))
  21. .word ((B5_Tacs<<13)+(B5_Tcos<<11)+(B5_Tacc<<8)+(B5_Tcoh<<6)+(B5_Tah<<4)+(B5_Tacp<<2)+(B5_PMC))
  22. //设置BANKCON0~BANKCON5
  23. .word ((B6_MT<<15)+(B6_Trcd<<2)+(B6_SCAN))
  24. .word ((B7_MT<<15)+(B7_Trcd<<2)+(B7_SCAN))
  25. //设置BANKCON6~BANKCON7
  26. .word ((REFEN<<23)+(TREFMD<<22)+(Trp<<20)+(Trc<<18)+(Tchr<<16)+REFCNT)
  27. //设置REFRESH,在S3C2440中11~17位是保留的,也即(Tchr<<16)无意义
  28. .word 0xb1 //设置BANKSIZE,对于容量可以设置大写,多出来的空内存会被自动检测出来
  29. .word 0x30 //设置MRSRB6
  30. .word 0x30 //设置MRSRB7

5、返回 start.s — 设置栈

  1. stack_setup: //设置栈,方便调用C函数
  2. ldr r0, _TEXT_BASE //代码段的初始地址:r0=0x33f80000
  3. sub r0, r0, #CFG_MALLOC_LEN //留出一段内存以实现malloc:r0=0x33f50000
  4. sub r0, r0, #CFG_GBL_DATA_SIZE //再留出一段存一些全局参数的变量:r0=0x33F4FF80
  5. #ifdef CONFIG_USE_IRQ
  6. sub r0, r0, #(CONFIG_STACKSIZE_IRQ+CONFIG_STACKSIZE_FIQ) //中断与快中断的栈:r0=0x33F4DF7C
  7. #endif
  8. sub sp, r0, #12 //留出12字节内存给abort异常 设置栈顶sp=r0-12;
  9. #ifndef CONFIG_SKIP_LOWLEVEL_INIT
  10. bl clock_init //进入clock_init函数
  11. #endif

6、进入 clock_init 函数设置时钟

  1. void clock_init(void)
  2. {
  3. S3C24X0_CLOCK_POWER *clk_power = (S3C24X0_CLOCK_POWER *)0x4C000000; //定义一个S3C24X0_CLOCK_POWER型结构体指针,clk_power->LOCKTIME=0x4C000000
  4. if (isS3C2410) //isS3C2410为0,执行else
  5. {... ...}
  6. else
  7. {
  8. /* FCLK:HCLK:PCLK = 1:4:8 */
  9. clk_power->CLKDIVN = S3C2440_CLKDIV; //S3C2440_CLKDIV=0X05
  10. /* change to asynchronous bus mod */
  11. __asm__( "mrc p15, 0, r1, c1, c0, 0\n" /* read ctrl register */
  12. "orr r1, r1, #0xc0000000\n" //使其从快总线模式改变为异步总线模式,在2440手册上看到
  13. "mcr p15, 0, r1, c1, c0, 0\n" /* write ctrl register */
  14. :::"r1" //:::"r1" 向GCC声明:我对r1作了改动
  15. );
  16. /* to reduce PLL lock time, adjust the LOCKTIME register */
  17. clk_power->LOCKTIME = 0xFFFFFFFF; //PLL 锁定时间计数寄存器
  18. /* configure UPLL */
  19. clk_power->UPLLCON = S3C2440_UPLL_48MHZ; //UCLK=48Mhz
  20. /* some delay between MPLL and UPLL */
  21. delay (4000); //等待UCLK时钟波形稳定
  22. /* configure MPLL */
  23. clk_power->MPLLCON = S3C2440_MPLL_400MHZ; //FCLK=400Mhz
  24. /* some delay between MPLL and UPLL */
  25. delay (8000); //等待FCLK时钟波形稳定
  26. }
  27. }

7、重定位、清 bss 段、跳转到 start_armboot 函数

  1. relocate: /* 拷贝u-boot到SDRAM */
  2. adr r0, _start //r0:当前代码开始地址
  3. ldr r1, _TEXT_BASE //r1:代码段连接地址(0X3FF8 0000)
  4. cmp r0, r1 //测试现在在FLASH中还是RAM中
  5. beq clear_bss //若_start==_TEXT_BASE,表示已经进行代码从Flash拷贝SDRAM了(通常是调试时直接下载到RAM中)
  6. ldr r2, _armboot_start //r2等于_armboot_start里的内容,也就是_start
  7. ldr r3, _bss_start //r3等于_bss_start里的内容,(在连接脚本u-boot.lds中定义,是代码段的结束地址)
  8. sub r2, r3, r2 //r2等于代码段长度
  9. bl CopyCode2Ram // r0: source, r1: dest, r2: size 将从NOR FLASH上代码段(r0~r0+r2)拷贝到sdram地址(r3)0x3ff80000代码段地址上
  10. clear_bss:
  11. ldr r0, _bss_start //r0=__bss_start
  12. ldr r1, _bss_end //r0等于_bss_end里的内容,也就是_end(在u-boot.lds里定义,是存bss的结束地址)
  13. mov r2, #0x00000000 //r2=0;用来清bss所有段
  14. clbss_l:
  15. str r2, [r0] //*r0=0;
  16. add r0, r0, #4 //r0+=4;
  17. cmp r0, r1
  18. ble clbss_l //小于等于一直执行clbss_l
  19. ldr pc, _start_armboot //pc等于_start_armboot里的内容,也就是跳转到start_armboot函数
  20. _start_armboot: .word start_armboot *(_start_armboot)=start_armboot

小结:
uboot - 第一阶段硬件初始化主要实现了:

  1. 执行设置 CPSR 程序程序状态寄存器为管理模式
  2. 关看门狗
  3. 屏蔽中断
  4. 关闭 MMU, 初始化 SDRAM
  5. 设置栈
  6. 时钟设置
  7. 重定位 (代码从 Flash 拷贝至 SDRAM 中)
  8. 清 bss 段 (未初始的全局 / 静态变量)
  9. 跳转到 start_armboot 函数 (位于 u-boot-1.1.6/lib_arm/borad.c, 用来实现第 2 阶段硬件相关的初始化)
    接下来开始分析 uboot - 第二阶段硬件初始化。

五、源码分析(第二阶段)

第二阶段的功能:
  初始化本阶段所需的硬件设备(主要设置系统时钟、初始化串口、Flash、网卡、USB)
  检测系统内存映射(memory map)
  将内核映像和根文件系统映象从 Flash 上读到 RAM 空间中
  为内核设置启动参数
  调用内核
从 start_armboot 开始,程序流程如图
U-BOOT源码分析 - 图3

六、附录

内容一的举例:

比如 common/cmd_nand.c 文件提供了操作 NAND Flash 的各种命令,这些命令调用 drivers/nand/nand_base.c 中的擦除、读写函数来实现;而这些函数是针对 NAND Flash 的共性做的封装,与平台 / 开发板相关的代码用宏或外部函数代替;平台相关则在 cpu/xxx,开发板相关则在 board/xxx。
以增加 yaffs 文件系统映像功能为例,先在 common 下的 cmd_nand.c 增加命令,比如 nand write.yaffs,这个命令调用 / drivers/nand/nand_util.c 中的函数,而这些函数依赖于 drivers/nand/nand_base.c、cpu/arm920t/s3c24x0/nand_flash.c 文件中的相关函数。

内容二的解释:

所谓检测内存映射,就是确定板上使用了多少内存、他们的地址空间是什么。由于嵌入式开发中的 Bootloader 多是针对某类板子进行编写,所以可以根据板子的情况直接设置,不需要考虑可以适用于各类情况的复杂算法。
Flash 上的内核映像有可能是经过压缩的,在读到 RAM 之后,还需要进行解压。当然,对于有自解压功能的内核,不需要 Bootloader 来解压。
将根文件系统映像复制到 RAM 中并不是必须的,这取决于是什么类型的根文件系统,以及内核访问它的方法。
将内核放在适当的位置后,在跳入执行内核之前,需要根据内核启动的需求,配置相应的启动参数。
调用内核的函数是什么还没找到,待会再找

参考链接