实时操作系统(Real Time Operating System,简称RTOS)是指当外界事件或数据产生时,能够接受并以足够快的速度予以处理,其处理的结果又能在规定的时间之内来控制生产过程或对处理系统做出快速响应,调度一切可利用的资源完成实时任务,并控制所有实时任务协调一致运行的操作系统。提供及时响应和高可靠性是其主要特点。 (来源:百度百科)
实时操作系统要实现的最基本的功能便是任务的切换,与裸机程序不同,在实时操作系统中,每个任务都会拥有其专属的任务堆栈用于任务变量的存储,个人认为操作系统中最难以理解之处就是涉及到汇编部分的CPU现场保存和新任务的切换。
我们这次将在STM32F4(cortex-M4 内核)上简单的实现使用汇编完成任务的切换,首先简单的写一下一下C语言部分的代码,我们需要写两个任务函数(Task1、Task2),这两个函数会在运行中不断的进行切换。
void Task1(){
while(1){
printf("1");
}
}
void Task2(){
while(1){
printf("2");
}
}
除了任务的执行代码外,我们还需要为两个任务初始化堆栈,我们简单的为两个任务风别分配200个32为的空间。 STM32的堆栈地址是从高地址向低地址生长的,即入栈地址减一,因此我们的堆栈地址从我们分配的数组的最后一个元素地址开始即 &TASK1_Stack[199] ,接着我们假设该任务已经被切换出,且此时CPU的所有寄存器都已经保存完毕,我们给他们赋予初值,除了 PS 和 LR 以及PSR 之外 我们可以随意赋值,PS为该任务的入口即Task1 函数的地址,PC该任务结束时返回的地址,我们让任务结束时运行EDN_Handle函数。
static unsigned int TASK1_Stack[200];
static unsigned int TASK2_Stack[200];
void task_init(){
/// 初始化 任务的堆栈
/// STM32 的堆栈是从 高地址 --》 低地址
Task1_stack_p = &TASK1_Stack[200-1];
Task2_stack_p = &TASK2_Stack[200-1];
/// 此时 两个任务的堆栈指针都存在 了 Task_X_stack_p 中
*(--Task1_stack_p) = 0x01000000uL; // 程序状态寄存器 PSR
*(--Task1_stack_p) = (unsigned int)Task1; // 程序开始地址 PC
*(--Task1_stack_p) = (unsigned int)END_Handle;
*(--Task1_stack_p) = 0x00001234uL; // R12
*(--Task1_stack_p) = 0x7788521AuL; // R3
*(--Task1_stack_p) = 0x7788521BuL; // R2
*(--Task1_stack_p) = 0x7788521CuL; // R1
*(--Task1_stack_p) = 0x00000001uL; // R0
*(--Task1_stack_p) = 0x7788521AuL; // R11
*(--Task1_stack_p) = 0x7788521BuL; // R10
*(--Task1_stack_p) = 0x7788521CuL; // R9
*(--Task1_stack_p) = 0x7788521DuL; // R8
*(--Task1_stack_p) = 0x7788521EuL; // R7
*(--Task1_stack_p) = 0x7788521FuL; // R6
*(--Task1_stack_p) = 0x7788521FuL; // R5
*(--Task1_stack_p) = 0x7788521FuL; // R4
实时操作系统要想实现任务的调度,就必须有时间的度量,在一个任务运行一段时间之后切换到下一个该执行的任务,以保证系统的实时性,因此我们必须让系统在指定的时间段进行任务的调度,在STM32中我们可以使用StsTick计数器的中断实现任务的切换,但是在STM32 中硬件为我们提供了另外一个中断(PendSV)顾名思义该中断是可以挂起的,在STM32中一般都会使用PendSv中断去执行任务的中断,因为其可挂起的特性,可以使程序在运行时先执行其他外设所产生的中断,在这些中断执行完毕后再去进行任务的调度,以此保证其他中断能够及时的得到相应,因此我们要在Systick的中断中去开启PendSV中断。以下代码中我们在Systick中断中找到接下来要运行的任务,使用 PENDSV( )函数开启PendSV中断。
/// os tick
void SysTick_Handler(void)
{
static unsigned char id = 1;
// 找到应该运行的任务 将任务的起始地址 任务的堆栈地址 放入
if(id == 1 ){
next_stack_p = &Task1_stack_p;
id = 2;
}else{
next_stack_p = &Task2_stack_p;
id = 1;
}
// 运行汇编的任务切换
PENDSV();
test = 6;
}
PENDSV()函数调用了汇编,开启了Pendsv中断,进入到PendSv_Handler中断回调函数。
PENDSV
;触发PendSv异常
LDR R0, =NVIC_INT_CTRL
LDR R1, =NVIC_PENDSVSET
STR R1, [R0]
BX LR
CPSIE I
在Pendsv_Handler中,在进入中断后我们CPU内核会将 R0-R3 这四个通用寄存器入栈,保存当前任务的CPU现场,我们还需要将R4-R11入栈到该任务的栈中,完成之后,我们再把新任务(R4-R11)出栈赋值到R3-R11的寄存器中,将该堆栈指针赋值到PSP(堆栈指针中),接着让CPU从MSP 堆栈指针切换到PSP堆栈,至此任务的切换就基本完成。
PendSV_Handler
CPSID I ; 关闭总中断
MRS R0, PSP ; 将状态寄存器的内容传送至通用寄存器。 判断 PSP 是否是 0
CBZ R0, OS_CPU_PendSVHandler_nosave ; 判断 R0 (PSP) 是否 是 0 如果是0 第一次任务不用进行现场保存
;直接跳转到 nosave
SUBS R0, R0, #0x20 ; 保存当前任务的CPU现场
STM R0, {R4-R11}
LDR R1, =now_stack_p ; 获得当前运行的任务保存的 堆栈地址 的 保存地址
LDR R1, [R1]
STR R0, [R1] ; R0 is SP of process being switched out
; 到此处 所有的cpu 现场 已经保存完成
OS_CPU_PendSVHandler_nosave
LDR R0, =now_stack_p ; 这个应该保存着 当前任务的 c 中的堆栈指针
LDR R1, =next_stack_p
; 获取了 堆栈指针 的 存储 地址
LDR R1,[R1]
STR R1,[R0]
; now_stack_p 应该存 堆栈指针的 的存储 地址 OK
LDR R1,[R1]
;LDR R2, [R1]
; 现在 R2 是 堆栈指针 了
; R0 is new process SP; SP = OSTCBHighRdy->OSTCBStkPtr;
LDM R1, {R4-R11} ; Restore r4-11 from new process stack
ADDS R1, R1, #0x20
;R0 存的地址是 现在是堆栈地址 存储空间
LDR R0, [R0]
STR R1, [R0]
; 好了 现在R0 存储的 是真正的堆栈 地址了
MSR PSP, R1 ; Load PSP with new process SP
ORR LR , LR, #0x04 ; Ensure exception return uses process stack
CPSIE I
BX LR ; Exception return will restore remaining context
运行程序我们可以串口中看到任务在运行中不同的切换。