3. BSP软件架构与启动流程

设备树,说白了就是一块「硬件描述板」。它告诉内核:你有哪些外设、它们挂在哪条总线上、寄存器地址是多少、中断号是什么。没有设备树,内核就得靠一堆硬编码的板级文件来认硬件——那维护起来简直是噩梦。

我在 Jacinto 平台上最早踩的坑,就是设备树里一个 reg 属性写错了字节数,结果 SPI 控制器死活不干活。查了两天才发现,嗯,就是少了个 0x 前缀。从那以后,我对 DTS 的每个字段都格外小心。

3.1 设备树基础语法——你得先会「读树」

设备树的源文件是 .dts,编译后变成 .dtb。内核启动时,Bootloader 把 .dtb 传给内核,内核解析后就知道硬件长什么样了。

一个典型的节点长这样:

/dts-v1/;

/ {
    model = "TI Jacinto 7 EVM";
    compatible = "ti,j721e-evm", "ti,j721e";

    chosen {
        stdout-path = &main_uart0;
    };

    memory@80000000 {
        device_type = "memory";
        reg = <0x00 0x80000000 0x00 0x80000000>;
    };

    main_uart0: serial@2800000 {
        compatible = "ti,j721e-uart", "ti,omap4-uart";
        reg = <0x00 0x02800000 0x00 0x100>;
        interrupts = <GIC_SPI 192 IRQ_TYPE_LEVEL_HIGH>;
        clocks = <&k3_clks 146 0>;
        status = "okay";
    };
};

这里有几个关键点:

  • 节点名:serial@2800000,@后面是基地址,方便人眼识别。
  • 标签:main_uart0:,方便在其他地方引用,比如 &main_uart0。
  • 属性:compatible 是驱动匹配的关键,内核靠它找到对应的驱动。
  • reg:地址和长度,格式是 <地址高位 地址低位 长度高位 长度低位>,64位平台尤其要注意。
我的习惯:写 reg 属性时,我会先查 TRM 里的 memory map 表格,确认外设的地址范围和长度。长度写小了,驱动访问会越界;写大了,可能和其他设备冲突。

3.2 绑定文档——驱动和设备的「契约」

绑定文档(Binding Document)是内核里 Documentation/devicetree/bindings/ 目录下的 .yaml 文件。它规定了某个外设节点必须有哪些属性、可选哪些属性、值的格式是什么。

举个例子,TI Jacinto 上的 McASP(多通道音频串行端口)绑定文档会告诉你:

属性 是否必须 说明
compatible 是 必须包含 "ti,j721e-mcasp"
reg 是 寄存器基地址和长度
interrupts 是 中断号,GIC 类型
clocks 是 时钟句柄,来自 k3_clks
dmas 否 DMA 通道,如果使用 DMA 传输则需要
status 否 默认 "okay",禁用时写 "disabled"

我曾经犯过一个错:给 McASP 配了 DMA 通道,但忘了在绑定文档里查 dmas 属性的格式。结果 DMA 请求号写反了,音频数据全乱套。后来我养成了习惯——每加一个新节点,先翻绑定文档,对着格式写属性。

注意:绑定文档不是摆设。内核在编译时会用 dt-bindings 检查你的 DTS 是否符合规范。如果 compatible 字符串写错了,编译不会报错,但驱动加载时会直接跳过你的设备。

3.3 在 Jacinto 平台上添加自定义外设节点

假设我们要在 Jacinto 7 的 MAIN 域添加一个自定义的 GPIO 扩展器,挂在 I2C0 总线上。地址是 0x20,支持 8 个 GPIO 口。

步骤很简单,但每一步都有坑:

  1. 找到正确的 .dtsi 文件——Jacinto 7 的 MAIN 域设备树在 arch/arm64/boot/dts/ti/k3-j721e-main.dtsi。别加到 WAKEUP 域去,否则驱动找不到时钟。
  2. 在 I2C0 节点下添加子节点——I2C0 的标签通常是 &main_i2c0。
  3. 写对 compatible 字符串——假设我们的驱动是 my_gpio_expander,那就写 "my,gpio-expander"。
  4. 配置 reg 为 I2C 地址——0x20,注意是 7 位地址,不要写 8 位。
  5. 添加 gpio-controller 和 #gpio-cells——告诉内核这是个 GPIO 控制器。

最终代码长这样:

&main_i2c0 {
    status = "okay";
    clock-frequency = <400000>;

    my_gpio: gpio-expander@20 {
        compatible = "my,gpio-expander";
        reg = <0x20>;
        gpio-controller;
        #gpio-cells = <2>;
        interrupt-parent = <&main_gpio0>;
        interrupts = <10 IRQ_TYPE_EDGE_FALLING>;
        status = "okay";
    };
};

这里有个细节:#gpio-cells = <2> 表示引用这个 GPIO 时,需要两个参数——GPIO 编号和标志位。比如 &my_gpio 3 GPIO_ACTIVE_LOW。

核心要点:设备树不是写完了就完事。你得用 dtc 工具编译成 .dtb,然后反编译回来检查有没有丢失属性。我每次都会做这个「编译-反编译」验证,确保设备树没有被编译器优化掉关键信息。

3.4 设备树知识体系总览

下面这张图帮你理清设备树的核心逻辑:从 DTS 源文件到内核驱动匹配的完整链路。

设备树知识体系总览 设备树知识体系总览 DTS 源文件 dtc 编译 DTB 二进制 Bootloader 传递给内核 内核解析 → 驱动匹配 绑定文档 定义属性格式 驱动与设备的契约 DTS 语法 节点、属性、标签 引用、覆盖、宏 编译验证:dtc -I dts -O dtb 反编译验证:dtc -I dtb -O dts

你看,从 DTS 到驱动匹配,中间每一步都有讲究。绑定文档和 DTS 语法是左右两条「护栏」,保证你写的设备树能被内核正确理解。

3.5 避坑指南——我替你踩过的雷

  • 地址写错:Jacinto 7 是 64 位平台,reg 属性要用两个 <> 表示高 32 位和低 32 位。我见过有人只写一个 <0x2800000 0x100>,结果内核把地址解析成 0x0000000002800000,长度变成 0x0000000000000100——虽然碰巧对了,但遇到高地址外设就崩。
  • 中断号搞混:Jacinto 有 MAIN 域和 WAKEUP 域,中断控制器不同。MAIN 域用 GIC_SPI,WAKEUP 域用 GIC_SPI 但中断号偏移不同。查 TRM 时一定要确认是哪个域的。
  • 时钟依赖没配:有些外设需要多个时钟,比如 McASP 需要 ahclkx 和 auxclk。只配一个时钟,驱动 probe 会失败,而且日志里只打印一句 clk_prepare_enable failed——排查起来很痛苦。
  • status 忘记写:默认 status = "disabled",如果你不显式写 "okay",内核不会使能这个设备。我调试时经常怀疑驱动有问题,结果发现是设备树没打开。
我的调试三板斧:第一,cat /proc/device-tree/ 看内核解析后的设备树;第二,dmesg | grep -i "of_" 看设备树解析日志;第三,反编译 .dtb 对比原始 .dts。这三招能解决 90% 的设备树问题。

设备树这东西,说难不难,说简单也不简单。关键是多写、多查、多验证。你想想看,一个 reg 属性写错,可能让你浪费一整天。但只要掌握了语法和绑定文档的套路,后面就顺了。