瑞芯微Linux驱动多设备复用:of_device_id与私有数据实战
2026/9/7 15:19:22 网站建设 项目流程

做瑞芯微平台 Linux 驱动开发,最常被问到的不是"怎么写一个驱动",而是"同一个驱动怎么同时管好几个设备"。我接触过的项目里,RK3568 板子上挂两路 I2C 触摸屏、RV1126 上接四颗摄像头、RK3588 上一组 GPIO LED 要分路控制,都是典型场景。很多朋友第一次接手时,单设备代码跑得很顺畅,一旦要多设备复用,就开始出现"第二个设备 probe 失败""寄存器互相踩踏""一个设备卸载把另一个设备也带崩"这类问题。这类问题的根子其实就两个:一是驱动和设备树的匹配关系没有设计清楚,二是驱动代码里用了太多全局状态。这篇文章就围绕这两个根子,讲两个小技巧:用of_device_id.data字段承载不同硬件版本的差异配置,以及用 per-instance 私有数据加devm_管理接口来撑起多实例共存。适合正在 RK3568/RK3588 这类平台上做 Linux 驱动开发、被多设备问题卡住的朋友参考。

1. 先分清场景:你遇到的"多设备"是匹配问题还是实例问题

1.1 两种"多设备"场景,本质完全不同

我见过太多人把"多设备"当成一个笼统的问题去解决,结果代码越写越拧巴。其实做驱动的人心里得有一杆秤:所谓"一个驱动支持多个设备",通常是指下面两种完全不同的情况。

第一种叫"异构多设备",也就是同一个驱动要兼容多个型号的硬件。比如一颗 I2C 触摸芯片,有标准版和精简版,寄存器地址差几个、初始化序列不一样;再比如音频 codec,同一系列的几颗料,主控寄存器地址一致,但增益表和耳机检测逻辑不同。这种情况的核心矛盾是:同一份驱动代码,如何根据硬件型号自动选择正确的配置参数

第二种叫"同构多实例",也就是同一型号的硬件在系统里出现多次。典型例子是一块板子上有 4 颗同型号的 LED 灯、两路同型号的 ADC 按键、三个同型号的 PWM 背光控制器。这种情况的核心矛盾是:驱动代码如何在同一时间被多次进入、每个实例不相互干扰

把这两者分开看,解法路径完全不同。前者靠的是"匹配表 + 静态配置",后者靠的是"私有数据 + 动态资源管理"。如果混在一起处理,比如为了兼容两个型号就开一个全局数组去记录配置,然后多实例的时候又共用这个数组,那不出问题才怪。

1.2 为什么"跑通一个设备"不等于"跑通一群设备"

很多初学者都有这个错觉:单个设备 probe 成功、读写正常,就觉得驱动没问题了。但驱动从"单设备"扩展到"多设备",其实是一次静默的架构升级,考验的是你有没有做对下面这几件事:

  1. 有没有把"设备相关"和"实例相关"的数据分清楚;
  2. 有没有保证两个设备在不同的设备树节点下,读到的配置是各自的;
  3. 有没有考虑并发访问,比如两个设备同时产生中断时,中断处理函数里的共享变量是否会串扰;
  4. 有没有在 remove 路径上做到一个设备卸载不影响另一个。

在瑞芯微平台上,设备树节点都是按硬件实际连接来写的,同一个驱动在树里往往存在多个匹配节点。如果你在驱动里用了全局变量存寄存器基地址、IRQ 号、设备状态这些内容,第二个设备 probe 的时候就会把第一个设备的信息覆盖掉,表面上看"两个节点都 match 了",实际只有最后一个设备能工作。这是我在评审代码时反复看到的问题。

所以,明确你要解决的是"匹配问题"还是"实例问题",是这篇文章所有内容的前提。接下来我就按这两个方向分别展开。

2. 技巧一:用of_device_id匹配表和.data字段覆盖多个硬件版本

2.1 设备树 compatible 与驱动的匹配流程

先说第一个技巧解决"异构多设备"的匹配问题。在 Linux 4.x 之后的统一设备模型里,驱动和设备是通过compatible字符串建立关联的。设备树里每个节点可以写一个或多个 compatible,比如:

i2c@0 { touch@14 { compatible = "goodix,gt911"; reg = <0x14>; }; };

驱动侧则在of_device_id表里声明自己支持哪些 compatible:

static const struct of_device_id touch_of_match[] = { { .compatible = "goodix,gt911" }, { .compatible = "goodix,gt928" }, { } }; MODULE_DEVICE_TABLE(of, touch_of_match);

当 device tree 里的某个节点 compatible 和这张表里的某一项匹配上,内核就会调用驱动的 probe 回调。MODULE_DEVICE_TABLE的作用是让编译系统生成模块别名,这样设备出现时modprobe能自动找到驱动模块,千万别漏。

很多人用匹配表就到此为止了,剩下的差异全靠在 probe 里 if/else 判断。比如:

if (of_machine_is_compatible("goodix,gt928")) { ... }

或者更粗暴的,根据 client->name 字符串去比较。这种写法在只有两颗设备的时候勉强能跑,但设备一旦变多(三颗、四颗、不同版本混用),代码里全是 if/else 和 magic string,而且还会遇到一个问题——从设备树里拿到的 compatible 不准确时,比较根本不会命中。

正确做法是在匹配表阶段就把"这台设备需要什么"确定下来。

2.2.data字段的用法与完整代码骨架

of_device_id结构体里有几个字段,大多数人都只用了.compatible,忽略了一个非常有用的.data字段。它的作用是把匹配项和一个自定义指针绑定,匹配成功之后,probe 里只需要一行代码就能拿到这个设备专属的配置数据。

以触摸驱动为例,假设驱动要支持 GT911 和 GT928 两颗芯片,它们的寄存器基址、坐标分辨率、中断触发方式都不一样,代码可以这样组织:

struct touch_cfg { u32 reg_ctrl; u32 max_x; u32 max_y; unsigned long irq_flag; const char *fw_name; }; static const struct touch_cfg gt911_cfg = { .reg_ctrl = 0x8040, .max_x = 1024, .max_y = 600, .irq_flag = IRQF_TRIGGER_FALLING, .fw_name = "goodix/gt911.fw", }; static const struct touch_cfg gt928_cfg = { .reg_ctrl = 0x8040, .max_x = 720, .max_y = 720, .irq_flag = IRQF_TRIGGER_RISING, .fw_name = "goodix/gt928.fw", }; static const struct of_device_id touch_of_match[] = { { .compatible = "goodix,gt911", .data = &gt911_cfg }, { .compatible = "goodix,gt928", .data = &gt928_cfg }, { } }; MODULE_DEVICE_TABLE(of, touch_of_match); static int touch_probe(struct i2c_client *client) { struct device *dev = &client->dev; const struct touch_cfg *cfg; cfg = device_get_match_data(dev); if (!cfg) { dev_err(dev, "no matching device data\n"); return -ENODEV; } dev_info(dev, "fingerprint %s, max %ux%u, irq %lu\n", dev_get_match_data(dev) ? "ok" : "none", cfg->max_x, cfg->max_y, cfg->irq_flag); /* 后续所有初始化、报点解析都用 cfg 里的参数 */ ... }

这里有几个细节值得说清楚。

第一,device_get_match_data()是通用接口,它内部会判断是 OF 匹配还是 ACPI 匹配,我们在 ARM 平台上基本都是 OF,用它准没错。在 I2C 子系统里,它和i2c_of_match_device(...)拿到的匹配项一样。

第二,.data指向的配置结构建议全部用static const修饰。const 保证这段数据放在只读段,多线程、多个 probe 并发读取时不会有写竞争;static 则是让它的生命周期贯穿整个驱动生命周期。这两个修饰符缺一不可。

第三,如果不同设备对应的配置结构类型不一样,可以把.data指向一个公共的"描述头"结构,里面放类型枚举和一个 void 指针,probe 拿到后再按类型转换。不过以我经验,绝大多数场景下把所有差异参数收敛到一个统一结构体里就够了,没必要引入更复杂的多态。

2.3 设备树 compatible 的写法与 fallback 设计

搞定了驱动侧,再看设备树侧。瑞芯微平台的 kernel 里,官方 dts 大量使用了这种写法:

touch@14 { compatible = "goodix,gt911", "goodix,gt9xx"; reg = <0x14>; interrupt-parent = <&gpio1>; interrupts = <RK_PA0 IRQ_TYPE_LEVEL_LOW>; reset-gpios = <&gpio1 RK_PA1 GPIO_ACTIVE_LOW>; };

这里同时写了两个 compatible,是有讲究的。第一个是具体型号,第二个是通用兼容族。Linux 的 OF 匹配逻辑会依次尝试:先用第一个去找驱动,找不到再用第二个。这种写法让一个驱动能兼顾"精确匹配"和"宽松匹配"。

比如驱动里我只写了{ .compatible = "goodix,gt9xx" },那么任何 compatible 里有 gt9xx 的节点都会匹配上,包括未来新出的型号。而如果同一颗芯片在不同项目上有寄存器差异,则可以再通过.data精确区分。

但要提醒一句:compatible的命名有规范,必须是"厂商名,型号"这种小写逗号形式,不能写Goodix,GT911,否则匹配直接失败。这个坑我在项目评审里见过不止一次。

还有一个常见误区:有些人喜欢在设备树里通过某个自定义 property(比如goodix,fw-version)来区分硬件版本,然后在 probe 里用of_property_read_u32去判断。这个思路不是不行,但它把"配置差异"从静态的编译时数据变成了设备树里的运行时数据,一旦设备树写错,驱动保护能力就弱了。我的建议是:能放进.data的差异配置,就不要放到设备树里。设备树里的 property 应该留给那些项目级可变的参数(比如 GPIO 引脚、中断号、最大频率),而不是芯片本身固有的特性。

3. 技巧二:去掉全局变量,用私有数据与devm_系列接口管理多实例

3.1 为什么全局数组是第一个要消灭的

如果说第一个技巧解决的是"一颗驱动适配多种硬件",那第二个技巧解决的就是"一个驱动同时服务多个同型号设备"。这时的头号敌人就是全局变量。

看这段代码,是不是很眼熟:

static void __iomem *base; static int irq_num; static struct gpio_led leds[4];

当系统里只有一个设备时,一切正常。但设备树里挂了两个节点,驱动 probe 被调用两次,base第二次会被覆盖,第一个设备的读写就全乱了。更麻烦的是,卸载时第一个设备触发的 remove 会把iounmap执行一遍,第二个设备再用base就成了悬空指针。

正确做法是彻底消灭全局状态,把每个实例的所有信息塞进一个私有结构体,用container_ofdev_get_drvdata取回来。这也是 Linux 设备驱动最基本的设计原则:一个设备节点对应一个私有数据对象,所有回调都从私有的角度去拿状态

3.2 一个多路 GPIO LED 驱动的实例拆分

拿瑞芯微板子上最常见的 GPIO LED 举例。设备树通常是这样的:

leds { compatible = "gpio-leds"; pinctrl-names = "default"; pinctrl-0 = <&led_pins>; work-led { gpios = <&gpio4 RK_PA0 GPIO_ACTIVE_HIGH>; linux,default-trigger = "heartbeat"; }; wifi-led { gpios = <&gpio4 RK_PA1 GPIO_ACTIVE_LOW>; linux,default-trigger = "phy0tx"; }; };

注意,gpio-leds这个驱动在一个 probe 调用里会遍历所有子节点,每个子节点对应一个 LED 实例。这类"一个节点下挂多个子实例"的驱动,处理方式和"多个顶层节点"略有不同,但核心思路是一样的:循环创建 per-instance 私有数据,而不是预先分配固定数组。

代码骨架大致如下:

struct gpio_led_priv { struct gpio_desc *gpiod; struct led_classdev cdev; bool active_low; }; static int gpio_led_probe(struct platform_device *pdev) { struct device *dev = &pdev->dev; struct fwnode_handle *child; int count = 0; device_for_each_child_node(dev, child) { struct gpio_led_priv *led; led = devm_kzalloc(dev, sizeof(*led), GFP_KERNEL); if (!led) return -ENOMEM; led->gpiod = devm_fwnode_gpiod_get(dev, child, NULL, GPIOD_OUT_LOW, fwnode_get_name(child)); if (IS_ERR(led->gpiod)) { fwnode_handle_put(child); return PTR_ERR(led->gpiod); } /* 注册 led_classdev,私有数据带进去 */ ... count++; } if (!count) { dev_err(dev, "no LED subnodes found\n"); return -ENODEV; } return 0; }

有几个点值得专门说一下。

一是devm_fwnode_gpiod_get这类devm_前缀的接口。它们把资源的生命周期绑定到了 struct device 上,probe 成功之后,用户空间只要把设备 unbind 或者驱动模块卸载,内核会自动释放 GPIO、irq、iomap、时钟等资源,不需要在 remove 里一个个手动清。在瑞芯微平台这种经常要热插拔外设模块的场景下,这是防止泄漏的关键。

二是fwnode系列接口。它同时兼容 OF 和 ACPI 两种固件描述方式,设备树平台下fwnode底层就映射到device_node。如果你习惯老的of_get_child_by_name+for_each_available_child_of_node那套,也能跑,但device_for_each_child_node更通用,而且它配合devm_语义更完整。

三是"一个节点下多个子实例"的 LED 驱动,remove 路径其实也是内核自动处理的。每个led_classdev的注册会绑定到设备和私有数据上,卸载时内核会逐个调用注销。你不需要维护一个局部count数组来管理"哪几个实例注册了"。这就是"不维护列表的列表管理"。

3.3 中断、I2C 和并发状态:多实例最容易翻车的地方

多实例驱动的第二个翻车高发区是中断处理。如果一个驱动同时管理两路外部中断(比如两颗同型号传感器),中断处理函数里如果引用全局变量来取寄存器基址,第二个设备触发中断时就会读到第一个设备的寄存器。正确的做法是这样的:

struct sensor_priv { void __iomem *base; struct device *dev; int irq; }; static irqreturn_t sensor_isr(int irq, void *dev_id) { struct sensor_priv *priv = dev_id; u32 status = readl(priv->base + REG_STATUS); ... return IRQ_HANDLED; } static int sensor_probe(struct platform_device *pdev) { struct sensor_priv *priv; int irq, ret; priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL); if (!priv) return -ENOMEM; priv->base = devm_platform_ioremap_resource(pdev, 0); if (IS_ERR(priv->base)) return PTR_ERR(priv->base); irq = platform_get_irq(pdev, 0); ret = devm_request_threaded_irq(&pdev->dev, irq, NULL, sensor_threaded_isr, IRQF_TRIGGER_RISING | IRQF_SHARED, pdev->name, priv); ... }

重点在于devm_request_threaded_irq的最后一个参数传的是priv,这是中断处理函数的dev_id,而中断处理函数用它来取回当前实例的私有数据。这样做的好处是:即使两个中断共享同一个 IRQ 号(在瑞芯微的 GPIO 中断扩展上很常见,多个 GPIO 通过同一个 GIC SPI 上报),中断处理也能通过dev_id区分到底是哪个设备触发的。注意IRQF_SHARED,没有这个标志共享中断会注册失败。

另外,很多驱动在 I2C 子系统中容易犯一个错:用client->addr去从全局表里反查设备。正确做法是把client指针直接放到私有结构体里,probe 时保存,之后所有操作只从私有结构体取。

struct my_i2c_priv { struct i2c_client *client; ... }; static int my_i2c_probe(struct i2c_client *client) { struct my_i2c_priv *priv; priv = devm_kzalloc(&client->dev, sizeof(*priv), GFP_KERNEL); if (!priv) return -ENOMEM; priv->client = client; i2c_set_clientdata(client, priv); ... }

i2c_set_clientdata底层就是dev_set_drvdata,之后你的 read/write 回调里用struct my_i2c_priv *priv = i2c_get_clientdata(client)就能拿到当前实例。整个链路从匹配到中断,没有任何全局变量。

4. 在 RK3568 上完整走一遍:从设备树到多实例 probe 的实操验证

4.1 内核编译与模块加载的基础流程

前面讲完两个技巧,这一节我用瑞芯微 RK3568 平台把整个验证流程串起来。官方 SDK 里内核通常在kernel/目录,设备树在kernel/arch/arm64/boot/dts/rockchip/下。

改完驱动和设备树后,编译命令一般是:

# 编译 dtb make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- rockchip/rk3568-evb.dtb # 编译内核模块(假设驱动编译为 module) make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- M=drivers/input/touchscreen modules # 把 dtb 和 ko 复制到开发板 adb push rk3568-evb.dtb /boot/ adb push goodix_ts.ko /data/

如果你用的瑞芯微 SDK 自带的 build 脚本,比如./build.sh kernel,那它会一键完成内核、dtb、模块的编译。我建议平时调试多设备驱动时,优先用模块方式(=m),这样迭代速度快,不用每次刷整个 boot.img,只要把 dtb 和 ko 推进去,重启或者insmod即可。

设备树覆盖机制的用法也要掌握。瑞芯微的 EVB 板子 dts 通常会 include 一个基础 dtsi,再在 board 级 dts 里覆盖节点。我们做定制项目时不要直接改 EVB 的 dts,而是新建一个 board 级 dts,比如rk3568-myproject.dts,里面 include 官方 dtsi,再覆盖&i2c3&leds这样的节点。这样后续同步官方 SDK 改动时不会被冲突折磨。

4.2 验证多实例是否真的都 probe 了

模块加载之后,第一件事不是看业务功能,而是确认每个设备节点都 probe 成功、每个实例都有独立的相关设备。

查看方法有几种,我按顺手程度排列。

第一种是看/sys/bus/platform/drivers/或者/sys/bus/i2c/drivers/目录下的驱动绑定情况。以 I2C 触摸驱动为例:

ls /sys/bus/i2c/devices/ # 会看到类似 3-0014、4-0014 这样的设备 ls /sys/bus/i2c/drivers/goodix-ts/ # 下面有 3-0014 和 4-0014 两个软链接,说明两个实例都绑定了

如果 driver 目录下只看到一个或没有,说明有个设备没有成功 probe。这时候去看dmesg

第二种是通过/sys/kernel/debug/device_componentdebugfs里的devices节点。不过更直接的是 dmesg:

dmesg | grep goodix

如果probe函数里用了dev_infodev_dbg,每条日志会带设备总线号前缀(比如i2c-3i2c-4),一眼就能看出两次 probe 是否都发生了。

第三种,也是多实例调试最实用的,是直接操作/sys/class/下的实例接口。比如 LED 驱动:

ls /sys/class/leds/ # work-led wifi-led

每个实例都是一个独立的目录,如果出现"两个节点只有一个 sysfs 接口",问题基本就出在驱动内存分配上——大概率是设备树子节点解析有问题,device_for_each_child_node循环在第二个子节点就返回错误了。

4.3 "看起来匹配了却没 probe"的常见原因

我自己在瑞芯微平台上调试时,遇到过不少"设备树里明明写了 compatible,驱动也编译进去了,但就是不调 probe"的情况。整理成表格方便你排查:

现象可能原因排查方向
两个节点只有第一个 probe第二个节点 status = "disabled" 或有未解析的 phandle检查设备树 status、pinctrl 引用
一个节点 probe 后另一个报 -ENODEVcompatible 拼写不一致,或驱动表没匹配核对 dts 和 of_device_id 的字符串
probe 里 GPIO 请求失败两个实例申请了同一个 GPIO检查 pinctrl 配置、对应引脚是否被其他节点用了
I2C 驱动不 probe设备地址不对,或总线上没有应答i2cdetect -y -r 3 探测地址
模块加载后 modprobe 不自动加载缺 MODULE_DEVICE_TABLE,模块别名没生成modinfo xxx.ko 看 alias,modprobe --show-depends

有个很隐蔽的问题我要单独拎出来说:在设备树里写status = "okay"之外,很多瑞芯微 dtsi 会把同一颗 I2C 控制器下的子节点在别的文件里重新定义覆盖。如果你在 board dts 里&i2c3 { touch@14 { ... } };,但 dtsi 里已经有一个touch@14,两个节点的 compatible 可能因为覆盖顺序问题只保留一个。建议在改动后执行make dtbs,然后用dtc -I dtb -O dts rk3568-myproject.dtb反编译确认节点确实存在且 compatible 正确。这一步能省下半小时的排查时间。

5. 多设备调试的排查链路:从随机失败到稳定运行的定位过程

5.1 故障现象分类与根因方向

多设备驱动一旦出问题,现象往往不是"完全不能工作",而是"时好时坏""两个设备互相干扰"。这类随机性故障最难弄。我根据经验把现象大致分成三类:

第一类:第二个设备 probe 失败,第一个工作正常。这通常是资源冲突,比如 GPIO、中断、regulator 被第一个实例占用了。排查方向是看 -EBUSY、-EINVAL 的具体返回值,定位是哪一个request失败了。

第二类:两个都 probe 成功,但读写数据互相串扰。这种几乎可以断定是驱动里用了共享缓冲区或全局寄存器基址。两个实例虽然各自调用了readl(priv->base),但priv->base可能被赋值成了同一个地址。排查方向就是全局变量搜索。

第三类:两个独立的设备,一个卸载后另一个报错。这是 remove 路径没有把资源归还干净,或者相反,一个实例 remove 时把另一个实例也释放了。典型错误是 remove 函数里用全局的irq_numbase,而不是从platform_get_drvdata里取当前实例。

5.2 一条可复现的排查命令链

面对随机失败,我建议按下面这个顺序走,不要一上来就怀疑驱动算法:

# 1. 先确认两个实例都 probe 了 dmesg | grep -E "probe|fail|error" | tail -50 # 2. 检查资源占用情况 cat /proc/interrupts | grep "gpio\|pinctrl" cat /sys/kernel/debug/gpio cat /sys/kernel/debug/pinctrl/pinctrl-rockchip-pinctrl/pinmux-pins | grep {需要检查的引脚} # 3. 检查 I2C 总线(如果是 I2C 设备) i2cdetect -y -r 3 i2cdump -y 3 0x14 # 4. 动态检查 sysfs ls -l /sys/bus/i2c/devices/3-0014/ cat /sys/bus/i2c/devices/3-0014/name

/proc/interrupts是个好东西,多设备共享中断时,你会看到同一个 IRQ 上挂了两个设备名。如果只剩一个,说明另一个设备的中断注册失败了,结合 dmesg 里devm_request_irq的报错信息,能快速判定是 GPIO 坏了还是IRQF_SHARED没加。

5.3 瑞芯微平台上特别容易踩的坑:pinctrl、regulator 与节点复用

最后说几个我在 RK3568/RK3588 项目里反复踩过的坑,都是多设备场景下的"地雷"。

第一个是 pinctrl 冲突。瑞芯微的 pinctrl 和 GPIO 子系统是一体的,同一个引脚不能同时被两个设备声明。多设备时最容易踩的坑是:第二个设备的reset-gpios和第一个设备的某个pinctrl-0引用了同一根脚。这种情况 probe 不一定会失败,但第二个设备的 GPIO 请求会返回-EBUSY,表现在外部就是"第二个设备没响应"或"系统启动时 GPIO 电平被拉乱"。排查手段就是上面给的pinmux-pins/sys/kernel/debug/gpio,看引脚是否已经被占用。

第二个是 regulator 供电。瑞芯微方案里外设的电源经常挂在 PMIC 的 regulator 上(如vcc_3v3),设备树里写vmmc-supply = <&vcc_3v3>。如果一个 regulator 被两个设备共用,其中一个设备 unbind 时如果你在 remove 里手动regulator_disable,另一个设备就会瞬间断电。正确做法是依赖devm_regulator_get+ 引用计数,内核自己会在最后一个使用者释放时才真正关掉电源。不要手动配对disable

第三个是 I2C 地址冲突。两路 I2C 总线接到同型号触摸屏,如果它们的地址一样(比如 0x14),没问题,因为是不同总线。但如果两个设备挂在同一 I2C 总线上且地址一样,那从硬件上就无解,只能改地址线电阻或者换一颗料。这个要在硬件评审阶段就确认,别等到驱动阶段再发现。

第四个,也是最容易被忽略的,是"设备树别名"和"路径"混用。在多实例回调里,如果你为了拿到某个寄存器偏移或配置而用of_find_node_by_path("/leds/work-led")这种方式全局查找节点,那你又把实例问题变成了全局问题,注定在多设备时拿错节点。所有从设备树拿信息的操作都应该发生在 probe 阶段,并且基于pdev->dev.of_nodefwnode句柄,之后只保存数据,不保存 node 指针去到处of_find_*

6. 最后说几句个人体会

这两个技巧说起来都很简单,一个"查表匹配",一个"私有数据",但我在实际项目里见过太多因为这两点没做好而返工三四轮的代码。每次帮同事 review 多设备驱动时,我几乎先不读业务逻辑,直接搜索全局变量、数组声明,以及 probe 里有没有用dev_get_drvdata之外的路径取状态,基本就能预判问题会出现在哪里。

如果你正卡在瑞芯微平台的多设备调试里,我建议按这个顺序动手:先把设备树反编译出来,确认每个设备节点的 compatible、地址、status 都没问题;再在驱动里把所有全局变量全部清掉,统一改成私有结构体加devm_接口;最后用/sys/bus/下的绑定关系和dmesg验证每个实例都独立 probe。做完这三步,你会发现"多设备"其实并没有传说中那么玄乎,它考验的只是你对设备模型最基本的理解是否扎实。

瑞芯微的 SDK 更新节奏很快,内核版本一换,有些接口名字会变,比如老的of_get_named_gpio慢慢都被devm_gpiod_get取代了。但"匹配表带数据""实例带私有数据"这两个原则,从 Linux 2.6 一直到现在的 6.x 都没变过。把这套东西内化成习惯,不管平台怎么换,你写出来的驱动在多设备场景下的稳定性都会有质的提升。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询