【STM32】 内部 Flash 的 HAL 库函数详解与自定义封装实战


STM32 内部 Flash 的 HAL 库函数详解与自定义封装实战

一、HAL 库操作 Flash 的关键函数

1.1 函数总览

操作 函数 说明
解锁 Flash HAL_FLASH_Unlock() 写两把密钥,打开"保险"
锁定 Flash HAL_FLASH_Lock() 置 LOCK 位,重新上锁
擦除 Flash HAL_FLASHEx_Erase() 页擦除 / 整片擦除(带结构体参数)
写入 Flash HAL_FLASH_Program() 半字 / 字 / 双字写入
读取 Flash 直接指针解引用 没有封装函数,读不需要解锁

核心认知:

HAL 库把"解锁→等 BSY→设模式→触发→等完成→清标志→退模式→加锁"整个流程封装进了这几个函数里。

函数内部走的流程,和我们上篇手写寄存器版完全一致------读源码就能互相印证。

1.2 解锁 / 锁定(HAL_FLASH_Unlock / HAL_FLASH_Lock)

函数签名:

cpp 复制代码
HAL_StatusTypeDef HAL_FLASH_Unlock(void);   // 参数:无;返回值:HAL_OK / HAL_ERROR
HAL_StatusTypeDef HAL_FLASH_Lock(void);     // 参数:无;返回值:HAL_OK / HAL_ERROR

源码解析:

cpp 复制代码
HAL_StatusTypeDef HAL_FLASH_Unlock(void)
{
    HAL_StatusTypeDef status = HAL_OK;

    if(READ_BIT(FLASH->CR, FLASH_CR_LOCK) != RESET)
    {
        /* Authorize the FLASH Registers access */
        WRITE_REG(FLASH->KEYR, FLASH_KEY1);   // 写入 0x45670123
        WRITE_REG(FLASH->KEYR, FLASH_KEY2);   // 写入 0xCDEF89AB

        /* Verify Flash is unlocked */
        if(READ_BIT(FLASH->CR, FLASH_CR_LOCK) != RESET)
        {
            status = HAL_ERROR;
        }
    }
    return status;
}

读源码的收获:

  1. HAL 库里把密钥定义成了宏 FLASH_KEY1 = 0x45670123、FLASH_KEY2 = 0xCDEF89AB;
  2. 解锁前会先检查 LOCK 位:如果已经解锁(LOCK=0),直接返回 HAL_OK,不重复写密钥;
  3. 写完后回读验证:LOCK 位如果还是 1,说明解锁失败,返回 HAL_ERROR------"写完不验"是新手手写版最常见的 bug。
cpp 复制代码
HAL_StatusTypeDef HAL_FLASH_Lock(void)
{
    /* Set the LOCK Bit to lock the FLASH Registers access */
    SET_BIT(FLASH->CR, FLASH_CR_LOCK);   // 置 LOCK=1,硬件自动锁
    return HAL_OK;
}

记住:

解锁用 FLASH_KEYR(密钥寄存器),锁定用 FLASH_CR 的 LOCK 位------解锁和锁定用的不是同一个寄存器。

1.3 擦除参数结构体 FLASH_EraseInitTypeDef

HAL_FLASHEx_Erase 的参数不是散装变量,而是一个结构体:

cpp 复制代码
typedef struct
{
    uint32_t TypeErase;     /*!< 擦除类型:全片擦除或页擦除 */
    uint32_t Banks;         /*!< 全片擦除时选择要擦除的存储体 */
    uint32_t PageAddress;   /*!< 页擦除时的起始页地址 */
    uint32_t NbPages;       /*!< 要擦除的页数 */
} FLASH_EraseInitTypeDef;

字段说明:

字段 取值 说明
TypeErase FLASH_TYPEERASE_MASSERASE 片擦除
FLASH_TYPEERASE_PAGES 页擦除
Banks FLASH_BANK_1 / FLASH_BANK_2 F1 单 Bank,此参数可忽略
PageAddress 页起始地址 仅页擦除用,必须 2KB 对齐
NbPages 页数量 仅页擦除用

1.4 擦除(HAL_FLASHEx_Erase)

函数签名:

cpp 复制代码
HAL_StatusTypeDef HAL_FLASHEx_Erase(FLASH_EraseInitTypeDef *pEraseInit,
                                    uint32_t *PageError);
  • pEraseInit:擦除配置(上面的结构体);
  • PageError:输出型参数------出错时返回出错页的地址;0xFFFFFFFF 表示所有页都擦除成功;
  • 返回值:HAL_OK / HAL_ERROR / HAL_TIMEOUT。

片擦除源码核心逻辑:

cpp 复制代码
if (FLASH_WaitForLastOperation(FLASH_TIMEOUT_VALUE) == HAL_OK)
{
    /* Mass erase to be done */
    FLASH_MassErase(FLASH_BANK1);

    /* Wait for last operation to be completed */
    status = FLASH_WaitForLastOperation(FLASH_TIMEOUT_VALUE);

    /* If the erase operation is completed, disable the MER Bit */
    CLEAR_BIT(FLASH->CR, FLASH_CR_MER);
}

static void FLASH_MassErase(uint32_t Banks)
{
    SET_BIT(FLASH->CR, FLASH_CR_MER);    // ① 使能片擦除
    SET_BIT(FLASH->CR, FLASH_CR_STRT);   // ② 触发擦除
}

页擦除源码核心逻辑(多页用 for 循环逐页擦):

cpp 复制代码
for(address = pEraseInit->PageAddress;
    address < ((pEraseInit->NbPages * FLASH_PAGE_SIZE) + pEraseInit->PageAddress);
    address += FLASH_PAGE_SIZE)
{
    FLASH_PageErase(address);                      // 单页擦除
    status = FLASH_WaitForLastOperation(FLASH_TIMEOUT_VALUE);
    CLEAR_BIT(FLASH->CR, FLASH_CR_PER);            // 擦完清 PER
    if (status != HAL_OK)
    {
        *PageError = address;                      // 记录出错页
        break;
    }
}

void FLASH_PageErase(uint32_t PageAddress)
{
    SET_BIT(FLASH->CR, FLASH_CR_PER);              // ① 使能页擦除
    WRITE_REG(FLASH->AR, PageAddress);             // ② 写页地址
    SET_BIT(FLASH->CR, FLASH_CR_STRT);             // ③ 触发擦除
}

读源码的收获:

页擦除的寄存器三连 = PER(选模式)→ AR(写地址)→ STRT(触发),和我们手写版完全一致。

1.5 编程写入(HAL_FLASH_Program)

函数签名:

复制代码
HAL_StatusTypeDef HAL_FLASH_Program(uint32_t TypeProgram,
                                    uint32_t Address,
                                    uint64_t Data);
  • TypeProgram:写入方式
宏 值 含义
FLASH_TYPEPROGRAM_HALFWORD 0x01U 半字写入(2 字节)
FLASH_TYPEPROGRAM_WORD 0x02U 字写入(4 字节)
FLASH_TYPEPROGRAM_DOUBLEWORD 0x03U 双字写入(8 字节)

源码核心逻辑(注意看它是怎么"拆"的):

cpp 复制代码
HAL_StatusTypeDef HAL_FLASH_Program(uint32_t TypeProgram, uint32_t Address,
                                    uint64_t Data)
{
    HAL_StatusTypeDef status = HAL_ERROR;
    uint8_t index = 0;
    uint8_t nbiterations = 0;

    status = FLASH_WaitForLastOperation(FLASH_TIMEOUT_VALUE);
    if(status == HAL_OK)
    {
        if(TypeProgram == FLASH_TYPEPROGRAM_HALFWORD)
            nbiterations = 1U;              // 半字:写 1 次
        else if(TypeProgram == FLASH_TYPEPROGRAM_WORD)
            nbiterations = 2U;              // 字:写 2 次半字
        else
            nbiterations = 4U;              // 双字:写 4 次半字

        for (index = 0U; index < nbiterations; index++)
        {
            FLASH_Program_HalfWord((Address + (2U*index)),
                                   (uint16_t)(Data >> (16U*index)));
            status = FLASH_WaitForLastOperation(FLASH_TIMEOUT_VALUE);

            /* If the program operation is completed, disable the PG Bit */
            CLEAR_BIT(FLASH->CR, FLASH_CR_PG);

            if (status != HAL_OK)
                break;
        }
    }
    return status;
}

static void FLASH_Program_HalfWord(uint32_t Address, uint16_t Data)
{
    SET_BIT(FLASH->CR, FLASH_CR_PG);          // ① 使能编程
    *(__IO uint16_t*)Address = Data;          // ② 指针解引用写半字
}

读源码的收获(重点):

  1. F103 硬件只支持半字(16bit)写入------HAL 库里的"字写入(32bit)"其实是拆成 2 次半字写,"双字写入(64bit)"是拆成 4 次半字写;
  2. 每次写完都调 FLASH_WaitForLastOperation 等 BSY 清零;
  3. 编程位 PG 在每次写完后立即清除(而不是全部写完后);
  4. 底层真正写 Flash 的动作就是一行:*(__IO uint16_t*)Address = Data;------和我们手写的指针解引用一模一样。

这就是为什么上一篇你问"为什么存在半字、单字、双字写入":

HAL 层给你提供了 1/2/4 字节的"名义宽度",但物理上 F1 永远是一个半字一个半字地写。字和双字只是 HAL 帮你循环了多次。


二、一个重要事实:HAL 的"写"不帮你擦除

写(编程)必须支持"先擦除,后写入"------因为 Flash 只能 1→0,目标地址必须是 0xFFFF 才能写。

但看源码你会发现:

HAL_FLASH_Program 里没有任何擦除动作,只负责写入。

为什么?

HAL 库不想承担这个风险。

**要写,你自己擦;**擦除函数给你,冷暖用户自知。

所以真实项目里永远是:先 HAL_FLASHEx_Erase 擦页,再 HAL_FLASH_Program 写数据,两步分开做。


三、案例:封装 InternalFlash 操作接口

3.1 设计目标

课件封装了 5 个接口,加上测试框架:

接口 功能
InternalFlash_Read 读取指定地址数据
InternalFlash_ErasePages 页擦除(含 2KB 对齐校验)
InternalFlash_Program 编程写入(半字打包,不擦除)
InternalFlash_Write 先擦后写(自动计算涉及页数)
InternalFlash_MassErase 整片擦除
InternalFlash_AutoTest 自动化测试入口

准备:

CubeMX 建工程、开 USART1、重定向 printf(之前的 BSP_USART_Redir 复用)。片上 Flash 不需要在 CubeMX 里配置------它作为程序存储器默认可读、可取指执行,不用像 GPIO 那样初始化,属于"零帧起手"。

3.2 InternalFlash.h

cpp 复制代码
#ifndef __INTERNAL_FLASH_H
#define __INTERNAL_FLASH_H

#ifdef __cplusplus
extern "C" {   // 告诉编译器,头文件里的函数是用C语言规则编译的
#endif

#include <stdbool.h>
#include <stdio.h>
#include "stm32f1xx_hal.h"

void InternalFlash_AutoTest(void);
void InternalFlash_Read(uint32_t start_addr, uint8_t *outbuffer, uint32_t len);
bool InternalFlash_ErasePages(uint32_t start_page_addr, uint32_t nb_pages);
bool InternalFlash_Write(uint32_t start_addr, const uint8_t *inbuffer, uint32_t len);
bool InternalFlash_MassErase(void);

#ifdef __cplusplus
}
#endif

#endif

3.3 InternalFlash_Read ------ 读操作

cpp 复制代码
void InternalFlash_Read(uint32_t start_addr, uint8_t *outbuffer, uint32_t len)
{
    for (uint32_t i = 0; i < len; i++)
    {
        outbuffer[i] = *(volatile uint8_t *)(start_addr + i);
    }
}
  • 参数:start_addr 起始地址、outbuffer 输出缓冲区、len 字节数;
  • 读不需要解锁、不需要初始化------Flash 被映射到地址空间,像读 RAM 一样用指针解引用即可;
  • volatile 防止编译器优化掉访问。

3.4 InternalFlash_ErasePages ------ 页擦除 + 2KB 对齐

cpp 复制代码
// 原则: 谁操作Flash,谁解锁
// 2KB对齐,2进制低11位是0
const uint32_t g_page_size = FLASH_PAGE_SIZE;            // 页大小 0x800 = 2048
const uint32_t g_page_offset_mask = FLASH_PAGE_SIZE - 1; // 页内偏移掩码 0x7FF
const uint32_t g_page_base_mask = ~(FLASH_PAGE_SIZE - 1);// 页基地址掩码

bool InternalFlash_ErasePages(uint32_t start_page_addr, uint32_t nb_pages)
{
    if (nb_pages == 0U)
        return false;
    if (start_page_addr & g_page_offset_mask)   // 不是2KB对齐
        return false;

    FLASH_EraseInitTypeDef erase_init = {0};
    erase_init.TypeErase   = FLASH_TYPEERASE_PAGES;
    erase_init.PageAddress = start_page_addr;
    erase_init.NbPages     = nb_pages;

    uint32_t page_error = 0;
    HAL_FLASH_Unlock();                                  // 谁操作,谁解锁
    bool res = (HAL_FLASHEx_Erase(&erase_init, &page_error) == HAL_OK);
    HAL_FLASH_Lock();                                    // 用完立刻上锁
    return res;
}

2KB 对齐的位运算原理(面试高频):

  • F103ZET6 页大小 = 2KB = 2048 = 2^11;
  • 一个地址是 2KB 的整数倍 ⟺ 它的低 11 位全是 0;
  • 判断对齐:addr & 0x7FF(0x7FF 是低 11 位的掩码)结果为 0 则对齐;
  • 所以三个掩码的来历:
    • g_page_offset_mask = 0x7FF → 提取"页内偏移";
    • g_page_base_mask = ~0x7FF → 保留"页基地址",把低 11 位清零。

5 个 2KB 对齐地址示例:

序号 十六进制 十进制 二进制(32 位) 低 11 位
1 0x08000000 134217728 0000 1000 0000 0000 0000 0000 0000 0000 000 0000 0000
2 0x08000800 134219776 0000 1000 0000 0000 0000 1000 0000 0000 000 0000 0000
3 0x08001000 134221824 0000 1000 0000 0000 0001 0000 0000 0000 000 0000 0000
4 0x08001800 134223872 0000 1000 0000 0000 0001 1000 0000 0000 000 0000 0000
5 0x08002000 134225920 0000 1000 0000 0000 0010 0000 0000 0000 000 0000 0000

3.5 InternalFlash_Program ------ 编程(半字打包 + 小端)

cpp 复制代码
static inline uint16_t PackHalfWord(const uint8_t *buf, uint32_t remain)
{
    if (remain == 0U)
        return 0;
    if (remain == 1U)
        return (uint16_t)buf[0];   // 高位自动补0
    return (uint16_t)(((uint16_t)buf[1] << 8U) | ((uint16_t)buf[0])); // 小端存储
}

static bool InternalFlash_Program(uint32_t start_addr, const uint8_t *inbuffer, uint32_t len)
{
    if (inbuffer == NULL || len == 0)
        return false;
    if (start_addr & 0x1U)   // 地址必须是2个字节的整数倍(半字对齐)
        return false;

    uint32_t i = 0;
    HAL_FLASH_Unlock();
    for (; i < len; i += 2)
    {
        uint16_t data = PackHalfWord(inbuffer + i, len - i);
        if (HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, start_addr + i, data) != HAL_OK)
        {
            break;
        }
    }
    HAL_FLASH_Lock();
    if (i < len)
        return false;
    return true;
}

为什么需要 PackHalfWord?

HAL_FLASH_Program 只能按半字/字/双字写,不能按字节写。

而用户给的是字节数组,所以要每两个字节拼成一个 uint16_t 再写。

小端字节序的细节(面试会问):

cpp 复制代码
// 小端:低位放低地址
buf[0] = 0x00, buf[1] = 0x11  →  拼成 0x1100  →  写到 0x0807E000(0x00 在低地址)
buffer 下标 buffer 数据 拼成的半字 Flash 物理地址 Flash 实际存储字节
0 0x00 0x1100 0x0807E000 0x00(低字节)
1 0x11 ↑ 0x0807E001 0x11(高字节)
2 0x22 0x3322 0x0807E002 0x22
3 0x33 ↑ 0x0807E003 0x33

关键结论:

Flash 物理地址从低到高读出来的字节,就是 buffer 从 0 到 n 的顺序,一模一样。

为什么必须按小端拼?

因为数据写进去未来要被 CPU 读。

STM32(Cortex-M3)是小端 CPU,它读 uint16_t 时默认低位在低地址。

HAL 库不会帮你做大小端转换、结构体序列化------这些都要你在调用前自己组织好。

大小端本质是"单片机逼着用户和自己建立共识"。

3.6 InternalFlash_Write ------ 先擦后写,自动计算页数

cpp 复制代码
// 页地址向下2KB对齐(往小取整)
static inline uint32_t AlignDown2Page(uint32_t addr)
{
    return (addr & g_page_base_mask);
}

// 页地址向上2KB对齐(往大取整)
static inline uint32_t AlignUp2Page(uint32_t addr)
{
    return (addr + g_page_size - 1) & g_page_base_mask;
}

bool InternalFlash_Write(uint32_t start_addr, const uint8_t *inbuffer, uint32_t len)
{
    if (inbuffer == NULL || len == 0)
        return false;
    if (start_addr & 0x1U)   // 半字对齐校验
        return false;

    // 计算涉及多少页
    uint32_t start_page = AlignDown2Page(start_addr);
    uint32_t end_page   = AlignUp2Page(start_addr + len);
    // 前闭后开:如写入[10,20),数字个数 = 20-10 = 10,不用纠结 +1/-1
    uint32_t nb_pages = (end_page - start_page) / g_page_size;

    // 先擦后写
    bool result = (InternalFlash_ErasePages(start_page, nb_pages) &&
                   InternalFlash_Program(start_addr, inbuffer, len));
    return result;
}

**为什么用"前闭后开"区间?**这是计算"两个地址之间有多少页"最干净的方式:

  • [start_page, end_page) 前闭后开------start_page 包含,end_page 不包含;
  • 页数 = (end_page - start_page) / 页大小,不需要 +1/-1;
  • 就像你写 for(int i = 0; i < 10; i++) 循环 10 次一样自然;
  • AlignUp2Page(addr + len) 保证 end_page 指向最后一个有效页的下一页(因为只要用到该页一个字节,该页就算涉及)。

注意该函数的"破坏性":

它会把涉及到的整页擦掉再写。

使用者必须知道:

起始地址所在的整页都会被擦除,页内其他数据会丢失------所以数据区要独立规划。

3.7 InternalFlash_MassErase ------ 整片擦除

cpp 复制代码
bool InternalFlash_MassErase(void)
{
    FLASH_EraseInitTypeDef erase_init = {0};
    erase_init.TypeErase   = FLASH_TYPEERASE_MASSERASE;  // 整片擦除
    erase_init.Banks       = FLASH_BANK_1;               // ZET6 单 Bank,忽略
    erase_init.NbPages     = 0;                          // 忽略
    erase_init.PageAddress = 0;                          // 忽略

    uint32_t page_error = 0;
    HAL_FLASH_Unlock();
    HAL_StatusTypeDef state = HAL_FLASHEx_Erase(&erase_init, &page_error);
    HAL_FLASH_Lock();
    return state == HAL_OK;
}

⚠️ 整片擦除会把程序也擦掉------课件实测:

复位后打印一条 "Mass Erase Begin" 就没下文了,之后怎么按复位都没反应。

因为 Main memory 全变成 0xFF,连打印语句的代码都没了。

这是演示"擦除物理效果"的经典实验,真项目里绝不乱跑。


四、测试代码与实验现象

4.1 测试框架(条件编译)

cpp 复制代码
#define DEBUG

void InternalFlash_AutoTest(void)
{
#ifdef DEBUG
    TestRead();        HAL_Delay(1000);
    TestErase();       HAL_Delay(1000);
    TestProgram();     HAL_Delay(1000);
    TestWrite();       HAL_Delay(1000);
    TestEraseMess();
#endif
}

各测试函数:

cpp 复制代码
// 测试读:读 0x08000000(程序起始处)
static void TestRead(void)
{
    printf(">>> Internal Flash Test Begin:\n");
    uint8_t buffer[16] = {0};
    uint16_t num = sizeof(buffer) / sizeof(buffer[0]);
    InternalFlash_Read(0x08000000, buffer, num);
    for (int i = 0; i < num; i++)
        printf("%02x ", buffer[i]);
    printf("\n");
}

// 测试页擦除:擦倒数第 2 页 0x0807F000
static void TestErase(void)
{
    printf(">>>0x0807F000(倒数第2页) Internal Flash Erase Page Begin:\n");
    bool res = InternalFlash_ErasePages(0x0807F000, 1);
    if (res) printf("擦除成功!\n"); else printf("擦除失败!\n");
    TestPrint(0x0807F000);
}

// 测试编程:擦倒数第 4 页后批量写 128 次
static void TestProgram(void)
{
    printf(">>> 0x0807E000(倒数第4页) Internal Flash Program Begin:\n");
    InternalFlash_ErasePages(0x0807E000, 1);   // 先擦一次
    uint8_t inbuffer[16] = {0x00,0x11,0x22,0x33,0x44,0x55,0x66,0x77,
                            0x88,0x99,0xAA,0xBB,0xCC,0xDD,0xEE,0xFF};
    for (uint32_t index = 0; index < 128; index++)   // 擦一次,写多次
    {
        bool res = InternalFlash_Program(0x0807E000 + index * 16, inbuffer, 16);
        (void)res;
    }
    TestPrint(0x0807E000);
}

// 测试写:内部自动"先擦后写"
static void TestWrite(void)
{
    printf(">>>0x0807E800(倒数第3页) Internal Flash Write Begin:\n");
    uint8_t inbuffer[16] = {0x0,0x1,0x2,0x3,0x4,0x5,0x6,0x7,
                            0x8,0x9,0xA,0xB,0xC,0xD,0xE,0xF};
    bool res = InternalFlash_Write(0x0807E800, inbuffer, 16);
    (void)res;
    TestPrint(0x0807E800);
}

main.c 调用:

cpp 复制代码
int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_USART1_UART_Init();

    /* USER CODE BEGIN 2 */
    BSP_USart_Init(&huart1);
    printf("测试开始!\n");
    InternalFlash_AutoTest();   // 自动化测试
    /* USER CODE END 2 */

    while (1) { }
}

4.2 实验现象

① 读测试(读程序起始区,和 Keil 调试 / STM32CubeProgrammer 8-bit 读出一致):

cpp 复制代码
Internal Flash Test Begin:
0x60 0x04 0x00 0x20 0x45 0x01 0x00 0x08 0xc5 0x0e 0x00 0x08 0xbd 0x0e 0x00 0x08

② 页擦除测试(擦完打印全是 0xFF):

复制代码
>>>0x0807F000(倒数第2页) Internal Flash Erase Page Begin:
擦除成功!
>> 0: 0xff 0xff 0xff 0xff ... 0xff   (整页恢复全 1)

③ 编程测试(擦一次、写 128 次,全部按预期写入):

复制代码
>>> 0x0807E000(倒数第4页) Internal Flash Program Begin:
>> 0: 0x00 0x11 0x22 0x33 0x44 0x55 0x66 0x77 0x88 0x99 0xaa 0xbb 0xcc 0xdd 0xee 0xff
>> 16: 0x00 0x11 ... 0xff   (每 16 字节一组,重复 128 次)

④ 写测试(先擦后写,未写区域保持 0xFF):

复制代码
>>>0x08078000 Internal Flash Write Begin:
>>00: 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f
>>16: 0xff 0xff ... 0xff   (页内其余位置都是 0xFF,因为先擦除了)

⑤ 整片擦除测试(演示"擦掉代码"的后果):

复制代码
测试开始!
>>> Mass Erase Begin
(然后没有然后了------程序代码已被擦除,复位也无响应)

选页策略:程序只有 19KB,Flash 有 512KB,所以选高地址尾部页测试(0x0807E000~0x0807F800),避开程序区。不要挑最后一个 Page------可能存在部分数据无法擦除的情况。

五、sct 链接脚本------怎么给 Flash 留数据区

5.1 什么是 .sct

.sct = Scatter-Loading Description File(分散加载描述文件),Keil/ARM 的链接脚本。文本文件,告诉链接器(armlink):

编译出来的代码和数据,分别放在芯片存储器的哪个位置。

文件 类比 作用
.sct 施工图纸 编译前定的规则:代码放哪、变量放哪、各区域多大
.map 完工报告 编译后链接器生成的报告:每个函数/变量实际被放到哪个地址

5.2 默认 .sct 内容

cpp 复制代码
; Keil 自动生成的分散加载文件
LR_IROM1 0x08000000 0x00080000  {  ; 加载域: Flash 起始 0x08000000,大小 512KB
  ER_IROM1 0x08000000 0x00080000  {  ; 执行域: 代码运行地址同烧录地址
    *.o (RESET, +First)  ; 中断向量表放最前面
    *(InRoot$$Sections)  ; 库启动相关代码
    .ANY (+RO)           ; 所有只读代码和常量
    .ANY (+XO)           ; 只执行代码
  }
  RW_IRAM1 0x20000000 0x00010000  {  ; RAM 域: 起始 0x20000000,大小 64KB
    .ANY (+RW +ZI)       ; 所有全局/静态变量
  }
}

三个域的理解:

  • 加载域(LR):程序"存放"的地方------烧录后代码和数据在 Flash 里的位置;
  • 执行域(ER):程序"运行"的地方------CPU 真正执行代码、读常量的地址。执行域地址 = 加载域地址(都是 0x08000000)→ 代码不用搬家,原地执行;
  • RAM 域(RW):全局变量、静态变量运行时存放的位置。

5.3 怎么生成 / 自定义

  1. 自动生成:Keil 魔法棒 → Target 页配置 ROM/RAM 起始地址和大小 → Linker 页勾选 Use Memory Layout from Target Dialog(默认勾选)→ 编译链接自动生成 xxx.sct;
  2. 自定义:Linker 页取消勾选"Use Memory Layout from Target Dialog" → Scatter File 里选择你自己的 .sct → 编译后用 .map 文件验证生效。

意义:

通过改 .sct,可以在 Flash 中准确留出一定范围供数据存储使用(比如划掉最后几个页作为数据区),程序永远不会写进这块区域------这是掉电保存/OTA 数据区规划的基础。


六、Bank 与预取缓冲

6.1 Bank(存储体)

  • 大容量 STM32 可能引入双 Bank 结构(本质内置两个 Flash);
  • 优势:并行擦写(擦 Bank1 同时写 Bank2)、数据隔离(擦 Bank1 不影响 Bank2);
  • F1 系列只有单 Bank------源码里 #if defined(FLASH_BANK2_END) 的分支直接跳过。

6.2 预取缓冲(Prefetch Buffer)

  • 问题:CPU 一条一条从 Flash 取指太慢(Flash 比 CPU 慢,72MHz 还要插 2 个等待周期);
  • 方案:Flash Interface 内集成一个小型 SRAM 做预取缓冲------一次从 Flash 取多条指令,CPU 从缓冲读;
  • 效果:顺序执行时命中率高,大幅提升效率;
  • 局限:遇到**分支跳转(if/while)**只能"猜",猜错就得让 CPU 直接从 Flash 读;
  • 类比:刷抖音------你看当前视频时,抖音已预下载好下一个(顺序);随机点主页某个视频就要现下载(跳转);
  • 代码:HAL_Init() 里 __HAL_FLASH_PREFETCH_BUFFER_ENABLE() → 实质是置 FLASH->ACR |= FLASH_ACR_PRFTBE(ACR 的 bit4)。

七、小结

知识点 一句话
HAL 四函数 Unlock / Lock / Ex_Erase / Program,内部走完九阶段流程
先擦后写 HAL 的 Program 不擦除,擦除要自己调 Erase
2KB 对齐 低 11 位全 0,用 addr & 0x7FF 判断
前闭后开 [start, end) 让页数计算免去 +1/-1
半字打包 字节数组两个一组拼 uint16_t,按小端
小端本质 CPU 认小端,写数据必须和 CPU 建立共识
.sct 链接脚本=施工图纸,可自定义给 Flash 留数据区
预取缓冲 Flash 接口里的小 SRAM,顺序取指加速

面试题

Q1. HAL 库操作 Flash 的四个关键函数是什么?

A:

HAL_FLASH_Unlock()(解锁)、HAL_FLASH_Lock()(锁定)、

HAL_FLASHEx_Erase()(擦除)、HAL_FLASH_Program()(编程)。

读取不需要函数,直接指针解引用。


Q2. HAL_FLASH_Unlock 内部做了什么?和手写版有什么不同?

A:

向 FLASH_KEYR 依次写 0x45670123 和 0xCDEF89AB。

手写版写完就完事;

HAL 版多了两步:

① 先查 LOCK 位,已解锁直接返回;

② 写完回读 LOCK 位验证,没解锁返回 HAL_ERROR。


Q3. 为什么 HAL_FLASH_Program 不包含擦除动作?

A:

Flash 只能 1→0,写前必须擦除。

但 HAL 库不想替用户承担"误擦数据"的风险,把擦除独立成 HAL_FLASHEx_Erase,先擦后写由用户自己组合。


Q4. HAL 库的"字写入"和"双字写入"底层是什么?

A:

F103 硬件只支持半字(16bit)写入。HAL 把字(32bit)拆成 2 次半字写、双字(64bit)拆成 4 次半字写,每次写完等 BSY 并清 PG 位。


Q5. FLASH_EraseInitTypeDef 结构体有哪些字段?

A:

TypeErase(擦除类型:MASSERASE/PAGES)、Banks(存储体,F1 忽略)、PageAddress(页擦除起始地址)、NbPages(擦除页数)。


Q6. 什么是 2KB 对齐?怎么判断一个地址是否对齐?

A:

地址是 2KB(2^11)的整数倍,即低 11 位全 0。

判断:(addr & 0x7FF) == 0。0x7FF 是低 11 位掩码。


Q7. 页擦除的寄存器三连是什么?

A:

① FLASH_CR 置 PER(选页擦除模式)→ ② FLASH_AR 写目标页地址 → ③ FLASH_CR 置 STRT(触发擦除)。


Q8. 为什么要把字节数组拼成半字再写?

A:

HAL_FLASH_Program 只能按半字/字/双字写,不能按字节写。

所以把用户给的字节数组每 2 字节拼成一个 uint16_t 再写入。


Q9. PackHalfWord 为什么是 buf[1]<<8 | buf[0]?

A:

小端字节序------低位放低地址。

buf0 是低字节放低位,buf1 是高字节左移 8 位放高位。

这样拼出的半字写进 Flash 后,按地址从小到大读出来正好是 buf0、buf1 的顺序。


Q10. 大小端和 Flash 写入有什么关系?

A:

数据写进去未来要被 CPU 读,而 Cortex-M3 是小端 CPU,它读多字节数据时默认低位在低地址。

所以保存时就必须按小端组织,这是"单片机逼着用户和自己建立共识"。


Q11. AlignDown2Page 和 AlignUp2Page 分别怎么实现?

A:

向下:addr & ~(2KB-1)(低 11 位清零);

向上:(addr + 2KB - 1) & ~(2KB-1)(先加再对齐)。


Q12. 什么是"前闭后开"区间?为什么用它算页数?

A:

[start, end) 包含 start 不包含 end。

两个地址之间的元素个数 = end - start,不用考虑 +1/-1。

页数 = (end_page - start_page) / 页大小,end_page 指向最后一个有效页的下一页。


Q13. InternalFlash_Write 有什么"破坏性"?

A:

它会把数据涉及的所有页整页擦除再重写。

起始地址所在页的其他数据会丢失,所以数据区要独立规划,不能和代码/其他数据混放。


Q14. 整片擦除会怎样?

A:

把 Main memory 全部置 1,包括烧录的程序代码。

执行后芯片里没有可执行代码,复位无响应,只能重新烧录。

真项目里绝不能乱跑。


Q15. .sct 和 .map 分别是什么?

A:

.sct 是分散加载描述文件(链接脚本),编译前告诉链接器代码/变量放哪、区域多大,是"施工图纸";

.map 是链接器生成的映射报告,显示每个函数/变量实际地址,是"完工报告"。

改 .sct 可以在 Flash 里预留数据区。


相关推荐
西索斯coding2 小时前
qwen3.8-max-0902 和 minimax-m3 改真实仓库差多少?2026 年 50 个缺陷复现实测框架
java·服务器·网络·ai
外收内放2 小时前
ENSP学习记录(2)
网络·智能路由器
m0_738120722 小时前
JWT 安全从入门到实战:六种典型攻击场景解析
前端·网络·python·安全·网络安全
zdd567892 小时前
tcpdump测试
网络
码途行者2 小时前
Linux 服务器 Golang 安装及环境变量配置
linux·运维·服务器
xbzb2 小时前
OpenStack 网络管理(实操):建网络、连路由、浮动 IP 与安全组
网络·安全·openstack·ip·安全组·浮动
叼烟扛炮2 小时前
Linux 学习笔记(四十七):TCP 状态机与 tcpdump 抓包排雷实战
linux·网络·笔记·学习·tcp/ip·tcpdump
车载中间件通信2 小时前
[TC8测试]TCP_FLAGS_INVALID_13_Flag SYN,ACK
网络·网络协议·tcp/ip
爱和冰阔落2 小时前
【Linux网络】UDP 聊天室怎么实现?从在线用户管理、消息广播到双线程收发与线程池
linux·运维·网络·udp