做嵌入式开发的人,应该都经历过这种让人血压拉满的瞬间:SDIO 初始化一切正常,读操作跑着跑着,程序就死在循环里,要么是 HAL_SD_ReadBlocks 等不到完成标志,要么是 SDIO 状态寄存器里的错误位怎么清都清不掉。ST 社区里那个 “SDIO Gets stuck” 的帖子,我翻来覆去看了很多遍,自己也在一套 STM32F103 + SD 卡方案上复现过这个问题。这一篇就把 SDIO 协议里最容易导致“卡死”的环节、F103 HAL 库的例程逻辑,以及我从波形到寄存器的完整排查思路一次说清楚。
先说结论:SDIO 卡死不是玄学,绝大多数情况是初始化序列、数据线方向切换、时钟分频和超时处理这几件事没做干净。尤其用 STM32F103 的 HAL 库时,官方例程看似能跑,但很多配置项不会主动帮你规避硬件层面的隐患。下面我把整个项目里踩过的坑和最终验证通过的方案完整摊开。
1. 一开始我以为只是没插好——一次SDIO卡死的表象
1.1 现象:时钟有、片选有,CMD就是没响应
那次问题出在一个基于 STM32F103RCT6 的音频播放模块上,SDIO 接的是 TF 卡座,跑的是 4 位 SD 模式。固件用的是 STM32CubeMX 生成、HAL 库 1.8.0 版本。开始很顺利,卡识别、读取 CID、读取扇区都没问题,但是连续读取一段时间后,程序就停在 HAL_SD_ReadBlocks 里的 while 循环里。
用示波器去点 SDIO_CK 和 SDIO_CMD 两根线,现象很奇怪:时钟还在正常翻转,CMD 线上却是一个低电平卡住不动,之后所有命令都发不出去。片选 CS 这种概念在 SDIO 模式里本来就不存在,卡就是用 CMD 和 DATA 线通信,所以所谓“片选有”只是我最初用 SPI 模式的经验惯性,SDIO 模式下根本没有 CS 脚,卡选是通过 CMD7 这种命令来实现的。
后来把逻辑分析仪接到 CMD 线,确认了问题:在 CMD24 写操作之后,卡返回了正常响应,但紧接着我发的 CMD25 或者 CMD18 始终收不到起始位。卡并没有物理损坏,它就是停在忙状态,SDIO 控制器的状态机也跟着缓不过来。
1.2 为什么“卡死”比“读错”更难查
如果 SDIO 只是读出错误数据,问题反而好定位:可能是 CRC、时钟边沿、线上信号完整性。但“卡死”是控制层面的故障,意味着主机和卡之间进入了对方都不打算继续推进的状态。
这里有个关键区别:数据线错误会反映在数据里,而协议级错误会反映在响应超时和状态机的“永远等待”上。HAL 库的阻塞式接口尤其容易放大这个问题,因为它内部用的循环等待没有充分处理错误位。当 SDIO 控制器检测到 CRC error、timeout、start bit error 时,如果软件没有及时处理,FIFO 和状态机就会残留一些脏标志。很多人的“卡死”其实第一次只是超时,但因为标志没清,下一次命令进来依旧被老状态干扰。
我在这个项目里第一次碰到时,第一反应是“卡坏了”,换了一张卡照样卡死,才确定问题在主机侧。这就引出一个排查原则:先排除卡的问题,再考虑协议时序,最后查寄存器状态。顺序不能反,否则很容易在无关方向浪费大量时间。
2. SDIO协议里最容易让你“卡住”的三个环节
2.1 初始化序列:CMD0到ACMD41的时序坑
SDIO 的初始化序列绝对不是简单发几条命令就能完事。标准流程是:CMD0 进入 idle 状态,然后 CMD8 探测卡是否支持 SDHC/SDXC 和电压范围,接着发 ACMD41(带 host capacity support 标志)获取 OCR,再 CMD2 获取 CID,CMD3 获取 RCA,最后 CMD7 选中卡。这串流程里,任何一个命令超时或响应字段不对,都不应该继续往下走。
我在 F103 上遇到的一个隐蔽坑是 ACMD41 的循环次数。HAL 库内部会循环发送 ACMD41,直到卡返回 busy 位清零。但如果卡一直返回 busy,HAL 库会在超时后返回错误,而我的应用逻辑没有对返回值做充分处理,导致后续操作在一个未初始化的卡上执行,现象就是命令永远等不到响应。
另一个坑是 CMD8 的参数。对于 SDHC/SDXC 卡,CMD8 参数是 0x000001AA,表示主机支持 2.7V 到 3.6V 电压。如果卡不支持 CMD8,它不会响应,主机应该把它当作 SDSC 1.x 卡处理,走 CMD1 或老式初始化流程。但 HAL 库的某些版本里,如果 CMD8 超时,它的处理分支并不直观,需要对照代码仔细排查。
2.2 数据线方向切换与CRC:硬件层的老实规矩
SDIO 的数据线是双向的,主机发命令时 CMD 线是主机输出,卡返回响应时 CMD 线会变成卡输出。这个方向切换不是瞬间完成的,总线需要一定的 turnaround time。如果 GPIO 配置成推挽输出模式,同时又没有外部上拉电阻,当主机释放总线准备接收卡响应时,线上电平可能因为寄生电容漏电而不稳定,导致起始位检测失败。
我在设计 PCB 时最初没有在 CMD 和 DATA0-DATA3 上加上拉电阻,结果在低时钟频率下还能正常工作,一旦把 SDIO_CK 提高到 24MHz,卡就开始随机超时。这是 SDIO 硬件层最容易忽略的一个原则:所有 CMD 和 DATA 线都必须配置为复用推挽,并且外部加上拉(通常是 10kΩ 到 3.3V)。SPI 模式可以靠内部上拉救命,但在 SDIO 4 位高速模式下,内部上拉远不够稳定。
CRC 也一样。SDIO 协议中 CMD 和 DATA 都有 CRC 校验。主机发送时可以自动生成,但接收时如果检测到 CRC 错误,控制器会置 CRC fail 标志。不处理这个标志,后续命令状态机可能卡在等待状态。HAL 库的 read/write 函数虽然会检查,但检查得往往不够及时,尤其使用 DMA 传输时,DMA 回调里如果没清标志,下一次传输立刻受影响。
2.3 时钟分频与卡识别:上电时序的隐性要求
SDIO 初始化阶段,时钟不能直接跑到最大频率。规范要求初始化时 SDIO_CK 通常在 400kHz 左右,等卡识别完成后再抬高到传输模式上限。STM32F103 的 SDIO 时钟源来自 PCLK2(通常 72MHz),通过 CLKDIV 分频。初始化阶段如果用默认 2 分频,SDIO_CK 会有 36MHz,这张卡当前未必支持。
HAL 库在 SD_Init 内部会做几次时钟切换,但如果你自己写了底层命令,很容易忽略这个环节。我见过一个项目,初始化阶段直接把 CLKDIV 配置成 2,结果新批次 SD 卡完全不识别,而老批次卡却能跑,就是因为不同卡对上电时钟的容忍度不同。
还有一个隐性要求是上电稳定时间。SDIO 卡从 VDD 上电到接受第一条 CMD0,需要至少 1ms 的稳定时间,部分卡甚至要求 2ms。如果在上电瞬间立刻发 CMD0,卡可能没有准备好,表现为无响应或返回错误。虽然这不完全算 SDIO 控制器的问题,但由主机端发起的复位序列直接影响后续流程,很多“stuck”现象就是这么埋下的。
3. 用STM32F103的HAL库跑SDIO:例程能跑,但坑都在配置里
3.1 官方例程的大致路径
很多人在网上搜“stm32f103的hal库有没有关于sdio的相关例程”,我的经验是:ST 官方并没有针对 F103 单独给出大量独立例程,但 CubeF1 固件包里包含了 SDK 驱动层例程,HAL_SD 模块提供了一整套像 HAL_SD_Init、HAL_SD_ReadBlocks、HAL_SD_WriteBlocks 这样的一层封装。
打开 stm32f1xx_hal_sd.c 源码,你会看到框架内部已经实现了卡上电、初始化、命令发送、状态机管理,理论上我们只需要调用接口即可。但问题也在这层封装上:HAL_SD_Init 内部会调用 SD_PowerON、SD_InitCard、SD_GetCardStatus 等一系列小函数。当其中某一步失败时,返回的错误码只告诉你“初始化失败”,并不会精确到是 CMD0 超时还是 ACMD41 busy 未清。
所以我的建议是:不要只把 HAL 函数当黑盒用。至少要在初始化失败时打印 SD_Handle.Instance->STA 寄存器的值和 RSP 寄存器的内容。响应寄存器和状态寄存器能直接告诉你命令有没有得到卡回应,错误标志是哪个,比单纯看返回值定位问题快得多。
3.2 时钟树与SDIO时钟上限的关联
F103 的 SDIO 挂载在 APB2 总线上,最大允许时钟是 72MHz。SDIO_CK 由 SDIOCLK 分频得到,而 SDIOCLK 一般就是 PCLK2。在实际使用中,4 位模式读写 SDHC 卡时,SDIO_CK 不要超过 25MHz 太保险,虽然卡和主机可能都标称支持更高,但 PCB 走线、卡座弹片、电源噪声都会限制实际频率。
我在项目里使用 PCLK2=72MHz,CLKDIV 配置为 3,得到 SDIO_CK=72/(2+3)=14.4MHz,稳定跑了一年多。如果 CLKDIV 配成 1,SDIO_CK=24MHz,大部分卡也能工作,但布线不好时容易出现偶发 CRC 错误。如果 CLKDIV=0,SDIO_CK=36MHz,很多卡直接不响应,千万别在生产固件里这么用。
时钟树配置里另一个容易忽略的是 SDIO 的时钟源使能。CubeMX 在生成代码时通常会自动开启 SDIO 外设时钟和 DMA 时钟,但如果你手写寄存器初始化,很容易漏掉 RCC->APB2ENR 里的 SDIO 时钟使能位,结果所有寄存器读写都是空操作,现象也是“卡死”。
3.3 引脚复用和上拉电阻的细节
F103 的 SDIO 引脚一般分布在 PC8-PC12,分别是 D0、D1、D2、D3 和 CK,另外还必须有 CMD 引脚 PD2。CubeMX 会自动配好复用功能,但有两个细节它不会帮你处理:一是 GPIO 速度等级,二是外部上拉。
GPIO 速度等级如果设成 Low,在 24MHz 时钟下 SDIO 信号沿会变得很钝,CMD 响应起始位可能识别失败。我习惯在初始化代码里把所有 SDIO 数据线、时钟线和命令线的 GPIO 速度都设为 High,并且确认外部上拉电阻焊接良好。
另外还要确认卡座 CD 引脚。如果你用卡座自带的卡检测引脚,它接到 MCU 上配置为普通 GPIO 输入,需要在软件里做防抖处理。如果不接这个引脚,也可以不用,但要在 HAL_SD_Init 之前屏蔽掉 HAL 层对卡检测的依赖。我遇到过 CubeMX 默认把 CD 引脚映射到一个空闲 GPIO,而实际板子没有连线,导致初始化时检测卡不存在,看起来就是 SDIO 完全不动作。
4. SDIO Gets stuck完整排查链路:从波形到寄存器
4.1 第一站:逻辑分析仪看CMD线
排查 SDIO 卡死,我第一步永远是拿逻辑分析仪抓 CMD 线。注意采样率要足够高,至少是 SDIO_CK 的 4 倍以上,否则命令边沿和起始位会被采坏。我常用 100MHz 采样率抓 24MHz 的 SDIO,很容易看清楚命令的开始和响应。
抓波形时重点看三个东西:命令是否有起始位(CMD 线拉低 1bit),命令内容是否完整,响应是否在 64 个时钟周期内出现。如果响应超时,CMD 线会一直保持高电平,主机也没有收到有效响应。这种状态一般是卡没进入预期状态,或者命令格式错误。
另一种典型波形是:数据线 D0 在写操作后长时间为低,这就是卡 busy。这时主机应该等待 D0 释放再发下一条命令。如果软件没有等待,直接发下一条命令,CMD 线上会只有命令没有响应,反复超时。看到这种波形,基本可以断定问题在“busy 处理”而不是 SDIO 控制器本身。
4.2 第二站:SDIO状态寄存器与超时时间
逻辑分析仪定位到协议层面后,下一步就是读 SDIO 控制器寄存器。F103 的 SDIO 外设有一堆状态位:STA 寄存器里包含 CMDREND、CTIMEOUT、DTIMEOUT、DCRCFAIL、DBCKEND、RXOVERR、TXUNDERR 等标志。卡死的时候,必须第一时间看 STA 值。
我用调试器挂起,读 SDIO->STA,发现常见的卡死状态有三个特征:CTIMEOUT 被置位,说明命令响应超时;RXOVERR 被置位,说明接收 FIFO 溢出;DCRCFAIL 被置位,说明数据 CRC 错误。这三个标志都会让 HAL 库在等待循环里出不去,因为 HAL 库可能在检查完成标志前没有对错误标志做充分处理。
正确做法是:在每次等待完成标志前,先清除所有错误标志,或者等标志判断里包含错误位,一旦出现错误位就跳出循环,返回错误码。我的代码里统一封装了一个SDIO_WaitFlag函数,超时时间设置为 100ms,超时后直接读取 STA 和 RESP 寄存器,以日志形式输出,再返回失败。这么改动之后,程序再没有出现过“死循环式卡死”。
4.3 第三站:卡初始化失败与重试策略
协议波形没问题,状态寄存器也没异常,但卡就是初始化失败,这种情况往往是卡本身的状态问题。我这里说的不是卡坏了,而是上次下电时卡里还残留着未完成的写操作,重新上电后需要更长的恢复时间。
应对策略是在 SDIO 初始化失败时,不要马上报错,而是做至少三到五次重试。每次重试先将 SDIO 模块软件复位(SDIO->POWER |= SDIO_POWER_PWRCTRL 关断再打开),再重新执行完整初始化序列。实测下来,很多偶发初始化失败都能靠这个策略解决。
还有一点要注意:ACMD41 的循环次数不能太少。有些卡在供电不干净时,需要若干次 ACMD41 才能完成内部初始化。HAL 库默认循环次数是 10 次,如果卡慢,10 次可能不够,可以手动改成 100 次,不会对正常卡造成负面影响,但会显著提高识别率。
5. 复现一次典型的CMD8卡死:根因、修复、验证
5.1 根因:电压选择与命令参数不一致
为了彻底说明“SDIO Gets stuck”,我重新搭了一块板子复现。这板子上的配置是:CubeMX 生成代码,手动修改 SDIO 初始化参数,把 CMD8 的参数写成了 0x000000AA 而不是 0x000001AA。理论上这只会影响电压窗口,但如果卡是 SDHC 且需要 2.7-3.6V 窗口,收到 0xAA 的电压匹配时会认为主机不支持,直接不响应。
结果就是:初始化序列卡在 CMD8 上,等不到响应。HAL 库返回超时错误,但卡并没有复位,后续继续发 CMD2、CMD3 也全部超时。表面上看是“SDIO stuck”,本质上却是主机侧的命令参数错误。
很多改动初期的人会在代码里抄一段初始化命令序列,却把 CMD8 参数写错,而卡又恰好对这种命令给出了“无响应”而非“错误响应”,所以表现特别像控制器卡死。这也是我在文章开头说“SDIO 卡死不是玄学”的原因——命令参数不匹配,就能伪装成控制器状态机卡住。
5.2 修复方法
这个问题的修复非常简单:把 CMD8 参数改回0x1AA,同时确认 VDD 电压窗口位为0b0001,表示 2.7V 到 3.6V。随后,代码中的检查逻辑应该这样处理:
if (HAL_SD_Init(&hsd) != HAL_OK) { // 打印 SDIO->STA 和 SDIO->RESPCMD printf("SD init failed, STA=0x%08lX\r\n", hsd.Instance->STA); // 软件复位 SDIO 外设后再重试 __HAL_SD_ENABLE(&hsd); HAL_SD_Init(&hsd); }这里要注意__HAL_SD_ENABLE不能在整个 SDIO 模块还处于错误状态时直接使用,建议先关闭电源控制位,再重新开启,这样可以保证卡重新上电。代码里我是这样做的:SDIO->POWER = 0;延时几毫秒,SDIO->POWER = SDIO_POWER_PWRCTRL_PWRCTRL_1;再延时几毫秒。
5.3 验证结果
修复后,逻辑分析仪抓到 CMD8 发出了 0x1AA,卡在 64 个时钟周期内返回了完整响应,紧接着 ACMD41 也正常完成。连续读写测试跑了 24 小时,没有再出现命令无响应的情况。
同时我把初始化阶段 SDIO_CK 从 400kHz 切换到 14.4MHz 后再运行读写,稳定性也很好。最后我还做了个压力测试:频繁拔插卡并复位 MCU,模拟用户的异常使用场景,结果重试逻辑能让系统每次都恢复正常。这个测试让我意识到,SDIO “卡死”不一定是你写错了代码,也可能是外部环境造成了暂时性状态异常。只要超时和重试机制完善,卡死问题基本可以被“自愈”覆盖掉。
6. 写完驱动之后,我建议你提前做的几件事
6.1 给SDIO加看门狗和超时状态机
很多人写 SDIO 驱动只关注功能,不关注失败路径。一旦 HAL 库内部循环等待时间过长,整个系统就卡死在那里。更可怕的是,如果项目里没有看门狗,系统会以假死状态一直运行,设备永远无法恢复。我给 SDIO 读写接口外面套了一层超时保护,用读取系统 tick 的方式判断超时,而不是依赖 HAL 库内部的无限等待。
代码大致思路:
uint32_t tick = HAL_GetTick(); while (HAL_SD_GetCardState(&hsd) != HAL_SD_CARD_TRANSFER) { if ((HAL_GetTick() - tick) > 1000) { // SDIO 错误恢复流程 SD_DeInit(); SD_Init(); return SDIO_ERR_TIMEOUT; } }这样即使 HAL 层出现问题,外层也能主动退出,而不是永久阻塞。再加上 MCU 独立看门狗,即使外层超时也没及时生效,复位也能兜底。我后来所有带 SDIO 的项目,默认都会加上这套机制。
6.2 用FATFS或裸机读写验证稳定性
SDIO 初始化通了,不代表整个链路稳定。我建议你用一个简单的扇区读写测试:先写 0xAA 和 0x55 交替模式的随机数据,再读出来比对。如果只是循环读同一块数据,很容易漏掉 DMA 缓冲区和 FIFO 溢出的问题。
我记得有一次调 DMA 读写,读取数据时偶发出错,排查了很久才发现是 DMA 配置里外设地址没有按 4 字节对齐,而缓存区又是局部变量,导致 DMA 搬运时访问越界。后来把缓冲区改到全局数组并做 32 位对齐,问题直接消失。
如果你想验证 SD 卡协议栈层面的稳定性,建议接一个文件系统。FATFS 这种文件系统会混合使用 CMD17/CMD18/CMD24/CMD25,覆盖更广的命令路径。我这里的小技巧是每次打开文件和写文件后强制f_sync,把内部缓存刷到卡上,能提前暴露出一些写路径上的问题。
6.3 几个容易被忽略的经验
第一,不要在所有 SDIO 引脚上随意加陶瓷电容滤波。SDIO 信号线上加电容会让信号边沿变缓,严重时直接导致卡无法识别。电源退耦电容要加,信号线上的电容尽量不加。
第二,卡座的选择会影响稳定性。那种带自弹锁紧的卡座,接触可靠性通常比直插式推入的卡座好。曾经有一个项目因为卡座弹片氧化,SDIO D1 线间歇性接触不良,表现出来就是读取卡死,换了一个镀金触点卡座后问题消失。
第三,一定要保留调试日志输出。在初始化每个阶段都打印关键寄存器和返回值,别嫌麻烦。我见过太多同事因为懒得加日志,结果每次定位 SDIO 问题都靠猜。你只要把 SDIO->STA、SDIO->RESP 等寄存器值打出来,很多问题一眼就能看出是命令超时、CRC 错误还是 FIFO 溢出。
正是这些细节,决定了你的 SDIO 是“偶尔卡死”还是“长期稳定”。我的体会是,SDIO 的难点不在协议的复杂度,而在你对失败路径的态度。只要你愿意在每次命令后多看几眼状态寄存器,多留一个超时出口,那个 “Gets stuck” 的标题,最后只会出现在别人的帖子里。