
1. 为什么非要有Platform总线不可先抛一个很多刚接触嵌入式Linux的开发者都会有的疑惑GPIO、I2C、SPI、UART这些外设控制器驱动模型里明明已经有对应的总线类型了为什么还要搞出一个Platform总线代码里没接在PCIe上也没挂在USB下一个内存映射设备凭什么非要往Platform上挂这个问题的答案得从Linux设备模型的设计逻辑说起。Linux内核把所有的硬件设备都抽象成设备-总线-驱动三个角色总线负责匹配设备和驱动。PCI设备有Vendor ID和Device ID可以精确识别USB设备有VID/PID可以识别但有一大类设备——SoC内部集成的外设控制器——它们直接挂在CPU的内存地址总线上没有自描述能力不响应任何枚举探测。你没法像插一块PCIe网卡那样通过扫描总线把它找出来它就在那儿地址固定、中断固定你需要主动告诉内核它存在。这类设备就是platform设备。内核搞出Platform总线本质上是给没有总线可挂的设备一个统一的挂载点和管理框架。i.MX6ULL内部的UART、GPIO、I2C控制器全部是platform设备。所以你写i.MX6ULL的驱动尤其是板级外设驱动比如某个GPIO控制的LED、某个挂在SPI总线上的LCD几乎绕不开Platform设备与驱动匹配这套机制。再直白一点在设备树DT大行其道的今天你在.dts文件里写的每一个节点绝大多数最终都会展开成一个platform_device注册进内核而你的每一个platform_driver负责和这些设备配对。理解不了这套配对逻辑你连probe函数为什么没被调用都排查不了更别说自己写内核模块了。这篇文章我就用i.MX6ULL平台的实际代码把Platform设备与驱动匹配机制从头到尾拆开讲清楚。包括设备是怎么注册进来的、驱动是怎么声明匹配条件的、内核的匹配优先级是什么、匹配成功后生命周期怎么走以及我实际开发中踩过的一些坑。内容尽量保持能够直接照着实验的颗粒度不只是讲概念。2. 设备这一侧是如何进入内核的从设备树DTS到platform_device2.1 设备树的展开过程在i.MX6ULL这种现代SoC平台上我们几乎不会手动用platform_device_register去注册设备而是写设备树。原因很简单SoC内部外设多、复用关系复杂、板级差异大用C代码逐个注册设备既死板又难维护。设备树则把硬件长什么样从内核源码里抽离出来变成一份独立描述文件。设备树到platform_device的转换路径大致如下DTS文件 -- DTB二进制 -- __unflatten_device_tree() 解析为device_node树 -- of_platform_bus_probe() 遍历根节点的compatible属性 -- 对每个匹配的节点调用of_platform_device_create_pdata() -- 创建struct platform_device并注册进内核这个转换不是一次性全做完的。内核先扫描根节点找到所有compatible属性匹配simple-bus之类值的节点把它们视作platform总线然后递归展开子节点持续创建platform_device。当然具体哪些设备会变成platform_device取决于节点的compatible属性是否匹配总线展开条件。以i.MX6ULL官方开发板比如正点原子或野火那一类的设备树为例你在.dts里定义一个LED节点/ { led { compatible gpio-leds; pinctrl-names default; pinctrl-0 pinctrl_led; led-gpios gpio1 3 GPIO_ACTIVE_LOW; status okay; }; };当内核启动、设备驱动模型初始化完成后这个节点会变成一个platform_device名字name字段根据节点名或compatible属性推断device_node指针指向对应的device_node。之后驱动侧就可以通过of_match_table里声明的compatible字符串和它配对。2.2 节点匹配simple-bus是第一个容易踩的坑这里有一个特别值得注意的细节很多初学者在设备树里自定义一个节点然后在驱动里用platform_driver_register注册驱动却发现probe始终不被调用。查来查去最后发现问题是自定义节点没有挂在任何匹配simple-bus的总线节点下或者自定义节点自身的compatible属性带上了某个不会被总线路由识别的值。更准确地说内核在把设备树节点转换成platform_device的时候不是对树上所有节点无差别转换。对于挂在根节点/下的平台设备内核的of_platform_default_populate_init会直接扫描根节点下一层并递归展开那些compatible为simple-bus、simple-mfd、isa等特定值的子节点。如果你的自定义节点不是挂在根节点下而是挂在某个I2C控制器节点下、SPI控制器节点下那它会被I2C或SPI核心识别为I2C设备或SPI设备而不是platform设备自然走不到platform_driver的probe。所以写设备树之前先想清楚我的设备是挂在哪个总线上的如果是GPIO控制的LED、内存映射的杂项外设、中断控制器这类内存映射设备放根节点或simple-bus节点下走platform机制如果是I2C从设备走I2C机制用i2c_driverSPI从设备同理用spi_driver。搞混了总线类型驱动写得再好也不会被调用这是设备模型的基本规矩。3. 驱动这一侧如何敲门platform_driver注册与of_match_table设备已经就位了驱动这边怎么注册自己并且告诉内核我能管哪些设备每个platform_driver本质上是一个struct platform_driver结构体最终通过platform_driver_register()注册到内核。这个结构体里最关键、也最容易出错的是两个字段driver.name和driver.of_match_table。以i.MX6ULL上一个简单的按键驱动为例static const struct of_device_id imx6ull_key_of_match[] { { .compatible fsl,imx6ull-key, }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, imx6ull_key_of_match); static struct platform_driver imx6ull_key_driver { .probe imx6ull_key_probe, .remove imx6ull_key_remove, .driver { .name imx6ull-key, .of_match_table imx6ull_key_of_match, }, }; module_platform_driver(imx6ull_key_driver);module_platform_driver是一个便捷宏展开后就是module_init调用platform_driver_register、module_exit调用platform_driver_unregister省得手写两个入口函数。3.1 匹配优先级of_match_table、id_table、name当内核需要给一个platform_device寻找驱动时匹配逻辑按以下优先顺序进行如果设备有device_node即来自设备树先检查驱动的of_match_table用compatible属性做匹配。这是当前设备树平台下最常用的路径。如果没有device_node则匹配platform_driver的id_tablestruct platform_device_id数组通过name字段匹配。仍然没有就退化成用driver.name和platform_device的name字段做字符串匹配。这里最让人困惑的是第三种情况。很多早期驱动或者习惯写内核模块的开发者会在platform_driver里只设置driver.name然后希望它匹配设备树节点名。系统确实会做字符串比较但有个坑platform_device的name字段优先取设备树节点中的compatible属性中第一个字符串而不是节点名。节点名通常被存在device_node-name里但platform_device的name已经做了转换。举个例子。你的设备树节点叫mydevicecompatible是vendor,mydevice。设备创建成platform_device之后它的name多半是vendor,mydevice而不是mydevice。如果驱动里只写了.driver.name mydevice那永远匹配不上。这就是为什么我在网上见过很多帖子问为什么名字明明对得上却probe不了——因为他们没搞明白这个name是从compatible来的。3.2 一个必须记住的MODULE_DEVICE_TABLE在of_match_table声明之后记得带上这行MODULE_DEVICE_TABLE(of, imx6ull_key_of_match);如果你打算把驱动编译成内核模块.ko这行宏会在模块编译时生成一个额外的别名段。这个别名用于模块自动加载场景当设备树中出现了匹配的compatible字符串时udev或mdev根据内核发出的uevent事件查找对应模块并自动加载。没有这行宏虽然不影响手动insmod但系统的自动加载机制就废了——很多嵌入式系统里设备都枚举出来了驱动却没被加载查到最后就是没写MODULE_DEVICE_TABLE。4. probe函数到底做了什么i.MX6ULL Platform驱动生命周期4.1 匹配成功后的完整生命周期平台设备和驱动匹配成功后内核会依次调用platform_driver的probe函数驱动正常工作设备移除或驱动卸载时调用remove函数系统关机或设备掉电时如果有需要调用shutdown函数以i.MX6ULL的GPIO按键驱动为例probe函数里通常会做这些事情static int imx6ull_key_probe(struct platform_device *pdev) { struct device *dev pdev-dev; struct imx6ull_key_dev *keydev; struct resource *res; int ret; keydev devm_kzalloc(dev, sizeof(*keydev), GFP_KERNEL); if (!keydev) return -ENOMEM; keydev-gpio of_get_named_gpio(dev-of_node, key-gpios, 0); if (!gpio_is_valid(keydev-gpio)) { dev_err(dev, failed to get key-gpio\n); return -EINVAL; } ret devm_gpio_request_one(dev, keydev-gpio, GPIOF_IN, imx6ull-key); if (ret 0) { dev_err(dev, failed to request gpio %d\n, keydev-gpio); return ret; } platform_set_drvdata(pdev, keydev); dev_info(dev, probe success\n); return 0; }4.2 为什么都推荐devm_系列API注意我上面全部使用了devm_前缀的资源管理函数devm_kzalloc、devm_gpio_request_one。这是设备驱动开发中特别值得养成习惯的一点。devm即device managed。这些API分配的资源自动绑定到设备生命周期上设备detach、驱动probe失败返回、设备释放的时候内核会自动帮你把这些资源全部释放掉。这有几个明显好处probe函数中途出错时不需要手工逐个回滚前面已经申请的资源直接return错误码即可。remove函数里不需要做一大堆释放操作代码干净很多。避免忘记释放导致的内存泄漏。这种泄漏在内核里排查起来相当痛苦devm从根本上降低犯错概率。传统非devm写法下probe里申请了GPIO、注册了中断、分配了内存中途某个步骤失败你得想着把前面的都释放掉。一旦漏了哪个轻则资源泄漏重则下次probe时申请资源失败一直起不来。我一直跟做驱动的新人强调能用devm就用devm这是Linux内核近几年主推的资源管理方式不是花架子。4.3 device_node、pdev-dev.of_node与of_get_*系列probe函数里另一个常见操作是从设备树节点中读取属性。pdev-dev.of_node就是devicetree node指针所有of_开头的API基本都围绕它操作。比如读取一个32位整数属性u32 poll_interval; ret of_property_read_u32(dev-of_node, poll-interval, poll_interval);比如读取GPIO编号除了上面那种of_get_named_gpio方式更现代的是用GPIO descriptor接口struct gpio_desc *key_gpio; key_gpio devm_gpiod_get_optional(dev, key, GPIOD_IN);两种接口的取舍我建议新项目优先用gpiod_系列。gpiod有引用计数、有设备绑定关系、还能直接处理设备树里的GPIO极性标志GPIO_ACTIVE_LOW/HIGH老gpio号接口在这些方面都很粗糙而且从设备树取出的gpio号范围还有全局gpio号变化的风险当gpio控制器探测顺序变化时。我在实际项目中就遇到过某个gpio controller驱动先加载时gpio号是某个值换了一种内核配置后gpio控制器探测顺序变了gpio号整体偏移老接口写死的gpio号全乱了。gpiod接口按name或index去查找内核内部处理了映射完全不受全局编号影响。这就是推荐它的核心理由。5. i.MX6ULL平台数据从哪来设备树属性解析与复用5.1 设备树节点和platform_data的关系在一些老旧的内核代码或非设备树平台比如以前的ARM板级代码、x86平台platform_device通常配合platform_data使用即platform_device.dev.platform_data指针指向一个结构体里面填好硬件配置参数。驱动通过pdev-dev.platform_data拿到参数。在设备树平台下platform_data的角色被设备树属性完全替代了。驱动不再直接拿platform_data而是从pdev-dev.of_node读取各种属性。从驱动开发的角度设备树节点本质就是一个键值对集合和platform_data提供参数的功能一样只是存储方式从C结构体变成了文本描述。唯一要注意的是代码里不能同时依赖of_node属性和platform_data来初始化同类参数否则可能出现平台数据为空却未做校验导致空指针访问的问题。5.2 实际i.MX6ULL开发中如何解析pinctrl和时钟Platform机制真正强大的地方在于它和Linux的pin control框架、clock框架深度集成。i.MX6ULL的每个外设引脚都要配置MUX模式、电气属性这些现在都在设备树里配置内核会在platform_device创建后自动应用这些配置。pinctrl属性通常在设备节点中这样配置iomuxc { pinctrl-names default; pinctrl-0 pinctrl_uart1; pinctrl_uart1: uart1grp { fsl,pins MX6UL_PAD_UART1_TX_DATA__UART1_DCE_TX 0x1b0b1 MX6UL_PAD_UART1_RX_DATA__UART1_DCE_RX 0x1b0b1 ; }; };驱动probe时不需要显式调用任何pinctrl API。platform子系统在设备probe之前会自动寻找并应用pinctrl-0指定的pin配置。驱动只需要在probe里正常操作UART寄存器即可。同理时钟框架也类似——设备节点里的clocks属性、clock-names属性驱动可以通过clk_get(dev, name)拿到对应的时钟也可以配置成runtime PM自动开关时钟。这几个框架协同工作的时候Platform驱动就能做到驱动代码里几乎没有硬件配置信息所有板级差异都收敛到设备树里。这也是为什么现在的ARM Linux驱动代码普遍短小精悍——配置和逻辑分离驱动专注于逻辑处理。6. 调试匹配问题的实战手段从sysfs看到force_probe实际开发中匹配不上的情况太多了我自己就经历过几次卡了一整天的调试。这里分享一下最实用的排查链路。第一步去sysfs确认设备到底有没有被创建。运行ls /sys/bus/platform/devices/如果设备树节点正确展开你会看到类似这样的条目1e10000.uart 20a0000.i2c led设备条目的名字通常是地址.名称格式或者纯节点名。如果这里没有你的设备那问题出在设备树展开阶段驱动代码看都不用看。第二步确认设备节点是否带driver链接ls -l /sys/bus/platform/devices/led/ # 如果已经匹配了驱动会出现driver - ../../bus/platform/drivers/imx6ull-key如果设备有了但driver链接没出现说明匹配失败。此时去查设备树compatible属性和驱动of_match_table是否完全一致注意厂商前缀是否多了少了、大小写。驱动是否已加载lsmod | grep key驱动自己的probe是不是失败了dmesg | grep imx6ull-key第三步就是一种比较少为人知的调试手法使用force_probe。比如你确认compatible是对的驱动也加载了系统却还是没匹配上而且你想快速验证驱动本身的probe逻辑有没有问题可以这样做echo imx6ull-key /sys/bus/platform/drivers/imx6ull-key/bindbind文件允许你手动把设备和驱动绑定起来。不过更常用的是force_probe需要内核开启CONFIG_DEBUG_FS且platform驱动支持。如果你不想重新编译内核可以手动把设备地址写入bind让platform核心尝试匹配。还有一种更狠的调试方式把驱动暂时改成不需要匹配直接在init函数里手动构造platform_device并注册。这种方式在早期开发裸外设驱动时效率很高先验证寄存器操作逻辑再接入设备树匹配流程。我不建议一直这样写但作为阶段性验证手段非常有效。7. 一个完整的可编译实例LED Platform驱动为了把上面这些机制串起来我给出一个完整的i.MX6ULL平台LED驱动示例并且会标注编译和加载的要点。设备树侧需要准备如下内容。在根节点下添加LED子节点/ { goodix_led: goodix-led { compatible example,goodix-led; pinctrl-names default; pinctrl-0 pinctrl_goodix_led; led-gpios gpio1 3 GPIO_ACTIVE_LOW; poll-interval 100; status okay; }; };iomuxc节点里添加引脚配置iomuxc { pinctrl_goodix_led: goodixledgrp { fsl,pins MX6UL_PAD_GPIO1_IO03__GPIO1_IO03 0x10b0 ; }; };内核模块代码如下保存为goodix_led_platform.c#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 struct goodix_led_dev { struct gpio_desc *led_gpio; int poll_interval; }; static int goodix_led_probe(struct platform_device *pdev) { struct device *dev pdev-dev; struct goodix_led_dev *ldev; u32 val 0; int ret; ldev devm_kzalloc(dev, sizeof(*ldev), GFP_KERNEL); if (!ldev) return -ENOMEM; platform_set_drvdata(pdev, ldev); ldev-led_gpio devm_gpiod_get(dev, led, GPIOD_OUT_HIGH); if (IS_ERR(ldev-led_gpio)) { ret PTR_ERR(ldev-led_gpio); dev_err(dev, failed to get led gpio: %d\n, ret); return ret; } ret of_property_read_u32(dev-of_node, poll-interval, val); if (ret 0) ldev-poll_interval val; else ldev-poll_interval 100; gpiod_set_value(ldev-led_gpio, 1); dev_info(dev, probe ok, poll_interval%d\n, ldev-poll_interval); return 0; } static void goodix_led_shutdown(struct platform_device *pdev) { struct goodix_led_dev *ldev platform_get_drvdata(pdev); gpiod_set_value(ldev-led_gpio, 0); dev_info(pdev-dev, shutdown, led off\n); } static int goodix_led_remove(struct platform_device *pdev) { struct goodix_led_dev *ldev platform_get_drvdata(pdev); gpiod_set_value(ldev-led_gpio, 0); dev_info(pdev-dev, remove, led off\n); return 0; } static const struct of_device_id goodix_led_of_match[] { { .compatible example,goodix-led, }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, goodix_led_of_match); static struct platform_driver goodix_led_driver { .probe goodix_led_probe, .remove goodix_led_remove, .shutdown goodix_led_shutdown, .driver { .name goodix-led, .of_match_table goodix_led_of_match, }, }; module_platform_driver(goodix_led_driver); MODULE_LICENSE(GPL); MODULE_AUTHOR(Your Name); MODULE_DESCRIPTION(i.MX6ULL Platform LED driver example);编译这个模块的Makefileobj-m : goodix_led_platform.o KERNELDIR : /path/to/your/kernel/source PWD : $(shell pwd) all: $(MAKE) -C $(KERNELDIR) M$(PWD) modules clean: $(MAKE) -C $(KERNELDIR) M$(PWD) clean加载注意事项insmod goodix_led_platform.ko dmesg | tail如果设备树里已经配好了节点dmesg里会出现goodix-led: probe ok, poll_interval100如果dmesg里没看到信息按前面说的方法逐层排查。这里还补充一个细节如果你的内核编译时启用了modules_install可以顺手把模块安装到根文件系统/lib/modules/$(uname -r)/extra/下然后depmod这样以后设备树节点出现时模块能够自动加载。8. 打通Platform机制后下一步往哪走从这个简单LED实例延伸出去i.MX6ULL平台驱动的进阶方向大概有这几个。第一是中断处理。把GPIO按键接到Platform驱动里使用request_irq或devm_request_irq注册中断处理函数注意在设备树里配置interrupts属性时i.MX6ULL的GPIO中断号需要经过gpio_to_irq或gpiod_to_irq转换不是设备树里直接写死某个数字。这一点写过x86 PCI驱动的人容易踩坑——PCI中断资源由总线枚举分配GPIO中断却要通过GPIO控制器动态分配。第二是miscdevice和platform_driver的组合。一个Platform驱动挂在一个Linux字符设备框架下主设备号使用misc动态分配实现read/write/ioctl接口这是嵌入式Linux里最常见的外设驱动形态LED、按键、传感器都这么干。misc设备注册和注销极其轻量一个ngpio引脚的设备用misc比用alloc_chrdev_region简单太多。第三是异步probe。如果某个Platform驱动的probe过程比较耗时比如要等待外部芯片复位、要初始化一个很大的DMA缓冲可以在platform_driver.driver中设置.probe_type PROBE_PREFER_ASYNCHRONOUS让内核允许该驱动异步执行probe不阻塞整条设备初始化链。尤其在系统上有多个设备并行初始化时能明显缩短启动时间。这一个字段就能解决的问题很多团队却为了优化启动时间在init脚本里做各种串行操作实在没有必要。最后还是要强调Platform机制不是孤立概念它是整个Linux设备模型的外延。通过platform_device和platform_driver这套配对逻辑你能自然理解到为什么驱动只需要写probe函数、为什么设备树改了引脚配置驱动代码不用动、为什么sysfs下有那层层叠叠的目录结构。这套机制看似绕实则把硬件描述和软件逻辑拆得干干净净。懂了这个再看PCI、USB、I2C这些具体总线驱动你会发现框架全是同一套路子只是匹配键值从compatible字符串变成了Vendor ID和Device ID而已。我在实际开发里最大的体会是花一个下午把Platform匹配机制的源代码走一遍drivers/base/platform.c和drivers/base/dd.c这两个文件就够了比看十篇博客都有用。drivers/base/dd.c里的really_probe函数就是驱动匹配成功后从probe到设备真正ready的全过程一行行看下去整个Linux驱动的一半江山你基本上就拿下了。