红外感应+冷热风切换+湿度联锁——附"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

参数STM32F103C8T6NXP LPC1768(/实际✅)
主频72MHz96MHz
Flash64KB 实际 512KB✅
SRAM20KB 实际 64KB✅
价格~15元(板)~40元(板)
生态标准库/HAL + 社区资料海量资料相对少

本系统 GPIO + 单总线 + I2C 的需求,C8T6 的 20KB SRAM 绰绰有余,选它主要因为便宜一半 + 开发资料多。

2.2 湿度:DHT11 vs SHT30

参数DHT11SHT30
湿度精度±5%RH±2%RH
湿度量程20~80%RH0~100%RH
响应时间~2s~0.5s
接口单总线I2C
价格~4元~12元

干手器场景只做显示和环境参考,DHT11 够用,选 DHT11。这条原文选型没问题(终于有一条没翻车的)。

2.3 手部检测:TCRT5000 对管 vs E3F 对射式

选 TCRT5000,理由:

  1. 检测距离短(1~15mm,靠板载电位器调)恰好匹配"手伸进出风口"场景——对射式 E3F 是量程可达几十厘米的"光幕",杀鸡用牛刀且安装要求两端对齐;
  2. 漫反射式只需要单边安装,结构简单。

2.4 显示:OLED12864(I2C)vs TFT LCD160x80(SPI)

选 OLED12864:I2C 只占 2 根线、3.3V 直供、黑白显示本场景足够。选型正确。

2.5 BOM 成本清单

器件型号/规格数量参考价
主控板STM32F103C8T6 最小系统板1¥15
湿度传感器DHT111¥4
红外对管TCRT5000 模块1¥2
显示屏0.96" OLED12864 I2C1¥10
继电器SRD-05VDC-SL-C 模块(含驱动)1¥3
风扇5V 直流风扇 80251¥8
加热模块5V PTC 加热片(150~300mA 小功率演示级)1¥10
按键、电阻、三极管、杜邦线、洞洞板杂若干¥12
合计≈¥64

演示原型建议用低压 PTC + 5V 风扇。真要接 220V 加热丝,必须加光耦隔离 + 熔断器 + 温度保险丝,这是安全红线,毕设答辩演示阶段别碰市电。


三、硬件接线(含原文翻车点修正)

3.1 接线总表

STM32 引脚外设说明
PA0DHT11 DATA单总线,4.7k~10kΩ 上拉到 3.3V
PA1TCRT5000 OUT数字输出(多数模块低电平=有障碍物,以实测为准)
PA2模式按键 KEY_MODE一端接 PA2,另一端接 GND,内部上拉输入
PA3继电器控制 IN经三极管驱动(见 3.2)
PA4风扇控制经 NPN 三极管(S8050)驱动风扇
PB6OLED SCLI2C1_SCL,4.7kΩ 上拉
PB7OLED SDAI2C1_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;
}

两个实战注意点:

  1. 中断要关:如果工程里开了 SysTick 之外的中断,读 DHT11 前后用 __disable_irq()/__enable_irq() 包住时序段,一次中断打进来,整帧数据错位;
  2. 读的频率别超过 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 元的原型跑通之后,还能往这几个方向加料(每一个都够写一节"系统优化"):

  1. 真正的"按干没干"停风:DHT11 不行,但 SHT30 + 出风口温湿度双点对比可以粗略估计水分蒸发状态;或者直接上 2.4 寸以上的红外测温阵列(MLX90640),手表面温度回升速率和干燥程度强相关——预算 150 元左右,作为毕设创新点很能打。
  2. 无级调速风扇:把风扇 GPIO 换成 TIM3_CH1 PWM 输出,按红外反射强度估计手距,距离越近风速越高,模拟商用干手器的体验。
  3. 加热功率闭环:加一路 DS18B20 贴在出风口,用简单的位式控制(PWM 占空比分三档)把出风温度稳定在 45±3℃,避免 PTC 满功率直吹烫手。
  4. 使用统计:外部 Flash 或 EEPROM 记录每日使用次数、单次时长,OLED 加一个统计页面——商业设备运营数据的雏形。
  5. 低功耗待机:待机态 MCU 进 Stop 模式,TCRT5000 上升沿唤醒(EXTI),整机待机电流从几十 mA 压到 mA 级,做"电池供电版"就有戏了。

七、资料内容完整工程资料(原理图、PCB、代码、元件清单)

八、资料获取方式  

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐