基于STM32的智能绿色风扇设计:从PWM调速到闭环控制
2026/9/3 3:20:19 网站建设 项目流程

在很多单片机毕业设计题目里,基于STM32的智能绿色风扇是一个很适合作为综合项目的方向。它看起来像是一个小家电改进方案,实际上会用到 GPIO、定时器、PWM、ADC、外部中断、按键消抖、状态机、串口调试和低功耗设计。这类项目最大的价值不在于“风扇会转”,而在于把上面的知识点串成一个能稳定运行、能现场演示、能写进论文的完整系统。

这篇文章适合正在做单片机课程设计、毕业设计或技术竞赛的人阅读。你可以用同样的思路完成一个“温度采集 + 自动调速 + 手动控制 + 状态显示”的智能风扇控制系统,并在此基础上继续加 WiFi、云平台、手机 App 等扩展功能。

1. 为什么把“绿色”设计成风扇的目标之一

1.1 普通风扇最大的浪费是按固定转速运行

普通风扇通常只有几个固定挡位。用户手动选一个挡位后,风扇就会一直以这个转速运行,不管房间温度是升高还是降低。这样的方式在原理上简单,但存在两个明显问题:一是温度较低时仍然高速运行,浪费电能;二是噪声和风感并不随着人的需求变化,长时间使用体验不好。

在毕业设计里加入“绿色”二字,并不是要求作品使用太阳能或储能电池,而是要把节能设计落到控制逻辑上。风扇可以根据环境温度自动决定转速,温度低时停止或低速运行,温度高时逐渐加速。这样既减少了不必要的用电,也降低了运行噪声,整套闭环会更有实际意义。

1.2 智能与绿色如何在同一个控制器里统一

实现这项设计需要两类输入和一类输出:

  • 输入 1:当前环境温度;
  • 输入 2:用户选择的控制模式;
  • 输出:风扇电机的 PWM 占空比。

控制的核心逻辑是“量入为出”。单片机周期读取温度,根据预设的温度区间计算目标转速,再通过 PWM 调节电机功率。用户也可以切换到手动模式,用按键直接选择挡位。整个过程没有复杂算法,却能把传感器、执行器、人机交互合在一起,这正是单片机综合项目最常见的工程形态。

1.3 项目主线从 PWM 到闭环控制

很多初学者会把人机交互或 OLED 显示当作核心内容,实际上这个项目的技术主线应当是:

  1. 用定时器产生频率稳定的 PWM 信号;
  2. 用 PWM 占空比控制电机功率;
  3. 用 ADC 或数字温度传感器得到环境温度;
  4. 用状态机处理自动模式和手动模式的切换;
  5. 用低功耗和周期间隙让系统更“绿色”。

后文会按照这条主线展开。先确定硬件方案,再配置工程,接着写控制逻辑,最后做测试和排错。

2. 系统方案与硬件准备

2.1 文字版系统框图和工作流程

先不画复杂框图,用文字描述一个最小系统的工作流程:

  1. 温度传感器将当前环境温度转换成电压或数字信号;
  2. STM32 周期性采集温度;
  3. 系统判断当前处于自动模式还是手动模式;
  4. 自动模式下,根据温度查表或线性计算目标占空比;
  5. 手动模式下,目标占空比来自按键设置;
  6. 控制函数以缓慢步进方式靠近目标占空比;
  7. PWM 输出到电机驱动模块,最终带动风扇转动;
  8. OLED 显示当前温度、模式、风速百分比和关键状态。

这套流程可以在一开始被简化成“固定温度驱动风扇转动”,再逐步加入自动控制。按流程做的好处是每一层都能独立验证。

2.2 元器件选型与参考引脚接线

课程设计常用主控是 STM32F103C8T6 最小系统板,这一类芯片资料多、价格常见、CubeMX 支持完善。风扇选择 5V 直流小风扇即可,如果使用 12V 风扇,需要额外提供 12V 电源和合适的驱动模块。

下面的引脚表是一组参考分配。实际项目中要根据自己的开发板、驱动模块和引脚占用情况微调,不能直接照抄。

外设MCU 引脚模块接口说明
PWM 输出PA6(TIM3_CH1)L9110S 的 IA控制风扇转速
方向控制PA7(GPIO 输出)L9110S 的 IB本设计固定转,可输出低电平
温度采集PA1(ADC1_IN1)LM35 的 VoutADC 读取电压
OLED SCLPB6OLED 的 SCL使用软件 I2C 时配置为 GPIO 输出
OLED SDAPB7OLED 的 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); } }

这个测试的检查点有四个:

  1. 下载后程序是否正常进入运行状态;
  2. 风扇是否在 2 秒后明显加速;
  3. 电机是否有异常噪声;
  4. 用手触摸驱动芯片是否温度过高。

如果风扇不转,先不检查控制逻辑,直接测量 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 自动调速本身就是节能手段

传统风扇在全速运行时功耗最高。智能风扇在温度较低时输出较低占空比,电机平均功率下降,这是最直接、最可解释的节能收益。

实际测试时不要只演示“风扇能自动转”,还要设计一组对比:

  1. 固定全速运行一段时间,记录电流或功率;
  2. 自动模式运行相同时间,记录电流或功率;
  3. 把加热源移走,让温度下降,记录风速回落时间。

通过这组测试可以解释“绿色”不是口头概念,而是控制策略带来的可测量结果。

5.2 选择效率更高的电机驱动方式

小功率直流电机可以使用 L9110S 模块。更追求性能时,可以选择由 N 沟道 MOS 管组成的电机驱动电路。MOS 导通电阻小,驱动压降低,整机效率更高。使用 ULN2003 这类达林顿管虽然接线简单,但导通压降偏大,在小电压电机上会造成额外损耗。

如果毕业设计把“能效”作为重点,可以对比几种驱动方式:

驱动方式优点注意点适用场景
L9110S 模块体积小、接线简单最大电流有限小型直流风扇
L298N 模块电流大、抗堵转能力较强压降大、体积大教学实验
MOS 管驱动损耗低、可做到高频 PWM需要保护电路追求效率的场景

5.3 降低系统自身的功耗

除了电机,控制器本身也有功耗。在“绿色”方向进一步优化时,可以这样做:

  • 不需要看 OLED 时,降低刷新频率或关闭 OLED;
  • 温度采集不是越快越好,可以每 500ms 到 1s 读一次;
  • 不需要用到的外设时钟在空闲时关闭;
  • 如果使用电池供电,可以尝试让 MCU 进入 Sleep 或 Stop 模式,由定时器或按键唤醒。

这里要区分课程设计和产品化。课程设计阶段先保证功能稳定,把主循环写成周期调度即可。如果要突出低功耗,再引入低功耗模式和外部中断唤醒,否则会引入大量调试问题。

5.4 能效测量方法

没有专业功率计时,可以用电流表串联在电机供电回路中。如果使用 USB 供电,也可以在 VBUS 和

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询