计划建两个底层模块,最底层叫MyFLASH,在这里面我们要实现闪存的最基本的3个功能,也就是读取、擦除和编程,之后在此模块上计划再建一个模块叫store,主要实现参数数据的读写和存储管理,因为我们最终应用要实现的功能是任意读写参数,并且这些参数是掉电不丢失的,至于存在什么地方,怎么擦除、采用什么读写策略这并不是应用层所关心的,所以在store这一层我们会定义一个SRAM数组,需要掉电不丢失的参数就写到SRAM数组里,之后调用保存的函数,这个SRAM数组就自动备份到闪存里,上电后Store会自动再把闪存里的数据读回到SRAM数组里,这是闪存的管理策略
最后就是main.c里面的应用层部分了,想要保存参数就写到Store层的数组再调用保存函数备份到闪存,这样就实现最终功能了。
对于本节代码的调试,我们还有一个非常强大的辅助软件可以使用:STM32 ST_LINK Utility
连接好STM32后,点击该软件的connect按键

可以看到窗口里显示的就是闪存里面存储的数据了,也就是说闪存里每个地址下到底存储了什么,我们通过这个软件都可以直接清晰地看到
除了查看之外,这个软件可以直接任意地修改闪存的数据
还有一个功能就是选项字节的读写,打开target-option bytes,就可以看到选项字节配置界面

读保护、用户配置部分、自定义的两个字节和写保护这四个部分的配置也是在这里点点就能完成了。配置完后点Apply,选项字节就自动配置好了
我们先梳理一下流程:首先闪存并不需要初始化直接操作即可,然后第一个的读取数据,我们直接把下面这句代码封装一下就好了:
uint16_t Data = *((__IO uint16_t *) (0x08000000));
之后第二个,擦除


全擦除和页擦除,这两个功能分别对应一个库函数都封装好了,详细流程不用我们写,但是要记得,执行之前手动调用一下解锁,执行之后一般还要再加锁一下
最后第三个,编程也是有对应的函数,调用就行

这就是闪存的三个操作,读取、擦除和编程,之后选项字节的擦除和编程和主闪存的擦除和编程类似,也都有对应的函数。
接下来先看看库函数,本节用到的库是flash.c和flash.h
void FLASH_Unlock(void);//解锁用的,就是在KEYR寄存器先写入KEY1再写入KEY2
void FLASH_Lock(void);//加锁,就是把CR寄存器的LOCK位设置为1
FLASH_Status FLASH_ErasePage(uint32_t Page_Address);//闪存擦除某一页,参数给一个页的起始地址,函数执行完后指定的一页就被擦除了,返回值是这个参数的完成状态。这是一个枚举,在执行完后会返回状态,
FLASH_BUSY = 1:表示芯片当前忙
FLASH_ERROR_PG:表示编程错误
FLASH_ERROR_WRP:写保护错误
FLASH_COMPLETE:执行完,完全没问题
FLASH_TIMEOUT:等待超时
FLASH_Status FLASH_EraseAllPages(void);//全擦除
FLASH_Status FLASH_EraseOptionBytes(void);//擦除选项字节
FLASH_Status FLASH_ProgramWord(uint32_t Address, uint32_t Data);//指定地址写入字,就是写入两次半字,先写如低16位再写高16位
FLASH_Status FLASH_ProgramHalfWord(uint32_t Address, uint16_t Data);//指定地址写入半字
FLASH_Status FLASH_ProgramOptionByteData(uint32_t Address, uint8_t Data);//变成选项字节自定义的data0和data1
FLASH_Status FLASH_EnableWriteProtection(uint32_t FLASH_Pages);//写保护
FLASH_Status FLASH_ReadOutProtection(FunctionalState NewState);//读保护
FLASH_Status FLASH_UserOptionByteConfig(uint16_t OB_IWDG, uint16_t OB_STOP, uint16_t OB_STDBY);//写入用户选项的三个配置为
uint32_t FLASH_GetUserOptionByte(void);//获取用户选项的三个配置位
uint32_t FLASH_GetWriteProtectionOptionByte(void);//获取写保护状态
FlagStatus FLASH_GetReadOutProtectionStatus(void);//获取读保护状态
void FLASH_ITConfig(uint32_t FLASH_IT, FunctionalState NewState);//中断使能
FlagStatus FLASH_GetFlagStatus(uint32_t FLASH_FLAG);//获取标志位
void FLASH_ClearFlag(uint32_t FLASH_FLAG);//清除标志位
FLASH_Status FLASH_GetStatus(void);//获取状态
FLASH_Status FLASH_WaitForLastOperation(uint32_t Timeout);//等待上一次操作完成,就是等待忙,等待BSY为0
建立好MyFLASH模块,我们要实现最开始规划的3个功能,就是读取、擦除和编程,FLASH不需要初始化,所以我们直接从读取的部分开始
uint32_t MyFLASH_ReadWord(uint32_t Address)//STM32中所有的地址都是32位宽度的
{
return *((__IO uint32_t *)(Address));
}
uint16_t MyFLASH_ReadHalfWord(uint32_t Address)//以半字16位的形式读出来
{
return *((__IO uint16_t *)(Address));
}
uint8_t MyFLASH_ReadByte(uint32_t Address)//以字节8位的形式读取
{
return *((__IO uint8_t *)(Address));
}
#include “stm32f10x.h” // Device header
#include “Delay.h”
#include “OLED.h”
#include “MyFLASH.h”
int main(void)
{
OLED_Init();
OLED_ShowHexNum(1, 1, MyFLASH_ReadWord(0x08000000), 8);//读取闪存的起始地址
OLED_ShowHexNum(2, 1, MyFLASH_ReadHalfWord(0x08000000), 4);
OLED_ShowHexNum(3, 1, MyFLASH_ReadByte(0x08000000), 2);
while (1)
{
}
}
这个数据我们可以用STM32 ST-LINK Utility来验证:
我们可以发现,这个数据是以小端模式存储的,就是低位字节存储在低位地址,如果以字节读取,第一个就是60,以半字读取,前两个字节组合到一起由高位到低位是06 60,以字读取,前四个字节组合到一起由高位到低位,是20 00 06 60,这是反过来的就是小端存储
接下来我们继续来写擦除的代码:
void MyFLASH_EraseAllPages(void)
{
//第一步,对FLASH进行解锁
FLASH_Unlock();
//第二步,直接调用库里的EraseAllPages
FLASH_EraseAllPages();
//执行完后,再将FLASH锁上
FLASH_Lock();
//得益于库函数的封装,实施的具体流程,比如置控制位,置STRT,等待忙等,这些操作,在擦除的一个函数里已经写好了
//但这个函数不包含解锁和加锁,所以需要我们另外调用函数实现,擦除函数也有一个返回值,表示执行的状态。
}
void MyFLASH_ErasePage(uint32_t PageAddress)
{
FLASH_Unlock();
FLASH_ErasePage(PageAddress);
FLASH_Lock();
}
主函数测试:
#include “stm32f10x.h” // Device header
#include “Delay.h”
#include “OLED.h”
#include “MyFLASH.h”
#include “Key.h”
uint8_t KeyNum;
int main(void)
{
OLED_Init();
Key_Init();
OLED_ShowHexNum(1, 1, MyFLASH_ReadWord(0x08000000), 8);//读取闪存的起始地址
OLED_ShowHexNum(2, 1, MyFLASH_ReadHalfWord(0x08000000), 4);
OLED_ShowHexNum(3, 1, MyFLASH_ReadByte(0x08000000), 2);
while (1)
{
KeyNum = Key_GetNum();
if (KeyNum == 1)//按下按键1
{
MyFLASH_EraseAllPages();
}
if (KeyNum == 2)//按下按键2
{
MyFLASH_ErasePage(0x08000000);//把闪存的第一页,前1K,1024个字节擦掉
}
}
}
按下按键1后:
可以看到闪存的所有数据都被擦除了,当按下Key1的时候,整个闪存都已经被擦除了,程序文件已经不复存在了,但是OLED显示的数值并没有消失,是因为OLED里面有显存可以保存最后一次显示的内容,断电后再上电,OLED就不显示任何内容了,无论按什么都没有反应,现在整个芯片是空白的,没有任何程序
按下按键2后,可以看到闪存只有第一页被擦除了:
我们继续完成下一个任务,编程:
void MyFLASH_ProgramWord(uint32_t Address, uint32_t Data)//编程整字
{
FLASH_Unlock();
FLASH_ProgramWord(Address, Data);
FLASH_Lock();
}
void MyFLASH_ProgramHalfWord(uint32_t Address, uint16_t Data)//编程半字,因为写入字节比较麻烦,最好用缓存区的方式来实现,所以函数就没有写入字节的
{
FLASH_Unlock();
FLASH_ProgramHalfWord(Address, Data);
FLASH_Lock();
}
接下来主函数测试,在写入的时候,尽量就不要破坏程序本身了,所以我们可以在闪存的最后一页写入测试数据,这样不会影响程序
对于64K的闪存,最后一页的起始地址就是0800FC00,所以我们在FC00的位置写一些数据看看,需要注意的是,写入之前一定要先执行擦除
MyFLASH_ErasePage(0x0800FC00);
MyFLASH_ProgramWord(0x0800FC00, 0x45678900);
MyFLASH_ProgramHalfWord(0x0800FC10, 0xCCDD);
可以看到FC00的位置写入了45678900,FC10的位置写入了CCDD
至此,FLASH的底层代码读取、擦除和编程都写完了,接下来我们来完成更上层的业务代码,现在要实现的是参数掉电不丢失的存储
基于这个MyFLASH层,我们就可以再建立一个Store模块
在Store模块,我们要用SRAM缓存数组来管理FLASH的最后一页,实现参数的任意读写和保存,因为闪存每次都是擦除再写入的,擦除之后很容易丢失数据,所以要想灵活管理数据还是要靠SRAM数组,需要备份的时候,再统一转到闪存里。
在闪存的最后一页,直接对它进行读写的话,肯定不方便,效率低,还容易丢失数据,所以我们在SRAM中定义一个数组,它就是闪存的“分身”,我们再读写任何数据,就直接对SRAM操作,但是SRAM掉电丢失所以我们需要闪存的配合,SRAM每次更改的时候都把数组整体备份到闪存里,而在上电的时候我们再把闪存里的数据初始化加载回到SRAM里,这样SRAM数组就相当于掉电不丢失了,另外为了判断这个闪存是不是之前保存过数据,我们还需要一个标志位来配合,如果标志位是A5A5就说明闪存已经保存过了,我们就上电直接加载回保存的数据,如果标志位不是A5A5就说明闪存是第一次使用,我们就先初始化一下再加载数据。
uint16_t Store_Data[512];//512个数据,每个数据16位,2字节,正好对应闪存的一页1024
void Store_Init(void)
{
//首先我们需要把闪存初始化一下,比如第一次使用这个代码,那闪存默认全是FF,而参数和SRAM一般默认0
//所以对于第一次使用,我们要给闪存的各个参数都置0,怎么判断是不是第一次使用的,我们就定一个标志位
//把闪存的最后一页的第一个半字当做标志位
if (MyFLASH_ReadHalfWord(0x0800FC00) != 0xA5A5)//这个A5A5是随便规定的一个标志位,如果第一个半字不是A5A5就说明是第一次使用
{
MyFLASH_ErasePage(0x0800FC00);
MyFLASH_ProgramHalfWord(0x0800FC00, 0xA5A5);//写入规定的标志位0xA5A5,让下一次判断知道不是第一次编程了
//把剩余的存储空间全都置为默认值0,因为原本全都是FF
for (uint16_t i = 1; i < 512; i++)//i从1开始是因为第一个字节是标志位
{
MyFLASH_ProgramHalfWord(0x0800FC00 + i * 2, 0x0000);//因为一个半字占用两个地址,所以要乘2
}
//没初始化的时候,闪存最后一页应该全是FF,初始化之后闪存最后一页的第一个半字是标志位A5A5,剩下的数据全是0
}
//接着还有第二大步,就是上电的时候把闪存的数据全都转到SRAM数组里,转到数组里的过程就是上电的时候恢复数据,实现数据掉电不丢失
for (uint16_t i = 0; i < 512; i++)//i要从0开始,标志位也要转到数组里,要不然后续备份数据的时候,标志位就丢失了
{
Store_Data[i] = MyFLASH_ReadHalfWord(0x0800FC00 + i * 2);//在上电的时候把闪存备份的数据恢复到SRAM数组里
}
}
//之后,我们想存储掉电不丢失的参数的时候,就先任意更改这个数组除了标志位的其他数据,更改好后,我们把这个数组整体备份到闪存的最后一页
void Store_Save(void)
{
//第一步,擦除最后一页
MyFLASH_ErasePage(0x0800FC00);
//第二步,把数组完全备份保存到闪存
for (uint16_t i = 0; i < 512; i++)//i要从0开始,标志位也要转到数组里,要不然后续备份数据的时候,标志位就丢失了
{
MyFLASH_ProgramHalfWord(0x0800FC00 + i * 2, Store_Data[i]);
}
}
//因为这个数组实现了掉电不丢失,正常情况下不太方便把所有数据清零,为了方便使用,来个清零函数
void Store_Clear(void)
{
for (uint16_t i = 1; i < 512; i++)//注意从1开始,别把标志位清零
{
Store_Data[i] = 0x0000;
}
Store_Save();//注意每次更改完后都要save
}
主函数测试:
#include “stm32f10x.h” // Device header
#include “Delay.h”
#include “OLED.h”
#include “Store.h”
#include “Key.h”
uint8_t KeyNum;
int main(void)
{
OLED_Init();
Key_Init();
Store_Init();
while (1)
{
KeyNum = Key_GetNum();
if (KeyNum == 1)//按下按键1
{
Store_Data[1] = 0x5678;
Store_Data[2] = 0xEEEE;
Store_Save();
}
}
}
按下按键后,可以通过软件看到,最后一页的第一个半字为标志位AFAF,后面两个半字都是我们在主函数里定义好的
#include “stm32f10x.h” // Device header
#include “Delay.h”
#include “OLED.h”
#include “Store.h”
#include “Key.h”
uint8_t KeyNum;
int main(void)
{
OLED_Init();
Key_Init();
Store_Init();
OLED_ShowString(1, 1, “Flag”);
OLED_ShowString(2, 1, “Data”);
while (1)
{
KeyNum = Key_GetNum();
if (KeyNum == 1)//按下按键1,数据变换
{
Store_Data[1] ++;
Store_Data[2] += 3;
Store_Data[3] += 6;
Store_Data[4] += 7;
Store_Save();
}
if (KeyNum == 2)//按下按键2,数据清零
{
Store_Clear();
}
OLED_ShowHexNum(1, 6, Store_Data[0], 4);
OLED_ShowHexNum(3, 1, Store_Data[1], 4);
OLED_ShowHexNum(3, 6, Store_Data[2], 4);
OLED_ShowHexNum(4, 1, Store_Data[3], 4);
OLED_ShowHexNum(4, 6, Store_Data[4], 4);
}
}
断电再上电后数据也不会丢失
目前我们的假设是程序文件比较小,最后一页肯定是没有用到的,所以我们放心地使用最后一页,但是如果程序比较大触及到了最后一页,那程序和用户数据存储的位置就冲突了,这种冲突如果我们没有发现就会产生非常隐蔽的bug,这时我们可以给程序文件限定一个存储范围,不让它分配到我们后面的用户数据的空间,打开工程选项,下面就是编译器给各个数据分配的空间地址和范围了,左下角的IROM1就是,起始地址是08000000,size是0x10000,默认全部的64K闪存都是程序代码分配的空间,如果想把闪存的尾部空间留着其他用,可以把这个程序空间的size改小一点,比如改成0xFC00,这样编译后的代码无论如何也分配不到最后一页了
到今天为止,可算是把STM32的入门教学看完了,很感谢江大的视频,做的真的很好。感觉学的也挺仔细的,基本每次跟着写代码的时候都会跟着写日志,像做笔记一样把觉得重要的点都记下来,感觉记得有点多了,笔记本差不多用了有6分之5了,感觉收获还是挺多的。但知识点还是比较基础,学到现在一个多月了,我也有些想法了,有看到几个想做的东西,比如一个旋转LED灯,觉得还挺炫的。还有就是接下来想学的东西也挺多的,一个是PCB板,一个是RTOS,一个是小熊派,小熊派是我们物联网控制课程的实验,我想顺便好好学学,自己也买了一块小熊派的板子。想学的东西挺多的,我感觉PCB板的优先级还是有点高的,因为跟着那些开源的项目学着做的话很多都是要自己做一下PCB板的。接着学吧,感觉现在学东西还是挺有动力也挺快的,主要还是有写日志的帮助我感觉,因为一边学一遍记下来,感觉就是真的有在产生一些东西出来,不然一直学,东西都在脑子里或者电脑里也没有东西能组织起来的话,不管是复习还是和别人说起来感觉都有点不那么踏实。确实写这么久的日志下来,感觉就是很踏实,希望以后都能一直写下去吧。
进入推荐歌曲:インナーワールド——sakanaction(点击左下角倒数第十三首即可收听)