目录
前言
1. 内部FLASH简介
2. 内部FLASH写入过程
3. 内部FLASH库函数
4. FLASH的读写保护及解除
5. FLASH相关寄存器
6. 实验程序
6.1 main.c
6.2 STMFlash.c
6.3 STMFlash.h
STM32F4本身并没有自带EEPROM,但是STM32F4具有IAP功能,也就是在应用编程功能。本节将IAP在应用编程功能的FLASH当成EEPROM来使用。
STM32编程方式:
①:在线编程(ICP,In-Circuit Programming)
通过JTAG/SWD协议或者系统加载程序(Bootloader)下载用户应用程序到微控制器中。
②:在程序中编程(IAP,In Application Programming)
通过任何一种通信接口(如IO端口,USB,CAN,UART,I2C,SPI等)下载程序或者应用数据到存储器中。也就是说,STM32允许用户在应用程序中重新烧写闪存存储器中的内容。然而,IAP需要至少有一部分程序已经使用ICP方式烧到闪存存储器中(Bootloader)
在STM32芯片内部有一个FLASH存储器,他主要用于存储代码,我们在电脑上编写好应用程序后,使用下载器把编译后的代码烧录到该内部FLASH中,由于FLASH存储器的内容在掉电后不会丢失,芯片重新上电复位后,内核可从内部FLASH中加载代码并运行。
注:事实上,我们将代码下载到开发板的MCU中,实际上都是下载到芯片内部的FLASH存储器中。
除了使用外部的工具(如下载器)读写内部FLASH外,STM32F4芯片在运行的时候,也能对自身的内部FLASH进行读写,因此,若内部FLASH存储了应用程序后还有剩余空间,我们可以把它像外部SPI-FLASH那样利用起来,存储一些程序运行时产生的需要掉电保存的数据。
由于访问内部FLASH比外部SPI-FLASH的速度快的多,所以在紧急状态下常常会使用内部FLASH存储关键记录;为了防止应用程序被抄袭,有的应用会禁止读写内部FLASH中的内容,或者在第一次运行时计算机机密信息并记录到某些区域,然后删除自身的部分加密代码,这些都涉及到内部FLASH的操作。
STM32内部FLASH包括:主存储器、系统存储器、OTP区域以及选项字节区域
其中系统存储器是STM32开发板出厂之前就已经使用的一块区域,用户是无法访问系统存储区的,主要是做串口下载程序的支持,以及USB、CAN等ISP烧录功能。(系统存储器主要是用来存放STM32F4的bootloader代码,此代码是出厂的时候就固化在STM32F4里面的,专门来给主存储器下载代码的)
OTP区域,即一次性可编程区域,共528字节,被分成两部分,前面512个字节(32字节为1块,分成16块),可以用来存储一些用户数据(一次性的,写完一次,永远不可以擦除!!!)后面16个字节,用于锁定对应块。
选项字节区域是用来配置FLASH的读写保护、待机/停机、软件/硬件看门狗功能。可以通过修改FLASH的选项控制寄存器进行修改。
主存储器:
像我们在介绍一款芯片的时候,提到的256K FLASH或者512K FLASH,其中256K和512K指的都是这个主存储器的大小。主存储器用来存放代码和数据常量(如const类型的数据)
主存储器分256页,每页大小2KB,共512KB。这个分页的概念,实质上就是FLASH存储器的扇区,与其他FLASH一样,在写入数据前,要先按照页,也就是扇区进行擦除。
STM32F4的主存储器块分为 4个 16KB 扇区、1个 64KB 扇区和 7个 128KB 扇区
型号STM32F4ZG:
其中型号中的字母G就表示FLASH的大小;
4表示16KB; 6表示32KB; 8表示64KB; B表示128KB;
C表示256KB; E表示512KB; F表示768KB; G表示1024KB;
闪存存储器接口寄存器,该部分用于控制闪存读写等,是整个闪存模块的控制机构。
在执行闪存写操作时,任何对闪存的读操作都是锁住总线,在写操作完成后,读操作才能正确进行;也就是说在进行写或者擦除操作时,不能进行数据或者代码的读取操作。
1. 解锁
由于内部 FLASH 空间主要存储的是应用程序,是非常关键的数据,为了防止误操作修改了这些内容,芯片复位后默认会给 FLASH 上锁,这个时候不再允许设置 FLASH 的控制寄存器,同时也不能修改 FLASH 中的内容。
所以对 FLASH 写入程序之前,需要先对其进行解锁操作。
2. 擦除扇区
在写入新的数据前,需要先擦除存储区域,STM32提供了扇区擦除指令和整个FLASH擦除(批量擦除)的指令,批量擦除指令仅针对主存储区。
扇区擦除的过程:
3. 写入数据
擦除完毕后即可写入数据,写入数据的过程并不是仅仅使用指针指向地址赋值,赋值前还需要配置一系列的寄存器
查看工程的空间分布:
由于内部FLASH本身存储有程序数据,若不是有意删除某段程序代码,一般不应该修改程序空间的内容,所以在使用内部FLASH存储其他数据前需要了解哪一些空间已经写入了程序代码,存储了程序代码的扇区都不应做任何的修改。通过查询应用程序编译时产生的 “*.map” 后缀文件,可以了解程序存储到了哪些区域。
1. FLASH解锁、上锁函数
解锁的时候,他对FLASH_KEYR寄存器写入两个解锁参数。上锁的时候,对FLASH_CR寄存器的FLASH_CR_LOCK位置1。
#define FLASH KEY1 ((uint32_t)0x45670123)
#define FLASH KEY2 ((uint32_t)0xCDEF89AB)
void FLASH Unlock(void)
{
if((FLASH->CR & FLASH_CR_LOCK)!=RESET)
{
FLASH->KEYR = FLASH KEY1;
FLASH->KEYR = FLASH KEY2;
}
}
void FLASH_Lock(void)
{
FLASH->CR |= FLASH_CR_LOCK;
}
2. 擦除函数
解除后擦除扇区时可调用FLASH_EraseSector完成;
该函数包含以Page_Address输入参数获得要擦除的地址。内部根据该参数配置FLASH_AR地址,然后擦除扇区,擦除扇区的时候需要等待一段时间,它使用FLASH_WaitForLastOperation等待,擦除完成的时候才会退出FLASH_EraseSector函数。
3. 写入数据
对内部FLASH写入数据不像对SDRAM操作那样直接指针操作就可以完成了,还要设置一系列的寄存器,利用FLASH_ProgramWord和FLASH_ProgramHalfWord函数可按字、半字节单位写入数据。
STM32F4内部FLASH库函数:
1. 锁定解锁函数
void FLASH_Unlock(void); //解锁函数 对FLASH操作前必须先进行解锁
void FLASH_Lock(void); //锁定FLASH
2. 写操作函数
FLASH_Status FLASH_ProgramDoubleWord(uint32_t Address,uint64_t Data); //写入双字函数
FLASH_Status FLASH_ProgramWord(uint32_t Address,uint32_t Data); //写入字函数
FLASH_Status FLASH_ProgramHalfWord(uint32_t Address,uint16_t Data); //写入半字函数
FLASH_Status FLASH_ProgramByte(uint32_t Address,uint8_t Data); //写入字节函数
3. 擦除函数
FLASH_Status FLASH_EraseSector(uint32_t FLASH_Sector,uint8_t VoltageRange); //擦除某个扇区函数
FLASH_Status FLASH_EraseAllSectors(uint8_t VoltageRange); //擦除整个扇区函数
FLASH_Status FLASH_EraseAllBank1Sectors(uint8_t VoltageRange); //STM32F4将所有的Sector分成两个Bank,所以定义两个函数来擦除两个Bank下的Sector
FLASH_Status FLASH_EraseAllBank2Sector(uint8_t VoltageRange); //擦除Bank下的Sector
函数第一个参数的取值范围为 FLASH_Sector_0~FLASH_Sector_11 (这些都是头文件中宏定义好的)
函数第二个参数是电压范围,STM32F4的电压范围是3.3V,所以选择VoltageRange_3即可
4. 获取FLASH状态
FLASH_Status FLASH_GetStatus(void); //获取FLASH状态函数
FLASH_Status FLASH_GetStatus(void); //获取FLASH状态 // 返回值通过枚举定义 typedef enum { FLASH_BUSY=1, //操作忙 FLASH_ERROR_RD, //读保护错误 FLASH_ERROR_PGS, //编程顺序错误 FLASH_ERROR_PGP, //编程并行位数错误 FLASH_ERROR_PGA, //编程对齐错误 FLASH_ERROR_WRP, //写保护错误 FLASH_ERROR_PROGRAM, //编程错误 FLASH_ERROR_OPERATION, //操作错误 FLASH_COMPLETE //操作结束 }FLASH_Status;
5. 等待操作完成函数
在执行闪存写操作时,任何对闪存的读操作都会锁住总线,在写操作完成后读操作才能正确的进行;因此在进行写操作或者擦除命令时,不能同时进行数据的读取
FLASH_Status FLASH_WaitForLastOperation(void); //返回FLASH的状态
在每次操作之前,都要等待上一次操作完成才能开始
6. 读FLASH特定地址数据函数
从指定地址读取一个字的函数
u32 STMFLASH_ReadWord(u32 faddress) //参数输入地址 { return *(vu32*)faddress; //定义一个vu32的指针指向该地址,解引用得到该地址上的值 //返回解引用得到的该地址的值 }
选项字节和读写保护:
在实际发布的产品中,STM32芯片的内部FLASH存储了控制程序,如果不做任何保护措施的话,可以使用下载器直接把内部FLASH的内容读取回来,得到bin或hex文件格式的代码拷贝,别有用心的厂家可能会利用该代码制造山寨产品、为此,STM32芯片提供了多种方式保护内部FLASH的程序不被非法读取,但是在默认状态下该保护功能是不开启的,若要开启该功能,需要改写内部FLASH选项字节(Option Bytes)中的配置。
修改选项字节的过程:
修改选项字节的内容可修改各种配置,但是,当应用程序运行时,无法直接通过选项字节改写他们的内容。
要改写其内容必须设置寄存器FLASH_OPTCR及FLASH_OPTCR1中对应数据位。
默认情况下,FLASH_OPTCR寄存器中的第0位OPTLOCK的值为1,它表示选项字节被上锁,需要解锁后才能进行修改,当寄存器的值设置完成后,对FLASH_OPTCR寄存器中的第1位OPTSTRT位设置为1,硬件就会擦除选项字节扇区的内容,并把FLASH_OPTCR/1寄存器中包含的值写入到选项字节。
修改选项字节的配置步骤:
闪存的读取:
STM32F4可以通过内部的 I-Code指令总线 或 D-Code数据总线 访问内置闪存模块;
数据的读写可以通过 D-Code数据总线 来访问内部闪存模块。为了准确的读取 Flash 数据,必须根据CPU时钟(HCLK)频率和器件电源电压在 Flash存取控制寄存器FLASH_ACR 中正确的设置等待周期数LATENCY。当电源电压低于2.1V时,必须关闭预取缓冲器。
等待周期WS通过FLASH存取控制寄存器FLASH_ACR寄存器LATENCY[2:0]三个位设置。系统复位后,CPU时钟频率为内部16M RC振荡器,LATENCY默认是0,即一个等待周期。供电电压一般是3.3V,所以设置168MHz频率作为CPU时钟之前,必须先设置LATENCY为5.(根据上表中对应的关系进行设置),否则FLASH读写可能出错,导致死机。
根据上表中的对应周期,正常工作168MHz时,对应的是6个CPU周期,但是只需要对FLASH存取状态寄存器的低三位写入101,也就是5.
这是因为STM32F4具有自适应实时存储器加速器(ART Accelerator),通过指令缓存存储器,预取指令,实现相当于0 FLASH等待的运行速度。
STM32F4的FLASH读取比较简单。例如,要从地址Address上读取一个字(字节为8位,半字为16位,字为32位),可以通过如下的语句读取:
Data=*(vu32*)Address
其中将地址Address强制转换成vu32的指针,然后解引用得到该指针指向地址的值。
闪存的编程和擦除:
执行任何Flash编程操作(擦除或编程)时,CPU时钟频率HCLK不能低于1MHz。如果在FLASH操作期间发生器件复位,无法保证Flash中的内容。
在对STM32F4的Flash执行写入或擦除操作期间,任何读取Flash的尝试都会导致总线阻塞。也就是说,STM32F4内部的FLASH进行写入或者擦除操作时,是不能进行数据的读取的。
FLASH_CR的解锁序列:
1. 写0x45670123到FLASH_KEYR密钥寄存器
2. 写0xCDEF89AB到FLASH_KEYR密钥寄存器
通过这两个步骤,即可解锁FLASH_CR,如果写入错误,那么FLASH_CR将被锁定,直到下次复位后才可以再次解锁。
STM32F4闪存编程位数:
闪存编程位数通过FLASH_CR的PSIZE字段配置,PSIZE的设置必须和电源电压匹配。
STM32F4开发板使用的是3.3V电压,所以PSIZE必须设置为10,也就是并行位数x32位。擦除或者编程都必须以32位为基础进行。
注:STM32F4的FLASH在编程的时候,必须要求其写入地址的FLASH是被擦除了的(STM32F4内部的FLASH只能写入1或者0,擦除以后的32位是0xFFFF FFFF,也就是说只能在擦除以后的1的基础之上改为0,否则就要重新进入擦除操作)
STM32F4的标准编程步骤:
1. 检查FLASH_SR中的BSY位,确保当前未执行任何FLASH操作
2. 将FLASH_CR寄存器的PG位置1,激活FLASH编程
3. 针对所需存储器地址(主存储器或OTP区域内)执行数据写入操作 (通过设置FLASH状态寄存器的 PSIZE 位,设置并行位数位x32时按字写入)
4. 等待BSY位清零,完成一次编程
注意:1. 编程前要确保要写入地址的FLASH已经擦除;2. 要先写入FLASH密钥寄存器解锁,否则是不能操作FLASH状态寄存器的;3. 编程操作对OTP区域同样有效。
STM32F4的FLASH编程的时候,要先判断所写地址是否被擦除了;STM32F4的闪存擦除分为两种:扇区擦除和整片擦除
扇区擦除步骤:
1. 检查FLASH_CR的LOCK是否解锁,如果没有则先解锁
2. 检查FLASH_SR寄存器的BSY位,确保当前未执行任何FLASH操作
3. 在FLASH_CR寄存器中,将SER位置1,并从主存储块的12个扇区中选择要擦除的扇区SNB
4. 将FLASH_CR寄存器中的 STRT位 置1,触发擦除操作
5. 等待BSY位清零
批量擦除步骤:
1. 检查FLASH_SR寄存器中的BSY位,确保当前未执行任何FLASH操作
2. 在FLASH_CR寄存器中,将MER位置1(批量擦除)
3. 将FLASH_CR寄存器中的STRT位置1,触发擦除操作
4. 等待BSY位清零
FLASH访问控制寄存器:FLASH_ACR
Flash Access Control Register:Flash访问控制寄存器用于使能/关闭加速功能,并且可根据CPU频率控制Flash访问时间
位10 DCEN:数据缓存使能(Data cache enable)
位9 ICEN:指令缓存(Instruction cache enable)
位8 PRFTEN:预取使能(Prefetch enable)
DCEN、ICEN 和 PRFTEN 这三个位也非常重要,为了达到最佳的性能,这三个位一般都设置为 1 即可
位2:0 LATENCY:延迟(Latency) 这些位表示CPU时钟周期与Flash访问时间之比
这三个位必须通过MCU的工作电压和频率来进行正确的设置,否则可能会死机。
FLASH密钥寄存器:FLASH_KEYR
Flash Key Register:借助Flash密钥寄存器,可允许Flash控制寄存器的访问,进而允许进行编程或擦除操作。
位31:0 FKEYR:FPEC密钥寄存器(FPEC key)
将FLASH_CR寄存器解锁并允许对其执行编程/擦除操作,必须顺序编程以下值:
FLASH选项密钥寄存器:FLASH_OPTKEYR
Flash option key register:借助Flash选项密钥寄存器,可允许在用户配置扇区中执行编程和擦除操作
位31:0 OPTKEYR:选项字节密钥(Option byte key) 将FLASH_OPTCR寄存器解锁并允许对其编程,必须顺序编程以下值:
FLASH状态寄存器:FLASH_SR
Flash Status Register:Flash状态寄存器提供正在执行的编程和擦除操作的相关信息
位16 BSY:繁忙(Busy)
该位指示Flash操作正在进行。该位在Flash操作开始时置1,在操作结束或出现错误时清零。
FLASH控制寄存器:FLASH_CR
Flash Control Register:Flash 控制寄存器用于配置和启动Flash 操作
位31 LOCK:锁定Lock
该位只能写入1。该位置1时,表示FLASH_CR寄存器已锁定。当检测到解锁序列时,由硬件将该位清0。如果解锁操作失败,该位仍保持置1,直到下一次复位。
位16 STRT:启动Start
该位置1后可触发擦除操作。该位只能通过软件置1,并在BSY位清零后随之清零。
位9:8 PSIZE:编程大小(Program size) 这些位用于选择编程并行位数
位7:3 SNB:扇区编号(Sector number) 这些位用于选择要擦除的扇区
位2 MER:批量擦除(Mass Erase)
针对所有用户扇区激活擦除操作
位1 SER:扇区擦除(Sector Erase)
激活扇区擦除
位0 PG:编程(Programming)
激活Flash编程
实验现象:
开机时在LCD上显示一些提示信息,然后在主循环里面检测两个按键,其中按键KEY1用来执行写入FLASH的操作,按键KEY0用来执行读出FLASH中的数据的操作。
#include "stm32f4xx.h"
#include "delay.h"
#include "usart.h"
#include "LED.h"
#include "lcd.h"
#include "Key.h"
#include "usmart.h"
#include "MyI2C.h"
#include "AT24C02.h"
#include "STMFlash.h"
//LCD状态设置函数
void led_set(u8 sta)//只要工程目录下有usmart调试函数,主函数就必须调用这两个函数
{
LED1=sta;
}
//函数参数调用测试函数
void test_fun(void(*ledset)(u8),u8 sta)
{
led_set(sta);
}
//要写入STM32F4 FLASH的字符串数组
const u8 TEXT_Buffer[]={"STM32 FLASH TEST"};
#define TEXT_LENTH sizeof(TEXT_Buffer) //数组长度
#define SIZE TEXT_LENTH/4+((TEXT_LENTH%4)?1:0) //判断字节长是不是4的倍数,也可以说是判断地址是否有效
//如果字能被4整除,那么TEXT_LENTH/4一定是一个正数,TEXT_LENTH%4一定是0,那么整体一定是真,返回SIZE等于1;
//如果不是4的倍数,那么返回SIZE是0;
#define FLASH_SAVE_ADDRESS 0x0800C004 //设置FLASH 保存地址(必须为偶数,且所在的扇区要大于本代码所用到的扇区)
//否则,写操作的时候,可能会导致擦除整个扇区,从而引起部分程序丢失,引起死机
int main(void)
{
u8 key=0;
u16 i=0;
u8 datatemp[SIZE];
delay_init(168);
uart_init(115200);
LED_Init();
LCD_Init();
Key_Init();
POINT_COLOR=RED;
LCD_ShowString(30,50,200,16,16,"Explorer STM32F4");
LCD_ShowString(30,70,200,16,16,"FLASH EEPROM TEST");
LCD_ShowString(30,90,200,16,16,"ATOM@ALIENTEK");
LCD_ShowString(30,110,200,16,16,"2023/07/18");
LCD_ShowString(30,130,200,16,16,"KEY1:Write KEY0:Read");
while(1)
{
key=KEY_Scan(0);
if(key==2) //KEY1按下 写STM32 FLASH
{
LCD_Fill(0,170,239,319,WHITE); //清除半屏 x范围是0-239,y范围是170-319
LCD_ShowString(30,170,200,16,16,"Start Write FLASH……");
STMFLASH_Write(FLASH_SAVE_ADDRESS,(u32*)TEXT_Buffer,SIZE); //在保存地址上写入字节长为SIZE的字,要写入的字来自于TEXT_Buffer
LCD_ShowString(30,170,200,16,16,"FLASH Write Finished!"); //提示传送完成
}
if(key==1) //KEY0按下 读取字符串并显示
{
LCD_ShowString(30,170,200,16,16,"Start Read FLASH……");
STMFLASH_Read(FLASH_SAVE_ADDRESS,(u32*)datatemp,SIZE); //从写入的地址上读出字节SIZE长的字,读到的字存储到datatemp数组中
LCD_ShowString(30,170,200,16,16,"The Data Readed Is: ");
LCD_ShowString(30,190,200,16,16,datatemp); //显示读到的字符串
}
i++;
delay_ms(10);
if(i==20)
{
LED0=!LED0; //提示系统正在运行
i=0;
}
}
}
#include "stm32f4xx.h"
#include "STMFlash.h"
#include "delay.h"
#include "usart.h"
//读取指定地址的字(32位数据)
//fAddress:读地址
//返回值:对应数据
u32 STMFLASH_ReadWord(u32 fAddress) //字节8位,半字16位,字为32位
{
return *(vu32*)fAddress; //将地址强制类型转换为vu32的指针,然后解引用得到该指针指向位置的数据
}
//获取某个地址所在的FLASH扇区
//Address:Flash地址
//返回值:0~11,即Address所在的扇区
uint16_t STMFLASH_GetFlashSector(u32 Address)
{
//该函数的架构思路是:首先我在头文件中宏定义了每个扇区的起始地址,只要给定地址Address小于某一扇区的起始地址,就认为该地址处于上一个扇区中
if(Address
#ifndef _STMFLASH__H_
#define _STMFLASH__H_
#include "sys.h"
//FLASH起始地址
#define STM32_FLASH_START_ADDRESS_BASE 0x08000000 //STM32 FLASH起始地址
//FLASH扇区起始地址 定义扇区的地址是定义FLASH主存储区的地址,本次使用的是STM32F4系列的芯片,对应FLASH主存储区大小1024K
#define ADDRESS_FLASH_SECTOR_0 ((u32)0x08000000) //扇区0起始地址,16 Kbytes
#define ADDRESS_FLASH_SECTOR_1 ((u32)0x08004000) //扇区1起始地址,16 Kbytes
#define ADDRESS_FLASH_SECTOR_2 ((u32)0x08008000) //扇区2起始地址,16 Kbytes
#define ADDRESS_FLASH_SECTOR_3 ((u32)0x0800C000) //扇区3起始地址,16 Kbytes
#define ADDRESS_FLASH_SECTOR_4 ((u32)0x08010000) //扇区4起始地址,64 Kbytes
#define ADDRESS_FLASH_SECTOR_5 ((u32)0x08020000) //扇区5起始地址,128 Kbytes
#define ADDRESS_FLASH_SECTOR_6 ((u32)0x08040000) //扇区6起始地址,128 Kbytes
#define ADDRESS_FLASH_SECTOR_7 ((u32)0x08060000) //扇区7起始地址,128 Kbytes
#define ADDRESS_FLASH_SECTOR_8 ((u32)0x08080000) //扇区8起始地址,128 Kbytes
#define ADDRESS_FLASH_SECTOR_9 ((u32)0x080A0000) //扇区9起始地址,128 Kbytes
#define ADDRESS_FLASH_SECTOR_10 ((u32)0x080C0000) //扇区10起始地址,128 Kbytes
#define ADDRESS_FLASH_SECTOR_11 ((u32)0x080E0000) //扇区11起始地址,128 Kbytes
u32 STMFLASH_ReadWord(u32 fAddress);
uint16_t STMFLASH_GetFlashSector(u32 Address);
void STMFLASH_Write(u32 WriteAddress,u32 *pBuffer,u32 NumToWrite);
void STMFLASH_Read(u32 ReadAddress,u32 *pBuffer,u32 NumToRead);
#endif