物理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盘,非安装路径)