实现rtos操作系统 【一】基本任务切换实现

一、实现 PendSV 中断

PendSV是什么

我们先引用《Cortex-M3权威指南》对PendSV的介绍:

PendSV(可悬起的系统调用),它是一种CPU系统级别的异常,它可以像普通外设中断一样被悬起,而不会像SVC服务那样,因为没有及时响应处理,而触发Fault。

也就是说 PendSV 是一个中断异常,那 PendSV 和其他的中断异常有什么区别呢? 

摘自 Cortex-M3 权威指南 127 页

如果我们仔细看上图会发现步骤 8 的时候,SysTick 会先回到之前抢占的 ISR 而不是,而不是立刻进入 PendSV 中(在 RTOS 中 SysTick 中都会调用 PendSV 中断)。

这是因为 PendSV 可以被悬起,触发 PendSV 后他会等到目前所有 ISR 中断结束再去中断。避免打断其他的中断,破坏 RTOS 的实时性。因为其他中断可能很紧急,不容被滞后。

所以PendSV的最大特点就是,它是系统级别的异常,但它又天生支持【缓期执行】。

我们将中断控制寄存器的 27 位置 1,以使能 PendSV

1.1 中断控制及状态寄存器 ICSR

#define NVIC_INT_CTRL       0xE000ED04      // 中断控制及状态寄存器
#define NVIC_PENDSVSET      0x10000000      // 触发软件中断的值
MEM32(NVIC_INT_CTRL) = NVIC_PENDSVSET;    // 向NVIC_INT_CTRL写NVIC_PENDSVSET,用于PendSV

摘自 Cortex-M3 权威指南 135 页

1.2 系统异常优先级寄存器

之后我们将PendSV的优先级降至最低

#define NVIC_SYSPRI2        0xE000ED22      // 系统优先级寄存器
#define NVIC_PENDSV_PRI     0x000000FF      // 配置优先级
MEM8(NVIC_SYSPRI2) = NVIC_PENDSV_PRI;   // 向NVIC_SYSPRI2写NVIC_PENDSV_PRI,设置其为最低优先级

 

摘自 Cortex-M3 权威指南 135 页

 最后我们获得了这段代码:

调用 triggerPendSVC 后便会进入 PendSV_Handler() 中断。

#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)

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
}

int main () 
{

    triggerPendSVC();
    for (;;) {
        __nop();
    }
    
    return 0;
}

__asm void PendSV_Handler ()
{

    BX   LR
}  

二、现场寄存器压栈与出栈

下列的这些寄存器即是当前程序运行的《现场》,在程序运行时,只要我们把这个《现场》保存在某个地方,等需要恢复的时候,再把他们写回寄存器中即可恢复《现场》。达到我们切换任务的目的。

标题Cortex-M3权威指南 26页

其中:

R15 程序计数器(PC):保存了当前代码执行的指令位置地址。

R14 连接寄存器(LR):则保存了当前函数执行完成后返回的指令位置地址。

R13 寄存器(MSP):指明当前堆栈位置地址。

R14 主堆栈指针(MSP): 是我们正常程序所使用的,进程堆栈指针(PSP)是任务所使用的,我们可以通过对相关寄存器置位进行切换。

其他都是临时变量寄存器,我编译器把c语言代码会转化成汇编会自动使用这些寄存器。

三、PendSVC 自动执行的步骤

如果我们保存现场,并不是所有的寄存器都需要我们手动保存再写入,PendSV 中断会像普通中断一样会帮我们自动保存当退出时,会帮我们自动恢复这些寄存器。

响应异常的第一个行动,就是自动保存现场的必要部分:依次把 xPSR, PC, LR, R12以及 R3‐R0 由硬件自动压入适当的堆栈中。如果当响应异常时,当前的代码正在使用PSP,则压入 PSP,即使用线程堆栈˗否则压入MSP,使用主堆栈。一进入了服务例程,就将一直使用主堆栈。 

至于 PSP (线程堆栈) 和 MSP (主堆栈的区别) 会在之后描述。

为什么不压栈 R4‐R11 寄存器呢,因为 ARM 上,有一套的C语言编译调用标准约定(C/C++ Procedure Call Standard for the ARM ArchitectureNJ, AAPCS, Ref5)它使得中断服务例程能用C语言编写。使汇编后的文件符合标准。

现在我们知道了 PendSV 会帮我们自动压栈 xPSR, PC, LR, R12以及 R3‐R0,然后等我们执行完毕 PendSV 中的代码后,退出 PendSV 时中断时则会自动回弹。当然,我们我们需要实现保存完整的《现场》,则需要手动压栈 R4‐R11 并且恢复。

四、汇编指令

以下是一些常用的汇编指令。

五、压栈示例代码代码解析

#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)

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
}

typedef struct _BlockType_t 
{
    unsigned long * stackPtr;
}BlockType_t;

BlockType_t * blockPtr;

unsigned long stackBuffer[1024];
BlockType_t block;

int main () 
{
    blockPtr = █
    for (;;) {
        block.stackPtr = &stackBuffer[1024]; //因为堆栈是从下向上增长,所以我们直接传递尾地址
        triggerPendSVC();
    }
    
    return 0;
}

__asm void PendSV_Handler ()
{
    //相当于c语言extren 导入blockPtr这个变量
    IMPORT  blockPtr
    
    // 加载寄存器存储地址
    LDR     R0, =blockPtr   //R0等于blockPtr变量地址
    LDR     R0, [R0]        //blockPtr解地址 此时R0等于BlockPtr的值,也就是block的地址
    LDR     R0, [R0]        //这还没完 此时R0的值只是block的地址,还需再解一次才能得到stackBuffer[1024]的地址

    // 保存寄存器
    STMDB   R0!, {R4-R11}   //递减读取进数组中,所以我们用stackBuffer[1024]的地址
    
    // 将最后的地址写入到blockPtr中
    LDR     R1, =blockPtr   //R1等于blockPtr变量地址
    LDR     R1, [R1]        //blockPtr解地址 此时R1等于blockPtr的值,也就是block的地址
    STR     R0, [R1]        //此时R0是栈顶,也就是stackBuffer[1024-7]的地址 此时将stackBuffer[1024-7]的地址赋给block的值
    
    // 修改部分寄存器,用于测试
    ADD R4, R4, #1
    ADD R5, R5, #1
    
    // 恢复寄存器
    LDMIA   R0!, {R4-R11}   //弹出寄存器 恢复到R4-R11
    
    // 异常返回
    BX      LR  //LR保存了子程序返回的代码地址 BX返回
}

5.1 汇编部分详解

在阅读下面这段汇编的时候,我们先有一个顺序捋清:

blockPtr 的值 = block 的地址

block 的值 = stackBuffer[1024] 的地址

__asm void PendSV_Handler ()
{
    //相当于c语言extren 导入blockPtr这个变量
    IMPORT  blockPtr
    
    // 加载寄存器存储地址
    LDR     R0, =blockPtr   //R0等于blockPtr变量地址
    LDR     R0, [R0]        //blockPtr解地址 此时R0等于BlockPtr的值
    LDR     R0, [R0]        //这还没完 此时R0的值是block的地址,还需再解一次才能得到stackBuffer[1024]的地址

    // 保存寄存器
    STMDB   R0!, {R4-R11}   //递减读取进数组中,所以我们用stackBuffer[1024]的地址
    
    // 将最后的地址写入到blockPtr中
    LDR     R1, =blockPtr   //R1等于blockPtr变量地址
    LDR     R1, [R1]        //blockPtr解地址 此时R1等于BlockPtr的值
    STR     R0, [R1]        //此时R0是栈顶,也就是stackBuffer[1024-7]的地址 此时将stackBuffer[1024-7]的地址赋给BlockPtr的值
    
    // 修改部分寄存器,用于测试
    ADD R4, R4, #1
    ADD R5, R5, #1
    
    // 恢复寄存器
    LDMIA   R0!, {R4-R11}   //弹出寄存器 恢复到R4-R11
    
    // 异常返回
    BX      LR  //LR保存了子程序返回的代码地址 BX返回
}

 在压栈前 R4-R11 寄存器的值

测试修改 R4 R5 的值

在出栈后 R4-R11 寄存器的值

六、基本任务切换实现

6.1 任务是什么

是一个永不返回的函数。要求无返回值,单个void* 参数,永不返回。

void taskNEntry(void *param)
{
    while()
    {

    }
}

切换任务需要保存前一任务的运行状态,恢复后一任务之前的运行状态。

需要保存线程的有:栈空间,内核寄存器。

其中,pendVS 中断会帮我们压栈 xPSR, PC, LR, R12以及 R3‐R0,我们自己需要手动压栈 R4‐R11 到任务的堆栈中即可。

6.2 任务切换的全部代码

main.h

#ifndef MAIN_H
#define MAIN_H

#include <stdint.h>

typedef uint32_t tTaskStack;

typedef struct _tTask {
    tTaskStack * stack;
    uint32_t delayTicks;
}tTask;

extern tTask * currentTask;
extern tTask * nextTask;

#endif

 main.c

#include "main.h"
#include "switch.h"
#include "ARMCM3.h"

tTask * currentTask;
tTask * nextTask;

tTask tTask1;
tTask tTask2;
tTaskStack task1Env[1024];     
tTaskStack task2Env[1024];

tTask * taskTable[2];

void delay (int count) 
{
    while (--count > 0);
}


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


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();
    }
}

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

switch.h

#ifndef SWITCH_H
#define SWITCH_H
#include "main.h"
void tTaskRunFirst (void); 
void tTaskSwitch (void);
void tTaskInit (tTask * task, void (*entry)(void *), void *param, uint32_t * stack);
#endif

switch.c

#include "switch.h"
#include "main.h"
#include "ARMCM3.h"

#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)

__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]                  //     地址是一样的,这么做不会有任何问题

PendSVHandler_nosave                  // 无论是tTaskSwitch和tTaskSwitch触发的,最后都要从下一个要运行的任务的堆栈中恢复
                                      // CPU寄存器,然后切换至该任务中运行
    LDR     R0, =currentTask          // 好了,准备切换了
    LDR     R1, =nextTask             
    LDR     R2, [R1]  
    STR     R2, [R0]                  // 先将currentTask设置为nextTask,也就是下一任务变成了当前任务
 
    LDR     R0, [R2]                  // 然后,从currentTask中加载stack,这样好知道从哪个位置取出CPU寄存器恢复运行
    LDMIA   R0!, {R4-R11}             // 恢复{R4, R11}。为什么只恢复了这么点,因为其余在退出PendSV时,硬件自动恢复

    MSR     PSP, R0                   // 最后,恢复真正的堆栈指针到PSP  
    ORR     LR, LR, #0x04             // 标记下返回标记,指明在退出LR时,切换到PSP堆栈中(PendSV使用的是MSP) 
    BX      LR                        // 最后返回,此时任务就会从堆栈中取出LR值,恢复到上次运行的位置
}

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

void tTaskRunFirst()
{
    __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
}
    
void tTaskSwitch()
{
    MEM32(NVIC_INT_CTRL) = NVIC_PENDSVSET;  // 向NVIC_INT_CTRL写NVIC_PENDSVSET,用于PendSV
}

 task1Env

 其中最核心代码是这一段:

__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]                  //     地址是一样的,这么做不会有任何问题

PendSVHandler_nosave                  // 无论是tTaskSwitch和tTaskSwitch触发的,最后都要从下一个要运行的任务的堆栈中恢复
                                      // CPU寄存器,然后切换至该任务中运行
    LDR     R0, =currentTask          // 好了,准备切换了
    LDR     R1, =nextTask             
    LDR     R2, [R1]  
    STR     R2, [R0]                  // 先将currentTask设置为nextTask,也就是下一任务变成了当前任务
 
    LDR     R0, [R2]                  // 然后,从currentTask中加载stack,这样好知道从哪个位置取出CPU寄存器恢复运行
    LDMIA   R0!, {R4-R11}             // 恢复{R4, R11}。为什么只恢复了这么点,因为其余在退出PendSV时,硬件自动恢复

    MSR     PSP, R0                   // 最后,恢复真正的堆栈指针到PSP  
    ORR     LR, LR, #0x04             // 标记下返回标记,指明在退出LR时,切换到PSP堆栈中(PendSV使用的是MSP) 
    BX      LR                        // 最后返回,此时任务就会从堆栈中取出LR值,恢复到上次运行的位置
}

6.3 切换任务代码解析 

我们在首次任务调度,因为 psp 寄存器是 0 条件相等所以会进入:

PendSVHandler_nosave                  // 无论是tTaskSwitch和tTaskSwitch触发的,最后都要从下一个要运行的任务的堆栈中恢复
                                      // CPU寄存器,然后切换至该任务中运行
    LDR     R0, =currentTask          // 好了,准备切换了
    LDR     R1, =nextTask             
    LDR     R2, [R1]  
    STR     R2, [R0]                  // 先将currentTask设置为nextTask,也就是下一任务变成了当前任务
 
    LDR     R0, [R2]                  // 然后,从currentTask中加载stack,这样好知道从哪个位置取出CPU寄存器恢复运行
    LDMIA   R0!, {R4-R11}             // 恢复{R4, R11}。为什么只恢复了这么点,因为其余在退出PendSV时,硬件自动恢复

    MSR     PSP, R0                   // 最后,恢复真正的堆栈指针到PSP  
    ORR     LR, LR, #0x04             // 标记下返回标记,指明在退出LR时,切换到PSP堆栈中(PendSV使用的是MSP) 
    BX      LR                        // 最后返回,此时任务就会从堆栈中取出LR值,恢复到上次运行的位置

前几行是将当前任务 (currentTask) 赋值给 (nextTask) 任务,使得当前任务就是下一个任务。

然后先出栈到 R4-R11 寄存器。

LDMIA   R0!, {R4-R11}             // 恢复{R4, R11}。为什么只恢复了这么点,因为其余在退出

 之后直接将 R0 赋值 PSP 堆栈指针,这样在退出 pendSV 时即可自动恢复其他的寄存器。

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

如果不是首次调度任务,仅需要将 R4-R11 寄存器压入即可。其他寄存器在进入pendSV之前就自动压入到 PSP 寄存器了。

    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]                  //     地址是一样的,这么做不会有任何问题

6.4 初始化任务代码解析

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

在初始化任务时只需要给默认堆栈寄存器赋值即可,但是一定要按照切换任务堆栈顺序操作。

在其中我们将 R4-R11 寄存器最后入栈,这是我们手动实现的。所以需要最后入栈。

R1 R2 R3 R12 R14 XPSR 则需要按这个顺序压栈初始化,因为这是 ARM 中断自动弹出到指定寄存器的。

6.5 开启伪任务调度

我们之前解释了任务调度的原理,那么要在什么时候开始调度呢?

为了方便观察,我们在这里仅在任务循环最后一行调度。

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

tTaskSched(); 这就是我们调度任务的函数了,其实很简单,只是切换了一下顺序。 

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

 tTaskSwitch(); 函数触发 PendSV 中断,之后就成功调度这两个任务啦。

七、使用滴答定时器实现时间片轮询

main.c

#include "main.h"
#include "switch.h"
#include "ARMCM3.h"

tTask * currentTask;
tTask * nextTask;

tTask tTask1;
tTask tTask2;
tTaskStack task1Env[1024];     
tTaskStack task2Env[1024];

tTask * taskTable[2];

void delay (int count) 
{
    while (--count > 0);
}

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


void tSetSysTickPeriod(uint32_t ms)
{
  SysTick->LOAD  = ms * SystemCoreClock / 1000 - 1; 
  NVIC_SetPriority (SysTick_IRQn, (1<<__NVIC_PRIO_BITS) - 1);
  SysTick->VAL   = 0;                           
  SysTick->CTRL  = SysTick_CTRL_CLKSOURCE_Msk |
                   SysTick_CTRL_TICKINT_Msk   |
                   SysTick_CTRL_ENABLE_Msk; 
}

void SysTick_Handler () 
{
    tTaskSched();
}


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

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


int main ()
{
    // 初始化任务1和任务2结构,传递运行的起始地址,想要给任意参数,以及运行堆栈空间
    tTaskInit(&tTask1, task1Entry, (void *)0x11111111, &task1Env[1024]);
    tTaskInit(&tTask2, task2Entry, (void *)0x22222222, &task2Env[1024]);
    
    // 接着,将任务加入到任务表中
    taskTable[0] = &tTask1;
    taskTable[1] = &tTask2;
    
    nextTask = taskTable[0];
    
    tTaskRunFirst();            //开启pendSV中断
    tSetSysTickPeriod(1);       //开启时间片调度
    return 0;
}

我们仅仅通过上面的代码修改 main.c 即可实现时间片轮询。

void tSetSysTickPeriod(uint32_t ms)
{
  SysTick->LOAD  = ms * SystemCoreClock / 1000 - 1; 
  NVIC_SetPriority (SysTick_IRQn, (1<<__NVIC_PRIO_BITS) - 1);
  SysTick->VAL   = 0;                           
  SysTick->CTRL  = SysTick_CTRL_CLKSOURCE_Msk |
                   SysTick_CTRL_TICKINT_Msk   |
                   SysTick_CTRL_ENABLE_Msk; 
}

void SysTick_Handler () 
{
    tTaskSched();
}

在任务结束函数中我们则不在主动去调度:

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

核心代码仅仅是使用滴答定时器调用切换任务。 

 关于 ARM 内核滴答定时器使用我有之前的一篇笔记,再此就不过多赘述

STM32 寄存器操作 systick 滴答定时器 与中断_stm32滴答中断-CSDN博客

实现效果如下,每次滴答定时器切换即调度一次任务。

放大来看,任务正在持续运行而且调度中。

 八、实现空闲任务

对于单片机来说来说,使用这样的延迟不仅不精准,而且还在浪费宝贵的cpu资源。

void delay (int count) 
{
    while (--count > 0);
}

我们可以这样来处理这个问题,如果 tank1 延迟,我们就调度到 tank2 运行。等tank1 延迟结束后,我们再切换为 tank1。

如果tank1 和 tank2 都在延时,我们就切换到空闲函数中。

8.1 实现代码

main.h

#ifndef MAIN_H
#define MAIN_H

#include <stdint.h>

typedef uint32_t tTaskStack;

typedef struct _tTask {
    tTaskStack * stack;
    uint32_t delayTicks;
}tTask;

extern tTask * currentTask;
extern tTask * nextTask;

#endif

main.c

#include "main.h"
#include "switch.h"
#include "ARMCM3.h"

tTask * currentTask;
tTask * nextTask;

tTask tTask1;
tTask tTask2;
tTask * idleTask; //空闲任务
tTaskStack task1Env[1024];     
tTaskStack task2Env[1024];

tTask * taskTable[2];

void delay (int count) 
{
    while (--count > 0);
}

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();
}


void tSetSysTickPeriod(uint32_t ms)
{
  SysTick->LOAD  = ms * SystemCoreClock / 1000 - 1; 
  NVIC_SetPriority (SysTick_IRQn, (1<<__NVIC_PRIO_BITS) - 1);
  SysTick->VAL   = 0;                           
  SysTick->CTRL  = SysTick_CTRL_CLKSOURCE_Msk |
                   SysTick_CTRL_TICKINT_Msk   |
                   SysTick_CTRL_ENABLE_Msk; 
}

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

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


int task1Flag;
void task1Entry (void * param) 
{
    tSetSysTickPeriod(10);
    for (;;) 
    {
        task1Flag = 1;
        tTaskDelay(1);
        task1Flag = 0;
        tTaskDelay(1);
    }
}

int task2Flag;
void task2Entry (void * param) 
{
    for (;;)
    {
        task2Flag = 1;
        tTaskDelay(1);
        task2Flag = 0;
        tTaskDelay(1);
    }
}

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 SysTick_Handler () 
{
    tTaskSystemTickHandler () ;
}

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

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

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

 除main函数改变外,其他函数不变。

8.2代码解析

首先我们修改了 _tTask 任务结构体,新添了一个 delayTicks。

typedef struct _tTask {
    tTaskStack * stack;
    uint32_t delayTicks;
}tTask;

 之后我们添加了一个rtos的延迟函数,用于取代原来的延迟函数。

并且延迟后立刻调用 tTaskSched() 判定调度或延迟。

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

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

滴答定时器则调用 tTaskSystemTickHandler () 函数,他会 -1 延迟。

void tTaskSystemTickHandler () 
{
    // 检查所有任务的delayTicks数,如果不0的话,减1。
    int i;
    for (i = 0; i < 2; i++) 
    {
        if (taskTable[i]->delayTicks > 0)
        {
            taskTable[i]->delayTicks--;
        }
    }
    
    // 这个过程中可能有任务延时完毕(delayTicks = 0),进行一次调度。
    tTaskSched();
}

最重要的来了。我们重写了 tTaskSched(); 任务调度函数。

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();
}

 实现效果如下:

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/725238.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

百度一下首页制作(HTML+CSS)

部分代码展示&#xff1a; <!DOCTYPE html> <html> <head><meta charset"utf-8"><title>百度一下&#xff0c;你就知道</title><style type"text/css">/*清除元素默认性质*/body { margin: 0;padding: 0;list-…

“鸿蒙开发之图片下载”--案例问题整理

鸿蒙开发之图片下载 关于以上连接中案例demo使用问题整理如下图 而且在写这个案例的时候记得添加权限 "requestPermissions":[{"name" : "ohos.permission.INTERNET"}]

24计算机应届生的活路是什么

不够大胆❗ 很多小伙伴在找工作时觉得自己没有竞争力&#xff0c;很没有自信&#xff0c;以至于很害怕找工作面试&#xff0c;被人否定的感觉很不好受。 其实很多工作并没有想象中的高大上&#xff0c;不要害怕&#xff0c;计算机就业的方向是真的广&#xff0c;不要走窄了&…

八字综合测算网整站源码程序/黄历/灵签/排盘/算命/生肖星座/日历网/周公解梦

八字综合测算网整站源码程序/黄历/灵签/排盘/算命/生肖星座/日历网/周公解梦 演示地址&#xff1a; https://s24.gvyun.com/ 手机端地址&#xff1a; https://ms24.gvyun.com/ 网站功能分类&#xff1a; 八字&#xff1a;八字测算&#xff1b;日干论命&#xff1b;称骨论命…

SCI一区TOP|常青藤优化算法(IVYA)原理及实现【免费获取Matlab代码】

目录 1.背景2.算法原理2.1算法思想2.2算法过程 3.结果展示4 .参考文献5.代码获取 1.背景 2024年&#xff0c;M Ghasemi受到自然界中常青藤生长行为启发&#xff0c;提出了常青藤优化算法&#xff08;Ivy Algorithm, IVYA&#xff09;。 2.算法原理 2.1算法思想 IVYA模拟常青…

几内亚ECTN是什么?怎么办理?建议收藏!

几内亚ECTN是什么&#xff1f;怎么办理&#xff1f;建议收藏&#xff01; 一、去往几内亚的货物&#xff0c;从六月一日开始强制实施ECTN制度&#xff0c;取消原来并行的ENS制度。如若货物到港前没申请ECTN&#xff0c;几内亚海关将会强行扣货。 ECTN是英文&#xff1a;ELECTR…

浅谈设计师的设计地位

在当今这个创意无限的时代&#xff0c;设计师的地位日益凸显。他们以独特的视角和精湛的技能&#xff0c;为我们的生活带来了无尽的色彩与灵感。然而&#xff0c;随着行业的不断发展&#xff0c;设计师如何在众多同行中脱颖而出&#xff0c;提升自己的设计地位呢&#xff1f;答…

Clickhouse监控_监控的指标以及Grafana配置Clickhouse指标异常时触发报警

使用PrometheusGrafana来监控Clickhouse服务和性能指标 Clickhouse监控指标的官方文档https://clickhouse.com/docs/zh/operations/monitoring 建议使用PrometheusGrafana组合监控Clickhouse服务和性能指标&#xff0c;数据流向&#xff1a;Prometheus的clickhouse_exporter组件…

换位置(C++)

问题描述 体育课上&#xff0c;有一个班级的同学站成了一队&#xff0c;体育老师请最高的和最矮的两位同学调换一下位置&#xff0c;其余的同学不要动&#xff0c;请编程实现&#xff01;&#xff08;假设所有人的高矮都是不一样的&#xff09; 输入 第一行有一个整数 &…

如何定制Spring的错误json信息

一&#xff0c;前言 相信很多同学都有遇到过这样的spring错误信息。 在我们没有做catch处理时或者做全局的exceptionHandle时&#xff0c;Spring遇到抛出向外的异常时&#xff0c;就会给我们封装返回这么个格式的异常信息。 那么问题来了&#xff0c;我们能否对这个返回增加错…

大数据工程师如何做到数据可视化?

好的数据可视化作品都是通过不断的数据对比分析实战出来的。 今天给大家带来一篇大数据工程师干货&#xff0c;从多角度解析做数据可视化的重要性&#xff0c;并解读一些适用的应用场景。大数据工程师们刷到这篇文章时一定要进来看看&#xff0c;满满的干货。 目录 1. 什么是数…

RAG 流程及论文串烧

文档切片 文档切片的五个层次 https://medium.com/anuragmishra_27746/five-levels-of-chunking-strategies-in-rag-notes-from-gregs-video-7b735895694d#b123 Basic RAG 与 Advanced RAG https://pub.towardsai.net/advanced-rag-techniques-an-illustrated-overview-04d…

用画图,将2张图片,合并成 一张图片 + 压缩体积

合并 第一步&#xff1a;选中要做比较的两张图片其中一张&#xff0c;单击鼠标右键&#xff0c;选择“打开方式--画图”。 第二步&#xff1a;如果图片过大&#xff0c;占据了整个屏幕不好观察&#xff0c;用右下角的标尺&#xff0c;缩小视图 第三步&#xff1a;鼠标左键按住…

Day55 代码随想录打卡|二叉树篇---二叉搜索树中的插入操作

题目&#xff08;leecode T701&#xff09;&#xff1a; 给定二叉搜索树&#xff08;BST&#xff09;的根节点 root 和要插入树中的值 value &#xff0c;将值插入二叉搜索树。 返回插入后二叉搜索树的根节点。 输入数据 保证 &#xff0c;新值和原始二叉搜索树中的任意节点值…

Java并发编程深度解析:构建高并发应用的实践与探究

摘要&#xff1a;随着互联网技术的飞速发展&#xff0c;大型分布式系统对并发处理能力的要求越来越高。Java作为企业级应用的主流开发语言&#xff0c;在并发编程方面有着深厚的积累和强大的生态支持。本文将深入探讨Java并发编程的基础知识&#xff0c;高级技巧&#xff0c;以…

​海康威视 isecure center 综合安防管理平台任意文件上传漏洞

文章目录 前言声明一、漏洞描述二、影响版本三、漏洞复现四、修复方案 前言 海康威视是以视频为核心的智能物联网解决方案和大数据服务提供商,业务聚焦于综合安防、大数据服务和智慧业务。 海康威视其产品包括摄像机、多屏控制器、交通产品、传输产品、存储产品、门禁产品、消…

C++初学者指南第一步---7.控制流(基础)

C初学者指南第一步—7.控制流&#xff08;基础&#xff09; 文章目录 C初学者指南第一步---7.控制流&#xff08;基础&#xff09;1.术语:表达式/语句Expressions表达式Statements语句 2.条件分支3.Switching(切换):基于值的分支4.三元条件运算符5.循环迭代基于范围的循环   C…

2024下《网络工程师》50个高频考点汇总,背就有效!

宝子们&#xff01;上半年软考已经结束一段时间了&#xff0c;准备考下半年软考中级-网络工程师的小伙伴们可以开始准备了&#xff0c;这里给大家整理了50个高频考点&#xff0c;涵盖全书90%以上重点&#xff0c;先把这个存下&#xff01;再慢慢看书&#xff0c;边看书边背这个…

示例:WPF中如何不卡顿页面的情况加载大量数据

一、目的&#xff1a;在开发过程中经常会遇到一个ListBox列表里面需要加载大量数据&#xff0c;但是加载过程中会假死卡顿影响用户体验&#xff0c;或者是你的主页面加载了大量控件&#xff0c;或者切换到一个有大量元素的页面都会有这种体验&#xff0c;因为加载的都是UI元素不…

工程打包与运行

黑马程序员Spring Boot2 文章目录 先点击cean&#xff0c;确保打包之前是个干净的环境点击package进行打包&#xff0c;打包成功之后可以看到target文件夹下的文件 到项目目录下使用终端打开&#xff0c;并使用以下命令运行打包好的程序 如果遇到没有主清单属性的问题&#xff…