AT24C02介绍
AT24C02是一个2K位串行CMOS E2PROM,内部含有256个8位字节,采用先进CMOS技术实质上减少了器件的功耗。AT24C02有一个8字节页写缓冲器,该器件通过IIC总线接口进行操作,有一个专门的写保护功能。是一种可以实现掉电不丢失的存储器,可用于保存单片机运行时想要永久保存的数据信息,储存介质是E2PROM
AT24C02的引脚图
应用电路
内部框架图
其主要主体是用于电可擦除可编程ROM,意思就是可以通过上电并且编程的模式来实现数据的存储以及读出,存储在内部的地址总共有256个,每个地址可以存储八位字节的数据
AT24C02是通过I2C来进行数据的传输进去以及读取出来,了解I2C也是学会使用AT24C02的重要一环,下面我来介绍I2C的主要内容及传输数据时的时序图及具体可实现代码
I2C总线的介绍
I2C是什么
在消费电子,工业电子等领域,会使用各种类型的芯片,如微控制器,电源管理,显示驱动,传感器,存储器,转换器等,他们有着不同的功能,有时需要快速的进行数据的交互,为了使用最简单的方式使这些芯片互联互通,于是I2C诞生了,I2C(Inter-Integrated Circuit)是一种通用的总线协议。它是由Philips(飞利浦)公司,现NXP(恩智浦)半导体开发的一种简单的双向两线制总线协议标准。
通用的I2C总线,可以使各种设备的通信标准统一,对于厂家来说,使用成熟的方案可以缩短芯片设计周期、提高稳定性,对于应用者来说,使用通用的通信协议可以避免学习各种各样的自定义协议,降低了学习和应用的难度
工作原理图,通过I2C配制地址可以控制多个结点的数据传输
所有I2C设备的SCL连在一起,SDA连在一起
设备的SCL和SDA均要配置成开漏输出模式 SCL和SDA各添加一个上拉电阻,阻值一般为4.7KΩ左右 开漏输出和上拉电阻的共同作用实现了“线与”的功能,此设计主要是为了解决多机通信互相干扰的问题
下面为电路的结构框图
I2C主要是操控SDA和SCL来控制输出为,SCL是串行时钟接口,通过SCL的时序变化,相应的数据就会传到SDA上,这取决于不同模式下是虚的不同
I2C的时序结构
最重要的一句话是:当SCL处于高电平时SDA的值不能改变(数据传输时),因为SCL在高电平时SDA的值如果改变那就证明这时I2C处于起始条件或者终止条件的判断中,这时不会向某个地址出传某个数据,如果在传输数据时出现这样的情况可能会导致数据的丢失。
I2C起始条件(发送开始信号)
SCL高电平期间,SDA从高电平切换到低电平
先把SDA置1(高电位)保证串行数据线的初始状态为1,随后把SCL置1,他们两同时保证高电位时按图上先把SDA置0,再把SCL置0,这样开始信号就发送完成了
I2C终止条件(发送终止信号)
SCL高电平期间,SDA从低电平切换到高电平
首先把SDA置低电平,再把SCL置高电平,这样在SCL高电平时SDA再置高电平,在SCL为搞得时候SDA发生了低到高的信号,此时向I2C发送了终止信号
I2C发送一个字节
SCL低电平期间,主机将数据位依次放到SDA线上(高位在前),然后拉高SCL,从机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可发送一个字节
这里主机先把要发送的数据位先放到SDA上,再拉高SCL,从机就会自动读取SDA上的数据
void I2C_SendByte(unsigned char Byte) //发送一个字节
{
unsigned char i=0;
for(i=0;i<8;i++)
{
I2C_SDA=Byte&(0x80>>i); //逐位发送
I2C_SCL=1; //拉高电位发送这一位的数据
I2C_SCL=0; //恢复电位以便下一个发送时能拉高产生上升沿
}
}
I2C接收一个字节
SCL低电平期间,从机将数据位依次放到SDA线上(高位在前),然后拉高SCL,主机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可接收一个字节(主机在接收之前,需要释放SDA)
在接收数据时是依靠从机控制我们的数据位,我们只要控制串行时钟即可按位读取到数据,这里把串行时钟置1就可以把数据从SDA上读取(SCL置高电位后,从机会把数据转移到SDA上,这时我们只需要取值即可)
unsigned char I2C_ReceiveByte()
{
unsigned char i,Data=0x00; //定义一个变量来接收数据
I2C_SDA=1; //这里读取数据SDA的控制权已经交给了AT24C02芯片只需一开始置1,在读数据时把SCL拉高表示主机释放SDA
for(i=0;i<8;i++)
{
I2C_SCL=1; //串行时钟置1接收数据
if(I2C_SDA) //当接收到的数据是1时,把对应位变成1
{
Data|=(0x80>>i);
}
I2C_SCL=0; //置零方便下次上升沿出现
}
I2C_SCL=0; //最后把串行时钟置回低电位
return Data;
}
在I2C发送数据或者接收数据时需要一个应答位来表示你是否接收到信息
I2C发送应答
在接收完一个字节之后,主机在下一个时钟发送一位数据(这时主机要想从机发送应答位),数据0表示应答,数据1表示非应答
I2C接收应答
接收应答:在发送完一个字节之后,主机在下一个时钟接收一位数据,判断从机是否应答,数据0表示应答,数据1表示非应答(主机在接收之前,需要释放SDA)
void I2C_SendAck(unsigned char AckBit)
{
I2C_SDA=AckBit; //发送应答
I2C_SCL=1;
I2C_SCL=0;
}
unsigned char I2C_ReceivedAck()
{
unsigned char AckBit;
I2C_SDA=1; //释放SDA让芯片进行操控
I2C_SCL=1; //拉高串行时钟即接收数据
AckBit=I2C_SDA;
I2C_SCL=0;
return AckBit;
}
I2C的数据帧
I2C数据帧主要完成的任务有:向谁(从机地址(这里我们用的是AT24C02))哪个位置(数据存储的位置)发送/接收什么数据
通过Start激活I2C随后发送从机的位置,发送应答,再发送数据存储的位置,发送应答,再把数据发送过去即写入数据完成
读取数据也是通过Start激活I2C随后发送从机的位置,发送应答,再发送数据存储的位置,发送应答,最后把数据读取出来即读取数据完成
这里介绍完I2C的写入和读出再介绍一下I2C与AT24C02搭配着应用
使用AT24C02主要是为了能把数据存储到AT24C02内部和从内部读出数据,这就需要我们的I2C来配合传输数据
写入数据到AT24C02
写入数据按照上面的步骤用I2C来传输数据,这里用到AT24C02主要就只有它的从机地址
这里复述一遍写入的过程
首先发送I2C的开始条件,发送从机的地址(这里AT24C02的地址是0xA0),接收从机发送的应答,发送数据需要存储在哪个位置上,接收从机应答,最后再发送数据,接收从机应答,即可关闭I2C
还要注意的一点是AT24C02在写入时它需要时间,这个时间内你不能对数据进行读取或者别的操作,这里官方定义的写入最长时间是5ms,所以我们在写入数据时还需要延时一段时间才能进行下一步操作
void AT24C02_WriteByte(unsigned char WordAddress,Data)
{
I2C_Start(); //给开始指令进行工作
I2C_SendByte(AT24C02_Address); //先发送从机地址(需要操控的芯片的地址)
I2C_ReceivedAck(); //接受应答
I2C_SendByte(WordAddress); //发送字节存储的地址
I2C_ReceivedAck();
I2C_SendByte(Data); //发送数据
I2C_ReceivedAck();
I2C_Stop();
}
从AT24C02读出数据
读出数据也是通过I2C传输
过程如下
首先先发送I2C开始工作的指令,发送从机地址(读的操作要在从机地址的最后一位加上一),应答,发送要读取的数据的地址,接收应答,把数据读取出来后向从机发送应答,最后关闭I2C
unsigned char AT24C02_ReadByte(unsigned char WordAddress)
{
unsigned char Data;
I2C_Start(); //给开始指令进行工作
I2C_SendByte(AT24C02_Address); //先发送从机地址(需要操控的芯片的地址)
I2C_ReceivedAck(); //接受应答
I2C_SendByte(WordAddress); //发送字节存储的位置(在哪里)
I2C_ReceivedAck();
I2C_Start();
I2C_SendByte(AT24C02_Address|0x01); //发送向AT24C02读取数据的命令(这里与上0x01就是最后一位置1表示读数据)
I2C_ReceivedAck();
Data=I2C_ReceiveByte();
I2C_SendAck(1);
I2C_Stop();
return Data;
}
这里再通过按键的功能尝试一下数据的写入和读出功能在AT24C02中的效果
AT24C02实践
#include <REGX52.H>
#include "LCD1602.h"
#include "AT24C02.h"
#include "Delay.h"
#include "Key.h"
unsigned char Data,KeyNum;
unsigned int Num;
void main()
{
LCD_Init();
/*
//测试读写功能
AT24C02_WriteByte(1,233);
Delay(5);
Data=AT24C02_ReadByte(1);
LCD_ShowNum(2,1,Data,3);
*/
LCD_ShowNum(1,1,Num,5);
while(1)
{
KeyNum=Key();
if(KeyNum==1)
{
Num++;
LCD_ShowNum(1,1,Num,5);
}
if(KeyNum==2)
{
Num--;
LCD_ShowNum(1,1,Num,5);
}
if(KeyNum==3) //写入AT24C02中
{
AT24C02_WriteByte(0,Num%256);
/*这里的unsigned int 的Num分来两位来存储,因为存储器中是以八个字节大小的ROM来存储数据的,而unsigned int的大小是16个字节,这里低位存储在地址0内
高位存储在地址1上*/
Delay(5); //写完数据之后要等待它写入,Delay个5ms不进行其他操作
AT24C02_WriteByte(1,Num/256);
Delay(5);
LCD_ShowString(2,1,"Write OK");
Delay(1000);
LCD_ShowString(2,1," ");
}
if(KeyNum==4) //读出
{
Num=AT24C02_ReadByte(0);
Num|=(AT24C02_ReadByte(1)<<8);
LCD_ShowNum(1,1,Num,5);
LCD_ShowString(2,1,"Read OK ");
Delay(1000);
LCD_ShowString(2,1," ");
}
}
}
再附上按键的代码
#include <REGX52.H>
#include "Delay.h"
unsigned char Key() //获取按键键码
{
unsigned char KeyNum=0;
if(P3_1==0) //按键1
{
Delay(20);
while(P3_1==0);
Delay(20);
KeyNum=1;
}
if(P3_0==0) //按键2
{
Delay(20);
while(P3_0==0);
Delay(20);
KeyNum=2;
}
if(P3_2==0) //按键3
{
Delay(20);
while(P3_2==0);
Delay(20);
KeyNum=3;
}
if(P3_3==0) //按键3
{
Delay(20);
while(P3_3==0);
Delay(20);
KeyNum=4;
}
return KeyNum;
}
还有LCD显示
#include <REGX52.H>
//引脚配置:
sbit LCD_RS=P2^6;
sbit LCD_RW=P2^5;
sbit LCD_EN=P2^7;
#define LCD_DataPort P0
//函数定义:
/**
* @brief LCD1602延时函数,12MHz调用可延时1ms
* @param 无
* @retval 无
*/
void LCD_Delay()
{
unsigned char i, j;
i = 2;
j = 239;
do
{
while (--j);
} while (--i);
}
/**
* @brief LCD1602写命令
* @param Command 要写入的命令
* @retval 无
*/
void LCD_WriteCommand(unsigned char Command)
{
LCD_RS=0;
LCD_RW=0;
LCD_DataPort=Command;
LCD_EN=1;
LCD_Delay();
LCD_EN=0;
LCD_Delay();
}
/**
* @brief LCD1602写数据
* @param Data 要写入的数据
* @retval 无
*/
void LCD_WriteData(unsigned char Data)
{
LCD_RS=1;
LCD_RW=0;
LCD_DataPort=Data;
LCD_EN=1;
LCD_Delay();
LCD_EN=0;
LCD_Delay();
}
/**
* @brief LCD1602设置光标位置
* @param Line 行位置,范围:1~2
* @param Column 列位置,范围:1~16
* @retval 无
*/
void LCD_SetCursor(unsigned char Line,unsigned char Column)
{
if(Line==1)
{
LCD_WriteCommand(0x80|(Column-1));
}
else if(Line==2)
{
LCD_WriteCommand(0x80|(Column-1+0x40));
}
}
/**
* @brief LCD1602初始化函数
* @param 无
* @retval 无
*/
void LCD_Init()
{
LCD_WriteCommand(0x38);//八位数据接口,两行显示,5*7点阵
LCD_WriteCommand(0x0c);//显示开,光标关,闪烁关
LCD_WriteCommand(0x06);//数据读写操作后,光标自动加一,画面不动
LCD_WriteCommand(0x01);//光标复位,清屏
}
/**
* @brief 在LCD1602指定位置上显示一个字符
* @param Line 行位置,范围:1~2
* @param Column 列位置,范围:1~16
* @param Char 要显示的字符
* @retval 无
*/
void LCD_ShowChar(unsigned char Line,unsigned char Column,char Char)
{
LCD_SetCursor(Line,Column);
LCD_WriteData(Char);
}
/**
* @brief 在LCD1602指定位置开始显示所给字符串
* @param Line 起始行位置,范围:1~2
* @param Column 起始列位置,范围:1~16
* @param String 要显示的字符串
* @retval 无
*/
void LCD_ShowString(unsigned char Line,unsigned char Column,char *String)
{
unsigned char i;
LCD_SetCursor(Line,Column);
for(i=0;String[i]!='\0';i++)
{
LCD_WriteData(String[i]);
}
}
/**
* @brief 返回值=X的Y次方
*/
int LCD_Pow(int X,int Y)
{
unsigned char i;
int Result=1;
for(i=0;i<Y;i++)
{
Result*=X;
}
return Result;
}
/**
* @brief 在LCD1602指定位置开始显示所给数字
* @param Line 起始行位置,范围:1~2
* @param Column 起始列位置,范围:1~16
* @param Number 要显示的数字,范围:0~65535
* @param Length 要显示数字的长度,范围:1~5
* @retval 无
*/
void LCD_ShowNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length)
{
unsigned char i;
LCD_SetCursor(Line,Column);
for(i=Length;i>0;i--)
{
LCD_WriteData(Number/LCD_Pow(10,i-1)%10+'0');
}
}
/**
* @brief 在LCD1602指定位置开始以有符号十进制显示所给数字
* @param Line 起始行位置,范围:1~2
* @param Column 起始列位置,范围:1~16
* @param Number 要显示的数字,范围:-32768~32767
* @param Length 要显示数字的长度,范围:1~5
* @retval 无
*/
void LCD_ShowSignedNum(unsigned char Line,unsigned char Column,int Number,unsigned char Length)
{
unsigned char i;
unsigned int Number1;
LCD_SetCursor(Line,Column);
if(Number>=0)
{
LCD_WriteData('+');
Number1=Number;
}
else
{
LCD_WriteData('-');
Number1=-Number;
}
for(i=Length;i>0;i--)
{
LCD_WriteData(Number1/LCD_Pow(10,i-1)%10+'0');
}
}
/**
* @brief 在LCD1602指定位置开始以十六进制显示所给数字
* @param Line 起始行位置,范围:1~2
* @param Column 起始列位置,范围:1~16
* @param Number 要显示的数字,范围:0~0xFFFF
* @param Length 要显示数字的长度,范围:1~4
* @retval 无
*/
void LCD_ShowHexNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length)
{
unsigned char i,SingleNumber;
LCD_SetCursor(Line,Column);
for(i=Length;i>0;i--)
{
SingleNumber=Number/LCD_Pow(16,i-1)%16;
if(SingleNumber<10)
{
LCD_WriteData(SingleNumber+'0');
}
else
{
LCD_WriteData(SingleNumber-10+'A');
}
}
}
/**
* @brief 在LCD1602指定位置开始以二进制显示所给数字
* @param Line 起始行位置,范围:1~2
* @param Column 起始列位置,范围:1~16
* @param Number 要显示的数字,范围:0~1111 1111 1111 1111
* @param Length 要显示数字的长度,范围:1~16
* @retval 无
*/
void LCD_ShowBinNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length)
{
unsigned char i;
LCD_SetCursor(Line,Column);
for(i=Length;i>0;i--)
{
LCD_WriteData(Number/LCD_Pow(2,i-1)%2+'0');
}
}
延时函数
#include <REGX52.H>
void Delay(int xms)
{
unsigned char i, j;
while(xms--)
{
i = 2;
j = 239;
do
{
while (--j);
} while (--i);
}
}