瑞芯微Linux驱动:一个驱动支持多个设备的两种高效技巧

瑞芯微Linux驱动:一个驱动支持多个设备的两种高效技巧 做瑞芯微平台的 Linux 驱动我遇到最多的需求不是从零写驱动而是“让一个驱动支持好几个设备”。这个“好几个设备”通常有两层含义一是同型号设备在系统里出现多份比如一块板子上挂了两个同型号的 ADC、两个同型号的传感器二是同一个驱动要兼容不同型号的芯片比如做显示的时候一个屏驱动要能同时适配三五块不同厂商的面板。两种场景都能把人逼疯尤其是当你在瑞芯微的 SDK 里找到一份参考驱动发现前辈们已经用“复制粘贴大法”写了三个几乎一模一样的文件的时候。这篇文章我想跟你聊两个我实战里最常用来解决这类问题的技巧。一个是利用驱动模型本身的多实例机制让一套platform_driver干净地服务多个同型号设备另一个是用设备树匹配表里的data字段加私有配置表让一个驱动兼容多个差异很大的芯片型号。两种技巧在 rk3568、rk3588、rv1126 这些常用平台上都能直接用而且代码量能少一半。适合正在写外设驱动、被多设备兼容问题折磨的嵌入式 Linux 工程师也适合刚入门想搞懂驱动模型匹配机制的朋友。1. 先把思路理清楚一个驱动对应多个设备本质是什么很多人在“一个驱动支持多个设备”这件事上卡住不是因为不会写代码而是没想明白 Linux 驱动模型的匹配逻辑。我见过有同事把一份驱动复制成三份然后在三个文件里改不同的寄存器地址问他为什么这么干他说“probe 不是每个驱动只执行一次吗”这个误解不解决后面怎么写都是别扭的。1.1 痛点场景复制粘贴式驱动为什么不能长久先说个我实际见过的例子。某项目在 rk3568 上同时接了三个同型号的 I2C 触摸屏控制器分布在不同的 I2C 总线上中断脚也不一样。最初的驱动代码里作者用一堆全局变量保存三个设备的寄存器基地址、中断号、上报状态然后在同一个中断处理函数里写if (index 0) ... else if (index 1) ...。刚开始能用后来改版把屏换成了另一款结构完全变了改起来痛不欲生。这种写法最核心的问题是它把“设备”和“驱动”绑死成一对一完全无视了 Linux 设备模型天生就支持一对多的事实。另一个高频场景是产品系列化。同一个主控硬件平台可能衍生三五个不同配置的产品有的用 720p 屏幕有的用 1080p 屏幕有的屏初始化序列完全不同。如果每一块屏都要写一个独立驱动维护起来就是灾难哪天发现一个初始化时序 bug你得同时改四个文件还容易漏。这类问题靠“技巧一”解决不了得靠“技巧二”从架构上规避。1.2 理解 platform 设备模型probe 是按设备执行的不是按驱动执行的要解决上面两个问题必须先搞清楚一个核心机制probe 函数什么时候被调用答案是当总线上有设备与驱动匹配时每匹配上一个设备就调用一次 probe。也就是说如果你的系统里有三个同 compatible 的设备树节点你的 driver 只需要注册一次probe 会被调用三次。每一次传入的struct platform_device *pdev都指向不同的设备资源、属性、寄存器地址都不一样。很多人的误区就是把 probe 当成“程序入口”觉得一个驱动只能对应一个设备这完全是没吃透模型。platform 总线的匹配过程简单说分这么几步。首先看 driver 的id_table非设备树平台的产物里面每一项有name和driver_data匹配的是设备名字字符串。其次看of_match_table这一项专为设备树设计匹配的是设备树节点的compatible属性。在瑞芯微平台绝大多数情况都走第二条路线因为 SDK 里设备树非常完善节点上写着compatible rockchip,xxx驱动里声明同名的of_device_id就行了。匹配成功之后内核就会调用 probe。到这里你应该明白了一个驱动对应多个同型号设备靠的不是魔法就是这套本来就存在的多实例机制只是你之前可能没有意识到于是自己用全局变量硬造了一套“多实例”反而把问题搞复杂了。1.3 瑞芯微平台驱动的常见组织方式与设备树地位瑞芯微平台的驱动开发和纯 x86 或者单片机裸机开发有一个明显差异设备树DTS的地位非常高几乎所有硬件资源描述都由设备树提供。你在驱动里拿寄存器地址用platform_get_resource或devm_platform_ioremap_resource而不是硬编码拿中断号用platform_get_irq拿自定义参数用device_property_read_u32。这套接口的好处是驱动代码里几乎不会出现“某个硬件具体接在哪儿”的信息硬件连接变了改 DTS 就行驱动不用动。这也是后面两个技巧能够成立的基础。如果不理解设备树是“描述板级信息的输入源”你会觉得驱动里到处都是if (board_type XXX)这种代码其实都可以通过设备树属性或者匹配表的扩展字段优雅处理。瑞芯微的 SDK 里Linux 内核版本通常从 4.19 到 5.10 不等但好消息是device_property_*系列接口和of_device_get_match_data这些 API 在这些版本里都很稳定代码写出来在不同内核之间迁移成本很低。2. 技巧一多个同型号设备用一套 platform_driver 服务多实例第一个技巧解决“同型号多个设备”的问题。核心思想很简单设备树里写多个 compatible 相同的节点驱动里只写一份 platform_driver通过每个设备独立的资源信息来区分它们。听起来容易实际操作里有几个细节必须处理好否则很容易翻车。2.1 设备树里如何规划多个节点假设底板上有两个 I2C ADC 芯片型号一样挂在同一条 I2C 总线上地址不同中断脚不同。设备树节点怎么排两个节点都用同一个compatible值这是关键因为这样才能让它们匹配到同一个驱动。i2c3 { status okay; adc0: adc48 { compatible rockchip,adc-multi-demo; reg 0x48; interrupt-parent gpio1; interrupts RK_PA0 IRQ_TYPE_LEVEL_LOW; vref-supply vcc_3v3; label adc0; }; adc1: adc49 { compatible rockchip,adc-multi-demo; reg 0x49; interrupt-parent gpio1; interrupts RK_PA1 IRQ_TYPE_LEVEL_LOW; vref-supply vcc_3v3; label adc1; }; };这里有几个设计安排值得解释。reg是 I2C 从设备地址内核会根据它生成不同的 device name 后缀系统里会存在类似2-0048和2-0049两个设备目录互不干扰。interrupts不同驱动里就能用platform_get_irq分别拿到各自的 irq 号。label是我自定义的属性用来在应用层区分这俩设备不是内核必需但实战中非常有用比自动生成的设备名直观得多。还有一点两个节点即使 compatible 相同地址必须唯一否则 I2C 子系统在注册时会报地址冲突这是设备树层面最容易犯的错。2.2 驱动代码写法彻底告别全局变量驱动的骨架只需要一份probe 会执行两次必须保证 probe 的代码是无状态的。也就是说局部变量、动态分配的数据结构、devm 系列函数都安全全局变量只要不保存“当前设备”特有的信息也安全。但如果你习惯用static int major; static void __iomem *base;这种方式就会出大问题第二次 probe 时第一次的指针被覆盖第一个设备就彻底废了。正确做法是把每个设备独有的资源全部放进一个私有数据结构用dev_set_drvdata挂在设备上之后中断、文件操作函数里都通过container_of拿到这个结构体。看下面的代码示例struct adc_demo_dev { struct i2c_client *client; int irq; int gpio_int; struct mutex lock; u32 raw_value; struct device *dev; const char *label; }; static int adc_demo_probe(struct i2c_client *client, const struct i2c_device_id *id) { struct device *dev client-dev; struct adc_demo_dev *priv; int ret; priv devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL); if (!priv) return -ENOMEM; priv-client client; priv-dev dev; mutex_init(priv-lock); priv-irq platform_get_irq(to_platform_device(dev), 0); if (priv-irq 0) return priv-irq; ret device_property_read_string(dev, label, priv-label); if (ret) priv-label unknown; dev_info(dev, probed irq%d label%s\n, priv-irq, priv-label); dev_set_drvdata(dev, priv); return 0; }注意platform_get_irq的入参I2C client 对应的 device 在设备模型里同样挂在 platform 总线上所以能直接转成 platform_device。不过这里有个更通用的写法用irq_of_parse_and_map或者直接device_get_irq也行但platform_get_irq在单 irq 场景下最省事。关键是每个设备有自己的priv中断来了比如使用线程化中断static irqreturn_t adc_demo_threaded_irq(int irq, void *data) { struct adc_demo_dev *priv data; mutex_lock(priv-lock); priv-raw_value i2c_smbus_read_word_data(priv-client, 0x00); mutex_unlock(priv-lock); dev_info(priv-dev, irq handled, raw%u\n, priv-raw_value); return IRQ_HANDLED; }request_threaded_irq(priv-irq, NULL, adc_demo_threaded_irq, IRQF_TRIGGER_LOW | IRQF_ONESHOT, priv-label, priv);这样注册data 参数把对应的设备实例带过去了两个中断怎么触发都不会混。这个套路最大的优势就是当你系统里挂 5 个同型号设备时这段代码一行都不用改probe 自动执行 5 次每个设备独立工作。2.3 为什么这样能区分设备资源与设备实例一一对应这套方案能成立本质上是因为内核为每个设备树节点都创建了一个独立的struct device而probe是每个设备调用一次platform_get_resource、device_property_*读到的内容都属于当前这个设备。你可以把设备树理解成一份“硬件连接清单”驱动 probe 的执行就是对清单里每一项执行一遍“装配流程”。每个设备实例在装配过程中拿到自己的资源装进自己的私有数据之后所有操作都只跟这个私有数据交互。两个设备之间的唯一关系可能只剩下共用同一个probe函数和同一个中断回调函数这就已经做到职责分离了。2.4 实战心得硬件设计上尽量让同类设备资源“可独立描述”用这个技巧时有几个硬件设计层面的建议。同类设备的差异尽量体现在设备树可描述的维度上比如 I2C 地址、中断脚、GPIO 编号、供电脚都做成可配置。千万不要出现“第二个设备默认接着第一个设备的 INT 脚”这种搞法也不要把两个设备的复位脚共用同一个 GPIO否则驱动里第二设备 probe 时gpio_request会失败。我在一个项目里就因为偷懒省了一个 GPIO导致第二颗芯片无法独立复位最后只能返工改板。3. 技巧二一套驱动兼容多型号芯片用匹配表 data 字段驱动配置如果说技巧一是“数量”上的扩展那技巧二就是“种类”上的扩展。真实产品里同一个外设接口今天用这颗芯片明天因为供货换成另一颗寄存器定义还完全不一样。你当然可以为每颗芯片写一个驱动然后用设备树去选择。但更优雅、更好维护的做法是写一个驱动通过一张配置表把不同芯片的差异收敛掉。3.1 核心原理of_device_id 的 data 字段到底怎么用设备树匹配表of_device_id有一个很容易被忽略的字段data。它是个const void *类型可以指向任意结构体。驱动匹配成功后通过of_device_get_match_data(dev)就能拿到这个指针。这意味着什么意味着你可以把“不同型号芯片之间的配置差异”定义成一张静态的表每个表项对应一种芯片型号probe 时根据匹配到的型号自动取对应表项然后初始化。代码里再也不需要一大串if (chip A) ... else if (chip B) ...。看一个典型写法。假设你要兼容两款 I2C 环境传感器一款温湿度寄存器在 0x00另一款在 0x10测量精度也不一样struct env_sensor_cfg { const char *chip_name; u32 temp_reg; u32 humi_reg; int resolution_mv; int (*init)(struct device *dev); }; static int init_chip_a(struct device *dev) { regmap_write(dev_get_regmap(dev, NULL), 0x00, 0x80); return 0; } static int init_chip_b(struct device *dev) { regmap_write(dev_get_regmap(dev, NULL), 0x10, 0x7f); return 0; } static const struct env_sensor_cfg chip_a_cfg { .chip_name aht30, .temp_reg 0x00, .humi_reg 0x01, .resolution_mv 10, .init init_chip_a, }; static const struct env_sensor_cfg chip_b_cfg { .chip_name sht40, .temp_reg 0x10, .humi_reg 0x11, .resolution_mv 5, .init init_chip_b, }; static const struct of_device_id env_sensor_of_match[] { { .compatible vendor,env-sensor-a, .data chip_a_cfg }, { .compatible vendor,env-sensor-b, .data chip_b_cfg }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, env_sensor_of_match);probe 里这样取配置static int env_sensor_probe(struct i2c_client *client, const struct i2c_device_id *id) { const struct env_sensor_cfg *cfg; struct device *dev client-dev; cfg of_device_get_match_data(dev); if (!cfg) return -ENODEV; if (cfg-init) cfg-init(dev); dev_info(dev, probed as %s, temp_reg0x%02x\n, cfg-chip_name, cfg-temp_reg); return 0; }这套东西的关键在于配置表是静态的、只读的驱动本身不关心你实际接的是哪颗芯片它只管“你给我哪个配置我就按哪个配置工作”。新增芯片时你只需要在设备树写一个 compatible 匹配项然后往表里塞一个结构体编译一次完事。3.2 不是所有差异都要消灭找准“配置维度”才是重点很多人第一次用配置表时容易走入另一个极端把所有寄存器都放到表里结构体里塞了 30 个字段最后配置表比驱动逻辑还难读。我自己的经验是配置表适合放“型号之间的静态差异”比如寄存器地址、初始化序列指针、采样精度、默认阈值、超时时间。至于那些跟硬件连接直接相关的参数中断号、GPIO、供电电压、时钟频率应该尽量归入设备树让板级工程师去填驱动里用device_property_read_u32读取。这样一条很实用的分工原则跟“这颗芯片本身是什么”相关的放配置表跟“在这块板上怎么接”相关的放设备树。举例来说一块板子上接了某款触摸屏它的 I2C 地址是 0x38另一块板子因为地址冲突改成了 0x39这个 0x39 就不该写进配置表而是直接写进各自板子的 DTS 的reg属性里。驱动里用client-addr读到的自然是对的。把这两类信息混在一起是配置表架构最常见的烂尾原因。3.3 瑞芯微平台上的典型应用兼容多版本屏幕初始化序列在瑞芯微平台配置表思路最典型的使用场景是 DRM 驱动里的屏幕兼容。面板厂商给的初始化序列经常因为玻璃版本、IC 版本不同而有差异如果你在设备树里针对不同屏幕写了不同的compatible驱动里用of_device_get_match_data取到一个struct panel_desc这个结构体里携带初始化命令数组、时序参数、供电时序就能优雅地管理。瑞芯微自己的panel-simple驱动就是这样设计的可以说它就是这套技巧的一个官方级范本。不过这种框架下有个特别容易犯的错设备树的compatible必须和of_match_table里的完全一致包括厂商前缀。你写vendor,panel-720p驱动里写vendor,panel-720p一个字符都不能差。很多坑都出在复制粘贴的时候把逗号后面的空格也带进去了或者大小写不一致导致匹配失败probe 根本不执行。排查这一类问题/sys/bus/platform/devices/.../uevent里会暴露模块的OF_COMPATIBLE一看便知。3.4 如果平台进程不使用设备树platform_device_id 的替代方案有些驱动可能还需要兼容老平台内核没开启设备树或者设备树的匹配被降级。这时of_device_id就不生效了需要用platform_device_id表它的driver_data字段同样可以携带一个枚举值或者指针。写法上大同小异static const struct platform_device_id env_sensor_id_table[] { { env-sensor-a, (kernel_ulong_t)chip_a_cfg }, { env-sensor-b, (kernel_ulong_t)chip_b_cfg }, { }, }; MODULE_DEVICE_TABLE(platform, env_sensor_id_table);在 probe 里判断传入的id是否为 NULL然后id-driver_data取配置。但现在的瑞芯微平台基本都是设备树路线这套老 API 主要用于兼容旧的板级文件我不建议新代码把精力花在这上面了解有这个东西就行。4. 实操演练rk3568 平台上让一个 I2C 驱动同时管理两个不同型号设备技巧归技巧把两个技巧真正组合起来用一个实际工程才能体会到威力。我以一个非常贴近日常需求的项目为例子rk3568 的 I2C2 总线上挂了两颗温度传感器一颗是 A 型号寄存器地址表不同一颗是 B 型号探测逻辑不同它们同时工作应用层希望分别读到各自的温度值。整包代码把多实例和配置表两个技巧都用上。4.1 工程架构建模一个数据结构拆成“配置表 运行时数据”两部分设计思路配置表const struct temp_sensor_cfg描述型号差异运行时数据struct temp_sensor_dev描述具体设备状态。这样既支持同型号多颗也支持多种型号混挂。整体目录结构可以保持简洁drivers/iio/temperature/my_temp_sensor/ ├── my_temp_sensor.c ├── my_temp_sensor_cfg.c └── my_temp_sensor.h头文件里定义公共数据结构。这里我故意把cfg放在结构体首部方便后续用container_of从 cfg 强制转运行时结构体不过正常的工程不建议依赖这种布局我只是为了展示灵活性。#ifndef _MY_TEMP_SENSOR_H_ #define _MY_TEMP_SENSOR_H_ struct temp_sensor_cfg { u8 temp_reg; u8 sign_bit; int default_offset; const char *compat_name; }; struct temp_sensor_dev { struct i2c_client *client; struct device *dev; const struct temp_sensor_cfg *cfg; struct mutex lock; int temperature; }; #endif两个型号的配置表写到my_temp_sensor_cfg.c#include my_temp_sensor.h static const struct temp_sensor_cfg cfg_model_a { .temp_reg 0x00, .sign_bit 12, .default_offset -500, .compat_name vendor,temp-sensor-a, }; static const struct temp_sensor_cfg cfg_model_b { .temp_reg 0x20, .sign_bit 8, .default_offset 0, .compat_name vendor,temp-sensor-b, }; const struct temp_sensor_cfg *temp_sensor_cfg_lookup(const char *compat) { if (!strcmp(compat, vendor,temp-sensor-a)) return cfg_model_a; if (!strcmp(compat, vendor,temp-sensor-b)) return cfg_model_b; return NULL; }直接用字符串查表是最低门槛的做法简单、直观也便于新工程师理解。实践里我会直接把这个查表逻辑改成of_match_table of_device_get_match_data更符合内核习惯。下面两个版本的驱动都给你参考。4.2 驱动主体代码把多实例和配置表串起来主驱动文件my_temp_sensor.c的关键代码我拆成几块说明。先是匹配表static const struct of_device_id my_temp_sensor_of_match[] { { .compatible vendor,temp-sensor-a, .data cfg_model_a }, { .compatible vendor,temp-sensor-b, .data cfg_model_b }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, my_temp_sensor_of_match);然后是 probe 函数。这个函数既会被型号 A 的节点触发也会被型号 B 的节点触发还会被两颗同型号的节点各触发一次。每一次执行都独立分配私有数据独立读取自己的硬件信息。static int my_temp_sensor_probe(struct i2c_client *client) { struct device *dev client-dev; const struct temp_sensor_cfg *cfg; struct temp_sensor_dev *sensor; int ret; cfg of_device_get_match_data(dev); if (!cfg) return -ENODEV; sensor devm_kzalloc(dev, sizeof(*sensor), GFP_KERNEL); if (!sensor) return -ENOMEM; sensor-client client; sensor-dev dev; sensor-cfg cfg; mutex_init(sensor-lock); // 验证芯片 ID避免驱动绑定到不支持的设备 ret i2c_smbus_read_byte_data(client, cfg-temp_reg); if (ret 0) { dev_err(dev, failed to read temp reg 0x%02x, ret%d\n, cfg-temp_reg, ret); return -ENODEV; } dev_info(dev, temp sensor probed, model%s, temp_reg0x%02x\n, cfg-compat_name, cfg-temp_reg); i2c_set_clientdata(client, sensor); return 0; }注意这里我加了一个“读寄存器验证”的步骤。为什么要这么做因为设备树里的 compatible 是软件约定的万一板级工程师写错了节点驱动却照样 probe 成功最后运行时读数据全是错的排查成本很高。在 probe 里做一次最小读取验证能尽早暴露硬件连接错误或者 compatible 指向错误。另一个好处是如果总线上同时挂了 A、B 两种型号但设备树里给 B 写成了 A 的 compatibleprobe 时读取 B 的 0x00 寄存器很可能返回异常值驱动直接返回-ENODEV不会进入后续的死循环重试。读取温度的接口static int my_temp_sensor_read_temp(struct temp_sensor_dev *sensor, int *temp) { int raw; int val; mutex_lock(sensor-lock); raw i2c_smbus_read_word_data(sensor-client, sensor-cfg-temp_reg); mutex_unlock(sensor-lock); if (raw 0) return raw; // 根据型号的符号位宽度和偏移量计算温度值 val raw BIT_MASK(sensor-cfg-sign_bit); if (val BIT(sensor-cfg-sign_bit - 1)) val - BIT(sensor-cfg-sign_bit); *temp val * 10 sensor-cfg-default_offset; return 0; }为了让用户空间能读数据我把它做成一个极简的 IIO 驱动或者 misc 设备。假如用 misc 设备需要注意设备号分配每个实例用misc_register时指定不同的 name内核会自动分配次设备号。多个实例共用一套 file_operations在open里从file-private_data拿到sensor结构体之后读写都靠它完全不需要全局状态。这样两个设备都会有独立的/dev/my_temp_sensor0、/dev/my_temp_sensor1节点应用层根据节点名区分就行。4.3 设备树写法让两个型号共存在 rk3568 的设备树里I2C2 节点下面挂两颗不同类型的传感器写法是这样的i2c2 { status okay; clock-frequency 400000; temp_sensor_a: temp-sensor18 { compatible vendor,temp-sensor-a; reg 0x18; status okay; }; temp_sensor_b: temp-sensor28 { compatible vendor,temp-sensor-b; reg 0x28; status okay; }; };两个节点 compatible 不同在驱动里匹配到的配置自然不同。这里的reg是芯片的 I2C 地址必须跟硬件实际跳线一致。有个细节瑞芯微的 I2C 设备树节点里通常继承pinctrl-0等属性不需要你手动加只要在i2c2下面新增子节点即可。编译内核时确保CONFIG_I2C_CHARDEV或者你的驱动编译成模块然后用insmod my_temp_sensor.ko加载。加载后看内核日志你会看到类似这样的输出my_temp_sensor: temp sensor probed, modelvendor,temp-sensor-a, temp_reg0x00 my_temp_sensor: temp sensor probed, modelvendor,temp-sensor-b, temp_reg0x20两次 probe 证明多实例机制正常工作。如果只打印了一次说明有一个节点没有匹配上检查两个途径设备树编译是否正确/proc/device-tree/i2cfe820000/temp-sensor18/compatible文件内容对不对以及驱动有没有被正确加载到内核。4.4 上层验证应用里同时读两个设备写一个简单的 C 程序读取两个设备节点的温度流程就是标准的 open read。但有两个实践细节值得提一下一类是“到底用哪个节点”我建议把设备名固定成带方向的名字比如/dev/temp-sensor-a、/dev/temp-sensor-b而不是依赖系统自动按注册顺序编号的my_temp_sensor0/my_temp_sensor1否则插拔顺序一变设备名就错位。二类是把温度读取延后到用户主动访问时再执行不要用内核线程周期轮询除非你有实时性要求。原因很简单内核线程轮询大多数时候读到的数据没人在用白耗 CPU 和 I2C 总线带宽用户态应用需要时才读反而更省资源。5. 常见问题与排查技巧实录两个技巧在实际项目中用久了翻车点其实有比较强的规律性。我整理了几个高频故障每个都按“现象、原因、排查、解决”四步讲清楚。这张速查表建议直接收藏遇到问题先过一遍。故障现象可能原因排查方法解决方案两个设备只 probe 了一次设备树里有个节点status disabled检查/proc/device-tree/.../status的值改为okayprobe 一直不执行compatible 不匹配查看/sys/bus/i2c/devices/*/uevent里的OF_COMPATIBLE统一 compatible 字符串probe 执行两次但第二个设备无法访问I2C 地址冲突i2cdetect -y bus看地址占用情况修改硬件地址或设备树 reg中断线程读到的数据总是第一个设备的中断回调里没有正确使用传入的 priv打印 dev_info(priv-dev, ...) 确认统一用 data 参数别用全局变量两个设备互相干扰用了全局变量保存状态审查全局变量移入私有数据结构加载模块后系统崩溃driver_data 类型转换错误查看 dmesg 的 oops 日志确保 of_match_table 的 data 类型一致5.1 probe 没执行或者只执行一次怎么快速定位这个是最常见的问题也是最容易自我排查的。先看dmesg里有没有驱动注册的打印。驱动注册成功说明 module 加载没问题接下来怀疑匹配。用ls /sys/bus/i2c/devices/看看 i2c 总线上有哪些设备比如2-0018、2-0028这种代表总线上确实有设备挂上去了。进到对应目录比如/sys/bus/i2c/devices/2-0018/cat uevent看OF_COMPATIBLE...的值。这个值必须和驱动of_match_table里的 compatible 一模一样。万一这里什么都不显示说明这个设备不是由设备树创建的很可能是另一个驱动在同一地址上先抢注了这时i2cdetect -l和i2cdetect -y 2马上能看出端倪。如果只有一个设备节点挂上来了另一个没出现先检查设备树语法和编译结果。瑞芯微的 SDK 里通常有编译 DTB 的脚本比如./make.sh dtb编出来的 dtb 路径一般在kernel/arch/arm64/boot/dts/rockchip/下。用dtc反编译看一下节点是否完整status是不是okay。很多时候是板级工程师在别的 dtsi 里覆盖了status disabled导致你加的子节点被屏蔽。5.2 两个设备都申请同一个中断号怎么避免冲突如果两块芯片的中断都接到了同一个 GPIO 上且硬件上没有做或逻辑合并驱动里第二个设备的request_irq会失败。这种事我碰过好几次处理方式分两种情况。第一种硬件上两个设备的中断确实必须分开那就改板把第二个设备的中断换到另一个 GPIO这类问题必须在硬件设计阶段规避第二种两个设备共用一根中断线驱动里可以只让第一个设备注册中断第二个设备不注册但中断服务函数需要遍历两个设备通过读取设备寄存器判断是哪个设备触发的。这种共享中断写法在驱动模型里属于“一对多共用 handler”模式注意要用IRQF_SHARED并且中断处理函数必须正确判断“不是我的中断”时返回IRQ_NONE。瑞芯微平台上的 GPIO 作为中断源时interrupts属性最好写清楚触发类型比如IRQ_TYPE_LEVEL_LOW否则很容易进中断风暴。5.3 配置表类型不匹配导致内核崩溃这类 bug 怎么防of_device_get_match_data返回的是const void *如果你在某个 compatible 表项里填了一个结构体指针另一个表项里填了一个整型值probe 函数却统一按结构体指针解引用内核会立刻报Unable to handle kernel paging request页面变成 oops。这种问题靠读代码基本很难一眼看出最好的防御手段是写一个编译期的类型安全检查辅助宏。内核里有个现成做法#define of_match_ptr(_ptr) (_ptr)这个宏不能保证类型但手写一个可以。更朴素的办法是保证of_match_table的表项清一色都填“指向配置结构体的指针”不要混用。我见过有同事偷懒某个表项直接写(kernel_ulong_t)0x1234表示版本号结果 probe 里直接cfg-xxx崩了。记住一条data字段永远填指针其他值都用device_property_read_*走设备树。类型统一靠约定保证长期可维护。5.4 宝贵教训瑞芯微平台上的 pinctrl 冲突这个坑比较有平台特色。瑞芯微的芯片GPIO 复用关系非常严格两个设备如果引用了同一个 pin设备树里描述 pinctrl 的时候也很容易踩坑。比如某颗传感器用了 I2C2 的 SCL/SDA另一个传感器想复用 SCL/SDA 做普通 GPIO 读取这个在硬件上就不成立dts 里写了也会在 pinctrl 子系统注册时报警告。你会看到类似pin ... already requested by ...的日志然后第二个设备 probe 失败。这类问题设备树里检查pinctrl-0属性把所有引用了同一个 pin 的地方列出来看有没有冲突。更重要的是这种问题要在画原理图的时候想清楚软件排查只能发现问题不能解决硬件错误最终还得改板。6. 写在最后的经验体会这两个技巧是你驱动架构的地基两个技巧单独看都不复杂但组合使用以后驱动架构会变得非常干净。设备树描述“这块板上有什么”配置表描述“这颗芯片是什么样的”probe 根据匹配到的节点拿到配置再根据具体资源实例化一个设备状态。新增硬件要么在设备树加节点要么往配置表加一项驱动代码本身几乎不动。这套模式我用了好几年从瑞芯微的 3288 到现在的 3588一直没变过。还是想提醒一句驱动开发里最怕的不是写功能而是写“看似能用、一换板就死”的代码。全局变量、复制粘贴驱动、hardcode 寄存器地址这些做法在新人阶段看起来很快但产品一旦多了维护成本是几何级上升的。设备树、配置表、私有数据结构、container_of这些工具内核已经提供了就看你自己用不用。每次动手写驱动之前先问一句自己如果这个驱动要支持两份硬件我现在的代码还能不能躺赢如果答案是不能那多半是设计有问题趁早重写。