读写内部闪存FLASH
右下角是OLED,然后左上角在PB1和PB11两个引脚,插上两个按键用于控制。下一个代码读取芯片ID,这个也是接上一个OLED,能显示测试数据就可以了。
STM32-STLINK Utility
本节的代码调试,使用辅助软件STM32-STLINK Utility,在使用之前我们需要用stink把STM32连接好,然后我们点击这个按钮连接,可以看到下面这个窗口里显示的,就是闪存里面存储的数据了。这个软件可以直接修改闪存里的数据,可以修改选项字节数据,不用写任何代码,非常方便。
Address:可以指定想要查看的起始地址,目前是0x0800 0000,也就是整个闪存的起始地址。
Size:就是从起始地址开始,总共查看多少个字节,目前0x10000,就是查看64KB的字节数
Data Width:数据宽度,可以指定以32位/16位/8位的形式显示,分别对应,字/半字/字节。
16位
8位
闪存的操作:
第一步,读取数据,直接封装一下uint16_t Data = *((__IO uint16_t *)(0x08000000));
第二步,擦除:全擦除或页擦除,这两个功能分别对应一个库函数。执行之前,手动调用,解锁;执行之后,在加锁。
第三步,编程,有对应库函数直接调用。执行之前,手动调用,解锁;执行之后,在加锁。
选项字节的擦除和编成与主闪存的擦除和编程类似,有对应的函数。
用代码配置读写保护,容易造成芯片自锁,如把闪存写保护了,但程序里并没有预留解除写保护的代码,这样锁住之后,芯片就没法下载程序。但还可以再软件里自救,在这个选项字节配置里,把读写保护去掉,再Apply就能救活芯片了。
flash.h库函数
FLASH_Status返回值,是这个操作的完成状态,函数返回状态功能:返回第一个,表示芯片当前忙;返回第二个,表示编程错误;返回第三个,表示写保护错误;返回第四个,表示返回完成;返回第五个,表示等待超时。
//这三个是内核相关,不用过多了解与调用
void FLASH_SetLatency(uint32_t FLASH_Latency);
void FLASH_HalfCycleAccessCmd(uint32_t FLASH_HalfCycleAccess);
void FLASH_PrefetchBufferCmd(uint32_t FLASH_PrefetchBuffer);
//加锁解锁
void FLASH_Unlock(void);//解锁
void FLASH_Lock(void);//加锁,把CR寄存器LOCK位置1
//主闪存
FLASH_Status FLASH_ErasePage(uint32_t Page_Address); //闪存擦除某一页
FLASH_Status FLASH_EraseAllPages(void);//全擦除
FLASH_Status FLASH_EraseOptionBytes(void);//擦除选项字节
FLASH_Status FLASH_ProgramWord(uint32_t Address, uint32_t Data);//在指定地址,写入字
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);//用户选项的3个配置位
//获取选项字节当前状态
uint32_t FLASH_GetUserOptionByte(void);//获取用户选项的三个配置位
uint32_t FLASH_GetWriteProtectionOptionByte(void);//获取写保护状态
FlagStatus FLASH_GetReadOutProtectionStatus(void);//获取读保护状态
............................ ;//获取自定义的Data0和Data1没有给现成函数,直接指针访问就可以了。
FlagStatus FLASH_GetPrefetchBufferStatus(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(上面主闪存这些函数内部已经实现等待忙操作了,此函数不用我们单独调用)
注意:使用完STLINK这个软件,要及时断开连接,要不然设备占用,keil下载出错。
在Store模块,要有SRAM缓存数组来管理FLASH的最后一页,实现参数任意读写和保存
MyFLASH.c
#include "stm32f10x.h" // Device header
/**
* 函 数:FLASH读取一个32位的字
* 参 数:Address 要读取数据的字地址
* 返 回 值:指定地址下的数据
*/
uint32_t MyFLASH_ReadWord(uint32_t Address)
{
return *((__IO uint32_t *)(Address)); //使用指针访问指定地址下的数据并返回
}
/**
* 函 数:FLASH读取一个16位的半字
* 参 数:Address 要读取数据的半字地址
* 返 回 值:指定地址下的数据
*/
uint16_t MyFLASH_ReadHalfWord(uint32_t Address)
{
return *((__IO uint16_t *)(Address)); //使用指针访问指定地址下的数据并返回
}
/**
* 函 数:FLASH读取一个8位的字节
* 参 数:Address 要读取数据的字节地址
* 返 回 值:指定地址下的数据
*/
uint8_t MyFLASH_ReadByte(uint32_t Address)
{
return *((__IO uint8_t *)(Address)); //使用指针访问指定地址下的数据并返回
}
/**
* 函 数:FLASH全擦除
* 参 数:无
* 返 回 值:无
* 说 明:调用此函数后,FLASH的所有页都会被擦除,包括程序文件本身,擦除后,程序将不复存在
*/
void MyFLASH_EraseAllPages(void)
{
FLASH_Unlock(); //解锁
FLASH_EraseAllPages(); //全擦除
FLASH_Lock(); //加锁
}
/**
* 函 数:FLASH页擦除
* 参 数:PageAddress 要擦除页的页地址
* 返 回 值:无
*/
void MyFLASH_ErasePage(uint32_t PageAddress)
{
FLASH_Unlock(); //解锁
FLASH_ErasePage(PageAddress); //页擦除
FLASH_Lock(); //加锁
}
/**
* 函 数:FLASH编程字
* 参 数:Address 要写入数据的字地址
* 参 数:Data 要写入的32位数据
* 返 回 值:无
*/
void MyFLASH_ProgramWord(uint32_t Address, uint32_t Data)
{
FLASH_Unlock(); //解锁
FLASH_ProgramWord(Address, Data); //编程字
FLASH_Lock(); //加锁
}
/**
* 函 数:FLASH编程半字
* 参 数:Address 要写入数据的半字地址
* 参 数:Data 要写入的16位数据
* 返 回 值:无
*/
void MyFLASH_ProgramHalfWord(uint32_t Address, uint16_t Data)
{
FLASH_Unlock(); //解锁
FLASH_ProgramHalfWord(Address, Data); //编程半字
FLASH_Lock(); //加锁
}
MyFLASH.h
#ifndef __MYFLASH_H
#define __MYFLASH_H
uint32_t MyFLASH_ReadWord(uint32_t Address);
uint16_t MyFLASH_ReadHalfWord(uint32_t Address);
uint8_t MyFLASH_ReadByte(uint32_t Address);
void MyFLASH_EraseAllPages(void);
void MyFLASH_ErasePage(uint32_t PageAddress);
void MyFLASH_ProgramWord(uint32_t Address, uint32_t Data);
void MyFLASH_ProgramHalfWord(uint32_t Address, uint16_t Data);
#endif
Store.c
#include "stm32f10x.h" // Device header
#include "MyFLASH.h"
#define STORE_START_ADDRESS 0x0800FC00 //宏定义,存储的起始地址 (闪存的最后一页)
#define STORE_COUNT 512 //宏定义,存储数据的个数
uint16_t Store_Data[STORE_COUNT]; //定义SRAM数组
/**
* 函 数:参数存储模块初始化
* 参 数:无
* 返 回 值:无
*/
void Store_Init(void) //第一大步,第一次使用的时候,对闪存进行初始化
{
/*判断是不是第一次使用*/
if (MyFLASH_ReadHalfWord(STORE_START_ADDRESS) != 0xA5A5) //读取第一个半字的标志位,if成立,则执行第一次使用的初始化
{
MyFLASH_ErasePage(STORE_START_ADDRESS); //擦除指定页
MyFLASH_ProgramHalfWord(STORE_START_ADDRESS, 0xA5A5); //在第一个半字写入自己规定的标志位,用于判断是不是第一次使用
//把剩余的存储空间,全都置为默认值0
for (uint16_t i = 1; i < STORE_COUNT; i ++) //循环STORE_COUNT次,除了第一个标志位。注意;要从1开始,而不是0,因为第一个半字是标志位,剩下的才是有效数据
{
MyFLASH_ProgramHalfWord(STORE_START_ADDRESS + i * 2, 0x0000); //除了标志位的有效数据全部清0。注:因为一个半字要占用两个地址,所以i要乘2
}
}
/*第2大步,上电时,将闪存数据加载回SRAM数组,就是上电时恢复数据,实现SRAM数组的掉电不丢失*/
for (uint16_t i = 0; i < STORE_COUNT; i ++) //循环STORE_COUNT次,包括第一个标志位
{
Store_Data[i] = MyFLASH_ReadHalfWord(STORE_START_ADDRESS + i * 2); //将闪存的数据加载回SRAM数组
}
}
/**
* 函 数:参数存储模块保存数据到闪存
* 参 数:无
* 返 回 值:无
*/
void Store_Save(void) //备份保存
{
MyFLASH_ErasePage(STORE_START_ADDRESS); //擦除指定页
for (uint16_t i = 0; i < STORE_COUNT; i ++) //循环STORE_COUNT次,包括第一个标志位
{
MyFLASH_ProgramHalfWord(STORE_START_ADDRESS + i * 2, Store_Data[i]); //将SRAM数组的数据,备份保存到闪存
}
}
/**
* 函 数:参数存储模块将所有有效数据清0
* 参 数:无
* 返 回 值:无
*/
void Store_Clear(void)
{
for (uint16_t i = 1; i < STORE_COUNT; i ++) //循环STORE_COUNT次,除了第一个标志位。注:i要从1开始,不用把标志位清0
{
Store_Data[i] = 0x0000; //SRAM数组有效数据清0
}
Store_Save(); //更新保存数据到闪存。每次修改完数组后,都要Store_Save();保证数值和闪存数据一样;如果不Save,那下次上电,加载的是以前保存的数据
}
Store.h
#ifndef __STORE_H
#define __STORE_H
extern uint16_t Store_Data[];
void Store_Init(void);
void Store_Save(void);
void Store_Clear(void);
#endif
main.c
#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(); //OLED初始化
Key_Init(); //按键初始化
Store_Init(); //参数存储模块初始化,在上电的时候将闪存的数据加载回Store_Data,实现掉电不丢失
/*显示静态字符串*/
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] += 2;
Store_Data[3] += 3;
Store_Data[4] += 4;
Store_Save(); //将Store_Data的数据备份保存到闪存,实现掉电不丢失
}
if (KeyNum == 2) //按键2按下
{
Store_Clear(); //将Store_Data的数据全部清0
}
OLED_ShowHexNum(1, 6, Store_Data[0], 4); //显示Store_Data的第一位标志位
OLED_ShowHexNum(3, 1, Store_Data[1], 4); //显示Store_Data的有效存储数据
OLED_ShowHexNum(3, 6, Store_Data[2], 4); //3行6列显示Store_Data[2],长度为4
OLED_ShowHexNum(4, 1, Store_Data[3], 4);
OLED_ShowHexNum(4, 6, Store_Data[4], 4);
}
}
程序现象:
Flag显示标志位:A5A5,这是之前保存的。按下Key2,可以去清0所以参数;按下Key1,可以变换测试数据,之后断定在重新上电,数据保持原样,按复位键,数据也不会丢失。
按下Key2,可以去清0所以参数
按下Key1,可以变换测试数据
前面一部分存储的是程序文件,最后一页存储的是用户数据,目前我们的假设是程序文件比较小,最后一页肯定是没有用到的,所以我们放心的使用最后一页,但是如果程序比较大,触及到了最后一页,那程序和用户数据存储的位置就冲突了,或者说如果你参数非常多,最后10页很大一部分都是留着存储用户数据的,这样如果前面的程序文件长一些,那样非常容易和用户数据冲突,并且这种冲突如果没发现,就会产生非常隐蔽的bug,那如何解决这个问题呢,这时我们可以给程序文件限定一个存储范围,不让它分配到后面我们用户数据的空间来。
这就是编译器,给各个数据分配的空间地址和范围了。如,片上的ROM,起始地址是0800 0000,注意,最高位的0省略了。它的Size:是0x10000,默认全部的64k闪存,都是程序代码分配的空间 ,
如果你想把闪存的尾部空间留着自己用,那可以把这个程序空间的size改小点,比如我们改成0XFC00,这样编译后的代码,无论如何也不会分配到最后一页了。如果Size过小,那编译的时候也会报错。
所以如果你计划把闪存尾部的很多空间都留着自己用,那就把这个程序代码的空间改小点,以免冲突。然后这个下载程序的起始地址也可以改,比如你想写个BOOTLOADER程序放在闪存尾部,那可以在这里修改下载到闪存的起始位置。
最右边这里是片上RAM的起始地址和大小,2000开始大小5000对应就是20K。
这里debug, settings,Flash download,在这里是配置下载选项,其中这个选项我们要选择第二个,擦除扇区,也就是页擦除。
第一个是,每次下载代码都全擦除再下载。
第二个是用到多少页,就擦多少页这个下载速度更快一些,如果你想在闪存尾部存储数据,那也最好选择页擦除的下载,要不然每次下载程序芯片都全擦除了。
程序编译之后,到底占用了多大的空间,我们可以全部编一下,在下面有一行信息就显示了Program Size,程序大小,其中有四个数,这四个数分别是什么意思,感兴趣的话可以网上搜索。
这里只需要记住,前三个数相加,得到的就是程序占用闪存的大小,后两个数相加,得到的是占用SRAM的大小。
这个程序大小,也可以在Target1这里(工程目录最上面)双击,会打开一个.map文件,这就是详细的编译信息。这个.map文件看完就删掉,要不然每次编译后,都会弹出窗口问你是不是要重新加载。
我们看最后面这里也有写程序的大小,并且有计算结果:
倒数第二行,是占用SRAM的大小,这里结果是2664字节,2.6kb,最后一行,是占用闪存的大小,这里是4576字节,4.47kb。
下载到STM32里,验证一下。可以看到,程序在0x0800 11E0地址终止,用计算机转换:16进制 11E0就是10进制的4576。观察的数据和编译的结果相符,没问题。
读取芯片ID
复制OLED的工程。
main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
int main(void)
{
OLED_Init(); //OLED初始化
OLED_ShowString(1, 1, "F_SIZE:"); //显示静态字符串
OLED_ShowHexNum(1, 8, *((__IO uint16_t *)(0x1FFFF7E0)), 4); //使用指针读取指定地址下的闪存容量寄存器
OLED_ShowString(2, 1, "U_ID:"); //显示静态字符串
OLED_ShowHexNum(2, 6, *((__IO uint16_t *)(0x1FFFF7E8)), 4); //使用指针读取指定地址下的产品唯一身份标识寄存器
OLED_ShowHexNum(2, 11, *((__IO uint16_t *)(0x1FFFF7E8 + 0x02)), 4); //第2次读是,基地址+0x02地址偏移
OLED_ShowHexNum(3, 1, *((__IO uint32_t *)(0x1FFFF7E8 + 0x04)), 8);
OLED_ShowHexNum(4, 1, *((__IO uint32_t *)(0x1FFFF7E8 + 0x08)), 8);
while (1)
{
}
}
程序现象:
ID号对不对,可以用软件来验证。
手册