STM32速成笔记(12)—Flash闪存
2024-04-03 来源:elecfans
一、Flash简介
快闪存储器(flash memory),是一种电子式可清除程序化只读存储器的形式,允许在操作中被多次擦或写的存储器。它是一种非易失性存储器,即断电数据也不会丢失。
二、STM32F1的Flash
STM32F103ZET6的Flash大小为512KB,属于大容量产品。在中文参考手册中给出了大容量产品的Flash模块组织结构图
大容量产品Flsh模块组织结构图
• 主存储器 主存储器用来存储我们的代码和定义的一些常量数据。当Boot0和Boot1都接GND时,芯片从主存储器的起始地址0x0800 0000开始运行代码。
• 信息块
系统存储器中存储的是启动程序代码。启动程序就是串口下载的代码。当Boot0接VCC,Boot1接GND时,运行的就是系统存储器中的代码。系统存储器中存储的启动代码,是ST公司在芯片出厂时就已经下载好的,用户无法修改。选择字节是用来配置写保护和杜保护功能。
• 闪存存储器接口寄存器 闪存存储器接口寄存器,是整个闪存的控制机构,里面包含了很多的闪存的控制寄存器和状态寄存器。
在执行闪存写操作时,任何对闪存的读操作都会被锁住。只有对闪存的写操作结束后,读操作才能够正常执行。也就是说,在对闪存进行写操作或者擦除操作时,无法对闪存进行读操作。
三、Flash操作步骤
• 解锁和锁定
• 写/擦除操作
• 获取Flash状态
• 等待操作完成
• 读取Flash指定地址数据
四、程序设计
操作内部Flash时,最小单位是半字(16位)。
44.1 读取数据
读取数据用的是指针的方式,在之前博主的文章中有关于如何利用指针在指定地址读写数据的操作。 ```c /*
*============================================================================== *函数名称:Med_Flash_ReadHalfWord *函数功能:读取指定地址的半字(16位数据) *输入参数:faddr:读取地址 *返回值:对应读取地址数据 *备 注:对内部Flash的操作是以半字为单位,所以读写地址必须是2的倍数 *============================================================================== */ vu16 Med_Flash_ReadHalfWord (u32 faddr) { return (vu16)faddr; }
```c
/*
*==============================================================================
*函数名称:Med_Flash_Read
*函数功能:从指定地址开始读出指定长度的数据
*输入参数:ReadAddr:读取起始地址;pBuffer:数据指针;
NumToRead:读取(半字)数
*返回值:无
*备 注:对内部Flash的操作是以半字为单位,所以读写地址必须是2的倍数
*==============================================================================
*/
void Med_Flash_Read (u32 ReadAddr,u16 *pBuffer,u16 NumToRead)
{
u16 i;
for(i = 0;i < NumToRead;i ++)
{
pBuffer[i] = Med_Flash_ReadHalfWord(ReadAddr); // 读取2个字节.
ReadAddr += 2; // 偏移2个字节.
}
}
4.2 写入数据(不检查)
这里的不检查,是指在写入之前,不检查写入地址是否可写。
/*
*==============================================================================
*函数名称:Med_Flash_Write_NoCheck
*函数功能:不检查的写入
*输入参数:WriteAddr:写入起始地址;pBuffer:数据指针;
NumToWrite:写入(半字)数
*返回值:无
*备 注:对内部Flash的操作是以半字为单位,所以读写地址必须是2的倍数
*==============================================================================
*/
void Med_Flash_Write_NoCheck (u32 WriteAddr,u16 *pBuffer,u16 NumToWrite)
{
u16 i;
for(i = 0;i < NumToWrite;i ++)
{
FLASH_ProgramHalfWord(WriteAddr,pBuffer[i]);
WriteAddr += 2; // 地址增加2.
}
}
4.3 写入数据(检查)
/*
*==============================================================================
*函数名称:Med_Flash_Read
*函数功能:从指定地址开始写入指定长度的数据
*输入参数:WriteAddr:写入起始地址;pBuffer:数据指针;
NumToRead:写入(半字)数
*返回值:无
*备 注:对内部Flash的操作是以半字为单位,所以读写地址必须是2的倍数
*==============================================================================
*/
// 根据中文参考手册,大容量产品的每一页是2K字节
#if STM32_FLASH_SIZE < 256
#define STM32_SECTOR_SIZE 1024 // 字节
#else
#define STM32_SECTOR_SIZE 2048
#endif
// 一个扇区的内存
u16 STM32_FLASH_BUF[STM32_SECTOR_SIZE / 2];
void Med_Flash_Write (u32 WriteAddr,u16 *pBuffer,u16 NumToWrite)
{
u32 secpos; // 扇区地址
u16 secoff; // 扇区内偏移地址(16位字计算)
u16 secremain; // 扇区内剩余地址(16位计算)
u16 i;
u32 offaddr; // 去掉0X08000000后的地址
// 判断写入地址是否在合法范围内
if (WriteAddr < STM32_FLASH_BASE || (WriteAddr >= (STM32_FLASH_BASE + 1024 * STM32_FLASH_SIZE)))
{
return; // 非法地址
}
FLASH_Unlock(); // 解锁
offaddr = WriteAddr - STM32_FLASH_BASE; // 实际偏移地址
secpos = offaddr / STM32_SECTOR_SIZE; // 扇区地址
secoff = (offaddr % STM32_SECTOR_SIZE) / 2; // 在扇区内的偏移(2个字节为基本单位)
secremain = STM32_SECTOR_SIZE / 2 - secoff; // 扇区剩余空间大小
if (NumToWrite <= secremain)
{
secremain = NumToWrite; // 不大于该扇区范围
}
while (1)
{
// 读出整个扇区的内容
Med_Flash_Read(secpos * STM32_SECTOR_SIZE + STM32_FLASH_BASE,STM32_FLASH_BUF,STM32_SECTOR_SIZE / 2);
// 校验数据
for (i = 0;i < secremain;i ++)
{
// 需要擦除
if (STM32_FLASH_BUF[secoff + i] != 0XFFFF)
{
break;
}
}
// 需要擦除
if (i < secremain)
{
FLASH_ErasePage(secpos * STM32_SECTOR_SIZE + STM32_FLASH_BASE); // 擦除这个扇区
// 复制
for (i = 0;i < secremain;i ++)
{
STM32_FLASH_BUF[i + secoff] = pBuffer[i];
}
// 写入整个扇区
Med_Flash_Write_NoCheck(secpos * STM32_SECTOR_SIZE + STM32_FLASH_BASE,STM32_FLASH_BUF,STM32_SECTOR_SIZE / 2);
}
else
{
// 写已经擦除了的,直接写入扇区剩余区间
Med_Flash_Write_NoCheck(WriteAddr,pBuffer,secremain);
}
if (NumToWrite == secremain)
{
break; // 写入结束了
}
// 写入未结束
else
{
secpos ++; // 扇区地址增1
secoff=0; // 偏移位置为0
pBuffer+=secremain; // 指针偏移
WriteAddr+=secremain; // 写地址偏移
NumToWrite-=secremain; // 字节(16位)数递减
if (NumToWrite >(STM32_SECTOR_SIZE/2))
{
secremain=STM32_SECTOR_SIZE/2; // 下一个扇区还是写不完
}
else
{
secremain=NumToWrite; // 下一个扇区可以写完了
}
}
}
FLASH_Lock(); // 上锁
}
宏定义如下
// STM32的Flash容量,单位为KB
#define STM32_FLASH_SIZE 512
// FLASH主存储块起始地址
#define STM32_FLASH_BASE 0x08000000
上面的读取数据和不检查的写入都比较简单,因此并没有再做分析。这里分析一下带检查的写入的程序设计思路。
• 首先用一小段条件编译来区分一下大容量产品和其他产品。因为大容量产品的一页(一个扇区)是2K字节,中小容量产品的一页是1K字节。定一个了一个数组,数组大小是一个扇区的大小。
// 根据中文参考手册,大容量产品的每一页是2K字节
#if STM32_FLASH_SIZE < 256
#define STM32_SECTOR_SIZE 1024 // 字节
#else
#define STM32_SECTOR_SIZE 2048
#endif
// 一个扇区的内存
u16 STM32_FLASH_BUF[STM32_SECTOR_SIZE / 2];
大容量产品,一个扇区2K字节,除以2是因为在对内部Flash操作时,最小单位是半字。
• 接下来,判断要写入的地址是否合法,也就是是否在主存储块地址范围内。
// 判断写入地址是否在合法范围内
if (WriteAddr < STM32_FLASH_BASE || (WriteAddr >= (STM32_FLASH_BASE + 1024 * STM32_FLASH_SIZE)))
{
return; // 非法地址
}
• 如果要写入的地址合法,那么解锁后计算一些参数值。
offaddr = WriteAddr - STM32_FLASH_BASE; // 实际偏移地址
实际偏移地址 ,指的是要写入的地址与主存储块基地址(0x0800 0000)的差值。
secpos = offaddr / STM32_SECTOR_SIZE; // 扇区地址
扇区地址指的是要写入的地址所在扇区前面的扇区数。由于所有的参数都不是浮点型,因此在做除法时,小数位都是0。最终除出来的结果就是当前扇区前面的扇区数。
secoff = (offaddr % STM32_SECTOR_SIZE) / 2; // 在扇区内的偏移(2个字节为基本单位)
在扇区内的偏移指的是要写入的地址与其所在扇区首地址的差值。用要写入的地址取余每一个扇区的字节数,余数就是偏移地址。但是由于操作内部Flash时的最小单位是半字,因此要除以2。
secremain = STM32_SECTOR_SIZE / 2 - secoff; // 扇区剩余空间大小
扇区内剩余空间大小只需要用扇区总的空间大小减去偏移地址即可得到。但是需要注意的是,单位都是半字。这里的剩余空间大小,并不是真正的剩余空间大小。而是指写入地址后面的扇区大小。这里不太好理解,画一个图表示一下。
扇区内剩余空间大小示意图
正是因为这里的扇区剩余空间大小并不是指真正的剩余空间大小。在剩余空间内,也可能存在已经写入数据的地址。所以后面需要进行判断,来确定是否需要擦除。
• 判断在写入地址所在扇区能否将写入内容全部写入完成
if (NumToWrite <= secremain)
{
secremain = NumToWrite; // 不大于该扇区范围
}
如果可以,直接将要写入的半字数赋值给当前扇区剩余空间大小。如果当前扇区剩余空间大小可以容纳要写入的半字数,那么只需要写入一次即可,在后续判断是否写完时,直接通过,while循环只执行一次。
• 读出整个扇区内容,判断是否需要擦除
// 读出整个扇区的内容
Med_Flash_Read(secpos * STM32_SECTOR_SIZE + STM32_FLASH_BASE,STM32_FLASH_BUF,STM32_SECTOR_SIZE / 2);
// 校验数据
for (i = 0;i < secremain;i ++)
{
// 需要擦除
if (STM32_FLASH_BUF[secoff + i] != 0XFFFF)
{
break;
}
}
要对内部Flash某个地址写入数据时,需要确保该地址数值为0xFFFF。判断方法就是从扇区内的偏移开始,利用for循环判断读出地扇区剩余空间内,是否存在已经被写入内容的地址。for循环找到i的值,i加上在扇区内的偏移加1之后的空间,才是真正的扇区剩余空间大小。
for循环结束后,判断是否需要进行擦除
// 需要擦除
if (i < secremain)
{
FLASH_ErasePage(secpos * STM32_SECTOR_SIZE + STM32_FLASH_BASE); // 擦除这个扇区
// 复制
for (i = 0;i < secremain;i ++)
{
STM32_FLASH_BUF[i + secoff] = pBuffer[i];
}
// 写入整个扇区
Med_Flash_Write_NoCheck(secpos * STM32_SECTOR_SIZE + STM32_FLASH_BASE,STM32_FLASH_BUF,STM32_SECTOR_SIZE / 2);
}
else
{
// 写已经擦除了的,直接写入扇区剩余区间
Med_Flash_Write_NoCheck(WriteAddr,pBuffer,secremain);
}
擦除时,最小单元为一个扇区。在大容量产品中,也就是2048字节。
• 最后,将需要写入的数据,写入到对应位置。如果是需要擦除的情况,写入时是先将原来的内容提取出来,然后在后面填充上需要写入的内容,擦除整个扇区之后再一起写入。如果是不需要擦除的情况,直接写入即可。
五、注意事项
在操作Flash时,注意不要对代码区内容进行擦写。如果擦写的地址在代码区,会导致程序运行异常。那么如何确保我们操作的地址不是在代码区?这就需要我们知道我们的代码所占的内存是多少。在Keil5编译完成后,会显示下面的内容
keil5编译后提示
• Code 程序所占用的内存大小(存放在Flash中)
• RO-data 程序定义的常量所占内存大小(存放在Flash中)
• RW-data 已被初始化的全局变量所占内存大小(在程序初始化的时候,RW-data会从FLASH中拷贝到RAM中)
ZI-data 未被初始化的全局变量所占内存大小(存放在RAM中)
最后,计算程序代码所占Flash空间。flash = Code + RO-data + RW-data。