I2C(Inter-Integrated Circuit)是Philips公司开发的一种简单、高效的双线制串行通信总线,广泛应用于各种嵌入式设备中。掌握I2C通信是嵌入式开发者的必备技能。
1. I2C基础概念
1.1 什么是I2C?
I2C是一种同步、半双工的串行通信协议,仅需两根信号线即可实现设备间的通信:
- SCL:串行时钟线,用于同步数据传输
- SDA:串行数据线,用于实际的数据传输
1.2 I2C通信特点
- 多设备支持:支持总线挂载多个设备(一主多从、多主多从)
- 地址寻址:每个从设备有唯一的7位或10位地址
- 速率灵活:标准模式(100kHz)、快速模式(400kHz)、高速模式(3.4MHz)
2. I2C时序基本单元
2.1 通信控制时序
- 起始条件:SCL高电平期间,SDA从高电平切换到低电平
- 终止条件:SCL高电平期间,SDA从低电平切换到高电平
2.2 数据传输时序
- 发送字节:SCL低电平期间放置数据,SCL高电平期间保持稳定
- 接收字节:SCL低电平期间准备数据,SCL高电平期间读取数据
- 应答机制:每个字节传输后都需要应答确认
2.3 寄存器访问
- 指定地址写:向从设备的特定寄存器写入配置数据
- 指定地址读:从从设备的特定寄存器读取数据
3. 软件I2C vs 硬件I2C
| 特性 | 软件I2C | 硬件I2C |
|---|---|---|
| 实现方式 | GPIO引脚 + 软件延时 | 专用I2C硬件电路 |
| CPU占用 | 高(CPU忙于控制时序) | 低(硬件自动处理) |
| 通信速度 | 慢(通常<100kHz) | 快(可达400kHz-1MHz) |
| 时序精度 | 依赖软件延时精度 | 硬件保证,精确稳定 |
| 引脚选择 | 任意GPIO引脚 | 固定专用引脚 |
| 开发难度 | 复杂(要写底层驱动) | 简单(调用库函数) |
| 可靠性 | 中等(受中断影响) | 高(硬件保证) |
| 多主机支持 | ❌ 很难实现 | ✅ 硬件支持 |
4. 软件I2C驱动代码详解
4.1 引脚控制函数
#include "stm32f10x.h" // STM32F10x系列头文件
#include "Delay.h" // 延时函数头文件
// 写SCL时钟线函数
void MyI2C_W_SCL(uint8_t BitValue)
{
GPIO_WriteBit(GPIOB, GPIO_Pin_10, (BitAction)BitValue); // 设置PB10引脚电平
Delay_us(10); // 延时10微秒,保持时序稳定
}
// 写SDA数据线函数
void MyI2C_W_SDA(uint8_t BitValue)
{
GPIO_WriteBit(GPIOB, GPIO_Pin_11, (BitAction)BitValue); // 设置PB11引脚电平
Delay_us(10); // 延时10微秒,保持时序稳定
}
// 读SDA数据线函数
uint8_t MyI2C_R_SDA(void)
{
uint8_t BitValue;
BitValue = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11); // 读取PB11引脚电平状态
Delay_us(10); // 延时10微秒
return BitValue; // 返回读取到的电平值(0或1)
}
4.2 I2C初始化函数
// I2C初始化函数
void MyI2C_Init(void)
{
// 开启GPIOB时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
// 配置GPIO初始化结构体
GPIO_InitTypeDef GPIO_InitsStructure;
GPIO_InitsStructure.GPIO_Mode = GPIO_Mode_Out_OD; // 开漏输出模式(重要!)
GPIO_InitsStructure.GPIO_Pin = GPIO_Pin_11 | GPIO_Pin_10; // PB10(SCL)和PB11(SDA)
GPIO_InitsStructure.GPIO_Speed = GPIO_Speed_50MHz; // 50MHz速度
GPIO_Init(GPIOB, &GPIO_InitsStructure); // 应用GPIO配置
// 将SCL和SDA引脚置高(总线空闲状态)
GPIO_SetBits(GPIOB, GPIO_Pin_11 | GPIO_Pin_10);
}
4.3 基本时序控制函数
// I2C起始信号函数
void MyI2C_Start(void)
{
MyI2C_W_SCL(1); // SCL高电平(确保总线空闲)
MyI2C_W_SDA(1); // SDA高电平(确保总线空闲)
MyI2C_W_SDA(0); // SDA从高变低,产生起始条件
MyI2C_W_SCL(0); // SCL拉低,准备数据传输
}
// I2C停止信号函数
void MyI2C_Stop(void)
{
MyI2C_W_SDA(0); // SDA低电平(确保稳定状态)
MyI2C_W_SCL(1); // SCL高电平
MyI2C_W_SDA(1); // SDA从低变高,产生停止条件
}
// I2C发送字节函数
void MyI2C_SendByte(uint8_t Byte)
{
uint8_t i;
for (i = 0; i < 8; i++) // 循环8次,发送8个bit
{
MyI2C_W_SDA(Byte & (0x80 >> i)); // 从高位到低位依次设置SDA
MyI2C_W_SCL(1); // SCL拉高,从机采样数据
MyI2C_W_SCL(0); // SCL拉低,准备下一个bit
}
}
// I2C接收字节函数
uint8_t MyI2C_ReceiveByte(void)
{
uint8_t i, Byte = 0x00;
MyI2C_W_SDA(1); // 释放SDA线,设置为输入模式
for (i = 0; i < 8; i++)
{
MyI2C_W_SCL(1); // SCL拉高,准备读取数据
if(MyI2C_R_SDA() == 1) // 读取SDA电平
{
Byte |= (0x80 >> i); // 如果为高电平,设置对应位
}
MyI2C_W_SCL(0); // SCL拉低,准备下一个bit
}
return Byte; // 返回接收到的字节
}
4.4 应答控制函数
// I2C发送应答信号函数
void MyI2C_SendAck(uint8_t AckBit)
{
MyI2C_W_SDA(AckBit); // 设置应答电平(0=应答,1=非应答)
MyI2C_W_SCL(1); // SCL拉高,从机采样应答
MyI2C_W_SCL(0); // SCL拉低
}
// I2C接收应答信号函数
uint8_t MyI2C_ReceiveAck(void)
{
uint8_t AckBit;
MyI2C_W_SDA(1); // 释放SDA线,设置为输入模式
MyI2C_W_SCL(1); // SCL拉高
AckBit = MyI2C_R_SDA(); // 读取从机的应答信号
MyI2C_W_SCL(0); // SCL拉低
return AckBit; // 返回应答位(0=应答,1=非应答)
}
5. I2C通信流程解析
5.1 完整的数据写入流程
- 发送起始条件
- 发送从机地址(写模式)
- 等待应答
- 发送寄存器地址
- 等待应答
- 发送数据
- 等待应答
- 发送停止条件
5.2 完整的数据读取流程
- 发送起始条件
- 发送从机地址(写模式) - 用于指定寄存器地址
- 等待应答
- 发送要读取的寄存器地址
- 等待应答
- 发送重复起始条件
- 发送从机地址(读模式)
- 等待应答
- 读取数据
- 发送非应答
- 发送停止条件
6. 使用注意事项
6.1 软件I2C的要点
- 开漏输出:必须配置为开漏模式,支持线与功能
- 延时精度:延时时间影响通信速率和稳定性
- 中断影响:软件延时可能被中断打断,影响时序
6.2 常见问题解决
- 通信失败:检查地址是否正确、上拉电阻是否接好
- 时序错误:调整延时时间,确保满足时序要求
- 应答异常:检查从设备是否正常工作
7. 总结
I2C通信是嵌入式系统中非常重要的通信方式,通过掌握软件I2C的实现:
- 深入理解I2C协议:通过手动控制时序,加深对协议的理解
- 灵活选择引脚:不受硬件I2C引脚限制
- 调试方便:可以单步调试,观察每个时序状态