查遍了C站上所有关于FreeRTOS调度器的分析,发现大家分析完vTaskStartScheduler()之后就戛然而止了,我就会比较迷糊,这个仅开启了调度器的调度,而FreeRTOS是一个实时操作系统,并不能体现出他的实时性在哪里,虽然已经在FreeRTOSConfig.h中设置了configUSE_PREEMPTION等于1,那他是怎么其的作用呢?
projectdemoFreeRTOSConfig.h
刚开始对PendSV系统调用还比较陌生,读过很多的资料之后,下面是我的理解:
我们都知道,CPU的内部会有一个系统滴答,也就是SysTick,在我的系统里面设置的是1ms。系统滴答就相当于是CPU的心跳,注意,在每次的系统滴答时,都会产生一次中断,来判断是否有更高的优先级需要处理,如果有,就发生一次上下文切换。那么问题来了,如果每次都产生中断的话,那么如果CPU此时正好正在处理中断程序呢,处于系统的实时性问题,系统滴答同样会产生一次系统时钟的中断来判断是否需要发生一次调度。那么为了能使中断顺利且全部一次完成,就加入了PendSV系统调用。他的目的就是为了能够使中断产生时,避免被系统时钟中断打断产生调度而引入的。
而PendSV就是一次SWI系统调用。我们先看SWI系统调用的结果是什么,SWI系统中断定义在中断向量表中,
当系统发生SWI中断的时候,就会触发调用FreeRTOS_SWI_Handler函数,而FreeRTOS_SWI_Handler的处理过程如下:
FreeRTOS_SWI_Handler: portSAVE_ConTEXT LDR R0, vTaskSwitchContextConst BLX R0 portRESTORE_ConTEXT vTaskSwitchContextConst: .word vTaskSwitchContext
根据上面的流程,可以知道,
首先它会先保存当前任务的上下文,他会继续调用到vTaskSwitchContext()函数,他是一个C函数,也是调度的关键函数,经过vTaskSwitchContext()后,当前任务已经不是上一个任务而是切换到了优先级最高的新任务,最后,将此时当前新任务的栈pop出来,继续执行,完成任务的切换工作。
void vTaskSwitchContext(void)
{
...
taskCHECK_FOR_STACK_OVERFLOW();
taskSELECT_HIGHEST_PRIORITY_TASK();
traceTASK_SWITCHED_IN();
}
挑出了其中的重要函数,首先检查栈是否溢出,然后,找出系统中优先级最高的任务,FreeRTOS的实时性问题就体现在这个地方,将pxCurrentTCB赋值一个最高优先级的任务等待调度。注意此时并没有发生切换,只是在等待调度,这个就与上面为什么要引入PendSV对应了起来!
2. FreeRTOS任务切换的场合- 可以执行的一个系统调用
- 系统能够滴答定时器(SysTick)中断
首先需要明确的是,FreeRTOS对于不同的架构,其实现方式是不同的。基于STM32的处理过程是 中断控制及状态寄存器ICSR(地址:0xE000_ED04),向ICSR的第28位写入1悬起PendSV(启动PendSV中断),而ARM_CA9的处理方式是,利用系统调用触发一次异常,执行SWI指令。
STM32的处理方式很多文章中都有提到,而这篇文章选择了ARM_CA9的处理方式。
2.1 系统调用系统调用就是执行FreeRTOS系统提供的相关API函数,比如,任务切换的函数taskYIELD(),或是有些调用了taskYIELD()函数的API函数。
#define taskYIELD() portYIELD() #define portYIELD() __asm volatile ( "SWI 0" );
根据系统调用的过程可以看出来,触发了一次SWI中断,之后就会执行上面第一节的函数了。
2.2 系统滴答SysTick在main函数中,任务创建完以后,就要开启调度器开始调度了,在开始调度器的函数中,有对系统滴答的注册,如下:
baseType_t xPortStartScheduler(void)
{
...
configSETUP_TICK_INTERRUPT();
...
}
它会调用到tick的注册函数,而它是一个宏定义,展开后
#define configSETUP_TICK_INTERRUPT()
do
{
void SystemSetupSystick(uint32_t tickRateHz, void *tickHandler, uint32_t intPriority);
SystemSetupSystick(configTICK_RATE_HZ, (void *)FreeRTOS_Tick_Handler, configUNIQUE_INTERRUPT_PRIORITIES - 2);
} while (0)
SystemSetupSystick()是关键函数,它注册了系统滴答的中断函数,系统滴答的回调函数是FreeRTOS_Tick_Handler(),注册过程如下:
void SystemSetupSystick(uint32_t tickRateHz, void *tickHandler, uint32_t intPriority)
{
system_register_irqhandler(GPT1_IRQn, (system_irq_handler_t)(uint32_t)tickHandler, NULL);
...
}
tickHandler(),就是我们的关键函数,也就是上面的FreeRTOS_Tick_Handler(),当系统每1ms tick一次,就会触发一次该函数,
void FreeRTOS_Tick_Handler(void)
{
portCPU_IRQ_DISABLE();
portICCPMR_PRIORITY_MASK_REGISTER = (uint32_t)(configMAX_API_CALL_INTERRUPT_PRIORITY << portPRIORITY_SHIFT);
__asm volatile("dsb n"
"isb n");
portCPU_IRQ_ENABLE();
if (xTaskIncrementTick() != pdFALSE)
{
ulPortYieldRequired = pdTRUE;
}
portCLEAR_INTERRUPT_MASK();
configCLEAR_TICK_INTERRUPT();
}
这个函数很关键的函数就是xTaskIncrementTick(),更新系统的时间,并判断是否有任务到达了唤醒的时间,如果有就加入到就绪链表中,但最重要的任务是判断是否需要发生任务调度,它的返回值就是,若等于pdTRUE表示需要调度,否则不需要。这个很关键。
对STM32的处理方式是将中断控制及状态寄存器ICSR(地址:0xE000_ED04),向ICSR的第28位写入1悬起PendSV(启动PendSV中断),改变它的标志位,等待调度。而ARM_CA9则不大一样了,它只是附给一个全局变量ulPortYieldRequired的值为pdTRUE。但并没有任何调度的痕迹。
当时就很迷惑,搜索了很久才发现它的处理过程如下。
我们搜索ulPortYieldRequired,发现它会在portASM.S中调用到,
LDR r1, =ulPortYieldRequired
LDR r0, [r1]
CMP r0, #0
BNE switch_before_exit
switch_before_exit:
MOV r0, #0
STR r0, [r1]
POP {r0-r4, r12}
CPS #IRQ_MODE
POP {LR}
MSR SPSR_cxsf, LR
POP {LR}
portSAVE_ConTEXT
LDR R0, vTaskSwitchContextConst
BLX R0
portRESTORE_CONTEXT
首先判断ulPortYieldRequired是否等于0,如果不等于0就会执行下面的switch_before_exit,在switch_before_exit中,终于看到了第一节中分析的函数vTaskSwitchContextConst,找出优先级最高的任务等待调度。
而问题是,他是怎么执行的呢。
FreeRTOS_IRQ_Handler:
...
LDR r1, =system_irqhandler
BLX r1
POP {r0-r3, lr}
ADD sp, sp, r2
...
LDR r1, =ulPortYieldRequired
LDR r0, [r1]
CMP r0, #0
BNE switch_before_exit
...
看到这段代码应该就会明白了,我们的ulPortYieldRequired是在中断处理过程中得到的执行,也就是,当系统产生中断,中断执行完成之后,就会判断ulPortYieldRequired是否需要产生调度,如果需要就按第一节的过程进行处理。而这个中断恰好就是我们的系统时钟中断。
整体的过程就是,CPU每个tick就会产生一次中断,首先对系统时钟加一,并判断是否需要调度,如果需要产生调度,将ulPortYieldRequired赋值为pdTRUE。之后根据ulPortYieldRequired决定是否执行第一节中的代码。
最后的最后,此时的pxCurrentTCB已经指向了最新需要执行的任务,portSAVE_CONTEXT已经将上一个执行的任务的上下文保存了起来,portRESTORE_CONTEXT就是恢复任务的上下文,这个任务已经切换了,它就是需要执行的最新任务的上下文,当portRESTORE_CONTEXT执行完,就完成了上下文的切换了。
其实产生任务切换的函数有很多,因为FreeRTOS是一个实时操作系统,必须确保系统的实时性问题,调度是很关键的,当我们看到taskYIELD()的时候,就意味着需要调度,分析源代码时,可以关注一下这一点。



