AT32 FLASH — 内部 Flash 读写(掉电不丢失的用户数据存储)

AT32F403A 内部 Flash 读写操作,把程序存储器当 EEPROM 用

FLASH — 内部 Flash 读写

难度: ⭐⭐(理解 Flash 特性即可)
固件库: AT32F403A_407_Firmware_Library V2.2.2
源文件: project/at_start_f403a/examples/flash/flash_write_read/src/main.c


这个实验是干嘛的?

MCU 的内部 Flash 主要用来存储程序代码。但它有一个"隐藏技能":你可以在程序运行时,把 Flash 的空闲区域当作 EEPROM 使用,存储掉电不丢失的用户数据

有什么用?

  • 存储校准参数(ADC 偏移、温度补偿系数)
  • 存储用户设置(亮度、音量、WiFi 密码)
  • 存储运行计数(开机次数、累计运行时间)
  • 存储故障日志(最后一条错误信息)

省掉了外部 EEPROM 芯片,省成本、省引脚、省 PCB 空间。

本实验在 Flash 地址 0x08002800(偏移 10KB)处写入 3000 个 16 位数据,然后读回比对。


硬件准备

不需要任何外部硬件! 操作的是 MCU 内部 Flash。


Flash 的特性(必须了解!)

Flash vs RAM vs EEPROM

特性 Flash RAM EEPROM
掉电保存
读取速度 最快
写入速度 慢(ms 级) 最快 慢(ms 级)
擦写次数 1 万~10 万次 无限 100 万次
最小擦除单位 扇区(2KB/4KB) 字节
写入限制 必须先擦后写

Flash 的铁律

  1. 只能把 1 写成 0,不能把 0 写成 1
  2. 必须先擦除(把所有位变成 1 = 0xFF),才能写入
  3. 擦除按扇区,最小 2KB(AT32F403A)
  4. 写入按半字(16 位)

地址规划

AT32F403A 的 Flash 从 0x08000000 开始:

1
2
3
4
5
6
7
0x08000000 ┌─────────────────┐
           │   程序代码区     │  ← 千万别覆盖!
           │   (前 10KB)      │
0x08002800 ├─────────────────┤  ← 本实验从这里开始写
           │   用户数据区     │
           │   (空闲 Flash)   │
0x08020000 └─────────────────┘  ← Flash 结束

警告:写入地址不能和程序代码重叠,否则会把程序擦掉,MCU 变砖!


完整代码

 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
#include "at32f403a_407_board.h"
#include "at32f403a_407_clock.h"
#include "flash.h"

#define TEST_BUFFER_SIZE       3000
#define TEST_FLASH_ADDRESS     (0x08000000 + 1024 * 10)  // 偏移 10KB

uint16_t buffer_write[TEST_BUFFER_SIZE];
uint16_t buffer_read[TEST_BUFFER_SIZE];

/* 缓冲区比对 */
error_status buffer_compare(uint16_t* buf1, uint16_t* buf2, uint16_t len)
{
  while(len--)
  {
    if(*buf1 != *buf2) return ERROR;
    buf1++;
    buf2++;
  }
  return SUCCESS;
}

int main(void)
{
  uint32_t index = 0;
  error_status err_status;

  system_clock_config();
  at32_board_init();

  /* 第一步:填充测试数据 */
  for(index = 0; index < TEST_BUFFER_SIZE; index++)
  {
    buffer_write[index] = index;  // 0, 1, 2, 3, ...
  }

  /* 第二步:写入 Flash */
  err_status = flash_write(TEST_FLASH_ADDRESS, buffer_write, TEST_BUFFER_SIZE);

  /* 第三步:从 Flash 读回 */
  flash_read(TEST_FLASH_ADDRESS, buffer_read, TEST_BUFFER_SIZE);

  /* 第四步:比对验证 */
  if(buffer_compare(buffer_write, buffer_read, TEST_BUFFER_SIZE) == SUCCESS &&
     err_status == SUCCESS)
  {
    at32_led_on(LED2);
    at32_led_on(LED3);
    at32_led_on(LED4);  // 3 个 LED 全亮 = 成功
  }

  while(1) {}
}

代码分段讲解

1. 写入地址选择

1
#define TEST_FLASH_ADDRESS  (0x08000000 + 1024 * 10)  // 偏移 10KB
  • 0x08000000 是 Flash 的起始地址
  • 偏移 10KB 是为了避开程序代码区
  • 你必须确保这个地址不和程序代码重叠!

如何确认?看 Keil 编译后的 .map 文件,找到程序占用的最大地址,写入地址必须大于它。

2. flash_write 内部做了什么

flash_write() 函数内部的流程:

1
2
3
4
5
1. 解锁 Flash(Flash 默认是写保护的)
2. 擦除目标扇区(把所有位变成 0xFF)
3. 逐个半字写入数据
4. 锁定 Flash(恢复写保护)
5. 比对验证

3. flash_read 做了什么

1
flash_read(TEST_FLASH_ADDRESS, buffer_read, TEST_BUFFER_SIZE);

Flash 读取非常简单——直接从内存地址读就行。Flash 就是内存映射的,读取和读 RAM 一样。

4. 为什么用 uint16_t?

AT32F403A 的 Flash 最小写入单位是半字(16 位)。所以数据类型用 uint16_t,不是 uint8_t


实验现象

  • 3 个 LED 全部亮起 → Flash 读写成功
  • LED 不亮 → 写入失败或数据不一致

实际应用:存储校准参数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 定义数据结构
typedef struct {
  uint16_t adc_offset;     // ADC 偏移校准值
  uint16_t temp_coeff;     // 温度补偿系数
  uint32_t boot_count;     // 开机次数
  uint32_t crc;            // 数据校验
} CalData;

// 保存到 Flash
void save_calibration(CalData* data)
{
  flash_write(FLASH_CAL_ADDR, (uint16_t*)data, sizeof(CalData) / 2);
}

// 从 Flash 读取
void load_calibration(CalData* data)
{
  flash_read(FLASH_CAL_ADDR, (uint16_t*)data, sizeof(CalData) / 2);
  // 验证 CRC,如果数据损坏则使用默认值
  if(calculate_crc(data) != data->crc)
  {
    set_default_calibration(data);
  }
}

常见问题

Q: 写入后程序跑飞了?

  • 写入地址和程序代码重叠了!
  • 检查 .map 文件,确保写入地址在程序占用区域之外
  • 建议用 Flash 的最后几个扇区存储数据

Q: 写入次数有限制吗?

  • Flash 擦写次数约 1 万~10 万次
  • 如果需要频繁写入,用 RAM 缓存 + 定期保存策略
  • 或者用外部 EEPROM(100 万次)

Q: 写入速度很慢?

  • Flash 写入是 ms 级的(每半字约 10~50us)
  • 3000 个半字 ≈ 150ms
  • 写入期间 CPU 会被阻塞(Flash 总线占用)

Q: 想存储字符串/结构体?

  • 强制转换为 uint16_t* 指针
  • 长度要对齐到半字(2 字节)
  • 建议加 CRC 校验,防止意外断电导致数据损坏

小结

概念 说明
内部 Flash MCU 自带的程序存储器,也能存用户数据
先擦后写 Flash 的铁律,只能 1→0
扇区擦除 最小擦除单位 2KB
半字写入 最小写入单位 16 位
地址规划 不能覆盖程序代码!
CRC 校验 防止数据损坏

学了 Flash 读写能做什么?

内部 Flash 是最省钱的"掉电存储方案":

数据存储

项目 原理 难度
校准参数 出厂校准一次,永久保存
用户设置 亮度、音量、WiFi 密码等
运行日志 开机次数、累计时间、故障记录 ⭐⭐
数据记录 温度/湿度历史数据(Flash 空间够的话) ⭐⭐

固件升级(IAP)

项目 原理 难度
IAP 自升级 从串口/USB 接收新固件,写入 Flash,跳转执行 ⭐⭐⭐⭐
A/B 双分区 两个程序分区,一个运行一个升级,防变砖 ⭐⭐⭐⭐⭐
Bootloader 最小程序,负责加载和跳转应用 ⭐⭐⭐

与外部 Flash 的对比

方案 优点 缺点
内部 Flash 免费、无外部接线、速度快 容量小、擦写次数少、不能边写边读
外部 SPI Flash 容量大(几 MB~几百 MB)、擦写次数多 需要 SPI 接线、占引脚
外部 EEPROM 逐字节读写、擦写次数多(100 万次) 容量小(几 KB)、需要 I2C 接线

核心思路:内部 Flash 是最简单的掉电存储方案——不需要额外硬件,不需要接线,直接写地址就行。适合存储少量关键数据(校准参数、用户设置)。如果数据量大或写入频繁,再考虑外部 Flash 或 EEPROM。


🎉 恭喜! 你已经完成了 AT32 入门系列的 8 个实验:GPIO → USART → ADC → SPI → I2C → TIM → DMA → FLASH。这些是嵌入式开发最核心的基础,掌握了它们,你就能独立开发各种嵌入式项目了!

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