Bootstrap

【蓝桥杯】单片机学习(10)——I2C通信协议与E2PROM

一、I2C总线

I2C总线是两线式串行总线,有两根双向信号线。一根是数据线SDA,一根是时钟线SCL。I2C总线的特点是:接口方式简单,两条线可以挂多个参与通信的器件,即多机模式,并且任何一个器件都可以作为主机(同一时刻只能有一个主机)。

1、I2C协议和UART协议的区别

  • 从原理上看:UART属于异步通信,比如计算机发送给单片机,计算机只负责把数据通过TXD发送出来即可,接收数据是单片机自己的事情。而 I2C属于同步通信,SCL时钟线负责收发双方的时钟节拍,SDA数据线负责传输数据。I2C的发送方和接收方都以SCL
    这个时钟节拍为基准进行数据的发送和接收。

  • 从应用上看:UART通信名用于板间通信,比如单片机和计算机,一个设备和另一个设备之间的通信;而 I2C多用王板内通信,比如单片机和EPROM之间的通信。

2、I2C时序认识

处理器和芯片间的通信可以形象的比喻成两个人的说话:①双方说的是同一种语言:双方约定好通信协议(在这里就是I2C通信协议)②双方的语速彼此都能接受:双方满足时序要求

I2C总线由时钟总线SCL和数据总线SDA构成,连接到总显示上的所有器件上的SCL都连到一起,所有SDA都连到一起。

在这里插入图片描述
I2C总线通过上拉电阻接正电源。当总线空闲时,两根线均为高电平。连到总线上的任一器件输出的低电平,都将使总线的信号变低,即各器件的SDA及SCL都是线“与”关系,这意味着任何一个器件都可以作为主机。51开发板上挂接了两个I2C设备,24C02和PCF8591。

在这里插入图片描述
UART通信流程包括起始位、数据位、·停止位三部分,其中每个字节都有一个起始位、8个数据位和1个停止位;I2C通信对应的包括起始信号、数据传输和停止信号三部分。数据传输部分可以在一次通信过程中传输很多个字节,每个字节后面都跟以为应答位,用ACK表示。I2C时序流程图如下:

I2C时序流程图

3、I2C总线的数据传送

(1)数据位的有效性规定

I2C总线进行数据传送时,时钟信号为高电平期间,数据线上的数据必须保持稳定,此时接收方在读取数据线的内容,只有在时钟线上的信号为低电平期间,数据线上的高电平或低电平状态才允许变化。

在这里插入图片描述

(2)起始信号和停止信号

UART通信的起始位标志是默认高电平的情况下出现一个低电平,停止位标志是一位固定的高电平信号。

对于I2C通信,SCL线为高电平期间,SDA线由高电平向低电平变化,产生一个下降沿,表示起始信号;SCL线为高电平期间,SDA线由低电平向高电平变化,产生一个上升沿,表示终止信号。

在这里插入图片描述
起始和终止信号都是由主机发出的,在起始信号产生后,总线一直处于被占用的状态;终止信号产生后,总线恢复空闲状态。连接到 I2C总线上的器件,若具有I2C总线的硬件接口,则可检测到起始和终止信号。

接收器件收到一个完整的数据字节后,有可能需要完成一些其它工作,如处理内部中断服务等,可能无法立刻接收下一个字节,这时接收器件可以将SCL线拉成低电平,从而使主机处于等待状态。直到接收器件准备好接收下一个字节时,再释放SCL线使之为高电平,从而使数据传送可以继续进行。

(3)数据传输

UART 的数据传输是低位在前,高位在后;数据位是固定长度,波特率分之一,每一位在固定的时间内发送完毕。

而 I2C通信是高位在前,低位在后;没有固定的波特率,但是有时序的要求,只要在SCL为低电平的时候,数据线SDA才可以发生变化。具体时序图如下:

在这里插入图片描述
每一个字节必须保证是8位长度。数据传送时,先传送最高位(MSB),每一个被传送的字节后面都必须跟随一位应答位(即一帧共有9位)。

  • 主机发送数据时:如果从机不能对主机的寻址信号进行应答,则必须将SDA(从机的数据线)置高电平,再由主机产生一个终止信号结束总线的数据传送;如果从机对主机进行了应答,在数据传输一段时间后,无法继续接收更多的数据,从机可以对无法接收的第一个数据字节 “非应答”,以通知主机,再由主机产生一个终止信号结束数据的继续传送。
  • 主机接收数据时:收到最后一个数据字节后,必须向从机发送一个结束传送的信号。这个信号通过对从机“非应答”来实现。然后,从机释放SDA线,允许主机产生终止信号。

每次数据传送总是由 主机 产生终止信号结束。但是,若主机希望继续占用总线进行新的数据传送,则可以不产生终止信号,马上再次发出起始信号对另一从机进行寻址。

在这里插入图片描述

(4)整体数据传送过程

I2C总线传送的数据信号是广义的,包括地址信号和真正的数据信号。 在起始信号后必须传送一个从机的地址(7位),第8位是数据的传送方向位(R/W),“0”表示主机发送数据(写),“1”表示主机接收数据(读)。

在总线的一次数据传送过程中,可以有以下几种组合方式:

  • 主机向从机发送数据,数据传送在整个传送过程中不变:

在这里插入图片描述
注:有阴影部分表示数据由主机向从机传送,无阴影部分则表示数据由从机向主机传送。 A表示应答, A非表示非应答(高电平)。S表示起始信号,P表示终止信号。

首先主机发送起始信号,紧接着发送7位从机地址和0(发送数据),此时地址匹配成功的从机发送应答位。接着不断重复主机发送数据,从机发送应答位,直到从机接收完毕后,发送非应答位,主机发送停止信号。

  • 主机在第一个字节后,立即从从机读数据

在这里插入图片描述

  • 在传送过程中,当需要改变传送方向时,起始信号和从机地址都被重复产生一次,两次读/写方向位正好反相。

在这里插入图片描述

4、I2C总线的寻址模式

I2C总线协议有明确的规定:采用7位的寻址字节(寻址字节是起始信号后的第一个字节)。

  • 寻址字节的位定义

在这里插入图片描述
D7~D1位组成从机的地址。D0位是数据传送方向位,为“0”时表示主机向从机写数据,为“1”时表示主机由从机读数据。

  • 主机发送地址时,总线上的每个从机都将这7位地址码与自己的地址进行比较,如果相同,则认为自己正被主机寻址,根据R/T位将自己确定为发送器或接收器。
  • 从机的地址由固定部分和可编程部分组成。在一个系统中可能希望接入多个相同的从机,从机地址中可编程部分决定了可接入总线该类器件的最大数目。如一个从机的7位寻址位有4位是固定位,3位是可编程位,这时仅能寻址8个同样的器件,即可以有8个同样的器件接入到该I2C总线系统中。

二、I2C总线器件的扩展

1、扩展电路

在这里插入图片描述

2、E2PROM

在实际应用中,保存在单片机RAM中的数据断电即丢失,保存在单片机的FLASH中的数据不能随意改变,所以不能用来记录变化的数值。当需要满足断电不丢失,记录完数据之后还能更改时,例如记录家用电表度数,电视剧里的频道记忆,一般就需要使用E2PROM来保存数据。使用的是AT24C系列器件,比如AT24C02,容量是2Kb,即256字节的E2PROM,可以反复写入30万 ~ 100万次,读取次数无限。

I2C是一种通信协议,24C02是基于T2C通信协议的一个器件,只是这个器件采用了I2C协议的接口与单片机相连而已,二者没有必然联系。E2PROM可以用其他接口,I2C也可以用在很多其他器件上。

(1)向E2PROM写数据流程

在这里插入图片描述
注:有阴影部分表示数据由主机向从机传送,无阴影部分则表示数据由从机向主机传送。 A表示应答, A非表示非应答(高电平)。S表示起始信号,P表示终止信号。

①首先是 I2C的起始信号,接着是 I2C的器件地址,并且在读写方向上选择 “写” 操作 “0”(共8位,即一个字节)。发送完后释放SDA线并在SCL线上产生第9个时钟信号。被选中的存储器器件在确认是自己的地址后,在SDA线上产生一个应答信号作为相应,单片机收到应答后就可以传送数据了。

②发送数据的存储地址。24C02一共256个字节的存储空间,地址从0x00 ~ 0xFF,想要把数据存储在哪个位置,此刻写的就是哪个地址。

③发送要存储数据的第一个字节、第二个字节…在写数据的过程中,E2PROM(从机)每个字节都会回应一个 “应答位0” ,告诉我们写E2PROM数据成功,如果没有回应应答位,则表示写入不成功。当要写入的数据传送完后,单片机应发出终止信号以结束写入操作。

在写数据的过程中,每成功写入一个字节,E2PROM存储空间的地址就会自动加1,当加到0xFF后,再写一个字节,地址会溢出又变成0x00。

(2)从E2PROM读数据流程

E2PROM读数据流程
①首先是 I2C的起始信号,接着是 I2C的器件地址,并且在读写方向上选择 “写” 操作(伪读)(共8位,即一个字节)。发送完后释放SDA线并在SCL线上产生第9个时钟信号。被选中的存储器器件在确认是自己的地址后,在SDA线上产生一个应答信号作为回应。

为什么明明是读数据,却要先选择写操作呢?因为不管是向E2PROM里面写数据还是从E2PROM里面读数据,都需要先进行地址的匹配,即需要主机先写入存储器地址,各个存储器器件进行地址匹配,确定选中的存储器件,紧接着再进行主机和选中从机之间的读/写操作。

②发送要读取的数据的地址。需要从24C02的哪一个地址中读取数据,就发送哪一个地址。地址存在,则24C02(从机)发送应答信号。

③重新发送 I2C 起始信号和器件地址,并且在方向位上选择“读”操作。核对无误后,从机发送应答位。

④主机读取从E2PROM(24C02)里面发回的数据,读一个字节,如果还想继续读下一个字节,就发送一个应答位ACK(0),如果不想读了,就发送一个非应答位NAK1,告诉E2PROM不想读了。

在写数据的过程一样,每读一个字节,E2PROM存储空间的地址就会自动加1。如果想继续读,就给E2PROM一个ACK(0)低电平,再继续给SCL完整的时序,E2PROM就会继续往外送数据,如果不想读,直接给一个NAK(1)高电平即可。

要注意以下几点:①在以上所述内容中,默认单片机是主机,E2PROM(24C02)是从机;②无论读写,都需要先写入存储器的地址,根据这个地址匹配到正确的从机。且SCL始终都是由主机控制的;③写的时候应答信号由从机给出,表示从机是否正确接收到数据;④读的时候应答信号则由主机给出,表示是否读下去。

三、模块化代码

1、i2c.h

#ifndef __I2C_h
#define __I2C_h

#include"system.h"

sbit SCL = P2^0;
sbit SDA = P2^1;

void I2cDelay(u16 t);
void I2cStart();
void I2cStop();
void I2cWrite(u8 dat);
u8 I2cRead();
bit Ack();
void E2Write(u8 addr,u8 dat);
u8 E2Read(u8 addr);

#endif

2、i2c.c

//延迟信号
void I2cDelay(u16 t)
{
    do
    {
        _nop_();
    }while(t--);
}

//起始信号
void I2cStart()
{
   SCL = 1;
   SDA = 1;
   I2cDelay(5);
   SDA = 0;
   I2cDelay(5);
   SCL = 0;
   I2cDelay(5);
}


//停止信号
void I2cStop()
{
   SCL = 0;
   SDA = 0;
   I2cDelay(5);
   SCL = 1;
   I2cDelay(5);
   SDA = 1;
   I2cDelay(5);
}

//总线读操作
u8 I2cRead()
{
   u8 dat ,i;
   for(i=0;i<8;i++)
   {
        SCL = 1;  //读之前必须保证SCL为高电平
        I2cDelay(5);
        dat <<= 1;
        if(SDA)
        {
            dat|=0x01;
        }
        SCL = 0;
   }
   return dat;
}


//总线写操作
void I2cWrite(u8 dat)
{
    u8 i;
    for(i=0;i<8;i++)
    {
        SCL = 0;  //写之前确保SCL为低电平
        SDA = dat & 0x80;
        SCL = 1;
        dat <<= 1;
    }
    SCL = 0;
}


//产生(非)应答信号
bit ACK()  //返回值为1,表示发送应答信号
{
    SCL = 1;//确保SCL为高电平,读取此时SDA的值,即(非)应答信号
    I2cDelay(5);
    if(SDA) //SDA = 1,表示发送非应答信号
    {
       SCL = 0;//拉低SCL完成应答位,保持住总线
       I2cStop();
       return 0;
    }
    else  //SDA = 0,表示发送应答信号
    {
        SCL = 0; //拉低SCL完成应答位,保持住总线
        return 1;
    }
}

//从E2PROM读取一个字节
u8 E2Read(u8 addr)
{
    u8 dat;
    I2cStart();
    I2cWrite(0xa0);//发送读器件的地址,后续为写操作
    Ack();
    I2cWrite(addr);//发送要读取数据的存储地址
    Ack();
    I2cStop();
    I2cStart();
    I2cWrite(0xa1);//发送读器件的地址,后续为读操作
    Ack();
    dat = I2cRead();
    Ack();
    return dat;
}

//向E2PROM写入一个字节
void E2Write(u8 addr,u8 dat)
{
    I2cStart();
    I2cWrite(0xa0); //发送写器件的地址,后续为写操作
    Ack();
    I2cWrite(addr); //发送要写入内存的地址
    Ack();
    I2cWrite(dat);  //发送要写的内容
    Ack();
    I2cStop();
}

四、官方驱动源码改写

1、i2c.h

#ifndef __I2C_h
#define __I2C_h

#include "system.h"

sbit SCL = P2^0; //时钟线
sbit SDA = P2^1; //数据线


void IIC_Start(); //起始信号
void IIC_Stop();  //停止信号

bit IIC_WaitAck();  //等待应答信号
void IIC_SendAck(bit ackbit); //发送(非)应答信号 

void IIC_SendByte(u8 byt); 
u8 IIC_RecByte(); 

void IIC_Write(u8 addr ,u8 dat);//此处选择没有返回值
//或者选择为应答信号的返回值
u8 IIC_Read(u8 addr,u8 ack);

#endif

2、i2c.c

/*
  程序说明: IIC总线驱动程序
  软件环境: Keil uVision 4.10 
  硬件环境: CT107单片机综合实训平台 8051,12MHz
  日    期: 2011-8-9
*/

#include "i2C.h"

//#define DELAY_TIME 5

#define SlaveAddrW 0xA0
#define SlaveAddrR 0xA1
#define somenop {_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();}
//定义延时时间,33个nop

//void IIC_Delay(unsigned char i)
//{
    //do{_nop_();}
    //while(i--);        
//}
	
//总线启动条件
void IIC_Start(void)
{
    SDA = 1;
    SCL = 1;
    somenop 
    SDA = 0;
    somenop 
    SCL = 0;	
}

//总线停止条件
void IIC_Stop(void)
{
    SDA = 0;
    SCL = 1;
    somenop 
    SDA = 1;
    somenop 
}

//发送应答(写过程)
void IIC_SendAck(bit ackbit)
{
    SCL = 0;        //发送前确保时钟线为低电平
    SDA = ackbit;  	// 0:应答,1:非应答
    somenop 
    SCL = 1;        //拉高时钟位
    somenop 
    SCL = 0; 
    SDA = 1;        //释放总线
    somenop 
}

//等待应答
bit IIC_WaitAck()
{
    bit ackbit;
	
    SCL  = 1;
    somenop 
    ackbit = SDA;
    SCL = 0;
    somenop 
    return ~ackbit;  //取反之后,有应答返回值为1,无应答返回值为0
}

//通过I2C总线发送数据
void IIC_SendByte(u8 byt)
{
    unsigned char i;

    for(i=0; i<8; i++)
    {
        SCL  = 0;  //写之前确保时钟在下降沿
        somenop 
        if(byt & 0x80) SDA  = 1;
        else SDA  = 0;
        somenop 
        SCL = 1;   //拉高时钟完成数据发送
        byt <<= 1;
        somenop 
    }
    SCL  = 0;  
}

//从I2C总线上接收数据
u8 IIC_RecByte(void)
{
    unsigned char i, da;
    for(i=0; i<8; i++)
    {   
    	SCL = 1;
		somenop 
		da <<= 1;
		if(SDA) da |= 1;
		SCL = 0;
		somenop 
		}
    return da;    
}


/*********************FOR_24C02***************/

//向从机中写入数据
void IIC_Write(u8 addr ,u8 dat)
{
	//bit ack;
	do{
		IIC_Start();
		IIC_SendByte(SlaveAddrW); //寻址器件,后续为写操作
		IIC_Stop();
	}while(!IIC_WaitAck());//检测是否接受到应答,
	//如果接收到,IIC_WaitAck()返回值为1,取反为0,跳出循环
	
	IIC_SendByte(addr);//写入存储地址
	IIC_WaitAck();
	IIC_SendByte(dat);//发送要写入的数据
	//ack = IIC_WaitAck();
	IIC_WaitAck();
	IIC_Stop();
	//return ack;	
}

//读取E2PROM中的一个字节
u8 IIC_Read (u8 addr,u8 ack)
{
	u8 dat;
	do{
			IIC_Start();
			IIC_SendByte(SlaveAddrW);//发送器件地址+写
			IIC_Stop();
	}while(!IIC_WaitAck());//检查是否收到应答
	IIC_SendByte(addr);//发送存储地址
	IIC_WaitAck();
	
	IIC_Start();
	IIC_SendByte(SlaveAddrR);//发送器件地址+读
	IIC_WaitAck();//检查是否收到应答
	dat=IIC_RecByte();//读取待返回的数据
	IIC_SendAck(ack);//应答读取结束发1,继续发0
	//IIC_Stop();
	return dat;
}

总结,改动的地方有以下几点:

①延时函数替换成somenop,定义如下
#define somenop {_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();} 一共33个_nop_();

官方驱动代码给的延时使用于89C52,蓝桥用的板子时速相对快的多,所以一直加到了33个_nop_()。

②等待应答函数 bit IIC_WaitAck() 返回值取反,返回1表示应答,返回0表示非应答,符合正逻辑。

③有关24C02部分需要自己写。

前一篇: 单片机学习(9)——多.c文件初认识及计算器实例

下一篇: 单片机学习(11)——实时时钟DS1302

;