FreeRTOS——任务的定义与切换

本节属于操作系统中基础中的基础,包括任务的创建与切换。以两个任务为例,在多任务系统中,两个变量波形完全一样,就好像CPU在同时做两件事,这才是多任务的意义。
在这里插入图片描述

一、什么是任务

裸机系统中,系统的主体是main函数里顺序执行的无限循环,这个无限循环里面CPU按照顺序完成各种事情。在多任务系统中,我们可以根据功能的不同,把整个系统分割为一个个独立的且无法返回的函数,这个函数我们就称为任务。如下:

void task_entry (void *parg)
{
 /* 任务主体,无限循环且不能返回 */
 for (;;) {
 /* 任务主体代码 */
 }
}

二、创建任务

1、定义任务栈

在裸机系统中,局部变量、全局变量和函数返回地址统统放在一个叫栈的地方,栈是单片机里一段连续的内存空间,栈的大小一般在启动文件或链接脚本里指定,最后由C库函数_main进行初始化。
在这里插入图片描述
在多任务系统中,每个任务都是独立的,互不干扰,所以要为每个任务都分配栈空间,这个栈空间通常是预先定义好的一个全局数组,也可以是动态分配的一段内存空间,但他们都存在与RAM中。
在这里插入图片描述
首先定义任务栈:

#define TASK1_STACK_SIZE 128                (2)
StackType_t Task1Stack[TASK1_STACK_SIZE];   (1)

#define TASK2_STACK_SIZE 128
StackType_t Task2Stack[TASK2_STACK_SIZE];

(1)任务栈是一个预先定义好的全局数组,数据类型为StackType_t ,大小为TASK1_STACK_SIZE,默认128,单位字,也是FreeRTOS推荐的最小任务栈。
FreeRTOS中,凡是涉及到的的数据类型,都会用typedef重新取名。这些经过重定义的数据类型放在 portmacro.h头文件中。

#ifndef PORTMACRO_H
#define PORTMACRO_H

/* 包含标准库头文件 */
#include "stdint.h"
#include "stddef.h"

/* 数据类型重定义 */
#define portCHAR char
#define portFLOAT float
#define portDOUBLE double
#define portLONG long
#define portSHORT short
#define portSTACK_TYPE uint32_t
#define portBASE_TYPE long

typedef portSTACK_TYPE StackType_t;
typedef long BaseType_t;
typedef unsigned long UBaseType_t;

#endif /* PORTMACRO_H */

2、定义任务函数

/* 软件延时 */
void delay (uint32_t count)
{
 for (; count!=0; count--);
}
/* 任务 1 */
void Task1_Entry( void *p_arg ) (1)
{
 for ( ;; )
 {
 flag1 = 1;
 delay( 100 );
 flag1 = 0;
 delay( 100 );
 }
}

/* 任务 2 */
void Task2_Entry( void *p_arg ) (2)
{
 for ( ;; )
 {
 flag2 = 1;
 delay( 100 );
 flag2 = 0;
 delay( 100 );
 }
}

以上就是两个独立、无限循环且不能返回的任务函数。

3、定义任务控制块

多任务系统中,任务的执行由系统调度。系统为了顺利调度任务,为每个任务额外定义了一个任务控制块,这个任务控制块就相当于任务的身份证,包含任务的所有信息,比如任务的栈指针、任务名称、任务的形参等。有了任务控制块后,系统对任务的全部操作都可以通过这个任务块实现。以下为一个任务控制块的声明:

typedef struct tskTaskControlBlock
{
 volatile StackType_t   *pxTopOfStack; /* 栈顶指针 */(1)
 ListItem_t             xStateListItem; /* 任务节点 */(2)
 StackType_t            *pxStack; /* 任务栈起始地址 */ (3)
 char                   pcTaskName[ configMAX_TASK_NAME_LEN ];/* 任务名称,字符串形式 */(4)
} tskTCB;
typedef tskTCB TCB_t; (5)

(2)作为内置在TCB控制块中的链表节点,通过这个节点,可以将这个任务控制块挂接到各种链表中。
在main.c中定义两个任务控制块:

/* 定义任务控制块 */
TCB_t Task1TCB;
TCB_t Task2TCB;

4、定义任务创建函数

任务的栈、任务的函数实体、任务的控制块最终需要联系起来才能由系统进行统一调度。这个联系工作就由任务创建函数xTaskCreateStatic()来实现。

TaskHandle_t xTaskCreateStatic( TaskFunction_t pxTaskCode, (2)
								const char * const pcName, (3)
								const uint32_t ulStackDepth, (4)
								void * const pvParameters, (5)
								StackType_t * const puxStackBuffer, (6)
								TCB_t * const pxTaskBuffer ) (7)
{
 TCB_t *pxNewTCB;
 TaskHandle_t xReturn; (8)

 if ( ( pxTaskBuffer != NULL ) && ( puxStackBuffer != NULL ) )
 {
	 pxNewTCB = ( TCB_t * ) pxTaskBuffer;
	 pxNewTCB->pxStack = ( StackType_t * ) puxStackBuffer;
	
	 /* 创建新的任务 */ (9)
	 prvInitialiseNewTask( pxTaskCode, /* 任务入口 */
						   pcName, /* 任务名称,字符串形式 */
						   ulStackDepth, /* 任务栈大小,单位为字 */
						   pvParameters, /* 任务形参 */
						   &xReturn, /* 任务句柄 */
						   pxNewTCB); /* 任务栈起始地址 */
 }
 else
 {
 	xReturn = NULL;
 }

 /* 返回任务句柄,如果任务创建成功,此时 xReturn 应该指向任务控制块 */
 return xReturn; (10)
}

FreeRTOS中,任务的创建有两种方法,一是动态创建,另一种是静态创建。动态创建时,任务控制块和栈的内存是动态分配的,任务删除时,内存可以释放。而静态创建时,任务控制块和栈的内存是事先定义好的,是静态内存,任务删除时,内存不能释放。
(2):任务入口,即任务的函数名称。TaskFunction_t 是在 projdefs.h中重定义的一个数据类型,实际就是空指针,如下:

#ifndef PROJDEFS_H
#define PROJDEFS_H

typedef void (*TaskFunction_t)( void * );

#define pdFALSE ( ( BaseType_t ) 0 )
#define pdTRUE ( ( BaseType_t ) 1 )

#define pdPASS ( pdTRUE )
#define pdFAIL ( pdFALSE )

#endif /* PROJDEFS_H */

(9):调用 prvInitialiseNewTask()函数,创建新任务,该函数在 task.c 实现。prvInitialiseNewTask()函数如下:

static void prvInitialiseNewTask( TaskFunction_t pxTaskCode, /*任务入口,任务函数名*/(1)
								  const char * const pcName, /*任务名称,字符串形式*/(2)
								  const uint32_t ulStackDepth, /*任务栈大小,单位为字*/(3)
								  void * const pvParameters, /*任务形参,没有时可以为NULL*/(4)
								  TaskHandle_t * const pxCreatedTask, /*任务句柄*/(5)
								  TCB_t *pxNewTCB ) /*任务控制块指针*/(6)

三、实现就绪列表

任务创建好后,我们需要把任务添加到就绪列表中,表示任务已经就绪,系统随时可以调度。

1、定义就绪列表

List_t pxReadyTasksLists[ configMAX_PRIORITIES ];

任务就绪列表是一个List_t类型的数组,数组大小有决定最大任务优先级宏configMAX_PRIORITIES决定,最大支持256个优先级,这里默认定义为5。数组的下表对应了任务的优先级,同一优先级的任务统一插入到同一就绪列表的链表中。一个空的就绪列表如下:
在这里插入图片描述

2、就绪列表初始化

就绪列表初始化工作在函数prvInitialiseTaskLists()里面实现。

void prvInitialiseTaskLists( void )
{
	 UBaseType_t uxPriority;
	 
	 for ( uxPriority = ( UBaseType_t ) 0U;
	 uxPriority < ( UBaseType_t ) configMAX_PRIORITIES;
	 uxPriority++ )
	 {
	 	vListInitialise( &( pxReadyTasksLists[ uxPriority ] ) );
	 }
}

初始化后如下图:
在这里插入图片描述

2、将任务插入到就绪列表中

我们通过任务控制块中的xStateListItem 这个节点插入到就绪列表中来实现的。如果把就绪列表比作是晾衣架,任务是衣服,那xStateListItem就是晾衣架上面的钩子,每个任务都自带晾衣架钩子,就是为了把自己挂在各种不同的链表中。
任务创建好后,紧跟着就要实现任务插入到就绪列表中。

/* 初始化与任务相关的列表,如就绪列表 */
prvInitialiseTaskLists();

Task1_Handle = /* 任务句柄 */
xTaskCreateStatic( (TaskFunction_t)Task1_Entry, /* 任务入口 */
				   (char *)"Task1", /* 任务名称,字符串形式 */
				   (uint32_t)TASK1_STACK_SIZE , /* 任务栈大小,单位为字 */
				   (void *) NULL, /* 任务形参 */
				   (StackType_t *)Task1Stack, /* 任务栈起始地址 */
				   (TCB_t *)&Task1TCB ); /* 任务控制块 */
 /* 将任务添加到就绪列表 */
vListInsertEnd( &( pxReadyTasksLists[1] ),&( ((TCB_t *)(&Task1TCB))->xStateListItem ) );

Task2_Handle = /* 任务句柄 */
xTaskCreateStatic( (TaskFunction_t)Task2_Entry, /* 任务入口 */
				   (char *)"Task2", /* 任务名称,字符串形式 */
				   (uint32_t)TASK2_STACK_SIZE , /* 任务栈大小,单位为字 */
				   (void *) NULL, /* 任务形参 */
				   (StackType_t *)Task2Stack, /* 任务栈起始地址 */
				   (TCB_t *)&Task2TCB ); /* 任务控制块 */
/* 将任务添加到就绪列表 */
vListInsertEnd( &( pxReadyTasksLists[2] ),&( ((TCB_t *)(&Task2TCB))->xStateListItem ) );

在这里,我们把Task1任务插入到就绪列表下标为1的链表中,把Task2任务插入到就绪列表下标为2的链表中,如下:
在这里插入图片描述

四、实现调度器

调度器是操作系统的核心,其主要功能就是实现任务的切换,即从就绪列表里面找到优先级最高的任务,然后去执行该任务。

1、启动调度器

由函数vTaskStartScheduler()完成

void vTaskStartScheduler( void )
{
	 /* 手动指定第一个运行的任务 */
	 pxCurrentTCB = &Task1TCB; (1)
	 /* 启动调度器 */
	 if ( xPortStartScheduler() != pdFALSE )
	 {
	 /* 调度器启动成功,则不会返回,即不会来到这里 */ (2)
	 }
}

(1):pxCurrentTCB 是一个在 task.c 定义的全局指针,用于指向正在运行或即将运行的任务的任务控制指针。
(2):调用函数 xPortStartScheduler()启动调度器, 调度器启动成功, 则不会返回。
xPortStartScheduler()函数

/*
 * 参考资料《STM32F10xxx Cortex-M3 programming manual》 4.4.3,百度搜索“PM0056”即可找到这个文档
 * 在 Cortex-M 中,内核外设 SCB 中 SHPR3 寄存器用于设置 SysTick 和 PendSV 的异常优先级
 * System handler priority register 3 (SCB_SHPR3) SCB_SHPR3: 0xE000 ED20
 * Bits 31:24 PRI_15[7:0]: Priority of system handler 15, SysTick exception
 * Bits 23:16 PRI_14[7:0]: Priority of system handler 14, PendSV
 */
 #define portNVIC_SYSPRI2_REG (*(( volatile uint32_t *) 0xe000ed20))

 #define portNVIC_PENDSV_PRI (((uint32_t) configKERNEL_INTERRUPT_PRIORITY ) << 16UL)
 #define portNVIC_SYSTICK_PRI (((uint32_t) configKERNEL_INTERRUPT_PRIORITY ) << 24UL )

 BaseType_t xPortStartScheduler( void )
{
 /* 配置 PendSV 和 SysTick 的中断优先级为最低 */ (1)
 portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;
 portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;

 /* 启动第一个任务,不再返回 */
 prvStartFirstTask(); (2)

 /* 不应该运行到这里 */
 return 0;
}

(1):配置 PendSV 和 SysTick 的中断优先级为最低。 SysTick 和PendSV 都会涉及到系统调度,系统调度的优先级要低于系统的其它硬件中断优先级, 即优先响应系统中的外部硬件中断, 所以 SysTick 和 PendSV 的中断优先级配置为最低。
(2)调用函数 prvStartFirstTask()启动第一个任务, 启动成功后, 则不再返回, 该函数由汇编编写,此处不贴出。

2、任务切换

任务切换就是在就绪列表中寻找到优先级最高的就绪任务,然后去执行该任务。
2.1 TaskYIELD():

/* 在 task.h 中定义 */
#define taskYIELD() portYIELD()

/* 在 portmacro.h 中定义 */
/* 中断控制状态寄存器: 0xe000ed04
 * Bit 28 PENDSVSET: PendSV 悬起位
 */
#define portNVIC_INT_CTRL_REG (*(( volatile uint32_t *) 0xe000ed04))
 #define portNVIC_PENDSVSET_BIT ( 1UL << 28UL )

 #define portSY_FULL_READ_WRITE ( 15 )

 #define portYIELD() \
 { \
 /* 触发 PendSV,产生上下文切换 */ \
 portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; (1) \
 __dsb( portSY_FULL_READ_WRITE ); \
 __isb( portSY_FULL_READ_WRITE ); \
 }

(1):portYIELD 的实现实际就是将 PendSV 的悬起位置 1,当
没有其它中断运行的时候响应 PendSV 中断,去执行我们写好的 PendSV 中断服务函数,在里面实现任务切换。
2.2 xPortPendSVHandler()函数是PendSV 中断服务函数是真正实现任务切换的地方。
参考:[野火®]《FreeRTOS 内核实现与应用开发实战—基于STM32》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章