• 注册 / 登录
  • 切换到窄版
  • 查看: 1846|回复: 0

    用小华MCU实现简单实用的FLASH模拟EEPROM程序

    [复制链接]

    665

    主题

    679

    帖子

    6481

    积分

    版主

    Rank: 7Rank: 7Rank: 7

    积分
    6481
    发表于 2023-3-9 15:40:49 | 显示全部楼层 |阅读模式

    路线栈欢迎您!

    您需要 登录 才可以下载或查看,没有帐号?立即注册

    x
    前言

    提出用FLASH模拟EEPROM的方案,对于保存少量的eeprom变量,速度快,占用资源少。用手里有一块HC32LFx3x-STK-V2.0开发板,芯片是HC32L136,试着将那个方案移植过来。HC32L136能用,HC32的L和F系列几乎拿来主义,因为HC32系列mcu的FLASH资源几乎是一样的。

    HC32操作FLASH

    其一,HC32需要设置FLASH擦写定时参数。

    其二,解锁FLASH写操作是一个写3个字的序列,中间不能插入别的写操作。以下是手册第330页中的描述:

    “注意:写  0x5a5a、0xa5a5、写目标寄存器,这三步写操作之间不可插入任何写操作(写ROM、RAM、REG) ,否则无法改写目标寄存器的数值。如改写失败,需要重新进行这三步操作。 ”

    其他的就一样了,一页有512字节,合128字,一个eeprom变量占用一个字,一页有127个次的eeprom变量保存空间,用两页轮换写,少量eeprom变量情况下,擦写寿命不低于eeprom吧,写速度比真的eeprom存储器更有优势,因为eeprom是边擦边写,擦的时间决定了快不了。

    可以按字、半字、字节写,本模拟程序只用到按字写,按字写时间52uS,按字写是常规操作。换页时有转存和页擦除操作,页擦除时间5mS,变量如果比较多,转存和擦除时间要长一些,但换页不是经常发生。

    页面擦写保护位设置也一样。但是在试验中发现一个问题:擦写保护位寄存器 SLOCK,手册里写的复位值是0,禁止擦写的意思,但芯片复位后读出来的却是全1,1是不保护,解锁的意思,这是什么情况呢?

    1.png

    对于定时参数,厂家给了一个基准时间,是在 HCLK=4MHz 的条件下,如果 HCLK 不一样就按频率乘数xFreq调整,并且在用到FLASH擦写时最好在几个推荐的频率上选择:
    4M:xFreq=1,8M:xFreq=2,16M:xFreq=4,24M:xFreq=6,32M:xFreq=8
    1. enum en_flash_prgtimer /* HCLK=4M 基准定时参数 */
    2.   {
    3.     Tnvs      = 32u,    /* 0x20 */
    4.     Tpgs      = 23u,    /* 0x17 */
    5.     Tprog     = 27u,    /* 0x1B */
    6.     Tserase   = 18000u, /* 0x4650 */
    7.     Tmerase   = 140000u,/* 0x222E0 */
    8.     Tprcv     = 24u,    /* 0x18 */
    9.     Tsrcv     = 240u,   /* 0xF0 */
    10.     Tmrcv     = 1000u,  /* 0x3E8 */
    11.   };
    复制代码

    对于解锁序列,只有中断会突然插入,由于FLASH擦写本身也是要停止CPU运行,中断也不会很好的处理,所以FLASH擦写期间干脆关闭总中断。对于不能关中断、对中断敏感的程序,在其运行时,可以不进行FLASH的擦写操作。

    主程序只有一个文件 eeprom.c ,添加到项目中,运用时有几个地方要改一下:

    1. 定义 eeprom 变量

    • eeprom 变量是用数组 u16 FEEdata[] 来组织的,成员就是 eeprom 变量,有几个eeprom变量,数组就设多大。
    • 这个方案限制了位宽只有16位,16位可以满足大部分的应用,可以扩展位宽,eeprom变量个数不到15个时,仅占用4位,剩余28位给变量的值用。
    • 数组不对外,免遭破坏,或引起混乱,数据访问是通过对外函数进行的。
    • 数组毕竟是用数字下标来访问,变量名称转变为数组下标还要烧脑,不如引入枚举来当下标,枚举名就是eeprom变量名。枚举类型要放到头文件里去,好让外部程序知道。

       枚举:typedef enum {En1,En2,En3,En4,En5,En6,En7,En8,En9} en_feedata_t;
       数组:u16 FEEdata[]={100,200,120,60,30,120,120,100,50};
             数组成员值就是eeprom变量的初始值(出厂值)
    然后,在程序的帮助下,对 eeprom 变量的操作就非常简单了:

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

    2. 指定所用到的页面地址

    1. #define FEE_P0ADDR  0x0000FC00  /* P0首地址 */
    2. #define FEE_LOCKS  (512*4) /* 解锁块字节数 */
    复制代码

    P0和P1用连续的两个页面即可,这样只需对 FEE_P0ADDR 指定即可,并且这两个页面都在一个 SLOCK 位保护区内。FLASH容量小的芯片,一个保护位保护4页(2048字节),而容量大的,一个保护位保护8页(4096字节),FEE_LOCKS要改一下。

    如果嫌一页128字不够用,用到2页,那 FEE_WORDS 和程序方面就都要改一下,主要是需要连续擦除2页,不是此文重点。

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

    由于没用库,程序又不复杂,有问题很容易查。其他的看程序吧。程序状态图在这里再贴一下,方便看程序。

    2.png

    1. ===== eeprom.h ====
    2. #include "j_hc32l13x.h"
    3. typedef enum {En1,En2,En3,En4,En5,En6,En7,En8,En9} 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 {En1,En2,En3,En4,En5,En6,En7,En8,En9} en_eepdata_t; // 这个是头文件里的枚举类型备注,以便和值对应
    11. static u16 FEEdata[]={100,200,120,60,30,120,120,100,50}; //变量表原始值,数组标号对应ID

    12.   /*** flash擦写定时参数,基于HCLK ***/
    13.   /*频率乘数,基于HCLK:1-4M,2-8M,4-16M,6-24M,8-32M(须插入FLASH等待周期) */
    14.   #define xFreq  2  /* 当前频率乘数 */
    15.   /***/
    16.   enum en_flash_prgtimer /* HCLK=4M 基准定时参数 */
    17.   {
    18.     Tnvs      = 32u,    /* 0x20 */
    19.     Tpgs      = 23u,    /* 0x17 */
    20.     Tprog     = 27u,    /* 0x1B */
    21.     Tserase   = 18000u, /* 0x4650 */
    22.     Tmerase   = 140000u,/* 0x222E0 */
    23.     Tprcv     = 24u,    /* 0x18 */
    24.     Tsrcv     = 240u,   /* 0xF0 */
    25.     Tmrcv     = 1000u,  /* 0x3E8 */
    26.   };
    27.   #define FlashUnlock() FLASH->BYPASS = 0x5A5A; FLASH->BYPASS = 0xA5A5 /* FLASH解锁命令 */
    28.   #define Irq_Disable asm("CPSID i") /* 禁止总中断 */
    29.   #define Irq_Enable  asm("CPSIE i") /* 开放总中断 */
    30.   /* --------- */

    31. #define FEE_P0ADDR  0x0000FC00  /* P0首地址 */
    32. #define FEE_WORDS  (512/4) /* 字数 = 页字节数/4 */
    33. #define FEE_P1ADDR (FEE_P0ADDR + FEE_WORDS*4) /* P1首地址 */
    34. #define FEE_LOCKS  (512*4) /* 解锁块字节数 */

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

    38. static void FEEeof(void); // 存满处理
    39. static void FEEput(u32 addr); // 转存处理
    40. static void FEEextr(u32 addr); // 提取数据
    41. static void FEEerase(u32 addr); // 擦除
    42. static void FEEerase_put(u32 addr); // 擦存处理
    43. /***/
    44. u16 FEE_rd(en_feedata_t id) // eeprom读
    45. {
    46.   return FEEdata[id]; // 直接读取数组成员
    47. }
    48. /***/
    49. void FEE_wr(u16 data, en_feedata_t id) // eeprom写
    50. {
    51.   if(data!=FEEdata[id])
    52.   {
    53.     u32 addr;
    54.     FEEdata[id]=data; // 更新数组成员
    55.    
    56.     Irq_Disable; // 禁止中断
    57.     addr=FEEaddr+FEEoffset*4; // 写FLASH地址
    58.     FlashUnlock(); FLASH->SLOCK = (1<<(addr/FEE_LOCKS)); // 解锁地址所属锁定区
    59.     while (FLASH->CR & 0x10); // 等待空闲
    60.     FlashUnlock(); FLASH->CR_f.OP = 0x1; // 编程模式
    61.     *((u32 *)addr) = ((u32)id)<<16|data; //id在高半字,值在低半字
    62.     while (FLASH->CR & 0x10); // 等待完成
    63.     FlashUnlock(); FLASH->CR_f.OP = 0; // 改为只读
    64.     FlashUnlock(); FLASH->SLOCK = 0; // 锁定页面
    65.     Irq_Enable; // 开放中断
    66.     FEEoffset++; // 指向下一个字
    67.     FEEeof(); // 存满处理
    68.   }
    69. }
    70. /***/
    71. static void FEEput(u32 addr) // 转存
    72. {
    73.   FEEaddr=addr; // 作为当前页
    74.   Irq_Disable; // 禁止中断
    75.   FlashUnlock(); FLASH->SLOCK = (1<<(addr/FEE_LOCKS)); // 解锁地址所属锁定区,转存用到的地址必须都在同一个锁定区
    76.   while (FLASH->CR & 0x10); // 等待空闲
    77.   FlashUnlock(); FLASH->CR_f.OP = 0x1; // 编程模式
    78.   for(FEEoffset=1; FEEoffset<=(sizeof(FEEdata)/sizeof(FEEdata[0])); FEEoffset++) // 数组各成员
    79.   {
    80.     *((u32 *)(addr+FEEoffset*4)) = ((FEEoffset-1)<<16)|FEEdata[FEEoffset-1]; // id在高半字,值在低半字
    81.     while (FLASH->CR & 0x10); // 等待完成
    82.   }
    83.   *((u32 *)addr) = ++FEEcounts; // 页首写累计擦除次数作为启用标记
    84.   while (FLASH->CR & 0x10); // 等待完成
    85.   FlashUnlock(); FLASH->CR_f.OP = 0; // 改为只读
    86.   FlashUnlock(); FLASH->SLOCK = 0; // 重新锁定
    87.   Irq_Enable; // 开放中断
    88. }
    89. /***/
    90. static void FEEerase(u32 addr) // 擦除指定页面
    91. {
    92.   Irq_Disable; // 禁止中断
    93.   FlashUnlock(); FLASH->SLOCK = (1<<(addr/FEE_LOCKS)); // 解锁地址所属锁定区
    94.   while (FLASH->CR & 0x10); // 等待空闲
    95.   FlashUnlock(); FLASH->CR_f.OP = 0x2; // 页擦除模式
    96.   *((u32 *)addr) = 0; // 页面上写启动擦除
    97.   while (FLASH->CR & 0x10); // 等待完成
    98.   FlashUnlock(); FLASH->CR_f.OP = 0; // 改为只读
    99.   FlashUnlock(); FLASH->SLOCK = 0; // 重新锁定
    100.   Irq_Enable; // 开放中断
    101. }
    102. /***/
    103. static void FEEerase_put(u32 addr) // 擦存
    104. {        
    105.   FEEerase(addr); //先擦除
    106.   FEEput(addr); //再转存
    107. }
    108. /***/
    109. static void FEEextr(u32 addr) // 提取数据
    110. {
    111.   u32 data;
    112.   FEEaddr=addr; //作为当前页
    113.   for(FEEoffset=1; FEEoffset<FEE_WORDS; FEEoffset++) // 首字是启用标记,从下一个字开始
    114.   {
    115.     data=*(u32 *)(addr+FEEoffset*4); // 字内容
    116.     if(~data) // 非空,有数据
    117.     {
    118.       FEEdata[data>>16] = data & 0xFFFF;// 替换数组成员值
    119.     }
    120.     else // 空,数据区结束
    121.     {
    122.       break;
    123.     }
    124.   }               
    125. }
    126. /***/
    127. static void FEEeof(void) // 存满处理
    128. {
    129.   if(FEEoffset>=FEE_WORDS) //指针到达字数
    130.   {
    131.     u32 temp = FEEaddr; // 当前页
    132.     FEEput((temp==FEE_P0ADDR)?(FEE_P1ADDR):(FEE_P0ADDR)); // 转存到另一页
    133.     FEEerase(temp); //擦除旧页               
    134.   }
    135. }
    136. /***/
    137. void FEE_timeinit(void)
    138. {
    139.   /*** flash擦写定时参数寄存器配置 ***/
    140.   Irq_Disable; // 禁止中断
    141.   FlashUnlock(); FLASH->TNVS_f.TNVS       = Tnvs    * xFreq;
    142.   FlashUnlock(); FLASH->TPGS_f.TPGS       = Tpgs    * xFreq;
    143.   FlashUnlock(); FLASH->TPROG_f.TPROG     = Tprog   * xFreq;
    144.   FlashUnlock(); FLASH->TSERASE_f.TSERASE = Tserase * xFreq;
    145.   FlashUnlock(); FLASH->TMERASE_f.TMERASE = Tmerase * xFreq;
    146.   FlashUnlock(); FLASH->TPRCV_f.TPRCV     = Tprcv   * xFreq;
    147.   FlashUnlock(); FLASH->TSRCV_f.TSRCV     = Tsrcv   * xFreq;
    148.   FlashUnlock(); FLASH->TMRCV_f.TMRCV     = Tmrcv   * xFreq;
    149.   Irq_Enable; // 开放中断
    150.   /*** --- ***/
    151. }
    152. /***/
    153. void FEE_init(void) // 上电初始化
    154. {
    155.   u32 temp,temp0,temp1;

    156.   FEE_timeinit(); // 初始化FLASH定时参数

    157.   temp=0;
    158.   temp0 = *(u32 *)(FEE_P0ADDR); //P0首字
    159.   if(~temp0) temp|=1; // 非空(有0)
    160.   if(~(*(u32 *)(FEE_P0ADDR + 4))) temp|=2; //P0数据非空
    161.   temp1 = *(u32 *)(FEE_P1ADDR); // P1首字
    162.   if(~temp1) temp|=4; // 非空
    163.   if(~(*(u32 *)(FEE_P1ADDR + 4))) temp|=8; //P1数据非空
    164.   switch (temp) /* 检查两个页面状态 */
    165.   {
    166.     case 0: case 2: // 0000:P0 P1全空 或 0010:P0初始化失败
    167.       FEEcounts=0;
    168.       FEEerase_put(FEE_P0ADDR); //擦存P0
    169.       break;
    170.     case 3: // 0011:P0启用中
    171.             FEEcounts=temp0; // 累计擦除次数
    172.             FEEextr(FEE_P0ADDR); //提取P0数据
    173.       break;
    174.     case 8: // 1011:P0>P1转存失败
    175.       FEEcounts=temp0;
    176.       FEEextr(FEE_P0ADDR); //提取P0数据
    177.       FEEerase_put(FEE_P1ADDR); //擦存P1
    178.       FEEerase(FEE_P0ADDR); //擦除P0
    179.             break;
    180.     case 12: // 1100:P1启用中
    181.       FEEcounts=temp1;
    182.       FEEextr(FEE_P1ADDR); //提取P1数据
    183.       break;
    184.     case 14: // 1110:P1>P0转存失败
    185.       FEEcounts=temp1;
    186.       FEEextr(FEE_P1ADDR); //提取P1数据
    187.       FEEerase_put(FEE_P0ADDR); //擦存P0
    188.       FEEerase(FEE_P1ADDR); //擦除P1
    189.             break;
    190.     case 15: // 1111:擦除失败
    191.       if(temp0>temp1) // 标记数大的作为启用页
    192.       {
    193.         FEEcounts=temp0;
    194.         FEEextr(FEE_P0ADDR); //提取P0数据
    195.         FEEerase(FEE_P1ADDR); //擦除P1
    196.       }
    197.       else
    198.       {
    199.         FEEcounts=temp1;
    200.         FEEextr(FEE_P1ADDR); //提取P1数据
    201.         FEEerase(FEE_P0ADDR); //擦除P0
    202.       }
    203.       break;
    204.   } /* 检查页面状态end */
    205.   FEEeof(); // 满页处理
    206. }
    207. /***/
    208. /*******************/
    复制代码

    结语

    使用FLASH模拟对比eeprom来说,有过之而不及。eeprom每次都是连擦带写,耗费3-4毫秒。本模拟一般写只耗费53微秒,换页时耗费时间,但换页很少发生。

    换页擦除FLASH会使cpu停顿,中断不能及时响应,如果风险,就要避免在需要响应这些中断的情况下写FLASH。这个问题在本身带eeprom的mcu上也是存在的。

    回复

    使用道具 举报

    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则

    小黑屋|路丝栈 ( 粤ICP备2021053448号 )

    GMT+8, 2024-9-19 09:16 , Processed in 0.066627 second(s), 30 queries .

    Powered by Discuz! X3.4

    Copyright © 2001-2021, Tencent Cloud.

    快速回复 返回顶部 返回列表