简介:面向STM32单片机开发者、物联网项目设计人员及高校相关专业学生,这套基于STM32+MQTT+WiFi的智能家居系统资源提供了从设备端到云平台通信的完整设计与实现方案,涵盖消息订阅发布、设备联动控制、WiFi入网配置等核心环节。压缩包共288个文件,体量约12.11MB,以C语言源文件与H头文件为主,同时包含Keil工程配置、AXF/HEX固件输出,以及DOCX和PPTX格式的设计文档,既便于直接编译运行,也能支撑设计报告的撰写与答辩展示。目前已有2422人学习使用。全部源码经过测试校正,可稳定运行;读者可基于此快速搭建具备MQTT通信能力的智能家居原型,理解STM32外设初始化、协议接入与业务逻辑分层,尤其适合毕业设计、课程实训或电子竞赛的方案参考与二次开发,目录结构清晰。
1. 从一块开发板到一套能住的家:这套源码不是「点灯」,是「断线恢复」
夏天进家门那一刻最烦的不是热,而是空调要等三分钟才凉下来。如果能在下班的路上用手机把空调先打开,进门正好是舒服的温度——这就是「基于STM32+MQTT+WiFi技术的智能家居系统」在做的事。这套资源自带源码和配套文档,拿它当底子搭出来的不是演示用的点灯项目,而是一整条「传感器采集 → WiFi 模块上云 → 手机下发指令」的数据通路。真正难的部分不是「连上」,而是断网后自动恢复、消息不丢包、开关指令不乱串,这些正好是这套源码里已经处理好的事。适合两类人:一是准备做智能家居课题、差一个完整可演示工程的学生,二是已经会点 STM32 外设、想快速把手动设备接进 MQTT 云平台的开发者。
2. 架构拆解与选型理由:STM32、ESP8266、MQTT 各管哪一段
2.1 为什么是 STM32 加 ESP8266,而不是一片 ESP32 搞定
先回答一个最常见的问题:现在 ESP32 又便宜又能跑 WiFi,为什么还要绕一圈用 STM32 加 ESP8266?
常见做法是把「控制」和「联网」拆成两个芯片。STM32F103C8T6 管传感器读取、继电器开关、OLED 显示、按键扫描这些本地逻辑;ESP8266 只管一件事——把 MQTT 协议栈跑起来,做数据透传。这样分完工之后,本地的传感器采样时序不会被网络阻塞。比如 DHT11 的 1-Wire 时序要求微秒级延时,如果此时 ESP8266 正在重连 WiFi、串口缓冲区被 AT 回包塞满,主循环一卡,DHT11 的时序就错了。双芯片方案里这种干扰被物理隔离,顺序执行的问题只剩串口一条线,调起来好定位得多。
另一个理由是外设资源。ESP8266 跑 AT 固件时,GPIO 本来就不富余,还得兼顾网络协议栈,再挂一组继电器加传感器,引脚和中断就紧张了。STM32F103C8T6 有 3 个 USART、2 个 SPI、2 个 I2C,主频 72MHz,扩展空间大。这套源码里主控只用了 USART2 接 WiFi,PB0 接 DHT11,PB1/PB2 接继电器,OLED 走 PB6/PB7 的 I2C,还剩一批 ADC 通道和定时器通道,后面加烟雾传感器、红外模块都不用换主控。
2.2 MQTT 为什么比 HTTP 更适合家常控制
设备端要每秒报一次温湿度,用 HTTP 就得每秒启一次 TCP 连接,握手开销大,云平台也会把你当恶意请求。MQTT 是长连接加发布订阅模型,设备端只维持一条 TCP 连接,数据按主题投递,服务器端主动推送,控制指令的延迟在毫秒级,不用像 HTTP 轮询那样每 3 到 5 秒查一次。
MQTT 协议里三个 QoS 级别在这套系统里也分得很清楚。传感器上行的温湿度数据用 QoS 0,丢一帧无所谓,下一帧马上补上;继电器控制指令用 QoS 1,保证至少到达一次,避免「按了手机灯却没亮」。QoS 2 恰好一次在智能家居这种场景几乎用不上,TCP 重传加应用层幂等处理比它划算得多。
MQTT 还有两个为 IoT 准备的特性值得留意。一个是 LWT 遗嘱消息,设备异常掉线时 broker 会主动推一条预设消息,云端就知道设备离线了;另一个是 keepalive 心跳,设备每隔 30 到 60 秒发一次心跳保活,服务器超过两个周期没收到就判定掉线。这两个机制正是「断线恢复」这个核心需求的地基。
2.3 Topic 怎么设计才不会乱成一锅粥
MQTT 里 topic 是消息的路由地址,设计不好就会出现「手机发的指令被所有设备同时收到」的经典翻车现场。这套源码里的 topic 结构是三层:设备类型/设备ID/消息类型。
| topic 模式 | 方向 | 作用 |
|---|---|---|
smart_home/{id}/sensor/data | 设备上行 | 上报温湿度等传感器数据 |
smart_home/{id}/status | 设备上行 | 上报在线/离线状态 |
smart_home/{id}/rele/light | 设备下行 | 控制照明继电器开关 |
smart_home/{id}/rele/curtain | 设备下行 | 控制窗帘电机继电器 |
通配符方面,+匹配单层,#匹配多层。但我在代码里不建议直接在 STM32 端订阅带#的 topic,设备端只需关心自己这台设备的指令,订阅smart_home/{id}/rele/+就够了,避免把别人的指令也收进来。保留消息(retained)默认关掉,否则新设备上线会立刻收到 broker 里存的旧状态值,OLED 上闪现 5 分钟前的温度,容易误导调试。
3. 让 WiFi 模块开口说话:AT 指令初始化、透传与状态机调度
3.1 硬件接线和引脚分配
这套源码默认用的是 STM32F103C8T6 加 ESP8266,其中一个关键点是串口接法。ESP8266 的 RXD 接 STM32 的 PA2(USART2_TX),ESP8266 的 TXD 接 PA3(USART2_RX),交叉连接别接反了。WiFi 模块的 VCC 接 3.3V,GND 和 STM32 共地,CH_PD 引脚必须拉高,否则模块不工作。完整的引脚分配参考这张表:
| STM32 引脚 | 外设 | 说明 |
|---|---|---|
| PA2 / PA3 | USART2 | 接 ESP8266 RXD/TXD,波特率 115200 |
| PA9 / PA10 | USART1 | 预留,接调试串口或上位机 |
| PB0 | DHT11 数据脚 | 开漏输出,外部上拉 4.7k |
| PB1 / PB2 | 继电器 1/2 | 低电平触发,需光耦隔离 |
| PB6 / PB7 | I2C1 | 接 0.96 寸 OLED,地址 0x3C |
| PB12 | 按键输入 | 本地手动开关,下降沿触发 |
供电是这里最容易翻车的环节。ESP8266 在发射 WiFi 信号的瞬间电流能冲到 300mA 以上,直接接 STM32 板载的 3.3V 稳压输出,电压会瞬间跌落然后模块复位。我一般会单独用一块 AMS1117 给 WiFi 模块供电,输入侧并联一个 100uF 电解电容加一个 0.1uF 陶瓷电容,输出侧再并一个 470uF,实测能压住瞬态跌落。上电顺序也很重要:先给 WiFi 模块上电,等 1 秒再初始化串口,给模块足够的时间跑完内部固件启动。
3.2 ESP8266 AT 指令序列:从配网到 MQTT 收发
买回来的 ESP8266 出厂固件不一定带 MQTT 指令,这套源码对应的版本是支持 MQTT AT 指令的固件,如果没有,先刷一版乐鑫的 MQTT AT 固件再继续。下面是完整的 AT 指令初始化序列,可以直接放到串口助手或代码里逐条发送:
AT # 测试模块是否响应,返回 OK AT+CWMODE=1 # 设置为 Station 模式,连接外部路由器 AT+CWJAP="MyWiFi","pass123" # 连接 2.4G WiFi,注意不是 5G AT+MQTTUSERCFG=0,1,"dev001","user01","pass01",0,0,"" AT+MQTTCONN=0,"bemfa.com",9501,1 AT+MQTTSUB=0,"smart_home/dev001/sensor/data",1 AT+MQTTPUB=0,"smart_home/dev001/sensor/data","{\"temp\":26.5,\"humi\":60.2}",1,0逐条说明一下。AT+CWJAP里的 SSID 只能填 2.4G 频段的网络,ESP8266 不支持 5G,家里路由器开了「5G 优先」的话,模块会反复连接失败;AT+MQTTUSERCFG第一个参数 0 是 TCP 连接 ID,第二个参数 1 表示启用 MQTT,后面依次是 clientId、用户名、密码,最后三个参数依次是 cleanSession、keepalive 和 TLS 配置,平时保持0,0,""即可;AT+MQTTCONN最后的 1 表示启用自动重连;AT+MQTTSUB和AT+MQTTPUB最后的参数是 QoS,设备端订阅用 1 保证指令不丢,上行数据用 0 减少开销。
这里有个容易忽略的点:MQTT 的 clientId 在同一个云平台上不能和别的设备重复,否则后连接的设备会把前面的挤下线。我一般用dev_加设备 MAC 后四位做 clientId,保证唯一性。
3.3 STM32 端状态机:判断 WiFi 在线不能只查 OK
CMO 的坑在于,AT返回OK只代表串口通了,并不代表 WiFi 已经连上路由器。这套源码里用状态机管理连接流程,每 10ms 调度一次,核心代码结构是这样:
typedef enum { WIFI_IDLE, // 初始空闲 WIFI_CONNECTING, // 正在发送配网指令 WIFI_JOINED, // 已连上路由器 MQTT_CONNECTED // MQTT 连接已建立 } net_state_t; typedef struct { uint8_t rx_buf[256]; // 串口接收缓冲 uint8_t rx_len; net_state_t state; uint16_t timeout_cnt; // 超时计时器 } net_handle_t; void net_state_machine(net_handle_t *h) { switch (h->state) { case WIFI_IDLE: uart_send_str("AT+CWMODE=1\r\n"); h->state = WIFI_CONNECTING; h->timeout_cnt = 5000; break; case WIFI_CONNECTING: if (uart_find_ack("OK")) { uart_send_str("AT+CWJAP=\"MyWiFi\",\"pass123\"\r\n"); h->state = WIFI_JOINED; h->timeout_cnt = 12000; } else if (h->timeout_cnt == 0) { h->state = WIFI_IDLE; // 超时后回到初始态重试 } break; case WIFI_JOINED: if (uart_find_ack("WIFI GOT IP")) { uart_send_str("AT+MQTTCONN=0,\"bemfa.com\",9501,1\r\n"); h->state = MQTT_CONNECTED; h->timeout_cnt = 8000; } else if (h->timeout_cnt == 0) { // 连路由器失败,整体复位 ESP8266 gpio_reset_esp8266(); h->state = WIFI_IDLE; } break; // MQTT_CONNECTED 状态里处理心跳和消息转发 } }这段代码的状态切换依据是串口回包里的关键字。注意WIFI_JOINED状态必须等WIFI GOT IP,而不是只等OK——AT+CWJAP之后的OK只表示指令被接受了,等WIFI GOT IP才算真正拿到了局域网地址。超时参数建议给足:连路由器给 12 秒,MQTT 建立连接给 8 秒。WiFi 连接失败时不要只重发指令,我一般会直接拉低 ESP8266 的 RST 引脚做硬复位,让模块从固件层面重新初始化,重发指令的效果远不如硬复位来得干净。
4. 云端对接实战:从巴法云切入看订阅与发布的边角问题
4.1 云平台选型:巴法云还是自建 EMQX
STM32 端的数据要有个地方中转,常见的选择有三类:巴法云这类免费公共 MQTT 平台、tlink 这类教学平台、自己用 EMQX 搭建的私有服务器。三者的对比如下:
| 平台方案 | 接入成本 | 端口 | 稳定性和限制 | 适用场景 |
|---|---|---|---|---|
| 巴法云(bemfa) | 低,注册后拿主题当 clientId | 9501 | 有免费主题数量限制,个人项目够用 | 课程设计、个人原型 |
| tlink 云平台 | 低,文档齐全 | 1883 | 平台功能偏教学演示 | 教学演示、毕设展示 |
| 自建 EMQX | 高,需维护服务器 | 1883 | 完全可控,无限制 | 生产调试、数据私有化 |
这套源码的示例配置走的是巴法云,因为它免费、国内访问快、不需要自己维护服务器。巴法云的接入参数有固定套路:clientId 填云平台创建的主题名,用户名和密码填任意非空字符串即可。如果不想用公共平台,本地调试时用 Docker 起一个 EMQX 是最快的,一条命令:
docker run -d --name emqx -p 1883:1883 -p 18083:18083 emqx/emqx:5.0-p 1883:1883暴露 MQTT 协议端口,-p 18083:18083暴露 Dashboard 管理界面,浏览器打开服务器 IP 的 18083 端口就能看到连接状态。本地用 EMQX 调试有个明显优势:可以在管理界面实时看到每个 topic 的收发消息数,定位「设备没发」还是「服务器没推」这种问题比公共云平台直观得多。
4.2 上行数据组包:JSON 拼串的两个注意点
STM32 上报温湿度时,payload 要拼成 JSON 格式。见过不少人直接在sprintf里拼,结果浮点格式化把缓冲区写爆了。这套源码里的组包方式是这样:
char payload[64]; char temp_str[8], humi_str[8]; // 先转成字符串再拼 JSON,避免 sprintf 的浮点转换出错 snprintf(temp_str, sizeof(temp_str), "%.1f", temp); snprintf(humi_str, sizeof(humi_str), "%.1f", humi); snprintf(payload, sizeof(payload), "{\"temp\":%s,\"humi\":%s}", temp_str, humi_str); // 通过 ESP8266 发布 uart_send_str("AT+MQTTPUB=0,\"smart_home/dev001/sensor/data\","); uart_send_str(payload); uart_send_str(",1,0\r\n");两个注意点。第一,snprintf一律用带长度限制的版本,STM32F103C8T6 的 RAM 只有 20KB,无界sprintf遇到异常数据会把相邻变量区冲掉;第二,JSON 里的双引号需要转义,C 语言里写成\",别手滑把转义符丢了,不然云平台解析 JSON 直接报错。浮点转字符串这里先单独格式化再拼,是因为snprintf里直接处理两个浮点参数时,在部分编译器优化选项下可能出现精度错乱,分开转更稳。
4.3 下行指令解析:为什么用小字符串匹配而不是 JSON 解析库
云平台下发控制指令时,payload 通常是这样一条:{"rele/light":"on"}。STM32 收到后需要判断是哪个继电器、动作是开还是关。这套源码里的解析方式很朴素:
void handle_downlink_msg(uint8_t *payload) { // 按设备类型分开匹配,避免一条指令同时触发多个继电器 if (strstr((char*)payload, "\"rele/light\":\"on\"")) { relay_on(RELAY_LIGHT); } else if (strstr((char*)payload, "\"rele/light\":\"off\"")) { relay_off(RELAY_LIGHT); } else if (strstr((char*)payload, "\"rele/curtain\":\"on\"")) { relay_on(RELAY_CURTAIN); } }为什么不直接引入 cJSON 库?F103C8T6 只有 20KB RAM,cJSON 解析一棵嵌套 JSON 树要动态分配大量内存,频繁解析容易导致堆碎片,跑几天后malloc失败,设备直接死机。控制指令这种格式固定、层级简单的场景,字符串匹配足够可靠。但匹配时一定要带上完整边界:"rele/light":"on"前后都带引号,防止出现 payload 里有"rele/light":"once"时被strstr("on")误匹配的尴尬。
mqtt 消息长度也有隐性限制。AT+MQTTPUB的 payload 超过 ESP8266 AT 固件允许的最大长度(常见是 256 字节)时,指令会被静默丢弃,模块不会返回ERROR。所以上行数据里不要塞冗余字段,温度湿度加设备 ID 就够了,真要传长数据就拆包分两次发。
5. 避坑指南:五个必踩坑与解决记录
5.1 ESP8266 反复重启,串口打印乱码
现象:模块上电后串口输出一堆乱码,或者每隔几秒就自动重启一次,MQTT 一直连不上。
原因:供电不足。WiFi 发射瞬间电流冲到 300mA 以上,板载稳压芯片输出被拉垮,模块检测到电压跌落就触发复位。如果用的是 ST-Link 的 3.3V 输出,问题更严重,ST-Link 的供电能力也就 100mA 级别。
解决:把 ESP8266 的 VCC 从 STM32 板载 3.3V 挪到独立的 AMS1117 模块上,输入侧并 100uF 电解电容,输出侧并 470uF 电容。CH_PD 引脚用 10k 电阻上拉到 3.3V。改完之后用示波器量 VCC,WiFi 发射瞬间电压跌落不超过 0.2V 才算合格。
5.2 手机发指令设备没反应,但设备上报正常
现象:设备端上报温湿度云平台能看到,但手机 App 发控制指令,继电器没反应。
原因:topic 不匹配。常见的原因是发布和订阅的 topic 差了一个字符。比如云平台下发到smart_home/dev001/rele/light,设备端订阅写的是smart_home/dev001/rele/light/,末尾多一个斜杠,MQTT 把这两个当成完全不同的主题,消息直接被 broker 丢弃。另一个原因是大小写不一致,topic 匹配是严格的字节匹配,Rele和rele是两个不同的主题。
解决:把所有 topic 的定义统一收敛到头文件里的宏,不要散落在代码各处手写字符串。调试时先用 MQTT.fx 桌面客户端模拟订阅和发布,确认 topic 通了再让设备端接入,别一上来就怀疑代码逻辑。
5.3 OLED 屏白屏不显示,但系统运行正常
现象:程序跑起来了,串口有打印,继电器能动作,OLED 却一直白屏或没字符。
原因:I2C 地址不对或驱动库用错了协议。0.96 寸 OLED 的 I2C 地址只有 0x3C 和 0x3D 两种,背板上的地址电阻焊盘往左是 0x3C,往右是 0x3D,代码里写死 0x3C 时如果硬件的焊盘焊到了另一侧,通讯就建立不起来。还有的 OLED 用的是 SH1106 驱动芯片,而不是 SSD1306,初始化时序不一样,强行用 SSD1306 的库初始化会白屏。
解决:先写一个 I2C 扫描程序,把总线上所有地址打印出来,确认这个屏的实际地址再改代码。驱动芯片型号看着屏背面的丝印确认,或者看初始化时用它支持的对比度设置指令有没有正常返回。白屏时先测地址,再查驱动型号,不要上来就把罪过扣在 GPIO 配置头上。
5.4 DHT11 读数固定为 0 或 255
现象:OLED 上温度湿度一直显示 0.0 或 255,串口打印也是同样值。
原因:GPIO 模式配置错误。DHT11 的数据脚在空闲时是高电平,上电后主控要先拉低再释放,由 DHT11 拉低响应。如果 GPIO 配置成了推挽输出,释放总线时主控引脚仍然强输出高电平,DHT11 根本拉不低总线,时序就废了。另一个常见原因是读取时序里延时不精准,delay_us在无操作系统裸机上要关中断跑,不然 SysTick 被抢占后时序漂移,DHT11 返回的 40 位数据全部是错的。
解决:GPIO 配置成开漏输出模式,外部接 4.7k 上拉电阻到 3.3V。读取时序里,主控拉低 18ms 后释放,然后等待 DHT11 响应;每一位数据的读时序要用delay_us精确控制,延时函数写成关中断的忙等待版本。读三次取中位数的容错逻辑也补上,DHT11 这个传感器偶尔发一次错误数据是家常便饭。
5.5 断电重启后要等 40 秒才能恢复控制
现象:设备断电再上电,手机端要等半分钟以上才能重新控制继电器。
原因:重连流程里每步都在等超时。ESP8266 上电要 1 到 2 秒完成内部固件初始化,AT 指令逐条下发还要等返回,如果配网失败再等 12 秒超时,整体重连时间就变得不可接受。另一个原因是最初设计的代码里把 WiFi 重连和 MQTT 重连串在一起做,任何一个环节超时都会卡住后面的流程。
解决:把重连拆成两级。WiFi 已连接但 MQTT 断开时,只重发AT+MQTTCONN指令,不走完整初始化流程;WiFi 完全断开时,直接拉低 ESP8266 RST 引脚硬复位,然后走完整初始化。硬复位比软指令重连快而且干净。我在实际测试里把恢复时间从 40 秒压到了 8 秒左右,硬复位那一步是制胜关键。
6. 从「能跑」到「验证过」:MQTT.fx 回环调试与进阶自动化
6.1 端到端回环验证:先让桌面客户端把链路走一遍
很多人的调试习惯是直接改 STM32 代码加打印,但 MQTT 链路涉及设备端、broker、客户端三个节点,出问题时机在哪个节点并不直观。我一般会先在 PC 上用 MQTT.fx 把整条链路验证一遍,确认链路通了再让 STM32 介入。
| 验证项 | 操作步骤 | 预期结果 |
|---|---|---|
| 传感器上行 | MQTT.fx 订阅smart_home/dev001/sensor/# | 几秒内收到包含温湿度数据的 JSON |
| 继电器下行 | MQTT.fx 向smart_home/dev001/rele/light发布{"rele/light":"on"} | 继电器吸合,OLED 状态图标变化 |
| 断线自动恢复 | 拔掉 ESP8266 供电再插回 | 8 秒内重新上线,MQTT.fx 恢复收到数据 |
| topic 隔离验证 | 向smart_home/dev002/rele/light发布指令 | dev001 设备无任何反应 |
这套验证表做完,链路还没有问题,才轮到手机 App 接入。如果真的需要把手机 App 也验证上,用巴法云自带的 App 扫码绑定 topic 就行,不用先改 STM32 代码。
6.2 进阶玩法:温度告警、定时任务和本地兜底
链路通了之后,加自动化是小菜一碟。我在这套源码基础上做过三个实用的扩展。第一个是温度告警:手机下发一个阈值到smart_home/dev001/config/temp_th,STM32 把阈值存进 Flash,每读一次 DHT11 就跟阈值比较,超过后立即发布一条{"alarm":"temp_high"}到告警主题,云平台配置规则引擎推送到企业 IM 群。第二个是定时任务:云端做定时有几个限制,公共平台通常不带定时功能,我就用 STM32 内部的 RTC 做本地定时,把一组{hour, minute, action}结构体数组存在 Flash 里,主循环每秒检查一次,实现工作日早上 7 点开窗帘这种自定义逻辑。第三个是本地兜底:WiFi 断了但家人还是能手动按按键开关灯,按键处理要加软件消抖加互斥标志,避免「按一下灯闪两下」的情况。
说回最初的动机。这套源码让我最有好感的一点,是它把「断线恢复」这件事当成一等公民在设计,而不是连上了就万事大吉。从那以后我每次调 MQTT 链路都强制走一遍「串口看 AT 回包 → MQTT.fx 验证 topic → 手机端再接入」的顺序,这个习惯帮我省下了大量排查时间,也希望帮到你。
本文还有配套的精品资源,点击获取