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 宏定义即可。

相关推荐
fengfuyao9855 小时前
STM32 HAL库实现串口DMA接收不定长数据
stm32·单片机·嵌入式硬件
yuan199975 小时前
STM32直流无刷电机六拍方波控制器程序
stm32·单片机·嵌入式硬件
番茄灭世神6 小时前
PN学堂GD32教程第21篇——WiFiIOT
c语言·stm32·单片机·嵌入式·gd32
不怕犯错,就怕不做7 小时前
ARM设备异常断电容易造成数据损坏,硬件如何设计
linux·驱动开发·嵌入式硬件
jghhh018 小时前
基于DSP28335的RS485串口通信与AD采样开发方案
单片机·嵌入式硬件
2zcode9 小时前
基于STM32的直流电机串级PID伺服控制系统设计与实现
stm32·单片机·嵌入式硬件·直流电机
都在酒里9 小时前
STM32低功耗休眠详解——睡眠、停止与待机模式实战,综合应用(三)
stm32·单片机·嵌入式硬件
嵌入式小站9 小时前
STM32 零基础可移植教程 06:外部中断按键,不用一直在 while 里盯着它
stm32·单片机·嵌入式硬件