在实际发布的产品中,在STM32芯片的内部FLASH存储了控制程序,如果不作任何保护措施的话,可以使用下载器直接把内部FLASH的内容读取回来,得到bin或hex文件格式的代码拷贝,别有用心的厂商即可利用该代码文件山寨产品。为此,STM32芯片提供了多种方式保护内部FLASH的程序不被非法读取,但在默认情况下该保护功能是不开启的,若要开启该功能,需要改写内部FLASH选项字节(Option Bytes)中的配置。
11 选项字节与读写保护
选项字节是一段特殊的FLASH空间,STM32芯片会根据它的内容进行读写保护等配置,选项字节的构成如下:
STM32F103系列芯片的选项字节有8个配置项,即上表中的USER、RDP、DATA0/1及WRP0/1/2/3,而表中带n的同类项是该项的反码,即nUSER的值等于(~USER)、nRDP的值等于(~RDP),STM32利用反码来确保选项字节内容的正确性。
选项字节的内容:
数据位配置(第一部分):
数据位配置(第二部分):
其中的RDP位和WRP位,它们分别用于配置读保护和写保护
RDP读保护级别
修改选项字节的RDP位的值可设置内部FLASH为以下保护级别:
- 0xA5:级别0,无保护 这是STM32的默认保护级别,它没有任何读保护,读取内部FLASH的内容都没有任何限制。也就是说,第三方可以使用调试器等工具,获取该芯片FLASH中存储的程序,然后可以把获得的程序以bin和hex的格式下载到另一块STM32芯片中,加上PCB抄板技术,轻易复制出同样的产品。
- 其它值:级别1,使能读保护 把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的代码会丢失,从而防止降级后原内容被读取到。
芯片被配置成读保护后根据不同的使用情况,访问权限不同,总结如下表:
WRP写保护
使用选项字节的WRP0/1/2/3可以设置主FLASH的写保护,防止它存储的程序内容被修改。
- 设置写保护
写保护的配置一般以4K字节为单位,除WRP3的最后一位比较特殊外,每个WRP选项字节的一位用于控制4K字节的写访问权限, 把对应WRP的位置0即可把它匹配的空间加入写保护。被设置成写保护后,主FLASH中的内容使用任何方式都不能被擦除和写入,写保护不会影响读访问权限,读访问权限完全由前面介绍的读保护设置限制。 - 解除写保护
解除写保护是逆过程,把对应WRP的位置1即可把它匹配的空间解除写保护。解除写保护后,主FLASH中的内容不会像解读保护那样丢失,它会被原样保留。
12 修改选项字节的过程
根据前面的说明,修改选项字节的内容可修改读写保护配置,不过选项字节复位后的默认状态是始终可以读但被写保护的,因此它具有类似前面《读写内部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擦除操作完成。
- 若是设置读保护及其解除,需要给芯片重新上电复位,以使新配置的选项字节生效;对于设置写保护及其解除,需要给芯片进行系统复位,以使新配置的选项字节生效。
13 操作选项字节的库函数
1.选项字结构体定义
标准库中定义的选项字节结构体,包含了RDP、USER、DATA0/1及WRP0/1/2/3这些内容,每个结构体成员指向选项字节对应选项的原始配置码及反码。不过,根据手册中的说明可了解到,当向选项字节的这些地址写入配置时,它会自动取低位字节计算出高位字节的值再存储,即自动取反码,非常方便。例如程序中执行操作给结构体成员WRP0赋值为0x0011时,最终它会自动写入0xEE11(0xEE是0x11的反码)。最后,从OB_BASE宏的定义可以确认它所指向的正是前面介绍的选项字节基地址,说明若在程序中使用该结构体赋值,会直接把内容写入到选项字节地址对应的空间中。
1 /**
2 * @brief 选项字节结构体
3 */
4 typedef struct {
5 __IO uint16_t RDP; /*RDP 及 nRDP*/
6 __IO uint16_t USER; /*USER 及 nUSER, 下面类似 */
7 __IO uint16_t Data0;
8 __IO uint16_t Data1;
9 __IO uint16_t WRP0;
10 __IO uint16_t WRP1;
11 __IO uint16_t WRP2;
12 __IO uint16_t WRP3;
13 } OB_TypeDef;
14
15 /* 强制转换为选项字节结构体指针 */
16 #define OB ((OB_TypeDef *) OB_BASE)
17 /* 选项字节基地址 */
18 #define OB_BASE ((uint32_t)0x1FFFF800)
库文件提供了 FLASH_EnableWriteProtection 函数,可用于设置写保护及解除:
1 #define RDP_Key ((uint16_t)0x00A5)
2
3 /**
4 * @brief 使能或关闭读保护
5 * @note 若芯片本身有对选项字节进行其它操作,
6 请先读出然后再重新写入,因为本函数会擦除所有选项字节的内容
7
8 * @param Newstate: 使能(ENABLE)或关闭(DISABLE)
9 * @retval FLASH Status: 可能的返回值: FLASH_ERROR_PG,
10 * FLASH_ERROR_WRP, FLASH_COMPLETE or FLASH_TIMEOUT.
11 */
12 FLASH_Status FLASH_ReadOutProtection(FunctionalState NewState)
13 {
14 FLASH_Status status = FLASH_COMPLETE;
15 /* 检查参数 */
16 assert_param(IS_FUNCTIONAL_STATE(NewState));
17 status = FLASH_WaitForLastOperation(EraseTimeout);
18 if (status == FLASH_COMPLETE) {
19 /* 写入选项字节解锁码 */
20 FLASH->OPTKEYR = FLASH_KEY1;
21 FLASH->OPTKEYR = FLASH_KEY2;
22 FLASH->CR |= CR_OPTER_Set; //擦除选项字节
23 FLASH->CR |= CR_STRT_Set; //开始擦除
24 /* 等待上一次操作完毕 */
25 status = FLASH_WaitForLastOperation(EraseTimeout);
26 if (status == FLASH_COMPLETE) {
27 /* 若擦除操作完成,复位 OPTER 位 */
28 FLASH->CR &= CR_OPTER_Reset;
29 /* 准备写入选项字节 */
30 FLASH->CR |= CR_OPTPG_Set;
31 if (NewState != DISABLE) {
32 OB->RDP = 0x00;//写入非 0xA5 值,进行读保护
33 } else {
34 OB->RDP = RDP_Key; //写入 0xA5,解除读保护
35 }
36 /* 等待上一次操作完毕 */
37 status = FLASH_WaitForLastOperation(EraseTimeout);
38
39 if (status != FLASH_TIMEOUT) {
40 /* 若操作完毕,复位 OPTPG 位 */
41 FLASH->CR &= CR_OPTPG_Reset;
42 }
43 } else {
44 if (status != FLASH_TIMEOUT) {
45 /* 复位 OPTER 位 */
46 FLASH->CR &= CR_OPTER_Reset;
47 }
48 }
49 }
50 /* 返回设置结果 */
51 return status;
52 }
该函数的输入参数可选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解除写保护的目的。
2.设置写保护及解除
由于读保护都是针对整个芯片的,所以读保护的配置函数相对简单,它通过输入参数ENABLE或DISABL参数来进行保护或解除。它的内部处理与前面介绍的修改选项字节过程完全一致,当要进行读保护时,往选项字节结构体OB->RDP写入0x00(实际上写入非0xA5的值均可达到目的),而要解除读保护时,则写入0xA5。 要注意的是,本函数同样有对FLASH_CR寄存器的访问,但并没有进行解锁操作,所以调用本函数前,同样需要先使用FLASH_Unlock函数解锁。
1 #define RDP_Key ((uint16_t)0x00A5)
2
3
4 /**
5 * @brief 使能或关闭读保护
6 * @note 若芯片本身有对选项字节进行其它操作,
7 请先读出然后再重新写入,因为本函数会擦除所有选项字节的内容
8
9 * @param Newstate: 使能(ENABLE)或关闭(DISABLE)
10 * @retval FLASH Status: 可能的返回值: FLASH_ERROR_PG,
11 * FLASH_ERROR_WRP, FLASH_COMPLETE or FLASH_TIMEOUT.
12 */
13 FLASH_Status FLASH_ReadOutProtection(FunctionalState NewState)
14 {
15 FLASH_Status status = FLASH_COMPLETE;
16 /* 检查参数 */
17 assert_param(IS_FUNCTIONAL_STATE(NewState));
18 status = FLASH_WaitForLastOperation(EraseTimeout);
19 if (status == FLASH_COMPLETE) {
20 /* 写入选项字节解锁码 */
21 FLASH->OPTKEYR = FLASH_KEY1;
22 FLASH->OPTKEYR = FLASH_KEY2;
23 FLASH->CR |= CR_OPTER_Set; //擦除选项字节
24 FLASH->CR |= CR_STRT_Set; //开始擦除
25 /* 等待上一次操作完毕 */
26 status = FLASH_WaitForLastOperation(EraseTimeout);
27 if (status == FLASH_COMPLETE) {
28 /* 若擦除操作完成,复位 OPTER 位 */
29 FLASH->CR &= CR_OPTER_Reset;
30 /* 准备写入选项字节 */
31 FLASH->CR |= CR_OPTPG_Set;
32 if (NewState != DISABLE) {
33 OB->RDP = 0x00;//写入非 0xA5 值,进行读保护
34 } else {
35 OB->RDP = RDP_Key; //写入 0xA5,解除读保护
36 }
37 /* 等待上一次操作完毕 */
38 status = FLASH_WaitForLastOperation(EraseTimeout);
39
40 if (status != FLASH_TIMEOUT) {
41 /* 若操作完毕,复位 OPTPG 位 */
42 FLASH->CR &= CR_OPTPG_Reset;
43 }
44 } else {
45 if (status != FLASH_TIMEOUT) {
46 /* 复位 OPTER 位 */
47 FLASH->CR &= CR_OPTER_Reset;
48 }
49 }
50 }
51 /* 返回设置结果 */
52 return status;
53 }
要注意的是,本函数同样有对 FLASH_CR 寄存器的访问,但并没有进行解锁操作,所以调用本 函数前,同样需要先使用 FLASH_Unlock 函数解锁。
14 实验:设置读写保护及解除
本实验要进行的操作比较特殊,由于设置成读写保护状态后,若不解除保护状态或者解除代码
工作不正常,将无法给芯片的 FLASH 下载新的程序,所以本程序在开发过程中使用内部 SRAM
调试的方式开发,便于测试程序(读写保护只影响 FLASH,SRAM 调试时程序下载到 SRAM 中,
不受影响)。工程中,提供了 FLASH 和 SRAM 调试的版本,见图两种版本的程序
工程的 FLASH 版本程序包含完整的保护及解除方案,程序下载到内部 FLASH 后,它自身可以正
常地进行保护及解除。另外,在学习过程中如果您想亲自修改该代码进行测试,也不用担心把解
除操作的代码修改至工作不正常而导致芯片无法解锁报废,处于这种情况时,只要使用本工程的
SRAM 版本下载到芯片中,即可实现解锁。只要具备前面章节介绍的 SRAM 调试知识并备份了
SRAM 版本的工程即可大胆尝试。
bsp_readWriteProtect.c
#include "./protect/bsp_readWriteProtect.h"
#include "./usart/bsp_usart.h"
/**
* @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();
}
}
/**
* @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");
}
}
bsp_readWriteProtect.h
#ifndef __INTERNAL_FLASH_H
#define __INTERNAL_FLASH_H
#include "stm32f10x.h"
/* 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)
typedef enum
{
WRITE_PROTECTED = 0,
NO_WRITE_PROTECT
} Protect_Status;
/*********************************************/
/*信息输出*/
#define FLASH_DEBUG_ON 1
#define FLASH_INFO(fmt,arg...) printf("<<-FLASH-INFO->> "fmt"\n",##arg)
#define FLASH_ERROR(fmt,arg...) printf("<<-FLASH-ERROR->> "fmt"\n",##arg)
#define FLASH_DEBUG(fmt,arg...) do{\
if(FLASH_DEBUG_ON)\
printf("<<-FLASH-DEBUG->> [%d]"fmt"\n",__LINE__, ##arg);\
}while(0)
Protect_Status WriteProtect_Status(void);
void WriteProtect_Toggle(void);
void ReadProtect_Toggle(void);
#endif /* __INTERNAL_FLASH_H */
main.c
//【 !!】注意事项:
//1.当芯片处于读写保护状态时,均无法下载新的程序,需要先解除保护状态后再下载
//2.本工程包含两个版本,可在MDK的“Load”下载按钮旁边的下拉框选择:
// FLASH版本: 接上串口调试助手后,直接点击MDK的“Load”按钮把程序下载到STM32的FLASH中,
// 复位运行,串口会输出当前芯片的保护状态,可使用KEY1和KEY2切换。切换写保护
// 状态时,芯片会自动复位,程序重新执行;切换读保护状态时,按键后需要重新给
// 开发板上电复位,配置才会有效(断电时,串口与电脑的连接会断开,所以上电后
// 注意重新打开串口调试助手),若是执行解除读保护过程,运行后芯片FLASH中自身
// 的代码都会消失,所以要重新给开发板下载程序。
// RAM版本 : 若无SRAM调试程序的经验,请先学习前面的《SRAM调试》章节。接上串口调试助手后,
// 只能使用MDK的“Debug”按钮把程序下载到STM32的内部SRAM中,然后点击全速运行,
// 可在串口查看调试输出。由于SRAM调试状态下,复位会使芯片程序乱飞,所以每次切
// 换状态后,都要重新点击“Debug”按钮下载SRAM程序,再全速运行查看输出。
//3.若自己修改程序导致使芯片处于读写保护状态而无法下载,
// 且 FALSH程序自身又不包含自解除状态的程序,可以使用本工程的“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();
}
}
}
void Delay(__IO uint32_t nCount)
{
for(; nCount != 0; nCount--);
}