什么是IIC?
IIC == Inter Integrated Circuit ==内部集成电路 == I2C;
是Philips公司开发的一种通讯协议
IIC有什么用?
通讯都是用来传数据的,IIC也不例外;
常见的应用有EEPROM读写、mems传感器数据获取、摄像头配置。。。。。。
IIC分类
硬件IIC:芯片里面把IIC的通讯协议通过电路实现了,有专用的IIC引脚,只需要配置下寄存器就能实现IIC通讯;
软件IIC:根据IIC通讯的时序协议,自己找两个引脚,按照IIC协议来比划一下,搞得跟硬件IIC差不多。
IIC物理层
所谓物理层就是硬件构成了
1、得有两根线,一根线是时钟线,一根线是数据线;
2、至少得有1个发送数据的对象,和1个接收数据的对象;
3、这个总线上的每个器件得有一个不同的地址,方便知道是谁发的数据,数据发给谁;
4、数据线和时钟线上一般都搞个上拉电阻,没人用的时候时钟线和数据线上都是高电平。
SCL就是IIC总线的时钟线,这个时钟线上的时钟,只能由主设备(Master)产生,常见的主设备就是各种单片机,处理器了;
SDA计时IIC的数据线,数据可以由主设备发给从设备(Slave),这个过程叫“写”,也可以由从设备发给主设备,这个过程叫“读”;
主机(master),就是老板,上班时间、下班时间、都是他来决定的,指令都是他来发出来的,他要找谁干啥,谁就干啥。
从机(Slave),就是员工,这个slave很形象,就是奴,不能有自己的想法,一切听老板的。
为什么要在总线上加上拉电阻?
有了这个上拉电阻,就可以保证IIC总线没人使用的时候一直是高电平;
IIC接口
漏极开路,Open Drain,也就是高阻态,可独立的输入输出低电平和高阻态,如果要使用高电平,需要外部的上拉电阻。
高阻态,三态门电路的一种,高电平、低电平、高阻态,高阻态可理解为开路,
IIC总线上没开启的设备都是高阻态,相当于开路,只有开启的设备才能正常通信
IIC协议
怎么开始
SCL为高电平的时候,SDA由高电平变为低电平,开始传送数据
怎么结束
SCL为高电平的时候,SDA由低电平变为高电平,停止传输数据
怎么应答
接收者收到8个bit数据后,想发送者发出低电平脉冲,表示收到数据
怎么非应答
IIC通讯流程
1、空闲的时候,也就是没有数据传输的时候,SCL和SDA都是高电平(这是拜上拉电阻所赐);
2、要开始了,就在上图的Start那个点,SCL是高电平,SDA从高电平变成低电平(这是主机干得);
3、数据传输,主设备产生时钟信号,就是SCL上的时钟电平,同时SDA线上就有数据,至于这个数据是主设备发给从设备的,还是从设备发给主设备的,暂时不用理会。每个BIT位的数据是0,还是1,这很重要,所以呢,规定每个BIT的数据都要在时钟处于搞电平的时候采样,采到的是1,那就是1,采到的是0 ,那这一位就是0;
4、只要SDA数据线上发了8个BIT的数据,就要发出一个应答信号ACK,谁接收数据,谁就要应答,应答信号是个低电平。
5、可以结束了,就在上图的Stop点,SCL为高电平,SDA从低电平变为高电平(这也是主机干的);
6、现在总线又恢复到空闲状态了,SCL和SDA都是高电平。
从上面可以看出,
什么时候开始传输数据是主机说的算的,什么时候停止传输数据也是主机说的算的,什么时候对数据进行采样,还是主机说的算的;
从机只有在应答和传输数据的时候,才能改变SDA线的状态,真是个好奴才啊。
上面只是传输一个字节的过程,帮助理解IIC协议是怎么规定起始信号、结束信号、应答信号。
IIC完整的通讯过程
1、总线是空闲状态,SCL=1,SDA =1;
2、要开始传输数据了,此时SCL还是高电平,SCL=1,主机将SDA从1变成0;
3、跟哪个从机通讯,把从机的地址发出去。一般地址是8个bit(也有16个bit的),这8个bit其实真是的地址是7个bit,最后1个bit是用来表示读或者写的。1表示读,0表示写;这个过程相当于主机往SDA上发了8个bit的数据(地址也是数据啊);
4、主机发地址的过程,相当于在找从机,从机是要给应答信号的,就是ACK,就像奴隶主喊奴隶,奴隶得回应一下,你老板喊你,你也得先回答声A吧;
5、应答之后,就是要传输数据了,如果第3步中发的地址是写操作,那就由主机来控制SDA的电平变化,如果第3步中发的地址是读操作,那就由从机来控制SDA的电平变化;
6、每次8bit的数据传输完成,都要有个应答信号,谁接收数据,谁来应答
7、完事之后,SCL就是高电平了,主机把SDA从低电平拉高,表示结束。
主机写的过程
奴隶主(master):都给我站好了(start);
奴隶主(master):奴隶A,吩咐你几件事。(写从设别地址)
奴隶A(slave):好的(应答信号ACK);(从设备应答)
奴隶主(master):事情A是XXXX;(主机写数据)
奴隶A(slave):好的(从机应答信号ACK);
奴隶主(master):事情B是XXXX;(主机继续写)
奴隶A(slave):好的(从机应答信号ACK);
奴隶主(master):行了,就这两件事,解散吧(stop);
主机读的过程(读比写要麻烦一些)
读一个字节
奴隶主(master):都给我站好了(start);
奴隶主(master):奴隶A,我要问你一个问题(写入从设备的地址,后面还要写东西)
奴隶A(slave):好的,你问吧(应答信号ACK);
奴隶主(master):告诉我,你儿子多大了(写入从设备的寄存器地址)
奴隶A(slave):好的(应答信号ACK);
奴隶A(slave):我儿子今年三岁了;(从机返回对应寄存器的数据)
奴隶主(master):知道了,没事了(主设备非应答No ACK)
奴隶主(master):都散了吧(Stop);
IIC的传输过程只需要牢记以下几点就可以了
1、开始信号和结束信号都是主机来控制的;
2、SCL上的时钟信号也是主机控制的;
3、谁接收数据,谁就要发出个应答信号ACK,ACK是低电平;
4、发送数据都是一个字节一个字节的发送的,每个字节都要有应答
起始信号、结束信号、应答信号、非应答信号的C语言实现
Microchip的24C02B(97年之前的)的IIC时序图
海天芯的24C02 (国产的20年左右的芯片) IIC时序图
开始与结束信号
/**
* 适用芯片Microchip 24C02B
* 函数: I2C_Start()
* 一个_nop_耗时,一个机器周期,针对89C52就是12个时钟周期,晶振为11.0592M的话
* 一个_nop_就好是12/11.0592=1.085us,5个_nop_,就是5.43us
*/
void I2C_Start()
{
SDA = 1; //SDA为高电平
nops(); //这个延时放着也没关系,从图中可以看出SDA比SCL先高电平
SCL = 1; //把时钟线拉高
nops(); //SCL拉高之后,要保持一段时间,这个高电平要持续Tsu:STA时间,至少也是4700ns
SDA = 0; //把 SCL 为高电平的时候把SDA拉低
nops(); //拉低之后需要维持一段时间,T_HD:STA,也是4000ns以上
SCL = 0; //再把SCL拉低
}
/**
* 适用芯片海天芯 HT24C02
* 函数: I2C_Start()
* 一个_nop_耗时,一个机器周期,针对89C52就是12个时钟周期,晶振为11.0592M的话
* 一个_nop_就好是12/11.0592=1.085us,5个_nop_,就是5.43us
*/
void I2C_Start()
{
SDA = 1; //SDA为高电平
_nop_(); //这个延时放着也没关系,从图中可以看出SDA比SCL先高电平
SCL = 1; //把时钟线拉高
_nop_(); //SCL拉高之后,要保持一段时间,这个高电平要持续Tsu:STA时间,至少也是4700ns
SDA = 0; //把 SCL 为高电平的时候把SDA拉低
_nop_(); //拉低之后需要维持一段时间,T_HD:STA,也是4000ns以上
SCL = 0; //再把SCL拉低
}
/**
* 适用芯片Microchip 24C02B
* 函数: I2C_Stop()
* 功能: 停止i2c
* 一个_nop_耗时,一个机器周期,针对89C52就是12个时钟周期,晶振为11.0592M的话
* 一个_nop_就好是12/11.0592=1.085us,5个_nop_,就是5.43us
*
*/
void I2C_Stop()
{
SCL = 0; //SCL先拉低
nops(); //这个时间是久了一些,改为一个nop就可以了
SDA = 0; //SDA也要拉低,
nops(); //这个时间是久了一些,改为一个nop就可以了
SCL = 1;//SCL拉高,
nops(); //SCL拉高维持时间T_SU:STO,4000ns以上
SDA = 1; //SDA拉高
nops(); //SDA拉高持续时间T_BUF,4700ns以上
}
/**
* 适用芯片海天芯 HT24C02
* 函数: I2C_Stop()
* 功能: 停止i2c
* 一个_nop_耗时,一个机器周期,针对89C52就是12个时钟周期,晶振为11.0592M的话
* 一个_nop_就好是12/11.0592=1.085us,5个_nop_,就是5.43us
*
*/
void I2C_Stop()
{
SCL = 0; //SCL先拉低
_nop_();
SDA = 0; //SDA也要拉低,
_nop_();
SCL = 1;//SCL拉高,
_nop_(); //SCL拉高维持时间T_SU:STO,
SDA = 1; //SDA拉高
_nop_(); //SDA拉高持续时间T_BUF
}
应答与非应答信号
前面说过,数据发给谁,谁就要给个应答ACK,如果没有发出应答,就表示数据没收到。
这句话本身没毛病,主机给从机发送一个数据,比如MCU给24C02发送一个数据,24C02都会自动发出一个ack信号,表示收到了,如果24C02没有返回ack信号,MCU有理由相信24C02没收到刚才发过去的数据,可以报错,也可以重新发送。
但是,如果从机给主机发数据,比如24C02给MCU发数据,讲道理,24C02每发一个数据,MCU就要发出一个ack信号,但是有个例外,24C02在发最后一个数据的时候(所谓的最后一个,是MCU认为的最后一个),MCU返回的是非应答信号nack(也叫no ack, 叫它非应答和不应答都可以),因为NACK就是让SDA释放总线,就是什么都不做,SDA总线自然就被上拉电阻拉高了。非应答信号之后,紧接着的就是主机发出结束信号。
所以大家要明白这其中的潜规则:
从机只要收到数据,就一定要返回应答信号ack,不返回就是有问题;
主机收到数据也要返回应答信号ack,但主机返回ack信号是为让从机接着发数据的;
主机收到数据后如果没有返回应答信号ack,而是nack,就表示主机要结束通讯了;
好比,你向领导汇报工作:
你:我做了事情A;
领导:嗯;(ack,暗示你接着说)
你:我还做了事情B;
领导:嗯;(ack,暗示你接着说)
你:我还做了事情C;
领导:可以了,你走吧(nack,要结束了)
有效应答ACK的要求是:接收器在第9个脉冲到来之前的低电平期间,将SDA拉低,一直保持到脉冲高电平期间。
产生应答信号(主机给从机的)
void I2C_Ack(void)
{
SCL = 0;
SDA = 0;
nops(); //在第9个脉冲到来之前的低电平期间,把SDA拉低
SCL = 1; //SCL高电平,表示第9个脉冲
nops();
SCL = 0 //SCL拉低,方便后续的读写
}
产生非应答信号(主机给从机的)
非应答和应答的唯一区别就是:
应答SDA是低电平
非应答SDA是高电平
void I2C_Nack(void)
{
SCL = 0;
SDA = 1;
nops(); //在第9个脉冲到来之前的低电平期间,释放SDA总线,让上拉电阻把SDA拉高
SCL = 1; //SCL高电平,表示第9个脉冲
nops();
SCL = 0 //SCL拉低,方便后续的读写
}
检测应答信号(从机给主机的)
从机只要收到数据就要无条件返回应答ack,主机就是通过检查这个ack来判断从机是否收到
unsigned char I2C_Wait_Ack(void)
{
unsigned char ucErr = 0;
SDA = 1;
nops();
SCL = 1;
nops();
while(SDA) //SDA为高电平,就表示没有检查到ACK,
{
ucErr++;
if(ucErr > 250) //等一段时间,还没有ack,就停止总线
{
I2C_Stop();
return 1;
}
}
SCL = 0;
return 0; //能到这里就表示检测到应答信号了
}
IIC发送一个字节的数据(主机发给从机的)
这个数据的宽度是有要求的,不同的从设备对有效数据的宽度时间不大一样,Microchip 的AT24C02B,要求T_HIGH 4000ns以上, T_LOW 4700ns以上,所以那些C语言实现的函数都是有适用对象的,不能随便复制就了事
void I2C_Send_Byte(unsigned char dat)
{
unsigned char i;
SCL = 0; //拉低时钟,准备数据
for(i=0;i<8;i++)
{
SDA = (dat & 0x80) >> 7;先发高位,再发低位,这个逻辑自己分析下
dat << 1 //数据的高位发出去了,左移一位,接着发送次高位......
nops(); //数据要保持一段时间,让SCL是高电平的时候,数据已经稳定了
SCL = 1; //SCL 拉高
nops(); //针对Microchip的24C02B,延时要4700ns以上
scl = 0; //SCL 拉低
nops(); //拉低也要持续一段时间
}
}
接收一个字节的数据(从机发给主机)
主机收到数据就要发应答信号,或者非应答信号
unsigned char I2C_Read_Byte(unsigned char ack)
{
unsigned char i;
unsigned char receive = 0;
for(i=0; i<8; i++)
{
SCL = 0;
nops();
SCL = 1;//时钟高电平的时候,接收的数据移位
receive <<= 1;
if(SDA) //如果SDA是高电平,receive +1
{
receive++;
}
nops(); //
}
if(!ack) //非应答信号
{
I2C_Nack();
} else{ //应答信号
I2C_Ack();
}
return receive;
}
EEPROM读写
向24C02的某个寄存器,写一个字节
1、主机(MCU)发出起始信号;
2、主机(MCU)发出从机的地址,并表示接下来还要写东西(就是发送写地址);
3、从机应答
4、主机(MCU)发出从机的寄存器地址;
5、从机应答
6、主机(MCU)发出要写的数据;
7、从机应答
8、主机(MCU)发出结束信号
//MCU 向24C02的某个寄存器写一个字节的数据
//很多人不喜欢检查从机的应答信号,其实在工程上是不规范的;
//虽然大部分情况下,不检查应答信号,也能正常使用
//就像国家免检产品一样,默认你不会出问题,一出问题就是大问题
void eeprom_Write_Byte(unsigned char addr, unsigned char dat)
{
//1、MCU发出起始信号
I2C_Start();
//2、MCU发送从设备的地址,最后一位是写
I2C_Send_Byte(DEV_ADDR | WR);
//3、MCU检查从机的应答
I2C_Wait_Ack();
//4、MCU发出从机的寄存器地址
I2C_Send_Byte(addr);
//5、MCU检查从机的应答
I2C_Wait_Ack();
//6、MCU发出要写的数据
I2C_Send_Byte(dat);
//7、MCU检查从机的应答
I2C_Wait_Ack();
//8、MCU发出停止信号
I2C_Stop();
}
写多个字节
//多次调用写一个字节的函数来实现写多个字节
eeprom_Write_NBytes(unsigned char addr, *pdata, size)
{
unsigned char i ;
for(i=0;i<size;i++)
{
eeprom_Write_Byte(addr, pdata[i]);
addr++; //寄存器地址自动增加
}
}
向24C02写多个字节(页写模式)
1、主机(MCU)发出起始信号;
2、主机(MCU)发出从机的地址,并表示接下来还要写东西(就是发送写地址);
3、从机应答
4、主机(MCU)发出从机的寄存器地址;
5、从机应答
6、主机(MCU)发出数据1;
7、从机应答
8、主机(MCU)发出数据2;
9、从机应答
......
10、主机(MCU)发出数据X;
11、从机应答
12、主机(MCU)发出结束信号
数据手册上有说明24C02的写周期是5ms,可以理解为每写完一次,就等5ms.
读当前寄存器的值
随机地址读取
读一个字节
1、MCU发送开始信号
2、MCU发送从设备写地址
3、MCU检测从机应答信号
4、MCU发送从机寄存器地址
5、MCU检测从机应答信号
6、MCU再次发送开始信号
7、MCU发送从设备读地址
8、MCU检测从机应答信号
9、从设备返回寄存器的数据
10、MCU发出非应答信号
11、MCU发出结束信号
//读24C02一个字节
void eeprom_Read_Byte(unsigned char addr, *pdata)
{
//1、MCU发出起始信号
I2C_Start();
//2、MCU发送从设备的写地址
I2C_Send_Byte(DEV_ADDR | WR);
//3、MCU检查从机的应答
I2C_Wait_Ack();
//4、MCU发出从机的寄存器地址
I2C_Send_Byte(addr);
//5、MCU检查从机的应答
I2C_Wait_Ack();
//6、MCU再次发出开始信号
I2C_Start();
//7、MCU发送从机的读地址
I2C_Send_Byte(DEV_ADDR | RD);
//8、MCU检测从机应答
I2C_Wait_Ack();
//9、从机返回寄存器的数据
//10、MCU发出非应答NACK信号
*pdata = I2C_Read_Byte(NACK);
//11、MCU发出结束信号
I2C_Stop();
}
读多个字节,可以把读一个字节重复几遍
//读eeprom多个字节
//就是把读一个字节重复几遍
void eeprom_Read_NBytes(unsigned char addr, *pdata, size)
{
unsigned char i;
for(i=0;i<size;i++)
{
eeprom_Read_Byte(addr, &pdata[i]);
addr++;
}
}