什么是任务
任务,可以说是FreeRTOS的重中之重,是实现操作系统的根本。
这里套用野火的例子:创建两个任务TASK1和TASK2,任务的内容都是让一个变量按一定的频率不断翻转,并让单片机将两个任务不断切换。
这个例子用裸机系统写的话,只用简单的delay延时加翻转标志位就能实现。在实现过程中,CPU是在按顺序执行每个任务,也就是轮询。如果我们使用软件仿真可以看到,两个任务是交替进行的。
而当我们采用多任务系统的方式去完成这个例子,仿真的效果是两个任务同时进行,就像两件事情在同时被处理。
为什么会有这样的差别呢?通过对任务的掌握,我们就能理解了。
任务,就是在多任务系统中,所有功能都分割成独立的且无法返回的函数。
在裸机系统中,比如全局变量,子函数,函数返回地址等环境参数,他们统统放在一个叫栈的地方(一般称为主栈)。栈是单片机 RAM 里面一段连续的内存空间,栈的大小一般在启动文件或者链接脚本里面指定,最后由 C 库函数_main 进行初始化。
简单来说,就是我们不需要管它存放在哪,只需要知道它大概是干什么的,给它分配空间就行。
对于操作系统来说,每个任务都是独立的,有多少个任务就需要定义多少个任务栈。因此要为每个任务分配独立的栈空间,通常是一个全局数组或者是动态分配的内存空间,存在于RAM中。
创建任务
定义任务栈
我们要实现两个变量按照一定的频率轮流的翻转,每个变量对应一个任务,所以需要定义两个任务栈。
任务栈其实就是一个预先定义好的全局数据,我们可以直接在main.c中定义,也可以放在FreeROS.h中。我这里是放在了main.c中。
#include "FreeRTOS.h"
#include "task.h"
#define TASK1_STACK_SIZE 128
StackType_t Task1Stack[TASK1_STACK_SIZE];
#define TASK2_STACK_SIZE 128
StackType_t Task2Stack[TASK2_STACK_SIZE];
任务栈(TaskxStack)数据类型为StackType_t,大小由 TASK1_STACK_SIZE 这个宏来定义,默认为 128,单位为字,即 512字节,这也是 FreeRTOS 推荐的最小的任务栈。
前面提到过,在 FreeRTOS 中,凡是涉及到数据类型的地方,FreeRTOS 都会将这些数据类型用 typedef 重新取一个类型名,目的是为了适配不同位的单片机。这些经过重定义的数据类型都放在 portmacro.h里,之后有重定义的地方将默认都在这个头文件里面定义。
创建任务和延时
接下来定义两个任务,在多任务系统中,每个任务都是一个独立的、无限循环的且无法返回跳出的函数,在main.c的主函数中定义他们。先准备一个简单的延时函数,然后完成两个任务的创建。
/* main.c中函数声明 */
void delay (uint32_t count);
void Task1_Entry( void *p_arg );
void Task2_Entry( void *p_arg );
/* 软件延时 */
void delay (uint32_t count)
{
for(; count!=0; count--);
}
/* 任务1 */
void Task1_Entry( void *p_arg )
{
for( ;; )
{
flag1 = 1;
delay( 100 );
flag1 = 0;
delay( 100 );
}
}
/* 任务2 */
void Task2_Entry( void *p_arg )
{
for( ;; )
{
flag2 = 1;
delay( 100 );
flag2 = 0;
delay( 100 );
}
任务控制块
在裸机系统中,程序的主体是由 CPU 按照顺序执行的。而在多任务系统中,任务的执行是由系统调度的。系统为了顺利的调度任务,为每个任务都额外定义了一个任务控制块,这个任务控制块存有任务的所有信息,比如任务的栈指针,名称,形参等。系统对任务的全部操作都可以通过任务控制块来实现。
定义任务控制块的结构体,使用它可以为每个任务都定义一个任务控制块实体。在task.h中定义。
typedef struct tskTaskControlBlock
{
volatile StackType_t *pxTopOfStack;
/* 栈顶指针,作为 TCB 的第一个成员*/
ListItem_t xStateListItem;
/* 任务节点,一个内嵌在 TCB 控制块中的链表节点,通过这个
节点,可以将任务控制块挂到各种链表中。类似链表指针和节点的关系*/
StackType_t *pxStack; /* 任务栈起始地址 */
char pcTaskName[ configMAX_TASK_NAME_LEN ];
/* 任务名称,字符串形式,控制长度的宏在 FreeRTOSConfig.h 中定义,默认为 16 */
} tskTCB;
typedef tskTCB TCB_t;
由于我们要实现两个任务的翻转切换,因此在 main.c 文件中定义两个任务控制块。
TCB_t Task1TCB;
TCB_t Task2TCB;
任务创建函数xTaskCreateStatic()
任务的栈,函数实体,任务控制块等都创建好之后,需要定义一个任务创建函数将这些东西放到一起,之后才能由系统进行统一调
度。这个函数函数在 task.c中定义,在 task.h 中声明,之后所有跟任务相关的函数都在这个文件定义。
声明:
#if( configSUPPORT_STATIC_ALLOCATION == 1 )
TaskHandle_t xTaskCreateStatic( TaskFunction_t pxTaskCode, /* 任务入口 */
const char * const pcName, /* 任务名称,字符串形式 */
const uint32_t ulStackDepth, /* 任务栈大小,单位为字 */
void * const pvParameters, /* 任务形参 */
UBaseType_t uxPriority, /* 任务优先级,数值越大,优先级越高 */
StackType_t * const puxStackBuffer, /* 任务栈起始地址 */
TCB_t * const pxTaskBuffer ); /* 任务控制块 */
#endif /* configSUPPORT_STATIC_ALLOCATION */
这里的configSUPPORT_STATIC_ALLOCATION在FreeRTOS.h中定义,当这个值为1时允许静态分配,为0则禁止。
函数定义:
/*静态任务创建函数*/
#if( configSUPPORT_STATIC_ALLOCATION == 1 )
TaskHandle_t xTaskCreateStatic( TaskFunction_t pxTaskCode,/* 任务入口,即任务的函数名 */
const char * const pcName, /* 任务名称,字符串形式 */
const uint32_t ulStackDepth, /* 任务栈大小,单位为字 */
void * const pvParameters, /* 任务形参 */
StackType_t * const puxStackBuffer, /* 任务栈起始地址 */
TCB_t * const pxTaskBuffer ) /* 任务控制块指针 */
{
TCB_t *pxNewTCB; //定义一个任务控制块
TaskHandle_t xReturn;
/*定义一个任务句柄 xReturn*/
if( ( pxTaskBuffer != NULL ) && ( puxStackBuffer != NULL ) )
{
pxNewTCB = ( TCB_t * ) pxTaskBuffer;
pxNewTCB->pxStack = ( StackType_t * ) puxStackBuffer;
/* 创建新任务 */
prvInitialiseNewTask( pxTaskCode, /* 任务入口 */
pcName, /* 任务名称,字符串形式 */
ulStackDepth, /* 任务栈大小,单位为字 */
pvParameters, /* 任务形参 */
&xReturn, /* 任务句柄 */
pxNewTCB); /* 任务栈起始地址 */
}
else
{
xReturn = NULL;
}
/* 返回任务句柄,如果任务创建成功,此时xReturn应该指向任务控制块,xReturn 作为形参传入到 prvInitialiseNewTask 函数。 */
return xReturn;
}
#endif /* configSUPPORT_STATIC_ALLOCATION */
任务句柄就是是指向任务控制块(TCB)的指针。
TaskFunction_t 在 projdefs.h中重定义,实际就是空指针,TaskHandle_t,在 task.h 中定义,也是一个空指针。
#ifndef PROJDEFS_H
#define PROJDEFS_H
/* TaskFunction_t定义 */
typedef void (*TaskFunction_t)( void * );
#endif /* PROJDEFS_H */
FreeRTOS 中,任务的创建有两种方法,一种是使用动态创建,一种是使用静态创建。动态创建时,任务控制块和栈的内存是创建任务时动态分配的,任务删除时,内存可以释放;静态创建时,任务控制块和栈的内存需要事先定义好,是静态的内存,任务删除时,内存不能释放。
以静态创建为例,configSUPPORT_STATIC_ALLOCATION 在 FreeRTOSConfig.h 中定义,配置为1。
创建新任务函数prvInitialiseNewTask()
创建新任务的函数prvInitialiseNewTask()在 task.c 实现。
static void prvInitialiseNewTask( TaskFunction_t pxTaskCode, /* 任务入口 */
const char * const pcName, /* 任务名称,字符串形式 */
const uint32_t ulStackDepth, /* 任务栈大小,单位为字 */
void * const pvParameters, /* 任务形参 */
TaskHandle_t * const pxCreatedTask, /* 任务句柄 */
TCB_t *pxNewTCB ) /* 任务控制块指针 */
{
StackType_t *pxTopOfStack;
UBaseType_t x;
/* 获取栈顶地址 */
pxTopOfStack = pxNewTCB->pxStack + ( ulStackDepth - ( uint32_t ) 1 );
//pxTopOfStack = ( StackType_t * ) ( ( ( portPOINTER_SIZE_TYPE ) pxTopOfStack ) & ( ~( ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK ) ) );
/* 向下做8字节对齐 */
pxTopOfStack = ( StackType_t * ) ( ( ( uint32_t ) pxTopOfStack ) & ( ~( ( uint32_t ) 0x0007 ) ) );
/* 将任务的名字存储在TCB中 */
for( x = ( UBaseType_t ) 0; x < ( UBaseType_t ) configMAX_TASK_NAME_LEN; x++ )
{
pxNewTCB->pcTaskName[ x ] = pcName[ x ];
if( pcName[ x ] == 0x00 )
{
break;
}
}
/* 任务名字的长度不能超过configMAX_TASK_NAME_LEN,并以'\0'结尾。 */
pxNewTCB->pcTaskName[ configMAX_TASK_NAME_LEN - 1 ] = '\0';
/* 初始化TCB中的xStateListItem节点 */
vListInitialiseItem( &( pxNewTCB->xStateListItem ) );
/* 设置xStateListItem节点的拥有者 */
listSET_LIST_ITEM_OWNER( &( pxNewTCB->xStateListItem ), pxNewTCB );
/* 初始化任务栈 */
pxNewTCB->pxTopOfStack = pxPortInitialiseStack( pxTopOfStack, pxTaskCode, pvParameters );
/* 让任务句柄指向任务控制块 */
if( ( void * ) pxCreatedTask != NULL )
{
*pxCreatedTask = ( TaskHandle_t ) pxNewTCB;
}
}
栈顶地址就是起始地址加栈的大小减一,因为是从0开始计数的。
将栈顶指针向下做 8 字节对齐。在 CM3、CM4、CM7内核的单片机中,因为总线宽度是 32 位的,通常只要栈保持 4 字节对齐就行,8字节对齐则是考虑到了浮点运算。如果栈顶指针是 8 字节对齐的,在进行向下 8 字节对齐的时候,指针不会移动,如果不是8 字节对齐的,在做向下 8 字节对齐的时候,就会空出几个字节,不会使用。
初始化 TCB 中的 xStateListItem 节点,即初始化该节点所在的链表为空,表示节点还没有插入任何链表。
设置 xStateListItem 节点的拥有者,即拥有这个节点本身的 TCB。将链表部分提的官方提供的宏定义加入list.h中后,通过该句代码将链表的owner指向这个任务。
调用 pxPortInitialiseStack()函数初始化任务栈,并更新栈顶指针,任务第一次运行的环境参数就存在任务栈中。
初始化任务栈函数pxPortInitialiseStack()
该函数因为是与硬件相关的,所以定义portable.h在中。
#ifndef PORTABLE_H
#define PORTABLE_H
#include "portmacro.h"
StackType_t *pxPortInitialiseStack( StackType_t *pxTopOfStack, TaskFunction_t pxCode, void *pvParameters );
#endif /* PORTABLE_H */
该函数在 port.c中定义。
#include "FreeRTOS.h"
#include "task.h"
#include "ARMCM3.h"
#include "list.h"
#define portINITIAL_XPSR ( 0x01000000 )
#define portSTART_ADDRESS_MASK ( ( StackType_t ) 0xfffffffeUL )
/* 任务栈初始化函数 */
static void prvTaskExitError( void )
{
/* 函数停止在这里,一般函数不会返回这里 */
for(;;);
}
StackType_t *pxPortInitialiseStack( StackType_t *pxTopOfStack, TaskFunction_t pxCode, void *pvParameters )
{
/* 异常发生时,CPU自动加载到CPU寄存器内的内容 */
pxTopOfStack--;
*pxTopOfStack = portINITIAL_XPSR; /* xPSR的bit24必须置1 */
pxTopOfStack--;
*pxTopOfStack = ( ( StackType_t ) pxCode ) & portSTART_ADDRESS_MASK;
/* PC,即任务入口函数 */
pxTopOfStack--;
*pxTopOfStack = ( StackType_t ) prvTaskExitError; /* LR,函数返回地址 */
pxTopOfStack -= 5; /* R12, R3, R2, R1 默认初始化为0 */
*pxTopOfStack = ( StackType_t ) pvParameters; /* R0,任务形参 */
/* 异常(中断)发生时,手动加载到CPU寄存器的内容 */
pxTopOfStack -= 8; /* R11, R10, R9, R8, R7, R6, R5, R4默认初始化为0 */
/* 返回栈顶指针,此时pxTopOfStack指向空闲栈 */
return pxTopOfStack;
}
任务栈初始化完毕之后,栈空间内部分布如下图:
异常发生时, CPU 自动从栈中加载到 CPU 寄存器的内容。包括 8个寄存器,分别为 R0、 R1、 R2、 R3、 R12、 R14、 R15 和 xPSR 的位 24,且顺序不能变。
之后栈顶指针不断减一,依次读取每个栈值。
xPSR 的 bit24 必须置 1,即 0x01000000。
通常任务是不会返回的,当异常发生时,便会发生中断,就会返回跳转到prvTaskExitError()中, 该函数是一个无限循环。
R12, R3, R2,R1 默认初始化为 0。
当异常发生时,需要手动加载到 CPU 寄存器的内容, 总共有 8 个,分别为 R4、 R5、 R6、 R7、 R8、 R9、 R10 和 R11,默认初始化为 0,最后返回栈顶指针,此时 pxTopOfStack 指向空闲栈。
任务第一次运行时,从这个栈指针开始手动加载 8 个字的内容到 CPU 寄存器: R4、 R5、 R6、 R7、R8、 R9、 R10 和 R11。当退出异常时,栈中剩下的 8 个字的内容会自动加载到 CPU 寄存器: R0、 R1、 R2、 R3、 R12、 R14、 R15 和 xPSR 的位 24。此时 PC 指针就指向了任务入口地址,从而成功跳转到第一个任务。
至此,任务的创建基本实现。