WK2XXX SPI 驱动:动态申请设备号解决双实例加载失败
在嵌入式Linux驱动开发中,基于WK2XXX系列SPI转串口芯片的驱动开发是很常见的需求,不少开发者会遇到两个WK2XXX SPI驱动无法同时加载的问题,排查日志后发现无复杂的资源争抢,核心症结其实就藏在设备号的硬编码里。本文就从问题现象出发,剖析根本原因,并用动态申请设备号这一简单方法彻底解决,同时附上实操步骤和排查修复流程图,新手也能快速上手。
一、问题背景:双驱动加载失败,日志无复杂报错
在基于WK2XXX系列芯片的项目开发中,需要同时加载wk2xxx_spi.c和wk2xxx_spi2.c两个SPI转串口驱动,实现多串口扩展。但实际操作中,第一个驱动能正常加载,加载第二个驱动时直接失败,查看内核日志dmesg仅出现以下关键信息,无其他资源冲突报错:
[ 3.752046]SPI driver wk2xxxspi0 has no spi_device_id for wkmic,wk2xxx_spi[ 3.752311]spi0.0: ttysWK0 at I/O0x1 (irq =0, base_baud =691200) is a wk2xxx[ 3.752669] wk02xxx_init: SPI driver for spi to Uart chip WK02XXX, etc.[ 3.752673] SPI driver wk2xxxspi02 has no spi_device_id for wkmic,wk2xxx_spi02
反复排查SPI控制器、片选、中断等资源,均未发现冲突,最终定位到设备号的硬编码是导致该问题的唯一原因。
二、根本原因:Linux主设备号全局唯一,硬编码导致冲突
要理解这个问题,首先要掌握Linux字符设备号的核心机制:
Linux的字符设备号由主设备号和次设备号组成,主设备号是全局唯一的,用于标识驱动类型,内核通过主设备号匹配对应的驱动程序;次设备号用于区分同一驱动类型下的不同设备实例。
在WK2XXX SPI驱动的原始代码中,开发人员对主设备号做了硬编码定义,且wk2xxx_spi.c和wk2xxx_spi2.c两个驱动使用了完全相同的主设备号宏定义:
// 两个驱动均使用该硬编码,无任何区分#defineSERIAL_WK2XXX_MAJOR 207 // 串口主设备号#defineCALLOUT_WK2XXX_MAJOR208// 呼叫主设备号
当第一个驱动加载时,会向内核申请并占用207、208这两个主设备号;第二个驱动加载时,再次向内核申请相同的主设备号,内核检测到该主设备号已被占用,会直接拒绝分配,导致驱动加载失败。
这也是本次问题的核心:无其他资源冲突,仅因主设备号硬编码重复,导致双驱动无法同时加载,因此只需修改设备号的分配方式,即可解决问题。
三、核心解决方法:弃用硬编码,动态申请主设备号
Linux内核推荐动态申请主设备号,而非硬编码指定,这也是解决本次问题的唯一方法。
动态申请设备号的优势
1.内核会自动分配未被占用的主设备号,从根本上避免全局主设备号冲突;
2.无需关注内核预留的主设备号范围,适配性更强,跨内核版本无需修改设备号;
3.多驱动、多实例场景下,无需手动区分设备号,开发更高效。
关键API说明
Linux内核提供了专门的字符设备号动态申请和释放API,本次修改仅需用到两个核心函数,无需引入其他头文件,原驱动代码已包含相关依赖:
1.动态申请:alloc_chrdev_region(&dev, minor_start, count, name)
○入参:dev(保存分配到的设备号)、minor_start(次设备号起始值)、count(申请的设备号数量)、name(驱动名称,用于标识);
○出参:成功返回0,失败返回负的错误码。
2.释放设备号:unregister_chrdev_region(dev, count)
○入参:dev(已分配的设备号)、count(申请的设备号数量);
○无返回值,用于驱动卸载时释放已申请的设备号,避免内存泄漏。
四、实操步骤:修改两个驱动代码,实现动态申请
本次修改基于原始的wk2xxx_spi.c和wk2xxx_spi2.c,两个驱动的修改逻辑完全一致,仅需保证驱动名称标识唯一即可,步骤分4步,新手也能快速完成。
步骤1:新增全局变量,保存动态分配的主设备号
在两个驱动的宏定义区下方,新增全局变量用于保存动态分配的主设备号,替代原硬编码的宏:
// 新增:保存动态分配的主设备号staticintser_major =0;// 原硬编码宏保留(无需删除,后续赋值为0即可)#defineSERIAL_WK2XXX_MAJOR 207#defineCALLOUT_WK2XXX_MAJOR208#defineMINOR_START 5#defineNR_PORTS 4
步骤2:修改uart_driver结构体,主设备号赋值为0
原驱动中定义了uart_driver结构体,硬编码指定了major字段,需将其修改为0,告诉内核采用动态分配方式:
// wk2xxx_spi.c的uart_driver修改staticstructuart_driverwk2xxx_uart_driver = { owner: THIS_MODULE, major: 0, // 关键修改:0表示动态分配主设备号 driver_name: "ttySWK", dev_name: "ttysWK", minor: MINOR_START, nr: NR_PORTS, cons: NULL};// wk2xxx_spi2.c的uart_driver修改,仅需保证driver_name/dev_name唯一即可staticstructuart_driverwk02xxx_uart_driver = { owner: THIS_MODULE, major: 0, // 关键修改:0表示动态分配主设备号 driver_name: "ttySWK02", dev_name: "ttysWK02", minor: MINOR_START, nr: NR_PORTS, cons: NULL};
步骤3:修改驱动初始化函数,添加动态申请设备号逻辑
找到驱动的__init初始化函数(wk2xxx_init/wk02xxx_init),在注册SPI驱动前添加动态申请设备号的代码,原硬编码相关逻辑无需删除,仅需新增:
// wk2xxx_spi.c的初始化函数修改staticint__init wk2xxx_init(void){ intret; dev_t dev;//新增:定义设备号变量 // 新增:动态申请主设备号,次设备号起始为MINOR_START,共NR_PORTS个 ret = alloc_chrdev_region(&dev, MINOR_START, NR_PORTS,"wk2xxxspi0"); if(ret < 0) { printk(KERN_ALERT "%s: 动态申请设备号失败, ret= :%dn",__func__,ret); return ret; } // 保存动态分配的主设备号 ser_major = MAJOR(dev); printk(KERN_ALERT "%s: 动态分配主设备号为:%dn",__func__,ser_major); // 原代码:注册SPI驱动 printk(KERN_ALERT"%s: " DRIVER_DESC "n",__func__);printk(KERN_ALERT "%s: " VERSION_DESC "n",__func__); ret = spi_register_driver(&wk2xxx_driver); if(ret<0){ printk(KERN_ALERT "%s,failed to init wk2xxx spi;ret= :%dn",__func__,ret); // 申请失败后释放设备号,避免泄漏 unregister_chrdev_region(dev, NR_PORTS); } return ret;}// wk2xxx_spi2.c的初始化函数修改,仅需修改驱动名称为wk2xxxspi02static int __init wk02xxx_init(void){ int ret; dev_t dev; // 新增:动态申请设备号,驱动名称唯一 ret = alloc_chrdev_region(&dev, MINOR_START, NR_PORTS, "wk2xxxspi02"); if (ret < 0) { printk(KERN_ALERT "%s: 动态申请设备号失败, ret= :%dn",__func__,ret); return ret; } ser_major = MAJOR(dev); printk(KERN_ALERT "%s: 动态分配主设备号为:%dn",__func__,ser_major); // 原代码 printk(KERN_ALERT"%s: " DRIVER_DESC "n",__func__);printk(KERN_ALERT "%s: " VERSION_DESC "n",__func__); ret = spi_register_driver(&wk02xxx_driver); if(ret<0){ printk(KERN_ALERT "%s,failed to init wk2xxx spi;ret= :%dn",__func__,ret); unregister_chrdev_region(dev, NR_PORTS); } return ret;}
步骤4:修改驱动退出函数,添加释放设备号逻辑
找到驱动的__exit退出函数(wk2xxx_exit/wk02xxx_exit),在注销SPI驱动后添加释放设备号的代码,保证资源正常回收:
// wk2xxx_spi.c的退出函数修改staticvoid__exitwk2xxx_exit(void){ printk(KERN_ALERT"%s!!--in--n", __func__); // 原代码:注销SPI驱动 spi_unregister_driver(&wk2xxx_driver); // 新增:释放动态分配的设备号 if(ser_major >0) { unregister_chrdev_region(MKDEV(ser_major,MINOR_START),NR_PORTS); printk(KERN_ALERT"%s: 释放主设备号:%dn",__func__,ser_major); }}// wk2xxx_spi2.c的退出函数修改staticvoid__exitwk02xxx_exit(void){ printk(KERN_ALERT"%s!!--in--n", __func__); // 原代码 spi_unregister_driver(&wk02xxx_driver); // 新增:释放设备号 if(ser_major >0) { unregister_chrdev_region(MKDEV(ser_major,MINOR_START),NR_PORTS); printk(KERN_ALERT"%s: 释放主设备号:%dn",__func__,ser_major); }}
关键注意点
两个驱动的修改仅需保证动态申请设备号的驱动名称(alloc_chrdev_region的第四个参数)和uart_driver的driver_name/dev_name唯一即可,其余逻辑完全一致,无需做额外修改。
五、WK2XXX双驱动加载失败排查&修复流程图
为了方便大家在实际开发中快速排查同类问题,整理了专属流程图,从问题发现到功能验证,一步到位,可直接收藏备用:

六、验证:三步确认驱动加载正常
修改代码并编译出.ko驱动模块后,依次加载两个驱动,通过以下三步验证是否修复成功,操作简单且高效。
步骤1:查看动态分配的主设备号
执行命令cat /proc/devices,查看字符设备列表,能看到两个驱动被分配了不同的主设备号,说明设备号申请成功:
# 示例输出,实际主设备号由内核分配Characterdevices: ...245ttySWK # wk2xxx_spi.c的驱动246ttySWK02 # wk2xxx_spi2.c的驱动 ...
步骤2:查看/dev下的设备节点
执行命令ls /dev/ttysWK*,能看到两个驱动对应的设备节点均成功创建,无重复:
/dev/ttysWK0 /dev/ttysWK1 /dev/ttysWK2 /dev/ttysWK3 /dev/ttysWK020 /dev/ttysWK021 /dev/ttysWK022 /dev/ttysWK023
步骤3:串口功能测试
通过minicom/screen等工具分别操作两个驱动的串口节点,向串口发送/接收数据,验证通信正常,说明双驱动已实现同时加载且功能正常。
七、拓展知识点:Linux主设备号的那些事
1.主设备号范围:Linux内核中主设备号的取值范围是1255,其中1127是内核预留的主设备号,128~255是用户自定义的主设备号,硬编码时建议使用128以后的数值,但仍有冲突风险;
2.硬编码的弊端:除了本次的多驱动冲突,硬编码主设备号还会导致跨内核版本适配失败(部分内核版本会调整预留主设备号),以及与其他第三方驱动冲突;
3.动态申请的适配性:动态申请主设备号时,内核会从未被占用的数值中自动分配,无需关注内核版本和其他驱动,是嵌入式Linux驱动开发的最佳实践。
八、总结
本次WK2XXX SPI双驱动无法同时加载的问题,是嵌入式Linux驱动开发中典型的全局资源硬编码冲突问题,核心仅因主设备号硬编码重复,无需排查复杂的SPI、中断、IO资源,仅通过动态申请主设备号这一招即可彻底解决。
同时也给我们一个开发提醒:在嵌入式Linux驱动开发中,尤其是多驱动、多实例的场景下,应尽量避免硬编码全局唯一的资源(如主设备号、IO地址、中断号),遵循Linux内核的动态分配原则,既能避免资源冲突,又能提升驱动的适配性和可移植性。
本次的修改方法不仅适用于WK2XXX SPI驱动,也适用于其他Linux字符设备驱动的多实例加载场景,可直接复用!
审核编辑 黄宇
继续浏览有关 dev 的文章
