3. GPIO与PWM输出:定时器配置、PWM频率与占空比计算、电机正反转控制逻辑
这一章,我们开始真正让电机动起来。很多初学者一上来就调PID,结果电机根本不转,或者转得乱七八糟——其实问题往往出在最基础的PWM配置上。我个人习惯先把底层驱动调稳了,再谈控制算法。你想想看,连占空比都输出不准,PID算得再好也没用。
3.1 定时器与PWM的基本原理
STM32F407的定时器资源非常丰富,有高级定时器(TIM1/TIM8)、通用定时器(TIM2~TIM5、TIM9~TIM14)和基本定时器(TIM6/TIM7)。做电机控制,我们最常用的是高级定时器和通用定时器,因为它们支持PWM输出。
PWM的本质,说白了就是定时器在计数过程中,比较输出引脚的电平翻转。定时器从0计数到ARR(自动重装载值),中间有个比较值CCR。当计数值小于CCR时,输出高电平;大于CCR时,输出低电平——这就是PWM的生成原理。
核心公式:
- PWM频率 = 定时器时钟 / (PSC + 1) / (ARR + 1)
- 占空比 = CCR / (ARR + 1) × 100%
- PWM分辨率 = 1 / (ARR + 1) × 100%
举个例子,我用TIM2做电机PWM输出。TIM2挂载在APB1总线上,时钟频率84MHz。我想要20kHz的PWM频率,分辨率达到0.1%左右,怎么算?
先定ARR。分辨率0.1%意味着ARR+1至少1000,我取ARR=999。那么PSC = 84MHz / (20kHz × 1000) - 1 = 3.2,取整PSC=3。实际频率 = 84MHz / 4 / 1000 = 21kHz,误差只有5%,完全够用。
我的经验:电机PWM频率一般选10kHz~20kHz。低于10kHz人耳能听到啸叫,高于20kHz开关损耗增大。我做过一个项目,客户非要50kHz,结果MOS管发热严重,最后老老实实改回16kHz。
3.2 定时器配置实战
下面我给出一个完整的TIM2 PWM初始化代码。这段代码我用了很多年,基本改改引脚和参数就能复用。
void Motor_PWM_Init(uint16_t arr, uint16_t psc)
{
GPIO_InitTypeDef GPIO_InitStructure;
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_OCInitTypeDef TIM_OCInitStructure;
// 1. 使能时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);
// 2. 配置GPIO为复用功能
GPIO_PinAFConfig(GPIOA, GPIO_PinSource0, GPIO_AF_TIM2); // PA0 -> TIM2_CH1
GPIO_PinAFConfig(GPIOA, GPIO_PinSource1, GPIO_AF_TIM2); // PA1 -> TIM2_CH2
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 3. 配置定时器时基
TIM_TimeBaseStructure.TIM_Period = arr;
TIM_TimeBaseStructure.TIM_Prescaler = psc;
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);
// 4. 配置PWM模式
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
TIM_OCInitStructure.TIM_Pulse = 0; // 初始占空比0
TIM_OC1Init(TIM2, &TIM_OCInitStructure); // CH1
TIM_OC2Init(TIM2, &TIM_OCInitStructure); // CH2
// 5. 使能定时器
TIM_Cmd(TIM2, ENABLE);
}
这里有几个坑,我当年都踩过:
- GPIO复用功能要配置对——每个定时器通道对应的引脚是固定的,查数据手册别想当然。
- 高级定时器需要额外使能主输出——TIM1和TIM8要调用TIM_CtrlPWMOutputs(TIM1, ENABLE),否则没输出。
- 初始占空比设为0——防止上电瞬间电机猛转,安全第一。
3.3 占空比与频率的实时调整
实际调试中,我们经常需要动态改变占空比和频率。改变占空比很简单,直接写CCR寄存器:
// 设置TIM2_CH1占空比,范围0~arr
void Motor_SetDuty(uint16_t duty)
{
TIM_SetCompare1(TIM2, duty);
}
// 设置TIM2_CH2占空比
void Motor_SetDuty2(uint16_t duty)
{
TIM_SetCompare2(TIM2, duty);
}
改变频率稍微麻烦一点,需要重新配置ARR和PSC。注意:改变ARR会影响占空比,因为占空比 = CCR / (ARR+1)。所以改变频率后,要重新计算CCR值。
我曾经犯过的错:在电机高速运转时直接修改ARR,结果电机突然抖动了一下。后来查手册才知道,修改ARR最好在定时器溢出中断里进行,或者先停止定时器再配置。对于电机控制,我建议频率固定,只调占空比。
3.4 电机正反转控制逻辑
直流有刷电机的正反转,靠的是H桥电路。我常用的方案是两路PWM控制:一路控制正转,一路控制反转,但同一时刻只能有一路有效。
控制逻辑其实很简单:
| 控制状态 | PWM1 (正转) | PWM2 (反转) | 电机行为 |
|---|---|---|---|
| 停止 | 0% | 0% | 电机不转 |
| 正转 | 占空比D | 0% | 正向旋转,速度与D成正比 |
| 反转 | 0% | 占空比D | 反向旋转,速度与D成正比 |
| 刹车 | 100% | 100% | 电机急停(短接制动) |
代码实现:
typedef enum {
MOTOR_STOP = 0,
MOTOR_FORWARD,
MOTOR_BACKWARD,
MOTOR_BRAKE
} MotorState_t;
void Motor_Control(MotorState_t state, uint16_t speed)
{
// speed范围0~arr,arr=999
switch(state) {
case MOTOR_STOP:
Motor_SetDuty(0);
Motor_SetDuty2(0);
break;
case MOTOR_FORWARD:
Motor_SetDuty(speed);
Motor_SetDuty2(0);
break;
case MOTOR_BACKWARD:
Motor_SetDuty(0);
Motor_SetDuty2(speed);
break;
case MOTOR_BRAKE:
Motor_SetDuty(999); // 100%占空比
Motor_SetDuty2(999);
break;
}
}
避坑指南:正反转切换时,一定要先停止,再换向。我曾经直接正转切反转,结果电流瞬间飙升,差点烧了驱动板。加个50ms的延时或者软切换,能有效保护MOS管。
3.5 知识体系总览
这一章的内容,我用一张图帮你理清思路:
3.6 调试要点与常见问题
最后,我总结几个调试中一定会遇到的问题:
- PWM没输出?先查GPIO复用功能配置,再用示波器看定时器时钟引脚有没有波形。我遇到过好几次,都是GPIO的AF配置错了。
- 电机抖动?大概率是PWM频率太低,或者占空比变化太快。加个低通滤波或者软件平滑处理。
- 电机只能一个方向转?检查H桥使能引脚,或者看另一路PWM的CCR是不是被意外改写了。
- 发热严重?PWM频率太高,或者死区时间没设置。高级定时器可以配置死区插入,这个后面讲。
安全提醒:调试电机时,手不要碰电机轴和驱动板散热片。我有个同事调试时没注意,手指被电机轴打了一下,肿了一个礼拜。还有,电源线上串个保险丝,短路时能救命。