STM32通信协议——I2C

目录

  • [1. 引言](#1. 引言)
  • [2. I2C 协议基础](#2. I2C 协议基础)
    • [2.1 物理层特性](#2.1 物理层特性)
    • [2.2 数据有效性](#2.2 数据有效性)
    • [2.3 起始与停止条件](#2.3 起始与停止条件)
  • [3. I2C 时序与数据传输](#3. I2C 时序与数据传输)
    • [3.1 字节传输格式](#3.1 字节传输格式)
    • [3.2 应答机制](#3.2 应答机制)
    • [3.3 完整读写时序](#3.3 完整读写时序)
  • [4. STM32 硬件 I2C 与软件模拟 I2C](#4. STM32 硬件 I2C 与软件模拟 I2C)
    • [4.1 硬件 I2C](#4.1 硬件 I2C)
    • [4.2 软件模拟 I2C](#4.2 软件模拟 I2C)
    • [4.3 如何选择](#4.3 如何选择)
  • [5. 实战:STM32 读写 EEPROM(AT24C02)](#5. 实战:STM32 读写 EEPROM(AT24C02))
    • [5.1 硬件连接](#5.1 硬件连接)
    • [5.2 初始化代码](#5.2 初始化代码)
    • [5.3 写一个字节](#5.3 写一个字节)
    • [5.4 读一个字节](#5.4 读一个字节)
    • [5.5 主函数测试](#5.5 主函数测试)
  • [6. 常见问题与调试技巧](#6. 常见问题与调试技巧)
    • [6.1 总线卡死(SDA 一直被拉低)](#6.1 总线卡死(SDA 一直被拉低))
    • [6.2 读不到 ACK](#6.2 读不到 ACK)
    • [6.3 使用逻辑分析仪](#6.3 使用逻辑分析仪)
  • [7. 总结](#7. 总结)

1. 引言

I2C(Inter-Integrated Circuit,集成电路间总线)是由 Philips 公司(现 NXP)于 20 世纪 80 年代初推出的一种串行通信总线。它仅需两根信号线(SCL 时钟线、SDA 数据线)即可实现多个设备之间的通信,广泛应用于传感器、存储器、显示屏、时钟芯片等外设的读写控制。

在 STM32 嵌入式开发中,I2C 是最常用的通信协议之一。本文将系统讲解 I2C 协议的基本原理、时序结构、STM32 硬件 I2C 与软件模拟 I2C 的差异,并通过一个完整的读写 EEPROM 实例,带你从原理走向实战。

2. I2C 协议基础

2.1 物理层特性

I2C 总线采用**开漏(Open-Drain)**结构,所有设备通过上拉电阻连接到 VCC。总线空闲时,SCL 和 SDA 均被上拉至高电平。

  • 两根线:SCL(串行时钟线)、SDA(串行数据线)
  • 多主多从:支持一主多从、多主多从
  • 地址寻址:7 位或 10 位设备地址
  • 速率标准:标准模式 100 kbit/s、快速模式 400 kbit/s、高速模式 3.4 Mbit/s

2.2 数据有效性

在 I2C 通信中,数据在 SCL 高电平期间必须保持稳定,只有在 SCL 低电平期间 SDA 才允许变化。这一规则保证了数据传输的可靠性。

2.3 起始与停止条件

  • 起始条件(START):SCL 为高电平时,SDA 由高电平跳变为低电平
  • 停止条件(STOP):SCL 为高电平时,SDA 由低电平跳变为高电平

起始和停止条件均由主机产生,从机只能被动响应。

下面用 ASCII 字符绘制起始与停止条件的时序波形,直观展示 SCL 与 SDA 的电平变化:

text 复制代码
起始条件(START):
SCL  ──────┐         ┌──────────────
           │         │
           └─────────┘
SDA  ────┐          ┌──────────────
         │          │
         └──────────┘
         ↑
       START(SCL 高电平期间,SDA 由高跳变为低)

停止条件(STOP):
SCL  ──────┐         ┌──────────────
           │         │
           └─────────┘
SDA  ──────────┐    ┌──────────────
               │    │
               └────┘
                    ↑
                  STOP(SCL 高电平期间,SDA 由低跳变为高)

3. I2C 时序与数据传输

3.1 字节传输格式

每个字节传输 8 位数据,高位在前(MSB first)。每传输完一个字节后,接收方需要回复一个应答位(ACK/NACK)。

3.2 应答机制

  • ACK(应答):接收方在第 9 个时钟周期将 SDA 拉低,表示继续传输
  • NACK(非应答):接收方在第 9 个时钟周期保持 SDA 为高,表示结束传输或未找到设备

3.3 完整读写时序

写操作时序:

text 复制代码
START | 设备地址+W | ACK | 寄存器地址 | ACK | 数据 | ACK | STOP

对应的写操作时序波形图如下(标注 SCL、SDA 电平变化及 START/ACK/STOP 位置):

text 复制代码
SCL  ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐
     │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │
SDA  ─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘
     ↑        A7 A6 A5 A4 A3 A2 A1 A0     ↑        A7 A6 A5 A4 A3 A2 A1 A0     ↑        D7 D6 D5 D4 D3 D2 D1 D0     ↑
   START      设备地址+W(0xA0)          ACK      寄存器地址(0x00)          ACK      数据(0x5A)              ACK  STOP

对应的 Mermaid 时序图如下,更直观地展示写操作中主机与从机之间的交互顺序:
从机(AT24C02) 主机(STM32) 从机(AT24C02) 主机(STM32) #mermaid-svg-JXJzmaoWLh8sA3OF{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-JXJzmaoWLh8sA3OF .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-JXJzmaoWLh8sA3OF .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-JXJzmaoWLh8sA3OF .error-icon{fill:#552222;}#mermaid-svg-JXJzmaoWLh8sA3OF .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-JXJzmaoWLh8sA3OF .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-JXJzmaoWLh8sA3OF .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-JXJzmaoWLh8sA3OF .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-JXJzmaoWLh8sA3OF .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-JXJzmaoWLh8sA3OF .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-JXJzmaoWLh8sA3OF .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-JXJzmaoWLh8sA3OF .marker{fill:#333333;stroke:#333333;}#mermaid-svg-JXJzmaoWLh8sA3OF .marker.cross{stroke:#333333;}#mermaid-svg-JXJzmaoWLh8sA3OF svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-JXJzmaoWLh8sA3OF p{margin:0;}#mermaid-svg-JXJzmaoWLh8sA3OF .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-JXJzmaoWLh8sA3OF text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-JXJzmaoWLh8sA3OF .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-JXJzmaoWLh8sA3OF .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-JXJzmaoWLh8sA3OF .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-JXJzmaoWLh8sA3OF .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-JXJzmaoWLh8sA3OF #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-JXJzmaoWLh8sA3OF .sequenceNumber{fill:white;}#mermaid-svg-JXJzmaoWLh8sA3OF #sequencenumber{fill:#333;}#mermaid-svg-JXJzmaoWLh8sA3OF #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-JXJzmaoWLh8sA3OF .messageText{fill:#333;stroke:none;}#mermaid-svg-JXJzmaoWLh8sA3OF .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-JXJzmaoWLh8sA3OF .labelText,#mermaid-svg-JXJzmaoWLh8sA3OF .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-JXJzmaoWLh8sA3OF .loopText,#mermaid-svg-JXJzmaoWLh8sA3OF .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-JXJzmaoWLh8sA3OF .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-JXJzmaoWLh8sA3OF .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-JXJzmaoWLh8sA3OF .noteText,#mermaid-svg-JXJzmaoWLh8sA3OF .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-JXJzmaoWLh8sA3OF .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-JXJzmaoWLh8sA3OF .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-JXJzmaoWLh8sA3OF .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-JXJzmaoWLh8sA3OF .actorPopupMenu{position:absolute;}#mermaid-svg-JXJzmaoWLh8sA3OF .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-JXJzmaoWLh8sA3OF .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-JXJzmaoWLh8sA3OF .actor-man circle,#mermaid-svg-JXJzmaoWLh8sA3OF line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-JXJzmaoWLh8sA3OF :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 写周期完成,等待内部写周期 (约 5ms) 产生 START 条件 发送设备地址+W (0xA0) ACK 发送寄存器地址 (0x00) ACK 发送数据 (0x5A) ACK 产生 STOP 条件

读操作时序:

text 复制代码
START | 设备地址+W | ACK | 寄存器地址 | ACK | START | 设备地址+R | ACK | 数据 | NACK | STOP

对应的读操作时序波形图如下(标注 SCL、SDA 电平变化及 START/ACK/NACK/STOP 位置):

text 复制代码
SCL  ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐
     │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │
SDA  ─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘
     ↑        A7 A6 A5 A4 A3 A2 A1 A0     ↑        A7 A6 A5 A4 A3 A2 A1 A0     ↑        A7 A6 A5 A4 A3 A2 A1 A0     ↑        D7 D6 D5 D4 D3 D2 D1 D0     ↑
   START      设备地址+W(0xA0)          ACK      寄存器地址(0x00)          ACK     START      设备地址+R(0xA1)          ACK      数据(读回)              NACK  STOP

对应的 Mermaid 时序图如下,展示读操作中主机先写寄存器地址、再重新发起 START 并切换为读模式的完整过程:
从机(AT24C02) 主机(STM32) 从机(AT24C02) 主机(STM32) #mermaid-svg-bxc8UYT63R24z7xE{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-bxc8UYT63R24z7xE .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-bxc8UYT63R24z7xE .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-bxc8UYT63R24z7xE .error-icon{fill:#552222;}#mermaid-svg-bxc8UYT63R24z7xE .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-bxc8UYT63R24z7xE .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-bxc8UYT63R24z7xE .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-bxc8UYT63R24z7xE .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-bxc8UYT63R24z7xE .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-bxc8UYT63R24z7xE .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-bxc8UYT63R24z7xE .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-bxc8UYT63R24z7xE .marker{fill:#333333;stroke:#333333;}#mermaid-svg-bxc8UYT63R24z7xE .marker.cross{stroke:#333333;}#mermaid-svg-bxc8UYT63R24z7xE svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-bxc8UYT63R24z7xE p{margin:0;}#mermaid-svg-bxc8UYT63R24z7xE .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-bxc8UYT63R24z7xE text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-bxc8UYT63R24z7xE .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-bxc8UYT63R24z7xE .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-bxc8UYT63R24z7xE .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-bxc8UYT63R24z7xE .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-bxc8UYT63R24z7xE #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-bxc8UYT63R24z7xE .sequenceNumber{fill:white;}#mermaid-svg-bxc8UYT63R24z7xE #sequencenumber{fill:#333;}#mermaid-svg-bxc8UYT63R24z7xE #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-bxc8UYT63R24z7xE .messageText{fill:#333;stroke:none;}#mermaid-svg-bxc8UYT63R24z7xE .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-bxc8UYT63R24z7xE .labelText,#mermaid-svg-bxc8UYT63R24z7xE .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-bxc8UYT63R24z7xE .loopText,#mermaid-svg-bxc8UYT63R24z7xE .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-bxc8UYT63R24z7xE .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-bxc8UYT63R24z7xE .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-bxc8UYT63R24z7xE .noteText,#mermaid-svg-bxc8UYT63R24z7xE .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-bxc8UYT63R24z7xE .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-bxc8UYT63R24z7xE .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-bxc8UYT63R24z7xE .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-bxc8UYT63R24z7xE .actorPopupMenu{position:absolute;}#mermaid-svg-bxc8UYT63R24z7xE .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-bxc8UYT63R24z7xE .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-bxc8UYT63R24z7xE .actor-man circle,#mermaid-svg-bxc8UYT63R24z7xE line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-bxc8UYT63R24z7xE :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 产生 START 条件 发送设备地址+W (0xA0) ACK 发送寄存器地址 (0x00) ACK 重新产生 START 条件 (Restart) 发送设备地址+R (0xA1) ACK 发送数据 (读回) NACK (主机发送非应答,结束读取) 产生 STOP 条件

读操作中,主机在写完寄存器地址后需要重新发送起始条件,并切换为读模式。

4. STM32 硬件 I2C 与软件模拟 I2C

4.1 硬件 I2C

STM32 内置硬件 I2C 外设,支持中断和 DMA 传输,CPU 占用低,时序由硬件自动生成,稳定性好。但 STM32F1 系列硬件 I2C 存在一些已知的 Bug,需要谨慎处理错误标志位。

4.2 软件模拟 I2C

软件模拟 I2C 通过 GPIO 翻转电平实现时序,代码可移植性强,不受硬件外设限制,适合对时序要求不高的场景。缺点是占用 CPU 资源,速率受限于 GPIO 翻转速度。

下面给出基于 GPIO 翻转的软件模拟 I2C 核心时序代码,包含起始、停止、发送字节、接收字节四个基本操作。与硬件 I2C 相比,这里需要手动控制 SCL 和 SDA 的电平翻转,并通过延时函数保证时序满足协议要求。

c 复制代码
// 引脚定义:SCL -> PB6,SDA -> PB7(推挽输出)
#define I2C_SCL_H()   GPIO_SetBits(GPIOB, GPIO_Pin_6)
#define I2C_SCL_L()   GPIO_ResetBits(GPIOB, GPIO_Pin_6)
#define I2C_SDA_H()   GPIO_SetBits(GPIOB, GPIO_Pin_7)
#define I2C_SDA_L()   GPIO_ResetBits(GPIOB, GPIO_Pin_7)
#define I2C_SDA_READ() GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_7)

// 关键延时:约 5us,决定总线速率(约 100kHz 标准模式)
// 延时越大速率越低,可根据实际需求调整
static void I2C_Delay(void)
{
    delay_us(5);
}

// 起始条件:SCL 高电平期间,SDA 由高跳变为低
void I2C_Start(void)
{
    I2C_SDA_H();
    I2C_SCL_H();
    I2C_Delay();
    I2C_SDA_L();   // SCL 为高时 SDA 拉低,产生 START
    I2C_Delay();
    I2C_SCL_L();   // 拉低 SCL,准备传输数据
    I2C_Delay();
}

// 停止条件:SCL 高电平期间,SDA 由低跳变为高
void I2C_Stop(void)
{
    I2C_SDA_L();
    I2C_SCL_H();
    I2C_Delay();
    I2C_SDA_H();   // SCL 为高时 SDA 拉高,产生 STOP
    I2C_Delay();
}

// 发送一个字节:高位在前(MSB first),每发送一位后等待从机 ACK
void I2C_SendByte(uint8_t data)
{
    uint8_t i;
    for (i = 0; i < 8; i++)
    {
        // 先拉低 SCL,再改变 SDA,保证数据在 SCL 高电平期间稳定
        I2C_SCL_L();
        if (data & 0x80)      // 取最高位
            I2C_SDA_H();
        else
            I2C_SDA_L();
        data <<= 1;           // 左移,准备下一位
        I2C_Delay();
        I2C_SCL_H();          // SCL 拉高,从机在上升沿采样数据
        I2C_Delay();
    }
    // 释放 SDA,等待从机返回 ACK(第 9 个时钟)
    I2C_SCL_L();
    I2C_SDA_H();              // 释放 SDA,由从机拉低表示 ACK
    I2C_Delay();
    I2C_SCL_H();
    I2C_Delay();
    // 读取 ACK 位(1 表示 NACK,0 表示 ACK)
    // 实际使用时可在此判断 I2C_SDA_READ() 的值
    I2C_SCL_L();
    I2C_Delay();
}

// 接收一个字节:高位在前,主机在第 9 个时钟释放 SDA 并读取数据
uint8_t I2C_ReceiveByte(void)
{
    uint8_t i, data = 0;
    I2C_SDA_H();              // 释放 SDA,由从机驱动数据线
    for (i = 0; i < 8; i++)
    {
        data <<= 1;
        I2C_SCL_L();
        I2C_Delay();
        I2C_SCL_H();          // SCL 拉高,主机在上升沿采样 SDA
        I2C_Delay();
        if (I2C_SDA_READ())   // 读取 SDA 电平
            data |= 0x01;
    }
    // 主机发送 ACK(拉低 SDA)或 NACK(保持高电平)
    I2C_SCL_L();
    I2C_SDA_L();              // 发送 ACK,表示继续接收
    I2C_Delay();
    I2C_SCL_H();
    I2C_Delay();
    I2C_SCL_L();
    I2C_Delay();
    return data;
}

与硬件 I2C 相比,软件模拟需要开发者手动控制每一位的时序,代码量更大,但完全不依赖芯片内部外设,可移植到任意 GPIO 平台,且便于理解 I2C 协议的底层细节。

4.3 如何选择

对比项 硬件 I2C 软件模拟 I2C
CPU 占用 低 高
时序稳定性 高 中
可移植性 依赖芯片 强
速率上限 高 受 GPIO 限制
适用场景 高速、大批量传输 简单外设、跨平台

5. 实战:STM32 读写 EEPROM(AT24C02)

5.1 硬件连接

AT24C02 是常用的 2 Kbit EEPROM 芯片,通过 I2C 接口与 STM32 连接:

  • SCL → PB6(I2C1_SCL)
  • SDA → PB7(I2C1_SDA)
  • A0/A1/A2 → GND(地址 0xA0)
  • WP → GND(允许写入)

5.2 初始化代码

c 复制代码
void I2C1_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;
    I2C_InitTypeDef I2C_InitStructure;

    RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);

    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOB, &GPIO_InitStructure);

    I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;
    I2C_InitStructure.I2C_ClockSpeed = 400000;
    I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;
    I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;
    I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
    I2C_InitStructure.I2C_OwnAddress1 = 0x00;
    I2C_Init(I2C1, &I2C_InitStructure);

    I2C_Cmd(I2C1, ENABLE);
}

5.3 写一个字节

c 复制代码
// 超时计数宏:避免总线异常时程序死循环
#define I2C_TIMEOUT_CNT  10000

// 写一个字节,返回 0 表示成功,非 0 表示超时错误
uint8_t AT24C02_WriteByte(uint8_t addr, uint8_t data)
{
    uint16_t timeout = 0;

    I2C_GenerateSTART(I2C1, ENABLE);
    timeout = I2C_TIMEOUT_CNT;
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT))
    {
        if (--timeout == 0)
        {
            printf("I2C Write Error: START timeout!\r\n");
            return 1;
        }
    }

    I2C_Send7bitAddress(I2C1, 0xA0, I2C_Direction_Transmitter);
    timeout = I2C_TIMEOUT_CNT;
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED))
    {
        if (--timeout == 0)
        {
            printf("I2C Write Error: Address ACK timeout!\r\n");
            return 2;
        }
    }

    I2C_SendData(I2C1, addr);
    timeout = I2C_TIMEOUT_CNT;
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED))
    {
        if (--timeout == 0)
        {
            printf("I2C Write Error: Register addr timeout!\r\n");
            return 3;
        }
    }

    I2C_SendData(I2C1, data);
    timeout = I2C_TIMEOUT_CNT;
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED))
    {
        if (--timeout == 0)
        {
            printf("I2C Write Error: Data timeout!\r\n");
            return 4;
        }
    }

    I2C_GenerateSTOP(I2C1, ENABLE);
    delay_ms(5);  // 等待 EEPROM 内部写周期完成
    return 0;
}

5.4 读一个字节

c 复制代码
// 读一个字节,返回 0 表示成功,非 0 表示超时错误
uint8_t AT24C02_ReadByte(uint8_t addr, uint8_t *data)
{
    uint16_t timeout = 0;

    I2C_GenerateSTART(I2C1, ENABLE);
    timeout = I2C_TIMEOUT_CNT;
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT))
    {
        if (--timeout == 0)
        {
            printf("I2C Read Error: START timeout!\r\n");
            return 1;
        }
    }

    I2C_Send7bitAddress(I2C1, 0xA0, I2C_Direction_Transmitter);
    timeout = I2C_TIMEOUT_CNT;
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED))
    {
        if (--timeout == 0)
        {
            printf("I2C Read Error: Address ACK timeout!\r\n");
            return 2;
        }
    }

    I2C_SendData(I2C1, addr);
    timeout = I2C_TIMEOUT_CNT;
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED))
    {
        if (--timeout == 0)
        {
            printf("I2C Read Error: Register addr timeout!\r\n");
            return 3;
        }
    }

    I2C_GenerateSTART(I2C1, ENABLE);
    timeout = I2C_TIMEOUT_CNT;
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT))
    {
        if (--timeout == 0)
        {
            printf("I2C Read Error: Restart timeout!\r\n");
            return 4;
        }
    }

    I2C_Send7bitAddress(I2C1, 0xA0, I2C_Direction_Receiver);
    timeout = I2C_TIMEOUT_CNT;
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED))
    {
        if (--timeout == 0)
        {
            printf("I2C Read Error: Read mode timeout!\r\n");
            return 5;
        }
    }

    timeout = I2C_TIMEOUT_CNT;
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED))
    {
        if (--timeout == 0)
        {
            printf("I2C Read Error: Data receive timeout!\r\n");
            return 6;
        }
    }
    *data = I2C_ReceiveData(I2C1);

    I2C_AcknowledgeConfig(I2C1, DISABLE);
    I2C_GenerateSTOP(I2C1, ENABLE);
    I2C_AcknowledgeConfig(I2C1, ENABLE);

    return 0;
}

5.5 主函数测试

c 复制代码
int main(void)
{
    uint8_t read_data = 0;
    uint8_t err = 0;

    I2C1_Init();
    USART1_Init();  // 用于打印调试信息

    // 写操作:检查返回值,超时则打印错误并退出
    err = AT24C02_WriteByte(0x00, 0x5A);
    if (err != 0)
    {
        printf("Write failed, error code: %d\r\n", err);
        while (1);  // 总线异常,停止执行
    }

    // 读操作:同样检查返回值
    err = AT24C02_ReadByte(0x00, &read_data);
    if (err != 0)
    {
        printf("Read failed, error code: %d\r\n", err);
        while (1);
    }

    printf("Read data: 0x%02X\r\n", read_data);

    while (1);
}

6. 常见问题与调试技巧

6.1 总线卡死(SDA 一直被拉低)

调试案例:SDA 一直被拉低,总线卡死

问题现象 :程序运行后,第一次读写 EEPROM 正常,但第二次调用 AT24C02_WriteByte 时卡死在等待 START 事件的 while 循环中,最终触发超时返回错误码 1。用万用表测量 SDA 引脚,发现其始终为低电平,无法恢复。

排查步骤:

  1. 断开所有从机设备,仅保留 STM32 与 AT24C02,问题依旧,排除地址冲突。
  2. 检查上拉电阻,发现 SDA 上拉电阻虚焊,阻值接近无穷大,导致 SDA 无法被拉高。
  3. 用逻辑分析仪抓取波形,确认 SDA 在 STOP 之后一直处于低电平:
text 复制代码
SCL  ──────┐         ┌──────────────┐         ┌──────────────
           │         │              │         │
           └─────────┘              └─────────┘
SDA  ────┐          ┌──────────────┐
         │          │              └──────────────────────────
         └──────────┘
         ↑            ↑              ↑
       START        数据/ACK        STOP 后 SDA 无法恢复高电平(总线卡死)

最终解决方案:重新焊接 SDA 上拉电阻(4.7kΩ 到 VCC),并检查 PCB 焊盘是否短路。修复后 SDA 在 STOP 后能正常恢复高电平,读写恢复正常。建议在硬件设计时给 SCL 和 SDA 各配一个 4.7kΩ 上拉电阻,并预留测试点便于排查。

6.2 读不到 ACK

调试案例:读不到 ACK,地址应答失败

问题现象 :调用 AT24C02_WriteByte 时,程序在等待地址 ACK 的 while 循环中超时,打印 I2C Write Error: Address ACK timeout!,返回错误码 2。读操作同样在地址阶段失败。

排查步骤:

  1. 核对代码中发送的设备地址为 0xA0,与硬件连接(A0/A1/A2 接 GND)一致,排除地址写错。
  2. 用逻辑分析仪抓取波形,发现主机发送的地址字节为 0xA0,但第 9 个时钟周期 SDA 保持高电平,从机未返回 ACK:
text 复制代码
SCL  ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐
     │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │
SDA  ─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ ──
     ↑       1 0 1 0 0 0 0 0        ↑
   START      设备地址+W(0xA0)     NACK(SDA 保持高电平,从机未应答)
  1. 检查电源,发现 AT24C02 的 VCC 引脚电压只有 1.8V(正常应为 3.3V),低于芯片最低工作电压,导致从机无法正常应答。

最终解决方案:修复 AT24C02 供电线路的虚焊,确保 VCC 稳定为 3.3V,并在 VCC 与 GND 之间加 100nF 去耦电容。修复后地址 ACK 正常返回,读写操作全部通过。排查 ACK 问题时,建议先用逻辑分析仪确认地址字节是否正确,再检查从机供电是否达标。

6.3 使用逻辑分析仪

调试 I2C 最有效的手段是逻辑分析仪。将 SCL 和 SDA 分别接入通道,设置合适的采样率,即可直观看到起始条件、地址、数据和应答位,快速定位问题。

7. 总结

I2C 协议凭借其引脚少、连接简单、支持多设备挂载等优势,成为嵌入式系统中不可或缺的通信方式。本文从协议原理、时序结构到 STM32 硬件实现,系统梳理了 I2C 的核心知识,并通过 AT24C02 读写实例展示了完整的开发流程。

在实际项目中,建议优先使用硬件 I2C 配合中断或 DMA,以获得更好的性能和稳定性;同时善用逻辑分析仪进行波形验证,能大幅提升调试效率。希望本文能帮助你快速掌握 STM32 的 I2C 通信开发。

参考资料

  • NXP 官方 I2C 总线规范文档(UM10204):I2C 总线协议的权威规范,详细定义了物理层特性、时序要求、起始/停止条件、寻址方式及各种速率模式,是理解 I2C 协议底层细节的第一手资料。
  • STM32 参考手册(RM0008):ST 官方针对 STM32F1 系列的技术参考手册,其中 I2C 章节详细介绍了硬件 I2C 外设的寄存器配置、事件标志、中断与 DMA 用法,以及常见 Bug 的规避方法。
  • AT24C02 数据手册(Microchip/Atmel):AT24C02 EEPROM 芯片的官方数据手册,包含器件地址配置、读写时序、内部写周期时间及电气特性等关键参数,是实战开发中不可或缺的参考。
  • STM32 固件库用户手册(STM32F10x Standard Peripheral Library):ST 官方标准外设库文档,提供了 I2C 相关 API 的详细说明与使用示例,便于快速上手硬件 I2C 编程。
  • I2C 总线规范中文解读资料:社区整理的 I2C 协议中文教程,结合时序图与实例讲解,适合初学者快速建立对 I2C 的整体认知,可作为上述官方文档的补充阅读。
相关推荐
千秋岁rr2 小时前
15.PWR电源控制
单片机·嵌入式硬件
额额额对了2 小时前
嵌入式 Linux 启动程序文件
开发语言·嵌入式硬件·php
甜甜的大香瓜2 小时前
【MT32F006】MT32F006之PWM控制背光灯(白光)
单片机
李日华大战鸡红2 小时前
FOC SVPWM过调制(学习记录)
stm32·单片机·学习
独孤九剑打醒他2 小时前
【原创开源·修订版】源-栅-漏-栅-源横向双栅MOS:从“被误解的短路”到“电流路径多值逻辑与顶层供电架构”
前端·嵌入式硬件·架构·开源·硬件工程
千里马023 小时前
STM32读取AD7606-4(HAL库)
stm32·单片机·嵌入式硬件
weixin_464078073 小时前
电路中补偿器的作用
嵌入式硬件
不会代码的小猴4 小时前
中断(NVIC)
单片机·嵌入式硬件