天天看点

FreeRTOS 从入门到精通5--详解任务管理

FreeRTOS 从入门到精通5--详解任务管理

任务(Task)的介绍

在FreeRTOS中,线程和任务的概念是相同的。每个任务就是一个线程,有着自己的一个程序。函数的模型示例如下所示,通常情况下包含一个不会退出的循环体。

void TaskFunction( void *pvParameters )
{
  int32_t Mustermann = 10; 
  for(;;)
  {
  }
  vTaskDelete(NULL);  
}      

这个任务函数不能有返回值(即使用return语句),不然会导致异常。如果不需要这个任务的话,必须要用语句显示地删除这个任务(比如调用vTaskDelete这个函数)。每一个创建的任务有自己的栈区。

下图展示了一个温度监测系统的案例。一个简单的温度监测系统可以由三个任务构成。第一个任务是温度传感器获取数据任务,这个任务的优先级最高,定期执行获取温度数据。第二个任务是传感器数据处理以及显示数据任务,等待第一个任务获取的数据并进行处理。第三个任务是用户的按键处理程序,优先级最低,可以对用户的按键输入进行处理执行不同的功能。

FreeRTOS 从入门到精通5--详解任务管理

一个应用可以包含多个任务程序,然而微处理器的计算资源有限,通常只有一个核心,所以在任意时间只能有一个任务被执行,仅由FreeRTOS的调度器(Scheduler)决定。 

任务的创建

任务由xTaskCreate()这个函数创建,这一个非常重要,频繁被调用的函数,需要牢记下每个参数的含义。

BaseType_t xTaskCreate( TaskFunction_t pvTaskCode,
                        const char * const pcName,
                        uint16_t usStackDepth,
                        void *pvParameters,
                        UBaseType_t uxPriority,
                        TaskHandle_t *pxCreatedTask );      

pvTaskCode 这是一个函数指针,指向执行任务的函数

pcName 任务的描述名称,方便调试,不用的话可以设为Null

usStackDepth 每个任务有自己的栈空间,这里根据任务占用需求设置栈空间的大小

pvParameters 用于传递给任务的参数,不用的话可以设为Null

uxPriority 设置任务的优先级,范围由0到(configMAX_PRIORITIES – 1)。数值越大,等级越高

pxCreatedTask 任务的具柄(handle),通过具柄可以对任务进行设置,比如改变任务优先级等,不用可以设为Null

函数的返回值有两个pdPass和pdFail,pdPass表示任务创建成功,相反pdFail表示创建失败,创建失败的原因大多是因为系统没有足够的堆空间来保存任务的数据。

下图表示了建立了两个任务程序之后RAM中的占用情况。图中的堆空间(Heap,更多堆栈相关内容请参考上一讲)在设置好后,每执行xTaskCreate()创建一个任务便会在堆空间中开辟一个TCB块(包含任务函数属性)和一个存放数据的STACK栈区(栈区大小由usStackDepth参数决定)

FreeRTOS 从入门到精通5--详解任务管理

温度监测系统的案例实现

这里我们简单实现上文说的温度监测系统的三个任务,创建并执行这三个任务。

首先,我们定义一个全局变量temp用于存放温度信息。定义全局变量其实不太安全,这里为了简便就使用了

int temp = 0;//全局变量temp用于存放温度信息      

然后分别实现三个任务

void vTask1(void const * argument)//温度传感器数据获取任务
{
  //模拟温度传感器数据
  int test=0;
  for(;;){
    temp = 20+test%20;
    test++;
    if(test>30000)
    {
      test=0;
    }
   }
   osDelay(1000);
 }
void vTask2(void const * argument)//数据处理以及屏幕显示
{
   char text[]= "Temperature:";
   char tempText[10];
   TM_LCD_SetFont(&TM_Font_11x18);
   TM_FONT_GetStringSize(text, &FontSize, &TM_Font_11x18);
   TM_LCD_Fill(COLOR_RED);
   for(;;)
   {
     //液晶屏显示温度数据
     TM_LCD_SetXY((TM_LCD_GetWidth() - FontSize.Width) / 2, FontSize.Height * 5);
     TM_LCD_Puts(text);
     sprintf(tempText, "%d", temp);
     TM_LCD_SetXY((TM_LCD_GetWidth() - FontSize.Width) / 2, FontSize.Height * 7);
     TM_LCD_Puts(strncat(tempText," Degree",7));
     osDelay(1000);
   }
}
void vTask3(void const * argument)//用户按键处理
{
    char text[12];
    char userInput[2];
    for(;;)
    {
      //模拟用户输入并显示
      sprintf(text, "%s", "User Input");
      sprintf(userInput, "%d", temp%10);
      TM_LCD_SetXY((TM_LCD_GetWidth() - FontSize.Width) / 2, FontSize.Height * 9);
      TM_LCD_Puts(strncat(text,userInput,2));
      osDelay(1000);
     }
}      

最后我们在main函数中创建任务并运行

int main(void)
{
  ......
  xTaskCreate( vTask1, "get sensor data", 200, NULL, 3, NULL );
  xTaskCreate( vTask2, "show sensor data", 200, NULL, 2, NULL );
  xTaskCreate( vTask3, "get user input", 200, NULL, 1, NULL );
  /* Start scheduler */
  vTaskStartScheduler();
  while (1)
  {
  }
}      

程序的运行视频如下,系统每过1秒会采集温度数据并输出到屏幕上,三个任务独立运行,以后如果维护或者添加功能的话只要修改对应的任务程序就好了,并不会影响其它功能模块的运行。相对于Java中面向对象的编程思路,在嵌入式编程中我们要有模块化编程的思路。

任务的优先级

任务的优先级可以用vTaskPrioritySet()函数设置。FreeRTOSConfig.h头文件中的configMAX_PRIORITIES可以设置最高优先级的值。0代表最低优先级, (configMAX_PRIORITIES – 1)代表最高优先级。

有两种影响设置configMAX_PRIORITIES的方式

  • 通用方式

configUSE_PORT_OPTIMISED_TASK_SELECTION这个值设为0时为通用方式。采用通用方式时FreeRTOS不会限制configMAX_PRIORITIES的最大值。通常建议把configMAX_PRIORITIES的值设置得小一点,因为值越高,占用的ram越多,程序的最坏运行时间(worst case execution time)会更长。

  • 架构优化方式

configUSE_PORT_OPTIMISED_TASK_SELECTION这个值设为1时为架构优化方式。架构优化方式采用了平台相关的汇编代码,比通用方式更快,configMAX_PRIORITIES的值不会影响程序的最坏运行时间。在这种方式下,configMAX_PRIORITIES的最大值不能超过32。因为是平台相关,不是所有的单片机支持这个方式。

任务的调度

FreeRTOS对任务的调度采用时间片(time slicing)的调度方式。时间片,顾名思义,把一段时间等分成了很多个时间段,在每一个时间段保证优先级最高的任务能执行,同时如果几个任务拥有相等的优先级,则它们会轮流使用每个时间段占用CPU资源。调度器会在每个时间片结束的时候通过周期中断(tick interrupt)执行一次,选择哪个任务在下一个时间片会运行。

时间片的大小由configTICK_RATE_HZ这个参数设置。如果configTICK_RATE_HZ设置为10HZ,则时间片的大小为100ms。configTICK_RATE_HZ的值由应用需求决定,通常设为100HZ(时间片大小相应为10ms)。

FreeRTOS 从入门到精通5--详解任务管理

在上图任务调度的演示中,Kernel表示系统内核即调度程序,Task1和Task2是两个优先级相同的任务。t1到t2是一个时间片,t2到t3是另一个时间片。在每一个时间片快结束的时候,调度程序通过周期中断(tick interrupt)被调用并选择在下一个时间片要执行的任务(红色部分代表调度程序Kernel在运行)。因为两个任务的优先级相同,调度程序会让两个任务轮流占用时间片进行运行(蓝色部分代表Task1在运行,绿色部分代表Task2在运行)。

通过pdMS_TO_TICKS()这个函数可以把时间转换成节拍数(一个节拍代表一个时间片),调用这个函数可以保证configTICK_RATE_HZ的值不同时,时间是一致的。

介绍任务的运行模式,同时与可编程控制器(PLC)以及安卓系统的运行模式进行比较。我在德国读书时专业是嵌入式开发,工作后从事的是西门子PCS7过程控制系统的编程,业余学习了安卓开发。在学习的过程中,我逐渐意识到单片机,PLC和智能手机本质上上都是一类控制器,很多对于系统开发的理念都是相近互通的,基于此便产生一种想法,想在介绍FreeRTOS的任务管理时,同时对比下PLC和安卓的任务管理机制。读者可以触类旁通推及其他类似的实时系统。

首先,我们先看一下FreeRTOS的任务状态的转化图

FreeRTOS 从入门到精通5--详解任务管理

其中每个状态的含义如下

  • 阻塞状态(Blocked)当任务等待某个事件或信号的时候处于此状态
  • 挂起状态(Suspended)当任务被vTaskSuspend()函数禁止运行的时候处于此状态
  • 就绪状态(Ready)当任务没有被阻塞或者挂起等待运行的时候处于此状态
  • 运行状态(Running)当任务被内核调度执行的时候处于此状态

在系统初始化所有任务被创建的时候,任务一开始都处于就绪状态(Ready),然后内核调度器开始调度首先选择执行优先级最高的任务,此时被执行的任务处于运行状态(Running)。当任务执行延时命令或者等待某个同步事件的时候便交出了自己的运行权,此时将处于阻塞状态(Blocked)。在任务运行的时候,它可以通过vTaskSuspend()函数将其他任务或者自身挂起进入挂起状态(Suspended)。被挂起的任务只有通过vTaskResume()函数恢复成就绪状态(Ready)。

接下来,让我们看看程序在可编程逻辑器(PLC)中的运行机制。

FreeRTOS 从入门到精通5--详解任务管理

在PLC中,一般任务都是写好后顺序执行的。在读取输入阶段,PLC扫描所有输入端子,并将各输入端的通/断状态存入相对应的输入映像寄存器中,刷新输入映像寄存器的值。CPU对用户程序按顺序进行扫描,逐条执行程序指令。在用户程序执行完毕后,PLC将输出映像寄存器中的通/断状态送到输出锁存器中,通过输出端子驱动用户输出设备或带动负载。在这里可以把PLC中运行的任务看作是优先级相同的任务,任务之间不会互相抢占运行的权利,内核调度器类似合作式调度(Co-operative Scheduling)- 按照预设的顺序先后执行控制任务。因为PLC程序的运行都是可预测的,所以PLC更适合对稳定性实时性要求更高的工业领域。

最后,让我们看看安卓系统中任务的运行模式,有个专有名词叫生命周期。在安卓编程中,每个活动(Activity)包含一个画面和对应的程序,这里可以类比成一个任务。活动通过OnCreate()函数创建,通过OnStart()函数启动,当被其他活动抢占之后会通过onPause()函数暂停并通过onStop()函数停止(此时活动将在手机屏幕上消失)。被停止的活动通过OnRestart()函数重新运行,或者通过onDestroy()函数被销毁。

FreeRTOS 从入门到精通5--详解任务管理

FreeRTOS中任务的运行状态机制和安卓编程中活动的生命周期比较相似。FreeRTOS通过xTaskCreate()函数创建任务,相当于安卓的onCreate()函数;FreeRTOS通过vTaskSuspend()函数挂起任务,相当于安卓的onPause()函数;FreeRTOS通过vTaskResume()函数恢复任务到就绪状态,相当于安卓的onResume()函数;FreeRTOS通过vTaskDelete()函数删除任务,相当于安卓的onDestroy()函数。通过这些相似之处可以看到,学好FreeRTOS对于安卓开发也有裨益,而玩转了嵌入式和安卓编程,融会贯通硬件和软件开发,你将立于紫禁城之巅,成为每个产品经理最缺的那个程序员大神。

FreeRTOS的调度算法及配置

抢占式时间片调度(Prioritized Pre-emptive Scheduling with Time Slicing)

这是比较通用的调度方式,上一篇提到的温度检测系统采用的就是这种方式。内核调度器在每个时间片结束的时候执行一次,选择处于就绪状态的任务中优先级最高的任务置于下一个时间片执行。如果优先级相同的话则交替执行。此时,FreeRTOSConfig.h头文件的设置如下:

configUSE_PREEMPTION(允许抢占) 1

configUSE_TIME_SLICING(采用时间片) 1

抢占式无时间片调度(Prioritized Pre-emptive Scheduling without Time Slicing)

在这种调度方式下,因为没有采取时间片,所以调度器的执行开销会比较小。如果两个任务的优先级相同的话,在抢占式时间片调度下,两个任务会交替运行;然而在抢占式无时间片调度下,当前运行的任务会一直运行,直到它进入阻塞或者挂起状态,另一个相同优先级的任务才会运行。高优先级的任务会抢占低优先任务。此时,FreeRTOSConfig.h头文件的设置如下:

configUSE_PREEMPTION(允许抢占) 1

configUSE_TIME_SLICING(采用时间片) 0

合作式调度(Co-operative Scheduling)