嵌入式中断向量表与ISR执行全流程揭秘
说人话、重实战、讲干货我是程序员古德你的专属软考顾问现阶段已经更新到第482篇软考原创文章, 这一篇是我写的, 并且, 还能够提供软考报名疑问解答、备考计划梳理、论文批改审阅、学习要点指导、应试困惑解析等多样服务。只要是我亲自验证过的方法, 只要是我亲自踩过的备考坑, 我都会坦诚相告, 若是有需求的小伙伴, 能够到后台私信联系我。嵌入式中断向量表与ISR执行全流程揭秘一、中断的本质CPU如何感知外部世界中断, 是嵌入式系统里极为核心的异步事件响应机制, 按硬件层面来界定, 中断乃是指 CPU 在正执行当前程序之际, 由内部也好, 外部也罢, 所触发的一种信号, 此信号会强迫 CPU 暂搁当前任务, 进而转去执行一段预先就定义好的中断服务程序, 等处理完之后再返回到原程序接着执行, 这个进程并非是软件轮询, 而是硬件级别的流程劫持。要是想弄明白理解中断里的那些事儿, 首先就得把中断跟异常区分开来。在ARM体系结构这个范畴里, 广义的中断涵盖了狭义硬件中断以及处理器异常这两大种类。硬件中断是由外部设备给发出来的, 是经过中断控制器才送达到CPU那边的中断请求引脚这儿的, 典型来自像定时器有溢出问题、串口数据到达了、 GPIO电平发生了变化、DMA传输大功告成了等这些情况。处理器异常呢, 是CPU自己在执行那条指令的进程当中检测出来的异常状况, 就比如出现未定义那指令了、在数据方面出现中止了、预取出现了中止、还有软件中断之类的情况。这两者存在的共性情形在于两者中每一个都会致使处理器进入到异常模式, 还会切换到向量表当中对应的那入口处地址, 而二者之间的区别之处在于引发触发的源头不一样 -- 中断其来源出自片外, 另外异常得来源是出自片内。程序员古德300篇原创中断的相对概念为轮询, 在轮询方式里, CPU会周期性地逐个检查各外设的状态寄存器去判断有无待处理事件, 这种方式实现起来挺简单, 然而却存在显著的效率瓶颈, 即CPU大量时间耗费在了虽逐一圈, 但无人响应的空转之上, 实时性同样难以得到保障, 在最坏状况时对应的响应延迟能够达到一个完整的轮询周期, 而中断机制达成了按需响应, 也, 即是外设有事件时可主动去通知CPU, CPU仅在必要之际才介入进行处理。这样一种, 由“主动问”转变至“被动听”的, 式样变换方式, 乃是可嵌入系统达成适时回应需求的, 硬件方面的根基所在。中断通路里, 中断控制器有着极为重大的用于路由以及管理的职责, 在ARM -M这一系列当中, 嵌套向量中断控制器也就是NVIC它整合了中断让能的控制, 对优先级的一种合理配置, 向量图表产生偏移的状况, 中断被搁至一旁以及活跃状态进行跟踪等一系列功能, NVIC它最多能支持240个外部中断发生的途径, 每一个这样的通道能够独立的对优先级予以设定。在更为复杂的ARM -A系列情况下, 普遍使用的中断控制器即GIC执行了中断分发, 把优先级进行仲裁的任务, 多核间的中断是怎么样进行路由的等活儿, 它还可以扶持中断分组, 通过软件生出的中断还有一些具备虚拟性的中断这么一些厉害的特性。控制器层是中断机制从能用到好用的关键一环。二、中断向量表中断响应的路由地图H3嵌入示例中断向量表的结构与寻址逻辑本质地说, 中断向量表是一个数组, 此数组用于存放中断服务程序入口地址。每个中断源于向量表中占有一个固定位置, 当该特定中断获响应之当时, 处理器硬件会在向量表里的对应偏移位置自动读取入口地址, 随后进行跳转致使执行。整个这一过程完全由硬件予以完成, 并不需要软件进行干预, 所以响应速度极其快速。在ARM -M3 、M4处理器里头, 向量表的前面的十六个条目, 被系统异常所占着, 从地址开始逐个依次呈现为: 首先是初始堆指针值哦此对应地址是以0x00为其偏移量哒、接着过来就是复位向量喽处于0x04的偏移位置呢、还有NMI这种不可被屏蔽阻止的中断哦它是与0x08的偏移对应着呀大、再有属于硬故障哒位于0x0C的偏移之处呢、跟着是内存管理方面的其偏移地址是0x10哒、然后还有总线故障的情况哦处于0x14偏移的地方那、用法故障方面啦它的偏移量是0x18哒, 另外, 与此同时的, 以及、、那些等等类型的系统服务向量。从0x40的这样一个偏移起始点开始之后部分, 是外部的中中断方面的向量哦依次逐个有序对应着IRQ0一直到IRQn啦这里面n的具体数值情况, 是视乎、取决于芯片厂商自主进行的实现状况罢了。代表向量的表的基础地址不是那种固定住了就绝对不会改变的, -M处理器给出了向量表偏移的寄存器VTOR, 它准许在运行的时候把向量表重新定位到SRAM或者别的存储区域。这样的一个特性对于引导加载程序来说是非常关键重要的: 在运行的时候把向量表指向自身的中断向量, 在完成应用代码的跳转之后, 再去把VTOR修改成应用程序的向量表地址, 以此来达成中断向量的没有缝隙地切换。与此同时, 处于SRAM里的向量表还能够支持在运行时动态地去把ISR入口做修改, 这就为在线升级以及动态补丁提供了一种潜在可能性。-A系列的两级向量表设计与之不同是, -M的单级向量表并非如此, -A系列所采用的乃是两级向量表结构, 第一级具体而言指的是异常向量表, 其固定于八个地址之上或者也可以说情况有所区别, 此取决于SCTLR.V位, 每一个异常类型皆对应着一条跳转指令, 大体且通常是LDR PC。PC, #指令为或B, 第二级是真正处理函数入口地址表的所在处, LDR指令从离当前PC较近之处的地址把目标入口地址加载到PC, 借此来达成间接跳转, 这般设计用心是在限定的八条指令空间里实现最大程度灵活性, 也就是更改二级表内地址便能对处理函数进行重定向。两级向量表的另外一个需要考虑的方面是虚拟内存支持, -A系列运行复杂的操作系统这种情况下, 向量表所所在的处于高位的地址一般映射到内核空间, 这样一种设计既保障了异常入口的所处位置固定不变, 又便利操作系统对于异常处理流程的接手。在Linux内核启动的时候, 它会把异常向量表复制到高高在上的虚拟地址, 以此保证用户态的进程没有法子直接去访问以及篡改异常处理的逻辑。三、中断响应全流程硬件自动执行的微操作序列中断请求信号被当中断控制器送达CPU之后, 处理器会在然后每条指令执行完毕之后去检查中断信号, 要是中断被已经使能并且优先级高于当前正在执行的中断这个倘若是有的话, CPU就将会进入中断响应序列, 这一序列是由硬件自动执行的, 程序员是没办法进行干预的, 理解它的细节对于排查针中断相关的疑难杂症是至关重要的。首先, 第一步是进行上下文保存, -M处理器会自动把八个寄存器压入当前栈, 这八个寄存器分别是xPSR、返回地址PC、链接寄存器LR、R12、R3、R2、R1、R0, 总共占32字节。该压栈的顺序是从高地址到低地址, 其中最后入栈的是xPSR。不得不说, 这个自动压栈动作完全由纯硬件完成, 并不需要任何汇编指令, 这是-M相较于传统ARM核在中断延迟方面的重大优化。而对于-A系列来说, 程序员在ISR入口是需要手动保存关键寄存器的, 不然中断返回的时候上下文可能就会被破坏。第二步是进行向量表寻址, 硬件依照中断号来计算向量表偏移, 偏移量是中断号乘4, 因为每个向量占用4字节, CPU 会从向量表基地址加上偏移量的那个位置去读取32位值, 此值便是 ISR 的入口地址, 对于 -M, 倘若该中断的优先级分组里的抢占优先级比当前中断高, 那么还得更新当前运行优先级寄存器。第三步, 是要更新处理器状态寄存器, 以此来标记当前进入中断上下文。其中, -M会把中断号写入到xPSR的异常编号字段, 还会将LR设置成特殊值, 像表示从主栈返回线程模式使用MSP这样的特殊值, 目的是方便中断返回时硬件能够识别返回目标。而ARM的LR特殊编码, 是一种设计得很精巧的硬件设计, 它是通过LR值的低位比特编码返回模式, 进而省去了额外的状态寄存器。第四步, PC被当作ISR的入口地址来加载, CPU接着开始执行中断服务程序。从硬件接收到中断信号起, 一直到ISR第一条指令开始执行的这个时间段, 就被称作中断延迟。-M3的典型中断延迟是12个时钟周期, 这里面涵盖了寄存器压栈、向量表读取以及流水线填充。要是当前正在执行的指令属于慢速指令像多加载/存储指令, 那么中断延迟有可能会更长。硬件自动化, 同样用于中断返回。当ISR执行完毕, 遇到从LR加载PC的操作, 像BX LR或POP PC这类, 硬件识别LR中的特殊编码, 自动执行出栈操作, 恢复R0 - R3、R12、LR、PC和xPSR, CPU回到被打断的线程继续执行。在这个过程里, 若有更高优先级的中断被挂起, 硬件也会在返回时自动进行尾链tail - 优化, 跳过不必要的出栈再次入栈动作, 直接切换到下一个ISR。四、中断优先级与嵌套实时性的命脉优先级位宽与分组策略-M处理器的优先级配置可不是单纯数字大小的比较, 首先, 芯片厂商生产时配置了实际的优先级位宽, 一般是3位8级到8位256级, 是借助优先级寄存器来控制分组的。优先级分组把优先级位宽划分成两部分, 分别是抢占优先级字段和子优先级字段。抢占优先级能决定一个中断可不可以打断另一个正在运行的ISR, 子优先级则是在多个悬而未决的同级中断里决定先处理哪一个。以3位优先级位宽作为例子来讲, 假设设置分组为5这种情况这里的5表明, 抢占优先级占据3位, 子优先级占据0位, 那么就会存在8个抢占等级, 在同一抢占优先级下的中断之间是不存在嵌套现象的要是设置分组为4也就是抢占优先级占2位, 子优先级占1位, 这样就有4个抢占等级, 并且在每个抢占等级的内部会有2种子优先级, 高抢占优先级的中断能够打断低优先级ISR, 然而相同抢占优先级的中断之间却不可以嵌套, 这是因为子优先级仅仅决定挂起排队的顺序。对于实际优先级寄存器而言, 其值呈现出左对齐的状态。当具备8位优先级位宽予以考量时, 值为0x00设定为具有最高的优先级, 而作为对照, 0xFF则被定义为最低优先级。然而, 要是芯片仅仅实现了4位的优先级位宽表现方面的情况, 那么仅仅使用高4位部分, 在此种情形下, 0x00依旧保持为最高优先级, 0xF0则被归结为最低优先级, 并且把0x0F写入的最终效果等同于0x00写入的情况。这样一种左对齐的设计方式, 促使不同实现之间在软件可移植性上获得了更为良好的表现。优先级反转及其解决方案实时嵌入式系统里边, 有个隐蔽且致命的问题, 叫优先级反转。经典场景就这样: 低优先级任务L拿着互斥锁, 正访问共享资源 , 之后出现高优先级任务H, 也需要同一资源, 结果被阻塞了。这时中等优先级 任务M就绪, 抢占了L , M跟锁毫无关系, 却间接把H也阻塞了 , 使得H的实际优先级, 被反转为低于M。在任务繁多, 嵌套还深的那般应用里 , H最长的阻塞时间, 有可能是无限大 , 这就违反了实时系统要求的确定性。学术界以及工业界针对优先级反转提出了两种主流的解法, 第一种是优先级继承协议, 当高优先级任务H因为锁被低优先级任务L持有从而引发阻塞的时候, L会临时将H的高优先级继承过来, 一直到L把锁释放。这致使中等优先级任务M没有办法打断L, L能够尽快完成临界区并释放锁, H的等待时长被限定在L的一个临界区长度范围之内。优先级继承的实现对于应用代码而言处于透明状态, 是由操作系统内核在互斥量获取以及释放时, 自主去对任务优先级进行调整的。第二种是优先级天花板协议具体如下: 先为每个互斥锁分配一个天花板优先级, 此优先级等于所有有可能获取该锁的任务里面最高优先级。一旦有任何任务获取这个锁, 那么且只是这个任务的优先级会立刻提升到天花板优先级。这样一种做法具备的优势在于能提前预防, 具体体现为持有锁这样一个任务从最开始的时候就会以最高优先级运行, 进而避免了优先级继承方案当中极有可能发生的两种问题, 这种问题一类是链式阻塞问题, 另一类是死锁问题。不过其代价在于就算是不存在高优先级任务去竞争该锁, 获取锁的任务依旧会被无缘无故地提升优先级, 进而也就很明显地存在一定程度的性能开销。在软考嵌入式系统设计师命题里, 优先级反转与优先级继承是高频考察要点。关键的区分之处在于, 优先级继承是那样一种情况, 即出现问题之后的补救措施, 而优先级天花板则是事前预防的手段。另外, 还需要留意的是, 优先级继承没办法彻底避免死锁, 要是两个高优先级任务相互交叉等待对方所持有的锁, 仅仅依靠优先级继承是无法解开死锁局面的。中断嵌套的实现约束允许一个具有高优先级的中断, 去打断那个正在执行的、低优先级的ISR, 进而形成多层中断调用栈, 此为中断嵌套。对于 -M处理器而言, 其原生就支持中断嵌套, 在硬件进入ISR时, 会自动把上下文保存到主栈MSP, 后续出现的嵌套中断, 会继续朝着同一栈压入新的上下文帧。然而, 中断嵌套的实现并非没有代价: 每一层嵌套都会消耗至少32字节的栈空间, 这其中包含8个自动压栈寄存器, 再加上ISR内部局部变量的消耗, 多层嵌套便有可能导致栈溢出。没有硬件栈溢出检测机制的 -M 处理器, -M23/M33 中引入了栈溢出检查, 栈溢出的后果是灾难性的, 会可能静默覆盖相邻内存区域的数据, 导致难以复现的随机故障。在工程实践里, 应该在链接脚本中预留充足的 ISR 栈空间, 还要在调试阶段通过填充已知模式字以及周期性检查来验证栈使用的安全性。五、中断服务程序ISR的编写铁律那编写ISR的质量, 会直接对系统的实时性以及稳定性产生影响。有这样一条铁律, 那就是ISR必须要短。在ISR执行的这个期间, 同优先级以及更低优先级的中断会被屏蔽掉, 如此一来CPU就没办法响应其他紧急事件了。要是在ISR当中执行那些耗时的操作, 像是复杂计算、外设轮询等待、字符串格式化这类的, 那系统的实时性就会急剧地恶化掉。而正确做法是, 在ISR当中只做最小化处理, 也就是读取状态寄存器, 清除中断标志, 把关键数据拷贝到缓冲区, 发送信号量来唤醒任务, 之后立刻返回。耗时处理全部交给任务上下文的后半部执行。第二条铁律的规定是, 在ISR当中, 禁止调用任何有可能造成阻塞的函数。像获取互斥锁、等待信号量、调用内存分配函数、执行文件系统操作, 这些操作在ISR的上下文里面均是绝对禁止的。因为ISR不存在独立的线程控制块, 一旦阻塞那整个系统就会陷入死锁状态。要是有在ISR跟任务之间共享数据的需求, 推荐使用无锁数据结构比如环形缓冲区或者使用后缀的特化API, 这些API在资源不可用时是直接返回失败而不是阻塞。