2. 嵌入式系统启动基础
好,咱们直接进入正题。U-Boot 的源码,说实话,第一次打开的时候确实有点懵。目录多,文件杂,不知道从哪看起。我当年刚接触的时候,也是对着屏幕发了好一会儿呆。
但别怕,这东西有规律。你只要抓住了 arch、board、config 这三个核心目录,再顺着启动流程走一遍,基本就通了。今天我就带你把这层窗户纸捅破。
2.1 顶层目录结构:别被吓到
U-Boot 的顶层目录,大概有二十多个。但真正跟你移植工作强相关的,其实就那么几个。我把它们分成了三类,你一看就明白:
| 分类 | 目录名 | 一句话说明 |
|---|---|---|
| 平台相关 | arch/ | 存放 CPU/架构相关的代码。ARM、x86、RISC-V 都在这里。 |
| 板级相关 | board/ | 每个板子一个文件夹。你的硬件差异,全在这里改。 |
| 配置相关 | include/configs/ | 板级的头文件配置。比如 DDR 参数、外设使能。 |
| 通用驱动 | drivers/ | 串口、网卡、MMC、USB 等驱动。一般不用动。 |
| 通用工具 | tools/ | mkimage 等打包工具。编译时会用到。 |
| 通用库 | lib/、common/ | 字符串处理、命令行解析等公共函数。 |
board/ 和 include/configs/ 目录下,找一个跟它最像的参考板。复制粘贴,然后改。这比从零开始写快得多。
2.2 三个核心目录:arch、board、config
这三个目录,是你移植工作的主战场。咱们一个一个说。
2.2.1 arch/:CPU 的骨架
arch/ 目录下,按 CPU 架构分了子目录。以最常用的 ARM 为例:
arch/arm/
├── cpu/ # CPU 核心代码,比如 cache、MMU 的开关
│ ├── armv7/ # Cortex-A7/A8/A9 等
│ └── armv8/ # Cortex-A53/A72 等
├── dts/ # 设备树文件 .dts 和 .dtsi
├── include/ # 架构相关的头文件
├── lib/ # 架构相关的库函数
└── mach-*/ # 具体 SoC 厂商的代码,比如 mach-sunxi/、mach-imx/
这里要注意的是 mach-*/ 目录。每个 SoC 厂商,比如全志、NXP、Rockchip,都会在这里放一些初始化代码。我遇到过一个问题:某款全志 H3 的板子,串口死活不输出。查了半天,发现是 mach-sunxi/ 里的时钟初始化代码,跟我的晶振频率不匹配。
2.2.2 board/:板子的血肉
board/ 目录下,每个板子一个文件夹。里面通常包含:
board.c:板级初始化函数,比如board_init()、dram_init()。Kconfig:板级的配置菜单。Makefile:编译脚本。
说白了,你的板子有什么特殊外设,上电要先初始化什么,都在这里写。
2.2.3 include/configs/:板子的灵魂
这个目录下的头文件,是 U-Boot 的“总开关”。比如:
include/configs/sunxi-common.h
include/configs/mx6ul_14x14_evk.h
里面定义了大量的宏:
CONFIG_SYS_TEXT_BASE:U-Boot 的运行地址。CONFIG_NR_DRAM_BANKS:DDR 的 bank 数量。CONFIG_BOOTCOMMAND:默认的启动命令。
CONFIG_SYS_TEXT_BASE 后,忘了同步修改链接脚本。结果 U-Boot 启动后,直接跑飞了。记住,这个地址必须跟你的硬件设计一致。
2.3 启动流程源码级解析
这部分是重点。U-Boot 的启动,说白了就三个阶段。我画了张图,你配合着看:
2.3.1 第一阶段:SPL(Secondary Program Loader)
SPL 是 U-Boot 的“迷你版”。为什么要有它?因为很多 SoC 的 SRAM 太小了,装不下完整的 U-Boot。SPL 只做一件事:初始化 DDR,然后把完整的 U-Boot 从 Flash 搬到 DDR 里。
入口在 arch/arm/cpu/start.S,汇编写的。你想想看,这时候 C 环境都还没建立呢。SPL 的流程大致是:
- 设置 CPU 为 SVC 模式,关中断。
- 初始化时钟、关闭 MMU 和 Cache。
- 调用
board_init_f(),初始化 DDR。 - 从 Flash/NAND/SD 卡读取完整的 U-Boot 到 DDR。
- 跳转到 U-Boot 的入口。
start.S 里加几个 GPIO 翻转的代码,用逻辑分析仪看波形,确认程序跑到哪一步了。
2.3.2 第二阶段:U-Boot 重定位
完整的 U-Boot 被加载到 DDR 后,会先运行 board_init_r()。这个函数在 common/board_r.c 里。它做几件关键的事:
- 重定位:U-Boot 把自己从加载地址,搬到运行地址。这一步是为了给内核腾出空间。
- 初始化中断向量表:让异常处理能正常工作。
- 初始化串口:这时候你才能在终端上看到 U-Boot 的启动信息。
- 初始化环境变量:加载
bootargs、bootcmd等。
这里有个细节:board_init_r() 里有一个巨大的初始化函数列表,叫 init_sequence_r[]。每个外设的初始化,都是一个函数指针。你如果想让某个外设早点初始化,可以调整这个列表的顺序。
2.3.3 第三阶段:命令行与启动内核
最后,进入 main_loop()。它会检查环境变量 bootdelay,如果在倒计时结束前你没有按任何键,就自动执行 bootcmd 里的命令。默认的 bootcmd 通常是:
# 从 MMC 读取内核到内存,然后启动
mmc read 0x42000000 0x800 0x4000
bootz 0x42000000
如果你按了键,就进入 U-Boot 的命令行,可以手动输入命令调试。
bootdelay 设成 -1,这样 U-Boot 启动后直接进命令行,不会自动启动内核。等所有外设都调通了,再改回正常值。
2.4 关键文件速查表
为了方便你以后查阅,我把关键文件整理成了表格:
| 文件路径 | 作用 | 移植时是否修改 |
|---|---|---|
arch/arm/cpu/start.S |
SPL 入口,汇编初始化 | 一般不修改 |
board/<vendor>/<board>/board.c |
板级初始化函数 | 必须修改 |
include/configs/<board>.h |
板级配置宏定义 | 必须修改 |
arch/arm/dts/<board>.dts |
设备树文件 | 根据硬件修改 |
common/board_r.c |
第二阶段初始化流程 | 一般不修改 |
common/main.c |
命令行主循环 | 一般不修改 |
嗯,到这里,U-Boot 的源码骨架你应该心里有数了。下次拿到一个新板子,别急着改代码。先找到 board/ 和 include/configs/ 下的参考文件,读一遍,再动手。你会发现,移植工作其实没那么玄乎。