** 1 **
** 什么是Flash **
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实现数据的存放,本文仅作参考。
** 2 **
** 页的概念 **
STM32 的闪存并不像普通的内存那样可以单字节、单字或双字写入或擦除。闪存的写入和擦除是按照“页”进行的, ** 每个页包含一定数量的字节 ** 。
具体的一页可能是1KB,可能是2KB具体需要查看STM32的参考手册才能知道。
STM32F103的页大小为1KB(0x400U),总共分成了64页。因此我们每次写入擦写的时候都需要先寻找到页的起始地址。然后计算总共需要写入的页数再进行擦写和写入。
** 3 **
** CubeMX设置 **
我们利用CubeMX创建一个空的工程,因为内部FLASH的读写不需要涉及到任何的外设。因此不需要其他的设置。
** 4 **
** 代码实现 **
在默认情况下Flash读写是被锁定的,因此我们要先对Flash解锁。
HAL_FLASH_Unlock();//解锁FlashHAL_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这样子。
我们调用我们的单字节写入函数。
可以看到我们成功的写入了0x488。
关于 ** 读取Flash ** 的内容就更为简单了。
众所周知,指针是用来指向内存空间的一种变量。单片机中亦是如此。可以使用 解引用 + (uint32_t) 地址的方式来获取Flash中某个地址的值。
例如我们上面向0x800FFF0中写入了0x488。可以用如下的代码来获取0x800FFF0的值。
uint32_t Data; Data = *(volatile uint32_t*)0x800FFF0;
可以看到成功的读取了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 != 0) { Error_Handler(); } while(*(str+i)!='')//不是结尾符号 { 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))); //最后截止符号''也写入 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地址 ** ,就可以获取字符串了。
内存空间和值是不变的,不同类型表达的输出方式不同。这也就是为什么我们要多走一步, ** 把字符串结束符号也写入Flash **
的目的,这样子才会让uint8_t*截到完整的字符串。
不过大家在使用的过程中一定要小心谨慎。中间出错容易造成内存溢出的情况。
之所以采用字符串,是因为我们可以用sprintf函数和sscanf函数来快速获取我们需要的值。例如这里我们写入Temp:32.9来假设我们保存了一个温度数据。
这样子就完成了字符串的读取并且提取中我们需要的数据啦。
再次声明,内部Flash可能会遇到很多情况,例如被创建的数组覆盖,被程序代码覆盖,过多的擦写导致Flash失效等等情况。非必要情况下建议使用外置Flash使用。