在STM32芯片内部有一个FLASH存储器,它主要用于存储代码,我们在电脑上编写好应用程序后,使用下载器把编译后的代码文件烧录到该内部FLASH中, 由于FLASH存储器的内容在掉电后不会丢失,芯片重新上电复位后,内核可从内部FLASH中加载代码并运行, 见图 STM32的内部框架图。
除了使用外部的工具(如下载器)读写内部FLASH外,STM32芯片在运行的时候,也能对自身的内部FLASH进行读写,因此, 若内部FLASH存储了应用程序后还有剩余的空间,我们可以把它像外部SPI-FLASH那样利用起来,存储一些程序运行时产生的需要掉电保存的数据。
由于访问内部FLASH的速度要比外部的SPI-FLASH快得多,所以在紧急状态下常常会使用内部FLASH存储关键记录;为了防止应用程序被抄袭, 有的应用会禁止读写内部FLASH中的内容,或者在第一次运行时计算加密信息并记录到某些区域,然后删除自身的部分加密代码,这些应用都涉及到内部FLASH的操作。
43.1.1. 内部FLASH的构成
STM32的内部FLASH包含主存储器、系统存储器以及选项字节区域, 它们的地址分布及大小见表 STM32大容量产品内部FLASH的构成 (在《STM32参考手册》中没有关于其内部FLASH的说明, 需要了解这些内容时,要查阅《STM32F10x闪存编程参考手册》)。
各个存储区域的说明如下:
主存储
一般我们说STM32内部FLASH的时候,都是指这个主存储器区域,它是存储用户应用程序的空间, 芯片型号说明中的256K FLASH、512K FLASH都是指这个区域的大小。
主存储器分为256页,每页大小为2KB,共512KB。这个分页的概念,实质就是FLASH存储器的扇区,与其它FLASH一样,在写入数据前,要先按页(扇区)擦除。
注意上表中的主存储器是本实验板使用的STM32RCT6型号芯片的参数,即STM32F1大容量产品。若使用超大容量、中容量或小容量产品, 它们主存储器的页数量、页大小均有不同,使用的时候要注意区分。
关于STM32内部FLASH的容量类型可根据它的型号名获知,见表 STM32芯片的命名规则。
系统存储区
系统存储区是用户不能访问的区域,它在芯片出厂时已经固化了启动代码,它负责实现串口、USB以及CAN等ISP烧录功能。
选项字节
选项字节用于配置FLASH的读写保护、待机/停机复位、软件/硬件看门狗等功能,这部分共16字节。可以通过修改FLASH的选项控制寄存器修改。
43.2. 对内部FLASH的写入过程
43.2.1. 解锁
由于内部FLASH空间主要存储的是应用程序,是非常关键的数据,为了防止误操作修改了这些内容,芯片复位后默认会给控制寄存器FLASH_CR上锁, 这个时候不允许设置FLASH的控制寄存器,从而不能修改FLASH中的内容。
所以对FLASH写入数据前,需要先给它解锁。解锁的操作步骤如下:
(1) 往FPEC键寄存器 FLASH_KEYR中写入 KEY1 = 0x45670123
(2) 再往FPEC键寄存器 FLASH_KEYR中写入 KEY2 = 0xCDEF89AB
43.2.2. 页擦除
在写入新的数据前,需要先擦除存储区域,STM32提供了页(扇区)擦除指令和整个FLASH擦除(批量擦除)的指令,批量擦除指令仅针对主存储区。
页擦除的过程如下:
(1) 检查 FLASH_SR 寄存器中的“忙碌寄存器位 BSY”,以确认当前未执行任何 Flash 操作;
(2) 在 FLASH_CR 寄存器中,将“激活页擦除寄存器位PER ”置 1,
(3) 用FLASH_AR寄存器选择要擦除的页;
(4) 将 FLASH_CR 寄存器中的“开始擦除寄存器位 STRT ”置 1,开始擦除;
(5) 等待 BSY 位被清零时,表示擦除完成。
43.2.3. 写入数据
擦除完毕后即可写入数据,写入数据的过程并不是仅仅使用指针向地址赋值,赋值前还还需要配置一系列的寄存器,步骤如下:
(1) 检查 FLASH_SR 中的 BSY 位,以确认当前未执行任何其它的内部 Flash 操作;
(2) 将 FLASH_CR 寄存器中的 “激活编程寄存器位PG” 置 1;
(3) 向指定的FLASH存储器地址执行数据写入操作,每次只能以16位的方式写入;
(4) 等待 BSY 位被清零时,表示写入完成。
43.3. 查看工程的空间分布
由于内部FLASH本身存储有程序数据,若不是有意删除某段程序代码,一般不应修改程序空间的内容, 所以在使用内部FLASH存储其它数据前需要了解哪一些空间已经写入了程序代码,存储了程序代码的扇区都不应作任何修改。 通过查询应用程序编译时产生的“*.map”后缀文件,可以了解程序存储到了哪些区域, 它在工程中的打开方式见图 打开工程的map文件 , 也可以到工程目录中的“Listing”文件夹中找到,关于map文件的详细说明可参考前面的《MDK的编译过程及文件详解》章节。
打开map文件后,查看文件最后部分的区域,可以看到一段以“Memory Map of the image”开头的记录(若找不到可用查找功能定位), 见 代码清单:FLASH-1。
代码清单:FLASH-1 map文件中的存储映像分布说明
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: 0x0000178c, Max: 0x00040000, ABSOLUTE) /*程序ROM执行空间*/ Execution Region ER_IROM1 (Base: 0x08000000, Size: 0x00001760, Max: 0x00040000, 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) /*...此处省略大部分内容*/ 0x080016cc 0x00000024 Code RO 772 .text mc_w.l(init.o) 0x080016f0 0x00000010 Code RO 483 i.__0printf$bare mc_w.l(printfb.o) 0x08001700 0x0000000e Code RO 784 i.__scatterload_copy mc_w.l(handlers.o) 0x0800170e 0x00000002 Code RO 785 i.__scatterload_null mc_w.l(handlers.o) 0x08001710 0x0000000e Code RO 786 i.__scatterload_zeroinit mc_w.l(handlers.o) 0x0800171e 0x00000022 Code RO 490 i._printf_core mc_w.l(printfb.o) 0x08001740 0x00000020 Data RO 782 Region$$Table anon$$obj.o |
这一段是某工程的ROM存储器分布映像,在STM32芯片中,ROM区域的内容就是指存储到内部FLASH的代码。
43.3.1. 程序ROM的加载与执行空间
上述说明中有两段分别以“Load Region LR_ROM1”及“Execution Region ER_IROM1”开头的内容, 它们分别描述程序的加载及执行空间。 在芯片刚上电运行时,会加载程序及数据,例如它会从程序的存储区域加载到程序的执行区域,还把一些已初始化的全局变量从ROM复制到RAM空间, 以便程序运行时可以修改变量的内容。加载完成后,程序开始从执行区域开始执行。
在上面map文件的描述中,我们了解到加载及执行空间的基地址(Base)都是0x08000000,它正好是STM32内部FLASH的首地址, 即STM32的程序存储空间就直接是执行空间;它们的大小(Size)分别为0x0000178c及0x00001760, 执行空间的ROM比较小的原因就是因为部分RW-data类型的变量被拷贝到RAM空间了; 它们的最大空间(Max)均为0x00040000,即256K字节,它指的是内部FLASH的最大空间。
计算程序占用的空间时,需要使用加载区域的大小进行计算,本例子中应用程序使用的内部FLASH是从0x08000000至(0x08000000+0x0000178c)地址的空间区域。
43.3.2. ROM空间分布表
在加载及执行空间总体描述之后,紧接着一个ROM详细地址分布表,它列出了工程中的各个段(如函数、常量数据)所在的地址BaseAddr及占用的空间Size, 列表中的Type说明了该段的类型,CODE表示代码,DATA表示数据,而PAD表示段之间的填充区域,它是无效的内容, PAD区域往往是为了解决地址对齐的问题。
观察表中的最后一项,它的基地址是0x08001740,大小为0x00000020,可知它占用的最高的地址空间为0x08001760,跟执行区域的最高地址0x08001760一样, 但它们比加载区域说明中的最高地址0x0000178c要小,所以我们以加载区域的大小为准。 对比表 STM32大容量产品内部FLASH的构成 的内部FLASH页地址分布表, 可知仅使用页0至页2就可以完全存储本应用程序,所以从页3(地址0x08001800)后的存储空间都可以作其它用途,使用这些存储空间时不会篡改应用程序空间的数据。
43.4. 操作内部FLASH的库函数
为简化编程,STM32标准库提供了一些库函数,它们封装了对内部FLASH写入数据操作寄存器的过程。
43.4.1. FLASH解锁、上锁函数
对内部FLASH解锁、上锁的函数见 代码清单:FLASH-2。
代码清单:FLASH-2 FLASH解锁、上锁
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。
43.4.2. 设置操作位数及页擦除
解锁后擦除扇区时可调用FLASH_EraseSector完成,见 代码清单:FLASH-3。
代码清单: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函数。
43.4.3. 写入数据
对内部FLASH写入数据不像对SDRAM操作那样直接指针操作就完成了,还要设置一系列的寄存器, 利用FLASH_ProgramWord和FLASH_ProgramHalfWord函数可按字、半字的单位单位写入数据, 见 代码清单:FLASH-4。
代码清单: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位,该函数内部的执行过程类似。
43.5. 实验:读写内部FLASH
在本小节中我们以实例讲解如何使用内部FLASH存储数据。
43.5.1. 硬件设计
本实验仅操作了STM32芯片内部的FLASH空间,无需额外的硬件。
43.5.2. 软件设计
本小节讲解的是“内部FLASH编程”实验,请打开配套的代码工程阅读理解。为了方便展示及移植, 我们把操作内部FLASH相关的代码都编写到“bsp_internal_Flash.c”及“bsp_internal_Flash.h”文件中,这些文件是我们自己编写的, 不属于标准库的内容,可根据您的喜好命名文件 。
43.5.2.1. 程序设计要点
(1) 对内部FLASH解锁;
(2) 找出空闲页,擦除目标页;
(3) 进行读写测试。
43.5.2.2. 代码分析
硬件定义
读写内部FLASH不需要用到任何外部硬件,不过在编写测试时我们要先确定内部FLASH的页大小以及要往哪些地址写入数据, 在本工程中这些定义在bsp_internal_Flash.h头文件中,见 代码清单:FLASH-5。
代码清单:FLASH-5 各个扇区的基地址(bsp_internal_Flash.h文件)
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,由于本工程使用的是STM32RCT6芯片,在工程的C/C++选项中包含了STM32F10X_HD的定义, 所以FLASH_PAGE_SIZE被定义成0x800,即2048字节。
另外,WRITE_START_ADDR和WRITE_END_ADDR定义了后面本工程测试读写内部FLASH的起始地址与结束地址,这部分区域与map文件指示的程序本身占用的空间不重合, 所以在后面修改这些地址的内容时,它不会把自身的程序修改掉。
读写内部FLASH
一切准备就绪,可以开始对内部FLASH进行擦写,这个过程不需要初始化任何外设,只要按解锁、擦除及写入的流程走就可以了, 见 代码清单:FLASH-6。
代码清单:FLASH-6 对内部地FLASH进行读写测试(bsp_internal_Flash.c文件)
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。
代码清单:FLASH-7 main函数(main.c文件)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | int main(void) { /*初始化USART,配置模式为 115200 8-N-1*/ USART_Config(); LED_GPIO_Config(); printf("\r\n 欢迎使用野火 STM32 开发板。\r\n"); printf("正在进行读写内部FLASH实验,请耐心等待\r\n"); if (InternalFlash_Test()== PASSED) { LED1_ON; printf("读写内部FLASH测试成功\r\n"); } else { printf("读写内部FLASH测试失败\r\n"); LED2_ON; } while (1) { } } |
main函数中初始化了用于指示调试信息的LED及串口后,直接调用了InternalFlash_Test函数, 进行读写测试并根据测试结果输出调试信息。
选项字节与读写保护
在实际发布的产品中,在STM32芯片的内部FLASH存储了控制程序,如果不作任何保护措施的话,可以使用下载器直接把内部FLASH的内容读取回来, 得到bin或hex文件格式的代码拷贝,别有用心的厂商会利用该方法山寨产品。为此,STM32芯片提供了多种方式保护内部FLASH的程序不被非法读取, 但在默认情况下该保护功能是不开启的,若要开启该功能,需要改写内部FLASH选项字节(Option Bytes)中的配置。
44.1.1. 选项字节的内容
选项字节是一段特殊的FLASH空间,STM32芯片会根据它的内容进行读写保护配置, 选项字节的构成见表 选项字节的构成。
STM32F103系列芯片的选项字节有8个配置项,即上表中的USER、RDP、DATA0/1及WRP0/1/2/3,而表中带n的同类项是该项的反码, 即nUSER的值等于(~USER)、nRDP的值等于(~RDP),STM32利用反码来确保选项字节内容的正确性。
选项字节的8个配置项具体的数据位配置说明见表 选项字节具体的数据位配置说明。
我们主要讲解选项字节配置中的RDP位和WRP位,它们分别用于配置读保护和写保护。
44.1.2. RDP读保护
修改选项字节的RDP位的值可设置内部FLASH为以下保护级别:
-
0xA5:无保护
这是STM32的默认保护级别,它没有任何读保护,读取内部FLASH的内容都没有任何限制。也就是说,第三方可以使用调试器等工具, 获取该芯片FLASH中存储的程序,然后可以把获得的程序以bin和hex的格式下载到另一块STM32芯片中,加上PCB抄板技术,轻易复制出同样的产品。
-
其它值:使能读保护
把RDP配置成除0xA5外的任意数值, 都会使能读保护。在这种情况下, 若使用调试功能(使用下载器、仿真器)或者从内部SRAM自举时都不能对内部FLASH作任何访问(读写、擦除都被禁止); 而如果STM32是从内部FLASH自举时,它允许对内部FLASH的任意访问。也就是说,任何尝试从外部访问内部FLASH内容的操作都被禁止。 例如,无法通过下载器读取它的内容,或编写一个从内部SRAM启动的程序,若该SRAM启动的程序读取内部FLASH,会被禁止。 而如果是芯片原本的内部FLASH程序自己访问内部FLASH(即从FLASH自举的程序),是完全没有问题的,例如芯片本身的程序, 若包含有指针对内部FLASH某个地址进行的读取操作,它能获取正常的数据。
另外,被设置成读保护后,FLASH前4K字节的空间会强制加上写保护,也就是说,即使是从FLASH启动的程序,也无法擦写这4K字节空间的内容; 而对于前4K字节以外的空间,读保护并不影响它对其它空间的擦除/写入操作。利用这个特性, 可以编写IAP代码(In Application Program)更新FLASH中的程序, 它的原理是通过某个通讯接口获取将要更新的程序内容,然后利用内部FLASH擦写操作把这些内容烧录到自己的内部FLASH中,实现应用程序的更新, 该原理类似串口ISP程序下载功能,只不过ISP这个接收数据并更新的代码由ST提供,且存放在系统存储区域,而IAP是由用户自行编写的, 存放在用户自定义的FLASH区域,且通讯方式可根据用户自身的需求定制,如IIC、SPI等,只要能接收到数据均可。
-
解除保护
当需要解除芯片的读保护时,要把选项字节的RDP位重新设置为0xA5。在解除保护前,芯片会自动触发擦除主FLASH存储器的全部内容, 即解除保护后原内部FLASH的代码会丢失,从而防止降级后原内容被读取到。
芯片被配置成读保护后根据不同的使用情况,访问权限不同,总结。
44.1.3. WRP写保护
使用选项字节的WRP0/1/2/3可以设置主FLASH的写保护,防止它存储的程序内容被修改。
-
设置写保护
写保护的配置一般以4K字节为单位,除WRP3的最后一位比较特殊外,每个WRP选项字节的一位用于控制4K字节的写访问权限, 把对应WRP的位置0即可把它匹配的空间加入写保护。被设置成写保护后,主FLASH中的内容使用任何方式都不能被擦除和写入, 写保护不会影响读访问权限,读访问权限完全由前面介绍的读保护设置限制。
-
解除写保护
解除写保护是逆过程,把对应WRP的位置1即可把它匹配的空间解除写保护。解除写保护后,主FLASH中的内容不会像解读保护那样丢失,它会被原样保留。
44.2. 修改选项字节的过程
根据前面的说明,修改选项字节的内容可修改读写保护配置,不过选项字节复位后的默认状态是始终可以读但被写保护的, 因此它具有类似前面《读写内部FLASH》章节提到的FLASH_CR寄存器的访问限制,要想修改,需要先对FLASH_OPTKEYR寄存器写入解锁编码。 由于修改选项字节时也需要访问FLASH_CR寄存器,所以同样也要对FLASH_KEYR写入解锁编码。
修改选项字节的整个过程总结如下:
-
解除FLASH_CR寄存器的访问限制
-
往FPEC键寄存器 FLASH_KEYR中写入 KEY1 = 0x45670123
-
再往FPEC键寄存器 FLASH_KEYR中写入 KEY2 = 0xCDEF89AB
-
解除对选项字节的访问限制
-
往FLASH_OPTKEYR中写入 KEY1 = 0x45670123
-
再往FLASH_OPTKEYR中写入 KEY2 = 0xCDEF89AB
-
配置FLASH_CR的OPTPG位,准备修改选项字节
-
直接使用指针操作修改选项字节的内容,根据需要修改RDP、WRP等内容
-
对于读保护的解除,由于它会擦除FLASH的内容,所以需要检测状态寄存器标志位以确认FLASH擦除操作完成。
-
若是设置读保护及其解除,需要给芯片重新上电复位,以使新配置的选项字节生效;对于设置写保护及其解除, 需要给芯片进行系统复位,以使新配置的选项字节生效。
44.3. 操作选项字节的库函数
为简化编程,STM32标准库提供了一些库函数,它们封装了前面介绍的修改选项字节时的操作过程。
44.3.1. 选项字结构体定义
对选项字节结构体定义的见 代码清单:保护及解除-1。
代码清单:保护及解除-1选项字节结构体的定义(stm32f10x.h文件)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | /** * @brief 选项字节结构体 */ typedef struct { __IO uint16_t RDP; /*RDP及nRDP*/ __IO uint16_t USER; /*USER及nUSER,下面类似*/ __IO uint16_t Data0; __IO uint16_t Data1; __IO uint16_t WRP0; __IO uint16_t WRP1; __IO uint16_t WRP2; __IO uint16_t WRP3; } OB_TypeDef; /*强制转换为选项字节结构体指针*/ #define OB ((OB_TypeDef *) OB_BASE) /*选项字节基地址 */ #define OB_BASE ((uint32_t)0x1FFFF800) |
标准库中定义的选项字节结构体,包含了RDP、USER、DATA0/1及WRP0/1/2/3这些内容,每个结构体成员指向选项字节对应选项的原始配置码及反码。不过, 根据手册中的说明可了解到,当向选项字节的这些地址写入配置时,它会自动取低位字节计算出高位字节的值再存储,即自动取反码,非常方便。 例如程序中执行操作给结构体成员WRP0赋值为0x0011时,最终它会自动写入0xEE11(0xEE是0x11的反码)。最后, 从OB_BASE宏的定义可以确认它所指向的正是前面介绍的选项字节基地址,说明若在程序中使用该结构体赋值,会直接把内容写入到选项字节地址对应的空间中。
44.3.2. 设置写保护及解除
库文件提供了FLASH_EnableWriteProtection函数,可用于设置写保护及解除,代码清单:保护及解除-2。
代码清单:保护及解除-2 设置写保护及解除(stm32f10x_flash.c文件)
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 | #define RDP_Key ((uint16_t)0x00A5) /** * @brief 使能或关闭读保护 * @note 若芯片本身有对选项字节进行其它操作, 请先读出然后再重新写入,因为本函数会擦除所有选项字节的内容 * @param Newstate: 使能(ENABLE)或关闭(DISABLE) * @retval FLASH Status: 可能的返回值: FLASH_ERROR_PG, * FLASH_ERROR_WRP, FLASH_COMPLETE or FLASH_TIMEOUT. */ FLASH_Status FLASH_ReadOutProtection(FunctionalState NewState) { FLASH_Status status = FLASH_COMPLETE; /* 检查参数 */ assert_param(IS_FUNCTIONAL_STATE(NewState)); status = FLASH_WaitForLastOperation(EraseTimeout); if (status == FLASH_COMPLETE) { /* 写入选项字节解锁码 */ FLASH->OPTKEYR = FLASH_KEY1; FLASH->OPTKEYR = FLASH_KEY2; FLASH->CR |= CR_OPTER_Set; //擦除选项字节 FLASH->CR |= CR_STRT_Set; //开始擦除 /* 等待上一次操作完毕 */ status = FLASH_WaitForLastOperation(EraseTimeout); if (status == FLASH_COMPLETE) { /* 若擦除操作完成,复位 OPTER 位 */ FLASH->CR &= CR_OPTER_Reset; /* 准备写入选项字节 */ FLASH->CR |= CR_OPTPG_Set; if (NewState != DISABLE) { OB->RDP = 0x00;//写入非0xA5值,进行读保护 } else { OB->RDP = RDP_Key; //写入0xA5,解除读保护 } /* 等待上一次操作完毕 */ status = FLASH_WaitForLastOperation(EraseTimeout); if (status != FLASH_TIMEOUT) { /* 若操作完毕,复位 OPTPG 位 */ FLASH->CR &= CR_OPTPG_Reset; } } else { if (status != FLASH_TIMEOUT) { /* 复位 OPTER 位 */ FLASH->CR &= CR_OPTER_Reset; } } } /* 返回设置结果 */ return status; } |
该函数的输入参数可选FLASH_WRProt_Pages0to1至FLASH_WRProt_Pages62to511等宏,该参数用于指定要对哪些页进行写保护。
从该宏的定义方式可了解到,它用一个32位的数值表示WRP0/1/2/3,而宏名中的页码使用数据位1来在WRP0/1/2/3中对应的位作掩码指示。 如控制页0至页1的宏FLASH_WRProt_Pages0to1,它由WRP0最低位控制,所以其宏值为0x00000001(bit0为1);类似地, 控制页2至页3的宏FLASH_WRProt_Pages2to3,由WRP0的bit1控制,所以其宏值为0x00000002(bit1为1)。
理解了输入参数宏的结构后,即可分析函数中的具体代码。其中最核心要理解的是对输入参数的运算,输入参数FLASH_Pages自身会进行取反操作, 从而用于指示要保护页的宏对应的数据位会被置0,而在选项字节WRP中,被写0的数据位对应的页会被保护。FLASH_Pages取反后的值被分解成WRP0/1/2/3_Data四个部分, 所以在后面的代码中,可以直接把WRP0/1/2/3_Data变量的值写入到选项字节中。关于这部分运算,您可以亲自代入几个宏进行运算,加深理解。
得到数据后,函数开始对FLASH_OPTKEYR寄存器写入解锁码,然后操作FLASH_CR寄存器的OPTPG位准备写入,写入的时候它直接往指向选项字节的结构体OB赋值, 如OB->WRP0 =WRP0_Data,注意在这部分写入的时候,根据前面的运算,可知WRP0_Data中只包含了WRP0的内容, 而nWRP0的值为0,这个nWRP0的值最终会由芯片自动产生。 代码后面的WRP1/2/3操作类似。
仔细研究了这个库函数后,可知它内部并没有对FLASH_CR的访问作解锁操作,所以在调用本函数前,需要先调用FLASH_Unlock解锁。另外,库文件中并没有直接的函数用于解除保护, 但实际上解除保护也可以使用这个函数来处理,例如使用输入参数0来调用函数FLASH_EnableWriteProtection(0),根据代码的处理,它最终会向WRP0/1/2/3选项字节全写入1, 从而达到整片FLASH解除写保护的目的。
44.3.3. 设置读保护及解除
类似地,库文件中提供了函数FLASH_ReadOutProtection 来设置FLASH的读保护及解除,见 代码清单:保护及解除-3。
代码清单:保护及解除-3 设置读保护及解除(stm32f10x_flash.c文件)
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 | #define RDP_Key ((uint16_t)0x00A5) /** * @brief 使能或关闭读保护 * @note 若芯片本身有对选项字节进行其它操作, 请先读出然后再重新写入,因为本函数会擦除所有选项字节的内容 * @param Newstate: 使能(ENABLE)或关闭(DISABLE) * @retval FLASH Status: 可能的返回值: FLASH_ERROR_PG, * FLASH_ERROR_WRP, FLASH_COMPLETE or FLASH_TIMEOUT. */ FLASH_Status FLASH_ReadOutProtection(FunctionalState NewState) { FLASH_Status status = FLASH_COMPLETE; /* 检查参数 */ assert_param(IS_FUNCTIONAL_STATE(NewState)); status = FLASH_WaitForLastOperation(EraseTimeout); if (status == FLASH_COMPLETE) { /* 写入选项字节解锁码 */ FLASH->OPTKEYR = FLASH_KEY1; FLASH->OPTKEYR = FLASH_KEY2; FLASH->CR |= CR_OPTER_Set; //擦除选项字节 FLASH->CR |= CR_STRT_Set; //开始擦除 /* 等待上一次操作完毕 */ status = FLASH_WaitForLastOperation(EraseTimeout); if (status == FLASH_COMPLETE) { /* 若擦除操作完成,复位 OPTER 位 */ FLASH->CR &= CR_OPTER_Reset; /* 准备写入选项字节 */ FLASH->CR |= CR_OPTPG_Set; if (NewState != DISABLE) { OB->RDP = 0x00;//写入非0xA5值,进行读保护 } else { OB->RDP = RDP_Key; //写入0xA5,解除读保护 } /* 等待上一次操作完毕 */ status = FLASH_WaitForLastOperation(EraseTimeout); if (status != FLASH_TIMEOUT) { /* 若操作完毕,复位 OPTPG 位 */ FLASH->CR &= CR_OPTPG_Reset; } } else { if (status != FLASH_TIMEOUT) { /* 复位 OPTER 位 */ FLASH->CR &= CR_OPTER_Reset; } } } /* 返回设置结果 */ return status; } |
由于读保护都是针对整个芯片的,所以读保护的配置函数相对简单,它通过输入参数ENABLE或DISABL参数来进行保护或解除。它的内部处理与前面介绍的修改选项字节过程完全一致, 当要进行读保护时,往选项字节结构体OB->RDP写入0x00(实际上写入非0xA5的值均可达到目的),而要解除读保护时,则写入0xA5。
要注意的是,本函数同样有对FLASH_CR寄存器的访问,但并没有进行解锁操作,所以调用本函数前,同样需要先使用FLASH_Unlock函数解锁。
44.4. 实验:设置读写保护及解除
在本实验中我们将以实例讲解如何修改选项字节的配置,设置读写保护及解除。
本实验要进行的操作比较特殊,由于设置成读写保护状态后,若不解除保护状态或者解除代码工作不正常,将无法给芯片的FLASH下载新的程序, 所以本程序在开发过程中使用内部SRAM调试的方式开发,便于测试程序(读写保护只影响FLASH,SRAM调试时程序下载到SRAM中,不受影响)。 工程中,提供了FLASH和SRAM调试的版本,见图 两种版本的程序 ,
工程的FLASH版本程序包含完整的保护及解除方案,程序下载到内部FLASH后,它自身可以正常地进行保护及解除。另外, 在学习过程中如果您想亲自修改该代码进行测试,也不用担心把解除操作的代码修改至工作不正常而导致芯片无法解锁报废, 处于这种情况时,只要使用本工程的SRAM版本下载到芯片中,即可实现解锁。只要具备前面章节介绍的SRAM调试知识并备份了SRAM版本的工程即可大胆尝试。
44.4.1. 硬件设计
本实验完全针对内部FLASH的操作,对外部硬件无特殊要求。即使是在SRAM调试模式下,由于是使用Debug强制加载PC和SP指针,所以也无需设置BOOT0和BOOT1的引脚。
44.4.2. 软件设计
本实验的工程名称为“设置读写保护与解除”,学习时请打开该工程配合阅读。为了方便展示及移植, 我们把读写保护相关的代码都编写到“bsp_readWriteProtect.c”及“bsp_readWriteProtect.h”文件中, 这些文件是我们自己编写的,不属于标准库的内容,可根据您的喜好命名文件。
44.4.2.1. 代码分析
设置写保护及解除
在本工程中,定义了一个WriteProtect_Toggle 函数用于设置写保护及解除,见 代码清单:保护及解除-4。
代码清单:保护及解除-4 设置写保护及解除(bsp_readWriteProtect.c文件)
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 | /** * @brief 反转写保护的配置,用于演示若芯片处于写保护状态,则解除,若不是写保护状态,则设置成写保护 * @param 无 * @retval 无 */ void WriteProtect_Toggle(void) { /* 获取写保护寄存器的值进行判断,寄存器位为0表示有保护,为1表示无保护 */ /* 若不等于0xFFFFFFFF,则说明有部分页被写保护了 */ if (FLASH_GetWriteProtectionOptionByte() != 0xFFFFFFFF ) { FLASH_DEBUG("芯片处于写保护状态,即将执行解保护过程..."); //解除对FLASH_CR寄存器的访问限制 FLASH_Unlock(); /* 擦除所有选项字节的内容 */ FLASH_EraseOptionBytes(); /* 对所有页解除 */ FLASH_EnableWriteProtection(0x00000000); FLASH_DEBUG("配置完成,芯片将自动复位加载新配置,复位后芯片会解除写保护状态\r\n"); /* 复位芯片,以使选项字节生效 */ NVIC_SystemReset(); } else { //无写保护 FLASH_DEBUG("芯片处于无写保护状态,即将执行写保护过程..."); //解除对FLASH_CR寄存器的访问限制 FLASH_Unlock(); /* 先擦除所有选项字节的内容,防止因为原有的写保护导致无法写入新的保护配置 */ FLASH_EraseOptionBytes(); /* 对所有页进行写保护 */ FLASH_EnableWriteProtection(FLASH_WRProt_AllPages); FLASH_DEBUG("配置完成,芯片将自动复位加载新配置,复位后芯片会处于写保护状态\r\n"); /* 复位芯片,以使选项字节生效 */ NVIC_SystemReset(); } } |
本函数主要演示写保护和解除功能,若芯片本身处于写保护状态,则解除保护,若芯片本身处于无写保护状态,则设置加入写保护。
WriteProtect_Toggle在操作前会先使用库函数FLASH_GetWriteProtectionOptionByte检测芯片当前的写保护状态,该函数的返回值为FLASH_WRPR寄存器的内容, 它反映了选项字节WRP0/1/2/3的配置。所以在代码中,它判断该函数的返回值不等于0xFFFFFFFF时,可知道芯片至少存在一页被写保护,则程序开始执行解除保护分支。
在解除保护分支中,先调用FLASH_Unlock解除FLASH_CR的访问限制,再使用参数0调用前面介绍的FLASH_EnableWriteProtection函数对所有页解除写保护, 解除配置写入完成后,调用库函数NVIC_SystemReset使芯片产生系统复位,从而使配置生效。
若WriteProtect_Toggle在执行判断时发现芯片本身处于无写保护的状态,则以上述同样的过程向选项字节写入配置, 调用FLASH_EnableWriteProtection函数时使用FLASH_WRProt_AllPages宏,对所有FLASH页加入写保护,最后同样调用NVIC_SystemReset产生系统复位使配置生效。
设置读保护及解除
针对读保护及其解除,本工程定义了ReadProtect_Toggle 函数,见 代码清单:保护及解除-5。
代码清单:保护及解除-5 配置PCROP保护(internalFlash_reset.c文件)
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 | /** * @brief 反转读保护的配置,用于演示 若芯片处于读保护状态,则解除, 若不是读保护状态,则设置成读保护 * @param 无 * @retval 无 */ void ReadProtect_Toggle(void) { if (FLASH_GetReadOutProtectionStatus () == SET ) { FLASH_DEBUG("芯片处于读保护状态\r\n"); //解除对FLASH_CR寄存器的访问限制 FLASH_Unlock(); FLASH_DEBUG("即将解除读保护,解除读保护会把FLASH的所有内容清空"); FLASH_DEBUG("由于解除后程序被清空,所以后面不会有任何提示输出"); FLASH_DEBUG("等待20秒后即可给芯片下载新的程序...\r\n"); FLASH_ReadOutProtection (DISABLE); //即使在此处加入printf串口调试也不会执行的, //因为存储程序的整片FLASH都已被擦除。 FLASH_DEBUG("由于FLASH程序被清空,所以本代码不会被执行,串口不会有本语句输出(SRAM调试模式下例外)\r\n"); } else { FLASH_DEBUG("芯片处于无读保护状态,即将执行读保护过程...\r\n"); //解除对FLASH_CR寄存器的访问限制 FLASH_Unlock(); FLASH_ReadOutProtection (ENABLE); printf("芯片已被设置为读保护,上电复位后生效(必须重新给开发板上电,只按复位键无效)\r\n"); printf("处于保护状态下无法正常下载新程序,必须要先解除保护状态再下载\r\n"); } } |
类似地,本函数主要演示读保护和解除功能,若芯片本身处于读保护状态,则解除保护,若芯片本身处于无读保护状态,则设置加入读保护。
ReadProtect_Toggle在操作前会先使用库函数FLASH_GetReadOutProtectionStatus检测芯片当前的写保护状态,该函数内部通过判断选项字节的RDP值, 返回SET(读保护状态)和RESET(无读保护状态)。
判断后,若进入到解除保护分支,会先调用FLASH_Unlock解除FLASH_CR的访问限制,然后使用前面介绍的FLASH_ReadOutProtection函数以DISABLE作为参数解除读保护。
必须注意的是,该函数执行后,所有存储在内部FLASH时的代码都会被删除,以防止原程序被读出,而由于自身代码已被清除, 所以代码中在FLASH_ReadOutProtection(DISABLE)语句后的串口输出是不会被执行的,因为此时这个程序已经不存在了, 但如果使用SRAM版本的程序测试,它是会有输出的,因为这时本程序自身是存储在内部SRAM空间的。
由于解除保护后会触发芯片FLASH的整片擦除操作,所以要稍等一段时间,等待20秒后,解除操作完成,可以重新给芯片的FLASH下载新的程序。
若ReadProtect_Toggle在执行判断时发现芯片本身处于无读保护的状态,它会使用FLASH_ReadOutProtection(ENABLE)语句把芯片设置为读保护状态。 仔细对比读写保护的配置函数,可以发现读保护设置后并没有调用NVIC_SystemReset函数使芯片产生系统复位,这是因为读保护的设置与解除, 是要使用上电复位才能生效的(即重新给芯片上电),系统复位不会产生效应。
main函数
最后来看看本实验的main函数,见 代码清单:保护及解除-6。
代码清单:保护及解除-6 main函数
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 56 57 58 59 60 | //【 !!】注意事项: //1.当芯片处于读写保护状态时,均无法下载新的程序,需要先解除保护状态后再下载 //2.本工程包含两个版本,可在MDK的“Load”下载按钮旁边的下拉框选择: // FLASH版本:程序下载到STM32的FLASH中,与普通的程序无异 // RAM版本 :程序下载到STM32的内部SRAM中,需要使用RAM调试方式, // 只能点Debug按钮运行(该运行方法可参考SRAM调试章节的说明) //3.若自己修改程序导致使芯片处于读写保护状态而无法下载, // 且 FALSH程序自身又不包含自解除状态的程序, // 可以按SRAM调试的方式运行本工程的“RAM版本”解除,解除即可重新下载。 /* * 函数名:main * 描述 :主函数 * 输入 :无 * 输出 :无 */ int main(void) { /*初始化USART,配置模式为 115200 8-N-1*/ USART_Config(); LED_GPIO_Config(); Key_GPIO_Config(); LED_BLUE; //芯片自动复位后,串口可能有小部分异常输出,如输出一个“?”号 printf("\r\n欢迎使用野火 STM32 开发板。\r\n"); printf("这是读写保护测试实验\r\n"); /* 获取写保护寄存器的值进行判断,寄存器位为0表示有保护,为1表示无保护 */ /* 若不等于0xFFFFFFFF,则说明有部分页被写保护了 */ if (FLASH_GetWriteProtectionOptionByte() !=0xFFFFFFFF ) { printf("\r\n目前芯片处于写保护状态,按Key1键解除保护\r\n"); printf("写保护寄存器的值:WRPR=0x%x\r\n",FLASH_GetWriteProtectionOptionByte()); } else { //无写保护 printf("\r\n目前芯片无 写 保护,按 Key1 键可设置成 写 保护\r\n"); printf("写保护寄存器的值:WRPR=0x%x\r\n",FLASH_GetWriteProtectionOptionByte()); } /* 若等于SET,说明处于读保护状态 */ if (FLASH_GetReadOutProtectionStatus () == SET ) { printf("\r\n目前芯片处于读保护状态,按Key2键解除保护\r\n"); } else { printf("\r\n目前芯片无 读 保护,按 Key2 键可设置成 读 保护\r\n"); } while (1) { if ( Key_Scan(KEY1_GPIO_PORT,KEY1_GPIO_PIN) == KEY_ON ) { LED1_TOGGLE; WriteProtect_Toggle(); } if ( Key_Scan(KEY2_GPIO_PORT,KEY2_GPIO_PIN) == KEY_ON ) { LED2_TOGGLE; ReadProtect_Toggle(); } } } |
在main函数中,初始化了串口、LED、按键等外设后,根据芯片当前的保护状态输出调试信息,接着循环轮询按键, 若按了KEY1按键,则执行前面的WriteProtect_Toggle反转写保护状态,若按了KEY2键,则执行前面的ReadProtect_Toggle反转读保护状态。
44.4.3. 下载测试
本工程包含两个版本,可在MDK的“Load”下载按钮旁边的下拉框选择:
-
FLASH版本:接上串口调试助手后,直接点击MDK的“Load”按钮把程序下载到STM32的FLASH中,复位运行,串口会输出当前芯片的保护状态, 可使用KEY1和KEY2切换。切换写保护状态时,芯片会自动复位,程序重新执行;切换读保护状态时,按键后需要重新给开发板上电复位, 配置才会有效(断电时,串口与电脑的连接会断开,所以上电后注意重新打开串口调试助手),若是执行解除读保护过程, 运行后芯片FLASH中自身的代码都会消失,所以要重新给开发板下载程序。
-
RAM版本 :若无SRAM调试程序的经验,请先学习前面的《SRAM调试》章节。接上串口调试助手后, 只能使用MDK的“Debug”按钮把程序下载到STM32的内部SRAM中, 然后点击全速运行,可在串口查看调试输出。由于SRAM调试状态下,复位会使芯片程序乱飞,所以每次切换状态复位后, 都要重新点击“Debug”按钮下载SRAM程序,再全速运行才能正常查看输出。