news 2026/7/14 12:15:11

【STM32】STM32实战笔记-软件模拟I2C驱动EEPROM(32)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【STM32】STM32实战笔记-软件模拟I2C驱动EEPROM(32)

1. 软件模拟I2C的必要性

在STM32开发中,我们通常会遇到两种I2C实现方式:硬件I2C和软件模拟I2C。硬件I2C虽然效率高,但由于STM32的硬件I2C外设存在一些历史遗留问题(比如时钟拉伸bug),很多开发者更倾向于使用GPIO模拟I2C时序。这种方式虽然会占用更多CPU资源,但胜在稳定可控,移植性强。

我曾在多个项目中使用软件模拟I2C驱动AT24C02 EEPROM,实测下来发现这种方案特别适合对实时性要求不高的场景。比如记录设备运行日志、存储校准参数等。AT24C02是经典的I2C接口EEPROM,容量2Kbit(256字节),支持页写入和随机读取,工作电压范围宽(1.8V-5.5V),是嵌入式系统中常用的非易失性存储器。

2. 硬件电路设计要点

2.1 GPIO配置

软件模拟I2C需要两个GPIO分别作为SCL(时钟线)和SDA(数据线)。这两个引脚必须配置为开漏输出模式(GPIO_Mode_Out_OD),原因有三:

  1. I2C协议允许总线上的多个设备同时驱动信号线
  2. 开漏输出可以避免总线竞争导致的短路
  3. 方便实现"线与"逻辑

以STM32F103为例,我们可以选择PB6和PB7作为I2C引脚:

GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); // SCL引脚配置 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); // SDA引脚配置 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7; GPIO_Init(GPIOB, &GPIO_InitStructure);

2.2 上拉电阻选择

I2C总线需要上拉电阻将信号线拉至高电平。电阻值的选择很关键:

  • 阻值太小:增加功耗,可能超出GPIO驱动能力
  • 阻值太大:上升沿变缓,影响通信速率

根据经验,常用4.7kΩ的上拉电阻。如果总线电容较大(比如线缆较长、设备较多),可以适当减小阻值,但不要低于1kΩ。我曾在一个长距离(约1米)的I2C总线中使用2.2kΩ上拉电阻,通信依然稳定。

3. I2C时序模拟实现

3.1 基础时序函数

软件模拟I2C的核心是精确控制GPIO电平变化。首先需要实现几个基本时序单元:

// 微秒级延时函数 void I2C_Delay(void) { uint16_t i = 10; // 根据主频调整 while(i--); } // 产生起始信号 void I2C_Start(void) { SDA_HIGH(); SCL_HIGH(); I2C_Delay(); SDA_LOW(); // SCL高电平期间SDA下降沿 I2C_Delay(); SCL_LOW(); } // 产生停止信号 void I2C_Stop(void) { SDA_LOW(); SCL_LOW(); I2C_Delay(); SCL_HIGH(); I2C_Delay(); SDA_HIGH(); // SCL高电平期间SDA上升沿 I2C_Delay(); }

3.2 字节收发函数

发送一个字节时,需要从高位开始依次发送。每个bit在SCL低电平时准备,高电平时稳定:

// 发送一个字节 uint8_t I2C_SendByte(uint8_t byte) { uint8_t i, ack; for(i=0; i<8; i++) { if(byte & 0x80) SDA_HIGH(); else SDA_LOW(); byte <<= 1; I2C_Delay(); SCL_HIGH(); I2C_Delay(); SCL_LOW(); } // 读取ACK SDA_HIGH(); // 释放SDA I2C_Delay(); SCL_HIGH(); ack = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_7); // 读取SDA状态 SCL_LOW(); return ack; // 0-应答, 1-非应答 } // 接收一个字节 uint8_t I2C_ReadByte(uint8_t ack) { uint8_t i, byte = 0; SDA_HIGH(); // 释放SDA for(i=0; i<8; i++) { byte <<= 1; SCL_HIGH(); if(GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_7)) byte |= 0x01; SCL_LOW(); I2C_Delay(); } // 发送ACK/NACK if(ack) SDA_HIGH(); else SDA_LOW(); I2C_Delay(); SCL_HIGH(); I2C_Delay(); SCL_LOW(); SDA_HIGH(); // 释放SDA return byte; }

4. AT24C02驱动实现

4.1 器件地址

AT24C02的7位设备地址由三部分组成:

  • 高4位固定为1010
  • 中间3位由A2,A1,A0引脚电平决定
  • 最低位是读写方向位(0-写,1-读)

例如,当A2=A1=A0=0时:

  • 写地址:0xA0
  • 读地址:0xA1

4.2 页写入操作

AT24C02支持页写入(Page Write),每页8字节。写入时需要先发送目标地址,再发送数据:

void EEPROM_PageWrite(uint8_t addr, uint8_t *data, uint8_t len) { I2C_Start(); I2C_SendByte(0xA0); // 器件地址+写 I2C_SendByte(addr); // 内存地址 for(uint8_t i=0; i<len; i++) { I2C_SendByte(data[i]); } I2C_Stop(); Delay_ms(5); // 等待写入完成 }

注意:跨页写入需要分多次操作。我曾经踩过一个坑,试图一次性写入16字节数据,结果只有前8字节被正确写入。

4.3 随机读取操作

随机读取需要先发送目标地址(伪写入),然后重新启动总线进行读取:

uint8_t EEPROM_RandomRead(uint8_t addr) { uint8_t data; I2C_Start(); I2C_SendByte(0xA0); // 器件地址+写 I2C_SendByte(addr); // 内存地址 I2C_Start(); I2C_SendByte(0xA1); // 器件地址+读 data = I2C_ReadByte(1); // 读取后发送NACK I2C_Stop(); return data; }

5. 完整测试例程

下面是一个完整的读写测试程序,包含写入和验证过程:

#define EEPROM_ADDR 0xA0 void EEPROM_Test(void) { uint8_t write_data[8] = {0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0}; uint8_t read_data[8]; uint8_t i, error = 0; // 页写入测试 I2C_Start(); I2C_SendByte(EEPROM_ADDR); I2C_SendByte(0x00); // 起始地址 for(i=0; i<8; i++) { I2C_SendByte(write_data[i]); } I2C_Stop(); Delay_ms(10); // 等待写入完成 // 顺序读取验证 I2C_Start(); I2C_SendByte(EEPROM_ADDR); I2C_SendByte(0x00); // 起始地址 I2C_Start(); I2C_SendByte(EEPROM_ADDR | 0x01); for(i=0; i<7; i++) { read_data[i] = I2C_ReadByte(0); // 发送ACK } read_data[7] = I2C_ReadByte(1); // 最后一个字节发送NACK I2C_Stop(); // 校验数据 for(i=0; i<8; i++) { if(read_data[i] != write_data[i]) { error = 1; break; } } if(error) { printf("EEPROM test failed!\r\n"); } else { printf("EEPROM test passed!\r\n"); } }

6. 常见问题排查

在实际项目中,我遇到过各种I2C通信问题,总结几个典型问题及解决方法:

  1. 无应答(NACK)

    • 检查器件地址是否正确
    • 确认上拉电阻已连接
    • 测量SCL/SDA波形是否正常
  2. 数据错误

    • 降低通信速率(增加延时)
    • 检查电源稳定性
    • 避免在中断中操作I2C
  3. 写入失败

    • 确保写入周期(5ms)足够
    • 页写入不要跨页
    • 检查写保护引脚状态

记得有一次调试时,I2C始终无法正常工作,最后发现是PCB设计问题——SCL和SDA走线太长且平行走线,导致串扰严重。重新布线后问题解决。这个教训告诉我,硬件设计同样重要。

7. 性能优化建议

虽然软件模拟I2C简单可靠,但在高速应用场景下仍需优化:

  1. 精确延时控制

    • 使用定时器产生精确延时
    • 根据主频动态调整延时参数
  2. 中断优化

    • 避免在中断服务程序中长时间操作I2C
    • 使用DMA传输大数据块
  3. 错误恢复机制

    • 添加超时检测
    • 实现总线复位功能

我曾经在一个需要频繁读写EEPROM的项目中,将I2C时钟频率优化到约100kHz(标准模式上限),实测传输一个字节约100us。虽然比不上硬件I2C的400kHz,但对于大多数应用已经足够。

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

AI驱动的跨境电商解决方案:破解OZON平台运营难题

1. 项目概述&#xff1a;AI驱动的跨境电商解决方案 "Captain AI&#xff1a;破解OZON困局&#xff0c;赋能竞争优势"这个项目名称揭示了其核心定位——通过人工智能技术解决跨境电商平台OZON运营中的典型痛点。作为俄罗斯本土最大的电商平台之一&#xff0c;OZON近年…

作者头像 李华
网站建设 2026/7/14 12:14:32

Prompt 注入防护与安全架构:从单点校验到纵深防御

Prompt 注入防护与安全架构&#xff1a;从单点校验到纵深防御 一、Prompt 注入的三类典型攻击 一个面向外部用户的 AI 客服系统上线两周后&#xff0c;安全团队发现了三类注入攻击&#xff1a; 直接指令覆盖&#xff1a;用户输入"忽略之前的所有指令&#xff0c;现在开始用…

作者头像 李华
网站建设 2026/7/14 12:14:24

JS逆向实战——补环境框架sdenv过RS6详解

1. 初识瑞数6代反爬与补环境框架sdenv第一次遇到瑞数6代反爬时&#xff0c;我盯着浏览器控制台里那串412状态码发了好一会儿呆。这种动态安全防护技术确实比传统反爬要难缠得多&#xff0c;它不像普通验证码那样有明确的破解路径&#xff0c;而是通过实时变化的JavaScript代码和…

作者头像 李华
网站建设 2026/7/14 12:14:23

如何快速配置黑苹果:OpCore-Simplify图形化工具终极指南

如何快速配置黑苹果&#xff1a;OpCore-Simplify图形化工具终极指南 【免费下载链接】OpCore-Simplify A tool designed to simplify the creation of OpenCore EFI 项目地址: https://gitcode.com/GitHub_Trending/op/OpCore-Simplify 还在为复杂的黑苹果配置而头疼吗&…

作者头像 李华
网站建设 2026/7/14 12:13:17

思源宋体CN:7种字重免费字体让你的中文设计焕然一新

思源宋体CN&#xff1a;7种字重免费字体让你的中文设计焕然一新 【免费下载链接】source-han-serif-ttf Source Han Serif TTF 项目地址: https://gitcode.com/gh_mirrors/so/source-han-serif-ttf 还在为中文排版设计找不到合适的免费字体而烦恼吗&#xff1f;想象一下…

作者头像 李华
网站建设 2026/7/14 12:12:30

架构师视角:从“AI Omnibus”法案看2026年技术合规的三大工程化实践

1. 当AI遇上法律&#xff1a;2026年技术合规的架构挑战2026年即将生效的"AI Omnibus"法案就像给狂奔的AI技术踩了一记刹车。作为经历过三次AI技术浪潮的老兵&#xff0c;我亲眼目睹过太多团队在合规问题上栽跟头。去年有个做智能客服的创业团队&#xff0c;因为没做好…

作者头像 李华