简介:基于STM32与ESP8266接入OneNET平台的实时环境监测系统设计资料,包含完整源码与工程文件,面向嵌入式开发学习者、电子类毕业设计及课程设计学生。系统以STM32C8T6为核心,集成DHT11温湿度、MQ-2可燃气体、GY302光照传感器,通过OLED屏实时显示,并借助ESP8266连接OneNET云平台实现数据上传,同时支持蓝牙透传与串口通信,方便二次开发与功能扩展。压缩包共278个文件,约7.87MB,主体为C语言源码(.c)、头文件(.h)及Keil工程文件(.uvprojx、.uvoptx),另含编译产物(.o、.axf、.hex)、备份文件(.zbak)和技术文档(.md、.txt),目录结构清晰,便于定位核心代码与烧录文件。目前已有105人学习。资料经过完整验证,提供工程配置、引脚定义和固件烧录文件,可直接导入Keil编译运行,也可参考其模块化编写思路自行改造。实现细节涵盖传感器初始化、OneNET接入协议封装与OLED驱动,适合对照学习嵌入式系统与物联网云平台对接方法,特别适用于工程项目实践、毕设选题和创新竞赛等场景。
1. 一套能远程看数据的实时环境监测系统:STM32、ESP8266 与 OneNET 的分工
给机房、花房或者实验室做一套能实时看温度和湿度的装置,传统做法是买一台带屏幕的温湿度计,再好一点是 USB 数据记录仪。这类设备的问题都一样:数据只存在本地,人不在现场就什么都看不到。用 STM32 采集传感器、ESP8266 负责联网、OneNET 云端做存储和可视化,这套基于 STM32 与 ESP8266 接入 OneNET 平台的实时环境监测系统,把采集、传输、存储三个环节拆开,每一层都能单独替换和调试。硬件成本能控制在 50 元以内,源码和工程文件都在手,课程设计、毕业设计、小成本产品原型都能直接拿来改。这篇按硬件选型、STM32 端采集、ESP8266 接入 OneNET、故障排查一路讲到底,照着做半天内就能跑通第一版数据上云。
2. 硬件选型与整体架构:为什么是 STM32F103 + ESP8266 而不是单芯片方案
2.1 主控与 WiFi 模块的分工逻辑
最常见的搭配是 STM32F103C8T6(Blue Pill 小板)加 ESP8266-01S 或 ESP-12F。选 STM32F103 的理由很实际:CubeMX 生成工程快,标准库和 HAL 库资料最全,网上随便搜都是现成的 ADC、DHT11、串口参考代码。课程设计答辩时老师说“换个芯片你还能写吗”,你能把外设驱动重新捋一遍,这是用 ESP32 单芯片方案很难展示的能力。
ESP8266 在这里的角色是网络协处理器,不跑业务逻辑。STM32 通过串口发 AT 指令让 ESP8266 连 WiFi、建 TCP 连接、发 HTTP 报文;STM32 自己只管传感器采集、数据打包和 GPIO 控制。这样分层的直接好处是:WiFi 协议栈的复杂度被隔离在 ESP8266 内部,STM32 的资源全花在采集和控制上,出问题也好定位——串口日志一看就知道是网络层挂了还是传感器层挂了。
有人会问,现在 ESP32 这么便宜,为什么不用 ESP32 一颗芯片全干?答案是可以,但偏离了“基于 STM32 与 ESP8266”的项目结构。用 ESP32 时 OneNET 的 SDK 直接跑在 ESP32 上,所有代码挤在一个芯片里,硬件成本大概能省 10 块钱,但整个系统的层次感没了。如果这项目是产品原型,后期要换国产主控或加 STM32 做电机控制,ESP8266 透传方案反而是更好迁移的架构。
2.2 传感器选型:单总线、ADC 与 I2C 三类信号链
实时环境监测系统最少要覆盖温湿度,再加一路空气质量和一路光照就比较完整了。我常用的三路信号链如下表:
| 传感器 | 型号 | 接口 | 输出形式 | 注意事项 |
|---|---|---|---|---|
| 温湿度 | DHT11 | 单总线 GPIO | 40 bit 数字 | 读取间隔必须大于 1 s,精度一般 |
| 空气质量 | MQ-135 | ADC | 0~3.3 V 模拟电压 | 上电预热 30 s 后读数才稳 |
| 光照 | 光敏电阻模块 | ADC | 0~3.3 V 模拟电压 | 模块上带电位器可调阈值 |
如果对温湿度精度有要求,把 DHT11 换成 SHT30,走 I2C 接口,库函数一换就行,代码结构不变。MQ-135 模块输出的是模拟量,接 STM32 的 ADC 引脚。注意买 3.3 V 供电的模块版本,别买 5 V 的,否则分压后超出 ADC 量程还要加电阻分压,徒增麻烦。
2.3 数据流向与工程文件组织:从传感器到云端的路径
数据流向分上行和下行两条。上行:传感器 → STM32 采集并封装 → USART1 → ESP8266 → WiFi → OneNET HTTP 接口 → 平台存储 → 可视化页面。下行:OneNET 平台下发命令 → ESP8266 收到+IPD包 → 串口发给 STM32 → STM32 解析后控制继电器或蜂鸣器。理解这条链路,后面无论改哪个环节都知道去哪查日志。
工程文件组织上,我习惯把 CubeMX 生成的代码和业务代码分开,避免下次重新生成工程时把业务逻辑覆盖掉。目录大致是:Core放 HAL 库和系统文件,Driver放 bsp_dht11.c、bsp_adc.c 这类板级驱动,App放数据采集上传任务和命令解析,User放 main.c 和中断回调。源码里先看main.c,再看App/app_upload.c,大概就能摸清整个程序脉络。
3. STM32 端采集与串口协议:DHT11、ADC 多通道与一帧数据的封装
3.1 CubeMX 初始化:时钟树与串口参数的设置
开始写代码前先把 CubeMX 配置对,否则后面全是坑。我一般这样配:HSE 外部 8 MHz 晶振,PLL 倍频到 72 MHz 主频;USART1 用 115200-8-N-1,接 ESP8266;USART2 也用 115200,作为调试日志输出口,专门打印 AT 指令交互过程和云端返回内容。DHT11 数据引脚用 PA8,配置为开漏输出带上拉,这样模拟单总线时序时拉低拉高的电平才干净。
ADC1 开三个通道:PA0 接 MQ-135,PA1 接光敏电阻,PA2 留作备用模拟输入。ADC 时钟设 12 MHz(最大 14 MHz),采样时间选 55.5 周期,精度和速度的平衡点最好。在 CubeMX 里还要把 USART1 的全局中断打开,因为后面要接收 OneNET 下发的命令,中断接收比轮询可靠得多。
3.2 DHT11 读取:起始时序与 40 bit 数据的位级解析
DHT11 是单总线协议,最容易出错的是时序。主机先把总线拉低至少 18 ms 再释放,DHT11 收到起始信号后回一个 80 us 低电平加 80 us 高电平。之后连续发 40 bit 数据:每 bit 先拉低 50 us,然后拉高,高电平持续 26~28 us 表示 0,持续 70 us 表示 1。40 bit 里湿度高 8 位、湿度低 8 位、温度高 8 位、温度低 8 位、校验和。
uint8_t DHT11_Read(float *temp, float *humi) { uint8_t data[5] = {0}; uint8_t i, j; // 主机起始信号:拉低至少 18ms HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, GPIO_PIN_RESET); HAL_Delay(20); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, GPIO_PIN_SET); delay_us(30); // 释放总线 20~40us,等待从机响应 if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_8) == GPIO_PIN_SET) return 1; // 从机没响应,引脚被外部拉高 while (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_8) == GPIO_PIN_RESET); // 等待 80us 低电平结束 while (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_8) == GPIO_PIN_SET); // 等待 80us 高电平结束 for (j = 0; j < 5; j++) { for (i = 0; i < 8; i++) { while (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_8) == GPIO_PIN_RESET); // 每 bit 前 50us 低电平 delay_us(35); // 高电平 35us 后判断:仍为高则是 1,否则为 0 if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_8) == GPIO_PIN_SET) { data[j] |= (0x80 >> i); while (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_8) == GPIO_PIN_SET); // 等剩余高电平结束 } } } if (data[4] != (data[0] + data[1] + data[2] + data[3]) & 0xFF) return 2; // 校验失败 *humi = data[0] + data[1] * 0.1; // 整数部分 + 小数部分 *temp = data[2] + data[3] * 0.1; return 0; }逻辑说明:delay_us需要用定时器或空循环实现微秒级延时,HAL_Delay 精度不够。整个读取过程必须关中断或只允许同优先级打断,否则时序一偏就是校验失败。读取间隔必须在 1 s 以上,DHT11 内部采样周期就是 1 s,连续读会被总线上的电平锁住,表现为数据永远不变或直接无响应。DHT11 本身就存在 ±2°C 的误差,系统标定时用标准温度计做两点校准比调代码更有效。
3.3 ADC 多通道采集:切换通道后必须处理的采样保持
ADC 多通道轮询最隐蔽的坑是通道切换后立刻读取,拿到的是上一个通道的残留值。STM32F103 的 ADC 只有一个采样保持电容,切换通道后需要等待电容充电到新通道的电压水平。采样时间设 55.5 周期大约 4.6 us,但传感器模块的输出阻抗通常不小,我习惯在切完通道后再加 10 us 的延时,保险起见用 1 ms 也不过分。
uint16_t ADC_ReadChannel(uint32_t channel) { ADC_ChannelConfTypeDef sConfig = {0}; sConfig.Channel = channel; sConfig.Rank = ADC_REGULAR_RANK_1; sConfig.SamplingTime = ADC_SAMPLETIME_55CYCLES_5; HAL_ADC_ConfigChannel(&hadc1, &sConfig); HAL_ADC_Start(&hadc1); HAL_ADC_PollForConversion(&hadc1, 10); uint16_t val = HAL_ADC_GetValue(&hadc1); HAL_ADC_Stop(&hadc1); delay_us(10); // 等待内部采样保持稳定 return val; }参数说明:每次读取前重新配置通道和采样时间,这种方法最慢但最稳,适合数据上报周期在秒级的环境监测。如果上报频率要提到 10 Hz 以上,改成 DMA 多通道循环采样,ADC_ReadChannel这种逐个配置的方式会被中断开销拖死。DMA 模式下还要注意 ADC 数据寄存器是 16 位,DMA 缓冲数组用uint16_t,取完数据后值是原始 ADC 码,换算公式是voltage = val * 3.3f / 4095.0f。
3.4 上行数据帧设计:HTTP JSON 由 STM32 直接拼还是分层拼
这是整个系统里我最想强调的决策点。很多人一开始想在 STM32 和 ESP8266 之间传自定义短帧,比如T:26.5,H:60.5,L:500,G:1800,让 ESP8266 去转成 OneNET 要的 JSON。但 AT 固件里的 ESP8266 只是个透传管道,它不会解析你的协议,所以最务实做法是 STM32 直接把整个 HTTP 报文拼好,通过 AT 指令转发。
char httpReq[512]; uint16_t len = sprintf(httpReq, "POST /devices/%s/datapoints?type=3 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" "{\"datastreams\":[" "{\"id\":\"temp\",\"datapoints\":[{\"value\":%.1f}]}," "{\"id\":\"humi\",\"datapoints\":[{\"value\":%.1f}]}," "{\"id\":\"light\",\"datapoints\":[{\"value\":%d}]}" "]}", deviceId, apiKey, jsonBodyLen, temp, humi, light);这里的jsonBodyLen不能用strlen(httpReq)直接算,因为 HTTP 头里的 Content-Length 只统计空行之后 JSON 部分的字节数。我一般先单独sprintf一个 body 字符串,算出长度,再拼完整报文。这样拼法有好处:整个 HTTP 报文在 STM32 内存里是一块连续缓冲区,发送时字节数可控,AT+CIPSEND=len和Content-Length能对齐,不会出现云端一直等数据的情况。
4. 让 ESP8266 稳定接入 OneNET:平台配置、AT 指令序列与命令下发的完整链路
4.1 OneNET 平台侧配置:产品、设备、APIKey 与数据流模板
OneNET 平台注册登录后,创建产品时选“多协议接入”里的 HTTP 协议,记录下产品 ID。然后在该产品下创建设备,设备接入方式选 WiFi,保存后能得到设备 ID 和 APIKey。APIKey 相当于设备的访问令牌,上报数据和用 HTTP API 读历史数据都靠它,丢了只能重置,代码里记得留个注释备份位置。
平台侧建议提前建好数据流模板:temp、humi、light、gas四条数据流。虽然设备第一次上报时平台会自动创建数据流,但提前建好模板后,后面做可视化面板时下拉框里直接能选,不用手动输名字。数据流标识命名要全小写,别用驼峰,OneNET 的 API 路径和数据流名是大小写敏感的,这块最容易被忽略。
| 参数 | 获取位置 | 主要用途 |
|---|---|---|
| 产品 ID | 产品详情页 | API 路径中标识产品 |
| 设备 ID | 设备列表 | API 路径中标识具体设备 |
| APIKey | 设备详情页 | HTTP 头部鉴权 |
| 数据流标识 | 数据流模板 | JSON 中 id 字段 |
4.2 ESP8266 固件选择:AT 固件配合 STM32,NonOS SDK 留给独立节点
市面上 ESP8266 模块出厂多数带 AT 固件,但版本和波特率五花八门。接到 STM32 前,先拿 USB-TTL 串口模块单独连 ESP8266,用 PC 串口助手发AT,能回OK就继续用;如果回乱码,试 9600、74880 波特率。出厂固件太老的话,用乐鑫的 Flash Download Tool 重新烧写 AT 固件,IO0 拉低进入下载模式。
如果项目确实要让 ESP8266 自己完成 MQTT 接入或者直接读传感器,那才需要搭 ESP8266 NonOS 开发环境,用 SDK 写应用层。那种方案的优点是 ESP8266 可以独立工作,缺点是调试周期拉长,而且和“基于 STM32”的课程设计定位不符。我的建议是:只要 STM32 是主控,AT 固件透传就是最省事的路线。
4.3 接入 OneNET 的 AT 指令序列:从连接 WiFi 到 HTTP 上报
STM32 通过 USART1 和 ESP8266 交互,完整的接入序列如下:
AT+CWMODE=1 # 返回 OK,设置 Station 模式 AT+CWJAP="你的SSID","你的密码" # 返回 WIFI CONNECTED 后跳 WIFI GOT IP,说明拿到局域网 IP AT+CIPSTART="TCP","api.heclouds.com",80 # 返回 CONNECT 和 OK,建立到 OneNET 的 TCP 连接 AT+CIPSEND=310 # 返回 > 后,STM32 紧接着发上一节拼好的 HTTP 报文 # 发送完返回 SEND OK逻辑说明:AT+CIPSTART里可以直接填域名,新版 AT 固件支持 DNS 解析,老固件只认 IP 地址。如果手头固件解析域名失败,就去 OneNET 后台看当前接入地址的 IP,用 IP 替换域名。AT+CIPSEND=len里的 len 必须和 HTTP 报文总字节数一致,短了模块会一直等待补发,长了会多出半截垃圾数据导致解析失败。
上报周期我一般设 15 s 一次,兼顾实时性和平台限流。OneNET 免费版对单设备的上报频率有限制,1 s 一次大概率会被限流,表现为偶发SEND FAIL或服务器无响应。15 s 一次能保证可视化曲线的平滑度,也不会把自己平台配额跑满。
提示:发送完 HTTP 报文后别立刻断开 TCP,OneNET 的下发命令依赖这条长连接。断开重连虽然也能上报,但下发通道就断了。
4.4 命令下发与串口解析:让 STM32 真正响应云端操作
OneNET 平台向设备下发命令时,ESP8266 会收到+IPD,<长度>:<数据>格式的数据,并通过串口原样转发给 STM32。STM32 在 USART1 接收中断里做状态机解析,典型流程是:
void USART1_IRQHandler(void) { uint8_t ch; if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_RXNE)) { HAL_UART_Receive(&huart1, &ch, 1, 0); // 检测 +IPD 头部 if (state == 0) { if (ch == '+') state = 1; else if (ch == 'O' && ackState == 0) { /* 处理 SEND OK */ } } // 中间状态机逐字节匹配 "+IPD," else if (state >= 1 && state < 5) { if (ch == "+IPD,"[state]) state++; else state = 0; } // 匹配到 "CMD=" 后开始累计命令内容 else if (state == 5) { if (ch == 'C') { cmdIndex = 0; memset(cmdBuf, 0, 8); cmdState = 6; } else state = 0; } else if (cmdState >= 6) { cmdBuf[cmdIndex++] = ch; if (cmdIndex == 3 && strncmp(cmdBuf, "ON1", 3) == 0) { HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET); // 开继电器 cmdState = 0; state = 0; } } } }逻辑说明:+IPD头后面跟的可能是平台原始下发内容,也可能带CMD=前缀,取决于平台侧消息格式配置。解析时要兼容这两种情况,稳妥做法是把+IPD和冒号之间、冒号后的完整数据都进环形缓冲区,主循环里用strstr搜关键字,别在中断里做字符串比较。OneNET 下发中文内容时,链路里是 UTF-8 编码,而 STM32 代码里中文字符串字面量在 Keil 下默认是 GBK,直接strstr匹配会翻车。这个编码坑我踩过,最省事的规避方案是下发命令用纯英文和数字,比如ON1、OFF1。
5. 常见问题与排查:5 个让新手翻车的现场与解决路径
5.1 上电后 AT 不回 OK:电源与电平两个硬伤
现象是 STM32 程序跑了,串口发 AT 没任何应答,或者 ESP8266 上电瞬间测量供电电压只有 2.8 V 且模块反复重启。原因九成是供电问题。ESP8266 发射峰值电流超过 300 mA,STM32F103 核心板上的板载 LDO 电流余量不够,模块一联网电压就被拉垮。解决方法是外接独立的 AMS1117-3.3 稳压模块给 ESP8266 供电,注意把两边的 GND 接在一起,否则串口电平没有参考地,AT 指令还是发不过去。如果供电正常但依然无响应,检查 TX/RX 有没有接反,STM32 的 TX 必须接 ESP8266 的 RX,交叉连接。
5.2 执行 AT+Restore 后程序死等 OK:恢复出厂不等于重启
这个场景经常出现在“模块连不上网先恢复出厂试试”的调试过程中。现象是执行AT+Restore后,单片机循环等待 OK,永远等不到,卡死在循环里。原因是AT+Restore会把模块参数全部恢复默认值并自动重启,重启瞬间串口输出的是乱码或ready,不是规范格式的 OK;更隐蔽的是,之前如果把波特率改成了 9600,恢复出厂后模块回落到默认 115200,单片机还傻傻按 9600 读,自然收不到 OK。
解决方法是:第一,产品代码里不要调用AT+Restore,这指令是出厂调试用的,不是运行期软件复位;第二,所有 AT 等待都加超时计数,超时后重发AT+RST而不是无限死等;第三,真需要恢复出厂时断开单片机,用 USB-TTL 单独连模块操作,避免两个串口争抢。
5.3 数据上报失败服务器返回 400:Content-Length 和 API 路径逐一核对
现象是串口调试日志里能看到HTTP/1.1 400 Bad Request,但不确定哪里错。原因基本集中在三处:API 路径里设备 ID 写错、api-key头缺失、Content-Length和实际发送的 JSON 字节数不一致。其中 Content-Length 错得最多,因为很多人直接算整个 HTTP 报文长度,而规范只要求统计空行之后 body 的长度。
解决方法是先断开 STM32,用串口助手手动发一遍完整 AT 指令序列,把 HTTP 报文原样粘贴过去,看 OneNET 返回什么。返回{"errno":0,"error":"succ"}说明报文没问题,问题出在 STM32 发送逻辑;如果还是 400,响应体里会有具体错误字段,把串口收到的响应体完整打印出来,比盲改参数高效得多。
5.4 ADC 数据一动不动或满量程:通道切换与引脚悬空
现象是光照或气体通道的数值永远固定在最大值 4095,或者怎么变化都只有几个稳定值。原因有两类:一类是 ADC 切换通道后没有预留采样保持时间,采集到的还是上一通道残留值;另一类是传感器模块供电和地没接好,引脚等效悬空,读出来的就是随机满量程值。解决方法是切换通道后加 10 us 以上延时,这个在 3.3 节已经写了;另外把传感器模块的 VCC 和 GND 单独量一遍,确认是 3.3 V 而不是模块内部自带的杂散电压。最彻底的排查办法是拿一个 10 k 电位器,中间抽头接 ADC 引脚,手动旋钮看读数是否线性变化,能快速区分是 ADC 问题还是传感器问题。
5.5 云端有数据但页面不刷新:可视化控件绑定与缓存
现象是设备端日志显示上报成功,用 Postman 调 OneNET API 也能查到最新数据点,但平台上拖的可视化图表就是不更新。这不是设备问题,是面板配置问题。原因通常是可视化控件绑定的数据流名不对,比如数据流叫temp,控件里绑的是temperature,平台不会报错,只是图表永远空白。
解决方法是编辑可视化控件,确认数据源绑定到正确的数据流标识,然后把时间范围选成“最近 1 小时”或“实时”,不要用默认的“最近 24 小时”加聚合统计。OneNET 对历史数据会做降采样,时间范围设得太宽时新数据点会被曲线压缩得看不出来,看起来像“没刷新”。
6. 进阶:OneNET 可视化面板、触发器报警与低功耗改造
实时环境监测系统跑通上报后,真正能拿去演示和交付的,是 OneNET 平台上的可视化面板和报警触发器。在平台“应用管理”里新建应用,拖一个实时曲线控件绑定temp数据流,再拖一个仪表盘控件绑定humi,页面刷新就能看到实时数据随时间变化。OneNET 可视化支持自定义布局,把温湿度、光照、气体四个控件排在一屏,现场演示比拿串口助手刷屏直观得多。
报警这块,用平台触发器功能:新建触发器,数据流选temp,触发条件设为大于 35,动作选消息推送。这样即使没有人在看页面,温度越限也能收到通知。触发器的判断是平台端做的,设备端零代码改动,这也是选 OneNET 而不是自建服务器的现实理由。
如果这项目要往产品方向走,低功耗是个绕不开的优化点。常见做法是 STM32 和 ESP8266 联合睡眠:STM32 进入 STOP 模式,RTC 定时 15 s 唤醒一次,采集数据、通过串口唤醒 ESP8266、发完报文后让 ESP8266 执行AT+GSLP进入 sleep,再一起睡。理想情况下平均电流能压到 20 mA 左右,配合一节 18650 电池供电,能撑两到三天,勉强够演示用。想功耗再低就得换 MQTT 协议并关闭 WiFi 保活,那已经不是这个标题的范畴了。
最后说一个我交付样机时踩过的坑:设备连续运行三天后,OneNET 突然收不到数据,但设备本身没死机。排查到最后发现是 TCP 长连接被服务端断开,ESP8266 不知道,一直往死连接上发数据,每次都是SEND FAIL。从那以后我在上报函数里加了一行逻辑:连续三次SEND FAIL就主动AT+CIPCLOSE重连,并且用外部看门狗定时喂狗,超过 10 s 没喂就复位整个系统。这套从课程设计到小批量原型都能用的框架,值得你在源码上继续堆功能,希望帮到你。
本文还有配套的精品资源,点击获取