1. i.MX6ULL平台U-Boot启动全景图
当一块i.MX6ULL开发板通电瞬间,处理器内部固化的Boot ROM会率先接管控制权。这个藏在芯片内部的"引路人"会根据BOOT引脚电平判断启动介质(如SD卡、eMMC等),然后将存储设备中的SPL(Secondary Program Loader)搬运到内部SRAM运行。这个不足100KB的微型引导程序,正是整个启动链条的第一个关键环节。
为什么需要SPL这个"二传手"?原因在于i.MX6ULL的内部SRAM仅有128KB,而完整版U-Boot通常超过400KB。SPL就像个精干的先遣队,只携带最必要的装备(时钟/DDR初始化代码)为后续大部队开辟道路。我曾用示波器测量过这个阶段的耗时:从电源稳定到SPL开始执行,整个过程通常在200ms内完成。
2. SPL阶段关键函数解剖
2.1 _start:一切的开端
在arch/arm/lib/vectors.S中定义的_start符号,是处理器跳出Boot ROM后遇到的第一个路标。这个看似简单的汇编代码段,实则暗藏玄机:
_start: b reset ldr pc, _undefined_instruction ldr pc, _software_interrupt ldr pc, _prefetch_abort ldr pc, _data_abort ldr pc, _not_used ldr pc, _irq ldr pc, _fiq这段代码构建了ARM异常向量表,其中第一条跳转指令直接指向reset函数。有趣的是,在早期调试时,我曾故意修改这里的指令顺序,结果系统直接卡死在启动阶段——这说明Boot ROM会严格校验向量表的完整性。
2.2 reset:处理器的重生仪式
reset函数就像给处理器做"全身检查":
- 设置SVC模式并关闭中断(避免初始化过程被打断)
- 调用cpu_init_cp15初始化协处理器
- 执行cpu_init_crit进行关键硬件初始化
特别值得注意的是MMU的关闭操作:
mrc p15, 0, r0, c1, c0, 0 bic r0, r0, #0x00002000 // 清除bit13(V) bic r0, r0, #0x00000007 // 清除bit2:0(CAM)这段代码通过CP15协处理器直接操作控制寄存器,关闭地址转换和缓存。我在早期移植时曾遗漏这个步骤,导致后续内存访问全部错乱。
2.3 lowlevel_init:板级定制的舞台
这个函数是硬件厂商的"自留地",主要完成:
- 初始化关键时钟(如ARM内核PLL)
- 配置DDR控制器时序参数
- 设置GPIO复用状态
以DDR初始化为例,i.MX6ULL需要精确配置如下参数:
struct mx6ul_iomux_ddr_regs { uint32_t dram_dqm[4]; uint32_t dram_ras; uint32_t dram_cas; // 共40多个寄存器... };这些值必须严格匹配具体板子的走线长度和存储器芯片规格。我曾遇到过因一个参数错误导致内存带宽下降50%的案例。
3. U-Boot主体阶段深度游
3.1 _main:C语言的奠基者
当SPL完成基础建设后,会通过_main函数将控制权移交给完整版U-Boot。这个过渡过程堪称精妙:
- 设置临时栈指针(CONFIG_SYS_INIT_SP_ADDR)
- 分配并初始化全局数据结构gd(global_data)
- 调用board_init_f进行前期初始化
内存布局在这个阶段会发生重大变化。通过实际调试输出可以看到:
Before relocate: Text base: 0x87800000 Stack top: 0x00910000 After relocate: Text base: 0x9ff47000 Stack top: 0x9ff460003.2 代码重定位的魔法
relocate_code函数实现的核心功能,可以用这个搬运过程描述:
while (r1 < r2) { *(uint32_t *)r0 = *(uint32_t *)r1; r0 += 4; r1 += 4; }但真正的难点在于位置无关码(PIC)处理。U-Boot通过.rel.dyn段动态修正所有绝对地址引用。例如:
原始指令:ldr r3, [pc, #12] ; 加载0x878042c8处值 重定位后:ldr r3, [pc, #12] ; 加载(0x878042c8 + offset)处值3.3 board_init_r:豪华装修阶段
这个函数就像房子的精装修工程:
- 初始化串口、网卡等外设
- 扫描存储设备分区
- 加载环境变量
- 准备Linux启动参数
特别值得一提的是环境变量的处理流程:
env_init → env_relocate → env_load如果检测到env分区损坏,会自动恢复默认值。这个机制让我在量产时避免了大量返修。
4. 内核引导的临门一脚
4.1 do_bootz:内核搬运工
bootz命令的执行包含这些关键步骤:
- 验证zImage头部签名
- 解析设备树结构
- 设置ARM寄存器传参:
- r0 = 0
- r1 = 机器ID(对于设备树可忽略)
- r2 = 设备树物理地址
实际调试时,可以通过这些命令观察准备状态:
=> bdinfo // 查看板级信息 => fdt addr 83000000 // 定位设备树 => fdt print / // 查看设备树结构4.2 内核交接的最后一米
kernel_entry的调用堪称电子世界的"时空穿越":
void (*kernel_entry)(int zero, int arch, uint params); kernel_entry = (void *)images->ep; kernel_entry(0, machid, r2);这个瞬间,处理器会:
- 关闭所有中断
- 清空流水线
- 跳转到内核入口地址
- 永远不会再返回
我在调试这个阶段时,曾用JTAG捕获到精确的跳转时序:从执行kernel_entry到内核第一个printk输出,通常只需要3-5ms。
通过示波器可以观察到这个过程中电源纹波的变化:当内核开始调度后,CPU电流会出现明显的波动特征,这与U-Boot阶段的平稳电流形成鲜明对比。这种物理层面的变化,正是软件世界层层递进的最佳印证。