STM32CubeMX学习笔记:读写内部Flash实战

一、简介

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

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

  • 主存储器
  • 一般我们说 STM32 内部 FLASH 的时候,都是指这个主存储器区域,它是存储用户应用程序的空间,芯片型号说明中的 256K FLASH、512K FLASH 都是指这个区域的大小。

    主存储器分为 256 页,每页大小为 2KB,共 512KB。这个分页的概念,实质就是 FLASH 存储器的扇区,与其它 FLASH 一样,在写入数据前,要先按页(扇区)擦除。

    注意上表中的主存储器是本实验板使用的 STM32VET6 型号芯片的参数,即 STM32F1 大容量产品。若使用超大容量、中容量或小容量产品,它们主存储器的页数量、页大小均有不同,使用的时候要注意区分。
    主存储器是以页为单位划分的。stm32根据FLASH主存储块容量、页面的不同,系统存储器的不同,分为小容量、中容量、大容量、互联型,共四类产品。

  • 小容量产品:主存储块1-32KB, 每页1KB。系统存储器2KB
  • 中容量产品:主存储块64-128KB, 每页1KB。系统存储器2KB
  • 大容量产品:主存储块256KB以上, 每页2KB。系统存储器2KB
  • 互联型产品:主存储块256KB以上, 每页2KB。系统存储器18KB
  • 系统存储区
  • 系统存储区是用户不能访问的区域,它在芯片出厂时已经固化了启动代码,它负责实现串口、USB 以及 CAN 等 ISP 烧录功能。

  • 选项字节
  • 选项字节用于配置 FLASH 的读写保护、待机/停机复位、软件/硬件看门狗等功能,这部分共 16 字节。可以通过修改 FLASH 的选项控制寄存器修改。

    二、新建工程

    1. 打开 STM32CubeMX 软件,点击“新建工程”

    2. 选择 MCU 和封装

    3. 配置时钟
    RCC 设置,选择 HSE(外部高速时钟) 为 Crystal/Ceramic Resonator(晶振/陶瓷谐振器)

    选择 Clock Configuration,配置系统时钟 SYSCLK 为 72MHz
    修改 HCLK 的值为 72 后,输入回车,软件会自动修改所有配置

    4. 配置调试模式
    非常重要的一步,否则会造成第一次烧录程序后续无法识别调试器
    SYS 设置,选择 Debug 为 Serial Wire

    三、添加串口打印

    串口打印功能查看 STM32CubeMX学习笔记(6)——USART串口使用

    四、生成代码

    输入项目名和项目路径

    选择应用的 IDE 开发环境 MDK-ARM V5

    每个外设生成独立的 ’.c/.h’ 文件
    不勾:所有初始化代码都生成在 main.c
    勾选:初始化代码生成在对应的外设文件。 如 GPIO 初始化代码生成在 gpio.c 中。

    点击 GENERATE CODE 生成代码

    五、查看工程的空间分布

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

    打开 map 文件后,查看文件最后部分的区域,可以看到一段以 “Memory Map of the image” 开头的记录(若找不到可用查找功能定位)

    观察表中的最后一项,它的基地址是 0x0800175c,大小为 0x00000020,可知它占用的
    最高的地址空间为 0x0800177c,跟执行区域的最高地址 0x0000177c 一样,但它们比加载
    区域说明中的最高地址 0x80017a8 要小,所以我们以加载区域的大小为准。对比表 45-1 的
    内部 FLASH 页地址分布表,可知仅使用页 0 至页 2 就可以完全存储本应用程序,所以从页
    3**(地址 0x08001800)**后的存储空间都可以作其它用途,使用这些存储空间时不会篡改应用程
    序空间的数据。

    六、官方HAL库Flash操作常见函数

    //源文件: stm32f1xx_hal_flash.c和stm32f1xx_hal_flash_ex.c
    HAL_FLASH_Unlock(void);	//解锁函数
    HAL_FLASH_Lock(void);	//锁定函数
    HAL_FLASH_Program(uint32_t TypeProgram, uint32_t Address, uint64_t Data);	//写操作函数
    HAL_FLASHEx_Erase(FLASH_EraseInitTypeDef *pEraseInit, uint32_t *SectorError);	//擦除函数
    HAL_FLASH_WaitForLastOperation(uint32_t Timeout);	//等待操作完成函数
    

    HAL库中定义了一个Flash初始化结构体,如下:

    /**
      * @brief  FLASH Erase structure definition
      */
    typedef struct
    {
      uint32_t TypeErase;   /*!< Mass erase or page erase.
                                 This parameter can be a value of @ref FLASH_Type_Erase */
      uint32_t Banks;       /*!< Select bank to erase.
                                 This parameter must be a value of @ref FLASH_Banks
                                 (FLASH_BANK_BOTH should be used only for mass erase) */
      uint32_t Page;        /*!< Initial Flash page to erase when page erase is disabled
                                 This parameter must be a value between 0 and (max number of pages in the bank - 1)
                                 (eg : 255 for 1MB dual bank) */
      uint32_t NbPages;     /*!< Number of pages to be erased.
                                 This parameter must be a value between 1 and (max number of pages in the bank - value of initial page)*/
    } FLASH_EraseInitTypeDef;
    

    七、读取Flash

    7.1 读取函数

    /* FLASH大小:STM32F103VET6:256K */
    #define STM32FLASH_SIZE         0x00040000UL
    /* FLASH起始地址 */
    #define STM32FLASH_BASE         FLASH_BASE
    /* FLASH结束地址 */
    #define STM32FLASH_END          (STM32FLASH_BASE | STM32FLASH_SIZE)
    /* FLASH页大小:1K */
    #define STM32FLASH_PAGE_SIZE    FLASH_PAGE_SIZE
    /* FLASH总页数 */
    #define STM32FLASH_PAGE_NUM     (STM32FLASH_SIZE / STM32FLASH_PAGE_SIZE)
    
    #define WRITE_START_ADDR        ((uint32_t)0x08008000)
    #define WRITE_END_ADDR          ((uint32_t)0x0800C000)
    
    /**
     @brief 内部Flash读取
     @param address -[in] 读取的地址
     @param pData -[out] 指向需要操作的数据
     @param dataLen -[in] 数据长度
     @return 读出成功的字节数
    */
    uint32_t Internal_ReadFlash(uint32_t addrStart, void *pData, uint32_t dataLen)
    {
        uint32_t nread = dataLen;
        uint8_t *pBuffer = (uint8_t *)pData;
        const uint8_t *pAddr = (const uint8_t *)addrStart;
    
        if(!pData || addrStart < STM32FLASH_BASE || addrStart > STM32FLASH_END)
        {
            return 0;
        }
    
        while(nread >= sizeof(uint32_t) && (((uint32_t)pAddr) <= (STM32FLASH_END - 4)))
        {
            *(uint32_t *)pBuffer = *(uint32_t *)pAddr;
            pBuffer += sizeof(uint32_t);
            pAddr += sizeof(uint32_t);
            nread -= sizeof(uint32_t);
        }
    
        while(nread && (((uint32_t)pAddr) < STM32FLASH_END))
        {
            *pBuffer++ = *pAddr++;
            nread--;
        }
    
        return dataLen - nread;
    }
    

    8、写入Flash

    8.1 写入过程

    8.1.1 解锁

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

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

    1. 往 FPEC 键寄存器 FLASH_KEYR 中写入 KEY1 = 0x45670123
    2. 再往 FPEC 键寄存器 FLASH_KEYR 中写入 KEY2 = 0xCDEF89AB

    8.1.2 页擦除

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

    1. 检查 FLASH_SR 寄存器中的“忙碌寄存器位 BSY”,以确认当前未执行任何 Flash 操作;
    2. 在 FLASH_CR 寄存器中,将“激活页擦除寄存器位 PER ”置 1;
    3. 用 FLASH_AR 寄存器选择要擦除的页;
    4. 将 FLASH_CR 寄存器中的“开始擦除寄存器位 STRT ”置 1,开始擦除;
    5. 等待 BSY 位被清零时,表示擦除完成。

    /**
     @brief 内部Flash页擦除
     @param pageAddress -[in] 擦除的起始地址
     @param nbPages -[in] 擦除页数
     @return 0 - 成功;-1 - 失败
    */
    int Internal_ErasePage(uint32_t pageAddress, uint32_t nbPages)
    {
    	uint32_t pageError = 0;
    	FLASH_EraseInitTypeDef eraseInit;
    	eraseInit.TypeErase = FLASH_TYPEERASE_PAGES;
    	eraseInit.PageAddress = pageAddress;
    	eraseInit.Banks = FLASH_BANK_1;
    	eraseInit.NbPages = 1;
    	if(HAL_FLASHEx_Erase(&eraseInit, &pageError) != HAL_OK)
    	{
    		return -1;
    	}
    	return 0;
    }
    

    8.1.3 写入数据

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

    1. 检查 FLASH_SR 中的 BSY 位,以确认当前未执行任何其它的内部 Flash 操作;
    2. 将 FLASH_CR 寄存器中的 “激活编程寄存器位 PG” 置 1;
    3. 向指定的 FLASH 存储器地址执行数据写入操作,每次只能以 16 位的方式写入;
    4. 等待 BSY 位被清零时,表示写入完成。

    8.2 写入函数

    /**
     @brief 内部Flash写入
     @param address -[in] 写入的地址
     @param pData -[in] 指向需要操作的数据
     @param dataLen -[in] 数据长度
     @return 实际写入的数据量,单位:字节
    */
    uint32_t Internal_WriteFlash(uint32_t addrStart, const uint16_t *pData, uint32_t dataLen)
    {   
        uint32_t i = 0;
        uint32_t pagepos = 0;         // 页位置
        uint32_t pageoff = 0;         // 页内偏移地址
        uint32_t pagefre = 0;         // 页内空余空间
        uint32_t offset = 0;          // Address在FLASH中的偏移
        uint32_t nwrite = dataLen;    // 记录剩余要写入的数据量
        const uint16_t *pBuffer = (const uint16_t *)pData;
        
        /* 非法地址 */
        if(addrStart < STM32FLASH_BASE || addrStart > (STM32FLASH_END - 2) || dataLen == 0 || pData == NULL)
        {
            return 0;
        }
        
        /* 解锁FLASH */
        HAL_FLASH_Unlock();
    
        /* 计算偏移地址 */
        offset = addrStart - STM32FLASH_BASE;
        /* 计算当前页位置 */
        pagepos = offset / STM32FLASH_PAGE_SIZE;
        /* 计算要写数据的起始地址在当前页内的偏移地址 */
        pageoff = ((offset % STM32FLASH_PAGE_SIZE) >> 1);
        /* 计算当前页内空余空间 */
        pagefre = ((STM32FLASH_PAGE_SIZE >> 1) - pageoff);
        /* 要写入的数据量低于当前页空余量 */
        if(nwrite <= pagefre)
        {
            pagefre = nwrite;
        }
        
        while(nwrite != 0)
        {
            /* 检查是否超页 */
            if(pagepos >= STM32FLASH_PAGE_NUM)
            {
                break;
            }
    
            /* 读取一页 */
            Internal_ReadFlash(STM32FLASH_BASE + pagepos * STM32FLASH_PAGE_SIZE, FlashBuffer, STM32FLASH_PAGE_SIZE);
    
            /* 检查是否需要擦除 */
            for(i = 0; i < pagefre; i++)
            {
                if(*(FlashBuffer + pageoff + i) != 0xFFFF) /* FLASH擦出后默认内容全为0xFF */
                {
                    break;
                }
            }
    
            if(i < pagefre)
            {
                uint32_t count = 0;
                uint32_t index = 0;
                uint32_t PageError = 0;
                FLASH_EraseInitTypeDef pEraseInit;
    
                /* 擦除一页 */
                pEraseInit.TypeErase = FLASH_TYPEERASE_PAGES;
                pEraseInit.PageAddress = STM32FLASH_BASE + pagepos * STM32FLASH_PAGE_SIZE;
                pEraseInit.Banks = FLASH_BANK_1;
                pEraseInit.NbPages = 1;
                if(HAL_FLASHEx_Erase(&pEraseInit, &PageError) != HAL_OK)
                {
                    break;
                }
    
                /* 复制到缓存 */
                for(index = 0; index < pagefre; index++)
                {
                    *(FlashBuffer + pageoff + index) = *(pBuffer + index);
                }
    
                /* 写回FLASH */
                count = Internal_WriteFlashNoCheck(STM32FLASH_BASE + pagepos * STM32FLASH_PAGE_SIZE, FlashBuffer, STM32FLASH_PAGE_SIZE >> 1);
                if(count != (STM32FLASH_PAGE_SIZE >> 1))
                {
                    nwrite -= count;
                    break;
                }
            }
            else
            {
                /* 无需擦除,直接写 */
                uint32_t count = Internal_WriteFlashNoCheck(addrStart, pBuffer, pagefre);
                if(count != pagefre)
                {
                    nwrite -= count;
                    break;
                }
            }
    
            pBuffer += pagefre;         /* 读取地址递增         */
            addrStart += (pagefre << 1);  /* 写入地址递增         */
            nwrite -= pagefre;          /* 更新剩余未写入数据量 */
    
            pagepos++;     /* 下一页           */
            pageoff = 0;   /* 页内偏移地址置零  */
    
            /* 根据剩余量计算下次写入数据量 */
            pagefre = nwrite >= (STM32FLASH_PAGE_SIZE >> 1) ? (STM32FLASH_PAGE_SIZE >> 1) : nwrite;
        }
    
        /* 加锁FLASH */
        HAL_FLASH_Lock();
    
        return ((dataLen - nwrite) << 1);
    }
    

    九、举例

    /**
      * @brief  The application entry point.
      * @retval int
      */
    int main(void)
    {
      /* USER CODE BEGIN 1 */
    
      /* USER CODE END 1 */
    
      /* MCU Configuration--------------------------------------------------------*/
    
      /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
      HAL_Init();
    
      /* USER CODE BEGIN Init */
    
      /* USER CODE END Init */
    
      /* Configure the system clock */
      SystemClock_Config();
    
      /* USER CODE BEGIN SysInit */
    
      /* USER CODE END SysInit */
    
      /* Initialize all configured peripherals */
      MX_GPIO_Init();
      MX_USART1_UART_Init();
      /* USER CODE BEGIN 2 */
        uint8_t in_data[5]={11,22,33,44,55};//要写入的数据
        uint8_t out_data[5];//读存放
        int i;
        uint32_t STATUS = 0;
        STATUS = Internal_WriteFlash(0x08001800, (uint16_t *)in_data, 5);
        HAL_Delay(1000);
        if(STATUS)
        {
            Internal_ReadFlash(0x08001800, (uint16_t *)out_data, 5);
            printf("\r\n The Five Data Is : \r\n");
            for(i = 0; i < 5; i++)
            {
                printf("\r %d \r", out_data[i]);
            }
        }
      /* USER CODE END 2 */
    
      /* Infinite loop */
      /* USER CODE BEGIN WHILE */
      while (1)
      {
        /* USER CODE END WHILE */
    
        /* USER CODE BEGIN 3 */
      }
      /* USER CODE END 3 */
    }
    

    查看打印:

    十、工程代码

    链接:https://pan.baidu.com/s/1zfp9AkJ5jiaugWfdKaxfQg?pwd=9kpr 提取码:9kpr

    十一、注意事项

    用户代码要加在 USER CODE BEGIN NUSER CODE END N 之间,否则下次使用 STM32CubeMX 重新生成代码后,会被删除。


    • 由 Leung 写于 2023 年 2 月 7 日

    • 参考:STM32CubeMX | STM32F1系列HAL库读写内部FLASH
        STM32CubeMX系列|STM32内部FLASH

    物联沃分享整理
    物联沃-IOTWORD物联网 » STM32CubeMX学习笔记:读写内部Flash实战

    发表评论