十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

嵌入式Linux驱动开发:Platform设备与驱动匹配机制全解析

嵌入式Linux驱动开发:Platform设备与驱动匹配机制全解析 我刚开始接触嵌入式Linux驱动的时候一直有个困惑网上那些驱动例程入口不是module_init吗怎么到了公司代码或者SDK里到处都是probe函数而且这个probe还不用自己调用加载完模块它自己就跑了。后来才明白90%的片上外设驱动都是Platform驱动而probe能不能被调用全看“设备与驱动匹配机制”有没有生效。这篇文章我就以i.MX6ULL平台为背景把Platform设备与驱动的匹配机制彻底讲透。既说清楚内核源码级别的匹配逻辑也把设备树节点怎么变成platform_device、驱动注册后probe为什么会被自动调用这两条链路梳理通。最后给出一套可以在开发板上直接验证的实操步骤以及排查“probe不执行”的完整思路。适合正在学嵌入式Linux驱动、或者已经被“Platform总线”搞得一头雾水的朋友。1. 为什么Linux要造出一条虚拟总线——Platform总线的设计初衷要弄懂Platform设备与驱动匹配机制首先得明白一个更基础的问题为什么内核非要设计一条“虚拟总线”出来这事得从Linux设备模型的底层逻辑说起。1.1 设备模型的三要素总线、设备、驱动Linux内核中任何设备都跑不出设备模型Device Model的框架这个框架由三个核心对象组成总线bus、设备device、驱动driver。它们三者的关系可以这样理解总线是设备与驱动之间的“红娘”负责给设备和驱动牵线搭桥。设备是物理硬件在内核中的抽象代表“有一个硬件存在”。驱动是软件逻辑的载体代表“我能操作某类硬件”。在一个典型的PC机上USB设备插入主机USB总线控制器会检测到硬件变化在内核里创建一个usb_device然后USB总线会去扫描已注册的所有usb_driver看看谁的id_table能对上这个新设备。对上了就调用驱动的probe设备正式开始工作。这套模型的核心思想是设备与驱动分离设备只管描述自己是谁驱动只管声明自己能处理谁两者不需要在代码里互相引用。总线匹配机制是连接它们的唯一纽带。1.2 SoC片上外设的尴尬处境问题来了。PCI有PCI总线USB有USB总线SDIO有SDIO总线它们都有物理存在、有标准枚举协议、支持热插拔。但i.MX6ULL这种SoC芯片里的UART、I2C、SPI、GPIO控制器怎么办这些外设并不是插在一个可枚举的物理总线上而是直接集成在芯片内部挂在SoC内部总线上通过寄存器地址映射来访问。从硬件上看它们不支持“热插拔”也没有标准的“厂商ID设备ID”供软件枚举。但它们依然是实实在在的设备也需要挂到某条总线上才能被设备模型管理。你总不能给每种片上外设都发明一条具体总线吧比如i2c控制器挂i2c总线、uart控制器挂uart总线那样维护成本太高而且它们本质上都是Memory-Mapped设备行为模式一模一样。于是内核的开发者在很久以前做了一个极具实用主义风格的决定统一挂到一条虚拟总线上命名为platform_bus_type。1.3 Platform到底是干什么的Platform总线在/Sysfs中对应的目录是/sys/bus/platform但它没有任何物理硬件对应。它的作用只有一个给“直接集成在SoC上、通过内存映射访问的设备”提供统一的挂载点。这类设备在内核里就叫platform_device专门驱动它们的就叫platform_driver。凡是满足下面特征之一的设备通常都会用Platform框架来写驱动设备直接集成在SoC内部通过寄存器地址访问GPIO控制器、UART、I2C控制器等。设备挂在无标准总线的外部总线上比如板级扩展IO芯片。系统自带的虚拟设备比如定时器、GPIO LED、寄存器映射的杂项设备。我总结过一句话特别适合入门时建立直觉Platform总线就是主板上的“焊死区”。USB设备像插线板上的插头可以随时插拔而片上外设就像焊接在电路板上的芯片物理上不可移动但软件层面它们同样需要“通电工作”Platform总线就是给它们统一供电供软件的通道。有了这个背景Platform设备与驱动匹配机制的必要性就很清楚了既然一个SoC上有几十上百个片上外设每个外设可能都有对应的驱动内核必须有一套高效且可靠的匹配规则让每个设备准确找到自己的驱动绝不能出现GPIO驱动跑到了UART设备上的情况。2. 匹配的入口platform_match函数到底在比什么知道了Platform总线存在的意义接下来要直面最核心的问题设备和驱动怎么匹配这个问题的答案全部集中在内核源码的一个函数里platform_match。它在drivers/base/platform.c中我以Linux 5.x内核为例把核心流程拆开看。2.1 platform_match的完整判断逻辑源码的大致逻辑如下不同版本略有差异但主干一致static int platform_match(struct device *dev, struct device_driver *drv) { struct platform_device *pdev to_platform_device(dev); struct platform_driver *pdrv to_platform_driver(drv); /* 1. 设备树匹配优先检查 compatible 属性 */ if (of_driver_match_device(dev, drv)) return 1; /* 2. ACPI 匹配x86/ARM64服务器场景使用嵌入式基本不涉及 */ if (acpi_driver_match_device(dev, drv)) return 1; /* 3. id_table 匹配驱动设备ID表逐个比对设备名 */ if (pdrv-id_table) return platform_match_id(pdrv-id_table, pdev) ! NULL; /* 4. 设备名与驱动名直接比对 */ return (strcmp(pdev-name, drv-name) 0); }这四步就是整个Platform设备与驱动匹配机制的“法律条文”。它按优先级从上到下执行任何一条命中了就直接返回匹配成功后面的逻辑不再执行。理解这四步基本就理解了匹配机制的全部精华。2.2 设备树匹配是绝对主流在i.MX6ULL这类用设备树Device TreeDT的平台中第1条of_driver_match_device几乎就是唯一命中的路径。它做的事情非常纯粹把设备树节点上的compatible属性和驱动里of_device_id表中的compatible逐个比对。驱动侧的示例代码如下static const struct of_device_id led_of_match[] { { .compatible myvendor,led-demo }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, led_of_match); static struct platform_driver led_driver { .probe led_probe, .remove led_remove, .driver { .name my_gpio_led, .of_match_table led_of_match, }, };设备树节点里面是这么写的led_demo: led-demo { compatible myvendor,led-demo; led-gpios gpio1 4 GPIO_ACTIVE_LOW; status okay; };匹配时内核拿着设备树节点里的myvendor,led-demo去遍历led_of_match数组字符串一致就返回匹配成功。这里有几个开发中容易踩的细节compatible的命名规范标准格式是厂商名,器件型号厂商名必须不带资本化、尽量用公司域名风格例如fsl,imx6ull-uart。这个前缀在设备树规范里有明确要求如果不按规范写虽然内核不会报错但很难看而且会被上游维护者拒绝。数组必须以空结构体结尾上面代码里的/* sentinel */就是结束标记。如果没有这个哨兵项内核遍历会越界直接崩溃这是新手最容易犯的错误之一。MODULE_DEVICE_TABLE的作用它的宏展开后会把of_device_id表编译进模块的一个特殊section。这个表在编译时会生成modinfo信息用户空间的udev或udevadm根据设备uevent里的MODALIAS就能自动加载对应驱动模块。嵌入式里虽然经常手写insmod但这个宏习惯上一定会写上别省。2.3 id_table和名字匹配老平台的后备方案在没有设备树的年代或者某些老驱动逻辑中会使用platform_device_id表来匹配。这种场景下设备侧是通过板级文件board.c注册的platform_device设备名直接写在结构体里例如platform_device_register时指定name mydrv。驱动侧写法如下static const struct platform_device_id mydev_ids[] { { .name mydev-v1, .driver_data (kernel_ulong_t)v1_data }, { .name mydev-v2, .driver_data (kernel_ulong_t)v2_data }, { } }; MODULE_DEVICE_TABLE(platform, mydev_ids); static struct platform_driver mydev_driver { .probe mydev_probe, .remove mydev_remove, .driver { .name mydev, }, .id_table mydev_ids, };这种方式的匹配逻辑是遍历id_table里的每一个.name和pdev-name做字符串比较。命中后对应项的.driver_data会被内核保存下来驱动可以在probe里通过platform_get_device_id(pdev)拿回来根据不同的硬件版本做差异化初始化。至于第4条直接比较驱动名和设备名在设备树时代已经很少单独依赖了。它更像一个兜底如果驱动没配置of_match_table也没配置id_table那就比较drv-driver.name和pdev-name。这是最原始的匹配方式。这里要特别注意一点同时配置了of_match_table和id_table时设备树匹配永远先执行。也就是说如果设备树节点的compatible命中了内核根本不会去看id_table。所以你在SDK驱动里经常看到两个表同时存在compatible给设备树平台用id_table给老式板级文件平台用两者互不干扰。2.4 compatible匹配背后隐藏的“厂商前缀陷阱”很多人第一次写设备树和驱动会觉得compatible就是一对一的字符串对应没什么坑。但实际上compatible可以包含多个字符串比如某节点可能这样写compatible myvendor,led-demo, gpio-leds;内核匹配时按顺序依次尝试任何一个字符串与驱动的of_match_table命中即算成功。这种“多个compatible”的写法在i.MX6ULL非常常见因为很多外设驱动可能被多个兼容芯片复用。比如fsl,imx6ul-uart和fsl,imx6ull-uart通常会在同一个节点里出现。但这个机制也带来一个隐蔽问题如果你的驱动led_of_match[]里写的是myvendor,led-demo而设备树节点里compatible的第一个字符串是othervendor,led-demo第二个才是myvendor,led-demo此时依然能匹配成功因为内核会遍历所有compatible字符串。反过来如果设备树里只写了othervendor,led-demo而你的驱动表里只有myvendor,led-demo那就匹配不上probe不会执行。这种字符串匹配的“一族多兼容”逻辑在排查问题时迷惑性很强。我建议在开发自定义驱动时设备树和驱动里的compatible只保留一个尽量少写多兼容除非你真的需要兼容多个硬件版本。3. 设备侧管线设备树节点是如何变成platform_device的理解了匹配逻辑你可能会困惑另一个问题设备树里那些节点到底是什么时候、被谁变成内核里的platform_device的很多人以为设备树被内核解析后所有节点天然就是platform_device这个认知是错的。3.1 设备树节点与platform_device的区别设备树节点本质上是硬件描述信息它存储在dtb二进制文件中本身不是一个内核对象。内核启动时会把dtb解析成device_node结构形成一棵树。但device_node只是“设备描述”不是“设备实例”。要让设备模型管理它必须创建对应的struct device具体到Platform机制就是创建struct platform_device。谁来创建答案是内核的设备树填充机制of_platform。3.2 of_platform_populate的扫描过程在内核启动流程中start_kernel早期完成设备树展开之后在init_machine阶段会调用of_platform_default_populate_init这是一个arch_initcall_sync级别的调用它会扫描设备树根节点下的所有子节点凡是被判定为“平台设备”的节点都会被动态创建为platform_device。判定规则简单粗暴但很有效节点必须有compatible属性否则无法匹配驱动。节点不能是status disabled被禁用则不创建设备。节点的父节点如果已经被某类特殊总线驱动接管比如PCI、I2C、SPI、MDIO子节点不会作为platform_device创建而是作为对应总线的client设备。最后一条特别重要。举个例子i.MX6ULL的设备树里I2C控制器节点本身会被创建为platform_device但它下面的某个触摸屏子节点touchscreen1a就不会是platform_device而是由I2C核心创建为i2c_client。这也是驱动面试里经常问的一个细节。在i.MX6ULL的imx6ull.dtsi中你能看到大量外设节点uart1: serial02020000 { compatible fsl,imx6ul-uart, fsl,imx6q-uart; reg 0x02020000 0x4000; interrupts GIC_SPI 26 IRQ_TYPE_LEVEL_HIGH; clocks clks IMX6UL_CLK_UART1; ... };这个uart1节点就是典型的会在init阶段被自动转换为platform_device的节点。转换时reg属性会变成platform_device的resourceinterrupts会变成中断资源这样驱动在probe里就可以用platform_get_resource和platform_get_irq来获取硬件信息而不需要硬编码地址。3.3 验证设备有没有成功注册在开发板上可以用以下命令快速确认设备是否已经变成platform_devicels /sys/bus/platform/devices/ dmesg | grep platform如果设备被成功创建dmesg里通常会有类似这样的字样platform 20a0000.uart1: Fixed dependency cycle(s) with /soc/aips-bus02000000/spba-bus02000000或者直接去看sysfscat /sys/bus/platform/devices/20a0000.uart1/uevent会看到MODALIASof:NserialTserialCfsl,imx6ul-uart之类的字符串。这个字符串就是设备树匹配机制和用户空间模块自动加载之间的桥梁。有点绕但理解它对你排查驱动不生效问题很有帮助。我自己遇到过一种情况在设备树里加了一个外设节点编译烧写后ls /sys/bus/platform/devices/里死活找不到对应设备最后发现是status没有写okay默认被禁用了。排查这类问题第一反应就应该是去看设备有没有被创建出来而不是盯着驱动代码改来改去。4. 驱动侧管线platform_driver_register到probe的全链路设备侧搞清楚了再看驱动侧。为什么驱动模块一加载probe就自动被调用了这背后是一条清晰的中断调用链。理解这条链路比死记硬背“注册驱动要写probe”有价值得多。4.1 module_platform_driver宏的展开驱动侧通常用module_platform_driver来注册module_platform_driver(led_driver);这个宏展开后相当于static int __init led_driver_init(void) { return platform_driver_register(led_driver); } static void __exit led_driver_exit(void) { platform_driver_unregister(led_driver); } module_init(led_driver_init); module_exit(led_driver_exit);所以一切的核心就是platform_driver_register。4.2 从driver_register到driver_attach的接力platform_driver_register内部会把pdrv-driver.bus设为platform_bus_type然后调用driver_register。driver_register经历一系列初始化后最终会调用bus_add_driver它会做两件事把驱动加入到总线的驱动链表klist_add_tail(priv-knode_bus, bus-p-klist_drivers)。调用driver_attach(drv)去遍历总线上当前已经注册的所有设备为这个新驱动寻找匹配的设备。driver_attach内部调用的是bus_for_each_dev(drv-bus, NULL, drv, __driver_attach)也就是遍历platform总线设备链表上的每一个device对每个设备都执行__driver_attach。__driver_attach的逻辑可以用伪代码概括static int __driver_attach(struct device *dev, void *data) { struct device_driver *drv data; /* 如果设备已经有driver了跳过 */ if (dev-driver) return 0; /* 调用platform_match判断驱动与设备是否匹配 */ if (!driver_match_device(drv, dev)) return 0; /* 匹配成功建立设备与驱动的绑定关系 */ device_driver_attach(drv, dev); return 0; }可以这样理解driver_attach像是拿着新来的简历逐个问已经坐满的招聘摊位上的应聘者设备“你愿意跟这个HR走吗”。4.3 really_probeprobe真正被调用的地方匹配成功后流程来到driver_probe_device它内部会调用really_probe这是整个设备驱动模型中最核心的“临门一脚”。really_probe执行的关键步骤包括设置dev-driver drv标记设备与驱动的绑定关系。调用dev_pm_domain_attach和pinctrl_bind_pins处理电源管理域和引脚控制pinctrl。尝试调用dev-bus-probe对于platform总线来说这个函数是platform_drv_probe。platform_drv_probe内部最终调用驱动的pdrv-probe(pdev)也就是你在驱动里编写的probe函数本体。如果probe返回0设备进入“已绑定”状态同时会在sysfs中创建驱动与设备之间的符号链接。整条链路用文字描述是platform_driver_register - driver_register - bus_add_driver - driver_attach - bus_for_each_dev - __driver_attach - driver_match_device - platform_match - driver_probe_device - really_probe - platform_drv_probe - pdrv-probe(pdev)从头到尾没有任何一步需要你手动调用probe。它之所以能被自动触发是因为内核在总线层面实现了一个“驱动注册时扫描设备、设备注册时扫描驱动”的双向匹配机制。4.4 设备后注册的情况另一个方向的匹配看到这里你可能会问如果加载驱动时设备还没创建呢在设备树环境下platform_device通常在init_machine阶段就创建好了驱动是后来insmod的所以上面那条链路足够用。但也有反过来的情况比如设备通过platform_device_register动态注册而驱动早已存在。这种情况走的是另一条路。platform_device_register内部会调用device_add它里面有一个关键调用bus_probe_device会去该总线上已注册的驱动链表中寻找匹配项。也就是说设备注册时也会触发一次匹配扫描。这正是Linux设备模型设计巧妙的地方不管谁先谁后只要设备和驱动都注册到同一条总线上系统总会在“后到者”注册时主动跑一次匹配确保不错过。实际效果就是probe一定会在设备和驱动都就绪后的某个时间点被调用。4.5 probe里的常见失败模式很多新手以为匹配成功probe就必然成功其实不是。really_probe在调用probe返回非0值时会认为“匹配成功但绑定失败”然后做一些清理设备会被标记为探测失败。我在i.MX6ULL上写驱动时probe里最容易出的问题是platform_get_resource拿到错误的地址或拿不到中断号。ioremap失败地址被占用或无效。devm_gpiod_get或gpio_request失败GPIO被pinmux占用了。请求中断时使用了错误的中断号。这些错误不会导致匹配机制本身失败但会导致probe返回错误码。排查时dmesg里能看到驱动自己的dev_err打印以及内核的probe of LED_DEMO failed with error -22这类信息。所以当你说“我的probe不执行”时先分清楚是“压根没匹配上”还是“匹配上了没绑定成功”。这两类问题的排查方向完全不同下一章我会给一套标准排查手册。5. i.MX6ULL实战从零验证一次完整的设备与驱动匹配理论讲再多不如动手跑一遍。这一章我以一个自定义LED设备为例带你在i.MX6ULL开发板上完整验证Platform设备与驱动的匹配过程。环境假设你用NXP官方的Linux SDK或者正点原子/野火等常见板卡BSP交叉编译工具链已经就绪。5.1 第一步写一个带of_match_table的platform驱动先在开发目录里创建led_platform_demo.c内容如下精简了核心逻辑重点看匹配相关的部分#include linux/module.h #include linux/platform_device.h #include linux/of.h #include linux/gpio/consumer.h static int led_probe(struct platform_device *pdev) { struct device *dev pdev-dev; struct gpio_desc *desc; dev_info(dev, led_probe enter\n); desc devm_gpiod_get(dev, led, GPIOD_OUT_LOW); if (IS_ERR(desc)) { dev_err(dev, failed to get led gpio: %ld\n, PTR_ERR(desc)); return PTR_ERR(desc); } dev_info(dev, led probe success, gpio assigned\n); return 0; } static void led_remove(struct platform_device *pdev) { dev_info(pdev-dev, led_remove\n); } static const struct of_device_id led_of_match[] { { .compatible myvendor,led-demo }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, led_of_match); static struct platform_driver led_driver { .probe led_probe, .remove led_remove, .driver { .name led_demo, .of_match_table led_of_match, }, }; module_platform_driver(led_driver); MODULE_LICENSE(GPL); MODULE_DESCRIPTION(i.MX6ULL platform match demo);这段代码重点看两个地方of_device_id数组里compatible是myvendor,led-demo它有.of_match_table led_of_match。这两项缺一不可否则就是回到名字匹配的老路不推荐。5.2 第二步修改设备树并编译DTC在设备树源文件里i.MX6ULL通常在arch/arm/boot/dts/imx6ull-14x14-evk.dts或你的板级dts中追加也可以放到根节点/下做测试添加一个节点/ { led_demo { compatible myvendor,led-demo; led-gpios gpio1 4 GPIO_ACTIVE_LOW; status okay; }; };编译设备树make ARCHarm imx6ull-14x14-evk.dtb把生成的dtb文件和你编译好的内核zImage一起烧录到开发板。如果你使用NAND或SD卡启动要把dtb放到对应的启动分区。这里要特别强调修改设备树后必须确认新dtb确实被bootloader加载了。我见过太多人改了设备树没烧录或者烧错了分区导致“设备树改了跟没改一样”。排查这个问题最简单的方法是在板子上执行ls /proc/device-tree/ | grep led_demo如果能看到led_demo目录说明新的设备树已经生效。如果看不到多半是dtb没烧对后边所有probe排查都没意义。5.3 第三步编译模块并加载编译驱动模块需要先确保内核源码已配置并启用了模块支持make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- modules单独编译这个模块可以这样make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- M$(pwd) modules把生成的led_platform_demo.ko拷贝到开发板然后在板子上insmod led_platform_demo.ko此时立刻观察dmesg输出正常情况应该能看到led_demo led_demo: led_probe enter led_demo led_demo: led probe success, gpio assigned这说明驱动加载时经过platform_match通过设备树compatible匹配到了设备并成功调用了probe。5.4 第四步从sysfs验证匹配结果除了dmesgsysfs是验证“设备与驱动是否绑定”的最好途径。执行ls -l /sys/bus/platform/devices/led_demo ls -l /sys/bus/platform/drivers/led_demo/如果绑定成功你会在第一个命令看到多了一个指向driver的符号链接内容类似/sys/bus/platform/devices/led_demo/driver - ../../../../bus/platform/drivers/led_demo同时驱动目录下也会出现设备链接/sys/bus/platform/drivers/led_demo/led_demo - ../../../../devices/platform/led_demo这两个符号链接就是设备模型“绑定成功”的最直观证据。看/sys/bus/platform/devices/led_demo/uevent还能看到MODALIASof:Nled_demoT...Cmyvendor,led-demo证明设备树匹配生成的modalias字符串是符合预期格式的。5.5 手动解绑与重新绑定调试时经常需要让设备与驱动解绑以便重新测试probe流程。Platform总线提供了运行时控制接口echo led_demo /sys/bus/platform/drivers/led_demo/unbind echo led_demo /sys/bus/platform/drivers/led_demo/bind执行unbind后dmesg会输出led_remove再执行bind会再次看到led_probe enter。这组命令在验证remove逻辑、或者测试驱动热插拔逻辑时极其好用不用反复rmmod/insmod。5.6 匹配不上的常见根因排查手册最后我把自己在i.MX6ULL上排查“probe不执行”的完整套路整理成了一张表直接对照操作就行症状直接原因排查方法修复思路设备树里找不到节点dtb没烧对ls /proc/device-tree/重新编译并烧录dtb节点存在但没有platform_devicestatus为disabledcat /proc/device-tree/led_demo/status改为okay并重新编译dtb有设备但驱动不匹配compatible不一致比对节点compatible和of_match_table统一字符串注意厂商前缀有设备有驱动但probe没跑驱动没加载lsmod、dmesggrep myvendorprobe报了error绑定失败而非匹配失败dmesgtail看具体错误在最终排查时我个人的习惯是先用dmesg搜platform和led_demo两个关键字基本能覆盖80%的线索。然后再去看sysfs的设备目录是否存在、driver链接是否建立。这个顺序比一上来就打开驱动源码逐行读要高效得多因为大多数问题其实发生在设备树和dtb这一层而不是驱动代码本身。6. probe不是终点匹配成功后的那些“隐形机制”匹配机制把设备和驱动拉到一起probe执行也不代表万事大吉。在probe真正跑起来之前和之后内核还做了很多你可能察觉不到的工作。这些机制在i.MX6ULL上尤其重要因为片上外设依赖的pinctrl、时钟、中断资源都跟它们相关。6.1 pinctrl在probe之前的自动绑定先看一个在i.MX6ULL上绕不开的东西引脚控制pinctrl。设备树节点里经常有类似下面的属性pinctrl-names default; pinctrl-0 pinctrl_led;pinctrl核心在really_probe阶段、probe函数运行之前会根据pinctrl-names里的名字去设备树里找对应的pinctrl-0、pinctrl-1等属性然后调用pinctrl_select_state把引脚切换到指定状态。这意味着你在probe里用devm_gpiod_get获取GPIO时引脚其实已经被设置好了。这也就是为什么很多i.MX6ULL的驱动probe里看不到任何pinmux配置代码它已经被框架自动处理了。如果设备树的pinctrl配置有误probe之前就可能失败。dmesg里会出现类似failed to get default pinctrl state的警告但很多时候它不会阻断probe只是引脚复用不对硬件功能异常。排查外设不工作时一定要记得检查pinctrl配置。6.2 设备树资源与platform_get_resource匹配成功并进入probe后驱动第一件事通常是获取硬件资源。这些资源就是设备树节点里的reg、interrupts属性内核在创建platform_device时已经把它们转换成了struct resource数组。在probe里这样获取struct resource *res; void __iomem *base; res platform_get_resource(pdev, IORESOURCE_MEM, 0); if (!res) return -ENXIO; base devm_ioremap_resource(dev, res); if (IS_ERR(base)) return PTR_ERR(base);这里有一个常见误区很多新手直接ioremap(0x02020000, 0x4000)硬编码地址。这在裸机开发没问题但在Linux驱动里是反模式因为设备树已经描述了资源硬编码会破坏可移植性。用platform_get_resource才能确保你的驱动在不同设备树配置下都能正确工作。顺便补充一个i.MX6ULL的细节它的外设地址空间都在0x02000000到0x02100000附近但ioremap后的虚拟地址是不固定的不要试图打印虚拟地址去和芯片手册里的物理地址对应它们是两码事。6.3 probe之后的生命周期remove与 shutdown匹配成功后设备与驱动建立了绑定关系但这个关系不是永久的。系统关机、设备卸载、驱动rmmod时内核会依次调用驱动的shutdown、remove等回调。在really_probe里remove对应关系会被记录。driver_unregister时所有绑定在该驱动上的设备都会触发解绑流程逐个调用remove。这也是为什么驱动框架要求在remove里释放所有资源、注销设备节点。我在i.MX6ULL上调试LED驱动时就遇到过一个典型问题remove里没有释放request_irq申请的中断导致rmmod后再次insmod时中断号被占用probe直接失败。后来改用devm_系列接口如devm_request_irq问题彻底消失。devm_系列API是设备管理资源机制它会在probe失败或设备移除时自动释放资源是现代Linux驱动的标配。写新驱动时能用devm_就用devm_能大大减少资源泄漏的坑。6.4 同一外设多驱动叠加的情况还有最后一个值得专门提的场景i.MX6ULL上经常出现一个硬件设备被多个驱动“层层接管”的情况。最典型的是GPIO子系统和GPIO-LED子系统。比如设备树里一个compatible gpio-leds的节点它会被LED子系统注册的platform驱动匹配。但这个驱动操作GPIO时真正干活的其实是底层GPIO控制器驱动compatible fsl,imx6ul-gpio。也就是说一个物理LED背后可能涉及两层platform驱动LED层和GPIO层。这就是设备模型中“设备叠加”的核心思想一个platform_device的probe里可能去请求另一个platform_device提供的能力比如通过gpiod_get请求GPIO而这个GPIO正是另一个platform驱动在管理。理解这套嵌套关系你在排查“为什么GPIO点不亮”时就不会只盯着应用层驱动看了还会去检查底层GPIO控制器驱动是否正常工作。在i.MX6ULL开发过程中我最后悔没早点做的事就是认真阅读/sys/bus/platform/devices下的完整设备列表。那里不是一堆乱码路径它真实反映了内核里所有platform设备的注册情况、资源信息、绑定状态。配合设备树源码基本能把整个SoC的外设框架摸透。Platform设备与驱动匹配机制其实并不神秘。它无非就是一条虚拟总线、四步匹配逻辑、两个方向触发的扫描机制。但正是这套机制把硬件描述设备树、软件逻辑驱动、内核对象platform设备三者彻底解耦让嵌入式Linux驱动开发变成了一件可拼接、可复用的工程活。你在i.MX6ULL上学到的这套Platform匹配流程换到任何一款现代SoC上思路依然成立这才是这篇文章最想让你带走的。
返回列表