打印
[CW32F003系列]

cw32系列mcu简单实用的flash模拟eeprom程序

[复制链接]
758|25
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
zhanan|  楼主 | 2023-2-7 11:26 | 只看该作者 |只看大图 回帖奖励 |倒序浏览 |阅读模式
#申请原创# 我是从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(); // 满页处理
}
/***/
/*******************/







使用特权

评论回复
沙发
zhanan|  楼主 | 2023-2-7 12:03 | 只看该作者
最后三个图片有点瑕次,FEEoffset每次都指向当前页面可写地址,图中误指到最后写入的地址了。
程序中前空格没对齐,是复制粘贴造成的,非有意。

使用特权

评论回复
板凳
zhanan|  楼主 | 2023-2-8 10:44 | 只看该作者

满页换页时序,第一个脉冲是写页尾最后一个字,因为满页了,切换到另一页,这是第二个脉冲。 5个eeprom变量+1个页面标记共写6个字。

切换完成后,擦除,第三个脉冲,耗费的时间要多一些。


使用特权

评论回复
地板
lw082273| | 2023-2-28 21:24 | 只看该作者
可以发个工程参考下吗

使用特权

评论回复
5
zhanan|  楼主 | 2023-3-1 15:08 | 只看该作者
lw082273 发表于 2023-2-28 21:24
可以发个工程参考下吗

只有一个文件 eeprom.c ,添加到你的项目中即可,运用时有几个地方要修改一下:
1. 定义 eeprom 变量
   eeprom 变量是用数组 u16 FEEdata[] 来组织的,成员就是 eeprom 变量,有几个eeprom变量,数组就设多大。
   引入枚举来当数组下标,枚举名就是eeprom变量名。枚举类型要放到头文件里去,好让外部程序知道。

   枚举:typedef enum {En0,En1,En2,En3,En4} en_feedata_t;
   数组:u16 FEEdata[]={100,200,120,60,30};
         数组成员的值也是eeprom变量的初始值(出厂值)

2. 指定所用到的页面地址
       #define FEE_P0ADDR  0x0000FC00  /* P0首地址 */
       #define FEE_LOCKS  (512*4) /* 解锁块字节数 */
       页面必须都在同一个PAGELOCK锁定区,使用前必须都是0xFF。

3. void FEE_init(void) 是FLASH初始化的程序,负责找到最后保存的数据,建立起状态,必须放到主程序 main 开始的地方。

4.寄存器名称可能需要改一下,官方的头文件寄存器定义是 CW_+寄存器,如 FLASH->PAGELOCK、CW_FLASH->PAGELOCK


使用:
        读操作:FEE_rd ( 枚举名 );

        写操作:FEE_wr ( 值, 枚举名 );

使用特权

评论回复
6
albertaabbot| | 2023-3-10 17:15 | 只看该作者
这个直接写入flash实现的吗              

使用特权

评论回复
7
lzbf| | 2023-3-10 17:35 | 只看该作者
为什么要用flash模拟eeprom?

使用特权

评论回复
8
uytyu| | 2023-3-10 17:48 | 只看该作者
Flash和EEPROM主要差别是FLASH按块/扇区进行读写操作,且写之前需擦除原有数据,EEPROM支持按字节读写操作。

使用特权

评论回复
9
wwppd| | 2023-3-10 17:57 | 只看该作者
模拟eeprom好难啊。              

使用特权

评论回复
10
wilhelmina2| | 2023-3-10 18:09 | 只看该作者
flash模拟eeprom稳定吗   

使用特权

评论回复
11
sdCAD| | 2023-3-10 18:15 | 只看该作者
https://gitee.com/epoko/eeprom_in_flash  

使用特权

评论回复
12
geraldbetty| | 2023-3-10 18:25 | 只看该作者
灵活性极强的flash模拟eeprom

使用特权

评论回复
13
zhanan|  楼主 | 2023-3-11 14:48 | 只看该作者
sdCAD 发表于 2023-3-10 18:15
https://gitee.com/epoko/eeprom_in_flash

简单看了下,从写的机制上看似乎不完美:
1. 往FLASH写入是从页末反向搜索到可用地址,
2. 如果页末有数据,表示这个页用完了,去擦另一页,写入。然后擦除本页。

对于1,用一个变量始终指向可写地址,就不用每次都搜索了。
对于2,在将要擦除另一页时发生断电,本次数据将丢失。如果断电发生在擦除本页前,重新上电后这个两个页面都有数据,0页优先? 0页和1页,哪个是上次的,哪个是上上次的,没看到确认过程。

使用特权

评论回复
14
eefas| | 2023-3-11 19:28 | 只看该作者
简易的flash模拟eeprom功能  

使用特权

评论回复
15
hearstnorman323| | 2023-3-11 19:35 | 只看该作者
如何使用片上flash来模拟EEPROM

使用特权

评论回复
16
sheflynn| | 2023-3-11 20:00 | 只看该作者
需要擦除一页的数据吗              

使用特权

评论回复
17
phoenixwhite| | 2023-3-11 20:06 | 只看该作者
还不如外界EEPROM了。              

使用特权

评论回复
18
mollylawrence| | 2023-3-11 20:13 | 只看该作者
把Flash部分扇区当作EEPROM使用??

使用特权

评论回复
19
kkzz| | 2023-3-11 20:27 | 只看该作者
这个写入的速度快吗?              

使用特权

评论回复
20
zhanan|  楼主 | 2023-3-11 20:33 | 只看该作者
sheflynn 发表于 2023-3-11 20:00
需要擦除一页的数据吗

是的,这是FLASH的特性,一擦擦一页。写可以按字或字节写。

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

9

主题

173

帖子

0

粉丝