江协科技STM32 15-1 FLASH闪存

发布于:2025-08-07 ⋅ 阅读:(20) ⋅ 点赞:(0)

本节我们学习的内容是STM32的FLASH,闪存。当然闪存是一个通用的名词,表示的是一种非易失性,也就是掉电不丢失的存储器。我们之前学习SPI协议的时候用的W25Q64就是一种闪存存储器芯片。而本节我们所说的闪存则特指STM32的内部闪存,也就是我们下载程序的时候,这个程序所存储的地方。我们下载的程序掉电后不会消失,说明程序存储在了一个非易失性的存储器中,这个存储器,也是一种闪存。本节我们来学习一下如何利用程序,来读写存储程序的这个存储器。

STM32F1系列的FLASH包含程序存储器、系统存储器和选项字节三个部分,通过闪存存储器接口(外设)可以对程序存储器和选项字节进行擦除和编程,这里不能修改系统存储器是因为其存储了原厂写入的BootLoader程序。

读写FLASH的用途:利用程序存储器的剩余空间来保存掉电不丢失的用户数据通过在程序中编程(IAP),实现程序的自我更新

在线编程(In-Circuit Programming – ICP)用于更新程序存储器的全部内容,它通过JTAGSWD协议或系统加载程序(Bootloader)下载程序。上面的JTAG、SWD就是仿真器下载程序。目前使用的STLINK就是SWD协议,系统加载程序(Bootloader)下载程序也就是串口下载。

之后,更高级的下载方式就是在程序中编程(In-Application Programming – IAP)可以使用微控制器支持的任一种通信接口下载程序。实现方法就是我们首先要自己写一个BootLoader程序,并且存放在程序存储器中不会在程序更新时被覆盖的地方,需要更新程序时,我们控制程序跳转到自己写的Bootloader里来,在这里面我们就可以接收任意一种通信接口传过来的数据,比如串口、USB、蓝牙转串口、WIFI转串口等等,这个传过来的数据就是待更新的程序,然后我们控制FLASH读写把收到的程序写入到前面程序正常运行的地方,写完之后再控制程序跳转回正常运行的地方或者直接复位,这样程序就完成了自我升级。这个过程其实就和系统存储器的Bootloader一样。

下表是中容量产品的闪存分配情况,我们C8T6芯片的闪存容量是64K,属于中容量产品。

在闪存模块组织中的最后一块是闪存存储器接口寄存器,这一块的存储器实际上并不属于闪存,其地址都是40开头的,说明这个存储器接口寄存器就是一个普通的外设。这个闪存存储器接口就是上面那些闪存的管理员,这些寄存器就是用来控制擦除和编程这个过程的。当然,闪存存储器接口只能进行擦除和编程,并没有读取,这时因为读取指定存储器直接使用指针读即可,用不到该外设。

对于主存储器,这里对它进行了分页,分页是为了更好地管理闪存,擦除和写保护都是以页为单位的,这一点和W25Q64那节的闪存一样。同为闪存,它们的特性基本一样:写入前必须擦除、擦除必须以最小单位进行、擦除后数据位全变为1、数据只能1写0不能0写1、擦除和写入之后都需要等待忙。W25Q64的分配方式是先分为块、再分为扇区。这里就比较简单了,它只有一个基本单位就是页,每页的大小都是1K,0-127,总共128页,总容量就是128K,对于C8T6来说它只有64K.

第一个页的存储地址就是程序存储器的起始地址0x0800 0000;再下面选项字节,起始地址是0x1FFF F800,容量是16个字节,里面只有几个字节的配置参数。这里还可以发现,我们平时说的芯片闪存容量是64K、128K,它指的只是主存储器的容量,下面信息块的两个东西虽然也是闪存,但是并不统计在这个容量里,这就是闪存的分配方式;最后就是闪存接口寄存器了,里面包括KEYR键寄存器、SR状态寄存器、CR控制寄存器等等,外设的起始地址是0x4002 2000, 每个寄存器都是4个字节,也就是32位,这就是这个外设的寄存器。

接下来看一下总结的基本结构图,整个闪存分为程序存储器、系统存储器和选项字节三部分,这里程序存储器以C8T6为例,它是64K的所以只有64页,最后一页的起始地址是0x0800 FC00。最左边是闪存存储器接口,手册里还有一个名称:闪存编程和擦除控制器(FPEC),这个控制器就是闪存的管理员,它可以对程序存储器进行擦除和编程,也可以对选项字节进行擦除和编程,当然系统存储器是不能擦除和编程的,之后选项字节里面有很大一部分配置位,其实是配置主程序存储器的读写保护的,所以右边画的写入选项字节可以配置程序存储器的读写保护,当然选项字节还有几个别的配置参数,这个待会再讲。这就是整个闪存的基本结构。

接下来我们来看一下细节问题,如何操作这个控制器FPEC来对程序存储器和选项字节进行擦除和编程。首先第一步是FLASH解锁,这个和之前W25Q64一样,W25Q64操作之前需要写使能;这个FLASH操作之前需要解锁,目的都是为了防止误操作。这里解锁的方式和之前独立看门狗一样,都是通过在键寄存器写入指定的键值来实现,使用键寄存器的好处就是更能防止误操作,每一个指令必须输密码才能完成。首先,FPEC共有三个键值:RDPRT键是解除读保护的密钥,值是0x000000A5、KEY1键值是0x45670123、KEY2键,值是0xCDEF89AB。然后在FLASH_KEYR键寄存器中,先写入KEY1,再写入KEY2,解锁;错误的操作序列会在下次复位前锁死FPEC和FLASH_CR;我们操作完成之后要尽快把FLASH重新加锁,以防止意外情况,加锁的操作是设置FLASH_CR中的LOCK位锁住FPECFLASH_CR,就是控制寄存器里面有个LOCK位,我们在这一位写1,就能重新锁住闪存了。我们操作闪存的第一步就是解锁;操作完成后就加锁。

接下来我们要学习的是如何使用指针访问存储器,因为STM32内部的存储器是直接挂在总线上的,所以这时再读写某个存储器就非常简单了,直接使用C语言的指针来访问即可。

首先是使用指针读指定地址下的存储器:uint16_t Data = *((__IO uint16_t *)(0x08000000));

第一步给定要读取存储器的地址,如果要对这个地址进行加减就必须在括号内部进行加减;第二步在地址前面加上强制类型转换,这里把变量强制转换为了uint16_t的指针类型,如果你想以16位的方式读出指定地址的数据,那就转换成uint16_t*,如果想以8位的方式读出来就转换成uint8_t*,想32位就转换成uint32_t*,这个指针类型前面还加了个__IO,在STM32库函数中,这是一个宏定义#define    __IO    volatile。这个宏定义对应C语言的关键字,volatile,其直译就是易变的数据,在这个数据类型前面加上volatile是一个安全保障措施,加上这个关键字的目的,用一句话来说就是防止编译器优化。第二步完成后,括号里的部分就是一个指针变量,并且这个指针已经指向了0x0800 0000这个位置;最后一步就是使用*号,指针取内容,把这个指针指向的存储器取出来了,这个值就是指定存储器的值。取出来后,我们可以把它赋值给自定义的变量Data,这样就完成了指定地址读的任务了。另外说一下,对于闪存的读取来说是不需要进行解锁的,因为读取只看看存储器,不对存储器进行更改。接下来继续看使用指针写指定地址下的存储器:*((__IO uint16_t *)(0x08000000)) = 0x1234;这句话的意思就很明显了,左边和上面一样,先给定地址,再强转为指针,最后指针取内容,这样就是指定地址的值,我们直接对它赋值,比如0x1234,这样就能完成指定地址写的功能了。另外这里存在一个注意事项:因为这个语句是写入数据,并且指定的是闪存的地址,闪存在程序运行时是只读的,不能轻易更改,而我们本节需要对闪存进行更改,这个所需的权限就比较高,需要提前解锁,并且还要套一个后面会介绍的流程。那如果你这个地址写的是SRAM的地址,比如0x2000 0000那可以直接写入了,因为SRAM在程序运行时是可读可写的。

接下来就看一下下面三个流程图。

首先是全擦除的流程图,第一步是读取LOCK位看一下芯片锁没锁,如果LOCK位=1,锁住了就执行解锁过程,解锁过程就是在KEYR寄存器,先写入KEY1,再写入KEY2。如果当前没锁住就跳过解锁。但是在库函数中并没有这个判断,库函数是直接执行解锁过程,不管你锁没锁都执行解锁。解锁之后首先,置控制寄存器里的MER(Mass Erase)位为1,然后再置STRT(Start)位为1,其中STRT为1是触发条件;STRT为1之后,芯片开始干活,然后芯片看到MER位是1,它就知道接下来要干的活就是全擦除,这样内部电路就会自动执行全擦除的过程,擦除也是需要花一段时间的,所以擦除过程开始后,程序要执行等待,判断状态寄存器的BSY(Busy)位是否为1,BSY位表示芯片是否处于忙状态,BSY为1,表示芯片忙,所以这里如果判断BSY为1就跳转回来,继续循环判断,知道BSY=0,跳出循环,这样全擦除过程就结束了。最后一步读出并验证所有页的数据是测试程序才要做的。

然后我们看一下页擦除,这个也是类似的过程,第一步,一样是解锁流程;第二步,置控制寄存器的PER(Page Erase)位为1。然后在AR(Address Register)地址寄存器中选择要擦除的页。最后,置控制寄存器的STRT位为1,置STRT为1也是触发条件,STRT为1,芯片开始干活,然后芯片看到PER=1,它就知道接下来要执行页擦除。因为闪存不止一页,页擦除芯片就要知道具体要擦哪一页,所以它会继续看AR寄存器的数据。AR寄存器我们要提前写一个页的起始地址,这样芯片就会把我们指定的一页擦除掉。擦除开始之后我们也需要等待BSY位,最后读出并验证数据就不用看了。这就是页擦除的过程。

最后看一下闪存的写入。擦除之后我们就可以执行写入的流程了,另外说明一下STM32的闪存在写入之前会检查指定地址有没有擦除,如果没有擦除就写入,STM32则不执行写入操作,除非写入的全是0。写入的第一步也是解锁,第二部我们需要置控制寄存器的PG位为1,表示我们即将写入数据,之后第三步就是在指定的地址写入半字,这一步我们就需要用到刚刚介绍的指针写指定地址下寄存器的代码。另外注意一下,写入操作只能以半字的形式写入(16位)。如果你想像SRAM一样随心所欲的读写,那最好的办法就是先把闪存的一页读到SRAM中,读写完成后再擦除一页,整体写回去。回到流程图中,写入数据这个代码就是触发开始的条件,不需要像擦除一样置STRT位了,写入半字之后,芯片会处于忙状态,我们等待一下BSY清零,这样写入数据的过程就完成了,那每执行这样一个流程,只能写入一个半字,如果要写入很多数据,那就不断循环调用这个流程,就可以了。

到这里,内存的解锁、指针读写数据、擦除和编程的流程我们就学完了,接下来我们来看一下选项字节的结构。图里的起始地址就是我们刚才说的选项字节的起始地址1FFF F800,这一块的这些数据就是前面闪存模块组织表中的选择用户字节部分,里面总共只有16个字节,把这些存储器给展开就是下面这个图。这里是对应的16个字节,其中有一半的名称前面都带了个n,比如RDP和nRDP,这个意思就是你在写入RDP数据时,要同时在nRDP写入数据的反码。如果芯片检测到这两个存储器不是反码的关系,那就代表数据无效,有错误,对应的功能就不执行,这是一个安全保障措施。那再看一下每个字节的功能,去掉所有带n的,就剩下8个字节存储器了,第一个RDP(Read Protect),是读保护配置位,在RDP存储器中写入RDPRT键(0x000000A5)后解除读保护,如果RDP不是A5,那闪存就不是读保护状态,无法通过调试器读取程序,避免程序被别人窃取;接下来看第二个字节USER,这个是一些零碎的配置位,可以配置硬件看门狗和进入停机/待机模式是否产生复位;第三个和第四个字节,Data0和Data1,在芯片中没有定义功能,用户可自定义使用,最后四个字节WRP(Write Protect) 0、1、2、3,配置的是写保护,在中容量产品里是每一个位对应保护4个存储页,四个字节总共32位,一位对应保护四页,总共保护32*4=128页,正好对应中容量的最大128页。

那如何去写入选项字节里的这些位呢?

首先是选项字节擦除的编程步骤:

1.检查FLASH_SRBSY位,以确认没有其他正在进行的编程操作

2.解锁FLASH_CROPTWRE

3.设置FLASH_CROPTPG位为1

4.写入要编程的半字到指定的地址

5.等待BSY位变为0

6.读出写入的地址并验证数据

第一步其实也是解锁闪存,这里文字并没有写;第二部,这里文字版的流程比前面流程图中多了一步,检查FLASH_SRBSY位,以确认没有其他正在进行的编程操作。这个实际上就是事前等待,但在之前的流程图中并未体现;然后下一步,解锁FLASH_CROPTWRE(Option Write Enable)位,这一步是选项字节的解锁,选项字节还有一个单独的锁,在解锁闪存后,还需要再解锁选项字节的锁,之后才能操作选项字节;解除小锁之后,和之前的擦除类似,先设置CR的OPTER(Option Erase)位为1,表示即将要擦除选项字节;之后设置CR的STRT位为1,触发芯片开始干活,这样芯片就会启动擦除选项字节的工作,之后等待BSY位变为0,擦除选项字节就完成了。

擦除之后就可以看写入了,

1.检查FLASH_SRBSY位,以确认没有其他正在进行的闪存操作

2.解锁FLASH_CROPTWRE

3.设置FLASH_CROPTER位为1

4.设置FLASH_CRSTRT位为1

5.等待BSY位变为0

6.读出被擦除的选择字节并做验证

和普通的闪存写入也差不多,先检测BSY,然后解除小锁,之后设置CR的OPTWRE(Option Programming)位为1,表示即将写入选项字节,再之后写入要编程的半字到指定的地址,这个是指针写入操作,最后等待忙,这样选项字节就完成了。

下面进入程序编写部分,首先介绍一下FLASH库函数中的各个函数。

首先FLASH_SetLatency、FLASH_HalfCycleAccessCmd、FLASH_PrefetchBufferCmd这三个函数是和内核运行代码有关的,不用我们过多了解,也不需要我们调用,所以不用看;FLASH_Unlock显然是用来解锁的,其函数定义就是在KEYR寄存器先写入KEY1再写入KEY2;FLASH_Lock,加锁,它就是把CR寄存器的LOCK位设置为1;FLASH_ErasePage,闪存擦除某一页,参数给一个页的起始地址,函数执行完后指定的一页就被擦除了,返回值是这个操作的完成状态;FLASH_EraseAllPages就是全擦除了;FLASH_ProgramWord和FLASH_ProgramHalfWord就分别是在指定地址写入字和写入半字了。FLASH_ProgramOptionByteData、FLASH_EnableWriteProtection、FLASH_ReadOutProtection、FLASH_UserOptionByteConfig这四个就是选项字节的写入了,选项字节的四个部分分别为自定义的Data0、Data1,写保护,读保护,用户选项的三个配置位就分别用上面四个函数来写入。FLASH_GetUserOptionByte、FLASH_GetWriteProtectionOptionByte、FLASH_GetReadOutProtectionStatus这三个读取的函数就是获取选项字节当前的状态,第一个是获取用户选项的三个配置位、第二个是获取写保护状态、第三个是获取读保护状态,当然还有一个获取自定义的Data0和Data1,这个没给函数,使用指针访问就行了;FLASH_GetPrefetchBufferStatus为获取预取缓冲区状态;最后FLASH_ITConfig,中断使能;FLASH_GetFlagStatus、FLASH_ClearFlag获取标志位和清除标志位;FLASH_GetStatus和FLASH_WaitForLastOperation是获取状态和等待上一次操作。FLASH_WaitForLastOperation就是等待忙,等待BSY为0,但我们刚刚提到的上面那些函数在执行耗时操作时在函数内部就已经调用了等待忙的函数了,所以这个函数并不需要我们单独调用。

 


网站公告

今日签到

点亮在社区的每一天
去签到