2. 中断源与中断向量:中断源分类(硬件/软件)、中断向量表(IVT)、中断向量地址映射

好,咱们接着聊。上一章我们把中断的“是什么”和“为什么”讲清楚了。这一章,咱们得深入一点,看看中断到底是从哪来的,以及CPU是怎么找到它的。

说白了,中断系统就像一个大楼里的应急响应中心。你得先知道“谁”能触发警报(中断源),然后还得知道“警报响了之后,应急手册放在哪”(中断向量表)。这两件事搞不明白,后面写中断服务程序就是瞎忙活。

2.1 中断源分类:硬件中断与软件中断

中断源,就是触发中断的源头。我习惯把它们分成两大类:硬件中断和软件中断。你想想看,一个来自外部世界,一个来自程序内部,本质完全不同。

2.1.1 硬件中断

硬件中断,是由芯片外部或内部的硬件模块产生的。它不受CPU控制,是“被动”发生的。

  • 外部硬件中断:来自芯片引脚。比如GPIO按键按下、外部传感器信号跳变。我在项目中遇到过,一个电机驱动芯片的故障引脚直接连到了DSP的GPIO上,一旦电机过流,这个引脚就拉低,触发紧急停机中断。嗯,这个设计救过我的板子。
  • 内部硬件中断:来自芯片内部外设。比如定时器溢出、ADC转换完成、SCI(串口)接收到数据。这些中断是DSP内部模块自己产生的,不需要外部连线。

核心区别:外部中断需要物理引脚,内部中断不需要。但它们的处理流程在CPU看来是一样的。

2.1.2 软件中断

软件中断,是程序主动“制造”出来的。它不是硬件触发的,而是通过执行一条特定的指令来触发。

  • 陷阱(Trap):故意触发的中断,用于系统调用。比如你在RTOS里调用一个API,底层可能就是通过软件中断切换到内核态。
  • 异常(Exception):程序出错时触发。比如除零错误、访问非法地址。这其实也算一种软件中断,但它是“被动”的。

我个人习惯把软件中断当作一种“受控的跳转”。你想让CPU立刻去执行一段特殊代码,但又不想用函数调用(因为函数调用会压栈很多寄存器),那就用软件中断。它只压栈关键上下文,效率更高。

我的经验:在调试阶段,我经常在关键位置插入一条软件中断指令。这样一旦程序跑到这里,就会停下来,我可以检查现场。这比设断点灵活多了。

2.2 中断向量表(IVT)

好,现在我们知道中断源有哪些了。但问题是:当某个中断发生时,CPU怎么知道该去执行哪段代码?

答案就是——中断向量表(Interrupt Vector Table,IVT)。

IVT,说白了就是一张“地址映射表”。它存放在内存的固定位置。这张表的每一项(称为“中断向量”)都存放着一个地址,这个地址就是对应中断服务程序(ISR)的入口地址。

我刚开始学DSP时,总觉得IVT很神秘。后来自己画了个图,一下就明白了。

中断向量表(IVT)结构示意图 中断向量号 中断向量(ISR入口地址) 0(复位) 0x3F FFC0(复位向量) 1(NMI) 0x3F FFC2 2(外部中断1) 0x3F FFC4 3(定时器0) 0x3F FFC6 4(ADC) 0x3F FFC8 ... CPU跳转 到该地址

你看,每个中断源都有一个固定的“中断向量号”。CPU拿到这个号后,直接去IVT里查表,找到对应的地址,然后跳过去执行。

注意:IVT的地址是固定的,由芯片厂商定义。你不能随便改。比如C2000系列DSP的复位向量通常位于0x3F FFC0。我曾经见过有人把IVT地址搞错了,结果一上电就跑飞了。

2.3 中断向量地址映射

中断向量地址映射,就是“中断向量号”到“IVT表项地址”的换算关系。这个关系通常很简单:

IVT表项地址 = IVT基地址 + (中断向量号 × 每个表项的大小)

举个例子。假设IVT基地址是0x3F FFC0,每个表项占2个字节(16位地址),那么:

  • 中断向量号0(复位):地址 = 0x3F FFC0 + 0×2 = 0x3F FFC0
  • 中断向量号1(NMI):地址 = 0x3F FFC0 + 1×2 = 0x3F FFC2
  • 中断向量号2(外部中断1):地址 = 0x3F FFC0 + 2×2 = 0x3F FFC4

嗯,这里要注意。不同DSP的IVT表项大小不一样。有些是2字节,有些是4字节(32位地址)。你得查数据手册确认。

核心要点:中断向量地址映射是硬件自动完成的。你只需要在IVT的对应位置填上你的ISR地址就行。CPU会帮你算好该去哪找。

2.4 实际配置示例(以TMS320F28335为例)

光说不练假把式。咱们看个实际例子。在TI的C2000系列DSP上,配置中断向量通常用PIE(外设中断扩展)模块。但底层原理是一样的。

// 定义中断服务函数
interrupt void myTimer0ISR(void)
{
    // 处理定时器中断
    PieCtrlRegs.PIEACK.all = PIEACK_GROUP1;  // 应答中断
}

// 主函数中配置中断向量
void main(void)
{
    // 初始化PIE控制寄存器
    InitPieCtrl();
    
    // 禁止所有中断
    DINT;
    
    // 初始化PIE向量表
    InitPieVectTable();
    
    // 将ISR地址写入PIE向量表
    // 定时器0的中断向量在PIE中的位置是1.7(组1,第7个)
    EALLOW;  // 允许访问受保护的寄存器
    PieVectTable.TINT0 = &myTimer0ISR;
    EDIS;    // 禁止访问受保护的寄存器
    
    // 使能中断
    PieCtrlRegs.PIEIER1.bit.INTx7 = 1;  // 使能定时器0中断
    IER |= M_INT1;                       // 使能CPU中断1
    EINT;                                // 全局使能中断
    
    while(1)
    {
        // 主循环
    }
}

你看,核心就是那一行 PieVectTable.TINT0 = &myTimer0ISR;。这行代码把ISR的地址写入了PIE向量表的对应位置。当定时器0中断发生时,CPU就会自动跳转到 myTimer0ISR 去执行。

避坑指南:我曾经在配置中断向量时,忘记调用 EALLOWEDIS。结果写向量表的操作被硬件忽略了,中断一直触发不了。查了两天才发现是这个问题。所以,操作受保护的寄存器时,一定要成对使用 EALLOWEDIS

2.5 总结

这一章我们讲了三个核心概念:

  1. 中断源:分硬件中断和软件中断。硬件中断来自外部引脚或内部外设,软件中断是程序主动触发的。
  2. 中断向量表(IVT):一张存放在固定内存地址的表格,每个表项存放一个ISR的入口地址。
  3. 中断向量地址映射:中断向量号到IVT表项地址的换算关系,由硬件自动完成。

搞懂了这些,你就掌握了中断系统的“骨架”。下一章,我们会往这个骨架上填“血肉”——也就是中断的优先级和嵌套。嗯,那才是真正考验设计能力的地方。


公众号:蓝海资料掘金营,微信deep3321