1. 中断基础概念:中断的定义、中断向量表、中断优先级、中断嵌套
各位同学,咱们今天聊聊中断。说实话,中断是嵌入式系统里最核心的机制之一。我做了十几年固件,可以负责任地告诉你——搞不懂中断,就等于没入门嵌入式。
1.1 中断到底是什么?
中断,说白了就是CPU正在干一件事,突然被一个外部或内部事件打断,转去处理这个事件,处理完再回来接着干原来的活。
举个生活中的例子:你正在看书(主程序),电话响了(中断事件)。你放下书去接电话(执行中断服务程序),接完电话回来继续看书(返回主程序)。嗯,就是这么回事。
在嵌入式系统里,中断的来源很多:
- 外部中断:按键按下、传感器信号跳变
- 定时器中断:定时时间到了
- 串口中断:收到一个字节数据
- ADC转换完成中断:模拟量转数字量完毕
我个人习惯把中断看作CPU的「紧急通道」——它让CPU不用一直轮询检查某个事件,而是等事件发生时主动通知CPU。这大大提高了效率。
核心要点:中断机制让CPU从「主动查询」变成「被动响应」,这是实时系统的基石。
1.2 中断向量表——中断的「通讯录」
CPU怎么知道每个中断该去哪里处理?答案就是中断向量表。
中断向量表,你可以把它想象成一本通讯录。每个中断源都有一个编号,对应通讯录里的一个条目。条目里存的是这个中断的服务程序入口地址。
举个例子,STM32F103的中断向量表长这样:
; 中断向量表(部分)
__Vectors DCD __initial_sp ; 栈顶指针
DCD Reset_Handler ; 复位中断
DCD NMI_Handler ; 不可屏蔽中断
DCD HardFault_Handler ; 硬件错误中断
DCD MemManage_Handler ; 内存管理中断
DCD BusFault_Handler ; 总线错误中断
DCD UsageFault_Handler ; 使用错误中断
...
DCD TIM2_IRQHandler ; 定时器2中断
DCD TIM3_IRQHandler ; 定时器3中断
DCD USART1_IRQHandler ; 串口1中断
你看,每个中断源都对应一个函数名。当定时器2产生中断时,CPU就去查向量表,找到TIM2_IRQHandler这个地址,跳过去执行。
我的经验:写中断服务程序时,函数名一定要和启动文件里的名字完全一致。我曾经因为拼写错误,导致中断触发后程序跑飞,查了整整一天才找到问题。嗯,这种坑踩过一次就记住了。
1.3 中断优先级——谁更紧急?
多个中断同时来了怎么办?谁先处理?这就涉及到中断优先级了。
中断优先级,就是给每个中断分配一个「紧急程度」的等级。优先级高的先执行,优先级低的后执行。
以Cortex-M3内核为例,优先级配置通常是这样:
// 设置中断优先级分组
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
// 配置两个中断的优先级
NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; // 抢占优先级
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; // 子优先级
NVIC_Init(&NVIC_InitStructure);
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2; // 抢占优先级
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; // 子优先级
NVIC_Init(&NVIC_InitStructure);
这里有个关键点:抢占优先级决定了一个中断能否打断另一个中断。抢占优先级数字越小,优先级越高。上面例子中,TIM2的抢占优先级是1,USART1是2,所以TIM2可以打断USART1。
| 中断源 | 抢占优先级 | 子优先级 | 实际优先级 |
|---|---|---|---|
| TIM2 | 1 | 0 | 高 |
| USART1 | 2 | 1 | 低 |
避坑指南:我曾经在一个项目中,把两个关键中断的抢占优先级设成了相同值,结果它们互相抢资源,系统时不时卡死。后来才意识到——同优先级的中断不会互相打断,必须等一个处理完另一个才能执行。所以,关键中断一定要分配不同的抢占优先级。
1.4 中断嵌套——中断里还有中断
中断嵌套,就是高优先级的中断打断了正在执行的低优先级中断服务程序。
你想想看:你正在接电话(低优先级中断),突然门铃响了(高优先级中断)。你会怎么做?当然是先跟电话那头说「稍等」,然后去开门,开完门再回来继续打电话。
中断嵌套的流程是这样的:
- 低优先级中断ISR开始执行
- 高优先级中断触发
- CPU暂停低优先级ISR,保存现场
- 执行高优先级ISR
- 高优先级ISR执行完毕,恢复现场
- 继续执行低优先级ISR
代码层面,中断嵌套是硬件自动完成的,我们只需要配置好优先级就行:
// 低优先级中断服务程序
void USART1_IRQHandler(void)
{
// 正在处理串口数据...
uint8_t data = USART1->DR;
// 此时TIM2中断来了!CPU自动跳转
// 处理完TIM2后再回来继续
process_data(data);
}
// 高优先级中断服务程序
void TIM2_IRQHandler(void)
{
// 定时器中断处理
TIM2->SR = ~TIM_SR_UIF;
toggle_led();
}
重要提醒:中断嵌套虽然好用,但嵌套层数不宜过多。我建议最多嵌套2-3层。嵌套太深会导致:
- 栈空间消耗巨大,容易栈溢出
- 低优先级中断响应延迟不可控
- 调试时逻辑混乱,难以定位问题
1.5 我的总结
中断这东西,说白了就是一套「紧急事件处理机制」。向量表告诉CPU去哪里处理,优先级决定谁先处理,嵌套机制让高优先级事件可以插队。
最后分享一个我早年踩过的坑:有一次做电机控制项目,我在中断服务程序里调用了printf函数。结果一跑起来,系统就死机。后来才发现——printf内部用了互斥锁,中断里调用会导致死锁。所以记住:中断服务程序里,尽量别调用那些可能阻塞的函数。
好了,中断的基础概念就讲到这里。下一章咱们聊聊中断服务程序的具体编写规范,包括那些让你少掉头发的注意事项。