19. STM32内部FLASH
一、STM32内部FLASH简介
不同型号的 STM32F40xx/41xx,其 FLASH 容量也有所不同,最小的只有 128K 字节,最大的则达到了 1024K 字节。STM32F407ZGT6 的 FLASH 容量为1024K 字节,STM32F40xx/41xx 的闪存模块组织如图所示:
STM32F4 的闪存模块由 主存储器、系统存储器、OPT 区域 和 选项字节 等 4 部分组成。
主存储器,该部分用来 存放代码和数据常数(如 const 类型的数据)。分为 12 个扇区,前 4个扇区为 16KB 大小,扇区 4 为 64KB 大小,扇区 5~11 为 128KB 大小,不同容量的 STM32F4,拥有的扇区数不一样,比如 STM32F407ZGT6,拥有 12 个扇区。从表中可以看出,主存储器的起始地址为 0x08000000,B0、B1 都接 GND 的时候,就是从 0x08000000 开始运行代码。
系统存储器,主要用来 存放 STM32F4 的 bootloader 代码,此代码在出厂的时候就固化在STM32F4 里面了,专门用来给主存储器下载代码的。当 B0 接 V3.3,B1 接 GND 的时候,从该存储器启动(即进入串口下载模式)。
OTP 区域,即 一次性可编程区域,总共 528 字节大小,被分成两个部分,前面 512 字节(32 字节为 1 块,分成 16 块),可以用来存储一些用户数据(一次性的,写完一次,永远不可以擦除。),后面 16 字节,用于锁定对应块。
选项字节,用于 配置读保护、BOR 级别、软件/硬件看门狗以及器件处于待机或停止模式下的复位。
闪存存储器接口寄存器,该部分用于控制闪存读写等,是整个闪存模块的控制结构。在执行闪存写操作时,任何对闪存的读操作都会锁住总线,在写操作完成后读操作才能正确地进行。既在进行写或擦除操作时,不能进行代码或数据的读取操作。
二、闪存的读取
STM32F4 可以通过内部的 I-Code 指令总线或 D-Code 数据总线访问内置闪存模块。为了准确读取 Flash 数据,必须根据 CPU 时钟(HCLK)频率和器件电源电压在 Flash 存取控制寄存器(FLASH_ACR)中正确地设置等待周期数(LATENCY)。当电源电压低于 2.1V 时,必须关闭预取缓冲器。Flash等待周期与 CPU 时钟频率之间的对应关系,如下表所示:
等待周期通过 FLASH_ACR 寄存器的 LATENCY[2:0] 三个位设置。系统复位后,CPU 时钟频率为内部 16M RC 振荡器(HIS),LATENCY 默认是 0,即 1 个等待周期。供电电压,我们一般是 3.3V,所以,在我们设置 168 MHz 频率作为 CPU 时钟之前,必须先设置 LATENCY 为 5,否则 FLASH 读写可能出错,导致死机。
正常工作时(168 MHz),虽然 FLASH 需要 6 个 CPU 等待周期,但是由于 STM32F4 具有自适应实时存储器加速器(ART Accelerator),通过指令缓存存储器,预取指令,实现相当于 0FLASH 等待的运行速度。关于自适应实时存储器加速器的详细介绍,请大家参考《STM32F4xx参考手册_V4(中文版).pdf》3.4.2 节。
STM23F4 的 FLASH 读取是很简单的。例如,我们要从地址 address,读取一个字(字节为 8 位,半字为 16 位,字为 32 位),可以通过如下的语句读取:
data = *(volatile uint32_t *)address;
将 address 强制转换为 volatile uint32_t 指针,然后取该指针所指向的地址的值,即得到了 address 地址的值。类似的,将上面的 volatile uint32_t 改为 volatile uint16_t,即可读取指定地址的一个半字。
三、闪存的编程和擦除
执行任何 Flash 编程操作(擦除或编程)时,CPU 时钟频率(HCLK)不能低于 1 MHz。如果在 Flash 操作期间发生器件复位,无法保证 Flash 中的内容。
在对 STM32F4 的 Flash 执行写入或擦除操作期间,任何读取 Flash 的尝试都会导致总线阻塞。只有在完成编程操作后,才能正确处理读操作。这意味着,写/擦除操作进行期间不能从 Flash 中执行代码或数据获取操作。
STM32F4 复位后,FLASH 编程操作是被保护的,不能写入 FLASH_CR 寄存器;通过写入特定的序列(0x45670123 和 0xCDEF89AB)到 FLASH_KEYR 寄存器才可解除写保护,只有在写保护被解除后,我们才能操作相关寄存器。
FLASH_CR 的解锁序列为:
- 写 0x45670123 到 FLASH_KEYR
- 写 0xCDEF89AB 到 FLASH_KEYR
通过这两个步骤,即可解锁 FLASH_CR,如果写入错误,那么 FLASH_CR 将被锁定,直到下次复位后才可以再次解锁。
STM32F4 闪存的编程位数可以通过 FLASH_CR 的 PSIZE 字段配置,PSIZE 的设置必须和电源电压匹配,见下表:
由于我们开发板用的电压是 3.3V,所以 PSIZE 必须设置为 10,即 32 位并行位数。擦除或者编程,都必须以 32 位为基础进行。
STM32F4 的 FLASH 在写入数据的时候,也必须要求其写入地址的 FLASH 是被擦除了的(也就是其值必须是 0xFFFFFFFF),无法写入。STM32F4 的标准写数据编程步骤如下所示:
- 检查 FLASH_CR 的 LOCK 是否解锁,如果没有则先解锁。
- 检查 FLASH_SR 寄存器的 BSY 位,以确认没有其他正在进行的编程操作。
- 设置 FLASH_CR 寄存器的 PG 位为 "1"。
- 在指定的地址写入数据(一次写入 32 字节,不能超过 32 字节)。
- 等待 BSY 位变为 "0"。
- 读出写入地址并验证数据。
在 STM32 的 FLASH 编程的时候,要先判断缩写地址是否被擦除了。STM32 的闪存擦除分为两种:页擦除 和 整片擦除。页擦除 过程如下所示:
- 检查 FLASH_CR 的 LOCK 是否解锁,如果没有则先解锁。
- 检查 FLASH_SR 寄存器中的 BSY 位,确保当前未执行任何 FLASH 操作。
- 在 FLASH_CR 寄存器中,将 SER 位置 1,并设置 SNB=0(只有 1 个扇区,扇区 0)。
- 将 FLASH_CR 寄存器中的 START 位置 1,触发擦除操作。
- 等待 BSY 位清零。
四、STM32内部FLASH常用寄存器
4.1、Flash访问控制寄存器
LATENCY[2:0] 这三个位,这三个位,必须根据我们 MCU 的工作电压和频率来进行正确的设置,否则,可能会死机。用于控制 FLASH 读延迟,必须根据我们 MCU 内核的工作电压和频率,来进行正确的设置,否则,可能死机。其他 DCEN、ICEN 和 PRFTEN 这三个位也比较重要,为了达到最佳性能,这三个位我们一般都设置为 1 即可。
4.2、FLASH密钥寄存器
该寄存器主要用来解锁 FLASH_CR,必须在该寄存器写入特定的序列(KEY1:0x45670123 和 KEY2:0xCDEF89AB)解锁后,才能对 FLASH_CR 寄存器进行写操作。
4.3、Flash状态寄存器
4.4、Flash控制寄存器
位 31 LOCK 位,该位用于指示 FLASH_CR 寄存器是否被锁住,该位在检测到正确的解锁序列后,硬件将其清零。在一次不成功的解锁操作后,在下次系统复位之前,该位将不再改变。
位 16 STRT 位,该位用于开始一次擦除操作。在该为写入 1,将执行一次擦除操作。
PSIZE[9:8]位,用于设置编程宽度,我们一般设置 PSIZE = 2 即可(32 位)。
SNB[6:3] 位,这 4 个位用于选择要擦除的扇区编号,取值范围为 0 ~ 1。
位 1 SER 位,该位用于选择扇区擦除操作,在扇区擦除的时候,需要将该位置 1。
位 0 PG 位,该位用于选择编程操作,在往 FLASH 写数据的时候,该位需要置 1。
五、FLASH驱动步骤
FLASH 在 HAL 库中的驱动代码在 stm32f4xx_hal_flash.c 和 stm32f4xx_hal_flash_ex.c 文件(及其头文件)中。
5.1、解锁闪存控制寄存器访问的函数
HAL_FLASH_Unlock() 用于解锁闪存控制寄存器的访问,在对 FLASH 进行写操作前必须先解锁,解锁操作也就是必须在 FLASH_KEYR 寄存器写入特定的序列(KEY1 和 KEY2)。该函数声明如下:
HAL_StatusTypeDef HAL_FLASH_Unlock(void);
该函数的返回值是 HAL_StatusTypeDef 枚举类型的值,有 4 个,分别是 HAL_OK 表示 成功,HAL_ERROR 表示 错误,HAL_BUSY 表示 忙碌,HAL_TIMEOUT 表示 超时。
typedef enum
{
HAL_OK = 0x00U, // 成功
HAL_ERROR = 0x01U, // 错误
HAL_BUSY = 0x02U, // 忙碌
HAL_TIMEOUT = 0x03U // 超时
} HAL_StatusTypeDef;
5.2、锁定闪存控制寄存器访问的函数
HAL_FLASH_Lock() 函数用于锁定闪存控制寄存器的访问。该函数声明如下:
HAL_StatusTypeDef HAL_FLASH_Lock(void);
该函数的返回值是 HAL_StatusTypeDef 枚举类型的值,有 4 个,分别是 HAL_OK 表示 成功,HAL_ERROR 表示 错误,HAL_BUSY 表示 忙碌,HAL_TIMEOUT 表示 超时。
5.3、闪存写操作函数
HAL_FLASH_Program() 函数用于 FLASH 的写入。该函数声明如下:
HAL_StatusTypeDef HAL_FLASH_Program(uint32_t TypeProgram, uint32_t Address, uint64_t Data);
形参 TypeProgram 是用来区分 要写入的数据类型,
#define FLASH_TYPEPROGRAM_BYTE 0x00000000U /*!< Program byte (8-bit) at a specified address */
#define FLASH_TYPEPROGRAM_HALFWORD 0x00000001U /*!< Program a half-word (16-bit) at a specified address */
#define FLASH_TYPEPROGRAM_WORD 0x00000002U /*!< Program a word (32-bit) at a specified address */
#define FLASH_TYPEPROGRAM_DOUBLEWORD 0x00000003U /*!< Program a double word (64-bit) at a specified address */
形参 Address 用来设置 要写入数据的 FLASH 地址。
形参 Data 是 要写入的数据类型。
该函数的返回值是 HAL_StatusTypeDef 枚举类型的值,有 4 个,分别是 HAL_OK 表示 成功,HAL_ERROR 表示 错误,HAL_BUSY 表示 忙碌,HAL_TIMEOUT 表示 超时。
5.4、闪存擦除函数
HAL_FLASHEx_Erase() 函数用于大量擦除或擦除指定的闪存扇区。
HAL_StatusTypeDef HAL_FLASHEx_Erase(FLASH_EraseInitTypeDef *pEraseInit, uint32_t *SectorError)
形参 pEraseInit 是 FLASH_EraseInitTypeDef 结构体类型指针变量。
typedef struct
{
uint32_t TypeErase; // 擦除类型
uint32_t Banks; // 擦除的Bank编号
uint32_t Sector; // 擦除的扇区编号
uint32_t NbSectors; // 擦除的扇区数量
uint32_t VoltageRange; // 电压等级
} FLASH_EraseInitTypeDef;
形参 SectorError 是 uint32_t 类型 指针变量,存放错误码,0xFFFFFFFF 值表示扇区已被正确擦除,其它值表示擦除过程中的错误扇区。
该函数的返回值是 HAL_StatusTypeDef 枚举类型的值,有 4 个,分别是 HAL_OK 表示 成功,HAL_ERROR 表示 错误,HAL_BUSY 表示 忙碌,HAL_TIMEOUT 表示 超时。
5.5、等待FLASH操作完成函数
FLASH_WaitForLastOperation() 函数用于等待 FLASH 操作完成。
HAL_StatusTypeDef FLASH_WaitForLastOperation(uint32_t Timeout)
形参 Timeout 是 FLASH 操作 超时时间。
该函数的返回值是 HAL_StatusTypeDef 枚举类型的值,有 4 个,分别是 HAL_OK 表示 成功,HAL_ERROR 表示 错误,HAL_BUSY 表示 忙碌,HAL_TIMEOUT 表示 超时。
六、程序源码
FLASH 读取一个字函数:
/**
* @brief 内部FLASH读取一个字节函数
*
* @param address 待读取数据的地址
* @return uint32_t 读取的数据
*/
uint32_t FLASH_ReadOneWord(uint32_t address)
{
return *(volatile uint32_t *)address;
}
FLASH 读取数据函数:
/**
* @brief 从指定地址开始读出指定长度的数据
*
* @param address 起始地址
* @param data 存放读取数据缓冲区的指针
* @param length 要读取的字(32位)数,即4个字节的整数倍
*/
void FLASH_ReadData(uint32_t address, uint32_t *data, uint32_t length)
{
for (uint32_t i = 0; i < length; i++)
{
data[i] = FLASH_ReadOneWord(address); // 读取一个字,四个字节
address += 4; // 地址偏移4个字节
}
}
FLASH
#define ADDRESS_FLASH_SECTOR_0 ((uint32_t )0x08000000) // 扇区 0 起始地址,16 Kbyte
#define ADDRESS_FLASH_SECTOR_1 ((uint32_t )0x08004000) // 扇区 1 起始地址,16 Kbyte
#define ADDRESS_FLASH_SECTOR_2 ((uint32_t )0x08008000) // 扇区 2 起始地址,16 Kbyte
#define ADDRESS_FLASH_SECTOR_3 ((uint32_t )0x0800C000) // 扇区 3 起始地址,16 Kbyte
#define ADDRESS_FLASH_SECTOR_4 ((uint32_t )0x08010000) // 扇区 4 起始地址,64 Kbyte
#define ADDRESS_FLASH_SECTOR_5 ((uint32_t )0x08020000) // 扇区 5 起始地址,128 Kbyte
#define ADDRESS_FLASH_SECTOR_6 ((uint32_t )0x08040000) // 扇区 6 起始地址,128 Kbyte
#define ADDRESS_FLASH_SECTOR_7 ((uint32_t )0x08060000) // 扇区 7 起始地址,128 Kbyte
#define ADDRESS_FLASH_SECTOR_8 ((uint32_t )0x08080000) // 扇区 8 起始地址,128 Kbyte
#define ADDRESS_FLASH_SECTOR_9 ((uint32_t )0x080A0000) // 扇区 9 起始地址,128 Kbyte
#define ADDRESS_FLASH_SECTOR_10 ((uint32_t )0x080C0000) // 扇区 10 起始地址,128 Kbyte
#define ADDRESS_FLASH_SECTOR_11 ((uint32_t )0x080E0000) // 扇区 11 起始地址,128 Kbyte
/**
* @brief 获取某个地址所在的FLASH扇区
*
* @param address 内存地址
* @return uint8_t address所在的扇区
*/
uint8_t FLASH_GetFlashSector(uint32_t address)
{
if (address < ADDRESS_FLASH_SECTOR_1) return FLASH_SECTOR_0;
else if (address < ADDRESS_FLASH_SECTOR_2) return FLASH_SECTOR_1;
else if (address < ADDRESS_FLASH_SECTOR_3) return FLASH_SECTOR_2;
else if (address < ADDRESS_FLASH_SECTOR_4) return FLASH_SECTOR_3;
else if (address < ADDRESS_FLASH_SECTOR_5) return FLASH_SECTOR_4;
else if (address < ADDRESS_FLASH_SECTOR_6) return FLASH_SECTOR_5;
else if (address < ADDRESS_FLASH_SECTOR_7) return FLASH_SECTOR_6;
else if (address < ADDRESS_FLASH_SECTOR_8) return FLASH_SECTOR_7;
else if (address < ADDRESS_FLASH_SECTOR_9) return FLASH_SECTOR_8;
else if (address < ADDRESS_FLASH_SECTOR_10) return FLASH_SECTOR_9;
else if (address < ADDRESS_FLASH_SECTOR_11) return FLASH_SECTOR_10;
return FLASH_SECTOR_11;
}
FLASH 写数据函数:
#define FLASH_WAITE_TIME 50000 // FLASH 等待超时时间
/**
* @brief 在FLASH指定位置,写入指定长度的数据(自动擦除)函数
*
* @param address 起始地址(此地址必须为4的倍数,否则写入出错!)
* @param data 数据指针
* @param length 要写入的字(32位)的个数
*
* @note 因为STM32F4的扇区实在太大,没办法本地保存扇区数据,所以本函数写地址如果非0XFF,
* 那么会先擦除整个扇区且不保存扇区数据.所以写非0XFF的地址,将导致整个扇区数据丢失。
* 建议写之前确保扇区里没有重要数据,最好是整个扇区先擦除了,然后慢慢往后写。
* 该函数对OTP区域也有效!可以用来写OTP区!
* OTP区域地址范围:0X1FFF7800~0X1FFF7A0F(注意:最后16字节,用于OTP数据块锁定!)
*/
void FLASH_WriteData(uint32_t address, uint32_t *data, uint32_t length)
{
FLASH_EraseInitTypeDef FLASH_EraseInitStruct = {0};
HAL_StatusTypeDef flash_status = HAL_OK;
uint32_t start_address = 0;
uint32_t end_address = 0;
uint32_t sectorerror=0;
// 写入地址小于或大于FLASH地址,或不是4的整数倍,非法
if (address < FLASH_BASE || (address % 4) || (address > FLASH_END))
{
return;
}
HAL_FLASH_Unlock(); // 解锁
FLASH->ACR &= ~(1 << 10); // FLASH擦除期间,必须禁止数据缓存
start_address = address; // 写入的起始地址
end_address = address + length * 4; // 写入的结束地址
if (start_address < 0x1FFF0000) // 只有主存储区,才需要执行擦除操作
{
while (start_address < end_address) // 对非0xFFFFFFFF的地方,先擦除
{
if (FLASH_ReadOneWord(start_address) != 0xFFFFFFFF) // 有非0xFFFFFFFF的地方,要擦除这个扇区
{
FLASH_EraseInitStruct.TypeErase = FLASH_TYPEERASE_SECTORS; // 擦除类型,扇区擦除
FLASH_EraseInitStruct.Sector = FLASH_GetFlashSector(start_address); // 要擦除的扇区
FLASH_EraseInitStruct.NbSectors = 1; // 一次只擦除一个扇区
FLASH_EraseInitStruct.VoltageRange = FLASH_VOLTAGE_RANGE_3; // 电压范围,VCC=2.7~3.6V之间!
if(HAL_FLASHEx_Erase(&FLASH_EraseInitStruct, §orerror) != HAL_OK)
{
break; // 发生错误了退出
}
}
else
{
start_address += 4; // 地址偏移4个字节
}
FLASH_WaitForLastOperation(FLASH_WAITE_TIME); // 等待上次操作完成
}
}
flash_status = FLASH_WaitForLastOperation(FLASH_WAITE_TIME); // 等待上次操作完成
if (flash_status == HAL_OK)
{
while (address < end_address) // 写数据
{
if (HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, address, *data) != HAL_OK) // 写入数据
{
break; // 写入异常则退出
}
address += 4; // 地址偏移4个字节
data++; // 数据指针后移
}
}
FLASH->ACR |= 1 << 10; // FLASH擦除结束,开启数据fetch
HAL_FLASH_Lock(); // 上锁
}
main() 函数:
int main(void)
{
uint8_t data[] = "Hello world!";
uint8_t length = sizeof(data) / 4 + ((sizeof(data) ? 1 : 0));
uint8_t temp[30] = {0};
HAL_Init();
System_Clock_Init(8, 336, 2, 7);
HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4); // 设置中断优先级分组
UART_Init(&g_usart1_handle, USART1, 115200);
FLASH_WriteData(0x08010000, (uint32_t *)data, length);
FLASH_ReadData(0x08010000, (uint32_t *)temp, length);
printf("data: %s\r\n", temp);
while (1)
{
}
return 0;
}