ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

瑞芯微Linux驱动开发:一个驱动支持多设备的两大实用技巧

瑞芯微Linux驱动开发:一个驱动支持多设备的两大实用技巧 做瑞芯微平台驱动开发的朋友几乎都会遇到这个问题板子上不止挂一个同类设备。比方说我最近在RK3568上调一个温度采集模块I2C总线下面带了两颗同型号的传感器另一个项目里还想让同一个驱动兼容三款规格不太一样的触摸IC。翻译成Linux的话就是“一个驱动支持多个设备”——既要能一驱多型号也要能一驱多实例。内核的device/driver模型本来就是为了解决这种问题设计的真正的难点在于你不熟悉它的套路。这篇文章不聊虚的直接把我在瑞芯微SDK上验证过的两个小技巧拆开讲代码可以直接抄到你的驱动里改一改就跑。1. 先把匹配机制弄明白设备树和驱动是怎么对上眼的1.1 Linux里“设备”和“驱动”不是一回事靠总线牵线很多刚接触驱动开发的人会把设备和驱动混为一谈其实Linux里这是两个独立的对象设备是硬件事实物理上存在一颗芯片挂在某条总线上某个地址驱动是软件能力我知道怎么操作这一类芯片。两者靠总线bus来牵线搭桥。具体到瑞芯微平台设备侧的描述基本都用设备树Device Tree来完成。你在dts里写了一个I2C子节点内核启动时解析设备树注册一个i2c_client设备驱动侧则注册一个i2c_driver。然后由I2C核心i2c-core去检查这个i2c_driver的匹配规则里有没有哪一条能匹配上这个i2c_client匹配成功就调用probe函数驱动才算真正接管了这颗芯片。这里有一张非常关键的“匹配规则表”驱动的名字叫作of_match_table旧内核里也叫of_device_id数组设备树侧的名字叫作compatible属性。两者里的字符串必须一字不差否则内核认为“你们不熟”。static const struct of_device_id mydrv_of_match[] { { .compatible vendor,chip-model }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, mydrv_of_match); static struct i2c_driver mydrv_driver { .driver { .name mydrv, .of_match_table mydrv_of_match, }, .probe mydrv_probe, .id_table mydrv_id_table, };MODULE_DEVICE_TABLE这行容易被忽略它的作用是生成模块的别名alias让内核在加载模块时知道这个驱动能给哪些设备使用。如果少了它设备树解析出来的设备即使compatible匹配也可能出现“模块没自动加载”的奇怪现象。1.2 I2C设备和平台设备要分清别把驱动类型搞混瑞芯微平台上有两类常见的设备驱动一类是挂在I2C/SPI等外设总线上的比如传感器、触摸IC、RTC、Codec驱动要注册成i2c_driver或spi_driver另一类是直接挂在CPU内部的platform总线上的比如看门狗、GPIO按键、PWM背光驱动注册成platform_driver。这两类驱动匹配设备的逻辑大同小异platform_driver通过compatible匹配设备树节点i2c_driver除了compatible还可能依赖id_table匹配。只不过设备节点在设备树里的“挂点”不一样I2C设备必须放在i2c控制器节点下面platform设备一般放在根节点或amba等总线节点下面。提示调试时不probe第一件事看设备节点挂在哪个父节点底下。我曾见过有人把I2C设备抄到根节点下驱动写对了设备树编译也过了但i2cdetect就是扫不到原因就是节点位置不对。2. 小技巧一一个驱动适配多款不同型号的设备2.1 为什么需要“一驱多型号”产品迭代太快同一款主板可能今天贴A厂的触摸屏明天换B厂的两家芯片寄存器配置不完全一样但功能都是“上报触摸坐标”。如果给每颗芯片单独维护一个驱动文件代码冗余还要处理多个驱动同时匹配同一个设备树节点的问题。正确做法是一个驱动of_match_table里列多组compatible每组compatible对应一份“芯片差异配置”。probe的时候内核会告诉我们“你匹配上了哪一条”我们据此初始化不同的寄存器参数。更妙的是of_device_id结构体里有一个data字段专门用来携带自定义数据。这个字段是void *类型可以指向任意结构体是“一驱多型号”的核心枢纽。2.2 用driver_data记录芯片差异别再拿字符串判断举个例子。我写过一个小型温度传感器驱动要同时兼容TI的TMP117、TMP116和TMP175三颗芯片都挂在I2C总线上分辨率、转换时间、寄存器偏移都有区别。先定义差异参数结构体和三份配置struct tsensor_chip_info { unsigned int resolution_bits; unsigned int default_rate_hz; unsigned int conf_reg_addr; unsigned int temp_reg_addr; }; static const struct tsensor_chip_info tmp117_info { .resolution_bits 16, .default_rate_hz 8, .conf_reg_addr 0x03, .temp_reg_addr 0x00, }; static const struct tsensor_chip_info tmp116_info { .resolution_bits 16, .default_rate_hz 4, .conf_reg_addr 0x03, .temp_reg_addr 0x00, }; static const struct tsensor_chip_info tmp175_info { .resolution_bits 12, .default_rate_hz 4, .conf_reg_addr 0x01, .temp_reg_addr 0x00, };然后匹配表里把数据和compatible绑在一起static const struct of_device_id tsensor_of_match[] { { .compatible ti,tmp117, .data tmp117_info }, { .compatible ti,tmp116, .data tmp116_info }, { .compatible ti,tmp175, .data tmp175_info }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, tsensor_of_match);probe里取出这条datastatic int tsensor_probe(struct i2c_client *client) { const struct of_device_id *match; const struct tsensor_chip_info *info; struct tsensor_priv *priv; match of_match_device(tsensor_of_match, client-dev); if (!match || !match-data) { dev_err(client-dev, no matched device info\n); return -ENODEV; } info match-data; priv devm_kzalloc(client-dev, sizeof(*priv), GFP_KERNEL); if (!priv) return -ENOMEM; priv-info info; priv-client client; i2c_set_clientdata(client, priv); /* 后续初始化寄存器用 info-conf_reg_addr 等字段 */ ... }这里有个细节of_match_device的返回值和client-dev.of_node直接拿node做字符串匹配不一样它会自动遍历匹配表找到之后把整条of_device_id返回给我们所以我们才能拿到.data。这也是为什么不推荐在probe里写if (of_node-name)或者strcmp(compatible, ti,tmp117)这种硬编码判断——不仅啰嗦而且一旦设备树里的compatible改了驱动也要跟着改而用匹配表的data字段驱动和设备树完全解耦新增一款芯片时只需要加一条配置不用动probe逻辑。2.3 别忘了一个附加工作i2c_device_id表也要同步维护I2C子系统还有一个传统匹配通道i2c_device_id表它服务于那种没有设备树的场景比如板卡通过ACPI或者直接实例化I2C设备。虽然瑞芯微基本都走设备树但在老内核SDK中有些模块还是会把i2c_device_id作为probe参数传入。static const struct i2c_device_id tsensor_id[] { { tmp117, (kernel_ulong_t)tmp117_info }, { tmp116, (kernel_ulong_t)tmp116_info }, { tmp175, (kernel_ulong_t)tmp175_info }, { } }; MODULE_DEVICE_TABLE(i2c, tsensor_id);上面这段代码里i2c_device_id的第二个字段driver_data同样可以用来携带差异参数。probe函数可以声明成接受id参数的形式static int tsensor_probe(struct i2c_client *client, const struct i2c_device_id *id) { const struct tsensor_chip_info *info NULL; const struct of_device_id *match; /* 设备树路径优先 */ match of_match_device(tsensor_of_match, client-dev); if (match match-data) { info match-data; } else if (id id-driver_data) { info (const struct tsensor_chip_info *)id-driver_data; } ... }实测下来这个“双保险”写法在不同的SDK内核版本之间移植时特别省心。有的内核版本probe回调已经从3个参数变成2个参数但基本的匹配逻辑不变把设备树匹配和id表匹配都处理好兼容性就稳了。3. 小技巧二一个驱动同时驱动多个同型号设备实例3.1 两个一模一样的芯片内核会不会“打架”还是用温度传感器举例。我的板子上有两颗TMP117一颗挂在I2C3总线上用来测环境温度另一颗挂在I2C4总线上用来测设备内部温升。两颗芯片型号一致、驱动一致设备树里只要写两个节点i2c3 { status okay; clock-frequency 100000; tmp117_env: tmp11748 { compatible ti,tmp117; reg 0x48; }; }; i2c4 { status okay; clock-frequency 100000; tmp117_board: tmp11748 { compatible ti,tmp117; reg 0x48; }; };这里会有一个很多人第一次都会问的问题两个节点compatible一模一样驱动是不是只probe一次不会。内核的逻辑是每注册一个i2c_client设备就独立执行一次匹配和probe。也就是说probe会被调用两次每次传入的i2c_client指针各自指向自己的设备实例。I2C核心会根据设备节点挂在哪个控制器下自动把地址解析成“总线号从机地址”比如3-0048和4-0048两者互不干扰。所以在驱动层面“支持多实例”其实不需要额外做什么惊天动地的操作关键在于绝对不能把设备私有状态放成全局变量。3.2 每个实例一套私有数据这是多设备驱动的命根子假设我在probe里定义了一个全局变量static struct tsensor_priv *g_priv;来保存设备信息那么第二颗芯片probe时就会把第一颗的指针覆盖掉。一旦两个设备都要读数据互相踩踏读出来的温度可能永远来自同一颗芯片。这种bug极难排查表现还非常随机。正确套路是probe里为每个实例单独分配一份私有数据结构所有状态、锁、缓冲区都放在这个结构体里struct tsensor_priv { struct i2c_client *client; struct mutex lock; struct regmap *regmap; const struct tsensor_chip_info *info; unsigned int last_temp_milli; struct miscdevice mdev; }; static int tsensor_probe(struct i2c_client *client) { struct tsensor_priv *priv; ... priv devm_kzalloc(client-dev, sizeof(*priv), GFP_KERNEL); if (!priv) return -ENOMEM; mutex_init(priv-lock); priv-client client; i2c_set_clientdata(client, priv); ... }devm_kzalloc是设备资源管理的标配接口。它跟着设备生命周期走设备树节点被销毁、驱动卸载时自动释放内存不需要我们在remove里一个个配对free省掉一大票“内存泄漏”隐患。i2c_set_clientdata则是把priv和client绑定后续在读写函数里用struct tsensor_priv *priv i2c_get_clientdata(client);拿回来。提示判断一个驱动适不适合多设备最粗暴的方法就是搜索源文件里有没有非const的全局变量。凡是带“状态”的全局变量在多实例场景下基本都会出问题。const常量表没关系比如上面的chip_info配置它们只读不写可以放心共享。3.3 container_of妙用从公共接口切回私有数据多实例驱动里经常遇到一个问题我们给每个设备创建一个miscdevice字符设备节点用户空间通过/dev/tsensor0、/dev/tsensor1分别读取两颗芯片的温度。但是miscdevice的操作函数比如read里只给我们一个struct file *怎么知道当前打开的是哪颗芯片答案就是container_of。先把miscdevice作为私有数据结构的一个成员这也意味着每个实例都有自己独立的miscdevice而file-private_data在open的时候会被内核设置为这个miscdevice指针static int tsensor_open(struct inode *inode, struct file *filp) { struct miscdevice *mdev filp-private_data; struct tsensor_priv *priv container_of(mdev, struct tsensor_priv, mdev); filp-private_data priv; return 0; } static ssize_t tsensor_read(struct file *filp, char __user *buf, size_t count, loff_t *pos) { struct tsensor_priv *priv filp-private_data; unsigned int temp; int ret; mutex_lock(priv-lock); ret tsensor_read_temp(priv, temp); mutex_unlock(priv-lock); if (ret) return ret; return simple_read_from_buffer(buf, count, pos, temp, sizeof(temp)); }container_of在这里做了一件事根据成员地址反推出整个结构体的起始地址。用生活类比就像你知道你家客厅沙发的位置然后根据设计图纸推出整栋房子的门牌号。这个宏是Linux内核驱动的基本功多实例驱动里出现频率极高因为几乎所有子系统回调file_operations、i2c_driver回调、中断处理函数、hwmon的读接口拿到的基本都是“公共结构体指针”要拿回私有数据必须靠它。注意一点open里我用了filp-private_data重新保存priv这样read里就不用再做一次container_of。这个小细节也可以让代码更容易理解。不过要记得miscdevice的.this_device、.parent等字段要保证设备树热插拔时引用关系正确否则可能出现“用户空间打开了设备driver卸载了read直接崩溃”的问题。虽然简单驱动不太会遇到但还是在remove里做好互斥或引用计数更稳。3.4 同型号多实例的另一个坑寄存器并发访问要加锁两个实例属于不同的I2C客户端但I2C控制器是分开的I2C3和I2C4理论上并发访问各自独立。但如果两颗芯片恰好挂在同一条I2C总线上而且邮递员I2C控制器是同一个读写操作如果交错进行可能会被内核的I2C核心自动串行化通常不会出问题。可一旦驱动里存在内部状态比如先写寄存器A再读寄存器B两次I2C传输之间要求原子性就必须用mutex保护好否则另一个实例的线程可能插进来导致读到错位的寄存器数据。我在调试时遇到过一种更隐蔽的情况两颗芯片挂在不同I2C总线上但驱动里用了一个全局的regmap_config里面不小心写了lock成员导致两个实例共享同一把锁。看起来没毛病实际上A总线上的设备被B总线上的读操作阻塞几毫秒的操作被拖到几十毫秒。排查下来才发现是把regmap的锁配置写死了给每个实例单独分配regmap之后问题消失。这就是“共享状态”的又一典型反面教材锁本身也要跟实例绑定不能全局共用一把锁。4. 设备没probe、数据串了这些排查经验能救命4.1 设备树节点没问题为什么驱动就是不被probe这是提问率最高的问题。我列一个排查清单按顺序走一遍基本能定位检查compatible字符串。一个字符都不能差大小写、厂商前缀都要对。ti,tmp117和ti,tmp1170不是一回事。最土的办法是把设备树反编译出来拿文本和驱动的匹配表逐字对比。检查设备树节点状态。默认写status okay如果某个父节点比如I2C控制器节点被设成disabled子节点一起跟着失效。检查节点挂载位置。I2C设备必须放在I2C控制器的子节点里不能放在根下。瑞芯微的dtsi里I2C控制器可能默认disabled板级.dts要覆盖成okay。检查地址是否真实存在。用i2cdetect -y bus扫描总线看0x48地址有没有出现UU或者48。如果扫不到先检查硬件上拉和芯片供电驱动写得再好也没用。检查驱动模块是否被加载。lsmod | grep tsensor没有就modprobe再dmesg看有没有提示Unknown symbol或者Invalid module format。检查到底走的是哪条匹配路径。有时i2c_device_id表和of_match_table同时存在内核优先级、匹配逻辑在版本间有细节差异可以在probe里临时加dev_info打印哪条路径进来的。4.2 两个实例都probe成功了数据却总是来自同一个设备如果一个驱动管理两个同型号芯片probe都成功了但上层读到的数据永远一模一样或者A设备读数据时会把B设备的配置改掉十有八九是“全局状态”问题。优先检查源文件里有没有非const全局变量包括结构体、数组、缓冲区、计数器。我踩过的最典型的坑是驱动里用了一个全局u8 rx_buf[8];来接收I2C读回的数据两个设备都是同一个驱动调用i2c_master_recv时都会往这个缓冲区写。A设备刚读完还没来得及解析B设备的读操作就把缓冲区内容覆盖了结果A设备解析出一堆乱码。解决办法非常简单把rx_buf挪进私有结构体里每个实例一份。后续所有I2C传输函数都通过priv-rx_buf访问彻底隔离。第二个隐形坑是中断处理。如果两颗芯片各自接了中断引脚共享同一个中断处理函数中断回调的参数必须带各自的priv指针不要在中断里再用全局变量去推断是哪个设备。我在注册中断时用的是devm_request_threaded_irq并把priv作为最后一个参数传入这样每个实例的中断上下文是独立的。4.3 同一总线上出现两个相同地址的设备怎么办如果两颗同型号芯片挂在同一条I2C总线上地址还都是0x48硬件设计时必须给芯片留出地址引脚。比如TMP117有ADD0引脚可以接地或接VDD形成两个不同地址。设备树里分别声明i2c3 { status okay; tmp117_a: tmp11748 { compatible ti,tmp117; reg 0x48; }; tmp117_b: tmp11749 { compatible ti,tmp117; reg 0x49; }; };驱动代码无需改动因为i2c_client的地址本身就封装在里面所有I2C传输都会带上从机地址。如果芯片没有地址引脚那就要考虑用I2C多路复用器比如PCA9548切换总线或者干脆把其中一颗挪到另外一条I2C控制器上。不要试图在驱动里强行固定地址那是跟硬件过不去迟早翻车。4.4 常见问题速查表现象常见原因解法probe完全不执行compatible不匹配 / 父节点disabled / 节点位置不对反编译dtb逐字对比 / 检查status / 确认挂在正确控制器下i2cdetect扫不到设备硬件无应答 / 供电、上拉问题 / SCL-SDA接反万用表测波形 / 用逻辑分析仪抓I2C时序 / 确认I2C总线号两个设备数据互相覆盖驱动用了全局缓冲区 / 全局状态变量所有状态移入私有结构体 / devm_kzalloc每实例分配多个设备读操作很慢共享锁 / 寄存器并发访问频繁锁放入私有数据结构 / 考虑per-device regmap卸载模块时崩溃devm接口和remove里手动free重复释放 / 中断未注销优先用devm_注册中断、内存 / remove只做必要清理模块不自动加载缺少MODULE_DEVICE_TABLE / 模块别名不对补上of/i2c的MODULE_DEVICE_TABLE / 检查modinfo输出5. 瑞芯微平台上的几个实操经验瑞芯微的SDK其实已经给了很好的示范尤其RK3568、RV1106这类芯片的Linux SDK里drivers/hwmon、drivers/iio、drivers/input/touchscreen等目录下有不少多实例驱动可以借鉴。我每次写新驱动前都会先在SDK里找一个结构最接近的驱动通读一遍看看它的probe是怎么分配私有数据的、of_match_table怎么写的再动手。还有三个小细节值得注意第一瑞芯微dts里的I2C节点有clock-frequency配置实测100kHz最稳400kHz在某些长走线板子上会出现丢数据。如果两个设备都挂在高速总线上跑压力测试时概率性读到错误数据先把频率降下来试试。第二留意pinctrl。RK3568的I2C引脚基本都是复用引脚设备树里pinctrl-names default指向的引脚组不能被其他外设抢占。两个外设复用同一组引脚内核不一定报错但I2C波形会乱。这种问题驱动层面排查不出来只能回到设备树看引脚分配。第三多实例设备命名要清晰。我给每个字符设备命名时会把总线号和地址拼进去比如tsensor-3-0048、tsensor-4-0048。用户空间分辨起来特别方便出问题也能快速定位是哪个设备报错。这个习惯花不了几分钟但在现场调试时能省大量时间。我在RK3568上调这两个同型号传感器时犯过最傻的错误就是把温度缓存数组定义成了全局变量结果两个设备互相覆盖上层读数永远一样。后来养成了一个习惯probe里第一件事就是创建私有数据结构任何状态都往里面放全局变量只保留const常量表。养成这个习惯之后多设备支持基本不会再出什么幺蛾子。这篇文章分享的两个小技巧一个是匹配表上的“一驱多型”一个是实例化上的“一驱多实例”内核框架其实都帮你搭好了关键是你得顺着它的车道开。
返回列表