做这个项目之前,我一直觉得“智能灯”至少得是几百块的成品才能叫智能,直到自己用一块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 R3 | 1 | 兼容板也行 |
| 人体感应模块 | HC-SR501 | 1 | 背面有两个电位器 |
| 光线感应模块 | 光敏电阻模块(数字量) | 1 | 板载阈值电位器 |
| 蓝牙模块 | HC-05 | 1 | 需要能进AT模式 |
| LED灯 | 5mm任意颜色 | 1 | 串220欧电阻 |
| 继电器/灯 | 5V继电器模块 + 5V灯带 | 1套 | 可选,控制大功率 |
| 杜邦线 | 公对公、公对母 | 若干 | 建议备20根 |
| 面包板 | 400孔或800孔 | 1 | 接线方便 |
HC-SR501的接线是最简单的:VCC接5V,GND接GND,OUT接Arduino的D2引脚。光敏模块的OUT接D3,如果要用模拟量,就把AO接A0。蓝牙模块用软串口接在D4和D5上,这样做的原因我下面会详细解释。LED正极串电阻接D9,负极接地。继电器模块的信号脚接D8,VCC和GND接5V和GND,注意很多继电器模块是低电平触发,信号脚给低电平才吸合。
2.2 分模块接线步骤详解
第一步先接PIR模块。HC-SR501的输出引脚是数字电平,有人活动时输出高电平,没人时输出低电平,所以直接连D2。接线的时候注意模块的插针顺序,不同厂家可能不一样,最稳妥的办法是看模块上的丝印标记,别只靠记忆。
第二步接光敏模块。我这个模块是四针的:VCC、GND、DO(数字输出)、AO(模拟输出)。DO接D3,AO接A0,两个我都在代码里做了支持。如果你拿到的是纯光敏电阻,需要自己搭一个10K电阻的分压电路,光敏电阻一端接5V,另一端接A0同时通过10K电阻接地,光照越强阻值越小,ADC值越大,这个反过来也成立。
第三步接蓝牙模块,这里有一个非常关键的细节:蓝牙模块的TXD接Arduino的D4,RXD接D5,而不是接D0和D1。D0和D1是Arduino的硬串口引脚,也是USB下载程序用的通道。如果蓝牙占用D0/D1,上传代码的时候信号会打架,导致上传失败或者上传成功后蓝牙一直乱发数据。用软串口(SoftwareSerial)把蓝牙挪到D4和D5,程序下载和蓝牙通信互不干扰,这个配置哪怕以后做别的蓝牙项目也能直接复用。
还有一点,HC-05模块的RXD引脚逻辑电平是3.3V,Arduino的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为RX,D5为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接5V(HC-05支持5V供电,HC-06要接3.3V或者5V建议你查自己模块的资料),GND接GND,模块RXD接USB转TTL的TX,模块TXD接USB转TTL的RX,然后打开串口监视器,设置正确波特率。HC-05的AT模式波特率默认是38400,HC-06默认是9600。串口监视器底部选择“NL和CR”,这样发送的指令自动带换行回车。
我常用的AT指令就这几个:
| 指令 | 作用 | 示例 |
|---|---|---|
| AT | 测试通信 | 返回OK |
| AT+NAME=xxx | 修改蓝牙名称 | AT+NAME=SmartLight |
| AT+UART=9600,0,0 | 设置波特率为9600 | 修改后要重启生效 |
| AT+ROLE=0 | 设置HC-05为从机模式 | 0为从机,1为主机 |
| AT+PSWD=1234 | 设置配对密码 | 默认1234一般不用改 |
改波特率这一步要特别小心,模块波特率改了之后,下次配置或者和Arduino通信的波特率也要跟着改。我的建议是统一改成9600,和Arduino代码里btSerial.begin(9600)保持一致。
有的朋友用Arduino的软串口发AT指令,理论上可以,但软串口对时序要求比较敏感,容易出错,我试过几次都不太稳定。如果你手头没有USB转TTL,用Arduino做中转也可以,但最好把软串口波特率凑准了,而且供电要稳定。总体来说,花几块钱买个USB转TTL调试器,会让整个配置过程顺畅很多。
4.3 手机App连接与指令发送
手机端我平时用的是蓝牙串口调试类App,Android上搜“蓝牙串口”就有很多,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上电稳定那段时间,容易让新手误以为模块坏了;第二,所有传感器的阈值和物理安装位置,都比代码逻辑更影响最终体验,代码再完美,装错地方也白搭;第三,串口日志永远是最可靠的调试手段,别怕刷屏,不怕不乱,就怕没信息。
这盏灯我放在家里玄关已经用好几个月了,晚上回家开门,灯自动亮,走过去又自动灭,这种“伸手不见五指的时候刚好有一盏灯等你”的感觉,确实比手机远程开关还要舒服。如果你也想动手做一个,欢迎照着这篇文章一步步来,中间遇到任何问题,建议先从串口监视器看起,大多数问题在那里都会给出答案。