尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Linux驱动多设备支持实战:设备树匹配与实例管理技巧

Linux驱动多设备支持实战:设备树匹配与实例管理技巧 在瑞芯微平台上做Linux驱动开发最难缠的问题之一就是“驱动怎么支持多个设备”。我前几年调RK3568的时候板子上接了两颗同型号的温湿度传感器驱动加载完只有挂在i2c-0上的那颗能读取数据i2c-1上那颗死活不出数。后来一查不是传感器坏了而是驱动里用了一个全局指针保存设备状态第二颗设备probe的时候把第一颗的指针覆盖掉了。类似的情况还有换了一颗同功能的兼容芯片型号一变驱动直接不匹配只能去改驱动代码、重新编译内核。这两类问题归结起来就是“一个驱动如何面对多个设备”的设计题今天我把自己的处理经验拆成两个技巧希望能帮到正在做瑞芯微平台Linux驱动适配的工程师。本文的两个技巧分别对应两种常见场景一个驱动兼容多个型号的设备以及一个驱动同时管理多个同类型设备的实例。这两个技巧在RK3568、RK3399、RK3588等平台的BSP驱动和外设驱动里都适用原理基本通用不管你现在用的是i2c、spi还是platform总线思路都能迁移过去。我会把设备树匹配机制、驱动私有数据的组织方式、sysfs导出、常见排查手段都过一遍尽量做到看完就能拿去改你自己的驱动。1. 动手之前先把“支持多个设备”这件事拆清楚很多人一说“驱动支持多个设备”脑子里就开始想“那要不要注册多个设备号要不要写多个fops函数”其实在Linux设备模型里这个问题已经被抽象得很干净了一个driver本来就允许对应多个device关键是怎么让每个device都能正确匹配、正确分配资源、正确被用户空间访问。1.1 在瑞芯微平台上你通常会遇到哪两类“多设备”需求第一类叫“一个驱动支持多型号设备”。比如主板上预留了几种同功能的PHY芯片、电源管理芯片、音频Codec或者触摸IC硬件上通过配置电阻来选择用哪一颗。软件上希望一个驱动模块就能把这几颗都认出来。这种情况虽然也叫“多个设备”但同一板型同一时刻往往只存在其中一颗有些系统也会同时挂多颗但型号不同。它的核心问题是“怎么让驱动能匹配到不同的compatible字符串”。第二类叫“一个驱动管理多个同类型设备实例”。典型例子一块RK板子接了8路同样的ADC芯片、2颗同样的温度传感器、4个相同的PWM风扇控制芯片。它们在设备树里各自占一个节点硬件资源完全不同但Linux下用的是同一个驱动模块。它的核心问题是“同一个probe函数怎么被调用很多次而且每次都管理好自己的那一份数据”。这两种需求不是互斥的。同一个驱动可能既兼容多个型号又同时挂载多个实例。把这两种情况区分开是因为它们的解决侧重点很不一样前者侧重匹配表的设计后者侧重驱动私有数据的管理。1.2 为什么驱动写得不对板子上第二颗芯片就没反应大多数从裸机或模块开发转过来的同学写驱动时脑子里还是“单设备”思路。驱动里放一个全局的struct my_dev *g_devprobe里把设备信息存进去read/write的时候直接取g_dev来用。这种写法在只接一个设备时一切正常一旦设备树里有两个同类型节点第二个节点匹配成功、probe被调用g_dev就被覆盖成第二个设备。最后用户读到的永远是第二颗芯片的数据第一颗芯片好像“消失”了一样但它的驱动节点其实还在只是操作接口已经指向了错误的目标。还有一种常见错误是固定寄存器地址、固定设备名。设备树里明明配了两段寄存器区域驱动却只用devm_platform_ioremap_resource(pdev, 0)去拿第一段第二个节点的寄存器地址完全没有被使用。在x86或一些非设备树平台上可能没这么明显但瑞芯微这类严格依赖设备树运行时配置的平台这类问题很容易暴露。与其靠“if (num 1)”这种临时补丁去堵不如从一开始就把驱动模型按“多设备”来设计也就是下面两个技巧的核心思路。2. 技巧一一套驱动兼容多型号设备先解决“换型号就不认”的问题。很多驱动首次写出来只针对一个芯片后面硬件改版换了供应商就不得不再写一个驱动或者把新代码用#ifdef堆在老代码里。实际上Linux的匹配机制早就支持一个驱动匹配多个设备只需要把匹配表配置好。2.1 of_match_table为什么能让你在同一个驱动里挂多颗芯片有个容易被忽略的事实在Linux设备模型里platform_driver也好i2c_driver也好probe函数从来就不是“一个驱动只能probe一个设备”的约束。一个driver可以对应很多个device内核会为每个匹配成功的设备分别调用probe。那么决定一个驱动能匹配哪些设备、匹配上以后怎么区分型号的正是of_match_table。设备树中每个节点都有compatible属性内核会为每个状态为okay的节点生成一个struct device。在总线匹配阶段内核拿设备节点里的compatible字符串和驱动的of_match_table里每一个.compatible条目做比对只要有一个相等就认为匹配成功然后调用驱动probe。所以of_match_table完全可以放很多条目代表这个驱动愿意接受哪些型号。这里还有一个容易被忽略的设计每个of_device_id条目里都有一个.data字段类型是const void *可以指向一个型号特有的配置结构体。那么当不同型号的设备都probe进同一个驱动时驱动能拿到各自不同的配置数据而不是靠比较字符串硬编码。这正是“一套驱动兼容多型号”的关键。2.2 实操以RK3568平台为例写一个可兼容多型号的驱动假设我们要写一个“供电监控芯片”驱动功能是读取输入电压、电流并支持过压报警。硬件上有两个型号V1和V2寄存器地址不同过压告警阈值也不同。板级DTS里分别这样写/* 板卡A使用V1版本 */ i2c2 { status okay; pmic_mon18 { compatible rockchip,pmic-mon-v1; reg 0x18; interrupt-parent gpio3; interrupts RK_PA5 IRQ_TYPE_LEVEL_LOW; }; };/* 板卡B使用V2版本 */ i2c2 { status okay; pmic_mon19 { compatible rockchip,pmic-mon-v2; reg 0x19; interrupt-parent gpio3; interrupts RK_PA6 IRQ_TYPE_LEVEL_LOW; }; };这两个节点compatible不一样但希望都绑定到同一个驱动。驱动里需要一个匹配表和对应的配置结构体struct pmic_mon_cfg { u32 voltage_reg; u32 current_reg; u32 overvoltage_threshold_mv; const char *model_name; }; static const struct pmic_mon_cfg mon_v1_cfg { .voltage_reg 0x10, .current_reg 0x11, .overvoltage_threshold_mv 5000, .model_name pmic-mon-v1, }; static const struct pmic_mon_cfg mon_v2_cfg { .voltage_reg 0x20, .current_reg 0x21, .overvoltage_threshold_mv 12000, .model_name pmic-mon-v2, }; static const struct of_device_id pmic_mon_of_match[] { { .compatible rockchip,pmic-mon-v1, .data mon_v1_cfg }, { .compatible rockchip,pmic-mon-v2, .data mon_v2_cfg }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, pmic_mon_of_match);注意最后一个条目必须是空结构体作为结束标记。MODULE_DEVICE_TABLE(of, ...)这个宏会把匹配信息导入模块的alias段这样insmod/modprobe时能自动生成modalias否则即使手动加载模块系统也可能没法自动绑定设备。probe函数里获取当前设备型号对应配置推荐直接用内核提供的APIstatic int pmic_mon_probe(struct i2c_client *client, const struct i2c_device_id *id) { const struct pmic_mon_cfg *cfg; struct pmic_mon_dev *pm; cfg of_device_get_match_data(client-dev); if (!cfg id) cfg (const struct pmic_mon_cfg *)id-driver_data; if (!cfg) return -EINVAL; pm devm_kzalloc(client-dev, sizeof(*pm), GFP_KERNEL); if (!pm) return -ENOMEM; pm-client client; pm-cfg cfg; mutex_init(pm-lock); dev_set_drvdata(client-dev, pm); dev_info(client-dev, probe %s, vreg0x%02x\n, cfg-model_name, cfg-voltage_reg); /* 后续所有寄存器读写都按cfg-voltage_reg来不再判断型号字符串 */ return 0; }of_device_get_match_data会从设备节点的匹配项中取出.data指针整个过程不需要自己去遍历匹配表推荐优先使用。如果你写的是老内核4.x之前可能没有这个API那时候可以用of_match_device(pmic_mon_of_match, client-dev)配合手动取data逻辑是一样的。2.3 用driver_data做差异化配置别再写一堆if/else很多驱动兼容性做得差是因为代码里到处是这种判断if (of_device_is_compatible(dev-of_node, rockchip,pmic-mon-v2)) { reg 0x20; } else { reg 0x10; }这样写不是不能用但一旦型号增加到5个以上代码就变得又长又乱而且新增型号时很容易漏改某个分支。更推荐的做法是基于配置结构体让probe函数只关心cfg-xxx不做任何of_device_is_compatible判断。这样做的好处很明显新增一个型号时只需要在驱动里增加一个cfg变量和一条of_device_id条目主流程代码完全不用动。这是Linux内核里很多成熟子系统的通用做法比如drivers/pinctrl里面对不同SoC的支持基本都采用“同驱动不同数据”的结构。还要注意如果驱动被用到非设备树环境i2c_device_id表也建议保留里面的driver_data同样可以指向cfgstatic const struct i2c_device_id pmic_mon_id[] { { pmic-mon-v1, (kernel_ulong_t)mon_v1_cfg }, { pmic-mon-v2, (kernel_ulong_t)mon_v2_cfg }, { } }; MODULE_DEVICE_TABLE(i2c, pmic_mon_id);这样在ACPI、静态板文件等兼容路径下也能工作。很多瑞芯微BSP提供的驱动为了兼容老式内核和不同启动方式会同时维护of_match_table和id_table就是这个原因。2.4 兼容性设计的几个经验这块是我实际踩坑后总结的几点经验。第一点compatible命名建议用“厂商,型号”格式。瑞芯微官方设备树里大量使用rockchip,前缀第三方芯片也要遵守这个规范别只写一个“gpio-wakeup”之类的通用名后续不同驱动之间冲突了很难排查。第二点不要把版本区分放在reg上。reg代表硬件地址或编号换了型号后地址可能完全一样靠reg区分型号非常脆弱。型号差异应该体现在compatible字符串或者数据字段里。第三点如果硬件上用配置电阻区分型号强烈建议在bootloader阶段读取GPIO然后在设备树中动态修改compatible属性让内核驱动匹配表保持不变。这种做法在现场维护时非常实用不需要为每种硬件配置重新编译内核用户只需要更新dts即可。第四点模块加载后如果发现设备没有被驱动绑定先检查MODULE_DEVICE_TABLE是否编写正确。这个宏会生成模块的alias信息缺少它时modprobe可能无法自动加载即使手动insmod成功系统也可能无法自动完成绑定。3. 技巧二用一个驱动管理多个同类型设备实例解决了“多个型号”的问题再看“多个实例”的问题。这块比匹配表更隐蔽因为很多驱动在单实例下怎么测都没问题一旦挂两个同型号设备数据就互相串扰或者只有最后一个设备响应。3.1 一次probe只代表一个设备别把状态写成全局变量内核原则很简单设备树里有多少个匹配且状态为okay的节点probe就会被调用多少次。比如在i2c3上挂两个同型号温度传感器compatible相同那么probe一定被调用两次。这两次调用里client指针不同dev指针也不同。很多人在写驱动时习惯放一个全局变量static struct tmp512_dev *g_priv;第一次probe时g_priv priv1第二次probe时g_priv priv2。于是第一个设备的私有数据失去了引用用户空间所有read操作实际都在操作priv2。如果两个设备地址不同、工作状态也不一样就会出现“第二个设备正常第一个设备怎么读都是第二个设备的数据”这种极其迷惑的现象。解决办法是让数据跟着设备走而不是跟着驱动走。每个probe函数内部用devm_kzalloc分配独立的私有结构体然后用dev_set_drvdata把它挂到当前设备上任何回调函数里需要用哪个设备的数据就从对应的struct device或者file里取。3.2 实例数据怎么组织以两个温度传感器为例DTS可以这样写i2c3 { status okay; clock-frequency 400000; temp0: temperature48 { compatible toshiba,tmp512; reg 0x48; }; temp1: temperature4a { compatible toshiba,tmp512; reg 0x4a; }; };注意两颗芯片挂在同一条i2c总线上地址不同是合法的。驱动私有结构体我习惯命名为xxx_dev内容大致如下struct tmp512_dev { struct i2c_client *client; struct mutex lock; struct miscdevice misc; int index; int last_temp; };probe里只初始化这些不在函数里定义任何static可变变量除非是真正全局只读的驱动配置。每次probe分配一份static int tmp512_probe(struct i2c_client *client, const struct i2c_device_id *id) { struct tmp512_dev *priv; struct device_node *np client-dev.of_node; int index 0; of_property_read_u32(np, reg-index, index); priv devm_kzalloc(client-dev, sizeof(*priv), GFP_KERNEL); if (!priv) return -ENOMEM; priv-client client; priv-index index; mutex_init(priv-lock); dev_set_drvdata(client-dev, priv); /* 注册miscdevice一个实例一个节点 */ priv-misc.minor MISC_DYNAMIC_MINOR; priv-misc.fops tmp512_fops; priv-misc.parent client-dev; snprintf(priv-misc.name, sizeof(priv-misc.name), tmp512-%d, index); return misc_register(priv-misc); }这里有个小细节reg-index是我自定义的属性用来区分实例编号。你完全可以用设备树标准的reg属性i2c子节点的reg通常就是芯片地址比如0x48、0x4a拿来当编号也可以。只是如果同一个总线上多个设备地址跨度较大用reg直接当编号会导致设备名不连续所以我更习惯加一个自定义属性来专门管理实例号。3.3 给每个实例分配唯一编号并导出sysfs多实例驱动最怕的就是“分不清设备”。我在前文用了reg-index在DTS里这样加temp0: temperature48 { compatible toshiba,tmp512; reg 0x48; reg-index 0; }; temp1: temperature4a { compatible toshiba,tmp512; reg 0x4a; reg-index 1; };当然也可以用更规范的方式在根节点aliases里定义索引aliases { temperature0 temp0; temperature1 temp1; };驱动里用of_alias_get_id(np, temperature)也能拿到编号。两种方式都可以看你的习惯。瑞芯微的很多BSP驱动里用aliases的场景很常见特别是需要固定设备名的多媒体设备、网络设备。拿到编号之后注册miscdevice时name必须是唯一的不能所有实例都叫tmp512。否则第二个设备注册时会返回-EEXIST导致第二个实例不可用。上面已经写了snprintf生成tmp512-0、tmp512-1这种名字这样在/dev/下就能看到两个独立节点同时/sys/class/misc/下也会有对应的两个目录。如果你希望每个实例在sysfs里暴露更多信息比如实时温度、报警阈值可以在probe里创建属性文件static ssize_t temp_show(struct device *dev, struct device_attribute *attr, char *buf) { struct tmp512_dev *priv dev_get_drvdata(dev); return sysfs_emit(buf, %d\n, tmp512_read_temp(priv)); } static DEVICE_ATTR_RO(temp); static int tmp512_probe(...) { ... device_create_file(client-dev, dev_attr_temp); ... }这样每个i2c设备在/sys/bus/i2c/devices/3-0048/下都有自己的temp属性调试时单独读、单独对比非常清晰。3.4 多实例并发访问的坑多实例驱动最容易翻车的地方不是probe阶段而是运行期并发。用户可能同时打开/dev/tmp512-0和/dev/tmp512-1分别读温度。如果驱动为了省事把锁定义成全局锁两个设备读操作就会互相阻塞功能没错但吞吐量没了还会出现“为什么一边卡住另一边也卡住”的奇怪现象。更好的做法是每个实例单独一把锁就像上面结构体里那个struct mutex lock。另一个常见坑是中断处理。如果在request_irq时传入的context参数用了同一个全局变量那么当第二颗芯片产生中断时中断服务函数操作的却是第一颗芯片的私有数据轻则状态错乱重则访问到已经被释放的资源。正确做法是把每份私有数据作为参数传给自己的中断处理函数ret devm_request_irq(client-dev, client-irq, tmp512_irq_handler, IRQF_TRIGGER_FALLING, priv-misc.name, priv);中断处理函数里拿到的void *data就是当前实例的priv不会再出现“中断串线”。还有一点容易被忽略如果驱动里注册miscdevice时复用了同一个fops结构体其实没问题因为内核会根据file-private_data定位到不同实例。关键是open函数里要正确设置private_data。我通常的做法是在miscdevice的open回调里用container_of拿到私有数据static int tmp512_open(struct inode *inode, struct file *file) { struct tmp512_dev *priv container_of(file-private_data, struct tmp512_dev, misc); file-private_data priv; return 0; }如果不太清楚file-private_data什么时候被填充可以打印确认一下。这个细节不处理好就会出现两个节点都打开成功但读写数据时操作对象错乱。4. 瑞芯微平台实测排障记录与速查表多设备驱动写完后联调阶段一定会遇到各种“设备没绑定”“数据不对”的现场问题。这里把我在瑞芯微平台上常见的问题整理成一张速查表再讲一个典型的排查过程。4.1 常见故障现象与排查方向下面这些场景我都在实际项目中遇到过整理成表格方便你对照现象最常见根因快速排查手段第二个设备节点没有被probecompatible不匹配、status为disabled、设备树节点未生成dmesg查驱动日志ls /sys/bus/i2c/devices/看节点probe执行成功但只有最后一个设备能工作全局指针被覆盖搜索驱动里有没有static变量保存设备指针miscdevice注册失败返回-EEXIST设备名重复检查miscdevice的name是否包含唯一编号操作设备A但影响设备B共用了同一个全局锁或全局数据检查锁和缓冲区是不是每个实例独立一份中断总是响应到第一颗芯片request_irq传入的context是全局变量查看irq handler的data参数来源i2cdetect看得到两颗芯片但sysfs里只有一个实例重复注册逻辑有问题或模块只有一个name检查probe里是否把miscdevice放在循环/全局位置设备树节点有两个但串口日志只有一次probe第二个节点status默认是disabled检查/proc/device-tree或DTS里status字段4.2 快速定位几条命令和日志调这类问题我一般先用命令确认设备树和设备绑定关系再决定要不要改代码。下面是几个最常用的命令。查看i2c总线上实际挂载的设备i2cdetect -y 3查看某个驱动绑定了多少个设备ls -l /sys/bus/i2c/drivers/tmp512/如果驱动在platform bus上就换成ls -l /sys/bus/platform/drivers/your_driver/查看i2c或platform下的设备节点是否生成ls -l /sys/bus/i2c/devices/查看DTS解析结果确认某个节点状态cat /proc/device-tree/i2c3/temperature48/status cat /proc/device-tree/i2c3/temperature4a/compatible查驱动probe日志和错误信息dmesg | grep -E tmp512|i2c3查看中断是否注册到了正确实例cat /proc/interrupts | grep tmp512这些命令组合起来基本能把“设备树问题”和“驱动问题”快速区分开。4.3 从失败到补齐一个真实排查流程讲一个我印象很深的例子。当时RK3568板子上按DTS配置了两个tmp512传感器但系统起来后/dev/下只有一个tmp512-0节点。我按下面的顺序排查先看dmesg | grep tmp512发现日志里只有tmp512-0 probe的记录完全没有第二个实例的probe输出。这说明问题很可能不在驱动逻辑而在设备树或匹配阶段。再看ls /sys/bus/i2c/devices/发现3-004a这个目录根本没有生成。接着检查设备树原始文件发现写第二个节点的地方确实缩进没问题但父节点i2c3没有打开或者被某个status disabled覆盖了。这类问题在多人合改DTS时特别容易发生改完一处忘了另一处。把DTS修正后重启系统两个节点都出现了。但此时发现两个/dev/tmp512-*都能打开读出来的温度却始终一样。用i2cdetect确认两颗芯片地址不同也确实都存在问题就出在驱动内部。继续查发现驱动里有一个全局static struct tmp512_dev *g_priv第二次probe时覆盖了第一次的指针。把所有全局状态改成实例私有数据后两个节点终于各读各的互不干扰。这类问题排查起来并不复杂难的是意识到“全局变量”才是元凶。所以我在写驱动时给自己定了一条规矩probe里不允许出现static修饰的设备状态变量所有实例相关数据都必须挂到dev私有数据上这样能避免90%的多实例串扰问题。5. 延伸到i2c/spi/regmap驱动的一些体会这两个技巧虽然是用瑞芯微平台举例但换到i2c、spi、regmap、misc等子系统同样适用。比如regmap驱动多个实例共享同一个regmap配置时要注意.regmap必须每个实例创建一份不能用全局复用的方式来管理不同设备的寄存器映射。很多瑞芯微的Codec、PMIC驱动都踩过这个坑。我个人的习惯是写一个新的多设备驱动时先画出私有结构体把“哪些数据是每实例一份、哪些数据是所有实例共享、哪些数据是驱动级只读”列清楚再开始写probe。每实例一份的字段放进私有结构体共享数据用引用计数或单独封装。这个习惯帮我省了很多现场调试时间特别是在RK3588这种外设特别多、经常要同时挂多路Camera或多路以太网的平台上。最后分享一个小技巧无论驱动能不能正常工作尽量在驱动里多放几条dev_info和dev_err日志并且统一前缀。多设备场景下日志里的dev_name会自动带上总线地址比如3-0048和3-004a光看日志就能知道当前在操作哪个实例。这比在驱动里到处打印num%d要可靠得多也让现场定位问题的时间大幅缩短。
返回列表