拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Linux驱动多设备支持:设备树与私有数据结构的实践

写在前面搞驱动的人应该都有同感一个驱动从零到一能跑通不算难真正烦的是“一个驱动要同时管好几个长得一模一样的外设”。比如板子上同时接了A和B两片SPI屏幕或者同一路I2C总线上挂了两个相同型号的传感器。你在驱动里写死了一个probe结果第二个设备也来凑热闹两个设备抢同一份全局变量数据错乱、注册失败、中断互相踩……这种问题排查起来比新写一个驱动还痛苦。我在瑞芯微平台上做Linux驱动开发也有几年了rk3568、rk3588、rv1106都摸过今天不聊高深的框架也不讲复杂的总线协议就聊两个我在实际项目里经常用、能真正解决“一个驱动支持多个设备”的小技巧。第一个是从设备树下手让同一份驱动代码自然匹配多个硬件实例第二个是从驱动内部的数据结构下手让每个设备实例各自维护自己的“私有户口”。这两个技巧配合起来基本可以覆盖绝大多数多设备支持的需求而且代码结构清晰后面要扩展第三个、第四个设备也不用动大手术。无论你是刚开始接触Linux驱动开发的新手还是已经被多实例问题折磨过几轮的“老油条”这篇文章应该都能给你一些实在的参考。1. 内容整体设计与思路拆解1.1 为什么“支持多个设备”会成为驱动开发的老大难先说一个很常见的场景。你在瑞芯微的板子上写了一个GPIO按键驱动最初只接了一个按键probe函数里platform_get_resource拿中断号gpiod_get拿GPIO然后input_register_device注册一个输入设备。一切正常。后来硬件改版板子上多了一个按键你以为把设备树里多写一个节点就完事了结果一编译一跑驱动直接报request_irq失败或者两个按键完全不受控制。问题出在哪很多人写驱动的时候习惯性地把设备的资源信息直接以“全局变量”或者“函数内静态变量”的方式存在代码里。比如中断号用一个static int irq_numGPIO编号用一个static struct gpio_desc *key_gpio。第一个设备probe进来这些变量被填充成设备1的资源第二个设备probe进来又把全局变量覆盖成设备2的资源。结果就是设备1想读自己的GPIO读到的却是设备2的两个设备互相干扰整个驱动处于“精神分裂”状态。这里的关键认知是Linux驱动的设计哲学里一份驱动代码要服务多个硬件实例靠的不是在驱动里写死“设备A怎么怎么样、设备B怎么怎么样”而是让驱动代码“不认识具体设备”只负责把设备树传进来的资源绑定到当前这个probe实例对应的私有数据结构上。1.2 瑞芯微平台设备树与驱动的匹配机制瑞芯微平台基于设备树Device TreeDT来描述硬件这一点在rk3568、rk3588、rv1106这些芯片上都一样。设备树里每个节点代表一个硬件设备节点的compatible属性就是驱动和设备之间的“接头暗号”。驱动这边注册一个platform_driver里面有of_match_table里面写着这个驱动能匹配哪些compatible字符串。内核扫描设备树的时候一旦发现某个节点的compatible和驱动的of_match_table对得上就会调用驱动里的probe函数一次。注意这里的“一次”是针对每个节点都会调用一次而不是整个内核启动过程只调用一次。也就是说如果你的设备树里有两个节点都是同一个compatible那么这个驱动的probe函数会被调用两次每次传入的struct platform_device *pdev指向不同的设备节点。这正是多设备支持的基础。但仅仅知道“probe会被调用两次”还不够关键是怎么把两次probe的资源、设备号、数据区分开。如果驱动代码在两次probe之间用了同一个全局变量保存状态那么第二次probe一定会把第一次probe的状态冲掉。所以整个多设备支持的设计思路可以概括成一句话设备树负责“描述差异”驱动代码负责“隔离差异”。设备树里通过不同的reg、不同的interrupts、不同的gpios、甚至不同的status属性来区分两个设备驱动代码里则通过为每个probe实例申请独立的数据结构把资源、状态、私有数据全部装进这个结构体里做到“一个实例一个户口”。1.3 两个小技巧的分工与配合第一个技巧解决的是“硬件怎么描述”的问题。很多人写设备树的时候习惯把多个同类型设备塞进同一个节点里比如在节点下面用reg 0; reg 1;这种奇怪的写法结果驱动侧根本读取不到第二组资源。正确做法是为每个物理设备单独建一个节点各自拥有完整的资源描述。这样驱动可以顺着设备树自然地拿到每个设备自己的资源。第二个技巧解决的是“驱动怎么隔离”的问题。在驱动内部probe函数每次被调用时都新分配一块内存devm_kzalloc把所有和设备实例相关的变量全部装进这个结构体。后续操作读写寄存器、处理中断、注册文件操作接口都以这个结构体为参数而不是去访问什么全局数组。这样即使只写一份驱动代码也能同时支持N个设备实例。这两个技巧是配套使用的。设备树不写对驱动写得再好也拿不到正确的资源驱动不做数据隔离设备树写了多个节点也照样打架。2. 核心细节解析与实操要点2.1 设备树侧如何正确声明多个同类设备以瑞芯微rk3568为例假设你有两个SPI接口的LCD屏幕分别挂在SPI0和SPI1上。很多初学者会这么写spi0 { status okay; mylcd: mylcd0 { compatible mycompany,mylcd; reg 0; spi-max-frequency 20000000; reset-gpio gpio4 RK_PA0 GPIO_ACTIVE_LOW; }; }; spi1 { status okay; mylcd: mylcd0 { compatible mycompany,mylcd; reg 0; spi-max-frequency 20000000; reset-gpio gpio4 RK_PA1 GPIO_ACTIVE_LOW; }; };这段代码在语法上没问题实际使用中却有两个隐患第一个隐患是标签label重复。mylcd: mylcd0这种写法在设备树编译阶段就会报警甚至直接报错因为mylcd这个标签在同一个设备树里被定义了两次。正确做法是给每个节点起不同的标签比如mylcd0和mylcd1。第二个隐患是节点名和reg混用导致的混乱。虽然reg 0在这里是SPI设备的片选号但如果两个节点的节点名都叫mylcd0哪怕他们挂在不同的SPI控制器下面阅读和调试时也极容易混淆。建议根据设备在板子上的实际位置或者功能来命名比如lcd_back和lcd_front。更重要的是SPI/I2C这类总线下挂多个同型号设备很多新人会想“既然设备型号一样那我能不能只写一个节点然后在驱动里for循环注册两个设备”这里我明确建议不要这么干。设备树是描述“硬件长什么样”的语言不是一个设备就该是一个节点。只有在硬件上确实是一个芯片里集成了两个功能模块比如双通道ADC芯片一个节点下通过reg区分两个channel才适合在一个节点里描述多份资源。2.2 compatible、reg、interrupts这些属性在多设备场景下的分工在多设备场景下设备树属性的作用可以简单归纳为三类第一类是“身份属性”主要是compatible。这类属性决定了哪个驱动来管这个设备多个同类设备的compatible可以一模一样驱动侧也是靠它来统一匹配的。需要注意的是设备树节点的compatible最好不要带包版本号、批次号这类信息比如compatible mycompany,mylcd-v2因为这会迫使驱动为每个版本写一套匹配表维护起来很痛苦。建议只写型号比如mycompany,mylcd驱动内部再通过of_device_get_match_data去读取具体的版本信息。第二类是“资源属性”包括reg、interrupts、gpios、clocks、resets等。这类属性描述了这个设备用到的硬件资源驱动在probe时通过platform_get_resource、gpiod_get、devm_clk_get等接口将它们解析出来。在多设备场景下这些属性的值要确保每个设备各不相同否则两个设备就会抢同一个中断号或寄存器地址。第三类是“策略属性”比如spi-max-frequency、status、自定义属性等。这类属性不直接影响驱动能不能跑但会影响驱动的行为参数。比如两个LCD一个支持20MHz一个只能跑10MHz就可以在设备树里分别指定驱动只要用device_property_read_u32读取就行不用为这种差异修改代码。这里我特别想强调一点多设备场景下中断号千万不要在驱动里写死也尽量不要在设备树里写死“同一根中断线”。瑞芯微的GPIO中断资源在很多情况下是通过interrupts-extended或者直接在节点里声明interrupt-parent来指定的如果两个设备不小心指向了同一个GPIO中断probe阶段request_irq只有一个能成功另一个会报irq busy。2.3 驱动probe函数中逐个实例解析资源的标准写法当设备树里有了合法的多个节点驱动侧probe函数就需要严格按照“当前实例”的方式来解析资源。这里给出我在瑞芯微平台上常用的标准写法框架static int mylcd_probe(struct platform_device *pdev) { struct device *dev pdev-dev; struct mylcd_priv *priv; struct resource *res; int ret; // 1. 为当前实例分配私有数据空间 priv devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL); if (!priv) return -ENOMEM; priv-dev dev; platform_set_drvdata(pdev, priv); // 2. 解析寄存器资源注意是platform_get_resource不是直接读固定地址 res platform_get_resource(pdev, IORESOURCE_MEM, 0); if (!res) return -ENXIO; priv-regs devm_ioremap_resource(dev, res); if (IS_ERR(priv-regs)) return PTR_ERR(priv-regs); // 3. 解析中断资源rk3568新内核建议用platform_get_irq ret platform_get_irq(pdev, 0); if (ret 0) return ret; priv-irq ret; // 4. 请求gpio描述符 priv-reset_gpio devm_gpiod_get(dev, reset, GPIOD_OUT_LOW); if (IS_ERR(priv-reset_gpio)) return PTR_ERR(priv-reset_gpio); // 5. 注册中断处理注意把priv作为data传进去 ret devm_request_irq(dev, priv-irq, mylcd_irq_handler, IRQF_TRIGGER_RISING, dev_name(dev), priv); if (ret) return ret; // 6. 注册字符设备/misc设备/帧缓冲设备等 // ... dev_info(dev, mylcd probed, irq%d\n, priv-irq); return 0; }核心点就两个一是所有资源都从这个pdev里拿不去引用什么extern全局变量二是所有解析出来的资源都放进priv结构体后续回调函数直接拿到priv就能工作。这样无论probe执行几次每次的priv都是独立的互不干扰。2.4 为什么选择devm_系列接口而不是手动释放这段代码里我大量用了devm_前缀的接口比如devm_kzalloc、devm_ioremap_resource、devm_gpiod_get、devm_request_irq。这些接口和传统版kzalloc、ioremap、gpiod_get、request_irq最大的区别是资源生命周期和设备绑定驱动和设备分离时自动释放。在多设备场景下这个优势特别明显。假设你的驱动里注册了设备1和设备2设备2在probe阶段失败了这时候如果没有devm_机制你得手动把设备1已经申请的资源逐个释放稍不留神就会漏一个然后系统内存泄漏或中断线被占用。用了devm_之后只要设备2的probe返回一个错误码内核会自动把和设备2相关的所有资源全部释放设备1完全不受影响。另外devm_接口还有一个隐藏的好处内核会自动确保释放顺序和申请顺序相反。在手动释放时如果反了顺序有些资源解引用的时机不对就会触发内核告警。用devm_之后就完全不用操这个心算是“花小钱办大事”的典型。3. 实操过程与核心环节实现3.1 场景设定为瑞芯微rk3568编写一个双串口设备驱动光讲理论容易飘我拿一个完整的案例走一遍。假设项目里用到两颗USB转串口芯片风格的设备——为了贴合瑞芯微平台这里直接把设备定义为“板载双UART控制器”设备树里挂两个节点驱动要能同时支持两路串口数据收发且每路串口的波特率、数据位可独立配置。由于是内部控制器不是真正的USB设备我们用platform_driver框架来写。首先在设备树中这样声明// 设备节点1UART0 uart_ctrl0: uart-ctrl0 { compatible mycompany,dual-uart-ctrl; reg 0x0 0x1000; interrupts 0 23 IRQ_TYPE_LEVEL_HIGH; interrupt-parent gic; current-speed 115200; }; // 设备节点2UART1 uart_ctrl1: uart-ctrl1 { compatible mycompany,dual-uart-ctrl; reg 0x1 0x1000; interrupts 0 24 IRQ_TYPE_LEVEL_HIGH; interrupt-parent gic; current-speed 9600; };注意这里的reg我刻意设成了0 0x1000和0x1 0x1000表示两个实例访问的寄存器区域不同。实际项目中这两组寄存器可能位于芯片内部不同的地址也可能用同一个基址加偏移量来区分。如果你只有一个寄存器基址两个设备分别使用不同偏移那么可以只在设备树中写共用的基址然后在驱动里根据pdev-id或自定义属性区分偏移。另外两个节点的current-speed分别为115200和9600这就是“策略属性”的典型用法。驱动不做任何硬编码直接读取即可。3.2 驱动代码为每个串口实例分配独立私有数据驱动主体代码我写成下面这样关键点都加了注释#include linux/module.h #include linux/platform_device.h #include linux/of.h #include linux/interrupt.h #include linux/serial_core.h #include linux/tty_flip.h #define MAX_UART_INSTANCE 8 struct dual_uart_priv { struct device *dev; void __iomem *regs; int irq; int uart_id; u32 current_speed; struct uart_port port; spinlock_t lock; }; static int dual_uart_probe(struct platform_device *pdev) { struct device *dev pdev-dev; struct dual_uart_priv *priv; struct resource *res; int ret; // 分配实例私有数据结构 priv devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL); if (!priv) return -ENOMEM; priv-dev dev; spin_lock_init(priv-lock); platform_set_drvdata(pdev, priv); // 读取寄存器资源 res platform_get_resource(pdev, IORESOURCE_MEM, 0); if (!res) return -ENXIO; priv-regs devm_ioremap_resource(dev, res); if (IS_ERR(priv-regs)) return PTR_ERR(priv-regs); // 读取中断号 ret platform_get_irq(pdev, 0); if (ret 0) return ret; priv-irq ret; // 读取自定义属性 current-speed ret device_property_read_u32(dev, current-speed, priv-current_speed); if (ret) priv-current_speed 115200; // 默认值 // 注册uart_port priv-port.dev dev; priv-port.private_data priv; priv-port.irq priv-irq; priv-port.type PORT_16550; priv-port.iotype UPIO_MEM; priv-port.mapbase res-start; priv-port.membase priv-regs; priv-port.fifosize 16; priv-port.ops dual_uart_ops; priv-port.line pdev-id; // 关键用平台设备id作为串口号区分 spin_lock_init(priv-port.lock); ret uart_add_one_port(dual_uart_driver, priv-port); if (ret) return ret; dev_info(dev, dual uart probed: line%d, irq%d, speed%u\n, priv-port.line, priv-irq, priv-current_speed); return 0; }关键点在于priv这个结构体。每个设备实例的probe都会重新调用一次devm_kzalloc所以设备1的priv和设备2的priv在物理上是两块不同内存字段完全隔离。后续中断处理、读写函数、uart操作函数里拿到uart_port后通过container_of或者port-private_data就能反推出对应的priv操作自然只影响当前设备。3.3 中断处理函数如何区分多个设备实例中断处理函数是多设备驱动里最容易翻车的地方。很多新手喜欢把中断处理函数写成“全局函数”然后在里面访问一个全局的g_priv指针。这在单设备时代没问题多设备时一旦设备1产生中断中断处理函数访问的却是设备2的寄存器数据错乱得莫名其妙。正确做法是在申请中断时把当前实例的priv通过devm_request_irq的最后一个参数传进去中断触发时内核会把它原样作为dev_id传回来static irqreturn_t dual_uart_irq_handler(int irq, void *dev_id) { struct dual_uart_priv *priv dev_id; unsigned long flags; u32 status; if (!priv) return IRQ_NONE; spin_lock_irqsave(priv-lock, flags); status readl(priv-regs UART_IRQ_STATUS); if (status UART_IRQ_RX_FIFO) { // 处理接收FIFO把数据交到tty层 // ... } if (status UART_IRQ_TX_EMPTY) { // 处理发送 // ... } writel(status, priv-regs UART_IRQ_STATUS); spin_unlock_irqrestore(priv-lock, flags); return IRQ_HANDLED; }这里的dev_id就是probe时传入的priv每个设备实例都有自己的中断处理入口。只要设备树里两个设备的中断号不同它们就绝不互相干扰。更保险的做法是在中断函数里再读一次priv-dev的状态来二次确认中断确实来自于本设备有些硬件存在中断共享的情况这一步对可靠性要求较高的场景建议加上。3.4 uart_ops回调中如何拿到当前设备实例在uart_ops结构体的回调函数里内核传入的是struct uart_port *port。port是我们priv结构体里的内嵌成员所以可以通过container_of拿到privstatic void dual_uart_set_termios(struct uart_port *port, struct ktermios *termios, const struct ktermios *old) { struct dual_uart_priv *priv container_of(port, struct dual_uart_priv, port); u32 lcr 0; unsigned long flags; spin_lock_irqsave(priv-lock, flags); if (termios-c_cflag CS8) lcr | UART_LCR_WLEN8; // ... 其他参数解析 writel(lcr, priv-regs UART_LCR); spin_unlock_irqrestore(priv-lock, flags); }这里有一个细节很多人会踩坑container_of依赖的是port在priv中的位置。如果你把struct uart_port port移到了结构体末尾或者其他位置container_of依然能正确计算偏移但前提是你在初始化时确实把priv-port的地址给了uart核心而不是另外创建了一个独立的uart_port。所以我在probe里专门加了一句priv-port.private_data priv这样回调里即使不用container_of也能通过port-private_data直接拿到priv。两种方式各有优劣container_of类型安全更明确private_data写起来更直接。我的习惯是两者结合初始化时用container_of强校验一次后续在回调里直接用private_data减少出错概率。3.5 从设备树到驱动的属性传递与读取技巧瑞芯微的设备树解析有几个实用的小技巧值得提一下。第一个是device_property_read_*系列函数的优势它同时兼容设备树DT和ACPI虽然我们在嵌入式平台上基本只用DT但用这套API以后代码可移植性更好而且读取字符串、u8、u32、u64、bool等都有一一对应的接口。第二个技巧是不要害怕在设备树里加自定义属性。很多人总想着“驱动里写死就行”但设备树的存在就是为了让同一个内核镜像跑在不同硬件设计上。像波特率这种参数写在设备树里硬件改版时只需改dts文件重新编译dtb驱动一行都不用动。这也是“设备树负责描述差异”思想的本质。第三个技巧是善用of_property_count_elems_of_size之类的辅助函数来检查属性是否存在。比如读取一组GPIO列表时int count device_property_count_u32(dev, led-gpios); if (count 0) { // 逐个读取批量注册 for (i 0; i count; i) { struct gpio_desc *desc gpiod_get_index(dev, led, i, GPIOD_OUT_LOW); // ... } }3.6 编译与验证过程实录驱动代码写完后需要把它编进内核模块。瑞芯微平台通常用SDK里的交叉编译工具链比如aarch64-linux-gnu-。我习惯先编成外部模块M方式验证通过后再考虑编进内核镜像。命令大概是export ARCHarm64 export CROSS_COMPILEaarch64-linux-gnu- make -j8 M/path/to/dual_uart modules编出dual_uart.ko后用adb或者scp推到板子上insmod加载。加载顺序有讲究如果驱动编译成了模块需要先确保设备树里对应的节点status okay否则驱动probe不会触发。加载成功后dmesg里应该能看到类似这样的输出dual-uart-ctrl 1000.uart-ctrl0: dual uart probed: line0, irq23, speed115200 dual-uart-ctrl 1001.uart-ctrl1: dual uart probed: line1, irq24, speed9600这行日志说明两次probe都成功了两个设备分别注册到了ttyS0和ttyS1具体名称取决于驱动里注册的uart driver。接下来可以用stty -F /dev/ttyS0 115200、echo hello /dev/ttyS1简单测试或者直接连一个USB转串口工具对接外部设备做回环测试。4. 常见问题与排查技巧实录4.1 两个设备只有一个能probe成功这是多设备支持最典型的问题。先看dmesg里失败的设备有没有报-EBUSY或者-ENXIO。-EBUSY多半是资源冲突重点是中断号或寄存器地址。检查设备树里两个节点的reg是否合理中断号是否被其他驱动占用。可以用cat /proc/interrupts查看当前系统里哪些中断被占了。不过更常见的是寄存器地址冲突——有次我在rk3568平台上把两个节点的reg都写成了0x0 0x1000结果第二个设备devm_ioremap_resource直接失败因为同一段地址已经被第一个设备映射了。这类问题用cat /proc/iomem能看得很清楚。-ENXIO多半是资源不存在。检查platform_get_resource的逻辑是否正确比如你想拿的是IORESOURCE_MEM但设备树里写的是interrupts属性那只有platform_get_irq能拿到用platform_get_resource拿必然失败。4.2 probe顺序不确定如何保证多个设备初始化有序在多设备情况下probe的顺序通常由设备树节点的扫描顺序和驱动注册顺序决定但这个顺序并不总能严格保证。如果你的设备之间有依赖关系比如设备2依赖设备1提供的某个功能那么不要指望probe顺序而是应该在驱动代码里做“延时探测”或者“依赖检查”。最简单的方案是在设备2的probe里主动去找设备1的privstruct dual_uart_priv *find_priv_by_line(int line) { struct device *dev bus_find_device_by_name(platform_bus_type, NULL, uart-ctrl0); if (!dev) return NULL; return dev_get_drvdata(dev); }如果返回NULL说明设备1还没probe设备2可以返回-EPROBE_DEFER让内核在稍后再次尝试probe。-EPROBE_DEFER这个机制在复杂驱动栈里非常有用很多新人不知道导致在驱动里忙等或直接失败其实内核早就为这种场景准备了标准解法。4.3 全局变量、全局数组还能不能用了这个问题几乎每个驱动开发者都纠结过。我的建议是驱动维护一个全局的“实例链表”或者“实例数组”是可以接受的但绝对不能用全局变量保存某个实例的资源状态。实例数组的作用只是方便其他子系统去查找某个设备实例而每个实例的数据仍然保存在自己的priv结构体里。比如static struct dual_uart_priv *g_instances[MAX_UART_INSTANCE]; static DEFINE_MUTEX(g_instances_lock); static int dual_uart_probe(struct platform_device *pdev) { // ... mutex_lock(g_instances_lock); for (i 0; i MAX_UART_INSTANCE; i) { if (!g_instances[i]) { g_instances[i] priv; break; } } mutex_unlock(g_instances_lock); // ... } static int dual_uart_remove(struct platform_device *pdev) { struct dual_uart_priv *priv platform_get_drvdata(pdev); mutex_lock(g_instances_lock); for (i 0; i MAX_UART_INSTANCE; i) { if (g_instances[i] priv) g_instances[i] NULL; } mutex_unlock(g_instances_lock); // ... }这个数组只用来做实例索引不存放任何硬件资源、状态变量所以它是安全的。我实际项目中这样的全局数组通常用来做按序号访问、统计设备总数、debugfs调试信息收集等。4.4 调试多设备驱动时好用的三板斧第一个是打开内核的动态调试。瑞芯微的内核一般默认开启了CONFIG_DYNAMIC_DEBUG可以在运行时动态打开不需要重新编译的内核日志echo file drivers/tty/serial/dual_uart.c p /sys/kernel/debug/dynamic_debug/control比printk方便的地方在于它可以精确控制只打开某个文件的调试信息而不会让整个内核日志刷屏。第二个是使用dev_dbg而不是pr_debug。dev_dbg在打印时会自动带上设备名比如uart-ctrl0多设备环境下一眼就能看出日志是哪个设备打的。第三个是善用/sys/kernel/debug/下的信息。对于uart这种设备/proc/tty/driver/目录下通常有驱动自报的设备统计对于其他设备类别/sys/bus/platform/devices/下列出了所有platform设备节点可以逐一确认设备树解析是否正确。4.5 常见问题速查表现象可能原因排查思路第二个设备probe返回-ENOMEM内存分配失败多数因为devm_kzalloc分配过大查看dmesg确认失败位置检查结构体是否有超大数组两个设备中断都申请成功但功能错乱中断处理函数使用了全局资源或没有正确使用dev_id检查中断回调是否拿priv作为dev_id而不是从全局变量访问设备树节点编译报“duplicate label”多个节点使用了相同的label给每个节点起不重复的label如mylcd0、mylcd1读取属性全是默认值自定义属性名拼写错误或设备树未重新编译用ls /proc/device-tree/节点名/确认属性是否存在设备1的修改影响到了设备2驱动里用了static变量保存状态审查代码把所有实例相关变量迁移到priv结构体5. 从单设备到多设备的经验复盘回到开头的问题一份驱动代码到底怎么才能优雅地支持多个设备我做了这么多年驱动最大的体会是多设备支持从来不是写代码的时候才考虑的而是在设计驱动架构的时候就应该预留的扩展位。具体来说有三点经验值得拿出来分享。第一点从一开始就用priv结构体包装所有实例数据。哪怕你当前项目里只有一个设备也建议把资源、状态、锁全部放进一个独立的structure里probe里统一初始化。这样以后要加第二个设备你只需要在设备树里加一个节点驱动侧几乎不用改。如果一开始就把资源写到全局变量里后面改造成本会翻倍。第二点设备树就是你的“配置文档”。多设备之间那些微小的差异——不同的中断号、不同的GPIO、不同的频率、不同的时序参数尽量用设备树属性来表达。驱动代码只管“读取属性并应用”而不是“根据设备名做if-else”。时间久了你会发现当硬件厂商送来新的板卡只要你更新dtb驱动不重编也能正常工作这种维护体验是写死代码完全没法比的。第三点测试多设备一定要测“热拔插”和“部分失败”。虽然瑞芯微这类嵌入式平台大多不支持热拔插但在驱动开发过程中你完全可以通过手动rmmod再insmod的方式来模拟设备离开和重新加入。用devm_接口的驱动在remove时应该能做到干净利落地释放资源不留任何“尾巴”。如果发现拔掉一个设备会连累另一个设备报错那你的实例隔离一定是没做透。另外调试多设备驱动时我习惯在每个probe函数的入口加一行dev_info(dev, enter probe\n)在出口加一行dev_info(dev, probe done\n)。日志虽土但能快速定位到是哪一次probe卡住、哪一次失败。真正干活的时候这种土办法往往比高大上的调试器更有效率。最后再补一个小技巧。如果你在驱动里用了devm_request_irq并且中断处理函数里需要访问priv一定要在申请irq之前把priv完整初始化好。因为中断随时可能触发虽然理论上probe阶段设备不会立刻产生中断但硬件BUG和电气杂波这种事谁都说不准。我遇到过几次“probe阶段就触发中断然后中断处理函数访问未初始化的寄存器导致内核panic”的情况都是在给priv字段做初始化之前就申请了中断。把顺序调整成“先初始化结构体再申请中断”这个坑就再也不会踩到了。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门