FLASH — 页擦除与写入(LL 库)
HAL vs LL:PUYA 固件库提供 HAL 和 LL 两套 API。
- HAL 库:封装度高,可移植性强,适合快速开发
- LL 库:接近寄存器操作,代码精简效率高,适合性能敏感场景
本文使用 LL 库版本。
固件库: PY32F0xx_Firmware V1.5.0
芯片: PY32F030(PY32F002A / F003 同库可参考)
源文件: PUYA/PY32F0xx_Firmware_V1.5.0/Projects/PY32F030-STK/Example_LL/FLASH/FLASH_PageEraseAndWrite/Src/main.c
功能简介
本示例演示如何安全地改写片内 Flash:向 0x08004000 起始的用户区写入一段 64 字的测试数据,掉电后仍可保留。完整流程是:解锁 Flash → 配置编程时序 → 页擦除 → 校验空白 → 按页编程 → 加锁 → 回读校验。按键用于避免每次上电都擦写。
与 HAL 版(HAL_FLASH_Unlock / HAL_FLASH_Program / HAL_FLASH_Lock + FLASH_EraseInitTypeDef)相比,LL 版把“每次擦除/编程都要等 BUSY、置/清 EOP、使能/关闭页操作”这些细节完全摊开——更接近 Flash 控制器的真实寄存器时序。
硬件准备
- PY32F030 开发板
- 板载用户按键(BUTTON_USER)、绿 LED
- 注意:
0x08004000 是用户 Flash 区,务必确认没有被你的程序占用(否则会破坏自身代码)
完整代码
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
|
#include "main.h"
#include "py32f030xx_ll_Start_Kit.h"
#define FLASH_USER_START_ADDR 0x08004000
const uint32_t DATA[64] = {
0x01010101, 0x23456789, 0x3456789A, 0x456789AB, /* ... 共 64 字 ... */
0x23456789, 0xAAAAAAAA, 0x55555555, 0x23456789
};
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)
{
APP_SystemClockConfig();
LL_APB1_GRP2_EnableClock(LL_APB1_GRP2_PERIPH_SYSCFG);
LL_APB1_GRP1_EnableClock(LL_APB1_GRP1_PERIPH_PWR);
BSP_LED_Init(LED_GREEN);
BSP_PB_Init(BUTTON_USER, BUTTON_MODE_GPIO);
while (BSP_PB_GetState(BUTTON_USER)) {} /* 等按键,避免每次上电都擦写 */
LL_FLASH_Unlock(FLASH); /* 解锁 Flash */
LL_FLASH_TIMMING_SEQUENCE_CONFIG_24M(); /* 按 24MHz 配置编程时序 */
APP_FlashErase(); /* 页擦除 */
APP_FlashBlank(); /* 校验空白 */
APP_FlashProgram(); /* 写数据 */
LL_FLASH_Lock(FLASH); /* 加锁 */
APP_FlashVerify(); /* 回读校验 */
BSP_LED_On(LED_GREEN);
while (1) {}
}
/**
* @brief 页擦除:逐页擦到覆盖完数据区
*/
static void APP_FlashErase(void)
{
uint32_t start = FLASH_USER_START_ADDR;
uint32_t end = FLASH_USER_START_ADDR + sizeof(DATA);
while (start < end)
{
while (LL_FLASH_IsActiveFlag_BUSY(FLASH) == 1); /* 等不忙 */
LL_FLASH_EnableIT_EOP(FLASH); /* 使能结束中断标志 */
LL_FLASH_EnablePageErase(FLASH); /* 进入页擦除模式 */
LL_FLASH_SetEraseAddress(FLASH, start); /* 设置擦除地址 */
while (LL_FLASH_IsActiveFlag_BUSY(FLASH) == 1); /* 等擦完 */
while (LL_FLASH_IsActiveFlag_EOP(FLASH) == 0); /* 等 EOP */
LL_FLASH_ClearFlag_EOP(FLASH);
LL_FLASH_DisableIT_EOP(FLASH);
LL_FLASH_DisablePageErase(FLASH);
start += FLASH_PAGE_SIZE; /* 指向下一页 */
}
}
/**
* @brief 按页编程
*/
static void APP_FlashProgram(void)
{
uint32_t start = FLASH_USER_START_ADDR;
uint32_t end = FLASH_USER_START_ADDR + sizeof(DATA);
uint32_t *src = (uint32_t *)DATA;
while (start < end)
{
while (LL_FLASH_IsActiveFlag_BUSY(FLASH) == 1);
LL_FLASH_EnableIT_EOP(FLASH);
LL_FLASH_EnablePageProgram(FLASH); /* 进入页编程模式 */
LL_FLASH_PageProgram(FLASH, start, src); /* 写一页 */
while (LL_FLASH_IsActiveFlag_BUSY(FLASH) == 1);
while (LL_FLASH_IsActiveFlag_EOP(FLASH) == 0);
LL_FLASH_ClearFlag_EOP(FLASH);
LL_FLASH_DisableIT_EOP(FLASH);
LL_FLASH_DisablePageProgram(FLASH);
start += FLASH_PAGE_SIZE;
src += FLASH_PAGE_SIZE / 4;
}
}
/**
* @brief 空白校验:擦除后应为全 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 回读校验:与源数据一致
*/
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;
}
}
/* APP_SystemClockConfig 配置 HSI=24MHz;APP_ErrorHandler 略 */
|
代码分段讲解
1. 解锁与时序
Flash 上电默认加锁,写操作前必须 LL_FLASH_Unlock()。此外有个 LL 特有、极易遗漏的步骤:LL_FLASH_TIMMING_SEQUENCE_CONFIG_24M()——它按当前 HSI 频率(24MHz)配置 Flash 编程等待时序。频率配错会导致编程失败或读出错误。
2. 页擦除时序(关键)
PY32 的 Flash 只能按页擦除(不能字节擦除),擦前须全 1。LL 的擦除动作由一连串寄存器操作组成:
- 等
BUSY=0;
- 使能 EOP 标志;
LL_FLASH_EnablePageErase() 进入页擦除模式;
LL_FLASH_SetEraseAddress() 设地址;
- 再等
BUSY=0 与 EOP=1,清 EOP、退出擦除模式。
注意每擦一页都要重复这套时序,且地址按 FLASH_PAGE_SIZE 递增。
3. 页编程时序
写入同理:进入页编程模式 → LL_FLASH_PageProgram() 写一页 → 等 BUSY/EOP → 清标志、退出。HAL 把这套时序封在 HAL_FLASH_Program() 里,LL 则逐句暴露,你能清楚看到“模式开关”和“EOP 轮询”的存在。
4. 校验与加锁
- 擦除后
APP_FlashBlank() 确认全是 0xFFFFFFFF(否则说明没擦干净,进错误)。
- 编程后
APP_FlashVerify() 回读与源数据比对。
- 最后
LL_FLASH_Lock() 重新加锁,防止误操作改写代码区。
实验现象
- 下载程序,复位后按住用户按键触发擦写流程(或先不按,等决定时机)。
- 流程执行完且校验通过 → 板载绿 LED 常亮。
- 掉电再上电,
0x08004000 处的数据依然保留(可用调试器 memory 窗口查看)。
- 若校验失败 → 程序停在
APP_ErrorHandler() 死循环(LED 不亮)。
常见问题
-
写入后读出来是 0x00000000 或乱码?
- 几乎都是漏了
LL_FLASH_TIMMING_SEQUENCE_CONFIG_24M(),或系统时钟实际频率与设置不符。
- 确认解锁
LL_FLASH_Unlock() 在写操作之前。
-
擦写把程序搞挂了?
- 确认
FLASH_USER_START_ADDR(0x08004000) 不在你程序的占用区间内。用超过芯片容量的地址也会失败。
-
想存用户配置(如校准值)?
- 把
DATA[] 换成你的结构体,注意按 4 字节对齐;读取时直接从该地址 HW32_REG() 读回。
小结
- LL 写 Flash 范式:解锁 → 配时序 → 循环(等BUSY→使能页操作→设地址→等EOP→清标志)擦除/编程 → 加锁 → 校验。
- 相比 HAL,LL 把“页操作模式开关”“EOP 标志轮询”等底层时序完全摊开,理解更深。
- “先配时序、再解锁、按页操作、最后加锁”是必须遵守的铁律。
拓展应用
- 把示例改成“只在数据变化时才擦写”,延长 Flash 寿命(Flash 擦写次数有限)。
- 配合“双备份+版本号”做掉电安全的参数存储。
- 用扇区擦除(
FLASH_SectorEraseAndWrite 示例)适配不同颗粒。
- 结合 IAP(在应用中编程)做固件远程升级。