1. 内部FLASH简介
之前的文章中介绍过STM32F1利用SPI与外部FLASH(W25QXX芯片)通讯的例程,本例程将介绍STM32F1的内部FLASH,通过内部FLASH实现数据读写操作。 不同型号的STM32,其FLASH容量也有所不同,最小的只有16K字节,最大的则达到了1024K字节。此处我们使用的是STM32F103ZET6,其FLASH容量为512K字节,属于大容量产品,大容量产品的闪存模块组织图如下图示
STM32F1的闪存模块由:主存储器、信息块和闪存存储器接口寄存器3部分组成
- 主存储器:用来存放代码和数据常量,起始地址是0x08000000,BOOT0和BOOT1都接GND时,就是从该起始地址运行代码的
- 信息块:分为2个小部分,启动程序代码是用来存储ST自带的启动程序,用于串口下载代码,BOOT0接3.3V,BOOT1接GND时,运行的就是这部分代码;选择字节则一般用于配置写保护、读保护等功能
- 闪存存储器接口寄存器:用于控制闪存读写等,是整个闪存模块的控制机构
对主存储器和信息块的写入由内嵌的闪存编程/擦除控制器(FPEC)管理:编程与擦除的高电压由内部产生。在执行闪存写操作时,任何对闪存的读操作都会锁住总线,在写操作完成后读操作才能正确地进行,即在进行写或擦除操作时,不能进行代码或数据的读取操作。
下面介绍闪存的读取、编程和擦除:
<1>. 闪存的读取 内置闪存模块可以在通用地址空间直接寻址,任何32位数据的读操作都能访问闪存模块的内容并得到相应的数据。
例如,要从地址addr,读取一个半字,可通过如下语句读取: data = *(__IO uint16_t*)addr
将addr强制转换为vu16指针,然后取该指针所指向的地址的值,即得到了addr地址的值
<2>. 闪存的编程 STM32的闪存编程是由FPEC(闪存编程和擦除控制器)模块处理的,这个模块包含7个32位寄存器,它们分别是:
FPEC键寄存器(FLASH_KEYR)
选择字节键寄存器(FLASH_OPTKEYR)
闪存控制寄存器(FLASH_CR)
闪存状态寄存器(FLASH_SR)
闪存地址寄存器(FLASH_AR)
选择字节寄存器(FLASH_OBR)
写保护寄存器(FLASH_WRPR)
其中FPEC键寄存器共有3中键值: PDPRT=0x000000A5; KEY1=0x45670123; KEY2=0xCDEF89AB
STM32复位后,FPEC模块是被保护的,不能写入FLASH_CR寄存器;通过写入特定的序列到FLASH_KEYR寄存器可以打开FPEC模块(即写入KEY1和KEY2),只有在写保护被解除后,才能操作相关寄存器。 闪存编程过程如下图所示:
<3>. 闪存的擦除 闪存编程的时候,要先判断其写入地址的FLASH是被擦除了的(也就是其值必须是0xFFFF),否则无法写入。闪存擦除分为页擦除和整片擦除。 闪存页擦除过程如下图示:
官方固件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); //等待操作完成函数
2、 硬件设计
led2指示灯用来提示系统运行状态,s1按键用来控制FLASH的数据写入,s2按键用来控制FLASH的数据读取,数据的写入与读取信息通过串口1打印出来
- LED2指示灯
- S2和S1按键
- USART1
- STM32F1内部FLASH
3、STM32CubeMX设置
- RCC设置外接HSE,时钟设置为72M
- PE5设置为GPIO推挽输出模式、上拉、高速、默认输出电平为高电平
- USART1选择为异步通讯方式,波特率设置为115200Bits/s,传输数据长度为8Bit,无奇偶校验,1位停止位
- PE3,PE4设置为GPIO输入模式、上拉模式
- 输入工程名,选择工程路径(不要有中文),选择MDK-ARM V5;勾选Generated periphera initialization as a pair of ‘.c/.h’ files per IP ;点击GENERATE CODE,生成工程代码
4、程序编程
- 创建按键驱动文件key.c 和相关头文件key.h
如果要对FLASH进行写入数据,需要执行以下四步:
- 解锁FLASH
- 擦除FLASH
- 写入FLASH
- 锁住FLASH
- 创建FLASH驱动文件stmflash.c 和相关头文件stmflash.h
#ifndef __STMFLASH_H__
#define __STMFLASH_H__#include "main.h" //FLASH起始地址
#define STM32_FLASH_BASE 0x08000000 //STM32 FLASH的起始地址#define FLASH_WAITETIME 50000extern void FLASH_PageErase(uint32_t PageAddress);
uint16_t STMFLASH_ReadHalfWord(uint32_t faddr); //读出半字
void STMFLASH_Write_NoCheck(uint32_t WriteAddr,uint16_t *pBuffer,uint16_t NumToWrite);
void STMFLASH_Write(uint32_t WriteAddr,uint16_t *pBuffer,uint16_t NumToWrite); //从指定地址开始写入指定长度的数据
void STMFLASH_Read(uint32_t ReadAddr,uint16_t *pBuffer,uint16_t NumToRead); //从指定地址开始读出指定长度的数据#endif
#include "stmflash.h"uint16_t STMFLASH_ReadHalfWord(uint32_t faddr)
{return *(__IO uint16_t*)faddr;
}void STMFLASH_Write_NoCheck(uint32_t WriteAddr,uint16_t *pBuffer,uint16_t NumToWrite)
{ uint16_t i;for(i=0;i<NumToWrite;i++){HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD,WriteAddr,pBuffer[i]);WriteAddr+=2; //地址增加2.}
} #define STM_SECTOR_SIZE 2048 //大容量STM32的扇区大小为2Kuint16_t STMFLASH_BUF[STM_SECTOR_SIZE/2];void STMFLASH_Write(uint32_t WriteAddr,uint16_t *pBuffer,uint16_t NumToWrite)
{uint32_t secpos; //扇区地址uint16_t secoff; //扇区内偏移地址(16位字计算)uint16_t secremain; //扇区内剩余地址(16位字计算) uint16_t i; uint32_t offaddr; //去掉0X08000000后的地址if(WriteAddr<STM32_FLASH_BASE||(WriteAddr>=(STM32_FLASH_BASE+1024*512)))return;//非法地址HAL_FLASH_Unlock(); //解锁offaddr=WriteAddr-STM32_FLASH_BASE; //实际偏移地址.secpos=offaddr/STM_SECTOR_SIZE; //扇区地址 0~127 for STM32F103RBT6secoff=(offaddr%STM_SECTOR_SIZE)/2; //在扇区内的偏移(2个字节为基本单位.)secremain=STM_SECTOR_SIZE/2-secoff; //扇区剩余空间大小 if(NumToWrite<=secremain)secremain=NumToWrite;//不大于该扇区范围while(1) { STMFLASH_Read(secpos*STM_SECTOR_SIZE+STM32_FLASH_BASE,STMFLASH_BUF,STM_SECTOR_SIZE/2);//读出整个扇区的内容for(i=0;i<secremain;i++) //校验数据{if(STMFLASH_BUF[secoff+i]!=0XFFFF)break;//需要擦除 }if(i<secremain) //需要擦除{FLASH_PageErase(secpos*STM_SECTOR_SIZE+STM32_FLASH_BASE); //擦除这个扇区FLASH_WaitForLastOperation(FLASH_WAITETIME); //等待上次操作完成CLEAR_BIT(FLASH->CR, FLASH_CR_PER); //清除CR寄存器的PER位,此操作应该在FLASH_PageErase()中完成!//但是HAL库里面并没有做,应该是HAL库bug!for(i=0;i<secremain;i++)//复制{STMFLASH_BUF[i+secoff]=pBuffer[i]; }STMFLASH_Write_NoCheck(secpos*STM_SECTOR_SIZE+STM32_FLASH_BASE,STMFLASH_BUF,STM_SECTOR_SIZE/2);//写入整个扇区 }else {FLASH_WaitForLastOperation(FLASH_WAITETIME); //等待上次操作完成STMFLASH_Write_NoCheck(WriteAddr,pBuffer,secremain);//写已经擦除了的,直接写入扇区剩余区间. }if(NumToWrite==secremain)break;//写入结束了else//写入未结束{secpos++; //扇区地址增1secoff=0; //偏移位置为0 pBuffer+=secremain; //指针偏移WriteAddr+=secremain*2; //写地址偏移(16位数据地址,需要*2) NumToWrite-=secremain; //字节(16位)数递减if(NumToWrite>(STM_SECTOR_SIZE/2))secremain=STM_SECTOR_SIZE/2;//下一个扇区还是写不完else secremain=NumToWrite;//下一个扇区可以写完了} }; HAL_FLASH_Lock(); //上锁
}void STMFLASH_Read(uint32_t ReadAddr,uint16_t *pBuffer,uint16_t NumToRead)
{uint16_t i;for(i=0;i<NumToRead;i++){pBuffer[i]=STMFLASH_ReadHalfWord(ReadAddr);//读取2个字节.ReadAddr+=2;//偏移2个字节. }
}
- 在main.c文件下编写STM32 flash测试代码
/* USER CODE BEGIN 0 */
const uint8_t Text_Buf[] = {"STM32F103ZET6 FLASH TEST"};
#define TEXTSIZE sizeof(Text_Buf)
#define FLASH_SAVE_ADDR 0x08070000/* USER CODE END 0 *//*** @brief The application entry point.* @retval int*/
int main(void)
{///***省略***////* USER CODE BEGIN WHILE */uint8_t key;uint8_t Read_Buf[TEXTSIZE];printf1("STM32 Flash Test...\r\n");while (1){key = KEY_Scan(0);if(key == 1){STMFLASH_Write(FLASH_SAVE_ADDR,(uint16_t *)Text_Buf,TEXTSIZE);printf1("FLASH Write : %s\r\n",Text_Buf);}if(key == 2){STMFLASH_Read(FLASH_SAVE_ADDR,(uint16_t *)Read_Buf,TEXTSIZE);printf1("FLASH Read : %s\r\n",Read_Buf);}HAL_GPIO_TogglePin(GPIOE,GPIO_PIN_5);HAL_Delay(200);/* USER CODE END WHILE *//* USER CODE BEGIN 3 */}/* USER CODE END 3 */
}
5、下载验证
编译无误下载到开发板后,可以看到LED2指示灯不断闪烁,当按下S1按键后数据写入到FLASH内,当按下S2按键后将写入的数据读取出来,同时串口打印出相应信息
6、参考文献
STM32CubeMX系列 | STM32内部FLASH - 知乎 (zhihu.com)
STM32CUBEMX-读写内部Flash_stm32cubemx flash-CSDN博客
STM32CubeMX学习笔记(51)——读写内部Flash_cubemx flash-CSDN博客