简介:这份资源面向嵌入式初学者与电子设计竞赛选手,提供一套以STM32F103为核心、融合PWM调速与多传感器联动的智能风扇完整工程。项目通过HCSR501人体红外感应实现有人自动开启、无人自动关闭,并借助DHT11温湿度传感器采集环境数据,动态调节风扇转速,兼顾节能与舒适体验,是学习微控制器编程、脉宽调制控制与传感器数据采集的典型实例。压缩包共315个文件,约16.96MB,以C源码、头文件、编译中间文件与工程配置为主,另含hex固件、pdf文档、jpg图片及Keil工程文件,覆盖从源码到可烧录程序的完整链路。目前已有8891人学习下载,热度较高。读者可据此快速搭建实验环境,理解PWM占空比与转速的对应关系、传感器触发逻辑及模块化电路设计,并参考工程目录结构完成二次开发与功能扩展。
1. 从一块 F103 最小系统到能用的智能风扇:这套资源到底值不值得拆
去年帮一个学弟调毕设,他拿来的板子上电后风扇要么不转,要么一通电就全速狂转,串口打印出来的 ADC 值还在 0 和 4095 之间反复横跳。翻他代码一看,PWM 初始化写在了 GPIO 配置前面,定时器通道映射也没对,人体红外模块的输出脚还接在一个没有外部中断能力的普通 IO 上。这套基于 STM32F103 的智能风扇资源,恰好就是冲着这类问题来的——它把 STM32PWM风扇调速、hcsr501 人体感应、温度采集和档位切换这几块拼成了一个能跑通的完整工程,而不是扔给你一堆零散的 .c 文件让你自己猜怎么接。
资源本身是围绕 STM32F103 这颗经典芯片展开的,核心功能链路很清晰:hcsr501 检测到人 → 主控读取温度 → 按温度区间或手动档位输出对应占空比的 PWM → 驱动风扇模块转动。适合正在做嵌入式课程设计、毕设,或者想拿一个真实小项目练手 STM32 定时器和外设联调的人。如果你已经能点亮 LED、串口能打印,但一到多外设协同就翻车,这份东西的参考价值最高。
2. 硬件链路与选型逻辑:为什么是 F103 + hcsr501 + PWM 驱动
2.1 主控为什么锁定 STM32F103
STM32F103 系列在这类小项目里几乎是默认答案,原因不复杂。它自带多个通用定时器(TIM2/TIM3/TIM4 等),每个定时器能输出 4 路独立 PWM,正好够做风扇调速加备用通道;ADC 通道多,接热敏电阻或 LM35 都不用外扩芯片;GPIO 的复用功能表在参考手册里写得明明白白,PA8 这类引脚既能做普通 IO 也能切到 TIM1_CH1 输出 PWM,配置路径短。
资源里用的是最小系统板,常见的是 STM32F103C8T6 或 RCT6 这类封装。这里有个新手容易忽略的点:F103 的 IO 口是 3.3V 电平,而 hcsr501 和很多风扇模块是 5V 供电、5V 输出。直接把 hcsr501 的 OUT 脚怼到 STM32 的 IO 上,短期可能能用,长期是在赌芯片的耐压。常见做法是加一个电平转换,或者至少串一个 1k 电阻再并一个 3.3V 稳压管做钳位。资源正文里如果没强调这点,自己补上,这是硬件链路能不能稳的第一道关。
2.2 hcsr501 的触发模式与接线
hcsr501 是数字输出的人体红外模块,输出高电平表示检测到人体活动。它上面有两个可调电位器,一个调灵敏度(探测距离),一个调延时(输出高电平保持时间)。很多人拿到模块直接上电就用,结果发现人走了风扇还在转,或者人站在那风扇反而停了,问题就出在这两个旋钮和触发模式跳线上。
模块背面通常有一个跳线帽,可选可重复触发(H)和不可重复触发(L)。可重复触发模式下,只要人在探测范围内,输出会一直保持高电平;不可重复触发则是检测到一次后延时结束就拉低,期间再有人也不重新计时。做风扇项目建议用可重复触发,否则人坐在那不动,延时一到风扇就停,体验很怪。
接线就三根:VCC 接 5V,GND 共地,OUT 接 STM32 的一个 GPIO。如果要用外部中断做唤醒,就接带 EXTI 功能的引脚,比如 PA0 或 PB0。资源里如果用的是轮询方式读 OUT 脚,那接哪个普通 IO 都行,但轮询周期要控制好,太快浪费 CPU,太慢响应迟钝,一般 50~100ms 读一次比较合适。
2.3 风扇驱动:PWM 直驱还是加驱动模块
这是整个项目里最容易烧板子的地方。小功率的 5V 风扇,比如电脑机箱那种,工作电流可能到 200mA 甚至更高,STM32 单个 IO 的拉电流能力也就 20mA 左右,直接拿 IO 口去驱动风扇,轻则风扇不转,重则 IO 口报废。资源里提到的“stm32风扇模块”,通常指的是带 MOSFET 或三极管驱动的模块,输入端接 PWM 信号,输出端接风扇,电源单独走 5V 或 12V。
如果你手上没有现成模块,用 AO3400 这类 N 沟道 MOSFET 自己搭一个低边驱动也很快:栅极接 STM32 的 PWM 输出脚,源极接地,漏极接风扇负极,风扇正极接电源正极,栅极和源极之间并一个 10k 下拉电阻防止悬空误导通。这样 PWM 占空比直接控制风扇平均电压,实现调速。
提示:风扇是感性负载,关断瞬间会产生反向电动势。在风扇两端反并一个肖特基二极管(如 1N5819),能有效吸收尖峰,保护 MOSFET 和主控。
3. PWM 调速的代码落地:定时器配置、占空比映射与温度联动
3.1 定时器 PWM 输出的初始化顺序
STM32F103 的 PWM 输出依赖定时器的输出比较通道,配置顺序不能乱。下面这段是基于标准外设库的 TIM3_CH1(PA6)初始化代码,资源里如果用的是 HAL 库,逻辑一样,只是函数名不同。
// 1. 使能时钟:TIM3 和 GPIOA RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // 2. 配置 PA6 为复用推挽输出 GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽,必须选这个 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); // 3. 定时器基础配置:72MHz 主频,预分频 71 得 1MHz 计数频率 TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_TimeBaseStructure.TIM_Period = 999; // 自动重装载值,1MHz/1000 = 1kHz PWM 频率 TIM_TimeBaseStructure.TIM_Prescaler = 71; // 72MHz/(71+1) = 1MHz TIM_TimeBaseStructure.TIM_ClockDivision = 0; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure); // 4. 输出比较通道配置 TIM_OCInitTypeDef TIM_OCInitStructure; TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; // PWM 模式 1 TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse = 0; // 初始占空比 0 TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; TIM_OC1Init(TIM3, &TIM_OCInitStructure); TIM_OC1PreloadConfig(TIM3, TIM_OCPreload_Enable); // 使能预装载,改占空比时不会出现毛刺 // 5. 启动定时器 TIM_Cmd(TIM3, ENABLE);这段代码里几个参数直接决定 PWM 频率和分辨率。TIM_Prescaler 设 71,是把 72MHz 主频分频到 1MHz,也就是计数器每 1 微秒加一。TIM_Period 设 999,计数器从 0 数到 999 溢出,一个周期正好 1000 微秒,对应 1kHz 的 PWM 频率。这个频率对风扇调速足够,太低会有可听噪声,太高又可能超出驱动模块的响应能力。TIM_Pulse 就是比较值,设 500 就是 50% 占空比。
初始化顺序上,必须先开时钟再配 GPIO 和定时器,否则寄存器写不进去。GPIO_Mode 必须选 AF_PP,选成 Out_PP 的话 PWM 信号出不来,这是血泪经验里排第一的坑。
3.2 占空比与温度档位的映射关系
PWM 调好了,接下来是把温度值翻译成占空比。资源里常见的做法是分档:低温不转或低速,中温中速,高温全速。下面是一个简单的映射函数,假设温度值已经通过 ADC 读取并转换成了摄氏度。
// 根据温度返回 PWM 比较值(0~999) uint16_t TempToPWM(float temp) { if (temp < 25.0f) { return 0; // 低于 25 度,风扇停转 } else if (temp < 30.0f) { return 300; // 25~30 度,30% 占空比 } else if (temp < 35.0f) { return 600; // 30~35 度,60% 占空比 } else { return 999; // 35 度以上,全速 } } // 在主循环中调用 uint16_t pwm_val = TempToPWM(current_temp); TIM_SetCompare1(TIM3, pwm_val); // 更新 TIM3_CH1 占空比这里的分档阈值和占空比不是固定的,要根据你的风扇实际转速和噪音接受度来调。比如有的风扇 30% 占空比根本转不起来,那就把最低档提到 400 或 500。TIM_SetCompare1 是标准库函数,HAL 库对应的是 __HAL_TIM_SET_COMPARE。改占空比时因为前面使能了预装载,新值会在下一个周期生效,不会在周期中间突然跳变导致风扇抖动。
3.3 hcsr501 信号读取与风扇启停联动
人体感应的逻辑要叠加到温度控制上。常见策略是:有人且温度超过阈值才转,没人就停,或者没人时延时一段时间再停。下面用轮询方式读 hcsr501 的输出脚,接在 PB0 上。
// 初始化 PB0 为上拉输入(hcsr501 输出高电平有效) GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 上拉输入,防止悬空误触发 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); // 主循环中的联动逻辑 uint8_t human_detected = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0); if (human_detected) { uint16_t pwm_val = TempToPWM(current_temp); TIM_SetCompare1(TIM3, pwm_val); } else { TIM_SetCompare1(TIM3, 0); // 无人直接停转 }上拉输入这个设置很关键。hcsr501 输出高电平表示有人,输出低电平表示没人,但如果模块没接好或者线松了,引脚悬空,读出来的值就是玄学。配上内部上拉,悬空时默认读到高电平,至少不会出现风扇乱转。如果你希望无人时延时停转,可以在检测到无人后启动一个软件计时器,计时到了再关 PWM,而不是立刻关。
4. 避坑与排查:从“风扇不转”到“乱转”的五个真实翻车点
4.1 风扇完全不转,但串口打印正常
现象:程序能跑,串口有温度数据,但风扇纹丝不动。 原因:PWM 信号没输出到正确的引脚,或者 GPIO 模式配错。常见的是把 PA6 配成了普通推挽输出而不是复用推挽,或者定时器通道和引脚映射没对应上——TIM3_CH1 默认在 PA6,但如果你重映射到了 PB4,代码里没开 AFIO 时钟和重映射配置,信号就出不来。 解决:先用示波器或万用表测 PWM 输出脚,看有没有方波。没有的话,检查 GPIO_Mode 是否为 AF_PP,检查定时器通道对应的默认引脚,检查是否需要 AFIO 重映射。没有示波器就用 LED 串电阻接在 PWM 脚上看亮度变化,能亮说明有输出。
4.2 风扇一上电就全速转,不受控制
现象:板子刚上电,风扇直接满转,调占空比也没反应。 原因:MOSFET 栅极悬空导致误导通,或者 PWM 初始占空比设成了 999,或者 hcsr501 输出脚悬空被上拉读成高电平,程序误判有人且温度高。 解决:在 MOSFET 栅极和源极之间加 10k 下拉电阻,确保没有驱动信号时栅极被拉低。检查 TIM_Pulse 初始值是否为 0。检查 hcsr501 的 OUT 脚是否接好,模块电源是否正常,必要时在代码里加一个启动延时,上电后前 2 秒强制 PWM 为 0。
4.3 人体感应不灵敏,人走了风扇还在转
现象:人离开后风扇持续转动,或者人站在那风扇反而停了。 原因:hcsr501 的延时电位器调得太长,或者触发模式跳线选在了可重复触发但延时没调好。另外,模块的探测角度和距离有限,人站在边缘或背对模块可能检测不到。 解决:把延时电位器逆时针调到最短,先确认响应正常,再根据需要慢慢加长。触发模式选可重复触发。安装时让模块正对人活动区域,避开热源和阳光直射,这些都会干扰红外探测。
4.4 温度读数跳变严重,风扇档位频繁切换
现象:串口打印的温度值在几度范围内反复跳,风扇跟着一档一档地变。 原因:ADC 采样没有做滤波,或者热敏电阻的参考电压不稳。F103 的 ADC 在 72MHz 下采样保持时间不够也会导致精度下降。 解决:在 ADC 读取后加一个滑动平均滤波或中值滤波,比如连续采 10 次去掉最大最小再取平均。ADC 采样时间设长一点,比如 239.5 个周期。参考电压用外部 3.3V 稳压源,不要直接拿 USB 的 5V 分压。
4.5 风扇转动时串口乱码或程序复位
现象:风扇一转,串口就出乱码,或者单片机直接重启。 原因:风扇启动瞬间电流大,拉低了电源电压,导致 STM32 欠压复位。或者 PWM 走线离串口线太近,高频开关噪声耦合到了通信线上。 解决:给风扇电源单独走一路,和主控电源之间加磁珠或电感隔离,风扇电源端并一个大电容(比如 470uF)储能。PWM 走线尽量短,远离晶振和串口线。串口线用屏蔽线或双绞线,波特率不要设太高,115200 够用。
5. 进阶调优:把开环 PWM 做成带反馈的闭环调速
上面那套逻辑本质是开环的——你给风扇 50% 占空比,但风扇实际转速受电压、负载、温度影响,可能并不是你想要的。如果你想让这套资源更接近“智能”两个字,可以加一个转速反馈,做成闭环。
最直接的办法是用风扇自带的转速输出线(很多三线或四线风扇有一根 FG 或 TACH 线,输出与转速成正比的方波)。把这根线接到 STM32 的定时器输入捕获通道,比如 TIM2_CH1(PA0),测量方波频率,换算出实际转速。
// TIM2 输入捕获初始化(简化版,只列关键配置) RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD; // 下拉输入,FG 信号为方波 GPIO_Init(GPIOA, &GPIO_InitStructure); TIM_TimeBaseStructure.TIM_Period = 0xFFFF; TIM_TimeBaseStructure.TIM_Prescaler = 71; // 1MHz 计数 TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure); TIM_ICInitTypeDef TIM_ICInitStructure; TIM_ICInitStructure.TIM_Channel = TIM_Channel_1; TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising; TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI; TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1; TIM_ICInitStructure.TIM_ICFilter = 0x0; TIM_ICInit(TIM2, &TIM_ICInitStructure); TIM_Cmd(TIM2, ENABLE);捕获到上升沿后读 CCR1 的值,两次捕获的时间差就是周期,频率 = 1MHz / 周期。得到实际转速后,和设定转速比较,用简单的比例控制调整 PWM 占空比:实际转速低于目标就加占空比,高于目标就减。比例系数先从小往大调,比如 0.1 起步,观察风扇响应,太大就会振荡。
| 参数 | 开环方案 | 闭环方案 |
|---|---|---|
| 转速稳定性 | 随电压波动 | 基本恒定 |
| 代码复杂度 | 低,只配 PWM | 中,需捕获+控制逻辑 |
| 硬件需求 | PWM 驱动即可 | 需风扇带 FG 线 |
| 适用场景 | 对转速不敏感 | 需要精确控速 |
从那以后我每次做带执行器的项目,都会先拿示波器把 PWM 波形和反馈信号一起抓一遍,确认时序和电平都对得上再往下写逻辑。很多所谓的“玄学”问题,其实在波形上看得一清二楚。希望帮到你。
本文还有配套的精品资源,点击获取