跟着野火从零开始手搓FreeRTOS(3.1)任务的了解与创建

本文详细介绍了在FreeRTOS中如何创建任务,包括任务栈的定义、任务控制块的作用,以及静态和动态任务创建的区别。重点讲解了任务栈的管理,如栈顶指针的计算和异常处理中的栈内容加载。
摘要由CSDN通过智能技术生成

什么是任务

任务,可以说是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 指针就指向了任务入口地址,从而成功跳转到第一个任务。

        至此,任务的创建基本实现。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值