简介:一套基于STM32 HAL库与FatFs文件系统(描述中称CUBEMAX)实现SD卡读写TXT文档的完整工程源码,面向嵌入式开发者及STM32入门学习者,解决日志存储、配置读取等场景下的文件操作需求。压缩包共213个文件,约12.21MB,包含C源文件(.c/.h)、STM32CubeMX配置(.ioc/.uvprojx)、编译中间文件(.o/.d/.crf)及可执行文件(.axf/.hex),是标准HAL库工程结构。已有2710人学习。工程内集成HAL_SD驱动、FatFs文件系统、cc936中文编码模块,并含完整读写TXT示例及错误处理逻辑,可直接参考或移植。对理解SPI/SDIO接口配置、文件系统挂载以及f_open/f_read/f_write等API调用非常有帮助。 ST这几年的调试记录,看得最多的问题不是“怎么初始化SPI”,而是“文件系统挂不上”、“SD卡明明插得好好的却报写保护”、“文件写进去了但电脑上看是乱码”。如果你正准备用STM32系列芯片做数据记录或固件升级功能,这篇内容应该能帮你省掉一大半的弯路。
1. 为什么是CubeMX + FatFS这条组合路线
1.1 先从标题里的“cubemax”说起
标题写的“cubemax”,实际就是ST官方工具STM32CubeMX。这个拼写错误非常普遍,GitHub上、论坛里搜"cubemax"能找到大量提问。CubeMX的作用是通过图形化界面帮你生成底层初始化代码,把GPIO、时钟、外设(比如SPI、SDIO、UART)这些繁琐的寄存器配置全部自动化,生成的是HAL库代码。
HAL库是ST主推的硬件抽象层库,相比早期的标准外设库(StdPeriph),它的特点是分层清晰、API统一,换芯片型号时移植成本低。对大部分项目来说,HAL库的性能损耗完全可以接受,尤其在F103这类Cortex-M3芯片上,文件系统读写本来就不追求极限吞吐,稳定易维护才是核心诉求。
1.2 为什么不用裸写SD卡协议
SD卡底层有两种通信方式:SDIO和SPI。如果用寄存器裸写,需要处理SD卡初始化命令序列(CMD0、CMD8、ACMD41等),还要管理CRC校验、R1/R3响应解析、块读写状态轮询,光调试SD卡初始化流程就能卡一两周。FatFS本身只是一个文件系统层,它不管底层存储介质,只负责FAT表的维护、目录项的增删改查、文件数据的分配与回收。
所以正确分工是:
- 底层:SD卡驱动(通过SPI或SDIO与卡通信,完成读扇区/写扇区)
- 中间层:FatFS模块(把扇区按FAT16/FAT32规则组织成文件和目录)
- 上层:用户应用程序(f_open、f_write、f_read、f_close等API)
CubeMX帮我们省去了前两层的绝大部分搭建工作:SD卡底层驱动可以用它生成的SDMMC或SPI驱动代码,FatFS也由CubeMX自动移植好,只留出底层接口(diskio.c中的SD_read、SD_write)来对接库函数。这个组合把原本一个月的开发周期压缩到一天内跑通。
2. 工程配置阶段最容易忽略的三个点
2.1 时钟树设计比外设配置更优先
很多人在CubeMX里先急着配置SPI引脚,结果后面发现SD卡读写不稳定,排查半天最后发现是SDIO时钟不对。以STM32F103C8T6为例,SD卡用SPI模式时,SPI时钟最高可以到18MHz左右,但实际调试建议先从400kHz开始(SD卡规范要求初始化阶段时钟不超过400kHz),初始化完成后再切换到高速模式。
在CubeMX的Clock Configuration页面,注意看APB1和APB2总线时钟:
- SPI1挂在APB2上,最高36MHz(F103系列)
- SPI2挂在APB1上,最高18MHz
- SDIO挂在APB2上,但F103没有SDIO外设(F407以上才有)
所以F103C8T6用SD卡,基本选SPI1。
!> 一个我踩过好几次的坑:CubeMX中如果使能了FatFS,它会默认要求为FATFS提供一个定时器(TIM)用于时钟基准(f_tick)。这个定时器不要和系统滴答(SysTick)或HAL库的时基共用,否则HAL_Delay()和文件系统同时跑时会死机。
2.2 引脚配置中的上下拉与速率
SD卡SPI模式下,SCK、MOSI(SDI)、CS这几个引脚建议配置为推挽输出,最大速度50MHz,MISO配置为输入。
重点提一下SPI的极性(CPOL)和相位(CPHA):SD卡规范使用的是SPI Mode 0(CPOL=0,CPHA=0),也就是空闲时时钟线为低电平,数据在第一个边沿采样。CubeMX里SPI参数设置中把这几个值调成Mode 0即可,如果选错,SD卡初始化时CMD0就过不去(返回0xFF或者超时)。
另一个容易忽略的是CS引脚的管理。有人直接在CubeMX里把SPI NSS配置为硬件自动管理,但FatFS底层驱动在读写时会频繁切换CS,硬件自动CS在部分芯片上时序不可控,容易出现“偶发读错误”。我的做法是:CubeMX中把NSS设为软件模式(Software),在diskio.c的SD_CS_LOW()和SD_CS_HIGH()宏中手动控制一个普通GPIO引脚,注意这里diskio.c文件是CubeMX生成的,需要自己在用户代码区(即USER CODE区块)写宏定义。
2.3 FatFS配置参数要按需求调
CubeMX的FatFS组件中有一堆配置选项,默认值有时不是最佳。
常用参数建议参考:
| 配置项 | 默认值 | 建议值 | 原因 |
|---|---|---|---|
FF_USE_LFN | 禁用 | 启用(LFN_CODE选择GB2312或UTF-8) | 支持长文件名,否则8.3格式的限制会让你怀疑人生 |
FF_VOLUMES | 1 | 1 | 单SD卡足够 |
FF_MIN_SS/FF_MAX_SS | 512 | 4096 / 512 | 部分大容量SD卡扇区为4096字节,不匹配会挂载失败 |
FF_USE_MKFS | 禁用 | 启用 | 后续在代码里格式化SD卡会用到 |
FF_FS_RPATH | 0 | 1 | 允许相对路径,代码写起来灵活 |
FF_USE_LFN开启后,RAM占用会增加,F103C8T6本身有48KB RAM,只要不搞大量缓冲,完全够用。
3. 代码层面让txt读写稳如狗的底层逻辑
3.1 挂载与格式化:别让f_mount返回FR_NO_FILESYSTEM
CubeMX生成的main.c中,在初始化函数MX_FATFS_Init()里会调用FATFS_LinkDriver(&SD_Driver, fatfs->fs_path)注册驱动。真正的挂载要自己在main()函数的循环前置区调用f_mount()。
常见写法:
FATFS fs; FIL file; FRESULT res; UINT bytes_written, bytes_read; // 挂载文件系统 res = f_mount(&fs, "S:", 1); if (res == FR_NO_FILESYSTEM) { // 说明SD卡是空的或文件系统损坏,需要格式化 res = f_mkfs("S:", NULL, work_buf, sizeof(work_buf)); if (res != FR_OK) { Error_Handler(); } f_mount(NULL, "S:", 1); // 重新挂载 res = f_mount(&fs, "S:", 1); if (res != FR_OK) { Error_Handler(); } }关键点是f_mount的第三个参数(opt):1表示立即挂载,0表示延迟挂载。很多人用0,结果后续f_open返回FR_INT_ERR,一脸懵。调试时建议使用1,让错误尽早暴露。
如果f_mount返回FR_DISK_ERR,问题基本不在文件系统,在底层SD卡驱动。优先检查SPI通信和上电时序。
3.2 打开txt文件的写入模式细节
FatFS的f_open模式常量继承了DOS时代的语义,容易搞混的有两组:
FA_OPEN_EXISTING:打开已有文件(不创建)FA_OPEN_ALWAYS:打开文件,如果不存在则创建;存在则打开FA_CREATE_NEW:创建新文件,如果已存在则返回错误FA_CREATE_ALWAYS:创建新文件,如果已存在则清空内容
// 打开或创建 data.txt,允许写入 res = f_open(&file, "S:/data.txt", FA_OPEN_ALWAYS | FA_WRITE); // 将文件指针移到文件末尾,实现追加写入 res = f_lseek(&file, f_size(&file)); // 写入字符串 res = f_write(&file, buffer, strlen(buffer), &bytes_written); // 关闭文件(很重要,确保缓存区数据落盘) f_close(&file);追加日志数据是最常见的需求,用FA_OPEN_ALWAYS加f_lseek末尾定位,测试下来比FA_CREATE_ALWAYS每次清空重建要可靠,原因在于FAT表操作少,写入次数多时不容易产生簇链碎片。
有一个特别隐蔽的问题:F103的RAM很小,FatFS内部带扇区缓冲(默认每个文件对象有FF_FS_LOCK和文件缓冲),打开文件后如果突然断电或复位,没来得及f_close的数据会丢。这个没法完全避免,只能靠数据冗余或者定时关闭文件。工业现场的解决办法是用积累一定长度(比如1KB)数据后写一次且执行f_sync,而不是每行都关闭,兼顾断电鲁棒性和Flash寿命。
f_sync比较关键,建议先用起来:
res = f_write(&file, buffer, len, &bytes_written); res = f_sync(&file); // 将缓存立即写入SD卡3.3 读取txt的经典姿势
读取相比写入简单,但要注意缓冲区大小。F103的RAM有限,一次f_read读多少取决于你的需求,这里推荐分块读取而非一次性整文件读入。
char read_buf[128]; UINT bytes_read = 0; res = f_open(&file, "S:/data.txt", FA_READ); if (res == FR_OK) { res = f_read(&file, read_buf, sizeof(read_buf)-1, &bytes_read); if (res == FR_OK) { read_buf[bytes_read] = '\0'; // 确保字符串结束 printf("%s", read_buf); } f_close(&file); }有一个容易犯的错是忘记给缓冲区末尾加\0。f_read不会自动帮你加字符串结束符,如果读出来是二进制或非整块文本,printf会越界读内存,轻则打印乱码,重则HardFault。这个是新手极易踩坑的地方。
4. 那些年SD卡文件系统遇到的迷之问题
4.1 SD卡没锁但报写保护
这是所有SD卡相关帖子中重复出现最多的问题。排除卡侧面卡片真的拨到LOCK的情况后,大概率是SPI模式下的写保护引脚检测逻辑。在SPI模式下,部分SD卡座有WP(Write Protect)引脚和CD(Card Detect)引脚,默认上拉或下拉状态如果不匹配,驱动层会误判。
CubeMX生成的diskio.c里如果检测到卡座不带CD/WP引脚,通常直接返回0(表示没有写保护)。但有些开发板的卡座是带引脚的,而且默认电平逻辑和代码假设相反。
建议排查方式:先量卡座CD引脚的电压,再对照diskio.c里的SD_Detect()函数,看它判定“卡是否存在”的电平条件。这个函数内部是通过HAL_GPIO_ReadPin去读引脚状态来决定返回值的,和硬件电路不匹配时,就会产生“明明卡槽里插着卡,却提示未检测到”或写保护误报。
如果板子上没有CD引脚,可以考虑直接修改diskio.c(既SD_DISK_IOCTL中CTRL_GET_SECTOR_COUNT等命令分支),因为CTRL_GET_SDK接口在CubeMX生成时其实只做了简单处理。
4.2 文件系统挂载失败与簇尺寸的坑
前面提到FF_MAX_SS,这是挂载64GB以上SD卡时的关键。FAT32格式的SD卡,扇区大小一般是512字节,但SDXC或部分大容量卡用了4096字节物理扇区。如果FatFS编译时的FF_MIN_SS和FF_MAX_SS不包含4096,f_mount会返回FR_NO_FILESYSTEM或FR_NOT_ENABLED。
另外有一个细节:很多所谓的“64G SD卡系统镜像img文件”在烧录到卡里后,Windows只能看到RAW分区,FatFS也挂不上。这个典型原因是镜像里带了MBR(主引导记录)和多个分区,FatFS默认只解析第一个可用的FAT分区,如果你烧录的镜像把FAT分区放在偏移位置,需要调整f_mount的挂载路径。简单说,f_mount中的path参数可以用"0:"或"S:",数字代表卷标序号,CubeMX的模板里对SPI用了"S:"和"0:"两种,虽然都能用,但数字符和字母不同,可能导致挂错卷。
4.3 中文文件名与编码
项目题目明确写了要读写txt,如果你在SD卡里放的txt是中文名或内容含中文,就绕不开编码问题。
FatFS的FF_USE_LFN开启后,FF_LFN_UNICODE选项决定了文件名如何存储:
0:ANSI/OEM(如GB2312中文)1:UTF-162:UTF-8
我通常设成UTF-8,并在ffconf.h里把FF_LFN_CODE设为0x936(GBK)。但嵌入式的核心问题不是FatFS本身不支持中文,而是你写入的文件内容编码。用记事本在Windows上建的txt,默认是ANSI(本地编码GBK),如果程序以UTF-8写内容,PC上打开是乱码;反过来也是。
稳妥做法:在程序里统一以ASCII/GB2312输出英文字符,或者明确用UTF-8编码并在文件头写入BOM(0xEF 0xBB 0xBF)。反正电脑的记事本(新版)对UTF-8识别的支持已经很稳定了。
// 写入UTF-8 BOM uint8_t bom[] = {0xEF, 0xBB, 0xBF}; f_write(&file, bom, 3, &bytes_written);如果嫌麻烦,就直接全部用英文命名文件、英文内容,项目记录只做数据排列,这样永远不踩编码坑。
4.4 同一块板子,K210与STM32通信带来的干扰
这个情况比较冷门,但确实有网友在SPI总线上既接SD卡又接LCD或其它传感器,比如K210与STM32通信共用SPI。如果是在同一SPI总线上挂SD卡和外设,片选信号没有很好隔离,SD卡数据会被其它设备的MISO拉高拉低干扰。
解决思路是确保每个SPI设备独立CS且空闲状态保持高电平。如果外设本身有自己专属的SPI引脚,最好复用同一个硬件SPI但用不同CS,访问外设前重新初始化(将SD卡和另一个设备的模式分别设置,读写前切换)。另外注意SD卡的SPI模式初始化必须在f_mount之前,如果你在程序中途插拔SD卡,SPI外设需要重新初始化。
5. 我的实测环境与性能参考
5.1 测试平台
我用的是STM32F103C8T6小板,SD卡模块走SPI1,引脚分配为:
| 信号 | 引脚 |
|---|---|
| SCK | PA5 |
| MOSI | PA7 |
| MISO | PA6 |
| CS | PA4 |
CubeMX版本6.x,HAL库版本1.8.x,FatFS版本R0.12c(CubeMX自带的版本)。SD卡分别测过Sandisk 16GB Class10、金士顿32GB Class10,还有一张不知名8GB卡。
5.2 实测读写速度
- 写速度约150~250 KB/s(取决于簇大小和卡的质量)
- 读速度约300~400 KB/s
- 连续写入4096字节块时效率最高,因为FatFS一个簇通常等于4K,写入正好对齐簇边界
如果你做的是采样数据记录,比如每100ms记一条10字节数据,这个速度绰绰有余。但如果要做音频或视频流写入,建议换SDIO接口的单片机(比如F407)或者是启用DMA。
5.3 缓冲区大小选择
F103C8T6的RAM是48KB,FatFS的work_buf如果用1KB,加上一个512字节的扇区缓冲,对大部分项目足够了。但是很多人喜欢开一个很大的数组做串口接收再写SD卡,比如定义一个uint8_t buf[4096],占据RAM后系统堆栈容易溢出,死机的时候怎么排查都看不出问题。建议:
- 串口接收用DMA+空闲中断,每收满256字节搬到SD卡写缓冲
- 写缓冲不超过1KB
- 如果必须大缓冲,把编译器的Stack和Heap调大,F103最高可以到0x1000
6. 升级思路:log系统、掉电保护与多文件滚动
6.1 从“能读写”到“稳定记录”
如果你只是验证功能,上面内容够了。但真要做项目(比如设备日志记录、温湿度采集、GPS轨迹存储),就需要再加一个轻量级的日志模块封装。建议在FatFS之上做一个简单的接口:
int Log_Init(void); int Log_Write(uint8_t *data, uint16_t len); int Log_Close(void);Log_Init里完成f_mount、打开文件(或创建新文件)、写文件头;Log_Write内部先判断当前文件大小是否超过阈值(比如1MB),超过就关闭当前文件并创建新文件继续写,避免单文件过大导致打开缓慢、FAT表检索耗时。这属于“滚动日志”的思路,实际项目中非常实用。
6.2 掉电保护的一个野路子技巧
SD卡最怕写一半断电。FAT目录项和FAT表不是原子的,写一半断电极容易让整个分区变成RAW。我常用的保护手段:
- 在文件头固定位置写入魔数(Magic Number)和当前写入序号
- 每次写入完成后更新文件尾部的校验和
- 上电启动检查魔数和校验和,不一致就自动
f_mkfs重建
这个方案虽然暴力,但能保证系统再次开机后至少处于可用状态,不会因为文件系统损坏导致整个设备变砖。对于记录类设备,偶尔丢一条数据可以接受,设备起不来才是大事故。
6.3 CubeMX升级后代码兼容性
CubeMX从6.0升级到6.10以上,生成的FatFS代码可能有一个变化:老版本用FATFS_LinkDriver,新版本依然保留,但增加了fs_path分配方式的变化,比如fatfs->fs_path变成了字符数组。如果你把老版本生成的工程导入新版CubeMX重新生成代码,diskio.c里可能出现重复定义或变量类型不匹配。
解决方式是不要直接在生成目录下改底层代码,把需要的FatFS操作封装在自己的用户文件里,CubeMX重新生成时保留USER CODE区域标记,这样重新生成也无压力。
7. 写在最后的几个小提醒
实际调试中,SD卡问题最难排查的其实不是代码逻辑,而是供电。SD卡瞬态电流可以达到100mA左右,很多人直接让板载3.3V稳压器给SD卡供电,在WiFi模块或电机同时启动时电压跌落,SD卡就会出现初始化成功但读写偶发失败。建议SD卡独立供电并加一个100uF电解电容和0.1uF陶瓷电容,VDD引脚滤波稳妥。
还有一个所有做数据记录的工程师都懂的小技巧:写完一轮数据后,把文件关闭挂载释放,插到电脑上确认内容无误。不要假设“写进去了就是写对了”,我在调试中遇到过一次f_write返回FR_OK,但SD卡拔下来内容是空的,原因是文件指针没定位正确,写到了文件之外的区域。这种坑只要验证一次就长记性了。
CubeMX生成的FatFS代码框架搭建好了以后,剩下的就是业务层的活。这次讲的是txt文档读写,如果你后面要处理二进制文件、多级目录创建、文件时间戳更新,思路是相通的——把底层驱动和文件系统层琢磨透了,上面怎么玩都不慌。
本文还有配套的精品资源,点击获取