ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

STM32读SD卡与FATFS移植:从扇区读写到掉电恢复

STM32读SD卡与FATFS移植:从扇区读写到掉电恢复

简介:这是一套面向STM32嵌入式学习者的SD卡读取与FATFS文件系统实践工程,围绕SPI/SDIO接口访问、扇区读写驱动、FATFS移植配置以及文件打开、读取、写入与关闭操作展开,适合具备C语言与STM32基础、希望掌握嵌入式文件管理的开发者。压缩包共229个文件,以83个C源码和83个头文件为主,配合36个汇编启动文件,并含网页说明、文本记录及KEIL工程配置,整体约1.48MB,便于导入Keil对照学习。已有3377人学习下载。工程保留完整项目组织与调试线索,涵盖CS、MISO、MOSI、SCK引脚连接,SD卡初始化与扇区读写,扇区与簇大小配置,SD卡未检测到、文件不存在、读写失败等错误处理,以及串口或LCD结果验证,可帮助理解从底层驱动到文件系统调用的完整链路,也能为数据记录、远程监控等场景提供文件读写参考。

1. 需求与边界:为什么STM32读SD卡不能只写一个f_open

很多人做基于STM32的SD卡读取FATFS文件系统,先把SDIO或SPI例程跑通,看到f_mount返回FR_OK就以为收工;真正交付时,冷启动偶发识别失败、拔卡后f_open卡死、写日志掉电后目录项损坏,才是答辩现场最容易翻车的地方。这套方案面向要交毕设、课设或做数据记录模块的读者,把需求拆成三层:底层让SD卡在规定上电时序和供电条件下稳定识别,中间让FATFS在扇区读写正确的前提下管理目录与簇链,业务层只关心文件路径、读写长度和异常恢复。后面按硬件选型、驱动封装、FATFS移植、避坑排查到验收矩阵展开,给出可直接改的代码骨架与参数表。

2. 总体方案设计:从SDIO/SPI链路到FATFS分层的职责划分

STM32读SD卡的项目最容易写成“例程拼接”:SD卡驱动抄一段,diskio.c抄一段,业务层再塞一个f_open,串口能打印文件名就结束。这样的工程在实验室桌面上大概率能跑,换一张卡、换一块板、加一个热插拔动作就开始出问题。合理的做法是先把整条链路拆成硬件介质、底层扇区访问、FATFS文件系统、业务记录四层,每层只暴露有限接口,错误码逐层向上映射。这样调试时能快速判断是卡没识别、扇区读错、文件系统没挂上,还是业务层把文件句柄用错了。

2.1 硬件链路与存储介质选型:SDIO、SPI和卡类型怎么定

STM32与SD卡之间常见两条链路:SDIO和SPI。SDIO支持1位或4位数据线,吞吐高,适合连续采样记录、音频片段、图像缓存这类对写入速度有要求的场景;代价是引脚多、时钟和DMA配置更细,PCB上数据线等长和上拉电阻也不能太随意。SPI链路引脚少、时序直观,调试时用逻辑分析仪很容易看清CMD和响应,但速度受SPI时钟和协议开销限制,适合参数配置、少量日志、课设演示。若项目要求“稳定读出配置文件和小数据文件”,SPI已经够用;若要求“持续写1MB以上日志且不能明显丢采样”,优先SDIO 4位模式。

卡类型也要先定。SDSC、SDHC、SDXC的寻址方式不同,SDHC/SDXC按扇区寻址,SDSC按字节寻址,底层diskio若不做转换,就会出现“能识别但读出来全是错数据”的玄学现象。通常的做法是工程只支持SDHC/SDXC,格式化成FAT32,减少地址换算分支;如果必须兼容老卡,就在卡初始化后读OCR里的CCS位,记录卡类型,在disk_read/disk_write里统一换算成扇区号。文件系统侧还要注意FATFS对exFAT的支持需要额外配置,课设阶段用FAT32最省事,但单文件不要超过4GB。

链路/介质典型引脚吞吐与复杂度适用场景主要注意点
SDIO 4位CLK、CMD、D0-D3、CD高吞吐,配置复杂连续数据记录、较大文件读写DMA、缓存一致性、时钟相位
SDIO 1位CLK、CMD、D0、CD中等吞吐,配置略简中等速率日志仍要处理DMA收尾
SPISCK、MOSI、MISO、CS、CD低到中等吞吐,调试直观课设、配置读取、小日志命令响应、CS时序、忙等待
SDHC/SDXC同上扇区寻址4GB以上卡不要按字节地址直接下发
SDSC同上字节寻址老卡兼容地址换算容易翻车

供电和卡座细节常被忽略。SD卡典型工作电压是3.3V,卡座附近要有去耦电容,插拔瞬间的浪涌会让MCU复位或卡进入异常状态;卡检测脚CD最好上拉并加简单滤波,热插拔中断里只置标志,不在中断里直接跑f_mount。若板子有电平转换或长排线,先降时钟到400kHz做识别,识别成功后再升频,这条血泪经验能排掉不少“换卡就好”的假故障。

2.2 软件分层与目录结构:BSP、diskio、FATFS、业务层四层边界

软件分层建议按四层落地:BSP层负责卡上电、时钟、命令、扇区读写和卡检测;diskio层只做FATFS要求的五个函数,把BSP的返回值翻译成RES_OK、RES_ERROR、RES_PARAMERR;FATFS层使用ff.c/ff.h,不直接碰GPIO;业务层只调用f_open、f_read、f_write、f_readdir,不直接发CMD。这样划分以后,换SPI卡驱动只改BSP,换文件系统版本只改diskio适配,业务代码基本不动。

目录可以按下面这种方式组织,核心是让每个文件的责任一眼能看出来:

project/ bsp/ sd_card.c /* 卡上电、识别、扇区读写 */ sd_card.h fatfs/ diskio.c /* BSP到FATFS的适配层 */ ff.c ff.h ffconf.h app/ file_log.c /* 业务层:挂载、遍历、读写 */ file_log.h main.c

BSP层对外不要暴露“读第几个块”这种模糊接口,统一成“读扇区号+扇区数量+缓冲区”。下面这组声明是常见做法,参数类型按所用FATFS版本微调,关键是错误码和扇区语义要固定:

/* sd_card.h:BSP层只暴露扇区级接口,业务层不要直接调用 */ #ifndef SD_CARD_H #define SD_CARD_H #include <stdint.h> typedef enum { SD_OK = 0, SD_ERR_TIMEOUT, SD_ERR_RESPONSE, SD_ERR_CRC, SD_ERR_PARAM } sd_status_t; sd_status_t sd_card_init(void); /* 上电识别,失败可重试 */ sd_status_t sd_read_blocks(uint32_t sector, /* 扇区号,不是字节地址 */ uint8_t *buf, uint32_t count); /* 连续扇区数量 */ sd_status_t sd_write_blocks(uint32_t sector, const uint8_t *buf, uint32_t count); uint32_t sd_get_sector_count(void); /* 供disk_ioctl使用 */ uint8_t sd_is_present(void); /* 卡检测脚状态 */ #endif

这组接口的边界很清楚:sector按扇区计数,buf由调用者保证有效,count为0时直接返回参数错误。sd_card_init内部可以重试3次,但每次重试前要重新走一遍上电延时和400kHz识别,不能只在原状态上反复发CMD。sd_is_present只反映卡座检测脚,不代替disk_status;真正判断卡是否可用,还要看最近一次扇区读写是否成功。

2.3 关键参数表:扇区、簇、缓存、栈与任务优先级

FATFS的性能和稳定性,很大一部分在参数表里就决定了。扇区大小通常固定512字节,卡内部物理块可能更大,但FATFS按512字节逻辑扇区访问最稳;簇大小在格式化时决定,簇太小会让大文件簇链变长、写日志时频繁改FAT表,簇太大则小文件浪费空间。数据记录类项目常用4KB或8KB簇,拿到PC上也能正常识别;如果卡容量很大又只存少量文件,32KB簇可以减少FAT更新次数,但小文件多时不合算。

参数/配置建议值作用改动时看什么
FF_MAX_SS512最大扇区大小不要小于卡逻辑扇区
FF_MIN_SS512最小扇区大小与disk_ioctl返回值一致
FF_USE_LFN1或2长文件名需要中文名时配编码
FF_MAX_LFN255长文件名缓冲占用栈或堆,别开太大
FF_VOLUMES1卷数量单卡项目填1
FF_FS_REENTRANT0或1多任务重入多任务写同一卷时开1并配互斥
FF_USE_FASTSEEK1快速定位大文件追加写有用
任务栈2KB起步FATFS调用与长文件名开长文件名后留余量
文件任务优先级中低避免抢占采样中断写卡不要放中断里

多任务项目要特别小心:FATFS对象不是线程安全的,两个任务同时f_open同一卷,目录项可能互相踩。通常的做法是文件操作统一收口到一个文件任务,其他任务通过队列发“写一条记录”的请求;如果必须多任务直接调用,就开FF_FS_REENTRANT并在diskio里实现互斥。SDIO中断优先级要高于文件任务,但文件任务里不能关中断等卡忙太久;超时时间按最坏扇区写时间留够,常见做法是单次扇区操作超时50到200ms,整文件同步再放宽。

3. 底层驱动实现:SD卡初始化、扇区读写与DMA收尾

底层驱动是整条链路里最容易被“例程能跑”掩盖的部分。卡识别阶段的命令顺序、响应类型、超时计数,扇区读写阶段的地址语义、DMA完成标志、错误恢复,任何一处含糊,到了FATFS层都会表现成FR_DISK_ERR或FR_NO_FILESYSTEM。调试时不要一上来就打印文件名,先把“卡能不能识别、扇区0能不能读、读出来是不是MBR或引导扇区”这三件事分开验证,后面会省很多时间。

3.1 上电时序与卡识别:CMD0/CMD8/ACMD41的检查点

SD卡上电后不能立刻高速发命令。常见流程是:给电、延时至少1ms让卡内部稳压完成,把时钟降到400kHz以下,发CMD0进入idle,发CMD8检查电压和卡版本,再循环发ACMD41等待卡退出idle。CMD8有响应说明卡支持2.7V到3.6V电压范围,后续可以按SDHC/SDXC处理;CMD8无响应可能是老卡或SPI模式,不能直接判死。ACMD41要带HCS位循环发,直到响应中的busy位清零,循环次数要有上限,失败后重新上电比死等更有效。

SPI模式的检查点不同:CS拉低后先发至少74个时钟让卡进入SPI模式,再发CMD0,接着CMD8、ACMD41。SPI模式没有SDIO那些中断标志,重点看命令响应字节和忙状态。下面这段是SDIO模式的识别骨架,寄存器名按所用HAL或标准外设库替换,逻辑顺序不变:

/* 识别流程示意:先低速,再根据CMD8和ACMD41结果判断卡类型 */ sd_status_t sd_card_init(void) { sd_power_on(); /* 卡座上电,延时至少1ms */ sd_clock_set_400khz(); /* 识别阶段必须低速 */ if (sd_send_cmd(CMD0, 0, SD_RESP_NONE) != SD_OK) { return SD_ERR_TIMEOUT; /* 复位失败先查供电和CMD线 */ } uint32_t cmd8_resp = 0; if (sd_send_cmd(CMD8, 0x1AA, SD_RESP_R7, &cmd8_resp) == SD_OK) { if ((cmd8_resp & 0x0FFF) != 0x01AA) { return SD_ERR_RESPONSE; /* 电压范围不匹配 */ } g_sd.is_sdhc_capable = 1; } else { g_sd.is_sdhc_capable = 0; /* 老卡或SPI模式,后续按字节寻址处理 */ } for (uint32_t retry = 0; retry < 2000; retry++) { if (sd_send_acmd41(g_sd.is_sdhc_capable) == SD_OK) { break; } sd_delay_ms(1); if (retry == 1999) { return SD_ERR_TIMEOUT; /* 不要无限等,重新上电再试 */ } } if (sd_read_ocr(&g_sd.ocr) != SD_OK) { return SD_ERR_RESPONSE; } g_sd.is_sdhc = (g_sd.ocr & (1UL << 30)) != 0; /* CCS位决定扇区寻址 */ sd_clock_set_high(); /* 识别成功后再升频 */ return SD_OK; }

这段代码的关键不是寄存器写法,而是三个检查点:CMD0必须成功,CMD8用来判断电压和卡能力,ACMD41必须带超时退出。g_sd.is_sdhc决定后面扇区号是否直接下发;如果卡是SDSC,disk_read里要把扇区号乘以512再发字节地址。识别失败时先看供电和卡座,再看CMD线是否上拉、时钟是否真的降到400kHz,最后才怀疑卡坏了。换卡能好不代表驱动没问题,很多接触不良和时序余量不足会伪装成“这张卡不兼容”。

3.2 扇区读写封装:diskio.c的四个函数怎么写

diskio.c是FATFS和BSP之间的翻译层,职责只有三件:报告卡状态、读写扇区、返回容量和同步。不要在这里做文件路径解析,也不要在disk_read里做重试到天荒地老。常见做法是单次扇区操作失败后重试1到2次,仍失败就返回RES_ERROR,让上层决定是否重新挂载或提示用户。下面这版代码把BSP错误码统一映射,读多扇区时直接调BSP的连续读,避免在diskio里拆成单扇区循环:

/* diskio.c:FATFS适配层,参数类型按所用FATFS版本微调 */ #include "ff.h" #include "diskio.h" #include "sd_card.h" DSTATUS disk_initialize(BYTE pdrv) { if (pdrv != 0) return STA_NOINIT; if (sd_card_init() != SD_OK) { return STA_NOINIT; /* 卡未识别,FATFS不会继续挂载 */ } return 0; } DSTATUS disk_status(BYTE pdrv) { if (pdrv != 0) return STA_NOINIT; if (!sd_is_present()) return STA_NODISK; return 0; /* 更严谨的做法是记录最近一次IO错误 */ } DRESULT disk_read(BYTE pdrv, BYTE *buff, LBA_t sector, UINT count) { if (pdrv != 0 || buff == 0 || count == 0) return RES_PARAMERR; for (UINT retry = 0; retry < 2; retry++) { if (sd_read_blocks((uint32_t)sector, buff, count) == SD_OK) { return RES_OK; } } return RES_ERROR; } DRESULT disk_write(BYTE pdrv, const BYTE *buff, LBA_t sector, UINT count) { if (pdrv != 0 || buff == 0 || count == 0) return RES_PARAMERR; for (UINT retry = 0; retry < 2; retry++) { if (sd_write_blocks((uint32_t)sector, buff, count) == SD_OK) { return RES_OK; } } return RES_ERROR; } DRESULT disk_ioctl(BYTE pdrv, BYTE cmd, void *buff) { if (pdrv != 0) return RES_PARAMERR; switch (cmd) { case CTRL_SYNC: return sd_wait_write_done() ? RES_OK : RES_ERROR; case GET_SECTOR_COUNT: *(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 = 1; /* 擦除块大小,不确定时填1最稳 */ return RES_OK; default: return RES_PARAMERR; } }

参数说明要落到调试动作上:sector是逻辑扇区号,不是字节地址;count是连续扇区数量,多扇区读要确认DMA缓冲区长度足够;buff不能是栈上很小的临时数组,长文件名和目录扫描时FATFS会传512字节以上缓冲。disk_status返回STA_NODISK后,上层f_open通常会返回FR_NOT_READY或FR_DISK_ERR,业务层要能识别并停止写入,而不是继续循环调用。CTRL_SYNC必须等卡内部写完成,否则f_sync返回成功但数据还在卡缓存里,掉电就会丢。

3.3 DMA与中断收尾:传输完成、错误标志和超时恢复

SDIO加DMA能明显提高吞吐,但收尾顺序不对会出现“偶发读错一个扇区”。常见问题是DMA传输完成中断先到,代码立刻把缓冲区交给FATFS,而SDIO的data end标志还没处理;或者DCache开启时,DMA写进内存的数据没invalidate,CPU读到旧缓存。通常的做法是在DMA完成回调里只置标志,文件任务等到“DMA完成+SDIO无错误+超时未到”三个条件都满足再使用缓冲区。写方向反过来,先clean缓存再启动DMA,等传输完成再检查卡忙。

/* 以常见HAL接口示意:DMA读扇区,回调里清标志,任务里等超时 */ static volatile uint8_t g_sd_dma_done; static volatile uint8_t g_sd_dma_error; void HAL_SD_RxCpltCallback(SD_HandleTypeDef *hsd) { g_sd_dma_done = 1; /* 回调里不要跑FATFS */ } void HAL_SD_ErrorCallback(SD_HandleTypeDef *hsd) { g_sd_dma_error = 1; } sd_status_t sd_read_blocks_dma(uint32_t sector, uint8_t *buf, uint32_t count) { g_sd_dma_done = 0; g_sd_dma_error = 0; if (HAL_SD_ReadBlocks_DMA(&hsd, buf, sector, count) != HAL_OK) { return SD_ERR_RESPONSE; } uint32_t start = HAL_GetTick(); while (!g_sd_dma_done && !g_sd_dma_error) { if ((HAL_GetTick() - start) > 200) { /* 单次多扇区读的超时上限 */ HAL_SD_Abort(&hsd); return SD_ERR_TIMEOUT; } } if (g_sd_dma_error) { HAL_SD_Abort(&hsd); return SD_ERR_RESPONSE; } return SD_OK; }

这段代码的参数要按卡和芯片能力改:count不要一次开到几十个扇区,先按4到8个扇区测,稳定后再加;超时200ms是单次读的保守值,写操作要更长。错误恢复顺序一般是先Abort当前传输,再清SDIO和DMA标志,然后重新初始化卡,最后重试一次;仍失败就上报业务层,不要在原地死循环。若芯片开了DCache,读DMA前invalidate对应缓冲区,写DMA前clean,否则会出现“串口打印数据对、文件里数据错”的黑匣子问题。

4. FATFS移植与文件操作:挂载、遍历、读写和掉电一致性

FATFS本身不复杂,难的是配置和业务习惯。ffconf.h里一个长文件名选项会改变栈占用,一个重入选项会改变链接方式,一个扇区大小配置不和disk_ioctl对齐就会挂载失败。文件操作也要按场景选API:只读配置用f_open配FA_READ,追加日志用FA_OPEN_ALWAYS加f_lseek到末尾,目录展示用f_opendir和f_readdir。把这几类操作写成固定函数,业务层就不容易在中断里打开文件,也不会忘记f_close。

4.1 ffconf.h关键配置:长文件名、扇区大小、编码与重入

配置先求一致,再求功能。FF_MAX_SS和FF_MIN_SS都设512,和disk_ioctl的GET_SECTOR_SIZE返回值保持一致;FF_USE_LFN按是否需要长文件名开,课设只读8.3文件名可以关掉省空间,需要中文文件名就开2或3并配FF_MAX_LFN。FF_FS_REENTRANT只有在多任务直接调用FATFS时才开,开了以后还要提供ff_mutex_create/ff_mutex_take等同步对象,否则链接都过不去。下面是一组偏稳妥的配置片段,具体宏名按所用FATFS版本核对:

/* ffconf.h:单卡、512字节扇区、长文件名、单任务收口 */ #define FF_MAX_SS 512 #define FF_MIN_SS 512 #define FF_USE_LFN 2 /* 1=静态缓冲,2=栈上缓冲,3=堆 */ #define FF_MAX_LFN 255 #define FF_LFN_UNICODE 1 /* 需要中文名时开,不需要可关 */ #define FF_VOLUMES 1 #define FF_FS_REENTRANT 0 /* 文件操作统一在一个任务里做 */ #define FF_USE_FASTSEEK 1 #define FF_USE_MKFS 1 /* 课设可留,量产可关 */ #define FF_FS_NORTC 1 /* 无RTC时不要伪造时间 */

FF_USE_LFN选2会把长文件名缓冲放栈上,任务栈就要留够;选1用静态缓冲省栈但占RAM;选3用堆,碎片多了会出问题。FF_LFN_UNICODE开1后,f_readdir返回的fname编码要和显示端一致,串口打印前确认是UTF-8还是本地编码,不然中文名会变乱码。FF_USE_MKFS在课设阶段方便现场格式化,交付版本建议关掉,避免误操作把卡格式化。配置改完先做一次“挂载+列根目录”,别直接跑写入测试。

4.2 挂载与文件遍历:f_mount、f_opendir、f_readdir的完整流程

挂载流程通常是先f_mount(&fs, "0:", 1)强制挂载,返回FR_OK后再打开目录。f_opendir打开根目录用“0:/”,f_readdir循环读,直到返回FR_OK且fno.fname[0]为0表示结束。每次读目录前不要把fno清空,f_readdir会填fname、fattrib、fsize;长文件名开启后fname可能超过8.3格式,显示端要按字符串处理。目录遍历完必须f_closedir,卸载时f_mount(NULL, "0:", 0)释放FATFS对象,热插拔前先卸载再重新初始化卡,能减少卡状态和文件系统状态不一致。

/* 挂载并列出根目录,适合课设演示和交付前自检 */ FATFS g_fs; FRESULT list_root(void) { DIR dir; FILINFO fno; FRESULT fr = f_mount(&g_fs, "0:", 1); if (fr != FR_OK) return fr; fr = f_opendir(&dir, "0:/"); if (fr != FR_OK) { f_mount(NULL, "0:", 0); return fr; } for (;;) { fr = f_readdir(&dir, &fno); if (fr != FR_OK || fno.fname[0] == 0) break; if (fno.fattrib & AM_DIR) { printf("[DIR ] %s\r\n", fno.fname); } else { printf("[FILE] %s %lu\r\n", fno.fname, (unsigned long)fno.fsize); } } f_closedir(&dir); f_mount(NULL, "0:", 0); /* 演示结束释放卷,热插拔前也建议这样做 */ return fr; }

这段代码的参数不多,但错误处理不能省:f_mount失败先看disk_initialize是否返回STA_NOINIT,再看卡是否格式化;f_opendir失败常见是路径写成“0:”少了斜杠,或者根目录簇链读错。f_readdir返回FR_OK但fname为空是正常结束,不要把它当错误。fno.fsize在SDHC卡上按32位打印够用,超过4GB文件要按项目需求换类型。目录遍历不要放在高频中断里,长文件名和扇区读会拖长中断关闭时间。

4.3 写入与同步:f_write、f_sync、缓存刷新和掉电保护

写文件先明确打开模式:FA_OPEN_ALWAYS表示不存在就创建、存在就打开;FA_WRITE配FA_READ可以读写;追加日志用f_lseek(&fil, f_size(&fil))定位到末尾。f_write的第三个参数是请求写入字节数,第四个参数返回实际写入字节数,必须检查bw是否等于请求长度,部分写通常意味着卡满、扇区错误或文件系统异常。写完一条记录调用f_sync,把FATFS缓存和卡缓存刷下去;f_close也会同步,但掉电发生在f_close之前就来不及了。

/* 追加一条记录并同步,适合低频日志;高频日志要按批写 */ FRESULT append_record(const char *path, const uint8_t *data, UINT len) { FIL fil; UINT bw = 0; FRESULT fr = f_open(&fil, path, FA_OPEN_ALWAYS | FA_WRITE | FA_READ); if (fr != FR_OK) return fr; fr = f_lseek(&fil, f_size(&fil)); /* 定位到文件末尾 */ if (fr == FR_OK) { fr = f_write(&fil, data, len, &bw); } if (fr == FR_OK && bw != len) { fr = FR_DISK_ERR; /* 部分写按磁盘错误处理 */ } if (fr == FR_OK) { fr = f_sync(&fil); /* 记录落盘,掉电保护的关键一步 */ } f_close(&fil); return fr; }

掉电一致性不要指望单次f_sync解决所有问题。更稳的做法是双文件轮换:A、B两个日志文件轮流写,每条记录带序号和CRC,写完f_sync再更新一个很小的状态文件;上电后比较两个日志的序号,选最后一条CRC正确的记录继续。f_sync频率太高会拖慢写入并增加卡磨损,太低又丢数据,通常按“每10到50条”或“每200ms”批量同步一次,关键事件立即同步。文件不要反复创建删除,日志按日期或序号滚动,旧文件归档而不是每次覆盖。

5. 避坑与排查:SD卡识别失败、挂载FR_NO_FILESYSTEM和写坏卡的现场处理

现场排查要有顺序:先看卡检测和供电,再看卡识别,再看扇区0能不能读,再看FATFS挂载,最后才看文件操作。很多“文件系统坏了”其实是disk_read读错扇区,很多“卡不兼容”其实是识别阶段时钟没降下来。下面几条是项目里最容易翻车的点,每条按现象、原因、解决展开,照着顺序查能少走弯路。

5.1 识别与挂载类现场

现象:冷启动偶尔disk_initialize返回STA_NOINIT,按复位键又正常。原因通常在上电时序和电源余量:卡座上电后延时不够,CMD0发得太早;或者3.3V在插卡瞬间被拉低,MCU虽然没复位,卡已经进入异常状态;还有一种是CMD线或数据线上拉不足,低速能识别,升频后偶发CRC错。解决时先把识别阶段固定到400kHz,上电延时从1ms加到5ms试,卡座电源脚就近加去耦,CD检测脚加RC滤波。初始化函数里加重试3次,每次重试前重新上电并重新走CMD0/CMD8/ACMD41,不要在原状态上反复发ACMD41。串口把每次失败的错误码打出来,区分超时、响应错和CRC错,比只打印“init fail”有用得多。

现象:f_mount返回FR_NO_FILESYSTEM,但卡在PC上能正常打开。先别怀疑FATFS源码,优先查三件事:卡是不是exFAT或NTFS,FF_MAX_SS和GET_SECTOR_SIZE是否一致,disk_read读0扇区读出来的内容是不是引导扇区。PC能识别exFAT不代表FATFS开了exFAT支持;卡容量大于32GB时默认格式化常是exFAT,课设阶段用工具重新格式化成FAT32最稳。读0扇区打前16字节,正常应看到跳转指令和OEM名,末尾0x55AA;如果全是0或全FF,说明disk_read的扇区号或DMA缓冲区有问题。还有一种隐蔽情况:SDSC卡按扇区号直接下发,卡按字节地址解释,读出来偏移512倍,挂载自然失败。

现象:换一张卡就挂载失败,旧卡一直正常。这种“卡兼容性”多半是容量和寻址差异。SDHC/SDXC的sector直接下发,SDSC要乘512;有的卡对CMD8响应慢,ACMD41循环次数不够;有的卡在SPI模式下需要额外时钟。解决时把卡类型记录到全局变量,disk_read/disk_write按类型换算地址;ACMD41超时从2000次加到5000次并加1ms间隔;SPI模式先发80个时钟再发CMD0。不要用“只支持某品牌卡”糊弄验收,至少准备4GB、8GB、16GB三张不同容量的卡各测一遍。

5.2 读写与一致性类现场

现象:f_open返回FR_DISK_ERR,单扇区读正常,多扇区读偶发错。常见原因是DMA缓冲未对齐或DCache未处理。SDIO DMA通常要求缓冲区4字节对齐,栈上临时数组可能不满足;开了DCache的芯片,DMA写内存后CPU读的是旧缓存,表现就是“第一次对,第二次错”。解决时把扇区缓冲定义成全局或静态的32字节对齐数组,读DMA前invalidate,写DMA前clean;多扇区count先降到1做对比,确认单扇区稳定后再逐步加。还有一种情况是disk_read返回RES_OK但count没读完,BSP内部超时提前退出,FATFS就会拿到半截数据,随后报FR_INT_ERR。

现象:写日志过程中拔卡或掉电,重新上电文件长度为0,目录里出现乱码文件。原因通常是f_write只写到FATFS缓存,没有f_sync;或者f_close前掉电,目录项和FAT表没更新;频繁创建删除小文件还会让目录项碎片化。解决时关键记录写完立即f_sync,普通记录按批同步;日志采用双文件轮换加序号和CRC,上电扫描最后一条完整记录;写文件期间检测到卡拔出,立即停止写入并卸载卷,不要继续f_write。目录里出现乱码文件时,先用PC备份数据,再在测试卡上复现,确认是掉电时机问题还是disk_write返回了假成功。

现象:连续记录一段时间后写入变慢,卡发热,偶尔f_sync超时。先看簇大小和同步频率。簇太小会让FAT表频繁更新,每条记录都f_sync会让卡内部垃圾回收反复触发;日志文件反复打开关闭也会增加目录项写入。解决时格式化用4KB或8KB簇,日志文件保持打开并批量写,按时间或条数f_sync;旧日志按日期滚动,不要在一个文件里无限追加;如果卡已经写得很满,留出10%以上空闲空间。测试时用逻辑分析仪看单次多扇区写耗时,如果从几毫秒涨到几百毫秒,先换卡对比,再查电源和写放大。

6. 验收与答辩:从冷启动到掉电恢复的测试矩阵

验收不要只看串口打印“mount ok”。我一般把测试分成冷启动、热插拔、文件操作、异常掉电四组,每组有明确的通过标准,答辩时按顺序演示,现场不容易乱。下面这张矩阵可以直接改成自己的验收表:

测试项操作步骤通过标准
冷启动识别断电重启20次,每次挂载并读根目录20次全部FR_OK,无卡死
多卡兼容4GB/8GB/16GB卡各格式化FAT32后测试均能挂载、列目录、读文件
长文件名建中文名和超过8.3格式的文件f_readdir显示正确,f_open可读
热插拔运行中拔卡,再插回拔卡后报错不卡死,插回可重新挂载
掉电恢复写日志时随机断电3次上电后能识别最后完整记录,坏记录不扩散
吞吐连续写1MB日志并计时达到项目采样率要求,无超时错误

答辩演示顺序建议固定成:上电、挂载、列目录、读一个配置文件、追加一条日志、拔卡触发错误、插回重新挂载。每一步把返回值打印出来,FR_OK和错误码都显示,评委问“拔卡会怎样”时直接现场演示,比口头解释可信。代码里提前留一个测试命令,比如串口输入“test”就跑一遍根目录遍历和1KB读写,避免现场手忙脚乱改代码。

从那以后我每次交付前都强制走一遍“冷启动20次、拔卡3次、掉电3次”的清单,不再只看第一次f_mount成功就收工。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进