
在i.MX6ULL上写Linux驱动大多数人第一次接触到Platform机制时都会有个共同的困惑字符设备驱动不是挺好的吗直接ioremap、申请设备号、注册file_operations为什么内核非要绕一个大弯搞出platform_device、platform_driver、of_match_table这一堆结构体我在带新人做项目时这个问题几乎每次都会被问到。今天就把这件事彻底讲透从“为什么需要Platform”讲起一路拆到设备树节点和驱动代码在底层怎么完成“设备与驱动匹配”最后再分享一些我在i.MX6ULL上实际调试probe函数不执行时踩过的坑。先说结论Platform机制的本质是把“硬件长什么样”和“驱动怎么操作硬件”两件事解耦。硬件描述交给设备树和platform_device操作逻辑放进platform_driver二者由内核的总线模型做匹配匹配成功后自动调用驱动里的probe函数。理解了这条主线i.MX6ULL上绝大多数外设驱动的加载流程都能一眼看穿。1. 从“设备地址硬编码”到“一条虚拟总线”Platform机制的出现逻辑1.1 一枚“一次性驱动”是怎么把自己写死的很多年前我写过一个小项目外接LED灯驱动代码里直接写#define GPIO1_DR (0x020C0000 0x0000) #define GPIO1_GDIR (0x020C0000 0x0004) static int __init led_init(void) { void __iomem *base ioremap(GPIO1_DR, 0x10); // 配置GPIO方向为输出 writel(readl(base 4) | (1 18), base 4); // 点亮LED writel(readl(base) | (1 18), base); return 0; }那时候看起来挺爽的代码短跑起来直接控制寄存器。但问题也很明显板子一换GPIO换成别的引脚或者芯片内存映射地址变了整个驱动就要打开源码重新改宏再重新编译。更麻烦的是如果某个外设挂了两份实例比如两块同型号串口芯片挂在不同地址上这套写法就没法用一个驱动同时管两份硬件。这就是“硬件资源与驱动逻辑强耦合”的典型症状。1.2 设备模型让硬件描述与驱动逻辑各归其位Linux内核为了解决这个问题引入了设备模型Device Model。核心思想是定义三条角色总线bus、设备device、驱动driver。总线负责牵线搭桥设备负责描述“我是什么、我在哪、我有哪些资源”驱动负责描述“我会做什么、我怎么做”。打个比方这就好比家里的墙插和电器。总线是墙上的插座标准规定了火线零线怎么排布设备是插头插头上写着“这台电器需要220V交流电、最大功率2000W”驱动是电器内部的电路逻辑它只需要按照插座标准取电至于电是从哪里的变电站送来的它根本不用关心。换一个房间换一块板子只要插座标准一致电器插上去就能用。这套模型的价值不只是可移植性还带来了热插拔支持、电源管理、sysfs统一视图等能力。USB设备为什么能即插即用本质上就是设备模型在背后做设备与驱动的自动匹配。1.3 Platform总线名字里的玄机搞清楚设备模型后Platform就很好理解了。它是一条“虚拟总线”并不对应芯片内部某种硬件总线协议而是用来收纳那些既不属于USB、PCIe、I2C、SPI等标准总线又确实挂在CPU内存映射总线上的片上外设和集成设备。i.MX6ULL这颗芯片上绝大多数的片上外设——UART、GPIO、I2C控制器、SPI控制器、SDIO控制器、DMA控制器、以太网MAC——最终都以platform_device的形式注册在platform总线上。对应的驱动则通过platform_driver注册到同一条总线上然后由platform_match函数完成配对。所以你会看到在i.MX6ULL的BSP内核里drivers目录下几乎每个驱动文件里都有一个struct platform_driver结构体。它不是Linux内核里可有可无的语法糖而是整个外设驱动架构的地基。2. 设备树、platform_device与platform_driver三位一体2.1 设备模型里的三件套结构体要理解匹配机制先要认清三个核心结构体。第一个是总线本身Linux里用struct bus_type描述。platform总线的实例叫platform_bus_type它在drivers/base/platform.c里定义最重要的成员就是.match函数指针指向platform_match。内核在匹配设备和驱动时本质就是调用这个函数问一句“这俩合适吗”。第二个是设备platform_device结构体。它包含了设备的名字、编号、资源数组内存段、中断号、设备树节点指针of_node等。i.MX6ULL上外设寄存器地址和中断号最终都会被解析存进这里的resource数组。第三个是驱动platform_driver结构体。它里面有一套操作函数probe、remove、shutdown、suspend、resume以及两个重要的匹配辅助表一个是of_match_table用于设备树compatible匹配另一个是id_table用于传统的设备名字匹配。用一个表格概括这三者的分工结构体角色关键成员i.MX6ULL上的例子bus_type媒人match回调platform_bus_typeplatform_device硬件身份证resource、of_node、name来自uart1、i2c1等设备树节点platform_driver操作逻辑包probe、remove、of_match_table、id_table对应驱动的platform_driver实例2.2 i.MX6ULL设备树节点是如何变成platform_device的这个转换过程非常关键很多人的误解都出在这里。系统启动早期内核会把dtb二进制解析成一颗device_node树这一步叫unflatten_device_tree。之后内核启动到一定阶段会调用of_platform_default_populate_init遍历设备树节点为符合条件的节点创建platform_device。具体到i.MX6ULL它的soc节点通常带有compatible simple-bus这是一种让内核“批量展开子节点”的标记。凡是挂在simple-bus下面的子节点只要status不是disabled基本都会被创建成platform_device。比如imx6ull.dtsi里的uart1节点写着compatible fsl,imx6ul-uart、reg、interrupts等属性最终这些属性会被解析成platform_device里的resource数组和of_node指针。你在驱动里通过platform_get_resource(pdev, IORESOURCE_MEM, 0)拿到的寄存器地址本质就是设备树节点里reg属性解析出来的结果。中断号也类似通过platform_get_irq拿到。这里有一个容易忽略的细节I2C总线下面的子设备比如触摸屏、RTC芯片它们不会变成platform_device而是由I2C控制器驱动在probe之后主动遍历自己的子节点创建成i2c_client。SPI下面的从设备变成spi_device。同理MMC下面的设备也不会走platform总线。所以“设备树节点一定变成platform_device”这种说法不准确只有挂在CPU总线或simple-bus体系下、且没有专属总线归属的节点才会落到platform总线上。2.3 为什么有些设备树节点不会生成platform_device排查匹配问题的时候我经常发现设备树节点根本没生成platform_device后面的一切都无从谈起。常见的原因有三个。第一父节点没有simple-bus或可展开的bus compatible标记导致内核认为这个子树不属于platform总线。第二节点status属性是disabled内核会跳过不可用的设备。第三节点没有compatible属性。在设备树里compatible是“我有一个驱动对应我”的最基本标志没有它of_platform_populate通常不会为它创建平台设备。所以当你发现设备树里明明写了一个节点但/sys/bus/platform/devices下面找不到对应的目录时不要急着怀疑驱动先用后面第5章的方法去确认节点到底有没有被解析。3. 一次匹配的全过程到底是谁在调用谁3.1 probe调用链一驱动注册后主动寻找设备当你的驱动程序以模块方式加载或者编译进内核启动时都会执行platform_driver_register。这个函数内部做了什么呢它调用driver_register进入设备模型层接着bus_add_driver把驱动挂到platform总线上然后driver_attach开始扫描总线上已存在的设备。这一串调用中有一个核心动作对总线上每一个已经注册的platform_device依次使用platform_match判断是否匹配。如果匹配成功就调用driver_probe_device最终走到really_probe执行platfrom_driver里的probe回调。换句话说如果设备先注册、驱动后注册驱动会主动“回头”去设备列表里找合适的设备。这就是为什么insmod一个驱动模块时即使设备早已存在probe也照样会被调用。3.2 probe调用链二设备注册后倒追驱动反过来如果驱动先注册好设备树节点稍后变成platform_device内核也不可能错过。在of_platform_default_populate_init创建出platform_device后会执行device_add这个函数除了把设备挂到总线上还会调用device_attach遍历总线上已注册的driver列表逐个用platform_match去匹配。所以“设备先来”和“驱动先来”不是问题两条链路最终汇聚到同一个匹配函数匹配成功后都会调用probe。理解了这一点就不会再纠结为何有时dmesg先打印驱动注册、后打印probe或者反过来。3.3 platform_match里那几步对比是怎么排的前面反复提到的platform_match是匹配机制里最核心的函数。它在drivers/base/platform.c里大致逻辑可概括为以下几种情况的先后对比。第一种设备树compatible匹配。若platform_device有of_node且驱动设置了of_match_table内核会逐条比较设备树节点的compatible属性字符串和驱动of_match_table里每项的.compatible字符串是否相等。i.MX6ULL上绝大多数的外设驱动都用这种方式因为它和设备树绑定最自然。第二种ACPI匹配。x86和部分ARM服务器上常见i.MX6ULL上基本用不到直接跳过即可。第三种id_table匹配。比较驱动id_table里每一项的name字段和platform_device的name字段传统非设备树平台主要靠这个。第四种兜底的名字匹配。直接把platform_driver的driver.name和platform_device的name字段做字符串比较相同也算匹配成功。这里有个顺序问题值得注意id_table匹配和兜底名字匹配在设备树模式下优先级相对靠后。很多驱动只在.driver.name里写了一个名字没有设置of_match_table那么即使设备树compatible写了“myvendor,gpio-key”驱动也没办法靠compatible匹配只能靠pdev-name和driver.name碰运气。因此凡是设备树驱动的of_match_table一定不能漏这是我在代码评审里反复强调的一条。4. i.MX6ULL实战驱动代码、设备树与匹配验证三步走4.1 为i.MX6ULL准备一个最小可行驱动为了验证匹配机制我用一个最简单的GPIO按键驱动来演示。先看驱动侧代码#include linux/module.h #include linux/platform_device.h #include linux/of.h #include linux/gpio/consumer.h #include linux/interrupt.h static irqreturn_t key_isr(int irq, void *data) { pr_info(key pressed\n); return IRQ_HANDLED; } static int gpio_key_probe(struct platform_device *pdev) { struct device *dev pdev-dev; struct gpio_desc *desc; int irq; dev_info(dev, probe enter, node %s\n, dev-of_node-full_name); desc devm_gpiod_get_optional(dev, key, GPIOD_IN); if (IS_ERR(desc)) return PTR_ERR(desc); if (desc) { irq gpiod_to_irq(desc); if (irq 0) devm_request_irq(dev, irq, key_isr, IRQF_TRIGGER_FALLING, gpio-key, NULL); } return 0; } static int gpio_key_remove(struct platform_device *pdev) { dev_info(pdev-dev, remove\n); return 0; } static const struct of_device_id gpio_key_of_match[] { { .compatible myvendor,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); MODULE_LICENSE(GPL);这个驱动本身做的事情很少probe进来只打一条日志、申请一个按键中断但这恰恰是验证匹配机制最干净的方式只要probe被调用就说明设备和驱动配对成功。后面你想扩充成完整的按键上报驱动只需要在中断回调里补input子系统相关的代码即可。4.2 改设备树让硬件节点“长成”驱动期待的样子驱动侧compatible是“myvendor,gpio-key”设备树里就必须给一个相同字符串的节点。以i.MX6ULL开发板为例在根节点下添加如下内容#include dt-bindings/gpio/gpio.h / { key-test { compatible myvendor,gpio-key; pinctrl-names default; pinctrl-0 pinctrl_key_test; key-gpios gpio1 18 GPIO_ACTIVE_LOW; status okay; }; };对应的pinctrl节点放到iomuxc下面让GPIO1_IO18这个引脚复用成GPIO模式iomuxc { pinctrl_key_test: keytestgrp { fsl,pins MX6UL_PAD_UART1_CTS_B__GPIO1_IO18 0x1b0b0 ; }; };这里MX6UL_PAD_UART1_CTS_B__GPIO1_IO18宏定义在imx6ul-pinfunc.h里编译设备树时内核构建系统会自动处理好头文件路径。0x1b0b0是引脚配置值包括上下拉和驱动能力直接照抄常用值就行不影响本次匹配验证。修改好之后重新编译dtb烧录到开发板启动。如果驱动是模块启动后insmod如果是编译进内核启动日志里就能看到probe。4.3 验证三步sysfs、dmesg、modinfo模块加载后第一件事是看dmesgi.MX6ULL# dmesg | tail gpio-key platform key-test: probe enter, node /key-test看到这一行说明compatible匹配成功probe被调用了。如果没看到别急着改代码先确认/sys/bus/platform/devices目录下有没有key-test这个目录i.MX6ULL# ls /sys/bus/platform/devices/ key-test目录存在说明设备树节点已经成功生成了platform_device。然后可以查看设备的uevent文件里面会有一个MODALIAS字段它是设备树设备对外暴露的“身份证”i.MX6ULL# cat /sys/bus/platform/devices/key-test/uevent MODALIASof:Nkey-testTmyvendor,gpio-key这个字符串里藏着节点名key-test和compatible字符串。再看一下编出来的模块信息$ modinfo gpio-key.ko alias: of:Nkey-testTmyvendor,gpio-key如果这两边的字符串结构能对应上系统就可以在启动时仅凭MODALIAS自动加载对应驱动模块不需要手动insmod。这套验证三步走是我每次新写一个platform驱动必做的功课。只用dmesg看probe有时候不能区分是compatible匹配还是兜底name匹配而结合uevent和modinfo就能基本锁定匹配走的是哪条路径。5. 实测排错probe没被调用的六种原因与排查链路5.1 一次真实案例的完整排查过程有次同事在i.MX6ULL上移植一个光线传感器驱动设备树和驱动代码看起来都没问题但insmod之后dmesg里始终没有probe的打印。同事一度怀疑是编译器优化把打印优化掉了我劝他冷静按照下面这条链路从头捋。第一步确认设备树节点有没有变成platform_device。查看/sys/bus/platform/devices发现没有对应名称的目录。这就说明问题出在设备树侧跟驱动代码一毛钱关系都没有。第二步去/sys/firmware/devicetree/base下面找同名节点结果同样找不到。说明dtb里根本没编译进这个节点。检查后发现同事改的是临时使用的一个dts文件编译脚本默认编译的却是另一个dts修改的文件压根没生效。这是一个极其低级但非常常见的错误。第三步修正编译目标后节点出现在/sys/firmware/devicetree/base里但/sys/bus/platform/devices依然没有对应设备。再去读节点的statuscat出来的值是disabled。原来设备树模板里默认带上status disabled同事在另一个overlay里才改成了okay而overlay没有加载。改掉status后platform_device终于出现。第四步设备出现了但probe还是没进。这时读/sys/bus/platform/devices/xxx/uevent里的MODALIAS和modinfo驱动的alias做对比发现compatible字符串一个写的是“vendor,light-sensor”另一个是“vendor,lightsensor”少了根连线还是多了个连字符。这种字符级不一致肉眼很容易漏掉但机器一对比就现形。第五步修正compatible后probe正常执行。整个排查过程下来真正的问题其实都不是驱动代码逻辑而是设备树侧的节点没生效、状态不对、字符串不匹配。这也是为什么我一直强调驱动不跑先查设备树再查匹配最后才去怀疑代码逻辑。5.2 常见原因一compatible对不上一个节点可能写多个compatible字符串例如compatible myvendor,gpio-key, gpio-key;驱动只需要和其中一个匹配上即可。最容易翻车的是大小写、连字符、点号等细节以及设备树里字符串末尾不小心多了一个空格。这个没有捷径只能仔细对比。也有一种情况驱动里of_match_table设置了多个compatible但没有以空项结尾。内核遍历表时读越界行为不可预期有些编译器甚至会报错。因此of_match_table结尾一定要写{ /* sentinel */ }。5.3 常见原因二status disabled或父节点没展开设备树节点被disabled时不会生成platform_device这属于预期行为。但有时节点本身状态是okay父节点却disabled那整个子树都不会展开子节点自然也不会变成platform_device。还有一种情况父节点没有compatible simple-bus。如果你的设备挂在某个gpio控制器下面而它的父节点不是一个会自动展开的总线节点就需要父驱动在probe后手动解析子节点。我建议在i.MX6ULL项目里如果只是测试某个外设直接把节点放到root下面或soc下面避开总线归属问题。5.4 常见原因三id_table和兜底name匹配失败如果你没有设置of_match_table而只设置了driver.name在设备树时代常常会踩坑。比如device树节点叫key-test生成的pdev-name是“key-test”驱动.driver.name写的却是“gpio_key”这两个字符串不一致probe永远不会被调用。还有一种情况pdev-driver_override被改掉了。driver_override是sysfs里可以强制指定设备由哪个驱动接管的一个属性一般不会主动去碰但如果调试中用过它优先级最高即使compatible匹配成功、其他匹配条件都满足最终还是会被它“一票否决”。所以出问题时也检查一下/sys/bus/platform/devices/xxx/driver_override。5.5 常见原因四两个驱动抢一个设备如果两个platform_driver都能匹配同一个platform_device后注册的那一个probe不会被调用因为设备已经被第一个驱动占用。这种情况在of_match_table里写重复compatible时尤其容易出现比如两个驱动模块都声明了对“fsl,imx6ul-uart”的支持但其中一个不是完全体驱动注册时机不对就会干扰。解决办法很简单在设备树compatible里区分更精确的字符串或者把不需要的驱动从内核里去掉。同时在dmesg里可以看到类似“device already bound to driver”的提示别忽略。5.6 常见原因五模块自动加载失败这种最隐蔽因为模块手动insmod时probe是正常的但rootfs启动后系统就是不自动加载。原因往往是驱动缺少MODULE_DEVICE_TABLE导出。内核靠uevent里的MODALIAS匹配/lib/modules/.../modules.alias文件没有alias信息modprobe就不知道加载哪个模块。所以只要你的platform驱动面向设备树下面这两行几乎是标配static const struct of_device_id xxx_of_match[] { { .compatible vendor,device, }, { } }; MODULE_DEVICE_TABLE(of, xxx_of_match);注意别漏了MODULE_DEVICE_TABLE那一行。漏了编译也能过因为代码里没有任何符号缺失但模块信息里就没有alias。6. i.MX6ULL上probe之后的资源获取与BSP适配细节6.1 pinctrl与引脚复用probe只是长途跑的第一步很多i.MX6ULL初学者以为probe进来自动就万事大吉接着去操作GPIO结果发现引脚电平不对、外设不工作这才意识到还有引脚复用这回事。i.MX6ULL的每个引脚都由IOMUXC控制器管理要工作成GPIO、UART还是其它功能必须在设备树里配置pinctrl。设备树里外设节点通常这样写pinctrl-names default; pinctrl-0 pinctrl_uart1;只要驱动调用devm_pinctrl_register或者内核在really_probe阶段自动应用default状态引脚复用就会按设备树配置生效。这也是为什么我在第4章的例子里特意放了一个pinctrl节点如果key-gpios配置正确但没有pinctrlGPIO中断永远触发不了。被pinctrl坑过的经验是当probe正常返回但硬件行为诡异时先查IOMUXC寄存器对应的pad值是否和应用手册预期一致再回头查pinctrl节点。用devmem2或busybox devmem直接读IOMUXC的寄存器地址是很实用的诊断手段。6.2 resource获取ioremap之外的正规军不熟悉platform的人习惯在probe里直接硬编码物理地址然后ioremap。知道了匹配机制后正确的做法是从platform_device里拿资源struct resource *res; res platform_get_resource(pdev, IORESOURCE_MEM, 0); if (!res) return -ENXIO; base devm_ioremap_resource(dev, res);新版内核还提供了一个合并函数devm_platform_ioremap_resource(pdev, 0)一条龙完成获取资源和ioremap。中断号则是int irq platform_get_irq(pdev, 0);这些API的数据来源就是设备树节点里的reg和interrupts属性。内核在创建设备时已经把它们解析成了resource数组所以驱动不需要也不应该自己去翻设备树物理地址。6.3 时钟、复位与电源域经常被漏掉的probe依赖i.MX6ULL的外设主时钟默认不一定是开启的。比如调试UART时如果时钟没开启寄存器怎么写都不工作。在probe里通常需要clk devm_clk_get(dev, NULL); if (IS_ERR(clk)) return PTR_ERR(clk); clk_prepare_enable(clk);设备树里对应着clocks和clock-names属性。不配置时钟probe可能正常但外设功能完全瘫痪。这类问题在dmesg里常常没有太多线索因为不是匹配失败是匹配成功后的依赖缺失。还有一些模块带复位控制比如需要释放复位才能正常工作这时要留意设备树里的resets属性以及驱动框架里对应的reset_control_get和reset_control_deassert。6.4 从i.MX6ULL到全平台一套逻辑通吃最后说点我个人层面的观察。Platform匹配机制并不是i.MX6ULL特有的也不是NXP的私有技术它是Linux内核设备模型的标准流程任何嵌入式Linux平台——STM32MP1、全志、瑞芯微、树莓派——驱动加载都走同一套逻辑。区别只是设备树里的compatible字符串、寄存器地址映射、时钟系统名称等不同。所以我一直建议新人不要在i.MX6ULL上死记硬背某个外设驱动的代码而是把platform_driver注册、of_match_table、probe资源获取、pinctrl/时钟依赖这条链路刻在脑子里。换一块SoC你只需要换设备树驱动主体结构基本原封不动。这也是为什么很多公司做方案移植时驱动工程师最重要的能力不是背寄存器手册而是理解设备模型和总线匹配的底层逻辑。我在实际项目里现在调试任何platform设备驱动的第一步永远是打开/sys/bus/platform/devices/目录确认设备在不在第二步读uevent里的MODALIAS确认系统和modinfo能对上第三步才看dmesg里的probe日志。这三步五分钟就能做完却避免了我无数次在错误方向上浪费时间。这套方法送给你希望你也能少走几个弯路。