cortex-m处理器架构实现了多个特性,保证了OS设计的方便和高效。例如:
①影子栈指针。有两个栈指针可用,MSP用于OS内核以及中断处理,PSP则用于应用任务。
②SysTick定时器。位于处理器内部的简单定时器,使得同一个嵌入式OS可用在多种cortex-m微控制器上。
③SVC和PendSV异常。这两种异常对于嵌入式OS中的操作非常重要,如上下文切换的实现等。
④非特权执行等级。可以利用其实现一种基本安全模型,限制某些应用任务的访问权限。特权和非特权等级的分离还可同存储器保护单元一起使用,进一步提高嵌入式系统的健壮性。
⑤排他访问。排他加载和存储指令用于OS中的信号量和互斥体操作
另外,低中断等待和指令集中的各种特性还有助于嵌入式OS的高效运行。例如,低中断等待带来了较小的上下文切换开销。而且,一个名为跟踪单元(ITM)的调试特性可在多种调试工具中用于OS调试。
cortex-m处理器中存在两个栈指针:
①主栈指针(MSP)为默认的栈指针。当CONTROL的bit[1]为0时用于线程模式,在处理模式中则总是使用。
②进程栈指针(PSP),当CONTROL的bit[1]为1时用于线程模式。
PUSH 和POP指令实现的栈操作及使用SP的多数指令都会使用当前选择的栈指针,还可以利用MRS和MSR指令直接访问MSP和PSP。对于不具有嵌入式OS或RTOS的简单应用,可以在所有操作中只使用MSP,而不用管PSP。
对于具有嵌入式OS或RTOS的系统,异常处理(包括部分OS内核)使用MSP,而应用任务则使用PSP。每个应用任务都有自己的栈空间,如下图所示。OS中的上下文切换代码在每次上下文切换时都会更新PSP。
这种设计有几个优点:
①若应用任务遇到会导致栈破坏的问题,OS内核使用的栈和其他任务的栈不会受到影响,因此可以提高系统的可靠性。
②每个任务的栈空间只需满足栈的最大需求加上一级栈帧(对于cortex-m3或无浮点单元cortex-m4,最大9个字,包括额外插入的字,或者对于具有浮点单元的cortex-m4则最大为27个字),用于ISR和嵌套中断处理的栈空间会被分配在主栈中。
③有助于创建cortex-m处理器用的高效OS.
④OS还可以利用存储器保护单元定义可以访问某个栈区域的应用任务。若某应用任务具有栈溢出的问题,MPU可以触发一次MemManage错误异常,并且避免该任务栈空间以外的存储器区域被覆盖。上电后,MSP被初始化为向量表中的数值,这也是处理器复位流程的一部分。工具链添加的C启动代码也可以执行对主栈进行初始化的其他操作。之后还可以利用MSR指令初始化PSP,并且写入CONTROL设置SPSEL,不过一般不会这么做。
初始化并使用PSP的最简单方法为(对多数OS是不适用的):
LDR R0, =PSP_TOP ;PSP_TOP为代表栈顶地址的常量
MSR PSP, R0 ;设置PSP为进程栈的顶部
MRS R0, CONTROL ;读取当前的CONTROL
ORRS R0, R0, #0X2 ;设置SPSEL
MSR CONTROL, R0 ;写入CONTROL
ISB ;更新CONTROL后执行ISB,这也是架构推荐的处理方式。
一般来说,要使用进程栈,需要将OS置于处理模式,直接编程PSP后利用异常返回流程跳转到应用任务。例如,当OS从线程模式启动时,可以利用SVC异常进入处理模式,如下图所示。然后可以创建进程栈中的栈帧,且触发使用PSP的异常返回。当加载栈帧时,应用任务就会启动。
在OS设计中,需要在不同任务间切换,这一般被称作上下文切换,其通常在PendSV异常处理中执行,该异常可由SysTick异常触发。在上下文切换操作中需要:
①将寄存器的当前状态保存到当前栈中
②保存当前PSP数值
③将PSP设置为下一个任务的上一次SP数值
④恢复下一个任务的上一次的数值
⑤利用异常返回切换任务
如下图所示为一个简单的上下文切换操作,需要注意的是,上下文切换在PendSV中执行,其异常优先级一般会被设置为最低。这样会避免在中断处理过程中产生上下文切换。
SVC (请求管理调用)的异常类型为11,优先级可编程。SVC异常由SVC指令触发,尽管可以利用写入NVIC来触发一个中断(如软件触发中断寄存器NVIC->STIR),不过处理器的行为有些不同,中断是不精确的。这就意味着在设置挂起状态后及中断实际产生前可能会执行多条指令。换句话说,SVC是不精确的。SVC处理必须得在SVC指令后执行,除非同时出现了另一个更高优先级的中断。在许多系统中,SVC机制可用于实现应用任务访问系统资源的API,如下图所示:
对于需要高可靠性的系统,应用任务可以运行在非特权访问等级,而且有些硬件资源可被设置为只支持特权访问(利用MPU),应用任务只能通过OS的服务访问这些受保护的硬件资源。按照这种方式,由于应用任务无法获得关键硬件的访问权限,嵌入式系统会更加健壮和安全。SVC异常由SVC指令产生,该指令需要一个立即数,这也是参数传递的一种方式。SVC异常处理可以提取出参数并确定它需要执行的动作。例如:
SVC #0x3
在执行SVC处理时,可以在读取压栈的PC数值后从该地址读出指令并屏蔽掉不需要的位,以确定SVC指令中的立即数。不过,执行SVC的程序可以使用主栈也可以使用进程栈。因此在提取压栈PC数值前,需要确定压栈过程使用的是哪个栈,此时可以查看进入异常处理时链接寄存器的数值,如下图所示:
提取SVC服务编号的汇编程序如下:
SVC_Handler
TST LR, #4 ;测试EXC_RETURN的第2位
ITE EQ ;
MRSEQ R0, MSP ;若为0,压栈使用的是MSP,复制到R0
MRSNE R0, PSP ;若为1,压栈使用的是PSP,复制到R0
LDR R0, [R0,#24] ;从栈帧中得到压栈的PC 压栈的PC= SVC后指令的地址
LDRB R0, [R0 #-2] ;读取SVC指令的第一个字节 SVC编号目前位于R0中
。。。
对于C编程环境,需要将SVC处理分为两个部分:
①提取栈帧的起始地址,并将其作为输入参数传递给第二部分。该处理要用汇编实现,这是因为需要检查LR的数值,而其无法用C实现
②从栈帧中提取压栈的PC数值,然后从程序代码中得到SVC编号。它还可以选择提取出压栈的寄存器数值等其他信息。
以MDK-ARM工具链为例实现如下:
_asm void SVC_HANDLER(void)
{
TST LR, #4
ITE EQ
MRSEQ R0, MSP
MRSNE R0, PSP
B _cpp(SVC_Handler_C)
ALIGN 4
}
void SVC_Handler_C(unsigned int * svc_args){
uint8_t svc_number;
uint32_t stacked_r0,stacked_r1,stacked_r2,stacked_r3;
svc_number = ((char*)svc_args[6])[-2];
stacked_r0 = svc_args[0];
stacked_r1 = svc_args[1];
stacked_r2 = svc_args[2];
stacked_r3 = svc_args[3];
//...其他处理
。。。
//返回结果(如前两个参数)
svc_args[0] = stacked_r0 + stacked_r1;
return;
}
传递栈帧地址的好处在于,C处理可以提取出栈帧中的任何信息,包括压栈寄存器在内。若要将参数传递给SVC服务并获得SVC服务的返回值,这一点是非常重要的。由于异常处理实际上可以为普通C函数,若调用了SVC服务,且同时产生了更高优先级的中断,则更高优先级的ISR会首先执行,但这样会修改R0~R3以及R12等数值。为了确保SVC处理能够得到正确的参数,需要从栈帧中获取参数值。
PendSV异常编号为14且具有可编程的优先级。可以写入中断控制和状态寄存器(ICSR)设置挂起位以触发PendSV异常。与SVC异常不同,它是不精确的。因此,它的挂起状态可在更高优先级异常处理内设置,且会在高优先级处理完成后执行。利用该特性,若将PendSV设置为最低的异常优先级,可以让PendSV异常处理在所有其他中断处理任务完成后执行。这对于上下文切换非常有用,也是这种OS设计中的关键。
OS内核的执行可由以下条件触发:
①应用任务中SVC指令的执行。例如,当应用任务由于等待一些数据或事件被耽搁时,它可以调用系统服务以便切换到下一个任务。
②周期性的SysTick异常。
在OS代码中,任务调度器可以决定是否应该执行上下文切换。若IRQ在SysTick异常前产生,则SysTick异常可能会抢占IRQ处理。在这种情况下,OS不应执行上下文切换,否则IRQ处理就会被延迟。对于Cortex-m3/4处理器,当存在活跃的异常服务时,设计默认不允许返回到线程模式(不过还是有例外情况的)。若存在活跃中断服务,且OS试图返回到线程模式,则使用错误异常会被触发。
在一些OS设计中,要解决这个问题,可以在运行中断服务时不执行上下文切换,此时可以检查栈帧中的压栈xPSR或NVIC的中断活跃状态寄存器。不过系统的性能可能会受到影响,特别是当中断源在SysTick中断前后持续产生请求时,这样上下文切换可能就没有执行的机会了。为了解决这个问题,PendSV异常将上下文切换请求延迟到所有其他IRQ处理都已经完成后,此时需要将PendSV设置为最低优先级。若OS需要执行上下文切换,它会设置PendSV的挂起状态,并在PendSV异常内执行上下文切换。下图为利用PendSV进行上下文切换的一个实例,它具有以下事件流程:
①A任务调用SVC进行任务切换(例如,等待一些工作完成)。
②OS收到请求,准备进行上下文切换,且挂起PendSV异常。
③当CPU退出SVC时,会立即进入PendSV且进行上下文切换
④当PendSV完成并返回线程等级时,OS会执行B任务
⑤中断产生且进入中断处理。
⑥在运行中断处理程序时,SysTick异常会产生
⑦OS执行重要操作,然后挂起PendSV异常并准备进行上下文切换。
⑧当SysTick异常退出时,会返回到中断服务程序。
⑨当中断服务程序结束后,PendSV开始执行实际的上下文切换操作
⑩当PendSV完成后,程序返回到线程等级,这次它会回到任务A并继续执行。
除了OS环境中的上下文切换,PendSV还可用于不存在OS的环境中,例如,中断服务程序可能需要一些处理时间,要处理的部分可能会需要高优先级,不过如若整个ISR都是在高优先级中执行的,其他的中断服务可能在很长时间内都无法执行。在这种情况下,可以将中断服务处理划分为两个部分:
①第一部分对时间要求比较高,需要快速执行,且优先级较高。它位于普通的ISR内,在ISR结束时,设置PendSV的挂起状态
②第二部分包括中断服务所需的剩余的处理工作,它位于PendSV处理内且具有较低的异常优先级。
对于多任务系统,多个任务共享有限资源的情况是很常见的。例如,可能只会一个控制台显示输出,而多个不同任务可能需要在上面显示信息。因此,几乎所有的OS都提供了一些方法,允许任务将资源锁定,并在不需要时将其释放。锁定机制一般基于软件变量,若锁定变量置位,其他任务发现其被锁定,则必须等待。为了使锁定资源操作在单处理器和多处理器环境中都是原子性的,cortex-m3和cortex-m4处理器支持一种名为排他访问的特性。
资源的情况是很常见的。例如,可能只会一个控制台显示输出,而多个不同任务可能需要在上面显示信息。因此,几乎所有的OS都提供了一些方法,允许任务将资源锁定,并在不需要时将其释放。锁定机制一般基于软件变量,若锁定变量置位,其他任务发现其被锁定,则必须等待。为了使锁定资源操作在单处理器和多处理器环境中都是原子性的,cortex-m3和cortex-m4处理器支持一种名为排他访问的特性。
备注:参考ARM Cortex-M3与Cortex-M4权威指南