news 2026/10/21 1:02:41

STM32驱动AT24C02 EEPROM读写实战:I2C时序、页写与避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32驱动AT24C02 EEPROM读写实战:I2C时序、页写与避坑指南

1. 项目缘起与整体设计思路

搞嵌入式开发的朋友大概率都碰过这样一个场景:板子跑起来了,传感器数据也读到了,但一断电,之前辛辛苦苦标定的参数全没了,下次上电又得重新来一遍。这时候就需要一颗能"记住事儿"的芯片,而AT24C02就是这类需求里出场率极高的角色。它是一颗2Kbit容量的EEPROM,换算成字节就是256字节,通过I2C总线跟主控通信,掉电不丢数据,价格便宜,封装小巧,几乎成了STM32入门阶段绕不开的一个外设。

这次要拆解的项目,就是用STM32F103这颗经典MCU,通过硬件I2C或者软件模拟I2C的方式,完成对AT24C02的完整读写操作。说白了就是三件事:往里面写一个字节、从里面读一个字节、以及按页批量读写。听起来简单,但真正动手的时候,时序、地址、应答、写周期等待这些细节,一个没处理好就是读出来一堆0xFF或者数据错位。我见过不少新手卡在这里好几天,最后发现是上拉电阻没接或者写周期没等够。

这个内容适合谁看?如果你刚接触STM32的I2C外设,或者之前用库函数糊里糊涂能跑但不知道底层发生了什么,那这篇拆解就是给你准备的。我会从硬件连接讲到寄存器层面的时序,再落到HAL库的实际代码,最后把常见坑一个个拎出来。目标很明确:看完你能自己独立完成一套稳定的EEPROM读写驱动,而不是复制粘贴一堆代码然后祈祷它能跑。

整体设计上,我选择用STM32F103C8T6作为主控,这是最常见的入门型号,I2C1挂在PB6和PB7上。AT24C02的A0/A1/A2全部接地,所以器件地址是0xA0(写)和0xA1(读)。通信速率先用100kHz标准模式,稳定之后再尝试400kHz快速模式。软件层面我打算两条腿走路:先讲硬件I2C的配置,再讲软件模拟I2C的实现,因为实际项目里这两种方案各有各的适用场景,硬件I2C省CPU但灵活性差,软件模拟占用CPU但引脚随便换、时序可控。

为什么这么设计?因为很多初学者一上来就用CubeMX生成硬件I2C代码,结果遇到总线锁死或者从机不响应的时候完全不知道从哪查起。如果先理解了软件模拟的时序,再回头看硬件I2C的寄存器配置,整个逻辑就通透了。这就像学开车,先知道离合油门怎么配合,再去开自动挡就心里有数。

2. AT24C02核心细节与I2C时序解析

2.1 器件地址与存储结构

AT24C02的存储空间是256字节,地址范围0x00到0xFF。它的器件地址是7位,高4位固定为1010,也就是0xA,低3位由A2/A1/A0三个引脚决定。这三个引脚接地就是000,接VCC就是111,所以一条I2C总线上最多可以挂8颗AT24C02,总容量2KB。实际用的时候如果只挂一颗,A0/A1/A2全接地最省事,器件地址就是0xA0。

这里有个容易混淆的点:很多人看到数据手册上写"1010"就以为是0xA0,其实0xA0是8位地址,包含了读写位。7位地址是0x50,左移一位加上写位0变成0xA0,加上读位1变成0xA1。我刚开始学的时候就在这里绕了半天,发送0x50死活没应答,后来才反应过来应该发0xA0。

存储结构上,AT24C02分成32页,每页8字节。这个"页"的概念非常关键,因为写操作分两种:字节写和页写。字节写就是一次写一个字节,页写是一次写一页最多8个字节,但页写有个坑——如果你从页中间开始写,写到页尾会自动翻转到本页开头,而不是下一页开头。比如从地址0x05开始写8个字节,实际会写到0x05、0x06、0x07、0x00、0x01、0x02、0x03、0x04,数据被覆盖了。这个翻转行为是硬件决定的,不处理好就是数据错乱。

2.2 I2C通信时序的关键节点

I2C总线就两根线:SCL时钟线和SDA数据线,都是开漏输出,所以必须接上拉电阻。阻值一般选4.7kΩ到10kΩ,速率越高阻值越小。我实测下来,100kHz用4.7kΩ很稳,400kHz建议用2.2kΩ到4.7kΩ。上拉电阻接到3.3V还是5V取决于MCU和EEPROM的供电电压,STM32F103是3.3V,AT24C02支持1.8V到5.5V,所以统一3.3V最方便。

起始条件:SCL为高时,SDA从高变低。停止条件:SCL为高时,SDA从低变高。这两个条件必须由主机产生,而且时序要标准,SDA变化时SCL必须保持稳定。数据位传输:SCL为低时SDA可以变化,SCL为高时SDA必须稳定,接收方在SCL高电平期间采样。每传输一个字节,接收方要拉低SDA一个时钟周期作为应答(ACK),主机在第九个时钟周期释放SDA并读取电平,低电平就是应答,高电平就是非应答(NACK)。

写一个字节的完整流程是这样的:起始条件 → 发送器件地址+写位(0xA0)→ 等待ACK → 发送字节地址(0x00到0xFF)→ 等待ACK → 发送数据字节 → 等待ACK → 停止条件 → 等待写周期(约5ms)。注意最后这个写周期,AT24C02在收到停止条件后会进入内部写操作,这段时间它不响应任何总线请求,必须等它写完才能进行下一次操作。数据手册标称最大5ms,我一般延时5到10ms保险。

读操作稍微复杂一点,分当前地址读、随机读和顺序读。随机读最常用:起始 → 发送0xA0 → 等待ACK → 发送字节地址 → 等待ACK → 重新起始 → 发送0xA1 → 等待ACK → 读取数据 → 发送NACK → 停止。注意最后主机要发NACK而不是ACK,告诉从机"我读够了,你别再发了",然后发停止条件。

2.3 写周期等待与应答轮询

前面说的延时5ms是最简单的处理方式,但效率低。更聪明的做法是应答轮询:发完停止条件后,立刻再发起始条件并发送器件地址+写位,如果AT24C02还在忙,它不会应答,主机收到NACK就知道还没写完,过一会儿再试,直到收到ACK说明写完了。这样平均等待时间远小于5ms,尤其在小数据量频繁写入的场景下能明显提升效率。

不过应答轮询也有个细节:每次轮询失败后要发停止条件再重新起始,不能一直挂着。我一般写成循环,最多轮询100次,每次间隔100微秒,超时就报错。实测下来,单字节写通常1到3ms就完成了,页写8字节大概3到5ms。

注意:AT24C02的写次数标称100万次,但这是整片寿命,不是每个字节。如果你在同一个地址反复写,那个字节会先坏。实际项目里要做磨损均衡,比如用一个计数器记录当前写到哪个位置,轮流使用不同地址。

3. 硬件连接与工程配置实操

3.1 硬件连线与上拉电阻选择

先把手上的东西理清楚:STM32F103C8T6最小系统板一块,AT24C02模块或者裸芯片一个,杜邦线若干,如果有条件的话用逻辑分析仪或者示波器看波形,没有的话靠串口打印调试也行。

连线方案:STM32的PB6接AT24C02的SCL,PB7接SDA,VCC接3.3V,GND接GND,A0/A1/A2全部接地,WP引脚接地(写保护关闭)。上拉电阻两个4.7kΩ,分别从SCL和SDA拉到3.3V。如果你买的是模块,板上通常已经带了上拉电阻,就不用外接了,但要注意有些模块的上拉电阻是10kΩ,跑400kHz可能波形上升沿不够陡,这时候要么换电阻要么降速。

我踩过的一个坑:有一次用杜邦线连了20厘米长,100kHz都通信失败,波形上升沿惨不忍睹。后来把线缩短到5厘米以内就正常了。I2C虽然只有两根线,但对分布电容很敏感,线越长电容越大,上升沿越慢。所以实际画PCB的时候,I2C走线尽量短,远离高频信号线。

3.2 CubeMX配置硬件I2C

打开CubeMX,选好STM32F103C8T6,在Connectivity里找到I2C1,模式选I2C,速度模式选Standard Mode,时钟频率填100000。引脚会自动分配到PB6和PB7。然后在Clock Configuration里确认I2C1的时钟源是APB1,频率36MHz。这里有个计算:I2C的CCR寄存器值等于APB1时钟除以(2乘以目标频率),36MHz除以200kHz等于180,所以CCR设为180,实际速率就是100kHz。CubeMX会自动算,但知道这个公式有助于你排查为什么实际速率不对。

生成代码后,HAL库会初始化I2C1。但要注意,HAL库的I2C函数有时候会卡死,尤其是从机不响应的时候。我建议在I2C初始化之后加一句__HAL_I2C_ENABLE_IT(&hi2c1, I2C_IT_ERR)使能错误中断,或者在超时参数上设一个合理的值,比如100ms,避免死等。

3.3 软件模拟I2C的引脚配置

如果你选择软件模拟,就把PB6和PB7配置成开漏输出模式,输出速度选50MHz。开漏模式配合外部上拉电阻,才能实现真正的双向SDA。有些新手把SDA配成推挽输出,读数据的时候主机和从机同时驱动,轻则读错重则烧引脚。记住:I2C的SDA在任何时候都应该是开漏,SCL主机驱动时也是开漏。

软件模拟的延时函数很关键。100kHz意味着一个时钟周期10微秒,半周期5微秒。用__NOP()或者简单的for循环延时都行,但要注意编译器优化等级。我一般用delay_us(2)这种,实测在72MHz主频下,2微秒的延时加上函数调用开销,刚好接近5微秒半周期。如果你用示波器看波形,发现频率偏差大,就调整延时值。

4. 完整读写流程与代码实现

4.1 硬件I2C字节写与页写

先看字节写的HAL库实现。假设器件地址宏定义AT24C02_ADDR_WRITE 0xA0,字节地址变量mem_addr,数据变量data:

HAL_StatusTypeDef AT24C02_WriteByte(uint8_t mem_addr, uint8_t data) { HAL_StatusTypeDef status; status = HAL_I2C_Mem_Write(&hi2c1, AT24C02_ADDR_WRITE, mem_addr, I2C_MEMADD_SIZE_8BIT, &data, 1, 100); if (status != HAL_OK) { return status; } HAL_Delay(5); return HAL_OK; }

HAL_I2C_Mem_Write这个函数封装了起始、发送器件地址、发送内存地址、发送数据、停止的完整流程。最后一个参数100是超时时间,单位毫秒。写完之后的HAL_Delay(5)就是等写周期。如果你用应答轮询,可以把延时换成轮询函数:

HAL_StatusTypeDef AT24C02_WaitReady(void) { uint8_t retry = 100; while (retry--) { if (HAL_I2C_IsDeviceReady(&hi2c1, AT24C02_ADDR_WRITE, 1, 10) == HAL_OK) { return HAL_OK; } HAL_Delay(1); } return HAL_ERROR; }

页写跟字节写类似,只是数据长度变成最多8字节,而且要注意不能跨页。我一般写一个页写函数,内部先判断起始地址和长度,如果跨页就拆成多次写:

HAL_StatusTypeDef AT24C02_WritePage(uint8_t mem_addr, uint8_t *data, uint8_t len) { uint8_t page_remain = 8 - (mem_addr % 8); if (len <= page_remain) { return HAL_I2C_Mem_Write(&hi2c1, AT24C02_ADDR_WRITE, mem_addr, I2C_MEMADD_SIZE_8BIT, data, len, 100); } else { HAL_StatusTypeDef status; status = HAL_I2C_Mem_Write(&hi2c1, AT24C02_ADDR_WRITE, mem_addr, I2C_MEMADD_SIZE_8BIT, data, page_remain, 100); if (status != HAL_OK) return status; HAL_Delay(5); status = HAL_I2C_Mem_Write(&hi2c1, AT24C02_ADDR_WRITE, mem_addr + page_remain, I2C_MEMADD_SIZE_8BIT, data + page_remain, len - page_remain, 100); HAL_Delay(5); return status; } }

4.2 随机读与顺序读实现

随机读用HAL_I2C_Mem_Read:

HAL_StatusTypeDef AT24C02_ReadByte(uint8_t mem_addr, uint8_t *data) { return HAL_I2C_Mem_Read(&hi2c1, AT24C02_ADDR_READ, mem_addr, I2C_MEMADD_SIZE_8BIT, data, 1, 100); }

注意这里器件地址用的是读地址0xA1。HAL_I2C_Mem_Read内部会自动处理重新起始条件,先写地址再读数据。顺序读就是读多个字节,把长度改成你要读的字节数就行。AT24C02支持整片顺序读,地址到0xFF后会自动回卷到0x00,不会出错。

4.3 软件模拟I2C的底层函数

软件模拟的核心是六个函数:起始、停止、发送字节、接收字节、发送应答、接收应答。我贴几个关键实现:

void I2C_Start(void) { SDA_OUT(); SDA_HIGH(); SCL_HIGH(); delay_us(4); SDA_LOW(); delay_us(4); SCL_LOW(); delay_us(4); } void I2C_SendByte(uint8_t byte) { SDA_OUT(); for (uint8_t i = 0; i < 8; i++) { if (byte & 0x80) SDA_HIGH(); else SDA_LOW(); byte <<= 1; delay_us(2); SCL_HIGH(); delay_us(4); SCL_LOW(); delay_us(2); } } uint8_t I2C_ReadByte(void) { uint8_t byte = 0; SDA_IN(); for (uint8_t i = 0; i < 8; i++) { SCL_HIGH(); delay_us(4); byte <<= 1; if (SDA_READ()) byte |= 0x01; SCL_LOW(); delay_us(4); } return byte; }

SDA_OUT()和SDA_IN()是切换引脚方向的宏,发送时输出,接收时输入。SDA_HIGH()和SDA_LOW()在输出模式下控制电平,在输入模式下SDA_HIGH()要配置成上拉输入或者浮空输入,让外部上拉电阻把线拉高。

基于这些底层函数,字节写就是:起始 → 发送0xA0 → 等待应答 → 发送内存地址 → 等待应答 → 发送数据 → 等待应答 → 停止 → 延时。读就是:起始 → 发送0xA0 → 等待应答 → 发送内存地址 → 等待应答 → 起始 → 发送0xA1 → 等待应答 → 读数据 → 发送NACK → 停止。

4.4 实测数据与波形验证

我用逻辑分析仪抓过一组波形,100kHz下写一个字节的完整帧大概是这样的:起始条件占约5微秒,器件地址字节9个时钟周期约90微秒,内存地址字节90微秒,数据字节90微秒,停止条件5微秒,总共约280微秒。加上5ms写周期,一次字节写耗时约5.3ms。页写8字节的话,数据传输部分约1.5ms,写周期5ms,总共6.5ms,平均每个字节0.8ms,比字节写快很多。

读操作没有写周期,随机读一帧约300微秒,顺序读N个字节就是300微秒加上(N-1)乘以90微秒。所以读操作比写操作快一个数量级。实际项目里如果频繁记录数据,建议攒够一页再写,能显著延长EEPROM寿命。

提示:如果你用HAL库发现HAL_I2C_Mem_Write返回HAL_BUSY或者HAL_TIMEOUT,先检查上拉电阻和连线,再用逻辑分析仪看波形。90%的问题出在硬件上,剩下10%是地址搞错了。

5. 常见问题排查与避坑经验

5.1 通信失败速查表

现象可能原因排查方法解决方式
一直返回HAL_ERROR器件地址错误确认A0/A1/A2接地,地址应为0xA0用示波器看发送的地址字节
读出来全是0xFF写周期没等够写后延时5ms再读加应答轮询或延长延时
读出来全是0x00SDA被拉死检查SDA是否被从机拉低重新上电或发9个时钟解锁
偶尔通信失败上拉电阻太大测上升沿时间换4.7kΩ或更小
400kHz下不稳定线太长或上拉不够缩短线长,减小电阻降到100kHz先用
页写数据错乱跨页翻转检查起始地址和长度拆分成不跨页的写

5.2 总线锁死的解锁方法

I2C总线锁死是常见问题,通常是因为主机在从机发送数据时复位了,导致从机一直拉着SDA不放。解锁方法很简单:把SCL配置成推挽输出,手动发送9个时钟脉冲,然后发一个停止条件。具体操作是:SCL拉低,延时,SCL拉高,延时,重复9次,然后SDA拉低,SCL拉高,SDA拉高,产生停止条件。之后重新初始化I2C外设就能恢复正常。

我一般会在I2C初始化之前加一段解锁代码,不管有没有锁死都执行一遍,成本很低但能避免很多莫名其妙的故障。尤其是调试阶段频繁复位的时候,这个习惯能省不少时间。

5.3 数据可靠性保障技巧

EEPROM虽然掉电不丢数据,但也不是绝对可靠。我一般会在数据后面加一个校验字节,比如CRC8或者简单的累加和。读的时候校验一下,不对就重读或者用默认值。另外重要数据存两份,一份在0x00,一份在0x80,读的时候对比,不一致就以其中一份为准并报警。

还有一点:AT24C02的写周期是5ms,但这5ms内如果电源波动,可能写失败。所以写完之后最好读回来验证一下,确认写进去了再继续。这个"写-读-验证"的流程在工业项目里几乎是标配,多花几毫秒换来的可靠性提升非常值得。

5.4 软件模拟与硬件I2C的取舍

最后说说选型。硬件I2C的优点是省CPU、速率高、代码简洁,缺点是引脚固定、调试困难、某些型号的STM32硬件I2C有已知bug。软件模拟的优点是引脚任意、时序可控、移植方便,缺点是占CPU、速率上限低、代码量大。

我的经验是:如果项目对功耗和CPU占用敏感,用硬件I2C;如果引脚紧张或者需要挂多个I2C设备在不同引脚上,用软件模拟。初学者建议先学软件模拟,把时序吃透,再用硬件I2C就会觉得很简单。反过来先学硬件I2C,遇到问题容易一头雾水。

我个人在实际操作中的体会是,AT24C02这颗芯片虽然简单,但把I2C协议、时序、写周期、页管理这些概念都串起来了,是学习嵌入式存储外设的绝佳入口。把这颗芯片玩透了,后面换AT24C256、FRAM、甚至SPI Flash,思路都是相通的。踩过几次坑之后,我现在拿到任何I2C器件,第一件事就是看时序图、确认地址、算上拉电阻、写测试代码,这套流程已经成了肌肉记忆。

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

GT911触摸屏I2C地址机制与上电时序调试指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/21 0:50:35

MATLAB张量分解实战:Tensor Toolbox安装、分解算法与避坑指南

简介&#xff1a;一款面向MATLAB用户的高阶张量分析工具箱&#xff0c;版本3.2.1&#xff0c;专攻多维数组的建模与分解。工具箱内置CP分解、Tucker型奇异值分解、张量乘除与索引操作等核心算法&#xff0c;可支撑图像处理、社交网络挖掘和多源数据融合等典型场景&#xff0c;供…

作者头像 李华
网站建设 2026/10/21 0:50:29

AI黑盒如何安全用于芯片设计?验证护栏与工程实践解析

部门的例行评审会上&#xff0c;三个项目组都碰到了同一个情况&#xff1a;AI工具给出的优化结果是“好的”&#xff0c;功耗降了、时序也收敛了&#xff0c;但当评审问“它为什么这么改、依据是什么、这个结论能不能写进签核报告”时&#xff0c;却只能看到一行冷冰冰的置信度…

作者头像 李华
网站建设 2026/10/21 0:44:02

Kubernetes集群二进制安装实战:从证书签发到组件启动全解析

如果有人问我&#xff0c;Kubernetes集群用二进制安装部署到底值不值得折腾&#xff0c;我的答案通常是&#xff1a;值得&#xff0c;但要看情况。现在网上大量教程默认kubeadm&#xff0c;默认一条init命令搞定一切&#xff0c;可等你真把集群搭起来&#xff0c;碰到apiserver…

作者头像 李华
网站建设 2026/10/21 0:42:06

自然保护区GIS数据从解压到出图:MXD、SHP、TIF与ArcPy实战指南

简介&#xff1a;全国3300多个自然保护区空间分布数据包&#xff0c;面向GIS分析人员、生态科研与环保管理者&#xff0c;可用于保护区分布格局研究、生态规划与制图。数据截至2024年&#xff0c;覆盖全国3300余个保护区&#xff0c;属性字段含级别、名称、始建年份、所属省市县…

作者头像 李华