2、中断系统基础:中断的概念、中断向量表、中断优先级、中断嵌套、中断服务函数(ISR)的编写规范

各位同学,咱们今天聊聊中断系统。说实话,中断是嵌入式运动控制里最核心的机制之一。没有中断,你的电机控制基本就是「轮询死等」,效率低得可怜。我刚开始做运动控制那会儿,就吃过这个亏——一个简单的步进电机加减速,用轮询写出来,CPU占用率直接飙到90%,啥也干不了。

后来我明白了,中断才是嵌入式系统的灵魂。你想想看,CPU正在忙着算轨迹,突然电机堵转了,或者编码器来了个脉冲,怎么办?总不能停下来等吧?中断就是来解决这个问题的。

核心观点:中断系统让CPU从「主动轮询」变成「被动响应」,效率提升不止一个量级。

2.1 中断的概念

中断,说白了就是「CPU正在干一件事,突然被另一件更紧急的事打断,处理完紧急事后,再回来接着干原来的事」。

举个例子:你在写代码,突然电话响了。你放下笔接电话,接完再继续写。这个电话就是「中断源」,接电话就是「中断服务函数」,放下笔的位置就是「断点」。

在运动控制里,常见的中断源有:

  • 定时器中断:用于产生PWM脉冲、控制步进电机速度
  • 外部中断:编码器信号、限位开关、急停按钮
  • DMA中断:数据传输完成通知
  • ADC中断:电流采样完成

我记得有个项目,客户要求电机在0.1ms内响应急停信号。用轮询?根本不可能。最后就是用外部中断,从信号触发到电机停止,只用了不到50μs。

2.2 中断向量表

中断向量表,你可以把它理解成一个「电话本」。每个中断源都有一个固定的编号,CPU收到中断信号后,就去查这个电话本,找到对应的处理函数地址,然后跳过去执行。

以STM32为例,中断向量表通常放在Flash的起始地址:

// 中断向量表示例(简化版)
__attribute__((section(".isr_vector")))
void (* const g_pfnVectors[])(void) = {
    &__StackTop,           // 0: 栈顶指针
    Reset_Handler,         // 1: 复位中断
    NMI_Handler,           // 2: 不可屏蔽中断
    HardFault_Handler,     // 3: 硬件错误中断
    // ... 其他中断
    TIM2_IRQHandler,       // 28: 定时器2中断
    TIM3_IRQHandler,       // 29: 定时器3中断
    // ...
};

我的经验:调试中断问题时,第一步就是检查中断向量表有没有配错。我曾经花了一整天查一个定时器不工作的bug,最后发现是中断向量表偏移地址写错了。嗯,这种低级错误,犯过一次就记住了。

2.3 中断优先级

多个中断同时来了怎么办?谁先响应?这就涉及到中断优先级了。

中断优先级分两种:

  • 抢占优先级:高优先级可以打断低优先级的中断
  • 子优先级:同抢占优先级时,谁先执行

在运动控制中,优先级分配有讲究:

中断类型 推荐优先级 原因
急停/限位开关 最高(0) 安全第一,必须立即响应
编码器脉冲 高(1) 丢失脉冲会导致位置误差
PWM定时器 中(2) 控制周期稳定很重要
通信中断 低(3) 可以容忍一定延迟

注意:优先级不是越高越好。你把所有中断都设成最高优先级,那跟没有优先级有什么区别?而且高优先级中断会阻塞其他中断,影响系统实时性。

2.4 中断嵌套

中断嵌套,就是「中断里套中断」。高优先级中断可以打断正在执行的低优先级中断服务函数。

举个例子:CPU正在处理定时器中断(优先级2),突然来了个急停信号(优先级0)。CPU会暂停定时器中断,先去处理急停,处理完再回来继续执行定时器中断。

我画了个流程图,帮你理解这个过程:

主程序执行 低优先级中断 (定时器中断) 高优先级中断 (急停信号) 继续执行低优先级中断 返回主程序 中断嵌套执行流程:主程序 → 低优先级中断 → 高优先级中断 → 返回低优先级 → 返回主程序

我的建议:中断嵌套层级不要超过3层。嵌套太深,栈空间容易溢出,而且调试起来非常痛苦。我曾经在一个项目里嵌套了5层中断,结果系统时不时死机,查了三天才发现是栈溢出了。

2.5 中断服务函数(ISR)的编写规范

ISR的编写,有几点铁律必须遵守:

  1. 短小精悍:ISR里只做最紧急的事,比如置个标志位、读个寄存器。复杂的计算、延时、打印,统统放到主循环里做。
  2. 避免阻塞:不要在ISR里用while循环等待,也不要用delay延时。你想想看,ISR里delay一下,其他中断全堵死了。
  3. 保护共享数据:ISR和主循环共享的变量,要用volatile修饰,必要时加临界区保护。
  4. 不要调用不可重入函数:比如printf、malloc这些,在ISR里调用可能会出问题。

来看一个规范的ISR示例:

// 定时器中断服务函数
volatile uint32_t g_uiTickCount = 0;  // 共享变量,用volatile修饰

void TIM2_IRQHandler(void)
{
    // 1. 清除中断标志位
    if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET)
    {
        TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
        
        // 2. 只做最紧急的事:递增计数
        g_uiTickCount++;
        
        // 3. 设置标志位,通知主循环处理
        g_bTimerFlag = 1;
    }
}

// 主循环中处理
int main(void)
{
    while (1)
    {
        if (g_bTimerFlag)
        {
            g_bTimerFlag = 0;
            // 在这里做复杂的计算、控制逻辑
            ProcessMotionControl();
        }
    }
}

避坑指南:我曾经在ISR里直接调用了电机控制函数,结果因为函数执行时间太长,导致下一个中断被错过,电机直接失步。从那以后,我定了个规矩:ISR里只设标志位,所有复杂逻辑都放到主循环里处理。

另外,ISR的命名要规范。不同芯片厂商的命名规则不一样:

  • STM32:TIM2_IRQHandler, USART1_IRQHandler
  • NXP:TIMER0_IRQHandler, UART0_IRQHandler
  • TI:TIMER0A_Handler, UART0_Handler

命名错了,中断向量表对不上,中断就进不去。这个坑,我见过太多人踩了。

总结一下:中断系统是嵌入式运动控制的基石。理解中断的概念、向量表、优先级、嵌套,以及ISR的编写规范,你就能写出高效、可靠的运动控制程序。记住:中断不是万能的,但没有中断是万万不能的。


专注资料整理