news 2026/4/16 17:53:43

STM32使用软件模拟I2C读写EEPROM代码详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32使用软件模拟I2C读写EEPROM代码详解

STM32软件模拟I²C驱动AT24C系列EEPROM实战详解

在嵌入式开发中,我们常常需要保存一些关键数据——比如设备校准参数、用户设置或运行日志。这些信息必须在断电后依然存在,这就离不开非易失性存储器。而其中最常用、成本最低的方案之一,就是使用通过I²C接口通信的AT24C系列EEPROM芯片

但问题来了:你的STM32板子上唯一的硬件I²C外设已经被OLED屏占用了,怎么办?难道要换主控、改PCB?

答案是:不需要!你可以用任意两个GPIO引脚“手搓”一个I²C总线出来

这就是本文要讲的核心技术——软件模拟I²C(Bit-Banging I²C)。我们将从零开始,深入剖析其底层机制,并实现一套稳定可靠的读写EEPROM代码,适用于所有STM32平台。


为什么选择软件模拟I²C?

现代MCU大多集成了硬件I²C控制器,按理说应该优先使用。但在实际项目中,以下场景屡见不鲜:

  • 主控只有1路I²C,却被传感器和RTC瓜分;
  • 硬件I²C引脚与调试接口(如SWD)冲突;
  • PCB布局已定型,无法连接到指定I²C管脚;
  • 某些老型号STM32的I²C模块存在bug,容易锁死总线。

此时,软件模拟I²C就成了救场利器

它的本质非常简单:用GPIO口手动控制SCL和SDA的电平变化,配合精确延时,复现标准I²C协议的时序波形

虽然CPU占用率比硬件方式高,但对于像EEPROM这种低频访问设备(通常每秒最多几次操作),完全可以接受。

更重要的是:
- ✅ 引脚可自由选择
- ✅ 不依赖特定外设
- ✅ 易于调试观测
- ✅ 学习价值极高


I²C协议精要:三步看懂通信流程

在写代码前,先搞清楚I²C是怎么工作的。

I²C总线只需要两根线:
-SCL:串行时钟线,由主机驱动;
-SDA:串行数据线,双向传输。

整个通信过程遵循“起始 → 地址 → 数据 → 停止”的基本模式,且每个字节后都有一个应答位(ACK)来确认接收成功。

关键信号解析

信号触发条件
StartSCL为高时,SDA从高变低
StopSCL为高时,SDA从低变高
ACK接收方在第9个时钟周期将SDA拉低

数据以字节为单位传输,每次发送8位,高位先行

举个例子,主设备想向地址为0x50的EEPROM写数据:
1. 发送起始信号
2. 发送设备写地址0xA0(即0x50 << 1 | 0
3. 等待ACK
4. 发送内存地址(比如要写入的位置0x0F)
5. 再次等待ACK
6. 发送要写的数据字节
7. 最后发Stop结束

读操作稍复杂一点,采用“两次启动”的方式:
1. 先以写模式发送目标地址(定位指针)
2. 不发Stop,而是再发一次Start(Repeated Start)
3. 切换为读模式,接收数据

理解了这一点,你就掌握了I²C的灵魂。


软件模拟I²C底层驱动实现

下面我们基于STM32 HAL库,用C语言一步步构建这套系统。

使用芯片:STM32F103C8T6
EEPROM型号:AT24C02(256字节)
GPIO选择:PB6(SCL)、PB7(SDA)

1. 引脚定义与宏配置

#define I2C_SCL_PIN GPIO_PIN_6 #define I2C_SDA_PIN GPIO_PIN_7 #define I2C_GPIO_PORT GPIOB

这两个引脚将分别作为时钟线和数据线使用。

2. SDA方向切换函数

由于SDA是双向引脚,在发送数据时需设为输出,在读取ACK或接收数据时需设为输入。

// 设置SDA为开漏输出模式 static void i2c_sda_output(void) { GPIO_InitTypeDef GPIO_InitStruct = {0}; GPIO_InitStruct.Pin = I2C_SDA_PIN; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD; // 开漏输出 GPIO_InitStruct.Pull = GPIO_PULLUP; // 启用内部上拉 GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(I2C_GPIO_PORT, &GPIO_InitStruct); } // 设置SDA为浮空输入模式(用于读取) static void i2c_sda_input(void) { GPIO_InitTypeDef GPIO_InitStruct = {0}; GPIO_InitStruct.Pin = I2C_SDA_PIN; GPIO_InitStruct.Mode = GPIO_MODE_INPUT; GPIO_InitStruct.Pull = GPIO_PULLUP; HAL_GPIO_Init(I2C_GPIO_PORT, &GPIO_InitStruct); }

注意:这里使用开漏输出 + 外部上拉电阻,是为了符合I²C物理层规范。即使没有外部电阻,也可借助内部上拉勉强工作(但不推荐用于正式产品)。

3. 延时函数设计

I²C标准模式要求SCL高电平时间 ≥ 4μs,因此我们需要一个微秒级延时。

static void i2c_delay(void) { uint32_t i = 50; while (i--) __NOP(); }

这个数值需要根据你的系统主频调整。例如在72MHz下,一条__NOP()约等于1个时钟周期,50次循环大约耗时0.7μs,加上其他指令开销,整体接近5~6μs,满足100kHz速率需求。

⚠️ 提示:若需更高精度,建议使用SysTick或DWT计数器替代硬循环。

4. 起始与停止信号生成

static void i2c_start(void) { // 初始状态:SCL=1, SDA=1 HAL_GPIO_WritePin(I2C_GPIO_PORT, I2C_SCL_PIN, GPIO_PIN_SET); HAL_GPIO_WritePin(I2C_GPIO_PORT, I2C_SDA_PIN, GPIO_PIN_SET); i2c_delay(); // SDA下降沿 → Start HAL_GPIO_WritePin(I2C_GPIO_PORT, I2C_SDA_PIN, GPIO_PIN_RESET); i2c_delay(); // 拉低SCL,进入数据传输阶段 HAL_GPIO_WritePin(I2C_GPIO_PORT, I2C_SCL_PIN, GPIO_PIN_RESET); i2c_delay(); } static void i2c_stop(void) { // 当前SCL=0, SDA=0 HAL_GPIO_WritePin(I2C_GPIO_PORT, I2C_SCL_PIN, GPIO_PIN_SET); // 先拉高SCL i2c_delay(); // SDA上升沿 → Stop HAL_GPIO_WritePin(I2C_GPIO_PORT, I2C_SDA_PIN, GPIO_PIN_SET); i2c_delay(); }

关键点在于:Start是SCL高时SDA下降,Stop是SCL高时SDA上升。顺序不能错!

5. 字节发送与ACK检测

static uint8_t i2c_write_byte(uint8_t data) { for (int i = 0; i < 8; i++) { HAL_GPIO_WritePin(I2C_GPIO_PORT, I2C_SCL_PIN, GPIO_PIN_RESET); i2c_delay(); if (data & 0x80) { HAL_GPIO_WritePin(I2C_GPIO_PORT, I2C_SDA_PIN, GPIO_PIN_SET); } else { HAL_GPIO_WritePin(I2C_GPIO_PORT, I2C_SDA_PIN, GPIO_PIN_RESET); } data <<= 1; i2c_delay(); HAL_GPIO_WritePin(I2C_GPIO_PORT, I2C_SCL_PIN, GPIO_PIN_SET); // 上升沿采样 i2c_delay(); } // 释放SDA,读取ACK HAL_GPIO_WritePin(I2C_GPIO_PORT, I2C_SCL_PIN, GPIO_PIN_RESET); i2c_sda_input(); // 切换为输入 i2c_delay(); HAL_GPIO_WritePin(I2C_GPIO_PORT, I2C_SCL_PIN, GPIO_PIN_SET); i2c_delay(); uint8_t ack = HAL_GPIO_ReadPin(I2C_GPIO_PORT, I2C_SDA_PIN); HAL_GPIO_WritePin(I2C_GPIO_PORT, I2C_SCL_PIN, GPIO_PIN_RESET); i2c_sda_output(); // 恢复输出 return ack == GPIO_PIN_RESET ? 0 : 1; // 0表示收到ACK }

重点注意事项:
- 每个bit在SCL低电平时准备,SCL上升沿被从机采样;
- 发送完8位后,主机释放SDA(设为输入),由从机拉低表示ACK;
- 读取完成后恢复SDA为输出模式,避免影响后续操作。

6. 字节接收(带ACK/NACK控制)

static uint8_t i2c_read_byte(uint8_t ack) { uint8_t data = 0; i2c_sda_input(); // SDA设为输入 for (int i = 0; i < 8; i++) { HAL_GPIO_WritePin(I2C_GPIO_PORT, I2C_SCL_PIN, GPIO_PIN_RESET); i2c_delay(); HAL_GPIO_WritePin(I2C_GPIO_PORT, I2C_SCL_PIN, GPIO_PIN_SET); i2c_delay(); data = (data << 1) | HAL_GPIO_ReadPin(I2C_GPIO_PORT, I2C_SDA_PIN); } // 发送ACK/NACK HAL_GPIO_WritePin(I2C_GPIO_PORT, I2C_SCL_PIN, GPIO_PIN_RESET); i2c_sda_output(); HAL_GPIO_WritePin(I2C_GPIO_PORT, I2C_SDA_PIN, ack ? GPIO_PIN_RESET : GPIO_PIN_SET); i2c_delay(); HAL_GPIO_WritePin(I2C_GPIO_PORT, I2C_SCL_PIN, GPIO_PIN_SET); i2c_delay(); HAL_GPIO_WritePin(I2C_GPIO_PORT, I2C_SCL_PIN, GPIO_PIN_RESET); return data; }

最后一个字节通常发送NACK(ack=0),通知从机停止发送。


驱动AT24C02 EEPROM:封装实用API

现在有了基础I²C操作函数,接下来针对AT24C02进行封装。

设备地址说明

AT24C02的7位从机地址为1010xxx,其中xxx由A2/A1/A0三个硬件引脚决定。默认接地时为0b1010000,即0x50。

因此:
- 写地址:0x50 << 1 | 0=0xA0
- 读地址:0x50 << 1 | 1=0xA1

#define AT24C_ADDR_WRITE 0xA0 #define AT24C_ADDR_READ 0xA1

单字节写入

uint8_t at24c_write_byte(uint16_t addr, uint8_t data) { i2c_start(); if (i2c_write_byte(AT24C_ADDR_WRITE)) return 1; // 未响应 i2c_write_byte((uint8_t)addr); // 发送内存地址 i2c_write_byte(data); // 发送数据 i2c_stop(); HAL_Delay(10); // 等待内部写入完成(最大10ms) return 0; }

⚠️ 注意:每次写入后必须延时至少10ms!否则芯片仍在忙于烧录,下次通信会失败。

单字节读取(随机读)

uint8_t at24c_read_byte(uint16_t addr) { uint8_t data; i2c_start(); if (i2c_write_byte(AT24C_ADDR_WRITE)) return 0xFF; i2c_write_byte((uint8_t)addr); // 定位地址指针 i2c_start(); // Repeated Start i2c_write_byte(AT24C_ADDR_READ); data = i2c_read_byte(0); // 读取并NACK i2c_stop(); return data; }

连续读取(高效批量读)

void at24c_read_buffer(uint16_t addr, uint8_t* buf, uint16_t len) { i2c_start(); i2c_write_byte(AT24C_ADDR_WRITE); i2c_write_byte((uint8_t)addr); i2c_start(); i2c_write_byte(AT24C_ADDR_READ); while (len--) { *buf++ = i2c_read_byte(len > 0); // 最后一字节前发ACK,最后一字节发NACK } i2c_stop(); }

这种方式可以一次性读出多个字节,适合加载配置表等场景。


实际应用中的坑点与秘籍

别以为代码跑通就万事大吉,真实项目中还有很多细节要注意。

🔧 上拉电阻怎么选?

I²C是开漏结构,必须接上拉电阻才能产生高电平。

  • 阻值一般取4.7kΩ ~ 10kΩ
  • 总线电容过大时(挂载设备多),应减小阻值以加快上升沿
  • 可参考公式:$ R_{pull-up} \geq \frac{V_{DD} - V_{OL}}{I_{OL}} $,并结合上升时间要求调整

🛑 写入延时能不能优化?

目前用的是固定HAL_Delay(10),效率很低。有没有办法提前知道写入完成了?

有!可以用轮询方式代替延时:

void at24c_wait_ready(void) { while (1) { i2c_start(); if (i2c_write_byte(AT24C_ADDR_WRITE) == 0) { // 收到ACK表示就绪 i2c_stop(); break; } // 否则继续尝试 } }

这样只要芯片一准备好就能继续通信,无需傻等10ms。

💡 如何提升可靠性?

加入重试机制,防止偶然干扰导致失败:

uint8_t at24c_write_with_retry(uint16_t addr, uint8_t data, uint8_t max_retries) { for (int i = 0; i < max_retries; i++) { if (at24c_write_byte(addr, data) == 0) { return 0; } HAL_Delay(10); } return 1; }

📈 寿命管理建议

AT24C02标称擦写寿命100万次,看似很多,但如果频繁更新某个地址(如心跳计数器),几年就可能报废。

应对策略:
- 将变量分散存储(磨损均衡)
- 使用RAM缓存,定时刷写(降低频率)
- 对关键数据做备份冗余


总结:你学到的不只是代码

本文从协议原理出发,逐步实现了完整的软件模拟I²C驱动EEPROM方案。你获得的不仅是几段可复用的代码,更是以下能力:

  • 深入理解I²C协议本质:不再把“I²C”当作黑盒调用;
  • 掌握位带操作思想:为日后驱动其他串行设备打下基础;
  • 具备跨平台移植能力:同一套逻辑可轻松迁移到ESP32、GD32、nRF等平台;
  • 增强调试信心:当通信异常时,你能快速定位是时序、电平还是地址问题。

更重要的是,当你亲手“敲”出每一个起始信号、看着逻辑分析仪上的波形完美对齐时,那种掌控硬件的感觉,正是嵌入式开发最迷人的地方。

如果你正在做一个小型物联网终端、工业控制器或者智能仪表,这套方案完全可以作为你的标配数据存储模块。

动手试试吧!哪怕只是往EEPROM里写入一句”Hello World”,也是通往资深工程师之路的重要一步。

如有疑问或遇到具体问题,欢迎留言交流。后续我们还可以拓展至页写优化、多设备挂载、与其他I²C传感器共用总线等进阶话题。

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

揭秘RBAC到ABAC的演进之路:如何实现真正的细粒度权限控制

第一章&#xff1a;揭秘权限控制的演进动因随着信息系统从单机走向分布式&#xff0c;再到微服务与云原生架构的普及&#xff0c;权限控制机制经历了深刻变革。传统的静态权限模型已无法满足复杂业务场景下的动态授权需求&#xff0c;推动权限体系不断演进。安全威胁的持续升级…

作者头像 李华
网站建设 2026/4/16 9:50:39

零基础入门:用AI智能文档扫描仪镜像快速矫正歪斜文档

零基础入门&#xff1a;用AI智能文档扫描仪镜像快速矫正歪斜文档 1. 引言 在日常办公、学习或财务报销场景中&#xff0c;我们经常需要将纸质文件快速数字化。然而&#xff0c;使用手机拍摄的文档照片往往存在角度倾斜、透视变形、阴影干扰等问题&#xff0c;严重影响可读性和…

作者头像 李华
网站建设 2026/3/21 6:08:32

办公效率翻倍:智能文档扫描仪镜像性能优化技巧

办公效率翻倍&#xff1a;智能文档扫描仪镜像性能优化技巧 1. 背景与核心价值 在现代办公场景中&#xff0c;纸质文档的数字化处理已成为高频刚需。无论是合同归档、发票报销&#xff0c;还是会议白板记录&#xff0c;传统手动裁剪和调色方式耗时耗力。而市面上主流的“全能扫…

作者头像 李华
网站建设 2026/4/16 14:31:37

基于keil编译器下载v5.06的PLC开发环境搭建完整指南

从零搭建工业级PLC开发平台&#xff1a;基于Keil MDK v5.06的实战配置全解析你是否曾为一个看似简单的“无法连接目标”问题耗费半天时间&#xff1f;是否在编译时遇到一堆Undefined symbol却不知从何查起&#xff1f;又或者&#xff0c;明明程序下载成功了&#xff0c;但MCU就…

作者头像 李华
网站建设 2026/4/16 10:22:13

隐私保护中的深度学习同态加密与代理重加密机制研究【附代码】

✅ 博主简介&#xff1a;擅长数据搜集与处理、建模仿真、程序设计、仿真代码、论文写作与指导&#xff0c;毕业论文、期刊论文经验交流。✅成品或者定制&#xff0c;扫描文章底部微信二维码。&#xff08;1&#xff09;隐私保护图像分类深度学习方案设计深度学习技术在图像识别…

作者头像 李华
网站建设 2026/4/16 8:40:50

HunyuanVideo-Foley应用场景:短视频创作者必备音效神器

HunyuanVideo-Foley应用场景&#xff1a;短视频创作者必备音效神器 1. 引言&#xff1a;短视频时代的音效挑战 在当前内容为王的短视频生态中&#xff0c;优质的视听体验已成为决定用户留存的关键因素。然而&#xff0c;大多数创作者在视频制作过程中仍面临一个长期痛点&…

作者头像 李华