PS:本人因为一些自己的原因,从本篇开始一些函数名之类的自定义的名字会用自己的方式命名,会和野火的教程有出入,还请自行甄别。
从本质上来讲,FreeRTOS的运行就是不断执行每个任务。本篇会介绍静态和动态创建任务的方法。
静态创建需要指定任务栈的大小和起始地址;动态创建在SRAM中申请内存,用完就释放。二者区别在于静态的内存始终存在,无法被释放。
静态单任务
硬件初始化
首先是静态创建单个任务。在main.c中定义初始化函数并进行声明,将会用到的外设都在这个函数里进行初始化。
#include "FreeRTOS.h"
#include "task.h"
#include "led.h"
#include "usart.h"
static void Device_Init(void);
int main(void)
{
}
/* 静态创建 硬件初始化 */
static void Device_Init(void)
{
/* 中断优先级分组,配置为4 */
NVIC_PriorityGroupConfig( NVIC_PriorityGroup_4 );
/* LED 初始化 */
LED_GPIO_Config();
/* 串口初始化 */
USART_Config();
}
STM32中断优先级分组为4,即4bit都用来表示抢占优先级,范围为:0~15。优先级分组只需要分组一次即可,以后如果有其他的任务需要用到中断,都统一用这个优先级分组,千万不要再分组。
在程序执行的过程中,会先运行上述函数。这个函数和裸机程序的硬件初始化是一样的,在初始化完成后才会启动操作系统。
因此,在实际开发过程中,可以在初始化函数中加入一些命令和死循环来进行测试,确认硬件没有问题后注释掉循环即可。测试命令因为只执行一次,可自行判断是否留下。
创建单个任务——静态
当我们静态创建任务的时候,FreeRTOSConfig.h的configSUPPORT_STATIC_ALLOCATION 这个宏必 须为 1,否则会报错。
定义任务函数
任务就是无限执行的函数,在main.c中定义任务:让LED1以500ms的频率闪烁。
static void LED1_Task(void* pvParameters);
static void LED1_Task(void* parameter)
{
while (1)
{
LED1_ON;
vTaskDelay(500); /* 延时500个tick */
printf("LED_Task Running,LED1_ON\r\n");
LED1_OFF;
vTaskDelay(500); /* 延时500个tick */
printf("LED_Task Running,LED1_OFF\r\n");
}
}
这里的延时函数必须使用 FreeRTOS 提供的延时函数,不要使用我们裸机编程中的软件延时。vTaskDelay()会让当前任务被阻塞转而去执行其他任务;而裸机的延时会让CPU陷入死循环,一直在执行当前任务无法跳出,无法实现多任务系统。
定义空闲任务与定时器任务堆栈函数
接下来实现两个函数 : vApplicationGetIdleTaskMemory()与 vApplicationGetTimerTaskMemory(),这两个函数是用户设定的空闲(Idle)任务与定时器(Timer)任务的堆栈大小,必须由用户自己分配。
/* 空闲任务堆栈 */
static StackType_t Idle_Task_Stack[configMINIMAL_STACK_SIZE];
/* 定时器任务堆栈 */
static StackType_t Timer_Task_Stack[configTIMER_TASK_STACK_DEPTH];
/* 空闲任务控制块 */
static StaticTask_t Idle_Task_TCB;
/* 定时器任务控制块 */
static StaticTask_t Timer_Task_TCB;
/* 获取空闲任务的任务堆栈和任务控制块内存 */
void vApplicationGetIdleTaskMemory(StaticTask_t **ppxIdleTaskTCBBuffer,
StackType_t **ppxIdleTaskStackBuffer,
uint32_t *pulIdleTaskStackSize)
{
*ppxIdleTaskTCBBuffer=&Idle_Task_TCB;/* 任务控制块内存 */
*ppxIdleTaskStackBuffer=Idle_Task_Stack;/* 任务堆栈内存 */
*pulIdleTaskStackSize=configMINIMAL_STACK_SIZE;/* 任务堆栈大小 */
}
/* 获取定时器任务的任务堆栈和任务控制块内存 */
void vApplicationGetTimerTaskMemory(StaticTask_t **ppxTimerTaskTCBBuffer,
StackType_t **ppxTimerTaskStackBuffer,
uint32_t *pulTimerTaskStackSize)
{
*ppxTimerTaskTCBBuffer=&Timer_Task_TCB;/* 任务控制块内存 */
*ppxTimerTaskStackBuffer=Timer_Task_Stack;/* 任务堆栈内存 */
*pulTimerTaskStackSize=configTIMER_TASK_STACK_DEPTH;/* 任务堆栈大小 */
}
定义任务栈和任务控制块
由于当前只有一个任务,在当前任务进入延时后,系统就会进入运行空闲任务。空闲任务的优先级最低,所以在任务延时结束后就会继续运行这个任务。
这些任务同样需要定义栈空间。
/* AppTaskCreate任务任务堆栈 */
static StackType_t AppTaskCreate_Stack[128];
/* LED任务堆栈 */
static StackType_t LED1_Task_Stack[128];
/* AppTaskCreate 任务控制块 */
static StaticTask_t AppTaskCreate_TCB;
/* AppTaskCreate 任务控制块 */
static StaticTask_t LED_Task_TCB;
在大多数系统中需要做栈空间地址对齐,在 FreeRTOS 中一般都是以 8 字节大小对齐,并且会检查堆栈是否已经对齐。字节对齐通过portmacro.h里的宏定义。此外用户也可以选择按 1、 2、 4、 8、 16、 32 等字节对齐。
静态创建任务
创建一个任务需要三样东西,分别是任务主体函数,任务栈和任务控制块。有了这些之后,通过静态任务创建函数 xTaskCreateStatic()将这些信息整合成任务。
我们把任务主体,任务栈,任务控制块这三部分都在 main.c 中进行编写和声明,并在main.c 文件中创建一个 AppTaskCreate 任务,我们创建的所有的任务都会放在这个函数里面。
/* 创建 AppTaskCreate 任务 */
AppTaskCreate_Handle = xTaskCreateStatic((TaskFunction_t)AppTaskCreate, //任务函数
(const char* )"AppTaskCreate", //任务名称
(uint32_t )128, //任务堆栈大小
(void* )NULL, //传递给任务函数的参数
(UBaseType_t )3, //任务优先级
(StackType_t* )AppTaskCreate_Stack,//任务堆栈
(StaticTask_t* )&AppTaskCreate_TCB); //任务控制块
空闲任务和定时器任务
在FreeRTOS中,只有我们自定义的任务是不行的,还需要有空闲任务和定时器任务。这两个任务在调度器中实现,在静态创建时我们只能自己来定义。这里我们定义在main.c中。
/* 获取空闲任务的任务堆栈和任务控制块内存 */
void vApplicationGetIdleTaskMemory(StaticTask_t **ppxIdleTaskTCBBuffer,
StackType_t **ppxIdleTaskStackBuffer,
uint32_t *pulIdleTaskStackSize)
{
*ppxIdleTaskTCBBuffer=&Idle_Task_TCB;/* 任务控制块内存 */
*ppxIdleTaskStackBuffer=Idle_Task_Stack;/* 任务堆栈内存 */
*pulIdleTaskStackSize=configMINIMAL_STACK_SIZE;/* 任务堆栈大小 */
}
/* 获取定时器任务的任务堆栈和任务控制块内存 */
void vApplicationGetTimerTaskMemory(StaticTask_t **ppxTimerTaskTCBBuffer,
StackType_t **ppxTimerTaskStackBuffer,
uint32_t *pulTimerTaskStackSize)
{
*ppxTimerTaskTCBBuffer=&Timer_Task_TCB;/* 任务控制块内存 */
*ppxTimerTaskStackBuffer=Timer_Task_Stack;/* 任务堆栈内存 */
*pulTimerTaskStackSize=configTIMER_TASK_STACK_DEPTH;/* 任务堆栈大小 */
启动任务
当任务创建好后,需要开启任务调度器,才能执行任务。任务调度器只会在整个系统中启动一次,之后就不会再次执行了。 当启动任务调度器后,便不会有返回,之后任务都由FreeRTOS进行管理。
if(NULL != AppTaskCreate_Handle)/* 创建成功 */
vTaskStartScheduler(); /* 启动任务,开启调度 */
PS:启动任务的代码请参考野火例程,本文只进行流程讲解。
动态单任务
动态任务使用的的任务控制块和任务栈是SRAM分配的,具体位置由编译器决定。
堆,也就是动态内存,也是在SRAM里的一段内存。操作系统在SRAM里定义一个大数组作为堆,提供给动态内存分配函数使用。在第一次使用的时候,系统会将定义的堆内存进行初始化,至于初始化的方式在官方提供的内存管理方案,即heap源文件中实现。
堆的大小在FreeRTOSConfig.h中,由宏定义#define configTOTAL_HEAP_SIZE确定,目前设置大小为36KB。系统关于内存空间的开辟在heap源文件中进行配置,不需要我们去管理。同时我们需要将支持动态内存申请的宏#define configSUPPORT_DYNAMIC_ALLOCATION置为1。
//支持动态内存申请
#define configSUPPORT_DYNAMIC_ALLOCATION 1
//系统所有总的堆大小
#define configTOTAL_HEAP_SIZE ((size_t)(36*1024))
定义任务函数
和静态创建一样,动态创建也需要定义任务函数。
static void LED_Task(void* parameter)
{
while (1)
{
LED1_ON;
vTaskDelay(500); /* 延时500个tick */
printf("LED_Task Running,LED1_ON\r\n");
LED1_OFF;
vTaskDelay(500); /* 延时500个tick */
printf("LED_Task Running,LED1_OFF\r\n");
}
}
任务必须是一个无限循环,如果出现异常,则必须通过 LR 返回。这时操作系统会运行任务退出函数prvTaskExitError(),使系统在这个死循环任务里一直循环下去,这种情况是很危险的。为了避免这种情况,只需要让AppTaskCreate 执行一次之后就删除掉即可。
任务里面的延时函数必须使用 FreeRTOS 里面提供的阻塞延时函数。关于阻塞延时在之前的文章提过,不了解的可以去翻一翻。
此外,动态创建任务时,任务栈会在任务创建的时候创建,不需要提前定义栈空间。动态内存就是按需分配内存,需要就分配,用完就释放。
因为这种随取随用的风格,一般我们都使用用动态创建任务。
定义任务控制块指针
其实任务控制块也是在任务创建的时候创建,任务创建函数会返回一个指针,用于指向任务控制块。因此要提前为任务栈定义一个任务控制块指针,也就是任务句柄。这部分在main.c中定义。
/* 创建任务句柄 */
static TaskHandle_t AppTaskCreate_Handle = NULL;
/* LED任务句柄 */
static TaskHandle_t LED_Task_Handle = NULL;
动态创建任务
动态创建任务时,使用 xTaskCreate()函数来创建任务,和静态创建有很多相似之处,也有些区别。
/* 创建AppTaskCreate任务 */
xReturn = xTaskCreate((TaskFunction_t )AppTaskCreate, /* 任务入口函数 */
(const char* )"AppTaskCreate",/* 任务名字 */
(uint16_t )512, /* 任务栈大小 */
(void* )NULL,/* 任务入口函数参数 */
(UBaseType_t )1, /* 任务的优先级 */
(TaskHandle_t* )&AppTaskCreate_Handle);/* 任务控制块指针 */
这里的是一个创建信息返回值,默认值为pdPASS。
BaseType_t xReturn = pdPASS;
这里再次把静态任务的定义放在这:
/* 创建 AppTaskCreate 任务 */
AppTaskCreate_Handle = xTaskCreateStatic((TaskFunction_t)AppTaskCreate, //任务函数
(const char* )"AppTaskCreate", //任务名称
(uint32_t )128, //任务堆栈大小
(void* )NULL, //传递给任务函数的参数
(UBaseType_t )3, //任务优先级
(StackType_t* )AppTaskCreate_Stack,//任务堆栈
(StaticTask_t* )&AppTaskCreate_TCB); //任务控制块
对比静态任务,动态任务少了堆栈的定义。
启动任务
同静态一样,动态任务也需要调度器才能启动任务。
/* 启动任务调度 */
if(pdPASS == xReturn)
vTaskStartScheduler(); /* 启动任务,开启调度 */
else
return -1;
创建多任务 ——动态
创建多任务和单任务很相似,只需要把任务创建有关的东西写两遍,注意一下函数名和优先级即可。接下来创建两个任务,任务 1 让一个 LED 灯闪烁,任务 2 让另外一个 LED 闪烁,两个 LED 闪烁的频率不一样。
接下来将不一样的地方展示出来,剩下的和动态单任务基本一致。
/* LED1任务句柄 */
static TaskHandle_t LED1_Task_Handle = NULL;
/* LED2任务句柄 */
static TaskHandle_t LED2_Task_Handle = NULL;
/* 函数声明 */
static void LED1_Task(void* pvParameters);/* LED1_Task任务实现 */
static void LED2_Task(void* pvParameters);/* LED2_Task任务实现 */
static void AppTaskCreate(void)
{
BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
taskENTER_CRITICAL(); //进入临界区
/* 创建LED_Task任务 */
xReturn = xTaskCreate((TaskFunction_t )LED1_Task, /* 任务入口函数 */
(const char* )"LED1_Task",/* 任务名字 */
(uint16_t )512, /* 任务栈大小 */
(void* )NULL, /* 任务入口函数参数 */
(UBaseType_t )2, /* 任务的优先级 */
(TaskHandle_t* )&LED1_Task_Handle);/* 任务控制块指针 */
if(pdPASS == xReturn)
printf("创建LED1_Task任务成功!\r\n");
/* 创建LED_Task任务 */
xReturn = xTaskCreate((TaskFunction_t )LED2_Task, /* 任务入口函数 */
(const char* )"LED2_Task",/* 任务名字 */
(uint16_t )512, /* 任务栈大小 */
(void* )NULL, /* 任务入口函数参数 */
(UBaseType_t )3, /* 任务的优先级 */
(TaskHandle_t* )&LED2_Task_Handle);/* 任务控制块指针 */
if(pdPASS == xReturn)
printf("创建LED2_Task任务成功!\r\n");
vTaskDelete(AppTaskCreate_Handle); //删除AppTaskCreate任务
taskEXIT_CRITICAL(); //退出临界区
}
/* LED1任务函数主体 */
static void LED1_Task(void* parameter)
{
while (1)
{
LED1_ON;
vTaskDelay(500); /* 延时500个tick */
printf("LED1_Task Running,LED1_ON\r\n");
LED1_OFF;
vTaskDelay(500); /* 延时500个tick */
printf("LED1_Task Running,LED1_OFF\r\n");
}
}
/* LED2任务函数主体 */
static void LED2_Task(void* parameter)
{
while (1)
{
LED2_ON;
vTaskDelay(500); /* 延时500个tick */
printf("LED2_Task Running,LED2_ON\r\n");
LED2_OFF;
vTaskDelay(500); /* 延时500个tick */
printf("LED2_Task Running,LED2_OFF\r\n");
}
}
在实际使用中,我们肯定不会只创建一两个任务。在这基础上,我们进行多任务的动态创建基本就能满足大部分需求。