首页 关于
树枝想去撕裂天空 / 却只戳了几个微小的窟窿 / 它透出天外的光亮 / 人们把它叫做月亮和星星
目录

片上闪存保存参数

我们知道存储设备大体上可以分为两大类,掉电保存和掉电丢失两类。ROM(Read Only Memory)就是常见的掉电保存的设备,虽然叫做Read Only,这类设备也不是完全就不能够写。 有些开发板上会有一个EEPROM的设备,前缀EEP表示的是电可擦除的ROM,也就是说经过特定的手段也是可以写的,可能比较慢而已。闪存(FLASH)也是一类掉电保存设备, 因为具有较快的访问速度,而且可以比较方便的做成比较大的容量,所以使用的比较广泛。STM32F407就有一个最多1M的片上闪存, 它主要用来保存我们烧录的代码。

RAM(Random Access Memory)是常见的掉电丢失的设备。它的特点就是系统掉电之后原先保存的数据就丢失了,但是具有比较快的读写访问速度, 所以我们程序中用到的变量都是在这个设备上存储的。因为RAM的数据掉电后丢失了,上电后的数据则是随机的,所以写程序的时候才会要求人们为变量赋予初值。 但是很多时候我们需要保存一些变量供下次上电时使用,比如说系统的一些配置参数,工作状态等。有些参数已经设定就不用再变更了,这样的我们可以在程序中用一个宏定义, 或者常量解决,它们最终会被烧录到片上的FLASH上。有些参数可能需要根据具体情况设定,比如说电机驱动针对不同的电机其控制率都是不一样的, 仍然采用硬编码的形式系统的灵活性就下降很多。

为解决上述问题,我们可以选择在系统中装一个EEPROM或者SD卡什么的保存这些数据,需要修改时再重新写好了。但很多时候这类参数很少,可能就只有几个字节, 再为了这样的需求添加一个器件就显得很浪费了,而且也要额外付出很多工作量。本文的主题就是为了解决这类问题而存在的,我们可以把这些参数保存到片上闪存空间中。

1. STM32F407的片上闪存

在STM32F407的芯片上,从0x08000000开始最长为1M字节的地址空间就是片上闪存的,探索者开发板所用的STM32F407ZET6的闪存大小是512K字节。 这1M的空间又被分成了4个16K、1个64K和7个128K字节的sector,它们可以被整体擦写(也就是所谓的mess erase),也可以按照sector擦写(即sector erase)。 下表1是各个sector及其对应的地址。

为管理片上闪存,STM32提供了一个称为Embeded Flash Memory Interface的接口,它用于管理I-Code和D-Code总线访问片上闪存,提供了擦除和烧录Flash的功能, 以及读写保护机制。此外,对I-Code总线的指令预取(instruction prefetch)功能可以提高系统的性能。它支持128位宽的数据读入功能,支持字节(8位)、半字(16位)、 全字(32位)、双字(64位)的写入功能。

表1 STM32F407的1M字节Flash空间

Sector地址空间容量
Sector 10x0800 0000 - 0x0800 3FFF16K Bytes
Sector 20x0800 4000 - 0x0800 7FFF16K Bytes
Sector 30x0800 8000 - 0x0800 BFFF16K Bytes
Sector 40x0800 C000 - 0x0800 FFFF16K Bytes
Sector 50x0801 0000 - 0x0801 FFFF64K Bytes
Sector 60x0802 0000 - 0x0803 FFFF128K Bytes
Sector 60x0804 0000 - 0x0805 FFFF128K Bytes
.........
Sector 110x080E 0000 - 0x080F FFFF128K Bytes

表2 不同电压和CPU时钟下的等待周期

等待周期(WS)
(LATENCY)
CPU频率HCLK(MHz)
电压范围
2.7V-3.6V
电压范围
2.4V-2.7V
电压范围
2.1V-2.4V
电压范围
1.8V-2.1V
0WS(1 CPU周期) 0 < HCLK ≤ 30 0 < HCLK ≤ 24 0 < HCLK ≤ 22 0 < HCLK ≤ 20
1WS(2 CPU周期) 30 < HCLK ≤ 60 24 < HCLK ≤ 48 22 < HCLK ≤ 44 20 < HCLK ≤ 40
2WS(3 CPU周期) 60 < HCLK ≤ 90 48 < HCLK ≤ 72 44 < HCLK ≤ 66 40 < HCLK ≤ 60
3WS(4 CPU周期) 90 < HCLK ≤ 120 72 < HCLK ≤ 96 66 < HCLK ≤ 88 60 < HCLK ≤ 80
4WS(5 CPU周期) 120 < HCLK ≤ 150 96 < HCLK ≤ 120 88 < HCLK ≤ 110 80 < HCLK ≤ 100
5WS(6 CPU周期) 150 < HCLK ≤ 168 120 < HCLK ≤ 144 110 < HCLK ≤ 132 100 < HCLK ≤ 120
6WS(7 CPU周期) 144 < HCLK ≤ 168 132 < HCLK ≤ 154 120 < HCLK ≤ 140
7WS(8 CPU周期) 154 < HCLK ≤ 168 140 < HCLK ≤ 160

为了能够正确的从闪存中读取数据,必须根据CPU时钟(HCLK)和设备供电电压设置闪存访问控制寄存器(Flash Access Control Register,FLASH_ACR), 以调整系统访问闪存的等待状态时间(LATENCY)。上表2列出了不同电压和工作频率下的等待周期。 系统复位后CPU时钟的频率为16MHz,FLASH_ACR默认配置为0WS。

在变更系统的工作频率时,最好通过如下的流程修改访问FLASH的等待周期:
提高CPU频率:

  1. 将新的等待周期写到FLASH_ACR的LATENCY字段中;
  2. 读FLASH_ACR以确定设置是否成功;
  3. 写RCC_CFGR的SW位修改系统时钟源;
  4. 如果有必要,RCC_CFGR的HPRE位配置系统的各种分频系数;
  5. 读RCC_CFGR的SWS和HPRE位确认是否成功地配置了系统时钟源和分频系数。
降低CPU频率:
  1. 写RCC_CFGR的SW位修改系统时钟源;
  2. 如果有必要,RCC_CFGR的HPRE位配置系统的各种分频系数;
  3. 读RCC_CFGR的SWS和HPRE位确认是否成功地配置了系统时钟源和分频系数。
  4. 将新的等待周期写到FLASH_ACR的LATENCY字段中;
  5. 读FLASH_ACR以确定设置是否成功;

2. 访问加速功能

我们知道在访问FLASH中的内容时,不可避免的需要有几个CPU周期的等待过程,这会降低CPU的工作效率。 因此,ST针对带有FPU的Cortex-M4架构的STM32系列ARM处理器设计了一种内存访问加速器(Adaptive Real-Time memory accelerator, ART Accelerator)。 这种加速器通过指令预取、指令缓存、数据缓存机制,提高了系统的工作效率。 根据CoreMark测试,当CPU工作在168MHz的频率下,ART加速器可以得到相当于以0个等待周期执行程序的效果。

每次对Flash的读访问都可以获得128位的指令,这可以是4条32位的指令,也可以是8条16位的指令,具体根据烧录在Flash中的代码决定。 这样至少需要4个CPU周期的时间,才能处理完这128位的指令。

所谓的指令预取功能是指,当CPU请求当前128位指令时,使用I-Code总线的预取操作,同时读取Flash中下一个连续存放的128位指令。 那么在执行非分支语句时,除了执行第一条语句时需要等待以外,以后将不再需要等待周期。 但是对于分支语句,后续执行的指令由分支语句的计算结果决定,有可能不在预取的指令中,此时该功能将没有效果,CPU仍需要等待。 可以通过FLASH_ACR的PRFTEN位写1打开指令预取功能。

为了降低因为指令跳转产生的等待时间,可以在指令缓存中存放64行128位指令。 CPU请求指令时会先从指令缓存中查找,如果目标指令在缓存中,无需任何等待就可以直接获得。 当出现指令缺失(请求指令不在当前指令行、预取指令行、指令缓存中)时,系统会将新读取的指令行存放到指令缓存中。 当指令缓存存满后,可采用LRU(最近最少使用)策略替换缓存中的指令。可以通过FLASH_ACR的ICEN为写1打开指令缓存功能。

在执行指令时,CPU会通过D-Code总线从Flash内存中获取数据,在得到需要的数据之前,系统将会阻塞。 为了减小阻塞时间,赋予AHB的D-Code高于I-Code总线更高的访问优先级。如果有些数据会被频繁的访问,可以通过向FLASH_ACR的DCEN位写1, 打开数据缓存。其工作方式与指令缓存类似,只是保留的数据只有8行128位的数据。

3. 读取FLASH中数据

读FLASH并没有什么特别的要求,就好像在直接操作内存一样,只要在系统初始化过程中按照表2配置好读取数据的等待时间就行。我们定义如下的三个宏定义,分别用来读取一个字节、半字、全字:

        #define flash_read_byte(addr)       (*(volatile uint8*)addr)
        #define flash_read_halfword(addr)   (*(volatile uint16*)addr)
        #define flash_read_word(addr)       (*(volatile uint32*)addr)
我们需要给出将要读取数据的地址,这里根据数据位宽进行类型转换得到指向目标地址的指针,并通过‘*’间接寻址得到相应的数据。此外,我们还定义了一个读取多个字节的函数:
        void flash_read_bytes(uint32 addr, uint8 *buf, uint32 len)
        {
            volatile uint8 *pflash = (volatile uint8 *)addr;
            for (uint32 i = 0; i < len; i++)
                buf[i] = pflash[i];
        }

需要注意的是,在对Flash进行擦写和编程操作期间,任何对Flash的读操作都会被阻塞,直到Flash操作结束后才能正常的进行读访问。

4. 擦写操作

STM32支持整体(mess)擦写和sector擦写。所谓的擦写就是把Flash中所有的存储单元都写成0xFF,这与FLASH的实现机理有关系。 我们直到控制存储单元的各个位表现为'0'或者'1'就可以表达不同的数据,向存储单元写入数据就是改变单元各位的逻辑。而正常状态下,FLASH的存储单元只能够从'1'转换到'0', 若想从'0'转换为'1'则需要对整个FLASH操作,也就是所谓的擦写。

擦写之后的所有的存储单元都写成了0xFF,那么我们就可以控制任何一位转换为0,得到想要保存的数据,只是这个过程是不可逆转的。0xFF可以转换为0x00到0xFF之间的任何一个数据, 假设转换成了0x02,那么如果不经过擦除操作,我们只能将0x02中唯一的一个'1'转换为'0'得到0x00。所以,人们才会强调向FLASH中写入数据时需要先擦除再写入数据。

STM32使用一个控制寄存器CR来控制FLASH。CR中有一个LOCK位,系统复位以后,该位被置'1'。此时任何对CR寄存器的写操作都是非法的将被忽略, 这是为了防止意外发生导致的对Flash的误操作。若要解锁CR寄存器,需要向KEYR寄存器先后写入键值0x45670123和0xCDEF89AB。只有按照正确的顺序写入键值才能解锁, 解锁之后向CR的LOCK位写'1'将再次锁定CR寄存器。下面的函数flash_unlock()和flash_lock()完成了解锁和加锁的操作,我们在实际的使用过程中应当保证这两个操作时成对出现的。

        #define FLASH_KEYR_KEY1 ((uint32)0x45670123)
        #define FLASH_KEYR_KEY2 ((uint32)0xCDEF89AB)

        void flash_unlock(void) {
            if (1 == FLASH->CR.bits.LOCK) {
                FLASH->KEYR = FLASH_KEYR_KEY1;
                FLASH->KEYR = FLASH_KEYR_KEY2;
            }
        }
        void flash_lock(void) {
            FLASH->CR.bits.LOCK = 1;
        }

解锁了CR之后,我们就可以通过CR进行擦写和编程操作了,但在操作之前,需要根据系统的供电电压和形式设置CR.PSIZE,确定每次编程操作时写入的字节数。 如果设置的CR.PSIZE值与电压不匹配,将产生不可预测的结果,即使状态寄存器指示写操作结束,也不能保证写操作一定正常执行了。 表2中列出了不同电压下对应的CR.PSIZE的取值。使用Vpp可以在比较宽的并行位数下进行编程操作,可以提高编程速度。 但需要在Vpp引脚施加一个8V到9V的外部电源,而且Vpp供电时间超过1个小时可能会导致Flash的损坏,所以一般只在出厂前进行初始编程时使用。

表3 不同电压CR.PSIZE取值

电压范围2.7-3.6V
使用外部Vpp
电压范围
2.7-3.6V
电压范围
2.4-2.7V
电压范围
2.1-2.4V
电压范围
1.8-2.1V
并行位数 x64 x32 x16 x8
PSIZE 2'b11 2'b10 2'b01 2'b00

Flash的擦除操作只能针对Sector或者整个Flash进行。通过置位CR.SER或者CR.MER来触发扇区或者整体擦写操作,但是不能同时置位这两个字段。 下面这两个函数flash_mass_erase()和flash_sector_erase()分别用整体和Sector擦除。

        void flash_mass_erase(void) {
            while (1 == FLASH->SR.bits.BSY);
            
            FLASH->CR.bits.PSIZE = FLASH_CR_PSIZE_X8;
            FLASH->CR.bits.MER = 1;
            FLASH->CR.bits.STRT = 1;
            
            while (1 == FLASH->SR.bits.BSY);
        }
        void flash_sector_erase(uint8 sector) {
            while (1 == FLASH->SR.bits.BSY);
            
            FLASH->CR.bits.PSIZE = FLASH_CR_PSIZE_X8;
            FLASH->CR.bits.SNB = sector;
            FLASH->CR.bits.SER = 1;
            FLASH->CR.bits.STRT = 1;
            
            while (1 == FLASH->SR.bits.BSY);
        } 

这两个函数中可以看到,整个过程中先查询状态寄存器SR的BSY确认当前没有FLASH操作,然后指定位宽,这里为了保险起见就用了8位的并行,实际上32位也是可以的,但是由于没有Vpp供电,所以64位是不行的。 接着指定MER或者SER位指定执行整体擦除还是Sector擦除,如果是Sector擦除还需要通过SNB位指定需要擦除的Sector。最后通过STRT位触发开始执行擦除操作,我们通过检测状态寄存器的BSY位确认操作结束。

5. 编程操作

所谓的编程操作实际上就是写入数据,手册中用的是Program一词,所以这里就翻译为编程操作。编程操作一样需要用CR寄存器控制,下面分别是以字节和半字为单位写入数据的函数。

        void flash_write_byte(uint32 addr, uint8 data) {
            // Todo: 非法地址检查
            
            while (1 == FLASH->SR.bits.BSY);
            
            FLASH->CR.bits.PSIZE = FLASH_CR_PSIZE_X8;
            FLASH->CR.bits.PG = 1;
            
            *(volatile uint8 *)addr = data;
            
            while (1 == FLASH->SR.bits.BSY);
            FLASH->CR.bits.PG = 0;
        }
        void flash_write_halfword(uint32 addr, uint16 data) {
            // Todo: 非法地址检查
            
            while (1 == FLASH->SR.bits.BSY);
            
            FLASH->CR.bits.PSIZE = FLASH_CR_PSIZE_X16;
            FLASH->CR.bits.PG = 1;
            
            *(volatile uint16 *)addr = data;
            
            while (1 == FLASH->SR.bits.BSY);
            FLASH->CR.bits.PG = 0;
        }

首先检测状态寄存器SR的BSY位确认没有FLASH操作,接着指定位宽并置位PG位表示开始编程操作。这里只列举了字节和半字的编程函数,只需要更改PSIZE位就可以全字为单位编程。 同样由于没有Vpp供电不能进行双字为单位的编程操作。最后按照PSIZE指定的单位写入数据,在监测到没有FLASH操作后清除PG位,结束编程操作。

对Flash的编程操作数据应当是128位对齐的。如果不满足写操作将不被执行,而且会将SR.PGAERR位置1。写访问操作的数据位宽必须和PSIZE的配置一致,否则写操作也不被执行,同时将SR.PGPERR位置1。 如果没有按照上述的编程流程操作,将置位SR.PGSERR,标识着编程顺序错误。在上面的函数中,并没有检查地址是否合适。严格来说,应该检查一下地址边界不要超出了片上闪存空间, 同时确认数据的128位对齐。

在进行编程和擦写操作时,可能会涉及到保存在缓存中的数据和指令。编程的写操作将同时修改Flash和缓存中的数据。擦写操作则必须保证在代码执行期间访问这些数据之前把它们重新写入缓存。 如果无法可靠的做到这一点,建议置位ACR.DCRST和ACR.ICRST,刷新缓存。需要注意的是只有在关闭了指令和数据缓存的情况下才可以刷新缓存。

6.总结

在STM32F407系列的芯片中最多有1M字节的片上Flash空间,并将这1M的空间划分成了11个大小不等的Sector。由于访问FLASH需要一定的时间,根据不同的总线时钟和供电电压配置合理的延迟周期。 但是FLASH是保存我们的程序的地方,为了提高访问速度,STM32设计了一个缓存结构用来提高访问速度。

读FLASH中的数据不需要特别的操作,直接当作内存通过地址就可以获取对应的数据了。由于FLASH的实现机制,为保证正确的写入数据,需要先擦除原来的数据再写入新的数据。 在STM32中通过一个片上闪存接口控制擦写和编程操作。STM32支持整体擦除和Sector擦除操作,需要通过控制寄存器CR进行配置。但是CR默认是上锁的,需要通过给KEYR寄存器按照顺序写入两个键值解锁, 才可以修改CR寄存器。




Copyright @ 高乙超. All Rights Reserved. 京ICP备16033081号-1