目录
vTaskSwitchContext()查找下一个要运行的任务
总结
1、SVC(系统服务调用,亦简称系统调用)异常,在调度器启动时启动第一个任务时使用,后面就一直没有使用了。
2、FreeRTOS任务切换中,都是在PendSV中断服务函数中进行,这个中断服务函数会在所有中断结束后执行,
任务切换的主要切换方式:
- 任务级任务切换函数taskYIELD() --> portYIELD()函数
- 中断级任务切换函数为 portYIELD_FROM_ISR(x),这里的x通常为xHigherPriorityTaskWoken。
- systick中断服务函数xPortSysTickHandler中,调用了xTaskIncrementTick函数,在调用listCURRENT_LIST_LENGTH判断当期优先级下是否有其他同优先级项目,如果同等级有ready,回到xPortSysTickHandler使能PendSV中断。
以上三个的切换方式中都是给PendSV的特定寄存器置一,portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;触发PendSV中断。最终在PendSV中断服务函数中进行具体的任务切换。
3、PendSV中断服务函数
主要进行这几项任务,也主要体现在vTaskSwitchContext这个函数:
1、选择出优先级最高的任务或者同等级切换,taskSELECT_HIGHEST_PRIORITY_TASK宏进行。
2、taskSELECT_HIGHEST_PRIORITY_TASK函数中调用listGET_OWNER_OF_NEXT_ENTRY进行时间片的调度。
PendSV中断服务函数xPortPendSVHandler(汇编代码) --> vTaskSwitchContext函数 --> taskSELECT_HIGHEST_PRIORITY_TASK宏,分为硬件方法和通用方法,(选择哪种方法通过宏 configUSE_PORT_OPTIMISED_TASK_SELECTION 来决定)选择出优先级最高的任务或者同等级切换 --> 通用方法:pxReadyTasksLists[configMAX_PRIORITIES]就绪列表,uxTopReadyPriority就绪态的最高优先级,每次创建任务和使用prvAddTaskToReadyList()函数会根据情况修改这个值-->listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) ),,在一个就绪态列表中选择此列表中的下一项,同一个列表中各项优先级相等,体现在时间片的调度中。
taskSELECT_HIGHEST_PRIORITY_TASK:
硬件方法:限制任务的优先级数,比如 STM32 只能有 32 个优先级,不过 32 个优先级已经完全够用了。效率高。
通用方法:肯定适用于不同的芯片和平台, 而且对于任务数量没有限制, 但是效率肯定相对于使用硬件方法的要低很多。
4、freeRTOS的时间片
所谓时间片就 是同一个优先级下可以有多个任务,每个任务轮流地享有相同的 CPU 时间,享有 CPU 的时间我们叫时间片。
在 RTOS 中,最小的时间单位为一个 tick,即 SysTick 的中断周期, RT-Thread 和 μC/OS 可以指定时间片的大小为多个 tick,但是 FreeRTOS 不一样,时间片只能是一个 tick。与其说 FreeRTOS 支持时间片,倒不如说它的时间片就是正常的任务调度。
多任务系统
原始的单片机裸机(未使用系统),使用如下的前后台系统:前后台系统的实时性差,前后台系统各个任务(应用程序)都是排队等着轮流执行,不管你这个程序现在有多紧急, 没轮到你就只能等着! 相当于所有任务(应用程序)的优先级都是一样的。
比如 FreeRTOS 是一个抢占式的实时多任务系统, 那么其任务调度器也是抢占式的,如下:
FreeRTOS 任务相关 API 函数
任务创建和删除 API 函数
函数 | 描述 |
xTaskCreate() | 使用动态的方法创建一个任务。 |
xTaskCreateStatic() | 使用静态的方法创建一个任务。 |
xTaskCreateRestricted() | 创建一个使用 MPU 进行限制的任务,相关内存使用动态内存分配。 |
vTaskDelete() | 删除一个任务。 |
BaseType_t xTaskCreate( TaskFunction_t pxTaskCode, const char * const pcName, const uint16_t usStackDepth, void * const pvParameters, UBaseType_t uxPriority, TaskHandle_t * const pxCreatedTask ) pxTaskCode: 任务函数。 pcName: 任务名字,一般用于追踪和调试,任务名字长度不能超过。 configMAX_TASK_NAME_LEN。 usStackDepth: 任务堆栈大小,注意实际申请到的堆栈是 usStackDepth 的 4 倍。其中空闲任务的任务堆栈大小为 configMINIMAL_STACK_SIZE。 pvParameters: 传递给任务函数的参数。 uxPriotiry: 任务优先级,范围 0~ configMAX_PRIORITIES-1。 pxCreatedTask: 任务句柄,任务创建成功以后会返回此任务的任务句柄, 这个句柄其实就是任务的任务堆栈。 此参数就用来保存这个任务句柄。其他 API 函数可能会使用到这个句柄。 |
任务挂起和恢复 API 函数
函数 | 描述 |
vTaskSuspend() | 挂起一个任务。 |
vTaskResume() | 恢复一个任务的运行。 |
xTaskResumeFromISR() | 中断服务函数中恢复一个任务的运行。 |
taskYIELD() | 任务切换函数 |
FreeRTOS 调度器开启和任务相关函数详解
任务调度器开启函数分析
前面的所有例程中我们都是在 main()函数中先创建一个开始任务 start_task,后面紧接着调
用函数 vTaskStartScheduler()。这个函数的功能就是开启任务调度器的,这个函数在文件 tasks.c
中有定义,缩减后的函数代码如下:
void vTaskStartScheduler( void ) { BaseType_t xReturn; xReturn = xTaskCreate( prvIdleTask, (1)"IDLE", configMINIMAL_STACK_SIZE,( void * ) NULL,( tskIDLE_PRIORITY | portPRIVILEGE_BIT ),&xIdleTaskHandle ); //创建空闲任务,如果使用静态内存的话使用函数 xTaskCreateStatic()来创建空闲任务,优先级为 tskIDLE_PRIORITY,宏 tskIDLE_PRIORITY 为 0,也就是说空闲任务的优先级为最低。 #if ( configUSE_TIMERS == 1 ) //使用软件定时器使能 { if( xReturn == pdPASS ) { xReturn = xTimerCreateTimerTask(); (2) 如果使用软件定时器的话还需要通过函数 xTimerCreateTimerTask()来创建定时器服务任务。定时器服务任务的具体创建过程是在函数 xTimerCreateTimerTask()中完成的,这个函数很简单,大家就 自行查阅一下 } else { mtCOVERAGE_TEST_MARKER(); } } #endif /* configUSE_TIMERS */ if( xReturn == pdPASS ) //空闲任务和定时器任务创建成功。 { portDISABLE_INTERRUPTS(); (3) //关闭中断,在 SVC 中断服务函数 vPortSVCHandler()中会打开中断。 #if ( configUSE_NEWLIB_REENTRANT == 1 ) //使能 NEWLIB { _impure_ptr = &( pxCurrentTCB->xNewLib_reent ); } #endif /* configUSE_NEWLIB_REENTRANT */ xNextTaskUnblockTime = portMAX_DELAY; xSchedulerRunning = pdTRUE; (4) //变量 xSchedulerRunning 设置为 pdTRUE,表示调度器开始运行。 xTickCount = ( TickType_t ) 0U; portCONFIGURE_TIMER_FOR_RUN_TIME_STATS(); (5) //当宏 configGENERATE_RUN_TIME_STATS 为 1 的时候说明使能时间统计功能,此时需要用户实现宏 portCONFIGURE_TIMER_FOR_RUN_TIME_STATS,此宏用来配置一个定时器/计数器。 if( xPortStartScheduler() != pdFALSE ) (6) //调用函数 xPortStartScheduler()来初始化跟调度器启动有关的硬件, 比如滴答定时器(vPortSetupTimerInterrupt();)、FPU 单元和 PendSV 中断等等 { //如果调度器启动成功的话就不会运行到这里,函数不会有返回值的 } else { //不会运行到这里,除非调用函数 xTaskEndScheduler()。 } } else { //程序运行到这里只能说明一点,那就是系统内核没有启动成功,导致的原因是在创建 //空闲任务或者定时器任务的时候没有足够的内存。 configASSERT( xReturn != errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY ); } //防止编译器报错,比如宏 INCLUDE_xTaskGetIdleTaskHandle 定义为 0 的话编译器就会提 //示 xIdleTaskHandle 未使用。 ( void ) xIdleTaskHandle; } |
内核相关硬件初始化函数分析
FreeRTOS 系统时钟是由滴答定时器来提供的,而且任务切换也会用到 PendSV 中断,这些硬件的初始化由函数 xPortStartScheduler()来完成,缩减后的函数代码如下:
BaseType_t xPortStartScheduler( void ) { /******************************************************************/ /****************此处省略一大堆的条件编译代码**********************/ /*****************************************************************/ portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI; (1)// 设置 PendSV 的中断优先级,为最低优先级 portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI; (2) 设置滴答定时器的中断优先级,为最低优先级 vPortSetupTimerInterrupt(); (3) 调用函数 vPortSetupTimerInterrupt()来设置滴答定时器的定时周期,并且使能滴答定时器的中断, uxCriticalNesting = 0; (4) 初始化临界区嵌套计数器。 prvEnableVFP(); (5) 调用函数 prvEnableVFP()使能 FPU *( portFPCCR ) |= portASPEN_AND_LSPEN_BITS; (6) 设置寄存器 FPCCR 的 bit31 和 bit30 都为 1,这样 S0~S15 和 FPSCR 寄存器在异常入口和退出时的壮态自动保存和恢复。并且异常流程使用惰性压栈的特性以保证中断等待。关于FPCCR 寄存器和惰性压栈的知识请参考《权威指南》的“第 12 章 浮点运算”。 prvStartFirstTask(); (7) //代码正常执行的话是不会到这里的! return 0; } |
启动第一个任务
函数 prvStartFirstTask()用于启动第一个任务,这是一个汇编函数,函数源码如下:具体见正点原子
SVC 中断服务函数
在函数 prvStartFirstTask()中通过调用 SVC 指令触发了 SVC 中断,而第一个任务的启动就是在 SVC 中断服务函数中完成的, SVC 中断服务函数应该为 SVC_Handler(),但是FreeRTOSConfig.h 中通过#define 的方式重新定义为了 xPortPendSVHandler(),如下:
#define xPortPendSVHandler PendSV_Handler |
函数 vPortSVCHandler()在文件 port.c 中定义,这个函数也是用汇编写的,函数源码如下
__asm void vPortSVCHandler( void ) { PRESERVE8 ldr r3, =pxCurrentTCB ;R3=pxCurrentTCB 的地址 (1) ldr r1, [r3] ;取 R3 所保存的地址处的值赋给 R1 (2) ldr r0, [r1] ;取 R1 所保存的地址处的值赋给 R0 (3) ldmia r0!, {r4-r11, r14} ;出栈 , R4~R11 和 R14 (4) msr psp, r0 ;进程栈指针 PSP 设置为任务的堆栈 (5) isb ;指令同步屏障 mov r0, #0 ;R0=0 (6) msr basepri, r0 ;寄存器 basepri=0,开启中断 (7) bx r14 (8) } |
空闲任务
函数 vTaskStartScheduler()说过,此函数会创建一个名为“IDLE”的任务,这个任务叫做空闲任务。顾名思义,空闲任务就是空闲的时候运行的任务,也就是系统中其他的任务由于各种原因不能运行的时候空闲任务就在运行。空闲任务是 FreeRTOS 系统自动创建的,不需要用户手动创建。任务调度器启动以后就必须有一个任务运行!但是空闲任务不仅仅是为了满足任务调度器启动以后至少有一个任务运行而创建的,空闲任务中还会去做一些其他的事情,如下:
1、判断系统是否有任务删除,如果有的话就在空闲任务中释放被删除任务的任务堆栈和任务控制块的内存。 2、运行用户设置的空闲任务钩子函数。 3、判断是否开启低功耗 tickless 模式,如果开启的话还需要做相应的处理空闲任务的任务优先级是最低的,为 0,任务函数为 prvIdleTask(),有关空闲任务的详细内容我们后面会有专门的章节讲解,这里大家只要知道有这个任务就行了。 |
FreeRTOS 任务切换
PendSV 异常
参考资源
Cm3权威指南中,《异常章节》SVC 和 PendSV,PendSV(可悬起的系统调用)
SVC(系统服务调用,亦简称系统调用)和 PendSV(可悬起系统调用)
SVC 和 PendSV(参考Cm3权威指南)
- 权威指南
SVC(系统服务调用,亦简称系统调用)和 PendSV(可悬起系统调用),它们多用于在操作系统之上的软件开发中。 SVC 用于产生系统函数的调用请求。例如,操作系统不让用户程序直接访问硬件,而是通过提供一些系统服务函数,用户程序使用 SVC 发出对系统服务函数的呼叫请求,以这种方法调用它们来间接访问硬件。
另一个相关的异常是 PendSV(可悬起的系统调用),它和 SVC 协同使用。
SVC异常是必须立即得到响应的(若因优先级不比当前正处理的高, 或是其它原因使之无法立即响应, 将上访成硬 fault——译者注)
PendSV 则不同,它是可以像普通的中断一样被悬起的(不像 SVC 那样会上访)。 OS 可以利用它“缓期执行” 一个异常——直到其它重要的任务完成后才执行动作。 悬起 PendSV 的方法是: 手工往 NVIC 的 PendSV 悬起寄存器中写 1。 悬起后, 如果优先级不够高,则将缓期等待执行。PendSV 的典型使用场合是在上下文切换时(在不同任务之间切换)。
- 正点原子
PendSV(可挂起的系统调用)异常对 OS 操作非常重要,其优先级可以通过编程设置。可以通过将中断控制和壮态寄存器 ICSR 的 bit28,也就是 PendSV 的挂起位置 1 来触发 PendSV 中断。 与 SVC 异常不同,它是不精确的,因此它的挂起壮态可在更高优先级异常处理内设置,且会在高优先级处理完成后执行。
利用该特性,若将 PendSV 设置为最低的异常优先级,可以让 PendSV 异常处理在所有其他中断处理完成后执行,这对于上下文切换非常有用,也是各种 OS 设计中的关键。
没有使用PendSV的实时系统一次错误案例
在具有嵌入式 OS 的典型系统中,处理时间被划分为了多个时间片。若系统中只有两个任务,这两个任务会交替执行,如图 9.1.1 所示:
上下文切换被触发的场合可以是:
-
- 执行一个系统调用
- 系统滴答定时器(SysTick)中断。
在 OS 中,任务调度器决定是否应该执行上下文切换,如图 9.1.1 中任务切换都是由 SysTick中断中执行,每次它都会决定切换到一个不同的任务中。
若中断请求(IRQ)在 SysTick 异常前产生,则 SysTick 异常可能会抢占 IRQ 的处理,在这种情况下, OS 不应该执行上下文切换,否则中断请求 IRQ 处理就会被延迟,而且在真实系统中延迟时间还往往不可预知——任何有一丁点实时要求的系统都决不能容忍这种事。对于 CortexM3 和 Cortex-M4 处理器,当存在活跃的异常服务时,设计默认不允许返回到线程模式,若存在活跃中断服务,且 OS 试图返回到线程模式,则将触发用法 fault,如图 9.1.2 所示。
(重点)防止中断处理被延误
在一些 OS 设计中,要解决这个问题,可以在运行中断服务时不执行上下文切换,此时可以检查栈帧中的压栈 xPSR 或 NVIC 中的中断活跃壮态寄存器。不过,系统的性能可能会受到影响,特别时当中断源在 SysTick 中断前后持续产生请求时,这样上下文切换可能就没有执行的机会了。
为了解决这个问题, PendSV 异常将上下文切换请求延迟到所有其他 IRQ 处理都已经完成后,此时需要将 PendSV 设置为最低优先级。若 OS 需要执行上下文切换,他会设置 PendSV 的挂起壮态,并在 PendSV 异常内执行上下文切换。如图 9.1.3 所示
中断控制及状态寄存器ICSR 0xE000_ED04
位段 | 名称 | 类型 | 复位值 | 描述 |
31 | NMIPENDSET | R/W | 0 | 写 1 以悬起 NMI。因为 NMI 的优先级最高且从不掩蔽,在置位此位后将立即进入 NMI 服务例程。 |
28 | PENDSVSET | R/W | 0 | 写 1 以悬起 PendSV。读取它则返回 PendSV 的状态 |
27 | PENDSVCLR | W | 0 | 写 1 以清除 PendSV 悬起状态 |
26 | PENDSTSET | R/W | 0 | 写 1 以悬起 SysTick。读取它则返回 PendSV 的状态 |
25 | PENDSTCLR | W | 0 | 写 1 以清除 SysTick 悬起状态 |
23 | ISRPREEMPT | R | 0 | 为 1 时,则表示一个悬起的中断将在下一步时进入活动状态(用于单步执行时的调试目的) |
22 | ISRPENDING | R | 0 | 1=当前正有外部中断被悬起(不包括 NMI) |
21:12 | VECTPENDING | R | 0 | 悬起的 ISR 的编号。如果不止一个中断悬起,则它的值是这引动中断中,优先级最高的那一个。 |
11 | RETTOBASE | R | 0 | 当从异常返回后将回到基级(base level),且没有其它异常悬起时,此位为 1。若是在线程模式下,在某个服务例程中,有不止一级的异常处于活动状态,或者在异常没有活动时执行了异常服务例程(此时执行返回指令将产生 fault。此乃高危行为,大虾专用),则此位为 0 |
9:0 | VECTACTIVE | R | 0 | 当前活动的ISR编号,该位段指出当前运行中的ISR是哪个中断的(提供异常序号),包括NMI和硬fault。 如果多个异常共享一个服务例程,该例程可根据本位段的值来判定是哪一个异常的响应导致它的执行。把本位段的值减去16,就得到了外中断的编号,并可以用此编号来操作外中断相关的使能/除能等寄存器。 |
FreeRTOS 任务切换场合
在 9.1 小节中讲解 PendSV 中断的时候提到了上下文(任务)切换被触发的场合:
-
- 可以执行一个系统调用
- 系统滴答定时器(SysTick)中断
执行系统调用
执行 FreeRTOS 系统提供的相关 API 函数
1、任务级任务切换函数taskYIELD()
taskYIELD()宏 --> portYIELD()函数,通过向中断控制和壮态寄存器 ICSR 的 bit28 写入 1 挂起 PendSV 来启动 PendSV 中断,这样在PendSV 中断服务函数中进行任务切换
2、中断级任务切换函数为 portYIELD_FROM_ISR(x),这里的x通常为xHigherPriorityTaskWoken。
portYIELD_FROM_ISR(x) --> portEND_SWITCHING_ISR( x); --> if( x!= pdFALSE ) portYIELD() -
任务级
执行系统调用就是执行 FreeRTOS 系统提供的相关 API 函数,比如任务切换函数taskYIELD(), FreeRTOS 有些 API 函数也会调用函数 taskYIELD(),这些 API 函数都会导致任务切换,这些 API 函数和任务切换函数 taskYIELD()都统称为系统调用。 函数 taskYIELD()其实就是个宏,在文件 task.h 中有如下定义:
#define taskYIELD() portYIELD() |
函数 portYIELD()也是个宏,在文件 portmacro.h 中有如下定义:
#define portYIELD() \ { \ portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; \ (1) //通过向中断控制和壮态寄存器 ICSR 的 bit28 写入 1 挂起 PendSV 来启动 PendSV 中断。 这样就可以在 PendSV 中断服务函数中进行任务切换了 \ __dsb( portSY_FULL_READ_WRITE ); \ __isb( portSY_FULL_READ_WRITE ); \ } |
中断级
中断级的任务切换函数为 portYIELD_FROM_ISR(x),定义如下
#define portYIELD_FROM_ISR( x ) portEND_SWITCHING_ISR( x ) #define portEND_SWITCHING_ISR( xSwitchRequired ) if( xSwitchRequired != pdFALSE ) portYIELD() |
可以看出 portYIELD_FROM_ISR()最终也是通过调用函数 portYIELD()来完成任务切换的。
举例
pxHigherPriorityTaskWoken标志位和API切换任务
/* * 往队列尾部写入数据,此函数可以在中断函数中使用,不可阻塞 */ BaseType_t xQueueSendToBackFromISR( QueueHandle_t xQueue, const void *pvItemToQueue, BaseType_t *pxHigherPriorityTaskWoken 1、xHigherPriorityTaskWoken的含义是:是否有更高优先级的任务被唤醒了。 2、pxHigherPriorityTaskWoken参数,就是用来保存函数的结果: pxHigherPriorityTaskWoken为pdTRUE:函数的操作导致更高优先级的任务就绪了,ISR应该进行任务切换。 pxHigherPriorityTaskWoken为pdFALSE:没有进行任务切换的必要。 |
函数的操作导致更高优先级的任务就绪了,在ISR中调用API时不进行任务切换,而只是在"xHigherPriorityTaskWoken"中标记一下,除了提高效率,还有多种好处:
如何切换任务
FreeRTOS的ISR函数中,使用两个宏进行任务切换:
portEND_SWITCHING_ISR( xHigherPriorityTaskWoken );
或
portYIELD_FROM_ISR( xHigherPriorityTaskWoken );
这两个宏做的事情是一样的,前者使用汇编实现,后者使用C语言实现。
举例说明:
void XXX_ISR() { int i; BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xQueueSendToBackFromISR(..., &xHigherPriorityTaskWoken); portYIELD_FROM_ISR(xHigherPriorityTaskWoken); } 串口中使用示例 void USART1_IRQHandler(void) { portBASE_TYPE xHigherPriorityTaskWoken = pdFALSE; char cChar; if( USART_GetITStatus( USART1, USART_IT_RXNE ) == SET ) { cChar = USART_ReceiveData( USART1 ); xQueueSendFromISR( xRxedChars, &cChar, &xHigherPriorityTaskWoken ); } portEND_SWITCHING_ISR( xHigherPriorityTaskWoken ); } |
系统滴答定时器(SysTick)中断
FreeRTOS 中滴答定时器(SysTick)中断服务函数中也会进行任务切换,滴答定时器中断服务函数如下:
#define xPortSysTickHandler SysTick_Handler |
void xPortSysTickHandler( void ) { vPortRaiseBASEPRI(); { /* Increment the RTOS tick. */ if( xTaskIncrementTick() != pdFALSE ) // //xTaskIncrementTick()的返回值不为 pdFALSE 的时候就会进行任务调度! { /* A context switch is required. Context switching is performed in the PendSV interrupt. Pend the PendSV interrupt. */ portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; //通过向中断控制和壮态寄存器 ICSR 的 bit28 写入 1 挂起 PendSV 来启动 PendSV 中 断。这样就可以在 PendSV 中断服务函数中进行任务切换了 } } vPortClearBASEPRIFromISR(); } |
xTaskIncrementTick函数在下面的时间片中进行讲解
PendSV 中断服务函数
PendSV 中断服务函数:主要调用函数 vTaskSwitchContext(),此函数用来获取下一个要运行的任务,并将pxCurrentTCB 更新为这个要运行的任务,然后切换到这个要运行的任务。
前面说了 FreeRTOS 任务切换的具体过程是在 PendSV 中断服务函数中完成的,本节我们就来学习一个 PendSV 的中断服务函数,看看任务切换过程究竟是怎么进行的。 PendSV 中断服务函数本应该为 PendSV_Handler(),但是 FreeRTOS 使用#define 重定义了,如下:
#define xPortPendSVHandler PendSV_Handler |
xPortPendSVHandler() 中断服务函数
函数 xPortPendSVHandler()源码如下:
__asm void xPortPendSVHandler( void ) { extern uxCriticalNesting; extern pxCurrentTCB; extern vTaskSwitchContext; PRESERVE8 mrs r0, psp (1) 读取进程栈指针,保存在寄存器 R0 里面。 isb ldr r3, =pxCurrentTCB (2) 获取当前任务的任务控制块,并将任务控制块的地址保存在寄存器 R2 里面。 ldr r2, [r3] (3) tst r14, #0x10 (4) 判断任务是否使用了 FPU it eq (5) vstmdbeq r0!, {s16-s31} (6) stmdb r0!, {r4-r11, r14} (7) str r0, [r2] (8) stmdb sp!, {r3} (9) mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY (10) msr basepri, r0 (11) 关闭中断,进入临界区 dsb isb bl vTaskSwitchContext (12) 调用函数 vTaskSwitchContext(),此函数用来获取下一个要运行的任务,并将pxCurrentTCB 更新为这个要运行的任务。 mov r0, #0 (13) msr basepri, r0 (14) 打开中断,退出临界区。 ldmia sp!, {r3} (15) ldr r1, [r3] (16) ldr r0, [r1] (17) ldmia r0!, {r4-r11, r14} (18) tst r14, #0x10 (19) it eq (20) vldmiaeq r0!, {s16-s31} (21) msr psp, r0 (22) 更新进程栈指针 PSP 的值 isb bx r14 (23) 执行此行代码以后硬件自动恢复寄存器 R0~R3、 R12、 LR、 PC 和 xPSR 的值,确定异常返回以后应该进入处理器模式还是进程模式,使用主栈指针(MSP)还是进程栈指针(PSP)。很明显这里会进入进程模式,并且使用进程栈指针(PSP),寄存器 PC 值会被恢复为即将运行的任务的任务函数,新的任务开始运行!至此,任务切换成功。 } |
vTaskSwitchContext()查找下一个要运行的任务
在 PendSV 中断服务程序中有调用函数 vTaskSwitchContext()来获取下一个要运行的任务,也就是查找已经就绪了的优先级最高的任务,缩减后(去掉条件编译)函数源码如下:
void vTaskSwitchContext( void ) { if( uxSchedulerSuspended != ( UBaseType_t ) pdFALSE ) (1) // 如果调度器挂起那就不能进行任务切换。 { xYieldPending = pdTRUE; } else { xYieldPending = pdFALSE; traceTASK_SWITCHED_OUT(); taskCHECK_FOR_STACK_OVERFLOW(); taskSELECT_HIGHEST_PRIORITY_TASK(); (2) 调用函数 taskSELECT_HIGHEST_PRIORITY_TASK()获取下一个要运行的任务。 traceTASK_SWITCHED_IN(); } } |
FreeRTOS 中查找下一个要运行的任务有两种方法:一个是通用的方法,另外一个就是使用硬件的方法,这个在我们讲解 FreeRTOSCofnig.h 文件的时候就提到过了,至于选择哪种方法通过宏 configUSE_PORT_OPTIMISED_TASK_SELECTION 来决定的。当这个宏为 1 的时候就使用硬件的方法,否则的话就是使用通用的方法,我们来看一下这两个方法的区别。
1、通用方法(任务数量没有限制,效率比硬件低)
顾名思义,就是所有的处理器都可以用的方法, 方法如下
#define taskSELECT_HIGHEST_PRIORITY_TASK() \ { \ UBaseType_t uxTopPriority = uxTopReadyPriority; \ while( listLIST_IS_EMPTY( &( pxReadyTasksLists[ uxTopPriority ] ) ) ) \ (1) { \ configASSERT( uxTopPriority ); \ --uxTopPriority; \ } \ listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) ); \ uxTopReadyPriority = uxTopPriority; \ } |
(1)、 在前面的 8.2.4 小节中说了
pxReadyTasksLists[configMAX_PRIORITIES]为就绪任务列表数组,一个优先级一个列表,同优先级的就绪任务都挂到相对应的列表中。
uxTopReadyPriority 代表处于就绪态的最高优先级值,每次创建任务的时候都会判断新任务的优先级是否大于 uxTopReadyPriority,如果大于的话就将这个新任务的优先级赋值给变量 uxTopReadyPriority。
函数 prvAddTaskToReadyList()也会修改这个值,也就是说将某个任务添加到就绪列表中的时候都会用 uxTopReadyPriority 来记录就绪列表中的最高优先级。 这里就从这个最高优先级开始判断,看看哪个列表不为空就说明哪个优先级有就绪的任务。
函数 listLIST_IS_EMPTY()用于判断某个列表是否为空,uxTopPriority 用来记录这个有就绪任务的优先级。
(2)、 已经找到了有就绪任务的优先级了,接下来就是从对应的列表中找出下一个要运行的任务,查找方法就是使用函数 listGET_OWNER_OF_NEXT_ENTRY()来获取列表中的下一个列表项,然后将获取到的列表项所对应的任务控制块赋值给 pxCurrentTCB,这样我们就确定了下一个要运行的任务了。
可以看出通用方法是完全通过 C 语言来实现的,肯定适用于不同的芯片和平台, 而且对于任务数量没有限制, 但是效率肯定相对于使用硬件方法的要低很多
2、硬件方法(stm32限制了任务数,只有32个优先级,因为32位的)
硬件方法就是使用处理器自带的硬件指令来实现的,比如 Cortex-M 处理器就带有的计算前导 0 个数指令: CLZ,函数如下:
#define taskSELECT_HIGHEST_PRIORITY_TASK() \ { \ UBaseType_t uxTopPriority; \ portGET_HIGHEST_PRIORITY( uxTopPriority, uxTopReadyPriority ); \ (1) configASSERT( listCURRENT_LIST_LENGTH( & \ ( pxReadyTasksLists[ uxTopPriority ] ) )> 0 ); \ listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, \ (2) &( pxReadyTasksLists[ uxTopPriority ] ) ); \ } |
(1) 、 通 过 函 数 portGET_HIGHEST_PRIORITY() 获 取 处 于 就 绪 态 的 最 高 优 先 级 ,portGET_HIGHEST_PRIORITY 本质上是个宏,定义如下:
#define portGET_HIGHEST_PRIORITY( uxTopPriority, uxReadyPriorities ) uxTopPriority = ( 31UL\ - ( uint32_t ) __clz( ( uxReadyPriorities ) ) ) |
使用硬件方法的时候 uxTopReadyPriority 就不代表处于就绪态的最高优先级了,而是使用每个 bit 代表一个优先级, bit0 代表优先级 0, bit31 就代表优先级 31,当某个优先级有就绪任务的话就将其对应的 bit 置 1。从这里就可以看出,如果使用硬件方法的话最多只能有 32 个优先级。 __clz(uxReadyPriorities)就是计算 uxReadyPriorities 的前导零个数,前导零个数就是指从最高位开始(bit31)到第一个为 1 的 bit,其间 0 的个数,如下例子:
二进制数 1000 0000 0000 0000 的前导零个数就为 0。
二进制数 0000 1001 1111 0001 的前导零个数就是 4。
得到 uxTopReadyPriority 的前导零个数以后在用 31 减去这个前导零个数得到的就是处于就绪态的最高优先级了,比如优先级 30 为此时的处于就绪态的最高优先级, 30 的前导零个数为1,那么 31-1=30,得到处于就绪态的最高优先级为 30。
(2)、已经找到了处于就绪态的最高优先级了,接下来就是从对应的列表中找出下一个要运行的任务,查找方法就是使用函数 listGET_OWNER_OF_NEXT_ENTRY()来获取列表中的下一个列表项,然后将获取到的列表项所对应的任务控制块赋值给 pxCurrentTCB,这样我们就确定了下一个要运行的任务了。
可以看出硬件方法借助一个指令就可以快速的获取处于就绪态的最高优先级,但是会限制任务的优先级数,比如 STM32 只能有 32 个优先级,不过 32 个优先级已经完全够用了。 要知道
FreeRTOS 是支持时间片的,每个优先级可以支持无限多个任务。
FreeRTOS 时间片调度
FreeRTOS学习记录(九):时间片_喜暖知寒的博客-CSDN博客_freertos时间片
关于时间片的原理知识。
前面多次提到 FreeRTOS 支持多个任务同时拥有一个优先级, 这些任务的调度是一个值得考虑的问题,不过这不是我们要考虑的。在 FreeRTOS 中允许一个任务运行一个时间片(一个时钟节拍的长度)后让出 CPU 的使用权,让拥有同优先级的下一个任务运行, 至于下一个要运行哪个任务?在《查找下一个要运行的任务》已经分析过了, FreeRTOS 中的这种调度方法就是时间片调度。
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()如下:
void xPortSysTickHandler( void ) { vPortRaiseBASEPRI(); { if( xTaskIncrementTick() != pdFALSE ) //xTaskIncrementTick()的返回值不为 pdFALSE 的时候就会进行任务调度! { portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; } } vPortClearBASEPRIFromISR(); } |
查看函数 xTaskIncrementTick()会发现有如下条件编译语句:
BaseType_t xTaskIncrementTick( void ) { TCB_t * pxTCB; TickType_t xItemValue; BaseType_t xSwitchRequired = pdFALSE; if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE ) { /***************************************************************************/ /***************************此处省去一大堆代码******************************/ /***************************************************************************/ #if ( ( configUSE_PREEMPTION == 1 ) && ( configUSE_TIME_SLICING == 1 ) ) (1) //所以要想使用时间片调度的话这这两个宏都必须为 1,缺一不可! { if( listCURRENT_LIST_LENGTH( &( \ pxReadyTasksLists[ pxCurrentTCB->uxPriority ] ) ) > ( UBaseType_t ) 1 ) (2) 判断当前任务所对应的优先级下是否还有其他的任务。 { xSwitchRequired = pdTRUE; (3) 如果当前任务所对应的任务优先级下还有其他的任务那么就返回 pdTRUE。 } else { mtCOVERAGE_TEST_MARKER(); } } #endif /* ( ( configUSE_PREEMPTION == 1 ) && ( configUSE_TIME_SLICING == 1 ) ) } return xSwitchRequired; } |
从上面的代码可以看出,如果当前任务所对应的优先级下有其他的任务存在,那么函数
xTaskIncrementTick() 就 会 返 回 pdTURE , 由 于 函 数 返 回 值 为 pdTURE 因 此 函 数
xPortSysTickHandler()就会进行一次任务切换。freeRTOS同等级任务切换时,每个任务使用的时间片只有一个。具体可以看验证实验。
freeRTOS的时间片实验
#define TASK1_TASK_PRIO 2 //任务优先级 (1) #define TASK1_STK_SIZE 128 //任务堆栈大小 TaskHandle_t Task1Task_Handler; //任务句柄 void task1_task(void *pvParameters); //任务函数 #define TASK2_TASK_PRIO 2 //任务优先级 (2) #define TASK2_STK_SIZE 128 //任务堆栈大小 TaskHandle_t Task2Task_Handler; //任务句柄 void task2_task(void *pvParameters); //任务函数 |
两个任务的优先级相同
int main(void) { prvSetupHardware(); //创建 TASK1 任务 xTaskCreate((TaskFunction_t )task1_task, (const char* )"task1_task", (uint16_t )TASK1_STK_SIZE, (void* )NULL, (UBaseType_t )TASK1_TASK_PRIO, (TaskHandle_t* )&Task1Task_Handler); //创建 TASK2 任务 xTaskCreate((TaskFunction_t )task2_task, (const char* )"task2_task", (uint16_t )TASK2_STK_SIZE, (void* )NULL, (UBaseType_t )TASK2_TASK_PRIO, (TaskHandle_t* )&Task2Task_Handler); vTaskStartScheduler(); } |
开始调度
//task1 任务函数 void task1_task(void *pvParameters) { while(1) { GPIO_ResetBits(GPIOF,GPIO_Pin_9); //LED0对应引脚GPIOF.9拉低,亮 等同LED0=0; } } //task2 任务函数 void task2_task(void *pvParameters) { u8 task2_num=0; while(1) { GPIO_SetBits(GPIOF,GPIO_Pin_9); //LED0对应引脚GPIOF.0拉高,灭 等同LED0=1; } } |
具体的任务代码如上,任务中就是进行控制F9的电平,F9连接逻辑分析仪的D0口
void systick_cycle(void){ static char i=0; if(i%2==0)GPIO_SetBits(GPIOF,GPIO_Pin_10); else GPIO_ResetBits(GPIOF,GPIO_Pin_10); i++; } |
systick_cycle 函数在systick中断中就会调用,每次中断触发时,这里的F10就会取反。F10连接逻辑分析仪的D2
在逻辑分析仪中能够清楚的看到两个任务是在来回切换的,每个任务的时间片就是1.