完善资料让更多小伙伴认识你,还能领取20积分哦, 立即完善>
本帖最后由 硬汉Eric2013 于 2015-1-8 17:46 编辑 第5章 任务切换设计 本期教程带领大家学习简易任务切换的设计,主要是学习SVC(Supervisor Call)和PendSV(Pendable Service Call)的使用,并在此基础上设计一个简单的时间片调度器。本期教程要涉及到一些汇编的知识,不过不用担心,用户只需了解一些简单的汇编命令就可以了,最重要的还是理解任务是如何实现切换的。 5.1 中断的响应序列 5.2SVC异常 5.3PendSV异常 5.4实验例程说明 5.5实验总结 5.1 中断的响应序列 由于SVC和PendSV中断需要用汇编来实现,用汇编来实现就得了解发生中断事件后,处理器内核是如何响应这个过程的。总的来说,当CM3/CM4开始响应一个中断时,会按照如下过程响应中断事件: l 入栈:不带浮点寄存器的情况下会有8个寄存器的值压入栈,带浮点寄存器的情况下会有26个寄存器压入栈。 l 取向量:从向量表中找出对应的服务程序入口地址。 l 选择堆栈指针MSP/PSP,更新堆栈指针SP,更新连接寄存器LR,更新程序计数器PC。 5.1.1 入栈 响应异常的第一个行动,就是自动保存现场的必要部分:依次把xPSR,PC, LR, R12以及R3-R0由硬 件自动压入适当的堆栈中(如果程序中做了浮点运算,还有18个浮点寄存器需要入栈):如果当响应异常时,当前的代码正在使用PSP,则压入PSP,也就是使用进程堆栈;否则就压入MSP,使用主堆栈。一旦进入了服务例程,就将一直使用主堆栈。 下表是带有浮点寄存器和不带有浮点寄存器的异常堆栈,地址由上到下递减。 注意左侧的浮点寄存器列表,在寄存器FPSCR上面是一个保留寄存器,在给任务堆栈分配大小时要考虑到这个寄存器,同时要注意这些寄存器的存储顺序。上面的寄存器是自动入栈的,还有寄存器R4-R11以及浮点寄存器S16-S31需要手动入栈的。这里有三个知识点,大家要注意: l 双字对齐的堆栈工作模式(大家可以查阅Cortex-M3权威指南中文版9.1.1小节进行了解,Cortex-M4现在最新的r0p1版,不知道此功能是否自动打开,后面查阅相关资料了,再做详细介绍)。 l 为什么R0-R3以及R12可以自动的入栈,而R4-R11是手动入栈?原因就在于ARM上,有一套的C函数调用标准约定(《C/C++ Procedure Call Standard for the ARM Architecture》,AAPCS, Ref5)。各种原因就在它上面:它使得中断服务例程能用C语言编写,编译器优先使用入栈了的寄 器来保存中间结果(当然,如果程序过大也可能要用到R4-R11,此时编译器负责生成代码来push它们)。 l 为什么R0-R3, R12是最后被压进去的?这样可以更容易地使用SP基址来索引寻址,(这也方便了LDM等多重加载指令。因为LDM必须加载地址连续的一串数据,而现在R0-R3, R12的存储地址连续了)。这种顺序也舒展了参数的传递过程:使之可以方便地通过读取入栈了的R0-R3取出 (主要为系统软件所利用,多见于SVC与PendSV中的参数传递)。 5.1.2 取向量 当数据总线(系统总线)正在进行入栈操作时,指令总线(I-Code总线)执行另一项重要的任务:从向量表中找出正确的异常向量,然后在服务程序的入口处预取指。由此可以看到各自都有专用总线的好处:入栈与取指这两个工作能同时进行。 5.1.3 更新寄存器 在入栈和取向量操作完成之后,执行服务例程之前,还要更新一系列的寄存器: l SP:在入栈后会把堆栈指针(PSP或MSP)更新到新的位置。在执行服务例程时,将由MSP 负责对堆栈的访问。 l PSR:更新IPSR位段(地处PSR的最低部分)的值为新响应的异常编号。 l PC:在取向量完成后,PC将指向服务例程的入口地址。 l LR:在出入ISR的时候,LR的值将得到重新的诠释,这种特殊的值称为“EXC_RETURN”(这个在上期教程有讲解)在异常进入时由系统计算并赋给LR,并在异常返回时使用它。 以上是在响应异常时通用寄存器的变化。另一方面,在NVIC中,也会更新若干个相关有寄存器。例如,新响应异常的悬起位将被清除,同时其活动位将被置位。 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都使用这个工具链。
接下来的SVC服务例程的主体就可以由C来写了,它使用R0作为输入参数(这也是堆栈帧的起始位置),用于进一步提取服务代号,并且传递参数(通过堆栈中的R0-R3)。
后面会有一个专门的例子跟大家讲解SVC的使用。 5.3 PendSV异常 可以说PendSV (PendedService Call)是OS设计中最重要的中断,OS中可以没有SVC的支持,但是PendSV必须得有。PendSV的异常类型时14,优先级可编程。中断可以通过设置ICSR寄存器的挂起位进行触发。不像SVC,PendSV的中断是不准确的,所以它的挂起状态可以在其它高优先级中断服务程序中进行设置,等中断服务程序执行完以后再执行。 鉴于这种特性,我们可以设置PendSV为最低优先级,这样等其它高优先级的中断任务执行完以后再执行PendSV中断,这种特性在OS的上下文切换中非常重要。 5.3.1 OS中使用PendSV 首先,我们来看一下上下文切换的基本概念。在一个典型的嵌入式OS中,处理时间被分成很多的时间片。例如,在一个系统中有两个任务,两个任务的执行是可选择的,如下图所示(两个任务间通过SysTick进行轮转调度的简单模式),OS内核可以通过下面的方式进行触发: l 系统滴答定时器(SYSTICK)中断。 l 用户任务执行SVC指令。比如,由于应用任务等待事件或者数据,此任务将被挂起,然后调用系统服务切换到另一个任务中。 上图是两个任务轮转调度的示意图。但若在产生SysTick异常时正在响应一个中断,如下图所示,则SysTick异常会抢占其ISR。在这种情况下,OS是不能执行上下文切换的,否则将使中断请求被延迟,而且在真实系统中延迟时间还往往不可预知——任何有一丁点实时要求的系统都决不能容忍这种事。因此,在CM3/CM4中也是严禁没商量——如果OS在某中断活跃时尝试切入线程模式,将触犯用法fault异常。 为解决此问题,早期的OS大多会检测当前是否有中断在活跃中,只有在无任何中断需要响应时,才执行上下文切换(切换期间无法响应中断)。然而,这种方法的弊端在于,它可以把任务切换动作拖延很久(因为如果抢占了IRQ,则本次SysTick在执行后不得作上下文切换,只能等待下一次SysTick异常),尤其是当某中断源的频率和SysTick异常的频率比较接近时,会发生“共振”,使上下文切换迟迟不能进行。 现在好了,PendSV来完美解决这个问题了。PendSV异常会自动延迟上下文切换的请求,直到其它的ISR都完成了处理后才放行。为实现这个机制,需要把PendSV编程为最低优先级的异常。如果OS检测到某IRQ正在活动并且被SysTick抢占,它将悬起一个PendSV异常,以便缓期执行上下文切换。如下图所示。 为方便大家理解,下面是上图的执行流程: 1. 任务A呼叫SVC来请求任务切换(例如,等待某些工作完成) 2. OS接收到请求,做好上下文切换的准备,并且悬起一个PendSV异常。 3. 当CPU退出SVC后,它立即进入PendSV,从而执行上下文切换。 4. 当PendSV执行完毕后,将返回到任务B,同时进入线程模式。 5. 发生了一个中断,并且中断服务程序开始执行 6. 在ISR执行过程中,发生SysTick异常,并且抢占了该ISR。 7. OS执行必要的操作,然后悬起PendSV异常以作好上下文切换的准备。 8. 当SysTick退出后,回到先前被抢占的ISR中,ISR继续执行 9. ISR执行完毕并退出后,PendSV服务例程开始执行,并且在里面执行上下文切换 10.当PendSV执行完毕后,回到任务A,同时系统再次进入线程模式。 5.3.2 裸机中使用PendSV 大家最常见的都是在OS中使用PendSV,在裸机中也用到PendSV。比如,一个中断服务程序需要大量的执行时间,这个程序中有一部分需要高优先级中断中执行,如果所有的中断服务程序都在这个高优先级任务中执行将阻塞低优先级中断的执行,为了解决这个问题,我们可以讲这个程序分为两个部分: l 前半部分是时间关键的部分,需要在高优先级任务中快速执行。我们将其放在高优先级的中断服务程序中,程序末尾将PendSV中断使能。 l 后半部分放在PendSV中断中执行,此时PendSV被设置为最低优先级的中断。 |
|
相关推荐
|
|
您好,有个问题一直不明白,以任务0的堆栈为例,任务0堆栈的大小task0_stack[1024],而在计算任务0的堆栈指针时:PSP_array[0] = ((unsigned int) task0_stack) + (sizeof task0_stack) - 16*4;按照这样计算,你的任务堆栈大小应该为17,而不是1024,您能解释一下吗?实在不明白。谢谢
|
|
|
|
|
|
811 浏览 0 评论
5294 浏览 0 评论
如何使用python调起UDE STK5.2进行下载自动化下载呢?
2684 浏览 0 评论
开启全新AI时代 智能嵌入式系统快速发展——“第六届国产嵌入式操作系统技术与产业发展论坛”圆满结束
3032 浏览 0 评论
获奖公布!2024 RT-Thread全球巡回线下培训火热来袭!报名提问有奖!
32361 浏览 11 评论
73322 浏览 21 评论
小黑屋| 手机版| Archiver| 电子发烧友 ( 湘ICP备2023018690号 )
GMT+8, 2024-11-28 19:21 , Processed in 0.671165 second(s), Total 42, Slave 33 queries .
Powered by 电子发烧友网
© 2015 bbs.elecfans.com
关注我们的微信
下载发烧友APP
电子发烧友观察
版权所有 © 湖南华秋数字科技有限公司
电子发烧友 (电路图) 湘公网安备 43011202000918 号 电信与信息服务业务经营许可证:合字B2-20210191 工商网监 湘ICP备2023018690号