Flash读写
201.2.9 Flash读写
#define INTER_FLASH_PARAM_ADDR (0x8006000U) /* inter_flashif.h:参数区首地址,补齐前导 0 后即 0x08006000。 */
#pragma pack(1) /* inter_flash_cfg.h:按 1 字节对齐结构体成员。 */
typedef struct /* 定义保存在 Flash 参数区中的配置结构。 */
{ /* 配置结构体开始。 */
uint8_t magic[4]; /* 4 字节魔数,课程使用 AA BB CC DD 判断数据是否有效。 */
uint32_t ota_bin_version; /* 4 字节 OTA 固件版本号。 */
uint8_t ota_flag; /* 1 字节升级标志。 */
uint8_t checksum; /* 1 字节累加校验值。 */
uint8_t format[2]; /* 2 字节占位字段。 */
} inter_flash_cfg_param_typeDef; /* 该结构在 1 字节对齐下共占 12 字节。 */
#pragma pack() /* 恢复编译器默认的结构体对齐方式。 */
uint8_t inter_flashif_read_page(uint32_t addr, uint8_t *buf, uint32_t len) /* 从 addr 开始读取 len 个字节。 */
{ /* Flash 读取函数开始。 */
uint32_t flash_addr = addr; /* 保存待读取的 Flash 起始地址。 */
if (len > FLASH_PAGE_SIZE) /* 课程封装规定一次最多读取一页,即 0x800 字节。 */
{ /* 长度超限分支开始。 */
return 1; /* 返回 1,表示本次读取未执行。 */
} /* 长度超限分支结束。 */
for (uint32_t i = 0; i < len; i++) /* 按字节遍历请求的读取长度。 */
{ /* 字节读取循环开始。 */
buf[i] = *(volatile uint8_t *)(flash_addr + i); /* 把地址转成易变字节指针,解引用后存入缓冲区。 */
} /* 字节读取循环结束。 */
return 0; /* 返回 0,表示循环读取已经完成。 */
} /* Flash 读取函数结束。 */
uint8_t inter_flashif_smart_write_page(uint32_t addr, uint32_t *buf, uint32_t len) /* 擦除一页后写入 len 个 32 位字。 */
{ /* 课程“擦除并写入”函数开始。 */
FLASH_EraseInitTypeDef user_flash; /* 声明 HAL 页擦除配置结构体。 */
HAL_FLASH_Unlock(); /* 解锁 Flash 控制器,允许执行擦除和编程。 */
user_flash.TypeErase = FLASH_TYPEERASE_PAGES; /* 选择按页擦除,而不是整片擦除。 */
user_flash.PageAddress = addr; /* 指定要擦除的页所在地址。 */
user_flash.NbPages = 1; /* 本次只擦除 1 页。 */
uint32_t PageError = 0; /* 接收 HAL 返回的出错页地址。 */
HAL_FLASHEx_Erase(&user_flash, &PageError); /* 执行页擦除;课程源码未检查返回状态。 */
for (uint32_t i = 0; i < len; i++) /* len 在本函数中表示 32 位字的数量,不是字节数。 */
{ /* 写入循环开始。 */
HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, addr + i * 4, buf[i]); /* 每次向递增 4 字节的地址写一个 32 位字。 */
} /* 写入循环结束。 */
HAL_FLASH_Lock(); /* 重新锁定 Flash 控制器,降低误擦写风险。 */
} /* 课程原函数到此结束,声明为 uint8_t 却没有 return。 */
uint8_t inter_flashif_erase_page(uint32_t addr) /* 单独擦除 addr 所在的一页。 */
{ /* 单页擦除函数开始。 */
FLASH_EraseInitTypeDef user_flash; /* 声明 HAL 页擦除配置结构体。 */
HAL_FLASH_Unlock(); /* 擦除前解锁 Flash 控制器。 */
user_flash.TypeErase = FLASH_TYPEERASE_PAGES; /* 选择按页擦除。 */
user_flash.PageAddress = addr; /* 指定待擦除页所在地址。 */
user_flash.NbPages = 1; /* 只擦除 1 页。 */
uint32_t PageError = 0; /* 接收可能发生错误的页地址。 */
HAL_FLASHEx_Erase(&user_flash, &PageError); /* 执行单页擦除;课程源码未检查结果。 */
HAL_FLASH_Lock(); /* 擦除后重新上锁。 */
} /* 课程原函数到此结束,声明为 uint8_t 却没有 return。 */
uint8_t inter_flashif_write_page(uint32_t addr, uint32_t *buf, uint32_t len) /* 只编程、不先擦除。 */
{ /* 单独写入函数开始。 */
HAL_FLASH_Unlock(); /* 编程前解锁 Flash 控制器。 */
for (uint32_t i = 0; i < len; i++) /* 依次写入 len 个 32 位字。 */
{ /* 写入循环开始。 */
HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, addr + i * 4, buf[i]); /* 把第 i 个字写到对应地址。 */
} /* 写入循环结束。 */
HAL_FLASH_Lock(); /* 编程完成后重新上锁。 */
} /* 课程原函数到此结束,声明为 uint8_t 却没有 return。 */
本课使用 uint3code/08Ymodem_Bootloader工程,讲解 STM32F103ZET6 内部 Flash 的读取、页擦除和编程。上方代码忠实保留了视频展示的业务源码;原注释被替换为逐行说明,源码本身存在的返回值和错误处理问题只在笔记中指出,没有擅自修改。
一、flash的读写
1.1 理论部分
1.1.1 flash读写
内部 Flash 的常见操作可分为三类:
| 操作 | 本课实现 | 是否需要解锁 | 关键粒度 |
|---|---|---|---|
| 读 | inter_flashif_read_page() | 不需要 | 本函数按字节读,并人为限制为最多 2048 字节 |
| 擦除 | inter_flashif_erase_page() | 需要 | STM32F103ZE 每次最少擦除 1 页,即 2 KiB |
| 编程(通常口语称“写”) | inter_flashif_write_page() | 需要 | 本工程每次向 HAL 传入 1 个 32 位字 |
| 擦除后编程 | inter_flashif_smart_write_page() | 需要 | 固定擦 1 页,再连续写入若干 32 位字 |
工程中与本课相关的文件是:
inter_flashif/inter_flashif.c:Flash 读取、擦除、编程接口;文件名中的if表示 interface(接口)。inter_flashif/inter_flash_cfg.c:基于上述接口读写 OTA 标志、版本号和校验值,属于业务逻辑层。inter_flashif/inter_flash_cfg.h:定义写入参数区的 12 字节结构体。Drivers/STM32F1xx_HAL_Driver/Inc/stm32f1xx_hal_flash_ex.h:HAL 扩展头文件。对本工程定义的STM32F103xE,其中FLASH_PAGE_SIZE为0x800U。
在 Keil µVision 中,可以选中 FLASH_PAGE_SIZE后右键执行 Go To Definition追到宏定义。跳转结果仍要结合当前工程的芯片宏判断:本项目定义的是 STM32F103xE,所以命中 0x800U分支;其他容量等级的 STM32F1 页大小可能不同。
原转录中的文件名“hell_flash-es.h”和数值“0x820”都是识别错误。正确关系为:
FLASH_PAGE_SIZE = 0x800U
= 2048 字节
= 2 KiB
0x820换算成十进制其实是 2080,并不是 2048。
1.1.2 存储器和总线构架
1.1.2.1 系统构架
STM32F103 的内部 Flash 既保存程序,也可以保存只读常量和用户参数。它被映射到处理器地址空间,所以读取 Flash 数据时可以像访问普通内存一样使用 C 指针,不必先调用专门的“读 Flash”外设函数。
从 Cortex-M3 总线角度看:
- CPU 通常经 I-Code 总线从 Flash 取指令;
- 程序中的常量等数据读取可经 D-Code 接口访问 Flash;
- Flash 接口负责预取、等待周期以及对编程/擦除控制器的访问。
这解释了为什么本课的读取函数只需解引用地址,而擦除和编程必须使用 Flash 控制器并先解锁。ST 的 PM0075《STM32F10xxx Flash memory microcontrollers》还说明:编程或擦除期间,如果 CPU 又访问同一片 Flash,总线会等待当前操作完成;执行擦写时 HSI 必须保持开启。本工程的系统时钟配置确实把 HSIState设为 RCC_HSI_ON。
1.1.2.2 存储器组织
本工程芯片是 STM32F103ZET6:
Z表示 144 引脚封装规格;E表示 512 KiB Flash;- 它属于 STM32F103 高容量(high-density)器件。
高容量 STM32F103 的主 Flash 由 256 页 × 每页 2 KiB组成:
256 × 2 KiB = 512 KiB
512 × 1024 = 524288 字节 = 0x80000 字节
因此 STM32F103ZE 的主 Flash 地址范围为:
起始地址:0x08000000
末地址: 0x0807FFFF
末端后一地址:0x08080000
页号与地址可用下式计算:
第 n 页起始地址 = 0x08000000 + n × 0x800
第 n 页末地址 = 第 n 页起始地址 + 0x7FF
例如第 0 页是 0x08000000~0x080007FF,第 12 页是 0x08006000~0x080067FF,第 255 页是 0x0807F800~0x0807FFFF。这些页数和边界可在 PM0075 的高容量器件 Flash 组织表中核对。
原笔记的“每个块大小为 256 个 2KB 的页”容易误解。准确说法是:整块 512 KiB 主 Flash 被划分成 256 个页,每页 2 KiB。
1.1.2.3 存储器映像
结合课程 IAP 分区图,本工程把 512 KiB Flash 划为:
| 区域 | 起始地址 | 末地址 | 容量 | 页号 |
|---|---|---|---|---|
| Bootloader | 0x08000000 | 0x08005FFF | 24 KiB | 0~11,共 12 页 |
| Param | 0x08006000 | 0x08007FFF | 8 KiB | 12~15,共 4 页 |
| Application | 0x08008000 | 0x0807FFFF | 480 KiB | 16~255,共 240 页 |
容量复算为:
24 KiB + 8 KiB + 480 KiB = 512 KiB
12 页 + 4 页 + 240 页 = 256 页
源码中的 0x8006000U与写成八位形式的 0x08006000U数值完全相同,前导零不影响数值。它是参数区第一页的起点。
需要特别注意:参数区总共 8 KiB、包含 4 页,但 inter_flashif_smart_write_page()将 NbPages固定为 1,所以一次只会擦除 addr所选中的一页。以 0x08006000调用时,被擦除的是 0x08006000~0x080067FF,不是整个 8 KiB 参数区。
1.1.2.4 启动配置
本节原笔记中的“启动配置”实际对应参数区在 IAP 启动流程中的用途。Bootloader 上电后可从固定地址 INTER_FLASH_PARAM_ADDR读取 OTA 配置,并依据升级标志决定是否进入升级流程。
inter_flash_cfg_param_typeDef开启了 #pragma pack(1),字段紧密排列:
| 偏移 | 字段 | 大小 | 含义 |
|---|---|---|---|
0 | magic[4] | 4 字节 | 魔数 AA BB CC DD,用于初步判断参数是否有效 |
4 | ota_bin_version | 4 字节 | 待升级固件版本 |
8 | ota_flag | 1 字节 | 升级标志 |
9 | checksum | 1 字节 | 参数累加校验值 |
10 | format[2] | 2 字节 | 占位字段 |
总大小为 4 + 4 + 1 + 1 + 2 = 12字节。业务层调用:
inter_flashif_smart_write_page(INTER_FLASH_PARAM_ADDR, (uint32_t *)&flash_cfg_param, sizeof(flash_cfg_param) / sizeof(uint32_t));
因此传入的写入长度是 12 / 4 = 3个 32 位字,循环实际写入 12 字节。这里再次说明:smart_write_page()的 len单位是“字”,而 read_page()的 len单位是“字节”,两者不能混用。
配置层读取这 12 字节后先检查 4 字节魔数,再对前 sizeof(flash_cfg_param) - 3 = 9字节做累加校验。这 9 字节正好覆盖魔数、版本号和升级标志;末尾的校验字节及 2 个占位字节不参与计算。该 8 位累加和只能发现一部分传输或存储错误,不能替代 CRC,更不具备防篡改能力。
固定分区地址还必须与链接器配置保持一致,确保 Bootloader 或 APP 的代码段不会占用参数区;否则页擦除可能把正在使用的程序代码一并清掉。
1.1.2.5 flash的读取
inter_flashif_read_page()的三个参数分别是:
| 参数 | 类型 | 含义 |
|---|---|---|
addr | uint32_t | Flash 起始地址 |
buf | uint8_t * | 接收读取结果的 RAM 缓冲区 |
len | uint32_t | 要读取的字节数 |
核心表达式为:
buf[i] = *(volatile uint8_t *)(flash_addr + i);
可按下面的顺序理解:
flash_addr + i计算第i个字节的绝对地址;(volatile uint8_t *)把整数地址转换为“指向易变 8 位无符号整数的指针”;- 前面的
*解引用该指针,从地址中取出 1 字节; - 把结果保存到
buf[i]。
所以转录中的“强转成八个字节的指针”应改为 强制转换成 uint8_t指针,即 8 位/1 字节元素指针。volatile要求编译器每次都执行真实的内存读取,不能把该位置当普通变量随意缓存或省略。
函数中的页大小判断也要准确理解:
if (len > FLASH_PAGE_SIZE)
{
return 1;
}
这只是课程封装设置的单次长度上限,Flash 的存储器映射读取在硬件上并不要求停在页边界。而且只判断 len <= 2048仍不能保证“不跨页”:例如从某页最后 16 字节处读取 32 字节,长度虽小于一页,仍会跨到下一页。
这段源码还没有检查:
addr是否位于有效 Flash 范围;addr + len是否溢出或越过允许区域;buf是否为空指针及其容量是否足够。
因此它适合当前受控工程调用,不能直接当成已经完成全部边界防护的通用驱动。
1.1.2.6 flash的写入
Flash 修改操作的基本顺序是:
解锁控制器
→ 擦除目标页
→ 编程新数据
→ 校验操作结果和回读数据
→ 重新上锁
“解锁”是向 Flash 控制器写入规定的密钥序列,使其允许修改主 Flash;它不是对某段普通数据做软件互斥。完成后调用 HAL_FLASH_Lock(),可以降低程序跑飞或错误指针造成误擦写的风险。
为什么通常要先擦除?
擦除会把整页恢复到全 1状态,即每个 16 位编程单元读作 0xFFFF。STM32F10xxx 的 FPEC 在标准编程前会检查目标半字是否已擦除;若不是 0xFFFF,通常跳过编程并置位 PGERR。官方手册列出的特殊例外是写入 0x0000。因此对需要任意更新的数据,不能把 Flash 当 RAM 直接覆盖,应先擦除目标页。
STM32F103 的底层主 Flash 标准编程粒度是 16 位半字。本工程调用 HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, ...)时选择的是 32 位“字”接口;当前 HAL 实现会把一个 32 位字拆成两次 16 位半字编程。因此业务代码要求目标地址按 4 字节对齐,并每次执行 addr + i * 4。
课程中的三个接口差异为:
| 函数 | 擦除 | 编程 | 适用前提 |
|---|---|---|---|
inter_flashif_smart_write_page() | 固定擦 1 页 | 写 len个 32 位字 | 允许丢弃该页原有全部数据 |
inter_flashif_erase_page() | 固定擦 1 页 | 不写 | 只需清空页面 |
inter_flashif_write_page() | 不擦 | 写 len个 32 位字 | 目标位置已经处于擦除态 |
函数名中的 smart不代表它会自动保留未修改数据。它只是把“擦一页”和“连续写入”组合起来;页内没有重新写入的部分会保持擦除后的 0xFF。
若只修改一页中的少量字段,同时还要保留该页其他内容,正确的软件流程是:
把整页 2048 字节读入 RAM
→ 在 RAM 副本中修改目标字段
→ 擦除原 Flash 页
→ 把完整 2048 字节写回
→ 回读并校验
但这个流程仍需考虑掉电安全:若在擦除后、写回完成前掉电,该页可能只剩部分数据。正式产品通常会采用双页冗余、序号/状态标记和 CRC,再在启动时选择最后一份完整记录。
课程源码本身还有以下风险,阅读时不能忽略:
inter_flashif_smart_write_page()、inter_flashif_erase_page()和inter_flashif_write_page()声明返回uint8_t,但函数末尾都没有return,若调用者读取返回值会产生未定义行为;- 没有检查
HAL_FLASH_Unlock()、HAL_FLASHEx_Erase()和HAL_FLASH_Program()的返回状态; - 没有检查
PageError;当前 HAL 以0xFFFFFFFF表示请求的页面全部擦除成功,也没有发生写后回读验证; smart_write_page()没有限制len <= 512个字,过大会写出刚擦除的一页;- 没有验证地址是否落在预留参数区、是否按 4 字节对齐。
这些是对现有代码的审阅结论,不是在上方代码中擅自补写后续实现。
二、实验演示
本课演示的实际使用场景是把 12 字节 flash_cfg_param保存到参数区起始地址 0x08006000:
擦除第 12 页(0x08006000~0x080067FF,共 2048 字节)
→ 写入 3 个 32 位字(共 12 字节)
→ 本页其余 2036 字节保持 0xFF
这里可以直接擦整页,是因为课程约定该页中除这份结构体外的内容都不需要保留。若同一页以后又存放产品信息或其他参数,就必须先整页读取、修改 RAM 副本、擦除、完整写回,不能继续“不管三七二十一全擦”。
还应注意擦写寿命。STM32F103xC/D/E 数据手册给出的 Flash 擦写耐久度最低值为 10 kcycles。升级标志适合在升级事件发生时偶尔写入,不适合在高频循环中反复擦页;高频持久化数据应使用磨损均衡、EEPROM 仿真或外部非易失存储器。
本课需要掌握的最终结论是:
- STM32F103ZET6 有 512 KiB 主 Flash,共 256 页,每页 2 KiB;
- 正确页大小宏是
0x800U,不是0x820; - Flash 可直接通过地址读取,读取函数中的 2 KiB 限制来自软件封装,不是硬件读取限制;
- 本工程读取长度按字节计,写入长度按 32 位字计;
- 写入任意新数据前应擦除目标页,擦除会影响整页;
- 更新页内部分数据时,应执行“整页读出—修改—擦除—整页写回—校验”;
- 课程当前实现没有完整的地址、长度、HAL 状态、掉电和寿命防护,不能未经补强直接用于可靠产品。
相关官方依据可查阅 PM0075 Flash 编程手册和 STM32F103ZE 数据手册。