MCU运行原理-Cortex-M3内核

虽然使用Cortex-M3的MCU有两三几年了,但主要是基于c语言开发,而且因为网络上资源非常多,官方提供的project也比较多,导致并未对MCU的运行原理进行详细的分析过。最近在使用uC-OSII做一些功能,发现如果想把uC-OSII透彻的使用好,必须要有一些MCU运行机理的知识,比如:

任务如何切换,切换过程要做哪些工作?

任务堆栈如何设置,任务堆栈用来存放哪些数据?

mdk工程项目中的heap、stack如何设置?

细想这些,都涉及到MCU的详细运行原理,或者如果对于cpu来讲,应该是微机原理方面的知识,下面把本人对于MCU运行原理的粗鄙理解记录下,望大牛指教。

相关知识主要参考:

Cortex-M3权威指南(中文).pdf

CM3技术参考手册.pdf

1、Cortex-M3 处理器内核 与 基于Cortex-M3的MCU

Cortex‐M3处理器内核是单片机的中央处理单元(CPU)。完整的基于CM3的MCU还需要
很多其它组件。在芯片制造商得到CM3处理器内核的使用授权后,它们就可以把CM3内核用
在自己的硅片设计中,添加存储器,外设,I/O以及其它功能块。不同厂家设计出的单片机
会有不同的配置,包括存储器容量、类型、外设等都各具特色

2、Cortex-M3架构简化视图

NVIC:向量中断控制器,负责中断控制以及中断处理事务

Instructin fetch unit :取指令单元

Decoder:指令解码

ALU:算术逻辑单元,

中央处理器(CPU)的执行单元,是所有中央处理器的核心组成部分,由"And Gate"(与门) 和"Or Gate"(或门)构成的算术逻辑单元,主要功能是进行二位元的算术运算

Trace Interface:跟踪接口

Debug System:调试系统

Memory Protection Unit:内存保护单元

Bus Interconnect:总线接口

外设:code memory、memory system等

从该功能框图知道,MCU的最主要处理单元及ALU,该单元是实现计算处理的核心单元,其他都是为该单元服务。

3、ALU如何进行计算处理

ALU是逻辑单元组合,其通过对相关内部寄存器操作,实现逻辑运算。运算处理涉及的寄存器组包括如下:

Cortex‐M3 处理器拥有R0‐R15 的寄存器组。其中R13 作为堆栈指针SP。SP 有两个,但在同一时刻只能有一个可以看到,这也就是所谓的“banked”寄存器。

R0-R12:通用寄存器

R0‐R12 都是32 位通用寄存器,用于数据操作。但是注意:绝大多数16 位Thumb 指令只能访

问R0‐R7,而32 位Thumb‐2 指令可以访问所有寄存器。

Banked R13: 两个堆栈指针

Cortex‐M3 拥有两个堆栈指针,然而它们是banked,因此任一时刻只能使用其中的一个。

主堆栈指针(MSP):复位后缺省使用的堆栈指针,用于操作系统内核以及异常处理例程(包

括中断服务例程)

进程堆栈指针(PSP):由用户的应用程序代码使用。

堆栈指针的最低两位永远是0,这意味着堆栈总是4 字节对齐的。

在ARM 编程领域中,凡是打断程序顺序执行的事件,都被称为异常(exception)。除了外部中断外,当有指令执

行了“非法操作”,或者访问被禁的内存区间,因各种错误产生的fault,以及不可屏蔽中断发生时,都会打断程序的

执行,这些情况统称为异常。在不严格的上下文中,异常与中断也可以混用。另外,程序代码也可以主动请求进入

异常状态的(常用于系统调用)。

R14:连接寄存器

当呼叫一个子程序时,由R14 存储返回地址

不像大多数其它处理器,ARM 为了减少访问内存的次数(访问内存的操作往往要3 个以上指令周期,带MMU

和cache 的就更加不确定了),把返回地址直接存储在寄存器中。这样足以使很多只有1 级子程序调用的代码无需访

问内存(堆栈内存),从而提高了子程序调用的效率。如果多于1 级,则需要把前一级的R14 值压到堆栈里。在ARM

上编程时,应尽量只使用寄存器保存中间结果,迫不得以时才访问内存。在RISC 处理器中,为了强调访内操作越过

了处理器的界线,并且带来了对性能的不利影响,给它取了一个专业的术语:溅出。

R15:程序计数寄存器

指向当前的程序地址。如果修改它的值,就能改变程序的执行流(很多高级技巧就在这里面—

—译注)。

特殊功能寄存器

Cortex‐M3 还在内核水平上搭载了若干特殊功能寄存器,包括

程序状态字寄存器组(PSRs)

中断屏蔽寄存器组(PRIMASK, FAULTMASK, BASEPRI)

控制寄存器(CONTROL)

                            Cortex-M3 中的特殊功能寄存器集合

表2.1 寄存器及其功能

寄存器 功能

xPSR 记录ALU 标志(0 标志,进位标志,负数标志,溢出标志),执行状态,以及当前正服务的中断号

PRIMASK 除能所有的中断——当然了,不可屏蔽中断(NMI)才不甩它呢。

FAULTMASK 除能所有的fault——NMI 依然不受影响,而且被除能的faults 会“上访”,见后续章节的叙述。

BASEPRI 除能所有优先级不高于某个具体数值的中断。

CONTROL 定义特权状态(见后续章节对特权的叙述),并且决定使用哪一个堆栈指针

4、寄存器组的相关说明

堆栈的PUSH 与POP

堆栈是一种存储器的使用模型。它由一块连续的内存,以及一个栈顶指针组成,用于实现“先进后出”的缓冲区。其最典型的应用,就是在数据处理前先保存寄存器的值,再在处理任务完成后从中恢复先前保护的这些值。

在执行PUSH 和POP 操作时,那个通常被称为SP 的地址寄存器,会自动被调整,以避免后续的操作破坏先前的数据。本书的后续章节还要围绕着堆栈展开更详细的论述。

在Cortex‐M3 中,有专门的指令负责堆栈操作——PUSH 和POP。它俩的汇编语言语法

如下例所演示

PUSH {R0} ; *(--R13)=R0。R13 是long*的指针

POP {R0} ; R0= *R13++

请注意后面C 程序风格的注释,Cortex‐M3 中的堆栈以这种方式来使用的,这就是所谓

的“向下生长的满栈”(本章后面在讲到堆栈内存操作时还要展开论述)。因此,在PUSH 新数据时,堆栈指针先减一个单元。通常在进入一个子程序后,第一件事就是把寄存器的值先PUSH 入堆栈中,在子程序退出前再POP 曾经PUSH 的那些寄存器。另外,PUSH 和POP 还能一次操作多个寄存器,如下所示:

subroutine_1

PUSH {R0-R7, R12, R14} ; 保存寄存器列表

… ; 执行处理

POP {R0-R7, R12, R14} ; 恢复寄存器列表

BX R14 ; 返回到主调函数

在程序中为了突出重点,你可以使用SP 表示R13。在程序代码中,both MSP 和PSP 都被称为R13/SP。不过,我们可以通过MRS/MSR 指令来指名道姓地访问具体的堆栈指针。

MSP,亦写作SP_main,这是复位后缺省使用堆栈指针,服务于操作系统内核和异常服务例程;而PSP,亦写作SP_process,典型地用于普通的用户线程中。

寄存器的PUSH 和POP 操作永远都是4 字节对齐的——也就是说他们的地址必须是0x4,0x8,0xc,……。这样一来,R13 的最低两位被硬线连接到0,并且总是读出0(Read As Zero)。

连接寄存器R14

R14 是连接寄存器(LR)。在一个汇编程序中,你可以把它写作both LR 和R14。LR 用于在调用子程序时存储返回地址。例如,当你在使用BL(分支并连接,Branch and Link)指令时,就自动填充LR 的值。

main ;主程序

BL function1 ; 使用“分支并连接”指令呼叫function1

            ; PC= function1,并且LR=main 的下一条指令地址

Function1

… ; function1 的代码

BX LR ; 函数返回(如果function1 要使用LR,必须在使用前PUSH,

; 否则返回时程序就可能跑飞了——译注)

尽管PC 的LSB 总是0(因为代码至少是字对齐的),LR 的LSB 却是可读可写的。这是历史遗留的产物。在以前,由位0 来指示ARM/Thumb 状态。因为其它有些ARM 处理器支持ARM 和Thumb 状态并存,为了方便汇编程序移植,CM3 需要允许LSB 可读可写。

程序计数器R15

R15 是程序计数器,在汇编代码中你也可以使用名字“PC”来访问它。因为CM3 内部使用了指令流水线,读PC 时返回的值是当前指令的地址+4。比如说:

0x1000: MOV R0, PC ; R0 = 0x1004如果向PC 中写数据,就会引起一次程序的分支(但是不更新LR 寄存器)。CM3 中的指令至少是半字对齐的,所以PC 的LSB 总是读回0。然而,在分支时,无论是直接写PC 的值还是使用分支指令,都必须保证加载到PC 的数值是奇数(即LSB=1),用以表明这是在Thumb 状态下执行。倘若写了0,则视为企图转入ARM 模式,CM3 将产生一个fault 异常。

特殊功能寄存器组

Cortex‐M3 中的特殊功能寄存器包括:

�� 程序状态寄存器组(PSRs 或曰xPSR)

�� 中断屏蔽寄存器组(PRIMASK, FAULTMASK,以及BASEPRI)

�� 控制寄存器(CONTROL)

它们只能被专用的MSR 和MRS 指令访问,而且它们也没有存储器地址。

MRS <gp_reg>, <special_reg> ;读特殊功能寄存器的值到通用寄存器

MSR <special_reg>, <gp_reg> ;写通用寄存器的值到特殊功能寄存器

程序状态寄存器(PSRs 或曰PSR)

程序状态寄存器在其内部又被分为三个子状态寄存器:

�� 应用程序 PSR(APSR)

�� 中断号 PSR(IPSR)

�� 执行 PSR(EPSR)

通过MRS/MSR 指令,这3 个PSRs 即可以单独访问,也可以组合访问(2 个组合,3 个

组合都可以)。当使用三合一的方式访问时,应使用名字“xPSR”或者“PSR”。

图3.3 Cortex‐M3 中的程序状态寄存器(xPSR)

图3.4 合体后的程序状态寄存器(xPSR)

PRIMASK, FAULTMASK 和BASEPRI

这三个寄存器用于控制异常的使能和除能。

表3.2 Cortex-M3 的屏蔽寄存器s

对于时间‐关键任务而言,PRIMASK 和BASEPRI 对于暂时关闭中断是非常重要的。而FAULTMASK 则可以被OS 用于暂时关闭fault 处理机能,这种处理在某个任务崩溃时可能需要。因为在任务崩溃时,常常伴随着一大堆faults。在系统料理“后事”时,通常不再需要响应这些fault——人死帐清。总之FAULTMASK 就是专门留给OS 用的。

要访问PRIMASK, FAULTMASK 以及BASEPRI,同样要使用MRS/MSR 指令,如:

MRS R0, BASEPRI ;读取BASEPRI 到R0 中

MRS R0, FAULTMASK ;似上

MRS R0, PRIMASK ;似上

MSR BASEPRI, R0 ;写入R0 到BASEPRI 中

MSR FAULTMASK, R0 ;似上

MSR PRIMASK, R0 ;似上

只有在特权级下,才允许访问这3 个寄存器。

其实,为了快速地开关中断,CM3 还专门设置了一条CPS 指令,有4 种用法

CPSID I ;PRIMASK=1, ;关中断

CPSIE I ;PRIMASK=0, ;开中断

CPSID F ;FAULTMASK=1, ;关异常

CPSIE F ;FAULTMASK=0 ;开异常

控制寄存器(CONTROL)

控制寄存器用于定义特权级别,还用于选择当前使用哪个堆栈指针。

表3.3 Cortex‐M3 的CONTROL 寄存器

CONTROL[1]

在Cortex‐M3 的handler 模式中,CONTROL[1]总是0。在线程模式中则可以为0 或1。仅当处于特权级的线程模式下,此位才可写,其它场合下禁止写此位。改变处理器的模式也有其它的方式:在异常返回时,通过修改LR 的位2,也能实现模式切换。这将在第5章中展开论述。

CONTROL[0]

仅当在特权级下操作时才允许写该位。一旦进入了用户级,唯一返回特权级的途径,就

是触发一个(软)中断,再由服务例程改写该位。

CONTROL 寄存器也是通过MRS 和MSR 指令来操作的:

MRS R0, CONTROL

MSR CONTROL, R0

5、操作模式

Cortex‐M3 支持2 个模式和两个特权等级。

图3.6 操作模式和特权等级

当处理器处在线程状态下时,既可以使用特权级,也可以使用用户级;另一方面,handler模式总是特权级的。在复位后,处理器进入线程模式+特权级。在线程模式+用户级下,对系统控制空间(SCS)的访问将被阻止——该空间包含了配置寄存器s 以及调试组件的寄存器s。除此之外,还禁止使用MSR 访问刚才讲到的特殊功能寄存器——除了APSR 有例外。谁若是以身试法,则将fault 伺候。在特权级下的代码可以通过置位CONTROL[0]来进入用户级。而不管是任何原因产生了任何异常,处理器都将以特权级来运行其服务例程,异常返回后将回到产生异常之前的特权级。用户级下的代码不能再试图修改CONTROL[0]来回到特权级。它必须通过一个异常handler,由那个异常handler 来修改CONTROL[0],才能在返回到线程模式后拿到特权级。

图3.7 特权级和处理器模式的改变图

把代码按特权级和用户极分开对待,有利于使架构更加安全和健壮。例如,当某个用户

代码出问题时,不会让它成为害群之马,因为用户级的代码是禁止写特殊功能寄存器和NVIC

中寄存器的。另外,如果还配有MPU,保护力度就更大,甚至可以阻止用户代码访问不属

于它的内存区域。

为了避免系统堆栈因应用程序的错误使用而毁坏,你可以给应用程序专门配一个堆栈,

不让它共享操作系统内核的堆栈。在这个管理制度下,运行在线程模式的用户代码使用PSP,

而异常服务例程则使用MSP。这两个堆栈指针的切换是全自动的,就在出入异常服务例程

时由硬件处理。第8 章将详细讨论此主题。

如前所述,特权等级和堆栈指针的选择均由CONTROL 负责。当CONTROL[0]=0 时,在异

常处理的始末,只发生了处理器模式的转换,如下图所示。

图3.8 中断前后的状态转换

但若CONTROL[0]=1(线程模式+用户级),则在中断响应的始末,both 处理器模式和特

权等极都要发生变化,如下图所示。

图3.9 中断前后的状态转换+特权等级切换

CONTROL[0]只有在特权级下才能访问。用户级的程序如想进入特权级,通常都是使

用一条“系统服务呼叫指令(SVC)”来触发“SVC 异常”,该异常的服务例程可以选择修改

CONTROL[0]。

6、异常与中断

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 服务

例程找出导致异常的具体原因。

表3.4 Cortex‐M3 中的异常类型

向量表s

当一个发生的异常被CM3 内核接受,对应的异常handler 就会执行。为了决定handler 的入

口地址,CM3 使用了“向量表查表机制”。这里使用一张向量表。向量表其实是一个WORD

(32 位整数)数组,每个下标对应一种异常,该下标元素的值则是该异常handler 的入口地

址。向量表的存储位置是可以设置的,通过NVIC 中的一个重定位寄存器来指出向量表的地

址。在复位后,该寄存器的值为0。因此,在地址0 处必须包含一张向量表,用于初始时的

异常分配。

表3.5 向量表结构

举个例子,如果发生了异常11(SVC),则NVIC 会计算出偏移移量是11x4=0x2C,然后

从那里取出服务例程的入口地址并跳入。0 号异常的功能则是个另类,它并不是什么入口地

址,而是给出了复位后MSP 的初值。

7、栈内存操作

在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 ;返回

;回到主程序

;R0=X, R1=Y, R2=Z (调用Fx1 的前后R0-R2 的值没有被改变)

图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。

译注[9]:虽然POP 后被压入的数值还保存在栈中,但它已经无效了,因为为下次的PUSH 将覆盖它的值!

在进入ISR 时,CM3 会自动把一些寄存器压栈,这里使用的是进入ISR 之前使用的SP

指针(MSP 或者是PSP)。离开ISR 后,只要ISR 没有更改过CONTROL[1],就依然使用先前

的SP 指针来执行出栈操作。

8、中断的具体行为

中断/异常的响应序列

当CM3开始响应一个中断时,会在它看不见的体内奔涌起三股暗流:

�� 入栈: 把8个寄存器的值压入栈

�� 取向量:从向量表中找出对应的服务程序入口地址

�� 选择堆栈指针MSP/PSP,更新堆栈指针SP,更新连接寄存器LR,更新程序计数器PC

入栈

响应异常的第一个行动,就是自动保存现场的必要部分:依次把xPSR, PC, LR, R12以及

R3‐R0由硬件自动压入适当的堆栈中:如果当响应异常时,当前的代码正在使用PSP,则压入

PSP,即使用线程堆栈;否则压入MSP,使用主堆栈。一旦进入了服务例程,就将一直使用

主堆栈。

假设入栈开始时,SP的值为N,则在入栈后,堆栈内部的变化如表9.1表示。又因为AHB

接口上的流水线操作本质,地址和数据都在经过一个流水线周期之后才进入。另外,这种入

栈在机器的内部,并不是严格按堆栈操作的顺序的——但是机器会保证:正确的寄存器将被

保存到正确的位置,如图9.1和表9.1的第3列所示。

表9.1 入栈顺序以及入栈后堆栈中的内容

CM3在看不见的内部打乱了入栈的顺序,这是有深层次的原因的。先把PC与xPSR的值保

存,就可以更早地启动服务例程指令的预取——因为这需要修改PC;同时,也做到了在早期

就可以更新xPSR中IPSR位段的值。

细心的读者一定在猜测:为啥袒护R0‐R3以及R12呢,R4‐R11就是下等公民?原来,在ARM

上,有一套的C函数调用标准约定(《C/C++ Procedure Call Standard for the ARM Architecture》,

AAPCS, Ref5)。个中原因就在它上面:它使得中断服务例程能用C语言编写,编译器优先使

用被入栈的寄存器来保存中间结果(当然,如果程序过大也可能要用到R4‐R11,此时编译器

负责生成代码来push它们。但是,ISR应该短小精悍,不要让系统如此操心——译者注)。

如果读者再仔细看,会发现R0‐R3, R12是最后被压进去的。这里也有一番良苦用心:为

的是可以更容易地使用SP基址来索引寻址,(以及为了LDM等多重加载指令,因为LDM必

须加载地址连续的一串数据)。参数的传递也是受益者:使之可以方便地通过压入栈的R0‐R3

取出(主要为系统软件所利用,多见于SVC与PendSV中的参数传递)。

取向量

当数据总线(系统总线)正在为入栈操作而忙得团团转时,指令总线(I‐Code总线)可

不是凉快地坐着看热闹——它正在为响应中断紧张有序地执行另一项重要的任务:从向量表

中找出正确的异常向量,然后在服务程序的入口处预取指。由此可以看到各自都有专用总线

的好处:入栈与取指这两个工作能同时进行。

更新寄存器

在入栈和取向量的工作都完毕之后,执行服务例程之前,还要更新一系列的寄存器:

�� SP:在入栈中会把堆栈指针(PSP或MSP)更新到新的位置。在执行服务例程后,

将由MSP负责对堆栈的访问。

�� PSR:IPSR位段(地处PSR的最低部分)会被更新为新响应的异常编号。

�� PC:在向量取出完毕后,PC将指向服务例程的入口地址,

�� LR:LR的用法将被重新解释,其值也被更新成一种特殊的值,称为“EXC_RETURN”,

并且在异常返回时使用。EXC_RETURN的二进制值除了最低4位外全为1,而其最低4

位则有另外的含义(后面讲到,见表9.3和表9.4)。

以上是在响应异常时通用寄存器的变化。另一方面,在NVIC中,也伴随着更新了与之相

关的若干寄存器。例如,新响应异常的悬起位将被清除,同时其活动位将被置位。

异常返回

当异常服务例程执行完毕后,需要很正式地做一个“异常返回”动作序列,从而恢复先

前的系统状态,才能使被中断的程序得以继续执行。从形式上看,有3种途径可以触发异常

返回序列,如表9.2所示;不管使用哪一种,都需要用到先前储的LR的值。

有些处理器使用特殊的返回指令来标示中断返回,例如8051就使用reti。但是在CM3中,

是通过把EXC_RETURN往PC里写来识别返回动作的。因此,可以使用上述的常规返回指令,

从而为使用C语言编写服务例程扫清了最后的障碍(无需特殊的编译器命令,如__interrupt)。

在启动了中断返回序列后,下述的处理就将进行:

1. 出栈:先前压入栈中的寄存器在这里恢复。内部的出栈顺序与入栈时的相对应,堆栈指

针的值也改回去。

2. 更新NVIC寄存器:伴随着异常的返回,它的活动位也被硬件清除。对于外部中断,倘若

中断输入再次被置为有效,悬起位也将再次置位,新一次的中断响应序列也可随之再次开始。

嵌套的中断

在CM3内核以及NVIC的深处,就已经内建了对中断嵌套的全力支持,根本无需使用用汇

编写封皮代码(wrapper code)。事实上,我们要做的就只是为每个中断适当地建立优先级,

不用再操心别的。表现在:

第一、 NVIC和CM3处理器会为我们排出优先级解码的顺序。因此,在某个异常正在响

应时,所有优先级不高于它的异常都不能抢占之,而且它自己也不能抢占自己。

第二、 有了自动入栈和出栈,就不用担心在中断发生嵌套时,会使寄存器的数据损毁,

从而可以放心地执行服务例程。

然而,有一件事情却必须更加一丝不苟地处理了,否则有功能紊乱甚至死机的危险,这

就是计算主堆栈容量的最小安全值。我们已经知道,所有服务例程都只使用主堆栈。所以当

中断嵌套加深时,对主堆栈的压力会增大:每嵌套一级,就至少再需要8个字,即32字节的

堆栈空间——而且这还没算上ISR对堆栈的额外需求,并且何时嵌套多少级也是不可预料的。

如果主堆栈的容量本来就已经所剩无几了,中断嵌套又突然加深,则主堆栈有被用穿的凶险。

这就好像已经表现出了高血压危象的时候,情绪又一激动,就容易导致中风一般。在这里,

堆栈溢出同样是很致命的,它会使入栈数据与主堆栈前面的数据区发生混迭,使这些数据被

破坏;若服务例程又更改了混迭区的数据,则堆栈内容被破坏。这么一来在执行中断返回后,

系统极可能功能紊乱,甚至当场被一击必杀——程序跑飞/死机!

另一个要注意的,是相同的异常是不允许重入的。因为每个异常都有自己的优先级,并

且在异常处理期间,同级或低优先级的异常是要阻塞的,因此对于同一个异常,只有在上次

实例的服务例程执行完毕后,方可继续响应新的请求。由此可知,在SVC服务例程中,就不

得再使用SVC指令,否则将fault伺候。

Fault 类异常

有若干个系统异常专用于fault 处理。CM3 中的Faults 可分为以下几类:

�� 总线 faults

�� 存储器管理 faults

�� 用法 faults

�� 硬 fault

总线Faults

当AHB 接口上正在传送数据时,如果回复了一个错误信号(error response),则会产生总

线faults,产生的场合可以是:

�� 取指,通常被称作“预取流产”(prefetch abort)

�� 数据读/写,通常被称作“数据流产”(data abort)

在CM3 中执行如下动作可以触发总线异常:

�� 中断处理起始阶段的堆栈 PUSH 动作。 称为“入栈错误”

�� 中断处理收尾阶段的堆栈 POP 动作。 称为“出栈错误”

�� 在处理器启动中断处理序列(sequence)后的向量读取时。这是一种罕见的特殊情况,

被归类为硬fault。

哪些因素会导致AHB 回复一个错误信号?

AHB 回复的错误信号会触发总线fault,诱因可以是:

�� 企图访问无效的存储器 region。常见于访问的地址没有相对应的存储器。

�� 设备还没有作好传送数据的准备。比如,在尚未初始化SDRAM 控制器的

时候试图访问SDRAM。

�� 在企图启动一次数据传送时,传送的尺寸不能为目标设备所支持。例如,

某设备只接受字型数据,却试图送给它字节型数据。

�� 因为某些原因,设备不能接受数据传送。例如,某些设备只有在特权级下

才允许访问,可当前却是用户级。

当上述这些总线faults 发生时(取向量的除外),只要没有同级或更高优先级的异常正

在服务,且FAULTMASK=0,就会执行总线fault 的服务例程。如果在检测到总线fault 时还检

测到了更高优先级的异常,则先处理后者,而总线fault 则被标记成悬起。最后,如果总线

fault 被除能,或者总线fault 发生时正在处理同级或更高优先级异常,则总线fault 被迫成为

“硬伤”——上访成硬fault,使得最后执行的是硬fault 的服务例程(如果当前没有执行NMI

服务例程,则立即执行硬fault 服务例程——译者注)。如果在硬fault 服务例程的执行中又

产生了总线fault(太钻牛角尖了),内核将进入锁定状态(第12 章详细讨论)。

欲使能总线fault 服务例程,需要在NVIC 的“系统Handler 控制及状态寄存器”中置位

BUSFAULTENA 位。要注意的是:在使能之前,总线fault 服务例程的入口地址必须已经在向

量表中配置好,否则就成了作法自毙——程序可能跑飞。

那么,发生了总线fault 后,我们将如何找出该fault 的事故原因呢?在这里,NVIC 提供

了若干个fault 状态寄存器,其中一个名为“总线fault 状态寄存器”(BFSR)的。通过它,总

线fault 服务例程可以确定产生fault 的场合:是在数据访问时,在取指时,还是在中断的堆

栈操作时。

对于精确的总线fault(见下框说明),肇事的指令的地址被压在堆栈中。如果BFSR 中

的BFARVALID 位为1,还可以找出是在访问哪块存储器时产生该总线fault 的——该存储器

的地址被放到“总线fault 地址寄存器(BFAR)”中。然而,如果是不精确的总线fault,就无

从定位了。因为在发生fault 时,处理器已经在执行肇事指令后,不知又流逝了多少个周期

了。

精确的总线fault vs. 不精确的总线fault

由数据访问产生的总线fault,可以进一步被归类为精确总线fault 和不精确总

线fault。在不精确的总线faults 中,导致此fault 的指令早已完成了。例如,缓冲区

写入。启动缓冲区写入的指令不知何时已经执行了,但是写到中途时才触发总线

fault。可见,这个操作是在若干个时钟周期前执行的,而且不能确定是具体几个周

期之前,也没有记录这期间的程序跳转动作,因此无法确认“肇事者”,故而是不

精确的。精确的总线fault 则不同,它是被最后一个完成的操作触发的。例如,一

个存储器读取导致的fault 总是精确的,因为该指令必须等全部读完时才算执行完

成。这样,任何在读取过程中发生的fault 总能落在该指令的头上。

由取指和堆栈操作产生的fault 总是精确的。

BFSR 寄存器的程序员模型如下所示:它是一个8 位的寄存器,并且可以使用字传送和

字节传送来读取它。如果以字方式访问,地址是0xE000_ED28,并且第2 个字节有效;如果

以字节方式访问,则地址直接就是0xE000_ED29,如表7.8 所示。

存储器管理faults

存储器管理faults 多与MPU有关,其诱因常常是某次访问触犯了MPU设置的保护策略。

另外,某些非法访问,例如,在不可执行的存储器区域试图取指,也会触发一个MemManage

fault,而且即使没有MPU 也会触发。

MemManage faults 的常见诱因如下所示:

�� 访问了 MPU 设置区域覆盖范围之外的地址

�� 往只读 region 写数据

�� 用户级下访问了只允许在特权级下访问的地址

在MemManage fault 发生后,如果其服务例程是使能的,则执行服务例程。如果同时还

发生了其它高优先级异常,则优先处理这些高优先级的异常,MemManage 异常被悬起。如

果此时处理器已经在处理同级或高优先级异常,或者MemManage fault 服务例程被除能,则

和总线fault 一样:上访成硬fault,最终执行的是硬fault 的服务例程。如果硬fault 服务例

程或NMI 服务例程的执行也导致了MemManage fault,那就不可救要了——内核将被锁定。

可见,和总线fault 一样,MemManage fault 必须被使能才能正常响应。MemManage fault

在NVIC“系统handler 控制及状态寄存器”中的使能位是MEMFAULTENA。如果把向量表置

于RAM 中,应优先建立好MemManage fault 服务例程的入口地址。

为了调查MemManage fault 的案发现场,NVIC 中有一个“存储器管理fault 状态寄存器

(MFSR)”,它指出导致MemManage fault 的原因。如果是因为一个数据访问违例(DACCVIOL

位)或是一个取指访问违例(IACCVIOL 位),则违例指令的地址已经被压入栈中。如果还有

MMARVALID 位被置位,则还能进一步查出引发此fault 时访问的地址——读取NVIC“存储

器管理地址寄存器(MMAR)”的值。

MFSR 寄存器的程序员模型如下所示。它是一个8 位的寄存器,并且可以使用字传送和

字节传送来读取它。并且两种访问方式的地址都是0xE000_ED28,按字访问时第1 个字节有

效。如表7.9 所示。

9、用法faults

用法faults 发生的场合可以是:

�� 执行了未定义的指令

�� 执行了协处理器指令(Cortex‐M3 不支持协处理器,但是可以通过fault 异常机制来

使用软件模拟协处理器的功能,从而可以方便地在其它Cortex 处理器间移植)

�� 尝试进入 ARM 状态(因为CM3 不支持ARM 状态,所以用法fault 会在切换时产生。

软件可以利用此机制来测试某处理器是否支持ARM 状态)

�� 无效的中断返回(LR 中包含了无效/错误的值)

�� 使用多重加载/存储指令时,地址没有对齐。

另外,通过设置NVIC 的对应控制位,可以在下列场合下也产生用法fault:

�� 除数为零

�� 任何未对齐的访问

如果用法fault 被使能,在发生用法fault 时通常会执行其服务例程。但是如果当时还发

生了更高优先级的异常,则用法fault 被悬起。如果此时处理器已经在处理同级或高优先级

异常,或者用法fault 服务例程被除能,则和总线fault 和MemManage fault 一样:上访成硬

fault,最终执行的是硬fault 的服务例程。如果硬fault 服务例程或NMI 服务例程的执行竟然

导致了用法fault,那就不可救要了——内核又将被锁定(真不嫌唠叨啊)。

可见,和总线fault 和MemManage fault 一样,用法fault 必须被使能才能正常响应。用

法fault 在NVIC“系统handler 控制及状态寄存器”中的使能位是USGFAULTENA。如果把向

量表置于RAM 中,应优先建立好用法fault 服务例程的入口地址(其实作者的本意是:应先

建立好fault 类异常服务例程的入口地址,再建立其它异常服务例程的入口地址——译者注)。

为了调查用法fault 的案发现场,NVIC 中有一个“用法fault 状态寄存器(UFSR)”,它指

出导致用法fault 的原因。在服务例程中,导致用法fault 的指令地址被压入堆栈中。

UFSR 的定义如图7.10 所示。它占用了2 个字节,可以被按半字访问或是按字访问。按

字访问时的地址是0xE000_ED28,高半字有效;按半字访问时的地址是0xE000_ED2A。和其

它的FAULT 状态寄存器一样,它里面的位可以通过写1 来清零。

硬fault

硬fault 是上文讨论的总线fault、存储器管理fault 以及用法fault 上访的结果。如果这

些fault 的服务例程无法执行,它们就会成为“硬伤”——上访(escalation)成硬fault。另

外,在取向量(异常处理是对异常向量表的读取)时产生的总线fault 也按硬fault 处理。在

NVIC 中有一个硬fault 状态寄存器(HFSR),它指出产生硬fault 的原因。如果不是由于取向

量造成的,则硬fault 服务例程必须检查其它的fault 状态寄存器,以最终决定是谁上访的。

HFSR 的定义如表7.11 所示。

表7.11 硬fault 状态寄存器(地址:0xE000_ED2C)

应对faults

在软件开发过程中,我们可以根据各种fault 状态寄存器的值来判定程序错误,并且改

正它们。附录E 给出了各种faults 的常见诱因,以及应对攻略。

然而,在一个实时系统中,情况则大不相同。Faults 如果不加以处理常会危及系统的运

行。因此在找出了导致fault 的原因后,软件必须决定下一步该怎么办。如果系统中运行了

一个RTOS,通常是终结肇事的任务。在其它情况,系统也许必须要复位。不同的目标应用

对fault 恢复的要求也不同,采取适当的策略有利于软件更健壮——当然最好还是防患于未

然。下面就给出一些应付fault 的常用方法。

复位。这也是最后一招。通过设置NVIC“应用程序中断及复位控制寄存器”中的

VECTRESET 位,将只复位处理器内核而不复位其它片上设施。取决于芯片的复位设计,有些

CM3 芯片可以使用该寄存器的SYSRESETREQ 位来复位。这种只限于内核中的复位不会复位

其它系统部件。

恢复:在一些场合下,还是有希望解决产生fault 的问题的。例如,如果程序尝试访问

了协处理器,可以通过一个协处理器的软件模拟器来解决此问题——当然是以牺牲性能为代

价的,要不然还要硬件加速干啥。

中止相关任务:如果系统运行了一个RTOS,则相关的任务可以被终结或者重新开始。

各个fault 状态寄存器(FSRs)都保持住它们的状态,直到手工清除。Fault 服务例程在处理

了相应的fault 后不要忘记清除这些状态,否则如果下次又有新的fault 发生时,服务例程在

检视fault 源时又将看到早先已经处理的fault 状态标志,因此无法判断哪个fault 是新发生

的。FSRs 采用一个写时清除机制(写1 时清除)。

芯片厂商也可以再添加自己的FSR,以表示其它fault 情况。

SVC 和PendSV

注意:阅读本节的后面需要一点点多任务编程的基础知识——译者注

SVC(系统服务调用,亦简称系统调用)和PendSV(可悬起系统调用),它们多用于在

操作系统之上的软件开发中。SVC 用于产生系统函数的调用请求。例如,操作系统不让用户

程序直接访问硬件,而是通过提供一些系统服务函数,用户程序使用SVC 发出对系统服务函

数的呼叫请求,以这种方法调用它们来间接访问硬件。因此,当用户程序想要控制特定的硬

件时,它就会产生一个SVC 异常,然后操作系统提供的SVC 异常服务例程得到执行,它再

调用相关的操作系统函数,后者完成用户程序请求的服务。

这种“提出要求——得到满足”的方式,很好、很强大、很方便、很灵活、很能可持续

发展。首先,它使用户程序从控制硬件的繁文缛节中解脱出来,而是由OS 负责控制具体的

硬件。第二,OS 的代码可以经过充分的测试,从而能使系统更加健壮和可靠。第三,它使

用户程序无需在特权级下执行,用户程序无需承担因误操作而瘫痪整个系统的风险。第四,

通过SVC 的机制,还让用户程序变得与硬件无关,因此在开发应用程序时无需了解硬件的操

作细节,从而简化了开发的难度和繁琐度,并且使应用程序跨硬件平台移植成为可能。开发

应用程序唯一需要知道的就是操作系统提供的应用编程接口(API),并且了解各个请求代号

和参数表,然后就可以使用SVC 来提出要求了(事实上,为使用方便,操作系统往往会提供

一层封皮,以使系统调用的形式看起来和普通的函数调用一致。各封皮函数会正确使用SVC

指令来执行系统调用——译者注)。其实,严格地讲,操作硬件的工作是由设备驱动程序完

成的,只是对应用程序来说,它们也是操作系统的一部分。如图7.14 所示

SVC 异常通过执行”SVC”指令来产生。该指令需要一个立即数,充当系统调用代号。SVC

异常服务例程稍后会提取出此代号,从而解释本次调用的具体要求,再调用相应的服务函数。

例如,

SVC 0x3 ; 调用3 号系统服务

在SVC 服务例程执行后,上次执行的SVC 指令地址可以根据自动入栈的返回地址计算

出。找到了SVC 指令后,就可以读取该SVC 指令的机器码,从机器码中萃取出立即数,就

获知了请求执行的功能代号。如果用户程序使用的是PSP,服务例程还需要先执行MRS Rn,

PSP 指令来获取应用程序的堆栈指针。通过分析LR 的值,可以获知在SVC 指令执行时,正

在使用哪个堆栈(细节在第8 章中讨论)。

SVC vs. SWI

如果你曾使用过其它的ARM 处理器(如ARM7),你也许会知道那里有一个被称为

“软件中断”的指令(SWI)。SVC 的地位与SWI 是相同的——而且连机器码都相同。

然而,因为在CM3 中,异常处理模型已经“洗心革面”了,就故意把该指令也重命名,

以强调它是在新生的系统中使用的。并且让程序员在把ARM7 代码移植到CM3 时,能

充分注意到这个本质的不同(至少必须得改名,每次改名时都得到警示)。

由CM3 的中断优先级模型可知,你不能在SVC 服务例程中嵌套使用SVC 指令(事实上

这样做也没意义),因为同优先级的异常不能抢占自身。这种作法会产生一个用法fault。同

理,在NMI 服务例程中也不得使用SVC,否则将触发硬fault。

另一个相关的异常是PendSV(可悬起的系统调用),它和SVC 协同使用。一方面,SVC

异常是必须立即得到响应的(若因优先级不比当前正处理的高,或是其它原因使之无法立即

响应,将上访成硬fault——译者注),应用程序执行SVC 时都是希望所需的请求立即得到响

应。另一方面,PendSV 则不同,它是可以像普通的中断一样被悬起的(不像SVC 那样会上

访)。OS 可以利用它“缓期执行”一个异常——直到其它重要的任务完成后才执行动作。悬

起PendSV 的方法是:手工往NVIC 的PendSV 悬起寄存器中写1。悬起后,如果优先级不够

高,则将缓期等待执行。

PendSV 的典型使用场合是在上下文切换时(在不同任务之间切换)。例如,一个系统中

有两个就绪的任务,上下文切换被触发的场合可以是:

�� 执行一个系统调用

�� 系统滴答定时器(SYSTICK)中断,(轮转调度中需要)

让我们举个简单的例子来辅助理解。假设有这么一个系统,里面有两个就绪的任务,并

且通过SysTick 异常启动上下文切换。如图7.15 所示。

上图是两个任务轮转调度的示意图。但若在产生SysTick 异常时正在响应一个中断,则

SysTick 异常会抢占其ISR。在这种情况下,OS 不得执行上下文切换,否则将使中断请求被延

迟,而且在真实系统中延迟时间还往往不可预知——任何有一丁点实时要求的系统都决不能

容忍这种事。因此,在CM3 中也是严禁没商量——如果OS 在某中断活跃时尝试切入线程模

式,将触犯用法fault 异常。

为解决此问题,早期的OS 大多会检测当前是否有中断在活跃中,只有没有任何中断需

要响应时,才执行上下文切换(切换期间无法响应中断)。然而,这种方法的弊端在于,它

可以把任务切换动作拖延很久(因为如果抢占了IRQ,则本次SysTick 在执行后不得作上下

文切换,只能等待下一次SysTick 异常),尤其是当某中断源的频率和SysTick 异常的频率比

较接近时,会发生“共振”。

现在好了,PendSV 来完美解决这个问题了。PendSV 异常会自动延迟上下文切换的请求,

直到其它的ISR 都完成了处理后才放行。为实现这个机制,需要把PendSV 编程为最低优先

级的异常。如果OS 检测到某IRQ 正在活动并且被SysTick 抢占,它将悬起一个PendSV 异常,

以便缓期执行上下文切换。如图7.17 所示

图7.17 使用PendSV 控制上下文切换

个中事件的流水账记录如下:

1. 任务 A 呼叫SVC 来请求任务切换(例如,等待某些工作完成)

2. OS 接收到请求,做好上下文切换的准备,并且pend 一个PendSV 异常。

3. 当 CPU 退出SVC 后,它立即进入PendSV,从而执行上下文切换。

4. 当 PendSV 执行完毕后,将返回到任务B,同时进入线程模式。

5. 发生了一个中断,并且中断服务程序开始执行

6. 在 ISR 执行过程中,发生SysTick 异常,并且抢占了该ISR。

7. OS 执行必要的操作,然后pend 起PendSV 异常以作好上下文切换的准备。

8. 当 SysTick 退出后,回到先前被抢占的ISR 中,ISR 继续执行

9. ISR 执行完毕并退出后,PendSV 服务例程开始执行,并且在里面执行上下文切换

10. 当 PendSV 执行完毕后,回到任务A,同时系统再次进入线程模式。

猜你喜欢

转载自blog.csdn.net/qingchunwang/article/details/86514097
今日推荐