51单片机六路抢答器设计:状态机与定时器中断实战
简介这是一份基于51单片机的抢答器设计实训资料包面向单片机课程设计、电子竞赛备赛及嵌入式硬件初学者完整覆盖从任务要求、电路设计、程序编写到实物焊接调试的全流程。资源共19个文件1.29MB以docx/doc设计文档、Keil工程文件uvproj、C源码与hex烧录文件、zip程序备份及txt说明为主既可直接参考原理图与源码也可按文档复现电路板制作。内容围绕主持人控制、6路抢答、数码管显示、5秒抢答倒计时、20秒答题计时及蜂鸣器提示等核心功能展开并附有扩展的按键调时功能程序适合用于课程验收或实训报告撰写。压缩包中包含源程序、参考电路原理图、总结文档解析和实物图较完整地呈现了设计思路与实现细节。该资源已有1665人学习下载对正在做单片机课设或想快速上手抢答器项目的读者有较高参考价值。1. 抢答器的核心矛盾六路并发按键、状态锁定与数码管反馈一个很反直觉的事实六路按键的抢答器绝大多数人做出来“能用”但不好用。主持人按下开始数码管从 5 往 0 倒计时这时候两路选手同时把键拍下去最终显示的是哪一个很多方案在这里会随机、会闪烁甚至直接归零。这套基于 51 单片机的抢答器设计资源包包含 Keil 源码、参考电路、实物图、题目要求以及基本版和调时版两套程序目标是让 6 路抢答、主持人控制开关、5 秒抢答窗口、20 秒答题倒计时、蜂鸣器提示、声光超时提醒全部落到一块自己焊的板子上。适合做单片机课程设计或电子实训、需要交实物和报告的人也适合想从原理图反推状态机怎么设计的开发者。2. 硬件电路设计引脚分配、数码管驱动与下载接口的取舍拿到题目不要先写代码先把“功能需求”翻译成“硬件模块”。题目里有 6 路抢答按钮、主持人开关、选手号显示数码管、倒计时双数码管、蜂鸣器、声光提示、下载接口下面一一对应到外设上。2.1 题目要求到硬件模块的映射先看这份资源里“抢答器设计题目要求.docx”列出的硬指标我整理成下面这张对照表后面选型都围绕它展开功能需求对应硬件方案说明6 路抢答按钮6 个独立按键接 P1.0~P1.5抢答并发性高必须用独立 IO 而不是矩阵扫描主持人控制开关拨动开关或按键接 P1.6只允许主持人操作放在离选手远的一侧显示选手号码1 位共阳数码管接 P0 口灌电流方式驱动P0 必须加上拉排阻5 秒抢答倒计时1 位数码管复用显示和选手号共用同一只管子20 秒答题倒计时2 位共阳数码管接 P0 口动态扫描位选由 P2.0~P2.2 经三极管驱动蜂鸣器响一下有源蜂鸣器接 P2.7NPN 三极管驱动抢答成功短响一声超时连续响声光超时提示LED 接 P2.6 配合蜂鸣器答题时间归零时同时动作下载接口4Pin 排针引出 P3.0/P3.1/VCC/GND兼容 STC 串口下载免拆芯片这里要特别说一个新手最容易踩的坑P0 口内部没有上拉电阻直接驱动共阳数码管段码时高电平输出能力很弱数码管会暗到几乎看不清。常规做法是 P0 口接 470Ω×8 的排阻到 VCC再用 P0 输出段码灌电流点亮。位选端则不建议直接用 P0 口推因为一个 IO 要带一只数码管的 8 个段电流不够工程上习惯在 P2.0~P2.2 后面加 S8550 这类 PNP 三极管低电平选中每路承受的电流压力就小得多。2.2 引脚定义与驱动逻辑参考电路里给出的接法基本是“P0 段选 P2 位选 P1 按键输入”这套典型结构。写程序之前先把引脚宏定义固定下来我一般会在 main.c 头部维护一份这样的映射#define SEG_PORT P0 // 段选经 470R 排阻接共阳数码管 sbit DIG_NUM P2^0; // 选手号数码管位选 sbit DIG_TEN P2^1; // 倒计时十位 sbit DIG_ONE P2^2; // 倒计时个位 sbit BUZZER P2^7; // 有源蜂鸣器低电平触发 sbit LAMP_LED P2^6; // 超时指示 LED高电平点亮 sbit HOST_KEY P1^6; // 主持人允许/复位开关 #define KEY_IN P1 // P1.0~P1.5 为 6 路抢答输入按键接在 P1 口是有讲究的P1 口内部带上拉按键另一端接地平时读回来是高电平按下变成低电平不需要额外接外部上拉省掉 6 个电阻。段码表要按共阳数码管来写0~9 的段码如下code uint8_t seg_tab[10] { 0xC0, 0xF9, 0xA4, 0xB0, 0x99, // 0 1 2 3 4 0x92, 0x82, 0xF8, 0x80, 0x90 // 5 6 7 8 9 };注意这是共阳接法段码为 0 的位对应的段点亮。如果你拿到的是共阴数码管这套表要全部按位取反这是实物焊接时最隐蔽的问题——程序看起来“没错”灯就是不亮。2.3 下载接口与调试便利性题目专门要求“画电路板时要增加下载接口方便调试程序”说明设计者一开始就把反复烧录当成了常态这是很务实的做法。资源里程序基于 Keil 工程抢答器.uvproj目标芯片常见做法是 STC89C52RC其下载走串口P3.0(RXD)、P3.1(TXD)、GND、VCC 四根线外接 CH340 模块就能烧录不需要买专用仿真器。对课程设计来说这比用 AT89S52 加 ISP 下载线更省事因为 AT89S52 需要并行编程器或者专用下载线学生在实验室里经常被这步卡住。下载口不要放在板子边缘靠近按键的位置防止插 USB 转串口模块时碰到键位另外在 VCC 和 GND 之间并一个 10µF 电解电容加 104 瓷片电容能避免下载时复位不稳导致的“握手超时”。3. 状态机与定时器中断抢答仲裁、5 秒窗口与 20 秒答题倒计时的实现绕不开的核心程序集中在“抢答判定”和“倒计时”这两个部分。资源包里的 main.c 是判断正误的关键因为大多数翻车案例都出在这里。3.1 为什么抢答逻辑不能写成顺序 if很多初学者会写出这样的循环先检测主持人开关再依次扫描 6 路按键谁先被读到就设置谁。这在逻辑上有个致命的盲区单片机的 while(1) 循环是从第 1 路扫到第 6 路的如果第 1 路和第 2 路同时按下第 1 路总是赢这不是“抢答”而是“扫描顺序的偏袒”。真正的抢答器核心是“仲裁”所有输入在同一个时间基准上竞争一旦某一路被锁定其他路全部失效。这个行为用 if-else 堆出来会很乱正确做法是引入一个显式的状态机让系统在任何时刻都处于唯一确定的状态。3.2 状态转移关系参考题目要求我把状态分成四个当前状态触发事件响应动作下一状态IDLE空闲主持人按下允许键蜂鸣器短响倒计时置 5READYREADY允许抢答某路选手按下锁存选手号蜂鸣器短响倒计时置为 20 或设定值ANSWERREADY允许抢答5 秒倒计时归零数码管清空IDLEANSWER答题中倒计时归零蜂鸣器连续响、LED 点亮OVEROVER本轮结束主持人按下允许键清空选手号和超时标志IDLE这个表跟题目要求是逐条对应的第 2 条“其他选手不能再抢答”对应从 READY 进入 ANSWER 后不再扫描抢答键第 3 条“5 秒内抢答有效”对应 READY 状态下的倒计时判断第 5 条“倒计时到时有声光提示”对应 ANSWER 归零后的 OVER 动作。3.3 主循环里的抢答判定代码主程序的结构就是状态机的翻译每一轮循环只处理当前状态需要做的事void main(void) { sys_state S_IDLE; timer0_init(); // 定时器 0 产生 50ms 中断作为时基 while (1) { uint8_t key scan_key(); // 读 P1.0~P1.5返回 0~6 switch (sys_state) { case S_IDLE: if (HOST_KEY 0) { // 主持人按下允许 sys_state S_READY; left_second 5; // 5 秒抢答窗口 BUZZER 0; // 响一声提示 } break; case S_READY: if (key 1 key 6) { // 有选手抢答 winner key; // 锁存后续按键全部无效 BUZZER 0; // 抢答成功短响 left_second answer_time; // 答题时间基本版固定 20 sys_state S_ANSWER; } break; case S_ANSWER: // 不检查任何抢答键天然实现“其他选手不能再抢答” if (left_second 0) { // 由中断置零主循环只判状态 sys_state S_OVER; } break; case S_OVER: if (HOST_KEY 0) { // 主持人复位进入下一轮 sys_state S_IDLE; LAMP_LED 0; BUZZER 1; } break; } } }scan_key()的做法是整组读取 P1 口低 6 位而不是逐位循环判断这样能在逻辑上尽量避免“扫描顺序偏袒”。读取后延时 10ms 再读一次两次一致才认为按键有效这是最简单的防抖也是抢答器不误触发的底线。值得强调的是answer_time这个变量在基本版里是常量 20在调时版里才是可变的两份源码的差异点就在这一处。3.4 定时器中断里的计时与刷新倒计时的精度完全依赖定时器中断不能在主循环里做软延时否则按下一个键的过程就会让计时慢半拍。资源配套文档里提到的 Keil 工程配置的是 12MHz 晶振定时器 0 工作在方式 116 位50ms 中断一次累加 20 次就是 1 秒void timer0_isr(void) interrupt 1 { static uint16_t ms_count 0; TH0 0x3C; // 12MHz、12T 模式下 50ms 初值 TL0 0xB0; if (buzz_cnt 0) { // 蜂鸣器非阻塞计时关闭 if (--buzz_cnt 0) BUZZER 1; } if (ms_count 20) { // 20 个 50ms 为 1 秒 ms_count 0; if (sys_state S_READY || sys_state S_ANSWER) { left_second--; if (left_second 0) { if (sys_state S_ANSWER) { // 答题超时声光提示 LAMP_LED 1; BUZZER 0; // 连续响 } } } } display_refresh(); // 数码管动态扫描 }这里有一个容易被忽略的细节display_refresh()放在中断末尾而不是主循环里。因为倒计时显示必须和秒数更新同步如果显示刷新放在主循环while(1) 里任何一个分支的执行时间波动都会让数码管出现跳变或者闪烁。中断里刷新可以保证数码管稳定在 1ms 级别的扫描周期内。4. 基本版与调时版源码差异按键设置倒计时长度的设计与掉电存储资源包里有一份“抢答器程序设计已完成基本要求.zip”和一份“添加调时功能.zip”对比着看能学到的比单看一份多得多。调时功能对应题目选做要求“用按键可设置倒计时的时间范围在 5 秒到 30 秒之间”这里的核心是基本版里写死的answer_time 20调时版把它改成了一个可由按键修改的变量同时引入了设置状态。4.1 两份源码的差异位置基本版的状态机只有 IDLE、READY、ANSWER、OVER 四个状态调时版在 IDLE 之上增加了一个 SET 状态。两者不是分叉设计而是同一套状态机上的扩展在 IDLE 下检测到设置键长按 1 秒以上就进入 SET此时数字不再是倒计时而是参数编辑。其余的 READY、ANSWER 逻辑完全不用动这是状态机设计带来的直接收益——加功能不需要打乱原有流程。从工程角度收到两份源码后应该先做一次文本对比重点看 main.c 里有几处left_second被赋值凡是赋值的地方就是倒计时长度的控制点。基本版只有两处一处在 READY 初始化成 5一处在抢答成功时初始化为 20。调时版把第二处改成了answer_time然后在新增的 SET 状态里维护这个变量。4.2 长按进入设置、短按调整的实现调节操作我用三段式来实现长按设置键进入 SET 状态、短按加键或减键修改数值、再次长按设置键退出。代码如下#define SET_KEY P3^4 // 设置键 #define UP_KEY P3^5 // 加 #define DOWN_KEY P3^7 // 减 void handle_set_mode(void) { static uint16_t press_cnt 0; if (SET_KEY 0) { if (press_cnt 200) { // 200 次主循环约 1 秒视为长按 if (sys_state S_IDLE) { sys_state S_SET; // 进入设置 flash_cnt 0; } else if (sys_state S_SET) { sys_state S_IDLE; // 再次长按退出 } press_cnt 0; } } else { press_cnt 0; } if (sys_state S_SET) { if (UP_KEY 0) { // 短按加 1 if (answer_time 30) answer_time; while (UP_KEY 0); // 等待松手防连加 } if (DOWN_KEY 0) { // 短按减 1 if (answer_time 5) answer_time--; while (DOWN_KEY 0); } } }这里的边界判断是题目要求的直接体现范围是 5 到 30当值已经到 30 时再按加没有任何反应到 5 时再按减也没反应。进入 SET 状态后数码管需要闪烁提示否则用户分不清当前显示的是倒计时还是设置值。闪烁做法是在中断里用一个flash_cnt变量做分频每 500ms 翻转一次显示开关不用阻塞延时效果比delay()实现平滑得多。4.3 要不要把设置值写进 EEPROM调时版把answer_time放在普通 RAM 里断电后就恢复默认 20课程设计答辩时够用。但如果要把这个设计用到真正的比赛现场就得考虑掉电保存的问题。STC89C52 内部有一块 Data Flash可以通过 IAP 方式读写不需要外挂 24C02省一根 I2C 总线和两个 IO 口。写入前必须先触发IAP_CONTR 0x80使能然后擦除扇区再写入这部分逻辑放到设置退出时执行一次。需要提醒的是Flash 擦写寿命在十万次量级正常使用没问题但如果在 SET 状态下反复加减触发频繁写入会把寿命吃光常见的处理办法是只在长按退出设置时存一次而不是每次按键都写。5. 定时器初值计算与数码管动态扫描的实测排错5.1 12MHz 与 11.0592MHz 的初值选择这份资源的电路里用的是 12MHz 晶振定时器初值算出来是整的机器周期 12 ÷ 12MHz 1µs50ms 需要计数 50000 次16 位定时器初值就是 65536 - 50000 15536 0x3CB0。换成 11.0592MHz 时机器周期约 1.085µs同样 50ms 需要计数 46080 次初值是 0x4C00。晶振频率机器周期(12T)50ms 对应计数TH0/TL0 初值12.000MHz1.000µs500000x3C / 0xB011.0592MHz1.085µs460800x4C / 0x006.000MHz2.000µs250000xA6 / 0x58如果工程里用了串口打印调试11.0592MHz 能让波特率做到整数但如果纯做抢答器、不涉及串口收发数据12MHz 更省事定时器初值全整不会积累误差。两种晶振电路都能把 50ms 误差控制在 0.05% 以内对秒级倒计时的影响可以忽略。5.2 动态扫描的消隐顺序3 位数码管共用 P0 段选靠位选轮流点亮扫描频率至少要 50Hz 以上才不会显眼地闪烁我习惯把周期控制在 5ms 以内。最容易出现的毛病是“拖影”切换位选的一瞬间上一位的段码残留到了下一位。消隐顺序必须是“先关闭所有位选再送新段码最后开新的位选”顺序反了就会出现轻微的重影。就算用 Proteus 仿真看不出问题实物上也会很明显这是仿真和实物的典型差异。5.3 用示波器验证刷新率验证动态扫描是否达标最直接的办法是拿示波器或逻辑分析仪夹在位选引脚上看波形每个位选的导通脉宽乘 3 就是完整扫描周期脉宽 2ms 说明周期 6ms刷新率约 166Hz足够稳定。抓波形时注意把探头地线夹在 GND 上用单次触发模式抓位选信号下拉沿如果看到低电平脉宽忽长忽短多半是主循环里的按键消抖阻塞了中断把消抖逻辑改成状态标志位就能解决。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →