4. 中断系统入门:中断向量表、中断控制器、中断优先级与嵌套

好,咱们今天聊聊中断。说实话,中断是嵌入式系统的灵魂。没有中断,你的CPU就得像个傻子一样轮询,浪费大量算力。我刚开始做Zephyr开发时,对中断的理解也就停留在“打断当前任务”这个层面。直到有一次,一个外设的中断响应慢了,导致数据丢包,我才真正开始深入研究中断的底层机制。

说白了,中断就是硬件给CPU发的一个信号:“嘿,别忙了,先处理我的事!” 而CPU收到信号后,会暂停当前工作,跳到一个固定的地方去执行中断服务程序(ISR)。这个“固定的地方”,就是中断向量表。

4.1 中断向量表:CPU的“紧急通讯录”

中断向量表,你可以把它想象成一本通讯录。每个中断源都有一个唯一的编号,叫中断号。CPU根据这个中断号,去向量表里找到对应的ISR入口地址。

在ARM Cortex-M系列处理器上,向量表通常放在Flash的起始地址(0x00000000)。第一个字是栈顶指针,第二个字就是复位向量。后面的依次是各种异常和中断的入口地址。

Zephyr里怎么配置?

Zephyr在启动时,会把中断向量表从Flash复制到RAM,然后让CPU指向RAM中的向量表。这样做的好处是,你可以在运行时动态修改中断处理函数。我个人习惯在设备树里定义中断,然后通过IRQ_CONNECT宏来注册ISR。

// 一个典型的Zephyr中断连接示例
#define MY_UART_IRQ 42
#define MY_UART_PRIORITY 2

void my_uart_isr(const void *arg) {
    // 处理UART中断
    uint32_t status = uart_irq_update(DEVICE_DT_GET(DT_NODELABEL(uart0)));
    if (status & UART_IRQ_FLAG_RX) {
        // 读取数据
    }
}

// 在初始化代码中
IRQ_CONNECT(MY_UART_IRQ, MY_UART_PRIORITY, my_uart_isr, NULL, 0);
irq_enable(MY_UART_IRQ);
小技巧: 在调试时,你可以直接打印向量表的内容,看看ISR地址是否正确映射。我曾经因为链接脚本配置错误,导致向量表偏移,中断一触发就跑飞,查了整整一天。

4.2 中断控制器:中断的“交通警察”

中断控制器,在ARM Cortex-M上叫NVIC(Nested Vectored Interrupt Controller)。它的职责很简单:管理所有中断的使能、挂起、优先级,以及决定哪个中断先被CPU处理。

你想想看,如果同时来了10个中断,CPU该先处理谁?这就是中断控制器的工作。它会根据优先级,选出最高优先级的中断,然后告诉CPU:“先处理这个!”

NVIC的核心功能:

  • 使能/禁止中断: 通过irq_enable()irq_disable()控制。
  • 挂起/清除挂起: 当中断条件满足但CPU无法立即响应时,中断会被“挂起”。
  • 优先级管理: 每个中断都有独立的优先级寄存器。

在Zephyr里,你不需要直接操作NVIC寄存器。驱动层已经封装好了。但理解底层机制,对排查问题很有帮助。比如,你发现某个中断不响应,可以检查一下是不是被意外屏蔽了,或者挂起位没有清除。

注意: 千万不要在ISR里长时间关中断!我曾经见过一个同事,在中断处理函数里用了一个while循环等待硬件就绪,结果导致系统滴答时钟中断无法响应,整个系统卡死。记住,ISR要快进快出。

4.3 中断优先级:谁更重要?

中断优先级,说白了就是给中断排个队。优先级高的中断,可以打断优先级低的中断。在ARM Cortex-M上,优先级数值越小,优先级越高(0是最高优先级)。

Zephyr的优先级配置:

Zephyr支持两种优先级模式:

  • 固定优先级: 每个中断的优先级在编译时就确定,运行时不能改。
  • 动态优先级: 可以在运行时通过irq_set_priority()修改。

我个人建议,除非有特殊需求,否则用固定优先级就够了。动态优先级容易引入竞态条件,调试起来很头疼。

优先级值 含义 典型用途
0 最高优先级 系统时钟、紧急错误
1-3 高优先级 实时通信、DMA完成
4-7 中等优先级 外设数据收发
8-15 低优先级 按键扫描、非关键事件

嗯,这里要注意:优先级分配要留有余地。别把所有中断都设成最高优先级,否则低优先级中断永远得不到响应。我在一个项目中,把UART中断设成了优先级0,结果它频繁打断低优先级的定时器中断,导致定时器不准。

4.4 中断嵌套:中断里还有中断

中断嵌套,就是高优先级中断可以打断低优先级中断的执行。ARM Cortex-M的NVIC天生支持嵌套,不需要软件干预。

嵌套的流程:

  1. CPU正在执行低优先级ISR。
  2. 高优先级中断触发。
  3. NVIC判断新中断优先级更高,通知CPU。
  4. CPU自动保存当前ISR的上下文(压栈),然后跳转到高优先级ISR。
  5. 高优先级ISR执行完毕,CPU恢复上下文,继续执行低优先级ISR。

听起来很完美,对吧?但实际项目中,嵌套会带来两个问题:

  • 栈溢出: 每次嵌套都要压栈,如果嵌套层数太多,栈空间可能不够。Zephyr默认给每个线程分配了栈,但ISR用的是系统栈,要小心。
  • 优先级反转: 低优先级ISR持有某个资源,高优先级ISR也要用这个资源,结果高优先级被阻塞。这在中级篇会详细讲。
避坑指南: 我曾经在一个多中断项目中,没有控制嵌套深度,结果一个中断嵌套了5层,直接导致栈溢出,系统复位。后来我强制规定:所有ISR里不允许再开中断,也就是禁止嵌套。虽然牺牲了一点实时性,但系统稳定多了。

在Zephyr里,你可以通过irq_lock()irq_unlock()来控制嵌套。在ISR开头调用irq_lock(),可以临时屏蔽所有中断,防止嵌套。但注意,这会增加中断延迟。

void my_isr(const void *arg) {
    // 防止嵌套
    unsigned int key = irq_lock();
    
    // 处理中断
    // ...
    
    // 恢复中断状态
    irq_unlock(key);
}

好了,中断的入门知识就这些。记住三点:向量表是入口,NVIC是调度员,优先级决定了谁先谁后。下一节,我们会深入中断延迟的测量和优化,到时候你会看到,这些基础概念有多重要。