
做这个项目之前我一直觉得“智能灯”至少得是几百块的成品才能叫智能直到自己用一块Arduino UNO加上三个几十块钱的模块把人体感应、光线感应、蓝牙控制这三样东西拼在一起做出了一个“人来了才亮、天黑了才亮、手机也能随时开关”的小灯这才发现原理一点都不玄乎。这篇文章就把我踩过的坑、写好的完整代码、接线和调试的细节全部整理出来适合手里有Arduino但不知道做什么的新手也适合想学多传感器配合和串口通信的进阶玩家。1. 方案设计思路一灯三控的联动逻辑1.1 三种控制方式的优先级怎么定先说清楚这盏灯要做成什么样。放在玄关或者床头白天太阳足的时候人走过去它不亮到了晚上或者光线暗下来人经过时它自动亮起如果你躺在床上不想动用手机蓝牙就能直接开灯、关灯还能调亮度。这看起来功能不多但三个控制源放在一起如果逻辑没想清楚灯就会出现“明明手动关了人一过又自己亮了”这种很蠢的表现。我的做法是先定优先级蓝牙手动控制 自动感应控制。也就是说你用手机发了ON或者调了一个亮度这就是手动状态自动感应暂时失效只有当手机发来AUTO指令系统才回到“人感光感”的自动模式。这样设计的好处是逻辑干净不会出现两个控制源打架的情况。如果你反过来把自动感应放最高优先级那手动开关就形同虚设半夜想起来关灯人往那一站传感器又给点亮了体验非常糟糕。自动模式里面的逻辑也很有意思。人体感应和光线感应不是“或”的关系而是“与”的关系用大白话说就是有人 AND 环境暗灯才亮。人走了或者环境变亮了灯就灭。这其实就是智能家居里最常见的“联动场景”理解了这一个状态机以后你去做更复杂的自动化项目思路也一样。1.2 为什么选这些硬件组合主控用的是Arduino UNO这没什么好争议的教程多、库多、坏了便宜新手入门选它最省心。人体感应模块选HC-SR501这是最常见的PIR红外传感器能检测人体发出的红外辐射探测距离大概3到7米躲在房间门口就能触发价格也就几块钱。光线感应这里要特别说一下。市面上有纯光敏电阻也有带比较器直接输出数字信号的模块还有I2C总线的数字光照传感器BH1750。我第一次做的时候直接买了几颗光敏电阻想在面包板上搭分压电路结果因为阻值选得不对读数漂得厉害后来干脆换成了带比较器的光敏电阻模块上面有电位器可以调阈值环境暗了输出高电平亮了输出低电平简单粗暴又稳定。代码里我也同时留了模拟量读取的选项方便你根据手头的硬件灵活切换。蓝牙模块这里推荐HC-05或者HC-06两者都是串口转蓝牙的模块功能上基本一致。HC-05支持主从一体还能做AT指令配置HC-06只能做从机价格稍低。如果你只是用手机连板子HC-06足够如果你以后想拿两块板子互相通信就买HC-05。我手里这块是HC-05所以下面讲解以它为主。灯的输出部分分两种玩法如果你只想控制一盏小LED做实验直接把LED串一个220欧电阻接到PWM引脚就行如果你想让它真正当床头灯或者橱柜灯那就加一个继电器模块控制5V灯带或者12V灯带。想调亮度就得用PWM输出控制MOS管模块继电器只能做开关没办法调光。我这次的代码默认用PWM控制LED灯同时留了继电器模式编译时改一个宏就能切。1.3 这个项目适合谁来玩我自己把这个项目定位成Arduino生涯里的“第一个有实际价值的作品”因为它的知识点覆盖非常典型数字输入PIR输出的是高/低电平、模拟输入光敏电阻的ADC值、串口通信蓝牙模块、PWM输出调光、状态机逻辑模式切换。你说它是教程题它确实能当台灯用你说它是玩具它又比单纯点个LED跑马灯高级不少。适合的人群大概有三类第一类是刚学完基础教程、手里有一堆模块但不知道能组合成什么的人第二类是家里有旧台灯、想动手改造一下的人第三类是准备参加学校创新比赛、需要一个低成本智能家居作品的学生。如果你本身完全没接触过Arduino建议先跑通一个LED闪烁程序再来做这个项目否则一会儿接线一会儿写代码出了问题很容易分不清到底是硬件还是软件的问题。2. 硬件准备与电路连接接线不改三次的秘诀2.1 材料清单器件型号/规格数量备注主控板Arduino UNO R31兼容板也行人体感应模块HC-SR5011背面有两个电位器光线感应模块光敏电阻模块数字量1板载阈值电位器蓝牙模块HC-051需要能进AT模式LED灯5mm任意颜色1串220欧电阻继电器/灯5V继电器模块 5V灯带1套可选控制大功率杜邦线公对公、公对母若干建议备20根面包板400孔或800孔1接线方便HC-SR501的接线是最简单的VCC接5VGND接GNDOUT接Arduino的D2引脚。光敏模块的OUT接D3如果要用模拟量就把AO接A0。蓝牙模块用软串口接在D4和D5上这样做的原因我下面会详细解释。LED正极串电阻接D9负极接地。继电器模块的信号脚接D8VCC和GND接5V和GND注意很多继电器模块是低电平触发信号脚给低电平才吸合。2.2 分模块接线步骤详解第一步先接PIR模块。HC-SR501的输出引脚是数字电平有人活动时输出高电平没人时输出低电平所以直接连D2。接线的时候注意模块的插针顺序不同厂家可能不一样最稳妥的办法是看模块上的丝印标记别只靠记忆。第二步接光敏模块。我这个模块是四针的VCC、GND、DO数字输出、AO模拟输出。DO接D3AO接A0两个我都在代码里做了支持。如果你拿到的是纯光敏电阻需要自己搭一个10K电阻的分压电路光敏电阻一端接5V另一端接A0同时通过10K电阻接地光照越强阻值越小ADC值越大这个反过来也成立。第三步接蓝牙模块这里有一个非常关键的细节蓝牙模块的TXD接Arduino的D4RXD接D5而不是接D0和D1。D0和D1是Arduino的硬串口引脚也是USB下载程序用的通道。如果蓝牙占用D0/D1上传代码的时候信号会打架导致上传失败或者上传成功后蓝牙一直乱发数据。用软串口SoftwareSerial把蓝牙挪到D4和D5程序下载和蓝牙通信互不干扰这个配置哪怕以后做别的蓝牙项目也能直接复用。还有一点HC-05模块的RXD引脚逻辑电平是3.3VArduino的D5输出是5V部分模块直接接没问题但为了长期稳定和防止个别模块烧毁我习惯在RXD和D5之间串一个1K电阻对信号没有实质影响安全性高不少。HC-06也是同样处理。2.3 供电与布线的几个坑先说5V电流问题。Arduino USB供电最大能提供500mA左右PIR模块和光敏模块电流都很小加起来不到20mA蓝牙模块大概30到50mA如果你控制的是单个LED完全够用。但如果继电器吸合、灯带点亮最好不要再从UNO的5V引脚取电而是用外部5V或者12V电源给灯带供电GND和Arduino的GND要共地否则控制信号没有参考地继电器会乱跳。再说继电器模块的触发类型。市面上低电平触发的继电器模块居多买到高电平触发的也有。如果接上之后发现灯常亮、控制无效第一反应就是检查是不是触发逻辑反了。我代码里默认低电平触发用的时候先确认一下模块上的说明或者卖家页面。最后是一个容易被忽略的问题PIR传感器上电之后需要大约30到60秒的稳定时间这段期间输出可能会乱跳不是模块坏了是里面的热释电传感器在自校准。第一次调试时如果发现灯不停闪先别急着改代码等一分钟再看。3. 控制逻辑拆解与完整代码3.1 主循环里的状态机设计整个主程序的核心是一个状态机状态分成“手动模式”和“自动模式”两大类。手动模式下灯的状态完全由蓝牙指令决定你发ON就是255亮度发OFF就是0亮度发了一个数字比如128就把灯调成半亮。自动模式下每次循环都读取PIR状态和环境亮度满足“有人且光线暗”就把灯点亮否则熄灭。这里有一个细节要处理PIR模块本身带一个“延时输出”功能就是检测到人之后输出会保持高电平一段时间时间长短由模块上的电位器调最长的能到几分钟。如果你想做到“人走灯灭”的效果建议把延时电位器往逆时针旋到底让输出延时降到最短然后在代码里每3秒读一次PIR状态这样人离开后最多几秒灯就会灭。如果延时不调短就会出现人已经走了灯还亮一上午的尴尬场面。还有一个状态管理的技巧自动模式下如果PIR输出高电平且光线暗就用一个autoBrightness变量来控制亮度默认是255即全亮。这个变量只在自动模式中使用不会影响你在手动模式下调好的亮度值。切回自动模式的时候不会出现“我手动调了半亮自动模式也永远半亮”的问题。3.2 传感器数据怎么读才够靠谱PIR模块的输出理论上很干净但我还是加了一个3秒的冷却时间也就是两次读取间隔至少3秒。这样做有两个原因一是省掉高频抖动带来的误判二是在调试的时候串口输出不会刷屏方便看日志。PIR触发后状态在冷却时间内是保持不变的整体逻辑更稳。光敏模块如果用的是数字量输出要注意在接线前就把电位器调好环境暗的时候模块上的指示灯亮起环境亮的时候指示灯灭。如果方向反了就旋转电位器直到临界点合适。如果你用模拟量读取方式就需要注意阈值标定问题。不同牌子、不同批次的光敏电阻在同一个环境下的ADC值可能差几百。建议写一段测试程序把analogRead(A0)的值直接打印到串口监视器记录天亮时大概是多少、天黑时大概是多少取中间值作为阈值。比如白天看窗外的值是150晚上拉上窗帘是850那阈值就设在500附近。这个标定过程虽然多花几分钟但能省掉后面无穷无尽的“为什么天黑了灯还不亮”的烦恼。3.3 完整Arduino代码这里给出我实际在用的完整代码直接在Arduino IDE里新建项目粘贴进去就能用。代码默认用数字量光敏模块控制单个LED做PWM调光。如果你要用继电器控制灯带把USE_RELAY改成1接线按D8输出处理如果你手里只有模拟量光敏模块把USE_ANALOG_LIGHT改成1。#include SoftwareSerial.h // 引脚定义 #define PIR_PIN 2 // 人体感应输出接D2 #define LIGHT_DIGITAL_PIN 3 // 光敏模块数字输出接D3 #define LIGHT_ANALOG_PIN A0 // 光敏模块模拟输出接A0 #define LED_PIN 9 // LED控制脚PWM输出 #define RELAY_PIN 8 // 继电器控制脚可选 // 功能开关 #define USE_ANALOG_LIGHT 0 // 光敏传感器用模拟量时改成1 #define USE_RELAY 0 // 用继电器控制灯带时改成1 // 模拟量模式下的黑暗阈值这个值需要根据实际环境标定 #define LIGHT_THRESHOLD 500 // 软串口D4为RXD5为TX对应蓝牙模块的TXD和RXD SoftwareSerial btSerial(4, 5); int autoBrightness 255; // 自动模式下的亮度 int manualBrightness 0; // 手动模式下的亮度 boolean autoMode true; // 当前模式true为自动 boolean pirState false; // PIR当前状态 boolean previousPirState false; unsigned long lastPirCheck 0; const unsigned long pirCooldown 3000; // PIR读取冷却3秒 void setup() { pinMode(PIR_PIN, INPUT); pinMode(LIGHT_DIGITAL_PIN, INPUT); pinMode(LED_PIN, OUTPUT); pinMode(RELAY_PIN, OUTPUT); // 低电平触发继电器的默认状态为断开 digitalWrite(RELAY_PIN, HIGH); Serial.begin(9600); btSerial.begin(9600); Serial.println(Smart Light Ready); } void loop() { handleBluetooth(); handlePIR(); updateLight(); } // 处理蓝牙指令ON、OFF、AUTO、0~255数值 void handleBluetooth() { while (btSerial.available()) { String cmd btSerial.readStringUntil(\n); cmd.trim(); if (cmd.equalsIgnoreCase(ON)) { autoMode false; manualBrightness 255; Serial.println(CMD: ON); } else if (cmd.equalsIgnoreCase(OFF)) { autoMode false; manualBrightness 0; Serial.println(CMD: OFF); } else if (cmd.equalsIgnoreCase(AUTO)) { autoMode true; Serial.println(CMD: AUTO); } else { int value cmd.toInt(); if (value 0 value 255) { autoMode false; manualBrightness value; Serial.print(CMD: Brightness); Serial.println(value); } } } } // 读取PIR状态用冷却时间过滤抖动 void handlePIR() { if (millis() - lastPirCheck pirCooldown) return; lastPirCheck millis(); pirState digitalRead(PIR_PIN); if (pirState ! previousPirState) { previousPirState pirState; Serial.print(PIR: ); Serial.println(pirState ? DETECTED : CLEAR); } } // 判断环境是否暗 boolean isDark() { #if USE_ANALOG_LIGHT int lightVal analogRead(LIGHT_ANALOG_PIN); Serial.print(Light ADC: ); Serial.println(lightVal); return lightVal LIGHT_THRESHOLD; #else return digitalRead(LIGHT_DIGITAL_PIN) HIGH; #endif } // 根据模式更新灯光输出 void updateLight() { int targetBrightness 0; if (autoMode) { if (pirState isDark()) { targetBrightness autoBrightness; } else { targetBrightness 0; } } else { targetBrightness manualBrightness; } #if USE_RELAY // 继电器不能调亮度亮度大于0就吸合 digitalWrite(RELAY_PIN, targetBrightness 0 ? LOW : HIGH); analogWrite(LED_PIN, 0); #else analogWrite(LED_PIN, targetBrightness); #endif }代码流程其实非常简单loop函数里每次都检查蓝牙有没有来指令然后检查PIR状态最后根据模式和传感器数据决定灯的亮度。蓝牙指令的解析用readStringUntil(\n)所以手机App发送的时候一定要带换行符一般蓝牙串口App都有一个“发送新行”的选项务必打开。如果不带换行符代码会一直等指令就无法生效。如果你不想用String也可以改成逐字节缓存的方式比如定义一个char数组收到一个字符存一个遇到\n再统一处理。String在小项目里没问题但如果你的程序很复杂、又需要长时间运行建议还是用字符数组更省内存这个就是后话了。4. 蓝牙模块配置与手机遥控实操4.1 HC-05和HC-06到底有什么不一样很多新手分不清这两个模块其实核心差别就两点HC-05支持主从一体既可以当被手机连接的从机也可以主动去连接别的蓝牙设备HC-06只能当从机被动等别人连它。对咱们这个项目来说手机当主机板子当从机两者都能用。真正的区别在配置方式。HC-06上电就直接是AT指令模式不需要按键直接把RXD接一个USB转TTL模块就能发AT指令改名字、改波特率。HC-05则要先按住模块上的小按键再上电这时候模块才进入AT模式LED大约两秒慢闪一次此时发送AT指令才能生效。如果没按住按键上电AT指令就会被当成普通数据通过蓝牙透传收走不会返回OK这是最常见的配置失败原因。4.2 进入AT模式改名称和波特率AT配置建议用USB转TTL模块VCC接5VHC-05支持5V供电HC-06要接3.3V或者5V建议你查自己模块的资料GND接GND模块RXD接USB转TTL的TX模块TXD接USB转TTL的RX然后打开串口监视器设置正确波特率。HC-05的AT模式波特率默认是38400HC-06默认是9600。串口监视器底部选择“NL和CR”这样发送的指令自动带换行回车。我常用的AT指令就这几个指令作用示例AT测试通信返回OKATNAMExxx修改蓝牙名称ATNAMESmartLightATUART9600,0,0设置波特率为9600修改后要重启生效ATROLE0设置HC-05为从机模式0为从机1为主机ATPSWD1234设置配对密码默认1234一般不用改改波特率这一步要特别小心模块波特率改了之后下次配置或者和Arduino通信的波特率也要跟着改。我的建议是统一改成9600和Arduino代码里btSerial.begin(9600)保持一致。有的朋友用Arduino的软串口发AT指令理论上可以但软串口对时序要求比较敏感容易出错我试过几次都不太稳定。如果你手头没有USB转TTL用Arduino做中转也可以但最好把软串口波特率凑准了而且供电要稳定。总体来说花几块钱买个USB转TTL调试器会让整个配置过程顺畅很多。4.3 手机App连接与指令发送手机端我平时用的是蓝牙串口调试类AppAndroid上搜“蓝牙串口”就有很多iOS因为系统限制这类App一般要收费但功能差不多。打开App扫描设备能找到你刚才在AT指令里设置的名称比如SmartLight配对密码默认1234连接之后就是一个类似串口调试的界面上面有输入框和发送按钮。连接成功之后发送ON灯就全亮发送OFF灯就灭发送AUTO回到自动模式发送一个0到255之间的数字比如128灯就半亮。如果你用继电器模式那么只要发送大于0的数字继电器就吸合发送0就断开亮度指令实际上变成了开关指令这个在程序里已经做了适配。这里有一个小经验有些蓝牙模块连接成功后模块上的LED会从快闪变成慢闪或者常亮但是数据就是发不过去。这时候十有八九是波特率不匹配检查一下模块实际波特率和Arduino软串口begin的是不是同一个值。另外串口监视器和蓝牙不能同时占用硬串口如果代码里调试信息用Serial.print蓝牙用软串口那就没事如果蓝牙接的是D0/D1上传程序的时候或者打开串口监视器的时候数据通道会互相干扰。5. 常见问题与调试实录5.1 PIR传感器乱触发、不触发怎么办PIR乱触发绝大多数是安装位置的问题。暖气管、空调出风口、阳光直射的地方都会让模块误认为有人。解决办法是调整安装位置让传感器不要正对热源或者把灵敏度电位器逆时针调低。HC-SR501背面有两个电位器小的调灵敏度大的调延时用螺丝刀轻轻转每次转一点点不要猛调。调到“人走过去能触发但静止一会儿不会反复触发”的状态最合适。不触发的情况一般有几种供电不足、接线不对、稳定时间不够。我排查过一次最后发现是杜邦线插在面包板上虚接了PIR的OUT引脚偶尔能读到高电平大多数时候是低电平表现出来就是灯偶尔亮偶尔不亮。重新拔插一遍线就好了。还有一个老生常谈的问题人静止站在传感器前面PIR是不会输出的因为热释电传感器只能检测“变化”的红外辐射。如果你希望人坐着不动灯也保持亮着那要靠模块的延时输出来维持这也是为什么延时电位器不要调到最短的原因要看你具体的使用场景。5.2 光线传感器阈值怎么标定自动模式里判断“暗”和“亮”的这一步如果弄错灯就会表现得很诡异。数字量光敏模块的标定非常简单面向实际使用环境用一个遮挡物逐渐挡住模块的光线观察指示灯的临界变化把临界点调到你需要的位置。比如你要让灯在接近傍晚的时候启动就在傍晚时对着模块调电位器直到LED刚好亮起。模拟量光敏模块的标定我前面提到过就是打印ADC值。还有一个技巧不要用眼睛去估直接在串口监视器里观察同时把当前时间记下来。比如下午五点半ADC值是550六点钟变成了700你想要的效果是六点开灯那阈值就设600。不同房间采光差别很大同一个阈值放到另一个房间是不成立的。5.3 蓝牙搜不到、配对不上蓝牙搜索不到先看模块上的LED状态。HC-05没有进入AT模式时上电后LED应该是快速闪烁大约每秒闪几次这个状态才代表模块正常工作、可以搜索到。如果LED不亮查供电如果LED常亮可能是模块进入了连接状态或者AT状态重新上电试一下。搜到设备但配对失败密码默认1234注意大小写和输入法很多App会在密码后面自动加空格我遇到过好几次。还有一个常见问题电脑的蓝牙和手机搜到了同一个模块这个模块已经被其中一个设备连接了另一个设备就搜不到或者连接不上。HC-05作为从机一般支持多设备配对但同一时间只能有一个连接断开旧的再连新的就行。如果实在不行重新上电让模块回到待连接状态。5.4 代码上传失败和灯不亮的排查上传代码失败如果提示的是端口被占用十有八九是Arduino IDE里开着串口监视器关掉再传。如果端口都找不到换一根数据线试试很多USB线只能充电不能传数据。还有一个坑是蓝牙模块占用了D0/D1不过咱们用了软串口之后这个问题基本就绕开了。灯不亮分几种情况D9引脚输出PWM但LED不亮先检查LED正负极接没接反或者电阻是不是太大了导致电流太小灯亮度微弱如果是继电器模式先确认继电器是不是低电平触发如果模块是高电平触发代码里要反着写还有一种情况手动模式下一切正常自动模式下灯没反应那问题基本集中在PIR或者光敏传感器其中一个环节用串口监视器看打印信息一目了然。调试的时候我强烈建议你把代码里的Serial.println留着所有关键状态都有输出。看串口监视器就像给系统做CT哪里不正常一查就知道。6. 扩展玩法与个人体会6.1 从按钮灯到智能家居的第一步做完这个项目你已经有了三个传感器的协调能力接下来可以往两个方向走。一个方向是保留Arduino把蓝牙模块换成ESP32或者ESP8266同样是用软串口或者直接硬件串口但换成WiFi之后就可以接入手机配网、MQTT服务器、智能音箱等实现远程开关和语音控制这也是我听很多玩智能家居的朋友说最常用到的升级路线。另一个方向是加一个OLED显示屏把当前的模式、亮度、传感器状态都显示出来这样调试和日常使用会直观很多。如果你现在没有硬件不想等快递也可以用Wokwi这类在线仿真平台跑代码支持Arduino UNO、PIR、光敏这些常见模块虽然没有真实的蓝牙模块但仿真串口部分能帮你验证逻辑。6.2 我这次做下来的几点感受第一这个项目最大的成本不是钱而是调试时间尤其是PIR上电稳定那段时间容易让新手误以为模块坏了第二所有传感器的阈值和物理安装位置都比代码逻辑更影响最终体验代码再完美装错地方也白搭第三串口日志永远是最可靠的调试手段别怕刷屏不怕不乱就怕没信息。这盏灯我放在家里玄关已经用好几个月了晚上回家开门灯自动亮走过去又自动灭这种“伸手不见五指的时候刚好有一盏灯等你”的感觉确实比手机远程开关还要舒服。如果你也想动手做一个欢迎照着这篇文章一步步来中间遇到任何问题建议先从串口监视器看起大多数问题在那里都会给出答案。