STM32 PWM实战:从呼吸灯到电机控制的完整驱动指南
1. PWM基础与呼吸灯实战
第一次接触STM32的PWM功能时,我被它强大的灵活性惊艳到了。PWM(脉冲宽度调制)就像个智能开关,通过快速通断来控制平均功率输出。想象一下水龙头,完全打开时水流最大,完全关闭时没有水流,而PWM就是让你以极快的速度开关水龙头,通过调整打开和关闭的时间比例来控制平均流量。
在STM32上配置PWM需要五个关键步骤,我把它总结为"时钟树-时基-输出-GPIO-启动"流程。首先通过RCC开启TIM定时器和GPIO时钟,这就像给整个系统通电。然后配置时基单元,这里ARR寄存器决定PWM周期,PSC预分频器调整计时频率。比如我的呼吸灯项目中使用72MHz主频,设置PSC=720-1和ARR=100-1,得到的就是100Hz的PWM频率(72MHz/720/100)。
输出比较单元配置才是PWM的精髓所在。在TIM_OCInitTypeDef结构体中,我选择PWM1模式、高电平有效,并启用输出。关键点在于CCR寄存器的值决定了占空比 - 当计数器值小于CCR时输出高电平,大于时输出低电平。通过动态修改CCR值,就能实现呼吸灯效果:
for(int i=0; i<=100; i++){ TIM_SetCompare1(TIM2, i); // 渐亮 Delay_ms(10); } for(int i=0; i<=100; i++){ TIM_SetCompare1(TIM2, 100-i); // 渐暗 Delay_ms(10); }实测中发现三个易错点:一是忘记GPIO要配置为复用推挽输出模式,二是TIM_Cmd忘记启用定时器,三是占空比计算要注意ARR值。比如ARR设为100-1时,CCR值50对应的就是50%占空比。建议调试时先用示波器观察波形,确保基础PWM信号正常再连接LED。
2. 深入PWM库函数解析
STM32标准库提供了丰富的PWM相关函数,我刚开始用时经常搞混。经过几个项目的磨练,现在把这些函数分成四大类来记忆:
第一类是初始化函数,TIM_OCxInit系列用于配置各通道参数。这里有个技巧:先用TIM_OCStructInit给结构体赋默认值,再修改需要的字段,能避免遗漏参数。比如PWM模式要设置为TIM_OCMode_PWM1,输出状态必须Enable。
第二类是动态调节函数,最常用的就是TIM_SetComparex系列。在电机控制项目中,我通过按键中断实时修改CCR值:
void EXTI0_IRQHandler(){ if(EXTI_GetITStatus(EXTI_Line0) != RESET){ static uint16_t duty = 0; duty = (duty + 10) % 100; TIM_SetCompare2(TIM3, duty); EXTI_ClearITPendingBit(EXTI_Line0); } }第三类是高级控制函数,包括强制输出(TIM_ForcedOCxConfig)、快速使能(TIM_OCxFastConfig)等。在舵机突然卡顿时,强制输出高电平能起到保护作用。
第四类是极性配置函数,TIM_OCxPolarityConfig可以动态改变输出极性。这个在驱动某些需要反向PWM的电机时特别有用。
特别提醒:高级定时器(TIM1/TIM8)还有互补输出和刹车功能,适合电机控制。当初我傻傻地用通用定时器驱动大功率电机,结果MOS管发热严重,后来改用TIM1的互补输出才解决问题。
3. 舵机角度精确控制
玩过机器人项目的朋友肯定对舵机不陌生。标准舵机需要50Hz的PWM信号(周期20ms),其中脉冲宽度在0.5ms-2.5ms对应0-180度角度。根据这个特性,我重新配置时基单元:
TIM_TimeBaseInitStruct.TIM_Period = 20000-1; // 20ms周期 TIM_TimeBaseInitStruct.TIM_Prescaler = 72-1; // 72MHz/72=1MHz角度转换公式为:CCR = (Angle/180 * 2000) + 500。比如90度对应CCR值1500(1.5ms脉冲)。我封装了角度设置函数:
void Servo_SetAngle(uint16_t angle){ angle = angle > 180 ? 180 : angle; uint16_t ccr = 500 + angle * 2000 / 180; TIM_SetCompare3(TIM4, ccr); }实际调试中发现两个坑:一是舵机供电不足会导致抖动,建议单独5V电源;二是机械限位不要强行超过,我的第一个舵机就这样报废了。通过OLED显示当前角度,配合按键逐步调试是不错的方法:
while(1){ KeyNum = Key_GetNum(); if(KeyNum == 1) angle += 10; if(KeyNum == 2) angle -= 10; Servo_SetAngle(angle); OLED_ShowNum(1,1,angle,3); }4. 直流电机调速系统
直流电机控制比前两者复杂,需要同时考虑调速和转向。我的方案是用PWM控制速度,GPIO控制方向。L298N驱动模块的IN1/IN2接GPIO,ENA接PWM信号。核心代码如下:
// 电机初始化 void Motor_Init(){ GPIO_Init(IN1_PIN, GPIO_Mode_Out_PP); GPIO_Init(IN2_PIN, GPIO_Mode_Out_PP); PWM_Init(ENA_PIN); // 初始化PWM通道 } // 设置转速(-100~+100) void Motor_SetSpeed(int8_t speed){ if(speed >=0){ GPIO_SetBits(IN1_PIN); GPIO_ResetBits(IN2_PIN); }else{ GPIO_ResetBits(IN1_PIN); GPIO_SetBits(IN2_PIN); speed = -speed; } TIM_SetCompare1(TIM2, speed); }按键控制逻辑实现了速度阶梯调整和自动反向:
KeyNum = Key_GetNum(); if(KeyNum == 1){ speed += 20; if(speed >100){ speed = -100; // 到达最大值后反向 } } Motor_SetSpeed(speed);在电机项目中,PWM频率选择很重要。太低会导致电机啸叫,太高可能使驱动芯片过热。经过测试,10-20kHz是比较理想的区间。另外一定要加续流二极管,否则关断时的反向电动势会损坏电路。有一次我忘记加二极管,烧掉了整个驱动板,这个教训记忆犹新。
5. 高级技巧与故障排查
当多个外设需要PWM时,定时器通道复用就派上用场了。STM32的定时器通常有4个通道,可以独立配置。我的四足机器人项目就用了TIM3的四个通道控制四个舵机。关键是要正确配置GPIO的复用功能,并开启AFIO时钟:
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); GPIO_PinRemapConfig(GPIO_FullRemap_TIM3, ENABLE);引脚重映射功能在PCB布线受限时特别有用。比如TIM2_CH1默认在PA0,可以重映射到PA15。但要注意重映射后的中断向量和GPIO配置也要相应调整。
常见问题排查经验:
- 无输出:先检查时钟使能、GPIO模式(必须AF_PP)、定时器使能
- 频率不对:重新计算PSC和ARR值,注意寄存器实际值要-1
- 占空比异常:确认CCR值范围不超过ARR,极性配置正确
- 抖动严重:检查电源稳定性,适当增加滤波电容
对于更复杂的应用,可以结合定时器中断实现PWM序列控制。比如我的RGB灯带项目就用TIM_IT_CCx中断实现WS2812协议。记住在中断服务函数中要清除标志位:
if(TIM_GetITStatus(TIM2, TIM_IT_CC1)){ // 处理逻辑 TIM_ClearITPendingBit(TIM2, TIM_IT_CC1); }