#申请原创# 我是从AVR转过来的,用的是CV编译器。CV编译器对于eeprom变量,只需在变量定义前面加上“eeprom”关键字即可,存储过程编程者是感觉不到的,用着很爽。
32位MCU普遍取消了eeprom,虽然开放了FLASH读写,但FLASH一擦至少擦一页,并且还有擦除寿命问题,擦写解锁操作,显然无法像eeprom那样直接使用。需要调度机制。
小的应用,需要用FLASH保存的数据并不多,想着和CV一样,对编程者透明,不在这上面耗费太多资源。但查阅网上资料,大多是参考stm32的笔记,笔记的例程复杂度太大和工程差不多了吧,还是用库函数写的。后来又看到一篇新唐的笔记--使用Data Flash模拟EEPROM,里面的页面标记是用擦除次数来做的,可以获知当前页面擦过几次,显然比stm32笔记例程中的页面标记更有用。在记录数据方面,stm32笔记以字为单位,eeprom变量的值与虚拟地址构成一个字。新唐的例程则是以块为单位的,可以记录任意长度的数据,显然比stm32复杂很多。结合两者优点,我编了一个简单实用的,不用库,直接操作寄存器,因为用到FLASH寄存器不多,没必要把库带进来。
芯源CW32系列MCU的FLASH设计的很有特点,擦写不需要设置擦写定时参数,一页有512字节,合128字的数据记录空间,不用考虑跨页问题。可以以字节、半字、全字写入,全字写入典型时间为53uS,比eeprom边擦边写不知快了多少倍,页擦除典型时间为4.5mS。感谢芯源,用了这么好用的芯片,没怎么费功夫就把模拟eeprom的程序搞定了,有了这个基础,似乎移植到别的芯片也不是什么难事。
具体实现(看图说话):
1. 模拟的eeprom变量受限于16~24位位宽,在RAM中用了一个数组,用到多少eeprom变量,数组就设多大。
同时用一个枚举变量将名称和数组下标对应,这样引用变量的时候,用枚举名,可以避免出错。
用户对FLASH的存取是对数组的操作,但又不是直接的,数组封装在模拟eeprom程序内。
通过函数来操作:
读取:FEE_rd(枚举名)
写入:FEE_wr(数据, 枚举名)
读取,因为是读RAM数组,所以几乎不耗费时间。
写入的数据先和数组中的数据比较,一致的话就什么也不用做。不一致则不仅数组的数据要更新,而且还要写入FLASH,达到非易失的目的。
写入FLASH是追加式写入,只要有新数据,就追加进去。
一个变量用一个字,高半字是数组下标(eeprom名称,类似于stm32笔记里的虚拟地址),低半字是变量的值。高半字也可只用一个字节,剩下的字节用于变量值,变量值的位宽可以达到24位。
一页有128字,首字用于页面标记,剩下127字合eeprom变量127个次的记录。在一页FLASH中,同名变量由于值的改变,会保存多个,只有最后保存的那个是有效的。
2. 两个页面轮换使用,一个页面用于当前随时保存更新的数据,另一个保持空白,处于备用状态,一旦当前使用的页存满了,就启用备用页,而原先使用的页擦除成空白当作备用。
页面结构不复杂,第一个字用于页面标记,位全是1表示空白备用。写入擦除次数,表示当前页处于启用状态。剩下的空间写数据。
在由备用转启用的过程中,先写入数据,数据写完再写入标记。这样因故中断时,可以判断中断时的状态:如果数据和标记都写了,说明转存过程是完整的。
如果有数据,无标记,说明数据没写完,转存过程失败。
擦除操作,必须有一页是正常启用状态,才可以擦除另一页。如果擦除失败,则会出现两个页面的页首标记和数据都正常,这时擦除次数就派上用场了,次数大的是新开的页,把次数小的页重新擦除即可。
3. 不多说了吧,看程序,备注写的很详细。请忽略变量名及函数名的命名风格,我是小白。
==== eeprom.h ====
#include "jjcw32f003.h"
typedef enum {En0,En1,En2,En3,En4} en_feedata_t; // 变量ID
extern void FEE_init(void);
extern u16 FEE_rd(en_feedata_t);
extern void FEE_wr(u16,en_feedata_t);
==================
==== eeprom.c ====
#include "eeprom.h"
//typedef enum {En0,En1,En2,En3,En4} en_feedata_t; // 变量ID
static u16 FEEdata[5]={1,2,3,4,5}; //变量表原始值,数组标号对应ID
#define FEE_P0ADDR 0x00004400 /* P0首地址 */
#define FEE_WORDS (512/4) /* 字数 */
#define FEE_P1ADDR (FEE_P0ADDR + FEE_WORDS*4) /* P1首地址 */
#define FEE_LOCKS (512*4) /* 锁定区字节数 */
static u32 FEEcounts; // 累计擦除次数(用作启用页页首标记)
static u32 FEEaddr; // 当前页首地址,指向当前启用页
static u16 FEEoffset; // 页指针,指向当前页内可写地址
static void FEEeof(void); // 存满处理
static void FEEput(u32 addr); // 转存处理
static void FEEextr(u32 addr); // 提取数据
static void FEEerase(u32 addr); // 擦除
static void FEEerase_put(u32 addr); // 擦存处理
/***/
u16 FEE_rd(en_feedata_t id) // eeprom读
{
return FEEdata[id]; // 直接读取数组成员
}
/***/
void FEE_wr(u16 data, en_feedata_t id) // eeprom写
{
if(data!=FEEdata[id])
{
u32 addr;
FEEdata[id]=data; // 更新数组成员
addr=FEEaddr+FEEoffset*4; // 写FLASH地址
FLASH->PAGELOCK = 0x5A5A0000|(1<<(addr/FEE_LOCKS)); // 解锁地址所属锁定区
while (FLASH->CR1 & 0x20); // 等待空闲
FLASH->CR1 = 0x5A5A0001; // 编程模式
*((u32 *)addr) = ((u32)id)<<16|data; //id在高半字,值在低半字
while (FLASH->CR1 & 0x20); // 等待完成
FLASH->CR1 = 0x5A5A0000; // 改为只读
FLASH->PAGELOCK = 0x5A5A0000; // 锁定页面
FEEoffset++; // 指向下一个字
FEEeof(); // 存满处理
}
}
/***/
static void FEEput(u32 addr) // 转存
{
FLASH->PAGELOCK = 0x5A5A0000|(1<<(addr/FEE_LOCKS)); // 解锁地址所属锁定区,转存用到的地址必须都在同一个锁定区
while (FLASH->CR1 & 0x20); // 等待空闲
FLASH->CR1 = 0x5A5A0001; // 编程模式
for(FEEoffset=1; FEEoffset<=(sizeof(FEEdata)/sizeof(FEEdata[0])); FEEoffset++) // 数组各成员
{
*((u32 *)(addr+FEEoffset*4)) = ((FEEoffset-1)<<16)|FEEdata[FEEoffset-1]; // id在高半字,值在低半字
while (FLASH->CR1 & 0x20); // 等待完成
}
*((u32 *)addr) = ++FEEcounts; // 页首写累计擦除次数作为启用标记
while (FLASH->CR1 & 0x20); // 等待完成
FLASH->CR1 = 0x5A5A0000; // 改为只读
FLASH->PAGELOCK = 0x5A5A0000; // 重新锁定
FEEaddr=addr; // 作为当前页
}
/***/
static void FEEerase(u32 addr) // 擦除指定页面
{
FLASH->PAGELOCK = 0x5A5A0000|(1<<(addr/FEE_LOCKS)); // 解锁地址所属锁定区
while (FLASH->CR1 & 0x20); // 等待空闲
FLASH->CR1 = 0x5A5A0002; // 页擦除模式
*((u8 *)addr) = 0xff; // 页面上写启动擦除
while (FLASH->CR1 & 0x20); // 等待完成
FLASH->CR1 = 0x5A5A0000; // 改为只读
FLASH->PAGELOCK = 0x5A5A0000; // 重新锁定
}
/***/
static void FEEerase_put(u32 addr) // 擦存
{
FEEerase(addr); //先擦除
FEEput(addr); //再转存
}
/***/
static void FEEextr(u32 addr) // 提取数据
{
u32 data;
FEEaddr=addr; //作为当前页
for(FEEoffset=1; FEEoffset<FEE_WORDS; FEEoffset++) // 页首字是启用标记,从下一个字开始
{
data=*(u32 *)(addr+FEEoffset*4); // 字内容
if(~data) // 非空,有数据
{
FEEdata[data>>16] = data & 0xFFFF;// 替换数组成员值
}
else // 空,数据区结束
{
break;
}
}
}
/***/
static void FEEeof(void) // 存满处理
{
if(FEEoffset>=FEE_WORDS) //指针到达字数
{
u32 temp=FEEaddr; // 当前页
FEEput((temp==FEE_P0ADDR)?(FEE_P1ADDR):(FEE_P0ADDR)); // 转存到另一页
FEEerase(temp); //擦除旧页
}
}
/***/
void FEE_init(void) // 上电初始化
{
u32 temp,temp0,temp1;
SYSCTRL->AHBEN |= 0x02; //开FLASH时钟
temp=0;
temp0 = *(u32 *)FEE_P0ADDR; //P0首字
if(~temp0) temp|=1; // 非空(有0)
if(~(*(u32 *)(FEE_P0ADDR + 4))) temp|=2; //P0数据非空
temp1 = *(u32 *)(FEE_P1ADDR); // P1首字
if(~temp1) temp|=4; // 非空
if(~(*(u32 *)(FEE_P1ADDR + 4))) temp|=8; //P1数据非空
switch (temp) /* 检查两个页面状态 */
{
case 0: case 2: // 0000:P0 P1全空 或 0010:P0初始化失败
FEEcounts=1;
FEEerase_put(FEE_P0ADDR); //擦存P0
break;
case 3: // 0011:P0启用中
FEEcounts=temp0; // 累计擦除次数
FEEextr(FEE_P0ADDR); //提取P0数据
break;
case 8: // 1011:P0>P1转存失败
FEEcounts=temp0;
FEEextr(FEE_P0ADDR); //提取P0数据
FEEerase_put(FEE_P1ADDR); //擦存P1
FEEerase(FEE_P0ADDR); //擦除P0
break;
case 12: // 1100:P1启用中
FEEcounts=temp1;
FEEextr(FEE_P1ADDR); //提取P1数据
break;
case 14: // 1110:P1>P0转存失败
FEEcounts=temp1;
FEEextr(FEE_P1ADDR); //提取P1数据
FEEerase_put(FEE_P0ADDR); //擦存P0
FEEerase(FEE_P1ADDR); //擦除P1
break;
case 15: // 1111:擦除失败
if(temp0>temp1) // 标记数大的作为启用页
{
FEEcounts=temp0;
FEEextr(FEE_P0ADDR); //提取P0数据
FEEerase(FEE_P1ADDR); //擦除P1
}
else
{
FEEcounts=temp1;
FEEextr(FEE_P1ADDR); //提取P1数据
FEEerase(FEE_P0ADDR); //擦除P0
}
break;
} /* 检查页面状态end */
FEEeof(); // 满页处理
}
/***/
/*******************/
|