任务的定义、任务切换的原理及实现

1 任务的定义

任务的外观:一个永远不会返回的函数。
在这里插入图片描述
任务的内在:栈、堆、数据区、代码区、内核寄存器相关信息。
在这里插入图片描述
我们可以按如下方式进行任务定义:
在这里插入图片描述


2 任务切换的原理

任务切换的本质:保存前一任务的当前运行状态,恢复后一任务之前的运行状态。

在这里插入图片描述
在这里插入图片描述
任务状态数据:

  • 代码、数据区:由编译器自动分配,各个任务相互独立,并不冲突。
  • 堆:不使用。
  • 栈:硬件只支持两个堆栈空间,不同任务能够共用?
  • 内核寄存器:编译器会在某些时间将值保存到栈中,如函数调用、异常处理。未保存的如何处理呢?
  • 其它的状态数据,如何处理呢?

解决方法:为每个任务配置独立的栈,用于保存该任务的所有状态数据。
在这里插入图片描述
总结一下任务切换:

任务切换的关键步骤:

  • 保存当前任务的状态(主要是寄存器信息)
  • 恢复下一个要运行的任务(主要是恢复寄存器信息)

在哪里切换任务?
对于Cortex-M3内核的单片来说,我们可以通过触发PendSVC异常(为了保证中断的实时响应,PendSVC的中断优先级应该设置为最低),然后在异常中进行任务切换。

切换任务是需要保存当前正在执行的任务的哪些寄存器信息?
对于一个任务来说,要想被中断后恢复到之前的运行状态,那么其所对应的寄存器信息和栈里的内容必须和之前一样。

对于寄存器:当发生异常时,处理器会自动将xpsr、r15(pc)、r14(lr)、r12、r3~r0压入栈中。不过我们需要知道的是,Cortex-M3提供了两个栈指针(PSP和MSP),进入异常时强制使用MSP,执行正常程序时可以通过在退出异常前设置LR的值决定是使用MSP还是PSP。发生异常前使用的PSP,处理器就会把相关的寄存器压入到PSP对应的栈中;发生异常前使用的MSP。处理器就会把相关的寄存器压入到MSP对应的栈中。CPU复位后会自动使用MSP,在第一次切换任务前使用的都是MSP。在一般的RTOS设计中,所有的任务都使用PSP,异常使用MSP。

对于栈:为了使得任务能够被正常恢复,我们需要给每个任务单独分配占空间(RAM中的一片区域,一般是全局数组即可)。当我们切换任务时,我们需要把CPU未保存的寄存器保存到该任务对应的栈中,然后保存当前任务对应的栈指针。恢复下一个任务时,则需要首先拿到该任务对应的栈指针,然后恢复我们之前手动保存的寄存器信息。然后将PSP的值更改为当前的栈指针。在CPU退出异常后,会自动将之前保存的寄存器信息进行恢复(Cortex-M3可以设置退出异常后是使用MSP还是PSP,CPU会根据这个值进行寄存器信息的恢复,对于任务切换来说,就是将PSP对应的栈中的信息恢复到相对应的寄存器中)。


3 任务切换的实现

3.1 设计目标

设计目标:

  • 构建一个最小的两个任务切换运行的最小系统。
    在这里插入图片描述

涉及要点:

  • 怎样跑起来第一个任务?
  • 怎样在两个任务间切换运行?

切换至初始任务,步骤如下:

  1. 设置初始任务的栈为当前栈。
    在这里插入图片描述
  2. 从栈中恢复状态。
    在这里插入图片描述
  3. 开始运行当前任务。

任务之间的切换过程(从任务1切换到任务2):

  1. 暂停运行任务1。
    在这里插入图片描述

  2. 需要保存的状态保存到当前栈(任务1的栈)。在这里插入图片描述

  3. 切换当前栈为任务2的栈。
    在这里插入图片描述

  4. 从当前栈(任务2的栈)中恢复状态。
    在这里插入图片描述

  5. 继续运行任务2的代码。

3.2 设计实现

任务的初始化:
在这里插入图片描述
任务的初始化:初始化任务为等待状态。
在这里插入图片描述在这里插入图片描述
切换至初始任务:
在这里插入图片描述
从前一任务切换至后一任务:
在这里插入图片描述
在PendSV中切换任务:
在这里插入图片描述

3.3 代码实现

项目组织结构如下:
在这里插入图片描述
tinyOS.h:

/*************************************** Copyright (c)******************************************************
** File name            :   tinyOS.h
** Latest modified Date :   2016-06-01
** Latest Version       :   0.1
** Descriptions         :   tinyOS的核心头文件。包含了所有关键数据类型的定义,还有核心的函数。
**
**--------------------------------------------------------------------------------------------------------
** Created by           :   01课堂 lishutong
** Created date         :   2016-06-01
** Version              :   1.0
** Descriptions         :   The original version
**
**--------------------------------------------------------------------------------------------------------
** Copyright            :   版权所有,禁止用于商业用途
** Author Blog          :   http://ilishutong.com
**********************************************************************************************************/
#ifndef TINYOS_H
#define TINYOS_H

// 标准头文件,里面包含了常用的类型定义,如uint32_t
#include <stdint.h>

// Cortex-M的堆栈单元类型:堆栈单元的大小为32位,所以使用uint32_t
typedef uint32_t tTaskStack;

// 任务结构:包含了一个任务的所有信息
typedef struct _tTask {
	// 任务所用堆栈的当前堆栈指针。每个任务都有他自己的堆栈,用于在运行过程中存储临时变量等一些环境参数
	// 在tinyOS运行该任务前,会从stack指向的位置处,会读取堆栈中的环境参数恢复到CPU寄存器中,然后开始运行
	// 在切换至其它任务时,会将当前CPU寄存器值保存到堆栈中,等待下一次运行该任务时再恢复。
	// stack保存了最后保存环境参数的地址位置,用于后续恢复
    tTaskStack * stack;
}tTask;

// 当前任务:记录当前是哪个任务正在运行
extern tTask * currentTask;

// 下一个将即运行的任务:在进行任务切换前,先设置好该值,然后任务切换过程中会从中读取下一任务信息
extern tTask * nextTask;

/**********************************************************************************************************
** Function name        :   tTaskRunFirst
** Descriptions         :   在启动tinyOS时,调用该函数,将切换至第一个任务运行
** parameters           :   无
** Returned value       :   无
***********************************************************************************************************/
void tTaskRunFirst (void); 

/**********************************************************************************************************
** Function name        :   tTaskSwitch
** Descriptions         :   进行一次任务切换,tinyOS会预先配置好currentTask和nextTask, 然后调用该函数,切换至
**                          nextTask运行
** parameters           :   无
** Returned value       :   无
***********************************************************************************************************/
void tTaskSwitch (void);

#endif /* TINYOS_H */

switch.c:

/*************************************** Copyright (c)******************************************************
** File name            :   switch.c
** Latest modified Date :   2016-06-01
** Latest Version       :   0.1
** Descriptions         :   tinyOS任务切换中与CPU相关的函数。
**
**--------------------------------------------------------------------------------------------------------
** Created by           :   01课堂 lishutong
** Created date         :   2016-06-01
** Version              :   1.0
** Descriptions         :   The original version
**
**--------------------------------------------------------------------------------------------------------
** Copyright            :   版权所有,禁止用于商业用途
** Author Blog          :   http://ilishutong.com
**********************************************************************************************************/
#include "tinyOS.h"
#include "ARMCM3.h"

// 在任务切换中,主要依赖了PendSV进行切换。PendSV其中的一个很重要的作用便是用于支持RTOS的任务切换。
// 实现方法为:
// 1、首先将PendSV的中断优先配置为最低。这样只有在其它所有中断完成后,才会触发该中断;
//    实现方法为:向NVIC_SYSPRI2写NVIC_PENDSV_PRI
// 2、在需要中断切换时,设置挂起位为1,手动触发。这样,当没有其它中断发生时,将会引发PendSV中断。
//    实现方法为:向NVIC_INT_CTRL写NVIC_PENDSVSET
// 3、在PendSV中,执行任务切换操作。
#define NVIC_INT_CTRL       0xE000ED04      // 中断控制及状态寄存器
#define NVIC_PENDSVSET      0x10000000      // 触发软件中断的值
#define NVIC_SYSPRI2        0xE000ED22      // 系统优先级寄存器
#define NVIC_PENDSV_PRI     0x000000FF      // 配置优先级

#define MEM32(addr)         *(volatile unsigned long *)(addr)
#define MEM8(addr)          *(volatile unsigned char *)(addr)

// 下面的代码中,用到了C文件嵌入ARM汇编
// 基本语法为:__asm 返回值 函数名(参数声明) {....}, 更具体的用法见Keil编译器手册,此处不再详注。

/**********************************************************************************************************
** Function name        :   PendSV_Handler
** Descriptions         :   PendSV异常处理函数。很有些会奇怪,看不到这个函数有在哪里调用。实际上,只要保持函数头不变
**                          void PendSV_Handler (), 在PendSV发生时,该函数会被自动调用
** parameters           :   无
** Returned value       :   无
***********************************************************************************************************/
__asm void PendSV_Handler ()
{   
    IMPORT  currentTask               // 使用import导入C文件中声明的全局变量
    IMPORT  nextTask                  // 类似于在C文文件中使用extern int variable
    

    // MRS Rx, PSP --- 将PSP堆栈寄存器的值传送给Rx寄存器
    MRS     R0, PSP                   // 获取当前任务的堆栈指针

    // CBZ Rx, label  -- 判断Rx的值是否为0,如果为0则跳转到指定标号处运行。否则继续往下运行
    CBZ     R0, PendSVHandler_nosave  // if 这是由tTaskSwitch触发的(此时,PSP肯定不会是0了,0的话必定是tTaskRunFirst)触发
                                      // 不清楚的话,可以先看tTaskRunFirst和tTaskSwitch的实现

   	// STMDB Rx!, {Rm-Rn} -- 将Rm-Rn之间的一堆寄存器写到Rx中地址对应的内存处。每写一个单元前,地址先自减4再写,先写Rm,最后写Rn。写完后将最后的地址保存到Rx寄存器中。
    STMDB   R0!, {R4-R11}             //     那么,我们需要将除异常自动保存的寄存器这外的其它寄存器自动保存起来{R4, R11}
                                      //     保存的地址是当前任务的PSP堆栈中,这样就完整的保存了必要的CPU寄存器,便于下次恢复

   	// 取currentTask这个变量符号的地址写到R1!注意,不是取currentTask的值
    LDR     R1, =currentTask          //     保存好后,将最后的堆栈顶位置,保存到currentTask->stack处    

   	// LDR R1, [R1] -- 从R1中的地址处,取32位,再写到R1。也就是从currentTask的地址处,取32位值。由于currenTask是指针,这个操作也就是取currentTask的值到R1。由于currentTask指向了某个tTask结构,也就是说此时R1的值是某个tTask结构变量的起始地址。而由于stack位于tTask的起始处,所以tTask.stack的地址与tTask相同。此时R1就是currentTask中stack的地址
    LDR     R1, [R1]                  //     由于stack处在结构体stack处的开始位置处,显然currentTask和stack在内存中的起始

   	// STR R0, [R1] -- 将R0的值写到R1中地址处。也就是将STMDB最后的地址,写到currentTask->stack处
    STR     R0, [R1]                  //     地址是一样的,这么做不会有任何问题

PendSVHandler_nosave                  // 无论是tTaskSwitch和tTaskSwitch触发的,最后都要从下一个要运行的任务的堆栈中恢复
                                      // CPU寄存器,然后切换至该任务中运行

    // 取currentTask的地址到R0
    LDR     R0, =currentTask          // 好了,准备切换了
    // 取nextTask的地址到R1   

LDR     R1, =nextTask        

	// 从nextTask的地址处取32位值,也就是R2 <= nextTask的值。
    LDR     R2, [R1]  

	// 向currentTask的地址处写nextTask的值,也就是实现currentTask <= nextTask
    STR     R2, [R0]                  // 先将currentTask设置为nextTask,也就是下一任务变成了当前任务
 

   	// 从currentTask指向的结构起始地址中取32位,由于stack成员位于结构体开始处,也就是R0 <= currentTask.stack
    LDR     R0, [R2]                  // 然后,从currentTask中加载stack,这样好知道从哪个位置取出CPU寄存器恢复运行

 	//  前面取了堆栈地址currentTask.stack。下面就是从该地址(R0中的值)连续取若干个32位单元,恢复到R4~R11。这个顺序和前面的STMDB恰好相反。
    LDMIA   R0!, {R4-R11}             // 恢复{R4, R11}。为什么只恢复了这么点,因为其余在退出PendSV时,硬件自动恢复

	// 恢复R4~R11后,我们需要切换到这个堆栈。所以将最后的R0地址,写到PSP堆栈寄存器中    

	MSR     PSP, R0                   // 最后,恢复真正的堆栈指针到PSP  

 	// 下面的代码,如果不懂,请忽略。只要知道是切换到PSP堆栈中。
    ORR     LR, LR, #0x04             // 标记下返回标记,指明在退出LR时,切换到PSP堆栈中(PendSV使用的是MSP) 
    BX      LR                        // 最后返回,此时任务就会从堆栈中取出LR值,恢复到上次运行的位置
}  

/**********************************************************************************************************
** Function name        :   tTaskRunFirst
** Descriptions         :   在启动tinyOS时,调用该函数,将切换至第一个任务运行
** parameters           :   无
** Returned value       :   无
***********************************************************************************************************/
void tTaskRunFirst()
{
    // 这里设置了一个标记,PSP = 0, 用于与tTaskSwitch()区分,用于在PEND_SV
    // 中判断当前切换是tinyOS启动时切换至第1个任务,还是多任务已经跑起来后执行的切换
    __set_PSP(0);

    MEM8(NVIC_SYSPRI2) = NVIC_PENDSV_PRI;   // 向NVIC_SYSPRI2写NVIC_PENDSV_PRI,设置其为最低优先级
    
    MEM32(NVIC_INT_CTRL) = NVIC_PENDSVSET;    // 向NVIC_INT_CTRL写NVIC_PENDSVSET,用于PendSV

    // 可以看到,这个函数是没有返回
    // 这是因为,一旦触发PendSV后,将会在PendSV后立即进行任务切换,切换至第1个任务运行
    // 此后,tinyOS将负责管理所有任务的运行,永远不会返回到该函数运行
}
    
/**********************************************************************************************************
** Function name        :   tTaskSwitch
** Descriptions         :   进行一次任务切换,tinyOS会预先配置好currentTask和nextTask, 然后调用该函数,切换至
**                          nextTask运行
** parameters           :   无
** Returned value       :   无
***********************************************************************************************************/
void tTaskSwitch() 
{
    // 和tTaskRunFirst, 这个函数会在某个任务中调用,然后触发PendSV切换至其它任务
    // 之后的某个时候,将会再次切换到该任务运行,此时,开始运行该行代码, 返回到
    // tTaskSwitch调用处继续往下运行
    MEM32(NVIC_INT_CTRL) = NVIC_PENDSVSET;  // 向NVIC_INT_CTRL写NVIC_PENDSVSET,用于PendSV
}

main.cpp:

/*************************************** Copyright (c)******************************************************
** File name            :   main.c
** Latest modified Date :   2016-06-01
** Latest Version       :   0.1
** Descriptions         :   主文件,包含应用代码
**
**--------------------------------------------------------------------------------------------------------
** Created by           :   01课堂 lishutong
** Created date         :   2016-06-01
** Version              :   1.0
** Descriptions         :   The original version
**
**--------------------------------------------------------------------------------------------------------
** Copyright            :   版权所有,禁止用于商业用途
** Author Blog          :   http://ilishutong.com
**********************************************************************************************************/
#include "tinyOS.h"

// 当前任务:记录当前是哪个任务正在运行
tTask * currentTask;

// 下一个将即运行的任务:在进行任务切换前,先设置好该值,然后任务切换过程中会从中读取下一任务信息
tTask * nextTask;

// 所有任务的指针数组:简单起见,只使用两个任务
tTask * taskTable[2];

/**********************************************************************************************************
** Function name        :   tTaskInit
** Descriptions         :   初始化任务结构
** parameters           :   task        要初始化的任务结构
** parameters           :   entry       任务的入口函数
** parameters           :   param       传递给任务的运行参数
** Returned value       :   无
***********************************************************************************************************/
void tTaskInit (tTask * task, void (*entry)(void *), void *param, uint32_t * stack)
{
    // 为了简化代码,tinyOS无论是在启动时切换至第一个任务,还是在运行过程中在不同间任务切换
    // 所执行的操作都是先保存当前任务的运行环境参数(CPU寄存器值)的堆栈中(如果已经运行运行起来的话),然后再
    // 取出从下一个任务的堆栈中取出之前的运行环境参数,然后恢复到CPU寄存器
    // 对于切换至之前从没有运行过的任务,我们为它配置一个“虚假的”保存现场,然后使用该现场恢复。

    // 注意以下两点:
    // 1、不需要用到的寄存器,直接填了寄存器号,方便在IDE调试时查看效果;
    // 2、顺序不能变,要结合PendSV_Handler以及CPU对异常的处理流程来理解
    *(--stack) = (unsigned long)(1<<24);                // XPSR, 设置了Thumb模式,恢复到Thumb状态而非ARM状态运行
    *(--stack) = (unsigned long)entry;                  // 程序的入口地址
    *(--stack) = (unsigned long)0x14;                   // R14(LR), 任务不会通过return xxx结束自己,所以未用
    *(--stack) = (unsigned long)0x12;                   // R12, 未用
    *(--stack) = (unsigned long)0x3;                    // R3, 未用
    *(--stack) = (unsigned long)0x2;                    // R2, 未用
    *(--stack) = (unsigned long)0x1;                    // R1, 未用
    *(--stack) = (unsigned long)param;                  // R0 = param, 传给任务的入口函数
    *(--stack) = (unsigned long)0x11;                   // R11, 未用
    *(--stack) = (unsigned long)0x10;                   // R10, 未用
    *(--stack) = (unsigned long)0x9;                    // R9, 未用
    *(--stack) = (unsigned long)0x8;                    // R8, 未用
    *(--stack) = (unsigned long)0x7;                    // R7, 未用
    *(--stack) = (unsigned long)0x6;                    // R6, 未用
    *(--stack) = (unsigned long)0x5;                    // R5, 未用
    *(--stack) = (unsigned long)0x4;                    // R4, 未用

    task->stack = stack;                                // 保存最终的值
}

/**********************************************************************************************************
** Function name        :   tTaskSched
** Descriptions         :   任务调度接口。tinyOS通过它来选择下一个具体的任务,然后切换至该任务运行。
** parameters           :   无
** Returned value       :   无
***********************************************************************************************************/
void tTaskSched () 
{    
    // 这里的算法很简单。
    // 一共有两个任务。选择另一个任务,然后切换过去
    if (currentTask == taskTable[0]) 
    {
        nextTask = taskTable[1];
    }
    else 
    {
        nextTask = taskTable[0];
    }
    
    tTaskSwitch();
}

/**********************************************************************************************************
** 应用示例
** 有两个任务,分别执行task1Entry和task2Entry。功能是分别对相应的变量进行周期性置0置1,每次完成从1->0的切换后,
** 自动切换至另一个任务中运行。这样便实现了两个任务交替性的使用一段时间CPU,对相应变量值修改。
**********************************************************************************************************/
void delay (int count) 
{
    while (--count > 0);
}

int task1Flag;
void task1Entry (void * param) 
{
    for (;;) 
    {
        task1Flag = 1;
        delay(100);
        task1Flag = 0;
        delay(100);
        tTaskSched();
    }
}

int task2Flag;
void task2Entry (void * param) 
{
    for (;;) 
    {
        task2Flag = 1;
        delay(100);
        task2Flag = 0;
        delay(100);
        tTaskSched();
    }
}

// 任务1和任务2的任务结构,以及用于堆栈空间
tTask tTask1;
tTask tTask2;
tTaskStack task1Env[1024];     
tTaskStack task2Env[1024];

int main () 
{
    // 初始化任务1和任务2结构,传递运行的起始地址,想要给任意参数,以及运行堆栈空间
    tTaskInit(&tTask1, task1Entry, (void *)0x11111111, &task1Env[1024]);
    tTaskInit(&tTask2, task2Entry, (void *)0x22222222, &task2Env[1024]);
    
    // 接着,将任务加入到任务表中
    taskTable[0] = &tTask1;
    taskTable[1] = &tTask2;
    
    // 我们期望先运行tTask1, 也就是void task1Entry (void * param) 
    nextTask = taskTable[0];

    // 切换到nextTask, 这个函数永远不会返回
    tTaskRunFirst();
    return 0;
}


3.4 PendSV_Handler的另一种实现

不再将PSP=0:

  • 芯片在上电启动后,默认使用MSP作为堆栈的指针, 在这里我们直接将PSP = MSP。
void tTaskRunFirst () {
    // 这里设置了一个标记,PSP = MSP, 用于与tTaskSwitch()区分,用于在PEND_SV
    // 中判断当前切换是tinyOS启动时切换至第1个任务,还是多任务已经跑起来后执行的切换
    __set_PSP(__get_MSP());

    MEM8(NVIC_SYSPRI2) = NVIC_PENDSV_PRI;   // 向NVIC_SYSPRI2写NVIC_PENDSV_PRI,设置其为最低优先级

    MEM32(NVIC_INT_CTRL) = NVIC_PENDSVSET;    // 向NVIC_INT_CTRL写NVIC_PENDSVSET,用于PendSV

    // 可以看到,这个函数是没有返回
    // 这是因为,一旦触发PendSV后,将会在PendSV后立即进行任务切换,切换至第1个任务运行
    // 此后,tinyOS将负责管理所有任务的运行,永远不会返回到该函数运行
}

不再判断PSP=0?
上面提到过,在芯片启动时,默认使用MSP堆栈。然后在tTaskRunFirst()中,我们设置了PSP=MSP。

第一次进入PendSV_Handler()时,已经设置了PSP=MSP,所以硬件自动将R0-R3等压入MSP堆栈。同时STMDB R0!, {R4-R11}会将R4-R11压入到MSP。系统跑起来之后,进入PendSV_Handler()前一直用的是PSP堆栈,所以硬件自动保存及STMDB保存的也是在PSP堆栈中。这样一来就不用再判断PSP = 0 ?的问题。

__asm void PendSV_Handler (void) { 
    IMPORT saveAndLoadStackAddr
    
    // 切换第一个任务时,由于设置了PSP=MSP,所以下面的STMDB保存会将R4~R11
    // 保存到系统启动时默认的堆栈中,而不是某个任务
    MRS     R0, PSP                 
    STMDB   R0!, {R4-R11}               // 将R4~R11保存到当前任务栈,也就是PSP指向的堆栈
    BL      saveAndLoadStackAddr        // 调用函数:参数通过R0传递,返回值也通过R0传递 
    LDMIA   R0!, {R4-R11}               // 从下一任务的堆栈中,恢复R4~R11
    MSR     PSP, R0
    
    MOV     LR, #0xFFFFFFFD             // 指明返回异常时使用PSP。注意,这时LR不是程序返回地址
    BX      LR
}

下面的C代码,用于替换原来的汇编代码,更容易理解。

uint32_t saveAndLoadStackAddr (uint32_t stackAddr) {
    if (currentTask != (tTask *)0) {                    // 第一次切换时,当前任务为0
        currentTask->stack = (uint32_t *)stackAddr;     // 所以不会保存
    }
    currentTask = nextTask;                     
    return (uint32_t)currentTask->stack;                // 取下一任务堆栈地址
}

参考资料:

  1. 【李述铜】从0到1自己动手写嵌入式操作系统

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值