PUYA FLASH — 页擦除与写入(LL 库)

PUYA PY32F0xx_Firmware V1.5.0 内部 Flash 页擦除与写入(FLASH_PageEraseAndWrite)示例教学(LL 库版本)

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 的擦除动作由一连串寄存器操作组成:

  1. BUSY=0
  2. 使能 EOP 标志;
  3. LL_FLASH_EnablePageErase() 进入页擦除模式;
  4. LL_FLASH_SetEraseAddress() 设地址;
  5. 再等 BUSY=0EOP=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() 重新加锁,防止误操作改写代码区。

实验现象

  1. 下载程序,复位后按住用户按键触发擦写流程(或先不按,等决定时机)。
  2. 流程执行完且校验通过 → 板载绿 LED 常亮。
  3. 掉电再上电,0x08004000 处的数据依然保留(可用调试器 memory 窗口查看)。
  4. 若校验失败 → 程序停在 APP_ErrorHandler() 死循环(LED 不亮)。

常见问题

  1. 写入后读出来是 0x00000000 或乱码?

    • 几乎都是漏了 LL_FLASH_TIMMING_SEQUENCE_CONFIG_24M(),或系统时钟实际频率与设置不符。
    • 确认解锁 LL_FLASH_Unlock() 在写操作之前。
  2. 擦写把程序搞挂了?

    • 确认 FLASH_USER_START_ADDR(0x08004000) 不在你程序的占用区间内。用超过芯片容量的地址也会失败。
  3. 想存用户配置(如校准值)?

    • DATA[] 换成你的结构体,注意按 4 字节对齐;读取时直接从该地址 HW32_REG() 读回。

小结

  • LL 写 Flash 范式:解锁 → 配时序 → 循环(等BUSY→使能页操作→设地址→等EOP→清标志)擦除/编程 → 加锁 → 校验
  • 相比 HAL,LL 把“页操作模式开关”“EOP 标志轮询”等底层时序完全摊开,理解更深。
  • “先配时序、再解锁、按页操作、最后加锁”是必须遵守的铁律。

拓展应用

  • 把示例改成“只在数据变化时才擦写”,延长 Flash 寿命(Flash 擦写次数有限)。
  • 配合“双备份+版本号”做掉电安全的参数存储。
  • 用扇区擦除(FLASH_SectorEraseAndWrite 示例)适配不同颗粒。
  • 结合 IAP(在应用中编程)做固件远程升级。
世界是你们
使用 Hugo 构建
主题 StackJimmy 设计