目录
一、任务功能
每个任务本身就是一个小程序。它有一个入口点,通常会在无限循环中永远运行,并且不会退出。FreeRTOS任务必须不允许以任何方式从它们实现的函数中返回——它们必须不包含' return '语句,并且必须不允许在函数结束后执行。如果不再需要某个任务,则应该显式地删除它。单个任务函数定义可用于创建任意数量的任务—每个创建的任务都是一个单独的执行实例,具有自己的堆栈和任务本身中定义的任何自动(堆栈)变量的副本。一个典型任务的结构如下:
void ATaskFunction( void *pvParameters )
{
/*变量可以像普通函数一样声明*/
for( ;; )
{
/*实现任务功能的代码放在这里*/
}
/*如果任务实现脱离了上述循环,则必须在到达其实现函数的末尾之前删除任务。
传递给vTaskDelete() API函数的NULL参数表示要删除的任务是调用(此)任务。*/
vTaskDelete( NULL );
}
二、顶级任务状态-简单理解
一个应用程序可以包含许多任务。如果运行应用程序的处理器包含单个核心,那么在任何给定时间只能执行一个任务。这意味着任务可以以两种状态之一存在:运行和未运行。其实“未运行状态”实际包含了许多子状态,如图1所示。
当任务处于运行状态时,处理器正在执行任务的代码。当任务处于“未运行”状态时,任务处于休眠状态,其状态已被保存,以便在调度程序决定下次进入“运行”状态时恢复执行。当任务恢复执行时,它会从上次离开“运行”状态之前将要执行的指令执行。
从非运行状态过渡到运行状态的任务被称为“交换入”或“交换入”。相反,从运行状态转换到非运行状态的任务被称为“切换出”或“交换出”。FreeRTOS调度器是唯一可以切换任务进出的实体。
图1 顶级任务状态和转换
三、创建任务原型
xTaskCreate() API函数原型
BaseType_t xTaskCreate( TaskFunction_t pxTaskCode,
const char * const pcName,
const uint16_t usStackDepth,
void * const pvParameters,
UBaseType_t uxPriority,
TaskHandle_t * const pxCreatedTask )
参数名称/返回值 | 描述 |
pvTaskCode | 任务只是永远不会退出的C函数,因此,通常以无限循环的形式实现。pvTaskCode参数只是一个指针,指向实现任务的函数(实际上,只是函数的名称)。 |
pcName | 任务的描述性名称。FreeRTOS不以任何方式使用它。它纯粹是作为调试辅助工具包含的。通过人类可读的名称识别任务比通过句柄识别任务要简单得多。 应用程序定义的常量configMAX_TASK_NAME_LEN定义了任务名可以使用的最大长度,包括NULL终止符。提供超过此最大值的字符串将导致字符串被静默截断。 |
usStackDepth | 每个任务都有自己唯一的堆栈,在任务创建时由内核分配给该任务。usStackDepth值告诉内核堆栈的大小。该值指定堆栈可以容纳的字数,而不是字节数。例如,如果堆栈是32位宽,usStackDepth作为100传入,那么将分配400字节的堆栈空间(100 * 4字节)。堆栈深度乘以堆栈宽度不能超过uint16_t类型变量所能包含的最大值。空闲任务使用的堆栈大小由应用程序定义的常量configMINIMAL_STACK_SIZE定义。 |
pvParameters | 任务函数接受指向void (void*)的指针类型的形参。分配给pvParameters的值是传递给任务的值。 |
uxPriority | 定义任务执行的优先级。优先级可以从0(最低优先级)分配到(configMAX_PRIORITIES - 1)(最高优先级)。configMAX_PRIORITIES是一个用户定义的常量。 |
pxCreatedTask | pxCreatedTask可用于向正在创建的任务传递句柄。然后,这个句柄可以用于在API调用中引用任务,例如,更改任务优先级或删除任务。 如果应用程序不需要任务句柄,则可以将pxCreatedTask设置为NULL。 |
Returned value | 有两种可能的返回值: 1. pdPASS (1):任务创建成功 2. pdFAIL (0):这表明任务还没有创建,因为FreeRTOS没有足够的堆内存来分配足够的RAM来保存任务数据结构和堆栈。 |
四、简单创建实例
4.1 简单实例1:创建2个任务
示例演示创建两个简单任务所需的步骤,然后开始执行任务。任务只是将同一个标志位置0或者置1。这两个任务都是以相同的优先级创建的,除了标志位外,其他都是相同的。需要用到的宏定义如下:
#define TASK1_SIZE 128 //任务1
#define TASK1_PRIORITY 3
TaskHandle_t task1_handle;
void task1(void *pvparameter);
#define TASK2_SIZE 128 //任务2
#define TASK2_PRIORITY 3
TaskHandle_t task2_handle;
void task2(void *pvparameter);
然后在main函数里创建两个任务
xTaskCreate((TaskFunction_t) task1,
(const char * ) "TASK1",
(uint16_t) TASK1_SIZE,
(void *) NULL,
(UBaseType_t) TASK1_PRIORITY,
(TaskHandle_t *)&task1_handle ) ;
xTaskCreate((TaskFunction_t) task2,
(const char * ) "TASK2",
(uint16_t) TASK2_SIZE,
(void *) NULL,
(UBaseType_t) TASK2_PRIORITY,
(TaskHandle_t *)&task2_handle ) ;
两个任务功能编写,任务1将FLAG置0,任务2将FLAG置1,2个任务都是无限循环且不会退出。
void task1(void *pvparameter)//任务1
{
while(1)
{
taskflagrun=0;//这是一个全局变量
}
}
void task2(void *pvparameter)//任务2
{
while(1)
{
taskflagrun=1;
}
}
通过keil的逻辑分析仪仿真运行结果如图2所示,从图2可以看出,即使在死循环的情况下2个任务也是轮流执行,将标志位置0置1,且每个任务运行的时间相同。
图2 标志位变化
4.2 简单实例2:使用任务的参数
和实例1一样,不过唯一改变的是利用的任务的参数进行赋值,首先定义2个变量,分别为10和20,然后修改2个任务函数的标志位赋值,以下程序为主要程序,其他参考实例1。
int a=10;
int b=20;
const int *pcTextForTask1 = &a; //任务的参数
const int *pcTextForTask2 = &b;
xTaskCreate((TaskFunction_t) task1,
(const char * ) "TASK1",
(uint16_t) TASK1_SIZE,
(void *) pcTextForTask1,//原来为NULL
(UBaseType_t) TASK1_PRIORITY,
(TaskHandle_t *)&task1_handle ) ;
xTaskCreate((TaskFunction_t) task2,
(const char * ) "TASK2",
(uint16_t) TASK2_SIZE,
(void *) pcTextForTask2,//原来为NULL
(UBaseType_t) TASK2_PRIORITY,
(TaskHandle_t *)&task2_handle ) ;
vTaskDelete(start_handle);
void task1(void *pvparameter)
{
while(1)
{
taskflagrun=*(int *)pcTextForTask1;//使用任务的参数
}
}
void task2(void *pvparameter)
{
while(1)
{
taskflagrun=*(int *)pcTextForTask2;
}
}
keil的逻辑分析仿真结果如图3所示。标志位轮流被置10和20。
图3 标志位变化
五、任务优先级
xTaskCreate() API函数的uxPriority参数为正在创建的任务分配初始优先级。通过使用vTaskPrioritySet() API函数,可以在调度程序启动后更改优先级。
可用优先级的最大数量由FreeRTOSConfig.h中应用程序定义的configMAX_PRIORITIES编译时配置常量设置。低数字优先级值表示低优先级任务,优先级0是可能的最低优先级。因此,可用的优先级范围是0到(configMAX_PRIORITIES - 1)。任何数量的任务都可以共享相同的优先级,从而确保最大的设计灵活性。
FreeRTOS由两种方法可以切换到下一个需要运行的任务,一个是通用的方法,另外一个是特殊的方法,也就是硬件方法,使用MCU自带的硬件指令实现。宏定义configUSE_PORT_OPTIMISED_TASK_SELECTION为0的时候使用的就是第一种通用方法;定义为1的时候使用的则是第二种特殊方法;STM32有计算前导零的指令,所以是可以使用第二种方法。
六、时间测量和滴答中断
每个任务在一个“时间片”中执行,在一个时间片开始时进入运行状态,在一个时间片结束时退出运行状态。为了能够选择要运行的下一个任务,调度器本身必须在每个时间片结束时执行周期性中断,称为“滴答中断”,用于此目的。时间片的长度有效地由tick中断频率设置,该频率由FreeRTOSConfig.h中应用程序定义的configTICK_RATE_HZ编译时间配置常数配置。例如,如果configTICK_RATE_HZ设置为100 (Hz),那么时间片将是10毫秒。两次滴答中断之间的时间称为“滴答周期”。一个时间片等于一个滴答周期。如图4所示,其中顶线显示调度程序执行的时间,细箭头显示从一个任务到计时中断,然后从计时中断返回到另一个任务的执行顺序。
图4 执行序列展开以显示滴答中断正在执行
宏pdMS_TO_TICKS()可用于将以毫秒为单位指定的时间转换为以ticks为单位指定的时间。例如,调用vTaskDelay(pdMS_TO_TICKS(100))将使任务保持阻塞状态100毫秒。
注意:不建议在应用程序中直接指定以刻度为单位的时间,而是使用pdMS_TO_TICKS()宏来指定以毫秒为单位的时间,这样做可以确保即使刻度频率发生变化,应用程序中指定的时间也不会改变。
七、扩展“未运行”状态
前面所讲的只有运行和未运行2种状态,但其实“未运行”状态也可以分为3种状态。
7.1 阻塞状态
等待事件的任务被称为“Blocked”状态,这是Not Running状态的一个子状态。任务可以进入Blocked状态来等待两种不同类型的事件:
1. 时间(与时间相关)事件—事件可能是即将到期的延迟时间,也可能是达到的绝对时间。例如,一个任务可能会进入阻塞状态,等待10毫秒通过。
2. 同步事件——事件起源于另一个任务或中断。例如,任务可能进入阻塞状态以等待数据到达队列。同步事件涵盖了广泛的事件类型。FreeRTOS队列、二进制信号量、计数信号量、互斥锁、递归互斥锁、事件组和直接到任务通知都可以用来创建同步事件。
任务可以用超时阻塞同步事件,从而有效地同时阻塞两种类型的事件。例如,一个任务可以选择等待最多10毫秒的数据到达队列。如果数据在10毫秒内到达,或者10毫秒内没有数据到达,任务将离开Blocked状态。
7.2 挂起状态
“挂起”也是“不运行”的子状态。处于“挂起”状态的任务对调度程序不可用。进入Suspended状态的唯一方法是通过调用vTaskSuspend() API函数,唯一的出路是通过调用vTaskResume()或xTaskResumeFromISR() API函数。大多数应用程序不使用挂起状态。
7.3 准备就绪状态
处于“未运行”状态但未被阻塞或挂起的任务称为处于“就绪”状态。它们能够运行,因此“准备好”运行,但当前不处于运行状态。
7.4 状态转换图
图5扩展了前面过度简化的状态图,包括所有Not Running子状态。到目前为止,在示例中创建的任务还没有使用阻塞或挂起状态;它们只是在就绪状态和运行状态之间进行了转换,如图5中的粗体所示。
图5 全任务状态机
7.5 实例
1.阻塞实例
使用vTaskDelay()的调用,将任务置于Blocked状态,直到延迟期过期。具体程序参考4.1简单实例1,唯一修改的是task2任务函数,添加vTaskDelay()函数使task2进入阻塞状态,修改程序如下:
void task2(void *pvparameter)
{
while(1)
{
taskflagrun=1;
vTaskDelay(5);//使task2任务进入阻塞状态5ms
}
}
通过keil逻辑分析仪进行仿真,仿真结果如图6。
图6 阻塞状态