本节属于操作系统中基础中的基础,包括任务的创建与切换。以两个任务为例,在多任务系统中,两个变量波形完全一样,就好像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》