Cortex-M3 处理器窥探

目录

1、寄存器组

2、特殊功能寄存器组

2.1、xPSR

2.2、PRIMASK

2.3、BASEPRI

2.4、FAULTMASK

2.5、CONTROL

2.6、特殊寄存器组访问方式

3、处理器工作模式

3.1、运行等级

3.2、运行模式

3.3、运行等级 VS 运行模式

4、堆栈

5、指令集

6、中断/异常向量表

7、中断/异常响应序列

7.1、中断/异常入栈

7.2、取向量

7.3、更新寄存器

7.4、异常返回值 EXC_RETURN

8、SVC 和 PendSV

8.1、SVC

8.2、PendSV


 

Cortex-M3 系列处理器是基于 ARMv7-M 架构的处理器,应用非常广泛,为了能够深入的分析在此平台上跑 RTOS 的各种细节,所以有必要写一篇关于 CM3 处理器的结构相关的文章(CM4 类似),在 OS 调度初始化、系统调用、进程调度等方面的细节均是和具体处理器息息相关,所以先让我们来看看 CM3 处理器的一些特征;

 

1、寄存器组

如下所示,CM3 处理器拥有 R0~R15 一共 16 个内部寄存器,其中:

R0~R12 称之为通用寄存器。在这 13 个寄存器中,根据指令集访问的特性,R0~R7 是所有指令都可以访问,而 R8~R12 只有很少的 16 位的 Thumb 指令可以访问,32 位的 Thumb-2 不受限制;

R13 默认情况下被用做堆栈指针;堆栈指针分为 MSP 和 PSP,后面会详细描述;

R14 默认情况作为 LR,也就是链接寄存器,当程序调用其他函数后,此寄存器保存了返回地址,使得子程序执行完毕后,得以返回;

R15 默认作为 PC 指针;

 

2、特殊功能寄存器组

CM3 中,除了上述 16 个寄存器以外,还有几个特殊的寄存器组:

xPSR:状态寄存器;

PRIMASK:中断屏蔽寄存器;

FAULTMASK:中断屏蔽寄存器;

BASEPRI:中断屏蔽寄存器,按照优先级进行屏蔽;

CONTROL:处理器模式和堆栈选择;

他们的含义如下:

下面我们一个一个看

 

2.1、xPSR

xPSR 是 Program Status Register 程序状态寄存器的意思,前面有个 x 代表他是由 3 个小的寄存器构成:

APSR:应用程序状态寄存器;

IPSR:中断程序状态寄存器;

EPSR:执行程序状态寄存器;

它们 3 个一起叫做程序状态寄存器,xPSR 的组成是 32 位的寄存器,在这 32 位中,APSR、IPSR、EPSR 各占一部分:

蓝色部分是 APSR,占领了高 27bit ~ 31bit

紫色部分是 EPSR,占领了高 9bit ~ 26bit

绿色部分是 IPSR,占领了低 0bit ~ 8bit

如果写汇编的话呢,APSR 的 N、Z、C、V、Q 这些标志会被使用到,详见指令集部分;

IPSR 中存储了当前服务的中断号;

 

2.2、PRIMASK

这个是只有单一 bit 的寄存器。当它被置位 1 后,就关掉了所有可屏蔽的异常(中断),只剩下 NMI 和 HardFault 可以响应。缺省值是 0,表示没有屏蔽中断;

PRIMASK 也可以叫一级中断开关,这里值得注意的是,即便是通过 PRIMASK 写 1 屏蔽了中断,但是中断依然会在门外被 Pending 住,只不过得不到执行,如果在 PRIMASK 为 1 的情况下,有中断在外 Pending 了,此刻往 PRIMASK 写 0,那么立马会进入 ISR;也就是说,PRIMASK 只是屏蔽掉中断,而并不是不让中断源产生中断!

 

2.3、BASEPRI

这个寄存器最多有 9 bit(由表达优先级的位数决定)。它定义了被屏蔽优先级的阈值;换言之,当这个寄存器被设置某个数值后,所有优先级号大于等于该值的中断都被关闭(优先级号越大,优先级越低);默认值是0,也就是不关闭任何中断;

 

2.4、FAULTMASK

这也是只有 1 bit 的寄存器,当设置为 1 的时候,只有 NMI 才能够响应,其他所有的异常,甚至是 HardFault 也不响应,默认值是 0,也就是都响应;

 

2.5、CONTROL

根据名字就知道,这是个控制寄存器,这个控制寄存器由两个 bit 构成,我们称之为 CONTROL[0] 和 CONTROL[1];

CONTROL[0]  用来指明运行的 CPU 的特权级别;

CONTROL[1] 用来指明使用的堆栈类型;

稍后会对堆栈指针和 CPU 特权级别以及线程模式/Handler 模式做说明;

 

2.6、特殊寄存器组访问方式

上述的特殊寄存器组 xPSR、PRIMASK、FAULTMASK、BASEPRI 以及 CONTROL 都是 CM3 内核的寄存器,CM3 定义的访问他们的方式是只能通过 MRSMSR 指令,比如:

MRS    R0,    BASEPRI    ; 读取 BASEPRI   到 R0
MRS    R0,    FAULTMASK  ; 读取 FAULTMASK 到 R0
MRS    R0,    PRIMASK    ; 读取 PRIMASK   到 R0
MRS    R0,    CONTROL    ; 读取 CONTROL   到 R0

MSR    BASEPRI,    R0    ; 将 R0 写入 BASEPRI
MSR    FAULTMASK,  R0    ; 将 R0 写入 FAULTMASK
MSR    PRIMASK,    R0    ; 将 R0 写入 PRIMASK
MSR    CONTROL,    R0    ; 将 R0 写入 CONTROL

其实,为了快速的开关中断,CM3 还专门定义了一条叫 CPS 的指令,有如下 4 种用法:

CPSID    I    ;PRIMASK=1,     ;关中断
CPSIE    I    ;PRIMASK=0,     ;开中断
CPSID    F    ;FAULTMASK=1    ;关异常
CPSIE    F    ;FAULTMASK=0    ;开异常

 

3、处理器工作模式

在 2.5、CONTROL 章节提到了工作模式和特权等级这里就需要说明一下 CM3 处理器的工作模式和特权等级做一下说明;

3.1、运行等级

CM3 有两种运行等级:

1、特权等级;

2、用户等级;

处理器在特权等级(Privilege )下,代码可以访问任意的寄存器;

处理器在用户等级(User)下,对系统控制寄存器 SCSSystem Control Space)和特殊寄存器(通过 MSR/MRS 访问的寄存器)的访问将被阻止(除了 APSR,因为 APSR 是专门用于给应用程序标志位的);如果在用户级下,访问了上述寄存器,那么 HardFault 伺候;

SCS 就是那些 NVIC、SCB(系统控制寄存器)这些的玩意;

也就是说,特权级和用户级的区别在于,访问 Core 寄存器的限制!

 

3.2、运行模式

CM3 的运行模式分为两种:

1、Thread 模式:也就是说的线程模式

2、Handler 模式;

简言之,线程模式就是跑普通代码时候处理器所处的模式,Handler 模式就是异常的时候处理器的模式;

 

3.3、运行等级 VS 运行模式

运行等级和运行模式之间有如下关系:

对这个图的解释为:

异常 Handler 一定是 Handler 模式,并且一定是特权级;

正常住应用代码,即,非 ISR 运行的程序(线程模式)可以是特权级(访问所有的寄存器),也可以运行在用户级(寄存器访问受限)

正常情况下,系统复位后,处理器处于特权级+线程模式(因为系统复位后,一般都需要先配置系统寄存器,状态等);

在配置完本机需要的系统寄存器后,可以选择往 CONTROL[0] 写1,进入用户线程模式,此刻系统寄存器不在接受改动,一旦进入了用户线程模式,用户级下的代码不能再试图修改 CONTROL[0] 来返回特权模式;

但是用户线程模式下,可以通过触发一个异常 Handler,进入 Handler 模式,所有 Handler 模式一定都是特权模式,可以在这个模式下去更改 CONTROL[0],让 Handler 返回的时候再次进入特权的线程模式;

状态转换如下所示:

典型的时序如下所示:

当在线程特权模式进入中断后,处理器的特权等级和模式一直处于特权级,仅仅有线程模式变化为 Handler 模式区别,如下:

当在线程用户模式进入中断后,处理器在 Handler 下处于特权级,退出 Handler 后,变化为用户级,如下:

 

4、堆栈

CM3 处理器使用的是 “向下生长的满栈” 模型,什么叫向下生长的满栈呢?首先我们聊一下处理器堆栈模型,堆栈模式分为 4 种:

1、向生长的满栈

2、向生长的空栈

3、向生长的满栈

4、向生长的空栈

向下生长的含义是:堆栈由高地址向低地址生长;

向上生长的含义是:堆栈由低地址向高地址生长;

满栈的含义是:栈指针pos指向的是一个空的 slot,也就是下一个可用的空闲。便于压栈,而弹的时候需要弹pos-1或者pos+1

空栈的含义是:栈指针pos指向的是一个有可用数据的 slot,也就是最后一个使用的空间。便于弹栈,而压的时候需要压pos+1或者pos-1。

OK,CM3 处理器使用了“向下生长的满栈”模型,R13 默认作为了 SP 堆栈指针;

既然是这样,在简单的应用场景下,那么在初始化堆栈指针的时候呢,最为安全的办法是,将 SP 指针初始化到 SRAM 的最高位置(也就是末尾);代码和数据从 SRAM 起始地址开始递增,这样便最大程度上避免数据互踩;

CM3 为了能够更好的支持 OS,支持了双堆栈机制,双堆栈不是指的有两个堆栈,而是说系统中支持两个堆栈指针(但是当前使用的 SP 只能是其中之一):

1、MSP:主堆栈指针;

2、PSP:用户堆栈指针;

还记得 CONTROL 寄存器么,它是 2 bit 构成,CONTROL[1] 用来决定使用哪个堆栈!

CONTROL[1] = 0 的时候,使用 MSP

CONTROL[1] = 1 的时候,使用 PSP

在简单的应用场景下,如果裸机的情况下,不打算对系统进行任何保护,CM3 上电后默认系统跑在特权的线程模式,默认使用 MSP 作为 SP 堆栈指针(即 CONTROL[1] = 0 );中断/异常 Handler 下也使用 MSP 作为 SP 堆栈指针;

当 CONTROL[1] = 1 的时候,线程模式不在使用 MSP,而是使用 PSP(Handler 模式永远使用 MSP);

那么什么时候使用 PSP 呢?比如你要跑一个 RTOS,多任务,那么每个任务都需要有自己的堆栈,此刻 PSP 就可以用起来了;PSP 将用户堆栈和系统堆栈 MSP 分开,防止用户堆栈破坏系统 OS 堆栈;

在这种情况下的 PSP 与 MSP 切换,是硬件自动完成并压栈的,无需软件干预;

在带 OS 情况下,OS 可以手动压栈弹栈,修改 PSP 来达到切换任务上下文目的;

访问 MSP 和 PSP 也需要通过使用 MRS、MSR指令来完成:

MRS    R0,    MSP    ; 读取 MSP 指针到 R0
MSR    MSP,   R0     ; 写 R0 的值到 MSP

MRS    R0,    PSP    ; 读取 PSP 指针到 R0
MSR    PSP,   R0     ; 写 R0 的值到 PSP

 

5、指令集

指令集部分内容较多,请参考 Cortex-M3 权威指南指令集章节;以后将会将常用的指令(STR, LDR, LDMIA, STMIA等)拿出来分析;

 

6、中断/异常向量表

CM3 的中断/异常依赖于一个异常向量表,0~15 的编号为系统所用,大于 16 的编号为芯片公司自行定义的中断,最大支持到中断标号 255(一般用不到那么多);

这里主要分为几类:

1、Reset Handler:复位信号;

2、NMI:不可屏蔽信号,通过接 NMI 引脚;

3、系统各种 fault:包括 HardFault,BusFault,MemManageFault,UsageFault;

4、SVC 系统调用;

5、PendSV:给 OS 调度预留;

6、IRQ #xxx:芯片公司定义;

既然称之为 “中断向量表”,那么它就是一张软硬件约定好的一个表,默认地址放在 0x0000_0000 开始(0x0000_0000 为 MSP,Reset Handler 放在 0x0000_0004),当发生对应中断/异常的时候,CPU 到这张表对应的地址去获取 ISR 的入口,并跳转到对应的 ISR 执行;

在 CM3 处理器中,实现了一个叫 NVIC 的东西,全名叫中断向量嵌套控制器;在软件层面,它是以一组寄存器的形式体现出来,软件可以编程 NVIC 寄存器,实现中断优先级,中断使能,中断禁能,清除 Pending,手动 Pending 等操作;

NVIC 能够支持中断嵌套,即高优先级的中断抢占低优先级的中断(注意,都是用的是 MSP),但自己无法抢占自己;

更多的 NVIC 相关的东西不在多说,配合权威指南,通俗易懂;

 

7、中断/异常响应序列

当系统发生中断/异常的时候,CM3 处理器会:

1、入栈:将 8 个寄存器的值压入栈;

2、取向量:从向量表中获取对应中断的 ISR 入口地址;

3、选择堆栈指针 MSP/PSP,更新到堆栈指针 SP 中,更新链接寄存器 LR,更新 PC;

入栈就是在进入中断/异常服务程序之前的现场保存,硬件自动将 xPSR、PC、LR、R12、R3、R2、R1、R0 压入堆栈:

如果当中断/异常发生时刻,正在使用 PSP,则压入 PSP;否则压入 MSP;

一旦进入 ISR,那就一直使用 MSP;

 

7.1、中断/异常入栈

假设准备入栈的时候,SP 的值为 N,那么在入栈顺序如下所示(由于处理器流水线,自动入栈过程中写入的时间顺序和空间顺序并不是一致的)

从存储序列的空间顺序来讲,是表从上到下的顺序,时间顺序是 PC,xPSR.... 的顺序;

CM3 这样做,也是有原因,先保存 PC 和 xPSR 可以更早的启动 ISR 的指令预取(因为需要修改 PC),同时也可以在早起就更新 xPSR 的 IPSR 的值;

R0~R3 和 R12 入栈了,那其他的 R4~R11 呢,在 ARM 的 C 语言标准函数调用约定中(AAPCS)编译器优先使用入栈的寄存器来保存中间结果,如果真的用到了 R4~R11,编译器生成代码来 push 它们;

 

7.2、取向量

在数据总线正在入栈操作的同时,指令总线从向量表中找出对应的 ISR 的入口,这两者同时进行;

 

7.3、更新寄存器

当上述两步完成之后,还需要更新一些寄存器:

SP:入栈后,把堆栈指针更新到新的位置,在 ISR 中使用 MSP;

xPSR:更新 IPSR 为对应的异常编号;

PC:取向量完成后,PC 将指向 ISR 的入口;

LR:在出入 ISR 的时候,LR 的值不再是我们之前理解的链接寄存器的含义,此刻的 LR 称为 EXC_RETURN;在异常进入的时候,由系统计算赋值给 LR 寄存器,在异常返回的时候使用它;

 

7.4、异常返回值 EXC_RETURN

当进入 ISR 的时候,LR 将被赋予新的含义:EXC_RETURN;这个是高 28 位全部为 1,只有 [3:0] 有含义;

当异常服务程序将这个值送给 PC,就意味着启动处理器的中断返回序列

如果主程序在线程模式并使用 MSP 的时候进入 ISR,则 EXC_RETURN=0xFFFF_FFF9

如果主程序在线程模式并使用 PSP 的时候进入 ISR,则 EXC_RETURN=0xFFFF_FFFD

如果当前运行在一个 ISR,此刻来了优先级更高的 ISR,则 EXC_RETURN=0xFFFF_FFF1

线程模式 + MSP 进入 ISR1 的时候,LR 被设置成为了 0xFFFF_FFF9,因为返回的时候是线程模式 + MSP

此刻被 ISR2 嵌套了,所以 LR 更新为了 0xFFFF_FFF1

线程模式 + PSP 的时候进入 ISR1,此刻 LR 更新为 0xFFFF_FFFD,因为返回的时候,是线程模式 + PSP

此刻 ISR2 优先级更高,嵌套了 ISR1,所以 LR 更新为 0xFFFF_FFF1

 

8、SVC 和 PendSV

这两个 IRQ 与操作系统相关,所以拉出来单独聊聊;

8.1、SVC

玩过 ARM7 的都知道,有一个指令叫 SWI,软件中断,SVC 和 SWI 是一样的,主要的目的是用来呼叫系统调用,进入操作系统内核;一般的,操作系统不允许让用户态的程序直接访问硬件(防止破坏),如果用户态的软件要访问硬件,需要通过系统调用(在 Linux 上的 open、write,read,ioclt 这些)进入内核态;那么这个 SVC(SWI)就是呼叫系统调用的方式;

这种方式使得用户代码和具体硬件无关,硬件全部交给 OS;

SVC 只是作为一个封皮,通过系统调用,进入 SVC Handler 特权级的 Handler 模式;

SVC 异常通过执行 SVC 指令产生,该指令需要一个 8 位的立即数充当系统调用号,SVC 异常的 ISR 会去拿出此立即数,从而判断本次的系统调用具体是要呼叫哪种系统调用函数(Open,Write,Read 的调用号不一样);比如:

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

 

注意:这个 8 位的立即数,被封装在指令本身中,就像上面的例子,呼叫 3 号系统服务,这个 3 被封装在触发这个 SVC 异常的 SVC 指令中;因此,在 SVC 的 ISR 中,需要读取本次触发 SVC 异常的 SVC 指令,并且提取出 8 位立即数的位段,来知道系统调用号,提取的代码如下:

首先是一段汇编,通过判断 EXC_RETURN 的值来判断是 PSP 还是 MSP:

__asm void SVC_Handler(void)
{
//  汇编操作,用于提出堆栈帧的起始位置,并放到R0中,然后跳转至实际的SVC服务例程中 
    IMPORT svc_handler 
    TST LR, #4 
    ITE EQ 
    MRSEQ R0, MSP 
    MRSNE R0, PSP 
    B svc_handler 
}

然后将 SP 堆栈指针放到 R0 中,跳转到 SVCHandler_main:

// “真正”的服务函数,接受一个指针参数(pwdSF):堆栈栈的起始地址。 
// pwdSF[0] = R0 , pwdSF[1] = R1 
// pwdSF[2] = R2 , pwdSF[3] = R3 
// pwdSF[4] = R12, pwdSF[5] = LR 
// pwdSF[6] = 返回地址(入栈的PC) 
// pwdSF[7] = xPSR 
unsigned long svc_handler(unsigned int* pwdSF) 
{ 
    unsigned int svc_number; 
    unsigned int svc_r0; 
    unsigned int svc_r1; 
    unsigned int svc_r2; 
    unsigned int svc_r3; 
    int retVal; //用于存储返回值 

    svc_number = ((char *) pwdSF[6])[-2]; // 没想到吧,C的数组能用得这么绝! 
    svc_r0 = ((unsigned long) pwdSF[0]); 
    svc_r1 = ((unsigned long) pwdSF[1]); 
    svc_r2 = ((unsigned long) pwdSF[2]); 
    svc_r3 = ((unsigned long) pwdSF[3]); 
    printf (“SVC number = %xn”, svc_number); 
    printf (“SVC parameter 0 = %x\n”, svc_r0); 
    printf (“SVC parameter 1 = %x\n”, svc_r1); 
    printf (“SVC parameter 2 = %x\n”, svc_r2); 
    printf (“SVC parameter 3 = %x\n”, svc_r3); 
    //做一些工作,并且把返回值存储到retVal中 
    pwdSF[0]=retVal; 
    return 0; 
}

//注意,这个函数返回的其实不是0!进一步地,灰色的文字只是用于哄编译器开心的,具体参考Cortex-M3权威指南P169

SVCHandler_main 提取 svc_number 这个地方和 CM3 处理器异常入栈顺序相关,这里获取到了引发异常的 PC,也就是呼叫 SVC 的那个地方的指令,并获取到 8 位立即数(数组[-2]的方式)

 

8.2、PendSV

PendSV 可以像普通中断一样被 Pending(往 NVIC 的 PendSV 的 Pend 寄存器写 1),常用的场合是 OS 进行上下文切换;它可以手动拉起后,等到比他优先级更高的中断完成后,在执行;

假设,带 OS 系统的 CM3 中有两个就绪的任务,上下文切换可以发生在 SYSTICK 中断中:

这里展现的是两个任务 A 和 B 轮转调度的过程;但是,如果在产生 SYSTICK 异常时,系统正在响应一个中断,则 SYSTICK 异常会抢占其他 ISR。在这种情况下 OS 是不能执行上下文切换的,否则将使得中断请求被延迟;

而且,如果在 SYSTICK 中做任务切换,那么就会尝试切入线程模式,将导致用法 fault 异常;

为了解决这种问题,早期的 OS 在上下文切换的时候,检查是否有中断需要响应,没有的话,采取切换上下文,然而这种方法的问题在于,可能会将任务切换的动作拖延很久(如果此次的 SYSTICK 无法切换上下文,那么要等到下一次 SYSTICK 再来切换),严重的情况下,如果某 IRQ 来的频率和 SYSTICK 来的频率比较接近的时候,会导致上下文切换迟迟得不到进行;

引入 PendSV 以后,可以将 PendSV 的异常优先级设置为最低,在 PendSV 中去切换上下文,PendSV 会在其他 ISR 得到相应后,立马执行:

 

上图的过程可以描述为:

1、任务 A 呼叫 SVC 请求任务切换;

2、OS 收到请求,准备切换上下文,手动 Pending 一个 PendSV;

3、CPU 退出 SVC 的 ISR 后,发现没有其他 IRQ 请求,便立即进入 PendSV 执行上下文切换;

4、正确的切换到任务 B;

5、此刻发生了一个中断,开始执行此中断的 ISR;

6、ISR 执行一半,SYSTICK 来了,抢占了该 IRQ;

7、OS 执行一些逻辑,并手动 Pending PendSV 准备上下文切换;

8、退出 SYSTICK 的 ISR 后,由于之前的 IRQ 优先级高于 PendSV,所以之前的 ISR 继续执行;

9、ISR 执行完毕退出,此刻没有优先级更高的 IRQ,那么执行 PendSV 进行上下文切换;

10、PendSV 执行完毕,顺利切到任务 A,同时进入线程模式;

 

©️2020 CSDN 皮肤主题: 技术黑板 设计师:CSDN官方博客 返回首页