资讯详情

资讯详情

ARM嵌入式SD卡驱动实战:从协议机制到文件系统移植

搞过ARM嵌入式开发的朋友十有八九都会跟SD卡打交道。这玩意儿看着不起眼插上就能存数据但真要在嵌入式项目里把它跑稳、跑快、跑可靠里面门道其实不少。网上关于SD卡的资料很杂要么是纯理论协议分析要么是某个开发板的例程搬运很少有把协议要点、驱动编写和实际调试串起来讲的。这篇文章我想把我学习ARM嵌入式过程中围绕SD卡这块的笔记和踩坑记录整理出来从协议基础到初始化流程从数据读写到文件系统移植再到实际项目中遇到的各种问题一次性说透。如果你正在学STM32或者类似的ARM平台想搞懂SD卡到底是怎么工作的或者项目中正被SD卡读写不稳定、速度上不去这些问题困扰这篇笔记应该能帮上忙。1. 基础认知SD卡在ARM系统里的定位与核心概念在深入代码之前先把SD卡在嵌入式系统里的角色理清楚。这很重要因为很多问题其实是概念没搞清导致的。1.1 从物理接口到存储介质SD卡本质是什么SD卡全称Secure Digital Memory Card是一种基于NAND Flash的存储设备。它在ARM系统里扮演的角色是外部非易失性存储器用来存放需要掉电保存的数据日志文件、配置文件、采集到的传感器数据、固件升级包等。相比芯片内部FlashSD卡容量大从几十MB到上百GB都有、可插拔、成本低而且可以方便地把数据拿到PC上分析所以成了嵌入式设备的标配外设。从硬件架构上看SD卡内部其实是一个小型的嵌入式系统——它自己也有控制器负责管理Flash的读写擦除、坏块管理、磨损均衡、ECC校验这些脏活累活。ARM处理器并不直接操作Flash的物理地址而是通过SD协议向卡内的控制器发命令。打个比方ARM像是个甲方把“我要读地址0x12345678处的数据”这个需求发给SD卡SD卡内部的控制器接到需求后再去指挥Flash干活然后把结果返回给甲方。这个“甲方乙方”的关系理解了后面看命令交互就好懂了。1.2 三种操作模式SD、SPI与SDIOARM处理器和SD卡通信主要有三种方式SD模式也叫SD总线模式使用CLK、CMD、DAT0-DAT3共6根线支持1位或4位并行传输。4位模式下理论吞吐率最高是性能首选。STM32的SDIO接口跑的就是这个模式。SPI模式用CS、CLK、MOSI、MISO四根线实现简单很多不带SDIO外设的芯片比如某些低端MCU通过GPIO模拟SPI也能驱动SD卡。代价是吞吐率低、没有硬件CRC校验保障适合对速度要求不高的场景。SDIO模式严格说SDIO是MMC/SD规范中定义的一种总线接口标准在ARM芯片里作为外设控制器存在通过它操作SD卡就是SD模式。实际做项目时如果芯片自带SDIO外设STM32F1系列、F4系列、H7系列基本都有优先用SD模式如果芯片没有SDIO外设或者引脚被占用太多就退而求其次用SPI模式。我早年做过一个低功耗采集节点主控是入门级Cortex-M0芯片没有SDIO控制器就是用SPI模式驱动的SD卡虽然写速度只有几百KB/s但存定期上传的温湿度数据绰绰有余。1.3 SD卡规范演进SDSC、SDHC、SDXC的区别整个SD家族按容量和规范版本分了三代对驱动编写有直接影响类型全称容量范围文件系统地址单位SDSCStandard Capacity最大2GBFAT12/FAT16字节地址SDHCHigh Capacity2GB - 32GBFAT32块地址512B/块SDXCeXtended Capacity32GB - 2TBexFAT块地址512B/块关键点在这里SDSC用字节地址SDHC/SDXC用块地址每块512字节也就是说访问SDHC时命令参数里的地址是“扇区号”不是“字节偏移量”。很多新手第一次调SDHC卡在CMD24写单块命令里填的实际字节地址结果卡半天没反应就是这个原因。还有一个隐藏知识点CMD8命令就是用来区分SDSC和SDHC的。初始化流程里发CMD8如果卡响应了0x1AA表示支持2.7V-3.6V电压范围且支持SDHC/SDXC那才能继续发ACMD41带HCS位来切换到高容量模式。不支持CMD8的老卡只能按SDSC处理。这个细节后面初始化的章节还会展开。2. SD总线协议核心命令、响应与数据传输SD协议本质上是一个命令-响应的主从式协议。ARM作为主机SD卡作为从机主机发命令从机回响应然后根据命令类型决定是否传输数据。先把这三要素搞明白驱动逻辑就是顺水推舟的事。2.1 命令格式48位的约定SD总线上传输的每一条命令都是48位固定格式如下起始位1位固定为0传输位1位固定为1表示方向是主机到卡命令索引6位CMD0到CMD63参数32位具体含义因命令而异可能是地址、电压范围、块数量等CRC77位校验除起始位和传输位外的45位结束位1位固定为1CRC7是硬件自动生成的在SDIO外设里由硬件电路负责不需要软件去算。但在SPI模式下CRC7是强制要求为合法值的SDIO模式对某些命令不检查CRC所以用SPI模式时CRC计算往往要软件实现或者直接参考官方SD卡初始化代码里的查表算法。2.2 常用命令速查表驱动编写中最常用的命令就这么几个建议背下来命令索引参数响应类型用途CMD000x00000000R1复位卡进入空闲态CMD880x000001AAR7发送电压范围查询检测卡类型CMD55550x00000000R1前缀命令通知下一条是应用命令ACMD41410x40000000R1初始化卡HCS位置1开启高容量CMD220x00000000R2读取卡CID寄存器CMD330x00000000R6读取卡相对地址RCACMD77RCA16R1选中/取消选中卡CMD1717扇区号R1读取单块数据CMD1818起始扇区号R1连续读取多块CMD2424扇区号R1写入单块数据CMD2525起始扇区号R1连续写入多块CMD12120x00000000R1b停止多块传输CMD1313RCA16R2读卡状态寄存器2.3 响应类型读懂卡在说什么SD卡会返回多种响应不同命令对应不同响应类型R1常规响应长度48位包含卡当前状态。每一位代表一个错误标志比如参数错误、地址错误、CRC错误、擦除失败等。初始化过程里最常打交道的响应。R2CID或CSD寄存器内容136位比普通响应长由发送命令的设备和卡共同组成。读取卡容量、制造商信息就靠它。R3OCR寄存器内容48位在ACMD41初始化时返回反映卡电压范围和初始化是否完成。最高位bit31为1表示卡上电完成可以开始寻卡了。R6RCA响应48位在CMD3之后返回包含卡的相对地址。R7CMD8专用响应48位回显主机发的电压范围和检查模式。如果低12位等于0x1AA说明卡支持SDHC。这些响应里R1最常用也最容易忽略细节。比如初始化时发CMD55后再发ACMD41如果R1的bit0是1说明卡还在初始化中busy需要继续轮询如果bit0是0说明初始化完成。判断逻辑对了才不会陷入死循环。2.4 数据传输块是基本单位在SD模式下数据传输以块block为单位每块固定512字节也可以在CSD里查询但实际通用卡就是512。读数据时卡准备好后会在DAT线上一帧一帧地往外发起始位、数据、CRC16、结束位。写数据时主机除了发数据和CRC16还要在卡侧等待忙信号DAT0拉低表示卡忙于写Flash主机必须等它拉高才能继续。多块读写的性能优势非常明显。单块读的命令开销是固定的命令响应传输时间但连续读的时候卡内部可以预读prefetch把连续的数据提前搬到缓冲区流水线作业。实测下来连续读16块以上吞吐率能比单块读提升好几倍。3. ARM平台SD卡驱动从初始化到读写全流程理论聊完了进入实操环节。以STM32的SDIO外设为例梳理一遍完整的驱动流程。这一步是硬骨头也是绝大多数问题的藏身之地。3.1 初始化流程一步步来别跳步初始化是SD卡驱动里最容易出错的地方因为命令顺序和时序有严格要求。我习惯把初始化分成四个阶段阶段一卡识别主机发送CMD0复位卡。条件时钟频率要低一般100kHz-400kHz因为卡上电初期工作频率低扛不住高速时钟。这步在SDIO外设里要先把CLK配成低速初始化完成后再提频。发送CMD8参数0x1AA读取R7响应。判断卡是否支持SDHC/SDXC。响应里的电压回显必须是0x1AA才算正常。如果卡无响应或回显异常多半是卡太老或者接触不良。循环发送CMD55 ACMD41带HCS位直到ACMD41响应的bit0变为0。注意每次CMD55和ACMD41之间不要插入其他命令。SDR21等高速卡可能在这个循环里轮询几十次才完成代码里要设置超时防止卡彻底挂了导致死循环。读OCR确认电压匹配后发送CMD2读取CID再发CMD3获取RCA。CMD3返回的RCA要保存好后面的CMD7、CMD13都要用到。阶段二卡选中发送CMD7参数是RCA左移16位选中这张卡。只有选中状态下的卡才能接收数据读写命令。阶段三读写配置这一步是新手最容易漏的。很多卡在初始化完成后默认是按单数据线、低时钟跑的如果你直接上4位模式或者高频时钟写入数据就会出错。所以要做两件事传输宽度发送CMD6SWITCH命令或者用ACMD6SET_BUS_WIDTH把数据线从默认的1位切换到4位。STM32的HAL库里有SDIO_SetWideBusMode接口底层就是发ACMD6。时钟提速先读CSD寄存器CMD9拿到卡的传输速度等级TRAN_SPEED、CCC等字段然后判断能不能跑高时钟。比如卡标称Class 10那50MHz基本没问题如果是老Class 2的卡还是老实跑25MHz甚至更低。时钟提频的顺序应该在切换到4位模式之后不然1位模式下带宽有限高频也发挥不出来。阶段四块大小确认读CSD或SCR寄存器确认块大小等于512字节这是唯一在实操中被所有消费级SD卡支持的块大小工业级特制卡另说。到这里SDIO和卡之间的链路就建立起来了可以开始真正的数据读写。3.2 单块读写代码示例用STM32的标准外设库写一个单块读的示例片段其他平台逻辑同理uint8_t SD_ReadSingleBlock(uint32_t sector, uint8_t *buf) { SDIO_DataInitTypeDef data {0}; uint32_t timeout SDIO_TIMEOUT; // 1. 发送CMD17 SDIO_SendCommand(SDIO_CMD17, sector, SDIO_RESP1); if (SDIO_GetCommandResponse() ! SDIO_RESP1) { return SD_ERROR_CMD_RESP; } // 2. 配置接收数据通道 data.DataPath SDIO_DATAPATH_RECEIVE; data.DataLength 512; data.DataBlockSize SDIO_DATABLOCK_SIZE_512B; data.TransferDir SDIO_TRANSFER_DIR_TO_SDIO; data.TransferMode SDIO_TRANSFER_MODE_BLOCK; SDIO_ConfigData(data); // 3. 等待数据接收完成 while (timeout--) { if (SDIO_GetFlagStatus(SDIO_FLAG_RXDAVL) ! RESET) { // 读取FIFO中的数据 for (int i 0; i 128; i) { // 512字节 / 4字节 128次 ((uint32_t *)buf)[i] SDIO_ReadFIFO(); } return SD_OK; } } return SD_ERROR_TIMEOUT; }注意几个细节CMD17的参数是扇区号不是字节地址。写SDHC卡时直接传逻辑扇区号就行SDSC卡则要换算成字节地址扇区号 * 512。读取FIFO时按4字节一次读因为SDIO外设的FIFO是32位宽。如果你的缓冲区地址不是4字节对齐就会出现HardFault。所以驱动入口处最好做一次地址对齐检查或者要求调用方必须传入对齐的缓冲区。超时时间不能设太短。SD卡在写Flash时会发生“写放大”现象实际写入时间可能比理论时间长一个数量级。我遇到过一张老卡写单块偶尔要20ms才返回超时设短了就会出现写失败。3.3 多块读写性能来源多块读写主要用在高吞吐场景录音设备写音频流、数据采集器周期性存批量数据、OTA升级包写入等。SD模式下多块读是连续地址的命令只需要一次CMD18发出去后续数据块按流水线持续传输直到主机发CMD12停止。写多块时有一个坑预擦除。很多卡在多块写前需要发ACMD23SET_WR_BLK_ERASE_COUNT告诉卡接下来要写多少块卡可以提前做Flash预擦除减少时间损耗。如果跳过ACMD23多块写的总耗时可能翻倍。我还遇到过某些卡不先擦除就直接连写写到一半返回数据错误加上ACMD23就一切正常。我在项目中测试过一张Class 10的32GB卡单块写CMD24吞吐率大概1.5MB/s多块写CMD25 ACMD23一次写64块能到4MB/s以上。读的差距更大单块读1.8MB/s多块读能接近8MB/s。如果你的应用是持续大量写日志多块写是必须的。3.4 中断和DMA别让CPU被FIFO拖死数据接收时如果纯粹用轮询方式读FIFOCPU会被OTL的时钟频率绑在循环里——每来4字节就打断一次主控干不了别的。这在实时性要求高的系统里是灾难。所以主流做法是DMA方式配置DMA自动把SDIO接收FIFO的数据搬运到内存缓冲区传输完成后触发DMA中断。CPU在DMA搬运期间可以处理其他中断任务。中断方式配置SDIO接收FIFO半满/全满中断在中断里搬数据。比DMA轻量但中断频率高适合DMA通道不够用的小资源芯片。ST的HAL库提供了SDIO_DMA传输的封装但有个暗坑FIFO的DMA突发模式配置、DMA请求和SDIO中断之间的竞争。我曾经遇到DMA传输完成的标志时刻晚于实际数据写完导致主程序去访问还没更新的缓冲区数据不对。解决方案是开启DMA的传输完成中断同时禁用FIFO interrupt让DMA中断作为唯一完成信号。4. 文件系统层面FatFs的引入与适配驱动层面搞定SD卡读写应用层还需要文件系统。嵌入式世界最流行的就是FatFs它开源的、可裁剪的、支持FAT12/16/32和exFAT代码量小移植也不难。4.1 标准挂载流程FatFs的移植核心是提供底层接口函数通常要写6个disk_status返回磁盘状态disk_initialize初始化SD卡调用SD读写驱动disk_read读扇区可多扇区disk_write写扇区可多扇区disk_ioctl控制命令如获取扇区数、扇区大小、擦除块数量get_fattime返回当前时间用于文件时间戳代码骨架如下DRESULT disk_read(BYTE pdrv, BYTE *buff, LBA_t sector, UINT count) { if (pdrv ! 0) return RES_PARERR; // 多块读连续读效率更高 if (count 1) { SD_ReadSingleBlock(sector, buff); } else { SD_ReadMultiBlocks(sector, buff, count); } return RES_OK; } DRESULT disk_write(BYTE pdrv, const BYTE *buff, LBA_t sector, UINT count) { if (pdrv ! 0) return RES_PARERR; if (count 1) { SD_WriteSingleBlock(sector, (uint8_t *)buff); } else { SD_WriteMultiBlocks(sector, (uint8_t *)buff, count); } return RES_OK; }单块/多块分派逻辑必须做。FatFs底层碎片化严重的时候会落成很多单扇区写这时候如果驱动直接把单扇区也当作多块来发会产生错误。4.2 FatFs配置要点ffconf.h里有几个关键配置项按项目需求设置_FS_READONLY只读文件系统设为1可省RAM和Flash但很多写日志的应用不能设为1。_USE_MKFS需要在设备上格式化SD卡通过f_mkfs函数时设为1。_USE_LFN支持长文件名。FAT格式里长文件名需要占用额外的扇区缓冲_USE_LFN设为2用堆分配能省一点静态内存但要注意malloc可用性。_MAX_SS扇区大小最大值FATFS默认512就够了。如果你的卡支持4096字节扇区高级格式卡要设为4096。_FS_EXFAT需要支持exFAT时开启。不过老一点的STM32工程RAM紧张exFAT缓冲区要求挺高8KB打底评估后再开。体积方面全功能配置下FatFs大概占6-10KB Flash1KB左右的RAM不含文件缓冲在F103这种256KB Flash的平台上毫无压力。4.3 格式化与挂载初始化示例设备首次上电处理SD卡时我用下面的逻辑FRESULT init_sd_fat(void) { FATFS fs; FRESULT res f_mount(fs, 0:, 1); // 立即挂载 if (res FR_NO_FILESYSTEM) { // 没有文件系统尝试格式化 res f_mkfs(0:, NULL, 0, 0); if (res ! FR_OK) return res; res f_mount(NULL, 0:, 0); res f_mount(fs, 0:, 1); } return res; }f_mkfs格式化时间不短大卡可能要几秒到几十秒视卡速而定。格式化进行中千万不能掉电否则文件系统半载状态是最难恢复的。建议在产品说明里写明“首次使用请保持供电等待初始化完成”。另外f_mount的第三个参数设为1会立即挂载如果卡没插或初始化失败返回的错误码要捕捉到并做好UI提示或系统状态位。5. 实操中我踩过的坑与通用排查思路最后这部分是最值钱的。SD卡在嵌入式里出问题很多不是大方向错了而是细节翻车。我把这些年收集到的经典问题和排查思路整理成清单按概率排序。5.1 卡初始化失败卡被系统识别不到这是出场率最高的问题。排查路径按顺序走先查硬件接线是否共地上拉电阻是否接好SD卡的CMD、DAT0-3、CLK都需要10k-100k上拉部分开发板已经自带上拉外接注意别重复。用示波器看CLK引脚是否有正常方波输出如果没有先修硬件。查上电时序确认卡插入后是否先给卡供电稳定至少要等几个ms的VDD建立时间再发CMD0。我见过有些板子卡座有热插拔供电切换电路上电瞬间电压爬坡不稳导致卡初始化失败。解决的方法是SD卡电源引脚加个大电容100uF级别并在代码里启动后延时50ms再初始化。查初始化时钟频率确认初始化阶段时钟是否低于400kHz。有的片子改动时钟树时把SDIO CLK直接配成48MHz去发CMD0这时初始化必失败。初始化阶段降频是很多老工程师的习惯不要跳。CMD8响应异常R7响应的低12位不是0x1AA或者卡完全无响应。大概率是卡不是标准卡比如山寨扩容卡、某些工业卡对CMD8有特殊处理这时候只能按SDSC流程跑发CMD55/ACMD41时不要置HCS位。5.2 写入数据后读出来校验失败问题表现为写文件后读文件内容不完整、乱码、开头数据对但后面全错。原因排查顺序缓冲区未对齐DMA访问内存需要4字节对齐。如果应用层传入的buf地址是奇地址比如结构体里的字段DMA写入就会错乱。我在项目里要求FatFs配置_USE_LFN2同时给文件缓冲的堆内存16字节对齐解决过好几次随机写坏的问题。DMA缓冲区正在被访问DMA搬运前主机已经把数据写到buf但DMA还在搬运时主机又改了buf内容最终DMA搬了一半旧数据、一半新数据。解决方法是DMA写完后等中断标志位再让主机改buf或者使用双缓冲区轮转交替更新。时钟过高超出卡能力卡标称Class 10不代表在任何条件下都能跑满50MHz。如果遇到整块数据校验异常先把时钟降到25MHz试试。我在一块PDD上买的所谓“高速卡”上遇到过必须降到12.5MHz才能稳定的情况。5.3 文件系统挂载慢或者挂载失败有时候卡能读写也格式化成FAT32了但挂载时间奇长几秒钟甚至挂不上。主要原因卡的数量多、坏块多、文件系统碎片严重。FatFs挂载时要扫描FAT表FAT32的FAT表占空间很大1GB的卡至少1MB FAT表如果底层驱动每读一个扇区都要额外附加延迟挂载自然慢。改善方法底层多块读直接传给驱动FatFs内部本来也是按簇读写连续簇的读取是连续扇区接得住。定期维护文件系统比如每周格式化一次避免长期增删文件导致碎片膨胀。对要求高可靠性的场景预留一个固定大小的预分配文件比如日志文件创建时直接设置大小减少运行时动态分配文件的碎片。5.4 热拔插损坏文件系统嵌入式设备如果允许用户带电拔卡那文件系统损坏是迟早的事。硬件上一定要加卡检测开关卡座通常带CDCard Detect引脚接一个GPIO检测卡在位状态。软件上轮询CD引脚比如每100ms读取一次检测到卡拔出时立即f_mount(NULL, 0:, 0)卸载文件系统之后所有文件操作返回错误。重要数据比如系统配置文件在写入时做“文件写完-落盘”两步走f_sync函数保证在拔卡瞬间最近的写操作已经同步到磁盘。我项目里用的策略是检测到拔卡事件后不立即执行卸载而是标记“卡即将移除”当前文件写入加锁下一次写操作前再真正卸载避免在中断上下文做重操作。5.5 大容量卡64GB/128GB格式化问题有些项目要用大容量卡存长时间数据。FAT32格式在Windows下默认支持到32GB64GB以上在PC上格式化时默认选exFAT。但很多嵌入式芯片和FatFs配置并没有开启exFAT支持这时如果你直接把一张128GB的exFAT卡插进设备系统会显示“无文件系统”。解决思路开启FatFs的exFAT支持_FS_EXFAT设为1并在disk_ioctl里正确返回扇区大小。或者用PC把卡重新格式化成FAT32第三方工具如大容量格式化工具但FAT32在128GB上性能一般而且Windows 原生工具限制在32GB。还有一种做法分区表多分区。嵌入式上把卡分成两个FAT32分区32GB存普通文件剩余存大文件流水数据。只是复杂度上去了一般不建议。5.6 速度达不到预期卡慢得像蜗牛写性能差的现象是写入文件时平均速率很低或者间歇性卡顿。诊断方向确认是否真的用了多块写。单块写每512字节就要一次完整命令-响应-忙流程效率极低。确认是否发了ACMD23预擦除。对某些卡来说这步能省一大半时间。检查是否有“写同步”拖后腿。FatFs的f_sync调用每次都会强制卡把缓存中的内容落盘到Flash太频繁地sync比如每写10字节就sync会让性能暴跌。合理的做法是每写一批数据比如4KB-32KB才sync一次或者关闭f_sync依赖卡自身的缓存管理。5.7 不同批次卡兼容性差异这是最让人头疼的。同一个工程A厂家的卡跑得好好的换成B厂家的卡就初始化失败或者写坏块。解决办法没有银弹核心思路是“降级兼容”初始化时序严格按规范来不依赖厂商扩展命令。时钟不要一上来就跑满高速启动阶段400kHz初始化后先跑25MHz确认读写正常再提速。可以在板级配置里预留一个“兼容模式”宏调试时切换。单块读写失败时驱动层做3次重试SD协议是允许重try的重试前重新配置数据通道和FIFO。对工业产品建议做一张经过测试的兼容卡列表比如只认证三家卡厂写在用户手册里避免用户随便插卡导致售后问题。6. 稳定性设计一个可靠的SD卡子系统怎么搭光把SD卡跑通还不够嵌入式产品要的是“长期稳定运行”。我分享几个我认为比较重要的设计习惯。6.1 超时机制与错误恢复SD卡命令都要有超时不能让代码无限等待。我一般在驱动层定义一个函数uint8_t SD_WaitResponse(uint8_t expected, uint32_t timeout_ms) { uint32_t tick 0; uint32_t start HAL_GetTick(); while (HAL_GetTick() - start timeout_ms) { if (SDIO_GetFlagStatus(...) expected) return OK; } return TIMEOUT; }超时后的恢复流程也很重要。卡超时可能是瞬时错误总线抖动也可能是永久错误卡损坏。我习惯先做一次卡复位CMD0如果复位后初始化成功就继续用如果复位后还是失败才认定卡失效。6.2 掉电保护与日志可靠性嵌入式设备最容易出问题的场景是写文件到一半系统断电然后文件系统损坏。除了硬件上增加掉电检测外软件层有不少能做的日志文件按天创建一天一个坏了只坏一天的日志不影响历史数据。关键配置文件比如系统参数写入时采用“双份冗余”写A区校验通过后再写B区读取时优先读A区A区校验失败则回退B区。这样即使某次写入被掉电打断至少有一份是完整的。写入文件后用f_truncate和f_sync组合确保文件大小字段先更新再更新数据可以减少FAT表损坏的概率。6.3 坏块的软件层辅助SD卡自身会管理坏块但主控层也可以做一些辅助。一个实用的做法是以“扇区写成功后记录”的方式维护坏扇区表。日常写入时如果某扇区返回错误标记备用重试一次不成再标记。这样连续多次错误时可以立即切换到另一张新卡或者降低工作模式不至于突然把整个文件系统干废。不过要注意别自己做低层坏块管理。SD卡内部的控制器已经做过了主控再做反而会冲突。主控层面的“坏扇区表”只是标记不可用的逻辑扇区避免反复写入仅此而已。6.4 性能预算与缓冲区规划SD卡子系统对MCU资源的消耗不小尤其是DMA缓冲区和FatFs的工作区。我一般把SD卡缓冲区单独划一块RAM用__attribute__((aligned(4)))或者链接脚本放到对齐地址。DMA的缓冲改成double buffer后内存翻倍我项目里是用8KB双缓冲Flash剩个几十KB时要注意。缓冲区规划的经验值如果只做写入读缓冲4KB、写缓冲4KB就够用如果还要做文件浏览、读取音频流读缓冲16KB以上写缓冲4KB以上配合二级缓存效果更好。7. 实测记录一张普通Class 10卡的读写性能纸上谈兵没意思放一组我在项目板STM32F407 168MHzSDIO 4位模式50MHz时钟上的实测数据供参考。操作块大小命令组合实测吞吐率单块读512BCMD171.5 MB/s多块读 (64块)32KBCMD188.1 MB/s单块写512BCMD241.4 MB/s多块写 (64块)32KBCMD25 ACMD234.2 MB/s多块写 (128块)64KBCMD25 ACMD234.5 MB/s读明显比写快这是NAND Flash的特性——写入需要擦除、纠错耗时远高于读。另外可以看到块大小从32KB加到64KB写速度提升有限说明瓶颈已经不在块调度而在Flash本身了。我还在另一张杂牌Class 4卡上做过测试多块写只能跑2.3MB/s而且单块写时经常需要重试。所以产品级设计时SD卡驱动最好带一个“性能自检”功能初始化后用固定的时间窗口测一遍实际吞吐如果低于阈值比如2MB/s可以提示用户换卡。7.1 速度上不去的下一步优化方向如果你对性能还有更高的追求可以考虑提高SDIO时钟H7系列能跑SDIO 100MHz但卡是否支持得看卡的标准。Class 10卡一般在50MHz内稳妥。准备多个活跃缓冲区多缓冲轮转在DMA搬运当前数据块时CPU提前准备下一块数据隐藏排队时间。数据调度把多个小文件写请求合并成一个大块连续写减少命令开销。关闭FatFs的FIFO缓冲如果系统RAM紧张FIFO缓冲其实可以关闭每次读写直接搬数据但性能会打折。SD卡这块学到一定程度你会发现它其实是个很好的“模版型外设”——掌握了它的命令-响应机制再去玩NAND Flash、eMMC、甚至NVMe思路都是相通的只是总线宽度、协议复杂度、命令集不一样。这也是为什么几乎所有的ARM嵌入式教程都会把SD卡当作一个必讲模块的原因。就我自己这几年的经验来说SD卡驱动和文件系统移植真正难的不是看懂协议而是把协议落到具体的硬件外设上还要考虑各种卡、各种环境、各种意外情况下的鲁棒性。你现在看到的所有“坑”其实都是好老师。如果你正在调SD卡相关代码卡在某个初始化步骤过不去或者连续写入总是偶尔出错把上面说的排查路径从头走一遍八成能找到原因。耐心一点SD卡的脾性摸透了后面用起来会顺手很多。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →