先说结论:这盏 Arduino 智能小灯,把人体感应、光线感应和蓝牙控制三套逻辑集成在了一起,天黑有人经过就自动亮,光线充足时再多人走也不亮,手机连上蓝牙还能随时手动开关。做这个项目的起因很实在——我家玄关开关离门口太远,每天拎着东西进门都要先摸黑找墙,冬天晚上起夜更是憋着一口气走到客厅才能开灯。市面上的成品感应灯要么没有环境光判断,白天晃晃悠悠乱亮,要么设置死板不能手动干预,我就动了手搓一套的心思。
现在这盏灯放在玄关实测了两周,自动模式几乎没出过错,蓝牙手动控制也稳定,所以我把整个项目的选型思路、接线方式、完整代码和踩坑记录都整理了出来。这篇攻略适合刚接触 Arduino 的朋友照着做,也适合有基础的人直接拿走逻辑改到自己场景里。
1. 三种控制的需求拆解:为什么人体感应、光线感应、蓝牙一个都不能少
1.1 单一传感器的局限
很多人最开始想的只是"我买个人体感应模块不就完了",实际用起来就会发现单靠人体感应做灯非常难受。HC-SR501 这类 PIR 传感器只要检测到移动的人体就会输出高电平,完全不区分白天黑夜,结果就是大白天阳光晒着,你从旁边经过它照样亮。灯倒是亮了,但纯属浪费电,白天亮着还显得很蠢。
只加光线感应也不行。光敏电阻只能告诉你"环境亮不亮",没法知道"有没有人来了",它压根就是个被动测量元件,永远无法主动触发开灯动作。至于蓝牙控制,它连"自动化"都算不上,本质是把手机当成一个遥控器,人还是得掏手机,比传统开关先进不了多少。
所以这三样东西其实是互补关系,不是堆砌功能。
1.2 三层逻辑怎么协同
我的实际决策逻辑是这样的:人体感应负责"触发",光线感应负责"条件",蓝牙负责"覆盖"。
- 触发层:HC-SR501 检测到人体移动,产生一个高电平脉冲,这是开灯的"请求信号"。
- 条件层:光敏电阻持续读取环境光强,只有光线暗到设定阈值以下,自动开灯这个动作才被允许执行。
- 覆盖层:特殊情况需要跳出自动化逻辑,比如白天我想看柜子下面的暗角,或者晚上坐在沙发上看书不想让玄关灯自动亮,直接用 App 发指令强制开灯或强制关灯,把自动模式临时切掉。
最终在代码里体现为:自动模式 = 环境光暗且人体信号有效,两者用 AND 逻辑串起来;蓝牙手动模式拥更高优先级,A 开、B 关、C 重新回到自动。
1.3 适合谁做、需要什么基础
这个项目不需要你有电路设计经验,认电路板上的字母方向、按图示插线就够了。软件方面只要能在 Arduino IDE 里复制粘贴代码、会选对开发板和串口号,剩下的逻辑我会一行一行拆开讲。整个项目预算我压在五十块钱以内:Arduino Uno 兼容板二十几块,HC-SR501 三四块,HC-06 蓝牙模块十块出头,光敏电阻和电阻电容这些加起来不到五块。
如果你的手机是 iPhone,直接看完全文再做决定——HC-06 这类经典蓝牙模块在 iOS 上兼容性很差,我会在蓝牙章节单独讲这个问题,以及更省事的替代方案。
2. 关键元器件选型与原理:PIR、光敏电阻、HC-06 各自怎么工作
2.1 人体感应核心:HC-SR501 PIR 传感器
HC-SR501 是目前最普及的热释电红外传感器模块,这块板子的核心是一个热释电传感元件,外面罩着半球形的菲涅尔透镜。人体体温大约 37℃,辐射的红外线波长集中在 9.4 微米左右,透镜把环境中的红外辐射汇聚到传感器表面,传感器本身只对"红外辐射的变化"敏感——换言之,它检测的是移动的热源,一个静止坐在沙发上的人反而触发不了它。
模块上有两个电位器和一个跳线帽,很多人买了直接用,根本不知道它们是干嘛的。
- 灵敏度电位器:调节检测距离,顺时针拧到底大概能覆盖 7 米范围,逆时针拧回来最小大概 3 米左右。家用小灯建议调到 3 到 4 米,太远容易把门外路过的人、对面楼的人照进来。
- 延时电位器:调节输出高电平的持续时间,可调范围约 5 秒到 300 秒。这里有个关键建议:把延时调到最短(逆时针到底附近),让硬件只负责产生一个短脉冲,真正的"亮灯维持时长"完全交给 Arduino 代码来控制。原因是代码改一个变量就能调整时长,而每次蹲下去拧电位器非常难受。
- 跳线帽:旁边标着 H 和 L。H 是可重复触发模式,检测到人持续输出高电平;L 是不可重复触发,人一直在也不会重新触发。本次项目要跳在 H 上,配合代码实现"人只要没走远灯就不会灭"的效果。
HC-SR501 工作电压 5V,输出 TTL 电平,Arduino 的数字引脚可以直接识别。上电初期有约 30 到 60 秒的预热时间,这个阶段它会随机乱触发,不是故障,测试时等 1 分钟再开始。
2.2 光线感应:光敏电阻与分压电路
光线感应方案我选的是最简单的光敏电阻(LDR)加固定电阻分压。你可能在网上看到过 BH1750 数字光照传感器,精度更高、直接输出勒克斯数值,但对于"判断环境暗不暗"这种二元决策,光敏电阻的精度完全够用,价格还便宜一大截。
电路原理是个初中物理就讲过的串联分压。我用的是这个接法:光敏电阻一端接 5V,另一端引到模拟输入 A0,同时 A0 通过一只 10KΩ 电阻接地。光敏电阻的阻值会随光照变化——光线越强,阻值越小;光线越暗,阻值越大。于是 A0 点的电压就跟着光强变化,Arduino 的analogRead会把 0 到 5V 的电压映射成 0 到 1023 的整数。
具体数值可以算一下感受量级:假设强光时光敏电阻降到 5KΩ,A0 点分压是 5 × 10 / (5 + 10) ≈ 3.33V,模拟读数接近 682;暗处光敏电阻升到 100KΩ,A0 点分压是 5 × 10 / (100 + 10) ≈ 0.45V,模拟读数只有 92 左右。中间的分界线我建议起始阈值设 300,然后在实测里根据你的房间环境微调。
有一点必须提醒:网上另一个常见接法是固定电阻接 5V、光敏电阻接地,那个接法下模拟值和光照的关系正好反过来,光越强数值越小。两种接法都能用,但代码判断方向的写法完全不同,后文会给出对应的改进说明,别照抄接完发现逻辑反了。
2.3 蓝牙通信:HC-06 从机模块与 AT 指令
蓝牙模块我选了 HC-06。HC-05 和 HC-06 外观几乎一样,但 HC-05 支持主从一体、可以配对其他主机设备,而 HC-06 是纯从机,只等手机来连接。这个项目里只需要手机主动连模块,HC-06 够用,价格又低,配置还简单。
HC-06 默认串口波特率 9600,默认名称通常是"HC-06",配对密码 1234 或者 0000。模块通过串口和 Arduino 通信,协议上没有任何额外封装,手机 App 发送一个字符,模块就把这个字符原样吐给 Arduino 的串口引脚。我最终用字符指令实现控制:A 开灯,B 关灯,C 回自动模式。
给 HC-06 改名或者改波特率需要进 AT 指令模式——HC-06 的 AT 指令直接通过串口发就行,不要求像 HC-05 那样上电前按住按键,这块很多教程混着写容易把人绕晕。需要用 AT 指令时,把模块的 TX、RX 先接到 USB 转 TTL 上,在电脑串口助手发送AT,能收到OK表示通信正常。但我必须说一句大实话:这个项目用默认配置就能跑,AT 指令属于锦上添花,不改名也能连,新手不必一开始就折腾它。
2.4 为什么用 Arduino Uno 打底,为什么不直接用 ESP32
很多做智能硬件的人会问:ESP32 自带蓝牙和 WiFi,价格和 Uno 差不多,为什么绕一圈用 Uno 再加蓝牙模块?我的回答是:Uno 有最庞大的教程资源和最成熟的排错路径。PIR 不触发、光敏数值不对、串口乱码,这些问题在任何平台都会遇到,但用 Uno 时你能在网上找到一万篇解法,对新手来说是巨大的隐形优势。
另一个原因涉及电平兼容性。HC-06 的 IO 耐压是 3.3V 级别,虽然很多教程直接把它接到 5V 的 Arduino 串口上也能用,这属于"没出事的幸存者偏差",我见过不止一个模块因此烧毁。Uno 配 HC-06 需要在数据线上做分压,而 ESP32 本身就是 3.3V 电平,接这类模块反而省心。所以这篇文章主流程用 Uno,但我会在最后一个章节专门讲,如果换成 ESP32,代码迁移只要改蓝牙初始化部分,其余逻辑几乎不动。
3. 硬件搭建全记录:接线表、供电方式和安装细节
3.1 元器件清单
| 元件 | 规格/型号 | 数量 | 参考价格 |
|---|---|---|---|
| 主控板 | Arduino Uno R3 兼容板 | 1 | 约 25 元 |
| 人体感应 | HC-SR501 PIR 模块 | 1 | 约 4 元 |
| 光线感应 | 光敏电阻 GL5528 或同类 | 1 | 约 1 元 |
| 固定电阻 | 10KΩ(1/4W 即可) | 1 | 几毛钱 |
| 蓝牙模块 | HC-06 | 1 | 约 12 元 |
| 输出负载 | LED + 220Ω 限流电阻 | 1 组 | 几毛钱 |
| 面包板 | 400 孔以上 | 1 | 约 8 元 |
| 跳线 | 公对公若干 | - | 约 5 元 |
| 供电 | USB 线 + 5V 充电头 | 1 | 家里现成的 |
如果最终想控制 220V 灯具,把 LED 部分替换成 5V 继电器模块再接灯,这个后文单说,原型阶段用 LED 验证逻辑最安全。
3.2 接线对照表
这是整个项目最重要的接线表,按照 1:1 对照着插就能用。我建议最后再接蓝牙模块,先把 PIR 和光敏电阻调通,减少排查范围。
| 模块 | 模块引脚 | Arduino 引脚 | 说明 |
|---|---|---|---|
| HC-SR501 | VCC | 5V | 模块供电 |
| HC-SR501 | GND | GND | 共地 |
| HC-SR501 | OUT | D2 | 人体信号输出 |
| 光敏电阻 | 一端 | 5V | 直接接电源 |
| 光敏电阻 | 另一端 | A0 | 分压点 |
| 10KΩ 电阻 | A0 同点 | GND | 上拉到地 |
| LED | 正极 | D9 | 经 220Ω 电阻串联 |
| LED | 负极 | GND | 直接接地 |
| HC-06 | VCC | 5V | 模块供电,注意别接反 |
| HC-06 | GND | GND | 共地 |
| HC-06 | RXD | D10 | 经 2KΩ + 3.3KΩ 分压后接入 |
| HC-06 | TXD | D11 | 3.3V 输出,Arduino 能直接识别 |
D10 到 HC-06 RXD 之间的分压是个易错点。Arduino 数字引脚输出 5V 高电平,HC-06 的 RX 引脚期望的是 3.3V 逻辑电平,直接对接长期使用有烧引脚的风险。正确做法是在 D10 和 HC-06 RXD 之间串一个 2KΩ 电阻,然后在 HC-06 RXD 对地接一个 3.3KΩ 电阻,两个电阻分压把 5V 降到约 3.1V,正好落在安全范围。HC-06 的 TXD 输出 3.3V 是它的原生电平,接到 Arduino D11 完全没问题,不需要任何处理。
3.3 供电与模块安装的注意点
整块面包板由 USB 口 5V 供电,总电流大概在 100mA 左右,充电头输出 500mA 以上的都能吃饱。要特别注意不同模块要共地,这点很多人嘴上知道但接线时容易漏,只要某个模块和 Arduino 不共地,信号电平就没有参考基准,表现就是传感器读取结果完全随机。
真实项目里我不会把传感器像实验室一样全堆在面包板上。HC-SR501 需要装在有一定高度、朝向人活动路径的位置,比如玄关鞋柜顶面;光敏电阻要放在能感受到整体环境光线但又不会被灯光直接照到的地方,否则会出现灯一开光敏电阻读数立刻变小判为"已是黑暗"的死循环——严格说这个项目的灯是开环控制,不会因为灯亮自动关灯,但如果光敏电阻正对着灯头,自动判断会明显失真。我的做法是把光敏电阻焊两根延长线,甩到灯侧的阴影位置,让传感器测的是"房间整体亮度"而不是"灯本体亮度"。
HC-06 模块天线区域最好悬空,不要贴合面包板金属接触片,也别贴在金属外壳上,信号会被屏蔽导致连接距离缩到一米内。
4. 完整代码逐段解读:状态判断、串口指令与防抖逻辑
4.1 引脚定义与全局状态
程序开头定义了所有引脚的用途,集中管理最大的好处是后续改硬件接线只要改这里,不用满代码翻。
#include <SoftwareSerial.h> #define PIR_PIN 2 // HC-SR501 输出 -> D2 #define LDR_PIN A0 // 光敏电阻分压点 -> A0 #define LED_PIN 9 // LED -> D9,支持 PWM #define BT_RX_PIN 10 // HC-06 RXD <- D10(经分压) #define BT_TX_PIN 11 // HC-06 TXD -> D11 SoftwareSerial myBT(BT_RX_PIN, BT_TX_PIN); int lightThreshold = 300; // 环境光阈值,低于它判定为"暗" const long keepOnMillis = 30000; // 自动模式下检测到人后保持亮灯 30 秒 const long triggerCooldown = 5000; // 连续触发最小间隔,防抖动 long lastTriggerTime = 0; // 上一次有效触发的毫秒时间戳 boolean autoMode = true; // 是否处于自动模式 boolean manualOn = false; // 蓝牙手动强制开灯状态 boolean lastPirState = false; // PIR 上一轮状态,用于边沿检测autoMode和manualOn是两个独立的状态变量,手动开灯时autoMode变 false、manualOn变 true;回到自动模式时autoMode变 true、manualOn清 false。这种设计比用一个布尔变量表达三种状态清晰得多,后续扩展"定时模式"时也不用重构逻辑。
4.2 传感器读取与边沿防抖
HC-SR501 的可重复触发模式下,只要人还在移动,输出就持续为高。但人在感应区边缘徘徊时,输出会有比较快的抖动,直接判断电平会导致灯一亮一灭闪个不停。我的处理方式是检测信号的上升沿而不是电平本身,并且记录一个冷却窗口。
void setup() { pinMode(PIR_PIN, INPUT); pinMode(LED_PIN, OUTPUT); digitalWrite(LED_PIN, LOW); Serial.begin(9600); myBT.begin(9600); Serial.println(F("Smart Light initialized. PIR warming up...")); } void loop() { boolean pirNow = digitalRead(PIR_PIN); int lightValue = analogRead(LDR_PIN); boolean darkEnough = (lightValue < lightThreshold); // 只在 PIR 从低跳变到高时记录一次触发,冷却期内忽略 if (pirNow && !lastPirState && (millis() - lastTriggerTime > triggerCooldown)) { lastTriggerTime = millis(); Serial.println(F("Motion detected")); } lastPirState = pirNow; // 人体仍在感应区内移动时,不断刷新"最后触发时间" if (pirNow && (millis() - lastTriggerTime > triggerCooldown)) { lastTriggerTime = millis(); } // 串口打印调试信息 static unsigned long lastPrintTime = 0; if (millis() - lastPrintTime > 500) { lastPrintTime = millis(); Serial.print(F("LDR=")); Serial.print(lightValue); Serial.print(F(" darkEnough=")); Serial.print(darkEnough); Serial.print(F(" autoMode=")); Serial.println(autoMode); } // ... 蓝牙指令解析与亮灯逻辑放在下一节 }注意这里我用了两种更新lastTriggerTime的方式:第一个是边沿触发,记录一次"刚刚有人进入";第二个是持续刷新,当 PIR 一直输出高时让计时器一直延后,表现为人体不离开感应区灯就不会进入倒计时熄灭。这两行配合起来,就实现了"人来了亮,人走 30 秒后灭"的效果。
triggerCooldown的设置很关键。第一次上电或者 PIR 预热期间,输出会有多次抖动,冷却窗口保证抖动不会在短时间内反复更新触发计时。如果你发现灯总是亮一小会儿就灭,先看是不是冷却时间设太长抢占了保持时间,其次是确认 PIR 的跳线帽确实在 H 位置。
4.3 蓝牙串口指令解析
HC-06 把手机发来的字符原样送到 Arduino 串口,我解析的指令只有三个:A 强制开灯、B 强制关灯、C 恢复自动模式。为了容错,代码同时支持大小写。
void loop() { // ... 延续上一节的传感器读取代码 // 蓝牙指令处理 while (myBT.available()) { char cmd = myBT.read(); Serial.print(F("BT CMD: ")); Serial.println(cmd); switch (cmd) { case 'A': case 'a': autoMode = false; manualOn = true; Serial.println(F("Manual ON")); break; case 'B': case 'b': autoMode = false; manualOn = false; Serial.println(F("Manual OFF")); break; case 'C': case 'c': autoMode = true; manualOn = false; Serial.println(F("Auto Mode")); break; default: // 忽略其他字符 break; } } // 亮灯逻辑 boolean shouldLight = false; if (autoMode) { // 自动模式:光线暗且检测到过人体,保持亮灯 keepOnMillis 毫秒 if (darkEnough && (millis() - lastTriggerTime < keepOnMillis)) { shouldLight = true; } } else { shouldLight = manualOn; } digitalWrite(LED_PIN, shouldLight ? HIGH : LOW); }自动模式的亮灯条件我刻意没有把"当前 PIR 为高"作为必要条件,而是看"最后触发时间距离现在是否小于 30 秒"。这样 PIR 输出短脉冲后,即使模块自身的延时旋钮调得很短,Arduino 也能控制保持时间,这正好回应了前面选型时把 PIR 延时调到最小的建议:硬件负责薄薄的"看一眼",软件负责"决定亮多久"。
蓝牙解析放在主循环里每轮都执行,响应基本是即时的。while (myBT.available())会把缓冲区里的所有字符读完,避免上一次指令残留在缓冲区里影响下一次判断。
4.4 完整可运行代码
把上面几段拼起来就是完整版,可以直接复制到 Arduino IDE。接线按照第 3 章的表格,烧录后打开串口监视器(波特率 9600),能看到实时的光敏读数和模式状态,方便后期调参。
#include <SoftwareSerial.h> #define PIR_PIN 2 #define LDR_PIN A0 #define LED_PIN 9 #define BT_RX_PIN 10 #define BT_TX_PIN 11 SoftwareSerial myBT(BT_RX_PIN, BT_TX_PIN); int lightThreshold = 300; const long keepOnMillis = 30000; const long triggerCooldown = 5000; long lastTriggerTime = 0; boolean autoMode = true; boolean manualOn = false; boolean lastPirState = false; void setup() { pinMode(PIR_PIN, INPUT); pinMode(LED_PIN, OUTPUT); digitalWrite(LED_PIN, LOW); Serial.begin(9600); myBT.begin(9600); Serial.println(F("Smart Light initialized. PIR warming up...")); } void loop() { boolean pirNow = digitalRead(PIR_PIN); int lightValue = analogRead(LDR_PIN); boolean darkEnough = (lightValue < lightThreshold); // 边沿检测:低到高跳变且冷却时间已过,记录一次触发 if (pirNow && !lastPirState && (millis() - lastTriggerTime > triggerCooldown)) { lastTriggerTime = millis(); Serial.println(F("Motion detected")); } lastPirState = pirNow; // 持续刷新:感应区内一直有人,时间戳不断更新 if (pirNow && (millis() - lastTriggerTime > triggerCooldown)) { lastTriggerTime = millis(); } // 每 500ms 打印一次调试信息 static unsigned long lastPrintTime = 0; if (millis() - lastPrintTime > 500) { lastPrintTime = millis(); Serial.print(F("LDR=")); Serial.print(lightValue); Serial.print(F(" darkEnough=")); Serial.print(darkEnough); Serial.print(F(" autoMode=")); Serial.println(autoMode); } // 蓝牙指令处理 while (myBT.available()) { char cmd = myBT.read(); Serial.print(F("BT CMD: ")); Serial.println(cmd); switch (cmd) { case 'A': case 'a': autoMode = false; manualOn = true; Serial.println(F("Manual ON")); break; case 'B': case 'b': autoMode = false; manualOn = false; Serial.println(F("Manual OFF")); break; case 'C': case 'c': autoMode = true; manualOn = false; Serial.println(F("Auto Mode")); break; default: break; } } // 亮灯逻辑 boolean shouldLight = false; if (autoMode) { if (darkEnough && (millis() - lastTriggerTime < keepOnMillis)) { shouldLight = true; } } else { shouldLight = manualOn; } digitalWrite(LED_PIN, shouldLight ? HIGH : LOW); }如果你按照"固定电阻接 5V、光敏电阻接地"的另一种接法搭的电路,只需要把darkEnough的条件从<改成>,也就是模拟值高于阈值判定为暗,其余代码完全不用动。
5. 手机蓝牙配对与控制:App 选型、调试命令和常见坑
5.1 一个让 iPhone 用户省心的结论
先说一个我在测试中踩得最深的坑:iPhone 对 HC-06 这类经典蓝牙 SPP 模块的支持非常糟糕。iOS 系统从底层就不允许普通 App 访问经典蓝牙的串口服务,市面上大多数号称兼容的 App 要么只能连接特定厂商模块,要么是历史遗留的残废版本。如果你手里只有 iPhone,我的第一条建议不是硬刚,而是把 HC-06 换成 ESP32 或者 Arduino Nano 33 BLE 这类自带 BLE 的板子,用 BLE 的 UART Service 通信,App 选择面一下就宽了。
安卓手机则没有这个限制,HC-06 配合安卓 App 体验很顺,这也是这套方案对新手最友好的组合。
5.2 推荐 App 与配对步骤
安卓端我用过比较顺手的两个:一个是 Serial Bluetooth Terminal,老牌串口终端,界面朴素但功能极全,可以自定义快捷发送按钮;另一个是国产的蓝牙调试助手,中文界面,适合第一次玩蓝牙模块的人。两者都能自定义发送内容,我把三个指令 A、B、C 各自绑了一个大按钮,实际使用时根本不用敲键盘。
配对流程按这个顺序来:
- HC-06 接好线,Arduino 上电,此时模块上的 LED 快速闪烁,表示等待配对。
- 打开手机蓝牙设置,搜索新设备,看到 HC-06(可能是默认名,也可能显示为 linvor),点击配对。
- 输入 PIN 码:HC-06 一般默认 1234,旧批次有些默认 0000,两个都试试。
- 打开 App,连接模块,如果 App 里有设备列表选择,选中 HC-06;如果没有列表,就在设置里手动选 SPP 服务。
- 波特率选 9600,和代码里
myBT.begin(9600)一致。 - 连接成功后模块 LED 会变成慢闪或者常亮,不同批次表现不同,但至少不再是快速闪烁。
5.3 常见调试问题和排查路径
配对不上是最常见的问题。先确认 Arduino 正在供电,模块指示灯在闪;再确认手机蓝牙能搜到设备——搜不到就检查 HC-06 的 VCC 和 GND 是否接反,接反模块发热,眼神不好的还能闻到糊味。能搜到但配对失败,多半是 PIN 码不对,把 1234 和 0000 都试一遍,再不行查模块批次,有些山寨模块出厂 PIN 被改成 8888。
连上了但 App 发送指令没反应,打开 Arduino IDE 的串口监视器,看看是否打印BT CMD: A。如果根本没打印,说明数据没到 Arduino,检查 D10 到 HC-06 RXD 的连接和分压电路;如果打印了但灯不亮,问题在逻辑代码或者 LED 接线,打印BT CMD: A之后应该连着打印Manual ON,看到这两行就说明代码分支进去了。
还有一个很多人会搞混的点:HC-06 的 RXD 要和 Arduino 的发引脚交叉连接,也就是 Arduino 的 D10 接模块的 RXD,Arduino 的 D11 接模块的 TXD。方向接反的现象是 App 发送没反应,但模块 TXD 到 Arduino D11 这个方向如果断了,则是连名字都收不到打印。
6. 实测表现、可靠性问题与值得尝试的进阶方向
6.1 放在玄关两周的真实表现
灯做好之后在玄关运行了两周,我把代码里keepOnMillis设成了 30 秒,光敏阈值调到 280。实测效果非常符合预期:傍晚天暗下来之后,我从门口走动,灯基本在 0.5 到 1 秒内亮起,人走出感应范围后大约 30 秒熄灭,误差很小。蓝牙控制的响应几乎感觉不到延迟,App 按钮按下去,灯基本同步亮灭。
最让我意外的是白天表现。大晴天下午从门口来回走,灯完全没反应,说明光敏阈值卡的位置很对。连续阴雨天问题就出现了,下午两三点室内已经很暗,灯在四点左右就自动亮起来,因为阈值是固定的。这不算 bug,但说明了一个设计原则:单阈值硬判断的"天暗"永远是相对值,想要更符合人的感受,要么分季节手动调,要么做成双阈值迟滞判断,或者换 BH1750 读具体勒克斯值再定上下限。
6.2 PIR 误触发处理的几条经验
PIR 误触发的头疼指数远超其他问题。实测中早上一楼阳光照进玄关,地砖反光被透镜捕捉,偶尔会有一次误触发;冬天家里有取暖器,暖风一吹也有概率触发。最有趣的一次是阳台的猫跳上鞋柜蹲了一会,灯一晚上亮了好几次。
我的处理经验按优先级排序:先把灵敏度电位器逆时针调低,这是最直接的手段;然后把模块安装位置抬高并调整角度,让透镜视场避开窗户和散热设备。PIR 先天只能感知"移动的热源",这是物理原理决定的,无法靠软件完美消除。如果误触发仍然不可接受,就该考虑换 RCWL-0516 微波雷达模块了,它用多普勒雷达检测移动目标,对温度不敏感,宠物误触率更低,代码和 HC-SR501 完全兼容,还是三根线。
6.3 代码层面的可靠性和功耗优化
当前代码每 500ms 串口打印一次调试信息,长时间运行会一直占用串口,但不影响功能。正式部署时可以删掉打印或者用宏开关包起来。另外delay()我一处没用,整个主循环是跑满的,蓝牙指令响应不会被延时阻塞,这在将来扩展功能时很重要——如果代码里出现delay(1000),蓝牙响应就会最多延迟 1 秒,体验很差。
低功耗方面,Uno 本身不是低功耗平台,待机电流就有 40mA 以上,靠电池供电不太现实。如果要做成内置锂电池的桌面小夜灯,两个方向:一是用 ESP32 的 deep sleep,没人时进入休眠、PIR 通过 RTC 引脚唤醒,整体功耗能压到 10mA 左右;二是直接用 PIR 模块的输出作为三极管开关控制 LED,Arduino 只在需要时启动,这是另一套思路,代码要整个重构。
6.4 往真灯升级:继电器模块与安全提醒
原型 LED 验证通过后,很多人下一步就是控制真实灯具。我强烈不建议把 220V 交流电直接和 Arduino 接在一起,正规做法是中间加一个带光耦隔离的 5V 继电器模块,Arduino 的 D9 接继电器 IN 引脚,模块 VCC 接 5V,再用继电器触点控制灯具火线。
这里有三条必须遵守的安全底线:
- 改造 220V 电路前断开总闸,确认断电再操作。
- Arduino、HC-06、光敏电阻这些低压部分和 220V 部分在物理上保持距离,绝缘胶带缠好裸露触点。
- 继电器模块务必买带光耦隔离的版本,不要用那种直接用三极管驱动的裸板,隔离成本就差几块钱,安全感天差地别。
6.5 迁移到 ESP32 的思路与 wokwi 在线仿真
如果你打算让灯接入 WiFi、加上 BLE 控制、以后还能对接语音助手,ESP32 是最划算的下一步。硬件上把 HC-06 完全去掉,ESP32 自带蓝牙,D2、A0、D9 这些引脚映射关系基本不变,代码里唯一大改的部分是串口初始化——ESP32 用Serial1.begin(9600)接传感器,BLE 部分则要引入BluetoothSerial.h库,调用SerialBT.begin("SmartLight")就能获得一个虚拟串口,手机用 BLE 调试助手连接后,收发逻辑和 HC-06 一模一样。
还有一件我认为特别值得推荐的事:如果你手头暂时没有硬件,或者只是想在收到货之前把逻辑跑通,可以去 Wokwi 网站在线仿真,选一个 Arduino Uno,画一个 PIR、一个光敏电阻、一个 LED,把本文代码粘进去就能直接看运行效果。我踩过的很多引脚定义错误、逻辑判断错误,其实在仿真里一分钟就能暴露出来,比接线后拿万用表量半天快得多。
这个项目做完之后,我最大的体会是"传感器 + 控制器 + 执行器"这个铁三角组合,能覆盖绝大多数入门智能硬件需求。光线感应给了系统"判断环境"的能力,人体感应给了系统"感知人"的能力,蓝牙通信则让系统接受人的干预,三者结合才是一盏不招人烦的智能灯。你在自己的应用场景里,把这三种传感器任意替换成温湿度、声音、土壤湿度,控制对象换成风扇、水泵、舵机,整套代码框架依然成立。这也是为什么我说,别小看这个小灯,它其实是智能家居里最典型的迷你样板间。