45. 读写内部FLASH¶
本章参考资料:《STM32F10x闪存编程参考手册》《STM32F10x 中文参考手册》、《STM32F10x规格书》、《Cortex-M3权威指南》。
45.1. STM32的内部FLASH简介¶
在STM32芯片内部有一个FLASH存储器,它主要用于存储代码,我们在电脑上编写好应用程序后,使用下载器把编译后的代码文件烧录到该内部FLASH中, 由于FLASH存储器的内容在掉电后不会丢失,芯片重新上电复位后,内核可从内部FLASH中加载代码并运行, 见图 STM32的内部框架图 。
除了使用外部的工具(如下载器)读写内部FLASH外,STM32芯片在运行的时候,也能对自身的内部FLASH进行读写,因此, 若内部FLASH存储了应用程序后还有剩余的空间,我们可以把它像外部SPI-FLASH那样利用起来,存储一些程序运行时产生的需要掉电保存的数据。
由于访问内部FLASH的速度要比外部的SPI-FLASH快得多,所以在紧急状态下常常会使用内部FLASH存储关键记录;为了防止应用程序被抄袭, 有的应用会禁止读写内部FLASH中的内容,或者在第一次运行时计算加密信息并记录到某些区域,然后删除自身的部分加密代码,这些应用都涉及到内部FLASH的操作。
45.1.1. 内部FLASH的构成¶
STM32的内部FLASH包含主存储器、系统存储器以及选项字节区域, 它们的地址分布及大小见表 STM32大容量产品内部FLASH的构成 (在《STM32参考手册》中没有关于其内部FLASH的说明, 需要了解这些内容时,要查阅《STM32F10x闪存编程参考手册》)。
各个存储区域的说明如下:
主存储
一般我们说STM32内部FLASH的时候,都是指这个主存储器区域,它是存储用户应用程序的空间, 芯片型号说明中的256K FLASH、512K FLASH都是指这个区域的大小。
主存储器分为256页,每页大小为2KB,共512KB。这个分页的概念,实质就是FLASH存储器的扇区,与其它FLASH一样,在写入数据前,要先按页(扇区)擦除。
注意上表中的主存储器是本实验板使用的STM32VET6型号芯片的参数,即STM32F1大容量产品。若使用超大容量、中容量或小容量产品, 它们主存储器的页数量、页大小均有不同,使用的时候要注意区分。
关于STM32内部FLASH的容量类型可根据它的型号名获知,见表 STM32芯片的命名规则 。
系统存储区
系统存储区是用户不能访问的区域,它在芯片出厂时已经固化了启动代码,它负责实现串口、USB以及CAN等ISP烧录功能。
选项字节
选项字节用于配置FLASH的读写保护、待机/停机复位、软件/硬件看门狗等功能,这部分共16字节。可以通过修改FLASH的选项控制寄存器修改。
45.2. 对内部FLASH的写入过程¶
45.2.1. 解锁¶
由于内部FLASH空间主要存储的是应用程序,是非常关键的数据,为了防止误操作修改了这些内容,芯片复位后默认会给控制寄存器FLASH_CR上锁, 这个时候不允许设置FLASH的控制寄存器,从而不能修改FLASH中的内容。
所以对FLASH写入数据前,需要先给它解锁。解锁的操作步骤如下:
(1) 往FPEC键寄存器 FLASH_KEYR中写入 KEY1 = 0x45670123
(2) 再往FPEC键寄存器 FLASH_KEYR中写入 KEY2 = 0xCDEF89AB
45.2.2. 页擦除¶
在写入新的数据前,需要先擦除存储区域,STM32提供了页(扇区)擦除指令和整个FLASH擦除(批量擦除)的指令,批量擦除指令仅针对主存储区。
页擦除的过程如下:
(1) 检查 FLASH_SR 寄存器中的“忙碌寄存器位 BSY”,以确认当前未执行任何 Flash 操作;
(2) 在 FLASH_CR 寄存器中,将“激活页擦除寄存器位PER ”置 1,
(3) 用FLASH_AR寄存器选择要擦除的页;
(4) 将 FLASH_CR 寄存器中的“开始擦除寄存器位 STRT ”置 1,开始擦除;
(5) 等待 BSY 位被清零时,表示擦除完成。
45.2.3. 写入数据¶
擦除完毕后即可写入数据,写入数据的过程并不是仅仅使用指针向地址赋值,赋值前还还需要配置一系列的寄存器,步骤如下:
(1) 检查 FLASH_SR 中的 BSY 位,以确认当前未执行任何其它的内部 Flash 操作;
(2) 将 FLASH_CR 寄存器中的 “激活编程寄存器位PG” 置 1;
(3) 向指定的FLASH存储器地址执行数据写入操作,每次只能以16位的方式写入;
(4) 等待 BSY 位被清零时,表示写入完成。
45.3. 查看工程的空间分布¶
由于内部FLASH本身存储有程序数据,若不是有意删除某段程序代码,一般不应修改程序空间的内容, 所以在使用内部FLASH存储其它数据前需要了解哪一些空间已经写入了程序代码,存储了程序代码的扇区都不应作任何修改。 通过查询应用程序编译时产生的“*.map”后缀文件,可以了解程序存储到了哪些区域, 它在工程中的打开方式见图 打开工程的map文件 , 也可以到工程目录中的“Listing”文件夹中找到,关于map文件的详细说明可参考前面的《MDK的编译过程及文件详解》章节。
打开map文件后,查看文件最后部分的区域,可以看到一段以“Memory Map of the image”开头的记录(若找不到可用查找功能定位), 见 代码清单:FLASH-1 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | =========================================================
Memory Map of the image //存储分布映像
Image Entry point : 0x08000131
/*程序ROM加载空间*/
Load Region LR_IROM1 (Base: 0x08000000, Size: 0x000017a8, Max: 0x00080000, ABSOLUTE)
/*程序ROM执行空间*/
Execution Region ER_IROM1 (Base: 0x08000000, Size: 0x0000177c, Max: 0x00080000, ABSOLUTE)
/*地址分布列表*/
Base Addr Size Type Attr Idx E Section Name Object
0x08000000 0x00000130 Data RO 3 RESET startup_stm32f10x_hd.o
0x08000130 0x00000000 Code RO 479 * .ARM.Collect$$$$00000000 mc_w.l(entry.o)
0x08000130 0x00000004 Code RO 742 .ARM.Collect$$$$00000001 mc_w.l(entry2.o)
0x08000134 0x00000004 Code RO 745 .ARM.Collect$$$$00000004 mc_w.l(entry5.o)
/*...此处省略大部分内容*/
0x080016e8 0x00000024 Code RO 772 .text mc_w.l(init.o)
0x0800170c 0x00000010 Code RO 483 i.__0printf$bare mc_w.l(printfb.o)
0x0800171c 0x0000000e Code RO 784 i.__scatterload_copy mc_w.l(handlers.o)
0x0800172a 0x00000002 Code RO 785 i.__scatterload_null mc_w.l(handlers.o)
0x0800172c 0x0000000e Code RO 786 i.__scatterload_zeroinit mc_w.l(handlers.o)
0x0800173a 0x00000022 Code RO 490 i._printf_core mc_w.l(printfb.o)
0x0800175c 0x00000020 Data RO 782 Region$$Table anon$$obj.o
|
这一段是某工程的ROM存储器分布映像,在STM32芯片中,ROM区域的内容就是指存储到内部FLASH的代码。
45.3.1. 程序ROM的加载与执行空间¶
上述说明中有两段分别以“Load Region LR_ROM1”及“Execution Region ER_IROM1”开头的内容, 它们分别描述程序的加载及执行空间。 在芯片刚上电运行时,会加载程序及数据,例如它会从程序的存储区域加载到程序的执行区域,还把一些已初始化的全局变量从ROM复制到RAM空间, 以便程序运行时可以修改变量的内容。加载完成后,程序开始从执行区域开始执行。
在上面map文件的描述中,我们了解到加载及执行空间的基地址(Base)都是0x08000000,它正好是STM32内部FLASH的首地址, 即STM32的程序存储空间就直接是执行空间;它们的大小(Size)分别为0x000017a8及0x0000177c, 执行空间的ROM比较小的原因就是因为部分RW-data类型的变量被拷贝到RAM空间了; 它们的最大空间(Max)均为0x00080000,即512K字节,它指的是内部FLASH的最大空间。
计算程序占用的空间时,需要使用加载区域的大小进行计算,本例子中应用程序使用的内部FLASH是从0x08000000至(0x08000000+0x000017a8)地址的空间区域。
45.3.2. ROM空间分布表¶
在加载及执行空间总体描述之后,紧接着一个ROM详细地址分布表,它列出了工程中的各个段(如函数、常量数据)所在的地址BaseAddr及占用的空间Size, 列表中的Type说明了该段的类型,CODE表示代码,DATA表示数据,而PAD表示段之间的填充区域,它是无效的内容, PAD区域往往是为了解决地址对齐的问题。
观察表中的最后一项,它的基地址是0x0800175c,大小为0x00000020,可知它占用的最高的地址空间为0x0800177c,跟执行区域的最高地址0x0000177c一样, 但它们比加载区域说明中的最高地址0x80017a8要小,所以我们以加载区域的大小为准。 对比表 STM32大容量产品内部FLASH的构成 的内部FLASH页地址分布表, 可知仅使用页0至页2就可以完全存储本应用程序,所以从页3(地址0x08001800)后的存储空间都可以作其它用途,使用这些存储空间时不会篡改应用程序空间的数据。
45.4. 操作内部FLASH的库函数¶
为简化编程,STM32标准库提供了一些库函数,它们封装了对内部FLASH写入数据操作寄存器的过程。
45.4.1. FLASH解锁、上锁函数¶
对内部FLASH解锁、上锁的函数见 代码清单:FLASH-2 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | #define FLASH_KEY1 ((uint32_t)0x45670123)
#define FLASH_KEY2 ((uint32_t)0xCDEF89AB)
/**
* @brief 对FLASH控制寄存器解锁,使能访问
* @param None
* @retval None
*/
void FLASH_Unlock(void)
{
if ((FLASH->CR & FLASH_CR_LOCK) != RESET) {
/* 写入确认验证码 */
FLASH->KEYR = FLASH_KEY1;
FLASH->KEYR = FLASH_KEY2;
}
}
/**
* @brief 对FLASH控制寄存器上锁,禁止访问
* @param None
* @retval None
*/
void FLASH_Lock(void)
{
/* 设置FLASH寄存器的LOCK位 */
FLASH->CR |= FLASH_CR_LOCK;
}
|
解锁的时候,它对FLASH_KEYR寄存器写入两个解锁参数,上锁的时候,对FLASH_CR寄存器的FLASH_CR_LOCK位置1。
45.4.2. 设置操作位数及页擦除¶
解锁后擦除扇区时可调用FLASH_EraseSector完成,见 代码清单:FLASH-3 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | /**
* @brief 擦除指定的页
* @param Page_Address: 要擦除的页地址.
* @retval FLASH Status:
可能的返回值: FLASH_BUSY, FLASH_ERROR_PG,
* FLASH_ERROR_WRP, FLASH_COMPLETE or FLASH_TIMEOUT.
*/
FLASH_Status FLASH_ErasePage(uint32_t Page_Address)
{
FLASH_Status status = FLASH_COMPLETE;
/* 检查参数 */
assert_param(IS_FLASH_ADDRESS(Page_Address));
/*...此处省略XL超大容量芯片的控制部分*/
/* 等待上一次操作完成 */
status = FLASH_WaitForLastOperation(EraseTimeout);
if (status == FLASH_COMPLETE) {
/* 若上次操作完成,则开始页擦除 */
FLASH->CR|= CR_PER_Set;
FLASH->AR = Page_Address;
FLASH->CR|= CR_STRT_Set;
/* 等待操作完成 */
status = FLASH_WaitForLastOperation(EraseTimeout);
/* 复位 PER 位 */
FLASH->CR &= CR_PER_Reset;
}
/* 返回擦除结果 */
return status;
}
|
本函数包含一个输入参数用于设置要擦除的页地址,即目标页的在内部FALSH的首地址,函数获取地址后,根据前面的流程检查状态位、 向控制寄存器FLASH_CR及地址寄存器FLASH_AR写入参数,配置开始擦除后,需要等待一段时间,函数中使用使用FLASH_WaitForLastOperation等待, 擦除完成的时候才会退出FLASH_EraseSector函数。
45.4.3. 写入数据¶
对内部FLASH写入数据不像对SDRAM操作那样直接指针操作就完成了,还要设置一系列的寄存器, 利用FLASH_ProgramWord和FLASH_ProgramHalfWord函数可按字、半字的单位单位写入数据, 见 代码清单:FLASH-4 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | /**
* @brief 向指定的地址写入一个字的数据(32位)
* @param Address: 要写入的地址
* @param Data: 要写入的数据
* @retval FLASH Status:
可能的返回值: FLASH_ERROR_PG,
* FLASH_ERROR_WRP, FLASH_COMPLETE or FLASH_TIMEOUT.
*/
FLASH_Status FLASH_ProgramWord(uint32_t Address, uint32_t Data)
{
FLASH_Status status = FLASH_COMPLETE;
__IO uint32_t tmp = 0;
/* 检查参数 */
assert_param(IS_FLASH_ADDRESS(Address));
/*...此处省略XL超大容量芯片的控制部分*/
/* Wait for last operation to be completed */
status = FLASH_WaitForLastOperation(ProgramTimeout);
if (status == FLASH_COMPLETE) {
/* 若上次操作完成,则开始页入低16位的数据(输入参数的第1部分) */
FLASH->CR |= CR_PG_Set;
*(__IO uint16_t*)Address = (uint16_t)Data;
/* 等待上一次操作完成 */
status = FLASH_WaitForLastOperation(ProgramTimeout);
if (status == FLASH_COMPLETE) {
/* 若上次操作完成,则开始页入高16位的数据(输入参数的第2部分) */
tmp = Address + 2;
*(__IO uint16_t*) tmp = Data >> 16;
/* 等待操作完成 */
status = FLASH_WaitForLastOperation(ProgramTimeout);
/* 复位 PG 位 */
FLASH->CR &= CR_PG_Reset;
} else {
/* 复位 PG 位 */
FLASH->CR &= CR_PG_Reset;
}
}
/* 返回写入结果 */
return status;
}
|
从函数代码可了解到,它设置FLASH->CR 寄存器的PG位允许写入后,使用16位的指针往指定的地址写入数据,由于每次只能按16位写入, 所以这个按字写入的过程使用了两次指针赋值,分别写入指定数据的低16位和高16位,每次赋值操作后,调用FLASH_WaitForLastOperation函数等待写操作完毕。 标准库里还提供了FLASH_ProgramHalfWord函数用于每次写入半个字,即16位,该函数内部的执行过程类似。
45.5. 实验:读写内部FLASH¶
在本小节中我们以实例讲解如何使用内部FLASH存储数据。
45.5.1. 硬件设计¶
本实验仅操作了STM32芯片内部的FLASH空间,无需额外的硬件。
45.5.2. 软件设计¶
本小节讲解的是“内部FLASH编程”实验,请打开配套的代码工程阅读理解。为了方便展示及移植, 我们把操作内部FLASH相关的代码都编写到“bsp_internal_Flash.c”及“bsp_internal_Flash.h”文件中,这些文件是我们自己编写的, 不属于标准库的内容,可根据您的喜好命名文件 。
45.5.2.2. 代码分析¶
硬件定义
读写内部FLASH不需要用到任何外部硬件,不过在编写测试时我们要先确定内部FLASH的页大小以及要往哪些地址写入数据, 在本工程中这些定义在bsp_internal_Flash.h头文件中,见 代码清单:FLASH-5 。
1 2 3 4 5 6 7 8 9 10 11 | /* STM32大容量产品每页大小2KByte,中、小容量产品每页大小1KByte */
#if defined (STM32F10X_HD) || defined (STM32F10X_HD_VL) ||\
defined (STM32F10X_CL) || defined (STM32F10X_XL)
#define FLASH_PAGE_SIZE ((uint16_t)0x800)//2048
#else
#define FLASH_PAGE_SIZE ((uint16_t)0x400)//1024
#endif
//写入的起始地址与结束地址
#define WRITE_START_ADDR ((uint32_t)0x08008000)
#define WRITE_END_ADDR ((uint32_t)0x0800C000)
|
代码中首先根据芯片类型定义了宏FLASH_PAGE_SIZE,由于本工程使用的是STM32VET6芯片,在工程的C/C++选项中包含了STM32F10X_HD的定义, 所以FLASH_PAGE_SIZE被定义成0x800,即2048字节。
另外,WRITE_START_ADDR和WRITE_END_ADDR定义了后面本工程测试读写内部FLASH的起始地址与结束地址,这部分区域与map文件指示的程序本身占用的空间不重合, 所以在后面修改这些地址的内容时,它不会把自身的程序修改掉。
读写内部FLASH
一切准备就绪,可以开始对内部FLASH进行擦写,这个过程不需要初始化任何外设,只要按解锁、擦除及写入的流程走就可以了, 见 代码清单:FLASH-6 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | /**
* @brief InternalFlash_Test,对内部FLASH进行读写测试
* @param None
* @retval None
*/
int InternalFlash_Test(void)
{
uint32_t EraseCounter = 0x00; //记录要擦除多少页
uint32_t Address = 0x00; //记录写入的地址
uint32_t Data = 0x3210ABCD; //记录写入的数据
uint32_t NbrOfPage = 0x00; //记录写入多少页
FLASH_Status FLASHStatus = FLASH_COMPLETE; //记录每次擦除的结果
TestStatus MemoryProgramStatus = PASSED;//记录整个测试结果
/* 解锁 */
FLASH_Unlock();
/* 计算要擦除多少页 */
NbrOfPage = (WRITE_END_ADDR - WRITE_START_ADDR) / FLASH_PAGE_SIZE;
/* 清空所有标志位 */
FLASH_ClearFlag(FLASH_FLAG_EOP | FLASH_FLAG_PGERR |
FLASH_FLAG_WRPRTERR);
/* 按页擦除*/
for (EraseCounter = 0; (EraseCounter < NbrOfPage) &&
(FLASHStatus == FLASH_COMPLETE); EraseCounter++) {
FLASHStatus = FLASH_ErasePage(WRITE_START_ADDR +
(FLASH_PAGE_SIZE * EraseCounter));
}
/* 向内部FLASH写入数据 */
Address = WRITE_START_ADDR;
while ((Address < WRITE_END_ADDR) && (FLASHStatus == FLASH_COMPLETE)) {
FLASHStatus = FLASH_ProgramWord(Address, Data);
Address = Address + 4;
}
FLASH_Lock();
/* 检查写入的数据是否正确 */
Address = WRITE_START_ADDR;
while ((Address < WRITE_END_ADDR) && (MemoryProgramStatus != FAILED)) {
if ((*(__IO uint32_t*) Address) != Data) {
MemoryProgramStatus = FAILED;
}
Address += 4;
}
return MemoryProgramStatus;
}
|
该函数的执行过程如下:
(1) 调用FLASH_Unlock解锁;
(2) 根据起始地址及结束地址计算要擦除多少页;
(3) 调用FLASH_ClearFlag清除各种标志位;
(4) 使用循环调用FLASH_ErasePage擦除页,每次擦除一页;
(5) 使用循环调用FLASH_ProgramWord函数向起始地址至结束地址的存储区域都写入变量 “Data” 存储的数值数值;
(6) 调用FLASH_Lock上锁;
(7) 使用指针读取写入的数据内容并校验。
main函数
最后我们来看看main函数的执行流程,见 代码清单:FLASH-7 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | int main(void)
{
/*初始化USART,配置模式为 115200 8-N-1*/
USART_Config();
LED_GPIO_Config();
LED_BLUE;
printf("\r\n 欢迎使用野火 STM32 开发板。\r\n");
printf("正在进行读写内部FLASH实验,请耐心等待\r\n");
if (InternalFlash_Test()== PASSED) {
LED_GREEN;
printf("读写内部FLASH测试成功\r\n");
} else {
printf("读写内部FLASH测试失败\r\n");
LED_RED;
}
while (1) {
}
}
|
main函数中初始化了用于指示调试信息的LED及串口后,直接调用了InternalFlash_Test函数, 进行读写测试并根据测试结果输出调试信息。
45.5.3. 下载验证¶
用USB线连接开发板“USB TO UART”接口跟电脑,在电脑端打开串口调试助手, 把编译好的程序下载到开发板。在串口调试助手可看到擦写内部FLASH的调试信息。