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

2024-06-20 03:36

本文主要是介绍实现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, #1ADD 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, #1ADD 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 variableMRS     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 variableMRS     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();
}

 实现效果如下:

这篇关于实现rtos操作系统 【一】基本任务切换实现的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1076984

相关文章

C++中零拷贝的多种实现方式

《C++中零拷贝的多种实现方式》本文主要介绍了C++中零拷贝的实现示例,旨在在减少数据在内存中的不必要复制,从而提高程序性能、降低内存使用并减少CPU消耗,零拷贝技术通过多种方式实现,下面就来了解一下... 目录一、C++中零拷贝技术的核心概念二、std::string_view 简介三、std::stri

C++高效内存池实现减少动态分配开销的解决方案

《C++高效内存池实现减少动态分配开销的解决方案》C++动态内存分配存在系统调用开销、碎片化和锁竞争等性能问题,内存池通过预分配、分块管理和缓存复用解决这些问题,下面就来了解一下... 目录一、C++内存分配的性能挑战二、内存池技术的核心原理三、主流内存池实现:TCMalloc与Jemalloc1. TCM

OpenCV实现实时颜色检测的示例

《OpenCV实现实时颜色检测的示例》本文主要介绍了OpenCV实现实时颜色检测的示例,通过HSV色彩空间转换和色调范围判断实现红黄绿蓝颜色检测,包含视频捕捉、区域标记、颜色分析等功能,具有一定的参考... 目录一、引言二、系统概述三、代码解析1. 导入库2. 颜色识别函数3. 主程序循环四、HSV色彩空间

mapstruct中的@Mapper注解的基本用法

《mapstruct中的@Mapper注解的基本用法》在MapStruct中,@Mapper注解是核心注解之一,用于标记一个接口或抽象类为MapStruct的映射器(Mapper),本文给大家介绍ma... 目录1. 基本用法2. 常用属性3. 高级用法4. 注意事项5. 总结6. 编译异常处理在MapSt

Python实现精准提取 PDF中的文本,表格与图片

《Python实现精准提取PDF中的文本,表格与图片》在实际的系统开发中,处理PDF文件不仅限于读取整页文本,还有提取文档中的表格数据,图片或特定区域的内容,下面我们来看看如何使用Python实... 目录安装 python 库提取 PDF 文本内容:获取整页文本与指定区域内容获取页面上的所有文本内容获取

基于Python实现一个Windows Tree命令工具

《基于Python实现一个WindowsTree命令工具》今天想要在Windows平台的CMD命令终端窗口中使用像Linux下的tree命令,打印一下目录结构层级树,然而还真有tree命令,但是发现... 目录引言实现代码使用说明可用选项示例用法功能特点添加到环境变量方法一:创建批处理文件并添加到PATH1

Java使用HttpClient实现图片下载与本地保存功能

《Java使用HttpClient实现图片下载与本地保存功能》在当今数字化时代,网络资源的获取与处理已成为软件开发中的常见需求,其中,图片作为网络上最常见的资源之一,其下载与保存功能在许多应用场景中都... 目录引言一、Apache HttpClient简介二、技术栈与环境准备三、实现图片下载与保存功能1.

canal实现mysql数据同步的详细过程

《canal实现mysql数据同步的详细过程》:本文主要介绍canal实现mysql数据同步的详细过程,本文通过实例图文相结合给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的... 目录1、canal下载2、mysql同步用户创建和授权3、canal admin安装和启动4、canal

Nexus安装和启动的实现教程

《Nexus安装和启动的实现教程》:本文主要介绍Nexus安装和启动的实现教程,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、Nexus下载二、Nexus安装和启动三、关闭Nexus总结一、Nexus下载官方下载链接:DownloadWindows系统根

SpringBoot集成LiteFlow实现轻量级工作流引擎的详细过程

《SpringBoot集成LiteFlow实现轻量级工作流引擎的详细过程》LiteFlow是一款专注于逻辑驱动流程编排的轻量级框架,它以组件化方式快速构建和执行业务流程,有效解耦复杂业务逻辑,下面给大... 目录一、基础概念1.1 组件(Component)1.2 规则(Rule)1.3 上下文(Conte