17. STM32内部FLASH

一、STM32内部FLASH简介

  不同型号的 STM32F40xx/41xx,其 FLASH 容量也有所不同,最小的只有 128K 字节,最大的则达到了 1024K 字节。STM32F407ZGT6 的 FLASH 容量为1024K 字节,STM32F40xx/41xx 的闪存模块组织如图所示:

STM32F407闪存模块组织表

  STM32F4 的闪存模块由 主存储器系统存储器OPT 区域选项字节 等 4 部分组成。

  主存储器,该部分用来 存放代码和数据常数(如 const 类型的数据)。分为 12 个扇区,前 4个扇区为 16KB 大小,扇区 4 为 64KB 大小,扇区 5~11 为 128KB 大小,不同容量的 STM32F4,拥有的扇区数不一样,比如 STM32F407ZGT6,拥有 12 个扇区。从表中可以看出,主存储器的起始地址为 0x08000000,B0、B1 都接 GND 的时候,就是从 0x08000000 开始运行代码。

  系统存储器,主要用来 存放 STM32F4 的 bootloader 代码,此代码在出厂的时候就固化在STM32F4 里面了,专门用来给主存储器下载代码的。当 B0 接 V3.3,B1 接 GND 的时候,从该存储器启动(即进入串口下载模式)。

  OTP 区域,即 一次性可编程区域,总共 528 字节大小,被分成两个部分,前面 512 字节(32 字节为 1 块,分成 16 块),可以用来存储一些用户数据(一次性的,写完一次,永远不可以擦除。),后面 16 字节,用于锁定对应块。

  选项字节,用于 配置读保护、BOR 级别、软件/硬件看门狗以及器件处于待机或停止模式下的复位

  闪存存储器接口寄存器,该部分用于控制闪存读写等,是整个闪存模块的控制结构。在执行闪存写操作时,任何对闪存的读操作都会锁住总线,在写操作完成后读操作才能正确地进行。既在进行写或擦除操作时,不能进行代码或数据的读取操作。

二、闪存的读取

  STM32F4 可以通过内部的 I-Code 指令总线或 D-Code 数据总线访问内置闪存模块。为了准确读取 Flash 数据,必须根据 CPU 时钟(HCLK)频率和器件电源电压在 Flash 存取控制寄存器(FLASH_ACR)中正确地设置等待周期数(LATENCY)。当电源电压低于 2.1V 时,必须关闭预取缓冲器。Flash等待周期与 CPU 时钟频率之间的对应关系,如下表所示:

CPU时钟(HCLK)频率对应的FLASH等待周期表

  等待周期通过 FLASH_ACR 寄存器的 LATENCY[2:0] 三个位设置。系统复位后,CPU 时钟频率为内部 16M RC 振荡器(HIS),LATENCY 默认是 0,即 1 个等待周期。供电电压,我们一般是 3.3V,所以,在我们设置 168 MHz 频率作为 CPU 时钟之前,必须先设置 LATENCY 为 5,否则 FLASH 读写可能出错,导致死机。

  正常工作时(168 MHz),虽然 FLASH 需要 6 个 CPU 等待周期,但是由于 STM32F4 具有自适应实时存储器加速器(ART Accelerator),通过指令缓存存储器,预取指令,实现相当于 0FLASH 等待的运行速度。关于自适应实时存储器加速器的详细介绍,请大家参考《STM32F4xx参考手册_V4(中文版).pdf》3.4.2 节。

  STM23F4 的 FLASH 读取是很简单的。例如,我们要从地址 address,读取一个字(字节为 8 位,半字为 16 位,字为 32 位),可以通过如下的语句读取:

data = *(volatile uint32_t *)address;

  将 address 强制转换为 volatile uint32_t 指针,然后取该指针所指向的地址的值,即得到了 address 地址的值。类似的,将上面的 volatile uint32_t 改为 volatile uint16_t,即可读取指定地址的一个半字。

三、闪存的编程和擦除

  执行任何 Flash 编程操作(擦除或编程)时,CPU 时钟频率(HCLK)不能低于 1 MHz。如果在 Flash 操作期间发生器件复位,无法保证 Flash 中的内容。

  在对 STM32F4 的 Flash 执行写入或擦除操作期间,任何读取 Flash 的尝试都会导致总线阻塞。只有在完成编程操作后,才能正确处理读操作。这意味着,写/擦除操作进行期间不能从 Flash 中执行代码或数据获取操作。

  STM32F4 复位后,FLASH 编程操作是被保护的,不能写入 FLASH_CR 寄存器;通过写入特定的序列(0x45670123 和 0xCDEF89AB)到 FLASH_KEYR 寄存器才可解除写保护,只有在写保护被解除后,我们才能操作相关寄存器。

  FLASH_CR 的解锁序列为:

  1. 写 0x45670123 到 FLASH_KEYR
  2. 写 0xCDEF89AB 到 FLASH_KEYR

  通过这两个步骤,即可解锁 FLASH_CR,如果写入错误,那么 FLASH_CR 将被锁定,直到下次复位后才可以再次解锁。

  STM32F4 闪存的编程位数可以通过 FLASH_CR 的 PSIZE 字段配置,PSIZE 的设置必须和电源电压匹配,见下表:

编程擦除并行位数与电压关系表

  由于我们开发板用的电压是 3.3V,所以 PSIZE 必须设置为 10,即 32 位并行位数。擦除或者编程,都必须以 32 位为基础进行。

  STM32F4 的 FLASH 在写入数据的时候,也必须要求其写入地址的 FLASH 是被擦除了的(也就是其值必须是 0xFFFFFFFF),无法写入。STM32F4 的标准写数据编程步骤如下所示:

  1. 检查 FLASH_CR 的 LOCK 是否解锁,如果没有则先解锁。
  2. 检查 FLASH_SR 寄存器的 BSY 位,以确认没有其他正在进行的编程操作。
  3. 设置 FLASH_CR 寄存器的 PG 位为 "1"。
  4. 在指定的地址写入数据(一次写入 32 字节,不能超过 32 字节)。
  5. 等待 BSY 位变为 "0"。
  6. 读出写入地址并验证数据。

  在 STM32 的 FLASH 编程的时候,要先判断缩写地址是否被擦除了。STM32 的闪存擦除分为两种:页擦除整片擦除页擦除 过程如下所示:

  1. 检查 FLASH_CR 的 LOCK 是否解锁,如果没有则先解锁。
  2. 检查 FLASH_SR 寄存器中的 BSY 位,确保当前未执行任何 FLASH 操作。
  3. 在 FLASH_CR 寄存器中,将 SER 位置 1,并设置 SNB=0(只有 1 个扇区,扇区 0)。
  4. 将 FLASH_CR 寄存器中的 START 位置 1,触发擦除操作。
  5. 等待 BSY 位清零。

四、STM32内部FLASH常用寄存器

4.1、Flash访问控制寄存器

Flash访问控制寄存器

  LATENCY[2:0] 这三个位,这三个位,必须根据我们 MCU 的工作电压和频率来进行正确的设置,否则,可能会死机。用于控制 FLASH 读延迟,必须根据我们 MCU 内核的工作电压和频率,来进行正确的设置,否则,可能死机。其他 DCEN、ICEN 和 PRFTEN 这三个位也比较重要,为了达到最佳性能,这三个位我们一般都设置为 1 即可。

4.2、FLASH密钥寄存器

FLASH密钥寄存器

  该寄存器主要用来解锁 FLASH_CR,必须在该寄存器写入特定的序列(KEY1:0x45670123 和 KEY2:0xCDEF89AB)解锁后,才能对 FLASH_CR 寄存器进行写操作。

4.3、Flash状态寄存器

Flash状态寄存器

4.4、Flash控制寄存器

Flash控制寄存器

  位 31 LOCK 位,该位用于指示 FLASH_CR 寄存器是否被锁住,该位在检测到正确的解锁序列后,硬件将其清零。在一次不成功的解锁操作后,在下次系统复位之前,该位将不再改变。

  位 16 STRT 位,该位用于开始一次擦除操作。在该为写入 1,将执行一次擦除操作。

  PSIZE[9:8]位,用于设置编程宽度,我们一般设置 PSIZE = 2 即可(32 位)。

  SNB[6:3] 位,这 4 个位用于选择要擦除的扇区编号,取值范围为 0 ~ 1。

  位 1 SER 位,该位用于选择扇区擦除操作,在扇区擦除的时候,需要将该位置 1。

  位 0 PG 位,该位用于选择编程操作,在往 FLASH 写数据的时候,该位需要置 1。

五、FLASH驱动步骤

  FLASH 在 HAL 库中的驱动代码在 stm32f4xx_hal_flash.c 和 stm32f4xx_hal_flash_ex.c 文件(及其头文件)中。

5.1、解锁闪存控制寄存器访问的函数

  HAL_FLASH_Unlock() 用于解锁闪存控制寄存器的访问,在对 FLASH 进行写操作前必须先解锁,解锁操作也就是必须在 FLASH_KEYR 寄存器写入特定的序列(KEY1 和 KEY2)。该函数声明如下:

HAL_StatusTypeDef HAL_FLASH_Unlock(void);

  该函数的返回值是 HAL_StatusTypeDef 枚举类型的值,有 4 个,分别是 HAL_OK 表示 成功HAL_ERROR 表示 错误HAL_BUSY 表示 忙碌HAL_TIMEOUT 表示 超时

typedef enum 
{
    HAL_OK = 0x00U,             // 成功
    HAL_ERROR = 0x01U,          // 错误
    HAL_BUSY = 0x02U,           // 忙碌
    HAL_TIMEOUT = 0x03U         // 超时
} HAL_StatusTypeDef;

5.2、锁定闪存控制寄存器访问的函数

  HAL_FLASH_Lock() 函数用于锁定闪存控制寄存器的访问。该函数声明如下:

HAL_StatusTypeDef HAL_FLASH_Lock(void);

  该函数的返回值是 HAL_StatusTypeDef 枚举类型的值,有 4 个,分别是 HAL_OK 表示 成功HAL_ERROR 表示 错误HAL_BUSY 表示 忙碌HAL_TIMEOUT 表示 超时

5.3、闪存写操作函数

  HAL_FLASH_Program() 函数用于 FLASH 的写入。该函数声明如下:

HAL_StatusTypeDef HAL_FLASH_Program(uint32_t TypeProgram, uint32_t Address, uint64_t Data);

  形参 TypeProgram 是用来区分 要写入的数据类型

#define FLASH_TYPEPROGRAM_BYTE        0x00000000U  /*!< Program byte (8-bit) at a specified address           */
#define FLASH_TYPEPROGRAM_HALFWORD    0x00000001U  /*!< Program a half-word (16-bit) at a specified address   */
#define FLASH_TYPEPROGRAM_WORD        0x00000002U  /*!< Program a word (32-bit) at a specified address        */
#define FLASH_TYPEPROGRAM_DOUBLEWORD  0x00000003U  /*!< Program a double word (64-bit) at a specified address */

  形参 Address 用来设置 要写入数据的 FLASH 地址

  形参 Data要写入的数据类型

  该函数的返回值是 HAL_StatusTypeDef 枚举类型的值,有 4 个,分别是 HAL_OK 表示 成功HAL_ERROR 表示 错误HAL_BUSY 表示 忙碌HAL_TIMEOUT 表示 超时

5.4、闪存擦除函数

  HAL_FLASHEx_Erase() 函数用于大量擦除或擦除指定的闪存扇区。

HAL_StatusTypeDef HAL_FLASHEx_Erase(FLASH_EraseInitTypeDef *pEraseInit, uint32_t *SectorError)

  形参 pEraseInitFLASH_EraseInitTypeDef 结构体类型指针变量。

typedef struct
{
    uint32_t TypeErase;     // 擦除类型
    uint32_t Banks;         // 擦除的Bank编号
    uint32_t Sector;        // 擦除的扇区编号
    uint32_t NbSectors;     // 擦除的扇区数量
    uint32_t VoltageRange;  // 电压等级
} FLASH_EraseInitTypeDef;

  形参 SectorError 是 uint32_t 类型 指针变量存放错误码,0xFFFFFFFF 值表示扇区已被正确擦除,其它值表示擦除过程中的错误扇区。

  该函数的返回值是 HAL_StatusTypeDef 枚举类型的值,有 4 个,分别是 HAL_OK 表示 成功HAL_ERROR 表示 错误HAL_BUSY 表示 忙碌HAL_TIMEOUT 表示 超时

5.5、等待FLASH操作完成函数

  FLASH_WaitForLastOperation() 函数用于等待 FLASH 操作完成。

HAL_StatusTypeDef FLASH_WaitForLastOperation(uint32_t Timeout)

  形参 Timeout 是 FLASH 操作 超时时间

  该函数的返回值是 HAL_StatusTypeDef 枚举类型的值,有 4 个,分别是 HAL_OK 表示 成功HAL_ERROR 表示 错误HAL_BUSY 表示 忙碌HAL_TIMEOUT 表示 超时

六、程序源码

  FLASH 读取一个字函数:

/**
 * @brief 内部FLASH读取一个字节函数
 * 
 * @param address 待读取数据的地址
 * @return uint32_t 读取的数据
 */
uint32_t FLASH_ReadOneWord(uint32_t address)
{
    return *(volatile uint32_t *)address;
}

  FLASH 读取数据函数:

/**
 * @brief 从指定地址开始读出指定长度的数据
 * 
 * @param address 起始地址
 * @param data 存放读取数据缓冲区的指针
 * @param length 要读取的字(32位)数,即4个字节的整数倍
 */
void FLASH_ReadData(uint32_t address, uint32_t *data, uint32_t length)
{
    for (uint32_t i = 0; i < length; i++)
    {
        data[i] = FLASH_ReadOneWord(address);                                   // 读取一个字,四个字节
        address += 4;                                                           // 地址偏移4个字节
    }
}

  FLASH

#define ADDRESS_FLASH_SECTOR_0  ((uint32_t )0x08000000)                         // 扇区 0 起始地址,16 Kbyte
#define ADDRESS_FLASH_SECTOR_1  ((uint32_t )0x08004000)                         // 扇区 1 起始地址,16 Kbyte
#define ADDRESS_FLASH_SECTOR_2  ((uint32_t )0x08008000)                         // 扇区 2 起始地址,16 Kbyte
#define ADDRESS_FLASH_SECTOR_3  ((uint32_t )0x0800C000)                         // 扇区 3 起始地址,16 Kbyte
#define ADDRESS_FLASH_SECTOR_4  ((uint32_t )0x08010000)                         // 扇区 4 起始地址,64 Kbyte
#define ADDRESS_FLASH_SECTOR_5  ((uint32_t )0x08020000)                         // 扇区 5 起始地址,128 Kbyte
#define ADDRESS_FLASH_SECTOR_6  ((uint32_t )0x08040000)                         // 扇区 6 起始地址,128 Kbyte
#define ADDRESS_FLASH_SECTOR_7  ((uint32_t )0x08060000)                         // 扇区 7 起始地址,128 Kbyte
#define ADDRESS_FLASH_SECTOR_8  ((uint32_t )0x08080000)                         // 扇区 8 起始地址,128 Kbyte
#define ADDRESS_FLASH_SECTOR_9  ((uint32_t )0x080A0000)                         // 扇区 9 起始地址,128 Kbyte
#define ADDRESS_FLASH_SECTOR_10 ((uint32_t )0x080C0000)                         // 扇区 10 起始地址,128 Kbyte
#define ADDRESS_FLASH_SECTOR_11 ((uint32_t )0x080E0000)                         // 扇区 11 起始地址,128 Kbyte
/**
 * @brief 获取某个地址所在的FLASH扇区
 * 
 * @param address 内存地址
 * @return uint8_t address所在的扇区
 */
uint8_t  FLASH_GetFlashSector(uint32_t address)
{
    if (address < ADDRESS_FLASH_SECTOR_1) return FLASH_SECTOR_0;
    else if (address < ADDRESS_FLASH_SECTOR_2) return FLASH_SECTOR_1;
    else if (address < ADDRESS_FLASH_SECTOR_3) return FLASH_SECTOR_2;
    else if (address < ADDRESS_FLASH_SECTOR_4) return FLASH_SECTOR_3;
    else if (address < ADDRESS_FLASH_SECTOR_5) return FLASH_SECTOR_4;
    else if (address < ADDRESS_FLASH_SECTOR_6) return FLASH_SECTOR_5;
    else if (address < ADDRESS_FLASH_SECTOR_7) return FLASH_SECTOR_6;
    else if (address < ADDRESS_FLASH_SECTOR_8) return FLASH_SECTOR_7;
    else if (address < ADDRESS_FLASH_SECTOR_9) return FLASH_SECTOR_8;
    else if (address < ADDRESS_FLASH_SECTOR_10) return FLASH_SECTOR_9;
    else if (address < ADDRESS_FLASH_SECTOR_11) return FLASH_SECTOR_10;
    return FLASH_SECTOR_11;
}

  FLASH 写数据函数:

#define FLASH_WAITE_TIME        50000                                           // FLASH 等待超时时间

/**
 * @brief 在FLASH指定位置,写入指定长度的数据(自动擦除)函数
 * 
 * @param address 起始地址(此地址必须为4的倍数,否则写入出错!)
 * @param data 数据指针
 * @param length 要写入的字(32位)的个数
 * 
 * @note    因为STM32F4的扇区实在太大,没办法本地保存扇区数据,所以本函数写地址如果非0XFF,
 *          那么会先擦除整个扇区且不保存扇区数据.所以写非0XFF的地址,将导致整个扇区数据丢失。
 *          建议写之前确保扇区里没有重要数据,最好是整个扇区先擦除了,然后慢慢往后写。
 *          该函数对OTP区域也有效!可以用来写OTP区!
 *          OTP区域地址范围:0X1FFF7800~0X1FFF7A0F(注意:最后16字节,用于OTP数据块锁定!)
 */
void FLASH_WriteData(uint32_t address, uint32_t *data, uint32_t length)
{
    FLASH_EraseInitTypeDef FLASH_EraseInitStruct = {0};
    HAL_StatusTypeDef flash_status = HAL_OK;

    uint32_t start_address = 0;
    uint32_t end_address = 0;
    uint32_t sectorerror=0;

    // 写入地址小于或大于FLASH地址,或不是4的整数倍,非法
    if (address < FLASH_BASE || (address % 4) || (address > FLASH_END))
    {
        return;
    }

    HAL_FLASH_Unlock();                                                         // 解锁
    FLASH->ACR &= ~(1 << 10);                                                   // FLASH擦除期间,必须禁止数据缓存

    start_address = address;                                                    // 写入的起始地址
    end_address = address + length * 4;                                         // 写入的结束地址

    if (start_address < 0x1FFF0000)                                             // 只有主存储区,才需要执行擦除操作
    {
        while (start_address < end_address)                                     // 对非0xFFFFFFFF的地方,先擦除
        {
            if (FLASH_ReadOneWord(start_address) != 0xFFFFFFFF)                 // 有非0xFFFFFFFF的地方,要擦除这个扇区
            {
                FLASH_EraseInitStruct.TypeErase = FLASH_TYPEERASE_SECTORS;      // 擦除类型,扇区擦除
                FLASH_EraseInitStruct.Sector = FLASH_GetFlashSector(start_address); // 要擦除的扇区
                FLASH_EraseInitStruct.NbSectors = 1;                            // 一次只擦除一个扇区
                FLASH_EraseInitStruct.VoltageRange = FLASH_VOLTAGE_RANGE_3;     // 电压范围,VCC=2.7~3.6V之间!

                if(HAL_FLASHEx_Erase(&FLASH_EraseInitStruct, &sectorerror) != HAL_OK) 
                {
                    break;                                                      // 发生错误了退出
                }
            }
            else
            {
                start_address += 4;                                             // 地址偏移4个字节
            }
            FLASH_WaitForLastOperation(FLASH_WAITE_TIME);                       // 等待上次操作完成
        }
    }

    flash_status = FLASH_WaitForLastOperation(FLASH_WAITE_TIME);                // 等待上次操作完成

    if (flash_status == HAL_OK)
    {
        while (address < end_address)                                           // 写数据
        {
            if (HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, address, *data) != HAL_OK)    // 写入数据
            {
                break;                                                          // 写入异常则退出
            }

            address += 4;                                                       // 地址偏移4个字节
            data++;                                                             // 数据指针后移
        }
    }

    FLASH->ACR |= 1 << 10;                                                      // FLASH擦除结束,开启数据fetch

    HAL_FLASH_Lock();                                                           // 上锁
}

  有关时钟配置函数请在 STM32 的时钟系统 篇章查看。

  有关 USART1 的配置请在 串口通信 篇章查看。

  main() 函数:

int main(void)
{
    uint8_t data[] = "Hello world!";
    uint8_t length = sizeof(data) / 4 + ((sizeof(data) ? 1 : 0));
    uint8_t temp[30] = {0};

    HAL_Init();

    System_Clock_Init(8, 336, 2, 7);
    HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_2);                         // 设置中断优先级分组

    USART1_Init(115200);

    FLASH_WriteData(0x08010000, (uint32_t *)data, length);
    FLASH_ReadData(0x08010000, (uint32_t *)temp, length);
    printf("data: %s\r\n", temp);

    while (1)
    {
  
    }
  
    return 0;
}
posted @ 2024-03-21 19:28  星光樱梦  阅读(500)  评论(0编辑  收藏  举报