ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

STM32物联网实战:DHT11+MQ-2+ESP8266接入OneNET远程控制

2026/9/16 15:48:25 拓冰建站 浏览量
STM32物联网实战:DHT11+MQ-2+ESP8266接入OneNET远程控制 简介这是一份基于STM32F103C8T6的物联网环境监测与智能控制完整工程资源面向嵌入式开发者、物联网初学者及毕业设计/竞赛人群覆盖ESP8266接入OneNET云平台、DHT11温湿度与MQ-2烟雾浓度采集、OLED屏显、超限蜂鸣器报警及风扇/室内灯联动控制等核心环节。包内共3436个文件以C语言工程源码c/h、编译产物o/hex/axf及Keil工程配置uvproj/uvopt为主另含原理图/PCB设计文件、PDF文档、大量bmp/png图片和Java源文件。压缩包约354.46MB目录层次丰富可支撑从原理图到代码调试的完整链路。该资源目前已有三千余人学习浏览说明其参考价值受到同类开发者认可。读者通过梳理代码与工程结构可快速掌握STM32外设驱动、AT指令连接OneNET、数据上报与云平台远程控制的具体写法也能借鉴其多传感器任务调度和报警联动设计思路。1. 从传感器到云平台一块C8T6如何把环境数据变成可远程干预的执行链路用STM32F103C8T6接DHT11和MQ-2做环境监测很多人的第一反应是“这不就是点个灯、读个温湿度”真正动手后才发现最难的不是传感器驱动而是让数据在“传感器→MCU→ESP8266→OneNET云平台→手机端”这条链路里稳定地流动起来。这个项目把完整的物联网闭环摆在面前DHT11采集温湿度、MQ-2采集烟雾浓度数据不仅显示在本地OLED上还会通过ESP8266以MQTT协议上报OneNET反过来云端下发的命令可以控制LED灯模拟室内照明、启动风扇排烟超标时蜂鸣器报警。适合正在学STM32的开发者、做毕设或课程设计的学生以及想快速搭建一套可用物联网原型的中级工程师。通过拆解这个工程可以一次性搞懂单总线时序、ADC采样换算、AT指令透传、MQTT报文格式和命令下发回调这五个关键环节。2. 引脚分配与传感数据采集DHT11单总线时序和MQ-2的ADC换算2.1 C8T6的引脚预算与通信外设选型F103C8T6是36引脚封装的Cortex-M3芯片GPIO资源只有37个但在这个项目中够用前提是把每类外设的引脚占用提前规划清楚。我的分配思路是DHT11占用一个普通GPIO即可因为单总线协议是半双工通信读和写共用同一条线MQ-2模块的模拟输出引脚接ADC输入通道STM32F103的ADC1支持最多10个通道选择PA0到PA7之间的任意一个即可OLED采用I2C接口只需要SCL和SDA两条线ESP8266通过USART串口通信占用TX和RX两脚LED和蜂鸣器各占一个GPIO输出口风扇控制如果只是开关用一个GPIO驱动三极管或MOS管就行若是PWM调速则要选择带定时器复用功能的引脚如PA8用于TIM1_CH1。外设模块通信方式占用的引脚说明DHT11单总线自定义GPIOPA1数据线需外接4.7kΩ上拉电阻MQ-2模拟电压输出PA0ADC1_IN0模块自带的比较器输出可作为数字开关量OLED 0.96寸I2CPB8SCL、PB9SDA地址一般为0x78或0x7AESP8266USART3PB10TX、PB11RX注意是交叉连接MCU的TX接ESP8266的RXLED模拟灯GPIO推挽输出PA2低电平点亮或高电平点亮取决于接法蜂鸣器GPIO推挽输出PA3有源蜂鸣器直接高低电平控制风扇控制GPIO输出/PWMPA4开关或PA8PWM电流大时需加三极管或MOS管驱动选型上要注意两点。第一DHT11和MQ-2都用5V供电但STM32的GPIO耐压是5V数据手册标称FT引脚可容忍5V输入所以DHT11的数据线在配置为开漏输出并外接上拉到3.3V时能正常工作这是常见做法。第二ESP8266的供电电流在Wi-Fi发射时峰值可到300mA以上不能直接从C8T6的3.3V LDO取电需要单独给ESP8266配一个AMS1117-3.3或DC-DC降压模块否则系统会在连接Wi-Fi时反复重启这是最常见的坑。2.2 DHT11单总线时序读取HAL库下的微秒级延时实现DHT11每次通信过程分为两步主机先发送起始信号然后释放总线等待DHT11响应最后读取40位数据。整个时序中最关键的是微秒级延时精度HAL库自带的HAL_Delay只支持毫秒级必须用定时器或DWT实现微秒延时。我习惯用DWTData Watchpoint and Trace单元因为它在Cortex-M3内核里不需要额外占用定时器资源通过CoreDebug-DEMCR使能DWT周期计数器即可。/* 使用DWT实现微秒延时精度稳定且不占用定时器 */ void delay_us(uint32_t us) { CoreDebug-DEMCR | CoreDebug_DEMCR_TRCENA_Msk; DWT-CYCCNT 0; DWT-CTRL | DWT_CTRL_CYCCNTENA_Msk; uint32_t start DWT-CYCCNT; uint32_t ticks us * (SystemCoreClock / 1000000); while ((DWT-CYCCNT - start) ticks); }这段代码的核心是先使能DWT计数器然后根据系统主频折算目标延时对应的时钟周期数最后轮询等待计数值累加到目标值。SystemCoreClock在标准库中通过SystemInit函数设置在HAL库中由SystemCoreClockUpdate()更新72MHz主频下1微秒等于72个周期。需要说明的是如果工程中其他地方启用了中断DWT计数的自增不受中断影响但延时代码本身如果被高优先级中断打断实际延时会偏大所以读取DHT11的时间敏感段最好关中断或保证该段代码运行在临界区。读取数据的核心逻辑是拉低总线18ms再拉高20~40us然后切换为输入模式通过测量引脚高电平持续时间判断每一位。DHT11数据位编码方式是50us低电平起始之后26~28us高电平代表070us高电平代表1。uint8_t DHT11_ReadByte(void) { uint8_t value 0; for (int i 0; i 8; i) { while (!HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN)); // 等待50us低电平结束 delay_us(40); // 延时40us后采样 if (HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN)) { value | (0x80 i); // 如果仍然为高说明是1 } while (HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN)); // 等待高电平结束 } return value; }这段代码的逻辑边界需要注意第一次while (!ReadPin)是等待低电平过去然后延时40us此时如果是数据0高电平会在26~28us内结束采样处已经为低如果是数据1高电平持续70us40us处采样仍为高。第二个while (ReadPin)用来确保当前位读完避免把下一位的低电平当成当前位的一部分。如果DHT11与MCU距离超过20cm建议在数据线上并联一个100nF电容滤波但要注意这会略微改变信号上升沿可能导致读取出错需要实测调整延时参数。2.3 MQ-2烟雾浓度与ADC电压换算MQ-2气敏传感器的本质是二氧化锡半导体当环境中可燃气体浓度升高时电导率增大模块输出的模拟电压随之升高。模块上的电位器可以调节数字输出阈值但模拟量需要自己做转换。C8T6的ADC是12位分辨率参考电压3.3V所以数字量换算电压的公式很简单。但换算成浓度则不能直接套线性公式因为MQ-2的浓度-电阻特性在对数坐标下才近似线性。/* ADC采集与烟雾浓度线性化处理 */ float Get_Smoke_Percent(void) { uint16_t adc_value 0; for (int i 0; i 16; i) { // 多次采样取平均滤除噪声 adc_value HAL_ADC_GetValue(hadc1); HAL_Delay(2); } adc_value 4; // 右移4位相当于除以16 float voltage (float)adc_value / 4095.0f * 3.3f; // 12位ADC满量程4095 float percent (voltage - 0.35f) / (2.8f - 0.35f); // 线性映射实测标定 if (percent 0.0f) percent 0.0f; if (percent 1.0f) percent 1.0f; return percent * 100.0f; }这里的百分比是相对量而非真实的ppm浓度。标定逻辑是在纯净空气中读取一个基准电压通常0.3~0.5V在烟雾浓度较高环境下读取一个上限电压通常2.5~3.0V然后把这两个点之间的电压差映射为0~100%。正式做产品时要用标准浓度的甲烷或丙烷气体对MQ-2进行多点校准拟合曲线至少需要3个标定点并对温漂做补偿。另外MQ-2上电后有热机过程模块内部的加热丝需要至少1分钟才能让传感器稳定工作刚上电时读取的数据会持续漂移程序里应该在初始化后延时几分钟再启动报警监测逻辑。3. ESP8266接入OneNETMQTT协议栈与AT指令透传3.1 OneNET设备创建与APIKey生成OneNET是国内成熟的物联网平台接入方式支持MQTT、HTTP、TCP透传等。对于这个项目选择MQTT协议有三个理由MQTT是长连接一次建连可反复上报数据避免频繁TCP握手带来的电量消耗和延迟支持订阅/发布模型天然适合云端下发命令控制LED和风扇平台侧对MQTT的支持完善数据流展示和API调用都基于统一的数据点模型。创建设备的过程不复杂但容易配置错。登录OneNET控制台后进入“多协议接入”页面选择MQTT旧版协议注意新版OneNET Studio的设备接入方式略有不同然后依次完成创建产品时“产品ID”会自动生成这个ID在设备接入时要写进MQTT的ClientId中设备注册后设备ID、APIKey都会作为身份标识。需要特别留意的是在MQTT接入鉴权方式中password是APIKey而不是设备注册时的密码搞混的话会一直报401或者connected refused。提示OneNET的MQTT服务器地址和端口为183.230.40.39:6002主题格式为topic和$creq/{device_id}。如果使用新版Studio接入点和鉴权方式会改变建议以控制台给出的参数为准。3.2 ESP8266的AT指令配置与TCP连接ESP8266模块出厂时固件可能差异很大新买的模块建议先刷成官方AT固件版本至少在1.5.0以上保证AT指令集完整支持MQTT透传所需的TCP连接功能。ESP8266的AT指令配置步骤分为四步设置工作模式、连接Wi-Fi、建立TCP连接、开启透传模式。/* ESP8266 AT指令初始化序列 */ const char *AT_CMD[] { AT\r\n, // 测试AT指令是否响应 ATE0\r\n, // 关闭回显减少数据量 ATCWMODE1\r\n, // 1: Station模式连接外部路由器 ATCWJAP\WiFi_SSID\,\WiFi_PASS\\r\n, // 替换为实际WiFi账号密码 ATCIPSTART\TCP\,\183.230.40.39\,6002\r\n, // 连接OneNET MQTT服务器 ATCIPMODE1\r\n, // 开启透传模式 ATCIPSEND\r\n // 开始发送数据 };每发送一条指令后必须等待模块返回“OK”或“ERROR”再发送下一条不能用固定的HAL_Delay盲等因为连接Wi-Fi的耗时可能从1秒到10秒不等。我一般用状态机逐条发送每条指令等待超时时间设为5秒失败则重试3次。透传模式开启后MCU向串口发送的任何数据都会被ESP8266不加修饰地转发到TCP服务器这正好适合直接发送MQTT报文。需要退出透传时发送不加回车换行即可回到AT命令模式。3.3 数据点上报JSON格式与心跳保活OneNET MQTT协议的数据上报格式是JSON需要把数据封装在特定的数据流结构中。标准的JSON数据点格式如下{datastreams:[{id:temperature,datapoints:[{value:25.5}]},{id:humidity,datapoints:[{value:60.5}]},{id:smoke,datapoints:[{value:30.2}]}]}这个JSON中datastreams是数组每个元素包含id数据流名称和datapoints数据点数组value可以是整数、浮点数或字符串。OneNET会根据数据流名称自动建立时序数据库云端的折线图就是基于这些数据流绘制的因此id命名最好在创建数据流模板时就固定避免混用大小写导致图表里出现两条线。MQTT的数据上报需要先发送一条固定报头加可变报头的报文然后紧跟JSON数据。这里给出一个精简的报文构造函数/* 构造MQTT Publish报文并发送 */ void MQTT_PublishData(float temp, float humi, float smoke) { char json_buf[256]; sprintf(json_buf, {\datastreams\:[{\id\:\temperature\,\datapoints\:[{\value\:%.1f}]}, {\id\:\humidity\,\datapoints\:[{\value\:%.1f}]}, {\id\:\smoke\,\datapoints\:[{\value\:%.1f}]}]}, temp, humi, smoke); uint16_t json_len strlen(json_buf); uint8_t mqtt_pkt[320]; mqtt_pkt[0] 0x30; // MQTT PUBLISH报文QoS 0 if (json_len 128) { mqtt_pkt[1] json_len 2; // 剩余长度 topic长度 2字节topic属性 } mqtt_pkt[2] 0x00; // topic长度高字节 mqtt_pkt[3] 0x04; // topic长度低字节此处topic为topic memcpy(mqtt_pkt[4], topic, 4); // 上报主题 memcpy(mqtt_pkt[6], json_buf, json_len); // JSON数据体 HAL_UART_Transmit(huart3, mqtt_pkt, 6 json_len, 1000); }MQTT报文结构里第1字节0x30表示PUBLISH报文且QoS为0Fire and Forget第2字节是剩余长度——它表示后面所有字节的数量topic属性topic名称payload所以用json_len 2计算其中2字节是topic长度字段。若payload长度超过127剩余长度字段需要按MQTT协议规则扩展为多字节编码。OneNET要求设备每隔一定时间发送心跳保活报文否则会断开连接常见做法是每60秒发送一个长度为2字节的PINGREQ报文0xC0 0x00。4. 设备端控制逻辑阈值判定、风扇联动与OLED显示4.1 阈值判定与蜂鸣器报警的优先级设计当温湿度或烟雾浓度超过预设阈值时系统要同时完成三件事本地蜂鸣器发出报警声风扇启动排除烟雾或降低温度向OneNET上报报警状态。这个逻辑看似简单但阈值的取舍和报警去抖值得注意。如果DHT11采集的温度在阈值边缘抖动蜂鸣器会反复启停因此在判定超标时需要连续多次采样都超过阈值才算报警而不是单次触发。/* 超标检测连续3次采样超过阈值才触发报警避免抖动 */ uint8_t over_threshold_count 0; void Check_Alarm(float temp, float humi, float smoke) { if (temp 30.0f || smoke 40.0f) { over_threshold_count; if (over_threshold_count 3) { HAL_GPIO_WritePin(BUZZER_PORT, BUZZER_PIN, GPIO_PIN_SET); HAL_GPIO_WritePin(FAN_PORT, FAN_PIN, GPIO_PIN_SET); Set_OLED_Warning(1); // OLED切换到报警画面 } } else { over_threshold_count 0; HAL_GPIO_WritePin(BUZZER_PORT, BUZZER_PIN, GPIO_PIN_RESET); HAL_GPIO_WritePin(FAN_PORT, FAN_PIN, GPIO_PIN_RESET); Set_OLED_Warning(0); } }阈值选择需要结合场景普通室内环境温度超过30℃比较合理而烟雾浓度阈值取决于传感器的标定基准。还有一个细节是蜂鸣器和风扇的驱动能力——C8T6的GPIO输出电流最大约25mA直接驱动无源蜂鸣器和风扇是不够的蜂鸣器要串联一个限流电阻100~220Ω风扇则必须通过S8050三极管或AO3400 MOS管驱动并在电机两端并联一个1N4007二极管吸收反向电动势。否则启动瞬间的冲击电流可能导致MCU复位这是硬件层面最常见的坑。4.2 从云端下发命令控制LED与风扇OneNET平台下发命令的方式是设备订阅指定的主题平台向该主题发布消息。设备端需要主动订阅$creq/{device_id}主题这样平台发出的命令才能被ESP8266接收并透传给STM32。MCU收到串口数据后从中解析出命令内容和JSON格式提取出LED开关或风扇开关的字段。/* 解析云端下发的控制命令 */ void Parse_Cloud_Command(uint8_t *buf, uint16_t len) { // 预期收到格式: {led:on} 或 {fan:off} if (strstr((char *)buf, \led\)) { if (strstr((char *)buf, \on\)) { HAL_GPIO_WritePin(LED_PORT, LED_PIN, GPIO_PIN_SET); } else { HAL_GPIO_WritePin(LED_PORT, LED_PIN, GPIO_PIN_RESET); } } if (strstr((char *)buf, \fan\)) { if (strstr((char *)buf, \on\)) { HAL_GPIO_WritePin(FAN_PORT, FAN_PIN, GPIO_PIN_SET); } } }这里用strstr做的是简单子串匹配优点是代码量小缺点是如果JSON中led:on和fan:off同时出现可能出现误判。实际工程中更严谨的做法是用cJSON库解析但这会为C8T6的ROM带来大约5KB的开销对于64KB Flash的F103C8T6来说还能接受。解析时要注意一个是缓冲区溢出ESP8266透传的数据长度可能超过256字节在串口接收中断中最好使用环形缓冲主循环中再逐字节解析不要把解析逻辑放在中断上下文里做。OneNET命令下发的数据经过ESP8266透传后会混合Wi-Fi模块的自身打印如IPD前缀一起发给MCU。如果开启了透传模式ESP8266不会添加IPD头但如果工作在上述AT模式而非透传模式收到的数据会带IPD,len:前缀解析时要先跳过这段再进行JSON解析。4.3 OLED显示与本地调试信息融合OLED屏在本项目中承担两层职责正常状态下轮播温湿度和烟雾浓度数值报警状态下显示哪一项超标并配合蜂鸣器。0.96寸OLED的分辨率是128x64像素SSD1306驱动的显存是1KB在I2C模式下全部刷新一次约需256个字节的数据传输耗时在毫秒级别不会影响主循环的实时性。SSD1306的驱动有几类常见封装如果使用的是u8g2库那么它占用的RAM比较大C8T6的20KB SRAM需要留意如果是自己移植的中景园驱动则显存缓冲可以只定义8页中的若干页。我的做法是只定义一个128x8字节的局部缓冲区每次只更新变化区域。DHT11的采集频率保持2秒一次即可OLED刷新频率和它同步避免频繁I2C写入导致DHT11的时序被干扰。OLED在显示浮点数时注意格式化输出占位符%d.%d方式拆分整数和小数部分比直接%f更稳定因为部分固件库的printf不支持浮点需要开启microlib并设置-u_printf_float链接选项才支持%f。5. 数据链路验证与调参技巧从串口日志到云端数据校准调试这个项目时建议分三段验证链路第一段用USB-TTL串口工具直接连接ESP8266单独调试AT指令和MQTT连接第二段把C8T6的USART3引到USB-TTL上观察MCU发给ESP8266的原始数据确认报文格式第三段才把三端串起来通过OneNET平台的设备日志验证完整链路。第一段调试中发送ATCIPSTARTTCP,183.230.40.39,6002后如果返回CONNECT FAIL先检查Wi-Fi是否已连接发送ATCWJAP?确认IP地址是否分配成功再检查服务器的端口是否被本地防火墙拦截。ESP8266获取到局域网IP后用电脑在同一路由器下ping服务器IP能通再回到AT指令排查。TCP连接成功后发送MQTT CONNECT报文OneNET会在连接成功后返回CONNACK如果收到的是非0x00的返回码对照MQTT协议状态表去检查ClientId/username/password三项鉴权信息。第二段验证中在C8T6侧添加一个调试开关用宏定义的方式决定是否把JSON报文通过USART1调试串口镜像输出。OneNET上报成功后如果云端数据流不显示优先确认JSON键名是否与控制台创建的数据流ID完全一致包括大小写和空格。OneNET的时序数据库对时间戳很敏感设备上报时如果没带时间戳平台会以接收时刻作为记录时间但如果设备本地的时钟比服务器慢新数据可能被平台以“过期”为由拒绝——解决方案是上报JSON的datapoints数组中显式添加t: 毫秒时间戳字段。第三段验证最常遇到的问题集中在云端下发命令。先在OneNET控制台的“设备日志”里触发一次命令下发观察ESP8266的串口是否收到IPD头。如果ESP8266在透传模式下收不到下发数据检查设备是否订阅了正确的主题如果收到数据但MCU没有动作在解析函数入口设置一个断点查看原始buffer里的字符很可能是因为OneNET下发命令时额外包裹了一层转义字符\子串匹配失败后直接把JSON原样打印出来就能发现。最后一个调参技巧是MQ-2的预热漂移补偿。MQ-2在上电后前3分钟内输出电压会逐渐升高后回落如果在预热期间做过标定后续判断阈值会产生系统性偏差。我在代码里加了参数自校准流程设备上电后记录60秒内的ADC平均值作为基线阈值判断使用(当前值 - 基线)的差值这样即使传感器因为环境湿度变化导致基线漂移也不需要重新烧写固件只需要把校准后的基线值通过串口日志打印出来对照云端曲线即可判断是否合理。本文还有配套的精品资源点击获取