简介一套基于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等APICubeMX帮我们省去了前两层的绝大部分搭建工作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上最高36MHzF103系列SPI2挂在APB1上最高18MHzSDIO挂在APB2上但F103没有SDIO外设F407以上才有所以F103C8T6用SD卡基本选SPI1。! 一个我踩过好几次的坑CubeMX中如果使能了FatFS它会默认要求为FATFS提供一个定时器TIM用于时钟基准f_tick。这个定时器不要和系统滴答SysTick或HAL库的时基共用否则HAL_Delay()和文件系统同时跑时会死机。2.2 引脚配置中的上下拉与速率SD卡SPI模式下SCK、MOSISDI、CS这几个引脚建议配置为推挽输出最大速度50MHzMISO配置为输入。重点提一下SPI的极性CPOL和相位CPHASD卡规范使用的是SPI Mode 0CPOL0CPHA0也就是空闲时时钟线为低电平数据在第一个边沿采样。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_VOLUMES11单SD卡足够FF_MIN_SS/FF_MAX_SS5124096 / 512部分大容量SD卡扇区为4096字节不匹配会挂载失败FF_USE_MKFS禁用启用后续在代码里格式化SD卡会用到FF_FS_RPATH01允许相对路径代码写起来灵活FF_USE_LFN开启后RAM占用会增加F103C8T6本身有48KB RAM只要不搞大量缓冲完全够用。3. 代码层面让txt读写稳如狗的底层逻辑3.1 挂载与格式化别让f_mount返回FR_NO_FILESYSTEMCubeMX生成的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的第三个参数opt1表示立即挂载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卡座有WPWrite Protect引脚和CDCard 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不包含4096f_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选项决定了文件名如何存储0ANSI/OEM如GB2312中文1UTF-162UTF-8我通常设成UTF-8并在ffconf.h里把FF_LFN_CODE设为0x936GBK。但嵌入式的核心问题不是FatFS本身不支持中文而是你写入的文件内容编码。用记事本在Windows上建的txt默认是ANSI本地编码GBK如果程序以UTF-8写内容PC上打开是乱码反过来也是。稳妥做法在程序里统一以ASCII/GB2312输出英文字符或者明确用UTF-8编码并在文件头写入BOM0xEF 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引脚分配为信号引脚SCKPA5MOSIPA7MISOPA6CSPA4CubeMX版本6.xHAL库版本1.8.xFatFS版本R0.12cCubeMX自带的版本。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是48KBFatFS的work_buf如果用1KB加上一个512字节的扇区缓冲对大部分项目足够了。但是很多人喜欢开一个很大的数组做串口接收再写SD卡比如定义一个uint8_t buf[4096]占据RAM后系统堆栈容易溢出死机的时候怎么排查都看不出问题。建议串口接收用DMA空闲中断每收满256字节搬到SD卡写缓冲写缓冲不超过1KB如果必须大缓冲把编译器的Stack和Heap调大F103最高可以到0x10006. 升级思路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文档读写如果你后面要处理二进制文件、多级目录创建、文件时间戳更新思路是相通的——把底层驱动和文件系统层琢磨透了上面怎么玩都不慌。本文还有配套的精品资源点击获取