第3章:U-Boot源码结构分析

好,咱们今天来聊聊U-Boot的源码结构。说实话,我刚接触U-Boot那会儿,打开源码目录一看,好家伙,几百个文件夹,头都大了。但后来我摸清了一个规律——U-Boot的目录结构其实非常清晰,它遵循了Linux内核的布局思想。你只要抓住几个关键目录和文件,整个框架就豁然开朗了。

3.1 U-Boot目录结构概览

我们先从顶层目录看起。我个人习惯把U-Boot的目录分成三大块:平台无关代码、平台相关代码、以及构建系统。下面这张表是我整理的核心目录说明:

目录名 作用 我的备注
arch/ CPU架构相关代码(arm、riscv、x86等) 移植时最常改的地方
board/ 具体板级初始化代码 每个板子一个子目录
common/ 通用功能模块(命令、控制台等) 这里面的代码大部分不用动
drivers/ 设备驱动(串口、网卡、存储等) 嗯,驱动开发的主战场
include/ 头文件和配置文件 configs/下放板级配置
lib/ 通用库函数(字符串、CRC等) 跟应用层编程差不多
net/ 网络协议栈(TFTP、NFS等) 调试时经常用到
cmd/ 命令行命令实现 想加自定义命令?来这里
tools/ 主机端工具(mkimage等) 打包镜像用的

你想想看,这么多目录,其实真正需要你深入理解的,也就那么几个。我建议你重点看arch/board/common/这三个。为什么?因为U-Boot的启动流程就藏在这三个目录里

3.2 关键文件深度解析

3.2.1 start.S — 一切从这里开始

这是U-Boot的入口文件,通常放在arch/arm/cpu/armv7/start.S(ARM为例)。说白了,CPU上电后第一条指令就在这里。我刚开始看汇编代码时也觉得头大,但后来发现它其实就干三件事:

  1. 设置CPU为SVC模式,关中断
  2. 初始化关键硬件:关闭MMU和Cache、设置栈指针
  3. 跳转到C语言入口:调用board_init_f()

这里有个细节我特别想强调——start.S中有一段代码叫“重定位”(relocation)。U-Boot会把自己从Flash拷贝到RAM中运行,这段逻辑就在start.S里。我曾经在一个项目里遇到启动失败,查了两天才发现是重定位地址配置错了。嗯,这种坑踩过一次就记住了。

/* 典型的start.S片段 */
reset:
    /* 设置SVC模式,关中断 */
    mrs r0, cpsr
    bic r0, r0, #0x1f
    orr r0, r0, #0xd3
    msr cpsr, r0

    /* 关闭MMU和Cache */
    mrc p15, 0, r0, c1, c0, 0
    bic r0, r0, #0x0005
    mcr p15, 0, r0, c1, c0, 0

    /* 设置栈指针 */
    ldr sp, =CONFIG_SYS_INIT_SP_ADDR

    /* 跳转到C代码 */
    bl board_init_f
注意:start.S中的代码是位置无关的(PIC)。这意味着它可以在任意地址执行。如果你要修改这段代码,一定要保持PIC特性,否则U-Boot在重定位前就会崩溃。

3.2.2 board.c — 板级初始化的心脏

这个文件在board/你的板子/board.c里。它实现了board_init_f()board_init_r()两个关键函数。我习惯把前者叫“前阶段初始化”,后者叫“后阶段初始化”。

board_init_f()干了什么?说白了就是初始化那些在重定位前就必须可用的硬件:

  • 串口(不然你连打印都看不到)
  • 定时器(用于延时)
  • GPIO(控制LED、复位等)
  • DRAM控制器(内存必须能用)

board_init_r()则是重定位完成后执行的:

  • 初始化所有驱动(网卡、存储等)
  • 设置环境变量
  • 进入主循环(等待用户输入命令)

我记得有一次调试一块新板子,串口死活没输出。查了半天,发现是board_init_f()里串口时钟配置错了。你想想看,串口都没输出,你连调试信息都看不到,那感觉就像在黑暗中摸索。后来我学乖了,先在board_init_f()里加个GPIO翻转,用示波器看波形,确认代码确实跑到了那里。

3.2.3 common.c — 通用功能的集散地

这个文件在common/common.c里,它实现了U-Boot的主循环。说白了,就是那个让你输入命令的shell。核心函数是main_loop()

void main_loop(void)
{
    /* 处理自动启动 */
    if (autoboot_command())
        return;

    /* 进入命令行循环 */
    for (;;) {
        len = readline(CONFIG_SYS_PROMPT);
        if (len > 0)
            run_command(len);
    }
}

这里有个关键点——自动启动(autoboot)。U-Boot启动时会检查环境变量bootdelay,如果在倒计时内用户没有按键,就自动执行bootcmd里的命令(通常是启动内核)。这个逻辑就在common/autoboot.c里。我建议你把这个流程理清楚,因为产品发布时,自动启动的稳定性直接影响用户体验。

3.3 编译系统:Kconfig/Makefile

U-Boot的编译系统是从Linux内核学来的,使用Kconfig + Makefile的组合。说实话,刚开始接触Kconfig时我也觉得有点绕,但用顺手了就会发现它真的很强大。

3.3.1 Kconfig — 配置的图形化界面

Kconfig文件定义了所有可配置的选项。比如你想开启某个驱动,就在make menuconfig里勾选。这些选项最终会生成include/config/auto.conf,被Makefile读取。

我举个例子,假设你要添加一个新的板子:

# board/myboard/Kconfig
config TARGET_MYBOARD
    bool "Support myboard"
    select CPU_V7
    help
      This is my custom board based on ARM Cortex-A7.

然后在arch/arm/Kconfig里加上:

source "board/myboard/Kconfig"

这样,你在make menuconfig里就能看到“Support myboard”这个选项了。我个人习惯是先跑一遍make defconfig生成默认配置,再用make menuconfig微调,这样不容易漏掉关键选项。

3.3.2 Makefile — 构建的骨架

U-Boot的Makefile分为三层:

  • 顶层Makefile:定义编译规则、链接脚本、生成u-boot.bin
  • 架构Makefile(arch/arm/Makefile):定义CPU相关的编译选项
  • 板级Makefile(board/myboard/Makefile):定义板级特有的源文件

这里有个小技巧——如果你想添加一个自定义文件到U-Boot中,只需要在对应目录的Makefile里加一行

# board/myboard/Makefile
obj-y += my_custom_init.o

obj-y表示这个文件总是被编译。如果你希望条件编译,可以用obj-$(CONFIG_MY_FEATURE)。我曾经犯过一个错误,把文件加到了obj-后面但忘了加y,结果编译出来啥都没有。嗯,这种低级错误犯一次就够了。

我的建议:在调试阶段,可以用make V=1查看详细的编译命令。这样你能看到编译器到底用了哪些参数,哪些文件被编译了。对于排查编译问题特别有用。

3.4 实战:如何快速定位代码

说了这么多,你可能觉得信息量有点大。别急,我给你一个实战方法——如何快速找到你想修改的代码

  1. 先确定你要改什么:是CPU初始化?板级外设?还是命令?
  2. CPU相关:去arch/目录下找
  3. 板级相关:去board/目录下找
  4. 驱动相关:去drivers/目录下找
  5. 命令相关:去cmd/目录下找

举个例子,如果你想修改串口驱动:

  • 先看drivers/serial/目录
  • 找到对应芯片的驱动文件(比如ns16550.c
  • 看它调用了哪些板级接口(通常在board/目录下实现)

这个方法我用了十年,从来没失手过。你试试看,会发现U-Boot的代码结构其实很友好。

核心要点回顾:

  • U-Boot目录分三大块:平台无关、平台相关、构建系统
  • start.S是入口,负责最底层的CPU初始化
  • board.c实现板级初始化,分前阶段和后阶段
  • common.c提供主循环和命令处理
  • Kconfig/Makefile构成灵活的编译系统

下一章,我们会深入U-Boot的启动流程,从第一条指令到命令行提示符出现,每一步都给你拆解清楚。到时候你会发现,理解了源码结构,启动流程就是水到渠成的事。