STM32G0-内部flash读写驱动详解(寄存器版)

基本概念

首先给出一个STM32G030芯片的闪存结构图:

STM32G0的闪存模块由于Main memory(主储存器),information block(信息块)两个部分组成。

Main memory(主储存器):用于储存用户编译烧录的代码和数据常量。

information block(信息块):信息块同样被分为了好几个部分:

  1. system memory(系统内存):系统存储区是用户不能访问的区域,它在芯片出厂时已经固化了启动代码,它负责实现串口、 USB 以及 CAN 等 ISP 烧录功能。
  2. OTP area(OTP 区域):指的是只能写入一次的存储区域,容量为1K,写入后数据无法更改, OTP 常用于存储应用程序的加密密钥。
  3. Engineering bytes(工程字节):这里芯片手册没有提及,暂时不知道干嘛用。
  4. 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的地址上成功写入了对应的数据。

物联沃分享整理
物联沃-IOTWORD物联网 » STM32G0-内部flash读写驱动详解(寄存器版)

发表评论