SDA和SCL信号
SDA和SCL都是双向线路,通过电流源或者上拉电阻连接到一个正向电压.(见下图)当总线空闲时,两条线都是高电平.连接到总线的设备的输出级必须是OD(漏极开路)或者OC(集电极开路)门才能执行线与功能.在标准模式下I2C总线传输数据可以达到100kb/s的速度,而在快速模式下达到400kb/s的速度,快速plus模式下是1Mb/s的速度,告诉模式下是3.4Mb/s的速度.总线的电容限制连接到总线上的接口数量.
对于一个单一主机的应用,如果总线上没有设备可以拉低时钟那么主机的SCL输出应该是push-pull(推挽)驱动设计.
SDA和SCL的逻辑电平
由于有各种不同的工艺的设备(CMOS NMOS 双极性)可以连接到I2C总线,低电平和高电平不是固定的而是依赖于相对应的VDD的电平.
数据有效性
在时钟为高期间SDA线上的数据必须稳定.只有在SCL线上的时钟信号低时数据线才可以改变高低状态.每个数据位传输都需要一个时钟产生.
开始和终止条件
所有的传输都由一个START(S)开始,有一个STOP(P)终止.
START条件是当SCL高时SDA从高到低
STOP条件是SCL高时SDA从低到高
开始条件和终止条件一直由主机产生.在开始条件后总线就处于忙状态.在终止条件后隔上一个固定时间总线就处于空闲状态.如果没有终止条件产生,而是一个重复的开始条件(Sr),那总线依旧是忙状态.这种情况下S和Sr在功能上是一样的.
如果连接到总线的设备包含了必须的接口硬件那么开始条件和终止条件的检测时很容易的.但是没有这样接口的微控制器在每个时钟周期内至少要采样两次SDA线来识别有没有电平变化.
//SDA 线上的数据必须在时钟的高电平周期保持稳定数据线的高或低电平状态
//只有在SCL线的时钟信号是低电平时才能改变
//起始条件: SCL 线是高电平时SDA 线从高电平向低电平切换
//停止条件: 当SCL 是高电平时SDA 线由低电平向高电平切换
//以下为头文件<I2C_C51.H>
#ifndef I2C_C51_H
#define I2C_C51_H
#ifndef uchar
#define uchar unsigned char
#endif
/*******************************************************************
无子地址发送字节数据函数
功能: 从启动总线到发送地址,数据,结束总线的全过程,从器件地址sla.
如果返回1表示操作成功,否则操作有误。
********************************************************************/
extern bit ISendByte(uchar sla,uchar c);
/*******************************************************************
有子地址发送多字节数据函数
功能: 从启动总线到发送地址,子地址,数据,结束总线的全过程,从器件
地址sla,子地址suba,发送内容是s指向的内容,发送no个字节。
如果返回1表示操作成功,否则操作有误。
********************************************************************/
extern bit ISendStr(uchar sla,uchar suba,uchar *s,uchar no) ;
/*******************************************************************
无子地址发送多字节数据函数
功能: 从启动总线到发送地址,子地址,数据,结束总线的全过程,从器件
地址sla,发送内容是s指向的内容,发送no个字节。
如果返回1表示操作成功,否则操作有误。
********************************************************************/
extern bit ISendStrExt(uchar sla,uchar *s,uchar no);
/*******************************************************************
无子地址读字节数据函数
功能: 从启动总线到发送地址,读数据,结束总线的全过程,从器件地
址sla,返回值在c.
如果返回1表示操作成功,否则操作有误。
********************************************************************/
extern bit IRcvByte(uchar sla,uchar *c);
/*******************************************************************
有子地址读取多字节数据函数
功能: 从启动总线到发送地址,子地址,读数据,结束总线的全过程,从器件
地址sla,子地址suba,读出的内容放入s指向的存储区,读no个字节。
如果返回1表示操作成功,否则操作有误。
********************************************************************/
extern bit IRcvStr(uchar sla,uchar suba,uchar *s,uchar no);
/*******************************************************************
无子地址读取多字节数据函数
功能: 从启动总线到发送地址,读数据,结束总线的全过程.
从器件地址sla,读出的内容放入s指向的存储区,
读no个字节。如果返回1表示操作成功,否则操作有误。
********************************************************************/
extern bit IRcvStrExt(uchar sla,uchar *s,uchar no);
#endif
//以下为头文件<I2C_C51.c>
#include <reg51.h>
#include <intrins.h>
#define uchar unsigned char /*宏定义*/
#define uint unsigned int
#define _Nop() _nop_() /*定义空指令*/
sbit SDA=P0^7; /*模拟I2C数据传送位*/
sbit SCL=P0^6; /*模拟I2C时钟控制位*/
bit ack; /*应答标志位*/
/*******************************************************************
起动总线函数
函数原型: void Start_I2c();
功能: 启动I2C总线,即发送I2C起始条件.
********************************************************************/
void Start_I2c()
{
SDA=1; /*发送起始条件的数据信号*/
_Nop();
SCL=1;
_Nop(); /*起始条件建立时间大于4.7us,延时*/
_Nop();
_Nop();
_Nop();
_Nop();
SDA=0; /*发送起始信号*/
_Nop(); /* 起始条件锁定时间大于4μs*/
_Nop();
_Nop();
_Nop();
_Nop();
SCL=0; /*钳住I2C总线,准备发送或接收数据 */
_Nop();
_Nop();
}
/*******************************************************************
结束总线函数
函数原型: void Stop_I2c();
功能: 结束I2C总线,即发送I2C结束条件.
********************************************************************/
void Stop_I2c()
{
SDA=0; /*发送结束条件的数据信号*/
_Nop(); /*发送结束条件的时钟信号*/
SCL=1; /*结束条件建立时间大于4μs*/
_Nop();
_Nop();
_Nop();
_Nop();
_Nop();
SDA=1; /*发送I2C总线结束信号*/
_Nop();
_Nop();
_Nop();
_Nop();
}
字节格式
发送到SDA线上的每个字节必须是8位.每次传输的字节数量是不受限制的.每个字节后必须跟着一个ACK应答位.数据从最高有效位(MSB)开始传输.如果从机要执行一些功能后才能接收或者发送新的完整数据,比如说服务一个内部中断,那么它可以将时钟线SCL拉低来强制使主机进入wait状态.当从机准备好新的字节数据传输时,释放时钟线SCL,数据传输便继续进行.
ACK和NACK
每个字节后都有ACK发生.ACK应答位允许接收器通知发送器字节成功接收了下一个字节可以发送了.主机产生所有的时钟脉冲,包括应答位的第9个时钟脉冲.
ACK应答信号是如下定义的:在ACK的第9个时钟脉冲中发送器释放SDA线,所以接收器可以将SDA拉低,使得在这个时钟脉冲的高电平期间保证SDA是低电平.建立和保持时间也应该计算在内.
当在第9个时钟脉冲期间SDA仍然是高,这时定义为NACK信号.这时主机可以产生一个终止条件来终止传输,或者一个重复的开始条件来开始一个新的传输.这里有5中情况导致NACK的产生:
1.总线当前的传输地址上没有接收器,所以没有设备用ACK来响应.
2.因为接收者正在处理一些实时的功能,尚未准备与主机的通信,所以接收者不能收发.
3.在传输期间,接收者收到不能识别的数据或者命令.
4.在传输期间,接收者无法接收更多的数据字节.
5.主-接收器要通知从-发送器传输的结束.
/*******************************************************************
字节数据发送函数
函数原型: void SendByte(uchar c);
功能: 将数据c发送出去,可以是地址,也可以是数据,发完后等待应答,并对
此状态位进行操作.(不应答或非应答都使ack=0)
发送数据正常,ack=1; ack=0表示被控器无应答或损坏。
********************************************************************/
void SendByte(uchar c)
{
uchar BitCnt;
for(BitCnt=0;BitCnt<8;BitCnt++) /*要传送的数据长度为8位*/
{
if((c<<BitCnt)&0x80) /*判断发送位*/
SDA=1;
else
SDA=0;
_Nop();
SCL=1; /*置时钟线为高,通知被控器开始接收数据位*/
_Nop();
_Nop(); /*保证时钟高电平周期大于4μs*/
_Nop();
_Nop();
_Nop();
SCL=0;
}
_Nop();
_Nop();
SDA=1; /*8位发送完后释放数据线,准备接收应答位*/
_Nop();
_Nop();
SCL=1;
_Nop();
_Nop();
_Nop();
if(SDA==1) /*判断是否接收到应答信号*/
ack=0;
else
ack=1;
SCL=0;
_Nop();
_Nop();
}
/*******************************************************************
字节数据接收函数
函数原型: uchar RcvByte();
功能: 用来接收从器件传来的数据,并判断总线错误(不发应答信号),
发完后请用应答函数应答从机。
********************************************************************/
uchar RcvByte()
{
uchar retc;
uchar BitCnt;
retc=0;
SDA=1; /*置数据线为输入方式*/
for(BitCnt=0;BitCnt<8;BitCnt++)
{
_Nop();
SCL=0; /*置时钟线为低,准备接收数据位*/
_Nop();
_Nop(); /*时钟低电平周期大于4.7μs*/
_Nop();
_Nop();
_Nop();
SCL=1; /*置时钟线为高使数据线上数据有效*/
_Nop();
_Nop();
retc=retc<<1;
if(SDA==1) /*读数据位,接收的数据位放入retc中 */
retc=retc+1;
_Nop();
_Nop();
}
SCL=0;
_Nop();
_Nop();
return(retc);
}
/********************************************************************
应答子函数
函数原型: void Ack_I2c(bit a);
功能: 主控器进行应答信号(可以是应答或非应答信号,由位参数a决定)
********************************************************************/
void Ack_I2c(bit a)
{
if(a==0)
SDA=0; /*在此发出应答或非应答信号 */
else
SDA=1;
_Nop();
_Nop();
_Nop();
SCL=1;
_Nop();
_Nop(); /*时钟低电平周期大于4μs*/
_Nop();
_Nop();
_Nop();
SCL=0; /*清时钟线,钳住I2C总线以便继续接收*/
_Nop();
_Nop();
}
/*******************************************************************
用户接口函数
*******************************************************************/
/*******************************************************************
向无子地址器件发送字节数据函数
函数原型: bit ISendByte(uchar sla,ucahr c);
功能: 从启动总线到发送地址,数据,结束总线的全过程,从器件地址sla.
如果返回1表示操作成功,否则操作有误。
注意: 使用前必须已结束总线。
********************************************************************/
bit ISendByte(uchar sla,uchar c)
{
Start_I2c(); /*启动总线*/
SendByte(sla); /*发送器件地址*/
if(ack==0)return(0);
SendByte(c); /*发送数据*/
if(ack==0)return(0);
Stop_I2c(); /*结束总线*/
return(1);
}
/*******************************************************************
向有子地址器件发送多字节数据函数
函数原型: bit ISendStr(uchar sla,uchar suba,ucahr *s,uchar no);
功能: 从启动总线到发送地址,子地址,数据,结束总线的全过程,从器件
地址sla,子地址suba,发送内容是s指向的内容,发送no个字节。
如果返回1表示操作成功,否则操作有误。
注意: 使用前必须已结束总线。
********************************************************************/
bit ISendStr(uchar sla,uchar suba,uchar *s,uchar no)
{
uchar i;
Start_I2c(); /*启动总线*/
SendByte(sla); /*发送器件地址*/
if(ack==0)return(0);
SendByte(suba); /*发送器件子地址*/
if(ack==0)return(0);
for(i=0;i<no;i++)
{
SendByte(*s); /*发送数据*/
if(ack==0)return(0);
s++;
}
Stop_I2c(); /*结束总线*/
return(1);
}
/*******************************************************************
向无子地址器件发送多字节数据函数
函数原型: bit ISendStr(uchar sla,ucahr *s,uchar no);
功能: 从启动总线到发送地址,子地址,数据,结束总线的全过程,从器件
地址sla,发送内容是s指向的内容,发送no个字节。
如果返回1表示操作成功,否则操作有误。
注意: 使用前必须已结束总线。
********************************************************************/
bit ISendStrExt(uchar sla,uchar *s,uchar no)
{
uchar i;
Start_I2c(); /*启动总线*/
SendByte(sla); /*发送器件地址*/
if(ack==0)return(0);
for(i=0;i<no;i++)
{
SendByte(*s); /*发送数据*/
if(ack==0)return(0);
s++;
}
Stop_I2c(); /*结束总线*/
return(1);
}
/*******************************************************************
向无子地址器件读字节数据函数
函数原型: bit IRcvByte(uchar sla,ucahr *c);
功能: 从启动总线到发送地址,读数据,结束总线的全过程,从器件地
址sla,返回值在c.
如果返回1表示操作成功,否则操作有误。
注意: 使用前必须已结束总线。
********************************************************************/
bit IRcvByte(uchar sla,uchar *c)
{
Start_I2c(); /*启动总线*/
SendByte(sla+1); /*发送器件地址*/
if(ack==0)return(0);
*c=RcvByte(); /*读取数据*/
Ack_I2c(1); /*发送非就答位*/
Stop_I2c(); /*结束总线*/
return(1);
}
/*******************************************************************
向有子地址器件读取多字节数据函数
函数原型: bit RecndStr(uchar sla,uchar suba,ucahr *s,uchar no);
功能: 从启动总线到发送地址,子地址,读数据,结束总线的全过程,从器件
地址sla,子地址suba,读出的内容放入s指向的存储区,读no个字节。
如果返回1表示操作成功,否则操作有误。
注意: 使用前必须已结束总线。
********************************************************************/
bit IRcvStr(uchar sla,uchar suba,uchar *s,uchar no)
{
uchar i;
Start_I2c(); /*启动总线*/
SendByte(sla); /*发送器件地址*/
if(ack==0)return(0);
SendByte(suba); /*发送器件子地址*/
if(ack==0)return(0);
Start_I2c(); /*重新启动总线*/
SendByte(sla+1);
if(ack==0)return(0);
for(i=0;i<no-1;i++)
{
*s=RcvByte(); /*发送数据*/
Ack_I2c(0); /*发送就答位*/
s++;
}
*s=RcvByte();
Ack_I2c(1); /*发送非应位*/
Stop_I2c(); /*结束总线*/
return(1);
}
/*******************************************************************
向无子地址器件读取多字节数据函数
函数原型: bit ISendStrExt(uchar sla,ucahr *s,uchar no);
功能: 从启动总线到发送地址,读数据,结束总线的全过程.
从器件地址sla,读出的内容放入s指向的存储区,
读no个字节。如果返回1表示操作成功,否则操作有误。
注意: 使用前必须已结束总线。
********************************************************************/
bit IRcvStrExt(uchar sla,uchar *s,uchar no)
{
uchar i;
Start_I2c();
SendByte(sla+1);
if(ack==0)return(0);
for(i=0;i<no-1;i++)
{
*s=RcvByte(); /*发送数据*/
Ack_I2c(0); /*发送就答位*/
s++;
}
*s=RcvByte();
Ack_I2c(1); /*发送非应位*/
Stop_I2c(); /*结束总线*/
return(1);
}
I2C总线的7bit从机地址
时钟拉伸(Clock stretching)
clock stretching通过将SCL线拉低来暂停一个传输.直到释放SCL线为高电平,传输才继续进行.clock stretching是可选的,实际上大多数从设备不包括SCL驱动,所以它们不能stretch时钟.
从字节级来看,一个设备可能在快速模式下接受数据,但是需要更多的时间来存储接收到的字节或者准备将要传输的另一个字节.从机可以以一种握手的处理方式在接受和应答字节后将SCL线拉低来强制使得主机进入wait状态知道从机准备好下一个字节的传输.
从位级来看,I2C总线上的设备可以通过增长每一个时钟的低周期来降低总线时钟.所以每个主机可以适应这个设备的内部操作速率.
在Hs模式,握手处理只能用在字节级别.
从机地址和R/W bit
下图是数据传输的格式:
在开始条件(S)后,发送从机地址.地址是7bit,后面的第8bit是数据的读写bit,0表示写,1表示读.具体的看下图:
数据传输被主机产生的终止条件(P)终止.然而,主机也可以无需先产生终止条件,产生一个重复的开始条件(Sr)和寻址另一个从机,
可能的数据传输格式如下:
主-发送器传到从-接收器.传输方向不变.从-接收器应答每一个字节.如下:
在第一个字节后主机从从机读数据.第一个应答后,主-发送器变为主-接收器而从-接收器变为从-发送器.第一个应答仍然是由从机产生的.主机产生余下的应答.主机在产生终止条件之前要发送一个NA.如下:
复合模式.在传输过程中改变方向,开始条件和从机地址都要重复,而读写bit要取反.如果主-接收器发送一个重复的开始条件,那么它在这之前要发送一个NA.
注意:
1.复合模式可以在比如控制串行内存器时用到.在第一个数据字节时一定要写内存器内部的地址.开开始条件和从地址重复后,数据就开始传输了.
2.自动增加或减少之前访问的内存位置都由设备的设计者决定.
3.每一个字节后面都跟着一个应答位,在图中用A或非A来表示.
4.兼容I2C总线的设备在接收到开始条件或重复开始条件时都一定要重启它们的总线逻辑,即使开始条件都不是正确的格式,它们都期望发送从机地址.
5.开始条件后立马跟着一个终止条件是不合法的格式.很多设备在设计时考虑了这一点,可以处理.
6.连接到总线上的每个设备都由唯一的地址来确定.通常是简单的主从关系,但可能存在多个一样的从机可以同时接收和响应,比如说组播.这里是以NXP的PCA9546A作为例子说明.(PCA9546A是NXP半导体生产的一款基于I2C总线控制的4通道双向多路复用器和开关。使用PCA9546A可以将一路SCL/SDA输入扩展为4路SCL/SDA输出,在对内部控制寄存器进行相应配置后,可同时选择一路或多路下行I2C总线与上行I2C总线通信。)