STM32 SD卡FATFS文件系统移植实战:从扇区读写到稳定挂载
发布时间:2026/10/13 13:55:24 锦皓数字建站

简介这份资源面向嵌入式开发初学者与STM32进阶开发者聚焦在KEIL环境下通过SPI或SDIO接口驱动SD卡并移植FATFS文件系统实现文件的打开、读取、写入与关闭等操作帮助读者打通从硬件连接到文件级读写的完整链路。压缩包共229个文件以83个c源文件与83个h头文件为核心配合36个汇编启动文件及uvproj、uvopt等KEIL工程文件另含少量html、txt说明与map、lst编译产物整体约1.48MB工程结构完整可直接编译。资源围绕SD卡初始化、扇区读写、FATFS参数配置与错误处理等关键环节展开涵盖cc936、cc949等多语言编码支持文件便于读者对照源码理解文件系统在资源受限平台上的运行机制。目前已有3372人学习下载适合希望掌握STM32存储方案、积累嵌入式文件管理实战经验的开发者参考借鉴。1. 从一块读不出数据的 SD 卡说起FATFS 到底在 STM32 上扮演什么角色一块 8GB 的 microSD 卡插进 STM32 板子SPI 能读到 CID扇区 0 也能读出 0x55AA 结束标志但f_open就是返回FR_NO_FILESYSTEM——这是很多人在做「基于 STM32 的 SD 卡读取 FATFS 文件系统」时遇到的第一个下马威。问题往往不在 SD 卡本身而在于 FATFS 是一层纯软件的文件系统中间件它不负责底层时序只负责把「扇区读写」翻译成「文件读写」。底层驱动给它的扇区数据只要有一个字节错位整个文件系统就解析失败。这个方案解决的核心诉求很明确让 STM32 能像在电脑上一样对 SD 卡做f_open、f_read、f_write、f_close而不是自己拿扇区地址去硬算 FAT 表。它适合三类人做数据采集记录仪的、做脱机音频/图片存储的、以及需要把传感器数据以文件形式导出给上位机的。前提是你得先把 SD 卡的物理层读写跑通再谈 FATFS 挂载。下面按「底层怎么通、FATFS 怎么接、参数怎么调、坑在哪」的顺序拆开讲。2. 先让 SD 卡在 SPI 模式下稳定读出扇区底层不通FATFS 免谈2.1 为什么多数人先选 SPI 而不是 SDIOSTM32 读 SD 卡有两条路SDIO 和 SPI。SDIO 速度快、支持 4 位宽总线但引脚固定、DMA 配置复杂一旦时序或时钟树没配好调试成本很高。SPI 模式虽然理论速度只有 SDIO 的几分之一但它只用 4 根线CS、SCK、MISO、MOSI任何 STM32 型号都能用逻辑分析仪一挂就能看波形。对于「基于 STM32 的 SD 卡读取 FATFS 文件系统」这个目标SPI 是绝大多数人第一次跑通的选择。常见做法是初始化阶段把 SPI 时钟压到 400kHz 以下等卡进入传输状态后再拉到 10~20MHz。2.2 扇区读写的最小验证代码在接 FATFS 之前必须先用裸扇区读写证明链路是通的。下面这段是 SPI 模式下读单个扇区的核心逻辑基于常见的 HAL 库风格// 读一个 512 字节扇区sector 为扇区号buf 为接收缓冲区 uint8_t sd_read_sector(uint32_t sector, uint8_t *buf) { uint8_t token; uint32_t i; // SDHC 卡扇区地址按块寻址直接传 sectorSDSC 卡需 sector 9 uint8_t cmd[6] {0x51, 0x00, 0x00, 0x00, 0x00, 0xFF}; cmd[1] (sector 24) 0xFF; cmd[2] (sector 16) 0xFF; cmd[3] (sector 8) 0xFF; cmd[4] sector 0xFF; SD_CS_LOW(); if (sd_send_cmd(cmd, 0x00) ! 0x00) { // CMD17 读单块R1 应为 0x00 SD_CS_HIGH(); return 1; } // 等待数据令牌 0xFE超时约 200ms for (i 0; i 0xFFFF; i) { token spi_read_byte(); if (token 0xFE) break; } if (token ! 0xFE) { SD_CS_HIGH(); return 2; } for (i 0; i 512; i) buf[i] spi_read_byte(); spi_read_byte(); spi_read_byte(); // 丢弃 2 字节 CRC SD_CS_HIGH(); spi_read_byte(); // 补 8 个时钟释放总线 return 0; }逻辑说明CMD17 是「读单块」命令R1 响应为 0x00 表示卡已接受。之后卡会拉低 MISO 并发送 0xFE 数据令牌再跟 512 字节数据。参数上sector的寻址方式取决于卡类型——SDHC/SDXC 按块寻址SDSC 按字节寻址这一点搞错会读出全 0 或乱码。超时计数 0xFFFF 是经验值SPI 时钟 400kHz 时约 200ms够用。2.3 初始化流程里最容易漏掉的 74 个时钟SD 卡上电后需要至少 74 个 SCK 时钟才能进入就绪状态很多人直接发 CMD0 导致复位失败。正确顺序是拉高 CS发 80 个时钟10 字节 0xFF再拉低 CS 发 CMD0。之后用 CMD8 判断电压范围用 ACMD41 循环查询卡是否初始化完成最后 CMD58 读 OCR 确认是 SDHC 还是 SDSC。这套流程跑完sd_read_sector(0, buf)应该能读出以 0x55AA 结尾的 MBR 扇区。读不到就查三件事CS 是否在命令期间保持低、MISO 是否被正确配置为上拉输入、SPI 模式是否设成了 Mode 0CPOL0CPHA0。3. 把 FATFS 挂到扇区读写之上三个函数就能跑通挂载3.1 diskio.c 里真正要改的只有四个函数FATFS 是平台无关的它通过diskio.c里的底层接口和硬件对话。移植时真正需要你实现的只有disk_initialize、disk_status、disk_read、disk_writedisk_ioctl里至少要实现GET_SECTOR_SIZE和GET_SECTOR_COUNT。下面是最小实现#include ff.h #include diskio.h #define DEV_SD 0 DSTATUS disk_initialize(BYTE pdrv) { if (pdrv ! DEV_SD) return STA_NOINIT; if (sd_init() ! 0) return STA_NOINIT; // 底层初始化含 74 时钟 return 0; // 返回 0 表示初始化成功 } DSTATUS disk_status(BYTE pdrv) { return (pdrv DEV_SD) ? 0 : STA_NOINIT; } DRESULT disk_read(BYTE pdrv, BYTE *buff, LBA_t sector, UINT count) { if (pdrv ! DEV_SD || !buff || !count) return RES_PARERR; for (UINT i 0; i count; i) { if (sd_read_sector(sector i, buff i * 512) ! 0) return RES_ERROR; } return RES_OK; } DRESULT disk_write(BYTE pdrv, const BYTE *buff, LBA_t sector, UINT count) { if (pdrv ! DEV_SD || !buff || !count) return RES_PARERR; for (UINT i 0; i count; i) { if (sd_write_sector(sector i, (BYTE *)(buff i * 512)) ! 0) return RES_ERROR; } return RES_OK; } DRESULT disk_ioctl(BYTE pdrv, BYTE cmd, void *buff) { if (pdrv ! DEV_SD) return RES_PARERR; switch (cmd) { case GET_SECTOR_SIZE: *(WORD *)buff 512; return RES_OK; case GET_SECTOR_COUNT: *(DWORD *)buff sd_get_sector_count(); return RES_OK; case CTRL_SYNC: return RES_OK; default: return RES_PARERR; } }逻辑说明disk_read的sector是逻辑块地址count是连续块数FATFS 一次可能请求多块所以必须循环处理而不是只读一块。GET_SECTOR_SIZE返回 512这是 FATFS 默认的扇区大小改成 4096 需要同步改ffconf.h里的FF_MIN_SS和FF_MAX_SS。GET_SECTOR_COUNT返回卡的总扇区数f_getfree算容量时要用。3.2 ffconf.h 里必须确认的四个宏FATFS 的配置全在ffconf.h默认值不一定适合 STM32。下面四个宏建议按实际需求改宏名默认值建议值作用FF_USE_LFN01 或 2开启长文件名0 只支持 8.3 短名FF_MAX_SS512512扇区大小和 disk_ioctl 返回一致FF_FS_RPATH01支持相对路径方便f_chdirFF_FS_TINY00为 0 时每个文件对象带独立缓冲区读写更快FF_USE_LFN设为 1 时用静态缓冲区设为 2 时用栈上动态缓冲区后者省 RAM 但要求栈够大。STM32F103 这类 RAM 只有 20KB 的型号开 LFN 后要留意_MAX_LFN别设太大默认 255 会吃掉不少空间改成 64 通常够用。3.3 挂载与读写的完整调用链底层和配置都就绪后挂载只需要三步FATFS fs; // 文件系统对象 FIL fil; // 文件对象 FRESULT res; UINT br; res f_mount(fs, 0:, 1); // 1 表示立即挂载 if (res ! FR_OK) { /* 挂载失败查 disk_initialize 返回值 */ } res f_open(fil, 0:/data.txt, FA_READ | FA_OPEN_EXISTING); if (res FR_OK) { res f_read(fil, read_buf, sizeof(read_buf), br); f_close(fil); }f_mount的第三个参数为 1 时会立刻调用disk_initialize并读取引导扇区。如果返回FR_NO_FILESYSTEM说明扇区 0 不是合法的 FAT 引导记录常见原因是卡被格式化成 exFAT 或 NTFSFATFS 默认只认 FAT12/16/32。返回FR_DISK_ERR则要回到disk_read里查底层。4. 避坑与排查五个让 FATFS 挂载失败的典型现场4.1 现象f_mount 返回 FR_NO_FILESYSTEM但扇区 0 能读出 0x55AA原因卡被格式化成 exFAT。exFAT 的引导扇区同样以 0x55AA 结尾但文件系统类型字符串是 EXFATFATFS 默认不解析。解决用电脑把卡重新格式化为 FAT32分配单元大小选 4096 或 8192或者在ffconf.h里开启FF_FS_EXFAT但后者会显著增加代码体积RAM 紧张的型号不建议。4.2 现象单块读写正常连续多块读时数据错位原因disk_read里没有按count循环只读了第一块就返回。FATFS 在读取目录项或大文件时会一次请求多个连续扇区。解决按 3.1 的写法用 for 循环逐块读每块地址是sector i缓冲区偏移是i * 512。注意buff是BYTE*偏移要按字节算。4.3 现象写入后拔卡再插文件内容丢失或变成 0 字节原因f_write之后没有f_closeFAT 表和目录项没回写。FATFS 有写缓存数据先落在文件对象的缓冲区里f_close才触发目录项更新和 FAT 表同步。解决每次写完必须f_close或者至少调用f_sync。做数据记录仪时建议每写一批就f_sync一次防止突然断电丢数据。4.4 现象SPI 时钟拉到 18MHz 后随机出现 FR_DISK_ERR原因信号完整性问题。杜邦线过长、没有串联匹配电阻、SD 卡座地线回流差都会让高速下的 MISO 采样出错。解决先把 SPI 降到 4MHz 验证功能确认无误后再逐步提高SCK 和 MOSI 上串 22~33 欧姆电阻尽量缩短飞线卡座地脚就近接地。血泪经验是功能调通前别用最高速否则你会分不清是代码问题还是硬件问题。4.5 现象f_open 返回 FR_NOT_ENOUGH_CORE 或直接 HardFault原因FF_USE_LFN开启后长文件名缓冲区分配失败或者栈溢出。STM32 默认栈通常 1KB开 LFN 后f_open内部会占用较多栈空间。解决把启动文件里的栈大小改到 2KB 以上如果用FF_USE_LFN 2确认_MAX_LFN不要超过实际需要RAM 实在紧张就退回FF_USE_LFN 0用 8.3 短文件名。5. 让 FATFS 在 STM32 上跑得更稳的两个进阶习惯第一个习惯是给disk_read和disk_write加超时重试。SD 卡在电压波动或接触不良时会偶发无响应一次失败就返回RES_ERROR会让整个文件操作中断。我一般会在sd_read_sector内部做三次重试每次失败后重新发 CMD17三次都失败才向上返回错误。这样在工业现场能把偶发错误率压下去一个数量级。第二个习惯是监控f_getfree的返回值来判断卡的健康状态。f_getfree会遍历 FAT 表统计空闲簇如果返回的可用空间突然比上次少了很多但实际写入量没变往往说明 FAT 表有异常簇被标记。做长期运行的数据记录设备时可以在每次开机挂载后调一次f_getfree把空闲簇数记下来和上次对比偏差超过阈值就提示换卡。DWORD fre_clust, fre_sect, tot_sect; FATFS *fs_ptr; if (f_getfree(0:, fre_clust, fs_ptr) FR_OK) { tot_sect (fs_ptr-n_fatent - 2) * fs_ptr-csize; fre_sect fre_clust * fs_ptr-csize; // fre_sect / 2 即为可用 KB 数可存入日志对比 }参数说明n_fatent是总簇数csize是每簇扇区数两者相乘再乘 512 字节就是总容量。fre_clust是空闲簇数。这个值在卡没有写入时应该基本不变如果持续下降就要查是否有文件句柄没关闭导致簇泄漏。最后说一个我自己的教训早期做记录仪时为了省事把f_write放在主循环里结果 SD 卡写入耗时把采样周期撑爆了。后来改成双缓冲加 DMA主循环只往缓冲区填数据另一个标志位触发 FATFS 写入才把时序稳住。FATFS 本身不慢慢的是你把它放在了不该放的位置。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。