3. 中断系统入门:中断的概念与优先级、中断向量表、中断服务函数(ISR)编写、外部中断实战
各位同学,欢迎来到第三章。这一章我们聊点真正让嵌入式系统“活”起来的东西——中断。
说实话,我刚入行那会儿,对中断的理解就是“一个东西突然跳出来打断你”。后来被项目毒打了几次,才真正明白中断是嵌入式系统的灵魂。没有中断,你的CPU就得像个傻子一样轮询,浪费大量时间在“等”上。
3.1 中断到底是什么?
想象一下,你正在写代码,突然有人拍你肩膀。你停下手中的活,转头看是谁,处理完他的事,再回头继续写。这就是中断。
在嵌入式世界里,中断是硬件或软件发出的一种信号,告诉CPU:“嘿,有急事,先处理我!”CPU会暂停当前任务,保存现场,跳去执行一个专门的函数,处理完再回来继续。
核心概念:中断的本质是“异步事件处理”。它让CPU从轮询的苦海中解脱出来,只在需要时才响应。
我做过一个项目,用轮询方式检测按键。CPU每毫秒都要去读一次GPIO,结果功耗高得离谱。后来改成中断触发,功耗直接降了80%。你想想看,这差距有多大。
3.2 中断优先级:谁更紧急?
多个中断同时来了怎么办?谁先处理?这就涉及到优先级。
中断优先级,说白了就是给每个中断源排个队。优先级高的先执行,低的靠后。如果高优先级中断正在处理,低优先级的来了也得等着。
大部分MCU支持可编程优先级。比如STM32的NVIC(嵌套向量中断控制器),可以设置抢占优先级和子优先级。
| 优先级类型 | 说明 | 典型应用 |
|---|---|---|
| 抢占优先级 | 高优先级可以打断低优先级 | 紧急故障处理 |
| 子优先级 | 同抢占优先级下,子优先级高的先执行 | 普通外设中断 |
注意:优先级设置不当会导致“中断风暴”或“死锁”。我曾经在一个项目中把定时器中断优先级设得比看门狗还高,结果看门狗永远无法复位系统……嗯,那是个难忘的加班夜。
3.3 中断向量表:中断的“电话簿”
CPU怎么知道中断来了该跳到哪里去执行?靠的就是中断向量表。
中断向量表是一张表格,每个中断源对应一个表项,里面存放着该中断服务函数(ISR)的入口地址。CPU收到中断信号后,根据中断号查表,直接跳转。
这张表通常放在Flash或RAM的起始地址。以ARM Cortex-M为例,向量表从0x00000000开始,前几个表项是栈指针和复位向量,后面依次是各种异常和中断向量。
// 典型的Cortex-M向量表布局(简化)
__attribute__((section(".isr_vector")))
void (* const g_pfnVectors[])(void) = {
(void *)&__StackTop, // 0: 栈顶指针
Reset_Handler, // 1: 复位向量
NMI_Handler, // 2: NMI
HardFault_Handler, // 3: 硬错误
// ... 其他异常向量 ...
TIM2_IRQHandler, // 28: 定时器2中断
USART1_IRQHandler, // 37: 串口1中断
EXTI0_IRQHandler, // 6: 外部中断0
// ...
};
我个人习惯把向量表放在独立的段里,这样链接时好控制位置。有些MCU支持向量表重定位,运行时可以搬到RAM里,方便动态修改。
3.4 中断服务函数(ISR)编写
ISR就是中断发生时执行的函数。写ISR有几个铁律,违反了系统就会出问题。
- 短小精悍:ISR里不要做复杂运算,不要调用printf,不要用延时。越快退出越好。
- 保存和恢复现场:编译器会自动处理,但如果你写汇编,就得手动来。
- 避免死锁:ISR里不要等待某个资源,尤其是主循环也在用的资源。
- 清除中断标志:处理完必须清除中断标志,否则会一直触发。
// 一个典型的外部中断ISR示例
void EXTI0_IRQHandler(void) {
// 1. 检查中断标志
if (EXTI->PR & EXTI_PR_PR0) {
// 2. 处理中断(这里只做标记,实际处理放主循环)
g_bButtonPressed = 1;
// 3. 清除中断标志
EXTI->PR = EXTI_PR_PR0;
}
}
经验之谈:ISR里尽量只做“标记”,把实际处理放到主循环或任务里。这样能减少ISR占用时间,避免影响其他中断。我见过有人直接在ISR里做FFT运算,结果系统直接卡死。
3.5 外部中断实战:按键触发
理论说完了,咱们来点实际的。以STM32F103为例,实现一个按键外部中断。
步骤很简单:配置GPIO为输入,使能外部中断线,设置触发边沿,编写ISR。
// 外部中断初始化
void EXTI_Button_Init(void) {
GPIO_InitTypeDef GPIO_InitStruct;
EXTI_InitTypeDef EXTI_InitStruct;
NVIC_InitTypeDef NVIC_InitStruct;
// 1. 使能GPIO时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE);
// 2. 配置PA0为输入
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU; // 上拉输入
GPIO_Init(GPIOA, &GPIO_InitStruct);
// 3. 连接EXTI线到PA0
GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0);
// 4. 配置EXTI线0
EXTI_InitStruct.EXTI_Line = EXTI_Line0;
EXTI_InitStruct.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStruct.EXTI_Trigger = EXTI_Trigger_Falling; // 下降沿触发
EXTI_InitStruct.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTI_InitStruct);
// 5. 配置NVIC
NVIC_InitStruct.NVIC_IRQChannel = EXTI0_IRQn;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 0x01;
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0x01;
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStruct);
}
这段代码配置了PA0作为外部中断输入,下降沿触发。按键按下时,电平从高变低,触发中断。
ISR里我们只设置一个全局标志,主循环检测到这个标志后再做具体处理。这样既保证了ISR的快速退出,又避免了在中断里做复杂操作。
避坑指南:我曾经在ISR里直接调用HAL_Delay(),结果程序卡死。原因是HAL_Delay依赖SysTick中断,而SysTick中断优先级比外部中断低,导致死锁。记住:ISR里不要调用任何可能依赖其他中断的函数。
3.6 中断系统的知识体系
为了让你更直观地理解中断系统的整体结构,我画了一张图。
这张图展示了中断的完整流程:中断源产生信号 -> 中断控制器判断优先级 -> CPU查向量表 -> 跳转执行ISR -> 返回主程序。每一步都环环相扣,缺一不可。
3.7 小结
中断是嵌入式系统的核心机制,掌握了它,你的程序才能真正做到“实时响应”。
这一章我们讲了中断的概念、优先级、向量表、ISR编写,还做了一个外部中断的实战。这些东西看起来简单,但用好了能让系统性能提升一个档次。
记住我踩过的坑:ISR里别做复杂操作,优先级别乱设,中断标志别忘了清。做到这三点,你的中断系统基本就稳了。