ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

STM32+ESP8266+MQTT接入OneNet平台:DHT11温湿度采集与继电器远程控制实战

STM32+ESP8266+MQTT接入OneNet平台:DHT11温湿度采集与继电器远程控制实战 简介面向 STM32 嵌入式开发者与物联网初学者这是一份基于 STM32F103ESP8266 的 MQTT 接入中移 OneNet 云平台的完整代码工程。资源以 KEIL 工程形式组织当前在 STM32F103C8T6 上运行也兼容其他 F103 型号只需调整芯片型号与 Flash 容量即可程序通过串口 2 驱动 ESP8266实现平台数据主动上报、云台下发的继电器控制指令解析以及温湿度采集与状态回传。压缩包共 189 个文件约 6.31MB以 .c/.h 源码为主辅以 Keil 工程配置、编译链接产物以及 hex/axf 可执行文件可直接打开编译或烧录验证。工程内包含定时器、ADC、USART、I2C 等标准外设驱动还保留了 map、lst 等调试文件便于自行跟踪运行流程并二次开发。目前已有 1859 人浏览学习适合需要快速搭建物联网上报链路、理解 MQTT 协议对接和继电器控制逻辑的开发者。1. 一套能跑的STM32ESP8266OneNet链路先看清数据怎么走把 STM32、ESP8266 和 OneNet 拼在一起做远程控制很多人第一反应是这不就是串口转 WiFi 再发个 HTTP 吗。实际动手会发现HTTP 轮询方式做设备控制延时大、服务器压力高而 MQTT 的长连接 订阅发布模型天然适合继电器这种实时性要求高的场景。本文要讲的这套方案数据流向是STM32 通过 DHT11 采集温湿度在本地处理继电器逻辑同时经串口把数据交给 ESP8266ESP8266 以 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 足够如果产品要做温湿度闭环调节直接换 SHT30I2C 时序比单总线稳定得多。DHT11 的数据格式是 40bit湿度整数 湿度小数 温度整数 温度小数 校验和。实际使用中小数部分恒为 0所以只需要读 16bit 湿度 16bit 温度校验和等于前四个字节之和的低 8 位。这个校验必须做我见过不少项目因为省掉校验偶尔读到 255℃ 这种脏数据就直接触发继电器误动作。2.2 单总线时序主机拉低18ms是启动信号DHT11 的时序分两个阶段。主机先把总线拉低至少 18ms典型 20ms再释放并拉高 20~40us这是启动信号。随后 DHT11 响应先拉低 80us再拉高 80us表示我准备好了。之后每个 bit 的传输是50us 低电平 26~28us 高电平代表逻辑 050us 低电平 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即计数频率 1MHz40 个计数就是 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_RXPA3RXD 接 USART2_TXPA2VCC 接 3.3V注意电流需求ESP8266 发射瞬间电流可达 300mA必须用独立 LDO 或供电模块不能吃 STM32 板载 LDO 的余量CH_PDEN引脚拉高。如果用的是 ESP-01 模组GPIO0 悬空或拉高进入运行模式拉低则进入烧录模式。上电后用 USB-TTL 转接模块接 ESP8266打开串口助手波特率 115200发送AT返回OK说明固件正常。接着发送ATGMR查看固件版本。这个版本信息很关键2017 年后的 AT 固件指令集和旧版有差异尤其是 MQTT 相关的自定义指令。如果固件太老建议用乐鑫官网的 AT 固件烧录工具升级或者直接用 ESP8266 原厂 SDK 方案。我一般会在项目文件夹里存一份固定版本的 AT 固件 bin避免队友用不同固件版本导致兼容问题。3.2 联网三步CWMODE、CWJAP、CIPSTARTESP8266 连上 WiFi 再建 TCP 连接需要三步指令。先设置工作模式再连接热点最后建立 TCP 会话。注意 OneNet 旧版 MQTT 服务的 TCP 端口是 80不是 1883新版 MQTT 套件是 1883这个要和平台侧对应后面章节细说。// 步骤1: 设为Station模式 ATCWMODE1 // 步骤2: 连接WiFi热点 (需要路由器2.4G频段5G的连不上) ATCWJAPYourSSID,your_password // 步骤3: 建立TCP连接到OneNet MQTT服务器 ATCIPSTARTTCP,183.230.40.39,80这里有个细节ATCWJAP返回WIFI DISCONNECT提示符是正常的只要最终出现WIFI CONNECTED和WIFI 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(ATCWMODE1\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(ATCIPSTART\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(ATCWJAP\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 上报数据的格式。我建了三个数据流temp、hum、relay_state。其中temp和hum的数据类型选浮点数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.39Port固定端口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 协议中有两个主题用频率最高发布数据到$dppayload 是 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: usernamepassword) 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 指令里的ATMQTTCONN来连 OneNet。ESP8266 的 AT 固件里的 MQTT 指令是针对通用 broker 设计的字段顺序和 OneNet 不匹配。正确做法是ATCIPSTART建立裸 TCP 连接然后ATCIPMODE1进入透传模式把上面这些 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] ATCIPSTART→[1560] CONNECT OK一把就能看出是 WiFi 连接慢还是 TCP 握手慢。第二个技巧ESPlorer或任意串口工具单独调 ESP8266先用 AT 指令手动把 MQTT 报文发一遍确认报文正确后再让 STM32 接管。第三个技巧DHT11 连续三次读取失败时不要连续重试间隔 2 秒再读DHT11 的刷新频率上限是 0.5Hz超过这个频率芯片会不响应表现为数据全 1。把这些细节检查到位一套基于 STM32 ESP8266 MQTT 接入 OneNet 的继电器温湿度链路就能稳定运行。本文还有配套的精品资源点击获取
返回列表