单片机--IIC总线篇

这里转载一位博主的文章,他写的IIC很详细,可以参照下,毕竟知识这个东西多多益善,且每个人口味不一样。
文章链接: https://blog.csdn.net/as480133937/article/details/105366932

下面为我自己结合大佬的一些文章作的一些总结,如有冒犯,请联系我进行删除,如有错误和不足,还请大佬评论。

IIC总线篇

一、IIC总线组成

两根双向信号线:一根数据线SDA;一根时钟线SCL。

在IIC总线上可以挂接多个设备,如下图:

在这里插入图片描述
每个接到IIC总线上的器件都有一个唯一的地址,设备多了,这样做可以根据地址很容易找到要与之通信的设备。

注:
1、IIC可以通过硬件支持,也可以通过软件来模拟
2、IIC总线需要通过上拉电阻接到电源正极,如下图:

在这里插入图片描述

二、IIC中涉及到的3种信号

  • 如下图所示,从IIC的时序图中我们可以看出,IIC通信中涉及三种信号,分别为开始信号、结束信号、应答信号(应答和非应答)。
    在这里插入图片描述

1、开始信号:SCL 为高电平时,SDA 由高电平向低电平跳变,开始传送数据。时序图如下:
在这里插入图片描述

2、结束信号:SCL 为高电平时,SDA 由低电平向高电平跳变,结束传送数据。时序图如下:
在这里插入图片描述

3、应答信号:每当主机向从机发送完一个字节的数据后,主机总是需要等待从机给出一个应答信号,以确认从机是否成功接收到了数据。

应答分两种:
1、应答:在SCL为高电平期间,SDA为低电平。一般表示成功收到数据。
2、非应答:在SCL为高电平期间,SDA为高电平。一般表示没有成功接收到数据。
在这里插入图片描述

注:之所以介绍这三种信号,是因为单片机中没有硬件IIC,我们要用软件模拟IIC通信,所以需要了解IIC中涉及到的3种信号,这样就可以在程序中进行模拟通信协议。

三、IIC总线的寻址

IIC总线协议有明确的规定:采用7位地址+1位方向位的寻址字节(寻址字节时起始信号后的第一个字节)。

1、寻址字节的位定义

在这里插入图片描述
D7-D1组成从机地址,D0是数据传方向位。0表示主机向从机;1表示主机从从机

主机发送地址时,总线上的每个从机都会接收这个地址数据,然后同自己的地址进行比较,如果相同,则根据T/R方向位将自己确定为发送器或接收器。
注意:从机的地址=固定部分+可编程部分
固定部分意思是:每个厂商在生产某种芯片时都会协商一个可以唯一标识芯片的数据。可编程部分意思是:在一个系统中可能希望接入多个相同的从机,我们可以通过可编程部分来对这些相同的从机进行地址编号,防止冲突。例如:在eeprom中,从机的7位寻址位有4位是固定位,3位时可编程位,这样我们就可以最多接入2^3=8个相同的eeprom到iic总线系统中。

四、IIC中数据传输

传输数据注意事项

  • 1、 I2C总线进行数据传送时,时钟信号为高电平期间,数据线上的数据必须保持稳定;只有在时钟线上的信号为低电平期间,数据线上的高电平或低电平状态才允许变化。如下图:**
    在这里插入图片描述

  • 2、IIC总线在空闲状态下SCL 和SDA都保持高电平

1、字节传送与应答

每一字节必须保证8位长度。数据传送时,先传送数据的最高位,然后次高位,直到最低位。每当传送完一个字节后面都必须跟随应答位(即一帧共有9位:8位数据+1位应答)。时序图如下:

1、主机先发送开始信号。
2、然后开始发送数据帧(这里的数据帧是广义的,它即包括地址数据,同时又包括真正的数据)。
3、然后对应接收数据的设备发送应答。
4、应答后,如果不需要发送数据了,就可以发出停止信号结束。但是若主机希望继续占用总线进行新
   的数据传送,则可以不产生终止信号,马上再次发送开始信号,重复上面的步骤。

在这里插入图片描述

2、数据帧格式

因为IIC总线上可以挂接多个设备,所以在开始信号后,必须传送一个从机地址(7位),第8位是数据的传送方向(T/R),用0表示主机发送数据(T),用1表示主机接收数据(R)。

每次数据传送总是由主机产生的终止信号进行结束。如下图:
在这里插入图片描述

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

1、主机向从机发送数据(主机——>从机)
2、主机从从机读取数据(主机<——从机)
3、传送过程中需要改变传送方向。(比较常用的方式,例如对EEPROM进行操作)
  • 1、主机向从机发送数据
1、先发送起始信号
2、接着发送要通信的从机地址+0(表示写方向T)
3、从机接收数据后进行应答
4、然后主机开始传送数据
5、从机接收数据后进行应答(或者非应答)
6、数据传送完后,主机发送停止信号结束,释放出总线。

在这里插入图片描述
【注】S:开始信号;A:应答信号;A非:非应答信号;P:停止信号。阴影表示主机向从机发送,无阴影表示从机向主机发送。

  • 2、主机从从机读取数据
1、先发送起始信号
2、接着发送要通信的从机地址+1(表示读方向R)
3、从机接收数据后进行应答
4、然后从机开始传送数据
5、主机接收数据后进行应答(或者非应答)
6、数据传送完后,主机发送停止信号结束,释放出总线。

在这里插入图片描述

  • 3、通讯复合格式(传送数据中改变方向)

在传送过程中,当需要改变传送方向时,起始信号和从机地址都将被重复产生一次,但两次读/写方向位正好相反(一般用于控制一个串行存储器,在第一个数据字节期间 ,要写内部存储器的位置,即我们要要告诉从机,我们要从存储器的哪个位置读,俗称伪写。只有在在重复起始条件和从机地址后 数据才可被传输)。

除了基本的读写, I2C 通讯更常用的是复合格式,该传输过程有两次起始信号 (S)。一般在第一次传输中,主机通过 SLAVE_ADDRESS 寻找到从设备后,发送一段“数据”,这段数据通常用于表示从设备内部的寄存器或存储器地址 (注意区分它与 SLAVE_ADDRESS 的区别);在第二次的传输中,对该地址的内容进行读或写。也就是说,第一次通讯是告诉从机读写地址,第二次则是读写的实际内容。

在这里插入图片描述
【注意】:上图第一个灰色区域的数据,表示主机向从机发送的内存地址,意在告诉主机要从哪个从机的哪个地方开始读数据。

总结:
1、写
对于内部只有一个存储地址的设备:发送开始信号后,发送了设备地址+写方向位,然后直接开始发送数据即可。
对于内部有多个存储地址的设备:发送开始信号后,发送了设备地址+写方向位,然后还要继续发送从设备的存储器地址(即具体从哪个位置写数据),然后开始发送数据即可。
2、读
对于内部只有一个存储地址的设备:发送开始信号后,发送了设备地址+读方向位,直接开始读取从机发送过来的数据即可。
对于内部有多个存储地址的设备:发送开始信号后,发送了设备地址+写方向位(伪写),还要继续发送从设备的存储器地址(即具体从哪个位置读数据)。然后再次发送开始信号,发送设备地址+读方向位,然后读取从机发送过来的数据即可。

五、程序中模拟IIC信号

1、开始信号的模拟

在这里插入图片描述

//上图虚线中的图形就是需要模拟的时序图

//1.先让SCL一直为高电平
//2.让SDA为高电平,持续4.7us以上时间
//3.再让SDA变为低电平,持续4us以上时间
//4.让SCL变为低电平,为后面传送数据做准备,因为传送数据时只有当SCL为低电平,数据才可以变化(当然这步可以不要,后面模拟数据时序图里再写)

void IIC_Start()         //启动信号
{
       SCL=1;  //确保SCL一直为高电平
       SDA=1; //SDA线为高电平
       delay_us(5);//延时4.7us以上
       SDA=0; //拉低SDA,即为开始信号形成
       delay_us(5);
       SCL=0;	//这里可以不写,写上当然最好
}

2、停止信号的模拟

在这里插入图片描述

//上图虚线中的图形就是需要模拟的时序图

//1.先让SCL一直为高电平
//2.让SDA为低电平,持续4us以上时间
//3.再让SDA变为高电平,持续4.7us以上时间
void IIC_Stop()         //启动信号
{
       SCL=1;  //确保SCL一直高电平
       SDA=0; //SDA线为低电平
       delay_us(5);//延时4.7us以上
       SDA=1; //拉高SDA,即为停止信号形成
       delay_us(5);
}

3、应答信号的模拟

在这里插入图片描述

******************************************************
//主机产生应答信号

//1.先拉低SCL,方便数据进行改变(因为数据传送过程中只有SCL为低时,SDA才允许改变)
//2.然后SDA输入 低电平 或 高电平 ,进行 应答 或 非应答
//3.SCL输入高电平,延时4us以上
//4.SCL为低电平,为下次数据改变做准备(当然这步也可以不要)。
void I2C_Ack(void)
{
   SCL=0;   //先拉低SCL,使得SDA数据可以发生改变
   SDA=0; 	//数据变化为0
   //delay_us(2);  
   SCL=1;
   delay_us(5);
   SCL=0;//为下次数据变化做准备(可不要)
}
*****************************************************
//主机产生非应答信号

//分析过程同上
void I2C_NAck(void)
{
   SCL=0;   //先拉低SCL,使得SDA数据可以发生改变
   SDA=1;   //拉高SDA,不产生应答信号
   //delay_us(2);
   SCL=1;
   delay_us(5);
   SCL=0;//为下次数据变化做准备(可不要)
}

注意: 应答信号在程序中,只有当单片机为从机(读取数据)的时候用得到,因为读取一个字节后,需要发送应答,告知对方已接收数据。

4、IIC读取一个字节

/*返回值为读取的字节数据*/

char I2cReadByte()
{
	char a=0,dat=0;
	//SDA=1;		//将SDA引脚设置为输入(单片机中个人感觉鸡肋)
	for(a=0;a<8;a++)	//接收8个字节
	{
		SCL=0;			//在SCL为低电平期间,SDA可以改变数据
		delay_us(5);
		SCL=1;		//数据改变后,使数据稳定
		delay_us(5);
		dat<<=1;
		dat|=SDA;		//开始一位一位读数据
		delay_us(5);
	}
	I2C_Ack()//数据读取完后,发送一个应答位
	return dat;		
}

5、IIC发送一个字节

在这里插入图片描述

//如果在开始始信号模拟中最后使SCL=0(方便后续数据改变,IIC规定数据传输中,SCL高电平期间数据要保持稳定,SCL低电平期间数据可以改变),这里可以不加,而我在上面的开始信号的模拟中,最后没有拉低SCL,所以这里加上,当然最保险是两处都加上
/*参数为要发送的数据,返回值为0表示对方成功接收,为1表示接收失败(是根据检测对方应答位判断的)*/

char I2cSendByte(char dat)		
{
	int a=0;				
	for(a=0;a<8;a++)	//发送8位,从最高位开始
	{
		SCL=0;			//表示可以直接改变SDA
		delay_us(2);	//个人感觉这些地方的延时可加可不加,可以去掉调试,看有无影响
		SDA=dat>>7;		//把最高位送入SDA
		dat=dat<<1;		//把次高位左移到最高位,方便下次传送
		delay_us(2);	//可加可不加
		SCL=1;			//保证数据稳定
		delay_us(5);	//建立时间>4.7us	
	}
	
	SCL=0;//为接下来应答位变换准备
	delay_us(5);//时间大于4us
		
	//上面数据接收结束,接下来判断应答位
	SDA=1;				//为了保证IIC总线在空闲的时候都是高电平,引脚为接收模式
	delay_us(5);
		
	SCL=1;				//为了保证数据稳定
	delay_us(10);						//此时数据传输完,SDA接收到应答位
	if(SDA==1)		//如果收到的应答位为1,
	{
		SCL=0;	//最后拉低时钟线,这条语句不能少,不然数据出错,上图数据手册最后说明了的.
		return 0;		//返回0,表示对方接收失败
	}
	else
	{
		SCL=0;	//最后拉低时钟线,这条语句不能少,不然数据出错,上图数据手册最后说明了的.
 		return 1;		//返回1,表示对方成功接收
 	}		
}

六、IIC总线在具体器件中的应用

1、在EEPROM中的应用

前言: Eeprom是一种可擦除反复编程的存储器,掉电也可以保存里面的数据不会丢失,可多次循环编程利用。 AT24C系列E2PROM芯片地址的固定部分为1010,A2、A1、A0引脚接高、低电平后得到确定的3位编码。形成的7位编码即为该器件的地址码。具体可以参照芯片手册。

AT24C02芯片手册

(1)、写入数据过程

在这里插入图片描述
注上面:
S:IIC开始信号;
A:接收方的应答信号;
P:IIC停止信号。
灰色区域:主机发送;
白色区域:从机发送。

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

传送数据时,单片机首先发送一个字节的存储地址(被写入器件的存储区的首地址),收到存储器器件的应答后,单片机就逐个发送各数据字节,但每发送一个字节后都要等待应答。

AT24C系列器件片内地址在接收到每一个数据字节后,片内地址自动加1,在芯片的“装载字节数”(不同芯片字节数不同,即存储大小不同)限度内,只需输入首地址。装载字节数超过芯片的“装载字节数”时,数据地址将“上卷”(从头开始覆盖存储),前面的数据将被覆盖。

当要写入的数据传送完后,单片机应发出终止信号以结束写入操作。

软件模拟代码如下
//参数1:写入哪一内存地址;参数2:写入的字符数据
void At24c02Write(unsigned char addr,unsigned char dat)
{
	I2cStart();			//发送开始信号
	I2cSendByte(0xa0);	//发送写器件地址
	I2cSendByte(addr);	//发送要写入内存地址
	I2cSendByte(dat);	//发送数据
	I2cStop();			//停止本次通信
}

(2)、读取数据过程

这里参照使用手册中的随机读方式(还有其它的方式)
在这里插入图片描述

在这里插入图片描述
注上面:
S:IIC开始信号;
A:接收方的应答信号;
A非:接收方的非应答信号;
P:IIC停止信号。
灰色区域:主机发送;
白色区域:从机发送。

单片机先发送该器件的7位地址码和写方向位“0”(“伪写”,个人理解:先通知对方我要开始向它的哪一存储区读数据,让它做好发送准备),然后从机给SDA线上产生一个应答信号作为回应。

然后,再发一个字节的要读出器件的存储区的首地址,收到应答后,单片机要重复一次起始信号并发出器件地址和读方向位(“1”),收到器件应答后就可以读出数据字节,每读出一个字节,单片机都要回复应答信号。当最后一个字节数据读完后,单片机应返回以“非应答”(高电平),并发出终止信号以结束读出操作。

软件模拟代码如下
//参数:读取内存地址
unsigned char At24c02Read(unsigned char addr)
{
	unsigned char num;
	I2cStart();
	I2cSendByte(0xa0); //发送写器件地址
	I2cSendByte(addr); //发送要读取的地址
	I2cStart();
	I2cSendByte(0xa1); //发送读器件地址
	num=I2cReadByte(); //读取数据
	I2cStop();
	return num;	
}

3、附上完整代码:

下面程序代码是网上找的资料,有些函数在模拟时序图时,和我上面模拟的时序代码有一些差异。问题不大,都可以解决,可以大致参考下。

#include "i2c.h"

/*******************************************************************************
* 函数名         : Delay10us()
* 函数功能		   : 延时10us
* 输入           : 无
* 输出         	 : 无
*******************************************************************************/

void Delay10us()
{
	unsigned char a,b;
	for(b=1;b>0;b--)
		for(a=2;a>0;a--);

}
/*******************************************************************************
* 函数名         : I2cStart()
* 函数功能		 : 起始信号:在SCL时钟信号在高电平期间SDA信号产生一个下降沿
* 输入           : 无
* 输出         	 : 无
* 备注           : 起始之后SDA和SCL都为0
*******************************************************************************/

void I2cStart()
{
	SDA=1;
	Delay10us();
	SCL=1;
	Delay10us();//建立时间是SDA保持时间>4.7us
	SDA=0;
	Delay10us();//保持时间是>4us
	SCL=0;
	Delay10us();		
}
/*******************************************************************************
* 函数名         : I2cStop()
* 函数功能		 : 终止信号:在SCL时钟信号高电平期间SDA信号产生一个上升沿
* 输入           : 无
* 输出         	 : 无
* 备注           : 结束之后保持SDA和SCL都为1;表示总线空闲
*******************************************************************************/

void I2cStop()
{
	SDA=0;
	Delay10us();
	SCL=1;
	Delay10us();//建立时间大于4.7us
	SDA=1;
	Delay10us();		
}
/*******************************************************************************
* 函数名         : I2cSendByte(unsigned char dat)
* 函数功能		 : 通过I2C发送一个字节。在SCL时钟信号高电平期间,保持发送信号SDA保持稳定
* 输入           : num
* 输出         	 : 0或1。发送成功返回1,发送失败返回0
* 备注           : 发送完一个字节SCL=0,SDA=1
*******************************************************************************/

unsigned char I2cSendByte(unsigned char dat)
{
	unsigned char a=0,b=0;//最大255,一个机器周期为1us,最大延时255us。		
	for(a=0;a<8;a++)//要发送8位,从最高位开始
	{
		SDA=dat>>7;	 //起始信号之后SCL=0,所以可以直接改变SDA信号
		dat=dat<<1;
		Delay10us();
		SCL=1;
		Delay10us();//建立时间>4.7us
		SCL=0;
		Delay10us();//时间大于4us		
	}
	SDA=1;
	Delay10us();
	SCL=1;
	while(SDA)//等待应答,也就是等待从设备把SDA拉低
	{
		b++;
		if(b>200)	 //如果超过2000us没有应答发送失败,或者为非应答,表示接收结束
		{
			SCL=0;
			Delay10us();
			return 0;
		}
	}
	SCL=0;
	Delay10us();
 	return 1;		
}
/*******************************************************************************
* 函数名         : I2cReadByte()
* 函数功能		   : 使用I2c读取一个字节
* 输入           : 无
* 输出         	 : dat
* 备注           : 接收完一个字节SCL=0,SDA=1.
*******************************************************************************/

unsigned char I2cReadByte()
{
	unsigned char a=0,dat=0;
	SDA=1;			//起始和发送一个字节之后SCL都是0
	Delay10us();
	for(a=0;a<8;a++)//接收8个字节
	{
		SCL=1;
		Delay10us();
		dat<<=1;
		dat|=SDA;
		Delay10us();
		SCL=0;
		Delay10us();
	}
	return dat;		
}


/*******************************************************************************
* 函数名         : void At24c02Write(unsigned char addr,unsigned char dat)
* 函数功能		   : 往24c02的一个地址写入一个数据
* 输入           : 无
* 输出         	 : 无
*******************************************************************************/

void At24c02Write(unsigned char addr,unsigned char dat)
{
	I2cStart();
	I2cSendByte(0xa0);//发送写器件地址
	I2cSendByte(addr);//发送要写入内存地址
	I2cSendByte(dat);	//发送数据
	I2cStop();
}
/*******************************************************************************
* 函数名         : unsigned char At24c02Read(unsigned char addr)
* 函数功能		   : 读取24c02的一个地址的一个数据
* 输入           : 无
* 输出         	 : 无
*******************************************************************************/

unsigned char At24c02Read(unsigned char addr)
{
	unsigned char num;
	I2cStart();
	I2cSendByte(0xa0); //发送写器件地址
	I2cSendByte(addr); //发送要读取的地址
	I2cStart();
	I2cSendByte(0xa1); //发送读器件地址
	num=I2cReadByte(); //读取数据
	I2cStop();
	return num;	
}

未完待续。。。

  • 1
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

掏一淘哆啦A梦的奇妙口袋

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值