4. 版本控制与分支策略

好,我们进入第四章。这一章的内容,说白了就是搞明白两件事:Linux内核是怎么活过来的,以及它怎么知道自己活在什么硬件上。前者是启动流程,后者就是设备树(DTS)。在TI Jacinto平台上,这两件事尤其关键——因为SoC太复杂了,没有设备树,内核就是个瞎子。

4.1 内核解压与启动流程:从Bootloader到用户空间

我记得刚入行那会儿,看着串口打印的启动日志,总觉得内核是“啪”一下就直接跑起来的。后来自己调了一次启动卡死,才老老实实把整个流程啃了一遍。

在ARM64平台上,典型的启动路径是这样的:

  1. Boot ROM:芯片上电,ROM代码执行,加载SPL(Secondary Program Loader)。
  2. SPL / ATF:初始化DDR、时钟,加载TF-A(Trusted Firmware),然后跳转到U-Boot。
  3. U-Boot:加载内核镜像(Image)、设备树(DTB)、ramdisk,然后执行booti命令。
  4. 内核解压:如果内核是压缩的(zImage或Image.gz),自解压代码先运行,解压出原始内核。
  5. 内核入口:跳转到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_kernelhead.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关键寄存器区域

区域偏移功能
GICD0x1800000Distributor:全局中断配置、使能、优先级
GICR0x1900000Redistributor:每CPU中断管理、LPI配置
ITS0x3a00000Interrupt Translation Service:MSI中断路由

4.5 本章知识结构图

下面这张图,把内核启动、设备树解析、GIC中断控制器的关系串起来了。你可以把它当作本章的“地图”。

第4章知识结构图:内核启动与DTS解析 第4章 知识结构:内核启动 → DTS解析 → GIC中断 Boot ROM SPL / ATF U-Boot 内核解压 start_kernel() → 早期初始化 unflatten_device_tree() 解析DTB → device_node树 of_platform_populate() 创建platform_device 驱动匹配 compatible匹配 GIC驱动初始化 (irq-gic-v3.c) GICD → GICR → ITS → irq_domain注册 核心逻辑:U-Boot传递DTB地址 → 内核解析DTS → 创建设备 → GIC驱动注册中断控制器 → 外设驱动使用中断

这张图从左到右、从上到下展示了整个链路。你可以看到,设备树解析是连接启动流程和驱动初始化的桥梁。没有DTS,内核不知道GIC在哪里,不知道串口在哪里,整个系统就是个空壳。

调试建议:如果你在Jacinto平台上遇到中断不工作的问题,我建议按这个顺序排查:

  1. 检查DTS中GIC节点的reg地址是否与TRM一致。
  2. 确认内核启动日志中是否有GICv3: ...的打印,以及是否检测到了所有CPU的Redistributor。
  3. 查看/proc/interrupts,确认目标中断是否被注册,以及是否被正确路由到某个CPU。
  4. 如果使用了ITS(MSI中断),检查ITS的映射表是否配置正确。

嗯,这一章的内容就到这里。设备树和GIC是BSP开发中最常打交道的东西,你后面写驱动、调中断,几乎天天都要跟它们见面。把这一章的基础打牢,后面的路会顺很多。