设备树在驱动中的使用:从内核崩溃到优雅解耦
上周调一块定制板SD卡死活识别不出来。老方法翻datasheet、查引脚复用、对照寄存器逐个配置折腾两天终于看到mmc0的识别日志了。结果一高兴手抖把另一个功能的引脚配置给冲掉了——系统直接挂死。这种“改一处崩一片”的体验搞过裸机驱动的人都懂。今天要聊的设备树就是来解决这种耦合噩梦的。为什么是设备树早年内核里充斥着大量的板级文件arch/arm/mach-xxx目录下全是板子的具体配置。同一个芯片换块板子就得重新编译内核这显然不符合Linux“一份内核走天下”的理想。设备树本质上是一种描述硬件拓扑和配置的数据结构独立于内核代码存在。启动时由bootloader传递给内核内核解析后动态生成设备节点。这样同一份内核镜像就能跑在不同硬件上驱动只关心设备树里描述的资源配置不再硬编码寄存器地址或引脚编号。驱动里怎么用设备树先看个实际例子。以前写一个LED驱动你可能这样定义硬件信息// 老方法直接硬编码#defineLED_GPIO_BANK0x20A0000#defineLED_PIN_OFFSET5现在设备树里这么描述leds { compatible my-led-driver; status okay; led1 { label sys_led; gpios gpio2 5 GPIO_ACTIVE_HIGH; // 引用GPIO控制器第2组第5脚 default-state off; }; };驱动代码里不再出现具体地址而是通过of_系列接口读取staticintled_probe(structplatform_device*pdev){structdevice_node*nodepdev-dev.of_node;structdevice_node*child;// 遍历所有子节点for_each_child_of_node(node,child){constchar*label;structgpio_desc*desc;// 读取label属性of_property_read_string(child,label,label);// 获取GPIO描述符内核推荐的新方法descdevm_gpiod_get_from_of_node(pdev-dev,child,gpios,0,GPIOD_OUT_LOW,label);if(IS_ERR(desc)){dev_warn(pdev-dev,Failed to get GPIO for %s\n,label);continue;// 别直接return可能还有其他LED}// 这里就能用desc操作GPIO了}return0;}注意那个devm_gpiod_get_from_of_node这是带资源管理的版本驱动卸载时自动释放GPIO避免内存泄漏。老式的of_get_gpio还在用但新代码不建议了。匹配机制compatible属性是灵魂内核怎么知道哪个驱动处理哪个设备节点靠的就是compatible属性。驱动里定义匹配表staticconststructof_device_idled_of_match[]{{.compatiblemy-led-driver},{/* sentinel */}};MODULE_DEVICE_TABLE(of,led_of_match);staticstructplatform_driverled_driver{.probeled_probe,.driver{.namemy_led,.of_match_tableled_of_match,// 关键在这里},};设备树里节点的compatible值一旦和驱动匹配上内核就会调用驱动的probe函数。多个兼容字符串可以这样写compatible vendor1,chip1, vendor2,chip2;内核会按顺序尝试匹配。资源读取的坑点读取寄存器地址要用of_iomap而不是of_get_address// 正确姿势void__iomem*base;structresourceres;if(of_address_to_resource(node,0,res)){dev_err(dev,Failed to get resource\n);return-ENXIO;}basedevm_ioremap_resource(dev,res);// 这个自带错误检查和资源管理// 错误示范新手常犯u32 addr;of_property_read_u32(node,reg,addr);// 直接读出来的不是物理地址baseioremap(addr,size);// 大概率会崩中断的获取也有讲究// 推荐方式intirqplatform_get_irq(pdev,0);// 第0个中断资源if(irq0)returnirq;// 负值就是错误码// 不推荐除非特殊情况irqof_irq_get(node,0);设备树覆盖与动态修改调试时经常需要临时修改设备树重编译整个dtb太慢。可以用动态覆盖overlay# 加载覆盖层fdtoverlay-ibase.dtb-onew.dtb overlay.dtbo# 或者内核启动后通过configfsmount-tconfigfs none /configcat/sys/kernel/config/device-tree/overlays/status# 查看状态不过生产环境慎用这功能对bootloader和内核版本都有要求。个人经验谈保持兼容性设备树节点改名字是大忌特别是稳定了的项目。一旦驱动发布了节点名、属性名就不要动新增属性用新名字。属性命名尽量用标准属性名比如gpios而不是my-gpio-pins。标准属性有现成的解析函数自己造轮子容易出问题。调试技巧cat /proc/device-tree能看到内核解析后的设备树结构echo 8 /proc/sys/kernel/printk打开调试日志后驱动里用of_node_full_name(pdev-dev.of_node)打印当前节点全路径定位问题快很多。别过度设计简单设备比如就一个GPIO直接写死在驱动里可能更省事。设备树不是银弹硬件固定不变的工控板用设备树反而增加复杂度。版本管理设备树文件要和内核版本绑定存放。我曾经掉进过一个坑用新内核的dtb给老内核用系统能启动但某些外设就是异常查了一周才发现是设备树语法兼容问题。最后说个真事有次调试I2C设备设备树里配置完全正确但probe函数就是不被调用。最后发现是I2C控制器的节点里status disabled。所以第一件事永远是确认父节点状态是不是okay——这种细节datasheet不会告诉你但能卡你两天。