本帖最后由 Eric2013 于 2014-12-18 19:33 编辑
5.2 SVC异常 SVC多用在上了操作系统的软件开发中,不过也不是OS设计所必须的,比如μCOS-III和μCOS-II就没有使用此中断,而RTX却充分的利用了这个中断,为了方便大家的学习,我们对SVC也做一个详细的介绍。 5.2.1 SVC功能介绍 SVC用于产生系统函数的调用请求。例如,操作系统通常不让用户程序直接访问硬件,而是通过提供一些系统服务函数,让用户程序使用SVC发出对系统服务函数的呼叫请求,以这种方法调用它们来间接访问硬件。因此,当用户程序想要控制特定的硬件时,它就要产生一个SVC异常,然后操作系统提供的SVC异常服务例程得到执行,它再调用相关的操作系统函数,后者完成用户程序请求的服务。 SVC这种“提出要求——得到满足”的方式很好: l 它使用户程序从控制硬件的繁文缛节中解脱出来,而是由OS负责控制具体的硬件。 l OS的代码可以经过充分的测试,从而能使系统更加健壮和可靠。 l 它使用户程序无需在特权级下执行,用户程序无需承担因误操作而瘫痪整个系统的风险。 l 通过SVC的机制,还让用户程序变得与硬件无关,因此在开发应用程序时无需了解硬件的操作细节,从而简化了开发的难度和繁琐度,并且使应用程序跨硬件平台移植成为可能。开发应用程序唯一需要知道的就是操作系统提供的应用编程接口(API),并且在了解了各个请求代号和参数表后,就可以使用SVC来提出要求了(事实上,为使用方便,操作系统往往会提供一层封皮,以使系统调用的形式看起来和普通的函数调用一致。各封皮函数会正确使用SVC指令来执行系统调用)。其实,严格地讲,操作硬件的工作是由设备驱动程序完成的,只是对应用程序来说,它们也相当于操作系统的一部分。如下图所示: SVC异常通过执行”SVC”指令来产生。该指令需要一个立即数,充当系统调用代号。SVC异常服务例程稍后会提取出此代号,从而获知本次调用的具体要求,再调用相应的服务函数。例如, SVC 0x3 ; 调用3号系统服务 在SVC服务例程执行后,上次执行的SVC指令地址可以根据自动入栈的返回地址计算出。找到了SVC指令后,就可以读取该SVC指令的机器码,从机器码中萃取出立即数,就获知了请求执行的功能代号。如果用户程序使用的是PSP,服务例程还需要先执行MRS Rn, PSP指令来获取应用程序的堆栈指针。通过分析LR的值,可以获知在SVC指令执行时,正在使用哪个堆栈。 注意,我们不能在SVC服务例程中嵌套使用SVC指令(事实上这样做也没意义),因为同优先级的异常不能抢占自身。这种作**产生一个用法fault。同理,在NMI服务例程中也不得使用SVC,否则将触发硬fault。
5.2.2 SVC触发方式 SVC的异常号是11,支持可编程。SVC异常可以由SVC指令来触发,也可以通过NVIC来软件触发(通过寄存器NVIC->STIR触发软中断)。这两种方式触发SVC中断有一点不同:软件触发中断是不精确的,也就是说,抢占行为不一定会立即发生,即使当时它没有被掩蔽,也没有被其它ISR阻塞,也不能保证马上响应。这也是写缓冲造成的,会影响到与操作NVIC STIR相临的后一条指令:如果它需要根据中断服务的结果来决定如何工作(如条件跳转),则该指令可能会误动作——这也可以算是紊乱危象的一种表现形式。为解决这个问题,必须使用一条DSB指令,如下例所示: MOV R0,#SOFTWARE_INTERRUPT_NUMBER LDRR1,=0xE000EF00 ; 加载NVIC软件触发中断寄存器的地址 STR R0, [R1] ; 触发软件中断 DSB ; 执行数据同步隔离指令 ... 但是这种方式还有另一种隐患:如果欲触发的软件中断被除能了,或者执行软件中断的程序自己也是个异常服务程序,软件中断就有可能无法响应。因此,必须在使用前检查这个中断已经在响应中了。为达到此目的,可以让软件中断服务程序在入口处设置一个标志。而SVC要精确很多,SVC指令后,只要此时没有其它高优先级的异常也发生了,SVC中断服务程序可以得到立即执行。
5.2.3 SVC的使用 SVC是用于呼叫OS所提供的API(RTX是采用的这种方式)。用户程序只需知道传递给OS的参数,而不必知道各API函数的地址。SVC指令带一个8位的立即数,可以视为是它的参数,被封装在指令本身中,如: SVC 3 ;呼叫3号系统服务 则3被封装在这个SVC指令中。因此在SVC服务例程中,需要读取本次触发SVC异常的SVC指令,并提取出8位立即数所在的位段,来判断系统调用号,工作流程如下:
上面的流程图用汇编来实现就是如下这样: SVC_Handler TST LR, #0x4 ; 测试EXC_RETURN的比特2 ITE EQ ; 如果为0, MRSEQ R0, MSP ; 则使用的是主堆栈,故把MSP的值取出 MRSNE R0, PSP ; 否则, 使用的是进程堆栈,故把MSP的值取出 LDR R1, [R0,#24] ; 从栈中读取PC的值 LDRB R0, [R1,#-2] ; 从SVC指令中读取立即数放到R0 ; 准备调用系统服务函数。这需要适当调整入栈的PC的值以及LR(EXC_RETURN),来进入OS内部 BXLR ; 借异常返回的形式,进入OS内部,最终调用系统服务函数. 上面的汇编代码结合着流程图就很好理解了,目的只有一个:得到调用号,用它来调用系统服务函数。接下来我们说一下如何在C中使用SVC。 因为晚到中断的关系(为什么这么说,请看Cortex-M3权威指南中文版11.6小节),SVC中不能再使用寄存器来传递参数,而是必须使用堆栈。因此,需要使用一段汇编代码来给SVC函数传参数。如果SVC服务例程的主部由C来写,则必须在前面伴随一个汇编写的封皮,用于把堆栈中的参数提取到寄存器中。下面给出一段代码来演示这个工作。这些代码是要使用ARM的编译(armcc)和汇编(armasm)工具来处理的,RVDS和Keil RVMDK都使用这个工具链。 /*汇编封皮,用于提出堆栈帧的起始位置,并放到R0中,然后跳转至实际的SVC服务例程中 */ __asmvoid SVC_Handler(void) { TSTLR, #4 ; Test bit 2 of EXC_RETURN ITEEQ MRSEQR0, MSP ; if 0, stackingused MSP, copy to R0 MRSNER0, PSP ;if 1, stacking used PSP, copy to R0 B__cpp(SVC_Handler_C) ALIGN4 } /*不必写下BX LR来返回,而是由svc_handler来做决定 */ 接下来的SVC服务例程的主体就可以由C来写了,它使用R0作为输入参数(这也是堆栈帧的起始位置),用于进一步提取服务代号,并且传递参数(通过堆栈中的R0-R3)。 //堆栈内容: //r0, r1, r2, r3, r12, r14, the return address and xPSR //- Stacked R0 = svc_args[0] //- Stacked R1 = svc_args[1] //- Stacked R2 = svc_args[2] //- Stacked R3 = svc_args[3] //- Stacked R12 = svc_args[4] //- Stacked LR = svc_args[5] //- Stacked PC = svc_args[6] //- Stacked xPSR= svc_args[7] voidSVC_Handler_C(unsigned int * svc_args) { uint8_tsvc_number; uint32_tstacked_r0, stacked_r1, stacked_r2, stacked_r3;
svc_number= ((char *) svc_args[6])[-2]; //Memory[(Stacked PC)-2] stacked_r0= svc_args[0]; stacked_r1= svc_args[1]; stacked_r2= svc_args[2]; stacked_r3= svc_args[3]; //.other processing . // Return result (e.g. sum of first two arguments) svc_args[0]= stacked_r0 + stacked_r1; return; } 后面会有一个专门的例子跟大家讲解SVC的使用。
|