一、实现 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
}
二、现场寄存器压栈与出栈
下列的这些寄存器即是当前程序运行的《现场》,在程序运行时,只要我们把这个《现场》保存在某个地方,等需要恢复的时候,再把他们写回寄存器中即可恢复《现场》。达到我们切换任务的目的。
其中:
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();
}
实现效果如下: