FEATURED · 精选文章

瑞芯微Linux驱动一驱多:设备树匹配表与私有数据隔离实战

发布时间 / 2026/9/7 13:48:04
来源 / 创域科博编辑部
栏目 / 资讯中心
瑞芯微Linux驱动一驱多:设备树匹配表与私有数据隔离实战 做瑞芯微平台的Linux驱动开发尤其是RK3568、RV1106这类SoC之后你会发现一个很现实的场景频繁出现同一个驱动要管的不止一个设备。要么是I2C总线上挂了两个同型号的触摸IC要么是同一个主控外接多路Sensor要么是产品硬件迭代后不同版本之间寄存器地址还不一样。“一个驱动只服务一个设备”这种写法在真实产品里根本不抗造改到怀疑人生。所以我想把自己在瑞芯微平台上简化这类问题的两个小技巧整理出来一个是关于设备树匹配表如何“一驱多型”另一个是关于驱动私有数据如何“一驱多实例”。这两个点几乎涵盖了我在多个项目里遇到的80%“一个驱动支持多个设备”的需求适用新手也适合给正在被硬件版本兼容折腾的人参考。1. 场景与思路为什么“一驱多”会成为瑞芯微开发里的高频需求1.1 我在实际项目中遇到的两类典型场景第一类场景是硬件叠屏或者多Sensor共存。我做过一个RK3568的工控板底板上要同时接两颗I2C接口的触摸屏型号一样挂在两条不同的I2C总线上。更麻烦的是这两个屏幕的固件版本还不完全一致其中一个需要通过驱动配置不同的偏移量和采样率。如果驱动代码里用全局变量保存设备状态那这两颗IC的配置参数就会互相覆盖表现为触摸坐标飘、灵敏度不一致非常难排查。再比如音频Codec、温湿度传感器、ADC采集芯片只要板子上出现“同型号二连”全局变量方案基本就会爆雷。第二类场景是硬件版本兼容。消费类产品常会做BOM切换比如同一定位用两颗不同厂商的触摸芯片封装兼容功能相近但寄存器和初始化序列有差别。如果你复制一份驱动改成xxx_v2.c再往内核里塞短期能跑中期维护就是灾难设备树、config、Makefile、休眠唤醒回调到处都是重复代码。正确做法是驱动主体共用差异通过匹配表里的数据指针区分一个.ko文件搞定所有版本。1.2 这两个技巧分别对应哪个痛点技巧一解决的是“驱动代码怎么写才能匹配多个硬件型号”。核心就是设备树compatible字段与驱动侧of_device_id/i2c_device_id表的关系以及怎么利用.data字段把不同型号的差异配置传进同一个probe。技巧二解决的是“同型号设备挂多个实例时驱动数据怎么隔离开”。核心是让每个设备实例拥有独立的私有数据结构体避免全局变量串数据同时用miscdevice动态次设备号让每个实例有自己独立的设备节点。这两件事看起来独立实际项目里经常要同时用。尤其是瑞芯微这类SoCI2C、SPI、GPIO资源丰富板级设计喜欢堆外设“一驱多”几乎是标配需求。2. 技巧一让一个驱动匹配多个硬件型号2.1 of_match_table 的结构设计Linux驱动的匹配机制里设备树节点通过compatible字符串与驱动侧of_device_id表建立联系。这个表不是摆设除了匹配它还有一个很实用的字段data。你可以把每个型号的差异配置定义成结构体然后在of_device_id里把结构体地址填进.data。以触摸屏驱动为例假设我们需要支持两个兼容型号分别是rockchip,touch-a和rockchip,touch-b它们的初始化参数完全不同。驱动侧先定义两份配置enum { TOUCH_MODEL_A 0, TOUCH_MODEL_B, }; struct rockchip_touch_cfg { int model; uint32_t settle_delay_ms; uint32_t sample_rate; uint32_t calib_offset_x; }; static const struct rockchip_touch_cfg touch_cfg_a { .model TOUCH_MODEL_A, .settle_delay_ms 20, .sample_rate 100, .calib_offset_x 0, }; static const struct rockchip_touch_cfg touch_cfg_b { .model TOUCH_MODEL_B, .settle_delay_ms 50, .sample_rate 60, .calib_offset_x 4, };然后写匹配表static const struct of_device_id rockchip_touch_of_match[] { { .compatible rockchip,touch-a, .data touch_cfg_a, }, { .compatible rockchip,touch-b, .data touch_cfg_b, }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, rockchip_touch_of_match);这里有个很容易忽略的细节.compatible字符串必须和设备树里保持完全一致一个字符都不能差包括大小写和连字符。很多匹配失败的问题最后查下来就是设备树里写touch_a驱动里写touch-a下划线和连字符不一样导致内核直接不认。2.2 probe 中如何根据匹配项区分硬件配置probe函数里怎么拿到这份配置呢可以用of_match_device()static int rockchip_touch_probe(struct platform_device *pdev) { const struct of_device_id *match; const struct rockchip_touch_cfg *cfg; struct rockchip_touch_dev *touch; match of_match_device(rockchip_touch_of_match, pdev-dev); if (!match || !match-data) { dev_err(pdev-dev, no matching device config found\n); return -ENODEV; } cfg match-data; touch devm_kzalloc(pdev-dev, sizeof(*touch), GFP_KERNEL); if (!touch) return -ENOMEM; touch-cfg cfg; dev_set_drvdata(pdev-dev, touch); dev_info(pdev-dev, touch model %d, sample_rate %u\n, cfg-model, cfg-sample_rate); return 0; }如果你用的是I2C、SPI这类总线驱动表名和获取方式会略有差异。I2C设备驱动用的是i2c_device_id表匹配逻辑也支持.driver_data传参static const struct i2c_device_id rockchip_touch_i2c_id[] { { rockchip,touch-a, (kernel_ulong_t)touch_cfg_a }, { rockchip,touch-b, (kernel_ulong_t)touch_cfg_b }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(i2c, rockchip_touch_i2c_id);在probe里通过i2c_match_id()或者直接用参数idstatic int rockchip_touch_i2c_probe(struct i2c_client *client, const struct i2c_device_id *id) { const struct rockchip_touch_cfg *cfg NULL; if (id id-driver_data) cfg (const struct rockchip_touch_cfg *)id-driver_data; if (!cfg) { const struct of_device_id *match; match of_match_device(rockchip_touch_of_match, client-dev); if (match match-data) cfg match-data; } if (!cfg) { dev_err(client-dev, no config for this device\n); return -ENODEV; } ... }为什么要把两套方式都写上因为内核匹配路径不是唯一的。设备树引导时走of_match_table但有些平台走i2c_device_id或者ACPI代码里两种都兼容驱动挂在不同平台时就不用再改。瑞芯微平台虽然以设备树为主但某些内部外设或者旧内核版本里两条路径都校验一下更稳。2.3 设备树侧如何配合设备树里需要写不同的compatible字符串来区分型号i2c3 { status okay; pinctrl-names default; pinctrl-0 i2c3_xfer; touch_a: touch1a { compatible rockchip,touch-a; reg 0x1a; interrupt-parent gpio3; interrupts RK_PB5 IRQ_TYPE_LEVEL_LOW; }; touch_b: touch2a { compatible rockchip,touch-b; reg 0x2a; interrupt-parent gpio3; interrupts RK_PB5 IRQ_TYPE_LEVEL_LOW; }; };注意reg不能相同否则I2C子系统会认为地址冲突第二个节点直接probe失败。touch1a这类node-name里的地址只是给人看的真正起作用的是reg属性。这个方案的精髓在于新硬件版本来了只需要加一份config结构体、一行匹配表再在设备树里把compatible换掉驱动主体一行不用动。我后来做固件兼容时一个驱动支持三四个外设版本就是这么扛过来的。3. 技巧二让一个驱动同时管理多个设备实例3.1 为什么要实例化私有数据很多刚从单片机和裸机开发转过来的朋友写Linux驱动时习惯用全局变量存设备状态比如static struct i2c_client *g_client; static int g_irq; static uint8_t g_buffer[1024];单设备的时候没问题一旦I2C总线上同时挂两个同型号芯片就会发生第二个设备probe的时候把g_client覆盖了第一个设备的读写回调拿到的却是第二个设备的i2c_client数据错乱、中断风暴、读写超时全来了。调试半天还以为硬件坏了其实就是驱动状态没隔离。Linux内核驱动的惯例是每个设备实例在probe时分配一块独立的私有数据保存这个实例的client、irq、锁、缓冲区、设备节点信息等再用i2c_set_clientdata()或者dev_set_drvdata()把它和内核设备对象挂钩。后续任何回调里都能拿回这块数据。struct rockchip_touch_dev { struct i2c_client *client; const struct rockchip_touch_cfg *cfg; struct miscdevice mdev; struct mutex lock; unsigned int instance_no; /* 该实例专属状态 */ bool suspended; uint32_t last_x; uint32_t last_y; };probe里这样初始化static int rockchip_touch_i2c_probe(struct i2c_client *client, const struct i2c_device_id *id) { struct rockchip_touch_dev *touch; int ret; touch devm_kzalloc(client-dev, sizeof(*touch), GFP_KERNEL); if (!touch) return -ENOMEM; touch-client client; mutex_init(touch-lock); i2c_set_clientdata(client, touch); /* 后续所有读写都从 client-dev 拿私有数据 */ ... return 0; }这里用devm_kzalloc而不是裸kzalloc好处是设备卸载或probe失败时内核会自动释放内存不需要在remove里手动kfree也能避免忘了释放导致的内存泄漏。3.2 miscdevice 动态分配次设备号实现一驱多节点设备实例多了之后/dev下的访问节点也要跟着区分。如果只用register_chrdev()注册一个主设备号两个同型号设备就只能共享同一个节点用户态根本无法区分打开的是哪个设备。我更推荐的做法是给每个实例注册一个miscdevice次设备号用MISC_DYNAMIC_MINOR由内核动态分配。这样每个设备实例都能在/dev下拥有独立节点比如/dev/rockchip_touch0、/dev/rockchip_touch1。static int rockchip_touch_register_misc(struct rockchip_touch_dev *touch) { int ret; touch-mdev.minor MISC_DYNAMIC_MINOR; touch-mdev.name devm_kasprintf(touch-client-dev, GFP_KERNEL, rockchip_touch%d, touch-instance_no); touch-mdev.fops rockchip_touch_fops; touch-mdev.parent touch-client-dev; touch-mdev.mode 0666; ret misc_register(touch-mdev); if (ret) { dev_err(touch-client-dev, failed to register miscdevice: %d\n, ret); return ret; } return 0; }instance_no哪里来可以在probe里用一个全局计数器递增也可以用设备树里的reg地址或者别名。我用得最多的是设备树alias比如给每个节点加serial-number或者直接用devm_kasprintf结合dev_name()保证节点名不重复。用全局计数器要注意设备拔掉再插上时编号可能变如果用户态有持久化依赖节点名的配置建议用固定的reg地址推导。remove的时候记得调用misc_deregister()static void rockchip_touch_i2c_remove(struct i2c_client *client) { struct rockchip_touch_dev *touch i2c_get_clientdata(client); misc_deregister(touch-mdev); }3.3 在瑞芯微平台上的验证要点瑞芯微的RK3568等平台I2C控制器挂载设备树节点后会按节点顺序依次调用各节点的probe。两个同型号设备共用一个驱动只要私有数据按实例隔离加载顺序完全不用管。但是有两点容易踩坑一中断号不同。两个设备的interrupts如果指向同一个GPIO内核不会拒绝注册但中断触发时会因为缺少实例判断而串扰。所以中断服务函数里一定要通过dev_id参数拿到私有数据判断到底是哪个实例触发static irqreturn_t rockchip_touch_irq_handler(int irq, void *dev_id) { struct rockchip_touch_dev *touch dev_id; /* 只有这个实例才能操作自己的 client */ disable_irq_nosync(touch-client-irq); ... enable_irq(touch-client-irq); return IRQ_HANDLED; }在request_threaded_irq时传touch而非clientret devm_request_threaded_irq(client-dev, client-irq, NULL, rockchip_touch_irq_handler, IRQF_TRIGGER_LOW | IRQF_ONESHOT, dev_name(client-dev), touch);二设备树的status务必要区分。一个节点status okay另一个忘了写或者写成disabled后者不会进probe。排查问题时先ls /sys/bus/i2c/devices/看看有没有对应的地址目录。4. 常见问题与排查技巧实录4.1 两个设备只有一个能 probe 成功这是“一驱多”最常见的故障。我遇到的情况有几种按概率排序现象可能原因排查手段只有地址靠前的设备probe成功设备树节点status未设置或错误检查dts每个节点的status必须显式okay两个设备都显示probe deferred依赖的时钟、复位GPIO、电源regulator未就绪dmesg里搜probe deferred查看等待列表第二个设备报-EBUSY中断号与第一个设备冲突或GPIO被占用检查/proc/interrupts和GPIO状态compatible匹配不上设备树字符串和驱动表不一致检查dts实际编译进去的compatible用dtc反查dtb一个很隐蔽的坑是I2C地址冲突。两个设备如果reg都是0x38且挂在同一条总线上I2C核心会直接拒绝第二个节点注册。这种问题在原理图阶段就应该确认地址跳线但实际项目里经常有漏看的。排查时用i2cdetect -y 3扫描一下总线地址看到两个0x38就明白了。4.2 读到的数据总是同一个设备的值这类问题十有八九是驱动代码里没有按实例隔离数据。比如I2C读写函数里用了模块级全局static struct i2c_client *g_client;第二个设备probe时覆盖了第一个。还有一种是缓冲区全局共用两个设备的DMA或者中断读写同时进行数据被互相踩掉。我的排查顺序是先看probe里是否调用i2c_set_clientdata()/dev_set_drvdata()再看所有读写回调里是否用i2c_get_clientdata()从struct i2c_client重新取回私有数据最后检查seq_file或者debugfs里输出的设备地址是不是不同实例各自的client-addr。只要IO路径上全程只用“当前实例私有数据”这个坑基本就不会踩。4.3 miscdevice 注册失败或节点重复一个驱动同时加载多个实例时miscdevice结构体如果定义在模块全局或者静态数组里第二个实例注册时就会报“already registered”。正确做法是每个实例的私有数据里都放独立的struct miscdevice mdev并且probe里每进一次就注册一次。如果同时使用devm_kasprintf生成不同名字就不会撞名。另外注意miscdevice.name不要太长超过DEVNAME_SIZE会被截断虽然不会导致注册失败但是用户态打开节点时会对不上。4.4 用 cdev 和 alloc_chrdev_region 是否可行也可以用cdevalloc_chrdev_region的方案每个实例分配独立的次设备号再通过device_create创建节点。但说实话如果只是简单字符设备没有复杂mmap需求用miscdevice要省事很多代码量少一半内核已经帮你管理好主设备号和生命周期。瑞芯微平台内核里大量GPIO、EEPROM、看门狗驱动都用miscdevice跟着平台习惯走后续review和移植阻力也小。5. 瑞芯微平台下的经验与扩展建议5.1 设备树、驱动、板级配置三者的联动思路在瑞芯微平台上做“多设备支持”光改驱动不够设备和板级配置必须一起看。比如RK3568的I2C频率一般配置在i2c3节点的clock-frequency里两个设备如果一个是高速Sensor、一个是低速触摸最好分两条总线避免一方拉低总线速率影响另一方。这在硬件设计阶段就要想清楚不是纯软件能兜底的。驱动侧除了匹配表还建议在probe里读取reg或者自定义属性来增强板级差异适配。比如在设备树节点里加rockchip,axis-swap这样的私有属性probe时用device_property_read_u32()读取。数值型配置尽量走设备树不要写死在驱动里这样同一个驱动支持多设备时硬件改动只需要改dts驱动一行不用变。5.2 日志、debugfs 与快速定位技巧多设备场景下日志信息一定要带设备标识。dev_info(client-dev, ...)这类接口会自动打印设备名和地址比用pr_info强太多。比如dev_info(touch-client-dev, probed touch model %d at addr 0x%02x\n, touch-cfg-model, touch-client-addr);两个设备probe完dmesg里能清楚看到两个不同地址、不同model一眼就知道匹配是否正常。我还会在驱动里加一个简单的debugfs节点把每个实例的cfg指针、instance_no、采样状态都导出来调试时比反复插拔硬件查得快很多。5.3 从“两个技巧”延伸出去的更大收益这套设计思路不局限于触摸屏I2C温湿度传感器、SPI ADC、GPIO扩展芯片、LED驱动阵列凡是同型号多实例、多版本兼容的场景都能套用。我在后续项目里甚至把一个音频Codec驱动和HRTIM驱动都用同样方式拆成了“公共框架 差异配置”的结构维护成本明显降低。瑞芯微平台设备树体系本身很规范只要驱动侧状态隔离做得好多设备支持就是水到渠成的事。我个人的体会是Linux驱动写多了之后你会发现“支持多个设备”不是功能列表里的一项而是一种防御性的编码习惯。每个probe都当成可能被调用多次来写每个状态变量都问自己是属于“模块”还是属于“实例”结构自然就对了。先把这两个技巧用熟再回头处理复杂的电源管理、热插拔、多实例并发访问会从容很多。
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻