嵌入式设备往往需要将大量数据存储到掉电不失的存储介质,如eeprom、flash中,以便下次掉电重启时能读入关键数据继续工作。将要存入的数据以结构体的形式归类并存储是良好的习惯,这有利于数据管理和存储读写。
而对存储介质的读写大家并不陌生,无非是通过通讯告诉存储芯片我要读写的地址和长度。如果把存储芯片比作一本书,那么地址就是书的目录,你如何规划这个目录至关重要,这决定了这你以后读书或者改书的便易。
在嵌入式项目中,这个“目录”通常会用结构体+宏定义的方法制定,以下将介绍如何整洁有序的在存储设备中存储数据。
一、例子
假如我有一个设备,它监控着某电子产品的电流、电压、功率、温度、湿度、光照等信息,当上述某一或某几项超过规定限值时我的设备将会进行报警灯一些列操作。这些限值当然不可以通过初始化的方式放在运行内存中,因为万一我通过通讯修改了这些值,那么掉电后岂不是又要修改一次?所以需要存在eeprom中是必要的。
与此同时,我还要在eeprom中存一些设备相关的生产信息,如设备型号、硬件版本、软件版本等,这些都是要求掉电不失的。
所以到这里,我需要存储的内容为:电流、电压、功率、温度、湿度、光照限值,以及设备型号、硬件版本、软件版本。
将这些信息归类并放入结构体中如下
typedef struct{
unsigned int current; //电流
unsigned int voltage; //电压
unsigned int power; //功率
}Para_ElectricTypedef; //电气参数结构体
typedef struct{
signed char temperature; //温度
unsigned char humidity; //湿度
unsigned char light; //光照
}Para_EnvironmentTypedef; //环境参数结构体
typedef struct{
Para_ElectricTypedef Para_Electric;
Para_EnvironmentTypedef Para_Environment;
}ParaTypedef; //参数总结构体
typedef struct{
unsigned char DevChara; //设备型号
char SoftWareVersion[10]; //软件版本
char HardWareVersion[10]; //硬件版本
}DeviceInfoTypedef; //设备信息结构体
二、目录的生成
这个例子很简单,有了数据结构,那么接下来就是制定存储的“目录”。其实就需要存两个结构体,一个ParaTypedef一个DeviceInfoTypedef。那么将ParaTypedef从eeprom的0地址开始存好了,它占用了sizeof(ParaTypedef)的空间大小,所以DeviceInfoTypedef要紧接其后,那么DeviceInfoTypedef就要从0 + sizeof(ParaTypedef)的地址开始存,所以就要在宏定义中定义好各结构体的起始地址,也就是目录,如下
//参数的起始地址,也是存储设备的起始地址
#define PARA_ADDR 0
//设备信息的起始地址
#define DEVICE_INFO_ADDR (PARA_ADDR + sizeof(ParaTypedef))
等以后有了新的东西要存,以此类推往后堆就可以了。
有了目录,之后我们需要关注的是如何从目录中定位到具体的内容。比如我虽然DeviceInfoTypedef结构体的起始地址为0 + sizeof(ParaTypedef),我还要知道里边的硬件、软件版本号的地址才能对某项数据进行读写。这时利用结构体偏移,取得某项在某结构中的偏移地址,然后再加上此结构体在eeprom中的起始地址,就是该项数据在eeprom中的存储地址,如下
//通过变量名取得在参数结构体中的偏移
#define GET_OFFSET_PARA(ItemName) ( &( ((ParaTypedef*)0)->ItemName ) )
//通过变量名取得在设备信息结构体中的偏移
#define GET_OFFSET_DEVICEINFO(ItemName) ( &( ((DeviceInfoTypedef*)0)->ItemName ) )
通过宏定义的方式很好理解,直接定义一个结构体指针0,其结构内变量的地址就是变量在结构体内的偏移。使用时很方便,比如我要获得电流在eeprom中的存储地址以对其进行读写,那么只需要利用以下一句即可
addr = PARA_ADDR + GET_OFFSET_PARA(Para_Electric.current);
三、windows环境下模拟eeprom的读写
在windows下完全可以新建一个文件模拟eeprom,然后以二进制方式打开文件,通过以上的存储结构对数据进行读写,并可以通过UltralEdit等工具清楚的看到“eeprom”中数据的改动。
举个简单且切合实际的流程
1、出厂前需要有个出厂设置,以将默认参数存到eeprom中;
2、需要读出数据;
3、需要能对数据进行修改,即写;
4、时eeprom,当然要有个模拟的擦eeprom的操作。
所以main函数代码如下
FILE *eeprom; //电脑上创建的文件,模拟eeprom
int main()
{
volatile int inputnbr;
char c;
eeprom = fopen("test.bin","rb+"); //以二进制读写方式打开test.bin
if(eeprom == 0) //若无文件则创建
{
eeprom = fopen("test.bin","wb+");
}
while(1)
{
printf("\n1->出厂初始化 2->读数据 3->写数据 4->退出 0->擦eeprom\n选择操作项:");
scanf("%d",&inputnbr);
switch(inputnbr)
{
case 1:
InitDate();
printf("初始化完成\n");
break;
case 2:
ReadAndShow();
break;
case 3:
WriteData();
break;
case 4:
fclose(eeprom);
return 0;
break;
case 0:
EraseEeprom();
break;
default:
break;
}
while((c = getchar()) != EOF && c != '\n');
}
}
运行界面如下
1.出厂初始化
即将各参数设一个默认值,写入eeprom中,代码如下
//出厂设置,即将各变量初始化,写入eeprom中
void InitDate()
{
ParaTypedef para;
DeviceInfoTypedef dev;
char SoftWareVersion[] = "V5.0.1";
char HardWareVersion[] = "V5.0.2";
memset(¶,0,sizeof(para));
memset(&dev,0,sizeof(dev));
para.Para_Electric.current = 10;
para.Para_Electric.voltage = 230;
para.Para_Electric.power = 50;
para.Para_Environment.temperature = 50;
para.Para_Environment.humidity = 80;
para.Para_Environment.light = 100;
dev.DevChara = 0;
memcpy(dev.SoftWareVersion,SoftWareVersion,strlen(SoftWareVersion) + 1);
memcpy(dev.HardWareVersion,HardWareVersion,strlen(HardWareVersion) + 1);
WriteEeprom(PARA_ADDR,(unsigned char *)¶,sizeof(para));
WriteEeprom(DEVICE_INFO_ADDR,(unsigned char *)&dev,sizeof(dev));
}
其中WriteEeprom为封装好的模拟eepom的写接口,其定义与实现如下
//写eeprom,传入地址、源数据指针、要写入的长度
void WriteEeprom(unsigned int addr,unsigned char *pSrcBuf,unsigned short len)
{
fseek(eeprom,addr,SEEK_SET);
fwrite(pSrcBuf,len,1,eeprom);
}
和嵌入式设备中常用的写eeprom接口相似,传入地址、源数据、长度,通过C语言的文件操作实现。
2.读数据
直接读出了所有数据并打印,代码如下
//读指定地址的指定长度,并打印
//这里将存的数据都读出来
void ReadAndShow()
{
ParaTypedef para;
DeviceInfoTypedef dev;
ReadEeprom(PARA_ADDR,(unsigned char *)¶,sizeof(para));
ReadEeprom(DEVICE_INFO_ADDR,(unsigned char *)&dev,sizeof(dev));
printf("current:%d\n",para.Para_Electric.current);
printf("voltage:%d\n",para.Para_Electric.voltage);
printf("power:%d\n",para.Para_Electric.power);
printf("temperature:%d\n",para.Para_Environment.temperature);
printf("humidity:%d\n",para.Para_Environment.humidity);
printf("light:%d\n",para.Para_Environment.light);
printf("DevChara:%d\n",dev.DevChara);
printf("SoftWareVersion:%s\n",dev.SoftWareVersion);
printf("HardWareVersion:%s\n",dev.HardWareVersion);
}
其中ReadEeprom为封装好的模拟eepom的读接口,其定义与实现如下
//读eeprom,传入地址、目的数组指针、要读入的长度
void ReadEeprom(unsigned int addr,unsigned char *pDesBuf,unsigned short len)
{
fseek(eeprom,addr,SEEK_SET);
fread(pDesBuf,len,1,eeprom);
}
3.写数据
通过输入编号选择要写的数据,然后通过编号获得要写数据在eeprom中的地址、要写的长度,要写的内容通过键盘输入,代码如下
//写数据
//根据不同的变量,取得变量在eeprom中的地址,然后写入
void WriteData()
{
int inputnbr,i;
int addr = 0,len = 0;
unsigned char *SetData = 0;
char c;
printf("选择设置项\n");
for(i = 0;i < sizeof(VariableName)/sizeof(VariableName[0]); i++)
{
printf("%d->%s\n",i + 1, VariableName[i]);
}
printf("\n选择操作项:");
scanf("%d",&inputnbr);
//根据选择的数据项取得三要素:地址、源数据、长度
switch(inputnbr)
{
case 1:
addr = PARA_ADDR + GET_OFFSET_PARA(Para_Electric.current);
len = sizeof(unsigned int);
SetData = (unsigned int *)malloc(len);
break;
case 2:
addr = PARA_ADDR + GET_OFFSET_PARA(Para_Electric.voltage);
len = sizeof(unsigned int);
SetData = (unsigned int *)malloc(len);
break;
case 3:
addr = PARA_ADDR + GET_OFFSET_PARA(Para_Electric.power);
len = sizeof(unsigned int);
SetData = (unsigned int *)malloc(len);
break;
case 4:
addr = PARA_ADDR + GET_OFFSET_PARA(Para_Environment.temperature);
len = sizeof(unsigned char);
SetData = (unsigned char *)malloc(len);
break;
case 5:
addr = PARA_ADDR + GET_OFFSET_PARA(Para_Environment.humidity);
len = sizeof(unsigned char);
SetData = (unsigned char *)malloc(len);
break;
case 6:
addr = PARA_ADDR + GET_OFFSET_PARA(Para_Environment.light);
len = sizeof(unsigned char);
SetData = (unsigned char *)malloc(len);
break;
case 7:
addr = DEVICE_INFO_ADDR + GET_OFFSET_DEVICEINFO(DevChara);
len = sizeof(unsigned char);
SetData = (unsigned char *)malloc(len);
break;
case 8:
addr = DEVICE_INFO_ADDR + GET_OFFSET_DEVICEINFO(SoftWareVersion[0]);
len = sizeof( ((DeviceInfoTypedef*)0 )->HardWareVersion );
SetData = (char *)malloc(len);
break;
case 9:
addr = DEVICE_INFO_ADDR + GET_OFFSET_DEVICEINFO(HardWareVersion[0]);
len = sizeof( ((DeviceInfoTypedef*)0 )->HardWareVersion );
SetData = (char *)malloc(len);
break;
default:
break;
}
printf("输入要设置的值:");
if(inputnbr > 0 && inputnbr < 8)
{
scanf("%d",SetData);
printf("SetData%d\n",*SetData);
}
else if(inputnbr >= 8)
{
scanf("%s",SetData);
}
WriteEeprom(addr,(unsigned char *)SetData,len);
//记得释放
if(SetData != 0)
{
free(SetData);
}
}
关键核心就是之前介绍的通过结构体起始地址和变量在结构体内的偏移,获得变量在eeprom中的确切地址,然后直接调用WriteEeprom写。
4.擦eeprom
就是意淫test.bin这个文件是一个256kb的eeprom,全给写成0xff,代码如下
//擦eeprom
void EraseEeprom()
{
int i;
unsigned char sector[1024];
memset(sector,0xff,1024);
fseek(eeprom,0,SEEK_SET);
for(i = 0; i < 256; i++)
{
fwrite(sector,1024,1,eeprom);
}
}
四、整体代码和运行现象
1.整体代码
为了方便阅读,都放在一个c文件里了
#include <stdio.h>
#include <string.h>
typedef struct{
unsigned int current; //电流
unsigned int voltage; //电压
unsigned int power; //功率
}Para_ElectricTypedef; //电气参数结构体
typedef struct{
signed char temperature; //温度
unsigned char humidity; //湿度
unsigned char light; //光照
}Para_EnvironmentTypedef; //环境参数结构体
typedef struct{
Para_ElectricTypedef Para_Electric;
Para_EnvironmentTypedef Para_Environment;
}ParaTypedef; //参数总结构体
typedef struct{
unsigned char DevChara; //设备型号
char SoftWareVersion[10]; //软件版本
char HardWareVersion[10]; //硬件版本
}DeviceInfoTypedef; //设备信息结构体
//参数的起始地址,也是存储设备的起始地址
#define PARA_ADDR 0
//设备信息的起始地址
#define DEVICE_INFO_ADDR (PARA_ADDR + sizeof(ParaTypedef))
//通过变量名取得在参数结构体中的偏移
#define GET_OFFSET_PARA(ItemName) ( &( ((ParaTypedef*)0)->ItemName ) )
//通过变量名取得在设备信息结构体中的偏移
#define GET_OFFSET_DEVICEINFO(ItemName) ( &( ((DeviceInfoTypedef*)0)->ItemName ) )
FILE *eeprom; //电脑上创建的文件,模拟eeprom
char *VariableName[] = {
"current",
"voltage",
"power",
"temperature",
"humidity",
"light",
"DevChara",
"SoftWareVersion",
"HardWareVersion"
}; //变量名字符串组,方便批量打印
//写eeprom,传入地址、源数据指针、要写入的长度
void WriteEeprom(unsigned int addr,unsigned char *pSrcBuf,unsigned short len)
{
fseek(eeprom,addr,SEEK_SET);
fwrite((void *)pSrcBuf,len,1,eeprom);
}
//读eeprom,传入地址、目的数组指针、要读入的长度
void ReadEeprom(unsigned int addr,unsigned char *pDesBuf,unsigned short len)
{
fseek(eeprom,addr,SEEK_SET);
fread(pDesBuf,len,1,eeprom);
}
//出厂设置,即将各变量初始化,写入eeprom中
void InitDate()
{
ParaTypedef para;
DeviceInfoTypedef dev;
char SoftWareVersion[] = "V5.0.1";
char HardWareVersion[] = "V5.0.2";
memset(¶,0,sizeof(para));
memset(&dev,0,sizeof(dev));
para.Para_Electric.current = 10;
para.Para_Electric.voltage = 230;
para.Para_Electric.power = 50;
para.Para_Environment.temperature = 50;
para.Para_Environment.humidity = 80;
para.Para_Environment.light = 100;
dev.DevChara = 0;
memcpy(dev.SoftWareVersion,SoftWareVersion,strlen(SoftWareVersion) + 1);
memcpy(dev.HardWareVersion,HardWareVersion,strlen(HardWareVersion) + 1);
WriteEeprom(PARA_ADDR,(unsigned char *)¶,sizeof(para));
WriteEeprom(DEVICE_INFO_ADDR,(unsigned char *)&dev,sizeof(dev));
}
//读指定地址的指定长度,并打印
//这里将存的数据都读出来
void ReadAndShow()
{
ParaTypedef para;
DeviceInfoTypedef dev;
ReadEeprom(PARA_ADDR,(unsigned char *)¶,sizeof(para));
ReadEeprom(DEVICE_INFO_ADDR,(unsigned char *)&dev,sizeof(dev));
printf("current:%d\n",para.Para_Electric.current);
printf("voltage:%d\n",para.Para_Electric.voltage);
printf("power:%d\n",para.Para_Electric.power);
printf("temperature:%d\n",para.Para_Environment.temperature);
printf("humidity:%d\n",para.Para_Environment.humidity);
printf("light:%d\n",para.Para_Environment.light);
printf("DevChara:%d\n",dev.DevChara);
printf("SoftWareVersion:%s\n",dev.SoftWareVersion);
printf("HardWareVersion:%s\n",dev.HardWareVersion);
}
//写数据
//根据不同的变量,取得变量在eeprom中的地址,然后写入
void WriteData()
{
int inputnbr,i;
int addr = 0,len = 0;
unsigned char *SetData = 0;
char c;
printf("选择设置项\n");
for(i = 0;i < sizeof(VariableName)/sizeof(VariableName[0]); i++)
{
printf("%d->%s\n",i + 1, VariableName[i]);
}
printf("\n选择操作项:");
scanf("%d",&inputnbr);
//根据选择的数据项取得三要素:地址、源数据、长度
switch(inputnbr)
{
case 1:
addr = PARA_ADDR + GET_OFFSET_PARA(Para_Electric.current);
len = sizeof(unsigned int);
SetData = (unsigned int *)malloc(len);
break;
case 2:
addr = PARA_ADDR + GET_OFFSET_PARA(Para_Electric.voltage);
len = sizeof(unsigned int);
SetData = (unsigned int *)malloc(len);
break;
case 3:
addr = PARA_ADDR + GET_OFFSET_PARA(Para_Electric.power);
len = sizeof(unsigned int);
SetData = (unsigned int *)malloc(len);
break;
case 4:
addr = PARA_ADDR + GET_OFFSET_PARA(Para_Environment.temperature);
len = sizeof(unsigned char);
SetData = (unsigned char *)malloc(len);
break;
case 5:
addr = PARA_ADDR + GET_OFFSET_PARA(Para_Environment.humidity);
len = sizeof(unsigned char);
SetData = (unsigned char *)malloc(len);
break;
case 6:
addr = PARA_ADDR + GET_OFFSET_PARA(Para_Environment.light);
len = sizeof(unsigned char);
SetData = (unsigned char *)malloc(len);
break;
case 7:
addr = DEVICE_INFO_ADDR + GET_OFFSET_DEVICEINFO(DevChara);
len = sizeof(unsigned char);
SetData = (unsigned char *)malloc(len);
break;
case 8:
addr = DEVICE_INFO_ADDR + GET_OFFSET_DEVICEINFO(SoftWareVersion[0]);
len = sizeof( ((DeviceInfoTypedef*)0 )->HardWareVersion );
SetData = (char *)malloc(len);
break;
case 9:
addr = DEVICE_INFO_ADDR + GET_OFFSET_DEVICEINFO(HardWareVersion[0]);
len = sizeof( ((DeviceInfoTypedef*)0 )->HardWareVersion );
SetData = (char *)malloc(len);
break;
default:
break;
}
printf("输入要设置的值:");
if(inputnbr > 0 && inputnbr < 8)
{
scanf("%d",SetData);
printf("SetData%d\n",*SetData);
}
else if(inputnbr >= 8)
{
scanf("%s",SetData);
}
WriteEeprom(addr,(unsigned char *)SetData,len);
//记得释放
if(SetData != 0)
{
free(SetData);
}
}
//擦eeprom
void EraseEeprom()
{
int i;
unsigned char sector[1024];
memset(sector,0xff,1024);
fseek(eeprom,0,SEEK_SET);
for(i = 0; i < 256; i++)
{
fwrite(sector,1024,1,eeprom);
}
}
int main()
{
volatile int inputnbr;
char c;
eeprom = fopen("test.bin","rb+"); //以二进制读写方式打开test.bin
if(eeprom == 0) //若无文件则创建
{
eeprom = fopen("test.bin","wb+");
}
while(1)
{
printf("\n1->出厂初始化 2->读数据 3->写数据 4->退出 0->擦eeprom\n选择操作项:");
scanf("%d",&inputnbr);
switch(inputnbr)
{
case 1:
InitDate();
printf("初始化完成\n");
break;
case 2:
ReadAndShow();
break;
case 3:
WriteData();
break;
case 4:
fclose(eeprom);
return 0;
break;
case 0:
EraseEeprom();
break;
default:
break;
}
while((c = getchar()) != EOF && c != '\n');
}
}
2.运行现象
1.擦eeprom
执行完擦eeprom后,用ultraledit看test.bin文件,全为FF,是不是有点像真的eeprom了哈哈哈
2.出厂初始化和读数据
设置好后读出来如下图
然后看test.bin,和设置的一一对应,如下图
3.写数据
将各变量分别按顺序设成11、22、33、44、55、66、77、88、99,读数据和读test.bin如下,没问题。