本节我们学习的内容是 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 这个数据,大家可以自己读取看看,肯定和这个是不一样的。好,这就是本节程序的现象。
1. FLASH 闪存
接下来,我们看一下本节的内容。首先看一下简介。
1.1 FLASH 简介
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),实现程序的自我更新
刚才说了,我们在存储用户数据时,要避开程序本身,以免破坏程序,但如果,我们就非要修改程序本身,这会发送什么呢?那这就是第二点提到的功能,在程序中编程,利用程序,来修改程序本身,实现程序的自我更新。
这个在程序中编程,就是 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 的基础。
好,简介就是这些,接着我们继续,看一下这个闪存模块的组织。
1.2 闪存模块组织
这个表是中容量产品的闪存分配情况。我们 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 基本结构图
1.3 FLASH 基本结构图
整个闪存分为程序存储器、系统存储器和选项字节三部分。
这里程序存储器以 C8T6 为例,它是 64K 的,所以总共只有 64 页,最后一页的起始地址是 0800 FC00。
左边是闪存存储器接口,手册里还有个名称,闪存编程和擦除控制器(FPEC),大家也知道这两个名称是一个东西就行。然后,这个控制器,就是闪存的管理员,它可以对程序存储器进行擦除和编程,也可以对选项字节进行擦除和编程。当然系统存储器是不能擦除和编程的。
之后选项字节,里面有很大一部分配置位,其实是配置主程序存储器的读写保护的,所以右边画的,写入选项字节可以配置程序存储器的读写保护。当然选项字节还有几个别的配置参数,这个待会儿再讲。
那这就是整个闪存的基本结构,大概就是这样。
接下来我们就来看一下细节问题,如何操作这个控制器 FPEC 来对程序存储器和选项字节进行擦除和编程。
1.4 FLASH 解锁
首先,第一步是 FLASH 解锁。
这个和之前 W25Q64 一样,W25Q64 操作之前需要写使能;这个 FLASH,操作之前需要解锁,目的都是为了防止误操作。
那这里,解锁的方式和之前独立看门狗一样,都是通过在键寄存器写入指定的键值来实现。使用键寄存器的好处就是,更能防止误操作,每一个指令,必须输密码才能完成。通过英文名称也能看出来,键的英文是 KEY,KEY 直译是不是钥匙的意思啊,所以这个更形象的翻译,我们可以把它叫作钥匙寄存器、密钥寄存器。那看一下,首先
- FPEC共有三个键值(也就是 3 把开锁的密钥):
- RDPRT键 = 0x0000 00A5
是解除读保护的密钥。
- KEY1 = 0x4567 0123
- KEY2 = 0xCDEF 89AB
为什么是这些值呢?通过它的值也可以看出来,实际上是随便定义的,只要你定义的不是很简单,不是随便就能把这个锁翘了就行。
继续看,怎么解锁呢?
- 解锁:
- 复位后,FPEC被保护,不能写入FLASH_CR
也就是复位后,FLASH 默认是锁着的。
- 在 FLASH_KEYR 键寄存器中,先写入 KEY1,再写入 KEY2,解锁
我们找到了锁,这个锁是 KEYR 寄存器,怎么解呢?要先用 KEY1 钥匙解,再用 KEY2 钥匙解,最终才能解锁成功。所以这个锁的安全性非常高,有两道锁,即使你程序跑飞了,歪打正着,正好写入了 KEY1,那也难以保证,下一次又歪打正着,写入了 KEY2,所以非人为情况下,基本不可能解锁。
然后第三条,还有进一步的保护措施。
- 错误的操作序列会在下次复位前锁死 FPEC 和 FLASH_CR
也就是它发现有程序在尝试撬锁时,一旦没有先写入 KEY1,再写入 KEY2,整个模块就会完全锁死,除非复位。这就是整个解锁操作,可以看到,安全性非常高。
接着继续看,解锁之后,如何加锁呢?我们操作完成之后,要尽快把 FLASH 重新加锁,以防止意外情况。
- 加锁:
- 设置 FLASH_CR 中的 LOCK 位锁住 FPEC 和 FLASH_CR
这个比较简单。就是控制寄存器里面有个 LOCK 位,我们在这一位写 1,就能重新锁住闪存了。
好,这就是解锁和加锁。
我们操作闪存的第一步,就是解锁;操作完成后,就是加锁,这个我们就清楚了。
接着看下一个知识点。
1.5 使用指针访问存储器
这个地方我们要学习的是如何使用指针访问存储器。
因为 STM32 内部的存储器,是直接挂在总线上的,所以这时,再读写某个存储器,就非常简单了,直接使用 C 语言的指针,来访问即可。那看一下这里的操作。
- 使用指针读指定地址下的存储器:
uint16_t Data = *((__IO uint16_t *)(0x08000000));
其中:
#define __IO volatile
我们需要用到这样一个格式的语句。这句代码什么意思呢,我们来一一分析。
- 我们需要给定要读取存储器的地址。
比如我这里以 0800 0000 为例,我想读取这个地址下的数据,那就把这个地址写到这里。另外这个括号,因为目前里面只有一个数,所以也可以不写,但是如果你要对这个地址进行加减,那就必须加上括号,并在括号里面进行加减,否则运行的优先级会有问题。总之,如果你不敢肯定各个运算符的优先级,那多加点括号,肯定是最保险的。
好,这是第一步,把地址写到这里。
- 在这个地址前面,加上强制类型转换。
这里把这个变量强制转换为了 uint16_t 的指针类型,如果你想以 16 位的方式读出指定地址的数据,那就转换成 uint16_t*;如果你想以 8 位的方式读出来呢,就转换成 uint8_t*;想 32 位,就转为 uint32_t*;想浮点类型,就转为 float* 或者 double*,这个根据你的读取形式来。
然后这个指针类型前面,还加了个 __IO,在 STM32 库函数中,这是一个宏定义。下面写了,这个宏定义,对应 C 语言的关键字,volatile,volatile 直译就是易变的数据,在这个数据类型前面加上 volatile,是一个安全保障措施,在程序逻辑上,没有作用,加上这个关键字的目的,用一句话来说,就是防止编译器优化。
首先说一下,Keil 编译器默认情况下是最低优化等级,这时,加不加这个 volatile,都没有影响;如果你要提高编译器优化等级,这时就会有这个问题了,那编译器优化有什么用呢,用途就是,可以去除无用的繁杂代码,降低代码空间,提升运行效率,但优化之后,编译器在某些地方可能会弄巧成拙。比如你想用变量计数空循环的方式实现延时函数,把编译器优化的时候,可能会说你这段延时函数好像没用啊,还白白浪费时间,我直接给你优化掉,不让你浪费时间了,这就弄巧成拙了。因为我们的本意就是靠浪费时间来延时,这时,我们就可以在延时的变量定义前面,加上 volatile,告诉编译器,我无论对这个变量干什么,你都原封不动的去执行,别给我优化掉了。
另外,编译器还会利用缓存来加速代码,比如如果你要频繁读写内存的某个变量,那最常见的优化方式就是先把变量转移到高速缓存里来,在 STM32 内核里,有一个类似缓存的工作组寄存器,这些寄存器的访问速度最快,我先把变量放在缓存里,需要读写的时候,直接访问缓存就行了,用完之后,再写回内存,这是一个优化方案。但是,如果你的程序里有多个线程,比如中断函数,在中断函数里,你改变了这个原始变量,那可能缓存并不知道你更改了,下次程序还看缓存的变量,就会造成数据更改不同步的问题,这时,我们的做法也是,读取变量定义的前面,加上一个 volatile,告诉编译器这个变量是易变的,每次读取你都得执行到位,要直接从内存里找,不要再用缓存优化了。
所以总结一下就是,如果开启了编译器优化,在无意义加减变量,多线程更改变量,读写与硬件相关的存储器时,都需要加上 volatile,防止被编译器优化。如果你默认,不开启编译器优化,那就无所谓了,加不加都一样。
所以这里,我们要直接读取存储器,为了严谨,可以加上 volatile,告诉编译器,我要直接了当的读取指定存储器,不要给我优化,或者绕弯子。
好,第二步完成之后,这最外层括号里面的部分,就是一个指针变量,并且这个指针,已经指向了 0x0800 0000 这个位置。然后最后一步,就是
- 使用 * 号,指针取内容。
把这个指针指向的存储器取出来了。这个值,就是指定存储器的值,取出来之后,我们可以把它赋值给自定义的变量 Data,这样就完成了指定地址读的任务了。另外说一下,对于闪存的读取来说,是不需要进行解锁的,因为读取只是看看寄存器,不对寄存器进行更改,所需权限很低,不用解锁,直接就能读。
那接下来继续看
- 使用指针写指定地址下的存储器:
*((__IO uint16_t *)(0x08000000)) = 0x1234;
其中:
#define __IO volatile
这个语句的意思就很明显了。左边和上面一样,先给定地址,再强转为指针,最后指针取内容,这样就是指定地址的值。我们直接对它赋值,比如 0x1234,这样就能完成指定地址写的功能了。
另外这里也有注意事项,因为这个语句是写入数据,并且指定的是闪存的地址,闪存在程序运行时,是只读的,不能轻易更改,而我们本节需要对闪存进行更改,这个所需的权限就比较高,需要提前解锁,并且还有套个程序存储器编程的流程,这个待会再说。
那如果你这个地址写的是 SRAM 的地址,比如 0x2000 0000,那就可以直接写入了。因为 SRAM 在程序运行时是可读可写的,好,这就是使用指针访问存储器的 C 语言代码。
其中,读取,可以直接读;写入,需要解锁,并且执行后面的流程。
那我们接下来,就来看一下下面这三个流程图。那首先说一下这些详细的流程,库函数已经帮我们都写好了,我们直接调用一个整体的函数就行,非常简单。这里我们只大概的了解一下详细步骤,研究的越深,操作的越得心应手。那我们从下往上看,先看擦除,再看编程。首先看一下全擦除。
第三个是全擦除。
把所有页,都给擦除掉。
- 读取 LOCK 位,看一下芯片锁没锁。如果 LOCK 位 = 1,R锁住了,就执行解锁过程,解锁过程就是在 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,跳出循环,这样全擦除过程就结束了。
- 最后一步,这里写的是,读出并验证所有页的数据,这个是测试程序才要做的,正常情况下,全擦除完成了,我们默认就是成功了。如果还要再全读出来验证一下,这个工作量太大了,所以这里的最后一步,我们就不管了。
这就是全擦除的流程。然后我们看一下页擦除。
第二个是页擦除。
STM32 的闪存也是写入前必须擦除。擦除之后,所有的数据位变为 1,擦除的最小单位就是一页,1K,1024 字节。
这个也是类似的过程。
- 方框上这一块,一样的,是解锁的流程。
- 这个方框里的,置控制寄存器的 PER(Page Erase)位为 1,然后在 AR(Address Register)地址寄存器中选择要擦除的页,最后,置控制寄存器的 STRT 位为 1,置 STRT 为 1,也是触发条件,STRT 为 1,芯片开始干活,然后芯片看到,PER = 1,它就直到,接下来要执行页擦除,然后闪存不止一页,页擦除,芯片就要知道要具体擦哪一页,所以,它会继续看 AR 寄存器的数据,AR 寄存器我们要提前写入一个页的起始地址,这样芯片就会把我们指定的一页,给擦除掉。
- 然后擦除开始之后,我们也需要等待 BSY 位。
- 最后,读出并验证数据,这个就不用看了。
这就是页擦除的过程。最后,看一下闪存的写入,擦除之后,我们就可以执行写入的流程了。
第一个是编程,也就是写入。
另外说明一下,STM32 的闪存再写入之前会检查指定地址有没有擦除,如果没有擦除就写入,STM32 则不执行写入操作,除非写入的全是 0,这一个数据是例外,因为不擦除就写入,可能会写入错误,但全写入 0 的话,写入肯定是没问题的。
那看一下流程图。
- 写入的第一步,也是解锁。
- 然后第二步,我们需要置控制寄存器的 PG(Programming)位为 1,表示我们即将写入数据。
- 之后第三步,就是在指定的地址写入半字,这一步,我们需要用到刚才说的
*((__IO uint16_t *)(0x08000000)) = 0x1234;
这句代码,使用指针,在指定地址写入数据,想写入什么数据,在这里指定即可。另外这里注意下,写入操作,只能以半字的形式写入。在 STM32 中,有几个术语,字、半字和字节,其中字,Word,就是 32 位数据;半字,HalfWord,就是 16 位数据;字节,Byte,就是 8 位数据,这个了解一下。那这里只能以半字写入,意思就是只能以 16 位的形式写入,一次性,写入两个字节;如果你要写入 32 位,就分两次完成;如果你只要写入 8 位,这个就比较麻烦了,如果你想单独写入一个字节,还要保留另一个字节的原始数据的话,那就只能把整页数据都读到 SRAM,再随意修改 SRAM 数据,修改全部完成之后,再把整页都擦除,最后再把整页都写回去。所以,如果你想像 SRAM 一样随心所欲地读写,那最好的办法就是先把闪存的一页读到 SRAM 中,读写完成后,再擦除一页,整体写回去。那回到流程图这里,写入数据这个代码,就是触发开始的条件,不需要像擦除一样,置 STRT 位了。 - 写入半字之后,芯片会处于忙状态,我们等待一下 BSY 清零,这样写入数据的过程就完成了。
那每执行这样一个流程,只能写入一个半字,如果要写入很多数据,那就不断循环调用这个流程,就可以了。
好,到这里。闪存的解锁、指针读写数据、擦除和编程的流程,我们就学完了。
接下里我们再介绍一下选项字节。
1.6 选项字节
这一块内容,大概了解一下就行。
RDP:写入RDPRT键(0x000000A5)后解除读保护
USER:配置硬件看门狗和进入停机/待机模式是否产生复位
Data0/1:用户可自定义使用
WRP0/1/2/3:配置写保护,每一个位对应保护4个存储页(中容量)
首先这里是选项字节的组织和用途。图里的起始地址,就是我们刚才说的选项字节的起始地址 1FFF F800,这一块的这些数据,就是前面闪存模块组织这个表的用户选择字节这一行,里面总共只有 16 个字节,把这些存储器给展开,就是这个图。
这里是对应的 16 个字节,其中有一半的名称,前面都带了个 n,比如 RDP 和 nRDP,USER 和 nUSER,等等,这个意思就是你在写入 RDP 数据时,要同时在 nRDP 写入数据的反码,其他的这些都是一样,写入这个存储器时,要在带 n 的对应的存储器写入反码,这样写入操作才是有效的,如果芯片检测到这两个存储器不是反码的关系,那就代表数据无效,有错误,对应的功能就不执行,这是一个安全保障措施。
当然这个写入反码的过程,硬件会自动计算,并写入,不需要我们操心,使用库函数的话,那就更简单了,函数都给我们封装好了,直接调用函数就行。
那然后看一下每个存储器的功能,去掉所有带 n 的,就剩下 8 个字节存储器了。
- 第一个 RDP(Read Protect),是读保护配置位,下面有解释,在 RDP 寄存器写入 RDPRT 键,就是刚才说的 A5,然后解除读保护;如果 RDP 不是 A5,那闪存就是读保护状态,无法通过调试器读取程序,避免程序被别人窃取。
- 接着看第二个字节 USER,这个是一些零碎的配置位,可以配置硬件看门狗和进入停机/待机模式是否产生复位,这个了解即可。
- 然后第三个和第四个字节,Data0 和 Data1,这个在芯片中没有定义功能,用户可自定义使用。
- 最后四个字节,WRP(Write Protect)0、1、2、3,这四个字节,配置的是写保护。
在中容量产品里,是每一个位对应保护 4 个存储页,4 个字节,总共 32 位,一位对应保护 4 页,总共保护 32*4 = 128 页,正好对应中容量的最大 128 页。
那对于小容量和大容量产品呢?可以看一下手册,2.5 选项字节说明这里,对于小容量产品,也是每一位对应保护 4 个存储页,但是小容量产品最大只有 32K,所以只需要一个字节 WRP0 就行,4*8 = 32,其他 3 个字节没用到。然后对于大容量产品,每一个位只能保护 2 个存储页,这样的话 4 个字节就不够用了,所以这里规定 WRP3 的最高位,这一位直接把剩下的所有页一起都保护了,这是写保护的定义。
这样选项字节,有哪些东西,都是干啥的,我们就清楚了,然后看一下,如何去写入这些位呢?
接下来展示的就是选项字节的擦除和编程。因为选项字节本身也是闪存,所以它也得擦除,这里,参考手册并没有给流程图,我们看一下文字流程。
这个文字流程和流程图细节上有一些出入,我们知道关键部分就行,先看一下选项字节擦除。
1.6.1 选项字节的擦除
- 检查FLASH_SR的BSY位,以确认没有其他正在进行的闪存操作
- 解锁FLASH_CR的OPTWRE位
- 设置FLASH_CR的OPTER位为1
- 设置FLASH_CR的STRT位为1
- 等待BSY位变为0
- 读出被擦除的选择字节并做验证
第一步,其实也是解锁闪存,这里文字并没有写。
然后第二部,这里文字版的流程,多了一步,检查 SR 的 BSY 位,以确认没有其他正在进行的闪存操作,这个实际上是事前等待,如果当前已经在忙了,我先等一下,这一步在刚才的流程图里并没有体现。
然后下一步,解锁 CR 的 OPTWRE(Option Write Enable)位,这一步是选项字节的解锁,选项字节里面还有一个单独的锁,在解锁闪存后,还需要再解锁选项字节的锁,之后才能操作选项字节。解锁选项字节的话,看一下闪存模块组织的寄存器,整个闪存的锁是 KEYR,里面选项字节的小锁,是下面的 OPTKEYR(Option Key Register),解锁这个小锁,也是类似的流程,我们需要在 OPTKEYR 里,先写入 KEY1,再写入 KEY2,这样就能解锁选项字节的小锁了。
然后继续,解除小锁之后,和之前的擦除类似,先设置 CR 的 OPTER(Option Erase)位为 1,表示即将要擦除选项字节。
之后设置 CR 的 STRT 位为 1,触发芯片,开始干活,这样芯片就会启动擦除选项字节的工作。
之后,等待 BSY 位变为 0,擦除选项字节就完成了。
擦除之后,就可以看写入了。
1.6.2 选项字节的编程
- 检查FLASH_SR的BSY位,以确认没有其他正在进行的编程操作
- 解锁FLASH_CR的OPTWRE位
- 设置FLASH_CR的OPTPG位为1
- 写入要编程的半字到指定的地址
- 等待BSY位变为0
- 读出写入的地址并验证数据
和普通的闪存写入也差不多,先检测 BSY;然后解除小锁;之后设置 CR 的 OPTPG(Option Programming)位为 1,表示即将写入选项字节;再之后,写入要编程的半字到指定的地址,这个是指针写入操作;最后,等待忙。这样写入选项字节就完成了。
好,那有关选项字节的内容,就介绍这么多。至此,闪存的整个的介绍,我们就介绍完了。
最后还是大概看一下手册。
首先看一下闪存编程手册。
最开始这里,是 ICP 和 IAP 的一些介绍,对闪存编程,实现 IAP,在做产品时,还是一个非常方便的功能,有利于便捷的进行程序升级。
然后往后看,这里有一些术语,小容量、中容量、大容量是什么意思;字、半字、字节的定义,等等名词,在这里有介绍。
然后是概述,这里闪存模块组织,是一个比较重要的内容,要读写闪存,首先你得知道它是怎么分配的,这里,小容量、中容量、大容量的分配方式,都在这个表里体现出来了。然后下面是一些说明文字,比如,正在执行写或擦除操作的时候,不能同时进行读取,然后写或擦除时,必须打开 HSI 时钟,这个 SystemInit 第一步就已经打开了,不要去动它就行。
然后下面读操作这里,这一页是涉及内核和 CPU 运行的内容,理解起来还是有点抽象的,大家可以不用过多关心,也不需要我们自己干什么的。比如这个预取缓冲器,可以提起读取程序来加快代码执行,还有主频比较低的时候,可以开启半周期访问加快代码执行,这些东西随便看看,不用过多了解。
然后就是闪存编程和擦除控制器 FPEC 了。键值和解锁,这些我们都介绍过。然后主闪存编程,里面有些说明,比如一次只能写入一个半字,任何非半字的数据,都会产生总线错误。
还有一个比较重要的注意事项就是:在编程过程中,任何读写闪存的操作都会使 CPU 暂停,直到此次闪存编程结束。这其实是读写内部闪存存储数据的一个弊端,就是闪存忙的时候,代码执行会暂停,因为执行代码需要读闪存,闪存在忙,没法读,所以 CPU 也就没法运行了,程序就会暂停,这会导致什么问题呢?假如你使用内部闪存存储数据,同时你的中断代码又是在频繁执行的,这样,读写闪存的时候,中断代码就无法执行了,这可能会导致中断无法及时响应。比如一个项目,STM32 驱动一个点阵屏,这个点阵屏需要用定时器中断,不断地去扫描刷新,否则屏幕就不会亮,同时程序里,我又使用了内部闪存来存储一些配置参数,然后测试的时候就会出现一个问题,就是一旦内部闪存进行读写,整个屏幕就会快速的闪一下,这个虽然不是大问题,但非常影响用户体验,问题的原因就是,读写闪存会导致中断扫描点阵的代码暂停,扫描暂停,屏幕就会闪一下,所以最终只能放弃内部闪存存储数据了。这是闪存存储的一大弊端,如果你的程序里有需要频繁执行,且对时间要求严格的中断函数,那就要慎用这个内部闪存来存储用户数据,这是这个注意事项。
然后继续看是编程过程。流程也介绍过,有个注意事项就是如果指定地址没有擦除,那就不会执行编程,同时提出警告,唯一的例外是写入 0000,这个没问题。然后如果指定地址为写保护,也是不执行编程,并提出警告。之后是文字版的流程。
然后闪存页擦除,文字版的流程和下面的流程图。整片擦除,文字版的流程和流程图,这些我们也介绍过,其中有个说明是,整片擦除,信息块的内容不受影响。
接下来就是选项字节的编程,选项字节,在 FPEC 解锁后,还要再在 OPTKEYR,写入 KEY1 和 KEY2,解锁单独的小锁。然后下面写的是,FPEC 会自动计算高字节的反码,所以反码不用我们操心。然后编程文字版的流程,我们介绍过,这里有个说明是,当闪存由保护变为未保护时,会自动执行整片擦除,防止代码被盗,之后擦除过程,我们介绍过。
再之后,就是保护相关的内容了,写保护和读保护,可以再自己看看这些说明。
然后选项字节的组织和每个字节的定义,这一块可以详细看看,我们也介绍过。当然有个注意事项就是这里的位,都使用的是反逻辑,1 表示无效,0 表示有效,因为闪存擦除之后都是 1,所以 1 会用来作为默认情况,比如这里的写保护,1,是默认的不实施写保护,而 0,才是实施写保护,这个注意一下。
最后,就是寄存器说明了。第一个闪存访问控制寄存器,这个是和内核执行代码有关的东西,不用了解。之后 FPEC 寄存器和 OPT 键寄存器,分别是闪存锁和选项字节的小锁,写入 KEY1,再写 KEY2,解锁。状态寄存器,表示电路的工作状态,重点了解的就是这个 BSY,忙标志位。控制寄存器,用于控制电路运行,重点了解这个 LOCK,加锁,STRT,开始,OPTER,选项字节擦除,OPTPG,选项字节编程,MER,全擦除,PER,页擦除,PG,编程,这些位,我们之前都介绍过。之后地址寄存器,这个是配合页擦除,指定擦除哪一页的。然后选项字节寄存器,这些寄存器会把闪存里的选项字节的内容加载进来,里面的内容和选项字节是对应的,这个也是一样。
最后就是寄存器总表了。好,这就是闪存的手册内容,大家可以再自己仔细看看。
最后,我们学一下器件电子签名。这个非常简单。既然介绍到闪存了,就顺便学习一下吧。
2. 器件电子签名
电子签名存放在闪存存储器模块的系统存储区域,包含的芯片识别信息在出厂时编写,不可更改,使用指针读指定地址下的存储器可获取电子签名。
电子签名,其实就是 STM32 的 ID 号,它的存放区域是系统存储器,就是 FLASH 基本结构图中的系统存储器。它不仅有 BootLoader 程序,还有几个字节的 ID 号,系统存储器,起始地址是 1FFF F000。
看一下这里,这里有两段数据:
- 闪存容量寄存器:
- 基地址:0x1FFF F7E0
- 大小:16位
通过地址,也可以确定,它的位置,就是系统存储器。它的值就是闪存的容量,单位是 KB。
- 产品唯一身份标识寄存器:
- 基地址: 0x1FFF F7E8
- 大小:96位
也就是每个芯片的身份证号。每一个芯片的这 96 位数据,都是不一样的,使用这个唯一 ID 号,可以做一些加密的操作。比如你想写入一段程序,只能在指定设备运行,那就可以在程序的多处加入 ID 号判断,如果不是指定设备的 ID 号,就不执行程序功能,这样即使你的程序被盗,在别的设备上也难以运行。
这就是 STM32 的电子签名。总共就是这么多内容,非常简单。我们也顺便学一下。好,到这里,我们本节的内容,差不多就介绍完了。
然后还有一小点内容,是器件电子签名的手册。这个看一下参考手册,第 28 章,器件电子签名,这个内容非常少。
上面这里写了,电子签名是出厂编写的,包含芯片识别信息。之后直接是寄存器,闪存容量寄存器,基地址,也就是起始地址,指示了这个数据存在什么地方了,里面有 16 个只读位,表示闪存容量。然后产品唯一身份标识寄存器,有什么用呢?比如,作为序列号,作为密码,用来激活带安全机制的自举过程,这 96 位,可以以字节为单位读取,也可以以半字或全字来读取,然后基地址是 0x1FFF F7E8,下面就是所有位的内容了,就是一串 ID 号数据,这就是器件电子签名的内容。
那到这里,本小节的内容就全部结束了,我们下一小节,来学习闪存的代码部分。