4. 定时器与PWM:基本定时器配置、高级定时器特性、PWM输出驱动舵机/电机

定时器,说白了就是STM32的“心脏节拍器”。

我刚开始做嵌入式那会儿,总觉得定时器不就是个计数器嘛,有啥好研究的。直到有一次做四轴飞行器,电机死活抖得跟筛糠似的……嗯,从那以后我才真正明白,定时器和PWM玩不转,电机控制就是纸上谈兵。

4.1 基本定时器:最简单的节拍

基本定时器(TIM6、TIM7)功能最单一,就俩活儿:计时和触发DAC。没有外部引脚,没有捕获/比较通道。

它的核心逻辑就一个公式:

溢出频率 = 定时器时钟 / (预分频器值 + 1) / (自动重装载值 + 1)

举个例子,STM32F4的APB1定时器时钟通常是84MHz。我想要一个1ms的定时中断:

// 1ms = 1/1000 秒
// 84MHz / (83+1) / (999+1) = 84,000,000 / 84 / 1000 = 1000 Hz = 1ms

TIM_HandleTypeDef htim6;
htim6.Instance = TIM6;
htim6.Init.Prescaler = 83;       // 预分频,84MHz / 84 = 1MHz
htim6.Init.Period = 999;         // 自动重装载,1MHz / 1000 = 1kHz
HAL_TIM_Base_Init(&htim6);
HAL_TIM_Base_Start_IT(&htim6);   // 开启中断
我的习惯:预分频器值我一般先算到让计数器频率为1MHz(1us一格),这样Period值直接写想要的微秒数,调试时脑子不用转。

4.2 通用定时器:功能大爆发

通用定时器(TIM2~TIM5,TIM9~TIM14)在基本定时器基础上,加了:

  • 4个独立通道:可配置为输入捕获、输出比较、PWM生成
  • 编码器接口:直接接正交编码器测转速
  • 霍尔传感器接口:无刷电机换相专用

我做过一个项目,用TIM2的编码器模式读电机转速,同时用TIM3输出PWM控制电机。一个定时器读,一个定时器写,互不干扰。

4.3 高级定时器:电机控制的王牌

高级定时器(TIM1、TIM8)才是重头戏。它比通用定时器多了几个杀手级功能:

功能 说明 我遇到的场景
互补输出 一对通道输出互补PWM,带死区插入 H桥驱动,上下管不能同时导通
刹车输入 紧急停止信号,强制PWM输出为安全状态 电机堵转时,刹车脚拉低立刻停
重复计数器 N次溢出后才触发更新事件 精确控制PWM脉冲个数,步进电机专用
霍尔/编码器接口 硬件自动换相 无刷电机FOC控制
我曾经踩过的坑:高级定时器的死区时间设置不当,导致H桥上下管直通,MOS管直接冒烟。死区时间要根据MOS管的关断延迟来算,别偷懒抄网上的值。

4.4 PWM输出:从原理到实战

PWM(脉冲宽度调制),说白了就是“开关开关开关”,通过调节高电平占整个周期的比例(占空比)来模拟不同的电压。

占空比 = CCR / ARR,其中CCR是捕获/比较寄存器值,ARR是自动重装载值。

配置一个PWM通道的步骤:

  1. 初始化定时器:设置预分频和周期
  2. 配置通道:选择PWM模式1或2,设置极性
  3. 设置占空比:写CCR寄存器
  4. 启动PWM:使能通道和主计数器
// 以TIM3通道1(PA6)输出50Hz舵机PWM为例
TIM_OC_InitTypeDef sConfigOC = {0};

htim3.Instance = TIM3;
htim3.Init.Prescaler = 167;      // 168MHz / 168 = 1MHz
htim3.Init.Period = 19999;       // 1MHz / 20000 = 50Hz
HAL_TIM_PWM_Init(&htim3);

sConfigOC.OCMode = TIM_OCMODE_PWM1;
sConfigOC.Pulse = 1500;          // 1.5ms = 1500us,舵机中位
sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
HAL_TIM_PWM_ConfigChannel(&htim3, &sConfigOC, TIM_CHANNEL_1);

HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1);

关键点:舵机PWM频率固定为50Hz(周期20ms),脉宽0.5ms~2.5ms对应0°~180°。电机PWM频率通常高得多,几kHz到几十kHz,频率太低电机会啸叫。

4.5 驱动舵机:角度控制实战

舵机控制其实就一个映射关系:角度 → 脉宽。

我习惯写一个函数,把0~180度映射到500~2500微秒:

void Servo_SetAngle(TIM_HandleTypeDef *htim, uint32_t Channel, uint16_t angle)
{
    // 角度范围 0~180,脉宽范围 500~2500 us
    uint16_t pulse = 500 + (uint32_t)angle * 2000 / 180;
    __HAL_TIM_SET_COMPARE(htim, Channel, pulse);
}

// 使用示例:让舵机转到90度
Servo_SetAngle(&htim3, TIM_CHANNEL_1, 90);

你想想看,如果直接写CCR寄存器,代码移植性差。封装成函数,换个定时器只要改参数就行。

4.6 驱动直流电机:速度与方向

直流电机控制需要两样东西:PWM调速 + H桥换向。

典型的L298N或TB6612驱动模块,用两个GPIO控制方向,一个PWM控制速度:

IN1 IN2 PWM 电机状态
0 0 X 刹车
0 1 占空比 正转(速度可调)
1 0 占空比 反转(速度可调)
1 1 X 急停
void Motor_Control(uint8_t dir, uint16_t speed)
{
    // speed范围 0~1000
    switch(dir)
    {
        case MOTOR_FORWARD:
            HAL_GPIO_WritePin(IN1_GPIO_Port, IN1_Pin, GPIO_PIN_RESET);
            HAL_GPIO_WritePin(IN2_GPIO_Port, IN2_Pin, GPIO_PIN_SET);
            __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, speed);
            break;
        case MOTOR_BACKWARD:
            HAL_GPIO_WritePin(IN1_GPIO_Port, IN1_Pin, GPIO_PIN_SET);
            HAL_GPIO_WritePin(IN2_GPIO_Port, IN2_Pin, GPIO_PIN_RESET);
            __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, speed);
            break;
        case MOTOR_STOP:
            HAL_GPIO_WritePin(IN1_GPIO_Port, IN1_Pin, GPIO_PIN_RESET);
            HAL_GPIO_WritePin(IN2_GPIO_Port, IN2_Pin, GPIO_PIN_RESET);
            break;
    }
}
避坑指南:电机启动瞬间电流很大,我建议用软启动——占空比从0逐渐增加到目标值,每次增加50,间隔10ms。不然电源会被拉垮,单片机直接复位。

4.7 知识结构图:定时器与PWM体系

下面这张图把本章的知识脉络串起来了,从基本定时器到高级定时器,再到舵机和电机的实际应用:

定时器与PWM知识体系 定时器与PWM知识体系 基本定时器 通用定时器 高级定时器 计时 触发DAC 输入捕获 输出比较 / PWM 互补输出 + 死区 刹车 / 重复计数 PWM生成:频率 = 时钟 / (PSC+1) / (ARR+1),占空比 = CCR / ARR 舵机控制 50Hz / 0.5~2.5ms脉宽 直流电机控制 PWM调速 + H桥换向 核心:预分频器决定精度,自动重装载决定周期,CCR决定占空比 高级定时器是电机控制的王牌,死区和刹车功能必不可少

4.8 常见问题与调试技巧

  • PWM没输出?先检查GPIO复用功能配置,再查定时器时钟是否使能。我经常忘了开AFIO时钟。
  • 舵机抖动?大概率是PWM频率不对,或者电源功率不够。舵机启动电流可达2A,别用开发板USB供电。
  • 电机不转?先测PWM引脚波形,再用万用表测驱动板供电。很多时候是GND没共地。
  • 高级定时器死区无效?检查TIM_Break和死区配置寄存器,刹车极性别搞反了。

我的调试三板斧:

  1. 逻辑分析仪看PWM波形——频率对不对?占空比变没变?
  2. 示波器看电机驱动输出——死区够不够?有没有直通?
  3. 串口打印CCR和ARR值——寄存器写进去了没?

定时器和PWM这部分,说白了就是“配参数、看波形、调细节”。配多了自然就熟了。我当年为了调一个无刷电机的启动时序,连续熬了三个晚上,最后发现是死区时间少配了0.5微秒……嗯,从那以后我再也不敢轻视定时器的配置了。