Bootloader原理与U-Boot移植

Bootloader,说白了就是板子上电后跑的第一段代码。它负责把内核从Flash里搬到内存,然后跳过去执行。我刚开始做BSP时,总觉得Bootloader就是个“搬运工”,后来踩了几次坑才明白——它其实是硬件初始化的总指挥。

Bootloader启动流程

一个典型的嵌入式Bootloader,启动过程可以分成两个阶段。为什么分两段?因为第一段代码通常跑在SRAM里,空间有限,只能做最基础的事。

第一阶段(汇编为主)

  • 关看门狗、关中断
  • 初始化时钟、配置PLL
  • 初始化DDR控制器
  • 把第二阶段代码从Flash拷贝到DDR
  • 跳转到第二阶段入口

第二阶段就可以用C语言写了。它要做的事情更多:初始化串口、网卡、Flash等外设,然后进入交互模式或者直接加载内核。

我记得有一次调试一块新板子,DDR时序参数配错了,第一阶段死活跳不过去。后来用逻辑分析仪抓波形才发现,原来是时钟相位差了90度。嗯,这种问题光看代码是看不出来的。

Bootloader 两阶段启动流程 第一阶段(汇编) 关看门狗 · 关中断 初始化时钟 / PLL 初始化 DDR 控制器 拷贝第二阶段到 DDR 跳转到第二阶段入口 第二阶段(C语言) 初始化串口 / 网卡 初始化 Flash / 存储 进入交互命令行 加载内核到 DDR 跳转到内核入口 第一阶段在SRAM运行,第二阶段在DDR运行

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的命令远不止这些,但掌握上面这些,日常调试基本够用了。等你遇到具体问题,再查文档也不迟。