FreeRTOS应用基础(二)——基本任务使用
这篇文章实际上是和上一篇文章一起写的,今天才整理了一下。下一篇或许遥遥无期吧,随心随笔。

本文主要介绍了FreeRTOS任务的基本概念与使用,此部分为FreeRTOS的最基础单元,也是RT:OS的基础构成部分。 此部分需要达到以下目标:
- 了解堆栈和内存管理;
- 理解任务的概念和状态;
- 熟练掌握任务的创建、删除和配置等操作;
内存管理
栈.(stack):由编译器自动分配释放,存放函数的参数值,局部变量的值等,函数结束,内存自动释放,栈向下增长,即内存地址由高到低。
堆(heao):一般为程序员自主分配的空间,如malloc()分配的内存即为这种,需手动分配和释放,堆向上增长,即内存地址由低到高。;
以STM32F103为例:

栈的大小,一般默认为0x00000400,即1K的大小(0x400/1024) 在C语言的库函数中,有mallc、free等函数,但是在FreeRTOS中,它们不适用:
- 不适合用在资源紧缺的嵌入式系统中
- 这些函数的实现过于复杂、占据的代码空间太大
- 并非线程安全的(thread-safe)
- 运行有不确定性:每次调用这些函数时花费的时间可能都不相同
- 内存碎片化
- 使用不同的编译器时,需要进行复杂的配置
- 有时候难以调试
因此一个内存管理模块是单片机程序架构和RTOS的刚需部分。
FreeRTOS自带5种内存管理文件:
| 文件 | 优点 | 缺点 |
|---|---|---|
| heap_1.c | 分配简单,时间确定 | 只分配、不回收 |
| heap_2.c | 动态分配、最佳匹配 | 碎片、时间不定 |
| heap_3.c | 调用标准库函数 | 速度慢、时间不定 |
| heap_4.c | 相邻空闲内存可合并 | 可解决碎片问题、时间不定 |
| heap_5.c | 在heap_4基础上支持分隔的内存块 | 可解决碎片问题、时间不定 |
内存的动态管理属于C语言的知识范畴,在RTOS中,内存管理方式的选择也很重要,但仅从入门使用角度来说,可以暂时放在后续学习;
任务概念
什么是任务?在操作系统中,有时候也把任务称为线程。个人理解,任务(task)即单片机要做的事,在代码中,任务就是一个函数;
在裸机程序中,对于简单的方式,经常采用查询方式,即一件事完成后,再去完成另一件事,按照顺序执行,这种执行导致当有紧急情况时,可能会得不到处理。对于更复杂的程序,为了能够去及时执行紧急任务,于是便产生了中断(ISR)。
查询+中断方式能够解决大部分裸机上的应用,但随着工程的复杂,裸机的程序可能会变得越来越复杂,中断得复杂和嵌套可能会带来难以解决得问题。
RTOS相对于裸机得最大优点便是其可以通过调度实现多任务管理,即由调度器来决定当前任务得执行,即便对于大部分单片机而言,同一时刻只能执行一个任务,但通过调度算法可以实现不同时刻,多个任务复杂有序的执行,从而达到多任务效果(看起来所有任务同时运行)。
任务状态
FreeRTOS支持以下四种任务状态:
- 运行状态——Running
即任务在当前时刻处于实际执行状态; - 阻塞状态——Blocked
由于某些原因,任务需要满足某些条件时,才能够运行的状态,一般由信号量、消息队列、事件标志等驱动。 - 暂停状态——Suspended
即将任务挂起,挂起后,这个任务不再执行,只有调用函数才能将其恢复。 - 就绪状态——Ready
即处于能够运行,但由于优先级问题还没有运行的任务状态;
四种任务的状态转换图如下:
![在这里插入图片描述]()
任务优先级、tick和延时
优先级和tick
- 在FreeRTOS中,可设置0~confingMAX_PRIORITIES-1范围内的优先级,数值越大,优先级越高。
- FreeRTOS会确保最高优先级的、可运行的任务,立马执行;
- 对于相同优先级的、可运行的任务,轮流执行;
FreeRTOS的系统心跳(系统节拍)来源于Systick系统定时器产生的周期性中断,如下图

-
t1、t2、t3为发射中断的时刻
-
t1和t2之间的时间称为时间片
-
时间片的长度由confingTICK_RATE_HZ决定,如设置为1000,则时间片长度为1ms,1/1000;
对于相同优先级的任务,可以从下图分析:
![在这里插入图片描述]()
从图中也可以看出,
-
任务切换时,需要运行中断处理函数,中断运行也需要时间,故任务运行的时间并不是严格从t1、t2、t3哪里开始的;
delay
FreeRTOS中,有两个delay函数:
- vTaskDelay:至少等待指定个数的Tick Interrupt才能变为就绪状态
- vTaskDelayUntil:等待到指定的绝对时刻,才能变为就绪态。
void vTaskDelay( const TickType_t xTicksToDelay ); /* xTicksToDelay: 等待多少给
Tick */
/* pxPreviousWakeTime: 上一次被唤醒的时间
* xTimeIncrement: 要阻塞到(pxPreviousWakeTime + xTimeIncrement)
* 单位都是Tick Count
*/
BaseType_t xTaskDelayUntil( TickType_t * const pxPreviousWakeTime,
const TickType_t xTimeIncrement );
vTaskDelay
对于vTaskDelay来说,基于tick的延时并不精确,vTaskDelay(2)本意是延迟2个tick周期,但可能一个多Tick就返回了。
/* 假设configTICK_RATE_HZ=100, Tick周期时10ms, 那么等待2个Tick,也就是等待20ms */
vTaskDelay(2);
/* 还可以使用pdMS_TO_TICKS宏把ms转换为tick */
vTaskDelay(pdMS_TO_TICKS(100)); // 等待100ms
使用vTaskDelay函数时,建议以ms为单位,使用pdMS_TO_TICKS把时间转换为Tick。这样的代码就与configTICK_RATE_HZ无关,即使配置项configTICK_RATE_HZ改变了,我们也不用去修改代码。
xTaskDelayUntil(&Pre, n)
xTaskDelayUntil可以实现相较于vTaskDelay()更加精准的延时,一般用于实现一个固定执行周期的需求(当你需要让你的任务以固定频率周期性的执行时),采用绝对时间维持,具体实现过程可参考
FreeRTOS学习笔记——精准延时_anobodykey的博客-CSDN博客
https://blog.csdn.net/anobodykey/article/details/42042965
使用delay后,会使当前任务立刻进入阻塞态,此时在一个tick内,会继续下一个任务。
任务基本操作
任务函数
在FreeRTOS中,任务就是一个函数,原型为:
void ATaskFunction( void *pvParameters );
注意:
- 函数无返回值;
- 同一个函数可以创建多个任务,即多个任务可以采用同一个函数;
- 函数内部,尽量使用局部变量
-
- 每个任务都有自己的栈;
- 有多个任务运行同一个函数时
-
- 任务A的局部变量放在任务A的栈中、任务B的局部变量放在任务B的栈中;
-
- 不如任务的局部变量,有自己的副本
- 若函数内部使用全局变量、静态变量的话
-
- 则多个任务使用的为同一个副本
-
- 使用过程中需要放在冲突
以下为一个任务函数的模板:
void ATaskFunction( void *pvParameters )
{
/* 对于不同的任务,局部变量放在任务的栈里,有各自的副本 */
int32_t lVariableExample = 0;
/* 任务函数通常实现为一个无限循环 */
while(1)
{
/* 任务的代码 */
}
/* 如果程序从循环中退出,一定要使用vTaskDelete删除自己
* NULL表示删除的是自己
*/
vTaskDelete( NULL );
/* 程序不会执行到这里, 如果执行到这里就出错了 */
}
创建任务
创建任务的函数原型为:
BaseType_t xTaskCreate(
TaskFunction_t pxTaskCode, // 函数指针, 任务函数
const char * const pcName, // 任务的名字
const configSTACK_DEPTH_TYPE usStackDepth, /*任务栈 大小,单位 word ,也就是 4 字节*/
void * const pvParameters, // 调用任务函数时传入的参数
UBaseType_t uxPriority, // 优先级
TaskHandle_t * const pxCreatedTask ); // 任务句柄, 以后使用它来操作这个任务
详细参数说明如下:
| 参数 | 解释 |
|---|---|
| pvTaskCode | 函数指针,可以简单地认为任务就是一个C函数。 它稍微特殊一点:永远不退出,或者退出时要调用"vTaskDelete(NULL)" |
| pcName | pcName任务的名字,FreeRTOS内部不使用它,仅仅起调试作用。 长度为:configMAX_TASK_NAME_LEN |
| usStackDepth | 每个任务都有自己的栈,这里指定栈大小, 单位 word ,也就是 4 字节,最大值为uint16_t的最大值。 大部分时候,只能估计栈的大小,精确的办法是看反汇编码。 |
| pvParameters | 调用pvTaskCode函数指针时用到:pvTaskCode(pvParameters) |
| uxPriority | 优先级范围:0~(configMAX_PRIORITIES – 1), 数值越大优先级越高, 如果传入过大的值,xTaskCreate会把它调整为(configMAX_PRIORITIES – 1) |
| pxCreatedTask | 用来保存xTaskCreate的输出结果:task handle。 以后如果想操作这个任务,比如修改它的优先级,就需要这个handle。 如果不想使用该handle,可以传入NULL。即TCB_t |
| 返回值 | 成功:pdPASS;失败:errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY(失败原因只有内存不足); 注意:文档里都说失败时返回值是pdFAIL,这不对。 pdFAIL是0,errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY是-1。 |
通过实验发现,可以使用同一个函数和同一个句柄可以多次创造任务,此时任务的运行会发生重叠,同时如果此时采用删除这个句柄所创造的任务,只能删除一个运行的任务(如果你创建了两个同样的,删除后还会剩一个),若再次使用这个句柄删除任务,则会直接发生硬件错误(怀疑是访问内存越界)。故创造任务时,还需自行判断任务存在状态后再执行。
空闲任务和钩子函数
空闲任务用于在用户任务都处于阻塞态时运行,通过空闲任务,可以使系统得到短暂的休息,同时可以在空闲任务中更好的实现低功耗,用户可以在空闲任务中实现睡眠,停机等低功耗措施。
空闲任务需要注意:
- 在使用vTaskStartScheduler()函数来创建、启动调度器时,这个函数内部会创建空闲任务;
- 空闲任务的优先级为0,不能阻碍用户任务运行;
- 空闲任务不会处于阻塞,要么处于就绪态、要么处于运行态;
- 当删除任务时,需要通过执行空闲任务,去释放该任务的内存;注意:此处的空闲任务指的是内核自带的空闲任务,用户无需特意去定义,只需在删除后,提供一段tick延时,确保系统可以自动释放内存即可,除非采用静态创建的任务,需要用户自己手动定义删除内存,教程里并未强调这一点,:
空闲任务内可添加钩子函数(Idle Task Hook Functions),即用于空闲任务执行时的函数,作用如下:
- 执行一些低优先级的、后台的、需要连续执行的函数
- 测量系统的空闲时间:空闲任务能被执行就意味着所有的高优先级任务都停止了,所以测量空闲任务占据的时间,就可以算出处理器占用率。
- 让系统进入省电模式:空闲任务能被执行就意味着没有重要的事情要做,当然可以进入省电模式了。
空闲任务的钩子函数的限制:
- 不能导致空闲任务进入阻塞状态、暂停状态
- 如果你会使用vTaskDelete()来删除任务,那么钩子函数要非常高效地执行。如果空闲任务移植卡在钩子函数里的话,它就无法释放内存。
要使用空闲任务的钩子函数,首先需要将以下宏定义打开,可以在FreeRTOSconfig.h中配置
#define configUSE_IDLE_HOOK 0
其次需要定义以下函数:
void vApplicationIdleHook( void );
相关代码可以在task.c中看到:

删除任务
删除任务需要使用以下函数
//配置宏定义
#define INCLUDE_vTaskDelete 1
/*
参数:
pvTaskCode:任务句柄,使用xTaskCreate创建任务时可以得到一个句柄。
可传入以下值:
自杀: vTaskDelete(NULL),即当前正在执行的任务;
被杀:别的任务执行vTaskDelete(pvTaskCode) ,pvTaskCode是自己的句柄;
杀人:执行vTaskDelete(pvTaskCode) ,pvTaskCode是别的任务的句柄
*/
void vTaskDelete( TaskHandle_t xTaskToDelete );
在执行删除函数后,任务的内存并没有被立刻释放,故调用这个函数时,一定要确保在删除前,空闲任务可以执行,将要删除的任务内存释放;
挂起任务
任务可通过以下函数进入Suspended状态:
//使用此函数需要先配置宏定义
#define INCLUDE_vTaskSuspend 1
/*
xTaskToSuspend:任务句柄,NULL则代表挂起当前正在执行的任务,即自己挂起自己;
*/
void vTaskSuspend (TaskHandle_t xTaskToSuspend /* 任务 句柄 */
恢复任务
非中断:
//使用此函数需要先配置宏定义,和挂起一样
#define INCLUDE_vTaskSuspend 1
/*
xTaskToSuspend:需要恢复的任务句柄;
*/
void vTaskResume (TaskHandle_t xTaskToResume /* 任务 句柄 */
此函数是用于任务代码中调用的,故不可以在中断服务程序中调用此函数,
中断:
中断服务程序中使用的 xTaskResumeFromISR(),以后缀FromISR结尾。
//配置宏定义
#define INCLUDE_xResumeFromISR 1
void vTaskResumeFromISR (TaskHandle_t xTaskToResume /* 任务 句柄 */
如果用户打算采用这个函数实现中断与任务的同步,要注意一种情况,如果此函数的调用优先于函数vTaskSuspend被调用,那么此次同步会丢失,这种情况下建议使用信号量来实现同步。 此函数是用于中断服务程序中调用的,故不可以在任务中使用此函数,任务中使用的是vTaskResume。
实验验证
问题:在一般的freertos程序中,该如何管理任务的创建和删除?如何避免重复创建或删除任务?
功能
可抢占时间片轮转
任务一:LED1快速闪烁
任务二:LED2间隔1s闪烁
key1:任务一,正常时挂起,再按一下恢复
key2:任务二,正常时挂起,再按一下恢复
key3:任务一,存在时删除,不存在则创建
WK_UP:任务二,正常时挂起,再按一下恢复
通过串口查看各任务状态
此处采用的按键消抖,通过正点原子的key.h和delay文件实现:
#include "main.h"
#include "includes.h"
TaskHandle_t StartTask_Handler;
TaskHandle_t xHandleTask1;
TaskHandle_t xHandleTask2;
/*
*********************************************************************************************************
* 函 数 名: vTaskLED1
* 功能说明:
* 形 参: 无
* 返 回 值: 无
* 优 先 级: 1
* 说 明:每隔1s闪烁一次
*********************************************************************************************************
*/
void vTaskLED1(void *pvParameters)
{
while(1)
{
LED_On(1);
vTaskDelay(pdMS_TO_TICKS(200));
LED_Off(1);
vTaskDelay(pdMS_TO_TICKS(200));
}
}
/*
*********************************************************************************************************
* 函 数 名: vTaskLED2
* 功能说明:
* 形 参: 无
* 返 回 值: 无
* 优 先 级: 2
* 说 明:1s亮1s灭
*********************************************************************************************************
*/
static void vTaskLED2(void *pvParameters)
{
while(1)
{
LED_Toggle(2);
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
/*
*********************************************************************************************************
* 函 数 名: vTaskKey
* 功能说明: 按键扫描函数
* 形 参: 无参
* 返 回 值: 无
* 优 先 级: 3
* 说 明:
*********************************************************************************************************
*/
static void vTaskKey(void *pvParameters)
{
int key;
int task_pass;//创建任务是否成功标志位
static int task1_state=1;//0:不存在,1:存在且正常运行,2:挂起
static int task2_state=1;
while(1)
{
key = KEY_Scan(0);
switch(key)
{
case 1://任务一:正常时挂起,再按一下恢复
{
if(task1_state==1)
{
vTaskSuspend (xHandleTask1);
task1_state=2;
printf("任务LED1挂起成功\r\n");
}
else if(task1_state==2)
{
vTaskResume (xHandleTask1);
printf("任务LED1已恢复\r\n");
task1_state=1;
}
break;
};
case 2://任务二:正常时挂起,再按一下恢复
{
if(task2_state==1)
{
vTaskSuspend (xHandleTask2);
task2_state=2;
printf("任务LED2挂起成功\r\n");
}
else if(task2_state==2)
{
vTaskResume(xHandleTask2);
printf("任务LED2已恢复\r\n");
task2_state=1;
}
break;
};
case 3://任务一:存在时删除,不存在则创建
{
if(task1_state)
{
vTaskDelete (xHandleTask1);
printf("任务LED1删除\r\n");
task1_state = 0;
}
else
{
task_pass = xTaskCreate( vTaskLED1, /* 任务函数*/
"vTask1", /* 任务名*/
512, /*任务栈 大小,单位 word ,也就是 4 字节*/
NULL, /* 任务参数*/
1, /* 任务优先级*/
&xHandleTask1); /* 任务句柄*/
task1_state = 1;
if(task_pass == 1)
printf("任务LED1创建成功\r\n");
else
printf("任务LED1创建失败\r\n");
}
break;
};
case 4://删除任务二
{
if(task2_state)
{
vTaskDelete (xHandleTask2);
printf("任务LED2删除\r\n");
task2_state = 0;
}
else
{
task_pass = xTaskCreate( vTaskLED2, /* 任务函数*/
"vTask2", /* 任务名*/
512, /*任务栈 大小,单位 word ,也就是 4 字节*/
NULL, /* 任务参数*/
1, /* 任务优先级*/
&xHandleTask2); /* 任务句柄*/
task2_state = 1;
if(task_pass == 1)
printf("任务LED2创建成功\r\n");
else
printf("任务LED2创建失败\r\n");
}
break;
}
}
vTaskDelay(10);//确保系统的空闲任务有空运行
}
}
void Task_Creat(void *pvParameters)
{
xTaskCreate( vTaskKey, /* 任务函数*/
"vTaskKEY", /* 任务名*/
512, /*任务栈 大小,单位 word ,也就是 4 字节*/
NULL, /* 任务参数*/
1, /* 任务优先级*/
NULL);/* 任务句柄*/
xTaskCreate( vTaskLED1, /* 任务函数*/
"vTask1", /* 任务名*/
512, /*任务栈 大小,单位 word ,也就是 4 字节*/
NULL, /* 任务参数*/
1, /* 任务优先级*/
&xHandleTask1); /* 任务句柄*/
xTaskCreate( vTaskLED2, /* 任务函数*/
"vTask2", /* 任务名*/
512, /*任务栈 大小,单位 word ,也就是 4 字节*/
NULL, /* 任务参数*/
2, /* 任务优先级*/
&xHandleTask2); /* 任务句柄*/
vTaskDelete(StartTask_Handler); //删除开始任务
taskEXIT_CRITICAL(); //退出临界区
}
int main()
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //设置NVIC中断分组2:2位抢占优先级,2位响应优先级
delay_init();
Key_Init();
LED_Init();
Uart_Init();
printf("start\r\n");
/* 创建开始任务*/
xTaskCreate( Task_Creat, /* 任务函数*/
"vTaskStart", /* 任务名*/
512, /*任务栈 大小,单位 word ,也就是 4 字节*/
NULL, /* 任务参数*/
1, /* 任务优先级*/
&StartTask_Handler); /* 任务句柄*/
/* 启动调度,开始执行任务*/
vTaskStartScheduler();
while(1);
}



浙公网安备 33010602011771号