目标:通过 I2C1 驱动 OLED 显示屏(SSD1306,地址 0x78)
I2C 使用两根线(SDA + SCL)连接多个设备:
主机 (STM32) ←→ SDA ──────────────────
SCL ──────────────────
│ │ │
从机1 从机2 从机3
(OLED) (传感器) (其他)
两根线都必须外部上拉到 VDD(3.3V 或 5V,模块通常自带)
为什么需要开漏+上拉?
STM32 I2C 外设包含时钟控制模块(配置 CCR 寄存器设定 SCL 频率)、数据控制模块(管理 SDA 收发移位)、自身地址寄存器(OAR1/OAR2,从机模式地址匹配)以及中断/DMA 控制逻辑。
I2C 物理层核心是开漏输出 + 外部上拉电阻。每个设备的 SDA/SCL 引脚内部为 NMOS 开漏结构:
多个设备输出直接"线与"(wired-AND):只要有一个设备拉低总线,总线即为低电平。这是多主机冲突检测的基础——若两主机同时启动传输,先拉低 SDA 的一方"取胜",另一方检测到电平不符即判仲裁失败。
每字节完成后,接收方在第 9 个 SCL 周期控制 SDA:
从机可主动拉低 SCL 迫使主机等待,用于处理时间不足时(如 EEPROM 正在写入)。从机处理完后释放 SCL,主机自动恢复时钟。
两主机同时驱动总线时,各自监测 SDA 电平。若某主机试图拉高 SDA 但检测到实际为低(另一主机正拉低),该主机立即退出仲裁,不干扰胜出方。仲裁过程不丢失数据位。
标准模式下 SCL 频率公式(STM32 I2C):
f_SCL = f_PCLK1 / (CCR × 2)
CCR 为 I2C 时钟控制寄存器中的 12 位分频值。上升时间要求:标准模式 t_rise_max = 1000 ns,快速模式 t_rise_max = 300 ns。典型值:f_PCLK1 = 36 MHz, CCR = 180 → f_SCL = 100 kHz。
每一帧 I2C 通信由主机发起:
主机发→从机收(写操作):
START → [7位地址 + W(0)] → ACK ← 从机 → DATA1 → ACK → DATA2 → ACK → ... → STOP
主机收←从机发(读操作):
START → [7位地址 + R(1)] → ACK ← 从机 → DATA1 → ACK ← 主机 → ... → NAK ← 主机 → STOP
关键点:
7 位地址 → 8 位地址:I2C 规范中从机地址是 7 位,但通信时左移 1 位,最低位表示读写:
| 函数 | 说明 |
|---|---|
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。
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); // 该列全部熄灭
}
}
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);