3、Bootloader移植(U-Boot):U-Boot源码结构、板级配置(Board Config)、设备树(Device Tree)基础、U-Boot启动流程分析
好,我们进入第三章。Bootloader移植,说白了就是给硬件平台装上「第一把钥匙」。没有它,Linux内核连启动的机会都没有。在自动驾驶平台上,这块尤其重要——你想想看,摄像头、激光雷达、IMU这些外设,如果Bootloader没配好,后面全是白搭。
我个人习惯用U-Boot,它几乎是嵌入式Linux的事实标准。今天我们就把它掰开揉碎,从源码结构一直聊到启动流程。
3.1 U-Boot源码结构:别被目录吓到
我第一次看U-Boot源码时,说实话有点懵。目录太多了。但摸清规律后,其实很简单。
核心目录就这几个:
| 目录 | 作用 |
|---|---|
arch/ |
架构相关代码。ARM、RISC-V、x86都在这里。我们做自动驾驶,ARM居多。 |
board/ |
板级代码。每个厂商的板子一个文件夹,比如board/ti/am57xx。 |
common/ |
通用功能。命令行、环境变量、启动逻辑都在这里。 |
drivers/ |
驱动。串口、网卡、MMC、I2C、SPI……应有尽有。 |
dts/ |
设备树源文件。这个后面重点讲。 |
include/ |
头文件。板级配置宏定义都在include/configs/下。 |
嗯,这里要注意:移植U-Boot,你主要改的是board/和include/configs/下的文件。其他目录基本不用动。
3.2 板级配置(Board Config):从零开始配一块板子
板级配置,说白了就是告诉U-Boot:「我的板子上有哪些外设?内存多大?时钟多快?」
配置入口在include/configs/下,每个板子一个头文件。比如include/configs/am57xx_evm.h。
核心配置项:
- 内存配置:
CONFIG_NR_DRAM_BANKS、CONFIG_SYS_SDRAM_BASE。我在项目中遇到过,DDR时序参数配错,板子死活起不来。后来用示波器一根一根量地址线才找到问题。 - 存储设备:
CONFIG_MMC、CONFIG_SPI_FLASH。自动驾驶平台一般用eMMC或SD卡存内核和文件系统。 - 网络配置:
CONFIG_ETH、CONFIG_PHY。调试阶段,网络是救命稻草。 - 命令行功能:
CONFIG_CMD_*。我建议把CONFIG_CMD_EXT4、CONFIG_CMD_FAT、CONFIG_CMD_NET都打开,调试时方便。
避坑指南: 我曾经在配置CONFIG_SYS_TEXT_BASE时写错了地址,导致U-Boot把自己覆盖了。嗯,从那以后我每次改这个宏都会反复确认。
举个例子,一个典型的板级配置片段:
/* include/configs/my_autonomous_board.h */
#define CONFIG_NR_DRAM_BANKS 2
#define CONFIG_SYS_SDRAM_BASE 0x80000000
#define CONFIG_SYS_TEXT_BASE 0x80800000
#define CONFIG_MMC
#define CONFIG_CMD_EXT4
#define CONFIG_CMD_FAT
#define CONFIG_CMD_NET
/* 网络PHY地址 */
#define CONFIG_PHY_ADDR 0x04
3.3 设备树(Device Tree)基础:硬件描述语言
设备树,说白了就是一份「硬件清单」。U-Boot用它来知道:哪个GPIO接了LED?哪个I2C总线上挂了摄像头?
设备树源文件(.dts)经过编译生成.dtb,U-Boot在启动时解析它。
基本语法:
/dts-v1/;
/ {
model = "My Autonomous Driving Board";
compatible = "mycompany,myboard";
chosen {
stdout-path = &uart0;
};
memory@80000000 {
device_type = "memory";
reg = <0x0 0x80000000 0x0 0x80000000>;
};
uart0: serial@ff010000 {
compatible = "arm,pl011";
reg = <0x0 0xff010000 0x0 0x1000>;
clock-frequency = <24000000>;
};
};
你想想看,如果没有设备树,每次换一个外设都要改U-Boot源码,多麻烦。设备树让硬件描述和驱动代码解耦了。
3.4 U-Boot启动流程分析:从复位到命令行
U-Boot启动,分两个阶段。我画个简化的流程图:
复位向量
↓
第一阶段(arch/arm/cpu/armv7/start.S)
├── 设置CPU模式(SVC模式)
├── 关闭中断
├── 初始化C运行环境(设置栈指针、清零BSS段)
└── 跳转到board_init_f()
↓
第二阶段(common/board_f.c → common/board_r.c)
├── board_init_f(): 初始化DRAM、串口、时钟
├── 重定位U-Boot自身到DRAM
├── board_init_r(): 初始化所有外设
├── 解析设备树
└── 进入命令行或自动启动内核
嗯,这里有几个关键点:
- 第一阶段用汇编写,因为这时候C环境还没准备好。我刚开始学的时候,总觉得汇编代码晦涩,后来发现其实就那么几个动作——设置栈、清BSS、跳转。
- 第二阶段分两步:
board_init_f做早期初始化,board_init_r做完整初始化。为什么要分?因为早期初始化时U-Boot还在Flash里跑,速度慢,只能做必要的事。重定位到DRAM后,才能放开手脚。 - 设备树解析在第二阶段。U-Boot会读取
/chosen节点下的bootargs,传给内核。
注意: 在自动驾驶平台上,启动时间很关键。我建议在board_init_f阶段就初始化串口,这样一旦出问题,能第一时间看到打印信息。我曾经因为串口初始化太晚,浪费了整整两天调试时间。
最后,U-Boot启动完成后,会执行bootcmd环境变量里的命令。典型的自动驾驶平台启动命令:
setenv bootcmd 'mmc read 0x82000000 0x800 0x4000; bootm 0x82000000'
这条命令的意思:从MMC(eMMC或SD卡)的第0x800个扇区开始,读取0x4000个扇区到内存0x82000000地址,然后启动内核。
好了,这一章的内容就这些。下一章我们会聊Linux内核移植,到时候你会看到,U-Boot配好了,内核移植就轻松一半。