1. 中断基础概念:中断的定义、中断向量表、中断优先级与嵌套
大家好,欢迎来到咱们这门课的第一节。中断这个话题,说实在的,是MCU开发的基石。你想想看,一个单片机如果没有中断,那就只能轮询——CPU得不停地去问“你好了没?你好了没?”,效率低得吓人。我刚开始做嵌入式那会儿,第一个项目就是搞一个串口通信,没用中断,结果CPU啥都干不了,光顾着在那轮询了。后来一换成中断驱动,整个世界都清净了。
1.1 中断到底是什么?
中断,说白了就是CPU正在干一件事,突然被一个外部或内部的事件打断了。CPU得把手头的活儿先放一放,去处理这个紧急事件,处理完了再回来接着干。
举个例子:你在家看书,突然门铃响了。你放下书去开门,跟快递员说几句话,签收包裹,然后回来继续看书。这个过程,就是中断。
- 门铃响 — 中断请求
- 放下书 — 保存现场(把当前读到哪一页记下来)
- 去开门 — 跳转到中断服务函数
- 签收包裹 — 执行中断处理
- 回来继续看书 — 恢复现场,继续执行
在MCU里,中断源有很多种:外部引脚电平变化、定时器溢出、串口收到数据、ADC转换完成……每个中断源都有一个唯一的中断号。
核心要点:中断机制让CPU从“被动轮询”变成了“主动响应”,实时性就是这么来的。
1.2 中断向量表 — CPU的“紧急联络簿”
中断向量表,我习惯叫它“中断跳转表”。它是一张存储在Flash或ROM里的表格,每个表项对应一个中断号,里面存放的是该中断服务函数(ISR)的入口地址。
当CPU收到一个中断请求时,硬件会自动做这几件事:
- 保存当前正在执行的指令地址(也就是PC指针)
- 根据中断号,去中断向量表里查对应的ISR地址
- 跳转到那个地址去执行
以STM32为例,它的中断向量表起始地址默认在0x00000000。第一个表项放的是栈顶指针,第二个表项放的是复位中断的入口地址,后面依次是各种外设中断。
; STM32F103 中断向量表示例(汇编)
__Vectors DCD __initial_sp ; 栈顶指针
DCD Reset_Handler ; 复位中断
DCD NMI_Handler ; NMI
DCD HardFault_Handler ; 硬错误
DCD MemManage_Handler ; 内存管理
DCD BusFault_Handler ; 总线错误
DCD UsageFault_Handler ; 用法错误
; ... 后面还有几十个中断向量
DCD TIM2_IRQHandler ; 定时器2中断
DCD TIM3_IRQHandler ; 定时器3中断
DCD USART1_IRQHandler ; 串口1中断
个人经验:我遇到过一个问题,程序跑飞了查了半天,最后发现是中断向量表被意外修改了。嗯,这里要注意——如果你的MCU支持中断向量表重定向(比如把向量表搬到SRAM里),一定要确保那片内存不会被其他代码踩到。
1.3 中断优先级 — 谁更紧急谁先来
多个中断同时发生怎么办?CPU得有个决策机制。这就是中断优先级干的事。
不同的MCU对优先级的实现方式不太一样。拿ARM Cortex-M系列来说,它支持两种优先级模型:
| 优先级模型 | 特点 | 应用场景 |
|---|---|---|
| 固定优先级 | 中断号越小,优先级越高 | 简单系统,中断源少 |
| 可编程优先级 | 用户可以给每个中断设置优先级数值 | 复杂系统,需要精细控制 |
在Cortex-M3/M4里,优先级数值越小,优先级越高。比如你给定时器中断设优先级0,串口中断设优先级1,那定时器中断永远比串口中断优先响应。
// STM32 中断优先级配置示例
NVIC_InitTypeDef NVIC_InitStructure;
// 配置USART1中断,优先级为1
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; // 抢占优先级
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; // 子优先级
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
// 配置TIM2中断,优先级为0(更高)
NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
避坑指南:我曾经在一个项目里把两个关键中断设成了相同优先级,结果它们同时触发时,系统行为变得不可预测。后来查手册才发现,同优先级的中断,硬件会按中断号顺序处理——但这并不是所有MCU都一样的。所以我的建议是:尽量给每个中断分配不同的优先级,别偷懒。
1.4 中断嵌套 — 中断里还能再进中断
中断嵌套,就是正在处理一个中断的时候,来了一个更高优先级的中断,CPU会暂停当前的中断服务函数,先去处理那个更紧急的。
这就像你正在接一个电话,突然另一个更重要的电话打进来了——你会跟第一个说“稍等一下”,然后接第二个。
中断嵌套的规则其实很简单:
- 高优先级中断可以打断低优先级中断
- 同优先级中断不能互相打断
- 低优先级中断不能打断高优先级中断
在Cortex-M系列里,中断嵌套是由硬件自动完成的。你不需要在ISR里写额外的代码来保存和恢复现场——硬件已经帮你做了大部分工作。但要注意一点:中断嵌套层数太多,会消耗大量的栈空间。
关键点:中断嵌套虽然能保证高优先级事件得到及时响应,但也会带来风险——栈溢出。我见过一个产品,平时跑得好好的,一旦所有中断同时爆发,系统就死机。最后查出来是中断嵌套把栈给撑爆了。所以,合理规划中断优先级,控制嵌套深度,这是基本功。
1.5 中断响应流程 — 硬件和软件的分工
一个完整的中断响应过程,硬件和软件各干各的活:
硬件自动完成:
- 检测到中断请求信号
- 判断优先级,决定是否响应
- 保存当前PC和xPSR寄存器(Cortex-M还会自动压栈)
- 从向量表取出ISR地址
- 跳转到ISR执行
软件需要完成:
- 在ISR里处理中断事件(读数据、清标志位等)
- 如果需要,通知主循环或RTOS任务
- 退出ISR(硬件自动恢复现场)
// 一个典型的中断服务函数
void USART1_IRQHandler(void)
{
// 检查是不是接收中断
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
{
uint8_t data = USART_ReceiveData(USART1); // 读数据
// 把数据放到缓冲区,或者设置一个标志位
g_rx_buffer[g_rx_index++] = data;
// 清中断标志位(重要!)
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
}
}
我的习惯:ISR里尽量只做最必要的事——读数据、清标志、设标志。复杂的处理逻辑放到主循环或任务里去。为什么?因为ISR执行时间越长,系统对其他事件的响应就越慢。我曾经把一个浮点运算放在ISR里,结果整个系统的实时性一塌糊涂。
1.6 中断延迟 — 从事件发生到CPU响应
中断延迟,指的是从中断事件发生到CPU开始执行ISR的第一条指令之间的时间。这个时间受几个因素影响:
- CPU当前正在执行的指令(有些指令执行时间很长)
- 中断优先级判断的时间
- 是否有关中断的情况
- 中断嵌套的深度
对于Cortex-M系列,中断延迟通常在12个时钟周期左右。但如果你在ISR里关了中断,或者执行了某些特殊指令,这个延迟会变长。
注意:中断延迟是衡量MCU实时性的关键指标。如果你在做电机控制、开关电源这类对时间敏感的应用,一定要把中断延迟算清楚。我见过一个做BLDC驱动的工程师,因为没算中断延迟,导致换相时机总是差那么几微秒,电机嗡嗡响。
好了,中断的基础概念就聊到这儿。下一节我们会深入讲讲中断的配置实战,包括怎么设置优先级分组、怎么处理中断标志位这些具体操作。记住一句话:中断用好了,你的MCU就像长了眼睛和耳朵;用不好,那就是给自己挖坑。