QSPI Flash 读写:擦除、页写与映射读回校验
源码路径:example/HC32F460JEUA/5.qspi/qspi_four_wire_io_fast_read/source/main.c
1. 功能简介
这个例子演示完整的外部 Flash 读写流程,也就是「给单片机外挂的硬盘存东西、再读出来核对」。
程序逻辑:
- 初始化 QSPI,默认配成四线快速读模式。
- 主循环里等你按下按键。
- 按键一按下,执行一整套 Flash 操作:
- 切到标准(单线)模式 → 擦除一个 4KB 扇区 → 把一句话写进去
- 切回四线模式 → 通过映射地址
0x98000000 像读数组一样把数据读回来
- 用
memcmp 对比写进去的和读出来的是否一致
- 一致亮绿灯,不一致亮红灯
- 地址往后挪 4KB,等你下次按键,测下一个扇区。
写进去的内容是这句话:
1
|
char txBuffer[] = "QSPI read and write flash example: Welcome to use HDSC micro chip";
|
每按一次按键,就测试一个新扇区。这样连续按下去,可以逐个扇区把整片 Flash 都验一遍——这就是 Flash 老化测试/出厂检测的雏形。
2. 硬件准备
用 HC32F460JEUA KIT 开发板,板载已经焊好了 QSPI Flash 芯片(W25Qxx 系列,8MB),六根线都接好了,无需接线。
1
2
3
4
5
6
7
8
9
10
|
HC32F460 QSPI Flash (W25Q64)
┌───────────┐ ┌──────────────────┐
│ PB14 QSPCK├───────────────────▶│ CLK │
│ PB1 QSNSS├───────────────────▶│ /CS (片选) │
│ PB13 QSIO0├◀──────────────────▶│ DI/IO0 │
│ PB12 QSIO1├◀──────────────────▶│ DO/IO1 │
│ PB10 QSIO2├◀──────────────────▶│ /WP·IO2 │
│ PB2 QSIO3├◀──────────────────▶│ /HOLD·IO3 │
└───────────┘ └──────────────────┘
容量 8MB (0x800000)
|
注意 IO2、IO3 在单线 SPI 模式下是 /WP(写保护)和 /HOLD 脚,在四线模式下才变成数据线——这也是为什么四线模式需要额外配置。
还要用到:
- 按键 K1(PC13):触发测试
- 红/绿 LED(PA0/PA1):显示测试结果
3. 完整代码
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
|
#include "hc32_ddl.h"
/* ---------- QSPI 六根引脚定义 ---------- */
#define QSPCK_PORT (PortB) /* 时钟 */
#define QSPCK_PIN (Pin14)
#define QSNSS_PORT (PortB) /* 片选 */
#define QSNSS_PIN (Pin01)
#define QSIO0_PORT (PortB) /* 数据0 */
#define QSIO0_PIN (Pin13)
#define QSIO1_PORT (PortB) /* 数据1 */
#define QSIO1_PIN (Pin12)
#define QSIO2_PORT (PortB) /* 数据2 */
#define QSIO2_PIN (Pin10)
#define QSIO3_PORT (PortB) /* 数据3 */
#define QSIO3_PIN (Pin02)
/* QSPI 映射到系统地址空间的基地址 —— 读 Flash 就靠它 */
#define QSPI_BUS_ADDRESS (0x98000000ul)
/* ---------- Flash 参数 ---------- */
#define FLASH_PAGE_SIZE (0x100u) /* 页 = 256 字节(写的最大单位)*/
#define FLASH_SECTOR_SIZE (0x1000u) /* 扇区 = 4096 字节(擦的最小单位)*/
#define FLASH_MAX_ADDR (0x800000ul) /* 容量 8MB */
#define FLASH_DUMMY_BYTE_VALUE (0xffu)
#define FLASH_BUSY_BIT_MASK (0x01u) /* 状态寄存器 bit0 = 忙碌位 */
/* ---------- Flash 标准指令码 ---------- */
#define FLASH_INSTR_WRITE_ENABLE (0x06u) /* 写使能 */
#define FLASH_INSTR_PAGE_PROGRAM (0x02u) /* 页写 */
#define FLASH_INSTR_ERASE_4KB_SECTOR (0x20u) /* 擦除 4KB 扇区 */
#define FLASH_INSTR_ERASE_CHIP (0xC7u) /* 全片擦除 */
#define FLASH_INSTR_READ_SR1 (0x05u) /* 读状态寄存器1 */
#define FLASH_INSTR_READ_SR2 (0x35u)
#define FLASH_INSTR_READ_SR3 (0x15u)
/*==============================================================
* QSPI 初始化
*=============================================================*/
void QspiFlash_Init(void)
{
stc_qspi_init_t stcQspiInit;
MEM_ZERO_STRUCT(stcQspiInit);
/* 开 QSPI 外设时钟 */
PWC_Fcg1PeriphClockCmd(PWC_FCG1_PERIPH_QSPI, Enable);
/* 六根引脚全部复用为 QSPI 功能 */
PORT_SetFunc(QSPCK_PORT, QSPCK_PIN, Func_Qspi, Disable);
PORT_SetFunc(QSNSS_PORT, QSNSS_PIN, Func_Qspi, Disable);
PORT_SetFunc(QSIO0_PORT, QSIO0_PIN, Func_Qspi, Disable);
PORT_SetFunc(QSIO1_PORT, QSIO1_PIN, Func_Qspi, Disable);
PORT_SetFunc(QSIO2_PORT, QSIO2_PIN, Func_Qspi, Disable);
PORT_SetFunc(QSIO3_PORT, QSIO3_PIN, Func_Qspi, Disable);
/* ---------- 填 QSPI 配置 ---------- */
stcQspiInit.enClkDiv = QspiHclkDiv3; /* QSPI 时钟 = HCLK/3 */
stcQspiInit.enSpiMode = QspiSpiMode3; /* SPI 模式 3 */
stcQspiInit.enBusCommMode = QspiBusModeRomAccess; /* ROM 映射访问模式 */
stcQspiInit.enPrefetchMode = QspiPrefetchStopComplete;
stcQspiInit.enPrefetchFuncEn = Disable; /* 预取功能关闭 */
stcQspiInit.enQssnValidExtendTime = QspiQssnValidExtendSck32;
stcQspiInit.enQssnIntervalTime = QspiQssnIntervalQsck8;
stcQspiInit.enQsckDutyCorr = QspiQsckDutyCorrHalfHclk;
stcQspiInit.enVirtualPeriod = QspiVirtualPeriodQsck6; /* 空周期(dummy) */
stcQspiInit.enWpPinLevel = QspiWpPinOutputHigh; /* WP 脚输出高 */
stcQspiInit.enQssnSetupDelayTime = QspiQssnSetupDelay1Dot5Qsck;
stcQspiInit.enQssnHoldDelayTime = QspiQssnHoldDelay1Dot5Qsck;
stcQspiInit.enFourByteAddrReadEn = Disable; /* 用 3 字节地址 */
stcQspiInit.enAddrWidth = QspiAddressByteThree;
/* 通信协议:读用四线 IO,指令/地址/接收都走扩展 SPI */
stcQspiInit.stcCommProtocol.enReadMode = QspiReadModeFourWiresIO;
stcQspiInit.stcCommProtocol.enTransInstrProtocol = QspiProtocolExtendSpi;
stcQspiInit.stcCommProtocol.enTransAddrProtocol = QspiProtocolExtendSpi;
stcQspiInit.stcCommProtocol.enReceProtocol = QspiProtocolExtendSpi;
/* ROM 访问时硬件自动发的读指令 */
stcQspiInit.u8RomAccessInstr = QSPI_3BINSTR_FOUR_WIRES_IO_READ;
QSPI_Init(&stcQspiInit);
}
/*==============================================================
* 写使能 —— 每次写/擦之前必须发一次
*=============================================================*/
void QspiFlash_WriteEnable(void)
{
QSPI_EnterDirectCommMode(); /* 进直通模式 */
QSPI_WriteDirectCommValue(FLASH_INSTR_WRITE_ENABLE); /* 发 0x06 */
QSPI_ExitDirectCommMode(); /* 退出 */
}
/*==============================================================
* 等待 Flash 内部操作完成(轮询忙碌位)
*=============================================================*/
en_result_t QspiFlash_WaitForWriteEnd(void)
{
en_result_t enRet = Ok;
uint8_t u8Status = 0u;
uint32_t u32Timeout;
stc_clk_freq_t stcClkFreq;
/* 用系统时钟频率算一个超时上限,防止死等 */
CLK_GetClockFreq(&stcClkFreq);
u32Timeout = stcClkFreq.sysclkFreq / 1000u;
QSPI_EnterDirectCommMode();
QSPI_WriteDirectCommValue(FLASH_INSTR_READ_SR1); /* 发读状态寄存器1指令 */
do
{
u8Status = QSPI_ReadDirectCommValue(); /* 不停读回状态 */
u32Timeout--;
} while ((u32Timeout != 0u) &&
((u8Status & FLASH_BUSY_BIT_MASK) == FLASH_BUSY_BIT_MASK)); /* bit0=1 表示忙 */
if (FLASH_BUSY_BIT_MASK == u8Status)
{
enRet = ErrorTimeout; /* 超时了还在忙 → 报超时 */
}
QSPI_ExitDirectCommMode();
return enRet;
}
/*==============================================================
* 页写:往指定地址写最多 256 字节
*=============================================================*/
en_result_t QspiFlash_WritePage(uint32_t u32Addr, const uint8_t pData[], uint16_t len)
{
en_result_t enRet = Ok;
uint16_t u16Index = 0u;
/* 参数合法性检查:地址越界 / 空指针 / 超过一页 */
if ((u32Addr > FLASH_MAX_ADDR) || (NULL == pData) || (len > FLASH_PAGE_SIZE))
{
enRet = Error;
}
else
{
QspiFlash_WriteEnable(); /* ① 写使能 */
QSPI_EnterDirectCommMode(); /* ② 进直通模式 */
QSPI_WriteDirectCommValue(FLASH_INSTR_PAGE_PROGRAM); /* 页写指令 0x02 */
/* ③ 发 3 字节地址,从高位到低位 */
QSPI_WriteDirectCommValue((uint8_t)((u32Addr & 0xFF0000ul) >> 16));
QSPI_WriteDirectCommValue((uint8_t)((u32Addr & 0xFF00u) >> 8));
QSPI_WriteDirectCommValue((uint8_t)(u32Addr & 0xFFu));
/* ④ 逐字节发数据 */
while (len--)
{
QSPI_WriteDirectCommValue(pData[u16Index]);
u16Index++;
}
QSPI_ExitDirectCommMode(); /* ⑤ 退出直通模式,Flash 开始内部编程 */
enRet = QspiFlash_WaitForWriteEnd(); /* ⑥ 等它写完 */
}
return enRet;
}
/*==============================================================
* 擦除一个 4KB 扇区
*=============================================================*/
en_result_t QspiFlash_Erase4KbSector(uint32_t u32Addr)
{
en_result_t enRet = Ok;
if (u32Addr >= FLASH_MAX_ADDR)
{
enRet = Error;
}
else
{
QspiFlash_WriteEnable(); /* 擦除也要先写使能 */
QSPI_EnterDirectCommMode();
QSPI_WriteDirectCommValue(FLASH_INSTR_ERASE_4KB_SECTOR); /* 0x20 */
QSPI_WriteDirectCommValue((uint8_t)((u32Addr & 0xFF0000ul) >> 16));
QSPI_WriteDirectCommValue((uint8_t)((u32Addr & 0xFF00u) >> 8));
QSPI_WriteDirectCommValue((uint8_t)(u32Addr & 0xFFu));
QSPI_ExitDirectCommMode();
enRet = QspiFlash_WaitForWriteEnd(); /* 擦除比写更慢,一定要等 */
}
return enRet;
}
/*==============================================================
* 全片擦除(慎用,几十秒级别)
*=============================================================*/
void QspiFlash_EraseChip(void)
{
QspiFlash_WriteEnable();
QSPI_EnterDirectCommMode();
QSPI_WriteDirectCommValue(FLASH_INSTR_ERASE_CHIP); /* 0xC7 */
QSPI_ExitDirectCommMode();
QspiFlash_WaitForWriteEnd();
}
/*==============================================================
* 读状态寄存器
*=============================================================*/
uint8_t QspiFlash_ReadStatusRegister(uint8_t u8Reg)
{
uint8_t regSta = 0u;
QSPI_EnterDirectCommMode();
QSPI_WriteDirectCommValue(u8Reg); /* 发读状态指令 */
regSta = QSPI_ReadDirectCommValue(); /* 读回一个字节 */
QSPI_ExitDirectCommMode();
return regSta;
}
/*==============================================================
* 主函数
*=============================================================*/
int32_t main(void)
{
uint32_t flashAddr = 0u; /* 当前测试的 Flash 地址 */
uint8_t *pFlashReadAddr; /* 指向映射地址的指针 */
uint16_t bufferLen = 0u;
char txBuffer[] = "QSPI read and write flash example: Welcome to use HDSC micro chip";
stc_qspi_comm_protocol_t stcQspiCommProtocol;
MEM_ZERO_STRUCT(stcQspiCommProtocol);
/* BSP 初始化 */
BSP_CLK_Init();
BSP_LED_Init();
BSP_KEY_Init(); /* 按键中断,按下后 u8GlobalKey = 1 */
QspiFlash_Init(); /* QSPI 初始化 */
bufferLen = (uint16_t)sizeof(txBuffer); /* 要写的字节数(含结束符)*/
while (1)
{
if (u8GlobalKey == 1) /* 检测到按键按下 */
{
BSP_LED_Off(LED_RED); /* 先熄灭两个指示灯 */
BSP_LED_Off(LED_GREEN);
/* ===== ① 切到标准(单线)模式,准备做写操作 ===== */
stcQspiCommProtocol.enReadMode = QspiReadModeStandard;
QSPI_CommProtocolConfig(&stcQspiCommProtocol);
/* ===== ② 擦除这个扇区(先擦后写铁律)===== */
QspiFlash_Erase4KbSector(flashAddr);
/* ===== ③ 把字符串写进去 ===== */
QspiFlash_WritePage(flashAddr, (uint8_t*)&txBuffer[0], bufferLen);
/* ===== ④ 切回四线快速读模式 ===== */
stcQspiCommProtocol.enReadMode = QspiReadModeFourWiresIO;
QSPI_CommProtocolConfig(&stcQspiCommProtocol);
/* ===== ⑤ 关键:算出映射地址,用指针直接读!===== */
pFlashReadAddr = (uint8_t *)((uint32_t)QSPI_BUS_ADDRESS + flashAddr);
/* ===== ⑥ 校验:内存里的原文 vs Flash 里读回来的 ===== */
if (memcmp(txBuffer, pFlashReadAddr, (uint32_t)bufferLen) != 0)
{
BSP_LED_On(LED_RED); /* 不一致 → 红灯:失败 */
}
else
{
BSP_LED_On(LED_GREEN); /* 一致 → 绿灯:成功 */
}
/* ===== ⑦ 地址往后挪一个扇区,下次按键测下一块 ===== */
flashAddr += FLASH_SECTOR_SIZE;
if (flashAddr >= FLASH_MAX_ADDR)
{
flashAddr = 0u; /* 到顶了从头再来 */
}
u8GlobalKey = 0; /* 清按键标志 */
}
}
}
|
4. 代码分段讲解
4.1 最精彩的一行:映射读
1
2
|
pFlashReadAddr = (uint8_t *)((uint32_t)QSPI_BUS_ADDRESS + flashAddr);
memcmp(txBuffer, pFlashReadAddr, bufferLen);
|
这两行是整个例子的灵魂,也是 QSPI 相对普通 SPI 的最大优势。
memcmp 直接把 pFlashReadAddr 当普通内存指针来读——没有任何「发指令、等数据」的代码。因为 QSPI 配在了 ROM 映射模式,硬件在后台自动完成了发送四线读指令、接收数据的全过程。
对程序员来说,8MB 的外部 Flash 就变成了从 0x98000000 开始的一个大数组。想读哪里读哪里,*p、p[i]、memcpy、strcmp 全都能直接用。
4.2 为什么写之前要切成标准模式
1
2
|
stcQspiCommProtocol.enReadMode = QspiReadModeStandard; // 写之前切单线
QSPI_CommProtocolConfig(&stcQspiCommProtocol);
|
原因:四线模式下 IO2/IO3 被当成数据线,但在单线 SPI 模式下它们是 /WP(写保护)和 /HOLD 脚。写操作的指令序列(写使能、页写)是标准单线 SPI 协议,切成标准模式最稳妥、兼容性最好。
写完之后再切回四线,享受读的高速。这个「写用单线、读用四线」的切换是 QSPI Flash 的常规操作。
4.3 三字节地址的拆分
1
2
3
|
QSPI_WriteDirectCommValue((uint8_t)((u32Addr & 0xFF0000ul) >> 16)); // 高字节
QSPI_WriteDirectCommValue((uint8_t)((u32Addr & 0xFF00u) >> 8)); // 中字节
QSPI_WriteDirectCommValue((uint8_t)(u32Addr & 0xFFu)); // 低字节
|
Flash 指令要求地址按字节、从高位到低位依次发送。3 字节地址能表示 2²⁴ = 16MB,够用 8MB 的 Flash。
容量超过 16MB 的 Flash(如 W25Q256,32MB)就需要 4 字节地址模式,代码里的 enFourByteAddrReadEn 就是控制这个的。
4.4 忙碌等待与超时保护
1
2
3
4
5
|
u32Timeout = stcClkFreq.sysclkFreq / 1000u; // 算个超时上限
do {
u8Status = QSPI_ReadDirectCommValue();
u32Timeout--;
} while ((u32Timeout != 0u) && ((u8Status & FLASH_BUSY_BIT_MASK) == FLASH_BUSY_BIT_MASK));
|
这里体现了工程化的思维:等待忙碌位清零,但不能无限等。万一 Flash 坏了或者没接好,忙碌位永远是 1,无超时保护的话程序就死在这儿了。
加了 u32Timeout 计数,等太久就返回 ErrorTimeout,让上层知道出了问题。所有等待硬件的循环都应该有超时保护,这是嵌入式代码健壮性的基本要求。
4.5 写操作的六步固定流程
看 QspiFlash_WritePage 的结构,Flash 写入永远是这六步:
1
2
3
4
5
6
|
① 写使能 (0x06)
② 进直通模式
③ 发页写指令 (0x02) + 3 字节地址
④ 逐字节发数据
⑤ 退出直通模式(此时 Flash 开始内部编程)
⑥ 轮询忙碌位,等编程完成
|
擦除也一样,只是第③步换成擦除指令、没有第④步。记住这个骨架,任何 SPI Flash 的驱动你都能自己写出来。
4.6 参数检查是好习惯
1
2
3
4
|
if ((u32Addr > FLASH_MAX_ADDR) || (NULL == pData) || (len > FLASH_PAGE_SIZE))
{
enRet = Error;
}
|
三个检查分别防:地址越界、空指针、超过一页。这就是「防御式编程」——外部 Flash 操作出错很难查(数据默默写错了,现象要很久才暴露),提前挡住非法参数能省下大量调试时间。
5. 实验现象
- 上电后两个 LED 都不亮,程序在主循环等按键。
- 按一下 K1:红绿灯先熄灭(表示开始测试),随后
- 绿灯亮 → 这个扇区读写校验通过 ✓
- 红灯亮 → 校验失败 ✗
- 再按一下 K1:测试下一个 4KB 扇区,地址自动 +0x1000。
- 一直按下去,会把整片 8MB Flash 逐扇区测完,然后地址回绕到 0 重新开始。
正常情况应该每次都是绿灯。
6. 常见问题
Q1:一直是红灯(校验失败)?
按顺序排查:
- 擦除是不是没成功? Flash 必须先擦后写。可以先用
QspiFlash_ReadStatusRegister(0x05) 读状态确认操作正常。
- 写入长度超过 256 字节了吗? 例子里字符串是 66 字节,没问题。但你换成更长的数据就会超页,
WritePage 会直接返回 Error,数据根本没写进去。
- QSPI 时钟太快? 试着把
enClkDiv 从 QspiHclkDiv3 改大(如 QspiHclkDiv4),降速能排除信号质量问题。
- 空周期(dummy cycle)配错? 四线快速读需要一定的空周期让 Flash 准备数据。
enVirtualPeriod 配少了会读到垃圾。不同型号 Flash 要求的空周期数不同,查芯片手册。
Q2:读回来全是 0xFF?
0xFF 是 Flash 擦除后的初始值。说明擦除成功了但写入失败了。重点查写使能有没有发、页写指令和地址对不对。
Q3:读回来全是 0x00 或随机值?
- 说明读的链路有问题。检查 QSPI 六根引脚复用配全了没(尤其 IO2/IO3,漏配就是四线模式失败的典型原因)。
- 检查映射基地址
0x98000000 是否写对。
Q4:程序卡在 WaitForWriteEnd 里?
- Flash 没响应,忙碌位读不到。检查片选(QSNSS/PB1)和时钟(QSPCK/PB14)的连接。
- 好消息是代码有超时保护,最终会返回
ErrorTimeout 而不是真死机。
Q5:为什么擦除比写慢那么多?
- Flash 物理特性决定的。擦除一个 4KB 扇区典型 50~300ms,页写只要 1~3ms。所以能避免擦除就避免——这也是「日志式写入」(每次往新地址追加,不覆盖旧数据)在 Flash 上很流行的原因。
Q6:能不能不擦除直接改一个字节?
- 不能。Flash 写入只能把 1 变 0,不能把 0 变 1。想改必须整个 4KB 扇区先擦除。标准做法:读出 4KB → 内存里改 → 擦除 → 写回。
7. 小结
- QSPI 的杀手级能力是 ROM 映射:Flash 变成
0x98000000 开始的大数组,用指针直接读。
- 写用标准(单线)模式,读用四线模式,中间用
QSPI_CommProtocolConfig 切换。
- Flash 写入六步骨架:写使能 → 进直通 → 发指令+地址 → 发数据 → 退直通 → 等完成。
- 铁律:先擦后写、擦 4KB 为单位、写 ≤ 256 字节/页。
- 所有等硬件的循环都要有超时保护。
- 校验用
memcmp,这是验证存储器最直接的方法。
8. 拓展应用
有了 8MB 外部 Flash,能做的事一下子多了很多:
- 中文字库存储:完整的 GB2312 汉字点阵字库约 260KB,内部 Flash 装不下但外部 Flash 轻松容纳。配合 LCD 就能显示任意汉字(见 SPI LCD 章节)。
- 图片 / 图标资源:GUI 界面的位图、开机 Logo、动画帧全放外部 Flash,靠映射地址直接读,
LCD_DrawBitmap((uint16_t*)0x98010000, ...) 一行显示。
- 数据记录仪 / 黑匣子:传感器数据按时间顺序追加写入 Flash,断电不丢。8MB 能存几十万条记录。工业设备的运行日志都这么做。
- 音频播放:把 WAV / ADPCM 音频存进 Flash,配合 DAC 或 I2S 播放语音提示、报警音。
- OTA 固件升级:新固件先通过串口/WiFi 下载到外部 Flash 暂存,校验通过后再搬到内部 Flash 覆盖旧程序。这是所有联网设备升级的标准方案,外部 Flash 是必备的「中转站」。
- 参数掉电保存:设备的配置参数(校准值、用户设置)存 Flash,比 EEPROM 容量大得多。注意做磨损均衡(轮流写不同扇区),因为 Flash 擦写寿命约 10 万次。
- 文件系统:移植 LittleFS 或 FatFS 到外部 Flash 上,就能像操作电脑硬盘一样
fopen/fwrite 管理文件,代码组织清爽很多。
- XIP(就地执行):高级玩法——把部分代码放外部 Flash 直接运行,突破内部 Flash 容量限制。