#申请原创# 我是从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(); // 满页处理
- }
- /***/
- /*******************/
|