3. Bootloader移植基础:U-Boot源码结构、ARMv8启动流程、板级配置与设备树基础

好,咱们进入第三章。Bootloader移植,说白了就是让板子先跑起来。很多新手一上来就急着改代码,结果连板子都点不亮。我刚开始做ARMv8移植时也犯过这毛病——U-Boot刷进去,串口没输出,折腾了两天才发现是时钟配置错了。

这一章,咱们把U-Boot的底裤扒开看看。你搞懂了源码结构、启动流程、板级配置和设备树,移植就是个填空游戏。

3.1 U-Boot源码结构:别被目录吓到

U-Boot的源码目录乍一看挺唬人,其实核心就几个。我习惯把它分成三块:平台无关层、平台相关层和板级层。

目录/文件 作用 移植时改不改?
arch/arm/cpu/armv8/ ARMv8通用CPU初始化代码 通常不改
arch/arm/mach-<soc>/ SoC厂商相关代码(如mach-bcm283x) 需要适配
board/<vendor>/<board>/ 板级初始化、DDR配置、GPIO 重点改
include/configs/<board>.h 板级配置头文件 重点改
dts/ 设备树源文件 必须改
drivers/ 驱动框架(串口、网卡、MMC等) 按需添加
我的习惯:拿到一个新板子,先看 board/ 和 include/configs/ 这两个目录。它们决定了板子能不能跑起来。驱动问题可以后面慢慢调,板级配置错了连串口都看不到。

3.2 ARMv8启动流程:从EL3到OS

ARMv8的启动流程,说白了就是异常级别的跳转。芯片上电后,从EL3开始,一步步往下交权。

为什么会这样设计?因为安全。EL3是最高权限,负责初始化安全世界;EL2给虚拟化用;EL1跑操作系统。你想想看,如果OS直接跑在EL3,一个内核崩溃就能把整个系统带崩。

我画了个流程图,你看一眼就明白了:

ARMv8 U-Boot 启动流程 ROM Boot (EL3) SPL/TPL (EL3) U-Boot Proper (EL2/EL1) 加载设备树 (FDT) 启动OS (EL1) 关键说明 • ROM: 固化代码,加载SPL • SPL: 初始化DDR,加载U-Boot • U-Boot: 加载设备树和内核 • 设备树: 硬件描述传递给OS • OS: 接管系统 异常级别变化: EL3 → EL3 → EL2/EL1 → EL1

嗯,这里要注意:SPL不是必须的。如果你的片内SRAM够大,可以直接跑完整的U-Boot。但大多数ARMv8芯片的SRAM只有几十KB,所以SPL几乎是标配。

核心要点:SPL负责初始化DDR和时钟,然后从存储介质(SD卡、eMMC、NAND)加载U-Boot proper到DDR。移植时,SPL的DDR初始化代码是第一个要搞定的。

3.3 板级配置:从零开始配一个板子

板级配置,说白了就是告诉U-Boot:你的板子用哪个串口、DDR多大、时钟多少。这些信息集中在两个地方:include/configs/ 下的头文件和 board/ 目录下的C代码。

我拿一个典型的ARMv8板子举例,配置头文件长这样:

/* include/configs/myboard.h */
#ifndef __MYBOARD_H
#define __MYBOARD_H

#include <configs/rpi.h>

/* 串口配置 */
#define CONFIG_SYS_NS16550
#define CONFIG_SYS_NS16550_SERIAL
#define CONFIG_SYS_NS16550_REG_SIZE    -4
#define CONFIG_SYS_NS16550_CLK         50000000
#define CONFIG_CONS_INDEX              1

/* DDR配置 */
#define CONFIG_NR_DRAM_BANKS           1
#define PHYS_SDRAM_1                   0x40000000
#define PHYS_SDRAM_1_SIZE              0x80000000  /* 2GB */

/* 环境变量存储 */
#define CONFIG_ENV_SIZE                0x20000
#define CONFIG_ENV_SECT_SIZE           0x20000
#define CONFIG_ENV_OFFSET              0x100000

/* 网络配置 */
#define CONFIG_ETHPRIME                "eth0"
#define CONFIG_PHY_GIGE

#endif /* __MYBOARD_H */

我曾经在一个项目里,因为 CONFIG_SYS_NS16550_CLK 写错了,串口输出全是乱码。查了两天才发现是时钟频率不对。你想想看,串口波特率计算依赖这个值,差一点就全乱套。

避坑指南:配置头文件里的宏定义,不要随便抄别的板子。每个SoC的时钟树、DDR地址映射都不一样。我建议你先看芯片手册,把关键参数确认一遍再写。

3.4 设备树基础:硬件描述的艺术

设备树(Device Tree),说白了就是一份硬件清单。U-Boot用它来知道板子上有哪些外设、地址是多少、中断怎么连。ARMv8的U-Boot已经全面转向设备树,不再像老ARM那样在头文件里硬编码。

一个简单的设备树节点长这样:

/* myboard.dts */
/dts-v1/;

#include "soc.dtsi"

/ {
    model = "My ARMv8 Board";
    compatible = "myvendor,myboard", "myvendor,soc";

    chosen {
        stdout-path = &uart0;
    };

    memory@40000000 {
        device_type = "memory";
        reg = <0x0 0x40000000 0x0 0x80000000>;
    };

    uart0: serial@1c090000 {
        compatible = "ns16550a";
        reg = <0x0 0x1c090000 0x0 0x1000>;
        clock-frequency = <50000000>;
        interrupts = <0 37 4>;
    };
};

移植时,设备树要改的地方主要是:

  • memory节点:DDR的起始地址和大小
  • chosen节点:指定控制台串口
  • 各外设节点:地址、时钟、中断号
  • compatible属性:板子和SoC的兼容字符串
我的经验:设备树编译用 dtc 工具。调试时可以用 fdt 命令在U-Boot命令行里查看和修改设备树。比如 fdt list / 可以看根节点下的所有子节点,非常方便。

3.5 移植实战:三步走策略

好了,理论说完了,咱们来点实际的。移植U-Boot到一块新ARMv8板子,我一般分三步:

  1. 选参考板:找一个SoC相近的板子,复制 board/ 和 include/configs/ 目录,改名字和关键参数。
  2. 配DDR和时钟:这是最关键的。DDR初始化代码通常在 board/<vendor>/<board>/ 下的 ddr.c 或 lowlevel_init.S 里。时钟配置在 arch/arm/mach-<soc>/ 里。
  3. 调串口:串口能输出,你就成功了一半。调通串口后,后续的调试就方便多了。

我记得有一次移植,DDR参数配错了,板子死活起不来。后来用JTAG抓波形,发现DDR的时序完全不对。嗯,从那以后我养成了一个习惯:DDR参数一定要对照芯片手册的时序图,一个一个算清楚。

总结一下:U-Boot移植的核心就是三板斧——板级配置、设备树、DDR初始化。这三样搞定了,剩下的驱动问题都是小菜一碟。