输入检测代码部分
034.3.2 输入检测代码部分
#include "reg52.h"
//#include <intrins.h>
sbit LED1 = P1^0;
sbit LED2 = P1^1;
sbit LED3 = P1^2;
sbit LED4 = P1^3;
sbit KEY1 = P3^2;
sbit KEY2 = P3^3;
sbit SUN = P3^7;
void Delay20ms(void) // @11.0592MHz
{
unsigned char data i, j;
i = 36;
j = 217;
do
{
while (--j);
} while (--i);
}
void main()
{
while (1)
{
if (KEY1 == 0)
{
Delay20ms();
while (KEY1 == 0); // while(1);松手检查
Delay20ms();
LED1 = ~LED1;
}
if (KEY2 == 0)
{
Delay20ms();
while (KEY2 == 0); // while(1);松手检查
Delay20ms();
LED2 = ~LED2;
}
if (SUN == 0)
{
LED3 = 1;
}
else
{
LED3 = 0;
}
}
}
一、输入检测代码部分
本节使用 3.2输入检测 工程,实现两个按键的单次触发和光敏模块的数字量检测。程序持续读取 P3.2、P3.3 和 P3.7,再分别控制 LED1、LED2 和 LED3。
flowchart LR
A[KEY1 / P3.2] --> D[按下与松手检测]
B[KEY2 / P3.3] --> D
D --> E[每次完整按压只产生一个事件]
E --> F[翻转 LED1 / LED2]
C[光敏模块 DO / P3.7] --> G[持续读取数字电平]
G --> H[控制 LED3 模拟智能路灯]
开发板上的输入和输出都是低电平有效,但两者的原因不同:
- 按键按下时把 P3 端口接到 GND,所以按下读到 0。
- LED 的阳极接 +5V,阴极经过限流电阻接 P1 端口,所以 P1 输出 0 时才能形成电流回路并点亮。
1.1 按键电平检测
开发板原理图中的 SW3、SW4 分别连接 P3.2、P3.3,每个按键的另一组触点接 GND。P3 口作为准双向输入时,由内部弱上拉维持释放状态的高电平;按下按键后,端口被开关拉到地。
STC89C52
┌────────────────┐
内部弱上拉 ───┤ P3.2 / INT0 ├──── SW3 ──── GND
内部弱上拉 ───┤ P3.3 / INT1 ├──── SW4 ──── GND
└────────────────┘
按键松开:开关断开,P3.x = 1
按键按下:开关闭合,P3.x = 0
由此得到最直接的电平控制写法:
if (KEY1 == 0)
{
LED1 = 0;
}
else
{
LED1 = 1;
}
这段代码让 LED1 直接跟随按键状态:按下亮,松开灭。开发板 LED1 的电路是:
+5V ── LED1 ── R8 10 kΩ ── P1.0
P1.0 = 0:+5V → LED1 → R8 → MCU 引脚 → GND,回路导通,LED1 亮
P1.0 = 1:LED 两端压差很小,电流近似为零,LED1 灭
“电平跟随”和“按一次切换一次”是两种不同需求:
| 控制方式 | 代码核心 | 按住时 | 松开后 |
|---|---|---|---|
| 电平跟随 | KEY1==0 时亮,否则灭 | LED 一直亮 | LED 灭 |
| 状态切换 | 每次有效按压执行 LED1=~LED1 | 状态只应改变一次 | 保持改变后的状态 |
如果只把状态切换写成下面这样,效果不会可靠:
if (KEY1 == 0)
{
LED1 = ~LED1;
}
while(1) 主循环运行速度远快于人的动作。按键保持低电平的几十或几百毫秒内,上述语句会执行很多次,LED 被高速反复取反;最终停在亮还是灭,取决于执行次数的奇偶。同时,机械触点抖动还会产生额外的高低电平变化。因此,要实现“按一次只切换一次”,既要处理抖动,也要把持续的按下电平转换为一次完整的按压事件。
编译和烧录时应选择 3.2输入检测/Objects/mcu51.hex,并结合文件时间确认它由当前工程生成;还要核对 STC89C52RC/LE52RC 芯片型号、CH340 对应的 COM 口和开发板供电状态。烧录后先验证按下灯亮、松开灯灭的跟随效果,再验证加入松手检测后的单次切换效果。
1.2 优化按键检测
轻触按键内部依靠金属弹片接通两组触点。弹片在刚接触和刚分离时会快速振动,使电路在“通”和“断”之间跳变数次,这就是机械抖动。
理想波形和实际波形可表示为:
时间 ─────────────────────────────────────────────►
动作: 按下 松开
理想电平:1 ────────┐_____________________┌──────── 1
│ 0 │
实际电平:1 ──────── 0-1-0-1-0 ───────── 0-1-0-1 ── 1
按下抖动 稳定按住 松手抖动
抖动通常持续数毫秒到数十毫秒,具体时间取决于按键结构、磨损、按压力度和电路条件。本实验在按下和松开阶段各使用 20 ms 延时,这是常用的入门经验值,不代表所有按键的抖动时间都精确等于 20 ms;产品设计应参考器件资料或示波器实测。
按键检测需要解决两类问题:
- 抖动重复触发:一次机械动作中出现多次 0/1 跳变。
- 持续电平重复触发:即使没有抖动,按住按键时
KEY1仍会长时间等于 0,快速轮询也会重复执行取反。
有效的处理流程是:
发现 KEY1=0
│
▼
等待约 20 ms,避开按下抖动
│
▼
确认按键仍然为低电平
│
▼
等待 KEY1 变为 1,也就是等待松手
│
▼
再等待约 20 ms,避开松手抖动
│
▼
执行一次 LED1 状态翻转
上方完整源码使用了其中的延时、松手等待和状态翻转。为了进一步排除偶然出现的短低脉冲,更严谨的写法还会在第一次延时后再次确认按键仍然按下:
if (KEY1 == 0)
{
Delay20ms();
if (KEY1 == 0)
{
while (KEY1 == 0);
Delay20ms();
LED1 = ~LED1;
}
}
这个二次确认并不会改变“低电平有效”的硬件事实,只是避免将很短的干扰脉冲当成真实按压。
1.3 消抖
消抖分为软件消抖和硬件消抖。
- 软件消抖:程序检测到可能的按键动作后,延时、重复采样或通过状态机确认电平稳定。优点是不增加元件、调整方便;缺点是阻塞式延时会占用 CPU 时间。
- 硬件消抖:使用 RC 低通网络、施密特触发器或专用消抖芯片,让送入 MCU 的电平先在硬件上变得平稳。优点是不依赖主程序响应;缺点是增加元件,并且 RC 参数需要结合输入门限设计。
常见的 RC 加施密特触发器结构如下:
R1(上拉)
VCC ──────────────────/\/\/\───●────────► 施密特输入 ───► MCU
│
├── C ─── GND
│
└── R2 ─── 按键 ─── GND
释放时:C 通过 R1 充电,节点缓慢变为高电平
按下时:C 通过 R2 和按键放电,节点缓慢变为低电平
施密特输入:使用不同的上升、下降门限,避免缓慢变化的电压在门限附近反复翻转
RC 变化速度由时间常数决定,近似为 τ = R × C。例如 10 kΩ × 1 µF ≈ 10 ms,但实际的上升和下降时间还取决于 R1、R2、输入门限及器件误差。R2 还可以限制按键闭合时电容的瞬时放电电流。本开发板的 SW3、SW4 原理图没有画出专用 RC 消抖网络,因此本实验采用软件消抖。
1.3.1 例题: 按键消抖
配套程序对 KEY1 的处理顺序是:
if (KEY1 == 0)
{
Delay20ms();
while (KEY1 == 0);
Delay20ms();
LED1 = ~LED1;
}
逐句理解如下:
if (KEY1 == 0):捕捉到可能的按下动作。Delay20ms();:跳过按下初期的抖动区间。while (KEY1 == 0);:只要按键仍为低电平,就停在这个空循环里等待松手。末尾分号非常关键,它表示循环体为空。- 第二个
Delay20ms();:按键刚松开时先等待一段时间,避开松手抖动。 LED1 = ~LED1;:完整按压结束后只翻转一次 LED1。
while (KEY1 == 0); 可以理解为一个带条件的“原地等待”:
KEY1 仍为 0(手没松) ──► 继续循环,不执行后续代码
KEY1 变为 1(已经松手) ──► 退出循环,继续执行
它虽然简单,但属于阻塞式写法。按住 KEY1 时,程序无法继续扫描 KEY2 和 SUN;若需要同时处理多个输入、显示、串口等任务,应改用定时扫描状态机。
Delay20ms() 是按 11.0592 MHz 时钟和相应 STC89 指令集生成的软件延时函数。生成延时时要同时核对晶振频率、时间单位、目标指令集和编译器设置;晶振或指令周期不一致时,函数名称仍叫 20 ms,实际延时却可能不同。该函数没有调用 _nop_(),所以本工程中的 <intrins.h> 可以保持注释状态。
KEY2 的处理方式相同,只需把输入和输出对应为 KEY2、LED2:
KEY1(P3.2)──► 消抖与松手检测 ──► 翻转 LED1(P1.0)
KEY2(P3.3)──► 消抖与松手检测 ──► 翻转 LED2(P1.1)
复制代码后必须同时检查条件、等待松手的变量和对应 LED,避免出现外层判断 KEY2、内层却仍等待 KEY1 的复制错误。
1.3.2 光敏电阻检测
光敏模块的数字输出 DO 接 P3.7,所以代码定义为 sbit SUN = P3^7;。这里的 ^7 表示 P3 寄存器的第 7 位;在 Keil C51 的 sbit 语法中,符号 ^ 可通过 Shift+6 输入。
光敏模块与单片机的连接为:
光敏模块 开发板
VCC ────────────────────────── +5V
GND ────────────────────────── GND
DO ────────────────────────── P3.7 / SUN
AO ────────────────────────── 本实验不连接
模块内部的关键电路是“光敏分压 + 可调参考电压 + LM393 比较器”:
VCC VCC
│ │
[10 kΩ] [DO 上拉]
│ │
AO ───────●──────► LM393 (+) ├──── DO ──► P3.7
│ \ │
[光敏电阻] ) 比较器 ─────┘ 开集电极输出
│ /
GND VREF ─►(-)
▲
电位器在 VCC 与 GND
之间产生可调阈值
在这套模块电路中,光越强,光敏电阻阻值越小,AO 电压越低;当 AO 低于电位器设定的 VREF 时,LM393 把 DO 拉低。因此实测关系是:
| 光照状态 | 光敏电阻 | DO / SUN | 源码写入 LED3 | LED3 实际状态 |
|---|---|---|---|---|
| 光线较强 | 阻值较小 | 0 | 1 | 灭 |
| 光线较弱 | 阻值较大 | 1 | 0 | 亮 |
LED3 之所以在写 0 时点亮,是因为它同样采用 +5V → LED3 → 10 kΩ → P1.2 的低电平有效电路。最终程序为:
if (SUN == 0)
{
LED3 = 1; // 光强:熄灭路灯
}
else
{
LED3 = 0; // 光弱:点亮路灯
}
判断条件和 LED 赋值是成对的:也可以判断 SUN == 1 后执行 LED3 = 0,效果相同。调试未知模块时,应先观察 DO 或模块状态灯在明暗环境下的变化,再决定软件极性,不能仅凭引脚名猜测。
光敏模块的 DO 与按键一样都能作为数字输入读取,但光敏电阻没有机械触点,因此通常不需要按键式“等待松手”消抖。若光照恰好位于比较阈值附近,DO 仍可能因光线噪声和比较器门限反复跳变;可以调节电位器使工作点远离临界位置,或在程序中采用多次采样确认、时间滤波和迟滞判断。