瑞芯微平台Linux驱动多设备匹配实战:alias与of_match_table技巧
聊一个在工程里特别容易翻车的问题在瑞芯微平台上写Linux驱动明明设备树里放了两个功能相同的设备节点结果系统起来后只有第一个节点被驱动绑定了第二个怎么调都不probe。这个坑我踩过好多次身边同事也经常被卡住。很多人第一反应是“驱动里是不是哪里写死了”或者怀疑平台设备模型能力不够但问题的根源往往在于——我们把“一个驱动”和“一个设备”的对应关系想得太死了。Linux的设备模型从来就不是一对一的关系。一个driver可以匹配多个device每个device在probe时都会获得一份独立的私有数据结构这才是嵌入式项目里适配多路的正统写法。这篇文章就基于瑞芯微RK3568平台聊两个实际项目中天天用得上的小技巧一是用alias让同一个compatible匹配多个设备节点二是用一个of_match_table兼容多款compatible配置。最后我会放一个完整的PWM风扇驱动案例以及调试过程中踩过的暗坑。如果你正在做基于瑞芯微的Linux驱动开发或者刚接触设备树、字符设备驱动框架这篇内容应该能帮你少走不少弯路。1. 一个驱动只能绑一个设备问题出在模型理解上1.1 最典型的翻车现场先说一个我印象特别深的场景。当时要在RK3568的板子上控制四路风扇想着驱动就写一个设备树里复制粘贴四个节点把PWM通道、温度传感器节点改一下应该就成了。结果内核起来之后dmesg里只有第一个节点的probe信息后面三个设备连个影子都没有。当时第一反应是“compatible没写对”反复检查了device tree文件四个节点的compatible完全一样。然后又怀疑“是不是pinctrl冲突了”因为瑞芯微平台上PWM引脚复用很容易出现问题但排查一圈也没发现复用冲突。最后才意识到是设备树里没有给四个节点做有效区分驱动里也没有办法为每个节点分配独立的管理结构匹配流程到第二个节点时就直接放弃了。这种问题在瑞芯微平台特别常见因为很多工程师是从单片机或者裸机开发转过来的习惯性思维是“一个外设驱动对应一个硬件实体”。到了Linux这边驱动框架的服务对象是“设备模型”不是“物理外设”。你在设备树里写了多少个节点模型就会尝试创建多少个platform_device而你的driver要做的是准备好应对多次probe。1.2 平台设备模型里“driver”和“device”的真实关系把Linux平台设备模型拆开看核心就是两条链表一条挂driver一条挂device靠匹配规则把它们连接起来。在设备树开启的情况下凡是设备树里带有compatible属性的节点都会被内核注册为platform_device。驱动这边通过of_match_table声明自己支持哪些compatible总线匹配层扫描到对应节点就会调用驱动的probe。关键在于同一份of_match_table可以同时匹配多个设备节点内核会为每个匹配成功的节点调用一次probe并传入不同的struct platform_device指针。也就是说你的驱动只需要写一次probe逻辑但它会被反复执行。每一次执行时pdev-dev.of_node指向不同的设备树节点pdev-dev.platform_data、resource、中断号全部跟着节点走。所以“一个驱动只能绑定一个设备”这个说法是不成立的。真正的问题是你的probe里是不是用了一个全局变量来保存设备状态如果是第二次probe就会把第一次的状态覆盖掉第三个设备也就没办法正常工作了。正确做法是什么每个probe调用时用devm_kzalloc分配一份独立的私有数据然后通过platform_set_drvdata挂到设备上后续open、ioctl、read、write都是通过这份私有数据操作对应硬件。1.3 你要做的不是“一个驱动对应一个设备”而是“一个驱动管理一堆实例”理解了模型之后写多设备驱动的思路就清晰了驱动不再是一对一的“控制器”而是一套“管理框架”。每个设备树节点都应该是独立实例有自己的PWM通道、中断号、GPIO引脚、私有数据、甚至独立的状态机。在瑞芯微的实际项目中这种模式覆盖的场景非常广。比如多个MIPI摄像头、多路GPIO按键、多路PWM风扇、多个同款I2C触摸屏还有热词里常提到的CH340、CP2102这类USB转串口芯片底层驱动也是一个驱动支持多个设备只不过它们走的是USB子系统实例管理靠struct usb_serial_driver的多个port节点。原理是相通的用独立的私有数据去描述“这一个”设备。想明白这一点后面两个技巧就是一层窗户纸的事。2. 小技巧一靠alias编号让probe知道自己管的是谁2.1 设备树里放多个同compatible节点先看一个最简单但也最有效的做法。回到PWM风扇的例子设备树里我们写多个pwm-fan节点compatible完全一样但使用的PWM通道不同。/ { compatible rockchip,rk3568-evb; fan0: pwm-fan { compatible rockchip,pwm-fan; pwms pwm0 0 25000 0; #cooling-cells 2; status okay; }; fan1: pwm-fan { compatible rockchip,pwm-fan; pwms pwm1 0 25000 0; #cooling-cells 2; status okay; }; };有人会问两个节点label分别是fan0和fan1驱动能不能直接读label不能。label是给设备树编译和引用时用的符号内核驱动运行时没法可靠地通过label来区分节点。这里需要的是alias机制。/ { aliases { fan0 fan0; fan1 fan1; }; };别小看这个alias它解决的是“设备编号”的问题。设备树里写两个一模一样的compatible驱动probe时收到的of_node不同但你没法知道谁是谁。industrial I/O子系统里的iio_dev、regmap子系统里的regmap、以及GPIO子系统里的gpiochip全都是靠alias或者硬件寄存器基址来区分实例的。在平台驱动里of_alias_get_id就是最顺手的工具。2.2 probe里取alias和设备索引驱动的of_match_table只要写一组compatible就够了static const struct of_device_id pwm_fan_of_match[] { { .compatible rockchip,pwm-fan, }, { } }; MODULE_DEVICE_TABLE(of, pwm_fan_of_match);probe函数里用of_alias_get_id(dev-of_node, fan)来拿当前节点对应的编号static int pwm_fan_probe(struct platform_device *pdev) { struct device *dev pdev-dev; struct pwm_fan_priv *priv; int id; priv devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL); if (!priv) return -ENOMEM; id of_alias_get_id(dev-of_node, fan); if (id 0) { dev_warn(dev, no alias defined, use -1\n); id pdev-id; } priv-id id; priv-pwm devm_of_pwm_get(dev, dev-of_node, NULL); if (IS_ERR(priv-pwm)) return PTR_ERR(priv-pwm); platform_set_drvdata(pdev, priv); dev_info(dev, fan%d probed ok\n, priv-id); return 0; }这里有几个细节值得说清楚devm_of_pwm_get是配套设备树API老内核4.19之前比较常见新内核推荐直接devm_pwm_get(dev, NULL)内部会走设备树解析。瑞芯微SDK现在主流是4.19和5.10两个内核版本写的时候留意一下差异。pdev-id在设备树没有提供reg的情况下通常是从-1开始的所以不要依赖它做编号alias才是那个“给设备起名”的正主。#cooling-cells是给温控子系统用的这里可以先不管但后面如果要把风扇接入thermal管理这个属性必须带上。2.3 资源获取pwm、gpio、中断都是按实例来的很多新手在写多设备驱动时习惯在probe里把PWM通道、GPIO号写死或者用数组索引去“指定”硬件资源。这在设备树体系下是没有必要的。各种devm_开头的资源获取API天然就是跟着of_node走的。devm_of_pwm_get、devm_gpiod_get、platform_get_irq它们读取的全是当前of_node下解析出来的资源。比如两个pwm-fan节点分别挂pwm0和pwm1驱动里根本不需要做任何区分API会从各自的pwms属性里拿到对应的struct pwm_device。你只需要把拿到的pwm指针存进当前probe对应的priv里后续操作就是独立的了。GPIO同理priv-en_gpio devm_gpiod_get_optional(dev, enable, GPIOD_OUT_LOW); if (IS_ERR(priv-en_gpio)) return PTR_ERR(priv-en_gpio);如果设备树节点里有enable-gpios属性这个API会拿到对应的gpio_desc没有就返回NULL。这样一套代码无论多少个设备节点只要属性名统一资源解析都是按节点逐个走的。2.4 这个方案为什么在瑞芯微上特别好用瑞芯微平台是典型的SoC平台芯片自带的PWM、I2C、SPI、UART控制器数量很多RK3568上有16路PWM多路复用的情况非常普遍。加上瑞芯微的SDK默认就在dts里铺好了大量aliases比如serial0 ~ serial9、i2c0 ~ i2c5、pwm0 ~ pwm15驱动里通过alias拿编号几乎不需要额外改dts就能匹配到已经规划好的编号体系。另外瑞芯微平台经常出现“同一份内核源码适配多个项目”的情况。A项目用两路风扇B项目用四路风扇如果驱动里写死编号或者资源两个项目就得维护两份驱动。用alias方案设备树里加一个aliases条目删除一个节点驱动完全不需要动灵活度一下就上来了。3. 小技巧二一张of_match_table匹配多款compatible3.1 多compatible解决“同驱动兼容不同硬件”的场景很多时候项目不只是“多个相同设备”而是“多个相似但不完全相同的设备”。举个例子A版本的风扇驱动PWM极性是正极性B版本的PWM极性是负极性A版本的调速范围是0到255B版本是0到1023。这些硬件差异如果靠设备树节点里的属性去描述当然也能做但更常见也更成熟的做法是定义多个compatible每个compatible对应一种硬件配置。of_match_table支持多个条目这是内核驱动开发里的基础能力但很多人在实际使用时只用了一组compatible。这个技巧应该成为习惯把of_match_table当作一张“配置索引表”每个条目不光有compatible字符串还能挂一个data指针指向该硬件变体的配置结构。enum fan_variant { FAN_V1, FAN_V2, }; struct fan_config { enum fan_variant variant; bool pwm_polarity_inverted; int max_speed; }; static const struct fan_config fan_cfg_v1 { .variant FAN_V1, .pwm_polarity_inverted false, .max_speed 255, }; static const struct fan_config fan_cfg_v2 { .variant FAN_V2, .pwm_polarity_inverted true, .max_speed 1023, }; static const struct of_device_id pwm_fan_of_match[] { { .compatible rockchip,pwm-fan, .data fan_cfg_v1 }, { .compatible rockchip,pwm-fan-v2, .data fan_cfg_v2 }, { } }; MODULE_DEVICE_TABLE(of, pwm_fan_of_match);probe里用of_device_get_match_data一行拿到当前设备对应的配置static int pwm_fan_probe(struct platform_device *pdev) { struct device *dev pdev-dev; const struct fan_config *cfg; struct pwm_fan_priv *priv; cfg of_device_get_match_data(dev); if (!cfg) return -EINVAL; priv devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL); if (!priv) return -ENOMEM; priv-cfg cfg; ... }这样驱动逻辑里后续所有需要“根据硬件型号做不同处理”的地方都从priv-cfg里读取配置而不是写一堆if else到处判断字符串。3.2 用data挂不同配置一个probe适配所有变体为什么推荐把“匹配compatible”和“硬件配置”绑定在of_match_table里而不是在probe里用of_node_compatible去一个个判断因为of_match_table是静态表内核在匹配阶段就做了字符串比对数据存放位置固定效率高而且编译时就能检查配置结构是否存在。更重要的是这种写法让驱动“声明自己支持什么”变得一目了然。probe函数应该保持“干净”。尽量不要做大量分支判断来确定硬件类型而是先把匹配到的配置取出来后面统一用配置字段驱动逻辑。我来举个例子static void pwm_fan_set_speed(struct pwm_fan_priv *priv, int speed) { u32 duty_cycle; int ret; if (speed 0) speed 0; if (speed priv-cfg-max_speed) speed priv-cfg-max_speed; duty_cycle speed * priv-period / priv-cfg-max_speed; if (priv-cfg-pwm_polarity_inverted) duty_cycle priv-period - duty_cycle; ret pwm_config(priv-pwm, duty_cycle, priv-period); if (ret) dev_err(priv-dev, pwm_config failed: %d\n, ret); }这只是一个简单示例但它能说明一个核心思想配置和逻辑解耦。以后产品出了V3版本只需要在驱动里加一组fan_cfg_v3再往of_match_table里插入一个条目不需要改动任何算法代码。做过多产品线的人应该能体会这个设计有多省事。3.3 跨平台扩展rk3568/rk3588/rv1126一套驱动瑞芯微平台之间其实是同源SDK芯片内部外设控制器虽然各有不同但设备树绑定接口大体一致。你在rk3568上写的驱动只要of_match_table里兼容字符串写得规范拿到rk3588或者rv1126的SDK里基本能直接编译加载。复用驱动的关键是不要把平台相关的寄存器操作散落在驱动代码里。PWM子系统、GPIO子系统、时钟和复位框架rk3568和rk3588都提供了标准内核API驱动通过pwm_apply_state、clk_prepare_enable这些通用接口操作硬件芯片差异由各子系统的底层驱动去吸收。这么一来一张of_match_table同时列出多个平台的compatible加上各自的data配置就能做到“一套驱动适配三块芯片”。当然跨平台不是免费午餐。比如RK3568上PWM的极性配置可能直接在设备树里用pwm-polarity属性描述而rk3588上可能要求在驱动里调用pwm_apply_state时设置PWM_POLARITY_INVERSED这些差异最好都收敛到cfg里。最忌讳的是在驱动里写#ifdef CONFIG_ARCH_ROCKCHIP_RK3588这类宏一旦编译进错误的内核代码路径直接跑偏还没法在设备树层面纠正。4. 实战RK3568上做一个多路PWM风扇驱动4.1 设备树怎么铺把前面两个技巧结合起来看一个完整案例。假设板子上有两路PWM风扇分别挂在pwm0和pwm1上其中pwm0对应的风扇是V1版本pwm1对应的是V2版本。设备树里这样写/ { aliases { fan0 fan0; fan1 fan1; }; fan0: pwm-fan-0 { compatible rockchip,pwm-fan; pwms pwm0 0 25000 0; #cooling-cells 2; status okay; }; fan1: pwm-fan-1 { compatible rockchip,pwm-fan-v2; pwms pwm1 0 25000 0; #cooling-cells 2; status okay; }; };两个节点一个用rockchip,pwm-fan一个用rockchip,pwm-fan-v2alias里分别叫fan0和fan1。这样驱动的of_match_table就能同时匹配它们probe会被调用两次每次拿到各自对应的alias编号和配置。这里要注意一个细节设备树节点的命名建议带一个索引后缀比如pwm-fan-0虽然它不影响驱动匹配但使用设备树覆盖或调试时一眼能看出节点归属。4.2 驱动框架怎么搭完整的驱动我用字符设备框架来演示因为大多数场景下用户态需要能够独立控制每路风扇。字符设备只是其中一种实现方式也可以用miscdevice、sysfs、甚至直接通过thermal子系统暴露冷却设备取决于产品需求。#include linux/module.h #include linux/platform_device.h #include linux/of.h #include linux/of_device.h #include linux/pwm.h #include linux/miscdevice.h #include linux/fs.h #include linux/uaccess.h #define FAN_IOC_MAGIC F #define FAN_IOCT_SET_SPEED _IOW(FAN_IOC_MAGIC, 1, int) #define FAN_IOCT_GET_SPEED _IOR(FAN_IOC_MAGIC, 2, int) struct pwm_fan_priv { struct device *dev; struct pwm_device *pwm; const struct fan_config *cfg; struct miscdevice mdev; int id; int speed; unsigned int period_ns; int fan_min_speed; int fan_max_speed; }; static int pwm_fan_set_speed(struct pwm_fan_priv *priv, int speed) { u64 duty; if (speed priv-fan_min_speed) speed priv-fan_min_speed; if (speed priv-fan_max_speed) speed priv-fan_max_speed; duty (u64)speed * priv-period_ns / 255; return pwm_config(priv-pwm, duty, priv-period_ns); }ioctl分发函数处理两路风扇各自的struct pwm_fan_privstatic long pwm_fan_ioctl(struct file *file, unsigned int cmd, unsigned long arg) { struct pwm_fan_priv *priv file-private_data; int speed; switch (cmd) { case FAN_IOCT_SET_SPEED: if (copy_from_user(speed, (void __user *)arg, sizeof(speed))) return -EFAULT; if (pwm_fan_set_speed(priv, speed)) return -EIO; priv-speed speed; return 0; case FAN_IOCT_GET_SPEED: speed priv-speed; if (copy_to_user((void __user *)arg, speed, sizeof(speed))) return -EFAULT; return 0; default: return -ENOTTY; } } static int pwm_fan_open(struct inode *inode, struct file *file) { struct pwm_fan_priv *priv container_of(file-private_data, struct pwm_fan_priv, mdev); file-private_data priv; return 0; } static const struct file_operations pwm_fan_fops { .owner THIS_MODULE, .open pwm_fan_open, .unlocked_ioctl pwm_fan_ioctl, };probe里把每个设备注册成一个唯一的miscdevice设备名带编号static int pwm_fan_probe(struct platform_device *pdev) { struct device *dev pdev-dev; const struct fan_config *cfg; struct pwm_fan_priv *priv; int id, ret; cfg of_device_get_match_data(dev); if (!cfg) return -EINVAL; id of_alias_get_id(dev-of_node, fan); if (id 0) { dev_err(dev, missing alias fan\n); return -EINVAL; } priv devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL); if (!priv) return -ENOMEM; priv-dev dev; priv-id id; priv-cfg cfg; priv-pwm devm_pwm_get(dev, NULL); if (IS_ERR(priv-pwm)) return PTR_ERR(priv-pwm); priv-period_ns pwm_get_period(priv-pwm); if (!priv-period_ns) return -EINVAL; platform_set_drvdata(pdev, priv); priv-mdev.minor MISC_DYNAMIC_MINOR; priv-mdev.name devm_kasprintf(dev, GFP_KERNEL, pwm_fan%d, id); priv-mdev.fops pwm_fan_fops; priv-mdev.parent dev; ret misc_register(priv-mdev); if (ret) return ret; dev_info(dev, pwm_fan%d: probe ok\n, id); return 0; }用miscdevice的好处是省去了自己管理主设备号和次设备号的麻烦每个实例的次设备号由内核动态分配/dev/pwm_fan0、/dev/pwm_fan1自然出现。如果你有特殊需求要固定次设备号或者一个设备下面要暴露多个子设备那才需要去管alloc_chrdev_region和cdev_add。4.3 加载验证与sysfs操作编译成模块后功能验证非常简单insmod pwm_fan.ko dmesg | tail -n 20期望看到两条probe日志pwm_fan0: probe ok pwm_fan1: probe ok如果只出现一条说明第二个设备节点没有被匹配上原因大概率是alias缺失或者设备的status属性不是okay。测试调速echo 128 /sys/class/misc/pwm_fan0 # 不是这个路径用ioctl采用ioctl方式时客户端程序调用int fd open(/dev/pwm_fan0, O_RDWR); int speed 180; ioctl(fd, FAN_IOCT_SET_SPEED, speed); close(fd);用示波器量PWM引脚的占空比应该能看到对应变化。这个案例里fan_min_speed、fan_max_speed没有从设备树读取而是留作空白实际产品中建议把这些参数也做成设备树属性或者挂进cfg结构避免修改不同转速档位时重新编译内核模块。5. 多设备驱动里的暗坑与排错手记5.1 只有第一个节点probe的排查链路这个现象应该算多设备驱动最经典的故障了。排除设备树语法错误以后按下面的顺序排查第一步确认设备节点是否生成了platform_device。在设备树里给节点加上status disabled再开机看驱动是否只剩另一个节点probe。如果disabled的节点仍然probe说明你的dtsi被其他文件覆盖了搜索整个dts目录里是否存在同名label修改。第二步确认drivers/of/platform.c是否处理了这些节点。根节点下没有reg的节点内核会创建platform_device但不会分配resource。如果驱动在probe里强制调用platform_get_resource(pdev, IORESOURCE_MEM, 0)就会得到NULL然后返回-ENXIO导致该设备匹配失败。第三步检查alias编号是否冲突。两个节点都写fan0系统会保留第一个第二个的of_alias_get_id也会返回一个有效编号但语义就乱了。alias编号应该全局唯一这点和设备树编译器没有硬性约束只能靠人工保证。我遇到过一个特别隐蔽的情况客户的内核定制里有一份久远的补丁把of_alias_get_id改成了只检查/aliases节点里的第一个匹配项导致后续节点全部拿到-1。这种改了内核基础行为的坑排查起来非常耗时间所以遇到“多个设备不probe”的问题时第一件事永远是看内核版本和补丁历史。5.2 次设备号冲突与ida使用时机用miscdevice时次设备号由内核自动分配基本不会冲突。一旦你决定手动管理多个设备节点的次设备号问题就来了。比如你希望fan0对应次设备号0fan1对应次设备号1注册的顺序和设备树扫描的顺序不一定一致手动硬编码很容易越界或者重复。推荐做法是用内核的ida机制动态分配次设备号释放时用ida_free回收。举一个简化流程static DEFINE_IDA(fan_ida); int minor ida_alloc_range(fan_ida, 0, 15, GFP_KERNEL); if (minor 0) return minor; dev_t devno MKDEV(major, minor); cdev_add(priv-cdev, devno, 1);这类代码在实际的多路设备驱动里很常用尤其是当设备支持热插拔时次设备号的管理更是必须依靠ida。虽然平台设备一般不支持热插拔但模块卸载再重新加载时ida能够保证编号不会因为probe顺序变化而错乱。5.3 设备树compatible字符串与匹配顺序的误解设备树里的compatible属性可以写多个字符串比如compatible rockchip,pwm-fan-v2, rockchip,pwm-fan;内核匹配驱动时是逐个尝试的优先与驱动of_match_table里的条目进行匹配。有些工程师会在of_match_table里同时保留新旧两个compatible目的是兼容旧设备树。这种“新旧字符串共存”的做法本身没问题但要注意一件事data字段指向的配置结构是跟随“当前匹配到的那一个条目”的。举例来说如果设备树的compatible只写了rockchip,pwm-fan即使你的of_match_table里rockchip,pwm-fan-v2排在前面of_device_get_match_data返回的也是rockchip,pwm-fan对应条目的data而不是第一个条目的data。所以不要把“排在最前面的data”当成默认值。想默认走某套配置就老老实实把那条compatible放最前面或者让所有variant的data字段都指向同一个基础配置。还有一个容易被忽略的点设备树节点里compatible字符串的顺序也会影响匹配结果。内核优先匹配设备树属性中靠前的字符串所以如果在你的dts里compatible rockchip,pwm-fan-v2, rockchip,pwm-fan;那么匹配到的可能是v2条目而不是基础的pwm-fan条目。出现“驱动看起来应该被匹配但行为却不是你预期”的情况时先检查设备树的compatible书写顺序。5.4 多设备驱动调试时常用到的几个命令调试这类驱动时这几条命令基本每回都要用到# 查看设备树里当前节点是否正常、status是否为okay ls /proc/device-tree/ cat /proc/device-tree/pwm-fan-0/compatible # 查看平台设备和驱动的绑定关系 ls /sys/bus/platform/devices/ cat /sys/bus/platform/drivers/pwm-fan/*/modalias # 强制解绑某个设备节点再重新绑定省去 reboot echo pwm-fan-0 /sys/bus/platform/drivers/pwm-fan/unbind echo pwm-fan-0 /sys/bus/platform/drivers/pwm-fan/bind强制解绑再绑定的方式特别适合验证“多个设备节点独立probe”的逻辑。你可以在驱动运行期间卸下某一个设备实例看其他实例是否不受影响。如果某个实例的释放回调在unbind时崩了说明私有数据里存在共享资源没有做好引用计数这类问题在正常开机流程中往往很难暴露一旦暴露就是比较棘手的内存越界。还有一个小技巧把驱动的probe时间点打上tracetrace-cmd record -e platform:platform_driver_probe -e platform:platform_driver_register然后触发加载trace里能看到所有设备节点的匹配结果以及哪些节点返回了probe_failed。瑞芯微平台内核默认开启了FTRACE如果没开可以编译内核时打开CONFIG_FTRACE和CONFIG_TRACING相关选项。回看这些坑其实根源都是同一个写驱动时把设备看成了“一个独立功能”而不是“设备树中的一个节点实例”。只要思维转过来多设备支持并没有想象中那么复杂。工具链、内核API、瑞芯微的SDK都替你把这些底层细节处理好了你要做的只是正确地声明、正确地取配置、正确地管理私有数据。希望这里聊的两个小技巧和几张排查路径图能让你在调试别的驱动的路上少走几条弯路。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →