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的编写,有几点铁律必须遵守:
- 短小精悍:ISR里只做最紧急的事,比如置个标志位、读个寄存器。复杂的计算、延时、打印,统统放到主循环里做。
- 避免阻塞:不要在ISR里用while循环等待,也不要用delay延时。你想想看,ISR里delay一下,其他中断全堵死了。
- 保护共享数据:ISR和主循环共享的变量,要用volatile修饰,必要时加临界区保护。
- 不要调用不可重入函数:比如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的编写规范,你就能写出高效、可靠的运动控制程序。记住:中断不是万能的,但没有中断是万万不能的。