Arduino 拍手开关:KY-037 声音传感器两次拍手控制灯,阈值这样调

Cartoon of two hands clapping beside a red KY-037 sound sensor board with a silver microphone capsule and blue trimmer, teal sound waves travelling from the clap into the microphone, with a red relay module with a blue relay cube and a lit table lamp with a cream shade to the right

一个用得住的拍手开关,读的是 KY-037 的模拟脚,不是它的数字脚:以 ADC 允许的最快速度采 A0,量出信号在 20 ms 的窗口里摆动了多大,再把间隔 120–800 ms 的两次摆动算作一次翻转。满网教程都在教的那套「拧电位器 + D0」,在台面上能用,到傍晚就飘了;这一套稳得住。

做一个 Arduino 拍手开关,需要哪些零件?

KY-037 声音传感器是一块红色小板,一头是两条弯脚撑起来的银色驻极体话筒,板上还有一颗蓝色微调电位器、一颗 LM393,以及四根直的排针。这排针把接线方式定死了:它直接插进面包板,再用公对公杜邦线接到 Uno 的排母上。

选配 —— USB 数据线与面板指示灯

ItemPriceQty
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
Round Head LED 3mm / 5mm / 8mm - Red/Yellow/Blue/Green/White - 5mm LED (GREEN)Round Head LED 3mm / 5mm / 8mm - Red/Yellow/Blue/Green/White - 5mm LED (GREEN)5GRELEDRM0.10
Electronic Component Set Beginner Electrical Learning Kit For Arduino Beginner Learning - ELECTRICAL STARTER KITElectronic Component Set Beginner Electrical Learning Kit For Arduino Beginner Learning - ELECTRICAL STARTER KITELECTRKRM39.95

Uno 那个插座认的是全尺寸 USB-B(打印机那种)方头 —— 抽屉里没有这个形状的线就加一条。这个作品的第一阶段只让 Uno 板上自带的 13 脚 LED 闪,什么都不用多买;那颗 5 mm 绿色 LED 是给面板做指示灯用的,串在它前面的限流电阻不能省 —— 一般取 220 Ω,入门套件里的电阻排就能找到。

继电器模块要选 5 V 版本,因为它那颗 Songle SRD-05VDC-SL-C 的线圈可以直接吃 Uno 自己的 5 V 轨;12 V 模块是同一块红板换了 12 V 线圈,属于本来就有 12 V 的系统。公对母杜邦线可以把面包板整个省掉;电脑跟 CH340 驱动过不去的话,就选 FT232 版 Uno

KY-037 是怎么把一次拍手变成信号的?

KY-037 的驻极体话筒,是从 + 脚经一颗电阻偏置起来的,而声音是压强的振荡,所以那个节点上是一个骑在稳定直流电平上的交流信号。一次拍手会让它在几毫秒里往静息电压的上下两边猛摆,随后房间的反射再拖着它振荡几十毫秒才平息。A0 接的就是这个节点。

KY-037 的 LM393 拿这个节点去和蓝色微调电位器设定的一个参考电压比较 —— 那颗电位器标着 104,10 后面四个 0,也就是 100 kΩ —— 再用比较结果驱动 D0。这个输出是开集的:里面只有一个能把引脚拉到地的三极管,没有任何能把它推上去的东西,所以板子自己配了上拉电阻和一颗指示 LED。D0 回答的是一个被硬件焊死的问题;A0 给的是整个信号。

引脚(按丝印顺序) 接到 它承载什么
A0 Uno A0 话筒节点本身 —— 骑在直流偏置上的交流摆动
G 电源轨(GND) 地。注意它排在第二位,不在最后
+ + 电源轨(5 V) 话筒偏置和 LM393 的供电
D0 不接 比较器输出。这份代码不用它
卡通信号图:深蓝色音频波形上有两段响亮的爆发,一条橙色虚线画出固定阈值、每段爆发都反复穿过它并在每个交点上标着橙色圆点,另有一条淡青色的自适应底噪带,每段爆发上各画一支青色双箭头量出它的峰峰高度
一次拍手,六次穿越阈值。橙色那条线是电位器设定的东西;青色那条带是代码量出来的底噪,而青色箭头才是它从每段爆发里取走的那一个数字。

KY-037 和继电器模块怎么接到 Uno 上?

Uno 只有一个 5 V 插孔,而这里有两个模块要用,所以先从那个插孔把面包板的电源轨喂起来,两路供电都从轨上取。插 KY-037 的时候照着它的丝印来:地排在这一排的第二位,所以模块插错一格,就是把 5 V 灌进模拟输入。继电器板的两条边上各有一个三位的螺丝端子 —— 触点这一侧是 NO/COM/NC,控制那一侧是 IN/DC−/DC+ —— 所以每颗螺丝底下压一根杜邦线的针脚。

接到 为什么
Uno 5VGND + 电源轨 两根公对公杜邦线。一个插孔就变成两个模块可以共用的一整条轨
KY-037 +G + 电源轨 话筒偏置和比较器的供电
KY-037 A0 Uno A0 代码要量的就是这一路信号
继电器 DC+DC− + 电源轨 给板子和线圈供电 —— 吸合时约 71 mA
继电器 IN Uno D8 只驱动光耦里那颗 LED,几个毫安而已
继电器 COMNO 继电器不动作、或者板子没电的时候,灯都是灭的
选配 LED Uno D13 → 220 Ω → LED 长脚,短脚回 电源轨 面板指示灯,和代码已经在驱动的板载 LED 并联。那颗电阻不是可选项

为什么「拧电位器 + D0」的拍手开关永远调不定?

KY-037 的那颗微调电位器只设定一个数字 —— 话筒节点要越过多高的电压,D0 才会翻转 —— 而这个唯一的固定阈值会带来三个问题。我们那篇 LDR 光敏电阻自动灯对光讲的是同一套道理。

阈值是固定的,房间不是。一台风扇、打在铁皮屋顶上的雨、空调压缩机启动,都会把环境声抬高几十毫伏,所以晚上十一点调好的那个点,到了中午不是漏掉拍手,就是被路过的车流触发。电位器对它所在的这个房间一无所知。

一次拍手是很多个边沿。波形上冲时越过阈值一次,回落时再越一次,之后每一个还够响的反射又各越一次,所以一次拍手出来的是一串毛糙的脉冲。按边沿翻转的代码就会翻好几次 —— 这正是最朴素的那版拍手开关只会闪、不会切换的原因。

贴着设定点它会抖。声音在阈值附近徘徊时会反复穿过它 —— 而这颗微调还是多圈的,螺丝要拧很多圈才走完整个量程,拧到两端还会打滑「咔咔」响。「拧到能用为止」,结果往往是螺丝已经顶死在一头了:

D0 不是没用,但它带不动这个继电器 —— 至少不是按想当然的那个方向。LM393 的灌电流很强,拉电流却只能靠自己那颗上拉电阻挤出来,不到一毫安,而光耦的 LED 要的是几毫安 —— 所以跳线帽必须挪到 L,让继电器在 D0 为 LOW 时吸合。而在 KY-037 上,LOW 恰恰是静息状态,拍手才是 HIGH —— 板子自己那颗 D0 指示 LED 会跟着翻,所以在接任何线之前,你就能亲眼看着这支脚回应一次拍手。于是安静的时候灯一直亮着,有噪声反而灭掉;把灯改接到 NC 而不是 NO,噪声又变回把灯点亮,代价是丢掉失效安全 —— 因为接在 NC 上的灯,在板子断电时也是亮的。不管哪一种,它都只是瞬时的。要「翻转」就要有记忆,要有记忆就要有代码。

拍手开关的代码是怎么在 A0 上认出一次拍手的?

量摆幅,不量电平。代码对 KY-037 的 A0 脚全速采样 20 ms,只留下这段里的最大值和最小值;两者之差就是峰峰摆幅。20 ms 长到足以让一次拍手最响的那几毫秒总能落进某一个窗口,又短到让后面那套双击计时还有够细的颗粒度。两个采样相减把直流偏置消掉了,所以根本不必知道静息电平在哪,信号先往上摆还是先往下摆也一样。

Uno 的 ADC 跑在 125 kHz 的时钟上 —— 16 MHz 除以 Arduino 内核默认的 128 分频 —— 每次转换要 13 个周期,所以 analogRead() 每秒大约给出 9,600 个采样,一个窗口里约 190 个。这远远不够重建音频,也不需要:高于采样率一半的成分不会凭空消失,而是折叠回低频,照样落成一个个大幅度的采样。我们量的是信号动得多猛,不是它在说什么。

阈值于是就是房间自己的底噪,加上一段余量。代码把安静窗口的摆幅做平均,每个窗口只往新值挪 3% —— 一步 20 ms,所以跟上房间的变化大约要三十个窗口、三分之二秒 —— 并且跳过任何含有拍手的窗口,这样一次拍手就永远抬不高它刚刚越过的那道杠。80 个计数的余量,就是 80 × 4.9 mV ≈ 0.39 V 的额外摆幅 —— 比一个安静房间自己游走的幅度高出一大截,作为一臂之遥拍手的起手数字很合适。A0 是话筒自己的节点,后面没有放大器,所以你一往后退,这个摆幅掉得很快:串口会把你的拍手实际量到多少打印出来,余量就跟着那个数字走。

卡通时序图:一道深蓝色的拍手尖峰之后是一段灰色的不应期方块,再接一段宽阔的青色接受窗口,窗口里的第二道尖峰点亮了一盏米白配深蓝的台灯;下方是被拒绝的单次拍手情形,末端是一个橙色禁止符号和一盏没亮的灯
一次拍手,一段把自己的回声吞掉的装聋时间,然后是一扇「第二次拍手才算数」的窗口。单独一次拍手会一路走到窗口尽头,什么也不会改变。

两次拍手怎么变成刚好一次翻转?

第一次被接受的摆动会启动一个时钟,之后的 120 ms 代码是故意装聋的:一次拍手的直达声只有几毫秒,但它的反射会把电平撑住久得多,而且没有人能在十分之一秒里拍两下 —— 所以这段时间里的一切,都是同一次拍手。从 120 ms 到 800 ms,第二次摆动就把灯翻转。过了 800 ms 状态机放弃,所以一记关门声自己不会改变任何东西,一声大笑也不会。

这两个数字都是常数,不是物理定律:第二次拍手老是来得偏晚,就把 GAP_MAX 放宽;在硬墙贴砖的房间里串口把一次拍手数成两次,就把 REFRACTORY 加长。

/*
  拍手开关 - KY-037 声音传感器 -> Arduino Uno -> 5 V 继电器模块。
  不需要任何库。

  KY-037  +  -> 面包板 + 电源轨(5 V)      G  -> 面包板 - 电源轨(GND)
  KY-037  A0 -> Uno A0                      D0 -> 这份代码不用
  继电器  DC+ -> + 电源轨   DC- -> - 电源轨  IN -> Uno D8
  继电器的跳线帽保持出厂时的 H(高电平触发)。

  这份代码量的是话筒信号在一小段窗口里摆动了多大,而不是它停在什么电平上,
  所以直流偏置和那颗微调电位器都无关紧要。
  计时窗口内的两次摆动,就把灯翻转一次。
*/

const uint8_t PIN_MIC   = A0;
const uint8_t PIN_RELAY = 8;
const uint8_t PIN_LED   = 13;   // Uno 板上自带的 LED - 第一阶段不用继电器

const uint16_t WINDOW_MS   = 20;    // 一个测量窗口的长度
const int      CLAP_MARGIN = 80;    // 拍手要比房间高出多少个 ADC 计数
const uint16_t REFRACTORY  = 120;   // 拍手之后装聋的毫秒数(吞掉它自己的回声)
const uint16_t GAP_MAX     = 800;   // 毫秒:第二次拍手最晚可以在何时到

// 跳线帽在 H(出厂状态)= HIGH 吸合。挪到 L 就把这里设成 false。
const bool RELAY_ACTIVE_HIGH = true;

float    ambient     = 0;   // 滚动的底噪,单位是每个窗口的摆幅计数
bool     lampOn      = false;
uint8_t  state       = 0;   // 0 = 等第一次拍手,1 = 等第二次拍手
uint32_t lastClapAt  = 0;
uint32_t firstClapAt = 0;

// 用 analogRead() 能达到的最快速度采样 WINDOW_MS,返回这段里的
// 峰峰差。最大值减最小值,直流偏置就被消掉了。
int measureWindow() {
  uint16_t lo = 1023, hi = 0;
  uint32_t start = millis();
  while (millis() - start < WINDOW_MS) {
    uint16_t s = analogRead(PIN_MIC);
    if (s < lo) lo = s;
    if (s > hi) hi = s;
  }
  return (int)(hi - lo);
}

void setLamp(bool on) {
  lampOn = on;
  digitalWrite(PIN_LED, on ? HIGH : LOW);
  digitalWrite(PIN_RELAY, (on == RELAY_ACTIVE_HIGH) ? HIGH : LOW);
}

void setup() {
  pinMode(PIN_RELAY, OUTPUT);
  pinMode(PIN_LED, OUTPUT);
  setLamp(false);                 // 别的事情之前,先把继电器驱到关闭
  Serial.begin(115200);

  // 认一认这个房间:50 个窗口大约一秒。这段时间里请保持安静。
  long sum = 0;
  for (uint8_t i = 0; i < 50; i++) sum += measureWindow();
  ambient = sum / 50.0;

  Serial.print(F("noise floor "));
  Serial.print(ambient, 1);
  Serial.print(F(" counts, clap needs "));
  Serial.println(ambient + CLAP_MARGIN, 1);
}

void loop() {
  int      amp  = measureWindow();
  uint32_t now  = millis();
  bool     loud = amp > (int)(ambient + CLAP_MARGIN);

  // 只有安静的窗口才挪动底噪:每 20 ms 的窗口挪 3%,大约三分之二秒
  // 跟上房间的变化,而一次 20 ms 的拍手永远撼不动它。
  if (!loud) ambient += 0.03f * (amp - ambient);

  bool clap = loud && (now - lastClapAt >= REFRACTORY);
  if (clap) {
    lastClapAt = now;
    Serial.print(F("clap, swing "));
    Serial.println(amp);
  }

  if (state == 0) {
    if (clap) { firstClapAt = now; state = 1; }
  } else {
    if (clap) {
      setLamp(!lampOn);
      Serial.println(lampOn ? F("lamp ON") : F("lamp OFF"));
      state = 0;
    } else if (now - firstClapAt > GAP_MAX) {
      state = 0;                  // 单独一次拍手什么都不改变
    }
  }
}

在 Uno 上这份代码编译出来占 4,418 字节 Flash —— 32 KB 的 13% —— 加 214 字节 RAM。先别接继电器就跑起来:同一份代码会驱动 Uno 板上自带的 13 脚 LED,所以在任何东西「咔嗒」响之前,你可以先把拍手认稳。串口监视器设 115200,会打印出量到的底噪和每一次拍手的摆幅,所以漏掉的那一次拍手,会告诉你它差了多少。

继电器这一级是怎么把灯切换掉的?

继电器模块上那颗 SRD-05VDC-SL-C 的线圈是 70 Ω,所以吸合一次要吃 5 V ÷ 70 Ω ≈ 71 mA。加上 Uno 自己的 45 mA 左右,再加上传感器那几个毫安,整套大约落在 120 mA —— 在 Uno 那颗 USB 自恢复保险丝允许的 500 mA 以内,这也正是这里不需要第二组电源的原因。Uno 的引脚从来不承担那 71 mA:IN 只驱动光耦里的 LED,几个毫安而已,线圈是由 DC+ 喂的。这里光耦的两侧共用 DC−,所以它起的是缓冲而不是隔离作用:真正管用的那层隔离在继电器内部 —— 它的触点和线圈本来就是机械上分开的两块金属。

黑色跳线帽出厂时扣在 H 上,也就是高电平触发 —— 板子在那个三针小排针的一端印着 L、另一端印着 H,通电之前扫一眼就能确认 —— 而这个默认值是对的:Arduino 的引脚从上电到 setup() 跑起来之间都是高阻输入,所以 IN 悬空或为 LOW 时,整个启动过程灯都是灭的。它同时也是「我的继电器反着动」最常见的原因,因为从低电平触发的板子上抄来的代码,是写 LOW 来开灯的。把跳线帽挪到 L,就必须把 RELAY_ACTIVE_HIGH 设成 false

卡通俯视图:一块红色单路继电器模块,中间是蓝色方块状继电器,两条边上各有一个蓝色三位螺丝端子,板上有红绿两个指示点,一个橙色圆圈标出那个三针跳线排针,跳线帽扣在靠右的两针上
从上往下看这块 5 V 继电器模块:一边的螺丝端子是被切换的触点,另一边是控制端,橙色圈出来的那个小跳线帽,决定的是 HIGH 还是 LOW 把灯点亮。

把灯接在 COMNO 之间,这样继电器不动作、或者整块板子没电的时候,灯都是暗的。这两颗螺丝是一个机械开关,不是一个输出:灯用的还是它自己那组电源 —— 把那组电源的正极线剪断,一头压进 COM,另一头压进 NO,Uno 那条轨上的电就永远流不到灯上。外壳上印着触点能承受多少:250 V AC 10 A、30 V DC 10 A,阻性负载。这里的一切都停在低压直流 —— 一条自带电源的 12 V 灯带,或者一盏小的直流灯。240 V 的灯自带另一套规矩:外壳、爬电距离、线缆载流量,还有你手能不能碰到的端子 —— 先读我们的继电器模块安全指南。拍手是词汇量只有一个词的语音控制,所以语音识别模块是从这里往上走最自然的一步;如果你要的是跟着「动作」走的灯,而不是跟着声音走的,那 PIR 人体感应才是更对的传感器。

真实客户身上常见的错误

指望它在大声音乐里也能用。只有安静的窗口才会挪动底噪 —— 这恰恰就是拍手抬不高自己那道杠的原因 —— 所以一段持续的大声音,就是一道底噪永远爬不上去的杠,代码于是把它读成一次接一次接一次的拍手。把 CLAP_MARGIN 调大、把传感器挪离音箱,或者接受一件事:拍手开关听的是一个安静房间之上的瞬态,而不是在一个吵闹房间里面听。

对着 D0 的每一个边沿做翻转。一次拍手产生六个边沿就翻六次,灯最后停在哪全看运气。修法不是去数边沿,而是在第一个边沿之后,把那 120 ms 里的一切都忽略掉。

想靠拧电位器来修一个模拟方案。那颗微调设定的是 LM393 的参考电压,而这个参考只到得了 D0 —— 它碰不到这份代码读的 A0 信号。

常见问题

为什么我的拍手开关会自己开开关关?

有东西在计时窗口里把阈值清过了两次。把 CLAP_MARGIN 调大,并且让传感器离继电器的「咔嗒」声远一点。

能用 KY-037 的 D0 脚代替 A0 吗?

做一个瞬时的声控继电器可以,前提是记住 D0 静息为 LOW、有声音才变 HIGH。做两次拍手的翻转就不行 —— 一个固定阈值,一次拍手好几个边沿。

为什么我的继电器动作方向反了?

跳线帽出厂扣在 H 上,所以 HIGH 才让线圈吸合,而低电平触发的代码写的是 LOW。要么挪跳线帽,要么把 RELAY_ACTIVE_HIGH 设成 false

Arduino 的 5 V 脚能带得动继电器模块吗?

5 V 那款可以:线圈 71 mA,对 USB 的 500 mA 预算。12 V 的线圈在 5 V 上是吸不合的。

我能站多远拍手?

只要摆幅还能比底噪高出 CLAP_MARGIN 就行。声压随距离衰减,底噪不会 —— 要么把余量调低,要么把传感器挪近。

最后更新:2026 年 8 月。有问题?WhatsApp 联系我们

Leave a Reply

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