☰
STM32环境温湿度监测系统源码拆解:从DHT11采集到OLED显示与报警
2026/10/30 3:14:12 网站建设 项目流程

简介:这份资源面向学习STM32嵌入式开发的学生与电子爱好者,围绕环境温湿度监测这一典型课程设计或竞赛题目,提供从传感器采集到报警控制的完整实现方案。系统通过DS18B20采集温度、DHT11采集湿度,数据实时显示于LCD液晶屏,并支持手动设置温湿度上下限,超限时触发无源蜂鸣器播放音乐与LED闪烁的声光报警,便于读者理解多外设协同与阈值判断逻辑。压缩包共195个文件、约4.34MB,以c源文件与h头文件为主体,配合o、d、crf等编译中间文件及axf、hex、map等构建产物,另有uvprojx、uvoptx工程配置与少量asm汇编文件,完整保留了Keil工程结构与调试痕迹。目前已有8727人学习下载,读者可据此对照工程组织方式,梳理传感器驱动、显示刷新与报警模块的代码分工,并借鉴其外设初始化与主循环调度思路,用于自己的课程设计或功能扩展。

1. 从一块“能自己说话”的板子说起

去年帮一个做仓储的朋友看现场,库房里三台老式温湿度表,靠人每天早晚各抄一次数,赶上回南天,墙面结露了才发现湿度早就爆表。他问我能不能弄个便宜的小板子,插上电就自己采数、自己显示、超了阈值还能叫一声。我翻出这份基于 STM32 的环境温湿度监测系统源码包,从工程结构到采集逻辑完整拆了一遍,正好拿它当例子讲清楚一个嵌入式小项目从能跑到能改的全过程。这份资源适合两类人:一类是刚学完 STM32 外设、想找个完整工程练手的同学;另一类是手上有类似采集需求、想直接拿现成框架改参数落地的开发者。它不复杂,但五脏俱全——采集、显示、报警、串口输出这条链路都在,拿来当二次开发的底座比从零搭要省不少事。

2. 工程结构与技术选型:一个采集系统由哪几块拼起来

2.1 目录骨架与文件职责

拿到压缩包解压后,第一件事不是急着编译,而是先看目录。这类 STM32 标准工程通常长这样,我按实际拆包的经验给你还原一份典型结构:

EnvMonitor_STM32/ ├── Core/ │ ├── Inc/ # 头文件:引脚定义、外设句柄、函数声明 │ │ ├── main.h │ │ ├── dht11.h │ │ ├── oled.h │ │ └── usart.h │ └── Src/ │ ├── main.c # 初始化 + 主循环调度 │ ├── dht11.c # 温湿度传感器驱动(单总线时序) │ ├── oled.c # 显示驱动(I2C 或 SPI) │ ├── usart.c # 串口输出,方便上位机看数据 │ └── stm32f1xx_it.c # 中断服务函数 ├── Drivers/ │ ├── CMSIS/ # 内核头文件 │ └── STM32F1xx_HAL_Driver/# HAL 库 ├── MDK-ARM/ # Keil 工程文件 .uvprojx └── README.md

这个骨架的关键在于Core/Src下每个.c只干一件事:dht11.c只管时序和读数,oled.c只管刷屏,main.c负责把它们串起来。新手最容易犯的错是把所有逻辑塞进main.c,改一个传感器就得动全身。这份工程的分层虽然朴素,但边界是清楚的,二次开发时替换传感器只需要新增一个驱动文件,主循环里换个调用即可。

2.2 技术栈与选型理由

整套系统跑在 STM32F1 系列上(常见是 F103C8T6 这类最小系统板),用 HAL 库而不是标准库,开发环境是 Keil MDK。传感器侧走 DHT11 单总线,显示侧走 0.96 寸 OLED(I2C 接口居多),再加一路 USART 把数据吐给串口助手。为什么这么选?DHT11 便宜、时序简单、一根数据线就够,缺点是精度一般(温度 ±2℃、湿度 ±5%RH),做仓储、大棚、机房这类场景够用;OLED 比 LCD1602 省引脚、自带字库、暗光下也看得清;串口输出则是调试和对接上位机的万能口子。

模块器件/接口作用常见替换
主控STM32F103C8T6调度与运算F401 / G0 系列
采集DHT11 单总线温湿度读数SHT30(I2C,精度更高)
显示OLED 0.96 寸 I2C本地数值展示LCD1602 / TFT
通信USART1数据上报调试加 ESP8266 上云
报警蜂鸣器 + LED阈值提醒继电器联动

选型背后有个现实考量:这套组合的物料成本压得很低,且每个模块都有大量现成驱动可参考,出问题好查。如果你要做工业级项目,DHT11 的精度和响应速度会成为瓶颈,那时候换 SHT30 是更稳的选择,接口从单总线变 I2C,驱动层改动量不大,这也是分层设计带来的好处。

2.3 主循环调度逻辑

嵌入式项目没有操作系统时,主循环就是“大脑”。这份工程的调度思路很直接:初始化外设 → 进 while(1) → 定时读传感器 → 刷新显示 → 判断阈值 → 串口打印 → 延时。看一段还原后的核心骨架:

int main(void) { HAL_Init(); // HAL 库初始化 SystemClock_Config(); // 配置系统时钟(通常 72MHz) MX_GPIO_Init(); // 引脚初始化 MX_USART1_UART_Init(); // 串口初始化,115200 8N1 OLED_Init(); // 显示屏初始化 DHT11_Init(); // 传感器引脚拉高准备 DHT11_Data_t data; // 存放温湿度结果的结构体 while (1) { if (DHT11_Read(&data) == DHT11_OK) // 读取成功才更新 { OLED_ShowTemp(data.temp); // 刷温度 OLED_ShowHumi(data.humi); // 刷湿度 CheckAlarm(data.temp, data.humi);// 阈值判断,超限鸣叫 printf("T=%d,H=%d\r\n", data.temp, data.humi); // 串口上报 } HAL_Delay(2000); // DHT11 采样间隔不能低于 1s,取 2s 稳妥 } }

这段代码里有两个参数值得注意:HAL_Delay(2000)的 2 秒不是随便写的,DHT11 两次采样之间至少间隔 1 秒,否则读出来是上一次的缓存值甚至直接失败;DHT11_Read的返回值必须判断,单总线器件通信失败是常态,不判断就会把脏数据刷到屏上。DHT11_Data_t结构体一般包含temp、humi两个整型字段,DHT11 本身只输出整数部分,别指望它给你小数。

3. 核心驱动拆解:DHT11 时序与 OLED 刷屏怎么写

3.1 DHT11 单总线时序的坑

DHT11 用的是单根数据线双向通信,主机先拉低至少 18ms 发开始信号,然后释放总线,传感器回应 80us 低电平 + 80us 高电平,接着连续输出 40 位数据(湿度整数 8 位 + 湿度小数 8 位 + 温度整数 8 位 + 温度小数 8 位 + 校验 8 位)。每一位用高电平持续时间区分 0 和 1:26~28us 是高电平为 0,70us 为 1。这段时序对延时精度要求高,HAL 库的HAL_Delay是毫秒级,位判断必须用微秒级延时。看驱动核心:

uint8_t DHT11_Read(DHT11_Data_t *out) { uint8_t buf[5] = {0}; // 1. 主机发送开始信号:拉低 18ms 再释放 DHT11_SetPinOutput(); HAL_GPIO_WritePin(DHT11_PORT, DHT11_PIN, GPIO_PIN_RESET); HAL_Delay(18); HAL_GPIO_WritePin(DHT11_PORT, DHT11_PIN, GPIO_PIN_SET); DHT11_SetPinInput(); delay_us(30); // 等传感器响应 // 2. 检测响应信号(80us 低 + 80us 高) if (DHT11_WaitLevel(GPIO_PIN_RESET, 100) != OK) return DHT11_ERR; if (DHT11_WaitLevel(GPIO_PIN_SET, 100) != OK) return DHT11_ERR; // 3. 逐位读取 40 bit for (int i = 0; i < 5; i++) { for (int j = 0; j < 8; j++) { if (DHT11_WaitLevel(GPIO_PIN_RESET, 100) != OK) return DHT11_ERR; delay_us(40); // 40us 后采样,区分 0/1 buf[i] <<= 1; if (HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) == GPIO_PIN_SET) buf[i] |= 1; if (DHT11_WaitLevel(GPIO_PIN_SET, 100) != OK) return DHT11_ERR; } } // 4. 校验:前 4 字节之和的低 8 位应等于第 5 字节 if ((uint8_t)(buf[0] + buf[1] + buf[2] + buf[3]) != buf[4]) return DHT11_ERR; out->humi = buf[0]; out->temp = buf[2]; return DHT11_OK; }

delay_us(40)这个采样点是整个驱动的命门:高电平开始后等 40us 再读引脚,此时若还是高电平说明这一位是 1(持续 70us),若已拉低说明是 0(持续 26us)。DHT11_WaitLevel是个带超时的电平等待函数,超时返回错误,防止传感器没接好时程序卡死。校验和那一步千万别省,我见过现场因为线太长导致某一位翻转,没校验的话屏上会显示一个离谱的数值,反而误导判断。

3.2 OLED 显示层的封装思路

显示部分不建议在主循环里直接调底层写命令函数,而是包一层业务接口。典型做法是OLED_ShowTemp(int t)内部先格式化字符串再调OLED_ShowString:

void OLED_ShowTemp(int temp) { char str[16]; sprintf(str, "Temp: %d C ", temp); // 尾部空格用于覆盖旧值 OLED_ShowString(0, 0, (uint8_t *)str, 16); // 第 0 行 } void OLED_ShowHumi(int humi) { char str[16]; sprintf(str, "Humi: %d %% ", humi); OLED_ShowString(0, 2, (uint8_t *)str, 16); // 第 2 行 }

sprintf里尾部留空格是个小技巧:数值从两位数变一位数时,不补空格会残留旧字符,屏上出现“25 C”变成“5 C5”这种鬼画符。OLED_ShowString(x, y, str, size)的y是页地址(0~7),每页 8 像素高,所以第 0 行和第 2 行之间隔了一页,视觉上不挤。字号size传 16 表示 16 号字,改 12 能多塞内容但可读性下降。

3.3 阈值报警与串口上报

报警逻辑简单但要有迟滞,否则数值在阈值附近抖动时蜂鸣器会“哒哒哒”响个不停。常见做法是设两个阈值:超过上限触发,回落到上限减 1 才解除:

static uint8_t alarm_on = 0; void CheckAlarm(int temp, int humi) { if (!alarm_on && (temp > TEMP_HIGH || humi > HUMI_HIGH)) { alarm_on = 1; HAL_GPIO_WritePin(BEEP_PORT, BEEP_PIN, GPIO_PIN_SET); } else if (alarm_on && temp < TEMP_HIGH - 1 && humi < HUMI_HIGH - 1) { alarm_on = 0; HAL_GPIO_WritePin(BEEP_PORT, BEEP_PIN, GPIO_PIN_RESET); } }

TEMP_HIGH、HUMI_HIGH定义在main.h里,改阈值不用动逻辑代码。串口那边用重定向的printf,底层fputc调到HAL_UART_Transmit,波特率 115200,接串口助手就能看到T=25,H=60这样的行,方便长时间记录。

4. 编译烧录与联调:从源码到板子跑起来

4.1 环境准备与工程打开

先装 Keil MDK5,再装 STM32F1 的器件支持包(Pack),这两步缺一个工程都打不开。打开MDK-ARM目录下的.uvprojx文件,检查三处配置:Target 里芯片型号是否和手上板子一致(F103C8 选 STM32F103C8)、Debug 页选 ST-Link 或 DAP-Link、Utilities 里勾选 “Use Debug Driver” 让下载走调试器。编译前按 F7,看 Build Output 有没有 Error,Warning 可以先放。

烧录用 ST-Link 的话,接线是 SWDIO、SWCLK、GND、3.3V 四根,别把 3.3V 和 5V 接反,这是烧板子的头号原因。点 Download(F8)后如果提示 “No target connected”,先查接线再查驱动,九成是接触问题。

4.2 上电自检顺序

板子跑起来后按这个顺序验证,能快速定位问题出在哪一层:

  1. 先看 OLED 有没有亮、有没有显示初始字符——不亮查 I2C 地址(常见 0x3C 或 0x78)和上拉电阻;
  2. 再看屏上温湿度是不是在变——不变查 DHT11 数据线有没有接对引脚、有没有 4.7k~10k 上拉;
  3. 然后开串口助手(115200、8N1)看有没有数据行——没有查 USART1 的 TX/RX 是否交叉接;
  4. 最后用手哈一口气对着传感器,看数值上升、超阈值时蜂鸣器响不响。

这个顺序的逻辑是从“最直观的输出”往“最底层的采集”倒推,哪一步断了就锁定那一层,比一上来就翻代码高效得多。

4.3 串口数据的二次利用

串口输出不只是调试用,它其实是这套系统对接上位机最省事的接口。数据格式T=25,H=60用 Python 的pyserial几行就能读进来存 CSV:

import serial, csv, time ser = serial.Serial('COM3', 115200, timeout=1) with open('env_log.csv', 'w', newline='') as f: writer = csv.writer(f) writer.writerow(['time', 'temp', 'humi']) while True: line = ser.readline().decode('ascii', errors='ignore').strip() if line.startswith('T='): parts = dict(p.split('=') for p in line.split(',')) writer.writerow([time.strftime('%Y-%m-%d %H:%M:%S'), parts['T'], parts['H']]) f.flush()

timeout=1防止读阻塞,errors='ignore'过滤上电时的乱码,f.flush()保证断电也不丢已写数据。这套组合拿来做小规模环境记录完全够用,不用急着上云。

5. 避坑与常见问题:那些让我返工的细节

5.1 读数一直是 0 或 255

现象:屏上温湿度固定不变,串口打印T=0,H=0或T=255,H=255。原因多半是单总线没收到响应,可能是引脚定义和实际接线不一致,也可能是数据线没加上拉电阻导致电平拉不上去。解决:先拿万用表量数据线空闲时是不是高电平,不是就补一个 4.7k 上拉到 3.3V;再核对dht11.h里的DHT11_PORT、DHT11_PIN和 CubeMX/原理图是否一致。

5.2 程序卡死在 while 等待里

现象:板子跑一会儿就没反应,OLED 定格。原因:DHT11_WaitLevel这类等待函数没设超时,传感器掉线后死等。解决:所有等待电平的地方都加计数上限,超过就返回错误码,主循环里判断到错误就跳过本次刷新,下一轮再试。这也是为什么前面驱动里每个WaitLevel都带了 100 这个超时参数。

5.3 显示残影和乱码

现象:数值变化后屏上留着旧字符的碎片。原因:新字符串比旧的短,没覆盖干净。解决:格式化时尾部补空格,或者刷之前先用OLED_ClearArea清掉那一行。另外 I2C 速率别设太高,400kHz 在长排线上容易出错,降到 100kHz 更稳。

5.4 蜂鸣器误报不停

现象:数值明明没超,蜂鸣器间歇性响。原因:没有迟滞判断,数值在阈值上下抖动。解决:按 3.3 节的写法加上下限回差,触发和解除用不同阈值。另外 DHT11 本身精度有限,阈值别设得太贴近正常工作范围。

5.5 换芯片后编译过但跑不起来

现象:从 F103 换到 F401,编译通过但外设不工作。原因:时钟树配置、HAL 库版本、启动文件都没跟着换。解决:换芯片不是改个型号就行,SystemClock_Config要按新芯片的主频重配,启动文件startup_stm32f401xx.s要换,HAL 驱动目录也要对应。稳妥做法是用 CubeMX 按新芯片重新生成一版工程骨架,再把业务代码搬过去。

6. 二次开发:把采集底座改成你自己的系统

这套工程真正的价值不在“能显示温湿度”,而在于它是一条完整可跑的最小链路,你可以沿着几个方向往外扩。第一个方向是换传感器提精度:把 DHT11 换成 SHT30,接口从单总线变 I2C,只需新增sht30.c,主循环里把DHT11_Read换成SHT30_Read,显示和报警层完全不用动——这就是分层带来的红利。第二个方向是加通信上云:USART1 已经被 printf 占用,可以再开 USART2 接 ESP8266,把T=25,H=60包成 MQTT 报文发出去,改动集中在新增一个wifi.c和主循环里的一次调用。

第三个方向是加数据存储:F103 内部 Flash 够存几百条记录,或者外挂一个 AT24C02 EEPROM,掉电不丢。这里有个我踩过的坑——Flash 写之前必须先擦除整页,直接写会失败,而且擦写次数有限,别在 2 秒一次的循环里写 Flash,改成超阈值或每 10 分钟存一次。

改造时建议守住一条原则:业务逻辑和驱动分离。判断阈值、决定存不存、发不发,这些写在main.c或单独的app.c里;怎么拉时序、怎么发 I2C,留在驱动文件里。这样下次换硬件,你只需要替换驱动,业务代码一行不动。

验证改造是否成功,别只看屏上数字对不对。我的习惯是三步走:先用串口打印原始字节确认驱动层读到的和传感器手册一致;再人为制造超阈值场景看报警和记录是否触发;最后断电重启看存储的数据还在不在。三步都过,才算真的改对了。

说个血泪教训:早些年我图省事,把所有逻辑堆在一个文件里,后来客户要换传感器,我花了整整两天在几百行里找哪几行是采集、哪几行是显示,改完还引入了新 bug。从那以后我每次接这类小系统,第一件事就是把驱动和业务切开,哪怕项目再小也强制走一遍分层。希望这份拆解能帮你少走那段弯路,源码包拿去直接编译就能跑,剩下的就看你往哪个方向长了。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询