
简介一套基于STM32与ESP8266的智能家居系统源码包面向物联网开发者与企业应用场景解决家庭环境实时监控、远程控制及智能联动问题。压缩包共127个文件以C源码为主体43个.c与46个.h覆盖STM32标准外设驱动如TIM、ADC、I2C、USART等与MQTT通信协议另含微信小程序前端js/wxml/wxss/json、Keil工程配置uvprojx/uvoptx、9张原理图、bat构建脚本及readme说明文档包体仅441KB目录结构清晰。已有246人浏览学习适合具备一定STM32基础、希望快速搭建完整物联网方案的开发者。资源包含src主控程序、sensors传感器驱动、firmware固件、docs设计文档及wechat_app小程序工程可从传感器数据采集、ESP8266无线传输一路贯通到小程序界面控制覆盖从底层驱动到上层应用的完整开发流程。同时集成AI自动调节逻辑能学习用户习惯并联动控制智能设备适合作为企业智能环境管理与个人项目毕业设计的可移植参考。1. 为什么 stm32esp8266 的智能家居系统值得从这套组合切入想给房间加远程开关的人第一反应往往是去买成品智能插座。但如果你有二三十个设备要控或者要接温湿度、燃气、人体红外这些传感器成品的成本很快就压不住而且本地逻辑全被锁在云端断网就变傻。反过来用 stm32 和 esp8266 的智能家居系统自己搭是嵌入式圈子里最成熟也最便宜的一条路stm32 管本地采集与控制esp8266 管联网透传两个芯片用一根串口线对话。很多人以为难点在 WiFi 连不上、代码跑不通实际真正决定这系统能不能长期用的是数据帧设计、状态管理和断线恢复这些容易被忽略的细节。这篇文章就把这套组合从接线到上云的完整路径拆给你包括每个参数为什么这么设、翻车点在哪。2. stm32esp8266 智能家居系统怎么搭架构、接线与通信协议选型2.1 架构分工为什么把 esp8266 只当通信模块用esp8266 本身也是一颗能跑程序的芯片完全可以用它直接接传感器做控制。但实际对比下来我一般不建议这么做。esp8266 的 GPIO 数量有限ADC 精度一般多路继电器和传感器并行处理时代码稍复杂就容易出现时序抖动。stm32 的外设资源、中断响应和定时器精度都更稳定本地实时性要求高的逻辑放在 stm32 上更踏实。更关键的是维护成本。esp8266 跑透传固件时它只是一个通信管道stm32 侧不用关心 WiFi 细节只管往串口写数据帧。如果以后要换 WiFi 模组只要接口不变stm32 代码几乎不用动。用「stm32 做本地大脑、esp8266 做网卡」这种拆分换来的是系统边界清晰、后续排查问题能快速定位是网络侧还是控制侧。2.2 硬件接线串口交叉连接与独立供电先看最小连接。stm32 选 USART1对应引脚是 PA9 和 PA10跟 esp8266 的串口交叉对接stm32 引脚对应外设接 esp8266 引脚说明PA9USART1_TXRXDstm32 发送给 esp8266PA10USART1_RXTXDesp8266 发送给 stm32GND电源地GND必须共地否则数据乱码3V3稳压输出VCC独立供电见下方注意两个芯片都是 3.3V 逻辑电平可以直接连不需要电平转换。需要单独强调的是供电esp8266 在发射瞬间电流能到 300mA 以上很多开发板上的 3.3V 稳压输出带不动它表现出症状就是连上 WiFi 后反复重启。我习惯用独立的 3.3V 稳压模块给 esp8266 供电stm32 这边只要保证它自己的电源稳定就行。波特率建议直接设 115200、8 位数据位、1 位停止位、无校验这是多数 esp8266 AT 固件的默认串口参数。stm32 侧如果用标准外设库初始化时对应设置好 USART_InitStructure 就行别图省事用默认的 9600后面 AT 指令交互会一塌糊涂。2.3 协议选型串口透传 MQTT 的实现路径stm32 和 esp8266 之间的通信协议有几种常见做法对比如下方案优点缺点适用场景esp8266 跑 AT 固件 串口透传实现简单、stm32 代码量小TCP 连接管理依赖 AT 指令大多数入门到中级项目esp8266 刷 NodeMCU 固件跑 Lua逻辑可写在 esp8266 侧调试困难、内存紧张极简单设备场景stm32 直接移植 TCP/IP 协议栈灵活度最高工作量大、内存和 flash 压力大对网络要求极高的工业项目最常见的方案是第一种esp8266 用 AT 固件建立 TCP 透传stm32 侧把要发的数据按帧拼好直接塞进串口云端协议用 MQTT因为 MQTT 报文本身就是一段可拼接的字符串stm32 不需要移植完整客户端只要按格式组包发给 MQTT broker 就行。这里有个容易被误解的点MQTT 和透传是两件事。透传解决的是 stm32 和云端之间的 TCP 通道问题MQTT 解决的是通道里跑的数据格式问题。stm32 侧发出去的内容是 MQTT CONNECT、PUBLISH 这些控制报文esp8266 只负责原样转发。理解了这个分层后面调试时思路会清晰很多。3. 让 esp8266 先联网AT 固件、配 WiFi 与透传模式的正确顺序3.1 开始前先做三件事接线确认、串口参数、AT 应答测试esp8266 侧我一般先单独调试不接 stm32避免两边互相干扰。用一个 USB 转串口模块把 esp8266 接到电脑串口工具里设 115200、8N1然后发一个 AT 指令测试通信。指令参数含义预期返回失败时检查AT测试串口通信OK接线是否交叉、波特率是否正确ATRST复位模组OK供电是否稳定ATE0关闭回显OK无ATCWMODE1设为 station 模式OK固件版本是否支持ATCWJAPssid,pwd连接 WiFi 路由OK 后出现 WIFI GOT IP密码是否错误、信号强度ATCIPSTARTTCP,服务器地址,端口建立 TCP 连接CONNECT、OK服务器地址是否可达ATCIPMODE1开启透传模式OK必须已在连接状态ATCIPSEND进入透传发送返回 提示符无新手容易卡在第一步发了 AT 没反应。先检查 USB 转串口模块和 esp8266 的 TXD、RXD 是不是交叉接的再看供电是否稳定。这个阶段把问题定位清楚比接了 stm32 之后再排查省事得多。3.2 配 WiFi 三步关回显、设 station 模式、接路由确认 AT 有响应后按下面的顺序操作ATE0 # 关闭回显后面收返回更干净 ATCWMODE1 # 1 表示 station 模式只做设备端 ATCWJAP你的WiFi名,你的WiFi密码 # 连接路由器每一条指令都要等上一句返回 OK 再发下一条。ATCWJAP 的返回比较特殊会先返回 OK过一两秒又出现一行 WIFI GOT IP这时候才说明真正拿到 IP 了。这里容易踩的一个坑是 WiFi 名带特殊字符。某些 AT 固件对中文 SSID 或空格处理有问题连不上时先换个纯英文数字的 SSID 测试。密码一定要检查有没有多余空格AT 指令是整行解析的多一个空格就认证失败。3.3 进透传模式并验证数据通路电脑端先用 TCP 工具收数据WiFi 连上之后先不开透传用 TCP 方式验证网络链路。在电脑上开一个 TCP Server 工具监听某个端口然后给 esp8266 发指令ATCIPSTARTTCP,192.168.1.100,8000 # 连接电脑的 IP 和端口 ATCIPMODE1 # 开启透传模式 ATCIPSEND # 进入发送状态进入透传模式后你在串口工具里输入的任何字符都会被 esp8266 原样转发到 TCP Server。反过来TCP Server 发来的数据也会原样出现在串口上。这个双向通路验证通过说明 esp8266 的联网功能已经没问题了后面接 stm32 就只是在串口上灌数据帧而已。注意一个细节退出透传模式不是发 AT,而是直接发三个加号而且不能带回车换行。这个序列被固件识别为退出透传的特殊指令带了回车它就会当成普通数据发出去。很多人在这一步卡住还以为是模块坏了。4. stm32 端程序接法串口中断、数据帧与命令路由4.1 串口初始化与接收中断环形缓冲避免丢帧esp8266 转发过来的数据是异步到达的stm32 如果在主循环里轮询串口处理其他任务时就会丢字节。正确做法是开接收中断把每个字节存进环形缓冲主循环再到缓冲区里取数据做解析。以标准外设库为例初始化加中断处理的代码框架如下#define RX_BUF_SIZE 256 volatile uint8_t rx_buf[RX_BUF_SIZE]; volatile uint16_t rx_head 0; // 中断写入的位置 volatile uint16_t rx_tail 0; // 主循环读取的位置 void USART1_IRQHandler(void) { uint8_t ch; if (USART_GetITStatus(USART1, USART_IT_RXNE) ! RESET) { ch USART_ReceiveData(USART1); uint16_t next (rx_head 1) % RX_BUF_SIZE; if (next ! rx_tail) // 判断缓冲区是否已满 { rx_buf[rx_head] ch; rx_head next; } // 缓冲区满时丢弃新字节不覆盖旧数据 } } void uart1_init(uint32_t baud) { GPIO_InitTypeDef gpio; USART_InitTypeDef usart; RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE); gpio.GPIO_Pin GPIO_Pin_9 | GPIO_Pin_10; gpio.GPIO_Mode GPIO_Mode_AF_PP; gpio.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, gpio); usart.USART_BaudRate baud; usart.USART_WordLength USART_WordLength_8b; usart.USART_StopBits USART_StopBits_1; usart.USART_Parity USART_Parity_No; usart.USART_Mode USART_Mode_RX | USART_Mode_TX; USART_Init(USART1, usart); USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); USART_Cmd(USART1, ENABLE); }环形缓冲的关键在于读写分离中断只写 head主循环只读 tail两者互不抢占。缓冲区大小取 256因为一帧数据最长也就几十字节256 足够缓冲掉主循环偶尔的延迟。判断满的条件是next rx_tail留一格不清空是为了让 head 和 tail 相等时有明确的「空」语义。中断里只做存字节这一件事不要在里面做数据帧解析或者发心跳。中断函数越短主程序被影响的概率越小这是嵌入式调试血泪经验换来的习惯。4.2 数据帧与校验协议固定下来后面少踩坑stm32 和 esp8266 之间传的是裸字节如果没有帧格式接收端根本不知道从哪开始读、读到哪结束。我用的帧格式很朴素但够用字节偏移字段说明0帧头 0xAA固定帧头1帧头 0x55两字节帧头降低误判概率2长度命令字 数据的字节数3命令字0x01 控制、0x02 查询、0x03 主动上报4~n数据按命令字定义可为空n1~n2校验累加和高字节、低字节对应的发送函数uint16_t frame_calc_sum(uint8_t *buf, uint16_t len) { uint16_t sum 0; for (uint16_t i 0; i len; i) sum buf[i]; return sum; } void frame_send(uint8_t cmd, uint8_t *data, uint8_t len) { uint8_t tx_buf[128]; uint16_t idx 0; tx_buf[idx] 0xAA; // 帧头1 tx_buf[idx] 0x55; // 帧头2 tx_buf[idx] 1 len; // 长度域命令字占1字节 tx_buf[idx] cmd; // 命令字 for (uint8_t i 0; i len; i) tx_buf[idx] data[i]; uint16_t sum frame_calc_sum(tx_buf, idx); // 帧头到数据全参与校验 tx_buf[idx] (sum 8) 0xFF; tx_buf[idx] sum 0xFF; for (uint16_t i 0; i idx; i) { while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) RESET); USART_SendData(USART1, tx_buf[i]); } }长度域设计成1 len目的就是让接收端拿到长度后能确定整帧边界不需要去猜。校验用累加和而不是 CRC8是因为帧很短、数据量小累加和的检错能力足够而且代码一眼能看懂。如果你以后要传大量二进制数据再换 CRC16 也来得及。发送函数里的 TXE 标志判断保证不丢字节。注意先查标志再发数据顺序反了会出现重复发送或者错位。4.3 命令解析与本地执行收到 0x01 翻转继电器接收端拿到完整帧后需要解析命令字并执行。解析用状态机比字符串查找可靠得多。下面这个函数供主循环逐字节调用返回 1 表示收到一帧完整且校验通过的数据uint8_t frame_rx_poll(uint8_t byte) { static uint8_t state 0; // 状态机当前状态 static uint8_t len 0, idx 0; static uint8_t buf[70]; static uint16_t sum 0; switch (state) { case 0: if (byte 0xAA) state 1; return 0; case 1: if (byte 0x55) { state 2; sum 0; idx 0; } else state 0; // 帧头错误重新找 return 0; case 2: len byte; sum byte; if (len 0 || len 64) { state 0; return 0; } // 非法长度 state 3; return 0; case 3: if (idx len) { buf[idx] byte; sum byte; } if (idx len) state 4; return 0; case 4: // 第一个校验字节高字节这里简化处理 state 5; return 0; case 5: // 第二个校验字节低字节与 sum 比较 state 0; if ((uint8_t)(sum 8) buf[0] (uint8_t)sum buf[1]) { frame_exec(buf, len - 1); // buf[0]是命令字 return 1; } return 0; } return 0; }这里状态 4 和 5 的校验比较做了一点简化实际项目中应当在状态 4 存下第一个校验字节、状态 5 收到第二个再统一比对。状态机的好处是每一帧的处理不会因为中间混入噪音字节而崩溃顶多多等几个字节就能重新对齐帧头。对应的执行函数void frame_exec(uint8_t *buf, uint8_t len) { uint8_t cmd buf[0]; switch (cmd) { case 0x01: // 远程控制继电器 relay_set(buf[1]); // buf[1]为0或1 frame_send(0x81, buf, len); // 回一个ACK帧 break; case 0x02: // 查询设备状态 frame_send(0x82, status_buf, 2); break; default: break; } }继电器控制函数里要注意如果继电器是低电平触发relay_set(1)对应 GPIO 写 0这个映射关系写清楚后面调云端联动时才不会出现「下发开却变成关」的灵异问题。4.4 周期上报把传感器值拼成帧10 秒一次主动上报是让云端知道设备状态的关键。我习惯把温湿度、开关状态打包成一帧每 10 秒上报一次void report_sensor(void) { uint8_t payload[4]; payload[0] (uint8_t)(temp 8); // 温度高字节 payload[1] (uint8_t)(temp 0xFF); // 温度低字节 payload[2] (uint8_t)(humi 8); // 湿度高字节 payload[3] (uint8_t)(humi 0xFF); // 湿度低字节 frame_send(0x03, payload, 4); }上报频率不用太猛。10 秒一次对智能家居足够服务器端判定设备在线也通常只看最近一两分钟内有没有心跳。如果以后接入的平台对上报频率有限制把这里改成动态间隔就行。5. stm32esp8266 智能家居最常见的 5 个坑现象、原因、解法5.1 程序下载失败Keil 提示连接不上目标设备现象stm32 接上 esp8266 后点下载总是报错拔掉 esp8266 的串口线就恢复正常。原因USART1 同时承担了下载和通信功能。esp8266 在 stm32 复位期间可能拉低 TX 引脚导致 boot 引脚的电平状态被干扰下载器握手失败。解决下载程序时把 esp8266 的 RXD、TXD 用跳线帽断开下载完再插回去。如果嫌麻烦可以在电路上给串口加一个拨码开关运行时合上、下载时断开。这不是玄学是串口复用的固有限制。5.2 esp8266 反复重启WiFi 一直连不上现象串口输出里不断出现乱码和重启信息模块外壳明显发热偶尔能连上路由器但几秒又掉线。原因供电不足。esp8266 发射瞬间电流可到 300mA而很多模块的 3.3V 输入来自 USB 转串口板那种板子的 3.3V 输出一般只有几十毫安。解决单独给 esp8266 供电或者在 esp8266 的 VCC 和 GND 之间并一个大电容比如 470uF 电解电容用来缓冲电流尖峰。更稳的做法是换一个输出电流 500mA 以上的稳压模块把 5V 降到 3.3V 独立供电。5.3 收到的数据乱码或半截现象stm32 串口收到的数据有乱码或者一帧数据经常缺尾。原因最常见的是 GND 没共地。stm32 和 esp8266 各接各的电源GND 电位不一致串口信号就会乱。其次是波特率不匹配stm32 设了 9600esp8266 固件默认 115200。解决先用万用表确认两边 GND 导通测量 VCC 对 GND 的电压稳定在 3.3V 附近。再用电脑串口工具单独测 esp8266 的返回确认它在哪个波特率下输出正常然后再去核对 stm32 的初始化代码。5.4 断电重连后设备再也回不到线上现象家里路由器重启一次esp8266 就再也连不上必须手动给模块断电才能恢复。原因AT 固件在 TCP 连接断开后不会自动重连。你看到的是 WiFi 连接还在但 TCP 通道已经断了服务器端显示设备离线。解决stm32 侧要对 esp8266 的返回做监控。如果串口里出现 CLOSED 或者长时间没有收到任何数据就主动重新执行 ATCIPSTART 建立连接。重试要加延时比如第一次失败后等 5 秒别在 1 秒内狂发指令否则模块可能根本来不及处理。5.5 继电器一吸合整个系统复位现象远程控制继电器时stm32 直接重启有时候 esp8266 也掉线。原因继电器线圈在断电瞬间会产生反向电动势如果没有续流二极管这个尖峰电压会通过电源线反灌到主控板。另一个常见原因是继电器模块和 stm32 共用一个电源吸合瞬间电流过大把电压拉垮。解决继电器模块选带光耦隔离的型号线圈两端加续流二极管。电源上把继电器模块单独一路供电别和 stm32、esp8266 混在一起。这一条看起来像玄学实际是电磁兼容的基本功不处理早晚出事。6. 进阶技巧用状态机做断线重连顺便加一个本地兜底开关6.1 一个能扛住掉线的状态机前面坑 5.4 提到了断线重连这里给出具体做法。stm32 主循环按状态迁移来管理网络连接比散落的 if else 可靠得多typedef enum { SYS_INIT, // 初始化 SYS_LINKING, // 正在建立连接 SYS_RUNNING, // 正常运行 SYS_RECONNECT // 等待重连 } sys_state_t; sys_state_t g_state SYS_INIT; void sys_tick_1s(void) { switch (g_state) { case SYS_INIT: at_send(ATRST); if (at_wait_ok(2000)) g_state SYS_LINKING; break; case SYS_LINKING: if (net_connect()) // 内部完成配网和建连 g_state SYS_RUNNING; else g_state SYS_RECONNECT; break; case SYS_RUNNING: report_sensor(); // 周期上报 if (check_link_alive() 0) // 检测到TCP断开 g_state SYS_RECONNECT; break; case SYS_RECONNECT: if (g_reconnect_cnt 5) // 5秒后再重连 { g_reconnect_cnt 0; if (net_connect()) g_state SYS_RUNNING; } break; } }关键在于 RECONNECT 状态里加了计数延时避免网络不稳定时 1 秒内疯狂重试。at_wait_ok和net_connect内部要非阻塞别用那种死等返回的写法否则主循环卡死继电器本地控制也响应不了。6.2 断网不慌本地手动控制兜底智能家居最尴尬的场景是断网后开关都控制不了。我在 stm32 侧保留了一个本地按键扫描按键直接翻转继电器状态并且把翻转结果也发到云端。这样即使 WiFi 挂了、云端连不上本地物理开关依然能用网络恢复后云端状态会自动同步回来。这个兜底逻辑成本极低但体验差异非常大。6.3 从串口到云端的验证清单系统联调阶段按下面顺序逐层验证验证点操作预期结果stm32 串口输出电脑接 stm32 串口周期出现 AA 55 开头的完整数据帧esp8266 透传电脑 TCP Server 收包收到 stm32 发出的相同数据帧云端下发平台侧发 0x01 控制帧stm32 串口收到帧并翻转继电器断线恢复重启路由器5~10 秒内设备自动回到在线状态这套验证做完系统才算真正达到可长期运行的状态。我早期做这类系统时没做状态机路由器一重启就要手动给模组断电后来把重连逻辑补上整台设备运行几个月都不用碰。血泪经验总结成一句话智能家居项目里断线恢复比功能本身更重要功能上线只是开始。希望这套 stm32esp8266 的方案能帮到你少走一段弯路。本文还有配套的精品资源点击获取