简介:面向STM32F4嵌入式开发者的一套SD卡读写工程资源,使用SPI接口连接SD卡,并移植FATFS文件系统实现文件创建、读写与删除等操作。内容覆盖SD卡初始化协议(CMD8/ACMD41)、SPI时序配置、FATFS底层驱动适配,以及基于HAL库的读写示例,适合需要为项目快速增加存储功能的工程师或学习文件系统移植的学生。压缩包共368个文件,以C/C++源码和头文件为主,辅以Keil工程配置、编译中间文件(.o/.crf)、烧录hex及少量说明文档和图片,整体大小约21.11MB,可直接导入Keil MDK查看或编译运行。已有3661人学习。资源内还包含多语言编码转换源码(如GBK对应文件)和完整的STM32F4 HAL驱动,工程结构完整,从底层SPI驱动到FATFS调用链路清晰,是理解嵌入式存储方案与排错调试的实用参考。
1. 为什么要在SD卡上挂FATFS:一个让很多开发者卡住的开始
很多人把SD卡接到单片机上的第一步是直接往扇区里写字节,写进去也能读回来,但等到真要把它当“数据记录仪”用、上位机要直接读文件、要按文件名管理多个log时,就会发现自己被扇区寻址、擦除块、坏块管理这些事缠住了。带FATFS文件系统的SD卡读写(SPI模式)要解决的正是这个问题:让MCU用通用文件格式读写SD卡,拔下来插读卡器就能看到文件名、复制出来用Excel打开。SPI模式则是最省接口资源的做法,普通SPI外设加四根线就能跑,几乎任何MCU都具备条件。这个方案适合做日志记录器、参数存储、字库更新、历史数据采集这类产品,给一块SD卡相当于给设备装了一块可插拔的硬盘。但它的坑比想象中多:初始化时序、卡类型判断、FATFS底层接口映射、掉电保护,不是f_mount一次就能跑稳的。
2. SPI模式下的SD卡:为什么它是首选,以及如何物理连接
2.1 SPI模式与SD模式的取舍:接口资源、速度、兼容性
SD卡本身支持两种通信模式:SD模式(4位并行)和SPI模式(单线串行)。SD模式速度快,能跑到几十MB/s,但至少占用CLK、CMD、DAT0-DAT3六条线,还得在MCU侧实现CMD/数据两套状态机。SPI模式只需要SCLK、MOSI、MISO、CS四根线,复用MCU自带SPI外设即可,代价是吞吐率上限大约25MHz,实际读写速度通常在1-2MB/s,对绝大多数数据记录场景已经足够。
我一般会优先选SPI模式,不是因为快,而是因为接口通用性。一个项目换MCU平台,SPI驱动几乎不用改,FATFS本身又是与硬件无关的中间层,换芯片的成本被压得很低。SD模式在换主控后往往要重写底层状态机,调试周期完全两个量级。
下表是两种模式的直观对比:
| 对比项 | SPI模式 | SD模式 |
|---|---|---|
| 引脚占用 | 4线(SCLK/MOSI/MISO/CS) | 6线(CLK/CMD/DAT0-DAT3) |
| 理论速率上限 | 25MHz | 50MHz以上(4位并行) |
| MCU外设要求 | 标准SPI口即可 | 需要SDIO控制器或高速GPIO模拟 |
| 协议复杂度 | 命令应答式,易用逻辑分析仪抓包 | 帧格式复杂,调试门槛高 |
| 常见卡兼容性 | 对SDSC/SDHC兼容性较好 | 高速卡支持好 |
| 适用场景 | 数据记录、低速存储 | 音视频录制、高速缓存 |
注意SDXC卡在SPI模式下的兼容性明显下降,产品定型时尽量锁SDHC卡,容量控制在32GB以内。
2.2 接线和电平:上拉、供电顺序、焊接细节
SD卡的SPI模式引脚分配很固定:Pin 1 CS(片选)、Pin 2 DI(MOSI,卡侧接收)、Pin 3 VSS(地)、Pin 4 VCC(电源)、Pin 5 SCLK(时钟)、Pin 7 DO(MISO,卡侧发送)。特别注意DO引脚在SPI模式下是推挽输出,空闲时为高电平,初始化阶段如果读到0xFF可能表示卡未就绪或线路断开。
供电用3.3V,绝不能直接接5V,否则卡内LDO和IO驱动会烧。如果MCU是5V供电,MOSI、SCLK、CS必须经电平转换芯片或电阻分压进卡,MISO方向也要确认主控的IO能容忍3.3V高电平。用杜邦线连接面包板时,信号线尽量短,VCC和GND之间加一个10uF钽电容再加一个100nF瓷片电容,放在卡座附近,防止SPI时钟切换瞬间的电流尖峰把电压拉低。
上电顺序也有讲究:VCC先稳定,再拉高CS,然后才允许SCLK翻转。如果SCLK先跑起来而VCC还没到,卡有可能进入未知状态,后面发任何命令都得不到正确应答。实际操作上,我在初始化函数最前面加了一个延时,确保电源建立后再拉CS高电平,再发时钟脉冲。还有一个细节:MISO线上建议加一个10k上拉电阻到3.3V,卡座没插卡时总线不会悬空,读到的值稳定为0xFF,排查问题时会轻松很多。
2.3 卡座选型:CD引脚、写保护开关、弹片质量
卡座不是随便选的。带卡检测(Card Detect)引脚和写保护开关的卡座,对产品可靠性有实质帮助:CD脚可以检测到卡是否插到位,避免在无卡状态下执行格式化。写保护开关是机械式的,拨到LOCK位置后卡内部不会响应写命令,FATFS会返回FR_WRITE_PROTECTED。有些便宜卡座弹片容易氧化,插拔几十次后接触电阻变大,写数据时偶尔出现CRC错误,这种问题很难在样机阶段暴露。批量做产品时卡座一定要用品牌件,模拟项目X里就出现过批量运行时卡座导致的间歇性写入失败问题,换卡座后故障消失。
3. 移植FATFS到工程:配置文件与底层diskio接口
3.1 文件结构和配置入口:ffconf.h 里真正要改的项
FATFS是一套成熟的开源嵌入式文件系统组件,源码结构和移植点都很清晰。把ff.h、ff.c、diskio.h、diskio.c、ffconf.h加入到工程,然后重点改ffconf.h中的宏配置:
#define FF_USE_LFN 2 // 使能长文件名,2表示使用堆栈缓冲 #define FF_CODE_PAGE 936 // 简体中文代码页,优盘里中文文件名不乱码 #define FF_FS_READONLY 0 // 0表示允许写操作,产品上有写需求必须为0 #define FF_VOLUMES 1 // 卷数量,单卡单分区填1 #define FF_MIN_SS 512 #define FF_MAX_SS 512 // SD卡固定扇区512字节,别改成4096 #define FF_USE_FASTSEEK 1 // 使能快速定位,随机访问大文件有效 #define FF_USE_STRFUNC 1 // 使能f_printf等格式化写函数,打印日志方便FF_USE_LFN建议设成2而非1,原因是长文件名需要在栈上或堆上分配缓冲区,设为1时文件名缓冲放全局变量,多任务环境有互斥问题;设为2时每次调用临时申请,可重入性更好。FF_CODE_PAGE与中文文件名直接相关,不设置的话UTF-8编码的中文文件名在FATFS里会乱码。FF_USE_STRFUNC一旦开启就能用f_printf向文件里格式化输出,写日志代码会简洁很多。
3.2 必须实现的6个diskio函数:从FATFS到物理层的桥梁
FATFS的移植核心是diskio.c,里面所有函数都需要根据实际存储介质实现。这张接口映射表是移植的地图:
| diskio函数 | 作用 | 瓶颈点 |
|---|---|---|
| disk_initialize | 初始化底层存储,对应SD卡SPI模式初始化流程 | 必须正确完成上电时序和CMD0/ACMD41 |
| disk_status | 查询存储媒介状态,返回STA_NOINIT或0 | 初始化失败后要返回非0,否则FATFS误以为介质可用 |
| disk_read | 读取指定扇区到缓冲,buf为NULL表示只是丢弃数据 | 注意读等待超时,不能死等 |
| disk_write | 将缓冲数据写入指定扇区 | 必须等待写完,卡busy期间不能发新命令 |
| disk_ioctl | 获取容量、扇区大小、擦除块大小等参数 | 参数不正确会直接导致f_mkfs建出损坏文件系统 |
| get_fattime | 返回当前时间戳,FATFS写入目录项时调用 | 无RTC时返回固定日期,文件时间显示1970年 |
disk_read和disk_write是最核心的两个函数。以disk_read为例,代码结构是这样的:
DRESULT disk_read(BYTE* pdrv, BYTE* buff, LBA_t sector, UINT count) { (void)pdrv; if (count == 0) return RES_PARERR; if (sd_status != 0) return RES_NOTRDY; // 卡未初始化直接返回 for (UINT i = 0; i < count; i++) { if (!sd_read_single_block(sector + i, buff + i * 512)) { return RES_ERROR; } } return RES_OK; }这段代码把FATFS下发的“多扇区连续读”拆成了单扇区循环处理,每个单扇区操作内部完成命令发送、令牌等待、CRC校验三件事。为什么不做CMD18多块读?因为多块读需要额外处理停止传输命令,出错后的恢复逻辑复杂,单扇区循环虽然慢一点,但可靠性高得多。计数字段乘以512字节偏移来定位缓冲区位置,这依赖FF_MAX_SS=512的配置。返回RES_NOTRDY而不是直接RES_ERROR,是为了让FATFS在卡未初始化时能感知介质不可用,而不是当成扇区读写失败反复重试。
disk_write的等待逻辑更关键,写入一个扇区的流程是发送CMD24、发送数据起始令牌0xFE、发送512字节数据、等待0x05应答、再等卡内部完成擦写。FATFS调用disk_write后默认底层已经完成写操作,如果不等busy结束就返回,FATFS下一个命令发出去,卡还在忙,数据就丢了。我的实现里等待busy的逻辑是轮询MISO引脚电平,低电平表示busy,高电平表示空。
3.3 让FATFS认识SDHC卡:disk_ioctl的三种返回值
disk_ioctl函数里最常被忽略的是GET_SECTOR_SIZE和GET_BLOCK_SIZE。FATFS格式化时默认扇区是512字节,但SD卡内部的擦除块可能是1KB、2KB甚至4KB,返回GET_BLOCK_SIZE能帮助FATFS在格式化时选择更合理的簇大小,减少小文件碎片。我的实现是这样:
DRESULT disk_ioctl(BYTE* pdrv, BYTE cmd, void* buff) { (void)pdrv; switch (cmd) { case GET_SECTOR_COUNT: // 返回卡总扇区数,必须从CSD寄存器解析 *(LBA_t*)buff = sd_get_sector_count(); return RES_OK; case GET_SECTOR_SIZE: *(WORD*)buff = 512; return RES_OK; case GET_BLOCK_SIZE: *(DWORD*)buff = sd_get_erase_block_size(); // 通常返回16或32,单位扇区 return RES_OK; case CTRL_SYNC: sd_wait_busy(); // 确保写入完成 return RES_OK; default: return RES_PARERR; } }SDHC卡和SDSC卡在容量读取方式上不同:SDHC卡是块寻址,CSD寄存器里的C_SIZE、C_SIZE_MULT直接换算扇区数;SDSC卡要乘以字节转扇区的系数。如果这里返回的容量比实际小,FATFS格式化后文件系统会建在卡的前部,后面空间直接浪费;如果返回得比实际大,f_mkfs可能建出一个写到卡外地址的文件系统,后续写操作会异常。FATFS传入LBA_t类型是64位的,SD卡最大容量只有2TB,注意类型转换别截断。
另一个隐藏点:disk_initialize成功返回后FATFS会调用disk_ioctl的GET_SECTOR_COUNT来确认介质容量,若返回0,f_mount会直接失败。所以SD卡初始化和容量解析必须在这一步完成,不能把容量查询逻辑放到别的函数里。
4. SD卡SPI模式初始化命令序列:从CMD0到ACMD41的每一步
4.1 上电时序:为什么先要“发74个时钟且CS拉高”才能发CMD0
SD卡上电后默认处于SD模式,要想切换到SPI模式,必须由主机完成一个非常讲究的握手过程。第一步是把CS拉高,让卡处于未选中状态;第二步发送至少74个时钟周期的脉冲,数据线维持高电平;第三步拉低CS,发送CMD0。CMD0的参数是0x00000000,CRC字段固定为0x95,这条命令是唯一一条在SPI模式下也必须携带正确CRC的命令。
为什么是74个时钟?SD卡内部协议规定上电后需要至少74个CLK周期让卡内部电压稳定、初始化状态机做复位,少于这个数量,卡可能还在处理上电内部逻辑,CMD0直接被忽略。74是数据手册给的最小值,为了留足余量我一般发80个周期,这个习惯从未出过问题。发时钟的办法是用SPI连续发送0xFF字节,因为0xFF在MOSI上产生一串高电平脉冲。这期间CS必须保持高电平,如果CS为低,卡会认为主机要开始传输数据,握手就会失败。
初始化阶段的SPI时钟频率不能太高。SD卡规范要求在初始通信期间时钟上限400kHz,因为卡内部复位逻辑是低速时钟域,频率太高时序跟不上。调试时我习惯先固定200kHz,等CMD0返回0x01确认进入SPI idle状态后,再切换到25MHz。很多开发者在初始化阶段直接用最大时钟跑,CMD0永远得不到正确应答,就是这个原因。
4.2 命令格式与send_cmd实现:R1应答、CRC7的作用
SD卡SPI模式下的所有命令都是固定6字节:1字节命令索引(bit7为0,bit6为1),4字节参数(大端序),1字节CRC7+结束位。SPI模式下除了CMD0必须带正确CRC外,其余命令卡并不校验CRC,但命令发送函数还是要按规范填CRC值,否则极少数严格实现的老卡会拒绝响应。这里给出一个带CRC计算的命令发送函数:
// sd_cmd只发送命令,不处理应答,返回值是R1应答字节 uint8_t sd_send_cmd(uint8_t cmd, uint32_t arg) { uint8_t frame[6]; frame[0] = 0x40 | cmd; // 命令索引,bit6置1 frame[1] = (arg >> 24) & 0xFF; frame[2] = (arg >> 16) & 0xFF; frame[3] = (arg >> 8) & 0xFF; frame[4] = arg & 0xFF; frame[5] = (crc7(frame, 5) << 1) | 0x01; // CRC7占bit7-bit1,bit0结束位 spi_cs_low(); spi_write_bytes(frame, 6); uint8_t resp = 0xFF; for (uint8_t i = 0; i < 8; i++) { resp = spi_read_byte(); if (resp != 0xFF) break; // 丢弃前面若干0xFF,读到非0xFF即为R1 } spi_cs_high(); return resp; }逻辑说明:命令帧构造的细节是第0字节必须为0x40与命令索引的或值,这是SPI模式命令的固定头部;CRC7是对frame[0]到frame[4]这5个字节计算,算法是7位CRC多项式0x09;结果左移1位后bit0置1作为结束位。发送完命令后,卡会先输出若干0xFF作为方向切换间隙,然后输出R1应答字节。无视应答和解析应答是两种境界,想快速定位初始化失败原因,最好把R1应答打印出来。R1的bit0为1表示卡处于idle状态,这是CMD0成功进入SPI模式的标志;bit1为1表示擦除内部忙;bit2及以上对应各类错误。CMD8和CMD58的应答是R7/R3,需要额外读4字节数据,send_cmd函数要区分处理。
4.3 初始化流程:CMD8判断卡类型、ACMD41循环就绪
整个初始化序列我固定按下面的顺序执行,任何一步的失败都会直接返回错误码,方便上层定位:
uint8_t sd_init_spi_mode(void) { spi_set_clock(200000); // 低速初始化 sd_cs_set(1); // CS先拉高 for (uint8_t i = 0; i < 10; i++) spi_write_byte(0xFF); // 80个时钟脉冲 sd_cs_set(0); if (sd_send_cmd(0, 0x00000000) != 0x01) return 1; // CMD0,必须是idle状态 // 发送CMD8检查是否支持SDHC,参数0x1AA表示2.7-3.6V供电,返回R7 uint8_t r = sd_send_cmd8(0x000001AA); // 内部会读取4字节R7 if (r == 0x01) { sd_is_sdhc = (sd_r7_arg & 0xFF) == 0xAA; // 低字节0xAA表示支持SDHC } else { return 2; // 不支持CMD8,老卡按SDSC处理 } // 循环发送ACMD41,arg=0x40000000表示请求SDHC类型,直到就绪 uint16_t timeout = 1000; do { sd_send_cmd(55, 0); // CMD55是ACMD的前缀 r = sd_send_cmd(41, 0x40000000); // ACMD41 if (r != 0x01) break; } while (--timeout); if (timeout == 0 || r != 0x00) return 3; // 读取OCR寄存器确认供电电压范围 if (sd_send_cmd58(0) != 0x01) return 4; // CMD58返回OCR的bit31位为1表示卡上电完成 if (!(sd_r7_arg & 0x80000000)) return 5; // 对SDSC卡发送CMD16设置块长度为512字节,SDHC强制512无需设置 if (!sd_is_sdhc) { sd_send_cmd(16, 512); } spi_set_clock(25000000); // 初始化完成,切到高速模式 return 0; }这段代码的关键点有几个。第一,CMD8的参数0x1AA是SD卡规范定义的固定值,低8位0xAA、高字节0x1表示支持2.7-3.6V供电范围,卡返回的R7低字节如果也是0xAA说明支持新标准,可以继续用ACMD41的HCS位请求SDHC;如果CMD8返回0x05表示不支持,其实就是老SDSC卡。第二,ACMD41的循环次数不能无限等,规范建议超时1秒左右,我给的1000次循环在200kHz时钟下大约几秒。第三,CMD58读取OCR后要检查bit31,它表示卡内部上电完成,FATFS挂载前必须确认这一步。
初始化失败时用逻辑分析仪抓MOSI和MISO最直观。MISO上的R1应答每字节之间应当都有0xFF间隔,如果应答粘连或波形平台电压不对,先查电平转换和供电,再查时钟极性配置。SPI的CPOL/CPHA通常配置为Mode 0或Mode 3,两者的区别只在时钟极性上,SD卡对这两种模式都兼容,前提是MISO在时钟上升沿采样,我的配置固定是CPOL=0、CPHA=0。
5. SD卡读写避坑:从初始化失败到数据损坏的排查清单
5.1 问题1:CMD0永远返回0xFF或0x01,卡不进入SPI模式
现象是发CMD0后从MISO上读不到任何应答字节,一直读到0xFF,或者应答是0x01但之后发ACMD41永远不成功。先说0xFF的情况:总线根本没接通,先检查MOSI/MISO有无接反,SPI模式中卡座Pin2是MOSI、Pin7是MISO,不少开发者按直觉接把这两条线对调了。再检查CS信号线是否是低电平有效,初始化时CS要先拉高后拉低,如果CS一直是低,卡一直处于选中状态,CMD0不会进入SPI模式识别逻辑。0x01但后续失败的情况,大概率是CMD0后没等卡状态稳定就直接发CMD55,我习惯在CMD0应答后加一个50ms延时(虽然规范没要求,但老卡配合更稳定)。
另一个被我踩过的坑是上电后第一个SPI时钟就用高速时钟。SD卡在SPI模式下只有一个时钟源,从主机来的SCLK同时驱动卡内部状态机,初始化阶段卡尚未锁定PLL或内部时钟分频未稳定,高频时钟会让命令识别出错。所有初始化命令发送前必须把SPI时钟降到400kHz以下。我的习惯是初始化阶段固定200kHz,进入数据传输阶段再切换,从不出事。
5.2 问题2:读扇区数据令牌超时,CMD17发出后死在等待0xFE
现象是发送CMD17后卡正确返回0x00应答,但后续读数据起始令牌时一直读不到0xFE。原因可能是时钟频率太高:读数据虽是卡主动推数据,但数据的移位由SCLK驱动,频率过高时卡内部数据输出跟不上,令牌前导的0xFF不足。我遇到过的另一类原因是卡的供电电压跌落,在写入过程中VCC被拉低到2.7V以下,卡内部电压监测触发掉电保护,数据端口直接冻结。解决的办法是按顺序排查:先用示波器量VCC,确保带载后不低于3.0V;再把SPI时钟从25MHz降到10MHz,多数卡在这个频率下都能稳定输数据。如果这两步都没用,换成逻辑分析仪看CMD17应答后的MISO波型,如果没有任何0xFF之外的电平变化,说明卡根本没进入数据输出状态,可能是CMD17的参数地址超出了卡的容量范围。SDHC卡按块寻址,地址是扇区号而非字节地址,SDSC卡要传字节地址,FATFS传下来的LBA_t是扇区号,底层函数里如果不做左移9位的换算,会在SDSC卡上读到随机地址的数据。
5.3 问题3:写文件后断电,再上电看到乱码、文件消失
这是数据记录类设备最让人头疼的问题。现象是设备正常运行时文件写入正常,但一旦掉电(比如用户拔电源),再上电后f_mount能成功,但之前写好的文件内容乱码,甚至文件名都读不出来。原因是FATFS的写流程要分两步:先写数据扇区,再更新FAT表和目录项。如果掉电发生在两步之间,数据扇区可能是新的,但FAT表里还指向老数据,文件系统就处于不一致状态。更糟的是,部分低成本SD卡对突然断电后的写状态保护做得不好,扇区写到一半卡内部逻辑崩溃,连读都读不出来。
解决思路分三层。第一层,每次写完一批数据后调用f_sync(),它会刷FATFS内部缓冲区并把FAT表与目录项更新落盘,代价是增加几次扇区写操作,但保证文件系统的元数据一致。第二层,如果产品要求极端掉电安全,应用层实现双缓冲策略:同一条日志轮流写两个文件,写入时记录序号,启动时比较两个文件确定哪个有效。第三层,SD卡内部并没有掉电保护机制,本质上是一个没有电容储能、没有日志式文件系统的简易存储,别拿它跟eMMC比。对重要参数采用eeprom或NOR Flash存放,SD卡只存非关键日志,这是最稳的产品划分。
5.4 问题4:SDXC大容量卡在SPI模式下挂载失败或容量为0
现象是插上64GB或128GB的卡,SD卡的版本检测显示支持SDHC,ACMD41初始化也返回0x00,但disk_ioctl返回的容量异常,FATFS格式化失败。原因一:不少SDXC卡固件里只验证SD模式时序,SPI模式下的ACMD41应答不稳定,出现偶发初始化失败;原因二:SDXC卡出厂格式化为exFAT,而FATFS的exFAT支持需要单独使能FF_FS_EXFAT宏,没有使能时f_mount会返回FR_NO_FILESYSTEM。我遇到过的现象是65536GB这种荒谬的容量值,根源是CSD寄存器解析时把C_SIZE的位宽读错,SDXC的C_SIZE字段是22位,代码按32位读,溢出到符号位成负数,在UI层显示成异常大容量。
产品上我给出的建议是明确给用户写清楚“支持SDHC标准卡,32GB以内”,测试时覆盖8GB/16GB/32GB几个常见品牌型号,而不是在说明书上写“支持最大128GB”。SPI模式本身就不是SDXC的推荐工作模式,拿大容量卡硬上,后续数据包错、挂载失败这类问题会不断冒出来。
5.5 问题5:f_open返回FR_DENIED或打开后写入失败
现象是新建文件返回FR_DENIED,或打开已有文件后f_write返回FR_WRITE_PROTECTED。FR_DENIED最直接的原因是卷上有写保护或文件本身被标记为只读。排查顺序是第一步看卡座的写保护开关是否拨到LOCK位置,卡座上的开关是机械触点,拨到LOCK后卡内部会把OCR的bit20置1,FATFS检查到写保护会拒绝f_open的写模式。第二步看FATFS挂载时是否用了只读模式,如果ffconf.h里设置FF_FS_READONLY为1,那么编译出的FATFS所有写操作都会返回FR_WRITE_PROTECTED,这个问题只在产品换配置时容易踩到——我从某开发者的工程里就把FF_FS_READONLY误设成1,导致整个系统只能读不能写。
另外注意f_open的创建标志选择。日志类场景用FA_OPEN_APPEND是追加写,对已有文件不会覆盖;如果要重建文件必须用FA_CREATE_ALWAYS,否则带FA_OPEN_EXISTING这种标志时文件不存在会返回FR_DENIED。我用f_open失败后打印错误码的习惯,比瞎改参数省很多时间。
6. 性能验证与可靠性测试:几组实测习惯
6.1 吞吐率与读写块大小的关系
SPI模式读写性能与FATFS请求的块大小强相关。连续写1MB数据,按512字节单扇区写与按4096字节多扇区写,耗时差异能到4倍以上。原因在协议层:每个扇区单独写都要发CMD24、等待0x05应答、再等卡内部擦除忙,每扇区多出约1ms协议开销,而多扇区写只用一次命令开销。所以应用层写日志时,不要每产生一条日志就f_write一次,而是攒到4KB或8KB再写。
我用一组固定测试数据校准平台的性能:写入512字节、1024字节、4096字节三种块大小,记录每秒写入字节数,得到一张性能曲线。这张曲线对产品定型很关键——如果产品宣称“最大记录速率1MB/s”,那实际写入块至少需要8192字节才能达到。低于这个块大小的频繁小写,吞吐率可能只有300KB/s,文档里的“1MB/s”就成了虚假宣传。
6.2 掉电可靠性测试与数据完整性校验
掉电测试不能只在调试器里做。我的方法是写一个测试固件:循环写入带序列号和校验和的数据块,每写完一段调用f_sync,然后通过GPIO控制一个继电器随机切断板卡电源。上电后固件自动扫描文件,逐条校验序列号连续性和数据校验和。每次测试跑500次掉电周期,记录失败率。这个过程用简单的bash脚本控制串口和电源即可:
while true; do python3 powerctl.py off && sleep 0.3 && python3 powerctl.py on sleep 2 if ! python3 check_datalog.py /dev/ttyUSB0; then echo "掉电测试失败,保留现场日志" break fi done这个测试脚本的价值在于能暴露两类问题:一是f_sync没有真正落盘时,重启后数据丢失;二是FATFS缓冲区脏标志管理出错,多卷同时挂载时换卷数据被错误写入。脚本跑过500次循环不失败,我才会把固件放给用户验证。
6.3 调试器里格式化SD卡的另一个教训
前几年做某跨平台系统的存储模块时,我发现SD卡在PC上格式化和在板子上格式化后的表现并不完全一致。FATFS的f_mkfs在格式化大容量SDHC卡时,若FAT表占用的扇区数计算依赖disk_ioctl返回的GET_BLOCK_SIZE,有些小厂卡返回的参数不准确,格式化出来的文件系统簇边界与卡内部物理擦除块错位,写入速度会波动很大。后来我养成了一个习惯:新卡先在PC上用标准FAT32格式化一次性成功后再插板,板子上的f_mkfs只作为备用,并且格式化前强制卸载所有卷,防止FATFS内部缓冲区里残存旧数据污染新文件系统。
调试SD卡协议时还有一条经验很有用:做一个只发命令、不读写数据的“裸命令模式”固件,把CMD0到CMD58过程中每一个R1应答字节都通过串口打印出来。配合逻辑分析仪,初始化序列的问题基本都能在几分钟内定位,比用f_mount失败后猜测原因高效得多。希望这些实践能帮你把SPI模式下的SD卡读写做成一个稳定、可交付的存储方案。
本文还有配套的精品资源,点击获取