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,一个内核崩溃就能把整个系统带崩。
我画了个流程图,你看一眼就明白了:
嗯,这里要注意:SPL不是必须的。如果你的片内SRAM够大,可以直接跑完整的U-Boot。但大多数ARMv8芯片的SRAM只有几十KB,所以SPL几乎是标配。
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 写错了,串口输出全是乱码。查了两天才发现是时钟频率不对。你想想看,串口波特率计算依赖这个值,差一点就全乱套。
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板子,我一般分三步:
- 选参考板:找一个SoC相近的板子,复制
board/和include/configs/目录,改名字和关键参数。 - 配DDR和时钟:这是最关键的。DDR初始化代码通常在
board/<vendor>/<board>/下的ddr.c或lowlevel_init.S里。时钟配置在arch/arm/mach-<soc>/里。 - 调串口:串口能输出,你就成功了一半。调通串口后,后续的调试就方便多了。
我记得有一次移植,DDR参数配错了,板子死活起不来。后来用JTAG抓波形,发现DDR的时序完全不对。嗯,从那以后我养成了一个习惯:DDR参数一定要对照芯片手册的时序图,一个一个算清楚。