基础入门

第 8 课:按钮——数字输入与消抖

板子第一次"听"你说话。但你说的它可能听成七八句

我是吴老师。前两节课,板子一直在往外输出——点灯、说话。

这节课反过来了:板子要开始"听"。

这是它第一次接收来自你的信号。而这里有个特别有意思的现象:你明明只按了一下,它却觉得你按了七八下。

一、数字输入:只有两种回答

跟数字输出对应,数字输入也只有两种可能——高电平,或者低电平。

这跟你家里的门铃一样:要么有人按,要么没人按,没有中间状态。

所以读按钮的代码,返回值就两种情况:

读到的值 意思
HIGH(高电平) 一种状态
LOW(低电平) 另一种状态

注意我故意没说"高电平 = 按下了"。 因为按下去到底是高还是低,取决于你怎么接线——下一节就讲这个。

这里有个新手常见误区:以为 HIGH 就是"按下"、LOW 就是"没按"。

不是的。HIGH 只表示"这根引脚上是 3.3V",至于 3.3V 对应按没按,得看你的电路

所以你写完按钮程序,第一件事永远是:按一下,看数值变成几。 别猜。

二、接线:三根线

按钮模块跟 LED 一样,也是三根线:

按钮模块 接到
VCC 3.3V
GND GND
OUT GPIO4(信号线,接任意 GPIO 都行)

接好之后,先别急着写程序。做一件事:用万用表量 OUT 那根脚。

  1. 万用表拨到直流电压档
  2. 黑表笔搭 GND红表笔搭 OUT
  3. 不按的时候,读数是多少?
  4. 按住按钮,读数变成多少?

记下这两个数。 你会发现一个是 3.3V 左右,一个是 0V 左右。

这就告诉你答案了——你这块板子上,"按下"对应的是哪个电平。后面写代码就按这个来,不用背。

这就是第 5 课那个"先测,再猜"的实际应用。别去猜、别去查教程,你自己的板子,量一下最准。

但这里有个问题:不按时,那个电压是"真的"吗

你量的时候可能发现:不按按钮,OUT 上的电压似乎是 3.3V,但它稳定吗?

这就是按钮最关键的一个坑,得单独讲。

上拉电阻的作用:避免引脚悬空,给它一个确定的默认电平

引脚不能"悬空"。

想象一下:按钮没按下的时候,信号引脚和电源、和地都没连通——它就像一个没人管的开关,悬在半空中。

这时候引脚上的电压是多少?说不准。 它可能被周围环境的电磁干扰影响,在 0 到 3.3V 之间随机乱飘

板子读到的值就会忽 0 忽 1,毫无规律。

对策叫上拉电阻:给引脚接一个电阻通到 3.3V。

它的作用就是:给引脚一个"默认答案"。 你没按的时候,它稳稳地是 3.3V;你按下去接地,它变成 0V。

打个比方:员工没事干的时候,你得给他派个默认的活,不然他就自己瞎晃悠。上拉电阻就是这个"默认的活"。

好消息:你的板子已经内置了这个电阻。

你在代码里写 pinMode(4, INPUT_PULLUP)(注意多了 _PULLUP),板子内部就自动把上拉电阻接上了,不用你自己加元件

这个 _PULLUP 很重要,忘了它,你的按钮就会"抽风"。

三、怎么读:程序里只有一句话

C++
int state = digitalRead(4);    // 读 GPIO4 现在是高还是低

就这么一句。 digitalRead ——"读一个数字值"。

但有个必须注意的地方

读按钮,一定要放在循环里

C++
void setup() {
  Serial.begin(115200);
  pinMode(4, INPUT_PULLUP);     // 注意 INPUT_PULLUP
}

void loop() {
  Serial.println(digitalRead(4));   // 不停地读、不停地打印
  delay(100);
}

为什么必须放循环里?

因为 digitalRead 读的是**"这一刻"的值。它像拍照片**——咔嚓一下,记录当下的状态。

你只拍一张,怎么知道你按了没有? 你得不停地拍,才能捕捉到"按下"这件事。

所以读按钮的标准写法是:loop 里不停地读

这也解释了为什么第 7 课的串口打印在按钮课上特别重要。

灯只能告诉你"亮/不亮",但按钮的问题是"什么时候读到什么值"——这是时间上的事,只有打印出来看得见。

烧进去,打开串口监视器,你会看到一串 0 和 1 在滚。按一下按钮,看它怎么变。

四、那个奇怪的数字:抖动

现在注意看。

你按住按钮不放,串口里打出来的数字——是不是在跳?

不是干净利落的从 0 变 1,而是在飞快地 0、1、0、1、1、0、1…… 跳几下才稳定。

这就是"抖动"。

按钮抖动:按下瞬间电平快速跳变多次,消抖后只保留一次变化

为什么会这样

按钮里面是两片金属。你按下去的时候,它们不是"啪"一下就贴合的——

中间有几毫秒,两片金属在不停地碰、分开、碰、分开。

这个时间短到你手完全感觉不到。但板子有多快?ESP32-C3 一秒钟能查上百万次。

结果就是:你按了一下,程序先后读到了几十次"通了",中间还夹着几次"断了"。

这不是按钮坏了,也不是你接错了。 任何机械开关都有这个问题,只是快到你感觉不到。

怎么解决:消抖

办法很朴素:读到变化之后,等一小会儿(几毫秒)再确认一次。

C++
int lastState = HIGH;      // 记着上次的状态

void loop() {
  int now = digitalRead(4);

  if (now != lastState) {    // 发现变化了
    delay(20);               // 等 20 毫秒——让抖动过去
    now = digitalRead(4);    // 再读一次确认

    if (now != lastState) {  // 确认是真的变了
      Serial.println(now);
      lastState = now;
    }
  }
}

关键在于那个 delay(20)——等 20 毫秒,抖动早就结束了,这时候读到的才是真实状态。

记住这句话:消抖不是"消除"抖动,是"忽略"它。

抖动客观存在,你消除不了。但你可以在它闹腾的时候先不看,等它安静下来再读。

但有时候你根本不需要消抖

这是我要强调的一点。

回到第 8 课开头那个问题——你要写什么程序?

  • 如果你要计数(按一次加一分),必须消抖,否则按一下分数涨几十
  • 如果你只想做**"按住就一直亮"**,根本不用消抖——抖动的几十次里,你的程序反正只关心"现在是不是按着",跳几下不影响结果

所以别背代码,先想清楚你要干什么。

五、"现在按着"和"刚按下"——两个不同的问法

这是按钮课最后一个、也是最容易搞混的点。

电平是按钮当前的状态,边沿是状态变化的那一瞬间

看那张波形图。同一个按钮信号,你可以问两个不同的问题

你想问的 这叫 什么时候为"真" 怎么实现
现在按着吗? 电平(状态) 按住的整个过程都是真 直接 digitalRead 判断
刚按下了吗? 边沿(变化) 只在变化那一瞬间为真 比较"这次"和"上次"

这两个问题,写出来的程序完全不一样。

举个例子你就懂了

"按住就亮,松开就灭" —— 这是问状态

C++
void loop() {
  int now = digitalRead(4);
  digitalWrite(3, now);    // 按钮什么状态,灯就什么状态
}

多简单——读到的值直接给灯就行了。

"按一下开,再按一下关" —— 这是问边沿

C++
int last = HIGH;

void loop() {
  int now = digitalRead(4);
  if (now != last) {          // 状态变了
    delay(20);                // 消抖
    if (digitalRead(4) != last) {
      last = digitalRead(4);
      digitalWrite(3, !digitalRead(3));   // 灯的当前状态取反
    }
  }
}

复杂多了。 因为你必须"记住上次是什么",才能知道"这次是不是刚变的"。

这里面藏着一个很重要的概念:程序没有记忆。

板子不会"记住"按钮上次是什么状态——你得自己用变量存起来(上面那个 int last)。

这个"用变量记住状态"的思路,后面做游戏机、做状态机的时候会反复用到。这是你这节课真正该带走的东西。

自测

单选题为什么读按钮的程序必须把 digitalRead 放在 loop 里?
单选题pinMode(4, INPUT_PULLUP) 里的 _PULLUP 是干什么的?
单选题"按住按钮灯就亮,松开就灭"——需要写消抖代码吗?
问答题"现在按着吗"和"刚按下了吗",这两个问题在程序实现上有什么本质区别?
拓展按钮里的两片金属,为什么按下去会"弹"?别的机械开关也有这个问题吗?

这个问题课上不讲,留给你自己找答案——**问 AI 是最好的办法**。

提示:搜"按键抖动 原理 金属弹片"。想一想:家里的电灯开关、键盘按键、鼠标微动,它们按下去的时候会抖吗?为什么我们平时感觉不到?

拓展除了"等一会儿再读",还有别的消抖办法吗?哪种更好?

这个问题课上不讲,留给你自己找答案——**问 AI 是最好的办法**。

提示:搜"消抖 硬件 RC 电路 软件消抖 对比"。想一想:如果电路上就加个电容把抖动滤掉,代码是不是就能省事?那为什么还有人坚持用软件消抖?

本节你要动手做的事

  • [ ] 接好按钮VCC3.3VGNDGNDOUTGPIO4接之前先断电
  • [ ] 用万用表量一次:不按的时候 OUT 是多少伏?按住的时候呢?把这两个数记下来
  • [ ] 打印出来看:写个循环不停打印 digitalRead(4)按住不放,亲眼看那个数字在跳
  • [ ] 实现"按住就亮":这个最简单,读到的值直接给灯
  • [ ] 实现"按一下切换":需要用到 last 变量记住上次状态——这个才是重点
  • [ ] 试试把消抖的 delay(20) 改成 delay(1)delay(200),看各有什么不同

这节课表面上在讲按钮,其实讲的是两件更底层的事。

第一件:输入端不会自己给你"想要的信号",它只给你"真实的东西"。

按钮给你的是一段带抖动的、跳来跳去的波形。把它变成"用户按了一下"这个干净的事件,是你的活——不是按钮的活。

第二件:程序没有记忆。

板子不会"记得"按钮上次是什么状态。你要它记住,就得自己用变量存

这两件事,在你后面做的每一个作品里都会遇到。今天你只是用按钮把它们演了一遍。

下一节,我们把前两节学的东西合起来——用输入控制输出,走通第一条完整闭环。那是所有实战课题的缩小版。