简介:这是一套基于STM32与ESP8266接入OneNET平台的实时环境监测系统源码工程,面向嵌入式开发者、毕设学生和课程实践者。系统集成DHT11温湿度、MQ-2可燃气、GY302光照传感器,通过OLED显示,支持蓝牙透传,并借助ESP8266将数据上传至OneNET云端,形成完整的远程环境监测方案。包内共278个文件,以C源码、H头文件、uvprojx工程和hex固件为主,另有编译中间文件与备份,大小7.87MB,可用Keil直接编译。代码包含STM32外设驱动、MQTT协议栈、ESP8266通信及传感器采集逻辑,经过实际测试。当前已有105人学习,适合毕业设计、电子竞赛和物联网项目二次开发,附完整工程与说明文档,可直接复现或扩展。
1. 基于STM32与ESP8266接入OneNET的实时环境监测系统:源码能直接跑通,坑也替你踩过了
这份资源是一套完整的STM32+ESP8266环境监测工程。主线很清楚:STM32负责采集数据,ESP8266负责上网,OneNET负责存数据和可视化。具体来说,STM32F103C8T6驱动DHT11温湿度、MQ-2烟雾浓度和光照检测模块,把读数打包成JSON,通过串口交给ESP8266;ESP8266用AT指令连上WiFi,以HTTP POST方式上报到OneNET云平台。你要做的只是改三处配置:WiFi账号密码、设备ID、API Key。它解决的最大痛点是“数据出来了但云端看不到”——源码里已经把传感器时序、串口协议、AT指令握手这些最费时间的部分写好了,适合正在做毕设、想从单片机上位机过渡到云端方案、或者想短时间搭出带看板的监测原型的从业者。
2. 系统架构与选型:为什么是STM32+ESP8266+OneNET
2.1 硬件选型:从传感器到通信模组
先讲明白选型逻辑,后面排错时才有依据。主控选STM32F103C8T6,这是入门项目里最稳的起点:两个串口(一个连ESP8266,一个留给调试)、三个ADC能同时挂多路模拟量、外部中断和定时器资源足够应付传感器时序,而且资料密度极高,遇到奇怪问题很容易找到同类案例。成本虽然比51高一点,但换来的是可复现性和调试体验,这套资源里所有代码都按这个芯片的习惯写法实现,换F103系列其他型号也能直接移植。
接口规划上也值得提前说清楚。我一般把USART1作为调试口,USART2专门接ESP8266,PB8留给DHT11,PA0和PA1分别接MQ-2和光照传感器。这样分配的好处是:调试口和数据口物理隔离,看日志不会干扰上报链路;ADC和单总线也不共用引脚,模块坏了不会互相拖累。很多新手喜欢把所有外设堆在一组GPIO上,结果一个传感器接线错误,整板都进调试模式,这是最容易避免的坑。
传感器端的选型看你要监测什么。这套工程里典型配置是三路:DHT11负责温湿度、MQ-2负责可燃气体浓度、光敏电阻负责光照强度。DHT11精度虽然只有±2℃和±5%RH,但在环境监测这种场景下完全够用,而且单总线协议只占一个GPIO,时序好控制;MQ-2输出的是模拟电压,直接接ADC,读数不用换算成PPM,变化趋势就能反映烟雾浓度;光敏电阻加分压电阻就更省事,拉一个AD口就行,成本几乎可以忽略。
| 传感器 | 接口 | 输出内容 | 读法 |
|---|---|---|---|
| DHT11 | 单总线 GPIO | 40bit温湿度数据 | 读时序加校验 |
| MQ-2 | 模拟 VCC | 0~3.3V 电压 | ADC采样 |
| 光敏电阻加分压 | 模拟 VCC | 0~3.3V 电压 | ADC采样 |
通信模组选ESP8266而不是直接上4G或NB-IoT,原因很实际:AT固件成熟、数据量小、价格便宜,OneNET对HTTP协议支持得也很完善。ESP8266-01S只有两个可用GPIO,但咱们只需要串口通信,两个GPIO绰绰有余。这里要提醒一下:ESP8266-01S的串口电平是3.3V,STM32也是3.3V,直接UART对接即可,不用加电平转换;如果换用其他型号模组,先确认电平一致再接线,否则串口数据会乱码。
2.2 OneNET接入方式:HTTP数据上报与数据流设计
OneNET的接入方式主要是HTTP和MQTT两种。标准AT固件的ESP8266默认不支持MQTT,需要刷带MQTT透传的固件,所以这份资源里选HTTP作为主链路更稳妥:AT指令建TCP连接、拼接POST报文、发送数据,任何固件版本都能跑,排查时也完全透明。MQTT的好处是支持订阅下发指令、长连接省流量,但在AT固件下调试成本更高,不建议作为第一条链路实现。
OneNET控制台里需要先准备几个固定信息:产品ID、设备ID、API Key,以及数据流名称。以本系统为例,数据流可以设计成四个:温度、湿度、烟雾、光照。HTTP上报接口要求用datastreams数组携带数据,键值对需要和数据流模板一一对应,服务端按数据流名称去匹配,匹配不上就报告成功但不上点,这一步是后面最容易翻车的地方。
控制台的准备工作按这个顺序走:
- 登录OneNET控制台,进入开发者中心,创建一个产品,协议选择HTTP;
- 在产品下新增设备,记录设备ID和设备级API Key;
- 进入设备详情,在“数据流模板”里新增四个数据流名称,全部用小写字母;
- 回到设备列表,确认设备状态是“已注册”,拿到完整的接入参数。
上报报文的格式如下:
POST /devices/802856465/datapoints?type=3 HTTP/1.1 Host: api.heclouds.com api-key: xxxxxxxxxxxx Content-Length: 82 {"datastreams":[{"id":"temperature","datapoints":[{"value":25}]}]}注意type=3这个参数表示用简化JSON格式上传;id必须和控制台里建的数据流名称完全一致;api-key是设备所属产品的API Key,不是设备ID。Content-Length必须等于body的实际字节数,多一个空格或少一个回车都会导致OneNET返回错误,这是ESP8266上报最常见的失败原因之一。
HTTP/1.1下要等服务端响应才能确认结果。ESP8266收到响应后会把一段以+IPD开头的内容通过串口打出来,里面包含状态码和OneNET返回的JSON。状态码是200或JSON里errno为0,说明上报成功,这时候再执行AT+CIPCLOSE关闭连接即可。整套链路的数据流就是:STM32读传感器→打包JSON→串口发ESP8266→AT建链→POST上报→OneNET收数据→控制台可见。
3. STM32采集端实现:DHT11时序与ADC多通道数据怎么读怎么发
3.1 DHT11温湿度读取:起始信号与40bit时序
DHT11是单总线器件,没有时钟线,全靠高低电平的时间宽度来传递数据。读一次温湿度的完整流程分三步:主机拉低总线至少18ms发出起始信号,然后释放总线;DHT11收到起始信号后主动拉低80us响应,再拉高80us准备发数据;之后连续输出40bit数据,包括湿度整数、湿度小数、温度整数、温度小数和一个校验字节。
实现这个时序最忌讳任务切换打断电平采样。在裸机环境下,读取函数里要临时关闭中断,读完之后再恢复,不然定时器中断一旦插入,某个bit的高电平被“拉长”,解析结果必然出错。下面是一段常见的读取实现:
uint8_t DHT11_ReadData(uint8_t *hum, uint8_t *tem) { uint8_t data[5] = {0,0,0,0,0}; uint32_t timeout; __disable_irq(); // 进入临界区,防止中断打断时序 GPIO_Mode_Set(GPIOB, GPIO_PIN_8, GPIO_MODE_OUT_PP); // 拉低18ms GPIO_ResetBits(GPIOB, GPIO_PIN_8); delay_ms(20); GPIO_SetBits(GPIOB, GPIO_PIN_8); GPIO_Mode_Set(GPIOB, GPIO_PIN_8, GPIO_MODE_IN_FLOATING); // 释放总线 delay_us(30); timeout = 1000; while(GPIO_ReadInputDataBit(GPIOB, GPIO_PIN_8) == 1) { // 等DHT11拉低 if(--timeout == 0) { __enable_irq(); return 1; } } timeout = 1000; while(GPIO_ReadInputDataBit(GPIOB, GPIO_PIN_8) == 0) { // 等80us低电平结束 if(--timeout == 0) { __enable_irq(); return 0; } } // 后续继续读取40bit数据并做校验 ... }这里两个while是读时序的基础骨架:第一个等待DHT11把总线从高拉低,第二个等待低电平结束后进入数据位传输。实际工程里还要继续读40bit,核心是每读一个bit时先等低电平结束,再量高电平持续时间,高电平超过约50us判为1,否则判为0。这个阈值决定了温湿度数据的准确度,delay_us的实现精度直接影响结果。
读完40bit后我一般会做一个校验:把前四个字节加起来,低8位等于第五个字节才算通过。校验没过直接丢弃本次数据,不更新缓存值,这样哪怕偶发误读一次,也不会把错误数据传给ESP8266。读DHT11的周期建议不小于2秒,因为传感器本身转换速度就是1秒左右,读太快只能拿到重复数据。
3.2 ADC多通道采集:切换通道的坑与数据帧封装
烟雾和光照都走ADC。以STM32F103C8T6为例,ADC1有10个通道,把PA0和PA1分别配成通道0和通道1即可。多通道采集用规则组扫描加连续转换模式,配合DMA后轮流采样,每次切换通道时需要注意采样时间不能太短,特别是接光敏电阻这种高阻抗信号源,采样时间低于7.5周期时读回来的电压会偏小,看起来就像光照突变一样。
void ADC_InitForMultiChannel(void) { ADC_InitTypeDef ADC_InitStructure; GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1 | RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE); RCC_ADCCLKConfig(RCC_PCLK2_Div6); // ADC时钟=PCLK2/6=12MHz GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; GPIO_Init(GPIOA, &GPIO_InitStructure); ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; ADC_InitStructure.ADC_ScanConvMode = ENABLE; // 扫描多通道 ADC_InitStructure.ADC_ContinuousConvMode = ENABLE; // 连续转换 ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; ADC_InitStructure.ADC_NbrOfChannel = 2; ADC_Init(ADC1, &ADC_InitStructure); // 此处继续配置通道采样时间与DMA }参数说明:RCC_PCLK2_Div6把12MHz作为ADC采样时钟,这是STM32F103在3.3V供电下的标准上限。扫描模式意味着两个通道各采一次,连续模式则会在通道间不停循环,配合DMA就不会把CPU时间浪费在等待转换结果上。这块和DHT11不一样,DHT11是时序器件要严格按bit来看,ADC是电压型采样,只要模拟量稳定,读取频率高低影响不大。
数据帧封装为了和OneNET对接方便,我会把四类数据统一打包成紧凑的JSON字符串,再串口发给ESP8266。注意浮点运算在Cortex-M3上更占栈空间,建议温湿度都拆成整数加小数两个字段发,或者用一个小技巧:把温度乘以100转成int16_t传输,云端再除以100。
char jsonbuf[128]; int16_t temp_scaled = (int16_t)(temp_integer * 100 + temp_decimal); int16_t humi_scaled = (int16_t)(humi_integer * 100 + humi_decimal); sprintf(jsonbuf, "{\"temperature\":%d.%d,\"humidity\":%d.%d,\"smoke\":%d,\"light\":%d}\r\n", temp_scaled / 100, temp_scaled % 100, humi_scaled / 100, humi_scaled % 100, adc_smoke, adc_light); USART2_SendString(jsonbuf);sprintf做整型拆解,避开了嵌入式上%f的麻烦;smoke和light直接传ADC原始值,OneNET只负责画曲线,不做换算。串口发送要逐字节确认发送完成再发下一个,否则0x0D这些控制字节会被串口中断吃掉,导致ESP8266收到半截JSON。每套板子的坑大多出在这个环节:数据明明是对的,但报文断尾,云端就是收不到。
4. ESP8266接入OneNET:AT指令建链与HTTP POST上报
4.1 上电等待与WiFi建链:AT指令握手三步走
ESP8266的AT固件启动有固定节奏:上电后模组先加载flash配置,大约需要0.8到1.5秒,期间不会响应任何AT指令,首次串口通信还会主动打印一段日志。这个阶段如果直接发测试指令,往往会得到奇怪响应甚至什么都得不到,于是很多人第一反应是“模组坏了”。其实只要先拉低复位再拉高,等1.5秒,再发AT\r\n,收到OK才是正常握手。
标准顺序是:AT→AT+CWMODE=1→AT+CWJAP="SSID","password"→AT+CIPSTART→AT+CIPSEND。第一步握手决定了后面所有指令的可靠性,工程里会做一个重试循环,最多试3次,每次超过500ms没收到完整OK就重新上电。WiFi连接有个常见误区:AT+CWJAP返回的WIFI DISCONNECT不一定是失败,可能是从旧AP断开到新AP重连过程的中间日志,真正的成功标志是后面的WIFI CONNECTED和OK两行。
uint8_t ESP8266_JoinAP(const char *ssid, const char *pwd, uint32_t timeout_ms) { char cmd[96]; sprintf(cmd, "AT+CWJAP=\"%s\",\"%s\"\r\n", ssid, pwd); UART_SendString(cmd); while(timeout_ms--) { if(strstr(recv_buf, "WIFI CONNECTED") && strstr(recv_buf, "OK")) { return 0; } delay_ms(1); } return 1; }这个函数的关键在于匹配两个标志词:WIFI CONNECTED表示物理层关联成功,OK表示AT指令执行完毕。只匹配OK不够,因为可能返回的是ERROR或还在扫描过程中。SSID和密码里的特殊字符需要转义,密码含中文时部分固件支持UTF-8直接传,但遇到带双引号的WiFi名字基本无解,更现实的做法是换热点而不是在转义上死磕。
4.2 HTTP POST上报与命令下发:CIPSEND的字节数陷阱
建链完成后是数据上报。为了避开域名解析在不同固件上的差异,我一般先用AT+CIPDOMAIN="api.heclouds.com"解析出IP,再执行AT+CIPSTART="TCP","该IP",80建立到OneNET的80端口连接。连接成功后模组返回CONNECT OK。HTTP头里的Host字段仍然用域名,因为服务端认的是Host,TCP连接用解析出的IP即可。
uint8_t ESP8266_ReportData(const char *json) { char http_req[320]; char len_str[8]; uint16_t body_len = strlen(json); int req_len; sprintf(http_req, "POST /devices/%s/datapoints?type=3 HTTP/1.1\r\n" "Host: api.heclouds.com\r\n" "api-key: %s\r\n" "Content-Length: %d\r\n" "\r\n" "%s", device_id, api_key, body_len, json); req_len = strlen(http_req); sprintf(len_str, "AT+CIPSEND=%d\r\n", req_len); UART_SendString(len_str); delay_ms(20); // 等待ESP8266返回 '>' UART_SendString(http_req); }这里最关键的参数是AT+CIPSEND后面带的字节数,它必须和后面实际发送的http_req总长度完全一致,多一个少一个都会导致OneNET返回400或者挂在半路。计算长度时容易漏掉的是HTTP头部里五个\r\n,以及Content-Length本身的值也要参与总长度计算。AT指令是半双工的,发完长度后模组返回>才允许发送数据,后面加20ms固定延时等待提示符,比轮询字符更省事。
上报成功后OneNET会返回带+IPD的响应段。我一般用串口助手把完整日志抓出来,核对三个点:是否出现"errno":0或HTTP/1.1 200;返回的JSON里是否带数据点信息;有没有立刻收到CLOSED。三个点都满足,说明上报闭环成功。
命令下发在当前HTTP方案里属于短连接场景,OneNET会在ESP8266连接期间把下行命令直接塞进+IPD数据段。工程里常见的做法是每30秒主动上报一次,在两次上报之间保持TCP连接暂不关闭,收到+IPD后从里面剥出命令字段,交给STM32解析执行。如果只需要展示数据,就不用管这块;需要远程控制风扇或报警器,就把工作重心放到下行报文解析上,解析时优先用strstr找定界符,不要在strtok上纠缠,因为AT固件可能在一个+IPD里拼多条日志。
提示:如果板子始终连接不上OneNET,先单独用电脑串口助手连ESP8266,手动敲一遍AT指令流程,确认模组本身能完成建链和上报,再谈STM32这边的代码问题。
5. 常见问题与避坑记录:五条实战踩坑
5.1 从源头到下游的高频故障:现象、原因、解决
这一章的内容全部来自实际复现这套工程时踩过的坑,按“现象→原因→解决”的格式整理。
第一条是ESP8266上电后发AT没响应或回一串乱码。现象是USB转串口直接连模组,波特率设成115200,发AT收不到OK,反而收到®ã这类字符。原因是模组从上电到AT固件就绪有固定延迟,直接发指令时固件还在启动阶段,串口只输出了boot日志;另外USB转串口的供电能力不足,电压被拉低后模组反复重启。解决办法是复位后等待1.5秒以上再发AT;同时模组VCC和GND之间并一个100uF电解电容,实测能解决大半诡异重启。
第二条是DHT11读到的温度偶尔跳到70度或者校验失败。现象是系统跑十分钟到半小时,出现一次湿度255或温度0的数据,之后又恢复正常。原因是读取时序被系统定时器等中断打断,某个bit的高电平被错误放大,导致解析出来的字节全错。解决方法是把DHT11_ReadData放进临界区,读完后立即恢复中断;如果工程里用了FreeRTOS,还要注意不能在临界区里做vTaskDelay这类会触发任务调度的调用,否则会死锁。
第三条是OneNET数据点不更新,但ESP8266侧一直返回HTTP 200。现象是POST返回的errno是0,但控制台数据流一条新点都没有。原因几乎都是数据流名称不匹配:控制台建的数据流叫temp,上报JSON里写的却是temperature,OneNET在匹配不到模板时会返回成功但丢弃数据。解决方法是把控制台的数据流名称、POST JSON里的id字段、代码里对应的标识三者对齐;我习惯在main函数入口处打一屏配置信息,把这三个名字直接打印出来逐字检查。
第四条是AT+CIPSEND发送后卡在>之后没有后续,或返回SEND OK但服务端没收到数据。现象是发完数据长度后串口迟迟不出现>,或者出现SEND OK但OneNET没有响应。原因是发送长度算大了:HTTP报文末尾的\r\n被漏算,或者用sizeof计算了固定数组的大小而数组里还有未使用的空字节。解决方法是统一用strlen(http_req),并且确保sprintf拼接完成后立刻计算长度,不要再对缓冲区做任何追加操作。
第五条跟AT+RESTORE恢复出厂设置有关:执行后自动重启,循环体里检测不到OK,程序死等。现象是恢复出厂后第一条指令就超时,代码在等待OK时跳出超时分支,结果后续AT指令全部失败。原因是AT+RESTORE后模组自动重启,重启期间的日志和正常OK混在一起,检测逻辑没区分boot日志与AT回复。解决方法是恢复出厂后先做一次AT握手清空缓冲,再执行后续指令;我在工程里固定了AT+RESTORE→延迟2秒→AT→延迟200毫秒→AT+CWMODE=1这个顺序,实测稳定。
5.2 系统级排查顺序:先看电源,再看日志,最后查报文
踩坑多了之后,我总结出一套排查顺序,比一上来就改代码有效率。第一步看电源:STM32板、ESP8266、传感器是分开供电还是共用一个USB口,USB口电压在WiFi射频发射瞬间可能出现几百毫伏波动,直接反映成DHT11偶发误读和WiFi断连。第二步看串口日志:ESP8266和STM32互连的两个串口都要接出来一份给串口助手,这样能判断数据到底是没生成、没发出还是没上报。第三步查HTTP报文,拿串口助手抓+IPD回包,能立刻定位是域名解析失败还是Content-Length不对。
这三个步骤的顺序不能换。我见过太多案例,一上来就怀疑代码怀疑AT指令,最后发现是USB转串口线本身质量差,数据在物理层就损坏了。所以这套工程的调试口我保留了三个位置:STM32调试串口、ESP8266串口日志、以及一个可选的逻辑分析仪引脚专门量DHT11时序。加上这三个采集点,系统里任何一环出问题,都能在两分钟内圈定范围。
6. OneNET可视化看板与本地联调:验证整条链路的最小路径
OneNET的看板是这类工程最有成就感的一环,配置起来其实不难。登录控制台进入设备对应的“数据流管理”,确认temperature、humidity、smoke、light四个数据流都有历史点,再进入“可视化”菜单新建看板,拖出图表组件绑定对应数据流,保存发布即可。不需要写任何代码,但有个前提:数据流里必须有历史点,否则组件拉不到数据,画布上会一直显示无数据。所以顺序一定是先跑通上报,再配看板。
我自己常用的联调方式是“三条命令验证法”:上电后先打开串口助手,看ESP8266是否打印WIFI CONNECTED;再手动敲一条AT+CIPSTART连到OneNET,确认返回CONNECT OK;最后把一条完整的POST报文直接用串口发进去,看HTTP回包里的errno。三条命令都能过,再把STM32的代码接上自动上报。这样把模组问题、网络问题和应用代码问题隔离开,不会在STM32程序里折腾半天最后发现是模组坏。
还有一个实用小技巧:验证阶段把上报周期暂时改短到5秒一次,方便在控制台快速看到曲线出现,确认没问题后再改回30秒或60秒。这个改法只动main函数里的一个延时宏,但不要为了省那几秒把DHT11的读周期也改短,DHT11的转换速度跟不上,拿到的还是重复数据。把这套工程下载下来后,第一步建议先按readme核对板子上的烧录路径和引脚分配,别急着接传感器。
从那以后我每套板子都强制走一遍“先AT握手、再建链、再点灯”的三步测试,这套习惯帮我挡掉了不少玄学问题。希望帮到你。
本文还有配套的精品资源,点击获取