3.任务调度器
3.1.vTaskStartScheduler()
这个函数的功能就是开启任务调度器的,这个函数在文件 tasks.c
中有定义
1973 /*-----------------------------------------------------------*/
1974
1975 void vTaskStartScheduler( void )
1976 {
1977 BaseType_t xReturn;
1978
1979 /* Add the idle task at the lowest priority. */
/*创建空闲任务,如果使用静态内存的话使用函数 xTaskCreateStatic()来创建空闲任务,
*优先级为 tskIDLE_PRIORITY,宏 tskIDLE_PRIORITY 为 0,也就是说空闲任务的优先级为最
*低。*/
1980 #if ( configSUPPORT_STATIC_ALLOCATION == 1 )
1981 {
1982 StaticTask_t * pxIdleTaskTCBBuffer = NULL;
1983 StackType_t * pxIdleTaskStackBuffer = NULL;
1984 uint32_t ulIdleTaskStackSize;
1985
1986 /* The Idle task is created using user provided RAM - obtain the
1987 * address of the RAM then create the idle task. */
1988 vApplicationGetIdleTaskMemory( &pxIdleTaskTCBBuffer, &pxIdleTaskStackBuffer, &ulIdleTaskStackSize );
1989 xIdleTaskHandle = xTaskCreateStatic( prvIdleTask,
1990 configIDLE_TASK_NAME,
1991 ulIdleTaskStackSize,
1992 ( void * ) NULL, /*lint !e961. The cast is not redundant for all compilers. */
1993 portPRIVILEGE_BIT, /* In effect ( tskIDLE_PRIORITY | portPRIVILEGE_BIT ), but tskIDLE_PRIORITY is zero. */
1994 pxIdleTaskStackBuffer,
1995 pxIdleTaskTCBBuffer ); /*lint !e961 MISRA exception, justified as it is not a redundant explicit cast to all supported compilers. */
1996
1997 if( xIdleTaskHandle != NULL )
1998 {
1999 xReturn = pdPASS;
2000 }
2001 else
2002 {
2003 xReturn = pdFAIL;
2004 }
2005 }
2006 #else /* if ( configSUPPORT_STATIC_ALLOCATION == 1 ) */
2007 {
2008 /* The Idle task is being created using dynamically allocated RAM. */
2009 xReturn = xTaskCreate( prvIdleTask,
2010 configIDLE_TASK_NAME,
2011 configMINIMAL_STACK_SIZE,
2012 ( void * ) NULL,
2013 portPRIVILEGE_BIT, /* In effect ( tskIDLE_PRIORITY | portPRIVILEGE_BIT ), but tskIDLE_PRIORITY is zero. */
2014 &xIdleTaskHandle ); /*lint !e961 MISRA exception, justified as it is not a redundant explicit cast to all supported compilers. */
2015 }
2016 #endif /* configSUPPORT_STATIC_ALLOCATION */
2017
2018 #if ( configUSE_TIMERS == 1 )
2019 {
2020 if( xReturn == pdPASS )
2021 {
2022 xReturn = xTimerCreateTimerTask();
/*如果使用软件定时器的话还需要通过函数 xTimerCreateTimerTask()来创建定时器服务
*任务。定时器服务任务的具体创建过程是在函数 xTimerCreateTimerTask()中完成的*/
2023 }
2024 else
2025 {
2026 mtCOVERAGE_TEST_MARKER();
2027 }
2028 }
2029 #endif /* configUSE_TIMERS */
2030
2031 if( xReturn == pdPASS )
/*空闲任务和定时器任务创建成功*/
2032 {
2033 /* freertos_tasks_c_additions_init() should only be called if the user
2034 * definable macro FREERTOS_TASKS_C_ADDITIONS_INIT() is defined, as that is
2035 * the only macro called by the function. */
2036 #ifdef FREERTOS_TASKS_C_ADDITIONS_INIT
2037 {
2038 freertos_tasks_c_additions_init();
2039 }
2040 #endif
2041
2042 /* Interrupts are turned off here, to ensure a tick does not occur
2043 * before or during the call to xPortStartScheduler(). The stacks of
2044 * the created tasks contain a status word with interrupts switched on
2045 * so interrupts will automatically get re-enabled when the first task
2046 * starts to run. */
2047 portDISABLE_INTERRUPTS();
/*关闭中断 在 SVC 中断服务函数 vPortSVCHandler()中会打开中断*/
2048
2049 #if ( configUSE_NEWLIB_REENTRANT == 1 )
2050 {
2051 /* Switch Newlib's _impure_ptr variable to point to the _reent
2052 * structure specific to the task that will run first.
2053 * See the third party link http://www.nadler.com/embedded/newlibAndFreeRTOS.html
2054 * for additional information. */
2055 _impure_ptr = &( pxCurrentTCB->xNewLib_reent );
2056 }
2057 #endif /* configUSE_NEWLIB_REENTRANT */
2058
2059 xNextTaskUnblockTime = portMAX_DELAY;
2060 xSchedulerRunning = pdTRUE;
/*变量 xSchedulerRunning 设置为 pdTRUE,表示调度器开始运行*/
2061 xTickCount = ( TickType_t ) configINITIAL_TICK_COUNT;
2062
2063 /* If configGENERATE_RUN_TIME_STATS is defined then the following
2064 * macro must be defined to configure the timer/counter used to generate
2065 * the run time counter time base. NOTE: If configGENERATE_RUN_TIME_STATS
2066 * is set to 0 and the following line fails to build then ensure you do not
2067 * have portCONFIGURE_TIMER_FOR_RUN_TIME_STATS() defined in your
2068 * FreeRTOSConfig.h file. */
2069 portCONFIGURE_TIMER_FOR_RUN_TIME_STATS();
/*当宏 configGENERATE_RUN_TIME_STATS 为 1 的时候说明使能时间统计功能,此时
*需要用户实现宏 portCONFIGURE_TIMER_FOR_RUN_TIME_STATS,此宏用来配置一个定时器/计数器*/
2070
2071 traceTASK_SWITCHED_IN();
2072
2073 /* Setting up the timer tick is hardware specific and thus in the
2074 * portable interface. */
2075 if( xPortStartScheduler() != pdFALSE )
/*调用函数 xPortStartScheduler()来初始化跟调度器启动有关的硬件,比如滴答定时器、
*FPU 单元和 PendSV 中断等等*/
2076 {
2077 /* Should not reach here as if the scheduler is running the
2078 * function will not return. */
/*如果调度器启动成功的话就不会运行到这里,函数不会有返回值的*/
2079 }
2080 else
2081 {
2082 /* Should only reach here if a task calls xTaskEndScheduler(). */
2083 }
2084 }
2085 else
2086 {
2087 /* This line will only be reached if the kernel could not be started,
2088 * because there was not enough FreeRTOS heap to create the idle task
2089 * or the timer task. */
2090 configASSERT( xReturn != errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY );
/*程序运行到这里只能说明一点,那就是系统内核没有启动成功,导致的原因是在创建
空闲任务或者定时器任务的时候没有足够的内存*/
2091 }
2092
2093 /* Prevent compiler warnings if INCLUDE_xTaskGetIdleTaskHandle is set to 0,
2094 * meaning xIdleTaskHandle is not used anywhere else. */
2095 ( void ) xIdleTaskHandle;
2096 }
2097 /*-----------------------------------------------------------*/
3.2 xPortStartScheduler()
FreeRTOS 系统时钟是由滴答定时器来提供的,而且任务切换也会用到 PendSV 中断,这些硬件的初始化由函数 xPortStartScheduler()来完成.
307 /*-----------------------------------------------------------*/
308
309 /*
310 * See header file for description.
311 */
312 BaseType_t xPortStartScheduler( void )
313 {
314 /* configMAX_SYSCALL_INTERRUPT_PRIORITY must not be set to 0.
315 * See https://www.FreeRTOS.org/RTOS-Cortex-M3-M4.html */
316 configASSERT( configMAX_SYSCALL_INTERRUPT_PRIORITY );
317
318 /* This port can be used on all revisions of the Cortex-M7 core other than
319 * the r0p1 parts. r0p1 parts should use the port from the
320 * /source/portable/GCC/ARM_CM7/r0p1 directory. */
321 configASSERT( portCPUID != portCORTEX_M7_r0p1_ID );
322 configASSERT( portCPUID != portCORTEX_M7_r0p0_ID );
323
324 #if ( configASSERT_DEFINED == 1 )
325 {
326 volatile uint32_t ulOriginalPriority;
327 volatile uint8_t * const pucFirstUserPriorityRegister = ( uint8_t * ) ( portNVIC_IP_REGISTERS_OFFSET_16 + portFIRST_USER_INTERRUPT_NUMBER );
328 volatile uint8_t ucMaxPriorityValue;
329
330 /* Determine the maximum priority from which ISR safe FreeRTOS API
331 * functions can be called. ISR safe functions are those that end in
332 * "FromISR". FreeRTOS maintains separate thread and ISR API functions to
333 * ensure interrupt entry is as fast and simple as possible.
334 *
335 * Save the interrupt priority value that is about to be clobbered. */
336 ulOriginalPriority = *pucFirstUserPriorityRegister;
337
338 /* Determine the number of priority bits available. First write to all
339 * possible bits. */
340 *pucFirstUserPriorityRegister = portMAX_8_BIT_VALUE;
341
342 /* Read the value back to see how many bits stuck. */
343 ucMaxPriorityValue = *pucFirstUserPriorityRegister;
344
345 /* The kernel interrupt priority should be set to the lowest
346 * priority. */
347 configASSERT( ucMaxPriorityValue == ( configKERNEL_INTERRUPT_PRIORITY & ucMaxPriorityValue ) );
348
349 /* Use the same mask on the maximum system call priority. */
350 ucMaxSysCallPriority = configMAX_SYSCALL_INTERRUPT_PRIORITY & ucMaxPriorityValue;
351
352 /* Calculate the maximum acceptable priority group value for the number
353 * of bits read back. */
354 ulMaxPRIGROUPValue = portMAX_PRIGROUP_BITS;
355
356 while( ( ucMaxPriorityValue & portTOP_BIT_OF_BYTE ) == portTOP_BIT_OF_BYTE )
357 {
358 ulMaxPRIGROUPValue--;
359 ucMaxPriorityValue <<= ( uint8_t ) 0x01;
360 }
361
362 #ifdef __NVIC_PRIO_BITS
363 {
364 /* Check the CMSIS configuration that defines the number of
365 * priority bits matches the number of priority bits actually queried
366 * from the hardware. */
367 configASSERT( ( portMAX_PRIGROUP_BITS - ulMaxPRIGROUPValue ) == __NVIC_PRIO_BITS );
368 }
369 #endif
370
371 #ifdef configPRIO_BITS
372 {
373 /* Check the FreeRTOS configuration that defines the number of
374 * priority bits matches the number of priority bits actually queried
375 * from the hardware. */
376 configASSERT( ( portMAX_PRIGROUP_BITS - ulMaxPRIGROUPValue ) == configPRIO_BITS );
377 }
378 #endif
379
380 /* Shift the priority group value back to its position within the AIRCR
381 * register. */
382 ulMaxPRIGROUPValue <<= portPRIGROUP_SHIFT;
383 ulMaxPRIGROUPValue &= portPRIORITY_GROUP_MASK;
384
385 /* Restore the clobbered interrupt priority register to its original
386 * value. */
387 *pucFirstUserPriorityRegister = ulOriginalPriority;
388 }
389 #endif /* conifgASSERT_DEFINED */
390
391 /* Make PendSV and SysTick the lowest priority interrupts. */
392 portNVIC_SHPR3_REG |= portNVIC_PENDSV_PRI;
/*设置 PendSV 的中断优先级,为最低优先级*/
393 portNVIC_SHPR3_REG |= portNVIC_SYSTICK_PRI;
/*设置滴答定时器的中断优先级,为最低优先级*/
394
395 /* Start the timer that generates the tick ISR. Interrupts are disabled
396 * here already. */
397 vPortSetupTimerInterrupt();
/*调用函数 vPortSetupTimerInterrupt()来设置滴答定时器的定时周期,并且使能滴答定时器的中断*/
398
399 /* Initialise the critical nesting count ready for the first task. */
400 uxCriticalNesting = 0;
/*初始化临界区嵌套计数器*/
401
402 /* Ensure the VFP is enabled - it should be anyway. */
403 prvEnableVFP();
/*调用函数 prvEnableVFP()使能 FPU*/
404
405 /* Lazy save always. */
406 *( portFPCCR ) |= portASPEN_AND_LSPEN_BITS;
407
408 /* Start the first task. */
409 prvStartFirstTask();
410
411 /* Should not get here! */
412 return 0;
413 }
414 /*-----------------------------------------------------------*/
3.3 prvEnableVFP()
在函数 xPortStartScheduler()中会通过调用 prvEnableVFP()来使能 FPU,这个函数是汇编形
式的,在文件 port.c 中有定义。
289 /*-----------------------------------------------------------*/
290
291 __asm void prvEnableVFP( void )
292 {
293 /* *INDENT-OFF* */
294 PRESERVE8
295
296 /* The FPU enable bits are in the CPACR. */
297 ldr.w r0, =0xE000ED88 (1)
298 ldr r1, [ r0 ] (2)
299
300 /* Enable CP10 and CP11 coprocessors, then save back. */
301 orr r1, r1, #( 0xf << 20 ) (3)
302 str r1, [ r0 ] (4)
303 bx r14 (5)
304 nop
305 /* *INDENT-ON* */
306 }
307 /*-----------------------------------------------------------*/
(1)、利用寄存器 CPACR 可以使能或禁止 FPU,此寄存器的地址为 0XE000ED88,此寄存器的 CP10(bit20 和 bit21)和 CP11(bit22和 bit23)用于控制 FPU。这 4 个 bit 的具体含义请参考《权威指南》,通常将这 4 个 bit 都设置为1 来开启 FPU,表示全访问。此行代码将地址 0XE000ED88 保存在寄存器 R0 中。
(2)、读取 R0 中保存的存储地址处的数据,也就是 CPACR 寄存器的值,并将结果保存在R1 寄存器中。
(3)、R1 中的值与(0xf<<20)进行按位或运算,也就是 R1=R1|0X00F00000。此时 R1 所保存的值的 bit20~bit23 就都为 1 了,将这个值写入寄存器 CPACR 中就可开启 FPU。
(4)、将 R1 中的值写入 R0 中保存的地址处,也就是寄存器 CPACR 中。
(5)、函数返回。bx 为间接跳转指令,一般为 BX <Rm>,也就是跳转到存放在 Rm 中的地址处,此处是跳转到 R14 存放的地址处。R14 寄存器也叫做链接寄存(LR),也可以用 LR 表示。
3.4 prvStartFirstTask()
启动第一个任务
258 /*-----------------------------------------------------------*/
259
260 __asm void prvStartFirstTask( void )
261 {
262 /* *INDENT-OFF* */
263 PRESERVE8
264
265 /* Use the NVIC offset register to locate the stack. */
266 ldr r0, =0xE000ED08 ;R0=0XE000ED08 (1)
267 ldr r0, [ r0 ] ;取 R0 所保存的地址处的值赋给 R0 (2)
268 ldr r0, [ r0 ] ;获取 MSP 初始值 (3)
269 /* Set the msp back to the start of the stack. */
270 msr msp, r0 ;复位 MSP (4)
271
272 /* Clear the bit that indicates the FPU is in use in case the FPU was used
273 * before the scheduler was started - which would otherwise result in the
274 * unnecessary leaving of space in the SVC stack for lazy saving of FPU
275 * registers. */
276 mov r0, #0
277 msr control, r0
278 /* Globally enable interrupts. */
279 cpsie i ;使能中断(清除 PRIMASK) (5)
280 cpsie f ;使能中断(清除 FAULTMASK) (6)
281 dsb ;数据同步屏障 (7)
282 isb ;指令同步屏障 (8)
283 /* Call SVC to start the first task. */
284 svc 0 ;触发 SVC 中断(异常) (9)
285 nop
286 nop
287 /* *INDENT-ON* */
288 }
289 /*-----------------------------------------------------------*/
(1)、将 0XE000ED08 保存在寄存器 R0 中。一般来说向量表应该是从起始地址(0X00000000)开始存储的,不过,有些应用可能需要在运行时修改或重定义向量表,Cortex-M 处理器为此提供了一个叫做向量表重定位的特性。向量表重定位特性提供了一个名为向量表偏移寄存器(VTOR)的可编程寄存器。VTOR 寄存器的地址就是 0XE000ED08,通过这个寄存器可以重新定义向量表,比如在 STM32F429 的 ST 官方库中会通过函数 SystemInit()来设置 VTOR 寄存器,
代码如下:
SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET; //VTOR=0x08000000+0X00
通过上面一行代码就将向量表开始地址重新定义到了 0X08000000,向量表的起始地址存储的就是 MSP 初始值。
(2)、读取 R0 中存储的地址处的数据并将其保存在 R0 寄存器,也就是读取寄存器 VTOR中的值,并将其保存在 R0 寄存器中。这一行代码执行完就以后 R0 的值应该为 0X08000000。
(3)、读取 R0 中存储的地址处的数据并将其保存在 R0 寄存器,也就是读取地址 0X08000000处存储的数据,并将其保存在 R0 寄存器中。我们知道向量表的起始地址保存的就是主栈指针MSP 的初始值,这一行代码执行完以后寄存器 R0 就存储 MSP 的初始值。现在来看(1)、(2)、
(3)这三步起始就是为了获取 MSP 的初始值而已!
(4)、复位 MSP,R0 中保存了 MSP 的初始值,将其赋值给 MSP 就相当于复位 MSP。
(5)和(6)、使能中断。
(7)和(8)、数据同步和指令同步屏障。
(9),调用 SVC 指令触发 SVC 中断,SVC 也叫做请求管理调用,SVC 和 PendSV 异常对于OS 的设计来说非常重要。SVC 异常由 SVC 指令触发。在 FreeRTOS 中仅仅使用 SVC 异常来启动第一个任务,后面的程序中就再也用不到 SVC 了。
3.5SVC中断服务函数
在函数 prvStartFirstTask()中通过调用 SVC 指令触发了 SVC 中断,而第一个任务的启动就是在 SVC 中断服务函数中完成的,SVC 中断服务函数应该为 SVC_Handler(),但是FreeRTOSConfig.h 中通过#define 的方式重新定义为了 xPortPendSVHandler(),如下:
#define xPortPendSVHandler PendSV_Handler
函数 vPortSVCHandler()在文件 port.c 中定义,这个函数也是用汇编写的,函数源码如下:
238 /*-----------------------------------------------------------*/
239
240 __asm void vPortSVCHandler( void )
241 {
242 /* *INDENT-OFF* */
243 PRESERVE8
244
245 /* Get the location of the current TCB. */
246 ldr r3, = pxCurrentTCB ;R3=pxCurrentTCB 的地址 (1)
247 ldr r1, [ r3 ] ;取 R3 所保存的地址处的值赋给 R1 (2)
248 ldr r0, [ r1 ] ;取 R1 所保存的地址处的值赋给 R0 (3)
249 /* Pop the core registers. */
250 ldmia r0 !, {r4-r11,r14} ;出栈 ,R4~R11 和 R14 (4)
251 msr psp, r0 ;进程栈指针 PSP 设置为任务的堆栈 (5)
252 isb ;指令同步屏障
253 mov r0, # 0 ;R0=0 (6)
254 msr basepri, r0 ;寄存器 basepri=0,开启中断 (7)
255 bx r14 ;(8)
256 /* *INDENT-ON* */
257 }
258 /*-----------------------------------------------------------*/
(1)、获取 pxCurrentTCB 指针的存储地址,pxCurrentTCB 是一个指向 TCB_t 的指针,这个指针永远指向正在运行的任务。
(2)、取 R3 所保存的地址处的值赋给 R1。通过这一步就获取到了当前任务的任务控制块的存储地址。
(3)、取 R3 所保存的地址处的值赋给 R0,我们知道任务控制块的第一个字段就是任务堆栈的栈顶指针 pxTopOfStack 所指向的位置,所以读取任务控制块所在的首地址(0X20001058)得到的就是栈顶指针所指向的地址。
可以看出(1)、(2)和(3)的目的就是获取要切换到的这个任务的任务栈顶指针,因为任务所对应的寄存器值,也就是现场都保存在任务的任务堆栈中,所以需要获取栈顶指针来恢复这些寄存器值!
(5)、设置进程栈指针 PSP
(6)、设置寄存器 R0 为 0。
(7)、设置寄存器 BASEPRI 为 R0,也就是 0,打开中断!
(8)、执行此行代码以后硬件自动恢复寄存器 R0~R3、R12、LR、PC 和 xPSR 的值,堆栈使用进程栈 PSP,然后执行寄存器 PC 中保存的任务函数。至此,FreeRTOS 的任务调度器正式开始运行!
任务切换
FreeRTOS 任务切换场合
● 可以执行一个系统调用
● 系统滴答定时器(SysTick)中断
执行系统调用
执行系统调用就是执行FreeRTOS系统提供的相关API函数,比如任务切换函数taskYIELD(),FreeRTOS 有些 API 函数也会调用函数 taskYIELD(),这些 API 函数都会导致任务切换,这些 API 函数和任务切换函数 taskYIELD()都统称为系统调用。函数 taskYIELD()其实就是个宏,在文件 task.h中有如下定义:
181 #define taskYIELD() portYIELD()
函数 portYIELD()也是个宏,在文件 portmacro.h 中有如下定义:
82 /*-----------------------------------------------------------*/
83
84 /* Scheduler utilities. */
85 #define portYIELD() \
86 { \
87 /* Set a PendSV to request a context switch. */ \
88 portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; \
/*通过向中断控制和壮态寄存器 ICSR 的 bit28 写入 1 挂起 PendSV 来启动 PendSV 中断。
*这样就可以在 PendSV 中断服务函数中进行任务切换了。*/
89 \
90 /* Barriers are normally not required but do ensure the code is completely \
91 * within the specified behaviour for the architecture. */ \
92 __dsb( portSY_FULL_READ_WRITE ); \
93 __isb( portSY_FULL_READ_WRITE ); \
94 }
95 /*-----------------------------------------------------------*/
系统滴答定时器(SysTick)
FreeRTOS 中滴答定时器(SysTick)中断服务函数中也会进行任务切换,滴答定时器中断服务函数如下:
void SysTick_Handler(void)
{
if(xTaskGetSchedulerState()!=taskSCHEDULER_NOT_STARTED)//系统已经运行
{
xPortSysTickHandler();
}
HAL_IncTick();
}
在滴答定时器中断服务函数中调用了 FreeRTOS 的 API 函数 xPortSysTickHandler(),此函数源码如下:
515 /*-----------------------------------------------------------*/
516
517 void xPortSysTickHandler( void )
518 {
519 /* The SysTick runs at the lowest interrupt priority, so when this interrupt
520 * executes all interrupts must be unmasked. There is therefore no need to
521 * save and then restore the interrupt mask value as its value is already
522 * known - therefore the slightly faster vPortRaiseBASEPRI() function is used
523 * in place of portSET_INTERRUPT_MASK_FROM_ISR(). */
524 vPortRaiseBASEPRI(); /*关闭中断*/
525 {
526 /* Increment the RTOS tick. */
527 if( xTaskIncrementTick() != pdFALSE )
528 {
529 /* A context switch is required. Context switching is performed in
530 * the PendSV interrupt. Pend the PendSV interrupt. */
531 portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
/*通过向中断控制和壮态寄存器 ICSR 的 bit28 写入 1 挂起 PendSV 来启动 PendSV 中
*断。这样就可以在 PendSV 中断服务函数中进行任务切换了。*/
532 }
533 }
534
535 vPortClearBASEPRIFromISR();/*打开中断*/
536 }
537 /*-----------------------------------------------------------*/
PendSV中断服务函数
前面说了 FreeRTOS 任务切换的具体过程是在 PendSV 中断服务函数中完成的,本节我们就来学习一个 PendSV 的中断服务函数,看看任务切换过程究竟是怎么进行的。PendSV 中断服务函数本应该为 PendSV_Handler(),但是 FreeRTOS 使用#define 重定义了,如下:
#define xPortPendSVHandler PendSV_Handler
函数 xPortPendSVHandler()源码如下:
451 /*-----------------------------------------------------------*/
452
453 __asm void xPortPendSVHandler( void )
454 {
455 extern uxCriticalNesting;
456 extern pxCurrentTCB;
457 extern vTaskSwitchContext;
458
459 /* *INDENT-OFF* */
460 PRESERVE8
461
462 mrs r0, psp
463 isb
464 /* Get the location of the current TCB. */
465 ldr r3, =pxCurrentTCB
466 ldr r2, [ r3 ]
467
468 /* Is the task using the FPU context? If so, push high vfp registers. */
469 tst r14, #0x10
470 it eq
471 vstmdbeq r0!, {s16-s31}
472
473 /* Save the core registers. */
474 stmdb r0!, {r4-r11, r14}
475
476 /* Save the new top of stack into the first member of the TCB. */
477 str r0, [ r2 ]
478
479 stmdb sp!, {r0, r3}
480 mov r0, # configMAX_SYSCALL_INTERRUPT_PRIORITY
481 msr basepri, r0
482 dsb
483 isb
484 bl vTaskSwitchContext
485 mov r0, # 0
486 msr basepri, r0
487 ldmia sp!, {r0, r3}
488
489 /* The first item in pxCurrentTCB is the task top of stack. */
490 ldr r1, [ r3 ]
491 ldr r0, [ r1 ]
492
493 /* Pop the core registers. */
494 ldmia r0!, {r4-r11, r14}
495
496 /* Is the task using the FPU context? If so, pop the high vfp registers
497 * too. */
498 tst r14, # 0x10
499 it eq
500 vldmiaeq r0!, {s16-s31}
501
502 msr psp, r0
503 isb
504 #ifdef WORKAROUND_PMU_CM001 /* XMC4000 specific errata */
505 #if WORKAROUND_PMU_CM001 == 1
506 push { r14 }
507 pop { pc }
508 nop
509 #endif
510 #endif
511
512 bx r14
513 /* *INDENT-ON* */
514 }
515 /*-----------------------------------------------------------*/
查找下一个要运行的任务
在 PendSV 中断服务程序中有调用函数 vTaskSwitchContext()来获取下一个要运行的任务,也就是查找已经就绪了的优先级最高的任务,函数源码如下:
2992 /*-----------------------------------------------------------*/
2993
2994 void vTaskSwitchContext( void )
2995 {
2996 if( uxSchedulerSuspended != ( UBaseType_t ) pdFALSE )
/*如果调度器挂起那就不能进行任务切换*/
2997 {
2998 /* The scheduler is currently suspended - do not allow a context
2999 * switch. */
3000 xYieldPending = pdTRUE;
3001 }
3002 else
3003 {
3004 xYieldPending = pdFALSE;
3005 traceTASK_SWITCHED_OUT();
3006
3007 #if ( configGENERATE_RUN_TIME_STATS == 1 )
3008 {
3009 #ifdef portALT_GET_RUN_TIME_COUNTER_VALUE
3010 portALT_GET_RUN_TIME_COUNTER_VALUE( ulTotalRunTime );
3011 #else
3012 ulTotalRunTime = portGET_RUN_TIME_COUNTER_VALUE();
3013 #endif
3014
3015 /* Add the amount of time the task has been running to the
3016 * accumulated time so far. The time the task started running was
3017 * stored in ulTaskSwitchedInTime. Note that there is no overflow
3018 * protection here so count values are only valid until the timer
3019 * overflows. The guard against negative values is to protect
3020 * against suspect run time stat counter implementations - which
3021 * are provided by the application, not the kernel. */
3022 if( ulTotalRunTime > ulTaskSwitchedInTime )
3023 {
3024 pxCurrentTCB->ulRunTimeCounter += ( ulTotalRunTime - ulTaskSwitchedInTime );
3025 }
3026 else
3027 {
3028 mtCOVERAGE_TEST_MARKER();
3029 }
3030
3031 ulTaskSwitchedInTime = ulTotalRunTime;
3032 }
3033 #endif /* configGENERATE_RUN_TIME_STATS */
3034
3035 /* Check for stack overflow, if configured. */
3036 taskCHECK_FOR_STACK_OVERFLOW();
3037
3038 /* Before the currently running task is switched out, save its errno. */
3039 #if ( configUSE_POSIX_ERRNO == 1 )
3040 {
3041 pxCurrentTCB->iTaskErrno = FreeRTOS_errno;
3042 }
3043 #endif
3044
3045 /* Select a new task to run using either the generic C or port
3046 * optimised asm code. */
3047 taskSELECT_HIGHEST_PRIORITY_TASK(); /*lint !e9079 void * is used as this macro is used with timers and co-routines too. Alignment is known to be fine as the type of the pointer stored and re trieved is the same. */
/*调用函数 taskSELECT_HIGHEST_PRIORITY_TASK()获取下一个要运行的任务*/
3048 traceTASK_SWITCHED_IN();
3049
3050 /* After the new task is switched in, update the global errno. */
3051 #if ( configUSE_POSIX_ERRNO == 1 )
3052 {
3053 FreeRTOS_errno = pxCurrentTCB->iTaskErrno;
3054 }
3055 #endif
3056
3057 #if ( configUSE_NEWLIB_REENTRANT == 1 )
3058 {
3059 /* Switch Newlib's _impure_ptr variable to point to the _reent
3060 * structure specific to this task.
3061 * See the third party link http://www.nadler.com/embedded/newlibAndFreeRTOS.html
3062 * for additional information. */
3063 _impure_ptr = &( pxCurrentTCB->xNewLib_reent );
3064 }
3065 #endif /* configUSE_NEWLIB_REENTRANT */
3066 }
3067 }
3068 /*-----------------------------------------------------------*/
FreeRTOS 时间片调度
前面多次提到 FreeRTOS 支持多个任务同时拥有一个优先级,这些任务的调度是一个值得考虑的问题,不过这不是我们要考虑的。在 FreeRTOS 中允许一个任务运行一个时间片(一个时钟节拍的长度)后让出 CPU 的使用权,让拥有同优先级的下一个任务运行,至于下一个要运行哪个任务?在 9.4 小节里面已经分析过了,FreeRTOS 中的这种调度方法就是时间片调度。下图展示了运行在同一优先级下的执行时间图,在优先级 N 下有 3 个就绪的任务。
1、任务 3 正在运行。
2、这时一个时钟节拍中断(滴答定时器中断)发生,任务 3 的时间片用完,但是任务 3 还没有执行完。
3、FreeRTOS 将任务切换到任务 1,任务 1 是优先级 N 下的下一个就绪任务。
4、任务 1 连续运行至时间片用完。
5、任务 3 再次获取到 CPU 使用权,接着运行。
6、任务 3 运行完成,调用任务切换函数 portYIELD()强行进行任务切换放弃剩余的时间片,从而使优先级 N 下的下一个就绪的任务运行。
7、FreeRTOS 切换到任务 1。
8、任务 1 执行完其时间片。
要使用时间片调度的话宏 configUSE_PREEMPTION 和宏 configUSE_TIME_SLICING 必须为 1。时间片的长度由宏 configTICK_RATE_HZ 来确定,一个时间片的长度就是滴答定时器的中断周期,比如本教程中 configTICK_RATE_HZ 为 1000,那么一个时间片的长度就是 1ms。时间片调度发生在滴答定时器的中断服务函数中,前面讲解滴答定时器中断服务函数的时候说了在中断服务函数 SysTick_Handler()中会调用 FreeRTOS 的 API 函数 xPortSysTickHandler(),而函数 xPortSysTickHandler() 会 引 发 任 务 调 度 , 但 是 这 个 任 务 调 度 是 有 条 件 的 , 函 数xPortSysTickHandler()如下:
515 /*-----------------------------------------------------------*/
516
517 void xPortSysTickHandler( void )
518 {
519 /* The SysTick runs at the lowest interrupt priority, so when this interrupt
520 * executes all interrupts must be unmasked. There is therefore no need to
521 * save and then restore the interrupt mask value as its value is already
522 * known - therefore the slightly faster vPortRaiseBASEPRI() function is used
523 * in place of portSET_INTERRUPT_MASK_FROM_ISR(). */
524 vPortRaiseBASEPRI(); /*关闭中断*/
525 {
526 /* Increment the RTOS tick. */
527 if( xTaskIncrementTick() != pdFALSE ) (1)
528 {
529 /* A context switch is required. Context switching is performed in
530 * the PendSV interrupt. Pend the PendSV interrupt. */
531 portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
/*通过向中断控制和壮态寄存器 ICSR 的 bit28 写入 1 挂起 PendSV 来启动 PendSV 中
*断。这样就可以在 PendSV 中断服务函数中进行任务切换了。*/
532 }
533 }
534
535 vPortClearBASEPRIFromISR();/*打开中断*/
536 }
537 /*-----------------------------------------------------------*/
上述代码中(1)表明只有函数 xTaskIncrementTick()的返回值不为 pdFALSE 的时候就会进行任务调度!查看函数 xTaskIncrementTick()会发现有如下条件编译语句:
不管是什么系统,运行都需要有个系统时钟节拍,前面已经提到多次了,xTickCount 就是FreeRTOS 的系统时钟节拍计数器。每个滴答定时器中断中 xTickCount 就会加一,xTickCount 的具体操作过程是在函数 xTaskIncrementTick()中进行的,此函数在文件 tasks.c 中有定义,如下:
2707 /*----------------------------------------------------------*/
2708
2709 BaseType_t xTaskIncrementTick( void )
2710 {
2711 TCB_t * pxTCB;
2712 TickType_t xItemValue;
2713 BaseType_t xSwitchRequired = pdFALSE;
2714
2715 /* Called by the portable layer each time a tick interrupt occurs.
2716 * Increments the tick then checks to see if the new tick value will cause any
2717 * tasks to be unblocked. */
2718 traceTASK_INCREMENT_TICK( xTickCount );
/*每个时钟节拍中断(滴答定时器中断)调用一次本函数,增加时钟节拍计数器 xTickCount 的
*值,并且检查是否有任务需要取消阻塞。*/
2719
2720 if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE )
/*判断任务调度器是否被挂起*/
2721 {
2722 /* Minor optimisation. The tick count cannot change in this
2723 * block. */
2724 const TickType_t xConstTickCount = xTickCount + ( TickType_t ) 1;
/*将时钟节拍计数器 xTickCount 加一,并将结果保存在 xConstTickCount 中,下一行程
*序会将 xConstTickCount 赋值给 xTickCount,相当于给 xTickCount 加一*/
2725
2726 /* Increment the RTOS tick, switching the delayed and overflowed
2727 * delayed lists if it wraps to 0. */
2728 xTickCount = xConstTickCount;
2729
2730 if( xConstTickCount == ( TickType_t ) 0U ) /*xConstTickCount 为 0,说明发生了溢出! */
2731 {
2732 taskSWITCH_DELAYED_LISTS();
/*如果发生了溢出的话使用函数 taskSWITCH_DELAYED_LISTS 将延时列表指针
*pxDelayedTaskList 和溢出列表指针 pxOverflowDelayedTaskList 所指向的列表进行交换,函数
*taskSWITCH_DELAYED_LISTS()本质上是个宏,在文件 tasks.c 中有定义,将这两个指针所指向
*的列表交换以后还需要更新 xNextTaskUnblockTime 的值。*/
2733 }
2734 else
2735 {
2736 mtCOVERAGE_TEST_MARKER();
2737 }
2738
2739 /* See if this tick has made a timeout expire. Tasks are stored in
2740 * the queue in the order of their wake time - meaning once one task
2741 * has been found whose block time has not expired there is no need to
2742 * look any further down the list. */
/*判断是否有任务延时时间到了,任务都会根据唤醒时间点值按照顺序(由小到大的升
*序排列)添加到延时列表中,这就意味这如果延时列表中第一个列表项对应的任务的
*延时时间都没有到的话后面的任务就不用看了,肯定也没有到。*/
2743 if( xConstTickCount >= xNextTaskUnblockTime )
/*变量 xNextTaskUnblockTime 保存着下一个要解除阻塞的任务的时间点值,如果
*xConstTickCount 大于 xNextTaskUnblockTime 的话就说明有任务需要解除阻塞了*/
2744 {
2745 for( ; ; )
2746 {
2747 if( listLIST_IS_EMPTY( pxDelayedTaskList ) != pdFALSE )
/*判断延时列表是否为空*/
2748 {
2749 /* The delayed list is empty. Set xNextTaskUnblockTime
2750 * to the maximum possible value so it is extremely
2751 * unlikely that the
2752 * if( xTickCount >= xNextTaskUnblockTime ) test will pass
2753 * next time through. */
2754 xNextTaskUnblockTime = portMAX_DELAY; /*lint !e961 MISRA exception as the casts are only redundant for some ports. */
2755 break;
2756 }
2757 else
2758 {
2759 /* The delayed list is not empty, get the value of the
2760 * item at the head of the delayed list. This is the time
2761 * at which the task at the head of the delayed list must
2762 * be removed from the Blocked state. */
/*延时列表不为空,获取延时列表的第一个列表项的值,根据判断这个值
*判断任务延时时间是否到了, 如果到了的话就将任务移除延时列表。*/
2763 pxTCB = listGET_OWNER_OF_HEAD_ENTRY( pxDelayedTaskList ); /*lint !e9079 void * is used as this macro is used with timers and co-routines too. Alignment is known to be fine as the type of the pointer stored and retrieved is the same. */
/*延时列表不为空,获取延时列表第一个列表项对应的任务控制块*/
2764 xItemValue = listGET_LIST_ITEM_VALUE( &( pxTCB->xStateListItem ) );
/*获取上述获取到的任务控制块中的壮态列表项值*/
2765
2766 if( xConstTickCount < xItemValue )
/*任务控制块中的壮态列表项值保存了任务的唤醒时间点,如果这个唤醒时间点值大于
*当前的系统时钟(时钟节拍计数器值),说明任务的延时时间还未到。*/
2767 {
2768 /* It is not time to unblock this item yet, but the
2769 * item value is the time at which the task at the head
2770 * of the blocked list must be removed from the Blocked
2771 * state - so record the item value in
2772 * xNextTaskUnblockTime. */
2773 xNextTaskUnblockTime = xItemValue;
/*任务延时时间还未到,而且 xItemValue 已经保存了下一个要唤醒的任务的唤醒时间
*点,所以需要用 xItemValue 来更新 xNextTaskUnblockTime*/
2774 break; /*lint !e9011 Code structure here is deedmed easier to understand with multiple breaks. */
2775 }
2776 else
2777 {
2778 mtCOVERAGE_TEST_MARKER();
2779 }
2780
2781 /* It is time to remove the item from the Blocked state. */
2782 ( void ) uxListRemove( &( pxTCB->xStateListItem ) );
/*任务延时时间到了,所以将任务先从延时列表中移除*/
2783
2784 /* Is the task waiting on an event also? If so remove
2785 * it from the event list. */
2786 if( listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) != NULL )
/*检查任务是否还等待某个事件,比如等待信号量、队列等。如果还在等待的话就任务
*从相应的事件列表中移除。因为超时时间到了!*/
2787 {
2788 ( void ) uxListRemove( &( pxTCB->xEventListItem ) );
/*将任务从相应的事件列表中移除*/
2789 }
2790 else
2791 {
2792 mtCOVERAGE_TEST_MARKER();
2793 }
2794
2795 /* Place the unblocked task into the appropriate ready
2796 * list. */
2797 prvAddTaskToReadyList( pxTCB );
/*任务延时时间到了,并且任务已经从延时列表或者事件列表中已经移除。所以这里需
*要将任务添加到就绪列表中*/
2798
2799 /* A task being unblocked cannot cause an immediate
2800 * context switch if preemption is turned off. */
2801 #if ( configUSE_PREEMPTION == 1 )
2802 {
2803 /* Preemption is on, but a context switch should
2804 * only be performed if the unblocked task has a
2805 * priority that is equal to or higher than the
2806 * currently executing task. */
2807 if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority )
/*延时时间到的任务优先级高于正在运行的任务优先级,所以需要进行任务切换了,标
*记 xSwitchRequired 为 pdTRUE,表示需要进行任务切换。*/
2808 {
2809 xSwitchRequired = pdTRUE;
2810 }
2811 else
2812 {
2813 mtCOVERAGE_TEST_MARKER();
2814 }
2815 }
2816 #endif /* configUSE_PREEMPTION */
2817 }
2818 }
2819 }
2820
2821 /* Tasks of equal priority to the currently running task will share
2822 * processing time (time slice) if preemption is on, and the application
2823 * writer has not explicitly turned time slicing off. */
2824 #if ( ( configUSE_PREEMPTION == 1 ) && ( configUSE_TIME_SLICING == 1 ) )
/*如果使能了时间片调度的话,还要处理跟时间片调度有关的工作
*当宏 configUSE_PREEMPTION和宏 configUSE_PREEMPTION 都为 1 的时候下面的
*代码才会编译。所以要想使用时间片调度的话这这两个宏都必须为 1,缺一不可!*/
2825 {
2826 if( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ pxCurrentTCB->uxPriority ] ) ) > ( UBaseType_t ) 1 )
/*判断当前任务所对应的优先级下是否还有其他的任务*/
2827 {
2828 xSwitchRequired = pdTRUE;
/*如果当前任务所对应的任务优先级下还有其他的任务那么就返回 pdTRUE*/
2829 }
2830 else
2831 {
2832 mtCOVERAGE_TEST_MARKER();
2833 }
2834 }
2835 #endif /* ( ( configUSE_PREEMPTION == 1 ) && ( configUSE_TIME_SLICING == 1 ) ) */
2836
2837 #if ( configUSE_TICK_HOOK == 1 )
/*如果使能了时间片钩子函数的话就执行时间片钩子函数 vApplicationTickHook(),函
* 数的具体内容由用户自行编写。*/
2838 {
2839 /* Guard against the tick hook being called when the pended tick
2840 * count is being unwound (when the scheduler is being unlocked). */
2841 if( xPendedTicks == ( TickType_t ) 0 )
2842 {
2843 vApplicationTickHook();
2844 }
2845 else
2846 {
2847 mtCOVERAGE_TEST_MARKER();
2848 }
2849 }
2850 #endif /* configUSE_TICK_HOOK */
2851
2852 #if ( configUSE_PREEMPTION == 1 )
2853 {
2854 if( xYieldPending != pdFALSE )
2855 {
2856 xSwitchRequired = pdTRUE;
2857 }
2858 else
2859 {
2860 mtCOVERAGE_TEST_MARKER();
2861 }
2862 }
2863 #endif /* configUSE_PREEMPTION */
2864 }
2865 else
/*如果调用函数 vTaskSuspendAll()挂起了任务调度器的话在每个滴答定时器中断就不
*不会更新 xTickCount 了。取而代之的是用 uxPendedTicks 来记录调度器挂起过程中的时钟节拍
*数。这样在调用函数 xTaskResumeAll()恢复任务调度器的时候就会调用 uxPendedTicks 次函数
*xTaskIncrementTick(),这样 xTickCount 就会恢复,并且那些应该取消阻塞的任务都会取消阻塞*/
2866 {
2867 ++xPendedTicks;
/*uxPendedTicks 是个全局变量,在文件 tasks.c 中有定义,任务调度器挂起以后此变量
*用来记录时钟节拍数*/
2868
2869 /* The tick hook gets called at regular intervals, even if the
2870 * scheduler is locked. */
2871 #if ( configUSE_TICK_HOOK == 1 )
2872 {
2873 vApplicationTickHook();
2874 }
2875 #endif
2876 }
2877
2878 return xSwitchRequired;
/*返回 xSwitchRequired 的值,xSwitchRequired 保存了是否进行任务切换的信息,如果
*为 pdTRUE 的话就需要进行任务切换,pdFALSE 的话就不需要进行任务切换。函数
*xPortSysTickHandler()中调用 xTaskIncrementTick()的时候就会判断返回值,并且根据返回值决定
*是否进行任务切换。*/
2879 }
2880 /*-----------------------------------------------------------*/
从上面的代码可以看出,如果当前任务所对应的优先级下有其他的任务存在,那么函数xTaskIncrementTick() 就 会 返 回 pdTURE , 由 于 函 数 返 回 值 为 pdTURE 因 此 函 数xPortSysTickHandler()就会进行一次任务切换。