STM32F103C8T6家庭环境监测系统:DHT11+MQ-2+OLED完整开源实战

发布时间:2026/9/9 2:56:08
STM32F103C8T6家庭环境监测系统:DHT11+MQ-2+OLED完整开源实战 1. 项目概述这套家庭环境监测系统到底在做什么这两年智能家居的话题热度一直不减但市面上的成品设备要么价格虚高要么封闭得很想改个阈值、加个传感器都无从下手。作为一个经常捣鼓STM32的嵌入式玩家我一直在琢磨能不能用手头现有的芯片和传感器自己拼一套够用、能扩展、还能随时看数据的家庭环境监测系统。这次开源的这套方案就是基于STM32F103C8T6最小系统板搭配DHT11温湿度传感器、MQ-2烟雾传感器、光敏电阻和OLED显示屏实现室内温湿度、烟雾浓度、光照强度的实时采集与显示并带有阈值报警和自动通风控制功能。整套系统做下来最大的感受是它不只是一块开发板加几个传感器的简单堆叠而是一个从硬件设计、驱动编写到系统联调的完整闭环。项目全部开源包含可编译的工程代码、详细的原理图文件、Proteus仿真工程和完整的DIY过程说明。无论你是刚学完STM32基础外设、想做点实际东西的学生还是想给家里添一套低成本环境监测方案的爱好者这套项目都能给你一个可以直接上手、还能按需改动的参考底座。从技术栈上看这套系统涉及的知识点非常典型GPIO模拟时序驱动DHT11、ADC多通道采集烟雾和光照信号、I2C驱动OLED屏、PWM控制风扇转速、外部中断加定时器做按键和报警逻辑。把这些点串起来基本就覆盖了单片机开发中90%的日常操作技能。后面我会把每个环节怎么拆、怎么选、怎么调全部展开讲清楚。2. 系统整体设计思路为什么选这套方案而不是别的做法2.1 项目需求拆解监测什么、显示什么、控制什么在设计这套环境监测系统之前我先把需求老老实实列了个清单。家庭环境监测最基础的就是温度和湿度考虑到厨房和燃气安全烟雾浓度必须要有这里的MQ-2传感器不仅能测烟雾对液化气、丙烷、氢气这类可燃气体也有响应再来就是光照强度可以反映室内采光情况也为后续自动调节灯光或者窗帘留了接口。光有采集还不够数据没有出口等于白采。所以系统需要一块显示屏把实时数据直观呈现出来。考虑到成本和驱动难度我选了0.96寸I2C接口的OLED屏四根线就能搞定显示效果也远比LCD1602那种老屏清晰。然后再加一个本地报警机制温湿度越限或者烟雾浓度超标时蜂鸣器发声、LED闪烁同时自动开启排风扇通风。这样一来系统就有了从感知到执行的完整闭环而不是一个只会“看”的哑巴设备。2.2 主控芯片选型为什么是STM32F103C8T6关于主控选型我几乎没有犹豫就定了STM32F103C8T6。这颗芯片大概是最适合新手从0到1做项目的一款Cortex-M3内核单片机72MHz主频64KB Flash20KB SRAM3路USART、2路SPI、2路I2C、2个ADC12个通道还有定时器、PWM、外部中断这些外设配置十分齐全。以这个项目的资源消耗来说Flash和SRAM连一半都用不到余量非常充足后续想加个ESP8266做Wi-Fi上云、加个蓝牙模块做手机联动空间都还够。选型这事很多新手容易陷入一个误区总想着“芯片越强越好”一上来就搞H743甚至H7系列结果引脚复杂、封装难焊、调试麻烦一个点灯程序都能折腾两天。我的建议是项目选型要看“够用”和“好上手”这两条线。C8T6的LQFP48封装手工焊接或直接买最小系统板都方便网上资料铺天盖地遇到问题搜一下就有答案。这颗芯片还有多个国产替代型号可以直接兼容比如APM32F103C8T6程序基本能直接烧录运行供应链上也更稳。因为用户量大Keil MDK、STM32CubeMX、Proteus这些工具对它的支持都非常完善后文要讲到的仿真环节里Proteus 8.9以上版本可以直接选这个型号做硬件仿真这一点是很多更高端芯片做不到的。2.3 传感器选型与取舍DHT11、MQ-2和光敏电阻的组合逻辑传感器选型这块我重点考虑的是“项目可复现性”和“开发体验”之间的平衡。温湿度传感器市面上常见的有DHT11、DHT22、SHT30等。DHT11精度一般温度±2°C湿度±5%RH但胜在便宜、库多、单总线时序简单非常适合做教学和入门项目。DHT22精度高一些但价格接近3倍且同样是单总线协议时序比DHT11更敏感稍不注意读取就会失败。SHT30是I2C接口精度和稳定性都非常好就是驱动代码稍微长一点。在这个项目里监测家庭环境温湿度DHT11的精度完全够用所以我最终选了它。如果你后续想追求更精准的数据直接换DHT22代码只需要改一下时序参数即可。烟雾检测这块选的是MQ-2。它属于半导体气敏传感器对液化气、丙烷、氢气、烟雾等的灵敏度都不错。它的输出是一个模拟电压信号电压值随气体浓度升高而变大所以只需接一个ADC通道就能读。MQ-2模块板上通常自带一个比较器可以输出数字信号但数字输出的阈值是固定的我一般不建议用最好还是读模拟量这样可以通过软件自由设定报警阈值。需要注意的一点是MQ-2上电初期有个预热过程大概需要几十秒这段时间输出会不稳定代码里要做个上电延时处理。光照强度部分我选的不是BH1750这种数字光照传感器而是一颗几十毫伏误差范围内的光敏电阻。原因是这个系统只需要把光照分成“亮、正常、暗”几个等级不需要精确到多少勒克斯。光敏电阻加一个10K电阻构成分压电路输出直接进ADC成本几毛钱逻辑简单可靠。如果你想做自动窗帘或者根据光照自动调灯光亮度那可以考虑升级成BH1750I2C接口、直接输出数字光照值驱动代码也不难写到时直接替换这部分电路即可。2.4 为什么仿真环节必不可少Proteus在这里扮演什么角色很多刚接触嵌入式开发的同学会问一个问题我手里都有板子了为什么还要做仿真我的回答是仿真不是用来替代实物验证的而是用来降低调试成本和快速验证逻辑的。尤其在做原理图设计阶段不可能每改一条线就打一次板或者飞线一次先在Proteus里把电路搭好、把程序烧进去跑一遍验证逻辑没问题了再去做实物能省掉一大半返工的痛苦。这套系统我提供了完整的Proteus仿真工程。仿真里用了STM32F103C8T6模型搭配了虚拟的DHT11、烟雾传感器、OLED屏和按键LED。Proteus的DHT11模型做得还算靠谱但跟真实芯片的时序要求还是有一点差别这会在后面的排障章节里展开说。仿真主要验证的是主程序的控制逻辑比如阈值判断、报警响应、PWM输出这些至于ADC采样的绝对精度、传感器的灵敏度曲线那还是得靠实物去校准。3. 硬件设计拆解原理图的核心模块与电路细节3.1 STM32最小系统原理图能跑起来的底线是什么先看主控部分。我见过不少新手画STM32原理图一上来就复制网上一大堆电路什么复位电路、晶振电路、BOOT电路、滤波电容全画上结果连最基本的供电都搞错了。其实STM32F103C8T6的最小系统核心就四个部分。一是电源3.3V供电每个VDD引脚旁边都要放一个100nF去耦电容尽可能靠近引脚放置这是EMC和稳定性的基本保障。二是外部晶振C8T6的HSE推荐用8MHz晶振两个20pF负载电容这里有个简单的经验公式CL (C1 × C2) / (C1 C2) Cstray其中Cstray是PCB寄生电容通常取3~5pF。以20pF的两个电容为例串联后10pF加上4pF左右的寄生生电容约等于14pF对8MHz这种常用频率来说是合适的。实际焊接时用15pF~22pF都问题不大系统能正常起振。三是复位电路NRST引脚接一个10K上拉电阻和100nF电容到地这是STM32官方推荐的经典配置实现上电自动复位。四是BOOT0引脚直接接10K下拉电阻到地让芯片从Flash启动。其他什么JTAG、SWD注意一下SWDIO和SWCLK两个引脚默认就是调试功能不需要额外电路。如果你用的是成品最小系统板这些都已经在板上做好了连一个USB转串口模块就能下载程序不用自己画。但我还是建议自己亲手画一遍原理图这个过程能帮你想清楚芯片每个引脚是怎么分配、怎么工作的。3.2 传感器与执行器电路信号链路怎么接最稳传感器的信号链路是整个原理图里最值得讲的部分。DHT11的数据引脚需要接一个4.7K上拉电阻到3.3V因为DHT11是开漏输出没有上拉电阻就读不到高电平。数据线接到STM32的一个普通GPIO我选的是PA9这个引脚没有特殊复用冲突方便后续调整。DHT11的VCC可以直接用3.3V供电但要注意如果你的DHT11是那种老版本模块有些对3.3V供电比较挑剔建议用5V供电再加一个电平转换电路新版本的基本都能直接在3.3V下工作。MQ-2模块用的是5V供电因为它的加热丝需要5V才能保证传感器工作在正常温度。而它的模拟输出AO引脚输出电压范围是0~5VSTM32的ADC输入范围最高只能到3.3V直接接上去会烧引脚。所以中间必须加一个分压电阻网络最简单可靠的做法是用两个10K电阻串联分压把MQ-2的AO输出先降到一半再进STM32的ADC引脚。我实际用的引脚是PA0对应ADC1的通道0。这里想提醒一下MQ-2模块在刚上电的几十秒内输出会飘得比较厉害原理图上可以在电源端加一个100uF的电解电容做滤波能稍微缓解这个问题。光敏电阻电路更简单一个光敏电阻和一个10K固定电阻串联中间抽头接ADC输入引脚我用的是PA1。光敏电阻的阻值随光照变化光照强时阻值变小中间节点的电压就变高ADC读数随之增大光照弱时反过来。这个关系刚好和“光照越强读数越大”的直觉一致后面写代码时不用反向换算省了不少麻烦。执行器部分蜂鸣器我选了有源蜂鸣器用一个S8050三极管驱动GPIO通过1K电阻接到三极管基极。这里要注意有源蜂鸣器内部带振荡源只要给高电平就会发声无源蜂鸣器需要用PWM波形驱动逻辑上更复杂一些。继电器控制排风扇的电路用的是5V继电器加一个1N4007续流二极管这个二极管必不可少因为继电器线圈断电瞬间会产生反向电动势没有续流二极管很容易打坏三极管的。最后OLED显示屏的SCL和SDA分别接到PB6和PB7这是I2C1的外设引脚地址用默认的0x78。3.3 原理图绘制实战经验用什么工具、怎么画才能少走弯路原理图我用了嘉立创EDA来画因为是国产软件里面的元器件库非常全DHT11、MQ-2、STM32F103C8T6这些东西在库里面都能直接搜到省去了自己画封装的痛苦。画图时我习惯遵循几个原则电源网络用红色地网络用黑色模拟信号用蓝色数字信号用绿色这样一眼扫过去就能看出信号流走向。电容和电阻这类无源器件数值标注要清晰最好用“10uF/16V”这种格式生产时不容易混淆。另外还有一个新手容易忽略的细节原理图的电源符号命名。整个系统里同时存在3.3V和5V以及GND和AGND如果你在画图时不小心把3.3V和5V都命名为VCC最后PCB布线时就会非常麻烦。我的做法是用电源端口符号区分3V3、5V、GND各用各的符号模拟地AGND和数字地GND在原理图里用0欧电阻单点连接这样可以避免数字电路的高频噪声串进模拟信号的参考地影响ADC采样精度。画完原理图之后强烈建议做一次ERC电气规则检查。嘉立创EDA的ERC能检查出悬空引脚、短路、重复标号这些低级错误很多时候比你自己盯着屏幕看半天管用得多。我这次画完第一遍ERC就报了三个告警都是放了但是没接线的空器件修正完再做第二遍就全部通过了。4. 软件实现与核心代码解析4.1 工程搭建与初始化从CubeMX到Keil的关键配置代码这部分我选了HAL库原因是可读性好、适合教学演示而且CubeMX生成的分层架构非常清晰对新手友好。整个工程基于STM32CubeMX生成配置路径和参数如下SYS - DebugSerial Wire这样保留SWD下载调试功能RCC - HSECrystal/Ceramic Resonator让系统时钟跑在72MHzI2C1Standard Mode速度100KHzADC1开启IN0和IN1两个通道采样时间设为55.5 Cycles连续转换模式关闭用软件触发TIM2Channel 1配置为PWM Generation用于控制风扇转速GPIOPA9设为输出推挽接DHT11数据PB8、PB9输出分别控制蜂鸣器和LEDPA11和PA12设为输入上拉作为按键时钟树的配置是另一个容易出错的地方。F103的最高主频是72MHz默认情况下CubeMX的HCLK那里需要手动输入72并回车它会自动帮你计算出PLL的倍频和分频系数。总线时钟也要注意APB1最高只能到36MHz系统默认会自动分频这个不用特别操心但如果APB1的定时器时钟忘了乘2后面用TIM2做PWM时频率就会差一半排查起来会让你怀疑人生。所以每配置完一个外设最好生成代码后先编译一次看看有没有错误再继续往下加。4.2 DHT11驱动时序详解高低电平怎么读才能不翻车DHT11使用的是单总线协议一次完整的数据传输包含40个bit8位湿度整数、8位湿度小数、8位温度整数、8位温度小数、8位校验和。主机先拉低数据线至少18ms然后释放DHT11检测到这个起始信号后会回一个80us左右的低电平响应然后拉高80us准备发送数据。读数据的核心在于判断每一位的时长DHT11的每位数据都以50us低电平开始随后拉高如果高电平持续26~28us代表“0”持续70us左右代表“1”。所以代码的关键就落在两个地方一是精确延时二是超时保护。使用HAL库时我用了DWTData Watchpoint and Trace来做微秒级延时这是Cortex-M系列内核自带的一个外设比for循环空转准得多。具体做法是开启DWT的CYCCNT计数器通过读计数值差来精确控制延时。核心代码如下// 微秒延时函数基于内核时钟周期计数精确可靠 static void delay_us(uint32_t us) { uint32_t start DWT-CYCCNT; uint32_t ticks us * (SystemCoreClock / 1000000); while ((DWT-CYCCNT - start) ticks); }读取一位数据的函数长这样// 读取一位等待50us低电平结束然后测量高电平持续时间 uint8_t dht11_read_bit(void) { uint8_t bit 0; uint32_t timeout 0; // 等待低电平结束约50us while (HAL_GPIO_ReadPin(DHT11_GPIO_Port, DHT11_Pin) GPIO_PIN_RESET) { if (timeout 500) return 0; // 超时保护防止死循环 delay_us(1); } // 高电平期间延时40us超过40us仍是高电平说明是“1” delay_us(40); if (HAL_GPIO_ReadPin(DHT11_GPIO_Port, DHT11_Pin) GPIO_PIN_SET) { bit 1; } // 等待剩余的高电平时间结束 timeout 0; while (HAL_GPIO_ReadPin(DHT11_GPIO_Port, DHT11_Pin) GPIO_PIN_SET) { if (timeout 200) break; delay_us(1); } return bit; }这里有个很重要的细节我不是等完整的70us或26~28us再判断而是固定延时40us后再读电平。因为不管是“0”还是“1”高电平时间都至少是26us40us正好落在“0”结束和“1”持续期之间此时读到高电平就是“1”读到低电平就是“0”。这个技巧比精确测量脉冲宽度要简单得多而且稳定性也够用。另外所有等待循环都要加超时保护否则DHT11没接好时程序会卡死在等待电平跳变里整个系统看起来就像死机了一样这个坑我刚开始写驱动的时候踩过不止一次。读完整帧数据后把5个字节的校验和做一下校验防止读到乱码。校验公式是humidity_h humidity_l temp_h temp_l checksum。如果校验不等这一帧数据直接丢弃下次再读。4.3 ADC多通道采集与数据平滑让读数不再跳来跳去MQ-2和光敏电阻分别占用ADC1的IN0和IN1通道。同一时间ADC只能转换一个通道所以多通道采集需要逐个配置。我用的是最简方式调用HAL_ADC_ConfigChannel配置一次然后启动转换读取一次再切到下一个通道配置如此循环。配置代码如下uint16_t adc_read_channel(uint32_t channel) { ADC_ChannelConfTypeDef sConfig {0}; sConfig.Channel channel; sConfig.Rank ADC_REGULAR_RANK_1; sConfig.SamplingTime ADC_SAMPLETIME_55CYCLES_5; HAL_ADC_ConfigChannel(hadc1, sConfig); HAL_ADC_Start(hadc1); HAL_ADC_PollForConversion(hadc1, 10); uint16_t value HAL_ADC_GetValue(hadc1); HAL_ADC_Stop(hadc1); return value; }采样时间我选了55.5个周期在这个项目里完全够用。如果你要采的信号噪声比较大可以适当调高采样时间比如239.5个周期这样ADC内部采样电容充电更充分读数稳定性会有改善。但代价是转换速度变慢对于烟雾检测这种变化速率不快的信号来说可以接受。ADC原始读数是0~4095MQ-2的模拟输出经过两个10K电阻分压一半后再按比例换算回真实电压值电压 原始值 × 3.3 / 4095。换算后再乘以2才是MQ-2模块的AO口实际输出电压。为了方便判断我最后把电压值按百分比归一化烟雾浓度百分比 MQ2电压值 / 3.3 × 100%超过某个阈值就触发报警。单次ADC读数很容易受到电源波动、电磁干扰的影响读数跳来跳去屏幕上看着也非常难受。我加了一个简单的滑动平均滤波维护一个长度为10的环形缓冲区每次采到新值就入队求平均值作为有效读数。代码很简单// 滑动平均滤波数组长度10返回平均值 uint16_t adc_filter(uint16_t new_value, uint16_t *buf, uint8_t len) { static uint8_t index 0; uint32_t sum 0; buf[index] new_value; index (index 1) % len; for (uint8_t i 0; i len; i) sum buf[i]; return (uint16_t)(sum / len); }这种做法在嵌入式里非常常用比单纯读一次数据稳得多。实测下来未滤波的MQ-2读数波动范围有±50左右滤波后能压到±10之内屏幕上的数字看起来舒服多了。4.4 I2C OLED屏驱动与界面设计信息怎么排最直观OLED屏用I2C驱动SSD1306控制芯片128×64分辨率。驱动代码可以直接移植网上的SSD1306库也可以用我本次开源项目里精简过的版本。这个库的核心是构造显存数组然后一次性往OLED的GDDRAM里刷。刷屏频率建议控制在30Hz以内太快反而会感觉闪烁。在界面设计上我分了两个界面通过按键切换。第一个界面显示温湿度和烟雾浓度字号选大号方便远距离看清第二个界面显示光照强度等级和风扇转速以及报警状态。显示逻辑是在main函数的大循环里每次更新传感器数据后刷新对应区域。SSD1306库的显示函数是整屏刷新但你可以在刷新前先用清屏函数把显存清零再往指定坐标写入新数据最后统一刷新这样局部更新也很方便。光照等级判断逻辑我写了四档ADC读数大于3000为“光照充足”2000~3000为“光照正常”1000~2000为“光照偏暗”小于1000为“光照不足”。OLED上还可以顺手显示一个简易的进度条用填充矩形函数画一个代表光照强弱的条直观程度比纯数字强多了。4.5 报警与自动控制逻辑阈值怎么设、状态机怎么搭报警控制这块需要一个清晰的状态机。系统状态分成三类NORMAL正常、WARN警告、ALARM报警。进入WARN状态的条件是温度超过30度或湿度超过70%RH或者烟雾浓度百分比超过15%。进入ALARM状态的条件是烟雾浓度超过30%或者温度超过35度说明情况已经比较严重了。不同状态下执行器的动作不同我用一个switch-case来管理typedef enum { SYS_NORMAL 0, SYS_WARN, SYS_ALARM } sys_state_t; void system_control(sys_state_t state) { switch (state) { case SYS_NORMAL: HAL_GPIO_WritePin(BUZZER_Port, BUZZER_Pin, GPIO_PIN_RESET); HAL_GPIO_WritePin(LED_Port, LED_Pin, GPIO_PIN_RESET); __HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_1, 0); // 风扇停止 break; case SYS_WARN: HAL_GPIO_WritePin(LED_Port, LED_Pin, GPIO_PIN_SET); // LED亮 __HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_1, 500); // 风扇半速 break; case SYS_ALARM: HAL_GPIO_WritePin(BUZZER_Port, BUZZER_Pin, GPIO_PIN_SET); // 蜂鸣器响 HAL_GPIO_WritePin(LED_Port, LED_Pin, GPIO_PIN_SET); __HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_1, 1000); // 风扇全速 break; } }PWM的占空比通过修改TIM_CHANNEL_1的比较寄存器实现0表示不转500是半速1000是全速。这里PWM的周期我配置成了20kHz频率超过人耳可听范围风扇不会发出尖锐的啸叫声。有些项目把PWM频率设成几百赫兹风扇转起来噪音非常大就是因为这个原因。关于阈值这里给的是我调试时用的默认值实际使用中要根据你家的环境来调整。比如南方夏天室内温度动不动35度以上你的报警阈值就不能设到35度否则一天到晚响个不停最后只能拆电池。建议温度阈值设为首段40度作为硬报警线35度只是软提示这样就灵活很多。关于这点我后来在代码里加了一个配置文件头把阈值统一定义成宏方便用户自行修改。4.6 按键交互短按切换界面长按实现报警消音两个按键一个负责界面切换一个负责报警消音交互逻辑本来应该是很简单的但实际调下来发现里面藏着不少细节。按键接在PA11和PA12上默认上拉输入按下为低电平。最基础的处理思路是检测到引脚变低后延时20ms再读取一次如果还是低电平就确认按键真正被按下这就是消抖。但很快我遇到了一个问题长按报警消音键的时候短按的逻辑也会被触发。所以我干脆用一个状态机来处理按键区分短按按下时间小于1秒和长按按下时间大于3秒。实现方式是在GPIO外部中断的下降沿里启动一个定时器计时上升沿里停止计时并判断持续时间落在哪个区间就走哪条逻辑。// 按键消抖与长短按判断伪代码示意 void EXTI15_10_IRQHandler(void) { if (__HAL_GPIO_EXTI_GET_IT(KEY_LONG_Pin) ! RESET) { // 关中断启动10ms定时器做软件消抖 HAL_NVIC_DisableIRQ(EXTI15_10_IRQn); HAL_TIM_Base_Start_IT(htim6); __HAL_GPIO_EXTI_CLEAR_IT(KEY_LONG_Pin); } }这段代码要想真正稳定一定得加上标志位防止在中断里同时处理两个按键事件导致状态互相干扰。我的做法是全局定义一个volatile修饰的按键事件标志变量主循环检测到这个标志后再执行具体操作。中断服务函数只负责置标志位不做耗时的操作这是嵌入式开发里最基本也最重要的软实时处理原则。5. Proteus仿真搭建与联调不买硬件也能把逻辑跑通5.1 仿真工程的关键点元器件哪里找、DHT11怎么连Proteus仿真这部分我用的是Proteus 8.9版本。新建工程时芯片型号直接搜STM32F103C8T6Proteus的元件库里有这个型号的仿真模型而且默认带有SWD调试接口和虚拟终端。值得留意的一点是Proteus里的STM32F103C8T6默认引脚布局和真实芯片的LQFP48封装是一致的画原理图时只需要按逻辑连接不需要关心实际引脚位置。DHT11在Proteus元件库里搜索“DHT11”可以直接找到。但Proteus里的DHT11模型有一个特性它要求数据线上必须有上拉电阻否则仿真时序会异常。这个和真实电路是一致的你在原理图里加一个4.7K上拉电阻就可以。OLED屏在Proteus里搜“OLED”能找到SSD1306的模型连接I2C引脚即可。MQ-2用“MQ-2”或“MQ2”搜索Proteus模型提供一个可调的滑动变阻器来模拟气敏电阻的变化拖动它就能改变模拟输出电压。光敏电阻则用一个电位器串联电阻网络替代拖动电位器相当于改变光照强度。连接完成后双击STM32芯片加载HEX文件。C8T6的HEX文件在Keil工程目录的Debug或List文件夹里可以找到编译通过后会生成。加载HEX后点击运行就能看到OLED屏和数据终端输出的效果。需要注意的是Proteus仿真上DHT11的时序和真实芯片有一定差异如果仿真时DHT11读到的数据固定是0或者温度保持不变多半是延时函数不够准确导致的。我实测发现DWT延时在Proteus里同样有效但如果用的是别的延时方法记得先确认微秒延时的实际执行时间是否和预期一致。5.2 仿真过程中踩过的3个坑给还没开始的人提前避雷第一个坑是PWM仿真时风扇转速看起来没变化。这个问题排查了很久最后发现是TIM2的PWM频率设置太高了Proteus的虚拟仪表刷新率跟不上20kHz导致波形看起来像是恒定输出。解决方法是把PWM频率降到了1kHz左右仿真现象就明显了。实物上20kHz没有问题但仿真时为了观察方便可以临时改低频率不影响逻辑验证。第二个坑是ADC通道读取顺序问题。如果你在代码里先把通道0配置成烟雾读取然后马上配置通道1读取光照两个值读出来都是通道0的数值。原因在于ADC需要一定的稳定时间每次切换通道后建议加一个短暂的延时或者多读一次丢弃第一拍。我最终的处理方式是每次切换通道后读两次第二次的值才有效。第三个坑是OLED在仿真里显示全白或全黑。SSD1306的初始化顺序比较讲究如果初始化序列里某条命令没发对OLED的显示就异常了。我查了官方数据手册把初始化序列逐条核对了一遍发现问题出在一个延时太短OLED的控制器还没准备好就收到了下一条命令。把初始化函数里的延时加长到10毫秒级别后问题就消失了。5.3 仿真到实物的差距哪些能信、哪些不能信仿真能帮你验证的是逻辑层面的正确性传感器数据读不读得出来、阈值判断正不正确、状态切换是否按预期走、显示内容有没有乱码。这些在Proteus里跑通了拿到实物上有80%的概率能直接工作。但有两件事是仿真代替不了的一是ADC的采样精度Proteus里电位器的输出电压是理想电压没有噪声、没有纹波而真实电路里的电源纹波、线间串扰都会影响ADC读数所以实物上必须加滤波二是传感器的响应速度MQ-2在仿真里是通过滑动变阻器瞬变模拟的而真实气体的扩散和传感器的响应都需要时间写业务逻辑时需要考虑这个滞后性。我之前做过一个类似的环境监测项目仿真阶段一切正常一上实物温度就乱跳后来排查发现是DHT11的数据线在杜邦线上干扰太严重。解决办法是让数据线尽量短而且是直接从MCU引脚拉到传感器的不跟着电机线走干扰问题立刻缓解。这次开源项目里的原理图已经考虑到了这个走线问题PCB打样的时候务必保持这个布局方式。6. 常见问题与排查技巧实录6.1 程序烧不进去怎么办ST-Link连接失败的排查路径这是整个项目里遇到最多的一个问题而且新手遇到基本都会慌。报错信息五花八门什么“Error: Flash Download failed - Cortex-M3”、“No STM32 Target found! If your product embeds debug authentication”核心都指向芯片没连上调试器。排查路径其实很固定按顺序来第一步检查ST-Link是否被电脑正确识别设备管理器里能看到ST-Link Debug这个设备。第二步检查接线SWDIO、SWCLK、GND三条线是最低要求三条都接到芯片对应引脚上不能接反。第三步确认目标板供电如果板子没有独立电源ST-Link的3.3V输出也可以给板子供一部分电但前提是板子上没有其他耗电大的模块。第四步检查Keil的Debug设置里选择的调试器是不是ST-Link如果不是下载时就会报错。第五步如果上面都对了还是连不上按住板子的复位键点击下载松开的瞬间让Keil抓住芯片这个方法能救回来不少锁死的芯片。还有一类特殊情况是芯片进入了低功耗模式或SWD引脚被复用成普通GPIO了也会导致连接失败。这种情况下需要把BOOT0拉高强制从系统存储器启动然后再尝试连接下载。这件事在网上被大家称作“芯片救砖”BOOT0跳线帽一拨问题迎刃而解。6.2 DHT11读出来永远是0或固定的值从哪几方面找原因如果你遇到DHT11的数据一直显示温度0、湿度0或者数值完全不变大概率是三种原因。第一种时序误差太大微秒延时函数不准导致读位判断全部超时。可以先用逻辑分析仪看一下数据线上的实际波形对比DHT11数据手册上的时序图一眼就能看出哪里不对。没有逻辑分析仪的话用固定的延时函数反复测试也可以用示波器看电平。第二种引脚配置错了。在CubeMX里配置GPIO时如果PA9被设成了复用功能或者模拟输入DHT11通信就会失败。它必须设置为输出推挽模式速度倒是可以保持Low档。第三种供电电压不稳定。DHT11对供电很敏感如果供电电压低于3V或噪音太大它的数据输出就可能异常。给DHT11加一个4.7uF的钽电容在电源引脚旁边很多时候就能解决问题。如果读出来是固定的0xFF那多半是上拉电阻没焊好或者数据线断路DHT11根本没把数据线拉低GPIO一直读到高电平。6.3 OLED屏幕不亮但有背光I2C地址和接线顺序的双重检查OLED模块最常遇到的坑是I2C地址不对。绝大多数0.96寸OLED的默认地址是0x787位地址0x3C左移一位但也有的模块是0x7A。如果你的代码固定用0x78而屏幕本身的地址是0x7A初始化函数会一直返回失败屏幕自然不亮。检查方法很直接写一个I2C扫描程序把总线上所有设备的地址打印出来一目了然。还有一个低级但高发的问题是接线顺序。OLED模块的引脚顺序在不同厂家之间并不统一有的模块是GND、VCC、SCL、SDA有的模块是GND、VCC、SDA、SCL不仔细看丝印直接插杜邦线非常容易接反。别问我怎么知道的烧掉一块OLED之后我就再也没跳过这一步了。真的每次接线前先看丝印确认SCL和SDA的位置再插线。6.4 报警器无故乱响阈值设置与传感器漂移的博弈乱报警大部分是阈值设置不合理。MQ-2在刚上电预热那段时间输出值会很高容易误报所以程序里做了上电后60秒内禁止烟雾报警的初始化逻辑。另外MQ-2的零点会随着环境温湿度变化缓慢漂移建议在系统里加一个自动校准逻辑开机前60秒采30次烟雾值取平均作为零点基准后续实时值和零点基准做差值再判断报警。这个方案在空气净化器的设计里很常见套在这里效果也立竿见影。如果蜂鸣器还时不时响一下也可能是电源电压跌落导致传感器输出波动。这时候就可以检查一下给MQ-2供电的那路5V是不是和电机共用了同一个电源如果是的话建议把电机驱动单独一路供电或者至少把电源线加粗一点电压跌落的问题会明显改善。7. 项目扩展思路与后续可做的事做完了基础版的环境监测功能之后这套系统的硬件和能力其实还有很多富余。我在这里列几个我自己尝试过、也觉得比较容易扩展的方向给大家做个参考。第一个方向是联网上云。STM32F103C8T6的两个串口一个用来调试另一个闲着正好可以接ESP8266或ESP-01模块通过AT指令把温湿度、烟雾浓度上传到云平台做成手机端实时查看的远程监控系统。需要关注的点是TCP长连接的维持、心跳包的发送频率、断线重连的机制。如果你用的单片机是STM32F103系列串口驱动和AT指令解析这两块代码网上有大量现成方案移植难度不大。第二个方向是增加空气质量检测。MQ-2只能测烟雾和可燃气体测不了甲醛、PM2.5这类物质。可以挂一个SGP30或PMS5003模块虽然成本会上来不少但这套系统的代码架构不用大改只需在传感器结构体里增加一个新成员再写一个对应的读取驱动就能把空气质量参数也纳入监测范围。SGP30走I2C读取逻辑比DHT11还简单。第三个方向是显示端的升级。OLED屏虽好但只适合近看如果你想把屏幕放在客厅可以考虑换成2.4寸的TFT彩屏或者干脆把屏幕去掉只保留数据和报警功能所有显示走手机端。我自己试过把一个8266接到这个系统上把数据通过MQTT推到手机上的APP用着比在屏幕上看舒服得多。第四个方向是控制端的扩展。目前只控制一个排风扇其实还可以加一路继电器控制加湿器、一路控制窗帘电机。控制逻辑在system_control函数里扩展就行每个执行器对应一个分支状态机框架依然适用。如果你手头的主控不是STM32F103C8T6是GD32F103、APM32F103或者CH32F103这类国产兼容芯片这套代码基本可以无缝移植顶多改一下启动文件和链接脚本HAL库层面完全兼容。这点我实际验证过程序跑起来没问题时序和ADC读数都和原版STM32差不太多大家如果手头有这些芯片也完全可以拿来跑这套方案。从项目管理的角度这个项目从立项到完成我大概花了三个完整的周末。第一个周末画原理图和建仿真工程第二个周末写驱动和联调第三个周末打样实物并校准阈值。如果你之前没有系统做过一个完整项目我强烈建议你也按这个节奏走一遍。别急着打板也别急着买传感器先在仿真里把逻辑跑通再动手做实物你会发现后面所有事情都顺畅很多。最后再分享一个我做这类项目时一直沿用的小技巧所有传感器的原始读数、滤波后的数据、控制状态机的当前状态都通过串口输出到调试终端。前期联调阶段OLED屏幕显示的是给人看的最终结果串口输出的是给调试者的过程信息两者配合起来排查问题的速度能快好几倍。我见过很多新手一上来就闷头调OLED显示数据不对也不知道是传感器的问题还是逻辑的问题白白浪费大量时间。这个习惯你养成之后以后再写任何单片机项目都会感谢自己当初的这个选择。