)
从事i.MX6ULL这块板子的Linux驱动开发有一段时间后我越来越觉得Platform设备驱动模型是整个BSP开发里绕不开的核心。很多初学者上来就对着芯片手册写ioremap、request_irq跑个点灯demo觉得挺顺但一旦开始接触设备树、接触NXP官方内核、要把自己的外设驱动注册进系统几乎都会在platform_driver_register和compatible匹配上卡住。这篇文章我想把Platform设备与驱动的匹配机制从原理到实操完整地捋一遍尤其是i.MX6ULL上最常用的设备树匹配方式配合实际代码和排错思路帮你在工程里少走弯路。这篇文章适合正在学嵌入式Linux驱动、拿到了i.MX6ULL开发板但不知道从哪下手的人也适合已经在写字符设备、想理解设备模型到底怎么工作的朋友。我不会只贴代码更想把每一步背后的为什么说清楚包括probe为什么不调用、MODULE_DEVICE_TABLE到底有什么用、of_match_table和id_table该选哪个这类细节。1. 为什么ARM Linux驱动非要搞出一套Platform机制1.1 从“一个驱动写死一片硬件”说起早期Linux驱动开发里设备和驱动是强绑定的。写一个LED驱动直接在驱动代码里写上寄存器地址比如0x020C406C然后ioremap、操作GPIO编译成模块insmod后设备就能工作。这套流程在单片机上很常见自己写裸机程序也这样干但放到Linux内核里就会出问题硬件地址硬编码换了板子、换了GPIO就改代码重新编译。一个驱动只能对应一个具体设备没法一套代码支持多个硬件变体。内核里设备和驱动耦合在一块想分离“设备信息”和“驱动逻辑”非常困难。后来Linux引入了设备模型把“设备”和“驱动”彻底分开。设备只负责描述“硬件有什么资源”比如寄存器地址、中断号、GPIO编号驱动只负责描述“怎么操作这类硬件”。两者通过总线来match。对于PCI、USB、I2C、SPI这些有真实物理总线的设备设备本身挂在总线上总线的枚举机制就能搞定匹配。但ARM内部很多外设比如UART、GPIO控制器、DMA控制器、看门狗它们不是挂在某个物理总线上而是SoC内部集成的在系统中没有一个天然的“父总线”可以挂靠。这时Platform总线就出现了。它是一条虚拟总线专门用来承载这些“没有物理总线”的设备。名字里的Platform翻译成“平台”听起来有点玄乎实际上你可以把它理解为一张“挂板”所有不方便归类的片上设备都往这张板上放设备和驱动通过名称、ID或者设备树里的compatible属性在这张板上完成配对。1.2 Platform设备驱动模型的基本分层理解Platform机制先记住两件事一个是platform_device一个是platform_driver。platform_device代表“这个硬件存在”它可以是内核代码里通过platform_device_register注册的也可以是由设备树解析自动生成的。在i.MX6ULL这种使用设备树的内核上后者是绝对主流。你打开arch/arm/boot/dts/imx6ull.dtsi里面每个被statusokay的节点内核启动时都会经过of_platform_populate等逻辑把设备树节点转换成platform_device挂到platform总线上。platform_driver代表“谁来操作这个硬件”它由驱动开发者实现核心包含probe函数、remove函数、id_table或of_match_table等。platform_driver注册到内核后会和总线上现有的platform_device挨个进行匹配测试。匹配成功系统就调用该driver的probe函数匹配失败driver就安静地待在总线上等待将来出现能匹配的设备。这套分层带来的直接好处是硬件变了只要改设备树驱动代码一个字节都不用动。举个例子同样是GPIO点灯旧做法是驱动里写死“GPIO1_IO03”新做法是设备树里写gpiogpio1 3 GPIO_ACTIVE_LOW驱动通过devm_gpiod_get_optional去获取GPIO。硬件换到GPIO5_IO01改dts重新编译内核或设备树驱动照常工作。这就是设备与驱动分离在实际工程里最真实的体现。2. 匹配机制核心compatible、id_table、name三条路径怎么选2.1 platform_match内核里那几行关键代码总线的核心工作就是match。platform总线的match回调函数在drivers/base/platform.c里名字叫platform_match。我建议每个人都把这段代码找出来读一遍源码是解释机制最好的老师。简化后的逻辑大概是首先尝试OF风格匹配如果device有of_node也就是它来自设备树节点就用driver的of_match_table去和节点的compatible属性比对。然后尝试ACPI匹配主要是x86等支持ACPI的平台ARM通常不走这条。接着尝试id_table匹配把driver的id_table里每个条目和device的name做比较。最后尝试最朴素的name匹配直接比较driver_driver.name和device.name。很多人以为platform匹配只有设备树那一种其实不对。哪怕不用设备树一个名字完全相同的platform_device和platform_driver也能匹配上只是这种方式在设备树普及之后很少用而已。2.2 OF匹配设备树compatible与of_match_table的工作方式在i.MX6ULL上绝大多数驱动走的都是OF匹配。设备树里每个节点都有compatible属性gpioled { compatible atkalpha-led; pinctrl-names default; pinctrl-0 pinctrl_led; led-gpio gpio1 3 GPIO_ACTIVE_LOW; status okay; };驱动端对应的of_match_table长这样static const struct of_device_id led_of_match[] { { .compatible atkalpha-led, }, { /* Sentinel */ } }; MODULE_DEVICE_TABLE(of, led_of_match); static struct platform_driver led_driver { .probe led_probe, .remove led_remove, .driver { .name led, .of_match_table led_of_match, }, }; module_platform_driver(led_driver);这里有几个容易忽略的关键点第一of_match_table数组最后一定要有一个空条目作为哨兵内核遍历时遇到全零条目就停止。漏掉它驱动注册时很可能会越界遍历导致各种奇怪的crash。第二MODULE_DEVICE_TABLE(of, led_of_match)看起来可有可无但如果这个驱动被编译成.ko模块这行宏会把of_match_table里的compatible信息提取出来放到模块的modinfo段中。系统里的udev/mdev工具读取modinfo后才能在设备出现时自动加载匹配的驱动模块。没有这行宏你只能手动insmod。对工程部署来说自动加载是刚需建议每个驱动都写上。第三compatible的命名虽然随意但行业惯例是“厂商,型号”的格式比如“fsl,imx6ull-gpio-led”、“ti,omap3-dma”。这样做的主要目的是避免不同厂商的不同设备出现同名冲突。你自己练习时可以写点个性化命名但一旦进入企业项目请严格遵守这个惯例。2.3 传统匹配id_table和name路径的适用场景OF匹配不是Platform匹配的全部。再说回id_table这种传统方式static const struct platform_device_id imx6ull_led_id_table[] { { .name gpio-led, .driver_data 0 }, {}, }; MODULE_DEVICE_TABLE(platform, imx6ull_led_id_table);当设备树不存在或者设备不是由设备树节点生成时platform_device就是通过platform_device_register这类API手动注册的。比如在板级文件老内核的mach-xxx.c里或者在一些简单的Linux内核测试里经常能看到这样的代码static struct platform_device led_device { .name gpio-led, .id -1, .resource led_resources, .num_resources ARRAY_SIZE(led_resources), }; platform_device_register(led_device);驱动端id_table里的name字段和platform_device的name字段一致匹配就成功。但请注意现代ARM Linux内核包括NXP官方维护的4.x、5.x内核全面转向设备树后mach-xxx.c里的板级platform_device代码基本被清空了。你如果是在i.MX6ULL上跟着新内核学习几乎没有机会手动注册platform_device但理解这条路径仍然有意义——很多网上老资料、老面试题、老代码片段还在用这种方式你看到了至少能一眼认出它在干什么。name路径则更简单直接把platform_driver里.driver.name设成“gpio-led”然后让platform_device的name也等于“gpio-led”就能匹配。它和id_table的区别在于id_table可以同时支持多个name而driver_name只能对应一个。实际项目中id_table比纯name匹配更常用因为它支持一个驱动匹配多个设备名还可以携带driver_data。2.4 三种匹配的优先级与如何选择就i.MX6ULL这个平台的实际开发来说首选OF匹配因为设备树已经是标准配置而且支持你在设备树里任意修改引脚、中断、时钟等资源驱动无需改动。如果没有设备树或者你写的是比较独立、不做DTS改动的模块才考虑id_table。name匹配是兜底方案中的兜底通常用于临时测试、教学demo不适合正经产品。还有一个优先级的知识点如果驱动的.of_match_table、id_table同时存在时内核优先使用OF匹配。也就是说即使id_table里有更精确的name匹配项只要device上有of_node还是先看compatible。所以在写驱动时不要想“两边都写上多一条路多一份保险”这实际上会让代码产生误导两个表同时存在时调试会更加混乱我建议根据项目情况只保留一种。3. i.MX6ULL实战从设备树节点到driver注册的完整链路3.1 i.MX6ULL平台的设备树结构与节点写法i.MX6ULL的官方设备树组织很有层次。底层是arch/arm/boot/dts/imx6ull.dtsi里面定义了SoC内部几乎所有外设控制器。这个.dtsi的基础结构如下soc { compatible simple-bus; #address-cells 1; #size-cells 1; ranges; gpio1: gpio0209c000 { compatible fsl,imx6ul-gpio, fsl,imx6ull-gpio; reg 0x0209c000 0x4000; interrupts GIC_SPI 66 IRQ_TYPE_LEVEL_HIGH, GIC_SPI 67 IRQ_TYPE_LEVEL_HIGH; gpio-controller; #gpio-cells 2; interrupt-controller; #interrupt-cells 2; }; };顶层的中soc节点有一个特殊属性compatible simple-bus。内核的of_platform_populate函数在遍历设备树时遇到simple-bus节点会继续递归创建子节点的platform_device。也就是说soc节点下的gpio1、uart1、i2c1这些节点都会被解析成platform_device。我们自己添加外设时推荐的做法不是在imx6ull.dtsi里直接改而是在板级dts文件比如imx6ull-myboard.dts里追加节点。比如添加一个GPIO LED节点/ { myled: myled { compatible myboard,led; pinctrl-names default; pinctrl-0 pinctrl_myled; led-gpio gpio1 3 GPIO_ACTIVE_LOW; status okay; }; }; iomuxc { pinctrl_myled: myledgrp { fsl,pins MX6UL_PAD_GPIO1_IO03__GPIO1_IO03 0x10b0 ; }; };这里我特意没有放在soc节点下而是放在根节点/下。根节点下没有被simple-bus承接的节点如果它带compatible属性也会被当作platform_device注册。内核函数of_platform_default_populate_init在初始化时会扫描整个设备树对满足条件的节点调用of_platform_bus_create最终生成platform_device。另外要注意节点里如果只有status okay而没有compatible内核根本不知道这个节点属于什么设备也就不会为它创建platform_device。关于status“disabled”的影响后面排错章节我还会再讲。3.2 driver端定义of_match_table、probe与remove设备树那边把硬件描述好了驱动端就是一套标准的模板。一个完整的LED Platform驱动框架如下#include linux/module.h #include linux/platform_device.h #include linux/of.h #include linux/of_gpio.h #include linux/gpio/consumer.h #include linux/delay.h static struct gpio_desc *led_gpio; static int myled_probe(struct platform_device *pdev) { struct device *dev pdev-dev; led_gpio devm_gpiod_get(dev, led, GPIOD_OUT_LOW); if (IS_ERR(led_gpio)) { dev_err(dev, Failed to get led gpio: %ld\n, PTR_ERR(led_gpio)); return PTR_ERR(led_gpio); } gpiod_set_value(led_gpio, 1); dev_info(dev, led probe success\n); return 0; } static int myled_remove(struct platform_device *pdev) { if (led_gpio) gpiod_set_value(led_gpio, 0); return 0; } static const struct of_device_id myled_of_match[] { { .compatible myboard,led }, { /* Sentinel */ } }; MODULE_DEVICE_TABLE(of, myled_of_match); static struct platform_driver myled_driver { .probe myled_probe, .remove myled_remove, .driver { .name myboard_led, .of_match_table myled_of_match, }, }; module_platform_driver(myled_driver); MODULE_LICENSE(GPL); MODULE_AUTHOR(Your Name); MODULE_DESCRIPTION(i.MX6ULL platform led driver);注意看probe里用的devm_gpiod_get。devm全称是managed device resource属于设备资源管理机制。用devm_开头的API申请的资源GPIO、时钟、中断、内存在驱动卸载或probe失败时内核会自动帮你释放。传统方式是request_mem_region、ioremap、gpio_request、free_irq每个都要手动配对释放漏一个就内存泄漏或者资源冲突。现在的内核开发强烈推荐devm_这套API省心且不容易出错。3.3 把驱动跑起来的完整命令与验证方法代码写完编译并加载驱动的过程有几个关键点。如果你是直接在NXP官方SDK对应的内核源码树上开发把led.c放到drivers/char或drivers/misc下然后修改Kconfig和Makefile这是正规的内核驱动工程做法。但快速验证时用外部模块编译更省事。我习惯建一个led目录里面放led.c和Makefileobj-m : myled.o KERNELDIR : /home/user/linux-imx ARCH : arm CROSS_COMPILE : arm-linux-gnueabihf- all: $(MAKE) -C $(KERNELDIR) ARCH$(ARCH) CROSS_COMPILE$(CROSS_COMPILE) M$(PWD) modules clean: $(MAKE) -C $(KERNELDIR) ARCH$(ARCH) CROSS_COMPILE$(CROSS_COMPILE) M$(PWD) clean注意KERNELDIR指向的必须是你正在运行的、设备树也在用的那个内核源码目录。用uname -r查一下板子上的内核版本确保源码对应。版本不一致会导致模块加载时报version magic错误。编译完成后# 拷贝到板子上假设已经用scp/nfs等方式传过去了 insmod myled.ko如果一切正常你在dmesg里能看到“led probe success”之类的输出。想确认匹配和驱动绑定状态可以查看sysfs# 查看myled设备是否存在 ls /sys/bus/platform/devices/myled # 或者用设备名查看 ls /sys/bus/platform/devices/ | grep myled # 查看driver是否绑定到device上 ls -l /sys/bus/platform/devices/myled/driver如果driver链接存在说明匹配成功并且probe已经执行了。另外还有一组更直观的目录/sys/bus/platform/drivers/myled_device/下面会会出现设备节点的符号链接比如myled这说明driver和device已经绑定。这两个目录是排查匹配问题时的第一站。4. probe函数进不去匹配失败的完整排查链路4.1 第一步确认设备是否真的注册成功遇到probe不调用先别急着改驱动代码。第一步是确认设备树节点到底有没有被解析成platform_device。在板子上执行ls /sys/bus/platform/devices/ | grep myled如果没有输出说明设备树那边就没生成device。此时就要回过去查设备树节点有没有写status okay设备树默认对不认识的节点可能不展开或者父节点是disabled。节点有没有compatible属性没有compatible内核不知道它是什么设备不会创建platform_device。dts编译有没有报错编译出来的dtb有没有真的被u-boot加载到这是最常被忽略的一步——你改了dts但板子启动用的还是之前烧写的旧dtb。我实际遇到过一种情况改完dts后发现设备节点一直不出现后来用了fdtdump去解板子实际运行的dtb才发现当前uboot环境变量里fdt_file指向的是另一个文件自己改的dts压根没编进用来启动的那个dtb。排查的时候不要默认“我改了dts就一定会生效”一定要确认内核运行时实际加载的dtb内容。用设备树源文件里的node名可能和生成的platform_device名不同。比如节点myled: myled平台设备名通常是“myled”但节点命名含地址时如myled020c4000platform_device名字可能是“myled.0”或“myled020c4000”。所以ls的时候最好用grep关键字而不是精确匹配避免名字对不上产生误判。4.2 第二步核对driver端匹配表设备已经存在但probe没进这时去查driver的注册情况和匹配表。执行ls /sys/bus/platform/drivers/ | grep myled如果没有这个driver目录说明模块加载失败了查看insmod时的报错、dmesg尾部日志。常见原因包括模块编译时的内核源码和当前运行内核不匹配、符号版本不一致、依赖的模块没先加载。driver目录存在但设备和driver没有绑定大概率是匹配表的问题。逐一对比设备树compatible写的是“myboard,led”of_match_table里compatible也写的是“myboard,led”但一个用逗号后面跟空格一个没有或者大小写不一致。驱动of_match_table数组后面忘了加哨兵条目或者加错了。设备树里compatible被写成了属性名i2c那边常见的“myboard,led”和“myboard,i2c-led”一类的多项而of_match_table没有包含全部。使用的设备树节点明明是i2c子节点但非要写platform_driver去匹配。i2c设备挂载在i2c总线上不会出现在platform总线上这属于挂错总线了。需要特别注意的是系统里可能存在多个设备共用同一个compatible比如内核自带的gpio-leds驱动兼容“gpio-leds”这个compatible你如果自己写一个driver也声称支持“gpio-leds”会产生竞争谁先注册谁绑上后注册的driver就绑不上设备。所以自己练习时尽量不要使用内核里已经存在的compatible字符串。4.3 第三步检查设备树状态与pinctrl配置的影响设备树节点的status字段容易被忽略。节点默认不存在status时等价于“okay”但如果某个父节点或者节点本身被标成了disabled内核遍历设备树时不会为它生成platform_device。还有一种情况节点本身statusokay但芯片的iomuxc等父节点被裁剪或状态异常驱动probe没执行到获取GPIO那一步就返回了错误。当然probe执行与否是匹配机制决定的和pinctrl没有直接关系但pinctrl配置出错可能导致probe内部失败现象上同样是“灯没亮”很容易被误认为“没有匹配上”。区分这两种情况最简单的办法就是在probe函数最开头加一行dev_info打印然后看dmesg。如果打印出现了说明匹配成功问题在probe内部逻辑如果没打印才真正需要回头查匹配机制。4.4 第四步MODULE_DEVICE_TABLE与模块加载自动化的坑很多人在自己板子上手动insmod没问题但一放到产品里就发现驱动不生效。大多数情况是缺少自动加载机制。前面说过MODULE_DEVICE_TABLE(of, myled_of_match)负责把compatible信息写进模块的modinfo。如果没有这行insmod可以正常工作但udev无法知道“device出现时需要加载哪个模块”。检查方法modinfo myled.ko | grep alias如果输出类似“alias: of:NmyledTNoneCmyboard,led”这样的alias行说明模块导出信息正常。如果没有alias基本就是MODULE_DEVICE_TABLE缺失或者参数写错了。此时即使设备树节点已经生成/sys/bus/platform/devices/myled也有了udev也不知道该modprobe哪个模块。另外模块安装路径也要注意。把myled.ko拷贝到/lib/modules/$(uname -r)/extra/再执行depmod -a然后modprobe myled才能正常工作。直接insmod虽然能加载但无法形成“按需自动加载”的完整链路。对一个产品系统来说这差别很大。5. 双端分离带来的工程化收效与进阶思考5.1 厂商BSP与方案商的协作方式理解了匹配机制之后你再去读NXP提供的BSP驱动代码就会有一种豁然开朗的感觉。厂商BSP里大量驱动都是platform_driver对外的差异几乎全部收敛到设备树里了。freescale官方的imx6ull系列BSP里gpio-keys、leds-gpio、pwm-backlight、sdhci-esdhc-imx等驱动全是这种模式。这种双端分离对实际团队协作的帮助非常大。方案商拿到一颗新SoC时硬件工程师说LED挪到了GPIO1_IO05软件这边只需要改dts不需要动C代码如果扩展板上一颗I2C触摸屏换了一个型号只要新触摸屏驱动也是i2c_client体系也可能只改dts。硬件改版带来的软件工作量被压缩到“改dts 增量编译dtb”这个粒度不再需要重新编译内核镜像更不需要重新编译驱动模块。对量产产品来说可以同时维护多个dtb一个内核固件适配多个硬件版本这是Platform机制和of_match_table在实际工程里的最大价值。NXP官方BSP里经常出现一个外设对应多个compatible的情况。以GPIO控制器为例imx6ull.dtsi的gpio1节点写了compatible fsl,imx6ul-gpio, fsl,imx6ull-gpio。这意味着一个device节点可以有多个compatible字符串驱动端的of_match_table里只要能匹配其中任意一个就能成功绑定。厂商这么写是为了兼容同一SoC系列的不同型号比如imx6ul和imx6ull共用一个内核gpio驱动可以通过这个方式同时支持两个芯片。我们自己做板级dts时也可以借鉴这个思路用兼容的多级命名让一个驱动适配多个变种硬件。5.2 从platform_driver看整个设备驱动模型Platform这套东西只是Linux设备驱动模型的一个子集。理解了它再去看i2c_driver、spi_driver、pci_driver会发现整个框架惊人相似——都是driver注册到总线总线根据某种规则匹配device匹配成功回调probe。曾经有人说Linux驱动开发的核心就是“结构体回调函数”这句话我是认同的。你在Platform驱动里用到的of_match_table、probe、remove、devm_xxx到了I2C驱动里几乎原样复用只是匹配表可能换成i2c_device_idprobe参数从platform_device换成i2c_client。所以建议不要只停留在会用platform_driver_register这个层面。去读一下/sys/bus/platform下面的目录结构观察一下device和driver的绑定关系再去看I2C子系统的driver文件你会很快建立起“总线-设备-驱动”的全局观。这个模型贯穿整个Linux内核是每个做嵌入式Linux驱动的人都该熟练掌握的基本功。debugfs和sysfs在排查时给你的信息远比反复打印调试信息来得直观。最后分享一个小技巧。很多人拿到一个内核镜像想知道某个外设到底被哪个驱动支持最快的方法是查看设备树里的compatible再去内核源码里grep这个compatible。比如grep -r atkalpha-led drivers/一次grep能找到of_match_table定义的地方再顺藤摸瓜找到probe函数整个驱动的入口和资源获取逻辑就清楚了。反过来看到一个struct of_device_id数组想知道该改设备树哪个节点也可以把compatible字符串拿到dts里搜索。这比抱着几千页的芯片手册硬啃高效得多。我自己排查驱动问题一半以上的时间都花在“对齐compatible字符串”和“确认dtb实际加载内容”这两件事上这是经验之谈也算是对后来者的一点提醒。