GD32 FMC — Flash 擦写

GD32 GD32F30x_Firmware_Library V3.0.3 FMC Flash 擦写 示例教学

FMC — Flash 擦写

固件库: GD32F30x_Firmware_Library V3.0.3
芯片: GD32F30x(以 GD32F307 评估板为例)
源文件: GD32F30x_Firmware_Library_V3.0.3/Examples/FMC/Erase_Program/main.c
配套: main.h(定义 led_typedef_enum、全局 lednum 等)


功能简介

本示例演示 FMC(Flash 控制器) 最基础也最重要的两项操作:擦除(Erase)编程(Program / 写) 片内 Flash,并对结果做校验。

整个流程分两步:

  1. 擦除:把 0x08004000 ~ 0x08004800 这一页 Flash 擦成全 0xFF(擦除后该页所有字节变为 0xFFFFFFFF)。
  2. 编程:把固定数据 0x01234567 逐字(每 4 字节)写入刚才擦除的那一页。
  3. 校验
    • 擦除后检查是否全为 0xFFFFFFFF;若不是 → 点亮 LED2 表示擦除失败。
    • 写入后检查是否全为 0x01234567;若不是 → 点亮 LED3 表示写入失败。
    • 全成功 → 点亮 LED4

这是一个"纯片内 Flash 自操作"的例子,无需任何外部器件,下载即可观察 LED,是理解 Flash 存储机制的绝佳起点。


硬件准备

  • GD32F30x 评估板一块(例程用 LED2 / LED3 / LED4,gd_eval_led_init 初始化)。
  • 无需外部接线。完全片内操作。
  • 若用调试器,可在擦除/编程后观察 0x08004000 处内存:擦除后应为连续的 0xFFFFFFFF,编程后应为连续的 0x01234567

⚠️ 地址安全:本例写的 0x08004000 是评估板默认空闲的 Flash 页。若你移植到自己的工程,请确认该地址不在你的程序代码区(查 .map 文件找空闲区),否则擦写会破坏正在运行的程序。


完整代码(带中文注释)

  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
#include "gd32f30x.h"
#include "gd32f307c_eval.h"
#include "main.h"

#define FMC_PAGE_SIZE           ((uint16_t)0x800U)   /* Flash 每页 2KB */
#define FMC_WRITE_START_ADDR    ((uint32_t)0x08004000U) /* 操作起始地址 */
#define FMC_WRITE_END_ADDR      ((uint32_t)0x08004800U) /* 操作结束地址(含) */

uint32_t *ptrd;
uint32_t address = 0x00000000U;
uint32_t data0   = 0x01234567U;        /* 要写入的固定数据 */
led_typedef_enum lednum = LED4;        /* 最终状态灯,默认 LED4(成功) */

/* 要计算的页数:(0x08004800 - 0x08004000) / 0x800 = 1 页 */
uint32_t PageNum = (FMC_WRITE_END_ADDR - FMC_WRITE_START_ADDR) / FMC_PAGE_SIZE;
/* 要计算的字数:(0x800) / 4 = 512 个字(即 2048 字节) */
uint32_t WordNum = ((FMC_WRITE_END_ADDR - FMC_WRITE_START_ADDR) >> 2);

/*!
    \brief      擦除 FMC 页(从起始地址到结束地址)
*/
void fmc_erase_pages(void)
{
    uint32_t EraseCounter;

    fmc_unlock();   /* ① 解锁 Flash(写前必做) */

    /* ② 清除所有待处理标志 */
    fmc_flag_clear(FMC_FLAG_BANK0_END);
    fmc_flag_clear(FMC_FLAG_BANK0_WPERR);
    fmc_flag_clear(FMC_FLAG_BANK0_PGERR);

    /* ③ 逐页擦除 */
    for(EraseCounter = 0; EraseCounter < PageNum; EraseCounter++){
        fmc_page_erase(FMC_WRITE_START_ADDR + (FMC_PAGE_SIZE * EraseCounter));
        fmc_flag_clear(FMC_FLAG_BANK0_END);
        fmc_flag_clear(FMC_FLAG_BANK0_WPERR);
        fmc_flag_clear(FMC_FLAG_BANK0_PGERR);
    }

    fmc_lock();     /* ④ 擦完上锁 */
}

/*!
    \brief      逐字编程 FMC(把 data0 写入每一字)
*/
void fmc_program(void)
{
    fmc_unlock();   /* ① 解锁 */

    /* ② 清除标志 */
    fmc_flag_clear(FMC_FLAG_BANK0_END);
    fmc_flag_clear(FMC_FLAG_BANK0_WPERR);
    fmc_flag_clear(FMC_FLAG_BANK0_PGERR);

    address = FMC_WRITE_START_ADDR;

    /* ③ 从起始地址到结束地址,每 4 字节写一个字 */
    while(address < FMC_WRITE_END_ADDR){
        fmc_word_program(address, data0);
        address += 4;    /* Flash 按字(4字节)编程 */
        fmc_flag_clear(FMC_FLAG_BANK0_END);
        fmc_flag_clear(FMC_FLAG_BANK0_WPERR);
        fmc_flag_clear(FMC_FLAG_BANK0_PGERR);
    }

    fmc_lock();     /* ④ 写上锁 */
}

/*!
    \brief      校验擦除结果(应全为 0xFFFFFFFF)
*/
void fmc_erase_pages_check(void)
{
    uint32_t i;

    ptrd = (uint32_t *)FMC_WRITE_START_ADDR;

    for(i = 0; i < WordNum; i++){
        if(0xFFFFFFFF != (*ptrd)){   /* 有任一个字不是全 1,说明没擦干净 */
            lednum = LED2;           /* 标记擦除失败 */
            gd_eval_led_on(lednum);
            break;
        } else {
            ptrd++;
        }
    }
}

/*!
    \brief      校验编程结果(应全为 data0=0x01234567)
*/
void fmc_program_check(void)
{
    uint32_t i;

    ptrd = (uint32_t *)FMC_WRITE_START_ADDR;

    for(i = 0; i < WordNum; i++){
        if((*ptrd) != data0){        /* 有任一个字不等于写入值,说明写失败 */
            lednum = LED3;           /* 标记写入失败 */
            gd_eval_led_on(lednum);
            break;
        } else {
            ptrd++;
        }
    }
}

int main(void)
{
    /* 初始化 LED2/LED3/LED4 */
    gd_eval_led_init(LED2);
    gd_eval_led_init(LED3);
    gd_eval_led_init(LED4);

    /* 步骤1:擦除并校验;失败则点亮 LED2 */
    fmc_erase_pages();
    fmc_erase_pages_check();

    /* 步骤2:编程并校验;失败则点亮 LED3 */
    fmc_program();
    fmc_program_check();

    /* 全部成功(lednum 仍为默认 LED4)→ 点亮 LED4 */
    if(LED4 == lednum){
        gd_eval_led_on(LED4);
    }

    while(1);
}

代码分段讲解

1. 全局宏与"页数/字数"计算

1
2
3
4
5
#define FMC_PAGE_SIZE        0x800     // 每页 2KB
#define FMC_WRITE_START_ADDR 0x08004000
#define FMC_WRITE_END_ADDR   0x08004800
uint32_t PageNum = (0x08004800 - 0x08004000) / 0x800;  // = 1 页
uint32_t WordNum = (0x08004800 - 0x08004000) >> 2;     // = 0x200 = 512 字
  • 操作区间是 0x08004000 ~ 0x08004800,跨度 0x800 = 2KB,正好是 1 个 Flash 页,所以擦除循环只擦 1 页。
  • WordNum = 512:这一页共 2048 字节,按 32 位(4 字节)字算就是 512 个字,写入/校验都按这 512 个字循环。

2. “解锁 → 清标志 → 操作 → 上锁"四步曲

擦除和编程函数结构完全对称,都遵循这条黄金流程:

1
2
3
4
5
6
fmc_unlock();                              // ① 解锁(Flash 默认锁着,不解锁写不进)
fmc_flag_clear(FMC_FLAG_BANK0_END);        // ② 清标志(避免上次残留状态干扰)
fmc_flag_clear(FMC_FLAG_BANK0_WPERR);      //    写保护错误标志
fmc_flag_clear(FMC_FLAG_BANK0_PGERR);      //    编程错误标志
/* ... 具体操作 ... */
fmc_lock();                                // ④ 上锁(写完保护 Flash)
  • 解锁:Flash 上电后处于锁定状态,任何写操作都会被拒绝。fmc_unlock() 内部写入特定密钥序列来解锁。每次重新解锁前若已锁,需先 lockunlock(例程每次操作开头都 unlock,结尾都 lock,规范)。
  • 清标志:Flash 操作会产生 END(完成)、WPERR(写保护错)、PGERR(编程错)等状态位。操作前清一次、每次操作后也清,确保状态干净、便于判断本次结果。

3. 擦除:整页擦成 0xFF

1
fmc_page_erase(FMC_WRITE_START_ADDR + 0x800 * EraseCounter);

Flash 不能单独改某个字节,只能"整页擦除”。擦除后该页每个位都回到 1,即读出 0xFFFFFFFF。所以擦除校验就是判断"是不是全 0xFFFFFFFF"。这也是为什么想改写 Flash 必须先擦——不擦直接写,旧数据会和新数据"与"在一起出错。

4. 编程:按字写 0x01234567

1
2
3
4
while(address < FMC_WRITE_END_ADDR){
    fmc_word_program(address, data0);  // 在 address 处写一个 32 位字
    address += 4;                      // 字 = 4 字节,地址 +4
}
  • fmc_word_program 一次写 4 字节(一个字)。所以循环里 address += 4,从 0x08004000 一直写到 0x08004800
  • 写入的值是固定 0x01234567,写满整页 512 个字。
  • 注意写入前必须确保该页已擦除(本例先擦后写,顺序正确)。

5. 两段校验:只读不写

1
2
3
4
5
ptrd = (uint32_t *)FMC_WRITE_START_ADDR;
for(i = 0; i < WordNum; i++){
    if(0xFFFFFFFF != *ptrd){ lednum = LED2; ... break; }
    ptrd++;
}

校验就是把 Flash 当普通内存(uint32_t * 指针)读出来比一比:擦除后应为全 0xFFFFFFFF,写之后应为全 0x01234567读 Flash 不需要解锁,直接指针访问即可。一旦有字不符,就通过 lednum 记下是哪步失败(LED2=擦除失败,LED3=写入失败)并点亮对应灯。

6. main:两步串起来,LED4 表成功

1
2
3
fmc_erase_pages();        fmc_erase_pages_check();   // 第一步
fmc_program();            fmc_program_check();        // 第二步
if(LED4 == lednum) gd_eval_led_on(LED4);             // 都没出错才亮 LED4

lednum 初值设为 LED4(成功态)。只有擦除和写入全程无误lednum 才保持 LED4,最后点亮 LED4;任一步出错,lednum 会被改成 LED2LED3,LED4 就不会亮。这是一种简洁的"状态灯"写法。


实验现象

  1. 下载程序并复位后,Flash 自动被擦除、写入、校验。
  2. 正常情况:只有 LED4 亮(代表擦除 + 写入 + 校验全部成功)。
  3. 异常情况:
    • 擦除失败 → LED2 亮(LED4 不亮)。
    • 写入失败 → LED3 亮(LED4 不亮)。
  4. 用调试器查看 0x08004000
    • 擦除后:FF FF FF FF FF FF FF FF ...(全 1)。
    • 写入后:67 45 23 01 67 45 23 01 ...(小端存储 0x01234567)。

常见问题

Q1:为什么一写就进 HardFault / 程序跑飞?
最常见是写到了正在运行的代码区。Flash 不能擦写当前执行代码的页。请确认 FMC_WRITE_START_ADDR 在你的程序代码范围之外(末尾空闲页)。评估板 0x08004000 默认空闲,但大工程可能代码已占满到此。

Q2:为什么擦除后某个字不是 0xFFFFFFFF?
擦除不完整通常因为:① 没有先 fmc_unlock();② 上一次操作残留的 PGERR/WPERR 没清;③ 该页被写保护了(见 Write_Protection 例程)。先确认解锁和清标志都做了。

Q3:想改某个字节的数据,必须整页擦除吗?
是的,Flash 的最小擦除单位就是"页"(2KB)。如果某页里还存有不想丢的数据,标准做法是:先把整页读出来备份到 RAM,修改目标字节,再整页擦除、把备份写回去。这也叫"Flash 模拟 EEPROM"的惯用手法。

Q4:写入函数一定要按 4 字节(字)吗?
本例用 fmc_word_program(32 位)。库也提供 fmc_halfword_program(16 位)、fmc_byte_program(8 位,视型号支持)。但底层仍受 Flash 物理对齐约束,写之前该地址必须已擦除

Q5:写之前一定要清 END/WPERR/PGERR 标志吗?
强烈建议清。不清的话,若之前有错误标志残留,本次操作可能立刻被判失败或卡住。例程在每次操作前后都清,是稳妥写法。

Q6:擦写 Flash 会不会很慢、影响中断?
Flash 擦除一页需要若干毫秒,写一字约几十微秒,期间 CPU 仍可运行(GD32 有 Flash 缓存/预取),但擦写期间若要从该 Flash 取指令可能等待。对时序极苛刻的中断,关键代码可放在 RAM 里执行。一般应用无需担心。


小结

本示例把 FMC 的黄金流程讲得干干净净:

  1. 算好范围:确定要操作的页数和字数(按页擦、按字写)。
  2. 解锁fmc_unlock() 是写 Flash 的前置条件。
  3. 清标志:操作前后清 END / WPERR / PGERR
  4. 擦页fmc_page_erase() 把整页擦成 0xFFFFFFFF(不能只改字节)。
  5. 写字fmc_word_program() 按 4 字节逐字写入。
  6. 上锁 + 校验fmc_lock() 保护;用指针读回比对,LED 指示成败。

记住 Flash = “先整页擦、再按字写、写前必解锁” 这三句话,你就掌握了片内存储的底层逻辑。


拓展应用

掌握 Flash 擦写后,可以做很多"掉电不丢"的实用功能:

  • 保存配置参数:把设备的校准值、用户设置写到指定 Flash 页,上电读取恢复;修改时按"备份→擦除→回写"流程。
  • IAP / OTA 升级:旧程序通过 FMC 把新固件写到另一块 Flash 区,再用跳转指令接管运行——这是绝大多数单片机远程升级的基础。
  • 运行计数器 / 公里数:如电机累计转数、设备累计工作时间,定期写入 Flash 持久化。
  • 黑匣子 / 日志:记录最近一次异常现场数据,掉电后仍能读取排查。
  • 数据加密存储:配合唯一 ID 与简单加解密,把敏感参数存进 Flash。
  • 结合 DMA:如 Flash_to_ram 例程,先把数据写进 Flash,再用 DMA 高速搬回 RAM 做处理。

想给 Flash 加保护防止误擦写,可阅读同模块 Write_Protection 例程。

世界是你们
使用 Hugo 构建
主题 StackJimmy 设计