1、基本任务切换

时间:2021-02-10 20:13:47

轮询模式:

int main () 
{
    for (;;) {
        flag = 0;
        delay(100);
        flag = 1;
        delay(100);
     }

return 0;
}

加入中断:

#define NVIC_INT_CTRL   0xE000ED04  // 中断控制及状态寄存器
#define NVIC_PENDSVSET  0x10000000  // 触发软件中断的值
#define NVIC_SYSPRI20xE000ED22  // 系统优先级寄存器
#define NVIC_PENDSV_PRI 0x000000FF  // 配置优先级

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


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

软件中断

CPU寄存器数据的保护(压栈)与恢复(出栈)

__asm void PendSV_Handler ()
{
IMPORT  blockPtr

// 加载寄存器存储地址
LDR R0, =blockPtr
LDR R0, [R0]
LDR R0, [R0]

// 保存寄存器
STMDB   R0!, {R4-R11}

// 将最后的地址写入到blockPtr中
LDR R1, =blockPtr
LDR R1, [R1]
STR R0, [R1]

// 修改部分寄存器,用于测试
ADD R4, R4, #1
ADD R5, R5, #1

// 恢复寄存器
LDMIA   R0!, {R4-R11}

// 异常返回
BX  LR
}  

简单的任务定义实现。包括任务函数、任务结构体、任务栈的定义。

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

typedef uint32_t tTaskStack;

任务结构:包含了一个任务的所有信息

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

任务栈

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

初始化任务结构体

/**********************************************************************************************************
** Function name:   tTaskInit
** Descriptions :   初始化任务结构
** parameters   :   task要初始化的任务结构
** parameters   :   entry   任务的入口函数
** parameters   :   param   传递给任务的运行参数
** Returned value   :   无
***********************************************************************************************************/
void tTaskInit (tTask * task, void (*entry)(void *), void *param, uint32_t * stack)
{
task->stack = stack;// 保存最终的值
}

基本任务切换的实现

先定义一些指针

当前任务:记录当前是哪个任务正在运行

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;// 保存最终的值
}

#

// 接着,将任务加入到任务表中
taskTable[0] = &tTask1;
taskTable[1] = &tTask2;

// 我们期望先运行tTask1, 也就是void task1Entry (void * param) 
nextTask = taskTable[0];

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

PendSV异常处理完善

任务切换过程中现场保护与现场恢复

__asm void PendSV_Handler ()
{   
IMPORT  currentTask               // 使用import导入C文件中声明的全局变量
IMPORT  nextTask                  // 类似于在C文文件中使用extern int variable

MRS     R0, PSP                   // 获取当前任务的堆栈指针
CBZ     R0, PendSVHandler_nosave  // if 这是由tTaskSwitch触发的(此时,PSP肯定不会是0了,0的话必定是tTaskRunFirst)触发
                                  // 不清楚的话,可以先看tTaskRunFirst和tTaskSwitch的实现
STMDB   R0!, {R4-R11}             //     那么,我们需要将除异常自动保存的寄存器这外的其它寄存器自动保存起来{R4, R11}
                                  //     保存的地址是当前任务的PSP堆栈中,这样就完整的保存了必要的CPU寄存器,便于下次恢复
LDR     R1, =currentTask          //     保存好后,将最后的堆栈顶位置,保存到currentTask->stack处    
LDR     R1, [R1]                  //     由于stack处在结构体stack处的开始位置处,显然currentTask和stack在内存中的起始
STR     R0, [R1]                  //     地址是一样的,这么做不会有任何问题

环境好了 看下两个任务是如何切换的

在各任务函数中调用任务调度函数来实现下一任务的选择及任务切换

任务1:

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

任务2:

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

#
任务切换:

void tTaskSched () 
{    
// 这里的算法很简单。
// 一共有两个任务。选择另一个任务,然后切换过去
if (currentTask == taskTable[0]) 
{
    nextTask = taskTable[1];
}
else 
{
    nextTask = taskTable[0];
}

tTaskSwitch();
}

void tTaskSwitch() 
{
// 和tTaskRunFirst, 这个函数会在某个任务中调用,然后触发PendSV切换至其它任务
// 之后的某个时候,将会再次切换到该任务运行,此时,开始运行该行代码, 返回到
// tTaskSwitch调用处继续往下运行
MEM32(NVIC_INT_CTRL) = NVIC_PENDSVSET;  // 向NVIC_INT_CTRL写NVIC_PENDSVSET,用于PendSV
}

PendSV异常处理的另外一种实现方法:

/**********************************************************************************************************
** Function name:   tTaskRunFirst
** Descriptions :   在启动tinyOS时,调用该函数,将切换至第一个任务运行
** parameters   :   无
** Returned value   :   无
***********************************************************************************************************/
void tTaskRunFirst () {
// 这里设置了一个标记,PSP = MSP, 二者都指向同一个堆栈
__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将负责管理所有任务的运行,永远不会返回到该函数运行
}

__asm void PendSV_Handler (void) { 
IMPORT saveAndLoadStackAddr

// 切换第一个任务时,由于设置了PSP=MSP,所以下面的STMDB保存会将R4~R11
// 保存到系统启动时默认的MSP堆栈中,而不是某个任务
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
}

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

双任务时间片运行原理。通过在SysTick定时器的周期性定时溢出中断服务函数中调用任务调度函数实现
void SysTick_Handler () 
{
// 什么都没做,除了进行任务切换
// 由于tTaskSched自动选择另一个任务切换过去,所以其效果就是
// 两个任务交替运行,与上一次例子不同的是,这是由系统时钟节拍推动的
// 如果说,上一个例子里需要每个任务主动去调用tTaskSched切换,那么这里就是不管任务愿不愿意,CPU
// 的运行权都会被交给另一个任务。这样对每个任务就很公平了,不存在某个任务拒不调用tTaskSched而一直占用CPU的情况
tTaskSched();
}
双任务延时原理与空闲任务。通过基于SysTick定时器(时基单元提供者)的软定时器实现延时
void SysTick_Handler () 
{
tTaskSystemTickHandler();
}

void tTaskSystemTickHandler () 
{
// 检查所有任务的delayTicks数,如果不0的话,减1。
int i;
for (i = 0; i < 2; i++) 
{
if (taskTable[i]->delayTicks > 0)
{
taskTable[i]->delayTicks--;
}
}

// 这个过程中可能有任务延时完毕(delayTicks = 0),进行一次调度。
tTaskSched();
}

void tTaskSwitch() 
{
// 和tTaskRunFirst, 这个函数会在某个任务中调用,然后触发PendSV切换至其它任务
// 之后的某个时候,将会再次切换到该任务运行,此时,开始运行该行代码, 返回到
// tTaskSwitch调用处继续往下运行
MEM32(NVIC_INT_CTRL) = NVIC_PENDSVSET;  // 向NVIC_INT_CTRL写NVIC_PENDSVSET,用于PendSV
}

    __asm void PendSV_Handler ()

void tTaskDelay (uint32_t delay) {
// 配置好当前要延时的ticks数
currentTask->delayTicks = delay;

// 然后进行任务切换,切换至另一个任务,或者空闲任务
// delayTikcs会在时钟中断中自动减1.当减至0时,会切换回来继续运行。
tTaskSched();
}

/**********************************************************************************************************
** Function name:   tTaskSched
** Descriptions :   任务调度接口。tinyOS通过它来选择下一个具体的任务,然后切换至该任务运行。
** parameters   :   无
** Returned value   :   无
***********************************************************************************************************/
void tTaskSched () 
{       
// 空闲任务只有在所有其它任务都不是延时状态时才执行
// 所以,我们先检查下当前任务是否是空闲任务
if (currentTask == idleTask) 
{
    // 如果是的话,那么去执行task1或者task2中的任意一个
    // 当然,如果某个任务还在延时状态,那么就不应该切换到他。
    // 如果所有任务都在延时,那么就继续运行空闲任务,不进行任何切换了
    if (taskTable[0]->delayTicks == 0) 
    {
        nextTask = taskTable[0];
    }           
    else if (taskTable[1]->delayTicks == 0) 
    {
        nextTask = taskTable[1];
    } else 
    {
        return;
    }
} 
else 
{
    // 如果是task1或者task2的话,检查下另外一个任务
    // 如果另外的任务不在延时中,就切换到该任务
    // 否则,判断下当前任务是否应该进入延时状态,如果是的话,就切换到空闲任务。否则就不进行任何切换
    if (currentTask == taskTable[0]) 
    {
        if (taskTable[1]->delayTicks == 0) 
        {
            nextTask = taskTable[1];
        }
        else if (currentTask->delayTicks != 0) 
        {
            nextTask = idleTask;
        } 
        else 
        {
            return;
        }
    }
    else if (currentTask == taskTable[1]) 
    {
        if (taskTable[0]->delayTicks == 0) 
        {
            nextTask = taskTable[0];
        }
        else if (currentTask->delayTicks != 0) 
        {
            nextTask = idleTask;
        }
        else 
        {
            return;
        }
    }
}

tTaskSwitch();
}

// 用于空闲任务的任务结构和堆栈空间
tTask tTaskIdle;
tTaskStack idleTaskEnv[1024];

void idleTaskEntry (void * param) {
for (;;)
{
// 空闲任务什么都不做
}
}