1. 课程导入与设备树全景

在工业控制、边缘网关、能源采集等场景中,一块核心板往往要适配多种外设:RS485 收发器、CAN 控制器、工业以太网 PHY、ADC 采集芯片、看门狗、RTC、GPIO 扩展器、温度传感器等。很多工程师第一次接触 Linux 驱动时,习惯把硬件信息写死在驱动源码里:在 probe() 中直接写死寄存器基地址、GPIO 编号、中断号、时钟频率。这样的代码在单一板卡上能跑,一旦换板、换芯片、换引脚复用方案,就要改驱动、重新编译内核,维护成本极高。

设备树(Device Tree,简称 DT)正是为解决这类问题而引入的硬件描述机制。它把“硬件长什么样、接在哪里、用什么参数”从内核代码中剥离出来,用一份可独立编译、可被 bootloader 传递的数据结构描述。内核启动时解析设备树,生成 device_node 与 platform_device,再与驱动中的 of_device_id 匹配表进行比对,匹配成功即调用驱动的 probe()。理解这条链路,是工业外设驱动适配的第一课。

设备树在启动链路中的位置

以常见的 ARM64 工业板为例,上电后的关键阶段如下:ROM Code 加载 bootloader(如 U-Boot),U-Boot 把内核镜像 Image 与设备树二进制 .dtb 一起加载到内存,并通过 x0 寄存器把 DTB 地址传给内核。内核在 setup_arch() 阶段调用 unflatten_device_tree(),把扁平的 DTB 展开成树形结构,随后在 of_platform_default_populate_init() 中遍历节点,为每个带 compatible 属性的节点创建 platform_device。驱动注册时,总线核心用 of_match_device() 比较设备节点的 compatible 与驱动匹配表,命中后执行 probe(),驱动再从节点中读取 reg、interrupts、clocks、gpios 等属性完成资源申请。

设备树在 Linux 启动与驱动匹配链路中的角色 工业板硬件 SoC / 外设 / 引脚复用 RS485、CAN、ADC、PHY 中断线、时钟、供电域 DTS 源文件 board.dts + soc.dtsi compatible / reg / interrupts pinctrl / clocks / gpios DTC 编译 dtc -I dts -O dtb 生成 board.dtb Bootloader U-Boot 加载 Image 与 board.dtb 到内存 x0 = DTB 物理地址 内核解析 unflatten_device_tree() 生成 device_node 树 of_platform_populate() platform_device 每个 compatible 节点 注册到 platform 总线 驱动模块 of_device_id 匹配表 platform_driver 注册 probe / remove 回调 匹配与 probe of_match_device() 读取 reg / irq / clk 申请资源并初始化 用户空间 /dev/ttyS*、/sys/class/* 工业应用读写外设 关键点:compatible 是设备与驱动之间的“契约字符串”,reg / interrupts / clocks / pinctrl 是驱动 probe 时读取的资源来源

工业外设适配的典型痛点

把设备树引入之前,工业外设适配常见四类问题。第一类是地址与中断硬编码:驱动里写死 0xFE000000 这样的基地址,换 SoC 后全部失效。第二类是引脚复用冲突:同一组引脚在不同板卡上可能接 RS485 的 DE 控制、也可能接 CAN 的收发使能,若在驱动中直接操作 GPIO,换板就要改代码。第三类是多实例管理混乱:一块板上有 4 路 RS485,驱动若只支持单实例,就需要复制多份代码。第四类是板级差异无法复用:同一驱动要同时支持 A 板和 B 板,只能靠 #ifdef 或模块参数,可读性差。

设备树用统一模型解决这些问题:地址、中断、时钟、引脚、GPIO 全部写在节点属性里;多路外设通过多个节点实例化;板级差异通过不同 .dts 文件覆盖 .dtsi 中的默认值。驱动只负责“拿到资源、操作硬件”,不再关心“资源在哪里”。

本课程实战目标与工具链准备

本课程围绕“能写、能编、能调、能验”四个目标展开。能写,指能根据原理图和 SoC 手册写出正确的设备树节点;能编,指能用 dtc 和内核构建系统生成 .dtb;能调,指能通过 /proc/device-tree、dmesg、of_node 调试匹配失败;能验,指能用示波器、逻辑分析仪或应用层读写验证外设真实工作。

工具链方面,建议准备以下环境。交叉编译工具链以 ARM64 为例,可使用 aarch64-linux-gnu- 前缀;设备树编译器 dtc 建议使用内核源码 scripts/dtc 下同版本,避免语法差异;内核源码需包含目标 SoC 的 arch/arm64/boot/dts/ 目录;调试侧准备串口终端、dmesg、ls /proc/device-tree、cat /sys/firmware/devicetree/base/... 等基本手段。若使用 U-Boot,还需确认 bootargs 与 fdt_addr_r 配置正确。

建议在实验板上先执行 ls /proc/device-tree 与 find /proc/device-tree -name compatible | head,观察真实设备树结构,再对照源码中的 .dts 文件,建立“源码—DTB—运行时节点”的对应关系。

一个最小可观察示例

下面是一段简化的 UART 节点示例,用于说明设备树如何描述一个工业串口。假设 SoC 手册给出 UART2 基地址为 0xFE001000,中断号为 34,时钟为 uart2_clk,引脚复用为 uart2_pins。示例参数仅用于教学,实际值需以具体手册为准。

&uart2 {
    compatible = "vendor,uart-2.0";
    reg = <0xFE001000 0x100>;
    interrupts = <GIC_SPI 34 IRQ_TYPE_LEVEL_HIGH>;
    clocks = <&cru CLK_UART2>;
    pinctrl-names = "default";
    pinctrl-0 = <&uart2_pins>;
    status = "okay";
};

驱动侧匹配表可写为 { .compatible = "vendor,uart-2.0" }。当内核启动后,若匹配成功,dmesg 中会出现类似 vendor-uart fe001000.serial: probed 的日志;若失败,常见原因是 status 为 disabled、compatible 拼写不一致、reg 与 ranges 不匹配或时钟未使能。通过 cat /proc/device-tree/soc/uart@fe001000/compatible 可确认运行时节点内容,通过 dmesg | grep -i uart 可观察匹配与 probe 过程。

设备树中的 compatible 是驱动匹配的唯一契约,必须与驱动 of_device_id 完全一致(含厂商前缀与版本后缀)。修改后需重新编译 DTB 并重启,不能只替换内核模块。