Linux驱动开发:用of_match_table与私有数据实现多设备实例支持
做瑞芯微平台驱动开发的朋友对这类场景应该不陌生同样是I2C总线板子上焊了两片同型号的温湿度传感器同样是SPI主控下面挂了4片ADC甚至RK3588这种多核SoC里光串口控制器就好几个。如果每个实例都单独写一个驱动那代码量直接爆炸维护起来更是噩梦。我最初在RK3288上做双Camera项目时就踩过这个坑同一款Sensor前后各一颗愣是写了两个差不多的驱动文件后来发现一个驱动完全能handle住。这篇文章就专门聊聊在Linux驱动开发中支持多个设备实例的两个核心技巧怎么让一个driver匹配多个设备节点以及probe之后怎么确保每个实例的数据不串味。这两个东西搞明白瑞芯微平台上的多路外设驱动基本都能写得干净又稳。1. 内容整体设计与思路拆解1.1 为什么一个驱动支持多个设备是Linux驱动开发的必修课很多刚开始写驱动的朋友脑子里的模型还是单片机时代的样子一个外设对应一个驱动文件驱动里写死寄存器地址、写死中断号、写死GPIO。这套思维搬到Linux内核里第一个问题就是没法做通用适配。Linux的设备模型是分开的device表示硬件实体driver表示驱动逻辑两边通过bus如platform_bus、i2c_bus、spi_bus来匹配。所以理论上一个driver可以对应多个device这是内核的底层设计机制决定的不是硬凑出来的技巧。在瑞芯微平台上这个需求尤其明显。比如RK3566/RK3568的SDIO接口可以同时接WiFi模组和SD卡二者共用一套控制器驱动RK3588的I2C控制器有好几路每路都可以接不同的外设。如果驱动不支持多实例直接用全局变量存寄存器映射地址第二个设备probe的时候就把第一个设备的信息冲掉了。轻则功能异常重则内核panic这种事我在实际调试中见过太多次。1.2 拆解多设备支持的实际场景与核心难点根据瑞芯微Linux SDKkernel 4.19/5.10/6.1的常见用法多设备支持主要分两类一类是同总线同型号多实例。比如I2C0上挂两片AT24C02或者SPI总线上挂两片MCP3008。这类设备在设备树里是两个不同的node但compatible完全一样。驱动的probe会被调用两次每次传入一组独立的platform_device/i2c_client资源各自独立。另一类是同型号多控制器实例。比如瑞芯微SoC内部有多个I2C控制器、多个SPI控制器、多个UART。设备树里写i2c0、i2c1、i2c2……虽然硬件IP相同但寄存器基地址、中断号都不一样。驱动要能通过传入的struct resource或设备树属性来区分当前操作的是哪一路控制器。核心难点在于probe函数中获取的本实例资源寄存器地址、中断号、GPIO、时钟要存在每个实例自己的空间里而不是存到全局变量。数据读写、中断处理时要从传入的参数比如i2c_client、platform_device反推出对应的私有数据结构。设备树节点的compatible、reg、中断属性等要在probe时准确读取不能读串。搞清楚这几个点两个技巧自然就浮现出来了。2. 核心细节解析与实操要点2.1 技巧一用of_match_table与compatible实现一对多匹配Linux驱动与设备节点的匹配方式有好几种但瑞芯微平台基于设备树最常用的是of_match_table也就是通过设备树节点里的compatible属性来匹配。设备树里这么写i2c0 { status okay; clock-frequency 400000; temp_sensor0: tmp11748 { compatible ti,tmp117; reg 0x48; }; temp_sensor1: tmp11749 { compatible ti,tmp117; reg 0x49; }; };两个节点compatible相同address不同。然后驱动里只需要声明一次匹配表static const struct of_device_id tmp117_of_match[] { { .compatible ti,tmp117 }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, tmp117_of_match); static struct i2c_driver tmp117_driver { .probe tmp117_probe, .remove tmp117_remove, .id_table tmp117_id_table, .driver { .name tmp117, .of_match_table tmp117_of_match, }, };内核的i2c-core会自动把总线上的0x48和0x49两个client分别与该driver匹配probe执行两次每次传入各自的struct i2c_client其中client-addr分别是0x48和0x49client-dev.of_node指向各自的设备树节点。这个机制的关键点在于of_match_table解决的是哪些设备归我管的问题而真正区分现在管的是哪个设备的是probe传入的struct device指针它封装了当前实例的全部上下文信息。很多新人会犯一个错误在probe里用of_find_node_by_name或of_find_compatible_node去手动找设备树节点。这非常坑因为这类全局搜索函数返回的可能是任意一个匹配节点多实例时就乱了。正确做法是直接使用传入的client-dev.of_node它就是当前这个实例对应的节点不需要你再去找。如果你做的是纯platform驱动比如驱动瑞芯微的某个内部外设控制器匹配表同样适用但要注意compatible的取名规范watchdog: wdtff848000 { compatible rockchip,rk3568-wdt; reg 0x0 0xff848000 0x0 0x100; interrupts GIC_SPI 42 IRQ_TYPE_LEVEL_HIGH; };驱动里用rockchip,rk3568-wdt去匹配即可。而如果这颗SoC有多个WDT控制器每个控制器有独立node同样是probe多次、每次资源不同。2.2 技巧二probe时把资源与私有数据绑定禁止裸用全局变量匹配问题解决之后第二个关键问题就是数据隔离。我先直接说结论一个支持多实例的驱动原则上所有per-device的数据都不能放全局变量。推荐做法是定义一个私有数据结构体统称xxx_dev或xxx_data里面放这个实例专属的内容struct tmp117_data { struct i2c_client *client; struct regmap *regmap; struct device *dev; int irq; u32 vref_mv; struct hwmon_chip_info *chip_info; };probe时用devm_kzalloc为当前实例分配一块独立内存把从设备树读到的资源都存进去然后用dev_set_drvdata或i2c_set_clientdata把这个结构体和当前设备绑定static int tmp117_probe(struct i2c_client *client) { struct device *dev client-dev; struct tmp117_data *data; u32 vref_mv; int ret; data devm_kzalloc(dev, sizeof(*data), GFP_KERNEL); if (!data) return -ENOMEM; >static int rk_xxx_probe(struct platform_device *pdev) { struct device *dev pdev-dev; struct rk_xxx_data *data; struct resource *res; data devm_kzalloc(dev, sizeof(*data), GFP_KERNEL); if (!data) return -ENOMEM; >temp_sensor0: tmp11748 { compatible ti,tmp117; reg 0x48; label pa_temp; }; temp_sensor1: tmp11749 { compatible ti,tmp117; reg 0x49; label power_temp; };3.2 驱动框架与多实例注册过程驱动主体是标准i2c_driver重点是probe怎么写。先看匹配表static const struct i2c_device_id tmp117_id_table[] { { tmp117, 0 }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(i2c, tmp117_id_table); static const struct of_device_id tmp117_of_match[] { { .compatible ti,tmp117 }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, tmp117_of_match); static struct i2c_driver tmp117_driver { .driver { .name tmp117, .of_match_table tmp117_of_match, }, .probe tmp117_probe, .remove tmp117_remove, .id_table tmp117_id_table, };这里同时注册了i2c_device_id和of_match_table。前者的作用是为了兼容非设备树的场景设备树平台主要靠后者。不过要注意如果开了CONFIG_OFof_match_table优先级更高。probe函数里核心逻辑是建立实例上下文并完成硬件初始化static int tmp117_probe(struct i2c_client *client) { struct device *dev client-dev; struct tmp117_data *data; const char *label; u32 vref_mv; int ret; /* 1. 分配本实例私有数据 */ data devm_kzalloc(dev, sizeof(*data), GFP_KERNEL); if (!data) return -ENOMEM; >static int tmp117_read(struct device *dev, enum hwmon_sensor_types type, u32 attr, int channel, long *val) { struct tmp117_data *data dev_get_drvdata(dev); unsigned int regval; int ret; switch (attr) { case hwmon_temp_input: ret regmap_read(data-regmap, TMP117_REG_TEMP, regval); if (ret) return ret; *val sign_extend32(regval, 15) * 1000 / 16; return 0; default: return -EOPNOTSUPP; } }注意这里读取时用的是data-regmap这个regmap由regmap_init_i2c创建时已经绑定了当前实例的i2c_client因此读的时候地址就是对应实例的地址。这就是私有上下文的价值所在即使两个实例并排工作读到0x48的就是PA温度读到0x49的就是电源温度寄存器操作互不干扰。3.3 中断与异步事件的多实例处理细节很多驱动不止做轮询读取还要处理中断。比如TMP117支持温度告警中断两个传感器分别把ALERT引脚接到RK3568的两个GPIO设备树里各自声明interrupts属性temp_sensor0: tmp11748 { compatible ti,tmp117; reg 0x48; label pa_temp; interrupt-parent gpio3; interrupts RK_PA2 IRQ_TYPE_EDGE_FALLING; }; temp_sensor1: tmp11749 { compatible ti,tmp117; reg 0x49; label power_temp; interrupt-parent gpio3; interrupts RK_PA3 IRQ_TYPE_EDGE_FALLING; };probe里这样申请中断>static irqreturn_t tmp117_irq_handler(int irq, void *dev_id) { struct tmp117_data *data dev_id; unsigned int status; regmap_read(data-regmap, TMP117_REG_CONFIG, status); if (status TMP117_CONFIG_ALERT) { /* 上报高温事件 */ sysfs_notify(data-dev-kobj, NULL, temp1_input); } regmap_update_bits(data-regmap, TMP117_REG_CONFIG, TMP117_CONFIG_ALERT, 0); return IRQ_HANDLED; }从这里能看出多实例驱动的安全性中断handler拿到的dev_id是probe时传入的data指向当前中断对应的那个实例。GPIO3_PA2中断进来时data是temp_sensor0的上下文GPIO3_PA3中断进来时data是temp_sensor1的上下文。如果写成全局变量dev_id两个中断同时到达时根本无法确定操作的是哪个设备。3.4 从sysfs验证多实例是否工作正常驱动加载后可以在板子上执行ls /sys/class/hwmon/正常会看到hwmon0和hwmon1分别对应两片TMP117。再用cat /sys/class/hwmon/hwmon0/temp1_input cat /sys/class/hwmon/hwmon1/temp1_input两颗传感器的温度值会各自独立更新。这里需要说明内核注册hwmon设备时hwmon0/hwmon1的编号取决于枚举顺序不是固定的。如果应用层需要固定映射,建议通过设备树alias或者在驱动里使用label属性辅助用户空间识别。也可以读取cat /sys/class/hwmon/hwmon0/name cat /sys/class/hwmon/hwmon1/name都显示tmp117然后用temp1_input和label关联cat /sys/class/hwmon/hwmon0/label cat /sys/class/hwmon/hwmon1/label如果label在probe里保存且通过hwmon注册时的chip_info提供这里就能看到pa_temp和power_temp方便应用层精确定位。实测中我遇到过一个有意思的现象有时候hwmon0对应的是addr0x49的传感器因为内核枚举顺序和设备树地址编址有关不是严格按照reg排序。所以应用层千万别写死hwmonX的编号用label或自定义属性来识别才是稳妥做法。4. 常见问题与排查技巧实录4.1 两个节点只probe了一次另一个设备没绑定成功这个是最常见的。排查思路按顺序来先看设备树节点有没有被正确识别。在板子上ls /sys/bus/i2c/devices/如果只看到0-0048没有0-0049说明设备树枚举阶段就漏了一个。大概率是I2C节点里device的status写错了或者reg地址冲突、总线上实际没焊、被内核跳过。如果两个都在但只有其中一个绑定了驱动cat /sys/bus/i2c/devices/0-0049/name ls -l /sys/bus/i2c/devices/0-0049/driver如果name是tmp117但driver是空的说明kernel的of_match_table没有匹配上。检查compatible字符串是不是完全一致特别注意设备树里的写法是ti,tmp117还是tmp117多一个前缀少一个前缀都匹配不上。还有一个隐蔽问题如果驱动里of_match_table没写MODULE_DEVICE_TABLE宏且驱动编译成模块.ko那么modprobe的时候模块不会自动加载导致设备存在但没有任何驱动被绑定。这是新手最容易忽略的坑。4.2 两个设备共用一个中断号导致的混乱某些场景下两个设备的中断信号会被设计成并联到同一个GPIO。这种情况下两个实例的client-irq会是同一个中断号。如果probe各自request_irq同一个中断号第二个会失败或者必须用IRQF_SHARED共享标志。实测中如果硬件设计没法改可以用中断状态寄存器来区分是谁触发的但一定要申请共享中断ret devm_request_threaded_irq(dev,>dev_info(data-dev, data%px regs%px addr0x%02x\n, data,>static const struct of_device_id rk_xxx_of_match[] { { .compatible rockchip,rk3568-xxx, .data (void *)RK3568 }, { .compatible rockchip,rk3588-xxx, .data (void *)RK3588 }, { /* sentinel */ } };这条匹配表里可以携带.data字段probe时通过of_device_get_match_data(dev)拿到是哪个SoC版本从而做差异化的寄存器和初始化。这又是多设备匹配的一个进阶技巧同一个驱动支持不同型号的同类芯片在瑞芯微不同芯片平台移植时特别有用。实测中这种方式省掉了大量#ifdef CONFIG_SOC_RK3568之类的条件编译代码干净很多。我后来在RK3568和RK3588上复用同一份GPIO扩展驱动就是用的这个模式改动量很小。5. 一点个人体会与延伸建议写驱动这些年最大的体会是Linux设备模型本身就是一套精密的面向对象设计device是对象实例driver是类方法匹配表是虚函数表。你顺着这个思路去写多实例支持就是水到渠成的事逆着来非要用全局变量硬扛越到后期越痛苦。瑞芯微平台因为SoC集成度高、外设控制器多多实例场景特别密集。无论是I2C、SPI、UART还是内部的PWM、ADC、WDT都会用到同样的套路。建议新手多读几遍SDK里现成的多实例驱动比如pinctrl-rockchip.c和i2c-rk3x.c结合这里的两个技巧去对照理解很快就能建立起正确的内核驱动开发心智模型。另外还有一个非常实用的调试技巧如果多实例驱动运行中怀疑数据串了在关键路径加一个dump_stack或者ftrace确认当前上下文绑定的device节点到底是谁。我见过不少诡异问题最后定位下来都是中断上下文拿错了data指针。只要每个入口都从参数反推实例而不是从全局量猜实例这类问题基本能避免。最后再分享一个配置层面的习惯多实例的设备树节点尽量用有意义的label比如dsi0_backlight、dsi1_backlight比单纯用reg地址可读性强太多。设备树是给人看的也是给内核用的注释和命名规范一点后面接手的人会少很多无谓的排查时间。

相关新闻

最新新闻

日新闻

周新闻

月新闻