μC/OS-Ⅱ移植的要点在哪里?初始化任务堆栈时有两个返回地址是怎么回事?其中一个永远用不到,可以省略吗?……
这些问题是移植μC/OS-Ⅱ的初学者常会遇到的问题,我也是μC/OS-Ⅱ的初学者,因需要曾两次移植μC/OS-Ⅱ,能体会遇到这些问题时的茫然,现在基本上明白了移植时遇到的一些问题和一点通用原理,就想把这些东西尽可能用通俗易懂的方式写下来,希望有助于跟我一样的广大初学者学习μC/OS-Ⅱ。因本人是计算机专业的学生,描述自然也是从计算机的角度进行的,所以可能更适合学计算机的一起参考。既然是初学者,文中错漏肯定不少,请大家多多指正,先谢过啦!^_^
补充:
1.这次的移植是为了做学校的毕业设计,为了避免到时学校有人在网上看到这篇文章还反而说我是抄别人的,请大家如果想转载的话,务必把这段话也一起转过去,我在图片里也加了水印,望理解,谢谢^_^
2.因为我打算把这篇文章发到多个论坛,所以若有更正我只会在博客那里更正,其他地方请恕不加更正了,博客地址为http://blog.csdn.net/speedan/archive/2010/04/05/5451235.aspx。
μC/OS-Ⅱ的移植要点小谈
μC/OS-Ⅱ在移植过程中最难理解也是最重要的地方就是任务的“上下文”的处理,这个过程一般是与堆栈的处理相结合的。下边从一般的操作系统开始,逐渐引入μC/OS-Ⅱ,以说明μC/OS-Ⅱ对任务的“上下文”的处理过程,再逐个说明μC/OS-Ⅱ各个需要移植的函数,特别是其中移植时较难理解和需要注意的地方,最后再说明如何让μC/OS-Ⅱ在MSP430上的移植能支持用C语言编写中断处理函数。
目录
μC/OS-Ⅱ的移植要点小谈
一、引入
二、μC/OS-Ⅱ在移植过程中对任务上下文环境的处理
三、μC/OS-Ⅱ各个需要移植函数的说明
(1) Os_cpu_c.c文件中的OSTaskStkInit()函数
(2) Os_cpu_a.asm文件中的OSStartHighRdy ()函数:
(3) Os_cpu_a.asm文件中的OSCtxSw()函数:
(4) 中断服务函数OSTickISR()
(5) Os_cpu_a.asm文件中的OSIntCtxSw ()函数
四、让μC/OS-Ⅱ在MSP430上的移植支持用C语言编写中断处理程序
一、引入
从微机原理的课程中可以知道,计算机指令是在CPU中执行的,在执行过程中,CPU是通过寄存器存取数据和指令的,因此每个CPU都有一套内部寄存器协助其工作,这套寄存器我们一般称之为“上下文环境”(Context,下边简称为“上下文”),一般包括程序计数器(PC,Program Counter的缩写)、状态寄存器(SR,Status Register)其他一些通用寄存器(General-Purpose Registers),其中PC存放下一条将要执行的指令在代码段的地址,任务切换时把PC的值保存在当前任务的堆栈中,才能在任务切换回来后从这个地址继续执行。
从操作系统的课程中可以知道,多任务OS(操作系统)要求做到每个独立的任务从自己的角度看,都会觉得自己独占CPU,因此每个任务都会有自己的“上下文环境”,一般保存在任务的堆栈中,当该任务运行时,就会从堆栈中弹出来,放入对应的寄存器中。当一个运行中的任务A因为某种原因要切换到其他任务去运行时,A必须保存自己的上下文,以保证以后再次切换回A运行时,能从已保存的上下文中恢复回来。这样当A再次运行时,对A而言上下文根本没有变化过,好像从来没有进行过任务切换一样,其他任务的切换也是这样。这就是多任务切换的一个过程。
μC/OS-Ⅱ也是一个多任务OS,也得有这样一个任务切换的过程,即也得保存用到的所有寄存器,这样就得直接操纵寄存器。但是μC/OS-Ⅱ是用C语言写的(为了可移植性等),而C语言是不能直接操纵寄存器的,只有汇编才能直接操纵寄存器。因此,在任务切换过程中必须想办法从C语言中调用汇编语言,以通过汇编来操纵寄存器。但是,不同的编译器对从C语言调用汇编这个方面有不同的规定和实现:
1. 有的编译器是允许直接在C语言中内嵌汇编的,所以可以直接在C语言中内嵌汇编来操纵寄存器;
2. 有的编译器不允许直接在C语言中内嵌汇编,但允许直接从C语言中调用汇编子程序(当然被调用的汇编子程序是放在汇编文件中的)。这样就可以在汇编子程序中操纵寄存器,再在C语言中进行调用;
3. 有的编译器既不允许在C语言中内嵌汇编,也不允许从C语言中调用汇编子程序,但可以实现软中断(即软件中断),所以可以用软中断的方式进入汇编子程序,再在汇编子程序中操纵寄存器。
这就是从C语言调用汇编的一般的三种实现方法。μC/OS-Ⅱ在不同编译器下也是用这三种方法实现从C语言中进入汇编,以实现任务切换的。
二、μC/OS-Ⅱ在移植过程中对任务上下文环境的处理
正如上边所说的,在任务切换时,OS得保存任务中用到的所有寄存器,而μC/OS-Ⅱ为了通用性考虑,一般情况下都是直接保存所有寄存器,而不管这些寄存器是否用到,一般的移植方案也是这样做的。
任务的上下文在切换时一般都是保存在堆栈中的。根据堆栈“先入后出(FILO)”的特性,保存寄存器的顺序和恢复寄存器的顺序必须是对应反过来的。比如:假设总共有R1、R2、R3三个寄存器,保存上下文时应该把这3个寄存器的内容(即数据,或称为“值”)按照以下顺序保存到堆栈中:R1->R2->R3,那恢复上下文时就应该按照以下顺序从堆栈中把对应寄存器的内容弹出到寄存器中:R3->R2->R1。
观察μC/OS-Ⅱ的移植要求,可以发现μC/OS-Ⅱ会处理到任务的上下文(这里的“上下文”只包括用到的所有寄存器,不包括通过堆栈传递的参数,这个之后再讨论)的地方有5个:
a)(Os_cpu_c.c文件中的OSTaskStkInit()函数)μC/OS-Ⅱ中每个任务都有自己的堆栈,新建立一个任务时,必须初始化该任务的上下文,这些被初始化的上下文放在该任务的堆栈中(任务建立时堆栈中除了放初始化的上下文,还会放其他东西,后边会讲到),将会在任务开始运行时从堆栈中弹出来放到对应的寄存器中。但由于任务未曾运行过,所以一般情况下,除了状态寄存器(它的中断位决定着任务开始运行时中断是否开启)和程序计数器(放置任务的起始地址)必须认真设置之外,任务的上下文被初始化成什么值是无所谓的。但是由于μC/OS-Ⅱ会给新建立的任务传递一个参数,而有的编译器会把前几个参数通过寄存器传递,所以也会涉及到上下文的初始化。
b)(Os_cpu_a.asm文件中的OSStartHighRdy ()函数)μC/OS-Ⅱ总是运行当前处于就绪状态的最高优先级的任务(Highest Priority Task,后边简称为HPT)。系统启动时,需要运行第一个任务,这个任务也必须是当前的HPT,而启动这个HPT的工作是由函数OSStart()执行的。
OSStart()函数先找出当前的HPT,再调用OSStartHighRdy (),在OSStartHighRdy ()中把HPT的上下文从HPT的堆栈中弹出到对应的寄存器中,以启动HPT的运行,即μC/OS-Ⅱ中第一个运行的任务。
c)
(Os_cpu_a.asm文件中的OSCtxSw()函数)这个函数执行任务切换的工作,函数中先把当前任务的上下文保存到该任务的堆栈中,再把HPT的上下文弹出到对应的寄存器中,任务切换就完成了。
d)(所有的中断服务函数,即ISR(Interrupt Service Routine))中断处理过程中:1先保存当前运行任务A的上下文,2进行具体的中断处理,3把HPT的上下文弹出到对应的寄存器中,以转入任务HPT运行【注意当A是当前最高优先级任务时,A和HPT是相同的】。
e)
(Os_cpu_a.asm文件中的OSIntCtxSw ()函数)这个函数是第(3)点中的3处调用的,用来检查A是否HPT:1如果A不是HPT就把HPT的上下文弹出到对应的寄存器中,以转入HPT运行;2如果A是HPT就返回调用处(即ISR中),在ISR中会把A的上下文弹出到对应的寄存器中,以转入任务A运行。
从a)~e)可以看出,只要这5个地方做到
上下文的保存和弹出顺序完全一致(即全部对应反过来),基本上就不会出什么问题。
那现在问题就在于上下文保存的“顺序”由什么决定?a)~e)中,有两个因素决定了这个“顺序”,这是不同的处理器所支持的汇编指令、中断和软中断的处理过程不一样所引起的:
(1)c)的中断处理与硬件相关。有的在发生中断或软中断时,系统会默认保存所有寄存器,即把所有寄存器压入堆栈中,当然压入是按一定的
顺序进行的(这样如果用软中断实现任务切换时,我们就不用再保存所有的寄存器了,因为系统已经帮我们做了这个工作);但有的系统默认只保存一部分寄存器,这样我们就必须在中断服务函数中保存其他还没保存的寄存器;
(2)有些处理器支持可以用一条汇编指令从堆栈中保存或恢复好几个寄存器,而保存或恢复是有固定
顺序的,这个顺序也是硬件决定的。有的处理器支持用一个汇编指令就可以保存所有的寄存器到堆栈中,同样用一个汇编指令也可以从堆栈中弹出所有寄存器;有的就不支持,这时就得用多条指令,逐个保存或弹出寄存器。在汇编程序中如果使用了某些特殊汇编指令,就得按照这些指令规定的顺序保存和恢复上下文。如果处理器支持这样的汇编指令,一般也是主要用于在进入和退出中断时用来保存和恢复多个寄存器,所以应该与(1)中的顺序一致。
三、μC/OS-Ⅱ各个需要移植函数的说明
接下来结合μC/OS-Ⅱ在80x86处理器和MSP430单片机的移植实例具体说明每个需要移植的函数,但主要讲与堆栈有关的部分,其他由μC/OS-Ⅱ系统规定的须在移植函数中做的事情,比如调用各个OS**Hook()函数之类的细节就暂且略过,因为《嵌入式实时操作系统μC\OS-II(第2版)》(邵贝贝译)中已经讲得很清楚。另外由于本人对80x86处理器并不熟悉,所以对80x86处理器部分的讲解主要是根据《嵌入式实时操作系统μC\OS-II(第2版)》中的例子进行的。
(1) Os_cpu_c.c文件中的OSTaskStkInit()函数
在这个函数中,我将会重点讲一下前边提到的传递参数的问题。
OSTaskStkInit()函数的功能是为新建立的任务准备一个
初始化好的堆栈,假设这个任务为MyTask(void *pdata)。那现在的问题是初始化这个堆栈时得在堆栈中放置什么东西呢?这就得考虑到MyTask(void *pdata)的
二重身份的要求了:
(1)
在编译器眼中,MyTask(void *pdata)只是一个
函数,也是以编译一个函数的方式为MyTask(void *pdata)生成目标代码的,所以初始化堆栈时,就得考虑到编译器在调用一个函数时对堆栈的要求。编译器调用一个函数时最重要的有两点,一是函数的返回地址要保存到堆栈中,以便函数执行完后能从这个地址返回,但MyTask(void *pdata)并不是真的从C语言里进行调用的,而是从汇编子程序OSStartHighRdy()、OSCtxSw()和OSIntCtxSw ()三个中的一个或者从ISR中进入的,也永远不会返回,所以这个地址是什么
无关紧要;另一点是参数的传递,不同编译器传递参数的方法是不同的:1有的编译器把所有的参数都通过堆栈进行传递,这样参数就必须放到堆栈中存储参数的单元中;2有的编译器把前一个或多个参数通过寄存器传递,其他参数才通过堆栈传递,同样,这些参数也必须放到相应的寄存器或堆栈单元中。当在MyTask(void *pdata)中用到这个参数时,就会到编译器保存参数的地方去取,因此,在初始化新建的任务的堆栈时,得按照编译器的规定把传递的参数(即pdata)放到对应的堆栈单元或寄存器中。当然,这两种情况下建立的任务堆栈所占用的
空间大小上有区别。
(2) 在μC/OS-Ⅱ中,MyTask(void *pdata)则是一个
任务。按照前边说的,任务运行时得有自己的上下文,μC/OS-Ⅱ要求一个新建立的任务把上下文(即全部的寄存器)放在该任务的堆栈中。
为了满足上边2点要求,μC/OS-Ⅱ把堆栈初始化成
发生一个事件后的堆栈的情况,这个事件就是:当从某个地方调用MyTask(void *pdata),并准备进入MyTask()运行时突然发生了中断,如
图1所示。从某个地方调用MyTask(void *pdata)时,会把函数返回地址和传递的参数pdata放置好;进入ISR运行前,会把当前的上下文保存到堆栈中。这样堆栈就初始化完成了。堆栈中被初始化的单元,可能被以下3个函数的任何一个在被调用时从堆栈中弹出:OSStartHighRdy()、OSCtxSw()和OSIntCtxSw (),也可能在ISR中切换到HPT时,新任务作为HPT而使其堆栈单元被弹出,以转入该任务运行。
下边分别在80x86处理器和MSP430单片机的环境下对堆栈情况进行说明:
【
注意:80x86处理器和MSP430单片机对寄存器的名字称呼是不同的:我们平常所说的程序计数器,在80x86中称为IP(Instruction Pointer),而在MSP430上称为PC(Program Counter);状态寄存器在80x86中称为SW(Status Word),在MSP430上称为SR(Status Register);其他通用寄存器也类似,因为用不到,就不多加说明了。
】
1 80x86的堆栈是满递减堆栈,函数调用时所有的参数都是通过堆栈传递的。当调用一个函数时,函数堆栈的情况如
图2所示;OSTaskStkInit()函数初始化后的堆栈情况如
图3所示,图中的“函数返回地址”虽然永远不会用到,但堆栈中还是得为它空出一个单元,因为参数pdata放在堆栈中,更具体的下边会讲到。
2 MSP430的堆栈也是满递减堆栈,函数调用时第1-4个参数通过寄存器R12-R15进行传递,其他参数则通过堆栈传递。当调用一个函数时,函数堆栈的情况如
图4所示;OSTaskStkInit()函数初始化后的堆栈情况如
图5所示,图中的“函数返回地址”永远不会用到,而且参数pdata放在寄存器R12中,所以OSTaskStkInit()函数在初始化堆栈时可以不为“函数返回地址”留出空间,即可以省略掉,更具体的下边会讲到。
此外,把任务的上下文和参数放到堆栈中,还要考虑两个问题:
(1) 一个是状态寄存器(即图中的处理器状态字)的中断位是否置位,以决定任务开始运行时中断是否开启,而是否开启是由用户自己决定的,但要注意这将影响到所有的任务,包括OSTaskIdle()和OSTaskStat()任务,这两个任务运行时必须要开启中断,关于这个问题的讨论请参见《嵌入式实时操作系统μC\OS-II(第2版)》(邵贝贝译)P343页;
(2) 另一个问题是,就是上边第3点提到的那样,堆栈中放置任务上下文的
“顺序”是由中断处理函数保存寄存器的顺序和使用的一些特殊汇编指令决定的,所以在堆栈中初始化任务的上下文时,也得按照这个
顺序进行。
(2) Os_cpu_a.asm文件中的OSStartHighRdy ()函数:
这个函数做的工作已经在上边的3.2中提到过了,其实它做的工作就是图1中的(2)部分所做的工作。进入MyTask(void *pdata)后,在MyTask(void *pdata)中取参数pdata时,对于80x86和MSP430是不同的:
1 对应到图3,对于80x86,当进入到MyTask(void *pdata)后,“上下文”部分已经从堆栈中弹出了,此时堆栈中只剩下参数pdata 和“函数返回地址”,堆栈指针指向“函数返回地址”所在单元,所以当在MyTask(void *pdata)中想要取得参数pdata,编译器会自动到“函数返回地址”所在单元的上一个单元处取得。因此,即使“函数返回地址”永远不会用到,但是堆栈中还是得为它空出一个单元。
2对应到图5,对于MSP430,当进入到MyTask(void *pdata)后,“上下文”部分已经从堆栈中弹出了,此时堆栈中只剩下“函数返回地址”,堆栈指针指向“函数返回地址”所在单元,参数pdata放在寄存器R12中。所以当在MyTask(void *pdata)中想要取得参数pdata,编译器会自动到R12中去取。因此,“函数返回地址”永远不会用到,所以在OSTaskStkInit()函数中初始化任务的堆栈时,不用为“函数返回地址”空出一个单元,即这个单元可以省略掉。
(3) Os_cpu_a.asm文件中的OSCtxSw()函数:
这个函数做的工作已经在上边的3.3中提到过了。
(4) 中断服务函数OSTickISR()
中断处理函数所需要做的工作在3.4中已经提到过了,其实每个中断服务函数的工作除了具体服务不同之外,其他部分都相同,比如进入和退出中断的过程。但这其中有一点得注意一下,就是在μC/OS-Ⅱ比较早期的版本中,中断堆栈和任务堆栈是没有分开的,所以在考虑每个任务的任务堆栈大小时,除了得考虑任务本身需要用到的部分,还得考虑中断时用到的堆栈,这样导致的直接后果就是每个任务都得提供一个中断堆栈,有多少个任务,就会有多少个中断堆栈。但其实这是没有必要的,因为中断的处理与任务在堆栈的使用上并没有必然联系,所以可以单独为中断提供一个堆栈作为中断堆栈,以使中断堆栈和任务堆栈分开,这个在μC/OS-Ⅱ的后期版本中得到了实现。
μC/OS-Ⅱ从main()函数中调用OSStartHighRdy ()函数切换到HPT运行后,永远不会再返回,所以main()函数所使用的堆栈(即系统堆栈)以后都不会再用到,正好拿来作为中断堆栈。作者在OSStartHighRdy ()函数中把main()函数所使用的堆栈地址保存在OSISRStkPtr全局变量中,以后在中断服务函数中就通过把这个地址赋值给SP,让系统堆栈作为中断堆栈。
(5) Os_cpu_a.asm文件中的OSIntCtxSw ()函数
这个函数做的工作已经在上边的3.5中提到过了。
四、让μC/OS-Ⅱ在MSP430上的移植支持用C语言编写中断处理程序
MSP430的一大特色是对C语言的支持相当好,连中断处理函数也能用C语言方便地实现,很多地方C编译器都已经帮用户做好。可惜的是μC/OS-Ⅱ在MSP430F149上的官方移植程序的中断处理函数必须用汇编语言才能编写,但移植μC\OS-II的其中一个目的就是为了更方便的编写程序,这样就与初衷有所矛盾。虽然也可以用C语言编写函数做中断处理的工作,然后再由汇编语言编写ISR对这个C函数进行调用,但对于一些并不熟悉汇编的用户而言,也是比较麻烦的,特别是当需要向C函数传递参数或获取返回值时,还得清楚从汇编向C函数传递参数的具体规则。所以我决定想办法改变移植的程序,使其在C语言中也能直接编写ISR。我使用的集成开发环境是官方网站提供的CCSv4.0(Code Composer Studio Version 4.0)。
不能用C语言编写ISR的主要原因在于μC/OS-Ⅱ对ISR的编写有一套要求,普通用C语言编写的ISR不能满足的要求只有一个,就是μC/OS-Ⅱ要求进入ISR时要保存当前任务的上下文,即
全部的寄存器,所以还是得借助汇编语言。CCSv4.0提供的编译器(下文简称为编译器)支持直接在C语言中内嵌汇编,也可以在C语言程序中直接调用汇编子程序。如果采用C语言中内嵌汇编的方式,每次都得在C程序中多加一段汇编代码,而从C程序中调用汇编子程序,跟调用普通C函数是一样用的,所以从编程的方便性考虑,我决定使用第2种方法。
C编译器在编译C语言编写的ISR时有一个规则,即在ISR中以及在ISR中调用的函数中用到的寄存器都必须在进入ISR时先保存在堆栈中,退出ISR时再从堆栈中恢复,对于C语言编写的ISR,这些是编译器
自动完成的。这对于平时在C语言中编写ISR是很方便的,但对于移植了μC/OS-Ⅱ的程序,用C语言编写ISR却会引起一个比较大的问题,就是如何保存所有寄存器的问题。因为编译器会自动保存用到的寄存器,那根据ISR中执行的程序不同,有可能每个ISR中自动保存的寄存器数量就不同,这样的话每次用户通过汇编保存的寄存器数量就无法确定。
但编译器调用函数时对寄存器的保存规则,使自动保存的寄存器数量固定下来成为可能。MSP430的寄存器按照保护方式分为两分为两套,在进行函数调用时默认保存的地方是不同的:1一套是调用保存(Save-on-call)的寄存器,即由调用其它函数的函数负责保存这些寄存器的内容,这套寄存器是R11-R15;2 另一套是入口保存(Save-on-entry)的寄存器,即由被调用的函数负责保存这些寄存器的内容,这套寄存器是R4-R10。如果在ISR中调用其他函数(
无论是C函数还是汇编子程序),编译器都会默认在ISR中保存Save-on-call的寄存器(R11-R15)。
我按照μC/OS-Ⅱ对ISR的编写要求编写了相应的C语言代码(不包括保存和恢复全部寄存器的部分),由于其中得调用其他函数,所以编译器至少得保存Save-on-call寄存器(R11-R15),而代码中的其他部分用到的寄存器也不会超出R11-R15,所以编译器也只会自动保存和恢复R11-R15。这样只要我在ISR的起始位置调用汇编子程序对其他寄存器(即R4-R10)进行保存,在ISR的结束位置再调用汇编子程序对R4-R10进行恢复,而ISR中实际要做的服务(比如时钟节拍ISR中要调用OSTimeTick()做实际的工作)
仅通过调用其他函数来完成,这样在ISR中不会再用到除R11-R15之外的寄存器,就可以把ISR自动保存的寄存器数量固定为R11-R15这5个寄存器,因此使用这个方法在C语言中也能直接编写ISR了。
对于保存寄存器的顺序,前边已经作了说明,这个顺序是由中断处理函数保存寄存器的顺序和使用的一些可以保存全部寄存器的特殊汇编指令决定的,MSP430没有这种特殊汇编指令,所以这个顺序就由中断处理函数保存寄存器的顺序决定,其他会处理到任务的上下文的地方只要跟这个顺序一致就行了。
但请注意移植的程序是
有局限性的,因为需要
依赖于具体的编译器。比如在IAR中编译器默认保存的寄存器可能就不是R11-R15了,而是其他寄存器。当然方法也一样,也可以同样通过调用汇编子程序对其余寄存器进行保存和恢复,但具体是哪些寄存器要在汇编子程序中保存就得查看编译器手册才能确定。
-------------------------------------------------------------------------------------------------------------------------------
上边说到,我让μC/OS-Ⅱ在MSP430上的移植支持C语言编写中断处理程序(ISR),主要思想在上边已经说了,实现时稍难处理的的地方就是调用汇编子程序对R4-R10进行保存和恢复。因为从C语言中调用其他函数(
无论是C函数还是汇编子程序)时,编译器会自动在堆栈中压入一个返回地址,这个也涉及到堆栈的操作,所以在汇编子程序得注意堆栈指针的操作。
下边结合移植的程序说下我使用的方法(请结合我移植的程序一起看)。
1. Push_R10_to_R4()汇编子程序实现保存R10~R4到堆栈中:
图6是从C语言中进入Push_R10_to_R4()汇编子程序时的堆栈,堆栈指针SP指向(1)处,PC_O是进入ISR时由编译器自动压入堆栈的中断返回地址,PC_i是从C语言的ISR中调用汇编子程序时压入堆栈的返回地址。图7是我们希望从汇编子程序中执行RET返回前的堆栈,以下是从图6转变图7的过程:
(a)把PC_i从图6的(1)处移到(2)处,为R10~R4留出空间;
(b)把R10~R4压入堆栈,此时的堆栈情形见图7;
(c)执行RET,使PC_i从堆栈中弹出放入PC寄存器中,实现从汇编子程序返回到ISR中。
2. Pop_R4_to_R10()汇编子程序实现从到堆栈中恢复R10~R4:
从C语言中进入Pop_R4_to_R10()汇编子程序时的堆栈跟图7一样,堆栈指针SP指向(2)处。图8是我们希望从汇编子程序中执行RET返回前的堆栈,执行RET会使PC_i从堆栈中弹出放入PC寄存器中,实现从汇编子程序返回到ISR中。以下是从图7转变图8的过程:
(a)把R10从图7的(1)处(堆栈中)移入R10(寄存器),为PC_i留出空间
(b)把PC_i从图7的(2)处入到(1)处,此时的堆栈情形见图8;
(c)把R4~R9从堆栈中弹出来放入到对应的寄存器中,此时的堆栈情形同图6;
(d)执行RET,使PC_i从堆栈中弹出放入PC寄存器中,实现从汇编子程序返回到ISR中。
;********************************************************************************************************
; PUSH AND POP CPU'S REGISTER R4~R10
;
; Description: These functions are used to push and pop cpu's register R4~R10 in ISR(Interrupt service
; routine)
;
; Notes : "PC_i" below represent the return address to the ISR, which is push into the stack when
; Push_R10_to_R4 or Pop_R4_to_R10 is called by ISR.
; Please pay attention to the sequence of the registers been pushed or pop from the stack.
;********************************************************************************************************
Push_R10_to_R4
SUB.W #14, SP ; SP = SP - 14, prepare the room for R10~R4
MOV.W 14(SP), 0(SP) ; PC_i → 0(SP), move the PC_i to the stack unit just after R10~R4
ADD.W #16, SP ; SP = SP + 16, go back to the stack unit where PC_i is place orginally,because the "PUSH" directive would do "SP-2 → SP" first
PUSH R10 ; push R10~R4
PUSH R9
PUSH R8
PUSH R7
PUSH R6
PUSH R5
PUSH R4
SUB.W #2, SP ; SP = SP - 2, because SP point to R4 now and the RET means doing "@SP → PC"
RET ; @SP → PC, SP + 2 → SP, means "PC_i → PC" and return to the ISR
Pop_R4_to_R10
MOV.W 14(SP), R10 ; R10(on stack) → R10(register), prepare the room for PC_i
MOV.W 0(SP), 14(SP) ; PC_i → 14(SP), means moving the return address to the stack unit where R10 is placed originally
ADD.W #2, SP ; SP = SP + 2, skip the stack unit where PC_i is placed originally
POP R4 ; pop R4~R9
POP R5
POP R6
POP R7
POP R8
POP R9
RET ; @SP → PC, SP + 2 → SP, means "PC_i → PC" and return to the ISR