不少做嵌入式开源项目的朋友都遇到过同一个尴尬:网上搜“STM32 环境监测”,出来的工程十个里有八个只有一个 main.c,原理图是一张模糊的截图,仿真干脆没有。代码能跑,但换个人、换个板子就废了。我自己早期做过一个温度湿度监测器,就是这个德行——毕业设计答辩现场,传感器数据死活跳变,最后只能靠嘴硬撑过去。
所以这次我把整套环境质量监测系统完整开源,不是只丢代码,而是把代码、原理图、仿真工程三样东西对齐了放出来。基于 STM32F103C8T6,搭配 DHT11 温湿度传感器、MQ-135 空气质量传感器、0.96 寸 OLED 屏和蜂鸣器报警,实现温湿度、空气质量等级的实时监测与超限报警。项目不复杂,但覆盖了从最小系统设计、传感器驱动、ADC 采样滤波到 Proteus 仿真的完整链路,适合正在学 STM32、准备做课设/毕设、或者想入坑嵌入式开源项目的朋友。这篇文章我会把项目的设计思路、关键代码逻辑、仿真搭建过程,以及从仿真转到实物后踩过的几个典型坑,一次性讲清楚。
1. 为什么我不再做“只有一个 main.c 的环境监测工程”
很多入门者做 STM32 项目时都有一个习惯:把全部逻辑塞进 main.c,GPIO 初始化、延时、传感器读取、显示刷新全写在 while(1) 里。这样做在仿真里确实能跑,但拿到实物上基本会翻车,因为问题根本不是“主循环写得对不对”,而是硬件设计、驱动时序、数据处理这些环节单独出了问题,你却无从排查。
1.1 这个项目解决的真实痛点
环境质量监测听起来简单,实际要处理的问题比表面多得多。DHT11 是单总线协议,时序要求以微秒为单位,延时函数稍微偏差一点就读取失败;MQ-135 是模拟输出的气敏传感器,刚上电的读数会持续漂移,不处理直接用 ADC 原始值显示,数值能飘到你看不懂;OLED 屏用 I2C 协议,上拉电阻没接对就会花屏;加上报警阈值设置、按键交互、数据滤波,这些细节叠在一起,才是“能用的监测系统”和“能跑的 demo”之间的真正差距。
我开源这个项目,就是想把这些真实痛点全部前置处理掉。你在原理图里能看到每个传感器的接口电路,在代码里能看到合理的文件分层,在仿真工程里能直接看到完整系统的运行效果。三者互相印证,你照着复现一次,等于把嵌入式开发的常用套路过了一遍。
1.2 整套开源内容能给你带来什么
先说代码部分。工程基于 STM32 标准外设库编写,文件结构分成硬件驱动、应用逻辑、主循环三层。DHT11 的时序读取、MQ-135 的 ADC 采样、OLED 的驱动,这些通通独立成模块,你换板子的时候只需要改引脚映射,不需要重写逻辑。
再说原理图。工程提供的是可以直接打样的完整原理图,包含 STM32F103C8T6 最小系统、电源电路、传感器接口电路、蜂鸣器驱动电路等。很多人第一次画 STM32 原理图时会在晶振电容、复位电路、BOOT 引脚上犯迷糊,这部分我会在下面详细展开。
最后是仿真。Proteus 工程文件可以直接打开运行,DHT11、OLED 在 Proteus 里都有模型,MQ-135 没有现成模型,我用了可调电位器模拟它的模拟输出,完整演示了 ADC 采样流程。仿真跑通了,至少能证明你的逻辑链路没有问题。
2. 原理图设计:最小系统、传感器接口与电源处理
网上流传的 STM32 最小系统原理图很多,但不少都有细节错误,比如晶振电容乱选、复位电路简化、BOOT 引脚悬空。这些错误在仿真里完全看不出来,到了实物上就是“下载不了程序”“跑起来不稳定”这种玄学问题。
2.1 STM32F103C8T6 最小系统原理图的关键点
先看核心芯片 STM32F103C8T6,LQFP48 封装,Flash 64KB,RAM 20KB,对这个项目来说完全够用。最小系统包含电源、晶振、复位、BOOT 配置、SWD 下载接口五个部分。
电源部分要注意的是,STM32 虽然有多个 VDD/VDDA 引脚,但 VDDA 必须单独接滤波电容,通常用 1uF + 0.1uF 并联到地。模拟电路和数字电路共用电源时,这个电容不能省。很多人在仿真里不会遇到 ADC 跳变的问题,但实物上 VDDA 不滤波,ADC 采样值基本没法看。
晶振电路是新手翻车重灾区。8MHz 主晶振两个引脚各接一个负载电容到地,电容值的计算公式是:
CL = (C1 × C2) / (C1 + C2) + CstrayCstray 是 PCB 走线和芯片引脚带来的杂散电容,约 3~5pF。如果晶振规格书的负载电容 CL 是 18pF,那 C1 和 C2 选 27pF 或 30pF 比较合适。计算公式是 C = (CL - Cstray) × 2,代入 18pF 和 5pF,得到 26pF,取标准值 27pF。很多最小系统板直接焊 20pF 也能起振,但你要是做自己的板子,按这个公式来最稳妥。
复位电路用 10k 电阻上拉到 3.3V,再接 100nF 电容到地,复位引脚 NRST 通过按键接地。BOOT0 和 BOOT1 都要串 10k 电阻接地,保证默认从主 Flash 启动。下载接口用 SWD,只需要 SWDIO、SWCLK、GND、3.3V 四根线,比 JTAG 省引脚。
2.2 DHT11、MQ-135、OLED 的接口电路设计
DHT11 是单总线协议,数据引脚需要接一个上拉电阻到 VDD。我用的 5.1k 上拉到 3.3V,经过一个 100 欧电阻串联到 MCU 引脚。串联电阻的作用是限制引脚的灌电流,DHT11 数据线较长时可以抑制振铃。数据引脚接 PA6。
MQ-135 模块的原理要特别注意。模块上有两个输出:数字输出 DO 和模拟输出 AO。DO 输出接在 LM393 比较器上,通过电位器调节阈值;AO 输出才是传感器真正的模拟信号。我的设计里用 PA1 作为 ADC 输入,测量 AO 引脚的电压。MQ-135 的加热电阻工作电压是 5V,所以模块供电接 5V,但 AO 输出经过模块内部的分压电路后,电压范围在 0~3.3V 左右,可以直接接 STM32 的 ADC 引脚。为了保险起见,ADC 引脚对地接一个 0.1uF 电容,滤掉高频噪声。
OLED 屏用的是 I2C 接口,SSD1306 控制器,SCL 接 PB6,SDA 接 PB7,对应 STM32 的 I2C1 外设。I2C 总线的 SCL 和 SDA 都需要上拉电阻,通常用 4.7k 上拉到 3.3V。有些 OLED 模块板上已经焊了上拉电阻,直接在原理图上再放一份也不会有问题,两个电阻并联后等效约 2.35k,仍然在 I2C 规范允许范围之内。
蜂鸣器驱动电路用的是 NPN 三极管 S8050。蜂鸣器接在 5V 和集电极之间,发射极接地,基极串 1k 电阻接 PB0。PB0 输出高电平驱动三极管导通,蜂鸣器发声。用三极管而不是直接用 MCU 引脚驱动,是因为蜂鸣器工作电流 30mA 左右,超过 STM32 单个 GPIO 的最大灌电流能力,长时间直驱可能损坏引脚。
2.3 电源与滤波设计
电源方案是这样的:USB 5V 输入,经过 AMS1117-3.3 稳压到 3.3V 给 MCU、OLED、DHT11 供电,5V 直接给 MQ-135 加热器和蜂鸣器供电。AMS1117 输入端和输出端各加一个 10uF 钽电容和 0.1uF 陶瓷电容。
有一点容易忽略:MQ-135 加热器消耗电流在 150mA 左右,如果 AMS1117 输入电压被拉低,DHT11 这种对供电敏感的传感器就会间歇性抽风。所以原理图里 AMS1117 输入端的 10uF 电容要靠近 USB 座放置,输出端的 10uF 电容靠近 MCU 的 VDD 引脚。这些都是实践里反复验证过的经验,仿真看不出来区别,但实物上就是稳定和不稳定的区别。
3. 代码实现:从底层驱动到业务逻辑怎么组织
拿到原理图之后,下一步是写代码。我的代码结构分了四层:系统初始化、硬件驱动、应用逻辑、主循环。Keil 工程文件放在 Firmware 目录,使用标准外设库,兼容 Keil 5 的 MDK-ARM 环境。
3.1 使用 STM32CubeMX 的引脚配置思路
虽然工程最终是用标准外设库写的,但我在设计之初先用 STM32CubeMX 生成了初始化框架,然后再替换驱动部分。这是提高效率的好办法:CubeMX 生成的 SystemClock_Config() 和 GPIO/I2C/ADC 初始化代码非常可靠,自己手写反而容易漏配置。
关键配置如下:
- RCC:外部 8MHz 晶振,系统时钟 72MHz
- GPIO:PA6 输入模式(DHT11 数据口),PA1 模拟输入(ADC),PB0 推挽输出(蜂鸣器),PA0 输入(按键)
- I2C1:标准模式 100kHz,地址 0x3C 即 7 位地址 0x3C,实际写入地址是 0x78
- ADC1:通道 1(PA1),采样时间 55.5 周期,软件触发
- SysTick:1ms 时基,用于延时函数
CubeMX 生成的 GPIO 初始化默认没有配置开漏和上拉,这点在 I2C 上要注意。标准外设库的 I2C 初始化函数会配置 I2C 引脚为复用开漏输出模式,但如果用了 CubeMX,需要手动检查 GPIO 配置是否正确。
3.2 DHT11 时序驱动的坑与稳定写法
DHT11 的驱动是这块最值得展开的部分。它的通信协议是单总线,一次完整读取流程是:
- MCU 拉低数据线至少 18ms,然后拉高并释放
- DHT11 响应,输出 80us 低电平和 80us 高电平
- DHT11 发送 40 位数据:湿度整数、湿度小数、温度整数、温度小数、校验和
- 每位数据以 50us 低电平开始,之后输出 26~28us 高电平表示逻辑 0,输出 70us 高电平表示逻辑 1
问题出在延时精度上。标准外设库的 delay_us 函数如果只是简单循环,会因为编译器优化和 CPU 频率误差导致实际延时偏大或偏小。我用的稳定写法是:先用定时器做一个微秒级延时函数。基本思路是利用 SysTick 的 72MHz 计数,通过读取 SysTick->VAL 实现精确延时,改造后稳定性提升非常明显。
另一个问题是电平读取。DHT11 数据线是开漏输出,MCU 引脚要配置为上拉输入。读取时序时要禁用中断,因为中断会让时序出现几十微秒级的毛刺,直接导致读取失败。读取完成后恢复中断。这个细节我在代码注释里专门标注了。
3.3 MQ-135 的 ADC 采样与滤波
MQ-135 的输出是模拟电压,用 ADC 采集。直接读 ADC 原始值肯定不行,因为传感器本身就有噪声,加上环境波动,相邻两次采样可能差出几十个 LSB。我的做法是先连续采样 10 次,去掉最大值和最小值,然后取平均,得到滤波后的 ADC 值。
代码逻辑是这样的:
uint16_t MQ135_ReadAvg(uint8_t times) { uint32_t sum = 0; uint16_t max_val = 0, min_val = 4095; uint16_t sample_val; for(uint8_t i = 0; i < times; i++) { sample_val = MQ135_ReadADC(); if(sample_val > max_val) max_val = sample_val; if(sample_val < min_val) min_val = sample_val; sum += sample_val; } sum -= max_val + min_val; return (uint16_t)(sum / (times - 2)); }注意,这里的 4095 是基于 12 位 ADC 的满量程值。STM32F103 的 ADC 是 12 位,测量范围 0~3.3V,所以 ADC 值和电压的换算公式就是:
V = ADC_Value × 3.3 / 4095空气质量等级的判定逻辑也简单:电压低于 1.0V 认为是“良好”,1.0V~2.0V 是“一般”,2.0V~2.5V 是“较差”,超过 2.5V 就是“很差”。这个阈值是需要校准的,具体原因后面踩坑部分再说。
3.4 OLED 显示、按键与蜂鸣器报警逻辑
显示部分用 SSD1306 标准驱动,原理是往显存里写数据,然后整帧刷新到屏幕。OLED 屏是 128x64,共 8 页,每页 8 行像素。显示逻辑分三个区域:第一行显示空气质量状态,第二行显示温度和湿度,第三行显示报警阈值。
按键和报警逻辑放在一起。按键 PA0 支持短按和长按两种操作:短按切换显示页面,长按进入阈值设置模式。进入设置模式后,按键每次短按增加阈值,超过上限后回到最小值。蜂鸣器在温度超过上限或空气质量指数低于阈值时鸣叫,鸣叫方式用定时器控制,响 200ms 停 200ms,避免一直响让人烦躁。
这个交互逻辑看起来简单,但代码实现时要注意按键消抖。我用的消抖方式是 20ms 延时扫描,确认电平稳定后再执行动作。用状态机管理按键的按下、释放、长按状态,比直接在主循环里扫描可靠得多。
4. Proteus 仿真:完整工程是怎么搭出来的
仿真部分是这个项目的亮点,也是很多开源项目不给的东西。Proteus 里搭 STM32F103C8T6 的仿真,可以直接把 Keil 生成的 hex 文件加载进去运行,看到板载外设的行为。
4.1 搭建 STM32 仿真工程的基本步骤
用 Proteus 8.9 以上版本,步骤是这样的:
- 新建工程,在元件库搜索 STM32F103C8T6,拖入画布
- 添加 8MHz 晶振,两个引脚分别通过 20pF 电容接地
- 添加复位电路:10k 上拉电阻 + 100nF 电容 + 按键
- 添加电源端子 VDD 和 GND,VDD 接 3.3V
- 添加 DHT11 模型(Proteus 库里有),数据引脚接 PA6,VCC 接 5V
- 添加 OLED 模型(SSD1306),SCL 接 PB6,SDA 接 PB7
- 添加电位器 POT,模拟 MQ-135 的模拟输出,中间抽头接 PA1
搭建完成后,双击 STM32 芯片加载 hex 文件,点击运行。此时 OLED 上应该能正常显示温湿度和空气质量指数。注意,Proteus 的晶振频率和实际芯片要一致,不然延时函数的时间基准就错了,DHT11 读取会失败。
这里顺便提一个很多新手会忽略的事情:Proteus 里 DHT11 模型的数据引脚要接上拉电阻。不接上拉,仿真里可能会读出一个固定的错误值。我在原理图里本来就放了 5.1k 上拉,所以仿真工程直接沿用了相同的设计。
4.2 仿真环境里怎么模拟 MQ-135 这类模拟传感器
Proteus 元件库没有 MQ-135 的直接模型,但有几种替代方案。最常用的是 POT 电位器,调节阻值可以得到 0~5V 的连续电压输出。因为 STM32 ADC 参考电压是 3.3V,所以电位器两端接 5V 时,中间抽头最大输出 5V,会超过 ADC 的测量范围,需要把分压电阻选好,让最大输出在 3.3V 以内。
我用的方案是电位器两端分别接 3.3V 和地,输出范围天然就是 0~3.3V,完全匹配 ADC 输入。运行仿真后,旋动电位器,OLED 和气质量等级会随之变化,ADC 滤波逻辑和报警逻辑都能得到验证。
除了电位器,Proteus 还有信号发生器,可以输出正弦波、三角波、慢变化的斜坡信号,模拟气体浓度缓慢上升的场景。这样验证报警阈值时看起来更真实。用信号输出到 PA1 的好处是能看到数据滤波的实际效果,因为信号发生器可以叠加噪声,动态效果比电位器直观得多。
4.3 仿真的能力边界:哪些验证要在实物上做
仿真跑通不代表实物一定能跑,这个道理我强调过很多次。Proteus 的仿真模型是理想化的,它不会模拟电磁干扰、电源纹波、传感器老化这些真实因素。具体来说,四件事必须在实物上验证:
- DHT11 的真实时序稳定性,仿真里延时稍微偏一点都不影响,实物上一旦时序偏差就会读不到数据
- MQ-135 的预热漂移,仿真里电位器旋到哪个值就是哪个值,实物上传感器通电后读数会持续漂移几十分钟甚至几小时
- OLED 的 I2C 时序,仿真里 I2C 速率快一点慢一点都能响应,实物上一旦上拉电阻配得不合适就会花屏
- ADC 的真实噪声水平,仿真里 3.3V 参考电压是理想的,实物上 VDDA 不滤波的话,ADC 低几位会在那里跳来跳去
仿真的价值在于验证逻辑、加快开发进度,而不是替代实物调试。这两者的关系一定要摆正。
5. 实物调试记录:五个最容易翻车的细节
我最初做这套系统时,从 PCB 打样回来到完全稳定运行,用了将近一周时间。总结下来,大部分时间都花在五个问题上。这里逐一记录,给大家排雷。
5.1 DHT11 读不出来不要先怀疑代码
第一次把程序烧进板子,串口打印 DHT11 读取结果,全是 0 或者超时错误。第一反应是代码时序不对,拿示波器看了数据线的波形,发现起始信号长度是对的,但 DHT11 响应信号几乎没有。怀疑是上拉电阻的问题——DHT11 数据线上拉用 4.7k 或者 5.1k 都没有问题,真正的问题是供电电压。
DHT11 的推荐供电是 3.3V~5V,但如果供电电压只有 3.0V,它的响应时序会变慢,MCU 的采样窗口就可能错过有效信号。我当时的板子用 AMS1117-3.3,但输入电压只有 4.2V 左右,AMS1117 压差不够,输出只有 3.0V 左右。后来换成 USB 供电,电压恢复到 5V,AMS1117 输出 3.3V,DHT11 就正常了。
这个案例说明,遇到传感器不工作,先量供电电压,再查信号波形,最后才怀疑代码。排查顺序反了会浪费很多时间。
5.2 MQ-135 预热和基线漂移
MQ-135 传感器上电后有一个预热阶段,加热器需要把传感器内部温度升到正常工作范围。这期间,传感器的电阻值会逐渐变化,表现为 AO 输出电压持续漂移。新传感器首次通电,漂移可能持续 24 小时以上。所以实物测试第一天看到的“空气质量很差”,不一定代表环境真的差,而只是传感器没稳定。
我的处理方式是在代码里加入一个预热倒计时逻辑。设备上电后前 10 分钟,只显示“传感器预热中”,不参与报警判定。同时把每次上电读取到的初始值记为基线值,后续的空气质量指数基于基线值的相对变化来计算,而不是直接用绝对电压。这样漂移影响就能被限制在可接受范围内。
5.3 OLED 花屏与 I2C 上拉的玄学
OLED 花屏的常见原因有两个:I2C 速率过快或者上拉电阻过大。我的板子上 I2C 用了标准模式 100kHz,上拉电阻 4.7k,按理说没问题。但实际测试时发现,只要 MCU 和 OLED 之间的连线超过 10cm,花屏概率就明显增加。
后来把 I2C 速率降到 50kHz,问题消失。这说明 OLED 屏的 I2C 接口虽然有内部上拉,但长线情况下信号边沿变缓,时序裕量不足。如果你的 OLED 花屏,先试着把 I2C 速率降下来,这是最简单粗暴的解决方案。
5.4 ADC 采样值跳变,参考电压没处理好
ADC 采样值跳变是这五个问题里最隐蔽的一个。现象是空气质量数值在某个区间来回抖,用串口打印 ADC 原始值,最低位在跳,高的位偶尔也跳。用万用表量 AO 引脚电压,是稳定的。这说明 ADC 参考电压或者采样通道有噪声。
STM32F103 的 ADC 参考电压是 VDDA,VDDA 和 VSSA 之间必须接滤波电容。我的第一版 PCB 偷懒,VDDA 直接接 VDD,没有单独的滤波电容,导致 ADC 采样时参考电压毛刺很大。在 VDDA 引脚附近加了一个 1uF 并联 0.1uF 的电容后,采样值稳定性立刻改善。
5.5 从原理图到 PCB 的布局建议
如果你打算把这个项目做成 PCB,我的建议是分区域布局:电源区靠近输入接口,MCU 区在中央,传感器接口靠近板边,蜂鸣器放在远离 MCU 的位置。
特别注意两点。一是晶振电路要紧挨着 MCU 的 OSC_IN/OSC_OUT 引脚,走线尽量短,两个负载电容最好放在晶振和 MCU 之间。二是 MQ-135 模块是插接件,它的模拟输出线别和蜂鸣器驱动线平行走线太长,否则蜂鸣器鸣叫时的脉冲干扰会窜进 ADC 采样通道。
6. 开源文件结构说明与下一步扩展思路
既然是开源项目,文件目录的组织方式直接影响别人的使用体验。我这套项目的仓库结构是经过几次迭代定下来的,给大家参考。
6.1 仓库目录怎么组织,文件各自是什么
STM32-Environmental-Monitoring/ ├── Hardware/ │ ├── Schematic/ // 嘉立创EDA工程文件,可在线打开编辑 │ ├── Datasheet/ // 关键元器件数据手册 │ └── BOM.xlsx // 物料清单,含采购参考价 ├── Firmware/ │ ├── Core/ // 启动文件、系统时钟、中断 │ ├── Drivers/ // DHT11、MQ-135、OLED、蜂鸣器驱动 │ ├── App/ // 显示逻辑、按键逻辑、报警逻辑 │ └── MDK-ARM/ // Keil 工程文件 ├── Simulation/ │ └── Environmental_Monitoring.pdsprj // Proteus 工程 ├── Docs/ │ ├── 接线说明.md │ ├── 调试记录.md │ └── 实物测试视频/ └── README.mdREADME.md 里放了系统的整体框图、引脚分配表、编译烧录步骤、以及常见问题说明。硬件资料放在 Hardware 目录,软件放在 Firmware,两者一一对应。别人拿到仓库,先看 README,再打开原理图,然后编译代码、加载仿真,整条链路就通了。
6.2 后续可以这样扩展
这版系统是本地监测的起点,扩展空间很大。我目前规划了几个方向:
一个是联网上报。通过串口接一个 ESP8266 模块,用 AT 指令把温湿度和空气质量数据发布到 MQTT 服务器,手机端就能远程查看。代码层只需要增加一个 Network 模块,在 App 层加一个定时上报任务,现有的驱动层不需要改动。
另一个是增加传感器种类。当前系统是温湿度加空气质量,如果要做更全面的环境监测,可以扩展 PM2.5 传感器(GP2Y1010AU0F)或者 CO2 传感器(MH-Z19)。这两个传感器都是模拟或串口输出,原理图设计思路和 MQ-135 类似,不过 PM2.5 传感器需要额外的 LED 驱动电路,功耗会明显上升。
还有一个方向是低功耗。当前系统在电池供电场景下有点浪费,因为 OLED 一直亮着,蜂鸣器驱动也常驻。后续可以加入 STM32 的 STOP 模式,用定时唤醒周期性采集数据,OLED 只在触摸按键或收到指令时才点亮。这种改造对硬件电路影响不大,但代码层面需要引入低功耗管理逻辑。
我自己在用这套项目继续改造成一个室内环境监测盒子,预计下一版会加入联网上报和 PM2.5 传感器。说句实在话,这类项目的难点从来不在单个传感器怎么读,而在于整个系统怎么把电源、时序、噪声、数据处理这些事儿协调好。把这套思路吃透,以后再做别的嵌入式项目,你会发现自己排查问题的速度快很多。