-独立按键控制LED:从亮灭、状态切换到二进制显示与左右移位)
写在前面上一篇刚从点亮第一盏 LED 开始完成了 LED 常亮、闪烁、流水灯和渐变加速几个基础实验。这次继续往前走了一步把“输入”加进来。前面都是单片机按照提前写好的程序自动控制 LED而加入独立按键之后程序开始能够根据人的操作做出反应按下按键 → 单片机读取 IO 电平 → 执行对应逻辑 → LED 状态发生变化。这次一共完成了四个练习3-1 独立按键控制 LED 亮灭3-2 独立按键控制 LED 状态切换3-3 独立按键控制 LED 显示二进制3-4 两个独立按键控制 LED 左右移位虽然表面上是在学单片机按键但真正写下来会发现代码里的大部分内容仍然是之前学过的 C 语言基础if条件判断、while循环、变量自增自减、函数封装、位运算、移位运算。只不过以前这些语法最终改变的是控制台里的数字现在改变的是实际 LED 的亮灭位置。而这次也踩到了一个非常典型的“代码复用 Bug”复制上一段按键代码以后只改了一部分端口号忘记把while(P3_10)改成while(P3_00)结果按一下P3_0后 LED 就开始不停变化。这个问题也算给我提了个醒复制代码很快但复制之后逐行检查变量和端口更重要。本篇源码仓库嵌入式-单片机/C51 · Luminous/Code_2026 - 码云 - 开源中国一、3-1 独立按键控制 LED 亮灭第一个实验非常直接按住按键LED 点亮松开按键LED 熄灭。代码#include REGX52.H int main() { while(1) { if(P3_10) { P2_00; } else { P2_01; } } }这里程序不断读取P3_1的电平状态。当检测到P3_1 0时将P2_0 0;对应 LED 点亮否则P2_0 1;LED 熄灭。整个判断一直放在while(1)中循环执行因此 LED 状态会实时跟随按键状态变化。这一版其实和最基础的 C 语言if(...) { } else { }没有本质区别。真正不同的是if判断的不再是普通变量而是单片机 IO 引脚上的实际电平。所以第一个实验更像是把之前学过的条件判断第一次真正接到了硬件上。二、3-2 从“按住才亮”到“按一下切换一次状态”第一个实验有一个很明显的特点手一直按着灯才保持亮手一松开灯马上熄灭。但实际很多按键功能更接近开关第一次按下 → LED 亮第二次按下 → LED 灭第三次按下 → LED 又亮也就是每按一次状态翻转一次。完整代码#include REGX52.H #include INTRINS.H void Delay(unsigned int xms) //11.0592MHz { unsigned char data i, j; while(xms) { _nop_(); i 2; j 199; do { while (--j); } while (--i); xms--; } } int main() { while(1) { if(P3_10) { Delay(20); while(P3_10); Delay(20); P2_0~P2_0; } } }这一版最大的变化是加入了完整的一次“按下—松开”判断流程Delay(20); while(P3_10); Delay(20);以及最后的P2_0~P2_0;代码会等待按键释放后再执行状态翻转因此一次完整按键动作只切换一次 LED 状态。2.1 为什么前后各有一个 Delay(20)机械按键并不是理想的数字开关真实按键在刚按下或刚松开的一瞬间触点可能会在很短时间内反复接触和断开这种现象叫做按键抖动。所以代码中的第一次Delay(20);主要用于过滤按下时的抖动。然后while(P3_10);表示只要按键还没有松开就一直在这里等待。最后再Delay(20);过滤松手时可能产生的抖动。于是一次按键处理可以概括成检测按下 ↓ 延时消抖 ↓ 等待松手 ↓ 再次消抖 ↓ 执行一次功能这也是后面几个按键实验一直重复使用的基本结构。三、3-3 独立按键控制 LED 显示二进制第三个实验开始不再只控制一个 LED而是引入一个变量unsigned char LEDNum 0;每按一次按键LEDNum;然后直接把这个数字送到 P2P2~LEDNum;完整代码#include REGX52.H #include INTRINS.H void Delay(unsigned int xms) //11.0592MHz { unsigned char data i, j; while(xms) { _nop_(); i 2; j 199; do { while (--j); } while (--i); xms--; } } int main() { unsigned char LEDNum 0; while(1) { if(P3_10) { Delay(20); while(P3_10); Delay(20); LEDNum; P2~LEDNum; } } }这里每完成一次按键动作LEDNum就增加 1再通过P2~LEDNum把它对应到 8 个 LED 上。例如LEDNum 1 二进制0000 0001 LEDNum 2 二进制0000 0010 LEDNum 3 二进制0000 0011由于当前开发板上的 LED 是低电平点亮因此真正输出到 P2 时需要进行按位取反~LEDNum这样 LED 的亮灭状态就能够直观表现变量的二进制形式。3.1 这里又把 C 语言基础重新用了一遍这一部分最有意思的是LEDNum;以前学自增运算时它只是一个变量增加 1。到了单片机这里一个变量增加 1 → 二进制发生变化 → IO 口电平发生变化 → 真实 LED 组合跟着变化。所以很多以前感觉比较抽象的二进制和位运算在单片机上反而更加直观。特别是unsigned char本身就是一个 8 位无符号数据类型而 P2 同样对应 8 个 IO 位两者天然就很适合放在一起观察。四、3-4 两个独立按键控制 LED 左右移位第四个实验是在前面流水灯和独立按键的基础上继续组合。目标是P3_1控制 LED 向一个方向移动P3_0控制 LED 向另一个方向移动同时使用LEDNum保存当前 LED 所在的位置。最终代码#include REGX52.H #include INTRINS.H void Delay(unsigned int xms) //11.0592MHz { unsigned char data i, j; while(xms) { _nop_(); i 2; j 199; do { while (--j); } while (--i); xms--; } } unsigned char LEDNum 0; int main() { P2~0x01; while(1) { if(P3_10) { Delay(20); while(P3_10); Delay(20); LEDNum; if(LEDNum8) { LEDNum0; } P2~(0x01LEDNum); } if(P3_00) { Delay(20); while(P3_00); Delay(20); if(LEDNum0) { LEDNum7; } else { LEDNum--; } P2~(0x01LEDNum); } } }程序一开始先执行P2~0x01;让第一盏 LED 作为初始位置亮起。随后P3_1分支通过LEDNum向前移动并在达到 8 时重新回到 0P3_0分支则使用LEDNum--向反方向移动并通过LEDNum0的判断避免无符号变量直接向下越界。最终统一使用P2~(0x01LEDNum);控制对应位置的 LED。独立按键控制 LED 显示二进制演示五、这一句代码到底在干什么第四个实验中最核心的一行应该就是P2~(0x01LEDNum);先看0x01对应0000 0001如果LEDNum 1那么0000 0001 1 0000 0010如果LEDNum 2则0000 0100继续增加0000 1000 0001 0000 0010 0000 0100 0000 1000 0000也就是说0x01 LEDNum本质上就是让唯一的那个1不断在 8 位数据中移动。而由于当前 LED 为低电平点亮所以最后再~按位取反。例如0000 0100取反后1111 1011于是对应位置变成唯一的0这颗 LED 就被点亮。所以以前 C 语言里学过的左移运算到了这里直接变成让真实的灯向旁边移动一格。六、这次真正踩到的 Bug复制代码忘记改 P3_1第四个实验也是这次最值得记录的一个错误因为前面的P3_1按键逻辑已经写好了所以在添加P3_0时我直接复制了一份原来的代码再修改成反方向移动。最开始写成了类似这样if(P3_00) { Delay(20); while(P3_10); // 错误这里忘记改成P3_0 Delay(20); if(LEDNum0) { LEDNum7; } else { LEDNum--; } P2~(0x01LEDNum); }表面上看两块代码几乎完全一样很容易一眼扫过去没发现问题。但实际上if(P3_00)检测的是P3_0。后面等待松手时却写成while(P3_10);检测的变成了另外一个按键。6.1 为什么按一下以后 LED 就不停变化假设我此时按住的是P3_0于是if(P3_00)成立程序进入分支。经过Delay(20);之后本来应该while(P3_00);等待我真正松开这个按键。但代码写成了while(P3_10);而P3_1此时根本没有被按下通常并不等于 0所以这个while根本不会等待程序直接继续执行下面的LEDNum--; P2~(0x01LEDNum);执行完成以后重新回到最外层while(1)但此时我的手还没有松开P3_0。于是P3_00又成立。程序再次进入。整个过程就变成P3_0仍然按着 ↓ 进入if ↓ 错误检查P3_1 ↓ 没有等待松手 ↓ LED移动一次 ↓ 重新循环 ↓ P3_0仍然为0 ↓ 再移动一次 ↓ ……于是原本应该按一下移动一格最终变成按一下之后快速连续移动看起来像一直闪。6.2 修复其实只有一个字符把while(P3_10);改成while(P3_00);问题就解决了。最终正确版本也是如此P3_1分支等待P3_1松开而P3_0分支等待P3_0松开。这个 Bug 并不是什么复杂的指针错误也没有涉及寄存器底层原理。纯粹就是复制代码以后漏改了一个变量。但也正因为简单反而很典型。七、从这个 Bug 学到的代码复用不是复制完就结束之前写数据结构时其实也遇到过不少类似问题函数复制后变量没有全部替换指针名字改了一半测试代码复用时传错参数。这次到了单片机里同一种问题又出现了。所以以后再复制结构相似的代码时我准备至少检查三个地方判断条件用的是不是当前对象循环内部引用的变量是否全部替换最后修改的目标是不是当前功能对应的变量或端口尤其是这种P3_1 P3_0只差一个数字的名字肉眼确实很容易漏过去。八、四个实验放在一起看这次四个实验其实是一层一层加东西实验功能核心知识3-1 按键控制亮灭按住亮松开灭if/else、IO 输入3-2 按键切换状态按一次切换一次按键消抖、等待松手、取反3-3 二进制显示每按一次数字 1自增、8 位变量、二进制3-4 LED 左右移位两个按键控制方向/--、边界判断、移位从 C 语言角度来看其实没有出现多少全新的语法。但以前学过的if / else while -- ~ 函数参数 unsigned char现在全部开始和硬件产生联系这也是我目前学 51 最大的感受单片机入门阶段很多时候并不是在重新学习一门语言而是在学习怎样把已经掌握的 C 语言映射到硬件行为上。九、写在最后上一篇还是让程序自己控制 LED亮 ↓ 灭 ↓ 流水这一篇加入按键之后终于开始有了一点真正的“交互”。人按下按键单片机读取电平再根据程序决定LED 是否点亮是否切换状态当前数字是多少下一盏灯应该往哪个方向移动。代码复杂度仍然很低但它已经从单片机自己运行变成单片机开始响应外部输入。而且这一节学下来我感觉很多内容其实还是 C 语言基础的继续应用。尤其是第四个实验P2~(0x01LEDNum);把位运算和真实 LED 的移动直接联系在一起之后之前学过的移位运算一下就直观了很多。当然这次最深刻的可能还是那个while(P3_10);复制代码忘记改端口号的小 Bug改起来只有一行但排查之后让我再次意识到程序出问题不一定是不会有时候只是眼睛把“看起来一样”的代码自动忽略了。目前独立按键这一部分算是正式开始了。后面继续往下学时应该还会接触数码管、定时器以及更多外设。从点亮 LED到按键真正控制 LED硬件这条线也开始一点一点连起来了。