不同的stm32单片机的flash大小不同,这个需要查阅芯片手册或者查看STM32CubeMX软件。
stm32的flash地址起始于0x0800 0000,结束地址是0x0800 0000加上芯片实际的flash大小,要操作flash时注意不要超出此范围。
Flash中的内容一般用来存储代码和一些定义为const的数据,和一些用户自定义的保存数据,它断电不丢失。
不同型号的单片机对flash的操作方式略有不同。下面我以自己用到的STML4R9VIT6为例贴上代码。
STM32的内部 FLASH 包含主存储器、系统存储器、 OTP 区域以及选项字节区域,具体分部查看响应的数据手册。
flash的写机制,只能将存储区域的1改为0,从0改为1只能选择擦除(页擦除或者扇区擦除),一次性擦除多个字节。
flash的读机制,只需要从flash的地址中读取数据就行了,可以字节读取,也可以按字读取。具体操作看代码。
首先需要说明的是,stm32内部flash空间包含多个用途,在保存自定义的数据时,千万不要去操作正在使用的区域。
其次,stm32是小端模式,数据的高字节保存在内存的高地址中, 而数据的低字节保存在内存的低地址中, 这种存储模式将地址的高低和数据位权有效地结合起来, 高地址部分权值高,低地址部分权值低。
//针对STM32L4R9VIT6单片机,通过地址获取所在页的函数如下
uint32_t getPage(uint32_t Address) //获取地址所在的
{
uint32_t page = 0;
if (Address < (FLASH_BASE + FLASH_BANK_SIZE))
page = (Address - FLASH_BASE) / FLASH_PAGE_SIZE;
else
page = (Address - (FLASH_BASE + FLASH_BANK_SIZE)) / FLASH_PAGE_SIZE;
return page;
}
//这是获取bank的代码
uint32_t GetBank(uint32_t Addr)
{
uint32_t bank = 0;
if (READ_BIT(SYSCFG->MEMRMP, SYSCFG_MEMRMP_FB_MODE) == 0)
{
if (Addr < (FLASH_BASE + FLASH_BANK_SIZE))/* No Bank swap */
bank = FLASH_BANK_1;
else
bank = FLASH_BANK_2;
}
else
{
if (Addr < (FLASH_BASE + FLASH_BANK_SIZE))/* Bank swap */
bank = FLASH_BANK_2;
else
bank = FLASH_BANK_1;
}
return bank;
}
//这是擦除函数,stm32在写数据之前,都需要先把地址中的数据擦除成0xFF,然后才能写数据
FLASH_EraseInitTypeDef EraseInitStruct;
uint8_t eraseFlash(uint32_t start_addr,uint32_t end_addr) //擦除flash
{
uint32_t BANK;
uint32_t FirstPages = 0,LastPages = 0, NbPages = 0;
uint32_t pageError = 0;
HAL_FLASH_Unlock(); //首先解锁flash
FirstPages = getPage(start_addr); //获取要擦除的第一个页
LastPages = getPage(end_addr);
NbPages = LastPages - FirstPages + 1; //获取擦除的页数量
BANK = GetBank(start_addr); //判断地址的Banks是1还是2
/* Fill EraseInit structure*/
EraseInitStruct.TypeErase = FLASH_TYPEERASE_PAGES; //页擦除
EraseInitStruct.Banks = BANK;
EraseInitStruct.Page = FirstPages;
EraseInitStruct.NbPages = NbPages;
if(HAL_FLASHEx_Erase(&EraseInitStruct, &pageError) != HAL_OK)
{
HAL_FLASH_Lock(); //上锁
return 2; //擦除有错误,返回2
}
//下面这些是清除标志位
__HAL_FLASH_DATA_CACHE_DISABLE();
__HAL_FLASH_INSTRUCTION_CACHE_DISABLE();
__HAL_FLASH_DATA_CACHE_RESET();
__HAL_FLASH_INSTRUCTION_CACHE_RESET();
__HAL_FLASH_INSTRUCTION_CACHE_ENABLE();
__HAL_FLASH_DATA_CACHE_ENABLE();
HAL_FLASH_Lock(); //上锁
return 0;
}
其中有个重要的东西是FLASH_EraseInitTypeDef结构体
typedef struct
{
uint32_t TypeErase; //页擦除和块擦除
uint32_t Banks; //选择擦除的bank区域
uint32_t Page; //擦除的起始页
uint32_t NbPages; //要擦除的页数
} FLASH_EraseInitTypeDef;
然后下面是写flash操作
__HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_OPTVERR);//清除flash标志位
eraseFlash(ADDR_FLASH_START,ADDR_FLASH_END);//擦除要写入的flash地址的数据
HAL_FLASH_Unlock();//解锁flash
HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD, ADDR_FLASH_START, (uint64_t)data);//写数据
HAL_FLASH_Lock();//上锁
这里详细解说一下HAL_FLASH_Program()函数,函数原型如下
HAL_StatusTypeDef HAL_FLASH_Program(uint32_t TypeProgram, uint32_t Address, uint64_t Data);
/*
返回值类型如下
typedef enum
{
HAL_OK = 0x00,
HAL_ERROR = 0x01,
HAL_BUSY = 0x02,
HAL_TIMEOUT = 0x03
} HAL_StatusTypeDef;
可以用返回值是否等于HAL_OK来判断是否写入成功.
参数TypeProgram是按什么类型写入数据,不同的stm32支持的数据类型不尽相同,需要自己查看程序定义,
支持的数据类型有按字节/半字/字/双字.
参数Address是要写的flash地址,这里需要注意的是,如果要写入两次数据,第二次写入时地址需要做偏移.一个地址对应一个字节.见如下实例.
*/
//如果是按字节写入(stm32L4R9VIT6不支持这种方式)
uint8_t a=1;
uint8_t b=2;
HAL_FLASH_Program(FLASH_TYPEPROGRAMDATA_BYTE, addr, a);//第一次写数据
HAL_FLASH_Program(FLASH_TYPEPROGRAMDATA_BYTE, addr+1, b);//第二次写数据,需要地址偏1
//如果是按双字写入(stm32L4R9VIT6支持)
uint64_t a=0x12345678;
uint64_t b=0x87654321;
HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD, addr, a);//第一次写数据
HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD, addr+8, b);//第二次写数据,需要地址偏8
//这里不同的是,按字节写入,占用一个地址的数据,所以偏移1,按双字写入,占用8个地址的数据,所以偏移8
读数据就很简单了
//读取操作中不需要加锁和解锁,直接一句代码即可完成一次数据的读取操作
//按字节读取
uint8_t a = 0;
a = *(__IO uint8_t*)(start_addr);
//按双字读取
uint64_t a = 0;
a = *(__IO uint64_t*)(start_addr);
//这里依然要注意读取多个数据时地址偏移量的问题,字节偏移1,半字偏移2,字偏移4,双字偏移8
另外,一个比较方便的操作,是读写结构体。这种操作可以很容易的获取和保存多个数据,从而不需要数组赋值
比如我们要保存如下结构体
typedef struct _userinfo//保存的信息
{
uint8_t id;
uint8_t ip[16]; //ip
uint16_t port; //端口
}USERINFO;
USERINFO User_Info = {1,"192.168.0.1",8808};
__HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_OPTVERR);
eraseFlash(ADDR_FLASH_USER_START,ADDR_FLASH_USER_END);
HAL_FLASH_Unlock();
for(uint8_t i = 0;i < sizeof(USERINFO);i+=8)
{
if(HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD, ADDR_FLASH_USER_START+i,*((__IO uint64_t*)(&userinfo->id+i)))!= HAL_OK)//这里的数据由于是结构体,需要从里面取值,一定要以第一个成员的地址开始偏移,不能使用结构体自身的地址来偏移,否则数据会出错
{
HAL_FLASH_Lock();
return 1;
}
}
HAL_FLASH_Lock();
//为了避免数据出错,读操作一定要和写操作对应,写是按双字写,读就要按双字读,否则就需要解决大小端的问题
for(uint8_t i = 0;iid+i)) = *(__IO uint64_t*)(ADDR_FLASH_USER_START + i);//注意赋值的左边,必须要用结构体第一个成员的地址来偏移,双字偏移量是8
}
//这样获取的结构体内容,可以直接通过结构体变量或者结构体指针来访问了.