资讯动态

瑞芯微平台Linux驱动多设备匹配实战:兼容多型号与实例隔离技巧

发布时间:2026/9/7 19:36:16 来源:尧图企业网站定制
做嵌入式 Linux 驱动的人只要不是整天只写 hello world早晚都会碰到“一个驱动要同时伺候多个设备”的需求。我最近在瑞芯微平台上调驱动一块 RK3568 的板子上同时挂了两把 PWM 调速风扇、两个 I2C 温度传感器而且同一套驱动还要兜住不同批次的物料——这已经不是“写个驱动点个灯”的难度而是 Linux 驱动模型里最典型的多设备匹配问题。今天我把踩坑过程中沉淀下来的两个小技巧整理出来一个解决“一个驱动兼容多个硬件型号”一个解决“一个驱动安全绑定多个同类型设备实例”都是可以直接抄回项目的实践方案。1. 先理清需求瑞芯微平台上的“多设备”到底指什么很多人在标题里看到“驱动支持多个设备”第一反应是那不就是多写几个 platform_device 吗其实没那么简单。在瑞芯微这类 ARM SoC 平台上“多设备”背后通常藏着两个完全不同的需求对应的解决思路也完全不同搞混了就容易写出一个看起来能跑、一接真机就崩的驱动。1.1 多型号兼容与多实例挂载是两个维度第一个维度是“兼容多个硬件型号”。比如你的产品前期用了一颗温度传感器后期因为成本换了另一颗两颗传感器寄存器地址、转换精度都不一样但接口都是 I2C。如果你为每颗传感器各写一个驱动代码重复不说设备树和驱动模块的管理也会变得很啰嗦。更合理的做法是一个驱动文件通过不同的 compatible 字符串匹配不同的硬件型号在 probe 的时候根据匹配到的型号加载不同的参数和初始化流程。第二个维度是“一个驱动绑定多个同类型设备实例”。这个更常见比如 RK3568 芯片上有 4 路 PWM、6 路 I2C、多路 SPI同一块板子上完全可以接两把 PWM 风扇、两个同型号的 ADC 按键芯片。此时设备树里会有两个 compatible 一模一样的节点驱动只写一份但内核会为每个节点都调用一次 probe于是你必须在驱动里把每个实例的寄存器地址、中断号、GPIO、PWM 通道隔离开不能让两个实例共享同一份全局状态。两个维度加上排列组合才是真实项目里的全貌。你在瑞芯微平台写驱动之前可以先问自己一句我是要同一个驱动吃下所有物料还是要一个驱动管住同一类器件的多个实例还是两个都要答案不同下面的设计侧重点就不同。1.2 不处理好会踩到什么坑先说一个我实际见过的反面案例。有人写了一个瑞芯微平台的多路 GPIO 按键驱动设备树里配了两个按键节点驱动里用了一个全局变量保存按键上报事件。结果只要按了第二个按键第一个按键的状态就会被覆盖上报给上层应用的数据错乱得没法看。这就是典型的“没把实例隔离当回事”。再说多型号兼容的问题。有人图省事在设备树里给所有型号都写了同一个 compatible然后靠车厂工程师手工改寄存器地址来适配不同批次。这套方案在样品阶段勉强能用一旦到了产线主板混料、批次混用bom 变了设备树没跟着改现场排查能把人折磨疯。更稳的做法是把型号差异体现到 compatible 上驱动里用匹配表统一管理出现问题时看 dmesg 就能立刻知道是哪个型号的设备在运行。理解了这两个维度的区别下面两个技巧才有落脚的根。2. 技巧一用一张 of_device_id 表兼容多个硬件型号瑞芯微平台的厂商 SDK 里几乎所有外设驱动都通过设备树匹配设备核心就是 platform_driver 结构体里的 of_match_table。技巧一的核心思路很简单把你要兼容的所有硬件型号全部写进同一个 of_device_id 数组让一个驱动挂多张“身份证”。2.1 核心思路同一套 ops多个 compatible先看传统写法。很多人写的驱动长这样static const struct of_device_id fan_of_match[] { { .compatible vendor,pwm-fan-v1 }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, fan_of_match); static struct platform_driver fan_driver { .probe fan_probe, .remove fan_remove, .driver { .name pwm-fan, .of_match_table fan_of_match, }, }; module_platform_driver(fan_driver);这里只能匹配 “vendor,pwm-fan-v1” 这一个型号。要想兼容 v2 型号朴素的做法是再把 v2 的 compatible 加进数组static const struct of_device_id fan_of_match[] { { .compatible vendor,pwm-fan-v1 }, { .compatible vendor,pwm-fan-v2 }, { /* sentinel */ } };这样两个设备节点都能进 probe。但如果你只在 probe 里写死 v1 的初始化流程那 v2 进来也是白搭。所以关键不在于匹配表里多写一行而是要用 of_device_id 里的 data 字段把“这个设备是什么型号”的信息带进 probe。2.2 通过 .data 区分不同型号的行为差异of_device_id 结构体里有个 .data 字段类型是 const void *专门用来挂厂商自定义数据。最常见的用法是给每种型号定义一个配置结构体然后把这些结构体指针填进匹配表struct fan_chip_config { unsigned int max_rpm; unsigned int pwm_freq; bool has_tacho; int (*init)(struct device *dev); }; static const struct fan_chip_config fan_v1_config { .max_rpm 8000, .pwm_freq 25000, .has_tacho false, }; static const struct fan_chip_config fan_v2_config { .max_rpm 12000, .pwm_freq 40000, .has_tacho true, }; static const struct of_device_id fan_of_match[] { { .compatible vendor,pwm-fan-v1, .data fan_v1_config }, { .compatible vendor,pwm-fan-v2, .data fan_v2_config }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, fan_of_match);然后在 probe 里通过 of_device_get_match_data 拿到当前匹配到的型号配置static int fan_probe(struct platform_device *pdev) { struct device *dev pdev-dev; const struct fan_chip_config *cfg; struct fan_priv *priv; cfg of_device_get_match_data(dev); if (!cfg) { dev_err(dev, no matching chip config\n); return -EINVAL; } priv devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL); if (!priv) return -ENOMEM; priv-cfg cfg; dev_info(dev, %s: max_rpm%u pwm_freq%u tacho%d\n, cfg fan_v1_config ? v1 : v2, cfg-max_rpm, cfg-pwm_freq, cfg-has_tacho); return 0; }这样一套驱动逻辑两种硬件型号进来后走同一套代码框架但参数完全隔离。后面如果来了 v3 型号只需要在驱动里加一个 config 结构体、在匹配表里加一行设备树里把 compatible 写对就行不动 probe 主体代码。2.3 用 dev_err_probe 和 MODULE_DEVICE_TABLE 补齐细节这里有两个细节很多人会忽略。第一是不要忘了在 of_match_table 后面写 MODULE_DEVICE_TABLE。如果你把驱动编成 .ko 模块modpost 工具是靠这个宏生成模块别名别名的内容是 of:N T 格式。没有它即使设备树里的 compatible 写对了modprobe 也不会自动加载驱动只能手动 insmod到时候你在现场排查半天找不到原因。第二是 probe 里的错误处理建议多用 dev_err_proberet fan_chip_init(dev, cfg); if (ret) return dev_err_probe(dev, ret, chip init failed\n);dev_err_probe 除了打印错误信息还会特殊处理 -EPROBE_DEFER。比如你的风扇驱动依赖 PWM 驱动而 PWM 驱动还没 probe 完成这时 pwm_get 会返回 -517也就是 -EPROBE_DEFER。dev_err_probe 会把这种情况标记为 deferred probe内核后续会自动重新调用你的 probe不需要你自己维护重试逻辑。这在瑞芯微这种多依赖的平台上特别实用。3. 技巧二让同一个驱动安全绑定多个同类型设备实例技巧一解决的是“换物料”技巧二解决的是“挂多路”。在瑞芯微平台上最常见的就是同一个 compatible 的节点出现好几次比如两路 PWM 风扇、两路温度传感器。你要理解的第一件事是内核本来就把 probe 当“每个设备实例各调一次”来设计真正把你绊倒的往往是驱动里的“全局状态”。3.1 理解 platform bus 的多实例 probe 机制当你把两个 compatible 相同的设备树节点都 status okay 时内核在设备初始化阶段会为每个节点创建一个 platform_device然后 platform bus 依次为每个 platform_device 寻找匹配的 platform_driver一旦找到就调用一次 probe。所以你的 probe 函数实际上会被执行两次每次传入的 platform_device *pdev 都指向不同的实例。这个机制本身没啥高深但很多驱动新手会犯一个致命错误在 probe 里把寄存器地址、中断号、PWM 句柄之类的东西一股脑存进全局变量。static void __iomem *base; static int irq_num; static struct pwm_device *fan_pwm;这么写的后果就是后 probe 的那个实例把前面那个实例的资源覆盖了。你以为你控制的是第一路风扇结果寄存器里写的是第二路风扇的地址。我调试过最诡异的一个现象是风扇转一下停一下两个实例在互相抢同一个 PWM 通道。正确思路是所有实例相关的状态全部放进一个私有数据结构每次 probe 新建一个用 platform_set_drvdata 挂到设备上。需要的时候再用 dev_get_drvdata 取出来各用各的互不干扰。3.2 实例隔离三件套私有数据、devm_ 资源、设备树属性实例隔离我用三个手段落实缺一不可。第一是私有数据结构。给每个设备实例建一个 struct把所有和这个实例相关的资源都放进去struct fan_priv { struct device *dev; struct pwm_device *pwm; struct mutex lock; unsigned int duty_percent; unsigned int max_rpm; };probe 里用 devm_kzalloc 给每个实例分配一份内存天然把两个实例的状态隔开。第二是 devm_ 系列资源管理接口。devm 是 device managed 的缩写意思是资源和设备生命周期绑定设备被移除时资源自动释放。你在瑞芯微平台驱动里能用 devm_ 就用 devm_devm_kzalloc、devm_of_pwm_get、devm_platform_ioremap_resource、devm_gpiod_get、devm_request_irq都比普通版本省心。特别是 devm_of_pwm_get如果第二个实例的 PWM 通道被第一个实例占用了它会返回错误你只需要向上抛错剩下的清理工作内核帮你做了。第三是通过设备树属性区分实例行为。两个实例型号相同不代表行为完全一致。比如第一路风扇有测速反馈第二路没有你可以在设备树里加一个私有属性fan0: pwm-fan0 { compatible vendor,pwm-fan; pwms pwm1 0 1000000; has-tacho; }; fan1: pwm-fan1 { compatible vendor,pwm-fan; pwms pwm3 0 1000000; };驱动里用 of_property_read_bool 区分priv-has_tacho of_property_read_bool(dev-of_node, has-tacho);这样就把“几个实例”和“每个实例长得不太一样”统一处理了。3.3 一个多路 PWM 风扇驱动的完整骨架我把一个 DEMO 级别的多路 PWM 风扇驱动骨架放在这里你在瑞芯微平台上可以直接套用。它的特点是两个 PWM 风扇节点用同一 compatible驱动通过 devm_of_pwm_get 分别拿到各自的 PWM 通道所有状态都存在各自的 fan_priv 里。#include linux/module.h #include linux/platform_device.h #include linux/pwm.h #include linux/of.h #include linux/of_device.h #include linux/of_pwm.h #include linux/mutex.h #include linux/err.h struct fan_priv { struct device *dev; struct pwm_device *pwm; struct mutex lock; unsigned int duty_percent; bool has_tacho; }; static int fan_set_duty(struct fan_priv *priv, unsigned int percent) { struct pwm_state state; struct pwm_device *pwm priv-pwm; if (percent 100) percent 100; pwm_init_state(pwm, state); state.period pwm_get_period(pwm); state.duty_cycle DIV_ROUND_UP(state.period * percent, 100); state.enabled true; mutex_lock(priv-lock); priv-duty_percent percent; mutex_unlock(priv-lock); return pwm_apply_state(pwm, state); } static int fan_probe(struct platform_device *pdev) { struct device *dev pdev-dev; struct fan_priv *priv; struct pwm_device *pwm; u32 default_duty 50; priv devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL); if (!priv) return -ENOMEM; priv-dev dev; mutex_init(priv-lock); priv-has_tacho of_property_read_bool(dev-of_node, has-tacho); of_property_read_u32(dev-of_node, default-duty-percent, default_duty); pwm devm_of_pwm_get(dev, dev-of_node, NULL); if (IS_ERR(pwm)) return dev_err_probe(dev, PTR_ERR(pwm), failed to get pwm\n); priv-pwm pwm; platform_set_drvdata(pdev, priv); fan_set_duty(priv, default_duty); dev_info(dev, fan probe ok, pwm%s duty%u%% tacho%d\n, pwm-label ? pwm-label : unknown, default_duty, priv-has_tacho); return 0; } static void fan_remove(struct platform_device *pdev) { struct fan_priv *priv platform_get_drvdata(pdev); if (priv priv-pwm) pwm_disable(priv-pwm); } static const struct of_device_id fan_of_match[] { { .compatible vendor,pwm-fan }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, fan_of_match); static struct platform_driver fan_driver { .probe fan_probe, .remove fan_remove, .driver { .name fan, .of_match_table fan_of_match, }, }; module_platform_driver(fan_driver); MODULE_LICENSE(GPL);注意这里我用了一个细节pwm 的 period 是从 pwm_get_period 拿的而不是自己拿 1000000 这种魔法数字。因为设备树里 pwms pwm1 0 1000000 的第三个参数就是 period用 pwm_get_period 拿驱动就和设备树解耦了。后面你调 PWM 频率只改设备树就行驱动代码不用动。4. 瑞芯微平台实操设备树、编译与验证全流程技巧归技巧落到瑞芯微平台的工程里还得过设备树、编译、烧录这三关。我以 RK3568 为例把从设备树写起到验证多实例驱动的完整流程走一遍这几步每个瑞芯微工程师都应该烂熟于心。4.1 设备树节点写法与 pinctrl 注意点瑞芯微平台的内核设备树一般在 arch/arm64/boot/dts/rockchip/ 目录下不同 SDK 版本目录名略有差异但基本结构一致。要挂两路 PWM 风扇第一件事是确保对应的 PWM 控制器节点已经使能并且引脚复用配置正确。在 RK3568 的 dtsi 里PWM 控制器一般长这样pwm1: pwmfe6f0000 { compatible rockchip,rk3568-pwm; reg 0x0 0xfe6f0000 0x0 0x10; #pwm-cells 3; clocks cru CLK_PWM1, cru PCLK_PWM1; clock-names pwm, pclk; pinctrl-names default; pinctrl-0 pwm1_pin; status disabled; };实际项目里你要在板级 dts 里打开 PWM 控制器并且把 PWM 的引脚复用配置好pwm1 { status okay; pinctrl-names default; pinctrl-0 pwm1_pin; }; pwm3 { status okay; pinctrl-names default; pinctrl-0 pwm3_pin; };pinctrl-0 引用的是芯片 dtsi 里已经定义好的 pin 复用节点它会把对应的 GPIO 口复用成 PWM 功能。如果你 PWM 输出没有波形九成是这一步没配置对。瑞芯微不同芯片的 pinctrl 节点命名不一样RK3568 用 pwm1_pinRK3588 用 pwm1_m0_pin、pwm1_m1_pin 这种带 mux 组的命名写之前先在 dtsi 里搜一下。PWM 控制器使能之后再写你的风扇节点。我习惯把多个同类型设备放到根节点下每个节点用唯一 label 标识/ { fan0: pwm-fan0 { compatible vendor,pwm-fan; pwms pwm1 0 1000000; default-duty-percent 60; status okay; }; fan1: pwm-fan1 { compatible vendor,pwm-fan; pwms pwm3 0 1000000; default-duty-percent 80; status okay; }; };两个节点 compatible 相同pwms 分别指向 pwm1 和 pwm3这样驱动就会跑两个实例各自拿各自的 PWM 通道。这里有个容易踩的坑pwms pwm1 0 1000000 中的三个参数在瑞芯微平台分别代表 PWM 控制器句柄、通道号、周期纳秒。同一个 PWM 控制器下有两个通道的话第二个风扇可以写 pwm1 1 1000000但如果两个实例用了同一个 PWM 控制器的同一个通道probe 第二次会失败dmesg 里能看到 -EBUSY。4.2 编译驱动模块与内核 dtb驱动写好后编模块或编进内核都可以。瑞芯微 SDK 通常用 buildroot 或直接跑脚本但单独验证一个驱动最快的办法是编成 .ko 模块然后用 adb push 到板子上 insmod。步骤是把驱动源码放到内核目录下比如 drivers/thermal/fan.c然后在对应 Makefile 里加一行obj-m fan.o如果你不想动内核源码树直接在外面用一个独立目录写 Makefile 编模块也行前提是内核已经配置好并导出了编译符号obj-m : fan.o KDIR : /path/to/kernel-source CROSS_COMPILE : aarch64-none-linux-gnu- ARCH : arm64 all: $(MAKE) ARCH$(ARCH) CROSS_COMPILE$(CROSS_COMPILE) -C $(KDIR) M$(PWD) modules注意瑞芯微平台的内核版本从 4.19 到 6.1 都有如果你用的是 SDK 自带交叉编译器最好用 SDK 里编内核时的工具链避免 ABI 不匹配。编完模块后把 .ko 和设备树 dtb 一起推上板子。改过设备树的话需要重新编 dtb并确保 bootloader 加载的是新 dtb。瑞芯微平台上最快的方式是先 adb push 到 /boot/ 或 /dev/block/by-name/ 对应的分区然后 reboot具体烧录方式不同 SDK 略有出入。4.3 如何快速验证多实例驱动是否真的绑定了两个设备驱动加载后验证是系统工程里最容易被忽略的一环。我一般按三步检查能从设备树一路打通到驱动逻辑。第一步看设备是否被创建为 platform_device。在板子上执行ls /sys/bus/platform/devices/正常情况下能看到 pwm-fan0 和 pwm-fan1 两个目录。如果只有一个说明另一个节点没进入 platform bus优先怀疑设备树节点状态、compatible 拼写或者父节点的 simple-bus 属性。第二步看驱动是否成功 probe。执行dmesg | grep fan能看到两条 “fan probe ok” 的日志并且 pwm 名称不同就说明两个实例各自拿到了 PWM 通道。如果只有一条说明第二个实例 probe 失败或者根本没有匹配。第三步看功能是否正常。用示波器抓 PWM 引脚波形或者用万用表量风扇供电电压。如果第一路是 60% 占空比、第二路是 80% 占空比两个波形明显不同基本可以确认实例隔离没问题。没有示波器也可以先 cat /sys/kernel/debug/pwm 查看 PWM 通道状态确认 pwm1 和 pwm3 都处于 enable 状态。5. 常见问题与排查技巧实录这部分是我个人在瑞芯微平台调试多设备驱动时踩过的坑合集。有些问题看一眼 dmesg 就能定位有些问题会让你怀疑人生我把典型的都列出来你照着排查能省不少时间。5.1 多设备驱动问题速查表现象可能原因排查方法dmesg 看不到 probe 日志compatible 不匹配或设备树节点 disabled检查 /proc/device-tree 下对应节点 status 和 compatible只有第一个设备 probe 成功两个节点使用了同一份资源同一 PWM 通道/同一中断查看 dmesg 是否有 -EBUSY检查 dts 的 pwms、interrupts两个设备互相干扰、数据错乱驱动用了全局变量保存实例状态把所有实例状态移入私有数据结构probe 返回 -517 反复重试依赖的 PWM/GPIO 驱动未准备好确认依赖驱动是否加载使用 dev_err_probe 观察日志insmod 报 module not found缺少 MODULE_DEVICE_TABLE 导致 modprobe 匹配不上补上 MODULE_DEVICE_TABLE(of, xxx_of_match)PWM 输出没有波形pinctrl 引脚复用没配或 PWM 控制器 disabled确认 pinctrl-0 引用正确pwm 节点 status okay这里我想特别强调一下 -EPROBE_DEFER 的情况。瑞芯微平台外设驱动依赖多经常出现device_probe时某个子系统的驱动还没就绪。如果你的驱动在 probe 早期就调用 devm_of_pwm_get 或 devm_gpiod_get返回 -517 是很正常的内核会把它放在 deferred probe 列表里等依赖的驱动 probe 完成后再次调用你的 probe。千万不要一看到 517 就放弃也不要自己在驱动里写 sleep 重试。5.2 别踩的几个“坑中之坑”第一个坑是全局数组冒充多实例。有些老工程师习惯用static struct fan_priv fans[2]这种固定数组来管理两个实例看着像隔离了但一旦第三个节点加入数组越界问题立刻暴露。不如老老实实 devm_kzalloc内核帮你管理生命周期。第二个坑是设备树节点里的 status 忘了改。瑞芯微 dtsi 里很多外设节点默认 disabled你在板级 dts 里写了新节点但没把 PWM 控制器和风扇节点设成 okayprobe 永远不触发。这个坑我见过太多次检查顺序永远是先 status、再 compatible、再资源。第三个坑是中断号冲突。如果你第二个实例的中断在 dts 里写错指向了别的设备占用的中断号probe 会返回 -EBUSY而且这个错误信息很难一眼看出来因为中断子系统不会总是打印完整上下文。排查时先在 dmesg 里搜 irq 相关关键字再对照芯片手册核对中断号。第四个坑是 remove 和 probe 的对称性。你 probe 里用 devm 申请的资源remove 里不需要手动释放但如果 probe 里手动申请了非 devm 资源remove 里忘了释放下次 probe 就会出问题。建议从头到尾统一用 devm_ 系列接口省心。第五个坑是我自己的习惯问题在 probe 里打印调试信息时一定要用 dev_info(pdev-dev, ...) 而不是 printk(KERN_INFO ...) 或者自己拼接设备名字。platform bus 会自动把设备名比如 pwm-fan0、pwm-fan1作为前缀打印你一眼就能看出来现在是哪个实例在跑调试多设备时这个信息太值钱了。如果两个实例的日志长得一模一样你都不知道当前 probe 的是哪个节点。我自己的体会是多设备驱动的问题九成不是 Linux 框架不支持而是驱动作者用“单设备思维”写“多设备代码”。只要从设计一开始就把型号差异放进匹配表、把实例状态放进私有数据、把资源申请交给 devm_瑞芯微平台上的多设备驱动基本不会出大问题。最后分享一个小习惯拿到一个新板子我会先把设备树里所有跟目标外设相关的节点都拉出来过一遍把 compatible、status、资源属性抄到一张表里再写驱动。这个习惯帮我在后续调试中省了很多来回翻 dts 的时间你也可以试试。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价