1.0 I2C 简介

本文章的参考代码已上传 Git 仓库:JRNitre/softwareI2C

I2C&IIC (Inter-Integrated Circuit - 集成电路总线) 是由 NXP (原 Phihips) 在八十年代初开发的一种多主机通用数据总线,主要用于近距离、低速的芯片之间通信;标准情况下最高传输速率为 100Kbps,快速模式下 400Kbps, 高速模式下 3.4Mbps。

其是一种两线式串行总线,顾名思义其由两根线完成数据通信;一根是数据线 SDA (Serial Data Line) 另一根是时钟线 SCL (Serial Clock Line) 主设备控制时钟频率来决定 I2C 的通信波特率。

[I2C 总线具有如下特性]

  • 传输的任意时刻仅能有一个主机。
  • 同步通信
  • 半双工通信
  • 带数据应答
  • 支持总线挂载多设备(一主多从、多主多从)

2.0 I2C 物理层

I2C 总线有两条线构成数据传输总线:

  • SCL:时钟线,用于主机控制数据发送的时序
  • SDA:数据传输线,用于传输数据
I2C 是多主从架构,每个设备都有唯一的通讯地址,理论上可以连接 127 个从设备。

I2C 的两条总线在空闲状态时默认为高电平,因此在 I2C 总线的电路设计中,两根总线需要上拉至通讯 VCC 电平中。

因此,在使用单片机进行 I2C 通讯时,通讯引脚使用开漏输出对总线电平状态进行控制,引脚内部由 MOS 管控制对地导通 MOS 管关断总线上拉至 VCC; MOS 导通时总线通过 MOS 管至地,电平状态为低。

3.0 I2C 协议层

通过规定好的协议,按照一定规则操纵时钟线和数据线,即可实现主机与从机之间的数据交换,I2C 的大致通信过程如下:

3.1 寻址方式

主机发送起始信号开始通讯后,必须先发送一个字节的数据用于寻址;其中高七位为地址数据,最后一位为后续字节传输方向。

  • 传输方向位为 0:主机 -> 从机
  • 传输方向位为 1:从机 -> 主机

3.2 基本时序单元

3.2.1 起始信号与停止信号

  • 起始信号 (Start):当 SCL 为高电平时, SDA 从高电平向低电平跳变,代表开始传输数据

  • 结束信号 (Stop):当 SCL 为高电平时, SDA 从低电平向高电平跳变,代表数据传输结束

其中,起始与终止信号均由通讯主机发出,起始信号发出后总线处于占用状态;而停止信号发出后总线被释放处于空闲状态。

停止信号的发出有两种:

  • 主机停止发送:发送停止信号
  • 从机停止接收,未向主机发送应答信号:此时主机发送停止信号结束通讯

3.2.2 应答信号

在发送数据的过程中,所有地址或者数据都以 8bit 为单位进行传输,如果接收端正确的接收了 8bit 的数据,则回复一个 bit 的 0 作为应答信号 (ACK) 如果数据接收不正确或者接收端不再接收数据,则不回复总线状态为一个 bit 1 的信号作为非应答信号 (NACK)。

因此 I2C 的一帧数据帧通常有 9 位。

3.3 数据传输

  • 数据有效性:I2C 协议规定,在信号传输的过程中,在 SCL 为高电平时,SDA 的状态必须稳定,不允许产生电平跳变;只有在 SCL 为低电平的时候 SDA 的电平状态才可以变化。

3.3.1 I2C 发送一个字节

基于上述基本时序单元可知,I2C 发送一个字节顺序如下:

  1. 发送起始条件
  2. 发送从机设备地址
  3. 发送一位方向位
  4. 接收从机应答
  5. 发送有效数据
  6. 接收从机应答
  7. 循环 5,6 步骤,直到数据发送完毕或者无从机应答
  8. 发送结束条件

3.3.2 I2C 读取一个字节

4.0 I2C 代码实现

4.1 基本功能单元的封装

4.1.1 GPIO 工作模式配置

由于 I2C 在工作时空闲状态为高电平,因此 SCL 和 SDA 引脚需要配置为开漏输出模式对总线进行控制。

// 使能需要使用的 GPIO 所在的总线时钟
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE);
GPIO_InitTypeDef GPIO_InitStruct;

GPIO_InitStruct.GPIO_Mode        = GPIO_Mode_OUT;    // 输出模式
GPIO_InitStruct.GPIO_OType    = GPIO_OType_OD;    // 开漏
GPIO_InitStruct.GPIO_PuPd = GPIO_PuPd_NOPULL;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;    

GPIO_InitStruct.GPIO_Pin = I2C_SCL_PIN | I2C_SDA_PIN;
GPIO_Init(I2C_GROUP,&GPIO_InitStruct);

4.1.2 配置 TIM 定时器

在使用 I2C 协议发送数据时,每个电平状态之间使用延时函数进行延时,从而对 I2C 的通信速度进行控制,这里使用 TIM2 定时器作为延时函数的基本实现。

在本文编写时,使用的 MCU 是 STM32F401CCU6 其默认时钟频率为 84Mhz 供给至 AB1 总线部分的时钟频率也为 84Mhz

而根据上文可知,I2C 的标准通信速度为 100Kbps 由此可知每比特数据之间的传输间隔为:

$10us = \frac{1}{100000} $

对于 I2C 协议数据采样发生在高或低电平的中点,因此两次电平状态的时间间隔为 5us

//

综上我们需要实现一个精度是 us 级别的延时函数,根据计数器时钟周期计算公式:

$T_{count} = \frac{(PSC + 1) * (ARR + 1)}{f_{clock}} $

计算可知 PSC 应该等于 63 然后 ARR = 延时时间 - 1 得到 ARR = 4。

RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);

TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;

TIM_TimeBaseStructure.TIM_Period = 0xFFFFFFFF;
TIM_TimeBaseStructure.TIM_Prescaler = 83;
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);

TIM_Cmd(TIM2, ENABLE);

4.1.3 电平操纵函数

将引脚操作函数和延时函数封装到一起,用于控制总线产生 0 或 1

void i2cCon_SCL(uint8_t BitValue){
    GPIO_WriteBit(I2C_GROUP, I2C_SCL_PIN, (BitAction)(BitValue));
    i2cDelay_us(i2cSpeedDelay);
}

void i2cCon_SDA(uint8_t BitValue){
    GPIO_WriteBit(I2C_GROUP, I2C_SDA_PIN, (BitAction)(BitValue));
    i2cDelay_us(i2cSpeedDelay);
}

4.1.4 开始信号与结束信号

基于上面封装的函数和 I2C 通信原理,组装信号时序:


void i2cSignal_Start(void){
    i2cCon_SDA(1);
    i2cCon_SCL(1);
    i2cCon_SDA(0);
    i2cCon_SCL(0);
}

void i2cSignal_Stop(void){
    i2cCon_SDA(0);
    i2cCon_SCL(1);
    i2cCon_SDA(1);
}

4.2 数据收发

4.2.1 发送一个 bit 数据

void i2cSend_Byte(uint8_t byte){
    for(int i = 0; i < 8; i++){
        i2cCon_SDA(!!(byte & (0x80 >> i)));
        i2cCon_SCL(1);
        i2cCon_SCL(0);
    }
}

4.2.2 接收一个 bit 数据

uint8_t i2cReceive_Byte(void){
    uint8_t rByte = 0x00;
    i2cCon_SDA(1);
    
    for(int i = 0; i < 8; i++){
        i2cCon_SCL(1);
        
        if(i2cReceive_SDA() == 1){
            rByte |= (0x80 >> i);
        }
        
        i2cCon_SCL(0);
    }
    
    return rByte;
}

4.3 应答信号的发送与处理

4.3.1 发送应答信号

void i2cSend_ACK(uint8_t ackBit){
    i2cCon_SDA(ackBit);
    i2cCon_SCL(1);
    i2cCon_SCL(0);
}

4.3.2 检查应答信号

uint8_t i2cReceive_ACK(void){
    i2cCon_SDA(1);
    i2cCon_SCL(1);
    uint8_t ackBit = i2cReceive_SDA();
    i2cCon_SCL(0);
    return ackBit;
}