文章目录
前言
EEPROM可以说也是蓝桥杯嵌入式的常客。EEPROM断电保存。总的来说并没有什么难度,但还是需要注意一些问题,越是简单的东西,越是容易出错。
笔者对于各种数据类型的读写,统一采用变量字节的读取方式。这种方式不需进行过多的转换,而且适用于几乎所有的基本数据类型的读取,也适用于小容量的结构体变量。
读者觉得这种方法很好,于是选择分享出来,有些地方,可能还不够完善,希望能对一些小伙伴有帮助。
蓝桥杯比赛会提供相应的IIC.c和IIC.h文件,我们需要做的就是如何使用文件里的相关函数实现相应的读写操作。有兴趣的读者,可以查阅相关资料,自行学习IIC协议。
一、EEPROM单字节读取方式
1、读写函数
写函数
void EEPROM_Write(uint8_t add,uint8_t date)
{
I2CStart();//起始
I2CSendByte(0xa0);//控制字,写
I2CWaitAck();
I2CSendByte(add);//片内单元地址
I2CWaitAck();
I2CSendByte(date);//写入的字节
I2CWaitAck();
I2CStop(); //停止
}
读函数
uint8_t EEPROM_Read(uint8_t add)
{
uint8_t temp;
I2CStart();//起始
I2CSendByte(0xa0);//控制字,告诉EEPROM进行写字节操作
I2CWaitAck();
I2CSendByte(add);//片内单元地址
I2CWaitAck();
I2CStop();//停止
I2CStart();//起始
I2CSendByte(0xa1);//控制字,告诉EEPROM进行读字节操作
I2CWaitAck();
temp=I2CReceiveByte();//读取字节
I2CSendNotAck();
I2CStop(); //停止
return temp;
}
AT24C02(EEPROM)片内地址从0x00到0xff共256个地址单元,每个地址单元可以存放一个字节。需要注意的是,连续多个字节写入EEPROM要进行10ms的延时,这是因为EEPROM处理数据的速度远小于单片机的速度,为了防止数据丢失,所以需要延时。
上述代码中的add即为EEPROM的片内地址,上述代码中的date即为写入EEPROM的数据。
以上代码就是单字节的读写函数,每次只能读写一个字节。这些代码比赛是需要我们自行编写的。
读写举例:
uint8_t date=26;
EEPROM_Write(0x00,date);//写入EEPROM的0x00单元,数据大小为26的uint8_t类型数据。
date=19;
date=EEPROM_Read(0x00);//读取EEPROM的0x00单元数据,当前date大小为26
2、读写不同的数据类型
整型:
对于uint8_t 、uint8_t 、char、unsigned char这些单字节的数据类型,直接进行读写操作即可。
代码举例:
略
对于uint16_t、int16_t、等2个字节的数据类型,需要进行位运算,屏蔽低8位或高8位。依次写入高8位和低8位。
代码举例:uint16_t 类型写操作:
//uint16_t类型写操作,我们向EEPROM读写变量temp
uint16_t temp=1200;
uint8_t date;
date=temp>>8;//右移8位,获取高8位
EEPROM_Write(0x00,date);//写入高8位
HAL_Delay(10);
date=temp&0x00ff;//屏蔽高8位,保留低8位
EEPROM_Write(0x01,date);//写入低8位
HAL_Delay(10);
uint16_t 类型读操作(变量和地址基于上述代码进行操作)
date=EEPROM_Read(0x00);//读取高8位
temp=date;
temp<<=8;
date=EEPROM_Read(0x01);//读取低8位
temp|=date;//此时的temp为1200
除了通过移位和按位与获取高8位和低8位,也可以使uint16_t 的变量除以256取整得高8位,模256取余得低8位。
例: uint16_t 类型写操作:
//uint16_t类型写操作,我们向EEPROM读写变量temp
uint16_t temp=1200;
EEPROM_Write(0x00,temp/256);//除以256,写入高8位
HAL_Delay(10);
EEPROM_Write(0x01,temp%256);//模256,写入低8位
HAL_Delay(10);
例: uint16_t 类型读操作:
temp=EEPROM_Read(0x00)*256;//读取高8位
temp=temp+EEPROM_Read(0x01);//读取低8位
局限性:
这种方式相对来说比较麻烦,2个字节的数据类型,相对还好。而对于uint32_t ,需要进行4次换算提取字节操作,而对于uint64_t类型更是需要进行8次操作,还需要去写新的处理函数,更为麻烦。
而对于float,double类型,写的时候需要转换为整数,读的时候还要转换为整数。程序编写效率极低,而且易出错。而且对于像1.2890899*1015这类高精读高范围的浮点数据,读写更加的困难。
对于大范围,高精度的数据double,int,uint32_t,有没有更好的方法呢?当然有,可以采用多字节的读写方式统一处理。
虽然目前题目上还没有没有遇到过这样的要求,但读者觉得一些知识还是应当掌握。
单字节读取方式读写浮点型数据的适用范围:
对于表示数据范围不大的float,double类型数据,可以采用这种单字节读写的方式,但有效位数最好不要超过4位。
代码举例:
double temp=1.2;
date=temp*10;
EEPROM_Write(0x00,date);//写操作
date=EEPROM_Read(0x00);//读操作
temp=date/10.0;//换算,注意是除以10.0,而不是10,不然会截取丢失。
当然也可以直接用整型变量存储一些double数据类型,只在进行显示和计算的时候进行相应的换算。
二、EEPROM多字节读写方式
1、多字节读写函数
优点:
EEPROM不但可以单字节读写,还可以按页一次性多个字节读写,每页8个字节,所以最多可以连续读写8个字节,非常的方便。我们只需要向EEPROM传递EEPROM每页的首地址,就可以连续读写多个字节。
除此之外,多字节读写方式对于多字节的基本数据类型的读写也非常简单。有效的解决了前面的问题。例如8个字节的double,4个字节的float,2个字节的uint16_t,很多同学都会进行相应的换算,这样着实麻烦。
我们知道在相同的平台上,相同的数据类型,在内存中所占的字节大小是固定的,而且单个数据变量的字节在内存上的排布是连续的,所以,我们可以直接将该变量的所有字节写入EEPROM中,读取的时候,也是将相应的字节都读取出来,不用任何类型换算。
当然理解这部分内容需要对于指针和地址,要有一点点的了解。不懂也没关系,会用就可以。
写函数
void EEPROM_Write(uint8_t add,uint8_t * array,uint8_t n)
{
I2CStart();
I2CSendByte(0xa0);//写操作
I2CWaitAck();
I2CSendByte(add);//片内单元地址
I2CWaitAck();
while(n--)//进行n次循环,写入n个数据,每次写入进行一次等待。
{
I2CSendByte(*array++);
I2CWaitAck();
}
I2CStop();
}
读函数
void EEPROM_Read(uint8_t add,uint8_t * array,uint8_t n)
{
I2CStart();
I2CSendByte(0xa0);
I2CWaitAck();
I2CSendByte(add);
I2CWaitAck();
I2CStop();
I2CStart();
I2CSendByte(0xa1);
I2CWaitAck();
while(n--)
{
*array++=I2CReceiveByte();//读取
if(n)//如果还有数据要读取
{
I2CSendAck();
}
else//如果没有数据要读取
{
I2CSendNotAck();
}
}
I2CStop();
}
以上的函数实际上就是在单字节读写的基础上加入了循环。操作都是一样的,只是读写了多个字节而已。
注意:读者并没有改函数名字,名字和单字节的读写函数名字是一样的,不要搞混。
2、多字节读写不同的数据类型
对于uint32_t 类型数据以及高精度高范围的double类型数据的读写,用单字节的读写方式读写很困难,但是用多字节的读写方式处理起来就很轻松了。
原理:
有C语言基础的同学都知道,头文件string.h里有这样一个函数memcpy();该函数是进行数组的复制的。使用过这个函数的同学都知道,该函数不关心数组内的元素类型和个数,只关心两个数组的首元素地址,以及需要复制的字节数。
注意:是字节数,而非是元素个数。
所以我们的EEPROM的读写,也可以采用这样的操作,无论对于什么基本数据类型变量,都只关心该变量的字节数,和该变量的地址,而不关心它的类型。
每次将该数据类型的所有字节都写入EEPROM中,读取的时候将所有的字节都读取出来。
使用多字节读取函数,就简单很多了。
我们只需要向EEPROM_Write和EEPROM_Read函数,提供该变量的地址,以及变量的字节大小就可以了。
变量的字节大小,我们可以通过sizeof运算符获取。
例:uint16_t类型读写:
uint16_t temp=1200;
EEPROM_Write(0x00,(uint8_t *)&temp,sizeof temp);//写入
HAL_Delay(10);
EEPROM_Read(0x00,(uint8_t *)&temp,sizeof temp);//读取
而对于double,float,以及int,uint32_t,就很简单了
例:double类型读取
double num1=3.1415926;
EEPROM_Write(0x00,(uint8_t *)&num1,sizeof num1);//写入
HAL_Delaly(10);
EEPROM_Read(0x00,(uint8_t *)&num,sizeof num);//读取
所以,对于所有的基本数据类型(整型,浮点型),只要数据类型字节数不大于8,就都可以统一使用这种方式。
格式:
EEPROM_Write(EEPROM单元地址,(uint8_t *)&变量名,sizeof 变量名);
EEPROM_Read(EEPROM单元地址,(uint8_t *)&变量名,sizeof 变量名);
注意:我们不用的数据写入EEPROM的片内地址不要重复。每次写多少字节,相应的EEPROM片内地址选择也要相应的偏移相应的大小。
3、复合类型数据(数组、结构体)的读写操作
如何进行多个数据的读写操作呢?之前讲到的都是单个数据的读写操作,如何一次读写多个数据呢?
我们可以将相同类型的数据存在数组中,不同类型的数据存放在结构体中。我们依旧只关心变量地址和变量字节数,而不关心类型和大小。
(1)一维数组的读写操作
我们只关心一维数组的首元素的地址,和它的字节数,每次最多写入8个字节。
例:
不足8字节大小的数组:
uint8_t array[6]={1,2,3,4,5,6};
EEPROM_Write(0x00,array,sizeof array);//写
HAL_Delay(10);
EEPROM_Read(0x00,array,sizeof array);//读
HAL_Delay(10);
大于8字节大小的数组:
uint16_t array[6]={1000,2,3,4,5,6};//该数组字节大小为2*6=12
EEPROM_Write(0x00,(uint8_t *)array,8);//写前8个字节
HAL_Delay(10);
EEPROM_Write(0x10,(uint8_t *)array+8,4);//写后四4个字节
HAL_Delay(10);
运用知识基础:
需要对于各个基本数据类型的字节大小有一定了解,要对于数组的大小进行正确的计算和分析。
对于小数据,多个数据类型,可以采用这样的操作。
而对于元素为uint32_t ,double的数据类型,要读写多个数据,可以自己再定义一个函数即可。
而对于表示范围不大的double类型,可以转换为uint8_t 类型,存储在uint8_t 的数组中,需要用的时候,进行相应的转换即可。这样存储1.2的double型变量原本需要读写8个字节,但是存储为表示12的uint8_t 类型的变量读写只要1个字节。
(2)二维数组的读写操作
我们知道二维数组的数据都是连续存储的。
所以,我们只关心字节大小和首地址,不考虑维度。
代码举例:
uint8_t array[2][2];
EEPROM_Write(0x00,(uint8_t *)array,4);//写
EEPROM_Read(0x00,(uint8_t *)array,4);//读
uint8_t array[3][5];
EEPROM_Write(0x00,(uint8_t *)array,8);//一共有3*5=15个字节,先写前8个
EEPROM_Write(0x10,(uint8_t *)array+8,7);//写后7个字节
EEPROM_Read(0x00,(uint8_t *)array,8);//一共有3*5=15个字节,先读前8个
EEPROM_Read(0x10,(uint8_t *)array+8,7);//读后7个字节
(3)结构体的读写操作
结构体也可以采用这样的方式,但是需要注意的是,结构体变量不能过大,需要能正确的计算结构体的大小。或者通过sizeof运算符得到。
例:
struct
{
uint8_t num;
uint16_t num1;
float date;
}Value;
EEPROM_Write(0x00,(uint8_t *)&Value,sizeof Value);//写
EEPROM_Read(0x00,(uint8_t *)&Value,sizeof Value);//读
(4)各个基本数据类型的大小
基本数据类型基于不同平台字节大小是不一样的,注意本表是统计stm32G431Rbt6数据类型字节大小,方便大家进行相应的读写操作。
类型 | 字节大小 |
---|---|
char | 1 |
short | 2 |
int | 4 |
long | 4 |
float | 4 |
double | 8 |
局限性:
多字节的读写方式虽然方法简单也很通用,但对于读写double比较浪费EEPROM的资源,最多只能存取256/8=32个double类型,所以,我们可以将double 类型的变量转化为uint8_t 、uint16_t 类型,存储在EEPROM中。但是话说回来,如果不是读取很多个double类型的数据,建议使用多字节的读写方式处理,资源不用白不用,留着也是浪费。
小节:
无论是各种基本数据类型,还是复合数据类型,单个,还是多个。对于多字节的读写操作,我们只需要明确变量首地址,变量字节大小就可以了。此方法通用。
格式:
EEPROM_Write(EEPROM单元地址,(uint8_t *)&变量名,sizeof 变量名);
EEPROM_Read(EEPROM单元地址,(uint8_t *)&变量名,sizeof 变量名);
五、初次上电默认值
题目可能会有这样的要求,对于相应的数据,要求初次上电的时候值为默认值(例如要求初次上电该数据为20),以后对该数据进行读写操作,进行保存。
如何实现呢?我们需要来判断是不是第一次上电。
如何判断呢?我们可以随便读取EEPROM里的任意单元地址(不要和我们需要保存的数据地址相同)里的数据。对该数据进行判断,是否等于一个0xf8(等于别的也可以),如果不等于0xf8,则读取到的是EEPROM原本存储的值,说明是第一次上电,将0xf8写进该单元,此时该单元的值等于0xf8。如果等于0xf8,则说明不是第一次上电。
例:
uint8_t date=36;//默认值为36,需要保存的数据
uint8_t temp;
temp=EEPROM_Read(0x00);//单字节读取方式
if(temp!=0xf8)//如果不等于0xf8是第一次上电
{
temp=0xf8;
EEPROM_Write(0x00,temp);
HAL_Delay(10);
EEPROM_Write(0x10,date);//将该数据的值存入EEPROM。
HAL_Delay(10);
}
date=EEPROM_Read(0x10);
HAL_Delay(10);
while(1)
{
}
可能有的同学会感觉,如果第一次上电,读取的EEPROM原来存储的数据与0xf8相同咋么办。这概率还是有点小,1/256,如果觉得小,就读取两个数据,进行两次判断,这样的概率为1/(256*256)。
读取两个吗?通过多字节的读写方式,一次读写一个uint16_t的变量进行判定,不是更方便?
六、延时问题
无论是单字节的读写方式,还是多字节的读写方式,向EEPROM写入数据的时候都要进行相应的延时,建议延时10ms,这个问题前面有讲到。主要是因为EEPROM的速度跟不上stm32的速度。
七、一点点的小BUG
建议IIC的初始化函数写在LCD的初始化函数后面,因为笔者遇到过这样的问题,原因不明。IIC的初始化函数写在LCD的初始化函数前面,可能会出现数据读写错误的问题。
总结
这是笔者的第一篇知识分享文章,因为不同人的基础都是不一样的,所以笔者尽量把内容写得详细一点,可能有些地方没有考虑到,由于内容都是笔者自己亲写的,第一次写,水平有限,有些语句可能读起来不是那么的通顺,或者有点逻辑上的矛盾,一些概念说的可能不是那么的准确。如果可能,后续会进行校对。
程序大部分都是经过笔者测试的,但可能有小量程序,由于时间问题,笔者没有进行测试。
如有一些不同的建议和想法,或者是发现笔者一些程序内容上的问题,欢迎评论区讨论留言。
读者后续也会出相应的教程,感兴趣的小伙伴,点个关注呀!