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

[复制链接]
3546|26
 楼主| 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的程序搞定了,有了这个基础,似乎移植到别的芯片也不是什么难事。

具体实现(看图说话):
模拟eeprom.png
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. 不多说了吧,看程序,备注写的很详细。请忽略变量名及函数名的命名风格,我是小白。


  1. ==== eeprom.h ====
  2. #include "jjcw32f003.h"
  3. typedef enum {En0,En1,En2,En3,En4} en_feedata_t; // 变量ID

  4. extern void FEE_init(void);
  5. extern u16 FEE_rd(en_feedata_t);
  6. extern void FEE_wr(u16,en_feedata_t);

  7. ==================
  8. ==== eeprom.c ====
  9. #include "eeprom.h"

  10. //typedef enum {En0,En1,En2,En3,En4} en_feedata_t; // 变量ID
  11. static u16 FEEdata[5]={1,2,3,4,5}; //变量表原始值,数组标号对应ID

  12. #define FEE_P0ADDR  0x00004400  /* P0首地址 */
  13. #define FEE_WORDS  (512/4)  /* 字数 */
  14. #define FEE_P1ADDR (FEE_P0ADDR + FEE_WORDS*4) /* P1首地址 */
  15. #define FEE_LOCKS  (512*4) /* 锁定区字节数 */

  16. static u32 FEEcounts; // 累计擦除次数(用作启用页页首标记)
  17. static u32 FEEaddr; // 当前页首地址,指向当前启用页
  18. static u16 FEEoffset; // 页指针,指向当前页内可写地址

  19. static void FEEeof(void); // 存满处理
  20. static void FEEput(u32 addr); // 转存处理
  21. static void FEEextr(u32 addr); // 提取数据
  22. static void FEEerase(u32 addr); // 擦除
  23. static void FEEerase_put(u32 addr); // 擦存处理
  24. /***/
  25. u16 FEE_rd(en_feedata_t id) // eeprom读
  26. {
  27.   return FEEdata[id]; // 直接读取数组成员
  28. }
  29. /***/
  30. void FEE_wr(u16 data, en_feedata_t id) // eeprom写
  31. {
  32.   if(data!=FEEdata[id])
  33.   {
  34.     u32 addr;
  35.     FEEdata[id]=data; // 更新数组成员

  36.     addr=FEEaddr+FEEoffset*4; // 写FLASH地址
  37.     FLASH->PAGELOCK = 0x5A5A0000|(1<<(addr/FEE_LOCKS)); // 解锁地址所属锁定区
  38.     while (FLASH->CR1 & 0x20); // 等待空闲
  39.     FLASH->CR1 = 0x5A5A0001; // 编程模式
  40.     *((u32 *)addr) = ((u32)id)<<16|data; //id在高半字,值在低半字
  41.     while (FLASH->CR1 & 0x20); // 等待完成
  42.     FLASH->CR1 = 0x5A5A0000; // 改为只读
  43.     FLASH->PAGELOCK = 0x5A5A0000; // 锁定页面
  44.     FEEoffset++; // 指向下一个字
  45.     FEEeof(); // 存满处理
  46.   }
  47. }
  48. /***/
  49. static void FEEput(u32 addr) // 转存
  50. {
  51.   FLASH->PAGELOCK = 0x5A5A0000|(1<<(addr/FEE_LOCKS)); // 解锁地址所属锁定区,转存用到的地址必须都在同一个锁定区
  52.   while (FLASH->CR1 & 0x20); // 等待空闲
  53.   FLASH->CR1 = 0x5A5A0001; // 编程模式
  54.   for(FEEoffset=1; FEEoffset<=(sizeof(FEEdata)/sizeof(FEEdata[0])); FEEoffset++) // 数组各成员
  55.   {
  56.     *((u32 *)(addr+FEEoffset*4)) = ((FEEoffset-1)<<16)|FEEdata[FEEoffset-1]; // id在高半字,值在低半字
  57.     while (FLASH->CR1 & 0x20); // 等待完成
  58.   }
  59.   *((u32 *)addr) = ++FEEcounts; // 页首写累计擦除次数作为启用标记
  60.   while (FLASH->CR1 & 0x20); // 等待完成
  61.   FLASH->CR1 = 0x5A5A0000; // 改为只读
  62.   FLASH->PAGELOCK = 0x5A5A0000; // 重新锁定
  63.   FEEaddr=addr; // 作为当前页
  64. }
  65. /***/
  66. static void FEEerase(u32 addr) // 擦除指定页面
  67. {
  68.   FLASH->PAGELOCK = 0x5A5A0000|(1<<(addr/FEE_LOCKS)); // 解锁地址所属锁定区
  69.   while (FLASH->CR1 & 0x20); // 等待空闲
  70.   FLASH->CR1 = 0x5A5A0002; // 页擦除模式
  71.   *((u8 *)addr) = 0xff; // 页面上写启动擦除
  72.   while (FLASH->CR1 & 0x20); // 等待完成
  73.   FLASH->CR1 = 0x5A5A0000; // 改为只读
  74.   FLASH->PAGELOCK = 0x5A5A0000; // 重新锁定
  75. }
  76. /***/
  77. static void FEEerase_put(u32 addr) // 擦存
  78. {       
  79.   FEEerase(addr); //先擦除
  80.   FEEput(addr); //再转存
  81. }
  82. /***/
  83. static void FEEextr(u32 addr) // 提取数据
  84. {
  85.   u32 data;
  86.   FEEaddr=addr; //作为当前页
  87.   for(FEEoffset=1; FEEoffset<FEE_WORDS; FEEoffset++) // 页首字是启用标记,从下一个字开始
  88.   {
  89.     data=*(u32 *)(addr+FEEoffset*4); // 字内容
  90.     if(~data) // 非空,有数据
  91.     {
  92.       FEEdata[data>>16] = data & 0xFFFF;// 替换数组成员值
  93.     }
  94.     else // 空,数据区结束
  95.     {
  96.       break;
  97.     }
  98.   }               
  99. }
  100. /***/
  101. static void FEEeof(void) // 存满处理
  102. {
  103.   if(FEEoffset>=FEE_WORDS) //指针到达字数
  104.   {
  105.     u32 temp=FEEaddr; // 当前页
  106.     FEEput((temp==FEE_P0ADDR)?(FEE_P1ADDR):(FEE_P0ADDR)); // 转存到另一页
  107.     FEEerase(temp); //擦除旧页               
  108.   }
  109. }
  110. /***/
  111. void FEE_init(void) // 上电初始化
  112. {
  113.   u32 temp,temp0,temp1;
  114.   
  115.   SYSCTRL->AHBEN |= 0x02; //开FLASH时钟
  116.   
  117.   temp=0;
  118.   temp0 = *(u32 *)FEE_P0ADDR; //P0首字
  119.   if(~temp0) temp|=1; // 非空(有0)
  120.   if(~(*(u32 *)(FEE_P0ADDR + 4))) temp|=2; //P0数据非空
  121.   temp1 = *(u32 *)(FEE_P1ADDR); // P1首字
  122.   if(~temp1) temp|=4; // 非空
  123.   if(~(*(u32 *)(FEE_P1ADDR + 4))) temp|=8; //P1数据非空
  124.   switch (temp) /* 检查两个页面状态 */
  125.   {
  126.     case 0: case 2: // 0000:P0 P1全空 或 0010:P0初始化失败
  127.       FEEcounts=1;
  128.             FEEerase_put(FEE_P0ADDR); //擦存P0
  129.       break;
  130.     case 3: // 0011:P0启用中
  131.             FEEcounts=temp0; // 累计擦除次数
  132.             FEEextr(FEE_P0ADDR); //提取P0数据
  133.       break;
  134.     case 8: // 1011:P0>P1转存失败
  135.       FEEcounts=temp0;
  136.       FEEextr(FEE_P0ADDR); //提取P0数据
  137.             FEEerase_put(FEE_P1ADDR); //擦存P1
  138.       FEEerase(FEE_P0ADDR); //擦除P0
  139.             break;
  140.     case 12: // 1100:P1启用中
  141.       FEEcounts=temp1;
  142.       FEEextr(FEE_P1ADDR); //提取P1数据
  143.       break;
  144.     case 14: // 1110:P1>P0转存失败
  145.       FEEcounts=temp1;
  146.       FEEextr(FEE_P1ADDR); //提取P1数据
  147.             FEEerase_put(FEE_P0ADDR); //擦存P0
  148.       FEEerase(FEE_P1ADDR); //擦除P1
  149.             break;
  150.     case 15: // 1111:擦除失败
  151.       if(temp0>temp1) // 标记数大的作为启用页
  152.       {
  153.         FEEcounts=temp0;
  154.         FEEextr(FEE_P0ADDR); //提取P0数据
  155.         FEEerase(FEE_P1ADDR); //擦除P1
  156.       }
  157.       else
  158.       {
  159.         FEEcounts=temp1;
  160.         FEEextr(FEE_P1ADDR); //提取P1数据
  161.         FEEerase(FEE_P0ADDR); //擦除P0
  162.       }
  163.       break;
  164.   } /* 检查页面状态end */
  165.   FEEeof(); // 满页处理
  166. }
  167. /***/
  168. /*******************/
上电初始化.png

使用中.png

存满切换.png


 楼主| zhanan 发表于 2023-2-7 12:03 | 显示全部楼层
最后三个图片有点瑕次,FEEoffset每次都指向当前页面可写地址,图中误指到最后写入的地址了。
程序中前空格没对齐,是复制粘贴造成的,非有意。
 楼主| zhanan 发表于 2023-2-8 10:44 | 显示全部楼层

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

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

cw32f003模拟eeprom时序.png
lw082273 发表于 2023-2-28 21:24 | 显示全部楼层
可以发个工程参考下吗
 楼主| 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 ( 值, 枚举名 );

albertaabbot 发表于 2023-3-10 17:15 | 显示全部楼层
这个直接写入flash实现的吗              
lzbf 发表于 2023-3-10 17:35 | 显示全部楼层
为什么要用flash模拟eeprom?
uytyu 发表于 2023-3-10 17:48 | 显示全部楼层
Flash和EEPROM主要差别是FLASH按块/扇区进行读写操作,且写之前需擦除原有数据,EEPROM支持按字节读写操作。
wwppd 发表于 2023-3-10 17:57 | 显示全部楼层
模拟eeprom好难啊。              
wilhelmina2 发表于 2023-3-10 18:09 | 显示全部楼层
flash模拟eeprom稳定吗   
sdCAD 发表于 2023-3-10 18:15 | 显示全部楼层
https://gitee.com/epoko/eeprom_in_flash  
geraldbetty 发表于 2023-3-10 18:25 | 显示全部楼层
灵活性极强的flash模拟eeprom
 楼主| 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页,哪个是上次的,哪个是上上次的,没看到确认过程。
eefas 发表于 2023-3-11 19:28 | 显示全部楼层
简易的flash模拟eeprom功能  
hearstnorman323 发表于 2023-3-11 19:35 | 显示全部楼层
如何使用片上flash来模拟EEPROM
sheflynn 发表于 2023-3-11 20:00 | 显示全部楼层
需要擦除一页的数据吗              
phoenixwhite 发表于 2023-3-11 20:06 | 显示全部楼层
还不如外界EEPROM了。              
mollylawrence 发表于 2023-3-11 20:13 | 显示全部楼层
把Flash部分扇区当作EEPROM使用??
kkzz 发表于 2023-3-11 20:27 | 显示全部楼层
这个写入的速度快吗?              
 楼主| zhanan 发表于 2023-3-11 20:33 | 显示全部楼层
sheflynn 发表于 2023-3-11 20:00
需要擦除一页的数据吗

是的,这是FLASH的特性,一擦擦一页。写可以按字或字节写。
您需要登录后才可以回帖 登录 | 注册

本版积分规则

11

主题

195

帖子

0

粉丝
快速回复 在线客服 返回列表 返回顶部