资讯动态

STM32+ESP8266+MQTT接入OneNet:温湿度上报与继电器控制

发布时间:2026/9/13 6:33:18 来源:尧图企业网站定制
简介基于STM32F103与ESP8266 WIFI模块通过MQTT协议接入中移OneNet云平台的完整嵌入式项目代码包含继电器控制与温湿度采集功能适合正在学习物联网通信、STM32外设驱动及云平台对接的开发者参考。工程采用KEIL开发在STM32F103C8T6上验证通过其他F103型号只需修改芯片型号与Flash容量即可移植。代码实现了主动上报本地温湿度与继电器状态并接收平台下发的控制指令执行动作覆盖串口AT指令、MQTT报文解析、GPIO控制等关键环节。源码中串口2连接ESP8266串口1可用于调试输出主循环状态机与函数模块划分清晰便于按需裁剪或移植。双向通信流程完整可帮助理解设备端定时上报与平台指令响应的实现方法。压缩包共189个文件以C源码和H头文件为主附带uvprojx工程文件、HEX烧录文件及编译中间文件整体仅6.31MB解压后可直接用KEIL打开查看结构。已有1859人学习下载对有实际设备接入需求的物联网初学者和竞赛项目开发者有直接参考价值。1. 为什么是 STM32 ESP8266 MQTT 接入 OneNet 这套方案在做一个基于 STM32 的鱼缸控制器时最花时间的不是焊板子而是让继电器开合和温湿度数据可靠到达云端。采用 ESP8266 做 WiFi 接入STM32 采集 DHT22再通过 MQTT 协议与中移 OneNet 物云平台交换数据是这类物联网项目最常见也最容易跑通的组合。难点在于 ESP8266 上电后不一定能立刻联网MQTT 的鉴权参数又比 HTTP 容易配错产品 ID、设备名、设备 key 三个值写错一个设备就一直离线。下面把 OneNet 平台的配置和 STM32 端的收发代码一起拆开讲新手可以照着接线老手重点看命令下发和掉线重连部分。2. OneNet 平台侧准备产品、设备、APIKey 与数据流2.1 创建产品时选对协议后面接入少踩一半坑登录 OneNet中移物联网开放平台在控制台选择“多协议接入”。创建产品时协议类型选 MQTT而不是 HTTP。操作系统随意联网方式选 WiFi。产品名称建议跟项目一致比如stm32-relay-dht22。创建后你会拿到产品 ID后面所有设备鉴权都用它。选 MQTT 而不是 HTTP 的理由HTTP 轮询没法做服务端主动下发继电器远程控制需要平台向设备推命令MQTT 的 PUBLISH/SUBSCRIBE 天然适合温湿度周期性上报加命令下行。OneNet 的设备接入方式很多HTTP、MQTT、CoAP、NB-IoT 都支持但对 ESP8266 这类 WiFi 模组来说MQTT 是最省流量的方案一条温湿度报文带上 JSON 结构也就 100 字节左右。提示OneNet 的老版本区分“设备云”和“OneNET Studio”本文说的是新版 Studio 的 MQTT 接入方式。如果你打开的是旧版控制台界面入口不同但 APIKey 和产品 ID 的用途一致。2.2 生成 APIKey 并在数据流模板里定义温湿度在“产品详情”页面找到“APIKey”管理点击生成。APIKey 是平台调用 API 和部分 MQTT 连接校验用的密钥。设备接入 MQTT 时用户名用产品 ID密码用 APIKey 或设备 key具体取决于你选的鉴权模式。建议先把所有 key 存到一个文本文件里因为后面要多次复制。在“数据流模板”里新增两个数据流temperature和humidity。别用中文名STM32 端发布的 topic 和平台数据流必须完全一致。如果还想上报继电器状态再加一个relay_state。数据流名称类型单位用途temperatureint/float°CDHT22 温度humidityint/float%RHDHT22 湿度relay_stateint无继电器开合状态创建完产品后在“设备”页面添加一个真实设备。设备名称要记好例如dev_relay_01。设备创建成功后平台会给出一个设备 key 或者让你自建。这个设备 key 在 MQTT 的 password 字段里面用。2.3 用 Python 脚本先验证平台连通性再动 STM32在写 STM32 代码之前先用电脑上的 MQTT 客户端把平台流程走通。用 paho-mqtt 脚本发布一条温湿度数据到 OneNet确认鉴权参数和 topic 格式没问题。这样可以避免把硬件问题和平台配置问题混在一起。import paho.mqtt.client as mqtt import json import time PRODUCT_ID 123456 DEVICE_NAME dev_relay_01 DEVICE_KEY your_device_key_here client mqtt.Client(client_idDEVICE_NAME, protocolmqtt.MQTTv311) client.username_pw_set(PRODUCT_ID, DEVICE_KEY) client.connect(mqtt.heclouds.com, 1883, 60) # OneNet MQTT 数据上报 topic topic f$sys/{PRODUCT_ID}/{DEVICE_NAME}/dp/post/json payload { datastreams: [ {id: temperature, datapoints: [{value: 26.5}]}, {id: humidity, datapoints: [{value: 60.2}]} ] } client.publish(topic, json.dumps(payload), qos0) time.sleep(1) client.disconnect()逻辑说明paho 连接时 client_id 用设备名用户名用产品 ID密码用设备 key。OneNet 的 MQTT 端口是 1883域名以你的产品接入信息页显示为准通常是mqtt.heclouds.com或带s的加密域名开发阶段先用不带 TLS 的那一个。topic 必须走$sys/{产品ID}/{设备名}/dp/post/json这是 OneNet 定义的 datapoint 上报路径。payload 采用 JSON 结构一个 datastreams 数组可以同时上报多个数据流正好对应前面的 temperature 和 humidity。参数说明qos 这里用 0 就行温湿度数据允许偶发丢失qos1 会多一次确认报文适合继电器状态这类需要确认的数据。time.sleep(1) 是等发布完成断线太快会丢包。跑通这段脚本后前往 OneNet 的“设备详情”页查看数据流能看到刚上报的 26.5 和 60.2平台也会自动画出折线图不需要另外写图表代码。如果没看到数据检查脚本里的三个标识符是不是复制多了空格或换行。3. STM32 与 ESP8266 的硬件连接和 MQTT 数据上行3.1 ESP8266 与 STM32 连接原理图电平、供电和引脚选择ESP8266 常用的是 ESP-01 或 NodeMCU。如果直接把 ESP8266 的 TX 接到 STM32 的 RX两者逻辑电平都是 3.3V不需要转换但很多板载 USB 转串口模块会引入 5V 干扰。建议给 ESP8266 单独供电避免从 STM32 的 3.3V 引脚取太大电流WiFi 发射瞬间电流能到 300mA会拉低单片机供电。常见连接关系如下STM32 引脚ESP8266 引脚说明PA9 / USART1_TXRXSTM32 发送 AT 指令给 ESP8266PA10 / USART1_RXTXESP8266 回显和 MQTT 下行数据3.3VVCC、EN/CH_PD供电EN 必须拉高GNDGND共地除表格中的引脚GPIO0 在正常运行时要保持悬空或通过 10k 电阻上拉到 3.3V不能接 GND否则上电后进入烧录模式。温湿度传感器 DHT22 数据脚接 STM32 PB12继电器 IN 接 PB13。DHT22 也是 3.3V 供电数据线上要加 4.7k 上拉电阻到 3.3V。有些继电器模块带光耦隔离需要 VCC 和 GND 独立供电继电器电源不要和单片机共用避免电机、水泵启停时把单片机复位。3.2 用 AT 指令初始化 ESP8266 并连 WiFi注意回显超时STM32 通过串口向 ESP8266 发送 AT 指令。常见做法是把 ESP8266 固件烧成 AT 固件然后 STM32 只做 AT 指令收发。另一种方式是给 ESP8266 刷 NodeMCU 或 Arduino 固件让 ESP8266 自己跑 MQTTSTM32 只负责采集数据通过串口把温湿度发给 ESP8266。AT 固件方案更适合入门两个芯片职责清楚STM32 主控逻辑改动小不用维护两套 SDK。先做最小联网测试用串口助手或 STM32 的调试串口逐条发送ATCWMODE1 ATCWJAP你的WiFiSSID,你的WiFi密码 ATMQTTUSERCFG0,1,产品ID,设备名,设备key,0,0, ATMQTTCONN0,mqtt.heclouds.com,1883,1 ATMQTTSUB0,$sys/产品ID/设备名/cmd/request/#,1 ATMQTTPUB0,$sys/产品ID/设备名/dp/post/json,{\datastreams\:[{\id\:\temperature\,\datapoints\:[{\value\:26.5}]}]},0逻辑说明第一行把 ESP8266 设为 Station 模式第二行连接 WiFi。第三行配置 MQTT 用户参数0 表示连接 id 为 0 的 MQTT 连接这里三个标识符的对应关系和前面 Python 脚本一致。第四行建立 MQTT 连接最后一个 1 表示开启 keepalive。第五行订阅平台下发给设备的命令 topic。第六行发布温湿度数据。参数说明ATMQTTUSERCFG中第二个参数 1 表示使能 MQTT第三到第五个参数分别是产品 ID、设备名、设备 key第六和第七个参数是 MQTT 版本号这里用 0 表示默认最后一个空字符串是证书。ATMQTTCONN的最后一个 1 代表自动重连这个对继电器远程控制很重要WiFi 断了之后 ESP8266 能自动恢复 MQTT 连接。注意不同 ESP8266 AT 固件版本的指令带0的连接编号旧版本可能叫ATMQTTCONN不带编号。如果发送返回 ERROR先执行ATRST并用ATGMR查看固件版本。很多“连不上 WiFi”的问题不是密码错而是模块固件太老不支持 MQTT AT 指令需要先升级 AT 固件。3.3 STM32 端读取 DHT22 并用串口 DMA 发送 MQTT 报文STM32 端的代码结构不复杂一个 10ms 定时器做系统心跳一个 1s 定时器触发 DHT22 采集采集完成后把温度湿度拼成 JSON再通过串口 DMA 发送给 ESP8266。用 DMA 发送的原因ESP8266 在发送 MQTT PUBLISH 包时需要连续写入CPU 逐字节等待会卡住其他中断。typedef struct { uint8_t humi_high; uint8_t humi_low; uint8_t temp_high; uint8_t temp_low; } dht22_data_t; typedef struct { int16_t temperature; int16_t humidity; } env_data_t; void send_env_data_to_onenet(const env_data_t *env) { char payload[160]; int len snprintf(payload, sizeof(payload), ATMQTTPUB0,\$sys/%s/%s/dp/post/json\, {\datastreams\:[ {\id\:\temperature\,\datapoints\:[{\value\:%d.%d}]}, {\id\:\humidity\,\datapoints\:[{\value\:%d.%d}]}]}\r\n, PRODUCT_ID, DEVICE_NAME, env-temperature / 10, env-temperature % 10, env-humidity / 10, env-humidity % 10); HAL_UART_Transmit_DMA(huart1, (uint8_t *)payload, len); }逻辑说明snprintf把 ATMQTTPUB 指令拼成完整字符串温度值用整数和小数分开输出比如 26.5 度就发value:26.5。由于 DHT22 返回整数位和小数位分别存储用env-temperature / 10和% 10拆分。最后用HAL_UART_Transmit_DMA把整条 AT 指令交给串口外设发送CPU 不用等它发完。参数说明PRODUCT_ID和DEVICE_NAME必须在编译时定义为宏宏值和平台创建的产品设备一致。payload 数组长度 160如果以后要加更多数据流记得改大snprintf会截断截断后 AT 指令不完整ESP8266 返回 ERROR。DHT22 的读取时序对中断很敏感建议在读取期间关闭更高优先级中断或用定时器中断直接读。很多网友用 DHT11 做实验发现温度值跳变多半是 GPIO 上下拉时间不对而不是代码逻辑错误。3.4 STM32 串口解析 ESP8266 返回和平台下行数据ESP8266 执行 AT 指令后返回OK或ERROR同时订阅的命令 topic 有消息时会主动推送一行数据形如MQTTSUBRECV:0,$sys/123456/dev_relay_01/cmd/request/xxx,54,{cmd:relay,state:1}STM32 串口接收中断需要把它们区分开。收到OK可以置一个标志位收到ERROR则重发上一条指令收到MQTTSUBRECV则要按逗号拆分出 topic 和 payload。最简单的方法是先用环形 buffer 存串口数据然后在主循环里用strstr找MQTTSUBRECV关键字。void parse_esp8266_line(char *line) { if (strstr(line, MQTTSUBRECV) NULL) return; char *payload strchr(line, {); if (payload NULL) return; cJSON *root cJSON_Parse(payload); cJSON *cmd cJSON_GetObjectItem(root, cmd); cJSON *state cJSON_GetObjectItem(root, state); if (cJSON_IsString(cmd) strcmp(cmd-valuestring, relay) 0 cJSON_IsNumber(state)) { HAL_GPIO_WritePin(RELAY_GPIO_Port, RELAY_Pin, state-valueint ? GPIO_PIN_SET : GPIO_PIN_RESET); /* 上报继电器状态 */ } cJSON_Delete(root); }逻辑说明串口收到整行后先查找MQTTSUBRECV标记再找到第一个{作为 JSON 起点用 cJSON 库解析。平台下发的cmd字段决定动作类型state字段控制继电器。如果后续要支持多个继电器可以在 payload 里再加id字段。参数说明state-valueint是 cJSON 对数字的常用表示OneNet 平台下发 JSON 时无论数字还是布尔都会被转成对应字段。如果你想让继电器默认开可以在初始化时给RELAY_Pin一个初始状态避免 ESP8266 还没连上时继电器误动作。如果你用 PlatformIO 构建 STM32 项目记得把 cJSON 源码加入 lib 目录否则链接阶段会报未定义符号。4. MQTT 下行命令控制继电器与协议细节4.1 OneNet 命令下发是“主题订阅平台缓存”不是直接写MQTT 协议详解的要点就三个topic 是发布订阅的路径payload 是不透明的字符串QoS 控制消息送达级别。OneNet 基于标准 MQTT 3.1.1所以 STM32 端即使不用 AT 固件自己用 paho-embedded-c 这一类库也能实现但用 ESP8266 AT 固件的 MQTT 指令后协议栈由模组承担主控不需要实现报文编解码。在 OneNet 上给继电器发命令常见两种方式方式一是通过平台页面“设备详情”里的“下发命令”按钮填写自定义 JSON方式二是通过 HTTP API 调用比如在 Node-RED 里用 HTTP Request 节点请求 OneNet 的 API。无论哪种方式最终都是向设备订阅的那个cmd/request/主题写数据。OneNet 的 MQTT 命令下发主题一般格式是$sys/{产品ID}/{设备名}/cmd/request/{msgId}设备端订阅时用通配符#订阅整个前缀即可。STM32 收到后回复一个响应主题$sys/{产品ID}/{设备名}/cmd/response/{msgId}如果不回复平台可能显示命令超时。这个 msgId 不是固定值所以设备端解析 payload 时不要依赖 topic 里的 msgId。注意OneNet 对 qos0 的下发命令如果设备刚好离线平台可能直接把命令丢弃。继电器控制需要确认到达建议在平台 API 或设备端把命令下发 qos 设为 1。MQTT qos1 会触发 PUBACK设备端即使没处理也能知道平台发过。4.2 网页端控制面板的“指令格式”和 API 调用参数在 OneNet 控制台添加一个“应用”或“自动化”按钮把按钮的动作设置为“下发命令”命令内容{cmd:relay,state:1}这里 state 用 1 表示开0 表示关。如果再要一个“临时开 10 秒”功能可以写成{cmd:relay,state:1,duration:10}STM32 解析 duration 后开继电器并启动一个软定时器10 秒后自动关。这个逻辑在鱼缸换水、水泵控制场景很实用。如果你要通过自己的网页控制需要调用 OneNet 的 HTTP API 向设备发送命令。这里给出 curl 示例curl -X POST \ -H Authorization: ${API_KEY} \ -H Content-Type: application/json \ https://iot-api.heclouds.com/device/{device_id}/cmd \ -d {cmd:relay,state:1}逻辑说明Authorization头是你在平台生成的 APIKeydevice_id 不是设备名是设备详情页里的数字 ID。如果 APIKey 没有调用权限HTTP 会返回 401。在 Node-RED 里可以用同样方式实现本地规则比如温度超过 28 度自动开风扇再转发到 OneNet 命令。参数说明device_id和PRODUCT_ID不同不要搞混。如果你用 Vue3 写控制页面APIKey 要放在后端服务里不要直接写成前端常量否则别人拿到 key 就能控制你的设备。4.3 STM32 端的命令处理逻辑和继电器状态回传命令处理的关键不是收到后马上翻转继电器而是要先做“防误触”。平台或者网络抖动可能下发重复命令所以要在 STM32 端做一个去重#define CMD_IGNORE_INTERVAL_MS 500 static uint32_t last_cmd_time 0; void handle_relay_cmd(int state, uint32_t now_ms) { if (now_ms - last_cmd_time CMD_IGNORE_INTERVAL_MS) return; last_cmd_time now_ms; HAL_GPIO_WritePin(RELAY_GPIO_Port, RELAY_Pin, state ? GPIO_PIN_SET : GPIO_PIN_RESET); /* 回传继电器状态给 OneNet */ char payload[128]; snprintf(payload, sizeof(payload), ATMQTTPUB0,\$sys/%s/%s/dp/post/json\, {\datastreams\:[{\id\:\relay_state\,\datapoints\:[{\value\:%d}]}]}\r\n, PRODUCT_ID, DEVICE_NAME, state); HAL_UART_Transmit_DMA(huart1, (uint8_t *)payload, strlen(payload)); }逻辑说明now_ms由系统的 1ms tick 提供两次命令间隔小于 500ms 直接忽略。继电器执行后马上上报relay_state这样手机端页面能立刻看到开关状态。如果不回传你在 OneNet 图表里只能看到温湿度继电器状态永远空着。参数说明CMD_IGNORE_INTERVAL_MS要大于串口传输一条 AT 指令的时间。如果 WiFi 慢500ms 可能不够可以调到 1 秒。但别太长否则用户连续点击按钮会有失灵感。4.4 常见误用把 AT 指令放在中断里发导致命令丢行很多人会把HAL_UART_Transmit_DMA放在串口接收中断里直接对收到的一行数据进行回复。这在高频命令下发时容易出问题因为 AT 指令需要以\r\n结尾如果前一条还在发送后一条就进入 DMA 队列ESP8266 会把两条指令粘连返回ERROR。正确做法是设置一个发送状态机只有在上一条指令收到OK或ERROR后才发送下一条。在 main loop 里维护一个发送队列typedef struct { char data[256]; uint8_t length; } esp_cmd_t; #define CMD_QUEUE_SIZE 8 static esp_cmd_t cmd_queue[CMD_QUEUE_SIZE]; static uint8_t cmd_head 0, cmd_tail 0; void esp_send_cmd(const char *cmd) { if ((cmd_head 1) % CMD_QUEUE_SIZE cmd_tail) return; /* 队列满 */ memcpy(cmd_queue[cmd_head].data, cmd, strlen(cmd) 1); cmd_queue[cmd_head].length strlen(cmd); cmd_head (cmd_head 1) % CMD_QUEUE_SIZE; esp_send_from_queue(); }逻辑说明所有要发给 ESP8266 的 AT 指令先进环形队列串口 DMA 发送完成中断会触发esp_send_from_queue()把下一条取出发送。温湿度上报、继电器回传、设备初始化都调用esp_send_cmd就不会出现指令粘连。这个队列长度 8对继电器控制足够。如果以后要发 OTA 升级或批量上报需要加大缓冲区或者用空闲 DMA 中断来判断串口发完。STM32 HAL 的HAL_UART_TxCpltCallback可以作为发送完成钩子。调试时优先看串口返回串口返回含义操作OK上一条指令执行成功发送队列下一条ERROR指令格式错误或执行失败重试超过 3 次复位MQTTSUBRECV收到平台命令解析 JSONMQTTDISCONMQTT 连接断开等待重连5. 排查、可视化与工程化落地5.1 用 MQTTX 和 Node-RED 验证 OneNet 命令链路开头用 Python 脚本测了数据上报下行链路建议用 MQTTX 从电脑模拟设备订阅命令。使用 MQTTX 连接 OneNet 时client ID 填设备名用户名填产品 ID密码填设备 key服务器地址填mqtt.heclouds.com:1883。连接后订阅$sys/{产品ID}/{设备名}/cmd/request/#然后在 OneNet 控制台点击“下发命令”MQTTX 里能看到命令文本就说明平台到 MQTT 链路没问题。如果要在本地做更复杂的联动用 Node-RED 很合适。常见做法是新增一个 MQTT In 节点订阅同一主题再接一个 Function 节点判断温度最后通过 OneNet HTTP API 下发继电器命令。这样不依赖平台自带的规则引擎调试时能看到每条消息的时间戳和 payload。5.2 三个必调的工程参数keepalive、DMA 缓冲、看门狗生产部署时把 ESP8266 的 MQTT keepalive 从默认的 60 秒改成 30 秒OneNet 对心跳超时判定较严格太长的 keepalive 会让平台误判设备离线。DMA 接收缓冲建议 512 字节OneNet 下发的命令如果带时间戳很容易超过 256 字节。STM32 的 IWDG 看门狗超时设置成 1 秒每个主循环喂一次防止 ESP8266 AT 指令卡死导致系统挂起。5.3 掉线自动恢复的完整状态机最后给出一个适合移植到 RTOS 的连接状态机typedef enum { ESP_STATE_RESET, ESP_STATE_SET_WIFI, ESP_STATE_MQTT_CONN, ESP_STATE_READY, ESP_STATE_WAIT_RECONNECT } esp_state_t;状态机从 RESET 开始每个状态只做一件事收到OK才迁移到下一状态收到ERROR则回到本状态重试连续 3 次失败跳到 RESET重新ATRST。WAIT_RECONNECT 状态延时 5 秒后重新走 WiFi 配置流程。这样可以应对路由器重启、WiFi 密码变更、OneNet 服务端断开连接等场景。验证方法断开路由器电源 30 秒再恢复观察 STM32 串口日志应该看到ATCWJAP重连成功然后ATMQTTCONN返回 OK主板继电器状态不丢失。如果你用的是 FreeRTOS把每个状态单独做成一个 task用消息队列传递串口行数据比在主循环 while 里等串口更可靠。调试阶段把 OneNet 的 TLS 加密关掉端口用 1883开发完成后再考虑加密和证书。确保 device id、产品 ID、APIKey 全部写在独立头文件中不要散落在业务代码里。把心跳时间、重试次数、队列深度单独做成宏现场调参时只需改一个文件。后期想把继电器接入智能音箱做语音控制只要在云端应用层增加一个 HTTP 接口依旧走这条cmd/request主题STM32 端代码完全不用动。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价