【环境专家之智能手表】Part13:各项数据存储至EEPROM
[复制链接]
本帖最后由 w494143467 于 2021-6-27 15:56 编辑
1.介绍
既然是手表,那一定要有存储数据的功能,板载了一个【N24RF64】芯片,可以当做EEPRPM使用同时还可以当作NFC标签使用,NFC标签支持的协议有【ISO15693和ISO18000】,之前只接触过ISO15693,有时间了解一下ISO18000,有外部存储芯片,就不用芯片内部的Flash存储数据了,岂不美哉!
2.N24RF64分析
首先参看【N24RF64】数据手册,发现这个芯片的EEPROM擦除的次数可达200万次,平均一天擦除50次,可以使用一百年的时间。这个EEPROM的存储大小为64Kb,这里的【b】是小位,转换为大B则是8KB,也就是可以存储8192个字节的数据。这个芯片除了EEPROM还有NFC标签的功能,既然有NFC标签的功能,那芯片中还存在存储NFC标签信息的区域,内存分配如下图1所示:
图1
对于NFC的IIC地址以及寄存器分配如下图2所示:
图2
这次设计主要使用EEPROM,其中EEPROM的0~5存储地址已经被手表绑定ID占用了,6~15存储地址被名字所占用,剩余的空间给其他数据使用。关于NFC的功能,这里暂时没有去研究,后面要是有时间就会去研究一下。
3.设计
首先需要分析有多少数据,同时需要存储多少天的数据,下面是我定义存储数据的结构体:
#pragma pack(1) //让编译器对这个结构作1字节对齐
typedef struct single_data
{
uint8_t act; //活动
uint8_t temp; //温度
uint8_t hum; //湿度
uint8_t lum; //光照
uint32_t pre; //气压
}single_data_t;
#pragma pack() //取消1字节对齐,恢复为默认4字节对齐
一共八个字节,为了避免字节对齐导致出错,添加上了强制1字节对齐,存储数据的时候一定要注意字节对齐这个问题,相信很多人都在这里栽过跟头。
这次设计半小时存储一次数据,那么一天就是48条数据,同时存储到EEPROM中还需要有一个标志,表示这个48条数据属于哪天的,这里就使用时间戳进行标记,一天数据的结构体如下所示:
#pragma pack(1) //让编译器对这个结构作1字节对齐
typedef struct one_day_data
{
uint32_t data_time; //数据时间,某一天
single_data_t data[48]; //一天的数据,半小时存储一次
}one_day_data_t;
#pragma pack() //取消1字节对齐,恢复为默认4字节对齐
同样需要强制1字节对齐,希望大家存储到Flash、EEPROM和蓝牙传输的时候,都强制1字节对齐,避免出现无效字节位。
设计存储14天数据,那么需要使用到的字节数为【(48*8 + 4) * 14】等于5432字节,小于8192字节,所以还是够存储的!
这里还需要记录当前存储到第几天,比如我开辟了14天的存储空间,但是我存到第15天了,那么我就需要把第一天的数据删除,把第15天的数据覆盖上去,这样轮询存储,所以定义了一个变量记录当前最后一天需要存储到14天数据中的那一块区域,当然这个变量也需要存储到EEPROM中。最终存储区域的分配如下表所示:
图3
数据方面好了,接下来就是接口了,之前说过,没有初始化时间则不进行数据存储,所以定义一个初始化函数,在设置时间的时候进行初始化,数据存储也是根据【watch_data_init_flag】标志位来判断是否需要存储,具体代码如下:
one_day_data_t today_data; //今天的数据
uint8_t watch_data_cnt = 0; //数据存储到第几个缓存
uint8_t watch_data_init_flag = 0; //初始化标志位
//该函数应该在设置时间的时候调用
void watch_data_init(uint32_t time_utc)
{
uint32_t today_zero = 0;
if(watch_data_init_flag)
return;
memset(&today_data, 0, sizeof(one_day_data_t));
I2CEeprom_Read(WATCH_DATA_DAY_CNT, (uint8_t*)&watch_data_cnt, sizeof(uint8_t), &eeprom); //读取Flash
today_zero = time_to_today_zero(time_utc);
if(watch_data_rec_day(today_zero, &today_data) != 0) //获取Flash中今天的数据,如果没有,则赋值给今日数据的结构体时间
{
today_data.data_time = today_zero;
I2CEeprom_Write(WATCH_DATA_START_ADDR + watch_data_cnt * WATCH_DATA_DAY_LENGTH, (uint8_t*)&today_data.data_time, sizeof(uint32_t), &eeprom); //写入Flash
}
watch_data_init_flag = 1;
}
首先通过【watch_data_init_flag】判断是否已经初始化过,然后将存储今日数据的结构体清零,读取当前存储到第几块区域,然后通过【time_to_today_zero】获取当前时间处于零点时的时间戳,再去通过【watch_data_rec_day】函数去Flash中查找这一天的数据,如果找到了则数据就存储到【today_data】结构体中,如果没有,则创建一个时间存储到数据块中。这里逻辑比较多,所以大家需要花点时间去理解一下。
获取当前时间零点的时间戳函数如下,只需要使用时间函数转换一下即可当然还有别的更方便的办法,求余86400,然后用当前时间减去求余的结果也为当前时间的0点,不过下面这个函数更容易理解。
uint32_t time_to_today_zero(uint32_t time_utc)
{
uint32_t time_zero = 0;
struct my_tm t;
localtime(time_utc, &t);
t.tm_hour = 0;
t.tm_min = 0;
t.tm_sec = 0;
time_zero = mktime(t); //得到零点时间
return time_zero;
}
然后就是存储数据,虽然EEPROM可以擦除200万次,但是也需要节省使用,所以当有一条新数据的时候,只将这一条数据进行存储,而不存储一整天的数据,具体实现如下:
//存储一天的数据
void watch_data_today_save(void)
{
if(watch_data_init_flag == 0) //初始化成功才能存储
return;
I2CEeprom_Write(WATCH_DATA_START_ADDR + watch_data_cnt * WATCH_DATA_DAY_LENGTH, (uint8_t*)&today_data, sizeof(one_day_data_t), &eeprom); //写入Flash
}
//存储一条数据
void watch_data_today_cnt_save(uint8_t today_cnt, single_data_t single_data)
{
if(watch_data_init_flag == 0) //初始化成功才能存储
return;
printf("save:%d\r\n", today_cnt);
memcpy(&today_data.data[today_cnt], &single_data, sizeof(single_data_t)); //拷贝信息
I2CEeprom_Write(WATCH_DATA_START_ADDR + watch_data_cnt * WATCH_DATA_DAY_LENGTH + 4 + today_cnt * 4, (uint8_t*)&today_data.data[today_cnt], sizeof(single_data_t), &eeprom); //写入Flash
}
也实现了存储一整天的数据,供特殊情况下使用。
然后就是从EEPROM中获取数据,这里通过的是每一块数据中都有一个零点的时间戳,用来判断该区域是否是我想要的那一天数据,如果没有该数据,则需要创建一个新的数据,具体实现如下:
//读取指定天数的数据
uint8_t watch_data_rec_day(uint32_t day_cnt, one_day_data_t *day_data)
{
uint8_t i = 0;
uint32_t data_day = 0;
for(i = 0;i < WATCH_DATA_DAY_MAX;i++)
{
I2CEeprom_Read(WATCH_DATA_START_ADDR + i * WATCH_DATA_DAY_LENGTH, (uint8_t*)&data_day, sizeof(uint32_t), &eeprom); //读取Flash
if(data_day == day_cnt) //判断天数是否正确
break;
}
//从Flash读取要查询的某天数据
if(i != WATCH_DATA_DAY_MAX)
{
I2CEeprom_Read(WATCH_DATA_START_ADDR + i * WATCH_DATA_DAY_LENGTH, (uint8_t*)day_data, sizeof(one_day_data_t), &eeprom); //读取Flash
return 0; //读取成功
}
else
{
return 1; //没有该天数据
}
}
最后就是实现切换新的一天的接口了,先切换存储区域为下一块地址,然后存储该天的零点时间戳,为了防止是覆盖的情况,所以需要将这一块区域的其他数据都清零,最后实现接口如下:
//新的一天
void watch_new_day(uint32_t today_utc)
{
uint32_t today_zero;
if(watch_data_init_flag == 0) //初始化成功才能存储
return;
watch_data_cnt++;
if(watch_data_cnt >= WATCH_DATA_DAY_MAX) //循环存储
watch_data_cnt = 0;
memset(&today_data, 0, sizeof(one_day_data_t));
today_zero = time_to_today_zero(today_utc);
today_data.data_time = today_zero; //保存当天0点时间
I2CEeprom_Write(WATCH_DATA_DAY_CNT, (uint8_t*)&watch_data_cnt, sizeof(uint8_t), &eeprom); //写入Flash
I2CEeprom_Write(WATCH_DATA_DAY_CNT + watch_data_cnt * WATCH_DATA_DAY_LENGTH, (uint8_t*)&today_data, sizeof(one_day_data_t), &eeprom); //写入Flash
}
测试一下结果,使用1S存储一次数据,开机之后,设置一下时间,等待一分钟,重启,通过调试看到启动时从Flash中读取到的数据如下图4所示:
图4
4.总结
存储数据比较繁琐的就是这个数据结构,如何进行存储,如何读取,只要这些问题解决了,其实存储数据就比较简单,下一篇就是与APP通信数据,让数据可视化!
|