IIC协议解析

一、IIC 模式设置

配置项含义与说明
IIC标准 I²C 模式。使用两根线(SDA 数据线、SCL 时钟线),支持多主机、多从机,通过地址寻址。这是最常用的模式。
SMBus-Alert-modeSMBus 的 警报信号模式。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数据协议规定

一次完整的 IIC 读写操作(以写操作为例)包含以下序列:

  1. 起始条件(S):主机发出起始信号,开始一次通信。

  2. 设备地址与读写位(第 1 个字节): 前 7 位(A6~A0)为从机地址,用于寻址总线上最多 127 个设备。 最低位(R/W)表示本次操作方向:

    • 0 表示主机向从机写入数据(写操作)
    • 1 表示主机从从机读取数据(读操作) 该字节传输完成后,从机会在第 9 个时钟周期回复一个应答位(A)。
  3. 寄存器地址(第 2 个字节): 该字节(B7~B0)指向从机内部的寄存器地址。后续的读写操作将从此地址开始。 该字节传输完成后,从机再次回复一个应答位(A)。

    注意:若本次操作为连续读写,每次访问该寄存器后,地址指针会自动加 1,指向下一个寄存器。

  4. 数据字节(第 3 个及后续字节)

    • 写操作:主机发送数据字节(D7~D0),写入第 2 步指定的寄存器。从机每收到一个字节回复一个应答位(A)。之后可继续发送多个数据字节,寄存器地址自动递增。
    • 读操作:主机转为接收模式,从机发送数据字节(D7~D0),内容为当前寄存器地址中的值。主机每收到一个字节回复一个应答位(最后一个字节后可回复非应答 NAK 表示结束)。之后可继续读取多个字节,寄存器地址自动递增。
  5. 停止条件(S):主机发出停止信号,释放总线,结束本次通信。

iic数据规定

硬件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();
}