ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

51单片机按键加减数码管显示:从消抖到动态扫描的完整实现

51单片机按键加减数码管显示:从消抖到动态扫描的完整实现 简介51单片机经典入门实例聚焦按键输入与数码管显示的联动控制。资源内含完整的Keil工程源码、C语言主程序以及编译生成的hex烧录文件并附带plg工程日志、m51映像文件、lst列表文件等辅助输出可帮助初学者直观理解工程结构。压缩包共11个文件整体仅13KB轻量易下载适合课堂同步练习或课外自学。已有4079人学习是检验中断/扫描式按键检测与动态数码管刷新思路的理想范例。通过阅读源码可掌握按键消抖处理、加减计数逻辑及数码管段码驱动等关键技能还可直接用hex文件在开发板上烧录验证快速获得直观反馈。对于刚接触51单片机、想弄清GPIO输入输出与显示时序关系的读者这份资料能提供从原理到实践的完整闭环。1. 实例53-2到底是什么51单片机按键加减数码管的最小闭环一个编号为53-2的按键加减例程讲的其实是把“按键输入→数值变化→数码管显示”这一闭环在51单片机上跑通。很多教材和网课把类似的例程叫做“数码管加减计数器”“按键调时间”本质都是同一个套路读按键、消抖、修改内部变量、刷新段码。真正需要花时间理解的不是加一减一那两行代码而是“按一次为什么跳了两下”和“数码管为什么显示成乱码”这两个现象。本篇按硬件接线、C语言实现、Proteus仿真的顺序展开最后给出长按连续加减的进阶写法适合已经跑通过LED闪烁和定时器、准备转向按键输入的新手。2. 硬件方案数码管段选、独立按键与按键保护电路怎么接2.1 数码管内部结构与段码表共阴共阳不要接错数码管内部是8个LED分别对应a、b、c、d、e、f、g和dp。公共端接在一起引出来的类型叫共阴极段码脚输出高电平时点亮对应段反过来把公共端接到VCC、段码脚输出低电平点亮的叫共阳极。教材里最常见的段码表是针对共阴极写的例如数字“0”点亮a到f六段对应段码0x3F。如果买到的是共阳极段码要逐位取反。数字共阴段码共阳段码00x3F0xC010x060xF920x5B0xA430x4F0xB040x660x9950x6D0x9260x7D0x8270x070xF880x7F0x8090x6F0x9051单片机硬件设计里有个高频翻车点P0口是开漏输出。把P0当段码端口时如果段码表输出的是1且外部没接上拉电阻这个1的驱动能力非常弱数码管会暗、缺段。Proteus仿真里看不出来实物一上电就露馅。正确做法是给P0接一个10kΩ排阻到VCC再在每个段码引脚串一只220Ω到330Ω的限流电阻。理解成“P0只能可靠拉低不能可靠拉高”就够用了。选引脚时要把三类信号分开段码占一整组口按键占两个IO位选占一个IO。下面的定义就是本实例的基础硬件绑定sbit SEG_SEL P2^0; // 数码管位选低电平选通 sbit KEY_UP P3^0; // 加一键按下为低 sbit KEY_DOWN P3^1; // 减一键按下为低 #define SEG_PORT P0 // 8个段码接P0.0~P0.7P0留给段码P3留给按键P2做位选这样后面扩展成两位或多位数码管时P2剩下的口正好接其他位选信号不用搬动原电路。2.2 独立按键接入与按键保护电路上拉电阻和限流给谁按键的接法分两类独立按键和矩阵键盘。本实例只用两个键选独立按键。最简单的接法是按键一端接IO口另一端接GNDIO口靠内部上拉保持高电平按下读到低电平松开回到高电平。P3口自带弱上拉阻值在几十kΩ量级噪声环境里容易误触发建议外部再并一只10kΩ上拉。按键两端并联0.1uF瓷片电容做硬件消抖成本低且非常有效。端口节点会引发一个典型问题按键悬空时读到的高低电平不确定程序里表现为“按键没按数值自己变”。解决思路是先把空白引脚电平固定住。因为P3有内部上拉想要让它稳定为高都是给IO外部加上拉并让按键导通到GND。反过来把按键接到VCC、IO做内部下拉的话51标准51口没有内部下拉外部还得加下拉电阻徒增成本。按键保护电路经常被跳过。人手上的静电、机械触点弹跳火花、热插拔时的瞬态电压都可能超过IO口耐受范围。教学板不影响功能项目实践里建议在按键到IO之间串一只1kΩ电阻限流按键两端再并TVS管。对可靠性要求更高的面板场景比如51单片机电磁炉程序里的大火小火按键还要考虑按键扫描引脚不能和蜂鸣器、继电器驱动引脚复用否则按下按键的瞬间会干扰其他设备。应急时候用临时接线我也试过用单片机内部上拉直接跑按键确实能用但抗干扰很差隔着一米去碰电源线数值就乱跳。有硬件保护电位的设计在长线按键场景下差别非常明显按键保护电路不只是课设评分项。2.3 一位数码管选静态显示为什么还要理解动态扫描本实例只显示0到9一只数码管工作于静态显示即可位选引脚给长期有效的电平每次只改段码。一位数码管不需要扫描刷新因为刷新频率、刷新周期这些概念根本不参与显示维持。很多人从六位数码管动态扫描的例程过来习惯性写一个定时刷新函数其实对一个数码管没有必要。不过动态扫描的原理必须提前建立起来多位显示时单片机不可能同时给所有位送数据只能逐位轮流点亮每位点亮1ms到2ms靠视觉暂留让整个屏幕看上去是常亮的。学会了静态显示再往四位数码管走只差一个定时器中断和一个位选循环。这也是实例53-2之后最常见的学习路线。我见过不少学生在“一位数码管”上直接套动态扫描模板结果发现不刷新数字还是亮的于是怀疑代码有问题。其实这正是静态显示和动态扫描两者的区分点静态显示由硬件持续供能软件只负责更新段码动态扫描必须由软件不断追赶刷新周期任何一个环节卡死显示马上熄灭或闪烁。本实例按键扫描的阻塞式写法放在静态显示下没有任何问题放动态显示下就会让中断来不及喂位选这是后面章节要随时记着的前提。3. 软件实现按键消抖、加减逻辑和数码管显示驱动3.1 按键消抖延时消抖代码与适用位置机械按键按下和释放的瞬间金属触点会来回碰撞产生几毫秒到二十毫秒的电平抖动。单片机读取速度远高于机械动作完成速度不做处理的话一次按键会被当成十几次按下。最朴素的处理是延时消抖逻辑读到低电平后等20ms左右再读一次确认还是低电平才认定有效然后等待松开。sbit KEY_UP P3^0; sbit KEY_DOWN P3^1; void delay_20ms(void) { unsigned char i; unsigned int j; for (i 0; i 20; i) for (j 0; j 1200; j); } void delay_5ms(void) { unsigned char i; unsigned int j; for (i 0; i 5; i) for (j 0; j 1200; j); } unsigned char read_key(void) { if (KEY_UP 0) { delay_20ms(); if (KEY_UP 0) { while (KEY_UP 0); delay_5ms(); return 1; } } else if (KEY_DOWN 0) { delay_20ms(); if (KEY_DOWN 0) { while (KEY_DOWN 0); delay_5ms(); return 2; } } return 0; }这段代码把20ms消抖和松手5ms停顿结合在一起有效屏蔽了按下与松开两个抖动段。while (KEY_UP 0) 的作用是等按键完全松开防止一次按下被识别成多次后面的delay_5ms是给松手抖动一个缓冲。返回值刻意设计成1和2main函数就可以直接用 switch 或 else if 区分加键减键。晶振频率delay_20ms里j的取值说明12MHz1200约等于20ms精确值受Keil优化影响11.0592MHz1150机器周期略长数值要微调6MHz600主频减半循环相应减半这个表给的是手工换算的粗略值。对于按键消抖误差几毫秒完全不影响判断不需要用工具精确计算。但如果以后做定时器中断重装值就必须按晶振精确计算不能沿用这套计法。延时消抖最大的副作用是阻塞while等松开的期间整个CPU都被占住如果你同时要刷新动态扫描显示数码管会按一次灭一次。所以延时消抖只适合静态显示和简单逻辑工程例如本实例。按键触发中断的写法在51里也可以做但中断只负责置标志位消抖还是要回到主循环或用定时器扫描否则误触发率很高。3.2 数码管显示驱动段码表、位选与刷新函数显示驱动只解决两件事把数字翻译成段码把段码送到P0口再给位选引脚一个选通电平。共阴极数码管公共端接到GND位选给低电平时数码管才整个接通电源。sbit SEG_SEL P2^0; #define SEG_PORT P0 unsigned char code seg_code[10] { 0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F, 0x6F }; void display_digit(unsigned char num) { SEG_SEL 0; SEG_PORT seg_code[num]; }SEG_SEL先置低、再送段码这样能避免位选导通瞬间P0口还在输出上一帧数据产生几十微秒的鬼影。段码表用code关键字修饰把数据放到程序存储区51单片机的片内RAM很小8个字节虽然不多但养成把常量放code区的习惯后面做四位数码管时就不会为RAM发愁。如果换成共阳极数码管段码全部取反同时位选逻辑也要变。共阳极的公共端一般由PNP三极管驱动基极接单片机的位选引脚低电平时三极管导通数码管通电。直接把共阳管接到P2.0上位选低电平时没有压差完全不亮位选高电平时驱动能力又不够亮度发暗这是接线不匹配的典型症状。3.3 主循环里的加减逻辑与边界处理主程序维护一个unsigned char变量value按键返回1就加一返回2就减一每次改完立即刷新显示。一位数码管只能表现0到9加到9再按加键不能变10否则段码表下标越界取出的是相邻地址的随机数据显示立刻乱掉。void main(void) { unsigned char value 0; display_digit(value); while (1) { unsigned char key read_key(); if (key 1) { if (value 9) value; } else if (key 2) { if (value 0) value--; } display_digit(value); } }上限用 if (value 9) 而不是 if (value 10)这个细节要专门说明一下9已经可以显示小于10包含了9本身而9再加一应该被继续拦住所以判断的是 value9。下限用 value0 同理。如果把value定义成unsigned char还忘了边界处理减到0再减会变成255数码管显示乱码拔掉电源重新烧程序才能恢复这在线下实验里特别容易发生。到这里Keil里新建工程、添加这几个函数编译烧录就能看到效果。编译时注意给51单片机工程正确选型比如STC89C52、AT89C52。工程文件放在非中文路径下Keil对中文路径支持不稳定经常出现能编译但生成不了hex文件的怪问题。4. Proteus仿真与实物排错从现象定位到接线上4.1 在Proteus仿真51单片机中搭建最小电路和加载hex想在Proteus仿真51单片机例程电路搭建顺序建议从上电和复位开始再连端口。AT89C51从库中拖出后放置12MHz晶振到XTAL1和XTAL2晶振两端各对地接一只30pF电容。复位电路用10uF电解电容到VCC、10kΩ电阻到GNDRST接两者中间。这个配置和真实硬件环境一致。段码侧按第二章的共阴极方案接P0.0到P0.7接数码管a到dpP0口放一个8位上拉排阻RESPACK-8公共端直接接GND。按键侧接P3.0和P3.1按键一端接IO、另一端接GND每个IO口到VCC之间放10kΩ电阻。位选P2.0接到数码管公共端的控制位置。双击单片机模型在Program File一栏选择Keil生成的hex文件。Keil里需要先确认已经勾选Options for Target - Output - Create HEX File否则编译后没有hex可加载。点运行数码管默认显示0按加键到9封顶按减键到0封底整个闭环就算通了。4.2 用探针和虚拟示波器检查按键抖动Proteus里的按键是理想开关模型仿真时电平切换基本没有真实触点抖动所以仿真中看不到“按一下跳两下”的效果。这反而有助于定位问题如果仿真中按下就连续跳了很多次那说明程序里压根没有消抖或者消抖只挡了按下没挡松手。把仿真现象当成软件逻辑的试金石比在仿真里模拟机械抖动更有价值。要看真实抖动波形就得转到实物上。用示波器探头夹在P3.0上设置时基到10ms每格按下按键就能看到下降沿附近一串高、低跳变脉冲。这些毛刺经过软件20ms延时后就被屏蔽了。如果抖动严重到几十毫秒还有多次跳变或者现场有其他设备干扰才需要把消抖时间提升到30ms并确认按键两端确实并联了0.1uF电容。4.3 Proteus仿真51单片机常见问题排查表现象可能原因处理上电无显示P0未加上拉段码高电平驱动不足用RESPACK-8把P0上拉到VCC数字暗、笔画缺共阴共阳不匹配公共端接反段码按表取反公共端重新检查按下一次值加多次消抖不足、没等待松开延时拉到20ms补while等待代码显示一直乱跳数码管公共端直连IO口电流过载公共端用三极管驱动段码串220Ω电阻Keil能编译但Proteus不亮没生成hex或单片机型号/时钟不对勾选Create HEX File晶振设为12MHz按键没反应按键IO接错、上拉缺失、引脚复用P3口外部接10k上拉确认按下读低电平这张表里的现象在校验过程中大部分都是接线问题不是代码问题。我自己的经验是优先用万用表量按键两端电压按下时IO引脚电压会从5V掉到0V再去量P0口是否有段码电压两级量完故障范围基本就能缩小到单个引脚上。直接把multimeter的探头接到P0.0显示数值为2.2V左右就说明段码已经送进去了问题基本锁定在数码管公共端一侧。5. 继续扩展长按连续加减与多位数码管动态扫描5.1 用定时器状态机实现长按连续加减延时消抖只管按一次触发一次要按住实现连续加、连续减需要把按键扫描改成定时器驱动的状态机。核心思路是每隔5ms扫描一次引脚把当前电平状态和上一次比较相同则计数累加不同则清零重来。连续稳定80次也就是400ms后判定为长按开始之后每隔5个扫描周期触发一次返回值这样用户按住加键数字就会以大约25ms一次的频率持续往上加。#define KEY_NONE 0 #define KEY_UPB 1 #define KEY_DOWNB 2 unsigned char key_scan_5ms(void) { static unsigned char stable KEY_NONE; static unsigned int hold_cnt 0; unsigned char cur; if (KEY_UP 0) cur KEY_UPB; else if (KEY_DOWN 0) cur KEY_DOWNB; else cur KEY_NONE; if (cur stable) { if (cur ! KEY_NONE hold_cnt 0xFFFE) hold_cnt; } else { stable cur; hold_cnt 0; } if (stable ! KEY_NONE) { if (hold_cnt 1) return stable; if (hold_cnt 80 (hold_cnt - 80) % 5 0) return stable; } return KEY_NONE; }这段函数在5ms定时器中断里调用返回1或2时主循环修改数值。第一次稳定判定在hold_cnt等于1时返回作用是替代原先按键的20ms延时消抖只触发一次之后hold_cnt到达80起自动连发每次返回间隔5个扫描周期。静态变量stable记录最近一次稳定状态cur读到的是当前电平对应按键编号。hold_cnt小于0xFFFE让16位计数器不要溢出回绕导致逻辑混乱。取模运算每25ms才执行一次开销可以忽略真要在老51上追求极致把偏移量计算换成减法分支也行。长按时主循环里原来的 if (value 9) 边界依然有效数字到9就自动停下按住也不会上溢这是个很好的副作用。5.2 从2位5脚数码管扩展到多位动态显示多数按键加减计数器都会在第二版改成两位或三位显示常见器件是2位5脚数码管。这类封装把八段引脚合并公共脚按位独立引出。硬件设计要先把公共脚接到位选三极管段码端仍由P0驱动。软件上把显示刷新挪进定时器中断用1ms中断轮流点亮两个位。unsigned char disp_buf[2] {0, 0}; unsigned char code seg_code[10] {0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F, 0x6F}; void timer0_isr(void) interrupt 1 { static unsigned char pos 0; TH0 0xFC; TL0 0x66; P2 0xFC; P0 0x00; P2 | (1 pos); P0 seg_code[disp_buf[pos]]; pos (pos 1) 0x01; }P2 0xFC 先关掉两位P0 0x00 清掉段码避免位切换瞬间拖影再打开当前位并送出对应段码。TH0重装值0xFC66对应12MHz下约1ms一次中断切换频率每秒500次每位数码管刷新率正好500Hz视觉上稳定且不闪烁。主循环只负责给disp_buf[0]和disp_buf[1]赋值按键扫描也在中断里驱动刷新和按键互不阻塞长按再快也不会看到显示停顿。这个结构就是实例53-2往下走最自然的形态静态显示换动态扫描阻塞式消抖换状态机所有改动围绕中断和缓冲区展开原来调通的加减逻辑一行都不用动。本文还有配套的精品资源点击获取
返回列表