3. Linux内核与ARM平台适配

U-Boot 移植,说白了就是给一块“裸”的 ARM 板装上第一个能跑的程序。我做了这么多年驱动,每次拿到新板子,第一件事就是搞定 U-Boot。它不亮,后面 Linux 内核、根文件系统全是白搭。

这一章,咱们就一步步走通 U-Boot 移植的完整流程。从源码获取、目录结构分析,到配置编译,最后烧录验证。嗯,都是实战干货。

3.1 U-Boot 源码获取与目录结构

先说说源码。U-Boot 官方源码托管在 https://source.denx.de/u-boot/u-boot,也可以用 git 直接拉:

git clone https://source.denx.de/u-boot/u-boot.git
cd u-boot
git checkout v2024.01  # 选一个稳定版本,我习惯用 LTS

我个人习惯用 v2024.01 这个版本。为什么?因为它在 ARMv7 和 ARMv8 上支持都比较全,而且社区活跃,遇到问题好找补丁。

拿到源码后,第一件事是看目录结构。别急着编译,先搞清楚每个目录是干嘛的。

目录/文件 作用 我的经验
arch/ 架构相关代码,ARM、x86、RISC-V 等 ARM 移植主要看 arch/arm/
board/ 板级支持包,每个板子一个目录 移植时重点修改的地方
configs/ 默认配置文件,make xxx_defconfig 用的 先找最接近的板子配置
include/configs/ 板级头文件,定义内存、时钟、外设参数 这里改错了,板子直接不跑
drivers/ 驱动框架,串口、网卡、MMC、NAND 等 一般不用动,除非外设很特殊
dts/ 设备树源文件,新版本 U-Boot 用 越来越重要,建议学一下
tools/ 烧录工具、打包脚本 mkimage 就在这里
我的习惯:拿到一个新板子,先在 configs/ 里搜一下芯片型号,比如 ls configs/ | grep stm32mp。找到最接近的配置,然后复制一份改。这样比从头写快得多。

3.2 配置与编译:针对特定 ARM 板

假设我们手上的板子是 STM32MP157D-EV1(意法半导体的一款双核 Cortex-A7 板子)。配置编译的步骤其实很固定:

  1. 设置交叉编译工具链
  2. 执行默认配置
  3. 自定义配置(可选)
  4. 编译

先看第一步。交叉编译链我推荐用 Linaro 的 gcc-arm-none-eabi 或者 ARM 官方的 arm-linux-gnueabihf-。设置环境变量:

export CROSS_COMPILE=arm-linux-gnueabihf-
export ARCH=arm

然后执行默认配置:

make stm32mp15_defconfig

这一步会从 configs/ 目录下读取 stm32mp15_defconfig,生成 .config 文件。如果你想微调,可以用 make menuconfig 打开图形界面。我个人习惯先编译一次默认配置,跑通了再慢慢加功能。

编译命令很简单:

make -j$(nproc)

编译完成后,会在当前目录生成 u-boot.bin、u-boot.img、spl/u-boot-spl.bin 等文件。对于 STM32MP1 系列,还需要一个 tf-a(Trusted Firmware-A)配合。嗯,这里要注意,不同芯片的启动流程不一样,后面我会细说。

我曾经踩过的坑:第一次编译 STM32MP1 的 U-Boot,忘了设置 ARCH=arm,结果编译出一堆 x86 的代码。烧进去板子毫无反应。后来查了半天才发现是环境变量没设对。所以,每次编译前,先 echo $ARCH 确认一下。

3.3 烧录与启动验证

编译出镜像后,怎么烧到板子上?这取决于板子的启动方式。常见的有:

  • SD 卡启动:把镜像写到 SD 卡的特定偏移位置
  • eMMC/NAND 启动:通过 USB 或串口烧录
  • SPI NOR Flash 启动:用烧录器或 U-Boot 自身更新

以 STM32MP157D-EV1 为例,它支持 SD 卡启动。烧录命令如下:

# 假设 SD 卡设备是 /dev/sdb
sudo dd if=u-boot-spl.stm32 of=/dev/sdb bs=1k seek=1 conv=fsync
sudo dd if=u-boot.img of=/dev/sdb bs=1k seek=69 conv=fsync

这里 seek=1 和 seek=69 是 STM32MP1 的固定偏移,不能搞错。烧完后,插上 SD 卡,上电,通过串口(115200 8N1)看输出。

如果一切正常,你会看到类似这样的启动信息:

U-Boot 2024.01 (Jan 15 2025 - 10:30:00 +0800)

CPU: STM32MP157DAC
Model: STMicroelectronics STM32MP157D-EV1
DRAM:  512 MiB
MMC:   mmc@58005000: 0
Loading Environment from MMC... OK
In:    serial
Out:   serial
Err:   serial
Net:   eth0: ethernet@5800a000
Hit any key to stop autoboot:  0
=>

看到 => 提示符,说明 U-Boot 已经跑起来了。这时候你可以输入 help 看看支持哪些命令,或者用 printenv 查看环境变量。

验证要点:
  • 串口输出是否正常?有没有乱码?
  • DRAM 大小是否识别正确?
  • MMC/SD 卡能不能读写?
  • 网络能不能 ping 通?

3.4 U-Boot 启动流程与核心逻辑

你想想看,U-Boot 从上电到出现提示符,中间经历了什么?我画了一张图,帮你理清整个流程。

U-Boot 启动流程 U-Boot 启动流程(ARM 架构) 阶段1: 芯片内部 ROM Code 阶段2: SPL(Secondary Program Loader) 阶段3: U-Boot 主镜像(u-boot.img) 阶段4: 初始化硬件(时钟、DDR、串口、MMC) 阶段5: 加载环境变量 → 启动内核 说明:SPL 负责初始化最小硬件(时钟、DDR),然后从存储介质加载 U-Boot 主镜像到 DDR 中运行

这张图展示了 U-Boot 的典型启动流程。对于 ARM 芯片,大部分都遵循这个模式:ROM Code → SPL → U-Boot 主镜像。SPL 的作用很关键,它就像一个小型的 bootloader,负责把真正的 U-Boot 从 Flash 或 SD 卡搬到 DDR 里。

我在项目中遇到过一块板子,SPL 能跑,但 U-Boot 主镜像死活加载不起来。后来发现是 DDR 初始化参数不对,SPL 里配的时序和实际硬件不匹配。嗯,这种问题最头疼,因为 SPL 阶段还没有串口输出,你根本不知道它卡在哪。

3.5 常见问题与避坑指南

我曾经踩过的坑:
  • 串口无输出:检查串口引脚配置、波特率、时钟。有时候是 pinmux 没配对。
  • DDR 识别错误:用 md 命令读内存,看看能不能正常读写。如果读出来全是 0 或 0xdeadbeef,说明 DDR 没初始化好。
  • SD 卡启动失败:确认烧录偏移是否正确。不同芯片的偏移不一样,查芯片手册。
  • 网络不通:先 ping 自己,再 ping 网关。检查 PHY 地址和复位时序。
调试小技巧:U-Boot 支持 CONFIG_DEBUG_UART,可以在 SPL 阶段就输出调试信息。打开这个选项,能帮你定位很多早期启动问题。我每次移植新板子,第一件事就是开这个。

3.6 本章小结

U-Boot 移植,说白了就是三板斧:拿源码、配板子、烧进去。但每一板斧里都有不少细节。源码目录要熟,配置要准,烧录偏移不能错。启动流程要心里有数,出了问题才知道从哪查起。

我个人觉得,U-Boot 移植是嵌入式 Linux 开发中最有成就感的一步。当你看到串口输出第一个字符时,那种感觉,嗯,就像给一块石头注入了生命。后面的事情,就顺理成章了。