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 发送一个字节顺序如下:
- 发送起始条件
- 发送从机设备地址
- 发送一位方向位
- 接收从机应答
- 发送有效数据
- 接收从机应答
- 循环 5,6 步骤,直到数据发送完毕或者无从机应答
- 发送结束条件
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;
}
评论区(暂无评论)