从硬件连接、按键消抖到首按锁存与 Proteus 仿真
最终效果 上电后数码管显示"-";任意选手按下 K1~K8 后,系统显示对应编号 1~8 并锁定结果;主持人按下 START/RESET 后进入下一轮。
适用器件:AT89C51 / AT89C52 / STC89C52 系列
开发环境:Keil C51 + Proteus
目录
[1 功能要求与总体方案](#1 功能要求与总体方案)
[1.1 功能要求](#1.1 功能要求)
[1.2 系统结构](#1.2 系统结构)
[2 元器件与端口分配](#2 元器件与端口分配)
[2.1 元器件清单](#2.1 元器件清单)
[2.2 端口分配表](#2.2 端口分配表)
[3 硬件电路设计](#3 硬件电路设计)
[3.1 51 单片机最小系统](#3.1 51 单片机最小系统)
[3.2 8 路按键输入电路](#3.2 8 路按键输入电路)
[3.3 主持人 START/RESET 键](#3.3 主持人 START/RESET 键)
[4 数码管显示电路](#4 数码管显示电路)
[4.1 共阳极数码管的逻辑](#4.1 共阳极数码管的逻辑)
[4.2 段码表](#4.2 段码表)
[4.3 限流与负载注意](#4.3 限流与负载注意)
[5 Proteus 原理图搭建](#5 Proteus 原理图搭建)
[5.1 放置元件](#5.1 放置元件)
[5.2 接线自检](#5.2 接线自检)
[6 软件设计](#6 软件设计)
[6.1 状态与变量](#6.1 状态与变量)
[6.2 消抖方法](#6.2 消抖方法)
[6.3 控制流程](#6.3 控制流程)
[7 完整 C51 程序](#7 完整 C51 程序)
[7.1 端口、段码与基础函数](#7.1 端口、段码与基础函数)
[7.2 按键译码与消抖](#7.2 按键译码与消抖)
[7.3 主函数:锁存与复位](#7.3 主函数:锁存与复位)
[7.4 关键语句解释](#7.4 关键语句解释)
[8 Keil 工程建立与编译](#8 Keil 工程建立与编译)
[8.1 编译前检查](#8.1 编译前检查)
[8.2 推荐的工程目录](#8.2 推荐的工程目录)
[9 Proteus 仿真步骤](#9 Proteus 仿真步骤)
[9.1 预期现象](#9.1 预期现象)
[10 测试与验收](#10 测试与验收)
[10.1 最小测试集](#10.1 最小测试集)
[10.2 验收结论](#10.2 验收结论)
[11 常见故障与排查](#11 常见故障与排查)
[12 设计改进与扩展](#12 设计改进与扩展)
[13 实验总结](#13 实验总结)
[附录 速查表](#附录 速查表)
使用说明与学习目标
教程默认使用一个共阳极一位数码管显示获胜者编号,8 个选手按键占用 P0 口,数码管占用 P1 口,主持人复位键接 P3.2。
完成后你应能做到
- 解释"扫描---消抖---判定---锁存---复位"的完整控制过程。
- 正确连接 51 单片机最小系统、P0 外接上拉、共阳极数码管与限流电阻。
- 在 Keil 中编译 C51 程序并生成 HEX 文件。
- 在 Proteus 中完成仿真,验证 8 个按键、锁定和复位功能。
- 根据故障现象判断是按键悬空、段码极性、端口映射还是复位逻辑出错。
学习路线
| 阶段 | 主要任务 | 可验证结果 |
|---|---|---|
| 1. 方案 | 确定输入、输出和锁存规则 | 能画出系统框图 |
| 2. 硬件 | 连接最小系统、8 个按键和数码管 | 端口电平关系明确 |
| 3. 软件 | 编写消抖、编号译码和状态锁存 | 程序能够生成 HEX |
| 4. 仿真 | 加载 HEX 并执行测试用例 | 首按有效、其余锁定、复位恢复 |
编号约定 **:**K1 接 P0.0,K2 接 P0.1,依次类推,K8 接 P0.7。
1 功能要求与总体方案
1.1 功能要求
| 功能 | 要求 | 实现方式 |
|---|---|---|
| 待机提示 | 上电或复位后显示"-" | P1 输出共阳极横杠段码 0xBF |
| 8 路输入 | K1~K8 任一路可参与抢答 | P0.0~P0.7 低电平有效 |
| 首按判定 | 只记录第一位有效选手 | 消抖后译码并写入 winner |
| 结果锁定 | 后续选手按键无效 | winner 非 0 时停止扫描选手键 |
| 新一轮 | 主持人可清除结果 | P3.2 的 START/RESET 键将 winner 清零 |
1.2 系统结构

图 1 8 路抢答器总体结构
核心思想是把"是否已经有人抢答"保存为一个状态变量 winner。winner 等于 0 时允许扫描选手键;winner 变为 1~8 后立即显示编号并停止接收新的选手输入;只有主持人复位才能把 winner 重新清零。
同时按下怎么办? 单片机并不能判断物理上绝对同时的先后。本程序按 P0.0→P0.7 的顺序译码,因此同一采样周期内多键同时为低电平时,编号较小者优先。
2 元器件与端口分配
2.1 元器件清单
| 元器件 | 建议规格 | 数量 | 作用 |
|---|---|---|---|
| 51 单片机 | AT89C52 / STC89C52 | 1 | 完成按键扫描、锁存和显示控制 |
| 一位数码管 | 共阳极 7 段 | 1 | 显示 1~8 或"-" |
| 选手按键 | 常开轻触开关 | 8 | K1~K8,按下接地 |
| 主持人按键 | 常开轻触开关 | 1 | START/RESET,接 P3.2 |
| P0 上拉电阻 | 4.7 kΩ~10 kΩ | 8 | 为 P0 提供稳定高电平 |
| 段限流电阻 | 220 Ω~330 Ω | 8 | 限制数码管段电流 |
| 晶振与电容 | 12 MHz;22~33 pF | 1+2 | 构成单片机时钟 |
| 复位元件 | 10 μF、10 kΩ、按键 | 各 1 | 构成上电与硬件复位 |
| 去耦电容 | 0.1 μF 陶瓷 | 1 | 抑制电源瞬态干扰 |
| 电源 | +5 V 稳压 | 1 | 为系统供电 |
2.2 端口分配表
| 端口 | 连接对象 | 有效电平 / 方向 | 说明 |
|---|---|---|---|
| P0.0~P0.7 | K1~K8 | 输入,低有效 | 每一路必须外接上拉电阻 |
| P1.0~P1.7 | a、b、c、d、e、f、g、dp | 输出,低电平点亮 | 每个段串联限流电阻 |
| P3.2 | START/RESET | 输入,低有效 | 主持人清除锁存结果 |
| RST | 硬件复位网络 | 高电平复位 | 与主持人复位不是同一功能 |
两个 " 复位 " 要区分 RST 引脚用于重启整个单片机;P3.2 的 START/RESET 只清除本轮抢答结果。做简化版时可以取消 P3.2,直接用硬件复位开始下一轮,但连续操作不够方便。
3 硬件电路设计
3.1 51 单片机最小系统
- 电源:VCC 接 +5 V,GND 接地;在 VCC 与 GND 附近并联 0.1 μF 去耦电容。
- 时钟:12 MHz 晶振跨接 XTAL1 与 XTAL2,晶振两端各用 22~33 pF 电容接地。
- 复位:RST 通过 10 kΩ 电阻接地、通过 10 μF 电容接 +5 V,并用按键将 RST 直接接 +5 V。
- 程序存储器:EA 接 +5 V,表示使用片内程序存储器。
3.2 8 路按键输入电路
P0 口是漏极开路结构,作为普通输入时没有像 P1/P2/P3 那样的内部上拉能力。因此每个 P0 引脚都要通过 4.7 kΩ~10 kΩ 电阻接 +5 V,按键另一端接地。未按下时读取 1,按下时读取 0。
| 按键状态 | P0 引脚电平 | 读取位值 | 程序含义 |
|---|---|---|---|
| 未按下 | 被上拉至 +5 V | 1 | 没有人抢答 |
| 按下 | 通过按键接地 | 0 | 该路发出抢答请求 |
例如 K3 接 P0.2。K3 按下时 P0 的理想读数是 11111011B,即 0xFB。程序不要求整个字节必须等于某个固定值,而是逐位检查,因此同时按下多个键也能给出确定的优先级。
3.3 主持人 START/RESET 键
将 P3.2 设为输入(向该位写 1),按键按下时接地。程序检测到稳定低电平后,把 winner 清零、数码管重新显示"-",并等待主持人键和全部选手键释放,再进入下一轮。
4 数码管显示电路
4.1 共阳极数码管的逻辑
共阳极数码管的公共端接 +5 V,各段阴极经限流电阻连接 P1。当 P1 某位输出 0 时,对应段导通点亮;输出 1 时熄灭。因此,段码与"1 点亮"的共阴极数码管相反。

图 2 P1 与共阳极数码管的段线映射
4.2 段码表
| 显示 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | - |
|---|---|---|---|---|---|---|---|---|---|
| 共阳极段码 | 0xF9 | 0xA4 | 0xB0 | 0x99 | 0x92 | 0x82 | 0xF8 | 0x80 | 0xBF |
段码成立的前提 **:**上表假定 P1.0~P1.7 依次连接 a、b、c、d、e、f、g、dp。若你的元件引脚顺序不同,数码管会显示乱码,需要按实际连线重新计算段码。
4.3 限流与负载注意
- 每一个段都要单独串联 220 Ω~330 Ω 电阻,不能只在公共端串一个电阻。
- 本实验只驱动一位数码管,静态显示即可,不需要动态扫描。
- 实物若亮度要求较高或驱动多位数码管,应增加三极管或专用驱动芯片,不要:让单片机端口长期承受过大电流。
5 Proteus 原理图搭建
5.1 放置元件
- 新建原理图,放置 AT89C52、CRYSTAL、CAP、RES、BUTTON、共阳极一位数码管和电源端子。
- 完成 +5 V、GND、EA、晶振和硬件复位网络,先保证最小系统完整。
- 把 K1~K8 分别接 P0.0~P0.7,每一路放置独立上拉电阻,按键另一端接地。
- 把 P1.0~P1.7 经 220 Ω~330 Ω 电阻接数码管 a~dp,共阳极端接 +5 V。
- 把主持人 START/RESET 键接 P3.2,另一端接地。

图 3 所附实验报告中的 Proteus 连接结构(元件参数以本教程为准)
5.2 接线自检
- P0 的 8 根输入线是否都存在上拉电阻,且按键按下时能接地。
- 数码管是否选择共阳极型号,公共端是否接 +5 V。
- P1 与 a~dp 的顺序是否与段码表完全一致。
- EA 是否接 +5 V,晶振频率是否与 Keil 工程设置一致。
- 主持人键是否接 P3.2,而不是误接到某一路选手键。
6 软件设计
6.1 状态与变量
| 对象 | 取值 | 含义 |
|---|---|---|
| winner | 0 | 待机,可扫描 K1~K8 |
| winner | 1~8 | 已锁定对应选手,忽略其他选手键 |
| P0 | 0xFF | 全部选手键释放 |
| P1 | 0xBF | 显示"-" |
| P1 | seg_cawinner | 显示获胜编号 |
6.2 消抖方法
机械按键刚闭合时会在 0 和 1 之间快速跳变。程序先读取一次 P0,发现不是 0xFF 后延时约 10 ms,再读取一次;两次读数一致才认为按键有效。这个方法适合教学和低速人机输入。对精确定时或高可靠系统,建议改用定时器周期扫描。
6.3 控制流程

图 4 抢答器的软件状态与复位路径
7 完整 C51 程序
下面的程序适用于 Keil C51,默认 12 MHz 晶振、共阳极数码管和本教程的端口连接。简单循环延时只用于按键消抖,精度不作为实验验收指标。
7.1 端口、段码与基础函数
cpp
#include <reg52.h>
typedef unsigned char u8;
typedef unsigned int u16;
#define KEY_PORT P0
#define SEG_PORT P1
#define SEG_DASH 0xBF
sbit START_KEY = P3^2;
/* Index 1..8 is used; index 0 is unused here. */
u8 code seg_ca[9] = {
0xC0, 0xF9, 0xA4, 0xB0, 0x99,
0x92, 0x82, 0xF8, 0x80
};
/* Approximate millisecond delay at 12 MHz. */
void delay_ms(u16 ms)
{
u16 i;
while (ms--)
{
for (i = 0; i < 110; i++);
}
}
7.2 按键译码与消抖
cpp
u8 decode_first_key(u8 value)
{
if ((value & 0x01) == 0) return 1; /* K1 */
if ((value & 0x02) == 0) return 2; /* K2 */
if ((value & 0x04) == 0) return 3; /* K3 */
if ((value & 0x08) == 0) return 4; /* K4 */
if ((value & 0x10) == 0) return 5; /* K5 */
if ((value & 0x20) == 0) return 6; /* K6 */
if ((value & 0x40) == 0) return 7; /* K7 */
if ((value & 0x80) == 0) return 8; /* K8 */
return 0;
}
u8 read_key(void)
{
u8 first;
u8 second;
first = KEY_PORT;
if (first == 0xFF) return 0;
delay_ms(10);
second = KEY_PORT;
if (second != first) return 0;
return decode_first_key(second);
}
7.3 主函数:锁存与复位
cpp
void main(void)
{
u8 winner = 0;
KEY_PORT = 0xFF; /* Release P0 for input. */
START_KEY = 1; /* Configure P3.2 as input. */
SEG_PORT = SEG_DASH; /* Ready indication. */
while (1)
{
/* Host starts a new round. */
if (START_KEY == 0)
{
delay_ms(10);
if (START_KEY == 0)
{
winner = 0;
SEG_PORT = SEG_DASH;
while (START_KEY == 0);
while (KEY_PORT != 0xFF);
delay_ms(10);
}
}
/* Only scan players before a winner is latched. */
if (winner == 0)
{
winner = read_key();
if (winner != 0)
{
SEG_PORT = seg_ca[winner];
}
}
}
}
7.4 关键语句解释
- KEY_PORT = 0xFF:P0 写 1 后处于释放状态,再由外部上拉电阻提供稳定高电平。
- decode_first_key:从低位到高位检查,因此多键同采样时 K1 的优先级最高。
- winner == 0:只有在尚未产生结果时才扫描选手键,实现软件锁存。
- SEG_PORT = seg_cawinner:winner 直接作为数组下标,显示编号 1~8。
- 等待全部选手键释放:避免主持人开始下一轮时,上一轮仍按住的选手立即再次抢答。
如果只使用硬件复位 **,**可以删除 P3.2 复位判断,并在显示获胜编号后停在循环中;按下 RST 重新上电即可回到"-"。
8 Keil 工程建立与编译
- 打开 Keil uVision,选择 Project → New uVision Project,建立工程目录。
- 在器件列表中选择 Atmel AT89C52(或与你的芯片兼容的 8051/8052 器件)。
- 新建 main.c,把完整程序粘贴进去,并通过 Add Existing Files to Group 添加到 Source Group 1。
- 打开 Options for Target,在 Target 页把晶振频率设置为 12.0 MHz。
- 在 Output 页勾选 Create HEX File,确认生成文件目录。
- 点击 Build;出现 0 Error(s) 后,在 Objects 目录找到 .hex 文件。
8.1 编译前检查
- 文件扩展名必须是 .c,不要误保存为 .txt。
- 头文件使用 reg52.h;若选择的是其他厂商芯片,可按器件支持包调整头文件。
- sbit START_KEY = P3^2; 是 Keil C51 语法,不能直接用普通桌面 C 编译器编译。
- 若改变晶振频率,软件逻辑仍可工作,但 delay_ms 的实际时间会随之改变。
8.2 推荐的工程目录
bash
Responder8/
|-- main.c
|-- Responder8.uvproj
|-- Objects/
| |-- Responder8.hex
|-- Listings/
生成 HEX 是关键 Proteus 加载的是编译后的 .hex 文件,不是 main.c。修改程序后必须重新 Build,Proteus 才能使用新逻辑。
9 Proteus 仿真步骤
- 双击 AT89C52,在 Program File 中选择 Keil 生成的 Responder8.hex。
- 把 Clock Frequency 设置为 12 MHz,与 Keil 工程和晶振保持一致。
- 启动仿真,确认数码管首先显示"-"。
- 依次单击 K1~K8,观察显示是否与按键编号一致;每次测试前按 START/RESET。
- 在已显示一个编号后再按其他选手键,显示应保持不变。
- 按下 START/RESET,释放全部按键后,数码管重新显示"-",进入新一轮。
9.1 预期现象
| 操作 | P0 典型值 | 数码管 | 系统状态 |
|---|---|---|---|
| 上电 | 0xFF | - | 等待抢答 |
| K1 按下 | 0xFE | 1 | 锁定 K1 |
| K4 按下 | 0xF7 | 4 | 锁定 K4 |
| K8 按下 | 0x7F | 8 | 锁定 K8 |
| 锁定后按其他键 | 任意 | 保持原编号 | 仍锁定 |
| 按 START/RESET | 等待释放后 0xFF | - | 新一轮 |
不要只测一个按键 8 路系统最常见的问题是个别线路接错。必须逐个测试 K1~K8,并额外测试"锁定后继续按键"和"复位后重新抢答"。
10 测试与验收
10.1 最小测试集
| 编号 | 测试步骤 | 通过标准 | 结果 |
|---|---|---|---|
| T01 | 上电,不按任何键 | 稳定显示"-" | □ 通过 □ 失败 |
| T02 | 分别让 K1~K8 首先按下 | 分别显示 1~8 | □ 通过 □ 失败 |
| T03 | K3 获胜后再按 K6 | 继续显示 3 | □ 通过 □ 失败 |
| T04 | 按 START/RESET | 清除结果并显示"-" | □ 通过 □ 失败 |
| T05 | 复位时保持某选手键按下 | 等待其释放,不立即误抢答 | □ 通过 □ 失败 |
| T06 | 近似同时按 K2 与 K5 | 按设定优先级显示 2 | □ 通过 □ 失败 |
| T07 | 快速轻触任意选手键 | 不乱码、不重复跳变 | □ 通过 □ 失败 |
10.2 验收结论
当 T01~T07 全部通过,说明电源与最小系统、8 路输入、数码管输出、消抖、首按锁存和主持人复位均满足设计要求。若只验证"某一个键能点亮某一个数字",不能视为完成 8 路抢答器验收。
11 常见故障与排查
| 现象 | 常见原因 | 排查与处理 |
|---|---|---|
| 数码管全灭 | 共阳/共阴型号用错;公共端未接 +5 V | 核对器件型号与公共端,确认低电平点亮 |
| 显示乱码或段错位 | P1 与 a~dp 顺序不一致 | 逐段点亮测试,按实际连线重排段码 |
| 未按键却随机抢答 | P0 没有外接上拉或地线悬空 | 检查 8 个上拉电阻和公共地 |
| 只有部分按键有效 | 某路按键、上拉或 P0 连接错误 | 用逻辑探针检查未按为 1、按下为 0 |
| 显示 0~7 而不是 1~8 | 沿用了原报告的数组下标 | 使用 seg_ca1~seg_ca8 并返回编号 1~8 |
| 复位后立即又锁定 | 上一轮选手键仍被按住 | 等待 P0 恢复 0xFF 后再开始扫描 |
| Proteus 现象未更新 | 修改代码后未重编译或未重新加载 HEX | Build 工程并确认 Program File 路径 |
| 实物反复重启 | 电源不稳、复位网络或去耦不当 | 检查 +5 V、RST 波形并增加 0.1 μF 去耦 |
12 设计改进与扩展
这些扩展大家可以试试去实现。
| 扩展方向 | 增加内容 | 软件变化 |
|---|---|---|
| 蜂鸣提示 | P2.0 接三极管驱动蜂鸣器 | 锁存获胜者时鸣叫 0.2~1 s |
| 倒计时 | 增加两位或四位数码管 | 用定时器中断做动态扫描与秒计时 |
| 违规抢答 | 主持人增加"开始"状态 | 开始前按键则记录违规者并报警 |
| 公平性提升 | 外部中断或并行锁存器 | 降低顺序扫描造成的固定优先级 |
| 记录多轮结果 | EEPROM / 串口 / LCD | 保存并显示历史获胜编号 |
| 按键节省 IO | 矩阵键盘或 74HC165 | 改为行列扫描或串行读取 |
13 实验总结
8 路抢答器把单片机实验中最常见的几个知识点串联起来:P0 的电气特性、按键低电平输入、软件消抖、数组段码、端口静态输出以及状态锁存。真正决定系统是否可靠的并不是 switch 或 if 的形式,而是输入是否稳定、编号映射是否一致、获胜状态是否只写入一次,以及复位后是否能干净地回到待机。
按照本教程完成后,系统应具有清晰且可重复验证的行为:待机显示"-",第一位有效选手被显示并锁定,后续按键不能改变结果,主持人复位后重新开始。这个结构还可以自然扩展到蜂鸣器、倒计时、违规检测和多位显示。
最终检查 逐项确认:P0 外接上拉、共阳极段码、1~8 编号、10 ms 消抖、winner 锁存、P3.2 复位、HEX 已重新编译并加载。
附录 速查表
| 项目 | 速查内容 |
|---|---|
| 选手键 | K1~K8 → P0.0~P0.7;低电平有效;每路 4.7 kΩ~10 kΩ 上拉 |
| 主持人键 | START/RESET → P3.2;低电平有效 |
| 显示端口 | P1.0~P1.7 → a、b、c、d、e、f、g、dp;每段 220 Ω~330 Ω |
| 待机段码 | 共阳极"-" = 0xBF |
| 获胜段码 | 1~8 = F9、A4、B0、99、92、82、F8、80 |
| 关键状态 | winner = 0 可抢答;winner = 1~8 已锁定 |
| 优先级 | 同一采样周期多键同时按下时,P0.0 优先于 P0.7 |