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); // 开启中断
4.2 通用定时器:功能大爆发
通用定时器(TIM2~TIM5,TIM9~TIM14)在基本定时器基础上,加了:
- 4个独立通道:可配置为输入捕获、输出比较、PWM生成
- 编码器接口:直接接正交编码器测转速
- 霍尔传感器接口:无刷电机换相专用
我做过一个项目,用TIM2的编码器模式读电机转速,同时用TIM3输出PWM控制电机。一个定时器读,一个定时器写,互不干扰。
4.3 高级定时器:电机控制的王牌
高级定时器(TIM1、TIM8)才是重头戏。它比通用定时器多了几个杀手级功能:
| 功能 | 说明 | 我遇到的场景 |
|---|---|---|
| 互补输出 | 一对通道输出互补PWM,带死区插入 | H桥驱动,上下管不能同时导通 |
| 刹车输入 | 紧急停止信号,强制PWM输出为安全状态 | 电机堵转时,刹车脚拉低立刻停 |
| 重复计数器 | N次溢出后才触发更新事件 | 精确控制PWM脉冲个数,步进电机专用 |
| 霍尔/编码器接口 | 硬件自动换相 | 无刷电机FOC控制 |
4.4 PWM输出:从原理到实战
PWM(脉冲宽度调制),说白了就是“开关开关开关”,通过调节高电平占整个周期的比例(占空比)来模拟不同的电压。
占空比 = CCR / ARR,其中CCR是捕获/比较寄存器值,ARR是自动重装载值。
配置一个PWM通道的步骤:
- 初始化定时器:设置预分频和周期
- 配置通道:选择PWM模式1或2,设置极性
- 设置占空比:写CCR寄存器
- 启动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;
}
}
4.7 知识结构图:定时器与PWM体系
下面这张图把本章的知识脉络串起来了,从基本定时器到高级定时器,再到舵机和电机的实际应用:
4.8 常见问题与调试技巧
- PWM没输出?先检查GPIO复用功能配置,再查定时器时钟是否使能。我经常忘了开AFIO时钟。
- 舵机抖动?大概率是PWM频率不对,或者电源功率不够。舵机启动电流可达2A,别用开发板USB供电。
- 电机不转?先测PWM引脚波形,再用万用表测驱动板供电。很多时候是GND没共地。
- 高级定时器死区无效?检查TIM_Break和死区配置寄存器,刹车极性别搞反了。
我的调试三板斧:
- 逻辑分析仪看PWM波形——频率对不对?占空比变没变?
- 示波器看电机驱动输出——死区够不够?有没有直通?
- 串口打印CCR和ARR值——寄存器写进去了没?
定时器和PWM这部分,说白了就是“配参数、看波形、调细节”。配多了自然就熟了。我当年为了调一个无刷电机的启动时序,连续熬了三个晚上,最后发现是死区时间少配了0.5微秒……嗯,从那以后我再也不敢轻视定时器的配置了。