zoukankan      html  css  js  c++  java
  • Cortex-M3 处理器

    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 定义的访问他们的方式是只能通过 MRS 和 MSR 指令,比如:

    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)下,对系统控制寄存器 SCS(System 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 使用的是 Thumb-2 指令集

    指令集部分内容较多,请参考 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 与操作系统相关,所以拉出来单独聊聊;

    玩过 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
    ”, svc_r0); 
        printf (“SVC parameter 1 = %x
    ”, svc_r1); 
        printf (“SVC parameter 2 = %x
    ”, svc_r2); 
        printf (“SVC parameter 3 = %x
    ”, 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,同时进入线程模式;

    9、其他

    NVIC 还提供了一些 fault 状态寄存器,以便于 fault 服务例程找出导致异常的具体原因。

    参考链接:

    https://blog.csdn.net/zhoutaopower/article/details/106993348

  • 相关阅读:
    第十一周上机练习
    JAVA第十周上机练习
    JAVA第九周上机练习
    JAVA第八周作业
    JAVA第八周上机作业
    JAVA第七周作业
    Java第七周上机练习
    Java第六周作业
    JAVA第六周上机练习
    34-指针与二维数组
  • 原文地址:https://www.cnblogs.com/god-of-death/p/14856578.html
Copyright © 2011-2022 走看看