4. 版本控制与分支策略
好,我们进入第四章。这一章的内容,说白了就是搞明白两件事:Linux内核是怎么活过来的,以及它怎么知道自己活在什么硬件上。前者是启动流程,后者就是设备树(DTS)。在TI Jacinto平台上,这两件事尤其关键——因为SoC太复杂了,没有设备树,内核就是个瞎子。
4.1 内核解压与启动流程:从Bootloader到用户空间
我记得刚入行那会儿,看着串口打印的启动日志,总觉得内核是“啪”一下就直接跑起来的。后来自己调了一次启动卡死,才老老实实把整个流程啃了一遍。
在ARM64平台上,典型的启动路径是这样的:
- Boot ROM:芯片上电,ROM代码执行,加载SPL(Secondary Program Loader)。
- SPL / ATF:初始化DDR、时钟,加载TF-A(Trusted Firmware),然后跳转到U-Boot。
- U-Boot:加载内核镜像(Image)、设备树(DTB)、ramdisk,然后执行
booti命令。 - 内核解压:如果内核是压缩的(zImage或Image.gz),自解压代码先运行,解压出原始内核。
- 内核入口:跳转到
start_kernel(),开始C语言世界。
这里有个细节,我特别想强调一下:设备树(DTB)的物理地址。U-Boot会把DTB放在内存的某个位置,然后通过寄存器x0(ARM64约定)把地址传给内核。内核在start_kernel()之前,会先解析这个DTB,建立device_node树。如果地址传错了,内核直接就panic了。我曾经在调一块新板子时,因为U-Boot的fdt_addr设错了,折腾了一整天——嗯,从那以后我每次都会在U-Boot里打印一下fdt addr确认。
关键节点速览
| 阶段 | 主要动作 | 涉及文件 |
|---|---|---|
| U-Boot | 加载Image + DTB到DDR,设置启动参数 | u-boot.dtb, boot.scr |
| 解压 | 自解压代码解压内核到目标地址 | arch/arm64/boot/Image |
| 早期汇编 | 设置页表、使能MMU、跳转start_kernel | head.S, proc.S |
| start_kernel | 初始化调度器、内存管理、中断、设备树 | init/main.c |
4.2 设备树(DTS)语法与绑定:硬件描述语言
设备树,说白了就是一份用树形结构描述硬件拓扑的配置文件。它不写逻辑,只写“有什么、在哪里、怎么用”。
我个人习惯把DTS语法分成三个层次来记:
- 节点(Node):用
{}包裹,代表一个硬件设备或总线。节点名格式是name@address。 - 属性(Property):键值对,描述节点的特性。比如
reg = <0x0a000000 0x1000>;表示寄存器基地址和范围。 - 绑定(Binding):约定俗成的规则,告诉驱动怎么解析这些属性。比如
compatible = "ti,j721e-gpio";就是绑定字符串。
来看一个最简单的例子:
/dts-v1/;
/ {
model = "TI J721E EVM";
compatible = "ti,j721e";
chosen {
stdout-path = &main_uart0;
};
memory@80000000 {
device_type = "memory";
reg = <0x00000000 0x80000000 0x00000000 0x80000000>;
};
};
你想想看,这个文件告诉内核三件事:板子型号是J721E、控制台用哪个串口、内存从哪里开始有多大。就这么简单。
避坑指南:我曾经在写DTS时,把reg属性的地址和长度写反了——ARM64下reg是<地址高位 地址低位 长度高位 长度低位>,一共4个32位值。如果只写了两个,内核会只认低32位地址,导致内存映射错误。这个坑,我踩过。
4.3 TI Jacinto平台DTS文件结构:k3-j721e.dtsi
TI Jacinto 7系列的DTS文件组织,是我见过的比较清晰的分层结构。它不像某些平台把所有东西塞进一个文件,而是拆成了多个.dtsi(包含文件),按功能模块划分。
以k3-j721e.dtsi为例,它的结构大致是这样的:
// 顶层SoC描述
k3-j721e.dtsi
├── 包含 k3-j721e-main.dtsi // 主域(Main Domain)设备
│ ├── GIC-500 // 中断控制器
│ ├── UART, I2C, SPI // 低速外设
│ ├── CPSW (Ethernet) // 网络
│ └── PCIe, USB // 高速外设
├── 包含 k3-j721e-mcu.dtsi // MCU域设备
│ ├── MCU UART, MCU I2C
│ └── R5F核心
└── 包含 k3-j721e-wakeup.dtsi // 唤醒域设备
└── DMSC (Device Manager)
这种分层的好处很明显:主域、MCU域、唤醒域的硬件资源是物理隔离的,DTS也对应隔离。调试时只需要关注出问题的那个域,不用在几万行的文件里大海捞针。
我个人的经验是,拿到一个新板子,第一件事就是看dts目录下的Makefile,找到最终生成的dtb文件是由哪些.dtsi组合而成的。这能帮你快速理解硬件拓扑。
注意:TI Jacinto平台使用了#include和/include/两种包含方式。前者是C预处理器,后者是DTC自己的包含指令。我建议统一用#include,因为可以配合宏定义,更灵活。
4.4 中断控制器与GIC驱动:ARM64的中枢神经
中断控制器,是SoC里最核心的模块之一。在Jacinto J721e上,使用的是GIC-500,属于ARM GICv3架构。GICv3相比GICv2,最大的变化是支持了LPI(Locality-specific Peripheral Interrupt)和ITS(Interrupt Translation Service),说白了就是能处理更多的中断源,而且支持虚拟化。
在DTS里,GIC的描述通常长这样:
gic: interrupt-controller@01800000 {
compatible = "arm,gic-v3";
reg = <0x00 0x01800000 0x00 0x10000>, // GICD
<0x00 0x01900000 0x00 0x10000>, // GICR
<0x00 0x3a000000 0x00 0x200000>, // ITS
<0x00 0x3a200000 0x00 0x200000>, // ITS
<0x00 0x3a400000 0x00 0x200000>; // ITS
interrupt-controller;
#interrupt-cells = <3>;
#address-cells = <2>;
#size-cells = <2>;
};
这里#interrupt-cells = <3>表示每个中断描述需要3个cell:中断类型(PPI/SPI/LPI)、中断号、触发方式。驱动通过of_irq_get()或platform_get_irq()来获取中断号。
GIC驱动在内核中的实现,主要在drivers/irqchip/irq-gic-v3.c。它负责初始化GICD(Distributor)、GICR(Redistributor),注册中断域(irq domain),然后给每个CPU接口分配中断向量。
我记得有一次调试一个网卡中断不响应的问题,查了两天才发现是GICR的初始化顺序不对——某个CPU的Redistributor没有正确映射。后来我在gic_populate_rdist()函数里加了调试打印,才定位到是ACPI表里的GICR基地址写错了。嗯,从那以后我养成了一个习惯:拿到新板子,先读一下/proc/interrupts,确认所有核心的中断控制器都正常工作。
GICv3关键寄存器区域
| 区域 | 偏移 | 功能 |
|---|---|---|
| GICD | 0x1800000 | Distributor:全局中断配置、使能、优先级 |
| GICR | 0x1900000 | Redistributor:每CPU中断管理、LPI配置 |
| ITS | 0x3a00000 | Interrupt Translation Service:MSI中断路由 |
4.5 本章知识结构图
下面这张图,把内核启动、设备树解析、GIC中断控制器的关系串起来了。你可以把它当作本章的“地图”。
这张图从左到右、从上到下展示了整个链路。你可以看到,设备树解析是连接启动流程和驱动初始化的桥梁。没有DTS,内核不知道GIC在哪里,不知道串口在哪里,整个系统就是个空壳。
调试建议:如果你在Jacinto平台上遇到中断不工作的问题,我建议按这个顺序排查:
- 检查DTS中GIC节点的
reg地址是否与TRM一致。 - 确认内核启动日志中是否有
GICv3: ...的打印,以及是否检测到了所有CPU的Redistributor。 - 查看
/proc/interrupts,确认目标中断是否被注册,以及是否被正确路由到某个CPU。 - 如果使用了ITS(MSI中断),检查ITS的映射表是否配置正确。
嗯,这一章的内容就到这里。设备树和GIC是BSP开发中最常打交道的东西,你后面写驱动、调中断,几乎天天都要跟它们见面。把这一章的基础打牢,后面的路会顺很多。