
简介基于STM32的室内环境监测系统课题设计资源包面向嵌入式初学者、高校电子类专业学生及STM32开发者。项目围绕STM32微控制器展开涵盖环境参数采集、处理与显示适用于课程设计、毕业设计或工程实践入门。压缩包约64.13MB核心内容包含C/C源码与电路图设计原理图涉及传感器读取、数据通信、显示驱动及电源管理等模块可帮助理解嵌入式系统从硬件连接到软件实现的完整流程。资源支持温湿度、PM2.5、CO2等常见室内环境参数的监测并可搭配OLED/LCD显示及Wi-Fi/蓝牙无线传输方案目前已有2366人学习下载极具参考价值。通过研读源码与原理图读者可掌握Keil等IDE的编译下载方法学会各类型传感器的接入与调试并了解数据上报至云端或手机端的实现思路是提升STM32编程能力与项目实战能力的实用资料。1. 拿到室内环境监测课题后我先把硬件框架画清楚了说实话看到基于STM32的室内环境监测系统这类题目时很多人第一反应是直接买模块、找例程、然后对着板子一顿接线——但这么做的结果往往是七天过后代码堆成了一团浆糊编译通过却跑不出合理数据。我自己的习惯是先别碰代码把需求和硬件框架在白纸上画出来哪怕只有五分钟。所谓室内环境监测核心逻辑无非就是采集–处理–呈现/告警这条链路。采集端解决的是环境里有什么可测的处理端是STM32这颗主控的工作呈现和告警则是让结果“看得见、听得着”。围绕这条链路我圈定了几个必须解决的问题需要感知哪些环境参数温度、湿度、光照强度以及可燃气体/烟雾浓度这四个是室内环境监测里最典型的维度做课题和做小产品都够用主控怎么选继续沿用手头最常见的STM32F103C8T6性能足够资料最多遇到问题最好搜数据怎么展示我选择了0.96寸I2C接口的OLED屏功耗低、接线少调试期还能直接打印状态信息比1602液晶省了一堆GPIO异常怎么提示一个无源蜂鸣器加三色LED阈值超标时软件触发告警扩展口留不留留一组串口方便连ESP8266之类的WiFi模块方便后期把数据送上云平台。这轮思考最大的价值不在于“选了什么”而在于“砍掉了什么”。比如有人喜欢加上风速传感器、PM2.5激光粉尘传感器但这些模块要么价格偏高要么接口复杂对于入门课题来说是负担而非亮点。一套系统的完成度从来不取决于传感器数量而取决于采集准确性、逻辑清晰性和运行稳定性——这句话在后来的调试中反复被验证。2. 传感器选型与接口设计每根线都有它存在的理由2.1 温湿度传感器选型思路温湿度测量是整套系统的地基我纠结过DHT11、DHT22和SHT30三款。最终选了DHT11理由很实际单总线协议、三线制接线VCC、GND、DATA、资料多得离谱、价格几块钱一片。虽然精度只有±2°C和±5%RH但对于环境监测系统这种应用场景已经足够。接线时我把DATA脚接到了PB0外部接了一个4.7kΩ上拉电阻。别小看这个上拉电阻DHT11的单总线是开漏输出结构不上拉的话时序根本读不到正确电平。网上很多人抱怨DHT11数据全是0xFF八成就是漏了这颗电阻或者杜邦线太长导致信号畸变。2.2 光照强度与气体浓度的采集方案光照部分我采用了最简单的方案光敏电阻配合ADC采集。一个10kΩ电阻和光敏电阻串联分压中间抽头接到STM32的PA1。这套电路不需要额外芯片成本极低缺点是需要自己标定“光照值”的物理意义。我在代码里直接用ADC原始值分档映射成“暗、较暗、适中、较亮、明亮”五个等级而不是硬算成勒克斯Lux这个做法实际体验下来非常舒服既避开了标定设备的缺失又满足了监测需求。气体浓度检测则选用了MQ-2烟雾传感器模块。这类传感器内部是二氧化锡半导体加热元件在不同浓度气体下电导率会变化模块板上已经集成了比较器可以输出数字量DO和模拟量AO。我没有用简单的DO引脚去判断“有没有气”而是把AO引脚接到PA2做连续ADC采样配合阈值判断这样既能看到浓度趋势又能在真正超标时触发告警。说到这里必须提醒一句MQ系列传感器上电后需要预热几十秒前几秒数据会虚高代码里必须加一个初始化延时否则一开机就误报。2.3 主控引脚分配一览整个系统的引脚分配如下这个表贴在工程文件夹里也方便后面写代码时对照。功能模块接口类型STM32引脚备注温湿度DHT11单总线PB0需4.7kΩ上拉光敏传感器ADCPA1分压电路MQ-2烟雾传感器ADCPA2模块AO输出OLED显示屏I2CPB6/PB7标准I2C无源蜂鸣器GPIOPB1定时器PWM驱动三色LEDGPIOPB12/PB13/PB14红绿蓝预留串口UARTPA9/PA10WiFi扩展用这套分配里面其实藏着一个经验ADC引脚和数字引脚尽量不要混用在同一组里做复杂的复用尤其是PB0这种单总线引脚它被占掉之后附近I2C的引脚选择就很容易被挤到PB6/PB7上。STM32F103的I2C外设虽然能用硬件I2C但网上无数人踩过I2C死锁的坑——后面我会专门讲这个问题这里先按能用软件模拟就用软件模拟的原则来处理。3. 环境搭建和工程配置别一上来就写main.c很多人打开Keil之后第一件事就是新建main.c然后从网上复制一个模板工程。这个做法不是不行但等到后面调试时你会发现代码组织结构直接决定了排查问题的效率。我用的是标准外设库Standard Peripheral Library搭配Keil MDK 5的环境工程目录按下面方式划分APP存放应用层代码包括main.c、DHT11驱动、ADC采集、数据处理逻辑BSP开发板底层驱动包括GPIO初始化、定时器、延时函数OLED专门放屏幕显示相关的代码SYSTEM存放系统级的串口printf重映射、时钟配置、中断服务函数。工程配置里有几个容易踩的坑我必须专门交代。第一C/C选项卡里的Include Paths必须老老实实把每一个包含头文件的文件夹都加进去漏一个就报fatal error: XXX.h: No such file or directory。第二在Magic Wand选项卡里记得勾选Use MicroLIB否则printf重定向到串口时会因为半主机模式Semihosting卡死在BKPT指令上这个坑在调试串口输出时几乎人人都会撞上。第三F103C8T6的Flash只有64KB我默认没有开启Cross-Module Optimization避免编译器优化把时序敏感的延时函数改动掉。工程里延时函数我用的是DWTData Watchpoint and Trace模块实现微秒级延时。相比软件循环延时DWT不依赖系统主频的精确计算也不会被中断影响对DHT11这种要求微秒级时序精度的协议来说格外重要。简单来说DWT是Cortex-M3内核自带的计数器可以把它配置为自由运行计数器读取两次计数值再除以主频就能得到精确的延时时间。初始化代码如下void DWT_Delay_Init(void) { CoreDebug-DEMCR | CoreDebug_DEMCR_TRCENA_Msk; DWT-CYCCNT 0; DWT-CTRL | DWT_CTRL_CYCCNTENA_Msk; } void delay_us(uint32_t us) { uint32_t start DWT-CYCCNT; uint32_t ticks us * (SystemCoreClock / 1000000); while ((DWT-CYCCNT - start) ticks); }这段代码在很多项目里可以直接复用尤其是涉及DS18B20、DHT11这类单总线传感器时能省掉无数延时不准导致时序错乱的排查时间。4. 底层驱动实现DHT11的时序坑和ADC多通道采集4.1 DHT11单总线时序我看透了这几个细节DHT11的驱动属于典型的协议不难时序要命。它的通信流程是主机先把总线拉低至少18ms然后释放并延时20-40us之后DHT11响应先拉低80us再拉高80us然后开始传输40bit数据。每一位数据都以50us的低电平开始高电平持续时间的长短决定该位是0还是1高电平26-28us表示0高电平70us左右表示1。听起来不复杂但实际写代码时很多人遇到的问题是读出来的数据永远是0xFF或者只有第一次正确。我自己排查下来90%的原因在于延时不准。解决方法是上面提到的DWT微秒延时把每一位的高低电平宽度完整读出来判断时机放在“高电平结束时”进行采样。核心读位函数如下uint8_t DHT11_ReadBit(void) { while (!DHT11_DQ_IN); // 等待低电平结束 delay_us(40); // 延时到高电平中段 if (DHT11_DQ_IN) { while (DHT11_DQ_IN); // 等待高电平结束 return 1; } else { return 0; } }这个40us的采样点位置是关键。如果放在30us以内可能把0误判成1如果放在60us以上又可能读到下一位的低电平。实测下来40us是最稳妥的折中值。另外一个很容易被忽略的点是DHT11上电后第一次读取往往会失败。原因是传感器刚上电时内部还在自检这时候主机发起读取请求总线应答不稳定。代码里我做了三次重试机制每次重试间隔至少1秒。这个设计在长时间运行中非常管用——很多跑一会儿就不更新数据的问题本质上是传感器偶尔应答异常而代码没有重试逻辑。4.2 ADC多通道采集的配置与滤波处理STM32F103的ADC有多个通道但只有一个转换结果寄存器。要读取多路模拟量光照、烟雾我用的是规则通道组的扫描模式Scan Mode。配置ADC1的通道1和通道2开启连续转换然后在DMA中断里周期性把转换结果搬进内存数组。这样CPU不用反复查询转换完成标志效率高很多。需要注意的一点是STM32的ADC在多个通道切换时需要设置足够的采样时间。芯片内部模拟开关在切换通道后需要一段时间稳定采样时间过短会导致通道串扰——表现为光照通道的数据里混入了烟雾通道的波动。我把采样时间统一设置为ADC_SampleTime_55Cycles5实测效果稳定。ADC原始数据抖动是另一个绕不开的问题。光敏电阻的模拟信号并不是稳定的直流加上环境光的50Hz频闪ADC读数会来回跳。我用的是滑动平均滤波维护一个长度为10的环形缓冲区每次取平均值。10次采样对实时性影响很小又能有效平滑抖动。注意滑动平均和每次读10次再平均是两个概念滑动平均是每次新数据进来后减去最旧的数据、加上最新数据、除以窗口长度整个计算是O(1)复杂度而后者每次都要重新累加在中断频繁的场合会浪费CPU时间。uint16_t ADC_SlidingAverage(uint16_t new_value, uint16_t *buffer, uint8_t *index, uint8_t len) { static uint32_t sum 0; sum - buffer[*index]; buffer[*index] new_value; sum new_value; *index (*index 1) % len; return sum / len; }5. 应用层逻辑设计时间片轮询替代裸奔延时硬件驱动写完之后最大的难点不是功能实现而是避免功能互相打架。很多入门项目喜欢在main函数里用大循环加delay的方式读一遍DHT11延时2秒读一遍ADC延时100ms刷新一遍OLED延时50ms。这套逻辑在只有单一功能时没问题但一旦同时要跑温湿度采集、烟雾采集、屏幕刷新、按键扫描、串口打印就会遇到OLED刷新慢半拍蜂鸣器响的过程中数据采集卡顿等一系列问题。我采用的方法是时间片轮询调度。设定一个1ms的时基中断用TIM2在主循环里维护多个任务标志位每个任务根据自己的周期决定是否执行。示意代码如下volatile uint32_t systick_ms 0; void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_Update)) { systick_ms; TIM_ClearITPendingBit(TIM2, TIM_IT_Update); } } uint8_t Task_TimeUp(uint32_t *last_run, uint32_t interval) { if (systick_ms - *last_run interval) { *last_run systick_ms; return 1; } return 0; }然后在main循环里while (1) { if (Task_TimeUp(t_dht, 2000)) { DHT11_Read(temp, humi); } if (Task_TimeUp(t_adc, 100)) { light ADC_GetValue(); smoke ADC_GetValue(); } if (Task_TimeUp(t_display, 500)) { OLED_ShowData(temp, humi, light_level, smoke_level); } if (Task_TimeUp(t_alert, 200)) { Alert_Check(temp, humi, smoke_level); } }这套结构的优势非常明确每个任务都在自己的时间片内运行哪怕某个任务偶尔执行超时比如OLED刷新因为I2C等待多花了几ms也不会阻塞其他任务的调度。整个系统的实时性和稳定性比单纯延时高一个档次。告警逻辑我设了三级阈值温度超过30°C或低于10°C、湿度超过70%RH、烟雾浓度超过阈值时蜂鸣器鸣响红色LED闪烁参数在边缘值时黄色LED常亮正常时绿色LED常亮。这里推荐蜂鸣器用PWM驱动而非GPIO高低电平驱动——无源蜂鸣器需要一定频率的方波才能发声直接给高电平只会让线圈发热而不是响。在TIM3的PWM输出通道里配置一个2.7kHz左右的频率即可。6. OLED显示与中文字库处理用最省事的方式做界面0.96寸OLED屏的驱动芯片通常是SSD1306I2C接口只要四根线代码库在各大社区都有现成的。但很多人把屏幕驱动代码复制过来之后发现显示中文会变成乱码——这是因为SSD1306默认只带ASCII字库中文字库需要自己取模。取模工具有很多我在Windows上用PCtoLCD2002设置阴码、逐列式、逆向生成16x16的点阵数组。注意取模时还要确认自定义格式里的字节排列顺序不同的取模方式逐行式/逐列式和扫描方向组合会导致显示镜像或错位这个没有标准答案只能边设边试。我们把字库放在一个专门的font.h文件里每个中文字符对应一个16x16的数组显示函数按行列循环输出。显示界面我分了三个页通过板载按键切换第1页当前温度、湿度显示最大的两个数字字体第2页光照等级文字烟雾浓度条用一列柱状图画出浓度百分比第3页系统状态包括运行时间、传感器状态、告警状态。这里有个小技巧OLED的像素点比较“娇贵”长期显示同一画面容易烧屏。我在页面上加了5秒无操作自动切换屏幕保护的功能——屏幕全黑只保留一个“运行中”的小点既省电又避免残影。7. 整机装配与实测数据稳定运行72小时的调优记录硬件和软件都完成后我把它装进了一个透明的亚克力外壳里传感器探出壳体通风孔OLED面板固定在正面。这里有两个装配层面的细节值得说第一DHT11传感器尽量远离MCU和蜂鸣器这些发热源否则测出来的温度会虚高两度以上第二MQ-2传感器工作时加热丝会产生热量且耗电接近150mA必须用独立稳压模块供电不能和主控共用LDO的同一路输出否则压降会导致系统复位。然后是连续72小时的实测调优。第一天发现的问题MQ-2的浓度读数在开机前半小时内缓慢漂移这是加热丝热稳定过程的正常现象属于半导体气体传感器的固有特性我不再纠结而是在代码里做了“开机后前60秒数据不参与告警判断”的处理。第二天发现夜间数据里光照值偶尔跳变排查多个小时后锁定是手机充电器的开关电源导致的50Hz干扰在ADC采样配置里把采样时间拉长后缓解。第三天发现OLED在低温环境下I2C通信偶尔丢字节最终定位是代码里I2C起始信号后没有足够的建立时间我在软件模拟I2C的起始函数里加了2us延时之后问题消失。最终记录到的数据曲线通过串口打印到PC再用Excel画图温度稳定在22.5°C ± 0.5°C没有出现明显漂移湿度在60%RH上下波动±2%RH符合DHT11手册标称精度光照等级随窗户方向变化明显正午和傍晚区分度很好烟雾浓度用打火机气体测试读数从2%上升到47%约8秒回落蜂鸣器在30%阈值处正确触发告警。8. 从课题到产品几个低成本扩展方向做完这套系统后如果你不想让它止步于课程设计有几个扩展方向性价比很高。上云在预留的串口上接一块ESP8266模块用AT指令把采集到的数据以JSON格式通过MQTT协议推送至公共物联网平台。这个改动只需要在现有代码的Task_TimeUp循环里加一个“上传函数”再加一段AT指令初始化逻辑即可总体代码量增加不到200行。换屏把0.96寸OLED换成1.54寸或2.4寸的TFT彩屏使用SPI接口速度更快可以增加趋势曲线绘制功能界面的观感会有本质提升。代价是GPIO占用更多以及LVGL图形库的移植成本。低功耗改造如果未来想用电池供电把屏幕刷新周期拉长到5秒STM32进入STOP模式通过RTC定时唤醒采集配合低压差稳压器和DHT11的低功耗模式整体功耗可以压到1mA以下。传感器扩展在ADC还有空余通道的前提下增加土壤湿度传感器就可以改成小型植物养护系统增加红外人体传感器就能实现有人自动亮灯和安防联动。如果你手头有一块STM32开发板不妨从最简版本开始——一块DHT11加一块OLED串联起来先跑通采集和显示再逐步叠加传感器、告警和通信功能。这个循序渐进的过程比直接照搬整套代码有意义得多。等到你把每个模块的原理都吃透了回头再看这份室内环境监测系统会发现它其实覆盖了绝大多数嵌入式开发的基础能力GPIO控制、单总线时序、ADC采样、滤波算法、定时器中断、状态机、外设驱动、串口调试。这些能力才是这个课题真正的价值所在。本文还有配套的精品资源点击获取