
摘要:本文介绍在STM32上使用4个GPIO引脚软件模拟I²C通讯的完整方案,涵盖协议时序、主从机GPIO配置、收发实现与异常处理,适合无硬件I²C外设或需灵活控制时序的嵌入式开发者。关键词:STM32、GPIO模拟I2C、软件模拟I2C、主从机通信、I2C时序、嵌入式开发、GPIO配置、I2C通信协议目录前言1. I²C协议基础与GPIO配置1.1 I²C协议基本时序1.2 主机(Master)GPIO配置1.3 从机(Slave)GPIO配置2. 主机(Master)发送实现2.1 发送起始信号2.2 发送停止信号2.3 发送字节与应答检测2.3.1 I²C通信异常处理2.4 多字节连续读写示例3. 从机(Slave)接收实现3.1 接收起始信号3.2 接收字节与应答发送3.3 从机发送数据实现4. 实现要点总结与注意事项4.1 实现要点总结4.2 注意事项4.3 硬件I2C与软件模拟I2C对比总结参考资料在STM32单片机中,若没有可用的硬件I²C外设或需要灵活控制I²C通信,可以通过4个GPIO引脚模拟I²C通讯协议。该方法适用于主机(Master)和从机(Slave)的实现,但需注意其性能限制(如速度较低、依赖软件控制等)。前言I²C(Inter-Integrated Circuit)是一种广泛使用的串行通信协议,仅需两根信号线(SCL时钟线和SDA数据线)即可实现多设备间的数据交换。在STM32开发中,硬件I²C外设虽然使用方便,但在某些场景下(如引脚复用冲突、外设资源不足或需要灵活控制时序)并不适用。此时,通过GPIO软件模拟I²C成为一种实用且灵活的替代方案。本文将以STM32单片机为例,详细介绍如何仅使用4个GPIO引脚(主机和从机各占用SCL、SDA两根线)实现I²C通讯。文章将从I²C协议的基本时序出发,逐步讲解GPIO配置、主机发送逻辑、从机接收逻辑以及实现过程中的关键注意事项,帮助读者在无硬件I²C外设的条件下,也能稳定实现主从机之间的数据通信。1. I²C协议基础与GPIO配置1.1 I²C协议基本时序I²C协议的基本时序包括以下关键信号:信号描述起始信号 (Start Condition)SCL为高电平时,SDA由高变低停止信号 (Stop Condition)SCL为高电平时,SDA由低变高数据位SCL为高时,SDA保持稳定,表示数据位应答信号 (ACK/NACK)接收方在SCL高电平期间将SDA拉低表示ACK,否则为NACK为了更直观地理解上述时序,下面用 ASCII 字符绘制一个完整的 I²C 通信时序图,依次展示起始信号、8 个数据位(以 0xA5 为例)、应答信号和停止信号:SCL: ‾‾|__|‾‾|__|‾‾|__|‾‾|__|‾‾|__|‾‾|__|‾‾|__|‾‾|__|‾‾|__|‾‾|__|‾‾|__|‾‾ SDA: ‾‾‾‾\_/‾‾\_/‾‾‾‾\_/‾‾\_/‾‾\_/‾‾\_/‾‾‾‾\_/‾‾\_/‾‾\_/‾‾\_/‾‾‾‾‾‾‾‾‾‾‾‾‾‾ | | | | | | | | | | | | | | | | | | | | | | | | | | S D0 D1 D2 D3 D4 D5 D6 D7 ACK P T (1)(0)(1)(0)(0)(1)(0)(1) S A T R O T P 说明: START : SCL 为高时,SDA 由高变低(起始信号) D7~D0 : 数据位,SCL 为高时 SDA 保持稳定(此处示例发送 0xA5 = 1010 0101) ACK : 第 9 个时钟,接收方将 SDA 拉低表示应答 STOP : SCL 为高时,SDA 由低变高(停止信号)其中,起始信号(START)和停止信号(STOP)都发生在 SCL 为高电平期间,通过 SDA 的电平跳变来区分;数据位则在 SCL 为高电平时保持稳定,接收方在 SCL 高电平期间采样 SDA;应答信号(ACK)由接收方在第 9 个时钟周期内将 SDA 拉低产生。在软件模拟I²C时,延时参数直接决定了通信速率和时序是否满足协议要求。下表列出了标准模式(100kHz)和快速模式(400kHz)下SCL高/低电平的最小持续时间、起始/停止信号的建立时间等关键时序参数,并给出了Delay_us()的推荐取值范围,供编写延时函数时参考:时序参数标准模式(100kHz)快速模式(400kHz)说明SCL高电平最小持续时间4.0 µs0.6 µsSCL为高电平的最短保持时间,对应Delay_us()的延时下限SCL低电平最小持续时间4.7 µs1.3 µsSCL为低电平的最短保持时间,通常略长于高电平时间起始信号建立时间4.7 µs0.6 µs起始信号前SCL为高、SDA由高变低所需的建立时间起始信号保持时间4.0 µs0.6 µs起始信号后SCL首次拉低前,SDA低电平需保持的最短时间停止信号建立时间4.0 µs0.6 µs停止信号前SCL为高、SDA由低变高所需的建立时间数据建立时间250 ns100 nsSDA电平变化后到SCL拉高前需保持稳定的最短时间数据保持时间0 µs(最小)0 µs(最小)SCL拉低后SDA允许变化的最短等待时间推荐Delay_us()取值范围5 ~ 10 µs1 ~ 3 µs实际取值需结合CPU主频和GPIO翻转开销综合调整需要说明的是,上表中的最小持续时间是I²C协议规定的下限值。软件模拟时,Delay_us()的取值应略大于理论最小值,以留出一定的时序裕量,避免因GPIO翻转延迟、中断抢占等因素导致时序不达标。例如在标准模式下,将SCL高、低电平的延时均设为5 µs左右即可稳定工作;在快速模式下,则需将延时压缩到1 ~ 3 µs,并尽量关闭中断或使用高优先级任务来保证时序精度。除了上述 ASCII 时序图,下面再用 Mermaid 绘制一张 I²C 通信的完整流程图,从宏观视角展示一次典型读写操作中起始、地址、数据、应答与停止等关键步骤的先后关系:flowchart TD A[主机发起通信] -- B[发送起始信号 START SCL为高时 SDA 由高变低] B -- C[发送设备地址 + 读写标志 7位地址 + 1位 R/W] C -- D{从机是否应答 ACK?} D -- 否 -- E[从机无应答 NACK 主机发送停止信号结束] D -- 是 -- F{读写标志判断} F -- 写操作 R/W=0 -- G[主机发送数据字节 每字节后等待从机 ACK] G -- H{是否还有数据?} H -- 是 -- G H -- 否 -- I[主机发送停止信号 STOP SCL为高时 SDA 由低变高] F -- 读操作 R/W=1 -- J[从机发送数据字节 每字节后主机返回 ACK] J -- K{主机是否返回 NACK?} K -- 否 -- J K -- 是 -- I E -- L[通信结束] I -- L[通信结束]上图完整展示了 I²C 通信的宏观流程:主机首先发送起始信号,随后发送由 7 位设备地址和 1 位读写标志组成的地址字节;从机地址匹配后在第 9 个时钟周期拉低 SDA 返回 ACK。若为写操作,主机继续发送数据字节,每发送一个字节后等待从机应答;若为读操作,则由从机向主机发送数据,主机在收到倒数第二个字节后返回 ACK、收到最后一个字节后返回 NACK。无论读写,通信最终都以主机发送停止信号结束。结合前面的 ASCII 时序图,读者既能从微观层面看清每一位的电平变化,也能从宏观层面把握整个通信流程的先后顺序。1.2 主机(Master)GPIO配置主机作为通信的发起方,需要主动控制SCL时钟并驱动SDA数据线,因此SCL和SDA均应配置为推挽输出模式。具体初始化代码如下:// 定义I²C引脚 #define SCL_PIN GPIO_PIN_0 #define SDA_PIN GPIO_PIN_1 #define SCL_PORT GPIOA #define SDA_PORT GPIOA // 初始化GPIO为推挽输出模式 void I2C_GPIO_Init(void) { // 使能GPIO时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitTypeDef GPIO_InitStruct; // 配置SCL和SDA为推挽输出 GPIO_InitStruct.GPIO_Pin = SCL_PIN | SDA_PIN; GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(SCL_PORT, amp;GPIO_InitStruct); }1.3 从机(Slave)GPIO配置从机作为通信的接收方,需要监听总线上的起始信号并读取SDA上的数据,因此SCL应配置为输入模式以检测时钟,SDA也需配置为输入模式以接收数据。具体初始化代码如下:// 从机需要接收数据,因此SDA应配置为输入模式 void I2C_Slave_GPIO_Init(void) { // 使能GPIO时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitTypeDef GPIO_InitStruct; // 配置SCL为输入,用于检测主机时钟 GPIO_InitStruct.GPIO_Pin = SCL_PIN; GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(SCL_PORT, amp;GPIO_InitStruct); // 配置SDA为输入,用于接收数据 GPIO_InitStruct.GPIO_Pin = SDA_PIN; GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(SDA_PORT, amp;GPIO_InitStruct); }2. 主机(Master)发送实现2.1 发送起始信号起始信号是主机发起通信的标志。其实现逻辑为:在SCL为高电平期间,将SDA从高电平拉低,随后将SCL拉低以准备传输数据。具体代码如下:void I2C_Start(void) { // SDA为高电平,SCL为高电平 GPIO_SetBits(SDA_PORT, SDA_PIN); GPIO_SetBits(SCL_PORT, SCL_PIN); // SDA拉低,SCL保持高电平 GPIO_ResetBits(SDA_PORT, SDA_PIN); Delay_us(5); // 等待时间根据实际需求调整 // SCL拉低 GPIO_ResetBits(SCL_PORT, SCL_PIN); }2.2 发送停止信号停止信号用于结束一次通信。其实现逻辑为:在SCL为高电平期间,将SDA从低电平拉高。具体代码如下:void I2C_Stop(void) { // SCL为低电平 GPIO_ResetBits(SCL_PORT, SCL_PIN); // SDA拉低 GPIO_ResetBits(SDA_PORT, SDA_PIN); Delay_us(5); // SCL拉高,SDA拉高 GPIO_SetBits(SCL_PORT, SCL_PIN); GPIO_SetBits(SDA_PORT, SDA_PIN); Delay_us(5); }2.3 发送字节与应答检测发送字节是主机通信的核心操作。主机需将8位数据按从高位到低位的顺序逐位发送,并在发送完成后释放SDA以检测从机的应答信号。具体代码如下:void I2C_SendByte(uint8_t data) { for (int i = 0; i 8; i++) { // 根据数据位设置SDA if (data 0x80) { GPIO_SetBits(SDA_PORT, SDA_PIN); } else { GPIO_ResetBits(SDA_PORT, SDA_PIN); } // SCL拉高,发送数据位 GPIO_SetBits(SCL_PORT, SCL_PIN); Delay_us(5); // SCL拉低 GPIO_ResetBits(SCL_PORT, SCL_PIN); Delay_us(5); // 移动数据位 data amp;lt;amp;lt;= 1; } // 检查应答 GP