16. 中断服务程序设计:定时器中断、编码器中断、中断嵌套处理
中断服务程序,简称 ISR。说白了,它就是嵌入式系统的“紧急通道”。
我刚开始做运动控制那会儿,总觉得主循环里轮询一下就够了。直到有一次,电机丢步了,整个机械臂撞上了限位。嗯,从那以后,我再也不敢轻视中断的设计。
在 RTOS 环境下,中断的设计比裸机要复杂得多。你得考虑优先级、嵌套、还有和任务之间的数据同步。今天我们就来聊聊这三个核心话题:定时器中断、编码器中断、以及中断嵌套处理。
16.1 定时器中断:运动控制的“心跳”
运动控制里,定时器中断就是整个系统的节拍器。插补算法、速度规划、位置闭环,全都依赖这个稳定的时间基准。
16.1.1 定时器中断的典型配置
我个人习惯用硬件定时器的比较匹配模式。比如 STM32 的 TIM 外设,配置成 1ms 中断一次。这个 1ms 就是我们的控制周期。
// 伪代码:定时器中断初始化
void Timer_Init(void) {
// 设置定时器时钟源
TIM_TimeBaseInitTypeDef timBase;
timBase.TIM_Prescaler = 72 - 1; // 72MHz / 72 = 1MHz
timBase.TIM_Period = 1000 - 1; // 1MHz / 1000 = 1kHz (1ms)
TIM_TimeBaseInit(TIM3, &timBase);
// 使能更新中断
TIM_ITConfig(TIM3, TIM_IT_Update, ENABLE);
// 配置NVIC优先级
NVIC_InitTypeDef nvic;
nvic.NVIC_IRQChannel = TIM3_IRQn;
nvic.NVIC_IRQChannelPreemptionPriority = 1; // 抢占优先级
nvic.NVIC_IRQChannelSubPriority = 0; // 子优先级
NVIC_Init(&nvic);
// 启动定时器
TIM_Cmd(TIM3, ENABLE);
}
16.1.2 中断服务函数里该做什么
定时器中断里,我们通常做三件事:
- 读取当前时间戳:用于速度规划和插补计算
- 触发插补计算:计算下一个周期的位置增量
- 发送脉冲或更新PWM:驱动电机
但要注意,ISR 里不能做耗时操作。比如浮点运算、打印日志、动态内存分配,这些都得扔到任务里去。
// 定时器中断服务函数
void TIM3_IRQHandler(void) {
if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET) {
TIM_ClearITPendingBit(TIM3, TIM_IT_Update);
// 1. 记录当前时间
g_CurrentTick++;
// 2. 触发插补计算(通过信号量通知任务)
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xSemaphoreGiveFromISR(xInterpolateSemaphore, &xHigherPriorityTaskWoken);
// 3. 如果需要,直接更新PWM输出
// TIM_SetCompare1(TIM1, g_PulseWidth);
// 4. 退出时检查是否需要任务切换
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
}
16.2 编码器中断:位置反馈的“眼睛”
编码器中断,说白了就是捕捉电机转子的位置变化。增量式编码器每转一圈会产生几百到几千个脉冲。每个脉冲都会触发一次中断。
16.2.1 编码器中断的触发方式
我建议用双边沿触发。为什么?因为单边沿触发会丢失一半的位置信息。你想想看,电机高速旋转时,一个脉冲的宽度可能只有几微秒。如果只检测上升沿,下降沿的信息就浪费了。
| 触发方式 | 分辨率 | 中断频率 | 适用场景 |
|---|---|---|---|
| 单边沿(上升沿) | 1x | 低 | 低速、低精度 |
| 双边沿(上升+下降) | 2x | 中 | 通用运动控制 |
| 四倍频(A+B相) | 4x | 高 | 高精度伺服 |
16.2.2 编码器中断服务函数
编码器中断里,核心任务就是更新位置计数器。同时,还要判断方向。
// 编码器中断服务函数(假设使用外部中断)
void EXTI0_IRQHandler(void) {
if (EXTI_GetITStatus(EXTI_Line0) != RESET) {
EXTI_ClearITPendingBit(EXTI_Line0);
// 读取A相和B相的电平,判断方向
uint8_t A_Phase = GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0);
uint8_t B_Phase = GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_1);
if (A_Phase == B_Phase) {
g_EncoderPosition++; // 正转
} else {
g_EncoderPosition--; // 反转
}
// 注意:这里不要做任何耗时操作
// 位置数据由任务定期读取
}
}
16.3 中断嵌套处理:优先级与抢占
中断嵌套,就是高优先级中断打断低优先级中断。在运动控制中,这很常见。比如编码器中断正在执行,定时器中断来了。谁先处理?
16.3.1 中断优先级的设计原则
我个人习惯这样分配优先级:
- 最高优先级:编码器中断(位置反馈,丢失一个脉冲就是误差)
- 次高优先级:定时器中断(控制周期,抖动会影响速度平稳性)
- 较低优先级:通信中断(如CAN、UART,可以容忍延迟)
- 最低优先级:按键、LED等外设中断
为什么会这样?你想想看,编码器脉冲是硬件产生的,错过了就真的错过了。定时器中断虽然重要,但偶尔延迟一个周期,系统还能扛得住。通信中断就更不用说了,有缓冲区撑着。
16.3.2 中断嵌套的注意事项
中断嵌套最怕什么?怕“死锁”和“栈溢出”。
- 死锁:高优先级中断等待低优先级中断释放资源。这不可能发生,因为低优先级中断已经被打断了。
- 栈溢出:每次嵌套都会消耗栈空间。嵌套层数越多,栈需求越大。
16.3.3 中断与任务的同步
在 RTOS 中,中断和任务之间需要传递数据。常用的方式有:
- 信号量:通知任务有事件发生(如定时器中断触发插补计算)
- 消息队列:传递数据(如编码器位置值)
- 共享变量:简单数据交换(如位置计数器)
使用共享变量时,一定要加 volatile 关键字。否则编译器优化后,任务可能读不到最新的值。
// 共享变量声明
volatile int32_t g_EncoderPosition = 0;
// 任务中读取位置
void vPositionControlTask(void *pvParameters) {
int32_t currentPos;
for (;;) {
// 关中断,保证原子操作
taskENTER_CRITICAL();
currentPos = g_EncoderPosition;
taskEXIT_CRITICAL();
// 处理位置数据...
vTaskDelay(pdMS_TO_TICKS(1));
}
}
16.4 本章小结
中断服务程序是运动控制的“地基”。地基不稳,上面盖的楼再漂亮也没用。
- 定时器中断提供稳定的时间基准,ISR 里只做轻量操作
- 编码器中断负责位置反馈,优先级要设高
- 中断嵌套要控制层数,避免栈溢出
- 中断与任务之间,优先使用 RTOS 提供的同步机制
嗯,这些经验都是我用实际项目换来的。希望你能少走一些弯路。