简介:基于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Ω上拉 |
| 光敏传感器 | ADC | PA1 | 分压电路 |
| MQ-2烟雾传感器 | ADC | PA2 | 模块AO输出 |
| OLED显示屏 | I2C | PB6/PB7 | 标准I2C |
| 无源蜂鸣器 | GPIO | PB1 | 定时器PWM驱动 |
| 三色LED | GPIO | PB12/PB13/PB14 | 红绿蓝 |
| 预留串口 | UART | PA9/PA10 | WiFi扩展用 |
这套分配里面其实藏着一个经验: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,避免编译器优化把时序敏感的延时函数改动掉。
工程里延时函数我用的是DWT(Data 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屏的驱动芯片通常是SSD1306,I2C接口只要四根线,代码库在各大社区都有现成的。但很多人把屏幕驱动代码复制过来之后,发现显示中文会变成乱码——这是因为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采样、滤波算法、定时器中断、状态机、外设驱动、串口调试。这些能力,才是这个课题真正的价值所在。
本文还有配套的精品资源,点击获取