
1. 从SPI到SD卡为什么这个组合在嵌入式领域经久不衰在嵌入式项目里需要存储点数据是再常见不过的需求。从记录传感器日志、保存设备配置到存储一些简单的用户界面资源一个可靠、低成本、容量又够用的存储方案总是绕不开的。这时候SD卡包括Micro SD卡就成了很多工程师的首选。它便宜、容量大、物理尺寸小而且几乎人人都用过生态成熟。但是怎么让咱们手头的单片机比如STM32去跟这张小卡片“对话”呢最直接的想法可能是SDIO接口它速度快是SD卡的原生协议。但对于很多成本敏感、或者引脚资源紧张的项目尤其是那些只用到了STM32F1、F0这类基础系列芯片的情况专门去接一个SDIO可能有点“杀鸡用牛刀”不仅硬件布线复杂软件驱动也相对厚重。这时候SPI模式就闪亮登场了。几乎所有的STM32芯片都内置了多个SPI外设而SD卡协议在设计之初就考虑到了兼容性特意支持了SPI模式。这就意味着你只需要3根线MOSI, MISO, SCK加上一根片选线CS就能让STM32和SD卡建立起通信桥梁极大简化了硬件连接和软件驱动复杂度。我做过不少需要离线数据记录的项目比如环境监测仪、简单的数据采集器SPI接SD卡几乎是标配方案。它的优势在于“够用且省心”速度对于日志记录绰绰有余接线简单飞线调试都方便相关的开源驱动和代码示例非常丰富踩坑了也容易找到解决方案。当然它也有局限比如理论速度不如SDIO但在实际应用中尤其是单片机本身处理能力有限的情况下SPI模式的瓶颈往往不在总线速度上。这篇文章我就结合自己多次在STM32上折腾SPI-SD卡的经验把从硬件连接到软件驱动再到文件系统挂载和实际读写的完整流程以及里面那些容易让人栽跟头的细节给你彻底捋清楚。2. 硬件连接与初始化确保物理层通信的绝对可靠一切软件操作的前提是硬件链路必须正确无误。SPI方式读写SD卡硬件上看似简单但每个细节都关乎后续调试的难易程度。2.1 引脚连接与电源设计首先明确连接关系。SD卡在SPI模式下我们主要关心以下几个引脚CS (Card Select)片选信号低电平有效。连接到STM32的一个GPIO口。DI (Data In)对应SD卡的命令/数据输入引脚。在SPI模式下这就是主设备输出、从设备输入MOSI连接STM32的SPI_MOSI引脚。DO (Data Out)对应SD卡的数据输出引脚。在SPI模式下这就是主设备输入、从设备输出MISO连接STM32的SPI_MISO引脚。SCLK (Serial Clock)时钟信号由STM32的SPI主设备产生连接STM32的SPI_SCK引脚。VDD / VSS电源和地。这是最容易出问题的地方之一。注意SD卡还有两个引脚DAT1和DAT2。在SPI模式下这两个引脚通常可以悬空或者内部上拉但有些资料建议将DAT1上拉以确保某些SD卡在SPI模式下的稳定性。最稳妥的做法是查阅你所使用SD卡模组的原理图。对于电源必须保证稳定和干净。SD卡在工作时尤其是进行写操作时瞬时电流可能达到几十甚至上百毫安。如果直接使用STM32开发板上的3.3V引脚通常由LDO线性稳压器提供务必确认该LDO的额定电流足够并且电源走线足够宽旁路电容104或106要紧靠SD卡座放置。我在早期项目中曾因忽略这点导致在连续写入数据时系统复位排查了半天才发现是电源被拉垮了。建议为SD卡单独使用一个性能良好的LDO或者确保主电源有充足的余量。2.2 SPI外设配置要点STM32的SPI配置需要与SD卡在SPI模式下的时序要求匹配。SD卡的SPI模式通信基于模式0CPOL0 CPHA0或模式3CPOL1 CPHA1。绝大多数驱动和SD卡都兼容模式0所以我们通常采用此模式。配置SPI时有以下几个关键参数波特率预设值BaudRatePrescaler在初始化阶段SD卡处于默认的低速模式此时SPI时钟不能太快通常要低于400kHz。许多驱动代码会先将分频值设得很大如256分频在初始化完成后再切换到高速模式如4分频或2分频。切忌一上来就用高速时钟否则SD卡可能无法响应。数据大小DataSize固定为8位。时钟极性CPOL与相位CPHA如前所述设为0。片选管理NSS建议将硬件NSSSS引脚设置为软件管理Soft NSS。即我们用一个普通的GPIO口来手动控制CS引脚的电平这样时序控制更灵活。下面是一个典型的SPI初始化代码片段以HAL库为例SPI_HandleTypeDef hspi1; void SPI1_Init(void) { hspi1.Instance SPI1; hspi1.Init.Mode SPI_MODE_MASTER; hspi1.Init.Direction SPI_DIRECTION_2LINES; // 全双工 hspi1.Init.DataSize SPI_DATASIZE_8BIT; hspi1.Init.CLKPolarity SPI_POLARITY_LOW; // CPOL 0 hspi1.Init.CLKPhase SPI_PHASE_1EDGE; // CPHA 0 hspi1.Init.NSS SPI_NSS_SOFT; // 软件控制NSS hspi1.Init.BaudRatePrescaler SPI_BAUDRATEPRESCALER_256; // 初始化低速 hspi1.Init.FirstBit SPI_FIRSTBIT_MSB; hspi1.Init.TIMode SPI_TIMODE_DISABLE; hspi1.Init.CRCCalculation SPI_CRCCALCULATION_DISABLE; hspi1.Init.CRCPolynomial 10; if (HAL_SPI_Init(hspi1) ! HAL_OK) { Error_Handler(); } }初始化SPI后别忘了初始化用于控制SD卡片选CS的GPIO口并将其设置为高电平无效状态。3. SD卡底层驱动理解命令与响应的对话机制硬件通路建立后我们就要开始和SD卡“对话”了。这个对话遵循一套严格的命令-响应协议。你需要忘记文件、目录这些高级概念此刻我们面对的是一个需要通过特定命令字CMD来操作的存储块设备。3.1 命令帧结构与发送SD卡的命令是一个6字节的帧结构字节1命令号如CMD0是0x40 起始位总是1 传输位总是0。所以实际发送的是0x40 | (cmd 0x3F)。例如CMD00x00发送的第一个字节就是0x40。字节2-5命令参数32位大端序。例如CMD16设置块大小的参数就是你希望的块大小如512。字节67位CRC校验值 停止位总是1。但在SPI模式下CRC校验是可以被禁用的默认禁用对于大多数命令除CMD0外我们可以直接发送0xFF或一个固定的CRC值如0x95用于CMD00x87用于CMD8。很多简化驱动对非CMD0的命令都直接送0xFF。发送命令前必须确保CS线被拉低选中SD卡并在命令帧的每个字节之间、以及命令发送完成后持续提供时钟脉冲。发送函数通常需要处理这些细节。3.2 响应格式与超时处理发送命令后SD卡会返回一个或多个字节的响应。常见的响应类型有R11字节、R1b类似R1后跟忙信号、R3/R7针对CMD8和ACMD41的响应包含更多信息。以最常见的R1响应为例它是一个字节每一位代表不同的状态位7总是0。位6参数错误。位5地址错误。位4擦除序列错误。位3CRC错误。位2非法命令。位1擦除复位。位0空闲状态IDLE。正确的响应处理逻辑是发送命令后连续读取SPI数据直到读到的字节最高位为0即不再是0xFF。这个等待过程必须有超时机制。我曾经遇到过因为SD卡响应慢而驱动代码超时时间设置过短导致初始化一直失败的情况。一个健壮的驱动对于CMD0这样的复位命令超时时间可以设长一些比如几百毫秒。uint8_t SD_SendCmd(uint8_t cmd, uint32_t arg, uint8_t crc) { uint8_t r1; uint8_t buf[6]; uint32_t timeout 0x0FFF; // 超时计数器 // 构造命令帧 buf[0] 0x40 | (cmd 0x3F); // 起始位命令 buf[1] (uint8_t)(arg 24); buf[2] (uint8_t)(arg 16); buf[3] (uint8_t)(arg 8); buf[4] (uint8_t)(arg); buf[5] crc; // 发送命令前先发送至少8个时钟周期通常通过发送0xFF实现并确保CS为低 SD_CS_LOW(); SPI_WriteByte(0xFF); // 额外时钟 // 发送命令帧 for(int i0; i6; i) { SPI_WriteByte(buf[i]); } // 等待响应直到非0xFF或超时 do { r1 SPI_ReadByte(); timeout--; } while((r1 0xFF) timeout); return r1; }3.3 初始化流程CMD0, CMD8, ACMD41, CMD58这是整个驱动中最关键、也最容易出错的一环。SD卡上电后默认处于SD总线模式。我们需要通过一系列命令将其切换到SPI模式并识别卡的类型V1标准卡、V2标准卡、高容量HC卡。标准初始化序列如下上电延时与时钟同步SD卡上电后需要至少等待74个时钟周期以上才能接收第一个命令。通常的做法是在初始化函数开始时将CS拉高不选中然后通过SPI连续发送超过74个0xFF即提供时钟脉冲。CMD0 (GO_IDLE_STATE)发送CMD0参数0CRC 0x95让SD卡进入空闲状态。这是切换到SPI模式的第一步。此时CS必须为低电平。成功后应收到R1响应值为0x01IDLE状态位被置位。CMD8 (SEND_IF_COND)这是一个V2版本引入的“探针”命令用于检查电压兼容性。参数的低12位可以设置你期望的电压如0x1AA表示2.7-3.6V。如果卡支持V2它会回送一个R7响应包含你发送的参数和电压信息。如果卡是V1或MMC卡它会返回“非法命令”错误R1的位2为1。这个命令的响应决定了后续的初始化路径。ACMD41 (SD_SEND_OP_COND)这是一个应用特定命令APP_CMD用于发送操作条件并等待卡退出空闲状态。发送ACMD41前必须先发送CMD55APP_CMD来告知SD卡下一个命令是应用命令。对于检测到是V2的卡CMD8有正确响应ACMD41的参数需要将HCSHigh Capacity Support位置位0x40000000以询问卡是否支持高容量SDHC/SDXC。对于V1卡或CMD8返回非法命令的卡ACMD41参数通常为0。需要循环发送CMD55ACMD41直到ACMD41的响应值变为0x00退出空闲状态。这个过程必须要有超时处理并且超时时间要足够长几秒钟因为卡需要时间进行内部初始化。CMD58 (READ_OCR)读取OCR操作条件寄存器主要是为了确认卡是否支持3.3V电压OCR的位15-23以及对于V2卡确认其是否为高容量卡OCR的位30CCS。如果CCS位为1则是SDHC/SDXC卡寻址方式为块寻址每块512字节如果为0则是标准容量SD卡SDSC寻址方式为字节寻址。整个初始化流程可以用下面的伪代码逻辑表示SD_Init() { // 1. 硬件初始化GPIO, SPI低速 // 2. 发送至少74个时钟脉冲CS高写0xFF // 3. 拉低CS发送CMD0进入SPI模式 // 4. 发送CMD8判断卡版本 // 5. 根据卡版本循环发送CMD55ACMD41带相应参数直到卡就绪 // 6. 发送CMD58读取OCR确认电压和卡类型SDSC or SDHC/SDXC // 7. 初始化成功可以切换SPI到高速模式 // 8. 发送CMD16如果需要设置块大小对于SDSC卡此命令有效SDHC/SDXC卡块大小固定为512 }实操心得很多开源驱动初始化失败问题就出在ACMD41的循环等待上。要么是超时时间太短卡还没初始化完就退出了要么是没有正确处理CMD55ACMD41的序列漏发了CMD55。务必在调试时通过调试器或串口打印出每一步命令的响应值这是定位问题的关键。4. 数据读写与擦除掌握块设备的操作核心初始化成功后我们就可以对SD卡进行最基本的块读写和擦除了。这是文件系统得以构建的基础。4.1 单块与多块读写CMD17, CMD18, CMD24, CMD25SD卡以块Block为单位进行读写。对于标准容量卡SDSC块大小可以通过CMD16设置通常为512字节。对于高容量卡SDHC/SDXC块大小固定为512字节且CMD16无效。CMD17 (READ_SINGLE_BLOCK)读取单个块。参数是块地址对于SDSC是字节地址对于SDHC是块地址。发送命令后SD卡会先返回一个数据响应令牌0xFE紧接着是512字节的数据块最后是2字节的CRCSPI模式下通常忽略。关键点在于发送CMD17后必须持续读取SPI数据直到读到起始令牌0xFE才能开始接收数据。如果读到的不是0xFE可能是命令错误或地址错误。CMD24 (WRITE_BLOCK)写入单个块。参数是块地址。发送命令后需要先发送一个起始令牌0xFE然后紧接着发送512字节的数据最后发送2字节的CRC可写0xFF。SD卡接收数据后会返回一个数据响应令牌并通过DO线保持低电平忙状态直到内部编程完成。写操作后必须等待忙状态结束否则后续操作可能失败。等待忙状态的方法是持续读取SPI的一个字节直到该字节为0xFF非忙。CMD18/CMD25用于多块连续读/写原理类似但需要以CMD12STOP_TRANSMISSION命令来终止传输。在简单的文件系统如FATFS中单块读写已经足够多块读写可以用于优化性能。这里有一个写单块的示例流程SD_WriteBlock(uint32_t blockAddr, const uint8_t *dataBuf) { uint8_t resp; // 1. 发送CMD24 resp SD_SendCmd(CMD24, blockAddr, 0xFF); if(resp ! 0x00) return SD_ERROR; // 2. 发送数据起始令牌 SPI_WriteByte(0xFE); // 3. 发送512字节数据 for(int i0; i512; i) { SPI_WriteByte(dataBuf[i]); } // 4. 发送2字节CRC可忽略 SPI_WriteByte(0xFF); SPI_WriteByte(0xFF); // 5. 读取数据响应令牌格式0bxxx0status1 resp SPI_ReadByte(); if((resp 0x1F) ! 0x05) { // 0x05表示数据被接受 return SD_ERROR; } // 6. 等待写操作完成忙等待 while(SPI_ReadByte() 0x00); // 等待DO线变高0xFF return SD_OK; }4.2 擦除操作CMD32, CMD33, CMD38SD卡支持擦除一组连续的块。这对于文件系统删除文件、格式化等操作很有用。擦除操作分为三步CMD32 (ERASE_WR_BLK_START)设置擦除起始块地址。CMD33 (ERASE_WR_BLK_END)设置擦除结束块地址。CMD38 (ERASE)执行擦除。擦除操作是物理擦除耗时较长且不可逆。在执行CMD38后同样需要等待忙状态结束。有些文件系统如FATFS的disk_ioctl函数中的CTRL_ERASE_SECTOR命令会调用底层的擦除函数来提升性能。4.3 时钟提速与性能考量初始化阶段完成后SD卡已经准备就绪此时可以将SPI的时钟频率提升到最高允许值以提高读写速度。SD卡在SPI模式下的最高时钟频率理论上可达25MHz对于某些卡可能更高。你可以将SPI的预分频器从初始化时的SPI_BAUDRATEPRESCALER_256切换到SPI_BAUDRATEPRESCALER_4甚至SPI_BAUDRATEPRESCALER_2。但是提速前有两点必须注意硬件兼容性过高的速率可能导致信号完整性变差尤其是连接线较长或未阻抗匹配时。如果提速后出现读写错误应首先怀疑硬件问题可以尝试降低速率或改善布线。SD卡本身的支持并非所有SD卡都支持最高速率。稳妥的做法是逐步提高速率并测试稳定性。在我的经验中对于大多数现代Micro SD卡在STM32F1系列上使用SPI2最高36MHz系统时钟分频达到18MHz的SCK频率通常是稳定可靠的。性能的另一个瓶颈在于软件实现。使用查询方式Polling进行SPI收发每个字节的读写都需要CPU轮询状态寄存器效率较低。如果可能应启用SPI的DMA传输尤其是在进行多块连续读写或文件系统操作时DMA能极大解放CPU提升整体吞吐量。许多成熟的SD卡驱动库如FatFs的底层驱动都提供了DMA支持选项。5. 集成FatFs文件系统从块设备到文件操作直接操作扇区对于应用来说太原始了。我们需要一个文件系统来管理文件和目录。FatFs是一个为小型嵌入式系统设计的通用FAT文件系统模块它独立于底层存储介质和平台完美契合我们的需求。我们的任务就是为FatFs实现磁盘I/O接口。5.1 FatFs模块介绍与移植FatFs模块由ff.c、ff.h、ffconf.h、diskio.c、diskio.h这几个核心文件组成。其中diskio.c是需要我们根据具体硬件STM32 SPI SD卡来实现的底层驱动接口。FatFs通过一个名为disk_ioctl的结构体实际上是函数指针集合来抽象磁盘操作。我们需要实现diskio.c中的以下几个函数disk_initialize初始化磁盘驱动对应我们之前写的SD_Init。disk_status获取磁盘状态是否初始化、是否写保护等。disk_read读取一个或多个扇区。disk_write写入一个或多个扇区。disk_ioctl控制函数用于获取磁盘信息扇区大小、扇区数量、刷新缓存、控制电源等。5.2 diskio.c 接口实现详解实现的核心是disk_read和disk_write函数。FatFs以扇区Sector为单位调用它们而我们的SD卡驱动以块Block为单位。幸运的是在SD卡驱动中我们通常将块大小设置为512字节这与FAT文件系统标准的扇区大小一致所以可以一一对应。// diskio.c 中的关键函数实现示例 DSTATUS disk_initialize (BYTE pdrv) { if(pdrv ! 0) return STA_NOINIT; // 我们只支持一个磁盘 if(SD_Init() SD_OK) { return 0; // 初始化成功 } else { return STA_NOINIT; } } DRESULT disk_read (BYTE pdrv, BYTE *buff, LBA_t sector, UINT count) { for(UINT i 0; i count; i) { if(SD_ReadBlock(sector i, buff i * 512) ! SD_OK) { return RES_ERROR; } } return RES_OK; } DRESULT disk_write (BYTE pdrv, const BYTE *buff, LBA_t sector, UINT count) { for(UINT i 0; i count; i) { if(SD_WriteBlock(sector i, buff i * 512) ! SD_OK) { return RES_ERROR; } } return RES_OK; } DRESULT disk_ioctl (BYTE pdrv, BYTE cmd, void *buff) { switch(cmd) { case GET_SECTOR_SIZE: *(WORD*)buff 512; // 扇区大小固定512字节 break; case GET_SECTOR_COUNT: { // 这里需要调用SD_GetCapacity函数通过CMD9或CMD10获取CSD寄存器计算 DWORD cap SD_GetCapacity(); *(DWORD*)buff cap; break; } case CTRL_SYNC: // 对于SD卡写操作后我们已经等待了忙状态这里可以直接返回OK break; default: return RES_PARERR; } return RES_OK; }disk_ioctl中的GET_SECTOR_COUNT是实现的关键。你需要通过SD卡命令通常是CMD9发送CSD寄存器或CMD10发送CID寄存器来获取卡的实际容量信息并计算出总扇区数。对于SDSC卡容量计算稍微复杂需要解析CSD寄存器中的C_SIZE,C_SIZE_MULT,READ_BL_LEN等字段。对于SDHC/SDXC卡计算就简单多了扇区数 (C_SIZE 1) * 1024其中C_SIZE来自CSD寄存器。网上有现成的CSD解析代码可以直接参考使用。5.3 文件操作示例与常见问题移植好FatFs后你就可以使用标准的f_open,f_read,f_write,f_close,f_mkfs等函数了。FATFS fs; FIL file; UINT bw; char buffer[] Hello, STM32 and SD Card!\n; // 挂载文件系统 f_mount(fs, 0:, 1); // 0: 对应 disk_initialize 中的 pdrv0 // 打开文件如果不存在则创建 f_open(file, 0:/test.txt, FA_WRITE | FA_CREATE_ALWAYS); // 写入数据 f_write(file, buffer, strlen(buffer), bw); // 关闭文件 f_close(file); // 重新打开并读取 f_open(file, 0:/test.txt, FA_READ); f_read(file, buffer, sizeof(buffer), bw); buffer[bw] \0; // 添加字符串结束符 printf(Read: %s, buffer); f_close(file);集成FatFs后最常见的几个坑f_mount失败返回FR_NO_FILESYSTEM这通常表示SD卡上没有有效的FAT分区。你需要先用f_mkfs函数格式化SD卡。注意格式化会清空所有数据可以在电脑上格式化为FAT32格式或者通过代码调用f_mkfs(“0:”, FM_FAT32, 0, workBuffer, sizeof(workBuffer))。读写文件时数据错乱或丢失首先检查disk_read/disk_write函数中的地址计算是否正确确保扇区号 * 512后没有溢出。其次检查SPI的读写函数是否在传输过程中被中断打断。如果使用了RTOS确保对SD卡底层驱动的访问特别是disk_read/disk_write是线程安全的通常需要加锁。长时间写操作后文件系统损坏这很可能是因为没有正确处理写缓存。FatFs在内部有缓存机制。在安全移除SD卡或发生意外断电前必须调用f_sync(file)函数将文件的缓存数据强制写入磁盘。对于整个卷可以调用f_mount(NULL, “0:”, 0)来卸载卸载过程也会同步缓存。多任务访问冲突如果多个任务线程同时操作文件系统必须使用FatFs提供的重入Re-entrancy控制或者使用信号量等机制在应用层保证同一时间只有一个任务调用FatFs的API。6. 调试技巧与稳定性优化实战把代码跑通只是第一步让它在各种环境下稳定可靠地运行才是真正的挑战。下面分享几个我踩过坑后总结的调试和优化经验。6.1 利用调试器与串口日志定位问题当SD卡初始化或读写失败时最有效的调试手段是打印关键节点的状态。命令响应值在SD_SendCmd函数中将每个命令发送后收到的R1响应值通过串口打印出来。对照SD卡规范可以精确判断是哪个环节出了问题例如响应0x05表示参数错误0x01表示卡处于空闲状态等。SPI数据流在读写数据时可以打印出起始令牌0xFE、数据响应令牌等关键字节。有时SPI时序稍有偏差就可能读不到正确的起始令牌。超时计数在等待响应或等待忙状态的循环中设置一个计数器超时后将计数器值打印出来可以帮助你判断是卡根本没有响应还是响应太慢。6.2 电源与信号完整性的深度排查很多间歇性故障尤其是高频率下出现的读写错误根源都在硬件。电源纹波测量在SD卡进行写操作时用示波器测量其VDD引脚上的电压。如果看到电压有明显跌落例如从3.3V跌到3.0V以下说明电源驱动能力不足或旁路电容不够。解决方法加大电源滤波电容在SD卡VDD和GND之间并联一个10uF的钽电容和一个100nF的陶瓷电容或者使用电流能力更强的LDO。SPI信号质量观察用示波器观察SCK、MOSI、MISO线上的信号。重点看上升/下降沿是否陡峭缓慢的边沿容易导致时序错误。是否存在过冲或振铃过长的走线或阻抗不匹配会引起信号反射。可以在信号线上串联一个22-33欧姆的小电阻进行阻尼。时钟频率是否准确确保STM32的SPI时钟配置正确。上拉电阻SD卡的DOMISO线在SPI模式下是开漏输出必须接一个上拉电阻通常10kΩ到VDD否则可能无法正确输出高电平。CS、DIMOSI、SCK线也建议加上拉以提高抗干扰能力。6.3 软件层面的鲁棒性增强错误重试机制在任何一次SD卡命令或数据读写失败后不要立即放弃。可以实现一个简单的重试逻辑例如重试3次每次重试前稍微延时。这对于应对偶尔的通信干扰非常有效。热插拔检测如果你的硬件支持SD卡热插拔通常通过卡座的检测引脚可以在软件中定期检查卡是否存在。当检测到卡被拔出时及时调用f_mount(NULL, …)卸载文件系统当卡重新插入时重新初始化并挂载。注意FatFs本身不直接支持热插拔需要在应用层管理。写平衡考虑对于需要频繁写入小文件的系统如数据日志频繁擦写同一个FAT表或目录项扇区可能导致该存储区块提前损坏。虽然SD卡内部有磨损均衡算法但为了更长的寿命可以考虑在应用层做简单的写平衡例如定期轮换日志文件或者使用更高级的、支持磨损均衡的文件系统如LittleFS不过LittleFS在SPI接口上的性能需要评估。7. 进阶话题从SPI切换到SDIO的性能考量最后简单聊聊什么时候该考虑放弃SPI转而使用SDIO。SPI模式简单稳定但其半双工和相对较低的速度是硬伤。SDIO使用4位或8位并行数据总线是全双工通信理论带宽远超SPI。考虑使用SDIO的场景需要高速连续读写例如存储摄像头采集的图片、音频数据或者充当系统的启动介质。项目使用了支持SDIO的高性能STM32系列如STM32F4, F7, H7等其SDIO外设性能强大不用可惜。硬件引脚资源不紧张SDIO需要更多的引脚CLK, CMD, D0-D3。切换到SDIO的代价硬件连接复杂引脚更多对PCB布线要求更高需要注意信号线等长。驱动复杂度增加STM32的SDIO外设配置比SPI复杂需要处理DMA、数据FIFO、命令响应等。功耗可能略高并行总线活动时功耗通常高于SPI。对于绝大多数中小型嵌入式应用——数据记录、参数存储、固件更新——SPI模式下的SD卡性能已经完全足够。它的简单、可靠和低资源占用是其最大的优势。只有当你的数据吞吐量成为系统瓶颈时才值得去挑战SDIO带来的复杂性。从我个人的项目经验来看十次里有九次SPISD卡的方案都是那个“刚刚好”的选择。