50. 读写内部FLASH

本章参考资料:《STM32F4xx参考手册》、《STM32F4xx规格书》、库说明文档《stm32f4xx_dsp_stdperiph_lib_um.chm》。

50.1. STM32的内部FLASH简介

在STM32芯片内部有一个FLASH存储器,它主要用于存储代码,我们在电脑上编写好应用程序后,使用下载器把编译后的代码文件烧录到该内部FLASH中, 由于FLASH存储器的内容在掉电后不会丢失,芯片重新上电复位后,内核可从内部FLASH中加载代码并运行,见图 STM32的内部框架图

STM32的内部框架图

除了使用外部的工具(如下载器)读写内部FLASH外,STM32芯片在运行的时候,也能对自身的内部FLASH进行读写,因此, 若内部FLASH存储了应用程序后还有剩余的空间,我们可以把它像外部SPI-FLASH那样利用起来,存储一些程序运行时产生的需要掉电保存的数据。

由于访问内部FLASH的速度要比外部的SPI-FLASH快得多,所以在紧急状态下常常会使用内部FLASH存储关键记录;为了防止应用程序被抄袭, 有的应用会禁止读写内部FLASH中的内容,或者在第一次运行时计算加密信息并记录到某些区域,然后删除自身的部分加密代码,这些应用都涉及到内部FLASH的操作。

50.1.1. 内部FLASH的构成

STM32的内部FLASH包含主存储器、系统存储器、OTP区域以及选项字节区域,它们的地址分布及大小见表 STM32内部FLASH的构成

STM32内部FLASH的构成

各个存储区域的说明如下:

  • 主存储器

一般我们说STM32内部FLASH的时候,都是指这个主存储器区域,它是存储用户应用程序的空间,芯片型号说明中的1M FLASH、2M FLASH都是指这个区域的大小。 主存储器分为两块,共2MB,每块内分12个扇区,其中包含4个16KB扇区、1个64KB扇区和7个128KB的扇区。如我们实验板中使用的STM32F407ZGT6型号芯片, 它的主存储区域大小为1MB,所以它只包含有表中的扇区0-扇区11。

与其它FLASH一样,在写入数据前,要先按扇区擦除,而有的时候我们希望能以小规格操纵存储单元, 所以STM32F42x/43x针对1MB FLASH的产品还提供了一种双块的存储格式,见表 1MB产品的双块存储格式 。(2M的产品按表 STM32内部FLASH的构成 的格式)

1MB产品的双块存储格式

通过配置FLASH选项控制寄存器FLASH_OPTCR的DB1M位,可以切换这两种格式,切换成双块模式后,扇区8-11的空间被转移到扇区12-19中,扇区细分了,总容量不变。

要强调的是:本实验板采用的是STM32F40x系列的芯片,它没有双块存储格式,也不存在扇区12-23,仅STM32F42x/43x系列产品才支持扇区12-23。

  • 系统存储区

系统存储区是用户不能访问的区域,它在芯片出厂时已经固化了启动代码,它负责实现串口、USB以及CAN等ISP烧录功能。

  • OTP区域

OTP(One Time Program),指的是只能写入一次的存储区域,容量为512字节,写入后数据就无法再更改,OTP常用于存储应用程序的加密密钥。

  • 选项字节

选项字节用于配置FLASH的读写保护、电源管理中的BOR级别、软件/硬件看门狗等功能,这部分共32字节。可以通过修改FLASH的选项控制寄存器修改。

50.2. 对内部FLASH的写入过程

50.2.1. 解锁

由于内部FLASH空间主要存储的是应用程序,是非常关键的数据,为了防止误操作修改了这些内容,芯片复位后默认会给FLASH上锁, 这个时候不允许设置FLASH的控制寄存器,并且不能对修改FLASH中的内容。

所以对FLASH写入数据前,需要先给它解锁。解锁的操作步骤如下:

(1) 往Flash 密钥寄存器 FLASH_KEYR中写入 KEY1 = 0x45670123

(2) 再往Flash 密钥寄存器 FLASH_KEYR中写入 KEY2 = 0xCDEF89AB

50.2.2. 数据操作位数

在内部FLASH进行擦除及写入操作时,电源电压会影响数据的最大操作位数,该电源电压可通过配置FLASH_CR 寄存器中的 PSIZE位改变,见表 数据操作位数

数据操作位数

最大操作位数会影响擦除和写入的速度,其中64位宽度的操作除了配置寄存器位外,还需要在Vpp引脚外加一个8-9V的电压源, 且其供电时间不得超过一小时,否则FLASH可能损坏,所以64位宽度的操作一般是在量产时对FLASH写入应用程序时才使用,大部分应用场合都是用32位的宽度。

50.2.2.1. 擦除扇区

在写入新的数据前,需要先擦除存储区域,STM32提供了扇区擦除指令和整个FLASH擦除(批量擦除)的指令,批量擦除指令仅针对主存储区。

扇区擦除的过程如下:

(1) 检查 FLASH_SR 寄存器中的“忙碌寄存器位 BSY”,以确认当前未执行任何 Flash 操作;

(2) 在 FLASH_CR 寄存器中,将“激活扇区擦除寄存器位SER ”置 1,并设置“扇区编号寄存器位SNB”,选择要擦除的扇区;

(3) 将 FLASH_CR 寄存器中的“开始擦除寄存器位 STRT ”置 1,开始擦除;

(4) 等待 BSY 位被清零时,表示擦除完成。

50.2.2.2. 写入数据

擦除完毕后即可写入数据,写入数据的过程并不是仅仅使用指针向地址赋值,赋值前还还需要配置一系列的寄存器,步骤如下:

(1) 检查 FLASH_SR 中的 BSY 位,以确认当前未执行任何其它的内部 Flash 操作;

(2) 将 FLASH_CR 寄存器中的 “激活编程寄存器位PG” 置 1;

(3) 针对所需存储器地址(主存储器块或 OTP 区域内)执行数据写入操作;

(4) 等待 BSY 位被清零时,表示写入完成。

50.2.2.3. 查看工程的空间分布

由于内部FLASH本身存储有程序数据,若不是有意删除某段程序代码,一般不应修改程序空间的内容, 所以在使用内部FLASH存储其它数据前需要了解哪一些空间已经写入了程序代码,存储了程序代码的扇区都不应作任何修改。 通过查询应用程序编译时产生的“*.map”后缀文件,可以了解程序存储到了哪些区域, 它在工程中的打开方式见图 打开工程的map文件 ,也可以到工程目录中的“Listing”文件夹中找到。

打开工程的map文件

打开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
24
25
26
27
28
29
30
==============================================================================

Memory Map of the image //存储分布映像

Image Entry point : 0x08000189
/*程序ROM加载空间*/
Load Region LR_IROM1 (Base: 0x08000000, Size: 0x00000aa4, Max: 0x00100000, ABSOLUTE)
/*程序ROM执行空间*/
Execution Region ER_IROM1 (Base: 0x08000000, Size: 0x00000a90, Max: 0x00100000, ABSOLUTE)
/*地址分布列表*/
Base Addr    Size         Type   Attr      Idx    E Section Name        Object

0x08000000   0x00000188   Data   RO            3    RESET               startup_stm32f40xx.o
0x08000188   0x00000000   Code   RO         4963  * .ARM.Collect$$$$00000000  mc_w.l(entry.o)
0x08000188   0x00000004   Code   RO         5226    .ARM.Collect$$$$00000001  mc_w.l(entry2.o)
0x0800018c   0x00000004   Code   RO         5229    .ARM.Collect$$$$00000004  mc_w.l(entry5.o)
0x08000190   0x00000000   Code   RO         5231    .ARM.Collect$$$$00000008  mc_w.l(entry7b.o)
0x08000190   0x00000000   Code   RO         5233    .ARM.Collect$$$$0000000A  mc_w.l(entry8b.o)
0x08000190   0x00000008   Code   RO         5234    .ARM.Collect$$$$0000000B  mc_w.l(entry9a.o)
0x08000198   0x00000000   Code   RO         5236    .ARM.Collect$$$$0000000D  mc_w.l(entry10a.o)
/*...此处省略大部分内容*/
0x08000902   0x00000002   PAD
0x08000904   0x00000010   Code   RO         4967    i.__0printf$bare    mc_w.l(printfb.o)
0x08000914   0x0000000e   Code   RO         5268    i.__scatterload_copy  mc_w.l(handlers.o)
0x08000922   0x00000002   Code   RO         5269    i.__scatterload_null  mc_w.l(handlers.o)
0x08000924   0x0000000e   Code   RO         5270    i.__scatterload_zeroinit  mc_w.l(handlers.o)
0x08000932   0x00000022   Code   RO         4974    i._printf_core      mc_w.l(printfb.o)
0x08000954   0x00000024   Code   RO         4879    i.fputc             bsp_debug_usart.o
0x08000978   0x000000f8   Code   RO         4765    i.main              main.o
0x08000a70   0x00000020   Data   RO         5266    Region$$Table       anon$$obj.o

这一段是某工程的ROM存储器分布映像,在STM32芯片中,ROM区域的内容就是指存储到内部FLASH的代码。

50.2.2.4. 程序ROM的加载与执行空间

上述说明中有两段分别以“Load Region LR_ROM1”及“Execution RegionER_IROM1”开头的内容,它们分别描述程序的加载及执行空间。 在芯片刚上电运行时,会加载程序及数据,例如它会从程序的存储区域加载到程序的执行区域, 还把一些已初始化的全局变量从ROM复制到RAM空间,以便程序运行时可以修改变量的内容。加载完成后,程序开始从执行区域开始执行。

在上面map文件的描述中,我们了解到加载及执行空间的基地址(Base)都是0x08000000,它正好是STM32内部FLASH的首地址, 即STM32的程序存储空间就直接是执行空间;它们的大小(Size)分别为0x00000aa4及0x00000a90, 执行空间的ROM比较小的原因就是因为部分RW-data类型的变量被拷贝到RAM空间了;它们的最大空间(Max)均为0x00100000,即1M字节,它指的是内部FLASH的最大空间。

计算程序占用的空间时,需要使用加载区域的大小进行计算,本例子中应用程序使用的内部FLASH是从0x08000000至(0x08000000+0x00000aa4)地址的空间区域。

50.2.2.5. ROM空间分布表

在加载及执行空间总体描述之后,紧接着一个ROM详细地址分布表,它列出了工程中的各个段(如函数、常量数据)所在的地址BaseAddr及占用的空间Size, 列表中的Type说明了该段的类型,CODE表示代码,DATA表示数据,而PAD表示段之间的填充区域,它是无效的内容,PAD区域往往是为了解决地址对齐的问题。

观察表中的最后一项,它的基地址是0x08000a70,大小为0x00000020,可知它占用的最高的地址空间为0x08000a90,跟执行区域的最高地址0x00000a90一样, 但它们比加载区域说明中的最高地址0x8000aa4要小,所以我们以加载区域的大小为准。对比表 STM32内部FLASH的构成 的内部FLASH扇区地址分布表, 可知仅使用扇区0就可以完全存储本应用程序,所以从扇区1(地址0x08004000)后的存储空间都可以作其它用途,使用这些存储空间时不会篡改应用程序空间的数据。

50.3. 操作内部FLASH的库函数

为简化编程,STM32标准库提供了一些库函数,它们封装了对内部FLASH写入数据操作寄存器的过程。

50.3.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  Unlocks the FLASH control register access
* @param  None
* @retval None
*/
void FLASH_Unlock(void)
{
    if ((FLASH->CR & FLASH_CR_LOCK) != RESET) {
        /* Authorize the FLASH Registers access */
        FLASH->KEYR = FLASH_KEY1;
        FLASH->KEYR = FLASH_KEY2;
    }
}

/**
* @brief  Locks the FLASH control register access
* @param  None
* @retval None
*/
void FLASH_Lock(void)
{
    /* Set the LOCK Bit to lock the FLASH Registers access */
    FLASH->CR |= FLASH_CR_LOCK;
}

解锁的时候,它对FLASH_KEYR寄存器写入两个解锁参数,上锁的时候,对FLASH_CR寄存器的FLASH_CR_LOCK位置1。

50.3.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
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
/**
* @brief  Erases a specified FLASH Sector.
*
* @note   If an erase and a program operations are requested simultaneously,
*         the erase operation is performed before the program one.
*
* @param  FLASH_Sector: The Sector number to be erased.
*
*  @note  For STM32F42xxx/43xxx devices this parameter can be a value between
*         FLASH_Sector_0 and FLASH_Sector_23.
*
* @param  VoltageRange: The device voltage range which defines the erase parallelism.
*          This parameter can be one of the following values:
*            @arg VoltageRange_1: when the device voltage range is 1.8V to 2.1V,
*                                  the operation will be done by byte (8-bit)
*            @arg VoltageRange_2: when the device voltage range is 2.1V to 2.7V,
*                                  the operation will be done by half word (16-bit)
*  @arg VoltageRange_3: when the device voltage range is 2.7V to 3.6V,
*                                  the operation will be done by word (32-bit)
*   @arg VoltageRange_4: when the device voltage range is 2.7V to 3.6V + External Vpp,
*                                  the operation will be done by double word (64-bit)
*
* @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_EraseSector(uint32_t FLASH_Sector, uint8_t VoltageRange)
{
    uint32_t tmp_psize = 0x0;
    FLASH_Status status = FLASH_COMPLETE;

    /* Check the parameters */
    assert_param(IS_FLASH_SECTOR(FLASH_Sector));
    assert_param(IS_VOLTAGERANGE(VoltageRange));

    if (VoltageRange == VoltageRange_1) {
        tmp_psize = FLASH_PSIZE_BYTE;
    } else if (VoltageRange == VoltageRange_2) {
        tmp_psize = FLASH_PSIZE_HALF_WORD;
    } else if (VoltageRange == VoltageRange_3) {
        tmp_psize = FLASH_PSIZE_WORD;
    } else {
        tmp_psize = FLASH_PSIZE_DOUBLE_WORD;
    }
    /* Wait for last operation to be completed */
    status = FLASH_WaitForLastOperation();

    if (status == FLASH_COMPLETE) {
        /* if the previous operation is completed, proceed to erase the sector */
        FLASH->CR &= CR_PSIZE_MASK;
        FLASH->CR |= tmp_psize;
        FLASH->CR &= SECTOR_MASK;
        FLASH->CR |= FLASH_CR_SER | FLASH_Sector;
        FLASH->CR |= FLASH_CR_STRT;

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

        /* if the erase operation is completed, disable the SER Bit */
        FLASH->CR &= (~FLASH_CR_SER);
        FLASH->CR &= SECTOR_MASK;
    }
    /* Return the Erase Status */
    return status;
}

本函数包含两个输入参数,分别是要擦除的扇区号和工作电压范围,选择不同电压时实质是选择不同的数据操作位数, 参数中可输入的宏在注释里已经给出。函数根据输入参数配置PSIZE位,然后擦除扇区,擦除扇区的时候需要等待一段时间, 它使用FLASH_WaitForLastOperation等待,擦除完成的时候才会退出FLASH_EraseSector函数。

50.3.3. 写入数据

对内部FLASH写入数据不像对外部SRAM操作那样直接指针操作就完成了,还要设置一系列的寄存器,利用FLASH_ProgramWord、 FLASH_ProgramHalfWord和FLASH_ProgramByte函数可按字、半字及字节单位写入数据,见 代码清单: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
/**
* @brief  Programs a word (32-bit) at a specified address.
*
* @note   This function must be used when the device voltage range is from 2.7V to 3.6V.
*
* @note   If an erase and a program operations are requested simultaneously,
*         the erase operation is performed before the program one.
*
* @param  Address: specifies the address to be programmed.
*         This parameter can be any address in Program memory zone or in OTP zone.
* @param  Data: specifies the data to be programmed.
* @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_ProgramWord(uint32_t Address, uint32_t Data)
{
    FLASH_Status status = FLASH_COMPLETE;

    /* Check the parameters */
    assert_param(IS_FLASH_ADDRESS(Address));

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

    if (status == FLASH_COMPLETE) {
/* if the previous operation is completed, proceed to program the new data */
        FLASH->CR &= CR_PSIZE_MASK;
        FLASH->CR |= FLASH_PSIZE_WORD;
        FLASH->CR |= FLASH_CR_PG;

        *(__IO uint32_t*)Address = Data;

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

        /* if the program operation is completed, disable the PG Bit */
        FLASH->CR &= (~FLASH_CR_PG);
    }
    /* Return the Program Status */
    return status;
}

看函数代码可了解到,使用指针进行赋值操作前设置了数据操作宽度,并设置了PG寄存器位,在赋值操作后, 调用了FLASH_WaitForLastOperation函数等待写操作完毕。HalfWord和Byte操作宽度的函数执行过程类似。

50.4. 实验:读写内部FLASH

在本小节中我们以实例讲解如何使用内部FLASH存储数据。

50.4.1. 硬件设计

本实验仅操作了STM32芯片内部的FLASH空间,无需额外的硬件。

50.4.2. 软件设计

本小节讲解的是“内部FLASH编程”实验,请打开配套的代码工程阅读理解。为了方便展示及移植, 我们把操作内部FLASH相关的代码都编写到“bsp_internalFlash.c”及“bsp_internalFlash.h”文件中, 这些文件是我们自己编写的,不属于标准库的内容,可根据您的喜好命名文件。

50.4.2.1. 程序设计要点

(1) 对内部FLASH解锁;

(2) 找出空闲扇区,擦除目标扇区;

(3) 进行读写测试。

50.4.2.2. 代码分析

硬件定义

读写内部FLASH不需要用到任何外部硬件,不过在擦写时常常需要知道各个扇区的基地址, 我们把这些基地址定义到bsp_internalFlash.h文件中,见 代码清单:FLASH-5

代码清单:FLASH-5 各个扇区的基地址(bsp_internal_Flash.h文件)
 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 ADDR_FLASH_SECTOR_0     ((uint32_t)0x08000000)
#define ADDR_FLASH_SECTOR_1     ((uint32_t)0x08004000)
#define ADDR_FLASH_SECTOR_2     ((uint32_t)0x08008000)
#define ADDR_FLASH_SECTOR_3     ((uint32_t)0x0800C000)
#define ADDR_FLASH_SECTOR_4     ((uint32_t)0x08010000)
#define ADDR_FLASH_SECTOR_5     ((uint32_t)0x08020000)
#define ADDR_FLASH_SECTOR_6     ((uint32_t)0x08040000)
#define ADDR_FLASH_SECTOR_7     ((uint32_t)0x08060000)
#define ADDR_FLASH_SECTOR_8     ((uint32_t)0x08080000)
#define ADDR_FLASH_SECTOR_9     ((uint32_t)0x080A0000)
#define ADDR_FLASH_SECTOR_10    ((uint32_t)0x080C0000)
#define ADDR_FLASH_SECTOR_11    ((uint32_t)0x080E0000)

#define ADDR_FLASH_SECTOR_12     ((uint32_t)0x08100000)
#define ADDR_FLASH_SECTOR_13     ((uint32_t)0x08104000)
#define ADDR_FLASH_SECTOR_14     ((uint32_t)0x08108000)
#define ADDR_FLASH_SECTOR_15     ((uint32_t)0x0810C000)
#define ADDR_FLASH_SECTOR_16     ((uint32_t)0x08110000)
#define ADDR_FLASH_SECTOR_17     ((uint32_t)0x08120000)
#define ADDR_FLASH_SECTOR_18     ((uint32_t)0x08140000)
#define ADDR_FLASH_SECTOR_19     ((uint32_t)0x08160000)
#define ADDR_FLASH_SECTOR_20     ((uint32_t)0x08180000)
#define ADDR_FLASH_SECTOR_21     ((uint32_t)0x081A0000)
#define ADDR_FLASH_SECTOR_22     ((uint32_t)0x081C0000)
#define ADDR_FLASH_SECTOR_23     ((uint32_t)0x081E0000)

这些宏跟表 STM32内部FLASH的构成 中的地址说明一致。

根据扇区地址计算SNB寄存器的值

在擦除操作时,需要向FLASH控制寄存器FLASH_CR的SNB位写入要擦除的扇区号,固件库把各个扇区对应的寄存器值使用宏定义到了stm32f4xx_flash.h文件。 为了便于使用,我们自定义了一个GetSector函数,根据输入的内部FLASH地址,找出其所在的扇区,并返回该扇区对应的SNB位寄存器值, 见 代码清单:FLASH-6

代码清单:FLASH-6 写入到SNB寄存器位的值(stm32f4xx_flash.h及bsp_internalFlash.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
/*固件库定义的用于扇区写入到SNB寄存器位的宏(stm32f4xx_flash.h文件)*/
#define FLASH_Sector_0     ((uint16_t)0x0000)
#define FLASH_Sector_1     ((uint16_t)0x0008)
#define FLASH_Sector_2     ((uint16_t)0x0010)
#define FLASH_Sector_3     ((uint16_t)0x0018)
#define FLASH_Sector_4     ((uint16_t)0x0020)
#define FLASH_Sector_5     ((uint16_t)0x0028)
#define FLASH_Sector_6     ((uint16_t)0x0030)
#define FLASH_Sector_7     ((uint16_t)0x0038)
#define FLASH_Sector_8     ((uint16_t)0x0040)
#define FLASH_Sector_9     ((uint16_t)0x0048)
#define FLASH_Sector_10    ((uint16_t)0x0050)
#define FLASH_Sector_11    ((uint16_t)0x0058)
#define FLASH_Sector_12    ((uint16_t)0x0080)
#define FLASH_Sector_13    ((uint16_t)0x0088)
#define FLASH_Sector_14    ((uint16_t)0x0090)
#define FLASH_Sector_15    ((uint16_t)0x0098)
#define FLASH_Sector_16    ((uint16_t)0x00A0)
#define FLASH_Sector_17    ((uint16_t)0x00A8)
#define FLASH_Sector_18    ((uint16_t)0x00B0)
#define FLASH_Sector_19    ((uint16_t)0x00B8)
#define FLASH_Sector_20    ((uint16_t)0x00C0)
#define FLASH_Sector_21    ((uint16_t)0x00C8)
#define FLASH_Sector_22    ((uint16_t)0x00D0)
#define FLASH_Sector_23    ((uint16_t)0x00D8)

/*定义在bsp_internalFlash.c文件中的函数*/
/**
* @brief  根据输入的地址给出它所在的sector
*         例如:
            uwStartSector = GetSector(FLASH_USER_START_ADDR);
            uwEndSector = GetSector(FLASH_USER_END_ADDR);
* @param  Address:地址
* @retval 地址所在的sector
*/
static uint32_t GetSector(uint32_t Address)
{
    uint32_t sector = 0;

if ((Address < ADDR_FLASH_SECTOR_1) && (Address >= ADDR_FLASH_SECTOR_0)) {
        sector = FLASH_Sector_0;
    } else if ((Address < ADDR_FLASH_SECTOR_2) && (Address >= ADDR_FLASH_SECTOR_1)) {
        sector = FLASH_Sector_1;
    }

    /*此处省略扇区2-扇区21的内容*/

    else if ((Address < ADDR_FLASH_SECTOR_23) && (Address >= ADDR_FLASH_SECTOR_22)) {
        sector = FLASH_Sector_22;
    } else { /*(Address < FLASH_END_ADDR) && (Address >= ADDR_FLASH_SECTOR_23))*/
        sector = FLASH_Sector_23;
    }
    return sector;
}

代码中固件库定义的宏FLASH_Sector_0-23对应的值是跟寄存器说明一致的, 见图 FLASH_CR寄存器的SNB位的值

FLASH_CR寄存器的SNB位的值

GetSector函数根据输入的地址与各个扇区的基地址进行比较,找出它所在的扇区,并使用固件库中的宏,返回扇区对应的SNB值。

读写内部FLASH

一切准备就绪,可以开始对内部FLASH进行擦写,这个过程不需要初始化任何外设,只要按解锁、擦除及写入的流程走就可以了,见 代码清单:FLASH-7。

代码清单:FLASH-7 对内部地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
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
87
88
89
90
91
92
93
/*准备写入的测试数据*/
#define DATA_32                 ((uint32_t)0x00000000)
/* 要擦除内部FLASH的起始地址 */
#define FLASH_USER_START_ADDR   ADDR_FLASH_SECTOR_8
/* 要擦除内部FLASH的结束地址 */
#define FLASH_USER_END_ADDR     ADDR_FLASH_SECTOR_12

/**
* @brief  InternalFlash_Test,对内部FLASH进行读写测试
* @param  None
* @retval None
*/
int InternalFlash_Test(void)
{
    /*要擦除的起始扇区(包含)及结束扇区(不包含),如8-12,表示擦除8、9、10、11扇区*/
    uint32_t uwStartSector = 0;
    uint32_t uwEndSector = 0;

    uint32_t uwAddress = 0;
    uint32_t uwSectorCounter = 0;

    __IO uint32_t uwData32 = 0;
    __IO uint32_t uwMemoryProgramStatus = 0;

    /* FLASH 解锁 ********************************/
    /* 使能访问FLASH控制寄存器 */
    FLASH_Unlock();

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


    uwStartSector = GetSector(FLASH_USER_START_ADDR);
    uwEndSector = GetSector(FLASH_USER_END_ADDR);

    /* 开始擦除操作 */
    uwSectorCounter = uwStartSector;
    while (uwSectorCounter <= uwEndSector) {
        /* VoltageRange_3 以“字”的大小进行操作 */
        if (FLASH_EraseSector(uwSectorCounter, VoltageRange_3) != FLASH_COMPLETE)

            /*擦除出错,返回,实际应用中可加入处理 */
            return -1;
        }
        /* 计数器指向下一个扇区 */
        if (uwSectorCounter == FLASH_Sector_11) {
            uwSectorCounter += 40;
        } else {
            uwSectorCounter += 8;
        }
    }

    /* 以“字”的大小为单位写入数据 ********************************/
    uwAddress = FLASH_USER_START_ADDR;

    while (uwAddress < FLASH_USER_END_ADDR) {
        if (FLASH_ProgramWord(uwAddress, DATA_32) == FLASH_COMPLETE) {
            uwAddress = uwAddress + 4;
        } else {
            /*写入出错,返回,实际应用中可加入处理 */
            return -1;
        }
    }


    /* 给FLASH上锁,防止内容被篡改*/
    FLASH_Lock();


    /* 从FLASH中读取出数据进行校验***************************************/
    /*  MemoryProgramStatus = 0: 写入的数据正确
        MemoryProgramStatus != 0: 写入的数据错误,其值为错误的个数 */
    uwAddress = FLASH_USER_START_ADDR;
    uwMemoryProgramStatus = 0;

    while (uwAddress < FLASH_USER_END_ADDR) {
        uwData32 = *(__IO uint32_t*)uwAddress;

        if (uwData32 != DATA_32) {
            uwMemoryProgramStatus++;
        }

        uwAddress = uwAddress + 4;
    }
    /* 数据校验不正确 */
    if (uwMemoryProgramStatus) {
        return -1;
    } else { /*数据校验正确*/
        return 0;
    }
}

该函数的执行过程如下:

(1) 调用FLASH_Unlock解锁;

(2) 调用FLASH_ClearFlag清除各种标志位;

(3) 调用GetSector根据起始地址及结束地址计算要擦除的扇区;

(4) 调用FLASH_EraseSector擦除扇区,擦除时按字为单位进行操作;

(5) 调用FLASH_ProgramWord函数向起始地址至结束地址的存储区域都写入数值“DATA_32”;

(6) 调用FLASH_Lock上锁;

(7) 使用指针读取数据内容并校验。

main函数

最后我们来看看main函数的执行流程,见 代码清单:FLASH-4

代码清单:FLASH-8 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
23
24
25
26
/**
* @brief  主函数
* @param  无
* @retval 无
*/
int main(void)
{
    /*初始化USART,配置模式为 115200 8-N-1*/
    Debug_USART_Config();
    LED_GPIO_Config();

    LED_BLUE;
    /*调用printf函数,因为重定向了fputc,printf的内容会输出到串口*/
    printf("this is a usart printf demo. \r\n");
    printf("\r\n 欢迎使用野火  STM32 F407 开发板。\r\n");
    printf("正在进行读写内部FLASH实验,请耐心等待\r\n");

    if (InternalFlash_Test()==0) {
        LED_GREEN;
        printf("读写内部FLASH测试成功\r\n");

    } else {
        printf("读写内部FLASH测试失败\r\n");
        LED_RED;
    }
}

main函数中初始化了用于指示调试信息的LED及串口后,直接调用了InternalFlash_Test函数,进行读写测试并根据测试结果输出调试信息。

50.4.3. 下载验证

用USB线连接开发板“USB TO UART”接口跟电脑,在电脑端打开串口调试助手,把编译好的程序下载到开发板。在串口调试助手可看到擦写内部FLASH的调试信息。