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);
    }
}
注意:在 FreeRTOS 的 ISR 中,必须使用带“FromISR”后缀的 API 函数。比如 xSemaphoreGiveFromISR,而不是 xSemaphoreGive。否则会导致系统崩溃。

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--;  // 反转
        }
        
        // 注意:这里不要做任何耗时操作
        // 位置数据由任务定期读取
    }
}
核心原则:编码器中断里只做“计数”这一件事。位置换算、速度计算、滤波处理,全部放到任务里去做。ISR 越短,系统越稳定。

16.3 中断嵌套处理:优先级与抢占

中断嵌套,就是高优先级中断打断低优先级中断。在运动控制中,这很常见。比如编码器中断正在执行,定时器中断来了。谁先处理?

16.3.1 中断优先级的设计原则

我个人习惯这样分配优先级:

  1. 最高优先级:编码器中断(位置反馈,丢失一个脉冲就是误差)
  2. 次高优先级:定时器中断(控制周期,抖动会影响速度平稳性)
  3. 较低优先级:通信中断(如CAN、UART,可以容忍延迟)
  4. 最低优先级:按键、LED等外设中断

为什么会这样?你想想看,编码器脉冲是硬件产生的,错过了就真的错过了。定时器中断虽然重要,但偶尔延迟一个周期,系统还能扛得住。通信中断就更不用说了,有缓冲区撑着。

16.3.2 中断嵌套的注意事项

中断嵌套最怕什么?怕“死锁”和“栈溢出”。

  • 死锁:高优先级中断等待低优先级中断释放资源。这不可能发生,因为低优先级中断已经被打断了。
  • 栈溢出:每次嵌套都会消耗栈空间。嵌套层数越多,栈需求越大。
我曾经踩过的坑:有一次,我把中断嵌套层数设到了5层。结果系统跑着跑着就挂了。查了半天,发现是任务栈不够用。每个中断嵌套层大约需要 200 字节的栈空间。5层就是 1KB。如果你的任务栈只有 2KB,那肯定溢出。后来我把嵌套层数限制在3层以内,问题就解决了。

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));
    }
}
我的建议:能用信号量就别用共享变量。信号量虽然慢一点,但安全。共享变量一旦出现竞态条件,排查起来非常痛苦。我花过整整两天时间,就为了找一个共享变量没加 volatile 的 bug。

16.4 本章小结

中断服务程序是运动控制的“地基”。地基不稳,上面盖的楼再漂亮也没用。

  • 定时器中断提供稳定的时间基准,ISR 里只做轻量操作
  • 编码器中断负责位置反馈,优先级要设高
  • 中断嵌套要控制层数,避免栈溢出
  • 中断与任务之间,优先使用 RTOS 提供的同步机制

嗯,这些经验都是我用实际项目换来的。希望你能少走一些弯路。

中断服务程序设计知识体系 定时器中断 控制周期:1ms 触发插补计算 更新PWM输出 优先级:次高 ISR内不做浮点运算 编码器中断 双边沿触发 更新位置计数器 判断旋转方向 优先级:最高 只做计数,不做计算 中断嵌套处理 优先级分配 避免死锁 控制嵌套层数 栈空间管理 嵌套不超过3层 核心原则:ISR 越短,系统越稳定 使用 FromISR 后缀API,共享变量加 volatile 避坑指南 • 定时器中断优先级不要设最高,留给编码器 • 中断嵌套层数控制在3层以内,防止栈溢出 • 优先使用信号量/消息队列,少用共享变量
一句话总结:中断服务程序的设计,就是“快进快出”。把复杂计算留给任务,ISR 只做最核心的“触发”和“计数”。这样你的运动控制系统才能跑得稳、跑得快。