暖风机干手器设计
红外感应+冷热风切换+湿度联锁——附"PIR原理配对管选型、继电器直推IO"翻车实录
暖风机干手器:红外感应+冷热风切换+湿度联锁,附"PIR原理配对管选型、继电器直推IO"翻车实录
- 暖风机干手器设计:红外感应+冷热风切换+湿度联锁——附"PIR原理配对管选型、继电器直推IO"翻车实录
一、系统功能拆解
先看整体架构,再逐个拆功能点:
| 功能模块 | 实现方式 | 设计要点 |
|---|---|---|
| 手部检测 | TCRT5000 红外对管 ×1 | 漫反射式,检测距离 1~15mm,比 PIR 更适合"手伸进出风口"这种近距离场景 |
| 湿度监测 | DHT11 数字温湿度传感器 | 单总线接口,1 路读出湿度+温度,用于显示和环境补偿 |
| 冷风/热风切换 | 独立按键 + 状态机 | 默认冷风,按键切换热风(继电器吸合加热模块) |
| 风扇控制 | GPIO + NPN 三极管驱动 | 检测到手 → 开风;手离开 + 延时 3s → 停风 |
| 加热控制 | SRD-05VDC-SL-C 继电器 | 必须加三极管驱动(原文直接接 IO,是硬伤,后文详述) |
| 状态显示 | OLED12864(I2C,SSD1306) | 显示当前模式、湿度、手部检测状态、运行倒计时 |
核心控制逻辑(修正版),我把它整理成一个三态状态机:
待机(STANDBY) ──检测到手──> 运行(RUN,冷风或热风) ──手离开且3s倒计时结束──> 待机
│ │
└──── 按键切换 冷风/热风 ────┘(任意状态可切)
安全联锁(硬性规则,优先级最高):
1. 加热继电器吸合 ⇒ 风扇必须同时运转(禁止"只加热不吹风",否则外壳过热)
2. 风扇停 ⇒ 先断加热,风扇再吹 5s 余风散热后才完全停转
二、模块选型对比(含成本)
2.1 主控:STM32F103C8T6 vs NXP LPC1768
| 参数 | STM32F103C8T6 | NXP LPC1768(/实际✅) |
|---|---|---|
| 主频 | 72MHz | 96MHz |
| Flash | 64KB | 实际 512KB✅ |
| SRAM | 20KB | 实际 64KB✅ |
| 价格 | ~15元(板) | ~40元(板) |
| 生态 | 标准库/HAL + 社区资料海量 | 资料相对少 |
本系统 GPIO + 单总线 + I2C 的需求,C8T6 的 20KB SRAM 绰绰有余,选它主要因为便宜一半 + 开发资料多。
2.2 湿度:DHT11 vs SHT30
| 参数 | DHT11 | SHT30 |
|---|---|---|
| 湿度精度 | ±5%RH | ±2%RH |
| 湿度量程 | 20~80%RH | 0~100%RH |
| 响应时间 | ~2s | ~0.5s |
| 接口 | 单总线 | I2C |
| 价格 | ~4元 | ~12元 |
干手器场景只做显示和环境参考,DHT11 够用,选 DHT11。这条原文选型没问题(终于有一条没翻车的)。
2.3 手部检测:TCRT5000 对管 vs E3F 对射式
选 TCRT5000,理由:
- 检测距离短(1~15mm,靠板载电位器调)恰好匹配"手伸进出风口"场景——对射式 E3F 是量程可达几十厘米的"光幕",杀鸡用牛刀且安装要求两端对齐;
- 漫反射式只需要单边安装,结构简单。
2.4 显示:OLED12864(I2C)vs TFT LCD160x80(SPI)
选 OLED12864:I2C 只占 2 根线、3.3V 直供、黑白显示本场景足够。选型正确。
2.5 BOM 成本清单
| 器件 | 型号/规格 | 数量 | 参考价 |
|---|---|---|---|
| 主控板 | STM32F103C8T6 最小系统板 | 1 | ¥15 |
| 湿度传感器 | DHT11 | 1 | ¥4 |
| 红外对管 | TCRT5000 模块 | 1 | ¥2 |
| 显示屏 | 0.96" OLED12864 I2C | 1 | ¥10 |
| 继电器 | SRD-05VDC-SL-C 模块(含驱动) | 1 | ¥3 |
| 风扇 | 5V 直流风扇 8025 | 1 | ¥8 |
| 加热模块 | 5V PTC 加热片(150~300mA 小功率演示级) | 1 | ¥10 |
| 按键、电阻、三极管、杜邦线、洞洞板 | 杂 | 若干 | ¥12 |
| 合计 | ≈¥64 |
演示原型建议用低压 PTC + 5V 风扇。真要接 220V 加热丝,必须加光耦隔离 + 熔断器 + 温度保险丝,这是安全红线,毕设答辩演示阶段别碰市电。
三、硬件接线(含原文翻车点修正)
3.1 接线总表
| STM32 引脚 | 外设 | 说明 |
|---|---|---|
| PA0 | DHT11 DATA | 单总线,4.7k~10kΩ 上拉到 3.3V |
| PA1 | TCRT5000 OUT | 数字输出(多数模块低电平=有障碍物,以实测为准) |
| PA2 | 模式按键 KEY_MODE | 一端接 PA2,另一端接 GND,内部上拉输入 |
| PA3 | 继电器控制 IN | 经三极管驱动(见 3.2) |
| PA4 | 风扇控制 | 经 NPN 三极管(S8050)驱动风扇 |
| PB6 | OLED SCL | I2C1_SCL,4.7kΩ 上拉 |
| PB7 | OLED SDA | I2C1_SDA,4.7kΩ 上拉 |
| 3.3V/5V/GND | 各模块供电 | OLED 用 3.3V;TCRT5000、继电器模块用 5V(模块自带电平兼容) |
3.2 接线坑 ①:继电器绝不能直接怼 IO 口
方案 A(推荐,最省事):直接买带驱动电路的继电器模块(三块钱那种,板载 NPN 三极管 + 续流二极管 + 光耦),模块 IN 脚输入几 mA 即可驱动,低电平触发的居多——下单前看清楚触发逻辑。
方案 B(自己搭):IO → 1kΩ → S8050 基极;S8050 集电极 → 继电器线圈 → 5V;发射极 → GND;线圈两端并联 1N4007 续流二极管(负极接 5V 侧),否则线圈断电瞬间的反电动势会打穿三极管。
/* 继电器控制(方案B,高电平导通示例)*/
#define HEATER_RELAY_PIN GPIO_Pin_3
#define HEATER_RELAY_PORT GPIOA
void Heater_Relay_On(void) { GPIO_SetBits(HEATER_RELAY_PORT, HEATER_RELAY_PIN); }
void Heater_Relay_Off(void) { GPIO_ResetBits(HEATER_RELAY_PORT, HEATER_RELAY_PIN); }
3.3 接线坑 ②:按键上拉/下拉写反
下拉电阻接地 + 未按下是高电平?这在电学上讲不通:下拉电阻的作用就是把引脚拉到低电平。原文把"内部上拉"的接法(按键另一端接 GND,未按下读高、按下读低)和"下拉"这个词焊在了一起。
3.4 接线坑 ③:加热与风扇必须硬联锁
这是干手器的安全设计底线:加热片上电而没有风流过,PTC 表面温度会在几秒内飙升,塑料外壳/海绵/纸巾靠近就是火灾隐患。接线层面我们用一路 GPIO 同时约束:
/* 安全联锁:任何情况下,开加热前必须先开风扇 */
void Safe_SetHeater(FunctionalState on)
{
if (on == ENABLE) {
Fan_On(); /* 先开风扇 */
delay_ms(200); /* 风流建立后再加热 */
Heater_Relay_On();
} else {
Heater_Relay_Off(); /* 先断加热 */
/* 风扇余风散热的延时由状态机处理,这里不直接关风扇 */
}
}
四、软件设计(完整源码)
开发环境:Keil MDK + STM32 标准外设库。程序分 5 块:主状态机、DHT11 单总线读取、TCRT5000 手部检测(消抖)、按键扫描、OLED 显示。
4.1 主程序(含三态状态机 + 安全联锁)
#include "stm32f10x.h"
#include "delay.h"
#include "dht11.h"
#include "oled.h"
#include "key.h"
#include "hand_detect.h"
#include "fan.h"
#include "heater.h"
typedef enum { MODE_COOL = 0, MODE_HEAT } DryMode;
typedef enum { ST_STANDBY, ST_RUN, ST_COOLDOWN } SysState;
static SysState g_state = ST_STANDBY;
static DryMode g_mode = MODE_COOL;
static uint32_t g_stopTick = 0; /* 手离开时间戳 */
static uint8_t g_humidity = 50;
static int16_t g_temp10 = 250; /* 温度x10 */
#define HAND_LEAVE_DELAY_MS 3000 /* 手离开后延时停风 */
#define HEATER_TAIL_DELAY_MS 5000 /* 断加热后余风散热时间 */
int main(void)
{
SysTick_Init();
OLED_Init();
DHT11_Init();
Key_Init();
HandDetect_Init();
Fan_Init();
Heater_Init();
Fan_Off(); /* 上电安全态:风扇加热全关 */
Heater_Relay_Off();
uint32_t lastDht = 0;
while (1)
{
/* ---- 1. 湿度采集(DHT11 最快 1Hz,别刷太快)---- */
if (GetTick() - lastDht >= 1500) {
lastDht = GetTick();
uint8_t hum, tmp;
if (DHT11_Read(&hum, &tmp) == 0) { g_humidity = hum; g_temp10 = tmp * 10; }
}
/* ---- 2. 按键:冷/热模式切换 ---- */
if (Key_Scan() == KEY_PRESSED) {
g_mode = (g_mode == MODE_COOL) ? MODE_HEAT : MODE_COOL;
if (g_state == ST_RUN && g_mode == MODE_HEAT)
Safe_SetHeater(ENABLE); /* 热风:联锁开加热 */
if (g_state == ST_RUN && g_mode == MODE_COOL)
Safe_SetHeater(DISABLE); /* 回冷风:只断加热,风不停 */
}
/* ---- 3. 状态机 ---- */
switch (g_state)
{
case ST_STANDBY:
if (HandDetect_IsPresent()) {
Fan_On();
if (g_mode == MODE_HEAT) Safe_SetHeater(ENABLE);
g_state = ST_RUN;
}
break;
case ST_RUN:
if (HandDetect_IsPresent()) {
g_stopTick = GetTick(); /* 手在,刷新时间戳 */
} else if (GetTick() - g_stopTick >= HAND_LEAVE_DELAY_MS) {
Safe_SetHeater(DISABLE); /* 先断加热 */
g_stopTick = GetTick();
g_state = ST_COOLDOWN; /* 余风散热 */
}
break;
case ST_COOLDOWN:
if (HandDetect_IsPresent()) { /* 散热中又伸手:立即恢复 */
Fan_On();
if (g_mode == MODE_HEAT) Safe_SetHeater(ENABLE);
g_state = ST_RUN;
} else if (GetTick() - g_stopTick >= HEATER_TAIL_DELAY_MS) {
Fan_Off(); /* 散热完成,全停 */
g_state = ST_STANDBY;
}
break;
}
/* ---- 4. OLED 显示 ---- */
OLED_Show();
delay_ms(10);
}
}
4.2 DHT11 单总线读取(时序关键)
DHT11 时序是毕设经典翻车区。主机拉低至少 18ms → 释放拉高 20~40μs → DHT11 拉低 80μs → 拉高 80μs → 连续 40bit 数据。每个 bit 以 50μs 低电平开始,高电平持续 26~28μs 表示"0"、70μs 表示"1"。
#include "dht11.h"
#include "delay.h"
#define DHT_PIN GPIO_Pin_0
#define DHT_PORT GPIOA
/* 引脚方向切换:单总线全靠这个 */
static void DHT_SetOutput(void) {
GPIO_InitTypeDef g;
g.GPIO_Pin = DHT_PIN; g.GPIO_Speed = GPIO_Speed_50MHz;
g.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_Init(DHT_PORT, &g);
}
static void DHT_SetInput(void) {
GPIO_InitTypeDef g;
g.GPIO_Pin = DHT_PIN;
g.GPIO_Mode = GPIO_Mode_IPU; /* 输入上拉 */
GPIO_Init(DHT_PORT, &g);
}
#define DHT_HIGH() GPIO_SetBits(DHT_PORT, DHT_PIN)
#define DHT_LOW() GPIO_ResetBits(DHT_PORT, DHT_PIN)
#define DHT_READ() GPIO_ReadInputDataBit(DHT_PORT, DHT_PIN)
/* 返回0=成功;hum湿度整数,tmp温度整数 */
uint8_t DHT11_Read(uint8_t *hum, uint8_t *tmp)
{
uint8_t buf[5] = {0};
uint8_t i, j, t;
/* 1. 主机起始信号 */
DHT_SetOutput();
DHT_LOW();
delay_ms(20); /* >=18ms */
DHT_HIGH();
delay_us(30); /* 20~40us */
DHT_SetInput();
/* 2. 等 DHT11 响应:先80us低,再80us高 */
t = 0; while (!DHT_READ()) { delay_us(2); if (++t > 50) return 1; }
t = 0; while ( DHT_READ()) { delay_us(2); if (++t > 50) return 1; }
/* 3. 读40bit:每bit前导50us低,高电平宽度判0/1 */
for (i = 0; i < 5; i++) {
for (j = 0; j < 8; j++) {
t = 0; while (!DHT_READ()) { delay_us(2); if (++t > 40) return 2; }
t = 0; while ( DHT_READ()) { delay_us(2); if (++t > 40) return 2; }
buf[i] <<= 1;
if (t > 15) buf[i] |= 0x01; /* 高电平>30us记为1(t单位2us) */
}
}
/* 4. 校验 */
if ((uint8_t)(buf[0]+buf[1]+buf[2]+buf[3]) != buf[4]) return 3;
*hum = buf[0];
*tmp = buf[2];
return 0;
}
两个实战注意点:
- 中断要关:如果工程里开了 SysTick 之外的中断,读 DHT11 前后用
__disable_irq()/__enable_irq()包住时序段,一次中断打进来,整帧数据错位; - 读的频率别超过 1Hz:DHT11 采集周期约 1s~2s,主循环里加 1.5s 间隔判断(见 main 代码),连读只会拿到重复值或校验错。
4.3 TCRT5000 手部检测(带消抖)
原文的消抖思路(检测到低电平 → 延时 10ms → 再确认)是对的,直接沿用并规范化:
#include "hand_detect.h"
#include "delay.h"
#define HAND_PIN GPIO_Pin_1
#define HAND_PORT GPIOA
#define HAND_ACTIVE_LEVEL 0 /* 多数TCRT5000模块:有障碍=低电平,实测为准 */
void HandDetect_Init(void)
{
GPIO_InitTypeDef g;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
g.GPIO_Pin = HAND_PIN;
g.GPIO_Mode = GPIO_Mode_IPU; /* 内部上拉输入 */
GPIO_Init(HAND_PORT, &g);
}
/* 返回1=检测到手(障碍物),0=无 */
uint8_t HandDetect_IsPresent(void)
{
if (GPIO_ReadInputDataBit(HAND_PORT, HAND_PIN) == HAND_ACTIVE_LEVEL) {
delay_ms(10); /* 消抖 */
if (GPIO_ReadInputDataBit(HAND_PORT, HAND_PIN) == HAND_ACTIVE_LEVEL)
return 1;
}
return 0;
}
delay_ms(10)阻塞式消抖在本项目里可接受(状态机 10ms 粒度);如果你的系统还挂着别的实时任务,把它改成"连续 N 次采样一致才更新标志"的非阻塞写法。
4.4 按键扫描(10ms 消抖 + 单次触发)
#include "key.h"
#include "delay.h"
#define KEY_PIN GPIO_Pin_2
#define KEY_PORT GPIOA
void Key_Init(void)
{
GPIO_InitTypeDef g;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
g.GPIO_Pin = KEY_PIN;
g.GPIO_Mode = GPIO_Mode_IPU; /* 内部上拉,按下=低 */
GPIO_Init(KEY_PORT, &g);
}
/* 返回 KEY_PRESSED(1) 或 KEY_IDLE(0),松手才算一次有效按(防长按连切) */
uint8_t Key_Scan(void)
{
static uint8_t last = 1; /* 上次电平,初始高 */
uint8_t now = GPIO_ReadInputDataBit(KEY_PORT, KEY_PIN);
if (last == 0 && now == 1) { /* 检测到上升沿:松手瞬间 */
delay_ms(10); /* 消抖 */
if (GPIO_ReadInputDataBit(KEY_PORT, KEY_PIN) == 1) {
last = now;
return KEY_PRESSED;
}
}
last = now;
return KEY_IDLE;
}
4.5 OLED 显示(I2C + SSD1306)
用现成的 ssd1306 标准库驱动(I2C 地址 0x78/0x3C,看你买的模块),显示层只需刷新四个字段:
void OLED_Show(void)
{
OLED_ShowString(0, 0, "Hand Dryer v1.0");
OLED_ShowString(0, 16, g_mode == MODE_HEAT ? "Mode: HOT " : "Mode: COOL");
OLED_ShowString(0, 28, "Hum : xx %RH");
OLED_ShowNum(56, 28, g_humidity, 2);
OLED_ShowString(0, 40, "Temp: xx.x C");
OLED_ShowNum(56, 40, g_temp10 / 10, 2);
OLED_ShowChar(68, 40, '.');
OLED_ShowNum(74, 40, g_temp10 % 10, 1);
OLED_ShowString(0, 56,
g_state == ST_STANDBY ? "State: STANDBY " :
g_state == ST_RUN ? "State: RUNNING " :
"State: COOLDOWN ");
}
五、系统调试与验证
按模块 → 联调的顺序推进,每步都有明确的"通过标准":
| 调试项 | 方法 | 通过标准 | 常见不通过原因 |
|---|---|---|---|
| TCRT5000 灵敏度 | 手从 15cm 缓慢靠近出风口 | 距离 5~8cm 处 OLED 状态跳变,且黑布/白纸测试差异不大 | 电位器没调好;深色/湿手反射弱,调灵敏度或缩短安装距离 |
| DHT11 读取 | 连续读 20 次 | 成功率 ≥95%,校验错误 ≤1 次 | 延时函数误差大(用逻辑分析仪核对 20ms/30μs);上拉电阻漏焊 |
| 继电器驱动 | 手动拉高/拉低 PA3 | 继电器"咔哒"干脆,万用表测 COM-NO 通断正常 | 触发电平搞反(很多模块低电平触发);驱动电流不够(用了裸继电器) |
| 冷/热切换 | 运行态按按键 | 冷→热:先风后热(间隔约 0.2s);热→冷:加热断、风不停 | 联锁顺序写反 |
| 停风逻辑 | 手移开 | 3s 后断热 → 余风 5s → 全停 | 时间戳没刷新导致立即停风 |
| 安全联锁 | 热风运行中直接拔风扇供电 | 加热必须同步断(保险测试,仅限演示级 PTC) | 联锁只在切换路径上做了,没覆盖异常路径 |
| 整机联调 | 模拟 20 次伸手/缩手 | 无一次误触发、无一次"只加热不吹风" | 按键长按连切(松手检测没做) |
调灵敏度时的一个实用技巧:TCRT5000 模块一般有指示灯,先不接单片机,单独供电调电位器到"手距 6cm 灯亮、10cm 灯灭",再装到整机里,比在整机上盲调快得多。
六、可扩展方向
这套 70 元的原型跑通之后,还能往这几个方向加料(每一个都够写一节"系统优化"):
- 真正的"按干没干"停风:DHT11 不行,但 SHT30 + 出风口温湿度双点对比可以粗略估计水分蒸发状态;或者直接上 2.4 寸以上的红外测温阵列(MLX90640),手表面温度回升速率和干燥程度强相关——预算 150 元左右,作为毕设创新点很能打。
- 无级调速风扇:把风扇 GPIO 换成 TIM3_CH1 PWM 输出,按红外反射强度估计手距,距离越近风速越高,模拟商用干手器的体验。
- 加热功率闭环:加一路 DS18B20 贴在出风口,用简单的位式控制(PWM 占空比分三档)把出风温度稳定在 45±3℃,避免 PTC 满功率直吹烫手。
- 使用统计:外部 Flash 或 EEPROM 记录每日使用次数、单次时长,OLED 加一个统计页面——商业设备运营数据的雏形。
- 低功耗待机:待机态 MCU 进 Stop 模式,TCRT5000 上升沿唤醒(EXTI),整机待机电流从几十 mA 压到 mA 级,做"电池供电版"就有戏了。
七、资料内容完整工程资料(原理图、PCB、代码、元件清单)

八、资料获取方式

更多推荐


所有评论(0)