基于STM32的多传感器环境监测系统:从原理图到Proteus仿真实战
2026/9/13 15:01:36 网站建设 项目流程

这套项目其实是我去年年底整理完毕的,包含完整的STM32工程代码、可生产的原理图以及一套Proteus仿真工程。最初只是想给自己实验室做一台环境监测小设备,后来陆续有朋友和同学找我要资料,干脆抽出时间把所有文件补全、注释写清楚,整理成一套开源项目放了出来。如果你正在做课设、毕设,或者刚入手STM32想找个综合性强但又不至于啃不动的实战项目,这套东西会很对你的胃口——它覆盖了GPIO、定时器、ADC、I2C、单总线通信、中断、状态机这些嵌入式基本功,同时又是一套能真实跑起来、能测出数据的完整系统,而不是那种点个灯就结束的demo。

1. 这套系统到底做了什么事

1.1 我最初想解决的实际问题

实验室里放了几台仪器,常年没人管,夏天温度一高有些设备就开始不稳定,但等发现的时候往往已经晚了。我需要的不是一台专业的工业环境监测仪,而是一台能实时盯着温度、湿度、空气质量,超限了能主动通知我,最好还能把数据留下来的小设备。市面上成品环境监测仪价格从几百到几千都有,功能倒是齐全,但没法按我的需求二次开发,数据接口也不开放。

自己动手做一个其实不复杂,关键是把“能用的系统”和“能跑通的demo”区分开。市面上的STM32开发板套件很多,但大部分例程都是单个传感器单独演示,真正组合在一起的时候,时序冲突、ADC通道切换、电源噪声这些问题就全冒出来了。这套项目就是把传感器数据采集、本地显示、超限报警、串口数据输出串成一条完整的链路,你拿到手之后烧进去就能跑,也能很清楚地看到每一层是怎么工作的。

1.2 监测指标与硬件构成总览

环境质量这个说法很大,真要全测齐全,那得上一套气象站了。所以我在立项的时候做了取舍,选了四类最有代表性的指标:

监测项传感器选型接口方式说明
温度、湿度DHT11单总线经典入门传感器,时序典型,适合学习
空气质量(烟雾/可燃气体)MQ-2ADC模拟量能测液化气、丁烷、烟雾等
PM2.5/粉尘浓度GP2Y1010AUADC模拟量(带PWM驱动)夏普的经典粉尘传感器
光照强度BH1750I2C数字输出,直接读lux

主控用的是STM32F103C8T6,这是一颗Cortex-M3内核、64KB Flash、20KB RAM的小芯片,资源虽然不算大,但跑这套系统绰绰有余。输出端搭配一块0.96寸OLED屏(I2C接口)做实时显示,一个无源蜂鸣器做报警,三个按键用于切换显示页面和设置报警阈值,一路串口用于和上位机通信。

1.3 代码、原理图、仿真三份交付物各自的价值

开源资料里我放了三个相对独立的部分。工程代码是核心,用的是标准外设库(Standard Peripheral Library),之所以不用HAL库是我个人习惯问题,后面会细说。原理图是PDF加工程文件两种格式,用嘉立创EDA画的,你可以直接打开看,也可以照着画板。仿真工程则是Proteus 8 Professional版本,里面已经把固件烧好了,打开就能跑。

三份东西对应三种使用场景:如果你想改功能、学代码,就看工程源码;如果你想自己打板做实物,就照原理图走;如果你暂时手上没有硬件,或者想先快速验证逻辑,仿真工程可以直接在电脑上跑。对于正在做课设的同学,这三份东西凑齐了,从设计报告到实物演示再到仿真截图,基本就完整了。

2. 硬件选型:这套组合背后的取舍逻辑

2.1 为什么是STM32F103C8T6而不是别的

选主控的时候,很多人会有疑问:测个温湿度、空气质量,用STC89C52或者Arduino Nano不就行了吗?确实能做,51单片机资源太紧张,跑个DHT11驱动加数码管显示就到头了,想上OLED、想加联网模块、想做多路ADC采集,Flash和RAM立刻捉襟见肘。Arduino倒是开发快,但它的抽象层把底层寄存器全包住了,做完一个项目你对MCU本身还是一无所知,这对学习来说反而是损失。

STM32F103C8T6这颗芯片,现在价格已经打到很低的水平(散片甚至不到十块钱),它恰好站在“资源够用”和“有学习深度”的交叉点上。APB1/APB2两条总线上的外设时钟可以独立配置,ADC支持规则组和注入组,定时器有PWM和输入捕获,这些机制是嵌入式的通用知识,你在这里学会了,以后换GD32、换N32、换其他Cortex-M芯片都能平滑迁移。而且这颗芯片的资料密度是全网最高的,遇到问题随便一搜就能找到答案,对新手极其友好。

2.2 传感器逐个拆解:接口、精度与成本

DHT11是单总线数字温湿度传感器,单总线是一条数据线既做供电又要传数据,时序要求很严格。它的精度是湿度±5%RH、温度±2℃,这个精度放在实验室环境监测里只能说够看,谈不上准,但它便宜、例程多、时序非常典型,用来学习单总线协议是很合适的。如果你的项目对精度有要求,可以平替成DHT22或者SHT30,代码层只需要改驱动,上层逻辑不用动。

MQ-2属于半导体气敏传感器,内部有一个加热电阻和一个二氧化锡半导体气敏层。工作时加热电阻要保持通电,所以模块整体功耗不小,实测大约150mW左右,比DHT11高一个数量级。它的输出有模拟量(AO)和数字量(DO)两种,我接的是AO,通过STM32的ADC读取电压值,再查气体浓度曲线换算成PPM。注意这个东西是“定性半定量”的,它告诉你“气体浓度上升了”是准确的,但具体是多少PPM,误差可能有三四成,用途定位是报警而不是精密计量。

GP2Y1010AU这个传感器有点意思,它有六个引脚,其中一个引脚需要接PWM脉冲驱动内部红外LED,LED导通时照射到粉尘颗粒产生反射光,接收管输出的电压经放大后送到VO引脚。所以它不只是简单接个ADC,还需要定时器产生一个周期10ms、脉宽320μs的脉冲,而且采样电压要在脉冲开始后某个特定时间窗口内读取,时序上有个讲究,后面代码部分会细讲。

BH1750是用I2C接口的数字光照传感器,芯片内部集成了光电二极管、积分放大器和16位ADC,直接通过I2C寄存器读出来的就是 lux 值,不需要自己做电压换算。I2C总线需要外接上拉电阻,模块上一般自带了,如果自己画板就要记得加。

2.3 供电与电平匹配的注意事项

整套系统的供电路径是这样的:USB的5V进来,一部分直接给传感器模块供电(DHT11、MQ-2、蜂鸣器都支持5V),另一路通过AMS1117-3.3稳压到3.3V给STM32、OLED和BH1750。这个设计是故意的——如果所有东西都统一3.3V供电也行,但MQ-2的加热丝在5V下工作状态最标准,DHT11在5V下的时序参数也最典型。

但这里有个非常容易踩的坑:5V供电的传感器,信号线输出到3.3V的STM32,引脚电平会不会超?DHT11的数据引脚在5V供电时高电平接近5V,直接接在STM32的PB口上,长期工作有损坏风险。我实测的情况是短时间问题不大,但不太稳妥。项目中做的处理比较简单实用:DHT11的数据线上串联一个1kΩ电阻,再对地接一个3.3V稳压管。如果你是直接用现成模块,很多模块板上已经做了电平转换,那就省心很多。另外系统内部所有传感器、主控必须共地,否则I2C和单总线通信会出现各种随机性错误。

提示:自己画板的时候,电源入口处一定要加一个100μF电解电容和0.1μF瓷片电容并联,放在USB座附近。这个位置少两个电容,ADC采样的噪声会让你怀疑人生。

3. 原理图设计:从最小系统到各传感器接口电路

3.1 STM32F103C8T6最小系统的四个必要部分

所谓最小系统,就是芯片能运行起来的最少外围电路。它有四个必要部分:电源、晶振、复位、启动模式配置。

电源部分,STM32F103C8T6有VDD、VDDA、VBAT等电源引脚,每一个VDD引脚旁边都要就近放一个100nF去耦电容,VDDA引脚要接一个1μF+0.01μF的组合滤波电容。曾经见过有人图省事,几个VDD共用一颗电容,结果系统经常莫名复位,查了半天最后才发现是电源纹波问题。

晶振部分,我用的是8MHz主晶振加32.768kHz副晶振。8MHz经过芯片内部的PLL倍频后得到72MHz主频,这是F103的最高工作频率。晶振的两个引脚各接一个20pF负载电容到地,这个电容的取值不是随便拍的,它取决于晶振本身的负载电容参数,一般在数据手册里会标。如果负载电容偏大偏小,最直接的表现就是RTC走时不准或者系统偶尔启动失败。32.768kHz副晶振主要是给RTC用的,如果你不需要RTC功能,这颗晶振可以直接省掉,我用上是为了以后加时间戳功能。

复位电路就是一个10kΩ上拉电阻加一个0.1μF电容到地,NRST引脚低电平复位。启动模式配置就是BOOT0和BOOT1两个引脚,BOOT0通过10kΩ电阻下拉到地,BOOT1同样下拉,这样芯片从内置Flash启动,正常工作。

3.2 传感器接口电路怎么设计最可靠

DHT11的接口电路看起来就一个上拉电阻,但细节在下拉。DHT11的数据引脚内部是开漏输出,必须靠外部上拉电阻才能输出高电平,典型值是4.7kΩ。上拉电阻太小,总线拉低时电流太大;上拉电阻太大,总线恢复高电平的速度变慢,在长线传输的时候容易出时序错误。项目中我在数据线上串了一个1kΩ电阻做人机交互口的保护,实测对时序影响很小。

MQ-2模块的AO输出直接进STM32的ADC输入引脚就行,但要注意模块的AO输出电压范围。MQ-2模块的AO在干净空气中大约是0.1V到0.5V,浓度升高时电压上升,最高能到4V左右。STM32的ADC输入范围是0到3.3V(以VDDA为参考),如果直接把4V送到PA1引脚,超出ADC量程不说,长期还可能损伤引脚。所以硬件上我加了一级电阻分压:AO经过一个10kΩ串联电阻和6.6kΩ并联电阻分压到地,把满量程压到3.3V以内。实际如果你买的模块自带电压比较器,也可以从DO引脚接数字量,但那就丢了浓度连续变化的信息,我不推荐。

GP2Y1010AU的驱动电路稍微复杂一点,它需要PWM驱动LED。按数据手册推荐的电路:LED驱动端(引脚2)通过一个150Ω电阻接MCU的PWM输出,LED供电端(引脚1)接5V,中间还要加一个220μF的电解电容。VO输出引脚(引脚5)通过一个100kΩ电阻和一个1.5nF电容组成低通滤波器进入ADC。我在这里加了两个细节:一是在采样软件里加了多次平均,二是在PCB布局时让这个模拟输出走线尽量短、远离PWM走线,不然粉尘数据会随着你PWM频率周期性跳动,看起来像是电路坏了。

3.3 去耦与布线的实战经验

原理图设计完成后,画PCB时还要注意几点,这些经验来自我自己打样回来的教训。

供电走线遵循“星型接地”思路:5V电源进来先到传感器供电节点,再到稳压器,3.3V出来优先经过主控附近的大电容,再分配到OLED、BH1750等外设。模拟地和数字地不强制分离,但对于GP2Y1010AU和MQ-2这种模拟信号,ADC参考电压的稳定性直接决定了测量精度。如果PCB空间允许,在LQFP48封装的芯片背面放一个2.2μF的钽电容,对ADC基准稳定性有明显改善。

我项目的原理图里还有几个排母扩展口,把USART1、I2C1、SPI1、SWD、3.3V、5V、GND都引出来了。这套系统做完了之后必然想加功能,预留这几个口子,后面想挂ESP8266做联网上报,或者挂GPS模块,都不用重新设计板子,直接飞线就行。

4. 代码架构与关键驱动实现

4.1 工程结构怎么分,代码五个模块各干什么

工程代码我按功能拆成了5个模块目录:

Project/ ├── Core/ // 启动文件、系统时钟配置 ├── BSP/ // 板级支持包:LED、蜂鸣器、按键 ├── Driver/ // 传感器驱动:DHT11、GP2Y1010、MQ2、BH1750 ├── APP/ // 应用逻辑:数据显示、报警判断、页面切换 └── System/ // 中断处理、延时函数

延时函数我强烈建议用定时器实现,不要用简单的循环空转。DHT11的时序要求非常精确,us级别的延时如果靠for循环估算,在编译器优化等级不同的情况下误差很大,同一段代码在-O0和-O2下跑出来的延时能差一倍。我封装了一个基于SysTick的延时函数,us和ms两个粒度都有,在系统时钟72MHz下,精度足够满足DHT11和GP2Y1010的时序需求。

4.2 DHT11单总线时序:最容易翻车的一段代码

DHT11的数据读取是这套系统中第一个容易翻车的地方。它的通信协议大概是这样的:主机先把总线拉低至少18ms,然后释放总线,延时20-40μs等待DHT11响应。DHT11会把总线拉低80μs作为响应信号,然后再拉高80μs,之后开始传输40位数据。

40位数据里,每一位的表示方法和常见的UART不太一样:DHT11用高电平持续时间来区分0和1。数据位开始前先拉低50μs,然后拉高,高电平持续26-28μs表示0,持续70μs表示1。读取的代码如下:

uint8_t DHT11_ReadByte(void) { uint8_t i, data = 0; for(i = 0; i < 8; i++) { while(DHT11_DQ_READ() == 0); // 等待50us低电平结束 delay_us(40); // 延时40us判断电平 if(DHT11_DQ_READ() == 1) data |= (1 << (7 - i)); // 是高电平且延续,判定为1 while(DHT11_DQ_READ() == 1); // 等待位结束 } return data; }

这段代码的坑在于,delay_us(40)这一步必须在两次电平读取之间做得很准。延时太短,会把0误判成1;延时太长,又会把1误判成0。我实测下来,在4.7kΩ上拉、数据线长度不超过20cm时,40μs是个比较居中的值。

另一个大坑是两次读取之间的间隔。DHT11数据手册明确要求两次读取间隔大于1秒,实际上如果间隔太短,传感器会不响应,总线一直保持高电平,读出来全是0xFF。我刚开始测试的时候误以为程序跑飞了,后来查了手册才发现是节奏太快。所以我代码里加了个状态机,每2秒才触发一次温湿度采集。

4.3 MQ-2和GP2Y1010的ADC采集要注意什么

ADC这部分我用的ADC1的规则组,开启DMA,同时采集MQ-2的AO、GP2Y1010的VO两个通道,还有一个通道接在3.3V分压电阻上做参考校准。使用DMA的好处是CPU不用每次等转换完成,数据自动搬运到内存数组里,主循环直接读数组就行。

MQ-2的浓度换算不是线性的。它测的是气体浓度导致的电导率变化,换算关系接近对数曲线。如果要做比较准确的PPM显示,需要查传感器手册中的灵敏度特性曲线,取几个关键点做对数插值。我这套代码里给了一个简化版的换算函数,把电压映射到相对浓度百分比,足够做趋势展示和阈值报警。如果你要精确测量,建议还是用专门的电化学传感器或者红外传感器。

GP2Y1010的ADC采集时机同样有讲究。它靠PWM脉冲驱动LED,LED发光后,反射光信号要经过一段传播时间到达接收管,所以VO输出比LED脉冲有一个相位延迟。数据手册推荐的时序是:PWM周期10ms,高电平时间320μs,而ADC采样要在PWM上升沿之后280μs开始、再往后推一段时间读取。我的实现是用定时器输出PWM的同时,在PWM中断里设置一个标志位,ADC的定时器触发延迟280μs后启动规则组转换。等效的代码结构大致是这样的:

void TIM3_PWM_IRQHandler(void) { if(TIM_GetITStatus(TIM3, TIM_IT_CC1) != RESET) { TIM_ClearITPendingBit(TIM3, TIM_IT_CC1); ADC_SoftwareStartConvCmd(ADC1, ENABLE); // 此时开始采样 } }

GP2Y1010的电压与粉尘浓度的关系,数据手册给了一个典型曲线,大概在0到0.5V对应0到500μg/m³左右,但这是特定粉尘类型下的参考值,实际上不同粉尘的反射特性差别很大。我把原始电压和折算浓度都通过串口输出,校准的时候你拿一个标准粉尘仪在旁边比对着改系数就行了。

4.4 OLED显示与报警状态机的设计思路

OLED用的0.96寸SSD1306驱动芯片,I2C接口,128x64分辨率。驱动代码用的标准SSD1306命令集,我把显示页面拆成四个:首页显示温湿度,第二页显示空气质量,第三页显示粉尘浓度和光照度,第四页显示报警阈值设置。按键短按切换页面,长按进入设置模式。

报警部分我用了个简单的状态机,没有用裸奔的if-else堆逻辑。三个状态:正常、注意、报警。温湿度、气体、粉尘、光照四个维度各自有上下限阈值,任何一个指标越限就把状态往上推一级。报警时蜂鸣器不同频率鸣叫,OLED上也同步显示是哪个参数出问题。设置界面上可以用按键调整某个维度的阈值,调整完的参数我存在Flash的最后几页里,掉电之后不丢。

5. Proteus仿真搭建:在电脑上先把逻辑跑通

5.1 为什么这套项目要配一份仿真工程

很多嵌入式项目做完之后只发源代码和原理图,对于手头没硬件、或者还处在学习阶段的朋友来说,门槛还是高。我配了一份Proteus仿真工程,目的就是让大家在没有实物的情况下,也能完整体验这套系统的工作流程——烧录固件、观察OLED显示变化、调节传感器输入看报警响应、打开虚拟串口看数据。做课设的时候,仿真的截图和视频甚至可以直接作为设计报告的材料,省去拍照录视频的麻烦。

Proteus里STM32的元件库其实有几种不同的模型,我用的版本是Proteus 8 Professional,元件搜索STM32F103C8T6就能找到。DHT11、MQ-2这些传感器,Proteus元件库里也有对应的仿真模型,DHT11会实时计算出温湿度返回,MQ-2模型可以通过滑动变阻器模拟气体浓度变化。GP2Y1010没有现成模型,我用了可调电阻分压电路来模拟它的输出电压,用不同电压值代表不同粉尘浓度。

5.2 仿真工程一步步搭建过程

搭建步骤大致是这样的:

  1. 从元件库拖出STM32F103C8T6、DHT11、LM016L(LCD)、滑动变阻器、虚拟终端等元件,摆放到原理图编辑区。
  2. 按我提供的仿真原理图连接电路,注意DHT11数据线要加上拉电阻,OLED这里仿真里用LCD1602替代显示(效果等同,更直观)。
  3. 双击STM32芯片,在Program File里选择用Keil编译生成的hex文件,设置晶振频率为8MHz。
  4. 点击运行,LCD开始显示温湿度数据,调节滑动变阻器改变模拟量输入,观察报警和数据显示的变化。
  5. 用Virtual Terminal连接USART1的TX/RX引脚,可以实时看到串口输出的传感器数据报文。

仿真里最需要注意的一点是,Proteus的STM32模型对GPIO初始化代码反应比较敏感。如果你在工程配置里开了RTOS或者在main函数里用了某些特殊的启动方式,可能仿真跑不起来。我的工程里没有上RTOS,就是裸机轮询加中断,仿真兼容性很好。

5.3 仿真和实物之间的差异

Proteus仿真最大的价值是验证逻辑,但千万别以为仿真跑通了实物就没问题。我遇到过的情况是:DHT11在仿真里读得稳稳的,上实物后偶尔读到乱码;仿真里ADC数值纹丝不动,实物上一看波动能有两三个百分点的幅度。这些差异来自仿真模型把传感器理想化了,省掉了电气噪声、时序漂移、器件离散性这些真实因素。

所以我对大家的建议是:仿真工程用于功能验证和设计报告,但最终一定要回到实物上测试一遍。项目里的代码和原理图都是经过实物验证的(我实打实焊了三块板子测试),大方向不会跑偏,但你自己的实际布局走线、电源质量会影响一些模拟量数据,需要现场微调。

6. 实测调参中的几个坑与后续扩展思路

6.1 DHT11误码率高的排查链路

我第一版板子打样回来,DHT11读取十次大概要错两三次,表现为温度突变到几十度或者湿度出现负值。排查过程我一步步列出来,给大家一个参考:

第一步看供电。用万用表量DHT11的VCC和GND之间电压,是否稳定在5V±0.2V。我这边实测电压一直是稳的,排除。

第二步看时序。用逻辑分析仪抓DHT11数据线的波形,和手册上的时序图做对比。我抓到的波形显示主机拉低时间够长,传感器响应正常,但是数据位的0和1不好区分,高电平持续时间介于临界值。

第三步查上拉电阻。把4.7kΩ换成10kΩ之后,数据线高电平恢复变慢,问题不但没解决还更严重了。换回4.7kΩ,问题依旧。

第四步查线路长度和干扰。DHT11的数据线我走了大约15cm的排线,把它缩短到5cm后,误码率立刻下降。原来是排线过长导致信号边沿变缓,大概属于IO翻转沿不干净从而干扰了延时判断。最后我改了驱动代码,把时序判断的延时参数从35μs调整到45μs,配合缩短排线,问题彻底解决。

6.2 ADC数据波动的几种对策

MQ-2和GP2Y1010的输出在实物上都有波动,MQ-2是加热丝温度波动引起的,GP2Y1010是气流中粉尘颗粒分布不均引起的。我给ADC读数加了一个滑动平均滤波器,保持最近10次采样的队列,每次新数据进来去掉最老的数据再取均值,效果很明显,波动从±5%压到±1%左右。

报警判断必须加回差。假设你设置温度上限35℃报警,如果不加回差,温度在34.9和35.1之间来回抖动时,蜂鸣器会反复启停,很烦人。我的做法是:温度升到35℃(或以上)才触发报警,但回落到33℃(即阈值减2℃)才解除报警。这样2℃的回差就避免了临界值抖动。

6.3 低功耗和联网扩展方向

如果你想把系统做成电池供电的,可以考虑三件事:第一,把MQ-2的加热丝改成间歇供电,每30秒加热一次、测量一次,其余时间断电,功耗立刻降一大半;第二,MCU进入Stop模式,用RTC定时唤醒,每两分钟采集一轮数据然后继续睡眠;第三,把OLED换成E-ink或者只在唤醒时点亮,能进一步压低功耗。

联网扩展是最多人问的。STM32F103C8T6没有以太网MAC,最简单的方式是串口挂一个ESP8266或者ESP01模块,AT指令把数据通过Wi-Fi上报。上位机可以接到阿里云物联网平台或者EMQX这类私有MQTT broker,再配合一个小程序做远程查看。项目原理图里我已经预埋了UART1的排针接口,就是给这个扩展留的。

实际用下来,这台设备在我实验室已经稳定跑了两个多月没重启过。中间有几天温度异常报警,一查是空调坏了,也算侧面验证了这套系统的价值。你把这个项目做完之后,很自然会想着加联网、加数据存储、加更多传感器——到那个时候你会发现,底层驱动全部是现成的,扩展工作只是往上叠加逻辑而已。

最后提一句,代码里所有模块都有详细的注释,原理图文件和仿真工程放在一起。如果你照着做遇到问题,先看串口输出,然后量传感器供电、查时序,基本都能解决。祝顺利。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询