基本概念
首先给出一个STM32G030芯片的闪存结构图:
STM32G0的闪存模块由于Main memory(主储存器),information block(信息块)两个部分组成。
Main memory(主储存器):用于储存用户编译烧录的代码和数据常量。
information block(信息块):信息块同样被分为了好几个部分:
- system memory(系统内存):系统存储区是用户不能访问的区域,它在芯片出厂时已经固化了启动代码,它负责实现串口、 USB 以及 CAN 等 ISP 烧录功能。
- OTP area(OTP 区域):指的是只能写入一次的存储区域,容量为1K,写入后数据无法更改, OTP 常用于存储应用程序的加密密钥。
- Engineering bytes(工程字节):这里芯片手册没有提及,暂时不知道干嘛用。
- Option bytes(选项字节):选项字节用于配置 FLASH 的读写保护、电源管理中的 BOR 级别、软件/硬件看门狗等功能,这部分共 32 字节。可以通过修改 FLASH 的选项控制寄存器修改。
Main memory(主储存器)通常以地址0X08000000开始,结束地址与不同规格芯片有关。这里采用的是STM32G030C8T6的芯片,其内部flash为64K,根据如上闪存结构图,STM32将其划分为了32页(page0-page31),每页的大小为2k。
我们往STM32中烧写的程序默认储存在stm32的内部flash中,但是stm32的内部flash空间是充足的,因此,除了烧写的程序占用的那部分flash以外,剩下的部分我们完全可以利用起来,用于储存我们自己程序运行时需要储存的数据和信息,不必要外接额外的flash芯片。
查看工程分布
正如前面所述,stm32烧写的程序占用了一部分flash空间,因此,在使用内部flash的时候,需要查看工程分布,避免覆盖烧写程序所占空间,引起硬件故障死循环。
我们在keil中双击Target,打开工程的.map文件,找到Memory Map of the image,这里就是程序的flash分布:
可以看到,空间的基地址就是从0x08000000开始,这里占用的范围为0x00000700,既我们的代码在stm32的内部flash中占用的范围为08000000-0x08000700。
而根据上述基本概念结构图可以看出,G0芯片的flash一页大小为2k,既2048字节,转为16进制为0x00000800,则该芯片一页flash的大小为0x080007FF(区间从0开始)。因此,从第二页0x08000800开始的空间,我们都可以充分利用起来。
时钟配置
1.根据时钟树:
FLASH的时钟树由AHB总线管理,因此需要开启FLASH在该总线上的时钟。
2.内部FLASH的写入速度远没有MCU的时钟频率快,因此,需要根据MCU时钟频率配置的不同,修改对应的闪存访问延迟时间
该配置在FLASH_ACR中可以进行修改:
FLASH写入过程
1.解锁
为了保护数据,防止错误操作造成的故障,STM32的FLASH模块在芯片复位之后是默认保护的,既:无法对内部FLASH的相关寄存器进行操作,因此,在操作内部FLASH之前,需要先往键寄存器FLASH_KEYR中依次写入对应KEY1和KEY2,以对其进行解锁。
在STM32G0系列中,KEY1和KEY2分别为:
FLASH_KEY1 0x45670123 //Flash key1
FLASH_KEY2 0xCDEF89AB //Flash key2
因此,我们可以配置解锁函数:
void FLASH_unlock(void)
{
FLASH->KEYR=FLASH_KEY1;//写入解锁序列.
FLASH->KEYR=FLASH_KEY2;
}
2.状态函数
FLASH的当前状态相关寄存器在FLASH_SR寄存器中:
其中:
BSY1:flash忙;
PROGERR:编程错误;
WRPERR:写保护错误;
OPERR:操作错误;
其他标志位可自行查阅芯片编程手册。
因此,这里配置了一个获取flash当前状态的函数,返回值:flash状态
u8 FLASH_GetStatus(void)
{
u32 res;
res=FLASH->SR;
if(res & FLASH_SR_BSY1) return 1;//flash忙
else if(res & FLASH_SR_PROGERR) return 2;//编程错误
else if(res & FLASH_SR_WRPERR) return 3;//写保护错误
else if(res & FLASH_SR_OPERR) return 4;//操作错误
return 0;//操作完成
}
3.等待完成函数
FLASH操作需要时间的,如果在FLASH操作未完成的时候,给内部FLASH写入新的命令,可能会导致跳转入硬件故障死循环,因此,需要配置一个等待完成函数。用于等待FLASH完成相关操作:
u8 FLASH_wait_time(u32 times)
{
u8 res;
do
{
res=FLASH_GetStatus();//获取flash状态
if(res !=1) break;//FLASH非忙,跳出
delay_Xus(1);
times--;
}while(times);
if(times==0) res=0xff;//flash超时
return res;//返回flash状态
}
4.擦除指定页
FLASH的写入过程是按页写入的,擦除同样如此,在写入新的数据前,需要先擦除对应的存储区域。在STM32G030系列MCU中,FLASH页擦除过程如下:
(1) 检查 FLASH_SR 寄存器的 BSY1位,检查是否有FLASH操作正在进行。
(2) 检查并清除由于以前的编程而导致的所有错误编程标志。
(3) 设置PER位并在FLASH控制寄存器(FLASH_CR)中选择要擦除的页面(PNB)。
(4) 设置FLASH控制寄存器(FLASH_CR)的STRT位。
(5) 等待,直到FLASH状态寄存器(FLASHSR)的BSY1位被清除
//faddr:传入的地址
u8 FLASH_earse(u32 faddr)
{
u8 res=0;
u32 page;
res=FLASH_wait_time(0x5FFF);//等待上次操作结束
page=(faddr-FLASH_BASE)/FLASH_PAGE_SIZE;
if(res==0)
{
FLASH_unlock();//解锁
FLASH->SR = (FLASH_SR_OPERR | FLASH_SR_PROGERR | FLASH_SR_WRPERR | \
FLASH_SR_PGAERR | FLASH_SR_SIZERR | FLASH_SR_PGSERR | \
FLASH_SR_MISERR | FLASH_SR_FASTERR | \
FLASH_SR_OPTVERR);//清除所有错误标志
CLEAR_BIT(FLASH->CR,FLASH_CR_PNB_Msk);//清除原有页号设置
SET_BIT(FLASH->CR,FLASH_CR_PER);//选择页擦除
FLASH->CR |= (page<<FLASH_CR_PNB_Pos);//要擦除的页地址
SET_BIT(FLASH->CR, FLASH_CR_STRT); //启动页擦除
res=FLASH_wait_time(0X5FFF);//等待擦除结束
if(res!=1)//flash非忙
{
CLEAR_BIT(FLASH->CR, FLASH_CR_PER);//关闭页擦除
FLASH_lock();//flash上锁
}
}
return res;
}
这里的FLASH_PAGE_SIZE就是FLASH的页地址宽度了,G0的内部FLASH宽度为2K,换算成16进制值,即:0x800u;而FLASH_BASE就是前面所说的FLASH起始地址0x08000000UL。
PNB页配置如下:
另外,需要补充的是,这里在擦除完之后,为了防止误操作,重新对FLASH的寄存器进行了上锁,上锁函数如下:
void FLASH_lock(void)
{
SET_BIT(FLASH->CR,FLASH_CR_LOCK);//锁定FLASH
}
5.全片擦除
全片擦除的配置与页擦除类似,区别在于不用单独配置需要擦除的页面,而是配置FLASH_CR_MER1大量擦除寄存器:
//擦除主储存分区全部数据
u8 FLASH_mass_earse(void)
{
u8 res=0;
res=FLASH_wait_time(0x5FFF);//等待上次操作结束
if(res==0)
{
FLASH_unlock();//解锁
FLASH->SR = (FLASH_SR_OPERR | FLASH_SR_PROGERR | FLASH_SR_WRPERR | \
FLASH_SR_PGAERR | FLASH_SR_SIZERR | FLASH_SR_PGSERR | \
FLASH_SR_MISERR | FLASH_SR_FASTERR | \
FLASH_SR_OPTVERR);//清除所有错误标志
SET_BIT(FLASH->CR,FLASH_CR_MER1);//大量擦除
SET_BIT(FLASH->CR, FLASH_CR_STRT); //启动页擦除
res=FLASH_wait_time(0X5FFF);//等待擦除结束
if(res!=1)//flash非忙
{
CLEAR_BIT(FLASH->CR, FLASH_CR_MER1);//关闭大量擦除位
FLASH_lock();//flash上锁
}
}
return res;
}
6.指定位置写双字
这里需要注意的是:STM32G030仅支持双字写入,与F103等MCU的半字写入不同,如果写入错误将导致STM32跳转入硬件故障死循环中断!!
STM32G0的标准写入过程如下:
(1) 通过检查Flash状态寄存器FLASH SR)的BSY1位,检查是否正在进行主Flash操作。
(2) 检查并清除由于以前的编程而导致的所有错误编程标志。
(3) 设置FLASH控制寄存器(FLASH_CR)PG位
(4) 在主存储器块或OTP区域内所需的内存地址执行数据写入操作。只能编程双字(64位)。
(5) 等待,直到FLASH状态寄存器(FLASH SR)的BSY1位被清除。
(6) 检查FLASH状态寄存器(FLASH SR)是否设置了EOP标志编程操作成功),并通过软件清除
(7) 清除FLASH控制寄存器(FLASHCR)的PG位.
这里我简化了写入流程,仅检测busy1位是否为忙碌状态,为程序严谨性,建议依据标准编程模式进行。
//G0的MCU一次写入2个字64bit
//faddr:指定地址,data:写入的数据
u8 FLASH_write_four(u32 faddr,u64 data)
{
u8 res;
res=FLASH_wait_time(0Xff);//等待操作完成
if(res==0)//flash空闲
{
FLASH_unlock();//解锁
SET_BIT(FLASH->CR,FLASH_CR_PG);//编程使能
*(u32*)faddr=data;
*(u32*)(faddr + 4u)=(u32)(data>>32);
res=FLASH_wait_time(0Xff);//等待操作完成
if(res!=1)//非忙
{
CLEAR_BIT(FLASH->CR,FLASH_CR_PG);//关闭编程使能
FLASH_lock();//flash上锁
}
}
return res;
}
7.FLASH写入
仅给出大概实现思路:
1.在特定位置写入数据,需要获取写入位置的页内偏移,页面剩余长度,页面地址。
2.使用memcpy函数可以方便的将数据拷贝到一个数组中暂时储存。
3.再次使用memcpy函数拷贝flash改写的部分数据
4.拷贝完成后擦除flash页面
5.将拷贝的数据通过双字写入的方式重新写入flash当前页面中。
u8 FLASH_write(u32 adder,u8 *pbuff,u32 para_size)
{
u32 offsec;//页内偏移
u32 offseclen;//页内剩余
u32 addsec;//页地址
u8 res;//状态标志位
u32 n;
while(para_size)//仅写入完成后跳出
{
offsec=adder & (FLASH_PAGE_SIZE-1);//页内偏移
offseclen=FLASH_PAGE_SIZE-offsec;//页剩余长度
addsec=adder & (~(FLASH_PAGE_SIZE-1));//页地址
if(offseclen>para_size) offseclen=para_size;//范围限制
memcpy(FLASH_buf,(u8*)addsec,FLASH_PAGE_SIZE);//拷贝flash原本页数据
memcpy(FLASH_buf + offsec,pbuff,offseclen);//flash改写部分的填充
res=FLASH_earse(adder);//拷贝完成擦除页
if(res==0xff) return res;//擦除超时返回
//写入内部flash
for(n=0;n<FLASH_PAGE_SIZE;n+=8)
{
res=FLASH_write_four(n+addsec,*(64*)&FLASH_buf[n]);//双字写入
if(res==0xff)//写入超时跳出
break;
}
adder=adder+offseclen;//下一个要写入数据地址
para_size=para_size-offseclen;//还需要写入数据大小
pbuff=pbuff+offseclen;//下一个要写入地址的位置
}
return res;//返回状态
}
8.FLASH读出
flash的读出操作相比较写入就简单多了,这里我还是采用了memcpy函数,将对应flash地址的数据拷贝出来:
void FLASH_read(u32 adder,u32 size)
{
memcpy(FLASH_read_buf, (u8*)adder, size);//从flash中拷贝出来放在数组里
}
验证
根据前面所述,查询空间分布,这里我从0x08000800地址开始写入一个测试数据:
“STM32G0 TEST”
并在写入完成后,通过串口将该数据循环重复读出并发送出来:
可以看到,在对应写入的位置能够正确的读出写入的flash数据,因此验证通过。
通过stm32cubeprogrammer直接查看芯片flash空间的对应地址,也可以看到,在0x08000800的地址上成功写入了对应的数据。