
做环境监测这类嵌入式项目说难不难说简单也真不简单。我最近把基于STM32的智能环境监测系统整个流程跑通了温湿度、光照强度、空气质量、PM2.5这些参数全部采集上来用OLED屏本地显示同时通过ESP8266把数据推到云端手机端随时能看。如果你正打算做类似的课设、毕设或者单纯想在桌面上放一个自己写的“环境管家”这篇文章就是给你准备的。先说下这个系统的构成STM32做主控挂上DHT22温湿度传感器、BH1750光照传感器、MQ135空气质量传感器、GP2Y1010粉尘传感器加上一块0.96寸OLED屏、几个按键、一个ESP8266 WiFi模块。所有传感器数据定时采集经过滤波处理后一方面在本地OLED上轮播显示另一方面打包成JSON通过MQTT协议发到云平台。整个项目做下来踩了不少坑从传感器时序到I2C上拉电阻从ADC参考电压到WiFi重连策略每一步都有值得记录的地方。这篇文章我把完整的设计思路、选型对比、代码要点、调试经历全部摊开讲力求你照着走一遍就能跑起来。1. 需求拆解与整体设计思路1.1 这个项目到底要解决什么问题环境监测系统的核心任务就是把“环境状态”从物理量变成“可读数据”再变成“可传播的数据”。听起来绕其实拆开看就三件事采集、处理、上报。采集环节负责把温度、湿度、光照、气体污染这些模拟世界里的信号通过传感器转换成电信号再交给MCU的ADC或数字接口读出来。处理环节解决的是“数据能不能用”的问题——传感器原始输出往往带噪声、带漂移、带单位换算误差不处理就显示或者上传数据根本没法看。上报环节解决的是“数据给谁用”的问题本地屏幕只能给现场的人看想要远程查看、历史记录、异常告警就必须走无线通信。我见过不少同学做这类项目一上来就买器件、接线、拷代码结果传感器数值乱跳WiFi连不上最后一查是I2C地址都没确认对。所以我的建议是先花半天把需求、选型、架构想明白后面调试能省一大半时间。1.2 为什么选STM32而不是51、Arduino、ESP32这个问题几乎每个做课设的人都会问。我把几个常见方案摆一起对比你就知道怎么选了。方案优点缺点适合场景51单片机教材多、上手简单主频低、资源少、外设弱纯入门教学不适合多功能系统Arduino生态丰富、库函数多代码效率低、板子偏贵、工程化弱快速原型验证、创客玩具STM32性能强、外设丰富、价格低、资料多学习曲线稍陡、寄存器配置繁琐课设、毕设、实际产品原型ESP32自带WiFi蓝牙、双核、性价比高引脚少、模拟外设不如STM32丰富直接做IoT节点、无线密集场景我自己倾向于STM32F103C8T6理由是它是一颗Cortex-M3内核的芯片72MHz主频64KB Flash20KB SRAM片上有I2C、SPI、USART、ADC、定时器几乎所有环境传感器都能直接挂。最关键的是价格便宜几块钱一颗资料铺天盖地出了任何问题都能搜到解决方案。ESP32虽然自带WiFi但环境监测系统要接的传感器种类多STM32的引脚和外设资源更宽裕而且学完STM32后面换其他芯片几乎零成本。1.3 系统架构感知层、控制层、交互层、传输层整个系统我习惯分成四层来看。感知层就是所有传感器DHT22负责温度和湿度BH1750负责光照度MQ135负责VOC和有害气体浓度GP2Y1010负责PM2.5粉尘浓度。这四个传感器覆盖了室内环境监测的主要指标而且接口类型刚好覆盖了单总线、I2C、模拟输出三种常用形态做一遍就能把STM32的常见外设都练熟。控制层由STM32F103C8T6承担。它负责定时唤醒传感器、读取数据、做滤波换算、驱动OLED显示、响应按键事件、封装数据帧、通过串口与WiFi模块交互。控制层的核心是一个定时中断驱动的状态机保证所有任务都有确定的执行周期。交互层由OLED屏和三个按键构成。OLED显示当前数据、历史趋势、设备状态按键用来切换显示页面、设置告警阈值、手动触发上报。传输层用ESP8266串口WiFi模块。STM32通过USART发AT指令配置模块连接路由器通过MQTT协议往云平台推数据。传输层单独拎出来是因为它的调试复杂度往往比感知层还高WiFi掉线、MQTT断连、数据粘包是三大经典问题后面会详细讲。2. 硬件选型与电路设计细节2.1 MCU最小系统和引脚规划STM32F103C8T6是LQFP48封装引脚不多但做这个系统绰绰有余。我实际用到的引脚如下你可以直接照抄。功能引脚复用/备注DHT22数据PA0普通GPIO输入输出切换BH1750 SCLPB6I2C1时钟BH1750 SDAPB7I2C1数据MQ135模拟输出PA1ADC1通道1GP2Y1010模拟输出PA2ADC1通道2GP2Y1010 LED脉冲PA3普通GPIO输出OLED SCLPB8I2C1软件模拟或硬件OLED SDAPB9I2C1软件模拟或硬件按键1/2/3PB0/PB1/PB10下拉输入ESP8266 TXPA9USART1 TXESP8266 RXPA10USART1 RX状态LEDPB12普通GPIO输出最小系统需要注意三点晶振我用的是8MHz无源晶振两个20pF负载电容直接接地复位引脚接10k电阻上拉到3.3V再并一个100nF电容到地BOOT0串一个10k电阻接地保证从Flash启动。很多人画PCB容易漏掉VDDA和VSSA的滤波电容ADC采样会莫名其妙地偏特别是接MQ135这种模拟传感器的时候一定要在VDDA引脚和GND之间加一个1uF和一个100nF的电容并联。2.2 传感器选型对比与接口梳理传感器选型是很多人纠结的地方我直接给你一套组合方案都是便宜、资料多、稳定性还不错的型号。DHT22AM2302温湿度传感器单总线协议测量范围温度-40到80℃湿度0到99.9%RH精度分别是±0.5℃和±2%RH。相比DHT11DHT22精度和采样周期都更好价格也就多几块钱强烈建议直接用DHT22别在DHT11上省那几块钱然后后面折腾半天。BH1750光照传感器I2C接口测量范围1到65535 lx16位精度自带光电二极管和ADC输出直接是数字量不需要自己算电压。I2C地址由ADDR引脚决定ADDR接地时地址是0x23接VCC时是0x5C新手常常在这个地址上翻车。MQ135空气质量传感器模拟输出对氨气、苯、烟雾、VOC等有害气体敏感。它的输出是一个电位器可以调节的电压信号通过ADC读取。MQ系列传感器有个通病上电后需要预热前几分钟输出会漂移我的做法是上电后等5分钟再开始采集或者软件里加一个“预热忽略”的逻辑。GP2Y1010粉尘传感器模拟输出专门测PM2.5。它的驱动电路比较特殊LED需要周期性脉冲驱动每10ms一个周期其中0.32ms为脉冲宽度采样点必须在脉冲开始后0.28ms左右取ADC值。这个时序要测试并调试好否则读回来的电压是错的。OLED屏0.96寸、128x64分辨率、I2C接口驱动芯片是SSD1306。OLED功耗比LCD低得多显示效果也好唯一的缺点是长期显示同一画面会烧屏所以我的固件里做了屏保逻辑超过两分钟无操作自动切换成较低亮度的时钟显示。2.3 电源和布线的实操经验整个系统的供电我建议这样设计USB 5V输入经AMS1117-3.3稳压到3.3V给MCU、传感器、OLED和ESP8266供电。ESP8266的峰值电流可以达到300mA以上所以在它的VCC引脚附近一定要放一个100uF电解电容和一个10uF陶瓷电容否则WiFi发射瞬间会把电压拉低导致MCU复位。我用洞洞板搭建的时候发现一个问题DHT22的线只要超过20cm数据就容易读失败。这是单总线协议的固有毛病线太长、寄生电容太大都会破坏时序。解决办法有三个连线尽量短、数据线上拉一个4.7k电阻到VCC、实在要长距离传输就换I2C接口的SHT30传感器。模拟传感器和数字电路之间的干扰也值得注意。MQ135和GP2Y1010的模拟输出线不要和ESP8266的天线区域、串口线靠太近否则WiFi发射时的射频干扰会让ADC读数周期性跳动。如果PCB空间允许最好把模拟地和数字地单点连接。3. 软件架构与驱动实现3.1 工程组织裸机状态机还是RTOS这种量级的项目用不上RTOS裸机加定时器调度就够了。很多人一上来就上FreeRTOS最后发现任务同步、优先级配置反而把自己绕进去了。我的工程结构是main.c负责初始化和主循环bsp层放各外设驱动app层放业务逻辑。具体划分如下Project/ ├── Core/ # 启动文件、系统时钟配置 ├── Drivers/ # HAL库或标准库文件 ├── BSP/ │ ├── dht22.c # DHT22单总线驱动 │ ├── bh1750.c # BH1750传感器驱动 │ ├── mq135.c # MQ135 ADC读取 │ ├── gp2y1010.c # 粉尘传感器驱动 │ ├── oled.c # OLED显示驱动 │ └── esp8266.c # WiFi模块AT指令封装 ├── APP/ │ ├── sensor_task.c # 采集任务 │ ├── filter.c # 滤波算法 │ ├── ui_task.c # OLED界面逻辑 │ └── upload_task.c # 数据上传逻辑 └── main.c主循环里用一个1ms的软件定时器作为系统时基然后根据标志位决定是否执行采集、显示、上传任务。比如采集周期设为5秒显示刷新设为500ms上传周期设为30秒。这些周期分开设置有个好处采集太频繁会浪费功耗和WiFi流量显示太频繁OLED会闪烁上传太频繁会被云平台限流。3.2 DHT22驱动单总线时序到底怎么调DHT22用单总线协议只有一根数据线时序要求相当严格。完整流程是这样的主机拉低数据线至少18ms然后释放并拉高20-40us。DHT22响应先把总线拉低80us再拉高80us。然后连续输出40位数据每一位的格式是先拉低50us再拉高高电平持续时间26-28us表示逻辑0持续70us表示逻辑1。40位数据的分布是湿度高8位、湿度低8位、温度高8位最高位为符号位、温度低8位、校验和8位。校验和必须等于前四个字节之和的低8位否则这组数据直接丢弃。核心代码比较敏感的是时序延时部分我用的标准库版本是这样写的uint8_t DHT22_ReadData(float *humidity, float *temperature) { uint8_t data[5] {0}; uint8_t i, j; // 主机发送起始信号 GPIO_InitTypeDef gpio; GPIO_InitStructure_Output(gpio, GPIO_Pin_0); // 配置为输出 GPIO_WriteBit(GPIOA, GPIO_Pin_0, Bit_RESET); delay_us(18000); // 至少18ms GPIO_WriteBit(GPIOA, GPIO_Pin_0, Bit_SET); delay_us(30); // 切换到输入模式 GPIO_InitStructure_Input(gpio, GPIO_Pin_0); // 等待响应信号 while(GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) Bit_SET); while(GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) Bit_RESET); while(GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) Bit_SET); // 读取40位数据 for(i 0; i 5; i) { for(j 0; j 8; j) { while(GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) Bit_RESET); delay_us(40); if(GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) Bit_SET) { data[i] | (0x80 j); while(GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) Bit_SET); } } } // 校验 if((uint8_t)(data[0] data[1] data[2] data[3]) ! data[4]) return 1; *humidity ((data[0] 8) | data[1]) / 10.0f; *temperature ((data[2] 0x7F) 8 | data[3]) / 10.0f; if(data[2] 0x80) *temperature -(*temperature); return 0; }这里的delay_us函数建议用DWT计数器实现精度比软件循环高得多。我踩过的坑是用nop循环做延时编译器开优化之后延时时间完全不对DHT22死活读不到数据。如果你也遇到类似问题先查一下是不是延时被优化掉了。3.3 BH1750的I2C读取和注意事项BH1750是I2C接口STM32的硬件I2C在标准库时代口碑不太好很多人宁愿用软件模拟I2C。我实测下来只要初始化正确、加上超时处理硬件I2C是没问题的。但新手图省事用软件模拟I2C也行代码网上大把时序对了就能跑。需要注意的是BH1750的工作模式。它有好几种测量模式连续高分辨率、连续低分辨率、一次高分辨率等。高分辨率模式测量时间约为120ms低分辨率约为16ms分辨率分别是1lx和4lx。我选的是一次高分辨率模式每次测量前先发测量命令然后延时120ms再读结果读回来的是16位数据除以1.2就得到光照度值单位是lx。void BH1750_Read(float *lux) { uint8_t buf[2]; // 发送一次高分辨率测量命令 BH1750_WriteCmd(0x20); // 0x20 一次高分辨率模式 delay_ms(180); // 留足转换时间 // 读取测量结果 BH1750_ReadData(buf, 2); uint16_t raw (buf[0] 8) | buf[1]; *lux (float)raw / 1.2f; }I2C通信失败十有八九是上拉电阻问题。I2C的SCL和SDA必须有上拉电阻阻值通常在2.2k到10k之间。我的板子上两个线都用了4.7k上拉到3.3V从来没出过问题。如果发现BH1750偶尔读出来是0xFFFF或者I2C卡死优先检查上拉电阻是否焊好、地址是否正确。3.4 数据滤波中值滤波和滑动平均的组合拳传感器原始数据的质量直接决定了整个系统的下限。MQ135和GP2Y1010这种模拟输出传感器ADC读数跳变个几十个LSB是家常便饭直接拿来显示会让你怀疑人生。我用的方案是两级滤波第一级是中值滤波每采集5次取中间值用来剔除突发噪声和尖峰第二级是滑动平均把最近10个中值再做平均平滑短时波动。中值滤波负责“去毛刺”滑动平均负责“润色”配合起来效果相当好。#define FILTER_WINDOW 5 #define SMOOTH_WINDOW 10 float MedianFilter(float *buf, uint8_t len) { // 冒泡排序求中间值注意不要改变原数组顺序 float tmp[FILTER_WINDOW]; for(uint8_t i 0; i len; i) tmp[i] buf[i]; for(uint8_t i 0; i len - 1; i) for(uint8_t j i 1; j len; j) if(tmp[i] tmp[j]) { float t tmp[i]; tmp[i] tmp[j]; tmp[j] t; } return tmp[len / 2]; }滑动平均的实现更简单维护一个环形缓冲区每次把新值加进去总和减去最旧的值再除以窗口长度。注意所有中间变量用float不要用int去收敛否则长期运行会出现累加误差。4. 本地显示、按键交互与远程上云4.1 OLED界面设计一屏看全还是分页轮播0.96寸的128x64屏幕一屏最多显示五六行文字。我做了三个页面通过按键切换第一页显示温湿度和光照度第二页显示PM2.5和空气质量评级第三页显示WiFi连接状态、MQTT连接状态和最近一次上报时间。OLED驱动要注意刷新策略。SSD1306的I2C速度上限是400kHz全屏刷新一次大约需要几十毫秒如果你每100ms刷新一次全屏CPU大部分时间都花在刷屏上了。我的做法是分成“静态区域”和“动态区域”只刷新变化的部分。比如显示实时温度的位置单独刷新其他区域保持不变这样既流畅又能减少I2C总线负担。字体方面建议准备两套字库一套是12x6的小字体用于数据正文一套是16x8的大字体用于数字高亮显示。我做界面时发现数字用大字体加粗显示视觉效果会好很多甚至不需要额外的图形美化。4.2 ESP8266接入AT指令还是直接刷固件ESP8266作为STM32的通信外设有两种接入方式一种是保留原厂AT固件STM32通过串口发AT指令控制另一种是给ESP8266刷NodeMCU或Arduino固件让它自己跑逻辑。对于这个项目我强烈建议用AT指令方案原因很简单STM32是主控所有逻辑在本地ESP8266只充当“透传管道”调试定位问题方便得多。AT指令的基本流程是AT # 测试模块是否响应 ATE0 # 关闭指令回显 ATCWMODE1 # 设置为Station模式 ATCWJAPSSID,password # 连接路由器 ATCIPSTARTTCP,broker地址,1883 # 建立TCP连接 ATCIPSEND长度 # 发送数据每个AT指令发送后一定要等待模块返回“OK”或“ERROR”再执行下一步不要盲目连续发送。而且ESP8266在连接WiFi的过程中会阻塞发送ATCWJAP后最好加一个10秒的超时等待然后轮询ATCWJAP?确认连接状态。我实际使用中发现一个高频问题ESP8266模块供电不足表现为WiFi连不上、发送AT指令偶尔无响应。前面提到的100uF电容一定要加另外串口线不要用太长太细的杜邦线给模块单独供电比让STM32板载稳压器硬扛要靠谱得多。4.3 MQTT数据上云JSON打包和断线重连数据上传我选的MQTT协议它比HTTP更适合物联网场景长连接、开销小、支持发布订阅模型。云平台选择上你可以用各种公共MQTT broker也可以自己搭一个Mosquitto服务器。项目的核心是STM32侧怎么把数据组织好、发出去。STM32侧的数据帧我用JSON格式简单、可读性强、云平台解析也方便char jsonBuffer[128]; snprintf(jsonBuffer, sizeof(jsonBuffer), {\temp\:%.1f,\humi\:%.1f,\lux\:%.1f,\pm25\:%.1f,\aqi\:%d,\ts\:%d}, temperature, humidity, lux, pm25, aqi, (int)time(NULL));这里有个小技巧先把所有传感器数据按照固定格式拼成字符串然后一次性通过串口发送给ESP8266。为什么要先拼好再发因为ESP8266的AT指令发送数据时需要先声明数据长度ATCIPSEND长度你不可能一边测数据长度一边发送所以必须先把完整的数据帧准备好再计算长度并发送。MQTT断线重连是必须做的逻辑而且要有退避策略。我实测下来如果服务器瞬间重启或者路由器断电ESP8266模块可能处于半死状态一直重连会导致日志刷屏、模块反复重启。我的方案是连续3次重连失败后等待60秒再发起下一次重连同时在OLED上显示“网络异常”提示。5. 调试排障与经验速查5.1 经典问题DHT22读到的数据全是0或恒定值这个问题的出现频率极高大概率是以下几点之一原因现象解决办法延时被编译器优化数据全0或偶尔读通用DWT或SysTick做精确延时上拉电阻缺失或太小通信波形不好数据错误数据线和VCC之间加4.7k电阻引脚模式切换错误读不到起始响应确认GPIO从输出切换到输入时CNF配置正确线太长导致波形畸变数据不稳定缩短连线或者改用SHT30传感器损坏完全无响应换一个备用传感器测试排查时我一般先用逻辑分析仪或示波器看波形。如果没有示波器也可以在程序里加调试代码比如连续读取5次并把每次的状态打印出来看卡在哪个while循环里能快速缩小问题范围。5.2 MQ135输出漂移和温湿度耦合MQ135是化学传感器除了预热漂移之外它的输出还会受温度和湿度影响。同样的气体浓度在冬天和夏天读出来的电压可能差很多。虽然专业做法是查datasheet的温湿度补偿曲线做修正但我实测下来在室内场景把MQ135的输出粗略换算成“空气质量等级”优、良、差三个档位就够了没必要纠结精确的PPM数值。换算方法不复杂ADC读到的电压经过一个电阻分压网络对应一个等效电阻值Rs把这个Rs和新鲜空气中的基准值R0做比值再查气体灵敏度特性曲线就能估算出目标气体的浓度。我的做法更简单直接根据电压范围划分等级电压低1V空气质量好电压中1V-2.5V空气质量一般电压高2.5V有污染风险。这个思路适合家用场景精度要求高的工业场景另说。5.3 现场调试的几个“土办法”这些调试技巧都是实操总结出来的正规教程很少写。第一个是串口调试配合“数据信息指纹”。在固件里每隔几十毫秒打印一条带时间戳的调试信息信息里包含当前执行到哪个任务、各传感器原始值、转换后的物理量值。把这些信息存到电脑上回放一遍就能看出问题发生在哪个环节。第二个是用手机的收音机功能间接判断ESP8266是否在正常工作。WiFi模块工作时会产生可听见的射频噪声把手机靠近模块打开收音机调到没有电台的频段能听到明显的“滋滋”声这说明模块在发射数据。这个小技巧不需要任何仪器就能判断ESP8266是否真的在发包。第三个是给所有传感器做“单点测试”意思是一次只接一个传感器把它的数据调通、确认无误再接入下一个。很多人一上来把所有传感器都挂上出问题根本不知道是哪个环节。我调试的时候先接OLED然后接DHT22然后是BH1750、MQ135、GP2Y1010最后才接ESP8266每一层都验证过了再往上叠。第四个是给数据加“健康度”自检。每个传感器驱动里加一个错误计数连续几次读取失败就把该传感器标记为“故障”界面上提示“传感器异常”而不是显示一个假的0值或者乱码。这个功能在系统长期运行时非常重要不然别人看数据还以为环境真的极端到爆表了。6. 后续扩展与个人体会6.1 可以扩展的方向这个系统的骨架做完之后很多方向都可以低成本扩展。硬件上可以加一个蜂鸣器做本地声光告警温度过高或PM2.5超标时直接响。可以加一个继电器控制加湿器、风扇或空气净化器做成闭环控制。比如温湿度低于阈值就自动打开加湿器PM2.5超标就自动启动净化器。还可以换成带电池的低功耗方案用STM32L系列加RTC定时唤醒采集完数据就睡环境监测站的续航能做到几个月。软件上可以在云平台侧加历史数据存储和图表展示用SQLite加一个简单的后端服务就能实现。还可以加数据导出功能或者用Webhook接入微信/钉钉机器人做远程告警推送。如果有条件还能接上语音交互对着智能音箱问一句“当前室内空气质量怎么样”让音箱去查平台数据返回结果。6.2 个人体会做这个项目最大的收获还不是熟悉了STM32和这些传感器而是建立了“系统思维”。单看每个传感器驱动代码量都不大但把采集、滤波、显示、上传、异常处理全部串起来才发现真正的复杂度在接口之间的衔接和边界情况的处理。我踩坑最深的就是数据上报——单纯上报平均数据看起来没问题但一旦WiFi断了、服务器压力大了、传感器偶尔报错一条链路上任何一个环节不稳定整个系统的可用性就崩了。后来加上数据格式校验、重发机制、断线退避、传感器故障标记系统才真正能长期运行。这也是我建议你在做完基础版本之后一定不要把“能跑”当“能交付”的原因。如果你正准备做类似的系统我的建议是先搭好最简单的回路——STM32点灯、OLED显示、串口打印然后一步一步往上加。每加一个模块就完整调试一次不要想着一次全部接好再统一调。这样看着慢实际是最快的路径。等你完整跑通一遍再去想优化和扩展你会发现这个项目能带给你的远不止一个“能显示数据的盒子”。