第三章 定时器与PWM:让电机听话的艺术

各位同学,今天我们来聊聊嵌入式控制里最实用的模块——定时器与PWM。说实话,我做了十几年嵌入式开发,用到的外设里,定时器的出场率绝对排前三。无论是控制舵机、驱动电机,还是测量脉冲宽度,都离不开它。

STM32的定时器功能非常强大,但很多人一开始会被它的复杂性吓到。别担心,我们一步步来拆解。

3.1 STM32通用定时器原理

STM32的定时器分三类:基本定时器、通用定时器、高级定时器。我们最常用的是通用定时器,比如TIM2、TIM3、TIM4、TIM5。

它的核心结构其实很简单:一个16位(或32位)的计数器,一个预分频器,几个捕获/比较通道。说白了,就是计数器从0数到某个值,然后触发各种事件。

我个人习惯把定时器想象成一个「电子秒表」:

  • 预分频器(PSC):相当于给秒表降频。比如72MHz的时钟,分频后变成1MHz,计数器每1微秒加1。
  • 自动重装载寄存器(ARR):就是你要数的最大值。数到ARR就归零,产生更新事件。
  • 计数器(CNT):当前计数值,一直在跑。

举个例子,你想产生1ms的中断:

// 假设时钟72MHz,预分频72-1,则计数频率1MHz
TIM_TimeBaseInitTypeDef tim;
tim.TIM_Prescaler = 72 - 1;      // 72分频,得到1MHz
tim.TIM_Period = 1000 - 1;       // 计数值1000,即1ms
tim.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM3, &tim);
TIM_ITConfig(TIM3, TIM_IT_Update, ENABLE);
TIM_Cmd(TIM3, ENABLE);

嗯,这里要注意:预分频器和周期值都要减1,因为计数器是从0开始数的。

3.2 PWM输出控制舵机/电机

PWM,全称脉冲宽度调制。说白了就是输出一串方波,通过改变高电平占的比例(占空比)来控制设备。

舵机控制:舵机看的是脉冲宽度,不是占空比。标准舵机要求20ms周期,脉宽0.5ms~2.5ms对应0°~180°。

我在项目中遇到过一个问题:用TIM3的CH1输出PWM控制舵机,结果舵机一直抖。查了半天,发现是PWM频率不对。标准舵机要求50Hz(20ms周期),而我设成了1kHz。

关键参数计算:

目标频率50Hz,时钟72MHz

预分频 = 72 - 1 → 计数频率1MHz

ARR = 20000 - 1 → 周期20ms

脉宽0.5ms → CCR = 500

脉宽1.5ms → CCR = 1500(中位)

脉宽2.5ms → CCR = 2500

// 配置TIM3_CH1输出PWM
TIM_OCInitTypeDef oc;
oc.TIM_OCMode = TIM_OCMode_PWM1;
oc.TIM_Pulse = 1500;          // 初始中位
oc.TIM_OutputState = TIM_OutputState_Enable;
oc.TIM_OCPolarity = TIM_OCPolarity_High;
TIM_OC1Init(TIM3, &oc);
TIM_OC1PreloadConfig(TIM3, TIM_OCPreload_Enable);
TIM_Cmd(TIM3, ENABLE);

// 改变角度
TIM_SetCompare1(TIM3, 2000);  // 转到约120°

直流电机控制:电机看的是占空比,频率一般选10kHz~20kHz。频率太低电机会嗡嗡响,频率太高MOS管开关损耗大。

我曾经做过一个平衡小车项目,电机PWM频率设成了100Hz,结果电机噪音大得吓人,而且低速时根本转不动。后来改成10kHz,问题就解决了。

我的经验:电机PWM频率选10kHz左右比较稳妥。如果驱动芯片支持,可以试试20kHz,人耳听不到,更安静。

3.3 输入捕获测量脉冲宽度

输入捕获,就是测量外部信号的脉宽或频率。原理很简单:当检测到边沿时,把当前计数器的值锁存到捕获寄存器。

测量高电平脉宽的步骤:

  1. 配置捕获通道为上升沿触发
  2. 捕获到上升沿时,记录当前CNT值(记为t1)
  3. 立即切换为下降沿触发
  4. 捕获到下降沿时,记录当前CNT值(记为t2)
  5. 脉宽 = (t2 - t1) × 计数周期
// 输入捕获初始化(TIM3_CH1)
TIM_ICInitTypeDef ic;
ic.TIM_Channel = TIM_Channel_1;
ic.TIM_ICPolarity = TIM_ICPolarity_Rising;  // 先上升沿
ic.TIM_ICSelection = TIM_ICSelection_DirectTI;
ic.TIM_ICPrescaler = TIM_ICPSC_DIV1;
ic.TIM_ICFilter = 0x0;
TIM_ICInit(TIM3, &ic);

// 中断中切换边沿
void TIM3_IRQHandler(void) {
    if(TIM_GetITStatus(TIM3, TIM_IT_CC1)) {
        uint16_t val = TIM_GetCapture1(TIM3);
        static uint16_t t_rise;
        static uint8_t state = 0;
        
        if(state == 0) {  // 上升沿
            t_rise = val;
            TIM_OC1PolarityConfig(TIM3, TIM_ICPolarity_Falling);
            state = 1;
        } else {           // 下降沿
            uint16_t width = val - t_rise;
            // width就是脉宽(计数个数)
            TIM_OC1PolarityConfig(TIM3, TIM_ICPolarity_Rising);
            state = 0;
        }
        TIM_ClearITPendingBit(TIM3, TIM_IT_CC1);
    }
}

注意:如果脉宽超过一个计数周期(ARR+1),需要处理溢出。一般用定时器的更新事件来记录溢出次数。

3.4 编码器模式测速

编码器模式,是STM32定时器的一个隐藏技能。它可以直接读取正交编码器的A、B相脉冲,自动计算位置和方向。

为什么用编码器?因为普通IO口读脉冲,容易丢步,而且还要自己判断方向。编码器模式硬件自动处理,精度高、不丢步。

配置要点:

  • 编码器模式有3种:TI1、TI2、TI1和TI2。一般用TI1和TI2(4倍频模式),精度最高。
  • 计数器在A相上升沿、下降沿,B相上升沿、下降沿都计数,所以4倍频。
  • 方向由A、B相相位差决定:A超前B为正转,B超前A为反转。
// 编码器模式配置(TIM3_CH1和CH2接编码器)
TIM_EncoderInterfaceConfig(TIM3, 
    TIM_EncoderMode_TI12,           // 4倍频
    TIM_ICPolarity_Rising,          // 不反相
    TIM_ICPolarity_Rising);

TIM_Cmd(TIM3, ENABLE);

// 读取位置
int16_t pos = TIM_GetCounter(TIM3);  // 有符号数,自动处理方向

// 测速:每隔固定时间读取一次,差值就是速度
static int16_t last_pos = 0;
int16_t speed = pos - last_pos;  // 速度值(脉冲数/采样周期)
last_pos = pos;

我记得有一次做AGV小车,编码器模式死活读不出数据。查了半天,发现是编码器的A、B相接反了。交换一下就好了。所以大家接线时一定要注意相位顺序。

测速公式:

转速(rpm) = (脉冲数 × 60) / (编码器线数 × 4 × 采样时间秒)

例如:编码器1000线,采样100ms,读到200个脉冲

转速 = (200 × 60) / (1000 × 4 × 0.1) = 30 rpm

好了,这一章的内容就到这里。定时器和PWM是嵌入式控制的基石,掌握了它们,你就能让电机听话、让舵机精准定位。下一章我们聊聊PID控制,那才是真正让系统稳定运行的核心算法。