按钮开关不是一个「有状态」的元件 —— 它只是两片会弹的金属碰在一起,而你在按钮上会遇到的每一个问题,都是从这一点长出来的。把引脚设成 INPUT_PULLUP,开关另一端接 GND,按下就读到 LOW;接点一动,先无视它 40 ms;而急停,一定要从它的常闭那一对接出来。
你需要哪一种开关:点动、自锁,还是保持型?
各种按钮开关之间的差别,其实只有一样 —— 你松手之后,接点怎么办。点动式(momentary)一松手就立刻弹回去。自锁式(latching)按下去就停在那里,要有一个刻意的动作才放得开:LA38 急停按钮被拍下去就锁住,只有把蘑菇头旋转一下才会弹回来,而蘑菇头上模出来的那两个弧形箭头,讲的就是这件事。保持式(maintained)则是你把它拨到哪里,它就留在哪里。
这个差别落在你的程序里,不落在接线上。点动按钮交给 Arduino 的是一个事件:程序必须抓住电平改变的那一刻,并且记住它代表什么。自锁或保持式开关交给它的则是一个状态:你想什么时候读这个引脚都行,答案照样成立。
| 类型 | 你松手之后 | 程序读到的是 | 例子 |
|---|---|---|---|
| 点动(小型轻触) | 弹回断开 | 一个要抓的事件 | 6×6 轻触开关、12×12 带帽轻触开关 |
| 点动(面板式) | 弹回断开 | 一个要抓的事件 | R13-507、LA38 平头 |
| 自锁 | 一直按着,旋转才放开 | 一个随时可读的状态 | LA38 急停按钮 |
| 保持 | 拨到哪里就停在哪里 | 一个随时可读的状态 | 船型开关、E-TEN 1121 拨动开关 |
| 电子自锁 | 每碰一次翻转一次 | 一个随时可读的状态 | 双稳态触摸模块 |
零件清单 —— 未消抖对比已消抖的实作,外加一颗真正的面板式急停
值得一起加
LA38 平头点动按钮用的是跟急停同一款 22 mm 本体、同一款螺丝端子接点块,所以两颗放在一起,就能在同一个产品家族里把点动和自锁讲清楚。R13-507 是经典的 16 mm 面板按钮,但它那两个端子是焊片 —— 要准备一把烙铁和一米线材,我们的排针焊接教程把手法走了一遍。至于那条备用 A 转 B 数据线,就是拿来给 Uno 烧录程序的。
NO 和 NC 是什么意思?为什么每一颗急停都走 NC?
常开(NO)和常闭(NC)讲的是接点在静止、没人碰它的时候是什么样子。NO 接点在按下之前是断开的;NC 接点在按下之前是导通的。几乎每一颗小按钮都是 NO —— 轻触开关是,R13-507 也是。这张零件清单上的例外是 LA38 家族:不管是平头点动还是急停,背后的接点块都是一对 NO 加一对 NC 各带一组,各自占自己的螺丝端子。
现在把故障走一遍。如果你把机器的停止信号接在 NO 那一对上:静止时电路是断的,按下去才闭合、才宣告停止。可是把这颗按钮的线剪断,外面看不出任何变化 —— 电路本来就是断的。这颗本来要救你的按钮已经死了,而唯一能揭穿它的测试,就是在真的出事的时候去按它。
同样的信号改接 NC 那一对,逻辑整个反过来。静止时接点是闭合的,电流流得过去,控制器把这股电流读成「一切正常」。拍下蘑菇头,它断开:停机。线被剪断、螺丝松掉、插头被扯脱 —— 一样是断开,一样是停机。现在每一种接线失效的方式,都落在安全的那一边,而这就是急停一定走 NC 的全部理由。

为什么没人碰按钮,Arduino 却读到「已按下」?
Arduino 的输入引脚量的是电压,而静止的开关一点电压也没给它。设成单纯 INPUT 的引脚,除了芯片里那个高阻抗的感测放大器之外什么都没接上,所以它的电压就交给任何飘过来的杂散电荷去决定 —— 你的手带过来的市电哼声、旁边那根正在切换的线。它一下读到 HIGH,一下 LOW,一下又 HIGH。这就是悬空输入,而那颗「自己会按自己」的按钮,永远都是这个原因。
解法是在开关什么都没做的那整段时间里,给这个引脚一个明确的电压。pinMode(pin, INPUT_PULLUP) 会在 ATmega328P 内部接上一颗 20–50 kΩ 的电阻,一头是引脚,一头是 5 V。开关断开时,这颗电阻把引脚牢牢按在 5 V,所以每一次都读到 HIGH。把开关的另一端接到 GND,一按下去,引脚就透过不到一欧姆的电阻直通 0 V —— 几十千欧是吵不赢一条直接短路的,引脚立刻掉到 LOW。
所以「按下 = LOW」,这看起来反直觉,大概别扭一个下午就习惯了。换来的是:两条线搞定一颗按钮、不必外接电阻,而且任何一瞬间引脚都有确定的电平。
6×6 轻触开关的四只脚,到底该用哪两只?
6×6 轻触开关有四只脚,却只有两个电路。它的脚每边出来两只,而每一只脚在壳子里面,都跟隔着身体正对面的那一只连成一体 —— 按不按都一样,那一对正对的脚本来就是同一片金属。按帽的工作,是把其中一对正对脚桥接到另一对。Hokuriku、Omron 和 SCHURTER 为自家 6×6 产品画的内部电路都是同一个样子,钻孔图也同样是横向 6.5 mm、纵向 4.5 mm,所以连在一起的那一对,永远是宽的那一对。
所以要避开的,正是隔着身体正对面的那一对。把这两只脚接上去,等于给你的输入焊了一条永久短路:引脚从上电那一刻起就坐在 0 V,按钮永远读到「已按下」,这也是最常见的那一张「我的按钮卡住了」工单。改挑对角的两只脚,不管这颗开关当初是朝哪个方向掉进板子的,你都保证一边取一只、各拿到一个电路。把它跨在面包板中间那道沟上,这条规则就变成看得见的东西:共用同一个编号行的那两只脚就是内部相连的一对,所以两条线要从不同的行取。
每一颗四脚小型开关都吃这一套规则,12×12 带帽按钮也不例外:同样两片内部金属、同样走对角,只是上面的目标大一点。它的脚距比 6×6 宽,所以按下去之前先对准,再垂直往下压 —— 把轻触开关歪着摇进板子会把脚掰开,而对一颗里面根本没有电子元件的零件来说,掰弯一只脚是唯一能把它弄坏的方法。

| 从 | 接到 | 为什么 |
|---|---|---|
| Uno GND | 面包板蓝色负极轨 | 整个作品共用同一路接地 |
| 6×6 轻触开关,任一只脚 | Uno D2 | 未消抖的按钮,让你看得见弹跳 |
| 6×6 轻触开关,对角那只脚 | 蓝色负极轨 | 按下去就把 D2 拉到 0 V |
| 12×12 带帽按钮,任一只脚 | Uno D3 | 已消抖的按钮 |
| 12×12 带帽按钮,对角那只脚 | 蓝色负极轨 | 同样的两线接法 |
| LA38 急停,NC 的一个螺丝端子 | Uno D4 | 安全时闭合,所以 D4 坐在 LOW |
| LA38 急停,NC 的另一个螺丝端子 | 蓝色负极轨 | 这一对上任何一处断开都读到 HIGH |
这七条连接全部用普通的公对公杜邦线:Uno 的排针是排母、面包板的孔也是插座,而 LA38 的螺丝端子正好夹得住一根裸露的公针端 —— 把针整根塞到压片底下,让螺丝咬到金属,不要咬到塑料胶壳。我们那篇杜邦线教程把公母的算术整个走了一遍。
那条单独的接地引线值回它一条杜邦线。Uno R3 全板一共只有三个 GND 插座,而这个作品本身就要三个地;从 GND 拉一条到面包板的蓝色负极轨,之后你加的每一颗开关、传感器、模块都接到这条轨上,不必再去抢板子上那几个插座。所有零件只有在「零点在哪里」这件事上取得共识,才会对 LOW 是什么有共识,而这个共识就是接地轨。
怎么给按钮消抖,才是真的有用?
这一页上的每一颗开关,从 6×6 轻触到 LA38,闭合的方式都是把两片会弹的金属压在一起,所以它们一碰上,就会先弹开、再碰回来,来回好几次才安定下来。Jack Ganssle 把十八颗开关接上示波器量过:十六颗普通开关平均抖动 1,557 µs,最差的一次 6,200 µs,还有一颗坏样品足足抖了 157 ms。Uno 一秒钟跑几万次 loop(),所以每一次弹跳它都当成一次全新的按下。
下面那 40 ms 是这样来的,不是拜出来的:它把 6.2 ms 的最差情况吞了六次都还有余,而且离「人开始觉得按钮迟钝」的大约 100 ms 还差得远。
最诱人的错误解法,是在 loop() 里塞一句 delay(50)。它在一份什么都不干的示范程序上有效,然后毒死每一个用上它的作品:那 50 ms 里,Arduino 读不到第二颗按钮、查不了急停,也刷不了显示屏。正确的做法是记下电平改变的那一毫秒,然后在新电平稳住之前拒绝相信它。millis() 是由一个硬件计时器数的,你的程序看不看它,它都照样在数。
// Arduino Uno 上的按钮开关:未消抖 vs 已消抖,外加一路故障安全急停。
// 每一颗开关都只是把自己的引脚短到 GND,所以不必外接任何电阻。
// D2 6x6 轻触开关,取对角两只脚:一只到 D2,一只到 GND 轨 (未消抖)
// D3 12x12 轻触开关,取对角两只脚:一只到 D3,一只到 GND 轨 (已消抖)
// D4 急停的 NC 那一对:一个螺丝端子到 D4,另一个到 GND 轨
// GND 轨 = 面包板的蓝色负极轨,由 Uno 的一个 GND 引脚供一次
// D13 板载 LED 在这里代表那台机器
const uint8_t PIN_RAW = 2;
const uint8_t PIN_CLEAN = 3;
const uint8_t PIN_ESTOP = 4;
// Ganssle 量过 16 颗普通开关,最差情况有 6.2 ms 的弹跳。
// 40 ms 清掉它还留了很大余量,而且仍然低于人开始觉得按钮
// 迟钝的那个 ~100 ms。
const unsigned long DEBOUNCE_MS = 40;
unsigned long rawCount = 0, cleanCount = 0;
bool rawLast = HIGH;
bool cleanReading = HIGH; // 引脚此刻说的是什么
bool cleanAccepted = HIGH; // 我们已经同意相信的那个电平
unsigned long cleanChangedAt = 0;
bool wasHalted = false;
void report() {
Serial.print(F("raw "));
Serial.print(rawCount);
Serial.print(F(" debounced "));
Serial.println(cleanCount);
}
void setup() {
Serial.begin(9600);
pinMode(PIN_RAW, INPUT_PULLUP);
pinMode(PIN_CLEAN, INPUT_PULLUP);
pinMode(PIN_ESTOP, INPUT_PULLUP);
pinMode(LED_BUILTIN, OUTPUT);
Serial.println(F("Press each button once, then compare the two counts."));
}
void loop() {
// --- 急停每一圈都最先读。 --------------------------------------------
// 一切正常时 NC 那一对是闭合的,把引脚按在 0 V。拍下蘑菇头会让它断开,
// 而线被剪断、螺丝松脱、引线被拔掉,也一样会断。这几种情况全都读到
// HIGH,也全都把负载停掉。这就是故障安全在实际上的意思。
bool halted = (digitalRead(PIN_ESTOP) == HIGH);
digitalWrite(LED_BUILTIN, halted ? LOW : HIGH);
if (halted != wasHalted) {
Serial.println(halted ? F("E-STOP OPEN - load off")
: F("E-stop closed - load on"));
wasHalted = halted;
}
if (halted) return; // 只要这条回路是断的,下面的东西一律不跑
// --- D2:引脚一动就相信它。 ------------------------------------------
bool rawNow = digitalRead(PIN_RAW);
if (rawLast == HIGH && rawNow == LOW) { // 上拉被打败了:接点闭合
rawCount++;
report();
}
rawLast = rawNow;
// --- D3:等电平稳住了才相信它。 --------------------------------------
bool cleanNow = digitalRead(PIN_CLEAN);
if (cleanNow != cleanReading) { // 电平动了:重新开始计时
cleanReading = cleanNow;
cleanChangedAt = millis();
}
// millis() 自己会一直数下去,所以等待的这段时间里 loop() 的其余部分
// 一点都不会被卡住 —— 这就是它跟 delay(40) 的根本差别。
if (millis() - cleanChangedAt >= DEBOUNCE_MS && cleanReading != cleanAccepted) {
cleanAccepted = cleanReading;
if (cleanAccepted == LOW) {
cleanCount++;
report();
}
}
}
它编译出来是 2,722 字节,占 Uno 闪存的 8%。打开串口监视器、波特率 9600,两颗按钮各按十下,然后看两个计数:消抖那个说十,未消抖那个更多。多出来的就是弹跳。
急停还没接上去的时候,这份程序会一直报 E-STOP OPEN,两颗按钮怎么按都不理。那不是程序坏了,那是故障安全逻辑把一个断开的回路读得完全正确 —— 而且这是验证这套逻辑最快的方法。从 D4 拉一条杜邦线到负极轨,冒充一个闭合的接点,剩下的部分就活过来了。

示波器上的抖动本身长这样:
面板式急停按钮怎么接到 Arduino?
LA38 急停按钮从 22 mm 的面板孔穿下去,背后的接点块在螺丝端子上给你一对 NO 和一对 NC。取 NC 那一对:一个端子到 D4,另一个到 GND,D4 设 INPUT_PULLUP。蘑菇头还弹着的时候,闭合的接点把 D4 按在 0 V;一拍下去,锁扣打开,上拉赢了,D4 变成 HIGH,负载停下来。线断掉也是同一个结果。
接之前先把 NC 那一对找出来。接点块会在螺丝旁边标出功能,你要的那一对写 NC、另一对写 NO;万一标记磨掉了,或者只印编号不印字母,这颗开关自己会告诉你:拿万用表的通断档去量其中一对,蘑菇头弹出状态下会响的那一对,就是常闭。Uno 也答得了这个问题 —— 把一对接到 D4 和 GND,上传上面那份程序,什么都没按却报负载 ON 的那一对就是 NC。在这里猜错,你拿到的正是上面讲过的那种接在 NO 上的急停:在实验台上完美无缺,真出事的时候一声不吭。
把它装到任何会动的东西上之前,先把这一段读完。接在 Arduino 输入上的急停,停的是你的程序。它停不了马达、加热棒或主轴:那些东西照样带着电,而一份跑飞的程序、或者一个被自己逻辑锁住的驱动器,会在按钮已经拍到底的情况下继续让它们转。真正的机器急停,是切断执行机构自己的供电,切在电源路径上,那里轮不到软件投票 —— 这一面我们的继电器安全教程讲了。Arduino 那一路照接,但绝不能拿它来取代。
LA38 的接点块同时带着市电等级的额定值,而那不是一份邀请。这里所有东西都是 5 V 逻辑;市电布线是持证电工的活,不在本文范围内。如果你要切的是车间机器自己的供电,一个封装好的 KAO-5M 启动/停止按钮盒才是对的零件 —— 那是电源开关,不是 GPIO 零件。
LA38 本体的实物上手:
按钮的接线端子有哪四种?
按钮的端子,就是你的线接上去的那一块,而它决定了这份活需要什么工具。
| 端子 | 长什么样 | 怎么接上去 | 例子 |
|---|---|---|---|
| 直插脚 | 弯折的银色脚,每边两只 | 插进面包板,或者焊上去 | 6×6 轻触开关 |
| 焊片 | 中间开一个孔的扁平金属片 | 把线穿过孔勾住再焊 | R13-507 |
| 螺丝端子 | 透明盖子底下的银色螺丝 | 夹住裸线,或者一根公针端 | LA38 急停 |
| 模块排针 | 小 PCB 上三根弯脚针 | 公对母杜邦线,完全不用焊 | 按钮传感器模块 |
只有模块那一款是两端都免焊的。R13-507 要先算进去:它那两个焊片需要一把烙铁、一点线材,再加上跟着我们那篇焊接教程的几分钟。它的规格书给的额定是 125 VAC 6 A、250 VAC 3 A,装在一个 16 mm 的孔上 —— 远远超过一个 5 V 引脚会向它要的任何东西。
这个额定值正是信号开关和电源开关的分界线,而它在这一页上摆动得很大。12×12 带帽按钮的额定是 12 V DC 0.1 A,只有 R13-507 过得了的电流的六十分之一。对上面那个作品来说,这两个数字都无所谓,因为一个 INPUT_PULLUP 引脚最多也就从接点里拉走四分之一毫安。但你一旦决定把 LED 灯条、水泵或风扇改成从按钮里过,它立刻就有所谓了:吃不消的接点会拉弧、烧出麻点,最后熔焊在一起,而熔死的接点是卡在 ON —— 对任何装着马达的东西来说,这是最糟糕的一种坏法。让负载走继电器或者 MOSFET,按钮只负责说「什么时候」。
我们从真实客户那里看到的常见错误
把轻触开关接在正对的那一对脚上。板子一上电按钮就读到「已按下」,代码怎么改都没用,因为那两只脚本来就是同一片金属。把其中一条线移到对角那只脚。
引脚留在单纯的 INPUT。读数飘来飘去,手一靠近程序就自己触发,看起来完全像开关坏了。一个词 —— INPUT_PULLUP —— 就把这一整类故障拿掉。
用 delay() 来消抖。按钮是听话了,然后第二颗按钮开始漏按、显示屏开始卡顿。那个 delay 从来不是免费的,它只是把账记到了程序其余的部分头上。
急停接在 NO 那一对。在实验台上测起来完美,到了现场却无声失效,因为对控制器来说,一条断掉的线和一颗没人按的按钮长得一模一样。改到 NC,故障本身就变成一次停机。
常见问题
四脚按钮开关该接哪两只脚?
任选一对对角的脚。隔着身体正对面的那两只脚在内部本来就是连着的,接那一对等于做了一条永久短路,按钮会永远读到「已按下」。
Arduino 按钮需要外接电阻吗?
不需要外接。pinMode(pin, INPUT_PULLUP) 会启用 ATmega328P 内部一颗 20–50 kΩ 的电阻,把引脚按在 5 V,所以开关只需要负责把引脚接到 GND。按下之后读到的就是 LOW。
点动和自锁按钮开关有什么分别?
点动开关只有在你按着的时候才闭合。自锁开关则停在你推到的位置:LA38 急停按钮被拍下去就锁住,只有把蘑菇头旋转一下才放开。
红色急停按钮为什么要接常闭?
为了让故障本身变成停机。NC 接点在一切正常时导通,按下按钮才断开。线被剪断、螺丝松脱、插头被扯掉也一样会断,所以每一种接线故障产生的都是一次停机,而不是一颗悄悄失效的按钮。
Arduino 按钮消抖时间该设多久?
普通开关大约 40 ms。实测普通接点的弹跳峰值接近 6 ms,所以 40 ms 留了很大余量,同时又低于人察觉得到延迟的那个门槛。用 millis() 计时,永远不要用 delay()。
最后更新:2026 年 8 月。有问题?WhatsApp 联系我们。



Arduino Uno Compatible SMD UNO R3 with Type B Cable - ATMEGA328P with CH340G-Microcontroller Project
MB102 Breadboard 170 400 830 Holes Breadboard Donut Board Arduino Prototype Multi Color - BREADBOARD (400 HOLES)
40pcs Dupont Wire 10cm 20cm 30cm for Breadboard DIY Experiment Jumper Wire Breadboard wire - DUPONT WIRE M-M 20CM
Tactile Button Momentary Switch 6mm x 6mm x 5mm Push Button 4 Pin
Push Button with Cap Tactile Switch with Round Hat 12mm x 12mm x 7.3mm - PUSH BUTTON(GREEN)
Industrial Emergency Momentary Button LA38 Push Switch RED Push To Stop Button - EMERGENCY BUTTON
Industrial Emergency Momentary Button LA38 Push Switch RED Push To Stop Button - MOMENTARY BUTTON
R13-507 Push Button Switch No-Locking Round Momentary Red Green Yellow Blue White Black - R13-507 BUTTON (RED)
Data Cable Type-A Type-C MicroUSB Type-B 0.5m 1m 30cm 0.3m 100cm Data Transfer Upload Code - TYPE-A TO TYPE-B (1.0M)