FatFS官网:https://elm-chan.org/fsw/ff/00index_e.html
当你用单片机做项目,代码调试靠串口、数据记录靠看屏幕、文件读写靠想象,久而久之,你会发现:没有文件系统,生活就像裸奔,哪都能跑,就是不太方便;
尤其是在一些需要长时间运行、持续采集数据 的应用场景中,比如环境监测、设备日志记录、传感器数据采集等,如果没有一个可靠的文件系统来进行数据持久化存储 ,不仅开发调试麻烦,维护和升级也会变得困难重重;你总不能每次都靠串口打印几十KB甚至几MB的数据吧?
这时候你可能听说了一个神器:FatFS ,一个轻量级的 FAT 文件系统,专为嵌入式系统设计,小巧灵活,支持 SD 卡、SPI Flash,甚至 RAMDisk;不论你用的是 STM32、GD32,还是别的 MCU 平台,都能把它“嫁接”过去;
有了文件系统,不仅可以更方便地与电脑共享数据 (比如通过 U 盘或 SD 卡读取设备日志),还能按时间归档、分类管理信息,甚至在设备意外断电或异常重启时保留关键数据 ,提升项目的健壮性和专业程度;
那么,这篇文章就是来讲一讲:如何在你的单片机上,成功移植 FatFS,让你的 MCU 拥有读写文件的能力;
FatFS 移植流程概览 FatFS 的移植主要包括以下几个步骤:
准备底层存储驱动(如 SPI Flash 驱动) 实现 FatFS 所需的 diskio.c 接口函数 配置 ffconf.h 以满足你的文件系统需求 在主函数中初始化 FatFS,挂载文件系统 实现文件的读写操作测试 接下来,我们从第一步开始,移植 SPI Flash 驱动;
FatFS 文件系统移植到 SPI Flash 要让 FatFS 在单片机上正常工作,首先你得有一个“存储设备”能读能写;虽然 SD 卡是最常见的选择,但很多时候,SPI Flash 是更方便的一种方式:不需要外接卡座、不怕接触不良,容量也够用;
对应的驱动文件如下,将文件添加到你的工程中,驱动文件来自沁恒例程,做了一点点的补充与修改:
spi_flash.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 #include "spi_flash.h" volatile uint8_t Flash_Type = 0x00 ; volatile uint32_t Flash_ID = 0x00 ; volatile uint32_t Flash_Sector_Count = 0x00 ; volatile uint16_t Flash_Sector_Size = 0x00 ; uint8_t SPI_FLASH_SendByte (uint8_t byte) { return User_SPI1_ReadWriteByte (byte); } uint8_t SPI_FLASH_ReadByte (void ) { return User_SPI1_ReadWriteByte (0xFF ); } uint32_t FLASH_ReadID (void ) { uint32_t dat; PIN_FLASH_CS_LOW(); SPI_FLASH_SendByte (CMD_FLASH_JEDEC_ID); dat = (uint32_t )SPI_FLASH_SendByte (DEF_DUMMY_BYTE) << 16 ; dat |= (uint32_t )SPI_FLASH_SendByte (DEF_DUMMY_BYTE) << 8 ; dat |= SPI_FLASH_SendByte (DEF_DUMMY_BYTE); PIN_FLASH_CS_HIGH(); return (dat); } void FLASH_WriteEnable (void ) { PIN_FLASH_CS_LOW(); SPI_FLASH_SendByte (CMD_FLASH_WREN); PIN_FLASH_CS_HIGH(); } void FLASH_WriteDisable (void ) { PIN_FLASH_CS_LOW(); SPI_FLASH_SendByte (CMD_FLASH_WRDI); PIN_FLASH_CS_HIGH(); } uint8_t FLASH_ReadStatusReg (void ) { uint8_t status; PIN_FLASH_CS_LOW(); SPI_FLASH_SendByte (CMD_FLASH_RDSR); status = SPI_FLASH_ReadByte(); PIN_FLASH_CS_HIGH(); return (status); } void FLASH_Erase_Sector (uint32_t address) { uint8_t temp; FLASH_WriteEnable(); PIN_FLASH_CS_LOW(); SPI_FLASH_SendByte (CMD_FLASH_SECTOR_ERASE); SPI_FLASH_SendByte ((uint8_t )(address >> 16 )); SPI_FLASH_SendByte ((uint8_t )(address >> 8 )); SPI_FLASH_SendByte ((uint8_t )address); PIN_FLASH_CS_HIGH(); do { temp = FLASH_ReadStatusReg(); } while (temp & 0x01 ); } void FLASH_RD_Block_Start (uint32_t address) { PIN_FLASH_CS_LOW(); SPI_FLASH_SendByte (CMD_FLASH_READ); SPI_FLASH_SendByte ((uint8_t )(address >> 16 )); SPI_FLASH_SendByte ((uint8_t )(address >> 8 )); SPI_FLASH_SendByte ((uint8_t )address); } void FLASH_RD_Block (uint8_t *pbuf, uint32_t len) { while (len--) { *pbuf++ = SPI_FLASH_ReadByte(); } } void FLASH_RD_Block_End (void ) { PIN_FLASH_CS_HIGH(); } void W25XXX_WR_Page (uint8_t *pbuf, uint32_t address, uint32_t len) { uint8_t temp; FLASH_WriteEnable(); PIN_FLASH_CS_LOW(); SPI_FLASH_SendByte (CMD_FLASH_BYTE_PROG); SPI_FLASH_SendByte ((uint8_t )(address >> 16 )); SPI_FLASH_SendByte ((uint8_t )(address >> 8 )); SPI_FLASH_SendByte ((uint8_t )address); if (len > SPI_FLASH_PerWritePageSize) { len = SPI_FLASH_PerWritePageSize; } while (len--) { SPI_FLASH_SendByte (*pbuf++); } PIN_FLASH_CS_HIGH(); do { temp = FLASH_ReadStatusReg(); } while (temp & 0x01 ); } void W25XXX_WR_Block (uint8_t *pbuf, uint32_t address, uint32_t len) { uint8_t NumOfPage = 0 , NumOfSingle = 0 , Addr = 0 , count = 0 , temp = 0 ; Addr = address % SPI_FLASH_PageSize; count = SPI_FLASH_PageSize - Addr; NumOfPage = len / SPI_FLASH_PageSize; NumOfSingle = len % SPI_FLASH_PageSize; if (Addr == 0 ) { if (NumOfPage == 0 ) { W25XXX_WR_Page (pbuf, address, len); } else { while (NumOfPage--) { W25XXX_WR_Page (pbuf, address, SPI_FLASH_PageSize); address += SPI_FLASH_PageSize; pbuf += SPI_FLASH_PageSize; } W25XXX_WR_Page (pbuf, address, NumOfSingle); } } else { if (NumOfPage == 0 ) { if (NumOfSingle > count) { temp = NumOfSingle - count; W25XXX_WR_Page (pbuf, address, count); address += count; pbuf += count; W25XXX_WR_Page (pbuf, address, temp); } else { W25XXX_WR_Page (pbuf, address, len); } } else { len -= count; NumOfPage = len / SPI_FLASH_PageSize; NumOfSingle = len % SPI_FLASH_PageSize; W25XXX_WR_Page (pbuf, address, count); address += count; pbuf += count; while (NumOfPage--) { W25XXX_WR_Page (pbuf, address, SPI_FLASH_PageSize); address += SPI_FLASH_PageSize; pbuf += SPI_FLASH_PageSize; } if (NumOfSingle != 0 ) { W25XXX_WR_Page (pbuf, address, NumOfSingle); } } } } void FLASH_IC_Check (void ) { uint32_t count; Flash_ID = FLASH_ReadID(); Flash_Type = 0x00 ; Flash_Sector_Count = 0x00 ; Flash_Sector_Size = 0x00 ; switch (Flash_ID) { case W25X10_FLASH_ID: count = 1 ; break ; case W25X20_FLASH_ID: count = 2 ; break ; case W25X40_FLASH_ID: count = 4 ; break ; case W25X80_FLASH_ID: count = 8 ; break ; case W25Q16_FLASH_ID1: case W25Q16_FLASH_ID2: count = 16 ; break ; case W25Q32_FLASH_ID1: case W25Q32_FLASH_ID2: count = 32 ; break ; case W25Q64_FLASH_ID1: case W25Q64_FLASH_ID2: count = 64 ; break ; case W25Q128_FLASH_ID1: case W25Q128_FLASH_ID2: count = 128 ; break ; case W25Q256_FLASH_ID1: case W25Q256_FLASH_ID2: count = 256 ; break ; default : if ((Flash_ID != 0xFFFFFFFF ) && (Flash_ID != 0x00000000 )) { count = 16 ; } else { count = 0x00 ; } break ; } count = ((uint32_t )count * 1024 ) * ((uint32_t )1024 / 8 ); if (count) { Flash_Sector_Count = count / DEF_SECTOR_SIZE; Flash_Sector_Size = DEF_SECTOR_SIZE; } else { } } uint8_t FLASH_Init(void ){ PIN_FLASH_CS_LOW(); SPI_FLASH_SendByte(CMD_FLASH_RESET_ENABLE); SPI_FLASH_SendByte(CMD_FLASH_RESET_MEMORY); PIN_FLASH_CS_HIGH(); FLASH_IC_Check(); return FLASH_ReadStatusReg(); }
spi_flash.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 #ifndef _SPI_FLASH_H_ #define _SPI_FLASH_H_ #include "spi.h" #define PIN_FLASH_CS_LOW() SPI1_CS_LOW() #define PIN_FLASH_CS_HIGH() SPI1_CS_HIGH() #define CMD_FLASH_READ 0x03 #define CMD_FLASH_SECTOR_ERASE 0x20 #define CMD_FLASH_BYTE_PROG 0x02 #define CMD_FLASH_RDSR 0x05 #define CMD_FLASH_EWSR 0x50 #define CMD_FLASH_WREN 0x06 #define CMD_FLASH_WRDI 0x04 #define CMD_FLASH_JEDEC_ID 0x9F #define CMD_FLASH_RESET_ENABLE 0x66 #define CMD_FLASH_RESET_MEMORY 0x99 #define DEF_DUMMY_BYTE 0xFF #define SPI_FLASH_SectorSize 4096 #define SPI_FLASH_PageSize 256 #define SPI_FLASH_PerWritePageSize 256 #define DEF_TYPE_W25XXX 0 #define DEF_SECTOR_SIZE 4096 #define SPI_FLASH_OK ((uint8_t)0x00) #define SPI_FLASH_ERROR ((uint8_t)0x01) #define SPI_FLASH_BUSY ((uint8_t)0x02) #define SPI_FLASH_TIMEOUT ((uint8_t)0x03) #define W25X10_FLASH_ID 0xEF3011 #define W25X20_FLASH_ID 0xEF3012 #define W25X40_FLASH_ID 0xEF3013 #define W25X80_FLASH_ID 0xEF4014 #define W25Q16_FLASH_ID1 0xEF3015 #define W25Q16_FLASH_ID2 0xEF4015 #define W25Q32_FLASH_ID1 0xEF4016 #define W25Q32_FLASH_ID2 0xEF6016 #define W25Q64_FLASH_ID1 0xEF4017 #define W25Q64_FLASH_ID2 0xEF6017 #define W25Q128_FLASH_ID1 0xEF4018 #define W25Q128_FLASH_ID2 0xEF6018 #define W25Q256_FLASH_ID1 0xEF4019 #define W25Q256_FLASH_ID2 0xEF6019 extern volatile uint8_t Flash_Type; extern volatile uint32_t Flash_ID; extern volatile uint32_t Flash_Sector_Count; extern volatile uint16_t Flash_Sector_Size; extern uint8_t FLASH_Init (void ) ;extern uint8_t SPI_FLASH_SendByte (uint8_t byte) ;extern uint8_t SPI_FLASH_ReadByte (void ) ;extern uint32_t FLASH_ReadID (void ) ;extern void FLASH_WriteEnable (void ) ;extern void FLASH_WriteDisable (void ) ;extern uint8_t FLASH_ReadStatusReg (void ) ;extern void FLASH_IC_Check (void ) ;extern void FLASH_Erase_Sector (uint32_t address) ;extern void FLASH_RD_Block_Start (uint32_t address) ;extern void FLASH_RD_Block (uint8_t *pbuf, uint32_t len) ;extern void FLASH_RD_Block_End (void ) ;extern void W25XXX_WR_Block (uint8_t *pbuf, uint32_t address, uint32_t len) ;#endif
该驱动程序在SPI1上实现了对SPI FLASH的读写操作,包括初始化、读取ID、写入使能、写入禁用、读取状态寄存器、检查IC、擦除扇区、读取块、写入块等操作;
程序中涉及的 User_SPI1_ReadWriteByte 定义如下:
1 2 3 4 5 6 7 8 uint8_t User_SPI1_ReadWriteByte (uint8_t TxData) { uint8_t RxData = 0 ; HAL_SPI_TransmitReceive(&hspi1, &TxData, &RxData, 1 , 0xFFFF ); return RxData; }
片选线的控制函数定义如下:
1 2 3 4 5 6 7 8 9 10 11 #define SPI1_CS_LOW() do \ { \ HAL_GPIO_WritePin(SPI1_CS_GPIO_Port, SPI1_CS_Pin, GPIO_PIN_RESET); \ } \ while(0) #define SPI1_CS_HIGH() do \ { \ HAL_GPIO_WritePin(SPI1_CS_GPIO_Port, SPI1_CS_Pin, GPIO_PIN_SET); \ } \ while(0)
执行FLASH_IC_Check函数之后,函数会根据返回的芯片 ID,设置Flash_Type、Flash_ID、Flash_Sector_Count、Flash_Sector_Size等变量,以便后续操作使用;
移植 FatFS 实现 diskio.c 接口 主要就是需要编写 diskio.c 文件,实现以下函数(可以直接复制):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 #include "ff.h" #include "diskio.h" #include "spi_flash.h" #define DEV_SPIFLASH 0 DSTATUS disk_status ( BYTE pdrv ) { DSTATUS stat; uint8_t result; switch (pdrv) { case DEV_SPIFLASH : result = FLASH_ReadStatusReg(); switch (result) { case SPI_FLASH_OK: stat = STA_NOINIT & (~STA_NOINIT); break ; case SPI_FLASH_ERROR: stat = STA_NOINIT; break ; case SPI_FLASH_BUSY: stat = STA_NOINIT; break ; case SPI_FLASH_TIMEOUT: stat = STA_NOINIT; break ; } return stat; } return STA_NOINIT; } DSTATUS disk_initialize ( BYTE pdrv ) { DSTATUS stat; uint8_t result; switch (pdrv) { case DEV_SPIFLASH : result = FLASH_Init(); switch (result) { case SPI_FLASH_OK: stat = STA_NOINIT & (~STA_NOINIT); break ; case SPI_FLASH_ERROR: stat = STA_NOINIT; break ; case SPI_FLASH_BUSY: stat = STA_NOINIT; break ; case SPI_FLASH_TIMEOUT: stat = STA_NOINIT; break ; } return stat; } return STA_NOINIT; } DRESULT disk_read ( BYTE pdrv, BYTE* buff, LBA_t sector, UINT count ) { DRESULT res; uint8_t result; switch (pdrv) { case DEV_SPIFLASH : FLASH_RD_Block_Start(sector * 4096 ); FLASH_RD_Block(buff, count * 4096 ); FLASH_RD_Block_End(); result = FLASH_ReadStatusReg(); switch (result) { case SPI_FLASH_OK: res = RES_OK; break ; case SPI_FLASH_ERROR: res = RES_ERROR; break ; case SPI_FLASH_BUSY: res = RES_NOTRDY; break ; case SPI_FLASH_TIMEOUT: res = RES_ERROR; break ; } return res; } return RES_PARERR; } #if FF_FS_READONLY == 0 DRESULT disk_write ( BYTE pdrv, const BYTE* buff, LBA_t sector, UINT count ) { DRESULT res; int result; switch (pdrv) { case DEV_SPIFLASH : for (UINT i = 0 ; i < count; i++) { FLASH_Erase_Sector((sector + i) * 4096 ); } W25XXX_WR_Block((uint8_t *)buff, sector * 4096 , count * 4096 ); result = FLASH_ReadStatusReg(); switch (result) { case SPI_FLASH_OK: res = RES_OK; break ; case SPI_FLASH_ERROR: res = RES_ERROR; break ; case SPI_FLASH_BUSY: res = RES_NOTRDY; break ; case SPI_FLASH_TIMEOUT: res = RES_ERROR; break ; } return res; } return RES_PARERR; } #endif DRESULT disk_ioctl ( BYTE pdrv, BYTE cmd, void * buff ) { DRESULT res = RES_OK; switch (pdrv) { case DEV_SPIFLASH : switch (cmd) { case GET_SECTOR_COUNT: { *(DWORD*)buff = Flash_Sector_Count; break ; } case GET_SECTOR_SIZE: { *(WORD*)buff = Flash_Sector_Size; break ; } case GET_BLOCK_SIZE: { *(DWORD*)buff = 1 ; break ; } } return res; } return RES_PARERR; }
修改 ffconf.h 配置 1 2 3 4 5 6 7 #define FF_MAX_SS 4096 #define FF_USE_MKFS 1 #define FF_CODE_PAGE 936 #define FF_FS_NORTC 1
使用 main.c 文件中大体如下操作即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 #include "ff.h" FATFS FsObject; FIL fp; static BYTE work_buffer[4096 ];int main (void ) { FRESULT result; result=f_mount(&FsObject,"0:" ,1 ); if (result == 13 ) { MKFS_PARM Format = {FM_FAT32, 0 , 0 , 0 , 0 }; result = f_mkfs("0:" , &Format, work_buffer, sizeof (work_buffer)); result=f_mount(&FsObject,"0:" ,1 ); } result = f_open(&fp, "0:test.txt" , FA_OPEN_ALWAYS|FA_WRITE|FA_READ); UINT test; uint8_t read[20 ]; result=f_read(&fp, read, f_size(&fp), &test); while (1 ) { } return 0 ; }
常见问题与调试技巧 Flash 相关问题 挂载与格式化相关 Q: f_mount 返回 FR_NO_FILESYSTEM(13),怎么解决? A: 说明当前设备上没有可识别的文件系统;应使用 f_mkfs 对 Flash 进行格式化,完成后再调用 f_mount 重新挂载;
Q: f_mount 返回 FR_INVALID_DRIVE(11)? A: 说明 FatFS 的卷编号配置有问题,请检查 ffconf.h 中 FF_VOLUMES 是否 >= 你的逻辑盘号,比如 f_mount(…, “0:”, 1) 表示你至少得设置 #define FF_VOLUMES 1;
FAT 文件系统格式相关 Q: f_mkfs() 返回 FR_INVALID_PARAMETER? A: f_mkfs 参数设置不当,建议使用如下方式初始化:1 2 MKFS_PARM fs_param = {FM_ANY, 0 , 0 , 0 , 0 }; f_mkfs("0:" , &fs_param, work_buffer, sizeof (work_buffer));
Q: 格式化完文件系统容量很小(比如识别为1MB)? A: 可能是扇区大小未正确返回,检查 disk_ioctl() 中 GET_SECTOR_SIZE 和 GET_SECTOR_COUNT 是否准确计算,是否符合你实际 Flash 容量;读写文件异常 Q: 文件写入后读出来的数据不对,乱码或者全 0? A: 可能原因
写入之前没有正确擦除扇区; 写操作未对齐页写入(W25系列对页写入有要求); 写入数据后未调用 f_close() 或 f_sync(),导致未刷新缓存到 Flash; diskio.c 中 FLASH_Erase_Sector() 和 W25XXX_WR_Block() 地址未正确计算; Q: 写文件成功了,但再次打开文件内容变空? A: 注意写模式是否是 FA_CREATE_ALWAYS,该模式会每次打开都清空内容;如果想保留内容,改为 FA_OPEN_ALWAYS | FA_WRITE 并调用 f_lseek(&fp, f_size(&fp)) 跳到末尾再写;
文件系统行为与配置相关 Q: 文件名太长无法识别? A: 默认 FatFS 禁用长文件名(LFN),需在 ffconf.h 中配置:1 2 #define FF_USE_LFN 1 #define FF_MAX_LFN 64
Q: 中文文件名乱码? A: 请设置正确的代码页,例如:1 #define FF_CODE_PAGE 936
Q: 同一个文件写入后再读读取不到内容? A: 若写入后未关闭文件或调用 f_sync(),FatFS 可能未刷新数据到底层 Flash,建议:1 2 f_write(...); f_sync(&fp);
运行异常 / 稳定性问题 调试技巧 1 2 3 4 5 6 7 f_open(&fp, "test.txt" , FA_CREATE_ALWAYS | FA_WRITE); f_write(&fp, "hello" , 5 , &bw); f_close(&fp); f_open(&fp, "test.txt" , FA_READ); f_read(&fp, buf, 5 , &br); f_close(&fp);
总结 本篇博客详细介绍了如何将 FatFS 移植到 SPI Flash,并通过 W25Q128 实现文件读写功能;从驱动实现、FatFS 配置、文件操作到问题排查,整个流程强调的是「实用」与「稳定」,希望对你的嵌入式项目有所帮助;
值得注意的是,SPI Flash 天生具备“写前擦除”“页擦除/块擦除”的特性,且写入寿命有限 (一般每个扇区约 10 万次擦写);这意味着在频繁写入场景下,Flash 容易出现写坏、性能衰减等问题;
为此,建议关注以下几点:
磨损均衡(Wear Leveling) :FatFS 本身不具备磨损均衡机制,如果使用 SPI Flash 存储频繁变更的数据(如日志、数据库),需要在应用层实现“循环覆盖”或“动态地址映射”来避免单点反复擦写;避免频繁格式化和 f_open/f_write/f_close 操作循环 ,应尽可能复用文件句柄,按需 flush 写入;设置合适的缓存机制 ,如启用 sector 缓冲,减少物理擦写次数;建议定期备份重要数据 ,并在系统初始化时进行 Flash 健康检查(可利用空闲位、标志位判断 Flash 是否写满或擦损);日志/配置文件等 建议使用固定格式(如简化版 TLV)写入,便于恢复和分析;最后,虽然 FatFS 的结构设计优雅轻量,但在用它搭配 SPI Flash 构建嵌入式文件系统时,我们仍需深入理解底层 Flash 的行为特性,并结合自身项目场景做出相应调整和优化;
下一步,我计划基于 USB Composite(复合设备) 实现 STM32 同时具备串口调试和 模拟U盘功能 ,通过 USB MSC 协议挂载 FatFS 文件系统,让用户能够在电脑端直接读写 SPI Flash 中的数据,这将进一步提升系统的易用性和可扩展性,敬请期待!