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/下的文件。其他目录基本不用动。

我的经验: 别一上来就改arch/下的代码。除非你确定CPU内核有bug,否则99%的问题出在板级配置上。

3.2 板级配置(Board Config):从零开始配一块板子

板级配置,说白了就是告诉U-Boot:「我的板子上有哪些外设?内存多大?时钟多快?」

配置入口在include/configs/下,每个板子一个头文件。比如include/configs/am57xx_evm.h

核心配置项:

  • 内存配置CONFIG_NR_DRAM_BANKSCONFIG_SYS_SDRAM_BASE。我在项目中遇到过,DDR时序参数配错,板子死活起不来。后来用示波器一根一根量地址线才找到问题。
  • 存储设备CONFIG_MMCCONFIG_SPI_FLASH。自动驾驶平台一般用eMMC或SD卡存内核和文件系统。
  • 网络配置CONFIG_ETHCONFIG_PHY。调试阶段,网络是救命稻草。
  • 命令行功能CONFIG_CMD_*。我建议把CONFIG_CMD_EXT4CONFIG_CMD_FATCONFIG_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源码,多麻烦。设备树让硬件描述和驱动代码解耦了。

我的习惯: 在自动驾驶平台上,我会把摄像头、激光雷达的I2C地址、中断号都写在设备树里。这样换传感器型号时,只改dts文件,不用动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配好了,内核移植就轻松一半。