目录
- IIC接口概述
- IIC工作原理
- IIC通信协议详解
- [STM32 IIC硬件架构](#STM32 IIC硬件架构)
- [STM32 IIC软件实现](#STM32 IIC软件实现)
- 常见IIC设备应用实例
- 调试技巧与常见问题
- 总结
1. IIC接口概述
1.1 什么是IIC
IIC (Inter-Integrated Circuit,也常写作I2C)是由Philips(现NXP)公司于1982年开发的一种串行、同步、半双工通信总线协议。它仅使用两根信号线即可实现设备间的数据交换,广泛应用于嵌入式系统中连接各种外设。
1.2 IIC的主要特点
| 特性 | 说明 |
|---|---|
| 线数少 | 仅需SDA(数据线)和SCL(时钟线)两根线 |
| 多主多从 | 支持一主多从、多主多从架构 |
| 地址寻址 | 通过7位或10位地址识别从设备 |
| 速率灵活 | 标准模式100Kbps,快速模式400Kbps,高速模式3.4Mbps |
| 硬件简单 | 开漏输出,需外接上拉电阻 |
| 仲裁机制 | 多主模式下具有总线仲裁功能 |
1.3 IIC与SPI、UART对比
| 特性 | IIC | SPI | UART |
|---|---|---|---|
| 信号线 | 2根(SDA+SCL) | 4根(MOSI+MISO+SCK+CS) | 2根(TX+RX) |
| 通信方式 | 半双工 | 全双工 | 全双工 |
| 设备数量 | 多从机(地址寻址) | 多从机(片选信号) | 点对点 |
| 时钟 | 同步(SCL) | 同步(SCK) | 异步 |
| 速率 | 100K~3.4M bps | 可达几十Mbps | 常见9600~115200 bps |
| 复杂度 | 协议较复杂 | 协议简单 | 协议简单 |
| 应用场景 | 传感器、EEPROM | 存储器、显示屏 | 调试、蓝牙模块 |
2. IIC工作原理
2.1 物理层结构
┌─────────┐ ┌─────────┐
│ Master │ │ Slave │
│ (STM32) │ │ (Device)│
└────┬────┘ └────┬────┘
│ │
SDA ─────┼──────────────────────────────┼───── 数据线(Serial Data)
│ ┌─────┐ │
│ │ 4.7k│ │
│ │ Ω │ │
│ └─────┘ │
│ │ │
SCL ─────┼──────────────────────────────┼───── 时钟线(Serial Clock)
│ ┌─────┐ │
│ │ 4.7k│ │
│ │ Ω │ │
│ └─────┘ │
│ │ │
GND VCC(3.3V/5V) GND
注意:SDA和SCL必须外接上拉电阻(通常4.7kΩ),因为IIC设备输出为开漏(Open-Drain)结构。
2.2 开漏输出与线与逻辑
IIC使用开漏输出(Open-Drain/Open-Collector)结构:
- 输出高电平:通过外部上拉电阻实现(释放总线)
- 输出低电平:主动拉低(NMOS导通)
这种结构天然支持线与(Wired-AND)逻辑:
-
任一设备拉低,总线即为低电平
-
所有设备释放,总线才为高电平
设备A输出 ──┐ ├──→ 线与 ──→ 总线状态 设备B输出 ──┘ 设备A=0, 设备B=1 → 总线=0 设备A=1, 设备B=0 → 总线=0 设备A=0, 设备B=0 → 总线=0 设备A=1, 设备B=1 → 总线=1(由上拉电阻拉高)
2.3 总线速度模式
| 模式 | 速率 | 说明 |
|---|---|---|
| 标准模式(Standard-mode) | 100 Kbps | 最基础模式 |
| 快速模式(Fast-mode) | 400 Kbps | 常用模式 |
| 快速模式+(Fast-mode Plus) | 1 Mbps | 需要更小的上拉电阻 |
| 高速模式(High-speed mode) | 3.4 Mbps | 需要电流源上拉 |
| 超快速模式(Ultra Fast-mode) | 5 Mbps | 单向传输 |
3. IIC通信协议详解
3.1 信号类型
IIC协议定义了以下几种信号:
3.1.1 起始信号(START)
SCL: ──────┐ ┌─────┐ ┌─────┐
│ │ │ │ │
└─────┘ └─────┘ └─────
SDA: ─────────┐
│
└───── 在SCL高电平时,SDA从高变低
条件:SCL为高电平期间,SDA从高电平跳变为低电平。
3.1.2 停止信号(STOP)
SCL: ──────┐ ┌─────┐ ┌─────┐
│ │ │ │ │
└─────┘ └─────┘ └─────
SDA: ──────┐
│
└───────── 在SCL高电平时,SDA从低变高
条件:SCL为高电平期间,SDA从低电平跳变为高电平。
3.1.3 应答信号(ACK)
SCL: ──────┐ ┌─────┐
│ │ │
└─────┘ └─────
SDA: ──────┐ ┌─────────
│ │
└───────────┘ 第9个时钟周期,SDA为低电平
ACK:接收方在第9个时钟周期将SDA拉低,表示成功接收数据。
3.1.4 非应答信号(NACK)
SCL: ──────┐ ┌─────┐
│ │ │
└─────┘ └─────
SDA: ──────────────────────────
第9个时钟周期,SDA保持高电平
NACK:接收方在第9个时钟周期保持SDA为高电平,表示未接收或接收错误。
3.1.5 数据位传输
SCL: ──────┐ ┌─────┐ ┌─────┐ ┌─────┐
│ │ │ │ │ │ │
└─────┘ └─────┘ └─────┘ └─────
SDA: ────┐ ┌─────────┐ ┌─────
│ │ │ │
└───────────┘ └───────────┘
数据位1 数据位0
(SCL低电平时变化,高电平时稳定)
规则:数据在SCL低电平时改变,在SCL高电平时保持稳定。
3.2 完整数据帧格式
3.2.1 写操作帧格式
START 设备地址+W ACK 数据1 ACK 数据2 ACK ... STOP
───┐ ┌────────┐ ─┐ ┌────┐ ─┐ ┌────┐ ─┐ ───┐
│ │ 7bit+0 │ │ │ │ │ │ │ │ │
└────┘ └────┘ └────┘ └────┘ └────┘ └────
↑
0表示写操作
3.2.2 读操作帧格式
START 设备地址+R ACK 数据1 ACK 数据2 NACK STOP
───┐ ┌────────┐ ─┐ ┌────┐ ─┐ ┌────┐ ──┐ ───┐
│ │ 7bit+1 │ │ │ │ │ │ │ │ │
└────┘ └────┘ └────┘ └────┘ └─────┘ └────
↑
1表示读操作
3.2.3 复合操作(先写后读)
START 设备地址+W ACK 寄存器地址 ACK
START 设备地址+R ACK 数据 NACK STOP
─────────────────────────────────────────────────────────────
这种格式常用于:先指定寄存器地址,再读取该寄存器数据
3.3 7位地址与10位地址
7位地址格式
┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐
│ A6 │ A5 │ A4 │ A3 │ A2 │ A1 │ A0 │ R/W │
└─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘
└────────── 7位设备地址 ──────────┘ └─ 读写位
- 7位地址范围:
0x00~0x7F(实际可用0x08~`0x77`) - 广播地址:
0x00(General Call)
10位地址格式
第一字节: 1 1 1 1 0 A9 A8 R/W
第二字节: A7 A6 A5 A4 A3 A2 A1 A0
- 以
11110开头标识10位地址模式 - 支持更多设备连接
4. STM32 IIC硬件架构
4.1 STM32 I2C外设特性
STM32系列MCU集成了功能强大的I2C外设,主要特性包括:
- 支持多主模式:具备总线仲裁功能
- 支持7位/10位地址:兼容各种从设备
- 支持DMA传输:减轻CPU负担
- 支持SMBus/PMBus:兼容系统管理总线协议
- 可编程时钟:灵活配置通信速率
- 状态标志丰富:便于调试和错误处理
4.2 STM32 I2C时钟源
┌─────────────┐
│ APB1总线 │ ← 通常36MHz(STM32F1)或42MHz(STM32F4)
│ 时钟(PCLK)│
└──────┬──────┘
│
▼
┌─────────────┐
│ I2C时钟 │
│ 分频器 │ ← 配置CCR寄存器
└──────┬──────┘
│
▼
┌─────────────┐
│ SCL时钟 │ ← 输出到I2C总线
│ (100K/400K)│
└─────────────┘
4.3 关键寄存器
| 寄存器 | 名称 | 功能 |
|---|---|---|
I2C_CR1 |
控制寄存器1 | 使能I2C、配置ACK、START/STOP生成等 |
I2C_CR2 |
控制寄存器2 | 时钟频率配置、DMA使能、中断使能 |
I2C_OAR1 |
自身地址寄存器1 | 配置本机地址(从模式) |
I2C_OAR2 |
自身地址寄存器2 | 双地址模式配置 |
I2C_DR |
数据寄存器 | 发送/接收数据缓冲 |
I2C_SR1 |
状态寄存器1 | 各种事件标志位 |
I2C_SR2 |
状态寄存器2 | 主从模式标志、总线忙标志等 |
I2C_CCR |
时钟控制寄存器 | 配置SCL时钟分频 |
I2C_TRISE |
上升时间寄存器 | 配置SCL最大上升时间 |
4.4 事件标志详解(SR1寄存器)
| 位 | 标志 | 含义 |
|---|---|---|
| Bit 0 | SB | 起始条件已发送 |
| Bit 1 | ADDR | 地址已发送/已匹配 |
| Bit 2 | BTF | 字节传输完成 |
| Bit 3 | ADD10 | 10位地址已发送 |
| Bit 4 | STOPF | 检测到停止条件(从模式) |
| Bit 6 | RxNE | 接收数据寄存器非空 |
| Bit 7 | TxE | 发送数据寄存器空 |
| Bit 8 | BERR | 总线错误 |
| Bit 9 | ARLO | 仲裁丢失(多主模式) |
| Bit 10 | AF | 应答失败 |
| Bit 11 | OVR | 过载/欠载 |
5. STM32 IIC软件实现
5.1 硬件I2C实现(HAL库)
5.1.1 初始化配置
#include "stm32f1xx_hal.h"
I2C_HandleTypeDef hi2c1;
void I2C1_Init(void)
{
hi2c1.Instance = I2C1;
hi2c1.Init.ClockSpeed = 100000; // 100KHz标准模式
hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2; // 占空比2:1
hi2c1.Init.OwnAddress1 = 0; // 主模式无需配置
hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT;
hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE;
hi2c1.Init.OwnAddress2 = 0;
hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE;
hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE;
if (HAL_I2C_Init(&hi2c1) != HAL_OK)
{
Error_Handler();
}
}
// GPIO初始化(通常在HAL_I2C_MspInit中)
void HAL_I2C_MspInit(I2C_HandleTypeDef* hi2c)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
if(hi2c->Instance == I2C1)
{
__HAL_RCC_I2C1_CLK_ENABLE();
__HAL_RCC_GPIOB_CLK_ENABLE();
// 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_HIGH;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
}
}
重要:GPIO必须配置为**开漏输出(Open-Drain)**模式,并启用内部上拉或外接上拉电阻。
5.1.2 发送数据(轮询方式)
/**
* @brief 向I2C设备写入数据
* @param devAddr: 设备地址(7位地址,左移1位)
* @param pData: 待发送数据指针
* @param Size: 数据长度
* @retval HAL状态
*/
HAL_StatusTypeDef I2C_Write(uint16_t devAddr, uint8_t *pData, uint16_t Size)
{
// HAL_I2C_Master_Transmit 参数说明:
// &hi2c1: I2C句柄
// devAddr: 设备地址(需要左移1位,HAL库自动处理读写位)
// pData: 数据缓冲区
// Size: 数据长度
// 1000: 超时时间(ms)
return HAL_I2C_Master_Transmit(&hi2c1, devAddr << 1, pData, Size, 1000);
}
// 使用示例:向地址0x50的EEPROM写入数据
uint8_t writeData[] = {0x00, 0x10, 0xAB, 0xCD}; // 寄存器地址+数据
I2C_Write(0x50, writeData, sizeof(writeData));
5.1.3 接收数据(轮询方式)
/**
* @brief 从I2C设备读取数据
* @param devAddr: 设备地址
* @param regAddr: 寄存器地址(可选)
* @param pData: 接收数据缓冲区
* @param Size: 读取长度
* @retval HAL状态
*/
HAL_StatusTypeDef I2C_Read(uint16_t devAddr, uint8_t regAddr,
uint8_t *pData, uint16_t Size)
{
// 先发送寄存器地址(写操作)
HAL_StatusTypeDef status;
status = HAL_I2C_Master_Transmit(&hi2c1, devAddr << 1,
®Addr, 1, 1000);
if (status != HAL_OK) return status;
// 再读取数据(读操作)
status = HAL_I2C_Master_Receive(&hi2c1, devAddr << 1,
pData, Size, 1000);
return status;
}
// 使用示例:从MPU6050读取加速度数据
uint8_t accelData[6];
I2C_Read(0x68, 0x3B, accelData, 6); // 从寄存器0x3B开始读取6字节
5.1.4 内存地址读写(常用封装)
/**
* @brief 向指定寄存器写入单字节
*/
HAL_StatusTypeDef I2C_WriteReg(uint8_t devAddr, uint8_t regAddr, uint8_t value)
{
uint8_t data[2] = {regAddr, value};
return HAL_I2C_Master_Transmit(&hi2c1, devAddr << 1, data, 2, 1000);
}
/**
* @brief 从指定寄存器读取单字节
*/
uint8_t I2C_ReadReg(uint8_t devAddr, uint8_t regAddr)
{
uint8_t value = 0;
HAL_I2C_Master_Transmit(&hi2c1, devAddr << 1, ®Addr, 1, 1000);
HAL_I2C_Master_Receive(&hi2c1, devAddr << 1, &value, 1, 1000);
return value;
}
/**
* @brief 向指定寄存器连续写入多字节
*/
HAL_StatusTypeDef I2C_WriteReg_Multi(uint8_t devAddr, uint8_t regAddr,
uint8_t *pData, uint16_t len)
{
// 使用MemWrite更简洁(HAL库封装)
return HAL_I2C_Mem_Write(&hi2c1, devAddr << 1, regAddr,
I2C_MEMADD_SIZE_8BIT, pData, len, 1000);
}
/**
* @brief 从指定寄存器连续读取多字节
*/
HAL_StatusTypeDef I2C_ReadReg_Multi(uint8_t devAddr, uint8_t regAddr,
uint8_t *pData, uint16_t len)
{
return HAL_I2C_Mem_Read(&hi2c1, devAddr << 1, regAddr,
I2C_MEMADD_SIZE_8BIT, pData, len, 1000);
}
5.1.5 中断方式传输
// 中断发送
HAL_StatusTypeDef I2C_Write_IT(uint16_t devAddr, uint8_t *pData, uint16_t Size)
{
return HAL_I2C_Master_Transmit_IT(&hi2c1, devAddr << 1, pData, Size);
}
// 中断接收
HAL_StatusTypeDef I2C_Read_IT(uint16_t devAddr, uint8_t *pData, uint16_t Size)
{
return HAL_I2C_Master_Receive_IT(&hi2c1, devAddr << 1, pData, Size);
}
// 中断回调函数
void HAL_I2C_MasterTxCpltCallback(I2C_HandleTypeDef *hi2c)
{
if (hi2c->Instance == I2C1)
{
// 发送完成处理
printf("I2C Transmit Complete!\n");
}
}
void HAL_I2C_MasterRxCpltCallback(I2C_HandleTypeDef *hi2c)
{
if (hi2c->Instance == I2C1)
{
// 接收完成处理
printf("I2C Receive Complete!\n");
}
}
void HAL_I2C_ErrorCallback(I2C_HandleTypeDef *hi2c)
{
if (hi2c->Instance == I2C1)
{
// 错误处理
printf("I2C Error! ErrorCode = %lu\n", hi2c->ErrorCode);
}
}
5.1.6 DMA方式传输
// DMA发送(适合大数据量传输)
HAL_StatusTypeDef I2C_Write_DMA(uint16_t devAddr, uint8_t *pData, uint16_t Size)
{
return HAL_I2C_Master_Transmit_DMA(&hi2c1, devAddr << 1, pData, Size);
}
// DMA接收
HAL_StatusTypeDef I2C_Read_DMA(uint16_t devAddr, uint8_t *pData, uint16_t Size)
{
return HAL_I2C_Master_Receive_DMA(&hi2c1, devAddr << 1, pData, Size);
}
// DMA传输完成回调
void HAL_I2C_MasterTxCpltCallback(I2C_HandleTypeDef *hi2c)
{
// 与中断方式共用回调
}
5.2 软件模拟I2C实现(GPIO模拟)
当硬件I2C出现问题或引脚不足时,可使用GPIO模拟I2C。
5.2.1 引脚定义与基础操作
#include "stm32f1xx_hal.h"
// 引脚定义(可根据实际修改)
#define I2C_SCL_PORT GPIOB
#define I2C_SCL_PIN GPIO_PIN_6
#define I2C_SDA_PORT GPIOB
#define I2C_SDA_PIN GPIO_PIN_7
#define I2C_SCL_H() HAL_GPIO_WritePin(I2C_SCL_PORT, I2C_SCL_PIN, GPIO_PIN_SET)
#define I2C_SCL_L() HAL_GPIO_WritePin(I2C_SCL_PORT, I2C_SCL_PIN, GPIO_PIN_RESET)
#define I2C_SDA_H() HAL_GPIO_WritePin(I2C_SDA_PORT, I2C_SDA_PIN, GPIO_PIN_SET)
#define I2C_SDA_L() HAL_GPIO_WritePin(I2C_SDA_PORT, I2C_SDA_PIN, GPIO_PIN_RESET)
#define I2C_SDA_READ() HAL_GPIO_ReadPin(I2C_SDA_PORT, I2C_SDA_PIN)
// 延时函数(根据主频调整)
static void I2C_Delay(void)
{
for(volatile uint8_t i = 0; i < 20; i++); // 约5us@72MHz
}
// 初始化GPIO
void Soft_I2C_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
__HAL_RCC_GPIOB_CLK_ENABLE();
// SCL: 推挽输出
GPIO_InitStruct.Pin = I2C_SCL_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(I2C_SCL_PORT, &GPIO_InitStruct);
// SDA: 开漏输出(可输入)
GPIO_InitStruct.Pin = I2C_SDA_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD; // 开漏!
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(I2C_SDA_PORT, &GPIO_InitStruct);
I2C_SCL_H();
I2C_SDA_H();
}
5.2.2 起始与停止信号
/**
* @brief 产生起始信号
*/
void Soft_I2C_Start(void)
{
I2C_SDA_H();
I2C_SCL_H();
I2C_Delay();
I2C_SDA_L(); // SCL高时,SDA从高变低
I2C_Delay();
I2C_SCL_L(); // 拉低SCL,准备发送数据
I2C_Delay();
}
/**
* @brief 产生停止信号
*/
void Soft_I2C_Stop(void)
{
I2C_SDA_L();
I2C_SCL_H();
I2C_Delay();
I2C_SDA_H(); // SCL高时,SDA从低变高
I2C_Delay();
}
5.2.3 发送与接收字节
/**
* @brief 发送一个字节
* @param byte: 待发送数据
* @retval 0: 收到ACK, 1: 收到NACK
*/
uint8_t Soft_I2C_SendByte(uint8_t byte)
{
for(uint8_t i = 0; i < 8; i++)
{
if(byte & 0x80)
I2C_SDA_H();
else
I2C_SDA_L();
byte <<= 1;
I2C_Delay();
I2C_SCL_H(); // 产生时钟上升沿
I2C_Delay();
I2C_SCL_L(); // 产生时钟下降沿
I2C_Delay();
}
// 释放SDA,准备接收ACK
I2C_SDA_H();
I2C_Delay();
I2C_SCL_H();
I2C_Delay();
uint8_t ack = I2C_SDA_READ(); // 读取ACK/NACK
I2C_SCL_L();
I2C_Delay();
return ack; // 0=ACK, 1=NACK
}
/**
* @brief 接收一个字节
* @param ack: 1=发送ACK, 0=发送NACK
* @retval 接收到的数据
*/
uint8_t Soft_I2C_ReceiveByte(uint8_t ack)
{
uint8_t byte = 0;
I2C_SDA_H(); // 释放SDA
for(uint8_t i = 0; i < 8; i++)
{
byte <<= 1;
I2C_SCL_H(); // 时钟上升沿
I2C_Delay();
if(I2C_SDA_READ())
byte |= 0x01;
I2C_SCL_L(); // 时钟下降沿
I2C_Delay();
}
// 发送ACK/NACK
if(ack)
I2C_SDA_L(); // ACK: 拉低SDA
else
I2C_SDA_H(); // NACK: 保持高
I2C_Delay();
I2C_SCL_H(); // 第9个时钟
I2C_Delay();
I2C_SCL_L();
I2C_Delay();
return byte;
}
5.2.4 完整读写函数
/**
* @brief 向设备写入数据
*/
uint8_t Soft_I2C_Write(uint8_t devAddr, uint8_t regAddr,
uint8_t *pData, uint8_t len)
{
Soft_I2C_Start();
if(Soft_I2C_SendByte(devAddr << 1 | 0x00) != 0) // 发送地址+写
{
Soft_I2C_Stop();
return 1; // 无应答
}
if(Soft_I2C_SendByte(regAddr) != 0) // 发送寄存器地址
{
Soft_I2C_Stop();
return 2;
}
for(uint8_t i = 0; i < len; i++)
{
if(Soft_I2C_SendByte(pData[i]) != 0)
{
Soft_I2C_Stop();
return 3;
}
}
Soft_I2C_Stop();
return 0; // 成功
}
/**
* @brief 从设备读取数据
*/
uint8_t Soft_I2C_Read(uint8_t devAddr, uint8_t regAddr,
uint8_t *pData, uint8_t len)
{
// 先发送寄存器地址
Soft_I2C_Start();
if(Soft_I2C_SendByte(devAddr << 1 | 0x00) != 0)
{
Soft_I2C_Stop();
return 1;
}
if(Soft_I2C_SendByte(regAddr) != 0)
{
Soft_I2C_Stop();
return 2;
}
// 重复起始,发送读命令
Soft_I2C_Start();
if(Soft_I2C_SendByte(devAddr << 1 | 0x01) != 0)
{
Soft_I2C_Stop();
return 3;
}
// 读取数据
for(uint8_t i = 0; i < len; i++)
{
pData[i] = Soft_I2C_ReceiveByte(i < len - 1 ? 1 : 0);
// 最后一个字节发送NACK
}
Soft_I2C_Stop();
return 0;
}
5.3 硬件I2C vs 软件模拟I2C
| 对比项 | 硬件I2C | 软件模拟I2C |
|---|---|---|
| CPU占用 | 低(有独立外设) | 高(占用CPU时间) |
| 速率 | 可达400KHz+ | 通常100KHz以下 |
| 引脚灵活性 | 固定引脚(复用功能) | 任意GPIO |
| 可靠性 | 高(硬件处理时序) | 依赖代码精度 |
| 多主支持 | 硬件支持 | 需软件实现 |
| 调试难度 | 较难(时序不可控) | 较易(可单步调试) |
| 适用场景 | 正式产品、高速通信 | 调试、引脚受限 |
6. 常见IIC设备应用实例
6.1 EEPROM(AT24C02/04/08/16/32/64)
设备特性
| 参数 | 说明 |
|---|---|
| 地址 | 0x50 ~ 0x57(由A0/A1/A2引脚决定) |
| 容量 | 2Kbit ~ 64Kbit |
| 页大小 | 8/16/32字节(依型号) |
| 写周期 | 最大5ms |
驱动代码
#define EEPROM_ADDR 0x50 // A0=A1=A2=GND
#define EEPROM_PAGE_SIZE 8 // AT24C02页大小
/**
* @brief 向EEPROM写入数据(支持跨页写入)
*/
HAL_StatusTypeDef EEPROM_Write(uint16_t memAddr, uint8_t *pData, uint16_t len)
{
HAL_StatusTypeDef status;
uint16_t pageRemain = EEPROM_PAGE_SIZE - (memAddr % EEPROM_PAGE_SIZE);
while(len > 0)
{
uint16_t writeLen = (len > pageRemain) ? pageRemain : len;
status = HAL_I2C_Mem_Write(&hi2c1, EEPROM_ADDR << 1, memAddr,
I2C_MEMADD_SIZE_8BIT, pData, writeLen, 1000);
if(status != HAL_OK) return status;
HAL_Delay(6); // 等待写入完成(>5ms)
len -= writeLen;
memAddr += writeLen;
pData += writeLen;
pageRemain = EEPROM_PAGE_SIZE; // 后续按整页处理
}
return HAL_OK;
}
/**
* @brief 从EEPROM读取数据
*/
HAL_StatusTypeDef EEPROM_Read(uint16_t memAddr, uint8_t *pData, uint16_t len)
{
return HAL_I2C_Mem_Read(&hi2c1, EEPROM_ADDR << 1, memAddr,
I2C_MEMADD_SIZE_8BIT, pData, len, 1000);
}
// 使用示例
uint8_t writeBuf[] = "Hello I2C!";
uint8_t readBuf[20] = {0};
EEPROM_Write(0x00, writeBuf, sizeof(writeBuf));
EEPROM_Read(0x00, readBuf, sizeof(writeBuf));
6.2 MPU6050(六轴传感器)
设备特性
| 参数 | 说明 |
|---|---|
| 地址 | 0x68(AD0=GND)或 0x69(AD0=VCC) |
| 功能 | 3轴加速度 + 3轴陀螺仪 |
| 寄存器 | 0x3B~0x48(加速度+温度+陀螺仪数据) |
| 量程 | 加速度±2g/±4g/±8g/±16g,陀螺仪±250°/s~±2000°/s |
驱动代码
#define MPU6050_ADDR 0x68
// 关键寄存器
#define MPU6050_REG_PWR_MGMT_1 0x6B
#define MPU6050_REG_WHO_AM_I 0x75
#define MPU6050_REG_ACCEL_XOUT_H 0x3B
#define MPU6050_REG_GYRO_XOUT_H 0x43
#define MPU6050_REG_CONFIG 0x1A
#define MPU6050_REG_GYRO_CONFIG 0x1B
#define MPU6050_REG_ACCEL_CONFIG 0x1C
/**
* @brief MPU6050初始化
*/
uint8_t MPU6050_Init(void)
{
uint8_t id = I2C_ReadReg(MPU6050_ADDR, MPU6050_REG_WHO_AM_I);
if(id != 0x68) return 1; // 设备ID错误
// 解除休眠模式
I2C_WriteReg(MPU6050_ADDR, MPU6050_REG_PWR_MGMT_1, 0x00);
HAL_Delay(100);
// 配置采样率
I2C_WriteReg(MPU6050_ADDR, 0x19, 0x07); // SMPLRT_DIV = 7
// 配置低通滤波器
I2C_WriteReg(MPU6050_ADDR, MPU6050_REG_CONFIG, 0x06);
// 配置陀螺仪量程:±2000°/s
I2C_WriteReg(MPU6050_ADDR, MPU6050_REG_GYRO_CONFIG, 0x18);
// 配置加速度量程:±2g
I2C_WriteReg(MPU6050_ADDR, MPU6050_REG_ACCEL_CONFIG, 0x00);
return 0;
}
/**
* @brief 读取加速度数据
*/
void MPU6050_ReadAccel(int16_t *ax, int16_t *ay, int16_t *az)
{
uint8_t buf[6];
I2C_ReadReg_Multi(MPU6050_ADDR, MPU6050_REG_ACCEL_XOUT_H, buf, 6);
*ax = (int16_t)((buf[0] << 8) | buf[1]);
*ay = (int16_t)((buf[2] << 8) | buf[3]);
*az = (int16_t)((buf[4] << 8) | buf[5]);
}
/**
* @brief 读取陀螺仪数据
*/
void MPU6050_ReadGyro(int16_t *gx, int16_t *gy, int16_t *gz)
{
uint8_t buf[6];
I2C_ReadReg_Multi(MPU6050_ADDR, MPU6050_REG_GYRO_XOUT_H, buf, 6);
*gx = (int16_t)((buf[0] << 8) | buf[1]);
*gy = (int16_t)((buf[2] << 8) | buf[3]);
*gz = (int16_t)((buf[4] << 8) | buf[5]);
}
// 使用示例
int16_t ax, ay, az, gx, gy, gz;
MPU6050_Init();
while(1)
{
MPU6050_ReadAccel(&ax, &ay, &az);
MPU6050_ReadGyro(&gx, &gy, &gz);
printf("Accel: %d, %d, %d | Gyro: %d, %d, %d\n", ax, ay, az, gx, gy, gz);
HAL_Delay(100);
}
6.3 OLED显示屏(SSD1306)
设备特性
| 参数 | 说明 |
|---|---|
| 地址 | 0x3C(SA0=GND)或 0x3D(SA0=VCC) |
| 分辨率 | 128x64 或 128x32 |
| 控制方式 | 命令/数据通过Co/Dc位区分 |
驱动代码(关键部分)
#define OLED_ADDR 0x3C
#define OLED_CMD_MODE 0x00 // 命令模式
#define OLED_DATA_MODE 0x40 // 数据模式
/**
* @brief 向OLED发送命令
*/
void OLED_WriteCmd(uint8_t cmd)
{
uint8_t data[2] = {OLED_CMD_MODE, cmd};
HAL_I2C_Master_Transmit(&hi2c1, OLED_ADDR << 1, data, 2, 1000);
}
/**
* @brief 向OLED发送数据
*/
void OLED_WriteData(uint8_t *pData, uint16_t len)
{
uint8_t buf[129];
buf[0] = OLED_DATA_MODE;
memcpy(&buf[1], pData, len);
HAL_I2C_Master_Transmit(&hi2c1, OLED_ADDR << 1, buf, len + 1, 1000);
}
/**
* @brief OLED初始化序列
*/
void OLED_Init(void)
{
HAL_Delay(100); // 等待上电稳定
OLED_WriteCmd(0xAE); // 关闭显示
OLED_WriteCmd(0xD5); // 设置时钟分频
OLED_WriteCmd(0x80);
OLED_WriteCmd(0xA8); // 设置多路复用
OLED_WriteCmd(0x3F); // 1/64 duty
OLED_WriteCmd(0xD3); // 设置显示偏移
OLED_WriteCmd(0x00);
OLED_WriteCmd(0x40); // 设置显示起始行
OLED_WriteCmd(0x8D); // 使能电荷泵
OLED_WriteCmd(0x14);
OLED_WriteCmd(0x20); // 设置内存寻址模式
OLED_WriteCmd(0x00); // 水平寻址模式
OLED_WriteCmd(0xA1); // 设置段重映射
OLED_WriteCmd(0xC8); // 设置COM扫描方向
OLED_WriteCmd(0xDA); // 设置COM引脚配置
OLED_WriteCmd(0x12);
OLED_WriteCmd(0x81); // 设置对比度
OLED_WriteCmd(0xCF);
OLED_WriteCmd(0xD9); // 设置预充电周期
OLED_WriteCmd(0xF1);
OLED_WriteCmd(0xDB); // 设置VCOMH
OLED_WriteCmd(0x30);
OLED_WriteCmd(0xA4); // 全局显示开启
OLED_WriteCmd(0xA6); // 正常显示(非反色)
OLED_WriteCmd(0xAF); // 开启显示
}
/**
* @brief 清屏
*/
void OLED_Clear(void)
{
uint8_t buf[128] = {0};
for(uint8_t page = 0; page < 8; page++)
{
OLED_WriteCmd(0xB0 + page); // 设置页地址
OLED_WriteCmd(0x00); // 设置列低地址
OLED_WriteCmd(0x10); // 设置列高地址
OLED_WriteData(buf, 128); // 写入128字节0
}
}
6.4 其他常见IIC设备
| 设备 | 地址 | 功能 | 典型应用 |
|---|---|---|---|
| BMP280 | 0x76/0x77 | 气压/温度传感器 | 海拔测量、气象站 |
| SHT30 | 0x44/0x45 | 温湿度传感器 | 环境监测 |
| BH1750 | 0x23/0x5C | 光照强度传感器 | 自动亮度调节 |
| PCF8574 | 0x20~0x27 | IO扩展芯片 | 扩展GPIO |
| ADS1115 | 0x48~0x4B | 16位ADC | 高精度模拟采集 |
| DS3231 | 0x68 | 实时时钟RTC | 时间记录 |
| HMC5883L | 0x1E | 电子罗盘 | 方向检测 |
7. 调试技巧与常见问题
7.1 调试工具
7.1.1 逻辑分析仪
使用逻辑分析仪抓取I2C波形是最有效的调试手段:
推荐工具:
- Saleae Logic(8通道,支持I2C协议解析)
- DSLogic(国产,性价比高)
- cheap 8通道逻辑分析仪(淘宝,约30元)
7.1.2 示波器
观察SCL和SDA波形:
- 检查电平幅度(应为3.3V或5V)
- 检查上升沿/下降沿(上升沿不应过缓)
- 检查时序是否符合标准
7.2 常见问题排查
问题1:总线一直忙(BUSY)
现象 :I2C_SR2的BUSY位一直为1
原因与解决:
-
SDA被拉低:检查是否有设备故障将SDA拉低
-
起始/停止条件不完整:用软件发送9个时钟脉冲+STOP恢复
-
上拉电阻问题:检查或更换上拉电阻
// 总线恢复程序
void I2C_BusRecovery(void)
{
// 配置SDA为GPIO输出
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_7;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);// 发送9个时钟脉冲 for(int i = 0; i < 9; i++) { HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_SET); HAL_Delay(1); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_RESET); HAL_Delay(1); } // 发送STOP条件 HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, GPIO_PIN_RESET); HAL_Delay(1); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_SET); HAL_Delay(1); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, GPIO_PIN_SET); HAL_Delay(1); // 重新初始化I2C HAL_I2C_Init(&hi2c1);}
问题2:发送地址后无ACK
现象 :AF标志置位,设备无响应
排查步骤:
- 检查设备地址:确认地址正确(注意7位地址vs 8位地址)
- 检查设备供电:确保设备已上电
- 检查接线:SDA和SCL是否接反或接触不良
- 检查上拉电阻:用万用表测量空闲时电平是否为高
- 检查设备是否损坏:换一块芯片测试
问题3:数据接收乱码
现象:读取的数据不正确
原因与解决:
- 寄存器地址错误:确认要读取的寄存器地址
- 数据格式错误:注意大小端(STM32为小端)
- 读取长度错误:确保读取长度与数据大小匹配
- 设备未准备好:某些设备需要延时等待
问题4:硬件I2C卡死
现象:程序卡在I2C等待循环中
解决方案:
-
使用超时机制:HAL库已内置超时,可调整超时时间
-
使用中断/DMA:避免轮询阻塞
-
错误处理:在错误回调中复位I2C外设
// I2C错误处理与复位
void I2C_Error_Handler(I2C_HandleTypeDef *hi2c)
{
HAL_I2C_DeInit(hi2c);
HAL_Delay(10);
HAL_I2C_Init(hi2c);
}
7.3 上拉电阻选择
| 总线条件 | 推荐上拉电阻 | 说明 |
|---|---|---|
| 标准模式100KHz,短距离 | 4.7kΩ | 通用选择 |
| 快速模式400KHz | 2.2kΩ~4.7kΩ | 减小电阻提高上升沿 |
| 长距离/多设备 | 1kΩ~2.2kΩ | 驱动能力更强 |
| 低功耗应用 | 10kΩ | 降低静态电流 |
计算公式:R_max = t_r / (0.8473 × C_b)
- t_r: 最大上升时间(标准模式1000ns,快速模式300ns)
- C_b: 总线电容(通常<400pF)
7.4 布线建议
- SDA和SCL尽量平行布线,减少串扰
- 避免与高频信号线平行走线
- 总线长度尽量短(标准模式<1米,快速模式<0.5米)
- 多设备时采用星型或链型拓扑
- 在总线两端加适当电容滤波(<100pF)
8. 总结
8.1 IIC核心要点回顾
┌─────────────────────────────────────────────────────────────┐
│ IIC总线核心要点 │
├─────────────────────────────────────────────────────────────┤
│ 1. 两线制:SDA(数据)+ SCL(时钟) │
│ 2. 开漏输出 + 上拉电阻 = 线与逻辑 │
│ 3. 起始条件:SCL高时SDA下降沿 │
│ 4. 停止条件:SCL高时SDA上升沿 │
│ 5. 数据有效性:SCL高电平期间SDA稳定 │
│ 6. 应答机制:每字节后第9个时钟为ACK/NACK │
│ 7. 7位/10位地址寻址从设备 │
│ 8. 支持多主模式,具有仲裁机制 │
└─────────────────────────────────────────────────────────────┘
8.2 STM32开发建议
- 优先使用HAL库:简化开发,提高可移植性
- 调试时先用软件模拟I2C:确认硬件连接无误
- 使用逻辑分析仪:快速定位时序问题
- 注意超时处理:防止程序卡死
- DMA用于大数据量:如OLED显存刷新
- 中断用于实时性要求高的场景
8.3 学习路径建议
第一阶段:理解原理
↓ 学习I2C协议时序、信号类型
第二阶段:软件模拟
↓ 用GPIO实现,加深对时序的理解
第三阶段:硬件I2C
↓ 使用STM32硬件外设,掌握HAL库API
第四阶段:设备驱动
↓ 编写EEPROM、传感器等设备的驱动
第五阶段:项目实战
↓ 综合应用,解决实际问题
附录
附录A:常用I2C设备地址速查表
| 设备 | 地址(7位) | 地址(8位写) | 地址(8位读) |
|---|---|---|---|
| AT24C02 | 0x50 | 0xA0 | 0xA1 |
| AT24C04 | 0x50 | 0xA0 | 0xA1 |
| AT24C08 | 0x50 | 0xA0 | 0xA1 |
| AT24C16 | 0x50 | 0xA0 | 0xA1 |
| AT24C32 | 0x57 | 0xAE | 0xAF |
| AT24C64 | 0x50 | 0xA0 | 0xA1 |
| MPU6050 | 0x68 | 0xD0 | 0xD1 |
| MPU9250 | 0x68 | 0xD0 | 0xD1 |
| BMP280 | 0x76 | 0xEC | 0xED |
| BMP280 | 0x77 | 0xEE | 0xEF |
| SSD1306 | 0x3C | 0x78 | 0x79 |
| SSD1306 | 0x3D | 0x7A | 0x7B |
| SHT30 | 0x44 | 0x88 | 0x89 |
| SHT30 | 0x45 | 0x8A | 0x8B |
| BH1750 | 0x23 | 0x46 | 0x47 |
| BH1750 | 0x5C | 0xB8 | 0xB9 |
| PCF8574 | 0x20~0x27 | 0x40~0x4E | 0x41~0x4F |
| ADS1115 | 0x48 | 0x90 | 0x91 |
| ADS1115 | 0x49 | 0x92 | 0x93 |
| DS3231 | 0x68 | 0xD0 | 0xD1 |
| HMC5883L | 0x1E | 0x3C | 0x3D |
附录B:I2C标准时序参数(标准模式100KHz)
| 参数 | 符号 | 最小值 | 最大值 | 单位 |
|---|---|---|---|---|
| SCL时钟频率 | f_SCL | 0 | 100 | KHz |
| 起始条件保持时间 | t_HD;STA | 4.0 | - | μs |
| SCL低电平时间 | t_LOW | 4.7 | - | μs |
| SCL高电平时间 | t_HIGH | 4.0 | - | μs |
| 起始条件建立时间 | t_SU;STA | 4.7 | - | μs |
| 数据保持时间 | t_HD;DAT | 0 | 3.45 | μs |
| 数据建立时间 | t_SU;DAT | 250 | - | ns |
| SDA/SCL上升时间 | t_R | - | 1000 | ns |
| SDA/SCL下降时间 | t_F | - | 300 | ns |
| 停止条件建立时间 | t_SU;STO | 4.0 | - | μs |
| 总线空闲时间 | t_BUF | 4.7 | - | μs |
附录C:参考资源
- STM32参考手册:RM0008(STM32F1)、RM0090(STM32F4)
- STM32 HAL/LL驱动用户手册:UM1850
- NXP I2C总线规范:UM10204(I2C-bus specification and user manual)
- 正点原子/野火STM32开发指南
- 《STM32库开发实战指南》 - 刘火良
文档说明:本教程基于STM32F1/F4系列HAL库编写,如需适配其他STM32系列,请查阅对应参考手册调整寄存器名称和时钟配置。