FLASH — 页擦除与写入 Flash_PageEraseAndWrite
固件库: PY32F403_Firmware V1.4.5
芯片: PY32F403(Cortex-M4)
源文件: PY32F403-STK/Example/FLASH/Flash_PageEraseAndWrite/Src/main.c
核心外设: 内部 Flash(0x08008000 起,按页 256B 操作)
1. 功能简介(大白话)
本例演示怎么把数据写进 MCU 自己的内部 Flash,并且断电不丢。
流程很像一个「先擦黑板再写字」的过程:
- 解锁 Flash;
- 整页擦除(把
0x08008000 开始的两页擦成全 0xFF);
- 检查擦完是不是真的全空(空白校验);
- 把一段测试数据
DATA[128](512 字节)按页写进去;
- 上锁;
- 再把 Flash 里的数据和原始
DATA 比对(写入校验);
- 全对则点亮 LED,任一步错就卡在错误处理(LED 不亮)。
注意:Flash 写之前必须先擦除,且写的最小单位是 32 位(4 字节)对齐。这正是 Flash 和 RAM 最大的不同。
2. 硬件准备
| 物品 |
说明 |
| PY32F403-STK 开发板 |
自带 LED(PA1)、用户按键 |
| USB 线 |
供电 + 下载 |
操作:烧录后按一下用户按键触发擦写流程,看 LED 是否常亮。
本例不连串口,结果完全靠 LED 表示:亮 = 成功,不亮 = 中间某步失败(卡在 APP_ErrorHandler)。
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
|
#include "main.h"
/* 用户 Flash 起始地址:0x08008000(程序代码之后的空闲区)*/
#define FLASH_USER_START_ADDR 0x08008000
/* 要写入的测试数据:128 个字 = 512 字节 */
const uint32_t DATA[128] =
{
0x01010101, 0x23456789, 0x3456789A, 0x456789AB, /* ... 共 128 个 32 位值 ... */
/* 中间省略,皆为固定测试图案 */
0x23456789, 0xAAAAAAAA, 0x55555555, 0xAAAAAAAA,
};
/* 函数声明 */
static void APP_SystemClockConfig(void);
static void APP_FlashErase(void);
static void APP_FlashProgram(void);
static void APP_FlashBlank(void);
static void APP_FlashVerify(void);
int main(void)
{
HAL_Init();
APP_SystemClockConfig();
BSP_PB_Init(BUTTON_KEY, BUTTON_MODE_GPIO); /* 用户按键 */
BSP_LED_Init(LED_GREEN);
/* 等用户按键按下(按键默认高,按下变低)*/
while (BSP_PB_GetState(BUTTON_USER)) { }
/* 1) 解锁 Flash(不解锁不能擦写)*/
HAL_FLASH_Unlock();
/* 2) 按页擦除 */
APP_FlashErase();
/* 3) 擦除后空白校验:每 4 字节应为 0xFFFFFFFF */
APP_FlashBlank();
/* 4) 按页写入数据 */
APP_FlashProgram();
/* 5) 上锁 */
HAL_FLASH_Lock();
/* 6) 写入校验 */
APP_FlashVerify();
/* 7) 成功亮灯 */
BSP_LED_On(LED_GREEN);
while (1) { }
}
/**
* @brief 按页擦除:从起始地址擦除「覆盖 DATA 所需的页数」
*/
static void APP_FlashErase(void)
{
uint32_t PAGEError = 0;
FLASH_EraseInitTypeDef EraseInitStruct = {0};
EraseInitStruct.TypeErase = FLASH_TYPEERASE_PAGEERASE; /* 页擦除 */
EraseInitStruct.PageAddress = FLASH_USER_START_ADDR; /* 起始地址 */
EraseInitStruct.NbPages = sizeof(DATA) / FLASH_PAGE_SIZE;/* 页数 = 512/256 = 2 */
if (HAL_FLASHEx_Erase(&EraseInitStruct, &PAGEError) != HAL_OK)
APP_ErrorHandler(); /* 擦除失败则卡死(LED 不亮)*/
}
/**
* @brief 按页编程:把 DATA 分两页写进去
*/
static void APP_FlashProgram(void)
{
uint32_t flash_program_start = FLASH_USER_START_ADDR;
uint32_t flash_program_end = FLASH_USER_START_ADDR + sizeof(DATA); /* 结束地址 */
uint32_t *src = (uint32_t *)DATA;
while (flash_program_start < flash_program_end)
{
/* 每次写一整页(页编程),成功后地址和源指针都跳到下一页 */
if (HAL_FLASH_Program(FLASH_TYPEPROGRAM_PAGE, flash_program_start, src) == HAL_OK)
{
flash_program_start += FLASH_PAGE_SIZE; /* 地址 += 256 */
src += FLASH_PAGE_SIZE / 4; /* 源指针 += 64 个字 */
}
}
}
/**
* @brief 空白校验:擦除后每 4 字节应为 0xFFFFFFFF
*/
static void APP_FlashBlank(void)
{
uint32_t addr = 0;
while (addr < sizeof(DATA))
{
if (0xFFFFFFFF != HW32_REG(FLASH_USER_START_ADDR + addr))
APP_ErrorHandler(); /* 发现非空白 → 报错 */
addr += 4;
}
}
/**
* @brief 写入校验:Flash 内容应与 DATA 完全一致
*/
static void APP_FlashVerify(void)
{
uint32_t addr = 0;
while (addr < sizeof(DATA))
{
if (DATA[addr / 4] != HW32_REG(FLASH_USER_START_ADDR + addr))
APP_ErrorHandler(); /* 不一致 → 报错 */
addr += 4;
}
}
/* 系统时钟:HSI 8MHz(APB2 二分频)。Flash 操作期间需正确设置等待周期 FLASH_LATENCY */
static void APP_SystemClockConfig(void)
{
RCC_OscInitTypeDef OscInitstruct = {0};
RCC_ClkInitTypeDef ClkInitstruct = {0};
OscInitstruct.OscillatorType = RCC_OSCILLATORTYPE_HSI;
OscInitstruct.HSIState = RCC_HSI_ON;
OscInitstruct.HSI48MState = RCC_HSI48M_OFF;
OscInitstruct.LSEState = RCC_LSE_OFF;
OscInitstruct.LSIState = RCC_LSI_OFF;
OscInitstruct.PLL.PLLState = RCC_PLL_OFF;
if (HAL_RCC_OscConfig(&OscInitstruct) != HAL_OK) APP_ErrorHandler();
ClkInitstruct.ClockType = RCC_CLOCKTYPE_SYSCLK | RCC_CLOCKTYPE_HCLK |
RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2;
ClkInitstruct.SYSCLKSource = RCC_SYSCLKSOURCE_HSI;
ClkInitstruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
ClkInitstruct.APB1CLKDivider = RCC_HCLK_DIV1;
ClkInitstruct.APB2CLKDivider = RCC_HCLK_DIV2;
if (HAL_RCC_ClockConfig(&ClkInitstruct, FLASH_LATENCY_0) != HAL_OK) APP_ErrorHandler();
}
void APP_ErrorHandler(void) { while (1) {} }
|
4. 代码分段讲解
4.1 为什么要先擦再写?
Flash 存储单元只能把 1 翻成 0,没法把 0 翻回 1。要改数据,必须先把整页擦成 0xFF(全 1),再写入把需要的位写成 0。所以例程顺序是 Unlock → Erase → (Blank 检查) → Program → Lock。
4.2 擦多少页?
DATA 是 128 个 uint32_t = 512 字节。每页 256 字节,所以 NbPages = 512 / 256 = 2 页。擦除从 0x08008000 开始的两页,刚好覆盖要写的范围。
4.3 写入为什么「按页 + 跳页」?
HAL_FLASH_Program(FLASH_TYPEPROGRAM_PAGE, addr, src) 一次写一页。循环里每次成功就把 addr += FLASH_PAGE_SIZE(256)、src += 64(个字)推进到下一页,直到覆盖完 512 字节(2 页)。
4.4 FLASH_LATENCY(等待周期)为何重要?
Flash 比 CPU 慢,高速时钟下读 Flash 要插入等待周期(latency),否则会读错。例程用 HSI 8MHz 低速,FLASH_LATENCY_0(0 等待)即可;若你把系统时钟提到几十 MHz,务必相应增大 FLASH_LATENCY,否则程序会跑飞。
4.5 校验的两道关
APP_FlashBlank:擦除后检查是否真的全 0xFF(防止没擦干净就写);
APP_FlashVerify:写完后逐字比对 Flash 内容与 DATA,确保写入无误。任何一步不符都进 APP_ErrorHandler(LED 不亮)。
5. 实验现象
- 烧录后按用户键:
- LED 常亮 = 擦除、写入、校验全部成功;
- LED 不亮 = 任一步失败(卡在错误处理),常见于地址选错或等待周期不匹配。
6. 常见问题
| 现象 |
原因 |
解决 |
| LED 不亮(卡死) |
写到了程序占用的页 |
把起始地址改到链接脚本里未使用的区域(如 Flash 末尾) |
| 写入后读出来不对 |
FLASH_LATENCY 不匹配 |
高速时钟下增大 FLASH_LATENCY_x |
| 只能写一次就失败 |
没先擦除就写 |
保证每次写前 Erase,或先读-改-擦-写 |
| 想存少量参数 |
没必要整页擦 |
累积多个参数到一页,或加「读写缓存 + 脏标记」策略 |
| 反复擦写寿命 |
Flash 有擦写次数上限 |
避免频繁写同一页,必要时用磨损均衡 |
| 程序跑飞 |
误擦了中断向量/代码 |
绝对避开 0x08000000 起的前若干页 |
7. 小结
- 内部 Flash 可当 EEPROM:掉电保存用户数据;
- 黄金顺序:解锁 → 擦除(页/扇区/块)→ 写入 → 校验 → 上锁;
- F403 粒度:页 256B / 扇区 2KB / 块 32KB,写最小 32 位对齐;
- 地址务必选程序未占用区,且按系统频率配好
FLASH_LATENCY;
- 本例擦 2 页、写 512 字节、双校验,LED 亮即成功。
8. 拓展应用(学了能做什么项目)
- 参数存储:把校准值、配置写 Flash,断电不丢,省 EEPROM;
- 运行统计:累计开机次数、运行小时,做设备健康度;
- 简易 OTA:A/B 双区,新固件写空闲块后切换启动;
- 日志缓存:把关键事件环形写入 Flash,事后回溯;
- 扇区/块级管理:参考
Flash_SectorEraseAndWrite、Flash_BlockEraseAndWrite,按更大粒度管理数据;
- 配合看门狗:写关键参数时关中断/看门狗,防写入被打断。
返回:Flash 存储总览