51. 设置FLASH的读写保护及解除

本章参考资料:《STM32F4xx参考手册》、《STM32F4xx规格书》、库说明文档《stm32f4xx_dsp_stdperiph_lib_um.chm》 以及《Proprietary code read-out protection on microcontrollers》。

51.1. 选项字节与读写保护

在实际发布的产品中,在STM32芯片的内部FLASH存储了控制程序,如果不作任何保护措施的话,可以使用下载器直接把内部FLASH的内容读取回来, 得到bin或hex文件格式的代码拷贝,别有用心的厂商即可利用该代码文件山寨产品。为此,STM32芯片提供了多种方式保护内部FLASH的程序不被非法读取, 但在默认情况下该保护功能是不开启的,若要开启该功能,需要改写内部FLASH选项字节(Option Bytes)中的配置。

51.1.1. 选项字节的内容

选项字节是一段特殊的FLASH空间,STM32芯片会根据它的内容进行读写保护、复位电压等配置,选项字节的构成见表 选项字节的构成

选项字节的构成

选项字节具体的数据位配置说明见表 选项字节具体的数据位配置说明

选项字节具体的数据位配置说明

我们主要讲解选项字节配置中的RDP位和PCROP位,它们分别用于配置读保护级别及代码读出保护。

51.1.2. RDP读保护级别

修改选项字节的RDP位的值可设置内部FLASH为以下保护级别:

  • 0xAA:级别0,无保护

这是STM32的默认保护级别,它没有任何读保护,读取内部FLASH及“备份SRAM”的内容都没有任何限制。 (注意这里说的“备份SRAM”是指STM32备份域的SRAM空间,不是指主SRAM,下同)

  • 其它值:级别1,使能读保护

把RDP配置成除0xAA或0xCC外的任意数值, 都会使能级别1的读保护。在这种保护下,若使用调试功能(使用下载器、 仿真器)或者从内部SRAM自举时都不能对内部FLASH及备份SRAM作任何访问(读写、擦除都被禁止);而如果STM32是从内部FLASH自举时,它允许对内部FLASH及备份SRAM的任意访问。

也就是说,在级别1模式下,任何尝试从外部访问内部FLASH内容的操作都被禁止,例如无法通过下载器读取它的内容,或编写一个从内部SRAM启动的程序, 若该程序读取内部FLASH,会被禁止。而如果是芯片自己访问内部FLASH,是完全没有问题的, 例如前面的“读写内部FLASH”实验中的代码自己擦写内部FLASH空间的内容,即使处于级别1的读保护,也能正常擦写。

当芯片处于级别1的时候,可以把选项字节的RDP位重新设置为0xAA,恢复级别0。在恢复到级别0前,芯片会自动擦除内部FLASH及备份SRAM的内容, 即降级后原内部FLASH的代码会丢失。在级别1时使用SRAM自举的程序也可以访问选项字节进行修改,所以如果原内部FLASH的代码没有解除读保护的操作时, 可以给它加载一个SRAM自举的程序进行保护降级,后面我们将会进行这样的实验。

  • 0xCC:级别2,禁止调试

把RDP配置成0xCC值时,会进入最高级别的读保护,且设置后无法再降级,它会永久禁止用于调试的JTAG接口(相当于熔断)。在该级别中, 除了具有级别1的所有保护功能外,进一步禁止了从SRAM或系统存储器的自举(即平时使用的串口ISP下载功能也失效),JTAG调试相关的功能被禁止, 选项字节也不能被修改。它仅支持从内部FLASH自举时对内部FLASH及SRAM的访问(读写、擦除)。

由于设置了级别2后无法降级,也无法通过JTAG、串口ISP等方式更新程序,所以使用这个级别的保护时一般会在程序中预留“后门”以更新应用程序, 若程序中没有预留后门,芯片就无法再更新应用程序了。所谓的“后门”是一种IAP程序(In Application Program),它通过某个通讯接口获取将要更新的程序内容, 然后利用内部FLASH擦写操作把这些内容烧录到自己的内部FLASH中,实现应用程序的更新。

不同级别下的访问限制见图 不同级别下的访问限制

不同级别下的访问限制

不同保护级别之间的状态转换见图 不同级别间的状态转换

不同级别间的状态转换

51.2. 修改选项字节的过程

修改选项字节的内容可修改各种配置,但是,当应用程序运行时,无法直接通过选项字节的地址改写它们的内容,例如, 接使用指针操作地址0x1FFFC0000的修改是无效的。要改写其内容时必须设置寄存器FLASH_OPTCR及FLASH_OPTCR1中的对应数据位, 寄存器的与选项字节对应位置见图 FLASH_OPTCR寄存器说明 及图 FLASH_OPTCR1寄存器说明 ,详细说明请查阅《STM32参考手册》。

FLASH_OPTCR寄存器说明 FLASH_OPTCR1寄存器说明

默认情况下,FLASH_OPTCR寄存器中的第0位OPTLOCK值为1,它表示选项字节被上锁,需要解锁后才能进行修改,当寄存器的值设置完成后, 对FLASH_OPTCR寄存器中的第1位OPTSTRT值设置为1,硬件就会擦除选项字节扇区的内容,并把FLASH_OPTCR/1寄存器中包含的值写入到选项字节。

所以,修改选项字节的配置步骤如下:

(1) 解锁,在 Flash 选项密钥寄存器 (FLASH_OPTKEYR) 中写入 OPTKEY1 = 0x0819 2A3B;接着在 Flash 选项密钥寄存器 (FLASH_OPTKEYR) 中写入 OPTKEY2 = 0x4C5D 6E7F。

(2) 检查 FLASH_SR 寄存器中的 BSY 位,以确认当前未执行其它Flash 操作。

(3) 在 FLASH_OPTCR 和/或 FLASH_OPTCR1 寄存器中写入选项字节值。

(4) 将 FLASH_OPTCR 寄存器中的选项启动位 (OPTSTRT) 置 1。

(5) 等待 BSY 位清零,即写入完成。

51.3. 操作选项字节的库函数

为简化编程,STM32标准库提供了一些库函数,它们封装了修改选项字节时操作寄存器的过程。

51.3.1. 选项字节解锁、上锁函数

对选项字节解锁、上锁的函数见代码清单:保护及解除-1。

代码清单:保护及解除-1选项字节解锁、上锁
 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
#define FLASH_OPT_KEY1           ((uint32_t)0x08192A3B)
#define FLASH_OPT_KEY2           ((uint32_t)0x4C5D6E7F)

/**
* @brief  Unlocks the FLASH Option Control Registers access.
* @param  None
* @retval None
*/
void FLASH_OB_Unlock(void)
{
if((FLASH->OPTCR & FLASH_OPTCR_OPTLOCK) != RESET)
{
    /* Authorizes the Option Byte register programming */
    FLASH->OPTKEYR = FLASH_OPT_KEY1;
    FLASH->OPTKEYR = FLASH_OPT_KEY2;
}
}

/**
* @brief  Locks the FLASH Option Control Registers access.
* @param  None
* @retval None
*/
void FLASH_OB_Lock(void)
{
/* Set the OPTLOCK Bit to lock the FLASH Option Byte Registers access */
FLASH->OPTCR |= FLASH_OPTCR_OPTLOCK;
}

解锁的时候,它对FLASH_OPTCR寄存器写入两个解锁参数,上锁的时候,对FLASH_ OPTCR寄存器的FLASH_OPTCR_OPTLOCK位置1。

51.3.2. 设置读保护级别

解锁后设置选项字节寄存器的RDP位可调用FLASH_OB_RDPConfig完成,见代码清单:保护及解除-2。

代码清单:保护及解除-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
27
/**
* @brief  Sets the read protection level.
* @param  OB_RDP: specifies the read protection level.
*          This parameter can be one of the following values:
*            @arg OB_RDP_Level_0: No protection
*            @arg OB_RDP_Level_1: Read protection of the memory
*            @arg OB_RDP_Level_2: Full chip protection
*
* /!\ Warning /!\ When enabling OB_RDP level 2 it's no more possible to go back to level 1 or 0
*
* @retval None
*/
void FLASH_OB_RDPConfig(uint8_t OB_RDP)
{
    FLASH_Status status = FLASH_COMPLETE;

    /* Check the parameters */
    assert_param(IS_OB_RDP(OB_RDP));

    status = FLASH_WaitForLastOperation();

    if(status == FLASH_COMPLETE)
    {
        *(__IO uint8_t*)OPTCR_BYTE1_ADDRESS = OB_RDP;

    }
}

该函数根据输入参数设置RDP寄存器位为相应的级别,其注释警告了若配置成OB_RDP_Level_2会无法恢复。类似地,配置其它选项时也有相应的库函数,如FLASH_OB_PCROP1Config、FLASH_OB_WRP1Config分别用于设置要进行PCROP保护或WRP保护(写保护)的扇区。

51.3.3. 写入选项字节

调用上一步骤中的函数配置寄存器后,还要调用代码清单:保护及解除-3中的FLASH_OB_Launch函数把寄存器的内容写入到选项字节中。

代码清单:保护及解除-3 写入选项字节
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
/**
* @brief  Launch the option byte loading.
* @param  None
* @retval FLASH Status: The returned value can be: FLASH_BUSY, FLASH_ERROR_PROGRAM,
*                       FLASH_ERROR_WRP, FLASH_ERROR_OPERATION or FLASH_COMPLETE.
*/
FLASH_Status FLASH_OB_Launch(void)
{
    FLASH_Status status = FLASH_COMPLETE;

    /* Set the OPTSTRT bit in OPTCR register */
    *(__IO uint8_t *)OPTCR_BYTE0_ADDRESS |= FLASH_OPTCR_OPTSTRT;

    /* Wait for last operation to be completed */
    status = FLASH_WaitForLastOperation();

    return status;
}

该函数设置FLASH_OPTCR_OPTSTRT位后调用了FLASH_WaitForLastOperation函数等待写入完成,并返回写入状态,若操作正常,它会返回FLASH_COMPLETE。

51.4. 实验:设置读写保护及解除

在本实验中我们将以实例讲解如何修改选项字节的配置,更改读保护级别、设置PCROP或写保护,最后把选项字节恢复默认值。

本实验要进行的操作比较特殊,在开发和调试的过程中都是在SRAM上进行的(使用SRAM启动方式)。例如,直接使用FLASH版本的程序进行调试时,如果该程序在运行后对扇区进行了写保护而没有解除的操作或者该解除操作不正常,此时将无法再给芯片的内部FLASH下载新程序,最终还是要使用SRAM自举的方式进行解 除操作。所以在本实验中为便于修改选项字节的参数,我们统一使用SRAM版本的程序进行开发和学习,当SRAM版本调试正常后再改为FLASH版本。

关于在SRAM中调试代码的相关配置,请参考前面的章节。

注意

若您在学习的过程中想亲自修改代码进行测试,请注意备份原工程代码。当芯片的FLASH被保护导致无法下载程序到FLASH时,可以下载本工程到芯片,并使用SRAM启动运行,即可恢复芯片至默认配置。但如果修改了读保护为级别2,采用任何方法都无法恢复!(除了这个配置,其它选项都可以大胆地修改测试。)

51.4.1. 硬件设计

本实验在SRAM中调试代码,因此把BOOT0和BOOT1引脚都使用跳线帽连接到3.3V,使芯片从SRAM中启动。

51.4.2. 软件设计

本实验的工程名称为“设置读写保护与解除”,学习时请打开该工程配合阅读,它是从“RAM调试—多彩流水灯”工程改写而来的。为了方便展示及移植,我们把操作内部FLASH相关的代码都编写到“internalFlash_reset.c”及“internalFlash_reset.h”文件中,这些文件是我们自己 编写的,不属于标准库的内容,可根据您的喜好命名文件。

51.4.2.1. 主要实验

  1. 学习配置扇区写保护;

  2. 学习配置读保护级别;

  3. 学习如何恢复选项字节到默认配置;

51.4.2.2. 代码分析

配置扇区写保护

我们先以代码清单:保护及解除-4中的设置与解除写保护过程来学习如何配置选项字节。

代码清单:保护及解除-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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
#define FLASH_WRP_SECTORS   (OB_WRP_Sector_0|OB_WRP_Sector_1)
__IO uint32_t SectorsWRPStatus = 0xFFF;

/**
* @brief  WriteProtect_Test,普通的写保护配置
* @param 运行本函数后会给扇区FLASH_WRP_SECTORS进行写保护,再重复一次会进行解写保护
* @retval None
*/
void WriteProtect_Test(void)
{
    FLASH_Status status = FLASH_COMPLETE;
    {
            /* 获取扇区的写保护状态 */
            SectorsWRPStatus = FLASH_OB_GetWRP() & FLASH_WRP_SECTORS;

            if (SectorsWRPStatus == 0x00)
            {
                /* 扇区已被写保护,执行解保护过程*/

                /* 使能访问OPTCR寄存器 */
                FLASH_OB_Unlock();

                /* 设置对应的nWRP位,解除写保护 */
                FLASH_OB_WRPConfig(FLASH_WRP_SECTORS, DISABLE);
                status=FLASH_OB_Launch();
                /* 开始对选项字节进行编程 */
                if (status   != FLASH_COMPLETE)
                {
                    FLASH_ERROR("对选项字节编程出错,解除写保护失败,status = %x",status);
                    /* User can add here some code to deal with this error */
                    while (1)
                    {
                    }
                }
                /* 禁止访问OPTCR寄存器 */
                FLASH_OB_Lock();

                /* 获取扇区的写保护状态 */
                SectorsWRPStatus = FLASH_OB_GetWRP() & FLASH_WRP_SECTORS;

                /* 检查是否配置成功 */
                if (SectorsWRPStatus == FLASH_WRP_SECTORS)
                {
                    FLASH_INFO("解除写保护成功!");
                }
                else
                {
                    FLASH_ERROR("未解除写保护!");
                }
                }
                else
                { /* 若扇区未被写保护,开启写保护配置 */

                /* 使能访问OPTCR寄存器 */
                FLASH_OB_Unlock();

                /*使能 FLASH_WRP_SECTORS 扇区写保护 */
                FLASH_OB_WRPConfig(FLASH_WRP_SECTORS, ENABLE);

                status=FLASH_OB_Launch();
                /* 开始对选项字节进行编程 */
                if (status   != FLASH_COMPLETE)
                {
                    FLASH_ERROR("对选项字节编程出错,设置写保护失败,status = %x",status);
                    while (1)
                    {
                    }
                }
                /* 禁止访问OPTCR寄存器 */
                FLASH_OB_Lock();

                /* 获取扇区的写保护状态 */
                SectorsWRPStatus = FLASH_OB_GetWRP() & FLASH_WRP_SECTORS;

                /* 检查是否配置成功 */
                if (SectorsWRPStatus == 0x00)
                {
                    FLASH_INFO("设置写保护成功!");
                }
                else
                {
                    FLASH_ERROR("设置写保护失败!");
                }
            }
    }
}

本函数分成了两个部分,它根据目标扇区的状态进行操作,若原来扇区为非保护状态时就进行写保护,若为保护状态就解除保护。其主要操作过程如下:

  • 调用FLASH_OB_GetWRP函数获取目标扇区的保护状态若扇区被写保护,则开始解除保护过程,否则开始设置写保护过程;

  • 调用FLASH_OB_Unlock解锁选项字节的编程;

  • 调用FLASH_OB_WRPConfig函数配置目标扇区关闭或打开写保护;

  • 调用FLASH_OB_Launch函数把寄存器的配置写入到选项字节;

  • 调用FLASH_OB_GetWRP函数检查是否配置成功;

  • 调用FLASH_OB_Lock禁止修改选项字节。

恢复选项字节为默认值

当芯片被设置为读写保护时,这时给芯片的内部FLASH下载程序时, 可能会出现图 擦除失败提示 和图 擦除进度条卡在开始状态 的擦除FLASH失败的错误提示。

擦除失败提示 擦除进度条卡在开始状态

只要不是把读保护配置成了级别2保护,都可以使用SRAM启动运行 代码清单:保护及解除-5 中的函数恢复选项字节为默认状态,使得FLASH下载能正常进行。

代码清单:保护及解除-5 恢复选项字节为默认值
 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
61
62
63
64
65
66
67
68
69
//@brief   OPTCR register byte 0 (Bits[7:0]) base address
#define OPTCR_BYTE0_ADDRESS         ((uint32_t)0x40023C14)

//@brief   OPTCR register byte 1 (Bits[15:8]) base address
#define OPTCR_BYTE1_ADDRESS         ((uint32_t)0x40023C15)

// @brief   OPTCR register byte 2 (Bits[23:16]) base address
#define OPTCR_BYTE2_ADDRESS         ((uint32_t)0x40023C16)

//@brief   OPTCR register byte 3 (Bits[31:24]) base address
#define OPTCR_BYTE3_ADDRESS         ((uint32_t)0x40023C17)

//@brief   OPTCR1 register byte 0 (Bits[7:0]) base address
#define OPTCR1_BYTE2_ADDRESS         ((uint32_t)0x40023C1A)

/**
* @brief  InternalFlash_Reset,恢复内部FLASH的默认配置
* @param  None
* @retval None
*/
int InternalFlash_Reset(void)
{
    FLASH_Status status = FLASH_COMPLETE;

    /* 使能访问选项字节寄存器 */
    FLASH_OB_Unlock();

    /* 擦除用户区域 (用户区域指程序本身没有使用的空间,可以自定义)**/
    /* 清除各种FLASH的标志位 */
     FLASH_ClearFlag(FLASH_FLAG_EOP | FLASH_FLAG_OPERR | FLASH_FLAG_WRPERR|
        FLASH_FLAG_PGAERR | FLASH_FLAG_PGPERR|FLASH_FLAG_PGSERR);

    FLASH_INFO("\r\n");
    FLASH_INFO("正在准备恢复的条件,请耐心等待...");

    //确保把读保护级别设置为LEVEL1,以便恢复
    //必须是读保护级别由LEVEL1转为LEVEL0时才有效,
    //否则修改无效
    FLASH_OB_RDPConfig(OB_RDP_Level_1);

    status=FLASH_OB_Launch();

    status = FLASH_WaitForLastOperation();

    //设置为LEVEL0

    FLASH_INFO("\r\n");
    FLASH_INFO("正在擦除内部FLASH的内容,请耐心等待...");

    FLASH_OB_RDPConfig(OB_RDP_Level_0);

    status =FLASH_OB_Launch();

    //设置其它位为默认值
    (*(__IO uint32_t *)(OPTCR_BYTE0_ADDRESS))=0x0FFFAAE9;
    (*(__IO uint16_t *)(OPTCR1_BYTE2_ADDRESS))=0x0FFF;
    status =FLASH_OB_Launch();

    if (status   != FLASH_COMPLETE) {
        FLASH_ERROR("恢复选项字节默认值失败,错误代码:status=%X",status);
    } else {
        FLASH_INFO("恢复选项字节默认值成功!");
    }

    //禁止访问
    FLASH_OB_Lock();

    return status;
}

这个函数进行了如下操作:

  • 调用FLASH_OB_Unlock解锁选项字节的编程;

  • 调用FLASH_ClearFlag函数清除所有FLASH异常状态标志;

  • 调用FLASH_OB_RDPConfig函数设置为读保护级别1,以便后面能正常关闭PCROP模式;

  • 调用FLASH_OB_Launch写入选项字节并等待读保护级别设置完毕;

  • 调用FLASH_OB_RDPConfig函数把读保护级别降为0;

  • 调用FLASH_OB_Launch定稿选项字节并等待降级完毕,由于这个过程需要擦除内部FLASH的内容,等待的时间会比较长;

  • 直接操作寄存器,使用“(*(__IO uint32_t *)(OPTCR_BYTE0_ADDRESS))=0x0FFFAAE9;”和 “(*(__IO uint16_t *)(OPTCR1_BYTE2_ADDRESS))=0x0FFF;”语句把OPTCR及OPTCR1寄存器与选项字节相关的位都恢复默认值;

  • 调用FLASH_OB_Launch函数等待上述配置被写入到选项字节;

  • 恢复选项字节为默认值操作完毕。

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
/**
* @brief  主函数
* @param  无
* @retval 无
*/
int main(void)
{
    /* LED 端口初始化 */
    LED_GPIO_Config();
    Debug_USART_Config();
    LED_BLUE;

    FLASH_INFO("本程序将会被下载到STM32的内部SRAM运行。");
    FLASH_INFO("下载程序前,请确认把实验板的BOOT0和BOOT1引脚都接到3.3V电源处!!");

    FLASH_INFO("\r\n");
    FLASH_INFO("----这是一个STM32芯片内部FLASH解锁程序----");
    FLASH_INFO("程序会把芯片的内部FLASH选项字节恢复为默认值");

#if 0  //工程调试、演示时使用,正常解除时不需要运行此函数
    WriteProtect_Test(); //修改写保护位,仿真芯片扇区被设置成写保护的的环境
#endif

    OptionByte_Info();

    /*恢复选项字节到默认值,解除保护*/
    if (InternalFlash_Reset()==FLASH_COMPLETE) {
        FLASH_INFO("选项字节恢复成功,请把BOOT0和BOOT1引脚都连接到GND,");
    FLASH_INFO("然后随便找一个普通的程序,下载程序到芯片的内部FLASH进行测试");
        LED_GREEN;
    } else {
        FLASH_INFO("选项字节恢复成功失败,请复位重试");
        LED_RED;
    }

    OptionByte_Info();

    while (1) {

    }
}

在main函数中,主要是调用了InternalFlash_Reset函数把选项字节恢复成默认值,程序默认时没有调用WriteProtect_Test函数设置写保护, 若您想观察实验现象,可修改条件编译的宏,使它加入到编译中。

51.4.2.3. 下载测试

把开发板的BOOT0和BOOT1引脚都使用跳线帽连接到3.3V电源处,使它以SRAM方式启动,然后用USB线连接开发板“USB TO UART”接口跟电脑, 在电脑端打开串口调试助手,把编译好的程序下载到开发板并复位运行,在串口调试助手可看到调试信息。程序运行后, 请耐心等待至开发板亮绿灯或串口调试信息提示恢复完毕再给开发板断电,否则由于恢复过程被中断,芯片内部FLASH会处于保护状态。

芯片内部FLASH处于保护状态时,可重新下载本程序到开发板以SRAM运行恢复默认配置。