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 知识体系总览

这一章的内容,我用一张图帮你理清思路:

GPIO与PWM输出知识体系 定时器配置 时钟与GPIO配置 时基与PWM模式 通道与极性配置 PWM参数计算:频率 = f_clk / (PSC+1) / (ARR+1) 占空比 = CCR / (ARR+1) 电机控制逻辑:停止 / 正转 / 反转 / 刹车 停止 (0,0) 正转 (D,0) 反转 (0,D) 刹车 (100%,100%) ⚠ 正反转切换需先停止 → 防电流冲击;高级定时器需使能主输出

3.6 调试要点与常见问题

最后,我总结几个调试中一定会遇到的问题:

  • PWM没输出?先查GPIO复用功能配置,再用示波器看定时器时钟引脚有没有波形。我遇到过好几次,都是GPIO的AF配置错了。
  • 电机抖动?大概率是PWM频率太低,或者占空比变化太快。加个低通滤波或者软件平滑处理。
  • 电机只能一个方向转?检查H桥使能引脚,或者看另一路PWM的CCR是不是被意外改写了。
  • 发热严重?PWM频率太高,或者死区时间没设置。高级定时器可以配置死区插入,这个后面讲。

安全提醒:调试电机时,手不要碰电机轴和驱动板散热片。我有个同事调试时没注意,手指被电机轴打了一下,肿了一个礼拜。还有,电源线上串个保险丝,短路时能救命。