4. 定时器基础与PWM输出:定时器工作原理、HAL库定时器配置、PWM输出驱动舵机/电机实战

定时器,说白了就是单片机的“心跳”。没有它,很多时序相关的操作都得靠CPU死等,效率低得可怜。我刚开始做嵌入式那会儿,总觉得定时器就是个计数的玩意儿,后来踩的坑多了才明白——用好定时器,你的系统就成功了一半。

4.1 STM32定时器家族:你手里到底有几把刷子?

STM32的定时器不是只有一种。我见过不少新手,上来就用高级定时器干基础活,结果资源浪费严重。咱们先理清楚这家族谱系:

定时器类型 位数 主要特性 典型应用场景
基本定时器(TIM6/TIM7) 16位 仅基础计数,无PWM/捕获 定时中断、DMA触发
通用定时器(TIM2~TIM5) 16/32位 PWM输出、输入捕获、编码器 舵机控制、电机调速、测频
高级定时器(TIM1/TIM8) 16位 互补PWM、刹车功能、死区插入 电机FOC、三相逆变、电源控制

我个人习惯:做舵机控制用通用定时器就够了,别拿高级定时器当普通用。高级定时器的刹车功能和死区插入,那是给电机驱动准备的硬货。

4.2 定时器核心工作原理:从计数器到PWM

定时器工作的本质,就是一个计数器在时钟驱动下不断累加。当计数值达到某个设定值时,产生事件或中断。PWM输出,就是在这个基础上加了个比较器。

核心公式:

  • PWM频率 = 定时器时钟 / (预分频器值 + 1) / (自动重装载值 + 1)
  • PWM占空比 = 比较值 / (自动重装载值 + 1) × 100%

举个例子:系统时钟72MHz,预分频器设为71(即72分频),自动重装载值设为999,那么PWM频率就是 72MHz / 72 / 1000 = 1kHz。占空比50%的话,比较值设为500就行。

我的经验:预分频器值和自动重装载值的选择,直接影响PWM的分辨率。想要精细调节占空比,就把自动重装载值设大一些。但注意,频率和分辨率是矛盾的——频率越高,分辨率越低。

4.3 HAL库定时器配置:三步走,不迷路

用HAL库配置定时器,其实就三步。我在项目里反复用这套流程,从来没出过问题。

  1. 初始化定时器句柄:设置预分频器、计数模式、自动重装载值
  2. 配置PWM通道:设置极性、比较值、使能输出
  3. 启动定时器:调用HAL_TIM_PWM_Start()

来看一段实际代码,这是我做舵机控制时用的配置:

// 定时器句柄定义
TIM_HandleTypeDef htim2;

// 定时器初始化函数
void MX_TIM2_Init(void)
{
  TIM_OC_InitTypeDef sConfigOC = {0};

  htim2.Instance = TIM2;
  htim2.Init.Prescaler = 71;          // 72MHz / 72 = 1MHz
  htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
  htim2.Init.Period = 19999;          // 1MHz / 20000 = 50Hz
  htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
  htim2.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
  HAL_TIM_PWM_Init(&htim2);

  // 配置PWM通道1
  sConfigOC.OCMode = TIM_OCMODE_PWM1;
  sConfigOC.Pulse = 1500;             // 初始占空比:1.5ms脉宽
  sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
  sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
  HAL_TIM_PWM_ConfigChannel(&htim2, &sConfigOC, TIM_CHANNEL_1);

  // 启动PWM输出
  HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1);
}

我曾经踩过的坑:启动PWM之前,一定要确保GPIO引脚已经配置为复用功能模式。否则,你看到的PWM波形就是一条直线。另外,别忘了使能定时器的时钟——HAL_TIM_PWM_Init()内部会做,但如果你手动改了时钟,记得重新初始化。

4.4 PWM输出驱动舵机:角度控制实战

舵机控制,说白了就是控制PWM的脉宽。标准舵机的工作频率是50Hz(周期20ms),脉宽范围0.5ms~2.5ms,对应0°~180°。

我常用的映射关系:

舵机角度 脉宽(ms) 比较值(50Hz, 1MHz时钟)
0° 0.5 500
45° 1.0 1000
90° 1.5 1500
135° 2.0 2000
180° 2.5 2500

写一个角度设置函数,其实就一行代码的事:

void Servo_SetAngle(uint16_t angle)
{
  // 角度范围0~180,映射到脉宽500~2500
  uint16_t pulse = 500 + (angle * 2000 / 180);
  __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, pulse);
}

注意:不同品牌的舵机,脉宽范围可能略有差异。我建议你在实际使用前,用示波器确认一下舵机的响应范围。有些廉价舵机,脉宽超过2.3ms就可能烧毁。

4.5 PWM输出驱动直流电机:速度与方向控制

直流电机控制比舵机复杂一些。你需要两个信号:一个控制方向(GPIO高低电平),一个控制速度(PWM占空比)。

我常用的电机驱动方案是L298N或TB6612,控制逻辑如下:

电机状态 IN1 IN2 PWM
正转 高 低 占空比可调
反转 低 高 占空比可调
刹车 高 高 任意
停止 低 低 任意

代码实现也很直接:

void Motor_Control(uint8_t direction, uint16_t speed)
{
  // speed范围0~1000,对应占空比0%~100%
  switch(direction)
  {
    case MOTOR_FORWARD:
      HAL_GPIO_WritePin(MOTOR_IN1_GPIO_Port, MOTOR_IN1_Pin, GPIO_PIN_SET);
      HAL_GPIO_WritePin(MOTOR_IN2_GPIO_Port, MOTOR_IN2_Pin, GPIO_PIN_RESET);
      __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_2, speed);
      break;
    case MOTOR_BACKWARD:
      HAL_GPIO_WritePin(MOTOR_IN1_GPIO_Port, MOTOR_IN1_Pin, GPIO_PIN_RESET);
      HAL_GPIO_WritePin(MOTOR_IN2_GPIO_Port, MOTOR_IN2_Pin, GPIO_PIN_SET);
      __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_2, speed);
      break;
    case MOTOR_STOP:
      HAL_GPIO_WritePin(MOTOR_IN1_GPIO_Port, MOTOR_IN1_Pin, GPIO_PIN_RESET);
      HAL_GPIO_WritePin(MOTOR_IN2_GPIO_Port, MOTOR_IN2_Pin, GPIO_PIN_RESET);
      break;
    case MOTOR_BRAKE:
      HAL_GPIO_WritePin(MOTOR_IN1_GPIO_Port, MOTOR_IN1_Pin, GPIO_PIN_SET);
      HAL_GPIO_WritePin(MOTOR_IN2_GPIO_Port, MOTOR_IN2_Pin, GPIO_PIN_SET);
      break;
  }
}

我曾经踩过的坑:电机启动瞬间电流很大,如果电源供电不足,会导致单片机复位。我的建议是:电机电源和单片机电源分开,共地即可。另外,PWM频率不要设得太低,否则电机会发出刺耳的啸叫声。1kHz~20kHz是比较合适的范围。

4.6 定时器PWM输出知识体系

下面这张图,是我自己总结的定时器PWM输出知识体系。你把它理清了,后面做电机控制、舵机控制、LED调光,基本就是套模板的事。

定时器PWM输出知识体系 定时器基础配置 预分频器设置 自动重装载值 计数模式选择 PWM通道配置:极性、比较值、快速模式 舵机控制 直流电机控制 LED调光/蜂鸣器 频率50Hz 脉宽0.5~2.5ms 频率1~20kHz 占空比0~100% 频率100Hz~1kHz 占空比调节亮度 核心公式:频率 = 时钟 / (PSC+1) / (ARR+1) 占空比 = CCR / (ARR+1) × 100%

4.7 实战中的避坑指南

做PWM驱动,有几个坑我反复踩过,今天一并告诉你:

  • GPIO复用功能没配置:PWM输出引脚必须配置为复用推挽输出,否则没波形。我见过有人查了半天,结果是GPIO模式设成了输出。
  • 定时器时钟没使能:虽然HAL_TIM_PWM_Init()内部会处理,但如果你在初始化之前手动操作了RCC,记得检查时钟是否正常。
  • 自动重装载预加载没使能:如果不使能,修改比较值时可能产生不完整的PWM周期,导致电机抖动。
  • 舵机供电不足:舵机启动瞬间电流可达1A以上,直接从单片机3.3V取电必死。用外部5V电源,共地即可。

最后说一句:定时器PWM输出,是嵌入式开发中最基础也最实用的技能之一。你把它吃透了,后面做电机控制、舵机控制、甚至音频输出,都会轻松很多。我当年做第一个平衡车项目时,就是靠这几个定时器撑起了整个控制逻辑。嗯,动手试试吧,代码跑起来才是硬道理。