4、定时器中断实现精准定时
定时器中断,说白了就是让芯片内部有个“闹钟”。
到了设定时间,它就自动跳出来喊CPU:“嘿,该干活了!”
我刚开始做运动控制那会儿,总觉得用延时函数也能凑合。后来发现,电机一跑起来,CPU被堵死在延时里,啥也干不了。嗯,那才叫一个惨。
4.1 配置定时器中断
配置定时器中断,其实就三步:
- 选定时器:一般用通用定时器,比如STM32的TIM2~TIM5。
- 设预分频:把时钟频率降下来,别让计数器跑太快。
- 设自动重装值:决定多久触发一次中断。
我习惯用库函数来配,清晰又不容易出错。给你看个例子:
// 以STM32F4为例,配置TIM3,1ms中断一次
void TIM3_Init(void)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
TIM_TimeBaseStructure.TIM_Prescaler = 84 - 1; // 84MHz / 84 = 1MHz
TIM_TimeBaseStructure.TIM_Period = 1000 - 1; // 1MHz / 1000 = 1kHz
TIM_TimeBaseStructure.TIM_ClockDivision = 0;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure);
TIM_ITConfig(TIM3, TIM_IT_Update, ENABLE);
NVIC_InitStructure.NVIC_IRQChannel = TIM3_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
TIM_Cmd(TIM3, ENABLE);
}
这里有个小细节:预分频和自动重装值都要减1。为什么?因为计数器是从0开始数的。你想想看,从0数到99,是不是正好100个数?
4.2 中断频率计算
中断频率的计算公式其实很简单:
中断频率 = 定时器时钟 / (预分频值 + 1) / (自动重装值 + 1)
拿上面的例子来说:
- 定时器时钟:84 MHz
- 预分频值:84 - 1
- 自动重装值:1000 - 1
算一下:84,000,000 / 84 / 1000 = 1000 Hz,也就是1ms中断一次。
常见中断频率与用途对照表
| 中断频率 | 周期 | 典型用途 |
|---|---|---|
| 100 Hz | 10 ms | 状态刷新、按键扫描 |
| 1 kHz | 1 ms | PID计算、速度环 |
| 10 kHz | 100 μs | 电流环、PWM生成 |
| 100 kHz | 10 μs | 编码器捕获、高频采样 |
我在项目中遇到过一个问题:把中断频率设得太高,结果CPU大部分时间都在进中断,主循环反而跑不动了。后来我学乖了,一般控制在1kHz以内,除非电流环这种必须高频的场合。
4.3 在中断中处理周期性任务
中断服务函数里,千万别写太长的代码。我见过有人把整个PID算法都塞进去,结果中断还没跑完,下一次中断又来了——这叫“中断嵌套”,搞不好就死机。
我的做法是:中断里只做三件事。
- 读数据:比如编码器值、ADC采样值。
- 算结果:比如PID输出、状态更新。
- 写标志:告诉主循环“数据准备好了”。
给你看个实际例子:
volatile uint8_t pid_ready_flag = 0;
volatile int16_t encoder_count = 0;
volatile int16_t target_speed = 1000;
void TIM3_IRQHandler(void)
{
if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET)
{
TIM_ClearITPendingBit(TIM3, TIM_IT_Update);
// 1. 读编码器
encoder_count = ReadEncoder();
// 2. 简单PID计算(位置式)
static int16_t last_error = 0;
int16_t error = target_speed - encoder_count;
int16_t output = KP * error + KD * (error - last_error);
last_error = error;
// 3. 设置PWM占空比
SetMotorPWM(output);
// 4. 置标志位(可选)
pid_ready_flag = 1;
}
}
我的小技巧:中断里用的变量,一定要加 volatile 关键字。否则编译器优化时,可能把变量值直接缓存到寄存器里,你读到的永远是旧数据。我曾经因为这个坑,查了整整两天bug。
4.4 避坑指南
注意!这些坑我踩过:
- 中断服务函数别太长:超过下一次中断周期,就会丢中断。
- 别在中断里用printf:打印函数太慢,会拖死系统。
- 全局变量加volatile:前面说过了,血的教训。
- 中断优先级要合理:高频率的中断优先级设高,低频率的设低。
嗯,这里还要提一句:如果你用FreeRTOS这类系统,中断里别调用系统API。比如 vTaskDelay(),那玩意儿只能在任务里用。中断里发个信号量或者消息队列就够了。
4.5 知识体系总览
下面这张图,是我自己总结的定时器中断知识结构。你看一眼,心里就有数了。
说白了,定时器中断就是运动控制的“心跳”。心跳稳了,电机才能跑得稳。我这些年做过的项目,从简单的步进电机到多轴伺服,底层全靠这个机制撑着。
你先把上面代码跑通,感受一下1ms中断的节奏。然后试着改改频率,看看电机响应有什么变化。实践出真知嘛。
公众号:蓝海资料掘金营,微信deep3321