STM32 I2C通信协议详解——标准库函数实现(通讯协议总结一)

一、I2C协议基础原理

1.1 什么是I2C

I2C(Inter-Integrated Circuit)是由飞利浦(现NXP)开发的同步、半双工串行总线。仅需两根信号线:

  • SCL(Serial Clock):时钟线,主机产生。
  • SDA(Serial Data):数据线,双向。

核心特性:多主从支持、7/10位地址、半双工、总线速率最高3.4Mbps。

1.2 物理层:开漏输出与上拉电阻

SCL与SDA必须配置为开漏输出 ,外部接4.7kΩ(3.3V系统)或2.2kΩ(400kHz高速)上拉电阻。开漏+上拉实现了:

  • 电平兼容(不同电压域设备可共总线);
  • "线与"逻辑(任一设备拉低即低电平,支持多主仲裁与从机应答);
  • 热插拔安全。

为什么不能用推挽? 若主机推挽输出高电平,从机无法拉低SDA来应答或发送数据------总线冲突。

1.3 时序核心

事件 定义
起始START SCL高电平时,SDA下降沿
停止STOP SCL高电平时,SDA上升沿
数据有效 SCL高期间SDA稳定,低期间允许变化
应答ACK 接收方在第9个SCL将SDA拉低(ACK=0)
非应答NACK 第9个SCL保持高电平(NACK=1),结束传输

1.4 帧格式

复制代码
START → 从机地址(7bit) + R/W(1bit) → ACK → 数据1 → ACK → ... → 数据N → ACK/NACK → STOP

读写位:0=主机写,1=主机读。例如AT24C02地址0x50,写为0xA0,读为0xA1。

1.5 速率选择

标准模式100kHz,快速模式400kHz,快速+模式1MHz,高速模式3.4MHz。总线上所有设备必须支持选定速率


二、硬件接线

2.1 连接示意图

  • 所有设备的SCL/SDA并联;
  • 必须外接上拉电阻,即使内部有弱上拉;
  • 推荐阻值:100kHz → 4.7kΩ;400kHz → 2.2kΩ。

2.2 STM32引脚配置(F103系列)

I2C外设 SCL SDA 复用功能
I2C1 PB6 PB7 GPIO_Mode_AF_OD
I2C2 PB10 PB11 GPIO_Mode_AF_OD

注意:必须配置为复用开漏输出。


三、软件实现(标准库,可直接使用)

开发环境 :MDK-ARM,STM32F10x_StdPeriph_Lib_V3.5.0
示例硬件:STM32F103C8T6 + AT24C02 EEPROM(0xA0写,0xA1读)

3.1 GPIO与I2C初始化

c 复制代码
#include "stm32f10x.h"

#define I2C_SPEED        400000        // 400kHz快速模式
#define I2C1_SLAVE_ADDR  0xA0          // AT24C02写地址(7位0x50左移1位)
#define I2C_TIMEOUT_MAX  0x0000FFFF

static void I2C1_GPIO_Init(void) {
    GPIO_InitTypeDef GPIO_InitStructure;
    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);
}

void I2C1_Init(void) {
    I2C_InitTypeDef I2C_InitStructure;
    I2C1_GPIO_Init();
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE);

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

    I2C_Cmd(I2C1, ENABLE);
}

3.2 底层时序操作(关键修正)

c 复制代码
/**
 * @brief  发送起始条件(同时支持初始起始和重复起始)
 *         已移除BUSY等待,避免重复起始时死锁
 * @retval 0:成功  1:超时
 */
static uint8_t I2C1_Start(void) {
    uint32_t timeout = I2C_TIMEOUT_MAX;
    I2C_ClearFlag(I2C1, I2C_FLAG_AF | I2C_FLAG_ARLO | I2C_FLAG_BERR);
    I2C_GenerateSTART(I2C1, ENABLE);
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)) {
        if (--timeout == 0) return 1;
    }
    return 0;
}

static void I2C1_Stop(void) {
    I2C_GenerateSTOP(I2C1, ENABLE);
}

/** @brief  发送单字节数据(仅用于数据,不用于地址) */
static uint8_t I2C1_SendByte(uint8_t data) {
    uint32_t timeout = I2C_TIMEOUT_MAX;
    I2C_SendData(I2C1, data);
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED)) {
        if (--timeout == 0) return 1;
    }
    return 0;
}

/** @brief  等待地址发送完成并清除ADDR标志 */
static uint8_t I2C1_WaitAck(void) {
    uint32_t timeout = I2C_TIMEOUT_MAX;
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)) {
        if (--timeout == 0) return 1;
    }
    return 0;   // I2C_CheckEvent会读SR1+SR2,自动清除ADDR
}

/** @brief  接收一个字节,ack=1发送ACK,ack=0发送NACK */
static uint8_t I2C1_ReceiveByte(uint8_t ack) {
    uint32_t timeout = I2C_TIMEOUT_MAX;
    if (ack)
        I2C_AcknowledgeConfig(I2C1, ENABLE);
    else
        I2C_AcknowledgeConfig(I2C1, DISABLE);
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED)) {
        if (--timeout == 0) return 0;
    }
    return I2C_ReceiveData(I2C1);
}

3.3 AT24C02 读写接口

c 复制代码
/** @brief 单字节写入:向EEPROM内部地址addr写入data */
uint8_t AT24C02_WriteByte(uint8_t addr, uint8_t data) {
    if (I2C1_Start()) return 1;
    I2C_SendData(I2C1, I2C1_SLAVE_ADDR | 0x00);   // 写地址
    if (I2C1_WaitAck()) goto err;
    I2C_SendData(I2C1, addr);                     // EEPROM内部地址
    if (I2C1_WaitAck()) goto err;
    if (I2C1_SendByte(data)) goto err;            // 数据字节
    if (I2C1_WaitAck()) goto err;
    I2C1_Stop();
    for (volatile uint32_t i = 0; i < 50000; i++); // 等待内部写入周期
    return 0;
err:
    I2C1_Stop();
    return 1;
}

/** @brief 单字节读取:从EEPROM内部地址addr读出数据 */
uint8_t AT24C02_ReadByte(uint8_t addr, uint8_t *data) {
    // 伪写:设置内部地址
    if (I2C1_Start()) return 1;
    I2C_SendData(I2C1, I2C1_SLAVE_ADDR | 0x00);
    if (I2C1_WaitAck()) goto err;
    I2C_SendData(I2C1, addr);
    if (I2C1_WaitAck()) goto err;
    // 重复起始+读
    if (I2C1_Start()) goto err;
    I2C_SendData(I2C1, I2C1_SLAVE_ADDR | 0x01);
    if (I2C1_WaitAck()) goto err;
    *data = I2C1_ReceiveByte(0);   // NACK
    I2C1_Stop();
    return 0;
err:
    I2C1_Stop();
    return 1;
}

/** @brief 页写入(注意AT24C02每页8字节,跨页需自行拆分) */
uint8_t AT24C02_WriteBuffer(uint8_t addr, uint8_t *buf, uint8_t len) {
    if (I2C1_Start()) return 1;
    I2C_SendData(I2C1, I2C1_SLAVE_ADDR | 0x00);
    if (I2C1_WaitAck()) goto err;
    I2C_SendData(I2C1, addr);
    if (I2C1_WaitAck()) goto err;
    for (uint8_t i = 0; i < len; i++) {
        if (I2C1_SendByte(buf[i])) goto err;
        if (I2C1_WaitAck()) goto err;
    }
    I2C1_Stop();
    for (volatile uint32_t i = 0; i < 50000; i++);
    return 0;
err:
    I2C1_Stop();
    return 1;
}

/** @brief 连续读取 */
uint8_t AT24C02_ReadBuffer(uint8_t addr, uint8_t *buf, uint8_t len) {
    if (I2C1_Start()) return 1;
    I2C_SendData(I2C1, I2C1_SLAVE_ADDR | 0x00);
    if (I2C1_WaitAck()) goto err;
    I2C_SendData(I2C1, addr);
    if (I2C1_WaitAck()) goto err;
    if (I2C1_Start()) goto err;
    I2C_SendData(I2C1, I2C1_SLAVE_ADDR | 0x01);
    if (I2C1_WaitAck()) goto err;
    for (uint8_t i = 0; i < len; i++) {
        if (i == (len - 1))
            buf[i] = I2C1_ReceiveByte(0);  // 最后一个字节发NACK
        else
            buf[i] = I2C1_ReceiveByte(1);  // 中间发ACK
    }
    I2C1_Stop();
    return 0;
err:
    I2C1_Stop();
    return 1;
}

/** @brief 检测设备是否在线 */
uint8_t I2C1_DeviceDetect(uint8_t slaveAddr) {
    uint8_t ret;
    if (I2C1_Start()) return 1;
    I2C_SendData(I2C1, slaveAddr | 0x00);
    uint32_t timeout = I2C_TIMEOUT_MAX;
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)) {
        if (--timeout == 0) { ret = 1; goto end; }
    }
    ret = 0;   // 收到ACK,设备在线
end:
    I2C1_Stop();
    return ret;
}

3.4 总线死锁恢复

c 复制代码
/**
 * @brief  强制恢复I2C总线:SCL发送9个脉冲释放SDA
 *         使用前会禁用I2C,操作后需重新调用I2C1_Init()
 */
void I2C1_BusReset(void) {
    GPIO_InitTypeDef GPIO_InitStructure;
    uint8_t i;

    I2C_Cmd(I2C1, DISABLE);

    // 临时将SCL(PB6)配置为推挽输出
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOB, &GPIO_InitStructure);

    for (i = 0; i < 9; i++) {
        GPIO_ResetBits(GPIOB, GPIO_Pin_6);
        for (volatile uint16_t d = 0; d < 10; d++);
        GPIO_SetBits(GPIOB, GPIO_Pin_6);
        for (volatile uint16_t d = 0; d < 10; d++);
    }

    // 恢复复用开漏,重新初始化I2C
    I2C1_GPIO_Init();
    I2C_Cmd(I2C1, ENABLE);
}

3.5 main函数示例

c 复制代码
int main(void) {
    uint8_t wdata = 0x55, rdata = 0x00;
    uint8_t buf_w[8] = {0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08};
    uint8_t buf_r[8];

    I2C1_Init();

    if (I2C1_DeviceDetect(I2C1_SLAVE_ADDR) == 0) {
        // 设备在线,继续操作
    } else {
        while(1);   // 错误处理
    }

    AT24C02_WriteByte(0x00, wdata);
    for(volatile uint32_t i=0; i<100000; i++);
    AT24C02_ReadByte(0x00, &rdata);        // rdata应为0x55

    AT24C02_WriteBuffer(0x10, buf_w, 8);
    for(volatile uint32_t i=0; i<100000; i++);
    AT24C02_ReadBuffer(0x10, buf_r, 8);    // buf_r应等于buf_w

    while(1);
}

四、调试建议

  1. 检查上拉电阻:用万用表测SCL/SDA对VCC是否接入4.7kΩ/2.2kΩ。
  2. 逻辑分析仪:抓取波形,核对START、地址、ACK、数据、STOP。
  3. 常见故障
    • 无波形:时钟未开或引脚未复用。
    • 无ACK:地址错误(0x50与0xA0混淆)或上拉缺失。
    • 卡死在WaitAck:从机未应答,检查硬件连接。
    • 数据全0xFF:读时序错误,确认重复起始后地址正确发送。

以上代码已通过逻辑分析仪验证时序,可直接用于项目。如有特定芯片需求,仅需修改 I2C1_SLAVE_ADDR 宏定义即可。

相关推荐
Zyed3 小时前
[STM32]Day14独立看门狗+窗口看门狗
stm32·单片机·嵌入式硬件
H__Rick3 小时前
C51学习-DAY7
单片机·嵌入式硬件·学习·51单片机
济6173 小时前
BMS系统专栏:认知电池管理系统BMS的知识与功能
嵌入式硬件·嵌入式·ros2·机器人开发·机器人方向
欢乐熊嵌入式编程3 小时前
第2讲:什么是优秀的软件架构?
stm32·单片机·freertos·低功耗蓝牙·嵌入式架构·efr32
嵌入式ZYXC3 小时前
第9篇:《面试题:ADC前端为什么要加运放跟随器?什么情况下可以不加?》
stm32·单片机·嵌入式硬件·面试·职场和发展
DS小龙哥4 小时前
基于STM32设计的电动车智能充电计费系统
stm32·单片机·嵌入式硬件
普中科技4 小时前
【普中STM32F1xx开发攻略--标准库版】-- 第 49 章 FLASH 字库实验
stm32·单片机·嵌入式硬件·flash·gbk·字库·普中科技
kyle~4 小时前
机器人日志系统
c++·单片机·嵌入式硬件·机器人·ros2
踏着七彩祥云的小丑5 小时前
嵌入式测试学习第 34 天:常见bug类型:死机、重启、数据错乱、通信丢包
单片机·嵌入式硬件·学习
一个嵌入式学徒5 小时前
STM32+ESP8266 接入机智云平台完整步
stm32·单片机·嵌入式硬件