IIC协议解析
一、IIC 模式设置
| 配置项 | 含义与说明 |
|---|---|
| IIC | 标准 I²C 模式。使用两根线(SDA 数据线、SCL 时钟线),支持多主机、多从机,通过地址寻址。这是最常用的模式。 |
| SMBus-Alert-mode | SMBus 的 警报信号模式。SMBus 是 I²C 的一个子集,增加了低功耗管理、超时检测等特性。Alert 模式允许从机通过一个专用信号线(SMBA#)主动通知主机发生事件,主机随后发起 Alert Response Address (ARA) 流程来识别哪个从机触发了警报。 |
| SMBus-two-wire-interface | 也是 SMBus,但 不使用专用警报线,所有通信(包括警报响应)都在标准的 SDA/SCL 两根线上完成。更接近 I²C 的物理层,但遵循 SMBus 的协议层(如超时、包错误校验 PEC 等)。 |
简单区分:
- IIC:标准模式,无超时限制,支持任意速率(常见 100k/400k)。
- SMBus Alert:需要额外 SMBA 线,用于从机主动报警。
- SMBus 2-wire:仅用两根线,兼容 I²C 电气特性,但协议更严格(如最小时钟低电平时间、超时等)。
二、参数设置
1. Speed Mode 速度模式
- Standard mode (100 kHz):经典速度,兼容性最好,适合大多数传感器、EEPROM。
- Fast Mode (400 kHz):高速模式,传输更快,要求总线电容更低,部分旧设备不支持。
2. Clock Speed
- 直接设定 SCL 时钟频率(单位通常是 Hz 或 kHz)。 如果已选 Speed Mode,此选项可能用于微调(例如设为 350 kHz 而非 400 kHz)。若没选模式,则需要手动输入频率值。
3. Clock No Stretch Mode 开启/关闭
- 时钟拉伸:当从机来不及处理数据时,可以主动拉低 SCL,迫使主机等待。
- Clock No Stretch Mode(无时钟拉伸模式):
- 开启:禁止从机使用时钟拉伸。主机不会等待,如果从机拉低 SCL 可能会造成通信错误。
- 关闭:允许从机拉伸时钟(标准 I²C 支持此特性)。
- 使用场景:与某些不支持拉伸的快速设备通信,或为了简化主机时序。
4. Primary Address Length selection 地址长度
- 7bit:标准 I²C 地址长度,最常用。实际发送时左移一位加 R/W 位。
- 10bit:扩展地址长度,用于需要超过 112 个设备的总线(7 位地址除去保留地址后只有 112 个可用)。10 位地址需要两个字节传输,兼容性略差。
5. Dual Address Acknowledged 开启/关闭
- 是否让从机 响应两个不同的从地址。
- 开启:从机有两个有效地址(例如主地址 + 第二地址),常用于需要区分不同功能的复合设备。
- 关闭:只响应 Primary slave address。
- 注意:此选项通常只在 从机模式 下有意义。
6. Primary slave address
- 当本机作为 从机 时,自己的主要地址(7 位或 10 位,与地址长度设置匹配)。
- 主机通过这个地址来访问本设备。
7. General Call Address detection
- 广播地址检测。通用广播地址是
0x00(8 位形式,即 7 位地址0x00+ R/W=0)。- 开启:从机响应广播地址,接收到主机发送的全局命令(如复位所有设备、改变从机地址等)。
- 关闭:从机忽略广播地址,只响应自己的专用地址。
- 一般传感器或简单从机不启用此功能,避免误响应。
IIC时序
SCK时钟稳定一高一低,高电平时开始采样。
采样时SDA数据线四个状态:
-
从高到低:为开始状态,告诉设备要开始发送数据了。
-
从低到高:为结束状态,告诉设备我已经发送完成了。
-
高电平:数据为 1
-
低电平:数据为 0

应答过程:设备主动拉低电平。
IIC数据协议规定
一次完整的 IIC 读写操作(以写操作为例)包含以下序列:
-
起始条件(S):主机发出起始信号,开始一次通信。
-
设备地址与读写位(第 1 个字节): 前 7 位(A6~A0)为从机地址,用于寻址总线上最多 127 个设备。 最低位(R/W)表示本次操作方向:
0表示主机向从机写入数据(写操作)1表示主机从从机读取数据(读操作) 该字节传输完成后,从机会在第 9 个时钟周期回复一个应答位(A)。
-
寄存器地址(第 2 个字节): 该字节(B7~B0)指向从机内部的寄存器地址。后续的读写操作将从此地址开始。 该字节传输完成后,从机再次回复一个应答位(A)。
注意:若本次操作为连续读写,每次访问该寄存器后,地址指针会自动加 1,指向下一个寄存器。
-
数据字节(第 3 个及后续字节):
- 写操作:主机发送数据字节(D7~D0),写入第 2 步指定的寄存器。从机每收到一个字节回复一个应答位(A)。之后可继续发送多个数据字节,寄存器地址自动递增。
- 读操作:主机转为接收模式,从机发送数据字节(D7~D0),内容为当前寄存器地址中的值。主机每收到一个字节回复一个应答位(最后一个字节后可回复非应答 NAK 表示结束)。之后可继续读取多个字节,寄存器地址自动递增。
-
停止条件(S):主机发出停止信号,释放总线,结束本次通信。

硬件IIC和软件IIC
- 硬件IIC不需要操作SDA,SCL引脚,硬件会自己处理,只需要发送数据或命令就好。
//配置好硬件配置
void OLED_WriteCmd(uint8_t cmd) {
uint8_t buf[2] = {0x00, cmd}; // 0x00 //cmd
HAL_I2C_Master_Transmit(&hi2c1,OLED_ADDR, buf, 2, 100);
}
void OLED_WriteData(uint8_t data) {
uint8_t buf[2] = {0x40, data}; // 0x40 //data
HAL_I2C_Master_Transmit(&hi2c1, OLED_ADDR, buf, 2, 100);
}
- 需要自己模拟整个时序过程。
static void I2C_Start(void) {
// SDA 和 SCL 初始高电平
HAL_GPIO_WritePin(Soft_IIC_SDA_GPIO_Port, Soft_IIC_SDA_Pin, GPIO_PIN_SET);
HAL_GPIO_WritePin(Soft_IIC_SCL_GPIO_Port, Soft_IIC_SCL_Pin, GPIO_PIN_SET); //时钟开始
HAL_Delay(5);
// SDA 拉低产生起始条件
HAL_GPIO_WritePin(Soft_IIC_SDA_GPIO_Port, Soft_IIC_SDA_Pin, GPIO_PIN_RESET);
HAL_Delay(5);
// SCL 拉低,准备传输数据
HAL_GPIO_WritePin(Soft_IIC_SCL_GPIO_Port, Soft_IIC_SCL_Pin, GPIO_PIN_RESET);//时钟结束
HAL_Delay(5);
}
static void I2C_Stop(void) {
//1.先拉低SCL,再拉低SDA
HAL_GPIO_WritePin(Soft_IIC_SCL_GPIO_Port, Soft_IIC_SCL_Pin, GPIO_PIN_RESET);
HAL_GPIO_WritePin(Soft_IIC_SDA_GPIO_Port, Soft_IIC_SDA_Pin, GPIO_PIN_RESET);
HAL_Delay(5);
// SCL 时钟开始
HAL_GPIO_WritePin(Soft_IIC_SCL_GPIO_Port, Soft_IIC_SCL_Pin, GPIO_PIN_SET);
HAL_Delay(5);
// SDA 拉高产生停止条件
HAL_GPIO_WritePin(Soft_IIC_SDA_GPIO_Port, Soft_IIC_SDA_Pin, GPIO_PIN_SET);
HAL_Delay(5);
// SCL 时钟结束
HAL_GPIO_WritePin(Soft_IIC_SCL_GPIO_Port, Soft_IIC_SCL_Pin, GPIO_PIN_SET);
}
// 发送一个字节,返回从机应答位(0=应答,1=非应答)
static uint8_t I2C_SendByte(uint8_t data) {
// 发送 8 位数据,高位在前
for (uint8_t i = 0; i < 8; i++) {
if (data & 0x80) {
HAL_GPIO_WritePin(Soft_IIC_SDA_GPIO_Port, Soft_IIC_SDA_Pin, GPIO_PIN_SET);
} else {
HAL_GPIO_WritePin(Soft_IIC_SDA_GPIO_Port, Soft_IIC_SDA_Pin, GPIO_PIN_RESET);
}
data <<= 1;
HAL_Delay(2);
// 产生时钟高电平,从机采样数据
HAL_GPIO_WritePin(Soft_IIC_SCL_GPIO_Port, Soft_IIC_SCL_Pin, GPIO_PIN_SET);
HAL_Delay(5);
HAL_GPIO_WritePin(Soft_IIC_SCL_GPIO_Port, Soft_IIC_SCL_Pin, GPIO_PIN_RESET);
HAL_Delay(2);
}
// 释放 SDA 总线,准备接收应答
HAL_GPIO_WritePin(Soft_IIC_SDA_GPIO_Port, Soft_IIC_SDA_Pin, GPIO_PIN_SET);
HAL_Delay(2);
HAL_GPIO_WritePin(Soft_IIC_SCL_GPIO_Port, Soft_IIC_SCL_Pin, GPIO_PIN_SET);
HAL_Delay(5);
// 读取应答位(低电平有效)
uint8_t ack = HAL_GPIO_ReadPin(Soft_IIC_SDA_GPIO_Port, Soft_IIC_SDA_Pin);
HAL_GPIO_WritePin(Soft_IIC_SCL_GPIO_Port, Soft_IIC_SCL_Pin, GPIO_PIN_RESET);
HAL_Delay(2);
return ack; // 0 表示应答,1 表示非应答
}
// 向 OLED 发送命令或数据(连续多个字节)
static void OLED_WriteBytes(uint8_t ctrl_byte, uint8_t *data, uint8_t len) {
I2C_Start();
I2C_SendByte(OLED_ADDR); // 发送设备地址 + 写位
I2C_SendByte(ctrl_byte); // 控制字节:命令或数据
for (uint8_t i = 0; i < len; i++) {
I2C_SendByte(data[i]);
}
I2C_Stop();
}
// 便捷函数:写单个命令
static void OLED_WriteCmd(uint8_t cmd) {
OLED_WriteBytes(0, &cmd, 1); // 0代表写命令,&cmd要发送的命令,1发送长度
}
// 便捷函数:写单个数据
static void OLED_WriteData(uint8_t data) {
OLED_WriteBytes(1, &data, 1);
}
读数据&写数据
向设备写数据,前面已经有相关示例了。
读设备数据:
- 硬件操作
// 从设备读取多个字节(标准流程:写寄存器地址 -> 重复起始 -> 读数据)
// ctrl_byte: 控制字节(例如 0x00 命令,0x40 数据,或者寄存器地址)
// data: 接收缓冲区
// len: 要读取的字节数
// 返回值:HAL 状态(HAL_OK 表示成功)
HAL_StatusTypeDef OLED_ReadBytes(uint8_t ctrl_byte, uint8_t *data, uint8_t len) {
HAL_StatusTypeDef ret;
// 1. 发送设备地址 + 写位,并发送控制字节
ret = HAL_I2C_Master_Transmit(&hi2c1, OLED_ADDR, &ctrl_byte, 1, 100);
if (ret != HAL_OK) return ret;
// 2. 重复起始,发送设备地址 + 读位,然后接收数据
ret = HAL_I2C_Master_Receive(&hi2c1, OLED_ADDR, data, len, 100);
return ret;
}
- 软件模拟
// 读取一个字节,ack: 0=主机发送应答(继续读取),1=主机发送非应答(结束读取)
static uint8_t I2C_ReadByte(uint8_t ack) {
uint8_t data = 0;
// 释放 SDA 总线,让从机控制数据线
HAL_GPIO_WritePin(Soft_IIC_SDA_GPIO_Port, Soft_IIC_SDA_Pin, GPIO_PIN_SET);
HAL_Delay(2);
for (uint8_t i = 0; i < 8; i++) {
data <<= 1;
// SCL 高电平,从机输出数据
HAL_GPIO_WritePin(Soft_IIC_SCL_GPIO_Port, Soft_IIC_SCL_Pin, GPIO_PIN_SET);
HAL_Delay(5);
if (HAL_GPIO_ReadPin(Soft_IIC_SDA_GPIO_Port, Soft_IIC_SDA_Pin) == GPIO_PIN_SET) {
data |= 0x01;
}
// SCL 低电平,准备下一位
HAL_GPIO_WritePin(Soft_IIC_SCL_GPIO_Port, Soft_IIC_SCL_Pin, GPIO_PIN_RESET);
HAL_Delay(2);
}
// 主机发送应答位
if (ack == 0) {
HAL_GPIO_WritePin(Soft_IIC_SDA_GPIO_Port, Soft_IIC_SDA_Pin, GPIO_PIN_RESET); // 应答(低电平)
} else {
HAL_GPIO_WritePin(Soft_IIC_SDA_GPIO_Port, Soft_IIC_SDA_Pin, GPIO_PIN_SET); // 非应答(高电平)
}
HAL_Delay(2);
// 产生应答时钟脉冲
HAL_GPIO_WritePin(Soft_IIC_SCL_GPIO_Port, Soft_IIC_SCL_Pin, GPIO_PIN_SET);
HAL_Delay(5);
HAL_GPIO_WritePin(Soft_IIC_SCL_GPIO_Port, Soft_IIC_SCL_Pin, GPIO_PIN_RESET);
HAL_Delay(2);
// 释放 SDA 总线
HAL_GPIO_WritePin(Soft_IIC_SDA_GPIO_Port, Soft_IIC_SDA_Pin, GPIO_PIN_SET);
return data;
}
// 从 OLED 读取多个字节(先写入控制字节,再连续读取)
static void OLED_ReadBytes(uint8_t ctrl_byte, uint8_t *data, uint8_t len) {
I2C_Start();
I2C_SendByte(OLED_ADDR); // 发送设备写地址
I2C_SendByte(ctrl_byte); // 发送控制字节(例如命令或寄存器地址)
I2C_Start(); // 重复起始条件
I2C_SendByte(OLED_ADDR | 0x01); // 发送设备读地址
for (uint8_t i = 0; i < len; i++) {
// 最后一个字节发送非应答,其余发送应答
uint8_t ack = (i == len - 1) ? 1 : 0;
data[i] = I2C_ReadByte(ack);
}
I2C_Stop();
}