第四章:定时器与PWM调试

定时器和PWM,这两个东西在嵌入式里太常用了。说白了,没有定时器,你的单片机就像个没闹钟的人——不知道什么时候该干什么。而PWM呢,就是让单片机学会「说话」的方式,用高低电平的比例来传递信息。

我个人习惯把定时器比作「心跳」,PWM比作「呼吸」。心跳要准,呼吸要稳。今天我们就来聊聊怎么调好这个心跳和呼吸。

4.1 定时器中断配置

定时器中断,就是让单片机每隔固定时间「叮」一下你。我在项目中遇到过不少新手,上来就开中断,结果程序跑飞了——为什么?因为中断频率太高,主循环根本没机会执行。

注意:定时器中断频率不要超过系统主频的1/1000。比如72MHz的主频,中断频率别超过72kHz,否则CPU大部分时间都在处理中断。

配置定时器其实就三步:

  1. 选时钟源——内部时钟还是外部时钟?一般用内部就行
  2. 设预分频——把高频时钟降下来,比如72MHz降到1MHz
  3. 设自动重装值——决定中断周期,比如设1000,那就是1ms中断一次

举个例子,STM32上配置一个1ms的定时器中断:

// 定时器2初始化,1ms中断一次
void TIM2_Init(void)
{
    TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
    
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
    
    TIM_TimeBaseStructure.TIM_Prescaler = 72 - 1;      // 72MHz / 72 = 1MHz
    TIM_TimeBaseStructure.TIM_Period = 1000 - 1;       // 1MHz / 1000 = 1kHz = 1ms
    TIM_TimeBaseStructure.TIM_ClockDivision = 0;
    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开始计数的。我曾经有个同事忘了减1,结果定时器慢了1个时钟周期,排查了半天。

4.2 PWM波形生成与占空比测量

PWM生成其实不复杂。定时器工作在PWM模式下,比较寄存器CCR的值决定了占空比。ARR是周期,CCR是高电平时间。占空比 = CCR / ARR × 100%。

你想想看,如果ARR=1000,CCR=500,那就是50%占空比。CCR=250就是25%。就这么简单。

核心公式:PWM频率 = 时钟频率 / (预分频+1) / (自动重装值+1)

配置PWM输出,以STM32的TIM3通道1为例:

void PWM_Init(void)
{
    TIM_OCInitTypeDef TIM_OCInitStructure;
    GPIO_InitTypeDef GPIO_InitStructure;
    
    // 使能时钟
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
    
    // 配置PA6为TIM3_CH1复用输出
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStructure);
    
    // 定时器配置
    TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
    TIM_TimeBaseStructure.TIM_Prescaler = 72 - 1;      // 1MHz
    TIM_TimeBaseStructure.TIM_Period = 20000 - 1;      // 50Hz,周期20ms
    TIM_TimeBaseStructure.TIM_ClockDivision = 0;
    TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
    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);
}

占空比测量呢?我一般用输入捕获模式。把待测信号接到定时器的输入通道上,捕获上升沿和下降沿的时间差,就是高电平宽度。再捕获两个上升沿的时间差,就是周期。两者一除,占空比就出来了。

小技巧:测量高频PWM时,记得把定时器的时钟频率设高一些,否则分辨率不够。比如测1MHz的PWM,时钟至少10MHz以上,才能有10%的分辨率。

4.3 实战:舵机控制与速度调节

舵机控制是PWM的经典应用。标准舵机要求50Hz的PWM,周期20ms。高电平宽度0.5ms~2.5ms对应0°~180°。中间值1.5ms对应90°。

我曾经调试过一个六轴机械臂,舵机死活转不到位。后来发现是PWM频率偏了——晶振误差导致定时器时钟不准。解决办法是用示波器实测PWM波形,微调预分频值。

舵机控制代码其实很简单:

// 设置舵机角度,angle范围0~180
void Servo_SetAngle(uint16_t angle)
{
    // 0.5ms ~ 2.5ms 对应 0°~180°
    // 定时器周期20ms,计数20000,1计数=1us
    uint16_t pulse = 500 + (uint16_t)((float)angle / 180.0f * 2000.0f);
    
    TIM_SetCompare1(TIM3, pulse);
}

速度调节呢?说白了就是控制舵机从当前角度转到目标角度的快慢。不能直接跳变,要一步步走。我习惯用「梯形加减速」——先加速,再匀速,最后减速。

速度调节核心思路:每次中断里只改变一点点角度,而不是一步到位。

举个例子,1ms中断一次,每次增加1°:

uint16_t current_angle = 90;   // 当前角度
uint16_t target_angle = 120;   // 目标角度
uint8_t speed = 1;             // 每步1°

void TIM_IRQHandler(void)
{
    if(TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET)
    {
        if(current_angle < target_angle)
            current_angle += speed;
        else if(current_angle > target_angle)
            current_angle -= speed;
        
        Servo_SetAngle(current_angle);
        
        TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
    }
}

嗯,这里要注意:舵机有物理响应时间,步进太快会丢步。我一般把步进间隔设在5ms以上,每步角度不超过2°。这样舵机才能跟得上。

避坑指南:我曾经遇到过舵机抖动的问题,原因是PWM频率不稳定。检查后发现是定时器中断里做了太多计算,导致PWM更新不及时。解决办法:把角度计算放到主循环里,中断里只做赋值操作。

最后说一句:调试PWM时,示波器是你的好朋友。别光靠眼睛看LED亮度变化来判断占空比——人眼对亮度的感知是非线性的,你看着50%亮度,实际可能只有30%占空比。用示波器看波形,一清二楚。

好了,定时器和PWM就聊到这儿。下一章我们讲通信接口调试,那又是另一片天地了。