news 2026/3/26 22:24:21

基于STM32F103的模拟I2C实现:手把手教程(零基础适用)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于STM32F103的模拟I2C实现:手把手教程(零基础适用)

玩转任意引脚的I2C通信:在STM32F103上从零实现软件模拟I2C

你有没有遇到过这样的情况?项目里要用好几个I2C传感器——一个温湿度、一个气压计、再来个EEPROM存配置。结果发现,你的STM32F103只有两个硬件I2C接口,还被串口调试和触摸芯片占了?或者某个传感器死活不回应ACK,示波器一抓才发现时序对不上……

这时候,别急着换主控或加I2C多路复用器。有一种更灵活、更可控、甚至更适合学习底层原理的方案——软件模拟I2C(Bit-Banging I2C)

今天我们就以STM32F103为例,手把手带你从GPIO操作开始,一步步构建一套稳定可靠的模拟I2C驱动。不仅讲清楚“怎么写”,更要让你明白“为什么这么写”。


为什么需要模拟I2C?硬件不够香吗?

先泼一盆冷水:硬件I2C确实高效省资源,但现实开发中它并不总是“即插即用”的完美选择。

硬件I2C的真实痛点

  • 资源有限:STM32F103系列通常只提供I2C1和I2C2,其中I2C1的默认引脚是PB6/PB7,常与调试接口冲突;
  • 兼容性问题频发:某些国产传感器对SCL低电平时间要求苛刻,标准库函数容易因中断打断导致超时;
  • 总线锁死无解:一旦SDA被拉低卡住,硬件模块往往无法恢复,只能靠外部复位;
  • 引脚固定不可变:你想用PA9/PA10做I2C?抱歉,除非重映射,否则不行。

模拟I2C恰恰能绕开这些坑:

✅ 可用任意GPIO
✅ 完全掌控时序细节
✅ 出错后可主动恢复(比如发送9个时钟脉冲唤醒设备)
✅ 不依赖特定外设,移植性强

当然,代价也很明显:CPU占用率高,不适合高频通信或实时系统。但对于大多数传感器应用(100kHz足矣),这点开销完全可以接受。


I2C协议精要:5步走通整个通信流程

在动手前,我们必须搞懂I2C协议的核心机制。记住一句话:所有操作都是围绕SCL和SDA的状态变化展开的

半双工同步串行通信的本质

I2C使用两条线:
-SCL:由主机驱动的时钟线
-SDA:双向数据线,支持多设备挂载

它的通信像一场“对话”:
1. 主机说:“大家注意!” → 起始信号
2. 主机喊名字:“DS1307出来!” → 发送地址 + 写标志
3. DS1307答:“到!” → 拉低SDA表示ACK
4. 主机传指令:“读第3寄存器” → 数据传输
5. 最后说:“散会!” → 停止信号

这五个关键动作构成了每一次I2C交互的基础。

关键信号时序图解

信号条件说明
起始 (Start)SCL=H, SDA从H→L标志一次通信开始
停止 (Stop)SCL=H, SDA从L→H标志一次通信结束
数据有效窗口SCL=L期间更新SDA数据必须在SCL上升前稳定
采样点SCL=H时读取SDA接收方在此刻读取数据

特别注意:SDA只能在SCL为低时改变状态,否则会被误判为Start/Stop!


STM32F103上的GPIO魔法:如何让普通IO变成I2C总线

现在我们把目光转向MCU本身。STM32F103的强大之处在于其灵活的GPIO控制能力,尤其是BSRR/BRR寄存器,可以单周期置位或清零引脚,这对精确时序至关重要。

为什么SDA必须配置为开漏输出?

I2C总线采用OD(Open Drain)结构,配合外部上拉电阻工作。好处是:
- 多设备共享总线不会短路(谁想说话就拉低,不想就说“放手”)
- 支持双向通信:主机发完数据后,释放SDA让从机拉低回ACK

所以我们这样配置:

// SCL: 推挽输出即可(仅主机驱动) GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; // 推挽 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; // SDA: 必须开漏!因为它要切换输入模式读ACK GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD; // 开漏

🔧 小贴士:实际布板时务必加上拉电阻(推荐4.7kΩ),VDD=3.3V或5V视设备而定。


核心代码实现:逐行拆解模拟I2C驱动

下面是最关键的部分。我们将用最基础的方式实现每一个通信环节,并解释每一行背后的逻辑。

宏定义简化操作

#define I2C_PORT GPIOB #define I2C_SCL_PIN GPIO_Pin_6 #define I2C_SDA_PIN GPIO_Pin_7 // 利用BSRR/BRR实现原子操作,避免读-改-写风险 #define SCL_H() I2C_PORT->BSRR = I2C_SCL_PIN // Set Pin High #define SCL_L() I2C_PORT->BRR = I2C_SCL_PIN // Reset Pin Low #define SDA_H() I2C_PORT->BSRR = I2C_SDA_PIN #define SDA_L() I2C_PORT->BRR = I2C_SDA_PIN // 读取SDA电平状态 #define READ_SDA() ((I2C_PORT->IDR & I2C_SDA_PIN) != 0)

⚠️ 注意:不能用GPIO_WriteBit()这类函数,它们效率太低,可能破坏微秒级时序。


微秒级延时函数设计

目标速率:100kHz→ 每bit约10μs,高低各5μs。

static void I2C_Delay(void) { uint32_t i = 70; // 经实测,在72MHz下约为5μs while (i--); }

📌 提示:你可以用DWT Cycle Counter来获得更高精度,但在简单应用中循环延时已足够。


起始与停止信号生成

void Soft_I2C_Start(void) { SDA_H(); SCL_H(); // 确保空闲状态 I2C_Delay(); SDA_L(); // 在SCL高时拉低SDA → Start! I2C_Delay(); SCL_L(); // 拉低SCL,准备发送数据 }
void Soft_I2C_Stop(void) { SCL_L(); SDA_L(); // 先拉低两者 I2C_Delay(); SCL_H(); // 先升SCL I2C_Delay(); SDA_H(); // 再升SDA → Stop! I2C_Delay(); }

✅ 关键点:Stop必须是SCL高时SDA从低变高,顺序不能错!


发送一个字节并等待ACK

uint8_t Soft_I2C_SendByte(uint8_t byte) { uint8_t i; for(i = 0; i < 8; i++) { if(byte & 0x80) { SDA_H(); // 数据位为1 } else { SDA_L(); // 数据位为0 } I2C_Delay(); SCL_H(); // 上升沿,从机采样 I2C_Delay(); SCL_L(); // 下降沿,允许主机改变数据 I2C_Delay(); byte <<= 1; // 左移一位,准备下一位 } // === 读取ACK === SDA_H(); // 释放SDA,让从机控制 I2C_Delay(); // 切换SDA为输入模式(上拉输入) GPIO_InitTypeDef cfg; cfg.GPIO_Pin = I2C_SDA_PIN; cfg.GPIO_Mode = GPIO_Mode_IPU; // 上拉输入 GPIO_Init(I2C_PORT, &cfg); SCL_H(); // 第9个时钟,读ACK I2C_Delay(); uint8_t ack = !READ_SDA(); // 若SDA为低,则收到ACK SCL_L(); // 恢复SDA为开漏输出 cfg.GPIO_Mode = GPIO_Mode_Out_OD; GPIO_Init(I2C_PORT, &cfg); return ack; }

🧠 思考点:为什么要临时切换输入模式?因为只有这样才能检测到从机是否拉低了ACK。


接收一个字节(支持NACK)

uint8_t Soft_I2C_ReadByte(uint8_t ack) { uint8_t i, data = 0; SDA_H(); // 释放总线 GPIO_InitTypeDef cfg; cfg.GPIO_Pin = I2C_SDA_PIN; cfg.GPIO_Mode = GPIO_Mode_IPU; GPIO_Init(I2C_PORT, &cfg); for(i = 0; i < 8; i++) { I2C_Delay(); SCL_H(); // 上升沿采样 I2C_Delay(); data = (data << 1) | READ_SDA(); SCL_L(); } // === 发送ACK/NACK === cfg.GPIO_Mode = GPIO_Mode_Out_OD; GPIO_Init(I2C_PORT, &cfg); if(ack) { SDA_L(); // ACK: 拉低表示继续接收 } else { SDA_H(); // NACK: 释放表示结束 } I2C_Delay(); SCL_H(); // 第9个时钟 I2C_Delay(); SCL_L(); SDA_H(); // 释放SDA return data; }

🎯 应用场景:最后一个字节通常发NACK,通知从机停止发送。


实战案例:向AT24C02 EEPROM写入一字节

假设我们要把数据0x55写入地址0x00

void AT24C02_Write_Byte(uint8_t addr, uint8_t data) { Soft_I2C_Start(); Soft_I2C_SendByte(0xA0); // 写设备地址 Soft_I2C_SendByte(addr); // 内部地址 Soft_I2C_SendByte(data); // 要写的数据 Soft_I2C_Stop(); Delay_ms(10); // 等待内部写周期完成(最大10ms) }

💡 注意:每次写操作后必须延时,否则下次读可能失败!


高级技巧与避坑指南

别以为写了就能跑通。以下是我在真实项目中踩过的坑和解决方案。

❌ 坑点1:ACK始终收不到?

常见原因:
- 上拉电阻缺失或阻值过大(>10kΩ)
- 电源未共地或电压不匹配
- 地址错误(注意有些芯片左移7位后再加R/W)

🔧 秘籍:用示波器看SDA波形,确认是否真有设备拉低ACK。


❌ 坑点2:偶尔通信失败?

可能是中断干扰了时序!

✅ 解决方法:在关键段禁用全局中断:

__disable_irq(); Soft_I2C_Start(); // ... 发送过程 ... Soft_I2C_Stop(); __enable_irq();

⚠️ 注意:时间越短越好,避免影响其他中断响应。


❌ 坑点3:总线被锁死(SDA一直低)?

某些设备掉电或异常会导致SDA拉死。

✅ 恢复大法:强制发送9个SCL脉冲尝试唤醒:

void I2C_Recover_Bus(void) { for(int i = 0; i < 9; i++) { SCL_L(); Delay_us(5); SCL_H(); Delay_us(5); } Soft_I2C_Stop(); // 尝试补一个Stop }

✅ 最佳实践清单

项目建议
上拉电阻4.7kΩ,靠近MCU端放置
通信速率初始调试建议设为50kHz,稳定后再提频
引脚选择尽量选同一端口(如都用GPIOB),减少初始化开销
电源管理所有I2C设备共地,跨压需用电平转换器
调试手段示波器抓波形 > 打日志 > 猜问题

扩展思路:不止于“替代”,还能做得更多

模拟I2C不只是“备胎”。正因为它是软件实现,反而带来了更多可能性:

🔄 多组I2C总线轻松扩展

// 第二组I2C使用PC0/PC1 #define I2C2_SCL_H() GPIOC->BSRR = GPIO_Pin_0 #define I2C2_SDA_H() GPIOC->BSRR = GPIO_Pin_1 // ... 同样实现一套函数,命名加2即可

无需任何硬件改动,就能接入更多设备。


🛠 自定义时序适配特殊器件

某些老旧EEPROM要求t_SU:DAT ≥ 1μs,标准库可能达不到。但在模拟I2C中:

// 加长建立时间 I2C_Delay_Long(); // 延时1μs以上再升SCL SCL_H();

完全自主掌控。


📈 结合RTOS实现总线互斥

在FreeRTOS中可用信号量保护总线访问:

SemaphoreHandle_t i2c_mutex; void task_sensor_read(void *pv) { xSemaphoreTake(i2c_mutex, portMAX_DELAY); read_bmp180(); xSemaphoreGive(i2c_mutex); }

防止多个任务同时操作造成混乱。


写在最后:理解比调用更重要

当你熟练掌握了模拟I2C,你会发现:
- 硬件I2C不再神秘;
- 遇到通信故障时,你能快速定位是时序、电平还是协议问题;
- 你开始关注数据手册中的时序参数表,而不是只看寄存器说明。

这正是嵌入式工程师成长的关键一步。

“授人以鱼不如授人以渔。”
模拟I2C不是为了取代硬件,而是为了让我们真正掌握通信的本质

如果你正在学习STM32,不妨亲手写一遍这套代码。哪怕最终换成硬件I2C,这段经历也会让你受益无穷。


💬互动话题:你在项目中用过模拟I2C吗?遇到过哪些奇葩问题?欢迎留言分享你的“排坑日记”!

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/3/24 12:29:27

通义千问2.5-7B-Instruct代码生成实战:媲美CodeLlama-34B的部署案例

通义千问2.5-7B-Instruct代码生成实战&#xff1a;媲美CodeLlama-34B的部署案例 1. 技术背景与选型动机 在当前大模型快速迭代的背景下&#xff0c;开发者对高性能、低成本、易部署的开源模型需求日益增长。尽管34B以上的大模型&#xff08;如CodeLlama-34B&#xff09;在代码…

作者头像 李华
网站建设 2026/3/25 0:20:51

74194四位移位寄存器仿真实践:从电路搭建到结果分析

74194四位移位寄存器仿真实践&#xff1a;从电路搭建到波形分析在数字电路的学习旅程中&#xff0c;总有一些芯片像“老朋友”一样反复出现——它们结构清晰、功能典型&#xff0c;既能用于教学演示&#xff0c;也能在实际项目中派上用场。74194四位双向移位寄存器正是这样一款…

作者头像 李华
网站建设 2026/3/20 8:14:13

跨语言检索怎么做?BAAI/bge-m3实战案例详细步骤分享

跨语言检索怎么做&#xff1f;BAAI/bge-m3实战案例详细步骤分享 1. 引言&#xff1a;跨语言语义理解的技术挑战与突破 在构建全球化AI应用的过程中&#xff0c;如何让模型真正“理解”不同语言之间的语义关联&#xff0c;是自然语言处理领域长期面临的挑战。传统方法依赖翻译…

作者头像 李华
网站建设 2026/3/15 2:00:38

AI视频编辑新突破:智能字幕去除技术深度解析

AI视频编辑新突破&#xff1a;智能字幕去除技术深度解析 【免费下载链接】video-subtitle-remover 基于AI的图片/视频硬字幕去除、文本水印去除&#xff0c;无损分辨率生成去字幕、去水印后的图片/视频文件。无需申请第三方API&#xff0c;本地实现。AI-based tool for removin…

作者头像 李华
网站建设 2026/3/24 2:37:53

WeMod专业版完整解锁指南:零成本获取高级功能特权

WeMod专业版完整解锁指南&#xff1a;零成本获取高级功能特权 【免费下载链接】Wemod-Patcher WeMod patcher allows you to get some WeMod Pro features absolutely free 项目地址: https://gitcode.com/gh_mirrors/we/Wemod-Patcher 还在为WeMod专业版的高额订阅费用…

作者头像 李华
网站建设 2026/3/12 9:03:14

AMD Ryzen终极性能调优指南:5大核心模块深度挖掘硬件潜力

AMD Ryzen终极性能调优指南&#xff1a;5大核心模块深度挖掘硬件潜力 【免费下载链接】SMUDebugTool A dedicated tool to help write/read various parameters of Ryzen-based systems, such as manual overclock, SMU, PCI, CPUID, MSR and Power Table. 项目地址: https:/…

作者头像 李华