模拟I2C读写流程系统学习:入门篇

从零实现模拟I2C:一位嵌入式工程师的实战手记

你有没有遇到过这样的场景?
项目进入关键阶段,突然发现MCU上唯一的硬件I2C接口已经被OLED屏幕占用,而你现在还要接一个温湿度传感器——偏偏它的地址还和另一个设备冲突。怎么办?

这时候,模拟I2C就成了你的“救命稻草”。

它不像硬件I2C那样依赖特定外设模块,而是用最朴素的方式:两个GPIO口 + 精确延时,手动复现整个通信流程。听起来像是“土法炼钢”,但在真实工程中,这招往往比高端方案更管用。

今天,我就带你一步步走完这个过程——不讲虚的,只说实战中踩过的坑、调过的波形、写过的代码。让你不仅能看懂,更能亲手实现一套稳定可靠的软件I2C驱动


为什么我们需要“模拟”I2C?

I2C协议本身很简单:一根时钟线(SCL),一根数据线(SDA),支持多主多从、地址寻址、应答机制。许多MCU都集成了硬件I2C控制器,配置几个寄存器就能通信。

但现实总是复杂的:

  • 引脚不够用?STM32F103C8T6只有I2C1可用,其他都是重映射;
  • 地址撞车了?MPU6050默认地址0x68,两个没法共存一条总线;
  • 电压不匹配?想让3.3V的ESP32读5V的EEPROM;
  • 调试抓瞎?硬件I2C一出错就是“忙等超时”,根本不知道哪一步失败了。

这些问题,模拟I2C全都能解

因为它的一切都在你的掌控之中:你可以把SCL和SDA接到任意GPIO上,可以为不同设备创建独立的虚拟总线,可以在每一步插入日志打印,甚至能用LED闪烁来“肉眼观察”ACK是否到来。

更重要的是,它是理解底层通信本质的最佳入口。当你亲手拉低SDA、等待SCL上升沿的时候,才会真正明白什么叫“数据在时钟上升沿被采样”。


核心三要素:电平控制、方向切换、精准延时

要让两条普通IO线变成I2C总线,必须搞定三个关键技术点。

1. GPIO操作:不只是高低电平那么简单

很多人以为,只要能设置高低电平就完事了。其实不然。

I2C的数据线SDA是双向的——主机发数据时是输出,接收ACK时又要变成输入去读从机的回应。

这就要求我们动态切换GPIO的方向。以STM32为例,典型宏定义如下:

#define SDA_OUT() { GPIOB->CRH &= ~0xF0; GPIOB->CRH |= 0x30; } // 推挽输出 #define SDA_IN() { GPIOB->CRH &= ~0xF0; GPIOB->CRH |= 0x80; } // 浮空输入 #define SET_SDA GPIOB->BSRR = GPIO_PIN_7 #define CLR_SDA GPIOB->BRR = GPIO_PIN_7 #define READ_SDA ((GPIOB->IDR & GPIO_PIN_7) != 0) #define SET_SCL GPIOB->BSRR = GPIO_PIN_6 #define CLR_SCL GPIOB->BRR = GPIO_PIN_6

注意这里的细节:
- 输出模式推荐使用开漏输出配合外部上拉电阻,这样才符合I2C电气规范;
- 输入模式要用浮空输入,避免内部上下拉干扰总线;
- 操作寄存器直接到位带,效率远高于库函数调用。

如果你的平台支持,最好启用开漏模式:

// 开漏输出配置(更贴近真实I2C) #define SDA_OUT_OD() do { \ GPIOB->CRH &= ~0xF0; \ GPIOB->CRH |= 0x10; /* CNF=01, MODE=10 -> 开漏输出 */ \ } while(0)

没有外部上拉?别急着通电!至少4.7kΩ的上拉电阻是必须的,否则总线永远拉不上去。

2. 延时函数:决定成败的关键

I2C的标准模式是100kHz,意味着每个时钟周期10μs,高/低电平各占约5μs。快速模式400kHz则压缩到2.5μs以内。

这些时间不是随便给的。比如:
- 数据建立时间(t_SU:DAT)需 ≥ 250ns;
- 起始信号建立时间(t_SU:STA)需 ≥ 4.7μs;
- 停止信号前SCL高电平时间(t_SU:STO)需 ≥ 4.0μs。

稍有偏差,某些“娇气”的传感器就会罢工。

所以延时不能靠for(i=0;i<100;i++);这种玄学写法。我推荐两种实用方案:

方案一:基于DWT周期计数(适用于Cortex-M3/M4)
static void i2c_delay_us(uint32_t us) { uint32_t start = DWT->CYCCNT; uint32_t cycles = us * (SystemCoreClock / 1000000); while ((DWT->CYCCNT - start) < cycles); }

前提是开启DWT时钟:

CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; DWT->CYCCNT = 0;
方案二:查表标定循环次数(适合资源紧张场景)
static void i2c_delay(void) { volatile int i = 72; // 经实测@72MHz ≈ 5μs while (i--) __NOP(); }

记得加volatile防止编译器优化掉空循环。


四步走通核心流程:起始 → 发地址 → 收ACK → 传数据

所有的I2C通信,归根结底就是四个动作的组合。我们逐个拆解。

第一步:发起通信 —— 起始条件(Start Condition)

规则很明确:当SCL为高时,SDA由高变低。

代码实现要注意顺序:

void i2c_start(void) { SDA_OUT(); // 确保SDA可输出 SET_SDA; // 初始高电平 SET_SCL; i2c_delay_us(4); // 保持≥4.7μs CLR_SDA; // SDA下降,此时SCL仍高 i2c_delay_us(4); CLR_SCL; // 拉低SCL,准备发送数据 }

关键点:
- 必须先保证SCL为高;
- SDA下降后要延时足够时间满足t_SU:STA;
- 最后拉低SCL进入数据传输阶段。

如果这里出错,示波器上看就是没有明显的“凹口”起始信号。

第二步:发送设备地址 + R/W位

所有I2C通信都以一个字节开始:7位地址左移一位,最低位表示读(1)或写(0)。

例如,AHT20地址为0x38,则写操作发送0x70,读操作发送0x71

uint8_t i2c_send_address(uint8_t addr, uint8_t rw) { return i2c_write_byte((addr << 1) | rw); }

其中i2c_write_byte按位发送:

uint8_t i2c_write_byte(uint8_t byte) { uint8_t i, ack; for (i = 0; i < 8; i++) { if (byte & 0x80) SET_SDA; else CLR_SDA; i2c_delay_us(1); SET_SCL; // 上升沿采样 i2c_delay_us(4); CLR_SCL; i2c_delay_us(1); byte <<= 1; } // 读取ACK SDA_IN(); i2c_delay_us(2); SET_SCL; i2c_delay_us(3); ack = !READ_SDA(); // 低电平为ACK CLR_SCL; SDA_OUT(); return ack; // 返回1表示收到ACK }

重点来了:
- 数据必须在SCL低电平时改变;
- SCL上升沿后保持一段时间供从机采样;
- 发完8位后释放SDA,切换为输入读ACK;
-ACK是低电平有效,即从机拉低才算确认。

如果你发现始终收不到ACK,先检查:
- 地址是否正确?
- 从机是否上电?
- 上拉电阻是否焊好?
- SDA是否被意外锁死?

第三步:读取数据并回应ACK/NACK

读操作稍微复杂一点。主机要在每个字节结束后主动告诉从机:“我还想要”或“到此为止”。

uint8_t i2c_read_byte(uint8_t ack) { uint8_t i, byte = 0; SDA_IN(); for (i = 0; i < 8; i++) { i2c_delay_us(1); SET_SCL; i2c_delay_us(2); byte <<= 1; if (READ_SDA()) byte |= 0x01; CLR_SCL; i2c_delay_us(1); } // 发送ACK/NACK SDA_OUT(); if (ack) CLR_SDA; // ACK: 拉低 else SET_SDA; // NACK: 保持高 i2c_delay_us(1); SET_SCL; i2c_delay_us(3); CLR_SCL; return byte; }

最后一字节通常发NACK,提醒从机停止发送。

第四步:结束通信 —— 停止条件(Stop Condition)

SCL高电平时,SDA由低变高。

void i2c_stop(void) { SDA_OUT(); CLR_SDA; SET_SCL; i2c_delay_us(4); SET_SDA; // SDA上升 i2c_delay_us(4); }

至此,一次完整的通信完成。


实战案例:读取AHT20温湿度传感器

我们来做一个完整例子,验证上面写的驱动能不能跑通。

AHT20通信流程

  1. Start
  2. 发送写地址(0x70)
  3. 收到ACK
  4. 发送命令0xE1(初始化)
  5. 发送参数0x08, 0x00
  6. Stop
  7. Delay 10ms
  8. Start
  9. 发送读地址(0x71)
  10. 连续读6字节
  11. 主机对前5字节回ACK,第6字节回NACK
  12. Stop

封装成函数:

uint8_t read_aht20(float *temp, float *humi) { uint8_t data[6]; uint32_t raw_humi, raw_temp; i2c_start(); if (!i2c_send_address(0x38, 0)) goto fail; i2c_write_byte(0xE1); i2c_write_byte(0x08); i2c_write_byte(0x00); i2c_stop(); delay_ms(10); i2c_start(); if (!i2c_send_address(0x38, 1)) goto fail; data[0] = i2c_read_byte(1); data[1] = i2c_read_byte(1); data[2] = i2c_read_byte(1); data[3] = i2c_read_byte(1); data[4] = i2c_read_byte(1); data[5] = i2c_read_byte(0); // 最后一个NACK i2c_stop(); raw_humi = ((uint32_t)data[1] << 12) | ((uint32_t)data[2] << 4) | (data[3] >> 4); raw_temp = (((uint32_t)data[3] & 0x0F) << 16) | ((uint32_t)data[4] << 8) | data[5]; *humi = (float)raw_humi / 1048576.0f * 100.0f; *temp = (float)raw_temp / 1048576.0f * 200.0f - 50.0f; return 1; fail: i2c_stop(); return 0; }

一次调用,返回温度和湿度值。简单粗暴,但非常可靠。


那些年我们踩过的坑:调试经验分享

别以为写了代码就能一次成功。以下是我亲身经历的问题与解决方案:

❌ 问题1:始终收不到ACK

最常见的原因:
-地址错了:有些芯片文档写的是7位地址,但实际应用要左移一位;
-SDA被卡住:GPIO没配置成输入,或者外部电路短路;
-上拉电阻缺失或过大:用万用表测一下SDA空闲时是不是高电平;
-电源未接稳:从机没工作,自然不会响应。

👉 解决方法:用逻辑分析仪抓波形,看是否有ACK脉冲。

❌ 问题2:读出来的数据全是0xFF或0x00

说明通信建立了,但数据不对。
- 可能是字节顺序搞反了,高位先行还是低位先行;
- 或者采样时机错误,应该在SCL上升沿后立即读,而不是下降沿;
- 也可能是延时太短,从机还没准备好数据。

👉 加大延时试试,特别是SCL上升沿后的等待时间。

❌ 问题3:偶尔通信失败

多半是中断打断了时序。比如SysTick中断打断了延时循环,导致某个高电平持续时间不足。

👉 解决办法:
- 在关键段禁用中断(慎用);
- 使用DWT这类不受中断影响的延时;
- 加入超时检测,避免死锁。

uint8_t i2c_wait_ack_timeout(uint32_t timeout_us) { uint32_t start = micros(); SDA_IN(); while (READ_SDA()) { if (micros() - start > timeout_us) return 0; } return 1; }

更进一步:如何写出工业级的模拟I2C驱动?

上面的例子够用了,但如果要做产品级开发,还需要考虑更多。

✅ 模块化设计

将底层GPIO操作抽象出来,便于移植:

typedef struct { void (*init)(void); void (*set_scl)(uint8_t level); void (*set_sda)(uint8_t level); uint8_t (*read_sda)(void); void (*delay_us)(uint32_t us); } i2c_bit_ops_t;

这样换MCU只需改操作函数,核心逻辑不变。

✅ 多总线支持

定义多个实例:

i2c_device_t i2c_sensor_bus = { .ops = &sensor_gpio_ops, .speed = 100000 }; i2c_device_t i2c_display_bus = { .ops = &display_gpio_ops, .speed = 400000 };

实现互不干扰的独立通信通道。

✅ 错误处理与重试机制

加入CRC校验、自动重试、状态记录:

uint8_t i2c_write_with_retry(uint8_t addr, uint8_t *buf, int len, int retries) { while (retries-- > 0) { if (i2c_do_write(addr, buf, len) == 0) return 0; delay_ms(10); } return -1; }

提升系统鲁棒性。


写在最后:为什么你应该掌握这项技能?

在这个动辄用HAL库、CubeMX生成代码的时代,还有必要手动写模拟I2C吗?

我的答案是:非常有必要

因为当你面对一款冷门国产MCU、一份残缺的数据手册、一块无法识别的传感器时,那些高级工具都会失效。唯有深入底层的能力,才能让你从容应对。

而且你会发现,一旦掌握了模拟I2C,SPI、单总线、红外遥控这些协议也都变得不再神秘。它们的本质,不过是一系列精确的时序控制而已。

所以,不妨今晚就动手试试:选一块开发板,接一个I2C传感器,不用任何库,从头写一遍读写流程。当你第一次在串口看到正确的温湿度数值跳出来时,那种成就感,远胜于复制粘贴一百行代码。

如果你在实现过程中遇到了挑战,欢迎留言交流。我们一起debug,一起进步。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/1141380.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

IAR软件优化等级选择图解说明:性能与体积平衡策略

IAR优化等级实战指南&#xff1a;如何在性能与体积间找到黄金平衡点&#xff1f;你有没有遇到过这样的情况&#xff1f;项目临近交付&#xff0c;Flash空间告急——明明代码没几行&#xff0c;固件却快爆表&#xff1b;或者实时性要求极高的中断服务函数&#xff0c;响应延迟死…

工业安全继电回路设计:基于Proteus元件对照表实战

工业安全继电回路设计实战&#xff1a;从Proteus仿真到真实世界的无缝衔接在现代工厂的控制柜中&#xff0c;你是否曾见过那些整齐排列、外壳标有“PNOZ”或“SR”字样的小盒子&#xff1f;它们不像PLC那样引人注目&#xff0c;也不像变频器那样复杂&#xff0c;但一旦急停按钮…

人类有史以来最伟大的10大壮举与天问一号

文章目录1. 人类有史以来最伟大的10大壮举&#xff08;按影响与突破排序&#xff09;2. 天问一号时间线&#xff08;含关键节点&#xff09;1. 人类有史以来最伟大的10大壮举&#xff08;按影响与突破排序&#xff09; 生命科学&#xff1a;人类基因组计划&#xff08;2003&…

LTspice 仿真验证正交线圈互感对于信号幅值的影响

简 介&#xff1a; 本文通过LTspice仿真研究了正交电感线圈的互感特性及其对角度测量的影响。仿真结果表明&#xff1a;1&#xff09;当两线圈存在互感时&#xff0c;其信号幅度会随磁场角度变化产生关联&#xff1b;2&#xff09;谐振电容取值差异会导致两路信号输出幅度不同&…

[特殊字符]_内存管理深度解析:如何避免GC导致的性能陷阱[20260110163933]

作为一名经历过无数性能调优案例的工程师&#xff0c;我深知内存管理对Web应用性能的影响有多大。在最近的一个项目中&#xff0c;我们遇到了一个棘手的性能问题&#xff1a;系统在高并发下会出现周期性的延迟飙升&#xff0c;经过深入分析&#xff0c;发现问题根源竟然是垃圾回…

工业控制通信模块PCB板生产厂家布局布线实战

工业控制通信模块PCB设计实战&#xff1a;从原理到量产的硬核避坑指南你有没有遇到过这样的场景&#xff1f;项目前期进展顺利&#xff0c;原理图画得漂亮&#xff0c;仿真波形也“完美”。结果一到试产阶段&#xff0c;问题接踵而至&#xff1a;- RS-485通信频繁丢包&#xff…

风电随机性动态经济调度模型(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

系统极客必备:Driver Store Explorer高级功能探索

驱动仓库的“清道夫”&#xff1a;深入驾驭 Driver Store Explorer 的实战艺术你有没有遇到过这样的情况——明明已经卸载了某款显卡驱动&#xff0c;可系统更新后它又“死而复生”&#xff1f;或者一台原本轻快的电脑&#xff0c;渐渐变得启动缓慢、磁盘告急&#xff0c;排查一…

2025年度GRIT全球最具创新性洞察与分析公司50强榜单

、美通社消息&#xff1a;益普索在2025年度GRIT全球最具创新性洞察与分析公司50强榜单中再度荣登榜首&#xff0c;已连续三年蝉联冠军宝座。年度GRIT最具创新性洞察与分析公司50强榜单源自对全球范围内消费者洞察领域客户、供应商及行业专业人士的调研。在参与评估的1,462家洞察…

基于多主体主从博弈的区域综合能源系统低碳经济优化调度【分层模型】(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

一文说清STM32F4串口通信的STM32CubeMX教程配置步骤

手把手教你用STM32CubeMX配置STM32F4串口通信&#xff1a;从零开始的实战指南你有没有遇到过这种情况&#xff1f;刚焊好一块STM32F4开发板&#xff0c;想通过串口打印“Hello World”验证一下基本功能&#xff0c;结果打开串口助手却一片漆黑——没输出。反复检查代码、波特率…

基于Java+SpringBoot+SSM知识产权管理系统(源码+LW+调试文档+讲解等)/知识产权管理软件/知识产权服务平台/知识产权保护系统/知识产权信息化系统/知识产权管理系统解决方案

博主介绍 &#x1f497;博主介绍&#xff1a;✌全栈领域优质创作者&#xff0c;专注于Java、小程序、Python技术领域和计算机毕业项目实战✌&#x1f497; &#x1f447;&#x1f3fb; 精彩专栏 推荐订阅&#x1f447;&#x1f3fb; 2025-2026年最新1000个热门Java毕业设计选题…

multisim仿真电路图模拟场效应管工作区:深度剖析

用Multisim“透视”场效应管&#xff1a;从仿真波形看透工作区的本质你有没有试过在实验室里搭一个FET放大电路&#xff0c;结果输出信号莫名其妙地削顶&#xff1f;或者明明计算好了偏置电压&#xff0c;实测电流却总对不上手册标称值&#xff1f;别急——问题很可能出在你没真…

STM32CubeMX下载安装包结构解析:系统学习资源组成

深入STM32CubeMX安装包&#xff1a;不只是配置工具&#xff0c;更是你的嵌入式开发资源库你有没有过这样的经历&#xff1f;按照网上一篇stm32cubemx下载教程一步步装好软件后&#xff0c;点开安装目录&#xff0c;面对一堆文件夹——Drivers、Utilities、Middlewares、db……一…

吐血推荐8个AI论文软件,专科生轻松搞定毕业论文!

吐血推荐8个AI论文软件&#xff0c;专科生轻松搞定毕业论文&#xff01; AI工具让论文写作不再难 对于专科生来说&#xff0c;撰写毕业论文往往是一个令人头疼的任务。面对繁重的文献阅读、复杂的结构安排以及反复的修改要求&#xff0c;很多学生感到力不从心。而随着AI技术的不…

espidf构建家庭环境监控系统:从零实现

用ESP-IDF从零打造家庭环境监控系统&#xff1a;实战全解析 你有没有过这样的经历&#xff1f;家里刚装修完&#xff0c;总觉得空气不对劲&#xff1b;或者孩子夜里咳嗽&#xff0c;怀疑是不是卧室太闷、湿度过高&#xff1f;其实&#xff0c;这些生活中的“小困扰”&#xff…

基于 YOLOv8 的桥梁病害(八类缺陷、病害高精度)自动检测 [目标检测完整源码]

基于 YOLOv8 的桥梁病害&#xff08;八类缺陷、病害高精度&#xff09;自动检测 [目标检测完整源码] 一、背景与问题&#xff1a;桥梁检测为什么需要 AI&#xff1f; 桥梁作为城市与交通网络中的关键基础设施&#xff0c;其服役周期长、受力复杂、环境影响显著。随着时间推移…

救命神器2026 TOP10 AI论文写作软件:本科生毕业论文全场景测评

救命神器2026 TOP10 AI论文写作软件&#xff1a;本科生毕业论文全场景测评 2026年AI论文写作工具测评&#xff1a;为何需要一份权威榜单&#xff1f; 随着人工智能技术的不断进步&#xff0c;AI写作工具在学术领域的应用日益广泛。对于本科生而言&#xff0c;撰写毕业论文不仅是…

AD导出Gerber文件在量产交付中的注意事项(项目应用)

AD导出Gerber文件在量产交付中的实战避坑指南你有没有遇到过这样的情况&#xff1a;PCB设计反复修改、熬夜调线&#xff0c;好不容易通过DRC&#xff0c;信心满满地把Gerber发给工厂&#xff0c;结果一周后收到回复——“阻焊开窗错了”、“钻孔偏了0.1mm”、“NPTH没输出”………

基于 YOLOv8 的多车型交通车辆实时检测识别项目 [目标检测完整源码]

基于 YOLOv8 的多车型交通车辆实时检测识别项目 [目标检测完整源码] 一、背景与问题引入 在智慧交通体系中&#xff0c;“看得清、分得准、跑得快”始终是视觉感知系统的核心诉求。传统基于规则或特征工程的方法&#xff0c;在复杂道路环境、密集车流、多车型混行的场景下&am…