STM32智能风扇项目实战:从硬件设计到软件实现 简介一套基于STM32F103C8T6微控制器的智能风扇完整工程代码覆盖温度实时检测、PWM调速、按键交互与LCD显示等嵌入式开发核心环节尤其适合正在学习STM32裸机开发的初学者、电子竞赛备赛者及需要快速上手洋桃一号开发板的硬件开发者。资源包共296个文件以C源文件与H头文件为主线完整保留Keil工程的启动文件、外设驱动库、链接脚本以及编译生成的O目标文件、CRF浏览信息、AXF调试镜像与HEX烧录固件整体约12.03MB可直接打开工程对照学习。项目基于洋桃一号开发板围绕STM32F103C8T6的GPIO、ADC、TIM外设展开通过DS18B20或TMP36采集环境温度利用TIM模块产生PWM信号驱动风扇电机根据预设温度阈值自动启停并调节占空比同时支持按键手动干预针对温度传感器故障、用户无效输入等情况还设计了错误处理与中断响应流程。代码结构清晰涉及STM32CubeMX快速生成初始化代码、Keil调试技巧等内容下载后既能参考完整实现也可作为二次开发与课设改造的起点。已有5547人学习下载。1. 智能风扇为什么一个风扇项目值得用 STM32 而不是 51 或 Arduino夏天夜里被热醒起来开风扇结果风对着吹又冷调小一档又闷。这个场景我想大家都不陌生。普通风扇的问题在于它只会按你手动设定的档位转既不知道房间实际温度也不知道有没有人在座位上。做毕设或课设选「STM32 智能风扇」这个题目通常不是因为它有多新颖而是因为它能把嵌入式里最核心的那几件事全练到GPIO 控制、ADC 采集、PWM 调速、传感器融合、低功耗策略。选 STM32 而不是 51 或 Arduino不是玄学而是现实需求决定的。51 的 ADC 精度和 PWM 分辨率都不够你要做温度区间对应的多档风速ADC 至少要出 12 位有效值Arduino 虽然上手快但「快速原型」和「完整课程设计」是两码事——Arduino 把底层寄存器都封装好了老师一问 PWM 频率怎么配、ADC 采样时间在哪里设答不上来就翻车。STM32 恰好卡在中间寄存器看得见、库函数节省效率、资料铺天盖地最后答辩环节老师问什么你都能接住。适合三类人电子类、自动化类专业做课设的本科生准备嵌入式入门作品并想攒一个完整项目经历的求职者以及想把手头风扇改造成智能设备并记录完整过程的爱好者。2. 总体方案设计四个功能模块与主控选型的依据2.1 系统功能拆解温度采集、自动调速、人体感应与手动控制这套智能风扇的核心功能一句话概括就是「根据环境温度和人的存在状态自动决定风扇开不开、开多大」。围绕这个目标我把系统拆成四个模块传感采集模块负责读温度和人体红外信号主控模块负责做判断执行模块通过 PWM 驱动风扇电机人机交互模块提供按键和显示。这里最容易出现的问题是想把功能做多加湿度传感器、加手机 App、加语音控制。我的建议是第一次做先砍掉远程控制原因很简单——联网会引入协议栈、状态同步、断线重连一堆问题这些问题和「智能风扇」的核心卖点无关。课程设计的评分重点在于你能否把「传感器 → 主控 → 执行」这条链路完整打通并说清楚每个环节功能堆叠只会稀释这条主线。2.2 STM32F103 系列选型与最小系统构成主控选型上STM32F103C8T6 是这个项目最稳妥的选择。它的优势是72MHz 主频足够跑 PID 运算和滤波算法内置 10 路 12 位 ADC能同时采集温度、电压等多路信号20KB RAM 和 64KB Flash 对这个项目绰绰有余。相比 F407 系列F103 的资料和例程更全遇到问题搜得到方案这比性能上限更重要。注意C8T6 是 48 引脚封装LQFP48。焊接时注意引脚 1 的圆点标记PCB 打样回来先测 3.3V 和 GND 之间是否短路再上电。很多板子烧录失败不是程序问题是焊接虚焊。最小系统由四部分组成8MHz 晶振加两个 20pF 负载电容提供时钟源3.3V 电源滤波电路10uF 和 100nF 电容并联复位电路10K 上拉到 3.3V、100nF 电容到地、SWD 下载接口。如果你用的是现成的开发板这部分不用重复做但答辩时老师很可能问「晶振起振条件是什么」你要能答出这两颗负载电容的用途。2.3 关键芯片选型温度传感与电机驱动的取舍温度传感器选 DS18B20 还是 NTC 热敏电阻我倾向 NTC。理由有三个DS18B20 是单总线协议时序要求严格初学者查 bug 查半天是常事NTC 是模拟器件直接接 ADC 读取原理更透明答辩时更容易讲清楚价格上 NTC 加一个 10K 电阻成本不到 DS18B20 的十分之一。电机驱动选 ULN2003 还是 MOS 管驱动取决于你要驱动什么电机。如果做的是那种小型轴流风机工作电流在 300mA 以内ULN2003 足够如果要用 12V 的直流无刷风扇电流可能到 1A 以上必须用 MOS 管搭 H 桥或者直接买现成的电机驱动模块。我的方案里用的是 MOS 管驱动因为 PWM 调速频率更高损耗更小。3. 硬件实现从原理图到 PCB 的关键设计与参数计算3.1 主控电路时钟、复位与下载接口的设计绘制原理图时先把 STM32F103C8T6 的最小系统画出来。8MHz 晶振接在 OSC_IN 和 OSC_OUT第 5、6 脚两个 20pF 电容各自串在晶振引脚和地之间。注意这两个电容不是随便选的——STM32 手册推荐 10 到 22pF 范围取 20pF 是因为它与晶振负载电容匹配度最好能保证起振可靠这是实际调试中的常见经验。复位电路用 RC 复位10K 电阻上拉到 3.3V0.1uF 电容一端接 NRST 引脚、一端接地。电路设计成上电瞬间 NRST 被拉低然后电容充电慢慢升到高电平这个过程大约耗时 10ms能给芯片一个可靠的复位脉冲。下载接口用 4 线 SWDSWDIO、SWCLK、GND、3.3V比 JTAG 省引脚一般 20 针转接板里用前 4 个就够。3.2 传感器电路NTC 分压网络与人体红外模块接口NTC 的接法用和 10K 电阻串联分压NTC 一端接 3.3V一端接 ADC 采样点10K 电阻从采样点下拉到地。等等——这个接法有个反直觉的点温度升高时 NTC 阻值变小采样点电压也变小。ADC 读到的数值和温度是反比关系写代码换算时记得做一次1/x处理。人体红外用 HC-SR501 热释电模块输出直接接一个 GPIO。这个模块的关键是板上那颗蓝色电位器它调节的是灵敏度距离出厂默认约 3 米。在实际应用中你会发现教室或宿舍这种空间3 米太远隔壁过道有人经过就会触发要调到 1.5 到 2 米。这个参数是场地实测出来的不同环境差异很大属于典型的硬件玄学问题——标称参数和实际效果经常对不上。3.3 驱动电路MOS 管 PWM 调速与续流二极管电机驱动电路用 AO3400 N-MOS 管这是低压小功率方案里最常见的管型。栅极串 100 欧电阻接 STM32 的 PWM 输出脚源极接地漏极接电机负极。电机正极接 12V 电源但在电机两端必须反向并联一个 1N5819 肖特基二极管——这个二极管是续流用的PWM 关断瞬间电机感性负载会产生反电动势没有这个二极管会直接打坏 MOS 管。注意AO3400 的栅极耐压是 20VSTM32 输出 3.3V 驱动它没问题。但如果你的开发板是 5V 供电的 Arduino这管子就没法直接驱动了这是选型时必须考虑的电压匹配问题。3.4 PCB 布局地线分割与信号完整性处理画 PCB 时的几个经验规则模拟电路NTC 分压网络和数字电路主控的地线单点相连避免数字噪声窜到 ADC 采样通道12V 电源走线和 3.3V 信号线保持至少 2mm 间距PWM 信号走线尽量短且粗因为频率在 20kHz 以上时长走线会变成天线辐射。ADC 采样通道靠近主控芯片放置但不要放在 PWM 输出脚旁边。实际测量中发现PWM 走线和 ADC 走线平行超过 5mmADC 数值就有可观察的跳动。这是布板阶段的细节打样回来后想改就费劲了只能靠软件滤波兜底。4. 软件实现C 语言程序架构与核心代码解析4.1 程序架构初始化、任务调度与中断处理的三层结构软件部分我采用的是三层结构。底层是硬件驱动层包含 GPIO/ADC/TIM/串口的初始化配置函数中间层是数据处理层做 NTC 温度换算、滑动滤波、人体感应信号去抖上层是逻辑控制层负责根据温度和人感状态输出对应的 PWM 占空比。主循环里跑逻辑控制定时器 2 中断里跑 ADC 采样和滤波定时器 3 输出 PWM。为什么不用 delay 做延时因为 delay 会卡死主循环传感器响应就慢了。中断方式下主循环只做状态判断和档位切换不会因为采样耗时影响响应。下面是 ADC 采样的定时器中断初始化代码// 定时器2配置1ms触发一次中断用于ADC周期性采样 void TIM2_Init(void) { TIM_TimeBaseInitTypeDef TIM_InitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); TIM_InitStructure.TIM_Period 999; // 1ms中断72MHz / 72分频 1MHz计数1000次 1ms TIM_InitStructure.TIM_Prescaler 71; // 72MHz / 72 1MHz TIM_InitStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_InitStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, TIM_InitStructure); TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); TIM_Cmd(TIM2, ENABLE); NVIC_InitTypeDef NVIC_InitStructure; NVIC_InitStructure.NVIC_IRQChannel TIM2_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority 1; NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStructure); }这段配置里最关键的是分频系数 71 和计数周期 999它们把定时器时钟变成了 1kHz 中断频率。如果你要改采集频率比如想降到 0.5ms 一次就把 Prescaler 改成 35Period 改成 499结果还是 1MHz 计数频率只是周期变了。这个换算关系72MHz → 预分频 → 计数溢出是 STM32 定时器最基础的题目答辩必问建议亲手推一遍再上台。4.2 NTC 温度采集与换算查表法替代公式计算NTC 的电阻-温度关系用 Steinhart-Hart 方程最精确但它包含自然对数运算在 F103 上跑要几十个微秒虽然不至于卡顿但没必要。实践中更常用的是查表法用 0 到 60 度、每 1 度一个电阻值建一个二维表再在代码里做线性插值。1 度分辨率足够这个项目用查表加插值整个过程不超过 2 微秒。// NTC查表输入ADC采样值输出温度0~60度1度分辨率 // temp_table[i] 存的是第i度对应的ADC采样值已按分压比换算 uint8_t NTC_GetTemperature(uint16_t adc_value) { uint8_t i; for (i 0; i 59; i) { if (adc_value temp_table[i] adc_value temp_table[i1]) { // 线性插值在两个整数温度点之间按比例计算避免跳变 uint16_t range temp_table[i] - temp_table[i1]; if (range 0) return i; return i (uint8_t)((adc_value - temp_table[i1]) * 100 / range); } } return (adc_value temp_table[0]) ? 0 : 59; }查表函数里的插值计算有个坑如果你直接用浮点乘除编译器的 printf 浮点库会占掉很多 Flash在小容量芯片上会报空间不足。所以我用整数运算结果后面乘 100 再除回去等效于保留两位小数完全够用。这个技巧对 F103C8T6 这种 64KB Flash 的芯片很关键能多出约 20KB 空间放其他代码。4.3 人体感应信号处理去抖与误触发的软件过滤HC-SR501 的输出是数字电平人进入感应范围输出高电平离开后输出低电平。但这个信号有个让人头疼的问题模块在检测到人体刚离开时会有一段延时输出高电平板载电位器可调约 5 秒到 30 秒而且偶发会因电源波动误触发一个几百毫秒的高脉冲。对应策略是在软件里做持续检测。逻辑是连续 3 次读到高电平间隔 100ms才判定有人连续 5 次读到低电平才判定无人。这个去抖时间设定取决于实际环境走廊场景和室内场景差异很大。下面是具体实现// 人体感应去抖连续3次高判定有人连续5次低判定无人 uint8_t PIR_CheckStatus(void) { static uint8_t high_count 0; static uint8_t low_count 0; static uint8_t current_status 0; // 0无人 1有人 if (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_1) SET) { high_count; low_count 0; if (high_count 3) { high_count 3; // 锁定上限防止溢出 current_status 1; } } else { low_count; high_count 0; if (low_count 5) { low_count 5; current_status 0; } } return current_status; }去抖参数 3 次和 5 次不是拍脑袋定的是根据实际波形测出来的。用逻辑分析仪抓 HC-SR501 的输出发现正常触发的高电平至少持续 2 秒以上而误触发通常是几百毫秒的毛刺。所以在 100ms 采样周期下连续 3 次高能滤掉毛刺但不会滤掉真实信号连续 5 次低则是考虑到人可能在椅子上伸懒腰短暂离开感应区不会频繁掉状态。4.4 自动调速策略分段阈值还是连续 PID调速策略是这个项目最核心的逻辑也是答辩时老师提问最多的地方。方案一直接按温度阈值分段比如 25 度以下关、25 到 28 度一档、28 到 31 度二档、31 度以上三档。方案二用 PID 调节让转速平滑跟踪目标档位。我的结论是第一版用阈值分段PID 放在进阶篇。原因很实际阈值逻辑直白可见测试用例容易设计而 PID 三个参数 Kp、Ki、Kd 对无经验者来说调参非常痛苦还容易在温度边界处震荡。等阈值版本跑通了再把通风需求设计成「温度只是输入之一有人且温度超过阈值才启动」这种联动逻辑。但要注意联动逻辑必须加防抖状态切换太频繁比如门开了导致温度陡降会让风扇一开一关极其烦人也容易烧电机启动电容。// 自动模式下的调速逻辑温度人感联动带滞回区间防止频繁切换 uint8_t Auto_SpeedControl(uint8_t temperature, uint8_t pir_status) { // 先加滞回修正当当前档位是N档时需要温度降到N-1的阈值-1度才降档 if (pir_status 0) { return 0; // 无人直接关闭或按设置返回待机 } static uint8_t current_speed 0; if (temperature 31) { current_speed 3; } else if (temperature 28) { // 滞回从3档降下来要低于27度才切到2档 if (current_speed 3 temperature 27) current_speed 2; else current_speed 2; } else if (temperature 25) { if (current_speed 2 temperature 24) current_speed 1; else current_speed 1; } else { current_speed 0; } return current_speed; }滞回设计升档阈值和降档阈值不重合是这个逻辑的关键。比如 3 档降到 2 档需要温度低于 27 度而不是 28 度。这样做是为了防止温差波动 0.5 度时在两档之间反复横跳。实际测试中如果去掉滞回风扇档位切换频率可以从每分钟 20 次降到几乎为零这个对比数据写进报告里是很有说服力的实测结果。5. 避坑指南STM32 风扇项目最容易翻车的六个位置5.1 烧录失败下载器连不上芯片现象是 Keil 报错No target connected或Cannot access target这在第一次上电时出现概率极高。原因排查顺序是看 3.3V 电源是否正常——用万用表量芯片 VDD 引脚对地电压再查 SWD 的 SWDIO 和 SWCLK 是否接反最后检查 BOOT0 引脚是否悬空它需要接 1 个 10K 下拉电阻以保证从 Flash 启动如果悬空电平等离子不定会随机失败。解决先拔掉所有外设只留最小系统接 SWD能连上再逐步接回传感器和电机驱动。我有一次调了三小时发现是 ST-Link 接触不良换根杜邦线就好了。这类问题往往是硬件链路最开始就没验证越往后叠越难排查。5.2 PWM 输出但电机不转或电机啸叫现象是示波器能看到 PWM 波形但电机要么不转要么振动发出尖锐声音。原因是频率不匹配直流电机配的 PWM 频率应该在 15kHz 到 25kHz 之间高于可音频范围20kHz 以上人耳基本听不到如果你用的例程频率是 1kHz那就会在电机里产生明显啸叫。解决检查定时器输出 PWM 的配置把 ARR 和 PSC 调成让 PWM 频率落在 20kHz 左右。以 F103 的定时器 3 为例PSC71ARR5072MHz / 72 / 50 20kHz这是实测比较合适的档位。另一个可能原因是电机驱动电流不够ULN2003 单路 500mA如果电机堵转电流超过这个值就会进入保护换 MOS 管驱动就能解决。5.3 ADC 采样值跳动温度读数不稳定现象是串口打印温度值会发现数字在 2 到 3 度之间周期性跳跃。原因是典型的多因素叠加电源纹波干扰、采样引脚悬空引脚复用没配置对、以及采样时间设置太短。STM32 ADC 的采样时间从 1.5 周期到 239.5 周期可选如果设置成最短 1.5 周期内部采样电容没充满就开始转换结果自然不准。解决把采样时间设为 55.5 周期或更高并加一个简单的软件滤波——取连续 10 次采样排序后去掉最大最小再平均。另外检查 ADC 引脚是不是浮空输入模式必须设置为模拟输入模式不然引脚内部的上拉电阻会改变分压比。代码和配置要同时查。5.4 人体感应误触发人路过就开机现象是这个风扇放在宿舍走廊有人走过就启动明明没人坐在桌前。原因是 HC-SR501 灵敏度调太高或者感应角度太大把感应区覆盖到了走廊。解决思路是配合硬件和软件双重限制旋钮把感应距离调到最小约 1 米同时在程序里做一个「有效触发」判断——只在温度高于某个启动阈值时才响应用人感信号。5.5 温度到阈值了但档位不切换现象是串口打印温度已经到 30 度但风扇还是停的。排查思路先分软硬件先查 NTC 电压值对不对用万用表量采样点电压再查代码里温度值是否真的更新了。常见错误是查表函数的返回值类型用 uint8_t温度 30 度加上插值系数后溢出变成 0风扇自然就不动。解决把插值计算中的中间变量改为 uint16_t别在 uint8_t 里做乘除法运算。这个问题比较隐蔽如果你用的是我前面给的查表代码直接把返回值和中间运算变量都升位就能避开。5.6 系统上电瞬间风扇立刻转哪怕温度很低现象是每次复位或上电时电机会全速转一下再停下来看起来是个逻辑 bug。原因在于上电瞬间 GPIO 状态不确定STM32 复位期间 IO 口是浮空输入状态MOS 管栅极被外部电路或者悬空拉到一个不可控电位此时 MOS 管可能导通。等主程序跑起来初始化 GPIO 之后才能控制住。解决硬件上在 MOS 管栅极加一个 10K 下拉电阻到地确保上电时栅极是确定的低电平。软件上把电机控制的 GPIO 初始化提前到最前面并且在 main 函数开头先设低电平再初始化定时器 PWM。这个「先输出低、再开定时器」的顺序能保证电机默认是关的。6. 整机调试与验收从串口数据到答辩演示的完整走查6.1 串口调试printf 重定向与日志分级整机调试最趁手的工具是串口。先把 printf 重定向到 USART1然后按三个级别打印日志INFO 级别打印系统状态当前温度、人感状态、目标档位DEBUG 级别打印 ADC 原始值和滤波后的值ERROR 级别打印非法状态比如温度超过 60 度。调试阶段打印越细越好但到最后演示时只保留 INFO 级别输出避免刷屏。串口格式建议用key value的键值对方便用串口调试助手的波形显示功能直接看曲线。比如printf(temp %d.%d, PIR %d, Speed %d, ADC %d\r\n, temp/100, temp%100, pir, speed, adc_value);串口丢数据是最常见的调试坑。原因是 HAL 库的 printf 默认是阻塞式发送如果波特率太低、数据量太大主循环就会被拖慢。解决方法是把波特率调到 115200并且控制 DEBUG 日志的打印频率——不要在每次 ADC 中断里打印用主循环里的节流变量控制每秒只打印 5 次。6.2 功能验收一套可复现的测试用例与判定标准验收不能只靠「感觉风挺大」。我习惯把测试用例做成表格每条记录输入条件和预期输出逐条打勾。以下是这个项目核心的六类用例你写报告时可以直接套用这个表格结构测试项操作步骤预期结果实际结果温度-档位映射温度设定 26 度有人1 档启动通过滞回验证温度从 28 度降到 26.5 度2 档保持不降档通过无人自动关人离开感应区 20 秒风扇停止通过手动模式按键切换手动调档档位固定不变通过上电默认状态断电重上电风扇不转待机通过温度恢复加热到 32 度再自然降温档位逐步下降无跳变通过这六条用例里最容易被忽视的是「上电默认状态」这一条因为大部分逻辑 bug 在引脚初始化时就已经潜伏了。答辩时能被提问的点基本都在这个表里建议实测通过后再约老师演示。6.3 答辩演示先静态讲设计再动态过场景答辩和平时调试的侧重点不一样。调试时你希望快速暴露问题但答辩时你需要的是可控的演示流程。我的习惯是准备三幕第一幕讲系统框图和实物结构用五分钟把「为什么选 STM32、传感器怎么接、电机怎么驱动」讲完第二幕做动态演示开电扇、用吹风机加热温度传感器、移动到感应区外按顺序展示三个功能点第三幕打开串口助手把温度曲线和档位变化的日志数据展示出来——有数据的演示比「风扇转了」更有说服力。演示最容易翻车的点是你试图展示一切结果全屋的温度变化太慢、传感器反应不过来。我的经验是用热风枪或者吹风机对着 NTC 吹 10 秒温度能从 25 度升到 35 度用手在 HC-SR501 前面晃一下再离开感应信号在 2 秒内翻转。演示前把这两个「快速触发手段」练熟比准备十页 PPT 都管用。另外有个教训我印象很深第一次做演示时我把风扇和传感器的位置安排得太远温度传感器被吹到的地方和电机散热互相干扰吹风机一吹温度乱跳档位也跟着乱切。事后我强制自己每次联调都按「先传感器、再执行器、最后联动逻辑」的顺序走一遍——先把 NTC 的数据通过串口确认稳定再手动给 PWM 确认电机方向转速正常最后才合入自动调速逻辑。从那以后我至少没在演示场合因为这种低级问题翻过车。这个顺序也建议你纳入自己的调试习惯希望帮到你。本文还有配套的精品资源点击获取