跳到主要内容

I2C通信协议

1. I2C通信

I2C(Inter IC Bus)是由Philips公司开发的一种通用数据总线

两根通信线:SCL(Serial Clock)、SDA(Serial Data)

  • 同步,半双工
  • 带数据应答
  • 支持总线挂载多设备(一主多从、多主多从

Image

2. 硬件电路

所有I2C设备的SCL连在一起,SDA连在一起.

Image

一主多从。左边CPU是单片机,作为总线的主机。主机的权利很大,包括,对SCL线的完全控制;在空闲状态下,主机还可以主动发起对SDA的控制。只有当从机发送数据和从机应答的时候,主机才会转交SDA的控制权给从机。

右图是被控IC的内部结构。引脚的信号进来,可以通过一个数据缓冲器或者是施密特触发器进行输入。输入对电路没有任何影响,任何设备在任何时刻都可以输入。输出低电平,开关管导通;引脚直接接地,是强下拉;输出高电平,开关管断开;引脚什么都不接,处于浮空状态。如此一来,所有的设备都只能输出低电平。

注意

为避免高电平造成的引脚浮空,此时需要在总线外面SCL和SDA各外置一个上拉电阻。属于弱上拉。

被控IC是挂载在I2C总线上的从机,可以是姿态传感器、OLED、存储器、时钟模块等。从机的权利很小。对于SCL时钟线,在任何时刻都只能被动的读取,不允许控制SCL线。只有在主机发送读取从机的命令/从机应答后,从机才能短暂地取得SDA的控制权。

提示

主机的SCL可以配置成推挽输出,所有从机的SCL都配置成浮空输入或者上拉输入。数据流向是:主机发送,所有从机接收。

主机的SDA发送的时候是输出,接收的时候是输入。从机的SDA也会在输入和输出之间反复切换。

危险

但是,如果总线时序没协调好,极有可能发生两个引脚同时处于输出的状态,如果这时正好是一个输出高电平,一个输出低电平,就是电源短路。

为了避免电源短路的问题,I2C被设计成禁止所有设备输出强上拉的高电平,采用外置若上拉电阻加开漏输出的电路结构。

信息

设备的SCL和SDA均要配置成开漏输出模式。

SCL和SDA各添加一个上拉电阻,阻值一般为4.7KΩ左右。

3. I2C时序基本单元

  • 起始条件:SCL高电平期间,SDA从高电平切换到低电平

  • 终止条件:SCL高电平期间,SDA从低电平切换到高电平

Image

发送一个字节:SCL低电平期间,主机将数据位依次放到SDA线上(高位先行),然后释放SCL,从机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可发送一个字节

第一个字节必须是主机发送的。最开始,SDA是高电平,主机如果想发送0,就拉低SDA到低电平。

Image

接收一个字节:SCL低电平期间,从机将数据位依次放到SDA线上(高位先行),然后释放SCL,主机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可接收一个字节(主机在接收之前,需要释放SDA)

Image

发送应答:主机在接收完一个字节之后,在下一个时钟发送一位数据,数据0表示应答,数据1表示非应答

接收应答:主机在发送完一个字节之后,在下一个时钟接收一位数据,判断从机是否应答,数据0表示应答,数据1表示非应答(主机在接收之前,需要释放SDA)

Image

4. I2C时序

  • 指定地址写。

对于指定设备(Slave Address),在指定地址(Reg Address)下,写入指定数据(Data)

Image

  • 当前地址读

对于指定设备(Slave Address),在当前地址指针指示的地址下,读取从机数据(Data)

Image

  • 指定地址读

对于指定设备(Slave Address),在指定地址(Reg Address)下,读取从机数据(Data)

Image

5. MPU6050简介

MPU6050是一个6轴姿态传感器,可以测量芯片自身X、Y、Z轴的加速度、角速度参数,通过数据融合,可进一步得到姿态角,常应用于平衡车、飞行器等需要检测自身姿态的场景。

  • 3轴加速度计(Accelerometer):测量X、Y、Z轴的加速度

  • 3轴陀螺仪传感器(Gyroscope):测量X、Y、Z轴的角速度

Image

6. I2C配置函数

6.1 初始化

#include "stm32f10x.h"

void I2C1_Init(void) {
// 1. 使能 I2C1 和 GPIOB 时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);

// 2. 配置 PB6 (SCL) 和 PB7 (SDA) 为复用开漏模式
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD; // 复用开漏模式
GPIO_Init(GPIOB, &GPIO_InitStructure);

// 3. 配置 I2C1
I2C_InitTypeDef I2C_InitStructure;
I2C_InitStructure.I2C_Mode = I2C_Mode_I2C; // I2C 模式
I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2; // 标准模式占空比
I2C_InitStructure.I2C_OwnAddress1 = 0x00; // STM32 作为主机时,地址可设为 0
I2C_InitStructure.I2C_Ack = I2C_Ack_Enable; // 使能应答
I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit; // 7位地址
I2C_InitStructure.I2C_ClockSpeed = 100000; // 100kHz (标准模式)

I2C_Init(I2C1, &I2C_InitStructure);

// 4. 使能 I2C1
I2C_Cmd(I2C1, ENABLE);
}

6.2 主机发送数据

uint8_t I2C1_Master_Transmit(uint8_t device_addr, uint8_t *data, uint16_t size) {
// 1. 等待 I2C 总线空闲
while (I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY));

// 2. 发送 START 条件
I2C_GenerateSTART(I2C1, ENABLE);

// 3. 等待 SB (START) 标志
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));

// 4. 发送从设备地址(写模式)
I2C_Send7bitAddress(I2C1, device_addr << 1, I2C_Direction_Transmitter);

// 5. 等待 ADDR 标志(确认从设备已响应)
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));

// 6. 发送数据
for (uint16_t i = 0; i < size; i++) {
I2C_SendData(I2C1, data[i]);

// 等待 TXE (发送缓冲区空) 或 BTF (字节传输完成)
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));
}

// 7. 发送 STOP 条件
I2C_GenerateSTOP(I2C1, ENABLE);

return 0; // 成功
}

6.3 主机接收数据

uint8_t I2C1_Master_Receive(uint8_t device_addr, uint8_t *data, uint16_t size) {
// 1. 等待 I2C 总线空闲
while (I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY));

// 2. 发送 START 条件
I2C_GenerateSTART(I2C1, ENABLE);

// 3. 等待 SB (START) 标志
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));

// 4. 发送从设备地址(读模式)
I2C_Send7bitAddress(I2C1, device_addr << 1, I2C_Direction_Receiver);

// 5. 等待 ADDR 标志(确认从设备已响应)
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED));

// 6. 接收数据
for (uint16_t i = 0; i < size; i++) {
if (i == (size - 1)) {
// 最后一个字节,不发送 ACK
I2C_AcknowledgeConfig(I2C1, DISABLE);
I2C_GenerateSTOP(I2C1, ENABLE); // 发送 STOP 条件
}

// 等待 RXNE (接收数据寄存器非空)
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED));

data[i] = I2C_ReceiveData(I2C1);
}

// 7. 重新使能 ACK(如果后续还要接收)
I2C_AcknowledgeConfig(I2C1, ENABLE);

return 0; // 成功
}