1. CH32V307 I2C通信协议深度解析
I2C(Inter-Integrated Circuit)是嵌入式系统中使用最广泛的串行通信协议之一。在CH32V307这类RISC-V架构的MCU上实现I2C通信,既可以通过硬件模块实现,也可以通过GPIO模拟(软件I2C)。本文将重点剖析软件I2C的实现细节,帮助开发者深入理解I2C协议的底层原理。
注意:本文示例基于CH32V307V-R1开发板,使用PC1(SCL)和PC2(SDA)作为通信引脚。实际开发时请根据硬件设计调整引脚定义。
2. I2C协议核心机制详解
2.1 物理层特性与电气规范
I2C总线采用两条线设计:
- SCL(Serial Clock):时钟线,由主机控制
- SDA(Serial Data):双向数据线
电气特性要求:
- 开漏输出设计:所有设备必须配置为开漏输出模式
- 上拉电阻:典型值4.7kΩ(3.3V系统)
- 通信速率:
- 标准模式:100kHz
- 快速模式:400kHz
- 高速模式:3.4MHz(需特殊处理)
2.2 协议时序关键点
2.2.1 起始条件与停止条件
起始条件(START):
- SCL为高电平时,SDA出现下降沿
- 代码实现要点:
c复制void I2C_Start(void) {
I2C_W_SDA(1); // 确保SDA高
I2C_W_SCL(1); // 确保SCL高
Delay_Us(1); // 保持时间(t_HD;STA)
I2C_W_SDA(0); // 产生下降沿
Delay_Us(1); // 保持时间(t_SU;STA)
I2C_W_SCL(0); // 准备数据传输
}
停止条件(STOP):
- SCL为高电平时,SDA出现上升沿
- 代码实现要点:
c复制void I2C_Stop(void) {
I2C_W_SDA(0); // 确保SDA低
I2C_W_SCL(0); // 确保SCL低
Delay_Us(1);
I2C_W_SCL(1); // 先拉高SCL
Delay_Us(1); // 保持时间(t_SU;STO)
I2C_W_SDA(1); // 产生上升沿
Delay_Us(1); // 总线空闲时间(t_BUF)
}
2.2.2 数据有效性规则
数据传输遵循以下时序:
- 数据变化必须在SCL低电平期间完成
- 数据采样在SCL高电平期间进行
- 每个字节(8bit)传输后必须跟随一个应答位
2.3 地址帧与数据帧结构
标准I2C通信包含以下要素:
- 从机地址:7位地址 + 1位方向位(R/W)
- 写操作:地址 | 0
- 读操作:地址 | 1
- 数据格式:MSB优先传输
- 应答机制:
- ACK(低电平):接收方确认收到数据
- NACK(高电平):接收方未确认
3. 软件I2C完整实现
3.1 GPIO初始化配置
c复制void I2C_GPIO_Init(void) {
GPIO_InitTypeDef GPIO_InitStructure;
// 使能GPIOC时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);
// 配置PC1(SCL)和PC2(SDA)为开漏输出
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1 | GPIO_Pin_2;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOC, &GPIO_InitStructure);
// 初始化总线状态
I2C_W_SCL(1);
I2C_W_SDA(1);
}
关键点:必须配置为开漏输出模式,并确保初始状态为高电平(总线空闲状态)。
3.2 字节传输函数实现
3.2.1 发送单字节
c复制void I2C_SendByte(uint8_t Byte) {
for(uint8_t i = 0; i < 8; i++) {
I2C_W_SCL(0); // 准备数据变化
Delay_Us(1);
I2C_W_SDA((Byte & (0x80 >> i)) ? 1 : 0); // 高位先行
Delay_Us(1);
I2C_W_SCL(1); // 数据有效
Delay_Us(2); // 保持高电平时间(t_HIGH)
I2C_W_SCL(0); // 准备下一位
Delay_Us(1);
}
// 释放SDA线准备接收ACK
I2C_W_SDA(1);
Delay_Us(1);
}
3.2.2 接收单字节
c复制uint8_t I2C_ReceiveByte(void) {
uint8_t Byte = 0;
I2C_W_SDA(1); // 释放SDA线
for(uint8_t i = 0; i < 8; i++) {
I2C_W_SCL(1); // 准备采样
Delay_Us(1);
if(I2C_R_SDA()) {
Byte |= (0x80 >> i); // 高位先进
}
Delay_Us(2);
I2C_W_SCL(0); // 准备下一位
Delay_Us(1);
}
return Byte;
}
3.3 应答处理机制
c复制// 发送ACK/NACK
void I2C_SendAck(uint8_t ack) {
I2C_W_SCL(0); // 准备数据变化
I2C_W_SDA(ack ? 1 : 0);
Delay_Us(1);
I2C_W_SCL(1); // 应答有效
Delay_Us(3); // 保持时间(t_ACK)
I2C_W_SCL(0); // 准备后续操作
I2C_W_SDA(1); // 释放SDA
}
// 等待ACK
uint8_t I2C_WaitAck(void) {
uint8_t ack = 0;
I2C_W_SCL(0);
I2C_W_SDA(1); // 释放SDA
Delay_Us(1);
I2C_W_SCL(1); // 准备采样ACK
Delay_Us(1);
if(I2C_R_SDA()) {
ack = 1; // 收到NACK
}
I2C_W_SCL(0);
return ack;
}
4. 典型应用场景实现
4.1 与MPU6050通信示例
c复制#define MPU6050_ADDR 0xD0
uint8_t MPU6050_ReadReg(uint8_t reg) {
uint8_t data;
I2C_Start();
I2C_SendByte(MPU6050_ADDR); // 发送设备地址(写)
I2C_WaitAck();
I2C_SendByte(reg); // 发送寄存器地址
I2C_WaitAck();
I2C_Start();
I2C_SendByte(MPU6050_ADDR | 1); // 发送设备地址(读)
I2C_WaitAck();
data = I2C_ReceiveByte(); // 读取数据
I2C_SendAck(1); // 发送NACK结束读取
I2C_Stop();
return data;
}
4.2 OLED屏幕驱动适配
c复制#define OLED_ADDRESS 0x78
void OLED_WriteCmd(uint8_t cmd) {
I2C_Start();
I2C_SendByte(OLED_ADDRESS); // 设备地址
I2C_WaitAck();
I2C_SendByte(0x00); // 控制字节(命令)
I2C_WaitAck();
I2C_SendByte(cmd); // 命令字节
I2C_WaitAck();
I2C_Stop();
}
5. 调试技巧与常见问题
5.1 时序问题排查
-
用逻辑分析仪捕获实际波形,检查:
- 起始/停止条件是否符合规范
- 数据建立时间(t_SU;DAT)和保持时间(t_HD;DAT)
- SCL高/低电平时间是否满足器件要求
-
常见时序错误:
- 缺少延时导致从机无法响应
- SDA变化发生在SCL高电平期间
- 应答位处理不当
5.2 电气特性问题
-
上拉电阻选择:
- 值过小:增加功耗,可能超出驱动能力
- 值过大:上升沿过缓,影响高速通信
-
总线冲突处理:
- 所有设备必须正确配置为开漏输出
- 总线空闲时检查SDA/SCL是否为高电平
5.3 代码优化建议
- 延时调整:
c复制// 根据不同从机器件调整延时参数
#define I2C_DELAY_US 1 // 标准模式(100kHz)
// #define I2C_DELAY_US 0.5 // 快速模式(400kHz)
- 错误重试机制:
c复制uint8_t I2C_WriteWithRetry(uint8_t devAddr, uint8_t reg, uint8_t data, uint8_t retry) {
while(retry--) {
I2C_Start();
if(!I2C_SendByte(devAddr)) break;
I2C_Stop();
Delay_Ms(1);
}
// ...完整写入流程...
}
6. 进阶应用技巧
6.1 多主机总线仲裁
当多个主机同时尝试控制总线时,I2C协议通过以下机制实现仲裁:
- 每个主机在发送时同时监测SDA线状态
- 如果检测到实际SDA电平与自身发送不符,立即退出
- 仲裁失败的主机转为从机模式
6.2 时钟拉伸处理
某些从机设备(如某些传感器)可能通过保持SCL低电平来延长时钟周期。软件I2C需要增加超时检测:
c复制uint8_t I2C_WaitSCLRelease(uint32_t timeout) {
while(timeout--) {
if(I2C_R_SCL()) return 1; // SCL已释放
Delay_Us(1);
}
return 0; // 超时
}
6.3 10位地址扩展
对于支持10位地址的设备,地址帧发送方式如下:
- 第一个字节:11110 + A9 + A8 + W
- 第二个字节:A7-A0
实现示例:
c复制void I2C_Send10bitAddr(uint16_t addr, uint8_t rw) {
// 发送第一字节
I2C_SendByte(0xF0 | ((addr >> 8) & 0x03) | (rw ? 1 : 0));
I2C_WaitAck();
// 发送第二字节
I2C_SendByte(addr & 0xFF);
I2C_WaitAck();
}
在实际项目开发中,软件I2C虽然占用CPU资源较多,但其灵活性和可移植性使其在以下场景具有优势:
- 需要多个I2C接口而硬件资源不足时
- 需要特殊时序控制的设备驱动
- 作为硬件I2C故障时的备用方案
掌握软件I2C的实现原理,不仅能够解决实际开发中的各种通信问题,更能深入理解I2C协议的本质,为后续学习更复杂的通信协议打下坚实基础。
