简介:一份基于89C51/89C52单片机的AT24C02读写应用资源,面向51单片机学习者和电子设计入门者,演示如何将按键次数写入AT24C02存储芯片,再读出并显示在1602LCD液晶屏上。工程基于Keil5编写C语言程序,配套Proteus 7.8仿真环境,适合需要快速理解I2C通信、掉电保存与液晶显示结合的实践项目。压缩包共5个文件,包含C源码、hex烧录文件、Proteus仿真工程(dsn)、原理图备份(dbk)与调试信息(pwi),整体仅39KB,轻量易用。c文件为核心控制代码,hex可直接烧录至单片机,dsn文件可在Proteus中打开查看电路连接与运行效果。目前已有941人学习下载,可用于课程设计、毕业设计或日常练手。资源内提供了完整工程与仿真,读者可直观看到AT24C02的读写时序、1602LCD的驱动方法以及按键计数逻辑,还能直接打开仿真验证效果,并根据源码修改扩展功能。无论硬件实验还是纯仿真学习,这套工程都能帮助夯实单片机外围接口编程基础。
1. 设备计数不上电就丢,AT24C02 存的就是这个“记忆”
做嵌入式的人大概率遇到过这个场景:样机装到现场跑了一周,用户问“这按键到底按了多少次”,你发现程序里counter++跑得挺欢,可一断电,所有计数归零。次数这种东西,存在 RAM 里等于没存。把按键次数写进 AT24C02,再读出来用 1602LCD 显示,是 51/STM32 学习板、智能工位计数器、考勤签到机、小家电寿命测试台上最常见的落地方案,也是理解 I2C 从机通信、EEPROM 的页写与读改写机制最直接的一个练习。
先说结论:这个项目不难,但坑不少。AT24C02 的 I2C 时序、写周期等待、页边界回卷、按键消抖与边沿检测、1602 的忙标志处理,任何一环偷懒,都能让你调半天。下面按“硬件连接 → I2C 读写 → 按键计数策略 → 调试验证”的顺序,把整条链路拆开讲透,每一段都有可以直接抄走的代码和参数说明。
2. 先搞清 AT24C02 和 1602LCD 的硬件连接,引脚不能凭感觉接
2.1 AT24C02 的器件地址与 WP 引脚是第一个容易翻车的地方
AT24C02 是 2Kbit(256 字节)的串行 EEPROM,走 I2C 接口。它一共有三个地址引脚 A0、A1、A2,用来决定器件在 I2C 总线上的从机地址。很多开发板上这三个引脚已经接地,所以 7 位地址固定是0x50,加上读写位后,写地址是0xA0,读地址是0xA1。但你如果自己搭板子,必须确认这三个引脚的电平状态,否则时序再对,器件也永远不会应答。
WP(写保护)引脚同样关键。WP 接高电平时,整个芯片被写保护,写操作会被静默忽略,而读操作不受影响。常见做法是把 WP 直接接地,但如果你设计的系统里有一段代码需要长期保护配置区不被误写,可以考虑用 GPIO 控制 WP,只在写操作的 5ms 窗口内拉低,写完立刻拉高。这个做法在固件升级场景里很实用,但这个项目里直接接地即可。
SDA 和 SCL 需要接上拉电阻,典型值是 4.7kΩ 到 10kΩ。如果你用的是现成开发板,板上通常已经焊好,但如果用面包板搭,忘了上拉电阻会导致 I2C 通信随机失败——表现为主机发送地址后收不到 ACK,或者偶尔正常偶尔超时。
2.2 1602LCD 的接线,重点在 RW 引脚和背光供电
1602LCD 是 16 字符 × 2 行的字符型液晶模块,数据总线有 8 位和 4 位两种模式。做按键计数显示这种数据量很小的项目,建议直接用 4 位模式,省 4 个 IO。需要接的引脚一共 6 个:RS、RW、E、D4、D5、D6、D7。注意这里其实是 7 根线,因为 4 位数据模式下 D0~D3 不接。
RS 接高电平表示传输的是数据,接低电平表示传输的是指令;RW 接低电平表示写,接高电平表示读。很多教程直接把 RW 接地,只做写入,这样也能工作,但你就失去了读取忙标志(BF)的能力,每次写指令和数据前只能靠固定延时。延时方案当然能用,但有局限性——如果你把主频从 12MHz 改到 11.0592MHz,或者换了更慢的 IO 操作库,固定的延时裕量就会变紧。我一般会建议把 RW 接出来,用读忙标志的方式替代延时,稳定性和自适应性都更好。
背光引脚(A/K 或 LED+/LED-)串一个 10Ω 到 100Ω 的电阻接电源,用来限流。1602 的工作电压通常是 5V,如果主控是 3.3V 的 STM32,背光可以接 5V,但逻辑引脚需要确认是否容忍 5V——大多数 STM32 的 IO 不是 5V 容忍的,需要电平转换或者用 3.3V 供电的 1602 模块。
2.3 整体接线表,照着对一遍再上电
下面给出一组适用于 STM32F103 或 STC89C52 的典型接线参考,具体引脚以你自己的板子丝印为准:
| 模块 | 引脚 | 连接到主控 | 说明 |
|---|---|---|---|
| AT24C02 | SCL | PB6(I2C1_SCL) | 接 4.7kΩ 上拉到 VCC |
| AT24C02 | SDA | PB7(I2C1_SDA) | 接 4.7kΩ 上拉到 VCC |
| AT24C02 | A0/A1/A2 | GND | 从机地址定为 0x50 |
| AT24C02 | WP | GND | 直接接地,允许写入 |
| 1602LCD | RS | PB0 | 寄存器选择 |
| 1602LCD | RW | PB1 | 读/写选择 |
| 1602LCD | E | PB2 | 使能脉冲 |
| 1602LCD | D4~D7 | PB3~PB6 | 4 位数据线,注意 PB6 同时用于 I2C SCL,这里要避开 |
上面表格里有一个非常隐蔽的坑:PB6 是 I2C1 的 SCL 复用引脚,同时也被我用在了 1602 的 D6 上——这显然是冲突的。实际接线时务必先查主控的引脚复用表,把 I2C 引脚和 LCD 数据引脚错开。比如把 1602 的数据线挪到 PA0~PA3 或 PC0~PC3,别从 PB 口一路顺下去。
// 引脚定义(以 STM32F103 为例,使用标准外设库) #define LCD_RS_PORT GPIOA #define LCD_RS_PIN GPIO_Pin_0 #define LCD_RW_PORT GPIOA #define LCD_RW_PIN GPIO_Pin_1 #define LCD_E_PORT GPIOA #define LCD_E_PIN GPIO_Pin_2 #define LCD_D4_PORT GPIOA #define LCD_D4_PIN GPIO_Pin_3 #define LCD_D5_PORT GPIOA #define LCD_D5_PIN GPIO_Pin_4 #define LCD_D6_PORT GPIOA #define LCD_D6_PIN GPIO_Pin_5 #define LCD_D7_PORT GPIOA #define LCD_D7_PIN GPIO_Pin_6提示:I2C 的 SCL/SDA 一定要查数据手册确认是否有硬件 I2C 外设可以复用。如果你用的是软件模拟 I2C,任意两个普通 GPIO 都可以,就不存在引脚冲突问题。
3. I2C 读写 AT24C02 的核心代码,地址和页写边界是分水岭
3.1 为什么推荐软件模拟 I2C,而不是硬件 I2C
STM32 的硬件 I2C 外设口碑有点两极分化。老款 F1 系列在总线错误恢复上确实有些麻烦,加上寄存器配置复杂,很多工程师(包括我)在做这种小项目时更倾向用 GPIO 模拟 I2C 时序。软件模拟的好处是:引脚随意、时序可控、出问题容易用逻辑分析仪排查,而且代码移植到任何平台都只需要改引脚定义。缺点是 CPU 占用高,但在这种按键计数的场景下完全不是问题。
I2C 的时序本质就是四件事:起始条件、停止条件、发送字节并等待 ACK、接收字节并发送 ACK。只要这四件事的时序满足 AT24C02 数据手册里的参数要求,就能稳定通信。
// I2C 引脚宏定义与延时函数 #define I2C_SCL_H() GPIO_SetBits(GPIOB, GPIO_Pin_6) #define I2C_SCL_L() GPIO_ResetBits(GPIOB, GPIO_Pin_6) #define I2C_SDA_H() GPIO_SetBits(GPIOB, GPIO_Pin_7) #define I2C_SDA_L() GPIO_ResetBits(GPIOB, GPIO_Pin_7) #define I2C_SDA_READ() GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_7) // 延时函数,I2C 时钟频率约 100kHz,即每 bit 约 10us static void i2c_delay(void) { // 空循环延时,约 5us @72MHz for (volatile int i = 0; i < 10; i++); }3.2 按字节写和当前地址读,最小可用的存取函数
写一个字节到 AT24C02 需要三个步骤:先发起始条件,再发器件写地址0xA0,接着发目标存储地址,最后发数据字节,然后停止。器件在接收到停止条件后进入内部写周期,大约需要 5ms,这个时间内芯片不响应任何命令,必须等待。
// 向 AT24C02 指定地址写入一个字节 // addr: 0~255, data: 待写入数据 // 返回值: 0 成功, 1 失败 uint8_t at24c02_write_byte(uint8_t addr, uint8_t data) { i2c_start(); // 发送器件写地址,等待 ACK if (i2c_send_byte(0xA0) != 0) { i2c_stop(); return 1; } // 发送片内存储地址 if (i2c_send_byte(addr) != 0) { i2c_stop(); return 1; } // 发送数据字节 if (i2c_send_byte(data) != 0) { i2c_stop(); return 1; } i2c_stop(); // 等待内部写周期完成 at24c02_wait_write_complete(); return 0; }at24c02_wait_write_complete()的实现方式是不断尝试发送器件写地址,直到收到 ACK 为止。因为器件在内部写周期内对任何地址都不应答,所以“发地址等 ACK”本身就是一个查询机制。注意用软件 I2C 做这个查询时要带超时机制,防止死循环卡死系统。
读一个字节相对简单,先发一个伪写操作把片内地址指针定位,然后重新发起始条件,发送器件读地址0xA1,最后读取一个字节并发送 NACK 表示结束。
// 从 AT24C02 指定地址读取一个字节 uint8_t at24c02_read_byte(uint8_t addr) { uint8_t data = 0xFF; i2c_start(); i2c_send_byte(0xA0); // 伪写,定位地址 i2c_send_byte(addr); i2c_start(); // 重新起始 i2c_send_byte(0xA1); // 读地址 data = i2c_receive_byte(); // 读一个字节 i2c_send_nack(); // 发送 NACK 结束 i2c_stop(); return data; }注意:读操作最后必须发 NACK,否则器件会认为主机还想继续读下一个地址的数据,总线释放流程就会出现问题。这是新手最容易漏的一步。
3.3 读改写是计数存档的关键,不能只写一个字节
按键次数的存档逻辑和普通数据写入有一个本质区别:每次按键都需要“把原来的值读出来,加一,再写回去”。这个读改写(read-modify-write)操作天然存在一个时间窗口:如果在“读出来”和“写回去”之间发生了两次按键,第二次按键读到的还是旧值,加一后写回,计数就少了一次。
解决这个问题的思路有两个层面。简单场景:把读改写放在一个不可被中断打断的临界区内,先关中断,读、加、写、等待写完成,再开中断。对于按键这种低频事件完全够用。但要注意,AT24C02 的写周期最长 5ms,关中断 5ms 对实时性要求高的系统可能不能接受。更稳妥的方案是:按键只负责把计数递增并写入一个内存变量,存档操作放到主循环的 10ms 周期任务里做,配合一个“脏标记”判断是否需要写。这个方案不关中断,也不丢数,缺点是断电瞬间可能有最多 10ms 的数据未落盘。对于按键次数统计这种场景,丢一次两次是能容忍的,但对不允许丢数的场景就得用“掉电检测 + 超级电容”的方案了。
3.4 页写跨边界回卷问题,一次写超过 8 字节时必现
AT24C02 有个页写特性:一次写操作最多可以连续写 8 字节,但如果跨过了页边界(即从地址 7 写到 8,或者从 15 写到 16,依此类推),数据并不会自动跨页连续写入,而是会回卷到当前页的开头,把之前写过的数据覆盖掉。
// 向 AT24C02 写入一组数据,自动处理页边界 // addr: 起始地址, buf: 数据缓冲区, len: 数据长度 void at24c02_write_page_safe(uint8_t addr, uint8_t *buf, uint16_t len) { while (len > 0) { // 计算当前页剩余空间 uint8_t page_remain = 8 - (addr % 8); uint8_t chunk = (len < page_remain) ? len : page_remain; i2c_start(); i2c_send_byte(0xA0); i2c_send_byte(addr); for (uint8_t i = 0; i < chunk; i++) { i2c_send_byte(buf[i]); } i2c_stop(); at24c02_wait_write_complete(); addr += chunk; buf += chunk; len -= chunk; } }上面的代码先把“当前地址所在页还剩多少字节”算出来,每次最多写到页边界就停,等写周期结束后再继续。注意addr % 8的求余运算在单片机上有一定的开销,但按键计数场景下不是热点路径,无所谓。这个函数用在你需要把“自上次清零以来的累积数据”批量写入时很有用,比如每分钟写一条带时间戳的记录到 EEPROM,做历史曲线回放。
4. 按键计数与消抖设计,边沿触发和“一直按着只算一次”是核心诉求
4.1 按键为什么必须消抖,机械抖动的时间尺度是多少
机械按键按下和释放的瞬间,触点会因为弹片的物理形变产生 5~20ms 的抖动,表现为一连串的高频通断信号。如果直接把 GPIO 的电平变化当成有效触发,一次物理按动会被识别成多次触发,计数会明显偏大。热词里的“按键一直按下只触发一次程序”说的就是这个场景——长按或者按住不松手时,程序必须只计一次数。
最常用的消抖方案是“延时确认法”:检测到电平变化后,延时 10~20ms,再次读取电平,如果和第一次相同,就认定为有效跳变。这个方案实现简单,缺点是延时期间 CPU 是阻塞的,如果系统还要处理其他任务就不合适了。进阶方案是用定时器扫描按键状态:每 5ms 扫描一次按键,连续读到 3 次相同状态才确认电平切换,这样既消抖又不用阻塞。
4.2 用状态机实现单次触发,长按不连发
实现“按一下只计一次数”的关键是检测“释放”而不是检测“按下”。如果按下时触发一次,那么抖动导致的电平反复会被消抖处理掉;但如果在消抖后仍然检测到低电平就触发,长按时会连续触发。
状态机思路是把这个过程拆成两个状态:松开态和按下态。只有从“松开态”迁移到“按下态”时才计数,在“按下态”内无论电平怎么变化(只要没恢复高电平并再次确认),都不计数。
// 按键状态机,边沿检测核心逻辑 // 返回值: 1 表示一次有效按键, 0 表示无事件 uint8_t key_scan_edge(void) { static uint8_t key_state = KEY_RELEASED; static uint8_t key_confirm = 0; uint8_t key_level = GPIO_ReadInputDataBit(KEY_PORT, KEY_PIN); uint8_t event = 0; // 消抖确认:连续多次读到相同电平才认为稳定 if (key_level == key_confirm) { switch (key_state) { case KEY_RELEASED: // 在松开态读到稳定低电平,说明按下,触发一次计数 if (key_level == KEY_PRESSED_LEVEL) { key_state = KEY_PRESSED; event = 1; // 只在状态迁移点产生事件 } break; case KEY_PRESSED: // 在按下态读到稳定高电平,说明释放,回到松开态 if (key_level == KEY_RELEASED_LEVEL) { key_state = KEY_RELEASED; } break; } } else { key_confirm = key_level; // 电平还在抖动,继续跟踪 } return event; }这段代码里的key_confirm变量做得是“最近一次读取电平”,key_state记录的是当前的稳定状态。每次扫描都做一次比较,只有连续两次读到相同电平时才认为状态稳定。这样 5ms 扫描一次,10ms 的抖动刚好能被过滤掉。定时器中断里每 5ms 调用一次key_scan_edge(),返回值放进一个队列,主循环处理计数。
注意:按键扫描中断里不要直接操作 I2C。I2C 时序对延时敏感,中断里插入 I2C 通信会让时序被高优先级中断打断,可能导致 SCL 高电平期间 SDA 变化,造成通信错误。正确做法是中断里只做状态机,把“需要加一”的标记置位,主循环处理计数和 EEPROM 写入。
4.3 短路保护:按键的 GPIO 也需要防护
热词里有“按键保护电路”和“按键电路”这两个查询,说明很多人调不通按键不是因为逻辑代码,而是硬件上缺少基本的防护。独立按键一端接 GPIO,另一端接地,GPIO 内部需要启用上拉电阻。如果按键引线较长,或者使用环境有静电风险,建议在 GPIO 对地并联一个 100nF 的电容,和消抖配合使用,硬件上把高频抖动先滤掉一层,软件消抖压力小很多。
另外,按键引线的在线长度也是独立的因素。如果按键和主板之间的连接线超过 20cm,建议不要用普通杜邦线,而是用双绞线或者带屏蔽的线缆,否则在电机、继电器等电磁干扰源附近,GPIO 上会感应出尖峰噪声,直接导致按键次数乱跳。这种问题靠软件消抖很难彻底解决,必须在硬件上处理。
5. 上电读出并显示到 1602LCD,存档地址规划和初始化顺序别搞反
5.1 1602LCD 的 4 位模式初始化时序,一步都不能少
1602LCD 上电后有大约 15ms 的内部复位时间,期间不能发送任何命令。我见过不少人在这个时序上翻车:上电后立刻发初始化命令,结果显示屏上全是乱码。标准做法是上电后延时 40ms 以上,然后再依次发送初始化序列。
// 1602LCD 4 位模式初始化 void lcd_init(void) { delay_ms(50); // 等待模块内部复位完成 // 设置为 8 位模式(前三次都是 0x30) lcd_write_cmd_4bit(0x03); delay_ms(5); lcd_write_cmd_4bit(0x03); delay_ms(5); lcd_write_cmd_4bit(0x03); delay_ms(5); // 切换为 4 位模式 lcd_write_cmd_4bit(0x02); delay_ms(2); // 以下指令都以 4 位模式发送完整命令 lcd_write_cmd(0x28); // 4 位模式,2 行,5x8 点阵 lcd_write_cmd(0x08); // 显示关闭 lcd_write_cmd(0x01); // 清屏 lcd_write_cmd(0x06); // 地址指针自动加一,写入后光标右移 lcd_write_cmd(0x0C); // 显示开启,光标关闭,不闪烁 }lcd_write_cmd_4bit(0x03)发送的是“高 4 位”,即一次只往数据线上送命令字节的高 4 位,低 4 位不管。前三次发 0x03 是让模块无论当前处于什么状态,都恢复到 8 位模式,然后发 0x02 切到 4 位模式。如果跳过前三次的 0x03 直接发 0x28,模块会因为在错误的总线宽度下工作而无法正确解析指令,显示自然不对。
5.2 存储区规划:地址 0x00 放计数主值,地址 0x01 放备份
AT24C02 有 256 字节,存一个按键次数(uint32_t,最大 42 亿次)只需要 4 字节。但如果你想做到“存储区域稳健”,就不能只写一份。EEPROM 有一个物理特性:每个字节的擦写寿命是 100 万次左右,如果你的系统每秒按一次键,4 字节的同一地址一天要写 86400 次,11 天就逼近寿命上限了。
简单做法:把计数用 4 字节存储在主地址 0x00~0x03,备份到 0x04~0x07。每次写入时先写主区,再写备份区。上电读取时先读主区并校验值范围,如果主区数据全是 0xFFFFFFFF 或 0x00000000(典型擦除态或损坏态),就尝试读备份区。这个策略不是最完善的,但能应对大部分写中断导致的数据损坏场景。
另一种做法是磨损均衡(wear leveling):把 256 字节全部分成 32 个槽位,每槽 8 字节,循环写入,同时用一个 2 字节的计数器记录当前写到哪个槽了。这样总寿命放大了 32 倍。对于工业计数器应用,这个方案在可靠性上明显更优,但代码会增加几十行,本项目的复杂度下可以不做,但关于磨损的概念应该留着,等以后产品化时用得上。
5.3 启动流程拼接:读 EEPROM → 显示初值 → 主循环响应按键
键计数的完整启动流程其实有个顺序问题。如果你先把 1602 初始化了再去读 EEPROM,用户看到的黑屏时间会比先读 EEPROM 再初始化 LCD 要长几十毫秒,体验不明显但确实有。更重要的是,EEPROM 的读操作不应放在 LCD 初始化之前,因为在 LCD 还没有初始化的阶段,IO 口处于默认状态,如果此时 EEPROM 的 SDA 引脚被复用或者存在上拉冲突,读出的数据可能不稳。
推荐的顺序是:先初始化 EEPROM 的 I2C 引脚和 LCD 的 GPIO 引脚 → 读 EEPROM 中的按键次数 → 初始化 1602LCD → 显示字符串 → 进入主循环。注意“初始化引脚”和“初始化显示器”是两回事,引脚上电时就可以配好,显示器的内部状态机要按 5.1 节的时序来。
int main(void) { uint32_t press_count = 0; // 1. 初始化 GPIO 和 I2C 引脚 gpio_init(); i2c_gpio_init(); // 2. 从 AT24C02 读取上次掉电前的按键次数 press_count = eeprom_read_counter(0x00); // 3. 初始化 1602LCD 并显示 lcd_init(); lcd_show_count(press_count); // 4. 主循环:扫描按键,更新计数,异步存档 while (1) { if (key_scan_edge() == 1) { press_count++; lcd_show_count(press_count); eeprom_write_flag = 1; // 置脏标记 } // 每 50ms 检查一次是否需要写入 EEPROM if (eeprom_write_flag && (get_tick() - last_save_tick > 50)) { eeprom_write_counter(0x00, press_count); eeprom_write_flag = 0; last_save_tick = get_tick(); } delay_ms(5); // 主循环节拍,与按键扫描定时器错开 } }这段代码里按键扫描写在主循环里,每 5ms 轮询一次,等效于前文状态机代码的调用方式。实际项目中如果把按键扫描放在定时器中断里,主循环也可以保持同样的结构,只需要把key_scan_edge()的返回值换成从队列里取。存档的 50ms 延零加上 EEPROM 写周期 5ms,最大掉电窗口是 55ms 左右。如果你担心这个窗口丢数,就进入临界区或使用掉电检测电路。
6. 一种更可靠的验证手段:写时间戳,按“读回比对”确认链路完整
6.1 用两个存储地址配合自检,区分“没写进去”和“没读出来”
这个项目的调试阶段,最让人头疼的问题就是:1602 上显示的数字不变化,到底是因为按键消抖没通过、计数变量没递增,还是 EEPROM 写入失败、LCD 显示逻辑出错?三层问题叠加在一起,靠肉眼观察很难定位。我的做法是在完成基本功能后,专门写一个“自检模式”:每次按键都在 EEPROM 的固定地址写当前系统运行时间戳(毫秒),下一次按键时先读上次的时间戳,再写新的。如果两次时间戳差值正常递增,说明按键、计数、EEPROM 写入、读出、LCD 显示整条链路都是通的;如果时间戳老是不变,问题一定在按键或写入环节。
时间戳方案还有一个额外的好处:它可以用来验证 EEPROM 的擦写寿命底数。对同一个地址按循环压测一万次写入,再读出校验,用统计数据判断当前芯片是否存在坏块或位翻转问题。注意 AT24C02 是 EEPROM,不是 FRAM,写周期是 5ms,一万次写大约要 50 秒,压测时要有耐心。
// 记录带时间戳的自检数据 // 返回 0 表示上一次的数据读回正确,1 表示数据不一致 uint8_t eeprom_selftest(uint32_t timestamp_ms) { uint8_t buf[6]; uint8_t readback[6]; // 布局: [0..1]=标志字, [2..5]=毫秒时间戳 buf[0] = 0x5A; buf[1] = 0xA5; buf[2] = (timestamp_ms >> 24) & 0xFF; buf[3] = (timestamp_ms >> 16) & 0xFF; buf[4] = (timestamp_ms >> 8) & 0xFF; buf[5] = timestamp_ms & 0xFF; // 写入后立即读回比对 at24c02_write_bytes(0x40, buf, 6); at24c02_read_bytes(0x40, readback, 6); for (int i = 0; i < 6; i++) { if (buf[i] != readback[i]) { return 1; // 不一致,说明写入或读取链路有问题 } } return 0; }这段代码把时间戳存到地址0x40(十进制 64),避开了前 8 字节的计数主区,也避开了常见的 EEPROM 首址。at24c02_write_bytes和at24c02_read_bytes是前文write_page_safe的多字节包装。调试完成后,你可以在产品出厂前把这段自检代码保留在固件里,用串口或一个隐藏按键组合触发,方便售后远程排查“存储是否坏了”的问题。
6.2 边界条件测试表:按键数和存储地址的极限组合
强中断和异常掉电的测试往往比功能验证更容易发现问题。下面是一个可以直接抄的验证清单:
| 测试场景 | 操作方式 | 预期结果 | 常见失败原因 |
|---|---|---|---|
| 按键消抖 | 用信号发生器模拟 10ms 低频抖动或快速点按 | 计数只加 1 | 消抖时间不够或边沿检测逻辑错误 |
| 断电保持 | 计数到 100,断电 5 秒后上电 | 显示 100 | 掉电瞬间主循环尚未存档 |
| 页写跨边界 | 从地址 7 连续写 4 字节 | 写入正确且不回卷覆盖 | page_remain计算错误 |
| 写周期等待 | 连续快速写两个字节,间隔 1ms | 第二次写成功 | 没有等待 ACK,器件忙导致的写入丢失 |
| 地址冲突 | 总线上同时挂两个 AT24C02,A2 电平不同 | 两个芯片都能独立读写 | 从机地址未正确区分 |
| LCD 初始化 | 反复上电 100 次 | 每次都能正常显示,无乱码 | 上电延时不足或初始化序列错位 |
以上清单里,最容易踩的是“断电保持”这一条。很多人测试时是正常断电,然后等几秒再上电,结果发现计数丢失,立刻就怀疑 EEPROM 坏了。实际上问题出在主循环的存档节拍上——你的程序可能每 50ms 才写一次,断电瞬间正好落在两次存档之间,自然会丢。这种丢数不是 EEPROM 的写入可靠性问题,而是存档策略没有满足“实时性”要求。判断方法很简单:把存档周期改到 10ms 再测,如果丢数概率明显下降,说明策略问题而不是硬件问题。如果还是丢,就得考虑用掉电检测中断 + 储能电容来保证最后这一次写入的供电裕量。
6.3 用“F1 键”思维简化操作:热词里的按键切换在自检中的应用
热搜词里有“F1 按键怎么切换”和“双手打字按键位置”这样无关嵌入式的内容,但“按键切换”这个思路本身是值得做进这个项目的:如果你有多个按键,完全可以给系统增加一个模式切换能力。比如设计三个按键:KEY_A负责计数,KEY_B负责清零(长按 3 秒清零并从 0 重新开始显存),KEY_C负责在“显示计数 / 显示自检信息”两个界面之间切换。
这个做法看似在扩功能,实际上对调试很有用:不需要改代码重新烧录,就能在同一个板子上观察不同阶段的运行状态。按键切换配合 1602LCD 的显示,比如第一行显示计数CNT: 000123,第二行显示自检状态EEP: OK,一眼就能看出系统在哪一层出了问题。如果你在 STM32 上做,加了中断做按键检测也不怕长按,因为中断里只置位标志,模式切换在主循环完成,不会因为长按导致反复触发。
本文还有配套的精品资源,点击获取