写在前面:
I2C可以说是单片机学习中听到最多的一个名词之一,也是最能考察一个人对单片机的掌握程度。那么为何I2C在单片机的学习中能起到举足轻重的作用呢?博主认为主要有两点:1、I2C的使用非常多;2、I2C的学习具有一定的难度;但是往往是这种难的东西才能体现出一个人的学习水平,博主在此将自己学习、理解的最细致的I2C知识写给大家;大家一定要认真的读完;
目录
一、I2C介绍
I2C总线是一个两线式串口总线,用于连接微控制器(MCU)以及其他外设;
I2C通信是一种同步、半双工、带数据应答的通信方式;特点:接口线少,控制方式简单,速率高等;
I2C 总线只有两根双向信号线。一根是数据线 SDA,另一根是时钟线 SCL。
I2C 通信通过一根时钟线和一根数据线在连接总线的两个设备之间进行信息的传递,为设备之间数据交换提供了一种简单高效的方法。每个连接到总线上的设备都有唯一的地址,任何器件既可以作为主机也可以作为从机;
一般情况下,主设备选用单片机(MCU),从设备选用(其他外设);
1.1 I2C物理层
上面说了,I2C总线上可以挂在多个需要通信的设备,这些设备通过两个线(SDA与SCL)进行连接,从而实现两两之间的通信。此时,大家的第一疑问应该是,如何做到准确的两两通信,而且其他设备还互不影响???
这就不得不进入I2C总线内部,看看它的电路是如何工作的,如何实现两两通信。
I2C电路规范:
所有I2C设备的SCL和SDA连接在一起;
设备的SCL和SDA配置为开漏输出模式;
SCL和SDA线上各有一个上拉电阻;
开漏输出与上拉电阻共同实现了“线与”功能;
首先给大家介绍一下什么叫做:开漏输出与上拉电阻;
上图为上拉电阻与开漏输出的电路图;
上拉电阻:打开开关时,输出通过电阻达到高电平,但是这种驱动能力是弱的,因此也称为弱上;关闭开关时,输出直接与地相连,达到低电平。
开漏输出:打开开关时,输出左端电路断开,电压不稳定,也称为高阻抗状态;关闭开关时,输出直接与地相连,达到低电平。
通过上拉电阻与开漏输出的联合作用,SDA与SCL可以被主从设备拉为低电平,但是不能被驱动为高电平,高电平需要上拉电阻作用。“线与”也就是说当总线上没有设备进行通信时(设备全部断开)总线才会被上拉电阻作用在高电平;
I2C两设备进行通信的流程为:首先主机控制总线,通过总线时序找到想要通信的从设备(每个从设备都有固定地址),被选中的从机准备发送/接收收据,未被选中的从机打开上述开关,断绝同总线的联系。主机与从机传递数据,如果传递1直接打开开关通过上拉电阻实现总线高电平,如果传递0关闭开关,使总线达到低电平。主从机设备传递信息严格遵循相应的时序。
相关术语:
主机:启动数据传送,产生时钟信号的设备;
从机:被主机寻址的设备;
同步:两个或多个器件同步时钟信号的过程;
多主机:同时有多于一个主机尝试控制总线但不破坏传输;
仲裁:是一个在有多个主机同时尝试控制总线但只允许其中一个控制总线并使传输不被破坏的过程;
1.2 I2C协议层
在物理层我们分析了如何让在众多设备中选择两个设备进行通信,而且不受其他设备影响。而在协议层我们要实现的是,当确定主从设备后,如何通过两条线实现主从设备双方达到良好的通信效果;
I2C 的协议定义了通信的起始和停止信号、数据有效性、响应、仲裁、时钟同步和地址广播等环节。其中I2C的时序结构是学习的关键,I2C相关的时序结构共6份,分别为:
1、起始条件与结束条件
起始条件:SCL高电平期间,SDA从高电平切换到低电平;
结束条件:SCL高电平期间,SDA从低电平切换到高电平;
这也就是传递数据开始之前与传递数据结束之后,主设备需要做出的标志;
当主从设备为51单片机与AT24C02时对应的代码为:
起始条件:
I2C_SDA=1;//先保证SDA与SCL处于高电平
I2C_SCL=1;
I2C_SDA=0;//SCL高电平期间,SDA从高电平切换到低电平;
I2C_SCL=0; //SCL置0;结束条件:
I2C_SDA=0;//先保证SDA处于低电平,SCL基本上不会处于高电平,因为置高后会马上置低;
I2C_SCL=1;//SCL高电平期间,SDA从高电平切换到低电平;
I2C_SDA=1;
2、发送一个字节
发送一个字节:SCL低电平期间,主机将数据位依次放在SDA线上(高位在前),然后拉高SCL,从机将在SCL高位期间读取数据位,所以SCL高位期间不允许SDA数据发生变化,依次进行循环上图8次,实现一个字节数据的传输。
当主从设备为51单片机与AT24C02时对应的代码为:
发送一个字节
for(i=0;i<8;i++)//循环8次,高位在前,低位在后;
{
I2C_SDA=Byte&(0x80>>i);//机将数据位依次放在SDA线上;
I2C_SCL=1;//拉高SCL,从机将在SCL高位期间读取数据;
I2C_SCL=0;//将SCL拉低;
}
3、接收一个字节
接收一个字节:SCL低电平期间,从机将数据位依次放在SDA线上,(高位在前),然后拉高SCL,主机将在SCL高电平期间读取数据,所以SCL高电平期间,不允许SDA数据发生变化,依次循环8次,即可接受接收一个字节(主机在前接受前,需要释放(SDA)。
当主从设备为51单片机与AT24C02时对应的代码为:
接收一个字节
for(i=0;i<8;i++)//循环8次,高位在前,低位在后;
{
I2C_SDA=1;//主机释放SDA;
I2C_SCL=1;//拉高SCL,主机将在SCL高电平期间读取数据;
if(I2C_SDA) {Byte|=(0x80>>i);}//判断SDA数据是0还是1;
I2C_SCL=0;//将SCL拉低;
}
return Byte;//将接受到的字节返回;
4、发送应答与接收应答
发送应答:在接收完一个字节后,主机在下一个时钟发送一位数据,数据0表示应答,数据1表示非应答;
接收应答:在发送完一个字节之后,主机在下一个时钟接收一位数据,判断从机是否应答,数据0表示应答,数据1表示非应答(主机在接收之前,应该释放SDA);
当主从设备为51单片机与AT24C02时对应的代码为:
发送应答:
I2C_SDA=AckBit; //主机发送一位数据,表示是否接收到收据
I2C_SCL=1;//在SCL高电平期间读取数据
I2C_SCL=0;接收应答:
I2C_SDA=1; //主设备释放SDA
I2C_SCL=1;//在SCL高电平期间读取数据
AckBit=I2C_SDA;//读取一位来自从设备是否接收的数据
I2C_SCL=0;
return AckBit;//返回接收到的数值
通过上述的六种时序,就能完成主设备与从设备之间进行基本的收发一帧数据;
a、主机向从机发送数据
b、主机接收从机数据
c、发送数据后又接收数据
到这里我们就介绍完 I2C 总线,主要是学习如何通信,确定哪两个通信,主从设备如何通信的问题;
二、 AT24C02介绍
AT24C02是一种可以实现掉电不丢失的存储器,可以用于保存单片机运行时想要保存的数据信息;
该器件通过 I2C 总线接口进行 操作,它有一个专门的写保护功能。我们开发板上使用的是 AT24C02(EEPROM) 芯片,此芯片具有 I2C 通信接口,芯片内保存的数据在掉电情况下都不丢失, 所以通常用于存放一些比较重要的数据等
存储介质:E2PROM;
通讯接口:I2C通信接口;
容量:256字节;
芯片引脚说明:
1 | A0 | 地址输入 |
2 | A1 | 地址输入 |
3 | A2 | 地址输入 |
4 | VSS | 电源地 |
5 | SDA | 串行地址和数据输入输出 |
6 | SCL | 串行时钟输入 |
7 | WP | 写保护 |
8 | VCC | 电源正极 |
AT24C02 器件地址为 7 位,高 4 位固定为 1010,低 3 位由 A0/A1/A2 信号线的电平决定。 因为传输地址或数据是以字节为单位传送的,当传送地址时, 器件地址占 7 位,还有最后一位(最低位 R/W)用来选择读写方向,它与地址 无关。其格式如下:
1 | 0 | 1 | 0 | A2 | A1 | A0 | R/W |
开发板已经将芯片的 A0/A1/A2 连接到 GND,所以器件地址为 1010000,即 0x50(未计算最低位)。如果要对芯片进行写操作时,R/W 即为 0, 写器件地址即为 0XA0;如果要对芯片进行读操作时,R/W 即为 1,此时读器件 地址为 0XA1。开发板上也将 WP 引脚直接接在 GND 上,此时芯片允许数据正常读写。
AT24C02数据帧
1、写入一字节数据
2、读取一字节数据
三、软件实现
功能说明:
系统运行时,数码管左 3 位显示 001;
按 K1 键将数据写入到 AT24C02 内保存;
按 K2 键读取 EEPROM 内保存的数据;
按 K3 键显示数据加 1;
按 K4 键显示数据清零,最大能写入的数据是 255。
源码:
main.c
#include <REGX52.H>
#include "key.h"
#include "delay.h"
#include "LCD1602.h"
#include "AT24C02.h"
#include "I2C.h"
unsigned char Data,KEY;
void main ()
{
LCD_Init();
Data=1;
LCD_ShowNumber(1,1,Data,3);
while (1)
{
KEY=key_scan();
if(KEY==1)//按下K1,将数据存入
{
AT24C02_WriteByte(0,Data);//将数据存入AT2402第0个字节中
Delay1ms(5);
LCD_ShowString(2,1,"Write OK");//存入后显示写入成功;
Delay100ms(20);
LCD_ShowString(2,1," ");
}
if(KEY==2)//按下K2,将数据取出
{
Data=AT24C02_ReadByte(0);
LCD_ShowNumber(1,1,Data,3);
LCD_ShowString(2,1,"Read OK");
Delay100ms(20);
LCD_ShowString(2,1," ");
}
if(KEY==3)//按下K3,将数据加1
{
Data++;
LCD_ShowNumber(1,1,Data,3);
}
if(KEY==4)//按下K4,将数据减1
{
Data--;
LCD_ShowNumber(1,1,Data,3);
}
}
}
AT24C02.c
#include <REGX52.H>
#include "I2C.h"
#define AT24C02_ADDRESS 0XA0
/**
*@breaf AT24C02写入一个字节
*@param WordAddress写入字节的地址
*@retval Data写入字节的数据
*/
void AT24C02_WriteByte(unsigned char WordAddress,Data)
{
I2C_Start();
I2C_SendByte(AT24C02_ADDRESS);
I2C_ReceiveAck();
I2C_SendByte(WordAddress);
I2C_ReceiveAck();
I2C_SendByte(Data);
I2C_ReceiveAck();
I2C_Stop();
}
/**
*@breaf AT24C02读取一个字节
*@param WordAddress读取字节的地址
*@retval Byte读取字节的内容
*/
unsigned char AT24C02_ReadByte(unsigned char WordAddress)
{
unsigned char Byte;
I2C_Start();
I2C_SendByte(AT24C02_ADDRESS);
I2C_ReceiveAck();
I2C_SendByte(WordAddress);
I2C_ReceiveAck();
I2C_Start();
I2C_SendByte(AT24C02_ADDRESS|0x01);
I2C_ReceiveAck();
Byte= I2C_ReceiveByte();
I2C_SendAck(1);
I2C_Stop();
return Byte;
}
I2C.c
#include <REGX52.H>
//定义端口
sbit I2C_SCL=P2^1;
sbit I2C_SDA=P2^0;
/**
*@breaf I2C开始函数
*@param 无
*@retval 无
*/
void I2C_Start()
{
I2C_SDA=1;
I2C_SCL=1;
I2C_SDA=0;
I2C_SCL=0;
}
/**
*@breaf I2C停止函数
*@param无
*@retval无
*/
void I2C_Stop()
{
I2C_SDA=0;
I2C_SCL=1;
I2C_SDA=1;
}
/**
*@breaf I2C发送字节函数
*@param Byte要发送的字节内容
*@retval 无
*/
void I2C_SendByte(unsigned char Byte)
{
unsigned char i;
for(i=0;i<8;i++)
{
I2C_SDA=Byte&(0x80>>i);
I2C_SCL=1;
I2C_SCL=0;
}
}
/**
*@breaf I2C接收字节函数
*@param 无
*@retval Byte接收来自外设的数据
*/
unsigned char I2C_ReceiveByte()
{
unsigned char Byte=0x00,i;
for(i=0;i<8;i++)
{
I2C_SDA=1;
I2C_SCL=1;
if( I2C_SDA) {Byte|=(0x80>>i);}
I2C_SCL=0;
}
return Byte;
}
/**
*@breaf I2C发送应答
*@param AckBit 应答位,0为应答,1为非应答
*@retval 无
*/
void I2C_SendAck(unsigned char AckBit)
{
I2C_SDA=AckBit;
I2C_SCL=1;
I2C_SCL=0;
}
/**
*@breaf I2C接收应答
*@param无
*@retval AckBit 应答位,0为应答,1为非应答
*/
unsigned char I2C_ReceiveAck()
{
unsigned char AckBit;
I2C_SDA=1;
I2C_SCL=1;
AckBit=I2C_SDA;
I2C_SCL=0;
return AckBit;
}
key.c
#include <REGX52.H>
#include <intrins.h>
//定义端口
sbit K1=P3^1;
sbit K2=P3^0;
sbit K3=P3^2;
sbit K4=P3^3;
/**
*@breaf 延时函数@11.0592MHz,延时单位ms
*@param t延时时间
*@retval 无
*/
void Delay1m(unsigned char t) //@11.0592MHz
{
unsigned char i, j;
while(t--)
{
_nop_();
i = 2;
j = 199;
do
{
while (--j);
} while (--i);
}
}
/**
*@breaf 按键按下函数
*@param无
*@retval key按键按下后,返回对应按键的数值
*/
unsigned char key_scan()
{
unsigned char key =0;
if(K1==0)
{
Delay1m(20) ;
while(K1==0);
Delay1m(20) ;
key=1;
}
if(K2==0)
{
Delay1m(20) ;
while(K2==0);
Delay1m(20) ;
key=2;
}
if(K3==0)
{
Delay1m(20) ;
while(K3==0);
Delay1m(20) ;
key=3;
}
if(K4==0)
{
Delay1m(20) ;
while(K4==0);
Delay1m(20) ;
key=4;
}
return key;
}
delay.c
#include <intrins.h>
/**
*@breaf 延时函数@11.0592MHz 基本单位是100ms
*@param t 为延时的时间 t*100ms
*@retval 无
*/
void Delay100ms(unsigned char t) //@11.0592MHz
{
unsigned char i, j;
while(t--)
{
i = 180;
j = 73;
do
{
while (--j);
} while (--i);
}
}
/**
*@breaf 延时函数@11.0592MHz 基本单位是10us
*@param t 为延时的时间 t*10us
*@retval 无
*/
void Delay10us(unsigned char t)//@11.0592MHz
{
while(t--)
{
unsigned char i;
i = 2;
while (--i);
}
}
/**
*@breaf 延时函数@11.0592MHz 基本单位是1ms
*@param t 为延时的时间 tms
*@retval 无
*/
void Delay1ms(unsigned char t) //@11.0592MHz
{
unsigned char i, j;
while(t--)
{
_nop_();
i = 2;
j = 199;
do
{
while (--j);
} while (--i);
}
}
AT24C02.h
#ifndef _AT24C02_H_
#define _AT24C02_H_
void AT24C02_WriteByte(unsigned char WordAddress,Data);
unsigned char AT24C02_ReadByte(unsigned char WordAddress);
#endif
I2C.h
#ifndef _AT24C02_H_
#define _AT24C02_H_
void AT24C02_WriteByte(unsigned char WordAddress,Data);
unsigned char AT24C02_ReadByte(unsigned char WordAddress);
#endif
delay.h
#ifndef _delay_H
#define _delay_H
void Delay100ms(unsigned char t); //延时函数@11.0592MHz 基本单位是100ms
void Delay1ms(unsigned char t); //延时函数@11.0592MHz 基本单位是1ms
void Delay10us(unsigned char t); //延时函数@11.0592MHz 基本单位是10us
#endif
key.h
#ifndef _key_H
#define _key_H
unsigned char key_scan();
#endif
实验现象:
I2C实验
总结:本节我们学习了I2C的相关概念,对于I2C的物理层以及协议层进行了详细的讲解,特别是对于使用I2C时的电路与时序进行了每一步的深入讲解;还借助AT24C02进行了相关的实验实现。读者在学习完后,一定一定要尝试着自己去写写,才能有更好的掌握。
创作不易,还请大家多多点赞支持!!!