ARM汇编学习笔记(八):I²C 通信详解:推挽/开漏、通信时序与代码实现
2026/9/24 8:01:28 网站建设 项目流程

一、GPIO 输出模式:推挽 vs 开漏

I²C 总线的物理层核心是开漏输出 + 上拉电阻。理解这两种输出模式,是理解 I²C 电气特性的前提。

1.1 推挽输出(Push-Pull)

结构:由两个 MOS 管(上管 mos1 接 VCC,下管 mos2 接 GND)组成,同一时刻只导通一个。

mos1mos2pad 电平
1(导通)0(截止)3.3V(强高)
0(截止)1(导通)GND(强低)
00高阻态(悬空)
11禁止(短路,烧管)

特点

  • 可以主动输出高电平和低电平,驱动能力强。
  • 不能多个设备同时挂在一根线上,否则一个输出高、一个输出低会短路。
  • 适用于:单独驱动 LED、蜂鸣器、SPI 的 MOSI/SCLK 等独占型信号

1.2 开漏输出(Open-Drain)

结构:只有下管 mos2,上管 mos1 永远不导通。

mos1mos2pad 电平
103.3V
01GND
00高阻态(悬空)
11禁止

特点

  • 只能主动拉低,无法主动输出高电平。
  • 高电平状态需依赖外部上拉电阻将总线拉到 VCC。
  • 输出“1”实际上是释放总线(高阻态),输出“0”是拉低。
  • 支持“线与”:只要有一个设备拉低,总线就是低;只有全部释放,总线才是高。

1.3 为什么 I²C 必须用开漏?

因为 I²C 是多主多从总线,同一根 SDA/SCL 上可能挂多个设备:

  • 如果使用推挽:A 输出高、B 输出低 →电源到地短路,器件烧毁。
  • 使用开漏:A 输出高(实际是释放)、B 输出低(拉低)→ 总线被 B 拉低,不会短路,实现“线与”。

上拉电阻典型值4.7kΩ ~ 10kΩ:阻值越小上升沿越快但功耗越大;阻值越大越省电但边沿变缓,高速通信时可能采不到数据。

1.4 电平转换应用

开漏结构还便于电平转换

  • 3.3V 的 chipA 与 5V 的 chipB 共挂 I²C,SDA/SCL 各自上拉到自己的 VCC。
  • 任意一方拉低,另一方都能检测到低电平;释放时各自被拉回各自的高电平,天然实现 3.3V↔5V 互通。

二、I²C 通信时序详解

2.1 空闲状态

SCL 和 SDA 均被上拉电阻拉为高电平,总线空闲。

2.2 起始信号(START)

SCL 为高电平时,SDA 由高变低。

此时所有从机检测到起始条件,开始准备接收地址。

2.3 停止信号(STOP)

SCL 为高电平时,SDA 由低变高。

主设备发出,结束本次通信,释放总线。

2.4 数据位传输规则

  • MSB 优先:先发最高位 bit7,最后发 bit0。
  • SCL 低电平期间:发送方可以改变 SDA 电平(准备数据)。
  • SCL 高电平期间:SDA 必须保持稳定,接收方在此时采样。
  • 每个字节 = 8 位数据 + 1 位应答 =9 个时钟周期

2.5 应答位(ACK / NACK)

第 9 个时钟周期,接收方控制 SDA:

类型SDA 电平含义
ACK低(0)接收成功,继续
NACK高(1)接收失败 / 结束读取

2.6 完整写时序帧结构

START → [从机地址7位 + W(0)] → ACK → [寄存器地址8位] → ACK → [数据8位] → ACK → ... → STOP

2.7 完整读时序帧结构

START → [从机地址7位 + W(0)] → ACK → [寄存器地址8位] → ACK → RESTART → [从机地址7位 + R(1)] → ACK → [数据8位] → ACK(主机发) → ... → NACK → STOP

2.8 时序图(以写 0xAC 为例)

┌──┐ ┌──┐ ┌──┐ ┌──┐ ┌──┐ ┌──┐ ┌──┐ ┌──┐ ┌──┐ SCL ───┘ └──┘ └──┘ └──┘ └──┘ └──┘ └──┘ └──┘ └──┘ └─── 1 2 3 4 5 6 7 8 9 SDA ──┐ 1 ┌─0─┐ 1 ┌─0─┐ 1 ┌─1─┐ 0 ┌─0─┐ ACK └───┘ └───┘ └───┘ └───┘ └────── MSB LSB

三、EEPROM 读写代码实现(软件模拟 I²C)

以 AT24C02(7 位地址0xA0>>1 = 0x50)为例,使用 GPIO 软件模拟 I²C。

3.1 宏定义与基础函数

#include"stm32f10x.h"#defineI2C_GPIOGPIOB#defineI2C_SCL_PINGPIO_Pin_6#defineI2C_SDA_PINGPIO_Pin_7#defineSCL_H()GPIO_SetBits(I2C_GPIO,I2C_SCL_PIN)#defineSCL_L()GPIO_ResetBits(I2C_GPIO,I2C_SCL_PIN)#defineSDA_H()GPIO_SetBits(I2C_GPIO,I2C_SDA_PIN)#defineSDA_L()GPIO_ResetBits(I2C_GPIO,I2C_SDA_PIN)#defineSDA_READ()GPIO_ReadInputDataBit(I2C_GPIO,I2C_SDA_PIN)/* 简单延时,适配 100kHz 标准模式 */staticvoidi2c_delay(void){for(volatileinti=0;i<10;i++);}/* 初始化:SCL/SDA 配置为开漏输出 */voidI2C_Init(void){GPIO_InitTypeDef gpio;RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);gpio.GPIO_Pin=I2C_SCL_PIN|I2C_SDA_PIN;gpio.GPIO_Mode=GPIO_Mode_Out_OD;// ★ 开漏输出gpio.GPIO_Speed=GPIO_Speed_50MHz;GPIO_Init(I2C_GPIO,&gpio);SCL_H();SDA_H();// 空闲态}

3.2 起始 / 停止信号

/* START: SCL 高时 SDA 下降沿 */voidI2C_Start(void){SDA_H();SCL_H();i2c_delay();SDA_L();// 高电平下拉低 SDAi2c_delay();SCL_L();// 钳住总线,准备发送i2c_delay();}/* STOP: SCL 高时 SDA 上升沿 */voidI2C_Stop(void){SDA_L();SCL_H();i2c_delay();SDA_H();// 高电平下释放 SDAi2c_delay();}

3.3 发送一个字节 + 等待应答

/* 发送 1 字节,返回 0=ACK,1=NACK */uint8_tI2C_SendByte(uint8_tbyte){for(uint8_ti=0;i<8;i++){SCL_L();if(byte&0x80)SDA_H();// MSB 优先elseSDA_L();byte<<=1;i2c_delay();SCL_H();// 高电平期间从机采样i2c_delay();}/* 第 9 个时钟:接收 ACK */SCL_L();SDA_H();// 主机释放 SDAi2c_delay();SCL_H();i2c_delay();uint8_tack=SDA_READ();// 0=ACK, 1=NACKSCL_L();i2c_delay();returnack;}

3.4 读取一个字节 + 发送应答

/* ack=0 主机发 ACK(继续读),ack=1 主机发 NACK(结束读) */uint8_tI2C_ReadByte(uint8_tack){uint8_tbyte=0;SDA_H();// 主机释放 SDA,交给从机驱动for(uint8_ti=0;i<8;i++){SCL_L();i2c_delay();SCL_H();// 高电平期间采样i2c_delay();byte<<=1;if(SDA_READ())byte|=0x01;}/* 第 9 个时钟:主机发 ACK/NACK */SCL_L();if(ack)SDA_H();// NACKelseSDA_L();// ACKi2c_delay();SCL_H();i2c_delay();SCL_L();SDA_H();// 释放总线returnbyte;}

3.5 EEPROM 写一个字节

#defineEEPROM_ADDR0xA0// 7位地址 0x50 左移 1 位 + W=0voidEEPROM_WriteByte(uint8_treg,uint8_tdata){I2C_Start();I2C_SendByte(EEPROM_ADDR|0);// 从机地址 + 写I2C_SendByte(reg);// 寄存器(内存)地址I2C_SendByte(data);// 写入数据I2C_Stop();/* AT24C02 需要 5ms 内部写周期 */for(volatileuint32_ti=0;i<50000;i++);}

3.6 EEPROM 读一个字节

uint8_tEEPROM_ReadByte(uint8_treg){uint8_tdata;/* 第一阶段:假写,定位寄存器地址 */I2C_Start();I2C_SendByte(EEPROM_ADDR|0);// 写I2C_SendByte(reg);/* 第二阶段:RESTART,切换到读 */I2C_Start();// RESTARTI2C_SendByte(EEPROM_ADDR|1);// 读data=I2C_ReadByte(1);// 读 1 字节并回 NACKI2C_Stop();returndata;}

3.7 页写与连续读(进阶)

/* 页写:AT24C02 一页 8 字节,跨页会回卷,注意地址对齐 */voidEEPROM_WritePage(uint8_treg,uint8_t*buf,uint8_tlen){I2C_Start();I2C_SendByte(EEPROM_ADDR|0);I2C_SendByte(reg);for(uint8_ti=0;i<len;i++)I2C_SendByte(buf[i]);I2C_Stop();for(volatileuint32_ti=0;i<50000;i++);}/* 连续读:地址自动递增 */voidEEPROM_ReadSeq(uint8_treg,uint8_t*buf,uint8_tlen){I2C_Start();I2C_SendByte(EEPROM_ADDR|0);I2C_SendByte(reg);I2C_Start();I2C_SendByte(EEPROM_ADDR|1);for(uint8_ti=0;i<len;i++){uint8_tack=(i==len-1)?1:0;// 最后一个 NACKbuf[i]=I2C_ReadByte(ack);}I2C_Stop();}

四、工程调试要点

问题原因解决
从机无 ACK地址算错 / 器件没上电确认 7 位地址左移 1 位后拼 R/W
数据全 0xFF上拉缺失 / SDA 一直高检查 SDA、SCL 是否都接 4.7k 上拉
偶发通信失败上升沿太慢减小上拉阻值 / 降低速率
EEPROM 写后读不出未等写周期加 5ms 延时或 ACK 轮询
高电平被拉不上去误配为推挽GPIO 必须配Open-Drain

五、总结

  1. 物理层:开漏 + 上拉 → 线与逻辑 → 多设备安全共存,且天然支持电平转换。
  2. 协议层:START/STOP 由 SCL 高电平期间 SDA 跳变产生;数据 SCL 高时采样、低时改变;每字节跟随一位 ACK/NACK。
  3. 代码层:软件模拟的关键是**严格按“SCL 低改变 SDA,SCL 高采样 SDA”**的顺序,起始、停止、ACK 三步最易写错。
  4. 读取 EEPROM必须使用RESTART而不是 STOP,才能保持对同一从机的连续操作。

掌握上面这三点(推挽/开漏电气基础、时序规则、代码实现),就能独立完成绝大多数 I²C 外设(EEPROM、SHT30、MPU6050、OLED 等)的驱动开发。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询