在很多单片机毕业设计题目里,基于STM32的智能绿色风扇是一个很适合作为综合项目的方向。它看起来像是一个小家电改进方案,实际上会用到 GPIO、定时器、PWM、ADC、外部中断、按键消抖、状态机、串口调试和低功耗设计。这类项目最大的价值不在于“风扇会转”,而在于把上面的知识点串成一个能稳定运行、能现场演示、能写进论文的完整系统。
这篇文章适合正在做单片机课程设计、毕业设计或技术竞赛的人阅读。你可以用同样的思路完成一个“温度采集 + 自动调速 + 手动控制 + 状态显示”的智能风扇控制系统,并在此基础上继续加 WiFi、云平台、手机 App 等扩展功能。
1. 为什么把“绿色”设计成风扇的目标之一
1.1 普通风扇最大的浪费是按固定转速运行
普通风扇通常只有几个固定挡位。用户手动选一个挡位后,风扇就会一直以这个转速运行,不管房间温度是升高还是降低。这样的方式在原理上简单,但存在两个明显问题:一是温度较低时仍然高速运行,浪费电能;二是噪声和风感并不随着人的需求变化,长时间使用体验不好。
在毕业设计里加入“绿色”二字,并不是要求作品使用太阳能或储能电池,而是要把节能设计落到控制逻辑上。风扇可以根据环境温度自动决定转速,温度低时停止或低速运行,温度高时逐渐加速。这样既减少了不必要的用电,也降低了运行噪声,整套闭环会更有实际意义。
1.2 智能与绿色如何在同一个控制器里统一
实现这项设计需要两类输入和一类输出:
- 输入 1:当前环境温度;
- 输入 2:用户选择的控制模式;
- 输出:风扇电机的 PWM 占空比。
控制的核心逻辑是“量入为出”。单片机周期读取温度,根据预设的温度区间计算目标转速,再通过 PWM 调节电机功率。用户也可以切换到手动模式,用按键直接选择挡位。整个过程没有复杂算法,却能把传感器、执行器、人机交互合在一起,这正是单片机综合项目最常见的工程形态。
1.3 项目主线从 PWM 到闭环控制
很多初学者会把人机交互或 OLED 显示当作核心内容,实际上这个项目的技术主线应当是:
- 用定时器产生频率稳定的 PWM 信号;
- 用 PWM 占空比控制电机功率;
- 用 ADC 或数字温度传感器得到环境温度;
- 用状态机处理自动模式和手动模式的切换;
- 用低功耗和周期间隙让系统更“绿色”。
后文会按照这条主线展开。先确定硬件方案,再配置工程,接着写控制逻辑,最后做测试和排错。
2. 系统方案与硬件准备
2.1 文字版系统框图和工作流程
先不画复杂框图,用文字描述一个最小系统的工作流程:
- 温度传感器将当前环境温度转换成电压或数字信号;
- STM32 周期性采集温度;
- 系统判断当前处于自动模式还是手动模式;
- 自动模式下,根据温度查表或线性计算目标占空比;
- 手动模式下,目标占空比来自按键设置;
- 控制函数以缓慢步进方式靠近目标占空比;
- PWM 输出到电机驱动模块,最终带动风扇转动;
- OLED 显示当前温度、模式、风速百分比和关键状态。
这套流程可以在一开始被简化成“固定温度驱动风扇转动”,再逐步加入自动控制。按流程做的好处是每一层都能独立验证。
2.2 元器件选型与参考引脚接线
课程设计常用主控是 STM32F103C8T6 最小系统板,这一类芯片资料多、价格常见、CubeMX 支持完善。风扇选择 5V 直流小风扇即可,如果使用 12V 风扇,需要额外提供 12V 电源和合适的驱动模块。
下面的引脚表是一组参考分配。实际项目中要根据自己的开发板、驱动模块和引脚占用情况微调,不能直接照抄。
| 外设 | MCU 引脚 | 模块接口 | 说明 |
|---|---|---|---|
| PWM 输出 | PA6(TIM3_CH1) | L9110S 的 IA | 控制风扇转速 |
| 方向控制 | PA7(GPIO 输出) | L9110S 的 IB | 本设计固定转,可输出低电平 |
| 温度采集 | PA1(ADC1_IN1) | LM35 的 Vout | ADC 读取电压 |
| OLED SCL | PB6 | OLED 的 SCL | 使用软件 I2C 时配置为 GPIO 输出 |
| OLED SDA | PB7 | OLED 的 SDA | 使用软件 I2C 时配置为 GPIO 输出 |
| 模式按键 | PA0 | 按键到 GND | 自动 / 手动切换 |
| 调挡按键 | PA2 | 按键到 GND | 手动模式上调风速 |
| 串口发送 | PA9(USART1_TX) | USB 转 TTL 的 RXD | 调试日志 |
| 串口接收 | PA10(USART1_RX) | USB 转 TTL 的 TXD | 可选遥控指令 |
需要注意,PA0 在很多板子上同时是 WKUP 引脚。如果把它作为普通按键输入,需要上拉电阻,并且按键按下后输出低电平。使用外部上拉时,按键的另一端接 GND,避免悬空导致误触发。
按键开关最好配置为输入上拉。初始化时 GPIO 默认是高电平,按下按键后变成低电平。读取到低电平后做 20ms 到 50ms 延时消抖,再判断一次,防止机械抖动产生多次触发。
2.3 电机驱动与 PWM 频率的关系
不要用 STM32 的 GPIO 直接驱动电机。STM32 引脚输出电流有限,电机起动电流又大,直接连接可能造成复位甚至芯片损坏。
常见做法是使用 L9110S 这类电机驱动模块。L9110S 模块有两个输入引脚 IA 和 IB,把 IA 接 PWM,IB 固定为低电平即可控制一个方向转动。PWM 占空比越大,电机平均电压越大,转速越快。
PWM 频率不能太低。如果选择 1kHz 以下的 PWM,电机转动时会有明显抖动,甚至出现可听的“吱吱”声。常见直流电机驱动会选择 10kHz 到 25kHz 的 PWM。下面示例工程以 20kHz 为目标频率,这个频率在 STM32 定时器上很容易配置,也可以在听感上避开大多数人的低频敏区间。
2.4 电源与复位安全注意事项
电机属于功率器件,MCU 属于弱电器件,两者不能过于简单共用一个供电:
- 如果使用 5V USB 供电的小风扇,理论上可以和开发板共用 5V,但必须共地;
- 如果使用 12V 风扇或电流较大的电机,建议单独使用 12V 适配器,电机电源与 MCU 电源分开;
- MCU 端电源可以由 12V 经过 DC-DC 降压到 5V,再经过 LDO 得到 3.3V,也可以用独立 USB 供电;
- 驱动模块的输入信号地必须和 STM32 的地连接,否则 PWM 信号没有参考地,电机无法正常控制。
注意:任何时刻都不要在通电状态下插拔电机线和传感器线。学习板上没有完整的防护设计,热插拔容易造成电压尖峰,严重时可能烧坏 ADC 引脚或驱动芯片。
3. 开发环境准备与工程初始化
3.1 软件工具清单
| 工具 | 用途 | 说明 |
|---|---|---|
| STM32CubeMX | 生成初始化工程 | 用来配置时钟、GPIO、ADC、USART、TIM |
| Keil MDK 或 STM32CubeIDE | 编译下载 | 两者任选,C 代码可以复用 |
| STM32 HAL 固件包 | 提供驱动库 | 安装与主控型号匹配的固件包 |
| 串口调试助手 | 查看调试日志 | 用来观察温度和占空比变化 |
| 万用表 | 测量电压和通断 | 排查接线问题 |
| USB 示波器或逻辑分析仪 | 观察 PWM 波形 | 不是必须,但排查信号很有效 |
在新建工程前,先确认开发板、下载器、板载仿真器各自是什么类型。ST-Link、J-Link、DAP-Link 的接线方式不同,最容易出问题的不是代码,而是 debug 配置和驱动没装好。
3.2 STM32CubeMX 关键配置
以 STM32F103C8T6 为例,在 CubeMX 中按以下顺序配置:
- RCC:HSE 选择 Crystal/Ceramic Resonator;
- Debug:选择 Serial Wire,保留 SWD 下载能力;
- 时钟:将 SYSCLK 配置为最大工作频率,常见板子可到 72MHz;
- PA1:配置为 ADC1_IN1;
- PA6:配置为 TIM3_CH1;
- PA7:配置为 GPIO Output;
- PA0、PA2:配置为 GPIO Input;
- PA9、PA10:配置为 USART1,波特率 115200;
- PB6、PB7:配置为 GPIO Output,用于 OLED 软件 I2C;
- TIM3:选择 PWM Generation CH1。
ADC 配置中,分辨率选择 12 位,转换模式可以先使用单次转换。控制逻辑中需要多个样本做平均,因此在代码里手动开始转换、等待转换完成、读值,再停止转换。连续转换看起来效率更高,但会让初学阶段的代码排查变得复杂。
使用版本较新的 CubeMX 时,生成的工程以.ioc为中心。不要直接改动 Makefile 或项目文件,所有引脚配置修改都要回到.ioc文件重新生成,否则很容易出现“代码改乱了但配置没同步”的问题。
3.3 PWM 参数计算公式
20kHz PWM 的计算思路如下:
假如系统时钟为 72MHz,APB1 定时器时钟也设置为 72MHz,PSC 设置为 0,则 PWM 频率为:
F_PWM = 定时器时钟 / ((ARR + 1) * (PSC + 1))如果希望频率为 20kHz,则:
ARR + 1 = 72000000 / 20000 = 3600 ARR = 3599因此 TIM3 配置为:
- Prescaler:0
- Counter Period:3599
- Pulse:初始值 0
- PWM 模式:Mode 1
之后调用 HAL 接口启动 PWM:
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1);设置不同占空比时,不直接写百分比,而是把百分比换算成 CCR 比较值:
void fan_set_speed( TIM_HandleTypeDef *htim, uint32_t channel, uint8_t percent) { uint16_t period = htim->Init.Period; uint32_t ccr; if (percent > 100U) { percent = 100U; } ccr = (uint32_t)((uint32_t)percent * (period + 1U) / 100U); __HAL_TIM_SET_COMPARE(htim, channel, ccr); }这里的percent表示 0 到 100 的百分比。ARR 等于 3599 时,完整周期为 3600 个计数;占空比 50% 对应 CCR 约等于 1800。
3.4 先做一个最小测试,确认 PWM 能输出
不用一上来就写完整控制逻辑,先做一个最小测试:
int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_TIM3_Init(); HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1); fan_set_speed(&htim3, TIM_CHANNEL_1, 20U); HAL_Delay(2000); fan_set_speed(&htim3, TIM_CHANNEL_1, 80U); HAL_Delay(2000); while (1) { fan_set_speed(&htim3, TIM_CHANNEL_1, 0U); } }这个测试的检查点有四个:
- 下载后程序是否正常进入运行状态;
- 风扇是否在 2 秒后明显加速;
- 电机是否有异常噪声;
- 用手触摸驱动芯片是否温度过高。
如果风扇不转,先不检查控制逻辑,直接测量 PWM 引脚是否输出信号。此时最容易定位问题:是驱动模块没工作,还是 STM32 没输出 PWM。若没有示波器,也可以用万用表测量占空比高于零时引脚的平均电压,但逻辑分析仪比万用表更直观。
4. 控制软件实现
4.1 使用状态机组织系统
当系统包含自动模式、手动模式、按键和 PWM 输出时,建议用一组 struct 保存所有运行状态,而不是在 main 函数里堆一堆全局变量。
typedef enum { FAN_MODE_AUTO = 0, FAN_MODE_MANUAL } FanMode_t; typedef struct { FanMode_t mode; uint8_t cur_percent; uint8_t target_percent; float temperature_c; uint32_t last_ctrl_tick; uint32_t last_display_tick; } Fan_State_t;cur_percent表示当前 PWM 输出比例,target_percent表示根据模式计算出的目标值。实际控制时每秒只变化几个百分点,避免风速突变。
使用 struct 的好处是调试时便于观察,也方便后续把系统封装成一个模块。代码中不需要每个函数都访问全局变量,而是把状态结构体的指针传给相关函数。
4.2 温度采集与简单滤波
LM35 是一种输出电压与环境温度成线性关系的模拟温度传感器,典型输出约为 10mV/℃。25℃ 时输出约 250mV,这个电压低于 3.3V,因此可以接到 STM32 的 ADC 引脚。
读取一次 ADC 的示例:
uint16_t read_adc_value(void) { uint16_t adc = 0; HAL_ADC_Start(&hadc1); if (HAL_ADC_PollForConversion(&hadc1, 50) == HAL_OK) { adc = HAL_ADC_GetValue(&hadc1); } HAL_ADC_Stop(&hadc1); return adc; }ADC 的 12 位转换结果范围是 0 到 4095。假设 ADC 参考电压为 3.3V,则每个计数对应的电压为:
电压(mV) = adc_value * 3300.0 / 4095.0 温度(℃) = 电压(mV) / 10.0把多次读取取平均,可以减少噪声:
float read_temperature_c(void) { uint32_t sum = 0U; uint8_t i; uint16_t adc_value; for (i = 0U; i < 8U; i++) { adc_value = read_adc_value(); sum += adc_value; HAL_Delay(1); } adc_value = (uint16_t)(sum / 8U); return (float)adc_value * 3.3f / 4095.0f * 100.0f; }这里结果的单位是 ℃。注意不要让传感器靠近 STM32、驱动芯片或电机本体。若传感器贴到发热器件上,自动调速功能会错误地认为环境温度已经很高。
4.3 自动模式目标风速计算
温度到风速的映射不需要非常复杂,但要避免“单阈值频繁启停”的问题。这里使用一个带关断回差的线性区间:
- 环境温度低于 24℃:风扇停止;
- 环境温度在 24℃ 到 26℃:允许最低转速运行;
- 环境温度在 26℃ 到 34℃:占空比从 20% 线性增加到 80%;
- 环境温度超过 34℃:自动模式最大输出 80%。
自动模式下保留 80% 作为上限,是考虑到节能和噪声。如果用户希望更高风速,可以切到手动模式。最低转速不要从 0% 直接跳到 20%,因为直流电机在太低电压下可能无法启动,需要提供更柔和且足够有效的初始 PWM。
计算函数可以写成:
uint8_t calc_auto_target(float temp_c) { if (temp_c < 24.0f) { return 0U; } else if (temp_c < 26.0f) { return 20U; } else if (temp_c < 34.0f) { float ratio = (temp_c - 26.0f) / (34.0f - 26.0f); uint8_t target = 20U + (uint8_t)(ratio * 60.0f); return target; } else { return 80U; } }这套区间不是唯一答案,真正落地时要根据风扇电机、场景和评审需求调整。这里要解释清楚的是思路:设定多个阈值而不是一个阈值,让输出与温度之间形成可理解的关系。
4.4 手动模式与按键读取
手动模式一般提供几个固定挡位,比如 0%、20%、40%、60%、80%、100%。模式按键负责在自动和手动之间切换,挡位按键负责循环增加挡位。
按键扫描要每隔 20ms 到 50ms 执行一次,配合状态缓存做边沿检测。下面的代码思路是按下一次改变一次,按住不放不会连续加挡:
static uint8_t last_key2 = 1U; uint8_t key2_pressed(void) { uint8_t current = HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_2); uint8_t pressed = 0U; if (last_key2 == 1U && current == 0U) { pressed = 1U; } last_key2 = current; return pressed; }这种“上一次状态为高、当前状态为低”的写法就是下降沿检测。如果按键没有接上拉电阻,GPIO 悬空会反复跳变,按键功能会极不稳定。
手动挡位可以定义为数组:
static const uint8_t manual_levels[] = {0U, 20U, 40U, 60U, 80U, 100U}; static uint8_t manual_index = 2U; void manual_step_up(void) { manual_index++; if (manual_index >= (sizeof(manual_levels) / sizeof(manual_levels[0]))) { manual_index = 0U; } }手动模式下,system.target_percent = manual_levels[manual_index]。
4.5 主循环调度
主循环不需要跑得特别快,否则所有任务都会挤在一起。更合适的做法是让不同任务按自己的周期运行。
while (1) { uint32_t now = HAL_GetTick(); if (now - system.last_ctrl_tick >= 50U) { system.last_ctrl_tick = now; if (system.mode == FAN_MODE_AUTO) { system.temperature_c = read_temperature_c(); system.target_percent = calc_auto_target(system.temperature_c); } else { system.target_percent = manual_levels[manual_index]; } if (system.cur_percent < system.target_percent) { system.cur_percent++; } else if (system.cur_percent > system.target_percent) { system.cur_percent--; } fan_set_speed(&htim3, TIM_CHANNEL_1, system.cur_percent); printf("temp=%.1f target=%d cur=%d mode=%d\r\n", system.temperature_c, system.target_percent, system.cur_percent, system.mode); } if (now - system.last_display_tick >= 500U) { system.last_display_tick = now; display_refresh(&system); } scan_mode_button(); scan_step_button(); }关键点是“先计算目标,再逐步靠近目标”。如果直接把温度换算出来的占空比赋给电机,温度跳变时风扇转速会瞬间突变。步进 1% 的方式让转速变化更平滑,也减少对机械结构的冲击。
4.6 OLED 显示内容
OLED 显示并非控制系统的必备部分,但能显著提升演示效果。建议显示:
- 当前温度;
- 当前模式(AUTO / MANUAL);
- 当前挡位或百分比;
- 传感器是否正常。
OLED 驱动选择常见的 SSD1306 方案即可。很多初学者会把 I2C 初始化和主循环刷屏放在一起,导致温度采集被频繁打断。比较好的做法是设置一个 500ms 刷新周期,只在主循环空闲时更新屏幕。
注意:OLED 屏幕刷新不要放在 PWM 中断里,也不要在按键消抖死循环中长时间调用。否则会造成主循环卡顿,温度更新不及时。
5. “绿色”如何落地:调速、休眠与能效测量
5.1 自动调速本身就是节能手段
传统风扇在全速运行时功耗最高。智能风扇在温度较低时输出较低占空比,电机平均功率下降,这是最直接、最可解释的节能收益。
实际测试时不要只演示“风扇能自动转”,还要设计一组对比:
- 固定全速运行一段时间,记录电流或功率;
- 自动模式运行相同时间,记录电流或功率;
- 把加热源移走,让温度下降,记录风速回落时间。
通过这组测试可以解释“绿色”不是口头概念,而是控制策略带来的可测量结果。
5.2 选择效率更高的电机驱动方式
小功率直流电机可以使用 L9110S 模块。更追求性能时,可以选择由 N 沟道 MOS 管组成的电机驱动电路。MOS 导通电阻小,驱动压降低,整机效率更高。使用 ULN2003 这类达林顿管虽然接线简单,但导通压降偏大,在小电压电机上会造成额外损耗。
如果毕业设计把“能效”作为重点,可以对比几种驱动方式:
| 驱动方式 | 优点 | 注意点 | 适用场景 |
|---|---|---|---|
| L9110S 模块 | 体积小、接线简单 | 最大电流有限 | 小型直流风扇 |
| L298N 模块 | 电流大、抗堵转能力较强 | 压降大、体积大 | 教学实验 |
| MOS 管驱动 | 损耗低、可做到高频 PWM | 需要保护电路 | 追求效率的场景 |
5.3 降低系统自身的功耗
除了电机,控制器本身也有功耗。在“绿色”方向进一步优化时,可以这样做:
- 不需要看 OLED 时,降低刷新频率或关闭 OLED;
- 温度采集不是越快越好,可以每 500ms 到 1s 读一次;
- 不需要用到的外设时钟在空闲时关闭;
- 如果使用电池供电,可以尝试让 MCU 进入 Sleep 或 Stop 模式,由定时器或按键唤醒。
这里要区分课程设计和产品化。课程设计阶段先保证功能稳定,把主循环写成周期调度即可。如果要突出低功耗,再引入低功耗模式和外部中断唤醒,否则会引入大量调试问题。
5.4 能效测量方法
没有专业功率计时,可以用电流表串联在电机供电回路中。如果使用 USB 供电,也可以在 VBUS 和