LV010-FLASH操作流程
我这里使用的是STM32CubeMX来配置生成工程,使用的就是HAL库,由于内部Flash不需要我们来做初始化,所以软件不用配置,我们直接调用相应的驱动函数即可。这些可以操作内部Flash的函数声明都存在于 以下几个文件中:
stm32f1xx_hal_flash_ex.h
stm32f1xx_hal_flash.h
stm32f1xx_hal_flash_ex.c
stm32f1xx_hal_flash.c关于Flash的相关资料,ST官方有另外的文档,叫做STM32F10xxx闪存编程手册,英文版在这里:STM32F10xxx Flash memory microcontrollers,这个手册也有中文版,但是只有本地可看:STM32F10xxx闪存编程参考手册
一、对内部Flash的读写过程
1. 解锁
1.1 解锁步骤
由于内部 FLASH 空间主要存储的是应用程序,是非常关键的数据,为了防止误操作修改了这些内容,芯片复位后默认会给控制寄存器 FLASH_CR 上锁,这个时候不允许设置 FLASH 的控制寄存器,从而不能修改 FLASH 中的内容。 我们可以参考STM32F10xxx闪存编程参考手册的2.3.2 解除闪存锁。所以对 FLASH 写入数据前,需要先给它解锁,也就是通过写入特定的序列到 FLASH_KEYR 寄存器可以打开 FPEC 模块(即写入 KEY1 和 KEY2)。解锁的操作步骤如下 :
(1) 往 FPEC 键寄存器 FLASH_KEYR 中写入 KEY1 = 0x45670123
(2) 再往 FPEC 键寄存器 FLASH_KEYR 中写入 KEY2 = 0xCDEF89AB
只有在写保护被解除后,我们才能操作相关寄存器,这 叫Unlock。需要注意的是键值不正确会产生总线错误。
1.2 解锁函数
/**
* @brief Unlock the FLASH control register access
* @retval HAL Status
*/
HAL_StatusTypeDef HAL_FLASH_Unlock(void);2. 上锁
2.1 上锁步骤
当我们操作完成后,我们还可以将内部FLASH上锁,我们向FLASH的CR寄存器的LOCK位写1即可完成上锁。
2.2 上锁函数
/**
* @brief Locks the FLASH control register access
* @retval HAL Status
*/
HAL_StatusTypeDef HAL_FLASH_Lock(void);3. 页擦除
3.1 页擦除步骤
在写入新的数据前,需要先擦除存储区域, STM32 提供了页(扇区)擦除指令和整个 FLASH 擦除 (批量擦除) 的指令,批量擦除指令仅针对主存储区。我们可以看一下STM32F10xxx闪存编程参考手册的2.3.4 闪存擦除一节,这里有一个流程图:

(1)检查FLASH_CR的LOCK是否解锁,如果没有则先解锁
(2)检查 FLASH_SR 寄存器中的“忙碌寄存器位 BSY”,也就是 bit[0],以确认当前未执行任何 Flash 操作;
(3)在 FLASH_CR 寄存器中,将“激活页擦除寄存器位PER ”置 1,也就是将bit[1]置 1 ;
(4)用FLASH_AR寄存器选择要擦除的页(该寄存器表示了要操作的地址,也叫闪存地址寄存器);
(5)将 FLASH_CR 寄存器中的“开始擦除寄存器位 STRT ”置 1,也就是将 bit[6] 置 1 ,然后就会开始擦除;
(6)等待 BSY 位被清零时,表示擦除完成。
(7)读出被擦除的页并做验证。
3.2 页擦除函数
/**
* @brief Perform a mass erase or erase the specified FLASH memory pages
* @note To correctly run this function, the @ref HAL_FLASH_Unlock() function
* must be called before.
* Call the @ref HAL_FLASH_Lock() to disable the flash memory access
* (recommended to protect the FLASH memory against possible unwanted operation)
* @param[in] pEraseInit pointer to an FLASH_EraseInitTypeDef structure that
* contains the configuration information for the erasing.
*
* @param[out] PageError pointer to variable that
* contains the configuration information on faulty page in case of error
* (0xFFFFFFFF means that all the pages have been correctly erased)
*
* @retval HAL_StatusTypeDef HAL Status
*/
HAL_StatusTypeDef HAL_FLASHEx_Erase(FLASH_EraseInitTypeDef *pEraseInit, uint32_t *PageError)本函数包含两个输入参数,分别是擦除flash初始化结构体和返回擦除出错编码,FLASH_EraseInitTypeDef 擦除 flash 初始化结构体主要包含擦除的方式,是扇区擦除还是批量擦除,选择不同电压时实质是选择不同的数据操作位数,并且确定擦除首地址即擦除的扇区个数。函数根据输入参数配置 PSIZE 位,然后擦除扇区,擦除扇区的时候需要等待一段时间,它使用 FLASH_WaitForLastOperation 等待,擦除完成的时候才会退出 HAL_FLASHEx_Erase 函数。
4. 写入数据
4.1 写入步骤
擦除完毕后即可写入数据,写入数据的过程并不是仅仅使用指针向地址赋值,赋值前还还需要配置一系列的寄存器, 我们可以参考STM32F10xxx闪存编程参考手册的2.3.3 主闪存编程一节:

对主闪存编程每次可以写入16位。当FLASH_CR寄存器的PG位为’1’时,在一个闪存地址写入 一个半字将启动一次编程;写入任何非半字的数据,FPEC都会产生总线错误。在编程过程中 (BSY位为’1’),任何读写闪存的操作都会使CPU暂停,直到此次闪存编程结束。
(1)检查FLASH_CR的LOCK是否解锁,如果没有则先解锁。
(2)检查FLASH_SR寄存器的BSY位,以确认没有其他正在进行的编程操作。
(3)设置FLASH_CR寄存器的PG位为 1 在指定的地址写入要编程的半字,也就是每次只能以 16 位(32位处理器下,一个字是32位,半字就是16位,也就是2个字节)的方式写入。
(4)等待BSY位变为 0 。
(5)读出写入的地址并验证数据。
4.2 写入函数
/**
* @brief Program halfword, word or double word at a specified address
* @note The function HAL_FLASH_Unlock() should be called before to unlock the FLASH interface
* The function HAL_FLASH_Lock() should be called after to lock the FLASH interface
*
* @note If an erase and a program operations are requested simultaneously,
* the erase operation is performed before the program one.
*
* @note FLASH should be previously erased before new programmation (only exception to this
* is when 0x0000 is programmed)
*
* @param TypeProgram: Indicate the way to program at a specified address.
* This parameter can be a value of @ref FLASH_Type_Program
* @param Address: Specifies the address to be programmed.
* @param Data: Specifies the data to be programmed
*
* @retval HAL_StatusTypeDef HAL Status
*/
HAL_StatusTypeDef HAL_FLASH_Program(uint32_t TypeProgram, uint32_t Address, uint64_t Data);形参依次设置了数据操作宽度,写入数据地址,写入的数据。在赋值操作后,调用了 FLASH_WaitForLastOperation 函数等待写操作完毕。
5. 读取数据
5.1 读取步骤
读取数据就比较简单了,我们直接定义一个指针,指针指向想要读的地址,就可以直接读取了。
5.2 读取函数
在HAL库或者标准库中是没有提供读内部Flash数据的函数的,我们可以自己封装一个.
- 读取半字数据(16位)
uint16_t internalFLASH_ReadHalfWord(uint32_t faddr)
{
return *(__IO unit16_t* )faddr;
}6. 读写实例
具体的可以看scm-dev: 35-FLASH/01-internal-flash
6.1 相关宏定义
// 用户根据自己的需要设置
#define internalFLASH_SIZE 512 // 所选STM32的FLASH容量大小(单位为K)
#define internalFLASH_WREN 1 // 使能FLASH写入(0,不是能;1,使能)
#define FLASH_WAITETIME 50000 // FLASH等待超时时间
//FLASH起始地址
#define internalFLASH_BASE 0x08000000 //STM32 FLASH的起始地址6.2 读取半字数据
/**
* @brief 读取指定地址的半字(16位) 数据
* @note 32位单片机的话,一个字是32位,半字是16位
* @param faddr 要读取的地址,32位地址
* @retval 返回一个16位数据
*/
uint16_t internalFLASH_ReadHalfWord(uint32_t faddr)
{
return *(__IO uint16_t*)faddr;
}6.3 不检查擦除的写入
/**
* @brief 不检查是否擦除,直接写入半字数据
* @note 注意:数据个数按半字(16位)计算
* @param WriteAddr 起始地址
* @param pBuffer 数据指针
* @param NumToWrite 要写入的数据的半字(16位)数,字节数x2
* @retval
*/
void internalFLASH_Write_NoCheck(uint32_t WriteAddr, uint16_t *pBuffer, uint16_t NumToWrite)
{
uint16_t i;
for (i = 0; i < NumToWrite; i++)
{
HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, WriteAddr, pBuffer[i]);
WriteAddr += 2; // 地址增加2,每次写入16位数据
}
}6.4 写入指定半字数的数据
#if internalFLASH_SIZE < 256
#define internalFLASH_SECTOR_SIZE 1024 // 字节
#else
#define internalFLASH_SECTOR_SIZE 2048
#endif
uint16_t internalFLASH_BUF[internalFLASH_SECTOR_SIZE / 2]; // 最多是2K字节,每个数据是16位,所以大小定义为扇区的一半
/**
* @brief 从指定地址开始写入指定长度的数据
* @note 注意:数据个数按半字(16位)计算
* @param WriteAddr 起始地址(此地址必须为2的倍数!!)
* @param pBuffer 数据指针
* @param NumToWrite 要写入的半字(16位)数(就是要写入的16位数据的个数)
* @retval
*/
void internalFLASH_Write(uint32_t WriteAddr, uint16_t *pBuffer, uint16_t NumToWrite)
{
uint32_t secpos; // 扇区地址
uint16_t secoff; // 扇区内偏移地址(16位字计算)
uint16_t secremain; // 扇区内剩余地址(16位字计算)
uint16_t i;
uint32_t offaddr; //去掉 0X08000000 后的地址,也就是相对于 0X08000000 的偏移地址
if (WriteAddr < internalFLASH_BASE || (WriteAddr >= (internalFLASH_BASE + 1024 * internalFLASH_SIZE)))
return; // 非法地址
HAL_FLASH_Unlock(); // 解锁
offaddr = WriteAddr - internalFLASH_BASE; // 计算实际偏移地址,要写入的地址 - 0x0800 0000
secpos = offaddr / internalFLASH_SECTOR_SIZE; // 计算要写的地址是哪个扇区,也就是页,偏移地址/2048即可。扇区地址 0~255 for STM32F103ZET6
secoff = (offaddr % internalFLASH_SECTOR_SIZE) / 2; // 在扇区内的偏移(2个字节为基本单位.)
secremain = internalFLASH_SECTOR_SIZE / 2 - secoff; // 计算扇区剩余空间大小
if (NumToWrite <= secremain) // 判断整个页中剩下的空间是否足够写入我们要写入的数据
secremain = NumToWrite; // 要写的数据长度不大于该扇区范围的话,我们将剩余长度设置为要写入的长度
while (1)
{
internalFLASH_Read(secpos * internalFLASH_SECTOR_SIZE + internalFLASH_BASE, internalFLASH_BUF, internalFLASH_SECTOR_SIZE / 2); // 读出整个扇区的内容
for (i = 0; i < secremain; i++) //校验数据,判断我们要写的区域中的数据是否都是0xFFFF // 校验数据
{
if (internalFLASH_BUF[secoff + i] != 0XFFFF)
break; // 需要擦除
}
if (i < secremain) // 需要擦除
{
// 这里的擦除操作也可以使用 HAL_FLASHEx_Erase() 函数,它包含了下边的三个步骤
FLASH_PageErase(secpos * internalFLASH_SECTOR_SIZE + internalFLASH_BASE); // 擦除这个扇区
FLASH_WaitForLastOperation(FLASH_WAITETIME); // 等待上次操作完成
CLEAR_BIT(FLASH->CR, FLASH_CR_PER); // 清除CR寄存器的PER位,此操作应该在FLASH_PageErase()中完成!
// 但是HAL库里面并没有做,因为HAL库一般使用HAL_FLASHEx_Erase()来擦除
for (i = 0; i < secremain; i++) /// 复制要写入的数据到缓冲区
{
internalFLASH_BUF[i + secoff] = pBuffer[i]; // 直接从偏移的位置复制数据,保护了其他不需要被覆盖的数据
}
internalFLASH_Write_NoCheck(secpos * internalFLASH_SECTOR_SIZE + internalFLASH_BASE, internalFLASH_BUF, internalFLASH_SECTOR_SIZE / 2); // 写入整个扇区
}
else
{
FLASH_WaitForLastOperation(FLASH_WAITETIME); // 等待上次操作完成
internalFLASH_Write_NoCheck(WriteAddr, pBuffer, secremain); // 写已经擦除了的,直接写入扇区剩余区间.
}
if (NumToWrite == secremain)
break; // 写入结束了
else // 写入未结束
{
secpos++; // 扇区地址增1
secoff = 0; // 偏移位置为0
pBuffer += secremain; // 指针偏移
WriteAddr += secremain * 2; // 写地址偏移(16位数据地址,需要*2)
NumToWrite -= secremain; // 字节(16位)数递减
if (NumToWrite > (internalFLASH_SECTOR_SIZE / 2))
secremain = internalFLASH_SECTOR_SIZE / 2; // 下一个扇区还是写不完
else
secremain = NumToWrite; // 下一个扇区可以写完了
}
}
HAL_FLASH_Lock(); // 上锁
}
#endif6.5 读取指定半字数的数据
/**
* @brief 从指定地址开始读出指定长度的数据
* @note 注意:数据个数按半字(16位)计算
* @param ReadAddr 起始地址
* @param pBuffer 数据指针
* @param NumToWrite 半字(16位)数
* @retval
*/
void internalFLASH_Read(uint32_t ReadAddr, uint16_t *pBuffer, uint16_t NumToRead)
{
uint16_t i;
for (i = 0; i < NumToRead; i++)
{
pBuffer[i] = internalFLASH_ReadHalfWord(ReadAddr); // 读取2个字节.
ReadAddr += 2; // 偏移2个字节.
}
}6.6 测试函数
void internalFlash_Test(void)
{
const uint8_t TEXT_Buffer[] = {"STM32F103 FLASH TEST!!!!"};
uint16_t datatemp[32];
uint32_t FLASH_SAVE_ADDR = 0X08070000;
internalFLASH_Write(FLASH_SAVE_ADDR, (uint16_t *)TEXT_Buffer, sizeof(TEXT_Buffer)/2+((sizeof(TEXT_Buffer)%2)?1:0));
internalFLASH_Read(FLASH_SAVE_ADDR, (uint16_t *)datatemp, sizeof(TEXT_Buffer)/2+((sizeof(TEXT_Buffer)%2)?1:0));
printf("TEXT_Buffer:%d, %d, data:%s\r\n", sizeof(TEXT_Buffer), sizeof(TEXT_Buffer)/2+((sizeof(TEXT_Buffer)%2)?1:0), (char *)datatemp);
}二、内部Flash的读写保护
1. 库函数说明
1.1 选项字节上锁、解锁
/**
* @brief Lock the FLASH Option Control Registers access.
* @retval HAL Status
*/
HAL_StatusTypeDef HAL_FLASH_OB_Lock(void)
/**
* @brief Unlock the FLASH Option Control Registers access.
* @retval HAL Status
*/
HAL_StatusTypeDef HAL_FLASH_OB_Unlock(void);解锁的时候,它对 FLASH_OPTCR 寄存器写入两个解锁参数,上锁的时候,对 FLASH_OPTCR 寄存器的 FLASH_OPTCR_OPTLOCK 位置 1。
1.2 设置选项字节函数
解锁后设置选项字节寄存器可以先初始化 FLASH_OBProgramInitTypeDef 结构体,然后调用 HAL_FLASHEx_OBProgram 完成。
/**
* @brief Program option bytes
* @note The function @ref HAL_FLASH_Unlock() should be called before to unlock the FLASH interface
* The function @ref HAL_FLASH_OB_Unlock() should be called before to unlock the options bytes
* The function @ref HAL_FLASH_OB_Launch() should be called after to force the reload of the options bytes
* (system reset will occur)
*
* @param pOBInit pointer to an FLASH_OBInitStruct structure that
* contains the configuration information for the programming.
*
* @retval HAL_StatusTypeDef HAL Status
*/
HAL_StatusTypeDef HAL_FLASHEx_OBProgram(FLASH_OBProgramInitTypeDef *pOBInit);该函数根据输入选项字节结构体 FLASH_OBProgramInitTypeDef 参数设置寄存器响应的位,特别注意,其注释警告了若 RDPLevel 位配置成 OB_RDP_LEVEL_2 会无法恢复。
1.3 写入选项字节
/**
* @brief Launch the option byte loading.
* @note This function will reset automatically the MCU.
* @retval None
*/
void HAL_FLASH_OB_Launch(void);该函数设置 FLASH_OPTCR_OPTSTRT 位后调用了 FLASH_WaitForLastOperation 函数等待写入完成,并返回写入状态,若操作正常,它会返回 FLASH_COMPLETE。
2. 读写保护测试
这里就没实际操作了,后边有需要再补充吧。
三、等待周期与访问速度
FLASH 的读出速度跟不上 CPU 的主频,所以主频一提上去,取指就必须插等待周期。这一项由 FLASH_ACR 的 LATENCY 位决定,PM0042 给出的对应关系是固定的:
| SYSCLK | 等待周期 | HAL 宏 |
|---|---|---|
| 0 < SYSCLK ≤ 24 MHz | 0 | FLASH_LATENCY_0 |
| 24 < SYSCLK ≤ 48 MHz | 1 | FLASH_LATENCY_1 |
| 48 < SYSCLK ≤ 72 MHz | 2 | FLASH_LATENCY_2 |
CubeMX 在 Clock Configuration 页改主频时会自动把这一项调对,生成到 SystemClock_Config() 里:
// Core/Src/main.c
__HAL_FLASH_SET_LATENCY(FLASH_LATENCY_2); /* HCLK = 72MHz 时必须为 2 */【注意】
这个参数与主频是绑死的,改主频时如果只改了 PLL 分频、没改等待周期,现象是"程序能跑但偶尔取到错指令",属于最难查的一类故障。反过来,等待周期设大了只是白白损失性能,不会出错。
访问通路分两条:I-Code 负责取指令,带 2 × 64 位预取缓冲器;D-Code 负责取数据常量,优先级高于 I-Code 的预取请求。跳转目标落在 8 字节整数倍边界时,预取命中率最高——这也是链接脚本里代码段按 4 或 8 字节对齐的实际收益。
四、读-改-写:改一个字节为什么要擦一页
擦除的最小单位是页(大容量产品 2 KB),编程的最小单位是半字(2 字节),两者差 1024 倍。所以往 FLASH 里改数据不能"就地覆盖",必须走完整的四步:
- 把整页读进 RAM 缓冲区;
- 在 RAM 里改目标字节,其余原样保留;
- 擦除该页;
- 把缓冲区整页写回。
// 驱动的写法(示意)
uint16_t page_buf[2048 / 2]; /* 2KB 页 = 1024 个半字 */
read_page(addr & ~0x7FFU, page_buf, 2048 / 2); /* ① 整页读出 */
page_buf[offset] = new_value; /* ② 只改需要改的那一个 */
erase_page(addr); /* ③ 擦整页 */
write_halfwords(addr & ~0x7FFU, page_buf, 2048/2); /* ④ 整页写回 */配套还有一处很实用的优化:写之前先检查目标区域是否已经是 0xFFFF。如果整段都是擦除态,就可以直接编程、跳过第 ③ 步——擦除的耗时(约 20 ms)和寿命开销都远高于编程(半字约 50 µs)。
for (i = 0; i < secremain; i++)
{
if (page_buf[secoff + i] != 0xFFFF) break; /* 有一个不是 0xFF, 就必须擦 */
}
if (i < secremain)
{
erase_page(page_addr);
/* ... 合并新数据后整页写回 ... */
}代价也很明确:这个 2 KB 的页缓冲区要常驻 RAM。在只有 64 KB SRAM 的 F103ZET6 上不算小,属于"直接移植"方案的实际成本之一。
1. 一次整页擦写的连锁风险
页擦除的 20 ms 里,CPU 取指会被硬件暂停,中断也只能挂起等待。由此引出三个必须提前想清楚的约束:
(1)擦写期间串口照收不误。此时没人读 DR,溢出(ORE)必然置位、新字节被丢弃。所以收帧过程中绝不能擦写 FLASH,落盘要放在"整帧收完、校验通过"之后。
(2)看门狗要按 20 ms 甚至整轮擦写时间放宽。满量程 128 KB 镜像要擦 64 页、编程 65536 次,总耗时接近 5 s,喂狗必须放在擦写循环之间,而不是等整个升级流程结束。
(3)写入可能被复位或掉电打断。片内 FLASH 与独立 EEPROM 有一个本质区别:EEPROM 只要供电正常就一定会写完,而片内 FLASH 的写入会被 CPU 复位或掉电打断,擦了一半的页处于不确定状态,原来存在上面的数据一起丢失。所以"参数区"和"待升级固件区"绝不能放在同一页——一次参数写入的擦除就可能把待升级的固件擦掉。
2. 磨损不是按字节算的
STM32F10xxx 保证每页 1 万次编程/擦除循环,这个数字要按每页理解:
- 频繁就地改写同一页,1 万次就是硬上限;
- 把数据分散到多页轮转(页 n 写满切到 n+1,回收后再擦),整体寿命按页数倍增;
- 写前先判断"是否已擦除"能省掉大量无谓的擦除——省下的正是寿命。
一个典型的反面例子是调试用的 internalFlash_Test():它每次都在同一个地址写一小段数据,而写入缓冲被清零、末尾字节不是 0xFF,于是每次写入都判定"需要擦除",每写一次就擦一次页。测试用无所谓,真做参数存储必须换结构(见 ../01-概述/LV012-EEPROM与Flash 里的磨损均衡做法)。