输入检测代码部分

Tutorial: 电子电路 Category: 嵌入式 Published: 2026-10-08 01:26:37 Views: 0 Likes: 0 Comments: 0

034.3.2 输入检测代码部分

/*
 * 源码位置:源码/2、51单片机部分/2、51单片机教学篇/代码/3.2输入检测/main.c。
 * 整体目的:轮询两个低电平有效的板载按键和光敏模块 DO,并分别控制 LED1、LED2、LED3。
 * 按键策略:检测按下后延时约 20ms,阻塞等待松手,再延时约 20ms,最后只翻转一次对应 LED。
 * 光敏策略:SUN 为 0 时令低电平有效的 LED3 熄灭,SUN 为 1 时点亮 LED3,形成“亮处关灯、暗处开灯”的效果。
 * 时序限制:Delay20ms() 是按 11.0592MHz 和课程所选 STC89 指令集生成的忙等待,按住任一按键时其他输入暂停检测。
 */
#include "reg52.h"    /* 引入 Keil C51 的 8052 特殊功能寄存器声明,使程序可以使用 P1、P3 等寄存器名称。 */
//#include <intrins.h>

sbit LED1 = P1^0;     /* 将 LED1 绑定到 P1.0;本板 LED 低电平有效,因此写 0 点亮、写 1 熄灭。 */
sbit LED2 = P1^1;     /* 将 LED2 绑定到 P1.1,供 KEY2 的事件处理代码单独翻转第 2 盏灯。 */
sbit LED3 = P1^2;     /* 将 LED3 绑定到 P1.2,作为光敏模块数字判断结果的输出指示灯。 */
sbit LED4 = P1^3;     /* 将 LED4 绑定到 P1.3;定义保留了完整板级映射,但当前程序没有使用 LED4。 */

sbit KEY1 = P3^2;     /* 将板载按键 K1 绑定到 P3.2;按下时开关把引脚接到 GND,所以读到 0。 */
sbit KEY2 = P3^3;     /* 将板载按键 K2 绑定到 P3.3;松开时端口保持高电平,按下时变为低电平。 */

sbit SUN = P3^7;      /* 将光敏模块 DO 绑定到 P3.7;这里读取的是阈值比较后的数字量,不是 AO 模拟电压。 */

void Delay20ms(void) // @11.0592MHz
{                                           /* 固定延时函数体开始,每次调用会让 CPU 忙等待约 20ms。 */
    unsigned char data i, j;                 /* 定义两个 8 位无符号计数器,并用 data 指定片内直接寻址 RAM。 */

    i = 36;                                  /* 装入外层循环计数初值,与 j 和各条指令耗时共同决定延时时长。 */
    j = 217;                                 /* 装入内层循环计数初值,让下面的空循环通过递减消耗 CPU 时间。 */
    do                                       /* 开始由 i 控制的后判断循环,保证循环体至少执行一次。 */
    {                                        /* 外层延时循环体开始。 */
        while (--j);                          /* 先将 j 减 1,结果非 0 就继续空转;末尾分号表示循环体有意为空。 */
    } while (--i);                            /* 一轮 j 计数结束后递减 i;j 会利用 8 位无符号回绕完成后续计数。 */
}                                            /* 约 20ms 忙等待结束,CPU 返回调用处继续执行。 */

void main()                                  /* 定义 Keil C51 应用程序入口;芯片启动代码完成初始化后进入这里。 */
{                                            /* 主函数体开始。 */
    while (1)                                /* 永久轮询三个输入,使程序持续响应按键和环境光变化。 */
    {                                        /* 主循环的一轮开始。 */
        if (KEY1 == 0)                       /* 读取 P3.2;等于 0 表示低电平有效的 K1 可能已被按下。 */
        {                                     /* KEY1 按键事件处理开始。 */
            Delay20ms();                      /* 先等待约 20ms,避开机械触点闭合初期的抖动。 */
            while (KEY1 == 0); // while(1);松手检查
            Delay20ms();                      /* 松手后再等待约 20ms,过滤触点释放时产生的抖动。 */
            LED1 = ~LED1;                     /* 完成一次按下并松开后翻转 P1.0,使 LED1 只改变一次亮灭状态。 */
        }                                     /* KEY1 按键事件处理结束。 */

        if (KEY2 == 0)                       /* 读取 P3.3;等于 0 表示低电平有效的 K2 可能已被按下。 */
        {                                     /* KEY2 按键事件处理开始。 */
            Delay20ms();                      /* 等待约 20ms,过滤 K2 按下瞬间的触点抖动。 */
            while (KEY2 == 0); // while(1);松手检查
            Delay20ms();                      /* 检测到松手后再次延时约 20ms,过滤释放抖动。 */
            LED2 = ~LED2;                     /* 翻转 P1.1,使每次完整操作 K2 后 LED2 切换一次状态。 */
        }                                     /* KEY2 按键事件处理结束。 */

        if (SUN == 0)                        /* 读取 P3.7;本实验模块在光线较强、超过阈值时把 DO 拉低。 */
        {                                     /* 光敏模块 DO 为低电平时的分支开始。 */
            LED3 = 1;                         /* 将 P1.2 置高;因 LED3 低电平有效,此操作会在亮处熄灭 LED3。 */
        }                                     /* SUN 为 0 的处理分支结束。 */
        else                                  /* SUN 不为 0 时进入此分支,对应本实验中光线较弱的状态。 */
        {                                     /* 光敏模块 DO 为高电平时的分支开始。 */
            LED3 = 0;                         /* 将 P1.2 拉低以点亮 LED3,模拟环境变暗后自动开启路灯。 */
        }                                     /* SUN 为高电平的处理分支结束。 */
    }                                         /* 一轮输入检测结束,立即回到 while 顶部重新采样。 */
}                                             /* 主函数的语法边界;正常运行时不会越过内部无限循环返回。 */

一、输入检测代码部分

本节使用 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)  /* 读取 P3.2,判断低电平有效的 K1 当前是否正被按住。 */
{               /* 按键为低电平时的分支开始。 */
    LED1 = 0;    /* 将 P1.0 拉低,形成 LED1 的灌电流回路,使 LED1 点亮。 */
}               /* 按下分支结束。 */
else            /* KEY1 不为 0,表示按键处于松开状态。 */
{               /* 按键松开分支开始。 */
    LED1 = 1;    /* 将 P1.0 置高,使 LED1 两端缺少足够正向压差,从而熄灭。 */
}               /* 松开分支结束;这种写法让 LED 状态直接跟随按键电平。 */

这段代码让 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; /* 每进入一次就翻转 LED1;按住期间会高速翻转很多次,所以最终状态不可预测。 */
}                /* 本次判断结束,但主循环会立即再次判断 KEY1。 */

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;产品设计应参考器件资料或示波器实测。

按键检测需要解决两类问题:

  1. 抖动重复触发:一次机械动作中出现多次 0/1 跳变。
  2. 持续电平重复触发:即使没有抖动,按住按键时 KEY1 仍会长时间等于 0,快速轮询也会重复执行取反。

有效的处理流程是:

发现 KEY1=0
      │
      ▼
等待约 20 ms,避开按下抖动
      │
      ▼
确认按键仍然为低电平
      │
      ▼
等待 KEY1 变为 1,也就是等待松手
      │
      ▼
再等待约 20 ms,避开松手抖动
      │
      ▼
执行一次 LED1 状态翻转

上方完整源码使用了其中的延时、松手等待和状态翻转。为了进一步排除偶然出现的短低脉冲,更严谨的写法还会在第一次延时后再次确认按键仍然按下:

if (KEY1 == 0)           /* 首次读到低电平,只把它视为“可能按下”,尚未立即改变 LED。 */
{                        /* 首次按下判断成立后的处理开始。 */
    Delay20ms();          /* 等待约 20ms,让按下阶段的机械抖动有时间稳定。 */
    if (KEY1 == 0)        /* 延时后再次采样;仍为低电平才确认这不是短暂干扰。 */
    {                     /* 二次确认按下有效后的处理开始。 */
        while (KEY1 == 0); /* 按键未松开时阻塞等待,P3.2 恢复高电平后才退出空循环。 */
        Delay20ms();      /* 松手后再延时约 20ms,过滤触点断开时的抖动。 */
        LED1 = ~LED1;     /* 一次完整且确认有效的按压结束后,仅翻转一次 LED1。 */
    }                     /* 二次确认分支结束。 */
}                         /* 本次 KEY1 检测结束。 */

这个二次确认并不会改变“低电平有效”的硬件事实,只是避免将很短的干扰脉冲当成真实按压。

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)           /* 捕捉 P3.2 上可能代表 K1 按下的低电平。 */
{                        /* KEY1 按下处理开始。 */
    Delay20ms();          /* 跳过按下初期约 20ms 的机械抖动区间。 */
    while (KEY1 == 0);    /* 按键仍为低电平时原地等待;末尾分号表示循环体为空,松手后才继续。 */
    Delay20ms();          /* 松手后再等待约 20ms,避开释放阶段的抖动。 */
    LED1 = ~LED1;         /* 完成一次按下和松手后翻转 LED1,使一次操作只对应一次状态变化。 */
}                         /* KEY1 事件处理结束。 */

逐句理解如下:

  1. if (KEY1 == 0):捕捉到可能的按下动作。
  2. Delay20ms();:跳过按下初期的抖动区间。
  3. while (KEY1 == 0);:只要按键仍为低电平,就停在这个空循环里等待松手。末尾分号非常关键,它表示循环体为空。
  4. 第二个 Delay20ms();:按键刚松开时先等待一段时间,避开松手抖动。
  5. 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源码写入 LED3LED3 实际状态
光线较强阻值较小01灭
光线较弱阻值较大10亮

LED3 之所以在写 0 时点亮,是因为它同样采用 +5V → LED3 → 10 kΩ → P1.2 的低电平有效电路。最终程序为:

if (SUN == 0)  /* 读取光敏模块 DO;在本次模块和阈值设置下,0 表示环境光较强。 */
{              /* SUN 为低电平时的处理分支开始。 */
    LED3 = 1; // 光强:熄灭路灯
}              /* 亮处关灯分支结束。 */
else           /* SUN 不为 0 时,按本次实测关系视为环境光较弱。 */
{              /* SUN 为高电平时的处理分支开始。 */
    LED3 = 0; // 光弱:点亮路灯
}              /* 暗处开灯分支结束。 */

判断条件和 LED 赋值是成对的:也可以判断 SUN == 1 后执行 LED3 = 0,效果相同。调试未知模块时,应先观察 DO 或模块状态灯在明暗环境下的变化,再决定软件极性,不能仅凭引脚名猜测。

光敏模块的 DO 与按键一样都能作为数字输入读取,但光敏电阻没有机械触点,因此通常不需要按键式“等待松手”消抖。若光照恰好位于比较阈值附近,DO 仍可能因光线噪声和比较器门限反复跳变;可以调节电位器使工作点远离临界位置,或在程序中采用多次采样确认、时间滤波和迟滞判断。