尧图精选

设备树、device、driver与platform总线:Linux驱动模型从匹配到probe全解析

🕒 发布时间:2026/10/1 8:54:35 📁 来源:尧图网络
搞过Linux驱动的人几乎都被这么一套概念怼过脸驱动程序、设备树、platform、device、driver。刚接触的时候我也挺懵——设备树是一份描述硬件的文件driver是操作硬件的代码这两个还能理解那device又是什么platform又是个啥为什么底层外设都要往platform上挂我记得第一次看drivers/base/platform.c的时候脑子里全是“这跟ARM平台有什么关系”的疑问后来才明白这个platform其实跟“瑞芯微平台”“全志平台”没有任何关系它只是Linux设备模型里一条虚拟总线的名字。这篇文章我想把“驱动程序、设备树、platform、device、driver”这五者之间的关系一次讲透顺带把设备树从文本到内核对象的完整链路、compatible匹配的规则、probe触发时机这些高频迷惑点全部拆开。适合正在做嵌入式Linux驱动开发、刚从裸机单片机转过来、或者被设备树折腾到头秃的工程师。读完你应该能自己写一个最简单的platform设备驱动并且能想明白为什么我改了设备树驱动有时候生效、有时候死活不跑。1. 为什么会有设备树把“硬件长什么样”从代码里拆出去1.1 没有设备树的年代驱动是怎么写的要理解设备树先得回到老一代的内核开发方式。在设备树大规模普及之前ARM平台的内核里有一大堆arch/arm/mach-xxx目录每个板子都有对应的board文件。比如你想在某个开发板上加一个外部UART扩展芯片你往往得在板级文件里手动写一个platform_device结构体把寄存器基地址、中断号、时钟信息全部写死在C代码里然后再注册一个对应的platform_driver去操作它。硬件稍有变动比如换了一个GPIO引脚、改了一个中断号就得改C代码重新编译整个内核维护成本高得离谱。当年不同厂家的板级文件风格还不统一有的把资源写成一个数组有的把复位引脚和中断号混在一起代码仓库里全是各种板卡的“私货”。更可怕的是一个内核镜像想要同时支持多块板子只能靠编译时选择MACH_START宏板子一多arch/arm目录就成了一个巨大的杂物间。1.2 设备树到底做了什么从DTS到DTB再到device_node设备树的核心思路就是把“硬件长什么样”从C代码里剥离出来用一份文本文件来描述。这份文本文件叫DTSDevice Tree Source经过dtc编译器处理之后生成二进制DTBDevice Tree Blob。Bootloader在启动内核之前把DTB加载到内存并把它所在的内存地址传给内核内核在启动早期会对这段二进制数据进行解析构建出一棵结构化的设备节点树每个节点对应一个struct device_node。这棵树可以类比成一个楼盘的登记册。CPU就是楼里的住户每个外部设备是什么型号、门牌号寄存器地址、用哪个中断号报修热线、挂在哪条总线下面全在这本册子里写得清清楚楚。以前这些信息是刻在“物业办公室的脑子”里的也就是写死在驱动代码里现在改成了一本独立的手册换硬件的时候只改手册不用把整栋楼推倒重建。设备树相关的内容我后来主要是在RK3568这类新一代SoC上实践的。这种多核ARMLinux平台的板级配置几乎全部靠arch/arm64/boot/dts/rockchip/rk3568.dtsi加板级DTS文件组合描述。修改屏幕方向、调整GPIO复用、新增外设节点全是改设备树的路子这也让“设备树文件”“rk3568 触摸竖屏改为横屏设备树修改”这类检索词成为嵌入式工程师的日常。1.3 设备树节点怎么变成驱动看到的“设备”这里必须先澄清一个特别常见的误区设备树节点本身不是device。内核解析DTB之后得到的是device_node它只是设备树在内存中的表达形式。后面设备模型会将合适的device_node进一步包装成platform_device这才真正挂到驱动模型的总线上成为驱动可以匹配和操作的对象。所以设备树的工作其实有两层第一层是启动早期的解析把DTB变成device_node树第二层是后续的遍历把符合条件的节点转换成platform_device并注册到platform总线上。从文本文件到内核对象这条路缺一步后边的driver-probe就永远不会触发。下边两章我分别把device/driver/platform的关系和设备树到device的转换链路讲清楚这俩合起来才是整个体系的全貌。2. device、driver是一对“合同双方”platform是撮合平台2.1 内核设备模型本身就是个“婚介所”Linux设备模型说白了就三个核心角色device、device_driver、bus_type。device代表一个具体的硬件设备对象它保存了硬件的资源信息、电源状态、与总线的连接关系device_driver代表一段操作硬件的逻辑它知道怎么初始化、怎么读写、怎么释放设备bus_type则是两者之间的匹配规则和维护者。这个概念可以类比成房屋租赁。房子硬件是客观存在的但房子本身不会说话租客驱动有居住需求但租客也不知道哪套房子在出租。中介总线掌握了一批房源信息同时登记了一批租客需求它负责把“合适的房子”和“合适的租客”匹配起来。匹配成功之后签合同对这个合同就是驱动的probe函数合同内容则是约定谁来管理房子的水电气。需要强调的是这套“婚介”模型不是platform独有的。USB、PCI、I2C、SPI凡是Linux里能枚举设备的总线底层都在用同一套设备模型。你熟悉了platform的匹配逻辑后面看i2c_driver、spi_driver只是换汤不换药。这里我放一个最精简的对照表格方便你建立第一印象角色内核结构体通俗理解核心职责devicestruct device硬件房子存放资源信息、连接关系、状态driverstruct device_driver租房合同与管家定义probe/remove等操作逻辑busstruct bus_type婚介所定义匹配规则、维护设备与驱动列表platform虚拟bus实例专门处理无物理总线发现能力的设备撮合板级外设与对应驱动2.2 platform不是“某芯片平台”而是一条虚拟总线很多人第一次看到platform_driver里的“platform”都会误以为它指代硬件平台比如“瑞芯微平台”“高通平台”。实际上它完全不是这个意思。芯片内部集成的很多外设控制器UART、I2C、SPI控制器、GPIO控制器等以及主板上直接焊接的板级外设它们不像PCIe设备那样可以通过配置空间自动枚举也不像USB设备那样有设备描述符。它们就是固定地挂在某段总线地址上中断号也是芯片手册设计死的。没有一套硬件机制能主动告诉内核“我在这里我是谁”。怎么办那就人为规定一条“虚拟总线”把这些设备统统挂上去然后由总线上的匹配机制决定哪个驱动来管它。这条虚拟总线就是platform_bus挂在它上边的设备叫platform_device对应的驱动叫platform_driver。设备树里大部分SoC内部控制器节点最终就是被转换成platform_device挂在platform总线上的。platform这个名字实际上是从“platform device”这个概念沿用下来的你可以理解为“放在板子上的设备”。它的bus_type在驱动模型层面和PCI、USB的bus_type是平级的不要因为名字里有个“platform”就觉得它很高层。在内核里它就是一根普普通通的总线只不过它的设备来源比较特别——可以是设备树节点转换而来也可以靠代码直接创建。2.3 一张表理清五者关系设备树、驱动程序、platform、device、driver这几个词单独看都好懂混在一起就容易打架。我按自己的理解画成一张静态关系说明你对着看会清晰很多概念实质在流程中的位置设备树硬件的静态描述文本解析后的node树起点提供“有谁”的信息device内核对象硬件实体在驱动模型中的具象匹配的一方driver内核对象操作硬件的代码逻辑匹配的另一方platform虚拟总线匹配的撮合者与容器驱动程序人对driver代码的总称最终要交付的成果设备树负责告诉内核硬件长什么样内核再根据设备树创建devicedevice和driver在platform总线上完成匹配匹配成功后driver的probe被调用这就是大家嘴里的“驱动程序工作了”。链条就是这么一环扣一环。3. 从设备树到 platform_device再到 driver完整链路拆解3.1 设备树节点里最重要的字段理解了宏观关系之后就得抠细节了。设备树里每个设备节点都有一些固定字段其中最关键的就是compatible。compatible是一个字符串列表格式通常是“厂商名,器件型号”比如rockchip,rk3568-uart0。它就是设备给驱动递上的名片驱动靠它来认人。常用的还有reg描述寄存器地址段interrupts描述中断号和触发方式clocks描述时钟来源status描述设备是否启用pinctrl-0描述引脚复用状态。这些字段在解析之后会被转换成驱动的资源reg变成struct resource的IORESOURCE_MEM类型资源interrupts变成IORESOURCE_IRQ类型资源。驱动在probe里用platform_get_resource()拿到的就是这些由设备树节点转过来的“家底”。3.2 启动阶段从DTB到device_node内核拿到DTB之后会在启动早期调用unflatten_device_tree()把二进制DTB解析成一棵device_node树。这一步只做了静态解析树上每个节点还都只是“登记册里的一行”没有和驱动模型发生任何关系。接下来of_platform_default_populate()会在根节点上开始遍历遇到有compatible属性的节点就对它调用of_platform_device_create()创建对应的platform_device再注册到platform总线上。如果遇到兼容性为simple-bus的中间节点还会继续往里面递归把子节点也逐层转换成platform_device。需要特别提醒的是并不是设备树里所有节点都会变成platform_device。比如挂在I2C控制器下面的温度传感器子节点它就不会变成platform_device而是由I2C控制器驱动在probe时通过of_i2c_register_devices()把它创建成i2c_client。SPI控制器下面的设备同理。platform_device主要对应的是SoC内部控制器和板级直连设备这个边界要分清楚否则你会遇到“为什么我设备树写了节点platform总线上就是看不到”的困惑。3.3 匹配规则compatible优先然后才是id_table和name设备树节点变成platform_device之后接下来就看总线怎么撮合了。总线上的匹配函数是platform_match()它的顺序大致是先走of_driver_match_device()如果driver带有of_match_table就用compatible与设备节点比对如果没匹配上再看driver的id_table与platform_device的name字符串比对最后再直接比较platform_device-name和driver-name。在实际开发中90%以上的场景走的是第一条也就是compatible匹配。所以你的设备树节点里写了什么compatible驱动里的of_device_id必须完全一致一个字符都不能差。驱动的骨架大概是这样的#include linux/module.h #include linux/platform_device.h #include linux/of.h static const struct of_device_id demo_of_match[] { { .compatible myvendor,my-demo-device }, { } }; MODULE_DEVICE_TABLE(of, demo_of_match); static int demo_probe(struct platform_device *pdev) { struct resource *res; res platform_get_resource(pdev, IORESOURCE_MEM, 0); dev_info(pdev-dev, probe ok, reg%pR\n, res); return 0; } static void demo_remove(struct platform_device *pdev) { dev_info(pdev-dev, remove\n); } static struct platform_driver demo_driver { .probe demo_probe, .remove demo_remove, .driver { .name demo_device, .of_match_table demo_of_match, }, }; module_platform_driver(demo_driver); MODULE_LICENSE(GPL);module_platform_driver()宏会帮你把platform_driver_register()和platform_driver_unregister()包好一个模块加载时会自动注册卸载时自动注销非常省事。3.4 probe的触发时机先后顺序其实无所谓还有一个高频疑问如果platform_device先注册platform_driver后注册或者反过来会不会导致匹配丢失答案是不会。总线在device注册时会主动走一遍匹配流程去驱动链表里找有没有合适的driver同样的在driver注册时也会遍历总线上的设备看有没有还没被认领的device。所以谁先谁后都能最终配对上这就是设备模型最优雅的地方。真正会卡住probe的往往是设备依赖的资源还没准备好。比如你的驱动需要某个时钟而时钟提供者还没注册这时probe会返回-EPROBE_DEFER内核会把这个设备放到延迟队列里等资源就绪后再次尝试probe。你在dmesg里看到类似“platform xxx: probe deferral”或者“xxx: deferred probe pending”的字样就是这种情况不需要慌通常过一会儿它会自己重新触发。要是你用的是built-in驱动而不是模块内核启动早期的排序问题也会导致defer但机制是一样的最终会被重试。4. 实操现场以RK3568设备树修改为例跑完整流程4.1 定位设备树源文件和目标节点理论讲完必须来点能直接抄作业的实操。以我熟悉的RK3568平台为例设备树源文件在arch/arm64/boot/dts/rockchip/目录下通常一个SoC对应一个.dtsi一块板子对应一个.dts。比如rk3568.dtsi是SoC级别的公共描述rk3568-evb.dts这类文件是具体板卡对SoC级描述的叠加和覆盖。拿到一块新板子第一步先搞清楚当前系统实际加载的是哪个DTB。很多新手直接改rk3568.dtsi改完发现没生效大概率是因为当前内核用的DTB是从另一个dts编译出来的或者板级dts覆盖了该节点的属性。稳妥的做法是先在板级dts里搜索目标外设的关键词比如搜uart0、touch确认你改的节点是最终生成的DTB里真正生效的节点。以“触摸竖屏改为横屏”这个场景为例触摸节点的方向控制会因触摸IC驱动而异常见的有rotation、touchscreen-inverted-x、touchscreen-inverted-y这类属性。你需要做的是找到触摸IC所在的I2C子节点在板级dts里对它进行覆盖比如i2c4 { touchscreen38 { status okay; touchscreen-rotation 90; }; };具体的属性名依赖于驱动源码不要盲抄别人的写法。正确姿势是在内核源码里搜touchscreen-rotation或者rotation看看这个驱动支持哪些可选属性然后再改。4.2 手工加一个自定义设备节点看全链路生效过程为了把“设备树节点-platform_device-driver-probe”这条链路讲到看得见摸得着我们可以手动加一个demo节点再写一个对应的驱动完整走一遍。假设你的板卡DTB最终由rk3568-evb.dts编译而来你可以在它的根节点下加一个demo设备/ { demo_device { compatible myvendor,my-demo-device; reg 0x0 0xfe010000 0x0 0x1000; status okay; }; };这里reg填了一个地址段我故意没有写实际对应的外设名称。如果你只是学习匹配流程完全可以只保留compatible去掉reg但那样platform_get_resource()就会返回NULL代码里注意判空就行。然后编译DTB通常一条命令就够make ARCHarm64 dtbs烧录前先确认板子的DTB分区挂载情况很多板子把DTB放在单独的boot分区也有不少RK方案会把DTB打包进boot.img。你只改设备树的话很多情况下可以单独更新DTB分区而不动内核镜像速度很快。烧完之后启动系统用ls /proc/device-tree/查看当前生效的设备树节点确认demo_device确实在内存解析后的设备树里存在ls /proc/device-tree/demo_device/ cat /proc/device-tree/demo_device/compatible下一步把前面那段demo驱动代码编译成模块insmod demo_driver.ko再看dmesg你应该能看到demo probe ok这样的打印。同时还能在platform总线上看到它ls /sys/bus/platform/devices/ cat /sys/bus/platform/devices/demo_device/ueventuevent文件里会打印出设备的compatible、MODALIAS等信息这也是排查匹配失败时的第一手证据。4.3 改了设备树却不生效的几个经典原因这个问题我在前面提了一嘴但值得单独列出来讲。实际项目里“我改了设备树但系统没用上”的情况实在太常见了通常是下面几个原因一是改错文件。你以为改的是最终编译的dts其实板级dts在某个地方用/delete-node/把这个节点删了或者用属性覆盖把它改回disabled。这种问题只能靠搜索确认没有捷径。二是编译了但没烧对分区。有的板子DTB打包在boot镜像里你只烧了boot分区里的内核没有把新DTB一起打进去。这种情况我建议养成一个习惯改完DTB后先strings一下新DTB确认里面包含你新增的compatible字符串再上机烧录。三是缓存。部分板子的Bootloader会在固定环境变量里指定DTB加载路径甚至会把DTB缓存在某个分区启动时优先读缓存版本。遇到这种平台需要先清掉缓存或者修改启动参数让Bootloader重新读取你烧录的DTB。这些操作因Bootloader而异但排查思路是通用的。5. 常见问题与排查技巧匹配失败时从哪下手5.1 compatible字符串对不上一切白搭驱动没有probe第一个要怀疑的就是compatible没匹配上。排查方法其实很简单进入系统后看设备树实际生效的compatible值cat /proc/device-tree/demo_device/compatible再看驱动里的of_device_id一个字符一个字符对比。这个看起来低级但我见过太多人就是在这里翻车。比如设备树写的是myvendor,my-demo-device驱动里写的是myvendor,my_demo_device中间一个横线一个下划线肉眼很难察觉但内核绝不给你通融。还有一种情况是节点路径没问题、compatible也一致但设备树节点所在的父节点没有被遍历。比如节点挂在某个disabled的I2C控制器下面那这个节点连device_node都不会被populate成任何设备驱动自然永远不会被调用。排查时先确认父节点状态是否okay。5.2 device和driver都在probe就是不执行如果/sys/bus/platform/devices/下能看到设备/sys/bus/platform/drivers/下也能看到驱动但probe始终不执行这就得换方向查了。先看status属性设备树节点如果显式写了status disabled即使compatible再匹配也不会创建设备。再看是不是所有依赖资源都就绪了内核用-EPROBE_DEFER延迟probe的机制前面说过遇到这种情况可以在dmesg里查deferred probe关键字也可以手动触发echo 1 /sys/kernel/debug/devices_deferred这样会让内核重新尝试所有挂起等待的设备probe对调试非常有用。另外确认驱动是编译成了模块还是内置如果模块没insmod驱动都还没注册设备再等等也是白等。如果想看驱动模型内部的匹配过程可以在内核配置中打开CONFIG_DEBUG_DRIVERy然后在内核启动参数里加dyndbgfile drivers/base/* pdmesg会输出大量bus匹配、driver注册的信息。调试完记得关掉不然日志煽得根本没法看。5.3 资源冲突、地址占用这类“硬件级”问题有时候probe确实执行了但死在platform_get_resource之后申请的步骤最常见的报错是resource already in use或者ioremap失败。这种问题的根因往往是两个设备节点写了相同的reg地址或者板级dts和SoC的.dtsi里的节点地址重叠。排查时看/proc/iomem确认你用的物理地址段没有被别的设备占用。尤其注意RK3568这类SoC不同型号之间外设地址映射有差异同样的外设在rk3568j和rk3568上可能地址不同设备树里写死之前务必对照芯片手册确认别拿隔壁项目的dts直接抄。如果是中断号冲突多半是interrupts的表示方式不对比如错误地把interrupt-cells当成了1而实际是2或者3。这种问题在dmesg里经常能看到interrupt-parent解析失败的提示顺着去看节点里的interrupt-parent有没有指到正确的中断控制器。5.4 老生常谈Windows驱动思维切到Linux要改掉什么最后一个问题算是经验之谈。很多人带着Windows下装驱动的习惯来学Linux遇到“nvidia-smi failed because it couldnt communicate with the nvidia driver”第一反应是去下载一个安装包重新装。在Linux环境里很多驱动问题根本不是“装不装”的问题而是“有没有被设备树正确描述”“模块有没有被正确加载”的问题。Windows的驱动模型与硬件ID、INF文件捆绑系统通过硬件ID自动查找匹配的驱动程序。Linux的设备驱动模型同样有自动匹配但匹配的上下文是设备树/ACPI表和compatible字符串。你不把设备树节点写好驱动代码写得再漂亮也是孤掌难鸣。至于类似“代码31”、驱动签名验证失败这类Windows驱动故障跟Linux这边的设备树、platform总线完全是两套逻辑不要用那一套来套Linux的开发经验。Linux里“装驱动”的正确姿势第一是确认硬件有没有在设备树里有正确描述第二是确认对应驱动是被编译进内核还是以模块形式加载第三才是看驱动probe有没有失败。把这三步顺序颠倒过来你会被各种奇怪现象折磨很久。如果你现在正在学Linux驱动我的建议是先别急着啃platform_driver的源码你只需要知道总线会怎么帮你匹配就好。把设备树里的compatible和驱动里的of_match_table这两行对齐你就能驱动90%的简单外设。剩下的坑大多都是在这两行之间翻来覆去踩出来的。等你亲自把一个LED、一个按键、一个外设控制器通过这条链路点亮这套关系就会彻底焊死在你的记忆里再也不会忘。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →