flash模拟e2prom实现均衡磨损(一)

物理e2prom存取操作

基本实现

 /*硬件接口........................................................................*/
    void eeprom_write_byte(uint16_t addr , uint8_t data);
    uint8_t eeprom_read_byte(uint16_t addr);
    void eeprom_write_buffer(uint16_t start_addr , const uint8_t* buff,uint16_t len);
    void eeprom_read_buffer(uint16_t start_addr,uint8_t* buff , uint16_t len);
    
    /*应用接口.......................................................................*/
    void cfg_write(uint16_t index,const void* data)
    {
        uint8_t len = CFG_GET_LEN(index);
        eeprom_write_buffer(index,(const uint8_t*)data,  len);
    }
    uint8_t cfg_read(uint16_t index, void* data)
    {
        uint8_t len = CFG_GET_LEN(index);
        eeprom_read_buffer(index,(uint8_t*)data,len);
    	return len;
    }
    /*应用实现..........................................................................*/
    #define  BASE_ADDR        0
    #define  STATE_ADDR       (BASE_ADDR)
    
    #define   MEMBER_OFFSET(type,member,offset)  (offset + (uint32_t)(&(type*)0->member))        
    /**1.定义需要存储的数据结构**/
    struct config
    {
        /*system*/
        char soft_version[24];  //软件版本
        char hard_version[24];  //硬件版本 
        char version_time[24];  //版本创建时间
        /*user*/
        uint8_t speed;
        uint8_t kp;
        uint8_t ki;
        uint8_t kd;
        
        uint16_t dir;
        uint16_t mode;
        
        uint32_t version;
        
        char name[16];
        char info[32];
    };
    /*2.定义成员的偏移地址*/
    #define   CFG_SVER_ADDR		MEMBER_OFFSET(struct config ,soft_version   , STATE_ADDR)
    #define   CFG_HVER_ADDR		MEMBER_OFFSET(struct config ,hard_version   , STATE_ADDR) 
    #define   CFG_VER_TIME_ADDR	MEMBER_OFFSET(struct config ,version_time   , STATE_ADDR) 
    
    #define   CFG_SPEED_ADDR    MEMBER_OFFSET(struct config ,speed   , STATE_ADDR) 
    #define   CFG_KP_ADDR    	MEMBER_OFFSET(struct config ,kp      , STATE_ADDR)  
    #define   CFG_KI_ADDR    	MEMBER_OFFSET(struct config ,ki      , STATE_ADDR)  
    #define   CFG_KD_ADDR    	MEMBER_OFFSET(struct config ,kd      , STATE_ADDR)  
    #define   CFG_DIR_ADDR    	MEMBER_OFFSET(struct config ,dir     , STATE_ADDR)  
    #define   CFG_MODE_ADDR    	MEMBER_OFFSET(struct config ,mode    , STATE_ADDR)  
    #define   CFG_VERSION_ADDR  MEMBER_OFFSET(struct config ,version , STATE_ADDR)  
    #define   CFG_NAME_ADDR  	MEMBER_OFFSET(struct config ,name    , STATE_ADDR)  
    #define   CFG_INFO_ADDR  	MEMBER_OFFSET(struct config ,info    , STATE_ADDR)  
    
    #define   CFG_GET_LEN(addr)         

应用举例一

  #define   H_VER         "stm32f103_evk 3.1.2"
    #define   SVER          "mark 1.1.0"
    #define   VER_TIME      __DATA__ __TIME__
    
    void cfg_init(void)
    {
    	cfg_read(CFG_SVER_ADDR,sver) ;
        cfg_read(CFG_HVER_ADDR,hver) ;
        cfg_read(CFG_VER_TIME_ADDR,ver_time) ;
        /*printf the version*/
        
        if( !stycmp(sver,SVER)  ||  !stycmp(hver,HVER)  ||!stycmp(ver_time,VER_TIME))
        {
            cfg_write_default(); 
        }
    	cfg_reload();
    }
    
    void cfg_write_default(void)
    {
        cfg_write (CFG_SVER_ADDR,&sver) ;
        cfg_write (CFG_HVER_ADDR,&hver) ;
        cfg_write (CFG_VER_TIME_ADDR,&ver_time) ;
        
    	cfg_write (CFG_SPEED_ADDR,&speed);   
        cfg_write (CFG_KP_ADDR, &kp); 	
        cfg_write (CFG_KI_ADDR,&ki);    	 
        cfg_write (CFG_KD_ADDR,&kd);    	 
        cfg_write (CFG_DIR_ADDR,&dir);    	
        cfg_write (CFG_MODE_ADDR,&mode);    	 
        cfg_write (CFG_VERSION_ADDR,&ver);  
        cfg_write (CFG_NAME_ADDR,&name);  	
        cfg_write (CFG_INFO_ADDR,&info);  	
    }
    void cfg_reload(void)
    {
        cfg_write (CFG_SVER_ADDR,&sver) ;
        cfg_write (CFG_HVER_ADDR,&hver) ;
        cfg_write (CFG_VER_TIME_ADDR,&ver_time) ;
        
    	cfg_read (CFG_SPEED_ADDR,&speed);   
        cfg_read (CFG_KP_ADDR, &kp); 	
        cfg_read (CFG_KI_ADDR,&ki);    	 
        cfg_read (CFG_KD_ADDR,&kd);    	 
        cfg_read (CFG_DIR_ADDR,&dir);    	
        cfg_read (CFG_MODE_ADDR,&mode);    	 
        cfg_read (CFG_VERSION_ADDR,&ver);  
        cfg_read (CFG_NAME_ADDR,&name);  	
        cfg_read (CFG_INFO_ADDR,&info);  
    }
    /*add your own cfg function..............................................*/
    
    void cfg_kp_write(uint8_t kp)
    {
        cfg_write(CFG_KP_ADDR,&kp);
    }
    uint8_t cfg_kp_read(void)
    {
        uint8_t temp;
        cfg_write(CFG_KP_ADDR,&temp);
        return temp;
    }
    ...

增添新的成员步骤:

  • 根据新的需要存储的长度 ,在结构体类型中创建对应的成员
  • 定义新成员的偏移地址
  • 定义默认值,并在cfg_write_default中增加写入默认值操作
  • 在cfg_reload 中添加 读取新成员 操作
  • 定义新成员的私有读写操作函数

易出错的点:

  • 定义的长度和实际不符,会导致在写入时出错,实际长度大于定义长度会截断数据;实际长度小于定义长度,
    可能发生严重错误,这里会写函数会访问实际数据后面的地址,并写入
  • 无法存储位域成员

应用举例二

需要解决上面存在的两个问题

   /*问题一:  定义的长度与实际用户输入不一致问题............................................*/
    /*必须对用户输入的长度和实际分配的长度作比较  (实际长度<= 分配长度)*/
    
    #define  VAR_AND_LEN(x)    &x , sizeof(x)
    
    int cfg_write(uint16_t index,const void* data , uint16_t len)
    {
        uint8_t  allow_len = CFG_GET_LEN(index);
       	if(len > allow_len) 
        {
            CFG_LOG("wrong size in index: %d",index);
            return -1;
        }
        eeprom_write_buffer(index,(const uint8_t*)data,  len);
        return 0;
    }
    
    //使用写
       cfg_write(CFG_SPEED_ADDR,VAR_AND_LEN(speed));
    
    
    uint8_t cfg_read(uint16_t index, void* data , uint16_t len)
    {
        uint8_t actual_len = CFG_GET_LEN(index);
        if(actual_len > len)
        	eeprom_read_buffer(index,(uint8_t*)data,len);
        else
            eeprom_read_buffer(index,(uint8_t*)data,actual_len);
    	return 0;
    }
    //使用读
     cfg_read(CFG_SPEED_ADDR,VAR_AND_LEN(speed));
    
    /*问题二:  位域问题解决............................................*/
    //位域无法获取到地址,降低了处理的灵活性,除非内存极其有限,否则尽量少用。
    
    //必须新建接口,不需要输入地址的(只是对e2prom单字节写入的简单封装)
    void cfg_write_field(uint16_t index,uint8_t data)
    {
        eeprom_write_byte(index , data);
    }
    uint8_t cfg_read_field(uint16_t index)
    {
        return eeprom_read_byte(index);
    }

依然存在的问题

可以看到,上述对问题一的处理,其实就是引入了长度的输入,并将输入长度和分配长度做比较,从而解决问题1。为了简化函数的参数,将变量与变量的长度用宏封装起来,通过sizeof来求变量的长度。sizeof必须需要确定的变量才能求长度,假设我要存储一个数组中间部分的值,那么这里就会出问题。例如,我从其他地方获取到了一段内存数据,如串口接收到了一帧数据, 通过解析我获得了一段有效的数值,那么对这段有效数值的存储便不能使用此种方式。

虽然存在问题,但是一般用户不会去直接存储一段内存的数据 , 更多的还是存储基本类型变量 ,数组,结构体,结构体位域等,处理这类数据,sizeof是有效的

如何解决产品的默认配置 和重复烧录问题

场景 : 在一批设备,我们会个设备做一个默认配置,即第一次开机会将默认配置写入,并将完成标志也写入,下次开机再去通过读完成标志来决定是否需要还原默认配置。若一批设备已经生产出来,且固件已经烧录(意味着已经初始化了默认配置),但是突然客户告知你需要修改默认配置 (版本号不需要更改),一般的实现是将程序中的写入完成标志更改成另外一个数值,生成的固件再次烧录便会重新写入新的默认配置,但是这样存在问题,如果修改了代码而未手动修改 完成标志的数值,那么配置将不会更新(实际上,设备涉及到升级固件,一般都要求更新配置文件,但也存在例外,比如一些非常重要的参数,如PID的参数,可能是设备长期运行而自调整出来的一套非常合适的值)

引入版本号和工程编译完成时间 宏定义(其实上面已经有使用)

 #define   SOFT_VERSION        "1.0.1"
 #define   VERSION_TAG         "JSON"
 #define   VER_TIME            __DATA__" "__TIME__
    
    void version_out(void)
    {
    	char buf[256]={0};
    
       sprintf(buf,"version: " SOFT_VERSION "  " VERSION_TAG " " VER_TIME "\r\n");
        printf("**********************Version*****************************\r\n");
    	printf("%s",buf);
    	printf("**********************************************************\r\n");
        
    /*移到配置初始化函数======================================================*/  
    	/*read out eeprom saved version*/
        cfg_read(CFG_SVER_ADDR,sver) ;
        cfg_read(CFG_HVER_ADDR,hver) ;
        cfg_read(CFG_VER_TIME_ADDR,ver_time) ;
    	/*compare two version*/  
        if( !stycmp(sver,SVER) ||!stycmp(ver_time,VER_TIME))
        {
            /*if not equal , write the new to the eeprom and updata the default config*/
            cfg_write_default(); 
        }
        cfg_reload();
    /*====================================================================*/
    }

Flash模拟e2prom的存取操作(均衡磨损)

背景:在项目设备没有外部e2prom设备时,又需要存储配置参数,这时可以使用内部flash或者外部SPI flash来存储配置参数,flash不同于e2prom的点在于只能扇区擦除,且擦除次数一般在10w次,写入前必须擦除(也就是说只有0xff的地方能写,如果某块区域为0xfe,那么只能擦除整个扇区才能再修改)。如果按常规做法,当变量需要存储,那么先将同一扇区的值备份到内存,然后擦除扇区,再还原备份值。这样,相当于同一扇区任何一个变量的改变都会增加一次擦除,flash寿命将会消耗很快。

针对这个问题,便有了均衡磨损的处理。为了解决每次写入都需要擦除的问题,这里引入了虚拟地址。如下图物理地址和虚拟地址的存储方式,可以看到,虚拟地址的存储相同的变量所占的空间是大于物理地址存储的,因为这种方式的最小单元还包括虚拟的地址。
在这里插入图片描述

两种方式需要修改speed变量的处理步骤。

物理地址存储:
在这里插入图片描述
虚拟地址存储:
在这里插入图片描述

两种方式需要读取speed变量的处理步骤。

物理地址存储:

直接通过其物理地址读取即可

虚拟地址存储:

可以发现,以虚拟地址存储我们是连续写入的,也就是说某个变量值改变,不去擦除处理,直接在接下来的空闲地址写入新的变量单元(虚拟地址+变量)。这样引入了2个问题。

  • 在同一扇区中会出现很多相同的变量单元,如上图中的speed,每修改一次就增加一个,那么该如何读取呢?由于是连续写入,那么可以确定的是,越靠近扇区下边的(地址高)变量越新,所以读取的时候就可以利用这一点,从sector的最底下开始检索 addr0 ,当搜索到第一个时,表明这是最新的speed。
  • 连续写入的最终结果是扇区被写满,对于这个过程的处理可以使用另一个扇区来配合,两个扇区交替来写入。一个满了写另一个,将满了的里面最新的变量单元搬移到新的扇区,然后将满的扇区擦除。具体过程如下。
    在这里插入图片描述

虚拟地址存储的特点

  • 当一个扇区写满了后才进行擦除,试想一下一般扇区的大小都是大于等于1K的,而我们一般系统需要存储的配置参数可能就几十字节。那么通过虚拟地址的方式相对物理地址可以大大节省擦除次数,而且扇区中的存储单元都得到了有效利用,这就是所谓的均衡磨损。
  • 若扇区较大或者其他特殊情况,可能不太适合使用。上面的实现至少需要两个扇区,有的mcu或者外部存储模块可能扇区是较大的,如stm32H750VBx 内部就一个扇区,无法实现;再如w600芯片扇区大小为32k,那么用户存储取就至少64k,为了存 几百个字节而牺牲64k的空间有点浪费。外部spi flash 一般为4k的扇区,可以使用。

如何定义变量的虚拟地址(*)

存储单元的粒度

上面所示的例子中 虚拟地址和数据分别占用两个字节,那么虚拟地址就可以表示0-65535的范围,每一个数据单元最小数据存储为2个字节。可以通过修改存储单元的粒度来适应不同的系统,如一个字节表示虚拟地址,一个地址表示数据,这样虚拟地址范围为0-255。或者4字节表示虚拟地址,4字节表示数据等等。最小数据单元最好是2的整数倍(扇区以k为单位)。

使用单字节地址单字节数据的粒度 存储利用率高 ,但只能存储0-255个字节 ,适用于存储变量个数较少的系统。如变量A为uint16_t ,这会用掉两个地址存储它,B为结构体占24个字节,需要24个字节来存储B, 总数不能超过255.使用2个字节来表示虚拟地址 , 2个字节表示数据也许是最为合适的粒度。0-65535的范围 虚拟地址范围为 0-64k。也就是说可以存储64K byte的数据。
在这里插入图片描述
后续会增加一版改进的:改进点,类似与UNICODE编码,能获得合适的粒度

参考:ST官方文档

EEPROM emulation.pdf
easyflash(github)
具体实现可参考 CubeMX 路径下的实现(C盘,非安装路径)
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值