05-I2C.md 5.9 KB

05 I2C 通信(HAL)

目标:通过 I2C1 驱动 OLED 显示屏(SSD1306,地址 0x78)

总线结构

I2C 使用两根线(SDA + SCL)连接多个设备:
  主机 (STM32) ←→ SDA ──────────────────
                   SCL ──────────────────
                         │       │       │
                      从机1    从机2    从机3
                      (OLED)  (传感器)  (其他)
  两根线都必须外部上拉到 VDD(3.3V 或 5V,模块通常自带)

为什么需要开漏+上拉?

  • I2C 是多主机协议,任何设备都可以拉低总线
  • 开漏输出 + 上拉电阻 = 多个设备可以"线与"(谁拉低谁赢)
  • SCL 低时改变 SDA,SCL 高时采样 SDA(保证数据稳定)

通信时序详解

每一帧 I2C 通信由主机发起:

主机发→从机收(写操作):
START → [7位地址 + W(0)] → ACK ← 从机 → DATA1 → ACK → DATA2 → ACK → ... → STOP

主机收←从机发(读操作):
START → [7位地址 + R(1)] → ACK ← 从机 → DATA1 → ACK ← 主机 → ... → NAK ← 主机 → STOP

关键点

  • ACK:每字节后接收方拉低 SDA 表示"收到了"——从机发 ACK 确认地址/数据
  • NAK:读操作的最后,主机不发 ACK 改为拉高 SDA,通知从机"不要发了"
  • 每个字节都是 MSB 先发

7 位地址 → 8 位地址:I2C 规范中从机地址是 7 位,但通信时左移 1 位,最低位表示读写:

  • 0x3C (7 位) → 左移 = 0x78 → 写
  • 0x3C (7 位) → 左移 + 1 = 0x79 → 读

CubeMX 配置

  • I2C1 → 模式 I2C
  • 参数:
    • Speed Mode:Standard Mode (100kHz) — 兼容性好;快速模式 400kHz
    • 其他保持默认
  • 引脚自动分配:PB6(SCL)、PB7(SDA)。CubeMX 自动设为开漏输出

关键 HAL API

函数 说明
HAL_I2C_Master_Transmit(&hi2c, DevAddr, pData, Size, Timeout) 主机发送数据到从机
HAL_I2C_Master_Receive(&hi2c, DevAddr, pData, Size, Timeout) 主机从从机接收数据
HAL_I2C_Mem_Write(&hi2c, DevAddr, MemAddr, MemAddrSize, pData, Size, Timeout) 向从机指定寄存器写入
HAL_I2C_Mem_Read(&hi2c, DevAddr, MemAddr, MemAddrSize, pData, Size, Timeout) 从从机指定寄存器读取

DevAddr = 7 位地址左移 1 位,例如 0x3C << 1 = 0x78

完整代码:OLED(SSD1306)基本驱动

SSD1306 的控制字节格式:Co(1) | D/C#(1) | 0(6)。其中:

  • 0x00 = 后续字节是命令(Co=0, D/C#=0)
  • 0x40 = 后续字节是数据(Co=0, D/C#=1)

    /* 写命令 */
    static void OLED_WriteCmd(uint8_t cmd)
    {
    uint8_t data[2] = {0x00, cmd};   // 控制字节 + 命令
    HAL_I2C_Master_Transmit(&hi2c1, 0x78, data, 2, HAL_MAX_DELAY);
    }
    
    /* 写数据 */
    static void OLED_WriteData(uint8_t dat)
    {
    uint8_t data[2] = {0x40, dat};   // 控制字节 + 数据
    HAL_I2C_Master_Transmit(&hi2c1, 0x78, data, 2, HAL_MAX_DELAY);
    }
    
    /* 初始化 —— 按功能分组注释 */
    static void OLED_Init(void)
    {
    HAL_Delay(100);                  // 等待 SSD1306 上电稳定
    
    OLED_WriteCmd(0xAE);             // [1] 关闭显示(配置期间保持关闭)
    
    OLED_WriteCmd(0xD5);             // [2] 振荡器频率
    OLED_WriteCmd(0x80);
    OLED_WriteCmd(0xA8);             // [3] 多路复用比 (MUX)
    OLED_WriteCmd(0x3F);             //     128×64: MUX=63
    OLED_WriteCmd(0xD3);             // [4] 显示偏移
    OLED_WriteCmd(0x00);
    OLED_WriteCmd(0x40);             // [5] 起始行 = 0
    OLED_WriteCmd(0x8D);             // [6] 电荷泵
    OLED_WriteCmd(0x14);             //     使能(内部升压到 ~7V)
    OLED_WriteCmd(0x20);             // [7] 内存地址模式
    OLED_WriteCmd(0x00);             //     水平模式
    OLED_WriteCmd(0xA1);             // [8] 段重映射(列 127→SEG0)
    OLED_WriteCmd(0xC8);             // [9] COM 扫描方向(从上到下)
    OLED_WriteCmd(0xDA);             // [10] COM 引脚配置
    OLED_WriteCmd(0x12);
    OLED_WriteCmd(0x81);             // [11] 对比度
    OLED_WriteCmd(0xCF);
    OLED_WriteCmd(0xD9);             // [12] 预充电周期
    OLED_WriteCmd(0xF1);
    OLED_WriteCmd(0xDB);             // [13] VCOMH 电压
    OLED_WriteCmd(0x40);
    OLED_WriteCmd(0xA4);             // [14] 全局显示恢复
    OLED_WriteCmd(0xA6);             // [15] 正常显示(非反色)
    OLED_WriteCmd(0x2E);             // [16] 停止水平滚动
    OLED_WriteCmd(0xAF);             // [17] 开启显示!
    }
    

清屏(逐个像素写入 0x00):

static void OLED_Clear(void)
{
    for (uint8_t page = 0; page < 8; page++)     // 128×64 = 8 pages × 128 cols
    {
        OLED_WriteCmd(0xB0 + page);  // 设置页地址
        OLED_WriteCmd(0x00);         // 列低位
        OLED_WriteCmd(0x10);         // 列高位
        for (uint8_t col = 0; col < 128; col++)
            OLED_WriteData(0x00);    // 该列全部熄灭
    }
}

I2C 读操作示例(以读取传感器寄存器为例)

uint8_t reg_addr = 0x00;
uint8_t value;

/* 写寄存器地址(不发送 STOP,而是 Repeated Start):
   发 START→从机地址+W→ACK→寄存器地址→ACK → (Repeated Start)
   发 START→从机地址+R→ACK→接收数据→NAK→STOP
*/
HAL_I2C_Mem_Read(&hi2c1, 0x78, reg_addr, I2C_MEMADD_SIZE_8BIT, &value, 1, HAL_MAX_DELAY);

常见坑点

  • ⚠️ I2C 引脚必须设为开漏输出(CubeMX 自动配置,不要手动改)
  • ⚠️ 地址左移 1 位:SSD1306 7 位地址 0x3C → HAL 中传 0x78
  • ⚠️ SDA/SCL 外部必须有上拉电阻(大多数模块自带 4.7kΩ 上拉到 VDD)
  • ⚠️ 如果总线卡死(SDA 一直为低),可复位从机或重新初始化 I2C 外设
  • ⚠️ STM32F1 的 I2C 硬件有已知 BUG(总线错误标志误触发),新版 HAL 已修复