15-FLASH内部存储器.md 15 KB


type: raw source: bilibili lecture: "15-1 FLASH闪存, 15-2 读写内部FLASH & 读取芯片ID" lecture_material: "15-1 FLASH闪存, 15-2 读写内部FLASH&读取芯片ID" project_code: "15-1 读写内部FLASH, 15-2 读取芯片ID"

tags: [stm32, flash, memory, chip-id, uid]

STM32-FLASH 内部闪存

1 Flash 架构概述

Flash 闪存模块组织 上图:STM32 Flash 闪存模块组织(来源:STM32入门教程 PPT 第196页)

Flash 内部结构 上图:STM32 Flash 内部结构(主存储器、系统存储器、选项字节)(来源:STM32入门教程 PPT 第197页)

STM32F103C8T6 的内部 Flash 组织结构(以 64KB 为例):

0x08000000  ┌──────────────────────┐
            │  第 0 页(1KB)       │  启动代码区的起始
            │  第 1 页(1KB)       │
            │  ...                  │
            │  第 62 页(1KB)      │
            │  第 63 页(1KB)      │
0x0800FFFF └──────────────────────┘     ← 64KB 主存储区

0x1FFFF000  ┌──────────────────────┐
            │  系统存储区(2KB)   │   ← 出厂 Bootloader
0x1FFFF7FF  └──────────────────────┘

0x1FFFF800  ┌──────────────────────┐
            │  选项字节(16B)     │   ← 读保护、看门狗等配置
0x1FFFF80F  └──────────────────────┘

教师讲解:Flash 按页(Page)管理,每页 1KB(大容量型号每页 2KB)。写 Flash 时必须以页为单位擦除,不能单独擦除某个字节。

1.1 分区

区域 地址范围 大小 用途
主存储器 0x08000000 ~ 结束 64KB~1MB 存储程序代码和数据
系统存储区 0x1FFFF000 ~ 0x1FFFF7FF 2KB ISP Bootloader
选项字节 0x1FFFF800 ~ 0x1FFFF80F 16B 读保护/看门狗配置

不同型号 Flash 容量对应关系:

型号 容量 页数 每页大小
STM32F103C4 16KB 16 1KB
STM32F103C8 64KB 64 1KB
STM32F103CB 128KB 128 1KB
STM32F103RC 256KB 256 1KB
STM32F103VE 512KB 512 2KB
STM32F103ZG 1MB 512 2KB

2 读操作

Flash 读操作不需要解锁,直接用地址指针读取:

uint32_t MyFLASH_ReadWord(uint32_t Address)
{
    return *((__IO uint32_t *)(Address));
}

uint16_t MyFLASH_ReadHalfWord(uint32_t Address)
{
    return *((__IO uint16_t *)(Address));
}

uint8_t MyFLASH_ReadByte(uint32_t Address)
{
    return *((__IO uint8_t *)(Address));
}

注意

  • 16 位总线宽度(半字访问最快)
  • __IO 防止编译器优化掉指针访问
  • 读取不受 Flash 控制器状态影响——任何时候都能读

教师强调:读 Flash 就像读普通内存变量一样简单——因为 Cortex-M3 直接从 Flash 总线读取指令和数据,ARM 已经把映射做好了。

3 写操作(编程)

3.1 解锁序列

Flash 控制器默认加锁,防止误写:

FLASH_Unlock();  // 写 KEY1=0x45670123 → KEY2=0xCDEF89AB 到 FLASH_KEYR

若还需写入选项字节:

FLASH_Unlock();         // 主解锁
FLASH_OB_Unlock();     // 选项字节解锁(写入 0x45670123, 0xCDEF89AB 到 FLASH_OPTKEYR)

教师解释:解锁序列就像输入"密码"——必须按照 ST 规定的密钥顺序写入。如果写错了顺序,Flash 控制器会视作非法操作并锁定。这是 Flash 写保护的基本机制。

3.2 擦除

写前必须擦除——Flash 只能将 1 写为 0,不能将 0 写回 1:

// 页擦除
FLASH_Unlock();
FLASH_ErasePage(PageAddress);   // 一次擦除整页(1KB 或 2KB)
FLASH_Lock();

// 全擦除
FLASH_Unlock();
FLASH_EraseAllPages();          // 擦除所有用户区(包括程序自身!)
FLASH_Lock();

擦除 HAL 库

FLASH_EraseInitTypeDef EraseInitStruct = {0};
uint32_t PageError = 0;

HAL_FLASH_Unlock();

EraseInitStruct.TypeErase   = FLASH_TYPEERASE_PAGES;     // 页擦除
EraseInitStruct.PageAddress = 0x0800FC00;                // 起始地址
EraseInitStruct.NbPages     = 1;                         // 擦除 1 页
if (HAL_FLASHEx_Erase(&EraseInitStruct, &PageError) != HAL_OK)
{
    // 擦除失败,通过 HAL_FLASH_GetError() 获取详细错误
    uint32_t err = HAL_FLASH_GetError();
}

// 全擦除
EraseInitStruct.TypeErase = FLASH_TYPEERASE_MASSERASE;
HAL_FLASHEx_Erase(&EraseInitStruct, &PageError);

HAL_FLASH_Lock();

3.3 写入

擦除后,Flash 全页为 0xFF,然后写入数据:

// 编程 32 位字
FLASH_Unlock();
FLASH_ProgramWord(Address, Data);
FLASH_Lock();

// 编程 16 位半字
FLASH_Unlock();
FLASH_ProgramHalfWord(Address, Data);
FLASH_Lock();

写入 HAL 库

HAL_FLASH_Unlock();

// 按字节写入
HAL_FLASH_Program(FLASH_TYPEPROGRAM_BYTE,      Address, Data8);

// 按半字写入
HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD,  Address, Data16);

// 按字写入
HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD,      Address, Data32);

HAL_FLASH_Lock();

⚠ 写入前必须保证该地址所在页已擦除(全 0xFF),否则写入不可靠。

3.4 完整写入流程

void MyFLASH_WritePage(uint32_t PageAddr, uint16_t *data, uint16_t count)
{
    FLASH_Unlock();                              // 1. 解锁

    FLASH_ErasePage(PageAddr);                   // 2. 擦除整页

    for (uint16_t i = 0; i < count; i++)
    {
        FLASH_ProgramHalfWord(PageAddr + i * 2, data[i]);  // 3. 逐半字写入
    }

    FLASH_Lock();                                // 4. 加锁
}

3.5 Flash 控制器状态检查

FLASH_Status status = FLASH_WaitForLastOperation(Timeout);  // 等待 BSY 位清零
// 可检查:FLASH_COMP_PG 编程错误 / FLASH_COMP_WRPRT 写保护错误

4 代码分析:参数存储模块(15-1 读写内部 Flash)

4.1 MyFLASH 封装层

// MyFLASH.c — 封装标准库 Flash 操作
void MyFLASH_ErasePage(uint32_t PageAddress)
{
    FLASH_Unlock();
    FLASH_ErasePage(PageAddress);
    FLASH_Lock();
}

void MyFLASH_ProgramHalfWord(uint32_t Address, uint16_t Data)
{
    FLASH_Unlock();
    FLASH_ProgramHalfWord(Address, Data);
    FLASH_Lock();
}

4.2 Store 层(参数存储模块)

// Store.c — 存储中间件
#define STORE_START_ADDRESS   0x0800FC00   // 最后 1KB 页(64KB 的最后一页)
#define STORE_COUNT          512           // 512 个 16 位数据

uint16_t Store_Data[STORE_COUNT];           // SRAM 缓冲数组

void Store_Init(void)
{
    // 首次使用:擦除页 → 写标志位 0xA5A5 → 清空其余位置
    if (MyFLASH_ReadHalfWord(STORE_START_ADDRESS) != 0xA5A5)
    {
        MyFLASH_ErasePage(STORE_START_ADDRESS);
        MyFLASH_ProgramHalfWord(STORE_START_ADDRESS, 0xA5A5);
        for (uint16_t i = 1; i < STORE_COUNT; i++)
            MyFLASH_ProgramHalfWord(STORE_START_ADDRESS + i * 2, 0x0000);
    }
    // 上电加载:Flash → SRAM
    for (uint16_t i = 0; i < STORE_COUNT; i++)
        Store_Data[i] = MyFLASH_ReadHalfWord(STORE_START_ADDRESS + i * 2);
}

void Store_Save(void)
{
    MyFLASH_ErasePage(STORE_START_ADDRESS);
    for (uint16_t i = 0; i < STORE_COUNT; i++)
        MyFLASH_ProgramHalfWord(STORE_START_ADDRESS + i * 2, Store_Data[i]);
}

void Store_Clear(void)
{
    for (uint16_t i = 1; i < STORE_COUNT; i++)
        Store_Data[i] = 0x0000;
    Store_Save();
}

4.3 参数存储 HAL 库实现

// Store.c — HAL 版,使用 HAL_FLASH 函数
#define STORE_START_ADDRESS   0x0800FC00
#define STORE_COUNT          512

uint16_t Store_Data[STORE_COUNT];

static void Store_ErasePage_HAL(void)
{
    FLASH_EraseInitTypeDef erase = {0};
    uint32_t PageError = 0;
    erase.TypeErase   = FLASH_TYPEERASE_PAGES;
    erase.PageAddress = STORE_START_ADDRESS;
    erase.NbPages     = 1;
    HAL_FLASHEx_Erase(&erase, &PageError);
}

void Store_Init_HAL(void)
{
    HAL_FLASH_Unlock();
    if (HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, STORE_START_ADDRESS, 0xA5A5) != HAL_OK)
    {
        // 第一次初始化:擦除后写标志
        Store_ErasePage_HAL();
        HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, STORE_START_ADDRESS, 0xA5A5);
        for (uint16_t i = 1; i < STORE_COUNT; i++)
            HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, STORE_START_ADDRESS + i * 2, 0x0000);
    }
    HAL_FLASH_Lock();
    // 上电加载
    for (uint16_t i = 0; i < STORE_COUNT; i++)
        Store_Data[i] = *((__IO uint16_t *)(STORE_START_ADDRESS + i * 2));
}

void Store_Save_HAL(void)
{
    HAL_FLASH_Unlock();
    Store_ErasePage_HAL();
    for (uint16_t i = 0; i < STORE_COUNT; i++)
        HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, STORE_START_ADDRESS + i * 2, Store_Data[i]);
    HAL_FLASH_Lock();
}

4.4 main.c 主逻辑

int main(void)
{
    OLED_Init();
    Key_Init();
    Store_Init();       // 上电加载 Flash → SRAM

    OLED_ShowString(1, 1, "Flag:");
    OLED_ShowString(2, 1, "Data:");

    while (1)
    {
        KeyNum = Key_GetNum();

        if (KeyNum == 1)               // 按键1:修改数据 → 保存
        {
            Store_Data[1] ++;
            Store_Data[2] += 2;
            Store_Data[3] += 3;
            Store_Data[4] += 4;
            Store_Save();
        }

        if (KeyNum == 2)              // 按键2:全部清0
        {
            Store_Clear();
        }

        OLED_ShowHexNum(1, 6, Store_Data[0], 4);   // 标志位
        OLED_ShowHexNum(3, 1, Store_Data[1], 4);   // 数据
        // ...
    }
}

教师总结设计要点:

  • 选择最后 1KB 页作为存储区——不占用 Flash 前面的代码空间。
  • 0xA5A5 标志位 判断是否首次上电——如果首次则格式化。
  • 使用 SRAM 缓冲 + Flash 备份 策略——运行中只操作 SRAM,保存时才写 Flash(Flash 写入很慢且有擦写寿命限制)。

4.5 设计要点

问题 解决方案
存储地址放在哪里? 最后 1 页(0x0800FC00),不干扰代码区
如何判断首次使用? 标志位 0xA5A5 存到第一个半字
写入前需要做什么? 先页擦除 → 再逐一写入
如何提高写寿命? SRAM 做缓冲,仅在有修改时才触发保存
Flash 擦写寿命(~10k 次) 每个保存操作成本 = 1 擦除 + 512 次写入

5 读取芯片 ID

STM32 在系统存储区中有两个关键只读寄存器:

5.1 闪存容量寄存器(0x1FFFF7E0 - 0x1FFFF7E1)

  • 0x1FFFF7E0 都一个 16 位只读寄存器
  • KB 为单位:C8 = 0x0040 = 64KB

    uint16_t flash_size = *((__IO uint16_t *)(0x1FFFF7E0));
    // C8T6 → 64 → 0x0040
    

5.2 96 位唯一标识(0x1FFFF7E8 - 0x1FFFF7F3)

  • 基址 0x1FFFF7E8,96 位(12 字节)
  • 每个芯片唯一,可用于加密验证、序列号

5.3 代码示例(15-2)

int main(void)
{
    OLED_Init();

    // 显示闪存容量
    OLED_ShowString(1, 1, "F_SIZE:");
    OLED_ShowHexNum(1, 8, *((__IO uint16_t *)(0x1FFFF7E0)), 4);

    // 显示 96-bit UID
    OLED_ShowString(2, 1, "U_ID:");
    OLED_ShowHexNum(2, 6,  *((__IO uint16_t *)(0x1FFFF7E8)),            4);
    OLED_ShowHexNum(2, 11, *((__IO uint16_t *)(0x1FFFF7E8 + 0x02)),    4);
    OLED_ShowHexNum(3, 1,  *((__IO uint32_t *)(0x1FFFF7E8 + 0x04)),    8);
    OLED_ShowHexNum(4, 1,  *((__IO uint32_t *)(0x1FFFF7E8 + 0x08)),    8);

    while (1) {}
}

教师说明:唯一 ID 的典型应用场景——软件加密:写固件时绑定 UID,即使固件被读出也无法在其他芯片上运行。或作为网络节点的 MAC 地址。

5.4 应用示例

// 软件加密验证
uint8_t CheckUID(void)
{
    uint8_t uid[12];
    for (int i = 0; i < 12; i++)
        uid[i] = *((__IO uint8_t *)(0x1FFFF7E8 + i));

    for (int i = 0; i < 12; i++)
        if (uid[i] != EXPECTED_UID[i])
            return 0;   // 芯片不匹配,拒绝运行

    return 1;
}

// 生成唯一 MAC 地址
void GenerateMAC(uint8_t *mac)
{
    uint32_t uid0 = *((__IO uint32_t *)(0x1FFFF7E8));
    uint32_t uid1 = *((__IO uint32_t *)(0x1FFFF7EC));
    uint32_t uid2 = *((__IO uint32_t *)(0x1FFFF7F0));
    mac[0] = 0x02;                       // 本地管理
    mac[1] = 0x00;
    mac[2] = (uid0 & 0xFF0000) >> 16;
    mac[3] = (uid0 & 0x0000FF00) >> 8;
    mac[4] = uid1 & 0xFF;
    mac[5] = uid2 & 0xFF;
}

寄存器:FLASH操作注意(尚硅谷参考)

尚硅谷课程中 FLASH 部分使用外部 SPI Flash(W25Q32)而非内部 FLASH 进行多字节写入,因为内部 FLASH 必须以页擦除再写入,不适合频繁小量写入。

内部 FLASH 解锁密钥寄存器参考:

寄存器 密钥值
FLASH->KEYR 0x45670123 → 0xCDEF89AB(依次写入)
FLASH->OPTKEYR 0x45670123 → 0xCDEF89AB(选项字节解锁)
FLASH->KEYR = 0x45670123;   // 密钥1
FLASH->KEYR = 0xCDEF89AB;   // 密钥2 → 解锁

完整操作序列参考标准库或 HAL 库封装。

6 总结

Flash 操作流程总结:

读 ──→ 直接指针访问,无需解锁
写 ──→ 解锁 → 擦除页 → 编程写入 → 加锁
芯片 ID ──→ 只读系统地址 0x1FFFF7E0 / 0x1FFFF7E8

应用场景对照

场景 使用策略
系统参数掉电保存 取最后一页 Flash,上电加载到 SRAM,修改后保存
固件加密 读取 96-bit UID 做加密校验
IAP 升级 使用扩展存储区(非当前运行页)存储新固件
数据日志 循环使用多页,磨损均衡(避免单页擦写死)

注意事项

  1. 页写入非原子:先擦后写,写过程中掉电 → 数据丢失。建议对关键数据保存两页(备份策略)。
  2. 擦写寿命:约 10,000 次。频繁写入场景(如日志记录)时需要磨损均衡。
  3. 程序写入自身:全擦除会删除程序。确保擦除的是非当前执行的页。
  4. Flash 等待周期:STM32F1 需要 2 个等待周期(72MHz 运行时),由 FLASH_ACR 配置。