I2C通信的实践,学习笔记

本文是我子自己实际工作中,对I2C通信协议的学习,实现过程的一个总结。它记录了我从对I2C一无所知到最终能够熟练实现I2C协议的一个过程。希望能够帮到不了解I2C通信协议却正好要使用I2C的一些小伙伴们。叙述的方式还是一点一点来,尽量单纯,用到哪儿在详细说哪儿。


一提到通信我们自然会想到要有两个设备,在它们之间相互传递数据的过程就叫通信。那么它们怎么传递数据呢?硬件上怎么连接?什么时候开始发送数据?什么时候结束发送?先发送高位还是低位?等等这一系列问题都要事先约定好,设备双方才能进行通信,那么这一系列事先约定好的通信规则就叫做通信协议,这里我们介绍的通信协议叫I2C通信协议

我们要将协议中规定的这些规则告诉这两个设备,以便它们在互相通信时能按照这套规则进行运转,最终达到收发数据的目的。怎么告诉它们呢?设备听不懂中国话,英国话......,但是它们能听懂C语言(当然最终会由编译器转换为机器指令),我们使用C语言编写程序,程序的功能是让这两个设备按照I2C协议规定的规则进行收/发数据,那么这个过程就叫做在这两个设备上实现I2C通信协议。为了分辨两个设备,我们把一个叫做主机(Master),另一个叫做从机(Slaver),它们的行为不太一样,所以所需要的程序也不同,一个叫做主机端的I2C协议实现,另一个叫做从机端的I2C协议实现,也就是说我们要写两套程序。注意:在实际项目中一般我们只涉及到一端。

对于通信协议的实现,其实最终的表现形式就是提供几个用于通信的接口函数,以便主程序通过调用这些接口来实现在两个设备间通信的目的。在I2C中一般应提供如下最基本的通信接口函数:

void i2c_master_read(char* buf, int len);

void i2c_master_write(char* buf, int len);

void i2c_slaver_read(char* buf, int len);

void i2c_slaver_write(char* buf, int len);

通过函数名称我们可以知道它们的作用。例如,如果Master想要向Slaver发送数据,就可以调用i2c_master_write(...)函数,如果想从Slaver接收数据,就调用i2c_master_read(...)。


讲到这里,我们要开始学习一点协议了,因为我们要设备按照协议中规定的规则运转,那么作为开发人员首先我们自己应该懂协议,才能使用C语言编写程序来控制设备。在I2C协议中,物理上在两个设备之间由两条线连接——数据线(SDA),时钟线(SCL)。GND和VCC那是硬件上的事,和协议无关,编程时当然也不涉及,所以我们不管。

那么这两条线有什么作用呢?SDA用来传递数据——高电平代表1,低电平代表0。SCL用来控制时序——按照一定频率荡起来。

我们再来聊一聊时钟线的作用,为什么在通信过程中时钟线一定要均匀稳定的荡起来才行?数据线低电平代表0,高电平代表1,只用这一根线行不行?如果你传输的是0101010...或101010...那还是可以的,但如果传输了一串0或一串1,那么接收方怎么知道你到底传输了多少位0或1呢。如果有了时钟就可以解决这个问题了,在一个时钟周期内采样得到的数据就是对方要传输的位数据,如果一共进行了4个周期的采样,那对方就是发送了4个bit,也就是说我们可以以时钟的脉冲为单位去对数据线采样。在I2C协议中,主机可以控制(拉低或者释放)SDA和SCL这两条线,而从机只能控制SDA线。当主机发送数据时,从机会适时地将SDA拉低或释放(拉高)。主机向从机发送数据前要先发送一个起始信号,发送完数据后发送一个结束信号。

一句话:SCL是单向的,由Master控制。而SDA是双向的,Master可以控制,Slaver也可以控制(前提是SCL为低电平时)。


下面我们来看看start和stop的波形,找找感觉。

起始信号波形

如何编程实现这个start呢,我们来看看(虚线框住的部分)

void i2c_master_start()
{
    i2c_delay(cycle);
    i2c_delay(cycle);
    sda_clr_bit();
    i2c_delay(half_cycle);
}
停止信号波形

停止信号的代码实现

static void i2c_master_stop()
{
    scl_clr_bit();
    sda_set_out();
    nop5();
    sda_clr_bit();
    i2c_delay(half_cycle);
    scl_set_bit();
    i2c_delay(half_cycle);
    sda_set_bit();
    i2c_delay(half_cycle);
}

总结一下I2C通信协议规范中对于起始信号和停止信号的规定:起始信号和停止信号必须由主机发出。


发送1个字节的代码实现

static byte_t i2c_master_sendByte(byte_t ch)
{
    byte_t i;
    byte_t ret;
    DISABLE_ALL_ISRS();
    scl_clr_bit();
    sda_set_out();
    for (i = 0; i < 8; i++)
    {
        i2c_delay(I2C_DELAY4);
        if (ch & 0x80)
        {
            sda_set_bit();
        }
        else
        {
            sda_clr_bit();
        }
        i2c_delay(I2C_DELAY4);
        scl_set_bit();
        i2c_delay(I2C_DELAY2);
        scl_clr_bit();
        
        ch <<= 1;
    }

    sda_set_bit();
    sda_set_in();
    i2c_delay(I2C_DELAY2);
    scl_set_bit();
    i2c_delay(I2C_DELAY2);

    if (sda_get_bit() == 0)
    {
        ret = 1; //ack
    }
    else
    {
        ret = 0; //nak
    }
    scl_clr_bit();
    i2c_delay(I2C_DELAY2);

    ENABLE_ALL_ISRS();
    return ret;
}

根据上面的代码,画出对应的波形

发送1个字节,注意这里有9个脉冲

接收1个字节的代码实现

static byte_t i2c_master_recieveByte(byte_t isAck)
{
    byte_t rdata = 0, i;
    
    DISABLE_ALL_ISRS();
    scl_clr_bit();
    sda_set_in();

    for (i = 0; i < 8; i++)
    {
        i2c_delay(I2C_DELAY2);
        scl_set_bit();
        i2c_delay(I2C_DELAY2);
        
        rdata <<= 1;
        if (sda_get_bit())
        {
            rdata|=1;
        }
        nop5();
        scl_clr_bit();
    }

    sda_set_out();
    nop5();
    if (1 == isAck)
    {
        sda_clr_bit();
    }
    else
    {
        sda_set_bit();
    }
    i2c_delay(I2C_DELAY2);
    scl_set_bit();
    i2c_delay(I2C_DELAY2);

    ENABLE_ALL_ISRS();
    return rdata;
}

根据上面代码,画出对应的波形

接收1个字节,注意这里有9个脉冲

确切的时序应该是这样的:

当主机要发送一个start时,Master会将SDA拉低,这就可以了,因为此时的SCL一定是高电平,这就是Master发送了一个start。好了,一个start就这样发出去了。而Slaver也会发现这个start信号的发生,Slaver便会准备好接收接下来的数据了。紧接着,Master要发送一个Byte的数据了,一位一位的发出这8个bits。这时Master会先将SCL拉低,然后在SCL为低的状态下将一个bit放到SDA上(比如要发送一个 0,Master就会通过拉低SDA来放好这个0),然后Master会把SCL拉高(释放),此时Slaver会立刻检测到SCL的变化,由此聪明的Slaver便知道Master已经将要发送的那个bit准备好了,Slaver便会在这个SCL的高电平期间尽快(Maser不会等你很久的哦)去读取一下SDA,嗯读到了一个0,Slaver就把这个0放到自己的移位寄存器中待后续处理。Master会在一个设定好的时间后把SCL再次拉低,然后在SCL为低电平期间把下一个bit放到SDA上,然后再把SCL拉高,然后Slaver在SCL的高电平期间再去读SDA......如此反复8次,一个Byte的传输便告结束。当这8个bit发完后,SCL是处于低电平的(被Master拉低的),SDA是处于高电平的(Master已经释放了SDA)

上面说了,当一个字节发送完毕后,Master会释放SDA(拉高)并拉低SCL。为什么要这样呢?这里就引出了接收应答(ACK)的概念。此时Slaver如果打算发出一个ACK的话,它必须在这个SCL被Master拉低的短暂时间内去主动将SDA拉低并保持住 (此前我们说过,SDA此时已经被Master释放,所以Slaver才有机会去拉低这个SDA)。Master会在一个确定的时间后再次将SCL拉高,并在拉高的期间去读取SDA线的状态,如果读到低电平,则认为收到了来自Slaver的响应(ACK),否则认为Slaver没有响应(NACK)刚才发送的那一个Byte。这个过程就是我们说的I2C通讯中的第9个时钟周期。当Master读完这个ACK / NACK 后,会再次将SCL拉低,用以通知Slaver:第9个时钟周期已经结束,你现在可以释放SDA了。而此时Master也可以向SDA上准备下一个Byte的第一个bit。继而重复上述过程。。。。。或者,Master也许想在接下来发送一个stop过去,那么Master会在这个SCL为低的时间内将SDA拉低,而后再将SCL拉高,在SCL为高的期间再将SDA释放(拉高) 。这样,一个stop位就产生了。你会发现此后的SDA和SCL都是高,这就是所谓的总线空闲了!

在实现I2C协议时要始终牢记:SCL为高电平期间SDA要保持稳定,这时可以对SDA进行采样,在SCL为低电平期间可以对SDA进行设置。(开始信号和结束信号例外)!

另外,需要注意的是,并非每传输8位数据之后,都会有ACK信号,有以下3种例外:

(1)当从机不能响应从机地址时(例如它正忙于其他事而无法相应I2C总线的操作,或者这个地址没有对应的从机),在第9个SCL周期内SDA线没有被拉低,即没有ACK信号。这时,主机发出一个P信号终止传输或者重新发出一个S信号开始新的传输。

(2)如果从机接收器在传输过程中不能接收更多的数据时,它也不会发出ACK信号。这样,主机就可以意识到这点,从而发出一个P信号终止传输或者发出一个S信号开始新的传输。

(3)主机接收器在接收到最后一个字节后,也不会发出ACK信号。于是,从机发送器释放SDA线,以允许主机发出P信号结束传输。


将上面i2c_master_sendByte(...)和i2c_master_receiveByte(...)封装一下,就得到了最终提供给主程序使用的接口。在这一层需要注意对起始信号,停止信号,应答的处理。

第一层封装

static void i2c_sendData(byte_t* buffer, word_t len)
{
    byte_t i;
    for (i = 0; i < len; i++)
    {
        if (i2c_sendByte(*(buffer+i)) != 1)
        {
            break; //no ack
        }
    }
    i2c_stop();
}

static void i2c_recieveData(byte_t* buffer, word_t len)
{
    byte_t i;
    for (i = 0; i < len; i++)
    {
        if (i == (len  - 1))
        {
            buffer[i] = i2c_recieveByte(0);
        }
        else
        {
            buffer[i] = i2c_recieveByte(1);
        }
    }
    i2c_stop();
}

最后的封装

void i2c_master_write(byte_t* buffer, word_t len)
{
REDO:
    // If the ACK of the slave is not received, the address information is always sent here.
    i2c_start();
    if (!i2c_sendByte(WRITR_ADDR))
    {
        i2c_delay(I2C_DELAY);
        sda_set_out();
        i2c_delay(I2C_DELAY2);
        sda_set_bit();
        scl_set_bit();
        wait(1000); //1000 =~1ms
        goto REDO;
    }

    i2c_sendData(buffer, len);
}

void i2c_master_read(byte_t* buffer, word_t len)
{
REDO:
    // If the ACK of the slave is not received, the address information is always sent here.
    i2c_start();
    if (!i2c_sendByte(READ_ADDR))
    {
        i2c_delay(I2C_DELAY);
        sda_set_out();
        i2c_delay(I2C_DELAY2);
        sda_set_bit();
        scl_set_bit();
        wait(1000); //1000 =~1ms
        goto REDO;
    }
    
    i2c_recieveData(buffer, len);
}

在i2c_master_write/read(...)的实现中,在Master发送完start后,分别向I2C总线上发送了WRITE_ADDR和READ_ADDR,这两个当然是常量定义了,那么为什么在通信的开始要先发送这个呢?其实,I2C协议中规定,在I2C总线上可以有多个设备(一个Master多个Slaver,每个设备都可以作为Master或Slaver),那么Master在向Slaver发送数据时,首先要将Slaver的地址放到I2C总线上,然后各个Slaver会从总线上读取这个地址,当Slaver读取到的地址和自己匹配时,这个Slaver就要准备从I2C总线上开始接收数据了。

地址用1个字节来表示,高7位为地址,最低位用来表示读写。例如:某个从机的地址为0x28,那么Master要向这个Slaver发送数据时,首先要发送地址信息:0x50,如果Master要从这个Slaver读数据时,首先要发送地址信息:0x51。这里要好好体会0x28怎么就变成0x50和0x51了呢!


根据上面的实际经验,我们再来学习一点协议。这里我们从Master的角度去介绍。一般有三种情况。

第一种情况:Master向Slaver写数据

标题

第二种情况:Master从Slaver读数据

标题

第三种情况:

1)Master向Slaver写数据,然后重启起始条件,紧接着从Slaver读数据;

2)Master从Slaver读数据,然后重启起始条件,紧接着向Slaver写数据。

标题

猜你喜欢

转载自blog.csdn.net/hanjing_csdn/article/details/82108324
今日推荐