简介:一套覆盖STM32、ESP8266与OneNET云平台的物联网综合实训资料,面向物联网工程、嵌入式开发方向的初学者和竞赛选手。系统以STM32F103C8T6为主控,通过ESP8266连接OneNET,实时采集DHT11温湿度和MQ-2烟雾浓度,并在OLED屏显示;温湿度或烟雾超标时联动蜂鸣器、风扇与LED模拟控制,同时支持云端下发指令远程控制设备。资料共3436个文件,以C/H源码、Java程序、原理图与PCB设计、PDF文档及hex/bin固件为主,总大小354.46MB,还包含大量工程备份与图片资料,适合对照学习物联网设备端、云端接入和上位机开发。已有3267人学习下载,对于希望快速复现智能家居环境监测与远程控制项目的开发者,具有较高参考价值;无论是毕业设计、课程设计还是物联网入门练手,都能获得从硬件到云端的完整参考。
1. F103C8T6 接 ESP8266 上 OneNET:这个家用环境监测盒子到底卡在哪
先给结论:这套组合里最难的不是 DHT11 和 MQ-2 的驱动,而是 STM32F103C8T6 和 ESP8266 之间的 UART 串口对决。ESP8266 模块的 RX 引脚电平是 3.3V,而很多人的 USB-TTL 已经调到 5V,一接上去模块要么不发数据要么直接烧引脚;STM32 的串口默认时钟是 PCLK1,不同倍频配置下 BaudRate 的误差能到 3%,时间一长 OneNET 侧收到的就是乱码。这个项目想跑通,本质上是把传感器采集、AT 指令状态机、云平台数据点上报和本地 LED 告警四条链路串在一起,其中任何一条不闭环都会让整个盒子看起来“点上电但不动”。下面按从传感器到云端的顺序把方案过一遍,硬件选型就用最常见的:F103C8T6 最小系统板、ESP8266-01S、DHT11 模块、MQ-2 模块,以及一个 GPIO 控制的 LED 模拟室内灯。
2. DHT11 和 MQ-2 在 F103C8T6 上的采集:HAL 库时序和 ADC 阈值的真实边界
2.1 HAL 库驱动 DHT11 的单总线时序怎么不踩坑
DHT11 是单总线协议,数据线只靠一根线完成主机控制和从机应答。常见错误是把 DHT11 当 I2C 或者直接拉高就读。实际上主机要先把总线拉低至少 18ms 启动,释放后等待从机拉低 80us 再拉高 80us 应答,然后从机连续发送 40 位数据。每一位的区分靠高电平持续时长:高电平持续约 26~28us 表示“0”,70us 左右表示“1”。用 STM32 HAL 库做,关键在于 GPIO 方向切换和延时精度。
// DHT11 数据线接 F103C8T6 的 PB0,开漏输出模式,外部加上拉电阻 #define DHT11_PORT GPIOB #define DHT11_PIN GPIO_PIN_0 void DHT11_Delay_us(uint16_t us) { // 使用 DWT 延时,避免 HAL_Delay 只支持 ms 精度 CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; DWT->CYCCNT = 0; DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; uint32_t start = DWT->CYCCNT; while (DWT->CYCCNT - start < us * (SystemCoreClock / 1000000)); } uint8_t DHT11_ReadByte(void) { uint8_t value = 0; for (int i = 0; i < 8; i++) { while (HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) == GPIO_PIN_RESET); // 等低电平结束 DHT11_Delay_us(40); if (HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) == GPIO_PIN_SET) { value |= (0x80 >> i); // 40us 后仍为高,判定为“1” } while (HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) == GPIO_PIN_SET); // 等高电平结束 } return value; }这段代码的关键是DHT11_Delay_us不能直接用HAL_Delay,后者在 DWT 配置不当的情况下最小单位只能到毫秒,每一位的判断都会偏掉。while (HAL_GPIO_ReadPin(...) == GPIO_PIN_RESET)这个等待循环必须放在第一位判断之前,因为从机在发送每一位之前有一个 50us 的低电平起始位,如果不等它结束,后续位时序会整体漂移。
2.2 DHT11 数据校验和湿度整数位的处理
DHT11 一次返回 5 个字节:湿度整数、湿度小数、温度整数、温度小数、校验和。校验和是前四个字节相加取低 8 位,HAL 驱动里必须在读完 40 位后立刻校验,否则一次错位会让后续所有数据失真。
uint8_t dht11_data[5]; uint8_t checksum = (dht11_data[0] + dht11_data[1] + dht11_data[2] + dht11_data[3]) & 0xFF; if (checksum != dht11_data[4]) { // 返回错误码,上位逻辑直接丢弃本次采样,不能缓存旧数据 return HAL_ERROR; }注意 DHT11 的湿度整数位和小数位不是十进制关系,通常模块返回的湿度小数位一直为 0。如果你需要更高的湿度分辨率,要么换 DHT22(AM2302),要么在应用层把湿度整数位直接作为最终值。F103C8T6 内部有硬件 CRC 外设,但 DHT11 的校验算法就是简单的字节累加,用硬件 CRC 反而要额外配置多项式,得不偿失。
2.3 MQ-2 测烟雾浓度:不是读 ADC 就完事
MQ-2 的模拟量输出 AO 引脚接到 F103C8T6 的 PA0,使用 ADC1 的通道 0。模块上电后需要预热,很多人的数值一直在漂,就是因为加热电阻还没到工作温度。MQ-2 的敏感体在冷态和热态下电阻差很大,常见做法是上电后先等 60 秒,再开始采集。下面是 ADC 配置和采样代码:
// PA0 配置为模拟输入,ADC1 采样序列里只有一个通道 ADC_HandleTypeDef hadc1; uint32_t mq2_value = 0; HAL_ADC_Start(&hadc1); for (int i = 0; i < 10; i++) { HAL_ADC_PollForConversion(&hadc1, 100); mq2_value += HAL_ADC_GetValue(&hadc1); HAL_Delay(10); } mq2_value /= 10; // 中值滤波,消除瞬时抖动 // 换算电压:3.3V 参考电压,12 位 ADC float voltage = (mq2_value * 3.3f) / 4095.0f;10 次采集中值滤波是必要的,因为 MQ-2 的 AO 输出在气体浓度稳定后仍有微小的波动,尤其烟雾刚飘到传感器附近时会出现尖峰。voltage并不是烟雾浓度的线性映射,MQ-2 在手册里给出的是灵敏度曲线,不同可燃气体斜率不同。实际工程里常见方案是:模块在干净的空气中会输出一个基础电压(通常在 0.3V 左右),你把当前电压减去这个基础电压,再除以基础电压得到一个相对变化率。烟雾超标阈值设定在这个相对变化率超过 0.5 时触发,而不是直接用绝对电压。
2.3.1 MQ-2 数字量输出 DO 能不能用
MQ-2 模块上还有一个 DO 引脚,它通过板载电位器调节比较器阈值,输出 0/1 数字量。如果上位机只做一个粗略的烟雾报警,直接用 DO 接 F103C8T6 的 GPIO 更省事,但要在线调节阈值时必须拧电位器,没法在代码里改。我的建议是 AO 接 ADC,用代码里的voltage_threshold常量做阈值,这样云平台升级时可以直接下发阈值参数,不用拆盖子去动螺丝刀。
3. ESP8266 与 F103C8T6 串口通信:从 AT 指令到 OneNET 数据点上报
3.1 F103C8T6 和 ESP8266 的硬件连接与电平匹配法则
这一节是整篇文章里最容易烧板子的一步。ESP8266-01S 的 VCC 是 3.3V,CH_PD(EN)要拉高,GPIO0 在运行状态下必须悬空或接高电平,GPIO2 保持悬空即可。STM32F103C8T6 的串口 TX 是 PA9,RX 是 PA10,要接 ESP8266 的 RX 和 TX,而不是同名互连。很多人的印象是“TX 接 RX”,在板载 CH340 的 USB-TTL 上成立,但到了两块开发板之间就糊涂了。这里明确:F103C8T6 的 PA9(USART1_TX)接 ESP8266 的 RXD,F103C8T6 的 PA10(USART1_RX)接 ESP8266 的 TXD。
// 使用 USART1,对应引脚 PA9 和 PA10 // 注意:不要额外给 ESP8266 的 RX 引脚接上拉,否则拉高电平可能让模块误判为命令输入 GPIO_InitStruct.Pin = GPIO_PIN_9 | GPIO_PIN_10; GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);另外,如果你的 F103C8T6 最小系统板是 5V 供电,ESP8266 绝不能从板子的 5V 引脚取电,必须用独立的 3.3V LDO。ESP8266 在 Wi-Fi 发射时峰值电流可以到 300mA,F103C8T6 板载的 AMS1117-3.3 通常只能稳定输出 500mA,勉强够用,但如果同时给 DHT11 和 MQ-2 供电,建议直接用外部 3.3V 电源,共地即可。
3.2 ESP8266 的 AT 指令配置:静态 IP 还是 DHCP
ESP8266-01S 出厂固件一般是 AT 指令版本,连接 OneNET 之前先要让它联网。常见做法是让 ESP8266 进入 Station 模式,连接家里的路由器。下面是一套稳定的 AT 指令序列,建议在串口助手里面先手工跑一遍,再写进 STM32 代码:
AT+RST // 复位,等待 Ready,大约 2 秒 AT+CWMODE=1 // 1 表示 Station 模式,ESP8266 作为终端连接外部 AP AT+CWJAP="你的WiFi名","你的密码" // 连接 Wi-Fi,等待 WIFI GOT IP 返回 AT+CIFSR // 查询 ESP8266 获取到的 IP,确认联网成功注意AT+CWJAP这条指令在路由器信号弱的情况下可能返回WIFI DISCONNECT,STM32 侧要设计重试机制,一般做法是每 5 秒重试一次,最多 3 次后让 LED 以 500ms 周期闪烁提示网络错误。不要用AT+CWJAP的返回内容作为判断联网成功的唯一标准,以AT+CIFSR能返回非 0.0.0.0 的 IP 为准。
3.3 OneNET 生成 APIKey 与创建设备:HTTP 数据点接口的完整流程
OneNET 平台接入分两步:创建产品、给设备添加数据流。旧版控制台里,你需要登录平台后选择“多协议接入”,创建一个 HTTP 协议的产品,然后在产品下添加设备。设备创建成功后页面会显示设备 ID 和 APIKey。APIKey 是设备上报数据的身份凭证,本质上是一个 HTTP 请求头里的字段。新版 OneNET Studio 的接入方式和旧版略有差异,但这个项目的核心逻辑不变:上报数据点要通过http://api.heclouds.com/devices/{device_id}/datapoints这个接口。
HTTP 上报数据点的请求格式(用串口助手或者 STM32 直接发): POST /devices/123456/datapoints HTTP/1.1 Host: api.heclouds.com api-key: 你的APIKey Content-Length: 69 Content-Type: application/json {"datastreams":[{"id":"temp","datapoints":[{"value":27.5}]}]}datastreams里嵌套的数组结构容易出错。每一个id对应 OneNET 控制台里的一个数据流名称,比如温度流的 id 设为temp,湿度流设为humi,烟雾流设为smoke。上报时可以把三个数据流放在同一个请求里,JSON 里的datapoints是个数组,value 类型可以是浮点数也可以是整数。OneNET 的 HTTP 接口返回的 JSON 里errno为 0 表示成功,非 0 时通常是api-key头部拼错或者设备 ID 不匹配。
3.3.1 ESP8266 通过 AT 指令直接 POST 数据到 OneNET
如果 F103C8T6 先读传感器,再把数据通过串口透传给 ESP8266,效率很低,常见做法是:STM32 负责拼好完整的 HTTP 请求字符串和 JSON 数据,然后通过 AT 指令让 ESP8266 建立 TCP 连接到api.heclouds.com的 80 端口,再透传发送整个 HTTP 报文。串口波特率建议 115200,因为 ESP8266 固件默认在这个速率下工作,F103C8T6 的 USART1 也配成 115200 才能避免乱码。
// STM32 侧用 sprintf 拼 HTTP 请求 char request[512]; int len = sprintf(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, (int)strlen(json_body), json_body); // 先发 AT 指令建立 TCP 连接 send_at("AT+CIPSTART=\"TCP\",\"api.heclouds.com\",80", "CONNECT", 5000); // 通知 ESP8266 接下来要发送的数据长度 char cipSendCmd[32]; sprintf(cipSendCmd, "AT+CIPSEND=%d", len); send_at(cipSendCmd, ">", 3000); // 发送 HTTP 请求正文 send_raw((uint8_t *)request, len); // 等待 OneNET 返回 HTTP 响应,最后发送 AT+CIPCLOSE 关闭连接 send_at("AT+CIPCLOSE", "OK", 2000);AT+CIPSEND这里不写长度会让 ESP8266 进入无限等待状态,必须传入待发送字节数。HTTP 请求里的Content-Length要精确等于 JSON 字符串长度,不能多一个换行符,否则 OneNET 的 HTTP 解析会报 400。我在调试时一般把request通过串口打印出来,直接复制到 PC 上用curl测试,确认 OneNET 接口本身没问题,再回头查 STM32 的发送逻辑。
4. 温湿度和烟雾超标联动 LED 室内灯:云平台触发器与本地 GPIO 双保险
4.1 LED 模拟室内灯的 GPIO 控制逻辑
F103C8T6 板载 LED 通常接在 PC13 引脚,但“室内灯”语义是模拟场景,建议外接一个 LED 到 PB1,用三极管驱动不限流,这样逻辑和实际照明灯更接近。LED 的开关状态由两个条件决定:用户手动开关标志manual_light_on和超限自动开启标志alarm_triggered。这两个标志位用按位或合成最终控制信号,当alarm_triggered为 1 时,不管手动开关状态如何,LED 都必须点亮,这是安全逻辑,优先级最高。
#define LAMP_PIN GPIO_PIN_1 #define LAMP_PORT GPIOB // 超限标志由 DHT11 和 MQ-2 的阈值判断共同驱动 if ((temp > 30.0f) || (humi > 70.0f) || (voltage > 1.2f)) { alarm_triggered = 1; } else { alarm_triggered = 0; } if (alarm_triggered || manual_light_on) { HAL_GPIO_WritePin(LAMP_PORT, LAMP_PIN, GPIO_PIN_SET); } else { HAL_GPIO_WritePin(LAMP_PORT, LAMP_PIN, GPIO_PIN_RESET); }需要注意的是“温度大于 30 度触发灯亮”只是模拟,真实室内灯不会因为温度高就自动开,但这个项目的课程设计场景里,煤气管道的联动灯就是个常见展示逻辑。实际部署时阈值应该放在常量区,方便后期通过 OneNET 的下发指令远程修改。
4.2 OneNET 端触发器的配置和 HTTPS 回调
除了本地 STM32 的告警逻辑,OneNET 平台侧还提供了一个触发器功能。在 OneNET 控制台的“触发器”菜单里,选择目标设备的数据流,设定条件类型:大于或小于某个阈值。当满足条件时,平台会向预设的 URL 发送一个 POST 请求。这个 URL 可以是服务器的接口,也可以对接第三方消息推送。常见做法是让触发器把超限信息用 POST 方式推给一个轻量级服务,服务端收到后再往微信公众号或者钉钉群发消息。
OneNET 触发器配置路径:选择产品 → 设备 → 数据流标签页 → 触发器管理 → 添加触发器。触发条件里有一个“数据点值”的选项,当上报的值超过阈值时触发。注意 OneNET 触发的判断周期取决于你的上报频率,如果 STM32 每 10 秒上报一次数据,那么触发器的判断也是最多 10 秒一次,不是秒级实时。另外,触发器回调 URL 需要公网可以访问,如果你在内网测试,可以用内网穿透工具把本地端口暴露到公网,但注意不要把回调 URL 写成 PC 的局域网地址。
4.3 超限告警上报时的数据格式设计
OneNET 的数据点 value 只支持基础类型,所以你不能直接把“超标”这个布尔值塞进去。常见做法是定义一个新的数据流alarm,每次上报时把警告状态编码为整数:0 表示正常,1 表示温度超标,2 表示湿度超标,3 表示烟雾超标,4 表示多项超标。这样在 OneNET 的折线图里可以很直观地看到告警变化,也能在触发器里只对alarm数据流做阈值判断,避免为温湿度和烟雾各写一条触发规则。
// JSON body 示例 { "datastreams": [ {"id": "temp", "datapoints": [{"value": 31.2}]}, {"id": "humi", "datapoints": [{"value": 68.5}]}, {"id": "smoke", "datapoints": [{"value": 0.92}]}, {"id": "alarm", "datapoints": [{"value": 4}]} ] }这里alarm值为 4 表示温度超温且烟雾超标同时发生。要注意浮点数在 JSON 序列化时不要用%f直接拼,因为不同编译器的浮点格式差异会让 OneNET 收到nan或inf字符串导致接口报错。STM32 的sprintf对浮点支持依赖新库,建议用整数运算放缩:温度乘以 10 存整数,上报时以浮点数解析,或者用dtostrf函数转字符串。
5. OneNET 折线图调参和上报踩坑:APIKey 失效与设备 ID 不一致
5.1 上报后用 curl 自查 OneNET 数据点状态
在 STM32 烧录之前,先用 PC 的 curl 命令模拟上报一次,把问题隔离在云平台配置这一侧。OneNET 的 HTTP 接口返回体里有个关键的errno字段,0 代表成功,其他值各有含义:20表示 APIKey 无效,21表示设备不存在或者设备 ID 不对,22表示数据流不存在,23表示 JSON 格式非法。你在串口助手或者 curl 输出里看到这些编号,不需要猜。
curl -X POST \ http://api.heclouds.com/devices/123456/datapoints \ -H 'api-key: 你的APIKey' \ -H 'Content-Type: application/json' \ -d '{"datastreams":[{"id":"temp","datapoints":[{"value":27.5}]}]}'返回{"errno":0,"error":"succ"}时,平台侧数据流已经多了一个点。如果返回错误,优先检查device_id是不是控制台里设备列表页的数字 ID,不是你自己命名的字符串,APIKey 需要从设备详情页里复制,产品级的 APIKey 有时和设备级不通用。
5.2 F103C8T6 串口乱码和 ESP8266 连不上 OneNET 的排查顺序
一套常见的排查顺序:先确认 STM32 的串口输出是不是乱码。乱码根源通常是 USART1 的波特率配置和 ESP8266 固件不一致,还有系统时钟源切换导致的波特率误差。F103C8T6 的 HSI 是 8MHz,如果你用 HSI 作为系统时钟,USART1 挂在 APB2 上,波特率计算会有较大偏差,所以一定要用外部晶振 HSE 倍频到 72MHz,然后单独配置 USART1 的时钟源为 PCLK2。
如果串口数据正常、ESP8266 能连 Wi-Fi 但连不上 api.heclouds.com,就要用AT+PING=api.heclouds.com测试域名解析和 ICMP 连通性。ESP8266-01S 内部 DNS 解析有时候会失败,这时可以在 AT 指令里把要连接的域名显式写成 IP,但 OneNET 的 IP 不稳定,不建议写死。更多时候问题出在AT+CIPSTART没等到 CONNECT 就发送数据,可以用一个超时状态机来管理,发送 AT 指令后等待响应的时间不能少于 5 秒,因为 TCP 拨号建立连接期间 ESP8266 内部会切换模式,响应变慢。
5.3 上报失败时 LED 闪烁频率表达的故障码
本地 LED 不只用来模拟室内灯,也可以复用作为故障指示。常见做法是用闪烁次数表示错误类别:LED 闪 1 次停 2 秒表示 DHT11 读取失败,闪 2 次表示 MQ-2 预热未完成,闪 3 次表示 ESP8266 连接 Wi-Fi 超时,闪 4 次表示 OneNET HTTP 上报返回非零 errno。注意这些一闪一停的状态会与室内灯逻辑冲突,所以要单独用一个 GPIO 接一个调试 LED,不要把两者混在同一个引脚上。
// 故障码展示伪代码,blink_count 表示第几种故障,间隔 2s 重复 void show_error_code(uint8_t code) { for (int i = 0; i < code; i++) { HAL_GPIO_WritePin(ERROR_LED_PORT, ERROR_LED_PIN, GPIO_PIN_SET); HAL_Delay(200); HAL_GPIO_WritePin(ERROR_LED_PORT, ERROR_LED_PIN, GPIO_PIN_RESET); HAL_Delay(200); } HAL_Delay(1800); }如果你在 OneNET 云端已经能看到数据点,但就是没有触发告警,大概率是触发条件和数据流 id 对不上。OneNET 触发器监控的是数据流级维度,你配置了temp > 30,但上报用的是新数据流temperature,那永远触发不了。多数据流时,建议上报前打印完整 JSON 到串口,人工确认每个 id 和控制台上的名字完全一致,包括大小写。LED 故障码只能走到这一层,最终的数据匹配还得靠日志。
本文还有配套的精品资源,点击获取