1. 概述

Cortex-M0有一部分特性针对嵌入式操作系统(OS),包括:

  • SysTick定时器,24位向下计数,且周期产生SysTick异常。

  • 支持主栈指针(MSP)和进程栈指针(PSP),可以使应用栈和OS内核栈相互独立;

  • SCV异常和SVC指令,通过异常机制,应用程序可以使用SVC访问OS服务;

  • PendSV异常,可以被OS、设备驱动或应用程序使用来产生可延迟的服务请求;

嵌入式应用程序中,OS一般用来管理任务。这种情况下,OS将处理器时间划分为多个时间片,并且在每个时间片上执行不同任务。当一个时间片结束时,OS任务调度器开始执行,这样在下一个时间片开始的时候,处理器已经切换到其它的任务中执行了。这种任务切换一般被称为上下文切换。

上下文切换上下文切换.png

除了支持多任务外,嵌入式OS也提供了其它各种功能,包括资源管理、内存管理、电源管理,以及应用程序编程接口(API)用以访问外设、硬件和信道。

嵌入式OS其它功能嵌入式OS其它功能.png

由于Cortex-M0处理器不具备内存管理单元(MMU),所以不能运行需要虚拟地址的嵌入式OS,比如Windows CE或Symbian OS等。

2. SysTick定时器

OS支持周期执行上下文切换,这样就需要有定时器之类的硬件资源打断程序执行。当定时器中断产生时,处理器就会在异常处理中进行OS任务调度,同时还会进行OS维护工作。

Cortex-M0处理器中有一个SysTick的简单定时器,用于产生周期性的中断请求。SysTick为24位的定时器,并且向下计数。定时器的计数减至0后,就会重新装载一个可编程的数值,并且同时产生SysTick异常(异常编号15)。

2.1 SysTick寄存器

systick寄存器.png

  • SysTick控制和状态寄存器(0xE000E010)

    systick寄存器_0x10systick寄存器_0x10.png

  • SysTick重装载值寄存器(0xE000E014)

    systick寄存器_0x14systick寄存器_0x14.png

  • SysTick当前值寄存器(0xE000E018)

    systick寄存器_0x18.pngsystick寄存器_0x18

  • SysTick校准值寄存器(0xE000E01C)

    systick寄存器_0x1Csystick寄存器_0x1C.png

使用CMSIS设备驱动库,可以采用如下方式来进行访问:

typedef struct
{
  __IO uint32_t CTRL;                    /*!< Offset: 0x000 (R/W)  SysTick Control and Status Register */
  __IO uint32_t LOAD;                    /*!< Offset: 0x004 (R/W)  SysTick Reload Value Register       */
  __IO uint32_t VAL;                     /*!< Offset: 0x008 (R/W)  SysTick Current Value Register      */
  __I  uint32_t CALIB;                   /*!< Offset: 0x00C (R/ )  SysTick Calibration Register        */
} SysTick_Type;

systick寄存器名称systick寄存器名称.png

2.2 设置SysTick

SysTick的设置需要遵循一定流程:

systick设置systick设置.png

使用CMSIS进行操作,代码如下:

uint32_t SysTick_Config(uint32_t ticks);

也可以直接操作SysTick寄存器,来控制SysTick,代码如下:

SysTick -> CTRL = 0;  // 禁止SysTick
SysTick -> LOAD = 999;  // 从999到0减计数
SysTick -> VAL  = 0;  // 将当前值清0
SysTick -> CTRL = 0x07;  // 使能SysTick以及SysTick异常,并且使用处理器时钟

3. 主栈指针和进程栈指针

Cortex-M0具有两个栈指针:主栈指针(MSP)和进程栈指针(PSP)。他们都是32位寄存器,并且都可通过R13访问,只是同一时间只能使用一个。

对于简单程序,可以只使用MSP,这种情况下,也只能由一个栈区域;对于使用嵌入式OS,由于需要较高的可靠性,需要定义多个栈区域,一个用于OS内核以及异常,其他的则用于不用任务。

OS栈指针的分布OS栈指针的分布.png

在典型OS环境中,MSP和PSP用法如下:

  • MSP,用于OS内核和异常处理;

  • PSP,用于应用任务;

OS内核在上下文切换时,需要一直跟踪每个任务栈指针的值,并且改变PSP的值以使得每个任务都有自己的栈空间。

OS栈使用示例OS栈使用示例.png

栈指针的选择由Cortex-M0处理器的当前模式和CONTROL寄存器的值来决定。关于CONTROL寄存器,参考章节  2.3 CONTROL 寄存器

如果发生异常,处理器会进入处理模式,并且选择MSP作为栈指针。根据CONTROL寄存器的值得不同,压栈过程会将R0-R3、R12、LR、PC和xPSR压入MSP或PSP中。

异常进入和退出栈指针变化示意图,如下:

异常进入和退出栈指针变化异常进入和退出栈指针变化.png

关于异常处理,参考章节 《ARM Cortex-M0 权威指南》笔记(6)—异常和中断》

使用CMSIS函数来操作MSP和PSP,如下:

uint32_t __get_MSP(void);     // 读取主栈指针的当前值
void __set_MSP(uint32_t topOfMainStack); // 设置主栈指针
uint32_t __get_PSP(void);     // 读取进程栈指针的当前值
void __set_PSP(uint32_t topOfProcStack); // 设置进程栈指针

简单OS的任务栈初始化:

OS栈初始化OS栈初始化.png

简单OS中的上下文切换:

OS上下文切换OS上下文切换.png

4. 请求管理调用(SVC)

要实现一个完整的OS,需要用到更多的处理器特性。第一个就是能够让任务触发特定OS异常的软件中断机制,在ARM处理器中,它被称作请求管理调用(系统服务调用)。SVC(Supervisor Call)即是一条指令,也是一种异常。SVC指令的执行就会触发SVC异常,如果没有相同优先级或更改优先级的异常在执行,处理器就立即执行SVC异常处理。

SVC为应用程序访问OS的系统服务提供了一个途径,并且这种访问不需要提供任何地址信息。因此,OS的应用程序可以单独的编译和启动,应用程序只需要正确的OS服务,并且提供所需的参数,就能与OS交互。

OS调用SVC接口OS调用SVC接口.png

SVC指令包含一个8位立即数,SVC处理可以将这个数据提取出来,并根据它来确定所需的OS服务。SVC指令的汇编调用规则如下:

SVC #3    ;调用SVC3号服务

FreeRTOS中的 SVC_Handler 函数定义:

__asm void vPortSVCHandler( void )
{
     PRESERVE8
    
     ldr r3, =pxCurrentTCB /* Restore the context. */
     ldr r1, [r3]   /* Use pxCurrentTCBConst to get the pxCurrentTCB address. */
     ldr r0, [r1]   /* The first item in pxCurrentTCB is the task top of stack. */
     ldmia r0!, {r4-r11}  /* Pop the registers that are not automatically saved on exception entry and the critical nesting count. */
     msr psp, r0    /* Restore the task stack pointer. */
     isb
     mov r0, #0
     msr basepri, r0
     orr r14, #0xd
     bx r14
}

5. 可悬起系统调用(PendSV)

PendSV也是一种异常,可以通过设置NVIC的挂起位来激活它。PendSV的激活可以被延迟,因此,即使你在执行比PendSV的优先级还高的异常处理,也可以设置它的挂起状态。

PendSV异常的主要功能:

  • 嵌入式OS的上下文切换;

  • 将一个中断处理过程划分为两部分:

    • 前半部分需要快速执行,并且在高优先级中断服务程序中处理;

    • 后半部分对时间要求不高,可以在延迟的PendSV中处理,并且具有较低的优先级;因此,其他的高优先级中断请求就得以快速处理。

典型的OS设计中,上下文切换可以有以下方式触发:

  • SysTick处理期间的任务调度;

  • 等待数据/事件的任务通过调用SVC服务切换到另外一个任务中;

通常SysTick异常被设置为高优先级,因此,即便当前有其它的中断处理正在运行,SysTick处理也可以执行。不过,当有中断服务程序正在运行时,OS就不应执行实际的上下文切换了,要不然,这个中断服务区程序会被分割成多个部分。按照传统的方式,如果OS检测到有中断服务程序正在执行,下一个OS时钟到来之前,它就不会执行上下文切换。

没有使用PendSV,OS检测到ISR正在运行时,不执行上下文切换,如下:

没有PendSV,OS中断处理没有PendSV,OS中断处理.png

通过将上下文切换延迟到下次SysTick异常,IRQ处理可以完成本次执行。不过,这个IRQ的生成节拍可能同任务切换一致,或者IRQ产生太过频繁,这样就会导致有些任务会获得大量的执行时间,或者上下文切换在很长时间内都无法得到执行的机会。

为了解决这个问题,实际的上下文切换过程可以发生在低优先级的PendSV处理中,并且与SysTick处理相分离。将PendSV异常的优先级设为最低以后,只要在没有其他中断服务运行时,PendSV处理才得以执行。

使用PendSV,在IRQ处理完成后,上下文切换可以执行,如下:

pendsv_上下文切换pendsv_上下文切换.png

如上图,SysTick异常周期性地触发OS任务调度器,使其进行任务调度。OS任务调度器可以在退出异常之前设置PendSV异常的挂起状态,如果此时没有IRQ处理在运行,在SysTick异常结束后,PendSV处理会立即启动并执行上下文切换。如果SysTick异常产生时有IRQ正在运行,由于PendSV的优先级最低,在IRQ结束前,PendSV不会开始执行。当所有的IRQ处理全部结束时,PendSV处理才会执行所需的上下文切换。

FreeRTOS中的 PendSV_Handler 函数定义:

__asm void xPortPendSVHandler( void )
{
     extern uxCriticalNesting;
     extern pxCurrentTCB;
     extern vTaskSwitchContext;
    
     PRESERVE8
    
     mrs r0, psp
     isb
    
     ldr r3, =pxCurrentTCB  /* Get the location of the current TCB. */
     ldr r2, [r3]
    
     stmdb r0!, {r4-r11}   /* Save the remaining registers. */
     str r0, [r2]    /* Save the new top of stack into the first member of the TCB. */
    
     stmdb sp!, {r3, r14}
     mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY
     msr basepri, r0
     dsb
     isb
     bl vTaskSwitchContext
     mov r0, #0
     msr basepri, r0
     ldmia sp!, {r3, r14}
    
     ldr r1, [r3]
     ldr r0, [r1]    /* The first item in pxCurrentTCB is the task top of stack. */
     ldmia r0!, {r4-r11}   /* Pop the registers and the critical nesting count. */
     msr psp, r0
     isb
     bx r14
     nop
}
注意:本站所有文章除特别说明外,均为原创,转载请务必以超链接方式并注明作者出处。 标签:ARM,Cortex-M0,Cortex-M0操作系统