《ARM Cortex-M0 权威指南》笔记(8)—支持操作系统
1. 概述
Cortex-M0有一部分特性针对嵌入式操作系统(OS),包括:
SysTick定时器,24位向下计数,且周期产生SysTick异常。
支持主栈指针(MSP)和进程栈指针(PSP),可以使应用栈和OS内核栈相互独立;
SCV异常和SVC指令,通过异常机制,应用程序可以使用SVC访问OS服务;
PendSV异常,可以被OS、设备驱动或应用程序使用来产生可延迟的服务请求;
嵌入式应用程序中,OS一般用来管理任务。这种情况下,OS将处理器时间划分为多个时间片,并且在每个时间片上执行不同任务。当一个时间片结束时,OS任务调度器开始执行,这样在下一个时间片开始的时候,处理器已经切换到其它的任务中执行了。这种任务切换一般被称为上下文切换。
除了支持多任务外,嵌入式OS也提供了其它各种功能,包括资源管理、内存管理、电源管理,以及应用程序编程接口(API)用以访问外设、硬件和信道。
由于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控制和状态寄存器(0xE000E010)
SysTick重装载值寄存器(0xE000E014)
SysTick当前值寄存器(0xE000E018)
SysTick校准值寄存器(0xE000E01C)
使用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;
2.2 设置SysTick
SysTick的设置需要遵循一定流程:
使用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环境中,MSP和PSP用法如下:
MSP,用于OS内核和异常处理;
PSP,用于应用任务;
OS内核在上下文切换时,需要一直跟踪每个任务栈指针的值,并且改变PSP的值以使得每个任务都有自己的栈空间。
栈指针的选择由Cortex-M0处理器的当前模式和CONTROL寄存器的值来决定。关于CONTROL寄存器,参考章节 2.3 CONTROL 寄存器
如果发生异常,处理器会进入处理模式,并且选择MSP作为栈指针。根据CONTROL寄存器的值得不同,压栈过程会将R0-R3、R12、LR、PC和xPSR压入MSP或PSP中。
异常进入和退出栈指针变化示意图,如下:
关于异常处理,参考章节 《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中的上下文切换:
4. 请求管理调用(SVC)
要实现一个完整的OS,需要用到更多的处理器特性。第一个就是能够让任务触发特定OS异常的软件中断机制,在ARM处理器中,它被称作请求管理调用(系统服务调用)。SVC(Supervisor Call)即是一条指令,也是一种异常。SVC指令的执行就会触发SVC异常,如果没有相同优先级或更改优先级的异常在执行,处理器就立即执行SVC异常处理。
SVC为应用程序访问OS的系统服务提供了一个途径,并且这种访问不需要提供任何地址信息。因此,OS的应用程序可以单独的编译和启动,应用程序只需要正确的OS服务,并且提供所需的参数,就能与OS交互。
SVC指令包含一个8位立即数,SVC处理可以将这个数据提取出来,并根据它来确定所需的OS服务。SVC指令的汇编调用规则如下:
SVC #3 ;调用SVC3号服务
FreeRTOS v9.0 中 ARM_CM3 对应函数 vPortSVCHandler(),如下:
__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的上下文切换;
将一个中断处理过程划分为两部分:
(1)前半部分需要快速执行,并且在高优先级中断服务程序中处理;
(2)后半部分对时间要求不高,可以在延迟的PendSV中处理,并且具有较低的优先级;因此,其他的高优先级中断请求就得以快速处理。
典型的OS设计中,上下文切换可以有以下方式触发:
SysTick处理期间的任务调度;
等待数据/事件的任务通过调用SVC服务切换到另外一个任务中;
通常SysTick异常被设置为高优先级,因此,即便当前有其它的中断处理正在运行,SysTick处理也可以执行。不过,当有中断服务程序正在运行时,OS就不应执行实际的上下文切换了,要不然,这个中断服务区程序会被分割成多个部分。按照传统的方式,如果OS检测到有中断服务程序正在执行,下一个OS时钟到来之前,它就不会执行上下文切换。
没有使用PendSV,OS检测到ISR正在运行时,不执行上下文切换,如下:
通过将上下文切换延迟到下次SysTick异常,IRQ处理可以完成本次执行。不过,这个IRQ的生成节拍可能同任务切换一致,或者IRQ产生太过频繁,这样就会导致有些任务会获得大量的执行时间,或者上下文切换在很长时间内都无法得到执行的机会。
为了解决这个问题,实际的上下文切换过程可以发生在低优先级的PendSV处理中,并且与SysTick处理相分离。将PendSV异常的优先级设为最低以后,只要在没有其他中断服务运行时,PendSV处理才得以执行。
使用PendSV,在IRQ处理完成后,上下文切换可以执行,如下:
如上图,SysTick异常周期性地触发OS任务调度器,使其进行任务调度。OS任务调度器可以在退出异常之前设置PendSV异常的挂起状态,如果此时没有IRQ处理在运行,在SysTick异常结束后,PendSV处理会立即启动并执行上下文切换。如果SysTick异常产生时有IRQ正在运行,由于PendSV的优先级最低,在IRQ结束前,PendSV不会开始执行。当所有的IRQ处理全部结束时,PendSV处理才会执行所需的上下文切换。
FreeRTOS v9.0 中 ARM_CM0 对应函数 PendSV_Handler(),如下:
__asm void xPortPendSVHandler( void ) { extern vTaskSwitchContext extern pxCurrentTCB PRESERVE8 mrs r0, psp ldr r3, =pxCurrentTCB /* Get the location of the current TCB. */ ldr r2, [r3] subs r0, #32 /* Make space for the remaining low registers. */ str r0, [r2] /* Save the new top of stack. */ stmia r0!, {r4-r7} /* Store the low registers that are not saved automatically. */ mov r4, r8 /* Store the high registers. */ mov r5, r9 mov r6, r10 mov r7, r11 stmia r0!, {r4-r7} push {r3, r14} cpsid i bl vTaskSwitchContext cpsie i pop {r2, r3} /* lr goes in r3. r2 now holds tcb pointer. */ ldr r1, [r2] ldr r0, [r1] /* The first item in pxCurrentTCB is the task top of stack. */ adds r0, #16 /* Move to the high registers. */ ldmia r0!, {r4-r7} /* Pop the high registers. */ mov r8, r4 mov r9, r5 mov r10, r6 mov r11, r7 msr psp, r0 /* Remember the new top of stack for the task. */ subs r0, #32 /* Go back for the low registers that are not automatically restored. */ ldmia r0!, {r4-r7} /* Pop low registers. */ bx r3 ALIGN }