【STM32入门项目】DHT11温湿度监测与声光报警系统
【STM32 入门项目】DHT11 温湿度监测与声光报警系统
- 前言
- 一、项目功能
- 二、硬件清单
- 三、引脚连接
- 四、项目实物与接线展示
- 项目整体实物图
- 硬件接线示意图
- 搭建要点
- 五、DHT11 通信原理
- 六、工程模块划分工程按功能进行封装:
- 七、DHT11 驱动代码
- 八、蜂鸣器与 LED 控制
- 九、按键读取按键使用内部上拉输入,按下时引脚被拉低。
- 九、主程序逻辑主程序中设置了四个阈值:
- 十、调试过程中遇到的问题
- 总结
前言
最近使用 STM32F103C8T6 做了一个温湿度监测与报警小项目。项目使用 DHT11 采集温湿度数据,使用 0.96 英寸 OLED 实时显示温度、湿度和报警状态。当温湿度超过设定范围时,红灯亮、蜂鸣器间歇报警;正常状态下绿灯亮。同时加入了按键调节阈值和错误码显示。
这个项目比较适合 STM32 入门练习,主要涉及 GPIO 输入输出、DHT11 单总线通信、OLED 显示、蜂鸣器控制、LED 状态指示、按键读取以及模块化编程。
一、项目功能
本项目实现功能如下:
- OLED 实时显示温度和湿度
- 支持温度上限、温度下限、湿度上限、湿度下限判断
- 超出阈值时红灯亮,蜂鸣器间歇报警
- 正常范围内绿灯亮,蜂鸣器关闭
- 使用两个按键调节报警阈值
- DHT11 读取异常时显示不同错误状态
- 按功能封装 DHT11、OLED、Buzzer、LED、Key 等模块
二、硬件清单
| 硬件 | 说明 |
|---|---|
| STM32F103C8T6 | 主控芯片 |
| DHT11 | 温湿度传感器 |
| 0.96 英寸 OLED | I²C 显示屏 |
| 有源蜂鸣器 | 低电平触发 |
| 红色 LED | 超阈值报警 |
| 绿色 LED | 正常状态 |
| 按键 x2 | 调节阈值 |
| ST‑Link | 程序下载 |
| 杜邦线、面包板 | 硬件连接 |
三、引脚连接
四、项目实物与接线展示
项目整体实物图
示意图:STM32F103C8T6 最小系统板居中放置,左侧通过杜邦线连接 DHT11 温湿度传感器,右侧连接 0.96 英寸 OLED 显示屏,上方为蜂鸣器与红绿 LED,下方为两个按键,整体通过面包板完成供电与信号连接。
硬件接线示意图
示意图:以 STM32F103C8T6 为核心,DHT11 的 DATA 接 PA6,OLED 的 SCL/SDA 接 PB8/PB9,蜂鸣器接 PB12,红灯 LED1 接 PA1,绿灯 LED2 接 PA2,KEY1 接 PB1,KEY2 接 PB11,所有模块共地并接入 3.3V/5V 电源。
搭建要点
- 先接电源与地线,确认 STM32 与面包板共地,避免信号电平不一致。
- DHT11 的 DATA 线尽量短,减少干扰;上拉电阻可选用 4.7kΩ 至 10kΩ。
- OLED 使用软件 I²C,SCL 与 SDA 不要接反,否则无法正常显示。
- 蜂鸣器与 LED 均为低电平触发,接线时注意正负极方向。
- 按键使用内部上拉,按下时引脚被拉低,接线时一端接 GPIO、另一端接地。
- 上电前用万用表检查电源与地之间是否短路,再下载程序。
| 模块 | STM32 引脚 | 说明 |
|---|---|---|
| DHT11 DATA | PA6 | 单总线数据引脚 |
| OLED SCL | PB8 | 软件 I²C 时钟线 |
| OLED SDA | PB9 | 软件 I²C 数据线 |
| 蜂鸣器 | PB12 | 低电平触发 |
| 红灯 LED1 | PA1 | 低电平点亮 |
| 绿灯 LED2 | PA2 | 低电平点亮 |
| KEY1 | PB1 | 低电平按下 |
| KEY2 | PB11 | 低电平按下 |
五、DHT11 通信原理
DHT11 使用单总线通信方式,只需要一根 DATA 线完成数据传输。
读取一次数据的大致流程如下:
- STM32 将 DATA 拉低至少 18 ms,发送起始信号。
- STM32 拉高 DATA 20~40 µs。
- STM32 将 DATA 引脚切换为输入模式。
- DHT11 响应低电平约 80 µs。
- DHT11 响应高电平约 80 µs。
- DHT11 连续发送 40 位数据。
- STM32 接收 5 个字节并进行校验。
DHT11 返回 5 个字节:
| 字节 | 含义 |
|---|---|
| Byte1 | 湿度整数 |
| Byte2 | 湿度小数 |
| Byte3 | 温度整数 |
| Byte4 | 温度小数 |
| Byte5 | 校验和 |
校验方式:
check_sum==humi_int+humi_dec+temp_int+temp_dec- DHT11 每一位数据的判断方式:
- 每一位开始先有约 50 µs 低电平
- 高电平约 26~28us 表示 0
- 高电平约 70us 表示 1
六、工程模块划分工程按功能进行封装:
| 文件 | 功能 |
|---|---|
| DHT11.c / DHT11.h | DHT11 初始化、响应检测、数据读取 |
| OLED.c / OLED.h | OLED 显示 |
| BUZZER.c / BUZZER.h | 蜂鸣器控制 |
| LED.c / LED.h | 红绿灯控制 |
| KEY.c / KEY.h | 按键读取 |
| Delay.c / Delay.h | 微秒、毫秒延时 |
| main.c | 主程序逻辑 |
七、DHT11 驱动代码
- 初始化与模式切换PA6 是 DHT11 的 DATA 引脚。由于 DHT11 是单总线通信,STM32 需要在输出模式和输入模式之间切换。
#include"stm32f10x.h"#include"Delay.h"voidDHT11_ModeOut(void){GPIO_InitTypeDef GPIO_InitStructure;GPIO_InitStructure.GPIO_Mode=GPIO_Mode_Out_OD;GPIO_InitStructure.GPIO_Pin=GPIO_Pin_6;GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;GPIO_Init(GPIOA,&GPIO_InitStructure);}voidDHT11_ModeIn(void){GPIO_InitTypeDef GPIO_InitStructure;GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IPU;GPIO_InitStructure.GPIO_Pin=GPIO_Pin_6;GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;GPIO_Init(GPIOA,&GPIO_InitStructure);}voidDHT11_Init(void){RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);DHT11_ModeOut();GPIO_SetBits(GPIOA,GPIO_Pin_6);}这里使用开漏输出 GPIO_Mode_Out_OD,适合这种一根数据线由双方轮流控制的通信方式。
- 发送起始信号
voidDHT11_Start(void){DHT11_ModeOut();GPIO_ResetBits(GPIOA,GPIO_Pin_6);Delay_ms(20);GPIO_SetBits(GPIOA,GPIO_Pin_6);Delay_us(30);}- 检测 DHT11 响应
uint8_tDHT11_Check(void){uint8_tretry=0;while(GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_6)==SET){retry++;Delay_us(1);if(retry>100)return1;}retry=0;while(GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_6)==RESET){retry++;Delay_us(1);if(retry>100)return1;}retry=0;while(GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_6)==SET){retry++;Delay_us(1);if(retry>100)return1;}return0;}返回值含义:
0:响应正常
1:无响应
这个函数依次等待:
- DHT11 拉低
- DHT11 拉高
- DHT11 再次拉低,进入数据发送阶段
- 读取 1 bit 数据
uint8_tDHT11_ReadBit(void){uint8_tbit=0;while(GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_6)==RESET);Delay_us(35);if(GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_6)==RESET){bit=0;}else{bit=1;while(GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_6)==SET);}returnbit;}这里的核心是延时 35us 后判断电平:
- 如果已经变低,说明高电平时间短,该位为 0
- 如果仍然是高电平,说明高电平时间长,该位为 1
需要注意:如果读到 1,要等待高电平结束,否则下一位可能会错位,导致校验失败。
- 读取 1 byte 数据
uint8_tDHT11_ReadByte(void){uint8_tByte=0x00;uint8_ti;for(i=0;i<8;i++){Byte=Byte<<1;Byte|=DHT11_ReadBit();}returnByte;}DHT11 是高位先发送,所以每读一位,就将当前字节左移一位,再把新读到的 bit 放到最低位。
6. 读取完整温湿度数据
uint8_tDHT11_ReadData(uint8_t*temp,uint8_t*humi){uint8_thumi_int;uint8_thumi_dec;uint8_ttemp_int;uint8_ttemp_dec;uint8_tcheck_sum;DHT11_Start();DHT11_ModeIn();if(DHT11_Check()==1){return1;}humi_int=DHT11_ReadByte();humi_dec=DHT11_ReadByte();temp_int=DHT11_ReadByte();temp_dec=DHT11_ReadByte();check_sum=DHT11_ReadByte();if((humi_int+humi_dec+temp_int+temp_dec)!=check_sum){return2;}*temp=temp_int;*humi=humi_int;return0;}返回值含义:
| 返回值 | 含义 |
|---|---|
| 0 | 读取成功 |
| 1 | DHT11 无响应 |
| 2 | 校验失败 |
八、蜂鸣器与 LED 控制
- 蜂鸣器控制蜂鸣器为低电平触发,因此拉低响,拉高关闭。
voidBUZZER_Init(void){RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);GPIO_InitTypeDef GPIO_INSTRUCTURE;GPIO_INSTRUCTURE.GPIO_Mode=GPIO_Mode_Out_PP;GPIO_INSTRUCTURE.GPIO_Pin=GPIO_Pin_12;GPIO_INSTRUCTURE.GPIO_Speed=GPIO_Speed_50MHz;GPIO_Init(GPIOB,&GPIO_INSTRUCTURE);GPIO_SetBits(GPIOB,GPIO_Pin_12);}voidBUZZER_ON(void){GPIO_ResetBits(GPIOB,GPIO_Pin_12);}voidBUZZER_OFF(void){GPIO_SetBits(GPIOB,GPIO_Pin_12);}- LED 控制
LED 同样采用低电平点亮。
voidLED_Init(void){RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);GPIO_InitTypeDef GPIO_INSTRUCTURE;GPIO_INSTRUCTURE.GPIO_Mode=GPIO_Mode_Out_PP;GPIO_INSTRUCTURE.GPIO_Pin=GPIO_Pin_1|GPIO_Pin_2;GPIO_INSTRUCTURE.GPIO_Speed=GPIO_Speed_50MHz;GPIO_Init(GPIOA,&GPIO_INSTRUCTURE);GPIO_SetBits(GPIOA,GPIO_Pin_1|GPIO_Pin_2);}voidLED1_ON(void){GPIO_ResetBits(GPIOA,GPIO_Pin_1);}voidLED1_OFF(void){GPIO_SetBits(GPIOA,GPIO_Pin_1);}voidLED2_ON(void){GPIO_ResetBits(GPIOA,GPIO_Pin_2);}voidLED2_OFF(void){GPIO_SetBits(GPIOA,GPIO_Pin_2);}本项目中:
LED1 接红灯,
LED2 接绿灯。
九、按键读取按键使用内部上拉输入,按下时引脚被拉低。
voidKey_Init(void){RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);GPIO_InitTypeDef GPIO_INITSTRUCTURE;GPIO_INITSTRUCTURE.GPIO_Mode=GPIO_Mode_IPU;GPIO_INITSTRUCTURE.GPIO_Pin=GPIO_Pin_1|GPIO_Pin_11;GPIO_INITSTRUCTURE.GPIO_Speed=GPIO_Speed_50MHz;GPIO_Init(GPIOB,&GPIO_INITSTRUCTURE);}uint8_tKey_GetNum(void){uint8_tKeyNum=0;if(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_1)==0){Delay_ms(20);while(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_1)==0);Delay_ms(20);KeyNum=1;}if(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_11)==0){Delay_ms(20);while(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_11)==0);Delay_ms(20);KeyNum=2;}returnKeyNum;}按键功能设计:
| 按键 | 功能 |
|---|---|
| KEY1 | 切换当前设置项 |
| KEY2 | 当前阈值加 1 |
九、主程序逻辑主程序中设置了四个阈值:
uint8_ttemp_high=40;uint8_ttemp_low=20;uint8_thumi_high=65;uint8_thumi_low=30;分别表示:
| 变量 | 含义 |
|---|---|
| temp_high | 温度上限 |
| temp_low | 温度下限 |
| humi_high | 湿度上限 |
| humi_low | 湿度下限 |
主函数如下:
#include"stm32f10x.h"#include"Delay.h"#include"OLED.h"#include"BUZZER.h"#include"DHT11.h"#include"LED.h"#include"KEY.h"uint8_ttemp,humi,status;uint8_ttemp_high=40;uint8_ttemp_low=20;uint8_thumi_high=65;uint8_thumi_low=30;uint8_tKeyNum;uint8_tset_item=0;voidShow_SetItem(void){if(set_item==0){OLED_ShowString(4,1,"Set TH:");OLED_ShowNum(4,8,temp_high,2);OLED_ShowString(4,10," ");}elseif(set_item==1){OLED_ShowString(4,1,"Set TL:");OLED_ShowNum(4,8,temp_low,2);OLED_ShowString(4,10," ");}elseif(set_item==2){OLED_ShowString(4,1,"Set HH:");OLED_ShowNum(4,8,humi_high,2);OLED_ShowString(4,10," ");}else{OLED_ShowString(4,1,"Set HL:");OLED_ShowNum(4,8,humi_low,2);OLED_ShowString(4,10," ");}}intmain(void){OLED_Init();BUZZER_Init();DHT11_Init();LED_Init();Key_Init();while(1){KeyNum=Key_GetNum();if(KeyNum==1){set_item++;if(set_item>3){set_item=0;}}elseif(KeyNum==2){if(set_item==0){temp_high++;}elseif(set_item==1){temp_low++;}elseif(set_item==2){humi_high++;}else{humi_low++;}}status=DHT11_ReadData(&temp,&humi);if(status==0){OLED_ShowString(1,1,"humi: % ");OLED_ShowString(2,1,"temp: C ");OLED_ShowNum(1,6,humi,2);OLED_ShowNum(2,6,temp,2);if(temp>temp_high||temp<temp_low||humi>humi_high||humi<humi_low){LED1_ON();LED2_OFF();OLED_ShowString(3,1,"Alarm: ON ");BUZZER_ON();Delay_ms(200);BUZZER_OFF();Delay_ms(200);Show_SetItem();}else{LED2_ON();LED1_OFF();BUZZER_OFF();OLED_ShowString(3,1,"Alarm: OFF ");Show_SetItem();}}elseif(status==1){BUZZER_OFF();LED1_OFF();LED2_OFF();OLED_ShowString(1,1,"NO RESPONSE ");OLED_ShowString(2,1,"Code:1 ");OLED_ShowString(3,1,"Sensor Error ");OLED_ShowString(4,1," ");}elseif(status==2){BUZZER_OFF();LED1_OFF();LED2_OFF();OLED_ShowString(1,1,"CHECKSUM ERROR ");OLED_ShowString(2,1,"Code:2 ");OLED_ShowString(3,1,"Data Error ");OLED_ShowString(4,1," ");}else{BUZZER_OFF();LED1_OFF();LED2_OFF();OLED_ShowString(1,1,"UNKNOWN ERROR ");OLED_ShowString(2,1," ");OLED_ShowString(3,1," ");OLED_ShowString(4,1," ");}Delay_ms(1000);}}程序逻辑说明:
- 读取按键,判断是否需要切换或修改阈值。
- 读取 DHT11 温湿度。
- 如果读取成功,显示温湿度。
- 判断温湿度是否超出上下限。
- 超限时红灯亮、蜂鸣器间歇报警。
- 正常时绿灯亮、蜂鸣器关闭。
- 如果 DHT11 读取失败,显示对应错误码。
十、调试过程中遇到的问题
- DHT11 第一次检测成功,后面一直失败
现象:复位后第一次 OLED 显示 correct,后面一直 error
原因:
第一次读取后,PA6 被切换成输入模式。下一次开始通信时,如果没有重新切回输出模式,STM32 就无法拉低 DATA 发送起始信号。
解决方法:
在DHT11_Start()函数开头加入:
DHT11_ModeOut();- status = 2,校验失败
现象:
DHT11 有响应,但是校验失败
原因:
读取 bit 时,如果该位是 1,35us 采样时 DATA 仍然处于高电平。如果此时直接返回,下一次读取会提前进入,导致数据错位。
解决方法:
读到 1 后等待高电平结束:
while(GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_6)==SET);- 蜂鸣器低于阈值后仍然响
原因:
GPIO 输出状态会保持上一次写入的状态。调用BUZZER_ON()后,如果后续分支没有明确调用BUZZER_OFF(),蜂鸣器会继续响。
解决方法:
在正常、报警、错误分支中都明确控制蜂鸣器状态。 OLED_Clear()放在循环中导致闪烁
原因:OLED_Clear()会整屏清空,如果在主循环中频繁调用,会出现明显闪烁。
解决方法:
使用空格覆盖旧字符,而不是每次清屏。例如:
OLED_ShowString(1,1,"humi: % ");OLED_ShowString(2,1,"temp: C ");总结
通过这个项目,我熟悉了 STM32F103C8T6 的 GPIO 输入输出、DHT11 单总线时序、OLED 显示、蜂鸣器控制、LED 状态指示、按键读取以及模块化编程方法。
虽然项目整体不算复杂,但它完整地包含了硬件连接、外设配置、传感器读取、数据显示、报警控制、按键调参和问题调试等过程,非常适合作为 STM32 入门阶段的综合练习项目。
后续可以继续扩展:
1、使用定时器替代阻塞延时
2、使用外部中断读取按键
3、将阈值保存到 Flash,断电不丢失
4、通过串口输出温湿度数据
5、加入 ADC 采集其他模拟传感器数据