个人学习的笔记,希望帮助能到和我一样阅读Cortex-M3权威指南Cn遇到困难的人。
强烈建议先阅读Cortex-M3权威指南Cn第七章在来观看笔记
在cortex m3中,向量表是什么
向量表(Vector Table)是一个中断服务程序入口地址的数组,存储在内存的固定位置。当异常或中断发生时,处理器会自动从这里查找对应异常的处理程序地址。
向量表的结构
向量表包含以下内容(按顺序):
- 初始主堆栈指针(MSP)值 - 第一个字(4字节)
- 复位向量 - 程序开始执行的地址
- 异常向量 - NMI、HardFault等系统异常
- 外部中断向量 - IRQ0、IRQ1等中断服务程序地址
典型向量表示例:
txt
地址 内容 说明
0x00000000 0x20001000 初始堆栈指针(MSP)
0x00000004 0x08000009 复位向量(Reset_Handler)
0x00000008 0x08000123 NMI_Handler
0x0000000C 0x08000145 HardFault_Handler
...
0x0000003C 0x08000234 SysTick_Handler
0x00000040 0x08000567 WWDG_IRQHandler (IRQ0)
0x00000044 0x08000789 PVD_IRQHandler (IRQ1)
...
在非Bootloader + Application模式下,如何指向向量表?
在非Bootloader + Application模式下,即单一应用程序运行时,向量表通常固定在Flash的起始位置(例如0x08000000)。这种情况下,指向向量表的方式如下:
- 硬件自动指向:Cortex-M3内核在上电复位后,会自动从地址0x00000000处读取向量表。在大多数微控制器中,Flash的起始地址(如0x08000000)会被映射到0x00000000,因此向量表实际上位于Flash的起始位置。
- 启动文件设置:在工程中的启动文件(通常是汇编文件,如startup_stm32f10x.s)中,会定义向量表,并将其放置在Flash的起始段。链接器脚本(.ld文件)会确保向量表被链接到Flash的起始地址(即0x08000000)。
- 无需手动设置VTOR:在单一应用程序中,通常不需要重定位向量表,因此可以不设置VTOR寄存器,使用默认的0x00000000地址(即映射后的Flash地址)。或者,为了代码清晰,可以在系统初始化时明确将VTOR设置为Flash的起始地址(例如0x08000000),但一般都会怎么做。
- 系统初始化代码 :在系统初始化函数(如SystemInit)中,会设置VTOR。例如,在STM32的标准库中,SystemInit函数可能会包含如下代码:
SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET;
其中,FLASH_BASE是Flash的起始地址(如0x08000000),VECT_TAB_OFFSET通常是0。这样,向量表就被明确设置为从Flash起始地址开始。 - 中断处理:当发生异常或中断时,处理器会自动根据VTOR所指向的向量表(如果已设置)或默认的向量表地址(0x00000000)来获取中断服务程序的入口地址。
在Bootloader + Application模式下,如何指向向量表?
在Bootloader + Application模式下,向量表的指向分为两个阶段:Bootloader阶段和Application阶段。具体步骤如下:
- 系统上电/复位 :
- Cortex-M3处理器从地址0x00000000读取初始主堆栈指针(MSP),从0x00000004读取复位向量(Reset_Handler地址),并跳转到复位向量执行。
- 在大多数微控制器中,Flash的起始地址(如0x08000000)被映射到0x00000000,因此实际上处理器访问的是Bootloader的向量表。
- Bootloader初始化 :
- Bootloader的启动代码首先执行,初始化系统环境(如时钟、必要的外设)。
- 在Bootloader的初始化代码中(通常在SystemInit函数或类似函数中),会设置向量表偏移寄存器(VTOR)指向Bootloader自己的向量表起始地址(例如0x08000000)。这一步确保在Bootloader运行期间,任何异常或中断发生时,处理器能正确跳转到Bootloader的中断服务程序。
- Bootloader运行 :
- Bootloader执行其主要功能,例如检查是否需要更新Application、验证Application完整性等。
- 在此期间,所有中断都使用Bootloader的向量表。
- 跳转到Application前的准备 :
- 当Bootloader决定跳转到Application时,它首先确保Application是有效的(例如,检查Application向量表的第一个字是否在合理的栈地址范围内)。
- 然后,Bootloader执行以下关键步骤:
a. 禁用全局中断,防止在切换过程中发生中断。
b. 将VTOR寄存器重新设置为Application向量表的起始地址(例如0x08004000)。这样,当跳转到Application后,中断将使用Application的向量表。
c. 从Application向量表的第一个字(地址0x08004000)读取初始主堆栈指针值,并更新MSP寄存器。
d. 从Application向量表的第二个字(地址0x08004004)读取Application的复位向量(即Application的Reset_Handler地址)。
- 跳转到Application :
- Bootloader将Application的Reset_Handler地址转换为函数指针,并跳转到该地址执行。
- 此时,处理器已经使用Application的堆栈指针,并且VTOR指向Application的向量表。
- Application初始化 :
- Application的启动代码开始执行,进行Application的系统初始化(如时钟、内存、外设等)。
- 在Application的初始化代码中,通常也会设置VTOR指向自己的向量表(尽管Bootloader已经设置过,但为了确保一致性,Application会重新设置一次)。
- Application运行 :
- Application正常运行,所有中断服务程序由Application提供,处理器根据VTOR指向的Application向量表来查找中断处理函数。
- 异常情况 :
- 如果在Application运行过程中发生复位,处理器会再次从0x00000000(映射到Bootloader的向量表)开始执行,进入Bootloader。Bootloader可以再次决定是否跳转到Application。
注意事项
- 如果在Application运行过程中发生复位,处理器会再次从0x00000000(映射到Bootloader的向量表)开始执行,进入Bootloader。Bootloader可以再次决定是否跳转到Application。
- VTOR寄存器设置时,向量表起始地址必须按照向量表大小对齐(通常为128字节对齐)。
- 在跳转前禁用中断,跳转后再启用,可以避免在切换过程中出现不可预料的中断行为。
- 有些Bootloader设计可能不会在跳转前禁用中断,而是确保在切换VTOR和堆栈指针的过程中不会发生中断(如通过优先级或确保没有中断发生)。
通过这种方式,实现了Bootloader和Application各自拥有独立的向量表,并且在运行时可以正确切换。
以下为Cortex‐M3 权威指南原文:
当发生了异常并且要响应它时,CM3需要定位其处理例程的入口地址。这些入口地址存储在所谓的"(异常)向量表"中。缺省情况下,CM3认为该表位于零地址处,且各向量占用4字节,因此每个表项占用4字节。
因为地址0处应该存储引导代码,所以它通常是Flash或者是ROM器件,并且它们的值不得在运行时改变。然而,为了动态重分发中断,CM3允许向量表重定位------从其它地
址处开始定位各异常向量。这些地址对应的区域可以是代码区,但也可以是RAM区。在RAM区就可以修改向量的入口地址了。为了实现这个功能,NVIC中有一个寄存器,称为"向量表偏移量寄存器"(在地址0xE000_ED08处),通过修改它的值就能定位向量表。但必须注意的是:向量表的起始地址是有要求的:必须先求出系统中共有多少个向量,再把这个数字向上增大到是2的整次幂,而起始地址必须对齐到后者的边界上。例如,如果一共有32个中断,则共有32+16(系统异常)=48个向量,向上增大到2的整次幂后值为64,因此地址地址必须能被64*4=256整除,从而合法的起始地址可以是:0x0,0x100,0x200等。
如果需要动态地更改向量表,则对于任何器件来说,向量表的起始处都必须包含以下向量:
- 主堆栈指针(MSP)的初始值
- 复位向量
- NMI
- 硬fault服务例程
后两者也是必需的,因为有可能在引导过程中发生这两种异常。
可以在SRAM中开出一块用于存储向量表。然后在引导完成后,就可以启用内存中的向量表,从而实现向量可动态调整的功能。
在cortex m3中,优先级分组,抢占优先级、子优先级(亚优先级)
定义:
优先级分组 → 定义拆分规则
抢占优先级 → 决定中断嵌套能力
亚优先级 → 决定相同抢占优先级下的执行顺序
详细解释:
- 优先级分组(Priority Grouping)
- 通过设置
PRIGROUP字段(位于AIRCR寄存器的bits[10:8])来定义 - 它决定如何分配优先级位给抢占优先级和亚优先级
- 共有8种分组方案(0-7)
2. 抢占优先级(Preemption Priority) - 也称为主优先级(Group Priority)
- 高抢占优先级的中断可以打断低抢占优先级的中断(嵌套中断)
- 数值越小,优先级越高
3. 亚优先级(Subpriority) - 也称为次要优先级
- 当两个中断的抢占优先级相同时,亚优先级决定谁先执行
- 亚优先级不能引起中断嵌套(相同抢占优先级的中断不能互相打断)
实际的工作方式:
每个中断的优先级由8位表示(但Cortex-M3通常只实现高4位[7:4],即16个优先级等级)。
优先级分组决定了这4位如何拆分:
- 例如:分组2 → 2位用于抢占优先级,2位用于亚优先级
- 抢占优先级:0-3(4级)
- 亚优先级:0-3(4级)
- 例如:分组3 → 3位用于抢占优先级,1位用于亚优先级
- 抢占优先级:0-7(8级)
- 亚优先级:0-1(2级)
以下为Cortex‐M3 权威指南原文:
在CM3中,优先级对于异常来说很关键的,它会影响一个异常是否能被响应,以及何时可以响应。优先级的数值越小,则优先级越高。CM3支持中断嵌套,使得高优先级异常会抢占(preempt)低优先级异常。有3个系统异常:复位,NMI以及硬fault,它们有固定的优先级,并且它们的优先级号是负数,从而高于所有其它异常。所有其它异常的优先级则都是可编程的(但不能编程为负数)。
原则上,CM3支持3个固定的高优先级和多达256级的可编程优先级,并且支持128级抢占(128的来历请见下文分解------译注)。但是,绝大多数CM3芯片都会精简设计,以致实际上支持的优先级数会更少,如8级,16级,32级等。它们在设计时会裁掉表达优先级的几个低端有效位,以达到减少优先级数的目的(可见,不管使用多少位,优先级号是以MSB对齐的------译注)。
通过让优先级以MSB对齐,可以简化程序的跨器件移植。比如,如果一个程序早先在支持4位优先级的器件上运行,在移植到只支持3位优先级的器件后,其功能不受影响。但若是对齐到LSB,则会使MSB丢失,导致数值大于7的低优先级一下子升高了,甚至会反转小于等于7的高优先级。如,8号优先级因为损失了MSB,现在反而变成0号了!
补充:
MSB对齐(最高有效位对齐,Most Significant Bit Alignment)
- MSB(最高有效位) :指一个二进制数中权重最大的位(最左边的位)
- MSB对齐 :将数据的最高有效位对齐到寄存器的最高位位置
LSB对齐(最低有效位对齐, Least Significant Bit Alignment) - LSB(最低有效位) :指一个二进制数中权重最小的位(最右边的位)
- LSB对齐:将数据的最低有效位对齐到寄存器的最低位位置
实际示例
假设我们有一个8位寄存器,要存储数值 5(二进制 0101):
LSB对齐
txt
寄存器位: 7 6 5 4 3 2 1 0
存储的值: 0 0 0 0 0 1 0 1 ← 5的二进制
↑ ↑
MSB LSB(对齐到bit 0)
数值直接放在低位,高位补0。大多数处理器(如x86, ARM)使用LSB对齐。
MSB对齐
txt
寄存器位: 7 6 5 4 3 2 1 0
存储的值: 0 1 0 1 0 0 0 0 ← 5左移4位
↑ ↑
MSB(对齐到bit 7) LSB
Cortex-M3的中断优先级使用MSB对齐:
- Cortex-M3的优先级寄存器是8位宽,但通常只实现高4位[7:4]
- 优先级数值必须左移到高4位
以下为Cortex‐M3 权威指南原文:
抢占优先级决定了抢占行为:当系统正在响应某异常L时,如果来了抢占优先级更高的异常H,则H可以抢占L。亚优先级则处理"内务":当抢占优先级相同的异常有不止一个悬起时,就优先响应亚优先级最高的异常。
这种优先级分组规定:亚优先级至少是1个位。因此抢占优先级最多是7个位,造成了最多只有128级抢占的现象。
但是CM3允许从比特7处分组,此时所有的位都表达亚优先级,没有任何位表达抢占优先级,因而所有优先级可编程的异常之间就不会发生抢占------相当于在它们之中除能了CM3的中断嵌套机制。当然还有凌架于法律之上的三位老大:复位,NMI和硬fault。它们无论何时出现,都立即无条件抢占所有优先级可编程的"平民异常"。
如果优先级完全相同的多个异常同时悬起,则先响应异常编号最小的那一个。如IRQ#3会比IRQ#5先得到响应。
虽然优先级分组的功能很强大,但是粗心地更改会使它变得很暴力,尤其是在设计硬实时系统的时候,这简直就是在玩火------常常会改变系统的响应特性,导致某些关键任务有可能得不到及时响应,凶多吉少的意外随时可能猛烈发作。其实在绝大多数情况下,优先级的分组都要预先经过计算论证,并且在开机初始化时一次性地设置好,以后就再也不动它了。只有在绝对需要且绝对有把握时,才小心地更改,并且要经过尽可能充分的测试。
在cortex m3中,中断(异常)状态、响应条件、转换行为
一、中断(异常)的四种状态
非活动状态 (Inactive)
- 中断未触发,也未悬起
- 中断源未发出请求,或请求已被完全处理
悬起状态 (Pending) - 中断源已发出请求,但处理器尚未开始执行对应的中断服务程序(ISR)
- 悬起状态被记录在NVIC的悬起寄存器中
- 即使中断源随后取消了请求,悬起状态仍保持
活动状态 (Active) - 处理器正在执行该中断的ISR
- 中断的"活动"状态位被置位
- 此时该中断不能再次被响应(除非是NMI或可重入中断)
活动且悬起状态 (Active and Pending) - 处理器正在执行该中断的ISR(活动状态)
- 同一中断源在此期间又发出了新的请求(悬起状态)
- 只有当前ISR执行完毕后,才会再次响应这个中断
二、状态转换条件
状态转换图的关键路径:
非活动 → 悬起
- 条件:中断源发出请求(硬件触发或软件写悬起寄存器)
- 行为:NVIC将对应悬起位置1
悬起 → 活动 - 条件:同时满足所有响应条件(见下文第三部分)
- 行为:
a. 处理器保存上下文(自动压栈)
b. 从向量表获取ISR入口地址
c. 清除该中断的悬起位(边沿触发中断)
d. 设置中断的活动状态位
e. 开始执行ISR
活动 → 非活动 - 条件:ISR执行完毕,执行异常返回指令(BX LR等)
- 行为:
a. 处理器恢复上下文(自动出栈)
b. 清除中断的活动状态位
c. 返回到被中断的程序
活动 → 活动且悬起 - 条件:当前中断正在执行时,同一中断源再次触发
- 行为:设置悬起位,但保持活动状态
活动且悬起 → 活动 - 条件:当前ISR执行完毕,但悬起位仍为1
- 行为:再次响应同一中断(重新进入ISR)
悬起 → 非活动 - 条件:软件主动清除悬起位
- 行为:中断被取消,不会得到响应
三、中断响应条件(必须同时满足)
中断使能条件
- 全局中断使能:PRIMASK=0, FAULTMASK=0
- 该特定中断的使能位被置位(NVIC_ISER寄存器)
优先级条件 - 该中断的优先级高于当前执行程序的优先级
- 该中断的优先级高于BASEPRI寄存器设置的值(如果使用了BASEPRI)
- 该中断的优先级是当前所有悬起中断中最高的
状态条件 - 中断处于悬起状态(悬起位=1)
- 中断不处于活动状态(除非是可重入的特殊情况)
处理器状态条件 - 处理器未在处理不可中断的操作(如某些原子操作)
- 处理器未处于锁定状态(如某些调试状态)
具体行为:
中断响应序列
- 压栈:自动保存xPSR, PC, LR, R12, R3-R0到当前堆栈
- 取向量:从向量表读取ISR入口地址
- 更新寄存器:更新PC, LR, SP, PSR
- 执行ISR
中断退出序列 - 出栈:自动恢复R0-R3, R12, LR, PC, xPSR
- 更新NVIC状态:清除活动状态位
- 继续执行被中断的程序
以下为Cortex‐M3 权威指南原文:
当中断输入脚被 assert 后,该中断就被悬起。即使后来中断源取消了中断请求,已经被标记成悬起的中断也被记录下来。到了系统中它的优先级最高的时候,就会得到响应。
但是,如果在某个中断得到响应之前,其悬起状态被清除了(例如,在 PRIMASK 或FAULTMASK 置位的时候软件清除了悬起状态标志),则中断被取消。
简单来说,这就像有人按了你家的门铃。
- 按门铃(中断触发)
有人按了门铃(中断输入脚被触发)。这时,门铃的"呼叫灯"会亮起(中断被标记为"悬起"状态),表示"有人叫过门"。 - 记录呼叫(保持悬起)
即使这个人马上松手不按了(中断源取消请求),"呼叫灯"依然亮着(悬起状态被记录)。这个"有人叫过门"的事实已经被系统记住了。 - 准备开门(等待响应)
当你手头的事忙完了,并且这个门铃的优先级最高时(比如没有其他更紧急的事),你就会去响应------准备开门。 - 关键情况:灯被提前关掉了(清除悬起状态)
然而,如果在你去开门之前 ,有人(可能是你自己,也可能是其他程序)手动把"呼叫灯"给关掉了(在PRIMASK/FAULTMASK屏蔽中断时,软件清除了悬起标志),那么会发生什么? - 中断被取消(响应失效)
当你终于准备去开门时,却发现"呼叫灯"是灭的。你会认为"没人按过门铃",于是就不会去开门了。这个中断就此消失,永远不会得到响应,即使门铃确实被按过。
以下为Cortex‐M3 权威指南原文:
当某中断的服务例程开始执行时,就称此中断进入了"活跃"状态,并且其悬起位会被硬件自动清除。在一个中断活跃后,直到其服务例程执行完毕,并且返回(亦称为中断退出,第九章详细讨论)了,才能对该中断的新请求予以响应(即单实例)。当然,新请求的响应亦是由硬件自动清零悬起标志位。中断服务例程也可以在执行过程中把自己对应的中断重新悬起(使用时要注意避免进入"死循环"------译注)
这就像一个客服电话的接听流程:
1. 接听电话(进入"活跃"状态)
- 当客服代表(CPU)终于接起一个已经亮灯响铃的来电(中断)时,这个来电的状态就从 "等待中" (悬起)变成了 "通话中"(活跃)。
- 关键动作 :代表接起电话的瞬间,"来电等待灯"会自动熄灭 (硬件自动清除悬起位)。这代表系统已经知道这个请求正在被处理,无需再记录"等待"状态。
2. 通话过程中(保持"活跃"状态) - 在客服代表处理这个电话(执行中断服务例程)的整个期间,该电话都处于 "通话中" 状态。
- 单实例规则 :在这通电话挂断之前,即使同一个客户再次打进来(同一中断源发出新请求),客服代表也不会接听 。新来的电话会再次亮起"等待灯"(新的悬起请求被记录),但必须等到当前通话结束后才会被处理。
3. 挂断电话(中断退出) - 客服代表处理完毕,挂断电话。此时,这个来电的 "通话中" 状态结束。
- 只有到了这个时候,如果"来电等待灯"还亮着(新的悬起标志存在),客服代表才会立刻接起下一通电话 (响应新的请求),并再次重复上述过程。
4. 特殊情况:通话中自己"重拨"(软件重新悬起) - 这段话最后提到,客服代表(中断服务例程自己)可以在通话过程中,主动按下"重拨"键(软件将自己对应的中断重新悬起)。
- 这意味着当前电话刚挂断,系统会立刻认为又有同一个电话打进来(悬起位被软件置1),从而可能让客服代表马上又开始处理一个相同的新请求。
- 风险提示 :如果设计不当,这会导致"挂断 -> 重拨 -> 接听 -> 挂断 -> 重拨..."的无限循环,也就是编程中必须避免的死循环。'
在cortex m3中,SVC是什么
SVC(Supervisor Call)是Cortex-M3内核中的一种系统调用异常,它允许运行在非特权模式(用户模式)下的应用程序通过执行SVC指令来请求特权模式(处理器模式)下的操作系统服务。
- SVC指令 :
- 当程序执行SVC指令时,会触发SVC异常,从而进入异常处理程序(即SVC服务例程)。
- SVC指令带有一个8位的立即数(例如:SVC
#0x03),这个立即数可以作为参数传递给SVC服务例程,用于区分不同的系统服务请求。
- SVC异常编号 :
- 在异常向量表中,SVC异常的位置是11(即向量表偏移为0x2C)。
- 触发方式 :
- 只能通过执行SVC指令触发,不能由外部硬件或软件写寄存器来触发。
- 优先级 :
- SVC异常的优先级可以配置(通过设置系统异常优先级寄存器,地址0xE000ED1C)。通常,操作系统会将其配置为较高的优先级,但低于不可屏蔽中断(NMI)和硬错误(HardFault)。
- SVC服务例程 :
- 当SVC异常被触发,处理器会进入SVC服务例程。在该例程中,操作系统可以根据SVC指令中的立即数(参数)来提供相应的服务。
- 为了获取SVC指令中的立即数,SVC服务例程需要从堆栈中读取被中断的程序的程序计数器(PC),然后从该地址读取SVC指令,最后提取立即数。
- 使用场景 :
- SVC通常用于实现操作系统的系统调用接口。应用程序运行在非特权模式,当需要操作系统提供的服务(如文件操作、任务创建等)时,就通过SVC指令切换到特权模式,由操作系统内核执行相应的服务。
- 注意点 :
- 在SVC服务例程中,不能再次触发SVC异常(即不能嵌套),除非已经修改了SVC的优先级。通常情况下,SVC异常被设计为不可重入,因此在其执行过程中,即使有更低优先级的SVC请求,也不会被响应,直到当前SVC例程执行完毕。
下面我们使用比喻的方式来说明一下:
想象一家现代化的餐厅,这里有:
- 顾客 = 用户程序(在普通区就餐,有限权限)
- 服务员 = SVC机制(连接顾客和后厨的桥梁)
- 厨师 = 操作系统内核(在后厨工作,有高级权限)
- 后厨 = 特权模式区域(普通顾客不能进入)
操作流程:
1. 顾客要点餐(用户程序需要服务)
顾客坐在用餐区(非特权模式),想要点一份牛排。但顾客不能直接闯入后厨自己做,因为:
- 后厨有危险设备(系统关键资源)
- 需要专业厨师操作(特权操作)
- 需要统一管理(系统安全)
2. 按下服务铃(执行SVC指令)
顾客按下桌上的服务铃按钮,这相当于执行:
c
SVC #0x02 // 0x02代表"点餐服务"
同时,顾客在点餐单上写下:
- 菜品编号:0x02(牛排)
- 具体要求 :七分熟,黑椒酱(通过寄存器传递参数R0、R1等)
3. 服务员响应(触发SVC异常)
服务员听到铃声后: - 立即停下手头工作(处理器暂停当前任务)
- 拿起点餐单(硬件自动保存现场到栈)
- 进入后厨通道(切换到Handler模式/特权模式)
- 将点餐单交给厨师 (跳转到SVC_Handler)
4. 厨师处理订单(操作系统提供服务)
厨师(操作系统内核)看到点餐单:
c
void SVC_Handler(void)
{
// 查看是什么服务请求
switch(订单类型) {
case 0x01: // 饮料服务
break;
case 0x02: // 牛排服务 <- 这个!
cook_steak(R0, R1); // R0=熟度,R1=酱料
break;
case 0x03: // 甜点服务
break;
}
}
厨师使用后厨的所有设备和食材(特权资源)制作牛排。
5. 服务员送回餐点(返回结果)
服务员:
- 把做好的牛排端给顾客(通过寄存器返回结果)
- 按一下"服务完成"按钮(执行异常返回指令)
- 顾客继续用餐(用户程序继续执行)
为什么需要这套系统?
安全控制
如果每个顾客都能随便进后厨:
- 可能会弄伤自己(程序崩溃)
- 可能偷拿食材(非法访问内存)
- 可能搞乱后厨(破坏系统状态)
统一管理 - 所有点餐通过服务员登记(统一入口)
- 厨师按顺序处理(避免竞争条件)
- 后厨资源有序使用(资源管理)
灵活扩展
餐厅可以轻松增加新服务: - 新菜品 = 新的SVC服务编号
- 服务员总能找到对应厨师(SVC_Handler分发)
- 顾客无需知道怎么做菜(用户程序不关心实现)
特殊场景
1. 多点几个菜(多个参数)
顾客想同时点牛排、沙拉和饮料:
- 填写多行点餐单(多个寄存器传参)
- 服务员一次传递给厨师
- 厨师按顺序制作
2. 催菜服务(优先级)
如果顾客按下紧急服务铃(更高优先级SVC): - 服务员会优先处理这个请求
- 但不会打断正在做菜的厨师(SVC_Handler执行中不能被同优先级打断)
3. 无效点餐(错误的SVC编号)
顾客点了菜单上没有的菜(无效SVC编号): - 服务员无法处理
- 叫来餐厅经理(触发HardFault)
- 经理处理这个错误
以下为Cortex‐M3 权威指南原文:
SVC(系统服务调用,亦简称系统调用)和 PendSV(可悬起系统调用),它们多用于在操作系统之上的软件开发中。SVC 用于产生系统函数的调用请求。例如,操作系统不让用户程序直接访问硬件,而是通过提供一些系统服务函数,用户程序使用 SVC 发出对系统服务函数的呼叫请求,以这种方法调用它们来间接访问硬件。因此,当用户程序想要控制特定的硬件时,它就会产生一个 SVC 异常,然后操作系统提供的 SVC 异常服务例程得到执行,它再调用相关的操作系统函数,后者完成用户程序请求的服务。
这种"提出要求------得到满足"的方式,很好、很强大、很方便、很灵活、很能可持续发展。首先,它使用户程序从控制硬件的繁文缛节中解脱出来,而是由 OS 负责控制具体的硬件。第二,OS 的代码可以经过充分的测试,从而能使系统更加健壮和可靠。第三,它使用户程序无需在特权级下执行,用户程序无需承担因误操作而瘫痪整个系统的风险。第四,通过 SVC 的机制,还让用户程序变得与硬件无关,因此在开发应用程序时无需了解硬件的操作细节,从而简化了开发的难度和繁琐度,并且使应用程序跨硬件平台移植成为可能。开发应用程序唯一需要知道的就是操作系统提供的应用编程接口(API),并且了解各个请求代号和参数表,然后就可以使用 SVC 来提出要求了(事实上,为使用方便,操作系统往往会提供一层封皮,以使系统调用的形式看起来和普通的函数调用一致。各封皮函数会正确使用 SVC指令来执行系统调用------译者注)。其实,严格地讲,操作硬件的工作是由设备驱动程序完成的,只是对应用程序来说,它们也是操作系统的一部分。
SVC 异常通过执行"SVC"指令来产生。该指令需要一个立即数,充当系统调用代号。SVC异常服务例程稍后会提取出此代号,从而解释本次调用的具体要求,再调用相应的服务函数。例如:
SVC 0x3 ; 调用 3 号系统服务
在 SVC 服务例程执行后,上次执行的 SVC 指令地址可以根据自动入栈的返回地址计算出。找到了 SVC 指令后,就可以读取该 SVC 指令的机器码,从机器码中萃取出立即数,就获知了请求执行的功能代号。如果用户程序使用的是 PSP,服务例程还需要先执行 MRS Rn,PSP 指令来获取应用程序的堆栈指针。通过分析 LR 的值,可以获知在 SVC 指令执行时,正在使用哪个堆栈(细节在第 8 章中讨论)。
在cortex m3中,PendSV是什么
在Cortex-M3中,PendSV(Pendable Service Call)是一种可挂起的异常,主要用于操作系统中的上下文切换(任务切换)。其特点是可以被挂起,直到系统没有其他重要的异常处理时才会执行。这使得PendSV非常适合于在多个任务之间进行切换,而不会影响到高优先级的中断处理。
PendSV的典型使用场景是在操作系统中,当需要执行任务切换时,不是立即切换,而是通过将PendSV异常挂起,然后在没有其他中断处理时再执行切换。这样可以避免在中断处理过程中进行上下文切换,从而使得中断响应更加及时,同时也能保证任务切换的顺利进行。
具体来说,PendSV的使用步骤通常如下:
- 设置PendSV的优先级为最低(这样它不会打断其他中断)。
- 当需要任务切换时,将PendSV异常挂起(即将中断控制和状态寄存器ICSR中的PENDSVSET位置1)。
- 当处理器退出所有中断处理程序后,它会检查挂起的PendSV异常,并进入PendSV异常处理程序。
- 在PendSV异常处理程序中,执行实际的上下文切换(保存当前任务的上下文,恢复下一个任务的上下文)。
由于PendSV的优先级最低,所以它会在所有高优先级中断处理完成后才执行,这保证了高优先级中断的响应性,同时将任务切换延迟到一个合适的时机。
在Cortex-M3中,PendSV的异常编号为14,可以通过系统控制块(SCB)中的寄存器来设置其优先级和挂起。
下面我们使用比喻来理解他:
比喻场景:医院的急诊调度系统
想象一家繁忙的医院:
角色对应
- 急诊病人 = 硬件中断(紧急,必须立即处理)
- 普通住院病人 = 操作系统任务(需要处理,但不紧急)
- 护士 = 中断服务程序
- 住院部医生 = 操作系统调度器
- PendSV = "请医生稍后来查房"的调度机制
PendSV的关键特性
1. 延迟执行机制
- 可挂起:请求可以被记录,但不立即执行
- 优先级最低:PendSV的优先级通常设为最低(0xFF)
- 等待时机 :等到所有更高优先级中断都完成后才执行
2. 在RTOS中的典型应用
c
// FreeRTOS中的PendSV使用(简化版)
void xPortPendSVHandler(void)
{
__asm volatile (
// 保存当前任务上下文
"mrs r0, psp \n"
"stmdb r0!, {r4-r11, lr} \n"
// 保存当前任务栈指针
"ldr r1, =pxCurrentTCB \n"
"ldr r1, [r1] \n"
"str r0, [r1] \n"
// 选择下一个任务
"bl vTaskSwitchContext \n"
// 恢复下一个任务的上下文
"ldr r1, =pxCurrentTCB \n"
"ldr r1, [r1] \n"
"ldr r0, [r1] \n"
"ldmia r0!, {r4-r11, lr} \n"
// 恢复栈指针
"msr psp, r0 \n"
// 异常返回
"bx lr \n"
);
}
3. 与SVC的对比
| 特性 | PendSV | SVC |
|---|---|---|
| 触发方式 | 可挂起(异步) | 立即执行(同步) |
| 执行时机 | 延迟到合适时机 | 立即响应 |
| 优先级 | 通常设为最低 | 根据需要设置 |
| 主要用途 | 任务切换 | 系统调用 |
| 比喻 | 医生查房(等病人稳定后) | 急诊手术(立即处理) |
PendSV处理程序模板
assembly
PendSV_Handler:
CPSID I ; 禁用中断
MRS R0, PSP ; 获取当前任务的栈指针
CBZ R0, PendSV_Handler_NoSave ; 如果是第一次切换,无需保存
; 保存上下文(R4-R11)
STMDB R0!, {R4-R11}
; 保存当前任务栈指针到TCB
LDR R1, =CurrentTCB
LDR R1, [R1]
STR R0, [R1]
PendSV_Handler_NoSave:
; 选择下一个任务
LDR R0, =NextTCB
LDR R0, [R0]
LDR R1, [R0]
; 恢复上下文(R4-R11)
LDMIA R1!, {R4-R11}
; 更新PSP
MSR PSP, R1
; 更新当前TCB指针
LDR R1, =CurrentTCB
STR R0, [R1]
CPSIE I ; 启用中断
BX LR ; 返回,开始新任务
FreeRTOS示例
c
// FreeRTOSConfig.h中配置
#define xPortPendSVHandler PendSV_Handler
// 任务切换函数
void vPortYieldFromISR(void)
{
// 从ISR中触发任务切换
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
}
// PendSV处理程序(实际上下文切换代码)
__asm void xPortPendSVHandler(void)
{
extern uxCriticalNesting;
extern pxCurrentTCB;
extern vTaskSwitchContext;
PRESERVE8
mrs r0, psp
isb
ldr r3, =pxCurrentTCB
ldr r2, [r3]
stmdb r0!, {r4-r11, r14}
str r0, [r2]
stmdb sp!, {r3}
mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY
msr basepri, r0
dsb
isb
bl vTaskSwitchContext
mov r0, #0
msr basepri, r0
ldmia sp!, {r3}
ldr r1, [r3]
ldr r0, [r1]
ldmia r0!, {r4-r11, r14}
msr psp, r0
isb
bx r14
nop
}
以下为Cortex‐M3 权威指南原文:
另一个相关的异常是 PendSV(可悬起的系统调用),它和 SVC 协同使用。一方面,SVC异常是必须立即得到响应的(若因优先级不比当前正处理的高,或是其它原因使之无法立即响应,将上访成硬 fault------译者注),应用程序执行 SVC 时都是希望所需的请求立即得到响应。另一方面,PendSV 则不同,它是可以像普通的中断一样被悬起的(不像 SVC 那样会上访)。OS 可以利用它"缓期执行"一个异常------直到其它重要的任务完成后才执行动作。悬起 PendSV 的方法是:手工往 NVIC 的 PendSV 悬起寄存器中写 1。悬起后,如果优先级不够高,则将缓期等待执行。
PendSV 的典型使用场合是在上下文切换时(在不同任务之间切换)。例如,一个系统中有两个就绪的任务,上下文切换被触发的场合可以是:
- 执行一个系统调用
- 系统滴答定时器(SYSTICK)中断,(轮转调度中需要)
在cortex m3中,SVC中断、PendSV中断、普通中断
为了方便理解这里将使用比喻来帮助我们理解他们:
比喻一:餐厅后厨工作流程
想象一个高效的餐厅后厨系统:
角色对应
- 厨师(CPU):正在制作菜品(执行主程序)
- 传菜口铃声(普通硬件中断):有新订单来了,必须立即处理
- 经理呼叫按钮(SVC中断):厨师需要申请特殊权限(如开保险柜取高级食材)
- 交班闹钟(PendSV中断) :换班时间到,但会等手头工作完成再交接
三种中断的运作场景
1. 传菜口铃声(普通硬件中断)
c
// 情景:厨师正在切菜,传菜口突然响了
"叮咚!" // 外部中断(如GPIO中断)
// 厨师反应:
1. 立即放下菜刀(保存当前状态)
2. 跑到传菜口(跳转到中断处理程序)
3. 拿取新订单(读取中断数据)
4. 确认收到(清除中断标志)
5. 回到原位继续切菜(恢复现场)
// 特点:必须立即响应,优先级最高
2. 经理呼叫按钮(SVC中断)
c
// 情景:厨师需要取昂贵的松露,但保险柜只有经理能开
厨师按下"经理呼叫"按钮 // 执行SVC指令
"SVC #0x03" // 3号服务:开保险柜
// 系统反应:
1. 厨师进入等待区(从用户模式切换到特权模式)
2. 经理过来验证身份(操作系统检查权限)
3. 经理打开保险柜(执行特权操作)
4. 厨师取出松露(获得请求的资源)
5. 厨师返回工作岗位(返回到用户模式)
// 特点:主动请求,同步执行,用于获取系统服务
3. 交班闹钟(PendSV中断)
c
// 情景:下午4点换班时间到,但厨师A正在处理紧急订单
闹钟响起:"该换班了!" // SysTick定时器到期
// 但实际情况:
1. 闹钟响起(SysTick中断触发)
2. 厨师A举手示意:"知道了,但请等我做完这道菜"
3. 系统记录:"需要换班,但可以等待"(挂起PendSV)
4. 厨师A继续完成紧急订单(处理更高优先级中断)
5. 所有紧急订单完成后(更高优先级中断都处理完)
6. 系统提醒:"现在可以换班了"(执行PendSV_Handler)
7. 厨师A与厨师B安全交接(任务切换)
// 特点:延迟执行,优先级最低,用于耗时的上下文切换
三者的互动
c
// 餐厅一天的工作流程示例
1. 厨师A正在准备食材(主程序运行)
2. 突然多个订单同时来(多个硬件中断)
3. 厨师A处理紧急订单(响应硬件中断)
4. 厨师A需要特殊调料(按下经理呼叫按钮/SVC)
5. 经理送来调料(SVC处理程序执行)
6. 换班时间到(PendSV被挂起)
7. 但还有VIP订单(更高优先级中断)
8. 处理完VIP订单后(所有高优先级中断完成)
9. 厨师A与厨师B换班(PendSV执行)
比喻二:操作系统中的交通管制系统
想象一个智能城市交通管理系统:
角色对应
- 交通指挥中心(CPU):管理整个城市的交通
- 交通事故报警(普通硬件中断):突发事故,必须立即处理
- 交警权限申请(SVC中断):交警需要特殊权限(如封锁道路)
- 交通信号计划切换(PendSV中断) :早晚高峰信号灯计划切换
三种中断的运作场景
1. 交通事故报警(普通硬件中断)
c
// 情景:十字路口发生车祸
报警电话响起:"中山路发生事故!" // 外部中断触发
// 指挥中心反应:
1. 立即中断当前工作(保存现场)
2. 调取事故地点监控(读取中断源)
3. 派遣最近交警(执行中断处理程序)
4. 疏导交通(处理中断事务)
5. 返回原工作计划(中断返回)
// 特点:紧急、不可预测、必须立即响应
2. 交警权限申请(SVC中断)
c
// 情景:交警需要临时封锁一条道路进行活动
交警使用对讲机:"请求封锁解放路" // 用户程序执行SVC
// 系统流程:
1. 交警身份验证(从用户模式切换到特权模式)
2. 检查申请合法性(操作系统安全检查)
3. 授权封锁指令(执行特权操作)
4. 更新交通信号系统(修改系统状态)
5. 返回确认信息(SVC返回结果)
// 特点:主动、可控、用于系统服务调用
3. 交通信号计划切换(PendSV中断)
c
// 情景:晚高峰时间(17:00)到了,需要切换信号灯计划
时钟到17:00:触发定时器中断 // SysTick中断
// 但实际处理:
1. 系统检测到17:00(SysTick中断发生)
2. 标记"需要切换信号计划"(挂起PendSV)
3. 但此时有交通事故正在处理(更高优先级中断)
4. 等待事故处理完毕(等待高优先级中断完成)
5. 所有紧急事件处理完后(无更高优先级中断)
6. 执行信号计划切换(PendSV_Handler)
7. 平滑过渡到晚高峰模式(任务切换)
// 特点:计划性、可延迟、优先级最低
操作系统中的具体代码对应
c
// 交通管制系统代码示例
// 1. 事故报警处理(硬件中断)
void Accident_IRQHandler(void) // 硬件中断处理程序
{
// 立即响应
dispatch_police(); // 派遣交警
redirect_traffic(); // 疏导交通
clear_accident_flag(); // 清除中断标志
}
// 2. 交警权限申请(SVC系统调用)
void Traffic_Officer_Request(void)
{
// 用户程序(交警)请求
asm volatile (
"MOV R0, %0 \n" // 参数:道路ID
"SVC #0x05 \n" // 5号服务:封锁道路
:
: "r" (road_id)
);
}
// SVC处理程序
void SVC_Handler(void)
{
uint8_t svc_num = get_svc_number();
if (svc_num == 0x05) {
// 检查权限
if (check_authorization()) {
block_road(R0); // 特权操作:封锁道路
set_return_value(SUCCESS);
}
}
}
// 3. 信号计划切换(PendSV调度)
void SysTick_Handler(void) // 定时器中断
{
if (is_rush_hour()) {
// 标记需要切换,但不立即执行
SCB->ICSR |= SCB_ICSR_PENDSVSET_Msk;
}
}
void PendSV_Handler(void) // 任务切换
{
// 保存当前信号计划状态
save_current_traffic_plan();
// 切换到晚高峰计划
switch_to_evening_plan();
// 恢复相关状态
update_all_traffic_lights();
}
三者的优先级与协作关系
c
// 优先级排序(数值越小优先级越高)
1. 交通事故报警 优先级:0x00 // 最高
2. 特殊车辆通行 优先级:0x40 // 中等
3. 交警权限申请 优先级:0x80 // SVC,可配置
4. 信号计划切换 优先级:0xFF // PendSV,最低
// 典型交互场景
下午16:59:50:
- 主程序:正常交通流动
下午16:59:55:
- 硬件中断:交通事故发生(优先级0x00)
- 立即处理事故
下午17:00:00:
- SysTick:晚高峰时间到(挂起PendSV)
- 但事故仍在处理,PendSV等待
下午17:00:10:
- 事故处理完毕
- 交警请求封锁旁路(SVC调用)
- SVC处理期间,又有救护车通过(更高优先级中断)
- 救护车优先通过
下午17:00:20:
- 所有紧急事件处理完毕
- 执行PendSV:切换信号计划
比喻三:医院急救中心与日常运营
背景设定
- CPU:整个医院系统,包括急救中心、门诊部、住院部等。
- 主程序:医院的日常运营(如查房、门诊接待、清洁、食堂等)。
- 普通硬件中断:急诊病人到来、医疗设备报警等紧急事件。
- SVC中断:医生请求进行特殊操作(如调用特殊设备、申请使用受限药品、调用专家会诊)。
- PendSV中断:医院内部资源调度与交接班(如医生换班、病人转科、批量病人转移)。
详细场景
1. 普通硬件中断(急诊病人到来)
场景 :医院正在日常运营中,突然救护车送来一个重症病人(外部硬件中断触发)。
处理流程:
- 立即响应:医院立即启动急救流程,暂停非紧急的日常事务(保存当前上下文)。
- 分诊处理:根据病人情况,分配急救室,调用急救医生(中断服务程序开始执行)。
- 处理过程中可能被打断:如果正在处理时,又来了一个更危重的病人(更高优先级的中断),则当前急救暂停,先去处理更紧急的。
- 处理完成:病人情况稳定,转入住院部(清除中断标志,中断返回)。
- 恢复日常运营 :但可能因为急救耽误了其他事情,需要调整计划。
特点:必须立即响应,可能嵌套,优先级高。
2. SVC中断(医生请求特殊操作)
场景 :住院部医生在治疗病人时,发现需要调用一台特殊仪器(比如MRI),但该仪器需要特殊权限和操作流程。
处理流程:
- 医生发出请求:医生按下请求按钮(执行SVC指令,带参数指明需要MRI服务)。
- 系统切换:医院系统从普通工作模式切换到特权管理模式(从用户模式切换到特权模式)。
- 权限验证:系统检查医生是否有权使用MRI,并检查MRI当前是否可用(操作系统安全检查)。
- 执行特权操作:安排技师操作MRI,为病人进行扫描(执行MRI服务例程)。
- 返回结果:将扫描结果返回给医生(SVC返回结果)。
- 医生继续治疗 :医生根据结果制定后续方案(继续执行用户程序)。
特点:由医生主动发起,同步执行,用于获取需要特权或系统管理的资源。
3. PendSV中断(医院内部资源调度与交接班)
场景 :医院每天有固定的交班时间(如下午6点),同时可能有一些非紧急的资源调度需求(如病人转科、批量转移)。
处理流程:
- 时间触发:下午6点,交班时间到(SysTick定时器中断触发)。
- 标记需求:系统标记"需要交班",但并不立即执行,因为可能还有急诊在处理(挂起PendSV)。
- 等待合适时机:继续处理当前的急诊和紧急事务(处理更高优先级中断)。
- 执行调度 :当所有紧急事务处理完毕,系统执行交班和资源调度(PendSV_Handler执行):
- 保存当前班次医生的工作状态(保存上下文)。
- 安排下一班医生接替(任务切换)。
- 进行病人转科(内存或资源重新分配)。
- 平滑过渡 :交接完成后,新班次医生开始工作(新任务开始执行)。
特点:延迟执行,优先级最低,用于耗时的资源调度和任务切换。
复杂互动场景
假设医院在下午5:55分发生以下事件:
- 主程序:医生正在查房(正常任务执行)。
- 硬件中断1 :救护车送来心脏病突发病人(高优先级中断)。
- 立即抢救,进入急救室。
- 硬件中断2 :急救室的心电监护仪报警(更高优先级中断,嵌套)。
- 医生立即处理报警,调整药物。
- SVC请求 :在抢救过程中,医生需要使用特殊药物,但需要药剂科授权(SVC调用)。
- 医生按下授权请求,系统切换到特权模式,验证并发放药物,然后返回抢救。
- 定时器中断 :下午6点整,交班时间到(SysTick中断)。
- 系统挂起PendSV,但因为有更高优先级的抢救任务,所以不立即执行。
- 硬件中断3 :又送来一个车祸重伤病人(另一个高优先级中断)。
- 暂停当前抢救,分出一部分医生处理新病人。
- 所有紧急处理完毕:两个病人都情况稳定,转入住院部。
- 执行PendSV :现在执行交班和资源调度。
- 保存当前所有医生的状态,安排下一班医生,转移病人到相应科室。
- 新班次开始:新医生开始查房、治疗等。
比喻四:国际太空空间站运营中心
想象一个高度复杂的国际太空空间站(ISS)运营系统:
角色对应
- CPU:空间站主控电脑
- 主程序:空间站的日常科研与维护计划表
- 普通硬件中断:突发紧急事件(舱压泄露、设备故障等)
- SVC中断:宇航员请求执行受控操作(出舱行走、实验设备使用)
- PendSV中断:资源调度与任务轮换(实验项目切换、宇航员作息调整)
复杂交互场景:典型空间站工作周期
背景设定
- 时间:第153天任务期,凌晨3点(空间站时间)
- 当前执行:俄罗斯宇航员正在"星辰号"舱进行材料科学实验
- 计划:2小时后将切换到美国舱段的生物实验
- 系统状态:3个后台任务运行(环境监测、数据传输、电池管理)
第一部分:突发事件(普通硬件中断)
c
// 场景:突发微流星体撞击警报
[警报!] "和谐号节点舱压力异常下降0.2kPa" // 传感器硬件中断
// 中断处理流程:
void Pressure_Alarm_IRQHandler(void)
{
// 1. 立即保存当前实验状态
// (硬件自动压栈:实验数据、寄存器状态)
// 2. 诊断问题来源(读取中断源)
source = detect_pressure_leak_source();
// 3. 优先级判断:这是紧急但非致命事件
// 比火警低,但比常规实验高
if (source == MICROMETEOROID_IMPACT) {
// 4. 启动应急协议
activate_emergency_patch(source);
// 5. 通知地面控制(嵌套中断!)
// 在压力处理中,通信系统可能触发更高优先级中断
request_ground_confirmation();
}
// 6. 清除中断标志,返回实验
// 但实验可能已被其他中断修改
}
嵌套中断的复杂性
c
// 在处理压力警报时,发生更紧急事件:
void Pressure_Alarm_IRQHandler(void)
{
// 正在定位泄漏点...
// 突然:火灾警报触发(更高优先级中断)
// 当前处理被暂停,先去处理火警
// 火警处理完成后,才返回继续处理压力警报
// 然后返回最初的材料科学实验
}
第二部分:受控系统请求(SVC中断)
c
// 场景:宇航员需要切换到"哥伦布实验舱"的特殊显微镜
// 但该设备需要地面授权和系统级配置
void Astronaut_Workflow(void)
{
// 正在进行的实验遇到瓶颈
// 决定使用高分辨率电子显微镜
// 但显微镜是共享资源,需要系统调配
asm volatile (
"MOV R0, %0 \n" // 参数1:设备ID(显微镜#3)
"MOV R1, %1 \n" // 参数2:使用时长(120分钟)
"MOV R2, %2 \n" // 参数3:实验优先级
"SVC #0x21 \n" // 33号服务:请求专用设备
:
: "r" (DEVICE_MICROSCOPE_3),
"r" (120),
"r" (PRIORITY_HIGH)
);
// SVC处理期间,可能发生:
// 1. 系统检查使用权限(身份验证)
// 2. 检查设备状态(是否被占用)
// 3. 预留资源(内存缓冲区、电力分配)
// 4. 重新配置设备参数
// 5. 记录审计日志(特权操作记录)
}
// SVC处理程序的复杂性
void SVC_Handler(void)
{
uint8_t service_id = extract_svc_number();
switch(service_id) {
case 0x21: // 设备请求服务
// 步骤1:切换到内核模式
enter_kernel_mode();
// 步骤2:验证宇航员权限
if (!verify_astronaut_credentials()) {
trigger_security_violation();
return;
}
// 步骤3:检查资源冲突
if (check_device_conflict(R0, R1)) {
// 资源被占用,可能触发调度
// 这里可能间接调用PendSV!
mark_for_rescheduling();
set_return_value(ERROR_BUSY);
} else {
// 步骤4:执行特权配置
reconfigure_power_supply(R0, R1);
allocate_memory_buffer(R2);
update_scheduling_table(); // 修改全局计划表
// 步骤5:返回成功
set_return_value(SUCCESS);
}
// 步骤6:返回用户模式
return_to_user_mode();
break;
// ... 其他50多种系统服务
}
}
第三部分:计划性任务调度(PendSV中断)
c
// 场景:预定实验时间结束,需要切换到下一个任务
// 但当前可能处于复杂的中断嵌套中
// 时间管理中断(每毫秒触发)
void SysTick_Handler(void)
{
// 更新空间站任务时间表
update_mission_timeline();
// 检查是否需要任务切换
if (current_experiment_time_expired()) {
// 标记需要切换,但不立即执行
// 因为可能有更紧急的中断正在处理
SCB->ICSR |= SCB_ICSR_PENDSVSET_Msk;
// 同时,设置一些上下文信息
set_next_experiment_type(BIOLOGICAL_STUDY);
set_scheduled_astronaut(ASTRONAUT_JONES);
// 立即返回,让紧急中断继续处理
}
}
// PendSV真正执行时的复杂情况
__asm void PendSV_Handler(void)
{
// 情况1:可能从不同的堆栈切换
// 检查当前使用的是MSP(主堆栈)还是PSP(进程堆栈)
MRS R0, CONTROL
TST R0, #0x02
BNE Using_PSP
// 使用主堆栈(在中断处理中发生PendSV)
MRS R0, MSP
B Save_Context
Using_PSP:
// 使用进程堆栈(在用户任务中发生PendSV)
MRS R0, PSP
Save_Context:
// 保存完整上下文(比普通中断更多寄存器)
// 需要保存R4-R11,因为硬件不会自动保存这些
STMDB R0!, {R4-R11, LR}
// 保存浮点寄存器(如果使用FPU)
// 空间站实验大量使用浮点计算
VSTMDB R0!, {S16-S31}
// 保存当前任务控制块
LDR R2, =Current_TCB
LDR R1, [R2]
STR R0, [R1]
// 复杂的调度决策
BL SpaceStation_Scheduler
// 调度器可能考虑:
// 1. 实验优先级
// 2. 宇航员疲劳度
// 3. 设备可用性
// 4. 地面指令优先级
// 5. 能源分配情况
// 加载下一个任务
LDR R2, =Current_TCB
LDR R1, [R2]
LDR R0, [R1]
// 恢复浮点上下文
VLDMIA R0!, {S16-S31}
// 恢复寄存器上下文
LDMIA R0!, {R4-R11, LR}
// 恢复正确的堆栈指针
MRS R1, CONTROL
TST R1, #0x02
BNE Restore_PSP
MSR MSP, R0
B Exit_PendSV
Restore_PSP:
MSR PSP, R0
Exit_PendSV:
// 特殊返回:可能改变处理器模式
BX LR
}
复杂交互:三中断同时发生的场景
c
// 时间:凌晨4:30,空间站正进行复杂实验
// 初始状态:日本宇航员进行晶体生长实验
// 实验使用专用炉具,需要精确温控
// 事件序列:
// 1. 普通硬件中断:温度传感器异常
void Temperature_IRQHandler(void)
{
// 炉温超过安全阈值
// 立即降低功率,防止设备损坏
reduce_heater_power(30%);
// 在温度处理中...
// 2. SVC中断:宇航员请求紧急协议覆盖
// 宇航员判断需要维持高温,手动覆盖
asm volatile("SVC #0x30"); // 紧急协议覆盖服务
// SVC处理程序需要:
// - 验证宇航员权限(3级授权)
// - 记录覆盖原因(审计日志)
// - 临时提高安全限制
// - 通知地面控制(异步)
// 3. 在处理SVC期间,PendSV挂起到期!
// 预定实验切换时间到(4:35切换到生物实验)
// 但PendSV优先级最低,等待当前处理完成
// 4. 返回温度中断处理
// 5. 返回晶体生长实验
}
// 6. 所有高优先级中断完成后
// 执行PendSV进行任务切换
// 但此时上下文极其复杂:
// - 炉具还在特殊覆盖模式下
// - 温度控制算法被修改
// - 审计日志未完全写入
// - 地面确认未收到
// PendSV处理程序必须:
// 1. 安全保存当前实验的所有状态
// (包括特殊的覆盖模式设置)
// 2. 通知下一个实验关于设备状态
// 3. 可能需要延迟切换,等待关键操作完成
操作系统中的具体实现复杂性
c
// 真实RTOS中的复杂中断交互
void Complex_Interrupt_Scenario(void)
{
// 假设优先级配置:
// 火警中断: 0x00 (最高)
// 压力泄露: 0x40
// 温度异常: 0x80
// SVC: 0xC0 (可配置)
// SysTick: 0xE0
// PendSV: 0xFF (最低)
// 场景:嵌套深度达4层
// 1. 主程序运行
// 2. 温度中断触发(优先级0x80)
// 3. 在温度中断中,压力泄露触发(0x40,更高)
// 4. 在压力处理中,宇航员执行SVC(0xC0,更低,被挂起)
// 5. 火警触发(0x00,最高,打断一切)
// 6. 火警处理中,SysTick到期(挂起PendSV)
// 处理顺序实际为:
// 主程序 → 温度中断 → 压力中断 → 火警中断
// → 返回压力中断 → 返回温度中断
// → 执行挂起的SVC → 返回主程序
// → 最后执行PendSV
}
// 中断管理的关键数据结构
typedef struct {
uint32_t saved_registers[16];
uint32_t fpu_registers[16];
uint8_t interrupt_nesting_level;
uint8_t pending_svc_requests;
uint8_t pending_pendsv;
uint32_t resource_locks;
void* current_tcb;
void* next_tcb;
uint32_t system_mode; // 用户/特权/调试
} Interrupt_Context_t;
// 中断入口处理
__asm void Interrupt_Entry_Handler(void)
{
// 判断中断来源
// 如果是PendSV,检查是否允许切换
// 如果是SVC,检查是否在中断上下文中(某些RTOS禁止)
// 如果是普通中断,更新嵌套计数器
// 维护中断嵌套计数器
LDR R0, =interrupt_nesting_count
LDR R1, [R0]
ADD R1, R1, #1
STR R1, [R0]
// 如果嵌套深度>3,可能触发特殊处理
CMP R1, #3
BGT High_Nesting_Mode
// 保存额外的上下文到任务控制块
// 因为可能需要在中断中切换任务
}
关键复杂性体现在:
- 中断可能在任何点嵌套发生
- 资源竞争需要在中断上下文中管理
- 任务切换必须保存完整状态(包括FPU、特殊寄存器)
- 系统必须保证实时性,同时不丢失任何关键事件
核心思想
- 普通硬件中断 :"快!有急事!" - 必须立即处理的事件
- SVC中断 :"我需要帮忙!" - 主动请求系统服务
- PendSV中断 :"该换班了,等手头工作完成" - 计划性、可延迟的调度
这三个机制共同确保了
- 实时性:紧急事件立即响应(硬件中断)
- 安全性:特权操作受控访问(SVC)
- 高效性:调度时机优化(PendSV)