Arduino 按钮开关接线:NO/NC、上拉与消抖

Cartoon of a large red mushroom emergency stop button with a yellow collar beside a green-capped square tactile switch and a tiny black 6x6 tactile switch with silver legs

按钮开关不是一个「有状态」的元件 —— 它只是两片会弹的金属碰在一起,而你在按钮上会遇到的每一个问题,都是从这一点长出来的。把引脚设成 INPUT_PULLUP,开关另一端接 GND,按下就读到 LOW;接点一动,先无视它 40 ms;而急停,一定要从它的常闭那一对接出来。

你需要哪一种开关:点动、自锁,还是保持型?

各种按钮开关之间的差别,其实只有一样 —— 你松手之后,接点怎么办。点动式(momentary)一松手就立刻弹回去。自锁式(latching)按下去就停在那里,要有一个刻意的动作才放得开:LA38 急停按钮被拍下去就锁住,只有把蘑菇头旋转一下才会弹回来,而蘑菇头上模出来的那两个弧形箭头,讲的就是这件事。保持式(maintained)则是你把它拨到哪里,它就留在哪里。

这个差别落在你的程序里,不落在接线上。点动按钮交给 Arduino 的是一个事件:程序必须抓住电平改变的那一刻,并且记住它代表什么。自锁或保持式开关交给它的则是一个状态:你想什么时候读这个引脚都行,答案照样成立。

类型 你松手之后 程序读到的是 例子
点动(小型轻触) 弹回断开 一个要抓的事件 6×6 轻触开关12×12 带帽轻触开关
点动(面板式) 弹回断开 一个要抓的事件 R13-507LA38 平头
自锁 一直按着,旋转才放开 一个随时可读的状态 LA38 急停按钮
保持 拨到哪里就停在哪里 一个随时可读的状态 船型开关E-TEN 1121 拨动开关
电子自锁 每碰一次翻转一次 一个随时可读的状态 双稳态触摸模块

值得一起加

ItemPriceQty
Industrial Emergency Momentary Button LA38 Push Switch RED Push To Stop Button - MOMENTARY BUTTONIndustrial Emergency Momentary Button LA38 Push Switch RED Push To Stop Button - MOMENTARY BUTTONRMOMENBRM9.95
R13-507 Push Button Switch No-Locking Round Momentary Red Green Yellow Blue White Black - R13-507 BUTTON (RED)R13-507 Push Button Switch No-Locking Round Momentary Red Green Yellow Blue White Black - R13-507 BUTTON (RED)R507REDRM2.45
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)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)ATBC100RM4.90

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 的全部理由。

卡通对比图:两颗 22mm 面板按钮,黑色本体和橙色螺丝端子块一模一样,一颗是银色圈里的红色平头镜片,另一颗是黄色颈圈上的红色蘑菇头
同一款 22 mm 本体,两种行为。红色平头一松手就直接弹回;蘑菇头则按下去锁住,非得旋转一下才肯放。

为什么没人碰按钮,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 宽,所以按下去之前先对准,再垂直往下压 —— 把轻触开关歪着摇进板子会把脚掰开,而对一颗里面根本没有电子元件的零件来说,掰弯一只脚是唯一能把它弄坏的方法。

卡通俯视图:一颗黑色 6x6 轻触开关剖开露出两片银色内部金属条,各自连着一对正对的脚,一条青色线画过一对对角脚,两对正对脚上各打一个红色叉
每一只脚在壳子里早就跟正对面那只连成一体了。任选一对对角脚 —— 千万别选正对着的那两只。
接到 为什么
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 拉一条杜邦线到负极轨,冒充一个闭合的接点,剩下的部分就活过来了。

卡通波形对比图:左边一条深藏青色的线裂成六道参差的尖峰,右边则是一个干净的单一台阶,每一处电平变化都用青色圈了出来
同一次按下,看两遍。未消抖时,接点在 HIGH 和 LOW 之间来回抖好几次;消抖之后,程序等到线稳住才报一次按下。

示波器上的抖动本身长这样:

面板式急停按钮怎么接到 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 联系我们

Leave a Reply

Your email address will not be published. Required fields are marked *