FreeRTOS总结

1、任务的创建、执行、切换

  1.1、创建任务

  1)定义任务栈,即为每个任务分配独立的任务栈

  2)定义任务函数

  3)定义任务控制块,用来存储任务的信息,比如任务的指针、任务名、任务的形参等

  4)任务创建,即将任务的栈、任务的函数实体、任务的控制块联系起来

  1.2、就绪列表

  任务创建好之后,我们需要把任务添加到就绪列表里面, 表示任务已经就绪,系统随时可以调度。 

  就绪列表实际上就是一个 List_t 类型的数组,数组的大小由决定最大任务优先级的宏 configMAX_PRIORITIES 决 定 , configMAX_PRIORITIES FreeRTOSConfig.h 中默认定义为 5,最大支持 256 个优先级。

  数组的下标对应了任务的优数组的下标对应了任务的优先级,同一优先级的任务统一插入到就绪列表的同一条链表中。 

  任务控制块里面有一个 xStateListItem 成员, 数据类型为 ListItem_t 我们将任务插入到就绪列表里面,就是通过将任务控制块的 xStateListItem 这个节点插入到就绪列表中来实现的。如果把就绪列表比作是晾衣架, 任务是衣服,那 xStateListItem 就是晾衣架上面的钩子,每个任务都自带晾衣架钩子,就是为了把自己挂在各种不同的链表中。

  就绪列表的下标对应的就是任务的优先级。

  就绪列表 pxReadyTasksLists[ configMAX_PRIORITIES ]是一个数组, 数组里面存的是就绪任务的 TCB(准确来说是 TCB 里面的 xStateListItem 节点) ,数组的下标对应任务的优先级,优先级越低对应的数组下标越小。空闲任务的优先级最低,对应的是下标为 0 的链表。 空闲任务自系统启动后会一直就绪,因为系统至少得保证有一个任务可以运行。

  一个任务对应一个链表节点,相同优先级的任务在同一条链表中,创建任务时会根据任务的优先级将任务插入就绪列表的不同位置。

  vListInsertEnd()函数用于将任务添加到就绪列表。

  1.3、调度器

  调度器是操作系统的核心,其主要功能就是实现任务的切换,即从就绪列表里面找到优先级最高的任务,然后去执行该任务。系统调度,最终也是产生 PendSV 中断,在 PendSV Handler 里面实现任务的切换。

  调度器的启动由 vTaskStartScheduler()函数来完成。

/*
* 参考资料《STM32F10xxx Cortex-M3 programming manual》 4.4.3,百度搜索“PM0056”即可找到这个文档
* 在 Cortex-M 中,内核外设 SCB 中 SHPR3 寄存器用于设置 SysTick 和 PendSV 的异常优先级
* System handler priority register 3 (SCB_SHPR3) SCB_SHPR3: 0xE000 ED20
* Bits 31:24 PRI_15[7:0]: Priority of system handler 15, SysTick exception
* Bits 23:16 PRI_14[7:0]: Priority of system handler 14, PendSV
*/
#define portNVIC_SYSPRI2_REG (*(( volatile uint32_t *) 0xe000ed20))

#define portNVIC_PENDSV_PRI (((uint32_t) configKERNEL_INTERRUPT_PRIORITY ) << 16UL)
#define portNVIC_SYSTICK_PRI (((uint32_t) configKERNEL_INTERRUPT_PRIORITY ) << 24UL )

BaseType_t xPortStartScheduler( void )
{
    /* 配置 PendSV 和 SysTick 的中断优先级为最低 */ (1)
    portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;
    portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;

    /* 启动第一个任务,不再返回 */
    prvStartFirstTask(); (2)

    /* 不应该运行到这里 */
    return 0;
}

 

  配置 PendSV 和 SysTick 的中断优先级为最低。 SysTick 和PendSV 都会涉及到系统调度,系统调度的优先级要低于系统的其它硬件中断优先级, 即优先相应系统中的外部硬件中断, 所以 SysTick 和 PendSV 的中断优先级配置为最低。

  prvStartFirstTask()函数用于开始第一个任务,主要做了两个动作,一个是更新 MSP 的值,二是产生 SVC 系统调用,然后去到 SVC 的中断服务函数里面真正切换到第一个任务。

#define xPortPendSVHandler PendSV_Handler
#define xPortSysTickHandler SysTick_Handler
#define vPortSVCHandler SVC_Handler

 

  1.3.1、SVC和PENDSV 基本概念

  SVC(系统服务调用,亦简称系统调用)和 PendSV(可悬起系统调用),它们多用于在操作系统之上的软件开发中。

  (1)SVC

 

  SVC 用于产生系统函数的调用请求。例如,操作系统不让用户程序直接访问硬件,而是通过提供一些系统服务函数,用户程序使用 SVC 发出对系统服务函数的呼叫请求,以这种方法调用它们来间接访问硬件。因此,当用户程序想要控制特定的硬件时,它就会产生一个 SVC 异常,然后操作系统提供的 SVC 异常服务例程得到执行,它再调用相关的操作系统函数,后者完成用户程序请求的服务。

  这种“提出要求——得到满足”的方式,很好、很强大、很方便、很灵活、很能可持续发展。

  1)它使用户程序从控制硬件的繁文缛节中解脱出来,而是由 OS 负责控制具体的硬件。
  2) OS 的代码可以经过充分的测试,从而能使系统更加健壮和可靠。
  3)它使用户程序无需在特权级下执行,用户程序无需承担因误操作而瘫痪整个系统的风险。
  4)通过 SVC 的机制,还让用户程序变得与硬件无关,因此在开发应用程序时无需了解硬件的操作细节,从而简化了开发的难度和繁琐度,并且使应用程序跨硬件平台移植成为可能。

  开发应用程序唯一需要知道的就是操作系统提供的应用编程接口(API),并且了解各个请求代号和参数表,然后就可以使用 SVC 来提出要求了(事实上,为使用方便,操作系统往往会提供一层封皮,以使系统调用的形式看起来和普通的函数调用一致。各封皮函数会正确使用 SVC指令来执行系统调用——译者注)。

  其实,严格地讲,操作硬件的工作是由设备驱动程序完成的,只是对应用程序来说,它们也是操作系统的一部分。如图所示:

 

 

 

  SVC 异常通过执行”SVC”指令来产生。 该指令需要一个立即数, 充当系统调用代号。 SVC异常服务例程稍后会提取出此代号,从而解释本次调用的具体要求,再调用相应的服务函数。

  例如:SVC 0x3 ; 调用 3 号系统服务

  在 SVC 服务例程执行后,上次执行的 SVC 指令地址可以根据自动入栈的返回地址计算出。

  找到了 SVC 指令后, 就可以读取该 SVC 指令的机器码,从机器码中萃取出立即数,就获知了请求执行的功能代号。

  如果用户程序使用的是 PSP, 服务例程还需要先执行 MRS Rn,PSP 指令来获取应用程序的堆栈指针。

  通过分析 LR 的值,可以获知在 SVC 指令执行时,正在使用哪个堆栈。

  (2)PendSV

  PendSV是可悬起异常,如果我们把它配置最低优先级,那么如果同时有多个异常被触发,它会在其他异常执行完毕后再执行,而且任何异常都可以中断它。更详细的内容在《Cortex-M3 权威指南》里有介绍,下面我摘抄了一段。

  OS 可以利用它“缓期执行”一个异常——直到其它重要的任务完成后才执行动 作。悬起 PendSV 的方法是:手工往 NVIC的 PendSV悬起寄存器中写 1。悬起后,如果优先级不够 高,则将缓期等待执行。

  PendSV的典型使用场合是在上下文切换时(在不同任务之间切换)。例如,一个系统中有两个就绪的任务,上下文切换被触发的场合可以是:

  1)执行一个系统调用
  2)系统滴答定时器(SYSTICK)中断,(轮转调度中需要)

 

  让我们举个简单的例子来辅助理解。假设有这么一个系统,里面有两个就绪的任务,并且通过SysTick异常启动上下文切换。但若在产生 SysTick 异常时正在响应一个中断,则 SysTick异常会抢占其 ISR。在这种情况下,OS是不能执行上下文切换的,否则将使中断请求被延迟,而且在真实系统中延迟时间还往往不可预知——任何有一丁点实时要求的系统都决不能容忍这 种事。因此,在 CM3 中也是严禁没商量——如果 OS 在某中断活跃时尝试切入线程模式,将触犯用法fault异常。

 

  为解决此问题,早期的 OS 大多会检测当前是否有中断在活跃中,只有在无任何中断需要响应 时,才执行上下文切换(切换期间无法响应中断)。然而,这种方法的弊端在于,它可以把任务切 换动作拖延很久(因为如果抢占了 IRQ,则本次 SysTick在执行后不得作上下文切换,只能等待下 一次SysTick异常),尤其是当某中断源的频率和SysTick异常的频率比较接近时,会发生“共振”, 使上下文切换迟迟不能进行。现在好了,PendSV来完美解决这个问题了。PendSV异常会自动延迟上下文切换的请求,直到 其它的 ISR都完成了处理后才放行。为实现这个机制,需要把 PendSV编程为最低优先级的异常。如果 OS检测到某 IRQ正在活动并且被 SysTick抢占,它将悬起一个 PendSV异常,以便缓期执行 上下文切换。

 

  使用 PendSV 控制上下文切换个中事件的流水账记录如下:

  1)任务 A呼叫 SVC来请求任务切换(例如,等待某些工作完成)
  2)OS接收到请求,做好上下文切换的准备,并且悬起一个 PendSV异常。
  3)当 CPU退出 SVC后,它立即进入 PendSV,从而执行上下文切换。
  4)当 PendSV执行完毕后,将返回到任务 B,同时进入线程模式。
  5)发生了一个中断,并且中断服务程序开始执行
  6)在 ISR执行过程中,发生 SysTick异常,并且抢占了该 ISR。
  7)OS执行必要的操作,然后悬起 PendSV异常以作好上下文切换的准备。
  8)当 SysTick退出后,回到先前被抢占的 ISR中,ISR继续执行
  9)ISR执行完毕并退出后,PendSV服务例程开始执行,并且在里面执行上下文切换
  10)当 PendSV执行完毕后,回到任务 A,同时系统再次进入线程模式。

 

2、临界段

  临界段用一句话概括就是一段在执行的时候不能被中断的代码段。 在 FreeRTOS 里面,这个临界段最常出现的就是对全局变量的操作, 全局变量就好像是一个枪把子,谁都可以对他开枪,但是我开枪的时候,你就不能开枪,否则就不知道是谁命中了靶子。 可能有人会说我可以在子弹上面做个标记,我说你能不能不要瞎扯淡。

  那么什么情况下临界段会被打断?一个是系统调度,还有一个就是外部中断。在FreeRTOS,系统调度,最终也是产生 PendSV 中断,在 PendSV Handler 里面实现任务的切换,所以还是可以归结为中断。 既然这样, FreeRTOS 对临界段的保护最终还是回到对中断的开和关的控制。即在执行临界段代码时需要关闭中断。

3、空闲任务与阻塞延时

  使用 RTOS 的很大优势就是榨干 CPU 的性能,永远不能让它闲着, 任务如果需要延时也就不能再让 CPU 空等来实现延时的效果。 RTOS 中的延时叫阻塞延时,即任务需要延时的时候, 任务会放弃 CPU 的使用权, CPU 可以去干其它的事情,当任务延时时间到,重新获取 CPU 使用权, 任务继续运行,这样就充分地利用了 CPU 的资源,而不是干等着。

  当任务需要延时,进入阻塞状态,那 CPU 又去干什么事情了?如果没有其它任务可以运行, RTOS 都会为 CPU 创建一个空闲任务,这个时候 CPU 就运行空闲任务。

  在FreeRTOS 中,空闲任务是系统在【启动调度器】 的时候创建的优先级最低的任务,空闲任务主体主要是做一些系统内存的清理工作,空闲任务不能被阻塞。鉴于空闲任务的这种特性,在实际应用中,当系统进入空闲任务的时候, 可在空闲任务中让单片机进入休眠或者低功耗等操作。

4、多优先级

  就绪列表 pxReadyTasksLists[ configMAX_PRIORITIES ]是一个数组, 数组里面存的是就绪任务的 TCB(准确来说是 TCB 里面的 xStateListItem 节点) ,数组的下标对应任务的优先级,优先级越低对应的数组下标越小。空闲任务的优先级最低,对应的是下标为 0 的链表。 图 10-1 演示的是就绪列表中有两个任务就绪, 优先级分别为 1 和 2,其中空闲任务没有画出来,空闲任务自系统启动后会一直就绪,因为系统至少得保证有一个任务可以运行。

  任务在创建的时候,会根据任务的优先级将任务插入到就绪列表不同的位置。相同优先级的任务插入到就绪列表里面的同一条链表中。  

  pxCurrenTCB 是一个全局的 TCB 指针,用于指向优先级最高的就绪任务的 TCB,即当前正在运行的 TCB。那么我们要想让任务支持优先级,即只要解决在任务切换(taskYIELD) 的时候,让 pxCurrenTCB 指向最高优先级的就绪任务的 TCB 就可以,现在我们要让 pxCurrenTCB 在任务切换的时候指向最高优先级的就绪任务的 TCB 即可,那问题的关键就是:如果找到最高优先级的就绪任务的 TCB。 FreeRTOS 提供了两套方法,一套是通用的,一套是根据特定的处理器优化过的,接下来我们重点讲解下这两个。

  4.1、 查找最高优先级的就绪任务相关代码

  寻找最高优先级的就绪任务相关代码在 task.c 中定义,具体见下面代码清单:

复制代码
#if ( configUSE_PORT_OPTIMISED_TASK_SELECTION == 0 )   (1)/* 查找最高优先级的就绪任务:通用方法 */
  /* uxTopReadyPriority 存的是就绪任务的最高优先级 */
  #define taskRECORD_READY_PRIORITY( uxPriority )\   (2)
  {\
    if( ( uxPriority ) > uxTopReadyPriority )\
    {\
      uxTopReadyPriority = ( uxPriority );\
    }\  
  } /* taskRECORD_READY_PRIORITY */

/*-----------------------------------------------------------*/

  #define taskSELECT_HIGHEST_PRIORITY_TASK()\ (3)
  {\
    UBaseType_t uxTopPriority = uxTopReadyPriority;\   (3-1)/*将 uxTopReadyPriority 的值暂存到局部变量 uxTopPriority, 接下来需要用到*/
    /* 寻找包含就绪任务的最高优先级的队列 */\ 
    while( listLIST_IS_EMPTY( &( pxReadyTasksLists[ uxTopPriority ] ) ) )\   (3-2)
    {\
     
      /*从最高优先级对应的就绪列表数组下标开始寻找当前链表下是否有任务存在,如果没有,则 uxTopPriority 减一操作,继续寻找下一个优先级对应的链表中是否有任务存在, 如果有则跳出 while 循环,表示找到了最高优先级的就绪任务。 \
      之所以可以采用从最高优先级往下搜索,是因为任务的优先级与就绪列表的下标是一一对应的,优先级越高,对应的就绪列表数组的下标越大。*/        \
 --uxTopPriority;\   

    }\
    /* 获取优先级最高的就绪任务的 TCB,然后更新到 pxCurrentTCB */\
    listGET_OWNER_OF_NEXT_ENTRY(pxCurrentTCB, &(pxReadyTasksLists[ uxTopPriority ]));\   (3-3)
    /* 更新 uxTopPriority 的值到 uxTopReadyPriority 
 */\
    uxTopReadyPriority = uxTopPriority;\ 
   } /* taskSELECT_HIGHEST_PRIORITY_TASK */(3-4)

  /*-----------------------------------------------------------*/

  /* 这两个宏定义只有在选择优化方法时才用,这里定义为空 */
  #define taskRESET_READY_PRIORITY( uxPriority )
  #define portRESET_READY_PRIORITY( uxPriority, uxTopReadyPriority )


#else /* configUSE_PORT_OPTIMISED_TASK_SELECTION */ (4)  /* 查找最高优先级的就绪任务:根据处理器架构优化后的方法 */

  #define taskRECORD_READY_PRIORITY( uxPriority ) \ (5)
  portRECORD_READY_PRIORITY( uxPriority, uxTopReadyPriority )

  /*-----------------------------------------------------------*/

  #define taskSELECT_HIGHEST_PRIORITY_TASK()\ (7)
  {\
    UBaseType_t uxTopPriority;\
    /* 寻找最高优先级 */\
    portGET_HIGHEST_PRIORITY( uxTopPriority, uxTopReadyPriority );\ (7-1)
    /* 获取优先级最高的就绪任务的 TCB,然后更新到 pxCurrentTCB */\
    listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) );\  (7-2)
  } /* taskSELECT_HIGHEST_PRIORITY_TASK() */

  /*-----------------------------------------------------------*/
  #if 0
    #define taskRESET_READY_PRIORITY( uxPriority )\ 
    {\
      if(listCURRENT_LIST_LENGTH(&(pxReadyTasksLists[( uxPriority)]))==(UBaseType_t)0)\
      {\
        portRESET_READY_PRIORITY( ( uxPriority ), ( uxTopReadyPriority ) );\
      }\
    }
  #else
    #define taskRESET_READY_PRIORITY( uxPriority )\    (6)/*taskRESET_READY_PRIORITY()用于根据传进来的形参(通常形参就是任务的优先级) 将变量 uxTopReadyPriority 的某个位清零。*/
    {\
      portRESET_READY_PRIORITY((uxPriority ), (uxTopReadyPriority));\
    }
  #endif

#endif /* configUSE_PORT_OPTIMISED_TASK_SELECTION */
复制代码

   代码清单(1):;查 找 最 高 优 先 级 的 就 绪 任 务 有 两 种 方 法 , 具 体 由configUSE_PORT_OPTIMISED_TASK_SELECTION 这个宏控制, 定义为 0 选择通用方法,定义为 1 选择根据处理器优化的方法,该宏默认在 portmacro.h 中定义为 1,即使用优化过的方法。

  4.1、通用方法

  (1) taskRECORD_READY_PRIORITY()

   代码清单(2):taskRECORD_READY_PRIORITY()用于更新 uxTopReadyPriority的值。 uxTopReadyPriority 是一个在 task.c 中定义的静态变量, 用于表示创建的任务的最高优先级, 默认初始化为 0,即空闲任务的优先级,具体实现见下面代码清单:

/* 空闲任务优先级宏定义,在 task.h 中定义 */
#define tskIDLE_PRIORITY ( ( UBaseType_t ) 0U )
/* 定义 uxTopReadyPriority,在 task.c 中定义 */
static volatile UBaseType_t uxTopReadyPriority = tskIDLE_PRIORITY;

  (2) taskSELECT_HIGHEST_PRIORITY_TASK()

  代码清单 (3): taskSELECT_HIGHEST_PRIORITY_TASK()用于寻找优先级最高的就绪任务, 实质就是更新 uxTopReadyPriority 和 pxCurrentTCB 的值。
  代码清单 (3-1): 将 uxTopReadyPriority 的值暂存到局部变量 uxTopPriority, 接下来需要用到。
  代码清单 (3-2):从最高优先级对应的就绪列表数组下标开始寻找当前链表下是否有任务存在,如果没有,则 uxTopPriority 减一操作,继续寻找下一个优先级对应的链表中是否有任务存在, 如果有则跳出 while 循环,表示找到了最高优先级的就绪任务。 之所以可以采用从最高优先级往下搜索,是因为任务的优先级与就绪列表的下标是一一对应的,优先级越高,对应的就绪列表数组的下标越大。
  代码清单 (3-3): 获取优先级最高的就绪任务的 TCB,然后更新到 pxCurrentTCB。
  代码清单 (3-4): 更新 uxTopPriority 的值到 uxTopReadyPriority。

  4.2、优化方法

  代码清单 (4):优化的方法,这得益于 Cortex-M 内核有一个计算前导零的指令CLZ,所谓前导零就是计算一个变量(Cortex-M 内核单片机的变量为 32 位)从高位开始第一次出现 1 的位的前面的零的个数。 比如: 一个 32 位的变量 uxTopReadyPriority, 其位 0、位 24 和 位 25 均 置 1 , 其 余 位 为 0 , 具 体 见 。 那 么 使 用 前 导 零 指 令 __CLZ(uxTopReadyPriority)可以很快的计算出 uxTopReadyPriority 的前导零的个数为 6。

  如果 uxTopReadyPriority 的每个位号对应的是任务的优先级,任务就绪时,则将对应的位置 1,反之则清零。那么上图就表示优先级 0、优先级 24 和优先级 25 这三个任务就绪,其中优先级为 25 的任务优先级最高。利用前导零计算指令可以很快计算出就绪任务中的最高优先级为: ( 31UL - ( uint32_t ) __clz( ( uxReadyPriorities ) ) ) = ( 31UL - ( uint32_t )6 ) = 25。

  (1)、taskRECORD_READY_PRIORITY()

  代码清单(5):taskRECORD_READY_PRIORITY()用于根据传进来的形参(通常形参就是任务的优先级) 将变量 uxTopReadyPriority 的某个位置 1。 uxTopReadyPriority 是一个在 task.c 中定义的静态变量,默认初始化为 0。与通用方法中用来表示创建的任务的最高优先级不一样,它在优化方法中担任的是一个优先级位图表的角色,即该变量的每个位对应任务的优先级,如果任务就绪,则将对应的位置 1,反之清零。根据这个原理,只需要计算出 uxTopReadyPriority 的前导零个数就算找到了就绪任务的最高优先级。 与taskRECORD_READY_PRIORITY() 作 用 相 反 的 是 taskRESET_READY_PRIORITY() 。

  taskRECORD_READY_PRIORITY()与 taskRESET_READY_PRIORITY()具体的实现见下面代码清单:

/*portmacro.h 中定义*/
#define portRECORD_READY_PRIORITY( uxPriority, uxReadyPriorities )\
 ( uxReadyPriorities ) |= ( 1UL << ( uxPriority ) )

#define portRESET_READY_PRIORITY( uxPriority, uxReadyPriorities )\
 ( uxReadyPriorities ) &= ~( 1UL << ( uxPriority ) )

   (2)、taskRESET_READY_PRIORITY()

  代码清单(6):taskRESET_READY_PRIORITY()用于根据传进来的形参(通常形参就是任务的优先级) 将变量 uxTopReadyPriority 的某个位清零。

  实际上根据优先级调用 taskRESET_READY_PRIORITY()函数复位 uxTopReadyPriority 变量中对应的位时, 要先确保就绪列表中对应该优先级下的链表没有任务才行。 但是我们当前实现的阻塞延时方案还是通过扫描就绪列表里面的 TCB 的延时变量 xTicksToDelay 来实现的, 还没有单独实现延时列表(任务延时列表将在下一个章节讲解),所以任务非就绪时暂时不能将任务从就绪列表移除,而是仅仅通过将任务优先级在变量 uxTopReadyPriority 中对应的位清零。 在下一章我们实现任务延时列表之后, 任务非就绪时, 不仅会将任务优先级在变量 uxTopReadyPriority 中对应的位清零,还会降任务从就绪列表删除。

  (3)、taskSELECT_HIGHEST_PRIORITY_TASK()

  代码清单 (7): taskSELECT_HIGHEST_PRIORITY_TASK()用于寻找优先级最高的就绪任务, 实质就是更新 uxTopReadyPriority 和 pxCurrentTCB 的值。
  代码清单 (7-1): 根据 uxTopReadyPriority 的值, 找到最高优先级, 然后更新到uxTopPriority 这个局部变量中。 portGET_HIGHEST_PRIORITY()具体的宏实现见代码清单10-4,在 portmacro.h 中定义。

#define portGET_HIGHEST_PRIORITY( uxTopPriority, uxReadyPriorities )\
uxTopPriority = ( 31UL - ( uint32_t ) __clz( ( uxReadyPriorities ) ) )

  代码清单 (7-2): 根据 uxTopPriority 的值, 从就绪列表中找到就绪的最高优先级的任务的 TCB,然后将 TCB 更新到 pxCurrentTCB。

5、任务延时列表

  在 FreeRTOS 中, 有一个任务延时列表(实际上有两个,为了方便讲解原理,我们假装合并为一个,其实两个的作用是一样的) ,当任务需要延时的时候, 则先将任务挂起,即先将任务从就绪列表删除,然后插入到任务延时列表,同时更新下一个任务的解锁时刻变量: xNextTaskUnblockTime 的值。

  xNextTaskUnblockTime 的值等于系统时基计数器的值 xTickCount 加上任务需要延时的值 xTicksToDelay。 当系统时基计数器 xTickCount 的值与 xNextTaskUnblockTime 相等时,就表示有任务延时到期了,需要将该任务就绪。 与 RT-Thread 和 μC/OS 在解锁延时任务时要扫描定时器列表这种时间不确定性的方法相比, FreeRTOS 这个 xNextTaskUnblockTime全局变量设计的非常巧妙。

  任务延时列表表维护着一条双向链表,每个节点代表了正在延时的任务,节点按照延时时间大小做升序排列。 当每次时基中断(SysTick 中断) 来临时, 就拿系统时基计数器的值 xTickCount 与下一个任务的解锁时刻变量 xNextTaskUnblockTime 的值相比较, 如果相等, 则表示有任务延时到期, 需要将该任务就绪, 否则只是单纯地更新系统时基计数器xTickCount 的值, 然后进行任务切换。

  当任务要延时的时候,将任务从就绪列表移除,然后添加到延时列表,同时将任务在优先级位图表 uxTopReadyPriority 中对应的位清除。在清除任务在优先级位图表 uxTopReadyPriority 中对应的位的时候, 与上一章不同的是需要判断就绪列表 pxReadyTasksLists[]在当前优先级下对应的链表的节点是否为 0,只有当该链表下没有任务时才真正地将任务在优先级位图表 uxTopReadyPriority 中对应的位清零。

6、时间片

  FreeRTOS 与 RT-Thread 和 μC/OS 一样,都支持时间片的功能。 所谓时间片就是同一个优先级下可以有多个任务,每个任务轮流地享有相同的 CPU 时间, 享有 CPU 的时间我们叫时间片。在 RTOS 中,最小的时间单位为一个 tick,即 SysTick 的中断周期,RT-Thread 和 μC/OS 可以指定时间片的大小为多个 tick,但是 FreeRTOS 不一样,时间片只能是一个 tick。 与其说 FreeRTOS 支持时间片,倒不如说它的时间片就是正常的任务调度。

7、任务状态

  7.1、任务状态的概念

  FreeRTOS 系统中的每一任务都有多种运行状态。系统初始化完成后,创建的任务就可以在系统中竞争一定的资源,由内核进行调度。

  任务状态通常分为以下四种:

  就绪( Ready):该任务在就绪列表中, 就绪的任务已经具备执行的能力,只等待调度器进行调度,新创建的任务会初始化为就绪态。
  运行(Running):该状态表明任务正在执行, 此时它占用处理器, FreeRTOS 调度器选择运行的永远是处于最高优先级的就绪态任务,当任务被运行的一刻,它的任务状态就变成了运行态。
  阻塞(Blocked): 如果任务当前正在等待某个时序或外部中断,我们就说这个任务处于阻塞状态,该任务不在就绪列表中。包含任务被挂起、任务被延时、任务正在等待信号量、读写队列或者等待读写事件等。
  挂起态(Suspended): 处于挂起态的任务对调度器而言是不可见的, 让一个任务进入挂起状态的唯一办法就是调用 vTaskSuspend()函数;而 把 一 个 挂 起 状态 的任 务 恢复的 唯 一 途 径 就 是 调 用 vTaskResume() 或 vTaskResumeFromISR()函数。

  我们可以这么理解挂起态与阻塞态的区别,当任务有较长的时间不允许运行的时候,我们可以挂起任务,这样子调度器就不会管这个任务的任何信息,直到我们调用恢复任务的 API 函数;而任务处于阻塞态的时候,系统还需要判断阻塞态的任务是否超时,是否可以解除阻塞。

  7.2、任务状态迁移

  图 (1): 创建任务→就绪态(Ready):任务创建完成后进入就绪态,表明任务已准备就绪,随时可以运行,只等待调度器进行调度。
  图 (2): 就绪态→运行态(Running):发生任务切换时,就绪列表中最高优先级的任务被执行,从而进入运行态。
  图(3): 运行态→就绪态:有更高优先级任务创建或者恢复后,会发生任务调度,此刻就绪列表中最高优先级任务变为运行态,那么原先运行的任务由运行态变为就绪态,是 CPU 使用权被更高优先级的任务抢占了)。
  图(4): 运行态→阻塞态( Blocked):正在运行的任务发生阻塞(挂起、延时、读信号量等待)时,该任务会从就绪列表中删除,任务状态由运行态变成阻塞态,然后发生任务切换,运行就绪列表中当前最高优先级任务。
  图(5): 阻塞态→就绪态:阻塞的任务被恢复后(任务恢复、延时时间超时、读信号量超时或读到信号量等),此时被恢复的任务会被加入就绪列表,从而由阻塞态变成就绪态;如果此时被恢复任务的优先级高于正在运行任务的优先级,则会发生任务切换,将该任务将再次转换任务状态,由就绪态变成运行态。
  图(6) (7) (8): 就绪态、阻塞态、运行态→挂起态(Suspended):任务可以通过调用 vTaskSuspend() API 函数都可以将处于任何状态的任务挂起,被挂起的任务得不到CPU 的使用权,也不会参与调度,除非它从挂起态中解除。
  图(9): 挂起态→就绪态: 把 一 个 挂 起 状态 的 任 务 恢复的 唯 一 途 径 就 是调 用 vTaskResume() 或 vTaskResumeFromISR() API 函数,如果此时被恢复任务的优先级高于正在运行任务的优先级,则会发生任务切换,将该任务将再次转换任务状态,由就绪态变成运行态。

8、全局变量、消息队列、信号量、

  8.1、信号量、消息队列和全局变量的区别

      在操作系统任务编程中,解决任务间通信问题,可以使用全局变量、信号量或者消息队列来完成。那么它们有什么区别,在遇到任务间通讯时,该怎样选择用哪一种方式呢?

  (1)任务间通讯内涵

  任务间通讯的内涵种类有两种,如下:

  1)只通知有事件发生,而没有内容,或者说不需要内容。例如任务A通知任务B“定时12:00时间到”,可见任务A通知任务B的是一个事件标志,不需要事件内容。

  2)除了通知有事件发生,还要告诉发生了什么事。例如任务A通知任务B“将风扇的风速调到“3.5米/秒”,可见除了要通知任务A给任务B发送了控制风扇的命令,还要告诉具体的命令内容是什么。

  (2)三者之间的相似处和区别

  三者相似之处在于都是可以解决任务间通信的问题,不过使用时要注意场景需求,选用恰当的方式实现。

  三者之间的区别在于:

  信号量只通知接收方某个事件标志的发生,但不能传递具体事件内容。这种方式属于操作系统API调用,可以引起任务的挂起和回复。

  消息队列通知接收方某个事件的发生,并且告知接收方事件内容。这种方式属于操作系统API调用,可以引起任务的挂起和回复。

  全局变量可以用于通知事件标志的发生,也可以用于通知事件发生并且传递事件内容。由于全局变量不是操作系统API调用,所以使用全局变量不会引起任务的挂起和恢复。而任务对事件的获知,一般需要不断查询该全局变量的值是否发生变化。

  相对来说,使用信号量和消息队列进行任务间通信要比使用全局变量更加安全,对任务间通信联系变量的隔离要高。

  8.2、信号量

  (1)互斥信号量

   互斥信号量的申请与释放是要在同一个任务中进行的,不能在一个任务中申请而在另一个任务中释放。

   互斥信号量主要解决的是,我在用的时候,别人都不能用。举个例子,我在像一段内存中写数据的时候,不允许别人去写和读的,这时候就需要互斥信号量,写之前获取信号量,写完之后再释放互斥信号量。

  (2)二值信号量

  二值信号量允许在一个任务中申请,在另外一个任务中释放。

  二值信号量主要解决的是任务同步的问题。举个例子,一个任务用于处理UART的数据,UART中断中接受数据,当任务处理数据时就获取信号量,中断接受到数据就释放信号量,这样就可以使得中断与任务接受和处理协同处理。

 
 

 

posted @ 2022-04-30 14:59  孤情剑客  阅读(1197)  评论(0)    收藏  举报