实现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

相关文章

如何使用Java实现请求deepseek

《如何使用Java实现请求deepseek》这篇文章主要为大家详细介绍了如何使用Java实现请求deepseek功能,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录1.deepseek的api创建2.Java实现请求deepseek2.1 pom文件2.2 json转化文件2.2

python使用fastapi实现多语言国际化的操作指南

《python使用fastapi实现多语言国际化的操作指南》本文介绍了使用Python和FastAPI实现多语言国际化的操作指南,包括多语言架构技术栈、翻译管理、前端本地化、语言切换机制以及常见陷阱和... 目录多语言国际化实现指南项目多语言架构技术栈目录结构翻译工作流1. 翻译数据存储2. 翻译生成脚本

如何通过Python实现一个消息队列

《如何通过Python实现一个消息队列》这篇文章主要为大家详细介绍了如何通过Python实现一个简单的消息队列,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录如何通过 python 实现消息队列如何把 http 请求放在队列中执行1. 使用 queue.Queue 和 reque

Python如何实现PDF隐私信息检测

《Python如何实现PDF隐私信息检测》随着越来越多的个人信息以电子形式存储和传输,确保这些信息的安全至关重要,本文将介绍如何使用Python检测PDF文件中的隐私信息,需要的可以参考下... 目录项目背景技术栈代码解析功能说明运行结php果在当今,数据隐私保护变得尤为重要。随着越来越多的个人信息以电子形

使用 sql-research-assistant进行 SQL 数据库研究的实战指南(代码实现演示)

《使用sql-research-assistant进行SQL数据库研究的实战指南(代码实现演示)》本文介绍了sql-research-assistant工具,该工具基于LangChain框架,集... 目录技术背景介绍核心原理解析代码实现演示安装和配置项目集成LangSmith 配置(可选)启动服务应用场景

使用Python快速实现链接转word文档

《使用Python快速实现链接转word文档》这篇文章主要为大家详细介绍了如何使用Python快速实现链接转word文档功能,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 演示代码展示from newspaper import Articlefrom docx import

前端原生js实现拖拽排课效果实例

《前端原生js实现拖拽排课效果实例》:本文主要介绍如何实现一个简单的课程表拖拽功能,通过HTML、CSS和JavaScript的配合,我们实现了课程项的拖拽、放置和显示功能,文中通过实例代码介绍的... 目录1. 效果展示2. 效果分析2.1 关键点2.2 实现方法3. 代码实现3.1 html部分3.2

Java深度学习库DJL实现Python的NumPy方式

《Java深度学习库DJL实现Python的NumPy方式》本文介绍了DJL库的背景和基本功能,包括NDArray的创建、数学运算、数据获取和设置等,同时,还展示了如何使用NDArray进行数据预处理... 目录1 NDArray 的背景介绍1.1 架构2 JavaDJL使用2.1 安装DJL2.2 基本操

最长公共子序列问题的深度分析与Java实现方式

《最长公共子序列问题的深度分析与Java实现方式》本文详细介绍了最长公共子序列(LCS)问题,包括其概念、暴力解法、动态规划解法,并提供了Java代码实现,暴力解法虽然简单,但在大数据处理中效率较低,... 目录最长公共子序列问题概述问题理解与示例分析暴力解法思路与示例代码动态规划解法DP 表的构建与意义动

java父子线程之间实现共享传递数据

《java父子线程之间实现共享传递数据》本文介绍了Java中父子线程间共享传递数据的几种方法,包括ThreadLocal变量、并发集合和内存队列或消息队列,并提醒注意并发安全问题... 目录通过 ThreadLocal 变量共享数据通过并发集合共享数据通过内存队列或消息队列共享数据注意并发安全问题总结在 J