ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Linux SDIO驱动开发实战:probe枚举与CMD53传输

2026/10/7 18:01:26 拓冰建站 浏览量
Linux SDIO驱动开发实战:probe枚举与CMD53传输 简介一份围绕SDIO驱动程序的完整学习与开发资料包面向嵌入式开发者重点覆盖SDIO协议基础、Linux内核驱动架构、设备探测/初始化/数据传输/中断处理/移除流程以及基于MMC框架的注册方式。内容包含Simplified SDIO Card Specification、SD Memory Card Spec等PDF文档以及项目总结文档、原理图/PCB备份、Keil工程源码SDIO.c/SDIO.h等、编译输出、链接映射等文件可辅助从硬件理解到代码实现的全链路学习。资源共有23个文件大小1.3MB类型涵盖PDF规范、DOC总结、C/H源码、工程配置与备份等麻雀虽小五脏俱全。已有658人学习浏览适合正在开发或调试SDIO外设如Wi-Fi/蓝牙模块的嵌入式工程师作为参考既能快速查阅协议规范也能对照真实工程梳理驱动初始化与中断处理流程。1. SDIO 设备驱动上手先把总线、卡和控制器三个角色摆对上周帮一个团队看 WiFi 模组模块焊到主控板上驱动 insmod 进去内核日志里一点动静都没有probe 根本没进。拉出原理图一看SDIO 四根数据线、一根时钟、一根命令物理层和 SD 卡完全一致最大工作频率标 50MHz。问题就出在这很多人把 SDIO 驱动当成某种串口协议来写实际上 SDIO 是把 SD 接口扩展成通用 I/O 通道的规范卡槽里插的不再是闪存而是 WiFi 芯片、蓝牙芯片、GPS 模组、指纹读头这些外设。SDIO 驱动开发的核心矛盾是“用 SD 通道跑非存储外设”。你要同时跟三套角色打交道MMC/SD 控制器负责物理时序SDIO 卡是协议层的外设宿主sdio_bus 负责把卡上各功能函数匹配到具体驱动。这篇笔记按我实际拆过的 Linux 侧驱动来写先搭注册、枚举、probe 的骨架再走通收发路径和中断设计最后把工单里反复出现的注意现场和一套能上板的验证手法放出来。适合刚接手 SDIO 接口无线模组、想快速定位驱动问题的开发者也适合已经在改驱动但总在枚举和传输边界上卡壳的人。2. 驱动框架先搭对sdio_driver 注册与匹配过程拆解2.1 一份最小可用的 sdio_driver 结构体Linux 内核里一个 SDIO 外设驱动挂在 sdio_bus 上不挂在 platform 总线。你即使把.probe写得天花乱坠id_table 对不上probe 也不会被调用。先放一个能通过编译、可以当模板用的最小框架这是我每次新建驱动时复制粘贴的起点#include linux/module.h #include linux/mmc/sdio_func.h #include linux/mmc/sdio_ids.h static const struct sdio_device_id my_sdio_ids[] { /* 厂商ID和设备ID来自芯片手册或枚举日志打印的实际值 */ { SDIO_DEVICE(0x6666, 0x0001) }, { /* 表项必须以空结构收尾 */ } }; MODULE_DEVICE_TABLE(sdio, my_sdio_ids); static int my_sdio_probe(struct sdio_func *func, const struct sdio_device_id *id) { dev_info(func-dev, probe vendor0x%04x device0x%04x\n, func-vendor, func-device); return 0; } static void my_sdio_remove(struct sdio_func *func) { dev_info(func-dev, remove\n); } static struct sdio_driver my_sdio_driver { .name my_sdio_wifi, .probe my_sdio_probe, .remove my_sdio_remove, .id_table my_sdio_ids, }; module_sdio_driver(my_sdio_driver); MODULE_LICENSE(GPL);这里三个关键点值得展开。第一SDIO_DEVICE(0x6666, 0x0001)里的两个数字不是随便填的。厂商 IDvendor是 SDIO 协会分配给芯片厂商的设备 IDdevice是厂商自己的型号编号。最靠谱的拿法不是翻手册猜而是等枚举之后把func-vendor和func-device打印出来回填手册里给出的支持列表只能当参考。probe 入口的dev_info比裸printk好用内核日志会带上mmc0:0001:1这类总线号和功能号多卡场景下排查省事很多。第二module_sdio_driver(my_sdio_driver)这个宏内部展开成module_driver把sdio_register_driver和sdio_unregister_driver包进module_init/module_exit。它的问题是加载失败时也不会有直观报错每次 insmod 之后必须看dmesg | tail别只看 insmod 的回显。第三MODULE_DEVICE_TABLE(sdio, ...)在编译时生成模块别名内核才有办法把“枚举出来的 vendor/device”跟这个模块关联起来。少了这一行哪怕手动 modprobe 成功总线匹配阶段也找不到驱动probe 照样不触发。这是我在好几个项目上踩过的坑后面 2.3 会专门写排查顺序。2.2 probe 里真正该做的事初始化顺序决定成败probe 被调用只代表 MMC 控制器读到了卡的 CID/CCCR外设本身的电压、时钟、总线宽度还没有就绪。probe 里的初始化顺序我一般固定如下顺序颠倒会让后面的寄存器访问全部翻车static int my_sdio_probe(struct sdio_func *func, const struct sdio_device_id *id) { u8 fbr; int ret 0; /* sdio_enable_func / sdio_set_block_size 内部会自行 claim host */ ret sdio_enable_func(func); if (ret) return ret; ret sdio_set_block_size(func, 64); if (ret) return ret; /* 访问自己的寄存器窗口前显式拿锁保护多 function 竞争 */ sdio_claim_host(func); fbr sdio_readb(func, 0x100 func-num * 0x100, ret); sdio_release_host(func); if (ret) return ret; dev_info(func-dev, FBR0x%02x\n, fbr); return 0; }先解释sdio_enable_func。它对应 SDIO 协议里往 CCCR 的 IOEnable 寄存器写位把指定功能从掉电或复位状态拉起来。在那之前读用户寄存器大概率读回 0xFF写进去也会被忽略。很多新手一上来就sdio_readb读到的全是垃圾值然后开始怀疑总线时序实际上是功能根本没使能。sdio_set_block_size(func, 64)设置的是块模式传输时单块字节数最大不能超过函数在 FBR 里声明的能力值最小一般 32 或 64。不为它赋值有些控制器会在 CMD53 块传输时直接报错或者反复重试。0x100 func-num * 0x100是 FBR 的地址计算。SDIO 的寄存器空间是这样分的0x00~0xFF 是 CCCR 公共区所有功能共享0x100 开始每个功能按 256 字节占一段 FBR描述这个功能支不支持中断、块大小上限、制造商信息等。读它主要是确认当前使能的功能号跟硬件对得上也顺便验证了单字节读通道是通的。2.3 probe 不进来先查三层别急着改 id_tableprobe 没被调用多数情况不是驱动代码的问题。我固定按三层查先看总线上有没有设备节点再看节点属性值最后才轮到 id_table 有没有写错。命令如下ls /sys/bus/sdio/devices/ cat /sys/bus/sdio/devices/mmc0:0001:1/vendor cat /sys/bus/sdio/devices/mmc0:0001:1/device cat /sys/bus/sdio/devices/mmc0:0001:1/numvendor文件里的值要和SDIO_DEVICE(0x6666, ...)里的 0x6666 完全一致。注意cat打印出来的可能是十进制的 “26214” 而不是0x6666转换回十六进制再对比别肉眼对齐出误判。num表示这是第几个功能多功能的 WiFi/BT 组合芯片经常会看到mmc0:0001:1和mmc0:0001:2两个节点分别对应 WiFi 和蓝牙需要两个驱动分别 bind。如果/sys/bus/sdio/devices/完全是空的说明设备枚举都没完成问题在更底层控制器有没有识别到卡、CD/DAT3 引脚上拉是否正确、时钟有没有起来。这时候查dmesg里 mmc core 的枚举日志常见日志有mmc0: new SDIO card at address 0001也常见到mmc0: error -110 whilst initialising SDIO card-110 是超时十有八九是时钟或上拉。还有一种情况值得单独说同一颗模块在 Windows 主机上出现“无法验证此设备所需的驱动程序的数字签名”这类提示不代表 Linux 侧枚举一定失败。信号路径是同一套但软件栈完全不同一个卡在签名校验一个是内核枚举器在跑。处理这类问题我从来是两条路径分开排查Linux 侧只看/sys/bus/sdio是否生成了节点绝不拿 Windows 的报错去反推内核里的匹配失败原因。3. 数据收发链路从单字节寄存器到多块 CMD53 的完整路径3.1 单字节访问背后的 IO_RW_DIRECTsdio_readb和sdio_writeb是驱动里最常用的两个接口但底层操作很多人没在意它们走的是 SDIO 协议的 CMD52IO_RW_DIRECT命令一次只能读写一个字节且数据直接在命令阶段携带不经过数据线因此不适合搬大块数据。代码里它长这样u8 val; int err 0; sdio_claim_host(func); val sdio_readb(func, 0x2, err); /* 读CCCR的 IOEx 寄存器 */ if (err) goto out; sdio_writeb(func, 0x01, 0x2, err); /* 使能 function1 */ out: sdio_release_host(func);err参数是输出型的拿到引用再判断比靠返回值判断更直观。sdio_readb返回的 u8 是读到的值如果err非零这个 u8 已经不可信。常见误用是读完才去判断返回值而返回值本身只是函数调用状态不是总线上的数据。另外这两个接口要求调用者已经sdio_claim_host内核文档里没强调实际多 func 并存时不拿锁很容易出现数据错乱。为什么要手动sdio_claim_hostMMC 控制器在同一时刻只能处理一条命令。SDIO 卡上的多个功能共享一个物理通道两个驱动线程同时发 CMD52控制器会直接冲突。sdio_claim_host拿的是 host 级互斥锁而不是某个功能自己的锁这是理解 SDIO 并发模型的关键。所有sdio_*系列 API 里文档明确说会自己 claim 的比如sdio_enable_func内部才能省掉这一步。3.2 搬大块数据用 CMD53块模式和字节模式的区别一旦驱动要搬 WiFi 的 RX/TX 队列比如一次传 512 字节以上的包再拿sdio_readb打循环就既慢又容易触发控制器超时。这时候必须用 CMD53IO_RW_EXTENDED内核封装成sdio_memcpy_*和sdio_readsb/writesb。典型用法#define TX_FIFO_ADDR 0x1000 #define TX_BUF_LEN 512 int my_sdio_send(struct sdio_func *func, const void *buf, size_t len) { int ret; sdio_claim_host(func); /* 这个接口会把 len 拆成若干 block块大小由 sdio_set_block_size 决定 */ ret sdio_writesb(func, TX_FIFO_ADDR, (void *)buf, len); sdio_release_host(func); return ret; }sdio_writesb走的是块模式要求buf长度是块大小的整数倍否则函数会返回-EINVAL。很多刚上手的人在这里翻车设了 64 字节块大小却传 100 字节的包返回 -EINVAL 就开始查中断查时钟其实只是长度约束。如果不按块走可以用sdio_memcpy_toio/sdio_memcpy_fromio它们走字节模式对长度没有块对齐要求适合小批量、不规则的寄存器组读写。取舍很明确块模式吞吐高、但要求对齐字节模式灵活、但一次传输的字节数受控制器 FIFO 深度限制。无线网卡这种高流量场景驱动里的数据通路几乎必然是块模式字节模式只用于配置寄存器。3.3 缓冲区对齐与 DMA 约束最容易磨掉半天时间块模式一旦用上DMA 对齐问题就跟着来了。很多 MMC 控制器的 ADMA/SDMA 引擎要求缓冲区起始地址和传输长度在 4 字节边界对齐部分控制器要求更狠必须 8 或 32 字节对齐。表现很怪数据偶尔出错、偶尔 CRC 错、连续跑后又恢复。我见过一个团队拿大缓冲测试跑几个小时才崩一次最后定位到是收包缓冲用了栈上数组地址没对齐。安全做法是数据缓冲区统一从kmalloc分配并做手动对齐static unsigned char *tx_buf; tx_buf kmalloc(2048 32, GFP_KERNEL); if (!tx_buf) return -ENOMEM; /* 手动对齐到 32 字节给控制器留足余量 */ tx_buf PTR_ALIGN(tx_buf, 32);PTR_ALIGN本质是把地址向上舍入到 32 的倍数。更干净的做法是像不少驱动那样在模块加载阶段就分配一块固定缓冲协议栈重置时复用同一块避免反复申请释放。对齐只是 DMA 约束的一半另一半是 cache 一致性。读方向的缓冲在传给控制器前后必要时要用dma_map_single配合dma_unmap_single并注意方向是DMA_FROM_DEVICE还是DMA_TO_DEVICE。方向填反数据表现会是“写进去了读不出来”或“第一次读对第二次读全是脏缓存”。这类问题在单核平台表现不明显一跑双核 SMP 就容易复现。3.4 一个小型收发闭环的骨架把前面的块传输和缓冲管理串起来最小收发闭环长这样int my_sdio_xfer(struct sdio_func *func, unsigned int addr, void *buf, size_t len) { void *aligned; int ret; aligned PTR_ALIGN(buf, 32); sdio_claim_host(func); if (len % 64) ret sdio_memcpy_toio(func, addr, aligned, len); else ret sdio_writesb(func, addr, aligned, len); sdio_release_host(func); return ret; }这里用len % 64判断是不是走块模式64 是sdio_set_block_size(func, 64)设定的值。逻辑上不严谨——块模式其实要求len % block_size 0但作为初版骨架够用正式驱动里应该在初始化阶段就固定下一条数据通路不要在收发路径上做动态分支。高速场景下每次sdio_claim_host/sdio_release_host本身就是开销驱动设计上要尽量保持 host 占用时间短长传输一次性做完而不是分成多次小传输。4. 中断通路SDIO IRQ 的触发条件与回调时机4.1 SDIO 中断的信号本质不是 GPIO 中断SDIO 中断走的是卡上的 DAT[1] 数据线电平触发低有效不是边沿触发。外设把这条线拉低host 控制器检测到低电平后由 mmc core 启动一个内核线程依次回调该卡上各个 function 注册的 handler。之所以不能用边沿是因为 SDIO 卡没有独立的中断引脚只能靠共享数据线上的电平状态来传递“有事件”这个信号。这个机制意味着两件事。第一handler 里必须主动清中断把 DAT[1] 恢复到高电平否则中断会一直触发第二handler 运行在 mmc core 的 irq 线程里属于进程上下文不是硬中断上下文所以可以在里面调用sdio_claim_host这类会睡眠的函数但不能把它当原子上下文用。很多新手用惯了 GPIO 中断看到sdio_claim_irq的名字就以为是硬中断直接在 handler 里做耗时操作结果把 mmc irq 线程卡死整个 SDIO 通道后面的所有传输都排队。处理这类问题我固定思路handler 只做两件事——读功能自己的中断状态寄存器确认事件源然后触发一个 workqueue 或 tasklet 去做实际的数据搬运。4.2 申请中断的完整路径sdio_claim_irq在 probe 阶段申请回调函数签名是固定的static void my_sdio_irq(struct sdio_func *func) { u8 status 0; int err 0; /* handler 运行在进程上下文可以安全拿锁 */ sdio_claim_host(func); status sdio_readb(func, 0x200, err); /* 功能自己的pending寄存器 */ sdio_release_host(func); if (err) return; if (status BIT(0)) { /* 事件来源确认触发数据通道继续搬数 */ } }在 probe 里注册static int my_sdio_probe(struct sdio_func *func, const struct sdio_device_id *id) { int ret; ret sdio_enable_func(func); if (ret) return ret; ret sdio_set_block_size(func, 64); if (ret) return ret; ret sdio_claim_irq(func, my_sdio_irq); if (ret) return ret; return 0; }sdio_claim_irq在内部会打开 host 对 DAT[1] 的电平侦测同时使能卡的中断能力所以直接调它即可不需要额外往 CCCR 里写中断使能位。回调里读的0x200是我们这个功能自己的中断状态寄存器具体偏移在芯片手册里不同芯片差异大。它存在的意义是SDIO 支持多个 function 共享同一根中断线handler 里不读状态就没法知道这次是谁的事件甚至可能替别的功能清了中断造成事件丢失。4.3 中断风暴比数据错乱更隐蔽的死锁中断风暴是 SDIO 中断设计里最典型的现场外设始终把 DAT[1] 拉低host 不停上报中断mmc irq 线程被反复唤醒CPU 占用飙升但业务数据一个都不对。常见原因是 handler 里清了卡的中断状态寄存器但功能内部的子事件没有真正处理完硬件把 pending 位一直保持为 1导致电平永远拉不低。另一个常见场景是寄存器读错了偏移读到的是保留位写了没反应DAT[1] 一直低。排这类问题我会在 handler 入口加一个计数器导到 debugfs 里看中断速率static unsigned long my_irq_cnt; static void my_sdio_irq(struct sdio_func *func) { my_irq_cnt; /* ... 原有处理逻辑 ... */ }然后观察计数增长曲线。如果每秒几万次基本就是 pending 没清干净如果完全不增长回到 DAT[1] 上拉和 host 使能位上查。中断处理这种问题玄学成分很低老老实实看计数和波形就能定位。另一个容易忽略的点申请中断后sdio_release_irq在 remove 路径里必须调用否则模块卸载时回调指针悬空下次中断直接 panic。顺序和申请反过来先 release_irq 再 disable_func。5. 避坑清单五条工单级的注意现场5.1 probe 成功但第一次 readb 读回全 0xFF现象probe 里的dev_info正常打印紧接着sdio_readb(0x00)返回 0xFF写寄存器也不生效芯片像没通电。原因sdio_enable_func只是把 IOEnable 位置位模块内部 MCU 和固件需要时间上电自检SDIO 寄存器空间在自检完成前不响应。另一个常见原因是供电的 GPIO 在 probe 前没有被拉起来模块压根没跑起来。解决probe 里 enable 之后加 10ms 延时再用循环轮询 CCCR 的 IORx0x03确认功能 ready 再往下走。硬件侧用示波器看 3.3V/1.8V 是否先于时钟稳定。5.2 切了 4-bit 模式后吞吐反而掉一半现象往SDIO_CCCR_BUS_WIDTH寄存器写值切 4-bit结果吞吐率从 20MB/s 掉到 10MB/s甚至传输直接停止。原因卡侧切完host 控制器没有同步到 4-bit 模式。SDIO 总线宽度是双向协商——驱动的sdio_set_block_size和写总线宽度寄存器只完成了卡侧动作host 还需要调用mmc_set_bus_width或者靠设备树里的 bus-width 属性。解决在设备树里配sdio-bus-width 4或者确认 host 的 caps 已带MMC_CAP_4BIT_DATA。先让 host 就绪再切卡侧寄存器别只在一个方向使劲。5.3 sdio_claim_irq 成功但中断永远不来现象中断注册不报错外设确实产生了事件handler 就是不执行。原因DAT[1] 线上没有合适的上拉或者 host 控制器的 SDIO 中断使能位没打开。也可能是外设的中断电平极性和 host 预期不一致低有效被配成了高有效。解决DAT[1] 需要外部上拉很多模块内部有上拉但转接板不一定接出来查原理图确认。host 侧通过/proc/interrupts看对应 mmc 中断计数是否增长计数增长但 handler 没跑是 SDIO 中断 pending/route 配置问题计数都不长回硬件上拉和极性上查。5.4 大包吞吐时偶发 CRC 错误单包测试全过现象小包连续读写没问题一上大块 CMD53 就跑出 mmc 层的 CRC/ECC 错误偶发可能要跑几分钟才出现一次。原因DMA buffer 地址或长度不对齐或 cache 一致性问题。ADMA 引擎在非对齐地址上做 split 时处理器和控制器容易产生竞态。解决收发包缓冲统一走kmallocPTR_ALIGN到 32 字节长度保持块大小整数倍DMA 映射时正确选择方向。血泪经验先把 WiFi 协议栈停掉只留一个循环搬数测试几分钟内就能复现别在完整协议栈上瞎猜。5.5 拔卡或休眠唤醒后驱动里所有寄存器全变 0xFF现象运行时一切正常系统 suspend/resume 后任何sdio_readb都返回 0xFF重新 probe 也不行。原因SDIO 卡的电源域和唤醒源配置不一致。多数模块在 suspend 时被直接断电但驱动没在休眠前保存并恢复寄存器状态或者卡还在掉电状态时钟就恢复控制器和模块不同步。解决在 suspend 回调里触发sdio_disable_func保存必要的寄存器resume 时按 probe 顺序重新 enable 并恢复配置。比依赖卡自己保存状态稳妥。项目上后来干脆把电源控制交给 regulator 框架休眠时序用 pinctrl 统一管理问题才彻底解决。6. 上板验证把 /sys 和 debugfs 当作第一层示波器驱动能加载、能收发之后离开串口日志就该有一套固定验证流程。我习惯分三层走每层都有明确的“通过”标准避免靠感觉确认。第一层是枚举层。/sys/bus/sdio/devices/下必须出现设备节点vendor/device 和驱动 id_table 一致。这个节点存在说明 MMC 控制器已经把卡从协议层面认出来了。第二层是时序层。通过内核 debugfs 看当前时钟、总线宽度、时序模式确认 host 是否真的跑在预期频率上。# 第一层枚举节点 ls /sys/bus/sdio/devices/ # 第二层时序与总线宽度路径因 host 而异常见为 mmc0 cat /sys/kernel/debug/mmc0/ios # 第三层中断计数观察 mmc 相关 irq 是否在增长 grep mmc /proc/interruptsios文件里会打印 clock、bus width、timing 等字段。比如配了 4-bit 模式这里要看到4 bits (sdio)如果还是1 bit说明 host 侧没切过去回设备树查。/proc/interrupts里找 mmc 对应的行触发一次外设事件后看计数是否增长这是确认中断通路活着的通用手段。如果这两层都正常再进数据层。手头有逻辑分析仪就抓 DAT[0:3] 和 CLK 波形确认 CMD53 的块边界没有分析仪就把数据通路做一次长跑测试——连续搬 512 字节块跑 10 万次统计错误率。我在验证时通常还会把一个简单的计数节点加到驱动里while true; do cat /sys/kernel/debug/my_sdio/irq_cnt; sleep 1; done数值稳定增长但增速平稳说明中断是“事件驱动”而非“电平抖动”这一步能提前把 5.3 和 5.4 的隐患暴露出来。整套验证流程走完驱动才算真正过了验收。项目收尾后我把这套三层核验存成了固定动作。从那以后我每次接手 SDIO 外设驱动的第一件事就是先在 sysfs 层确认枚举再进 debugfs 看好时序最后才动手改代码。这套习惯让我少走了很多弯路希望帮到你。本文还有配套的精品资源点击获取