STM32+ESP8266物联网系统实战:从硬件连接到MQTT上云全解析
2026/9/16 2:07:20 网站建设 项目流程

简介:基于STM32与ESP8266的智能物联网家居系统源码包,面向嵌入式开发者和物联网方向学习者,适合用于智能家居原型搭建、课程设计或毕业设计参考。项目以STM32为主控芯片,结合ESP8266 Wi-Fi模块,涵盖GPIO控制、ADC采集、定时器、串口通信、网络数据收发等关键开发环节,能够帮助读者理解传感器数据如何采集、处理并上传至云端或APP的完整链路。压缩包共2000个文件,约40.72MB,文件类型以C、H源码为主,配合JS、JSON、MD等文件,大致覆盖固件逻辑、网页/小程序前端交互、设备配置参数和项目说明文档;其中数量较多的JS文件也提示资源内包含较完整的上位机或Web端界面,适合前后端对照学习。除源码外,部分项目还附带设计报告与原理图,便于对照硬件连接进行系统性调试。该资源已有1862人学习下载,对于想快速入门STM32+ESP8266物联网开发、搭建智能家居小系统的读者来说,是一份内容充实、结构较完整的参考资料。

1. 一款“毕业设计标配”物联网系统,为什么先想清楚 ESP8266 的角色

拿到这个标题时,第一反应是又一个“STM32+ESP8266+云平台”的三件套。这类项目在STM32毕业设计和物联网入门里出现频率极高,但多数人把它做成了“两块板子用杜邦线连起来,串口助手看到数据就算成功”。真正的难点不在 GPIO 点灯,也不在 AT 指令配网,而在于把系统的分层想清楚:STM32 到底管什么,ESP8266 管什么,两者之间用什么协议说话,断网了怎么处理。ESP8266 在这类系统里最容易被当成一个“无线串口”——这是能跑通的最低理解,但如果你要在答辩或实际环境里让系统稳定工作,就得把它放到网络协议栈的位置上看。这篇博文从硬件连接、联网方案、Keil 工程骨架一直讲到调试三板斧,照着一套我常用的实现路径,让你能复现一个可上报、可控制、可断线重连的物联网家居系统。

2. 硬件链路:STM32 做什么、ESP8266 做什么,以及连接原理图的 3 个关键细节

2.1 系统拆解:传感器采集、控制执行、无线回传的职责划分

一个典型的智能家居节点,硬件层面可以拆成四个部分:传感器、执行器、主控 MCU、无线模块。传感器负责采集环境数据,执行器(继电器、蜂鸣器、电机)接受指令动作,STM32 负责把这些外设的逻辑串起来,ESP8266 负责让设备“上线”。我习惯把 STM32 定位成“现场控制单元”,ESP8266 定位成“网络接入单元”。为什么不让 ESP8266 直接把传感器读了呢?这其实是很多教程绕开的问题。

ESP8266 的 GPIO 确实能读 DHT11 这种单总线传感器,也能驱动继电器,但它在复杂逻辑上并不擅长。一旦你需要在本地做多传感器轮询、按键消抖、多路继电器联动,还要保证毫秒级响应,ESP8266 的 SDK 开发体验和调试手段都不如 STM32 顺手。反过来,STM32 又没有 IP 协议栈,无法直接上云。所以两者是一对互补关系:STM32 对“现场”负责,ESP8266 对“网络”负责。这个分工明确了,后续的协议设计才有依据。

模块职责接口方式典型器件
传感器采集温湿度、光照、烟雾等GPIO / ADC / I2CDHT11、BH1750、MQ-2
执行器通断电源、开关门窗GPIO + 驱动电路继电器、ULN2003
STM32轮询、控制、组帧USART / SPI / I2CSTM32F103C8T6
ESP8266TCP/IP、MQTT、上云UART / SPI / I2CESP-01S、NodeMCU

执行器驱动这块,有个常见误解是直接用 STM32 的 GPIO 推继电器。继电器线圈是感性负载,关断瞬间会产生反向电动势,轻则干扰系统复位,重则击穿引脚。正确做法是 GPIO 输出到三极管或 ULN2003,由外部电源驱动继电器线圈,同时在线圈两端并联续流二极管。ULN2003 是一个 7 路达林顿管阵列,可以同时驱动多路继电器,也兼容 3.3V 逻辑电平输入,在物联网项目里很实用。选它不是因为“单片机 IO 不够”,而是因为隔离和驱动能力更可靠。

2.2 ESP8266 与 STM32 的连接原理图:串口通信与电平匹配

ESP8266 与 STM32 之间最常用的连接方式就是 UART。STM32F103 的 USART1 和 USART2 都可以,但建议把 USART1 留给烧录调试,USART2 给 ESP8266,因为很多 STM32 开发板的 USB 转串口芯片直接挂在 USART1 上,调试输出和 ESP8266 通信最好不要抢同一个外设。典型接线是:STM32 的 PA2(USART2_TX)接 ESP8266 的 RXD,PA3(USART2_RX)接 ESP8266 的 TXD,GND 共地。

电平匹配务必重视。STM32F103 的 GPIO 是 3.3V 逻辑,ESP8266 也是 3.3V 逻辑,电平本身是兼容的。但如果你用的是 5V 供电的 STM32 开发板,板上可能带电平转换电路,这没问题;如果自己画板,就得确认 STM32 的 VDD 是 3.3V。很多 ESP-01S 模块的引脚没有做电平转换,强行接 5V TTL 电平会烧模块。

在 ESP8266 侧,还需要注意 CH_PD(或 EN)引脚必须拉高,模块才会正常工作。ESP-01S 的 GPIO0 在正常运行时要悬空或拉高,拉低会进入烧录模式。这个细节没有处理好的话,模块表现为“上电后指示灯亮但不响应 AT 指令”,非常容易误导排查方向。

2.2.1 一键下载电路:BOOT 与复位时序

如果在调试阶段你需要反复给 STM32 烧写新固件,建议直接用 ST-Link 的 SWD 接口,占用 PA13/PA14,不冲突。手动按复位键进 BOOTLOADER 的方式虽然可行,但在项目迭代后期会让人抓狂。更省事的是在 STM32 的 BOOT0 引脚上接一个跳线或按键,配合一键下载电路,实现“按 BOOT -> 复位 -> 进下载模式”的流程。

ESP8266 的固件烧录同理:GPIO0 拉低后重新上电,才能进入 UART 下载模式。如果用的是 NodeMCU 开发板,板载 USB 转串口芯片已经处理好了这些时序,直接用 USB 烧录即可。如果是 ESP-01S 裸模块,需要自己搭一个下载电路:CH_PD 接 3.3V,GPIO0 接一个按键到 GND,上电前按住,上电后松开,然后就可以用串口工具烧写固件了。

2.3 电源设计:3 个让系统稳定运行 24 小时的关键点

电源是整个系统里最容易被低估的部分。ESP8266 在 Wi-Fi 发射瞬间的峰值电流可达 300mA 左右,STM32F103 全速运行加上外设也就几十毫安,两者共用一个 3.3V LDO 时,如果 LDO 的电流输出能力不足,电压会被瞬间拉低,导致 ESP8266 反复重启或者 STM32 死机。

第一个关键点是分路供电:ESP8266 单独用一颗 AMS1117-3.3 或更低压差的 LDO,STM32 用另一路,不要共用一颗 LDO。输入电源用 5V/2A 的 USB 适配器比较稳妥,继电器直接由 5V 驱动,STM32 和 ESP8266 各自降压到 3.3V。

第二个关键点是去耦电容。ESP8266 的 VCC 和 GND 之间要并一个 10uF 电解电容和一个 0.1uF 陶瓷电容,距离模块引脚越近越好。这颗 10uF 电容能吸收发射瞬间的电流尖峰,减少电压跌落幅度。STM32 的每个 VDD 引脚附近都放 0.1uF 电容,这是 STM32 数据手册明确要求的,但很多自制板会省掉,导致系统在继电器动作时复位。

第三个关键点是地线布局。如果继电器和 ESP8266 用同一个 GND 回路,继电器吸合瞬间的大电流会在地线上产生压差,干扰 UART 通信。正确做法是功率地(继电器、电机)和信号地(STM32、ESP8266)单点连接,或者在继电器驱动电路和 MCU 之间加光耦隔离。光耦隔离在毕业设计里不是必须的,但如果你发现继电器一动作,串口就收到乱码,优先检查这一项。

3. 联网方案选型:AT 指令透传与 MQTT SDK 直连怎么选

3.1 方案 A:AT 指令透传,STM32 通过串口裸收发

最常见的联网做法是给 ESP8266 刷一个 AT 固件,STM32 通过串口发 AT 指令去操作它。这种方案的优点是逻辑简单:ESP8266 只是一个透明管道,所有网络行为都由 STM32 串口指令触发。缺点也很明显:ESP8266 的协议栈在运行,但 STM32 并不知道网络断开这件事,需要自己做心跳检测。

一个从 AT 固件进入透传模式的指令序列大概是这样:

AT+RST # 复位模块 AT+CWMODE=1 # 设为 Station 模式 AT+CWJAP="SSID","PASSWORD" # 连接家庭 Wi-Fi AT+MQTTUSERCFG=0,1,"clientId","username","password",0,0,"" AT+MQTTCONN=0,"broker.emqx.io",1883,1 AT+MQTTSUB=0,"home/bedroom/cmd",1 AT+MQTTPUB=0,"home/bedroom/data","{\"temp\":26.5}",1

逐条拆解一下:AT+CWMODE=1只做 Station,不开 AP,避免模块既连路由器又开热点导致功耗翻倍;AT+MQTTUSERCFG里的第 2 个参数1表示使能 MQTT 的认证信息,第 4、5 个参数是设备连接到云平台时使用的身份标识;AT+MQTTCONN里的0是 MQTT 连接号,因为 ESP8266 AT 固件 2020 年之后的版本支持多路 MQTT,但通常一路就够用了;最后的1是 keepalive 周期,单位是秒,默认 120 秒,这里设成 1 秒是为了更快发现断线,但代价是每秒钟都会有一次心跳包,流量消耗会增加。

这种方案用起来很顺手,但有一个陷阱:通过AT+MQTTPUB发送的 JSON 字符串中,如果包含中文字符或者特殊符号,转义会非常痛苦。数据域里只放 ASCII 字符,对于温湿度、开关状态来说完全够用。心跳包的问题同样需要注意:如果 STM32 在 60 秒内没有主动上报数据,ESP8266 和云平台之间的 MQTT 连接可能会被服务端判定为死链而断开,所以 STM32 侧要写一个定时器,周期性上报或者发送空的心跳消息。

3.2 方案 B:ESP8266 刷入 NodeMCU 固件或 MQTT 固件,STM32 只发 JSON

另一个常见做法是给 ESP8266 刷 NodeMCU 固件,用 Lua 脚本直接跑 MQTT 客户端,STM32 只需要在串口上发送预设好的命令帧,ESP8266 解析后自动完成上报。这种方案把 Wi-Fi 和 MQTT 的逻辑全部放在了 ESP8266 侧,STM32 的代码量大幅减少,调试也更快。

用 NodeMCU 的mqtt模块实现订阅的逻辑非常短:

wifi.setmode(wifi.STATION) wifi.sta.config("SSID", "PASSWORD") m = mqtt.Client("bedroom_node", 120, "user", "pass") m:on("connect", function(client) client:subscribe("/home/bedroom/cmd", 1, function(conn) print("subscribed") end) end) m:on("message", function(client, topic, payload) print(topic .. ": " .. payload) uart.write(0, "CMD:" .. payload .. "\r\n") -- 转发给 STM32 end) m:connect("broker.emqx.io", 1883, 0)

这段脚本的核心逻辑是:ESP8266 连接 Wi-Fi 后主动建立 MQTT 连接,订阅控制主题;当云端下发指令时,message回调被触发,ESP8266 将原始 payload 经过uart.write原封不动地转发给 STM32。好处是 STM32 不需要关心任何网络状态,只需要解析CMD:xxx这种串口帧。

这个方案的风险点是 Lua 脚本和固件版本的匹配。NodeMCU 固件是按需编译的,官方固件生成器上可以勾选需要的模块,例如mqttgpiouart等。如果你下载了一个精简固件,缺少mqtt模块,运行时会报attempt to index global 'mqtt' (a nil value)。烧录时多确认一下固件是否包含了 mqtt 模块,否则排查起来很耗时。另一个问题是 ESP8266 在异常复位后,Lua 脚本默认从init.lua启动,如果脚本里没有做断线重连,设备会一直停留在离线状态,直到手动重启。

3.3 主题规划与 JSON 字段设计:让云端和设备层不“鸡同鸭讲”

无论选哪种方案,MQTT 的主题(Topic)规划都需要提前想清楚。很多人在项目里图省事,所有设备都用一个test/topic,设备一多就分不清谁是谁。我常用的主题结构是:

home/{room}/{node}/{property}

比如home/bedroom/node01/temperature表示卧室 1 号节点的温度数据,home/bedroom/node01/relay1表示卧室 1 号节点的第一路继电器控制。有人会问,属性拆这么细,主题数量会不会爆炸?对于家庭节点数量不超过十个的场景,完全没有问题,而且这种结构在云平台的规则引擎里做数据流转非常自然。

JSON 字段设计也遵循“小步快跑”的原则。一条上报数据长这样:

{ "node": "bedroom_01", "type": "data", "temp": 26.5, "humi": 60.1, "light": 320, "relay1": 0, "ts": 1710000000 }

ts是 Unix 时间戳,由 ESP8266 在转发前补上,或者由云平台规则引擎打上。在数据入库之后做趋势分析时,时间戳是最重要的字段。另一个实用字段是relay1,把执行器的状态也嵌入到上报数据里,这样云端 APP 展示设备状态时,不需要再单独查询一次控制记录。

控制下发的消息则更简洁:

{ "cmd": "set_relay", "ch": 1, "val": 1 }

这种成对设计的好处是:设备端处理逻辑统一,上报上行数据、执行下行指令时,不需要为每种业务写一套独立的解析器。STM32 端拿到CMD:{"cmd":"set_relay","ch":1,"val":1},只需要拆 JSON 就够了,而不需要自己去定义复杂的二进制协议。

4. 用 Keil5 搭建 STM32 工程,跑通传感器采集与串口转发

4.1 工程配置:标准库还是 HAL 库,怎么选

STM32 的开发环境是 Keil5,这个问题上标准库和 HAL 库的选择直接影响后续的编码工作量。对于这种以串口和 GPIO 为主的项目,标准库更直接,代码量少,调试时单步跟踪也更清晰;HAL 库的好处是 CubeMX 生成代码,引脚分配方便,不容易漏配时钟。我的建议是,想一次点亮、把精力放在系统逻辑上就选 HAL 库;想彻底看懂寄存器级原理的,用标准库顺手,但要注意 ST 官方已停止维护标准库,新板子如 G0/L4 系列不再支持。

工程建立之后,先处理 GPIO 和时钟的初始化。使用 HAL 库时,MX_GPIO_Init()里需要把 PA2/PA3 复用为 USART2 引脚,同时把继电器控制引脚(例如 PB0-PB3)配置为推挽输出。有一个参数值得留意:GPIO 的速度等级默认是 LOW,但 USART 引脚如果速度等级太低,高速串口通信时波形会被拉变形,建议把 USART 引脚的速度设为 HIGH,时钟频率达到 50MHz,这个设置在 CubeMX 的 GPIO_Speed 里可以直接选择。另一个是 ADC 引脚的采样时间,传感器如果输出阻抗较高,采样时间可以拉长到 55.5 周期,避免采样值抖动。

4.2 代码骨架:ADC 采集、DHT11 读取与串口收发

主循环的逻辑顺序可以固定为:采集传感器 -> 组帧 -> 串口发送 -> 等待串口指令 -> 执行控制。下面是一段用 HAL 库写的简化主流程:

int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_USART2_UART_Init(); MX_ADC1_Init(); uint16_t adc_val; float temp; while (1) { HAL_ADC_Start(&hadc1); if (HAL_ADC_PollForConversion(&hadc1, 100) == HAL_OK) { adc_val = HAL_ADC_GetValue(&hadc1); temp = (float)adc_val * 3.3f / 4095.0f * 100.0f; } HAL_ADC_Stop(&hadc1); char buf[64]; snprintf(buf, sizeof(buf), "{\"temp\":%.1f,\"humi\":%.1f}\r\n", temp, humi); HAL_UART_Transmit(&huart2, (uint8_t *)buf, strlen(buf), 1000); if (HAL_UART_Receive(&huart2, (uint8_t *)rx_buf, 1, 100) == HAL_OK) { // 逐字节累积到命令帧,解析 CMD: 前缀 } HAL_Delay(2000); } }

逐参数解释一下:HAL_ADC_PollForConversion(&hadc1, 100)的第二个参数是超时时间,单位是毫秒,ADC 转换通常只需要几十微秒,100 毫秒足够了,超时时间设太长会让系统卡死在等待中;temp = adc_val * 3.3f / 4095.0f * 100.0f这一步是把 12 位 ADC 的原始值换算成实际温度,3.3V 是参考电压,100 是传感器的灵敏度系数,不同传感器这个系数差异很大,需要根据手册调整;HAL_UART_Receive(&huart2, rx_buf, 1, 100)每次只收一个字节,是为了不阻塞主循环,配合状态机可以逐字解析出完整的控制指令。

这个骨架没有用中断接收,确实会牺牲一些实时性,但好处是代码路径单一,做毕业设计或原型验证时最不容易出隐蔽 bug。如果要在生产级项目里用,建议改为 DMA 接收 + 空闲中断,在串口收到一帧数据后自动触发解析,这个优化点放到最后一章讲。

4.3 数据帧封装与状态点上报指令

串口组帧格式建议遵循一个固定模板,而不是每次发一条 JSON 字符串到底。约定一个最小帧格式:

CMD:get_state CMD:set_relay,1,1 DATA:{"temp":26.5,"humi":60}

STM32 收到CMD:get_state后,把当前所有传感器状态打包成一条DATA:前缀的 JSON 报文发给 ESP8266。这个设计的好处是:ESP8266 端的解析逻辑只需判断前缀是CMD:还是DATA:,前者是云端下发的控制指令需要转发给 STM32,后者是 STM32 上行的数据,直接透传上报到 MQTT。

在 STM32 端实现一个命令分发函数,用strncmp匹配前缀,避免使用strstr之类的模糊匹配,因为strstr("CMD:set_relay", "relay")在子串同名时会误触发。正确的做法是逐字节解析后做全等比较:

if (strncmp(rx_buf, "CMD:set_relay", 13) == 0) { int ch = rx_buf[14] - '0'; int val = rx_buf[16] - '0'; HAL_GPIO_WritePin(GPIOB, 1 << ch, val ? GPIO_PIN_RESET : GPIO_PIN_SET); }

这里有个值得注意的点:GPIO 的输出电平和继电器动作是反相关系,因为驱动电路是三极管或达林顿管,GPIO 输出低电平才能让继电器线圈导通,所以代码里写的是val ? GPIO_PIN_RESET : GPIO_PIN_SET。如果你直接让 GPIO 输出高电平控制继电器,会得到相反的动作结果,这也是新手最容易踩的坑之一。

5. 上电调试三板斧:串口分级输出、断线重连、指示灯状态机

先配置好整个系统的调试基线,再去折腾联网。第一步是用串口打印把 STM32 和 ESP8266 的通信链路分开验证。常见做法是给串口输出加上分级标签,[DBG]表示调试信息,[ERR]表示错误,[DATA]表示业务数据。STM32 通过printf重定向输出到 USART1,ESP8266 的透传数据从 USART2 进,这样在串口助手里可以同时看到两路日志,快速定位“数据是没采集到还是没发出去”。

第二步是断线重连。无论是 AT 方案还是 NodeMCU 方案,ESP8266 在 Wi-Fi 断开或 MQTT 掉线后,短则几秒长则几分钟才能察觉。一个有效的做法是在 STM32 主循环里维护一个上报计数:每 5 秒向 ESP8266 发送一条PING帧,如果连续 3 次没有收到PONG响应,就判断链路异常,随即重启 ESP8266 的电源或拉低 RST 引脚。注意,复位 ESP8266 后不要马上发数据,需要等待至少 500ms 等模块重新上电完成,然后重新执行 MQTT 连接序列。

第三步是状态机。用 LED 的亮灭组合指示系统处于什么状态:上电自检时 LED 快闪,Wi-Fi 连接中 LED 每秒闪一次,MQTT 已连接 LED 常亮,断线重连时 LED 双闪。这个状态机写在 STM32 里,同时把状态通过串口上报到云端。调试时你根本不用看电脑,光瞄一眼灯的节奏就知道系统卡在哪一步。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询