跳到主要内容

第8章 任务管理

在本章中,会涉及如下内容:

  • FreeRTOS如何给每个任务分配CPU时间
  • 如何选择某个任务来运行
  • 任务优先级如何起作用
  • 任务有哪些状态
  • 如何实现任务
  • 如何使用任务参数
  • 怎么修改任务优先级
  • 怎么删除任务
  • 怎么实现周期性的任务
  • 如何使用空闲任务

8.1 基本概念

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

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

以日常生活为例,比如这个母亲要同时做两件事:

  • 喂饭:这是一个任务
  • 回信息:这是另一个任务

这可以引入很多概念:

  • 任务状态(State):
    • 当前正在喂饭,它是running状态;另一个"回信息"的任务就是"not running"状态
    • "not running"状态还可以细分:
    • ready:就绪,随时可以运行
    • blocked:阻塞,卡住了,母亲在等待同事回信息
    • suspended:挂起,同事废话太多,不管他了
  • 优先级(Priority)
    • 我工作生活兼顾:喂饭、回信息优先级一样,轮流做
    • 我忙里偷闲:还有空闲任务,休息一下
    • 厨房着火了,什么都别说了,先灭火:优先级更高
  • 栈(Stack)
    • 喂小孩时,我要记得上一口喂了米饭,这口要喂青菜了
    • 回信息时,我要记得刚才聊的是啥
    • 做不同的任务,这些细节不一样
    • 对于人来说,当然是记在脑子里
    • 对于程序,是记在栈里
    • 每个任务有自己的栈
  • 事件驱动
    • 孩子吃饭太慢:先休息一会,等他咽下去了、等他提醒我了,再喂下一口
  • 协助式调度(Co-operative Scheduling)
    • 你在给同事回信息
      • 同事说:好了,你先去给小孩喂一口饭吧,你才能离开
      • 同事不放你走,即使孩子哭了你也不能走
    • 你好不容易可以给孩子喂饭了
      • 孩子说:好了,妈妈你去处理一下工作吧,你才能离开
      • 孩子不放你走,即使同事连发信息你也不能走

这涉及很多概念,后续章节详细分析。

8.2 任务创建与删除

8.2.1 什么是任务

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

void ATaskFunction( void *pvParameters );

要注意的是:

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

下面是一个示例:

void ATaskFunction( void *pvParameters )
{
/* 对于不同的任务,局部变量放在任务的栈里,有各自的副本 */
int32_t lVariableExample = 0;

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

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

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

8.2.2 创建任务

创建任务时可以使用2个函数:动态分配内存、静态分配内存。

使用动态分配内存的函数如下:

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。

使用静态分配内存的函数如下:

TaskHandle_t xTaskCreateStatic (
TaskFunction_t pxTaskCode, // 函数指针, 任务函数
const char * const pcName, // 任务的名字
const uint32_t ulStackDepth, // 栈大小,单位为word,10表示40字节
void * const pvParameters, // 调用任务函数时传入的参数
UBaseType_t uxPriority, // 优先级
StackType_t * const puxStackBuffer, // 静态分配的栈,就是一个buffer
StaticTask_t * const pxTaskBuffer // 静态分配的任务结构体的指针,用它来操作这个任务
);

相比于使用动态分配内存创建任务的函数,最后2个参数不一样:

参数描述
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)
puxStackBuffer静态分配的栈内存,比如可以传入一个数组, 它的大小是usStackDepth*4。
pxTaskBuffer静态分配的StaticTask_t结构体的指针
返回值成功:返回任务句柄; 失败:NULL

8.2.3 任务的删除

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

void vTaskDelete( TaskHandle_t xTaskToDelete );

参数说明:

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

怎么删除任务?举个不好的例子:

  • 自杀:vTaskDelete(NULL)
  • 被杀:别的任务执行 vTaskDelete(pvTaskCode) ,pvTaskCode是自己的句柄
  • 杀人:执行 vTaskDelete(pvTaskCode) ,pvTaskCode是别的任务的句柄

8.3 示例1: 创建任务

8.3.1 使用RASC创建任务

代码为:0801_create_task

使用动态、静态分配内存的方式,分别创建多个任务:监测遥控器并在LCD上显示、LED闪烁、使用无源蜂鸣器播放音乐。

使用RASC可以在图形界面创建任务,但是本课程不想依赖于RASC工具,所以我们自己编写代码来创建任务。ra_gen/main.c是由RASC自动生成的,里面没有添加用户代码的地方。

我们先在图形界面创建一个“user_tasks”,如下图所示:

<img src="https://photos.100ask.net/renesas-docs/DShanMCU_RA6M5/FreeRTOS/chapter-8/image2.png" />

上图中,②处用来设置任务的优先级,把user_tasks任务的优先级设置为2,以后在user_tasks里创建其他任务时把优先级设置为1,这样user_tasks就可以不被打扰地创建完所有任务后,其他任务才能运行。

③处指定user_tasks任务的栈是动态分配的,这样当user_tasks执行完毕后,它的栈可以被回收。

然后仿照《5.2.2 添加堆分配算法模块》添加堆算法。

生成代码后,main.c的代码如下:

int main(void)
&#123;
......
/* Init RTOS tasks. */
user_tasks_create();

/* Start the scheduler. */
vTaskStartScheduler();
return 0;
&#125;

第5行的“user_tasks_create”函数里,会创建一个名为“user_tasks”的任务,它的入口函数是“user_tasks_func”(在user_tasks.c里),它会去调用“user_tasks_entry”(在user_tasks_entry里)。

我们在user_tasks_entry函数里添加自己的代码:

1.3.2 手工创建任务

在user_tasks_entry函数里,分别使用静态分配内存、动态分配内存的方法,创建3个任务:LED任务(让LED闪烁)、音乐任务(通过无源蜂鸣器播放音乐)、IRDA任务(显示红外遥控器的键值)。

代码如下:

/* user_tasks entry function */
static StackType_t g_pucStackOfLightTask[128];
static StaticTask_t g_TCBofLightTask;
static TaskHandle_t xLightTaskHandle;

void user_tasks_entry(void * pvParameters)
&#123;
BaseType_t ret;
TaskHandle_t xSoundTaskHandle;
extern void led_thread_entry(void * pvParameters);
extern void irda_thread_entry(void *params);

FSP_PARAMETER_NOT_USED(pvParameters);

/* TODO: add your own code here */
UARTDrvInit();

/* 创建任务: 声 */
extern void PlayMusic(void *params);
ret = xTaskCreate(PlayMusic, "SoundTask", 128, NULL, 1, &xSoundTaskHandle);

/* 创建任务: 光 */
xLightTaskHandle = xTaskCreateStatic(led_thread_entry, "LightTask", 128, NULL, 1, g_pucStackOfLightTask, &g_TCBofLightTask);

/* 创建任务: IRDA+LCD */
ret = xTaskCreate(irda_thread_entry, "IrdaTask", 128, NULL, 1, NULL);

vTaskDelete(NULL);
&#125;

第20、23、26行分别创建了3个任务。其中第23行使用静态分配内存的方式创建任务,需要提供栈、TCB结构体:第2、3行使用全局变量定义了TCB结构体、数组(用作栈)。

“user_tasks”任务执行完后,不能直接返回,使用第28行的代码结束任务。

8.3.3 上机实验

按下图接线:

100ask

烧录程序后,可以看到LED闪烁、LCD屏幕有输出并且会显示遥控器的键值,同时无源蜂鸣器会播放音乐。

缺点:音乐播放得有点卡顿。

8.4 示例2: 使用参数创建任务

代码为:0802_create_task_use_params,关键源文件“applications/lcd_app.c”。

创建多个任务时,可以使用同一个函数,怎么体现它们的差别?

● 栈不同:每个任务有有自己的栈,即使使用同一个函数,但是栈里面存储的调用过程、局部变量是不一样的。 ● 创建任务时可以传入不同的参数

我们创建3个任务,使用同一个函数,但是在LCD上打印不一样的信息。

struct TaskPrintInfo &#123;
uint8_t x;
uint8_t y;
char name[16];
&#125;;

static struct TaskPrintInfo g_Task1Info = &#123;0, 0, "Task1"&#125;;
static struct TaskPrintInfo g_Task2Info = &#123;0, 16, "Task2"&#125;;
static struct TaskPrintInfo g_Task3Info = &#123;0, 32, "Task3"&#125;;
static int g_LCDCanUse = 1;

void lcd_thread_entry(void *params)
&#123;
struct TaskPrintInfo *pInfo = params;
uint32_t cnt = 0;
int len;

while (1)
&#123;
/* 在LCD上显示文字 */
if (g_LCDCanUse)
&#123;
g_LCDCanUse = 0;
len = LCD_PrintString(pInfo->x, pInfo->y, pInfo->name);
len += LCD_PrintString(len*8, pInfo->y, ":");
LCD_PrintSignedVal(len*8, pInfo->y, cnt++);
LCD_Flush();
g_LCDCanUse = 1;
&#125;
R_BSP_SoftwareDelay(500, BSP_DELAY_UNITS_MILLISECONDS);
&#125;
&#125;

第14行info来自参数params;params又来自哪里?创建任务时传入的。

创建任务的代码如下:

void create_lcd_threads(void)
&#123;
BaseType_t ret;

LCD_Init();
LCD_Clear();

ret = xTaskCreate(lcd_thread_entry, "lcdthread1", 128, &g_Task1Info, 1, NULL);
ret = xTaskCreate(lcd_thread_entry, "lcdthread1", 128, &g_Task2Info, 1, NULL);
ret = xTaskCreate(lcd_thread_entry, "lcdthread1", 128, &g_Task3Info, 1, NULL);
&#125;

8.5 示例3: 删除任务

代码为:0803_delete_task,关键源文件“applications/irda_app.c”。

功能为:先创建一个IRDA任务,它读取红外遥控器的键值,当发现播放键被按下时,创建音乐任务;当发现电源键被按下时,删除音乐任务。

关键代码如下:

while (1)
&#123;
/* 读取红外遥控器 */
if(ESUCCESS == pIRDA->Read(pIRDA, &dev, &data))
&#123;
if (data == IR_KEY_PLAY) /* play */
&#123;
/* 创建播放音乐的任务 */
extern void PlayMusic(void *params);
if (xSoundTaskHandle == NULL)
&#123;
LCD_ClearLine(0, 0);
LCD_PrintString(0, 0, "Create Task");
LCD_Flush();
ret = xTaskCreate(PlayMusic, "SoundTask", 128, NULL, 1, &xSoundTaskHandle);
&#125;
&#125;

else if (data == IR_KEY_POWER) /* power */
&#123;
/* 删除播放音乐的任务 */
if (xSoundTaskHandle != NULL)
&#123;
LCD_ClearLine(0, 0);
LCD_PrintString(0, 0, "Delete Task");
LCD_Flush();
vTaskDelete(xSoundTaskHandle);
PassiveBuzzer_Control(0); /* 停止蜂鸣器 */
xSoundTaskHandle = NULL;
&#125;
&#125;
&#125;
&#125;

第4行读取遥控键值,第15、27行分别创建、删除音乐任务。

程序缺点:每次重新播放音乐时,都是从头播放,没有暂停功能。

8.6 任务优先级和Tick

8.6.1 任务优先级

怎么让播放的音乐更动听?提高优先级。优先级的取值范围是:0~(configMAX_PRIORITIES – 1),数值越大优先级越高。

在学习调度方法之前,你只要初略地知道:

● FreeRTOS会确保最高优先级的、可运行的任务,马上就能执行 ● 对于相同优先级的就绪任务,轮流执行

这无需记忆,就像我们举的例子:

● 厨房着火了,当然优先灭火 ● 喂饭、回复信息同样重要,轮流做

8.6.2 Tick

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

"一会"怎么定义?人有心跳,心跳间隔基本恒定。FreeRTOS中也有心跳,它使用定时器产生固定间隔的中断。这叫Tick、滴答,比如每1ms发生一次时钟中断。

如下图:

  • 假设t1、t2、t3发生时钟中断
  • 两次中断之间的时间被称为时间片(time slice、tick period)
  • 时间片的长度由configTICK_RATE_HZ 决定,假设configTICK_RATE_HZ为1000,那么时间片长度就是1ms

相同优先级的任务怎么切换呢?请看下图:

  • 任务2从t1执行到t2
  • 在t2发生tick中断,进入tick中断处理函数:
    • 选择下一个要运行的任务
    • 执行完中断处理函数后,切换到新的任务:任务1
  • 任务1从t2执行到t3
  • 从图中可以看出,任务运行的时间并不是严格从t1,t2,t3那里开始

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

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改变了,我们也不用去修改代码。

8.6.3 修改优先级

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

UBaseType_t uxTaskPriorityGet( const TaskHandle_t xTask );

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

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

void vTaskPrioritySet( TaskHandle_t xTask,
UBaseType_t uxNewPriority );

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

8.7 示例4: 优先级与阻塞

代码为:0804_task_priority

本程序会:提高音乐播放任务的优先级,使用vTaskDelay进行延时。

0804_task_priority跟0803_delete_task的代码几乎相同,仅有如下差别:

① applications/irda_app.c中,创建音乐任务时,使用优先级2

ret = xTaskCreate(PlayMusic, "SoundTask", 128, NULL, 2, &xSoundTaskHandle);

② applications/music_app.c中,MUSIC_Analysis函数里使用vTaskDelay进行延时:

//R_BSP_SoftwareDelay(MusicSpeed/Music_Lone_Brave[i][2], BSP_DELAY_UNITS_MILLISECONDS);
vTaskDelay(pdMS_TO_TICKS(MusicSpeed/Music_Lone_Brave[i][2]));

这样,当音乐任务要运行时,因为它的优先级最高,它能马上就运行,音乐播放效果不再卡顿。在MUSIC_Analysis函数里设置PWM频率后,它使用vTaskDelay进行延时:这会导致音乐任务阻塞、放弃CPU资源,其他低优先级的任务才能运行。如果不使用vTaskDelay,而是继续使用原来的R_BSP_SoftwareDelay进行延时,那么其他低优先级的任务比如LED任务、IRDA任务无法运行,LED就不会闪烁、使用红外遥控器也无法控制音乐任务。

8.8 任务状态

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

对于非运行的状态,还可以继续细分,比如前面的0804_task_priority中:

● 音乐任务执行vTaskDelay后:处于非运行状态,要过500毫秒才能再次运行 ● 音乐任务运行期间,LED任务、IRDA任务也处于非运行状态,但是它们 随时可以运行 ● "非运行"状态有三种,可以细分为:

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

8.8.1 阻塞状态(Blocked)

在日常生活的例子中,母亲在电脑前跟同事沟通时,如果同事一直没回复,那么母亲的工作就被卡住了、被堵住了、处于阻塞状态(Blocked)。重点在于:母亲在 等待

在0804_task_priority实验中,如果把音乐任务中的vTaskDelay调用注释掉,那么LED任务、IRDA任务根本没有执行的机会,LED任务、IRDA任务"饿死"了(starve)。

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

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

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

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

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

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

8.8.2 暂停状态(Suspended)

在日常生活的例子中,母亲正在电脑前跟同事沟通,母亲可以暂停:

  • 好烦啊,我暂停一会
  • 领导说:你暂停一下

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

void vTaskSuspend( TaskHandle_t xTaskToSuspend );

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

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

  • 别的任务调用:vTaskResume
  • 中断程序调用:xTaskResumeFromISR

实际开发中,暂停状态用得不多。

8.8.3 就绪状态(Ready)

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

8.8.4 完整的状态转换图

8.9 示例5: 任务暂停

代码为:0805_task_suspend

本程序会:使用vTaskSuspend暂停音乐播放任务,使用vTaskResume恢复它,实现音乐的暂停播放、继续播放功能。

0805_task_suspend的关键代码在/applications/irda_app.c中,关键代码如下

while (1)
&#123;
/* 读取红外遥控器 */
if(ESUCCESS == pIRDA->Read(pIRDA, &dev, &data))
&#123;
if (data == IR_KEY_PLAY) /* play */
&#123;
/* 创建播放音乐的任务 */
extern void PlayMusic(void *params);
if (xSoundTaskHandle == NULL)
&#123;
LCD_ClearLine(0, 0);
LCD_PrintString(0, 0, "Create Task");
LCD_Flush();
ret = xTaskCreate(PlayMusic, "SoundTask", 128, NULL, 2, &xSoundTaskHandle);
bRunning = 1;
&#125;
else
&#123;
/* 要么suspend要么resume */
if (bRunning)
&#123;
LCD_ClearLine(0, 0);
LCD_PrintString(0, 0, "Suspend Task");
LCD_Flush();
vTaskSuspend(xSoundTaskHandle);
PassiveBuzzer_Control(0); /* 停止蜂鸣器 */
bRunning = 0;
&#125;
else
&#123;
LCD_ClearLine(0, 0);
LCD_PrintString(0, 0, "Resume Task");
LCD_Flush();
vTaskResume(xSoundTaskHandle);
bRunning = 1;
&#125;
&#125;
&#125;

else if (data == IR_KEY_POWER) /* power */
&#123;
/* 删除播放音乐的任务 */
if (xSoundTaskHandle != NULL)
&#123;
LCD_ClearLine(0, 0);
LCD_PrintString(0, 0, "Delete Task");
LCD_Flush();
vTaskDelete(xSoundTaskHandle);
PassiveBuzzer_Control(0); /* 停止蜂鸣器 */
xSoundTaskHandle = NULL;
bRunning = 0;
&#125;
&#125;
&#125;
&#125;

第1次按下红外遥控器的播放按钮时,执行第15行的代码来创建音乐任务。

后续按下红外遥控器的播放按钮时,要么使用第26行的代码来暂停音乐任务,要么使用第35行的代码来恢复音乐任务。

按下红外遥控器的电源按钮时,执行第49行的代码来删除音乐任务。

8.10 Delay函数

有两个Delay函数:

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

这2个函数原型如下:

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的时刻是“Pre+n”,其中的Pre是上次调用xTaskDelayUntil时设置的。即xTaskDelayUntil使得前后2次退出xTaskDelayUntil的时间差是n个tick:

■ 退出xTaskDelayUntil时任务就进入的就绪状态,一般都能得到执行机会

■ 所以可以使用xTaskDelayUntil来让任务周期性地运行

<img src="https://photos.100ask.net/renesas-docs/DShanMCU_RA6M5/FreeRTOS/chapter-8/image7.png" />

8.11 示例6: Delay函数实验

本节代码为:0806_taskdelay,它是在0802_create_task_use_params的基础上修改得来。

本程序会比较vTaskDelay和vTaskDelayUntil实际阻塞的时间,并在LCD上打印出来。

代码如下:

void lcd_thread_entry(void *params)
&#123;
struct TaskPrintInfo *pInfo = params;
uint32_t cnt = 0;
int len;
volatile uint32_t t0, t1, t2;
BaseType_t preTime;

preTime = xTaskGetTickCount();
while (1)
&#123;
/* 在LCD上显示文字 */
len = LCD_PrintString(pInfo->x, pInfo->y, pInfo->name);
len += LCD_PrintString(len*8, pInfo->y, ":");
len += LCD_PrintSignedVal(len*8, pInfo->y, cnt++);
LCD_Flush();

vTaskDelay(rand() %10);
#if 0
t0 = system_get_us();
vTaskDelay(pdMS_TO_TICKS(1000));
t1 = t2;
t2 = system_get_us();
#else
t0 = system_get_us();
vTaskDelayUntil(&preTime, pdMS_TO_TICKS(1000));
t1 = t2;
t2 = system_get_us();
#endif

len = LCD_PrintString(0, pInfo->y+16, "Delay time(ms): ");
len += LCD_PrintSignedVal(len*8, pInfo->y+16, (t2-t0)/1000);
LCD_ClearLine(len*8, pInfo->y+16);

len = LCD_PrintString(0, pInfo->y+32, "Exit time Between 2 Delay(ms): ");
len += LCD_PrintSignedVal(len*8, pInfo->y+32, (t2-t1)/1000);
LCD_ClearLine(len*8, pInfo->y+32);

LCD_Flush();
&#125;
&#125;

第18行,让当次while循环执行的时间增加一个扰动。

做实验时,可以修改第19行,把“#if 0”改为“#if 1”,那么第20~23的t0、t1、t2被设置后,“t2-t0”就是第21行vTaskDelay函数执行的时间,“t2-t1”就是前后两次while循环的间隔(第21行vTaskDelay退出时的时间差)。LCD上的结果如下:

Task1: 1
Delay time(ms): 1000 /* 无变化 */
Exit time Between 2 Delay(ms): 1790 /* 有变化 */

使用vTaskDelay时,它阻塞指定时间;每次while循环执行多长时间,与它无关。

还可以修改保持第19行为“#if 0”,那么第25~28的t0、t1、t2被设置后,“t2-t0”就是第26行vTaskDelayUtil函数执行的时间,“t2-t1”就是前后两次while循环的间隔(第21行vTaskDelayUtil退出时的时间差)。LCD上的结果如下:

Task1: 123
Delay time(ms): 201 /* 有变化 */
Exit time Between 2 Delay(ms): 1000 /* 无变化 */

使用vTaskDelayUtil时,它阻塞的时间是变化的;这次while循环执行时间长了,那么vTaskDelayUtil阻塞的时间就少一点,它保证前后两次while循环的间隔一致。

8.12 空闲任务及其钩子函数

8.12.1 介绍

空闲任务(Idle任务)的作用之一:释放被删除的任务的内存。

除了上述目的之外,为什么必须要有空闲任务?一个良好的程序,它的任务都是事件驱动的:平时大部分时间处于阻塞状态。有可能我们自己创建的所有任务都无法执行,但是调度器必须能找到一个可以运行的任务:所以,我们要提供空闲任务。在使用 vTaskStartScheduler() 函数来创建、启动调度器时,这个函数内部会创建空闲任务:

  • 空闲任务优先级为0:它不能阻碍用户任务运行
  • 空闲任务要么处于就绪态,要么处于运行态,永远不会阻塞

空闲任务的优先级为0,这意味着一旦某个用户的任务变为就绪态,那么空闲任务马上被切换出去,让这个用户任务运行。在这种情况下,我们说用户任务"抢占"(pre-empt)了空闲任务,这是由调度器实现的。

要注意的是:如果使用 vTaskDelete() 来删除任务,那么你就要确保空闲任务有机会执行,否则就无法释放被删除任务的内存。

我们可以添加一个空闲任务的钩子函数(Idle Task Hook Functions),空闲任务的循环每执行一次,就会调用一次钩子函数。钩子函数的作用有这些:

  • 执行一些低优先级的、后台的、需要连续执行的函数
  • 测量系统的空闲时间:空闲任务能被执行就意味着所有的高优先级任务都停止了,所以测量空闲任务占据的时间,就可以算出处理器占用率。
  • 让系统进入省电模式:空闲任务能被执行就意味着没有重要的事情要做,当然可以进入省电模式了。
  • 空闲任务的钩子函数的限制:
  • 不能导致空闲任务进入阻塞状态、暂停状态
  • 如果你会使用*vTaskDelete()*来删除任务,那么钩子函数要非常高效地执行。如果空闲任务移植卡在钩子函数里的话,它就无法释放内存。

8.12.2 使用钩子函数的前提

FreeRTOS/Source/tasks.c 中,可以看到如下代码,所以前提就是:

  • 把这个宏定义为1:configUSE_IDLE_HOOK
  • 实现 vApplicationIdleHook 函数

8.13 示例7: 空闲任务实验

本节代码为:0807_idle_task,它是在0805_task_suspend的基础上修改得来。

本程序里,IRDA任务接收到红外遥控器的Play按键后,会创建LED任务;而LED任务执行一会就自杀。IRDA任务的优先级是1,高于空闲任务的优先级0,如果IRDA程序一直不阻塞,那么空闲任务无法执行,无法回收自杀的LED任务的内存,最后会导致:无法创建LED任务。

关键代码在“applications/irda_app.c”中,代码如下:

while (1)
&#123;
/* 读取红外遥控器 */
if(ESUCCESS == pIRDA->Read(pIRDA, &dev, &data))
&#123;
if (data == IR_KEY_PLAY) /* play */
&#123;
len = LCD_PrintString(0, 16, "count = ");
LCD_PrintSignedVal(len*8, 16, cnt++);
LCD_Flush();

/* 创建LED任务 */
void led_thread_entry(void * pvParameters);
ret = xTaskCreate(led_thread_entry, "LEDTask", 8*1024, NULL, 1, NULL);
if (ret == pdPASS)
LCD_PrintString(0, 32, "Create Task ok ");
else
LCD_PrintString(0, 32, "Create Task err");
LCD_Flush();
&#125;
&#125;
// vTaskDelay(1);
&#125;
&#125;

接收到遥控器的Play按键后,第14行会创建LED任务。可以多次按下Play键,创建多个LED任务。

如果第22行的代码被注释掉,当前的IRDA任务与LED任务轮流运行,空闲任务没有机会运行。多次创建LED任务后,最后因为堆耗尽而无法创建更多LED任务。

如果第22行的代码启用了,在IRDA阻塞期间空闲任务就有机会运行,进而回收自杀的LED任务的内存。这样,以后就可以再次使用Play按键来创建LED任务了。

8.14 调度算法

8.14.1 重要概念

这些知识在前面都提到过了,这里总结一下。

正在运行的任务,被称为"正在使用处理器",它处于运行状态。在单处理器系统中,任何时间里只能有一个任务处于运行状态。

非运行状态的任务,它处于这3中状态之一:阻塞(Blocked)、暂停(Suspended)、就绪(Ready)。就绪态的任务,可以被调度器挑选出来切换为运行状态,调度器永远都是挑选最高优先级的就绪态任务并让它进入运行状态。

阻塞状态的任务,它在等待"事件",当事件发生时任务就会进入就绪状态。事件分为两类:时间相关的事件、同步事件。所谓时间相关的事件,就是设置超时时间:在指定时间内阻塞,时间到了就进入就绪状态。使用时间相关的事件,可以实现周期性的功能、可以实现超时功能。同步事件就是:某个任务在等待某些信息,别的任务或者中断服务程序会给它发送信息。怎么"发送信息"?方法很多,有:任务通知(task notification)、队列(queue)、事件组(event group)、信号量(semaphoe)、互斥量(mutex)等。这些方法用来发送同步信息,比如表示某个外设得到了数据。

8.14.2 配置调度算法

所谓调度算法,就是怎么确定哪个就绪态的任务可以切换为运行状态。

通过配置文件FreeRTOSConfig.h的两个配置项来配置调度算法:configUSE_PREEMPTION、configUSE_TIME_SLICING。

还有第三个配置项:configUSE_TICKLESS_IDLE,它是一个高级选项,用于关闭Tick中断来实现省电。现在我们假设configUSE_TICKLESS_IDLE被设为0,先不使用这个功能。

调度算法的行为主要体现在两方面:高优先级的任务先运行、同优先级的就绪态任务如何被选中。调度算法要确保同优先级的就绪态任务,能"轮流"运行,策略是"轮转调度"(Round Robin Scheduling)。轮转调度并不保证任务的运行时间是公平分配的,我们还可以细化时间的分配方法。

从3个角度统一理解多种调度算法:

● 可否抢占?高优先级的任务能否优先执行(配置项: configUSE_PREEMPTION) ■ 可以:被称作"可抢占调度"(Pre-emptive),高优先级的就绪任务马上执行,下面再细化。 ■ 不可以:不能抢就只能协商了,被称作"合作调度模式"(Co-operative Scheduling)。当前任务执行时,更高优先级的任务就绪了也不能马上运行,只能等待当前任务主动让出CPU资源。其他同优先级的任务也只能等待:更高优先级的任务都不能抢占,平级的更应该老实点 ● 可抢占的前提下,同优先级的任务是否轮流执行(配置项:configUSE_TIME_SLICING) ■ 轮流执行:被称为"时间片轮转"(Time Slicing),同优先级的任务轮流执行,你执行一个时间片、我再执行一个时间片 ■ 不轮流执行:英文为"without Time Slicing",当前任务会一直执行,直到主动放弃、或者被高优先级任务抢占 ● 在"可抢占"+"时间片轮转"的前提下,进一步细化:空闲任务是否礼让用户任务(配置项:configIDLE_SHOULD_YIELD): ■ 空闲任务低人一等,每执行一次循环,就看看是否主动让位给用户任务 ■ 空闲任务跟用户任务一样,大家轮流执行,没有谁更特殊

列表如下:

配置项ABCDE
configUSE_PREEMPTION11110
configUSE_TIME_SLICING1100x
configIDLE_SHOULD_YIELD1010x
说明常用很少用很少用很少用几乎不用

注:

① A:可抢占+时间片轮转+空闲任务让步 ② B:可抢占+时间片轮转+空闲任务不让步 ③ C:可抢占+非时间片轮转+空闲任务让步 ④ D:可抢占+非时间片轮转+空闲任务不让步 ⑤ E:合作调度

我们可以在RASC里配置调度算法,上述三个宏在如下界面里有对应的项:

<img src="https://photos.100ask.net/renesas-docs/DShanMCU_RA6M5/FreeRTOS/chapter-8/image9.png" />

8.14.3 内部机制