初稿第3章Cortex-M3权威指南中断返回中断服务例程(ISR)中断事件主程序入栈出栈Time线程模式线程模式Handler模式(特权级)(特权级)(特权级)图3.8中断前后的状态转换但若CONTROL[O]=1(线程模式+用户级),则在中断响应的始末,both处理器模式和特权等极都要发生变化,如下图所示。中断返回中断服务例程(ISR)中断事件主程序入栈出栈Time线程模式线程模式Handler模式(用户级)(用户级)(特权级)图3.9中断前后的状态转换十特权等级切换CONTROL[o]只有在特权级下才能访问。用户级的程序如想进入特权级,通常都是使用一条“系统服务呼叫指令(SVC)”来触发“SVC异常”,该异常的服务例程可以选择修改CONTROL[O]。异常与中断Cortex-M3支持大量异常,包括16-4-1=11个系统异常,和最多240个外部中断一一简称IRQ。具体使用了这240个中断源中的多少个,则由芯片制造商决定。由外设产生的中断信号,除了SysTick的之外,全都连接到NVIC的中断输入信号线。典型情况下,处理器一般支持16到32个中断,当然也有在此之外的。作为中断功能的强化,NVIC还有一条NMI输入信号线。NMI究竞被拿去做什么,还要视处理器的设计而定。在多数情况下,NMI会被连接到一个看门狗定时器,有时也会是电压监视功能块,以便在电压掉至危险级别后警告处理器。NMI可以在任何时间被激活,甚至是在处理器刚刚复位之后。表3.4列出了Cortex-M3可以支持的所有异常。有一定数量的系统异常是用于fault处理的,它们可以由多种错误条件引发。NVIC还提供了一些fault状态寄存器,以便于fault服务例程找出导致异常的具体原因。44
Cortex‐M3 权威指南 初稿 第 3 章 图 3.8 中断前后的状态转换 但若 CONTROL[0]=1(线程模式+用户级),则在中断响应的始末,both 处理器模式和特 权等极都要发生变化,如下图所示。 图 3.9 中断前后的状态转换+特权等级切换 CONTROL[0]只有在特权级下才能访问。用户级的程序如想进入特权级,通常都是使 用一条“系统服务呼叫指令(SVC)”来触发“SVC 异常”,该异常的服务例程可以选择修改 CONTROL[0]。 异常与中断 Cortex‐M3 支持大量异常,包括 16‐4‐1=11 个系统异常,和最多 240 个外部中断——简 称 IRQ。具体使用了这 240 个中断源中的多少个,则由芯片制造商决定。由外设产生的中断 信号,除了 SysTick 的之外,全都连接到 NVIC 的中断输入信号线。典型情况下,处理器一般 支持 16 到 32 个中断,当然也有在此之外的。 作为中断功能的强化,NVIC 还有一条 NMI 输入信号线。NMI 究竟被拿去做什么,还要 视处理器的设计而定。在多数情况下,NMI 会被连接到一个看门狗定时器,有时也会是电压 监视功能块,以便在电压掉至危险级别后警告处理器。NMI 可以在任何时间被激活,甚至是 在处理器刚刚复位之后。 表 3.4 列出了 Cortex‐M3 可以支持的所有异常。有一定数量的系统异常是用于 fault 处理 的,它们可以由多种错误条件引发。NVIC 还提供了一些 fault 状态寄存器,以便于 fault 服务 例程找出导致异常的具体原因。 44
初稿第3章Cortex-M3权威指南表3.4Cortex-M3中的异常类型编号类型优先级简介0N/AN/A没有异常在运行1复位-3(最高)复位2-2NMI不可屏蔽中断(来自外部NMI输入脚)3-1硬(hard)fault所有被除能的fault,都将“上访”成硬fault。除能的原因包括当前被禁用,或者FAULTMASK被置位。4MemManage可编程存储器管理fault,MPU访问犯规以及访问非法位置均可引发,fault企图在“非执行区”取指也会引发此fault5总线fault可编程从总线系统收到了错误响应,原因可以是预取流产(Abort)或数据流产,或者企图访问协处理器6用法(usage)可编程由于程序错误导致的异常。通常是使用了一条无效指令,或者是Fault非法的状态转换,例如尝试切换到ARM状态7-10保留N/AN/A11SVCall可编程执行系统服务调用指令(SVC)引发的异常12调试监视器可编程调试监视器(断点,数据观察点,或者是外部调试请求13保留N/AN/A14PendSV可编程为系统设备而设的“可悬挂请求”(pendablerequest)15SysTick可编程系统滴答定时器(也就是周期性溢出的时基定时器—译注)16IRQ#0可编程外中断#017IRQ#1可编程外中断#1...255可编程外中断#239IRQ#239第7-9章给出了异常操作的详细信息。向量表s当一个发生的异常被cM3内核接受,对应的异常handler就会执行。为了决定handler的入口地址,CM3使用了“向量表查表机制”。这里使用一张向量表。向量表其实是一个WORD(32位整数)数组,每个下标对应一种异常,该下标元素的值则是该异常handler的入口地址。向量表的存储位置是可以设置的,通过NVIC中的一个重定位寄存器来指出向量表的地址。在复位后,该寄存器的值为0。因此,在地址0处必须包含一张向量表,用于初始时的异常分配。表3.5向量表结构异常类型表项地址异常向量偏移量00x00MSP的初始值1复位0x042NMI0x083OxOC硬fault40x10MemManagefault50x14总线fault45
Cortex‐M3 权威指南 初稿 第 3 章 表 3.4 Cortex‐M3 中的异常类型 编号 类型 优先级 简介 0 N/A N/A 没有异常在运行 1 复位 -3(最高) 复位 2 NMI -2 不可屏蔽中断(来自外部 NMI 输入脚) 3 硬(hard) fault -1 所有被除能的 fault,都将“上访”成硬 fault。除能的原因包括 当前被禁用,或者 FAULTMASK 被置位。 4 MemManage fault 可编程 存储器管理 fault,MPU 访问犯规以及访问非法位置均可引发。 企图在“非执行区”取指也会引发此 fault 5 总线 fault 可编程 从总线系统收到了错误响应,原因可以是预取流产(Abort)或 数据流产,或者企图访问协处理器 6 用法(usage) Fault 可编程 由于程序错误导致的异常。通常是使用了一条无效指令,或者是 非法的状态转换,例如尝试切换到 ARM 状态 7-10 保留 N/A N/A 11 SVCall 可编程 执行系统服务调用指令(SVC)引发的异常 12 调试监视器 可编程 调试监视器(断点,数据观察点,或者是外部调试请求 13 保留 N/A N/A 14 PendSV 可编程 为系统设备而设的“可悬挂请求”(pendable request) 15 SysTick 可编程 系统滴答定时器(也就是周期性溢出的时基定时器——译注) 16 IRQ #0 可编程 外中断#0 17 IRQ #1 可编程 外中断#1 . . . . 255 IRQ #239 可编程 外中断#239 第 7‐9 章给出了异常操作的详细信息。 向量表 s 当一个发生的异常被 CM3 内核接受,对应的异常 handler 就会执行。为了决定 handler 的入 口地址,CM3 使用了“向量表查表机制”。这里使用一张向量表。向量表其实是一个 WORD (32 位整数)数组,每个下标对应一种异常,该下标元素的值则是该异常 handler 的入口地 址。向量表的存储位置是可以设置的,通过 NVIC 中的一个重定位寄存器来指出向量表的地 址。在复位后,该寄存器的值为 0。因此,在地址 0 处必须包含一张向量表,用于初始时的 异常分配。 表 3.5 向量表结构 异常类型 表项地址 偏移量 异常向量 0 0x00 MSP 的初始值 1 0x04 复位 2 0x08 NMI 3 0x0C 硬 fault 4 0x10 MemManage fault 5 0x14 总线 fault 45
初稿第3章Cortex-M3权威指南60x18用法fault保留7-100x1c-0x28110x2cSVC120x30调试监视器保留130x34140x38Pendsv150x3cSysTick160x40IRQ#O170x44IRQ#118-255Ox48-0x3FFIRQ#2-#239举个例子,如果发生了异常11(SVC),则NVIC会计算出偏移移量是11x4=Ox2C,然后从那里取出服务例程的入口地址并跳入。0号异常的功能则是个另类,它并不是什么入口地址,而是给出了复位后MSP的初值。栈内存操作在Cortex-M3中,除了可以使用PUSH和POP指令来处理堆栈外,内核还会在异常处理的始末自动地执行PUSH与POP操作。本节让我们来检视一下具体的动作,第9章则讨论异常处理时的自动栈操作。堆栈的基本操作笼统地讲,堆栈操作就是对内存的读写操作,但是其地址由SP给出。寄存器的数据通过PUSH操作存入堆栈,以后用POP操作从堆栈中取回。在PUSH与POP的操作中,SP的值会按堆栈的使用法则自动调整,以保证后续的PUSH不会破坏先前PUSH进去的内容。堆栈的功能就是把寄存器的数据放入内存,以便将来能恢复之一一当一个任务或一段子程序执行完毕后恢复。正常情况下,PUSH与POP必须成对使用,而且参与的寄存器,不论是身份还是先后顺序都必须完全一致。当PUSH/POP指令执行时,SP指针的值也根着自减自增。(主程序)R0=X,R1=Y,R2=ZBLFx1Fx1PUSH(RO):把RO存入栈&调整SEPUSH(R1];把R1存入栈&调整SPPUSH(R2]:把R2存入栈&调整SP;执行Fx1的功能,中途可以改变RO-R2的值"POP(R2);恢复R2早先的值&再次调整SPPOP(R1);恢复R1早先的值&再次调整SPPOP(RO);恢复RO早先的值&再次调整SPLR;返回BX;回到主程序R0=X,R1=Y,R2=Z(调用Fx1的前后R0-R2的值没有被改变)46
Cortex‐M3 权威指南 初稿 第 3 章 6 0x18 用法 fault 7‐10 0x1c‐0x28 保留 11 0x2c SVC 12 0x30 调试监视器 13 0x34 保留 14 0x38 PendSV 15 0x3c SysTick 16 0x40 IRQ #0 17 0x44 IRQ #1 18‐255 0x48‐0x3FF IRQ #2 ‐ #239 举个例子,如果发生了异常 11(SVC),则 NVIC 会计算出偏移移量是 11x4=0x2C,然后 从那里取出服务例程的入口地址并跳入。0 号异常的功能则是个另类,它并不是什么入口地 址,而是给出了复位后 MSP 的初值。 栈内存操作 在 Cortex‐M3 中,除了可以使用 PUSH 和 POP 指令来处理堆栈外,内核还会在异常处理 的始末自动地执行 PUSH 与 POP 操作。本节让我们来检视一下具体的动作,第 9 章则讨论异 常处理时的自动栈操作。 堆栈的基本操作 笼统地讲,堆栈操作就是对内存的读写操作,但是其地址由 SP 给出。寄存器的数据通 过 PUSH 操作存入堆栈,以后用 POP 操作从堆栈中取回。在 PUSH 与 POP 的操作中,SP 的 值会按堆栈的使用法则自动调整,以保证后续的 PUSH 不会破坏先前 PUSH 进去的内容。 堆栈的功能就是把寄存器的数据放入内存,以便将来能恢复之——当一个任务或一段子 程序执行完毕后恢复。正常情况下,PUSH 与 POP 必须成对使用,而且参与的寄存器,不论 是身份还是先后顺序都必须完全一致。当 PUSH/POP 指令执行时,SP 指针的值也根着自减/ 自增。 .(主程序) ; R0=X, R1=Y, R2=Z BL Fx1 Fx1 PUSH {R0 } ;把 R0 存入栈 & 调整 SP PUSH {R1} ;把 R1 存入栈 & 调整 SP PUSH {R2} ;把 R2 存入栈 & 调整 SP . ;执行 Fx1 的功能,中途可以改变 R0-R2 的值 POP {R2} ;恢复 R2 早先的值 & 再次调整 SP POP {R1} ;恢复 R1 早先的值 & 再次调整 SP POP {R0} ;恢复 R0 早先的值 & 再次调整 SP BX LR ;返回 ;回到主程序 46 ;R0=X, R1=Y, R2=Z (调用 Fx1 的前后 R0-R2 的值没有被改变)
初稿第3章Cortex-M3权威指南图3.10基本的堆栈操作:每次处理单个寄存器译者添加:如果参与的寄存器比较多,这种PUSH和POP岂不是又臭又长?放心,PUSH/POP指令足够体贴,支持一次操作多个寄存器。像这样:;压入R0-R2PUSH(R0-R2)PUSH(R3-R5,R8,R12);压入R3-R5,R8,以及R12在POP时,可以如下操作:POP;弹出RO-R2(R0-R2]POP{R3-R5,R8,R12)弹出R3-R5,R8,以及R12注意:不管在寄存器列表中,寄存器的序号是以什么顺序给出的,汇编器都将把它们升序排序。然后PUSH指令按照从大到小的顺序依次入栈,POP则按从小到大的顺序依次出栈。如果不按升序写寄存器,有些汇编器可能会给出一个语法错误。PUSH/POP对子还有这样一种特殊形式,形如PUSH(RO-R3,LR)POP(RO-R3,PC)请注意:POP的最后一个寄存器是PC,并不是先前PUSH的LR。这其实是一个返回的小技巧。因为总要把先前LR的值弹出来,再使用此值返回,干脆绕过LR,直接传给PC!那不怕LR的值没有被恢复吗?不怕,因为LR在子程序返回时的唯一用处就是提供返回地址,在返回后,先前保存的返回地址就没有利用价值了,所以只要PC得到了正确的值,不恢复也没关系。PUSH指令等效于与使用R13作为地址指针的STMDB指令,而POP指令则等效于使用R13作为地址指针的LDMIA指令——STMDB/LDMIA还可以使用其它寄存器作为地址指针。至于这两个指令的细节,后续章节讲到指令系统时再介绍。图3.10中的子程序返回后,R0-R2的值仍然是执行前的一一仿佛什么事都没有发生一样。Cortex-M3堆栈的实现Cortex-M3使用的是“向下生长的满栈”模型。堆栈指针SP指向最后一个被压入堆栈的32位数值。在下一次压栈时,SP先自减4,再存入新的数值。RO0x12345678PUSH (RO)OccupiedOccupiedOccupiedOccupiedMemorySPOccupiedLastpusheddataaddressSPOx12345678StackgrowthPOP操作刚好相反:先从SP指针处读出上一次被压入的值,再把SP指针自增4。47
Cortex‐M3 权威指南 初稿 第 3 章 图 3.10 基本的堆栈操作:每次处理单个寄存器 译者添加: 如果参与的寄存器比较多,这种 PUSH 和 POP 岂不是又臭又长?放心,PUSH/POP 指令 足够体贴,支持一次操作多个寄存器。像这样: PUSH {R0-R2} ;压入 R0-R2 PUSH {R3-R5,R8, R12} ;压入 R3-R5,R8,以及 R12 在 POP 时,可以如下操作: POP {R0-R2} ;弹出 R0-R2 POP {R3-R5,R8, R12} ;弹出 R3-R5,R8,以及 R12 注意:不管在寄存器列表中,寄存器的序号是以什么顺序给出的,汇编器都将把它们升 序排序。然后 PUSH 指令按照从大到小的顺序依次入栈,POP 则按从小到大的顺序依次出栈。 如果不按升序写寄存器,有些汇编器可能会给出一个语法错误。 PUSH/POP 对子还有这样一种特殊形式,形如 PUSH {R0-R3, LR} POP {R0-R3, PC} 请注意:POP 的最后一个寄存器是 PC,并不是先前 PUSH 的 LR。这其实是一个返回的小 技巧。因为总要把先前 LR 的值弹出来,再使用此值返回,干脆绕过 LR,直接传给 PC!那不 怕 LR 的值没有被恢复吗?不怕,因为 LR 在子程序返回时的唯一用处就是提供返回地址,在 返回后,先前保存的返回地址就没有利用价值了,所以只要 PC 得到了正确的值,不恢复也 没关系。 PUSH 指令等效于与使用 R13 作为地址指针的 STMDB 指令,而 POP 指令则等效于使用 R13 作为地址指针的 LDMIA 指令——STMDB/LDMIA 还可以使用其它寄存器作为地址指针。至 于这两个指令的细节,后续章节讲到指令系统时再介绍。 图 3.10 中的子程序返回后,R0‐R2 的值仍然是执行前的——仿佛什么事都没有发生一样。 Cortex-M3 堆栈的实现 Cortex‐M3 使用的是“向下生长的满栈”模型。堆栈指针 SP 指向最后一个被压入堆栈的 32 位数值。在下一次压栈时,SP 先自减 4,再存入新的数值。 POP 操作刚好相反:先从 SP 指针处读出上一次被压入的值,再把 SP 指针自增 4。 47
初稿第3章Cortex-M3权威指南POP(RO)OccupiedOccupiedOccupiedOccupiedMemoryOccupiedOccupiedSPaddressSP0x123456780x12345678RoRO0x12345678译注[9]:虽然POP后被压入的数值还保存在栈中,但它已经无效了,因为为下次的PUSH将覆盖它的值!在进入ISR时,CM3会自动把一些寄存器压栈,这里使用的是进入ISR之前使用的SP指针(MSP或者是PSP)。离开ISR后,只要ISR没有更改过CONTROL[1],就依然使用先前的SP指针来执行出栈操作。再论Cortex-M3的双堆栈机制我们已经知道了CM3的堆栈是分为两个:主堆栈和进程堆栈,CONTROLI11决定如何选择。当CONTROL[1]=O时,只使用MSP,此时用户程序和异常handler共享同一个堆栈。这也是复位后的缺省使用方式。InterruptExitInterruptServiceRoutine (ISR)InterruptEventMainProgramiStackingUnstackingTimeThread ModeHandlerModeThread Mode(UseMSP)(UseMSP)(UseMSP)图3.15CONTROL[1]=0时的堆栈使用情况当CONTROL[1]=1时,线程模式将不再使用PSP,而改用MSP(handler模式永远使用MSP)。[译注10]:此时,进入异常时的自动压栈使用的是进程堆栈,进入异常handler后才自动改为MSP,退出异常时切换回PSP,并且从进程堆栈上弹出数据。InterruptExitInterruptServiceRoutine (ISR)InterruptEventMainProgramIStackingUnstackingTimeThreadModeHandlerModeThreadMode(UsePSP)(UseMSP)(UsePSP)图3.16CONTROL[1]=0时的堆栈切换情况在特权级下,可以指定具体的堆栈指针,而不受当前使用堆栈的限制,示例代码如下:48
Cortex‐M3 权威指南 初稿 第 3 章 译注[9]:虽然 POP 后被压入的数值还保存在栈中,但它已经无效了,因为为下次的 PUSH 将覆盖它的值! 在进入 ISR 时,CM3 会自动把一些寄存器压栈,这里使用的是进入 ISR 之前使用的 SP 指针(MSP 或者是 PSP)。离开 ISR 后,只要 ISR 没有更改过 CONTROL[1],就依然使用先前 的 SP 指针来执行出栈操作。 再论 Cortex-M3 的双堆栈机制 我们已经知道了 CM3 的堆栈是分为两个:主堆栈和进程堆栈,CONTROL[1]决定如何选 择。 当 CONTROL[1]=0 时,只使用 MSP,此时用户程序和异常 handler 共享同一个堆栈。这 也是复位后的缺省使用方式。 图 3.15 CONTROL[1]=0 时的堆栈使用情况 当 CONTROL[1]=1 时,线程模式将不再使用 PSP,而改用 MSP(handler 模式永远使用 MSP)。 [译注 10]:此时,进入异常时的自动压栈使用的是进程堆栈,进入异常 handler 后才自动改为 MSP,退出 异常时切换回 PSP,并且从进程堆栈上弹出数据。 图 3.16 CONTROL[1]=0 时的堆栈切换情况 在特权级下,可以指定具体的堆栈指针,而不受当前使用堆栈的限制,示例代码如下: 48