基于AT89S52的数码管按键计数器设计:解决按键消抖与蜂鸣器延时的显示闪烁问题

0 阅读6分钟

基于AT89S52的数码管按键计数器设计:解决按键消抖与蜂鸣器延时的显示闪烁问题

摘要

本文介绍了基于AT89S52单片机的数码管动态显示与独立按键控制系统的设计。针对单片机裸机开发中“延时阻塞导致数码管闪烁”的经典问题,提出了一种“分时复用”的代码架构。通过编写带刷新功能的延时函数,实现了在按键消抖和蜂鸣器长鸣期间,数码管依然保持稳定显示的效果。文章包含完整原理图解、单文件源码及工程模块化源码。


一、 实验要求

本次实验基于AT89S52,主要功能如下:

1. 基础功能

  • 显示内容:两位数码管显示 00 - 19 的数值。
  • 按键交互
    • K0 (加法):数值 +1,上限 19。
    • K1 (减法):数值 -1,下限 00。

2. 扩展功能 (核心难点)

  • 交互反馈
    • 数值正常变化:蜂鸣器响 0.5秒
    • 触碰边界(19再加或00再减):蜂鸣器报警 1秒
  • 性能指标
    • “全过程无闪烁”:无论是按键按下的20ms消抖期,还是蜂鸣器鸣响的1000ms报警期,数码管必须保持常亮,不得熄灭或闪烁。

二、 代码逻辑深度解读

1. 为什么普通代码会闪烁?

数码管动态显示依赖于人眼的视觉暂留(Visual Persistence),通常需要每隔几毫秒扫描一次。 如果使用传统的 delay_ms(1000) 来控制蜂鸣器,CPU 会在一个死循环里空转 1秒钟。这 1秒钟内,数码管得不到扫描信号,自然就熄灭了。

2. 解决方案:带显示的“智能延时”

我们设计一个核心函数 delay_with_display(ms)。 它的逻辑不再是“傻等”,而是“在等待的间隙干活”。

  • 算法:将长延时切分成无数个小片段(例如 4ms)。
  • 执行:每经过一个小片段,就调用一次数码管刷新函数。
  • 结果:宏观上完成了延时,微观上数码管一直在刷新。

3. 程序执行流程图

graph TD
    Start(系统上电) --> Init["初始化IO口与变量"]
    Init --> MainLoop{主循环}
    
    MainLoop -->|常态| Display[刷新数码管显示]
    MainLoop --> CheckKey[检测按键状态]
    
    CheckKey --无动作--> MainLoop
    CheckKey --有按键按下--> Debounce["智能消抖<br/>Delay_With_Display"]
    
    Debounce --> Confirm{确认按下?}
    Confirm --误触--> MainLoop
    Confirm --有效--> Action[数值加减处理]
    
    Action --> Beep["蜂鸣器响铃<br/>Delay_With_Display"]
    Beep --> WaitRelease["等待松手<br/>期间持续刷新显示"]
    WaitRelease --> MainLoop


三、 硬件端口与功能定义

变量名对应IO口功能说明
DIG_SEG_CTRLP2.6数码管段选锁存 (DU)
DIG_BIT_CTRLP2.7数码管位选锁存 (WE)
P0P0.0-P0.7数据总线 (传输段码/位码)
BUZZERP2.3蜂鸣器 (低电平触发)
KEY_ADDP3.4加法按键 (K0)
KEY_SUBP3.5减法按键 (K1)

四、 完整版代码 (单文件便于调试)

/******************************************************************
 * Project: 基于AT89S52的数码管按键计数器
 * Author:  名字太难起了QAQ
 * MCU:     AT89S52 @ 11.0592MHz
 * Date:    2026-02-16
 * Description: 解决延时阻塞导致的数码管闪烁问题
 ******************************************************************/
#include <reg52.h>

typedef unsigned char uint8;
typedef unsigned int  uint16;

// --- 硬件定义 ---
sbit DIG_SEG_CTRL = P2^6;
sbit DIG_BIT_CTRL = P2^7;
sbit BUZZER       = P2^3;
sbit KEY_ADD      = P3^4;
sbit KEY_SUB      = P3^5;

// 共阴极段码表
code uint8 DIG_SEG_TABLE[10] = {0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,0x7F,0x6F};
uint8 display_num = 0;  

// --- 基础延时 ---
void delay_1ms(uint16 ms)
{
    uint16 i, j;
    for(i = 0; i < ms; i++)
        for(j = 0; j < 110; j++);
}

// --- 数码管刷新 ---
void digital_tube_display(void)
{
    uint8 tens = display_num / 10;
    uint8 units = display_num % 10;

    // 显示十位
    P0 = DIG_SEG_TABLE[tens]; DIG_SEG_CTRL = 1; DIG_SEG_CTRL = 0;
    P0 = 0xFE; DIG_BIT_CTRL = 1; DIG_BIT_CTRL = 0;
    delay_1ms(2);
    P0 = 0x00; DIG_SEG_CTRL = 1; DIG_SEG_CTRL = 0; // 消隐

    // 显示个位
    P0 = DIG_SEG_TABLE[units]; DIG_SEG_CTRL = 1; DIG_SEG_CTRL = 0;
    P0 = 0xFD; DIG_BIT_CTRL = 1; DIG_BIT_CTRL = 0;
    delay_1ms(2);
    P0 = 0x00; DIG_SEG_CTRL = 1; DIG_SEG_CTRL = 0; // 消隐
}

// --- [核心] 带显示的延时 ---
void delay_with_display(uint16 ms)
{
    // 每次刷新约耗时4ms,循环次数 = ms/4
    uint16 i = ms / 4;
    while(i--)
    {
        digital_tube_display();
    }
}

// --- 报警控制 ---
void buzzer_alarm(uint16 ms)
{
    BUZZER = 0;             // 响
    delay_with_display(ms); // 边响边显示
    BUZZER = 1;             // 停
}

void main(void)
{
    DIG_SEG_CTRL = 0; DIG_BIT_CTRL = 0; BUZZER = 1;

    while(1)
    {
        digital_tube_display(); // 常态显示

        // 加法处理
        if(KEY_ADD == 0)
        {
            delay_with_display(20); // 边消抖边显示
            if(KEY_ADD == 0)
            {
                if(display_num == 19) buzzer_alarm(1000);
                else { display_num++; buzzer_alarm(500); }
                while(KEY_ADD == 0) digital_tube_display(); // 边等待边显示
            }
        }

        // 减法处理
        if(KEY_SUB == 0)
        {
            delay_with_display(20);
            if(KEY_SUB == 0)
            {
                if(display_num == 0) buzzer_alarm(1000);
                else { display_num--; buzzer_alarm(500); }
                while(KEY_SUB == 0) digital_tube_display();
            }
        }
    }
}


五、 模块化代码 (工程进阶)

为了符合“工程规范”,我们将代码拆分为多个文件。这在大型项目中是必须的。

1. display.c (负责底层显示驱动)

#include "display.h"
#include <reg52.h>

// 引用外部变量
extern unsigned char display_num;
code unsigned char DIG_SEG_TABLE[10] = {0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,0x7F,0x6F};

sbit DIG_SEG = P2^6;
sbit DIG_BIT = P2^7;

void System_Show_Num(void)
{
    // ...此处填入 digital_tube_display 的具体实现...
    // 注意:只负责刷新一次数码管
}

2. sys_utils.c (负责智能延时)

这是解决闪烁的关键层,它调用 display.c

#include "sys_utils.h"
#include "display.h" 

// 智能延时:延时的同时刷新屏幕
void Delay_And_Refresh(unsigned int ms)
{
    unsigned int i = ms / 4; 
    while(i--)
    {
        System_Show_Num(); // 调用显示层的函数
    }
}

3. main.c (业务逻辑层)

/******************************************************************
 * Project: Modular Counter
 * Author:  名字太难起了QAQ
 ******************************************************************/
#include <reg52.h>
#include "display.h"
#include "sys_utils.h"

sbit KEY_ADD = P3^4;
// ...其他定义...

void main()
{
    while(1)
    {
        System_Show_Num(); // 常态刷新
        
        if(KEY_ADD == 0)
        {
            Delay_And_Refresh(20); // 模块化调用消抖
            if(KEY_ADD == 0)
            {
                // ... 业务逻辑 ...
                // 报警时调用 Delay_And_Refresh(500);
            }
        }
    }
}


六、 易错点与注意事项

  1. 鬼影问题 (Ghosting)
  • 现象:数字虽然显示正确,但未点亮的段隐约发光,或者下一位的数字重叠在当前位。
  • 原因:位选切换太快,段选数据还未更新。
  • 解决:必须在代码中加入 “消隐” 步骤,即在显示完一位后,执行 P0=0x00 并锁存,再进行下一位的显示。
  1. 死循环延时陷阱
  • 现象:按下按键后,数码管黑屏一下再亮。
  • 解决:严禁在 main 循环中使用普通的 delay_ms() 处理超过 10ms 的逻辑。必须使用文中提供的 delay_with_display()
  1. 按键灵敏度
  • 如果发现按键反应迟钝,可以适当减小 delay_with_display(20) 中的参数,例如改为 10ms。
  1. 硬件冲突
  • 部分开发板(如普中)的 LED 流水灯与数码管共用 P0 口。如果数码管显示乱码,请检查是否插着 LED 流水灯的跳线帽,建议拔除。