
环境质量监测这类项目实验室里做一遍和真正把它跑稳中间差着好几条街。我手上这套基于STM32的环境质量监测系统前后改了三版硬件、两版固件从最初只能读个温湿度到现在能同时采集温度、湿度、空气污染物浓度、粉尘颗粒物带OLED本地显示、阈值报警和串口上报代码、原理图、仿真工程三样都开源了出来。写这篇东西的起因很简单网上能找到的单点教程太多DHT11怎么读、ADC怎么配都是碎片但把传感器、电源、显示、报警、上位机通信这几块拼成一个能交付的系统中间那些没人写的坑才是真正花时间的地方。这篇博文面向的是刚上手STM32想做完整项目的同学也适合已经能点灯但没做过综合系统的人我会把每个设计选择的理由、关键参数怎么算、原理图上每颗电阻电容为什么在那儿、代码为什么这么分层全部摊开讲。看完你应该能把这套东西自己复现一遍并且知道哪里最容易出问题。1. 项目整体架构与方案选型思路1.1 环境质量监测到底该测哪些量先想清楚需求边界再动手画图这是我踩了第一次坑之后总结出来的顺序。最初我图省事想着环境质量嘛温湿度加个空气质量就够了结果做完发现完全不是那么回事。真正意义上的环境质量监测至少覆盖三类物理量一是热环境参数也就是温度和湿度这两项直接决定人体舒适度也影响其他传感器的读数漂移二是气态污染物代表性的是一氧化碳、氨气、烟雾、可挥发性有机物这一类用半导体气体传感器能给出一个综合的空气质量趋势三是颗粒物也就是常说的粉尘PM2.5、PM10 这类需要专门的粉尘传感器用光学散射原理测。为什么不能只用一个传感器糊弄过去因为半导体气体传感器比如常见的 MQ 系列对多种还原性气体都有响应它输出的是一个混合浓度信号你没法区分到底是香烟烟雾还是酒精挥发。而粉尘传感器测的是颗粒物原理完全不同。这两类数据互相不能替代放在一起才能拼出一张相对完整的环境画像。至于温湿度它的意义不只是显示一下后面会讲到半导体气体传感器的输出会随温湿度漂移做长期监测时温湿度数据是做补偿的参考。所以这套系统的传感器组合我最终定成DHT11 负责温湿度MQ-135 负责空气质量趋势GP2Y1010AU0F 负责粉尘浓度。三个传感器接口类型各不相同——单总线、模拟电压、脉冲驱动加模拟输出正好把 STM32 的 GPIO、ADC、定时器三大外设都用上做出来的东西不空。1.2 主控芯片与外围器件的取舍逻辑主控这一块没什么悬念STM32F103C8T6也就是大家常说的蓝板子那颗芯片。为什么选它而不是别的三个理由很实在。第一资源够用且不浪费。它有一颗 72MHz 的 Cortex-M3 内核、64KB Flash、20KB SRAM跑我们这个系统绰绰有余——代码量撑死三四十 KBRAM 占用也就几 KB。上更高端的 F4 系列属于杀鸡用牛刀成本上去了对学习者来说寄存器配置还更复杂。第二外设匹配度好。它有 2 个 12 位 ADC、3 个通用定时器、2 个 USART、2 个 I2C我们需要的 ADC 采集、定时器做单总线时序和 PWM、USART 做上报、I2C 驱动 OLED全部原生支持不用软件模拟时序稳定性高一大截。第三生态资料厚。这一点对开源项目特别重要别人拿到你的工程想改随便一搜就有大量基于这颗芯片的例程可参考。外围器件的选择也有讲究。显示屏我没用 LCD1602而是选了 0.96 寸的 SSD1306 OLEDI2C 接口。原因很直接1602 要占用 6 根以上 IO 口做并行驱动接线复杂而且只能显示字符没法画趋势曲线OLED 只要两根线SCL、SDA分辨率 128×64后面想加个小柱状图表示浓度变化完全没问题。报警用有源蜂鸣器注意是有源——内部带振荡电路给高电平就响不需要单片机输出 PWM 方波省一个定时器。如果用无源蜂鸣器就得额外生成频率代码复杂度上去了。通信这块留了 USART1 做串口上报波特率 115200方便接串口助手或者后面扩展接无线模块。电源部分我用的是 Micro USB 5V 输入经过 AMS1117-3.3 降到 3.3V 给主控和数字器件供电传感器那一路 5V 直接取 USB 的 5V因为 MQ-135 和粉尘传感器的加热/驱动部分需要 5V这一点在画原理图的时候必须分开走线不能全用 3.3V。1.3 代码、原理图、仿真三件套为什么都要有很多开源项目只丢一堆代码别人拿到手连不上硬件等于没法用。我这套东西坚持出三样是有明确目的的。原理图解决线怎么接的问题尤其是初学者最容易搞混的电源分配、上拉电阻、去耦电容这些图上标清楚比文字描述强一百倍。代码解决逻辑怎么跑的问题而且要分层写清楚不能一坨 main 函数里全塞满。仿真解决没硬件怎么验证的问题——这是最容易被忽略的一点。很多人手头暂时没有元器件或者传感器还没到货仿真能让你先把代码逻辑跑通看着虚拟的 OLED 显示出数据、虚拟的蜂鸣器按阈值响起来心里就有底了。等硬件到齐再烧进去调试难度直线下降。这里我要强调一个心态问题仿真不是万能的它只能验证逻辑验证不了真实电路的电气特性。比如 DHT11 的单总线时序仿真环境里模型响应是理想的你代码里延时写错了它也能过但真实硬件上就会读失败。所以我的建议是代码逻辑在仿真里过一遍时序和电气相关的东西必须在真板上调。两者配合用效率最高。2. 硬件原理图设计要点与关键电路解析2.1 主控最小系统别小看那几颗电容最小系统是整个板子的地基看起来简单但出问题最集中的就是这里。STM32F103C8T6 的最小系统包含四部分电源、晶振、复位、启动模式配置。电源部分芯片的 VDD 引脚一共有好几组每一组旁边都必须配一颗 100nF 的陶瓷去耦电容紧贴着引脚放走线越短越好。为什么要这么做因为数字电路在工作时电流是脉冲式的高频跳变会在电源线上产生噪声去耦电容相当于一个就近的小水库把这些瞬态电流就地消化掉不让噪声串到别的引脚。我见过有人为了省事只在芯片旁边放一颗 10uF结果 ADC 采集出来的数据一直抖换了就近的 100nF 之后立刻稳定。另外 VDDA模拟电源单独走一路也要加 100nF 加 1uF 的组合因为我们的 ADC 要采模拟量模拟电源干净直接影响采集精度。还有个细节VBAT 引脚如果不接纽扣电池直接接到 3.3V 就行别悬空。晶振部分我用的是外接 8MHz 无源晶振搭配内部倍频到 72MHz。晶振两端各接一颗 22pF 的负载电容到地。这个 22pF 不是随便定的它要和晶振本身的负载电容参数匹配常见 8MHz 晶振的负载电容是 20pF 左右考虑 PCB 上的寄生电容取 22pF 附近比较稳妥。如果用内部 RC 振荡器省掉外部晶振也能跑但时钟精度差做串口通信时波特率误差会让通信出错所以正式项目还是老老实实外接。复位电路是 10kΩ 上拉电阻加 100nF 电容到地再加一个复位按键。上电瞬间电容充电NRST 引脚维持短暂低电平完成复位之后被 10k 拉高正常工作。启动模式 BOOT0 和 BOOT1 我各接了一个 10kΩ 下拉到地默认从主 Flash 启动。如果要做串口下载BOOT0 要能跳到高电平所以那个位置可以用跳线帽或者按键切换画图的时候留出来。2.2 三个传感器的接口电路差异在哪三个传感器接口各不相同这是原理图里最需要动脑的部分。DHT11 是单总线数字传感器数据线一根。它的通信协议要求主机和从机分时驱动同一根线所以这根线必须接一个 4.7kΩ 到 10kΩ 的上拉电阻到 3.3V。为什么需要上拉因为总线在空闲状态要靠上拉电阻维持高电平双方都不驱动的时候线是高谁要发数据谁就把它拉低。没有上拉电阻线会浮空读出来全是乱码。我一般取 4.7kΩ速度响应好一些线长的时候可以适当调小到 3.3kΩ太长的话上拉不能太小否则静态功耗大。DHT11 的供电是 3.3V 到 5V 都行我接的是 3.3V这样数据线电平和 MCU 一致不用电平转换。MQ-135 是模拟输出模块上一般有四个引脚VCC、GND、DO、AO。VCC 接 5V因为它的加热丝需要 5V 才能正常工作接 3.3V 的话灵敏度会明显下降。AO 是模拟输出接 MCU 的 ADC 输入引脚。这里有个坑MQ-135 输出的是 0 到 5V 范围的电压而 STM32 的 ADC 参考电压是 3.3V直接接进去超压会损坏引脚。解决办法有两种要么在 AO 和 ADC 之间加一个电阻分压比如 10k 对 20k把 5V 分到 3.33V要么选用那些输出范围本来就限制在 3.3V 以内的模块。我选的是分压方案成本低两个电阻的事。DO 数字输出我没用因为它的阈值是电位器手动调的精度没法保证。粉尘传感器 GP2Y1010AU0F 稍微麻烦一点它是六脚封装里面有一颗红外 LED 和光电接收管需要外部给它一个脉冲信号驱动 LED然后在特定时刻采集模拟输出。典型接法是LED 驱动脚通过一个 150Ω 限流电阻接 5V同时这个脚接 MCU 的一个 GPIO 用于给脉冲它的模拟输出脚接 ADC输出端还要并一颗 220uF 加 100nF 的滤波电容因为它的输出信号上叠加了 LED 的开关噪声不滤波采出来全是毛刺。采集时序也很关键LED 给低电平点亮等大概 0.28ms 后再去采 ADC这个延时必须对早了信号还没稳定晚了 LED 已经关了。这套时序后面代码部分会详细写。三路传感器接口的对比整理成表格更清楚传感器接口类型供电关键外围MCU 资源DHT11单总线数字3.3V4.7k 上拉GPIO 微秒延时MQ-135模拟电压5V电阻分压至 3.3VADC 通道GP2Y1010AU0F脉冲驱动 模拟5V150Ω 限流、220uF 滤波GPIO ADC 定时器2.3 显示、报警与通信外设的电路细节OLED 显示这块SSD1306 的 I2C 接口只有 SCL 和 SDA 两根线接 MCU 的 PB6、PB7I2C1 的默认引脚。I2C 是开漏输出结构所以这两根线也必须各接一个 4.7kΩ 上拉到 3.3V。很多人忘了上拉结果 OLED 死活不亮或者偶尔亮一下又黑屏排查半天以为是代码问题。另外 OLED 的供电是 3.3V不能接 5V接了会烧。它的 I2C 地址一般是 0x787 位地址 0x3C 左移一位如果买到的模块背面有地址跳线可能是 0x7A写代码的时候要注意。报警这一块我用的是有源蜂鸣器加一颗 S8050 三极管驱动。为什么不用 GPIO 直接驱动因为蜂鸣器的工作电流通常有 20 到 30mA超出 STM32 单个 IO 引脚的驱动能力一般建议不超过 8mA 到 20mA长时间直驱会损伤引脚。三极管方案是标准做法GPIO 通过一颗 1kΩ 基极电阻接 S8050 的基极发射极接地集电极接蜂鸣器负极蜂鸣器正极接 3.3V 或 5V。蜂鸣器两端再反并一颗 1N4148 续流二极管因为蜂鸣器内部有线圈关断瞬间会产生反向电动势二极管把它泄放掉保护三极管。这颗二极管经常被省略短时间看不出问题但长期用容易把管子打坏。串口上报用 USART1PA9 是 TXPA10 是 RX交叉接到 USB 转串口模块。115200 波特率下两米以内的线长基本不会出错。如果要做长距离通信建议降低波特率到 9600或者加 485 转换芯片。这一路我留了排针方便随时接上串口助手看数据。2.4 画完原理图之后别急着打样原理图画完很多人手一抖就去下单打板了我的建议是先做两件事。第一件是电气规则检查用软件自带的 ERC 功能跑一遍重点看有没有电源引脚没接、网络标签有没有重名、悬空的输入引脚。ERC 报告里有些是警告可以忽略但凡是提示未连接的一定要逐个确认是不是真的该悬空。第二件是把关键网络的走线在脑子里过一遍特别是电源和地。我一般习惯在原理图上把 5V、3.3V、GND 用不同颜色标出来肉眼确认一遍没有把 5V 传感器接到 3.3V 网络上去。这个错误非常常见因为画图时网络标签写快了容易混。如果要把原理图导出成 PDF 分享给别人注意选全页面范围导出不然有些软件默认只导出当前视图别人拿到的 PDF 只有局部看着一头雾水。3. 固件代码分层实现从驱动到业务逻辑3.1 工程目录怎么分才不会被自己搞乱代码写乱是新手到进阶路上最大的坎。我见过太多工程所有代码全塞在 main.c 里两千行改一个地方要在里面翻半天。我这套工程坚持分四层硬件抽象层、驱动层、业务层、应用层。目录结构是这样的EnvMonitor/ ├── Core/ │ ├── main.c // 主循环与应用调度 │ └── stm32f1xx_it.c // 中断服务函数 ├── Drivers/ │ ├── dht11.c/.h // 温湿度驱动 │ ├── mq135.c/.h // 气体传感器驱动 │ ├── dust.c/.h // 粉尘传感器驱动 │ ├── oled.c/.h // 显示驱动 │ ── buzzer.c/.h // 报警驱动 ├── BSP/ │ ├── delay.c/.h // 微秒/毫秒延时 │ └── usart.c/.h // 串口收发 └── App/ └── monitor.c/.h // 数据融合、阈值判断、上报为什么这么分硬件抽象层BSP负责最底层的延时和串口这些通用能力换芯片时基本不用改驱动层每个传感器一个文件只干一件事——把物理量读出来对外提供一个干净的接口函数业务层做数据整合和逻辑判断比如判断哪个值超阈值、要不要报警应用层的 main.c 只负责初始化和周期调度。这样写的好处是哪天你想把 DHT11 换成 SHT30只需要改驱动层的文件业务层和应用层一行都不用动。这就是分层的价值也是让代码能被别人复用的前提。3.2 DHT11 的单总线时序到底怎么卡DHT11 的时序是这个项目里对时间最敏感的部分微秒级的误差就会导致读取失败。整个过程是这样的主机先把数据线拉低至少 18ms然后拉高 20 到 40 微秒接着释放总线进入输入模式等待 DHT11 响应。DHT11 检测到主机的起始信号后会先拉低约 80 微秒表示响应再拉高约 80 微秒然后开始发送 40 位数据。每一位数据都以一个约 50 微秒的低电平开始之后的高电平持续时间决定这一位是 0 还是 1高电平持续 26 到 28 微秒是 0持续 70 微秒左右是 1。关键代码长这样// 主机发送起始信号 GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; DHT11_SetMode(GPIO_InitStructure); DHT11_Data_Low(); delay_ms(20); // 拉低至少 18ms DHT11_Data_High(); delay_us(30); // 拉高 20-40us // 切换为输入模式等待 DHT11 响应 GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPU; DHT11_SetMode(GPIO_InitStructure); if (DHT11_WaitLevel(0, 100) ! 0) return 1; // 等待 80us 低电平 if (DHT11_WaitLevel(1, 100) ! 0) return 1; // 等待 80us 高电平 // 读取 40 位数据 for (i 0; i 5; i) { dat 0; for (j 0; j 8; j) { if (DHT11_WaitLevel(0, 100) ! 0) return 1; // 等待 50us 低电平 delay_us(40); // 延时 40us 后采样若仍为高则是 1 dat 1; if (DHT11_ReadPin()) dat | 1; if (DHT11_WaitLevel(1, 100) ! 0) return 1; // 等待高电平结束 } buf[i] dat; }这里有几个要点必须说清楚。第一延时函数必须准。我用的不是系统滴答定时器它的最小单位是毫秒级而是额外配了一个定时器做微秒延时或者在 72MHz 下用循环做精确的微秒级空操作。用软件循环做延时要注意编译优化等级开了 -O2 优化后循环次数会变延时就不准了所以微秒延时函数要加 volatile 修饰循环变量。第二读数据前先关总中断或者至少把中断优先级调低因为一个中断进来可能耽误几十微秒直接把位判断打乱。第三读完之后一定要做校验和验证DHT11 的第五个字节是前四个字节之和的低八位校验不通过就丢弃这次数据不要显示出来误导自己。3.3 ADC 采集与浓度换算的真实做法MQ-135 和粉尘传感器都是模拟输出走 ADC。STM32F103 的 ADC 是 12 位精度参考电压 3.3V所以最小分辨电压是 3.3 除以 4096约等于 0.806 毫伏。这个数要记住后面算浓度全靠它。采集的时候我用了多通道扫描加 DMA这样不占用 CPU采完自动搬到数组里。配置上选了 55.5 个周期的采样时间因为传感器输出阻抗比较高采样时间太短的话内部采样电容充不满读数会偏小。MQ-135 的浓度换算网上很多代码直接拿 ADC 值当浓度显示这是不严谨的。正确的思路是先算出传感器电阻 Rs再对比洁净空气中的基准电阻 R0得到一个比值然后用对数曲线换算。具体公式是这样先根据分压关系算出负载电阻 RL 上的电压 VRLRs 等于 (VCC 减 VRL) 除以 VRL 再乘以 RL。R0 需要在洁净空气里校准一般让模块预热 24 小时后在室外清新空气中读一个值作为基准。有了 Rs 和 R0 的比值再套用数据手册里的对数关系就能得到 ppm 量级的估算值。说实话MQ-135 的绝对精度一般别太当真它的价值在于看趋势——今天 200明天 800说明空气变差了这就够用了。我在代码里把换算结果同时保留原始 ADC 值和换算值方便调试时对比。float MQ135_GetPPM(uint16_t adc_val) { float vrl adc_val * 3.3f / 4096.0f; float rs (5.0f - vrl) / vrl * RL_VALUE; // RL_VALUE 取 10.0 float ratio rs / R0_VALUE; // R0 需现场校准 // 经验公式系数来自传感器特性曲线拟合 return 116.6020682f * powf(ratio, -2.769034857f); }粉尘传感器的处理又是另一套。因为它需要脉冲驱动我在定时器里每隔 10ms 触发一次先把 LED 驱动脚拉低点亮延时 280 微秒让光路稳定然后启动 ADC 采集一次采完把 LED 驱动脚拉高关闭。这样采到的值才对应正确的光散射强度。换算浓度时用厂家给的近似关系输出电压每升高 0.1V 大约对应 10 微克每立方米实际使用中可以按这个比例做粗略标定。同样这个值也不是绝对准确的看相对变化更有意义。3.4 OLED 显示与串口上报的排版经验OLED 显示不难难的是排版。128×64 的分辨率一行 8 个像素高最多显示 8 行每行 16 个字符8×16 点阵。我第一版显示就是把所有数据一股脑堆上去三行温湿度、两行空气质量、两行粉尘加上单位挤得满满当当看着特别乱。后来改成左边显示名称、右边显示数值加单位中间留出对齐空隙清爽多了。OLED_ShowString(0, 0, Temp:, 16); OLED_ShowNum(40, 0, temp_int, 2, 16); OLED_ShowString(56, 0, ., 16); OLED_ShowNum(64, 0, temp_dec, 1, 16); OLED_ShowString(72, 0, C, 16);刷新的频率也要控制。OLED 全屏刷新一次I2C 要传 1KB 数据按 400kHz 速率算大概 20 多毫秒。如果主循环里每次都刷会把 CPU 大量时间耗在通信上影响传感器读取的实时性。我的做法是每 500 毫秒刷新一次屏幕传感器采集和逻辑判断每 2 秒一轮两者分开计时用滴答定时器做时间戳判断不阻塞主循环。串口上报用 printf 重定向的方式最省事。重定向 fputc 函数把 printf 的内容定向到 USART1这样代码里直接写 printf 就行。上报格式我用了自定义的简单协议以固定逗号分隔方便上位机解析printf($DATA,%d.%d,%d.%d,%u,%u,%u\r\n, temp_int, temp_dec, humi_int, humi_dec, mq135_raw, dust_raw, air_quality_level);开头用 $DATA 做帧头结尾加回车换行上位机按行读取再按逗号分割就能拿到所有字段。这套格式简单粗暴但很好用后面想接上位机画曲线改改解析脚本就行。3.5 主循环与阈值报警该怎么组织主循环千万不要写成一大串顺序执行加一堆 delay那样任何一个环节卡住整个系统都停摆。我用的是时间片轮询结构主循环里不断检查各个任务的计时器到点了就执行对应的任务其他时间空转。伪代码逻辑是这样的while (1) { if (tick - last_sample 2000) { last_sample tick; g_temp DHT11_ReadTemp(); g_humi DHT11_ReadHumi(); g_mq135 MQ135_Read(); g_dust Dust_Read(); g_level JudgeAirLevel(g_mq135, g_dust); } if (tick - last_display 500) { last_display tick; UI_Update(g_temp, g_humi, g_mq135, g_dust, g_level); } if (g_level LEVEL_BAD) { Buzzer_On(); } else { Buzzer_Off(); } }阈值判断我分了三档优良、一般、较差。空气质量综合等级取气体浓度和粉尘浓度中较差的那一档避免出现一个指标很差但被另一个指标拉平的情况。报警不是一超就猛响我加了 3 秒的滞回也就是说浓度超标后要连续 3 秒都超标才响避免瞬时波动导致误报。这个细节在实际使用中很重要否则蜂鸣器会一直滴滴答答烦得你想把它拆了。4. 仿真环境搭建与联调实录4.1 仿真工程怎么搭起来仿真我是在 Proteus 里做的配合 Keil 编译出来的 hex 文件运行。搭建步骤并不复杂但有几个地方容易卡住。先把 STM32F103C8 的模型拖进原理图然后在元件库里找 DHT11、OLED、蜂鸣器这些器件。DHT11 在 Proteus 里有现成的模型但它的行为是理想化的你给它一个触发它就返回一个固定值。OLED 如果用 SSD1306 的模型直接搜可能搜不到可以退而求其次用 12864 图形液晶模型替代或者用字符液晶显示数字重点是验证代码逻辑而不是显示效果。MQ-135 和粉尘传感器这类模拟器件仿真里通常用可调电位器模拟它们的模拟输出这样你转动电位器就能看到显示数值变化直观验证采集和换算逻辑对不对。把 Keil 生成的 hex 文件路径填进 STM32 模型的属性里设置好晶振频率为 8MHz然后点运行。如果一切正常你会看到虚拟屏幕上出现数据转动电位器数值跟着变。这里要提醒一句仿真里的时序是理想化的DHT11 那套微秒级时序在仿真里几乎必过但到真板上可能失败所以仿真通过的代码不要盲目认为硬件上也能跑通。4.2 仿真和真实硬件差在哪儿这个坑我踩得很深必须单独说。仿真是逻辑验证工具不是硬件验证工具两者的差距主要体现在三个方面。第一是电气特性仿真里不存在上拉电阻不够导致的边沿变缓、不存在电源噪声、不存在 ADC 输入阻抗匹配问题这些在真板上都会让读数变化。第二是时序余量仿真里 CPU 执行速度和真实芯片可能有差异我遇到过仿真跑得好好的延时烧到真板上 DHT11 就读不出来最后发现是仿真环境里的空循环次数和真实芯片不一样。第三是外设行为比如 ADC 在仿真里可能瞬时返回一个理想值真实 ADC 需要采样保持时间配置不对就会采到跳变的值。所以我的建议是仿真只用来验证三件事代码能不能编译通过、主循环逻辑有没有死锁、数据显示格式对不对。至于传感器读数准不准、时序对不对、通信稳不稳一律要在真板上验证。4.3 联调过程中的现象记录真板联调的第一天我遇到的现象是 OLED 亮但不显示内容。用逻辑分析仪抓 I2C 波形发现 SCL 有信号但 SDA 一直没有应答。排查过程是这样的先怀疑地址错了翻了模块手册确认是 0x78代码里也是 0x78排除再怀疑上拉电阻用万用表量 SDA 对 3.3V 的阻值发现是无穷大说明板上根本没焊上拉电阻补上两颗 4.7k 之后屏幕立刻正常。这个例子说明遇到通信问题先用仪器看波形比盲改代码高效得多。第二个现象是 MQ-135 读数一直在满量程附近跳。查下来是分压电阻选错了我一开始用两个 10k 分压得到 2.5V但这个电压对应的 ADC 值已经接近满量程稍微有点噪声就顶到上限。换成 10k 加 20k 的分压后输出电压范围落到 0 到 2.2V 左右留出了充足余量读数就稳了。这里面其实有个简单的校验方法算一下传感器最大输出电压经过分压后是多少确保它不超过 3.0V留出 10% 以上的余量。第三个现象是系统跑几个小时后死机。这个最麻烦因为它不是每次都出现。后来我在主循环里加了看门狗同时把串口上报的 printf 改成非阻塞发送问题就消失了。原因应该是串口发送用了阻塞等待某个时刻上位机没及时收数据导致发送缓冲区满程序卡在等待里加上看门狗后即使卡住也能自动复位。这个经验很值钱凡是涉及外部通信的地方都要考虑对方不响应的情况别让程序无限等待。5. 常见问题与排查速查表5.1 传感器类问题怎么定位传感器的问题占了调试时间的一大半我整理了一张速查表按现象找原因现象可能原因排查方法DHT11 一直读失败上拉电阻缺失或过大量数据线对 VCC 阻值应在 4.7k 左右DHT11 偶发校验错误中断打断时序读期间关中断或降低中断优先级MQ-135 读数不变化加热丝未通电确认 VCC 接的是 5V 而非 3.3VMQ-135 读数满量程分压比不对重算分压后电压确保不超 3.0V粉尘读数全是噪声输出未滤波输出端并 220uF 加 100nF粉尘读数偏低采样时刻不对确认 LED 点亮后延时约 280us 再采表里的每一条都是实际踩过的。特别说一下最后一条粉尘传感器的采样时刻如果提前采到的是 LED 还没完全点亮的暗电流数值会明显偏低这个现象很容易被误判成传感器坏了。5.2 编译烧录和显示通信类的坑编译烧录类的问题相对好定位但有几个高频的。一是头文件路径没加报找不到头文件去工程设置里把 Drivers 和 BSP 目录加进包含路径就行。二是重复定义通常是把变量定义写在头文件里被多个源文件包含后冲突正确做法是头文件里用 extern 声明源文件里定义。三是烧录时提示找不到设备先确认 BOOT0 是不是接对了再检查 ST-Link 的驱动装了没有最后看芯片是不是被读保护了用工具解除保护即可。四是烧进去没反应检查复位电路和晶振有没有起振用示波器量一下晶振引脚有没有波形。显示通信类的问题OLED 不亮先查上拉和供电花屏一般是初始化序列不对或者通信速率太快把 I2C 速率降到 100kHz 试试。串口乱码九成是波特率不匹配或者系统时钟配置和实际晶振不符。我习惯在系统初始化后先往串口发一句固定的问候字符串上位机能正确收到就说明时钟和波特率都对这一步能省掉大量排查时间。5.3 几条没人写但很值钱的避坑经验第一条焊接顺序有讲究。先焊主控和最小系统烧个点灯程序确认芯片活着再焊传感器接口。如果一次性全焊完出问题你都不知道是芯片的事还是传感器的事。分步验证虽然慢一点但总时间更短。第二条每个传感器单独测试。别指望一次把所有传感器都接上就能全部读对先把 DHT11 调通再把 MQ-135 调通一个一个来。每个都通了再整合整合时如果出问题至少你知道单个模块本身没问题。第三条给关键变量加调试输出。我在传感器读取函数里都留了条件编译的调试打印定义 DEBUG 宏就输出原始数据不定义就不输出。这样出问题时打开宏串口一看原始值就知道是采集的问题还是换算的问题定位速度快很多。第四条电源要留余量。整套系统全速运行时的电流峰值可能比静态时大不少尤其是蜂鸣器响、传感器加热同时进行的时候。如果电源适配器余量不够会出现复位或者读数异常这类问题特别隐蔽容易误判成代码 bug。建议选 1A 以上的 5V 电源别用电脑 USB 口凑合。6. 我在这套项目里的一些实际体会整套东西做下来最大的感受是STM32 项目的难点从来不在会不会写代码而在知不知道硬件此刻在做什么。我调试传感器读取失败的时候前几次都在改代码里的延时参数改来改去没用后来拿逻辑分析仪抓了一次波形才发现是上拉电阻的阻值让上升沿变得太缓DHT11 把这个缓变的高电平识别成了低电平。这种问题光看代码永远找不到。所以我现在形成习惯任何和外部器件打交道的地方先想清楚信号在物理上是什么样子是电压、是电流、是时序想清楚了再动手写代码效率比盲目试错高太多。代码分层这件事我也想多说两句。刚开始写单片机程序谁都想把所有东西塞进 main 里觉得这样最直观。但做到这套系统这种规模三四个传感器加显示加通信如果还不分层后面想改一个功能就得通读几百行改完还怕影响别的地方。分层不是为了显得专业是为了让未来的自己少受罪。你现在多花十分钟把文件分好后面调试的时候能省好几个小时。最后再说一个仿真的事。我现在的流程是代码先在仿真里把逻辑跑通确认没有死循环、显示格式没问题、阈值判断逻辑对然后再烧真板。真板上只调仿真验证不了的东西——时序、电气、传感器实际读数。这样分工之后真板调试的时间明显缩短了。仿真不是替代硬件是帮你把能提前解决的问题提前解决掉剩下的精力全用在真正难的地方。这套工程后续还能往下延伸。比如把串口上报换成连无线模块做成远程监测比如在 OLED 上加一个简单的浓度趋势曲线把最近几十次采样画出来再比如用上 SD 卡做本地数据记录方便回溯。这些都是在这套骨架上加积木不太需要动底层感兴趣的话可以自己接着往下做。