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)
函数签名:
cppHAL_StatusTypeDef HAL_FLASH_Unlock(void); // 参数:无;返回值:HAL_OK / HAL_ERROR HAL_StatusTypeDef HAL_FLASH_Lock(void); // 参数:无;返回值:HAL_OK / HAL_ERROR源码解析:
cppHAL_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; }读源码的收获:
- HAL 库里把密钥定义成了宏
FLASH_KEY1 = 0x45670123、FLASH_KEY2 = 0xCDEF89AB;- 解锁前会先检查 LOCK 位:如果已经解锁(LOCK=0),直接返回 HAL_OK,不重复写密钥;
- 写完后回读验证:LOCK 位如果还是 1,说明解锁失败,返回 HAL_ERROR------"写完不验"是新手手写版最常见的 bug。
cppHAL_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的参数不是散装变量,而是一个结构体:
cpptypedef struct { uint32_t TypeErase; /*!< 擦除类型:全片擦除或页擦除 */ uint32_t Banks; /*!< 全片擦除时选择要擦除的存储体 */ uint32_t PageAddress; /*!< 页擦除时的起始页地址 */ uint32_t NbPages; /*!< 要擦除的页数 */ } FLASH_EraseInitTypeDef;字段说明:
字段 取值 说明 TypeEraseFLASH_TYPEERASE_MASSERASE片擦除 FLASH_TYPEERASE_PAGES页擦除 BanksFLASH_BANK_1/FLASH_BANK_2F1 单 Bank,此参数可忽略 PageAddress页起始地址 仅页擦除用,必须 2KB 对齐 NbPages页数量 仅页擦除用 1.4 擦除(HAL_FLASHEx_Erase)
函数签名:
cppHAL_StatusTypeDef HAL_FLASHEx_Erase(FLASH_EraseInitTypeDef *pEraseInit, uint32_t *PageError);
pEraseInit:擦除配置(上面的结构体);PageError:输出型参数------出错时返回出错页的地址;0xFFFFFFFF表示所有页都擦除成功;- 返回值:HAL_OK / HAL_ERROR / HAL_TIMEOUT。
片擦除源码核心逻辑:
cppif (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 循环逐页擦):
cppfor(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_HALFWORD0x01U 半字写入(2 字节) FLASH_TYPEPROGRAM_WORD0x02U 字写入(4 字节) FLASH_TYPEPROGRAM_DOUBLEWORD0x03U 双字写入(8 字节) 源码核心逻辑(注意看它是怎么"拆"的):
cppHAL_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; // ② 指针解引用写半字 }读源码的收获(重点):
- F103 硬件只支持半字(16bit)写入------HAL 库里的"字写入(32bit)"其实是拆成 2 次半字写,"双字写入(64bit)"是拆成 4 次半字写;
- 每次写完都调
FLASH_WaitForLastOperation等 BSY 清零;- 编程位 PG 在每次写完后立即清除(而不是全部写完后);
- 底层真正写 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 #endif3.3 InternalFlash_Read ------ 读操作
cppvoid 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 ------ 编程(半字打包 + 小端)
cppstatic 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 ------ 整片擦除
cppbool 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 调用:
cppint 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 读出一致):
cppInternal 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 怎么生成 / 自定义
- 自动生成:Keil 魔法棒 → Target 页配置 ROM/RAM 起始地址和大小 → Linker 页勾选
Use Memory Layout from Target Dialog(默认勾选)→ 编译链接自动生成xxx.sct;- 自定义: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 里预留数据区。
【STM32】 内部 Flash 的 HAL 库函数详解与自定义封装实战
Brilliantwxx2026-11-02 11:21
相关推荐
西索斯coding2 小时前
qwen3.8-max-0902 和 minimax-m3 改真实仓库差多少?2026 年 50 个缺陷复现实测框架外收内放2 小时前
ENSP学习记录(2)m0_738120722 小时前
JWT 安全从入门到实战:六种典型攻击场景解析zdd567892 小时前
tcpdump测试码途行者2 小时前
Linux 服务器 Golang 安装及环境变量配置xbzb2 小时前
OpenStack 网络管理(实操):建网络、连路由、浮动 IP 与安全组叼烟扛炮2 小时前
Linux 学习笔记(四十七):TCP 状态机与 tcpdump 抓包排雷实战车载中间件通信2 小时前
[TC8测试]TCP_FLAGS_INVALID_13_Flag SYN,ACK爱和冰阔落2 小时前
【Linux网络】UDP 聊天室怎么实现?从在线用户管理、消息广播到双线程收发与线程池



