ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

设备树与Linux驱动开发:从原理到RK3568实战

设备树与Linux驱动开发:从原理到RK3568实战 1. 设备树到底在解决什么问题很多刚接触嵌入式Linux的朋友第一次看到“设备树”Device Tree这三个字第一反应是这玩意儿跟我平时写的驱动有什么关系我直接在内核源码里改平台设备注册信息不就完了为什么要引入这么一层“中间商”先说结论设备树本质上是把“硬件长什么样”和“驱动怎么写”这两件事彻底剥离开。在早期ARM Linux内核里几乎每一块开发板的硬件配置信息都是直接写死在arch/arm/mach-xxx目录下的C代码里。什么意思呢就是每次出一块新板子都得往内核里塞一段平台设备注册代码把板载资源的地址、中断号、时钟、GPIO等信息用C语言结构体描述一遍然后调platform_device_register()注册进去。改动一次硬件就要改一次内核源码重新编译整个内核。早期ARM SoC厂商少板子种类有限这套做法虽然丑但勉强能转。等到SoC种类爆发式增长一个内核要同时支持几百种板卡时这种硬编码方式彻底成了灾难。设备树的思路很朴素用一套独立的、与内核代码完全无关的纯文本描述文件把硬件信息写清楚。内核在启动时解析这份文件动态生成平台设备列表驱动再用统一的接口去读取硬件资源。这样一来换板子就只需要换一个.dts文件内核代码一行都不用动。这个解耦思路跟PC世界里ACPI表的作用是一样的。所以网上常有人说“设备树就是ARM版的ACPI”我觉得这个类比非常准确。从驱动开发者的视角看设备树引入后最大的变化是你写驱动时不再关心具体板子上哪个寄存器对应哪个外设而是专注处理“当遇到compatible属性匹配到的设备时如何初始化它”。硬件细节全部由设备树描述驱动只需要知道如何消费这些描述。理解了这个本质后面学起来就会顺畅很多。2. 设备树语法与关键属性2.1 从一颗LED灯开始理解基本语法别一上来就去看全量语法手册那东西能把人劝退。我建议从最简单的例子切入——点一颗LED灯。这颗LED用GPIO控制对应设备树节点长这样/ { leds { compatible gpio-leds; status okay; power_led { label power; gpios gpio4 0 GPIO_ACTIVE_HIGH; default-state on; }; }; };这个节点信息量不小拆开慢慢看。根节点“/”下面挂了一个“leds”子节点compatible属性告诉内核这个节点由gpio-leds驱动接管。子节点power_led则具体描述了一颗LED灯label是它的名字gpios指定它接在gpio4控制器第0号引脚上高电平有效。default-state是初始化状态。仔细观察设备树其实是一种“树形键值对集合”。每个节点是一个硬件对象节点名通常是设备类型同级节点通过unit address比如gpioff7b0000来区分。每个属性和值则描述这个硬件对象的具体细节。比如reg属性是一组地址信息通常配合#address-cells和#size-cells告诉内核“地址占几个32位字长度占几个32位字”。2.2 几个新手最容易写错的属性第一个是reg和ranges。reg描述设备自身的寄存器空间地址和长度ranges描述父总线地址到子总线地址的翻译关系。很多新手会混淆这两个reg告诉内核“我需要的寄存器在哪”ranges告诉内核“从CPU视角看子节点地址和父节点地址怎么换算”。尤其涉及PCIe、外部存储器控制器这类桥接设备时ranges一旦写错子设备就全部识别不了。第二个是interrupts和interrupt-parent。热词里提到了DPU驱动、GPU驱动开发绝大多数外设驱动都会用到中断。设备树里描述中断要分两步先用interrupt-parent声明“我用哪个中断控制器”再用interrupts声明具体的中断号和触发方式。经常有新手只写了interrupts却漏了interrupt-parent结果驱动注册时拿不到有效中断号中断申请失败的报错查半天查不出来。第三个是clocks。瑞芯微RK3568这样的大SoC几乎每个外设都要做时钟门控。设备树里一般用clocks属性和assigned-clock-rates来指定外设工作时钟及频率。比如UART想要跑1.5M波特率光改驱动里波特率参数是不够的必须确保设备树里uart时钟源频率配置正确否则算出来的分频系数是错的串口输出全是乱码。第四个是status属性。经常出现的情况是硬件明明板载了某个控制器但它默认在设备树里被写成了status disabled。比如某些SoC同一个I2C控制器可能复用多个引脚组板厂根据实际接线决定用哪组没用到的就disabled掉。遇到“内核说设备不存在”的时候先检查status是不是okay。这个排查思路能解决一半的外设不工作问题。2.3 dtsi与dts的关系看瑞芯微RK3568的SDK时你会发现arch/arm64/boot/dts/rockchip/目录下有一堆文件rk3568.dtsi、rk3568-evb.dts、rk3568-evb1-ddr4-v10.dts…… 很多人刚开始直接懵了到底哪个是正在用的这里涉及一个关键概念dtsi是SoC公共描述文件描述芯片整体资源CPU核心、中断控制器、各IP控制器寄存器基址、时钟树、内部总线结构等。dts是具体板卡描述文件描述这块板子上实际接了哪些外设、使用了SoC的哪些引脚功能。dts通过#include引用dtsi叠加自身板级配置。改变板载外设就改dts换SoC才动dtsi。RK3568这种平台一颗SoC会被做成各种不同规格的开发板和商业板卡所以你会看到rk3568-evb、rk3568-evb1-ddr4-v10、rk3568-rock-3a等下划线后缀各异的dts。它们都是同一个思路公共芯片信息放在rk3568.dtsi板级差异放在各自的dts里。编译时内核的build系统会扫描这些dts生成对应的dtb文件。到底该选哪个要看你手上板子是哪个型号、DDR是DDR3还是DDR4、屏幕接口是哪路LVDS还是MIPI DSI。板厂提供的内核源码里一般有一个默认defconfig和对应的dts文件照着板子型号匹配即可。注意修改dts文件以后必须重新编译生成dtb。很多新手改了dts却发现不生效最后发现是编译产物没更新或者烧录时烧错了dtb分区。这个很低级但确实非常常见。3. 驱动怎么和设备树对上号3.1 compatible匹配的完整流程设备树节点写得再漂亮驱动不认也是白搭。驱动与设备树的“接头暗号”就是compatible属性。以GPIO LED驱动为例内核源码里drivers/leds/leds-gpio.c的驱动匹配表长这样static const struct of_device_id gpio_leds_of_match[] { { .compatible gpio-leds, }, {} }; MODULE_DEVICE_TABLE(of, gpio_leds_of_match); static struct platform_driver gpio_led_driver { .probe gpio_led_probe, .remove gpio_led_remove, .driver { .name leds-gpio, .of_match_table gpio_leds_of_match, }, }; module_platform_driver(gpio_led_driver);内核启动时设备树解析出的每个platform_device都会被拿来跟驱动的of_match_table做匹配。只要compatible字符串完全一致驱动就绑定了这个设备probe函数会被调用。匹配不上的话设备就处于“有设备没驱动”的状态在/dev下自然看不到对应节点。这里要提示一个关键点compatible字符串必须做到板级DTS和驱动源码严格一致一个字符都不能差。很多时候驱动不probe问题就出在DTS里写的是gpio-led驱动里匹配的是gpio-leds差个s就查半天。3.2 在驱动中读取设备树资源的常用API匹配只是第一步probe函数里要真正拿到硬件资源还得借助OF API。这里列几个最常用的每一个都有明确的使用场景。读取reg属性对应的资源用platform_get_resource或更上层的devm_platform_ioremap_resource。这两个函数会从platform_device里抽取内存区域直接返回映射后的虚拟地址。过去我们会直接用of_iomap(node, index)不过在新内核里更推荐platform系列接口它会自动处理资源生命周期管理。读取GPIO信息用devm_gpiod_get或of_get_named_gpio。前者是新的GPIO descriptor接口后者是老式的整型GPIO编号接口。同样是拿GPIOdescriptor接口更安全因为它替你关注了GPIO是否有效、方向是否正确、active-low是否翻转等问题。DTS里写的GPIO_ACTIVE_LOW会被descriptor接口自动消化掉而老接口需要自己手动判断有效电平。读取中断信息直接用platform_get_irq或of_irq_get。拿到IRQ号之后再用request_irq或devm_request_threaded_irq注册中断处理函数。注册的时机要小心必须在设备正常工作前把中断注册好否则硬件信号到了内核找不到处理函数轻则丢中断重则触发“irq X nobody cared”错误。读取clocks信息用devm_clk_get配合clk_prepare_enable。设备树里指定了clocks属性后驱动侧按clock名字获取时钟句柄使能时钟外设才能正常工作。RK3568的PCIe控制器、GMAC网卡这类高速外设时钟配置尤其重要漏使能一个core clock整个控制器直接挂掉。读取自定义属性用of_property_read_u32、of_property_read_string等系列函数。DTS里经常会有max-speed 1000、phy-mode rgmii这类自定义扩展驱动侧用这些接口读取。读取之前最好用of_property_read_bool判断属性是否存在防止读一个不存在的属性返回负值还没察觉。3.3 一个完整的平台驱动骨架把上面这些串起来一个规范的平台驱动骨架长这样#include linux/module.h #include linux/platform_device.h #include linux/of.h #include linux/gpio/consumer.h #include linux/interrupt.h struct my_device_priv { void __iomem *base; struct gpio_desc *enable_gpio; int irq; }; static irqreturn_t my_device_isr(int irq, void *data) { struct my_device_priv *priv data; u32 val ioread32(priv-base 0x10); /* 处理中断事件 */ return IRQ_HANDLED; } static int my_device_probe(struct platform_device *pdev) { struct resource *res; struct device *dev pdev-dev; struct my_device_priv *priv; int ret; priv devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL); if (!priv) return -ENOMEM; platform_set_drvdata(pdev, priv); res platform_get_resource(pdev, IORESOURCE_MEM, 0); priv-base devm_ioremap_resource(dev, res); if (IS_ERR(priv-base)) return PTR_ERR(priv-base); priv-enable_gpio devm_gpiod_get(dev, enable, GPIOD_OUT_LOW); if (IS_ERR(priv-enable_gpio)) return PTR_ERR(priv-enable_gpio); priv-irq platform_get_irq(pdev, 0); if (priv-irq 0) return priv-irq; ret devm_request_irq(dev, priv-irq, my_device_isr, 0, dev_name(dev), priv); if (ret) return ret; gpiod_set_value(priv-enable_gpio, 1); dev_info(dev, init done\n); return 0; } static int my_device_remove(struct platform_device *pdev) { struct my_device_priv *priv platform_get_drvdata(pdev); gpiod_set_value(priv-enable_gpio, 0); return 0; } static const struct of_device_id my_device_of_match[] { { .compatible myvendor,my-device, }, {} }; MODULE_DEVICE_TABLE(of, my_device_of_match); static struct platform_driver my_device_driver { .probe my_device_probe, .remove my_device_remove, .driver { .name my_device, .of_match_table my_device_of_match, }, }; module_platform_driver(my_device_driver); MODULE_LICENSE(GPL); MODULE_AUTHOR(Your Name); MODULE_DESCRIPTION(My Device Driver);这个骨架里已经涵盖了资源映射、GPIO获取、中断申请、设备私有数据保存等核心逻辑。实际项目里框架基本都是一样的变化的只是硬件操作细节和业务逻辑。把这段代码弄熟再套用到具体外设上思路一通全通。4. RK3568实战修改设备树并让驱动跑起来4.1 为什么RK3568有那么多设备树到底怎么选这个问题在热词里反复出现特别是在OpenHarmony、Ubuntu这类第三方系统适配中更加突出。RK3568的SDK里设备树文件非常多因为同一颗SoC会被多个评估板、参考板、量产板使用每个板子的DDR颗粒、显示屏、网口个数、PCIe通道分配可能都不一样。选设备树的原则只有一条找到与你的硬件设计最接近的那个dts然后在它基础上微调。如果是买来的开发板板厂一般会在SDK的dts文件名里体现板子型号直接匹配即可。如果自己画了底板那就基于官方的evb dts改去掉没有的器件节点加上新增的器件节点修改GPIO引脚复用配置。判断设备树是否生效的方法也很简单内核启动日志里搜索“Kernel command line”确认bootargs里指定的dtb路径。再看“Machine model”或者产品型号打印确认实际加载的是哪块板级的compatible字符串。如果这些信息都对应不上说明烧录的dtb不对或者u-boot环境变量CONFIG_DEFAULT_FDT_FILE设错了。4.2 修改Ubuntu下RK3568设备树并重新编译真实场景里很多朋友是在Ubuntu主机上做RK3568支持的开发。流程分三步修改dts、编译dtb、烧录确认。第一步找到需要修改的dts。在SDK内核目录下执行find arch/arm64/boot/dts/rockchip/ -name rk3568*.dts按板卡型号筛选出目标文件。比如你的板子是RK3568 EVB1 DDR4版本那就是rk3568-evb1-ddr4-v10.dts。找到后用文本编辑器打开定位到需要修改的节点。第二步编译。最简单的做法是使用SDK自带的构建脚本但仅想验证dts修改效果时可以直接在SDK顶层目录执行export ARCHarm64 export CROSS_COMPILEaarch64-linux-gnu- make rockchip_linux_defconfig make dtbs编译完成后在arch/arm64/boot/dts/rockchip/目录下会生成同名.dtb文件。把这个dtb文件替换到根文件系统/boot分区或者独立dtb分区即可具体取决于你的Android/Ubuntu系统分区布局。用RK开发工具烧录时通常会把kernel.img和resource.img分开处理dtb可能打包在resource.img或者boot.img里。每个SDK版本略有差异烧录前先确认打包脚本。第三步验证。这一步很多人会跳过但强烈建议别省。有两种验证姿势一种是启动后用root权限查看/sys/firmware/devicetree/base目录这是设备树的运行时视图按节点路径逐层展示属性。你在dts里改的内容只要编译烧录正确一定会体现在这里。另一种是看/proc/device-tree这是/sys/firmware/devicetree/base的符号链接本质相同。比如你改了LED默认状态直接cat /sys/firmware/devicetree/base/leds/power_led/default-state比对值是否与预期一致。提示修改dts后务必同步更新内核的DTC版本。RK3568的平台一般不需要旧版本DTC支持复杂宏展开但如果从老SDK升级建议用SDK内置的dtc工具编译避免因为DTC版本差异导致编译告警或二进制约束不一致投影到实际设备上出现“属性读不到”这种怪异问题。4.3 一个硬件修改的完整推演假设我的板子上新增了一个I2C温度传感器地址是0x48挂载在I2C2总线。设备树的修改逻辑是这样的首先确认SoC的I2C2控制器节点位置。RK3568的i2c2节点在rk3568.dtsi里已经定义好了默认状态可能是disabled因为同一组I2C引脚可能被复用成GPIO。所以第一步是把它的status改成okay然后在它下面新建一个子节点i2c2 { status okay; clock-frequency 400000; lm75: temperature-sensor48 { compatible national,lm75; reg 0x48; }; };reg 0x48表示设备的I2C从机地址。驱动侧通过i2c_driver匹配compatible后在probe里用i2c_smbus_read_word_data之类的接口读取温度寄存器即可。整个过程驱动代码完全不需要知道这个传感器挂在哪个I2C总线上也不需要管引脚复用怎么配置这些都由设备树消化掉了。这就是设备树真正的价值。4.4 串口芯片驱动为什么老出问题热词里CP2102、CH340、FT232R这几个串口芯片驱动出现的频率非常高。先说结论这几颗USB转串口芯片在Linux主线内核里都自带驱动正常情况下插上就能用不需要额外安装驱动。CP2102对应cp210x.koCH340对应ch341.koFT232R对应ftdi_sio.ko。你看到“驱动装不上”的帖子大部分是两类问题。第一类是系统压根没识别到USB设备。插上芯片后执行dmesg看内核日志如果只有USB enumerate但不产生ttyUSB节点可能是usbserial驱动没自动绑定。此时手动执行modprobe cp210x再看一次。如果还没动静检查USB线是不是纯充电线很多廉价USB线只有电源线没有数据线这个问题经常被忽视。第二类是识别到了但打开串口失败。这里重点检查操作权限dailout、dialout、uucp等用户组权限决定了非root用户能否访问串口设备。执行ls -l /dev/ttyUSB0如果权限是crw-rw---- root dialout那你的账号需要加入dialout组然后重新登录shell。这个步骤论坛里翻来覆去讲但是几乎每天都有新朋友问。5. 设备树与驱动开发中的常见坑5.1 编译报错“unit address mismatch”怎么办DTC编译器对设备树的检查非常严格。比如你把节点名写成i2cff7b0000但reg属性写的地址是0xff7a0000DTC会直接报“node has a unit name, but no reg property”或者“unit address mismatch”。这类错误绝大多数是手误检查reg值和节点名的地址部分是否一致即可。还有一种情况是节点名合法但重复定义了相同unit address。比如两个节点都叫gpioff7b0000DTC也会报错。这种重复往往来自dtsi文件层层include之后的定义冲突排查方法是编译时加上DTC_FLAGS-或者打开编译日志详细输出定位到出错的具体文件与行号。5.2 驱动probe不执行先别急着怀疑驱动代码驱动probe不执行是设备树开发里最高频的问题。我的排查顺序一般是先看/sys/bus/platform/devices下有没有对应的设备节点。如果没有说明设备树解析阶段就没生成设备问题在DTS。如果有设备但没有驱动绑定检查compatible是否完全匹配。如果设备和驱动都有但是probe报错看dmesg里probe失败的具体返回值-EPROBE_DEFER要特别注意它表示当前依赖的资源还没准备好内核会自动重试通常是因为某个clock、regulator或pin controller还没注册完成。有一个容易被忽略的点platform_driver的id_table。有些驱动同时支持OF匹配和非OF匹配代码里写了两套匹配表。如果of_match_table里的compatible和DTS对不上但id_table里另一个兼容名对得上驱动也会绑定但probe函数拿到的资源格式可能完全不同。对于新代码建议只用OF匹配逻辑简单可控。5.3 引脚复用配置不当导致的功能失灵瑞芯微平台最让人头疼的就是IOMUX引脚复用。RK3568的每个引脚往往有多个功能比如GPIO3_C2这个引脚既可以是普通GPIO也可以是I2C3_SCL还可以是UART1_TX。默认状态在芯片数据手册里有定义但在Linux下具体启用哪个功能由设备树pinctrl子系统统一控制。最常见的坑是同一个引脚在多个节点里都被引用。比如A节点用了GPIO3_C2做LED控制B节点又声明这个引脚是I2C3的SCLpinctrl子系统在初始化时可能会报冲突警告导致其中一个功能失效。排查方法是打开内核的pinctrl调试开关在debugfs的pinctrl目录下查看每个pin的当前mux状态与是否有claim冲突。5.4 热词里的WSL和虚拟机热词里有一条“适用于 Linux 的 Windows 子系统必须更新到最新版本才能继续”这就是WSL的典型报错。很多人想用WSL做嵌入式Linux开发省去装虚拟机的麻烦但WSL本质上是一个受限的Linux子系统对USB设备直通、串口访问、GPIO调试等支持并不完善。做纯软件编译、脚本验证、内核模块交叉编译可以但要做设备树调试、驱动烧录验证还是建议用真实Linux物理机或者专门的虚拟机USB透传方案。如果你的开发调试跑在WSL里遇到USB串口映射不到/ttyUSB0的现象先不要怀疑驱动代码大概率是WSL没有把Windows的COM端口转发到Linux子系统。新版WSL支持usbipd-win方案可以把Windows宿主机上的USB设备共享给WSL2使用但配置复杂稳定性也一般。生产环境别折腾这个久经考验的方案还是实体机装Linux。5.5 设备树里改了参数但不生效的几个原因这个场景在Ubuntu改RK3568设备树时最典型。我整理一个对照表方便你自查现象常见原因检查手段修改的属性完全没变dtb没重新编译或者没烧录对比/sys/firmware/devicetree/base下的实时值修改了dtsi但没生效dts里用status disabled覆盖了dtsi配置搜索dts里是否包含同节点的status属性修改了引脚功能但外设仍异常引脚复用冲突另一个节点抢占了同一引脚cat /sys/kernel/debug/gpio 查看引脚占用新加的节点在/sys下看不到设备树编译没问题但compatible没有驱动匹配看dmesg里是否有“no driver”提示修改clock频率后外设不工作时钟树配置无误但驱动内部使用了旧频率检查驱动是读设备树属性还是使用硬编码5.6 内核文档里找不到答案时怎么自救设备树和驱动的调试很多时候不是网上没有答案而是你知道的不够多。碰到磕磕绊绊的问题首要动作是什么打开内核源码。设备树绑定文档在Documentation/devicetree/bindings目录下每个子系统都有独立的yaml格式说明。驱动源码里的of_match_table和probe函数写得很清楚比任何论坛帖子都可靠。再看一下drivers/gpio、drivers/pinctrl、drivers/clk这些基础子系统在对应平台下的实现很多疑难杂症都能从底层实现里找到线索。另一个被很多人忽略的利器是trace。内核的tracepoint机制里有一个of事件组可以跟踪设备树解析过程。打开tracefs后执行以下命令mount -t tracefs nodev /sys/kernel/tracing echo 1 /sys/kernel/tracing/events/of/enable cat /sys/kernel/tracing/trace这样可以直观看到每个节点的解析耗时和错误信息对于定位设备树大文件的性能问题以及节点解析中断问题非常有帮助。6. 从驱动开发角度看整个生态写到这里我想聊聊对设备树和驱动开发这件事的整体感受。很多人一上来就背语法、抄驱动模板结果换个平台就抓瞎。设备树本质上是描述硬件的一种“数据契约”驱动是消费这份契约的“服务方”。“数据契约”设计得好不好直接决定了驱动好不好写。所以每次拿到一块新板子我习惯先花半小时仔细读它的dts文件梳理清楚整块板卡的硬件拓扑CPU通过哪些总线连了哪些外设哪些控制器被使能、哪些被禁用GPIO和中断脚位分配是否合理时钟频率是否匹配外设需求。这个过程就像装修前先看户型图没有这一步后面做设计全是空中楼阁。再聊聊生态。设备树在ARM Linux里已经是绝对主流而且随着RISC-V的兴起设备树这套机制也被完整继承了下来。x86平台虽然传统上不用设备树但在一些特定嵌入式场景也开始尝试。对整个行业来说设备树不是某个芯片厂商的私有协议而是所有跑Linux的嵌入式平台的通用交流语言。学会设备树与驱动开发本质上学会了跨平台的硬件抽象思维方式这门手艺长期有效。从热词里还能看出很多人关心设备树到底怎么选、怎么改、怎么和系统适配这说明大部分开发者的痛点并不在驱动代码本身而在于对整个启动流程、编译流程、烧录流程的链路理解。这个链路就是dts - dtb - bootloader加载 - 内核解析 - platform_device生成 - driver匹配绑定 - probe初始化。任何一环断了你看到的现象都是“设备不工作”但根因可能千差万别。把这条链路彻底吃透比背一千行驱动代码都有用。我给新人的建议很简单不要急着追新框架、新工具先把一块最简单的外设比如GPIO按键或者LED灯从dts修改到驱动编写完整走通一遍。这一遍里你会踩到pin control、platform device、OF API、中断注册、设备生命周期管理这些几乎所有后续开发都会用到的核心概念。把这个闭环打通后面学USB、PCIe、MIPI DSI等复杂驱动都是同样的套路沿着这个思路套上去就能上手。
返回列表