简介:这份PDF面向电子、物联网与自动化方向的在校学生及DIY爱好者,围绕Arduino UNO核心控制板,完整给出了一套智能家居控制系统的设计思路与实现方案,可用于课程设计、毕业设计选题参考或自学开源硬件入门。资源包共1个PDF文件,约1.9MB,正文以中文撰写并附英文摘要与关键词,含目录、系统总体设计、模块化硬件方案、EDR蓝牙通信及易安卓客户端开发等章节,结构清晰便于按需查阅。内容涵盖智能开关控制、LED灯光状态提示、离家与回家等场景模式、温湿度等设备状态监控,以及烟雾和门窗传感器安全报警功能,并分析了高效传输、操作简便、可靠性强、易于扩展等优势。已有1108人学习下载,适合希望理解蓝牙无线控制链路与模块化设计方法的读者对照参考,快速建立从核心板到手机客户端的整体认知。
1. 从一盏灯说起:Arduino 智能家居控制设计到底在控制什么
很多人对 Arduino 智能家居控制设计的第一印象是"用手机点一下,灯就亮了",但真正动过手的人会发现,卡住项目进度的往往不是代码,而是继电器一上电就吸合、DHT22 第一次读出来是 NaN、断网之后设备状态一片空白这类工程问题。这套设计的本质,是把温湿度、光照、人体感应这些物理量采上来,经过一层确定的判断逻辑,再去驱动继电器、舵机、风扇这类执行器,最后给用户留一个能看、能改的入口。
它的适用人群比想象中宽:电子类课程设计需要一份能跑起来的完整方案,创客想给出租屋加个自动通风,嵌入式初学者想找一个软硬件都覆盖的练手项目。控制规模和成本完全可控,一块 Arduino UNO 加几个模块就能搭出原型,换成 ESP32 之后还能顺手把局域网控制页面做出来。
需要提前说清边界:Arduino 方案适合几十毫秒级响应的家居场景,不适合直接驱动 220V 大功率负载的裸接,也不适合当作安防主控。把继电器、隔离、供电这三件事做对,后面所有代码才有意义。
2. 主控与传感器选型:Arduino UNO 和 ESP32 在家居节点里怎么分工
2.1 按"要不要联网"决定主控,而不是按性能
选型的第一个岔路口是联网。Arduino UNO 没有内置无线能力,想联网得外挂模块,代码量和不确定性都会增加;ESP32 自带 Wi-Fi,跑 Arduino 框架下同一套语法,直接就能起一个 HTTP 服务。常见做法是:纯本地联动、只做采集和显示的节点用 UNO,需要远程查看或远程改参数的节点用 ESP32。
这不是性能问题。UNO 是 8 位 AVR,主频 16MHz,2KB SRAM;ESP32 是双核 240MHz,几百 KB 可用内存,还有硬件 PWM 和 ADC。对温湿度采样、继电器控制这种任务,UNO 绰绰有余。真正的差别在于:ESP32 的 ADC 线性度一般,测模拟量要校准;UNO 的 3.3V 输出能力弱,某些模块需要电平转换。搞清楚这两点,选型就不会反复。
如果项目里既有本地面板又有联网需求,比较稳的拆法是 ESP32 当主控,负责联网和规则判断;UNO 只做一块"哑终端",负责按键和数码管显示,两者用串口协议通信。这种分工在需要驱数码管或控制舵机的场景里很常见,因为 UNO 的时序控制写起来更直观。
2.2 传感器、执行器与引脚分配对照表
引脚分配是设计阶段最该先落纸的部分。UNO 上 D0/D1 被串口占用,D2/D3 支持外部中断,D9/D10 支持 PWM,模拟口 A4/A5 是 I2C。ESP32 的 GPIO34 到 GPIO39 只能输入不能输出,GPIO6 到 GPIO11 接了内部 Flash 不能用。这些限制直接决定模块接哪里。
| 模块 | 典型型号 | 接口类型 | UNO 建议引脚 | ESP32 建议引脚 | 关键备注 |
|---|---|---|---|---|---|
| 温湿度 | DHT22 | 单总线 | D2 | GPIO4 | 需 4.7k 到 10k 上拉 |
| 温湿度 | SHT30 | I2C | A4/A5 | GPIO21/22 | 精度高,走 I2C 不易受干扰 |
| 显示屏 | SSD1306 0.96 寸 | I2C | A4/A5 | GPIO21/22 | 默认地址 0x3C,少数为 0x3D |
| 继电器 | 单路 5V 光耦模块 | 数字输出 | D8 | GPIO25 | 低电平触发居多,注意默认态 |
| 舵机 | SG90 | PWM | D9 | GPIO13 | 单独供电,勿从板子 5V 取大电流 |
| 风扇驱动 | MOS 管模块 | PWM | D10 | GPIO26 | 感性负载必须加续流二极管 |
| 人体感应 | HC-SR501 | 数字输入 | D3 | GPIO27 | 输出 3.3V,UNO 可直读 |
| 光照 | BH1750 | I2C | A4/A5 | GPIO21/22 | 与 OLED 共总线需换地址无冲突 |
表格里最容易踩的是继电器默认态。市面上多数光耦继电器是低电平触发,也就是说上电瞬间如果引脚悬空,模块可能误吸合。解决办法有两个:把控制脚在setup()里第一时间设成输出并写高电平,或者在有条件时用高电平触发的模块。
2.3 供电与继电器隔离:烧板子的高频原因
继电器、舵机、风扇都不要从开发板的 5V 引脚取电。SG90 堵转电流能到 700mA 以上,继电器线圈吸合瞬间也有几十毫安的冲击,这些电流会把板载稳压芯片拉到欠压,表现就是开发板反复重启、串口打印乱码,严重的直接烧稳压。
正确接法是外部 5V 电源给执行器供电,开发板的地和外部电源的地连在一起,信号线只走控制信号。如果是控制 220V 负载,继电器模块本身的光耦隔离只在信号端有效,强电侧的走线间距、绝缘、外壳必须按安全规范处理,别把强电和弱电捆在一根扎带里。
注意:舵机和继电器共用一个 5V 电源时,舵机启动的电压跌落会让继电器抖动,表现为"灯自己闪"。分开供电或者加一颗 1000uF 电解电容就近滤波,都能缓解。
3. Arduino IDE 与库配置:把 DHT、SSD1306 和继电器跑通的最小工程
3.1 开发板管理器地址与 ESP32 支持包安装
Arduino IDE 装好后默认只有 AVR 系列。要用 ESP32,先打开"文件 - 首选项",在"附加开发板管理器网址"里填入开发板索引地址,然后进"工具 - 开发板 - 开发板管理器"搜索 esp32 并安装。安装包体积不小,国内网络环境下载慢是常态,可以先把开发板管理器网址换成一个访问更快的镜像再试。
装完之后在开发板列表里能看到 ESP32 Dev Module、ESP32S3 Dev Module 等条目。选型要和实物一致,ESP32S3 的引脚映射和普通 ESP32 不一样,选错会出现能编译、能上传、但引脚行为完全不对的情况。串口驱动也别漏,CH340 和 CP2102 是两种常见芯片,需要对应的驱动。
如果同时用 VS Code 加 Arduino 扩展,开发板包会在两个 IDE 之间共享,装一次即可。但要注意扩展的配置文件路径可能和 Arduino IDE 不同,出现"能编译不能上传"时先看串口是否被另一个 IDE 的串口监视器占用。
3.2 装库时的版本坑:DHT 与 Adafruit 统一风格
库管理器里搜 DHT 会出现多个同名来源。Adafruit 的 DHT sensor library 配套 Adafruit Unified Sensor,装一个会自动提示装另一个。另一套是 DHTesp,接口更简单但精度处理口径不同。混用会出现编译报错找不到Adafruit_Sensor.h,或者读出来的湿度和另一份代码对不上。
SSD1306 也是同样的情况。Adafruit_SSD1306 需要配合 Adafruit_GFX,U8g2 则是另一套体系,内存占用更小但 API 完全不同。课程设计里最常见的是 Adafruit 组合,因为示例多。选定一套之后全工程统一,别在同一个项目里既用 U8g2 又用 Adafruit_GFX。
提示:库装完编译报错,先看"文件 - 示例"里对应库的官方示例能不能编译过。示例都过不了,问题在库或开发板包,不在自己的代码。
3.3 温湿度采集 + OLED 显示 + 继电器联动的最小可运行代码
下面这段代码把 DHT22 采集、SSD1306 显示、继电器按温度联动串起来,是可以直接烧进 UNO 或 ESP32 的最小骨架。
#include <Wire.h> #include <Adafruit_GFX.h> #include <Adafruit_SSD1306.h> #include <DHT.h> #define DHTPIN 2 #define DHTTYPE DHT22 #define RELAY_PIN 8 #define OLED_ADDR 0x3C DHT dht(DHTPIN, DHTTYPE); Adafruit_SSD1306 oled(128, 64, &Wire, -1); void setup() { Serial.begin(115200); pinMode(RELAY_PIN, OUTPUT); digitalWrite(RELAY_PIN, HIGH); // 低电平触发继电器:先断开,避免上电误吸合 dht.begin(); if (!oled.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR)) { Serial.println(F("SSD1306 init failed")); // 地址写错或 SDA/SCL 接反 } oled.clearDisplay(); oled.setTextColor(SSD1306_WHITE); } void loop() { float t = dht.readTemperature(); float h = dht.readHumidity(); if (isnan(t) || isnan(h)) { // 首帧或总线异常会返回 NaN Serial.println(F("DHT read failed")); delay(2000); return; } digitalWrite(RELAY_PIN, t > 30.0 ? LOW : HIGH); // 超过 30 度吸合风扇 oled.clearDisplay(); oled.setCursor(0, 0); oled.print(F("Temp: ")); oled.print(t, 1); oled.println(F(" C")); oled.print(F("Humi: ")); oled.print(h, 1); oled.println(F(" %")); oled.display(); Serial.print(t, 1); Serial.print(','); Serial.println(h, 1); delay(2000); // 最小示例写法,下一章会换成非阻塞结构 }代码的逻辑说明:setup()里先把继电器引脚写成高电平,是为了在 DHT 和屏幕初始化之前就锁死执行器状态,这一步顺序不能调。oled.begin()的返回值一定要判,返回 false 基本就是 I2C 地址或接线问题,不判就会得到一个永远黑屏、但串口一切正常的假象。loop()里用isnan过滤无效读数,是因为 DHT22 上电后第一次读取经常失败,直接参与判断会导致继电器乱动。
参数说明:DHTTYPE改成DHT11也能跑,但 DHT11 湿度分辨率只有 1%,温度精度 ±2℃,联动阈值别设太细。OLED_ADDR如果接的是 0x3D 型号,扫描不到就换这个值。阈值 30.0 是硬编码的,实际项目里应该提成变量,方便后面接网页配置。
3.4 Arduino 上传项目出错:按这四步定位
上传失败是最高频的报错,按顺序排查比反复重插线快得多。
第一步看端口。开发板管理器装好了、线也插了,但工具菜单里没有端口,说明是驱动或线材问题。很多充电线只有电源线没有数据线,换一根再试。
第二步看端口占用。串口监视器开着、或者 VS Code 的串口终端连着,上传时会报"端口被占用"。关掉所有打开的串口工具。
第三步看开发板型号。型号选错时上传日志里会出现avrdude: stk500_recv(): programmer is not responding之类的内容。UNO 要选 Arduino Uno,Nano 老版本要选处理器为 ATmega328P(Old Bootloader)。
第四步看接线。RX/TX 上挂了外设会干扰上传,拔掉 D0/D1 上的线再传一次。ESP32 卡在Connecting........时,按住 BOOT 键再点上传,看到Writing at再松手。
4. 控制逻辑设计:从延时轮询走向状态机与局域网控制页面
4.1 为什么 delay 写法会在智能家居里翻车
上一章的代码里有delay(2000)。单看温湿度采样没问题,但一旦加上按键、网页请求、舵机动作,问题就出来了:delay 期间处理器什么都不做,按键按下去没反应,网页请求超时,舵机动作被推迟。家居场景里用户对"点了没反应"的容忍度极低,两秒的卡顿就会让人认为设备坏了。
另一个隐患是阈值抖动。温度在 30.0 度附近来回跳,继电器就会反复吸合断开,一天下来触点寿命掉得很快。这不是硬件问题,是判断逻辑缺少回差。
常见做法是两条线并行改:把时间相关的 delay 全部换成基于millis()的周期判断,把阈值判断换成带滞回的区间判断。代码结构从"顺序执行"变成"每轮都快速扫一遍,到点才做事"。
4.2 用 millis 搭建非阻塞采样与控制骨架
const unsigned long SAMPLE_MS = 2000; // 采样周期 const unsigned long CONTROL_MS = 500; // 控制判定周期 const float TEMP_ON = 30.0; // 高于此值吸合 const float TEMP_OFF = 28.0; // 低于此值断开,形成 2 度回差 unsigned long lastSample = 0; unsigned long lastControl = 0; float lastTemp = NAN, lastHumi = NAN; bool fanOn = false; void loop() { unsigned long now = millis(); if (now - lastSample >= SAMPLE_MS) { // 采样任务 lastSample = now; float t = dht.readTemperature(); float h = dht.readHumidity(); if (!isnan(t)) lastTemp = t; // 读失败时保留上次有效值 if (!isnan(h)) lastHumi = h; } if (now - lastControl >= CONTROL_MS) { // 控制任务,独立周期 lastControl = now; if (!fanOn && lastTemp > TEMP_ON) { fanOn = true; digitalWrite(RELAY_PIN, LOW); } if ( fanOn && lastTemp < TEMP_OFF) { fanOn = false; digitalWrite(RELAY_PIN, HIGH); } } handleWebClient(); // 每轮都跑,内部不能有 delay }逻辑说明:millis()返回的是开机以来的毫秒数,无符号长整型溢出后回绕依然能靠减法正确计算间隔,所以写法上一定是now - last >= 周期,不要写成now >= last + 周期。采样和控制拆成两个周期,是因为传感器读取慢、控制判定要快,绑在一起会让判定被采样拖着走。
参数说明:SAMPLE_MS设 2000 是因为 DHT22 本身采样率上限约 0.5Hz,设更小只会读到重复值。TEMP_ON和TEMP_OFF之间的 2 度就是回差,实际调试时按传感器精度取,DHT11 建议放到 3 度以上。
4.3 ESP32 作为网络服务器的控制接口设计
把主控换成 ESP32 之后,加一层 HTTP 接口就能用手机浏览器控制。
#include <WiFi.h> #include <WebServer.h> WebServer server(80); float setTemp = 30.0; void handleSet() { // /set?t=27.5 if (server.hasArg("t")) setTemp = server.arg("t").toFloat(); server.send(200, "application/json", "{\"ok\":true}"); } void handleState() { // /state 返回当前状态快照 String json = "{\"temp\":" + String(lastTemp, 1) + ",\"humi\":" + String(lastHumi, 1) + ",\"set\":" + String(setTemp, 1) + "}"; server.send(200, "application/json", json); } void setupNetwork() { WiFi.mode(WIFI_STA); WiFi.begin("your-ssid", "your-pass"); server.on("/set", handleSet); server.on("/state", handleState); server.begin(); } void handleWebClient() { server.handleClient(); // 必须每轮调用,否则请求得不到响应 }逻辑说明:WebServer是 ESP32 Arduino 核心自带的同步服务器,用起来简单,但它依赖handleClient()被频繁调用。如果把handleClient()放在有 delay 的循环里,网页就会转圈。handleSet只更新变量,不直接操作继电器,实际动作留给 4.2 的控制任务统一判定,这样网页请求和本地逻辑不会打架。
参数说明:server.arg("t")拿到的是字符串,toFloat()在解析失败时返回 0,所以生产代码里要加范围校验,把非法值挡在 15 到 35 之外。返回 JSON 而不是纯文本,是为了后面接手机页面或第三方平台时不用改协议。
4.4 断网与掉电后的状态恢复
联网设备一旦依赖网页下发配置,断网就成了必须处理的状态。常见做法是把最近一次有效的设定值写进 EEPROM 或 Preferences,开机时先读出来当作默认值,网络恢复后再以远端为准。ESP32 用Preferences库比裸写 EEPROM 省事,它会自动处理磨损均衡。
掉电恢复的关键是执行器的安全默认态。风扇、加热这类负载,上电必须默认断开,等采样和控制判定跑完一轮再决定是否吸合。代码里的顺序就是:引脚初始化写默认断开,读存储配置,启动网络,进入主循环。任何一步失败都不能让继电器变成吸合状态。
如果是纯 UNO 节点没有存储需求,掉电恢复就退化成"上电重新判定",只要保证setup()里继电器先行置位就够了。
5. Wokwi 仿真与实机排错:智能家居控制设计的验证技巧
5.1 先在 Wokwi 里验证逻辑,再碰烙铁
Wokwi 是一个在线 Arduino 仿真平台,支持 UNO、ESP32 以及 DHT22、SSD1306、舵机、超声波这些常用器件。它的价值不在替代实机,而在于把"逻辑错误"和"接线错误"分开。实机上遇到灯不亮,你分不清是代码判断错了还是线接错了;在仿真里接线是确定的,跑不通就一定是代码问题。
具体做法:把工程里的引脚定义原样搬进仿真,DHT22 的温湿度可以用滑动条实时拖动。拖动温度穿过 30 度,看继电器状态和 OLED 显示是否同步变化;再拖回 29 度,确认继电器没有立刻断开,验证回差逻辑生效。这一步能在几分钟内把阈值抖动、状态锁死这类问题筛出来。
仿真跑通之后再上实机,排查范围就只剩供电、接线和驱动。ESP32 的 Wi-Fi 部分在仿真里支持有限,网络相关的逻辑建议直接上实机验证,但把 SSID 和密码提成宏定义,改起来会快很多。
5.2 常见现象与排查对照表
| 现象 | 优先怀疑 | 具体动作 |
|---|---|---|
| 上电继电器就吸合 | 触发极性判断反了 | 把digitalWrite的 HIGH/LOW 对调验证 |
| OLED 全黑但串口正常 | I2C 地址或接线 | 换 0x3D,检查 SDA/SCL 是否接反 |
| DHT 一直返回 NaN | 上拉电阻缺失 | D2 与 5V 之间加 10k 电阻 |
| 屏幕偶尔花屏 | 供电跌落 | 靠近模块加 100uF 电容,执行器独立供电 |
| 网页请求超时 | handleClient()被阻塞 | 检查循环里是否还有 delay |
| 温度贴阈值时继电器抖动 | 缺少回差 | 设置 ON/OFF 两个不同阈值 |
| 长时间运行后死机 | 内存碎片或看门狗 | 减少 String 拼接,改用字符数组 |
这张表里最值得留意的是最后一行。ESP32 上有看门狗,循环里长时间阻塞会被复位;而 Arduino 上频繁用String拼接 JSON 会慢慢耗尽内存,跑几个小时之后表现就是莫名其妙重启。处理办法是把状态信息拆成固定长度的字符数组,或者直接分段发送,避免每次请求都生成新字符串。
5.3 让节点长期运行更稳的三个写法
第一个是给网络请求加超时。ESP32 的server.handleClient()在客户端半开连接时会占用较长时间,可以在配置里限制连接数和超时时间,避免一个卡住的手机把整个节点拖死。同时在主循环里加一个"看门狗喂狗"的动作,或者干脆用任务看门狗并确保每轮循环都足够短。
第二个是把执行器动作做幂等。判断逻辑里如果每次都调用digitalWrite,虽然电平不变,但继电器模块的输入端仍可能产生微小抖动。改成只在状态发生变化时才写引脚,也就是上面代码里的fanOn布尔量,能显著减少触点动作次数。
第三个是给采样加异常连续计数。传感器偶发读失败保留上次值是合理的,但如果连续失败超过 20 次,就应该把状态标记为不可信,让执行器回到安全默认态,并在 OLED 上显示提示。这一条在长期无人值守的场景里比任何滤波算法都管用。
本文还有配套的精品资源,点击获取