STM32+ESP8266+MQTT接入OneNet平台:DHT11温湿度采集与继电器远程控制实战
2026/9/13 8:21:34 网站建设 项目流程

简介:面向 STM32 嵌入式开发者与物联网初学者,这是一份基于 STM32F103+ESP8266 的 MQTT 接入中移 OneNet 云平台的完整代码工程。资源以 KEIL 工程形式组织,当前在 STM32F103C8T6 上运行,也兼容其他 F103 型号,只需调整芯片型号与 Flash 容量即可;程序通过串口 2 驱动 ESP8266,实现平台数据主动上报、云台下发的继电器控制指令解析,以及温湿度采集与状态回传。压缩包共 189 个文件,约 6.31MB,以 .c/.h 源码为主,辅以 Keil 工程配置、编译链接产物以及 hex/axf 可执行文件,可直接打开编译或烧录验证。工程内包含定时器、ADC、USART、I2C 等标准外设驱动,还保留了 map、lst 等调试文件,便于自行跟踪运行流程并二次开发。目前已有 1859 人浏览学习,适合需要快速搭建物联网上报链路、理解 MQTT 协议对接和继电器控制逻辑的开发者。

1. 一套能跑的STM32+ESP8266+OneNet链路,先看清数据怎么走

把 STM32、ESP8266 和 OneNet 拼在一起做远程控制,很多人第一反应是"这不就是串口转 WiFi 再发个 HTTP 吗"。实际动手会发现,HTTP 轮询方式做设备控制延时大、服务器压力高,而 MQTT 的长连接 + 订阅发布模型,天然适合继电器这种实时性要求高的场景。本文要讲的这套方案,数据流向是:STM32 通过 DHT11 采集温湿度,在本地处理继电器逻辑,同时经串口把数据交给 ESP8266,ESP8266 以 MQTT 协议接入中移 OneNet 物云平台,平台侧下发指令再沿同一条链路回到 STM32 执行。

这套架构的巧妙之处在于把 WiFi 协议栈完全剥离出 STM32。你不需要在单片机上跑 lwIP 或者折腾 W5500 这类以太网芯片,ESP8266 用 AT 固件就能独立完成 TCP 连接、MQTT 报文封装和心跳维持,STM32 只负责 AT 指令解析和业务逻辑。对做毕业设计、智能家居小项目或者想快速验证物联网方案的工程师来说,这是成本最低、调试手段最透明的组合。

整条链路上有三个关键决策点:其一是 DHT11 时序读取要关闭中断,否则采样值会漂;其二是 ESP8266 的固件版本决定了 AT 指令集差异;其三是 OneNet 的 MQTT 接入地址、端口和 topic 格式跟通用 MQTT Broker 不一样,照搬 Mosquitto 的用法连不上。下文按"采集 → 传输 → 上云 → 联调"的顺序,把每步的命令、代码和参数都摊开讲。

2. STM32采集温湿度:DHT11时序采样与HAL库代码

2.1 为什么用DHT11而不是DS18B20或SHT30

温湿度传感器选型上,DHT11 精度一般(湿度 ±5%RH,温度 ±2℃),但胜在单总线协议简单、库函数满地都是、价格几块钱。DS18B20 只测温度,SHT30 走 I2C 精度高但代码复杂度上了一个台阶。我的建议是:如果项目目标是验证 MQTT 链路和继电器控制逻辑,DHT11 足够;如果产品要做温湿度闭环调节,直接换 SHT30,I2C 时序比单总线稳定得多。

DHT11 的数据格式是 40bit:湿度整数 + 湿度小数 + 温度整数 + 温度小数 + 校验和。实际使用中小数部分恒为 0,所以只需要读 16bit 湿度 + 16bit 温度,校验和等于前四个字节之和的低 8 位。这个校验必须做,我见过不少项目因为省掉校验,偶尔读到 255℃ 这种脏数据就直接触发继电器误动作。

2.2 单总线时序:主机拉低18ms是启动信号

DHT11 的时序分两个阶段。主机先把总线拉低至少 18ms(典型 20ms),再释放并拉高 20~40us,这是启动信号。随后 DHT11 响应:先拉低 80us,再拉高 80us,表示"我准备好了"。之后每个 bit 的传输是:50us 低电平 + 26~28us 高电平代表逻辑 0,50us 低电平 + 70us 高电平代表逻辑 1。

也就是说,你要分辨 0 和 1,关键是测量高电平持续时长。用 HAL 库的HAL_GetTick()根本不够用——它的分辨率是 1ms,而 DHT11 的位宽是微秒级。必须用 SysTick 的微秒延时,或者直接操作定时器计数器。代码实现上用while循环翻转 GPIO 读取电平更可靠。

// dht11.c - 基于STM32F103 HAL库的DHT11读取 #include "dht11.h" #include "main.h" extern TIM_HandleTypeDef htim2; // 用定时器2做us级延时/计数 static GPIO_PinState DHT11_ReadBit(void) { // 每bit起始: 50us低电平 + 高电平(26~70us) while (HAL_GPIO_ReadPin(DHT11_GPIO_Port, DHT11_Pin) == GPIO_PIN_RESET); // 跳过50us低 uint32_t t = __HAL_TIM_GET_COUNTER(&htim2); while (HAL_GPIO_ReadPin(DHT11_GPIO_Port, DHT11_Pin) == GPIO_PIN_SET); // 等高电平结束 t = __HAL_TIM_GET_COUNTER(&htim2) - t; return (t > 40) ? GPIO_PIN_SET : GPIO_PIN_RESET; // 高电平超过40个时钟=1 }

这段代码有两个关键点。第一,while循环检测电平翻转而不是HAL_Delay,因为 HAL_Delay 依赖于 SysTick 中断,在关中断环境下会死等;第二,判断阈值用t > 40,这里的 40 对应定时器计数个数。如果定时器时钟是 72MHz 且预分频 72(即计数频率 1MHz),40 个计数就是 40us,正好落在 26~28us 和 70us 的中间,区分度高且抗干扰。

2.2.1 完整读取流程与校验
uint8_t DHT11_ReadData(dht11_data_t *out) { uint8_t data[5] = {0}; __disable_irq(); // 采样期间关闭全局中断,防止被抢占 // 主机启动信号: 拉低≥18ms HAL_GPIO_WritePin(DHT11_GPIO_Port, DHT11_Pin, GPIO_PIN_RESET); HAL_Delay(20); HAL_GPIO_WritePin(DHT11_GPIO_Port, DHT11_Pin, GPIO_PIN_SET); // 释放总线后需等待DHT11响应,延时40us左右 for (volatile int i = 0; i < 40; i++); // 粗略延时,具体值按主频调 // 检查DHT11是否拉低总线(响应信号) if (HAL_GPIO_ReadPin(DHT11_GPIO_Port, DHT11_Pin) == GPIO_PIN_SET) { __enable_irq(); return 1; // 无响应 } while (HAL_GPIO_ReadPin(DHT11_GPIO_Port, DHT11_Pin) == GPIO_PIN_RESET); // 跳过80us低 while (HAL_GPIO_ReadPin(DHT11_GPIO_Port, DHT11_Pin) == GPIO_PIN_SET); // 跳过80us高 // 40bit数据读取: 5字节 × 8bit for (int i = 0; i < 5; i++) { for (int j = 0; j < 8; j++) { data[i] = (data[i] << 1) | (DHT11_ReadBit() == GPIO_PIN_SET); } } __enable_irq(); // 校验和 = 前4字节之和的低8位 if ((uint8_t)(data[0] + data[1] + data[2] + data[3]) != data[4]) return 2; out->humidity = (data[0] << 8 | data[1]) / 10.0f; out->temperature = (data[2] << 8 | data[3]) / 10.0f; return 0; }

注意开头的__disable_irq()。DHT11 时序要求微秒级精确,而 STM32 的 SysTick 中断每 1ms 触发一次,如果不关中断,读 bit 的过程中一旦被中断打断,电平判断就会错位,最终读出的数据是乱的。但关中断有个隐患:如果你的系统里跑着 RTOS 或者有其他关键中断(比如串口接收),长时间关中断会导致丢数据。解决办法是只在这 5ms 的读取窗口内关中断,读完立即打开。

如果你用的是 FreeRTOS,这里还有一个坑:HAL_Delay(20)在关中断后无法工作(SysTick 停了),所以启动信号必须放在__disable_irq()之前,或者用HAL_Delay的替代实现。更稳妥的做法是把 DHT11 读取任务放到最高优先级,靠任务调度保证时序。

3. ESP8266做WiFi猫:AT指令连网、建TCP会话

3.1 硬件接线与固件版本确认

ESP8266 与 STM32 的典型接法是串口直连:ESP8266 的 TXD 接 STM32 的 USART2_RX(PA3),RXD 接 USART2_TX(PA2),VCC 接 3.3V(注意电流需求,ESP8266 发射瞬间电流可达 300mA,必须用独立 LDO 或供电模块,不能吃 STM32 板载 LDO 的余量),CH_PD(EN)引脚拉高。如果用的是 ESP-01 模组,GPIO0 悬空或拉高进入运行模式;拉低则进入烧录模式。

上电后用 USB-TTL 转接模块接 ESP8266,打开串口助手(波特率 115200)发送AT,返回OK说明固件正常。接着发送AT+GMR查看固件版本。这个版本信息很关键:2017 年后的 AT 固件指令集和旧版有差异,尤其是 MQTT 相关的自定义指令。如果固件太老,建议用乐鑫官网的 AT 固件烧录工具升级,或者直接用 ESP8266 原厂 SDK 方案。我一般会在项目文件夹里存一份固定版本的 AT 固件 bin,避免队友用不同固件版本导致兼容问题。

3.2 联网三步:CWMODE、CWJAP、CIPSTART

ESP8266 连上 WiFi 再建 TCP 连接,需要三步指令。先设置工作模式,再连接热点,最后建立 TCP 会话。注意 OneNet 旧版 MQTT 服务的 TCP 端口是 80(不是 1883),新版 MQTT 套件是 1883,这个要和平台侧对应,后面章节细说。

// 步骤1: 设为Station模式 AT+CWMODE=1 // 步骤2: 连接WiFi热点 (需要路由器2.4G频段,5G的连不上) AT+CWJAP="YourSSID","your_password" // 步骤3: 建立TCP连接到OneNet MQTT服务器 AT+CIPSTART="TCP","183.230.40.39",80

这里有个细节:AT+CWJAP返回WIFI DISCONNECT提示符是正常的,只要最终出现WIFI CONNECTEDWIFI GOT IP就算成功。如果一直卡在WIFI CONNECTED没有 IP,通常是路由器开启了 AP 隔离,或者 DHCP 地址池耗尽。另外 ESP8266 是 2.4G 单频,5G 热点根本搜不到,这也是新手最容易卡住的地方。

3.2.1 STM32侧发送AT指令的封装

STM32 和 ESP8266 之间是串口通信,STM32 发 AT 指令、收OK/ERROR响应,必须做超时处理。很多项目直接在while里死等,一旦 ESP8266 没响应,整个程序就挂死。我用的是有限状态机 + 超时计数的方式:

// esp8266.c - AT指令发送与响应解析 static esp_state_t esp_state = ESP_STATE_INIT; void ESP8266_Task(void) { static uint32_t last_tick = 0; switch (esp_state) { case ESP_STATE_INIT: ESP8266_SendCmd("AT+CWMODE=1\r\n", "OK", 2000); esp_state = ESP_STATE_WIFI_CONNECT; last_tick = HAL_GetTick(); break; case ESP_STATE_WIFI_CONNECT: // 等待 "WIFI GOT IP" if (g_rx_buffer_find("WIFI GOT IP")) { ESP8266_SendCmd("AT+CIPSTART=\"TCP\",\"183.230.40.39\",80\r\n", "CONNECT", 5000); esp_state = ESP_STATE_TCP_CONNECTED; } // 超时5s重发CWJAP if (HAL_GetTick() - last_tick > 5000) { ESP8266_SendCmd("AT+CWJAP=\"SSID\",\"pwd\"\r\n", "WIFI GOT IP", 8000); last_tick = HAL_GetTick(); } break; default: break; } }

这个封装思路是:g_rx_buffer_find在串口接收中断里把数据存入环形缓冲区,主循环轮询查找关键字。相比阻塞等待,好处是 ESP8266 在CIPSTART阶段可能先返回一些空行或busy提示,关键字匹配能跳过这些噪声。超时重发机制则保证 WiFi 临时断连后能自愈。

4. OneNet平台侧配置与MQTT报文对接

4.1 创建产品、设备和APIKey:数据流模板决定JSON结构

OneNet 平台分为旧版 MQTT 老协议和新版 MQTT 物联网套件,两者接入方式不同。本文以旧版 MQTT 老协议为基础(这也是标题里"中移 OneNet 物云平台"最常见的历史接入方式),新版差异会在末尾参数表中标出。

登录 OneNet 控制台后,在"多协议接入"中选择 MQTT 协议。创建产品时需要填写产品名称、行业分类,技术方案选"公开协议"。设备创建完成后,系统生成 device_id 和 APIKey。APIKey 在项目里有两个用途:一是平台侧 API 调用的鉴权,二是设备接入 MQTT 时作为 password。两者别搞混。

数据流模板决定了 MQTT 上报数据的格式。我建了三个数据流:temphumrelay_state。其中temphum的数据类型选"浮点数",relay_state选"整数型(布尔)",后续 App 展示和定时触发都要依赖这个类型定义。如果模板里没建数据流,设备上报的数据在平台上能看到但不会自动保存到云数据库,历史数据查询会一片空白。

4.2 MQTT参数映射:clientId、username、password分别填什么

这是最容易翻车的地方。OneNet 老版 MQTT 的接入参数和标准 MQTT 差别很大,对照关系如下:

标准MQTT字段OneNet要求取值示例
ClientId产品ID你的产品创建后生成的product_id,纯数字
Username设备ID设备管理中的device_id,纯数字
PasswordAPIKey形如Kk4xG4h8vQnZx7m3Yg8s9c=,创建APIKey时生成
Server固定IP183.230.40.39
Port固定端口80(老版),1883(新版套件)
KeepAlive心跳间隔推荐30~60秒,平台超时2倍则判离线
// mqtt_config.h - OneNet MQTT连接参数定义 #define ONENET_SERVER_IP "183.230.40.39" #define ONENET_SERVER_PORT 80 #define ONENET_PRODUCT_ID "284567" // 产品ID -> 替换成你自己的 #define ONENET_DEVICE_ID "620345819" // 设备ID -> 从平台设备列表复制 #define ONENET_APIKEY "Kk4xG4h8vQnZx7m3Yg8s9c=" // APIKey -> 严格区分大小写 #define ONENET_KEEPALIVE 30 // 心跳间隔, 单位秒

注意 ClientId 和 Username 的方向:标准 MQTT 里 clientId 只是一个标识,而 OneNet 把产品ID和设备ID拆到了两个字段里,并且有匹配校验。如果填反,连接会直接拒绝,返回0x05表示未授权。调试时用 MQTTX 这种客户端工具先连一下,能快速定位是参数问题还是网络问题。MQTTX 里同样要填写上述四项,连接成功后能看到 broker 返回的CONNACK报文。

4.3 报文踩坑:$dp发布数据、$creq接指令,cmdid必填

ESP8266 用透传模式发 MQTT 报文时,数据是裸的 TCP 字节流。我们需要手动构造 MQTT 报文。老的 OneNet MQTT 协议中,有两个主题用频率最高:

  • 发布数据到$dp:payload 是 JSON 格式,外层套datastreams数组
  • 订阅$creq/<device_id>:接收平台下发的指令
// 上报温湿度和继电器状态的payload {"datastreams":[{"id":"temp","datapoints":[{"value":25.6}]}, {"id":"hum","datapoints":[{"value":60.3}]}, {"id":"relay_state","datapoints":[{"value":1}]}]}
// mqtt_packet.c - 构造MQTT CONNECT报文 (略去固定头细节) uint8_t mqtt_packet[128]; // 报文固定部分: 协议名 + 协议级别0x04 + 连接标志(0xC0: username+password) + keepalive mqtt_packet[0] = 0x10; // CONNECT报文类型 // ... 填充可变头 ... // clientId, username, password填入对应CDATA字段 // OneNet老协议需要在payload中附加设备id和APIKey memcpy(payload, "{\"clientId\":\"284567\",\"username\":\"620345819\"," "\"password\":\"Kk4xG4h8vQnZx7m3Yg8s9c=\",\"cmdid\":10001}", strlen(cmd));

这里有个 OneNet 特有的字段cmdid,它用于关联请求和响应。比如平台下发一条指令到设备,设备执行完要回响应,响应消息里带上相同的cmdid,平台才知道这条指令已经被处理了。如果不带这字段,平台侧的行为日志会显示"未确认"。

重要提示:不要试图用模组自带 AT 指令里的AT+MQTTCONN来连 OneNet。ESP8266 的 AT 固件里的 MQTT 指令是针对通用 broker 设计的,字段顺序和 OneNet 不匹配。正确做法是AT+CIPSTART建立裸 TCP 连接,然后AT+CIPMODE=1进入透传模式,把上面这些 MQTT 字节流原样发送。

5. 联调验证与断线重连:一晚上跑不挂的收尾技巧

5.1 用MQTTX先验证平台配置,再让设备上电

设备上电前,我习惯先用 MQTTX 桌面客户端模拟设备接入。MQTTX 里新建连接,填上 4.2 节的四个参数,连接成功后向$dp发布一条测试 JSON,看平台数据流的曲线是否跳动。这一步能过滤掉 80% 的平台配置问题,避免拿着示波器去调 ESP8266 才发现是 APIKey 打错了。

平台侧验证的另一个手段是看"操作日志"和"数据流管理"。旧版 OneNet 控制台里有设备在线状态显示,数据流里能看到最新值更新时间。如果设备显示在线但数据流无更新,优先检查 JSON 格式——平台会把格式错误的数据静默丢弃,不会主动报错。

5.2 心跳与掉线重连:指数退避比固定间隔好用

ESP8266 的 TCP 连接在公网环境下非常容易被动断开:路由器 NAT 超时、运营商空闲回收、WiFi 信号波动,都会导致 socket 静默关闭。OneNet 平台以 keepalive 周期判断设备存活,设备侧要在超时前主动发心跳。常见做法是每 30 秒发一条 MQTT PINGREQ(报头0xC0,长度 0),平台回 PINGRESP。

掉线检测的难点在于:你无法感知 TCP 半开连接(一方断开但另一方不知道)。设备侧的心跳如果收不到响应,连续 3 次就判定链路异常,主动关闭 TCP 连接并回退到重连流程。

重连策略定义
基础退避3s 后重试连 WiFi,失败则 6s、12s、30s,最大上限 30s
无限重试不设最大次数,保证设备在网
数据缓存离线期间温湿度数据存入一段环形区,恢复后补发最近一条
// main.c - 主循环调度, 心跳与重连 while (1) { // 每5s读取一次DHT11 if (HAL_GetTick() - dht_tick > 5000) { DHT11_ReadData(&dht); ESP8266_ReportSensor(&dht); // 通过$dp上报 dht_tick = HAL_GetTick(); } // 30s心跳 if (HAL_GetTick() - mqtt_tick > 30000) { ESP8266_SendMQTTKeepAlive(); mqtt_tick = HAL_GetTick(); } // 串口收到平台下行指令, 解析并执行继电器 if (g_cmd_received) { ProcessOneNetCommand(g_cmd_buffer); // 提取relay_state值, 控制PB0 } // 心跳连续无响应 -> 重连 if (mqtt_no_response_count >= 3) { ESP8266_Reconnect(); mqtt_no_response_count = 0; } }

继电器控制这里补充一点:平台下发的指令 payload 一般是{"relay_state":1}或平台规则引擎的触发动作,解析后直接操作 GPIO。为了防止 STM32 复位或 ESP8266 重启导致继电器状态丢失,建议把当前继电器状态通过$dp上报到平台,这样 App 端显示的状态永远来自设备真实状态,而不是本地缓存。

5.3 最后三个可复现的调试技巧

第一个技巧,用串口打印 + 时间戳定位卡点。在 ESP8266 的每个状态迁移处打印带毫秒时间戳的日志,比如[1000] AT+CIPSTART[1560] CONNECT OK,一把就能看出是 WiFi 连接慢还是 TCP 握手慢。第二个技巧,ESPlorer(或任意串口工具)单独调 ESP8266,先用 AT 指令手动把 MQTT 报文发一遍,确认报文正确后再让 STM32 接管。第三个技巧,DHT11 连续三次读取失败时,不要连续重试,间隔 2 秒再读;DHT11 的刷新频率上限是 0.5Hz,超过这个频率芯片会不响应,表现为数据全 1。把这些细节检查到位,一套基于 STM32 + ESP8266 + MQTT 接入 OneNet 的继电器温湿度链路就能稳定运行。

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

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

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

立即咨询