瑞芯微Linux驱动多设备管理:compatible兼容与多实例隔离实践
2026/9/7 11:00:22 网站建设 项目流程

在瑞芯微平台上做Linux驱动开发,最常遇到的一个需求就是“一个驱动要管多个设备”。比如板子上接了两颗同型号的I2C传感器,或者一颗SoC通过不同总线挂了同一款触摸屏芯片,再或者一个系列的产品线用了不同型号的codec,你想只维护一份驱动代码就把它们全撑起来。这事说大不大,说小不小,处理不好就是probe冲突、资源覆盖、中断抢断,调试的时候能把人折磨到怀疑人生。

这篇文章我结合瑞芯微RK3568、RK3588、RV1106这几个平台上实际跑过的项目,梳理出两个最实用的技巧:一个是怎么用compatible兼容多个不同型号的设备,另一个是怎么让同一个compatible下的多个实例各自安好、互不干扰。内容偏实践,代码和设备树片段都是可以直接拿去改的,适合正在做BSP、驱动移植,或者被多设备驱动困扰的朋友参考。

1. 多设备场景与驱动设计思路

1.1 哪些场景下需要“一个驱动管多个设备”

很多刚接触驱动开发的同学会觉得,一个驱动对应一个设备,一一对应不是挺清晰吗?但在实际产品里,情况远没有这么理想。我列几个最常见的场景,你看看是不是都遇到过:

第一个场景,同型号芯片的多路接入。比如一个RK3588主板上同时接了四路同型号的ADC芯片,分别采集不同的模拟信号。这些芯片挂在不同的I2C总线上,地址可能相同也可能不同。如果按常规思路每路写一个独立驱动,代码重复度极高,而且后续如果要统一修改某个初始化流程,得改四个文件,维护成本直接起飞。

第二个场景,不同型号芯片的兼容替换。消费类产品经常会遇到“这颗料涨价了,换一颗兼容的”。通常替换芯片的功能寄存器基本一致,但芯片型号、部分配置参数不同。这时候你希望驱动能自动识别当前板子上到底是哪一颗,然后走对应的初始化流程,而不是为了换料单独fork一个驱动分支。

第三个场景,同一设备树节点下有多个子设备。例如一个MIPI-DSI接口上挂了触摸屏,触摸芯片和显示驱动虽然是两个不同的驱动,但它们共享同一个电源域、同一个复位引脚。这种场景下,两个驱动要协调好资源,不能各自乱抢。

这三个场景的共同点就是:硬件上存在多个实例或者多个变种,但驱动逻辑高度相似。如果你不做统一管理,最终代码会变成一坨到处是ifdef的分支补丁,过两个月你自己都看不懂当时是怎么想的。

1.2 设备树如何参与驱动与设备的匹配

要说清楚多设备支持,必须先搞明白Linux里设备和驱动是怎么牵线搭桥的。在设备树(Device Tree)进入主流视野之前,驱动和设备之间通常靠平台设备编号、总线ID这类硬编码方式匹配,代码里到处是数组和宏定义,扩展一个新设备要改驱动源码再重新编译,非常痛苦。

现在瑞芯微平台的标准做法是:设备树里描述硬件长什么样,驱动里声明自己支持哪些compatible,内核在启动阶段自动完成匹配。整个过程可以简化成三步:

第一步,设备树节点中有一个compatible属性,比如:

compatible = "vendor,device-model";

这个字符串就是设备的“身份证”。它可以有多个值,按优先级从高到低排列。

第二步,驱动里定义一个of_device_id数组,声明自己认哪几张“身份证”:

static const struct of_device_id mydrv_of_match[] = { { .compatible = "vendor,device-model", }, { }, }; MODULE_DEVICE_TABLE(of, mydrv_of_match);

第三步,当设备树解析到这个节点时,内核会遍历所有已注册驱动的of_match_table,找到compatible匹配的那个驱动,然后调用驱动的probe函数,把struct device指针传进去。

这里有一个关键点:一次匹配就会调用一次probe。也就是说,如果设备树里有两个同compatible的节点,驱动只写一份,但probe会被调用两次。这个特性正是我们实现多设备支持的基础。理解了这一层,后面的两个技巧就顺理成章了。

2. 技巧一:用compatible兼容多个型号的设备

2.1 设备树侧的写法

先说第一种情况:同一个驱动要兼容多个芯片型号。这种需求在国产替代的浪潮里特别常见。比如原来用的是A公司的传感器,后来换成B公司管脚兼容、寄存器兼容的芯片,或者同一家厂商的芯片分标准版和高配版,初始化流程略有差异。

设备树里最简单直接的做法,就是在不同板型或者不同外设节点上写不同的compatible。比如标准版用GT911,高配版用GT9271:

/* 标准版设备树 */ &i2c2 { status = "okay"; gt911_std: touchscreen@5d { compatible = "goodix,gt911"; reg = <0x5d>; interrupt-parent = <&gpio3>; interrupts = <15 IRQ_TYPE_LEVEL_LOW>; reset-gpios = <&gpio3 14 GPIO_ACTIVE_LOW>; }; }; /* 高配版设备树,同一个驱动支持 */ &i2c2 { status = "okay"; gt9271_high: touchscreen@5d { compatible = "goodix,gt9271"; reg = <0x5d>; interrupt-parent = <&gpio3>; interrupts = <15 IRQ_TYPE_LEVEL_LOW>; reset-gpios = <&gpio3 14 GPIO_ACTIVE_LOW>; irq-flags = <8>; }; };

两个节点的寄存器地址相同,都在0x5d,中断脚也是同一个,唯一的区别是compatible字符串。这样一来,同一块主板只需要更换触摸芯片和对应的设备树,驱动不用重新编译。这个“设备树描述硬件差异、驱动只保留通用逻辑”的边界,一定要把握好。

2.2 驱动侧的of_device_id与私有配置

设备树侧已经做了区分,驱动侧怎么感知到当前是哪一个型号呢?关键就在of_device_id数组的.data字段。这个字段可以挂一个指针,指向任意类型的数据结构。我们可以把不同型号的设备差异放到一个结构体里,让匹配的过程直接把对应配置“带”出来。

看一段实际可用的内核驱动框架:

#include <linux/module.h> #include <linux/platform_device.h> #include <linux/of.h> #include <linux/of_device.h> /* 每个型号的差异配置 */ struct gt9xx_chip_config { const char *chip_name; u32 irq_flags; // 中断触发方式 int reset_delay_ms; // 复位延时 bool support_gesture; // 是否支持手势唤醒 }; static const struct gt9xx_chip_config gt911_config = { .chip_name = "GT911", .irq_flags = IRQ_TYPE_LEVEL_LOW, .reset_delay_ms = 20, .support_gesture = false, }; static const struct gt9xx_chip_config gt9271_config = { .chip_name = "GT9271", .irq_flags = IRQ_TYPE_LEVEL_LOW, .reset_delay_ms = 50, .support_gesture = true, }; /* 匹配表:compatible 字符串 与 配置结构体绑定 */ static const struct of_device_id gt9xx_of_match[] = { { .compatible = "goodix,gt911", .data = &gt911_config }, { .compatible = "goodix,gt9271", .data = &gt9271_config }, { }, }; MODULE_DEVICE_TABLE(of, gt9xx_of_match);

然后在probe函数里,通过of_match_device拿到当前匹配到的配置:

static int gt9xx_probe(struct i2c_client *client) { const struct gt9xx_chip_config *cfg; const struct of_device_id *match; match = of_match_device(gt9xx_of_match, &client->dev); if (match && match->data) { cfg = match->data; dev_info(&client->dev, "detected %s\n", cfg->chip_name); /* 按照 cfg 里的参数执行差异化初始化 */ } else { return -EINVAL; } /* 剩余通用初始化代码 */ return 0; }

如果用的是platform_driver而不是i2c_driver,逻辑完全一样,只是of_match_device的第一个参数类型略有区别。核心思路就是把“变”的部分抽出来放到data里,“不变”的部分留在probe里通用处理。

2.3 一个完整的兼容匹配示例

为了让大家看得更清晰,我补一个platform_driver的完整骨架。这个例子模拟的是瑞芯微平台上常见的PWM背光驱动,分别兼容两种不同厂商的背光控制芯片:

#include <linux/module.h> #include <linux/platform_device.h> #include <linux/pwm.h> #include <linux/of.h> #include <linux/of_device.h> #include <linux/delay.h> struct backlight_chip_cfg { const char *name; int max_brightness; int min_duty_ns; int max_duty_ns; }; static const struct backlight_chip_cfg chip_a_cfg = { .name = "chip-a", .max_brightness = 255, .min_duty_ns = 1000, .max_duty_ns = 1000000, }; static const struct backlight_chip_cfg chip_b_cfg = { .name = "chip-b", .max_brightness = 1023, .min_duty_ns = 500, .max_duty_ns = 2000000, }; static const struct of_device_id backlight_of_match[] = { { .compatible = "vendor,backlight-a", .data = &chip_a_cfg }, { .compatible = "vendor,backlight-b", .data = &chip_b_cfg }, { }, }; MODULE_DEVICE_TABLE(of, backlight_of_match); static int backlight_probe(struct platform_device *pdev) { struct device *dev = &pdev->dev; const struct of_device_id *match; const struct backlight_chip_cfg *cfg; struct pwm_device *pwm; match = of_match_device(backlight_of_match, dev); if (!match || !match->data) { dev_err(dev, "no matching chip config\n"); return -EINVAL; } cfg = match->data; dev_info(dev, "init %s backlight\n", cfg->name); pwm = devm_pwm_get(dev, NULL); if (IS_ERR(pwm)) return PTR_ERR(pwm); /* 用 cfg->min_duty_ns 和 cfg->max_duty_ns 做参数配置 */ /* ... */ return 0; } static struct platform_driver backlight_driver = { .probe = backlight_probe, .driver = { .name = "vendor-backlight", .of_match_table = backlight_of_match, }, }; module_platform_driver(backlight_driver); MODULE_LICENSE("GPL");

实际编译这个模块,加载后如果设备树里的compatible是“vendor,backlight-a”,就会打印“init chip-a backlight”;如果是“vendor,backlight-b”,就打印“init chip-b backlight”。所有不同的参数都从cfg结构体里取,probe主体逻辑完全复用。

2.4 常见误区与经验

这里有几个我踩过坑之后总结出的经验,值得多说两句。

第一,compatible的命名规范。强烈建议按照“厂商名,器件型号”的格式,例如“goodix,gt911”,厂商名一般和芯片厂商一致。有些同学随意写“mytouch”这种不带厂商名的字符串,虽然能跑起来,但是一旦后面要引入其他厂家的驱动,很容易冲突。内核的of_match_table匹配是严格字符串比较,小写、逗号都不能写错,大小写也会导致匹配失败,而且这类报错在日志里不太显眼,排查起来很费劲。

第二,data字段的生命周期。.data指向的内容通常是static的全局结构体,驱动生命周期内一直存在。不要在这里放那些需要在probe期间动态申请的东西。如果配置里有可变内容,建议在probe里拷贝一份到动态申请的结构体中。

第三,不要把芯片型号的区分完全依赖在compatible上。有时候你会遇到同一个芯片、同一个compatible,但不同批次或者不同硬件版本需要不同的初始化时序。这种情况建议在设备树里增加自定义属性,比如“vendor,init-version”,在驱动中用device_property_read_u32读取。compatible负责“选型”,自定义属性负责“细节调参”,两者配合使用才是完整方案。

3. 技巧二:同型号多实例的设备拆分管理

3.1 多实例场景的典型问题

说完了兼容多个型号,现在来聊第二个技巧:同型号芯片、多个实例。这是很多人写驱动时栽跟头最多的地方。典型场景就是在RK3568上通过两个不同的I2C控制器挂了两个同型号的IO扩展芯片,或者通过两个SPI片选控制两颗同型号的ADC。

这类场景最容易踩的坑有三个:

第一个坑是全局变量。很多从单片机和裸机开发转过来的同学,习惯用全局变量保存设备状态,比如:

static struct my_chip *g_chip;

这个写法在单实例下没有任何问题,但两个实例一加载,第二个probe就会把g_chip覆盖掉,导致第一个实例的所有操作都指向了第二个实例的硬件。轻则数据错乱,重则直接操作到不存在的地址,系统直接oops。

第二个坑是设备号管理。如果你用alloc_chrdev_region手动分配字符设备号,并且把设备号存在全局变量里,第二个probe会尝试重复注册一个已经存在的设备号,返回-EBUSY。你需要为每个实例分配独立的设备号,并建立设备号与私有数据的映射关系。

第三个坑是资源释放。如果probe过程中某个步骤失败了,不能简单return了事。前面申请的中断、GPIO、时钟、内存都要释放干净,否则第二次probe或者驱动卸载后再加载,系统会报告资源忙,好像设备被“卡死”了一样。

3.2 私有数据结构与devm_资源管理

解决这些问题的方法论其实很简单:永远不要使用全局变量保存设备相关状态,把每个实例的数据放到独立的私有数据结构中,并通过dev_set_drvdata绑定到对应的struct device上。

每一路设备都有一份独立的上下文,互不影响。所以驱动里最核心的设计就是定义好这个私有数据结构:

struct my_chip_priv { struct device *dev; void __iomem *base; int irq; struct clk *clk; struct gpio_desc *reset_gpio; int id; /* 当前实例编号 */ /* 其他运行时状态 */ };

配合devm系列API使用,资源释放的问题也能大幅简化。devm的意思是device-managed,内核会在设备driver detach时自动释放这个设备申请过的所有devm资源,包括内存、IRQ、GPIO、时钟等。用devm_kzalloc、devm_request_irq、devm_clk_get、devm_gpiod_get这些接口,probe失败时可以少写很多清理代码。

注意,devm资源是和struct device绑定的,不是和struct driver绑定的。所以多实例场景下,每个设备都可以有一份独立的devm资源,天然适合多设备管理。

3.3 完整的多实例驱动框架

我写一个简化的platform_driver,展示多实例场景下probe函数的正确姿势。假设瑞芯微平台的FPGA外挂了两个相同的中断控制器,寄存器地址不同但驱动逻辑一样:

#include <linux/module.h> #include <linux/platform_device.h> #include <linux/of_address.h> #include <linux/interrupt.h> #include <linux/of_irq.h> struct intc_priv { struct device *dev; void __iomem *base; int irq; u32 instance_id; }; static irqreturn_t intc_isr(int irq, void *data) { struct intc_priv *priv = data; u32 status; status = readl(priv->base + 0x00); dev_info(priv->dev, "intc-%u irq status: 0x%x\n", priv->instance_id, status); writel(status, priv->base + 0x04); /* 清中断 */ return IRQ_HANDLED; } static int intc_probe(struct platform_device *pdev) { struct device *dev = &pdev->dev; struct intc_priv *priv; struct resource *res; int ret; priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL); if (!priv) return -ENOMEM; priv->dev = dev; priv->instance_id = pdev->id; /* 多实例编号 */ res = platform_get_resource(pdev, IORESOURCE_MEM, 0); priv->base = devm_ioremap_resource(dev, res); if (IS_ERR(priv->base)) return PTR_ERR(priv->base); priv->irq = platform_get_irq(pdev, 0); if (priv->irq < 0) return priv->irq; ret = devm_request_irq(dev, priv->irq, intc_isr, 0, dev_name(dev), priv); if (ret) { dev_err(dev, "failed to request irq %d\n", priv->irq); return ret; } /* 设备私有数据与 struct device 绑定 */ dev_set_drvdata(dev, priv); dev_info(dev, "intc-%u probed, base=%px irq=%d\n", priv->instance_id, priv->base, priv->irq); return 0; } static const struct of_device_id intc_of_match[] = { { .compatible = "vendor,multi-intc", }, { }, }; MODULE_DEVICE_TABLE(of, intc_of_match); static struct platform_driver intc_driver = { .probe = intc_probe, .driver = { .name = "vendor-multi-intc", .of_match_table = intc_of_match, }, }; module_platform_driver(intc_driver); MODULE_LICENSE("GPL");

设备树侧这么写:

/ { intc0: intc@10001000 { compatible = "vendor,multi-intc"; reg = <0x0 0x10001000 0x0 0x1000>; interrupts = <0 45 IRQ_TYPE_LEVEL_HIGH>; }; intc1: intc@10002000 { compatible = "vendor,multi-intc"; reg = <0x0 0x10002000 0x0 0x1000>; interrupts = <0 46 IRQ_TYPE_LEVEL_HIGH>; }; };

内核在启动阶段扫描到两个节点,会分别调用两次probe,每次都会实例化一个独立的intc_priv结构体,各自拥有自己的寄存器地址和中断号。第二次probe不会影响第一次probe的任何状态。

3.4 如何监听和管理多个设备实例

驱动加载后,怎么确认两个实例都正常工作了?我常用的命令是dmesg配合sysfs。如果probe成功,dmesg里能看到两条“intc-0 probed”和“intc-1 probed”的日志。

另外,在/sys/bus/platform/devices/目录下,你会看到两个以设备树节点名命名的目录,比如“10001000.intc”和“10002000.intc”。这表示内核确实为这两个节点创建了独立的platform_device。

代码里如果需要从某个具体的struct device回溯到对应的私有数据,用dev_get_drvdata(dev)即可。比如你在某个子系统回调里拿到的是struct device *,通过dev_get_drvdata就能找回你自己的intc_priv结构体。

有一点要注意,pdev->id在纯设备树情况下经常是-1,并不是我代码里注释里写的“实例编号”。设备树派生的platform_device多数没有设置id,如果你确实需要区分实例,建议根据寄存器地址或者设备树里的reg属性来赋予一个逻辑编号,使用of_get_address或者platform_get_resource拿到物理地址后再做一次数值映射。

4. 瑞芯微平台上的实操验证流程

4.1 在RK3568/RK3588上添加设备树节点

前面几个章节都是通用Linux驱动知识,现在结合瑞芯微平台说说实际操作。瑞芯微官方SDK中,内核设备树的路径一般是kernel/arch/arm64/boot/dts/rockchip/,RK3568和RK3588的主设备树文件分别是rk3568-evb.dts、rk3588-evb.dts这类以板型命名的文件。不同方案公司可能会改名为自己项目的后缀,比如rk3588-myproduct.dts,这个具体看SDK版本。

添加设备树节点之前,先确认你要挂的外设走的是什么总线。如果是I2C,确认对应I2C控制器在设备树里的status是否为okay。比如RK3588的i2c2节点在SoC dtsi中默认是disabled的,你必须在自己板级的dts里打开它:

&i2c2 { status = "okay"; pinctrl-names = "default"; pinctrl-0 = <&i2c2m0_xfer>; /* 具体引脚 mux 看 SoC 手册 */ clock-frequency = <400000>; temp_sensor_0: tempsensor@48 { compatible = "vendor,tmp117"; reg = <0x48>; }; temp_sensor_1: tempsensor@49 { compatible = "vendor,tmp117"; reg = <0x49>; }; };

这里我在同一个i2c2上挂了两颗地址不同的温度传感器芯片,都配了compatible “vendor,tmp117”。两颗芯片共用一条I2C总线,靠从机地址区分,这是单总线上多个同型号设备的标准做法。如果两个设备挂不同I2C总线,那设备树节点分别写在两个控制器下面就行。

4.2 编译与烧录内核dtb

设备树修改完,需要编译生成新的dtb。瑞芯微SDK一般建议直接用SDK顶层脚本,但如果你想单独编译内核和设备树,也可以进到kernel目录手动执行:

cd kernel make ARCH=arm64 rockchip_linux_defconfig # 如果还没配置过 make ARCH=arm64 dtbs -j$(nproc)

编译好的dtb会输出到kernel/arch/arm64/boot/dts/rockchip/目录下,比如rk3588-evb.dtb。如果你的SDK支持单独打包boot.img,执行./build.sh bootimg即可。烧录的时候只需要替换boot分区或者resource分区,不同平台分区名不一样,RK3568和RK3588常用的是boot分区。这个没太多可说的,用瑞芯微的RKDevTool或者upgrade_tool烧录都行。

如果你不想整机烧录,也可以用uboot的tftp命令直接把dtb加载到内存里启动验证,开发阶段用这种方法迭代会快很多。不过这种操作需要uboot环境变量支持,而且每次重启都要重新加载,适合调试不适合量产,具体看你手头环境。

4.3 设备与驱动的匹配验证

设备树编译烧录完成后,启动系统,第一步看内核日志:

dmesg | grep tmp117

如果你看到两条probe日志,说明两个设备都绑定成功。如果只看到一条,大概率是第二颗芯片的I2C地址写错了,或者芯片没有正常上电。这时候可以用i2cdetect工具扫描一下总线设备地址:

i2cdetect -y -r 2

其中2对应i2c-2控制器。在输出的地址矩阵里,0x48和0x49位置会显示设备编号,比如“48”和“49”。如果扫描不到,那就是硬件连接问题,先检查I2C上拉电阻、设备供电和地址引脚。

确认I2C设备都能探测到之后,再看设备树是否被正确解析。可以通过/proc/device-tree来读取当前生效的设备树内容:

cat /proc/device-tree/i2c@fe8a0000/temp_sensor_0/compatible cat /proc/device-tree/i2c@fe8a0000/temp_sensor_1/compatible

如果都能正常输出“vendor,tmp117”,说明设备树解析正常。注意/proc/device-tree里的节点名顺序和dts里的顺序存在差别,最好对照/sys/firmware/devicetree/base/路径来看,后者更直观。

4.4 多设备的运行日志与sysfs检查

设备绑定成功后,再检查/sys/bus/i2c/devices/目录,会看到两个以总线号和地址命名的符号链接:

ls -l /sys/bus/i2c/devices/ ... 2-0048 -> ../../../devices/platform/fe8a0000.i2c/i2c-2/2-0048 2-0049 -> ../../../devices/platform/fe8a0000.i2c/i2c-2/2-0049

每个目录下都有一个driver符号链接,指向你加载的驱动。这表示设备与驱动已经正确绑定。

接下来可以实测两个设备是否互相独立。写一个简单的应用层测试程序,通过/dev或sysfs分别对两个传感器发起读取操作,观察返回值是否符合预期。如果你做的是字符设备驱动,确认/dev下的两个设备节点分别对应不同硬件,可以用dd或echo写入不同的寄存器地址,再读取对应硬件寄存器确认。

我在项目里就遇到过一种情况:两个设备的I2C寄存器都能读到值,但写入某个寄存器时第二个设备的值会覆盖第一个设备。排查半天发现,我的驱动里用了静态变量保存I2C client指针,两个probe共享了同一个client。改成每个实例独立保存后问题立刻消失。这就是典型的多设备“看起来正常、实际上串线”的问题。

5. 常见问题与排查技巧实录

5.1 常见问题速查表

下面这几个问题,是我在瑞芯微平台上调多设备驱动时反复遇到的,整理成表方便你对照排查。

问题现象可能原因排查方向
第一个设备正常,第二个probe失败全局变量覆盖导致设备状态串扰检查驱动中是否有全局或static变量保存设备私有数据
两个设备只有第一个能打开设备号重复注册检查是否有全局主设备号、是否每个实例都执行了register_chrdev_region
中断只触发一次,之后失效中断处理函数没有正确清中断标志,或两个设备复用了同一个中断号但没加IRQF_SHARED检查中断状态寄存器读写时序,确认request_irq的flags是否带IRQF_SHARED
设备树有两个节点,但只有一个probe另一个设备节点status属性为disabled,或compatible拼写错误检查dmesg中是否有“failed to match”的提示,用i2cdetect确认硬件地址
i2cdetect能扫到设备,但驱动不probe设备树reg值与实际I2C地址不一致,或驱动模块未加载检查设备树reg地址是否与芯片地址引脚配置一致,确认驱动module已insmod或built-in
probe成功后,读寄存器全是0xff或0x00I2C上拉异常、芯片复位引脚没有释放、供电异常用i2cdetect检查地址是否存在,然后用逻辑分析仪抓I2C波形

5.2 排查思路与命令

多设备问题有一个通用的排查思路:先确认硬件可见,再确认设备树匹配,最后确认驱动状态。这个顺序不能乱,否则很容易被表面的驱动问题带偏。

硬件可见性用i2cdetect、spidev_test这类工具验证。如果硬件都探不到,驱动再怎么调也没用。设备树匹配用dmesg里的一条标志性日志:mac上注册驱动后,每次匹配成功,内核的driver core会打印类似“tmp117 2-0048: supply vcc not found, using dummy regulator”这类信息,不同内核版本略有差异。最后一个阶段看驱动内部状态,可以用dev_dbg、tracepoint,必要时候在probe里临时加printk打印关键变量的值。

排查过程中,不要忽略内核日志的优先级。用dmesg -n 8打开所有内核消息,有时候resource busy这类关键信息被隐藏了,只有设置日志级别才能看到。还有一个小建议,开发阶段把内核的initcall_debug开关打开,可以在启动日志里看到所有驱动probe的调用情况和返回值,对定位“哪个驱动匹配不上”很有帮助。

5.3 三个我在项目里踩过的坑

说来惭愧,这三个坑都是在同一个项目中踩的,每次排查都花了大半天。

第一个坑是把内存申请写成了普通kmalloc,而不是devm_kzalloc。当时两个实例分别申请了内存,第一个实例probe完成后手动释放了内存,导致第二个实例的私有数据指针变成悬空指针,访问时系统直接panic。后来换成devm系列接口,由内核统一管理资源生命周期,这个坑再也没出现过。

第二个坑是中断共享。两路外部中断在SoC内部复用了同一个中断号,我在request_irq时没有加IRQF_SHARED标志,导致第二个设备注册中断时直接返回-EBUSY。当时查了很久,后来看了SoC手册才发现这两个中断源确实共享同一个GIC中断线。解决方法是注册时加IRQF_SHARED,并在中断处理函数里判断硬件寄存器状态,确认中断事件是否属于自己的设备。

第三个坑最有意思:我在驱动里用了一个静态变量作为“当前正在处理的实例序号”,想在中断下半部里区分是哪个设备触发了中断。理论上没毛病,但实际上两个设备的中断频率都不低,竞态条件极容易触发。要么A设备的中断下半部里处理了B设备的数据,要么两个设备同时触发时只有一个能正确执行。最后改成用struct work_struct嵌入私有数据,每个实例独立分配工作队列项,彻底解决了并发问题。

这三个坑本质上都是同一个教训:多设备驱动的核心是多实例隔离。无论是内存、中断还是工作队列,凡是有“状态”的东西,都必须做到每设备一份。

我在实际项目中还总结出了一个更省心的套路:先写一个单实例驱动,验证所有寄存器读写和中断逻辑没问题,再扩展成多实例。单实例阶段只有全局变量,排查起来非常简单;一旦功能跑通,再把所有全局变量“沉入”私有结构体,改成devm系列接口。这样拆成两个阶段调试,定位问题会快很多。如果你正打算把现有驱动改造成多设备支持,不妨先试试这个思路。

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

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

立即咨询