FreeRTOS应用学习笔记

任务管理

3.1任务的概念

对于整个单片机程序,我们称之为application,应用程序。 1

使用FreeRTOS时,我们可以在application中创建多个任务(task),有些文档把任务也称为线程(thread)。

3.2 任务创建与删除

3.2.1 什么是任务

在FreeRTOS中,任务就是一个函数,原型如下:

1
void ATaskFunction( void *pvParameters );

要注意以下几点:

  1. 这个函数不能返回
  2. 同一个函数,可以用来创建多个任务;换句话说,多个任务可以运行同一个函数
  3. 函数内部,尽量使用局部变量
  4. 每个任务都有自己的栈
  5. 每个任务运行这个函数时
  6. 任务A的局部变量放在任务A的栈里、任务B的局部变量放在任务B的栈里
  7. 不同任务的局部变量,有自己的副本
  8. 函数使用全局变量、静态变量的话
  9. 只有一个副本:多个任务使用的是同一个副本
  10. 要防止冲突(后续会讲)

下面是一个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void ATaskFunction( void *pvParameters )
{
/* 对于不同的任务,局部变量放在任务的栈里,有各自的副本 */
int32_t lVariableExample = 0;

/* 任务函数通常实现为一个无限循环 */
for( ;; )
{
/* 任务的代码 */
}

/* 如果程序从循环中退出,一定要使用vTaskDelete删除自己
* NULL表示删除的是自己
*/
vTaskDelete( NULL );

/* 程序不会执行到这里, 如果执行到这里就出错了 */
}

3.2.2 创建任务

创建任务时使用的函数如下:

1
2
3
4
5
6
BaseType_t xTaskCreate( TaskFunction_t pxTaskCode, // 函数指针, 任务函数
const char * const pcName, // 任务的名字
const configSTACK_DEPTH_TYPE usStackDepth, // 栈大小,单位为word,10表示40字节
void * const pvParameters, // 调用任务函数时传入的参数
UBaseType_t uxPriority, // 优先级
TaskHandle_t * const pxCreatedTask ); // 任务句柄, 以后使用它来操作这个任务

参数说明:

参数 描述
pvTaskCode 函数指针,可以简单地认为任务就是一个C函数。
它稍微特殊一点:永远不退出,或者退出时要调用”vTaskDelete(NULL)”
pcName 任务的名字,FreeRTOS内部不使用它,仅仅起调试作用。
长度为:configMAX_TASK_NAME_LEN
usStackDepth 每个任务都有自己的栈,这里指定栈大小。
单位是word,比如传入100,表示栈大小为100 word,也就是400字节。
最大值为uint16_t的最大值。
怎么确定栈的大小,并不容易,很多时候是估计。
精确的办法是看反汇编码。
pvParameters 调用pvTaskCode函数指针时用到:pvTaskCode(pvParameters)
可将任务参数通过该指针传入任务函数中
uxPriority 优先级范围:0~(configMAX_PRIORITIES – 1)
数值越小优先级越低,如果传入过大的值,
xTaskCreate会把它调整为(configMAX_PRIORITIES – 1)
pxCreatedTask 用来保存xTaskCreate的输出结果:task handle。
以后如果想操作这个任务,比如修改它的优先级,就需要这个handle。如果不想使用该handle,可以传入NULL。
返回值 成功:pdPASS;
失败:errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY(失败原因只有内存不足)
注意:文档里都说失败时返回值是pdFAIL,这不对。
pdFAIL是0,errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY是-1。

*在已经内置FreeRTOS的系统中,无需启用调度器,直接在需要的位置创建任务即可

3.2.4 任务的删除

删除任务时使用的函数如下:

1
void vTaskDelete( TaskHandle_t xTaskToDelete );

参数说明:

参数 描述
pvTaskCode 任务句柄,使用xTaskCreate创建任务时可以得到一个句柄。
也可传入NULL,这表示删除自己。

3.3任务优先级和系统Tick

3.3.1 任务优先级

优先级的取值范围是:0~(configMAX_PRIORITIES – 1),数值越大优先级越高。

FreeRTOS的调度器可以使用2种方法来快速找出优先级最高的、可以运行的任务。使用不同的方法时,configMAX_PRIORITIES 的取值有所不同。

在学习调度方法之前,你只要初略地知道:
- FreeRTOS会确保最高优先级的、可运行的任务,马上就能执行
- 对于相同优先级的、可运行的任务,轮流执行

3.3.2 Tick

对于同优先级的任务,它们“轮流”执行。怎么轮流?你执行一会,我执行一会。

“一会”怎么定义?

人有心跳,心跳间隔基本恒定。

FreeRTOS中也有心跳,它使用定时器产生固定间隔的中断。这叫Tick、滴答,比如每10ms发生一次时钟中断。

有了Tick的概念后,我们就可以使用Tick来衡量时间了,比如:

1
2
3
4
vTaskDelay(2);  // 等待2个Tick,假设configTICK_RATE_HZ=100, Tick周期时10ms, 等待20ms

// 还可以使用pdMS_TO_TICKS宏把ms转换为tick
vTaskDelay(pdMS_TO_TICKS(100)); // 等待100ms

注意,基于Tick实现的延时并不精确,比如vTaskDelay(2)的本意是延迟2个Tick周期,有可能经过1个Tick多一点就返回了。

使用vTaskDelay函数时,建议以ms为单位,使用pdMS_TO_TICKS把时间转换为Tick。

这样的代码就与configTICK_RATE_HZ无关,即使配置项configTICK_RATE_HZ改变了,我们也不用去修改代码。

3.3.4 修改优先级

使用uxTaskPriorityGet来获得任务的优先级:

1
UBaseType_t uxTaskPriorityGet( const TaskHandle_t xTask );

使用参数xTask来指定任务,设置为NULL表示获取自己的优先级。

使用vTaskPrioritySet 来设置任务的优先级:

1
2
void vTaskPrioritySet( TaskHandle_t xTask,
UBaseType_t uxNewPriority );

使用参数xTask来指定任务,设置为NULL表示设置自己的优先级;
参数uxNewPriority表示新的优先级,取值范围是0~(configMAX_PRIORITIES – 1)。

3.4 任务状态

以前我们很简单地把任务的状态分为2中:运行(Runing)、非运行(Not Running)。

对于非运行的状态,还可以继续细分:

  • 阻塞状态(Blocked)
  • 暂停状态(Suspended)
  • 就绪状态(Ready)

3.4.1 阻塞状态(Blocked)

在实际产品中,我们不会让一个任务一直运行,而是使用”事件驱动”的方法让它运行:

  • 任务要等待某个事件,事件发生后它才能运行
  • 在等待事件过程中,它不消耗CPU资源
  • 在等待事件的过程中,这个任务就处于阻塞状态(Blocked)

在阻塞状态的任务,它可以等待两种类型的事件:

  • 时间相关的事件
    • 可以等待一段时间:我等2分钟
    • 也可以一直等待,直到某个绝对时间:我等到下午3点
  • 同步事件:这事件由别的任务,或者是中断程序产生
    • 例子1:任务A等待任务B给它发送数据
    • 例子2:任务A等待用户按下按键
    • 同步事件的来源有很多(这些概念在后面会细讲):
      • 队列(queue)
      • 二进制信号量(binary semaphores)
      • 计数信号量(counting semaphores)
      • 互斥量(mutexes)
      • 递归互斥量、递归锁(recursive mutexes)
      • 事件组(event groups)
      • 任务通知(task notifications)

在等待一个同步事件时,可以加上超时时间。比如等待队里数据,超时时间设为10ms:

10ms之内有数据到来:成功返回
10ms到了,还是没有数据:超时返回

3.4.2 暂停状态(Suspended)

FreeRTOS中的任务可以进入暂停状态,唯一的方法是通过vTaskSuspend函数。函数原型如下:

1
void vTaskSuspend( TaskHandle_t xTaskToSuspend );

参数xTaskToSuspend表示要暂停的任务,如果为NULL,表示暂停自己。

要退出暂停状态,只能由别人来操作:

别的任务调用:vTaskResume
中断程序调用:xTaskResumeFromISR
实际开发中,暂停状态用得不多。

3.4.3 就绪状态(Ready)

这个任务完全准备好了,随时可以运行:只是还轮不到它。这时,它就处于就绪态(Ready)。

3.4.4 完整的状态转换图

状态转换图

3.5 Delay函数

3.5.1 两个Delay函数

有两个Delay函数:

  • vTaskDelay:至少等待指定个数的Tick Interrupt才能变为就绪状态
  • vTaskDelayUntil:等待到指定的绝对时刻,才能变为就绪态。

这2个函数原型如下:

1
2
3
4
5
6
7
8
void vTaskDelay( const TickType_t xTicksToDelay ); /* xTicksToDelay: 等待多少给Tick */

/* pxPreviousWakeTime: 上一次被唤醒的时间
* xTimeIncrement: 要阻塞到(pxPreviousWakeTime + xTimeIncrement)
* 单位都是Tick Count
*/
BaseType_t xTaskDelayUntil( TickType_t * const pxPreviousWakeTime,
const TickType_t xTimeIncrement );
  • 使用vTaskDelay(n)时,进入、退出vTaskDelay的时间间隔至少是n个Tick中断
  • 使用xTaskDelayUntil(&Pre, n)时,前后两次退出xTaskDelayUntil的时间至少是n个Tick中断退出xTaskDelayUntil时任务就进入的就绪状态,一般都能得到执行机会所以可以使用xTaskDelayUntil来让任务周期性地运行

3.6 空闲钩子任务

3.7 调度算法