Linux SDIO驱动开发:从CMD52/CMD53到probe与中断避坑 简介面向嵌入式驱动开发学习者的SDIO驱动资料包聚焦SDIO协议基础、驱动分层、设备探测与初始化、数据传输、中断处理及Linux MMC框架下的注册流程适合需要接入Wi-Fi、蓝牙、GPS等SDIO设备的中高级开发者也可供课程设计与项目开发参考。压缩包共23个文件约1.3MB以协议规范PDF、项目总结文档、C/H源码、Keil/IAR工程文件以及原理图和PCB设计资料为主兼顾理论查阅与工程实践。已有669人学习下载具备一定参考热度。资料围绕FSEL功能选择、命令响应、DMA与中断传输、错误恢复、低功耗和并发同步等环节展开并指向Linux下示例驱动的学习路径便于读者对照规范梳理驱动结构、搭建调试环境、排查总线通信与中断问题形成可复用的开发思路。同时保留规范文档与工程输出方便按模块核对配置参数与编译结果。1. 板子上了电dmesg 里死活不出现 new SDIO card前段时间在调一块焊死的 SDIO 模块硬件说供电正常、复位也拉了结果上电后/dev底下什么都没有dmesg 里mmc1翻了半天只报一句初始化超时。我把这份 sdio 驱动程序的源码拆开对照才发现问题根本不在驱动代码而是在 host 侧的 pwrseq 时序和 CIS 里的 vendor/device id 上——驱动本身写得没问题是我把SDIO_DEVICE()的参数填成了芯片型号。这套代码值得读的地方是它把 SDIO 从协议层到驱动层串成了一条完整的链路CMD52 单字节寄存器访问、CMD53 块传输、sdio_driver注册与 probe 时序、中断线程化拆分再到 sysfs 调试接口一层都没跳过。适合已经写过字符设备、想往 mmc 子系统和嵌入式 BSP 方向深入的开发者如果你只是想要个能跑的 demo也能照着第 3 章直接编译加载。2. 协议与框架一笔数据从 CMD52 到 CMD53 走了哪几层SDIO 的坑有一半出在概念混淆上很多人以为 SDIO 就是“SD 卡加了个中断脚”于是拿 SD 存储卡的经验去套结果在功能号、块大小、CIS 这几处反复翻车。这一章先把协议侧和内核侧的结构对齐后面写代码时你才知道每一行调用落在了哪一层。2.1 CMD52 与 CMD53慢速寄存器口和高速数据口SDIO 在 SD 总线的命令集上扩展了两条命令。CMD52 是单字节读写用来访问每个功能的寄存器空间特点是慢但简单一次搬运一个字节还要带地址CMD53 是批量读写支持字节模式和块模式FIFO 数据、固件下载、大包收发都走它。一张 SDIO 卡最多有 7 个功能function 17另外还有一个所有卡都有的 function 0叫 CIACommon I/O Area里面放的是公共控制寄存器。功能自己的寄存器区叫 FBRFunction Basic Register每个功能占 0x100 字节。常用的几个位置几乎是所有 SDIO 驱动都会碰的寄存器地址作用SDIO_CCCR_IOEx0x02功能使能位第 n 位置 1 表示使能 function nSDIO_CCCR_IENx0x04中断使能配合 IENM 使用SDIO_CCCR_IF0x07总线宽度、块模式/字节模式选择SDIO_CCCR_BLKSIZE0x10功能 0 的块大小低字节FBR 0x000x100×n功能 n 的使能状态与块大小这里有个反直觉的点块大小在 CCCR 里也有一份FBR 里也有一份。很多人只调了sdio_set_block_size()就以为完事实际固件那边读的是 FBR 的值两边不一致时会出现“发得出去、收不回来”的玄学现象。常见做法是设完块大小后立刻回读一遍 FBR 校验。2.2 内核里的四层host 控制器、core、sdio bus、功能驱动Linux 的 mmc 子系统把 SDIO 拆得比想象中干净。最底下是 host 控制器驱动各家 SoC 一套负责发命令、搬 DMA往上是mmc_core处理卡的初始化、枚举、电源管理再往上是sdio_bus把每个功能抽象成一个struct sdio_func设备挂到总线上最上面才是你我要写的功能驱动也就是struct sdio_driver。这个分层带来的直接好处是功能驱动里看不到任何寄存器地址也不需要知道 host 是哪个 SoC。你能用的接口就那几个——sdio_readb、sdio_writeb、sdio_readsb、sdio_writesb、sdio_memcpy_fromio、sdio_memcpy_toio、sdio_claim_irq。驱动结构和 platform 驱动几乎一模一样只是probe的参数换成了struct sdio_func *。/* 功能驱动要填的东西id 表 probe/remove 回调 */ static const struct sdio_device_id demo_ids[] { { SDIO_DEVICE(0x02df, 0x9135) }, /* vendor, device 来自 CIS */ { /* 必须以空项结尾 */ } }; MODULE_DEVICE_TABLE(sdio, demo_ids); static struct sdio_driver demo_driver { .name demo_sdio, .id_table demo_ids, .probe demo_probe, .remove demo_remove, }; module_sdio_driver(demo_driver);SDIO_DEVICE(vendor, device)这两个参数必须是 CIS 里读出来的 16 位 MANFID 和 PRODID不是数据手册封面上的芯片型号。module_sdio_driver()这个宏展开后就是module_init/module_exit加注册注销和module_platform_driver()一个套路。设备端不用自己创建总线sdio_bus是内核启动时就注册好的。2.3 枚举是谁做的从设备树到 sdio_func很多人以为 SDIO 设备要在设备树里写一个节点驱动才会被匹配。实际上枚举是 mmc core 自己做的host 控制器探测到卡之后mmc_attach_sdio()会读 CIS、算出这张卡有几个功能、每个功能的 vendor/device id 是多少然后对每个功能调sdio_add_func()注册成设备。设备树节点的作用只有一个——给这个已经创建出来的功能设备绑上of_node好让驱动能用of_match_table或者从节点里取 GPIO、时钟这些板级资源。/* 板级 dts给焊死的 SDIO 设备准备 host 控制器 */ mmc1 { status okay; bus-width 4; non-removable; /* 焊在板上没有插拔检测 */ keep-power-in-suspend; cap-sdio-irq; /* 允许 SDIO 中断走 DAT1 */ mmc-pwrseq sdio_pwrseq; vmmc-supply vcc_3v3; /* 子节点的 reg 就是功能号不是片选 */ demo_card: demo1 { reg 1; compatible demo,demo1000; }; }; sdio_pwrseq { reset-gpios gpio1 12 GPIO_ACTIVE_LOW; post-power-on-delay-ms 50; /* 复位释放到第一条命令的间隔 */ };non-removable是焊死模块的标配缺了它 core 会不停做卡检测日志里刷一堆 rescan。cap-sdio-irq决定 DAT1 中断能不能用这个不写后面sdio_claim_irq()直接返回-EINVAL。子节点的reg 1对应 function 1#address-cells/#size-cells在某些内核版本上要求父节点必须声明否则子节点解析会失败——这也是个容易被忽略的小坑。3. 最小可跑骨架一个 sdio_driver 从注册到 probe 的完整代码协议理清之后落地其实很快。这一章按“文件放哪 → id 表怎么填 → probe 里按什么顺序做事 → host 侧还要准备什么”的顺序走一遍代码可以直接抄进你的工程改。3.1 目录与构建把驱动塞进内核树的哪个位置我的习惯是先做成树外模块调通了再决定要不要进内核树。树外模块的好处是改一行make一下不用整编内核。目录结构就三个文件demo_sdio/ ├── Makefile ├── Kconfig # 进内核树时才需要 └── demo_sdio.c# 树外编译make -C /lib/modules/$(uname -r)/build M$PWD modules obj-m demo_sdio.o KDIR ? /lib/modules/$(shell uname -r)/build PWD : $(shell pwd) all: $(MAKE) -C $(KDIR) M$(PWD) modules clean: $(MAKE) -C $(KDIR) M$(PWD) cleanKDIR指向当前运行内核的构建目录交叉编译时换成目标板的 kernel build 路径即可同时要带上ARCH和CROSS_COMPILE。如果之后要进内核树Kconfig里加一个tristate Demo SDIO device driverdepends on MMC再把它加进drivers/misc/Kconfig和对应的Makefile。这一步不着急先把树外跑通。提示交叉编译时KDIR一定要指向目标内核的 build 目录而不是宿主机的。指错了会出现“模块版本魔数不匹配”insmod直接拒绝加载。3.2 id_table 与注册先让 probe 被调起来写驱动最怕的就是“代码全对probe 不进来”。所以第一步不是写初始化而是先把 id 表填对、把注册打出来看内核有没有认这个设备。#include linux/module.h #include linux/mmc/sdio_func.h #include linux/mmc/sdio_ids.h struct demo_card { struct sdio_func *func; struct work_struct rx_work; u8 *rx_buf; u8 irq_status; }; /* 这两个值必须和 CIS 里的一致改成芯片型号是匹配不上的 */ #define DEMO_VENDOR_ID 0x02df #define DEMO_DEVICE_ID 0x9135 static const struct sdio_device_id demo_ids[] { { SDIO_DEVICE(DEMO_VENDOR_ID, DEMO_DEVICE_ID) }, { } }; MODULE_DEVICE_TABLE(sdio, demo_ids);模块加载后/sys/bus/sdio/devices/下会列出形如mmc1:0001:1的目录名字里的0001是 vendor最后的1是功能号。进去cat vendor和cat device把这两个十六进制值抄回来填进SDIO_DEVICE()匹配基本就稳了。# 板子上执行确认功能设备已经枚举出来 ls /sys/bus/sdio/devices/ # mmc1:0001:1 mmc1:0001:2 cat /sys/bus/sdio/devices/mmc1:0001:1/vendor cat /sys/bus/sdio/devices/mmc1:0001:1/device如果/sys/bus/sdio/devices/是空的别急着改驱动——那是枚举阶段就没成功问题在 host 侧供电或时序回头去看第 5 章第一条。3.3 probe 的正确顺序使能功能、占用 host、设块大小、申请中断probe里最容易出错的是顺序和错误路径。少了sdio_enable_func()就去读寄存器读到的是全 0xffsdio_claim_host()和sdio_release_host()没配对host 被锁住整张卡后面的访问全部卡死。static int demo_probe(struct sdio_func *func, const struct sdio_device_id *id) { struct demo_card *card; int ret; /* 先打印 CIS 里的真实 id方便核对 */ dev_info(func-dev, demo probe: vendor%04x device%04x func%u\n, func-vendor, func-device, func-num); card devm_kzalloc(func-dev, sizeof(*card), GFP_KERNEL); if (!card) return -ENOMEM; card-func func; card-rx_buf devm_kmalloc(func-dev, 4096, GFP_KERNEL); if (!card-rx_buf) return -ENOMEM; sdio_set_drvdata(func, card); /* 一串需要原子完成的时序整段占住 host */ sdio_claim_host(func); ret sdio_enable_func(func); /* 置位 CCCR IOEx */ if (ret) { dev_err(func-dev, enable func failed: %d\n, ret); goto err_release_host; } ret sdio_set_block_size(func, 512); /* 块模式的块大小 */ if (ret) { dev_err(func-dev, set block size failed: %d\n, ret); goto err_disable_func; } /* 厂商私有的初始化序列放这里写配置、下载固件、开中断掩码 */ ret demo_hw_init(func); if (ret) goto err_disable_func; sdio_release_host(func); INIT_WORK(card-rx_work, demo_rx_work); /* 申请中断内部自己会占一次 host别放在 claimed 区间里 */ ret sdio_claim_irq(func, demo_irq); if (ret) { dev_err(func-dev, claim irq failed: %d\n, ret); goto err_release_host_after; } return 0; err_disable_func: sdio_disable_func(func); err_release_host: sdio_release_host(func); err_release_host_after: return ret; }这段代码有三个细节值得说。sdio_enable_func()失败通常意味着这个功能在卡上就不存在或者 CIS 里的功能号写错了先回ls /sys/bus/sdio/devices/核对功能号。sdio_set_block_size(func, 512)的第二个参数必须是 2 的幂且不超过 host 支持的上限func-cur_blksize里能看到最终生效的值传 0 的话内核会用一个默认值。错误路径用了三段 goto分别对应“已 enable”“已 claim”“都已做”三种状态这种分层写法看着啰嗦但漏掉任何一次sdio_release_host()debug 的时候你会想砸键盘。sdio_claim_irq()之所以放到release_host之后是因为它内部会自己占一次 host放在已占用的区间里就变成自己等自己。这是典型的黑匣子式死锁现象是 insmod 之后系统整个卡住连串口都没输出。3.4 host 侧还要准备什么上电时序和 32k 时钟驱动代码没问题但 probe 不进来九成是这一节的事。焊死的 SDIO 模块通常需要三样东西同时到位3.3V 主供电、复位脚释放、32.768kHz 低速时钟部分模块用于睡眠唤醒。主供电和复位用mmc-pwrseq-simple描述代码见 2.3 的 dts。这里的关键是post-power-on-delay-ms很多模块手册上写的是“复位释放后 10ms 可用”实测往往要 3050ms 才稳给少了就是随机枚举失败换个板子又能跑非常迷惑。32k 时钟一般是 host 控制器的一个输出脚需要在 pinctrl 里配置成低功耗模式并且确认mmc1节点上开了对应属性。有些 SoC 把它叫mmc-clkgate-delay有些叫keep-power-in-suspend配套的低速时钟使能具体看 host 驱动。这部分是纯粹的平台差异我的经验是直接找 SoC 厂商提供的同型号参考 dts 对照比自己啃手册快得多。4. 数据面实现寄存器封装、块传输和中断拆分probe 通了之后就到了真正干活的部分。SDIO 驱动的数据面其实就三件事怎么安全地读写寄存器、怎么高效地搬大块数据、中断来了之后在哪里做耗时处理。4.1 寄存器读写封装别在业务代码里裸调 sdio_readb裸调sdio_readb()的问题是错误码没地方放。它的返回值是读到的字节读写失败时返回 0xff错误码只能通过最后一个指针参数带回。业务代码里到处写int ret显然不现实所以我的习惯是包一层/* 返回负数表示失败成功时把值写进 *val */ static int demo_reg_read(struct demo_card *card, unsigned int addr, u8 *val) { int ret 0; *val sdio_readb(card-func, addr, ret); if (ret) dev_dbg(card-func-dev, read %#x failed: %d\n, addr, ret); return ret; } static int demo_reg_write(struct demo_card *card, unsigned int addr, u8 val) { int ret 0; sdio_writeb(card-func, val, addr, ret); return ret; }封装之后if (demo_reg_read(card, REG_CHIP_ID, id))这种写法就清爽了错误码也统一进dev_dbg。addr是功能内的偏移不用自己加SDIO_FBR_BASE()内核在sdio_readb里已经按func-num处理过了。要读 function 0 的 CCCR需要用sdio_func[0]那个特殊的 func但一般不建议驱动直接碰交给 core 管。顺手加一个 sysfs 属性调试时不用改代码重编模块static ssize_t chip_id_show(struct device *dev, struct device_attribute *attr, char *buf) { struct sdio_func *func dev_to_sdio_func(dev); u8 id; int ret; id sdio_readb(func, DEMO_REG_CHIP_ID, ret); if (ret) return ret; return sysfs_emit(buf, 0x%02x\n, id); } static DEVICE_ATTR_RO(chip_id);dev_to_sdio_func()把设备指针还原成sdio_funcsysfs_emit()是新内核里替代sprintf的写法会自己处理缓冲区边界。属性在 probe 里用device_create_file(func-dev, dev_attr_chip_id)创建remove 里对应删掉。之后cat /sys/bus/sdio/devices/mmc1:0001:1/chip_id就能读到芯片 ID验证通信是否正常最直接的办法就是这个。4.2 块传输块大小、块数上限和缓冲区对齐大块数据走sdio_readsb/sdio_writesb它们内部用的是 CMD53。这里有两个硬约束一次 CMD53 最多 511 个块字节模式单次最多 512 字节缓冲区如果给的是栈上地址或者 vmalloc 出来的非连续内存走 DMA 的 host 会直接报错或者搬出错乱数据。#define DEMO_FIFO_REG 0x20 #define DEMO_MAX_BLOCKS 64 /* 自己限一个安全值别贴着 511 */ static int demo_read_fifo(struct demo_card *card, u8 *buf, int len) { struct sdio_func *func card-func; int blocks, ret; if (len 0 || len % func-cur_blksize) return -EINVAL; blocks len / func-cur_blksize; if (blocks DEMO_MAX_BLOCKS) return -EINVAL; /* buf 必须是 kmalloc DMA 可访问的内存 */ ret sdio_readsb(func, buf, DEMO_FIFO_REG, len); if (ret) dev_err(func-dev, fifo read %d bytes failed: %d\n, len, ret); return ret; }func-cur_blksize是上一步sdio_set_block_size()生效后的值直接拿来算块数最稳妥。sdio_readsb的第四个参数是字节数是块大小整数倍时内核自动走块模式不是整数倍就退回字节模式——后者效率差很多所以缓冲长度最好按块对齐。缓冲区用devm_kmalloc()分配的普通内存就够物理连续DMA 能直接用如果非要用自己管理的环形缓冲记得用dma_map_single()映射一遍再传进来。对于字节数不是块大小整数倍的零碎读取常见做法是先算sdio_align_size(func, size)拿到对齐后的长度再传避免内核在内部做一次 bounce buffer 拷贝——那层拷贝不会报错但会悄悄吃掉性能。4.3 中断拆分回调里只做三件事SDIO 中断有个和普通外设很不一样的地方它不是硬中断上下文。mmc core 为每张卡起了一个sdio_irq_thread内核线程你的回调是在这个线程里被调用的所以可以睡眠、可以读寄存器不用担心原子上下文那一套限制。但有个前提必须先说清楚回调进来的时候 host 已经被这个线程占住了你在里面再调一次sdio_claim_host()就是自己等自己系统直接挂死。这是我在这个项目里踩得最深的一脚。static void demo_irq(struct sdio_func *func) { struct demo_card *card sdio_get_drvdata(func); u8 status; int ret; /* 进来时 host 已被 mmc 的 irq 线程持有这里不能再 claim */ status sdio_readb(func, DEMO_REG_IRQ_STATUS, ret); if (ret || !status) return; /* 写 1 清中断位定义看模块手册别照抄别的芯片 */ sdio_writeb(func, status, DEMO_REG_IRQ_STATUS, ret); card-irq_status | status; if (status DEMO_IRQ_RX_READY) schedule_work(card-rx_work); /* 搬运丢给工作队列 */ }回调里做的事情严格控制在“读状态、清中断、唤醒工作队列”三件。真正的 FIFO 搬运放到demo_rx_work里做因为一次读 4KB 可能要几百微秒到几毫秒占着 irq 线程会挡住后续中断的处理表现出来就是高负载下丢包。工作队列里第一件事是sdio_claim_host()搬完sdio_release_host()中间不要有msleep()之类的长延时。sdio_claim_irq(func, demo_irq)是在 probe 里申请的remove 里必须sdio_release_irq(func)再sdio_disable_func(func)顺序反了会出现“中断还在来、功能已经关了”的竞态。很多人在这里翻车因为 rmmod 之后系统不一定立刻崩等你再 insmod 一次就炸了。5. 避坑与排查枚举不到、probe 不进、传输超时的现场处理这几条都是我在实际板子上遇到的按出现的阶段分了组。每条都写清现象、原因和解决办法你遇到类似症状可以直接对号入座。5.1 枚举阶段设备根本没出现在 sysfs 里现象dmesg 里反复刷mmc1: error -110 whilst initialising SDIO card/sys/bus/sdio/devices/是空的insmod之后 probe 一次都没进。原因-110是 ETIMEDOUT说明 CMD5 发出去没有响应。最常见的是复位脚释放太早或者供电还没稳其次是 32k 低速时钟没给。还有一种情况是bus-width配成 4 位但硬件只接了 1 位命令阶段能过、数据阶段超时。解决先把post-power-on-delay-ms从 50 加到 200 试一次能过说明就是时序问题再慢慢往下调回最优值。然后抓一眼/sys/kernel/debug/mmc1/ios确认clock和bus width是否符合预期。如果 ios 里时钟是 400kHz 一直没上去那是 host 侧识别失败跟驱动无关回去查 pinctrl 和供电。5.2 probe 阶段设备在但驱动匹配不上现象/sys/bus/sdio/devices/mmc1:0001:1存在但dmesg里没有任何demo probe的打印lsmod显示模块已加载。原因SDIO_DEVICE()里的 vendor/device 填错了。这个值来自 CIS和芯片型号、和厂商名字的缩写都没有必然关系凭印象填几乎必错。解决先cat /sys/bus/sdio/devices/mmc1:0001:1/vendor和cat .../device把真实值抄回去。如果设备树里写了compatible demo,demo1000也可以在驱动里加一份of_match_tablesdio_bus_match会依次尝试 id 表、设备树、ACPI 三种匹配方式。查匹配过程可以开echo 8 /proc/sys/kernel/printk看驱动核心的调试输出。5.3 probe 阶段insmod 之后整个系统卡住现象insmod demo_sdio.ko敲下去串口没输出shell 也不响应只能断电重启。原因sdio_claim_host()和sdio_release_host()没有配对。典型写法是中途return -EIO直接返回把 host 永久锁住了或者在已经 claim 的区间里调了sdio_claim_irq()内部又去 claim 一次。解决probe 里所有错误路径统一走 goto 分层释放不要图省事直接 return。sdio_claim_irq()挪到sdio_release_host()之后调用。调试期可以打开内核的CONFIG_PROVE_LOCKING虽然查不出 mmc host 这种自定义锁但能顺手发现别的问题。注意host 被锁死之后重启模块是没用的因为锁在mmc_host结构体里必须重新加载 host 驱动或者直接重启。所以这类 bug 一定要在第一次写的时候就避免。5.4 传输阶段CMD53 报 -84 或 -110现象寄存器读写都正常一旦开始搬 FIFO 就报-84EILSEQ或者-110偶尔能成功一次。原因三个可能。块大小和 FBR 里报的不一致单次请求的块数超过 511传入的缓冲区不是 DMA 可访问的连续内存。-84通常是数据线 CRC 错多半是总线宽度配错或者信号完整性问题-110是没响应多半是参数越界。解决先用demo_read_fifo只读一个块512 字节验证通路通了再往上加。缓冲区改用devm_kmalloc()分配。如果单块也失败把bus-width临时改成 1 位试试1 位能过 4 位不过就是硬件布线或者上拉电阻的问题。5.5 中断阶段中断风暴或者一个中断都收不到现象sdio_claim_irq()返回 0但回调一次都没进或者进了之后疯狂刷屏CPU 占满。原因收不到中断一般是 CCCR 里的IENM/IENx没打开或者设备树的cap-sdio-irq没写。中断风暴则是回调里没清干净状态位——只清了主状态没清子状态中断线一直拉着。解决cap-sdio-irq补上sdio_claim_irq()之后回读SDIO_CCCR_IENx确认。风暴的话把状态寄存器每一位都打出来看看是不是有个位一直置着然后按手册逐位清。有些模块要求先清子状态再清主状态顺序反了也会反复触发。6. 二次开发把这个骨架改成你自己的 SDIO 设备驱动拿到这份代码之后真正要改的地方其实就四处用一张表说清楚比逐行讲更快位置改什么怎么验证demo_ids[]vendor/device 换成你设备 CIS 里的值cat /sys/bus/sdio/devices/*/vendordemo_hw_init()换成你的上电初始化序列probe 日志里加返回值打印DEMO_FIFO_REG等宏换成你模块的寄存器偏移sysfs 属性读一遍对照手册demo_irq()换成你的中断状态位和清除方式用echo 1 /sys/.../irq_test手动触发改完之后最省事的验证手段不是接调试器而是把内核的 mmc 跟踪点打开看每条命令的 opcode、参数和返回值# 看 mmc 子系统的命令级 trace cd /sys/kernel/debug/tracing echo 1 events/mmc/enable echo 1 tracing_on cat trace_pipe | grep -E CMD52 p a hrefhttps://download.csdn.net/download/qcmc123/2698232 stylecolor:#ec7500;font-size:14px; 本文还有配套的精品资源点击获取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p