51单片机学习6--IIC通讯

什么是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++;
    }
}

连续读

  • 7
    点赞
  • 34
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值