
1. 项目缘起为什么我非要做一套“断网可用”的开关1.1 智能开关离了云就“变砖”这个痛点你得承认先说说我做这个项目的起因。家里陆陆续续装过几个所谓“智能开关”宣传页面都说得好好的手机App控制、语音控制、定时开关样样齐全。但实际住进去之后我发现了一个特别尴尬的现状这些设备对公网的依赖程度高得离谱。路由器一断网App里面那个开关的图标就变成灰色点了半天没反应。更搞笑的是语音助手音箱本身是在线的但音箱和开关之间的联动走的是云端的设备通信云服务一抖动整个链路就瘫掉了。网线一拔几乎所有“智能”功能全部失效剩下的只有物理机械按键还能用——那这和普通开关有什么区别踩过几次坑之后我下定决心自己搞一套完全不依赖公网、不依赖任何云平台的智能开关系统。核心诉求就两条第一语音控制必须在本地完成识别不能靠云端第二开关之间的通信要是点对点的无线链路路由器挂了、网断了这套系统照样能工作。于是就有了这个项目离线语音 LoRa 的智能通断器系统。顺便提一嘴很多人现在搜“LoRa”这个词搜出来的大部分是AI大模型微调里的那个“LoRA”全称叫 Low-Rank Adaptation是训练深度学习模型用的。我这次用的 LoRa 是无线通信领域的长距离低功耗调制技术全称 Long Range这是两个完全不同的东西别搞混了。1.2 为什么是“离线语音 LoRa”而不是 WiFi/蓝牙/Zigbee确定要自研之后第一件事是选通信方案。市面上常见的智能家居无线技术就那么几种我一个一个盘了一遍。先看 WiFi。家里路由器带宽大、覆盖也还行但WiFi模块功耗高而且协议栈复杂设备要联网就必须走 TCP/IP 那套要分配IP、要配网、要处理路由器重启后的重连。最要命的是WiFi天然依赖路由器路由器挂了所有设备直接失联。这和我的“离线可用”目标背道而驰。再看蓝牙 Mesh。蓝牙穿墙能力弱得感人一个房间放个节点隔两堵墙信号基本就是残废。组网协议自己也复杂还要搞转发、搞广播风暴控制对单片机来说负担不轻。Zigbee 倒是不错低功耗、组网能力强但问题是它需要一个网关。市面上的 Zigbee 网关大多要连接外网才能和手机App协同工作我费了半天劲网关还是云端的“一座桥”断网之后本地控制就变玄学了。最后再看 LoRa。这玩意儿是Semtech公司搞出来的Chirp扩频调制技术工作在亚GHz频段常见的是433MHz和868/915MHz国内DIY市场最常见的模块是433MHz频段的SX1278芯片。它有几个特别契合智能开关场景的优势点对点通信距离远空旷环境几百米到一两公里很轻松穿墙能力比2.4GHz那些方案强不少功耗低电池供电也能撑很久协议栈极其简单两个模块之间直接发数据帧就行不依赖任何基础设施。开关控制这个场景数据量本来就小得可怜一次只是发几个字节的开灯关灯指令LoRa这种窄带低速传输完全就是量身定做。语音识别这边我选了离线语音识别模块。所谓离线语音就是识别模型和声学算法全部跑在本地芯片上不需要把音频上传到服务器再返回识别结果。现在市面上的离线语音识别模块已经做得相当成熟了可以自定义唤醒词、命令词识别准确率在安静环境下都挺能打。离线语音解决的是“人怎么告诉开关要干什么”LoRa解决的是“开关怎么把指令送到远端设备”两者各管一摊都不依赖公网这套系统断网之后依然能正常工作。2. 总体设计一天理清架构两天画完框图2.1 系统架构一个语音网关面板加N个远端通断器整个系统我设计成两类设备。第一类是语音控制面板安装在客厅或者玄关这种经常有人经过的地方。它负责采集语音指令完成离线识别然后通过LoRa把指令发出去。面板上我保留了实体触摸按键万一语音识别抽风了手动按一下也能控制双保险。第二类是远端通断器装在各个房间的灯位、插座或者吊顶灯带上。通断器本质上就是一个可控的继电器本地MCU收到LoRa无线指令之后驱动继电器吸合或者断开从而控制220V交流电的通断。两类设备之间用LoRa无线链路通信一个语音控制面板可以管理多个远端通断器只要给每个通断器分配不同的地址就行。这个架构的好处是扩展性很好你想加一个开关不需要动原来的面板只需要在房间里多装一个通断器模块然后把地址写进去面板那边在映射表里加一条记录就行。从拓扑结构上看这是一种星型网络面板是中心节点通断器是终端节点。对于家庭环境来说这种结构比Mesh简单得多逻辑清晰调试起来也方便。如果以后想搞多面板协同可以给多个面板分配不同的地址范围各自管理各自的设备组。2.2 器件选型主控、语音模块、LoRa模块、继电器怎么选主控这块我一开始纠结过是用STM32F103C8T6还是用Arduino Nano。后来想了想这套系统的逻辑并不复杂核心就是串口接收语音模块的指令、通过SPI和LoRa模块通信、用GPIO控制继电器不需要跑什么复杂的算法也不需要WiFi协议栈。最后为了方便开发和测试我选了Arduino Nano兼容板也行用起来顺手烧录调试都快。如果你手头有STM32F103的最小系统板也完全可以用逻辑完全一样只是多花点时间折腾库和底层初始化。语音模块我选的是SU-03T。这款模块是深圳那边一家公司做的离线语音识别方案本地识别支持自定义唤醒词和命令词通过串口TTL输出识别结果。开发工具叫天问Block图形化编程拖拽积木就能配置语音模型对不太熟悉代码的硬件爱好者非常友好。模块自带DAC驱动功放可以接一个喇叭播放语音提示像“已开灯”、“已关灯”这种反馈都能自己录。LoRa模块我用的是SX1278方案具体型号是Ra-02或者ATK-LoRa-01工作频率433MHz发射功率最高20dBm。这里要提醒一句选模块的时候尽量选带屏蔽罩的版本抗干扰能力会好一些。市面上也有一些串口透传型的LoRa模块但我不推荐用那种来做这个项目因为透传模块把协议都封装死了你没法自定义帧格式也没法拿到RSSI信号强度数据排查问题会很痛苦。用SX1278原生SPI接口配合开源的LoRa库想怎么折腾就怎么折腾。继电器我选用的是3.3V驱动的低功耗继电器额定负载10A 250VAC控制普通灯具、插座、电风扇绰绰有余。这里有一个重要的选型逻辑整套板子的控制电源是3.3V如果继电器也是3.3V驱动就只需要一组电源轨不用额外搞5V转换简化电源设计也降低故障点。电源模块选的是HLK-PM03这是一个220V交流转3.3V直流的隔离电源模块体积小带载能力大概3W足够MCU、LoRa、语音模块和继电器的工作了。如果你要驱动5V继电器那就要选择HLK-PM05再配一个稳压电路多一级电路就多一个发热点所以我还是建议直接走3.3V继电器路线。2.3 通信协议一条自定义帧打天下LoRa通信不像是走MQTT那种应用层协议它本质上就是一个无线串口你能做的就是在物理层上发一段字节流。为了能让系统可靠工作我自定义了一套精简的控制帧协议。帧格式设计如下字节偏移 0 1 2 3 4 5 6 7 含义 帧头 帧头 目标地址 源地址 类型 命令 CRC高 CRC低 取值 0xAA 0x55 0x01 0x10 0x01 0x10 0x1A 0x2B帧头固定是0xAA 0x55用来做同步检测。目标地址是指你要控制的通断器地址比如客厅灯是0x01卧室灯是0x02源地址是发送方的地址便于接收方回ACK。类型字段用来区分这一帧是控制帧0x01、状态查询帧0x02还是应答帧0x03。命令字段就是具体动作开0x10、关0x11、查询状态0x12。最后两个字节是CRC16校验用来检查数据在传输过程中有没有被干扰破坏。这个协议乍一看很简单但实际用起来极其顺手。发送端发一条8字节的帧整个发送过程在125kHz带宽、SF10扩频因子下大约耗时100多毫秒对开关控制这种场景来说完全没压力。CRC校验加目标地址过滤双保险基本杜绝了误触发邻居设备或者被环境噪声干扰导致误动作的问题。3. 硬件实操从面包板到86盒调试3.1 先搭最小系统把语音模块、MCU、LoRa跑通我建议所有想复刻这个项目的朋友第一版不要急着画PCB先在面包板上把最小系统搭出来把一个个功能模块分别调通再考虑整合。我自己的调试顺序是这样的先把Arduino Nano和LoRa模块的SPI接口接好用两个模块对发数据确认无线链路通然后接语音模块通过串口调试看能不能收到识别结果最后再接继电器确认GPIO能控制吸合。LoRa模块和Arduino Nano的接线是很标准的SPI四线制NSS接D10引脚SCK接D13MOSI接D11MISO接D12RST接D9DIO0接D2。如果用的模块是Ra-02这种邮票孔封装的记得把IPEX天线焊好不然功率输出完全出不去。语音模块SU-03T的接线更简单VCC接3.3V或5V看模块手册一般支持3.3V和5V宽电压GND共地TX接Arduino的RX串口接收RX接Arduino的TX串口发送SPK正负极接一个小喇叭。模块默认波特率是9600这个可以在天问Block工程里配置。在面包板上调试最大的问题是飞线太长、地回路不干净LoRa的RSSI会偏低。不过这不影响功能验证你先确认数据能通就行。我第一版飞线调试的时候两个模块隔着桌子收发RSSI在-95dBm左右后面上了PCB板同样距离直接变成-65dBm差距非常大。所以面包板阶段如果觉得距离不行先别怀疑模块大概率是布线问题。3.2 继电器驱动与防回灌新手最容易烧GPIO继电器驱动这块我必须多说几句因为我见过太多人在这一步把MCU的GPIO烧了。如果你的继电器是3.3V驱动的而且模块上已经带了三极管驱动电路那你可以直接拿MCU的GPIO去控制IN引脚问题不大。但如果你用的是那种裸继电器加5V线圈千万不要直接把GPIO接到线圈上MCU的GPIO输出电流通常只有几毫安到十几毫安驱动线圈需要几十毫安带不动不说继电器动作时线圈产生的反向电动势还会沿着引脚倒灌进MCU轻则复位重则烧引脚。正确的接法是加一级NPN三极管驱动比如S8050或者直接用一个ULN2003达林顿驱动芯片。继电器线圈两端要并联一个续流二极管1N4007方向是负极接电源正极、正极接三极管的集电极作用是在线圈断电时给反向电动势一个泄放回路保护驱动电路。这一步非常重要千万不要省略。软件上也要做个去抖处理。继电器从GPIO拉高到触点完全吸合大概需要5到10毫秒如果在这个时间内反复切换GPIO继电器会“嗒嗒嗒”一通乱抖触点寿命急剧下降。我实现在发送控制指令之后会加一个100毫秒的锁定时间期间忽略任何新的控制指令确保继电器稳定完成一次动作。3.3 电源、天线和220V走线安全细节别疏忽处理220V交流电的时候安全是第一位的所有操作必须断电后进行。通断器内部是这样的火线L进经过继电器触点再到灯具回来形成回路零线N直接接到电源模块给控制板供电。控制板的供电可以从继电器前端的火线和零线之间取电用HLK-PM03转成3.3V。这里有一个很重要的布局原则高压区和低压区必须在物理上分开。开关电源模块、继电器触点、220V接线端子算高压区MCU、语音模块、LoRa算低压区。两边至少保持5mm以上的爬电距离不然绝缘不好容易跳火。天线放置是另一个大坑。LoRa模块的天线一定要离开关电源模块远一点更不要贴着金属外壳或者高压走线。我实测过天线贴在开关电源模块上方通信距离直接从30米缩水到5米。如果你要把板子装进86底盒金属底盒会屏蔽很大一部分信号建议用带延长线的外置天线把天线引到塑料面板外侧。如果是塑料底盒问题不大但也要让天线尽量朝向开阔方向。4. 软件实现语音指令如何“翻译”成LoRa报文4.1 SU-03T语音模块配置5分钟搞定唤醒词和命令词语音模块的配置在天问Block的图形化界面里完成。新建工程之后选择目标芯片SU-03T然后在“方案配置”里自定义唤醒词和命令词。我这里列一份我实际使用的配置方案类型具体内容触发后串口输出唤醒词小智小智0x00命令词打开客厅灯0x01命令词关闭客厅灯0x02命令词打开卧室灯0x03命令词关闭卧室灯0x04命令词全部开灯0x05命令词全部关灯0x06在天问Block里你可以给每个命令词设置“串口打印”的内容打印值就是上表里的十六进制数值。编译烧录到模块之后模块一旦识别到对应的命令词就会通过串口把对应的字节发出来。MCU这边的任务非常单纯读串口收到什么值就查表然后组LoRa帧发出去。有一点要注意唤醒词和命令词不要选发音太相近的词不然识别率会打架。比如你唤醒词叫“小智小智”命令词里就别再有任何带“智”发音的词。我一开始把“打开电视”和“打开电扇”放在一起结果经常误触发后来换成了“打开风扇”才消停。4.2 LoRa收发逻辑状态机设计避免丢命令和乱响应LoRa通信这部分我使用开源的 sandhda/LoRa 库这个库封装了SX1278的底层操作用起来很直接。初始化代码大致是这样的#include SPI.h #include LoRa.h #define NSS_PIN 10 #define RST_PIN 9 #define DIO0_PIN 2 void setupLoRa() { LoRa.setPins(NSS_PIN, RST_PIN, DIO0_PIN); if (!LoRa.begin(433.0)) { Serial.println(LoRa init failed!); while (1); } LoRa.setSpreadingFactor(10); LoRa.setSignalBandwidth(125E3); LoRa.setCodingRate4(5); LoRa.setTxPower(20); // 20dBm LoRa.enableCrc(); }这段配置里频率433MHz是国内最常见的LoRa频段扩频因子SF10是在通信距离和速率之间取了个平衡点带宽125kHz是LoRa标准配置发射功率开到20dBm约100mW。设置完这些参数LoRa模块就可以收发数据了。发送端组帧发送的核心代码如下typedef struct { uint8_t header[2]; // 0xAA 0x55 uint8_t dstAddr; // 目标地址 uint8_t srcAddr; // 源地址 uint8_t type; // 0x01控制 0x02查询 0x03应答 uint8_t cmd; // 0x10开 0x11关 0x12查询 uint16_t crc; // CRC16 } LoRaFrame; void sendCommand(uint8_t dstAddr, uint8_t cmd) { LoRaFrame frame; frame.header[0] 0xAA; frame.header[1] 0x55; frame.dstAddr dstAddr; frame.srcAddr 0xF0; // 面板地址 frame.type 0x01; frame.cmd cmd; frame.crc calcCrc((uint8_t*)frame, 6); LoRa.beginPacket(); LoRa.write((uint8_t*)frame, sizeof(frame)); LoRa.endPacket(); }接收端这边我在主循环里不断检查有没有收到数据。收到后先校验帧头、CRC再判断目标地址是不是自己。只有帧头对、CRC对、地址匹配的帧才会被放行然后执行对应的控制动作。这套三重校验下来误动作概率真的可以忽略不计。我实际跑了一个多月没有出现过一次因为无线干扰导致的误开关。这里还涉及一个状态机设计的问题。发送端发出控制指令后不能直接认定指令已经到达必须等远端通断器回一帧ACK应答。如果1秒内没收到ACK就自动重发最多重发两次。如果两次都超时就判定通信失败面板语音播报“操作失败请检查设备”。这个机制能有效避免用户按了开关但实际上没执行回头还要跑过去看灯的尴尬。4.3 按需唤醒与状态同步一个容易被忽略的细节做通断器系统的时候很多人只关注“怎么把开灯指令发出去”却忽略了“怎么知道灯现在已经开了还是关了”。如果不同端的状态没有同步就会出现一种很恼人的情况你在A面板上开了客厅灯走到B面板上按“开灯”它又把灯开了一遍——实际上是关了因为继电器状态翻转了。为了避免这种问题我在协议里加了一个状态同步机制。每个通断器本地维护一个状态变量记录当前继电器是吸合还是断开。收到控制指令并成功执行之后通断器会主动向源地址回一帧状态报文内容包括当前状态。源面板收到状态报文后更新本地的设备状态表。此外面板上电之后会向所有已配置的地址广播状态查询帧通断器收到后立即回报状态这样面板重启之后也能很快恢复全屋的开关状态表不用用户手动去拉。另一个细节是LoRa模块的发送前监听。SX1278支持读取当前信道的RSSI值发送之前先读一下如果信道忙RSSI超过阈值就随机退避几十毫秒再发这算是一种简易的CSMA/CA机制。家里多个开关同时操作的概率不高但接了这套机制之后几乎不会出现两个模块同时发送导致冲突丢包的情况。5. 实测与调优距离、时延、并发这些硬指标5.1 距离和穿墙实测数据心里有个底LoRa最吸引人的地方就是通信距离但实际使用中到底能覆盖多大的房子还是得实测才知道。我做完第二版PCB之后专门做了一轮完整的距离测试。测试方法是固定语音面板在客厅电视柜位置拿着通断器板子往远处走边走边观察LoRa模块的RSSI值收到ACK的临界点作为信号边界。场景距离RSSI收发结果同房间无障碍10m-45dBm稳定收发隔一堵24cm砖墙15m-78dBm稳定收发隔两堵砖墙加拐角12m-102dBm稳定收发偶有重传同楼层最远对角线25m-92dBm稳定收发跨一层楼板垂直距离4m-88dBm稳定收发这个结果对普通家庭来说完全够用哪怕是一两层的自建房一台面板也能覆盖全屋。需要注意的是不同模块、不同天线、不同墙体结构差异很大我这里的数字仅供参考。如果你对距离有更高的要求可以考虑把扩频因子从SF10提高到SF12代价是传输速度从约1kbps降到约0.3kbps但对开关控制这种极低速应用来说毫无影响。再或者换一个更大功率的射频前端比如加大功率版本的SX1278模块1W距离还能翻倍。5.2 时延、误触和并发问题处理整个系统的时延拆开来算大概是这样语音识别在SU-03T本地完成从说完话到串口输出结果实测大约0.6秒到1秒LoRa帧发送加ACK往返大约200到300毫秒继电器动作本身只有10毫秒左右。整体从你喊出“小智小智打开客厅灯”到灯亮大约1.2秒到2秒之间家里的长辈一开始会觉得“有点慢”但用了几天就习惯了毕竟不用自己走过去按开关。误触问题主要靠协议层的三重校验来杜绝。CRC16漏检的概率极低目标地址过滤又能排除无关设备。我在测试中还故意拿了一台SDR设备在旁边干扰433MHz频段也没有诱发过一次误动作说明这种窄带调制技术的抗干扰能力确实名不虚传。并发问题主要体现在多个通断器同时应答的时候。如果面板发了一个“全部开灯”广播指令所有通断器会在同一时间回传ACK虽然面板不用处理这些ACK但空中信道里会有大量数据碰撞。为了缓解这个问题每个通断器可以配置一个随机延时收到广播指令后不立即回ACK而是随机等待20到80毫秒再回分散信道占用。这个方法简单粗暴但在实际测试中效果显著。5.3 装进86盒的注意事项能避开就尽量避开把板子做出来是一回事把它装进家里的86底盒又是另一回事。86盒内部空间极其有限我第一版PCB做出来的尺寸是70mm乘70mm结果发现大多数国产86底盒实际内径只有65mm乘65mm根本塞不进去。后来重新改版把板子压缩成54mm乘54mm才勉强装得下。如果实在装不进86盒我建议优先选择明装盒或者导轨盒买一个标准的4P导轨塑壳体积够大接线也方便虽然会凸出墙面一点但对追求实用性的人来说完全能接受。走线的时候要注意把220V的线缆固定在盒内尽量不要松垮地搭在LoRa模块和天线附近否则通信距离会受影响。金属底盒是LoRa信号的杀手。铁质86盒对433MHz的屏蔽作用极强天线放盒内基本上就是自废武功。如果你的家里底盒是金属材质天线必须通过延长线引到盒子外面或者干脆改用塑料面板把天线贴在内侧。6. 踩过的坑和排查技巧实录6.1 常见问题速查表做这套系统的过程中我前前后后踩了不少坑挑几个典型的问题整理成表格给后来人排雷现象可能原因解决方法继电器反复吸合抖动电源功率不足或GPIO驱动能力不够换更大功率电源模块检查继电器驱动电路是否带续流二极管语音模块唤醒无反应MIC孔被遮挡、唤醒词和命令词发音太相近检查MIC位置重新录制唤醒词避免发音冲突LoRa通信距离只有3米天线虚焊、天线类型不匹配、模块没有地平面重新焊接天线确认天线长度是433MHz频段的匹配尺寸改PCB并预留地平面控制板在继电器动作瞬间重启开关电源纹波大或瞬态压降严重电源输出端并联1000uF电解电容避免多个继电器同时吸合关灯后LED灯有微光继电器断开了火线但零线仍有回灌电流改用双控接线方案在负载两端并联RC吸收电路温度升高后误码率上升LoRa对晶振频偏敏感温漂导致频率偏移选用带TCXO温度补偿晶振的模块降低扩频因子烧录语音模块失败天问Block版本和模块固件不匹配更新IDE到最新版本重新给模块烧录底层固件6.2 几个我从文档里没读到的经验最后分享几个纯粹靠实际操作摸索出来的经验这些在官方文档里基本找不到。第一LoRa模块的地平面问题。模块底下铺不铺地平面通信距离差三到五倍。面包板飞线调试的时候模块悬在半空地回路很长RSSI烂到不行。我把模块焊到PCB上、底下铺了一整块实铜地之后同样的距离RSSI一下子提升了30dB以上。所以千万不要图省事直接飞线装机一定要老老实实画PCB。第二433MHz天线的长度是四分之一波长理论值约17.3厘米市面上卖的433MHz橡胶天线实际长度一般在17到18厘米左右这个匹配是出厂调好的。如果你自己用导线做天线长度宁肯略短不要过长因为过长的天线在末端会呈现容性失谐反而让效率变差。如果想测天线是否正常可以用万用表量天线中心导体和外壳地之间正常应该呈几十欧姆左右的阻抗开路或者短路都说明天线有问题。第三继电器驱动芯片输出电压压降的问题。ULN2003/2803这类达林顿芯片饱和导通时管压降会到1V到1.5V如果用3.3V逻辑去驱动5V继电器实际加到继电器线圈上的电压可能只有3.5V左右继电器会处于半吸合状态反复抖动、发热、触点烧蚀。所以我前面特别强调要么用3.3V继电器要么给继电器单独供5V电源不要靠MCU的逻辑电源硬撑。第四关于LoRa调制方式的温度敏感性。SX1278这种Chip扩频调制对晶振精度非常敏感普通晶振的温漂在高温环境下会导致频率偏移几十百赫兹接收灵敏度直线下降。如果你的设备安装位置是阳台、屋顶或者厨房这种温度变化大的地方建议模块选择带TCXO温度补偿晶振的版本贵不了几块钱但稳定性好很多。我实际使用中还发现一个有意思的现象环境温度升高之后LoRa的RSSI会小幅度下降误码率也会升高。这个在夏天尤其明显。排查了很久才确定是晶振温漂导致的问题后来换成TCXO版本就再没出现过。另一个容易被忽略的细节是LoRa模块的复位脚处理。如果RST引脚悬空上电瞬间模块可能进入异常状态表现就是偶尔收不到数据。接MCU的GPIO并且在初始化时做一次低电平复位脉冲可以大大提升模块的启动稳定性。这个坑我排查了两天最后看了SX1278的时序图才反应过来。最后再提一个跟语音相关的经验。SU-03T模块在初次配置时麦克风拾音增益默认值偏保守环境稍微有点噪音识别率就掉得厉害。后来我在天问Block里把麦克风增益调高了几个档位识别率明显上升。但也不能调太高否则会持续触发识别反馈嘟嘟响个不停。这个参数需要根据自己的家居环境反复试没有一个万能值。这套系统前后做了四版从面包板到最终稳定运行的PCB板前后用了大概两个星期。目前在家里已经跑了几个月经历了路由器重启、宽带故障、云服务商宕机等好几次状况家里的灯都不受影响地正常开关。我个人体会是这套系统的价值不完全在于“自己动手做智能家居”的成就感而是把控制链路完全掌握在自己手里任何外部服务挂了都不影响我们居家最基本的照明需求。后续如果想扩展我计划加一个ESP32做桥接把设备状态同步到HomeAssistant这样既能享受开源智能家居平台的自动化能力断网时又保留了本地这套底线控制链路。也可以给通断器加一路电流检测通过负载电流的有无来判断灯具是否损坏实现真正意义上的“灯坏了自己上报”。这套架构的底子是稳的往上加功能只是时间问题。