上一文链接:FreeRTOS笔记(三)配置文件FreeRTOSConfig.h
单片机裸机编程的时候,都会用一个while(1)
去包裹所有应用程序,换言之,裸机编程中的应用程序就运行在一个死循环while(1)
中,而且各个应用程序比如串口、LED、显示屏等都是排队轮流执行的(中断除外),只要前面的程序没有完成,后面的程序就必须等待。
这种裸机编程非常容易实现,对于顺序执行的逻辑可以控制得很好,但是当外部资源庞大,程序逻辑复杂,需要突然程序跳转的设置,某些应用程序会在某个不可预测的时间上需要实时性执行,此时裸机编程就无能为力了,裸机编程的系统一般称为单任务系统,显然现在需要多任务系统。
多任务系统假若在单核版上运行(嵌入式中往往是单核),那么意味着每个时刻只有一个任务在进行,下一个时刻到底选择哪个任务去执行?需要一个掌控全局任务执行时间安排的角色:调度器。调度器的基本功能是使得任务可以切出和切入,在某个任务需要马上执行的时候立刻打断当前正在执行的任务,这样就使得系统具有实时性。调度器的存在也使得任务具有多种特性,比如优先级、堆栈空间等等。
函数 | 描述 |
---|---|
xTaskCreate() | 堆栈由FreeRTOS动态分配 |
xTaskCreateStatic() | 堆栈由用户指定分配 |
xTaskCreateRestricted() | 堆栈由FreeRTOS动态分配,使用MPU(内存保护单元)进行限制 |
xTaskDelete() | 删除任务 |
根据FreeRTOS提供的API,能够知道任务具有的基本特性,这些特性需要在创建的时候确定。
首先了解怎样创建和删除一个任务,上述提到,任务应该具有一个死循环函数,除了函数,任务还有优先级、堆栈空间等特性,而这个函数称为任务函数,它告诉任务需要执行的内容,任务函数既然是死循环,就不需要返回值,于是返回值就是void,那么参数呢?FreeRTOS规定任务函数的参数是一个指针,基类型是void(void*指针可以指向任一个类型的指针,理解为包罗万象的盒子,任何东西都能放进去),任务函数的原型如下:
void ATaskFunction( void *pvParameters );
这个任务函数需要绑定在任务中才能被执行,而任务需要创建,FreeRTOS提供任务相关的API,创建任务的API有3种,常用的是xTaskCreate()
,不同的API用途在上表查看,xTaskCreate()
的原型及需要的头文件如下:
#include “FreeRTOS.h”
#include “task.h”
aseType_t xTaskCreate( TaskFunction_t pvTaskCode, //任务函数
const char * const pcName, //任务名称
unsigned short usStackDepth, //堆栈大小
void *pvParameters, //任务函数的参数
UBaseType_t uxPriority, //任务优先级
TaskHandle_t *pxCreatedTask ); //任务句柄
参数和返回值可以在官方手册中查看,目前暂时理解部分。值得注意的是,任务的创建可以在调度器启动前,也可以是调度器启动后。
任务函数运行在死循环之内,不能以任何方式返回,如果任务函数实在不需要继续执行,那么可以使用vTaskDelete()
API函数主动或者被动删除任务。任务删除后就不再运行,变成了“僵尸”,等待着内核收尸。vTaskDelete()
的原型及需要的头文件如下:
#include “FreeRTOS.h”
#include “task.h”
void vTaskDelete( TaskHandle_t pxTask ); //任务句柄
FreeRTOS多任务系统需要调度器进行任务的切换,而被切出的任务下一次可以被完整地继续运行的原因在于,每个任务都有自己的堆栈,调度器进行任务切换的时候保护切出任务当前的堆栈现场,当下一次切入的时候再恢复现场,这样任务中的数据就不会被破坏。
在创建任务的时候需要指出堆栈的大小,这对于一般的开发者而言很难估计,目前只需要写入一个中等大的值即可,如果嵌入式单板的RAM资源短缺,就需要非常仔细地计算每个任务的堆栈大小。任务堆栈的大小由参赛unsigned short usStackDepth
指定,单位是字,不是字节。
调度器选择任务执行的依据是任务的优先级,FreeRTOS的任务优先级是0~(configMAX_PRIORITIES-1)
,configMAX_PRIORITIES
在FreeRTOSConfig.h(可以看上一文)中定义,0是最低优先级,数值越大优先级越高。
调度器选择任务需要有一个调度算法,调度器总是选择优先级最高的任务去执行,如果任务队伍中的优先级一样,就采用时间片的方法法轮流执行,每个任务执行一段时间,随后切换同级的其它任务。
接下来进行任务的一些基础测试,先测试一些任务的基础特性,真实感受FreeRTOS的任务执行,比如创建删除、堆栈大小和优先级,配合官方的一些API进行使用,任务的创建仅仅测试xTaskCreate()
函数。
#define START_TASK_PRIO 1 //优先级
#define STATR_STACK_SIZE 128 //堆栈大小
TaskHandle_t StartTask_Handler; //任务句柄
//任务函数
void start_task(void *pParam)
{
for(;;)
{
printf("task running, task number: %ld\r\n",uxTaskGetNumberOfTasks());
vTaskDelay(1000);
}
}
void main(void)
{
printf("before create task : %ld\r\n",uxTaskGetNumberOfTasks());
//创建任务
xTaskCreate( (TaskFunction_t)start_task,
(const char*)"start_task",
(uint16_t)STATR_STACK_SIZE,
(void*)NULL,
(UBaseType_t)START_TASK_PRIO,
(TaskHandle_t*)&StartTask_Handler
);
printf("after create task : %ld\r\n",uxTaskGetNumberOfTasks());
//开启调度器
vTaskStartScheduler();
}
这里产生疑问,为什么有3个任务?从输出可知,调度器启动前只有1个任务,启动后有3个,意味着调度器创建了2个任务,这2个是什么任务?打开启动调度器函数vTaskStartScheduler()
查看源码:
void vTaskStartScheduler( void )
{
#if( configSUPPORT_STATIC_ALLOCATION == 1 )
{
……
xIdleTaskHandle = xTaskCreateStatic();
……
}
#else
{
……
xReturn = xTaskCreate();
……
}
#endif
……
#if ( configUSE_TIMERS == 1 )
{
if( xReturn == pdPASS )
{
xReturn = xTimerCreateTimerTask();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif
……
}
问题解决了,这2个任务,一个是IDLE空闲任务,一个是定时器任务,空闲任务是一定会创建的,因为if…else…
都有创建任务的函数,而定时器任务是根据宏configUSE_TIMERS
来创建的,这个宏在FreeRTOSConfig.h中配置,上一篇文章中配置了这个宏,所以就会有3个任务。
void A_task(void *pParam)
{
printf("A task running, task number: %ld\r\n",uxTaskGetNumberOfTasks());
vTaskDelete(NULL);
}
void B_task(void *pParam)
{
eTaskState state;
for(;;)
{
printf("B task running, task number: %ld\r\n",uxTaskGetNumberOfTasks());
state = eTaskGetState(ATask_Handler);
if(state == eDeleted)
{
printf("B task running, A is delete\r\n");
}
else
{
printf("B task running, A is other\r\n");
}
vTaskDelay(1000);
}
}
能够看到,调度器开启后串口输出混乱,这是资源访问的问题,暂时不理会。还是能够观测到,任务A输出了信息,此时还有4个任务(A、B、空闲任务和定时器任务),当A删除后,只有B在运行,并显示A任务已经删除,目前只有3个任务。
#define START_TASK_PRIO 1 //优先级
#define STATR_STACK_SIZE 16 //堆栈大小
TaskHandle_t StartTask_Handler; //任务句柄
void start_task(void *pParam)
{
for(;;)
{
printf("task running, task number: %ld\r\n",uxTaskGetNumberOfTasks());
vTaskDelay(1000);
}
}
当任务堆栈不足够的时候,任务是无法运行的,此时的处理取决于你有没有使用FreeRTOS提供的栈溢出检测,如果在FreeRTOSConfig.h配置文件中定义configCHECK_FOR_STACK_OVERFLOW
不为0,那么就会使用vApplicationStackOverflowHook()
进行处理,如果没有使用栈溢出检测,那么后果将不可预料,有可能系统会奔溃,有可能会正常运行,有可能会打乱系统运作。
/* A 任务 */
#define A_TASK_PRIO 2 //优先级
#define A_STACK_SIZE 128 //堆栈大小
TaskHandle_t ATask_Handler; //任务句柄
/* B 任务 */
#define B_TASK_PRIO 1 //优先级
#define B_STACK_SIZE 128 //堆栈大小
TaskHandle_t BTask_Handler; //任务句柄
void A_task(void *pParam)
{
for(;;)
{
printf("A task running, task number: %ld\r\n",uxTaskGetNumberOfTasks());
}
}
void B_task(void *pParam)
{
for(;;)
{
printf("B task running, task number: %ld\r\n",uxTaskGetNumberOfTasks());
}
}
在前面任务创建与删除的测试中,A、B两个任务的优先级是相同的,于是调度器采用时间片调度法,任务轮流执行,时间片是多少?可以通过FreeRTOSConfig.h文件去配置,默认情况下我们可知,A、B任务还没有完整输出串口的时候就被切换了,所以出现了乱码。但是现在任务A永远是可以执行的最高优先级任务,任务B则永远得不到执行。