FreeRtos中任务的有关理解
创建任务的接口函数
osThreadNew 和 xTaskCreate 都是用于创建任务(线程)的函数,但它们属于不同的接口和框架。
接口层级:
-
- xTaskCreate: 是 FreeRTOS 的原生 API 直接用于创建任务,属于 FreeRTOS 的核心函数。这是一个更低级的接口,直接与 FreeRTOS 的调度器交互。
- osThreadNew: 是根据 CMSIS-RTOS API 规范定义的接口,目的是提供一个更高层次的、硬件无关的 RTOS 接口。CMSIS-RTOS API 可以被视为在 FreeRTOS 之上提供了一层抽象,使得代码更具可移植性。
- 使用
xTaskCreate是一个针对 FreeRTOS 的直接接口,提供了更底层的控制。 - 使用
osThreadNew则是一个更加标准化和可移植的方式,适合需要与不同 RTOS 系统兼容的项目。
freertos中创建任务时使用的函数:(静态和动态)
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 内部不使用。pcName 的长度为 configMAX_TASK_NAME_LEN。 |
| usStackDepth | 每个任务都有自己的栈,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)。如果以后需要对该任务进行操作,如修改优先级,则需要使用此句柄。如果不需要使用该句柄,可以传入 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 |
那么动态和静态分别适合在什么场景下使用呢?
如果系统有固定的、确定的任务需求,并且对内存有严格的使用要求,建议使用静态创建任务。另一方面,如果任务数量和优先级在运行时可以动态变化,动态创建任务会是更灵活的选择。
编程实例1
在02_freertos_first_app中,默认任务运行LCD程序,显示字符,自己使用xTaskCreate函数创建的任务中运行LED闪烁程序。LCD程序和LED闪烁程序内部都是死循环实现,在裸机中是不可能同时运行的,但是在freertos中,通过任务调度,可以让用户看到两个程序同时运行的现象。(本质上是交替执行的)
void Led_Test(void) { Led_Init(); while (1) { Led_Control(LED_GREEN, 1); HAL_Delay(500); Led_Control(LED_GREEN, 0); HAL_Delay(500); } } void OLED_Test(void) { uint16_t cnt=0; OLED_Init(); // 清屏 OLED_Clear(); while (1) { // 在(0, 0)打印'A' OLED_PutChar(0, 0, 'A'); // 在(1, 0)打印'Y' OLED_PutChar(1, 0, 'Y'); // 在第0列第2页打印一个字符串"Hello World!" OLED_PrintString(0, 2, "Hello World!"); OLED_PrintSignedVal(0, 4, cnt++); } } //默认任务 defaultTaskHandle = osThreadNew(StartDefaultTask, NULL, &defaultTask_attributes); //led任务 xTaskCreate(MyTask,"myfirsttask",128,NULL,osPriorityNormal,NULL); void StartDefaultTask(void *argument) { /* USER CODE BEGIN StartDefaultTask */ /* Infinite loop */ for(;;) { //osDelay(1); //Led_Test(); LCD_Test(); } } void MyTask(void *argument) { while(1) { Led_Test(); } }
在创建任务时,FreeRTOS会在堆中开辟出一块空间,用于存放任务的控制信息TCB块和栈区Stack用于储存任务相关的变量,每一个任务都有自己的栈。
在任务切换时,FreeRTOS 需要保存当前任务的上下文,以便将来能够恢复。而后,切换到新任务时,FreeRTOS 需要加载新的任务上下文。这一过程称为上下文切换。保存现场保存在哪里呢,就保存在栈里。保存的什么呢,包括寄存器的值,pc,sp等。
- 做何事
- 栈
- 优先级
freertos任务调度策略:
高优先级的先执行,若是优先级相同的任务,这些任务会以轮询的方式进行调度,即时间片轮转。时间片的长度由系统的节拍频率来决定,FreeRTOSConfig.h 文件中configTICK_RATE_HZ 宏定义来确定节拍频率。在轮转过程中如果某个任务进入就绪状态(例如,任务主动阻塞等待某个事件或延迟),调度器会选择运行的下一个任务。一旦有更高优先级的任务就绪,会马上得到运行。
调度方法:使用链表进行理解

在创建一个任务之后,它就处于就绪状态,根据优先级挂载在就绪链表中,如上图所示,startDefaultTask、Led_Test、ColorLED_Test三个任务都是默认优先级24,他们根据创建顺序依次挂载在ReadyList[24]中,pxCurrentTCB指向当前任务TCB(pxCurrentTCB根据创建的任务,依次指向startDefaultTask、Led_Test、ColorLED_Test)。那么系统开始运行时,ColorLED_Test任务会率先开始运行,运行一个时间片之后,系统遍历就绪链表,找到第一个非空链表(即优先级最高的),然后pxCurrentTCB就指向startDefaultTask,这个任务得到运行。
如果运行过程中,某一个任务发生阻塞,那么就把它从就绪链表删除,移动到阻塞链表,阻塞解除之后,再放回来。当调用vTaskSuspend时,会将任务挂载在挂起链表中。

当发生tick中断,也就是要切换任务时,会发生:
- cnt++
- 判断阻塞链表中的任务是否可以恢复到就绪链表
- 便利就绪链表进行调度
创建任务之使用任务参数(多个任务使用同一个函数,传递不同的参数)
要传的参数
typedef struct TaskPrintInfo{ uint8_t x; uint8_t y; char name[16]; }LcdPrintInfo; //要传的参数 static LcdPrintInfo g_Task1Info = {0,0,"Task1"}; static LcdPrintInfo g_Task2Info = {0,3,"Task2"}; static LcdPrintInfo g_Task3Info = {0,6,"Task3"};
任务函数
void LcdPrintTask(void *params) { LcdPrintInfo *pInfo = params; //指针赋值 uint32_t cnt = 0; uint8_t len = 0; while(1) { len = LCD_PrintString(pInfo->x,pInfo->y,pInfo->name); len+= LCD_PrintString(len,pInfo->y,":"); LCD_PrintSignedVal(len,pInfo->y,cnt++); } }
创建任务
xTaskCreate(LcdPrintTask,"LCDTask1",128,&g_Task1Info,osPriorityAboveNormal,NULL); xTaskCreate(LcdPrintTask,"LCDTask2",128,&g_Task2Info,osPriorityAboveNormal,NULL); xTaskCreate(LcdPrintTask,"LCDTask3",128,&g_Task3Info,osPriorityAboveNormal,NULL);
要注意LCD基于IIC传输,属于互斥资源,在这里我们先使用一个简单的全局变量来保护互斥资源
static int g_LCDCanUse = 1; void LcdPrintTask(void *params) { LcdPrintInfo *pInfo = params; //指针赋值 uint32_t cnt = 0; uint8_t len = 0; while(1) { if(g_LCDCanUse) { g_LCDCanUse = 0; len = LCD_PrintString(pInfo->x,pInfo->y,pInfo->name); len+= LCD_PrintString(len,pInfo->y,":"); LCD_PrintSignedVal(len,pInfo->y,cnt++); g_LCDCanUse = 1; } } }
此时运行程序,我们发现只有task3在运行,task1和task2都无法运行,这是因为,task3首先运行后,任务调度过程中,切换另一个任务的时刻大概率发生在耗时较长的打印语句中,被切换后,全局变量仍然为0,因此其他任务不能够运行。这说明这种方法是不可靠的。更可靠的方法见下一节。
任务删除
void vTaskDelete( TaskHandle_t xTaskToDelete );
| 参数 | 描述 |
|---|---|
| pvTaskCode | 任务句柄,使用xTaskCreate创建任务时可以得到一个句柄。 也可传入NULL,这表示删除自己。 |
怎么删除任务?
- 自杀:vTaskDelete(NULL)
- 被杀:别的任务执行vTaskDelete(pvTaskCode),pvTaskCode是自己的句柄
- 杀人:执行vTaskDelete(pvTaskCode),pvTaskCode是别的任务的句柄
优先级与阻塞(任务四状态)
创建三个任务,task1:音乐播放任务 task2:LED闪烁任务 task3:红外接收任务 创建任务时设置同一优先级,运行后发现task1音乐播放比较卡顿,这是因为本质上这三个任务是交替执行的。
解决方法:提高task1的优先级,并进行vTaskDelay延时。
void vTaskDelay( const TickType_t xTicksToDelay )
BaseType_t vTaskDelayUntil( TickType_t * const pxPreviousWakeTime,
const TickType_t xTimeIncrement ); //起始时间和增量时间
vTaskDelay(2); // 等待2个Tick,假设configTICK_RATE_HZ=100, Tick周期时10ms, 等待20ms
如果想要暂停音乐播放,可以使用vTaskSuspend将任务挂起,与此相对应的是vTaskResume。
void vTaskSuspend( TaskHandle_t xTaskToSuspend );
在阻塞状态的任务,它可以等待两种类型的事件:
- 时间相关的事件
- 可以等待一段时间:我等2分钟(vTaskDelay)
- 也可以一直等待,直到某个绝对时间:我等到下午3点(vTaskDelayUntil)
- 同步事件:这事件由别的任务,或者是中断程序产生
- 例子1:任务A等待任务B给它发送数据
- 例子2:任务A等待用户按下按键 同步事件的来源有很多(这些概念在后面再了解):
- 队列(queue)
- 二进制信号量(binary semaphores)
- 计数信号量(counting semaphores)
- 互斥量(mutexes)
- 递归互斥量、递归锁(recursive mutexes)
- 事件组(event groups)
- 任务通知(task notifications)
空闲任务
空闲任务在启动调度器时自动创建,拥有最低优先级(0),其作用是:在所有其他任务都处于阻塞状态或者就绪状态时运行,并可以释放被删除的任务的内存。空闲任务运行时调用一个钩子函数,vApplicationIdleHook()钩子函数中可以执行一些低优先级的、后台的、需要连续执行的函数,用户可自定义。

浙公网安备 33010602011771号