ARTICLE DETAIL

资讯详情

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

Linux Platform设备驱动匹配机制详解:从设备树到probe的完整链路

Linux Platform设备驱动匹配机制详解:从设备树到probe的完整链路 作为一个常年跟i.MX6ULL打交道的驱动开发工程师每次看到群里有人问为什么我的probe函数不执行为什么设备树里明明写了节点驱动就是match不上我都觉得大家对Platform匹配机制的理解还停留在背面试题的阶段。这套机制其实没那么玄乎它就是Linux内核为了管理那些不依附于PCI、USB这类热插拔总线的设备而建的一套虚拟总线模型。但恰恰是这套模型把设备树、驱动、内核对象模型串在了一起只要一环没对上整个驱动就静悄悄的不干活。这篇文章我打算从i.MX6ULL这颗NXP的Cortex-A7芯片出发把Platform设备与驱动的匹配机制整个拆开揉碎。不光是讲匹配的几种方式更重要的是告诉你设备树节点到底是怎么变成platform_device的compatible属性是怎么被拿去比较的为什么of_match_table里的名字能和设备树对上驱动probe的触发路径又是怎样的最后再分享一些调试技巧和真实踩坑经历。先说明一下文中涉及的内核版本是Linux 4.9.88NXP官方BSP常用的版本其他版本源码路径可能略有差异但机制大同小异。1. 为什么要搞出个Platform总线理解设备、驱动、总线三者关系要弄清楚匹配机制先得明白一个基础问题Linux的设备模型里为什么专门有Platform平台这套东西1.1 不是所有设备都能挂在物理总线上你想想看PCI设备有PCI总线USB设备有USB总线I2C设备有I2C总线SPI设备有SPI总线。这些总线的特点是有标准的硬件接口协议内核有对应的总线核心设备可以被枚举、可以被热插拔。但SoC内部或者SoC直接引出的那些外设呢比如UART控制器、GPIO控制器、DMA控制器、网卡MAC它们并不依附于某条物理总线。它们就是芯片设计时直接挂在CPU内存总线上的一组寄存器地址就固定在那里中断号写死在芯片手册里。这类设备在Linux中统一纳入Platform总线管理。Platform总线是一条虚拟总线它存在的意义就是让设备和驱动这两类内核对象有一个统一的挂载点、匹配规则和probe触发机制。在老的、不用设备树的内核比如board-xxx.c这种板级文件时代里platform_device结构体是开发者手动在C代码里填充的。而在现代内核里如果你启用了设备树Device TreeDT那platform_device的创建权就交给了内核的设备树解析代码。1.2 Linux设备模型的核心结构体Platform框架下有三个关键结构体platform_device描述一个设备包括它的名字、编号、资源IO地址、中断号、DMA通道等。在设备树模式下它由of_platform_bus_probe或of_platform_populate从设备树节点转换而来。platform_driver描述一个驱动包括probe、remove、shutdown等操作函数以及驱动要匹配的设备ID列表id_table或of_match_table。bus_type描述总线本身Linux内核里的platform_bus_type它定义了如何匹配设备和驱动、如何枚举设备等。这三者关系可以这样理解设备是我是谁驱动是我会干什么总线是红娘负责把合适的驱动和合适的设备牵到一起。如果红娘牵线成功就调用驱动里的probe函数如果驱动和设备一直没能匹配上probe就永远不会执行你在驱动里写的初始化代码就相当于不存在。1.3 i.MX6ULL场景下的典型Platform设备i.MX6ULL这颗芯片上几乎所有的内部外设都是Platform设备。比如1001000.esdhcUSDHC SD卡控制器20c0000.uartUART串口控制器20e0000.ecspiECSPI控制器2090000.ethernetENET网卡控制器20cc000.gpioGPIO控制器这些设备节点都定义在设备树源文件.dts里内核启动时设备树解析代码将每个带compatible属性的节点变成一个platform_device并挂到Platform总线上。随后Platform总线会拿这个设备和平台上注册过的每一个platform_driver做相亲。这个相亲条件就是匹配机制的核心。2. 设备树节点如何变身platform_deviceof_platform的幕后工作前面说了设备树节点最终会变成platform_device但这个过程不是自动发生的它有一套完整的触发流程。很多时候驱动不工作问题就出在这个环节你以为设备树写了节点就万事大吉但内核压根没把你的节点变成platform_device。2.1 从DTS文件到device_node先梳理一下设备树相关的完整链路。一个.dts文件在编译时会变成.dtb二进制文件内核启动早期引导程序U-Boot会把.dtb加载到内存中并把地址传给内核。内核里的unflatten_device_tree()函数负责把二进制的FDTFlattened Device Tree解析成一颗由struct device_node节点组成的内存树。struct device_node是设备树节点在内核中的表示它保存了节点的名字、compatible字符串列表、reg地址属性、interrupts中断属性等。但注意device_node还不是platform_device——它只是一个静态描述还没有纳入内核的设备驱动模型。2.2 什么条件会触发platform_device的创建不是所有device_node都会变成platform_device。内核在遍历设备树时会检查节点的compatible属性。如果一个节点的compatible是和某个已注册的platform_driver的of_match_table能匹配上那内核就在这个时点创建platform_device。但更常见的是内核根据节点类型直接决定是否创建。具体规则大致如下节点有compatible属性且不是simple-bus、simple-mfd等特殊桥接节点会被创建为platform_device。节点是simple-bus类型的它本身不创建为platform_device而是递归遍历它的子节点把符合条件的子节点创建为platform_device。这就是为什么很多SoC厂商喜欢在dts里把外设控制器放在一个simple-bus节点下面——因为这样可以让内核自动去枚举。在imx6ull.dtsiNXP官方BSP里的SoC级设备树文件中soc节点的compatible simple-bus这保证了aips1、aips2、aips3这些总线容器节点下的子节点都能被枚举为platform设备。2.3 of_platform_default_populate的调用时机在Linux 4.9里of_platform_default_populate()函数被调用的路径大致是start_kernel() - setup_arch() - unflatten_device_tree() - rest_init() - kernel_init() - kernel_init_freeable() - do_basic_setup() - driver_init() - of_platform_default_populate(NULL, NULL, NULL)这里driver_init()负责初始化内核设备模型的各个子系统而of_platform_default_populate()则遍历整棵设备树对每个符合条件的节点调用of_platform_bus_create()最终创建出platform_device。有一点要注意每个platform_device有一个dev.of_node指针指向它对应的device_node。这个指针在后面的匹配过程中至关重要因为内核就是通过它去找设备树节点的compatible属性的。调试时你会在/sys/devices/platform/目录下看到一堆设备目录这些就是已经创建好的platform设备。2.4 实际开发中容易忽视的细节我在实际项目中遇到过一个挺典型的案例在uboot里改了设备树给某个外设节点加了status okay但内核起来后/sys/devices/platform/下找不到对应的设备目录。后来排查发现问题出在fsl,imx6ull-gpio这个节点的父节点是一个compatible既不是simple-bus也不是simple-mfd的节点内核默认不会递归创建子节点的设备。这个问题的解决方案是在父节点补充simple-bus的compatible或者直接在父节点的驱动probe里手动调用of_platform_populate()去创建子设备。所以如果probe不执行第一件事不是去翻匹配表而是先确认你的节点到底有没有被变成platform_device。怎么确认最简单的办法是设备起来后看/sys/bus/platform/devices/或者看启动日志里有没有platform ...相关打印。3. 匹配机制核心四种匹配方式的优先级与背后逻辑现在进入正题Platform总线上设备和驱动是怎么匹配的platform_match()函数是这套机制的核心。它的实现位于drivers/base/platform.c我直接说结论匹配顺序依次是OF类型匹配比较platform_driver-driver.of_match_table里的每个of_device_id与platform_device-dev.of_node的compatible属性。ACPI类型匹配比较platform_driver-driver.acpi_match_table。id_table匹配比较platform_driver-id_table里的每个platform_device_id的name字段与platform_device的name字段。默认匹配直接比较platform_driver-driver.name和platform_device-name。3.1 OF匹配设备树时代的绝对主流在现代ARM Linux开发中绝大多数情况走的就是OF匹配。当设备树使能CONFIG_OF时platform_match()会优先调用of_driver_match_device()。该函数的核心逻辑是拿platform_driver中of_match_table里的每一条of_device_id去和设备树节点的compatible列表比对。of_device_id结构体和设备树节点里的compatible是怎么联系起来的关键在这几个字段struct of_device_id { char name[32]; // 设备名一般不用 char type[32]; // 设备类型一般不用 char compatible[128]; // 用于匹配 compatible 属性 const void *data; // 匹配成功后回传给驱动的私有数据 };当你在驱动里写static const struct of_device_id my_gpio_of_match[] { { .compatible fsl,imx6ull-gpio, }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, my_gpio_of_match);设备树里写gpio5: gpio20ac000 { compatible fsl,imx6ull-gpio, fsl,imx6ul-gpio; reg 0x020ac000 0x4000; interrupts GIC_SPI 74 IRQ_TYPE_LEVEL_HIGH; ... };匹配时内核拿fsl,imx6ull-gpio去和节点compatible列表里的第一项fsl,imx6ull-gpio比——完全一致匹配成功。如果第一项不匹配继续比后面的fsl,imx6ul-gpio。只要有一项相同就算匹配成功。3.2 一个容易混淆的点compatible匹配到底谁和谁比有个知识点我要专门强调of_match_table里的compatible是逐项和设备树节点compatible列表比的而且设备树compatible列表是有顺序的。第一项是当前具体芯片型号后面跟着的是兼容的旧型号或通用型号。比如上面例子中的compatible fsl,imx6ull-gpio, fsl,imx6ul-gpio;它表示imx6ull这个引脚控制器硬件兼容imx6ul的引脚控制器。如果你的驱动of_match_table里只有{ .compatible fsl,imx6ul-gpio }那它同样能匹配上因为内核会一路比对到第二项。这正是Linux设备树新芯片兼容旧芯片的思路。另外内核里匹配compatible字符串时用的是of_device_is_compatible()该函数会比较字符串完全相等。没有什么模糊匹配、前缀匹配的说法必须是严格的字符串相等。3.3 id_table匹配老派但依然有效的方案在没有设备树的时代platform_device_id是主要的匹配方式。它的定义是struct platform_device_id { char name[PLATFORM_NAME_SIZE]; kernel_ulong_t driver_data; };驱动中这样声明static const struct platform_device_id my_uart_id_table[] { { .name imx6ull-uart, .driver_data (kernel_ulong_t)imx6ull_uart_data }, { } };匹配时内核会拿platform_device-name去和id_table里每一项的name比较。platform_device-name在设备树模式下通常取的是设备节点的node-name比如节点名uart02020000的name经过处理是uart或者of_device_id匹配成功后用的也是设备树节点的name而不是compatible。这里细节比较绕我放到后面实战中怎么确认的小节讲。3.4 默认匹配最容易被忽视的后门默认匹配很简单直接把platform_driver.driver.name和platform_device.name做字符串比较。比如驱动注册时指定name为my_driver而某个platform设备也叫my_driver那就匹配上了。这种方式在老代码里非常常见但现代开发中如果是设备树环境基本不用这种匹配方式容易出问题因为你没法保证设备name的稳定性。但理解它有助于你排查一些奇怪现象比如你注册驱动时没写of_match_table结果某个设备的platform_device.name恰好和驱动name相同probe也被触发了你可能还以为是自己配置对了——其实匹配的是默认路径。3.5 匹配优先级对开发者的启示因为OF匹配优先级最高所以当你的设备树节点存在时优先保证of_match_table写得对。只有在你确认设备树没有对应节点的情况下才考虑id_table或者默认name匹配。这一点对移植驱动特别重要老内核比如3.x时代基于board文件的代码换到新内核设备树体系光改驱动文件里的of_match_table还不够还要确认设备树里节点compatible写得和表里一致。4. i.MX6ULL实际案例从设备树到probe的完整匹配链路理论讲了不少下面我用手头一个i.MX6ULL上的GPIO按键驱动为例把整个链路完整走一遍。这个例子我在项目里用过简单清晰适合作为匹配机制的解剖样本。4.1 设备树侧的写法假设我要在i.MX6ULL的GPIO5_IO01上接一个按键。设备树中首先需要把GPIO5这个控制器节点所在的gpio5引用出来然后在根节点或合适的总线节点下定义一个按键节点/ { key_gpio { compatible mycompany,gpio-key; pinctrl-names default; pinctrl-0 pinctrl_key_gpio; key-gpio gpio5 1 GPIO_ACTIVE_LOW; status okay; }; }; iomuxc { pinctrl_key_gpio: key_gpiogrp { fsl,pins MX6UL_PAD_SNVS_TAMPER1__GPIO5_IO01 0x17059 ; }; };这个节点的compatible是mycompany,gpio-key。它位于根节点不是simple-bus但放心根节点本身也会被of_platform_default_populate()扫描只要是compatible属性存在并且不是某些特殊类型就会被创建为platform_device。4.2 驱动侧的匹配表驱动代码中我们需要定义匹配表static const struct of_device_id gpio_key_of_match[] { { .compatible mycompany,gpio-key, }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, gpio_key_of_match); static struct platform_driver gpio_key_driver { .probe gpio_key_probe, .remove gpio_key_remove, .driver { .name gpio-key, .of_match_table gpio_key_of_match, }, }; module_platform_driver(gpio_key_driver);驱动注册时platform_driver_register()会将驱动挂到Platform总线上。此时如果设备树已经把key_gpio节点变成了platform_device那么platform总线会立刻对每个既有设备执行匹配。匹配成功gpio_key_probe被调用。4.3 匹配成功的日志与行为验证在调试串口上应该能看到类似gpio-key gpio-key.0: GPIO key probed或者我们自己打印的probe日志[ 1.234567] my_gpio_key_probe: enter如果没有看到probe日志根据我前面的分析先检查/sys/bus/platform/devices/下有没有gpio-key.0这个目录。如果连目录都没有说明设备树节点没有被转换为platform_device问题在设备树解析阶段。如果目录有probe没执行问题在匹配表。4.4 如果连目录都没有怎么继续排查确认设备树有没有被正确编译进去、uboot传递的dtb是哪个文件、节点语法是否正确这些老生常谈的就不展开了我提一个比较隐蔽的点如果节点设置了status disabledof_platform_bus_create()会跳过这个节点不创建platform_device。所以在调试时status okay一定要显式写上。还有一个坑节点名node name不要带地址后缀的情况下有些特殊字符也会导致解析异常。5. 匹配成功后执行过的那些钩子probe到remove的生命周期匹配机制只是入口真正驱动工作的还是probe函数以及后续的生命周期管理。这里我顺着匹配成功的时机往后讲把Platform设备的完整生命周期捋一遍很多驱动只写了probe但remove、shutdown、PM操作函数处理不好系统休眠唤醒后设备就出问题这类问题在i.MX6ULL的工业产品上特别常见。5.1 probe执行的详细路径匹配成功并不是直接调用probe那么简单中间还隔着一层。当platform_match()返回真后内核会进一步调用platform_drv_probe()这个函数做了一些准备工作如果设备树节点有dma-ranges或dma-coherent等属性为设备初始化DMA掩码。获取设备的ACPI句柄如果启用ACPI。将platform_device关联到platform_driverdev-driver drv-driver。调用drv-probe(dev)即我们的probe函数。在probe函数里开发者一般要做这几件事static int gpio_key_probe(struct platform_device *pdev) { struct device *dev pdev-dev; struct device_node *np dev-of_node; struct gpio_key_data *data; int ret; /* 1. 分配私有数据 */ data devm_kzalloc(dev, sizeof(*data), GFP_KERNEL); if (!data) return -ENOMEM; /* 2. 获取设备树资源 */ >static int gpio_key_remove(struct platform_device *pdev) { struct gpio_key_data *data platform_get_drvdata(pdev); if (data-timer) del_timer_sync(data-timer); if (data-thread) kthread_stop(data-thread); return 0; }在实际系统里remove触发有两个时点一是驱动模块卸载时二是设备节点在sysfs里被手动unbindecho设备名到/sys/bus/platform/drivers/xxx/unbind。如果你在调试驱动更新频繁insmod/rmmodremove函数写不好系统日志会刷一堆BUG: scheduling while atomic或者资源泄漏。我见过一个同事的驱动每次rmmod直接重启最后发现是remove里访问了已经释放的内存。5.3 shutdown和PM操作函数Platform驱动的生命周期里还有两个容易被忽略的成员shutdown系统关机或重启时调用要在这个函数里把设备置回安全状态比如关中断、停DMA、把GPIO拉到安全电平。pm一个struct dev_pm_ops指针包含suspend/resume等回调。i.MX6ULL这类工业级芯片在低功耗场景下suspend/resume是否正常直接影响产品体验。我建议只要驱动注册了中断、启用了时钟就必须实现这几类回调。不然你的设备在系统睡眠唤醒后中断可能变成哑巴——中断号还在但中断控制器侧的配置丢了结果设备不再上报中断。6. 实战调试当probe不执行时怎么一步步定位这部分是全文最容易抄作业的内容。probe不执行几乎能占到Platform驱动开发调试的一半工作量。我把排查思路按照由外到内、由低到高的顺序整理一下。6.1 第一步确认设备是否注册系统起来后执行ls /sys/bus/platform/devices/如果能找到类似gpio-key.0的目录说明platform_device已经创建成功。如果找不到检查dts节点是否在编译后的dtb里fdtdump your.dtb | grep -A 5 -B 5 gpio-key检查节点是否有status okay检查父节点是否满足枚举条件simple-bus等检查内核启动日志中有没有设备树相关error6.2 第二步确认驱动是否注册查看ls /sys/bus/platform/drivers/gpio-key/如果没有这个驱动目录说明module_platform_driver()那步就没生效。可能是驱动没有编译进内核检查Kconfig/Makefile或者module加载失败dmesg | tail查看。6.3 第三步确认驱动目录下有没有设备软链接如果设备已经注册、驱动也已经注册但驱动目录下没有任何设备的软链接说明匹配失败。此时最有效的手段是打开内核的驱动模型debugecho file drivers/base/platform.c p /sys/kernel/debug/dynamic_debug/control dmesg -w然后重新触发或者rmmod/insmod驱动platform_match内部的一些匹配过程会打印出来。kernel 4.9上platform_match里有dev_dbg级别的打印打开后能清楚看到匹配到了哪一步、为什么没通过。6.4 第四步核对compatible是否真的相等这一步是最常见的坑你在驱动里写的compatible和设备树里的compatible看起来一样实际上由于大小写、内容顺序、多余空格对不上。# 查看设备树节点最终的compatible属性 ls /proc/device-tree/ | grep gpio-key cat /proc/device-tree/gpio-key/compatible记住/proc/device-tree下的状态就是内核最终解析到的状态。如果cat compatible出来显示mycompany,gpio-key而驱动里写的是mycompany,gpio_key注意下划线那就永远匹配不上。开发时这种低级错误不在少数。6.5 第五步检查是否有多个驱动抢同一个设备不过多提但确实有这种情况两个驱动都声明了相同的compatible内核先注册的那个会match成功并probe后注册的发现设备已经有驱动的跳过。你觉得自己驱动写对了其实是另一个驱动抢了先。用ls -l /sys/bus/platform/devices/gpio-key.0/driver看一下实际driver链到哪里。6.6 第六步终极手段——手动强制probe如果所有配置看起来都对但probe就是不执行可以用一种暴力但非常有效的方式在sysfs里手动触发。echo gpio-key.0 /sys/bus/platform/drivers/gpio-key/bind如果手动bind能成功说明驱动本身没问题问题出在自动匹配流程中。如果手动bind也报错那么probe函数内部很可能有问题比如返回值错误此时dmesg会直接告诉你probe失败原因。7. 深入一下of_match_table里data字段的妙用匹配机制还有一些进阶玩法对于管理同类多型号外设特别实用比如在of_device_id的data字段里带上设备专属配置。匹配成功后驱动可以通过of_match_device()拿到这个data指针从而实现同一个驱动兼容多个型号的设备。7.1 用data字段区分不同型号以i.MX系列的FEC以太网控制器为例imx6ull和imx6ul的FEC寄存器布局几乎一样但PHY接口配置可能有细微差异。与其写两个驱动不如在一个驱动里区分static const struct fec_variant imx6ull_fec_data { .phy_interface PHY_INTERFACE_MODE_RMII, .has_ptp true, }; static const struct fec_variant imx6ul_fec_data { .phy_interface PHY_INTERFACE_MODE_MII, .has_ptp false, }; static const struct of_device_id fec_dt_ids[] { { .compatible fsl,imx6ull-fec, .data imx6ull_fec_data }, { .compatible fsl,imx6ul-fec, .data imx6ul_fec_data }, { } }; static int fec_probe(struct platform_device *pdev) { const struct of_device_id *of_id; const struct fec_variant *variant; of_id of_match_device(fec_dt_ids, pdev-dev); if (of_id) variant of_id-data; else variant default_fec_data; ... }驱动代码里用of_match_device()而不是直接访问of_match_table好处是它内部做了设备节点匹配有效性的检查。匹配成功时返回的指针就是从of_match_table里命中那一项。然后用of_id-data取到设备专属配置。这在设备树模式下比platform_get_device_id()拿id_table的driver_data更自然。因为设备树本身就指明了我是哪种芯片驱动只是顺着设备树的描述找到对应的配置。7.2 通过内核打印确认data字段是否生效调试时可以在probe里加一句dev_info(pdev-dev, variant: phy%d, has_ptp%d\n, variant-phy_interface, variant-has_ptp);根据输出就能确认设备树里的compatible命中了哪组data。如果发现打印的配置和预期不符多半是设备树里compatible的顺序导致它命中了前面的旧型号配置。调整compatible顺序或者增加更精确的匹配项就能解决。8. 从匹配机制看驱动模型的整体设计思路最后这一节我想跳出具体代码聊聊Platform匹配机制背后的设计哲学。理解这些对于你写出更符合内核习惯的驱动有帮助。8.1 为什么是驱动找设备而不是设备找驱动在类Unix的系统里驱动是被动等待的。设备插入或设备节点被创建时内核会遍历总线上所有已注册驱动找一个最适合的来匹配。这么做的好处是驱动可以在系统启动的任意时点被加载模块化即使设备已经存在只要驱动一注册匹配立即发生probe立刻触发。反过来如果设备在驱动注册之后才出现在某些动态创建platform_device的场景总线也会主动通知驱动来做匹配。这种解耦设计让设备驱动模型变得非常灵活设备可以静态存在于设备树也可以由其他驱动动态创建驱动可以直接编译进内核也可以按需加载。两边都是即插即用的。8.2 设备节点与platform_device的关系不是穷举而是映射很多人把设备树理解成硬件配置清单这没错但不够准确。设备树其实是一个硬件描述语言它描述的是硬件拓扑和资源而不是驱动逻辑。内核解析设备树并创建platform_device相当于把描述性的硬件信息实例化成了内核对象。匹配机制就是在实例化后的对象和驱动能力之间搭桥。理解这一点后你会发现很多奇怪的驱动行为都有了合理解释为什么驱动probe会执行两次因为设备树里有两个compatible相同的节点每个节点都会生成一个独立的platform_device驱动自然要为每个设备实例执行一次probe。这在多串口、多GPIO控制器的芯片上特别常见也是platform_device命名里带.0、.1这类序号的原因。8.3 内核源码阅读建议如果你想把匹配机制吃透我建议按下面顺序读内核源码drivers/base/platform.cplatform_match()、platform_drv_probe()、platform_driver_register()drivers/of/platform.cof_platform_bus_create()、of_platform_default_populate()drivers/of/device.cof_driver_match_device()、of_match_device()drivers/base/dd.cdriver_probe_device()这是驱动模型里真正的媒人函数读的时候不要只看函数实现要结合/sys/bus/platform下的实际目录结构去验证理解。内核源码配合sysfs观察是最直观的学习路径。匹配机制是整个Linux设备模型里最基础也最重要的一块。i.MX6ULL的开发板上跑通一个Platform驱动并不难但真正搞懂匹配的每个环节、知道异常时怎么定位需要时间和实践的积累。希望这篇文章能帮你少走一些弯路尤其是probe不执行时不知道从哪里下手的问题。如果你正在做嵌入式Linux驱动开发遇到类似的情况不妨按文中的排查链路一步一步来多数问题都能在半小时内定位。
返回列表