背景
flash为嵌入式设备中常见的存储器,优点:便宜,容量大,但缺点也比较明显,最大的缺点是寿命问题,flash编程只能将bit由1位置0,不能将0位置1,将0置1只能擦除扇区,而扇区往往比编程单位要大很多,哪怕我们只对对一个地址写两个字节的数据,也需要擦除整个扇区来完成数据更新,频繁擦写导致flash坏块。
本人这边做的一个小玩意里面需要存储一些掉电保存的数据,但修改频次又有点多,硬件上没有掉电维持电路,只能尽可能减少flash扇区擦写次数了。
磨损均衡原理
说人话就是,将一个大数据扇区差分城多个数据帧,轮流写操作数据帧,当所有的数据帧都写过了再擦除重新来过。这样本来我们每次修改都需要擦出扇区,优化为现在写满一个扇区再擦除,寿命得到了成倍的提升。这里是对flash单一扇区里的数据帧磨损均衡,同样可以对多个扇区进行扇区磨损均衡,同理。
磨损均衡实现
定长数据读写
本人需要存储的数据一个定长浮点型数组,只针对本人项目的需要,实现起来也相对来说简单些。
例如一个flash 扇区 2k,我们存储一个short[14]的数组。
我们将数据帧格式定义为 |帧头|data[28]||unused[2]|帧尾|
帧头 0xA5 为 数据帧有效,0XFF代表未使用,其他则代表数据帧废弃。扇区中只存在一个有效的数据帧。
读取:从扇区中遍历数据块,找到帧头0XA5的数据帧并检验数据返回
写入:从扇区中遍历数据块,找到帧头0XA5的数据帧置0(本人亲测STM32F103内部flash是可写入,可能有的平台不可以,未使用数据可拿一位做擦除标记位,),找到0XFF的帧头,若未找到则擦除扇区写入。
源码如下
#define SECTOR_SIZE 2048
#define FRAME_SIZE 32 //枕头帧尾 14*U16 + 2*U8
typedef struct {
const u32 addr; //起始地址
u32 currAddr; //起始地址
u16 count; //当前读写块
u8 buff[FRAME_SIZE]; //数据
} FLASH_LEVELINGType;
FLASH_LEVELINGType flashLeveling = {
.addr = 0x0803F800,
.currAddr= 0x0803F800,
.count= 0,
.buff = {0},
};
void FlashLeveingWrite(u8 * data)
{
FLASH_Unlock(); //解锁
FlashNewFrame( );
flashLeveling.buff[0] = 0xa5;
flashLeveling.buff[FRAME_SIZE-1] = 0x5a;
memcpy(flashLeveling.buff+1,data,FRAME_SIZE-2);
FLASH_WaitForLastOperation(FLASH_WAITETIME); //等待上次操作完成
STMFLASH_Write_NoCheck(flashLeveling.currAddr,(u16 *)flashLeveling.buff,FRAME_SIZE/2);
DBG_DEBUG("0x%x %d写完%x\r\n",flashLeveling.currAddr,flashLeveling.count,flashLeveling.buff[0]);
FLASH_Lock(); //上锁
}
u8* FlashLeveingRead()
{
FlashActiveFrame();
return flashLeveling.buff;
}
void FlashNewFrame()
{
u16 i = 0;
while(i <= SECTOR_SIZE/FRAME_SIZE)
{
flashLeveling.currAddr = flashLeveling.addr +i* FRAME_SIZE;
FLASH_Read(flashLeveling.buff,flashLeveling.currAddr, FRAME_SIZE);
if( flashLeveling.buff[0] == 0xFF && flashLeveling.buff[FRAME_SIZE-1] == 0xFF)
{
DBG_DEBUG("%d写\r\n",i);
flashLeveling.count = i;
return;
}else if( flashLeveling.buff[0] == 0xa5 && flashLeveling.buff[FRAME_SIZE-1] == 0x5a)
{
memset(flashLeveling.buff,0,FRAME_SIZE);
FLASH_WaitForLastOperation(FLASH_WAITETIME); //等待上次操作完成
STMFLASH_Write_NoCheck(flashLeveling.currAddr,(u16 *)flashLeveling.buff,FRAME_SIZE/2);//清0
}
i++;
}
if(i >SECTOR_SIZE/FRAME_SIZE )
{
FLASH_WaitForLastOperation(FLASH_WAITETIME); //等待上次操作完成
DBG_DEBUG("擦%d\r\n",FLASH_ErasePage(flashLeveling.addr));
CLEAR_BIT(FLASH->CR, FLASH_CR_PER); /
flashLeveling.count = 0;
flashLeveling.currAddr = flashLeveling.addr;
}
}
void FlashActiveFrame()
{
u16 i = 0;
while(i <= SECTOR_SIZE/FRAME_SIZE)
{
flashLeveling.currAddr = flashLeveling.addr +i* FRAME_SIZE;
FLASH_Read(flashLeveling.buff,flashLeveling.currAddr, FRAME_SIZE);
if( flashLeveling.buff[0] == 0xa5 && flashLeveling.buff[FRAME_SIZE-1] == 0x5a)
{
flashLeveling.count = i;
return;
}
i++;
}
if(i >SECTOR_SIZE/FRAME_SIZE ) //没存数据
{
flashLeveling.count = 0xffFF;//标示未使用一个数据块
flashLeveling.buff[0] = 0xff;
}
}
不定长数据读写
只需要修改一下数据帧格式就好,数据帧可做类链表串联,留一个字节值向下一个数据帧序号。
|帧头|data[28]||nextFrame|帧尾|
代码有空再补。
多个不定长数据存储读写
上面的其实都是对单一数据存储,一个时间点中只有一个有效数据在读写,而多个不定长数据存储,则某种程度上已经类似文件管理。
这样扇区应当分成两个区,一个作为 Frame Allocation Table(数据帧分配表),另一个作为数据区
Frame Allocation Table(数据帧分配表) 子项结构
以2048字节一个扇区为例 ,前64个字节作为FAT表,后面的为数据区,32字节为一数据帧。
FAT表子项结构定义:u16 :0x00 擦除帧,0xf7 数据帧被使用且是首帧, 0xf3 数据帧被使用非头帧
FRAME 结构:
|帧头|data[28]||nextFrame|帧尾|
这样可以根据FAT表获取扇区中总共有多少个数据被存储,至于区分这些数据哪一个数自己想要的,可以在frame结构着手。
例如 数据首帧,最开始存储的字符串为数据名,根据数据名获取对应的数据。
例如 saveDate = data1+ data2+ data3;
根据以上思路,应该是可以实现的。
代码有空再补。
总结
只做抛砖引玉。