目录
一. 前言
大家好,我是旭辉君,一个致力于智能硬件干货分享的技术博主。
在上篇文章中,我们理解了FreeRTOS内核的一些关键概念,链接如下:
FreeRTOS系列教程(一):内核关键概念-CSDN博客文章浏览阅读712次,点赞7次,收藏17次。本文介绍了FreeRTOS内核的一些关键概念,包括:链表与节点,任务,堆与栈,临界段,阻塞延时,优先级,时间片等。这些概念的理解有助于我们后续在自己的项目中使用FreeRTOS的时候,能更得心应手。这些概念在初次理解的时候可能有些晦涩难懂,我尽量用一些形象的语言给大家展示出来。https://blog.csdn.net/weixin_42434952/article/details/137438517?spm=1001.2014.3001.5501接下来本文将带大家一起进入如何把FreeRTOS应用好的篇章。从系统的角度看,任务是竞争系统资源的最小运行单元。 FreeRTOS 是一个支持多任务的操作系统,每个任务在自己的环境中运行。在任何时刻,只有一个任务得到运行, FreeRTOS 调度器决定运行哪个任务。
本文将重点介绍FreeRTOS中任务管理的知识,看完本文,我们将会知道:
- 如何创建一个或多个任务?
- 任务都有哪些状态?
- FreeRTOS 如何选择在任何给定时间执行哪个任务?
- 任务的优先级对系统运行有什么影响?
带着这些问题,我们进入FreeRTOS的任务之旅。
二. 任务管理原理
2.1 任务调度机制
得益于FreeRTOS操作系统的任务调度机制的优势,调度器可以在每次系统滴答中断后进行任务调度,然后根据任务的优先级和状态,决定下一个要执行的任务。当一个任务正在执行时,如果有更高优先级的任务变为就绪状态,调度器将保存当前任务的状态,然后加载并执行新的任务。当新的任务完成或被阻塞时,调度器再次保存其状态,然后加载并恢复之前的任务。通过这种方式,虽然单片机在任何时刻都只在执行一个任务,但由于任务切换的速度非常快,它可以看起来像是在同时执行多个任务。为了能够选择要运行的下一个任务,调度程序本身必须在每个时间片,也就是滴答中断的末尾执行,滴答中断频率由应用程序在 FreeRTOSConfig.h
中定义的 configTICK_RATE_HZ
配置。例如,如果 configTICK_RATE_HZ
设置为 100(Hz),则时间片将为 10 毫秒。
下图显示了调度程序是如何执行的:在Task1时间片的末尾进入滴答中断,然后从滴答中断返回到另一个任务。
下图更具体的演示了使用“基于时间片的固定优先级抢占式调度”算法抢占调度任务的调度过程,大部分FreeRTOS操作系统都使用这种任务调度策略。
这里面,task1是最高优先级的任务,task2是中等优先级的任务,task3是低优先级的任务,Idle task是空闲任务,空闲任务一般具有最低优先级。
系统启动后,task1的时间还没到,运行task2,等task2进入阻塞态后运行空闲任务,当task3的事件到达就会抢占空闲任务,task3运行期间,如果task2的周期到了,因为task2优先级高就会抢占task3,task2运行期间,由于task1优先级高,一旦task1等待的事件到了就会抢占task2,如此往复。
不同于单片机的中断的优先级,在FreeRTOS中,数字越大,优先级越高。可用优先级的最大数量在 FreeRTOSConfig.h
中定义的 configMAX_PRIORITIES
配置。 优先级 0 是最低优先级,分配给空闲任务使用,一般不建议用户来使用这个优先级。因此,可用优先级的范围是 0 到(configMAX_PRIORITIES - 1
)。在同一个优先级下,也可以放任意数量的任务,确保最大的设计灵活性。
2.2 任务状态
2.2.1 任务状态的定义
FreeRTOS 系统中的任务有如下四种状态定义。系统初始化完成后,创建的任务就可
以根据自己所处的不同状态,在系统由内核进行调度:
- 就绪( Ready):该任务在就绪列表中, 就绪的任务已经具备执行的能力,只等待调度器进行调度,新创建的任务会初始化为就绪态。
- 运行(Running):该状态表明任务正在执行, 此时它占用处理器, FreeRTOS 调度器选择运行的永远是处于最高优先级的就绪态任务,当任务被运行的一刻,它的任务状态就变成了运行态。
- 阻塞(Blocked): 如果任务当前正在等待某个时序或外部中断,我们就说这个任务处于阻塞状态,此时任务不在就绪列表中。任务被挂起、 任务被延时、任务正在等待信号量、读写队列或者等待读写事件等都属于阻塞态。
- 挂起态(Suspended): 处于挂起态的任务对调度器而言是不可见的, 让一个任务进入挂起状态的唯一办法就是调用 vTaskSuspend()函数;而 把 一 个 挂 起 状态 的任 务 恢复的 唯 一 途径 就 是 调 用 vTaskResume() 或 vTaskResumeFromISR()函数,我们可以这么理解挂起态与阻塞态的区别,当任务有较长的时间不允许运行的时候,我们可以挂起任务,这样子调度器就不会管这个任务的任何信息,直到我们调用恢复任务的 API 函数;而任务处于阻塞态的时候,系统还需要判断阻塞态的任务是否超时,是否可以解除阻塞,比处于挂起态费时一些。大多数应用程序不使用挂起状态。
2.2.2 任务状态的迁移
FreeRTOS 系统中的每一个任务多种状态之间的转换关系是怎么样的呢?从运行态任务变成阻塞态,或者从阻塞态变成就绪态,这些任务状态是如何进行迁移?下面就让我们一起了解。
如下图是一个完整的状态机迁移图,包含如下九种状态迁移方式:
- 创建任务→就绪态:任务创建完成后进入就绪态,表明任务已准备就绪,随时可以运行,只等待调度器进行调度。
- 就绪态→运行态:发生任务切换时,就绪列表中最高优先级的任务被执行,从而进入运态。
- 运行态→就绪态:有更高优先级任务创建或者恢复后,在滴答中断会发生任务调度,此刻最高优先级任务变为运行态,那么原先运行的任务由运行态变为就绪态,依然在就绪列表中,等待最高优先级的任务运行完毕继续运行原来的任务(此处可以看做是 CPU 使用权被更高优先级的任务抢占了)。
- 运行态→阻塞态:正在运行的任务发生阻塞(挂起、延时、读信号量等待)时,该任务会从就绪列表中删除,任务状态由运行态变成阻塞态,然后发生任务切换,运行就绪列表中当前最高优先级任务。
- 阻塞态→就绪态:阻塞的任务被恢复后(任务恢复、延时时间超时、读信号量超时或读到信号量等),此时被恢复的任务会被加入就绪列表,从而由阻塞态变成就绪态;如果此时被恢复任务的优先级高于正在运行任务的优先级,则会发生任务切换,将该任务将再次转换任务状态,由就绪态变成运行态。
- 就绪态→挂起态:任务可以通过调用 vTaskSuspend() 函数可以将处于就绪态的任务挂起,被挂起的任务得不到CPU 的使用权,也不会参与调度,除非它从挂起态中解除。
- 阻塞态→挂起态:同样,任务可以通过调用 vTaskSuspend() 函数将处于阻塞态的任务挂起。
- 运行态→挂起态:同样,任务可以通过调用 vTaskSuspend() 函数将处于运行态的任务挂起。总之,不管当前任务处于何种状态,调用vTaskSuspend()后都会将任务挂起。
- 挂起态→就绪态: 把 一 个 挂 起 状态 的 任 务 恢复的 唯 一 途 径 就 是调 用 vTaskResume() 或 vTaskResumeFromISR() 函数,如果此时被恢复任务的优先级高
于正在运行任务的优先级,则会发生任务切换,将该任务将再次转换任务状态,由就绪态
变成运行态。
三. 任务管理API函数
下面我们对任务管理常用的函数进行讲解,重点在于如何正确使用其接口函数。
3.1 任务创建函数:xTaskCreate
BaseType_t xTaskCreate( TaskFunction_t pvTaskCode,
const char * const pcName,
const configSTACK_DEPTH_TYPE uxStackDepth,
void *pvParameters,
UBaseType_t uxPriority,
TaskHandle_t *pxCreatedTask
);
其中,
| 指向实现任务的函数的指针(即要创建的任务函数的名称)。记得要创建的任务必须是无限循环的吗,没有返回值。 |
| 任务的描述性名称。FreeRTOS 不会以任何方式使用它,它纯粹是作为调试辅助工具而包含。 |
| 每个任务都有自己的唯一堆栈,该堆栈在创建任务时由内核分配给任务。 该值指定堆栈可以容纳的字数,而不是字节数。 例如,如果堆栈是 32 位宽并且 空闲任务使用的堆栈大小由应用程序定义的常量 没有简单的方法来确定任务所需的堆栈空间。一般分配一个我们认为合理的值,然后可以使用FreeRTOS 提供的任务的堆栈使用量API函数 uxTaskGetSystemState()查看,来确保分配的空间确实足够,并且RAM 不会被浪费。 |
pvParameters | 分配给 pvParameters 的值是传递给任务的值,如果没有可以设置为 NULL |
| 定义任务执行的优先级。可以将任务优先级设置为从 0(最低优先级)到( |
|
|
返回值 | 有两种返回值:
|
3.2 任务删除函数:vTaskDelete
void vTaskDelete( TaskHandle_t xTask );
其中,
xTask | 待删除的任务的句柄。传递 NULL 将删除正在执行的任务本身。删除任务后,将从所有就绪,阻塞,挂起和事件列表中删除。 |
3.3 任务相对延时函数:vTaskDelay
void vTaskDelay( const TickType_t xTicksToDelay );
其中,
xTicksToDelay | 调用任务应阻塞的 tick 周期数。 |
vTaskDelay()延时是相对性的延时,它指定的延时时间是从调用 vTaskDelay()结束后开
始计算的, 经过指定的时间后延时结束。比如 vTaskDelay(100), 从调用 vTaskDelay()结束
后,任务进入阻塞状态,经过 100 个系统时钟节拍周期后,任务解除阻塞。此外,该延迟时间不是绝对准确的,其它任务和中断活动, 会影响到 vTaskDelay()的使用(比如调用前高优先级任务抢占了当前任务),进而影响到任务的下一次执行的时间。
3.4 任务绝对延时函数:vTaskDelayUntil
void vTaskDelayUntil( TickType_t *pxPreviousWakeTime,
const TickType_t xTimeIncrement );
其中,
pxPreviousWakeTime | 指向一个变量的指针,该变量用于保存任务最后一次解除阻塞的时间。 该变量在第一次使用前必须用当前时间进行初始化,即得到当前时间, 在这之后,该变量会在vTaskDelayUntil() 中自动更新。 |
xTimeIncrement | 任务执行的周期。 该任务将在 (*pxPreviousWakeTime + xTimeIncrement)时间解除阻塞。 配合相同的 xTimeIncrement 参数值 调用 vTaskDelayUntil 将导致任务以固定的间隔期执行。 |
不同于vTaskDelay()的相对延时,vTaskDelayUntil()的延时是绝对的,适用于周期性执行的任务。 当指定唤醒时间(*pxPreviousWakeTime +xTimeIncrement)到达后, vTaskDelayUntil()函数立刻返回,如果任务是最高优先级的,那么任务会立马解除阻塞。所以说 vTaskDelayUntil()函数的延时是绝对性的。
3.5 任务挂起函数:vTaskSuspend
void vTaskSuspend( TaskHandle_t xTaskToSuspend );
其中,
xTaskToSuspend | 被挂起的任务句柄。传递空句柄NULL将暂停任务自身。 |
任务可以通过调用 vTaskSuspend()将处于任何状态的任务挂起,被挂起的任务得不到 CPU 的使用权,也不会参与调度,它相对于调度器而言是不可见的,除非它从挂起态中解除。比如我们想暂停某个任务一段时间,但后面某个时候又想让这个任务继续执行吗,那么不能用任务删除函数vTaskDelete(),因为删除后任务就完全被系统删除了,这时候vTaskSuspend()是一个比较好的选择,想要任务暂停的时候直接将任务挂起,其内部的资源都会保留下来,同时也不会参与系统中任务的调度。
3.6 任务恢复函数:vTaskResume
void vTaskResume( TaskHandle_t xTaskToResume );
其中,
xTaskToResume | 要恢复的任务句柄。注意这里不能传递空句柄NULL。 |
当任务挂起之后,如果想要恢复,就可以调用vTaskResume()函数让任务从挂起前的状态恢复到就绪状态,无论任务在挂起时候调用过多少次vTaskSuspend()函数,也只需调用一次vTaskResume ()函数即可将任务恢复运行,同样,无论调用多少次的 vTaskResume()函数,也只在任务是挂起态的时候才进行恢复。这里注意,要恢复的任务不能在任务自身中恢复,而应该在其他运行的任务中恢复,因为这时候挂起的任务已经不运行了,调度器都看不到它。
3.7 其他任务管理API函数
上面几个任务管理API函数已经能适用大部分的工况,当然,FreeRTOS官方提供的任务管理函数不止于此,想要了解更多的话,我们可以参考官方网址,里面对每个API函数都有详细的说明:
四. 任务管理相关实验
4.1 任务创建与删除实验
在这个实验中,我创建了两个任务task_example_1和task_example_2,task_example_1优先级高于task_example_2,task_example_1每间隔1s即100个tick进入阻塞状态,软件中已设置tick周期为10ms;stask_example_2每间隔500ms进入阻塞状态。在task_example_1的循环中对cnt进行累加,累加到5也就是5s后,删除任务task_example_2,用来模拟删除任务现象。主体代码如下:
TaskHandle_t task_example_1_handle = NULL;
TaskHandle_t task_example_2_handle = NULL;
static int32_t cnt;
TickType_t xWakeTime_task1;
TickType_t xWakeTime_task2;
static void task_example_1(void* arg)
{
while (1)
{
xWakeTime_task1 = xTaskGetTickCount();
printf("task_example_1 is Running at %d\r\n",xWakeTime_task1);
cnt ++;
printf("cnt: %d\n", cnt);
vTaskDelay(1000 / portTICK_RATE_MS); /* 延时1s */
}
}
static void task_example_2(void* arg)
{
while (1)
{
xWakeTime_task2 = xTaskGetTickCount();
printf("task_example_2 is Running at %d\r\n",xWakeTime_task2);
if(cnt == 5)
{
printf("task_example_2 has deleted\r\n");
vTaskDelete(task_example_2_handle);
}
vTaskDelay(500 / portTICK_RATE_MS);
}
}
void app_main(void)
{
BaseType_t xReturn;
xWakeTime_task1 = xTaskGetTickCount();
printf("app_main is Running at %d\r\n",xWakeTime_task1);
//create task
xReturn = xTaskCreate(task_example_1, "task_example_1", 2048, NULL, 2, &task_example_1_handle);
if(xReturn == pdPASS) printf("创建 task_example_1 任务成功!\r\n");
else printf("创建 task_example_1 任务失败!\r\n");
xReturn = xTaskCreate(task_example_2, "task_example_2", 2048, NULL, 1, &task_example_2_handle);
if(xReturn == pdPASS) printf("创建 task_example_2 任务成功!\r\n");
else printf("创建 task_example_2 任务失败!\r\n");
printf("Minimum free heapconfigMINIMAL_STACK_SIZE size: %d bytes\n", esp_get_minimum_free_heap_size());
}
下图为运行代码后的串口输出,可以看到: 任务创建成功,每间隔100个tick,task_example_1运行一次,之后进入阻塞状态,优先级更低的task_example_2得以运行,每50个tick运行一次。在cnt计数达到5之后,task_example_2被删除,之后串口就只有task_example_1运行显示。
4.2 任务挂起与恢复实验
在这个实验中,同样创建了两个任务task_example_1和task_example_2,task_example_1优先级高于task_example_2,task_example_1每间隔1s即100个tick进入阻塞状态;stask_example_2每间隔500ms进入阻塞状态。在task_example_1的循环中对cnt进行累加,累加3s后,挂起任务task_example_2,累加6s后,恢复任务task_example_2,主体代码如下:
TaskHandle_t task_example_1_handle = NULL;
TaskHandle_t task_example_2_handle = NULL;
static int32_t cnt;
TickType_t xWakeTime_task1;
TickType_t xWakeTime_task2;
static void task_example_1(void* arg)
{
while (1)
{
xWakeTime_task1 = xTaskGetTickCount();
printf("task_example_1 is Running at %d\r\n",xWakeTime_task1);
cnt ++;
printf("cnt: %d\n", cnt);
if(cnt == 3)
{
printf("task_example_2 has suspended\r\n");
vTaskSuspend(task_example_2_handle);
}
if(cnt == 6)
{
printf("task_example_2 has resumed\r\n");
vTaskResume(task_example_2_handle);
}
vTaskDelay(1000 / portTICK_RATE_MS); /* 延时1s */
}
}
static void task_example_2(void* arg)
{
while (1)
{
xWakeTime_task2 = xTaskGetTickCount();
printf("task_example_2 is Running at %d\r\n",xWakeTime_task2);
vTaskDelay(500 / portTICK_RATE_MS);
}
}
void app_main(void)
{
BaseType_t xReturn;
xWakeTime_task1 = xTaskGetTickCount();
printf("app_main is Running at %d\r\n",xWakeTime_task1);
xReturn = xTaskCreate(task_example_1, "task_example_1", 2048, NULL, 2, &task_example_1_handle);
if(xReturn == pdPASS) printf("创建 task_example_1 任务成功!\r\n");
else printf("创建 task_example_1 任务失败!\r\n");
xReturn = xTaskCreate(task_example_2, "task_example_2", 2048, NULL, 1, &task_example_2_handle);
if(xReturn == pdPASS) printf("创建 task_example_2 任务成功!\r\n");
else printf("创建 task_example_2 任务失败!\r\n");
printf("Minimum free heapconfigMINIMAL_STACK_SIZE size: %d bytes\n", esp_get_minimum_free_heap_size());
}
下图为运行代码后的串口输出,可以看到: 任务创建成功,在第3s的时候,task_example_2被挂起,之后串口就只有task_example_1运行显示;在第6s的时候,task_example_2被恢复,就得以继续运行。
五. 小结
本文主要探索了任务管理的原理及其使用方法,相信通过本文,大家对第一节的几个问题有了自己的答案,同时也对如何使用任务管理API函数有了更直观的认识。有某些地方不太明白的同学,欢迎留言交流。原创不易,大家的点赞和关注是对于持续更新莫大的鼓励,谢谢!
想要完整源码的同学,可以扫码,或者微信搜索:硬件电子与嵌入式小栈 ,关注我的微信公众号,留言即可获取,公众号里面也会有丰富的干货文章。