第四章:定时器与PWM调试
定时器和PWM,这两个东西在嵌入式里太常用了。说白了,没有定时器,你的单片机就像个没闹钟的人——不知道什么时候该干什么。而PWM呢,就是让单片机学会「说话」的方式,用高低电平的比例来传递信息。
我个人习惯把定时器比作「心跳」,PWM比作「呼吸」。心跳要准,呼吸要稳。今天我们就来聊聊怎么调好这个心跳和呼吸。
4.1 定时器中断配置
定时器中断,就是让单片机每隔固定时间「叮」一下你。我在项目中遇到过不少新手,上来就开中断,结果程序跑飞了——为什么?因为中断频率太高,主循环根本没机会执行。
配置定时器其实就三步:
- 选时钟源——内部时钟还是外部时钟?一般用内部就行
- 设预分频——把高频时钟降下来,比如72MHz降到1MHz
- 设自动重装值——决定中断周期,比如设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输出,以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);
}
占空比测量呢?我一般用输入捕获模式。把待测信号接到定时器的输入通道上,捕获上升沿和下降沿的时间差,就是高电平宽度。再捕获两个上升沿的时间差,就是周期。两者一除,占空比就出来了。
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时,示波器是你的好朋友。别光靠眼睛看LED亮度变化来判断占空比——人眼对亮度的感知是非线性的,你看着50%亮度,实际可能只有30%占空比。用示波器看波形,一清二楚。
好了,定时器和PWM就聊到这儿。下一章我们讲通信接口调试,那又是另一片天地了。