Bootloader原理与U-Boot移植
Bootloader,说白了就是板子上电后跑的第一段代码。它负责把内核从Flash里搬到内存,然后跳过去执行。我刚开始做BSP时,总觉得Bootloader就是个“搬运工”,后来踩了几次坑才明白——它其实是硬件初始化的总指挥。
Bootloader启动流程
一个典型的嵌入式Bootloader,启动过程可以分成两个阶段。为什么分两段?因为第一段代码通常跑在SRAM里,空间有限,只能做最基础的事。
第一阶段(汇编为主)
- 关看门狗、关中断
- 初始化时钟、配置PLL
- 初始化DDR控制器
- 把第二阶段代码从Flash拷贝到DDR
- 跳转到第二阶段入口
第二阶段就可以用C语言写了。它要做的事情更多:初始化串口、网卡、Flash等外设,然后进入交互模式或者直接加载内核。
我记得有一次调试一块新板子,DDR时序参数配错了,第一阶段死活跳不过去。后来用逻辑分析仪抓波形才发现,原来是时钟相位差了90度。嗯,这种问题光看代码是看不出来的。
U-Boot源码结构
U-Boot的源码目录,你第一次看可能会觉得乱。其实它有自己的逻辑。我建议你重点关注这几个目录:
| 目录 | 作用 | 我常用的场景 |
|---|---|---|
arch/ |
CPU架构相关代码 | 改ARMv8启动代码时必看 |
board/ |
板级初始化代码 | 移植新板子主要改这里 |
drivers/ |
外设驱动 | 调网卡、Flash驱动时翻 |
include/configs/ |
板级配置头文件 | 配DDR参数、环境变量 |
dts/ |
设备树源文件 | 新板子必须自己写dts |
你想想看,U-Boot的代码量其实不小,但真正需要你改的,通常就是 board/ 和 include/configs/ 这两个地方。其他目录,除非你要加新功能,否则基本不用动。
U-Boot配置与编译
配置U-Boot,现在主流做法是用 make menuconfig。但早期可不是这样——以前全靠手写头文件里的宏定义,配错了就得重新编译,效率很低。
我个人习惯的编译流程是这样的:
# 1. 清理旧配置
make distclean
# 2. 加载默认配置(以某款ARM板为例)
make <board_name>_defconfig
# 3. 进入图形化配置界面
make menuconfig
# 4. 编译
make -j$(nproc)
小技巧:编译时加 V=1 可以打印详细日志。我调试驱动时经常用这个参数,能直接看到编译器调用了哪些文件,方便定位问题。
配置完成后,会在根目录生成 .config 文件。你可以直接编辑它,但我建议还是用 menuconfig,因为手动改容易漏掉依赖项。
曾经踩过的坑:有一次我直接改了 .config 里的 CONFIG_SYS_TEXT_BASE,结果编译出来的U-Boot死活跑不起来。后来发现这个宏还影响了链接脚本的地址分配,光改一处是不够的。所以,能用 menuconfig 就别手写。
U-Boot设备树基础
设备树(Device Tree)是什么?说白了,就是用一种结构化的文本,告诉内核你的板子上有哪些硬件、它们怎么连接。以前的做法是在内核源码里写一大堆板级文件,每换一个板子就要改代码。设备树把硬件描述和驱动代码分开了,这确实是个好设计。
一个简单的设备树节点长这样:
/ {
model = "My Custom Board";
compatible = "vendor,custom-board";
chosen {
stdout-path = &uart0;
};
uart0: serial@10000000 {
compatible = "ns16550";
reg = <0x10000000 0x1000>;
clock-frequency = <24000000>;
status = "okay";
};
};
U-Boot在启动时会解析设备树,把硬件信息传递给内核。我建议你在移植新板子时,先写一个最小化的设备树,只包含CPU、内存和串口。等系统能跑起来了,再慢慢加其他外设。
设备树编译命令:
# .dts 编译成 .dtb
dtc -I dts -O dtb -o myboard.dtb myboard.dts
# 反编译 .dtb 查看内容
dtc -I dtb -O dts -o myboard.dts myboard.dtb
U-Boot常用命令
U-Boot启动后,你会看到一个命令行提示符。这里我列几个最常用的命令,也是我平时调试时用得最多的:
| 命令 | 作用 | 我常用的场景 |
|---|---|---|
printenv |
打印环境变量 | 查看启动参数、IP地址 |
setenv |
设置环境变量 | 修改bootargs、serverip |
saveenv |
保存环境变量到Flash | 改完参数后必须执行 |
tftp |
通过网络下载文件 | 远程加载内核、dtb |
fatload |
从FAT分区加载文件 | 从SD卡读内核镜像 |
bootm |
启动内核 | 手动启动调试 |
md |
查看内存内容 | 检查DDR数据是否正确 |
mw |
写内存 | 修改寄存器测试 |
举个例子,我经常这样手动启动内核:
# 设置网络参数
setenv ipaddr 192.168.1.100
setenv serverip 192.168.1.1
# 通过tftp下载内核和设备树
tftp 0x42000000 zImage
tftp 0x43000000 myboard.dtb
# 启动内核
bootm 0x42000000 - 0x43000000
个人习惯:我会把常用的启动命令写成一个环境变量,比如 setenv myboot 'tftp 0x42000000 zImage; tftp 0x43000000 myboard.dtb; bootm 0x42000000 - 0x43000000',然后直接执行 run myboot。这样省得每次敲一长串命令。
嗯,U-Boot的命令远不止这些,但掌握上面这些,日常调试基本够用了。等你遇到具体问题,再查文档也不迟。