许多单片机毕业设计题目看起来相似,但真正拉开差距的往往是系统设计的完整性和“绿色智能”这两个词的落地程度。市面上很多风扇控制方案只是用手动按键调速,离“智能”还有一段距离;而如果只做一个温度控制风扇,又缺少人体感应、自动/手动模式切换等工程化设计,难以体现完整的系统思维。
本文围绕“基于 STM32 的智能绿色风扇”展开,梳理一套可直接用于毕业设计答辩和实物演示的实现方案。内容覆盖需求分析、器件选型、电路设计、STM32 核心代码、传感器驱动、PWM 调速逻辑、调试方法以及常见坑点。无论你是刚接触 STM32 的本科生,还是想快速搭建一个单片机综合项目的开发者,都可以照着本文一步步完成。
1. 智能绿色风扇的需求背景与设计思路
1.1 什么是智能绿色风扇
智能绿色风扇并不是一个标准名词,而是单片机系统设计中常见的“节能 + 自动控制”类项目。它通常具备以下能力:
- 根据环境温度自动调整风扇转速,温度高时转速升高,温度低时转速降低或停止。
- 通过人体红外感应判断是否有人存在,无人在场时自动进入待机/停转状态。
- 提供自动模式和手动模式,既能全自动运行,也能由用户手动调速。
- 能够显示当前温度、工作模式、风速档位等信息,方便演示和答辩。
从“绿色”角度看,核心目标不是“风扇本身有多节能”,而是通过合理控制策略,避免风扇长期满速空转,从而节省电能、延长电机寿命。
从“智能”角度看,系统至少需要两路输入:一路是环境温度,一路是人体存在信号。单片机作为控制核心,根据这两路输入决定执行器(直流电机/风扇模块)的输出状态。如果再增加光照检测、定时、无线控制,就属于进阶扩展。
1.2 为什么选择 STM32 作为主控
这套设计选择 STM32 而不是传统的 51 单片机,主要考虑以下几点:
- STM32 主频高、外设丰富,除了普通 GPIO,还内置多路定时器、ADC、I2C、USART 等外设,扩展能力强。
- 项目演示时,STM32 能够直接驱动 OLED、读取传感器数据、产生 PWM 波形,整体可展示性强。
- 毕业生在黑板上/论文中能写的内容更多,比如中断优先级、定时器配置、PWM 频率计算、ADC 采样,这些都是答辩时可以深入展开的知识点。
- STM32 开发资料非常成熟,网上可以找到大量参考工程,遇到问题容易排查。
当然,如果项目要求比较简单,用 STM32F103C8T6 最小系统板作为核心即可,成本较低、资料多、引脚足够。
1.3 系统整体框图
在正式开发前,先画出系统框图。典型的智能绿色风扇系统结构如下:
[温度传感器 DS18B20 / DHT11] ---> STM32 GPIO 读取 [人体红外模块 HC-SR501] ---> STM32 GPIO 电平检测 [按键(模式切换/手动调速)] ---> STM32 GPIO 输入 [OLED 显示屏] ---> STM32 I2C 接口 [直流电机/风扇模块] ---> STM32 定时器 PWM 输出 \\--- 电机驱动电路系统的信息流是:
- STM32 周期性读取温度传感器数据。
- STM32 检测人体红外模块输出电平。
- 根据当前模式(自动/手动)计算目标风速。
- 通过 PWM 占空比调节电机转速。
- OLED 显示温度值、模式和档位信息。
- 按键可切换模式、手动调节档位。
2. 硬件选型与开发环境准备
2.1 主控芯片与开发板
推荐使用 STM32F103C8T6 最小系统板,也就是常说的“STM32 蓝板”或“核心板”。它成本低、引脚引出方便,非常适合毕设实物焊接和面包板搭建。
关键技术参数:
- 内核:ARM Cortex-M3
- 主频:最高 72 MHz
- Flash:64 KB
- SRAM:20 KB
- 定时器:多个,包含高级定时器 TIM1 和通用定时器 TIM2/TIM3/TIM4
- ADC:12 位逐次逼近型 ADC,多通道
- 通信接口:I2C、SPI、USART、CAN 等
对于风扇控制项目,Flash 和 SRAM 完全够用。
2.2 电机与驱动模块
智能风扇项目通常有两种方式:
- 小功率直流风机:直接使用 5V 直流风扇模块,配合三极管/MOS 管或者 L298N/ULN2003 驱动。这种方式适合桌面小风扇演示。
- 大功率交流风扇:需要继电器或可控硅调压,涉及 220V 强电,安全性要求高,一般不推荐毕设实物随意操作。
如果没有特殊说明,建议选择 5V 直流电机或电脑散热风扇模块,由 USB/充电宝/电源适配器供电即可。驱动方式可以使用 NPN 三极管(如 S8050)+ 续流二极管,或者使用 MOSFET(如 AO3400),或者使用现成的 DRV8833 / L298N 模块。从学习角度出发,使用三极管或 MOS 管自己搭一个 PWM 驱动电路更有技术含量,但直接使用 L298N 模块更稳定。
在 PWM 驱动电机时,要注意频率不能太低,否则电机会发出明显的噪声;也不能太高,否则开关损耗增加。常见习惯选择 10kHz~20kHz 之间的 PWM 频率。
2.3 温度传感器选型
智能风扇需要感知环境温度。常见选择有两种:
- DS18B20:单总线数字温度传感器,测量范围 -55℃~+125℃,精度 ±0.5℃(-10℃~+85℃),可通过 GPIO 口直接读取,无需 ADC。
- DHT11/DHT22:温度和湿度传感器,DHT11 精度相对较低,适合要求不高的项目。
毕业设计建议优先使用 DS18B20。理由是:
- 单总线协议简单直观,能够展示底层时序编程能力。
- 数据是数字量,不需要校准。
- 演示时可以用手捂住传感器看到温度变化,再观察风扇转速变化,演示效果直观。
2.4 人体感应模块
常用模块是 HC-SR501 人体红外感应模块,基于热释电红外传感器(PIR)。
模块特性:
- 工作电压 5V~20V,通常用 5V 供电。
- 输出高电平信号,默认无入时输出低电平,检测到人体活动时输出高电平。
- 可以通过模块上的两个电位器调节灵敏度和延时时间。
使用时要特别注意:
- HC-SR501 在刚上电时有约 30s~60s 的预热/初始化时间,此时输出不稳定,容易被误触发。
- 人体传感器检测的是人体移动,静止的人长时间待在原地可能会因红外变化不明显而恢复低电平。所以软件控制逻辑要设计合理,不能完全依赖它判断“房间是否始终有人”。
2.5 显示模块
推荐使用 0.96 寸 OLED 显示屏,I2C 接口,SSD1306 驱动芯片。OLED 显示清晰、占用引脚少、刷新简单,非常适合在毕设实物中展示当前状态。
如果不想用 OLED,也可以用 LCD1602 液晶屏。但 LCD1602 需要占用 6 个甚至更多 IO,并且显示内容有限。对于能显示中文和图形的需求,OLED 是更好的选择。如果题目明确要求更复杂的界面,可以考虑带中文字库的 LCD12864 或 TFT 彩屏。
2.6 按键与电源
按键用于切换工作模式和手动调速。常见做法是接 4 个独立按键:
- 模式切换键(自动 / 手动)
- 风速加
- 风速减
- 开关机/确认
按键电路采用“按键一端接 GPIO,另一端接 GND”,内部启用上拉电阻,或外部接 10kΩ 上拉电阻。按下时引脚读到低电平,实现低电平有效。
电源部分,如果所有模块都是 5V 供电,可以直接用 USB 5V 电源;如果个别模块需要 3.3V,则用 AMS1117-3.3 稳压芯片转换,或者使用开发板自带的 3.3V 输出。
2.7 开发软件与版本说明
本文示例使用以下开发环境:
- Keil MDK 5(ARMCC 编译器),也可以使用 STM32CubeMX + Keil/GCC 组合,或者 VSCode + EIDE/PlatformIO,核心思路一致。
- STM32 标准外设库(Standard Peripheral Library),对于 STM32F1 系列资料较多,示例代码易读性好。
- STM32CubeMX 用于生成初始化代码时,可以选择 HAL 库版本,结构会更清晰。
版本方面,不同系列 Pack 会导致函数名略有差异。实际环境中的芯片型号、器件库版本可能不同,所以示范代码需要按自己的工程情况调整。最关键的是理解配置原理,而不是死记某个函数名。
3. 核心模块原理与配置方法
3.1 定时器 PWM 输出原理
PWM(Pulse Width Modulation,脉宽调制)通过调节高电平在一个周期内的占比,也就是占空比,来改变输出到电机的平均电压。
STM32 通用定时器输出 PWM 的核心逻辑是:
- 定时器计数器 CNT 从 0 增加到自动重装载值 ARR。
- 比较寄存器 CCR 存储一个比较值。
- 当 CNT 小于 CCR 时输出有效电平,当 CNT 大于等于 CCR 时输出另一个电平。
- 改变 CCR 即可改变占空比。
例如 ARR = 999 时,PWM 周期为 1000 个计数单位。若 CCR = 250,占空比为 25%,CCR = 500 时占空比为 50%。
PWM 频率计算公式:
PWM频率 = 定时器时钟频率 / ((ARR + 1) * (PSC + 1))例如 STM32F103 内部定时器挂在 APB1 上,APB1 经过倍频后通常为 72MHz。如果 PSC = 71,ARR = 99,则:
PWM频率 = 72MHz / ((99 + 1) * (71 + 1)) = 72000000 / 7200 = 10000 Hz也就是说,配置定时器分频系数为 71,自动重装载值为 99,即可得到 10kHz 的 PWM 波。
3.2 温度传感器 DS18B20 读取流程
DS18B20 使用单总线协议。一次完整温度转换需要以下步骤:
- 发送复位脉冲:主机将总线拉低至少 480μs,然后释放并等待存在脉冲。
- 检测存在脉冲:DS18B20 收到复位脉冲后,会拉低总线 60μs~240μs。
- 发送 ROM 命令:如果总线上只有一个传感器,可以发送跳过 ROM 命令 0xCC。
- 发送功能命令:启动温度转换命令 0x44。
- 等待转换完成:通常需要 750ms(12 位分辨率)。
- 再次复位并跳过 ROM,发送读取暂存器命令 0xBE。
- 读取低字节和高字节,合成为 16 位温度数据。
温度值计算方法:
- 符号位为 0 时,温度为正值。
- 温度 =(高字节 << 8 | 低字节)>> 4,精确到 0.0625℃。
- 也可以直接乘 0.0625 得到实际小数。
在编写底层代码时,最重要的是严格控制延时。DS18B20 对时序要求比较高,如果延时不准,读取会失败。可以用 STM32 的微秒级延时函数实现。
3.3 人体红外检测逻辑
HC-SR501 输出的是数字电平信号,单片机只需要读 GPIO 电平即可。但需要注意逻辑设计,不能简单认为“检测到高电平就一直开着风扇”。
推荐的控制逻辑是:
- 若人体传感器输出高电平,表示当前检测到有人活动,系统进入工作状态。
- 若人体传感器输出低电平,不代表一定没人,因为模块有延时设置,通常默认延时约 2s~3s。
- 为避免风扇因传感器短暂失效而频繁启停,软件上应加入“关断延时”。例如检测到无人后,继续运行 30s 或 60s 再停止,或者当温度较低且无人才逐渐关闭。
3.4 系统状态机设计
为了让程序结构清晰,推荐使用简单的状态机。
状态说明: [0] 关机/待机状态:不输出 PWM,OLED 显示待机。 [1] 自动模式运行状态:温度决定风速,无检测到人则等待关闭。 [2] 手动模式运行状态:用户通过按键加减风速。主循环每次扫描按键、传感器,然后根据状态机决定输出。这种方式比让空 while 循环里的代码堆在一起更清晰,也方便答辩时讲“系统采用状态机编程”。
4. 完整代码实战
下面以一个简化但功能完整的工程为例展开代码。重点展示:
- 初始化流程
- DS18B20 底层读取
- PWM 调速
- 主循环状态控制
- OLED 显示逻辑
为了便于阅读,以下代码使用“标准外设库”思路,不过为了工程通用性,也会注意函数命名友好。
4.1 工程结构建议
建议按照以下结构组织文件:
SmartFan/ ├── User/ │ ├── main.c │ ├── stm32f10x_it.c ├── Hardware/ │ ├── ds18b20.c │ ├── ds18b20.h │ ├── pwm.c │ ├── pwm.h │ ├── key.c │ ├── key.h │ ├── oled.c │ ├── oled.h │ ├── infrared.c │ └── infrared.h ├── System/ │ └── STM32 启动文件和库文件 └── Project/ └── MDK-ARM 工程文件这样功能拆分明确,不同模块之间的耦合度低,后续将 OLED 换成 LCD 或增加蓝牙模块,只需要新增对应硬件文件即可。
4.2 主函数框架
// 文件路径:User/main.c #include "stm32f10x.h" #include "delay.h" #include "ds18b20.h" #include "pwm.h" #include "key.h" #include "oled.h" #include "infrared.h" typedef enum { SYS_POWER_OFF = 0, SYS_AUTO_MODE, SYS_MANUAL_MODE } SysMode; volatile SysMode sysMode = SYS_AUTO_MODE; volatile uint8_t currentLevel = 1; // 当前风速档 0~4 volatile uint8_t manualLevel = 2; // 手动档位 volatile uint8_t personDetect = 0; // 是否检测到人 volatile float temperature = 25.0f; // 当前温度 static void System_Init(void); static void Task_KeyScan(void); static void Task_TemperatureRead(void); static void Task_MotorControl(void); static void Task_Display(void); int main(void) { System_Init(); while (1) { Task_KeyScan(); Task_TemperatureRead(); Task_MotorControl(); Task_Display(); delay_ms(100); } } static void System_Init(void) { delay_init(); OLED_Init(); DS18B20_Init(); PWM_Init(); KEY_Init(); Infrared_Init(); OLED_Clear(); OLED_ShowString(0, 0, "Smart Fan System"); }主循环采用轮询方式,每 100ms 执行一次。对于这个项目来说,轮询足够满足实时性要求。如果后续要加入无线模块、语音识别等耗时任务,再考虑使用定时器中断调度或 FreeRTOS。
4.3 DS18B20 驱动代码
下面是 DS18B20 初始化与读取温度的核心代码。
// 文件路径:Hardware/ds18b20.c #include "ds18b20.h" #include "delay.h" // GPIO 定义,这里以 PA0 为例 #define DS18B20_PORT GPIOA #define DS18B20_PIN GPIO_Pin_0 #define DS18B20_RCC RCC_APB2Periph_GPIOA static void DS18B20_Mode_Input(void) { GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = DS18B20_PIN; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; GPIO_Init(DS18B20_PORT, &GPIO_InitStructure); } static void DS18B20_Mode_Output(void) { GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = DS18B20_PIN; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(DS18B20_PORT, &GPIO_InitStructure); } static void DS18B20_Reset(void) { DS18B20_Mode_Output(); GPIO_ResetBits(DS18B20_PORT, DS18B20_PIN); delay_us(500); GPIO_SetBits(DS18B20_PORT, DS18B20_PIN); delay_us(60); DS18B20_Mode_Input(); } static uint8_t DS18B20_CheckPresence(void) { uint8_t presence = 1; // Reset 后读取存在脉冲 if (GPIO_ReadInputDataBit(DS18B20_PORT, DS18B20_PIN) == 0) { presence = 0; // 拉低说明存在 } delay_us(200); return presence; } static void DS18B20_WriteByte(uint8_t data) { uint8_t i; for (i = 0; i < 8; i++) { DS18B20_Mode_Output(); GPIO_ResetBits(DS18B20_PORT, DS18B20_PIN); delay_us(2); if (data & 0x01) { GPIO_SetBits(DS18B20_PORT, DS18B20_PIN); } else { GPIO_ResetBits(DS18B20_PORT, DS18B20_PIN); } delay_us(60); GPIO_SetBits(DS18B20_PORT, DS18B20_PIN); data >>= 1; } } static uint8_t DS18B20_ReadByte(void) { uint8_t i, data = 0; for (i = 0; i < 8; i++) { data >>= 1; DS18B20_Mode_Output(); GPIO_ResetBits(DS18B20_PORT, DS18B20_PIN); delay_us(2); GPIO_SetBits(DS18B20_PORT, DS18B20_PIN); DS18B20_Mode_Input(); if (GPIO_ReadInputDataBit(DS18B20_PORT, DS18B20_PIN)) { data |= 0x80; } delay_us(60); } return data; }然后封装一个获取温度的函数:
float DS18B20_GetTemperature(void) { uint8_t tempL = 0, tempH = 0; int16_t raw = 0; float temperature = 0.0f; DS18B20_Reset(); DS18B20_CheckPresence(); DS18B20_WriteByte(0xCC); // 跳过 ROM DS18B20_WriteByte(0x44); // 启动温度转换 delay_ms(750); // 等待转换完成 DS18B20_Reset(); DS18B20_CheckPresence(); DS18B20_WriteByte(0xCC); DS18B20_WriteByte(0xBE); // 读取暂存器 tempL = DS18B20_ReadByte(); tempH = DS18B20_ReadByte(); raw = (int16_t)((tempH << 8) | tempL); temperature = raw * 0.0625f; return temperature; }DS18B20 比较耗时的一点是温度转换等待时间较长。如果每 100ms 主循环都调用一次 DS18B20_GetTemperature,会卡住系统。因此更合理的做法是:
- 定义转换状态,每 750ms 才发起一次完整读取。
- 温度读取放在后台或低频任务中,主控制逻辑不依赖每次实时温度。
4.4 PWM 电机驱动代码
以 TIM2 的 CH1(PA0 在某些板子上作为 TIM2_CH1,但如果 PA0 被 DS18B20 占用,需要调整引脚,这里只作演示逻辑)为例。实际使用时,DS18B20 和数据手册引脚需要根据你的核心板分配,一般 PA0 是 TIM2_CH1 和 ADC 等多功能引脚,需要认真查看原理图。
如果使用 PA1 作为 TIM2_CH2,配置代码如下:
// 文件路径:Hardware/pwm.c #include "pwm.h" #include "stm32f10x.h" void PWM_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1; // TIM2_CH2 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); TIM_TimeBaseStructure.TIM_Prescaler = 71; // 72MHz / 72 = 1MHz TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseStructure.TIM_Period = 999; // PWM 频率 1kHz TIM_TimeBaseStructure.TIM_ClockDivision = 0; TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure); TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse = 0; TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; TIM_OC2Init(TIM2, &TIM_OCInitStructure); TIM_OC2PreloadConfig(TIM2, TIM_OCPreload_Enable); TIM_Cmd(TIM2, ENABLE); } void PWM_SetDuty(uint8_t duty_percent) { uint16_t ccr = 0; if (duty_percent > 100) { duty_percent = 100; } ccr = (uint16_t)(duty_percent * 10); // ARR=999,占空比调整,最大不超过999 TIM_SetCompare2(TIM2, ccr); }注意:这个配置里 PWM 频率是 1kHz,适合小风扇;如果想要 10kHz,TIM_Prescaler 不变,把 TIM_Period 改为 99,那么占空比调整公式也需要变。实际频率选择请根据电机噪声与驱动效果调试。
4.5 自动控制策略代码
下面是最关键的“智能控制逻辑”。自动模式下可以设定一个简单的温度区间:
- 温度低于 20℃:风扇停转。
- 温度在 20℃~25℃:风扇低速(30% PWM)。
- 温度在 25℃~30℃:风扇中速(60% PWM)。
- 温度高于 30℃:风扇高速(100% PWM)。
- 如果无人检测信号超过设定时间,则不管温度多高都逐渐停止,体现节能。
具体代码如下:
static void Task_MotorControl(void) { uint8_t targetDuty = 0; if (sysMode == SYS_POWER_OFF) { PWM_SetDuty(0); return; } // 检测人体传感器电平 personDetect = Infrared_ReadLevel(); if (sysMode == SYS_AUTO_MODE) { if (personDetect == 0) { // 无人状态,为节电可降低风速;如果使用延时关机逻辑,需要在这里维护时长变量 PWM_SetDuty(0); currentLevel = 0; } else { if (temperature < 20.0f) targetDuty = 0; else if (temperature < 25.0f) targetDuty = 30; else if (temperature < 30.0f) targetDuty = 60; else targetDuty = 100; if (targetDuty > 0) { currentLevel = (uint8_t)(targetDuty / 25); // 1~4挡备查 } PWM_SetDuty(targetDuty); } } else if (sysMode == SYS_MANUAL_MODE) { switch (manualLevel) { case 0: PWM_SetDuty(0); break; case 1: PWM_SetDuty(25); break; case 2: PWM_SetDuty(50); break; case 3: PWM_SetDuty(75); break; case 4: PWM_SetDuty(100); break; default: PWM_SetDuty(0); break; } } }上面的 targetDuty 直接代表 PWM 占空比。实际项目还会增加“无人延时关闭计时”:检测到无人后并不立即停转,而是持续运行比如 30s 再停。要完成这个功能,需要在主循环中维护一个递减计数变量,代码结构如下:
若 personDetect 为 1: 清零 noPersonCounter; 正常运行自动模式逻辑; 若 personDetect 为 0: noPersonCounter++; 若 noPersonCounter > 300(也就是30秒): 停转;否则维持当前转速;4.6 OLED 显示逻辑
OLED 显示逻辑一般比较简单,调用字符显示接口即可。以 SSD1306 的 0.96 英寸 OLED 为例,常用 API 包括:
OLED_Clear(); OLED_ShowString(uint8_t x, uint8_t y, char *str); OLED_ShowNum(uint8_t x, uint8_t y, uint32_t num, uint8_t len);显示主界面时,第一行显示当前模式,第二行显示温度,第三行显示档位/转速百分比,第四行显示人体检测状态。
static void Task_Display(void) { char buf[32]; OLED_Clear(); OLED_ShowString(0, 0, "Mode:"); if (sysMode == SYS_AUTO_MODE) { OLED_ShowString(40, 0, "Auto "); } else if (sysMode == SYS_MANUAL_MODE) { OLED_ShowString(40, 0, "Manual"); } else { OLED_ShowString(40, 0, "Off "); } // 温度显示,单位 ℃ OLED_ShowString(0, 2, "Temp:"); OLED_ShowNum(36, 2, (uint32_t)temperature, 2); OLED_ShowString(56, 2, "."); uint16_t tempDec = (uint16_t)((temperature - (int)temperature) * 10); OLED_ShowNum(64, 2, tempDec, 1); OLED_ShowString(76, 2, "C "); OLED_ShowString(0, 4, "Level:"); if (currentLevel == 0) { OLED_ShowString(48, 4, "Stop "); } else { OLED_ShowNum(48, 4, currentLevel, 1); OLED_ShowString(56, 4, "/4 "); } OLED_ShowString(0, 6, "Human:"); if (personDetect) { OLED_ShowString(48, 6, "YES "); } else { OLED_ShowString(48, 6, "NO "); } OLED_Refresh(); }OLED 刷新频率不能太高,否则会造成闪烁和占用 CPU。建议 1~5Hz 刷新即可,例如每 200ms 刷新一次。
4.7 按键处理
按键处理要处理机械抖动。一个简单的扫描函数如下:
// 返回按键值:1 模式切换,2 档位加,3 档位减,4 开关机 uint8_t Key_Scan(void) { static uint8_t keyState = 0; uint8_t keyValue = 0; if (KEY_MODE_PIN == 0 || KEY_PLUS_PIN == 0 || KEY_MINUS_PIN == 0 || KEY_POWER_PIN == 0) { delay_ms(10); if (KEY_MODE_PIN == 0) { keyValue = 1; } else if (KEY_PLUS_PIN == 0) { keyValue = 2; } else if (KEY_MINUS_PIN == 0) { keyValue = 3; } else if (KEY_POWER_PIN == 0) { keyValue = 4; } } // 等待按键释放,避免一次长按多次触发的副作用 while (KEY_MODE_PIN == 0 || KEY_PLUS_PIN == 0 || KEY_MINUS_PIN == 0 || KEY_POWER_PIN == 0) { delay_ms(10); } return keyValue; }注意:如果在主循环中直接写死等待按键释放的 while,那么长按会阻塞整个主循环,有可能会卡住传感器读取或 PWM 控制。对于简单演示项目影响不大,但如果是工程开发,建议使用“检测下降沿 + 防抖标志再触发一次”的方式,或把按键扫描放入定时器中断。
在主函数中按键动作对应的处理:
static void Task_KeyScan(void) { uint8_t key = Key_Scan(); if (key == 1) // 模式切换:自动/手动 { if (sysMode == SYS_AUTO_MODE) { sysMode = SYS_MANUAL_MODE; } else if (sysMode == SYS_MANUAL_MODE) { sysMode = SYS_AUTO_MODE; } } else if (key == 2) // 档位加 { if (manualLevel < 4) { manualLevel++; } } else if (key == 3) // 档位减 { if (manualLevel > 0) { manualLevel--; } } else if (key == 4) // 开关机,进入待机 { sysMode = SYS_POWER_OFF; PWM_SetDuty(0); } }5. 系统联调与演示验证
5.1 基本功能验证
完成代码后,建议按以下顺序进行硬件联调。
第一步:单独验证最小系统
只连接 STM32 最小系统板和下载器,确认程序能下载并且 LED 闪烁程序运行正常。
第二步:验证 OLED 显示
连接 OLED,下载 OLED 测试程序,观察能否正常显示字符。常见问题是 I2C 地址不对、接线反了、模块供电不足。
第三步:验证 DS18B20 温度读取
单独调试 DS18B20 驱动,用串口或 OLED 输出当前温度。可以用手捏住传感器看温度是否上升,或者用冰水测试降温。
第四步:验证 PWM 输出与电机转动
将 PWM 代码占空比固定为 50%,用示波器或万用表测量输出波形,再连接电机驱动电路,观察风扇转速是否正常。
第五步:验证人体感应模块
在代码中临时写一个读 GPIO 的逻辑,把人体感应输出接上,人走过时观察电平变化。如果一直有输出或一直无输出,要调节 HC-SR501 上的两个电位器。
全部模块单独正常工作后,再整合完整程序。
5.2 演示建议
毕业设计现场演示时,建议提前准备几个有说服力的场景:
- 用手握 DS18B20,风扇转速会提高。这个过程只需要几秒钟,体感明显。
- 让手掌在 HC-SR501 前晃动,风扇会立刻启动,然后人离开后几十秒自动停止。
- 按键切换到手动模式,手动加减档位时 OLED 上档位和风速同步变化。
- 将 OLED 放到显著位置,让评委能直观看到温度、模式等数据。
如果答辩现场环境温度已经高于 30℃,可以让温度一直处于高速状态,演示“降温到阈值后自动恢复低速”可能比较困难。这时可以在代码临时把阈值改高,例如 35℃,方便现场演示。
6. 常见问题与排查思路
在 STM32 开发过程中,最容易出问题的地方通常是硬件接线和底层延时,下面列举高频问题。
6.1 程序可以下载,但 OLED 不显示
可能原因:
- OLED 的 SCL/SDA 引脚接错。
- I2C 地址不对。
- 核心板供电不足,OLED 屏幕异常。
- 焊接虚焊或杜邦线接触不良。
解决措施:
- 使用 I2C 扫描程序扫描地址。
- 检查核心板原理图确认复用引脚。
- 替换一根杜邦线或直接焊接。
6.2 DS18B20 读取温度固定为 85℃ 或 0℃
DS18B20 上电后的默认温度为 85℃,如果读到 85℃ 且不变化,大概率数据转换没成功或程序读到了“上电默认值”。
如果温度持续显示 85℃,重点检查:
- GPIO 引脚模式是否配置为开漏/上拉输入。
- 单总线是否需要外部 4.7kΩ 上拉电阻。
- 复位脉冲和时序的延时是否准确。
DS18B20 数据线通常需要接一个 4.7kΩ 上拉电阻到 VCC,虽然部分开发板已经内置,但使用杜邦线连接时建议自行加上。
6.3 风扇转速无法调节,只高速或只停
可能原因:
- PWM 频率太低或太高,导致驱动异常。
- 电机驱动部分没有真正接 PWM 信号,而是接了普通高电平。
- Timer 通道复用冲突,GPIO 没有配置为复用推挽输出。
解决办法:
- 用示波器或简单用 LED 亮度观察 PWM 输出是否变化。
- 在代码中先写死不同的占空比,验证是否逐级变化。
- 检查是否用了正确的定时器通道函数,例如 TIM2_CH2 要调用 TIM_SetCompare2。
6.4 HC-SR501 误触发或一直触发
HC-SR501 模块上有两个电位器,分别调节灵敏度和延时。上电前需要先等待 30~60 秒预热。如果输出一直为高,可以逆时针减小灵敏度。如果输出常低,可以顺时针调高灵敏度。
6.5 系统频繁复位或 OLED 花屏
常见原因是电源纹波过大或者电机启动瞬间电流拉低电压。解决方式:
- 电机电源与控制逻辑电源分开,电机使用单独电源。
- 在电机电源两端并联 100μF 电解电容和 104 陶瓷电容。
- 电机是感性负载时,续流二极管必须接好,否则驱动管容易被击穿。
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| OLED 无显示 | 接线错误/地址不对 | 检查 I2C 地址与接线 |
| 温度不变 85℃ | DS18B20 上拉电阻或时序错误 | 检查上拉电阻与延时函数 |
| 风扇转速不均 | PWM 频率不匹配 | 调整定时器 PSC/ARR |
| 电机启动导致复位 | 供电不足或被拉低 | 分离电源并加去耦电容 |
| 人体传感器乱触发 | 模块调试参数不合适 | 调节灵敏度和延时旋钮 |
7. 最佳实践与工程化建议
7.1 代码规范与可维护性
毕业设计如果是多人配合或需要后续扩展,建议代码上注意分层:
- 驱动层:只负责硬件寄存器操作和底层协议,例如 DS18B20 驱动只提供 GetTemperature。
- 逻辑层:只处理模式切换、档位计算、温度阈值判断。
- 应用层:负责把结果显示到 OLED 或响应按键。
这样做的最大好处是,以后把 DS18B20 换成 DHT11,或者把 OLED 换成 LCD,只需要重写驱动层,核心控制逻辑不用大改。
7.2 异常处理与边界条件
不要假设传感器永远正常。例如 DS18B20 拔掉时,读取温度可能返回极值,容易导致风扇误动作。建议在代码里增加温度合法范围判断:
if ((temperature < -20.0f) || (temperature > 80.0f)) { // 传感器异常,进入安全模式:停止风扇并提示报警 PWM_SetDuty(0); return; }如果人体传感器故障,一直输出低电平,建议设计“故障超时”或“手动模式兜底”,避免整个系统无法使用。
7.3 低功耗与节能策略
绿色智能风扇的“绿色”体现在:
- 使用 PWM,而不是电阻降压调速,能显著降低损耗。
- 无人自动延时停机。
- 温度不高的场景,尽量让风扇低速运转而不是开关频繁切换。
- 睡眠模式下,OLED 可以关闭背光或降低刷新率。
如果项目要求更严格的功耗指标,可以让 STM32 进入 STOP 或 STANDBY 模式,通过 RTC 定时唤醒或人体传感器外部中断唤醒。当然这会增加代码复杂度,答辩时可以作为“改进方向”来写。
7.4 硬件安全提醒
如果最终方案使用 220V 交流风扇,务必注意以下几点:
- 强电与弱电必须隔离。
- 控制板不要裸露接触高压。
- 继电器或可控硅需要满足额定电流。
- 实际操作和演示时必须有指导教师在场,注意人身安全。
如果只是毕设展示,通常建议以 5V 直流风扇作为执行机构,安全、方便、演示效果一样良好。
7.5 扩展方向
如果一个基础自动控制风扇不足以体现工作量,可以从以下方向扩展:
- 增加手机 App 控制,通过 ESP8266/蓝牙模块与 STM32 通信,实现远程开关和调档。
- 增加 WIFI 温湿度云上报,通过 ESP8266 + MQTT 上传数据到云平台。
- 增加语音控制,用 LD3320 或离线语音模块识别“打开风扇”“提高风速”等指令。
- 增加太阳能充电供电模块,进一步体现“绿色”主题。
- 增加学习记忆功能,记录用户的使用习惯,自动在常用时段提前调速。
扩展时需要注意 STM32F103C8T6 的引脚资源,如果外设过多,可以把显示部分换成串口屏,或者升级到 STM32F407 等更多引脚/更高性能的型号。
8. 总结与后续学习建议
本文以智能绿色风扇为例,完整梳理了一个基于 STM32 的自动化控制系统从需求分析到硬件设计、代码实现、联调排错的闭环流程。核心知识点包括:
- 温度传感器 DS18B20 的单总线读取方法。
- STM32 定时器输出 PWM 的原理与配置。
- 人体红外传感器在节能控制中的使用方式。
- 自动/手动模式切换的状态机编程思路。
- OLED 信息显示与按键交互设计。
如果你正在准备单片机毕业设计,下一步建议先不要急着写大量代码,而是把上面第 2 节中的硬件连接图画清楚,然后按照 5.1 的顺序逐模块验证。每完成一个模块,就拍照留档,这些都是论文和答辩 PPT 中可以使用的素材。
在此基础上,如果想让方案更有竞争力,可以继续研究 FreeRTOS 在 STM32 上的移植,把温度采集、按键扫描、屏幕刷新放到不同任务里,这会让代码结构上一个台阶。也可以结合 WIFI 模块做 App 联动,增加项目的现代感。学习时建议多阅读官方参考手册和数据手册,尤其是定时器与 GPIO 章节,这是理解所有外设的基础。对代码中的每一处延时、每一个寄存器配置,都尝试回答“如果删除这行会怎样”,能够帮助你快速建立系统级认识。实物调试时不要怕出问题,把每个现象记录下来,多试几组参数,你会逐渐积累出一套属于自己的排查方法。