先说个最近的实际场景。我在基于瑞芯微RK3568的板子上调一个双路CAN驱动,板子上两路CAN控制器用的是同一个IP,设备树里也确实是两个独立节点,看起来工程量不大。可真动手写驱动的时候才发现,一个Linux驱动要同时管好两个硬件实例,坑比想象中多。最初图省事,在驱动里用了一个全局变量保存寄存器基地址,结果第二路设备probe进来之后,第一路的地址被覆盖,中断回调里读到的寄存器全是第二路的,现场用惨不忍睹来形容一点不过分。
这篇文章就是围绕这个主题展开的。Linux驱动支持多设备,说到底是两件事:第一,驱动怎么“认识”多个硬件节点;第二,驱动怎么把每个硬件节点各自的运行状态分开记,不互相串扰。文章会给出两个亲测好用的技巧,都基于瑞芯微平台的真实开发环境,适合正在做嵌入式Linux驱动、或者准备在RK系列芯片上做二次开发的工程师参考。文章里涉及的代码、排查思路,我尽量按实际调试时的真实过程来写。
1. 先摸清底细:驱动与设备原本就是“一对多”,不是“一对一”
1.1 多设备场景在瑞芯微平台上有多常见
有人可能会觉得,一个驱动管一个设备不是挺正常的吗?为什么非要追求“一驱多设备”?如果你只看过那种教学例程,确实容易产生这种错觉。但瑞芯微平台的实际情况是,一颗芯片上集成了大量同类型的外设控制器,而且同一个IP会被反复例化多次。
以RK3568为例,CAN控制器有俩,UART可能有八九路,I2C、SPI的控制器数量也不少。你在设备树里看到的节点往往是这样的:can0节点、can1节点,或者uart0到uart7各一个节点。这些节点的compatible字符串经常是同一个,背后对应同一个平台驱动,但每个节点拥有独立的寄存器地址、独立的中断号、独立的时钟和引脚配置。
换句话说,一个驱动被probe多次,是Linux驱动模型下的默认行为,不是需要什么特殊魔法才能做到的事情。问题只在于,很多人在写驱动的时候,并没有意识到这一点,用的是“一个驱动、一份全局数据”的老思路,于是多设备一接入就乱了套。
1.2 全局变量管理多设备状态,为什么必挂
前面我说的全局变量覆盖问题,不是偶发,是必然会遇到的问题。驱动被probe几次,不代表顺序是串行安全的。设备模型在枚举的时候可能连续触发两次probe,第二次probe执行到priv_base = ioremap(...)时,第一次probe里辛辛苦苦保存的地址就被覆盖掉了。
更麻烦的是中断上下文。中断是异步发生的,你没法保证它触发的时候,使用的正是对应设备的全局变量。一旦两个设备共用一个中断处理函数,而处理函数里读的是同一个全局变量,那中断来了之后,你到底该操作哪个设备?完全取决于这个变量最后被哪次probe写过,不确定性极大。
这还只是地址覆盖的问题。如果全局变量是一个数组,比如struct mydev *devs[2],你确实可以通过索引区分设备了,但接下来要面对的是并发保护、数组越界、设备热插拔带来的空指针等一系列麻烦。我在实际项目中见过不少因为这