嵌入式Linux设备树开发实战:从DTS编写到驱动匹配全链路
导读:设备树是嵌入式Linux驱动开发的基础设施——硬件信息写在DTS里,驱动通过compatible字符串找到对应设备。本文覆盖DTS编写、DTB编译、内核解析、驱动匹配全流程,附代码示例和踩坑方案。
一、原理简析
设备树(Device Tree)是一种描述硬件的数据结构,最早源于Open Firmware,2005年被引入PowerPC Linux,后来推广到ARM、MIPS、RISC-V等架构,现在已经是嵌入式Linux的标配。
没有设备树的时候,硬件信息(寄存器地址、中断号、GPIO编号)直接写死在驱动代码里,换一块板子就得改驱动、重新编译内核。设备树的做法很简单:把硬件描述从驱动代码里拿出来,放到独立的DTS文本文件中,内核启动时解析这个文件,根据内容加载对应的驱动。
内核拿到设备树后干三件事:平台识别(根节点的compatible匹配machine_desc)、运行时配置(/chosen节点传内核参数)、设备枚举(有compatible属性的节点注册为platform_device)。
整条链路:DTS编写 → DTC编译为DTB → Bootloader传递DTB → 内核解析为device_node → 转换为platform_device → compatible匹配驱动 → 调用probe函数。
二、实操步骤
2.1 编写DTS设备树源文件
设备树用树形结构描述硬件,每个节点靠compatible属性告诉内核"我是什么设备"。下面是一个LED设备的DTS示例:
/dts-v1/;
/ {
model = "My Embedded Board";
compatible = "myboard";
leds {
compatible = "gpio-leds";
led0: led-heartbeat {
label = "heartbeat";
gpios = <&gpio1 21 0>; /* GPIO1_21, 低电平有效 */
linux,default-trigger = "heartbeat";
default-state = "off";
};
led1: led-status {
label = "status";
gpios = <&gpio1 22 0>;
default-state = "on";
};
};
};
关键属性说明:
| 属性 | 含义 | 格式说明 |
|---|---|---|
compatible |
驱动匹配字符串 | 推荐格式 "manufacturer,model" |
reg |
寄存器基地址和长度 | <地址 长度>,父节点#address-cells和#size-cells决定cell数量 |
status |
设备状态 | "okay"启用,"disabled"禁用 |
gpios |
GPIO引用 | <&gpio控制器 引脚号 标志> |
2.2 编译DTS为DTB
# 使用dtc编译器
dtc -I dts -O dtb -o board.dtb board.dts
# 或使用内核构建系统(推荐)
make dtbs ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf-
# 反编译DTB用于调试
dtc -I dtb -O dts -o board.dts board.dtb
编译出来的DTB由Bootloader(U-Boot等)加载到内存,内核调用unflatten_device_tree()把扁平化的DTB解析成运行时的device_node树。
2.3 编写驱动匹配代码
驱动用of_match_table声明自己支持哪些设备:
static const struct of_device_id my_led_match[] = {
{ .compatible = "gpio-leds", },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, my_led_match);
static struct platform_driver my_led_driver = {
.probe = my_led_probe,
.remove = my_led_remove,
.driver = {
.name = "my_led_driver",
.of_match_table = my_led_match,
},
};
module_platform_driver(my_led_driver);
内核遍历设备树节点时,拿节点的compatible值和所有已注册驱动的of_match_table逐个比对,匹配上了就调驱动的probe函数,把对应的platform_device传进去。
三、设备树 vs 传统硬编码
| 维度 | 传统硬编码方式 | 设备树方式 |
|---|---|---|
| 硬件信息存放 | 散落在驱动C文件中 | 集中在DTS文本文件 |
| 换板适配 | 修改驱动源码,重新编译内核 | 修改DTS,重新编译DTB即可 |
| 同一驱动多板卡 | 需要#ifdef条件编译,代码膨胀 |
一份驱动代码,多份DTS覆盖 |
| 硬件变更影响 | 驱动代码 + 内核均需重编 | 仅需重编DTB |
| 可读性 | 寄存器地址散落各处,难以维护 | 树形结构,硬件拓扑一目了然 |
| 内核镜像通用性 | 每块板子一个内核镜像 | 同一SoC系列可共用内核镜像 |
四、核心流程图
五、常见问题解决
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 驱动probe不触发 | compatible字符串与驱动of_match_table不一致 |
检查DTS和驱动中的字符串是否完全一致(含大小写);用dmesg \| grep "probe"确认 |
| 设备不工作 | 节点status = "disabled" |
改为status = "okay",或删除该属性(默认视为okay) |
| GPIO申请失败 | GPIO编号计算错误,或pinctrl未配置 | 确认&gpio1 21在目标SoC上对应正确的bank和pin;检查pinctrl节点是否声明 |
| DTB未更新 | 烧录了旧的DTB文件 | 重新make dtbs后替换目标板/boot/下的DTB,重启验证 |
运行时调试命令:
# 查看内核识别的设备树
ls /proc/device-tree/
# 查看某节点的compatible值
cat /proc/device-tree/leds/compatible
# 查看设备树解析日志
dmesg | grep -i "of:"
六、总结
设备树干的事就一件:把硬件描述从驱动代码里拆出来。DTS写硬件拓扑,驱动靠compatible字符串匹配设备,中间靠内核的OF子系统衔接。搞清楚"DTS编写 → DTB编译 → 内核解析 → 驱动匹配"这条线,大部分嵌入式Linux驱动适配问题都能定位。