第三章 Bootloader原理与U-Boot
好,咱们进入第三章。这一章讲的是Bootloader,说白了就是嵌入式系统上电后第一个跑起来的程序。你想想看,CPU上电后,内存还没初始化,文件系统还没挂载,谁来干这些脏活累活?就是Bootloader。
3.1 Bootloader的作用
Bootloader到底干了些什么?我把它总结为三个核心任务:
- 硬件初始化:设置时钟、初始化DDR、配置串口等外设。没有这一步,后面的代码根本没地方跑。
- 引导内核:从Flash、SD卡或网络把内核镜像加载到内存,然后跳转执行。
- 提供交互接口:让你能在开发阶段通过串口输入命令,调试启动参数。
我在做第一块分析仪器主板时,就吃过亏。当时Bootloader里忘了初始化DDR的时序参数,结果内核一启动就死机。查了两天才发现是DDR配置不对。嗯,从那以后我每次移植都会先确认DDR初始化代码。
核心要点:Bootloader是硬件和操作系统之间的桥梁。它不负责具体的业务逻辑,但负责把硬件带到可运行状态。
3.2 U-Boot简介
U-Boot,全称是Universal Boot Loader。为什么选它?因为它是目前嵌入式Linux领域事实上的标准。我这些年经手的项目,从ARM9到Cortex-A72,几乎都在用U-Boot。
U-Boot的几个特点:
- 支持架构广:ARM、x86、MIPS、RISC-V,基本你能想到的架构它都支持。
- 驱动模型完善:从早期的简单驱动到现在的Driver Model(DM),代码结构越来越清晰。
- 网络引导方便:支持TFTP、NFS,开发阶段不用反复烧写Flash。
- 社区活跃:遇到问题基本都能在邮件列表或论坛找到答案。
我个人习惯用U-Boot 2020.04以后的版本,因为它的Driver Model已经非常成熟了。早期版本那种直接操作寄存器的写法,说实话维护起来挺头疼的。
小提示:如果你的板子用的是新芯片,建议直接拉U-Boot主线代码。厂商提供的BSP版本往往落后主线好几年,bug也多。
3.3 U-Boot的启动流程
U-Boot的启动流程,我把它拆成两个阶段:SPL(Secondary Program Loader)和U-Boot proper。为什么分两段?因为早期的CPU内部SRAM太小,装不下完整的U-Boot。所以先跑一个极简的SPL,初始化DDR,再把完整的U-Boot加载到DDR里运行。
3.3.1 SPL阶段
SPL的代码量很小,通常只有几十KB。它做的事情很纯粹:
- 设置CPU为SVC模式,关中断
- 初始化时钟和PLL
- 初始化DDR控制器
- 从启动设备(SD卡、NAND Flash等)读取U-Boot proper到DDR
- 跳转到U-Boot proper的入口
我曾经在移植SPL时遇到一个坑:DDR初始化时序参数是从芯片手册里抄的,但实际板子走线长度不同,导致DDR不稳定。后来用示波器量了信号,微调了参数才搞定。所以啊,手册上的参数只是参考,实际板子要自己调。
3.3.2 U-Boot proper阶段
U-Boot proper启动后,流程就丰富多了。我画个简化的流程图给你看:
start_armboot()
├── init_sequence[] // 板级初始化函数数组
│ ├── board_init_f() // 早期板级初始化
│ ├── initf_malloc() // 内存分配器初始化
│ ├── initf_bootstage() // 启动阶段记录
│ ├── arch_cpu_init() // CPU架构相关初始化
│ ├── dram_init() // DDR信息获取
│ └── ...
├── relocate_code() // 代码重定位到DDR高位地址
├── board_init_r() // 后期板级初始化
│ ├── env_init() // 环境变量初始化
│ ├── initr_serial() // 串口初始化
│ ├── initr_net() // 网络初始化
│ └── ...
└── main_loop() // 进入命令行交互循环
这里有个关键点:代码重定位。U-Boot一开始在Flash或ROM里运行,但为了速度,它会把自己复制到DDR的高地址处。重定位后,原来的代码就不能再用了,必须用位置无关码(PIC)来写。
注意:如果你在U-Boot里加了自定义函数,一定要确认它是不是位置无关的。我见过有人直接在函数里写绝对地址,重定位后程序就跑飞了。
3.3.3 环境变量与启动命令
U-Boot启动后,会读取环境变量。环境变量存在什么地方?可以是Flash、EEPROM、甚至文件系统。默认的启动命令是bootcmd,通常长这样:
bootcmd=run loadimage; run mmcboot
loadimage=load mmc 0:1 ${loadaddr} ${image_name}
mmcboot=mmc dev 0; fatload mmc 0:1 ${fdtaddr} ${fdt_name}; bootm ${loadaddr} - ${fdtaddr}
说白了,就是先加载内核和设备树到内存,然后用bootm命令启动。你想想看,如果没有U-Boot,每次调试都要重新烧写Flash,那得多痛苦。
3.3.4 启动内核的细节
U-Boot启动内核时,会做几件事:
- 设置启动参数(ATAGS或设备树)
- 关闭MMU和Cache(有些架构需要)
- 跳转到内核入口地址
对于现代ARM Linux,都是用设备树(DTB)传递硬件信息。U-Boot会把DTB地址放在寄存器里,内核启动时读取。我建议你在bootm命令里显式指定DTB地址,别依赖默认值,否则容易出问题。
调试技巧:如果内核启动卡住了,可以在U-Boot里用bdinfo命令查看板级信息,用printenv查看环境变量。我曾经靠这两个命令定位过不下十次启动问题。
3.4 小结
这一章我们聊了Bootloader的作用、U-Boot的基本情况,以及它的启动流程。说白了,U-Boot就是嵌入式系统的「看门人」,它把硬件初始化好,把内核请进门,然后功成身退。
下一章我们会深入U-Boot的移植实战,包括如何添加新板子、如何配置驱动、如何调试启动问题。到时候我会拿一个实际的分析仪器主板做例子,一步步带你走通整个流程。
嗯,今天就到这儿。记住一句话:Bootloader移植搞不定,后面的Linux系统移植就是空中楼阁。这块功夫一定要下扎实。