11 IIC通讯协议

前言

IIC协议在前面03 OLED显示屏中初略的介绍了一下IIC协议,并且使用软件模拟IIC协议来和OLED显示屏进行通讯,但是之前的那一章主要是介绍如何写入数据到从设备中,没有介绍如何从从设备那接收发送过来的数据,并且还有硬件实现IIC也没有说,这一节就讲读取和硬件IIC来统一说明。

一、IIC介绍

IIC、SPI、USART等都属于通讯协议,是由飞利浦公司开发的一款同步半双工、一主一从、一主多从或多主多从的通讯协议,它有两条线组成,一根是SCL时钟线,另一根是SDA数据线。

SCL主要是提供一个时钟,只能由主设备来控制,主设备和从设备根据这一根时钟线来进行同步的数据传输。

1.IIC的时序

这里介绍一下IIC的时序,在前面讲OLED显示屏的时候是讲过该IIC的时序的,这里再介绍一下IIC的时序。

首先是开始和结束时序:

img

在IIC中,要开始进行IIC协议的通讯前需要发送一个开始信号,这个开始信号在START框中,当开始信号进行后就可以开始传送数据了,当传输完成后就需要发送一个结束信号来结束当前的IIC协议,这个信号在STOP中。

然后是发送数据时序:

img

可以看到,当SCL时钟为低电平的时候SDA的数据开始切换,当SCL上升沿的时候稳定,并将数据发送出去。

之后是读取数据时序:

img

和发送数据时一样,只不过就是在高电平时不是发送数据了,而是读取数据,SCL在低电平时,从机改变SDA的电平,当SCL上升沿时SDA稳定然后开始读取从机的数据。

最后就是读取ACK和发送ACK的时序:

img

这个就是读取和发送ack的时序,其实就是上面的读取和发送时序,很简单的。

2.使用IIC对从机寄存器的写操作流程

这个操作流程是:

  1. 发送IIC开始信号
  2. 发送从机写地址
  3. 接收从机发送的ack信号
  4. 发送需要操作的寄存器地址
  5. 接收从机发送的ack信号
  6. 发送需要写入的数据
  7. 接收从机发送的ack信号
  8. 发送IIC结束信号

这样就是一个发送数据到从机的过程。

3.使用IIC对从机寄存器的读操作流程

这个操作流程是:

  1. 发送IIC开始信号
  2. 发送从机写地址
  3. 接收从机发送的ack信号
  4. 发送需要读的寄存器地址
  5. 接收从机发送的ack信号
  6. 再次发送IIC开始信号
  7. 发送从机读地址
  8. 读取SDA线上的数据
  9. 发送NACK结束读取
  10. 发送结束信号

这样就完成了主机读取从机的数据操作。

需要注意这里接收完成后发送NACK是接收一个字节的数据才发送,当发送完NACK给从机后,从机会将SDA信号线拉高,也就是将SDA的控制权交给主机。

如果从机的这个寄存器中的数据有多个字节,那需要在第一次接收8位数据后发送一个ACK响应,当从机接收到后会继续发送数据。

二、软件实现IIC协议

1.GPIO口配置

这里用软件实现IIC协议推荐使用开漏输出,然后从机的引脚需要接上一个上拉电阻,为了防止在一条IIC总线上大家都进行推挽输出导致电流的灌制。

配置的代码如下:

GPIO_InitTypeDef GPIO_InitStruct = {0};
    
RCC_APB2PeriphClockCmd(MPU6050_GPIO_RCC, ENABLE);

GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_OD;
GPIO_InitStruct.GPIO_Pin = MPU6050_IIC_SCL | MPU6050_IIC_SDA;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(MPU6050_GPIO, &GPIO_InitStruct);

这里直接用我写好的代码了,我把IIC引脚全部使用宏定义替换了,这样后面移植的时候好进行更改。

2.IIC开始信号

这里可以看一下上面的图:

img

在开始时,SDA是高电平,然后被拉低,而SCL是高电平,当SDA拉低后SCL才被拉低,所以代码可以这样写:

IIC_MPU6050_SCL_Set(1);
IIC_MPU6050_SDA_Set(1);

IIC_MPU6050_SDA_Set(0);
IIC_MPU6050_SCL_Set(0);

这里我做了一个封装,把SDA和SCL的操作封装到一个函数中,其实本质上的操作就是GPIO_WriteBit()函数进行操作,这里就懒一下了。

3.IIC结束信号

这里就需要写一下结束信号,参考上面的图,可以看到SDA信号线一开始是低电平,然后被拉为高电平,而SCL信号线一直都是高电平,所以代码就可以这样写,先让SDA为低电平,SCL为高电平,最后拉高SDA即可:

IIC_MPU6050_SDA_Set(0);
    
IIC_MPU6050_SCL_Set(1);
IIC_MPU6050_SDA_Set(1);

这样就完成了结束信号的编写。

4.发送数据

这里可以看一下上面的时序:

img

这里是在SCL为低电平的时候更改SDA的电平,当SCL被拉高后就会将SDA上的数据发送到从机中,所以这里的代码可以这样写,先将数据给SDA,然后拉高SCL信号线,这样就会将数据发送给从机了,然后再把SCL信号线拉低更改SDA上的数据。

也可以先拉低SCL的电平,但因为前面在开始信号的时候SCL的电平就是为低电平了,所以一开始可以不用将SCL拉低:

uint8_t i;
for (i = 0; i < 8; i++)
{
    IIC_MPU6050_SDA_Set(bit & (0x80 >> i));
    
    IIC_MPU6050_SCL_Set(1);
    IIC_MPU6050_SCL_Set(0);
}

其中

IIC_MPU6050_SDA_Set(bit & (0x80 >> i));
    
IIC_MPU6050_SCL_Set(1);
IIC_MPU6050_SCL_Set(0);

是发送一位数据的操作,重复8次就是发送一个字节的数据了。

5.接收数据

这里看一下时序:

img

可以看到,也是在SCL为高电平的时候读出数据,所以代码可以这样写,先将SDA信号线释放,直接给高电平即可,然后当SCL拉高后读取SDA信号线上的数据,再将SDA拉低为下一次读取做准备:

uint8_t i, bit = 0;
    
IIC_MPU6050_SDA_Set(1);       // 释放SDA信号线

for (i = 0; i < 8; i++)
{
    bit <<= 1;
    IIC_MPU6050_SCL_Set(1);
    bit |= IIC_MPU6050_SDA_Read();
    IIC_MPU6050_SCL_Set(0);
}

6.接收ACK响应

这里就不多说了,直接就使用接收数据的那个逻辑即可,这里是一条数据,所以只需要执行一次即可,就不用使用循环了:

uint8_t ack = 0;
IIC_MPU6050_SDA_Set(1);       // 释放SDA信号线
IIC_MPU6050_SCL_Set(1);
ack = IIC_MPU6050_SDA_Read();
IIC_MPU6050_SCL_Set(0);

7.发送ACK和NACK响应

这个东西很简单,其实就是发送一个位即可,主要是ACK和NACK是怎么发送而已。

在前面接收从机发送的ACK信号可以判断是否接收的为0,如果为0就是接收成功了,所以我们知道了,ACK信号其实就是0,而NACK就是ACK信号的取反,也就是1。

这样不就清楚了吧,当发送0时就是发送ACK,当发送1时就是发送NACK。

发送ACK代码如下:

IIC_MPU6050_SDA_Set(0);
IIC_MPU6050_SCL_Set(1);
IIC_MPU6050_SCL_Set(0);

发送NACK代码如下:

IIC_MPU6050_SDA_Set(1);
IIC_MPU6050_SCL_Set(1);
IIC_MPU6050_SCL_Set(0);

代码一样,也就是发送的SDA不一样而已。

IIC协议的基础代码就写完了,后面就开始使用写好的这些封装来进行寄存器的操作了。

8.对寄存器进行写处理

这里可以看一下上面写的那个写操作:

  1. 发送IIC开始信号
  2. 发送从机写地址
  3. 接收从机发送的ack信号
  4. 发送需要操作的寄存器地址
  5. 接收从机发送的ack信号
  6. 发送需要写入的数据
  7. 接收从机发送的ack信号
  8. 发送IIC结束信号

现在来把它转换为代码的写法:

void MPU6050_WriteByte(uint8_t cmd, uint8_t date)
{
    // 开始IIC协议
    IIC_MPU6050_Start();
    
    // 写入从机的写地址
    IIC_MPU6050_WritBit(MPU6050_ADDR);
    // 等待从机发送的ACK
    IIC_MPU6050_ReadACK();
    
    // 写需要写入的从机寄存器地址
    IIC_MPU6050_WritBit(cmd);
    // 等待从机发送的ACK
    IIC_MPU6050_ReadACK();
    
    // 向从机寄存器地址中写入数据
    IIC_MPU6050_WritBit(date);
    // 等待从机发送的ACK
    IIC_MPU6050_ReadACK();
    
    // 结束IIC协议
    IIC_MPU6050_Stop();
}

这样就可以对从机的寄存器中写入一些数据了。

9.对寄存器进行读处理

这里还是看前面说的那个过程:

  1. 发送IIC开始信号
  2. 发送从机写地址
  3. 接收从机发送的ack信号
  4. 发送需要读的寄存器地址
  5. 接收从机发送的ack信号
  6. 再次发送IIC开始信号
  7. 发送从机读地址
  8. 读取SDA线上的数据
  9. 发送NACK结束读取
  10. 发送结束信号

现在来进行实现:

uint8_t MPU6050_ReadByte(uint8_t cmd)
{
    uint8_t byte = 0;
    
    IIC_MPU6050_Start();    // 开启IIC
    
    IIC_MPU6050_WritBit(MPU6050_ADDR);    // 写入从机的写地址
    IIC_MPU6050_ReadACK();
    
    IIC_MPU6050_WritBit(cmd);      // 写入需要读取的寄存器地址
    IIC_MPU6050_ReadACK();
    
    IIC_MPU6050_Start();        // 再次开启IIC协议
    
    IIC_MPU6050_WritBit(MPU6050_ADDR | 0x01);       // 写入从机读地址
    IIC_MPU6050_ReadACK();
    
    byte = IIC_MPU6050_ReadBit();       // 接收读取的数据
    IIC_MPU6050_SendNACK();          // 发送NACK结束接收
    
    IIC_MPU6050_Stop();         // 结束IIC协议
    return byte;
}

这样模拟的IIC协议就写完了,大家可以使用MPU6050陀螺仪模块来进行测试,但我这手表上的陀螺仪忘记画上拉电阻了,导致读取不了数据,所以就不展示了。

三、硬件实现IIC协议

使用硬件可以节约CPU的使用率,让其可以只控制IIC控制器完成IIC协议的通讯。

当时在stm32f103中只有2个IIC通讯引脚,硬件资源还是蛮少的,但是IIC可以一主多从,其实也是够用的。

这里主要介绍一下硬件IIC产生的事件,使用硬件IIC会根据不同的时段产生一些信号,我们可以通过判断这些信号来进行相应的处理。

img

上图的硬件IIC主机发送数据的序列图,可以看到有两个,这个不用了解,一般的IIC通讯的设备都是7位地址的,我没见过10位的,所以这里只讲7位的。

当发送IIC开始信号后会产生一个EV5的事件,当发送完地址和响应后会产生一个EV6EV8_1的事件,这里只需要判断一下EV6即可,而EV8_1不需要进行判断,因为在刚开始肯定都是空的。

然后就开始发送数据到移位寄存器中,这个时候会产生一个信号EV8,当数据从移位寄存器中到数据寄存器中后这个事件就会清除。

最后不想发送数据后,就可以等待EV8_2,当移位寄存器和数据寄存器都清空了就可以发送停止位了。

发送的还是比较简单,接下来看一下接收的:

img

可以看到一下子复杂起来了,这里还是看7位的,这里是直接列出二次开启后的事件产生了,第二次产生也是产生一个EV5事件,然后接收从机读地址,还是一个EV6信号,然后到DR数据寄存器中读取数据后就会讲EV7进行清空。

这里需要注意一下,如果只读一个字节数据,那这里需要先进行结束后再去读取DR寄存器中的值,因为在接收一次后会存放到DR寄存器中,然后继续读,所以要先发送停止后再读取并发送NACK信号。

这样就能明白其中的意义了,就可以来写代码了。

1.初始化GPIO口

这里需要注意一下,因为硬件IIC是GPIO口的一个复用功能,所以要使用复用开漏输出,我在写的时候就遇到这个问题,一直读取不了数据,并且开启AFIO的时钟,代码如下:

GPIO_InitTypeDef GPIO_InitStruct = {0};

RCC_APB2PeriphClockCmd(MPU6050_GPIO_RCC, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);

GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_OD;
GPIO_InitStruct.GPIO_Pin = MPU6050_IIC_SCL | MPU6050_IIC_SDA;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(MPU6050_GPIO, &GPIO_InitStruct);

这样就配置好GPIO口了。

2.配置IIC的时钟

使用一个外设最重要的就是开启对应的时钟,IIC的时钟是在APB1总线下的,所以要通过APB1来进行开启:

RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C2, ENABLE);

3.配置IIC

这里也是使用结构体的方式进行IIC的配置,使用的结构体如下:

typedef struct
{
  uint32_t I2C_ClockSpeed;          /*!< 指定时钟频率。
  此参数必须设置为低于 400kHz 的值 */

  uint16_t I2C_Mode;                /*!< 指定 I2C 模式。
  此参数的值可以是 @ref I2C_mode */

  uint16_t I2C_DutyCycle;           /*!< 指定 I2C 快速模式占空比。
  此参数的值可以是 @ref I2C_duty_cycle_in_fast_mode */

  uint16_t I2C_OwnAddress1;         /*!< 指定第一个设备自己的地址。
  此参数可以是 7 位或 10 位地址。 */

  uint16_t I2C_Ack;                 /*!< 启用或禁用确认。
  此参数的值可以是 @ref I2C_acknowledgement */

  uint16_t I2C_AcknowledgedAddress; /*!< 指定是确认 7 位地址还是 10 位地址。
  此参数的值可以是 @ref I2C_acknowledged_address */
}I2C_InitTypeDef;

这里不一个一个解释了,直接上配置代码:

I2C_InitTypeDef I2C_InitStruct = {0};
I2C_InitStruct.I2C_Ack = ENABLE;                                            // 使能ACK响应
I2C_InitStruct.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;      // 设置地址为7位地址
I2C_InitStruct.I2C_ClockSpeed = 50000;                                      // 时钟速率
I2C_InitStruct.I2C_DutyCycle = I2C_DutyCycle_2;                             // 快速模式的占空比
I2C_InitStruct.I2C_Mode = I2C_Mode_I2C;                                     // I2C模式
I2C_InitStruct.I2C_OwnAddress1 = 0x00;                                      // 自身地址
I2C_Init(I2C2, &I2C_InitStruct);     // 第一个是选择使用的IIC编号

可以看到上面的这个注释来进行了解,其中IIC的速率可以去看对应从机能接收的最大速率来进行配置,然后占空比其实速率不高用1:2即可,如果速度高就用6:9。

4.对寄存器进行写处理

这里不需要再自己写开始信号和结束信号,这里直接用库函数即可。

首先开始信号使用的是I2C_GenerateSTART(),第一个参数是IIC的编号,第二个填使能或者失能,如果要发送开始信号就填写使能。

发送完成后用I2C_CheckEvent()函数判断一下EV5信号是否产生,EV5信号的宏定义是I2C_EVENT_MASTER_MODE_SELECT,然后产生会返回SUCCESS,否则返回ERROR

然后开始发送从机地址,这里使用I2C_Send7bitAddress()函数进行发送,第一个参数是IIC的编号,第二个参数是填写从机地址,从机地址可以在数据手册中寻找,第三个参数是填写读还是写操作。

发送完成后用I2C_CheckEvent()函数判断一下EV6信号是否产生,这里就需要注意一下了,它有两个EV6信号,分别是:I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED

第一个是发送,也就是写操作的时候产生的信号,第二个是接收,也就是读操作的时候产生的信号,这里需要看你在I2C_Send7bitAddress()函数的第三个参数填写的是什么,如果是写,那就用第一个,如果是读就用第二个。

然后就是写数据,这里使用I2C_SendData函数进行写数据,第一个参数也是IIC的编号,第二个是要写入的数据。

最后使用I2C_GenerateSTOP()发送结束信号,参数和I2C_GenerateSTART()一致。

所以这里的完整代码如下:

I2C_GenerateSTART(I2C2, ENABLE);
while (I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT) == ERROR);      // 判断是否接收到EV5信号

I2C_Send7bitAddress(I2C2, MPU6050_ADDR, I2C_Direction_Transmitter);
while (I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED) == ERROR);     // 判断是否接收到EV6信号的写信号

I2C_SendData(I2C2, addr);
while (I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTING) == ERROR);     // 判断是否接收到EV8,后面还需要传输一个字节数据

I2C_SendData(I2C2, dat);
while (I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED) == ERROR);       // 判断是否接收到EV8_2,

I2C_GenerateSTOP(I2C2, ENABLE);

5.对寄存器进行读处理

主要注重于读操作即可,前面和二次的都一样,在发送地址这就有问题了,这里要的是读信号,所以得判断是否产生I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED

然后使用I2C_ReceiveData()函数进行读取,但在读取前需要用I2C_AcknowledgeConfig()函数不发送ACK,然后发送结束信号,发送完结束信号后用读取函数进行读取,最后再用I2C_AcknowledgeConfig()函数发送ACK信号。

这里我觉得麻烦,所以把I2C_CheckEvent()给封装了一下,代码如下:

uint8_t byte;
    
I2C_GenerateSTART(I2C2, ENABLE);
MPU6050_WaitEvent(I2C_EVENT_MASTER_MODE_SELECT);     // 检测是否产生EV5事件

I2C_Send7bitAddress(I2C2, MPU6050_ADDR, I2C_Direction_Transmitter);
MPU6050_WaitEvent(I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED);   // 检测是否产生EV6事件

I2C_SendData(I2C2, addr);
MPU6050_WaitEvent(I2C_EVENT_MASTER_BYTE_TRANSMITTING);     // 检测是否产生EV8事件

I2C_GenerateSTART(I2C2, ENABLE);
MPU6050_WaitEvent(I2C_EVENT_MASTER_MODE_SELECT);    // 检测是否产生EV5事件

I2C_Send7bitAddress(I2C2, MPU6050_ADDR, I2C_Direction_Receiver);
MPU6050_WaitEvent(I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED);    // 检测是否产生EV6事件

I2C_AcknowledgeConfig(I2C2, DISABLE);       // 不发送ACK
I2C_GenerateSTOP(I2C2, ENABLE);        // 发送停止信号

MPU6050_WaitEvent(I2C_EVENT_MASTER_BYTE_RECEIVED);        // 检测是否产生EV7事件
byte = I2C_ReceiveData(I2C2);            // 读取数据

I2C_AcknowledgeConfig(I2C2, ENABLE);     // 发送ACK

总结

IIC协议其实了解了它的时序图后会发现,其实IIC是非常的简单的,其实就做个项目就可以明白了的,这一章只讲其中驱动的使用,后面我会单独出一个专题来介绍一些大型项目的实现。

posted @ 2024-08-19 16:37  Lavender·edgar  阅读(45)  评论(0编辑  收藏  举报