3、MCU定时器配置:以STM32为例,讲解通用定时器(TIM)的时钟源、预分频器、自动重装载寄存器配置,生成精确PWM。
好,咱们直接进入正题。舵机控制的核心,说白了就是PWM。而PWM的精度和稳定性,全靠定时器撑着。我见过不少新手,上来就调PWM频率,结果舵机抖得像筛糠,最后发现是定时器时钟源没选对。今天咱们就以STM32的通用定时器为例,把时钟源、预分频器、自动重装载寄存器这三个东西彻底讲透。
3.1 时钟源:PWM的“心跳”从哪来?
定时器要工作,首先得有时钟。STM32的通用定时器(比如TIM2、TIM3、TIM4、TIM5)的时钟源,通常来自APB1总线。但这里有个坑——APB1的时钟频率不一定等于定时器时钟频率。
我习惯先查一下芯片的时钟树。以STM32F103为例,如果APB1预分频器设置为1(不分频),那么定时器时钟就等于APB1时钟。但如果APB1预分频器大于1,定时器时钟会翻倍,变成APB1时钟的两倍。嗯,这里要注意:很多库函数初始化时,默认APB1是2分频,所以定时器时钟其实是72MHz(如果系统时钟是72MHz的话)。
重要结论: 定时器时钟频率 = APB1时钟频率 × 2(当APB1预分频系数≠1时)
你想想看,如果这个搞错了,后面算出来的PWM频率全都会翻车。我在项目中遇到过一回,明明算好了50Hz的舵机控制频率,结果输出成了100Hz,舵机直接不认。查了半天,才发现是时钟源理解错了。
3.2 预分频器(PSC):把时钟“降频”到合适范围
时钟源频率太高,直接给计数器用肯定不行。预分频器的作用,就是把时钟频率降下来。说白了,就是分频。
预分频器的值是一个16位的寄存器,范围从0到65535。实际分频系数 = PSC寄存器的值 + 1。举个例子:
- 如果PSC = 0,分频系数 = 1,时钟不变
- 如果PSC = 71,分频系数 = 72,72MHz变成1MHz
- 如果PSC = 7199,分频系数 = 7200,72MHz变成10kHz
我个人习惯,先把定时器时钟降到1MHz,这样每个计数周期就是1微秒,算起来特别方便。舵机控制一般需要50Hz的PWM,周期20ms,也就是20000微秒。用1MHz的计数时钟,自动重装载值设成20000-1,刚刚好。
小技巧: 把PSC设成(定时器时钟频率 / 1000000) - 1,就能得到1MHz的计数时钟。这个公式我用了好多年,基本没出过问题。
3.3 自动重装载寄存器(ARR):决定PWM周期
ARR决定了计数器从0数到多少,然后重新开始。PWM的周期,就是由ARR决定的。
计算公式很简单:
PWM频率 = 定时器时钟频率 / ((PSC + 1) × (ARR + 1))
还是用刚才的例子:定时器时钟72MHz,PSC = 71(分频到1MHz),ARR = 19999(计数20000次),那么PWM频率 = 72MHz / (72 × 20000) = 50Hz。完美。
我曾经犯过一个低级错误——ARR设成了20000,结果周期变成了20001个计数,频率偏了一点点。舵机对频率其实挺敏感的,差个几赫兹,角度就会抖。所以,记得ARR要减1。
注意: ARR和PSC都是16位的寄存器,最大值65535。如果你的PWM频率特别低(比如1Hz),可能需要把PSC设得很大,或者用32位定时器(比如TIM2、TIM5在某些系列上是32位的)。
3.4 生成PWM:捕获/比较寄存器(CCR)上场
有了时钟、有了周期,接下来就是控制占空比了。CCR寄存器就是干这个的。
当计数器的值小于CCR时,PWM输出高电平;大于等于CCR时,输出低电平(或者反过来,取决于极性设置)。所以,占空比 = CCR / (ARR + 1) × 100%。
舵机的控制信号,周期20ms,高电平脉宽0.5ms到2.5ms对应0°到180°。换算成CCR值:
| 舵机角度 | 高电平脉宽 | CCR值(1MHz计数) |
|---|---|---|
| 0° | 0.5ms | 500 |
| 90° | 1.5ms | 1500 |
| 180° | 2.5ms | 2500 |
嗯,这里要注意:不同品牌的舵机,脉宽范围可能略有差异。我建议拿到舵机后,先用示波器实测一下,确认最小和最大脉宽。别问我为什么知道——我曾经因为没看手册,直接把一个舵机烧了。
3.5 代码示例:STM32 HAL库配置
说了这么多理论,咱们直接上代码。以下是用STM32 HAL库配置TIM3输出PWM的示例:
// 定时器句柄
TIM_HandleTypeDef htim3;
// 定时器初始化函数
void MX_TIM3_Init(void)
{
TIM_OC_InitTypeDef sConfigOC = {0};
htim3.Instance = TIM3;
htim3.Init.Prescaler = 71; // PSC = 71,72MHz / 72 = 1MHz
htim3.Init.CounterMode = TIM_COUNTERMODE_UP;
htim3.Init.Period = 19999; // ARR = 19999,周期20ms
htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
htim3.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
if (HAL_TIM_PWM_Init(&htim3) != HAL_OK)
{
Error_Handler();
}
// 配置PWM通道1
sConfigOC.OCMode = TIM_OCMODE_PWM1;
sConfigOC.Pulse = 1500; // CCR = 1500,初始占空比1.5ms(90°)
sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
if (HAL_TIM_PWM_ConfigChannel(&htim3, &sConfigOC, TIM_CHANNEL_1) != HAL_OK)
{
Error_Handler();
}
}
// 启动PWM输出
void Start_PWM(void)
{
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1);
}
// 动态修改占空比
void Set_Servo_Angle(uint16_t pulse_width)
{
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, pulse_width);
}
代码很简单,但有几个细节我提一下:
- AutoReloadPreload 建议使能。这样修改ARR时,会在下一个周期生效,避免产生不完整的PWM脉冲。
- Pulse 初始值设成1500,对应90°。这样上电后舵机不会突然转到极限位置,比较安全。
- 修改占空比 用
__HAL_TIM_SET_COMPARE宏,效率高,而且不会产生毛刺。
避坑指南: 我曾经在中断里直接修改CCR,结果因为中断优先级设置不当,导致PWM波形出现了毛刺。后来我改成在主循环里修改,或者用DMA更新CCR,问题就解决了。如果你需要实时控制舵机角度,建议用DMA或者定时器更新事件中断。
3.6 总结一下
定时器配置PWM,说白了就是三步:
- 选时钟源——搞清楚定时器时钟到底是多少MHz
- 设预分频器——把时钟降到方便计算的频率(比如1MHz)
- 设自动重装载值——决定PWM周期(舵机就是20ms)
最后用CCR控制占空比,就搞定了。你想想看,是不是比想象中简单?
我个人习惯,每次配置完定时器,都会用示波器看一眼波形。频率对不对?占空比准不准?有没有毛刺?确认无误了再往下走。这个习惯帮我省了不少调试时间。
下一章咱们聊聊PWM的极性和对齐模式,这些在电机控制里也很关键。到时候见。