FreeRTOS分析二—任务的启动
上一篇文章我们带着三个问题开始了对 FreeRTOS 代码的探究。
1. FreeRTOS 是如何建立任务的呢?
2. FreeRTOS 是调度和切换任务的呢?
3. FreeRTOS 是如何保证实时性呢?
并且在上一篇文章 FreeRTOS从代码层面进行原理分析(1 任务的建立) 中对任务的创建进行了分析。
这篇文章让我们一起对 FreeRTOS 是如何进行任务的调度和切换从代码的角度进行逻辑分析。
任务的调度
在 FreeRTOS 官方教程中,在创建完成任务之后只要调用 xTaskStartScheduler
函数就可以将创建的任务给执行起来了。
那么在 FreeRTOS 中任务究竟是如何调度起来的呢?现在让我们看一下 xTaskStartScheduler
这个函数的内部。
BaseType_t xPortStartScheduler( void )
{
...
/* Make PendSV and SysTick the lowest priority interrupts. */
portNVIC_SHPR3_REG |= portNVIC_PENDSV_PRI;
portNVIC_SHPR3_REG |= portNVIC_SYSTICK_PRI;
/* Start the timer that generates the tick ISR. Interrupts are disabled
* here already. */
vPortSetupTimerInterrupt();
/* Initialise the critical nesting count ready for the first task. */
uxCriticalNesting = 0;
/* Start the first task. */
prvPortStartFirstTask();
/* Should never get here as the tasks will now be executing! Call the task
* exit error function to prevent compiler warnings about a static function
* not being called in the case that the application writer overrides this
* functionality by defining configTASK_RETURN_ADDRESS. Call
* vTaskSwitchContext() so link time optimisation does not remove the
* symbol. */
vTaskSwitchContext();
prvTaskExitError();
/* Should not get here! */
return 0;
}
配置 SysTick
经过省略,上面的代码中的逻辑已经非常清晰的展现在我们的面前了。先是调整 PendSV & SysTick 的中断优先级。
然后调用 vPortSetupTimerInterrupt
设置 SysTick 中断,对应 FreeRTOS 里面就是设置任务的切换时间颗粒度。这个函数里面的代码也是很简单的。
__attribute__( ( weak ) ) void vPortSetupTimerInterrupt( void )
{
...
/* Stop and clear the SysTick. */
portNVIC_SYSTICK_CTRL_REG = 0UL;
portNVIC_SYSTICK_CURRENT_VALUE_REG = 0UL;
/* Configure SysTick to interrupt at the requested rate. */
portNVIC_SYSTICK_LOAD_REG = ( configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ ) - 1UL;
portNVIC_SYSTICK_CTRL_REG = ( portNVIC_SYSTICK_CLK_BIT_CONFIG | portNVIC_SYSTICK_INT_BIT | portNVIC_SYSTICK_ENABLE_BIT );
}
其中两个关于 SysTick 寄存器定义如下
#define portNVIC_SYSTICK_CTRL_REG ( *( ( volatile uint32_t * ) 0xe000e010 ) )
#define portNVIC_SYSTICK_LOAD_REG ( *( ( volatile uint32_t * ) 0xe000e014 ) )
在 cortex-m3 中, 0xe000e014 地址对应的是 系统滴答定时器重载值寄存器(Systick Reload Value Register)。
该寄存器类似一个倒计时的感觉,产生一次系统时钟就在这个寄存器上减少1,这个寄存器中的数值就是这个倒计时的初始值。当倒数结束时可以产生系统滴答定时器中断。
需要注意的是,如果是多次循环倒数的话,时钟脉冲就会多一次,如果需要 100 的初始值,那么实际设置 SRVR 寄存器的时候就要设置为 99 ( 所以代码中才需要减 1UL )。如果是单次倒数的话,那么就按实际情况进行设置就可以了,不用减 1 。
0xe000e010 中断类型控制寄存器(Interrupt Controller Type Register)。设置的内容在代码的注释中也说的比较清楚了,下面直接上图。
所以这里的代码翻译过来就是将 SysTick重新加载值寄存器 设置好后,当寄存器里面的值减到 0 时,就会引发中断。这个中断后面就会用来对 FreeRTOS 中的任务进行切换。切换的代码在后面的章节进行分析。
现在先继续分析后面的代码。
启动第一个任务
prvPortStartFirstTask
这个函数也是根据不同的单片机架构进行实现的。 在 FreeRTOS\Source\portable\GCC\ARM_CM3\port.c
中可以找到关于 Cortex-m3 的实现。
static void prvPortStartFirstTask( void )
{
__asm volatile (
" ldr r0, =0xE000ED08 \n"/* Use the NVIC offset register to locate the stack. */
" ldr r0, [r0] \n"
" ldr r0, [r0] \n"
" msr msp, r0 \n"/* Set the msp back to the start of the stack. */
" cpsie i \n"/* Globally enable interrupts. */
" cpsie f \n"
" dsb \n"
" isb \n"
" svc 0 \n"/* System call to start first task. */
" nop \n"
" .ltorg \n"
);
}
可以看出这里就是使用汇编代码进行实现的了,代码中的注释比较多,非常有助于我们对逻辑的理解。
0xE000ED08
是向量表偏移寄存器(Vector Table Offset Register),这个寄存器是用于记录 中断向量表的存储位置和其地址的偏移。
在汇编代码中将中断向量表的的起始地址读出来,写入到 MSP 中,用于在 handler 模式下。
这里稍微补充一下 Cortex-m3 中的 SP 寄存器分为 MSP 和 PSP,MSP 用于在 handler 模式下, PSP 用于在 thread 模式下。这样对实现 OS 的各个进程/线程 的多个堆栈是十分方便的办法。详细可以看这篇优秀的博客中的介绍。
后面的 cpsie i & f 注释中已经说的比较清晰了就是 中断的开启。 再往下的 dsb & isb 是设置内存屏障,在这里面作为刷新流水线的作用。
重点介绍 SVC 0 这一行汇编代码。
SVC 指令会引发 SVC 异常,在 STM32F10x_Startup.s
文件中有中断向量表,其顺序对应 STM32 参考手册中的中断和异常向量。代码和文档都太长了,所以就各截取了部分。
从上面途中可以看出 SVC 异常对应的就是 SCVall 对应到代码中就是调用 vPortSVCHandler
函数。让我们继续看这个函数。
void vPortSVCHandler( void )
{
__asm volatile (
" ldr r3, pxCurrentTCBConst2 \n"/* Restore the context. */
" ldr r1, [r3] \n"/* Use pxCurrentTCBConst to get the pxCurrentTCB address. */
" ldr r0, [r1] \n"/* The first item in pxCurrentTCB is the task top of stack. */
" ldmia r0!, {r4-r11} \n"/* Pop the registers that are not automatically saved on exception entry and the critical nesting count. */
" msr psp, r0 \n"/* Restore the task stack pointer. */
" isb \n"
" mov r0, #0 \n"
" msr basepri, r0 \n"
" orr r14, #0xd \n"
" bx r14 \n"
" \n"
" .align 4 \n"
"pxCurrentTCBConst2: .word pxCurrentTCB \n"
);
}
这里前三行汇编代码表明, r0 中存入了当前 TCB(Task Control Blank, 前面提到过可以利用这个结构索引到任务的栈空间) 的栈定位置的内容。
使用 msr 指令将 r0 的内容存到 PSP 中。关于 PSP 前面有介绍,要是忘记的话这里可直接理解为线程堆栈的指针。
isb 内存屏障,因为本文的主旨是探究 FreeRTOS 是如何对任务进行调度和切换的,所以 这里就暂时不深究为什么 prvPortStartFirstTask
函数使用了 dsb & isb 两个指令,而 vPortSVCHandler
函数只使用了 isb 这一个指令。好奇的读者也可以自行研究一下,欢迎把查到的资料放到评论区哦。
msr basepri, r0
对于这行汇编也十分的好懂,我就直接把 basepri 的介绍放在下面的图片中了。
切换上下文到任务中
" orr r14, #0xd \n"
" bx r14 \n"
看到了这两行代码之后大家可能立马就出现了了个疑问, 我知道 R14 代表的是 LR 寄存器,里面一般存的都是函数或者异常的返回地址。
那么新的任务的运行地址是什么时候如何操作才能把它存到 R14 中呢?
实际情况并不是这样的,如果从 handler 模式中返回有三种情况可以使得将 EXC_RETURN 载入 PC 寄存器中。
- 使用 POP 指令从 PC 中进行弹栈操作
- 使用 BX 指令跳转到任意寄存器
- 使用 LDM 或者 LDR 指令给 PC 指定地址
实际上汇编代码正是使用 BX 指令。而 EXC_RETURN[3:0] bits 是可以指定接下来的 SP 是使用 MSP 还是 PSP。
到这里我们已经知道了,BX r14
会返回 thread mode 并将 SP 设置为 PSP。
但是这样还是不够的,要如何能跳转到正确的任务呢?
其实 《Cortex-m3 技术参考手册》上还介绍了,当处理器陷于异常的时候会自动对8个寄存器进行压栈,而从中断返回时又会对这 8 个寄存器自动进行出栈操作。
到这里是不是对前一篇文章 FreeRTOS从代码层面进行原理分析(1 任务的建立) 中 xTaskCreate
函数在创建任务的时候 在 port.c 文件中 Cortex-m3 是怎么对栈进行初始初始化的嘛~ 帮你回忆下~
StackType_t * pxPortInitialiseStack( StackType_t * pxTopOfStack,
TaskFunction_t pxCode,
void * pvParameters )
{
/* Simulate the stack frame as it would be created by a context switch
* interrupt. */
pxTopOfStack--; /* Offset added to account for the way the MCU uses the stack on entry/exit of interrupts. */
*pxTopOfStack = portINITIAL_XPSR; /* xPSR */
pxTopOfStack--;
*pxTopOfStack = ( ( StackType_t ) pxCode ) & portSTART_ADDRESS_MASK; /* PC */
pxTopOfStack--;
*pxTopOfStack = ( StackType_t ) portTASK_RETURN_ADDRESS; /* LR */
pxTopOfStack -= 5; /* R12, R3, R2 and R1. */
*pxTopOfStack = ( StackType_t ) pvParameters; /* R0 */
pxTopOfStack -= 8; /* R11, R10, R9, R8, R7, R6, R5 and R4. */
return pxTopOfStack;
}
hhhh~ 是不是一下子就明白了,这些就是处理器自动帮助出入栈的寄存器。 其他寄存器 R4 ~ R11 单独也留了位置。这些设计好的位置会保存所有的寄存器免遭破坏,以达到恢复之前中断任务的作用(后面任务切换的时候会再提)。
再回头看 vPortSVCHandler
函数是不是就逻辑很清晰了~ 把 PSP 指向当前任务的堆栈。再设置通过 R14 设置 EXC_RETURN,将进入 thread 模式,SP 使用 PSP。 再通过 Cortex-m3 的自动将这 8 个寄存器弹栈的功能,完成切换当前执行任务的功能。
这篇写的也有点长了,任务的切换写到下一篇 FreeRTOS从代码层面进行原理分析(3 任务的切换)