RTOS任务切换原理
任务是什么?
- 任务的外观:一个永远不会返回的函数。
- 无返回值
- 单个
void *
参数,即可以传递任意参数 - 永远不会返回, 任务内一般是一个死循环
- 任务的内在: 一个函数的执行
- 分为堆、栈、数据区、代码区
- 代码区由程序员实现,存放编写的代码
- 通过代码可以控制寄存器进行运算等操作
- 数据区的分布由编译器根据代码自动实现
- 堆、栈由编译器自动控制,同时也可以由代码来显示控制
- 内核寄存器主要也由编译器自动控制,看代码需要来分配哪些寄存器使用
任务切换原理
本质
保存前一任务当前的运行状态,恢复后一任务之前的运行状态**
不保存会怎么样?一个简单的例子:假如任务1即将运算
R12+1
,此时发生任务切换,任务2将R12的值更改,那么再次切换回任务1时,计算R12+1
的结果就不是预期值!
任务运行状态
** 发生任务切换时,我们需要保存哪些东西?
** 前面我们提到,任务的内在就是一个函数的执行,可以分为堆、栈、数据区、代码区,代码执行时会用到内核寄存器
- 代码区、数据区:由编译器自动分配,每个函数有自己独立的代码区和数据区,并不冲突,因此也不需要保存。
- 堆:一般不使用。
- 栈:硬件只支持两个堆栈空间,不同任务不能共用!原理同上一个info。
- 内核寄存器:编译器会在发生如:函数调用、异常处理等情况时自动将某些内核寄存器保存到栈中,但仍有一些是没有被保存的,例如上一个info中的R12寄存器。
- 还有一些其他的状态数据,也需要保存。 ** 因此我们需要保存的有:一些未被硬件自动保存的内核寄存器、一些其它的状态数据、栈(指针)。
保存任务运行状态
** 保存方法:为每个任务配置独立的栈空间,用于保存该任务的所有状态数据。这个栈中存放编译器自动保存的数据、内核寄存器的值以及其它的状态数据。
切换原理
发生任务切换时,将当前任务的运行状态保存到该任务的栈空间,同时从下一个任务的栈空间中恢复该任务的运行状态。
任务切换的实现
代码定义任务
1
2
3
4
5
6
7
// 定义任务堆栈的类型为uint32
typedef uint32_t taskStack_t;
// 定义任务结构
typedef struct _t_Task {
taskStack_t *stack; // 任务的栈指针
}task_t;
当前的任务结构,只有一个任务栈指针。
任务初始化
要实现切换任务,首先需要实现对任务的初始化。因为任务有任务的运行状态,我们需要初始化这个状态,也就是向任务的栈空间中预存任务的初始运行状态。这样在进入PendSVC异常的时候,我们就可以将这些预存的状态恢复到CPU。
在初始化中,我们需要在任务的栈中保存xPSR、PC(R15)、LR(R14)、R12以及R3-R0, R11-R4
。因为在进入异常的时候,硬件会自动按顺序保存xPSR、PC(R15)、LR(R14)、R12以及R3-R0
这些寄存器,我们需手动保存R11-R4
寄存器。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 任务初始化
void taskInit (task_t* task, void (*entry)(void*), void* param, taskStack_t* stack) {
// 进入中断/异常时, 硬件会自动将8个寄存器压栈,顺序是xPSR、PC(R15)、LR(R14)、R12以及R3-R0
*(--stack) = (unsigned long)(1 << 24); // xPSR中第24位,即T标志位设置为1,否则进入ARM模式,这在CM3上不允许!
*(--stack) = (unsigned long)entry; // 任务的地址(函数地址)
*(--stack) = (unsigned long)0x14;
*(--stack) = (unsigned long)0x12;
*(--stack) = (unsigned long)0x03;
*(--stack) = (unsigned long)0x02;
*(--stack) = (unsigned long)0x01;
*(--stack) = (unsigned long)param;
// 手动保存R11-R4寄存器
*(--stack) = (unsigned long)0x11;
*(--stack) = (unsigned long)0x10;
*(--stack) = (unsigned long)0x09;
*(--stack) = (unsigned long)0x08;
*(--stack) = (unsigned long)0x07;
*(--stack) = (unsigned long)0x06;
*(--stack) = (unsigned long)0x05;
*(--stack) = (unsigned long)0x04;
task->stack = stack;
}
我们在PendSVC异常中,需要手动恢复R11-R4
寄存器,随后将栈指针(PSP
)指向R0
寄存器在栈中的位置,这样在退出PendSVC异常的时候,硬件会自动从PSP
寄存器指向的地址开始依次弹出R0-R3, R12, R14, R15, xPSR
。 因为我们指定了PC(R15)
寄存器,因此在退出异常之后,就会到PC指向的地址开始运行,也就实现了切换到另一个任务。
运行第一个任务
要运行第一个任务,按上述理论,只需将第一个任务进行初始化,然后在PendSVC异常中进行恢复即可。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
__asm void PendSV_Handler (void) {
IMPORT currentTask // 指针,指向当前运行的任务
IMPORT nextTask // 指向下一个要运行的任务
MRS R0, PSP
CBZ R0, noSave // 检测标志位,是第一个任务就跳过保存阶段
STMDB R0!, {R4-R11} // 手动保存R4-R11到当前任务的栈空间,其它的寄存器已经被硬件自动保存
LDR R1, =currentTask
LDR R1, [R1]
STR R0, [R1] // 将更新后的栈指针保存到当前任务的栈指针中
noSave
LDR R0, =currentTask
LDR R1, =nextTask
LDR R1, [R1]
STR R1, [R0] // 更新currenttask指向nexttask
LDR R0, [R1]
LDMIA R0!, {R4-R11} // 从nexttask中手动恢复R4-R11
MSR PSP, R0 // 更新PSP,主要是跳过R4-R11,指向保存R0的地址,以便退出异常时硬件自动恢复寄存器
ORR LR, LR, #0x04 // 指定LR,即指定退出异常后使用PSP指针而不是MSP
BX LR
}
因为第一次进入第一个任务,没有运行状态需要保存,只需要从第一个任务的栈中恢复运行状态就可以了。 因此在第一次运行第一个任务之前将PSP置0,作为标志位来跳过保存阶段。当然,也可以设置currenttask为一个无意义的地址,这样进行保存也没什么,就可以省略用标志位这一步。
到此为止就实现了简单的任务切换。
实现任务延时
软定时器
要实现任务延时,需要使用定时器,而且每个任务都配备一个定时器才行,但是硬件只有一个定时器而任务数量很多,因此可以利用SysTick这个硬件定时器来实现软定时器。因为SysTick周期性触发中断,因此可以以这个周期为最基本的软件定时器的时间单位。每触发一次SysTick就将软定时器的值-1即可,因此软定时器的定时时间都是SysTick中断的倍数。中断处理也需要时间,不能太频繁这样会导致系统在切换任务这个事情上占用太多资源,本末倒置,一般设置为10ms-100ms就可以。
软定时器延时精度并不准确,要注意使用场合 eg:延时一个单位,在两个SysTick中间开始延时,那么只能延时半个SysTick中断周期。假如在将要触发定时器中断的时候发生了更高级别的中断,会导致延时时间变长
END