设计需求
在项目开发中,经常需要保存配置数据至非易失性存储介质中(Flash 或EEPROM中),以便上电重启后获取配置信息。常见的STM32开发中,一般将数据存储在FLASH、备份区域寄存器、外挂EEPROM。由于内部Flash不能像EEPROM一样直接按字节进行读写操作,地址数据只能由1变为0,对固定地址重复写入,必须擦除整页使得地址数据为全FF,才可重新写入数据,通常为了提高内部FLASH的擦除寿命,一般采用内部flash模拟EEPROM方法。
FLASH概述
本文以STM32F103VET6为例,介绍其内部512KByte的flash存储器。该处理器flash单元主要包括三个部分:存储单元、信息块部分、存储器相关控制寄存器。存储单元以page的形式组织,每页2Kbyte,共256页。信息块部分包括system memory(2Kbyte)和option byte(16Byte),system memory单元主要用于存储ST公司固化的bootloader,实现USART1串口更新用户程序。STM32F103VET6内部flash地址分配如下表所示。
需要注意的几点问题:
(1)、擦除和编程FLASH所需的高压是由STM32内部电路产生,且在此过程中内部晶振HSI需开启;
(2)、页写保护和读保护;
(3)、在写操作过程中,读操作将被挂起,待写操作完成后进行读操作。
2、Flash控制器FPEC ---flash programming and erase controller
默认情况下,内部的flash的FPEC和控制寄存器是处于写保护状态,要实现对FLASH的写操作,则首先unlock 存储器,即向寄存器FLASH_KEYR顺序写入两个key值。
KEY1 = 0x45670123
KEY2 = 0xCDEF89AB
FPEC控制器解锁后,通过查询FLASH_SR_BUSY位检查当前工作状态,配合FLASH_CR_PG、FLASH_CR_PER、FLASH_CR_MER分别实现对存储器的编程、页擦除、块擦除。
对于option byte的编程和擦除操作,首先对option byte部分解锁,即向FLASH_OPTKEY写入key值,配合FLASH_CR_OPTPG、FLASH_CR_OPTER实现对option byte进行编程和擦除操作。
3、option byte
选项字节主要用于配置flash的读写保护及看门狗,其分配地址如下:
RDP和WRPx分别用于内部flash的读保护、页写保护设置。系统复位后,the option byte loader---OBL自动加载option byte,并保存option byte 的部分数据至寄存器FLASH_OBR和FLASH_WRPR。RDP默认值为RDPRT key = 0x00A5,即处于非读保护状态。WRPx每位管理着2页的写保护状态。
上图可以看出:WRP0(对应寄存器FLASH_WRPR[7:0])的每位控制2页的写保护状态,共计16页;
WRP1(对应寄存器FLASH_WRPR[15:0])的每位控制2页的写保护状态,共计16页;
WRP2(对应寄存器FLASH_WRPR[23:16])的每位控制2页的写保护状态,共计16页;
WRP3(对应寄存器FLASH_WRPR[30:24])的每位控制2页的写保护状态,共计14页;
WRP3的最高位(对应寄存器FLASH_WRPR[31])控制页62至255页。
默认情况下,寄存器FLASH_WRPR的值均为0,即存储器处于写保护状态。若解除某一页存储器的写保护状态,需要对option byte进行擦除,并进行编程操作,完成后复位系统。
cpp
#define OB_BASE ((uint32_t)0x1FFFF800) /*!< Flash Option Bytes base address */
typedef struct
{
__IO uint16_t RDP;
__IO uint16_t USER;
__IO uint16_t Data0;
__IO uint16_t Data1;
__IO uint16_t WRP0;
__IO uint16_t WRP1;
__IO uint16_t WRP2;
__IO uint16_t WRP3;
} OB_TypeDef;
#define OB ((OB_TypeDef *) OB_BASE)
4、访问存储器时间latency的设置
if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK)
{
Error_Handler();
}
应用1---操作FLASH
使用__attribute__后,编译器自动将将所需的配置信息固化Flash地址中。
cpp
const uint16_t Version __attribute__((at(0x08010000)))= 0x1234;
const uint32_t Device_SN __attribute__((at(0x08010004))) = 0x55678911;
重新变更FLASH中的SN序列号。
cpp
/* USER CODE BEGIN 2 */
FlashWrite(FLASH_TYPEPROGRAM_WORD, 0x08010004, 0x88992266);
/* USER CODE END 2 */
cpp
void FlashWrite(uint32_t TypeProgram, uint32_t Address, uint64_t Data)
{
FLASH_EraseInitTypeDef EraseInitstruct =
{
.TypeErase = FLASH_TYPEERASE_PAGES,
.NbPages = 1,
.PageAddress = Address
};
uint32_t PageError = 0;
HAL_FLASH_Unlock();
HAL_FLASHEx_Erase(&EraseInitstruct, &PageError);
HAL_FLASH_Program(TypeProgram, Address, Data);
HAL_FLASH_Lock();
}
从上面的结果可以看到,对SN序列号的更改导致了version也变成FF了,这是因为对flash地址数据变更时,必须对整页进行擦除。
应用2 --- FLASH模拟EEPROM
先聊聊本质。从应用1来看,要像读写EEPROM一样对FLASH固定地址的数据进行写操作,必须擦除整页的数据,而flash的擦除寿命是有限的,频繁的擦除势必会对MCU折损。flash模拟EEPROM策略就是对外提供虚拟物理地址,表面上看,可对同一地址进行重复写入,实质是在一页中按地址写入,当一页写满时,拷贝此页有效数据至另一页,并对此页进行整页擦除。以2K/页来说,4096/4 = 1024,即写入1024次后才会擦除一页,这样大大降低了FLASH擦除次数。
Flash模拟EEPROM接口(官网示例)
cpp
uint16_t EE_Init(void);
uint16_t EE_ReadVariable(uint16_t VirtAddress, uint16_t* Data);
uint16_t EE_WriteVariable(uint16_t VirtAddress, uint16_t Data);
使用时需要重新定义如下几个条目。NumOfVar是写入uint16型的变量数目,VirtAddVarTab数组是定义的虚拟地址。
cpp
#define PAGE_SIZE (uint16_t)0x800 /* Page size = 2KByte */
/* EEPROM start address in Flash */
#define EEPROM_START_ADDRESS ((uint32_t)0x08010000)
#define NumbOfVar ((uint8_t)0x03)
/* Virtual address defined by the user: 0xFFFF value is prohibited */
uint16_t VirtAddVarTab[NumbOfVar] = {0x01, 0x02, 0x03};
由于官方例子不是使用HAL库函数,需要在EEPROM源文件做一下封装兼容。
cpp
#define FLASH_COMPLETE HAL_OK
#define FLASH_Status HAL_StatusTypeDef
FLASH_Status FLASH_ErasePage(uint32_t PageAddr)
{
uint32_t PageError = 0;
FLASH_EraseInitTypeDef EraseInitstruct =
{
.TypeErase = FLASH_TYPEERASE_PAGES,
.NbPages = 1,
.PageAddress = PAGE0_BASE_ADDRESS
};
EraseInitstruct.PageAddress = PageAddr;
return HAL_FLASHEx_Erase(&EraseInitstruct, &PageError);
}
#define FLASH_ProgramHalfWord(PageAddr, Data) \
HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, (PageAddr), (Data))
读写示例
cpp
HAL_FLASH_Unlock();
EE_Init();
EE_WriteVariable(0x01, 0x3344);
EE_WriteVariable(0x02, 0x5671);
EE_WriteVariable(0x03, 0x8899);
HAL_FLASH_Lock();
cpp
EE_ReadVariable(0x03, &ReadDat);
应用3 ------ 程序加密
待发.....