编辑推荐: |
本文主要介绍了
FreeRTOS任务之使用篇相关内容。 希望对您的学习有所帮助。
本文来自于CSDN,由火龙果软件Linda编辑、推荐。 |
|
1.区分
在FreeRTOS中,**任务(task)和线程(thread)的概念是相似的,但它们之间有一些微小的区别。**一般情况下将任务理解成线程也是可以的
1.1 相似之处:
任务和线程的功能:无论是任务还是线程,它们都是操作系统调度执行的基本单位。它们都代表了一个独立的执行流,执行一定的代码,拥有自己的执行上下文(例如程序计数器、堆栈、寄存器等)。
多任务/多线程的目的:任务和线程的主要目的是实现并发执行,使得多个代码块或功能模块能够“并行”处理,提高系统的响应性和效率。
上下文切换:任务和线程都需要支持上下文切换,也就是说操作系统需要在不同的任务或线程之间进行切换,保存当前任务的状态并加载另一个任务或线程的状态。
1.2 区别
FreeRTOS中的任务(Task)
在FreeRTOS中,任务是FreeRTOS调度的基本单位。FreeRTOS中的任务通常与线程在其他操作系统中的概念相似,但由于FreeRTOS是一个实时操作系统(RTOS),它的任务管理与一般操作系统中的线程管理有一些不同之处。
任务调度:FreeRTOS中的任务由FreeRTOS内核调度,而不像一般操作系统中的线程那样由操作系统调度器调度。
任务创建:FreeRTOS通过 xTaskCreate() 来创建任务,每个任务都有自己的栈,执行的是一个具体的任务函数。任务的调度基于优先级,可以是抢占式的,也可以是协作式的。
优先级:FreeRTOS任务具有优先级,任务可以设置不同的优先级。高优先级的任务会抢占低优先级的任务。
资源占用:FreeRTOS任务通常占用较少的资源,相比线程,它的实现和切换开销较小,因此适合在资源受限的嵌入式系统中使用。
传统操作系统中的线程(Thread):
在线程的传统操作系统(如Windows、Linux)中,线程是一个操作系统级别的概念。线程与进程相比,进程是独立的资源分配单位,而线程是在进程内执行的最小单位。
线程调度:传统操作系统的线程由操作系统的内核调度,线程可以有自己的内存空间(例如,堆栈和局部变量),并且与其他线程共享父进程的资源。
线程的资源和开销:线程通常占用较多的资源,因为它们在操作系统的调度中有更多的管理和控制,例如多核处理器的使用、内存分配等。线程切换的开销通常比FreeRTOS任务要大,因为操作系统需要管理更多的系统资源。
线程的多核支持:传统的操作系统支持在多核系统中调度线程,它们可能会在不同的CPU核心上并行执行。
**FreeRTOS中的任务(Task)**可以理解为一种特殊的线程,优化了资源管理并更适合嵌入式和实时操作系统。任务的创建和调度相对轻量级,任务优先级对调度有重要影响,且任务间的切换开销较小。
**传统操作系统中的线程(Thread)**通常是更重的概念,包含更多的资源管理和调度机制,适用于多核操作系统和大规模并发应用。
尽管它们在许多方面有相似性,任务和线程的区别主要在于它们的实现细节、资源占用、调度机制等,FreeRTOS中的任务更倾向于嵌入式和实时性要求高的场景,而线程则更常见于桌面操作系统和大型应用程序中。
2.任务的创建和删除
2.1 任务创建
每个任务的创建都需要分配一个栈的空间的给它,并且每个任务的栈空间是独一无二的,不能重复。freeRtos中有两种方式创建任务:
动态创建,指任务的栈的空间由系统随机分配
静态创建,指任务的栈的空间可以由自己指定,比如创建一个全局静态数组作为栈分配给该任务,只不过数组的容量需要和指定的栈的深度对应
2.1.1 动态创建
BaseType_t xTaskCreate(
TaskFunction_t pxTaskCode,
const char * const pcName,
const configSTACK_DEPTH_TYPE usStackDepth,
void * const pvParameters,
UBaseType_t uxPriority,
TaskHandle_t * const pxCreatedTask
);
|
xTaskCreate 函数的作用是创建一个任务,任务的定义包括任务代码、任务名称、栈大小、传递给任务的参数、优先级和任务句柄。这个函数是
FreeRTOS 创建和管理任务的核心接口。任务的参数根据任务的需求和系统的资源来配置,正确设置这些参数非常重要,以确保任务的正确运行和内存的合理利用。参数介绍如下:
pxTaskCode (任务函数指针)
TaskFunction_t(通常定义为 void (*)(void *pvParameters))。
这是一个指向任务函数的指针,任务函数是执行任务的主体代码。你需要为每个任务创建一个函数,这个函数会一直运行,除非任务被删除或挂起。
任务函数通常是一个无限循环,在函数中可以执行你需要完成的任务。如果任务有结束时需要做的事情,任务函数必须调用
vTaskDelete(NULL) 来删除自己。
void myTask(void *pvParameters) {
while(1) {
}
}
|
pcName (任务名称)
const char * const,是任务的名称(字符串类型)。
这是任务的名字,它对调试非常有用。FreeRTOS 内部不会使用这个名字,只是为了开发人员调试和识别任务。
名字的长度由 configMAX_TASK_NAME_LEN 决定,通常是在 FreeRTOSConfig.h
中配置的。如果没有特别要求,通常保持默认值即可。
xTaskCreate(myTask, "MyTask", 128, NULL, 1, NULL);
|
在这个例子中,任务的名字是 "MyTask"。
usStackDepth (栈深度)
configSTACK_DEPTH_TYPE,通常是 uint16_t 类型。
为每个任务分配的栈大小。任务栈用于存储任务的局部变量、函数调用等。这里的 usStackDepth
表示栈的深度(单位是字)。例如,如果传入 100,表示栈大小为 100 字(通常是 400 字节,如果
word 是 4 字节。) ---- 查看了该函数内部的调用,发现貌似栈的基本单位是uint_32,也就是word默认的就是4字节:
pxNewTCB->pxStack = ( StackType_t * ) pvPortMallocStack(
( ( ( size_t ) usStackDepth ) * sizeof( StackType_t
) ) );其中StackType_t:typedef portSTACK_TYPE StackType_t;,而portSTACK_TYPE
:#define portSTACK_TYPE uint32_t,为4字节
注意:栈的大小要根据任务的需求来设置,太小可能导致栈溢出,太大则浪费内存。栈的大小有时需要通过经验或分析工具来确定。FreeRTOS
本身没有提供非常精确的栈计算方法,但可以通过调试工具分析栈的使用情况。
xTaskCreate(myTask, "MyTask", 128, NULL, 1, NULL);
|
pvParameters (任务参数)
void *(指向任何类型的指针)。
这个参数会被传递给任务的任务函数 pxTaskCode。如果任务不需要任何参数,可以传递 NULL。它使得任务可以接收传递给它的额外数据(如结构体或指针等)。
void myTask(void *pvParameters) {
}
xTaskCreate(myTask, "MyTask", 128, (void *)parameter, 1, NULL);
|
在这个例子中,pvParameters 是一个传递给任务的指针,可以让任务访问外部传递的数据。
uxPriority (任务优先级)
UBaseType_t,通常是 uint32_t 或 unsigned int 类型。
定义任务的优先级。FreeRTOS 中的任务是基于优先级调度的,优先级值越小,优先级越低。有效优先级范围是
0 到 configMAX_PRIORITIES - 1,其中 configMAX_PRIORITIES
是在 FreeRTOSConfig.h 中定义的最大优先级数。
xTaskCreate(myTask, "MyTask", 128, NULL, 2, NULL);
|
如果传入的优先级超过了 configMAX_PRIORITIES - 1,FreeRTOS 会自动将其调整为
configMAX_PRIORITIES - 1。
pxCreatedTask (任务句柄)
TaskHandle_t *,这是一个指向任务句柄的指针。 在FreeRTOS中,每个任务在创建时都会分配一个任务句柄。这个句柄是一个指向任务控制块(TCB)的指针。
如果想在任务创建后保存任务的句柄(用于之后操作任务,比如修改任务优先级、删除任务等),可以传入这个参数。任务句柄是一个标识符,实现在任务间进行交互或控制任务。
如果不需要这个句柄,可以将其设为 NULL,这样就不保存任务句柄。
TaskHandle_t xTaskHandle;
xTaskCreate(myTask, "MyTask", 128, NULL, 1, &xTaskHandle);
|
之后,可以使用 xTaskHandle 来控制任务,例如改变任务优先级:
vTaskPrioritySet(xTaskHandle, 2);
|
2.1.2 静态创建
TaskHandle_t xTaskCreateStatic( TaskFunction_t pxTaskCode,
const char * const pcName,
const uint32_t ulStackDepth,
void * const pvParameters,
UBaseType_t uxPriority,
StackType_t * const puxStackBuffer,
StaticTask_t * const pxTaskBuffer )
|
参6的StackType_t * const puxStackBuffer,可以指定一个静态数组传入作为所创建任务的栈,需要注意数组类型实际上是uint_32
要想使用该文件还要去配置FreeRtos中的文件:
实现函数
2.1.3 最后一个参数
xTaskCreate`的最后一个参数:`TaskHandle_t * const pxCreatedTask
xTaskCreateStatic`的最后一个参数:` StaticTask_t * const pxTaskBuffer
|
其实这两个参数最后都是指向一个TCB_t结构体:
typedef tskTCB TCB_t;
typedef struct tskTaskControlBlock
{
volatile StackType_t * pxTopOfStack;
#if ( portUSING_MPU_WRAPPERS == 1 )
xMPU_SETTINGS xMPUSettings;
#endif
ListItem_t xStateListItem;
ListItem_t xEventListItem;
UBaseType_t uxPriority;
StackType_t * pxStack;
char pcTaskName[ configMAX_TASK_NAME_LEN ];
#if ( ( portSTACK_GROWTH > 0 ) || ( configRECORD_STACK_HIGH_ADDRESS == 1 ) )
StackType_t * pxEndOfStack;
#endif
#if ( portCRITICAL_NESTING_IN_TCB == 1 )
UBaseType_t uxCriticalNesting;
#endif
#if ( configUSE_TRACE_FACILITY == 1 )
UBaseType_t uxTCBNumber;
UBaseType_t uxTaskNumber;
#endif
#if ( configUSE_MUTEXES == 1 )
UBaseType_t uxBasePriority;
UBaseType_t uxMutexesHeld;
#endif
#if ( configUSE_APPLICATION_TASK_TAG == 1 )
TaskHookFunction_t pxTaskTag;
#endif
#if ( configNUM_THREAD_LOCAL_STORAGE_POINTERS > 0 )
void * pvThreadLocalStoragePointers[ configNUM_THREAD_LOCAL_STORAGE_POINTERS ];
#endif
#if ( configGENERATE_RUN_TIME_STATS == 1 )
uint32_t ulRunTimeCounter;
#endif
#if ( configUSE_NEWLIB_REENTRANT == 1 )
struct _reent xNewLib_reent;
#endif
#if ( configUSE_TASK_NOTIFICATIONS == 1 )
volatile uint32_t ulNotifiedValue[ configTASK_NOTIFICATION_ARRAY_ENTRIES ];
volatile uint8_t ucNotifyState[ configTASK_NOTIFICATION_ARRAY_ENTRIES ];
#endif
#if ( tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE != 0 )
uint8_t ucStaticallyAllocated;
#endif
#if ( INCLUDE_xTaskAbortDelay == 1 )
uint8_t ucDelayAborted;
#endif
#if ( configUSE_POSIX_ERRNO == 1 )
int iTaskErrno;
#endif
} tskTCB;
|
freeRtos给出的解释:Task control block. A task control block
(TCB) is allocated for each task, and stores task
state information, including a pointer to the task’s
context(the task’s run time environment, including
register values)
任务控制块。为每个任务分配一个任务控制块(TCB),它存储任务的状态信息,包括指向任务上下文的指针(任务的运行时环境,包括寄存器值)。
具体的是如何指向该结构体,感兴趣的可以去看看函数的内部实现
2.1.4 示例程序
#include "FreeRTOS.h"
#include "task.h"
#include <stdio.h>
#define TASK_STACK_SIZE 128
#define TASK_PRIORITY 1
static StackType_t xTaskStack[TASK_STACK_SIZE];
static StaticTask_t xTaskBuffer;
void vTaskDynamic(void *pvParameters) {
while(1) {
printf("This is the dynamic task!\n");
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
void vTaskStatic(void *pvParameters) {
while(1) {
printf("This is the static task!\n");
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
int main(void) {
xTaskCreate(vTaskDynamic, "DynamicTask", TASK_STACK_SIZE, NULL, TASK_PRIORITY, NULL);
xTaskCreateStatic(
vTaskStatic,
"StaticTask",
TASK_STACK_SIZE,
NULL,
TASK_PRIORITY,
xTaskStack,
&xTaskBuffer
);
vTaskStartScheduler();
while(1);
return 0;
}
|
下面是各自的示例程序,静态和动态
freertos_example_createtaskstatic.zip
freertos_example_createtask.zip
以上都是基于同个优先级的情况下的,同优先级的任务表面上是并行,实际上却是交叉运行的
2.2 任务删除
void vTaskDelete( TaskHandle_t xTaskToDelete );
|
2.2.1 三种删除
#include "FreeRTOS.h"
#include "task.h"
#include <stdio.h>
TaskHandle_t xTaskHandle1 = NULL;
TaskHandle_t xTaskHandle2 = NULL;
void vTaskFunction1(void *pvParameters) {
while(1) {
printf("Task 1 is running.\n");
if (some_condition) {
printf("Task 1 is deleting itself.\n");
vTaskDelete(NULL);
}
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
void vTaskFunction2(void *pvParameters) {
while(1) {
printf("Task 2 is running.\n");
if (some_condition) {
printf("Task 2 is being deleted by Task 1.\n");
vTaskDelete(xTaskHandle2);
}
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
void vTaskFunction3(void *pvParameters) {
while(1) {
printf("Task 3 is running.\n");
if (some_condition) {
printf("Task 3 is deleting Task 1.\n");
vTaskDelete(xTaskHandle1);
}
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
int main(void) {
xTaskCreate(vTaskFunction1, "Task1", 128, NULL, 1, &xTaskHandle1);
xTaskCreate(vTaskFunction2, "Task2", 128, NULL, 1, &xTaskHandle2);
xTaskCreate(vTaskFunction3, "Task3", 128, NULL, 1, NULL);
vTaskStartScheduler();
while(1);
return 0;
}
|
自杀(**vTaskDelete(NULL)**):任务1在满足某个条件时调用 vTaskDelete(NULL),删除自己。这个任务会从调度中移除,并且其栈和控制块被释放。
被杀(**vTaskDelete(xTaskHandle2)**):任务2可以被任务1删除。在任务1中,调用
vTaskDelete(xTaskHandle2) 删除任务2。这是通过任务句柄完成的,任务2会被从调度中移除。
杀人(**vTaskDelete(xTaskHandle1)**):任务3可以删除任务1。任务3通过调用
vTaskDelete(xTaskHandle1) 删除任务1。这是通过任务句柄完成的,任务1会被从调度中移除。
以上内容都是基于创建任务的时候,任务的优先级都是相同的情况 — 参数:**uxPriority**
2.2.2 涉及优先级
FreeRTOS在xTaskCreate中分配TCB和栈,但并不一定就是在vTaskDelete中释放TCB和栈。可以尝试去不断轮流调用xTaskCreate和vTaskDelete,最终的结果是内存被消耗殆尽(自杀的情况下)
调用 vTaskDelete() 删除任务时,FreeRTOS 不会立即释放任务的 TCB 和栈内存。这是因为:
任务可能正在执行代码(例如处于临界区、持有资源),直接释放内存可能导致不可预测的行为(如内存访问冲突)。
FreeRTOS 的 vTaskDelete 会将待释放的任务的 TCB 和栈内存标记为“待回收”,并插入到一个待删除任务列表中。
实际的释放操作由 Idle 任务(空闲任务)完成。Idle 任务在系统空闲时(即没有其他任务运行时)会检查这个列表,并安全地释放内存。
Idle 任务的优先级是 0(最低优先级)。如果其他任务始终处于就绪状态(例如高优先级任务通过 vTaskDelay()
主动释放 CPU),Idle 任务可能无法及时运行。需要设计任务调度策略,确保 Idle 任务有机会执行。例如,在任务中适当使用
vTaskDelay() 或阻塞式 API。
void vTask1( void *pvParameters )
{
const TickType_t xDelay100ms = pdMS_TO_TICKS( 100UL );
BaseType_t ret;
for( ;; ){
printf("Task1 is running\r\n");
ret = xTaskCreate( vTask2, "Task 2", 1000, NULL, 2, &xTask2Handle );
if (ret != pdPASS)
printf("Create Task2 Failed\r\n");
vTaskDelay( xDelay100ms );
}
}
void vTask2( void *pvParameters )
{
printf("Task2 is running and about to delete itself\r\n");
vTaskDelete(xTask2Handle);
}
int main( void )
{
prvSetupHardware();
xTaskCreate(vTask1, "Task 1", 1000, NULL, 1, NULL);
vTaskStartScheduler();
return 0;
}
|
结果是可以看到先打印:Task1 is running,然后打印:Task2 is running
and about to delete itself,后面依次轮流打印
main函数中创建任务1,优先级为1。任务1运行时,它创建任务2,任务2的优先级是2。任务2的优先级最高,它马上执行。
任务2打印一句话后,就删除了自己。(可以尝试去将任务2中的代码加一个循环打印,就可以发现任务1的代码接下来没机会执行到)
任务2被删除后,任务1的优先级最高,轮到任务1继续运行,它调用 vTaskDelay() 进入Block状
态
任务1 Block期间,轮到Idle任务执行:它释放任务2的内存(TCB、栈)时间到后,任务1变为最高优先级的任务继续执行。
如此循环。
如果将任务1中的vTaskDelay注释到,起初的程序现象没啥问题,但是后面就会出现:Create
Task2 Failed,这就是前面提到的内存被消耗殆尽,任务1一直在执行导致最低优先级的Idle任务没执行到
3.任务优先级
这一部分上面《任务的删除》部分已经讲的差不多了,高优先级的任务先运行,这里做补充:
FreeRTOS的调度器可以使用2种方法来快速找出优先级最高的、可以运行的任务。使用不同的方法时,configMAX_PRIORITIES
的取值有所不同。
(1) 通用方法(C 语言实现)
配置:configUSE_PORT_OPTIMISED_TASK_SELECTION = 0 或未定义。
兼容所有架构,代码可移植。
对 configMAX_PRIORITIES 无限制(但越大越消耗内存和时间)。
通过遍历优先级位图(uxTopReadyPriority)找到最高优先级任务。
适用于优先级数量较多(>32)或硬件不支持优化指令的架构。
(2) 架构优化方法(汇编加速)
配置:configUSE_PORT_OPTIMISED_TASK_SELECTION = 1。
依赖硬件指令(如 ARM 的 CLZ 指令)快速定位最高优先级位。
限制:configMAX_PRIORITIES ≤ 32。
查找效率高,时间复杂度为 O(1)。
适用于优先级数量 ≤32 且硬件支持优化指令(如 Cortex-M 系列)。
猜测的内部大概实现如下:
1. 就绪任务列表(Ready List)
按优先级分组:每个优先级对应一个链表,任务根据优先级插入对应链表。
优先级位图(uxTopReadyPriority):
一个 32 位变量,每一位表示对应优先级是否有就绪任务。
例如,优先级 5 的任务就绪时,第 5 位被置 1。
2. 查找最高优先级任务的流程
从最高优先级向最低优先级遍历 uxTopReadyPriority。
找到第一个非零位,确定最高优先级。
从该优先级的链表中取出第一个任务执行。
for (priority = configMAX_PRIORITIES - 1; priority >= 0; priority--) {
if (uxTopReadyPriority & (1UL << priority)) {
return priority;
}
}
|
任务的优先级这部分涉及到调度算法,调度行为规则如下
抢占式调度:
高优先级任务就绪时,立即抢占低优先级任务(也就是低优先级任务要把CPU让给已经就绪的高优先级任务,让其去执行)。
通过中断或任务主动释放 CPU(如 vTaskDelay())触发调度。
时间片轮转(同优先级任务):
相同优先级的任务共享 CPU 时间,通过时间片轮流执行(如“喂饭”和“回复信息”交替执行)。
时间片长度由 configTICK_RATE_HZ 定义的系统节拍周期决定。
创建任务后给定的任务优先级,在后续也是可以在任务当中去进行更改的
3.1 实验1
以上的内容都是freertos内部对任务优先级的一些设计内容,接下来猜一下这个代码的结果,能猜出就对任务优先级有大概的了解了。
void vTask1( void *pvParameters )
{
for( ;; ){
printf("T1\r\n");
}
}
void vTask2( void *pvParameters )
{
for( ;; ){
printf("T2\r\n");
}
}
void vTask3( void *pvParameters )
{
const TickType_t xDelay3000ms = pdMS_TO_TICKS( 3000UL );
for( ;; ){
printf("T3\r\n");
vTaskDelay( xDelay3000ms );
}
}
int main(void)
{
prvSetupHardware();
xTaskCreate(vTask1, "Task 1", 1000, NULL, 1, NULL);
xTaskCreate(vTask2, "Task 2", 1000, NULL, 1, NULL);
xTaskCreate(vTask3, "Task 3", 1000, NULL, 2, NULL);
vTaskStartScheduler();
return 0;
}
|
3.2 实验2:修改任务优先级
使用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)。
下面是示例代码:
void vTask1( void *pvParameters )
{
UBaseType_t uxPriority;
uxPriority = uxTaskPriorityGet( NULL );
for( ;; )
{
printf( "Task 1 is running\r\n" );
printf("About to raise the Task 2 priority\r\n" );
vTaskPrioritySet( xTask2Handle, ( uxPriority + 1 ) );
}
}
void vTask2( void *pvParameters )
{
UBaseType_t uxPriority;
uxPriority = uxTaskPriorityGet( NULL );
for( ;; )
{
printf( "Task 2 is running\r\n" );
printf( "About to lower the Task 2 priority\r\n" );
vTaskPrioritySet( NULL, ( uxPriority - 2 ) );
}
}
int main( void )
{
prvSetupHardware();
xTaskCreate( vTask1, "Task 1", 1000, NULL, 2, NULL );
xTaskCreate( vTask2, "Task 2", 1000, NULL, 1, &xTask2Handle );
vTaskStartScheduler();
return 0;
}
|
其中提到的阻塞或者暂停状态,后续任务的状态中会将讲。
|