基础入门
第 8 课:按钮——数字输入与消抖
板子第一次"听"你说话。但你说的它可能听成七八句
我是吴老师。前两节课,板子一直在往外输出——点灯、说话。
这节课反过来了:板子要开始"听"。
这是它第一次接收来自你的信号。而这里有个特别有意思的现象:你明明只按了一下,它却觉得你按了七八下。
一、数字输入:只有两种回答
跟数字输出对应,数字输入也只有两种可能——高电平,或者低电平。
这跟你家里的门铃一样:要么有人按,要么没人按,没有中间状态。
所以读按钮的代码,返回值就两种情况:
| 读到的值 | 意思 |
|---|---|
HIGH(高电平) |
一种状态 |
LOW(低电平) |
另一种状态 |
注意我故意没说"高电平 = 按下了"。 因为按下去到底是高还是低,取决于你怎么接线——下一节就讲这个。
这里有个新手常见误区:以为
HIGH就是"按下"、LOW就是"没按"。不是的。
HIGH只表示"这根引脚上是 3.3V",至于 3.3V 对应按没按,得看你的电路。所以你写完按钮程序,第一件事永远是:按一下,看数值变成几。 别猜。
二、接线:三根线
按钮模块跟 LED 一样,也是三根线:
| 按钮模块 | 接到 |
|---|---|
| VCC | 3.3V |
| GND | GND |
OUT |
GPIO4(信号线,接任意 GPIO 都行) |
接好之后,先别急着写程序。做一件事:用万用表量 OUT 那根脚。
- 万用表拨到直流电压档
- 黑表笔搭 GND,红表笔搭
OUT - 不按的时候,读数是多少?
- 按住按钮,读数变成多少?
记下这两个数。 你会发现一个是 3.3V 左右,一个是 0V 左右。
这就告诉你答案了——你这块板子上,"按下"对应的是哪个电平。后面写代码就按这个来,不用背。
这就是第 5 课那个"先测,再猜"的实际应用。别去猜、别去查教程,你自己的板子,量一下最准。
但这里有个问题:不按时,那个电压是"真的"吗
你量的时候可能发现:不按按钮,OUT 上的电压似乎是 3.3V,但它稳定吗?
这就是按钮最关键的一个坑,得单独讲。
引脚不能"悬空"。
想象一下:按钮没按下的时候,信号引脚和电源、和地都没连通——它就像一个没人管的开关,悬在半空中。
这时候引脚上的电压是多少?说不准。 它可能被周围环境的电磁干扰影响,在 0 到 3.3V 之间随机乱飘。
板子读到的值就会忽 0 忽 1,毫无规律。
对策叫上拉电阻:给引脚接一个电阻通到 3.3V。
它的作用就是:给引脚一个"默认答案"。 你没按的时候,它稳稳地是 3.3V;你按下去接地,它变成 0V。
打个比方:员工没事干的时候,你得给他派个默认的活,不然他就自己瞎晃悠。上拉电阻就是这个"默认的活"。
好消息:你的板子已经内置了这个电阻。
你在代码里写
pinMode(4, INPUT_PULLUP)(注意多了_PULLUP),板子内部就自动把上拉电阻接上了,不用你自己加元件。这个
_PULLUP很重要,忘了它,你的按钮就会"抽风"。
三、怎么读:程序里只有一句话
int state = digitalRead(4); // 读 GPIO4 现在是高还是低就这么一句。 digitalRead ——"读一个数字值"。
但有个必须注意的地方:
读按钮,一定要放在循环里
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 一秒钟能查上百万次。
结果就是:你按了一下,程序先后读到了几十次"通了",中间还夹着几次"断了"。
这不是按钮坏了,也不是你接错了。 任何机械开关都有这个问题,只是快到你感觉不到。
怎么解决:消抖
办法很朴素:读到变化之后,等一小会儿(几毫秒)再确认一次。
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 判断 |
| 刚按下了吗? | 边沿(变化) | 只在变化那一瞬间为真 | 比较"这次"和"上次" |
这两个问题,写出来的程序完全不一样。
举个例子你就懂了
"按住就亮,松开就灭" —— 这是问状态:
void loop() {
int now = digitalRead(4);
digitalWrite(3, now); // 按钮什么状态,灯就什么状态
}多简单——读到的值直接给灯就行了。
"按一下开,再按一下关" —— 这是问边沿:
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 里?答案与解析
digitalRead 像拍照片——咔嚓一下记录当下。你只拍一张,怎么知道你按了?得不停地拍,才能捕捉到"按下"这个事件。放 setup 里只执行一次,之后就再也不读了。
pinMode(4, INPUT_PULLUP) 里的 _PULLUP 是干什么的?答案与解析
按钮不按时,引脚跟电源和地都不连通,处于"悬空"状态——电压会随机乱飘,读数毫无规律。上拉电阻就是给引脚一个"默认答案"。好消息是你这块板子内置了它,写 _PULLUP 就自动生效,不用外加元件。
答案与解析
消抖不是必需的,要看你的程序在干什么。 这里程序问的是状态——按住的整个过程中,抖动的几十次读取结果里绝大多数都是"按下",灯的表现完全不受影响。而当你要"计数"时就必须消抖,否则按一下分数涨几十。
答题要点
答题要点应该包含:"现在按着吗"问的是电平,直接 digitalRead 判断当前值就行,不需要额外信息。"刚按下了吗"问的是边沿,程序必须知道"上次是什么状态",才能判断出"这次变了"——所以必须用一个变量把上次的状态存起来。本质区别在于:前者只需要"当前信息",后者需要"当前 + 历史"。这个"用变量记住状态"的思路,是所有状态类程序的基础。
拓展按钮里的两片金属,为什么按下去会"弹"?别的机械开关也有这个问题吗?
这个问题课上不讲,留给你自己找答案——**问 AI 是最好的办法**。
提示:搜"按键抖动 原理 金属弹片"。想一想:家里的电灯开关、键盘按键、鼠标微动,它们按下去的时候会抖吗?为什么我们平时感觉不到?
拓展除了"等一会儿再读",还有别的消抖办法吗?哪种更好?
这个问题课上不讲,留给你自己找答案——**问 AI 是最好的办法**。
提示:搜"消抖 硬件 RC 电路 软件消抖 对比"。想一想:如果电路上就加个电容把抖动滤掉,代码是不是就能省事?那为什么还有人坚持用软件消抖?
本节你要动手做的事
- [ ] 接好按钮:
VCC→3.3V,GND→GND,OUT→GPIO4(接之前先断电) - [ ] 用万用表量一次:不按的时候
OUT是多少伏?按住的时候呢?把这两个数记下来 - [ ] 打印出来看:写个循环不停打印
digitalRead(4),按住不放,亲眼看那个数字在跳 - [ ] 实现"按住就亮":这个最简单,读到的值直接给灯
- [ ] 实现"按一下切换":需要用到
last变量记住上次状态——这个才是重点 - [ ] 试试把消抖的
delay(20)改成delay(1)和delay(200),看各有什么不同
这节课表面上在讲按钮,其实讲的是两件更底层的事。
第一件:输入端不会自己给你"想要的信号",它只给你"真实的东西"。
按钮给你的是一段带抖动的、跳来跳去的波形。把它变成"用户按了一下"这个干净的事件,是你的活——不是按钮的活。
第二件:程序没有记忆。
板子不会"记得"按钮上次是什么状态。你要它记住,就得自己用变量存。
这两件事,在你后面做的每一个作品里都会遇到。今天你只是用按钮把它们演了一遍。
下一节,我们把前两节学的东西合起来——用输入控制输出,走通第一条完整闭环。那是所有实战课题的缩小版。