
不少做嵌入式开源项目的朋友都遇到过同一个尴尬网上搜“STM32 环境监测”出来的工程十个里有八个只有一个 main.c原理图是一张模糊的截图仿真干脆没有。代码能跑但换个人、换个板子就废了。我自己早期做过一个温度湿度监测器就是这个德行——毕业设计答辩现场传感器数据死活跳变最后只能靠嘴硬撑过去。所以这次我把整套环境质量监测系统完整开源不是只丢代码而是把代码、原理图、仿真工程三样东西对齐了放出来。基于 STM32F103C8T6搭配 DHT11 温湿度传感器、MQ-135 空气质量传感器、0.96 寸 OLED 屏和蜂鸣器报警实现温湿度、空气质量等级的实时监测与超限报警。项目不复杂但覆盖了从最小系统设计、传感器驱动、ADC 采样滤波到 Proteus 仿真的完整链路适合正在学 STM32、准备做课设/毕设、或者想入坑嵌入式开源项目的朋友。这篇文章我会把项目的设计思路、关键代码逻辑、仿真搭建过程以及从仿真转到实物后踩过的几个典型坑一次性讲清楚。1. 为什么我不再做“只有一个 main.c 的环境监测工程”很多入门者做 STM32 项目时都有一个习惯把全部逻辑塞进 main.cGPIO 初始化、延时、传感器读取、显示刷新全写在 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 最小系统原理图的关键点先看核心芯片 STM32F103C8T6LQFP48 封装Flash 64KBRAM 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 接 PB6SDA 接 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 晶振系统时钟 72MHzGPIOPA6 输入模式DHT11 数据口PA1 模拟输入ADCPB0 推挽输出蜂鸣器PA0 输入按键I2C1标准模式 100kHz地址 0x3C 即 7 位地址 0x3C实际写入地址是 0x78ADC1通道 1PA1采样时间 55.5 周期软件触发SysTick1ms 时基用于延时函数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 和 GNDVDD 接 3.3V添加 DHT11 模型Proteus 库里有数据引脚接 PA6VCC 接 5V添加 OLED 模型SSD1306SCL 接 PB6SDA 接 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 供电电压恢复到 5VAMS1117 输出 3.3VDHT11 就正常了。这个案例说明遇到传感器不工作先量供电电压再查信号波形最后才怀疑代码。排查顺序反了会浪费很多时间。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 参考电压是 VDDAVDDA 和 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 传感器。说句实在话这类项目的难点从来不在单个传感器怎么读而在于整个系统怎么把电源、时序、噪声、数据处理这些事儿协调好。把这套思路吃透以后再做别的嵌入式项目你会发现自己排查问题的速度快很多。