基于STM32的智能环境监测系统实战:从硬件到代码全记录

发布时间:2026/9/8 8:39:27
基于STM32的智能环境监测系统实战:从硬件到代码全记录 寒假综合实验这个题目听起来像一块橡皮泥——捏什么形状全看你自己。说实话我在选题之前也犹豫了很久最后决定做一个基于STM32的智能环境监测系统把温湿度传感器、光照传感器、OLED显示屏和串口通信全串起来一边实时采集环境数据一边在本地显示顺带把数据上传到电脑端记录。这个项目刚好覆盖了我这学期学过的单片机、传感器、C语言、通信协议和基本数据处理知识是我理解中“综合”两个字该有的分量。这篇内容不打算空谈概念我会把整个寒假实验从0到1的完整过程写出来包括为什么选这个方案、每个硬件怎么接线、驱动代码怎么写、串口协议怎么定、调试踩了哪些坑以及寒假这种“没人管你”的时间里怎么逼自己跑完全程。无论你是电子、自动化、物联网方向的学生还是单纯想趁寒假做个能动手的东西这篇文章都应该能给你一套可以照抄的作业。1. 选题与整体方案寒假综合实验的第一步1.1 为什么选“智能环境监测”这个方向综合实验最忌讳的就是“大而空”。我见过不少同学寒假前雄心勃勃要做什么四轴飞行器、人脸识别门禁、智能小车结果元器件等快递等了十天寒假结束连电路都没焊完。这种项目的工程量不是一个人三四周能收尾的最后基本都变成了应付式的文字报告。环境监测系统的工程量则刚刚好。它不需要复杂的机械结构不需要高精度运动控制核心就是把几个传感器数据读出来、处理一下、显示出去、传出去。但你别小看这个“简单”项目它背后涉及的原理很完整I2C时序、单总线时序、ADC采样、中断、定时器、串口DMA、数据帧校验、滤波算法一整套嵌入式开发的基本功全在里头。做完之后你对嵌入式系统的认识是成体系的而不是零散的知识点。另外一个重要原因是成本可控。整个系统的物料清单加起来不到50块钱就算中途烧了几个模块也不肉疼。寒假经费本来就有限把每一分钱花在刀刃上这点很重要。1.2 整体功能拆分与技术栈选定从“我能做什么”出发把整个系统拆分成了四个层次数据采集层负责读DHT22温湿度传感器和BH1750光照传感器这是系统的“眼睛”。数据处理层对原始数据进行滤波和单位换算这是系统的“大脑”。显示层用0.96寸OLED把当前温度、湿度、光照强度、采样时间展示出来这是系统的“脸面”。通信层通过串口把数据按自定义帧格式发送到电脑电脑端用串口助手或简单脚本记录成CSV文件这是系统的“对外接口”。主控芯片选了STM32F103C8T6也就是大家常说的“蓝丸”核心板。原因很直接它便宜、资料多、用寄存器操作能真正理解底层。如果选Arduino确实开发速度更快但很多时序和寄存器细节被库函数包住了做综合实验的“综合”感就弱了不少。不知道你们有没有这种感觉用别人的库一次就跑通心里反而发虚自己翻参考手册调通一次哪怕调了一整天那种踏实感是完全不一样的。所以这次实验我给自己定了一条规矩——不调用任何现成的HAL库驱动全部用寄存器方式写底层驱动代码自己造轮子。2. 硬件选型与电路连接把方案落地到元器件2.1 主控与传感器选型选型其实是综合考虑了下述因素的结果价格、学习价值、资料丰富度、功耗和稳定性。我不是在给自己买一个“能用就行的玩具”而是在配一套能反复折腾的试验平台。模块型号/规格接口供电单价参考选型理由主控板STM32F103C8T6核心板引脚直插5V/3.3V12元左右性价比高寄存器底层资料极多温湿度传感器DHT22AM2302单总线3.3V~5V8元左右精度高±0.5℃数字输出免校准光照传感器BH1750I2C3.3V~5V6元左右直接输出lux光谱响应接近人眼模块自带电平转换显示屏0.96寸OLED SSD1306I2C3.3V~5V10元左右低功耗、对比度高、驱动协议经典USB转串口CH340模块UART5V5元左右兼容性好串口实验通用稳压模块AMS1117-3.3VDC-DC线性稳压5V输入2元左右核心板自带可省但外接传感器多时建议独立供电这里插一句DHT22和DS18B20名字有点像但完全是两回事。DS18B20也是单总线可只测温度DHT22输出的是温湿度组合数据直接省一个传感器。实验需求是温湿度都要所以DHT22是更省事的选择。2.2 接线、供电与电路搭建要点接线方案要提前在纸上画清楚别拿到板子就乱插。我在实验第一天就吃了“杜邦线乱飞查错半小时”的亏后来老老实实做了接线表模块模块引脚接入STM32引脚备注DHT22VCC3.3V别接5V虽然支持但接3.3V更稳DHT22GNDGND共地必须保证DHT22DATAPA0单总线数据线需要外部上拉4.7kΩBH1750VCC3.3V模块自带10k上拉电阻BH1750GNDGND共地BH1750SCLPB6I2C时钟线BH1750SDAPB7I2C数据线OLEDVCC/GND3.3V/GND同上OLEDSCL/SDAPB6/PB7与光传感器共用I2C总线CH340TXDPA10MCU的UART1接收端接CH340的发送CH340RXDPA9MCU的UART1发送端接CH340的接收供电上有个新手容易犯的错把传感器的VCC全接到核心板的3.3V引脚然后把USB转串口的5V输出接到核心板5V。这样从电脑USB口拉出来的电本身是5V/500mA经过板载稳压到3.3V以后还剩多少实测带两个传感器加一个OLED纹波最大跑到200mV以上DHT22读数就开始飘。我的办法是传感器统一由核心板3.3V供电但核心板用外接的USB充电头供电而不是和CH340共用一个USB口。CH340只负责数据线把TXD/RXD接上VCC不要接到核心板。这样既保证了电源干净又不会因为两个USB模块的地电位不一致产生乱码。接线完成之后用万用表量一遍所有VCC和GND是不是真的短接了量一下每个模块的VCC对地电阻防止模块内部短路把主控烧了。这一步花了五分钟但能避免后面一上电就冒烟的悲剧。3. 软件设计从驱动到应用层怎么写3.1 工程结构与代码组织方式整个工程在Keil MDK里建代码分模块存放每个模块管一件事。当时给自己定的目录结构是这样的Project/ ├── USER/ │ ├── main.c │ ├── stm32f10x_it.c │ └── system_stm32f10x.c ├── HARDWARE/ │ ├── dht22.c / dht22.h │ ├── bh1750.c / bh1750.h │ ├── oled.c / oled.h │ └── uart.c / uart.h ├── CORE/ │ └── startup_stm32f10x_hd.s └── SYSTEM/ ├── delay.c / delay.h ├── i2c.c / i2c.h └── usart.c / usart.h这套结构的思路是单片机底层资源延时、I2C、串口放SYSTEM具体芯片驱动DHT22、BH1750、OLED放HARDWARE业务逻辑只在main.c里写。这样以后想换一颗芯片只需要替换SYSTEM和HARDWARE业务层基本不动。很多人写单片机代码喜欢把所有函数堆在一个main.c里几百行只靠注释分隔。短期跑demo没问题但综合实验要面对的是多模块协同一旦有个bug在一个上千行的文件里翻找会崩溃。模块化写法的另一个好处是每写完一个模块就可以单独用一个小main函数测试它驱动全通了再去集成定位问题容易得多。3.2 传感器驱动的关键时序处理DHT22是最考验时序的传感器。它的单总线协议要求主机先拉低总线至少18ms然后释放总线等待传感器响应。传感器会拉低80us再拉高80us作为应答然后开始输出40位数据。麻烦的关键点在于每一位数据传感器会拉低50us然后拉高26到28us表示逻辑0拉高70us表示逻辑1。你要在释放总线之后的50us窗口内用定时器或者延时函数去测量高电平持续的时间。如果用的是主频72MHz的STM32写一个精密延时的微秒级函数也不容易我直接调用了SysTick的延时函数us级别去读引脚电平。// 读取一个bit根据高电平持续时长判断0或1 uint8_t DHT22_ReadBit(void) { uint8_t retry 0; while (GPIO_ReadInputDataBit(DHT22_PORT, DHT22_PIN) RESET) { if (retry 100) return 0; // 超时保护 Delay_us(1); } uint32_t high_time 0; while (GPIO_ReadInputDataBit(DHT22_PORT, DHT22_PIN) SET) { if (high_time 100) return 0; // 高电平时间太长异常 Delay_us(1); } // 高电平持续时间约70us表示1约28us表示0 return (high_time 40) ? 1 : 0; }这个判断阈值40us是我实际测过的。理论值是逻辑1高电平70us、逻辑0是26到28us中间值是48us左右我取了40us做分界线实测稳定。不同厂商的DHT22时序略有差别如果你用的传感器在40us附近抖动明显可以把阈值调到45us再试。还有个细节经验STM32的GPIO速度等级也很关键。DHT22数据线对应的引脚要设置成50MHz输出模式如果用2MHz低速模式电平翻转时边沿不够陡峭传感器会误判时序。这个坑我调了整整一个下午才反应过来。BH1750就友好很多它走的是标准I2C协议地址固定是0x23写作设备地址时左移一位为0x46。初始化的核心指令有两个0x01是上电0x10是连续高分辨率模式分辨率1lux测量时间最长约120ms。读数据的时候先发地址读命令然后连续读两个字节把高字节左移8位和低字节做或运算就是实际的lux值。uint16_t BH1750_ReadLux(void) { uint8_t buf[2]; I2C_Start(); I2C_SendByte(BH1750_ADDR_WRITE); // 0x46 I2C_SendByte(0x10); // 连续高分辨率模式 I2C_Stop(); Delay_ms(180); // 至少等一次完整测量周期 I2C_Start(); I2C_SendByte(BH1750_ADDR_READ); // 0x47 buf[0] I2C_RecvByte(ACK); buf[1] I2C_RecvByte(NACK); I2C_Stop(); return (buf[0] 8) | buf[1]; }OLED驱动底层的I2C命令序列也不复杂但要理解它的显存机制SSD1306内置了1KB的显存对应128x64像素屏页面分8页每页8行像素。我封装了一个OLED_ShowChar()函数和一个OLED_ShowString()函数显示中文字库太占Flash了所以所有界面提示语都用英文这也省去字库的麻烦。3.3 上位机通信协议设计通信协议是整个系统里最能体现“综合”能力的一环。串口收发数据本身很简单但怎么保证电脑端拿到的数据和设备端发送的数据是一致的、完整的这就涉及一个微型协议的设计。我用的帧格式如下字节位置内容说明00xAA帧头110x55帧头220x01命令字0x01表示环境数据帧3温度整数部分与第4字节组合为实际值4温度小数部分例如0x13 0x05表示19.05℃5湿度整数部分同上6湿度小数部分例如0x2E 0x03表示46.03%RH7光照高字节8光照低字节例如0x05 0xDC表示1500lux9CRC8校验值从第0字节到第8字节计算CRC8帧尾其实不需要加因为固定帧长10字节上位机靠帧头和长度就能截包。加一个帧尾反而容易把帧边界搞乱。CRC8我用的是多项式0x31初始值0xFF算法照着网上公开的CRC-8/MAXIM实现抄下来的。为什么用CRC8而不用简单的累加和因为累加和能够检错的能力太弱两个字节一个错位一个错位补回来就变成了合法帧。做综合实验我希望体现的是工业级的数据可靠性思路CRC8虽然代码多十几行但完全值得。上位机接收端我用Python写了一个简单脚本借助pyserial读取串口数据按协议解析之后追加写入CSV文件。脚本不去管UI只做数据采集和落盘逻辑简单适合入门。import serial import csv from datetime import datetime ser serial.Serial(COM3, 115200, timeout1) with open(env_log.csv, a, newline) as f: writer csv.writer(f) while True: data ser.read(10) if len(data) 10: continue if data[0] ! 0xAA or data[1] ! 0x55: continue temp data[3] data[4] / 100.0 humi data[5] data[6] / 100.0 lux (data[7] 8) | data[8] if crc8(data[0:9]) ! data[9]: continue writer.writerow([datetime.now(), temp, humi, lux]) print(f{datetime.now():%H:%M:%S} T{temp:.2f}C H{humi:.2f}% Lux{lux})这里有个很调皮的细节CRC校验用的是数据帧的前9个字节算出来的值存到第9个字节里。如果先把CRC字节放进去再算那校验值本身就依赖于嵌套自身就闭环了这是我一开始就踩过的逻辑坑。4. 数据处理与分析让数据在乱世中保持清醒4.1 软件滤波与异常值剔除传感器的数据不可能总是平滑的。BH1750和DHT22输出的原始数据在供电不稳或者附近有电磁干扰的时候会出现毛刺。所谓毛刺就是瞬间跳变到远超正常范围的值比如温度为23℃时突然跳出一个35℃的读数。处理毛刺最直接的办法是中位值滤波连续读取5个值排序后取中间那个。中位值对随机脉冲噪声的抑制效果很好但代价是数据更新速度变慢每次输出需要等5次采样完成。我这个系统每秒采样一次5次就是5秒显示端显得有点“迟钝”。所以我把中位值滤波和滑动平均结合了先做中位值滤波剔除离群点再做滑动平均平滑曲线。具体实现是维护一个长度为5的环形缓冲区新数据到来时替换最旧的数据然后对这5个值求平均作为最终显示值。这样既滤掉了毛刺又不会让响应速度慢到不可接受。#define FILTER_WINDOW 5 float filter_buf[FILTER_WINDOW]; uint8_t filter_index 0; uint8_t filter_count 0; float SlideAverage(float new_value) { filter_buf[filter_index] new_value; filter_index (filter_index 1) % FILTER_WINDOW; if (filter_count FILTER_WINDOW) filter_count; float sum 0; for (uint8_t i 0; i filter_count; i) { sum filter_buf[i]; } return sum / filter_count; }注意环形缓冲区的取模操作(filter_index 1) % FILTER_WINDOW。我一开始图省事直接让filter_index自增结果发现缓冲区越界写坏了内存程序跑几分钟就死机。这算是C语言的老生常谈但实际写多线程程序或者中断服务程序时很多人还是会栽在数组越界上。4.2 数据的单位换算与零点校准BH1750直接输出的值就是luxDHT22输出的温度是带符号的二进制补码湿度是高16位整数加低16位小数。很多第一次用DHT22的同学会忽略它的温度数据有符号位的问题低温环境下直接读出的0xFFFF表示的是-0.1℃而不是6553.5℃。所以解析时要先判断最高位int16_t raw_temp (data[2] 8) | data[3]; if (raw_temp 0x8000) { raw_temp -(~raw_temp 1); // 转为负数 } float temperature raw_temp / 10.0f;这个符号位处理是我在数据记录里面发现的坑。北方冬天室内温度虽然都在0℃以上但把设备放到阳台几分钟后数据直接变成了6553.5℃一看就是符号位没处理。如果实验要在低温环境下跑这个细节分分钟要命。传感器在出厂时都有一定的个体差异我还做了简单的零点校准用一个标准的温湿度计放在传感器旁边等30分钟稳定后对比读数把偏差值以宏定义的方式写死在程序里。比如标准温度计显示21.5℃DHT22显示21.3℃那偏差就是0.2℃显示和上报时加上这个偏差就能把系统校到相对准确的水平。4.3 实验数据的可视化采集不是终点数据要能看出趋势才有价值。CSV落盘之后我用Python的matplotlib画了一张寒假期间的温湿度曲线图横轴是时间纵轴是温度/湿度光照强度因为量纲差异太大单独画了第二个子图。这样一份完整的“实验结果分析”材料就出来了白天光照强度随太阳升落呈明显正弦波曲线傍晚开灯时lux值会突然上升然后稳定温度曲线有一个明显的周期性白天供暖时段升高夜间回落湿度曲线和阳台通风动作强相关开窗后湿度迅速下降再恢复。画图代码大概三十行但实验结果的数据分析价值不是代码长度能衡量的。写综合实验报告的时候这个正是最有力的论据说明你的系统不只是“能跑”而且“数据可信、规律可见”。5. 综合调试与问题排查实录整个寒假实验最磨人的不是写代码而是调试。我把自己实际遇到过的、有代表性的问题整理成了下面的表格并且把排查思路也一并写出来方便你以后照葫芦画瓢。故障现象可能原因排查方法解决办法OLED白屏无内容地址错误SCL/SDA接反I2C上拉缺失用示波器看SCL波形换默认地址0x3C试万用表量上拉电阻确认地址和接线补4.7kΩ上拉DHT22一直返回0xFF起始信号时序不对引脚速度设置太低供电纹波大用逻辑分析仪抓时序检查GPIO速度等级示波器看VCC纹波调整延时参数引脚设为50MHz独立供电串口乱码波特率不匹配CH340驱动异常发送端用了5V电平检查串口设置重新安装驱动用示波器看TXD信号波特率统一波特率115200换USB口数据偶发跳变传感器和主控共地不良信号线和电源线并行走线过长理清地线拉大导线间距改屏蔽线重新连接地线使用短杜邦线程序下载失败Boot0跳线不对Flash写保护仿真器不兼容检查BOOT0引脚电平查看下载器日志下载时BOOT0拉高烧录完恢复低电平5.1 OLED白屏的完整排查过程我接好线第一次上电OLED是不亮然后是亮了但全白最后才是正常显示。这个“全白”其实很有代表性——它说明显示屏已经通电、有数据信号进入只是在I2C通信上没有建立起正确的显示内容刷新。排查步骤依次是第一步确认地址。SSD1306的I2C地址由SA0引脚决定常见的模块固定为0x3C。部分模块默认是0x3D需要你去查你买的那个版本的原理图或规格书。第二步确认接线。OLED的SCL和SDA不能简单地和BH1750“共用”就完事了共用没问题但必须先保证两个模块挂在同一条总线上时总线上有正确的上拉电阻。有些模块板载上拉有些没有我用的BH1750模块带10k上拉OLED模块也带并联后等效电阻约5k算是合规范围。第三步用逻辑分析仪抓取I2C波形。从波形图上我能看到ACK信号是否正常SCL频率有没有被拉得太慢总线上是否有设备的地址响应冲突。这步对快速定位I2C问题很有价值。5.2 DHT22读数是“薛定谔的温湿度”“有时正常有时随机”这个问题困扰了我快两天。现象是系统刚上电时读数正常运行半小时后开始出现0xFF或跳变。我一开始怀疑是DHT22芯片坏了换了新的还是老样子。后来发现核心板上的AMS1117稳压芯片在持续负载下发热严重表面温度超过60℃整块板上方热浪翻涌DHT22距离主控只有3cm被连续烘烤后内部晶振频率漂移单总线时序就乱了。这个问题的解决非常简单粗暴把DHT22放到离核心板较远的地方用15cm杜邦线连接同时在DHT22旁边加了一个小型散热片其实就是铝片折的。从此之后温度读数稳定在22℃上下再也没有乱飘。这也给我一个教训传感器系统的可靠性热设计占一大半芯片离热源远一点往往比堆一堆滤波算法管用。5.3 串口数据偶发丢帧的真相串口丢帧问题一度让我怀疑人生。一开始是串口助手偶尔少一帧数据后来频率越来越高甚至一整段时间都没有数据。仔细检查后发现CH340模块和STM32核心板分别由两个USB口供电而笔记本电脑的两个USB口的GND电位并不完全一致存在零点几伏的电位差。这个电位差会让串口信号的逻辑电平失真导致丢帧。解决办法是把两边的GND用一根杜邦线直接连起来实现共地。共地之后串口通信稳定得像老黄牛一样连续跑了12小时都没丢一帧。这件事让我明白了为什么所有串口、I2C、SPI通信器件都要求“先共地再通信”这个铁律。6. 寒假项目的进度管理与资料沉淀6.1 时间安排参考表寒假做项目最大的敌人是懒散和拖延。我给自己排了下面这个12天计划节奏不紧不慢每个阶段都有明确的交付物天数任务交付物第1天明确需求、选型、下单买齐所有硬件物料清单和接线图第2-3天学习STM32寄存器开发基础跑通LED、SysTick、串口点灯工程可运行的最小系统工程第4-5天编写I2C、OLED驱动跑通OLED显示字符OLED能正常显示第6-7天编写BH1750、DHT22驱动单模块分别测试传感器数据能独立读出来第8-9天集成所有模块完成主循环逻辑和串口协议完整系统本地跑通第10天编写Python上位机收数据脚本连续记录24小时数据CSV数据文件第11天数据分析和可视化生成趋势图图表和分析结果第12天整理代码、写综合实验报告、拍演示视频报告和演示材料寒假中间还有春节我承认在除夕和初一两天完全没碰代码。计划表里没排这两天是故意给自己留的缓冲。做寒假项目目标不是把自己逼成机器人而是建立可持续的节奏。哪怕中间停两天只要大方向是朝前走的项目就能完成。6.2 代码注释与实验文档怎么写实验做完之后最重要的事情是写报告而写报告最难的其实是回忆和整理。所以我从一开始就养成了随手记日志的习惯每天在项目的README.md下面追加一个“今日进度”小节记录今天的完成事项、踩过的坑、明天的计划。这个习惯对期末综合实验报告的撰写帮助巨大我甚至把当时的日志片段直接抄进了报告里当作“过程记录”证据。代码注释我遵循的原则是“注释为什么而不是注释什么”。例如下面这段// 这里必须释放总线至少18msDHT22才能正确识别起始信号。 // 我之前用10ms传感器直接不响应查了数据手册发现是18~30ms。这段注释不是在念代码而是在记录一个“为什么”。以后每当你看到注释里“之前用什么不行改用这个才行”的描述就明白前人踩过的坑能帮你节省大量时间。6.3 后续还能怎么扩综合实验做完之后你觉得已经到顶了其实扩展空间非常大。这个系统目前是本地上传到一个PC端脚本未来可以做的事情很多接入ESP8266或ESP32模块把数据通过WiFi发到局域网内网服务器做一个简单的远程监测系统加一个继电器模块当温度超过设定阈值时自动打开风扇或加热器从“监测”升级为“控制”将Python脚本改成Flask服务数据实时写入SQLite数据库前端用ECharts做仪表盘把STM32F103换成STM32L431低功耗芯片改成电池供电的低功耗采集节点可以放户外跑一整个寒假。这些扩展方向的工程量都不大但每个方向都对应着一个不同的技术栈适合作为下一个综合实验的起点。最后分享一个我自己这几天最大的体会寒假做综合实验拼的不是谁天赋高、谁对课程内容记得牢而是谁能每天固定坐在桌前写一点、调一点、测一点。专业能力是在调试的过程中长出来的当你把一堆模块拼在一起并让它们协同工作的时候那些躺在课本里的原理突然就活了。如果你也在寒假做自己的综合实验希望这篇内容能帮你少走一些弯路也希望你多踩一些属于自己的坑——毕竟踩坑才是真正学到东西的时刻。