1. 项目背景与核心功能
最近在工作室捣鼓STM32的时候,突然想到做个能实时显示状态的智能风扇应该挺有意思。这个项目最吸引我的地方在于,它把嵌入式开发中最常用的几种外设控制都串起来了——GPIO中断处理按键、定时器PWM控制电机、I2C驱动OLED屏,还有LED状态指示。实际做下来发现,这种多外设联动的项目特别适合用来练手。
这个智能风扇有三个核心功能:一是通过物理按键切换三档风速(低速/中速/高速),二是电机转速会随档位实时变化,三是OLED屏会同步显示当前风速和PWM占空比。我特意加了LED流水灯效果作为视觉反馈,不同档位对应不同的灯光模式。比如低速时只有两个LED交替闪烁,高速时四个LED会跑马灯式循环,操作起来特别有仪式感。
2. 硬件选型与电路设计
2.1 主控与外围器件选择
主控用的STM32F103ZET6,这款Cortex-M3内核的芯片性价比超高,72MHz主频完全够用。电机驱动选的L9110S,这种双H桥芯片特别适合驱动小功率直流电机,最大输出电流能达到800mA。OLED屏是0.96寸的SSD1306,I2C接口省引脚,显示效果也够清晰。
有个细节要注意:L9110S的工作电压是2.5-12V,而电机额定电压是5V。我在PCB上单独放了两个电源接口,电机用5V/2A的电源适配器供电,STM32和OLED屏用3.3V供电。这样既避免电机启动电流干扰MCU,又能保证显示稳定。
2.2 关键电路连接
电机驱动部分的接线要特别注意:L9110S的OA接PA15(PWM输出),OB接PC13(固定低电平),这样通过单路PWM就能实现调速。四个按键接PE2-PE4和PA0,都配置成上升沿触发的外部中断。LED灯接PC0-PC3,推挽输出模式。OLED的SCL/SDA分别接PB10/PB11,记得要加上拉电阻。
实测中发现个坑:如果直接用杜邦线连接电机,运行时可能会因为接触不良导致MCU复位。后来改用了带锁紧功能的接线端子,问题就解决了。建议大家在PCB设计时,大电流线路最好预留焊盘,方便后期飞线加固。
3. 软件开发环境搭建
3.1 工具链配置
我用的是STM32CubeMX+Keil MDK的组合。CubeMX版本是6.5.0,配置外设特别方便。安装时记得勾选STM32F1系列的HAL库支持。Keil要安装Device Family Pack for STM32F1xx,建议用最新版的ARM Compiler 6。
有个小技巧:在CubeMX里配置工程时,把Toolchain/IDE选为"Makefile",这样生成的代码可以直接用VSCode+PlatformIO开发。我后来调试OLED显示时就切到VSCode了,因为它的串口绘图功能超好用。
3.2 关键外设初始化
定时器2的PWM配置是核心:时钟源选内部时钟,Channel1设成PWM Generation1。预分频系数PSC设为7199,自动重载值ARR=99,这样得到的PWM频率就是72MHz/(7199+1)/(99+1)=100Hz。这个频率对电机调速来说刚刚好,既不会听到明显的啸叫声,控制精度也足够。
I2C配置为标准模式(100kHz),要开启I2C中断。OLED的初始化代码要放在主循环前,记得先执行清屏操作。按键中断优先级保持默认就行,但要在NVIC里把所有EXTI线都使能。
4. 核心功能实现细节
4.1 多档位PWM调速
在HAL_TIM_PWM_Start()之后,电机并不会立即转动,因为CCR寄存器初始值为0。我在中断回调函数里用__HAL_TIM_SET_COMPARE()动态修改占空比:30%对应低速档(CCR=30),60%中速档,90%高速档。实测发现占空比低于20%时电机可能无法启动,所以最低档设在了30%。
有个优化点:直接切换占空比会导致转速突变,电机可能打齿。后来我改成每次增减10%的渐变方式,用for循环实现软启动。代码里加了限制条件,防止占空比超过100%:
for(int i=current_duty; i!=target_duty; i+=step){ __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, i); HAL_Delay(50); }4.2 状态同步机制
定义了一个全局变量sys_state来同步所有外设状态:
typedef enum { OFF_MODE, LOW_SPEED, MEDIUM_SPEED, HIGH_SPEED } FanState;按键中断里修改这个状态量,主循环根据当前状态控制LED和OLED。比如中速档时,OLED显示"风速:中速(60%)",同时LD1-LD3三个灯开始流水效果。这种状态机设计让代码逻辑特别清晰,后期加新功能也方便。
5. 显示界面优化技巧
5.1 汉字显示实现
用Pctolcd2002生成字模时,建议选"阴码+逐列式+顺向"模式,字体大小16x16。一个实用技巧:把常用汉字(如"风速"、"低速"等)做成数组,再用指针数组索引:
const uint8_t CHS_风速[] = {...}; const uint8_t *chs_table[] = {CHS_风速,...}; void ShowChinese(uint8_t x, uint8_t y, uint8_t idx){ OLED_ShowCHinese(x, y, chs_table[idx]); }5.2 动态效果设计
除了静态文字,我还加了动态进度条显示占空比。在OLED_DrawBMP()基础上封装了个函数:
void ShowProgressBar(uint8_t x, uint8_t y, uint8_t width, uint8_t height, uint8_t percent){ OLED_DrawRectangle(x, y, x+width, y+height); uint16_t fill_width = (width-2)*percent/100; OLED_Fill(x+1, y+1, x+1+fill_width, y+height-1); }每次按键操作后,屏幕会先全白闪烁三次再刷新内容。这个视觉反馈特别重要,能明确告知用户操作已生效。实现起来就几行代码:
for(int i=0; i<3; i++){ OLED_Fill(0,0,128,64,1); HAL_Delay(100); OLED_Fill(0,0,128,64,0); HAL_Delay(100); }6. 常见问题排查
6.1 电机异常抖动
调试时遇到过电机间歇性抖动的问题,后来发现是电源功率不足。用万用表测量发现,电机启动瞬间电压会被拉到4V以下。解决方法有两个:一是给电机电源并联大电容(我加了2个1000μF的电解电容),二是采用分时上电策略,先让MCU和OLED启动完成再使能电机。
6.2 OLED显示花屏
I2C通信不稳定会导致花屏。首先要检查上拉电阻(我用的是4.7kΩ),其次可以降低I2C时钟速度。如果还不行,试试在每次传输前加I2C总线恢复序列:
void I2C_Recovery(){ GPIO_InitTypeDef GPIO_InitStruct = {0}; GPIO_InitStruct.Pin = GPIO_PIN_10|GPIO_PIN_11; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD; HAL_GPIO_Init(GPIOB, &GPIO_InitStruct); for(int i=0; i<9; i++){ HAL_GPIO_WritePin(GPIOB, GPIO_PIN_10, GPIO_PIN_RESET); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_11, GPIO_PIN_RESET); HAL_Delay(1); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_10, GPIO_PIN_SET); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_11, GPIO_PIN_SET); HAL_Delay(1); } }7. 功能扩展方向
现在已经实现了基础功能,但还有不少升级空间。比如可以加个温湿度传感器,实现自动调速。我用DHT11试过,读取到温度超过30度就自动切到高速档。代码里加个条件判断就行:
if(temp >= 30 && sys_state != HIGH_SPEED){ SetFanSpeed(HIGH_SPEED); OLED_ShowString(0,5,"Auto Mode"); }另一个有意思的改进是加入语音控制。我用LD3320模块实现了简单的"开机"、"加速"等指令识别。需要额外开个定时器做超时检测,防止误触发。还可以通过串口连接上位机,用Python写个控制界面,实时显示转速曲线。