stm32——I2C协议

I2C协议概述


I2C协议详解

概述

I2C(Inter-Integrated Circuit)协议是由Philips(现NXP)开发的串行通信总线,用于连接微控制器和外围设备(如EEPROM、传感器、ADC/DAC等)。它是一种同步、半双工、多主多从的串行通信协议。

主要特点

  • 两线制 :仅使用两根线进行通信:
    • SCL (Serial Clock Line):串行时钟线(由主设备生成)
    • SDA (Serial Data Line):串行数据线(双向传输)
  • 多主多从:总线上可连接多个主设备和多个从设备
  • 半双工:同一时刻只能单向传输数据
  • 地址寻址:每个从设备有唯一7位或10位地址
  • 开漏输出:SCL和SDA均为开漏输出,需外接上拉电阻(4.7kΩ或10kΩ)
  • 线与逻辑:支持多主设备仲裁

连接拓扑

SCL SDA SCL SDA SCL SDA SCL SDA Master 1 SCL总线 Master 2 Slave 1 Slave 2 上拉电阻

核心原理

1. 起始条件(Start Condition)

  • SCL高电平时,SDA从高→低跳变
  • 主设备获取总线控制权
c 复制代码
SDA: -----|________
        |
SCL: _____|

2. 停止条件(Stop Condition)

  • SCL高电平时,SDA从低→高跳变
  • 主设备释放总线
c 复制代码
SDA: ________|-----
        |
SCL: _______|

3. 数据传输

  • 数据在SCL高电平期间保持稳定
  • 数据变化发生在SCL低电平期间
  • 8位数据,MSB(最高位)优先
c 复制代码
SDA: --Bit7--Bit6--...--Bit0--
      _   _   _   _   _   _   _   _
SCL: | |_| |_| |_| |_| |_| |_| |_| |

4. 应答机制(ACK/NACK)

每字节传输后的第9个时钟周期:

  • ACK:接收方拉低SDA(成功接收)
  • NACK:接收方保持SDA高电平(接收失败)
c 复制代码
SDA: ... Bit0 | ACK/NACK
             _
SCL: ... Bit0|_| 

5. 地址帧

  • 起始条件后发送
  • 7位地址 + 1位R/W方向位:
    • R/W=0:主→从写操作
    • R/W=1:主←从读操作
  • 地址匹配的从设备回复ACK

通信时序

写操作时序(主→从)

c 复制代码
主设备: [START] → [Addr+W] → [Data1] → [Data2] → [STOP]
从设备:           [ACK]     → [ACK]     → [ACK]

读操作时序(主←从)

c 复制代码
主设备: [START] → [Addr+R] → [NACK] → [STOP]
从设备:           [ACK]     → [Data1] → [Data2]

STM32实现(HAL库)

初始化流程

c 复制代码
int main(void) {
  HAL_Init();                  // 1. HAL库初始化
  SystemClock_Config();        // 2. 系统时钟配置
  MX_GPIO_Init();              // 3. GPIO初始化
  MX_I2C1_Init();              // 4. I2C外设初始化
  
  while(1) {
    // 数据传输操作
  }
}

GPIO配置示例

c 复制代码
static void MX_GPIO_Init(void) {
  GPIO_InitTypeDef GPIO_InitStruct = {0};
  
  __HAL_RCC_GPIOB_CLK_ENABLE();  // 使能GPIOB时钟
  
  // 配置PB6(SCL), PB7(SDA)
  GPIO_InitStruct.Pin = GPIO_PIN_6 | GPIO_PIN_7;
  GPIO_InitStruct.Mode = GPIO_MODE_AF_OD;  // 开漏复用
  GPIO_InitStruct.Pull = GPIO_PULLUP;      // 上拉电阻
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
  GPIO_InitStruct.Alternate = GPIO_AF4_I2C1;
  HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
}

I2C外设配置

c 复制代码
static void MX_I2C1_Init(void) {
  hi2c1.Instance = I2C1;
  hi2c1.Init.Timing = 0x00702999;       // 时序配置(根据时钟计算)
  hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT;
  hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE;
  hi2c1.Init.OwnAddress1 = 0;          // 主模式设为0
  hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE;
  hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE;
  
  if (HAL_I2C_Init(&hi2c1) != HAL_OK) {
    Error_Handler();
  }
}

数据传输操作

c 复制代码
// 写操作示例
#define SLAVE_ADDR 0x68 << 1  // 7位地址左移1位
uint8_t txData[] = {0x00, 0x01, 0x02}; 

HAL_I2C_Master_Transmit(&hi2c1, SLAVE_ADDR, txData, 3, 100);

// 读操作示例(先写寄存器地址,再读数据)
uint8_t regAddr = 0x32;
uint8_t rxData[2];

HAL_I2C_Master_Transmit(&hi2c1, SLAVE_ADDR, &regAddr, 1, 100);
HAL_I2C_Master_Receive(&hi2c1, SLAVE_ADDR, rxData, 2, 100);

高级模式

c 复制代码
// 中断模式
HAL_I2C_Master_Transmit_IT(&hi2c1, SLAVE_ADDR, txData, 3);

// DMA模式
HAL_I2C_Master_Transmit_DMA(&hi2c1, SLAVE_ADDR, txData, 3);

// 回调函数
void HAL_I2C_MasterTxCpltCallback(I2C_HandleTypeDef *hi2c) {
  // 传输完成处理
}

注意事项

  1. 上拉电阻必须:SCL/SDA线需外接4.7kΩ-10kΩ上拉电阻
  2. 地址格式:HAL库使用8位地址(7位地址左移1位)
  3. 时钟频率:确保主从设备支持相同速率(标准100kHz/快速400kHz)
  4. 错误处理 :检查HAL函数返回值,处理常见错误:
    • HAL_ERROR:总线错误(ACK未收到)
    • HAL_TIMEOUT:操作超时
  5. 时钟延长:STM32默认支持从设备时钟延长功能
  6. 重复起始条件:HAL库在读写组合操作中自动处理
  7. 信号完整性:长距离通信需考虑总线电容和信号干扰

协议对比(SPI vs I2C)

特性 SPI I2C
线数 4线(最小) 2线
速度 高(可达50MHz+) 中(最大3.4MHz)
传输模式 全双工 半双工
拓扑结构 主从(支持菊花链) 多主多从
寻址方式 硬件片选(CS) 软件地址
错误检测 无内置机制 ACK/NACK机制
引脚占用 随从设备增加而增加 固定2线
复杂度 硬件简单,协议简单 硬件简单,协议较复杂

STM32 I2C操作的整体流程(HAL库)

  1. GPIO初始化:配置I2C相关的GPIO引脚(SCL、SDA)为开漏复用功能,净化能上拉。
  2. I2C外设时钟使能: 使能I2C外设时钟。
  3. I2C外设置初始化:配置I2C的工作模式(主/从)、参与、地址(如果是从设备模式)等参数。
  4. 数据传输:调用HAL库提供的发送、接收函数。

STM32 I2C函数操作顺序(使用STM32CubeIDE生成的HAL库为例)

假设我们使用STM32CubeIDE生成了项目,并且已经配置好I2C外设。以下是核心的代码片段和函数调用顺序:

2.1. 物品工作(通常由CubeIDE自动生成)

main.c文件中,你会看到以下结构:

c 复制代码
// 定义一个I2C句柄
I2C_HandleTypeDef hi2c1; // 假设使用I2C1

// GPIO初始化函数声明
static void MX_GPIO_Init(void);
// I2C初始化函数声明
static void MX_I2C1_Init(void);

int main(void)
{
  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init(); // 1. HAL库初始化

  /* Configure the system clock */
  SystemClock_Config(); // 2. 系统时钟配置

  /* Initialize all configured peripherals */
  MX_GPIO_Init();   // 3. GPIO初始化
  MX_I2C1_Init();   // 4. I2C外设初始化

  /* Infinite loop */
  while (1)
  {
    /* Add user code here */
  }
}
2.2. GPIO 初始化函数 ( MX_GPIO_Init)

该函数通常由CubeIDE根据你在GPIO配置页面的设置自动生成。它使能GPIO时钟,并配置各个引脚的模式、上下拉、速度等。

c 复制代码
// Example: GPIO init for I2C1 (uses PB6 for SCL, PB7 for SDA)
static void MX_GPIO_Init(void)
{
  GPIO_InitTypeDef GPIO_InitStruct = {0};

  /* GPIO Ports Clock Enable */
  __HAL_RCC_GPIOB_CLK_ENABLE(); // Enable GPIOB clock (assuming I2C1 uses PB6, PB7)

  /*Configure GPIO pins : PB6 (SCL), PB7 (SDA) */
  GPIO_InitStruct.Pin = GPIO_PIN_6|GPIO_PIN_7; // SCL, SDA
  GPIO_InitStruct.Mode = GPIO_MODE_AF_OD; // Alternate Function Open-Drain (开漏)
  GPIO_InitStruct.Pull = GPIO_PULLUP; // 上拉电阻 (很重要!)
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH; // High speed for I2C
  GPIO_InitStruct.Alternate = GPIO_AF4_I2C1; // Set alternate function to I2C1
  HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
}
2.3. I2C外设初始化函数 ( MX_I2C1_Init)

该函数也是由CubeIDE自动生成,负责配置I2C的各种工作参数。

c 复制代码
static void MX_I2C1_Init(void)
{
  /* I2C1 parameter configuration*/
  hi2c1.Instance = I2C1; // 选择I2C1外设

  // !!! 对于STM32G0/G4/H7等新的I2C,需要配置Timing Register !!!
  // 这个值需要根据你的系统时钟和I2C速度(如100kHz, 400kHz)通过STM32CubeIDE计算或手册查找
  // 例如,对于48MHz系统时钟,100kHz I2C,可能是0x10901021UL
  hi2c1.Init.Timing = 0x00702999; // 这是一个示例值,请根据实际情况配置

  // 如果是较旧的STM32F系列,可能需要配置以下参数而不是Timing
  // hi2c1.Init.ClockSpeed = 100000; // 100kHz
  // hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2; // 占空比
  // hi2c1.Init.OwnAddress1 = 0; // 主设备模式下为0
  // hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT; // 7位地址
  // hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE; // 禁用双地址
  // hi2c1.Init.OwnAddress2 = 0;
  // hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE; // 禁用通用呼叫
  // hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE; // 禁用时钟延长

  hi2c1.Init.OwnAddress1 = 0; // 主设备模式下,自身地址通常不重要
  hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT; // 7位地址模式
  hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE;
  hi2c1.Init.OwnAddress2 = 0;
  hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE;
  hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE;

  // 调用HAL库函数进行I2C初始化
  if (HAL_I2C_Init(&hi2c1) != HAL_OK)
  {
    Error_Handler(); // 初始化失败,调用错误处理函数
  }

  // 使能模拟降噪滤波器(可选,CubeIDE默认开启)
  if (HAL_I2CEx_ConfigAnalogFilter(&hi2c1, I2C_ANALOGFILTER_ENABLE) != HAL_OK)
  {
    Error_Handler();
  }
  // 使能数字降噪滤波器(可选,CubeIDE默认开启)
  if (HAL_I2CEx_ConfigDigitalFilter(&hi2c1, 0) != HAL_OK)
  {
    Error_Handler();
  }
}

函数操作顺序总结(初始化阶段):

  1. HAL_Init(): 初始化HAL库。

  2. SystemClock_Config():配置系统时钟。

c 复制代码
MX_GPIO_Init()
  • __HAL_RCC_GPIOx_CLK_ENABLE():使能相关GPIO端口的时钟。
  • HAL_GPIO_Init():配置I2C引脚为开漏复用功能上拉
c 复制代码
MX_I2C1_Init()
  • __HAL_RCC_I2Cx_CLK_ENABLE():使能I2C外设定的时钟。
  • 配置hi2c1结构体成员(Instance,TimingClockSpeed等)。
  • HAL_I2C_Init(&hi2c1): 调用HAL库函数执行I2C硬件初始化。
  • (任选)HAL_I2CEx_ConfigAnalogFilter()/ HAL_I2CEx_ConfigDigitalFilter(): 配置滤波器。
2.4. 数据传输操作(作为主设备)

main函数的while(1)循环中,可以执行数据传输操作。

写入操作(主设备向从设备写数据):

c 复制代码
#define SLAVE_ADDRESS_WRITE 0x68 << 1 // 假设从设备地址是0x68 (ADXL345),左移1位因为HAL库期望的是8位地址 (包含R/W位)

uint8_t txData_write[] = {0x00, 0x01, 0x02}; // 第一个字节通常是内部寄存器地址,后面是要写入的数据
HAL_StatusTypeDef status;

// 1. 向从设备写入数据
// HAL_I2C_Master_Transmit(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size, uint32_t Timeout)
status = HAL_I2C_Master_Transmit(&hi2c1, SLAVE_ADDRESS_WRITE, txData_write, sizeof(txData_write), HAL_MAX_DELAY);
if (status != HAL_OK)
{
    // 错误处理,例如:
    if (status == HAL_ERROR) {
        // 总线错误,如ACK未收到
    } else if (status == HAL_TIMEOUT) {
        // 超时
    }
    Error_Handler();
}
HAL_Delay(10); // 延时等待从设备处理

读取操作(主设备从从设备读取数据):

  • 场景1:直接读取(某些设备支持,不常用,通常需要先读取接收地址)

    c 复制代码
    #define SLAVE_ADDRESS_READ 0x68 << 1 // 从设备地址
    uint8_t rxData_read[5]; // 接收5个字节
    
    // 1. 从从设备读取数据
    status = HAL_I2C_Master_Receive(&hi2c1, SLAVE_ADDRESS_READ, rxData_read, sizeof(rxData_read), HAL_MAX_DELAY);
    if (status != HAL_OK)
    {
        Error_Handler();
    }
    HAL_Delay(10);
  • 场景2:先读取寄存器地址,再从该寄存器读取数据(最常用)

    c 复制代码
    #define SLAVE_ADDRESS 0x68 << 1 // 从设备地址
    uint8_t regAddress = 0x32; // 假设要读取的寄存器地址
    uint8_t rxData_reg[2]; // 接收2个字节数据
    
    // 1. 向从设备写入要读取的寄存器地址 (这里TxData只有一个字节:寄存器地址)
    status = HAL_I2C_Master_Transmit(&hi2c1, SLAVE_ADDRESS, &regAddress, 1, HAL_MAX_DELAY);
    if (status != HAL_OK)
    {
        Error_Handler();
    }
    
    // 2. 紧接着,从该从设备读取数据 (HAL库内部会处理重复起始条件)
    status = HAL_I2C_Master_Receive(&hi2c1, SLAVE_ADDRESS, rxData_reg, sizeof(rxData_reg), HAL_MAX_DELAY);
    if (status != HAL_OK)
    {
        Error_Handler();
    }
    HAL_Delay(10);

函数操作顺序总结(数据传输阶段 - 支撑模式):

  1. 写操作

    c 复制代码
    HAL_I2C_Master_Transmit(&hi2c, DevAddress, pData, Size, Timeout)

    :主设备向从设备发送数据。

    • DevAddress:从设备的7位地址左移1位,并包含读写位(写操作时为0)。
    • pData: 指向要发送数据的弧度。
    • Size: 要发送的数据字节数。
    • Timeout: 超时时间。
  2. 读操作

    • 方法一(直接读取)

      c 复制代码
      HAL_I2C_Master_Receive(&hi2c, DevAddress, pData, Size, Timeout)

      主设备从从设备接收数据。

      • DevAddress:从设备的7位地址左移1位,并包含读写位(读操作时为1)。
    • 方法二(先写注册地址再读,推荐)

      • HAL_I2C_Master_Transmit(&hi2c, DevAddress, &RegAddress, 1, Timeout): 发送地址。
      • HAL_I2C_Master_Receive(&hi2c, DevAddress, pData, Size, Timeout): 接收数据。
2.5. 中断和DMA模式(高级应用)

与SPI类似,I2C也支持中断和DMA模式,以提高CPU利用率和数据吞吐量。

中断模式:

  1. 使能I2C中断

    c 复制代码
    MX_I2C1_Init()

    之后,通常会包含以下代码(或在CubeIDE中勾选):

    c 复制代码
    HAL_NVIC_SetPriority(I2C1_EV_IRQn, 0, 0); // 设置事件中断优先级
    HAL_NVIC_EnableIRQ(I2C1_EV_IRQn); // 使能I2C1事件中断
    HAL_NVIC_SetPriority(I2C1_ER_IRQn, 0, 0); // 设置错误中断优先级
    HAL_NVIC_EnableIRQ(I2C1_ER_IRQn); // 使能I2C1错误中断
  2. 传输函数

    • HAL_I2C_Master_Transmit_IT(&hi2c, DevAddress, pData, Size)
    • HAL_I2C_Master_Receive_IT(&hi2c, DevAddress, pData, Size)
  3. 中断回调函数

    • HAL_I2C_MasterTxCpltCallback(&hi2c):主设备发送完成回调
    • HAL_I2C_MasterRxCpltCallback(&hi2c):主设备接收完成回调
    • HAL_I2C_ErrorCallback(&hi2c): 错误回调

DMA模式:

  1. 配置DMA通道:在CubeIDE中配置DMA(Tx和Rx)。
  2. 传输函数
    • HAL_I2C_Master_Transmit_DMA(&hi2c, DevAddress, pData, Size)
    • HAL_I2C_Master_Receive_DMA(&hi2c, DevAddress, pData, Size)
  3. DMA中断回调函数:与中断模式类似,DMA传输完成后会触发对应的I2C回调函数。

函数操作顺序总结(中断/DMA模式):

  1. 初始化阶段
    • 确定中断或DMA配置正确(使能中断/DMA通道,设置优先级)。
    • HAL_I2C_Init()内部会处理中断使能(如果是中断模式)或DMA关联。
  2. 传输等级
    • 调用HAL_I2C_Master_Transmit_IT/DMA()HAL_I2C_Master_Receive_IT/DMA()。这些函数会立即返回。
    • 在主程序中,可以执行其他任务。
    • 等待或响应回调 :在HAL_I2C_MasterTxCpltCallbackHAL_I2C_MasterRxCpltCallback中处理数据完成的逻辑。
    • 处理HAL_I2C_ErrorCallback中的错误。

3、注意事项

  • 上拉电阻:I2C通道必须连接外部上拉电阻,否则无法正常工作。
  • 地址 :从设备的I2C地址通常是7位的。在HAL库中,格式化的DevAddress参数是8位的,即7位地址左移1位,最低位用于表示读/写方向。例如,如果从设备地址是0x68,那么写入时格式0x68 << 1(即0xD0),读取时也格式0x68 << 1(即0xD0),HAL库会自动处理读写位。
  • 时钟频率:I2C的时钟频率通常有标准模式(100kHz)、快速模式(400kHz)和快速模式+(1MHz)。一定要保证主从设备的时钟频率兼容。
  • 重复起始条件 :对于大多数先读写寄存器再读取数据需要的I2C设备,HAL库的HAL_I2C_Master_Transmit()HAL_I2C_Master_Receive()组合使用时,会自动在两次传输之间生成一个重复起始条件,这是非常方便的。
  • 错误处理:I2C通信比较复杂,容易出现ACK错误、存储器丢失、超时等问题。一定要检查HAL函数的返回值,并在出现错误时进行适当处理(例如,重试、停止发送条件复位初始化等)。
  • 时钟延长(Clock Stretching):从设备可以在SCL线低电平来"暂停"通信,直到它准备好继续延续。STM32 I2C外设默认保持支持时钟延长。
相关推荐
章鱼哥嵌入式开发2 小时前
#STM32 HAL库实现的STM32F407时钟配置程序以及和STM32F103配置对比
stm32·单片机
jmlinux2 小时前
C 语言开发中常见的开发环境
c语言·开发语言·stm32·单片机
百里东风2 小时前
STM32CubeDAC及DMA配置
android·stm32·嵌入式硬件
平凡灵感码头2 小时前
STM32 智能小车项目 L298N 电机驱动模块
stm32·单片机·嵌入式硬件
Kandiy180253981873 小时前
ESP8285乐鑫SOCwifi芯片32bit MCU和2.4 GHz Wi-Fi
单片机·嵌入式硬件
明早你自己说3 小时前
根据Cortex-M3(包括STM32F1)权威指南讲解MCU内存架构与如何查看编译器生成的地址具体位置
stm32·架构·内存
小水林4 小时前
InlineHook的原理与做法
单片机·嵌入式硬件
南棱笑笑生5 小时前
20250602在荣品的PRO-RK3566开发板的Android13下打开HDMI显示
单片机·嵌入式硬件
茯苓gao5 小时前
STM32G4 电机外设篇(四)DAC输出电流波形 + CAN通讯
stm32·单片机·嵌入式硬件
嵌入式之入坑笔记8 小时前
怎么快速判断一款MCU能否跑RTOS系统
单片机·嵌入式硬件