在学习蓝桥杯的时候,经常会用的EEPROM来保存调用数据。我在写程序的过程中,会出现读取失败或者是写入数据不准确的情况。在这里我总结了一种可以很稳定读写EEPROM的方法
蓝桥杯开发板上使用的存储芯片为24C02,可以把它简单理解为一个专门用来保存数据用的芯片,里面有256个字节的空间(00-FF)。它通过IIC协议与单片机进行通信(收发数据),且保存在里面的数据掉电也不会丢失。想要向它写入或者读取数据,我们需要简单了解一下IIC通信协议,想要写好与各种通信协议有关的程序,必须严格按照时序要求
。
IIC协议是一种双向、两线串行通讯接口,分别是串行数据线SDA和串行时钟线SCL.两根线都必须通过一个上拉电阻接到电源
总线上发送数据的器件被称作发送器,接收数据的器件被称作接收器。控制信息交换的器件被称作主器件,受主器件控制的器件则被称作从器件。主器件产生串行时钟SCL,控制总线的访问状态、产生START和STOP条件。24C02在IIC总线中作为从器件工作。只有当总线处于空闲状态时才可以启动数据传输。每次数据传输均开始于START条件,结束于STOP条件,二者之间的数据字节数是没有限制的,由总线上的主器件决定。信息以字节(8位)为单位传输,第9位时由接收器产生应答。
IIC是一种通信协议,由主器件(单片机)控制两条线:时钟线SCL,数据线SDA进行数据的收发,24C02作为从器件接收
我们要使用IIC协议来进行数据的收发,我们只需要记住读写的时序即可。
写时序
字节写
字节写是常用的写操作。我简单概括一下,有关字节写具体的描述见手册
1.发送起始命令
2.发送器件地址和写命令0xa0*
高四位为器件地址固定,A2-A0接地,最后一位为读写控制,写是0读是1,所以是0xa0;
3.等待应答信号 (即拉低SDA)
4.发送字节地址(你数据想要保存的位置0x00-0xff)
5.等待应答信号
6.发送保存的数据
7.等待应答信号
8.发送停止信号
以上每个步骤都对应一个函数,这些函数在比赛会提供的数据包里的驱动文件里(IIC.c,IIC.h)
代码示例
//按照上面步骤写一个往EEPROM写数据的函数
void Write_eep(uchar addr,uchar dat) //定义两个形参,一个代表想要写入的位置,一个代表想要写入的数据。引入形参,我们可以再调用此函数时设定任意的地址和数据,如果我想把我测的温度数据保存在0x00位置上,那么就可以*Write_eep(0x00,temp)*
{
IIC_Start(); //起始信号
IIC_SendByte(0xa0); //发送器件地址和写命令
IIC_WaitAck(); //等待应答信号
IIC_SendByte(addr); //发送你想要保存的位置
IIC_WaitAck();
IIC_SendByte(value); //发送要保存的数据
IIC_WaitAck();
IIC_Stop(); //发送停止信号
}
读时序
随机读
随机读就是读取指定位置的数据,需要提供地址,是比较常用的读操作。我还是简单概括一下,感兴趣的伙伴可以查看24C02的读操作。
由上图可以看出,前半段就是写一个地址(你要读取的数据的位置),和写操作完全一样,后半段是读操作。
1.发送起始命令
2.发送器件地址和写指令(0xa0)
3.等待应答
4.发送想要读取的数据所在地址
5.等待应答
6.发送起始命令
7发送器件地址和读操作(0xa1)
8.等待应答
9.调用读取函数,读出数据
10.发送一个非应答信号(即拉高SDA,NO ACK)
11.发送停止信号
代码示例
//接上一个示例,把保存的温度数据给读出来
uchar Read_eep(uchar adde) //此函数有返回值,返回value
{
unsigned char value; //定义一个变量,用来保存读取的数据,并返回出去
IIC_Start(); //起始信号
IIC_SendByte(0xa0); //发送器件地址和写命令
IIC_WaitAck(); //等待应答信号
IIC_SendByte(addr); //发送你想要读取的位置
IIC_WaitAck();
IIC_Start();
IIC_SendByte(0xa1); //发送器件地址和读命令
IIC_WaitAck();
value=IIC_RecByte(); //调用函数读取数据
IIC_SendAck(1); //发送一个非应答信号
IIC_Stop();
return value; //返回value
}
实际练习
使用蓝桥杯开发板,模拟显示温度值(小数点后保留两位),按下S5保存当前温度数据,按下S6回显保存的数据,再次按下S6返回温度显示。
[[include]] "stc15f2k60s2.h"
[[include]] "iic.h"
[[include]] "intrins.h"
[[define]] uchar unsigned char
[[define]] uint unsigned int
sbit S6=P3^1;
sbit S5=P3^2;
void Display_temp();
void Display_dat();
void IintTimer0();
void keyscan();
uchar code SEG_duan[]={0xc0,0xf9,0xa4,0xb0,0x99,0x92,0x82,0xf8,0x80,0x90,0xc6,0x89};//后两位为C和H的段码
uchar SEG_buf[8]={0};//定义一个数组作为缓存区
uint dat=2340; //模拟温度值为2340,在3的位置加上小数点就可以了
uint temp; //保存读取的温度值
bit flag=0; //S6按下的标志位
void main()
{
IintTimer0();
while(1)
{
keyscan();
if(flag==0) //初始为0,显示初始温度数据
Display_dat();
else
Display_temp();//S6按下了,显示读取的温度数据
}
}
void Delay10ms() //@12.000MHz
{
unsigned char i, j;
i = 117;
j = 184;
do
{
while (--j);
} while (--i);
}
void hc138(uchar n)
{
switch(n)
{
case 4: P2= (P2&0x1f) | 0x80;
break;
case 5: P2= (P2&0x1f) | 0xa0;
break;
case 6: P2= (P2&0x1f) | 0xc0;
break;
case 7: P2= (P2&0x1f) | 0xe0;
break;
}
}
void SEG_bit(uchar pos,uchar value)
{
hc138(6);
P0= 0x01 << pos;
hc138(7);
P0=value;
}
/*因为IIC对时序要求较为严格,所以在执行有关RRPROM函数的时候关闭中断,
有关IIC的函数里有延时函数,所以我们在延时里面打开中断,这样写更有效率*/
uchar Read_epp(uchar addr)
{
uchar value=0;
EA=0;
IIC_Start();
IIC_SendByte(0xa0);
IIC_WaitAck();
IIC_SendByte(addr);
IIC_WaitAck();
IIC_Start();
IIC_SendByte(0xa1);
IIC_WaitAck();
value=IIC_RecByte();
IIC_SendAck(1);
IIC_Stop();
EA=1;
return value;
}
void Write_eep(uchar addr,uchar value)
{
EA=0;
IIC_Start();
IIC_SendByte(0xa0);
IIC_WaitAck();
IIC_SendByte(addr);
IIC_WaitAck();
IIC_SendByte(value);
IIC_WaitAck();
IIC_Stop();
EA=1;
}
void baocun()
{
uchar i,j;
j=2350/100; //因为温度值是四位数,超过了一个字节,分为两个
i=2350%100; //字节逐个保存,为验证是否保存了数据,这里假设温度为23.50
Write_eep(0x00,i); //保存地址选择0x00,把dat的高八位先保存(保存顺序可随意)
Delay10ms(); //手册说明,当接受完一个字节的数据后,EEPROM进入内部写周期
Write_eep(0x01,j); //此时任何输入无效,所以我们要等待这个周期过去,经过我的测试
Delay10ms(); //这个周期的时间大概在几个微妙,具体时间大家可以去测试一下。
}
void duqu()
{
uchar i,j;
j=Read_epp(0x00); //读出放在这个位置的高八位数据
i=Read_epp(0x01);
temp=i*100+j;
}
void Display_dat()
{
SEG_buf[0]=SEG_duan[10];
SEG_buf[1]=0xff;
SEG_buf[2]=0xff;
SEG_buf[3]=0xff;
SEG_buf[4]=SEG_duan[dat/1000]; //提取个十百千位
SEG_buf[5]=SEG_duan[(dat/100)%10]-0x80; //因为要显示小数点,所以把最高位置低。
SEG_buf[6]=SEG_duan[(dat/10)%10];
SEG_buf[7]=SEG_duan[dat%10];
}
void Display_temp()
{
SEG_buf[0]=SEG_duan[11];
SEG_buf[1]=SEG_duan[10];
SEG_buf[2]=0xff;
SEG_buf[3]=0xff;
SEG_buf[4]=SEG_duan[temp/1000]; //提取个十百千位
SEG_buf[5]=SEG_duan[(temp/100)%10]-0x80;
SEG_buf[6]=SEG_duan[(temp/10)%10];
SEG_buf[7]=SEG_duan[temp%10];
}
void keyscan()
{
if(S5==0)
{
Delay10ms();
if(S5==0)
{
while(S5==0); //等待弹起,避免重复执行
baocun(); //按键弹起,保存温度值
}
}
if(S6==0)
{
Delay10ms();
if(S6==0)
{
while(S6==0);
duqu();
flag=~flag; //S6按下一次,标志位取反即0变1或者1变零
}
}
}
void IintTimer0()
{
TMOD=0x00;
TH0=(65536-2000)/255;
TL0=(65536-2000)%255;
ET0=1;
EA=1;
TR0=1;
}
void Timer0() interrupt 1
{
static uchar i=0;
SEG_bit(i,SEG_buf[i]);//通过改变SEG_buf内的数据,可以显示你想要的内容
i++;
i&=0x07;
}