本文手把手教你用GPIO口实现灵活可靠的I2C主设备,解决硬件I2C资源冲突问题,适用于所有嵌入式平台。
当遇到以下场景时,硬件I2C可能不再适用:
MCU硬件I2C外设数量不足
特殊引脚映射需求
硬件I2C存在已知BUG(如STM32早期版本的I2C缺陷)
需要兼容非标准时序设备
软件I2C优势:
✅ 任意GPIO均可实现
✅ 时序完全可控
✅ 跨平台移植性强
✅ 避开了硬件设计缺陷
特性 |
说明 |
信号线 |
SDA(数据线) + SCL(时钟线) |
拓扑结构 |
总线结构,支持多主多从 |
上拉电阻 |
4.7kΩ典型值(3.3V系统) |
传输速率 |
标准模式100kbps,快速模式400kbps |
// 典型I2C帧结构
[start][7bit地址 + R/W][ACK][数据][ACK]...[stop]
起始条件(S):SCL高电平时SDA从高→低
停止条件(P):SCL高电平时SDA从低→高
数据有效性:SCL高电平期间SDA必须保持稳定
ACK/NACK:第9个时钟周期接收方拉低SDA表示ACK
MCU.GPIO1 ----> SDA
MCU.GPIO2 ----> SCL
╔═ 4.7KΩ上拉电阻
╚---> VCC(3.3V/5V)
// 初始化配置(以STM32 HAL库为例)
void SW_I2C_Init(void) {
GPIO_InitTypeDef GPIO_InitStruct = {0};
// 配置为开漏输出模式,支持线与特性
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(SDA_PORT, &GPIO_InitStruct);
HAL_GPIO_Init(SCL_PORT, &GPIO_InitStruct);
// 初始状态释放总线
HAL_GPIO_WritePin(SDA_PORT, SDA_PIN, GPIO_PIN_SET);
HAL_GPIO_WritePin(SCL_PORT, SCL_PIN, GPIO_PIN_SET);
}
void I2C_Start(void) {
SDA_HIGH(); // 确保SDA为高
SCL_HIGH();
Delay_us(5); // 保持时间t_HD;STA
SDA_LOW(); // 产生下降沿
Delay_us(5);
SCL_LOW(); // 钳住SCL准备发送数据
}
void I2C_Stop(void) {
SDA_LOW(); // 确保SDA为低
SCL_LOW();
Delay_us(5);
SCL_HIGH(); // 先拉高SCL
Delay_us(5);
SDA_HIGH(); // 再拉高SDA产生上升沿
Delay_us(5);
}
uint8_t I2C_WriteByte(uint8_t dat) {
for(uint8_t i = 0; i < 8; i++) {
(dat & 0x80) ? SDA_HIGH() : SDA_LOW();
dat <<= 1;
SCL_HIGH(); // 上升沿锁存数据
Delay_us(3);
SCL_LOW();
Delay_us(2);
}
// 接收ACK
SDA_HIGH(); // 释放SDA
GPIO_InitStruct.Mode = GPIO_MODE_INPUT; // 切换为输入模式
HAL_GPIO_Init(SDA_PORT, &GPIO_InitStruct);
SCL_HIGH();
Delay_us(2);
uint8_t ack = HAL_GPIO_ReadPin(SDA_PORT, SDA_PIN);
SCL_LOW();
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD; // 切回输出
HAL_GPIO_Init(SDA_PORT, &GPIO_InitStruct);
return ack; // 0:ACK received, 1:NACK
}
uint8_t I2C_ReadByte(uint8_t ack_flag) {
uint8_t dat = 0;
SDA_HIGH(); // 释放数据线
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
HAL_GPIO_Init(SDA_PORT, &GPIO_InitStruct);
for(int i=0; i<8; i++) {
dat <<= 1;
SCL_HIGH();
Delay_us(3);
if(HAL_GPIO_ReadPin(SDA_PORT, SDA_PIN))
dat |= 0x01;
SCL_LOW();
Delay_us(2);
}
// 发送ACK/NACK
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD;
HAL_GPIO_Init(SDA_PORT, &GPIO_InitStruct);
ack_flag ? SDA_HIGH() : SDA_LOW(); // 0:发送ACK, 1:发送NACK
SCL_HIGH();
Delay_us(2);
SCL_LOW();
return dat;
}
// 使用SysTick实现精准延时(STM32示例)
void Delay_us(uint32_t us) {
uint32_t ticks = us * (SystemCoreClock / 1000000);
uint32_t start = DWT->CYCCNT;
while((DWT->CYCCNT - start) < ticks);
}
参数 |
标准模式 |
快速模式 |
SCL时钟频率 |
≤100kHz |
≤400kHz |
起始条件保持时间 |
>4.0μs |
>0.6μs |
数据保持时间 |
>0μs |
>0μs |
SCL/SDA上升时间 |
<1000ns |
<300ns |
void I2C_Bus_Recover(void) {
// 1. 切换SDA为输出
// 2. 产生9个时钟脉冲
for(int i=0; i<9; i++) {
SCL_LOW();
Delay_us(5);
SCL_HIGH();
Delay_us(5);
}
// 3. 发送停止条件
I2C_Stop();
}
// 发送数据时持续监测SDA状态
void I2C_WriteByte_WithArbitration(uint8_t dat) {
for(int i=0; i<8; i++) {
// ... 正常发送流程
// 仲裁检测点
if((dat_bit != 0) && (SDA_READ() == 0)) {
// 检测到总线冲突
return ARBITRATION_LOST;
}
}
}
uint8_t AT24C02_Read(uint8_t addr) {
I2C_Start();
// 发送器件地址+写
if(I2C_WriteByte(0xA0)) {
I2C_Stop();
return 0xFF; // 无应答
}
// 发送内存地址
I2C_WriteByte(addr);
// 重复起始条件
I2C_Start();
// 发送器件地址+读
I2C_WriteByte(0xA1);
// 读取数据(最后发送NACK)
uint8_t data = I2C_ReadByte(1);
I2C_Stop();
return data;
}
循环展开:对关键位操作展开循环减少跳转
汇编优化:对时序关键路径使用内联汇编
中断处理:在SCL低电平期间处理中断避免时序错乱
DMA辅助:大数据传输时配合DMA减少CPU占用
无ACK响应
检查设备地址(7位地址通常左移1位)
确认从设备上电
测量SDA/SCL电压(应能被拉低)
数据错位
检查延时函数精度
确认SCL高低电平时间比例
用逻辑分析仪捕获实际波形
总线锁死
实现超时释放机制
加入总线恢复函数
终极调试工具:使用Saleae逻辑分析仪或DSView捕获实际波形,对照I2C时序图分析
软件I2C虽不如硬件高效(通常极限在400kbps左右),但其灵活性和可控性使其成为嵌入式开发不可或缺的技能。掌握本文内容后,你已具备:
从零实现软件I2C的能力
时序分析和调试技巧
解决复杂总线问题的思路
技术讨论:你在实现软件I2C时遇到过哪些有趣的问题?欢迎评论区分享!