第三章 定时器与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 输入捕获测量脉冲宽度
输入捕获,就是测量外部信号的脉宽或频率。原理很简单:当检测到边沿时,把当前计数器的值锁存到捕获寄存器。
测量高电平脉宽的步骤:
- 配置捕获通道为上升沿触发
- 捕获到上升沿时,记录当前CNT值(记为t1)
- 立即切换为下降沿触发
- 捕获到下降沿时,记录当前CNT值(记为t2)
- 脉宽 = (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控制,那才是真正让系统稳定运行的核心算法。