ARTICLE DETAIL

资讯详情

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

Linux Platform设备驱动完全解析:以i.MX6ULL为例,搞懂设备树与匹配机制

Linux Platform设备驱动完全解析:以i.MX6ULL为例,搞懂设备树与匹配机制 我搞Linux驱动开发这些年有一个感受越来越深很多初学者在字符设备驱动上练得挺熟一碰到Platform框架就懵了。明明struct file_operations那一套已经会了可为什么工业级的驱动代码里见不到直接注册字符设备的影子反而到处都是platform_driver、platform_device这两个结构体其实不是你基础差而是没搞懂Linux内核为什么要引入Platform这套设备与驱动分离的设计。这篇文章我打算拿i.MX6ULL这颗NXP的Cortex-A7处理器当例子把Platform设备的注册、驱动编写、以及设备树和匹配机制这整条链路从头到尾捋一遍。文章适合刚学完字符设备驱动、准备向真正嵌入式Linux驱动开发进阶的读者也适合那些想把设备树、总线、驱动模型一次弄明白的人。先说一个特别重要的认知Platform不是一个物理上的硬件总线它是Linux内核在软件层面虚拟出来的一条总线。为什么需要这种东西原因很简单在真正的SoC片上系统里有大量设备——比如I2C控制器、SPI控制器、GPIO控制器、DMA控制器——它们虽然挂在芯片内部的不同地址上却不像PCI设备那样有标准的枚举机制。在内核眼里这些设备不知道自己在哪也没法通过硬件协议去探测。Platform总线就是给这些设备一个虚拟挂载点让设备device和驱动driver能够在这个软件层上相亲匹配上了就执行驱动的probe函数。这一套机制你要是搞明白了再看i.MX6ULL上任何一个外设驱动都会觉得豁然开朗。1. 为什么Linux要引入Platform机制1.1 从硬编码注册说起没有Platform时驱动长啥样回到好几年前的内核版本或者在很多教学代码里驱动注册的套路是这样的写一个init函数在里面调register_chrdev()申请设备号、class_create()创建设备类、device_create()创建设备节点一气呵成。这种方法写出来的驱动有个致命问题——它和具体的硬件地址、中断号、时钟配置死死绑定在一起。今天这块板子LED接在GPIO1_IO03上明天换一块板子LED接到了GPIO1_IO07上你得重新编译驱动。如果SoC从i.MX6ULL换成了i.MX8M Mini那更是要推翻重写。这种写法在单片机和裸机开发里没问题因为整个程序就是为一块板子服务的。但Linux是通用操作系统内核要面对的是千千万万种硬件组合。内核开发者希望实现驱动的代码与硬件资源分离——驱动只负责描述我该怎么操作某类设备而具体的操作哪个GPIO、用哪个中断、寄存器地址在哪由板级描述去提供。这样同一份驱动代码就能在不同板卡上跑起来只需要改描述信息就行。1.2 设备、驱动、总线内核设备模型的三板斧理解Platform机制前得先建立起Linux内核设备模型Linux Device Model的基本概念。这个模型有三个核心角色设备device、驱动driver、总线bus。打个比方设备总线就像婚恋网站设备是征婚者驱动是求偶者。征婚者在网站上填好自己的简历——名字name、住址寄存器地址、爱好中断号、家庭背景时钟、DMA通道等资源求偶者也写清楚自己想找什么样的对象——擅长处理哪类设备id_table、能适配哪些型号compatible字符串。总线负责撮合它根据一套规则match函数去评判设备和驱动是否合适一旦匹配成功就安排它们领证——调用驱动里的probe()函数让驱动真正开始干活。Platform总线就是这套模型里最典型、最常用的一条总线。它不依赖任何物理信号纯粹是内核中的一段逻辑。所有长得不像PCI那种即插即用总线的设备都可以挂到Platform总线上来统一管理。在i.MX6ULL里你几乎找不到哪个外设不跟Platform打交道——GPIO控制器、UART控制器、I2C控制器、LCD控制器、看门狗清一色全是platform_driver。1.3 为什么设备树绕不开Platform再说现在几乎必修的设备树Device Tree。从内核角度讲设备树的作用就是用一种树形数据结构把板级硬件信息描述清楚。它和Platform的关系极其紧密设备树里的每一个compatible属性节点最终都会在内核启动阶段被解析成一个platform_device。具体流程是这样的引导程序U-Boot把设备树二进制文件DTB传给内核内核启动时用unflatten_device_tree()把DTB解成设备树节点的数据结构平台初始化代码随后调用of_platform_bus_probe()或其他类似函数遍历树中的节点——凡是符合平台设备特征的节点比如有compatible属性、不是挂在I2C/SPI等真实总线下的都会被创建为platform_device挂到Platform总线上。所以你写设备树节点、写platform_driver里的of_match_table本质就是为了让这两者在Platform总线上成功对上暗号。搞明白这一点你就知道设备树和Platform从来就不是两条独立的学习路线而是同一条链路上的两个环节。2. Platform机制的核心角色拆解2.1 platform_device硬件资源的简历platform_device在内核里的定义大致是这样一个结构体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表时会指向对应项 ... };这个结构体最关键的两个成员是name和resource。name是设备的名字在早期的、不使用设备树的方式里它和驱动的driver-name相同就能匹配。resource则描述了设备需要用到的硬件资源——寄存器区域的起始地址和长度IORESOURCE_MEM、中断号IORESOURCE_IRQ等。驱动运行时通过platform_get_resource()或platform_get_irq()去取这些资源而不是自己在代码里写死地址。在i.MX6ULL这类现代处理器上我们一般不直接在C代码里静态定义platform_device了而是通过设备树节点来声明设备。但理解platform_device的结构仍然重要因为你后续用的很多接口函数操作的对象其实是它所内嵌的struct device。比如devm_platform_ioremap_resource()这个常用函数第一个参数传的就是struct platform_device *。2.2 platform_driver驱动能力与probe入口platform_driver的定义也不复杂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; // 传统ID匹配表 };probe函数是整个驱动的灵魂。当设备和驱动匹配成功后内核立刻调用probe。你在probe里做的事通常包括获取硬件资源寄存器地址、中断号、申请并映射IO内存、注册中断处理函数、初始化硬件、注册字符设备或各类子系统框架设备。简单说probe就是驱动正式开始工作的入职仪式。remove函数是probe的逆过程在设备被移除时调用负责释放资源。i.MX6ULL开发中大多数时候设备不会被热拔插但卸载驱动模块时内核会走remove流程所以释放资源一定要写干净不然会引发内核崩溃或资源泄漏。2.3 platform总线match函数如何撮合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. 尝试 platform ID 表匹配 */ if (pdrv-id_table) return platform_match_id(pdev, pdrv-id_table) ! NULL; /* 3. 尝试驱动名和设备名字符串完全相等 */ return (strcmp(pdev-name, drv-name) 0); }看到没匹配规则其实有优先级顺序设备树的compatible匹配排第一其次是驱动里id_table的匹配最后才是简单粗暴的名字字符串比较。在i.MX6ULL的实际开发中90%以上情况走的是第一种——设备树compatible匹配。理解了这个顺序你就能明白为什么设备树节点的compatible属性第一个字符串必须和驱动of_match_table里的compatible完全一致——差一个字符、一个标点都有可能匹配失败。3. 核心细节解析i.MX6ULL上Platform匹配的完整链路3.1 设备树侧的简历怎么填在i.MX6ULL的设备树文件里通常是arch/arm/boot/dts/imx6ull.dtsi或板级dts文件硬件设备是这样描述的。我以经典的GPIO按键为例gpio-keys { compatible gpio-keys; pinctrl-names default; pinctrl-0 pinctrl_gpio_keys; status okay; user_key { label User Key; linux,code KEY_ENTER; gpios gpio5 1 GPIO_ACTIVE_LOW; // GPIO5_IO01 }; };这个节点里最重要的就是compatible字符串gpio-keys。这个字符串会被内核解析作为platform_device的名字之一用来和驱动侧的of_match_table比对。注意节点里还可以有其他属性比如gpios描述所用的引脚interrupts描述中断号这些都会转换成platform_device的resource。再比如中断方式的按键设备树节点会是key { compatible my-key-driver; interrupt-parent gpio5; interrupts 1 IRQ_TYPE_EDGE_BOTH; status okay; };这里interrupt-parent指明中断控制器GPIO5interrupts的第一个数表示GPIO5内的偏移IO01对应1第二个数表示触发类型双边沿触发。驱动侧就能用platform_get_irq()拿到这个中断号而不需要自己去查原理图再硬编码数字。3.2 驱动侧的求偶要求怎么写写platform_driver时关键的初始化是这样的static const struct of_device_id my_key_of_match[] { { .compatible my-key-driver }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, my_key_of_match); static struct platform_driver my_key_driver { .probe my_key_probe, .remove my_key_remove, .driver { .name my-key-driver, .of_match_table my_key_of_match, }, }; module_platform_driver(my_key_driver);这段代码有两个关键点。第一个是of_device_id数组里面的.compatible字段必须和设备树节点的compatible字符串完全一致。第二个是MODULE_DEVICE_TABLE(of, ...)这个宏的作用是让驱动模块在编译时生成一个设备ID表这样当驱动没有加载、但设备存在时系统可以通过modprobe机制自动加载对应驱动模块。很多人调试时发现驱动没生效第一步就该查设备树的compatible和驱动里的compatible是否一致、是否多了空格或大小写不对。3.3 module_platform_driver其实是个语法糖注意到上面代码最后一行module_platform_driver(my_key_driver)这个宏定义大致是#define module_platform_driver(__platform_driver) \ module_driver(__platform_driver, platform_driver_register, \ platform_driver_unregister)它帮你自动生成了module_init和module_exit函数分别调用platform_driver_register()注册驱动到Platform总线、platform_driver_unregister()注销驱动。省得每次写两段样板代码。搞清楚这个展开过程你看到驱动里没有明确写init/exit函数就不会慌了。3.4 匹配成功之后probe函数里的那些拿资源操作匹配成功后内核会调用驱动的probe函数。这一步在i.MX6ULL里基本是固定的几个动作。第一步获取寄存器内存资源并映射。驱动程序不能直接物理地址访问外设寄存器必须通过ioremap映射到虚拟地址空间。Platform框架提供了便捷函数struct resource *res; void __iomem *base; res platform_get_resource(pdev, IORESOURCE_MEM, 0); if (!res) { dev_err(pdev-dev, failed to get memory resource\n); return -ENXIO; } base devm_ioremap_resource(pdev-dev, res); if (IS_ERR(base)) return PTR_ERR(base);如果你只需要默认的第一个MEM资源还有更简洁的写法base devm_platform_ioremap_resource(pdev, 0);这里的devm_前缀表示设备资源管理device managed意思是这些资源由内核自动跟踪释放。驱动卸载或probe失败时内核会自动释放已经申请的资源可以避免很多泄漏问题。第二步获取中断资源。如果设备产生中断通过平台接口拿int irq platform_get_irq(pdev, 0); if (irq 0) { dev_err(pdev-dev, failed to get irq\n); return irq; }需要注意platform_get_irq()可能在设备树中断描述有误时返回负数不能直接当成功要做错误检查。第三步注册各类子系统或字符设备。比如GPIO按键驱动会注册input子系统设备LED驱动会注册led_classdev普通的自定义设备则注册miscdevice或字符设备。这一步跟你在纯字符设备驱动里做的差不多只不过设备号申请、cdev添加这些操作都挪进了probe里。4. 完整实操在i.MX6ULL上编写一个Platform驱动4.1 实验目标与硬件准备为了把前面的原理串起来我这里给出一个完整的、可以在i.MX6ULL开发板上验证的示例。假设我们做一个简单的LED驱动LED接在GPIO5_IO03上高电平点亮。我们不直接用gpiolib的通用接口比如gpiod_set_value而是模拟一个平台外设的实现流程——通过设备树描述LED的寄存器对应的GPIO组和引脚偏移驱动用Platform框架拿资源并操作。实验环境硬件正点原子或任意i.MX6ULL开发板LED接GPIO5_IO03内核4.1.15或5.4.x均可下面代码兼容交叉编译器arm-linux-gnueabihf-gcc4.2 修改设备树在板级设备树文件比如imx6ull-myboard.dts中追加节点/ { myled { compatible my-company,myled; reg 0x020c4000 0x10; /* GPIO5 base address on i.MX6ULL */ gpiopin 3; /* GPIO5_IO03 */ status okay; }; };这里做个说明i.MX6ULL的GPIO5寄存器物理基地址是0x020C4000我在这里通过reg属性描述驱动里用platform_get_resource()拿到这个地址后做ioremap。实际正规产品中往往直接用gpio子系统或pinctrl子系统管理引脚这里为了演示Platform资源获取故意用原始寄存器方式。gpiopin是自定义属性用来告诉驱动用这个GPIO组里的第几个引脚。设备树里允许自定义属性只要驱动用device_property_read_u32()之类的API读取即可。这体现了设备树灵活描述硬件的精神——你不需要让内核预先知道每个属性是什么意思只要驱动知道就行。4.3 编写platform_driver下面给出完整的驱动代码#include linux/module.h #include linux/platform_device.h #include linux/of.h #include linux/of_device.h #include linux/io.h #include linux/miscdevice.h #include linux/fs.h #include linux/uaccess.h #define GPIO5_BASE 0x020C4000 #define GPIO_DR 0x00 #define GPIO_GDIR 0x04 #define GPIO_DR_SET 0x84 #define GPIO_DR_CLEAR 0x88 #define LED_OFF 0 #define LED_ON 1 static void __iomem *gpio5_base; static int led_gpio_pin; static int led_open(struct inode *inode, struct file *filp) { return 0; } static ssize_t led_write(struct file *filp, const char __user *buf, size_t count, loff_t *ppos) { char val; if (count ! 1) return -EINVAL; if (copy_from_user(val, buf, 1)) return -EFAULT; if (val LED_ON) writel(1 led_gpio_pin, gpio5_base GPIO_DR_SET); else writel(1 led_gpio_pin, gpio5_base GPIO_DR_CLEAR); return count; } static const struct file_operations led_fops { .owner THIS_MODULE, .open led_open, .write led_write, }; static struct miscdevice led_miscdev { .minor MISC_DYNAMIC_MINOR, .name myled, .fops led_fops, }; static int myled_probe(struct platform_device *pdev) { struct resource *res; int ret; /* 从设备树读取自定义属性 gpiopin */ ret device_property_read_u32(pdev-dev, gpiopin, led_gpio_pin); if (ret) { dev_err(pdev-dev, failed to get gpiopin property\n); return -EINVAL; } /* 从 reg 属性获取 GPIO5 寄存器物理地址并映射 */ res platform_get_resource(pdev, IORESOURCE_MEM, 0); if (!res) { dev_err(pdev-dev, failed to get mem resource\n); return -ENXIO; } gpio5_base devm_ioremap_resource(pdev-dev, res); if (IS_ERR(gpio5_base)) return PTR_ERR(gpio5_base); /* 设置 GPIO5_IO03 为输出并默认输出低电平 */ writel(readl(gpio5_base GPIO_GDIR) | (1 led_gpio_pin), gpio5_base GPIO_GDIR); writel(1 led_gpio_pin, gpio5_base GPIO_DR_CLEAR); /* 注册 misc 设备自动创建 /dev/myled 节点 */ ret misc_register(led_miscdev); if (ret) { dev_err(pdev-dev, failed to register misc device\n); return ret; } dev_info(pdev-dev, myled probed, pin%d\n, led_gpio_pin); return 0; } static int myled_remove(struct platform_device *pdev) { misc_deregister(led_miscdev); /* devm_ 系列资源由内核自动释放无需手动 iounmap */ dev_info(pdev-dev, myled removed\n); return 0; } static const struct of_device_id myled_of_match[] { { .compatible my-company,myled }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, myled_of_match); static struct platform_driver myled_driver { .probe myled_probe, .remove myled_remove, .driver { .name myled, .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);4.4 编译与Makefile内核驱动的编译方式和普通应用程序完全不同需要借助内核的Kbuild系统。Makefile写得很固定obj-m : myled.o KERN_DIR : /path/to/kernel/source CROSS_COMPILE : arm-linux-gnueabihf- all: $(MAKE) -C $(KERN_DIR) M$(PWD) ARCHarm CROSS_COMPILE$(CROSS_COMPILE) modules clean: $(MAKE) -C $(KERN_DIR) M$(PWD) ARCHarm clean这里要强调两点。第一KERN_DIR必须指向已经配置好、并且和你板子上运行内核版本一致的源码目录而且源码目录最好先编译过生成过Module.symvers等文件否则编译时可能出现找不到头文件或版本不匹配的问题。第二如果板子是启动SD卡里的内核编译驱动用的内核源码要和SD卡内核是同一份源代码编译出来的不然insmod时会出现version magic不匹配的报错。编译好后生成myled.ko文件用nfs或者TFTP传到开发板上。加载步骤insmod myled.ko如果没有报错说明Platform驱动注册成功。然后操作LEDecho 1 /dev/myled # 点亮 echo 0 /dev/myled # 熄灭在调试过程中最直观的确认方法是在probe函数里的dev_info打印——加载驱动后如果能在dmesg里看到myled probed这条日志说明设备树、匹配、probe整个链路都通了。如果看不到就需要对照下一节的排查步骤一个一个查。5. 常见问题与排查技巧实录5.1 驱动加载了probe却没被调用这是初学者遇到最多的问题。现象是insmod没报错但dmesg里没有自己的probe日志。可能原因按优先级排列依次是设备树节点没生效。检查设备树节点是否被编译进DTB里可以用fdtget工具查看板子实际用的设备树里有没有这个节点。比如fdtget /proc/device-tree/myled/compatible value如果这个命令报错说明节点根本不在设备树里后面一切都无从谈起。compatible字符串不匹配。设备树里写的是my-company,myled驱动of_match_table里写的compatible必须一模一样。注意逗号前后不要有多余空格字符串大小写也必须区分。驱动编译时用的内核源码和板子运行内核不匹配。这会导致设备模型或者设备树解析模块行为不一致通常伴随version magic或Unknown symbol之类的报错但有时也会悄悄失败。设备树里status属性被设置成disabled。有些SoC默认很多外设节点是disabled如果你append节点时没有改动status设备就不会被注册成platform_device。在调试时把status写成okay是最省事的。5.2 匹配成功但probe里获取资源失败probe被调用了说明匹配机制没问题但常常卡在platform_get_resource()返回NULL或者devm_ioremap_resource()返回错误。这种情况几乎都是设备树reg属性写错了。比如GPIO5的基地址在i.MX6ULL参考手册里是0x020C4000如果你写成了0x20C4000少一个0那就不是同一个地址了ioremap之后操作寄存器的结果完全不可预期甚至可能导致内核崩溃。另外reg里的length值也不要太小至少要覆盖到你访问的寄存器偏移。比如我们要操作到GPIO_DR_CLEAR偏移0x88reg长度就不能小于0x8C不然devm_ioremap_resource会警告resource size invalid。还有一点值得提醒GPIO5这组寄存器里有些偏移是CPU对应的GPIO模块寄存器如果你只是想要一个普通的输出引脚控制和中断内核提供了更好的gpiolib和pinctrl子系统。我这篇文章特意用原始寄存器方式是为了演示Platform资源获取的流程真正产品代码里建议还是优先用devm_gpiod_get()和pinctrl框架避免自己操作寄存器引入硬件时序问题。5.3 probe只执行一次其实是正常现象很多人在开发板上反复insmod/rmmod发现第二次insmod后probe不再打印日志以为出了问题。实际上这不是故障而是因为你第一次加载驱动时已经成功probe并注册了misc设备rmmod虽然卸载了驱动但如果设备树里的节点还在Platform设备仍然存在。设备还在驱动再次注册匹配后就会再次probe。如果你发现重新加载驱动后probe没有调用先检查rmmod是不是真的卸载成功了可以用lsmod | grep myled查看。另外misc设备注册了一次没释放干净再次注册时会报register_chrdev_region失败那说明就是remove函数里的释放逻辑没写对。5.4 同名驱动的覆盖问题在平台总线上如果两个platform_driver的name或者compatible有包含关系匹配时可能会抢设备。比如一个驱动的compatible是my-company,myled另一个驱动写的是myled内核的of_match会先精确匹配compatible字符串如果两个驱动都匹配同一个设备节点实际生效的取决于注册顺序和匹配算法的迭代顺序这在模块化开发中非常隐蔽。经验就是compatible字符串一定用厂商,设备型号的格式不要用裸单词既规范又不容易撞车。5.5 如何快速验证匹配成功与否调试时我习惯用一个最快的方法在probe入口第一行加dev_info(pdev-dev, probe called\n)然后在加载驱动后马上执行dmesg | tail。但更好的方式是查看内核在设备模型里留下的sysfs痕迹ls /sys/bus/platform/devices/在这里你能看到所有platform_device的名字。如果你写的设备树节点已经被解析成platform_device这里会有一个对应条目。然后ls /sys/bus/platform/drivers/确认驱动的名字在不在。再去对应驱动目录下cat /sys/bus/platform/drivers/myled/uevent可以看到这个驱动绑定了哪个设备。如果驱动的bind目录下没有生成设备链接那就说明设备或驱动某一方没有注册成功匹配就没有发生。5.6 关于找不到platform_device的历史遗留我还想特别提醒一点网上很多老教程、旧代码还在用platform_device_register()在C代码里静态注册platform_device。在i.MX6ULL这种新型设备树平台下这种方式已经彻底过时了。内核启动时主要通过设备树来生成platform_device如果在C代码里还另外注册同名设备反而可能引发冲突。所以当你参考老代码时看到platform_device_register、platform_add_devices这类调用基本可以判断那是没有设备树时代比如S3C2440、S5PV210的写法不要直接照搬到i.MX6ULL上。6. Platform机制在i.MX6ULL驱动开发中的实际定位6.1 别把所有驱动都硬套Platform熟悉了Platform框架后容易产生一个误区什么外设都想写成platform_driver。实际上Platform包的是一类特定的设备——片上集成且不可枚举、也不挂接在I2C/SPI/USB等物理总线上的设备。i.MX6ULL上确实大部分控制器都符合这个特征但比如你要外接一个I2C接口的传感器芯片那就应该写成i2c_driver挂到I2C总线上由I2C核心来管理匹配。如果你强行写成platform_driver反而没法利用I2C框架的总线读写机制。这就要求你在设计驱动框架时先判断外设的归属挂在I2C总线上 → i2c_driver挂在SPI总线上 → spi_driver挂在USB总线上 → usb_driver片上集成、地址映射型外设 → platform_driver在i.MX6ULL上做项目时遵循这个分类逻辑驱动结构才清晰。6.2 与设备树、pinctrl、gpiolib的协作关系很多初学者以为Platform只是单纯的匹配probe其实在真实的i.MX6ULL驱动里Platform框架还会和其他子系统紧密协作。你在probe里对设备树属性进行解析通常还会调用pinctrl子系统选择引脚复用状态调用gpiolib申请GPIO资源。这些子系统的初始化顺序很讲究pinctrl在设备probe之前就已经被系统按pinctrl-names和pinctrl-0自动配置好了所以在probe里你直接操作gpio、i2c等外设时引脚复用已经就绪。这就是为什么设备树节点里写pinctrl-0 pinctrl_xxx驱动代码里却看不到任何pinctrl调用也能正常工作。同理gpiolib的devm_gpiod_get()为什么会自动关联到设备树里的gpios属性本质也是基于设备模型通过device的of_node去解析。Platform机制的价值就在于它把这一整套从设备树描述到驱动代码的桥接工作规范化了。你写的probe函数看似就几行拿资源、注册设备的代码背后却是整个Linux设备模型在支撑。6.3 官方驱动是怎么用Platform的如果你去阅读i.MX6ULL的官方BSP内核源码比如看drivers/tty/serial/imx.c里UART驱动的probe函数或者drivers/watchdog/imx2_wdt.c你会发现它们的套路和我们上面写的示例完全一致定义of_device_id数组、定义platform_driver、在probe里拿资源、注册子设备。官方驱动的复杂度虽高但骨架一模一样。所以别被那些几千行的驱动源码吓到你理解了Platform的骨架再去看官方驱动看到的就不是天书而是一个套路不断重复的工程集合。6.4 后续扩展从Platform走向更深的设备模型把Platform机制吃透之后你的Linux驱动学习会进入一个正反馈循环。比如后面学I2C驱动、SPI驱动你会发现它们的核心结构其实和Platform是亲兄弟都有driver结构体、都需要match机制、都靠probe触发初始化。区别只是不同的总线类型的match规则和device结构不同而已。反过来你去研究电源管理框架、时钟框架、引脚控制框架都会发现它们都架构于统一的linux设备模型之上。所以Platform的学习价值从来不只在Platform本身它是一把打开Linux内核设备驱动大门的钥匙。7. 一点实操心得我给初学者的四条建议第一先手动编译一次设备树再写驱动。很多问题都出在设备树上你可以先用dtc反编译自己的DTB文件看看设备树节点到底有没有被编译进去、compatible对没对上比反复insmod/rmmod查dmesg高效得多。第二充分利用devm_系列函数。在platform_driver的probe里尽量用devm_ioremap_resource、devm_gpiod_get、devm_platform_ioremap_resource这一类的API内核会在设备解绑时自动释放资源能直接省掉一多半remove函数里的清理工作也减少了内存泄漏的可能。第三熟悉/sys/bus/platform这个虚拟目录。我在调试时的心态是设备模型看不见摸不着但sysfs把所有对象都暴露成了文件和目录。你完全可以眼见为实地看到设备和驱动的绑定状态这是排查匹配问题的第一现场。第四多读官方BSP内核源码。遇到不熟悉的Platform用法去drivers/目录下找一个同类型设备的驱动看看别人在probe里怎么拿资源、怎么注册子设备。i.MX6ULL的BSP内核里几乎每种外设都能找到参照模板。说实话Platform这套机制我在第一次接触时也觉得绕明明只是一个字符设备驱动为什么要搞出设备、驱动、总线三个概念出来。但后来做实际项目需要在不同硬件配置的多块板子上复用同一个驱动才真切体会到这套设计的意义。没有它每换一块板子就得改一次驱动源码重新编译有了它只需要改设备树、换一个DTB文件驱动代码一行不用动。Linux驱动开发的魅力就在于此——不是用代码去将就硬件而是用一套优雅的框架把硬件变化挡在驱动之外。希望这篇文章能帮你把Platform的这条线理清楚少走我当年走过的弯路。
返回列表