最近很多电子信息、自动化专业的大四同学在做毕业设计时,都会选“基于STM32的智能XX系统”这类题目,原因很直接:STM32资料丰富、外设齐全,用来做嵌入式课程设计和毕业设计既容易出实物,也方便展示功能。不过题目看起来常见,真正动手做的时候,很多人还是会在传感器选型、电机驱动、代码框架和实物调试上卡住。这篇文章就围绕一个典型的“智能绿色风扇”项目,把从需求分析到软硬件实现的完整思路拆开讲清楚。如果你正准备做类似的STM32项目,或者只是想把一个风扇系统做得更“智能”,可以先收藏备用。
1. 项目背景与核心功能
“智能绿色风扇”这个题目,本质上并不是让你做一个能吹风的风扇,而是要求把单片机技术、传感器检测、自动控制和人机交互集成到一起,实现风扇根据环境状态自动调节。相比普通风扇,它增加的智能点主要体现在几个方面:
- 能够检测环境温度,温度高时自动提高风速,温度适中时降低风速或停止。
- 能够感知是否有人靠近,人在时启动运行,人离开后延时关闭,避免电能浪费。
- 支持自动模式与手动模式切换,手动模式下可以用按键调节风速和摇头方向。
- 尽量采用节能设计,比如 PWM 调速取代电阻降压调速,根据实际需求动态调整电机功率。
- 提供状态显示,实时查看温度、风速档位、当前模式等信息。
从专业角度来看,这样的题目覆盖了 STM32 的多个核心外设:GPIO 控制、定时器 PWM 输出、ADC 采样、外部中断或定时器输入捕获、I2C 或 SPI 驱动显示模块等。所以它非常适合作为单片机课程设计、毕业设计或者实验室创新项目。
对于学习者来说,做这个项目的价值不只是“做出一台风扇”,而是通过它建立一套完整的嵌入式系统开发思路:需求怎么拆解,硬件怎么选型,代码怎么写才容易调试,问题怎么排查。当你把这套流程跑通之后,再做智能台灯、智能窗帘、智能风扇等同类设计,基本就只有传感器和电机驱动部分的差异了。
2. 系统总体方案设计
在写代码之前,需要先把系统方案定下来。智能风扇的整体工作链路大致是:传感器采集环境信息 → 单片机对数据进行处理与逻辑判断 → 控制风扇电机和舵机动作 → 通过显示模块和按键完成人机交互。
2.1 核心器件选型
主控芯片可以选择 STM32F103C8T6 或者 STM32F103RCT6。这两款都是经典的 Cortex-M3 内核芯片,前者性价比高,资源对大多数小型项目够用;后者在容量和引脚数量上更有优势。如果你用的是其他系列的 STM32,配置思路也是相同的,只需要注意底层库函数或者 HAL 库函数名称的差异。
关键器件清单大致如下:
| 模块 | 推荐选型 | 作用 |
|---|---|---|
| 主控 | STM32F103C8T6 | 数据处理与控制核心 |
| 测温 | DS18B20 数字温度传感器 | 采集环境温度 |
| 人体检测 | HC-SR501 人体红外传感器 | 检测是否有人靠近 |
| 电机驱动 | 三极管/MOS 管 + PWM | 控制直流风扇转速 |
| 摇头机构 | SG90 舵机 | 实现左右摇头送风 |
| 显示 | OLED(I2C)或 LCD1602 | 显示温度与工作状态 |
| 输入 | 独立按键 | 模式切换、手动调速、摇头控制 |
| 电源 | 5V 电源适配器/LM2596 降压模块 | 给主控与外设供电 |
有人可能会问,为什么不直接用继电器控制风扇开关?这里要解释一下。继电器只能控制通断,不能调速;如果希望风扇有“低、中、高”多档风速,用继电器的方案会非常笨重。而 STM32 定时器产生 PWM 信号,经过 MOS 管或三极管开关电路,可以改变电机的平均电压,从而平滑调速,也更容易实现“温度越高、风速越大”这种渐变逻辑。
2.2 系统工作流程
系统上电后首先完成所有外设的初始化,比如时钟配置、GPIO 模式、定时器 PWM 输出通道、ADC、串口、OLED 等。然后进入主循环,主循环里反复执行以下几件事:
- 按照设定的周期读取温度传感器数据。
- 读取人体红外模块的电平状态。
- 根据当前模式判断目标转速。
- 调用电机控制函数更新 PWM 占空比。
- 刷新 OLED 显示内容。
- 检测按键动作,实时修改系统参数。
流程图可以用文字简单表示:
上电初始化 ↓ 读取温度、人体检测信号 ↓ 自动模式?——是——→ 根据温度与人体状态决策风速 ↓否 读取按键、手动调整风速 ↓ PWM 控制风扇转速,控制舵机摇头 ↓ OLED 显示温度、模式、档位 ↓ 循环这个思路看起来简单,但是在代码设计时有一个需要提前规划好的问题:主循环中如果每个传感器都采用阻塞式读取,比如 DS18B20 的时序操作会占用大量时间,那么按键响应和 OLED 刷新都会出现明显延迟。所以建议代码里尽量把耗时操作拆成“每隔一段时间执行一次”的节奏,避免所有外设抢占同一个 CPU 时间片。
3. 环境准备与开发工具说明
嵌入式开发与纯软件程序开发有一点明显不同:你不仅要写代码,还要处理工程配置、编译下载和硬件调试。
3.1 软件环境
做 STM32 开发常用的软件工具链是:
- Keil MDK5,最普及的 STM32 集成开发环境。
- STM32CubeMX,用于可视化配置引脚、时钟、外设,快速生成初始化代码。
- STM32 ST-LINK Utility,用来下载和烧录程序。
- XCOM 或野火串口助手,用于调试时查看串口打印数据。
Keil MDK5 的安装需要注意:如果你还要开发 51 单片机,需要同时安装 C51 和 ARM 两个编译器版本;如果只用 STM32,则选择 ARM 编译器对应的 MDK 版本即可。网上常见的问题是安装完 Keil 后找不到 STM32 芯片 pack,解决办法是在 Keil 的 Pack Installer 里安装对应的 STM32F1xx_DFP 支持包。
版本需要根据你的项目实际情况调整,本文示例以常见的 Keil MDK5 + 标准外设库或 HAL 库环境为例,重点演示设计思路。
3.2 硬件调试工具
- ST-Link V2 下载器,价格便宜,支持调试和串口通信模拟。
- 杜邦线和面包板,前期验证电路用。
- 万用表,用于排查供电短路、信号线接触不良等硬件问题。
- 逻辑分析仪,如果做定时器捕获、舵机脉冲调试时会非常有用。
在实际开发时,不建议一上来就把所有模块焊在一个板子上,而是先用面包板和杜邦线把最小系统搭建起来,分模块逐一验证。每个模块验证通过之后再整合,这样出了问题定位起来会快很多。
3.3 项目文件结构规划
如果你使用的是 STM32CubeMX 生成工程,建议在工程的 User 目录下按模块创建源文件和头文件,例如:
User/ ├── main.c ├── bsp/ │ ├── bsp_motor.c │ ├── bsp_motor.h │ ├── bsp_ds18b20.c │ ├── bsp_ds18b20.h │ ├── bsp_hcsr501.c │ ├── bsp_hcsr501.h │ ├── bsp_oled.c │ └── bsp_oled.h └── app/ ├── app_fan_control.c └── app_fan_control.h这种模块化方式虽然初期文件更多,但主函数会很干净,后期调试也能快速定位具体模块。不要把所有代码随手堆在 main.c 里,尤其是做毕业设计时,老师通常很看重代码规范和程序结构。
4. 硬件电路设计要点
硬件电路是整套系统能不能稳定跑起来的基础。下面把每一部分的电路思路说明清楚。
4.1 STM32 最小系统
STM32F103C8T6 的最小系统包括电源电路、复位电路、晶振电路和启动模式选择。如果用集成度更高的最小系统板,比如常见的“蓝丸”开发板,这部分基本不需要自己设计,直接通过 USB 或者杜邦线供电就行。
需要注意的是电源稳定性。风扇电机启动瞬间电流较大,可能会导致芯片电压跌落。如果 STM32 频繁重启,首先检查电源是否足够,或者在电机供电和单片机供电之间加一个隔离,最简单的做法是分开供电并共地。很多第一次做实物的人都会在这里踩坑:电机一转,OLED 闪一下就黑屏了,这就是典型的电源干扰问题。
4.2 温度采集电路
DS18B20 是一个单总线数字温度传感器,它的优势是接口简单,只需要一个 GPIO 引脚,并且测量精度可以到 0.5℃,对风扇控制这种场景完全够用。DS18B20 的数据线需要接一个 4.7kΩ 上拉电阻到 3.3V 或 5V,保证单总线空闲时处于高电平。
如果你选择使用 NTC 热敏电阻加 ADC 的方案,电路上需要把热敏电阻与固定电阻组成分压电路,然后通过 STM32 的 ADC 引脚测量电压。NTC 的优点是成本低,但读取到的温度需要查表或者用公式计算,标定过程比 DS18B20 复杂一些。毕业设计不是非要追求极致精度,选 DS18B20 更省事。
4.3 人体红外检测电路
HC-SR501 人体红外模块输出的是数字电平,有人活动时输出高电平,没有人时输出低电平。模块上有两个电位器,一个调节感应距离,另一个调节延时时间。接 STM32 时需要注意,模块工作电压是 4.5V~20V,一般 5V 供电,输出高电平也是 5V,如果 STM32 引脚不是 5V 容忍引脚,建议用电阻分压或者通过光耦隔离后再送入 GPIO,防止烧坏引脚。
HC-SR501 还有一个特点:上电后大约需要 30 秒到 60 秒的初始化时间,在此期间模块会误触发几次。在实际程序中通常增加一个延时开门逻辑,避免开机瞬间误把人离开判断成人进入。
4.4 风扇电机驱动电路
直流小风扇的驱动方式有几种,最简单的是用一个 NPN 三极管,比如 S8050 或者 2N2222,把 PWM 信号送到三极管基极,电机接在集电极和电源之间。这个方法适用于电流在几百毫安以内的小风扇。
如果风扇功率稍大,建议使用 MOS 管,比如 AO3400 N-MOS,RDS 很小,开关损耗低。电路如下:
- MOS 管栅极接单片机 PWM 输出引脚,可以串联一个 100Ω 电阻抑制振铃。
- 栅源之间并联一个 10kΩ 下拉电阻,防止单片机上电瞬间引脚悬空导致电机误转。
- 电机两端反向并联一个二极管,比如 1N4007 或者 SS14,用于吸收电机断电时的反向感应电动势。
这里特别要说一下续流二极管的作用。直流电机是感性负载,PWM 关断瞬间会产生反向电动势,如果不加续流二极管,这个反向电压很容易把 MOS 管击穿。设计 PCB 或者接线时,二极管越靠近电机越好。
另外,风扇转速检测如果要做,可以在风扇霍尔信号输出端接一个 GPIO,然后通过定时器输入捕获模式测量脉冲频率,从而换算出实际转速。这个功能不是必须的,但如果你想让毕业设计更有亮点,可以考虑加入。
4.5 OLED 显示电路
OLED 显示屏目前很常用的是 0.96 寸 I2C 接口版本,分辨率 128x64,只需要四根线:VCC、GND、SCL、SDA。在 STM32 驱动时,选择硬件 I2C 或软件模拟 I2C 都可以。硬件 I2C 速度高,但引脚配置相对严格;软件模拟 I2C 更灵活,任何两个 GPIO 都可以。对于显示需求不高的项目,软件模拟 I2C 已经足够稳定。
OLED 模块的 I2C 地址通常是 0x78(8 位地址)或者 0x3C(7 位地址),不同厂家模块地址可能不同,驱动代码里需要注意。
5. 软件核心逻辑设计
硬件电路只解决了“能不能动”的问题,风扇能不能“聪明”,完全取决于程序逻辑。
5.1 状态机设计
系统建议用一个简单的状态机来管理运行模式。定义两种主要模式:
- MANUAL_MODE:手动模式,用户按按键调节风速档位。
- AUTO_MODE:自动模式,风扇根据温度和人体红外信号自动运行。
自动模式下,又可以进一步分为几个子状态:
- STANDBY:待机状态,人不在附近时不转。
- LOW_SPEED:低速运行,人在且温度低于 26℃。
- MID_SPEED:中速运行,人在且温度介于 26℃ 和 30℃ 之间。
- HIGH_SPEED:高速运行,人在且温度高于 30℃。
- DELAY_STOP:人离开后的延时状态,超过设定时间后转为 STANDBY。
为什么还要一个 DELAY_STOP 状态?因为人体红外模块检测的是“活动”而不是“存在”,人坐在电脑前不动时,模块可能检测不到信号,如果立即停转,体验就很差。所以人离开后延长 15 秒或 30 秒停转更合理,也符合“绿色节能”的题目定位。
5.2 ADC 与温度读取
DS18B20 温度读取属于数字信号读取方案,不涉及 ADC。它的核心是按单总线时序进行初始化、写命令、读数据。
温度读取的流程是:
- MCU 拉低总线 480μs 以上,然后释放总线,检测存在脉冲。
- 写入跳过 ROM 命令 0xCC。
- 写入启动温度转换命令 0x44。
- 等待转换完成,默认 12 位精度需要 750ms 左右。
- 再次初始化,写入 0xCC 和 0xBE 读取暂存器数据。
- 将低字节和高字节组合,得到温度值。
DS18B20 返回的原始数据是 16 位有符号补码,需要把两个字节拼接后判断符号位,再乘 0.0625 得到小数点精度的温度值。代码里如果只做整数处理,可以右移 4 位得到 0.5℃ 精度的整数温度。
如果把温度传感器换成 NTC 热敏电阻,就需要通过 ADC 采集电压,再经过热敏电阻分压公式计算当前温度。这种方式允许你练习 STM32 的 ADC 外设,也是很多课程设计喜欢采用的方式。不过需要注意的是,ADC 采集到的电压如果抖动明显,可以在软件上多采样几次取平均值,比如连续采集 10 次后排序取中位值,或者直接求平均。
5.3 PWM 调速原理
STM32 的定时器可以输出多路 PWM 信号,拿通用定时器 TIM2 举例,配置为 PWM 模式 1,通过设置比较寄存器 CCR 的值,控制高电平持续时间。PWM 的频率和占空比是由三个参数决定的:
- 预分频器 PSC:决定计数时钟频率。
- 自动重装载值 ARR:决定 PWM 周期。
- 比较值 CCR:决定占空比。
如果定时器使用内部 72MHz 时钟,设置 PSC=71,则计数时钟为 1MHz,即每个计数周期 1μs。再设置 ARR=999,则 PWM 周期为 1000μs,也就是 1kHz。占空比 = CCR/ARR,CCR 在 0 到 999 之间变化时,占空比就从 0 变化到 100%。
风扇调速使用 1kHz 到 25kHz 之间比较合适。频率太低会听到明显噪音,太高的话驱动电路开关损耗增大。对于普通直流风扇,20kHz 左右的 PWM 频率既安静又流畅。
5.4 按键与消抖
按键电路如果直接接单片机 GPIO,需要配置内部上拉输入。很多初学者问“4x4键盘需不需要上拉电阻”,如果你是直接用单片机内部上拉,就不再需要外部上拉;如果使用外部按键模块,看模块原理图决定。独立按键建议直接在代码里做软件消抖,因为在 STM32 的主循环中逐次扫描,机械按键按下瞬间会产生 5ms 到 20ms 的不稳定电平。
经典的软件消抖思路是:
- 检测到电平变化后,记录当前时间戳。
- 延时 10ms 左右再次读取。
- 如果两次读取值一致,认为按键有效。
- 如果不是,则视为干扰抖动。
使用按键时还要考虑是“按下时有效”还是“释放时有效”。手动模式下,每次按键按下只希望档位加 1 或者减 1,而不是连续加。这里需要增加一个按键事件标志,只有检测到按下动作完成后才处理一次档位更新。
6. 完整代码实现
这一部分给出核心模块的配置与代码示例。下面以 HAL 库为基础,因为 HAL 库的代码结构更清晰,也比较容易移植。
6.1 初始化配置(STM32CubeMX)
用 STM32CubeMX 配置工程时,建议先按下表分配引脚:
| 功能 | 引脚 | 配置 |
|---|---|---|
| 风扇 PWM | PA8(TIM1_CH1) | 复用推挽输出 |
| 温度数据 | PB1 | 开漏输出或推挽输出 |
| 人体红外 | PB2 | 输入模式 |
| OLED SCL | PB6 | I2C1_SCL |
| OLED SDA | PB7 | I2C1_SDA |
| 模式切换键 | PA0 | 输入 |
| 风速加键 | PA1 | 输入 |
| 风速减键 | PA2 | 输入 |
| 摇头控制 | PA3 | PWM 输出到舵机 |
在 CubeMX 中需要设置:
- RCC 选择 HSE 晶振时钟源。
- TIM1 选择 Channel 1 为 PWM Generation,预分频与周期可以暂不设置,代码中再细化。
- I2C1 选择 Standard Mode,速度 100kHz 即可。
- 串口 UART1 打开,用来打印调试日志,波特率 115200。
生成工程后,优先在 main.c 里添加用户代码到 USER CODE BEGIN 区段,避免 CubeMX 重新生成代码时把自己的代码覆盖掉。
6.2 PWM 电机控制代码
先来看电机控制模块的头文件内容,它定义了速度档位和数据接口。
// 文件路径:User/bsp/bsp_motor.h #ifndef __BSP_MOTOR_H #define __BSP_MOTOR_H #include "main.h" #define MOTOR_MIN_DUTY 0 #define MOTOR_LOW_DUTY 400 #define MOTOR_MID_DUTY 650 #define MOTOR_HIGH_DUTY 900 void Motor_Init(void); void Motor_SetSpeed(uint16_t duty); void Motor_Stop(void); uint16_t Motor_GetCurrentDuty(void); #endif电机控制的初始化代码需要启动 PWM 输出。在 HAL 库中,主要函数是__HAL_TIM_SET_COMPARE(),用来修改比较寄存器值。
// 文件路径:User/bsp/bsp_motor.c #include "bsp_motor.h" static uint16_t current_duty = 0; void Motor_Init(void) { HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1); __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, 0); } void Motor_SetSpeed(uint16_t duty) { if (duty > 1000) { duty = 1000; } current_duty = duty; __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, current_duty); } void Motor_Stop(void) { current_duty = 0; __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, 0); } uint16_t Motor_GetCurrentDuty(void) { return current_duty; }这里把 ARR 设置为 1000,所以 CCR 的比较值 0 到 1000 就对应 0 到 100% 占空比。设置 400 表示 40% 占空比,这是低风速;650 是中风速;900 是高速挡。实际调试时需要根据你用的风扇调整这些值,因为不同风扇的启动电压不同,可能 30% 占空比时风扇根本无法启动。此时上一档位要设置成启动所需的更大值,避免软件发出某个档位命令而风扇纹丝不动。
6.3 DS18B20 温度读取代码
DS18B20 的驱动核心是严格的时序操作。下面给出一份简化的驱动代码,重点展示温度读取和数据处理逻辑。
// 文件路径:User/bsp/bsp_ds18b20.c #include "bsp_ds18b20.h" #define DS18B20_GPIO_PORT GPIOB #define DS18B20_PIN GPIO_PIN_1 static void DS18B20_DelayUs(uint32_t us) { // 简单延时,实际项目中建议使用定时器或 DWT 精确延时 uint32_t i; for (i = 0; i < us * 8; i++) { __NOP(); } } static uint8_t DS18B20_Reset(void) { uint8_t presence; GPIO_InitTypeDef GPIO_InitStruct = {0}; HAL_GPIO_WritePin(DS18B20_GPIO_PORT, DS18B20_PIN, GPIO_PIN_RESET); DS18B20_DelayUs(500); HAL_GPIO_WritePin(DS18B20_GPIO_PORT, DS18B20_PIN, GPIO_PIN_SET); DS18B20_DelayUs(60); GPIO_InitStruct.Pin = DS18B20_PIN; GPIO_InitStruct.Mode = GPIO_MODE_INPUT; GPIO_InitStruct.Pull = GPIO_PULLUP; HAL_GPIO_Init(DS18B20_GPIO_PORT, &GPIO_InitStruct); presence = HAL_GPIO_ReadPin(DS18B20_GPIO_PORT, DS18B20_PIN); DS18B20_DelayUs(480); GPIO_InitStruct.Pin = DS18B20_PIN; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(DS18B20_GPIO_PORT, &GPIO_InitStruct); HAL_GPIO_WritePin(DS18B20_GPIO_PORT, DS18B20_PIN, GPIO_PIN_SET); return presence; }上述代码是单总线操作的底层部分。需要注意的是,DS18B20 对时序要求比较严格,如果使用简单的 for 循环延时,在不同编译优化等级下时间会有差异。调试时如果读出来的温度一直是 85℃,通常是初始化复位时序不合格,或者总线没有接上拉电阻。更稳妥的方法是使用 DWT 精确延时,或者放在定时器中断里做延时管理。
单总线的操作细节在这里不过度展开,简单总结起来就是:主机通过拉低总线产生时间片,DS18B20 依靠总线上的高低电平持续时间来识别 0 和 1。
如果不想在单总线时序上花太多时间,也有一个替代方案:使用 NTC 热敏电阻。此时代码只需要配置 ADC,并读取转换值即可。
6.4 风扇控制逻辑代码
把控制逻辑独立成文件,主循环就会非常简洁。下面的代码展示了一个简化版的风扇控制状态机。
// 文件路径:User/app/app_fan_control.c #include "app_fan_control.h" #include "bsp_motor.h" #include "bsp_ds18b20.h" #include "bsp_hcsr501.h" #include "bsp_oled.h" typedef enum { MODE_MANUAL = 0, MODE_AUTO } FanMode_t; typedef enum { STATE_STANDBY = 0, STATE_LOW_SPEED, STATE_MID_SPEED, STATE_HIGH_SPEED, STATE_DELAY_STOP } AutoState_t; static FanMode_t current_mode = MODE_AUTO; static AutoState_t current_state = STATE_STANDBY; static uint8_t manual_level = 1; static uint16_t delay_stop_counter = 0; #define DELAY_STOP_THRESHOLD 300 // 主循环计数延时,相当于15秒 #define TEMP_LOW_THRESHOLD 26.0f #define TEMP_HIGH_THRESHOLD 30.0f void FanControl_Task(void) { float temperature = 25.0f; uint8_t human_detected = 0; uint8_t key_mode = Key_GetMode(); uint8_t key_add = Key_GetAdd(); uint8_t key_sub = Key_GetSub(); if (key_mode == 1) { if (current_mode == MODE_AUTO) { current_mode = MODE_MANUAL; } else { current_mode = MODE_AUTO; } } temperature = DS18B20_GetTemperature(); human_detected = HCSR501_GetStatus(); if (current_mode == MODE_MANUAL) { if (key_add == 1 && manual_level < 3) { manual_level++; } if (key_sub == 1 && manual_level > 0) { manual_level--; } if (manual_level == 0) { Motor_Stop(); } else if (manual_level == 1) { Motor_SetSpeed(MOTOR_LOW_DUTY); } else if (manual_level == 2) { Motor_SetSpeed(MOTOR_MID_DUTY); } else { Motor_SetSpeed(MOTOR_HIGH_DUTY); } Oled_ShowMode("Manual"); } else { if (human_detected == 0) { if (current_state != STATE_STANDBY) { delay_stop_counter++; if (delay_stop_counter >= DELAY_STOP_THRESHOLD) { current_state = STATE_STANDBY; delay_stop_counter = 0; Motor_Stop(); } } } else { delay_stop_counter = 0; if (temperature < TEMP_LOW_THRESHOLD) { current_state = STATE_LOW_SPEED; Motor_SetSpeed(MOTOR_LOW_DUTY); } else if (temperature < TEMP_HIGH_THRESHOLD) { current_state = STATE_MID_SPEED; Motor_SetSpeed(MOTOR_MID_DUTY); } else { current_state = STATE_HIGH_SPEED; Motor_SetSpeed(MOTOR_HIGH_DUTY); } } if (current_state == STATE_STANDBY) { Oled_ShowState("Standby"); } else { Oled_ShowState("Running"); } Oled_ShowMode("Auto"); } Oled_ShowTemp((int)temperature); Oled_ShowLevel(manual_level); }这段逻辑有几个地方需要注意。第一个是主循环周期假设约为 50ms,所以 300 次计数大约等于 15 秒的延时停转。具体延时时间要根据你的主循环速度来调整。第二个是人体的检测结果不是直接决定开关,而是只影响 delay_stop_counter 的累加或清零,这样就能实现“人离开后延时停转”的效果,避免红外模块短暂无信号导致风扇突然停止。
手动模式与自动模式的切换优先级在代码中放到了最前面,保证按键随时可以响应。这样设计比较符合实际使用习惯。
6.5 舵机摇头控制
摇头功能由一个 SG90 舵机控制,舵机的控制信号和 PWM 类似,只是周期要求固定为 20ms,占空比在 0.5ms 到 2.5ms 之间对应 0 度到 180 度。
如果使用定时器输出周期为 20ms 的 PWM,ARR 设置为 19999,那么 CCR 对应关系是:
- 0.5ms 高电平:CCR = 500,舵机 0 度。
- 1.5ms 高电平:CCR = 1500,舵机 90 度。
- 2.5ms 高电平:CCR = 2500,舵机 180 度。
摇头时不断在 0 度和 180 度之间来回摆动。为了防止舵机死区抖动,建议在到达目标角度后等待几十毫秒,让舵机完成动作,再切换方向。
SG90 舵机需要独立供电,因为它动作瞬间电流有可能达到几百毫安,如果直接从 STM32 的 3.3V 引脚取电,会造成系统电压跌落。正确做法是舵机电源接 5V,控制信号线接 STM32 的 PWM 输出引脚,并且两者共地。
6.6 OLED 显示逻辑
OLED 驱动可以直接使用开源的 u8g2 库或者 SSD1306 驱动。使用 I2C 方式时,SSD1306 初始化代码会发送一系列命令,初始化显示模式、设置对比度、打开显示等。
在用户代码中,你不需要直接操作底层命令,只需要封装几个显示函数:
void Oled_ShowTemp(int temp) { char buf[16]; sprintf(buf, "Temp:%d C", temp); OLED_ShowString(0, 0, buf); } void Oled_ShowMode(char *mode) { char buf[16]; sprintf(buf, "Mode:%s", mode); OLED_ShowString(0, 2, buf); } void Oled_ShowState(char *state) { char buf[16]; sprintf(buf, "State:%s", state); OLED_ShowString(0, 4, buf); }显示刷新频率不用太高,每秒刷新两次即可。OLED 如果频繁全屏刷新,不仅有闪烁感,还会占用主循环大量时间。更好的方式是只在数据变化时才更新屏幕上的对应字符串。
7. 调试过程与常见问题
7.1 风扇不转但程序能正常下载
出现这种问题,先不要怀疑主控,优先排查三个点:
- 电机供电是否正常,电源模块输出的电流够不够。
- MOS 管或三极管的基极和集电极接线是否正确。
- 控制引脚是否输出了 PWM 波形。
排查 PWM 波形最直观的方法是使用万用表测量 GPIO 引脚的平均电压。把占空比设置为 50%,用万用表量到的电压大约应该是 3.3V 的一半,也就是 1.65V 左右。如果电压为 0,说明 PWM 没有输出;如果电压一直是 3.3V,说明引脚可能被配置成了普通推挽输出直通高电平。
7.2 DS18B20 读数一直是 85℃ 或者 0℃
DS18B20 在工作异常时,默认上电读数值是 85℃,这是芯片内部存储器的复位默认值,并不代表稳定读取到了数据。
可能原因包括:
- 数据线没有接上拉电阻。
- GPIO 模式配置错误,在读取时没有切换输入模式。
- 延时不够精确,单总线时序超时。
- 传感器供电不稳定。
一种快速验证 DS18B20 是否正常的方法是:先用串口打印每次 Reset 函数返回的存在脉冲状态码。如果一直是 1,说明传感器根本没有响应主机。如果存在脉冲正常,但温度读取异常,可以检查读时序里每一位的电平持续时间。
DS18B20 读取还有一些细节,比如读取字节时应该从低位开始循环 8 次;读每一位时,主机要先拉低总线产生读时隙。很多第一次移植的人会在这里写错,导致读出的字节完全不对。
7.3 舵机抖动或者无法转到指定角度
舵机抖动的常见原因是 PWM 周期不是精确的 20ms。如果你的定时器预分频计算有误差,舵机可能会在一个角度附近来回抖动。SG90 的响应非常敏感,建议先用逻辑分析仪抓一下脉冲长度,确认高电平持续时间在 0.5ms 到 2.5ms 之间,再验证角度。
另一个问题是舵机的扭矩不足,尤其在摇头结构机械阻力较大时,舵机会卡在某个位置并发出嗡嗡声。此时需要检查风扇摇头机构装配是否顺滑,或换用 MG996R 这类大扭矩舵机。
7.4 人体红外模块误触发
HC-SR501 在开机前 30 秒左右会输出几个高电平脉冲,程序里可以通过添加“开机自检延时”来避免,也就是系统启动后先延时 30 秒再启用人体检测模块的判断逻辑。
另外,模块的感应距离默认最远可达 7 米,对于桌面风扇来说太远了。建议把感应距离电位器调到最小,大概 1 米到 2 米,这样系统更符合“人坐在风扇前”就转、离开后停的预期。
7.5 Keil 工程中“.c 文件文件夹需要 include 吗”
有些初学者会问:“STM32 include 要包含 .c 的文件夹吗?”其实头文件搜索路径 add 的是.h文件所在目录,不是.c文件目录。编译器编译时会自动编译工程里添加的.c源文件,而#include指令查找的是.h头文件。如果编译报错找不到xxx.h,就去 Options for Target → C/C++ → Include Paths 中添加对应路径。
7.6 STM32 延时函数卡死问题
在 STM32 开发中常会遇到延时函数卡死的情况,比如使用 HAL_Delay() 时死循环。主要原因有两个:一是没有正确配置 SysTick 中断优先级,二是在中断服务函数中调用了延时函数。使用 HAL_Delay() 时不要在中断里调用,特别是优先级高于 SysTick 的中断回调中,一旦调用就会死锁。可以用非阻塞标志位方式替代延时,或者改用 DWT 计数器实现延时。
8. 最佳实践与工程建议
8.1 代码规范
做单片机项目时,许多人的通病是“功能先跑通再说”,结果到后期添加新功能时,自己都看不懂自己写的代码。在做这种多外设协同的项目时,建议从一开始就实行模块化编程,文件命名要清晰,函数功能要单一,变量命名尽量体现含义。比如用human_detected而不是flag,用temperature而不是tmp。
每次修改代码前先备份一个可用版本,或者使用 Git 管理工程。不要觉得一个人做项目就不需要版本管理,很多时候改了一下午代码调不出来,回头想恢复到昨天的可用版本却发现已经回不去了,这在毕业设计阶段非常浪费时间。
8.2 调试信息输出
调试信息输出是这个项目快速定位问题的关键。可以把串口的 printf 重定向做好,在关键节点打印日志,例如:
- DS18B20 的复位状态。
- 当前读取的原始温度值。
- 人体红外引脚电平。
- 自动模式状态机的切换动作。
- PWM 占空比设定值。
通过串口可以观察到传感器数据是否正常、代码逻辑是否按预期跳转,这样就不需要经常停下来点仿真器查看变量了。如果你在 Keil 仿真时看变量不熟练,或者发现 System View 窗口是空白的,直接用串口打印反而是更直观有效的方式。
8.3 硬件安全与防错
涉及电机、舵机这类感性负载时,要在电路设计阶段就考虑保护问题。反向续流二极管不能省略,MOS 管栅极下拉电阻不能省。电源部分尽量区分数字地和功率地,如果做不到,也要保证系统共地可靠。
做实物调试时,建议先用稳压电源限流供电,比如先限制电流不超过 500mA。一旦接线有问题,也不会立刻烧坏芯片或传感器。同时,严禁在上电状态下插拔杜邦线,因为带电插拔很容易造成引脚间短路,导致单片机 IO 口损坏。
8.4 毕业设计文档写作建议
如果你的这个风扇项目是作为毕业设计或者课程设计提交,除了实物和代码,还需要写一份完整的设计文档。论文结构可以参考如下顺序:
- 课题背景与意义。
- 国内外研究现状或同类产品调研。
- 系统需求分析与方案选型。
- 硬件设计方案,按模块讲解电路原理。
- 软件设计方案,给出主流程、状态机图、关键代码分析。
- 系统调试与结果分析,展示温度检测精度、调速响应、功耗对比。
- 总结与展望。
写论文时不要把代码全部贴进去,更建议只贴核心伪代码或者关键代码片段,然后在文字中解释设计意图。要让评阅老师能看出你理解系统、理解代码,而不只是搬运了一套现成源码。
8.5 “绿色节能”的数据支撑
因为是“智能绿色风扇”,建议在调试阶段测一组有说服力的数据。比如在同样环境下,普通定速风扇连续运行 2 小时,记录平均功率;智能风扇在使用者短暂离开时自动停转,温度较低时自动降速,对比两组耗电量。用功率计或万用表电流数据做一个小表格,不仅能让项目看起来更有完成度,也直接支撑了题目里“绿色”这个关键词。
9. 总结与后续扩展方向
到这里,基于 STM32 的智能绿色风扇项目涉及的核心内容就已经讲得比较完整了,从器件选型到硬件电路,从软件状态机到代码实现,再到实物调试与常见问题。这套开发方法其实不仅适用于风扇项目,换一个外壳和传感器,你完全可以把它改造成智能台灯、智能空气净化器甚至小型农业大棚控制系统。
如果接下来想给这个项目增加亮点,可以从几个方向入手:
- 增加蓝牙或 Wi-Fi 模块,做一个手机 App 远程控制风扇,比如采用 ESP8266 与 STM32 通信,在 App 上切换模式和查看温度。
- 增加温湿度传感器,把湿度也纳入风扇控制逻辑,适合南方潮湿天气时使用。
- 增加风扇堵转检测,堵转时自动断电保护,延长电机寿命。
- 加入 RTOS,比如 FreeRTOS 或 RT-Thread,把传感器采集、控制逻辑、显示刷新分别放到不同任务中。
- 使用低功耗模式,待机状态时进入 STOP 模式,把整机功耗降到最低,这也真正呼应“绿色节能”的主题。
刚开始调试这个项目时,不建议追求一次把所有功能都接上。更稳的路线是:先点亮 OLED,再用按键控制 LED 亮灭,然后把按键换成 PWM 控制风扇转速,最后再加入温度传感器和人体红外模块。每增加一个功能就验证一次,整个项目做下来会顺畅得多。