软件 I2C 通信:从原理到代码实现的深度解析

本文手把手教你用GPIO口实现灵活可靠的I2C主设备,解决硬件I2C资源冲突问题,适用于所有嵌入式平台。

一、为什么需要软件I2C?

当遇到以下场景时,硬件I2C可能不再适用:

  • MCU硬件I2C外设数量不足

  • 特殊引脚映射需求

  • 硬件I2C存在已知BUG(如STM32早期版本的I2C缺陷)

  • 需要兼容非标准时序设备

软件I2C优势:

✅ 任意GPIO均可实现

✅ 时序完全可控

✅ 跨平台移植性强

✅ 避开了硬件设计缺陷

二、I2C协议核心原理回顾

2.1 物理层特性

特性

说明

信号线

SDA(数据线) + SCL(时钟线)

拓扑结构

总线结构,支持多主多从

上拉电阻

4.7kΩ典型值(3.3V系统)

传输速率

标准模式100kbps,快速模式400kbps

2.2 协议关键点

// 典型I2C帧结构
[start][7bit地址 + R/W][ACK][数据][ACK]...[stop]
  1. 起始条件(S):SCL高电平时SDA从高→低

  2. 停止条件(P):SCL高电平时SDA从低→高

  3. 数据有效性:SCL高电平期间SDA必须保持稳定

  4. ACK/NACK:第9个时钟周期接收方拉低SDA表示ACK

三、软件I2C实现详解

3.1 硬件连接示例

MCU.GPIO1  ----> SDA
MCU.GPIO2  ----> SCL
           ╔═ 4.7KΩ上拉电阻
           ╚---> VCC(3.3V/5V)

3.2 GPIO配置要点

// 初始化配置(以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);
}

3.3 关键时序函数实现

起始信号生成
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);
}

3.4 数据收发核心算法

发送一个字节(含ACK检查)
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
}
读取一个字节(含ACK发送)
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;
}

四、时序精准控制技巧

4.1 延时函数优化

// 使用SysTick实现精准延时(STM32示例)
void Delay_us(uint32_t us) {
  uint32_t ticks = us * (SystemCoreClock / 1000000);
  uint32_t start = DWT->CYCCNT;
  while((DWT->CYCCNT - start) < ticks);
}

4.2 关键时序参数

参数

标准模式

快速模式

SCL时钟频率

≤100kHz

≤400kHz

起始条件保持时间

>4.0μs

>0.6μs

数据保持时间

>0μs

>0μs

SCL/SDA上升时间

<1000ns

<300ns

五、高级功能实现

5.1 总线错误恢复

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();
}

5.2 多主机仲裁支持

// 发送数据时持续监测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;
    }
  }
}

六、实战:读取AT24C02 EEPROM

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;
}

七、性能优化建议

  1. 循环展开:对关键位操作展开循环减少跳转

  2. 汇编优化:对时序关键路径使用内联汇编

  3. 中断处理:在SCL低电平期间处理中断避免时序错乱

  4. DMA辅助:大数据传输时配合DMA减少CPU占用

八、常见问题排查

  1. 无ACK响应

    • 检查设备地址(7位地址通常左移1位)

    • 确认从设备上电

    • 测量SDA/SCL电压(应能被拉低)

  2. 数据错位

    • 检查延时函数精度

    • 确认SCL高低电平时间比例

    • 用逻辑分析仪捕获实际波形

  3. 总线锁死

    • 实现超时释放机制

    • 加入总线恢复函数

终极调试工具:使用Saleae逻辑分析仪或DSView捕获实际波形,对照I2C时序图分析

结语

软件I2C虽不如硬件高效(通常极限在400kbps左右),但其灵活性和可控性使其成为嵌入式开发不可或缺的技能。掌握本文内容后,你已具备:

  • 从零实现软件I2C的能力

  • 时序分析和调试技巧

  • 解决复杂总线问题的思路

技术讨论:你在实现软件I2C时遇到过哪些有趣的问题?欢迎评论区分享!

你可能感兴趣的:(单片机,嵌入式硬件,I2C)