
做嵌入式这几年我接触的芯片基本逃不开这三类RISC-V、ARM、x86。每次带新人入门大家最爱问的一个问题就是它们的中断流程到底差在哪直接丢三份手册太劝退所以我习惯先讲一条主线再往三个架构里套。这篇文章就把这条主线理清楚从向量表、特权级切换、现场保存到中断结束一条线看懂三种架构。1. 中断主线先抓住五步再谈架构差异1.1 中断的本质从“正常执行”切换为“突发事件处理”中断和异常说白了就是CPU正在好好执行指令突然被一件事打断需要转去执行另一段更紧急的代码处理完再回到原来被打断的地方继续跑。区别只在“打断”的来源外部硬件发来的是中断指令本身触发的是异常。但在大多数现代CPU里处理机制是高度统一的所以RISC-V干脆管它们都叫trap。理解中断前先抓五步就能把骨架立起来触发外设拉高中断线或CPU内部检测到异常。仲裁与路由多个中断源同时到来时由中断控制器决定谁先到CPU。向量定位CPU根据中断类型或中断号跳转到对应的处理函数入口。处理执行中断服务程序完成具体业务。结束与恢复通知中断控制器处理完毕CPU恢复保存的现场回到原任务继续执行。无论哪家架构本质都是在走这五步。差别在于向量表放在哪、跳转时硬件帮你做了多少、现场保存在哪、结束中断时谁来写确认寄存器。把这几个点对比着看复杂度就降下来了。1.2 影响中断流程的三个关键变量先看硬件“帮你保存”了什么。x86每次进中断都会自动把EFLAGS、CS、RIP压栈异常还会压入错误码ARM的Cortex-M会硬件自动压一堆寄存器但ARM64体系的A/R核、RISC-V则大部分靠软件自行保存通用寄存器。这个差异决定了你在写中断入口汇编代码时的工作量。再看特权级怎么切换。x86有Ring0到Ring3ARM有EL0到EL3RISC-V有U、S、M三种模式。中断来了CPU通常从低特权跳到高特权这个跳转过程决定了谁能被中断打断、谁的资源不能碰。最后是向量表的组织方式。x86用门描述符管理0到255的向量号ARM64用异常等级加异常类型组合查表RISC-V更像是拿“异常原因”直接做索引。理解了这三点三种架构的页面读起来就不会再一团乱麻。2. x86一张IDT管天下向量号写得明明白白2.1 从IVT到IDT实模式到保护模式的变化x86在8086时代用固定地址的IVT中断向量表表固定在内存最低地址每个表项4字节直接存段地址和偏移。从80286进入保护模式后改用IDT也就是中断描述符表位置由IDTR寄存器指向不再固定在地址0。IDT里的每个表项叫门描述符常见的有中断门、陷阱门老系统里还有任务门。中断门除了能指定处理函数地址还有一个属性进入中断门时CPU会自动清IF标志也就是自动关中断防止同一中断在服务期间再次触发。陷阱门则不会重置IF。这些细节别小看关中断的时机和范围直接决定了你写驱动时的嵌套策略。2.2 一次完整的中断进入CPU替你干了哪些活以现代x86的长模式为例假设用户态程序运行在Ring3外设发来中断。CPU会这样走流程根据中断向量号查找IDT找到对应门描述符并做一系列特权级检查。确认需要切换特权级后从TSS中取出内核栈指针RSP0把原来的栈指针切换过去。依次压入旧的SS、RSP、RFLAGS、CS、RIP。如果异常有错误码还会压入错误码。跳转到门描述符中指定的处理函数处理器开始执行ISR。也就是说x86在硬件层面帮你完成了切换栈和压入关键上下文的工作。这个设计比RISC-V省事不少代价是IDT的层级结构、权限检查、TSS结构复杂理解门槛高。2.3 外部中断如何走到CPULAPIC和中断向量号x86外部设备的中断不是直接拉一根中断脚到CPU内核就完事的而是要先经过中断控制器。现代x86把中断控制器拆成两部分IOAPIC收集外设的中断本地APIC挂在CPU内部。IOAPIC把中断映射成一个中断向量号发给本地APIC本地APIC再依据该向量查找IDT。所以x86的世界观里中断向量号是一等公民。外部中断一般占用从0x20往后的一段向量你写驱动时注册的是“IRQ号”而CPU真正看到的是经过映射后的向量号。IRQ号和向量号之间没有固定的算术关系具体看系统怎么配置APIC这也是刚接触x86的人比较容易绕晕的地方。3. ARM异常等级与向量表之间的配合3.1 别把ARM当成一种A/R和M走的是完全不同路子说ARM中断前一定要先分清方向否则会越聊越乱。ARMv7-A/R、ARMv8-A使用的AArch32/AArch64走的是“异常等级 向量表”那套而Cortex-M系列走的是内置NVIC中断控制器配合向量表硬件自动压栈的机制。本文主要说A/R核因为它在嵌入式Linux、服务器、车规芯片里更常见。但我也提一句M核在Cortex-M上中断来临时硬件会自动把R0到R3、R12、LR、PC、xPSR等寄存器压到当前栈NVIC负责优先级仲裁和尾链优化开发者几乎不用手写现场保存汇编。这套思路对新手友好得多但和下面要说的ARM64模式相反。3.2 ARM64的异常向量表按“身在何级何种异常”找入口AArch64的中断入口不叫IDT也不叫中断向量而是叫“异常向量表”。每级异常等级都有自己的向量表基地址放在VBAR_EL1、VBAR_EL2、VBAR_EL3中。每个向量表内部不是一长串顺序排列的中断号而是按“栈指针选择 异常等级来源 异常类型”分成若干固定偏移的入口。向量表里大致会看到同步异常、IRQ、FIQ、SError几大类。发生异常时CPU根据当前异常等级和被打断的等级跳转到VBAR_ELx加上固定偏移量的地址。每个入口之间通常间隔固定大小有的留32条指令的空间有的需要你主动跳到更远处。我调试ARM64平台上经常用到的几个系统寄存器VBAR_EL1指向EL1的异常向量表基地址。ELR_EL1被打断位置的返回地址。SPSR_EL1被打断前的程序状态。ESR_EL1异常原因寄存器用于区分同步异常、IRQ、FIQ。DAIF中断屏蔽位。3.3 ARM多级安全与中断路由异常等级决定处理归属ARM64最大的特点是把特权级抽象成EL0到EL3四个异常等级。操作系统内核跑在EL1Hypervisor跑在EL2固件和安全监控跑在EL3。外部中断来了之后并非一定跳到EL1而是由异常路由配置决定跳到哪一级。如果在虚拟化场景下把中断配成直通虚拟机IRQ可能被路由到EL2再由虚拟机管理程序决定怎么处理。普通裸机系统则通常让中断上到EL1。这个路由配置是通过HCR_EL2、SCR_EL3等寄存器里的开关位来控制的。这就是为什么ARM架构下说“中断处理在哪一层运行”比单纯说“进入中断函数”更重要毕竟固件、内核、虚拟机可能都在等同一根中断线。发生IRQ后CPU并不会自动帮你把一堆通用寄存器压栈。它只做三件事把返回地址写入ELR_ELx、把原状态存入SPSR_ELx、设定ESR_ELx的原因值。接下来靠内核异常入口汇编指令把通用寄存器保存到栈上。这也是很多人看ARM64的异常入口汇编时发现有一大段保存指令的原因。如果需要扩展外部中断源ARM一般配套GIC。GIC会向CPU核发送IRQ或FIQ信号CPU跳转到异常向量然后软件读取GICC_IAR寄存器通过这个寄存器拿到中断号并隐含完成中断确认动作。中断处理完软件还要写GICC_EOIR寄存器告诉GIC可以清掉这个中断了。读IAR和写EOIR这两个操作对很多刚从x86平台转过来的人特别容易遗漏。4. RISC-V极简CSR把现场交给软件自己管4.1 只用几个CSR就把trap讲清楚了RISC-V可以看作把x86和ARM的复杂度拨开后的极简派。它没有复杂的门描述符结构也没有必须分四级的异常等级体系。RISC-V规定的基础特权级有三种U用户、S系统、M机器模式。如果芯片只用嵌入式场景通常U和M两级就够了Linux则是U、S、M三态配合。发生trap时核心寄存器就这么几个mtvec/stvec存放trap入口地址即异常向量表基地址也可以配成向量模式。mcause/scause记录trap原因最高位表示是不是外部中断低位置存放原因编号。mepc/sepc记录被打断的那条指令地址。mstatus/sstatus记录处理器状态其中MIE/SIE是全局中断开关MPIE/SPIE用来保存进入trap前的中断使能状态。mtval/stval记录导致异常的附加信息比如缺页地址。这套CSR设计最直观的好处是你只要看懂这几个寄存器整个中断流程就揭开了一半。4.2 trap进入与退出硬件只做最小动作以机器模式处理trap为例CPU收到中断后会自动完成把当前PC写入mepc。把trap原因写入mcause。保存当前中断使能状态到MPIE然后清除MIE关掉M模式中断。处理器切换到M模式跳到mtvec指向的地址。注意RISC-V硬件不会像x86那样帮你把返回地址、状态字压栈更不会像Cortex-M那样自动保存一堆通用寄存器。通用寄存器的保存完全由软件完成。进入trap后你写的第一段汇编通常就是把通用寄存器保存到内存或栈里退出前再从栈里恢复出来。这种极简做法让RISC-V的实现开销很低但也把复杂度留给了编译器、操作系统和BSP开发者。你在看RISC-V的Linux内核代码时会看到类似trap_entry这样的汇编宏作用就是把sp切换到内核栈并保存现场。以U模式到S模式的切换为例Linux会通过CSR操作将sscratch与实际sp对调再把必要的寄存器压栈。想弄懂RISC-V中断最好先熟悉这些汇编宏中保存与恢复的顺序。4.3 外部中断源怎么确认靠可编程中断控制器RISC-V在指令集层面只定义了“中断来了”却没有规定外部中断源号如何编码。因此具体外设怎么区分由中断控制器实现决定常见的有PLIC、APLIC以及CLINT等。PLIC收集来自多个外设的中断请求进行优先级仲裁后向某个CPU核发送一条外部中断信号。CPU核看到的是“外部中断”这个类别其mcause对应的是机器外部中断或间接反映为sstatus中的S外部中断。随后软件必须去访问中断控制器的claim寄存器拿到真正的中断源编号相当于告诉控制器“我要处理这个中断”。处理完毕后还要写complete寄存器表示“这个中断我已经结束了”。对比一下就看得出x86直接把每个中断做成一个向量号作为IDT索引ARM需要读GIC中断号RISC-V需要访问外部控制器claim寄存器。三个架构都把“谁触发的中断”这个问题剥离开只是完成方式完全不同。5. 三架构对照现场、向量、结束三张牌各有不同5.1 一张表看懂硬自动保存的差异我常建议手头备一张自己整理的对照表尤其是面试或移植驱动时临时翻一翻很管用。以下是我平时常用的一套维度对比维度x86ARM64 GICRISC-V PLIC入口表IDT按中断向量号找门描述符VBAR_ELx指向的异常向量表按异常类型找入口stvec指向的trap入口可选direct或vectored模式一般寄存器如何保存硬件压栈关键上下文通用寄存器靠软件通用寄存器靠软件保存通用寄存器靠软件保存返回地址/状态保存返回地址和状态由硬件压栈ELR_ELx与SPSR_ELx保存mepc/sepc与mstatus/sstatus保存特权级Ring0到Ring3EL0到EL3U、S、M三级外部中断源获取中断向量号由IDT管理器直接映射读GICC_IAR获取中断号读中断控制器的claim寄存器结束后通知LAPIC发送EOI写GICC_EOIR写PLIC completion寄存器这张表只作为纲领不能覆盖所有实现细节因为哪怕同样是ARM64不同SoC的GIC版本不同中断处理都有细微差异。但抓这几点去看代码基本方向不会跑偏。5.2 中断“结束”动作是最容易写错的一环很多人调试中断卡死问题不在处理逻辑而在结束动作没做对。x86平台处理完外部中断后软件要告诉本地APIC中断已经处理完毕否则APIC不会清除当前正在服务的中断状态后续同级别中断无法继续上报。ARM平台是写GIC的EOIR寄存器完成EOI操作而且如果用了优先权还要注意中断优先级组的配置写错EOI可能会关掉不该关的优先级组。RISC-V平台则要回写PLIC的complete寄存器把当前中断源标记为完成。这里有个很容易犯的错把“中断号确认”和“中断结束”混为一谈。读ARM的IAR、读PLIC的claim相当于告诉中断控制器“我已经受理了”但不等同于“已经干完了”。干完活后必须再写EOIR或者complete中断控制器才认为这个中断彻底结束。如果只确认不完成表面上看第一个中断能进来但后续同优先级中断很可能被压住这时候排查方向基本都在中断控制器状态。5.3 为什么会有人觉得“x86中断更像异常ARM中断更像消息”这个感受其实挺有意思。x86的IDT把外部中断和CPU异常都放在同一张表而且CPU依靠一个向量号就能跳转驱动开发者的心智模型是“中断号就是入口编号”和异常处理几乎平行。ARM的向量表只是把IRQ/FIQ领进门具体是哪个外设必须从GIC拿IAR所以更像“来了一条消息我得查一下是谁发的”。RISC-V也一样trap里只标记了中断类别具体来源要问PLIC。三种模型的差别并不代表孰优孰劣更多是历史包袱和应用场景决定的。x86为了兼容积累了几十年的软件生态接口复杂但稳定ARM一开始面向移动和嵌入式把安全隔离做成了异常等级RISC-V从零出发核心指令集做得很干净把复杂功能留给外置控制器和软件。理解这个演进逻辑你就能明白为什么RISC-V常被称为“用来做控制器的CPU”而x86更像“一台什么中断都能接的通用机”。6. 实践者才关心的几个坑和排查习惯6.1 在写中断入口之前先把五问答清楚一个人说他会写中断通常体现在翻手册和看反汇编的能力。我个人写底层中断入口或移植操作系统时会先强制自己回答五个问题中断发生在哪个特权级目标处理器会跳到哪一级向量表地址是谁提供的启动代码是否已经设置好了进入中断后硬件关不关中断如果不关嵌套中断怎么保护现场现场保存是在硬件自动完成还是需要软件汇编处理中断结束该写哪个寄存器确认后会不会误清其他状态这五个问题如果都能当场答出八成不会出现“中断一开就死机”的尴尬。如果答不上来别急着写代码先把对应架构的启动代码和中断控制器驱动读明白。6.2 调试中断卡死时我最常用的三种策略第一是看返回地址。x86上发生异常时RIP指向的可能是触发异常的指令也可能是返回点要看异常类型。ARM上直接读ELR_ELxRISC-V上读mepc或sepc。如果发现返回地址落在奇怪的地址比如零地址或全是0xFFFF多半是现场保存顺序出了问题而不是中断逻辑本身。第二是查向量表和入口地址有没有对齐。ARM64要求异常向量表按一定边界对齐RISC-V也要求stvec按至少4字节对齐如果你在运行时动态改向量表得注意对齐属性。某些平台在链接脚本里没有把向量段放在正确位置中断一跳就跳到空地址这种问题用示波器看中断线已经拉高但CPU根本没动作基本就是向量表没配对。第三是打印中断控制器状态。GIC、APIC、PLIC都有寄存器可以查当前pending中断和当前servicing的中断状态。如果Pend一直拉高但CPU不进中断优先查中断是否被mask或者目标CPU使能位没开。如果中断能进一次但之后再无响应优先查结束寄存器有没有写再查优先级配置。6.3 最后说两条经验调试RISC-V时多利用GDB的CSR查看功能直接读mcause和mepc比反复看日志快得多。比如mi/v $mcause 查看中断原因。mi/v $mepc 查看被打断的位置。mi/v $stvec 查看当前S模式trap入口。ARM平台上用调试器查看VBAR_EL1时经常发现它被内核改写成了某个动态地址。这时你要知道那不是bug而是内核在启动初期用临时向量表跑了一段时间后又切换到了真正的内核向量表。如果断点打在旧地址上自然永远不触发。做这套工作久了我的切身体会是与其把所有中断细节死记硬背不如记死一条主线、五个问题再按架构把“向量表、特权级、现场保存、结束通知”这四个锚点填进去。每次拿到新SoC哪怕厂商文档写得再乱按这条思路也能很快定位到底层中断该怎么上手。