文章目录
前言
介绍下FreeRTOS的任务调度实现机制。
1. 任务调度的实现
这里讨论的任务调度是指FreeRTOS中负责任务的创建、执行、切换、销毁等操作的任务管理器。
如果过去没有操作系统相关的开发经验,那么直接去理解什么是操作系统级别的任务调度会有点懵。这时我们可以借助裸机实现软件定时器去间接理解FreeRTOS的任务调度
只是相较于定时器的调度方式,操作系统的任务调度增加了优先级、打断、挂起、现场恢复等特性。
1.1 定时器的调度实现方式分析
当我们需要实现一个只支持定时运行软件定时器时,只要有以下几个要素就行
- 定时器状态循环监测(负责检查是否有定时器可以执行了,然后进行定时器的任务切换)
- 基准时钟(负责定时器的计时,一般是用滴答定时器产生的计数值)
- 存放定时器的队列或者链表(用于存放定时器的信息)
定时器状态监测
在软件定时器中我们是通过main中执行的while(1),不停的进行循环执行Timer_Task(),然后监测到哪个定时器运行时间到了,则调用该定时器的函数。
void Timer_Task(void)
{
uint8_t i = 0;
Timer_S* timer = NULL;
uint32_t curTickCnt = 0;
TimerRet_E ret = TIMER_RET_OK;
for(i=0; i < TIMER_NUMBER; i++){
//检查已经正在使用并且是RUN状态的定时器
timer = &g_allTimer[i];
if(timer->useFlag && timer->state == TIMER_STATE_RUN){
//获取当前的tick时间
curTickCnt = Plat_Dleay_GetSysTick();
//计算是否需要运行定时器
if(CAL_TIME_PASS(curTickCnt , timer->lasTickCnt) > timer->ms){
ret = timer->entry(timer->args);
if(ret != TIMER_RET_REPEAT){//如果定时器不需要反复执行,则暂停定时器
Timer_Stop(timer);
}
timer->lasTickCnt = curTickCnt;
}
}
}
}
int main(void)
{
//进行必要的初始化xxx
timer_init();//初始化定时器
while(1){
Timer_Task();
}
}
基准时钟
基准时钟,用于我们定时器的基准计时。基准时钟最好是用滴答定时器这种,因为基准时钟的精度,直接影响了定时器的精度。
队列或者链表
实际上也不只是队列或者链表啦,只要有个能存放定时器信息的地方就行了。像我之前博客中提到的逻机软件定时器的实现就是使用的数组存放定时器信息。
1.2 定时器的基础上实现操作系统级别的任务调度
如果我们要做一个操作系统级别的软件定时器,那么我们需要在原有的基础上增加优先级调度策略、时间片调度策略、定时器的挂起以及现场还原。
那我们一点点的看,这些应该怎么实现。
优先级和时间片调度策略
大家首先要理解什么是优先级和时间片调度策略:
- 优先级:
高优先级的任务可以打断低优先级任务的执行,然后让系统转去执行高优先级任务,用于保证实时性。 - 时间片调度
同等优先级的任务采用时间片的方式进行切换,例如每个任务运行10ms后让出CPU的资源给其它的定时器执行。这样可以保证并行运行。
如果我们要实现优先级和时间片,那么我们就不能在while(1)中运行定时器的监测主体Timer_Task。
为啥呢?大家想一下如果我们在while(1)()中执行,那么是不是就要等上一个任务执行完了,才能切换到下一个任务。这样高优先级的任务就不能打断低优先级的(因为要等低优先级的运行完了才会执行下一轮监测),同时时间片调度也基本上实现不了(因为每次都要等一个任务运行结束了才能运行下一个任务。)
那到底怎么做呢?答案是:借助中断
在中断中去执行定时器状态检查,因为中断可以打断正在运行的定时器。当中断中检查到某个高优先级定时器到来了或者定时器的运行时间片结束了,则执行任务切换。
但是我们并非是在中断中直接执行软件定时器,而是将要运行的软件定时器的地址交给硬件的寄存器。让CPU在下次执行指令时,执行该定时器。因为如果我们直接在中断中直接直接定时器,那么中断内的执行时间会太长,中断会被卡住,不符合我们使用中断的常识和规范。
定时器的挂起以及现场还原
借助中断我们能够实现打断任务的执行,但是对于打断的任务怎么恢复其现场呢? 答案是:保存任务的堆栈信息
每个任务在执行时都会在CPU占据一定的资源,包括寄存器的值、程序计算器(PC)、堆栈指针(SP)等。任务在执行过程中,这些资源会不断地被修改。所以,在任务切换时,需要将当前任务的执行状态保存下来,以便以后能够恢复并从上次中断的地方继续执行。
- 寄存器的状态:包括通用寄存器(如 r0, r1 等),它们保存了当前任务的数据和执行状态。不同的任务会使用这些寄存器来保存自己的数据,因此它们的值在任务切换时必须被保存。
- 程序计数器(PC):程序计数器记录着任务执行的下一条指令的位置。当任务切换时,必须保存当前任务的程序计数器的值,以便以后恢复时从正确的位置继续执行。
- 堆栈指针(SP):堆栈用于保存函数调用的局部变量、返回地址、保存的寄存器值等。当任务切换时,堆栈指针的位置也必须被保存,以确保任务的栈在切换后可以正确恢复。
1.3 FreeRTOS的任务调度到底是怎么实现的
任务状态检查
通过上面定时器的解释,结合FreeRTOS需要支持优先级调度和时间片,所以需要在任务(函数)执行过程中,能够打断任务的执行。因此就要借助中断,而FreeRTOS的任务状态检查函数就是运行在tick终端中这个定时器中断一般1ms产生一次。
每次中断触发时,FreeRTOS的调度器就会去检查当前是否有高优先级任务需要打断低优先级任务,或者有些任务的时间片达到了。如果达到了那么就触发任务切换。
之前我们有讨论过出发任务切换不能在终端中直接去运行某个函数,因为这样会阻塞住中断。
这里同样它是采用 激活PendSV中断,由PendPendSV中断去执行任务的切换。
任务的时间基准
任务调度器的时间基准值或者说心跳值,也是在tick中断中触发的。
任务的切换
在上面任务的状态检查中我们有提到当需要任务切换时会触发pendSV
中断,由该中断去执行任务的切换。
为啥这样做呢?
首先我们需要知道pendSV是ARM Cortex-M 微控制器中的一种特殊中断,经常用于执行任务切换和上下文切换的操作。因为该中断的优先级很低一般是优先级最低的那个,当我们在tick中断中触发了pendSV终端时,由于该中断优先级低于tick中断,所以它的执行会有一定的延后性(需要等高优先级的tick运行结束后才会去运行该中断),这样做的好处是进行任务的切换时不会影响其他高优先级中断的执行。
任务的切换和状态保存
上面我们提到其主要是在pendSV的中断中所进行的。
其源码如下,下面这段代码的大概含义是
- 先将当前任务的psp栈帧信息保存到当前任务的TCB中。
- 执行任务切换(任务切换实际上就是检查就绪态链表中的任务,先从最高优先级的开始遍历,不同优先级的任务链表是不一样的。)
- 恢复新任务的栈信息。(要执行新任务了,对应的我们需要将新任务的现场信息恢复)
pendSV中断的代码
__asm void xPortPendSVHandler( void )
{
extern uxCriticalNesting;
extern pxCurrentTCB;
extern vTaskSwitchContext;
PRESERVE8
mrs r0, psp
isb
/* Get the location of the current TCB. */
ldr r3, =pxCurrentTCB
ldr r2, [r3]
/* Is the task using the FPU context? If so, push high vfp registers. */
tst r14, #0x10
it eq
vstmdbeq r0!, {s16-s31}
/* Save the core registers. */
stmdb r0!, {r4-r11, r14}
/* Save the new top of stack into the first member of the TCB. */
str r0, [r2]
stmdb sp!, {r0, r3}
mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY
msr basepri, r0
dsb
isb
bl vTaskSwitchContext
mov r0, #0
msr basepri, r0
ldmia sp!, {r0, r3}
/* The first item in pxCurrentTCB is the task top of stack. */
ldr r1, [r3]
ldr r0, [r1]
/* Pop the core registers. */
ldmia r0!, {r4-r11, r14}
/* Is the task using the FPU context? If so, pop the high vfp registers
too. */
tst r14, #0x10
it eq
vldmiaeq r0!, {s16-s31}
msr psp, r0
isb
#ifdef WORKAROUND_PMU_CM001 /* XMC4000 specific errata */
#if WORKAROUND_PMU_CM001 == 1
push { r14 }
pop { pc }
nop
#endif
#endif
bx r14
}