9.1 IIC通讯协议的分析
一、简介
IIC(Inter-Integrated Circuit)总线是一种由NXP(原PHILIPS)公司开发的两线式串行总线,用于连接微控制器及其外围设备。多用于主控制器和从器件间的主从通信,在小数据量场合使用,传输距离短,任意时刻只能有一个主机等特性。
在 CPU 与被控 IC 之间、IC 与 IC 之间进行双向传送,高速 IIC 总线一般可达 400kbps 以上。
PS: 这里要注意IIC是为了与低速设备通信而发明的,所以IIC的传输速率比不上SPI
IIC一共有只有两个总线: 一条是双向的串行数据线SDA,一条是串行时钟线SCL
SDA(Serial data)是数据线,D代表Data也就是数据,Send Data 也就是用来传输数据的
SCL(Serial clock line)是时钟线,C代表Clock 也就是时钟 也就是控制数据发送的时序的
二、主要特性
- 通常我们为了方便把IIC设备分为主设备和从设备,基本上谁控制时钟线(即控制SCL的电平高低变换)谁就是主设备。
- IIC主设备功能:主要产生时钟,产生起始信号和停止信号
- IIC从设备功能:可编程的IIC地址检测,停止位检测
- IIC的一个优点是它支持多主控(multimastering), 其中任何一个能够进行发送和接收的设备都可以成为主总线。一个主控能够控制信号的传输和时钟频率。当然,在任何时间点上只能有一个主控。
- 支持不同速率的通讯速度,标准速度(最高速度100kHZ),快速(最高400kHZ)
- SCL和SDA都需要接上拉电阻 (大小由速度和容性负载决定一般在3.3K-10K之间) 保证数据的稳定性,减少干扰。
- IIC是半双工,而不是全双工 ,同一时间只可以单向通信
- 为了避免总线信号的混乱,要求各设备连接到总线的输出端时必须是漏极开路(OD)输出或集电极开路(OC)输出。这一点在等下我们会讲解
三、硬件结构
总线结构
IIC使用两根信号线进行通信,要求两根线都使用 开漏输出接上拉电阻 的配置,以此实现总线上所有节点SDA、SCL信号的 线与 逻辑关系。
Rp电阻的取值有一定的要求,太小会导致灌入电流过大,使’低’数据不稳定,甚至损坏端口;太大会导致信号上升缓慢,使得数据传输出错。在不同应用场景及供电电压下有不同的取值要求。
IIC的高阻态
漏极开路(Open Drain)即高阻状态,适用于输入/输出,其可独立输入/输出低电平和高阻状态,若需要产生高电平,则需使用外部上拉电阻
高阻状态:高阻状态是三态门电路的一种状态。逻辑门的输出除有高、低电平两种状态外,还有第三种状态——高阻状态的门电路。电路分析时高阻态可做开路理解。
我们知道IIC的所有设备是接在一根总线上的,那么我们进行通信的时候往往只是几个设备进行通信,那么这时候其余的空闲设备可能会受到总线干扰,或者干扰到总线,怎么办呢?
为了避免总线信号的混乱,IIC的空闲状态只能有外部上拉, 而此时空闲设备被拉到了高阻态,也就是相当于断路, 整个IIC总线只有开启了的设备才会正常进行通信,而不会干扰到其他设备。
IIC器件地址
每一个IIC器件都有一个器件地址,有的器件地址在出厂时地址就设定好了,用户不可以更改,比如OV7670的地址为0x42。有的器件例如EEPROM,前四个地址已经确定为1010,后三个地址是由硬件链接确定的,所以一IIC总线最多能连8个EEPROM芯片。
四、工作时序
1.空闲状态
void IIC_init() //IIC初始化
{
SCL=1; //首先把时钟线拉高
delay_us(4);//延时函数
SDA=1; //在SCL为高的情况下把SDA拉高
delay_us(4); //延时函数
}
2.开始和结束信号
开始信号(START/S): SCL保持高电平,SDA由高电平变为低电平后,延时(>4.7us),SCL变为低电平。
//产生IIC起始信号
//1.先拉高SDA,再拉高SCL,空闲状态
//2.拉低SDA
void IIC_Start() //启动信号
{
SDA=1; //确保SDA线为高电平
delay_us(5);
SCL=1; //确保SCL高电平
delay_us(5);
SDA=0; //在SCL为高时拉低SDA线,即为起始信号
delay_us(5);
SCL=0; //钳住I2C总线,准备发送或接收数据
}
结束信号(STOP/P) :SCL保持高电平。SDA由低电平变为高电平。
//产生IIC停止信号
//1.先拉低SDA,再拉低SCL
//2.拉高SCL
//3.拉高SDA
//4.停止接收数据
void IIC_Stop(void)
{
IIC_SCL=0;
IIC_SDA=0; //STOP:当SCL高时,数据由低变高
delay_us(4);
IIC_SCL=1;
IIC_SDA=1; //发送I2C总线结束信号
delay_us(4);
}
在起始条件产生后,总线处于忙状态,由本次数据传输的主从设备独占,其他I2C器件无法访问总线;而在停止条件产生后,本次数据传输的主从设备将释放总线,总线再次处于空闲状态。
3.应答信号
每当主机向从机发送完一个字节的数据,主机总是需要等待从机给出一个应答信号,以确认从机是否成功接收到了数据,
应答信号:主机SCL拉高,读取从机SDA的电平,为低电平表示产生应答
- 应答信号为低电平时,规定为有效应答位(ACK,简称应答位),表示接收器已经成功地接收了该字节;
- 应答信号为高电平时,规定为非应答位(NACK),一般表示接收器接收该字节没有成功。
4.数据有效性
IIC信号在数据传输过程中,当SCL=1高电平时,数据线SDA必须保持稳定状态,不允许有电平跳变,只有在时钟线上的信号为低电平期间,数据线上的高电平或低电平状态才允许变化。
SCL=1 时 数据线SDA的任何电平变换会看做是总线的起始信号或者停止信号。
也就是在IIC传输数据的过程中,SCL时钟线会频繁的转换电平,以保证数据的传输
每发送一个字节(8个bit)在一个字节传输的8个时钟后的第九个时钟期间,接收器接收数据后必须回一个ACK应答信号给发送器,这样才能进行数据传输。
主机发送数据,从机接收时,ACK信号由从机发出。当在SCL第9位时钟高电平信号期间,如果SDA仍然保持高电平,则主机可以直接产生STOP条件终止以后的传输或者继续ReSTART开始一个新的传输
从机发送数据,主机读取数据时,ACK信号由主机给出。主机响应ACK表示还需要再接收数据,而当主机接收完想要的数据后,通过发送NACK告诉从机读取数据结束、释放总线。随后主机发送STOP命令,将总线释放,结束读操作。
应答出现在每一次主机完成8个数据位传输后紧跟着的时钟周期,低电平0表示应答,1表示非应答
//主机产生应答信号ACK
//1.先拉低SCL,再拉低SDA
//2.拉高SCL
//3.拉低SCL## 标题
void I2C_Ack(void)
{
IIC_SCL=0; //先拉低SCL,使得SDA数据可以发生改变
IIC_SDA=0;
delay_us(2);
IIC_SCL=1;
delay_us(5);
IIC_SCL=0;
}
//主机不产生应答信号NACK
//1.先拉低SCL,再拉高SDA
//2.拉高SCL
//3.拉低SCL
void I2C_NAck(void)
{
IIC_SCL=0; //先拉低SCL,使得SDA数据可以发生改变
IIC_SDA=1; //拉高SDA,不产生应答信号
delay_us(2);
IIC_SCL=1;
delay_us(5);
IIC_SCL=0;
}
等待应答信号:
//等待应答信号到来
//返回值:1,接收应答失败
// 0,接收应答成功
char IIC_Wait_Ack(void)
{
u8 ucErrTime=0;
IIC_SDA=1;delay_us(1);
IIC_SCL=1;delay_us(1);
while(IIC_SDA)
{
ucErrTime++;
if(ucErrTime>250)
{
IIC_Stop();
return 1;
}
}
IIC_SCL=0;//时钟输出0
return 0;
}
4. 字节格式
SDA线上的数据在SCL时钟“高”期间必须是稳定的,只有当SCL线上的时钟信号为低时,数据线上的“高”或“低”状态才可以改变。输出到SDA线上的每个字节必须是8位,数据传送时,先传送最高位(MSB),每一个被传送的字节后面都必须跟随一位应答位(即一帧共有9位)。
当一个字节按数据位从高位到低位的顺序传输完后,紧接着从设备将拉低SDA线,回传给主设备一个应答位ACK, 此时才认为一个字节真正的被传输完成 ,如果一段时间内没有收到从机的应答信号,则自动认为从机已正确接收到数据。
5. 7-bit 设备的数据传输
一个完整的7-bit通信过程如图所示:
在START信号后八位设备地址=7位从机地址+读/写地址,结构如下图:
再给地址添加一个方向位位用来表示接下来数据传输的方向,
- 0表示主设备向从设备(write)写数据,
- 1表示主设备向从设备(read)读数据
发送完这个字节之后,主机释放SDA总线等待从机给出ACK应答。如果从机给出了ACK应答,表示从机地址正确(有从机响应)并且已知晓是读还是写,便可以开始读写数据。如果从机没有给出ACK应答,则说明接收设备可能没有收到数据(如寻址的设备不存在或是设备正在忙碌)或无法解析收到的数据,如果是这样,则由主机来决定如何处理(STOP或ReSTART)。
IIC发送一个字节数据:
//IIC发送一个字节
//返回从机有无应答
//1,有应答
//0,无应答
//IIC_SCL=0;
//在SCL上升沿时准备好数据,进行传送数据时,拉高拉低SDA,因为传输一个字节,一个SCL脉冲里传输一个位。
//数据传输过程中,数据传输保持稳定(在SCL高电平期间,SDA一直保持稳定,没有跳变)
//只有当SCL被拉低后,SDA才能被改变
//总结:在SCL为高电平期间,发送数据,发送8次数据,数据为1,SDA被拉高,数据为0,SDA被拉低。
//传输期间保持传输稳定,所以数据线仅可以在时钟SCL为低电平时改变。
void IIC_Send_Byte(u8 txd)
{
u8 t;
SDA_OUT();
IIC_SCL=0;//拉低时钟开始数据传输
for(t=0;t<8;t++)
{
//IIC_SDA=txd&0x80; //获取最高位
//获取数据的最高位,然后数据左移一位
//如果某位为1,则SDA为1,否则相反
if(txd&0x80)
IIC_SDA=1;
else
IIC_SDA=0;
txd<<=1;
delay_us(2);
IIC_SCL=1;
delay_us(2);
IIC_SCL=0;
delay_us(2);
}
}
或者:
//IIC_SDA=txd&0x80; //获取最高位
//获取数据的最高位,然后右移7位,假设为 1000 0000 右移7位为 0000 0001
// 假设为 0000 0000 右移7位为 0000 0000
//如果某位为1,则SDA为1,否则相反
IIC_SDA=((txd&0x80)>>7);
txd<<=1;
IIC读取一个字节数据:
//读1个字节,ack=1时,发送ACK,ack=0,发送nACK
u8 IIC_Read_Byte(unsigned char ack)
{
unsigned char i,receive=0;
SDA_IN(); //SDA设置为输入
for(i=0;i<8;i++ )
{
IIC_SCL=0;
delay_us(2);
IIC_SCL=1;
receive<<=1;
if(READ_SDA)receive++;
delay_us(1);
}
if (!ack)
IIC_NAck(); //发送nACK
else
IIC_Ack(); //发送ACK
return receive;
}
6.主机写-从机收,传输方向不变
主机对从机发送数据时,主机对从机发送一个开始字节,然后即可一直发送数据。以示例来讲解,其第一帧数据为要操作的寄存器地址,所以为:“[1-Byte]开始字节(写) + [1-Byte]寄存器地址 + [1-Byte]寄存器数据”。
主机要向从机写数据时:
- 主机首先产生START信号
- 然后紧跟着发送一个从机地址,这个地址共有7位,紧接着的第8位是数据方 向位(R/W),0表示主机发送数据(写),1表示主机接收数据(读)
- 主机发送地址时,总线上的每个从机都将这7位地址码与自己的地址进行比较,若相同,则认为自己正在被主机寻址,根据R/T位将自己确定为发送器和接收器
- 这时候主机等待从机的应答信号(A)
- 当主机收到应答信号时,发送要访问从机的那个地址, 继续等待从机的应答信号
- 当主机收到应答信号时,发送N个字节的数据,继续等待从机的N次应答信号,
- 主机产生停止信号,结束传送过程。
7.主机读-从机发,传输方向改变
主机对向从机读取数据时,方式同发送数据有所不同,要多一次通信过程。
主机需要先向从机发送一次信号,告诉从机”我要读取数据“,然后重开一次通信,等待从机主动返回数据。以示例来讲解,发送 “[1-Byte]开始字节(写) + [1-Byte]要读取的寄存器的地址”,之后结束通信,或者重开始,来进入到第二次通信中,先发送 [1-Byte]开始字节(读),然后等待读取从机发送过来的 [1-Byte]数据 即可。
主机要从从机读数据时
- 主机首先产生START信号
- 然后紧跟着发送一个从机地址,注意此时该地址的第8位为0,表明是向从机写命令,
- 这时候主机等待从机的应答信号(ACK)
- 当主机收到应答信号时,发送要访问的地址,继续等待从机的应答信号,
- 当主机收到应答信号后,主机要改变通信模式(主机将由发送变为接收,从机将由接收变为发送)所以主机重新发送一个开始start信号,然后紧跟着发送一个从机地址,注意此时该地址的第8位为1,表明将主机设 置成接收模式开始读取数据,
- 这时候主机等待从机的应答信号,当主机收到应答信号时,就可以接收1个字节的数据,当接收完成后,主机发送非应答信号,表示不在接收数据
- 主机进而产生停止信号,结束传送过程。
9.2 以AT24C02为例子说明
下面是向0x13位置读数据:0x13
24C02是一个2K Bit的串行EEPROM存储器(掉电不丢失),内部含有256个字节。在24C02里面有一个8字节的页写缓冲器。
A0,A1,A2:硬件地址引脚
WP:写保护引脚,接高电平只读,接地允许读和写
SCL和SDA:IIC总线
芯片的寻址:
AT24C设备地址为如下,前四位固定为1010,A2~A0为由管脚电平。AT24CXX EEPROM Board模块中默认为接地。A2~A0为000,最后一位表示读写操作。所以AT24Cxx的读地址为0xA1,写地址为0xA0。也就是说如果是
写24C02的时候,从器件地址为10100000(0xA0);
读24C02的时候,从器件地址为10100001(0xA1)。
片内地址寻址:
芯片寻址可对内部256B中的任一个进行读/写操作,其寻址范围为00~FF,共256个寻址单位。
向AT24C02中写数据:
1.MCU先发送一个开始信号(START)启动总线
2.接着跟上首字节,发送器件写操作地址(DEVICE ADDRESS)+写数据(0xA0)
3.等待应答信号(ACK)
4.发送数据的存储地址。24C02一共有256个字节的存储空间,地址从0x00~0xFF,想把数据存储在哪个位置,此刻写的就是哪个地址。
5.发送要存储的数据第一字节、第二字节、…注意在写数据的过程中,E2PROM每个字节都会回应一个“应答位0”,告诉我们写E2PROM数据成功,如果没有回应答位,说明写入不成功。
6.发送结束信号(STOP)停止总线
注意:
在写数据的过程中,每成功写入一个字节,E2PROM存储空间的地址就会自动加1,当加到0xFF后,再写一个字节,地址就会溢出又变成0x00。
写数据的时候需要注意,E2PROM是先写到缓冲区,然后再“搬运到”到掉电非易失区。所以这个过程需要一定的时间,AT24C02这个过程是不超过5ms!
所以,当我们在写多个字节时,写入一个字节之后,再写入下一个字节之前,必须延时5ms才可以。
从AT24C02中读数据:
1.当前地址读
1.MCU先发送一个开始信号(START)启动总线
2.接着跟上首字节,发送器件写操作地址(DEVICE ADDRESS)+写数据(0xA0)
注意:这里写操作是为了要把所要读的数据的存储地址先写进去,告诉E2PROM要读取哪个地址的数据。
3.发送要读取内存的地址(WORD ADDRESS),通知E2PROM读取要哪个地址的信息。
4.重新发送开始信号(START)
5.发送设备读操作地址(DEVICE ADDRESS)对E2PROM进行读操作 (0xA1)
6.E2PROM会自动向主机发送数据,主机读取从器件发回的数据,在读一个字节后,MCU会回应一个应答信号(ACK)后,E2PROM会继续传输下一个地址的数据,MCU不断回应应答信号可以不断读取内存的数据
7.如果不想读了,告诉E2PROM不想要数据了,就发送一个“非应答位NAK(1)”。发送结束信号(STOP)停止总线
/****
*******向AT24C02的地址address中写入一个字节的数据info
*****/
void At24c02_Write_Add(uchar address,uchar info)
{
At24c02_Start();
At24c02_Writex(0xa0);
At24c02_Clock();
At24c02_Writex(address);
At24c02_Clock();
At24c02_Writex(info);
At24c02_Clock();
At24c02_Stop();
At24c02_Delay(50);
}
/****
*******从AT24C02的地址address中读取一个字节的数据
*****/
uchar At24c02_Read_Add(uchar address)
{
uchar i;
At24c02_Start();
At24c02_Writex(0xa0);
At24c02_Clock();
At24c02_Writex(address);
At24c02_Clock();
At24c02_Start();
At24c02_Writex(0xa1);
At24c02_Clock();
i=At24c02_Readx();
At24c02_Stop();
At24c02_Delay(1);
return(i);
}
9.3 AT24C02驱动程序
/**********************************
包含头文件
**********************************/
#include "at24c02.h"
/****************************************************
* 函 数 名 : delay_10us
* 函数功能 : 延时函数,ten_us=1时,大约延时10us
* 输 入 : ten_us
* 输 出 : 无
****************************************************/
void At24c02_Delay(uint ten_us)
{
while(ten_us--);
}
/****
*******总线初始化,拉高释放总线函数
*****/
void At24c02_Init(void)
{
AT24C02_SCL=1;
At24c02_Delay(1);
AT24C02_SDA=1;
At24c02_Delay(1);
}
/****
*******当SCL为高电平时,SDA由高变为低
*****/
void At24c02_Start(void)
{
AT24C02_SDA=0; //发送起始信号
At24c02_Delay(1);
AT24C02_SCL=0; //钳住IIC总线,准备发送或接收数据
At24c02_Delay(1);
}
/****
*******当SCL为高电平时,SDA由低变为高
*****/
void At24c02_Stop(void)
{
AT24C02_SDA=0; //发送结束条件的数据信号
At24c02_Delay(1);
AT24C02_SCL=1; //发送结束条件的时钟信号
At24c02_Delay(1);
AT24C02_SDA=1; //发送结束信号
At24c02_Delay(1);
}
/****
*******等待应答信号
*****/
void At24c02_Clock(void)
{
uchar i=0;
AT24C02_SCL=1;
At24c02_Delay(1);
while((AT24C02_SDA==1)&&(i<255))
i++;
AT24C02_SCL=0;
At24c02_Delay(1);
}
/****
*******写一个字节
*****/
void At24c02_Writex(uchar temp)
{
uchar i;
AT24C02_SCL=0;
for (i=0;i<8;i++)
{
if((temp&0x80)>0)
IIC_SDA=1;
else
IIC_SDA=0;
temp<<=1;
AT24C02_SCL=1;
At24c02_Delay(1);
AT24C02_SCL=0;
At24c02_Delay(1);
}
}
/****
*******读取一个字节
*****/
uchar At24c02_Readx(void)
{
uchar i,j,k=0;
AT24C02_SCL=0;
At24c02_Delay(1);
AT24C02_SDA=1;
for (i=0;i<8;i++)
{
AT24C02_SCL=0;
At24c02_Delay(1);
AT24C02_SCL=1;
At24c02_Delay(1);
if (AT24C02_SDA==1)
j=1;
else
j=0;
k=(k<<1)|j; //将8个独立的位放在一个字节中
}
At24c02_Delay(1);
return(k);
}
/****
*******向AT24C02的地址address中写入一个字节的数据info
*****/
void At24c02_Write_Add(uchar address,uchar info)
{
At24c02_Start();
At24c02_Writex(0xa0);
At24c02_Clock();
At24c02_Writex(address);
At24c02_Clock();
At24c02_Writex(info);
At24c02_Clock();
At24c02_Stop();
At24c02_Delay(50);
}
/****
*******从AT24C02的地址address中读取一个字节的数据
*****/
uchar At24c02_Read_Add(uchar address)
{
uchar i;
At24c02_Start();
At24c02_Writex(0xa0);
At24c02_Clock();
At24c02_Writex(address);
At24c02_Clock();
At24c02_Start();
At24c02_Writex(0xa1);
At24c02_Clock();
i=At24c02_Readx();
At24c02_Stop();
At24c02_Delay(1);
return(i);
}