stm32f103c8t6学习笔记(学习B站up江科大自化协)-FLASH闪存

FLASH简介

        ·闪存的操作较为麻烦,涉及擦除、编程、等待忙、解锁等等操作,需要把指令和数据写入到外设的相应寄存器,外设会自动去操作对应的存储空间。

        ·外设可以对程序存储器和选项字节进行擦除和编程,不能对系统存储器进行操作,因为系统存储器是原厂写入的bootloader程序,不允许修改

        ·对于C8T6程序存储容量是64K,一般写一个程序只占前边很小一部分空间,剩下的大部分空间可以利用,比如存储一些自定义的数据,充分利用资源,但是需要注意不能覆盖原有程序,否则将程序破坏,无法运行。存储少量参数一般选最后几页就行了

        ·在程序中编程:首先需要自己写一个bootloader程序,存放在程序更新时不会覆盖的地方,比如放在存储区域靠后的部分,需要更新程序时,控制程序跳转到我们自己写的bootloader里,这里边可以接受任意通信接口传过来的数据,如串口、蓝牙转串口、USB、WIFi转串口等等,传过来的数据就是待更新的程序,然后控制FLASH读写,把收到的程序写入到存储器前边程序正常运行的地方,写完之后再控制程序跳转到正常运行的地方,或直接复位,完成程序的自我升级。

        实现自我升级的过程和系统的bootloader一样,由于程序要实现自我升级,需要布置一个辅助的小机器人来临时干活,不过系统存储器的bootloader写死了,只能用串口下载到指定位置,启动方式也不方便,只能配置BOOT引脚触发启动,如果自己写bootloader的话就可以自定义收的方式和写的位置以及启动的方式,整个升级过程程序都可以自主完成,实现在程序中编程,更进一步可以实现远程升级

闪存模块组织

        ·小容量产品,页数较少,只有32页,每页1k;中容量产品128页每页1k;大容量产品256页每页2k

        ·分为三个块,

        第一个是主存储器,也就是程序存储器,用于存放程序代码,是最主要也是容量最大的一块

        第二个是信息块,里边可以分为启动程序代码和用户选择字节,其中启动程序代码就是系统存储器,用于存放原厂写入的bootloader,用于串口下载;用户选择字节是是选项字节,存放独立的参数,在手册中称为选择字节(Option Bytes),是同一个东西

        第三块是闪存存储器接口寄存器,这一块的寄存器并不属于闪存,因为地址都是40开头的,说明是一个普通的外设,和GPIO、定时器、串口等属于一个性质的东西,存储介质都是SRAM。闪存存储器接口属于上边的管理员,用于控制擦除和编程这个过程的

        ·读取指定存储器直接使用指针即可,用不到外设

        ·对主存储器进行分页是为了更好的管理闪存,擦除和写保护都是以页为单位的

        ·闪存的特性类似,写入前必须擦除,擦除必须以最小单位进行,擦除后数据位全变为1,数据只能1写0不能0写1,擦除和写入之后都要等待忙

        ·看地址范围可以发现,只要地址以000、400、800、C00结尾的一定是页的起始地址

        ·平时说的芯片闪存容量是64K、128K指的是主存储器的容量,下边信息块的容量虽然也是闪存但是不统计在这个容量里

FLASH基本结构

        ·整个闪存分为程序存储器、系统存储器和选项字节三个部分

        ·“闪存存储器接口“ 和 “闪存编程和擦除控制器(FPEC)实际上是同一个东西”,别名而已。闪存存储器接口相当于闪存的管理员,可以对程序存储器进行擦除和编程,也可以对选项字节进行擦除和编程,系统存储器不可以

        ·选项字节很大一部分配置位是配置主程序存储器读写保护的,所以可以配置读写保护

操作控制器FPEC对程序存储器和选项字节进行擦除和编程

一、FLASH解锁

         ·操作和W25Q64类似,W25Q64操作之前需要写使能,FLASH操作之前也需要进行解锁,目的是为了防止误操作。解锁的方式和独立看门狗一样,在键寄存器写入指定的键值实现,好处是防止误操作,每一个指令必须输入密码才能完成

        ·RDPRD键是解除保护的密钥,值是0xa5,三个键值是是随意定义的,只要定义的不是很简单,不会随意解锁即可

        复位之后FLASH默认上锁,在FLASH_KER寄存器中,先写入KEY1再写入KEY2解锁,安全性较高,如果没按顺序正确写入密码,整个模块会完全锁死,除非复位

        控制寄存器中有个lock位,在这一位写1就可以重新锁住闪存了

二、使用指针访问存储器

        ·STM32内部的存储器是直接挂载在总线上的,这时读写某个寄存器只需要使用C语言的指针即可。

使用指针读指定地址下的寄存器

        ·第一步给定读取存储器的地址,*((__IO uint16_t *)(0x08000000))在这里的括号里边由于只有一个数,所以可以不加括号;但是如果要对地址进行加减,必须加上括号,并在括号里面进行加减,否则运算的优先级会有问题

        ·第二步在地址前边加上强制类型转换,这里将变量强制转换为了uint16_t的指针类型,如果想以16位的方式读出指定地址的数据,就转换为uint16_t*,如果想以8位读出就uint8_t*,想浮点类型就float*或double*。

        在指针类型前边加了__IO,在STM32中这时一个宏定义,#define __IO volatile。对应c语言的关键字volatile,volatile直译就是易变的数据,加上这个是一种安全保障措施,在程序逻辑上没有作用,只是用于防止编译器优化。keil编译器默认情况下是最低优化等级,加不加volatile都不影响,如果提高编译优化等级就会产生问题。

        编译优化的作用是取出无用的繁杂代码,降低代码空间,提升运行效率,但是优化之后在某些地方编译器会弄巧成拙,比如想使用变量计数空循环的方式实现延时函数,编译器优化的时候会觉得这段代码没用会直接优化掉,这时可以在前边加上volatile告诉编译器不进行优化,原封不动的执行。

        编译器会利用缓存加速代码,比如要频繁读写内存的某个变量,最常见的优化方式是先把变量移到高速缓存里,在STM32里有一个类似缓存的工作组寄存器,这些寄存器的访问速度最快,先把变量放在缓存里,需要读写的时候直接读写缓存即可,用完之后再写回内存,这是优化的方案。但是如果程序存在多个线程,比如中断函数,在中断函数里改变了原始变量,但是缓存并不知道已经更改了,下次程序还看缓存中的变量,会造成数据更改不同步的问题,解决方法也是在变量定义前边加上一个volatile,告诉编译器运行的时候要从内存里找,不使用缓存进行优化

        如果开起了编译器优化,在无意义加减变量,多线程更改变量,读写与硬件相关的存储器时,都需要加上volatile防止被编译器优化。如果默认不开启编译器优化则无所谓,加不加都一样

         ·第三步使用*号,指针取内容,把指针指向的存储器取出来,这个值是指定存储器的值,取出来后赋值给自定义的变量Data。

        对于闪存的读取是不需要解锁的,因为读取是只看寄存器,不对寄存器进行更改,所需权限低,无需解锁可直接读

使用指针写指定地址下的寄存器

        ·如上边的图,等号左边先给定地址,再强转为指针,最后指针取内容,就是指定地址的值,直接进行赋值,对其赋值比如0x1234,完成指定地址写的功能。

        由于是写入数据,并且指定的是闪存的地址,闪存在程序运行时是只读的,不能轻易更改,对闪存进行更改的时候需要较高的权限,需要提前解锁,并且套上后边的 “ 程序存储器编程 ” 的流程,如果地址写的是SRAM的地址,比如0x20000000,则可以直接写入,因为SRAM在运行的时候是可读可写的

流程图

程序存储器编程(写入)

        ·在擦除之后就可以执行写入的流程,STM32的闪存在写入之前会检查指定地址有没有擦除,如果没有擦除就执行写入,STM32则不执行写入操作,除非写入的数据全是0

        ·写入的第一步也是解锁,然后置控制寄存器的PG(Programing)位为1,表示即将写入数据。然后在指定的地址写入半字,也就是前边的使用指针在指定地址写入数据,不过写入操作只能以半字的形式写入。

        ·字World是32位数据,半字halfword就是16位数据,字节Byte是8位数据。在写入的时候规定是16位,如果要写入32位的话则需要分两次完成。如果想单独写入一个字节8位数据,还要保留另一个字节的原始数据的话,只能将整页数据读到SRAM,再随意修改SRAM数据,修改完成之后将闪存整页擦除,最后将SRAM整页写回闪存。

        ·写入数据的代码是触发开始的条件,无需像擦除一样置Start位,写入半字之后芯片会处于忙状态,等待busy清零,完成写入数据的过程,每执行一个流程只能写入半字,如果要写入很多数据,循环不断调用这个流程即可
 

程序存储器页擦除(写入前必须擦除)

        ·擦除之后所有的数据位变为1,擦除的最小单位就是1页,1k,1024字节 

        ·页擦除类似于下边的全擦除,第一步是解锁的流程,和全擦除一样

        ·第二步置控制寄存器里PER(page Erase)位为1,在AR地址寄存器中选择要擦除的页,最后置控制寄存器的Start位为1,置Start为1也是触发条件,Start为1芯片开始干活,芯片看到per = 1就知道接下来要执行页擦除,然后继续看AR寄存器的数据,AR寄存器要提前写入一个页起始的地址,芯片就会将指定的一页给擦除

        擦除开始之后页需要等待busy位,最后读出并验证数据不用看

程序存储器全擦除

        ·把所有的页都擦除掉,但是信息块的内容不受影响

        ·第一步是读取lock位,查看芯片锁没锁,如果lock位 = 1,锁住了就执行解锁过程,解锁过程就是在KEYR寄存器先写入KEY1再写入KEY2,如果当前没有锁住就无需解锁,不过在库函数中是直接执行解锁,不管有没有上锁

        ·解锁之后,首先置控制寄存器里的MER(Mass Erase)位为1,然后再置Start位为1,其中Start位为1是触发条件,Start为1之后芯片开始干活,芯片看到MER位是1,接下来就会执行全擦除的过程,内部电路自动执行全擦除的过程

        ·擦除过程开始之后程序要执行等待,判断状态寄存器的BSY(BUSY)位是否为1,BSY位表示芯片是否处于忙状态,BSY为1表示芯片忙,如果判断为1则跳转回来继续判断,直到BSY为0跳出循环,结束全擦除过程

        ·最后一步是读出并验证所有页的数据,一般是测试程序才需要做的,正常情况下全擦除完成了,默认就是成功,如果要全部读出来验证的话工作量太大了,所以最后一步一般不管 

选项字节

        ·这部分分为选项字节的组织和用途,图中的起始地址就是前边的选项字节的起始地址1fff f800,(在前边闪存模块组织 - 信息块 - 用户选择字节 这一行,共有16个字节),把存储器展开就是对应这里的图

        图中有一半的名称带上了n,比如RDP和你RDP,这个是在RDP写入数据时,要同时在nRDP写入数据的反码,其余的寄存器同理,只有这样写入的数据才是有效的,如果芯片检测到这两个不是反码的关系,则代表数据无效,有错误,对应的功能不执行,这是一个安全保障措施。这个写入反码的过程硬件会自动计算并写入
        RDP(Read Project)是读保护配置位,在RDP存储器写入RDPRT键(A5)后解除读保护,如果RDP不是A5,那闪存就是读保护状态,无法通过调试器读取程序,防止程序被别人窃取
        USER是属于比较零碎的配置位,配置硬件看门狗和进入停机、待机模式之后是否产生复位
        DATA0、1在芯片中没有定义功能,用户可自定义使用
        WRP0、1、2、3配置的是写保护,在中容量产品里每一个位对应保护四个存储页,四个字节总共32位,一位保护4页,总共保护32*4 = 128页,正好对应中容量的128页    
        ·对于小容量产品也是每一个位对应保护四个存储页,但是小容量产品最大只有32k,所以只需要一个位WRP0即可,4*8 = 32,

        对于大容量产品,每一个位只能保护两个存储页,四个字节不够用,所以WRP3的最高位将剩下的所有页一起进行保护

        ·选项字节本身也是闪存,写入前也需要进行擦除

选项字节擦除 

        第一步是解锁闪存,但是这里并没有写

        第二步检查sr的busy位,以确认没有其他正在进行的闪存操作,也就是事前等待

        第三步解锁CR的OPTWRE(Option Write Enable)位 ,这一步是选项字节的解锁,在选项字节中还有一个单独的锁,在解锁闪存之后还需要再解除选项字节里边的锁,之后才能解锁选项字节。解锁选项字节的锁也是一样的流程,在OPTKEYR里先写入key1再写入key2

        解除小锁之后先置CR的OPTER(Option Erase)位为1,表示即将擦除选项字节,之后设置CR的Start位为1,触发芯片开始擦除选项字节的工作,之后等待BSY位变为0,完成擦除选项字节

 选项字节编程

        ·选项字节的写入和普通闪存的写入差不多,先检测BSY然后解除小锁

        设置CR的OPTPG(Option Programming)位为1,表示即将写入选项字节

        写入要编程的半字到指定地址,这个是指针写入操作

        等待忙

        这样就完成了写入选项字节

器件电子签名

        ·电子签名实际上就是STM32的ID号,存放区域是系统存储器。在系统存储器里不仅有bootloader程序还有几个字节的ID号。

        系统存储器的起始地址是1fff f000,在图中有两个地址,闪存容量寄存器的基地址是0x1fff e7e0,通过地址可以确定其位置就是系统存储器,这个寄存器的大小是16位,其值就是闪存的容量,单位是kb;第二个是产品唯一身份标识寄存器,也就是每个芯片的身份证号,大小是96位,每一个芯片的这96位数据都是不一样的。

        使用这个唯一的ID号可以执行一些加密操作,比如想写入一段程序,只能在指定设备运行,那么就可以在程序的多处加入ID号判断,如果不是指定设备的ID号,就不执行此功能,这样即使程序被盗,在别的设备上也难以运行

        这96位可以以字节为单位读取,也可以以半字或全字读取。

        唯一身份标识寄存器可以作为序列号、密码,用来激活带安全机制的自举过程

读写内部闪存存储数据的一个弊端

        在编程过程中,任何读写闪存的操作都会使CPU暂停,直到此次闪存编程结束,这其实是读写内部闪存存储数据的一个弊端,闪存忙的时候代码执行会暂停,因为执行代码需要读闪存,闪存在忙,没办法读,CPU自然无法运行,程序就会暂停

        这个弊端会导致一写问题,比如使用内部闪存存储数据,同时中断代码是在频繁执行的,这样读写闪存的时候中断代码无法执行,会导致中断无法及时相应、

        如果程序中有需要频繁执行且对时间要求严格的中断函数,需要慎用内部闪存存储用户数据

代码&功能实现部分 

接线图

工程结构     

        ·工程计划建立两个底层模块,最底层计划叫MyFLASH,在这边实现闪存最基本的三个功能,读取、擦除和编程。在此模块之上再建一个模块叫STORE,主要实现参数数据的读写和存储管理。

        应用层最终要实现的功能是任意读写参数,并且这些参数是掉电不丢失的,至于具体的存在什么地方,怎么擦除,采用什么读写策略,这并不是应用层所关心的。在store这一层会定义一个SRAM数组,需要掉电不丢失的参数就写入到SRAM数组里,之后调用保存的函数,这个SRAM数组就自动保存到闪存里,上电后store初始化,会自动把闪存里的数据读到SRAM数组里,闪存管理策略主要在store这一层实现。

        在main.c里边的应用层部分,想要保存参数,就写到store层的数组,再调用保存函数备份到闪存,实现最终功能             

ST-LINK Unity

        这个软件中可以芯片内部存的是什么,一目了然,可以单独测试读取、擦除和编程,验证程序功能非常方便

软件的功能分布

                                                                             

         如图圈中部分,Address可以选择指定想要查看的起始地址,目前是0800 0000,也就是整个闪存的起始地址,所以下面的界面就是从0800 0000开始的,里边存的就是程序代码

        上边的第二个框size,从起始地址开始,总共查看多少个字节,0x10000就是查看64kb字节的字节数。0x10000转换为十进制就是65536,除以1024 = 64,

        第三个框是数据宽度,可以指定是以32位、16位还是8位的形式显示,分别对应字、半字和字节,现在是32位就是四个字节在一个框里,如果改成16位就是两个字节一个框,改成8位就是一个字节一个框

        这个软件可以直接任意修改闪存的数据,但是正常不建议随便改,万一破坏了程序可能会运行会出问题。

选项字节的读写

        

        ·进入选项字节配置界面之后,从上到下框选的部分依次是读保护、用户配置部分、自定义的两个字节和写保护,这四个部分的配置也是可以直接点击就可以配置。    

        写保护点一下是四页同时打钩,对应选项字节里的一个位配置4个闪存页

        ·配置完成之后点击底部apply即可,无需写代码。用代码配置读写保护的话容易造成芯片自锁,比如把闪存写保护了,但是程序里并没有预留解除写保护的代码,锁住之后芯片没办法下载程序,靠芯片自己没办法实现自救,可以通过这个软件进行配置,在选项字节里把第一个框的读写保护都去掉,apply,就可以救活芯片了

stm32f10x_flash.h的函数

        ·函数被分为3个部分,第一部分的注释写的是所有F10x设备都可以使用的函数;第二部分是所有设备都可以使用的、新的函数;第三部分是只有xl加大容量的设备才可以使用的新的函数,并且第三部分的函数有预编译,只有定义了XL这个宏,函数才是有效的        

        加大容量xl比小 ld 中 md 大 ld 容量多了一块新的独立闪存,xl有两块闪存,为了区分,设计者命名新加的这一块为bank2,原来小中大容量的叫做bank1


//与内核运行有关 无需调用
void FLASH_SetLatency(uint32_t FLASH_Latency);
void FLASH_HalfCycleAccessCmd(uint32_t FLASH_HalfCycleAccess);
void FLASH_PrefetchBufferCmd(uint32_t FLASH_PrefetchBuffer);
/
//解锁 转到定义里边是先写入key1再写入key2
void FLASH_Unlock(void);
//加锁 把CR寄存器的lock位设置为1
void FLASH_Lock(void);
///
//一下一片是对主闪存和选项字节进行擦除和编程的函数   返回值是对应操作的完成状态
//typedef enum		//FLASH_Status转到定义后是一个枚举 执行完后会返回对应执行状态
//{ 
//  FLASH_BUSY = 1,			返回这个表示芯片当前忙
//  FLASH_ERROR_PG,			返回这个表示编程错误
//  FLASH_ERROR_WRP,		返回这个表示写保护错误
//  FLASH_COMPLETE,			如果执行完全没问题返回这个
//  FLASH_TIMEOUT			返回这个表示等待超时
//}FLASH_Status;
//闪存擦除某一页 参数给一个起始地址 将指定的页擦除
FLASH_Status FLASH_ErasePage(uint32_t Page_Address);
//全擦除
FLASH_Status FLASH_EraseAllPages(void);
//擦除选项字节
FLASH_Status FLASH_EraseOptionBytes(void);
//在指定地址写入字
FLASH_Status FLASH_ProgramWord(uint32_t Address, uint32_t Data);
//在指定地址写入半字
FLASH_Status FLASH_ProgramHalfWord(uint32_t Address, uint16_t Data);
//自定义的DATA0和DATA1
FLASH_Status FLASH_ProgramOptionByteData(uint32_t Address, uint8_t Data);
//写保护
FLASH_Status FLASH_EnableWriteProtection(uint32_t FLASH_Pages);
//读保护
FLASH_Status FLASH_ReadOutProtection(FunctionalState NewState);
//用户选项的3个配置位
FLASH_Status FLASH_UserOptionByteConfig(uint16_t OB_IWDG, uint16_t OB_STOP, uint16_t OB_STDBY);
/
//获取用户选项的三个配置位
uint32_t FLASH_GetUserOptionByte(void);
//获取写保护状态
uint32_t FLASH_GetWriteProtectionOptionByte(void);
//获取读保护状态
FlagStatus FLASH_GetReadOutProtectionStatus(void);
//获取预取缓冲区状态
FlagStatus FLASH_GetPrefetchBufferStatus(void);
//中断使能
void FLASH_ITConfig(uint32_t FLASH_IT, FunctionalState NewState);
//获取标志位
FlagStatus FLASH_GetFlagStatus(uint32_t FLASH_FLAG);
//清除标志位
void FLASH_ClearFlag(uint32_t FLASH_FLAG);
//获取状态
FLASH_Status FLASH_GetStatus(void);
//等待上一次操作	在执行耗时操作的时候函数内部已经调用了 无需我们单独调用
FLASH_Status FLASH_WaitForLastOperation(uint32_t Timeout);

擦除

main.c部分代码

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "MyFLASH.h"

int main()
{
	OLED_Init();
	
	OLED_ShowHexNum(1,1,MyFLASH_ReadWord(0x08000000),8);//读取闪存的起始地址
	OLED_ShowHexNum(2,1,MyFLASH_ReadHalfWord(0x08000000),4);//读取闪存的起始地址
	OLED_ShowHexNum(3,1,MyFLASH_ReadByte(0x08000000),2);//读取闪存的起始地址
	while(1)
	{

	}
}

        编译烧录后发现,屏幕显示第一行是2000 0660,第二行是0660 ,第三行是60,打开STLINK unity,查看08000000起始地址下的第一个数据,发现和OLED显示的内容一样,说明读写没有问题

        将数据宽度改为16位,发现第一个数据显示变为了0660,正好符合OLED显示的第二行,半字读取的效果;再将数据宽度改为8位,发现显示的内容是60,与OLED第三行显示的内容一致。

        从这数据宽度改变,读取的数值改变可以发现,数据是以小端模式存储的,也就是低位字节存储在低位地址,如果以字节读取,第一个就是60;以半字读取,前两个字节读取,由高位到低位是06 60;以字读取,前四个字节组合到一起,由高位到低位是20 00 06 60,是与显示在STLINK unity相反,如下图

        ·编译烧录完代码之后,按下复位键屏幕会进行刷新,数据不会丢失

        ·如果按下按键,进行页擦除,之后再按下复位键,发现屏幕不会再进行刷新,原因是程序已经被擦除,损毁,无法运行,此时可以打开STLINK unity观察FLASH中的数据,发现0800 0000起始的这一页的数据全部被擦除了。

        但是此时OLED显示的值并没有消失,因为OLED内部具有显存,可以保存最后一次显示的内容,如果断电之后重新上电的话会发现OLED没有任何的显示,如果是进行全擦除也是同理

编程

        ·对于64K的闪存,最后一页的起始地址就是0800 FC00。在写入之前需要先执行页擦除。在myflsah.c处添加如下函数

void MyFLASH_ProgramWord(uint32_t Address,uint32_t Data)
{
	FLASH_Unlock();
	FLASH_ProgramWord(Address,Data);
	FLASH_Lock();
}
void MyFLASH_ProgramHalfWord(uint32_t Address,uint16_t Data)
{
	FLASH_Unlock();
	FLASH_ProgramHalfWord(Address,Data);
	FLASH_Lock();
}

        在main.c部分加上如下代码

	MyFLASH_ErasePage(0x0800fc00);
	MyFLASH_ProgramWord(0x0800fc00,0x12345678);
	MyFLASH_ProgramHalfWord(0x0800fc10,0xabcd);

        编译下载后打开STLINK unity,下滑到0x0800 fc00,检查存储的数据,恰好是我们写入的数据。如果写入新的数据在同一页,那么会将原来的数据擦除,因为在写入之前会先执行擦除的操作,这里执行的是页擦除。

掉电不丢失存储

        main.c部分代码

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "MyFLASH.h"
#include "Key.h"
#include "Store.h"

uint8_t KeyNum;

int main()
{
	OLED_Init();
	GPIO_Key_Init();
	Store_Init();//第一次使用的时候初始化闪存,把闪存备份的数据加载会SRAM数组
	OLED_ShowString(1,1,"flag:");
	OLED_ShowString(2,1,"data:");
	while(1)
	{
		KeyNum = Key_GetNum();
	
		if (KeyNum == 1)
		{
			Store_DATA[1]  ++;
			Store_DATA[2]  +=2;
			Store_DATA[3]  +=3;
			Store_DATA[4]  +=4;
			Store_Save();
		}
		if (KeyNum == 2)
		{
			Store_Clear();
		}
		OLED_ShowHexNum(1,6,Store_DATA[0],4);
		OLED_ShowHexNum(3,1,Store_DATA[1],4);
		OLED_ShowHexNum(3,6,Store_DATA[2],4);
		OLED_ShowHexNum(4,1,Store_DATA[3],4);
		OLED_ShowHexNum(4,6,Store_DATA[4],4);
	}
}

        store.c部分代码

#include "stm32f10x.h"
#include "MyFLASH.h"

#define STORE_START_ADDRESS 0x0800fc00
#define STORE_COUNT			512


//使用SRAM缓存数组来管理FLASH的最后一页,实现参数的任意读写和保存。由于闪存每次都是擦除再写入,擦除之后容易丢失数据,需要靠SRAM来灵活管理数组,需要备份的时候再统一保存到数组里
//想存储掉电不丢失的参数的时候,先任意更改数组中除了标志位外的数据,之后将数组整体备份到闪存的最后一页
uint16_t Store_DATA[STORE_COUNT];//STORE_COUNT个数据,每个数据16位,2字节 正好对应闪存的一页1024字节


void Store_Init(void)
{
	//需要先初始化一下,比如第一次使用闪存的话全部默认ff 参数和SRAM一般默认0 对于第一次使用要对各个参数都置0
	//把闪存最后一页的第一个半字当做标志位判断
	if(MyFLASH_ReadHalfWord(STORE_START_ADDRESS) != 0xa5a5)	//这里的a5a5是自己定义的标志位 不是标志位说明第一次使用
	{
		MyFLASH_ErasePage(STORE_START_ADDRESS);
		MyFLASH_ProgramHalfWord(STORE_START_ADDRESS,0xa5a5);//写上规定的标志位a5a5
		for(uint16_t i = 1;i < STORE_COUNT;i ++)	//将剩余的空间全部置为默认值0 注意从1开始跳过标志位
		{
			MyFLASH_ProgramHalfWord(STORE_START_ADDRESS + i*2,0x0000);//一个半字占用两个地址
		}
	}
	
	for(uint16_t i = 0;i < STORE_COUNT;i ++)	//把闪存备份的数据恢复到SRAM数组里
	{
		Store_DATA[i] = MyFLASH_ReadHalfWord(STORE_START_ADDRESS + i*2);
		
	}
}

void Store_Save(void)
{
	//擦除最后一页
	MyFLASH_ErasePage(STORE_START_ADDRESS);
	//将数组完全备份到闪存
	for(uint16_t i = 0;i < STORE_COUNT;i ++)
	{
		MyFLASH_ProgramHalfWord(STORE_START_ADDRESS + i*2,Store_DATA[i]);
	}
}

void Store_Clear(void)
{
	for(uint16_t i = 1;i < STORE_COUNT;i ++)
	{
		Store_DATA[i] = 0;
	}
	Store_Save(); //归零之后需要把数据更新到闪存里 否则下次上电加载的是之前的数据
}

        将该程序编译烧录之后发现,按下按键1是执行各个数的操作,按下按键2是执行全擦除,其中对数进行增加的操作时如果掉电了是会自动保存数据的,复位也不会将其清除。在连接到STLINK unity之后是可以查看其变化的数据,但是查看期间无法进行按键操作。

闪存的空间分配问题

        ·在我们目前的代码中,闪存前边一部分是程序文件,后边是用户数据,是基于假设程序文件比较小的情况下,最后一页用不到,如果程序比较大触及到了最后一页,程序和用户数据存储的位置冲突。或者说最后10页都打算留给用户存储数据,如果前边的程序长一点就会容易和用户数据冲突,难以发现,易产生隐蔽的bug。

        可以通过给程序文件限定一个存储范围,不让其分配至后边存储数据的空间

        如上图,片上的起始地址是08000000,最高位的0省略了,size是10000,默认全部的64K 闪存都作为程序代码的分配空间,如果想把闪存尾部的空间留着自己使用,可以把程序空间的size改小,比如改成FC00,编译后的代码就不会分配到最后一页了,但是size过小编译会报错。

        下载程序的起始地址也可以更改,比如想写个bootloader放在闪存尾部,可以在这里修改下载到闪存的位置。

        右边是片上RAM的起始地址和大小,2000开始,5000大小,对应就是20K

下载选项-下载方式的选择

        ·debug - settings - flash download中配置下载选项,这里选择的是第二个,擦除扇区,也就是页擦除

        第一个是每次下载代码都全擦除再下载

        第二个是用到多少页就擦多少页,下载会更快一些,如果想在闪存尾部存储数据,最好选择页擦除下载,否则每次下载程序芯片都全擦除

程序编译后占用空间大小的查看

        在编译后底部会显示program size程序大小,前三个数相加就是程序占用闪存的大小,后两个数相加就是占用SRAM的大小。

        可以双击左侧的target1,会自动打开.map文件,里边是详细的编译信息。划到最后面可以看到如下图                 倒数第二行是占用SRAM的大小,大小是2664字节,2.6KB

        最后一行是占用闪存的大小,4576字节,4,。47kb

        看完这个内容之后建议是把.map文件关闭,不然编译后会弹出询问是否重新加载的窗口

查看芯片ID

查看芯片的闪存容量

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"


int main()
{
	OLED_Init();
	
	OLED_ShowString(1,1,"F_SIZE:");
	OLED_ShowHexNum(1,8,*((__IO uint16_t*)(0x1ffff7e0)),4);
	while(1)
	{

	}
}

        编译烧录后,显示出的是 0080,说明闪存大小是128K,如果是0040就是63K

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"


int main()
{
	OLED_Init();
	
	OLED_ShowString(1,1,"F_SIZE:");
	OLED_ShowHexNum(1,8,*((__IO uint16_t*)(0x1ffff7e0)),4);
	
	OLED_ShowString(2,1,"U_ID:");
	OLED_ShowHexNum(2,6,*((__IO uint16_t*)(0x1ffff7e8)),4);
	OLED_ShowHexNum(2,11,*((__IO uint16_t*)(0x1ffff7e8 + 0x02)),4);
	OLED_ShowHexNum(3,1,*((__IO uint16_t*)(0x1ffff7e8 + 0x04)),8);	
	OLED_ShowHexNum(4,1,*((__IO uint16_t*)(0x1ffff7e8 + 0x08)),8);
	while(1)
	{

	}
}

        编译烧录后,最终展示的就是芯片的ID了,代码以多种形式进行读取,通过STLINK unity也可以验证数据的准确性。其中如果选择32位形式查看的话,OLED显示的16位的数据要以小端模式进行读取。

  • 62
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值