做ST嵌入式这些年,我隔三差五就会刷到“环境质量监测系统”这类开源项目。为什么这个方向这么火?因为它基本覆盖了嵌入式开发的完整链路:传感器驱动、数据处理、显示交互、报警输出、仿真验证,五个环节一个不少,而且难度曲线平缓,不会有那种一半人直接被劝退的硬门槛。这次分享的STM32环境质量监测系统,就是一个完整的开源工程,包含工程代码、原理图文件和Proteus仿真文件,核心器件是STM32F103C8T6、DHT11温湿度传感器、MQ135空气质量传感器和OLED屏幕。
先交代清楚这套系统到底能干什么。上电之后,DHT11负责读温湿度,MQ135负责读空气质量(以模拟电压形式给到ADC),STM32把数据处理之后刷新到OLED屏上;如果温度、湿度或者空气质量超过预设阈值,蜂鸣器会响,LED会亮。同时串口每2秒输出一帧当前数据,方便在PC端用串口助手观察,也为后续接入WiFi模块或上位机留了接口。整体功能不花哨,但非常适合学习和二次开发。
什么人适合拿这套东西?第一类是正在做课程设计或毕业设计的同学,拿来改一改就能满足大部分要求;第二类是刚学完STM32基础、想找完整项目练手的开发者,代码和原理图对照着读,理解会深很多;第三类是想快速搭环境监测原型的硬件工程师,传感器选型、电路、驱动都有现成参考,能省掉不少踩坑时间。
1. 项目总体设计与方案选型
1.1 环境质量监测系统需要哪些核心能力
环境质量监测系统的本质是一个“采集-处理-呈现-响应”的闭环。采集端需要考虑传感器种类和数据接口,处理端需要处理ADC转换或单总线时序,呈现端需要把数据变成人能看懂的形式,响应端则要快速判断并触发报警。如果只做采集显示,难度确实不高,但一套合格的原型系统,还需要考虑数据可靠性、报警滞后、低功耗和扩展接口。
这套开源项目在设计时,把“容易复现、便于扩展、教学价值高”放在第一位,所以没有盲目追求传感器数量,而是在温湿度和空气质量这两个最核心的指标上做扎实。温湿度决定了环境舒适度,空气质量决定了环境安全性,这两个维度合在一起,基本能覆盖室内环境监测的典型需求。而且从电子设计角度看,DHT11用单总线、MQ135用ADC模拟量、OLED用I2C,刚好能把嵌入式开发里最常用的三种外设接口都练一遍,这也是选这个组合的一个重要原因。
1.2 硬件选型:为什么是STM32F103 + DHT11 + MQ135
STM32F103C8T6这块芯片,在开源项目里快被用成“国民级”了,但恰恰因为用的人多,它的生态是最好的。CubeMX可以一键生成底层代码,HAL库文档齐全,Keil和STM32CubeIDE都能直接编译,网上遇到问题基本都有现成答案。C8T6虽然是低配型号,但主频72MHz、64KB Flash、20KB RAM,拿来跑一个环境监测系统绰绰有余,还能剩大量IO和定时器资源做扩展。
传感器方面,DHT11的知名度基本是“新手必备”级别。它采用单总线协议,一根线同时完成通信,数据格式是40bit:8bit湿度整数、8bit湿度小数、8bit温度整数、8bit温度小数、8bit校验和。精度不算高,温度正负2℃,湿度正负5%RH,但胜在成本低、连线和时序简单,非常适合教学和原型验证。MQ135则是模拟量输出,对CO2、氨气、苯类蒸汽都有响应,直接接到STM32的ADC引脚就能读数,价格也很便宜,适合做空气质量趋势监测而非气体精确定量。OLED屏幕选的是I2C接口的SSD1306,只有两根信号线,接线简单,显示内容丰富度比1602强好几个档次。
1.3 开源三件套的分工与用法
这套项目能直接跑起来,靠的是三个文件块的配合:工程代码负责逻辑,原理图负责说明器件怎么接,仿真负责在没有实体板卡的情况下做逻辑验证。拿到资料之后,我建议的顺序是先看原理图,理清引脚分配;再开仿真,把外设逻辑跑通;最后再看代码,对照原理去理解每个驱动函数为什么要这么写。千万别一上来就打开main.c想着赶紧编译过,那样容易只见树木不见森林。
这个顺序背后是有逻辑的。原理图是硬件层面的说明书,引脚映射、电源网络、上下拉电阻、去耦电容全都体现在这里;仿真是逻辑层面的验证工具,能在你焊板之前先确认程序方向没搞反;代码是最终交付物,理解它需要前两步打底。如果是纯学习,跑完仿真、看懂代码,知识收获就很大了。如果是做实物,那再加一步:先仿真、再焊板、最后下载调试,可以省掉大量反复修改的时间。
2. 核心硬件设计与原理图细节
2.1 主控最小系统与电源设计要点
STM32F103C8T6的最小系统并不复杂:8MHz外部晶振、两个20pF负载电容、复位电路、BOOT0下拉到地、VBAT接3.3V、每个电源引脚旁边放一个100nF去耦电容。这些都是常规操作,但有两个细节值得重点强调。
第一,去耦电容不要省。不要只放一个0.1uF就完事,最好是靠近MCU每个电源脚各放一个100nF,再在电源入口放一个10uF的钽电容或陶瓷电容,实测对ADC采样的稳定性有明显帮助。第二,BOOT0不能悬空。如果BOOT0浮空,引脚电平可能被干扰拉到高,导致芯片进入串口下载模式而不是从Flash启动,程序就永远跑不起来。所以BOOT0要明确接一个10k电阻到地。
电源部分用的是USB 5V输入,经过AMS1117-3.3稳压到3.3V。原理图上输入输出电容一定要加,AMS1117这类LDO对输入输出电容有要求,一般输入放10uF,输出放10uF并联100nF,否则带载时容易振荡。另外,如果系统里接了蜂鸣器这类感性负载,建议在电源输出端预留一个较大的电解电容位置,仿真时可能看不出来,但实物上电源跌落导致复位是很常见的问题。
2.2 传感器接口:DHT11与MQ135的电路设计
DHT11是单总线器件,数据线上必须接一个4.7k左右的上拉电阻到3.3V,这是它正常工作的前提。原理图里要给DHT11的数据引脚预留一个排座或焊盘,最好再在靠近传感器位置加一个100nF滤波电容,减小电磁干扰对时序的影响。
这里有一个电平匹配的坑:DHT11的供电范围虽然能到5V,但它的数据输出高电平接近自身供电电压。如果传感器用5V供电而MCU是3.3V,数据线直连可能超出STM32的IO承受范围,长时间运行有风险。更稳妥的做法是让DHT11和MCU共用3.3V供电,整个系统电源轨就统一了,原理图也更好画。
MQ135模块一般自带比较器和电位器,有AO和DO两个输出。模拟量AO接到STM32的ADC引脚,比如PA1,ADC参考电压直接用3.3V。如果用的是裸传感器而不是集成模块,那输出端要接一个4.7k到10k的负载电阻,把传感器电流信号转成电压信号。还有一点要注意,气体传感器的加热电阻工作电流较大,电源设计时得留足余量,不然加热瞬间的电压跌落会让ADC读数跟着一起跳。
2.3 显示、报警与人机交互电路
OLED(SSD1306)在I2C模式下只需要SCL、SDA、VCC、GND四根线。SCL建议用PB6,SDA用PB7,正好对应STM32的I2C1外设。地址方面,SA0引脚接GND时,器件地址是0x7C(7位寻址下的0x3C),这是大多数OLED模块的默认配置。如果程序里I2C扫描不到设备,先查一下是不是模块地址需要配置成0x3D。
报警部分的核心是有源蜂鸣器加一个LED指示灯。有源蜂鸣器自带振荡源,给高电平就会响,但这东西工作电流有几十毫安,直接接STM32的IO口驱动能力不够,需要加一个S8050三极管做开关。电路形式是:蜂鸣器一端接3.3V,另一端接三极管集电极,发射极接地,基极经过1k电阻接MCU IO口。同时在蜂鸣器两端反向并联一个1N4148二极管,吸收断电瞬间的反向电动势。这个二极管非常关键,不加的话,蜂鸣器断电瞬间产生的感应电压可能把IO口打坏。这类问题仿真里表现不出来,但实物上我见过好几块板子因为省了续流二极管,MCU的IO口慢慢就坏了。
3. 代码实现:从驱动到应用逻辑
3.1 用STM32CubeMX搭工程骨架
代码部分基于STM32CubeMX加HAL库生成,开发环境用Keil MDK。CubeMX里需要配置的外设包括:RCC(外部晶振)、SYS(Serial Wire调试口)、GPIO(DHT11数据引脚、蜂鸣器、LED)、ADC1(PA1模拟输入)、I2C1(OLED)、USART1(调试打印)、TIM(可选,用于校准微秒延时)。
系统时钟配置为8MHz外部晶振倍频到72MHz,这个倍频关系要搞清楚。如果实际板上用的不是8MHz晶振而是别的大小,配置就要跟着改,不然串口波特率、定时器定时全是乱的。HAL库的好处是外设初始化代码自动生成,应用层自己控制。但坏处也很明显,HAL库的时序开销比标准库大,尤其是DHT11这种对微秒级时序有要求的单总线协议,直接调用HAL_Delay根本不行,因为它的单位是毫秒,微秒级延时得自己基于SysTick或裸循环实现。工程里提供了一套delay_us函数,底层用定时器或多个空循环实现,实测时序比较稳定。
3.2 DHT11单总线驱动与数据校验
DHT11的驱动是整个工程最需要死磕的部分。主机要先拉低总线至少18ms,建议直接拉低20ms,然后释放总线并等待DHT11响应。传感器拉低80us表示应答,再拉高80us准备发送数据,之后就是连续40bit的数据。每一位都是以50us低电平开头,关键是后面高电平持续的时间:如果高电平持续26到28us,代表逻辑0;如果高电平持续70us左右,代表逻辑1。
代码里的典型做法是,先等引脚变低,再等引脚变高,然后延时40us再读取引脚状态。如果40us之后引脚还是高电平,说明这一位是1;否则就是0。代表性的读取函数如下:
uint8_t DHT11_ReadByte(void) { uint8_t i, data = 0; for (i = 0; i < 8; i++) { while (GPIO_ReadLevel(DHT11_PIN) == 0); // 等待50us低电平结束 delay_us(40); if (GPIO_ReadLevel(DHT11_PIN) == 1) data |= (0x80 >> i); // 高电平持续超过40us则为1 while (GPIO_ReadLevel(DHT11_PIN) == 1); // 等待当前位结束 } return data; }这段代码看着简单,实际容易在三个地方翻车。第一,循环等待没有超时机制,如果传感器没接或者损坏,程序会卡死在里面。真实工程里建议加一个超时计数器,超时就退出并返回错误。第二,delay_us的准确性特别关键,如果延时不准,40us的判断点偏移,数据就会频繁读错。第三,40bit数据读完之后一定要做校验,校验和等于前面四个字节相加的低8位,校验不过直接丢弃整帧数据。不要用错误数据去刷新显示,宁可不刷新,也不能显示错的。
3.3 ADC采集、滤波与阈值报警逻辑
MQ135的模拟输出接到ADC1的PA1引脚。HAL库的ADC读取方式很固定,先启动转换再等待转换完成,最后拿结果。核心代码就几行:
HAL_ADC_Start(&hadc1); HAL_ADC_PollForConversion(&hadc1, 10); uint16_t raw = HAL_ADC_GetValue(&hadc1); float voltage = raw * 3.3f / 4096.0f;但直接读单次值,噪声会比较大,因为气体传感器本身输出就在不断波动。我在代码里做了滑动平均值滤波:每200ms采样一次,累计5次后取平均,再更新一次数据。滑动窗口的好处是既能平滑噪声,又不会让响应变得太迟钝,很适配这种缓慢变化的模拟量。更进阶一点可以用一阶低通滤波,公式是y[n] = alpha * x[n] + (1 - alpha) * y[n-1],alpha取0.1到0.3之间,效果也不错。
报警逻辑设计上,我设置了三组阈值:温度超过30℃、湿度超过70%RH、空气质量原始ADC值超过2500(大约对应2V电压),任何一个条件满足就触发蜂鸣器和LED。阈值定义成宏常量放在配置文件里,想改直接改宏,不需要动主逻辑。报警还引入了10秒的持续判断,数据连续超限10秒才真正报警,避免瞬时尖峰误触发。这个“去抖”思路在实际产品里非常常见,简单有效。
3.4 OLED显示与串口打印
OLED驱动基于SSD1306的I2C命令集实现,写字符和汉字需要取模。工程里我把中文字库精简成了几个常用字,比如温度、湿度、质量、报警这几个词,这样占用Flash空间小,又能满足显示需求。显示刷新放在主循环里,每500ms刷新一次,内容包括温度值、湿度值、空气质量等级。
空气质量等级怎么划分?我根据MQ135的电压做了简单三档:低于1.5V算是良好,1.5V到2.5V算一般,高于2.5V算较差。这个分档逻辑完全可以根据自己的实际环境调整,比如家里通风好,可能长期都在1V以下,那就应该把阈值下限再压低。
串口打印用USART1,波特率115200,输出格式是“Temp: 26.5C, Humi: 48%RH, Air: 1800”。串口的作用不只是调试,后续接ESP8266上云,或者接USB转TTL做上位机显示,这套接口都能直接复用。有一个我经常遇到的坑:在Keil里做printf重定向到串口,MDK必须勾选Use MicroLIB,否则程序编译没问题,但串口输出会卡住甚至完全没输出,坑过不少新手。
4. Proteus仿真搭建与系统联调
4.1 仿真能验证什么、不能验证什么
很多朋友希望仿真能“完全替代实物”,这个期望要放平。Proteus仿真最大的价值,是能验证逻辑层面的正确性:程序能不能跑起来、有没有死循环、GPIO状态对不对、显示和报警逻辑是否正常。但它模拟不了真实传感器的物理特性,DHT11在仿真里的时序和真实器件有很大差异,MQ135的输出也不会真的随气体浓度变化。
所以在仿真时,一般会用虚拟电位器来模拟MQ135的电压变化,测试ADC逻辑,或者直接给DHT11模型固定一组温度湿度值,让程序去读,重点验证的是程序读时序和解析逻辑有没有写对。换句话说,仿真是“逻辑验证工具”,不是“物理仿真工具”。明白这一点,仿真阶段的目标就会很清晰,不会花大量时间纠结仿真和实物的细微差别。
4.2 仿真电路搭建要点
在Proteus里搭建这套电路,需要准备的元器件包括:STM32F103C8T6模型、DHT11模型、LCD或虚拟串口终端、LED、蜂鸣器、电阻和电位器。要注意的是,不同版本Proteus的DHT11模型行为不完全一致,有的版本仿真时序很严格,有的比较宽松。如果仿真里DHT11读取一直失败,先怀疑微延时函数在仿真环境里的执行速度——Proteus的CPU仿真速度和真实芯片不一样,有可能需要把delay_us的延时次数调大或调小。
OLED屏在Proteus里不一定有现成的SSD1306模型。遇到这种情况,有两个可行方案:一是用虚拟I2C调试设备观察数据是否正常;二是在仿真阶段先改成LCD1602验证主逻辑,等实物阶段再换回OLED。这不是偷懒,而是“仿真验证逻辑、实物验证外设”的开发思路,很多商业项目也是这么干的。
4.3 联调流程与仿真独有坑
我推荐的联调流程是一步一步叠加上去:先在Proteus里只放MCU、LED和蜂鸣器,编译烧录后确认最基本的程序能跑;然后加上串口虚拟终端,确认打印输出正常;再加DHT11,确认能读到数据;最后加电位器模拟MQ135,验证ADC和报警逻辑。每次都只增加一个变量,出了问题范围非常小,定位很快。
仿真里有一个挺常见的问题:程序在Keil能编译,烧录到Proteus里却不跑或者乱跑。大部分时候是晶振配置和Proteus的模型不匹配。有些仿真模型默认使用内部RC时钟,而你在CubeMX里配置的是外部晶振输入,上电后时钟源不对,程序自然起不来。解决办法是在CubeMX里先临时改成HSI内部时钟,验证逻辑,确认没问题之后,再到实物上改回外部晶振配置。这个坑几乎每个用Proteus做STM32仿真的人都会遇到,提前知道能省很多时间。
5. 常见问题排查与避坑实录
5.1 DHT11读不到数据的几种典型原因
DHT11读不到数据这个问题,在交流群里几乎每天都会被问。按照出现频率排序,我把踩过的坑列一下:
第一,数据引脚模式配置错误。DHT11数据引脚要配置成开漏输出带上拉,不要用推挽输出,推挽输出在释放总线后拉高电平的驱动能力过强,会影响时序。第二,上拉电阻没接或者阻值太大。4.7k是公认比较合适的值,10k能用但边沿变缓,33k以上在长导线场景下基本就别指望时序还能对了。第三,主机起始信号的低电平时间不够长。DHT11规格要求至少18ms,我实测拉低20ms最稳定,少于15ms基本就是偶发失败。第四,微秒延时函数不准。用HAL_Delay做不了微秒级定时,必须用独立延时函数,这是所有DHT11驱动问题的根源之一。
5.2 数值跳变、偏差大的处理思路
温湿度读数偶尔跳变,很多人第一反应是传感器坏了,其实大概率是电源问题。DHT11再便宜,它对电源噪声也是敏感的。如果用的是开关电源供电,纹波大一点,读数就会时不时抽风一下。建议在传感器电源引脚旁边加100nF电容,如果还不行就加一个10uF电解电容,同时采样频率不要太高,DHT11本身就建议读取周期不要小于1秒。
MQ135的ADC读数抖动也是类似的排查思路:先加滤波,再看电源。数据跳变时,先检查是不是ADC的参考电压VREF不稳定,如果VREF和DHT11共用一路LDO输出,传感器加热瞬间拉低电压也会影响ADC读数。所以硬件上最好把模拟部分和数字部分的电源稍微分隔一下,软件上做滑动滤波,两者搭配基本能解决90%的跳变问题。
5.3 问题速查表
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 程序不跑,OLED无显示 | 时钟配置错误、BOOT0悬空 | 检查CubeMX时钟树,BOOT0接10k下拉 |
| DHT11一直读不到数据 | 单总线时序不对、上拉缺失 | 逻辑分析仪看波形,确认起始信号18ms以上 |
| 串口输出乱码 | 波特率不匹配、时钟倍频错误 | 核对CubeMX时钟树,确认波特率配置 |
| ADC读数不变化 | 通道配置错误、引脚被复用 | 检查CubeMX的Pinout,PA1确认配置为ADC1_IN1 |
| 蜂鸣器不响 | 三极管驱动接错、缺续流二极管 | 对照原理图查基极电阻和C/E极方向 |
| 仿真跑飞 | 外部晶振配置和模型不匹配 | 先用HSI内部时钟验证逻辑 |
| OLED白屏 | I2C地址不对、SCL/SDA接反 | 扫描I2C设备地址,核对0x3C或0x3D |
| 数据偶尔跳变 | 电源纹波大、滤波不足 | 加100nF去耦,软件加滑动平均滤波 |
这个项目我前后做过三个版本,最大的体会是:环境监测类项目的技术难点不在主控,而在“数据是怎么从传感器可靠进到MCU里的”。DHT11的单总线时序、MQ135的ADC采样噪声、电源纹波对模拟量的影响,这些才是真正值得花时间琢磨的地方。把它完整调通一遍,你对GPIO、定时器、ADC、I2C、串口的理解,比看十遍教程都深刻。后续想继续扩展的话,方向也很多:加ESP8266模块联网上云、加SD卡存历史数据、换成精度更高的SHT30或BME280都行。这套代码和原理图已经给这些扩展留好了接口,从这个基础上出发,会比从零开始顺很多。