从 0 搭建 SPI Flash 文件系统:驱动、FatFS、读写与坑点
EMTime

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 的移植主要包括以下几个步骤:

  1. 准备底层存储驱动(如 SPI Flash 驱动)
  2. 实现 FatFS 所需的 diskio.c 接口函数
  3. 配置 ffconf.h 以满足你的文件系统需求
  4. 在主函数中初始化 FatFS,挂载文件系统
  5. 实现文件的读写操作测试

接下来,我们从第一步开始,移植 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; /* FLASH chip: 0: W25XXXseries */
volatile uint32_t Flash_ID = 0x00; /* FLASH ID */
volatile uint32_t Flash_Sector_Count = 0x00; /* FLASH sector number */
volatile uint16_t Flash_Sector_Size = 0x00; /* FLASH sector size */

/*******************************************************************************
* Function Name : SPI_FLASH_SendByte
* Description : SPI send a byte
* Input : byte: byte to send
* Output : None
* Return : None
*******************************************************************************/
uint8_t SPI_FLASH_SendByte (uint8_t byte)
{
return User_SPI1_ReadWriteByte (byte);
}

/*******************************************************************************
* Function Name : SPI_FLASH_ReadByte
* Description : SPI receive a byte
* Input : None
* Output : None
* Return : byte received
*******************************************************************************/
uint8_t SPI_FLASH_ReadByte (void)
{
return User_SPI1_ReadWriteByte (0xFF);
}

/*******************************************************************************
* Function Name : FLASH_ReadID
* Description : Read FLASH ID
* Input : None
* Output : None
* Return : chip id
*******************************************************************************/
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);
}

/*******************************************************************************
* Function Name : FLASH_WriteEnable
* Description : FLASH Write Enable
* Input : None
* Output : None
* Return : None
*******************************************************************************/
void FLASH_WriteEnable (void)
{
PIN_FLASH_CS_LOW();
SPI_FLASH_SendByte (CMD_FLASH_WREN);
PIN_FLASH_CS_HIGH();
}

/*******************************************************************************
* Function Name : FLASH_WriteDisable
* Description : FLASH Write Disable
* Input : None
* Output : None
* Return : None
*******************************************************************************/
void FLASH_WriteDisable (void)
{
PIN_FLASH_CS_LOW();
SPI_FLASH_SendByte (CMD_FLASH_WRDI);
PIN_FLASH_CS_HIGH();
}

/*******************************************************************************
* Function Name : FLASH_ReadStatusReg
* Description : FLASH Read Status
* Input : None
* Output : None
* Return : status
*******************************************************************************/
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);
}

/*******************************************************************************
* Function Name : FLASH_Erase_Sector
* Description : FLASH Erase Sector
* Input : None
* Output : None
* Return : None
*******************************************************************************/
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);
}

/*******************************************************************************
* Function Name : FLASH_RD_Block_Start
* Description : FLASH start block read
* Input : None
* Output : None
* Return : None
*******************************************************************************/
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);
}

/*******************************************************************************
* Function Name : FLASH_RD_Block
* Description : FLASH read block
* Input : None
* Output : None
* Return : None
*******************************************************************************/
void FLASH_RD_Block (uint8_t *pbuf, uint32_t len)
{
while (len--)
{
*pbuf++ = SPI_FLASH_ReadByte();
}
}

/*******************************************************************************
* Function Name : FLASH_RD_Block_End
* Description : FLASH end block read
* Input : None
* Output : None
* Return : None
*******************************************************************************/
void FLASH_RD_Block_End (void)
{
PIN_FLASH_CS_HIGH();
}

/*******************************************************************************
* Function Name : W25XXX_WR_Page
* Description : Flash page program
* Input : address
* len
* *pbuf
* Output : None
* Return : None
*******************************************************************************/
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);
}

/*******************************************************************************
* Function Name : W25XXX_WR_Block
* Description : W25XXX block write
* Input : address
* len
* *pbuf
* Output : None
* Return : None
*******************************************************************************/
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);
}
}
}
}

/*******************************************************************************
* Function Name : FLASH_IC_Check
* Description : check flash type
* Input : None
* Output : None
* Return : None
*******************************************************************************/
void FLASH_IC_Check (void)
{
uint32_t count;

/* Read FLASH ID */
Flash_ID = FLASH_ReadID();
// printf ("Flash_ID: %08x\n", (uint32_t)Flash_ID);

Flash_Type = 0x00;
Flash_Sector_Count = 0x00;
Flash_Sector_Size = 0x00;

switch (Flash_ID)
{
/* W25XXX */
case W25X10_FLASH_ID: /* 0xEF3011-----1M bit */
count = 1;
break;

case W25X20_FLASH_ID: /* 0xEF3012-----2M bit */
count = 2;
break;

case W25X40_FLASH_ID: /* 0xEF3013-----4M bit */
count = 4;
break;

case W25X80_FLASH_ID: /* 0xEF4014-----8M bit */
count = 8;
break;

case W25Q16_FLASH_ID1: /* 0xEF3015-----16M bit */
case W25Q16_FLASH_ID2: /* 0xEF4015-----16M bit */
count = 16;
break;

case W25Q32_FLASH_ID1: /* 0xEF4016-----32M bit */
case W25Q32_FLASH_ID2: /* 0xEF6016-----32M bit */
count = 32;
break;

case W25Q64_FLASH_ID1: /* 0xEF4017-----64M bit */
case W25Q64_FLASH_ID2: /* 0xEF6017-----64M bit */
count = 64;
break;

case W25Q128_FLASH_ID1: /* 0xEF4018-----128M bit */
case W25Q128_FLASH_ID2: /* 0xEF6018-----128M bit */
count = 128;
break;

case W25Q256_FLASH_ID1: /* 0xEF4019-----256M bit */
case W25Q256_FLASH_ID2: /* 0xEF6019-----256M bit */
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)
{
// 如果是内部,那么DEF_UDISK_SECTOR_SIZE是512,如果是外部,则DEF_SECTOR_SIZE是4096
Flash_Sector_Count = count / DEF_SECTOR_SIZE; // DEF_SECTOR_SIZE;
Flash_Sector_Size = DEF_SECTOR_SIZE; // DEF_SECTOR_SIZE;
// printf ("\r\nFlash_Sector_Count:%d\r\n", Flash_Sector_Count);
// printf ("Flash_Sector_Size:%d\r\n", Flash_Sector_Size);
}
else
{
// printf ("External Flash not connected\r\n");
// while(1);
}
}

/*******************************************************************************
* Function Name : FLASH_Init
* Description : init spi flash
* Input : None
* Output : None
* Return : status
*******************************************************************************/
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"

/* SPI Pins */
#define PIN_FLASH_CS_LOW() SPI1_CS_LOW()

#define PIN_FLASH_CS_HIGH() SPI1_CS_HIGH()

/******************************************************************************/
/* SPI Serial Flash OPERATION INSTRUCTIONS */
#define CMD_FLASH_READ 0x03 /* Read Memory at 25 MHz */
#define CMD_FLASH_SECTOR_ERASE 0x20 /* Erase 4 KByte of memory array */
#define CMD_FLASH_BYTE_PROG 0x02 /* To Program One Data Byte */
#define CMD_FLASH_RDSR 0x05 /* Read-Status-Register */
#define CMD_FLASH_EWSR 0x50 /* Enable-Write-Status-Register */
#define CMD_FLASH_WREN 0x06 /* Write-Enable */
#define CMD_FLASH_WRDI 0x04 /* Write-Disable */
#define CMD_FLASH_JEDEC_ID 0x9F /* JEDEC ID read */

#define CMD_FLASH_RESET_ENABLE 0x66
#define CMD_FLASH_RESET_MEMORY 0x99

/******************************************************************************/
#define DEF_DUMMY_BYTE 0xFF

/******************************************************************************/
/* FLASH Parameter Definition */
#define SPI_FLASH_SectorSize 4096
#define SPI_FLASH_PageSize 256
#define SPI_FLASH_PerWritePageSize 256

/******************************************************************************/
/* SPI FLASH Type */
#define DEF_TYPE_W25XXX 0 /* W25XXX */

#define DEF_SECTOR_SIZE 4096

/* SPI FLASH STATUS */
#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)

/******************************************************************************/
/* SPI FLASH Type Define */

/* W25XXX*/
#define W25X10_FLASH_ID 0xEF3011 /* 1M bit */
#define W25X20_FLASH_ID 0xEF3012 /* 2M bit */
#define W25X40_FLASH_ID 0xEF3013 /* 4M bit */
#define W25X80_FLASH_ID 0xEF4014 /* 8M bit */
#define W25Q16_FLASH_ID1 0xEF3015 /* 16M bit */
#define W25Q16_FLASH_ID2 0xEF4015 /* 16M bit */
#define W25Q32_FLASH_ID1 0xEF4016 /* 32M bit */
#define W25Q32_FLASH_ID2 0xEF6016 /* 32M bit */
#define W25Q64_FLASH_ID1 0xEF4017 /* 64M bit */
#define W25Q64_FLASH_ID2 0xEF6017 /* 64M bit */
#define W25Q128_FLASH_ID1 0xEF4018 /* 128M bit */
#define W25Q128_FLASH_ID2 0xEF6018 /* 128M bit */
#define W25Q256_FLASH_ID1 0xEF4019 /* 256M bit */
#define W25Q256_FLASH_ID2 0xEF6019 /* 256M bit */

/******************************************************************************/
/* Variable Definition */
extern volatile uint8_t Flash_Type; /* FLASH chip: 0: W25XXXseries */
extern volatile uint32_t Flash_ID; /* FLASH ID */
extern volatile uint32_t Flash_Sector_Count; /* FLASH sector number */
extern volatile uint16_t Flash_Sector_Size; /* FLASH sector size */

/******************************************************************************/
/* external functions */
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 /* _SPI_FLASH_H_ */

该驱动程序在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
/*-----------------------------------------------------------------------*/
/* Low level disk I/O module SKELETON for FatFs (C)ChaN, 2019 */
/*-----------------------------------------------------------------------*/
/* If a working storage control module is available, it should be */
/* attached to the FatFs via a glue function rather than modifying it. */
/* This is an example of glue functions to attach various exsisting */
/* storage control modules to the FatFs module with a defined API. */
/*-----------------------------------------------------------------------*/

#include "ff.h" /* Obtains integer types */
#include "diskio.h" /* Declarations of disk functions */

#include "spi_flash.h"

/* Definitions of physical drive number for each drive */
//#define DEV_RAM 0 /* Example: Map Ramdisk to physical drive 0 */
//#define DEV_MMC 1 /* Example: Map MMC/SD card to physical drive 1 */
//#define DEV_USB 2 /* Example: Map USB MSD to physical drive 2 */
#define DEV_SPIFLASH 0

/*-----------------------------------------------------------------------*/
/* Get Drive Status */
/*-----------------------------------------------------------------------*/

DSTATUS disk_status(
BYTE pdrv /* Physical drive nmuber to identify the drive */
)
{
DSTATUS stat;
uint8_t result;

switch(pdrv)
{
case DEV_SPIFLASH :
result = FLASH_ReadStatusReg();

// translate the reslut code here
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;
}



/*-----------------------------------------------------------------------*/
/* Inidialize a Drive */
/*-----------------------------------------------------------------------*/

DSTATUS disk_initialize(
BYTE pdrv /* Physical drive nmuber to identify the drive */
)
{
DSTATUS stat;
uint8_t result;

switch(pdrv)
{
case DEV_SPIFLASH :
result = FLASH_Init();

// translate the reslut code here
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;
}

/*-----------------------------------------------------------------------*/
/* Read Sector(s) */
/*-----------------------------------------------------------------------*/

DRESULT disk_read(
BYTE pdrv, /* Physical drive nmuber to identify the drive */
BYTE* buff, /* Data buffer to store read data */
LBA_t sector, /* Start sector in LBA */
UINT count /* Number of sectors to read */
)
{
DRESULT res;
uint8_t result;

switch(pdrv)
{
case DEV_SPIFLASH :
// translate the arguments here

FLASH_RD_Block_Start(sector * 4096);
FLASH_RD_Block(buff, count * 4096);
FLASH_RD_Block_End();

result = FLASH_ReadStatusReg();

// translate the reslut code here
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;
}

/*-----------------------------------------------------------------------*/
/* Write Sector(s) */
/*-----------------------------------------------------------------------*/

#if FF_FS_READONLY == 0

DRESULT disk_write(
BYTE pdrv, /* Physical drive nmuber to identify the drive */
const BYTE* buff, /* Data to be written */
LBA_t sector, /* Start sector in LBA */
UINT count /* Number of sectors to write */
)
{
DRESULT res;
int result;

switch(pdrv)
{
case DEV_SPIFLASH :
// translate the arguments here

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();

// translate the reslut code here
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

/*-----------------------------------------------------------------------*/
/* Miscellaneous Functions */
/*-----------------------------------------------------------------------*/

DRESULT disk_ioctl(
BYTE pdrv, /* Physical drive nmuber (0..) */
BYTE cmd, /* Control code */
void* buff /* Buffer to send/receive control data */
)
{
DRESULT res = RES_OK;
// int result;

switch(pdrv)
{
case DEV_SPIFLASH :
switch(cmd)
{
case GET_SECTOR_COUNT://将驱动器上可用扇区的数目返回到buff指向的DWORD变量中
{
/* 扇区数量:1536*4096/1024/1024=6(MB) */
*(DWORD*)buff = Flash_Sector_Count; // 1536; //要的是DWORD类型的数据,所以给这个形参返回这样的数据
break;
}
case GET_SECTOR_SIZE://将媒体的扇区大小返回到buff指向的WORD变量中
{
*(WORD*)buff = Flash_Sector_Size; //类型是WORD的类型,每个扇区是4096的大小,这里同时还需要修改MAX_SS的值
break;
}
case GET_BLOCK_SIZE://将闪存介质的擦除块大小(以扇区为单位)返回到buff指向的DWORD变量中
{
*(DWORD*)buff = 1; //每次擦除的大小是1个扇区,因为单位是扇区
break;
}
}
// Process of the command for the RAM drive

return res;

}

return RES_PARERR;
}

修改 ffconf.h 配置

1
2
3
4
5
6
7
#define FF_MAX_SS		4096  // 使用的是SPI FLash,所以这个需要修改为4096

#define FF_USE_MKFS 1 // 这个需要修改为1启用格式化的功能

#define FF_CODE_PAGE 936 // 可以设置成936,增加对中文的支持

#define FF_FS_NORTC 1 // 这个需要设置成1,就是先不搞RTC相关的日期功能

使用

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); // 这个0:就是路径,和#define FF_VOLUMES 1有关,设定为1则路径只有0:

// 如果是13,则表明没有格式化,我们进行格式化
// 如果是11,则表明数量对不上,需要去改设备个数,也就是FF_VOLUMES
if(result == 13)
{
MKFS_PARM Format = {FM_FAT32, 0, 0, 0, 0}; // 为了兼容,可以改为FM_ANY,对16MB来说,FAT16最合适
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;
// result = f_write(&fp,"test1234",sizeof("test1234"),&test);
// f_close(&fp);


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: FLASH_ReadID() 返回 0xFFFFFF 或 0x000000?
    A: SPI Flash 没有接好,或者 SPI 接口初始化未正确完成;建议检查:

    • SPI 时钟、模式是否与 Flash 兼容;
    • Flash 供电是否稳定;
    • CS 引脚是否正确拉低后开始通信;
  • Q: Flash 容量识别不对?
    A: FLASH_IC_Check() 中只处理了常见型号,若你使用的是不在列表内的型号,请根据 datasheet 添加对应的 JEDEC ID 和容量;

挂载与格式化相关

  • 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  // 简体中文 GBK
  • Q: 同一个文件写入后再读读取不到内容?
    A: 若写入后未关闭文件或调用 f_sync(),FatFS 可能未刷新数据到底层 Flash,建议:
1
2
f_write(...);
f_sync(&fp); // 确保数据落盘

运行异常 / 稳定性问题

  • Q: 写入操作中系统卡死或死循环?
    A: Flash 的写入/擦除是阻塞操作,部分芯片擦除单个扇区可能耗时几十毫秒;建议你:

    • 在写函数中加入 watchdog 喂狗机制;
    • 考虑用非阻塞 Flash 驱动 + 文件系统缓存策略来优化;
  • Q: Flash 写入过程中掉电,数据损坏?
    A: 推荐使用 FatFS 的事务机制,例如在写入文件时增加 f_sync(),或使用 FAT 的备用区功能(需高级配置);另外也可以借助 CRC 校验机制保证文件有效性;

调试技巧

  • 建议在初始化完成后先跑一个简单的读写测试函数
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);
  • 使用串口或者 RTT 等工具打印中间步骤结果,比如挂载结果、读写返回值、实际读出的内容,有助于快速定位问题;

  • 调试期间建议将所有错误码都打印出来对应 FR_XXX 含义,便于对照 FatFS 源码中的错误枚举;

总结

本篇博客详细介绍了如何将 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 中的数据,这将进一步提升系统的易用性和可扩展性,敬请期待!

 Comments
Comment plugin failed to load
Loading comment plugin
💡 请正确填写您的邮箱,以确保能接收到来自本博客的评论回复通知~(*^▽^*)