ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

基于i.MX6ULL的Platform总线机制解析:设备树匹配与驱动编写实战

基于i.MX6ULL的Platform总线机制解析:设备树匹配与驱动编写实战 我刚开始在 i.MX6ULL 上写 Linux 驱动的时候最摸不着头脑的就是 Platform 这套机制。明明直接操作寄存器也能点灯、读按键为什么 Linux 非要搞出一个虚拟总线把设备和驱动拆成两个独立角色再定一套所谓的“匹配规则”后来被 probe 不进、匹配失败这些问题折磨了几次才慢慢体会到这层设计真正解决的问题它让驱动代码和具体硬件解耦让同一份代码可以在不同板卡上稳定运行。这篇就围绕 i.MX6ULL 实际开发场景把 Platform 设备与驱动的匹配机制讲透从原理到实操再结合我踩过的坑让你看完能自己写、能排查不再对着 demsg 发怵。1. 先搞懂 Platform 总线的来龙去脉1.1 传统驱动模型的问题直接注册驱动的尴尬在没有 Platform 机制之前写一个简单的驱动大家习惯的做法是直接调register_driver比如register_chrdev或者platform_driver_register之前更原始的模型里驱动代码会把自己直接注册进内核然后靠一个名字去“碰”设备。这种方式在硬件设备固定、数量稀少的年代还能凑合但一旦到了 i.MX6ULL 这种外设资源丰富的 SoC 上问题立刻暴露资源分配不透明驱动代码里直接写死中断号、IO 地址、时钟频率。换一个板子同样的外设挂到了不同引脚上就得改代码重新编译。设备和驱动耦合严重没有统一的注册中心驱动不知道设备什么时候ready设备也不知道哪个驱动可以服务自己。系统启动时只能靠加载顺序来保证运气。热插拔和运行时管理难做USB、SD 卡这类可插拔设备需要一套动态的通知机制而不是靠“写在代码里的顺序”。所以 Linux 引入了 device、driver、bus 三个核心概念。设备是硬件实体驱动是操作逻辑总线负责牵线搭桥。而 Platform 总线就是为那些不挂在 PCI、USB、I2C、SPI 等真实物理总线上的设备准备的一条虚拟总线。1.2 Platform 总线的定位CPU 内部外设的“收纳箱”i.MX6ULL 的 GPIO 控制器、UART、ECSPI、I2C、SDMA、LCD 控制器等这些东西内部都挂在 SoC 的内部总线上如 AXI、AHB、APB它们没有 PCI 枚举、没有 USB 热插拔但却是实实在在的硬件设备。Platform 机制做的事情把每个这样的内核外设抽象成platform_device把每个外设对应的驱动程序抽象成platform_driver然后挂在 platform_bus_type 这条虚拟总线上。设备侧描述“我有什么”驱动侧声明“我能干什么”总线负责匹配。配套的数据结构如下struct platform_device { const char *name; // 设备名传统匹配时用 int id; struct device dev; // 通用设备结构体内嵌 u32 num_resources; struct resource *resource; // IO/中断资源关键 const struct platform_device_id *id_entry; // 匹配成功后的ID表项 }; struct platform_driver { int (*probe)(struct platform_device *); int (*remove)(struct platform_device *); void (*shutdown)(struct platform_device *); int (*suspend)(struct platform_device *, pm_message_t state); int (*resume)(struct platform_device *); struct device_driver driver; // 内嵌通用驱动结构体 const struct platform_device_id *id_table; };这里有个关键点真正驱动操作的核心数据不是 dev 节点本身而是resource数组。里面存放了寄存器物理地址范围、中断号这些关键信息。设备侧通过设备树节点提供这些资源驱动侧通过platform_get_resource和platform_get_irq获取这样驱动代码里就不需要写死任何具体地址了。1.3 设备树在其中扮演的角色在 i.MX6ULL 这种现代 ARM 平台上设备树的地位几乎是决定性的。简单说设备树就是“用文本描述板级硬件布局”的配置文件。它会把每一个外设的信息组织成节点节点里写清楚ecspi1 { pinctrl-0 pinctrl_ecspi1; status okay; /* ... */ };设备树节点最终会被内核解析在启动时生成一个个platform_device挂到总线上。这个过程不需要重新编译内核只需要修改 dts 文件重新编译 devicetree blob 即可。这对项目开发效率的提升是巨大的硬件改版后很多时候只需要改 dts不用动 C 代码。2. 设备与驱动是怎么“对上眼”的匹配机制逐层拆解2.1 匹配过程总览bus_type 的回调函数Platform 总线能实现自动匹配核心是 platform_bus_type 这个结构体里注册了匹配函数struct bus_type platform_bus_type { .name platform, .dev_groups platform_dev_groups, .match platform_match, .uevent platform_uevent, .probe platform_drv_probe, .remove platform_drv_remove, .shutdown platform_drv_shutdown, };每当系统里添加一个新的 platform_device 或者 platform_driver总线核心就会调用.match回调也就是platform_match函数。它的逻辑就是一套条件判断按优先级从高到低看当前位置的设备是否匹配新的驱动或当前位置的驱动是否匹配新插上的设备。2.2 设备树匹配现代 ARM Linux 的主角设备树匹配是当前 i.MX6ULL 驱动匹配的核心方式。驱动侧需要定义一个of_device_id数组static const struct of_device_id my_led_of_match[] { { .compatible myvendor,myled, }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, my_led_of_match);然后把这个表挂到 platform_driver 的driver.of_match_table上static struct platform_driver my_led_driver { .probe my_led_probe, .remove my_led_remove, .driver { .name my_led, .of_match_table my_led_of_match, }, };设备侧在 dts 里写myled { compatible myvendor,myled; reg 0x020C406C 0x4; interrupt-parent gpio1; interrupts 18 IRQ_TYPE_EDGE_FALLING; };匹配时内核会比较设备节点的 compatible 属性值和 of_match_table 中每个条目里的 compatible 字段或者类型、姓名等只要有一个相同就匹配成功。注意设备的 compatible 如果包含多个字符串比如compatible myvendor,myled, fsl,imx6ull-gpio;那么匹配时按顺序依次与驱动表对比只要驱动表里有任意一个相同即可。这块有个新手很容易搞混的坑of_match_table 的匹配不看驱动名只看 compatible 字符串。也就是说如果 dts 里 compatible 写成了myvendor,myled-v2而驱动表里是myvendor,myled即使驱动名一致也不会触发 probe。2.3 ID 表匹配老派但依然实用的方式在设备树还没普及的年代platform 设备多用platform_device_id表来匹配。这种方式就是简单的字符串比较。驱动侧定义static struct platform_device_id my_led_id_table[] { { my_led, (kernel_ulong_t)my_led_data }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(platform, my_led_id_table);设备侧如果不用设备树而是直接在平台代码里注册platform_device_register并设置platform_device.name my_led那么匹配时两条路都能走总线匹配时会扫描platform_driver.id_table逐个与设备的name字段或设备树节点的device_node.name比较。如果匹配成功pdev-id_entry会指向命中的表项驱动里可以拿id_entry-driver_data作为设备差异化的数据。即便现在主流开发都是设备树ID 表方式仍并未淘汰。比如在一些快速原型、无设备树场景、或兼容旧平台时它依然高效。还有一个常见场景dts 节点里会有compatible但同时也会有节点名比如myled20c406c某些老内核版本的逻辑里如果 of_match 失败会退回去匹配节点名即device_node.name这个行为在内核版本演进中有所收敛但道理上要明白。2.4 ACPI 匹配与名称匹配边缘但要知道在 x86/ARM64 服务器等 ACPI 平台驱动可以通过acpi_driver_match_device来匹配 ACPI 表条目。i.MX6ULL 主打低功耗工业控制几乎没有 ACPI 的场景这部分可以略过但如果你日后移植驱动到其他平台需要了解内核会再调用platform_match_id去尝试匹配pdev-name和pdrv-id_table。最后如果还是没有匹配就会尝试比较pdev-name和pdrv-driver.name。这里要注意platform_match内部对名称匹配的判断还有一层逻辑不是简单“相等就行”它会检查(pdrv-id_table)是否为 NULL。换句话说如果驱动没有提供 id_table则直接允许pdev-name与pdrv-driver.name相等时匹配如果提供了 id_table则必须要先过 id_table再退回名称匹配。2.5 匹配优先级与触发时机内核源码中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. 设备树匹配 */ if (of_driver_match_device(dev, drv)) return 1; /* 2. ACPI匹配 */ if (acpi_driver_match_device(dev, drv)) return 1; /* 3. ID表匹配 */ if (pdrv-id_table) if (platform_match_id(pdrv-id_table, pdev) ! NULL) return 1; /* 4. 名称匹配 */ if (strcmp(pdev-name, drv-name) 0) return 1; return 0; }所以匹配优先级是of_match ACPI id_table name。这这个顺序决定了你在排查问题时应该优先检查哪一环。匹配触发的时机在设备注册时总线会遍历总线上的驱动列表逐一调用 match。在驱动注册时总线会遍历总线上的设备列表逐一调用 match。在 device 的uevent 事件触发时如 udev 管理也会执行匹配过程。这个“设备端主动、驱动端主动都可以”的双向注册模型是设备模型设计的精髓。驱动编写者最关心的时机就是驱动insmod进去后如果设备树里已经有对应设备节点应该立刻触发 probe。实际如果没触发的往往是 dts 里的节点没有被解析或者 compatible 不一致或者 status 为 disabled。3. 基于 i.MX6ULL 的 Platform 驱动实操3.1 硬件准备与核心思路我们用 i.MX6ULL 官方评估板做实验目标是从零写一个 Platform 驱动的 LED 驱动挂载在设备树下probe里获取 GPIO 控制寄存器地址然后通过操作寄存器点亮 LED。同时驱动里注册一个 misc 设备让 app 可以通过 write 来控制灯的开关。硬件信息i.MX6ULL SoCCortex-A7 内核。LED 接到 GPIO1_IO09这个在不同板卡上引脚不同以自己板子为准。我们这里用作示例。GPIO1 的基地址是 0x0209C000通过设备树给出。对应的 GPIO direction 寄存器偏移是 0x400DR数据寄存器偏移是 0x000。注意不同芯片的 GPIO 寄存器布局不同比如 imx6ull 的 GPIO1 有 0x000 (DR)、0x004 (GDIR)、0x008 (PSR)、0x00C (ICR1) 等。读芯片参考手册一定要对照寄存器偏移不能凭记忆硬写。我们的目标不是做一个完美的生产级驱动而是把 Platform 机制的每个环节走通同时搞清楚“为什么这么写”。3.2 设备树节点编写在 imx6ull 的 dts 文件里找到根节点添加自己的设备节点/ { myled { compatible myvendor,myled; reg 0x0209C000 0x1000; interrupt-parent gpio1; interrupts 9 IRQ_TYPE_LEVEL_LOW; status okay; }; };这里我们指定设备树节点物理地址为 0x0209C000长度 0x1000包含了 GPIO1 整个寄存器块。中断这里先预留实际 LED 控制没用到但驱动里可以演示platform_get_irq怎么拿中断号。说明几个重点compatible 必须用厂商,型号这种格式避免与其他平台的 compatible 冲突。直接写单个单词也可以但最好养成带厂商前缀的习惯。如果你只让 GPIO 作为输出点灯可以不配 interrupt这里只是为了演示。status 字段可选 “okay” / “disabled”。如果忘了写默认节点是启用状态如果写成了 “disabled”即使 compatible 匹配成功设备也不会被创建。reg 地址要根据实际芯片手册填写。i.MX6ULL 每个 GPIO bank 基址不同GPIO1 是 0x0209C000GPIO2 是 0x020A0000GPIO5 是 0x020AC000细则一定要查手册。设备树编译make dtbs将生成的 dtb 放到 SD 卡 boot 分区或者通过 tftp 加载。3.3 驱动端完整代码解析下面的驱动代码是我在一台 i.MX6ULL 板子上实测过的简化版 LED 驱动。为了便于演示 Platform 机制故意保留了 probe / remove 的标准流程。#include linux/module.h #include linux/platform_device.h #include linux/of.h #include linux/of_address.h #include linux/miscdevice.h #include linux/fs.h #include linux/uaccess.h #include linux/io.h #include linux/interrupt.h #define LED_OFF 0 #define LED_ON 1 static void __iomem *gpio1_base; static int led_gpio 9; /* 默认使用 GPIO1_IO09 */ static void led_set(int status) { u32 val; if (status LED_ON) { val readl(gpio1_base 0x000); val | (1 led_gpio); writel(val, gpio1_base 0x000); } else { val readl(gpio1_base 0x000); val ~(1 led_gpio); writel(val, gpio1_base 0x000); } } static long led_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) { switch (cmd) { case LED_ON: led_set(LED_ON); break; case LED_OFF: led_set(LED_OFF); break; default: return -EINVAL; } return 0; } static struct file_operations led_fops { .owner THIS_MODULE, .unlocked_ioctl led_ioctl, }; static struct miscdevice led_miscdev { .minor MISC_DYNAMIC_MINOR, .name myled, .fops led_fops, }; static int my_led_probe(struct platform_device *pdev) { struct resource *res; int ret, irq; dev_info(pdev-dev, my_led_probe enter\n); /* 获取设备树 reg 属性里的寄存器资源 */ res platform_get_resource(pdev, IORESOURCE_MEM, 0); if (!res) { dev_err(pdev-dev, failed to get memory resource\n); return -ENXIO; } gpio1_base devm_ioremap_resource(pdev-dev, res); if (IS_ERR(gpio1_base)) { dev_err(pdev-dev, failed to ioremap\n); return PTR_ERR(gpio1_base); } /* 设置 GPIO 方向寄存器把 GPIO1_IO09 设为输出 */ u32 gdir_val readl(gpio1_base 0x004); gdir_val | (1 led_gpio); writel(gdir_val, gpio1_base 0x004); /* 获取中断资源演示如果设备树有定义 */ irq platform_get_irq(pdev, 0); if (irq 0) dev_info(pdev-dev, got irq: %d\n, irq); /* 注册 misc 设备 */ ret misc_register(led_miscdev); if (ret) { dev_err(pdev-dev, failed to register misc device\n); return ret; } dev_info(pdev-dev, led initialized at %px\n, gpio1_base); /* 默认点亮 */ led_set(LED_ON); return 0; } static int my_led_remove(struct platform_device *pdev) { misc_deregister(led_miscdev); led_set(LED_OFF); dev_info(pdev-dev, led removed\n); return 0; } static const struct of_device_id my_led_of_match[] { { .compatible myvendor,myled, }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, my_led_of_match); static struct platform_driver my_led_driver { .probe my_led_probe, .remove my_led_remove, .driver { .name my_led, .of_match_table my_led_of_match, }, }; module_platform_driver(my_led_driver); MODULE_LICENSE(GPL); MODULE_AUTHOR(Your Name); MODULE_DESCRIPTION(i.MX6ULL platform led driver);解析一下关键点module_platform_driver(my_led_driver);是一行宏等价于module_init(platform_driver_register)和module_exit(platform_driver_unregister)。它把 platform_driver 注册进内核并且初始化好 prob/remove 的回调时机。platform_get_resource返回的是resource结构指针里面包含了从设备树解析出的寄存器物理地址范围。devm_ioremap_resource会做三件事检查资源是否合法、做 ioremap 映射到虚拟地址空间、注册 devm 自动释放机制。这样做的好处是即使 probe 后续返回错误内核也会自动释放已经映射的地址不用手动 iounmap。irq platform_get_irq(pdev, 0);获取第一个中断资源。这个方式在需要中断的驱动里非常常用拿到的是一个虚拟中断号后续可以request_irq连接中断处理函数。modprobe 后dmesg能看到my_led_probe enter和led initialized说明匹配成功。这里我故意没有写复杂的 pinctrl 配置实际工程中还需要设置 pinmux否则 GPIO 功能可能不对。pinctrl 一般也在 dts 里通过pinctrl-0属性声明驱动里只需要在 probe 里调用pinctrl_select_state或依赖内核自动配置即可。不过控制 GPIO 的方向和电平仍需要访问 GPIO 寄存器。3.4 编译与加载验证驱动编译建议用内核树外模块方式export ARCHarm export CROSS_COMPILEarm-linux-gnueabihf- make -C /path/to/linux-kernel M$PWD modules你会得到my_led_driver.ko。把.ko拷到板子上执行insmod my_led_driver.ko如果 dts 已经加载成功dmesg会看到my_led_probe enter led initialized at (ptrval)如果没有任何 probe 输出第一件事查/sys/bus/platform/devices/下有没有myled节点ls /sys/bus/platform/devices/或者直接查看设备树是否被正确解析ls /proc/device-tree/myled/ cat /proc/device-tree/myled/compatible如果/proc/device-tree/myled/存在说明设备树节点被解析了如果设备节点存在但 probe 没有调用再去检查驱动匹配表是否一致。内核里还有一个很直观的目录/sys/bus/platform/drivers/my_led/里面会列出当前驱动绑定了哪些设备。绑定的设备会以符号链接形式出现在driver子目录下加上一个叫unbind的文件你可以用echo触发重新绑定测试这个在调试中非常好用echo myled /sys/bus/platform/drivers/my_led/unbind echo myled /sys/bus/platform/drivers/my_led/bind这样无需反复 insmod/rmmod就能测试驱动的搬移与解绑逻辑在开发后期特别省时间。3.5 验证 GPIO 输出led_misc 设备注册成功后会在/dev/myled出现。写一个小 app#include stdio.h #include fcntl.h #include sys/ioctl.h #include unistd.h #define LED_ON 1 #define LED_OFF 0 int main(int argc, char **argv) { int fd; int cmd; if (argc ! 2) return -1; fd open(/dev/myled, O_RDWR); if (fd 0) { perror(open); return -1; } cmd strcmp(argv[1], on) 0 ? LED_ON : LED_OFF; ioctl(fd, cmd, 0); close(fd); return 0; }编译后运行./test_led onLED 亮./test_led offLED 灭。这里走的是完整的应用—驱动—硬件链路app 通过 ioctl 进入内核调用 misc fops 里的led_ioctl最终通过 ioremap 后的虚拟地址操作物理寄存器。4. 匹配失败排查与常见问题速查4.1 现象insmod 后 probe 不执行这是大家问的最多的。排查思路按优先级来第一步确认设备树节点是否被解析到。执行ls /proc/device-tree/ | grep my没有输出则说明设备树里没这个节点或没编译进 dtb。重新检查 dts 拼写和编译过程。第二步确认状态是否为 okay。如果在设备树里写了status disabled内核不会注册该 platform 设备。此外如果节点挂在某个挂载状态为 disabled 的总线下即使自己写了 okay父节点 disabled 也会导致不生效。第三步确认 compatible 字符串完全一致。这里最容易踩的坑compatible里不能有多余空格、大小写要一致、不能用单引号。可以对比/proc/device-tree/myled/compatible的输出注意其中的字符串是以\0结尾的有的工具 cat 会显示正常字符串有的会显示带乱码不用惊慌。第四步加载驱动后检查ls /sys/bus/platform/drivers/my_led/。如果驱动注册成功但没绑定设备目录下只有bind、uevent、unbind等公共文件没有myled符号链接说明匹配失败。此时dmesg通常不会有任何错误输出因为匹配失败是“静默”的。常见的原因还有一个驱动编译进内核与模块方式不统一。如果你把驱动直接编进内核obj-y但 dts 里 compatible 不匹配也会导致设备不 probe。内核里没有设备节点时platform_driver 注册时也是不会触发任何报错的。4.2 现象匹配到了但 probe 打印不到如果绑定成功probe 函数被调用但没看到第一个dev_info输出则要查驱动模块本身是不是有问题。比如符号未解析、misc_register失败导致早期返回。可以在 probe 入口第一行就加 printk这样能最快确定内核是否进入了 probe。不过更常用的做法是看lsmod里的模块“Used by”计数如果模块被绑定到一个设备上即使应用层没打开设备模块计数不会为0。4.3 现象设备树 reg 资源解析不出来platform_get_resource: failed to get memory resource这时检查 dts 里 reg 属性是否写对。比如reg 0x0209C000 0x1000;注意这里是两个 cell地址和长度。如果用devm_ioremap_resource拿不到资源多半是因为#address-cells和#size-cells配置问题导致内核解析 reg 时把地址长度算错。根节点的#address-cells通常是 1#size-cells通常是 1如果你的平台用的是 64 位地址如 ARM64就需要在节点中再定义#address-cells 2等。i.MX6ULL 是 32 位 ARM一般不会踩这个坑但如果你的 dtb 是从别的平台改过来的要留意。4.4 现象中断号获取失败platform_get_irq(pdev, 0)返回负值或 0原因一般是设备树节点里没写interrupts属性。interrupt-parent写错或者指向了不存在的 interrupt controller。中断类型定义错误比如IRQ_TYPE_LEVEL_LOW写成了IRQ_TYPE_LEVEL_HIGH用错符号不会导致获取失败但可能在使用时出问题。如果 GPIO 中断一定确认interrupt-parent gpio1和gpio1这个 phandle 已经定义且能正常工作。获取不到中断时可以先用cat /proc/interrupts看下系统里实际注册的中断确认 GPIO bank 的 base irq 是多少。i.MX6ULL 上 GPIO1 的中断号一般在 32 左右但不要写死用设备树传才是正道。4.5 常见问题速查表现象可能原因排查/解决办法probe 不调用compatible 不匹配对照 /proc/device-tree/myled/compatible 与驱动 of_match_tableprobe 不调用status 为 disableddts 里改为 okay 或删除probe 不调用设备树节点未解析ls /proc/device-tree/ 确认节点存在probe 报 -ENXIOreg 资源未找到检查 reg 与 address-cells/size-cellsioremap 失败资源被其他驱动占用看 dmesg 有无 conflict 提示中断号获取失败interrupts 属性缺失补充 interrupt-parent 与 interrupts设备绑定后无法打开misc 注册失败检查 /dev/myled 是否存在rmmod 后系统崩溃资源释放不干净用 devm 系列函数自动管理4.6 一个容易被忽视的细节MODULE_DEVICE_TABLE调试中还有一个非常隐蔽的坑MODULE_DEVICE_TABLE(of, my_led_of_match)。对于内核模块这个宏的作用是把 of_match_table 里的信息导出到模块的.modinfo段里供热插拔工具如 udev、mdev、systemd-modload使用。如果你写的是模块并且想让系统插入设备时自动加载驱动这个宏必须写。否则即使你的驱动代码在板子上运行时可用依赖 hotplug 自动加载时也不会生效。此外这个宏对 of_match 本身不会影响匹配结果即使漏写只要手动insmod依然能正常匹配并 probe。因为真正匹配时用的是driver.of_match_table指针而不是 modinfo。这个知识对理解匹配机制非常重要MODULE_DEVICE_TABLE 是给用户态工具看的of_match_table 是给内核看的。5. 从匹配到 probe解码一键绑定的内幕5.1 platform_drv_probe 到底做了什么设备与驱动匹配成功后总线会调用platform_drv_probe这个函数最终会调用platform_driver.probe。但实际上在调用我们写的 probe 之前内核会做一个关键动作将 device 与 driver 关联。也就是说dev-driver会被赋值sysfs里会出现driver符号链接然后才是执行我们注册的 probe 回调。在这个阶段内核会先保证设备依赖的时钟、电源、pinctrl 等资源处于正常状态。这就是为什么有些驱动不需要显式操作时钟内核设备模型会自动为 device 关联它们。5.2 probe 正确的打开方式资源获取顺序我自己的习惯是probe 里严格按下面顺序做从设备树获取硬件资源platform_get_resource、platform_get_irq。申请并映射寄存器地址devm_ioremap_resource、devm_ioremap。检查中断有效性注册中断处理函数request_irq / devm_request_irq。初始化内核对象misc_register、cdev_add、注册输入子系统、注册 netdev 等。最后再开始操作硬件开中断、使能设备、点亮 LED。这样做的好处是如果中途出错可以快速返回错误码且不会留下半初始化的状态。devm_ 系函数配合返回错误码可以做到“出错即自动清理”大大减少内存泄漏和资源泄漏的可能。5.3 一个进阶点设备树资源的实时性设备树在系统启动时被解析成 device_node 后驱动内读取的都是一次性的“快照”。也就是说如果你在驱动加载后手动修改/proc/device-tree/里的内容内核不会动态感知。修改设备树需要重新加载 dtb 并重启板子。但/sys/bus/platform/drivers/下的bind/unbind操作是实时的可以动态让一个设备与驱动脱钩或重新绑定。利用这个特性我在调试驱动经常这么干# 模拟拔插设备 echo myled /sys/bus/platform/drivers/my_led/unbind echo myled /sys/bus/platform/drivers/my_led/bind这样反复测试remove和probe排查资源泄漏的效率比反复 reboot 高得多。6. 扩展与个人经验谈写 Platform 驱动的三个习惯6.1 先写 dts再写驱动很多人习惯先写驱动代码再补 dts结果经常出现“驱动写完了设备树不会加”的尴尬。我更推荐反向操作先把设备树节点梳理清楚明确需要哪些寄存器、中断、时钟、pinctrl然后驱动代码里依据设备树写资源获取逻辑。这样驱动的可移植性天然就好代码里不会出现硬编码地址。在 i.MX6ULL 里另一个重要原因是很多外设的 base address、中断号、时钟号都在芯片手册里但你不用全部写在驱动里。设备树就是那一份“硬件描述清单”驱动只是“清单的使用者”。如果你驱动里还要写#define GPIO1_BASE 0x0209C000说明设计已经退回到传统模式了。6.2 打印多一点但要有节制调试驱动时dev_info和dev_dbg非常有用。dev_info会无条件输出取决于 printk 级别适合 probe/remove 这些关键路径dev_dbg则依赖dynamic debug机制适合在后期望关闭日志时使用。在内核开发期间我习惯在 probe 入口、拿到资源后、注册成功后各打一行。这样 dmesg 就能完整还原整个初始化过程。上线前再把不必要的dev_info去掉或改成dev_dbg。6.3 善用 /sys 文件系统Platform 驱动调式过程/sys/bus/platform/devices/和/sys/bus/platform/drivers/是除了 dmesg 之外最大的金矿。有时你不想看内核日志直接ls /sys/bus/platform/devices/myled/driver就能看到它当前绑定的驱动名。或者cat /sys/bus/platform/devices/myled/uevent可以看到设备名和 modalias 等信息。这些信息在热插拔环境、udev 规则编写中也大有用处。我实际踩过多次的坑是设备树生成了 platform device但是因为of_match_table里 compatible 写的是全小写加厂商前缀dts 里却用了驼峰或下划线导致匹配失效。这类错误编译器不会管你内核也不会报告只有自己逐一比对才能发现。好在/proc/device-tree/myled/compatible里面会原样输出设备树字符串拿它跟驱动源码里的 compatible 字符串直接对比问题一目了然。还有一次我把设备节点挂在了某个 disabled 的总线节点下面结果外面看节点存在但总线上就是没有对应的 device。后来才发现是父节点status disabled导致整个子树都被跳过。这个现象非常有迷惑性因为/proc/device-tree下整棵树都完整展示但如果父节点未 enable内核不会为子节点生成 platform device。如果你遇到“节点在设备不在”的情况先看父系节点的状态。最后再分享一个实际经验如果你的设备树中有多个 compatible 字符串比如compatible myvendor,myled, myvendor,generic-led;驱动的 of_match_table 只需要命中其中一个即可。但如果你将来想向 u-boot 或者内核传递不同模式可以通过不同 compatible 顺序来决定匹配优先级这也是一种“板级差异化”的巧妙手段。例如有两个驱动都支持同一设备最终谁 probe 取决于谁的注册顺序和设备遍历顺序但因为有 of_match_table 优先级你可以通过控制 compatible 顺序来稳定地选中期望驱动。这个项目做完之后我最大的体会是Platform 机制不是玄学它就是设备模型中一条清晰的生产线。设备侧提交原料资源描述驱动侧提交加工能力操作逻辑总线负责配对。你只要能沿着 dts - device - match - driver - probe 这条链路逐段排查任何匹配问题终究都能定位。而且这套机制在 i.MX6ULL 之外的几乎所有嵌入式 Linux 平台上都通用。搞懂了这一次以后换任何 SoC核心思路都还是那套资源描述、匹配、probe、资源管理。
返回列表