4. 内核模块基础

设备树(Device Tree)这东西,说白了就是一块「硬件描述板」。你告诉内核:我板子上接了哪些外设,地址是多少,中断挂在哪条线上。内核不再靠一堆硬编码的板级文件去猜,而是直接读设备树。

我在Tina Linux上最早碰设备树时,也觉得不就是个文本文件嘛。后来踩了几个坑才明白——设备树写错了,驱动根本 probe 不起来,连 log 都不给你留一句。嗯,咱们今天就把这块啃透。

4.1 设备树长什么样?——DTS 语法入门

设备树源文件(.dts)的语法非常直观。它用树形结构描述硬件,根节点是 /,下面挂 CPU、内存、总线、外设。每个节点用 node-name@unit-address 命名,花括号里放属性和子节点。

/dts-v1/;

/ {
    model = "Allwinner H3 Tina Board";
    compatible = "allwinner,sun8i-h3";

    chosen {
        stdout-path = &uart0;
    };

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

    soc {
        compatible = "simple-bus";
        #address-cells = <1>;
        #size-cells = <1>;
        ranges;

        uart0: serial@1c28000 {
            compatible = "allwinner,sun8i-h3-uart";
            reg = <0x01c28000 0x400>;
            interrupts = <0 0 4>;
            status = "disabled";
        };
    };
};

你看,根节点下直接挂 chosen、memory、soc。每个节点用 标签:节点名@地址 的格式,标签(如 uart0)方便其他地方引用。

我的习惯:每个外设节点都加一个标签,哪怕当前没用到。后面要覆盖或引用时,直接 &标签 就行,不用去翻路径。

4.2 节点与属性——设备树的「零件」

节点就是硬件模块,属性就是模块的参数。常用的属性有这些:

属性名 作用 示例
compatible 驱动匹配的关键字,内核靠它找驱动 "allwinner,sun8i-h3-uart"
reg 寄存器地址和长度 <0x01c28000 0x400>
interrupts 中断号、触发类型 <0 0 4>
status 设备状态:okay 或 disabled "okay"
clocks 时钟源引用 <&ccu CLK_UART0>
pinctrl-0 引脚复用配置 <&uart0_pins>

这里有个关键点:compatible 的命名规则是 "厂商,芯片-外设"。内核的 of_match_table 就是拿这个字符串去匹配的。我见过有人把 compatible 写反了顺序,驱动死活不干活——查了两天才发现是字符串没对上。

4.3 reg 属性——地址怎么描述?

reg 描述设备的地址空间。格式是 reg = <地址1 长度1 地址2 长度2 ...>。地址和长度各占几个 cell(32位),由父节点的 #address-cells 和 #size-cells 决定。

// 父节点定义
soc {
    #address-cells = <1>;   // 地址用1个32位
    #size-cells    = <1>;   // 长度用1个32位

    uart0: serial@1c28000 {
        reg = <0x01c28000 0x400>;  // 基址0x01c28000,长度0x400
    };

    emac: ethernet@1c30000 {
        reg = <0x01c30000 0x1000>;
    };
};
我曾经踩过的坑:某次给 SPI 控制器配 reg,父节点 #address-cells=2,我却只写了 1 个地址值。编译不报错,但内核读到的地址完全错位。记住:地址和长度的 cell 数必须跟父节点定义一致。

4.4 interrupts 属性——中断怎么配?

中断描述比 reg 稍微复杂一点,因为它依赖中断控制器。常见格式:

interrupts = <中断类型 中断号 触发方式>;

在全志平台上,GIC(通用中断控制器)通常用 3 个 cell:

  • 第一个 cell:0 表示 SPI(共享外设中断),1 表示 PPI(私有外设中断)
  • 第二个 cell:中断号(从 32 开始偏移)
  • 第三个 cell:触发类型——1 上升沿,4 高电平有效
uart0: serial@1c28000 {
    interrupts = <0 0 4>;
    // 解释:SPI类型,中断号32+0=32,高电平触发
};

gmac: ethernet@1c30000 {
    interrupts = <0 5 4>;
    // 解释:SPI类型,中断号32+5=37,高电平触发
};

你想想看,如果中断号写错了,驱动注册中断时要么冲突,要么根本收不到中断。我调试 GMAC 网卡时,就遇到过中断号偏移算错一位,结果收包中断一直不触发——查了半天的波形才发现是设备树写错了。

4.5 设备树在 Tina 中的位置

Tina Linux 的设备树文件放在 target/allwinner/<芯片系列>/ 目录下。以 H3 为例:

target/allwinner/h3/
├── config-4.9
├── linux/
│   └── linux-4.9/
│       └── arch/arm/boot/dts/
│           ├── sun8i-h3.dtsi          // SoC 级通用定义
│           ├── sun8i-h3-orangepi-pc.dts  // 具体板级
│           └── sun8i-h3-nanopi-m1.dts
└── board/
    └── h3_xxx/
        └── board.dts                  // 板级覆盖

这里有个重要的分层设计:

  • .dtsi 文件:SoC 级通用描述,所有同系列芯片共用。定义了 CPU、GIC、时钟、所有外设节点,但 status 默认设为 disabled。
  • .dts 文件:具体板级描述,通过 #include 包含 .dtsi,然后使能需要用到的外设,配置引脚、时钟等。
  • board.dts:Tina 特有的板级覆盖文件,编译时会跟 .dtsi 合并成最终的 .dtb。
核心原则:不要修改 .dtsi 文件!你只需要在板级 .dts 或 board.dts 中用 &标签 去覆盖属性。这样换板子时,只换 .dts 就行,SoC 级定义完全复用。

4.6 知识结构总览

下面这张图把设备树的核心逻辑串起来了——从 DTS 源文件到内核驱动匹配,再到外设寄存器操作:

设备树核心知识结构 DTS 源文件 (.dts / .dtsi) DTC 编译 → .dtb 二进制 Bootloader 加载 dtb → 内核 内核 OF 子系统解析设备树 compatible 匹配驱动 of_match_table → probe reg 解析地址资源 of_iomap / platform_get_resource interrupts 注册中断 irq_of_parse_and_map Tina 特有:board.dts 覆盖机制 target/allwinner/<芯片>/board/<板型>/board.dts 编译时与 .dtsi 合并,只覆盖差异,不改 SoC 通用定义

4.7 实战建议:从零开始写一个外设节点

假设你要在 H3 板子上添加一个 I2C 触摸屏。步骤很简单:

  1. 找到 SoC 的 .dtsi,确认 I2C 控制器节点已经定义(通常 status = "disabled")。
  2. 在板级 .dts 中使能:&i2c0 { status = "okay"; };
  3. 在 I2C 节点下挂子节点:触摸屏作为 I2C 从设备,用 reg 指定地址。
  4. 配置中断和引脚:触摸屏的 INT 脚接到哪个 GPIO,就在 pinctrl 里配好。
// 在 board.dts 中添加
&i2c0 {
    status = "okay";
    clock-frequency = <400000>;

    touchscreen@38 {
        compatible = "goodix,gt911";
        reg = <0x38>;                 // I2C 从设备地址
        interrupt-parent = <&pio>;    // 中断父节点为 GPIO 控制器
        interrupts = <1 5 2>;         // PH5,下降沿触发
        irq-gpios = <&pio 1 5 GPIO_ACTIVE_LOW>;
        reset-gpios = <&pio 1 4 GPIO_ACTIVE_LOW>;
    };
};
我的建议:写完设备树后,先 make dtbs 编译,再用 dtc -I dtb -O dts xxx.dtb 反编译看看最终生成的设备树对不对。这一步能发现很多 include 和覆盖的问题。

设备树这东西,初看觉得啰嗦,用熟了会发现它让驱动开发变得特别干净。你换板子时,改改 .dts 就行,驱动代码一行都不用动。嗯,这就是设备树的魅力。