4. NVIC中断系统与低层驱动

在SAM系列(如SAMD21/SAMD51)这类基于Cortex-M0+/M4F内核的MCU上,外设驱动能否稳定响应,关键往往不在外设寄存器本身,而在NVIC这一层。NVIC(Nested Vectored Interrupt Controller)负责把外设产生的中断请求,按优先级仲裁后交给CPU执行对应的中断服务程序(ISR)。理解NVIC,实际上就是理解“谁先被响应、响应后跳到哪、能不能被打断、什么时候返回”这四个问题。

向量表:中断入口的地址映射

Cortex-M的向量表是一张存放在Flash起始处的地址数组。复位后,CPU从地址0x00000000读取初始MSP,从0x00000004读取Reset_Handler地址;随后每个异常源占4字节,依次排列。SAM器件通常把向量表放在0x00000000,但也可以通过VTOR寄存器重定位到SRAM或其他Flash区域,这在Bootloader与应用程序分离时非常常见。

向量表的前16项是内核异常,包括NMI、HardFault、SVCall、PendSV、SysTick等;从第16项开始是外设中断,例如SERCOM0_IRQn、TC3_IRQn、EIC_IRQn。每个外设中断号在头文件中以IRQn_Type枚举给出,向量表项中填写的正是对应ISR的函数名。若某个中断未使用,通常填入Default_Handler,一旦误触发就会停在死循环中,便于定位。

Cortex-M NVIC 向量表与中断响应路径 向量表(Flash 起始) 0x00 初始 MSP 0x04 Reset_Handler 0x08 NMI_Handler 0x0C HardFault_Handler 0x2C SVCall_Handler 0x38 PendSV_Handler 0x3C SysTick_Handler 0x40 SERCOM0_Handler 0x44 SERCOM1_Handler NVIC 优先级仲裁 嵌套控制 使能/挂起 活动状态 VTOR 重定位 Cortex-M 内核 取向量 → 压栈 执行 ISR 尾链 / 嵌套 出栈 → 返回 PRIMASK / BASEPRI 外设中断源 SERCOM / TC / EIC / ADC / DMA ...

优先级分组:抢占与子优先级的划分

Cortex-M的每个中断都有一个8位优先级字段,但SAM器件通常只实现高2位或高4位。以SAMD21(Cortex-M0+)为例,只实现2位,可配置4个优先级;SAMD51(Cortex-M4F)实现4位,可配置16个优先级。数值越小,优先级越高。

在M4F上,优先级被分为“抢占优先级”和“子优先级”两部分,由AIRCR.PRIGROUP决定划分点。例如设置NVIC_SetPriorityGrouping(3),则高3位为抢占优先级、低1位为子优先级。抢占优先级决定能否嵌套:高抢占优先级的中断可以打断正在执行的低抢占优先级ISR;子优先级只在多个中断同时挂起时决定谁先执行,不能形成嵌套。若两个中断抢占优先级相同,后到的必须等待前一个ISR返回,即使子优先级更高。

实际配置时,常用NVIC_SetPriority(IRQn, priority)直接写入优先级值。需要注意:在M0+上不存在分组概念,所有位都是抢占优先级,因此不要照搬M4F的分组代码。

中断服务程序编写要点

ISR不是普通函数,它由硬件自动调用,没有参数、没有返回值。编写时应遵循几条硬性约束:第一,执行时间尽量短,只做标志清除、数据搬运、状态置位,把耗时处理放到主循环或任务中;第二,访问共享变量时使用volatile,防止编译器优化导致读取不到更新;第三,若主循环与ISR共享多字节数据,应使用临界区或原子操作保护;第四,必须清除外设中断标志,否则退出后会立即再次进入。

volatile uint8_t rx_flag = 0;
volatile uint8_t rx_data;

void SERCOM0_Handler(void)
{
    if (SERCOM0->USART.INTFLAG.bit.RXC) {
        rx_data = SERCOM0->USART.DATA.bit.DATA;
        rx_flag = 1;
        /* 标志由读DATA自动清除,无需额外写 */
    }
}

上面这段代码展示了SAM USART接收中断的典型写法:读取INTFLAG判断来源,读取DATA取走数据并自动清标志,最后置位软件标志通知主循环。注意不要在ISR里做printf或延时,那会显著拉长中断延迟。

中断延迟与嵌套

中断延迟指从中断请求有效到ISR第一条指令执行的时间。Cortex-M的硬件响应本身只需约12个周期(压栈8个寄存器+取向量),但实际延迟还包括:当前指令是否可中断、是否处于更高优先级ISR中、是否被PRIMASK屏蔽、Flash等待周期等。在48MHz的SAMD21上,若Flash等待周期为1,典型响应时间约在1μs量级。

嵌套发生在高抢占优先级中断打断低抢占优先级ISR时。硬件会自动把被打断的现场压栈,返回时自动恢复。需要特别注意的是“尾链”(tail-chaining):当一个ISR结束、另一个同优先级或更低优先级中断已挂起时,CPU不会先出栈再压栈,而是直接跳到下一个ISR,节省约6个周期。这是Cortex-M提升中断吞吐的重要机制。

若发现中断响应忽快忽慢,优先检查是否在ISR中调用了阻塞函数、是否长时间关闭全局中断、以及是否有高优先级中断频繁抢占。

外设中断注册与回调机制

裸机开发时,ISR函数名必须与启动文件中的向量表符号一致,否则链接器会报重复定义或使用默认弱符号。为了让驱动层与业务层解耦,通常采用“注册+回调”的方式:驱动层提供注册接口,保存用户传入的函数指针;ISR中只做最小处理,然后调用该回调。

typedef void (*uart_rx_cb_t)(uint8_t byte);
static uart_rx_cb_t s_rx_cb = 0;

void uart_register_rx_cb(uart_rx_cb_t cb) { s_rx_cb = cb; }

void SERCOM0_Handler(void)
{
    if (SERCOM0->USART.INTFLAG.bit.RXC) {
        uint8_t b = SERCOM0->USART.DATA.bit.DATA;
        if (s_rx_cb) s_rx_cb(b);
    }
}

这种结构让上层可以注册自己的处理函数,而无需修改驱动源码。注册时要注意:回调函数本身运行在中断上下文,不能调用可能阻塞的API;若回调需要传递更多上下文,可把函数指针与void*参数一起保存。对于多个外设共用同一IRQ的情况(如SAMD51中多个定时器共享TC3_IRQn),ISR内还需读取各外设的INTFLAG判断来源,再分派到对应回调。

配置检查清单

检查项典型值/操作判断依据
向量表位置VTOR = 0x00000000 或重定位复位后PC进入Reset_Handler
优先级分组NVIC_SetPriorityGrouping(3)抢占/子优先级划分符合预期
中断使能NVIC_EnableIRQ(SERCOM0_IRQn)ISR能被触发
外设中断源INTENSET对应位置1INTFLAG能置位
标志清除读DATA或写INTFLAG退出ISR后不重复进入
回调注册s_rx_cb非空业务函数被调用

把这六项逐一确认后,绝大多数“中断不进”“进了出不来”“进了多次”的问题都能定位。NVIC层看似简单,但优先级分组、标志清除和回调上下文这三处,是SAM外设驱动调试中最容易出错的环节。