linux系统移植篇(二)—— Uboot启动流程与关键命令实战解析

1. Uboot启动流程全景解析

刚接触嵌入式Linux开发时,第一次看到Uboot的启动日志滚动,那种"黑魔法"般的感觉至今难忘。今天我们就来彻底拆解这个引导程序的启动奥秘,用实际开发板的运行过程为例,带你走完从芯片上电到内核加载的完整旅程。

硬件上电后的第一站是芯片内部的ROM代码。以i.MX6ULL为例,芯片会根据BOOT_MODE引脚选择启动方式(比如SD卡或eMMC),然后从存储设备的固定位置(对于SD卡是第2KB处)加载SPL(Secondary Program Loader)。这个阶段就像电脑开机时按F12选择启动盘,只不过嵌入式设备是通过硬件引脚决定的。

关键阶段转移过程可以形象地理解为接力赛:

  • BL1(ROM代码)→BL2(SPL)→Uboot完整版→Linux内核 每个阶段都会为下一阶段准备好运行环境。我曾在调试时遇到过SPL加载失败的情况,通过示波器发现是SD卡时钟频率设置不当导致,这个经历让我深刻理解了硬件初始化的必要性。

2. 汇编阶段的硬核操作

当Uboot的汇编代码开始执行时,它做的第一件事就是给系统"降噪"。这包括三个关键操作:

关闭中断和看门狗的代码示例:

mrs r0, cpsr @ 读取当前程序状态寄存器 orr r0, r0, #0xC0 @ 设置中断禁止位 msr cpsr, r0 @ 写回寄存器 ldr r0, =0x020C0000 @ 看门狗控制寄存器地址 mov r1, #0x30 str r1, [r0] @ 禁用看门狗

内存管理单元(MMU)关闭的原因很实际:早期启动时物理地址就是编程地址,不需要虚拟内存映射。我曾遇到过因为忘记关闭MMU导致内存访问异常的问题,后来用JTAG调试器单步跟踪才发现问题所在。

时钟初始化就像给CPU装上节拍器。以i.MX6ULL为例,需要先切换时钟源到24MHz晶振,再配置PLL锁相环提升主频到792MHz。这个过程需要严格遵循芯片手册的时序要求,有次我因为跳过了稳定等待周期导致系统频繁死机。

3. C语言环境搭建的艺术

当汇编代码完成基础硬件初始化后,就要为C语言创造舒适的运行环境了:

栈空间设置是第一个关键步骤。在mx6ull开发板上,我们会将栈指针指向DDR内存的顶端(例如0x8FF00000),留出足够的空间供函数调用使用。这就像给程序运行划定一个安全的活动区域。

重定位(Relocation)过程特别有趣:Uboot会把自己从Flash拷贝到DDR内存的高地址区域。通过objdump反汇编可以看到,编译时链接地址是0x87800000,但实际运行时可能从0x80000000开始。这个"自举"过程通过精巧的地址无关代码实现。

BSS段清零这个看似简单的操作却很重要。有次我移植时忘记初始化BSS段,导致全局变量随机初始化为内存残留值,引发了难以追踪的逻辑错误。现在每次调试都会先检查这个环节。

4. 硬件初始化实战技巧

进入board_init_f阶段后,Uboot开始全面初始化硬件:

DDR内存初始化是个精细活。以镁光DDR3L为例,需要依次配置:

  1. 时钟使能和复位释放
  2. PHY训练参数设置
  3. 时序参数配置(tRFC=110ns,tWR=15ns等)
struct mx6_ddr3_cfg mem = { .mem_speed = 800, .density = 2, .rowaddr = 13, .coladdr = 10, .trcd = 1375, .trcmin = 4875, .trasmin = 3500, }; mx6_dram_cfg(&ddr, &mem, &cfg);

外设初始化要根据板级设计调整。比如调试串口需要确认:

  • 引脚复用模式(UART1_TXD复用为GPIO1_IO16)
  • 波特率分频系数(115200bps对应0x158)
  • 流控设置(通常禁用CTS/RTS)

5. 命令系统与调试利器

Uboot的命令系统就像嵌入式开发的瑞士军刀,几个常用命令组合就能解决大部分问题:

内存操作三剑客

md.b 0x80000000 10 # 以字节格式查看16个内存单元 mw.w 0x83000000 0x1234 # 写入16位数据 cmp.b 0x80000000 0x80100000 100 # 比较两段内存

网络调试技巧

setenv ipaddr 192.168.1.100 setenv serverip 192.168.1.1 tftp 0x80800000 zImage # 下载内核镜像 ping 192.168.1.1 # 测试网络连通性

遇到网络问题时,我通常会先用mii info检查PHY寄存器状态,再通过phy read/phy write命令调试链路参数。

EMMC/SD卡操作的典型场景:

mmc dev 1 # 切换到EMMC设备 mmc part # 查看分区表 fatls mmc 1:1 # 列出FAT分区文件 ext4load mmc 1:2 0x83000000 /boot/dtb # 加载ext4分区文件

6. 内核引导的临门一脚

当所有准备工作就绪,Uboot最后要完成向内核的优雅交接:

镜像加载策略对比:

镜像类型加载方式典型地址
zImagetftp 0x80008000 zImage0x80008000
uImagefatload mmc 0:1 0x8000C000 uImage0x8000C000
FIT Imageload mmc 0:1 0x82000000 fit.img0x82000000

设备树传递的常见问题排查:

  1. 确认dtb文件与内核版本匹配
  2. 检查bootargs中的根文件系统路径
  3. 通过fdt addr命令验证设备树加载地址
fdt addr 0x83000000 fdt print /memory # 查看内存节点信息

启动参数优化实例:

setenv bootargs 'console=ttymxc0,115200 root=/dev/mmcblk1p2 rootwait rw' setenv bootcmd 'mmc dev 1; ext4load mmc 1:1 0x80800000 zImage; ext4load mmc 1:1 0x83000000 imx6ull.dtb; bootz 0x80800000 - 0x83000000' saveenv

在实际项目中,我遇到过因为bootargs中rootdelay设置过短导致根文件系统挂载失败的情况。通过逐步增加延迟时间(从1s到5s)最终定位到是eMMC初始化速度较慢导致的。这种细节问题正是嵌入式开发中需要特别注意的地方。