
简介基于STM32与ESP8266的智能家居系统项目源码面向嵌入式开发者和物联网工程学习者解决家庭环境实时采集、远程控制与智能化联动需求。工程以STM32为控制核心借助ESP8266 Wi-Fi模块完成数据透传支持温湿度、光照、烟雾等传感器接入并可通过微信小程序实时查看室内状态、远程操控智能灯泡或插座同时融合简单机器学习思路为设备自动调节提供参考适用于智能家居、智慧办公等多类场景。压缩包内共127个文件整体约441KB主要包含46个C头文件、43个C源文件、2个Keil工程配置文件uvprojx/uvoptx覆盖STM32各外设驱动及MQTT网络协议层另有6个JSON、6个JS、3个WXSS和2个WXML构成完整微信小程序前端代码外加若干PNG图片和说明文档便于对照调试。目前已有244人学习下载适合需要完整源码示例、希望快速上手STM32和ESP8266协同开发的读者借鉴。对课程设计、毕业设计或物联网产品原型搭建均有直接参考价值整体目录结构清晰便于按模块检索与二次开发。1. 为什么智能家居主控选 stm32 esp8266 而不是一颗 SoC把老房子的机械开关改成手机控制是很多人入手嵌入式的第一个目标。淘宝上 stm32 最小系统板和 NodeMCU 加起来不到 40 元资料却非常散有人用 stm32 直接跑 AT 指令有人给 esp8266 刷 Arduino 固件做 MQTT还有人干脆用 ESP32 单芯片。真正上手后你会发现问题从来不是“哪个芯片更强”而是两颗芯片的分工和中间那根串口线怎么设计。stm32 负责灯、窗帘电机、继电器这些需要毫秒级响应的被控对象esp8266 负责不太可靠但必须存在的 WiFi 连接两者通过串口交换一份自定义帧——这套组合在可靠性和成本之间平衡得相当好。本文面向正在做毕业设计、或者想把手头设备真的跑起来的工程师我会把链路层协议、固件结构、电源和 Flash 寿命这几个最容易埋雷的地方讲透。2. 串口链路是命根子stm32 与 esp8266 的通信协议与引脚设计2.1 用 USART 而非 SPI接口选型的现实理由esp8266 其实有 SPI 接口但官方 AT 固件的产品化路径是串口透传SDK 模式需要自己维护 TCP/IP 协议栈对大多数项目不值得。USART 在 stm32 上中断粒度可控协议分析仪、串口调试助手随手就能接上出问题时的定位成本最低。硬件连接只有三根线交叉对应即可共地不可省。stm32 引脚esp8266 引脚方向说明PA9USART1_TXRXDGPIO3stm32 发送数据给 esp8266PA10USART1_RXTXDGPIO1esp8266 返回数据给 stm32GNDGND必须共地否则电平参考不一致提示stm32 和 esp8266 都是 3.3V 逻辑可以直接相连。如果你的 stm32 板子上有 5V 电平的串口芯片先确认跳线帽是否把 MCU 的 USART 引脚引出了。波特率选 115200这是 esp8266 出厂固件的默认值。数据位 8、无校验、停止位 1即常见的 8N1。不要盲目拉到 460800串口线质量、干扰和双方时钟误差都会放大对智能家居这种传输频率低、帧长小的场景115200 足够支撑几十路设备的命令吞吐。2.2 AT 指令模式下的裸跑流程如果你手头的 esp8266 还是出厂 AT 固件先用串口调试助手验证链路是最快的办法。上电后手动发送这几条指令AT ATCWMODE1 ATCWJAP你的WiFi名,你的密码 ATCIPMUX0 ATCIPSTARTTCP,192.168.1.100,1883 ATCIPSEND20参数含义ATCWMODE1把模块设为 station 模式只连接路由器而不创建热点ATCIPMUX0是单连接模式简化指令交互ATCIPSTART后面是协议、服务器 IP 和端口ATCIPSEND后跟发送长度此时模块进入透传之后发送的内容会直接交给 TCP 连接。注意不同固件版本对ATCIPSEND的返回值格式有差异常见的是提示符也可能直接回SEND OK。如果你的项目要跑 MQTT用 AT 指令去拼 TCP 包会很痛苦因为要手动计算 MQTT 协议的剩余长度字节所以工厂化方案更多是给 esp8266 刷 Arduino 固件让它的 SDK 处理这些细节。AT 模式的价值在于它是最短路径能帮你确认串口引脚、电平、波特率这三个基础项是否正常。2.3 自研帧协议与 stm32 串口发送代码即使 esp8266 跑 Arduino 固件stm32 和它之间仍然需要一份约定明确的帧格式。常见做法是帧头 2 字节、命令字 1 字节、长度 1 字节、数据区最多 32 字节、CRC 校验 1 字节。帧头选AA 55这两个不常用的连续字节降低误触发概率。#define FRAME_HEADER0 0xAA #define FRAME_HEADER1 0x55 #define MAX_DATA_LEN 32 uint8_t crc8(const uint8_t *buf, uint8_t len) { uint8_t crc 0; while (len--) { crc ^ *buf; for (int i 0; i 8; i) { crc (crc 0x80) ? (crc 1) ^ 0x07 : (crc 1); } } return crc; } void app_send_frame(uint8_t cmd, uint8_t *data, uint8_t len) { uint8_t frame[4 MAX_DATA_LEN 1]; frame[0] FRAME_HEADER0; frame[1] FRAME_HEADER1; frame[2] cmd; frame[3] len; memcpy(frame[4], data, len); frame[4 len] crc8(frame, 4 len); HAL_UART_Transmit(huart1, frame, 5 len, 100); }这段代码选 CRC-8 多项式0x07校验范围从头到数据区长度本身也被保护。数据区限制 32 字节是因为 115200 波特率下 40 字节一帧耗时约 3.5ms即使 TCP 重传或 WiFi 抖动也不会对 stm32 的主循环造成明显阻塞。HAL_UART_Transmit最后一个参数是超时时间 100ms帧长不超过 38 字节时实际耗时远小于它超时只是兜底。注意如果你在 AT 固件模式下不要把这条指令发给已经进入透传的模块AA 55会被原样解释为业务数据。透传模式下不能用帧头做唤醒必须靠协议内部约定。3. ESP8266 端固件Arduino IDE 下的 WiFi 连接、MQTT 与 JSON 解析3.1 Arduino IDE 开发环境与 NodeMCU 引脚选择给 esp8266 刷 Arduino 固件是目前维护成本最低的路线社区库生态比 AT 指令完整得多。在 Arduino IDE 的“文件 - 首选项 - 附加开发板管理器网址”中填入 esp8266 社区提供的 JSON 地址然后从开发板管理器安装 esp8266 平台选 NodeMCU 1.0 或 Generic ESP8266 Module 即可编译烧录。开发板上的丝印 D0~D8 和 GPIO 编号不是一回事接 stm32 串口时优先选不影响下载和启动的引脚。开发板丝印GPIO 编号用途建议D4GPIO2板载 LED可做状态指示D6GPIO12接 stm32 TXDesp8266 接收D7GPIO13接 stm32 RXDesp8266 发送D8GPIO15需外部下拉默认不适合做输入用 D6/D7 做 UART 是因为它们不涉及上电启动电平也不依赖外部下拉电阻。GPIO1 和 GPIO3 虽然也是 UART 引脚但 GPIO1 同时也是 TXD容易在启动时输出日志干扰通信。3.2 用 PubSubClient 实现 MQTT 通信下一个典型工程做法是让 esp8266 直接做 MQTT 客户端订阅云端或局域网服务器下发的主题然后把解析后的命令通过 UART 发给 stm32。核心依赖是PubSubClient和ArduinoJson两个库前者处理 MQTT 报文后者解析 JSON 格式的控制指令。#include ESP8266WiFi.h #include PubSubClient.h #include ArduinoJson.h const char* ssid your_ssid; const char* password your_password; const char* mqtt_host 192.168.1.50; const int mqtt_port 1883; const char* topic_sub home/room1/relay/cmd; WiFiClient espClient; PubSubClient mqtt(espClient); void callback(char* topic, byte* payload, unsigned int length) { StaticJsonDocument128 doc; deserializeJson(doc, payload); int relay_id doc[relay] | -1; bool state doc[state] | false; if (relay_id 0) return; uint8_t frame[7]; frame[0] 0xAA; frame[1] 0x55; frame[2] 0x01; // 命令字控制继电器 frame[3] 0x02; // 数据长度 frame[4] relay_id; frame[5] state ? 1 : 0; frame[6] crc8(frame, 6); // 与 stm32 端相同的 CRC 算法 Serial.write(frame, 7); } void setup() { Serial.begin(115200); WiFi.begin(ssid, password); while (WiFi.status() ! WL_CONNECTED) { delay(200); } mqtt.setServer(mqtt_host, mqtt_port); mqtt.setCallback(callback); }这里不用StaticJsonDocument128以外的动态内存是因为 esp8266 可用 RAM 只有几十 KB堆碎片的后果是随机重启。doc[relay] | -1的写法在键不存在时返回 -1避免解析空消息时误操作设备。Serial.write发的是二进制帧不是可读字符串这样 stm32 端不需要做 ASCII 到十六进制的转换。3.3 处理掉线与重连家用路由器的 DHCP 租约、AP 漫游、信道拥挤都会导致 esp8266 掉线。不处理重连智能家居系统会越用越“呆”。常见做法是在loop()里维护一个非阻塞心跳检测掉线后先关掉 MQTT 连接再主动重连 WiFi而不是让库内部的阻塞重试占住 CPU。unsigned long lastReconnect 0; const unsigned long reconnectInterval 5000; void loop() { if (!mqtt.connected()) { unsigned long now millis(); if (now - lastReconnect reconnectInterval) { lastReconnect now; mqtt.connect(esp8266_room1); if (mqtt.connected()) { mqtt.subscribe(topic_sub); } } } else { mqtt.loop(); } }mqtt.connect的第一个参数是客户端 ID同一时刻局域网里不能有两个相同 ID 的设备在线否则服务端会互踢。5 秒的重试间隔是经验值太短会导致连续断连风暴太长则会让 stm32 误以为 esp8266 已死。注意mqtt.loop()必须放在非阻塞分支里它负责收包、分发回调、发 PINGREQ 心跳默认 keepalive 是 60 秒。4. stm32 端多任务与命令解析状态机、环形缓冲与看门狗4.1 串口接收的三层结构stm32 接收 esp8266 的数据不要在主循环里用HAL_UART_Receive死等。串口数据到来是不确定的主循环可能正在扫描按键或刷新 OLED。工程做法是分三层中断收字节、环形缓冲区缓存、上层状态机解析。每个字节进中断的开销在 115200 波特率下大约是 87 微秒一次stm32 主频 72MHz 时中断服务程序只做入队操作不会丢数据。#define RINGBUF_SIZE 128 static volatile uint8_t ringbuf[RINGBUF_SIZE]; static volatile uint16_t head 0, tail 0; void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { uint16_t next (head 1) % RINGBUF_SIZE; if (next ! tail) { ringbuf[head] rx_byte; head next; } HAL_UART_Receive_IT(huart1, rx_byte, 1); }环形缓冲区里尾指针没有被主循环消费时不能覆盖旧数据这行判断防止缓冲区溢出导致帧错位。rx_byte是全局变量每次重新调用HAL_UART_Receive_IT前必须先赋给缓冲否则新数据会覆盖旧值。这个结构吃内存很小128 字节足够容纳几十帧叠加的极端情况。4.2 帧解析状态机的实现从环形缓冲区取一个字节喂给状态机状态机按帧结构逐字节前进。不要用阻塞式查找帧头那样碰到半截帧时会卡住整个循环。typedef enum { ST_HEADER0, ST_HEADER1, ST_CMD, ST_LEN, ST_DATA, ST_CRC } ParserState; uint8_t parse_frame[40]; static ParserState st ST_HEADER0; static uint8_t data_idx 0; static uint8_t frame_len 0; void parser_feed(uint8_t b) { switch (st) { case ST_HEADER0: if (b 0xAA) st ST_HEADER1; break; case ST_HEADER1: st (b 0x55) ? ST_CMD : ST_HEADER0; break; case ST_CMD: parse_frame[2] b; st ST_LEN; break; case ST_LEN: frame_len b; data_idx 0; st (frame_len 32) ? ST_HEADER0 : ST_DATA; break; case ST_DATA: parse_frame[4 data_idx] b; if (data_idx frame_len) st ST_CRC; break; case ST_CRC: if (crc8(parse_frame, 4 frame_len) b) { dispatch_frame(parse_frame[2]); } st ST_HEADER0; break; default: st ST_HEADER0; break; } }状态机的价值在于不完整帧到来时不会误业务逻辑不管是一帧被分包还是多帧粘连解析器都能正确收敛。dispatch_frame里再根据命令字执行继电器控制、LED 亮度调节等动作。如果帧长度字段异常大直接回到帧头状态防止恶意或损坏数据拖垮系统。4.3 多任务的两种路线裸机状态机与 FreeRTOS很多入门者会问要不要上 FreeRTOS。答案取决于你的外设数量如果只有串口、几个 GPIO、一个定时器裸机主循环加状态机完全够用代码更容易背下来也更好排查如果同时要驱动屏幕、SD 卡、多路传感器采集再叠加 WiFi 命令解析才值得引入 RTOS 做任务切分。裸机方案的关键是主循环不能阻塞。所有延时用定时器计数替代比如用HAL_GetTick()判断某个继电器得电是否超过 5 秒。需要同时维护多个时间片时可以用结构体数组存放任务的状态和到期时间typedef struct { uint32_t interval_ms; uint32_t last_run; void (*callback)(void); } SoftTimer; SoftTimer timers[] { {100, 0, scan_keyboard}, {500, 0, update_oled}, {1000, 0, check_wifi_heartbeat} };主循环里遍历这个数组到期则执行。这种写法比在中断里处理业务干净得多也不会因为某个回调执行时间太长而干扰串口接收中断。上 FreeRTOS 后反而要处理任务栈大小、队列深度和优先级反转的问题对一个小型灯控项目性价比不高。5. 智能家居项目的三个隐藏难点电源纹波、Flash 磨损与协议粘包5.1 esp8266 发射瞬间的电源跌落esp8266 在 WiFi 发射的瞬间功耗接近 300mA如果供电走的是 stm32 板子上的 3.3V LDO瞬间压降会让模块重启表现为物联网系统“间歇性离线”。更隐蔽的表现是 stm32 的 ADC 采集值在 esp8266 发包时跳变。解决方法是给 esp8266 的供电脚单独加储能电容并尽量用独立稳压器。元器件参数位置电解电容470uF / 6.3Vesp8266 供电脚附近滤低频跌落陶瓷电容100nF芯片 VCC 引脚正下方滤高频噪声磁珠600R100MHz串在 esp8266 供电回路里隔离高频注意不要只靠面包板上的长跳线供电线电感在瞬态电流冲击下会产生压降。PCB 上电容要贴近模块引脚线越短效果越明显。5.2 AT 指令写参数与 Flash 寿命esp8266 的擦写寿命在 10 万次左右这数值看着高但如果代码里每次重启都执行ATCWJAP或频繁调用EEPROM.commit()一年内就能写穿。Arduino 固件中EEPROM库底层是 flash 映射每次commit()都是整扇区擦除。正确做法是只在配置变更时写入上电先读读不到才走初始化配置流程。stm32 内部的 Flash 写入寿命只有约 1 万次如果用 Flash 存用户设置必须做磨损均衡或干脆改用外部 EEPROM。很多智能家居项目里“设置丢失”的 bug 就来自这里表现是重启后继电器状态回退到默认值。5.3 粘包问题与超时重传TCP 层会把多个串口帧合并到一个段里发过来stm32 的串口中断看到的现象是“一收一大坨”。帧头校验能解决同步问题但真实工程里还需要考虑更复杂的情况WiFi 信号差时 TCP 重传导致 stm32 收到重复帧esp8266 的串口缓冲溢出时帧被截断两边波特率偏差大时停止位采样出错。应对方案分三层。第一层是协议层做序列号stm32 收到同序列号的帧直接丢弃避免重复执行继电器动作。第二层是超时机制esp8266 发出命令后 200ms 内没收到确认帧就重发最多重试 2 次。第三层是恢复机制stm32 连续 3 帧 CRC 校验失败就主动清空 esp8266 的串口发送队列有时能解决模块内部缓冲死锁。6. 用逻辑分析仪验证 stm32 与 esp8266 的帧格式与实时性6.1 抓取串口波形的操作步骤接好硬件后不要急着打开串口助手看解码先用逻辑分析仪看物理层波形。把逻辑分析仪的通道 0 接到 stm32 的 PA9TX通道 1 接到 PA10RXGND 与系统共地。采样率设置在 1MHz 以上115200 波特率下每个 bit 约有 8.7 个采样点足够判断边沿位置。在 PulseView 或 Saleae Logic 中新建 UART 解码器波特率 115200、数据位 8、无校验位、停止位 1。正常波形应该看到每帧起始位是低电平结束位是高电平字节间隙至少一个位宽。如果字节间隙出现额外低电平尖峰基本可以判断是接线干扰或 TX/RX 接反而不是协议问题。6.2 从波形判断时序是否达标抓一帧 stm32 发送的继电器控制帧量出第一个字节起始位到最后一个字节停止位的总时长。38 字节在 115200 波特率下理论耗时约 3.3ms38 字节 × 10 bit / 115200实际加上软件延时应在 4ms 内。如果超过 10ms说明你用了阻塞式发送且中间被更高优先级中断打断在靠状态机驱动的系统里这就是隐患。用分析仪同时看 RX 和 TX 两条线还能发现 esp8266 是否在 stm32 发送后立刻回 ACK。如果 ACK 帧在 1ms 内出现说明 esp8266 的串口中断处理及时如果延迟到几十毫秒需要考虑它在忙于处理 WiFi 协议时丢字节的可能。把逻辑分析仪的阈值调到 1.65V 附近再观察边沿能更清楚地区分数据位和噪声毛刺这是排查高波特率下通信不稳最直接的手段。本文还有配套的精品资源点击获取