以下内容皆是个人学习过程中的总结,记录一下整个过程,用于后期复习,如有不对之处,麻烦各位大佬指出~
(喜欢的朋友麻烦点个关注~~~ 后期还会进行持续更新)
在FreeRTOS 中应用既可以使用任务,也可以使用协程(Co-Routine),或者两者混合使用。 但是任务和协程使用不同的API函数,因此不能通过队列(或信号量)将数据从任务发送给协程, 反之亦然。协程是为那些资源很少的 MCU 准备的,其开销很小,但是 FreeRTOS 官方已经不打算再更新协程了,因此我们对于协程进行一个简单的了解就好了,主要还是学习任务
任务(Task)的特性:
在使用 RTOS 的时候一个实时应用可以作为一个独立的任务。每个任务都有自己的运行环境,不依赖于系统中其他的任务或者 RTOS 调度器。任何一个时间点只能有一个任务运行,具 体运行哪个任务是由 RTOS 调度器来决定的,RTOS 调度器因此就会重复的开启、关闭每个任务。任务不需要了解 RTOS 调度器的具体行为,RTOS 调度器的职责是确保当一个任务开始执 行的时候其上下文环境(寄存器值,堆栈内容等)和任务上一次退出的时候相同。为了做到这一 点,每个任务都必须有个堆栈,当任务切换的时候将上下文环境保存在堆栈中,这样当任务再 次执行的时候就可以从堆栈中取出上下文环境,任务恢复运行。
任务特性:
1、简单。
2、没有使用限制。
3、支持抢占
4、支持优先级
5、每个任务都拥有堆栈导致了 RAM 使用量增大。
6、如果使用抢占的话的必须仔细的考虑重入的问题。.
协程(Co-routine)的特性:
协程是为那些资源很少的 MCU 而做的,但是随着 MCU 的飞速发展,性能越来越强大,现 在协程几乎很少用到了!但是 FreeRTOS 目前还没有把协程移除的计划,但是 FreeRTOS 是绝对 不会再更新和维护协程了,因此协程大家了解一下就行了。在概念上协程和任务是相似的,但 是有如下根本上的不同:
1、堆栈使用所有的协程使用同一个堆栈(如果是任务的话每个任务都有自己的堆栈),这样就比使用任务消耗更少的 RAM。
2、调度器和优先级协程使用合作式的调度器,但是可以在使用抢占式的调度器中使用协程。
3、宏实现协程是通过宏定义来实现的。
4、使用限制为了降低对 RAM 的消耗做了很多的限制
在了解完任务的基本特性之后,我们现在开始正式学习
一、任务状态:
首先我们要知道,在FreeRTOS 中任务永远处于下面几个状态中的某一个:
● 运行态 当一个任务正在运行时,那么就说这个任务处于运行态,处于运行态的任务就是当前正在 使用处理器的任务。如果使用的是单核处理器的话那么不管在任何时刻永远都只有一个任务处 于运行态。
● 就绪态 处于就绪态的任务是那些已经准备就绪(这些任务没有被阻塞或者挂起),可以运行的任务, 但是处于就绪态的任务还没有运行,因为有一个同优先级或者更高优先级的任务正在运行!
● 阻塞态 如果一个任务当前正在等待某个外部事件的话就说它处于阻塞态,比如说如果某个任务调 用了函数 vTaskDelay()的话就会进入阻塞态,直到延时周期完成。任务在等待队列、信号量、事 件组、通知或互斥信号量的时候也会进入阻塞态。任务进入阻塞态会有一个超时时间,当超过 这个超时时间任务就会退出阻塞态,即使所等待的事件还没有来临!
● 挂起态 像阻塞态一样,任务进入挂起态以后也不能被调度器调用进入运行态,但是进入挂起态的 任务没有超时时间。任务进入和退出挂起态通过调用函数 vTaskSuspend()和 xTaskResume()。
任务状态之间的转换如下图所示:
二、任务优先级:
每 个 任 务 都 可 以 分 配 一 个 从 0~(configMAX_PRIORITIES-1) 的 优 先 级 , configMAX_PRIORITIES 在文件 FreeRTOSConfig.h 中有定义,前面我们讲解 FreeRTOS 系统配 置的时候已经讲过了。如果所使用的硬件平台支持类似计算前导零这样的指令(可以通过该指令 选 择 下 一 个 要 运 行 的 任 务 , Cortex-M 处 理 器 是 支 持 该 指 令 的 ) ,并且宏 configUSE_PORT_OPTIMISED_TASK_SELECTION 也 设 置 为 了 1 , 那 么 宏 configMAX_PRIORITIES 不能超过 32!也就是优先级不能超过 32 级。其他情况下宏 configMAX_PRIORITIES 可以为任意值,但是考虑到 RAM 的消耗,宏 configMAX_PRIORITIES 最好设置为一个满足应用的最小值。 优先级数字越低表示任务的优先级越低,0 的优先级最低,configMAX_PRIORITIES-1 的优 先级最高。空闲任务的优先级最低,为 0。 FreeRTOS 调度器确保处于就绪态或运行态的高优先级的任务获取处理器使用权,换句话说 就是处于就绪态的最高优先级的任务才会运行。当宏 configUSE_TIME_SLICING 定义为 1 的时 候多个任务可以共用一个优先级,数量不限。默认情况下宏 configUSE_TIME_SLICING 在文件 FreeRTOS.h 中已经定义为 1。此时处于就绪态的优先级相同的任务就会使用时间片轮转调度器 获取运行时间。综合所述,优先级可设置的范围为1~31,优先级数字越小优先级越低,优先级可设置为同样的,同样的优先级则按时间先后来进行
三、任务实现:
在使用 FreeRTOS 的过程中,我们要使用函数 xTaskCreate()或 xTaskCreateStatic()来创建任 务,这两个函数的第一个参数 pxTaskCode,就是这个任务的任务函数。什么是任务函数?任务 函数就是完成本任务工作的函数。我这个任务要干嘛?要做什么?要完成什么样的功能都是在 这个任务函数中实现的。 比如我要做个任务,这个任务要点个流水灯,那么这个流水灯的程序 就是任务函数中实现的。FreeRTOS 官方给出的任务函数模板如下
void vATaskFunction(void *pvParameters) (1)
{
for( ; ; ) (2)
{
--任务应用程序-- (3)
vTaskDelay(); (4)
}
/*不能从任务函数中返回或者退出, 从任务函数中返回或退出的话就会调用
configASSERT(),前提是你定义了 configASSERT()。如果一定要从任务函数中退出的话那一定要调用函数 vTaskDelete(NULL)来删除此任务。*/
vTaskDelete(NULL); (5)
}
(1)、任务函数本质也是函数,所以肯定有任务名什么的,不过这里我们要注意:任务函数 的返回类型一定要为 void 类型,也就是无返回值,而且任务的参数也是 void 指针类型的!任务 函数名可以根据实际情况定义。
(2)、任务的具体执行过程是一个大循环,for(; ; )就代表一个循环,作用和 while(1)一样,笔者习惯用 while(1)。
(3)、循环里面就是真正的任务代码了,此任务具体要干的活就在这里实现!
(4)、FreeRTOS 的延时函数,此处不一定要用延时函数,其他只要能让 FreeRTOS 发生任务 切换的 API 函数都可以,比如请求信号量、队列等,甚至直接调用任务调度器。只不过最常用 的就是 FreeRTOS 的延时函数。
(5)、任务函数一般不允许跳出循环,如果一定要跳出循环的话在跳出循环以后一定要调用 函数 vTaskDelete(NULL)删除此任务! FreeRTOS 的任务函数和 UCOS 的任务函数模式基本相同的,不止 FreeRTOS,其他 RTOS 的任务函数基本也是这种方式的。
四、任务控制块:
FreeRTOS 的每个任务都有一些属性需要存储,FreeRTOS 把这些属性集合到一起用一个结 构体来表示,这个结构体叫做任务控制块:TCB_t,在使用函数 xTaskCreate()创建任务的时候就 会自动的给每个任务分配一个任务控制块。在老版本的 FreeRTOS 中任务控制块叫做 tskTCB, 新版本重命名为 TCB_t,但是本质上还是 tskTCB,本教程后面提到任务控制块的话均用 TCB_t 表示,此结构体在文件 tasks.c 中有定义,如下:
typedef struct tskTaskControlBlock
{
volatile StackType_t *pxTopOfStack; //任务堆栈栈顶
#if ( portUSING_MPU_WRAPPERS == 1 )
xMPU_SETTINGSxMPUSettings; //MPU 相关设置
#endif
ListItem_t xStateListItem; //状态列表项
ListItem_t xEventListItem; //事件列表项
UBaseType_t uxPriority; //任务优先级
StackType_t *pxStack; //任务堆栈起始地址
char pcTaskName[ configMAX_TASK_NAME_LEN ];//任务名字
#if ( portSTACK_GROWTH > 0 )
StackType_t *pxEndOfStack; //任务堆栈栈底
#endif
#if ( portCRITICAL_NESTING_IN_TCB == 1 )
UBaseType_t uxCriticalNesting; //临界区嵌套深度
#endif
#if ( configUSE_TRACE_FACILITY == 1 ) //trace 或到 debug 的时候用到
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; //定义一个 newlib 结构体变量
#endif
#if( configUSE_TASK_NOTIFICATIONS == 1 )//任务通知相关变量
volatile uint32_t ulNotifiedValue; //任务通知值
volatile uint8_t ucNotifyState; //任务通知状态
#endif
#if( tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE != 0 )
//用来标记任务是动态创建的还是静态创建的,如果是静态创建的此变量就为 pdTURE,
//如果是动态创建的就为 pdFALSE
uint8_t ucStaticallyAllocated;
#endif
#if( INCLUDE_xTaskAbortDelay == 1 )
uint8_t ucDelayAborted;
#endif
} tskTCB;
//新版本的 FreeRTOS 任务控制块重命名为 TCB_t,但是本质上还是 tskTCB,主要是为了兼容
//旧版本的应用。
typedef tskTCB TCB_t;
可以看出来 FreeRTOS 的任务控制块中的成员变量相比 UCOSIII 要少很多,而且大多数与 裁剪有关,当不使用某些功能的时候与其相关的变量就不参与编译,任务控制块大小就会进一 步的减小。
五、任务堆栈:
FreeRTOS 之所以能正确的恢复一个任务的运行就是因为有任务堆栈在保驾护航,任务调度器在进行任务切换的时候会将当前任务的现场(CPU 寄存器值等)保存在此任务的任务堆栈中,等到此任务下次运行的时候就会先用堆栈中保存的值来恢复现场,恢复现场以后任务就会接着从上次中断的地方开始运行。创建任务的时候需要给任务指定堆栈,如果使用的函数 xTaskCreate()创建任务(动态方法) 的话那么任务堆栈就会由函数 xTaskCreate()自动创建,如果使用函数 xTaskCreateStatic()创建任务(静态方法)的话就需要程序员自行定义任务堆栈
六、任务创建和删除API函数:
FreeRTOS 最基本的功能就是任务管理,而任务管理最基本的操作就是创建和删除任务, FreeRTOS 的任务创建和删除 API 函数如下表所示:
6.1、函数 xTaxkCreate()(动态)
此函数用来创建一个任务,任务需要 RAM 来保存与任务有关的状态信息(任务控制块),任 务也需要一定的 RAM 来作为任务堆栈。如果使用函数 xTaskCreate()来创建任务的话那么这些 所需的 RAM 就会自动的从 FreeRTOS 的堆中分配,因此必须提供内存管理文件,默认我们使用 heap_4.c 这个内存管理文件,而且宏 configSUPPORT_DYNAMIC_ALLOCATION 必须为 1。如 果使用函数 xTaskCreateStatic()创建的话这些 RAM 就需要用户来提供了。新创建的任务默认就 是就绪态的,如果当前没有比它更高优先级的任务运行那么此任务就会立即进入运行态开始运 行,不管在任务调度器启动前还是启动后,都可以创建任务。此函数也是我们以后经常用到的, 本教程所有例程均用此函数来创建任务,函数原型如下:
BaseType_t xTaskCreate( TaskFunction_t pxTaskCode,
const char * const pcName,
const uint16_t usStackDepth,
void * const pvParameters,
UBaseType_t uxPriority,
TaskHandle_t * const pxCreatedTask )
参数:
pxTaskCode: 任务函数。
pcName: 任务名字,一般用于追踪和调试,任务名字长度不能超过。 configMAX_TASK_NAME_LEN。
usStackDepth: 任务堆栈大小,注意实际申请到的堆栈是 usStackDepth 的 4 倍。其中空闲任 务的任务堆栈大小为 configMINIMAL_STACK_SIZE。
pvParameters: 传递给任务函数的参数。 uxPriotiry: 任务优先级,范围 0~ configMAX_PRIORITIES-1。
pxCreatedTask: 任务句柄,任务创建成功以后会返回此任务的任务句柄,这个句柄其实就是 任务的任务堆栈。此参数就用来保存这个任务句柄。其他 API 函数可能会使 用到这个句柄。
返回值:
pdPASS: 任务创建成功。
errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY: 任务创建失败,因为堆内存不足!
6.2、函数 vTaskDelete()(删除)
删除一个用函数 xTaskCreate()或者 xTaskCreateStatic()创建的任务,被删除了的任务不再存 在,也就是说再也不会进入运行态。任务被删除以后就不能再使用此任务的句柄!如果此任务 是使用动态方法创建的,也就是使用函数 xTaskCreate()创建的,那么在此任务被删除以后此任 务之前申请的堆栈和控制块内存会在空闲任务中被释放掉,因此当调用函数 vTaskDelete()删除 任务以后必须给空闲任务一定的运行时间。 只有那些由内核分配给任务的内存才会在任务被删除以后自动的释放掉,用户分配给任务 的内存需要用户自行释放掉,比如某个任务中用户调用函数 pvPortMalloc()分配了 500 字节的内 存,那么在此任务被删除以后用户也必须调用函数 vPortFree()将这 500 字节的内存释放掉,否 则会导致内存泄露。此函数原型如下:
vTaskDelete( TaskHandle_t xTaskToDelete )
参数:
xTaskToDelete: 要删除的任务的任务句柄。
返回值: 无
七、任务创建和删除实验(动态):
本实验设计三个任务:start_task、task1_task 和 task2_task ,这三个任务的任务功能如下: start_task:用来创建其他两个任务。
task1_task :当此任务运行 5 次以后就会调用函数 vTaskDelete()删除任务 task2_task,此任务也会控制 LED0 的闪烁
task2_task :此任务普通的应用任务,此任务也会控制 LED1 的闪烁
首先我们先打开上一节的跑马灯实验,然后把多余的代码删除掉,删除后的main.c界面如下图显示:
删除完毕之后,我们先对start_task任务的优先级、堆栈、句柄、函数进行表示,如下图所示:
紧接着,我们开始调用函数xTaxkCreate()创建 start_task 任务,函数中的各个参数就是上图里所定义的,其他任务的创建也用这种方法。创建完成之后调用函数vTaskStartScheduler()开启 FreeRTOS 的任务调度器,FreeRTOS 开始运行,如下图所示:
start_task()任务创建完成之后,接着我们要编写start_task()任务要实现的功能,参照我们之前设定的实验目标,start_task()任务是用于创建另外两个任务的,因此我们在start_task()的任务函数中进行另外两个任务的创建,并在创建完成之后调用函数vTaskDelete()删除掉start_task()任务自身。如下图所示:
PS:自行参照上面的方法分别对task1_task(),task2_task()任务参数进行表示
最后按照我们之前设定的实验目标,开始编写task1_task(),task2_task()任务所要实现的功能,如下图所示:
PS:变量stata是起一个标志位的作用,用于防止变量num在计数到达255之后会自动清零,清零之后会重新计数,在计数再次到达5之后又一次执行删除任务,从而报错的问题
最终实现的效果如图所示:
task1_task()任务在运行到第五次的时候,调用函数将task2_task()任务删除,紧接着task1_task()继续运行,task2_task()则不再运行
至此,我们的实验宣告成功,希望各位能从中有所收获,最后附上完整的main.c代码
#include "led.h"
#include "delay.h"
#include "sys.h"
#include "FreeRTOS.h"
#include "task.h"
#define START_TASK_PRIO 1 //任务优先级
#define START_STK_SIZE 128 //任务堆栈大小
TaskHandle_t StartTask_Handler; //任务句柄
void start_task(void *pvParameters); //任务函数
#define TASK1_TASK_PRIO 3 //任务优先级
#define TASK1_STK_SIZE 128 //任务堆栈大小
TaskHandle_t Task1Task_Handler; //任务句柄
void task1_task(void *pvParameters); //任务函数
#define TASK2_TASK_PRIO 2 //任务优先级
#define TASK2_STK_SIZE 128 //任务堆栈大小
TaskHandle_t Task2Task_Handler; //任务句柄
void task2_task(void *pvParameters); //任务函数
int main(void)
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);//设置系统中断优先级分组4
delay_init(); //延时函数初始化
uart_init(115200); //初始化串口
LED_Init(); //初始化LED
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); //任务句柄
vTaskStartScheduler(); //开始任务调度
}
//start_task任务函数
void start_task(void *pvParameters)
{
while(1)
{
taskENTER_CRITICAL(); //进入临界区
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); //任务句柄
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); //任务句柄
vTaskDelete(StartTask_Handler); //删除start_task()任务
taskEXIT_CRITICAL(); //退出临界区
}
}
//task1_task任务函数
void task1_task(void *pvParameters)
{
int i=0;
int num=0;
int stata=1;
while(1)
{
i++;num++;
LED0=~LED0;
printf("\r\n正在运行task1_task:%d\r\n",num);
if(stata==1)
{
if(i==5)
{
vTaskDelete(Task2Task_Handler);
stata=0;
printf("\r\n删除task2_task任务!!\r\n");
}
}
vTaskDelay(500);
}
}
//task2_task任务函数
void task2_task(void *pvParameters)
{
while(1)
{
LED1=1;
vTaskDelay(500);
LED1=0;
vTaskDelay(800);
printf("\r\n正在运行tatk2_task\r\n");
}
}