Flash(闪)在单片机指Flash Memory闪存,是一种非易失性存储器(NVM),所谓的非易失性存储器指断电后仍然保持数据的存储器。常见的NVM有Flash,EEPROM(电可擦除可编程只读存储器),ROM(只读存储器)等等。
在单片机中,Flash常用来存储代码和固件,用于存储启动程序(Bootloader)以及主应用程序代码。
以STM32F103C8T6为例,其FLASH大小为64K。从地址0x08000000开始,到0x0800FFFF总共64K的空间用来存放Bootloader和用户代码。

用STM32CubeProgram可以查看Flash具体的所写内容。

0x08000000开始,可以看到我的程序写到了0x8008CF0就结束了,后面都是未写的空间。
本期我们将使用STM32F103C8T6介绍如何读写Flash的剩余空间内容来存放一些数据使其断电不会丢失。
不过需要注意的是,这种方法可能会在擦写数据的时候(例如全片擦写)将数据擦写。真正的保险方式应该是采用外部EEPROM或者外部FLASH实现数据的存放,本文仅作参考。
STM32 的闪存并不像普通的内存那样可以单字节、单字或双字写入或擦除。闪存的写入和擦除是按照“页”进行的,每个页包含一定数量的字节。
具体的一页可能是1KB,可能是2KB具体需要查看STM32的参考手册才能知道。

STM32F103的页大小为1KB(0x400U),总共分成了64页。因此我们每次写入擦写的时候都需要先寻找到页的起始地址。然后计算总共需要写入的页数再进行擦写和写入。

我们利用CubeMX创建一个空的工程,因为内部FLASH的读写不需要涉及到任何的外设。因此不需要其他的设置。
在默认情况下Flash读写是被锁定的,因此我们要先对Flash解锁。
HAL_FLASH_Unlock(); //解锁Flash HAL_FLASH_Lock(); //上锁Flash
在写入之前,需要对我们写入的部分先进行擦写,这需要我们确定起始地址和结束地址。
void
Erase_Flash
(
uint32_t
startAddress,
uint32_t
endAddress)
{
FLASH_EraseInitTypeDef eraseInit;
uint32_t
pageError;
HAL_FLASH_Unlock();
// 设置擦除操作:按照页擦除
eraseInit.TypeErase = FLASH_TYPEERASE_PAGES;
eraseInit.PageAddress = startAddress;
eraseInit.NbPages = (endAddress - startAddress) / FLASH_PAGE_SIZE +
1
;
// 计算需要擦除的页数
// 执行擦除操作
if
(HAL_FLASHEx_Erase(&eraseInit, &pageError) != HAL_OK) {
Error_Handler();
}
HAL_FLASH_Lock();
}
擦写函数,根据起始地址和结束地址计算需要擦写的页。

我们先用STlink在Flash中写入一些数据,之后调用擦写函数。

可以看到,这一页的内容被成功的擦除了。之后我们再定义一个写入内容的函数。
void
Write_Flash
(
uint32_t
address,
uint32_t
data)
{
HAL_FLASH_Unlock();
// 确保地址对齐到 4 字节边界
if(address % 4!= 0) {
// 错误处理:地址不对齐
Error_Handler();
}
// 写入数据到 Flash
if
(HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, address, data) != HAL_OK) {
Error_Handler();
}
HAL_FLASH_Lock();
}
单字写入,这里需要注意的是,Flash寻址的时候需要四字节对其,例如0x800FFF0而不能是0x800FFF1这样子。

可以看到成功的读取了Flash地址800FFF0的值。这里用了volatile关键字,目的是防止目标地址的值被优化(不加也不会出什么大问题)。
接着在这个的基础之上,我们来实现一个写入字符串的函数。
void
Write_Spring_Flash(uint32_t address, uint8_t * str)
{
uint32_t data = 0;
//每32位数据缓存
uint32_t i = 0;
//位置索引
HAL_FLASH_Unlock();
// 确保地址对齐到 4 字节边界
if (address % 4 != ) {
Error_Handler();
}
while(*(str+i)!='\0')
//不是结尾符号
{
data = data | (((uint32_t)*(str+i))<<(8*(i%4))); i++; if(i%4 == 0)
//4*8 = 32位 之后写入一次
{
HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, address+(i-4), data);data = 0;
}
}
i++;
data = data | (((uint32_t)*(str+i))<<(8*(i%4)));
//最后截止符号'\0'也写入
HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, address+((i-4)/4+1)*4, data);
HAL_FLASH_Lock();
}
这个函数的主要流程是这样子的:


可以看到我们的数据成功的写入了进去,其实这里面还涉及到了单片机的大端小端存储方式。这里就不过多赘述了。
需要读字符串的时候,也较为简单,大家只要对指针的理解较为深入,就明白。一个uint8_t*的指针是可以用来存放字符串的。
uint8_t* strss; (uint32_t*)strss = (uint32_t*)0x800F000;
我们只需要定义一个字符串指针,在让他指向存放我们字符串的Flash地址,就可以获取字符串了。
内存空间和值是不变的,不同类型表达的输出方式不同。这也就是为什么我们要多走一步,把字符串结束符号\0也写入Flash的目的,这样子才会让uint8_t*截到完整的字符串。
不过大家在使用的过程中一定要小心谨慎。中间出错容易造成内存溢出的情况。

之所以采用字符串,是因为我们可以用sprintf函数和sscanf函数来快速获取我们需要的值。例如这里我们写入Temp:32.9来假设我们保存了一个温度数据。

这样子就完成了字符串的读取并且提取中我们需要的数据啦。
再次声明,内部Flash可能会遇到很多情况,例如被创建的数组覆盖,被程序代码覆盖,过多的擦写导致Flash失效等等情况。非必要情况下建议使用外置Flash使用。


登录 或 注册 后才可以进行评论哦!
还没有评论,抢个沙发!