蓝桥杯嵌入式(G431RBT6): EEPROM进阶学习

本文详细介绍了如何使用IIC协议对EEPROM进行单字节和多字节的读写操作,包括基本数据类型及复合类型如数组、结构体的处理。讲解了如何处理浮点型数据,以及如何处理初次上电的默认值设定,并提醒了延时问题和可能出现的程序bug。此外,还提供了多字节读写方式在处理大范围、高精度数据时的优势。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


前言

    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数据类型字节大小,方便大家进行相应的读写操作。

类型字节大小
char1
short2
int4
long4
float4
double8

局限性:
    多字节的读写方式虽然方法简单也很通用,但对于读写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的初始化函数前面,可能会出现数据读写错误的问题。

总结

    这是笔者的第一篇知识分享文章,因为不同人的基础都是不一样的,所以笔者尽量把内容写得详细一点,可能有些地方没有考虑到,由于内容都是笔者自己亲写的,第一次写,水平有限,有些语句可能读起来不是那么的通顺,或者有点逻辑上的矛盾,一些概念说的可能不是那么的准确。如果可能,后续会进行校对。
    程序大部分都是经过笔者测试的,但可能有小量程序,由于时间问题,笔者没有进行测试。
    如有一些不同的建议和想法,或者是发现笔者一些程序内容上的问题,欢迎评论区讨论留言。
    读者后续也会出相应的教程,感兴趣的小伙伴,点个关注呀!
在这里插入图片描述

评论 16
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值