简易0S设计
多任务抢占式调度器
1
本文档以 ARM9(三星 2410/2440)为平台,介绍一个多任务抢占式调度器------抢占式
任务调度,提供延时,挂起,恢复任务操作。最精简化,没有加入信号量邮箱等同步通信机制。
只实现一个基本任务调度器的功能。
虽然不能称为操作系统,但已体现了小型嵌入式操作系统的精髓。OS代码不到 1.5K,
核心函数只有几个,思路简单明了。比起 UCOS,更适合用作多任务系统原理的学习入门。
对初学者来说,看 UCOS的源代码很容易迷糊。
回想初学嵌入式多任务系统时,什...
多任务抢占式调度器
1
本文档以 ARM9(三星 2410/2440)为平台,介绍一个多任务抢占式调度器------抢占式
任务调度,提供延时,挂起,恢复任务操作。最精简化,没有加入信号量邮箱等同步通信机制。
只实现一个基本任务调度器的功能。
虽然不能称为操作系统,但已体现了小型嵌入式操作系统的精髓。OS代码不到 1.5K,
核心函数只有几个,思路简单明了。比起 UCOS,更适合用作多任务系统原理的学习入门。
对初学者来说,看 UCOS的源代码很容易迷糊。
回想初学嵌入式多任务系统时,什么都不懂,Jean J.Labrosse 的经典之作《嵌入式实
时操作系统 uc/osII》看得我一头雾水。事实上,使我对多任务的原理印象最深的是网上的
一篇文章----《建立一个属于自己的 AVR的 RTOS》。
学习就应该这样,循序渐进。把一步步把简单的东西弄懂了,便没有复杂的了,所谓
水到渠成。
这篇文章是面对初学者的,把很多问题简化了。希望对刚接触嵌入式多任务系统的兄
弟有所帮助。
必定存在不少 bug,欢迎指正。
多任务抢占式调度器
2
简易多任务 OS设计
-------ARM9上运行的简单多任务调度器
By lisuwei
http://group.ednchina.com/999/20520.aspx
这里提供一个简易的多任务抢占任务调度器供大家学习。虽然太简单不实用,但对理
解多任务抢占式调度的原理是很有益处的。
先直观地看一下多任务系统中的代码与单任务程序(前后台系统)的区别。
Main.c
int Main(void)
{
TargetInit(); //初始化目标板
OSInit(); //初始化操作系统
OSTaskCreate(Task0,&StackTask0[StackSizeTask0 - 1],PrioTask0); // 创建一个任务
Uart_Printf("Ready to start OS\n");
OSStart(); //运行操作系统
return 0; //程序不会运行至此
}
void Task0(void)
{
TargetStart(); //设置中断向量,启动操作系统的硬件定时器中断
Uart_Printf("Start OS\n");
// 创建其他任务
OSTaskCreate(Task1,&StackTask1[StackSizeTask1 - 1],PrioTask1);
OSTaskCreate(Task2,&StackTask2[StackSizeTask2 - 1],PrioTask2);
OSTaskCreate(Task3,&StackTask3[StackSizeTask2 - 1],PrioTask3);
while(1)
{
Uart_Printf("Task0\n");
OSTimeDly(100); //1秒运行一次
}
}
void Task1(void)
{
while(1)
{
Uart_Printf("Task1\n");
OSTimeDly(300); //3秒运行一次
}
}
多任务抢占式调度器
3
void Task2(void)
{
while(1)
{
Uart_Printf("Task2\n");
OSTaskSuspend(PrioTask2); // 使自己进入挂起状态
}
}
void Task3(void)
{
while(1)
{
Uart_Printf("Resume Task2\n");
OSTaskResume(PrioTask2); // 恢复任务 2
OSTimeDly(800);
}
}
程序中创建了四个任务,任务 0每 1秒运行一次,任务 1每 3秒运行一次,任务 2运
行一次即把自己挂起,任务 3每 8秒运行一次并把任务 2恢复。
在终端的运行结果如下图:
多任务抢占式调度器
4
什么是多任务系统?
就像我们用电脑时可以同时听歌,上网,编辑文档等。在多任务系统中,可以同时
执行多个并行任务,各个任务之间互相独立。通过操作系统执行任务调度而实现宏观上
的“并发运行”。从宏观上不同的任务并发运行,好像每个任务都有自己的 CPU一样。
其实在单一 CPU的情况下,是不存在真正的多任务机制的,存在的只有不同的任务
轮流使用 CPU,所以本质上还是单任务的。但由于 CPU执行速度非常快,加上任务切换
十分频繁并且切换的很快,所以我们感觉好像有很多任务同时在运行一样。这就是所谓
的多任务机制。
多任务的最大好处是充分利用硬件资源,如在单任务时(大循环结构,如大部分 51
程序)遇到 delay函数时,CPU在空转;而在多任务系统,遇到 delay或需等待资源时系
统会自动运行下一个任务,等条件满足再回来运行先前的任务,这样就充分利用了 CPU,
提高了效率。
任务有下面的特性:
l 动态性。任务并不是随时都可以运行的,而一个已经运行的任务并不能保证
一直占有 CPU直到运行完。一般有就绪态,运行态,挂起态等。
运行态。一个运行态的任务是一个正在使用 CPU的任务。任何时刻有且只有
一个运行着的任务。
就绪态。一个就绪态任务是可运行的,等待占有 CPU的任务释放 CPU。
挂起态。某些条件不满足而挂起不能运行的状态。
任务三种状态转换图
下面我们来分析代码是如何实现状态转换的。
RTOS.h
INT32U OSRdyTbl; /* 就绪任务表 */
上面定义一个 32 位变量,每一位代表一个任务,0 表示挂起状态,1 表示就绪状
态。它
了各任务的就绪与否状态,称它为就绪表。OSRdyTbl定义为 32位变
量,对应 32个任务。当然,定义为 64位的话,便最多能支持 64个任务。
多任务抢占式调度器
5
这样,可以定义两个宏,实现把任务的状态变为就绪或挂起态。
RTOS.h
/* 在就绪表中登记就绪任务 */
#define OSSetPrioRdy(prio) \
{ \
OSRdyTbl |= 0x01<
0 ) /* 当延时有效 */
{
OS_ENTER_CRITICAL();
OSDelPrioRdy(OSPrioCur); /* 把任务从就绪表中删去 */
TCB[OSPrioCur].OSTCBDly = ticks; /* 设置任务延时节拍数 */
OS_EXIT_CRITICAL();
OSSched(); /* 重新调度 */
}
}
多任务抢占式调度器
8
在 T0的中断服务函数中,依次对各个延时任务的延时节拍数减 1。若发现某个任务
的延时节拍数变为 0,则把它从挂起态置为就绪态。
RTOS.c
void TickInterrupt(void)
{
static INT8U i;
OSTime ++;
//Uart_SendByte('I');
for(i = 0; i < OS_TASKS; i++) /* 刷新各任务时钟 */
{
if(TCB[i].OSTCBDly )
{
TCB[i].OSTCBDly --;
if(TCB[i].OSTCBDly == 0) /* 当任务时钟到时,必须是由定时
器减时的才行*/
{
OSSetPrioRdy(i); /* 使任务可以重新运行 */
}
}
}
rSRCPND |= BIT_TIMER0;
rINTPND |= BIT_TIMER0;
}
系统自身创建了一个空闲任务,并设它为最低优先级,当系统没有任何任务就绪时,
则运行这个任务,让 CPU“有事可干”。用户程序可以在这个任务中加入一些“无关紧要”
的功能,如统计 CPU使用率等。
RTOS.c
void IdleTask(void)
{
IdleCount = 0;
while(1)
{
IdleCount++;
//Uart_Printf("IdleCount %d\n",IdleCount);
}
}
TIPS:
不要在空闲任务中运行有可能使任务挂起的函数。空闲任务应该一直处于就绪状态。
多任务抢占式调度器
9
如何实现多任务?
只有一个 CPU,如何在同一时间实现多个独立程序的运行?要实现多任务,条件是
每个任务互相独立。人如何才能独立,有自己的私有财产。任务也一样,如果一个任务
有自己的 CPU,堆栈,程序代码,数据存储区,那这个任务就是一个独立的任务。
下面我们来看看是任务是如何“独立”的。
首先是程序代码,每个任务的程序代码与函数一样,与 51的裸奔程序一样,每个任
务都是一个大循环。
void task ( )
{
//initialize
while(1)
{
//your code
}
}
TIPS:
如果一个任务正在运行某个公共函数时(如 Printf),被另一个高优先级的任务抢占,
那么当这个高优先级的任务也调用同一个公共函数时,极有可能破坏原任务的数据。
因为两个任务可能共用一套数据。为了防止这种情况发生,常采用两种措施:可重入设
计和互斥调用。
可重入函数中所有的变量均为局部变量,局部变量在调用时临时分配空间,所以不
同的任务在不同的时刻调用该函数时,它们的同一个局部变量所分配的存储空间并不
相同(任务私有栈中),互不干扰。另外,如果可重入函数调用了其他函数,则这些被
调用的函数也必须是可重入函数。
互斥调用稍后说明。
然后是数据存储区,由于全局变量是系统共用的,各个任务共享,不是任务私有,
所以这里的数据存储区是指任务的私有变量,如何变成私有?局部变量也。编译器是把局
部变量保存在栈里的,所以好办,只要任务有个私有的栈就行。
多任务抢占式调度器
10
TIPS:
临界资源是一次仅允许一个任务使用的共享资源。每个任务中访问临界资源的那段程
序称为临界区。
在多任务系统中,为保障数据的可靠性和完整性,共享资源要互斥(独占)访问,所
以全局变量(只读的除外)不能同时有多个任务访问,即一个任务访问的时候不能被其他
任务打断。共享资源是一种临界资源。
实现互斥(独占)访问的方法有关中断,关调度,互斥信号量,计数信号量等。
定义了如下处理临界资源访问的代码, 主要用于在进入临界区之前关闭中断,在退出
临界区后恢复原来的中断状态。
RTOS.h
INT32U cpu_sr; /* 进入临界代码区时保存 CPU状态 */
/* 进入临界资源代码区 */
#define OS_ENTER_CRITICAL() (cpu_sr = OSCPUSaveSR()) /* 关总中断 */
/* 退出临界代码区 */
#define OS_EXIT_CRITICAL() (OSCPURestoreSR(cpu_sr)) /* 恢复原来中断状态*/
RTOS_ASM.S
OSCPUSaveSR
mrs r0,CPSR ; 保存当前中断状态,参阅汇编子程序调用 ATPCS规则,
; r0保存传递的参数
orr r1,r0,#NOINT ; 关闭所有中断
msr CPSR_c,r1
mov pc,lr
OSCPURestoreSR
msr CPSR_c,r0 ; 恢复原来的中断状态
mov pc,lr
为了不影响程序当前的中断状态,先当前的 CPSR保存起来,退出临界区后再恢复。
注意 OS_ENTER_CRITICAL()和 OS_EXIT_CRITICAL()要成对使用。
示例:
OS_ENTER_CRITICAL();
Printf(…);//临界代码
OS_EXIT_CRITICAL();
这样,在临界区内,只要任务不主动放弃 CPU使用权,别的任务就没有占用 CPU的
机会,相当与独占 CPU了。临界区的代码要尽量短,因为它会使系统响应性能降低。
多任务抢占式调度器
11
私有栈的作用是存放局部变量,函数的参数,它是一个线性的空间,所以可以申请一
个静态数组,把栈顶指针 SP指向栈的数组的首元素(递增栈)或最后一个元素(递减栈)。
即可打造一个人工的栈出来。
Main.c
///******************任务堆栈大小定义***********///
#define StackSizeTask0 512
///******************建立任务堆栈***************///
INT32U StackTask0[StackSizeTask0];
TIPS:
若在任务中使用 Printf函数,栈要设得大些,否则栈越界溢出,会有意想不到的问题。
每个任务还要有记录自己栈顶指针的变量,保存在任务控制块(TCB)中。
什么是任务控制块?系统中的每个任务具有一个任务控制块,任务控制块记录任务执
行的环境,这里的任务控制块比较简单,只包含了任务的堆栈指针和任务延时节拍数。
RTOS.h
struct TaskCtrBlock /* 任务控制块数据结构 */
{
INT32U OSTCBStkPtr; /* 保存任务的堆栈顶 */
INT32U OSTCBDly; /* 任务延时时钟 */
};
struct TaskCtrBlock TCB[OS_TASKS + 1]; /* 定义任务控制块 */
任务控制块是任务的身份证。它把任务的程序与数据联系起来,找到它就可以得到任
务的所有资源。
void task ( )
{
//initialize
while(1)
{
//your code
}
}
运行地址 PC
程序代码、私有堆栈、任务控制块是任务的三要件
OSTCBStkPtr
OSTCBDly
多任务抢占式调度器
12
这里把 OSTCBStkPtr 放在结构体的最前面是有原因的,目的是使得在汇编中访问这
个变量比较容易。因为结构体的地址就是它的首元素的地址,要在汇编中访问 OSTCBStkPtr
这个变量,只需取得结构体的地址即可。
在 C语言中不能直接实现如 TCB. TCB[OSPrioCur].OSTCBStkPtr = SP的功能,只
能用汇编语言完成。
定义如下指向结构体的指针变量:
RTOS.h
struct TaskCtrBlock *p_OSTCBCur ; /* 指向当前任务控制块的指针 */
在 C语言中,先让 p_OSTCBCur指向当前运行任务的 TCB。
p_OSTCBCur = &TCB[OSPrioCur]; /* 目的是在汇编中引用任务的 TCB地
址取得栈顶指针 */
这样,运行下面的汇编代码即可把当前任务的堆顶指针取出到 SP中。
RTOS_ASM.S
SaveSPToCurTcb ; 保存当前任务的堆顶指针到它的
ldr r4,=p_OSTCBCur ; 取出当前任务的 PCB地址
ldr r5,[r4]
str sp,[r5] ; 保存当前任务的堆顶指针到它的
; TCB(因为 TaskCtrBlock地址亦即
; OSTCBStkPtr的地址)
多任务抢占式调度器
13
最后来看看任务是如何“拥有”自己的 CPU的。只有一个 CPU,各个任务共享,轮
流使用。如何才能实现?我们先来看看中断的过程,当中断来临时,CPU 把当前程序的运
行地址,寄存器等现场数据保存起来(一般保存在栈里),然后跳到中断服务程序执行。待
执行完毕,再把先前保存的数据装回 CPU又回到原来的程序执行。这样就实现了两个不同
程序的交叉运行。
借鉴这种思想不就能实现多任务了吗!模仿中断的过程就可以实现任务切换运行。
任务切换时,把当前任务的现场数据保存在自己的任务栈里面,再把待运行的任务
的数据从自己的任务栈装载到 CPU中,改变 CPU的 PC,SP,寄存器等。可以说,任务的
切换是任务运行环境的切换。而任务的运行环境保存在任务栈中,也就是说,任务切换的
关键是把任务的私有堆栈指针赋予处理器的堆栈指针 SP。
两个任务的切换过程如下:
当前任务的运行环境保存 待运行的任务的数据从自己的任务栈装载到 CPU
程序调用下面函数进行任务切换:
RTOS.c
void OSSched (void)
{
OS_ENTER_CRITICAL();
OSGetHighRdy(); /* 找出就绪表中优先级最高的任务 */
if(OSPrioHighRdy != OSPrioCur) /* 如果不是当前运行的任务,进行任务调度 */
{
p_OSTCBCur = &TCB[OSPrioCur]; /* 目的是在汇编中引用任务的 TCB地
址取得栈顶指针 */
p_OSTCBHighRdy = &TCB[OSPrioHighRdy];
OSPrioCur = OSPrioHighRdy; /* 更新 OSPrioCur */
OS_TASK_SW(); /* 调度任务 */
}
OS_EXIT_CRITICAL();
}
多任务抢占式调度器
14
RTOS_ASM.S
OS_TASK_SW ; 任务级的任务切换
;
stmfd sp!,{lr} ; PC 入栈
stmfd sp!,{r0-r12,lr} ; r0-r12,lr入栈
PUAH_PSR
mrs r4,cpsr
stmfd sp!,{r4} ; cpsr入栈
SaveSPToCurTcb ; 保存当前任务的堆顶指针到它的 TCB.
; TCB[OSPrioCur].OSTCBStkPtr = SP;
ldr r4,=p_OSTCBCur ; 取出当前任务的 PCB地址
ldr r5,[r4]
str sp,[r5] ; 保存当前任务的堆顶指针到它的 TCB(因为
;TaskCtrBlock 地址亦即 OSTCBStkPtr 的地址)
GetHigTcbSP ; 取出更高优先级任务的堆顶指针到 SP ,
; SP = TCB[OSPrioCur].OSTCBStkPtr
ldr r6,=p_OSTCBHighRdy ; 取出更高优先级就绪任务的 PCB的地址
ldr r6,[r6]
ldr sp,[r6] ; 取出更高优先级任务的堆顶指针到 SP
b POP_ALL ; 根据设定的栈结构顺序出栈
任务切换流程图如下:
保存当前任务的运行环境
待运行的任务的栈顶指针装载到 CPU的 SP
恢复待运行任务的运行环境
多任务抢占式调度器
15
如何建立任务?
OSTaskCreate函数在创建任务时调用,如下:
OSTaskCreate(Task0,&StackTask0[StackSizeTask0 - 1],PrioTask0); // 创建一个任务
任务未运行或被剥夺运行权后,它的运行环境以一定的结构保存在私有栈中。这个
规定的顺序是十分重要的,以后任务切换时都要按这个顺序入栈出栈。
建立任务时栈结构的初始化如下,
RTOS.c
void OSTaskCreate(void (*Task)(void),INT32U *p_Stack,INT8U TaskID)
{
if(TaskID <= OS_TASKS)
{
*(p_Stack) = (INT32U)Task; /* pc */
*(--p_Stack) = (INT32U)13; /* lr */
*(--p_Stack) = (INT32U)12; /* r12*/
*(--p_Stack) = (INT32U)11; /* r11*/
*(--p_Stack) = (INT32U)10; /* r10*/
*(--p_Stack) = (INT32U)9; /* r9 */
*(--p_Stack) = (INT32U)8; /* r8 */
*(--p_Stack) = (INT32U)7; /* r7 */
*(--p_Stack) = (INT32U)6; /* r6 */
*(--p_Stack) = (INT32U)5; /* r5 */
*(--p_Stack) = (INT32U)4; /* r4 */
*(--p_Stack) = (INT32U)3; /* r3 */
*(--p_Stack) = (INT32U)2; /* r2 */
*(--p_Stack) = (INT32U)1; /* r1 */
*(--p_Stack) = (INT32U)0; /* r0 */
*(--p_Stack) = (INT32U)(SVCMODE|0x0); /* SREG 保存在任务栈中*/
TCB[TaskID].OSTCBStkPtr = (INT32U)p_Stack; /* 将人工堆栈的栈顶,保存到
堆栈的数组中*/
TCB[TaskID].OSTCBDly = 0; /* 初始化任务延时 */
OSSetPrioRdy(TaskID); /* 在任务就绪表中登记 */
}
else
{
Uart_Printf("TaskID Error\n");
while(1);
}
}
多任务抢占式调度器
16
上面函数的作用就是创建一个任务。它接收三个参数,分别是任务的入口地址,任务
堆栈的首地址和任务的优先级。调用本函数后,系统会根据用户给出的参数初始化任务栈,
并把栈顶指针保存到任务控制块中,在任务就绪表标记该任务为就绪状态。最后返回,这
样一个任务就创建成功了。
可见,初始化后的任务堆栈空间由高到低将依次保存着 PC,LR,R12…R0,CPSR。
当一个任务将要运行时,便通过取得它的堆栈指针(保存在任务控制块中)将这些寄存器
出栈装入 CPU相应的位置即可。
初始状态的堆栈要与任务切换后的堆栈保存结构相一致,因为任务被创建后并不是直
接就获得执行的,而是通过 OSStartHighRdy()或OSSched()函数进行调度分配,满足
执行条件后才能获得执行的。为了使这个调度简单一致,把栈结构统一。
TIPS:
为方便处理,所有任务运行在管理模式下。
系统通过运行 OSStartHighRdy来开始第一个任务。
该函数是在多任务系统启动后,负责从第一个任务的 TCB控制块中获得该任务的堆
栈指针 SP,通过 SP依次将 CPU 现场恢复。这时系统就将控制权交给该任务,直到该任
务被阻塞或者被其它任务抢占 CPU。该函数仅仅在多任务启动时被执行一次,用来启动第
一个运行的任务。
RTOS_ASM.S
OSStartHighRdy
ldr r4,=p_OSTCBHighRdy ; 取出最高优先级就绪任务的 PCB的地址
ldr r4,[r4] ; 取得任务的栈顶指针(因为 TaskCtrBlock地址
; 亦即 OSTCBStkPtr的地址)
ldr sp,[r4] ; 任务的栈顶指针赋给 SP
b POP_ALL ; 根据设定的栈结构顺序出栈
RTOS_ASM.S
; 根据设定的栈结构顺序出栈
POP_ALL
ldmfd sp!,{r4} ; psr出栈
msr CPSR_cxsf,r4
ldmfd sp!,{r0-r12,lr,pc} ; r0-r12,lr,pc出栈
多任务抢占式调度器
17
如何实现抢占式调度?
基于任务优先级的抢占式调度,也就是最高优先级的任务一旦处于就绪状态,则立
即抢占正在运行的低优先级任务的处理器资源。
为了保证 CPU总是执行处于就绪条件下优先级最高的任务,每当任务状态改变后,
即判断当前运行的任务是否是就绪任务中优先级最高的,否则进行任务切换。
任务状态会在什么时候发生改变呢?有下面两种情况:
l 高优先级的任务因为需要某种资源或延时,主动请求挂起,让出处理器,此时将调度
就绪状态的低优先级任务获得执行,这种调度称为任务级的切换。
如任务执行 OSTimeDly()或 OSTaskSuspend()把自身挂起就属于这种。
l 高优先级的任务因为时钟节拍到来,或在中断处理结束后,内核发现更高优先级任务
获得了执行条件(如延时的时钟到时),则在中断后直接切换到更高优先级任务执行。这
种调度也称为中断级的切换。
第一种任务级的切换已经分析过了,这里来分析中断级的任务切换。
在目标板初始化函数 TargetInit中有
TargetInit.c
pISR_IRQ = (INT32U)ASM_IRQHandler;
把 ASM_IRQHandler的地址赋给中断向量服务程序入口,只要发生中断,系统便跳
到 ASM_IRQHandler函数处执行,它统管了系统的所有中断。
RTOS_ASM.S
ASM_IRQHandler ; 中断入口地址,在中断向量表初始化时被设置
sub lr,lr,#4 ; 计算中断返回地址
stmfd sp!,{r0-r3,r12,lr} ; 保护现场,此时处于中断模式下,sp指向中
; 断模式下的堆栈.不能进行任务切换(各任务
; 堆栈处在管理模式堆栈).
; R4-R11装的是局部变量,在进行函数跳转时,
; 编译器它会自动保护它们,
; 即 C语言函数返回时,寄存器 R4-R11、SP
; 不会改变,无需人为保护
bl OSIntEnter
bl C_IRQHandler ; 调用 c语言的中断处理程序
bl OSIntExit ; 判断中断后是否有更高优先级的任务进入就
; 绪而需要进行任务切换
ldr r0,=OSIntCtxSwFlag ;if(OSIntCtxSwFlag == 1) OSIntCtxSw()
ldr r1,[r0]
cmp r1,#1
beq OSIntCtxSw ; 有更高优先级的任务进入了就绪状态,则跳
; 转到 OSIntCtxSw进行中断级的任务切换
ldmfd sp!,{r0-r3,r12,pc}^ ; 不进行任务切换,出栈返回被中断的任务。
; 寄存器出栈同时将 spsr_irq的值复制到
; CPSR,实现模式切换
多任务抢占式调度器
18
TIPS:
在 ADS编译器中,“__irq”专门用来声明 IRQ中断服务程序,如果用“__irq”来声
明一个函数,那么表示该函数为一个 IRQ中断服务程序,汇编时编译器便会自动在该函
数体内加入中断现场保护和恢复的代码。
但在这里,中断由系统管理,中断服务函数不能使用“__irq”关键字进行声明。
在保存返回地址和现场数据后(注意是保存在中断模式下的栈中),随即跳到
OSIntEnter 去执行。OSIntEnter()是为了实现中断嵌套而调用的函数,OSIntNesting 记录
的是中断嵌套层数,每进入一次中断,便把 OSIntNesting 的值加 1,在退出中断时,若
OSIntNesting不为 0,则说明中断仍未完成,要待所有的中断完成后才能返回到任务中。
RTOS.c
void OSIntEnter (void)
{
if (OSIntNesting < 255)
{
OSIntNesting++;
}
}
随即进入 C语言中断入口程序 C_IRQHandler,它通过判断 INTOFFSET寄存器来
识别当前发生的中断,然后跳转到相应的中断服务函数。
RTOS.c
void C_IRQHandler(void)
{
(* ((void(*)()) (*((U32 *)((int)&pISR_EINT0 + (rINTOFFSET<<2)))))) ();
}
多任务抢占式调度器
19
执行完中断服务程序后,不会马上退出返回先前的任务,它将 OSIntNesting 的值减
1,当 OSIntNesting的值为 0时,表示所有的嵌套中断都完成了。可以返回原来的任务或
进行中断级任务切换。这时判断之前运行的任务是否仍然是最高优先级的就绪任务,如
果中断让更高优先级的任务就绪,则需要切换任务,通过置位 OSIntCtxSwFlag变量来标
记。
RTOS.c
void OSIntExit(void)
{
OS_ENTER_CRITICAL();
if (OSIntNesting > 0)
{
OSIntNesting --;
}
if (OSIntNesting == 0) /* 退出所有中断才能进行任务切换*/
{
OSGetHighRdy(); /* 找出就绪表中优先级最高的任务 */
if(OSPrioHighRdy != OSPrioCur)
{
p_OSTCBCur = &TCB[OSPrioCur]; /* 目的是在汇编中引用
任务的 TCB地址取得栈顶指针 */
p_OSTCBHighRdy = &TCB[OSPrioHighRdy];
OSPrioCur = OSPrioHighRdy; /* 更新 OSPrioCur */
OSIntCtxSwFlag = TRUE; /* 进行中断级任务调度 */
}
}
OS_EXIT_CRITICAL();
}
最后对OSIntCtxSwFlag标志进行判断,若OSIntCtxSwFlag置位则执行OSIntCtxSw
函数准备任务切换,否则退出中断返回先前的任务。
多任务抢占式调度器
20
OSIntCtxSw 函数是中断后需任务切换时的跳转程序,由它转到中断级任务切换程
序。为什么需要它?.
若中断后需任务切换,则中断结束后不返回先前的任务,而去执行中断级任务切换
函数 OS_TASK_SW_INT。这是正确的。但此时仍处于中断模式下,SP指向中断模式下
的堆栈.,不能进行任务切换(各任务堆栈处在管理模式堆栈)。所以要待退出中断后才进
行任务切换。如何保证退出中断后马上运行 OS_TASK_SW_INT?这得做点手脚。方法
是把中断返回地址换成 OS_TASK_SW_INT函数的地址。因为中断返回地址保存了在中
断模式的堆栈中(中断时先保存在中断模式的 lr 中,之后压栈),这里把它用一个变量
Int_Return_Addr_Save 保存下来,待后面任务切换时再把它取出压入先前任务的堆栈中
(管理模式下)。这样,中断返回时,出栈的 PC值就是 OS_TASK_SW_INT的地址。
RTOS_ASM.S
OSIntCtxSw ; 把中断的返回地址保存到
; Int_Return_Addr_Save变量中
ldr r0,=OSIntCtxSwFlag ; 清 OSIntCtxSwFlag标志位
mov r1,#0
str r1,[r0] ; OSIntCtxSwFlag = 0
add sp,sp,#20 ; 调整SP,使之指向之前入栈的 lr(中断模
; 式下的 lr保存的是中断返回地址),
ldr r0,[sp] ; lr -> r0
ldr r1,=Int_Return_Addr_Save ; &Int_Return_Addr_Save -> r1
str r0,[r1] ; lr -> Int_Return_Addr_Save
ldr r0,=OS_TASK_SW_INT
str r0,[sp] ; OS_TASK_SW_INT -> lr
sub sp,sp,#20 ; 调整 SP回到栈顶
ldmfd sp!,{r0-r3,r12,pc}^ ; 退出中断后跳到 OS_TASK_SW而不返回
OSIntCtxSw函数完成的功能
多任务抢占式调度器
21
这样,执行 ldmfd sp!,{r0-r3,r12,pc}^后,PC便指向 OS_TASK_SW_INT函数,执行
中断级任务切换。此时的栈为进入中断之前运行的任务的栈,要把这个任务的运行环境
保存到栈中。之后的代码便与任务级的切换的一样。
RTOS_ASM.S
OS_TASK_SW_INT ; 中断级任务切换,中断服务时使用另外
; 的堆栈,所以要回到管理模式中才进行任务切换
sub sp,sp,#4 ; 为 PC保留位置
stmfd sp!,{r0-r12,lr} ; r0-r12,lr入栈
ldr r0,=Int_Return_Addr_Save; 取出进入中断之前保存的 PC
ldr r0,[r0] ; Int_Return_Addr_Save -> r0
add sp,sp,#56 ; 调整 SP
stmfd sp,{r0} ; r0(PC)入栈
sub sp,sp,#56 ; 调整 SP回到栈顶
b PUAH_PSR
PUAH_PSR
mrs r4,cpsr
stmfd sp!,{r4} ; cpsr入栈
SaveSPToCurTcb .
; TCB[OSPrioCur].OSTCBStkPtr = SP;
ldr r4,=p_OSTCBCur ; 取出当前任务的 PCB地址
ldr r5,[r4]
str sp,[r5] ; 保存当前任务的堆顶指针到它的 TCB(因为
;TaskCtrBlock 地址亦即 OSTCBStkPtr 的地址)
GetHigTcbSP ; 取出更高优先级任务的堆顶指针到 SP ,
; SP = TCB[OSPrioCur].OSTCBStkPtr
ldr r6,=p_OSTCBHighRdy ; 取出更高优先级就绪任务的 PCB的地址
ldr r6,[r6]
ldr sp,[r6] ; 取出更高优先级任务的堆顶指针到 SP
b POP_ALL ; 根据设定的栈结构顺序出栈
把保存在 Int_Return_Addr_Save中的内容取出来保存在任务栈中
待运行的任务的栈顶指针装载到 CPU的 SP
恢复待运行任务的运行环境
把保存在 Int_Return_Addr_Save中的地址取出来和其它寄存器一起入栈
保存当前任务的堆顶指针到它的 TCB
多任务抢占
本文档为【简易0S设计】,请使用软件OFFICE或WPS软件打开。作品中的文字与图均可以修改和编辑,
图片更改请在作品中右键图片并更换,文字修改请直接点击文字进行修改,也可以新增和删除文档中的内容。
[版权声明] 本站所有资料为用户分享产生,若发现您的权利被侵害,请联系客服邮件isharekefu@iask.cn,我们尽快处理。
本作品所展示的图片、画像、字体、音乐的版权可能需版权方额外授权,请谨慎使用。
网站提供的党政主题相关内容(国旗、国徽、党徽..)目的在于配合国家政策宣传,仅限个人学习分享使用,禁止用于任何广告和商用目的。