STM32环境监测:温湿度、甲醛、二氧化碳代码与仿真

📅 发布时间:2026/9/14 19:27:57
STM32环境监测:温湿度、甲醛、二氧化碳代码与仿真
简介基于STM32的环境监测系统设计与仿真资源面向嵌入式系统学习者、电子竞赛备赛者以及需要快速完成室内空气质量监测项目的人群。系统实现温湿度、甲醛浓度、二氧化碳浓度三路实时监测超出阈值自动报警涵盖完整工程代码与Proteus仿真便于直接运行验证与实际部署参考。压缩包共172个文件以C语言源码、头文件、编译生成的hex可执行文件及仿真工程文件为主另含Keil/Proteus工作区配置、演示录屏和辅助脚本整体约27.79MB目录结构清晰便于按需查阅。已有230人学习下载可帮助读者理解传感器数据采集、阈值判断与LCD显示的整体流程并为二次功能扩展提供可修改的基础工程。1. 环境监测不只是一块屏STM32 把温湿度、甲醛、二氧化碳搬进同一个工程室内甲醛释放周期可以长达十几年二氧化碳到了 1000ppm 头就开始发沉。做一块环境监测板难点不在于多接几个传感器而是把温湿度的数字时序、甲醛的模拟小信号、二氧化碳的红外串口帧统一到一个 MCU 程序里。基于 STM32 环境监测温湿度甲醛二氧化碳空气质量代码仿真这类题目用 STM32F103 这样的入门芯片就能胜任一路单总线读温湿度一路 ADC 采甲醛电压一路串口读 CO2 浓度再在 Proteus 里先把工程跑通验证逻辑后再焊板子。这篇文章会按这条链路讲清楚传感器怎么选、代码怎么写、仿真怎么搭最后给出标定补偿的工程做法适合正在做毕业设计、智能家居原型或者想自己从零复现一套室内空气质量采集系统的读者。2. 传感器选型与信号链温湿度、甲醛、CO2 分别走哪条总线2.1 三类传感器的输出接口为什么不一样温湿度传感器的主流方案是数字输出DHT11/DHT22 用单总线协议SHT30 用 I2C数据已经完成温度补偿甲醛传感器大多数是电化学原理电极输出微弱的电流信号模块内部经过 I-V 变换后给出 0~2V 或 0~5V 的模拟电压和浓度近似成正比二氧化碳传感器常用非色散红外NDIR原理模块内部把测量结果直接通过 UART、PWM 或 I2C 发出来。所以从接口角度看环境监测扩展板天然需要 GPIO、ADC、UART、I2C 四类资源。STM32F103C8T6 在这一点上很合适内部集成多路 12 位 ADC、多个 USART、硬件 I2C 和丰富 GPIO一个芯片不用外加扩展器件就能把上述传感器全部挂上。2.2 常用传感器型号与关键参数被测对象传感器模块输出形式量程参考典型精度接口温湿度DHT22单总线数字-40~80°C0~100%RH±0.5°C±2%RH1-Wire GPIO甲醛ZE07-CH2O0~2V 模拟电压0~3ppm±0.01ppmADC二氧化碳MH-Z19BUART/PWM400~5000ppm±50ppm 或 ±5%USART备选环境量SGP30I2C VOC/eCO2400~60000ppm室内级估算I2C参数表里有几个容易误判的地方DHT22 的“±2%RH”是在 25°C 附近的典型值湿度越低允差越大ZE07-CH2O 需要预热至少 3 分钟才能进入稳定工作区标定时不能上电就读数MH-Z19B 的 UART 是 9600bps、8N1数据帧固定 9 字节。按这张表选型时优先选输入电压 5V 以内的模块否则还要额外加电平转换和稳压电路。SGP30 的 eCO2 是根据 TVOC 估算出来的不等于真实 CO2如果论文或产品里需要严格意义上的二氧化碳浓度应当选择 NDIR 原理的传感器而不是拿 SGP30 的估算值下结论。2.3 通用接线与信号调理我一般把传感器分布到三组引脚上温湿度接 PA0甲醛模拟输出接 PA1CO2 串口接 USART2 的 PA2/PA3I2C 保留给 OLED 屏幕或 SGP30。电化学甲醛模块的内阻很高模拟输出容易被单片机采样时的寄生电容影响所以需要在模块输出和 STM32 ADC 之间加一个 100Ω 电阻和一个 0.1μF 电容组成低通滤波再从滤波点引线到 PA1。采样时不要用 ADC 内置的短采样时间电化学传感器输出源阻抗较高把 ADC 的采样时间配置到 55.5 或 239.5 个 ADC 时钟周期读数会更稳定。CO2 模块用 3.3V 供电时部分批次会因电流需求波动造成电压跌落常见做法是在模块电源引脚附近并联一个 100μF 电解电容并在软件中增加上电延时。#define DHT_GPIO GPIOA #define DHT_PIN GPIO_Pin_0 #define HCHO_ADC ADC1 #define HCHO_CH ADC_Channel_1 #define CO2_USART USART2 void Sensor_GPIO_Init(void) { GPIO_InitTypeDef gpio; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_ADC1, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE); gpio.GPIO_Mode GPIO_Mode_Out_PP; // DHT 引脚先设为推挽输出用于发送起始信号 gpio.GPIO_Speed GPIO_Speed_50MHz; gpio.GPIO_Pin DHT_PIN; GPIO_Init(DHT_GPIO, gpio); gpio.GPIO_Mode GPIO_Mode_AIN; // ADC 输入 gpio.GPIO_Pin GPIO_Pin_1; GPIO_Init(GPIOA, gpio); }这段初始化代码的作用是把三类接口用到的 GPIO 分成两组DHT 引脚在读取前要由输出模式临时切到输入模式所以先配置成开漏输出PA1 必须配置为模拟输入否则 ADC 读到的会一直是数字电平干扰后的结果。USART2 的 TX/RX 复用配置放在串口初始化里一起做不能只开 GPIO 时钟不开复用时钟。注意这里没有配置 I2C 引脚如果后续接入 SGP30 或 OLED通常把 PB6/PB7 配置为开漏复用输出并外接 4.7kΩ 上拉电阻。3. STM32 代码实现从初始化到数据帧解析3.1 标准库工程里的时钟与外设配置标题里的“代码”部分我建议先用 STM32 标准库而不是 HAL 库因为网上能找到的温湿度、电化学传感器例程大多基于标准库资料多改起来直接。KEIL 工程创建后先确认系统时钟和 ADC 时钟没有冲突使用 HSE 8MHz 晶振PLL 倍频到 72MHzADC 预分频设置为 6 分频得到 12MHz ADC 时钟这在 STM32F103 的 ADC 规格内。若使用 Proteus 仿真晶振频率必须和代码里的一致否则串口波特率和 DHT 时序都会偏移。void SystemClock_Config_StdPeriph(void) { ErrorStatus status; RCC_DeInit(); status RCC_HSEConfig(RCC_HSE_ON); if (status SUCCESS) { RCC_HCLKConfig(RCC_SYSCLK_Div1); RCC_PCLK1Config(RCC_HCLK_Div2); // 72MHz/2 36MHz RCC_PCLK2Config(RCC_HCLK_Div1); FLASH_SetLatency(FLASH_Latency_2); FLASH_PrefetchBufferCmd(FLASH_PrefetchBuffer_Enable); RCC_PLLConfig(RCC_PLLSource_HSE_Div1, RCC_PLLMul_9); // 8*972 RCC_PLLCmd(ENABLE); while (RCC_GetFlagStatus(RCC_FLAG_PLLRDY) RESET) {} RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK); while (RCC_GetSYSCLKSource() ! 0x08) {} } }这里的参数必须和硬件晶振匹配8MHz 晶振对应RCC_PLLSource_HSE_Div1和RCC_PLLMul_9如果开发板上焊接的是 12MHz 晶振倍频系数要改成 6否则 USART 波特率会出现明显误差。这段代码来自标准库功能是完成时钟切换后等待 PLL 锁定。3.2 DHT22 温湿度读取的时序关键点DHT22 是单总线器件一次完整读取的过程是主机把总线拉低 18ms 以上发起开始信号释放总线后等待从机响应从机先拉低 80μs 再拉高 80μs然后输出 40 位数据其中湿度高字节在前温度高字节在后最后是校验和。STM32 运行在 72MHz 时直接用delay_us循环计时在 Proteus 仿真里虚拟器件模型对时序更敏感因此我建议把等待超时控制在 200μs 以内否则仿真时会卡死在循环里。一个可用的读取函数如下uint8_t DHT22_ReadData(uint16_t *humi, int16_t *temp) { uint8_t data[4] {0}; uint8_t bits[8] {0}; uint16_t timeout 0; GPIO_ResetBits(DHT_GPIO, DHT_PIN); Delay_Ms(2); // 拉低 2ms发起开始信号 GPIO_SetBits(DHT_GPIO, DHT_PIN); Delay_Us(30); for (int i 0; i 8; i) { while (GPIO_ReadInputDataBit(DHT_GPIO, DHT_PIN) RESET) { if (timeout 200) return 1; // 等低电平结束 } Timing_Delay_Us(40); // 低电平位置采样 bits[i] GPIO_ReadInputDataBit(DHT_GPIO, DHT_PIN); while (GPIO_ReadInputDataBit(DHT_GPIO, DHT_PIN) SET) { if (timeout 200) return 1; // 等高电平结束 } } for (int i 0; i 4; i) { data[i] (bits[i * 2] 7) | (bits[i * 2 1] 6) | (bits[i * 2 2] 5) | (bits[i * 2 3] 4) | (bits[i * 2 4] 3) | (bits[i * 2 5] 2) | (bits[i * 2 6] 1) | bits[i * 2 7]; } *humi (data[0] 8) | data[1]; *temp ((data[2] 0x7F) 8) | data[3]; if (data[2] 0x80) *temp -*temp; return ((data[0] data[1] data[2] data[3]) 0xFF) data[4]; }这里的核心逻辑是“等待低电平结束→延时 40μs 采样→等待高电平结束”。40μs 这个延时用于区分每一位的 0 和 1脉冲宽度在 26~28μs 左右代表 070μs 左右代表 1采样点放在低电平后的 40μs 处正好落在高电平区间内。DHT22 的分辨率达到 0.1°C湿度是 16 位无符号温度是 16 位带符号符号位在第一个温度字节的最高位DHT11 只有整数不能用同一套解析。调试时如果返回值一直是 1先用示波器看 DHT 引脚的响应波形多数是 GPIO 输入输出切换时模式未更新其次是开始信号的拉低时间不够或者延时函数被中断破坏。3.3 甲醛 ADC 采样与电压-浓度换算电化学甲醛模块输出的是线性电压以 0~2V 对应 0~3ppm 为例浓度值等于电压值乘以 1.5ppm/V。但 ADC 的参考电压通常是 3.3V所以要先算出实际电压。为了保证稳定性每次读取时连续采样 8 次直接取平均再通过滤波系数做一阶低通。写法和参数说明如下uint16_t ADC1_ReadChannel(uint8_t ch) { ADC_RegularChannelConfig(ADC1, ch, 1, ADC_SampleTime_239Cycles5); ADC_SoftwareStartConvCmd(ADC1, ENABLE); while (ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) RESET) {} return ADC_GetConversionValue(ADC1); } float HCHO_GetPPM(void) { uint32_t sum 0; for (int i 0; i 8; i) sum ADC1_ReadChannel(ADC_Channel_1); sum / 8; float v (float)sum / 4095.0f * 3.3f; return v * 1.5f; }ADC_SampleTime_239Cycles5决定了单次采样时间为 239.5 个 ADC 时钟周期在 12MHz ADC 时钟下约 20μs适合源阻抗较高的电化学模块。这里的量程系数 1.5 来自传感器标称输出必须在代码注释里写明依据不同厂商的传感器量程可能是 0~5V 对应 0~3ppm换算系数会完全不同。另外STM32 的 ADC 引脚内部有寄生电容如果外部滤波电容过大采样保持电路会来不及充电所以 RC 滤波不能无限加大0.1μF 在多数场景下是折中选择。实际调试时如果HCHO_GetPPM返回值跳动超过 ±0.05ppm优先检查 PA1 是否受到板子上的 5V 电源纹波干扰。3.4 MH-Z19 二氧化碳 UART 数据帧解析MH-Z19 的串口指令很短读气体浓度的请求帧固定 9 字节FF 01 86 00 00 00 00 00 79响应帧第 2 字节和第 3 字节是 CO2 浓度的高位和低位。发送请求后等待 20~50ms然后从串口缓冲中取 9 个字节校验和是从第 1 字节到第 7 字节累加取反再加 1。对应的数据帧格式如下字节序号请求帧内容响应帧含义00xFF帧头10x01功能码20x86读浓度命令30x00浓度高位40x00浓度低位50x00保留字节60x00保留字节70x00保留字节80x79校验和一个简化实现是uint8_t CO2_RequestFrame[9] {0xFF, 0x01, 0x86, 0x00, 0x00, 0x00, 0x00, 0x00, 0x79}; uint16_t MHZ19_ReadCO2(void) { uint8_t buf[9]; USART_SendArray(CO2_USART, CO2_RequestFrame, 9); Delay_Ms(50); // 等待传感器响应 if (USART_RecvArray(CO2_USART, buf, 9) ! 9) return 0xFFFF; if (!CO2_CheckSum(buf)) return 0xFFFE; return (uint16_t)(buf[2] 8) | buf[3]; }注意不同批次的 MH-Z19 响应帧在 3.3V/5V 供电下偶有差异有的模块在帧头前多一个字节空闲量建议先用串口抓取原始数据确认字节偏移。UART 配置为 9600bps、8 位数据、无校验、1 停止位不要开启硬件流控。Delay_Ms(50)是阻塞等待主循环里调用时会占用 CPU改进方法是把请求和解析放进状态机或者用 DMA 加空闲中断。对实时性要求不高的环境监测场景阻塞版已经够用。3.5 三路传感器的主循环调度环境监测系统不需要所有传感器同时读取尤其电化学模块和 NDIR 模块都对电源瞬态敏感。常见做法是定时 2 秒轮询一次一次周期内依次读 CO2、甲醛、温湿度错开传感器工作电流尖峰。使用 SysTick 做 1ms 时基主循环里做非阻塞判断static uint32_t lastTick 0; while (1) { uint32_t now GetSysTick(); if (now - lastTick 2000) { lastTick now; CO2_Read MHZ19_ReadCO2(); HCHO_Read HCHO_GetPPM(); DHT22_ReadData(humi, temp); // 在这里刷新 LCD 或串口日志 } }这段代码把三个读取函数放在同一个 2 秒周期内对原型演示足够。但在产品化时要注意DHT22 本身要求两次读取间隔不小于 1s甲醛模块需要长时间预热CO2 传感器自校准周期长不能把重采样频率提得太高。数据稳定性比更新速率更重要这是环境监测和高速采集最大的区别。4. Proteus 仿真 STM32 环境监测系统先跑逻辑再焊板子4.1 仿真能验证什么不能验证什么先说结论Proteus 的 STM32 模型可以仿真 GPIO、ADC、UART、I2C 和中断因此代码移植、数据帧解析、阈值报警、LCD 显示这些逻辑都能在电脑上跑通但电化学甲醛传感器和 NDIR 二氧化碳传感器的真实响应曲线不会出现在仿真里。常见做法是用电位器模拟甲醛模块的 0~2V 输出用虚拟串口终端回发 MH-Z19 的数据帧用 Proteus 自带的 DHT11 模型验证温湿度读取流程。这样就把仿真重点放在 STM32 代码对三种信号的采集与显示上。提示在 Proteus 中给 STM32 添加虚拟串口终端时注意把终端设置为 Hex Display否则在终端窗口里只能看到 ASCII 字符无法核对 CO2 数据帧原始的十六进制内容。4.2 Proteus 元件摆放与虚拟连接在 Proteus ISIS 中新建工程后从元件库添加 STM32F103C8然后添加 LCD1602 或 OLED、一个电位器 POT-HG、一个虚拟终端 VTERM。使用 Virtual Terminal 时注意 RX/TX 连接方向STM32 的 TX 接终端 RXSTM32 的 RX 接终端 TX。双击 STM32 芯片在 Program File 中选择 Keil 生成的 hex 文件CKSEL 选择外部晶振晶振频率填 8MHz。下面这张表是出图之前最常用的元件清单元件关键字用途关键配置STM32F103C8主控Program File 写 hexCrystal Frequency 8MHzPOT-HG模拟甲醛电压上端接 5V下端接 GND抽头接 PA1VTERM模拟 CO2 串口波特率 96008N1勾选 Hex DisplayDHT11温湿度与 PA0 连接不同版本模型响应速度不同LCD1602显示接并行口或用 PCF8574I2C 模型DHT11 模型在 Proteus 中有多个版本老版本的时序模型比真实芯片慢代码里 20μs 级别的延时在仿真时容易出现读取失败。如果遇到这种情况先把 DHT 读取函数中的超时上限放宽到 500μs确认逻辑正确后再恢复到 200μs 移植到真实硬件上。甲醛模拟部分电位器输出电压范围要限制在 0~2V不能直接使用 5V否则代码计算出的 ppm 会超出合理范围。4.3 仿真调试的三个高频问题第一个高频问题是程序烧进去后虚拟终端一个字节都不出。先检查 Keil 是否生成 hex 文件默认工程只生成 axf需要在Option for Target - Output里勾选Create HEX File修改后重新编译再检查 STM32 模型里的时钟配置是否与代码中的 8MHz 外部晶振一致。第二个问题是 ADC 读数全量程或全零在 Proteus 里电位器抽头和 PA1 之间不能直接连一根导线因为模型里 ADC 输入阻抗很敏感建议串联一个 10kΩ 电阻再进 PA1与真实硬件的小 RC 滤波等效。第三个问题是串口收到的 CO2 数据乱码Virtual Terminal 的波特率要与代码里 USART 初始化一致但 Proteus 的串口模块在某些版本里对带小数波特率寄存器的计算存在误差把系统时钟外部晶振从 8MHz 改成 72MHz 直连能规避但这样会偏离真实硬件我一般保持 8MHz并在代码里允许波特率误差容忍度。#ifdef PROTEUS_SIM const uint16_t fakeCO2 600; const int16_t fakeTemp 26; const uint16_t fakeHumi 55; #else uint16_t fakeCO2 MHZ19_ReadCO2(); int16_t fakeTemp 0; uint16_t fakeHumi 0; DHT22_ReadData(fakeHumi, fakeTemp); #endif这段仿真专用代码写在一个#ifdef里通过 Keil 的C/C Define窗口定义PROTEUS_SIM来控制。这样在 Proteus 里可以先把显示、报警逻辑测完再关闭宏回到真实传感器数据避免被传感器模型的不确定性干扰。注意在正式交付代码时不要保留该宏否则烧进真实板子后读到的永远是假数据。5. 交叉干扰与标定补偿让甲醛和 CO2 数据在真实环境里更可信5.1 零点校准与温湿度补偿电化学甲醛传感器在出厂时做过标定但安装到系统后会面临零点漂移。常见做法是在干净室外或密封袋中采集一段基线电压取 50 个样本的平均值作为Vzero每次计算时减去它。温度对电化学传感器的影响用一次近似温度每升高 10°C灵敏度大约增加 5%但具体系数需要根据传感器规格书或实测曲线调整不能直接用经验值当结论。在代码里加一个补偿函数把温度、湿度系数作为可调参数输出到串口日志里方便后续校准。float HCHO_Compensate(float ppm, float tempC, float rh) { // 温度补偿以 25°C 为基准每偏离 1°C 修正 -2% float tComp 1.0f - 0.02f * (tempC - 25.0f); // 湿度补偿湿度超过 60% 后引入 10% 的交叉修正 float hComp (rh 60.0f) ? 1.0f - 0.1f : 1.0f; return ppm * tComp * hComp; }这里的系数 0.02 和 0.1 是工程上可调的占位值真实系统要用标准气体法重新拟合。标定流程是先通洁净空气记录零点再通 1.0ppm 甲醛标准气体记录系数写入 Flash 断电保存上电后前 10 秒显示预热倒计时不参与显示和控制避免把预热漂移当成浓度波动。5.2 二次滑动平均与阈值迟滞验证环境监测数据的展示不需要毫秒级响应反而需要让人眼感觉到稳定。我在读取缓存里维护 10 个样本排序后取中间 6 个的平均值比普通滑动平均更抗尖峰。当甲醛超过 0.08ppm 或 CO2 超过 1000ppm 时把阈值状态通过 GPIO 亮灯或蜂鸣器输出阈值区域设置一个 30 秒的迟滞避免风扇在临界点反复通断。用串口调试助手观察每秒一行数据格式为T26.3 H55.2 F0.08 C850同时对比独立温湿度计和手持甲醛仪的读数观察曲线是否随时间平滑变化。如果数据在更换房间后 30 分钟内仍无明显趋势优先检查传感器预热是否完成而不是怀疑代码算术逻辑。本文还有配套的精品资源点击获取