news 2026/9/10 3:25:35

STM32F407多传感器智能风扇:从硬件接线到状态机控制

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32F407多传感器智能风扇:从硬件接线到状态机控制

简介:一套基于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通道PA10~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 4095

PA1作为ADC输入时不要开启内部上拉。分压网络已经有确定的对地阻抗,内部上拉会改变分压比例,读出来的电压整体偏高。

2.3 风扇驱动电路与引脚映射

风扇不能直接挂在STM32F407的GPIO上。GPIO推挽输出虽然能瞬间给出20mA以上电流,但风扇正常工作电流在200mA量级,启动瞬间更高,直接驱动会烧内部驱动管。我用N沟道MOS管AO3400做功率开关,栅极串一个100Ω电阻接PWM输出脚,源极接地,漏极接风扇负极,风扇正极接5V。风扇是感性负载,PWM关断瞬间会产生反向电动势,必须在风扇两端反向并联一颗1N5819肖特基二极管,把这个尖峰钳位到电源轨内,否则MOS管的DS电压会被瞬间拉到几十伏,一次就击穿。

实际工程里我用的引脚映射如下,信号分布在F407不同端口上,既方便跳线,也方便在逻辑上区分信号类型:

信号MCU引脚连接目标说明
PIR_OUTPE0HC-SR501 DO检测到人时输出高电平
DQPE1DS18B20数据单总线,需4.7kΩ上拉
FIRE_AOPA1火焰模块AOADC1_IN1,分压后输入
FAN_PWMPB6MOS管栅极TIM4_CH1,10kHz PWM
GNDGND各模块地与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值占空比适用场景
停止00%无人且温度低
低速210025%有人且室温正常
高速525062.5%温度超过上限
全速8400100%火警状态

完整档位低档从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变1HC-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引脚,显示频率和占空比读数,同时画面里能看到风扇转速变化,这样切换档位时占空比数值和风扇转速同步变化,硬件证据链就完整了。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 3:24:21

SEO推广工具的数据分析功能:从排名监控到流量决策

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/10 3:23:30

C++20 std::ranges 管道性能探秘:策略内联与编译期优化

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/10 3:21:21

AI智能体技能套件:让大模型从“会想”到“会做”

做AI智能体的同学&#xff0c;应该都遇到过这种尴尬&#xff1a;模型推理能力再强&#xff0c;一旦让它查个数据库、调个外部API、按模板生成一份报表&#xff0c;就瞬间从“学霸”变成“手脚僵硬的书呆子”。最近我一直在倒腾的SenseNova-Skills&#xff0c;就是专门用来治这个…

作者头像 李华
网站建设 2026/9/10 3:17:51

食堂刷脸与园区门禁如何统一?云识客鸿蒙人脸消费机协同方案实践

去年我们园区做了一个说大不大、说小不小的改造&#xff1a;把食堂刷脸消费和园区门禁两套系统合并成了一整套协同方案。核心设备用的是云识客的鸿蒙人脸消费机&#xff0c;门禁侧保留了原有的闸机和控制器&#xff0c;但识别、底库、权限管理全部统一到同一套平台上。忙完以后…

作者头像 李华