资讯动态

STM32F103C8T6+ESP8266接入ONENET:环境监测与远程报警系统实战

发布时间:2026/9/16 19:05:04 来源:尧图企业网站定制
简介针对STM32F103C8T6与ESP8266接入OneNET云平台的物联网综合实例面向嵌入式开发者和物联网课程设计/竞赛人群整合DHT11温湿度采集、MQ-2烟雾浓度检测、OLED实时显示以及温湿度或烟雾超标时蜂鸣器报警与风扇联动还以LED模拟室内灯光可通过OneNET平台远程控制LED和风扇形成“采集—显示—上报—控制”的完整物联网闭环。资源包共3436个文件约354.46MB以C语言源文件与头文件为主另含Java/Python辅助脚本、Keil工程配置uvproj/uvopt、原理图与PCB设计、Hex/Bin固件及OLED驱动代码同时保留51单片机和STM32两个平台目录便于对比底层差异、按需移植。资料内含ESP8266与OneNET的通信配置、数据点上传和远程指令下发等关键实现工程代码按模块划分关键功能组织清晰能帮助读者快速搭建设备端与云平台的数据链路理解物联网项目从传感器驱动、本地逻辑处理到云端交互的完整流程。目前已有3267人学习下载适合希望系统掌握OneNET接入、传感器数据处理及硬件联调的开发者参考。1. F103C8T6 ESP8266 接入 ONENET这套环境监测系统到底在解决什么把一块 STM32F103C8T6、一块 ESP8266、一支 DHT11、一个 MQ-2 气体传感器和几颗 LED 拼在一起能做什么最常见的落地场景是智能家居里的「环境感知 远程报警」最小系统DHT11 测温湿度、MQ-2 测烟雾或可燃气体浓度、LED 当作室内灯来模拟执行器STM32 负责采集与逻辑判断ESP8266 负责把数据推到 ONENET 云平台手机或电脑随时能看到曲线湿度太高、温度异常、烟雾超标时也能远程或本地触发灯光动作。这个标题覆盖了物联网嵌入式开发的几个核心环节传感器数据采集DHT11 单总线协议、MQ-2 ADC 采样、MCU 与 Wi-Fi 模组通信UART AT 指令、物联网平台接入ONENET 设备/数据流/APIKey、边缘逻辑与云平台联动超标判断、LED 模拟执行。对正在学 STM32 或做毕业设计、课程设计、入门智能家居项目的人它是一套完整的工程链路对已经写过点灯的开发者本文真正值得看的是 ESP8266 透传 ONENET 时 HTTP 报文怎么拼、DHT11 时序在 HAL 库下面怎么不被延时函数坑到、MQ-2 的 ADC 数值如何换算成可用的浓度量纲。先说结论这套系统的难点不在单个模块而在「四个设备各自独立凑一起时电平、时序、协议互相打架」。下面按硬件规划、传感器采集、Wi-Fi 入网、平台对接、断线重传的顺序把一个能长期稳定跑通的方案讲清楚。2. 硬件搭建与引脚规划F103C8T6 引脚分配、DHT11 与 MQ-2 接线、ESP8266 电平匹配2.1 先理解 F103C8T6 的资源边界STM32F103C8T6 是 64KB Flash、20KB RAM 的 Cortex-M3 芯片。这个容量决定了代码风格如果你用标准外设库可以较随意地写如果直接用 HAL 库全量编译20KB RAM 在开了一堆外设句柄和协议解析缓冲区之后会吃紧。我一般时钟配置跑 72MHz外部 8MHz 晶振 PLL 倍频ADC、USART、GPIO 全开移植 FreeRTOS 也够但没必要——裸机状态机足够。引脚规划要先定「模拟量、单总线、串口、PWM」各自占用哪些口避免和 SWD 调试口冲突外设引脚类型说明DHT11PB0GPIO 推挽输出/开漏输入单总线数据线必须外接 4.7k~10k 上拉电阻MQ-2 模拟输出 AOPA0ADC 输入STM32 ADC1 通道 0MQ-2 数字输出 DOPA1GPIO 输入比较器阈值输出可留作冗余报警ESP8266 TXPB10USART3 RX交叉连接3.3V TTL 电平ESP8266 RXPB11USART3 TX交叉连接3.3V TTL 电平ESP8266 CH_PD/ENPA2GPIO 输出模块使能默认拉高LED 室内灯模拟PA6GPIO 推挽输出低电平点亮加 220Ω 限流电阻板载 LEDPC13GPIO 输出状态指示或故障指示为什么 F103C8T6 和 ESP8266 之间要特别注意电平匹配ESP8266 的 GPIO 是 3.3V 电平F103C8T6 的 PB10/PB11 也是 3.3V 电源域两者可以直接互连。但如果你用的是 5V 供电的 Arduino 或者手头有 5V 的单片机板子就必须加电平转换。另外 ESP8266 的 RX 引脚不能直接接 5V否则长期运行会烧芯片。用 USB-TTL 调试 ESP8266 时同理先把跳线帽调到 3.3V 再接线。2.1.1 DHT11 的接线与上拉电阻细节DHT11 是单总线器件数据线在空闲时被上拉到高电平。STM32 的 GPIO 内部上拉电阻约 30~50kΩ阻值偏大、驱动能力不足在长线或潮湿环境下容易读取出错。常见做法也是我推荐的做法是在 PB0 外部并联一个 4.7kΩ 上拉电阻到 3.3V。DHT11 数据手册给出的供电范围是 3.3V~5V接 3.3V 时传输距离短一些但在开发板上 20cm 以内完全没问题而且避免了 5V 回灌给 STM32 的风险。DHT11 的引脚定义是丝印面朝自己从左到右依次为 VCC、DATA、NC、GND。VCC 接 3.3VNC 悬空即可。接线顺序别搞反接反了上电后模块会持续发热。2.1.2 MQ-2 模块的 AO / DO 双输出怎么选MQ-2 烟雾气敏传感器模块的板载比较器通常是 LM393提供 DO 数字输出和 AO 模拟输出两个引脚。DO 输出只有「高于/低于阈值」两种电平阈值靠电位器调节但无法得到连续变化程度——只能做「超标/不超标」判断。如果只做开关量联动 LEDDO 就够了如果想把烟雾浓度变化曲线传到 ONENET 上看趋势AO 必须接入 STM32 的 ADC。这里有一个新手常踩的坑MQ-2 的 AO 输出电压不是线性的浓度值。它内部有加热丝和二氧化锡气敏材料不同气体液化气、丁烷、烟雾、酒精对应不同的灵敏度曲线。对 MCU 来说最方便的做法是把 AO 电压经过 ADC 量化为 0~4095 的数字量然后在代码里做「相对阈值」判断——记录上电稳定后的基线值比如 ADC 值在 800 附近超过基线 200 就认为浓度异常。这样可以避免去查手册里的对数坐标曲线和复杂的电阻比值计算。MQ-2 模块上电后需要预热 30 秒以上加热丝工作传感器表面到达工作温度这段时间 ADC 输出值会从高到低缓慢漂移。所以我一般在程序初始化时做一个 3 秒的短采样只用于确认 ADC 通道工作正常真正的超标判断放在系统运行 30 秒之后。你也可以在 ONENET 平台上看曲线刚上电时烟雾值偏高是正常物理现象不是代码 bug。2.2 ESP8266 模块选型ESP-01 还是 NodeMCU标题里写的是 ESP8266实际上有两种用法一是用 AT 固件的 ESP-01 模块只有 8 个引脚通过串口 AT 指令操作二是用 NodeMCU 开发板带 USB 转串口、板载稳压和 LED。在 STM32 做主控时我建议用 ESP-01 这类纯模块让 F103C8T6 扮演主机角色ESP8266 只负责 Wi-Fi 透传。NodeMCU 更适合用 Arduino IDE 直接开发两者的开发方式和代码完全不同。如果购买的是 ESP-01注意它虽然有 8 个引脚但实际可用的 GPIO 只有 GPIO0 和 GPIO2TX 引脚 GPIO1 也被占用IO 扩展能力很弱。不过本项目里 ESP8266 不需要外部 IO——它的任务是「收到 STM32 串口发来的数据通过 TCP 转发到 ONENET再把 ONENET 下发的数据原样返回给 STM32」。用 AT 指令的透传模式即可。2.2.1 ESP8266 固件确认与 AT 指令前的准备工作拿到 ESP-01 第一步不是接线而是确认固件版本。用 USB-TTL 连接模块TX 接 USB-TTL 的 RX、RX 接 TX、VCC 接 3.3V、GND 接 GND、CH_PD 接 3.3V打开串口助手波特率 115200发送AT收到OK表示固件正常。如果是老模块可能默认波特率是 9600 或 74880固件启动时的调试输出。模块能 AT 响应后建议先把固件升级到支持ATCIPMODE1透传模式的版本AT 1.7.x 以后基本都支持出厂默认就是。透传模式的意思是建立 TCP 连接后串口收到的所有数据都会原样发到服务器不再需要每条数据前加ATCIPSEND指令吞吐效率高很多代码也好写。如果你手里的固件没有透传模式就必须用「普通模式 定长发送」的流程每次发 HTTP 报文前先发ATCIPSENDlength这个细节到第 3 章展开。3. 传感器采集与数据预处理HAL 库驱动 DHT11 的精确延时实现3.1 DHT11 单总线时序为什么 HAL_Delay 不能用DHT11 的通信协议是典型的单总线时序主机先发起始信号然后释放总线等待从机响应之后 DHT11 连续输出 40 bit 数据。关键时间参数如下主机起始信号拉低18~30ms然后拉高 20~40usDHT11 响应信号拉低 80us再拉高 80us数据位 0低电平 50us 高电平 26~28us数据位 1低电平 50us 高电平 70us 以上完整数据帧8bit 湿度整数 8bit 湿度小数 8bit 温度整数 8bit 温度小数 8bit 校验和HAL 库的HAL_Delay()是以 1ms 为最小单位无法实现 us 级的精确延时。如果用HAL_Delay(1)去等待 50us 的时序实际延迟会放大到 1ms直接导致读取失败。所以在 HAL 库工程里驱动 DHT11必须先解决微秒延时问题。常见做法是用 Cortex-M3 内核的 DWTData Watchpoint and Trace单元它有一个 32 位的自由运行计数器 CYCCNT以 CPU 主频递增计数精度高且不占用定时器资源/* dht11_delay.c */ static volatile uint32_t us_ticks; /* 当前us换算后的tick数 */ void DWT_Delay_Init(void) { CoreDebug-DEMCR ~CoreDebug_DEMCR_TRCENA_Msk; CoreDebug-DEMCR | CoreDebug_DEMCR_TRCENA_Msk; /* 使能DWT */ DWT-CYCCNT 0; DWT-CTRL | DWT_CTRL_CYCCNTENA_Msk; /* 使能CYCCNT计数器 */ } void DWT_Delay_Us(uint32_t us) { uint32_t ticks us * (SystemCoreClock / 1000000U); /* 72MHz下1us72个tick */ uint32_t start DWT-CYCCNT; while ((DWT-CYCCNT - start) ticks); }这段代码的思路SystemCoreClock在 72MHz 主频时等于 72000000SystemCoreClock / 1000000得到 1 微秒对应的 CPU 周期数 72。延时函数让 CYCCNT 从起始值累加到目标差值即可。主频不是 72MHz 时这个计算会自动适配比如降到 8MHz 时 1us 等于 8 个 tick。注意 DWT 的减法在计数器溢出时依然正确无符号减法回绕所以不需要处理溢出边界。提示如果不想用 DWT也可以把 SysTick 中断关掉后直接读取 SysTick-VAL 寄存器做微秒延时但代码里要手动把当前重装载值减当前 VAL 值写法比 DWT 复杂还容易和 HAL 的 SysTick 中断冲突。DWT 是本项目最省心的方案。3.2 DHT11 读取函数起始信号、响应判断、40 bit 数据解析与校验在 CubeMX 中把 PB0 配置为 GPIO_Output 即可代码里动态切换方向。完整的 DHT11 读取代码如下/* dht11.c */ #define DHT11_PORT GPIOB #define DHT11_PIN GPIO_PIN_0 #define TIMEOUT 200 /* 每次电平等待超时上限约200*10us2ms */ static void DHT11_Set_Pin_Output(void) { GPIO_InitTypeDef gpio_init {0}; gpio_init.Pin DHT11_PIN; gpio_init.Mode GPIO_MODE_OUTPUT_PP; /* 推挽输出注意不是开漏 */ gpio_init.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(DHT11_PORT, gpio_init); } static void DHT11_Set_Pin_Input(void) { GPIO_InitTypeDef gpio_init {0}; gpio_init.Pin DHT11_PIN; gpio_init.Mode GPIO_MODE_INPUT; /* 输入模式外部4.7k上拉保证空闲高电平 */ gpio_init.Pull GPIO_NOPULL; /* 外部已有上拉内部不上拉 */ HAL_GPIO_Init(DHT11_PORT, gpio_init); } uint8_t DHT11_Read_Data(uint8_t *humidity, uint8_t *temperature) { uint8_t buf[5] {0}; uint32_t timeout; DHT11_Set_Pin_Output(); /* 1. 主机起始信号拉低至少18ms */ HAL_GPIO_WritePin(DHT11_PORT, DHT11_PIN, GPIO_PIN_RESET); HAL_Delay(20); /* 20ms确保DHT11识别为起始信号 */ HAL_GPIO_WritePin(DHT11_PORT, DHT11_PIN, GPIO_PIN_SET); DWT_Delay_Us(30); /* 拉高30us然后释放总线 */ DHT11_Set_Pin_Input(); /* 2. 等待DHT11拉低响应低电平80us */ timeout TIMEOUT; while (HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) GPIO_PIN_SET) if (--timeout 0) return 1; /* 3. 等待响应结束高电平80us */ timeout TIMEOUT; while (HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) GPIO_PIN_RESET) if (--timeout 0) return 1; /* 4. 读取40bit数据高位在前 */ for (int i 0; i 40; i) { timeout TIMEOUT; while (HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) GPIO_PIN_SET) if (--timeout 0) return 1; timeout TIMEOUT; while (HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) GPIO_PIN_RESET) if (--timeout 0) return 1; DWT_Delay_Us(40); /* 40us后采样超过28us即判定为数据位1 */ if (HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) GPIO_PIN_SET) { buf[i / 8] 1; buf[i / 8] | 1; /* 高电平持续70us - 数据位1 */ } else { buf[i / 8] 1; buf[i / 8] | 0; /* 高电平持续28us - 数据位0 */ } } /* 5. 校验和前4字节累加低8位等于第5字节 */ if ((uint8_t)(buf[0] buf[1] buf[2] buf[3]) ! buf[4]) return 2; *humidity buf[0]; /* 相对湿度整数部分单位%RH */ *temperature buf[2]; /* 温度整数部分单位℃buf[1]和buf[3]为小数部分本项目忽略 */ return 0; }代码里的关键判断逻辑数据位 0 和数据位 1 的区别在「低电平之后的高电平持续时间」。每一位的低电平时间都是一样的 50us不同点在数据位 1 的高电平更长70us vs 26~28us。所以我用一个循环等待低电平结束while (READ RESET)然后在低电平结束后延时 40us 再读电平。如果读到高电平说明高电平持续时间超过了 40us 加上前面已消耗的时间必然落在 70us 区间判为 1如果是 0此时高电平已经结束28us 40us读回低电平。DHT11 的采样频率不能太高两次读取之间至少要间隔 1 秒否则传感器会不响应或返回错数据。我在主循环里把 DHT11 读取频率控制在 2 秒一次并在读取失败后不立刻重试而是等待下一个周期。连续读取失败超过 5 次就把传感器标为异常并置位故障标志位避免主循环卡死在等待时序里。3.3 MQ-2 ADC 采样均值滤波与基线校准MQ-2 的 AO 引脚接 PA0CubeMX 里开启 ADC1、通道 0采样时间设为 239.5 周期更长的采样时间能获得更稳定的结果连续转换模式关闭用单次转换由软件触发/* mq2.c */ uint16_t MQ2_Read_ADC(void) { ADC_ChannelConfTypeDef sConfig {0}; sConfig.Channel ADC_CHANNEL_0; sConfig.Rank ADC_REGULAR_RANK_1; sConfig.SamplingTime ADC_SAMPLETIME_239CYCLES_5; HAL_ADC_ConfigChannel(hadc1, sConfig); HAL_ADC_Start(hadc1); HAL_ADC_PollForConversion(hadc1, 100); return (uint16_t)HAL_ADC_GetValue(hadc1); } uint16_t MQ2_Read_Average(uint8_t samples) { uint32_t sum 0; for (uint8_t i 0; i samples; i) { sum MQ2_Read_ADC(); DWT_Delay_Us(5000); /* 两次采样间隔5ms让电容放电 */ } return (uint16_t)(sum / samples); }烟雾传感器的本质是一个气敏电阻它的响应有「吸附-脱附」过程瞬时采样值抖动非常明显。我在主循环里取 10 次采样的平均值等效于一个低通滤波器。如果你觉得 10 次还不够稳可以取 20 次但每次间隔要拉开到 10ms 以上否则数据重叠没有意义。关于「烟雾浓度」如何计算MQ-2 的 AO 电压和气体浓度在双对数坐标下近似线性但低浓度段偏差很大且不同厂家的模块参数不一致。所以我建议把 ADC 值作为相对量处理开机后记录空气环境下的基线mq2_base预热完成后采 20 次取平均实时值超过基线一定比例就判断为烟雾超标。下面这段代码体现了这个逻辑/* main.c 主循环中的超标判断 */ #define MQ2_THRESHOLD_OFFSET 200 /* ADC值相对基线偏移量 */ #define TEMP_ALERT_THRESHOLD 30 /* 温度超标值单位℃ */ #define HUM_ALERT_THRESHOLD 70 /* 湿度超标值单位%RH */ uint16_t mq2_val MQ2_Read_Average(10); uint8_t smoke_alarm (mq2_val mq2_base MQ2_THRESHOLD_OFFSET); uint8_t temp_alarm (temp TEMP_ALERT_THRESHOLD); uint8_t hum_alarm (humi HUM_ALERT_THRESHOLD); if (smoke_alarm || temp_alarm || hum_alarm) { HAL_GPIO_WritePin(LED_PORT, LED_PIN, GPIO_PIN_RESET); /* 点亮模拟室内灯 */ alarm_triggered 1; } else { HAL_GPIO_WritePin(LED_PORT, LED_PIN, GPIO_PIN_SET); /* 熄灭 */ alarm_triggered 0; }超标联动 LED 的策略分成两级小超标比如温度 29°C、湿度 65%RH时 LED 慢速闪烁提醒严重超标烟雾 ADC 偏移超过 500 或温度超过 35°C时 LED 常亮并打开继电器驱动蜂鸣器。这样一个简单的状态机就形成了边缘自动控制不需要云端参与就能本地响应同时上报给 ONENET 的数据加上alarm_triggered字段让云平台也能看到报警状态。4. ESP8266 与 ONENET 云平台接入AT 指令透传、APIKey 生成、HTTP 数据上报4.1 ESP8266 串口通信初始化与入网 AT 指令序列STM32 通过 USART3PB10/PB11和 ESP8266 通信。CubeMX 里把 USART3 模式设为异步、波特率 115200、8 数据位、1 停止位、无校验。初始化完成后STM32 依次向 ESP8266 发送下面几条 AT 指令。每条指令我都检查返回的OK或具体关键字超时 3000ms 认为失败并重试。/* esp8266_at.c */ #define CMD_AT AT\r\n #define CMD_CWMODE ATCWMODE1\r\n #define CMD_RST ATRST\r\n #define CMD_JOINAP ATCWJAP\MyWiFi\,\password123\\r\n #define CMD_TCP_START ATCIPSTART\TCP\,\183.230.40.40\,80\r\n #define CMD_TRANSMODE ATCIPMODE1\r\n #define CMD_TRANSMIT ATCIPSEND\r\n顺序和每个指令的作用AT—— 检查 ESP8266 是否在线收到OK继续。ATCWMODE1—— 设置为 Station 模式只连接路由器不创建热点。ATRST—— 软复位让 CWMODE 生效。复位后等待 2~3 秒ESP8266 冷启动时间较长。ATCWJAPMyWiFi,password123—— 让 ESP8266 连接无线路由器。这个命令可能耗时 2~10 秒我把等待时间设置为 10 秒并循环检查WIFI CONNECTED和WIFI GOT IP两个返回值。ATCIPSTARTTCP,183.230.40.40,80—— 建立 TCP 连接。这里的目标地址是 ONENET 平台 HTTP 接入的服务器地址。新版本的 ONENET 平台可能在控制台显示不同的接入 IP 或域名你需要以「产品详情-设备接入」页面为准。端口 80 是 HTTP 服务默认端口不需要加密如果使用 MQTT 方式则端口为 6002。ATCIPMODE1—— 开启透传模式之后 STM32 串口发什么ESP8266 就原样发给服务器。ATCIPSEND—— 进入透传发送状态。发送成功会返回提示符。从此刻起所有后续串口数据都直接进入 TCP 连接。要退出透传模式发送不加回车换行。注意一个细节ATCIPSTART在透传模式下必须在ATCIPMODE1之前执行顺序反了会返回ERROR。另外 ESP8266 和 ONENET 服务器之间的 TCP 连接是长连接但运营商基站或路由器 NAT 表可能在某些环境下比如数据中心网络、手机热点强制回收空闲连接。ONENET 平台本身对 HTTP 长连接的支持也有超时限制。因此我建议每次上报完数据后不关闭 TCP 连接而是定期发送心跳数据比如每 10 秒发一个ATPING或小包数据让连接保持活跃如果检测到 TCP 连接断开ESP8266 返回CLOSED就重新走一遍ATCIPSTART到ATCIPSEND的流程。4.2 ONENET 平台侧配置创建产品、设备、数据流、APIKeyONENET 经典版平台的接入流程对应热点词「onenet生成apikey」「onenet云平台接入」如下登录 ONENET 控制台进入「多协议接入」页面选择「HTTP」协议点击「创建产品」。产品名称可填「智能环境监控」技术类型选「HTTP」联网方式「Wi-Fi」设备类型「智能家居」。创建完成后进入「设备管理」添加设备。设备名称填「test-device-01」。系统会分配一个设备ID类似5244831048实际数值以平台为准这个 ID 是 HTTP URL 路径的一部分。进入「管理-密钥管理」生成或查看APIKey。APIKey 是设备上报身份凭证它出现在 HTTP 请求头api-key字段中用于鉴别设备归属。不要把 APIKey 暴露在公网日志里。「数据流模板」是可选的但不建议跳过。在「数据流」页面新建temperature、humidity、smoke_level、alarm_state四个数据流。创建后ONENET 平台才能为这些数据流绘制折线图——就是热点词里「onenet物联网平台折线图绘制」对应的界面功能。保存产品的「接入地址」和「端口」。HTTP 协议上报地址通常是http://api.heclouds.com/devices/设备ID/datapoints其中api.heclouds.com是接入域名。这里有一个平台层面的坑ONENET 平台区分「设备 ID」和「产品 ID」HTTP 上报 URL 里用的是设备 ID而 APIKey 可以关联一个或多个设备。如果 URL 里的设备 ID 和你 APIKey 关联的设备不一致服务器返回401 Unauthorized——排查时优先检查这两个值是否匹配。4.3 HTTP 数据上报报文拼接与透传模式下发送实现ONENET 的 HTTP 数据上报接口请求体是 JSON 格式外层是datastreams数组每个元素包含数据流 ID 和datapoints数组。一条包含温湿度和烟雾数据的请求体如下{ datastreams: [ { id: temperature, datapoints: [{ value: 26 }] }, { id: humidity, datapoints: [{ value: 55 }] }, { id: smoke_level, datapoints: [{ value: 835 }] }, { id: alarm_state, datapoints: [{ value: 0 }] } ] }完整的 HTTP POST 请求报文如下在透传模式下STM32 把这整个字符串通过 USART3 发给 ESP8266/* 构造HTTP请求 */ char http_request[512]; int len snprintf(http_request, sizeof(http_request), POST /devices/%s/datapoints HTTP/1.1\r\n Host: api.heclouds.com\r\n api-key: %s\r\n Content-Type: application/json\r\n Content-Length: %d\r\n \r\n %s, DEVICE_ID, API_KEY, json_len, json_payload ); HAL_UART_Transmit(huart3, (uint8_t *)http_request, len, HAL_MAX_DELAY);报文头部字段含义说明POST /devices/{设备ID}/datapoints HTTP/1.1—— 请求路径携带设备 ID。Host: api.heclouds.com—— 指定 HTTP 服务的域名。即使你已经通过ATCIPSTART直连了 IPHost 头也必须携带域名ONENET 的虚拟主机靠它区分产品。api-key: {APIKey}—— 注意请求头里是小写的api-key不是apikey。ONENET 平台在解析时把这两个视为不同字段我在实际开发中碰到过因大小写不一致导致鉴权失败的情况。Content-Type: application/json—— 声明请求体格式。Content-Length: {JSON长度}—— 必须与json_payload的字节数严格一致。多了或少了都会导致平台解析失败或连接被服务端关闭。透传模式下发送完请求后服务器端会返回 HTTP 响应包含200 OK状态码和一段 JSON 数据整体通过 ESP8266 串口返回给 STM32。STM32 端不解析响应内容只检测是否出现errno:0或 HTTP200——表示数据已被 ONENET 接收。如果串口收到的是CLOSED说明 TCP 连接断开了要从ATCIPSTART重新开始。4.4 命令下发ONENET 平台向 F103C8T6 发送控制指令ONENET 不仅能收数据还能下发命令对应 LED 远程控制的场景。平台侧的「命令下发」页面填一个字符串比如LED_ON平台会将其作为 HTTP POST 请求体发送给设备——但这里有一个关键机制ONENET 命令下发需要设备端先发起一次 HTTP 请求建立长连接平台才能通过这个连接反向推送命令。在你刚完成数据上报、保持 TCP 连接未断开时ONENET 平台可以在这个连接上下发命令。ESP8266 的串口会收到一段 HTTP 响应头加上请求体内容。STM32 在每次上报后可以等待 200ms 检查是否有新数据到达 USART3。收到数据后从中搜索LED_ON或LED_OFF关键字来操作 LED/* main.c 命令响应逻辑 */ uint8_t cmd_buffer[128]; uint16_t cmd_len 0; uint16_t data_len HAL_UART_Receive(huart3, cmd_buffer, sizeof(cmd_buffer) - 1, 10); if (data_len 0) { cmd_buffer[data_len] \0; if (strstr((char *)cmd_buffer, LED_ON)) { HAL_GPIO_WritePin(LED_PORT, LED_PIN, GPIO_PIN_RESET); /* 开灯 */ cloud_cmd 1; } else if (strstr((char *)cmd_buffer, LED_OFF)) { HAL_GPIO_WritePin(LED_PORT, LED_PIN, GPIO_PIN_SET); /* 关灯 */ cloud_cmd 0; } }注意HAL_UART_Receive的最后一个参数是超时时间单位 ms。这里设 10ms 不影响主循环的实时性。命令下发功能在 HTTP 协议下不够实时需要等下一次上报建立连接如果项目对「按下手机瞬间灯就要亮」这种交互要求较高就得改用 MQTT 协议接入 ONENET。MQTT 的接入方式是建立 TCP 长连接后设备订阅自己的 topic平台命令下发通过 publish 推给设备延迟通常在几百毫秒内。不过 MQTT 需要对报文进行二进制编码连接报文、订阅报文、PINGREQ 心跳代码复杂度比 HTTP 高不少作为进阶扩展即可。对于环境监测这类十几秒一次上报的场景HTTP 方案完全够用。5. 实战进阶F103C8T6 的断线重连、数据补发与 5 个高频调试问题5.1 断线重连状态机指数退避 缓存补发ESP8266 在复杂无线环境下断开连接是常态不是异常。我写了一个简单的状态机来管理系统网络状态而不是在主循环里无脑重连typedef enum { NET_IDLE, NET_JOIN_AP, NET_TCP_CONNECT, NET_TRANSMIT_READY, NET_SEND_DATA } net_state_t; #define RETRY_MAX_ATTEMPTS 5 #define RETRY_BACKOFF_BASE 2 /* 指数退避基数秒 */ uint16_t retry_count 0; uint32_t next_retry_time 0;每次检测到连接断开串口收到CLOSED或上报超时就进入重连流程。重连采用指数退避第 1 次等 2 秒、第 2 次等 4 秒、第 3 次等 8 秒……到第 5 次等 32 秒。如果超过 5 次仍然失败不再自动重试而是拉高故障指示灯等待用户手动按复位键或触发诊断模式。这个策略能避免在无线信号弱、路由器故障的情况下MCU 陷入无休止的重连循环把功耗和日志都撑爆。指数退避还有一个好处是让出频谱给路由器处理拥塞——Wi-Fi 模块反复快速重连路由器会把它当恶意设备拒绝服务。下面的代码展示了核心逻辑bool esp8266_reconnect(void) { if (HAL_GetTick() next_retry_time) return false; if (esp8266_send_cmd(CMD_AT, OK, 3000) ! ESP_OK) { /* ESP8266 完全无响应需要硬件复位 */ HAL_GPIO_WritePin(ESP_EN_PORT, ESP_EN_PIN, GPIO_PIN_RESET); HAL_Delay(200); HAL_GPIO_WritePin(ESP_EN_PORT, ESP_EN_PIN, GPIO_PIN_SET); HAL_Delay(1000); return false; } if (esp8266_send_cmd(CMD_JOINAP, WIFI GOT IP, 10000) ! ESP_OK) { retry_count; next_retry_time HAL_GetTick() (RETRY_BACKOFF_BASE retry_count) * 1000; return false; } if (esp8266_send_cmd(CMD_TCP_START, CONNECT, 5000) ! ESP_OK) { retry_count; next_retry_time HAL_GetTick() (RETRY_BACKOFF_BASE retry_count) * 1000; return false; } esp8266_send_cmd(CMD_TRANSMODE, OK, 1000); esp8266_send_cmd(CMD_TRANSMIT, , 1000); retry_count 0; return true; }补发机制每次 DHT11 读取成功且 MQ-2 采样完成后我把数据存到一个 8 字节的结构体数组里作为环形缓冲区。如果发送失败数据条目标记pending 1。网络恢复后先发缓存的旧数据带时间戳字段再发当前最新数据。这样即使 Wi-Fi 断了 1 分钟云端也不会出现数据空洞。ONENET 的数据点默认带平台接收时间你也可以在 JSON 里加上at: 2025-01-15 14:30:00字段自定义时间。typedef struct { uint8_t temp; uint8_t humi; uint16_t smoke; uint8_t alarm; uint32_t timestamp; uint8_t pending; } sensor_data_t; #define DATA_CACHE_LEN 16 sensor_data_t data_cache[DATA_CACHE_LEN]; uint8_t cache_head 0, cache_tail 0; void cache_push(uint8_t t, uint8_t h, uint16_t s, uint8_t a) { data_cache[cache_tail].temp t; data_cache[cache_tail].humi h; data_cache[cache_tail].smoke s; data_cache[cache_tail].alarm a; data_cache[cache_tail].timestamp HAL_GetTick(); data_cache[cache_tail].pending 1; cache_tail (cache_tail 1) % DATA_CACHE_LEN; if (cache_tail cache_head) { cache_head (cache_head 1) % DATA_CACHE_LEN; /* 覆盖最旧数据 */ } }5.2 五个高频调试问题与排查方法第一个问题DHT11 读取返回超时函数返回 1。先用逻辑分析仪抓 PB0 波形——起始信号有没有被主机正确拉低 20msDHT11 有没有把总线拉低 80us 再释放波形正常而代码超时大概率是 DWT 延时函数的主频参数不对。程序里SystemCoreClock只有在 CubeMX 生成的SystemInit()执行后才会更新如果你在进入main()之前修改了时钟配置需要在主程序开头调用SystemCoreClockUpdate()刷新值。第二个问题DHT11 湿度清零、温度正常或反过来。这是单总线协议在长线上出现的信号反射导致的数据位误判。解决方法是降低采样频率比如从 2 秒改为 3 秒并增加 200ms 的总线空闲时间。另外确认 PB0 的 GPIO 工作在高速模式GPIO_SPEED_FREQ_HIGH否则 GPIO 翻转速率不够会拉不低电平。第三个问题ESP8266 连接 ONENET 服务器超时。先用 USB-TTL 单独连 ESP8266手动发 AT 指令逐条测试。ATCIPSTARTTCP...返回CONNECT后用ATCIPSEND发一个简单的 HTTP GET 请求看服务器是否响应。如果服务器无响应检查你的设备 ID 是否写成了产品 ID以及 APIKey 是否与设备绑定。平台侧也可以在「设备日志」里看到设备最近一次上线时间如果日志显示「未连接」说明 TCP 连接建立后被你自己的代码主动断开了。第四个问题ONENET 折线图没有新数据点。上报返回200 OK但数据没进数据流通常是数据流 ID 写错。JSON 里的id字段必须和平台「数据流模板」中创建的id完全一致区分大小写。另一个隐蔽的原因是Content-Type写成了text/plain平台可能返回400 Bad Request或直接丢弃。第五个问题LED 模拟灯误亮。烟雾传感器的 ADC 值在有人靠近、吹气、喷消毒酒精时都会瞬间上升触发误报警。我的处理方式是在代码里加个确认次数连续 3 次采样间隔 500ms都超过阈值才置位报警标志。这样把「瞬时抖动」过滤掉。此外MQ-2 在刚上电 10 分钟内基线会持续漂移建议在程序运行 60 秒后再采集基线值并且每 5 分钟更新一次基线取历史中位数而不是平均值避免把真实烟雾误判成基线。5.3 用 ONENET 数据验证现场传感器最后说一个验证整个链路的小技巧。在 ONENET 控制台打开设备对应的数据流页面把刷新周期设为 1 分钟观察折线图曲线变化。拿一支体温计放在 DHT11 旁边 30 秒看温度折线是否上升用手在 MQ-2 上方挥动 3 秒看smoke_level折线是否有明显脉冲。如果曲线有反应说明从传感器到 STM32 到 ESP8266 到 ONENET 整条链路是通的。若曲线无反应先从最近的节点开始排查串口助手上看 STM32 是否输出了日志、AT 指令透传是否有回显、平台日志里设备有没有上报记录。逐段二分比从物理层开始盲猜要快得多。这套 F103C8T6 ESP8266 ONENET 的架构本身就是物联网设备端一个可复用的模板换掉 DHT11 和 MQ-2接个 PM2.5 传感器或人体红外数据流和阈值逻辑稍作修改就变成空气质量监测或安防系统。乐见你把它拿来继续改。本文还有配套的精品资源点击获取

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

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

免费获取报价