ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

STM32F103C8T6裸机跑FATFS v0.09a读写SD卡全链路指南

STM32F103C8T6裸机跑FATFS v0.09a读写SD卡全链路指南 简介本资源是面向STM32F103C8T6初学者与嵌入式开发者的FATFS文件系统实战例程聚焦SD卡SPI模式在资源受限MCU上的中文支持难题。针对官方FATFS V0.09A默认不支持长文件名及中文编码导致的存储异常问题作者通过深度修改ff_convert函数集成CC932/CC936/CC949/CC950等多套中文编码适配逻辑实现稳定可靠的中文路径与文件名读写并兼容标准SD卡与SDHC卡。压缩包共269个文件含48个C源码如ff.c、cc936.c、dlt645_api_07.c、43个头文件、41个编译中间文件及1个PDF版FATFS V0.09A中文手册完整保留官方未改动源码便于对照学习与二次开发工程基于Keil MDK构建含uvproj、uvopt、sct等配置文件结构清晰、模块可拆解。目前已有871人学习下载适合需要掌握嵌入式文件系统移植、中文编码定制及SPI-SD卡驱动调试的进阶开发者。1. STM32F103C8T6 上跑 FATFS v0.09a 读写 SD 卡不是配好引脚就能用的“开箱即用”功能很多刚拿到 STM32F103C8T6 最小系统板的人下载了名为stm32f103c8t6_FATFS V0.09A-SD Card.zip的压缩包解压后看到ff.c、diskio.c和一堆.h文件就以为把代码复制进工程、接上 SD 卡座、烧录进去就能像 Arduino 读 SD 模块那样直接f_open()成功。结果卡在FR_NO_FILESYSTEM或FR_NOT_READY串口打印出一串0x01、0x05、0x07的错误码连 SD 卡是否被识别都搞不清。这其实暴露了一个关键事实FATFS v0.09a 是一个高度依赖底层驱动适配、对时序和初始化流程极其敏感的轻量级文件系统实现它不封装硬件抽象层HAL也不自动处理 SPI 模式切换、CS 电平保持、卡类型判别等细节。你面对的不是“调用 API”而是要亲手打通从寄存器配置、SPI 波形控制、SD 卡命令交互到 FAT 表解析的全链路。本文面向已能点亮 LED、配置 GPIO 和基础 SPI 的 STM32F103C8T6 开发者聚焦如何用标准外设库非 HAL在真实硬件上让这个经典 FATFS 版本稳定挂载 SDHC/SDSC 卡——不绕过原理不跳过调试所有命令和参数都可直接粘贴复现。2. 为什么选 FATFS v0.09a 而不是更新版本从源码结构看它的最小依赖与硬约束2.1 v0.09a 的定位为资源受限 MCU 定制的“裸金属友好型”文件系统FATFS 官方版本演进中v0.09a发布于 2011 年是一个分水岭。它首次引入ffconf.h配置头文件将文件系统行为如长文件名支持、只读模式、扇区大小与硬件驱动完全解耦更重要的是它彻底移除了对 C 标准库malloc/free的隐式依赖所有内存分配均通过用户定义的WORK_SPACE数组完成。这对仅有 20KB Flash、20KB RAM 的 STM32F103C8T6 至关重要——v0.13 版本虽支持动态内存池但默认启用FF_USE_LFN 1时会消耗数百字节栈空间而 v0.09a 在ffconf.h中强制要求#define _USE_LFN 0将 LFN 支持设为编译期开关极大降低 RAM 压力。网络热词中频繁出现的 “stm32f103c8t6最小系统板” 正是这类资源紧绷场景的典型载体v0.09a 的设计哲学与之天然契合。2.2 源码包结构解析diskio.c是唯一可修改的“硬件胶水层”解压stm32f103c8t6_FATFS V0.09A-SD Card.zip后核心文件如下├── ff.c // FATFS 主逻辑f_open/f_read/f_write 等 API 实现 ├── ff.h // 公共类型定义与函数声明 ├── diskio.c // 关键必须重写的硬件接口层含 disk_initialize/disk_read/disk_write ├── diskio.h // diskio.c 的接口声明 ├── ffconf.h // 必须按硬件修改的配置文件重点_MCU_ENDIAN, _USE_WRITE, _MAX_SS └── stm32f10x_conf.h // 示例工程的外设库头文件非 FATFS 官方文件仅作参考提示diskio.c中的五个函数disk_initialize()、disk_status()、disk_read()、disk_write()、disk_ioctl()构成 FATFS 与硬件的唯一契约。FATFS 内核ff.c完全不关心你是用 SPI 还是 SDIO它只调用这五个函数。因此所有调试工作应聚焦于diskio.c的实现是否符合 SD 卡物理层规范而非修改ff.c。2.3 关键配置项ffconf.h的三项必改参数ffconf.h是 FATFS 的“控制中枢”以下三项必须根据 STM32F103C8T6 和 SD 卡特性精确设置宏定义推荐值说明_MCU_ENDIAN0STM32 是小端Little-Endian设为 0若误设为 1大端FAT 表解析将全错导致FR_NO_FILESYSTEM_MAX_SS512SD 卡逻辑扇区大小固定为 512 字节。即使使用 SDHC 卡物理块更大FATFS 仍以 512 字节为单位读写此值不可更改_USE_WRITE1启用写操作。若设为 0则f_write()返回FR_DENIED。注意启用写入需确保disk_write()函数完整实现且 SD 卡未写保护// ffconf.h 中必须确认的三行其他项可保持默认 #define _MCU_ENDIAN 0 // 小端模式STM32F103C8T6 必须为 0 #define _MAX_SS 512 // SD 卡扇区大小不可更改 #define _USE_WRITE 1 // 启用写入功能否则 f_write 失败注意_USE_LFN建议保持0。若强行开启长文件名_USE_LFN 1需额外提供LFN_UNICODE编码支持和FF_USE_STRFUNC字符串函数这在无 RTOS 的裸机环境下极易因栈溢出导致 hardfault。3. SPI 模式下 SD 卡初始化全流程从 CMD0 到成功返回 RCA 的 7 步握手3.1 硬件连接与电平匹配为什么 STM32F103C8T6 必须加电平转换STM32F103C8T6 的 IO 口为 3.3V TTL 电平而标准 SD 卡非 SDIO 模式工作电压为 2.7~3.6V看似兼容。但实际问题在于SD 卡的 DOMISO引脚是开漏输出需外部上拉。若直接连接SPI 通信中 MISO 信号上升沿缓慢易在高速模式下1MHz采样错误。常见最小系统板的 SD 卡座往往省略上拉电阻导致disk_initialize()卡死在等待 CMD0 响应阶段。正确接法以 PA4-CS、PA5-SCK、PA6-MISO、PA7-MOSI 为例CSPA4→ SD_CS直接连接低电平有效SCKPA5→ SD_SCK直接连接MOSIPA7→ SD_DI直接连接MISOPA6→ SD_DO必须经 4.7kΩ 电阻上拉至 3.3VVDD/VSS接板载 3.3V 和 GND3.2 初始化时序详解为什么前 74 个 SCK 时钟不可或缺SD 卡上电后并非立即响应 CMD0必须先发送至少74 个连续的 SCK 脉冲让卡内部状态机完成上电复位Power-Up Clocking。这是 SD 规范强制要求忽略此步将导致后续所有命令返回0xFF无响应。disk_initialize()的典型实现如下// diskio.c 中 disk_initialize() 的关键片段SPI 模式 DSTATUS disk_initialize (BYTE pdrv) { BYTE n, ty, ocr[4]; init_spi(); // 初始化 SPI 外设CPOL0, CPHA0, BR256 (约 111kHz) // Step 1: 发送至少 74 个 SCK期间 CS1, MOSI1 for (n 0; n 10; n) spi_xfer(0xFF); // 发送 10 字节 0xFF产生 80 个 SCK // Step 2: 拉低 CS发送 CMD0 (0x40) 0x00000000 0x95 SELECT(); // CS 0 spi_xfer(0x40); // CMD0 spi_xfer(0x00); spi_xfer(0x00); spi_xfer(0x00); spi_xfer(0x00); // Argument spi_xfer(0x95); // CRC for CMD0 // Step 3: 等待响应最多 10ms for (n 0; n 100; n) { if ((spi_xfer(0xFF) 0x80) 0) break; // 响应字节最高位为 0 表示有效 Delay_us(100); } if (n 100) return STA_NOINIT; // 超时卡未就绪 // Step 4: 发送 CMD8 检测卡版本SDHC/SDSC DESELECT(); Delay_us(100); SELECT(); spi_xfer(0x48); // CMD8 spi_xfer(0x00); spi_xfer(0x00); spi_xfer(0x01); spi_xfer(0xAA); // Arg: 0x000001AA spi_xfer(0x87); // CRC // ... 后续 CMD55ACMD41 流程此处省略见完整代码 }逻辑说明spi_xfer(0xFF)是 SPI 通信中的“空闲字节”用于产生时钟并读取 MISO。在发送 CMD 命令时每发送一个字节必须同时接收一个字节即使无意义否则 SPI FIFO 会阻塞。Delay_us(100)是关键——SD 卡命令响应有严格时间窗过快轮询会导致误判。3.3 CMD55ACMD41区分 SDSC 与 SDHC 卡的核心判据SD 卡初始化中CMD55App Command必须紧跟在CMD0成功后发送然后立即发ACMD41SD Send Op Cond。ACMD41的参数决定卡的类型若参数为0x00000000请求 SDSC 卡容量 ≤2GB成功后返回 RCARelative Card Address若参数为0x40000000请求 SDHC/SDXC 卡容量 2GB成功后同样返回 RCAdisk_initialize()中必须根据ACMD41响应判断卡类型并设置ty变量// 续上 disk_initialize() DESELECT(); Delay_us(100); SELECT(); spi_xfer(0x77); // CMD55 spi_xfer(0x00); spi_xfer(0x00); spi_xfer(0x00); spi_xfer(0x00); spi_xfer(0x65); // CRC for CMD55 // 等待 CMD55 响应应为 0x01 DESELECT(); Delay_us(100); SELECT(); spi_xfer(0x69); // ACMD41 spi_xfer(0x40); spi_xfer(0x00); spi_xfer(0x00); spi_xfer(0x00); // Arg for SDHC spi_xfer(0x77); // CRC for ACMD41 // 循环发送 ACMD41 直到卡就绪最多 1000 次 for (n 0; n 1000; n) { DESELECT(); Delay_us(100); SELECT(); spi_xfer(0x69); spi_xfer(0x40); spi_xfer(0x00); spi_xfer(0x00); spi_xfer(0x00); spi_xfer(0x77); // 读取响应0x00 表示就绪0x01 表示忙 if ((spi_xfer(0xFF) 0x01) 0) { ty CT_SD2 | CT_BLOCK; // SDHC 卡 break; } Delay_ms(10); } if (n 1000) return STA_NOINIT;参数说明0x40000000中的0x40位表示 HCSHigh Capacity Support是 SDHC 卡的标志位。若此处用0x00000000且卡是 SDHC则ACMD41永远返回忙态初始化失败。这也是sd memory card formatter工具格式化 SDHC 卡后仍无法被识别的常见原因——格式化未改变卡的物理类型但初始化代码未适配。4.disk_read()与disk_write()的扇区对齐陷阱为什么单字节读写必然失败4.1 SD 卡的物理限制所有读写必须以 512 字节扇区为单位SD 卡协议规定CMD17Read Single Block和CMD24Write Block命令的操作对象只能是完整的 512 字节扇区。这意味着disk_read(buff, sector, count)中的sector是逻辑扇区号LBAcount是扇区数量绝不能传入count1以外的值v0.09a 不支持多扇区连续读写。更关键的是buff地址必须是 4 字节对齐STM32 DMA 要求且sector必须是整数——任何试图读写非对齐地址或非整数扇区的操作都会被 SD 卡拒绝返回FR_DISK_ERR。4.2disk_read()实现CMD17 数据块头校验// diskio.c 中 disk_read() 的核心逻辑 DRESULT disk_read (BYTE pdrv, BYTE* buff, DWORD sector, UINT count) { BYTE cmd, ty g_card_type; // g_card_type 由 disk_initialize() 设置 if (!g_stat) return RES_NOTRDY; if (!count) return RES_PARERR; if (!(ty CT_BLOCK)) sector * 512; // 若为字节寻址模式SDSC转换为字节地址 SELECT(); // 发送 CMD17: Read Single Block cmd 0x40 | 17; spi_xfer(cmd); spi_xfer((BYTE)(sector 24)); spi_xfer((BYTE)(sector 16)); spi_xfer((BYTE)(sector 8)); spi_xfer((BYTE)sector); spi_xfer(0x01); // CRC for CMD17 (fixed) // 等待数据起始令牌 0xFE for (int i 0; i 1000; i) { if (spi_xfer(0xFF) 0xFE) break; Delay_us(10); } // 读取 512 字节数据 for (int i 0; i 512; i) { buff[i] spi_xfer(0xFF); } // 读取 2 字节 CRC可选v0.09a 默认不校验 spi_xfer(0xFF); spi_xfer(0xFF); DESELECT(); return RES_OK; }逻辑说明spi_xfer(0xFF)在读数据阶段持续发送时钟SD 卡在每个 SCK 下降沿输出一位数据。buff[i] spi_xfer(0xFF)完成一次字节收发。若在等待0xFE时超时说明卡未进入数据传输态常见于sector计算错误或卡未初始化完成。4.3disk_write()实现CMD24 数据块头与 CRC 强制校验写操作比读更严格必须发送正确的起始令牌0xFE和 16 位 CRC。v0.09a 的disk_write()必须包含 CRC 计算否则 SD 卡直接丢弃数据块// diskio.c 中 disk_write() 的关键部分CRC 计算函数需自行实现 DRESULT disk_write (BYTE pdrv, const BYTE* buff, DWORD sector, UINT count) { BYTE cmd, ty g_card_type; if (!g_stat || !count) return RES_NOTRDY; if (!(ty CT_BLOCK)) sector * 512; SELECT(); // CMD24: Write Single Block cmd 0x40 | 24; spi_xfer(cmd); spi_xfer((BYTE)(sector 24)); spi_xfer((BYTE)(sector 16)); spi_xfer((BYTE)(sector 8)); spi_xfer((BYTE)sector); spi_xfer(0x01); // CRC for CMD24 // 等待卡准备好返回 0x00 for (int i 0; i 1000; i) { if (spi_xfer(0xFF) 0x00) break; Delay_us(10); } // 发送起始令牌 0xFE spi_xfer(0xFE); // 发送 512 字节数据 for (int i 0; i 512; i) { spi_xfer(buff[i]); } // 发送 16 位 CRC必须计算 WORD crc crc16(buff, 512); // 自定义 CRC16-CCITT 函数 spi_xfer((BYTE)(crc 8)); spi_xfer((BYTE)crc); // 等待写完成响应0x05 表示接受0x0B 表示写错 BYTE resp spi_xfer(0xFF); if ((resp 0x1F) ! 0x05) { DESELECT(); return RES_ERROR; } // 等待卡退出忙态MISO 拉高 while (spi_xfer(0xFF) 0x00); DESELECT(); return RES_OK; }参数说明crc16()必须实现标准 CRC16-CCITT 算法初始值 0xFFFF多项式 0x1021。若此处用固定0x0000替代SD 卡会返回0x0B响应f_write()失败。这是stm32f103c8t6项目密码锁等需要存储配置的项目中最隐蔽的坑。5. 调试与验证用最简代码验证 FATFS 挂载成功的 3 个黄金指标5.1 串口输出 FATFS 错误码将FR_*宏映射为可读字符串FATFS 返回的FRESULT是一个枚举值FR_OK0,FR_NO_FILESYSTEM13直接打印数字毫无意义。在调试阶段必须添加错误码翻译函数// 添加到 main.c 或单独 error.c const char* fr_str[] { OK, DISK_ERR, INT_ERR, NOT_READY, NO_FILE, NO_PATH, INVALID_OBJECT, WRITE_PROTECTED, INVALID_DRIVE, NOT_ENABLED, NO_FILESYSTEM, MKFS_ABORTED, TIMEOUT, LOCKED, NOT_ENOUGH_CORE, TOO_MANY_OPEN_FILES, INVALID_PARAMETER }; void print_fresult(FRESULT res) { if (res sizeof(fr_str)/sizeof(fr_str[0])) { printf(FATFS Result: %s\r\n, fr_str[res]); } else { printf(FATFS Result: Unknown (%d)\r\n, res); } }提示当f_mount()返回FR_NO_FILESYSTEM13时90% 的原因是 SD 卡未格式化为 FAT16/FAT32。此时应使用官方sd memory card formatter工具非 Windows 自带格式化重新格式化确保分配单元大小为 4KB 或 512B。5.2f_getfree()验证获取剩余扇区数是最可靠的挂载成功信号f_mount()成功仅表示驱动加载不代表文件系统可用。真正验证 FATFS 工作正常的黄金方法是调用f_getfree()获取空闲簇数// main.c 中验证代码 FATFS fs; FILINFO fno; DWORD fre_clust; FRESULT res; res f_mount(fs, , 0); // 挂载驱动器 0 print_fresult(res); if (res ! FR_OK) while(1); // 挂载失败则死循环 res f_getfree(, fre_clust, fs); // 获取空闲簇数 print_fresult(res); if (res FR_OK) { printf(Free clusters: %lu\r\n, fre_clust); printf(Total size: %lu KB\r\n, (fs.n_fatent - 2) * fs.csize / 2); }逻辑说明f_getfree()内部会读取 FAT 表并统计空闲簇若返回FR_OK证明 FAT 表可解析、根目录可访问、整个文件系统链路畅通。这是比f_open()更底层、更可靠的验证点。5.3f_opendir()f_readdir()遍历根目录确认长文件名与短文件名兼容性最后一步验证 FATFS 是否能正确解析目录项。创建一个测试文件TEST.TXT在 SD 卡根目录然后遍历DIR dir; FILINFO fno; res f_opendir(dir, ); if (res FR_OK) { while (1) { res f_readdir(dir, fno); if (res ! FR_OK || fno.fname[0] 0) break; // 到达末尾 if (fno.fname[0] .) continue; // 跳过 . 和 .. printf(File: %s, Size: %lu\r\n, fno.fname, fno.fsize); } f_closedir(dir); }注意若fno.fname显示乱码如T?ST.TXT检查_MCU_ENDIAN是否为 0若显示为TESTTXT无扩展名检查_USE_LFN是否为 0 且 SD 卡格式化时未启用长文件名。stm32f103c8t6原理图中若 SD 卡座走线过长或未加滤波电容也会导致f_readdir()读取目录项 CRC 错误返回空文件名。6. 性能优化与稳定性加固针对 STM32F103C8T6 的 3 项关键调整6.1 SPI 时钟频率分级初始化用 100kHz读写提至 4MHzSD 卡初始化阶段CMD0-CMD8必须使用低速 SPI≤400kHz否则时序不满足。但数据读写阶段可提升至 4MHzSTM32F103C8T6 SPI1 最高支持 18MHz但 SD 卡可靠上限为 25MHz4MHz 是安全平衡点。在disk_read()/disk_write()开头动态切换 SPI 波特率// 在 disk_read() 开头添加 SPI_Cmd(SPI1, DISABLE); SPI_InitStructure.SPI_BaudRatePrescaler SPI_BaudRatePrescaler_4; // 72MHz/4 18MHz → 改为 SPI_BaudRatePrescaler_18 使 SCK≈4MHz SPI_Init(SPI1, SPI_InitStructure); SPI_Cmd(SPI1, ENABLE);提示SPI_BaudRatePrescaler_18对应 72MHz APB2 时钟下的 4MHz SCK。若系统时钟非 72MHz需按SCK PCLK / Prescaler重新计算。提速后f_read()512 字节耗时可从 8ms 降至 1.5ms。6.2disk_ioctl()中CTRL_SYNC的正确实现避免缓存不一致disk_ioctl()中CTRL_SYNC命令用于强制刷新写缓存。v0.09a 默认不实现但若disk_write()使用了局部缓冲区必须在此处清空DRESULT disk_ioctl (BYTE pdrv, BYTE cmd, void* buff) { switch (cmd) { case CTRL_SYNC: // 若使用 write buffer此处应将 buffer 刷入 SD 卡 // 对于直写模式只需确保上次 write 已完成 while (spi_xfer(0xFF) 0x00); // 等待卡空闲 return RES_OK; case GET_SECTOR_COUNT: *(DWORD*)buff 128 * 1024; // 假设 128MB 卡实际应从 CSD 寄存器读取 return RES_OK; default: return RES_PARERR; } }6.3 电源去耦与 SD 卡座选型被忽视的硬件稳定性根源所有软件优化的前提是硬件可靠。stm32f103c8t6最小系统电路图中SD 卡座旁必须放置100nF 陶瓷电容紧贴卡座 VDD 引脚10μF 钽电容跨接 VDD-VSSSD_CS 信号线上串联 33Ω 电阻抑制高频振铃若使用山寨 SD 卡座如某宝 2 元包邮款其弹片接触电阻常达 1~2Ω在 4MHz SPI 下导致信号边沿畸变表现为偶发FR_DISK_ERR。更换为原装 Hirose 或 Molex 卡座后错误率下降 90%。这是stm32f103c8t6开发学习过程中最容易被忽略的“玄学”环节。本文还有配套的精品资源点击获取
返回列表