拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Linux Platform总线驱动全解析:从设备树匹配到probe执行

这两年做嵌入式Linux驱动开发i.MX6ULL是绕不开的一块板子网上教程也一大堆。不过我发现一个很普遍的问题很多人照着教程把register_chrdev、class_create这些API一调驱动能跑起来led灯也能点就算完事了。一旦让他把驱动改成标准Linux设备模型里的Platform驱动或者让他解释一下“设备是怎么找到驱动的”立马就卡壳。这篇文章就专门把Platform总线这块骨头啃透。我会从“为什么Linux内核非要搞出一套platform_device和platform_driver分离的机制”讲起然后按启动流程把设备和驱动的注册、匹配、probe调用全过程拆开揉碎再给出一份i.MX6ULL上可以直接落地跑的完整示例代码。文章最后是我实际调试中经常遇到的几个坑以及对应的排查思路。内容适合正在学i.MX6ULL驱动开发、但又不想只停留在“点灯”阶段的人。看完之后你再去看内核里其他总线的驱动比如I2C、SPI会发现套路几乎是一样的会省下大量时间。1. 为什么要有Platform总线设备模型的设计初衷1.1 直接写驱动init函数的问题在哪很多做单片机出身的人第一次写Linux驱动最习惯的思路就是在模块入口函数里直接去操作寄存器。比如i.MX6ULL的点灯可以直接ioremap一个GPIO组的基地址然后设置复用寄存器、方向寄存器、数据寄存器一气呵成。这种写法在Linux驱动里叫“直接注册式”驱动也就是驱动代码把硬件信息写死在驱动内部。它的缺点显而易见一旦硬件管脚变了比如灯从GPIO1_IO03换到了GPIO5_IO01你就得改驱动源码重新编译。如果一块板子上有10颗灯10种不同的接法你得维护10份驱动代码或者用宏定义切换代码里全是#ifdef。这违反了设备驱动模型最基本的解耦原则设备是什么应该由板级描述来决定驱动怎么做才是驱动开发者的事。两者不该死死捆绑在一起。1.2 设备、驱动、总线三者的关系Linux设备模型引入了几个核心概念device设备、device_driver驱动、bus总线。通俗点说总线就是一条“红线”设备和驱动各自挂在这条红线的两端内核的匹配机制负责撮合它们。但问题来了i.MX6ULL上的GPIO控制器、UART控制器、LED、按键这些设备它们并没有像PCI设备那样挂在某条真实存在的物理总线上也没有厂商ID和设备ID可以用来枚举识别。这种情况下Linux内核就抽象出了一条“虚拟总线”叫Platform总线。它专门用来管理那些直接挂在CPU内存总线上的设备。所以记住一个判断标准凡是设备地址直接映射在内存空间里、不依赖热插拔总线USB、PCI、SDIO这种来枚举发现的硬件基本都可以用Platform机制来描述和管理。1.3 i.MX6ULL里Platform机制的具体应用场景在i.MX6ULL的实际开发中Platform这套机制无处不在。举几个最典型的例子设备树里写的compatible fsl,imx6ull-gpio内核启动时会根据设备树节点创建一个个platform_device。你在驱动里用of_match_table声明了支持的设备型号内核就会把设备树里对应的设备节点和你的驱动匹配起来。gpio-leds、gpio-keys这些常见的内核驱动本质上就是注册了一个platform_driver然后对接到设备树里的led、key节点。可以说在i.MX6ULL这种以设备树为硬件描述核心的平台上写驱动的主要工作就是实现一个Platform驱动的probe和remove函数剩下的注册、匹配、生命周期管理内核全部给你包办了。2. 设备与驱动的注册入口理解device和driver的“档案”结构2.1 platform_device结构体剖析在内核源码里platform_device定义在include/linux/platform_device.h中核心内容如下struct platform_device { const char *name; int id; bool id_auto; struct device dev; u32 num_resources; struct resource *resource; /* ... */ };其中name字段是设备的“名字”这个在非设备树时代是匹配的关键resource字段保存了设备的中断号、寄存器物理地址范围等信息。我看到很多人不理解struct resource是干嘛的这里额外说一下。它描述的是设备占用的硬件资源比如起始地址0x0209C000、长度0x1000的一段寄存器空间或者一个GPIO中断号。在传统非设备树模式下这些资源是你在代码里手动填的在设备树的模式下内核会帮你解析设备树节点里的reg和interrupts属性自动填充到resource里。struct resource { resource_size_t start; resource_size_t end; const char *name; unsigned long flags; /* ... */ };2.2 platform_driver结构体剖析再看驱动侧。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; /* ... */ };其中probe是最重要的回调。内核在完成设备与驱动的匹配之后会调用probe函数。你的驱动初始化逻辑包括申请内存、注册字符设备、获取中断、初始化GPIO等全部放在里面。还有一个容易忽略的字段是driver里嵌套的of_match_table。以i.MX6ULL上常用的GPIO按键驱动为例注册驱动时都会带一个设备树匹配表static const struct of_device_id gpio_keys_of_match[] { { .compatible gpio-keys, }, { /* sentinel */ } }; static struct platform_driver gpio_keys_device_driver { .probe gpio_keys_probe, .driver { .name gpio-keys, .of_match_table gpio_keys_of_match, .pm gpio_keys_pm_ops, } };匹配表里每一行是一个of_device_id核心比较的是compatible字符串。这里有个细节数组最后一个元素必须是空结构也就是sentinel内核遍历时遇到compatible为NULL就停止。2.3 非设备树时代的注册方式回顾虽然现在i.MX6ULL的开发基本都是基于设备树了但理解传统方式能帮你更好地掌握内核设备模型的演进逻辑。在早期内核比如Linux 3.x以前没有设备树或者没使能设备树时设备信息一般定义在板级文件里比如arch/arm/mach-imx/mx6ul_14x14_evk.c里会有一堆platform_device_register的调用。static struct platform_device led_device { .name imx6ull-led, .id -1, .num_resources ARRAY_SIZE(led_resources), .resource led_resources, }; platform_device_register(led_device);驱动侧则写在另一个文件里static struct platform_driver led_driver { .probe led_probe, .remove led_remove, .driver { .name imx6ull-led, .owner THIS_MODULE, }, }; module_platform_driver(led_driver);这种情况下设备侧和驱动侧通过name字段是否一致来判断是否匹配。所以名字必须保持一致这就是为什么你会看到有人说“platform_driver的名字要跟platform_device的名字一样才能匹配”这句话只适用于非设备树的情况。3. 匹配过程全解析内核是怎么把设备和驱动牵上线的3.1 设备注册后的连锁反应当内核注册一个platform_device时它会把这个设备挂到Platform总线的设备链表上然后触发一次总线自身的“匹配检查”看看当前有没有哪个驱动的名字或者匹配表能和它对应上。具体到代码层面platform_device_register最终会走到device_add函数然后调用bus_probe_device。这个函数检查如果总线上已经有匹配的驱动就直接执行device_attach里的绑定流程。换句话说设备可以“主动”去找驱动。反过来也一样。当你注册一个platform_driver内核会遍历该总线上的所有设备依次做匹配尝试。这就是设备晚到和驱动晚到都能正常工作原因——不管是设备先来还是驱动先来内核都要保证最终能配对成功。platform_match是整个匹配逻辑的核心函数在内核的drivers/base/platform.c里它的处理顺序大致是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等平台 */ if (acpi_driver_match_device(dev, drv)) return 1; /* 3. 直接比较驱动名和设备名 */ if (pdrv-id_table) return platform_match_id(pdev, pdrv-id_table) ! NULL; /* 4. 兜底比较 name 字段 */ return (strcmp(pdev-name, drv-name) 0); }注意这种优先级顺序在i.MX6ULL的设备树开发模式下走的是第一条路径也就是of_driver_match_device。它内部会去设备树节点里查找compatible属性值然后和驱动的of_match_table里各项compatible做字符串比对。3.2 实际匹配流程中的关键代码路径如果驱动的of_match_table里声明的compatible是一个字符串会生成一个类型为OF_MATCH_TYPE_COMPATIBLE的匹配项。内核对照设备树节点的compatible属性时会依次取出属性值里的每个字符串与驱动的匹配条目比较。设备树的compatible属性可以同时声明多个值比如compatible fsl,imx6ull-gpio, fsl,imx6ul-gpio;匹配时只要有一个命中就算匹配成功。我在实际开发中见过有人纠结这两个值排序有没有讲究。整理一下第一个值表示“最精确的型号”后面的值逐步宽泛兼容。比如一个设备内核里专门写了imx6ull-gpio的驱动也写了兼容imx6ul的驱动这种情况下应该优先匹配第一个值对应型号最精确的驱动。3.3 匹配成功后执行probe的完整链条匹配成功并不代表驱动立刻就能工作内核还需要完成绑定的“最后一公里”。对于驱动侧触发的情况其调用链是platform_driver_register - driver_register - bus_add_driver - driver_attach - bus_for_each_dev // 遍历总线设备 - __device_attach - device_match_dev // 实际就是之前的 platform_match - driver_probe_device - really_probe - drv-probe // 最终调到你的 probereally_probe这个函数值得记住因为以后排查“probe为什么没跑”的时候经常会跟它打交道。在这条链路里它会做几件关键的事设备与驱动绑定dev-driver drv调用pm_runtime相关的初始化调用dev_groups里的属性创建最后调用pdrv-probe(pdev)进入你的驱动函数如果probe过程中返回错误码内核会执行dev-driver NULL的回滚操作并把设备状态恢复到匹配前。4. 从零编写i.MX6ULL的Platform设备驱动CPU_GPIO按键实战4.1 硬件与需求梳理我这边以i.MX6ULL开发板上的一个用户按键为例。按键连接到SNVS_TAMPER1引脚对应GPIO5_IO01按下为低电平。驱动要做的事很简单注册Platform驱动匹配设备树里的按键节点probe时获得GPIO中断注册一个输入设备按键按下时上报按键事件这里的驱动通过设备树获得设备信息不带任何板级硬编码。同样的驱动换一块板子只要设备树里描述的引脚不同驱动源码不需要动。4.2 设备树节点的编写要点设备树里节点定义这么写gpio-keys { compatible gpio-keys; pinctrl-names default; pinctrl-0 pinctrl_key; status okay; user-key { label user-key; linux,code KEY_ENTER; gpios gpio5 1 GPIO_ACTIVE_LOW; }; };这里有几个点需要解释一下。compatible gpio-keys不是随便写的内核里已经有现成的gpio-keys驱动它的of_match_table里就是这一个字符串匹配上后由内核自带的drivers/input/keyboard/gpio_keys.c处理根本不用你自己写驱动。所以这里真正的价值是演示设备树如何描述一个Platform设备。假如你想自己写一个名为imx6ull-key的驱动来管理这个按键可以把compatible改成imx6ull-key然后在驱动里声明static const struct of_device_id imx6ull_key_of_match[] { { .compatible imx6ull-key }, { /* sentinel */ } };这样写的好处是你可以完全控制按键去抖逻辑、上报策略。新手练手用自研驱动更好因为能观察完整的注册过程。4.3 驱动代码实现与关键逻辑注释这是驱动部分的核心代码。按照标准Linux字符设备的方式我习惯拆成probe里做硬件初始化和设备创建file_operations只负责读操作。#include linux/module.h #include linux/platform_device.h #include linux/of.h #include linux/of_gpio.h #include linux/gpio.h #include linux/interrupt.h #include linux/miscdevice.h #include linux/uaccess.h #define DEVICE_NAME imx6ull-key static int key_gpio; static int key_irq; static int key_value 1; static irqreturn_t key_irq_handler(int irq, void *dev_id) { key_value gpio_get_value(key_gpio); return IRQ_WAKE_THREAD; } static irqreturn_t key_irq_handler_thread(int irq, void *dev_id) { /* 实际上真正的上报逻辑在这里做避免在中断上下文里处理复杂事务 */ return IRQ_HANDLED; } static ssize_t key_read(struct file *filp, char __user *buf, size_t size, loff_t *offset) { int ret; char val key_value 0; ret copy_to_user(buf, val, 1); if (ret) { return -EFAULT; } return 1; } static const struct file_operations key_fops { .owner THIS_MODULE, .read key_read, }; static struct miscdevice key_miscdev { .minor MISC_DYNAMIC_MINOR, .name DEVICE_NAME, .fops key_fops, }; static int key_probe(struct platform_device *pdev) { struct device_node *np pdev-dev.of_node; int ret; key_gpio of_get_named_gpio(np, key-gpio, 0); if (!gpio_is_valid(key_gpio)) { dev_err(pdev-dev, invalid gpio\n); return -EINVAL; } ret devm_gpio_request_one(pdev-dev, key_gpio, GPIOF_IN, DEVICE_NAME); if (ret) { dev_err(pdev-dev, gpio request failed\n); return ret; } key_irq gpio_to_irq(key_gpio); ret devm_request_threaded_irq(pdev-dev, key_irq, key_irq_handler, key_irq_handler_thread, IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING, DEVICE_NAME, NULL); if (ret) { dev_err(pdev-dev, irq request failed\n); return ret; } ret misc_register(key_miscdev); if (ret) { dev_err(pdev-dev, misc register failed\n); return ret; } dev_info(pdev-dev, key driver probed\n); return 0; } static int key_remove(struct platform_device *pdev) { misc_deregister(key_miscdev); return 0; } static const struct of_device_id key_of_match[] { { .compatible imx6ull-key }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, key_of_match); static struct platform_driver key_platform_driver { .probe key_probe, .remove key_remove, .driver { .name DEVICE_NAME, .of_match_table key_of_match, }, }; module_platform_driver(key_platform_driver); MODULE_LICENSE(GPL); MODULE_AUTHOR(Your Name); MODULE_DESCRIPTION(i.MX6ULL platform key driver);这里面有很多值得细说的工程细节。module_platform_driver是一个封装宏它帮你自动生成了模块加载和卸载时要调用的注册、注销函数。如果驱动需要额外的初始化逻辑你也可以不用这个宏自己写module_init和module_exit。devm_gpio_request_one里的devm前缀是内核里的managed device资源管理机制。用devm_系列函数申请的资源在设备注销或者驱动probe失败时会自动释放。省去了写大量错误处理代码的麻烦。中断里用到了request_threaded_irq它是为了把中断处理分成顶半部和底半部按键消抖这种耗时操作放到线程化中断上下文完成。顶半部里不能调用可能睡眠的函数线程化以后就可以。4.4 设备树属性与驱动匹配验证把设备树节点和key_of_match放在一起看整个匹配流程就很清晰了。内核启动解析设备树时会在gpio-keys这个节点上创建platform设备。当我insmod驱动时platform_driver_register里的遍历动作会找到这个设备内核把设备树节点的compatible属性值“imx6ull-key”与驱动匹配表里的字符串比对命中后调用key_probe。这整个过程可以查看内核日志确认。如果你在内核启动参数里加了loglevel8或者用dmesg查看正常情况下能看到这句输出imx6ull-key imx6ull-key: key driver probed如果probe没有执行最大的可能性就是compatible两边写的不一致。一个很容易踩的坑是设备树里字符串后面带了多余空格或者在驱动里MODULE_DEVICE_TABLE(of, key_of_match)没有写导致设备树匹配表没有编进模块的modinfo信息里。5. 老平台如何匹配id_table详解与适用场景现在i.MX6ULL的开发已经全面进入了设备树时代但很多老代码、老项目还保留着传统的platform匹配方式也就是id_table。理解它仍然很有必要因为工业项目里升级内核、移植驱动时很可能碰到几十上百个驱动还都是老写法。platform_device_id结构定义如下struct platform_device_id { char name[PLATFORM_NAME_SIZE]; kernel_ulong_t driver_data; };driver_data是一个无符号长整型可以在匹配成功后传给驱动用来区分是同类型但版本不同的设备。在实际的匹配流程中第3步会调用platform_match_id它的实现逻辑不复杂static const struct platform_device_id *platform_match_id( struct platform_device *pdev, struct platform_device_id *id) { while (id-name[0]) { if (strcmp(pdev-name, id-name) 0) { pdev-id_entry id; return id; } id; } return NULL; }也就是说如果设备侧platform_device的name是“imx6ull-key”驱动侧的id_table里有一项name也是“imx6ull-key”就能匹配成功随后probe会被执行。需要重点提醒的是在设备树模式下有些老驱动为了兼容会同时提供id_table和of_match_table。此时匹配顺序是先尝试of匹配如果of匹配失败还会回头查id_table。而如果设备树里没有compatible属性或者某节点的compatible与任何驱动都不一致但platform_device是从设备树节点创建的它的name字段会取自节点名这种情况下id_table依然可能命中。6. 调试与排查技巧probe没跑、匹配失败的定位方法6.1 probe没有被调用的常见原因与排查路径做Platform驱动开发时遇到最多的问题肯定是驱动编译加载都没报错但probe就是不执行或者没有任何日志输出。先把最基础的排查顺序捋一遍。第一步使用ls /sys/bus/platform/devices/查看设备是否已经创建。如果设备树解析正常i.MX6ULL上会看到大量节点生成的platform设备比如2000000.aips-bus、20a0000.uart之类的。如果你的设备节点没有出现在这个列表里说明设备树解析环节出了问题先回过去查设备树语法和节点状态。第二步查看/sys/bus/platform/drivers/下你的驱动是否注册成功。如果驱动没有出现在这个目录里说明platform_driver_register就没成功检查模块加载日志。第三步检查匹配条件这一步最常用。如果设备存在、驱动也在但probe没执行直接查看驱动目录下是否生成了设备软链接。例如ls /sys/bus/platform/drivers/imx6ull-key/理论上如果匹配成功会出现一个指向你那个platform设备的符号链接。如果没出现就用cat /sys/bus/platform/devices/设备名/uevent查看设备的compatible参数再对照驱动的of_match_table排查。6.2 通过debugfs观察设备与驱动的绑定状态内核的driver core在CONFIG_DEBUG_FS使能时会提供设备与驱动绑定状态的调试节点。挂在Platform总线上的设备其状态可以通过下面的路径查看mount -t debugfs none /sys/kernel/debug cat /sys/kernel/defs/devices_deferred尤其要注意devices_deferred这个文件。如果设备因为probe返回了-EPROBE_DEFER而进入延迟探测队列它会在这里列出来。所谓-EPROBE_DEFER是说本次probe因为依赖的某个设备还没就绪比如引用的时钟、中断控制器还没注册需要等待后续重试。我实际碰到过的一个典型情况是按键驱动里用了devm_gpio_request_one但这个GPIO对应的gpiochip还没注册完成probe返回-EPROBE_DEFER设备就被挂到延迟队列。等GPIO控制器驱动加载完毕内核会自动重新触发这个设备的probe。如果这个过程反复失败devices_deferred文件里就能看到问题所在。6.3 内核日志级别的调整技巧内核的printk输出会被日志级别拦截。很多驱动用dev_info打印信息如果当前控制台的日志级别低于INFO你是看不到输出内容的。调试阶段建议直接加dev_err或者把控制台级别调高。在uboot启动参数里可以加loglevel8或者在运行时echo 8 /proc/sys/kernel/printk如果驱动里用了dev_dbg这种调试信息默认在CONFIG_DYNAMIC_DEBUG未开启时完全不输出。需要开启动态调试并单独使能echo file gpio_key.c p /sys/kernel/debug/dynamic_debug/control6.4 常见问题速查表问题现象可能原因排查方法probe不调用设备不存在设备树节点没有编译进dtbstatus属性为disabled检查dtb、status改为okayprobe不调用设备存在但驱动没绑定compatible字符串不一致对照uevent和of_match_tableprobe返回-EPROBE_DEFER依赖资源未就绪查看devices_deferred确认依赖模块加载顺序misc_register失败设备号冲突、misc设备注册数超限检查内核日志换MISC_DYNAMIC_MINOR中断申请失败GPIO没有正确配置成中断模式引脚被复用用gpio_request后检查pinmux设置读key_value永远不变中断没触发或gpio_get_value读错电平先确认引脚电平变化再看中断是否注册成功7. 基于Platform机制的扩展i.MX6ULL外围驱动的通用写法Platform机制掌握好之后能力是可以直接平移的。i.MX6ULL上的UART、I2C、SPI控制器驱动都基于类似的总线模型把platform_driver换成i2c_driver、spi_driver逻辑几乎一样。比如要写一个挂在I2C总线上的外部RTC芯片驱动只需要在驱动结构体里用i2c_driver并实现id_table核心思路还是“设备侧注册、驱动侧匹配、匹配后probe”这三板斧。而且i.MX6ULL的IOMUX配置也是通过pinctrl子系统管理的跟Platform设备的关系很紧密。设备树节点里的pinctrl-0属性会指定一个pin控制状态内核在设备probe之前就会根据pinctrl-names里的default状态自动配置好引脚。这也是为什么驱动里你不需要手动去操作IOMUX寄存器只要设备树描述对了引脚功能、上下拉、驱动能力这些事情都被内核处理了。所以当你写的Platform驱动需要打开某个引脚对应的外设功能时检查一下你的设备树节点里有没有配pinctrl-0。如果没有probe里访问的寄存器可能完全无效因为引脚还是默认的GPIO功能外部设备根本没法工作。我当时调I2C时遇到过类似问题折腾了很久最后发现是设备树里引脚复用配置忘写了。8. 深入理解platform_driver注册宏的编译期行为很多教程提到module_platform_driver就是“帮你自动注册驱动”但没讲它到底怎么做的。这个宏在内核源码include/linux/platform_device.h里定义#define module_platform_driver(__platform_driver) \ module_driver(__platform_driver, platform_driver_register, \ platform_driver_unregister)它实际是module_driver宏的套壳。module_driver做的事情是定义两个函数一个模块入口函数调用platform_driver_register一个模块出口函数调用platform_driver_unregister并分别指定为module_init和module_exit入口。而且注意一个细节如果用module_platform_driver驱动的probe和remove函数指针是直接放在platform_driver结构体里的。如果你想支持设备树还要记得在.driver.of_match_table里填好匹配表即使整个驱动是编进内核而不是模块这个匹配表也照常生效。有些驱动会额外定义__maybe_unused的pm_ops指针用来支持电源管理但基础示例里可以先不关注这个。9. 驱动测试与验证的实操记录我写这个按键驱动时的完整验证思路是这样的。把生成的key.ko拷贝到开发板依次执行insmod key.ko dmesg | tail -n 20 ls /sys/bus/platform/devices/ ls -l /sys/bus/platform/drivers/imx6ull-key/ cat /proc/interrupts | grep key/proc/interrupts里能看到注册的中断号以及触发的次数。用手按几下按键再查看计数值变化确认中断触发正常。再用应用层验证读取逻辑。因为这里用了miscdevice注册成功后会在/dev下生成imx6ull-key节点cat /dev/imx6ull-key按键按下前读到的是高电平对应的字符按下后读到低电平对应的字符。这个验证方式能直观确认probe里的资源配置全部生效。如果追求更完善的上报方式建议结合input子系统把按键事件上报给应用层这样用evtest工具就能验证。限于篇幅这篇先不展开后续我会专门写一篇i.MX6ULL输入子系统驱动的文章单独讲。10. 一些个人经验和后续可以扩展的方向对照框架学的这些内容再回看之前写的GPIO驱动你会发现自己已经开始区分“设备描述”和“驱动逻辑”了这就是Linux驱动开发入门到进阶最重要的分水岭之一。这块内容后续可扩展的方向太多了。最简单的下一步是给这个按键驱动加poll接口让应用层可以用select来等待按键事件而不是死循环读取。再进阶一点可以改造成input子系统驱动上报EV_KEY事件并且用input_register_device注册输入设备这样应用层就能用标准的/dev/input/eventX接口拿到按键数据。如果要把驱动做成产品级还需要考虑中断底半部处理加锁、防抖延时、以及和设备树的耦合关系如何进一步弱化。每次调试驱动时我习惯把内核的drivers/base/platform.c源码放在手边这个文件是整个Platform机制的核心所有匹配逻辑都在里面。花点时间读一下配合本篇文章提到的几个关键函数遇到了奇怪的问题基本都能定位出来。驱动开发这条路光看教程确实容易晕最好的老师永远是内核心源和实际调试记录。希望这篇文章能帮你把i.MX6ULL的platform设备驱动搞顺手后面再遇到总线匹配的问题至少能自己动手查源码了。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门