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, ®Addr, 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) {
// 传输完成处理
}
注意事项
- 上拉电阻必须:SCL/SDA线需外接4.7kΩ-10kΩ上拉电阻
- 地址格式:HAL库使用8位地址(7位地址左移1位)
- 时钟频率:确保主从设备支持相同速率(标准100kHz/快速400kHz)
- 错误处理 :检查HAL函数返回值,处理常见错误:
HAL_ERROR
:总线错误(ACK未收到)HAL_TIMEOUT
:操作超时
- 时钟延长:STM32默认支持从设备时钟延长功能
- 重复起始条件:HAL库在读写组合操作中自动处理
- 信号完整性:长距离通信需考虑总线电容和信号干扰
协议对比(SPI vs I2C)
特性 | SPI | I2C |
---|---|---|
线数 | 4线(最小) | 2线 |
速度 | 高(可达50MHz+) | 中(最大3.4MHz) |
传输模式 | 全双工 | 半双工 |
拓扑结构 | 主从(支持菊花链) | 多主多从 |
寻址方式 | 硬件片选(CS) | 软件地址 |
错误检测 | 无内置机制 | ACK/NACK机制 |
引脚占用 | 随从设备增加而增加 | 固定2线 |
复杂度 | 硬件简单,协议简单 | 硬件简单,协议较复杂 |
STM32 I2C操作的整体流程(HAL库)
- GPIO初始化:配置I2C相关的GPIO引脚(SCL、SDA)为开漏复用功能,净化能上拉。
- I2C外设时钟使能: 使能I2C外设时钟。
- I2C外设置初始化:配置I2C的工作模式(主/从)、参与、地址(如果是从设备模式)等参数。
- 数据传输:调用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();
}
}
函数操作顺序总结(初始化阶段):
-
HAL_Init()
: 初始化HAL库。 -
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
,Timing
或ClockSpeed
等)。 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, ®Address, 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);
函数操作顺序总结(数据传输阶段 - 支撑模式):
-
写操作
cHAL_I2C_Master_Transmit(&hi2c, DevAddress, pData, Size, Timeout)
:主设备向从设备发送数据。
DevAddress
:从设备的7位地址左移1位,并包含读写位(写操作时为0)。pData
: 指向要发送数据的弧度。Size
: 要发送的数据字节数。Timeout
: 超时时间。
-
读操作
-
方法一(直接读取)
cHAL_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利用率和数据吞吐量。
中断模式:
-
使能I2C中断
cMX_I2C1_Init()
之后,通常会包含以下代码(或在CubeIDE中勾选):
cHAL_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错误中断
-
传输函数
HAL_I2C_Master_Transmit_IT(&hi2c, DevAddress, pData, Size)
HAL_I2C_Master_Receive_IT(&hi2c, DevAddress, pData, Size)
-
中断回调函数
HAL_I2C_MasterTxCpltCallback(&hi2c)
:主设备发送完成回调HAL_I2C_MasterRxCpltCallback(&hi2c)
:主设备接收完成回调HAL_I2C_ErrorCallback(&hi2c)
: 错误回调
DMA模式:
- 配置DMA通道:在CubeIDE中配置DMA(Tx和Rx)。
- 传输函数
HAL_I2C_Master_Transmit_DMA(&hi2c, DevAddress, pData, Size)
HAL_I2C_Master_Receive_DMA(&hi2c, DevAddress, pData, Size)
- DMA中断回调函数:与中断模式类似,DMA传输完成后会触发对应的I2C回调函数。
函数操作顺序总结(中断/DMA模式):
- 初始化阶段
- 确定中断或DMA配置正确(使能中断/DMA通道,设置优先级)。
HAL_I2C_Init()
内部会处理中断使能(如果是中断模式)或DMA关联。
- 传输等级
- 调用
HAL_I2C_Master_Transmit_IT/DMA()
或HAL_I2C_Master_Receive_IT/DMA()
。这些函数会立即返回。 - 在主程序中,可以执行其他任务。
- 等待或响应回调 :在
HAL_I2C_MasterTxCpltCallback
或HAL_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外设默认保持支持时钟延长。