本节我们学习的内容是 STM32 的 FLASH,闪存。
当然闪存是一个通用的名词,表示的是一种非易失性,也就是掉电不丢失的存储器。比如,我们之前学习 SPI 的时候,用的 W25Q64 芯片,就是一种闪存存储器芯片。
而本节,我们所说的闪存,则特指 STM32 的内部闪存,也就是我们下载程序的时候,这个程序所存储的地方。我们下载的程序掉电后肯定不会消失,这说明程序存储在了一个非易失性的存储器中,这个存储器,也是一种闪存,那我们本节,就来学习一下,如何利用程序,来读写存储程序的这个存储器。
那开始本节之前,我们还是看一下本节代码的现象。本节课主要有两个代码,第一个是读写内部 FLASH,第二个是读取芯片 ID。
先看一下第一个,这个代码的目的就是,利用内部 FLASH 程序存储器的剩余空间,来存储一些掉电不丢失的参数,如果你有一些配置参数,需要掉电不丢失的保存,再外挂一个存储器芯片的话,显然会增加硬件成本;那 STM32 本身不就是有掉电不丢失的程序存储器吗,我们直接把参数存在这里,是不是就又方便、又节省成本啊。
所以这里的程序是,按下 Key1,变换一下测试数据,然后存储到内部 FLASH 里,按下 Key2,把所有参数清零,最后 OLED 显示一下。
我们下载看一下,这里 OLED 显示了 Flag 和 Data,Flag 当作标志位,内容随便定义,这里定义的是 A5A5,Flag 的作用就是判断一下之前是不是存储过数据,如果存储过数据,就直接读取,如果没存储过,就先初始化一下,这个思路和之前 RTC 的那一节的代码是一样的。然后下面的 Data 就是要掉电存储的数据了,我们按下 Key1,可以变换一下测试数据,每变换一次,这个数据就更新存储到 FLASH 里了。比如现在,4 个数据分别是 5、A、F、14,我们直接把整个芯片断电,再重新上电,可以看到,数据仍然存在,和之前保存的一样;继续变换几次呢,再断电重启,可以看到,数据也还是存在的;继续测试,按复位键,可以看到,这个数据也不会丢失。那为了能清零这个数据呢,我们可以按 Key2 按键,这样就把所有参数归零了。这就是利用内部 FLASH 存储掉电不丢失数据的现象。
可以看到,整个电路,我不需要外挂任何存储芯片,在电路上,也不需要有任何新增设备,所以利用内部 FLASH 实现这个功能,是一个非常灵活和节省的方案,这就是第一个代码的现象。
然后继续看第二个代码,读取芯片 ID。这个代码非常简单,就顺便讲一下。那在 STM32 里,指定的一些地址下,存储的有原厂写入的 ID 号,我们直接使用指针的操作方式读取,就可以得到 ID 号了。
那下载看一下现象。可以看到,这里有两个 ID 号,第一个是 F_SIZE,表示芯片 FLASH 的容量,0040 表示 FLASH 容量,单位是 KB,换成十进制就是 64 KB,当然有的芯片 FLASH 容量会大于 64K B,这个数据有些不一样,也是正常的;然后第二个是 U_ID,是 96 位的芯片唯一 ID 号,每个芯片的唯一 ID 号都不一样,目前这个芯片读取出来是 FF32 066E 这个数据,大家可以自己读取看看,肯定和这个是不一样的。好,这就是本节程序的现象。
接下来,我们看一下本节的内容。首先看一下简介。
STM32F1 系列的 FLASH 包含程序存储器、系统存储器和选项字节三个部分,通过闪存存储器接口(外设)可以对程序存储器和选项字节进行擦除和编程
那首先,FLASH 包含程序存储器、系统存储器和选项字节三个部分,这个我们之前介绍过。我们回顾一下,在 DMA 这一节我们介绍过存储器映像,STM32 内部的存储空间主要有这些部分,其中 ROM 区,就是掉电不丢失的,存储介质是 FLASH 闪存;RAM 区,掉电丢失,存储介质是 SRAM。闪存主要有程序存储器、系统存储器和选项字节 3 个部分,这就是我们本节要学习的内容。
- 其中程序存储器是这三者之中,空间最大、最主要的部分,所以也称作主存储器,起始地址是 0800 开头的,用途是存储程序代码。系统存储器起始地址是 1FFF F000,用途是存储 BootLoader,用于串口下载。选项字节起始地址是 1FFF F800,用途是存储一些独立的配置参数。
- 然后下面的地址也看一下,运行内存的起始地址是 2000 开头的;外设寄存器的起始地址是 4000;内核外设寄存器的起始地址是 E000。
- 这些起始地址要记一下,要做到,当你看到一个存储器的地址时,一眼就能知道它处于什么区域,有什么特性,大概是做什么的,这是这一块内容,我们再回顾一下,等会儿还会用到的。
接着回到本节继续看,FLASH 包括这三部分,我们本节的任务,就是对这些存储器进行读写,那我们怎么操作这些存储器呢?就需要用到这个闪存存储器接口了,闪存存储器接口是一个外设,是这个闪存的管理员,毕竟闪存的操作很麻烦,涉及到擦除、编程、等待忙、解锁等等操作,所以这里,我们需要把我们的指令和数据,写入到这个外设的相应寄存器,然后这个外设就会自动去操作对应的存储空间。
那后面写的是,这个外设可以对程序存储器和选项字节,这两部分,进行擦除和编程,对比上面的三个部分呢,少了系统存储器这个区域,因为系统存储器是原厂写入的 BootLoader 程序,这个是不允许我们修改的。
读写FLASH的用途:
像我们刚才演示的代码,就是这个用法。对于我们这个 C8T6 芯片来说,它的程序存储器容量是 64K,一般我们写个简单的程序,可能就只占前面的很小一部分空间,剩下的大片空余空间,我们就可以加以利用。比如存储一些我们自定义的数据,这样就非常方便,而且可以充分利用资源。不过这里要注意,我们在选取存储区域时,一定不要覆盖了原有的程序,要不然程序自己把自己给破坏了,之后程序就运行不了了。
一般存储少量的参数,我们就选最后几页存储就行了。关于如何查看程序所占用空间的大小,这个我们下一小节也会介绍,那这就是第一个用途。
刚才说了,我们在存储用户数据时,要避开程序本身,以免破坏程序,但如果,我们就非要修改程序本身,这会发送什么呢?那这就是第二点提到的功能,在程序中编程,利用程序,来修改程序本身,实现程序的自我更新。
这个在程序中编程,就是 IAP,在数码圈,也有个可能大家更熟悉的技术,叫 OTA,这两是类似的东西,都是用来实现程序升级的。当然这个 IAP 升级程序的功能比较复杂,我们本节暂时就不涉及了。
在线编程(In-Circuit Programming – ICP)用于更新程序存储器的全部内容,它通过JTAG、SWD协议或系统加载程序(Bootloader)下载程序
英文直译过来叫在电路中编程,意思就是下载程序你只需要留几个引脚就行,不用拆芯片了,就叫在电路中进行编程。
这个 JTAG、SWD,就是仿真器下载程序,就是我们目前用的 STLINK 使用 SWD 下载程序,每次下载,都是把整个程序完全更新掉。那系统加载程序,就是系统存储器的 BootLoader,也就是串口下载,串口下载,也是更新整个程序。这就是我们一直在用的 ICP 下载方式。
之后,更高级的下载方式,就是在程序中编程。
在程序中编程(In-Application Programming – IAP)可以使用微控制器支持的任一种通信接口下载程序
怎么实现呢?我们首先需要自己写一个 BootLoader 程序,并且存放在程序更新时,不会覆盖的地方,比如我们放在整个程序存储器的后面。然后,需要更新程序时,我们控制程序跳转到这个自己写的 BootLoader 里来,在这里面,我们就可以接收任意一种通信接口传过来的数据,比如串口、USB、蓝牙转串口、WIFI 转串口等等,这个传过来的数据,就是待更新的程序,然后我们控制 FLASH 读写,把收到的程序,写入到整个程序存储器的前面程序正常运行的地方,写完之后,再控制程序跳转回正常运行的地方,或者直接复位,这样程序就完成了自我升级。
这个过程,其实就是和系统存储器这个的 BootLoader 一样,因为程序要实现自我升级,在升级过程中,肯定需要布置一个辅助的小机器人来临时干活。只不过是系统寄存器的 BootLoader 写死了,只能用串口下载到指定位置,启动方式也不方便,只能配置 BOOT 引脚触发启动;而我们自己写 BootLoader 的话,就可以想怎么收怎么收,想写到哪就写到哪,想怎么启动就怎么启动,并且这整个升级过程,程序都可以自主完成,实现在程序中编程。更进一步,就可以直接实现远程升级了,非常灵活方便。
那有关 IAP 的内容,我就介绍这么多,更进一步的内容,大家再自己研究。
接下来的内容,我们就只涉及最基本的对 FLASH 进行读写,这也是实现 IAP 的基础。
好,简介就是这些,接着我们继续,看一下这个闪存模块的组织。
这个表是中容量产品的闪存分配情况。我们 C8T6 芯片的闪存容量是 64K,属于中容量产品。对于小容量产品和大容量产品,闪存的分配方式有些区别,这个可以参考一下手册。
那首先提醒一下,闪存这一章的内容,在手册里是单独列出来的,并不在之前的参考手册里。我们需要打开闪存编程参考手册,打开之后,可以看到,这个文档也不是很多,其实就是单独一章的内容,这个注意一下,别找错位置了。
那我们打开 1.2 这一节,这里就是闪存模块组织。首先是小容量产品,这个页数少一些,总共 32 页,每页 1K;然后中容量产品,页数多一些,总共 128 页,每页 1K;最后大容量产品,页数更多,总共 256 页,并且每页容量也更大,是 2K。这是小中大容量产品闪存分配方式的不同,其他地方,基本都是一样的。
那回到本节,这里以中容量产品为例来讲解。首先看一下第一列的几个块,这里分为了 3 个块,第一个是主存储器,也就是我们刚才说的程序存储器,用来存放程序代码的,这是最主要,也是容量最大的一块。第二个是信息块,里面又可以分为启动程序代码和用户选择字节,其中启动程序代码就是刚才说的系统存储器,存放的是原厂写入的 BootLoader,用于串口下载,这个手册的名称经常会有不同的表述方式,但是大家要知道,某些名称,描述的其实是一个东西;然后下面这个用户选择字节,也就是刚才说的选项字节,存放一些独立的参数,这个选项字节,在手册里一直都称作选择字节,可能是翻译的问题,英文是 Option Bytes,我们一般都叫选项字节,大家也知道是一个东西就行。然后最后一块,是闪存存储器接口寄存器,这一块的存储器,实际上并不属于闪存,你看它的地址就知道,地址都是 40 开头的,说明这个存储器接口寄存器就是一个普通的外设,和之前介绍的 GPIO、定时器、串口等等,都是一个性质的东西,这些存储器,它们的存储介质,也都是 SRAM。这个闪存存储器接口,就是上面这些闪存的管理员,这些寄存器,就是用来控制擦除和编程这个过程的。
那到这里,这个表的整体我们就清楚了,上面是真正的闪存,分为三部分,主存储器(就是程序存储器),启动程序代码(就是系统存储器),用户选择字节(就是选项字节)。其中系统存储器和选项字节,又可以合称为信息块,这一点,和刚才 FLASH 简介里介绍的闪存分为三部分是对应的,没问题。
然后下面一部分,是闪存的管理员,我们擦除和编程,就通过读写这些寄存器来完成,这一点,和刚才 FLASH 简介里介绍的闪存存储器接口可以对闪存进行擦除和编程是对应的,当然这里只有擦除和编程,并没有读取,这是因为,读取指定存储器,直接使用指针读即可,用不到这个外设。
好,那我们继续看这个表。对于主存储器,这里对它进行了分页,分页是为了更好的管理闪存。擦除和写保护,都是以页为单位的,这一点和之前 W25Q64 那一节的闪存一样,同为闪存,它们的特性基本一样,写入前必须擦除,擦除必须以最小单位进行,擦除后数据位全变为 1,数据只能 1 写 0,不能 0 写 1,擦除和写入之后都需要等待忙啊,这些都是一样的。学习这节之前,大家可以再复习一下 W25Q64 那一节,相信你学过 W25Q64 之后,再学这一节,就会非常轻松了。那 W25Q64 的分配方式是,先分为块 Block,再分为扇区 Sector,比较复杂;这里,就比较简单了,它只有一个基本单位,就是页,每页的大小都是 1K,0~127,总共 128 页,总容量就是 128K,对于 C8T6 来说,它只有 64K,所以 C8T6 的页只有一半,0~63,总共 64 页,共 64K。
然后看一下页的地址范围,第一个页的起始地址就是程序存储器的起始地址,0x0800 0000,之后就是一个字节一个地址,依次线性分配了。看一下每页起始地址的规律,首先是 0000,然后 0400、0800、0C00,再之后,1000,后面按照规律,就是 1400、1800、1C00、2000、2400、2800、2C00 等等等等,最后一直到 1 FC00,所以地址只要以 000、400、800、C00 结尾的都一定是页的起始地址,这个稍微记一下。之后如果想要给一个页的起始地址,就需要用到这个规律。
然后继续,系统存储器它的起始地址是 0x1FFF F000,这个之前介绍过的,它的容量是 2K,这个就不用多说了。
在下面,选项字节,起始地址是 0x1FFF F800,容量是 16 个字节,里面只有几个字节的配置参数,这个后面还会继续说的。
那这里还可以发现,我们平时说的,芯片闪存容量是 64K、128K,它指的只是主存储器的容量,下面信息块的两个东西,虽然也是闪存,但是并不统计在这个容量里,这就是闪存的分配方式。
那最后,就是这个闪存接口寄存器了。里面包括 KEYR 键寄存器,SR 状态寄存器,CR 控制寄存器等等,外设的起始地址是 0x4002 2000,每个寄存器都是 4 个字节,也就是 32 位,这就是这个外设的寄存器。
好,按这个表就看到这里。
接下来看一下总结的 FLASH 基本结构图