最近把家里那套“开关靠吼、温湿度靠猜”的设备统一换了一遍最后沉淀成一套以ESP32为核心的WiFiBLE双模智能家居方案。ESP32这芯片是真的适合折腾智能家居一颗芯片自带WiFi和BLE既能当终端节点跑传感器和继电器也能当简易网关做蓝牙Mesh中转成本还能压到二十块钱以内。这篇文章就把我这套方案的选型思路、硬件接线、固件开发、常见坑位完整展开一遍适合手里有ESP32开发板、想自己做灯控/温湿度/门磁这类场景的玩家也适合准备入坑智能家居但不想被各家私有协议绑死的朋友。1. 方案设计为什么是ESP32双模组合解决什么问题1.1 先想清楚智能家居的痛点很多新手一开始就会陷入“设备孤岛”的困境。A家的灯、B家的传感器、C家的插座各用各的App各连各的网关彼此之间完全聊不起来。想让门磁联动玄关灯发现两个品牌不互通。这时候自建一套统一平台就非常值。ESP32方案的优势在于协议在你自己手里硬件逻辑透明数据不出屋扩展自由。对比ESP8266WiFi性能可靠但缺BLE配网只能SmartConfig近场调试还得额外连路由。对比树莓派性能强但成本和功耗高适合当集中式网关不适合在每个开关插座里塞一块。ESP32是少数在“终端节点”和“小网关”两个角色之间都能胜任的芯片一颗SoC集成了2.4GHz WiFi和BLE双协议栈GPIO、ADC、DAC、I2C、SPI、UART几乎全有一颗板子就能把信号采集、本地控制、远程通信全部承担起来。1.2 WiFi和BLE各自该干什么很多入门资料喜欢把WiFi和BLE混在一起讲实际项目里必须让它们各司其职否则一发广播、一跑TCP两个协议抢射频设备表现会非常怪。我长期跑下来的一套职责划分是这样通信方式特点在智能家居里的典型任务WiFi覆盖远、吞吐高、能与路由器生态直接打通MQTT上报、OTA升级、远程日志、接入Home AssistantBLE功耗低、广播式发现、无需路由器介入手机近场配网、低功耗传感器回传、断网应急控制所以我的分工原则是能走WiFi的走WiFi因为要和服务器、App、语音音箱通信能走BLE的走BLE尤其电池供电的传感器以及需要“首次配对”的场景。ESP32的BLE还能做GATT Server手机App直接连上来读写特征值这对调试和应急控制非常重要。1.3 为什么不直接用ESPHome/成品网关有人会问不是有ESPHome、Tasmota直接刷固件吗对纯用户来说刷ESPHome确实省事但做“一站式方案”如果连代码逻辑都不掌握后续加传感器、加联动逻辑、解决疑难问题时非常被动。而且ESPHome偏重WiFiBLE能力只暴露了一部分。我的做法是用Arduino框架自己写底层库成熟逻辑全可控。成品网关同理虽然省心但私有协议封闭你永远不知道数据去了哪里。自建方案的另一个好处是后续接入Home Assistant、小爱、天猫精灵都能按标准协议走不会被厂商绑死。在实际项目里我把这套方案定义为“协议自主、数据可控、节点可扩展”的轻量级自建智能家居。2. 硬件准备与电路设计2.1 选型清单与器件说明我选的核心板是ESP32-DevKitC模组是WROOM-32理由很朴素资料多、引脚兼容性强、WiFiBLE同时启用都不掉链子。如果你是新项目可以考虑ESP32-S3或者ESP32-C3S3的GPIO和Flash更大C3主打低成本和低功耗但没有经典蓝牙只有BLE。很多人纠结选哪个我用这个标准做主控设备灯、开关、插座选WROOM-32或S3做电池传感器节点选C3做音频或屏幕应用选S3。外设方面我最终方案用的硬件温湿度SHT30模块I2C接口精度在1%RH左右比DHT22稳价格也就十来块。光照BH1750模块I2C量程到65535 lx适合做自动窗帘和灯光调节。继电器5V低电平触发光耦继电器模块控制灯和插座。人体存在毫米波雷达可选一般项目用PIR也可以。门磁干簧管直接接GPIO输入。电源5V/2A适配器 ME6211或AMS1117降3.3V注意AMS1117压差大。这里面最不建议省钱的其实是电源部分。很多ESP32无故重启、烧录失败、ADC读值飘最后排查下来都是供电不干净。智能家居是7x24小时常电运行的设备比开发板上插USB调试的要求高一个等级电源模块建议一步到位。2.2 GPIO分配表和接线的几个讲究因为我同时要跑WiFi、BLE、I2C、ADC、继电器引脚必须预先规划不然等焊完再改就痛苦了。这里有一份实测稳定的分配表功能引脚说明I2C SDAGPIO21接SHT30/BH1750I2C SCLGPIO22同上门磁/开关输入GPIO4输入带内部上拉继电器1灯GPIO16低电平触发模块继电器2插座GPIO17低电平触发模块ADC电池电压GPIO32属于ADC1避开WiFi干扰UART调试GPIO1/GPIO3TX/RX刷机日志有几个坑必须提醒GPIO34到GPIO39是输入-only只能做输入不能拿来接继电器输出。GPIO12是MTDI引脚部分模组上电时会因为外部干扰导致启动失败尽量不用非要用就得处理上拉时序。ADC2引脚GPIO25-27等在WiFi开启时analogRead基本不能用实测数据乱跳所以电池电压检测尽量放ADC1。继电器模块的IN接ESP32 GPIO时多数组件是低电平触发吸合电流由光耦提供直接用5V供电给模块但GPIO输出3.3V高电平在某些模块上可能无法关闭这时候选低电平触发版本最省心。接线顺序也要说清楚继电器模块VCC接5VGND接ESP32的GNDIN接GPIO16/17负载火线串入继电器COM和NO触点零线直连负载。注意负载功率10A规格的继电器带普通灯具和插座没问题带大功率取暖器要核算一下电流。2.3 供电与低功耗设计WiFi和BLE同时开启时ESP32正常工作电流在80-150mA瞬态能到300mA以上所以别指望靠树莓派那种USB口随便拉电源要留余量。电池供电节点就完全换思路用ESP32-C3配一颗CR2032不现实虽然能跑但WiFi发射太高真实做法是上3.7V锂电池RT9193降压平时deep sleep每30秒唤醒一次唤醒后发完BLE广播马上再睡平均电流能压到200uA级别。deep sleep唤醒条件设计要从“事件触发”和“定时上报”两条同时考虑比如门磁开合用GPIO唤醒温湿度用定时器唤醒。代码里要用esp_sleep_enable_gpio_wakeup和esp_sleep_enable_timer_wakeup。常电节点和电池节点的供电思路完全不同千万不要把所有节点都设计成WiFi常连。全屋十几个节点每个都跑MQTT保活心跳路由器会被烦死你自己看日志也烦。正确分层是常电节点用WiFiMQTT电池节点用BLE广播低功耗最后汇总到网关节点统一上报。2.4 接口扩展以太网与语音模块如果觉得WiFi不稳定或者设备位置在弱电箱里信号天然不好LAN8720模块可以给ESP32补一个有线口。这块我踩过不少坑后面单独说。音频方向ESP32-S3配合ES8311 Codec可以做离线语音交互或者用DY-SV17F这种串口语音模块把“开灯”“关灯”命令直接映射到GPIO实现纯本地语音控制。扩展接口都预留好后面再加模块不用重新布局。核心板引脚有限做项目时先列需求清单再分配引脚比焊完发现少一个I2C口再飞线舒服得多。3. 固件开发实操从零到能用的完整流程3.1 开发环境与烧录准备我用的Arduino IDE 2.x配ESP32离线包。如果网络条件一般手动下载esp32_package_3.11离线压缩包解压后放到本机Arduino15目录的packages/esp32下重启IDE就能识别。离线包好处很明显不用反复拉索引装完以后项目编译稳定很多。装完在开发板管理器确认esp32版本选择开发板为ESP32 Dev Module。这里有个细节Flash大小选8MB还是4MB要看模组Partition Scheme如果做OTA要选Huge APP或自定义分区默认的怕空间不够。烧录方式和顺序很重要先插USB看设备管理器出现的是什么串口芯片。CP2102是比较常见的CH340也很多S3的板子有些是USB CDC直连不用额外驱动。如果烧录报错“Connecting...”按住开发板上的BOOT键点烧录等到显示Connecting时松开。另外USB线材差、供电不稳也会导致烧录失败换根带屏蔽的数据线有时能解决一半问题。要是用FlashDownloadTools全片烧录我一般保持SPI速度40MHz、模式DIO地址按官方分区表偏移写比手动填地址稳妥。3.2 WiFi连接配网是第一个拦路虎ESP32连接WiFi的代码非常简单但真实场景里90%的返工都栽在配网体验上。固定SSID固化的方案只适合自己玩给家里长辈用就麻烦了。所以我做了一套“APBLE双通道配网”第一通道是SoftAP。开机后读NVS里保存的配置如果没有就地启动APSSID叫ESP32-XXXX手机连上去打开浏览器访问192.168.4.1输入WiFi账号密码保存。第二通道是BLE配网。手机用蓝牙助手扫描看到名字为ESP32Config的设备连上去往写特征值写“SSID:密码”格式的数据芯片收到后解析并尝试连接WiFi。配网后要保存在NVS里否则断电重新来。代码我习惯用Preferences库key存ssid、pass、ip等读取时先检查是否存在存在就直接WiFi.begin。这套机制的好处是老年人也能用手机App扫一下输一次密码后面再不用管。实际调试时的一个经验ESP32只支持2.4G频段路由器开了双频合一或5G优先会导致连不上。如果家里5G信号覆盖不了角落板子放在角落就会反复掉线这时候要么改路由器策略要么给ESP32区域单独放一个2.4G AP。信道方面2.4G干扰是常态把路由器信道固定到1、6、11中相对干净的那个能明显改善掉线情况。3.3 BLE GATT服务设计BLE部分我们用ESP32做GATT Server。一个温湿度服务Service、一个设备控制服务每个服务底下分特征Characteristic。温湿度特征做成ReadNotify控制特征做成Write。也就是说手机App可以连接ESP32之后直接读当前室温也可以下发开灯指令不需要经过云服务器。这个设计在WiFi断了的时候尤其珍贵只要手机和板子在一个屋子里BLE距离内就能临时操作不至于回家发现灯打不开。UUID不要满大街都用默认的0xFFE0/0xFFE1建议自定义一个128-bit随机UUID比如服务设为6E400001-B5A3-F393-E0A9-E50E24DCCA9E两个特征分别用6E400002...、6E400003...。自己做应用时便于识别也不容易和手机里预装的蓝牙设备起冲突。BLE广播启动的重点必须等WiFi连接动作结束或超时后再启动否则两个协议同时抢占射频广播包发不出来手机扫不到。我实测要加一个500ms左右的延时再启动广播。另外如果设备名称要显示成中文注意广播字节长度限制最好直接用英文短名。3.4 MQTT接入Home AssistantWiFi连通后ESP32需要和智能家居大脑对话。我直接走MQTTbroker用Mosquitto部署在树莓派上Home Assistant通过MQTT集成自动发现设备。ESP32作为客户端订阅主题home/room1/light/set发布主题home/room1/light/state和传感器主题home/room1/temp、home/room1/humidity。整套通信走局域网不需要暴露到外网隐私和数据可控。MQTT状态上报要注意频率温湿度不要每次都发布我设了60秒上报一次且变化量超过0.3才上报减少无效消息。订阅命令的循环里要处理retained消息否则断电重连后开关状态可能与执行器不一致。我的做法是每次MQTT重连成功后主动发布一次当前状态让Home Assistant侧的状态立即同步。3.5 综合代码骨架工程我不建议把所有逻辑塞进一个文件后面维护会很想死。我通常分成wifi_conf.h、ble_server.h、sensors.h和main.cpp四个模块wifi_conf管配网和连接状态ble_server管GATT服务和回调sensors管I2C数据采集main.cpp只管初始化、MQTT订阅和调度。下面给出整合后的核心流程代码#include WiFi.h #include BLEDevice.h #include BLEServer.h #include BLEUtils.h #include BLE2902.h #include PubSubClient.h #include Preferences.h #include Wire.h #include SHT30.h // 修改为你自己的配置 const char* AP_SSID ESP32-Config; const char* AP_PASS 12345678; Preferences prefs; WiFiClient net; PubSubClient mqtt(net); SHT30 sht30; // 继电器引脚 #define RELAY1 16 #define RELAY2 17 // BLE服务/特征 #define SERVICE_UUID 6E400001-B5A3-F393-E0A9-E50E24DCCA9E #define CHAR_TEMP_UUID 6E400002-B5A3-F393-E0A9-E50E24DCCA9E #define CHAR_CTRL_UUID 6E400003-B5A3-F393-E0A9-E50E24DCCA9E BLECharacteristic* tempChar; BLEServer* bleServer; bool deviceConnected false;完整代码我通常放在Git仓库里这里贴最关键的一部分实现也就是BLE回调接收开灯指令并同步推送MQTTclass CtrlCallback: public BLECharacteristicCallbacks { void onWrite(BLECharacteristic* c) { String cmd c-getValue(); if (cmd on) { digitalWrite(RELAY1, LOW); // 低电平触发 mqtt.publish(home/room1/light/state, on); } else if (cmd off) { digitalWrite(RELAY1, HIGH); mqtt.publish(home/room1/light/state, off); } } };这套代码跑得很稳不过有一件事必须提醒不要让MQTT回调里写太重的逻辑比如在订阅回调里直接读I2C传感器这样会引发阻塞导致WiFi任务饿死表现为设备挂了但LED还在闪。我的习惯是回调里只设置标志位loop里统一执行动作。4. 常见问题与排查技巧实录4.1 烧录失败和“卡烧录”排查顺序遇到烧录连接不上先按这个顺序检查串口驱动装了吗选择端口对不对USB线是不是只能充电的数据线板子的BOOT按键需不需要手动按住如果IDE一直显示Connecting绝大多数情况是线材和驱动的问题。更有意思的是部分ESP32-S3板卡没有自动下载电路必须按住Boot进入下载模式手动复位后再烧录。另外FlashDownloadTools全片擦除时如果地址和波特率选的太激进也会失败我一般选921600波特率、默认SPI配置。4.2 WiFi连不上、掉线、DHCP异常这个坑我在实际项目里遇到最多。首先是频段ESP32只支持2.4GHz路由器开了5G优先或双频合一后设备会被引导到5G频段结果直接掉线。解决方法是把2.4G和5G SSID分开或者关掉双频合一。其次是信道拥挤周围几十个WiFi时2.4G信道像早高峰马路把路由器信道固定到1、6、11中相对干净的那个能明显改善。再说一个特定的场景如果路由器关闭了DHCP你就要手动给ESP32设静态IP很多人只填了IP地址没填网关和DNS结果设备显示连上了却ping不通路由器。我遇到过最典型的是IP网段填错路由器在192.168.50.x设备填了192.168.1.x当然上不了网。用WiFi.config()时要把local_ip、gateway、subnet、dns1四个参数都补全了才稳定。还有一种情况WiFi连接成功但MQTT连不上。先排查broker地址是IP还是域名域名解析失败的可以先硬编码IP确保网络通路后再处理DNS。实测下来ESP32的解析器对某些路由器下发的IPv6 DNS会卡住建议在初始化时强制只用IPv4。4.3 BLE搜不到、连不上、收不到数据BLE这个方向新手容易在三个地方卡住。第一广播没启动起来。很多人只创建了服务和特征忘了启动广播手机当然扫不到。代码里调用BLEAdvertising ad bleServer-getAdvertising(); ad.start();。第二UUID对不上。手机端App和固件端必须使用同一组UUID很多成品GATT调试工具默认显示的是通用服务要展开后看具体服务或者直接复制自定义UUID搜索。第三收不到Notify数据。如果特征定义时没有增加BLE2902描述符客户端没办法订阅通知你哪怕调notify也发不出来。这里贴一个典型tempChar-addDescriptor(new BLE2902()); // 开启通知 tempChar-setValue(...); tempChar-notify();还有连接参数问题某些手机会自动用低功耗参数导致2秒才唤醒一次延迟明显。可以在BLEAdvertising里设置连接间隔但一般场景不用太纠结除非做控制的响应。BLE配网时MTU默认只有23字节一次写不了太长数据配网信息要拆分或者控制长度。4.4 LAN8720以太网扩展的3个高频问题如果要把ESP32做成有线网关我试过LAN8720模块踩过坑可以总结成三个高频问题。第一是PHY通信不通最常见是TXD/RXD接线接反LAN8720的TXD1接ESP32的RXD0不要按字面意思理解。第二是RMII时钟PHY需要一个50MHz时钟要么直接用ESP32内部APLL时钟输出把ETH_RMII_CLK设成输出很多代码模板里没处理对要么外接有源晶振否则ping不通。第三是PHY地址LAN8720默认PHY地址是0但复位时序不对会导致其PHY ADDR读到1要仔细看模块上的电平配置或者加一个复位GPIO延时。这块细节多如果不是特别需要有线我建议先用WiFi真要稳定有线时再单独折腾。4.5 ADC读数漂移和电源噪声智能家居里经常要测电池电压或光照模拟量ADC这块最大的教训就是ADC2不能用。很多入门例程用GPIO34读光敏电阻那是ADC1没问题一旦用到GPIO25-27这类ADC2引脚WiFi开启时数据直接混乱。因为ADC2被WiFi驱动占用了两个外设抢资源analogRead返回的全是垃圾值。调整到ADC1之后读数稳定很多。另外ADC输入阻抗很高你并联一个0.1uF电容到地可以滤掉一些毛刺但要注意这会降低响应速度对光敏这类慢变量完全没关系。4.6 传感器读数异常DHT22/SHT30数据错乱传感器不读数是排查重点。DHT22是单总线接线时如果没有4.7k-10k上拉时序就会错乱读出来永远是0或NAN。SHT30走I2C最常见问题是地址不对默认地址0x44但有些模块是0x45。用I2C扫描程序先扫一遍确认地址再决定代码。另外I2C线上要上拉电阻很多模块板上自带但如果你自己飞线接传感器不上拉的话SDA和SCL会拉低。电源也不要直接从GPIO借接3.3V供电比较稳。我在做多传感器并联时还遇到过地址冲突同一台总线上两个相同模块必须把其中一个SDO引脚拉高改成0x45。4.7 电源引起设备重启继电器一吸合、LED一全开ESP32就重启这是智能家居项目里最经典的“灵异事件”。原因基本是电源压降或地线回流。继电器线圈瞬间电流大和ESP32共用一条5V线时压降导致ESP32掉电复位。解决办法是继电器模块单独供电或者至少用粗一点的线地线共地加一个大电容缓冲。我实测在5V电源输出端并联470uF电容问题基本消失。另一个可能原因是GPIO引脚控制继电器时继电器模块的IN引脚电流过大直接拉垮了GPIO有条件就用三极管或光耦隔离。5. 这套方案的扩展方向5.1 离线语音控制智能家居如果只靠手机App家人用起来还是觉得不顺手所以我又加了离线语音。DY-SV17F语音模块通过串口和ESP32通信说“开灯”模块解析后发一个0x11命令ESP32收到就翻转继电器。这样整套系统的核心逻辑仍然在ESP32上语音只是输入通道不依赖云服务断网也能用。如果对音质要求更高可以上ES8311 I2S音频方案。5.2 BLE Mesh和传感器网络传感器节点如果都用WiFi功耗和网络压力都大。理想分层是电池传感器用BLE广播低功耗上报ESP32主控节点作为BLE Mesh设备或扫描器收集后统一走WiFi/MQTT上传。ESP32-C3做这类叶子节点很合适三五年换一次电池都能实现。我目前用ESP32 BLE Mesh网关把几个开关传感器串起来收到蓝牙广播后会直接转成MQTT消息整个架构从“一主一从”升级成了“一个网关N个低功耗节点”。5.3 多房间扩展与命名规范一个房间的ESP32跑通后其他房间复制方案非常快。统一的MQTT Topic结构最简单比如home/room2/temphome/room2/light/set。Home Assistant里用模板或者自动发现就能把所有房间都纳进来。如果要做跨房间联动例如主卧门磁触发时打开走廊灯核心仍然是统一走MQTT事件而不需要让两个ESP32直接互通。我在实际折腾过程中的一个体会是别一上来就想做一个全屋超级系统先把单房间的单点控制跑通再扩展出去。整套方案的核心竞争力不在某个单独功能多炫而在于“每个节点都自主可控、通信链路透明、出问题能一条条排查”这个基础的可靠性。只要把WiFi和BLE各自的任务分配清楚后续加设备基本就是复制粘贴再调参数的事。这篇文章写到这儿最后分享一个小技巧在给每个ESP32命名时用room-device-role的格式比如room1-light、room2-temp同时在BLE广播名和MQTT Topic里保持一致后面排查问题、看日志时会无比省心。我自己因为前期命名随意曾经在一堆debug log里找设备找了一个小时。
