4. 硬件基础:中断系统基础(NVIC、优先级、中断向量表)

中断系统,说白了就是CPU的“紧急事务处理机制”。没有它,你的单片机只能傻乎乎地轮询——就像你每隔5秒去检查一下邮箱,而不是等邮件到了铃声响了再去看。我刚开始做嵌入式时,总觉得中断就是个“跳转”,后来踩了坑才明白,这里面门道深着呢。

4.1 中断到底是个什么玩意儿?

想象一下你正在写代码,突然有人拍你肩膀。你停下手中的活,记下当前写到哪一行,然后转头处理那个人的事。处理完了,再回头接着写。这就是中断的完整流程:暂停 → 保存现场 → 处理中断 → 恢复现场 → 继续执行。

在单片机里,这个“拍肩膀”的动作,就是硬件给你CPU发的一个电信号。ADC转换完成、定时器溢出、串口收到数据……这些都能触发中断。我个人习惯把中断看作“硬件主动通知软件”的唯一高效途径。

核心要点: 中断让CPU从“被动轮询”变成“主动响应”,实时性就是这么来的。

4.2 NVIC:中断的“总调度官”

NVIC(Nested Vectored Interrupt Controller),嵌套向量中断控制器。名字很长,但功能很纯粹:管理所有中断的使能、挂起、优先级和响应顺序。它是ARM Cortex-M系列内核的标配,也是我们做ADC中断+DMA协同调度的基石。

NVIC能干几件事:

  • 使能/禁止中断——决定哪个外设能“拍肩膀”
  • 设置优先级——决定谁先被响应
  • 处理中断嵌套——高优先级可以打断低优先级
  • 管理挂起状态——中断来了但CPU正忙,先记着

我记得第一次用STM32时,以为只要使能中断就能跑,结果发现中断响应顺序完全不对。后来查手册才意识到,NVIC的优先级分组没配好。嗯,这里要注意:优先级分组一旦设定,整个系统都得按这个规则来。

4.3 优先级:抢占优先级 vs 响应优先级

Cortex-M的优先级分两档:抢占优先级和响应优先级(也叫子优先级)。

概念 作用 类比
抢占优先级 决定能否打断正在执行的中断 VIP通道,来了就能插队
响应优先级 同抢占优先级时,决定谁先执行 排队顺序,不能插队

举个例子:中断A的抢占优先级为0,响应优先级为1;中断B的抢占优先级为1,响应优先级为0。那么A可以打断B,因为A的抢占优先级更高。但如果A和B的抢占优先级相同(比如都是1),那就看响应优先级,谁小谁先执行,且不能互相打断。

我的习惯: 在ADC中断+DMA的系统中,我会把DMA传输完成中断设为最高抢占优先级(0),ADC转换完成中断设为次高(1)。因为DMA传输完成意味着数据已经就绪,必须立刻处理,否则可能被后续数据覆盖。

4.4 优先级分组:4位怎么分?

Cortex-M用4位二进制数表示优先级,但这4位怎么拆成“抢占”和“响应”两部分,由优先级分组寄存器(PRIGROUP)决定。常见的分组方式有5种:

分组编号 抢占优先级位数 响应优先级位数 抢占优先级等级数 响应优先级等级数
0 0 4 1 16
1 1 3 2 8
2 2 2 4 4
3 3 1 8 2
4 4 0 16 1

你想想看,如果你的系统里中断数量不多,但每个中断的实时性要求差异很大,那就选分组4(16级抢占,无子优先级)。如果中断很多,但实时性要求差不多,那就选分组0(所有中断同一抢占级,靠子优先级排队)。

我曾经踩过的坑: 在一个项目里,我把优先级分组设成了分组2(2位抢占+2位响应),结果ADC中断和定时器中断的抢占优先级都是3(二进制11),导致它们互相不能打断。ADC转换完成时,定时器中断正在执行,ADC数据等了整整100μs才被处理,采样率直接掉了一半。后来改成分组4,ADC中断抢占优先级设为0,问题解决。

4.5 中断向量表:中断的“电话簿”

中断向量表,就是一张地址映射表。每个中断源对应一个固定的表项,表项里存放的是该中断服务函数(ISR)的入口地址。CPU收到中断信号后,直接查表跳转,不需要if-else判断。

这张表通常放在Flash的起始地址(0x00000000),但Cortex-M允许你把它重映射到SRAM或其他位置。为什么需要重映射?

  • Bootloader场景:APP程序需要把向量表搬到自己的起始地址
  • 动态更新ISR:运行时修改中断服务函数(很少用,但确实有)
  • 双Bank启动:从备份区启动时,向量表指向备份区

在STM32上,重映射向量表只需要写一行代码:

SCB->VTOR = (uint32_t)APP_BASE_ADDRESS;

就这么简单。但要注意:向量表必须256字节对齐,因为VTOR寄存器的低9位是保留位。我见过有人把地址写成0x08010001,结果中断全乱套了——因为地址没对齐,VTOR直接忽略了低9位。

4.6 中断响应流程:从硬件到软件

一个完整的中断响应,硬件和软件各干各的活:

  1. 硬件自动完成:
    • 压栈:自动把R0-R3、R12、LR、PC、xPSR压入当前栈
    • 取向量:从向量表中读取ISR地址
    • 更新PC:跳转到ISR执行
  2. 软件需要完成:
    • 保存现场:如果ISR中使用了R4-R11,需要手动压栈
    • 清除中断标志:避免重复触发
    • 处理中断逻辑:读取ADC数据、启动DMA传输等
    • 恢复现场:出栈,返回主程序

这里有个细节:硬件压栈只压8个寄存器,如果你的ISR里调用了函数,编译器会自动生成额外的压栈/出栈代码。所以ISR越短越好,我一般只在ISR里设一个标志位,真正的处理放到主循环或任务里。

避坑指南: 在ISR里不要做printf、malloc、延时等操作。这些函数要么耗时太长,要么不可重入。我曾经在一个ADC中断里调用了printf,结果中断执行时间超过了下一次ADC转换间隔,导致数据丢失。后来改成用环形缓冲区+标志位,ISR只负责放数据,主循环负责打印。

4.7 中断优先级与DMA的协同关系

在ADC+DMA的系统中,中断优先级的设计直接影响数据采集的可靠性。我一般遵循以下原则:

  • DMA传输完成中断:最高优先级。因为DMA传输完成意味着缓冲区已满或半满,必须尽快处理,否则新数据会覆盖旧数据。
  • ADC转换完成中断:次高优先级。如果ADC是连续转换模式,转换完成中断会频繁触发,优先级太高会饿死其他中断。
  • 其他外设中断:根据实时性需求分配,但不要高于DMA中断。

你想想看,如果ADC转换完成中断的优先级高于DMA传输完成中断,会发生什么?ADC每转换一次就触发中断,而DMA传输完成中断一直被推迟。等到DMA缓冲区满了,数据还没来得及处理就被新数据覆盖了。这就是典型的优先级设计失误。

4.8 本章知识结构图

下面这张图展示了中断系统的核心组成和它们之间的关系:

中断系统核心结构图 中断系统核心结构 外设中断源 ADC / TIM / USART NVIC 使能 / 挂起 / 优先级 中断向量表 ISR入口地址 中断请求 查表跳转 CPU 内核 跳转执行ISR 优先级分组 配置规则 核心流程:外设触发中断 → NVIC判断优先级 → 查向量表 → CPU跳转ISR 优先级分组决定抢占/响应位分配,NVIC根据优先级决定响应顺序

4.9 小结

中断系统是实时嵌入式系统的基石。NVIC负责调度,优先级决定响应顺序,中断向量表提供跳转目标。这三者配合好了,你的ADC中断和DMA传输才能协同工作,不丢数据、不卡顿。

我个人建议,在开始写代码之前,先把优先级分组和每个中断的优先级画出来。用一张表格列清楚:中断源、抢占优先级、响应优先级、ISR名称。这样写代码时思路清晰,调试时也容易定位问题。

最后提醒一句: 中断优先级不是越高越好。优先级太高会饿死其他中断,优先级太低会导致实时性不达标。找到那个平衡点,才是嵌入式工程师的真本事。