通过代码直观看到软件 I2C 和硬件 I2C 的区别,这个思路特别好!我以 STM32F103 为例,分别给出驱动同一个 I2C 设备(比如温湿度传感器)的核心代码,你对比着看就能立刻明白差异。
一、软件 I2C 核心代码(模拟时序)
软件 I2C 完全用 GPIO 口模拟 SCL/SDA 的高低电平变化,所有时序都靠代码手动控制:
#include "stm32f10x.h" // 定义软件I2C引脚(可任意选GPIO) #define I2C_SCL_PIN GPIO_Pin_0 #define I2C_SDA_PIN GPIO_Pin_1 #define I2C_GPIO_PORT GPIOB #define I2C_GPIO_CLK RCC_APB2Periph_GPIOB // 引脚电平控制(核心:手动改电平) #define SCL_HIGH() GPIO_SetBits(I2C_GPIO_PORT, I2C_SCL_PIN) #define SCL_LOW() GPIO_ResetBits(I2C_GPIO_PORT, I2C_SCL_PIN) #define SDA_HIGH() GPIO_SetBits(I2C_GPIO_PORT, I2C_SDA_PIN) #define SDA_LOW() GPIO_ResetBits(I2C_GPIO_PORT, I2C_SDA_PIN) #define SDA_READ() GPIO_ReadInputDataBit(I2C_GPIO_PORT, I2C_SDA_PIN) // 软件I2C初始化(就是配置GPIO为推挽输出) void Soft_I2C_Init(void) { GPIO_InitTypeDef GPIO_InitStruct; RCC_APB2PeriphClockCmd(I2C_GPIO_CLK, ENABLE); // SCL引脚配置 GPIO_InitStruct.GPIO_Pin = I2C_SCL_PIN; GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(I2C_GPIO_PORT, &GPIO_InitStruct); // SDA引脚配置 GPIO_InitStruct.GPIO_Pin = I2C_SDA_PIN; GPIO_Init(I2C_GPIO_PORT, &GPIO_InitStruct); // 初始电平拉高 SCL_HIGH(); SDA_HIGH(); } // 软件I2C起始信号(手动模拟时序) void Soft_I2C_Start(void) { SDA_HIGH(); SCL_HIGH(); delay_us(2); // 必须加延时保证时序,否则通信失败 SDA_LOW(); delay_us(2); SCL_LOW(); } // 软件I2C发送一个字节(每一位都要手动控制时钟) uint8_t Soft_I2C_Send_Byte(uint8_t data) { uint8_t i; for(i=0; i<8; i++) // 逐位发送 { if(data & 0x80) SDA_HIGH(); // 最高位先发送 else SDA_LOW(); data <<= 1; SCL_HIGH(); // 时钟拉高,从机读取数据 delay_us(2); SCL_LOW(); // 时钟拉低,准备下一位 delay_us(2); } // 等待从机应答 SDA_HIGH(); SCL_HIGH(); delay_us(2); uint8_t ack = SDA_READ(); SCL_LOW(); return ack; } // 其他停止、接收字节等函数都是类似的手动时序控制...二、硬件 I2C 核心代码(外设自动处理)
硬件 I2C 只需要配置寄存器,剩下的时序由硬件自动完成:
#include "stm32f10x.h" #define I2Cx I2C1 #define I2C_CLK RCC_APB1Periph_I2C1 #define I2C_GPIO_CLK RCC_APB2Periph_GPIOB #define I2C_SCL_PIN GPIO_Pin_6 #define I2C_SDA_PIN GPIO_Pin_7 // 硬件I2C初始化(配置外设参数,引脚是固定的) void Hard_I2C_Init(void) { GPIO_InitTypeDef GPIO_InitStruct; I2C_InitTypeDef I2C_InitStruct; // 1. 使能时钟 RCC_APB1PeriphClockCmd(I2C_CLK, ENABLE); RCC_APB2PeriphClockCmd(I2C_GPIO_CLK, ENABLE); // 2. 配置引脚为复用开漏输出(硬件I2C必须用固定引脚+复用模式) GPIO_InitStruct.GPIO_Pin = I2C_SCL_PIN | I2C_SDA_PIN; GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_OD; // 复用开漏 GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStruct); // 3. 配置I2C外设参数 I2C_DeInit(I2Cx); I2C_InitStruct.I2C_Mode = I2C_Mode_I2C; I2C_InitStruct.I2C_DutyCycle = I2C_DutyCycle_2; I2C_InitStruct.I2C_OwnAddress1 = 0x00; // 主机模式,自己的地址随便设 I2C_InitStruct.I2C_Ack = I2C_Ack_Enable; I2C_InitStruct.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit; I2C_InitStruct.I2C_ClockSpeed = 400000; // 400KHz高速模式 I2C_Cmd(I2Cx, ENABLE); I2C_Init(I2Cx, &I2C_InitStruct); } // 硬件I2C发送一个字节(调用外设函数,不用管时序) void Hard_I2C_Send_Byte(uint8_t addr, uint8_t data) { // 1. 等待I2C总线空闲 while(I2C_GetFlagStatus(I2Cx, I2C_FLAG_BUSY)); // 2. 发送起始信号(硬件自动生成时序) I2C_GenerateSTART(I2Cx, ENABLE); while(!I2C_CheckEvent(I2Cx, I2C_EVENT_MASTER_MODE_SELECT)); // 3. 发送设备地址(硬件自动处理时钟和应答) I2C_Send7bitAddress(I2Cx, addr, I2C_Direction_Transmitter); while(!I2C_CheckEvent(I2Cx, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)); // 4. 发送数据(硬件自动移位,不用逐位控制) I2C_SendData(I2Cx, data); while(!I2C_CheckEvent(I2Cx, I2C_EVENT_MASTER_BYTE_TRANSMITTED)); // 5. 发送停止信号 I2C_GenerateSTOP(I2Cx, ENABLE); }三、核心区别一眼看明白
| 对比项 | 软件 I2C | 硬件 I2C |
|---|---|---|
| 引脚控制 | 任意 GPIO,手动改高低电平 | 固定引脚,复用模式,硬件自动控制 |
| 时序实现 | 靠 delay_us () 和循环逐位模拟 | 硬件逻辑自动生成,无需手动延时 |
| CPU 占用 | 高(全程要 CPU 盯着) | 低(配置完 CPU 可干别的) |
| 代码复杂度 | 简单(直接操作 GPIO) | 稍复杂(配置外设寄存器) |
| 灵活性 | 极高(可自定义时序) | 低(引脚 / 时序固定) |
总结
- 软件 I2C:像你手动掰开关控制电路,代码直观、引脚随便选,但要自己控制每一步时序,CPU 不能干别的;
- 硬件 I2C:像你给机器人下指令,只需要告诉它 “发什么数据”,剩下的由硬件自动完成,CPU 解放出来,但必须用固定引脚;
- 新手做简单项目(比如读单个传感器)选软件 I2C 更易上手,需要高频通信 / 多设备时选硬件 I2C 更稳定。