简介:一套基于STM32F407的智能风扇系统设计资料,面向嵌入式单片机学习者、课程设计及电子竞赛参赛者。系统以人体感应、温度采集与火焰检测为核心,能自动判断是否有人、环境是否过热或存在火灾险情,并据此调节风扇启停与发出警报,从而避免无人时持续运转的电力浪费,也提升危险场景下的响应速度。压缩包共84个文件、约14.54MB,包含H/C语言源文件、Keil工程配置(uvprojx)、演示视频mp4、答辩文档doc等,从源码到可执行文件一应俱全。源码按模块拆分为多组头文件与C文件,传感器驱动、应用逻辑分层清晰,便于理解外设初始化和中断处理;项目采用CMSIS标准目录结构,方便直接移植;两个演示视频展示实际运行效果,配套答辩讲稿帮助梳理设计思路。已有1943人学习下载,适合需要快速搭建智能风扇或同类温控、安防联动项目的开发者参考。
1. 为什么风扇要“智能”比想象中复杂
一个风扇加三个传感器,图纸上只多几根线,实际调起来却不是一回事。PIR输出数字电平,DS18B20返回单总线时序,火焰传感器输出模拟电压,三路信号格式和实时性完全不同。只对原理图接线,不区分GPIO、ADC、外部中断的边界,后面每一步都在返工。
这套基于STM32F407的智能风扇系统,核心是三个判断:是否有人、温度是否超标、是否有火焰,再组合成风扇启停与调速策略。它解决两类问题:人走风扇不停导致的电力浪费,以及高温或火情出现后人工响应不及时。对嵌入式开发新手和智能家居原型设计,这个结构都值得拆。
接下来依次讲硬件选型、驱动实现、调度逻辑,以及调试和答辩演示的细节。
2. STM32F407智能风扇的系统选型与硬件接线
2.1 为什么选STM32F407:外设资源和实时性边界
先回答一个实际的问题:这个项目用8位单片机也能做,为什么选STM32F407?关键在三个地方。PIR触发一次外部中断,系统要在极短时间内完成状态置位,中断响应延迟过大会丢掉人进人出的沿信号;DS18B20是单总线协议,对微秒级时间窗要求很高,主频低的芯片在几十微秒的时隙里容易被其他逻辑打乱;风扇调速需要定时器输出kHz级PWM,分辨率低于8位时低速档会出现明显的电机抖振。STM32F407的168MHz主频、NVIC可编程优先级和16位定时器,把这三个条件一次满足。
嵌入式硬件开发里还有一个容易被低估的点:外设之间是否存在资源竞争。如果火焰传感器的ADC采样和PWM占空比更新共用同一个DMA流,某次ADC触发可能延迟PWM更新,风扇转速就会偶发抖动。STM32F407有独立ADC、独立定时器和独立EXTI中断线,我一般把三个传感器拆开放到互不争夺的外设上,而不是把所有信号挤在同一个端口上,这样后续扩展其他功能时不用重构底层。
这里的固件选经典标准外设库,不用HAL库。F407的标准库工程目录一般是CMSIS、Project、USER三个文件夹,启动文件、外设库和用户代码分层清晰。拿这个项目举例,解压出来就是Fan_Project,其下正好对应这三层结构。标准库代码里直接操作外设寄存器结构体,一件事对应一个库函数,答辩时对着工程逐段讲很直观。HAL库当然也能跑,但初始化代码包含大量句柄绑定逻辑,新手讲“为什么这么配置”时容易被外设句柄绕进去。对课设和第一版产品原型,标准库更合适。
2.2 三路传感器的接口选型
工程里用到的三路传感器列成一张表,方便对照选型:
| 传感器 | 输出类型 | F407外设 | 数据格式 | 典型参数 |
|---|---|---|---|---|
| PIR人体红外(HC-SR501) | 数字电平 | GPIO外部中断EXTI0 | 高/低电平 | 探测距离3~7米可调 |
| DS18B20温度传感器 | 单总线 | GPIO模拟时序 | 16位温度数据 | -55~125℃,分辨率0.0625℃ |
| 火焰传感器模块 | 模拟电压 | ADC1通道PA1 | 0~3.3V | 火焰探测距离约1米 |
选型逻辑很简单。PIR模块自带红外热释电信号处理,输出稳定的数字电平,不需要单片机做滤波,直接进GPIO外部中断。DS18B20挂在任意空闲GPIO上,不需要占用硬件I2C或SPI,F407的IO翻转速度完全能模拟单总线时序。火焰模块自带LM393比较器,DO脚可以输出数字量,但接了AO模拟输出才能判断火焰远近,这个电压信号进ADC1的PA1通道。
接线时有两个坑经常导致返工。第一个是DS18B20的数据线必须配4.7kΩ上拉电阻到3.3V。买模块板时上拉电阻已经在板上,裸芯片就需要自己加。若上拉到5V,数据线高电平超过GPIO耐压,短时间工作正常,时间一长引脚内部保护二极管老化,读数会随机跳变。第二个是火焰模块AO输出的分压。模块用5V供电时,AO满量程也是5V,直接进PA1会超出ADC量程,我用10kΩ和5.6kΩ电阻分压:
/* 火焰AO输出0~5V,经10k/5.6k分压后进PA1 满量程电压 = 5 * 5600 / (10000 + 5600) = 1.79V */ #define FIRE_VREF 1.79f #define FIRE_ADC_MAX 4095PA1作为ADC输入时不要开启内部上拉。分压网络已经有确定的对地阻抗,内部上拉会改变分压比例,读出来的电压整体偏高。
2.3 风扇驱动电路与引脚映射
风扇不能直接挂在STM32F407的GPIO上。GPIO推挽输出虽然能瞬间给出20mA以上电流,但风扇正常工作电流在200mA量级,启动瞬间更高,直接驱动会烧内部驱动管。我用N沟道MOS管AO3400做功率开关,栅极串一个100Ω电阻接PWM输出脚,源极接地,漏极接风扇负极,风扇正极接5V。风扇是感性负载,PWM关断瞬间会产生反向电动势,必须在风扇两端反向并联一颗1N5819肖特基二极管,把这个尖峰钳位到电源轨内,否则MOS管的DS电压会被瞬间拉到几十伏,一次就击穿。
实际工程里我用的引脚映射如下,信号分布在F407不同端口上,既方便跳线,也方便在逻辑上区分信号类型:
| 信号 | MCU引脚 | 连接目标 | 说明 |
|---|---|---|---|
| PIR_OUT | PE0 | HC-SR501 DO | 检测到人时输出高电平 |
| DQ | PE1 | DS18B20数据 | 单总线,需4.7kΩ上拉 |
| FIRE_AO | PA1 | 火焰模块AO | ADC1_IN1,分压后输入 |
| FAN_PWM | PB6 | MOS管栅极 | TIM4_CH1,10kHz PWM |
| GND | GND | 各模块地 | 与MCU共地 |
这套映射还有个好处:PIR和DS18B20在不同端口,即使某个引脚损坏,只需改宏定义,不影响其他模块。火焰ADC通道和PWM定时器通道相互独立,采样和输出之间不会发生外设资源抢占。电源入口建议加一个470uF电解电容,否则风扇启动瞬间会把5V拉低,轻则PIR电平抖动,重则MCU直接复位,这个现象在USB供电时尤其明显。
3. 基于标准外设库的驱动层实现
3.1 GPIO与外部中断配置
先完成PIR的GPIO初始化和外部中断映射。
static void Sensor_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; EXTI_InitTypeDef EXTI_InitStructure; NVIC_InitTypeDef NVIC_InitStructure; RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOE, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_SYSCFG, ENABLE); /* PE0接PIR输出 */ GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN; GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL; GPIO_Init(GPIOE, &GPIO_InitStructure); /* PE1接DS18B20数据线,输入模式下等待外部上拉 */ GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN; GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL; GPIO_Init(GPIOE, &GPIO_InitStructure); /* PIR上升沿和下降沿都捕获 */ SYSCFG_EXTILineConfig(EXTI_PortSourceGPIOE, EXTI_PinSource0); EXTI_InitStructure.EXTI_Line = EXTI_Line0; EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising_Falling; EXTI_InitStructure.EXTI_LineCmd = ENABLE; EXTI_Init(&EXTI_InitStructure); NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); }这里没有开启内部上拉或下拉,因为HC-SR501模块DO引脚是推挽输出,外部电平已经确定,内部上拉反而会干扰低电平判断。EXTI触发方式选择双边沿,模块检测到人时输出高电平,延时结束后恢复低电平,两个沿都代表状态变化,必须都收。NVIC抢占优先级设为1,后面PWM更新中断如果需要开启,优先级要高于这个值,否则长时间运行后占空比会出现微小偏差。
中断服务函数里只做一件事:读电平,置全局标志。不要在中断里做温度读取或ADC转换,这些操作耗时超过1ms,会挤占主循环中对DS18B20时序的要求。
volatile uint8_t pir_flag = 0; void EXTI0_IRQHandler(void) { if (EXTI_GetITStatus(EXTI_Line0) != RESET) { if (GPIO_ReadInputDataBit(GPIOE, GPIO_Pin_0) == SET) { pir_flag = 1; } else { pir_flag = 0; } EXTI_ClearITPendingBit(EXTI_Line0); } }pir_flag是volatile全局变量,主循环每次循环都重新读取当前状态。这样设计即使PIR在极短时间反复触发,主循环也只处理最终状态,不会积压一大堆中断事件。
3.2 DS18B20单总线时序实现
DS18B20是整个工程里对时序最敏感的部分。我先用开漏输出的方式初始化PE1,利用外部上拉电阻直接模拟总线的释放与拉低,不需要反复切换GPIO模式:
#define DQ_PORT GPIOE #define DQ_PIN GPIO_Pin_1 #define DQ_LOW() GPIO_ResetBits(DQ_PORT, DQ_PIN) #define DQ_HIGH() GPIO_SetBits(DQ_PORT, DQ_PIN) /* 释放总线,由外部上拉拉高 */ #define DQ_READ() GPIO_ReadInputDataBit(DQ_PORT, DQ_PIN) uint8_t DS18B20_Reset(void) { uint8_t presence; DQ_LOW(); /* 主机拉低总线 */ delay_us(480); /* 复位脉冲不小于480us */ DQ_HIGH(); /* 释放总线 */ delay_us(70); /* 等待从机回应 */ presence = DQ_READ(); /* 0表示设备存在 */ delay_us(410); return presence; }读一位的时序更讲究。DS18B20规定主机拉低总线产生起始信号,然后在15us内读取引脚电平,这个采样窗口非常窄。如果此时PIR外部中断插入,读到的位就错了,温度值会变成0xFFFF或剧烈跳变。我在读位函数里临时关中断:
uint8_t DS18B20_ReadBit(void) { uint8_t val = 0; __disable_irq(); DQ_LOW(); delay_us(1); DQ_HIGH(); /* 释放总线,进入采样窗口 */ delay_us(10); if (DQ_READ()) val = 1; delay_us(45); __enable_irq(); return val; }延时6us结束之后主机拉高释放总线,从设备在15us内把数据放到总线上。关中断的时长大约在60us左右,对系统其他功能影响很小。如果后面移植到FreeRTOS,这里要换成临界区保护函数,而不是直接__disable_irq,否则会和任务调度器冲突。
读温度时,每次先发复位,然后跳过ROM、启动转换,等待750ms后再读暂存器:
float DS18B20_GetTemperature(void) { uint8_t low, high; int16_t raw; if (DS18B20_Reset() != 0) return -1000; /* 返回哨兵值表示设备丢失 */ DS18B20_WriteByte(0xCC); /* 跳过ROM指令 */ DS18B20_WriteByte(0x44); /* 启动温度转换 */ delay_ms(750); DS18B20_Reset(); DS18B20_WriteByte(0xCC); DS18B20_WriteByte(0xBE); /* 读暂存器 */ low = DS18B20_ReadByte(); high = DS18B20_ReadByte(); raw = (high << 8) | low; return raw * 0.0625f; /* 12位分辨率,每位0.0625℃ */ }返回-1000这个哨兵值是有意的。主循环里判断如果温度小于-50,就认为传感器丢失,使用上一次正常值或默认25℃继续运行,不让风扇因为这个故障停摆。调试时若打印出125或85,通常是上拉电阻没接或PE1的ODR被意外改动,先查硬件再查代码。
3.3 定时器PWM输出与ADC采样通道
风扇调速用TIM4的通道1,输出引脚是PB6。初始化代码如下:
void FAN_PWM_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL; GPIO_Init(GPIOB, &GPIO_InitStructure); GPIO_PinAFConfig(GPIOB, GPIO_PinSource6, GPIO_AF_TIM4); TIM_TimeBaseStructure.TIM_Period = 8400 - 1; TIM_TimeBaseStructure.TIM_Prescaler = 0; /* 84MHz直接计数 */ TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM4, &TIM_TimeBaseStructure); TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse = 0; TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; TIM_OC1Init(TIM4, &TIM_OCInitStructure); TIM_Cmd(TIM4, ENABLE); }参数上要算一笔账:TIM4挂在APB1上,系统主频168MHz时APB1定时器时钟是84MHz。预分频设0,即84MHz直接计数,自动重装值8400-1,PWM频率就是84MHz/8400=10kHz。10kHz对直流无刷风扇是合适的选择,MOS开关损耗可控,也听不到可闻噪声。占空比则由TIM_Pulse控制,CCR值越大高电平时间越长。几个常用档位我整理成表:
| 风速档位 | CCR1值 | 占空比 | 适用场景 |
|---|---|---|---|
| 停止 | 0 | 0% | 无人且温度低 |
| 低速 | 2100 | 25% | 有人且室温正常 |
| 高速 | 5250 | 62.5% | 温度超过上限 |
| 全速 | 8400 | 100% | 火警状态 |
完整档位低档从25%起步,而不是从0%开始,这是有意为之。直流风扇PWM占空比低于20%时,电机转矩不足以克服静摩擦,会出现“通电但不转”的状态,电流还在白白消耗。需要用全速PWM启动1秒再切回低速,工程里叫软启动。
火焰传感器的ADC配置和读取也放这一层:
void ADC1_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; ADC_InitTypeDef ADC_InitStructure; RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AN; GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL; GPIO_Init(GPIOA, &GPIO_InitStructure); ADC_InitStructure.ADC_Resolution = ADC_Resolution_12b; ADC_InitStructure.ADC_ScanConvMode = DISABLE; ADC_InitStructure.ADC_ContinuousConvMode = DISABLE; ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; ADC_InitStructure.ADC_NbrOfConversion = 1; ADC_Init(ADC1, &ADC_InitStructure); ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 1, ADC_SampleTime_56Cycles); ADC_Cmd(ADC1, ENABLE); } uint16_t GetFireADC(void) { ADC_SoftwareStartConv(ADC1); while (ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET); return ADC_GetConversionValue(ADC1); }采样时间选择56周期,对火焰传感器这种变化较慢的模拟信号足够,采样值稳定。每次转换由软件触发,主循环调用一次就阻塞等待结果,不引入DMA中断,逻辑最简单,也最不容易出错。
4. 智能调度逻辑:从传感器数据到风扇控制
4.1 状态机模型:空闲、有人、高温、火警
三路传感器组合出来的情况很多,我把它收敛成4个状态,不给每种组合单独写if-else:
| 状态 | 触发条件 | 风扇动作 |
|---|---|---|
| FAN_OFF | 无人、温度低于下限、无火焰 | 停止 |
| FAN_NORMAL | 有人、温度低于上限、无火焰 | 低速25% |
| FAN_HIGH | 温度超过上限且有人,或温度超高无人 | 高速62.5% |
| FIRE | 火焰ADC超过触发阈值 | 全速100% |
这里最关键的设计是火警状态的优先级。温度高、有人、火焰三者同时存在时,状态必须从任何当前状态强制切到FIRE。这不是简单的代码顺序问题,而是整个控制策略的安全边界。如果优先级写反,人动一下风扇反而停在低速,高温房间需要几秒才进入高速,这种响应在火灾场景下完全不能接受。
4.2 阈值与滞回比较
温度阈值不能只设一个点。只设27℃启动风扇、低于27℃关闭,当温度在26.9℃和27.1℃之间波动时,风扇会以秒级频率反复启停,MOS管很快过热。所以代码里设两个温度阈值,中间留出滞回区间:
#define TEMP_ON_SET 27.5f /* 温度超过该值进入高温档 */ #define TEMP_OFF_SET 24.5f /* 回落到该值以下才退出高温档 */ #define FIRE_TRIGGER 2400 /* 火焰ADC触发阈值 */ #define FIRE_RELEASE 1800 /* 火警解除阈值 */ typedef enum { STATE_FAN_OFF = 0, STATE_FAN_NORMAL, STATE_FAN_HIGH, STATE_FIRE } FanState; volatile FanState fan_state = STATE_FAN_OFF; volatile uint8_t pir_flag = 0; volatile float temp_c = 25.0f; volatile uint16_t fire_adc = 0; void FanSys_Update(void) { switch (fan_state) { case STATE_FAN_OFF: if (pir_flag || temp_c >= TEMP_ON_SET) fan_state = STATE_FAN_NORMAL; if (fire_adc >= FIRE_TRIGGER) fan_state = STATE_FIRE; break; case STATE_FAN_NORMAL: if (fire_adc >= FIRE_TRIGGER) fan_state = STATE_FIRE; else if (!pir_flag && temp_c < TEMP_OFF_SET) fan_state = STATE_FAN_OFF; else if (temp_c >= TEMP_ON_SET) fan_state = STATE_FAN_HIGH; break; case STATE_FAN_HIGH: if (fire_adc >= FIRE_TRIGGER) fan_state = STATE_FIRE; else if (temp_c <= TEMP_OFF_SET) fan_state = STATE_FAN_NORMAL; break; case STATE_FIRE: if (fire_adc < FIRE_RELEASE) fan_state = (pir_flag) ? STATE_FAN_HIGH : STATE_FAN_OFF; break; } }逻辑上有两个注意点。第一,每个状态的入口都先判断火警,保证火焰始终优先。第二,FAN_HIGH退出到FAN_NORMAL用的是24.5℃,进入时是27.5℃,这3℃就是滞回带。温度从28℃缓慢降到24℃,风扇才从高速退回低速,不会在27.5℃附近来回切换。FIRE_TRIGGER和FIRE_RELEASE之间也有600个ADC码值的滞回距离,防止火焰在临界距离闪烁导致状态频繁抖动。
风扇转速输出单独放一个函数,状态切换和输出驱动分离:
static const uint16_t duty_table[4] = {0, 2100, 5250, 8400}; void FanSys_Apply(void) { if (fan_state == STATE_FIRE) { TIM_SetCompare1(TIM4, duty_table[STATE_FIRE]); } else { TIM_SetCompare1(TIM4, duty_table[fan_state]); } }占空比数组的索引直接用枚举值,读代码时一眼能看出每个状态对应的PWM输出。FIRE状态单独处理,是防止有人后续调整枚举顺序导致数组错位,这个细节在工程维护时很有价值。
4.3 自动/手动模式与安全兜底
成品风扇通常会留一个物理按键切换自动和手动模式。函数逻辑如下:
uint8_t manual_mode = 0; uint8_t manual_level = 1; void FanSys_ManualSwitch(void) { if (manual_mode) { manual_mode = 0; FanSys_Update(); /* 切回自动,立即按当前状态调整 */ } else { manual_mode = 1; TIM_SetCompare1(TIM4, duty_table[manual_level]); } }进入手动模式后状态机仍然继续运行,只是输出不再自动更新,由按键调整扇速档位。这里有一个安全兜底:一旦fire_adc超过FIRE_TRIGGER,主循环检测后立即清除manual_mode,强制回到自动并全速运行。做这个兜底的原因很实际,手动模式的目的是省电和静音,但火警出现时任何省电策略都要给安全让路。
主循环按固定节奏执行整个调度:
while (1) { temp_c = DS18B20_GetTemperature(); if (temp_c < -50) temp_c = 25.0f; /* 传感器丢失时维持默认温度 */ fire_adc = GetFireADC(); if (!manual_mode) { FanSys_Update(); FanSys_Apply(); } else { if (fire_adc >= FIRE_TRIGGER) { /* 火警优先于手动 */ manual_mode = 0; FanSys_Update(); FanSys_Apply(); } } printf("temp=%d.%d fire=%u state=%d pwm=%u\r\n", (int)temp_c, (int)(temp_c * 10) % 10, fire_adc, fan_state, TIM_GetCapture1(TIM4)); delay_ms(200); IWDG_ReloadCounter(); /* 喂狗 */ }200ms一次的循环节拍足够快,PIR中断置位的事件最迟200ms内就会被处理,人体走动在风扇响应上察觉不到延迟。独立看门狗超时会导致芯片复位,复位后PWM引脚默认空闲,风扇停止,相当于一个故障安全位置。如果后续移植到FreeRTOS,喂狗操作要放在低优先级任务里,否则空闲任务占用CPU时看门狗容易误复位。
5. 串口调试、阈值标定与答辩演示技巧
5.1 用串口打印状态切换点
多路传感器同时工作,只靠眼睛盯风扇转速很难判断状态机到底走了哪一步。我习惯重定向printf到USART2,主循环每秒打印一组完整数据:
int fputc(int ch, FILE *f) { USART_SendData(USART2, (uint8_t)ch); while (USART_GetFlagStatus(USART2, USART_FLAG_TXE) == RESET); return ch; }printf的参数顺序和状态机一一对应,串口屏上看一行数据就能定位问题。调试时常用的观测点整理如下:
| 动作 | 串口显示 | 说明 |
|---|---|---|
| 人走进探测区 | pir_flag=1,state变1 | HC-SR501距离调到最小 |
| 吹风机热风靠近 | state变2,pwm=5250 | 热源不要直接贴传感器 |
| 打火机火花靠近 | state变3,pwm=8400 | 保持30cm以上,闪一下就移开 |
| 人走开后延时结束 | state回1或0 | 先调小PIR延时电位器 |
如果state在0和1之间无规律跳,多半是pir_flag没有正确清掉;如果温度打印出-1000或125,说明DS18B20复位失败,先检查上拉电阻。
5.2 阈值标定方法
温度阈值不能凭感觉定。把DS18B20和水银温度计放在同一位置,等5分钟后读取单片机打印值,记录偏差。如果整体偏高2℃,代码里加一个偏移量补偿,不需要改硬件。火焰阈值用打火机在20cm、15cm、10cm处分别记录ADC值,取触发距离对应的AD值再留20%余量,每组数据至少测三次取中位数。
5.3 演示视频的录制建议
演示视频分两个场景拍。第一个拍硬件完整运行,从PIR触发、风扇启动到温度上升后切换高速,画面同时包含开发板和传感器模块。第二个拍示波器或串口画面,把示波器探头夹在PB6引脚,显示频率和占空比读数,同时画面里能看到风扇转速变化,这样切换档位时占空比数值和风扇转速同步变化,硬件证据链就完整了。
本文还有配套的精品资源,点击获取