拓冰建站拓冰建站
首页 / 资讯中心 / 正文

基于STM32的室内环境监测系统设计与实现:温湿度、空气质量、光照采集

前阵子做室内空气质量检测买了几款成品检测仪要么是显示数值不太透明要么是数据拿不出来做二次分析。后来索性决定自己用STM32做一套环境质量监测系统把温湿度、空气质量、光照这些核心参数全部采集起来在本地OLED屏上实时显示同时通过串口把数据输出给上位机整套方案还做到了原理图开源、代码开源、仿真可跑。如果你正在入门STM32或者想做一款小体积、低成本、可复现程度高的环境数据采集终端这篇文章应该能帮你少走不少弯路。项目里我用的是STM32F103C8T6这颗经典芯片配合DHT11读取温湿度、MQ135检测空气质量、BH1750采集光照强度外加一块0.96寸I2C接口的OLED屏幕作为本地显示终端。整套系统的代码量不大按功能模块拆解后每一块都很清晰适合拿来练手也适合在原有框架上继续扩展比如PM2.5传感器、继电器控制、WiFi上传等。仿真部分用的是Proteus传感器均能找到对应的仿真模型哪怕手上暂时没有硬件也可以先把逻辑和代码调通。1. 系统架构与方案选型1.1 为什么选择STM32F103C8T6作为主控市面上做环境监测的MCU选择非常多51单片机、Arduino、ESP32、STM32都是常见选项。但在这个项目里我最终还是选了STM32F103C8T6核心原因有几个。首先是资源够用且不浪费这颗芯片基于ARM Cortex-M3内核主频72MHzFlash为64KBSRAM为20KB跑一个轻量级的环境数据采集逻辑绰绰有余。它内置多个定时器、ADC、USART、I2C、SPI等外设恰好覆盖本项目需要的模拟量采集MQ135输出为模拟电压、数字温湿度读取单总线协议、I2C显示OLED、串口通信上位机交互。其次是生态成熟度STM32F103系列的材料和学习资源非常丰富不管是HAL库还是标准外设库都能支撑一个完整的项目。即便你是第一次接触STM32遇到问题时的检索成本会低很多。第三点是价格和供货稳定作为一颗出货量极大的芯片C8T6的最小系统板价格很友好用来做原型验证或者课程设计都非常合适。对比来看51单片机虽然上手简单但片上外设数量少后续想扩展PWM调速、ADC多通道采集或者DMA传输时会觉得处处受限。ESP32虽然自带WiFi和蓝牙但对纯本地环境监测来说有点大材小用而且它的功耗控制、模拟采集精度在入门场景下反而不如STM32直观。所以在“硬件成本低、代码易理解、扩展空间足”这三个目标之间STM32F103C8T6是一个平衡点非常合适的选择。1.2 环境参数维度与传感器选型思路环境质量是一个综合概念普通人最关心的几个指标无非是温度、湿度、空气质量以及光照。这四个参数覆盖了“舒不舒适”“空气干不干净”“光线亮不亮”三个核心场景而且它们的传感器方案都相当成熟非常适合作为系统第一版的功能基线。温湿度测量选了DHT11。它是一颗数字温湿度传感器采用单总线协议只需要一根数据线就能完成通信。测量范围是温度0到50摄氏度、湿度20%到90%RH精度分别是正负2摄氏度和正负5%RH。对室内环境监测来说这个精度完全够用了。如果你对精度有更高要求后续可以直接换成DHT22或SHT30软件层只需要改驱动函数。空气质量检测选了MQ135。它是半导体气体传感器对氨气、硫化物、苯系蒸汽、烟雾等有一定敏感度输出信号可以是模拟量也可以经过比较器电路输出数字量。本项目中我直接读取它的模拟电压输出通过ADC转换成浓度相关的数值再映射到“优/良/差”等级。光照强度选了BH1750。这是一颗I2C接口的数字环境光传感器测量范围1到65535 lux内部自带16位ADC和光敏二极管使用非常方便。它可以直接读到物理光照值不需要自己做线性标定。显示终端0.96寸OLEDSSD1306驱动芯片I2C接口。显示密度高、功耗低、调试时反馈直观代码驱动也比较成熟。传感器的选型逻辑其实很明确优先选数字接口或标准模拟输出的减少后端信号调理电路的复杂度。DHT11和BH1750都是数字输出开发时几乎不需要外围电路MQ135输出模拟电压只需要一个ADC通道即可读取配上一路参考电压做阈值判断就可以了。1.3 系统整体工作流程整套系统上电后先做外设初始化包括时钟配置、GPIO方向设定、ADC通道初始化、I2C初始化、串口初始化以及OLED屏幕的显示初始化。随后主循环按顺序执行以下逻辑读取DHT11温湿度数据、读取BH1750光照数据、通过ADC读取MQ135电压值计算空气等级把所有这些数据打包后同时送OLED和串口输出。比较关键的设计点是我把传感器读取和显示刷新做了分时处理。DHT11读取一次需要等待约20msMQ135的ADC采样很快BH1750一次转换需要约16ms。如果串行地从头到尾跑一遍整个周期大约在80ms左右人眼在OLED上看不出刷新问题。但串口上位机如果同时用9600波特率接收会出现轻微的数据堆积现象。后来我把主循环设计成状态机模式每个传感器读取放在对应的时间片里整体调度周期稳定在100ms上位机每500ms接收一次完整数据帧整个系统运行顺畅很多。2. 硬件原理图设计深度解析2.1 最小系统电路设计STM32最小系统是整块板子的地基包含电源电路、复位电路、时钟电路和启动模式选择四大部分原理图设计时每一块都有需要注意的细节。电源部分我采用的是USB 5V输入经过AMS1117-3.3稳压芯片降到3.3V给MCU和传感器供电。AMS1117是LDO线性稳压器纹波小、电路简单适合这种小电流传感器系统。输入和输出端各加一个10uF钽电容和一个0.1uF陶瓷电容用于滤除低频纹波和高频噪声。特别要提醒的是电容要尽量靠近稳压芯片的引脚放置走线先经过电容再到芯片电源脚否则滤波效果会打折扣。复位电路是经典的上电复位结构NRST引脚接一个10K上拉电阻到3.3V同时接一个0.1uF电容到GND。按下复位按键时NRST被拉低MCU执行复位。有些入门设计中会把按键省掉用ST-Link也能复位但板子上放一个独立复位键在调试时会方便很多尤其是程序跑飞或者进入异常状态时。时钟电路用了8MHz无源晶振加两个20pF负载电容。STM32F103内部有HSI 8MHz RC振荡器理论上不接外部晶振也能跑但内部RC精度有限而且后续如果要做USB通信就必须使用外部晶振保证48MHz时钟准确。所以最小系统板上晶振电路是必备的。两个负载电容的值根据晶振规格书选择一般在10pF到22pF之间实测20pF最稳定。启动模式选择通过BOOT0引脚实现BOOT0串联10K电阻接GND保证默认从用户Flash启动。BOOT1引脚直接悬空或者也下拉到GND因为FLASH启动模式下BOOT1状态无影响。2.2 传感器接口电路设计DHT11的接口电路非常简单DATA引脚接MCU的一个普通GPIO我用的PA1数据线上加一个4.7K上拉电阻到3.3V。因为DHT11采用单总线协议空闲时数据线保持高电平MCU发送起始信号后释放总线DHT11拉低响应整个时序过程都是开漏结构必须有上拉电阻才能正常工作。如果你用的模块板上已经自带上拉电阻原理图中可以不用重复添加但要在BOM上标注清楚。BH1750模块是标准的I2C接口SDA和SCL分别接PB7和PB6同样需要上拉电阻。I2C协议要求总线空闲时SDA和SCL都为高电平STM32的I2C外设配置为开漏输出后必须通过外部上拉电阻提供高电平驱动能力。我用的是4.7K上拉接到3.3V。如果你用的模块板上有集成上拉外部可以省略但建议预留焊盘方便调试时根据实际波形调整电阻值。MQ135模块的输出有模拟量AO和数字量DO两种引脚。本项目中我只使用AO引脚接一个电压跟随器后送入STM32的ADC输入引脚PA0。为什么要加电压跟随器因为半导体气体传感器的加热电阻和敏感电阻阻值较大输出阻抗不低直接接入ADC可能导致采样值偏低或不稳定。用一个轨到轨运放搭建电压跟随器可以在不影响信号幅值的前提下大幅降低输出阻抗保证ADC采样的准确性。市售模块上通常已经集成了比较器电路AO输出的驱动能力够用也可以不用额外跟随器直接接PA0加一个0.1uF滤波电容即可。2.3 OLED显示与串口通信电路OLED模块我选用的是0.96寸、SSD1306控制器、I2C接口版本。模块的VCC接3.3VGND接GNDSDA和SCL与BH1750共用同一条I2C总线。因为两个设备的I2C地址不同BH1750的地址是0x23ADDR引脚接低OLED的地址是0x3C总线仲裁没有问题。共用总线的好处是少占用两个GPIO布线也更简洁。不过共享总线上多个设备时I2C速率需要根据最慢设备来设定BH1750支持400kHzSSD1306手册标称最大400kHz实测跑400kHz没问题但为了稳妥我统一配置成200kHz。串口部分用的是USART1TX为PA9RX为PA10。由于STM32的IO电平是3.3V和PC的RS-232电平不兼容所以需要经过USB转TTL芯片如CH340或CP2102与电脑通信。原理图中我把USART1的收发引脚直接通过排针引出外部接CH340模块。这样板子本身更简洁也方便灵活换用不同品牌的USB转串口模块。调试时注意共地USB转TTL模块和主控板必须在同一个GND电平上否则通信会出现乱码。2.4 蜂鸣器报警与预留扩展接口为了让环境数据超限时能本地提示我加了一个有源蜂鸣器接在PB5引脚通过NPN三极管驱动。为什么不能直接用GPIO驱动蜂鸣器因为STM32的GPIO输出电流能力有限标准模式下最大约20mA而有源蜂鸣器正常工作电流往往在30mA以上直接驱动可能导致GPIO口烧坏或电压跌落。我用的是SS8050三极管搭建的开关电路GPIO输出高电平时三极管导通蜂鸣器发声GPIO输出低电平时截止。原理图上蜂鸣器两端反并联一个1N4148二极管用于吸收关断瞬间感性负载产生的反向电动势防止击穿三极管。扩展接口方面我预留了两组5V和3.3V电源排针以及一组包含PB3、PB4、PA4、PA5的通用GPIO排针。这些接口可以方便地接入继电器模块、ESP8266 WiFi模块、PWM风扇等外设。实际测试中我在PB3上挂过一个继电器通过修改报警逻辑实现了空气质量超标时自动启动排风风扇的效果整个扩展过程不需要改动原有原理图架构。3. 软件代码实现解析3.1 工程结构与模块划分代码工程采用标准HAL库架构按外设模块做了清晰的文件夹划分。核心文件包括main.c系统初始化、主循环调度dht11.c/dht11.hDHT11单总线时序驱动、温湿度数据解析bh1750.c/bh1750.hBH1750的I2C寄存器配置、光照读取adc_mq.c/adc_mq.hADC初始化、MQ135电压采集与等级映射oled.c/oled.hSSD1306初始化、字符与图形显示usart1.c/usart1.h串口初始化、数据帧打包发送systick_delay.c/systick_delay.h基于SysTick的毫秒和微秒延时模块化设计有几个明显好处。每个传感器只有一个明确的程序接口比如DHT11_Read_TempHumidity(float *temp, float *hum)主函数只关心调用这个函数后能不能拿到数值不关心底层时序细节。后续如果要替换传感器或者更改通信协议只需要改动对应的驱动文件不需要波及主循环逻辑。对于刚接触嵌入式开发的读者来说这种“一处功能一个文件”的组织方式也比较容易对照理解。3.2 DHT11单总线时序驱动要点DHT11的驱动是整个项目中时序最敏感的部分也是新手最容易踩坑的地方。单总线协议要求MCU必须精确控制微秒级别的电平变化时序错误会导致传感器不应答或者数据全部为0。标准读取流程是这样的MCU先把数据线拉低至少18ms然后释放并延时20到40us此时DHT11检测到起始信号会拉低总线80us作为响应然后再拉高80us准备发送数据。随后DHT11开始发送40位数据每一位都由低电平50us和高电平的持续时间来区分高电平持续26到28us代表逻辑0持续70us代表逻辑1。所以在驱动代码中我会先拉低PA1并延时20ms然后拉高并延时30us左右再切换IO为输入模式等待传感器响应。读取每一位时先等待电平变低再等待电平拉高然后测量高电平持续的时间超过40us判为逻辑1否则判为逻辑0。为了避免阻塞时间过长造成系统卡死我为每一次等待都加了超时判断比如等待低电平最多持续200us超过就跳出并返回错误码。延时函数我用的SysTick实现了微秒级延时实测精度够用。这里有一个重要的注意事项开启编译器优化等级为O2时部分空循环延时会因为代码优化的原因被大幅压缩导致时序失败。所以我强烈建议用SysTick或定时器实现延时函数而不是用简单的for循环空转。这个坑我踩过一次当时一开优化等级DHT11数据就全错了排查了很久才发现是空循环延时被优化掉了。3.3 BH1750光照读取的寄存器配置BH1750的I2C操作相对友好它支持两种地址模式地址由ADDR引脚决定。ADDR接低电平时地址为0x23接高电平时为0x5C。我的原理图中ADDR直接接地所以器件地址是0x23。芯片上电后默认处于断电模式需要先发送连续高分辨率测量模式的指令0x10然后等待约180ms手册标称最大120ms我留了余量再连续读取两个字节数据。高字节在前低字节在后合成一个16位整数后除以1.2即得到以lux为单位的光照值。这里除以1.2是因为在H分辨率模式下1个LSB对应1.2lux。如果用的是低分辨率模式则1个LSB对应的是4lux换算系数不同。需要注意的细节是BH1750对START和STOP条件的时序要求比一般I2C设备苛刻一些特别是从发送测量指令到读取数据之间不能插入其他I2C总线的通信。在工程实现中我读取光照时会对I2C总线加锁等两个字节的数据取回来后再释放总线避免OLED刷新正好打在这个时间段里造成数据错乱。3.4 ADC采集与空气质量等级映射MQ135输出的模拟电压范围大约是0到4V这个范围超出了STM32的3.3V参考电压上限所以我在原理图设计时对传感器模块的供电使用了5V并且通过电阻分压后再送ADC。具体实现有两种方案一是在模块输出端串联10K电阻再接GND的10K电阻做1/2分压二是用运放搭建减法电路。考虑到成本和简洁性我选择了电阻分压方案。但需要提醒的是电阻分压会降低传感器的分辨率3.3V量程分配到0到4V的输入范围上每个ADC单位代表的电压值变大精度自然下降。配置ADC1的通道0采样时间选择239.5个周期这样对高阻抗信号源更友好。我连续采样8次后取平均可以有效降低噪声干扰。采集到的原始值通过公式计算出实际电压再映射到空气等级电压低于0.3V对应“优”电压在0.3V到1.0V之间对应“良”电压在1.0V到2.0V之间对应“轻度污染”电压超过2.0V对应“重度污染”这个阈值是根据MQ135在清洁空气中的典型输出电压和污染环境下的响应特性粗略划分的。如果你用的是不同批次或不同品牌的模块阈值可能需要根据实测数据微调。在做原型验证时我一般会在正常房间内记录一组数据作为基线然后靠近酒精、烟雾或者香薰测试一组数据用两组数据的差值来修正判级边界。3.5 OLED显示与串口数据帧协议OLED显示逻辑相对直观SSD1306初始化后我把屏幕划分为三个区域顶部显示“Temp: xx.x C”和“Humi: xx.x%”中间显示光照强度“Lux: xxxx”底部大字体显示空气等级。因为SSD1306不带字库中文字符需要自己取模所以我统一使用ASCII字符集等级用英文缩写EXCELLENT, GOOD, LIGHT, HEAVY表示避免取模的麻烦。实际效果清晰可读界面信息一屏览尽。串口协议我定义成了一个简单但完备的JSON行格式{T:25.3,H:58.2,L:320,A:1}其中T代表温度、H代表湿度、L代表光照强度、A代表空气等级编号。每条数据以换行符结尾。这个格式虽然比纯二进制数据多一点字节开销但调试时可以直接在串口助手里阅读也能被Python、Node-RED等上位机直接解析。实测9600波特率下发送一行约60字节的数据耗时不到70ms完全满足500ms一次的上报频率。4. Proteus仿真设计与联调实践4.1 仿真工程的搭建方法Proteus 8以上的版本自带STM32F103C8T6模型器件库里可以直接搜索并放置。搭建工程时除了放置MCU还需要从库中调出DHT11模型、BH1750模型、MQ135模型、OLED模型以及虚拟终端。特别注意Proteus的DHT11模型与实物行为一致单总线时序必须严格按照手册要求操作才能读到数据BH1750模型支持I2C仿真时能正确响应读取命令MQ135模型则通过一个可调电位器模拟气体浓度变化方便观察ADC电压变化。连线方式和原理图保持一致PB6和PB7接入SDA和SCL上拉后接OLED和BH1750PA1接DHT11PA0接MQ135模块的输出节点。虚拟终端连接在PA9和PA10上用于查看串口输出的数据。连线完成后将编译好的HEX文件加载到MCU模型上开始运行仿真。第一次跑仿真时最容易出现的问题是MCU没有时钟源。Proteus中STM32模型的时钟频率需要手动设置默认可能是4MHz或12MHz如果代码里配置的SystemCoreClock是72MHz而仿真器没有同步修改延时函数就全乱了。在仿真项目的“Design Configure Power Rail”弹出的对话框里把处理器时钟频率设为72MHz同时确认外部晶振参数才能保证和实际硬件运行节奏一致。4.2 仿真中如何模拟传感器数据变化Proteus里的MQ135模型使用一个模拟电位器来改变输出电压。运行仿真后可以通过点击电位器并滑动旋钮来调整电压。如果你希望实现自动变化的阶梯波形可以在信号源中选择“DSP”类别的“SINE”或者“DC”信号源接在MQ135输出节点上作为替代。实际测试中我用一个低频正弦信号源接在PA0节点使电压在0.3V到2.5V之间缓慢变化观察到OLED上的空气等级随时间从“优”变到“重度污染”ADC采集和等级映射逻辑均正确响应。BH1750在仿真中通过一个光强度调节旋钮来改变照度值。把旋钮从低到高旋转OLED屏幕上的光照数据随之线性增长验证了I2C通信链路的正确性。DHT11的温度和湿度则分别在模型属性里设置具体数值修改后点击运行OLED上立即显示出新的温湿度。这比在真实硬件上反复改变环境条件要方便太多特别适合用来验证代码逻辑是否存在明显bug。4.3 仿真与硬件验证的差异点Proteus仿真能帮你验证核心逻辑和大部分时序问题但它和真实硬件仍有几个明显差异需要提前预判。第一仿真的I2C通信时序和真实器件比更理想化信号不存在毛刺或上升沿缓慢导致的问题所以在仿真中跑通的I2C代码烧到实机上可能偶发误码建议使用ST-Link配合逻辑分析仪实际观察一次波形。第二MQ135的真实响应速度非常慢半导体传感器从接触到气体到输出稳定电压往往需要数十秒甚至数分钟而仿真中的电位器电压是即时变化的所以仿真适合验证判级逻辑不适合模拟真实的气体响应曲线。第三DHT11的时序在仿真中相对宽容即使在延时函数上有一点误差也能读到数据但实机上时序要求苛刻得多所以我在3.2节提到的微秒延时实现问题一定要在实机阶段重点验证。5. 常见问题与调试经验实录5.1 DHT11一直返回0或超时这个问题的概率极高我在群里帮不少朋友排查过。首要检查的是GPIO模式和上拉电阻。DHT11通信过程中GPIO需要在输出和输入模式之间切换如果用HAL库配置成GPIO_MODE_OUTPUT_OPEN_DRAIN同时外部加4.7K上拉电阻可以省略模式切换的麻烦开漏输出天然支持输出低电平和输入读电平的双向操作。如果配置成推挽模式数据线就永远被强行拉高或拉低单总线时序无法正常工作。其次是延时精度问题。实测用SysTick实现的us延时在72MHz主频下可以做到正负2us误差再用空循环做延时的话误差可能到10us以上。DHT11对0和1的电平持续时间划分是40us40us到70us的窗口期只有30us延时误差过大会导致全部位识别为同一逻辑值最终结果就是数据校验失败返回0。5.2 OLED白屏或显示乱码白屏通常说明I2C地址配置错误或者上拉电阻缺失。用I2C扫描程序确认一下总线上的设备地址OLED一般是0x3C或0x3D0x3D出现在ADDR引脚接高电平时。如果扫描结果只有0x23BH1750而没有OLED的地址基本可以确定OLED模块供电或者SDA、SCL接线有误。显示乱码则大概率是初始化序列的对比度或显示方向配置问题。SSD1306初始化序列中有一条0xA8命令设置多路复用比率0x1F对应128x32屏幕0x3F对应128x64屏幕。你用的0.96寸OLED大多是128x64如果初始化代码是按32行配置的会出现只有上半屏有内容或字形被拉扁的现象。另外显示方向可以通过0xC8命令上下翻转和0xA1命令左右翻转调整要根据模块的硬件走线灵活配置否则屏幕上的图像可能是镜像或倒置的。5.3 ADC采样值跳变严重MQ135的输出信号本身带有较强噪声特别是模块靠近电源变压器或者开关稳压器时。我在原理图中已经加了0.1uF的滤波电容但实测下来还远远不够最后在软件上做了更激进的处理连续采样8次去掉一个最大值和一个最小值再对剩余6次取平均。配合ADC的硬件过采样最终数据稳定性有了明显提升。另外要检查ADC参考电压是否干净。STM32F103的内部VREF和VDDA是同一个网络如果板子上3.3V电源纹波大参考电压抖动直接反映到采样结果中。建议在VDDA引脚上额外加一个10uF电容和1uH磁珠组成的LC滤波电路实测这个办法能有效减少电源纹波对ADC采样精度的干扰。5.4 串口收到的数据乱码乱码九成是波特率不匹配。ST-Link板载的虚拟串口一般支持常用波特率而CH340模块也兼容广泛但有些国产USB转串口芯片在9600波特率下配合有源晶振精度不够的MCU时会产生微小误差累积。查看MCU的时钟树配置确保USART1的时钟源选择正确如果用的是HSE 8MHz经过PLL倍频到72MHz那USART波特率计算不会有问题。再用示波器或逻辑分析仪测量TX引脚的电平宽度确认每一位信号的实际时间是否和9600波特率理论值相符。还有一个容易忽略的点是接线顺序。很多USB转TTL模块上的TXD和RXD标注是从模块自身角度看的TXD应该接MCU的RXRXD应该接MCU的TX。如果按“同名相接”很容易造成串口收不到任何数据。这个错误不涉及硬件损坏但非常浪费排查时间。5.5 仿真运行时处理器不执行Proteus仿真工程加载HEX文件后屏幕没有反应虚拟终端也没有输出。先检查单片机模型是否设置了正确的晶振频率再检查是否添加了启动代码。STM32F103在Proteus模型里需要勾选“Clock Divider”设置为HSE/1并确保8MHz晶振挂在OSC_IN和OSC_OUT引脚。如果模型里没有外部晶振选项也可以把时钟源改成内部HSI 8MHz代码侧同时把SystemClock配置改为HSI方案。仿真里对时钟的配置容错比实机低很多很多实机上HAL会自动处理的细节在仿真环境都需要手动确认。说实话我在这个项目上踩过的坑比预想的多尤其是DHT11单总线时序和ADC参考电压这两个环节一度都想放弃改用全I2C传感器方案省心了。但坚持调通了之后整个系统的稳定性和可维护性都让我很满意。目前这套系统已经在我们项目组办公室连续运行了将近两个月数据每天通过串口采集到一个Python脚本里自动绘制成温湿度和光照变化曲线。如果你也想做一套类似的环境监测终端我建议先照这个设计跑通原型然后把MQ135换成分辨率更高、选择性更好的SGP30或SGP40再按自己的需求加上报警联动和数据上传这套框架完全可以撑得住后续的二次开发。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门