ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

STM32上基于FatFs实现SD卡CSV数据记录:从移植到性能优化

STM32上基于FatFs实现SD卡CSV数据记录:从移植到性能优化 1. 项目缘起与整体设计思路1.1 为什么要在STM32上折腾CSV文件做过数据采集类项目的朋友大概率都遇到过这个场景板子上的传感器每隔几秒采一组数据跑了一整天想把数据导出来用Excel画个曲线结果发现数据全在串口助手的窗口里复制粘贴还得手动整理格式。更尴尬的是有些设备部署在现场根本没法连着电脑看串口输出这时候就需要一个能离线存储、方便导出、还能被PC端软件直接识别的方案。SD卡加CSV文件就是目前最接地气的组合。SD卡便宜、容量大、拔下来插电脑就能读CSV格式本质上是纯文本用记事本都能打开Excel、MATLAB、Python的pandas全部原生支持。把这两个东西通过FatFs文件系统在STM32上跑通基本上就解决了嵌入式设备“数据落地”的大半问题。我这次做的项目是一个环境监测节点STM32F103C8T6最小系统板挂了一个BME280温湿度气压传感器和一个MH-Z19二氧化碳模块每10秒采集一次数据以CSV格式追加写入SD卡。整个系统跑了一个月SD卡里积累了大约26万行数据文件大小约12MB全程没有出现文件系统崩溃或者数据丢失的情况。这篇文章就把这套方案的完整实现过程拆开来讲包括FatFs的移植配置、CSV文件的读写策略、性能优化技巧以及我踩过的那些坑。1.2 方案选型为什么是FatFs而不是自己写裸数据有人可能会想SD卡不就是块Flash吗我直接按扇区读写自己定义个二进制格式不就行了何必上文件系统这么重的东西。这个想法在数据量小、结构固定的场景下确实可行但一旦涉及到PC端读取问题就来了你得自己写上位机解析程序还得处理不同操作系统的兼容性更麻烦的是数据追加和文件管理全得自己实现。FatFs的好处在于它是标准FAT文件系统的嵌入式实现SD卡在电脑上格式化之后STM32直接就能读写不需要任何额外的驱动或转换。CSV文件在电脑上双击就能用Excel打开这对现场调试和数据分析来说太重要了。而且FatFs的API设计得很简洁f_open、f_write、f_read、f_close这几个函数就能覆盖大部分需求学习成本很低。另一个考虑是实时性。有人担心文件系统会拖慢采集速度但实际上FatFs在SPI模式下配合DMA写入速度可以做到几百KB每秒对于每秒几十字节的传感器数据来说绰绰有余。真正影响实时性的往往是SD卡本身的写入延迟这个后面会详细讲怎么优化。1.3 硬件连接与SPI模式选择STM32和SD卡的连接方式有两种SDIO模式和SPI模式。SDIO模式速度快理论上可以跑到48MHz时钟但占用的引脚多而且不是所有STM32型号都支持。SPI模式只需要4根线CS、SCK、MISO、MOSI随便一个SPI外设都能用移植性极强。我选的是SPI模式接线如下STM32引脚SD卡模块引脚说明PA4CS片选低电平有效PA5SCK时钟PA6MISO主入从出PA7MOSI主出从入3.3VVCC供电GNDGND共地这里有个细节要注意SD卡模块的供电必须是3.3V虽然有些模块标称支持5V但那是板载了电平转换芯片的版本。如果你买的是裸卡座直接接5V会烧卡。另外MISO引脚在SPI模式下是推挽输出但SD卡初始化阶段需要检测卡的存在有些模块会在这个引脚上加下拉电阻如果初始化不成功可以先检查一下这个电阻是否影响了信号电平。SPI时钟方面初始化阶段必须低于400kHz初始化完成后可以提高到18MHz甚至更高。我实测在STM32F103上跑到18MHz很稳定再高就容易出现数据错误。这个分频系数在SPI_Init函数里设置后面代码部分会具体讲。2. FatFs移植与底层驱动适配2.1 FatFs源码裁剪与工程结构FatFs的源码可以从官网下载最新版本是R0.15。下载下来之后真正需要加入到工程里的文件其实不多ff.c文件系统核心实现ff.h头文件ffconf.h配置文件diskio.c底层驱动接口需要自己实现diskio.h底层驱动头文件其他的像ffunicode.c是用于长文件名支持的如果你不需要中文文件名可以直接不加入工程能省不少Flash空间。我这次的项目不需要长文件名所以只用了8.3格式ffconf.h里把_USE_LFN设为0_CODE_PAGE设为1ASCII整个文件系统占用的Flash大约在12KB左右。工程结构建议这样组织Project/ ├── Core/ │ ├── Inc/ │ └── Src/ ├── FatFs/ │ ├── ff.c │ ├── ff.h │ ├── ffconf.h │ ├── diskio.c │ └── diskio.h ├── Drivers/ │ ├── SPI/ │ └── SD/ └── User/ └── main.c把FatFs单独放在一个文件夹里方便以后升级版本。diskio.c是移植的关键里面需要实现五个函数disk_initialize、disk_status、disk_read、disk_write、disk_ioctl。其中disk_ioctl还需要处理CTRL_SYNC、GET_SECTOR_COUNT、GET_SECTOR_SIZE、GET_BLOCK_SIZE这几个命令。2.2 ffconf.h关键配置项解读ffconf.h里的配置项很多但真正影响功能和性能的就那么几个我挑重点说#define _FS_TINY 0 /* 0:普通模式 1:Tiny模式 */ #define _FS_READONLY 0 /* 0:读写 1:只读 */ #define _FS_MINIMIZE 0 /* 0:全功能 1/2/3:逐步裁剪 */ #define _USE_STRFUNC 1 /* 是否启用f_puts/f_printf等字符串函数 */ #define _USE_FIND 0 /* 是否启用f_findfirst/f_findnext */ #define _USE_MKFS 0 /* 是否启用f_mkfs格式化函数 */ #define _USE_FASTSEEK 1 /* 是否启用快速定位 */ #define _USE_LABEL 0 /* 是否启用卷标 */ #define _CODE_PAGE 1 /* 代码页1为ASCII */ #define _USE_LFN 0 /* 长文件名支持0为禁用 */ #define _MAX_SS 512 /* 扇区大小SD卡固定512 */ #define _FS_REENTRANT 0 /* 是否启用重入保护 */_FS_TINY设为0是必须的Tiny模式虽然省RAM但不支持多文件同时操作而且f_write的性能会下降。_USE_STRFUNC建议开启这样可以直接用f_printf往文件里写格式化字符串省得自己拼缓冲区。_USE_FASTSEEK开启后f_lseek在文件内部跳转时会快很多对于需要定位到文件末尾追加数据的场景很有用。_CODE_PAGE设为1表示只支持ASCII这样ffunicode.c就不需要了。如果你确实需要中文文件名得把_CODE_PAGE设为936并且加入ffunicode.cFlash占用会增加大约30KBRAM也会多消耗不少自己权衡。2.3 diskio.c底层驱动实现diskio.c是FatFs和硬件之间的桥梁五个函数必须全部实现。先看disk_initializeDSTATUS disk_initialize(BYTE pdrv) { if (pdrv ! 0) return RES_PARERR; if (SD_Init() ! SD_OK) { return STA_NOINIT; } return 0; }SD_Init()是SD卡初始化函数需要按照SD卡协议走完整的初始化流程上电后至少发送74个时钟脉冲然后发送CMD0进入Idle状态接着CMD8检查电压范围ACMD41等待卡就绪最后CMD58读取OCR寄存器确认容量类型。这个过程比较长网上有很多现成的驱动可以参考我这里就不贴完整代码了重点说几个容易出问题的地方。第一个坑是上电时序。SD卡上电后需要至少1ms的稳定时间然后才能发送时钟脉冲。有些模块的电源滤波做得不好上电瞬间电压波动会导致初始化失败建议在VCC和GND之间并一个10uF的钽电容。第二个坑是CMD8的响应。如果你的卡是SDHC高容量卡CMD8必须返回正确响应否则说明卡不支持2.0协议。有些老卡只支持SD1.0这时候CMD8会超时你需要跳过CMD8直接走ACMD41。判断方法很简单CMD8有响应就按SDHC流程走没响应就按SDSC流程走。disk_read和disk_write的实现相对直接但要注意SPI的读写速度。我用的是SPI1配置如下void SPI1_Init(void) { SPI_InitTypeDef SPI_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE); SPI_InitStructure.SPI_Direction SPI_Direction_2Lines_FullDuplex; SPI_InitStructure.SPI_Mode SPI_Mode_Master; SPI_InitStructure.SPI_DataSize SPI_DataSize_8b; SPI_InitStructure.SPI_CPOL SPI_CPOL_High; SPI_InitStructure.SPI_CPHA SPI_CPHA_2Edge; SPI_InitStructure.SPI_NSS SPI_NSS_Soft; SPI_InitStructure.SPI_BaudRatePrescaler SPI_BaudRatePrescaler_4; SPI_InitStructure.SPI_FirstBit SPI_FirstBit_MSB; SPI_InitStructure.SPI_CRCPolynomial 7; SPI_Init(SPI1, SPI_InitStructure); SPI_Cmd(SPI1, ENABLE); }SPI_BaudRatePrescaler_4对应72MHz/418MHz这是初始化完成后的速度。初始化阶段需要先设为SPI_BaudRatePrescaler_256281kHz等SD卡初始化完成后再切换回来。disk_ioctl里最需要注意的是CTRL_SYNC命令它要求把缓存中的数据立即写入物理介质。FatFs在f_close和f_sync时会调用这个命令如果你不实现它数据可能会滞留在SD卡内部缓存里拔卡的时候容易丢数据。实现方法就是发送CMD13检查卡状态或者简单地等待一段时间确保写入完成。3. CSV文件读写核心实现3.1 CSV格式规范与生成策略CSV全称是Comma-Separated Values逗号分隔值。虽然名字里有“逗号”但实际使用中分隔符可以是逗号、分号、制表符等Excel默认用逗号。一个标准的CSV文件长这样Timestamp,Temperature,Humidity,Pressure,CO2 2024-01-15 08:00:00,23.5,45.2,1013.25,412 2024-01-15 08:00:10,23.6,45.1,1013.24,415 2024-01-15 08:00:20,23.4,45.3,1013.26,410第一行是表头说明每一列的含义。后面每行是一条记录字段之间用逗号分隔。如果字段内容本身包含逗号需要用双引号把整个字段括起来比如Beijing, China。如果字段内容包含双引号则用两个双引号表示一个双引号比如He said hello。在STM32上生成CSV我建议用f_printf函数它内部实现了格式化输出用起来跟标准C的printf几乎一样f_printf(file, %s,%.1f,%.1f,%.2f,%d\n, timestamp_str, temperature, humidity, pressure, co2);这里有几个细节要注意。第一浮点数格式化在FatFs里默认是不支持的需要在ffconf.h里把_USE_STRFUNC设为1并且确保你的编译器支持浮点格式化。Keil MDK默认的MicroLIB是不支持浮点printf的需要在工程选项里勾选“Use MicroLIB”或者改用标准库。第二换行符用\n还是\r\n。Windows的记事本对\n的识别有问题会显示成一行所以建议用\r\n。Excel倒是两种都认但为了兼容性还是用\r\n比较稳妥。第三时间戳的生成。STM32本身没有RTC的话可以用定时器计数来生成相对时间比如从开机开始的秒数。如果需要绝对时间得外挂RTC芯片或者用网络对时。我这次用的是DS3231 RTC模块通过I2C读取时间精度还不错一个月误差在2分钟以内。3.2 文件打开模式与追加写入FatFs的文件打开模式有好几种常用的有模式含义文件不存在时文件存在时FA_READ只读返回错误打开成功FA_WRITE只写返回错误打开成功FA_CREATE_NEW新建创建新文件返回错误FA_CREATE_ALWAYS新建创建新文件清空内容FA_OPEN_ALWAYS打开或新建创建新文件打开成功FA_OPEN_APPEND追加创建新文件定位到末尾对于数据记录场景FA_OPEN_APPEND是最合适的。它会在文件末尾追加数据不会覆盖已有内容。但要注意FA_OPEN_APPEND在FatFs R0.15里的行为是如果文件存在打开后文件指针自动定位到末尾如果文件不存在创建新文件。这个模式在R0.14之前的版本里是没有的老版本需要用FA_OPEN_ALWAYS配合f_lseek手动定位到末尾。FRESULT res; FIL file; res f_open(file, data.csv, FA_OPEN_APPEND | FA_WRITE); if (res ! FR_OK) { // 打开失败可能是卡没插好或者文件系统损坏 return; } // 写入一行数据 f_printf(file, %s,%.1f,%.1f,%.2f,%d\r\n, timestamp_str, temperature, humidity, pressure, co2); // 关闭文件确保数据写入物理介质 f_close(file);这里有个性能问题如果每次采集都f_open、f_write、f_closeSD卡的写入次数会非常频繁不仅速度慢还会加速卡的老化。更好的做法是保持文件一直打开定期调用f_sync把缓存刷入卡中// 初始化时打开文件 f_open(file, data.csv, FA_OPEN_APPEND | FA_WRITE); // 每次采集后写入 f_printf(file, %s,%.1f,%.1f,%.2f,%d\r\n, timestamp_str, temperature, humidity, pressure, co2); // 每10次采集同步一次 if (sync_counter 10) { f_sync(file); sync_counter 0; } // 系统关机前关闭文件 f_close(file);f_sync的作用是把FatFs内部缓存的数据写入SD卡但不关闭文件。这样既保证了数据安全又避免了频繁打开关闭的开销。实测下来每10次同步一次即使突然断电最多丢失10条记录对于10秒采集间隔来说就是100秒的数据可以接受。3.3 数据读取与解析读取CSV文件比写入要复杂一些因为需要解析每一行的字段。FatFs提供了f_gets函数可以按行读取char line_buffer[128]; FIL file; FRESULT res; res f_open(file, data.csv, FA_READ); if (res ! FR_OK) return; // 跳过表头 f_gets(line_buffer, sizeof(line_buffer), file); while (f_gets(line_buffer, sizeof(line_buffer), file) ! NULL) { // 解析line_buffer中的字段 // 可以用strtok或者手动查找逗号位置 char *token strtok(line_buffer, ,); int col 0; while (token ! NULL) { switch (col) { case 0: strcpy(timestamp, token); break; case 1: temperature atof(token); break; case 2: humidity atof(token); break; case 3: pressure atof(token); break; case 4: co2 atoi(token); break; } token strtok(NULL, ,); col; } // 处理解析后的数据 } f_close(file);f_gets的第二个参数是缓冲区大小如果一行数据超过这个长度会被截断。所以缓冲区要留够余量我一般用128字节对于传感器数据来说足够了。如果CSV文件里有中文或者特殊字符缓冲区还要再大一些。strtok函数会修改原始字符串把分隔符替换成\0所以如果你需要保留原始行内容得先拷贝一份。另外strtok不是线程安全的如果在中断里也调用它会出现问题。不过一般CSV解析都在主循环里做问题不大。对于大文件逐行读取的效率还可以但如果你需要频繁查找特定行建议用f_lseek配合二分查找。不过CSV是变长记录没法直接计算行偏移除非你额外维护一个索引文件。这个需求比较小众这里就不展开了。4. 性能优化与稳定性保障4.1 写入速度实测与瓶颈分析我在STM32F103C8T672MHz上做了一组写入速度测试SPI时钟18MHzSD卡是Class 10的16GB卡结果如下写入方式平均速度说明每次f_open/f_write/f_close12 KB/s频繁打开关闭开销大保持打开每次f_write85 KB/s缓存生效速度提升明显保持打开每10次f_sync78 KB/s同步有开销但可接受保持打开每次f_sync45 KB/s同步太频繁速度下降从数据可以看出保持文件打开是最关键的优化。f_open和f_close涉及目录项更新和FAT表写入开销很大。保持打开后f_write只是往FatFs的缓存里写数据速度很快。f_sync会把缓存刷入SD卡每次大约消耗几毫秒所以同步频率要权衡。SD卡本身的写入延迟是另一个瓶颈。SD卡内部有控制器和Flash阵列写入操作需要擦除-编程周期单次写入延迟可能在几毫秒到几十毫秒不等。如果FatFs的缓存满了默认512字节会自动触发写入这时候如果SD卡正忙SPI通信就会阻塞。解决办法是增大FatFs的缓存在ffconf.h里把_MAX_SS设为512已经是最大值或者用多块写入来减少命令开销。还有一个容易被忽略的点SD卡的碎片化。如果频繁创建和删除文件SD卡上的空闲簇会变得分散写入时需要频繁跳转速度会明显下降。对于数据记录场景建议一次性创建一个足够大的文件然后一直追加不要频繁删除重建。4.2 掉电保护与数据完整性嵌入式设备最怕的就是突然断电如果正好在写SD卡的时候断电轻则当前记录丢失重则整个文件系统损坏。我做过一个暴力测试在连续写入的过程中随机断电100次结果有3次导致文件系统损坏需要重新格式化。后来我加了几个保护措施再测试100次没有再出现文件系统损坏的情况第一每次f_sync之后检查返回值。如果返回FR_OK说明数据已经安全写入。如果返回错误立即停止写入避免进一步破坏文件系统。第二在文件开头预留一个“写入中”标志。每次开始写入前先把标志置1写入完成后置0。如果系统重启后发现标志为1说明上次写入被中断可以丢弃最后一条不完整的记录。第三使用两个文件交替写入。比如data_a.csv和data_b.csv每次切换时先写完一个再写另一个。这样即使一个文件损坏另一个还是完好的。这个方法牺牲了一半的存储空间但对于关键数据来说值得。第四电源端加一个大电容比如1000uF可以在断电后维持几百毫秒的供电足够完成一次f_sync。这个方法成本最低效果也最直接。4.3 常见问题排查速查表在实际调试过程中我遇到了不少问题这里整理成速查表方便大家快速定位现象可能原因解决方法f_mount返回FR_NOT_READYSD卡未初始化成功检查SPI接线、供电、上拉电阻f_open返回FR_NO_FILESYSTEM卡未格式化或文件系统损坏用电脑格式化为FAT32f_write返回FR_DISK_ERRSPI通信错误或卡忙降低SPI时钟增加超时重试写入速度极慢频繁f_open/f_close保持文件打开减少同步次数数据乱码编码问题或缓冲区溢出检查_CODE_PAGE设置增大缓冲区文件系统损坏写入过程中断电加掉电保护使用双文件备份f_gets读取不到数据文件指针位置错误检查f_lseek定位确认文件打开模式中文文件名乱码未启用长文件名支持设置_USE_LFN为1加入ffunicode.c还有一个比较隐蔽的问题SD卡的热插拔。如果在文件系统挂载状态下直接拔卡FatFs的缓存可能还没写入会导致数据丢失甚至文件系统损坏。正确的做法是先调用f_mount(NULL, , 0)卸载文件系统再拔卡。如果必须支持热插拔可以在检测到卡移除时立即调用f_sync然后卸载。5. 完整代码框架与实操步骤5.1 主程序流程与关键函数整个项目的main函数流程如下int main(void) { // 系统初始化 SystemInit(); Delay_Init(); USART1_Init(115200); SPI1_Init(); BME280_Init(); MHZ19_Init(); DS3231_Init(); // 挂载文件系统 FATFS fs; FRESULT res f_mount(fs, 0:, 1); if (res ! FR_OK) { printf(Mount failed: %d\r\n, res); while (1); } // 打开CSV文件 FIL file; res f_open(file, 0:/data.csv, FA_OPEN_APPEND | FA_WRITE); if (res ! FR_OK) { printf(Open failed: %d\r\n, res); while (1); } // 如果文件为空写入表头 if (f_size(file) 0) { f_printf(file, Timestamp,Temperature,Humidity,Pressure,CO2\r\n); } uint8_t sync_counter 0; while (1) { // 读取传感器数据 float temp, hum, press; int co2; BME280_Read(temp, hum, press); MHZ19_Read(co2); // 读取RTC时间 char timestamp[20]; DS3231_GetTimeString(timestamp); // 写入CSV f_printf(file, %s,%.1f,%.1f,%.2f,%d\r\n, timestamp, temp, hum, press, co2); // 定期同步 if (sync_counter 10) { f_sync(file); sync_counter 0; } // 延时10秒 Delay_ms(10000); } }这个框架很简洁核心就是f_open、f_printf、f_sync、f_close四个函数。实际项目中还需要加上错误处理、看门狗喂狗、低功耗管理等但基本结构就是这样。5.2 编译配置与常见编译错误在Keil MDK里编译FatFs有几个地方需要特别注意第一勾选“Use MicroLIB”。MicroLIB是Keil提供的精简版C库占用空间小但默认不支持浮点printf。如果你需要在f_printf里输出浮点数要么改用标准库要么自己实现浮点转字符串。我建议用标准库虽然Flash占用大一点但省事。第二在ffconf.h里把_FS_TINY设为0_USE_STRFUNC设为1。如果编译时报错说找不到ff_convert或ff_wtoupper说明_CODE_PAGE设置有问题改成1ASCII即可。第三如果报错“undefined symbol f_mount”检查ff.c是否加入了工程。有时候Keil的工程管理里文件加了但没勾选“Include in Target Build”也会导致链接错误。第四如果报错“L6406E: No space in execution regions”说明Flash不够了。FatFs加上标准库大概需要30-40KB FlashSTM32F103C8T6有64KB够用。如果不够可以裁剪FatFs功能或者换用MicroLIB。5.3 实测数据与效果验证项目跑了一个月SD卡里生成了约26万行数据文件大小12.3MB。用Excel打开曲线绘制正常没有乱码或格式错误。用Python的pandas读取也能正确解析import pandas as pd df pd.read_csv(data.csv) print(df.describe())输出显示温度范围18.2-32.5°C湿度范围30.1-85.6%气压范围1008.2-1018.5hPaCO2范围380-1200ppm数据分布合理没有明显的异常值。写入速度方面10秒采集间隔下每次写入耗时约2msCPU占用率不到1%对系统实时性几乎没有影响。SD卡剩余空间充足按这个速度可以连续记录好几年。唯一遇到的问题是有一次现场断电后最后一条记录不完整只有时间戳没有传感器数据。这是因为f_printf写入到一半时断电了。后来加了“写入中”标志和双文件备份再没出现过这个问题。6. 进阶技巧与扩展思路6.1 用DMA提升SPI读写效率SPI通信如果不用DMACPU需要一直轮询等待浪费大量时间。STM32的SPI支持DMA传输配置好之后CPU只需要发起传输剩下的交给DMA控制器传输完成后再处理数据。配置SPI1的DMA通道void SPI1_DMA_Init(void) { DMA_InitTypeDef DMA_InitStructure; RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); // TX通道 DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)SPI1-DR; DMA_InitStructure.DMA_MemoryBaseAddr (uint32_t)tx_buffer; DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralDST; DMA_InitStructure.DMA_BufferSize 0; DMA_InitStructure.DMA_PeripheralInc DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize DMA_PeripheralDataSize_Byte; DMA_InitStructure.DMA_MemoryDataSize DMA_MemoryDataSize_Byte; DMA_InitStructure.DMA_Mode DMA_Mode_Normal; DMA_InitStructure.DMA_Priority DMA_Priority_High; DMA_InitStructure.DMA_M2M DMA_M2M_Disable; DMA_Init(DMA1_Channel3, DMA_InitStructure); // RX通道类似配置 // ... SPI_I2S_DMACmd(SPI1, SPI_I2S_DMAReq_Tx | SPI_I2S_DMAReq_Rx, ENABLE); }用DMA之后SPI读写速度可以提升30%以上而且CPU可以腾出来处理其他任务。不过DMA配置稍微复杂如果项目对速度要求不高用轮询也够用。6.2 文件分割与循环存储策略如果设备需要长期运行单个CSV文件会越来越大最终可能超过FAT32的4GB限制。解决办法是定期分割文件比如每天创建一个新文件文件名用日期命名char filename[32]; DS3231_GetDateString(date_str); sprintf(filename, 0:/data_%s.csv, date_str); // 检查文件是否存在 FILINFO fno; if (f_stat(filename, fno) ! FR_OK) { // 文件不存在创建并写入表头 f_open(file, filename, FA_CREATE_NEW | FA_WRITE); f_printf(file, Timestamp,Temperature,Humidity,Pressure,CO2\r\n); } else { // 文件存在追加写入 f_open(file, filename, FA_OPEN_APPEND | FA_WRITE); }如果存储空间有限还可以实现循环覆盖保留最近N天的数据超过N天的文件自动删除。用f_findfirst和f_findnext遍历文件按日期排序删除最旧的文件。6.3 数据加密与校验有些应用场景下CSV文件需要加密存储防止被随意查看。FatFs本身不支持加密但可以在写入前对数据进行加密读取时再解密。简单的异或加密就够用了void xor_encrypt(char *data, int len, uint8_t key) { for (int i 0; i len; i) { data[i] ^ key; } }更安全的方式是用AES加密但STM32F103没有硬件AES软件实现比较耗资源。如果只是防君子不防小人异或加密足够了。另外可以在每行末尾加一个CRC校验值读取时验证数据完整性uint16_t crc crc16(line_buffer, strlen(line_buffer)); f_printf(file, %s,%04X\r\n, line_buffer, crc);读取时先分离出CRC计算前面数据的CRC对比是否一致。不一致说明数据损坏可以丢弃该行。6.4 与上位机联调的经验最后分享一个联调技巧在开发阶段可以同时往串口和SD卡输出数据。串口用于实时监控SD卡用于长期记录。这样调试的时候不用频繁拔卡直接看串口就知道数据对不对。f_printf(file, %s,%.1f,%.1f,%.2f,%d\r\n, timestamp, temp, hum, press, co2); printf(%s,%.1f,%.1f,%.2f,%d\r\n, timestamp, temp, hum, press, co2);等系统稳定后再把串口输出关掉节省CPU资源。另外SD卡里的文件可以通过USB读卡器直接在电脑上查看不需要额外的上位机软件这也是CSV格式的一大优势。我在实际使用中发现SD卡的质量对稳定性影响很大。杂牌卡虽然便宜但写入延迟不稳定偶尔会出现几百毫秒的卡顿。换成三星或闪迪的Class 10卡之后写入延迟稳定在几毫秒以内再也没有出现过数据丢失。如果项目对可靠性要求高建议在SD卡上多花点钱省下的调试时间远比卡钱值钱。
返回列表