Flash读写

Tutorial: 卓越工程师 Category: 嵌入式 Published: 2026-10-23 11:05:29 Views: 0 Likes: 0 Comments: 0

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 划为:

区域起始地址末地址容量页号
Bootloader0x080000000x08005FFF24 KiB0~11,共 12 页
Param0x080060000x08007FFF8 KiB12~15,共 4 页
Application0x080080000x0807FFFF480 KiB16~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),字段紧密排列:

偏移字段大小含义
0magic[4]4 字节魔数 AA BB CC DD,用于初步判断参数是否有效
4ota_bin_version4 字节待升级固件版本
8ota_flag1 字节升级标志
9checksum1 字节参数累加校验值
10format[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()的三个参数分别是:

参数类型含义
addruint32_tFlash 起始地址
bufuint8_t *接收读取结果的 RAM 缓冲区
lenuint32_t要读取的字节数

核心表达式为:

buf[i] = *(volatile uint8_t *)(flash_addr + i);

可按下面的顺序理解:

  1. flash_addr + i计算第 i个字节的绝对地址;
  2. (volatile uint8_t *)把整数地址转换为“指向易变 8 位无符号整数的指针”;
  3. 前面的 *解引用该指针,从地址中取出 1 字节;
  4. 把结果保存到 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,再在启动时选择最后一份完整记录。

课程源码本身还有以下风险,阅读时不能忽略:

  1. inter_flashif_smart_write_page()、inter_flashif_erase_page()和 inter_flashif_write_page()声明返回 uint8_t,但函数末尾都没有 return,若调用者读取返回值会产生未定义行为;
  2. 没有检查 HAL_FLASH_Unlock()、HAL_FLASHEx_Erase()和 HAL_FLASH_Program()的返回状态;
  3. 没有检查 PageError;当前 HAL 以 0xFFFFFFFF表示请求的页面全部擦除成功,也没有发生写后回读验证;
  4. smart_write_page()没有限制 len <= 512个字,过大会写出刚擦除的一页;
  5. 没有验证地址是否落在预留参数区、是否按 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 数据手册。