第三章 定时器与计数器:运动控制的时间基石

各位同学,今天我们来聊聊运动控制里最基础、也最关键的东西——定时器与计数器。

说实话,我刚开始做嵌入式的时候,觉得定时器不就是个计时器嘛,有啥好学的?直到我在一个伺服驱动项目里,因为PWM频率抖动导致电机啸叫,排查了整整三天……嗯,从那以后,我再也不敢小看定时器了。

3.1 定时器工作原理

定时器的本质是什么?说白了就是一个不断累加的计数器,配合一个时钟源。

你想想看,单片机内部有一个时钟,比如72MHz。定时器每来一个时钟脉冲,计数值就加1。当计数值达到我们设定的上限(自动重装载值),就会触发一个事件——比如产生中断,或者翻转一个引脚。

核心公式:

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

举个例子。我常用的STM32F103,定时器时钟是72MHz。如果我想产生一个1ms的定时中断:

// 定时器配置:1ms中断
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);

// 预分频:72MHz / (72-1+1) = 1MHz,即1us计数一次
TIM_TimeBaseStructure.TIM_Prescaler = 71;    
// 自动重装载:1000次计数 = 1ms
TIM_TimeBaseStructure.TIM_Period = 999;      
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);

TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);
TIM_Cmd(TIM2, ENABLE);

这里有个坑,我当年踩过——预分频值和自动重装载值都要减1,因为寄存器是从0开始计数的。我曾经写了个72分频,结果定时时间翻了一倍,电机转得我一脸懵。

3.2 PWM生成实现

PWM,脉冲宽度调制。运动控制里,PWM用来控制电机速度、舵机角度,甚至直接驱动功率管。

定时器生成PWM的原理其实很简单:

  • 计数器从0向上计数到ARR(自动重装载值)
  • 内部有一个比较寄存器CCR
  • 当计数值 < CCR时,输出高电平
  • 当计数值 ≥ CCR时,输出低电平
  • 计数值到达ARR后归零,重新开始

这样,CCR就决定了占空比,ARR决定了频率。

个人习惯:我一般把ARR设成1000或10000,这样占空比调节就是千分级或万分级,调试时心里有数。

来看一个50Hz舵机控制PWM的配置:

// 50Hz PWM,用于舵机控制
// 定时器时钟72MHz,预分频72 -> 1MHz
// ARR = 20000 -> 1MHz/20000 = 50Hz
TIM_OCInitTypeDef TIM_OCInitStructure;

TIM_TimeBaseStructure.TIM_Prescaler = 71;    // 72分频
TIM_TimeBaseStructure.TIM_Period = 19999;    // 50Hz
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure);

// 配置PWM通道1
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
TIM_OCInitStructure.TIM_Pulse = 1500;        // 1.5ms脉宽,舵机中位
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
TIM_OC1Init(TIM3, &TIM_OCInitStructure);

TIM_Cmd(TIM3, ENABLE);

改变占空比只需要改TIM_SetCompare1(TIM3, value)就行。舵机角度从0°到180°,脉宽对应500us到2500us,换算成计数值就是500到2500。

我曾经踩过的坑:PWM输出引脚没有配置为复用推挽输出。结果折腾了半天,示波器上啥都没有。记得检查GPIO模式!

3.3 输入捕获

输入捕获,说白了就是测量外部信号的频率或脉宽。定时器检测到引脚上的上升沿或下降沿时,会立刻把当前计数值"抓拍"下来,存到捕获寄存器里。

应用场景很多:

  • 测量遥控器PWM信号的脉宽
  • 读取超声波模块的回波时间
  • 测量编码器脉冲的频率

测量脉宽的原理:

  1. 配置上升沿捕获
  2. 捕获到上升沿,记录计数值T1
  3. 切换为下降沿捕获
  4. 捕获到下降沿,记录计数值T2
  5. 脉宽 = (T2 - T1) × 定时器周期
// 输入捕获中断服务函数示例
void TIM5_IRQHandler(void)
{
    static uint16_t rise_time = 0;
    uint16_t fall_time = 0;
    uint32_t pulse_width = 0;
    
    if(TIM_GetITStatus(TIM5, TIM_IT_CC1) != RESET)
    {
        if(TIM_GetCapture1(TIM5) & 0x8000)  // 检测到上升沿
        {
            rise_time = TIM_GetCapture1(TIM5) & 0x7FFF;
            TIM_OC1PolarityConfig(TIM5, TIM_ICPolarity_Falling); // 切换下降沿
        }
        else  // 下降沿
        {
            fall_time = TIM_GetCapture1(TIM5) & 0x7FFF;
            pulse_width = (fall_time - rise_time) & 0x7FFF;
            // pulse_width 就是脉宽计数值
            // 实际时间 = pulse_width × (1/1MHz) = pulse_width us
        }
        TIM_ClearITPendingBit(TIM5, TIM_IT_CC1);
    }
}

这里要注意,如果信号频率很低,计数值可能会溢出。我一般会配合定时器的溢出中断,记录溢出次数,这样能测量更宽的脉宽。

3.4 正交编码器接口

正交编码器,运动控制里用来测电机转速和转向的标配。它输出两路相位差90°的方波信号,叫A相和B相。

定时器的编码器模式,硬件上就能自动处理这两路信号:

  • A相超前B相 → 正转,计数器递增
  • B相超前A相 → 反转,计数器递减
  • 同时检测上升沿和下降沿 → 4倍频,分辨率翻4倍

为什么用4倍频? 假设编码器一圈1000个脉冲,4倍频后一圈就是4000个计数。分辨率从0.36°提升到0.09°,精度直接翻4倍。

配置代码:

// 正交编码器模式配置
TIM_EncoderInterfaceConfig(TIM4, 
    TIM_EncoderMode_TI12,      // 同时检测TI1和TI2
    TIM_ICPolarity_Rising,     // 上升沿触发
    TIM_ICPolarity_Rising);

// 定时器基础配置
TIM_TimeBaseStructure.TIM_Prescaler = 0;     // 不分频
TIM_TimeBaseStructure.TIM_Period = 0xFFFF;   // 16位最大计数值
TIM_TimeBaseInit(TIM4, &TIM_TimeBaseStructure);

TIM_Cmd(TIM4, ENABLE);

// 读取当前计数值
int16_t encoder_value = TIM_GetCounter(TIM4);

读取计数值时,我习惯用int16_t类型。因为编码器正转反转,计数值可能从0xFFFF递减到0,用有符号数才能正确判断方向。

避坑指南:我曾经在高速电机上直接用中断读取编码器计数值,结果中断频率太高,CPU忙不过来。后来改用DMA定时读取,或者直接用定时器的从模式做硬件触发,CPU负载瞬间降下来了。

知识体系总览

下面这张图,是我自己总结的定时器知识框架。你看一眼,心里就有谱了。

定时器/计数器 定时器工作原理 时钟源与预分频 自动重装载ARR PWM生成实现 比较寄存器CCR 占空比与频率 输入捕获 脉宽测量 频率测量 正交编码器接口 A/B相信号解码 4倍频技术 运动控制时间基石

这张图把定时器的四个核心功能串起来了。你从中心往外看,每个分支都是一个独立的知识点,但底层都依赖同一个硬件——定时器。

我个人觉得,学定时器最好的方法就是动手。找个开发板,先配一个1ms的中断,让LED闪烁。然后试着输出PWM,用示波器看波形。再找个编码器电机,接上读位置。一步步来,很快就能上手。

嗯,这一章的内容就到这里。定时器这东西,说简单也简单,说复杂也复杂。关键是要理解它的计数本质,剩下的都是应用层面的变化。


公众号:蓝海资料掘金营,微信deep3321