news 2026/4/6 18:42:09

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/1 14:56:34

AnimeGANv2入门必读:照片转新海诚风格动漫指南

AnimeGANv2入门必读&#xff1a;照片转新海诚风格动漫指南 1. 技术背景与应用价值 随着深度学习在图像生成领域的持续突破&#xff0c;风格迁移&#xff08;Style Transfer&#xff09; 已从学术研究走向大众化应用。传统方法如神经风格迁移&#xff08;Neural Style Transfe…

作者头像 李华
网站建设 2026/4/4 1:30:41

Mac版STM32CubeMX安装包配置指南:手把手教程

Mac上跑通STM32开发第一站&#xff1a;手把手搞定STM32CubeMX安装与配置 你是不是也曾在Mac上打开ST官网&#xff0c;看着那个 .dmg 文件犹豫了一下&#xff1a;“这东西真能用&#xff1f;” 别担心&#xff0c;我也经历过——点开下载链接时信心满满&#xff0c;结果双击…

作者头像 李华
网站建设 2026/4/5 1:13:05

无接触雷达睡眠监测算法与技术架构解析

无接触雷达睡眠监测算法与技术架构解析 睡眠质量对身体和情绪健康的益处已得到充分证明&#xff0c;但仍有三分之一的成年人睡眠不足。基于某中心在机器学习和雷达技术领域的专长&#xff0c;旨在发明一种设备&#xff0c;通过全面审视影响良好夜间休息的因素&#xff0c;帮助用…

作者头像 李华
网站建设 2026/4/2 19:12:55

HunyuanVideo-Foley沙箱隔离:保障系统安全的运行环境

HunyuanVideo-Foley沙箱隔离&#xff1a;保障系统安全的运行环境 1. 技术背景与安全挑战 随着AI生成技术的快速发展&#xff0c;端到端音视频生成模型正逐步进入开发者和内容创作者的工作流。HunyuanVideo-Foley是由腾讯混元于2025年8月28日宣布开源的一款端到端视频音效生成…

作者头像 李华
网站建设 2026/4/2 7:19:48

告别扫描APP!AI智能文档扫描仪本地处理更安全

告别扫描APP&#xff01;AI智能文档扫描仪本地处理更安全 1. 引言&#xff1a;为什么你需要一个本地化的智能文档扫描方案&#xff1f; 在日常办公与学习中&#xff0c;我们经常需要将纸质文件、发票、合同或白板笔记转化为电子版。传统方式依赖手机上的“全能扫描王”类应用…

作者头像 李华
网站建设 2026/3/24 10:45:27

S32DS使用环境下多核启动与驱动加载机制讲解

深入S32DS多核启动与驱动加载&#xff1a;从复位向量到系统协同的实战解析你有没有遇到过这样的场景&#xff1f;在S32DS中配置好了一个双核项目&#xff0c;主核跑得稳稳当当&#xff0c;但从核就是“纹丝不动”——没有日志输出、无法打断点、甚至JTAG都检测不到它的运行痕迹…

作者头像 李华