从SPL到内核:i.MX6ULL平台U-Boot启动流程与关键函数深度解析
1. i.MX6ULL平台U-Boot启动全景图当一块i.MX6ULL开发板通电瞬间处理器内部固化的Boot ROM会率先接管控制权。这个藏在芯片内部的引路人会根据BOOT引脚电平判断启动介质如SD卡、eMMC等然后将存储设备中的SPLSecondary 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 _mainC语言的奠基者当SPL完成基础建设后会通过_main函数将控制权移交给完整版U-Boot。这个过渡过程堪称精妙设置临时栈指针CONFIG_SYS_INIT_SP_ADDR分配并初始化全局数据结构gdglobal_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 0r1 机器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阶段的平稳电流形成鲜明对比。这种物理层面的变化正是软件世界层层递进的最佳印证。