十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于STM32的开源环境质量监测系统:原理图、代码与Proteus仿真全解析

基于STM32的开源环境质量监测系统:原理图、代码与Proteus仿真全解析 1. 为什么我要把环境质量监测系统做成开源项目环境质量监测这件事说大可以很大说小也可以很小。大到城市级空气质量网格化监测小到一个十几平米的卧室里温湿度是不是让人舒服。我这次做的就是把后者做到极致——一个基于STM32的桌面级环境质量监测系统能测温度、湿度、空气质量带本地显示和报警代码、原理图、仿真工程全部开源。先说说这东西能干什么。它本质上是一个嵌入式数据采集终端STM32做主控DHT11负责温湿度采集MQ系列气敏传感器负责空气质量检测OLED或者LCD做本地显示蜂鸣器做超标报警按键做阈值设置。整套东西成本控制在五十块钱以内一个周末就能焊出来跑通。适合谁看电子类专业的学生做课程设计或者毕业设计嵌入式初学者想找一个完整的、从原理图到代码到仿真都能跑通的项目练手还有那些想给自己书房或者婴儿房加一个空气质量看板的人。我为什么选择开源因为我自己当年入门的时候最痛苦的不是找不到资料而是找到的资料全是碎片——有人给了代码没给原理图有人给了原理图但代码跑不起来有人仿真能跑但实物一焊就废。一个完整的、经过验证的、三个环节代码、原理图、仿真能对得上的项目对初学者来说价值极大。所以我把整个项目整理出来包括踩过的坑和最后的解决方案全部公开。核心关键词先摆出来STM32、环境质量监测系统、原理图、仿真、开源。这五个词贯穿全文后面每一部分都会围绕它们展开。不管你是刚学完51单片机想进阶STM32还是已经用过STM32但没做过完整项目这篇文章都能让你直接抄作业。2. 系统整体设计与方案选型思路2.1 主控芯片为什么选STM32F103C8T6市面上做环境监测的主控选择很多51单片机、Arduino、ESP32、STM32都能做。我选STM32F103C8T6理由很实在。第一性能够用且有余量。Cortex-M3内核72MHz主频64KB Flash20KB SRAM。环境监测这个场景要跑传感器驱动、显示刷新、按键扫描、报警逻辑还要留出后续扩展空间比如加WiFi模块上传数据20KB的SRAM完全撑得住。你要是用STC89C528KB的程序空间写个OLED驱动就快满了后面想加点东西都加不进去。第二生态成熟。STM32F103系列的资料铺天盖地标准外设库、HAL库都有大量例程Keil、STM32CubeIDE、PlatformIO都能开发。遇到问题搜一下基本都能找到答案。这对初学者太重要了——你不想在配置一个GPIO的时候卡三天。第三价格便宜。C8T6核心板现在十块钱左右自己画板子用裸片也就五六块。相比ESP32虽然少了WiFi但环境监测不一定需要联网本地显示加报警已经能满足大部分需求。需要联网的时候再加一个ESP-01S模块成本也就多几块钱。第四引脚资源刚好。这个项目用到的外设DHT11占1个GPIOMQ传感器占1个ADC通道OLED占I2C两个引脚或者SPI四个蜂鸣器1个GPIO按键2到3个GPIO。加起来不到10个引脚C8T6的48个引脚绰绰有余后面想加什么都有地方。注意买C8T6核心板的时候注意看是不是正品芯片。市面上有不少翻新片或者国产替代片价格便宜但ADC精度和稳定性差很多。环境监测对ADC精度有要求MQ传感器的模拟输出直接进ADC芯片不行数据就飘。2.2 传感器选型的取舍逻辑传感器这块我纠结过一阵。温湿度有DHT11、DHT22、SHT30、DS18B20可选空气质量有MQ-2、MQ-135、CCS811、SGP30可选。最后定的是DHT11加MQ-135的组合。为什么DHT11的精度是温度±2℃湿度±5%RH对于室内环境监测够用了。DHT22精度更高但价格贵三倍SHT30精度最高但需要I2C驱动且价格更贵。初学者做项目DHT11的时序驱动是一个很好的学习素材——单总线协议自己写一遍对理解时序帮助很大。而且DHT11的驱动代码网上到处都是出问题了容易对照排查。MQ-135对空气质量氨气、苯系物、烟雾等敏感模拟输出直接接STM32的ADC。它的缺点是输出不是标定过的浓度值只是一个相对变化的电压。但对于“空气质量变差了要报警”这个需求来说相对值完全够用。你不需要知道具体是多少ppm只需要知道比干净空气时高了多少。实操心得MQ-135需要预热。刚上电的时候输出很不稳定要等20到30秒才能读数。我的做法是上电后前30秒显示“预热中”不参与报警判断。这个细节很多例程里没写但实际用的时候不加会误报。2.3 显示与交互方案显示我选了0.96寸OLEDSSD1306驱动I2C接口。理由体积小、功耗低、显示效果好、驱动简单。I2C只需要两根线接在PB6和PB7上不占其他资源。你要是用LCD1602占引脚多还得调对比度电位器麻烦。交互用两个按键一个设置键一个加减键。短按设置键进入阈值设置模式加减键调整报警阈值再按设置键切换下一个参数长按退出。这个交互逻辑简单直接代码量小用户上手快。报警用有源蜂鸣器给高电平就响不需要PWM驱动。加一个LED做视觉指示蜂鸣器响的时候LED同步闪烁。2.4 仿真方案的选择仿真这块我用的是Proteus 8.9。为什么不用Multisim或者LTspice因为Proteus能仿真单片机。你可以在Proteus里放一个STM32模型加载编译好的hex文件然后看OLED显示、蜂鸣器动作、按键响应。这对于验证逻辑正确性非常有用——你可以在打板之前就把代码逻辑跑通省去打板焊接调试的时间。但Proteus仿真STM32有几个坑要注意。第一不是所有STM32型号都有仿真模型F103C8T6有但有些型号没有。第二仿真速度慢尤其是带OLED刷新的场景跑起来一卡一卡的。第三DHT11在Proteus里没有现成模型需要自己用信号发生器模拟时序或者用Proteus的DHT11元件库有些版本自带。我的做法是Proteus仿真主要验证主循环逻辑、按键响应、报警逻辑和显示刷新传感器数据用模拟值代替。实物调试的时候再接真实传感器。这样分工效率最高。3. 硬件原理图设计与关键细节3.1 最小系统部分STM32F103C8T6的最小系统包括主芯片、晶振电路、复位电路、启动模式选择、电源滤波、SWD调试接口。晶振用8MHz无源晶振配两个20pF电容。STM32F103的外部晶振经过PLL倍频到72MHz作为系统时钟。这里有个细节晶振的负载电容要根据晶振规格书来选不是随便拿两个20pF就行。我用的晶振负载电容是20pF所以配20pF电容刚好。如果你用的晶振负载电容是12.5pF那匹配电容要选小一些否则起振不稳定。复位电路用10K上拉电阻加100nF电容经典配置。启动模式BOOT0通过10K电阻下拉到地BOOT1也下拉这样默认从Flash启动。电源部分3.3V稳压用AMS1117-3.3输入5V输出3.3V。输入输出各加一个10uF电解电容和一个100nF陶瓷电容做滤波。这里注意AMS1117的压差大概是1.1V所以输入5V输出3.3V没问题但如果你输入3.7V锂电池输出就只有2.6V了不够。所以供电要么用5V USB要么用7.4V以上的电池加稳压。SWD接口引出SWDIO、SWCLK、GND、3.3V四根线用4Pin排针。下载程序用ST-Link或者DAP-Link都行。3.2 传感器接口电路DHT11的接口很简单VCC接3.3VGND接地DATA接STM32的一个GPIO我用的PA0。DATA线上需要加一个4.7K到10K的上拉电阻。DHT11是单总线协议主机拉低总线至少18ms作为起始信号然后释放DHT11响应。上拉电阻保证总线在空闲时是高电平。MQ-135的接口VCC接5V注意MQ-135需要5V供电3.3V加热电压不够GND接地AO模拟输出接STM32的ADC引脚我用的PA1。MQ-135的AO输出范围是0到5V但STM32的ADC参考电压是3.3V直接接会烧引脚。所以需要分压。我用两个电阻分压10K和20K串联AO接10K上端20K下端接地中间抽头接ADC。这样5V分压后是3.33V刚好在ADC量程内。注意分压电阻的精度直接影响ADC读数准确性。用1%精度的金属膜电阻不要用5%的碳膜电阻。另外分压后的阻抗要匹配STM32的ADC输入阻抗要求一般要求信号源阻抗小于10K。10K和20K并联后的等效阻抗是6.67K满足要求。3.3 显示与报警电路OLED的I2C接口VCC接3.3VGND接地SCL接PB6SDA接PB7。I2C总线上需要上拉电阻一般4.7K。但很多OLED模块自带上拉电阻所以外接不接都行。我建议还是留出焊盘调试的时候如果通信失败先检查上拉电阻。蜂鸣器电路有源蜂鸣器正极接3.3V负极接NPN三极管S8050的集电极三极管基极通过1K电阻接STM32的GPIO我用的PB0发射极接地。STM32输出高电平时三极管导通蜂鸣器响。这里加三极管是因为STM32的GPIO驱动能力有限直接驱动蜂鸣器可能电流不够而且蜂鸣器是感性负载关断时会产生反向电动势可能损坏GPIO。加一个续流二极管1N4148在蜂鸣器两端反接吸收反向电动势。按键电路两个按键一端接地另一端接GPIOPA2和PA3GPIO配置为上拉输入。按键按下时GPIO被拉低松开时被内部上拉拉高。不需要外部上拉电阻STM32内部有。但如果你要加外部上拉4.7K到10K都行。3.4 PCB布局与焊接注意事项如果你要打板布局上注意几点。晶振尽量靠近芯片走线短且粗下面不要走其他信号线。模拟部分MQ-135分压电路和数字部分OLED、按键尽量分开地线单点接地。电源走线要粗至少20mil。SWD接口放在板边方便插调试器。焊接的时候STM32芯片如果买的是LQFP48封装注意引脚间距是0.5mm需要一定的焊接技巧。我的建议是先用烙铁固定对角两个引脚然后拖焊。如果对自己手艺没信心直接买核心板用排针插到底板上省事。实操心得MQ-135传感器第一次使用需要老化。官方建议老化24小时以上让加热丝和敏感材料稳定。我实测下来老化8小时后读数就基本稳定了但前几次上电还是会有漂移。所以代码里我加了一个开机自校准上电后前60秒采集环境本底值作为后续判断的基准。4. 软件代码架构与核心驱动实现4.1 工程结构与开发环境我用的是Keil MDK 5配合STM32F10x标准外设库。为什么不用HAL库因为标准库的代码更直观寄存器操作更透明对初学者理解STM32外设工作原理更有帮助。HAL库封装太厚出了问题不好排查。工程结构是这样的Project/ ├── CMSIS/ # 内核支持文件 ├── FWLIB/ # 标准外设库 │ ├── inc/ │ └── src/ ├── USER/ │ ├── main.c # 主程序 │ ├── stm32f10x_it.c # 中断服务函数 │ └── system_stm32f10x.c ├── HARDWARE/ │ ├── dht11.c/h # DHT11驱动 │ ├── mq135.c/h # MQ135驱动 │ ├── oled.c/h # OLED驱动 │ ├── key.c/h # 按键驱动 │ └── beep.c/h # 蜂鸣器驱动 └── APP/ └── monitor.c/h # 监测逻辑分层清晰硬件驱动和业务逻辑分开。这样你换一个传感器只需要改HARDWARE层APP层不用动。4.2 DHT11单总线驱动实现DHT11的驱动核心是时序控制。单总线协议对时间要求很严格微秒级的误差就可能导致通信失败。起始信号主机拉低总线至少18ms然后拉高20到40us然后释放总线配置为输入模式。DHT11检测到起始信号后等待20到40us然后拉低总线80us作为响应再拉高80us然后开始传输数据。数据传输每一位数据以50us低电平开始然后高电平持续26到28us表示0持续70us表示1。40位数据依次是湿度整数、湿度小数、温度整数、温度小数、校验和。代码实现的关键是微秒延时。我用的是SysTick定时器做微秒延时比空循环准确。具体做法是配置SysTick为1us中断一次在中断里递减计数器。void DHT11_DelayUs(uint32_t us) { uint32_t start SysTick-VAL; uint32_t ticks us * (SystemCoreClock / 1000000); while (1) { uint32_t now SysTick-VAL; uint32_t elapsed; if (now start) elapsed start - now; else elapsed start (SysTick-LOAD - now); if (elapsed ticks) break; } }读取一位数据的逻辑uint8_t DHT11_ReadBit(void) { uint8_t retry 0; while (DHT11_ReadPin() retry 100) { retry; DHT11_DelayUs(1); } retry 0; while (!DHT11_ReadPin() retry 100) { retry; DHT11_DelayUs(1); } DHT11_DelayUs(40); if (DHT11_ReadPin()) { while (DHT11_ReadPin()); return 1; } else { return 0; } }注意DHT11的读取间隔不能小于1秒。连续读取会导致传感器来不及响应返回错误数据。我在主循环里用了一个定时器每2秒读一次既保证数据新鲜度又不会让传感器过载。4.3 MQ-135 ADC采集与滤波MQ-135的输出是模拟电压通过STM32的ADC1通道1PA1采集。ADC配置为12位分辨率连续转换模式采样时间设为55.5个周期最长的采样时间保证精度。原始ADC值波动比较大直接用来判断会误报。我用了滑动平均滤波维护一个长度为10的数组每次新数据进来替换最旧的数据然后求平均。这样滤波后的值平滑很多但响应速度会慢一点。对于环境监测这个场景慢一点没关系稳定最重要。#define FILTER_LEN 10 static uint16_t adc_buf[FILTER_LEN] {0}; static uint8_t adc_idx 0; uint16_t MQ135_GetValue(void) { uint32_t sum 0; adc_buf[adc_idx] ADC_GetConversionValue(ADC1); adc_idx (adc_idx 1) % FILTER_LEN; for (int i 0; i FILTER_LEN; i) { sum adc_buf[i]; } return sum / FILTER_LEN; }除了滑动平均我还加了一个开机自校准。上电后前60秒每秒钟采集一次取平均值作为基准值。后续的报警判断基于基准值的相对变化而不是绝对值。这样不同传感器之间的个体差异就被消除了。4.4 OLED显示驱动与界面设计OLED我用的是SSD1306I2C接口128x64分辨率。驱动代码包括初始化序列、写命令、写数据、设置光标位置、显示字符和字符串。初始化序列比较长主要是设置对比度、显示模式、扫描方向、时钟分频等。这些参数在SSD1306数据手册里都有我直接抄的常见配置。界面设计上我分了三个区域顶部显示温湿度中间显示空气质量底部显示报警状态。温湿度格式是“T:25.3C H:60%”空气质量显示“AQ:1234”报警状态显示“OK”或者“ALARM”。刷新策略每2秒刷新一次传感器数据按键响应实时刷新。OLED刷新的时候要注意不要频繁全屏刷新否则会有闪烁。我的做法是只刷新变化的区域用SSD1306的局部刷新功能。实操心得OLED的I2C地址一般是0x78写地址或者0x3C7位地址。如果你买的模块背面有电阻可以选择地址注意看。我遇到过地址不对导致屏幕不亮的情况排查了半天才发现是模块默认地址和代码里写的不一样。4.5 按键扫描与状态机按键用状态机实现支持短按和长按。状态包括空闲、按下消抖、等待释放、长按判断。typedef enum { KEY_IDLE, KEY_DEBOUNCE, KEY_PRESSED, KEY_LONG_PRESS } KeyState; KeyState key_state KEY_IDLE; uint32_t key_tick 0; uint8_t Key_Scan(void) { uint8_t key_val KEY_Read(); switch (key_state) { case KEY_IDLE: if (key_val 0) { key_state KEY_DEBOUNCE; key_tick GetTick(); } break; case KEY_DEBOUNCE: if (GetTick() - key_tick 20) { if (key_val 0) { key_state KEY_PRESSED; key_tick GetTick(); } else { key_state KEY_IDLE; } } break; case KEY_PRESSED: if (key_val 1) { key_state KEY_IDLE; return KEY_SHORT; } else if (GetTick() - key_tick 1000) { key_state KEY_LONG_PRESS; return KEY_LONG; } break; case KEY_LONG_PRESS: if (key_val 1) { key_state KEY_IDLE; } break; } return KEY_NONE; }消抖时间20ms长按判断1秒。这个参数可以根据手感调整。我试过10ms消抖偶尔会误触发20ms比较稳。5. Proteus仿真工程搭建与调试5.1 仿真元件库的准备Proteus 8.9自带STM32F103C8T6的仿真模型但DHT11和MQ-135没有。DHT11可以用Proteus的“DHT11”元件有些版本在“Sensor”分类里如果没有可以用一个自定义的信号发生器模拟单总线时序。MQ-135可以用一个电位器模拟手动调节输出电压来模拟空气质量变化。OLED用Proteus的“OLED12864I2C”元件SSD1306驱动和实物一致。蜂鸣器用“BUZZER”元件按键用“BUTTON”元件。5.2 仿真电路连接在Proteus里画原理图和实物原理图基本一致。注意几点STM32的电源引脚要接上VCC和GND否则仿真不运行。晶振电路可以省略Proteus里STM32默认使用内部时钟。SWD接口不需要因为仿真直接加载hex文件。DHT11的数据引脚接PA0MQ-135的模拟输出接PA1OLED的SCL接PB6、SDA接PB7蜂鸣器接PB0按键接PA2和PA3。5.3 仿真调试技巧加载hex文件双击STM32元件在“Program File”里选择Keil编译生成的hex文件。时钟频率设为72MHz。仿真运行后如果OLED不显示先检查I2C地址和初始化序列。如果DHT11读数不对检查时序延时是否准确。Proteus的仿真时间和真实时间有差异微秒延时在仿真里可能不准所以DHT11在仿真里可能读不出数据。我的做法是仿真时用固定值代替DHT11读数只验证显示和报警逻辑。注意Proteus仿真STM32的ADC时输入电压不要超过3.3V否则仿真会报错。MQ-135的模拟输出用电位器分压到0到3.3V之间。仿真跑通之后把hex文件烧到实物里基本就能直接运行。但实物调试的时候传感器读数可能需要微调比如DHT11的时序延时、MQ-135的基准值。这些在仿真里验证不了只能实物调。6. 常见问题排查与避坑经验6.1 程序下载失败这是最常见的问题。排查顺序第一检查SWD接线SWDIO、SWCLK、GND、3.3V四根线是否接对。第二检查STM32的BOOT0和BOOT1是否都下拉到地。第三检查调试器驱动是否安装。第四如果用的是ST-Link检查Keil里的调试设置是否选对了调试器型号。第五如果还是不行试试按住复位键点击下载然后松开复位键。我遇到过一种情况STM32的Flash被写保护了下载的时候提示“Flash Download failed”。解决办法是用ST-Link Utility连接解除写保护然后重新下载。6.2 DHT11读数失败DHT11读不出数据原因通常有三个时序不对、上拉电阻没接、读取间隔太短。时序问题最常见。DHT11对延时精度要求高如果你用空循环延时在不同优化等级下延时时间会变。建议用SysTick或者定时器做延时。另外起始信号拉低时间要足够至少18ms我一般用20ms。上拉电阻DHT11的数据线必须接上拉电阻4.7K到10K。有些模块自带上拉但如果你用的是裸传感器一定要接。读取间隔DHT11两次读取之间至少间隔1秒。我见过有人放在主循环里不停读结果一直返回错误。加一个定时器控制读取频率就好了。6.3 OLED显示异常OLED不亮、花屏、显示错位排查思路第一检查I2C地址0x78还是0x3C。第二检查初始化序列对比SSD1306数据手册。第三检查电源OLED需要3.3V5V可能烧毁。第四检查I2C上拉电阻有些模块不带需要外接。花屏通常是初始化序列不对或者通信速率太快。降低I2C速率试试。显示错位检查设置光标位置的命令是否正确。6.4 MQ-135读数漂移MQ-135读数漂移是正常现象气敏传感器受温湿度影响大。解决办法第一充分预热至少30秒。第二开机自校准采集本底值。第三滑动平均滤波。第四如果要求高加温度补偿用DHT11的温度值修正MQ-135的读数。我实测下来不加温度补偿的情况下夏天和冬天同一环境的读数能差20%左右。如果你只是做相对判断空气质量变差了报警这个差异可以接受。如果要显示具体浓度值必须加补偿。6.5 仿真与实物不一致仿真能跑实物跑不了或者反过来。原因通常是仿真模型和实物有差异比如STM32的ADC在仿真里是理想值实物有噪声。或者仿真里的时序和实物不一致比如DHT11在仿真里响应快实物响应慢。我的建议是仿真主要验证逻辑实物调试才是重点。仿真跑通后实物调试时先单独测试每个模块确认每个模块都能正常工作再整合到一起。这样出问题了容易定位。问题现象可能原因排查方法解决方案程序下载失败SWD接线错误检查四根线重新接线程序下载失败Flash写保护ST-Link Utility查看解除写保护DHT11无数据时序不对示波器看波形调整延时DHT11无数据上拉电阻缺失万用表测电压加4.7K上拉OLED不亮I2C地址错误扫描I2C地址改代码地址OLED花屏初始化序列错误对比数据手册修正初始化MQ135漂移未预热等待30秒加预热逻辑MQ135漂移无温度补偿对比温湿度加补偿算法仿真正常实物异常电源问题测电压换稳压模块仿真正常实物异常时序差异示波器对比调整延时实操心得调试的时候我习惯先写一个最简单的测试程序只点亮一个LED确认下载和运行正常。然后逐步加模块每加一个测试一个。这样出问题了知道是哪个模块的问题。不要一次性把所有代码写完再调试那样出了问题排查起来很痛苦。7. 项目扩展方向与个人体会这个项目做完之后我陆续加了一些扩展。一个是加ESP-01S WiFi模块把数据上传到本地服务器用手机看历史曲线。另一个是加SD卡模块做本地数据记录方便回溯。还有一个是加继电器空气质量超标时自动开启新风系统。代码和原理图我都放在开源仓库里了包括Keil工程、Proteus仿真文件、原理图PDF和BOM清单。你可以直接下载下来打板焊接烧录程序一个周末就能跑起来。最后分享一个小技巧如果你觉得DHT11精度不够可以换成SHT30I2C接口代码改动不大只需要把DHT11驱动换成SHT30驱动APP层的逻辑不用动。这就是分层架构的好处。我个人在实际操作中的体会是嵌入式项目最花时间的不是写代码而是调试硬件。一个虚焊、一个接反的电容、一个选错的电阻都能让你卡半天。所以原理图设计的时候多花点时间检查焊接的时候多测测通断能省下大量调试时间。另外开源项目的价值不在于代码多高级而在于完整和可复现。一个能跑通的简单项目比一个跑不通的复杂项目有价值得多。
返回列表