文章目录
一、任务的创建的API函数
可以看到,这里主要分成了动态创建和静态创建两种方法。
1.动态创建和静态创建的区别
(1)对于内存分配与管理:
动态创建任务:在运行时动态分配任务所需的内存空间,并在任务完成后释放内存空间。这种方式可以根据任务的需要动态创建和删除任务,适用于任务数量不确定或需要动态管理任务的场景。需要注意的是,动态创建任务需要在堆上分配足够的内存空间来存储任务的栈。
静态创建任务:在编译时分配任务所需的内存空间,并且在运行时不会释放。这种方式适用于任务数量是固定的,并且在编译时已知的情况下。静态创建任务的内存分配是在全局范围内定义任务的结构体和栈空间,在任务创建时使用这些静态分配的内存。
2.动态创建任务函数原型
动态创建任务函数这几个参数极其重要。
(1)pxTaskCode: 指向任务函数的指针,表示任务要执行的函数。
任务函数的原型为void vTaskFunction(void *pvParameters),其中pvParameters是传递给任务函数的参数。
(2)pcName: 任务名字,是一个字符串,用于标识任务的名称。最大长度由宏configMAX_TASK_NAME_LEN定义。
(3)usStackDepth: 任务堆栈大小,以字为单位。表示任务所需的堆栈空间大小。32位单片机,一个字是4个字节,因此比如任务堆栈是128,相当于这个任务有512字节的动态空间。
(4)pvParameters: 传递给任务函数的参数,可以是任意类型的指针。通过该参数,可以向任务函数传递所需的参数。其实就是函数的传参。
(5)uxPriority: 任务优先级,范围是0 ~ configMAX_PRIORITIES - 1。数值越大,优先级越高。这里范围是0-32。
(6)pxCreatedTask: 任务句柄,即任务的任务控制块。通过该参数,可以获取创建的任务的句柄,可以用于后续操作,如删除任务等。
任务句柄
任务句柄是一个用于标识任务的变量,它在FreeRTOS中用来唯一标识一个任务。通过任务句柄,可以对任务进行操作,如挂起、恢复、删除等。
任务句柄的类型在FreeRTOS中一般是TaskHandle_t,它实际上是一个指向任务控制块(TCB,Task Control Block)的指针。任务控制块是一个数据结构,用于存储和管理任务的相关信息,如任务的状态、优先级、堆栈指针等。
创建任务时,可以通过传递一个指向TaskHandle_t类型的指针作为参数,来获取创建的任务的句柄。这样就可以在后续的操作中使用该句柄来引用该任务。
任务句柄的主要作用有:
任务的创建:通过任务句柄可以获取创建的任务的句柄,可以用于后续的操作。
任务的挂起和恢复:可以使用任务句柄来挂起和恢复某个任务的执行。
任务的删除:可以使用任务句柄来删除某个任务。
任务的查询和状态获取:可以使用任务句柄来查询和获取任务的状态、优先级等信息。
创建动态任务的流程
我就以实际代码为例:
//创建TASK1任务
xTaskCreate((TaskFunction_t )task1_task, //任务函数
(const char* )"task1_task", //函数名
(uint16_t )TASK1_STK_SIZE, //任务堆栈大小
(void* )NULL, //传参为空
(UBaseType_t )TASK1_TASK_PRIO, //任务优先级
(TaskHandle_t* )&Task1Task_Handler); //任务句柄
//创建TASK2任务
xTaskCreate((TaskFunction_t )task2_task,
(const char* )"task2_task",
(uint16_t )TASK2_STK_SIZE,
(void* )NULL,
(UBaseType_t )TASK2_TASK_PRIO,
(TaskHandle_t* )&Task2Task_Handler);
正点原子总结如下:宏为1说明是动态创建任务形式,然后两步就是上面的代码实现了。
这里在内部,有一个TCB结构体成员赋值,TCB即TASK CONTROL BLOCK任务控制块。
任务控制块结构体成员介绍
3.静态创建任务函数原型
这里,与动态的xTaskCreate函数不同的是,静态xTaskCreateStatic函数使用用户分配的任务控制块(pxTaskBuffer)和任务堆栈(puxStackBuffer),而不是动态分配内存。
需要注意的是,任务控制块和任务堆栈的分配和管理由用户负责,因此在使用xTaskCreateStatic函数创建任务时,需要确保分配的内存足够,并在任务创建后,不再使用这些内存。此外,任务控制块和任务堆栈的生命周期必须覆盖整个任务的执行周期。
创建静态任务的流程
//获取空闲任务地任务堆栈和任务控制块内存,因为本例程使用的
//静态内存,因此空闲任务的任务堆栈和任务控制块的内存就应该
//有用户来提供,FreeRTOS提供了接口函数vApplicationGetIdleTaskMemory()
//实现此函数即可。
//ppxIdleTaskTCBBuffer:任务控制块内存
//ppxIdleTaskStackBuffer:任务堆栈内存
//pulIdleTaskStackSize:任务堆栈大小
void vApplicationGetIdleTaskMemory(StaticTask_t **ppxIdleTaskTCBBuffer,
StackType_t **ppxIdleTaskStackBuffer,
uint32_t *pulIdleTaskStackSize)
{
*ppxIdleTaskTCBBuffer=&IdleTaskTCB;
*ppxIdleTaskStackBuffer=IdleTaskStack;
*pulIdleTaskStackSize=configMINIMAL_STACK_SIZE;
}
//获取定时器服务任务的任务堆栈和任务控制块内存
//ppxTimerTaskTCBBuffer:任务控制块内存
//ppxTimerTaskStackBuffer:任务堆栈内存
//pulTimerTaskStackSize:任务堆栈大小
void vApplicationGetTimerTaskMemory(StaticTask_t **ppxTimerTaskTCBBuffer,
StackType_t **ppxTimerTaskStackBuffer,
uint32_t *pulTimerTaskStackSize)
{
*ppxTimerTaskTCBBuffer=&TimerTaskTCB;
*ppxTimerTaskStackBuffer=TimerTaskStack;
*pulTimerTaskStackSize=configTIMER_TASK_STACK_DEPTH;
}
而代码层面创建任务跟动态基本一模一样。
二、任务的删除的API函数
举例:
//创建开始任务
xTaskCreate((TaskFunction_t )start_task, //任务函数
(const char* )"start_task", //任务名称
(uint16_t )START_STK_SIZE, //任务堆栈大小
(void* )NULL, //传递给任务函数的参数
(UBaseType_t )START_TASK_PRIO, //任务优先级
(TaskHandle_t* )&StartTask_Handler); //任务句柄
vTaskDelete(StartTask_Handler); //删除开始任务
我先创建了一个开始任务,然后执行完开始任务后就调用vTaskDelete()函数删除这个任务,传参是前面设置好的任务句柄StartTask_Handler。
注意:
动态创建的任务,删除时空闲任务会自动帮你释放掉系统分配的内存,但是静态创建的任务,必须提前由用户自己去释放内存,否则会内存泄露。
删除任务的流程
用户层面很简单,就两步,但是其实内部很复杂。
三、动态任务创建和删除的代码例程
这里包括后续写代码都有个写法:用户自己需要的功能任务的创建要放在start_task开始任务里面创建,然后把开始任务删除即可。开始任务只执行一次。
先使用宏定义缺点确定每个任务相关配置。
任务堆栈
(1)为什么任务堆栈设置是128字?
答:这是一个偏大的值,为了使得这个任务开辟的堆栈空间大一点,防止爆内存了。
(2)有一个函数可以查看指定任务的任务栈的历史剩余最小值?
答:是的。在FreeRTOS中,可以使用vTaskGetStackHighWaterMark函数来查看指定任务的任务栈的历史剩余最小值。
函数原型如下:
UBaseType_t vTaskGetStackHighWaterMark( TaskHandle_t xTask );
参数xTask是要查询的任务的句柄。
函数返回的是任务栈的历史剩余最小值,以字为单位。该值表示任务栈中最少剩余的字节数,即任务栈使用的最大字节数。
可以通过周期性地调用vTaskGetStackHighWaterMark函数来监测任务栈的使用情况,以便调整任务堆栈的大小,以防止堆栈溢出。
建立四个任务函数
void freertos_demo(void)
{
xTaskCreate((TaskFunction_t ) start_task,
(char * ) "start_task",
(configSTACK_DEPTH_TYPE ) START_TASK_STACK_SIZE,
(void * ) NULL,
(UBaseType_t ) START_TASK_PRIO,
(TaskHandle_t * ) &start_task_handler );
vTaskStartScheduler();
}
void start_task( void * pvParameters )
{
taskENTER_CRITICAL(); /* 进入临界区 */
xTaskCreate((TaskFunction_t ) task1,
(char * ) "task1",
(configSTACK_DEPTH_TYPE ) TASK1_STACK_SIZE,
(void * ) NULL,
(UBaseType_t ) TASK1_PRIO,
(TaskHandle_t * ) &task1_handler );
xTaskCreate((TaskFunction_t ) task2,
(char * ) "task2",
(configSTACK_DEPTH_TYPE ) TASK2_STACK_SIZE,
(void * ) NULL,
(UBaseType_t ) TASK2_PRIO,
(TaskHandle_t * ) &task2_handler );
xTaskCreate((TaskFunction_t ) task3,
(char * ) "task3",
(configSTACK_DEPTH_TYPE ) TASK3_STACK_SIZE,
(void * ) NULL,
(UBaseType_t ) TASK3_PRIO,
(TaskHandle_t * ) &task3_handler );
vTaskDelete(NULL);
taskEXIT_CRITICAL(); /* 退出临界区 */
}
具体三个功能函数代码
/* 任务一,实现LED0每500ms翻转一次 */
void task1( void * pvParameters )
{
while(1)
{
printf("task1正在运行!!!\r\n");
LED0_TOGGLE();
vTaskDelay(500);
}
}
/* 任务二,实现LED1每500ms翻转一次 */
void task2( void * pvParameters )
{
while(1)
{
printf("task2正在运行!!!\r\n");
LED1_TOGGLE();
vTaskDelay(500);
}
}
/* 任务三,判断按键KEY0,按下KEY0删除task1 */
void task3( void * pvParameters )
{
uint8_t key = 0;
while(1)
{
printf("task3正在运行!!!\r\n");
key = key_scan(0);
if(key == KEY0_PRES)
{
if(task1_handler != NULL)
{
printf("删除task1任务\r\n");
vTaskDelete(task1_handler);
task1_handler = NULL;
}
}
vTaskDelay(10);
}
}
具体效果
有一个问题是:为什么低优先级的task1和2先执行,然后再执行的task3呢?
答:开始任务里,是按照创建了task1-task2-task3的顺序的,创建好task1后优先级比start开始任务优先级高,所以直接先进入task1任务了,task1里面delay500ms时进入阻塞态,使得回到start开始任务,创建好task2任务,同样先执行task2任务,阻塞后再创建task3任务,这样就导致了如图情况的发生。
那如何避免这种情况呢?----使用临界区
进入临界区的目的是为了保护对任务列表的操作。在创建任务之前,通过调用taskENTER_CRITICAL()函数进入临界区。然后创建了两个任务:task1_task和task2_task。创建任务的过程中,可能涉及对任务列表的修改操作,例如将任务的控制块和堆栈信息添加到任务列表中。通过进入临界区,可以确保在创建任务的过程中,只有一个任务能够访问任务列表,避免了竞争条件和数据一致性的问题。
四、动态创建任务的内在过程
xTaskCreate()函数会在内部进行一系列的操作来创建任务。
①任务堆栈的分配:首先,xTaskCreate()函数会根据指定的任务堆栈大小(TASKx_STK_SIZE)来分配一块内存空间,用作任务的堆栈。堆栈是用来保存任务的局部变量和函数调用信息的空间。
②任务控制块的创建:接下来,xTaskCreate()函数会创建任务的控制块,也称为任务控制结构(TCB)。任务控制块包含了任务的状态信息、优先级、堆栈顶部指针和其他与任务相关的参数。
③初始化任务控制块:xTaskCreate()函数会对任务控制块进行初始化,包括设置任务的优先级、堆栈指针、状态等信息。
④将任务添加到就绪队列:创建任务后,xTaskCreate()函数会将任务添加到就绪队列中,使任务可以被调度器调度。
⑤任务的执行:一旦任务被添加到就绪队列中,调度器会根据任务的优先级和调度算法来决定任务的执行顺序。任务会在分配给它的时间片内执行,直到任务主动挂起或被其他任务抢占。
五、删除任务的内在过程
在FreeRTOS中,删除任务的过程可以通过调用vTaskDelete()函数来实现。vTaskDelete()函数会执行以下内在过程:
①任务自愿挂起:首先,vTaskDelete()函数会将当前任务自愿挂起。这意味着当前任务将进入挂起状态,不再参与调度,等待被删除。
②释放任务资源:vTaskDelete()函数会释放当前任务占用的资源,包括任务的控制块和堆栈空间。这样,其他任务就可以使用这些资源了。
③从任务列表中移除:vTaskDelete()函数会将当前任务从任务列表中移除,使得调度器不再调度该任务。
④删除任务的清理操作:vTaskDelete()函数会执行一些清理操作,例如释放任务所持有的信号量或删除任务的消息队列等。
⑤任务切换:一旦vTaskDelete()函数完成了任务的删除操作,调度器会选择一个新的任务来执行。这个新的任务可以是就绪队列中的其他任务,或者是空闲任务(如果没有其他任务可执行)。
六、静态创建任务的方法
第一张是动态创建时的提前声明,第二张是静态创建任务。二者区别在于静态要创建一个静态任务控制块。
1.为什么创建静态任务要创建空闲任务和定时器任务?
创建空闲任务和定时器任务的目的是为了提供系统的基本功能和调度。
空闲任务(Idle Task): 空闲任务是系统中的一个特殊任务,它在没有其他任务需要运行时执行。空闲任务的作用是防止系统进入死循环,保证系统的正常运行。当系统中没有其他任务需要运行时,空闲任务会被调度执行。它通常执行一些低优先级的操作,如系统休眠、节能模式等。
创建空闲任务的静态任务控制块和堆栈是为了分配空闲任务的内存空间,并在系统启动时使用这些内存空间创建空闲任务。
定时器任务(Timer Task): 定时器任务是一个特殊的任务,用于管理系统的软件定时器。软件定时器是FreeRTOS提供的一个功能,用于在特定时间间隔内触发任务或事件。定时器任务负责管理和处理软件定时器的相关操作,包括创建、删除、启动、停止定时器等。
创建定时器任务的静态任务控制块和堆栈是为了分配定时器任务的内存空间,并在系统启动时使用这些内存空间创建定时器任务。
通过创建空闲任务和定时器任务的静态任务控制块和堆栈,可以为这些任务分配足够的内存空间,并在系统启动时使用这些内存空间创建和管理这些任务。这样可以保证系统的基本功能和调度的正常运行。
2.为什么创建动态任务不需要创建空闲任务和定时器任务?
创建动态任务时,不需要显式地创建空闲任务和定时器任务的原因是因为FreeRTOS会自动创建和管理这些任务。
空闲任务: FreeRTOS会自动创建一个名为"Idle"的空闲任务,并在系统没有其他任务需要运行时执行。空闲任务的优先级是最低的,它会执行一些低优先级的操作,如系统休眠、节能模式等。由于空闲任务是自动创建和管理的,因此在动态创建任务时不需要显式地创建空闲任务。
定时器任务: FreeRTOS会自动创建一个名为"Timer"的定时器任务,用于管理系统的软件定时器。定时器任务负责管理和处理软件定时器的相关操作,包括创建、删除、启动、停止定时器等。由于定时器任务是自动创建和管理的,因此在动态创建任务时不需要显式地创建定时器任务。
创建动态任务时,只需要调用FreeRTOS提供的任务创建函数,传入任务相关的参数,即可创建动态任务。FreeRTOS会自动进行任务调度和管理,包括空闲任务和定时器任务的创建和执行。
七、总结