在裸机系统中,系统的主体就是
C
P
U
CPU
CPU按照预先设定的程序逻辑在
m
a
i
n
main
main函数里面顺序执行的无限循环。在多任务系统中,根据功能的不同,把整个系统分割成一个个独立的,无限循环且不能返回的的函数,这个函数我们称为任务。
在几乎所有的处理器架构中都会用到
S
T
A
C
K
,栈
STACK,栈
STACK,栈这种数据结构,它用来存储函数调用的参数,局部变量。当异常发生的时候它也可以用来存储处理器当前的状态和寄存器值。当发生函数调用的时候可以用来存储当前的原始数据,原始数据一般存储在系统的某些寄存器中,如果在函数调用之前不把它们存储在栈中的话,被调用的函数也会用到这些系统中的存储器,这样就会导致调用函数的某些原始数据会被覆盖而丢失,因而函数调用返回的时候也无法恢复到调用之前的环境。对于裸机系统,也可以被认为是只有一个任务的多任务系统,因此我们可以不用去关心栈数据结构,但是对于多任务系统,每个任务都是独立的,互不干扰的,因此每个任务都需要有独立的栈区域。在
F
r
e
e
R
T
O
S
FreeRTOS
FreeRTOS中每个任务的栈区域是一个预先定义好的全局数组,也可以是动态分配的一段内存空间。本章实现了两个简单的任务,因此需要定义两个任务栈。这里我们将会以
C
O
R
T
E
X
−
M
3
CORTEX-M3
CORTEX−M3芯片为例进行介绍,最后在实战的时候会基于
S
T
M
32
F
103
Z
E
T
6
STM32F103ZET6
STM32F103ZET6芯片的开发板进行实战演示。
任务栈的数组类型的定义位于
p
o
r
t
m
a
c
r
o
.
h
portmacro.h
portmacro.h文件中,如图1和图2所示。其实就是
u
i
n
t
32
_
t
uint32\_t
uint32_t类型。
在下面的代码中我们定义了两个任务 T a s k 1 _ E n t r y Task1\_Entry Task1_Entry和 T a s k 2 _ E n t r y Task2\_Entry Task2_Entry以及和它们对应的任务控制块(下面会讲到),任务句柄,任务栈。在裸机系统中,程序是按照预先设定的程序逻辑顺序执行的,而在多任务系统中,任务的执行是由系统调度的,为了实现任务的调度功能,每个任务都有一个和其息息相关的任务控制块 T C B , T a s k C o n t r o l B l o c k TCB,Task\quad Control\quad Block TCB,TaskControlBlock,这个任务控制块就相当于任务的身份证,系统对任务的全部操作都通过和它息息相关的任务控制块来实现。
/*
*****************************************************************************
Task control block and related stack memory definition
*****************************************************************************
*/
TaskHandle_t Task1_Handle;
#define TASK1_STACK_SIZE 20
StackType_t Task1Stack[TASK1_STACK_SIZE];
TCB_t Task1TCB;
TaskHandle_t Task2_Handle;
#define TASK2_STACK_SIZE 20
StackType_t Task2Stack[TASK2_STACK_SIZE];
TCB_t Task2TCB;
/*
*****************************************************************************
Task 1
*****************************************************************************
*/
void Task1_Entry( void *p_arg )
{
while(1)
{
flag1 = 1;
delay(100);
flag1 = 0;
delay(100);
/*Task switch*/
taskYIELD();
}
}
/*
*****************************************************************************
Task 2
*****************************************************************************
*/
void Task2_Entry( void *p_arg )
{
while(1)
{
flag2 = 1;
delay(100);
flag2 = 0;
delay(100);
/*Task switch*/
taskYIELD();
}
}
任务控制块的定义在 F r e e R T O S FreeRTOS FreeRTOS源码中的定义放在 t a s k . c task.c task.c文件中,但是在这里野火将它放到了 F r e e R T O S . h FreeRTOS.h FreeRTOS.h这个文件中。具体定义如下所示(相对于源码中的定义,这里做了大量精简)。其中元素 p x T o p O f S t a c k pxTopOfStack pxTopOfStack是栈顶指针,元素 x S t a t e L i s t I t e m xStateListItem xStateListItem表示当前任务的状态(如果这个链表节点挂载在就绪列表那就表明这个任务已经就绪,如果这个链表节点挂载在阻塞列表那就表明这个任务已经阻塞,如果这个链表节点挂载在暂停列表那就表明这个任务已经暂停,等等),元素 p c T a s k N a m e pcTaskName pcTaskName存储任务的名字,元素 p x S t a c k pxStack pxStack存储任务栈的起始地址。
/*
* Task control block. A task control block (TCB) is allocated for each task,
* and stores task state information, including a pointer to the task's context
* (the task's run time environment, including register values)
*/
typedef struct tskTaskControlBlock /* The old naming convention is used to prevent breaking kernel aware debuggers. */
{
volatile StackType_t * pxTopOfStack; /*< Points to the location of the last item placed on the tasks stack. THIS MUST BE THE FIRST MEMBER OF THE TCB STRUCT. */
ListItem_t xStateListItem; /*< The list that the state list item of a task is reference from denotes the state of that task (Ready, Blocked, Suspended ). */
StackType_t * pxStack; /*< Points to the start of the stack. */
char pcTaskName[ configMAX_TASK_NAME_LEN ]; /*< Descriptive name given to the task when created. Facilitates debugging only. */ /*lint !e971 Unqualified char types are allowed for strings and single characters only. */
} tskTCB;
/* The old tskTCB name is maintained above then typedefed to the new TCB_t name
* below to enable the use of older kernel aware debuggers. */
typedef tskTCB TCB_t;
任务栈,任务的函数实体以及任务的控制块经过前面的介绍我们已经有所了解,但是一个任务相关的这些元素必须链接起来之后才能融入整个系统,由系统调度执行。这个链接的工作是由任务创建接口实现的,野火这里使用的是 x T a s k C r e a t e S t a t i c xTaskCreateStatic xTaskCreateStatic(相对于源码中的定义,这里做了大量精简,并且源码中还有很多其它的任务创建接口)这个任务创建接口,它在 t a s k s . c tasks.c tasks.c这个文件中定义。如下所示。参数 p x T a s k C o d e pxTaskCode pxTaskCode对应任务的函数,参数 p c N a m e pcName pcName对应任务的名字,参数 u l S t a c k D e p t h ulStackDepth ulStackDepth对应任务栈的长度(因为这里的任务栈实际就是一个数组,因此这里实际就是数组的长度),参数 p v P a r a m e t e r s pvParameters pvParameters可以先不关心,参数 p v P a r a m e t e r s pvParameters pvParameters对于任务栈的起始地址,参数 p x T a s k B u f f e r pxTaskBuffer pxTaskBuffer对应任务控制块。这个接口的操作比较简单,它将任务控制块和对用的任务栈链接起来之后就调用了接口 p r v I n i t i a l i s e N e w T a s k prvInitialiseNewTask prvInitialiseNewTask。
TaskHandle_t xTaskCreateStatic( TaskFunction_t pxTaskCode,
const char * const pcName, /*lint !e971 Unqualified char types are allowed for strings and single characters only. */
const uint32_t ulStackDepth,
void * const pvParameters,
StackType_t * const puxStackBuffer,
TCB_t * const pxTaskBuffer )
{
TCB_t * pxNewTCB;
TaskHandle_t xReturn;
if( ( pxTaskBuffer != NULL ) && ( puxStackBuffer != NULL ) )
{
/* The memory used for the task's TCB and stack are passed into this
* function - use them. */
pxNewTCB = ( TCB_t * ) pxTaskBuffer; /*lint !e740 !e9087 Unusual cast is ok as the structures are designed to have the same alignment, and the size is checked by an assert. */
pxNewTCB->pxStack = ( StackType_t * ) puxStackBuffer;
prvInitialiseNewTask( pxTaskCode, pcName, ulStackDepth, pvParameters, &xReturn, pxNewTCB);
}
else
{
xReturn = NULL;
}
return xReturn;
}
接口 p r v I n i t i a l i s e N e w T a s k prvInitialiseNewTask prvInitialiseNewTask(相对于源码中的定义,这里做了大量精简)也在 t a s k s . c tasks.c tasks.c这个文件中定义,它主要完成对任务控制块中所有未初始化的元素的初始化。如下所示。参数 p x T a s k C o d e pxTaskCode pxTaskCode对应任务的函数,参数 p c N a m e pcName pcName对应任务的名字,参数 u l S t a c k D e p t h ulStackDepth ulStackDepth对应任务栈的长度(因为这里的任务栈实际就是一个数组,因此这里实际就是数组的长度),参数 p v P a r a m e t e r s pvParameters pvParameters可以先不关心,参数 p v P a r a m e t e r s pvParameters pvParameters对于任务栈的起始地址,参数 p x C r e a t e d T a s k pxCreatedTask pxCreatedTask用来返回已经完成初始化的任务控制块,参数 p x N e w T C B pxNewTCB pxNewTCB对应任务控制块。这个接口首先计算了任务栈的栈顶地址并对栈顶地址做了8字节对齐(处理器架构需要,有需要了解的可以再详细查询资料),然后接着对任务控制块里面的任务名字进行了初始化,接着对任务控制块里面的任务状态节点进行了初始化,最后调用接口 p x P o r t I n i t i a l i s e S t a c k pxPortInitialiseStack pxPortInitialiseStack对任务栈进行了初始化并最终初始化了任务控制块的栈顶指针元素。
static void prvInitialiseNewTask( TaskFunction_t pxTaskCode,
const char * const pcName, /*lint !e971 Unqualified char types are allowed for strings and single characters only. */
const uint32_t ulStackDepth,
void * const pvParameters,
TaskHandle_t * const pxCreatedTask,
TCB_t * pxNewTCB)
{
StackType_t * pxTopOfStack;
UBaseType_t x;
/* Calculate the top of stack address. */
pxTopOfStack = &( pxNewTCB->pxStack[ ulStackDepth - ( uint32_t ) 1 ] );
pxTopOfStack = ( StackType_t * ) ( ( ( uint32_t ) pxTopOfStack ) & ( ~( ( uint32_t ) 0x0007 ) ) );
/* Store the task name in the TCB. */
if( pcName != NULL )
{
for( x = ( UBaseType_t ) 0; x < ( UBaseType_t ) configMAX_TASK_NAME_LEN; x++ )
{
pxNewTCB->pcTaskName[ x ] = pcName[ x ];
/* Don't copy all configMAX_TASK_NAME_LEN if the string is shorter than
* configMAX_TASK_NAME_LEN characters just in case the memory after the
* string is not accessible (extremely unlikely). */
if( pcName[ x ] == ( char ) 0x00 )
{
break;
}
}
/* Ensure the name string is terminated in the case that the string length
* was greater or equal to configMAX_TASK_NAME_LEN. */
pxNewTCB->pcTaskName[ configMAX_TASK_NAME_LEN - 1 ] = '\0';
}
else
{
while(1);
}
vListInitialiseItem( &( pxNewTCB->xStateListItem ) );
/* Set the pxNewTCB as a link back from the ListItem_t. This is so we can get
* back to the containing TCB from a generic item in a list. */
listSET_LIST_ITEM_OWNER( &( pxNewTCB->xStateListItem ), pxNewTCB );
pxNewTCB->pxTopOfStack = pxPortInitialiseStack( pxTopOfStack, pxTaskCode, pvParameters );
if( pxCreatedTask != NULL )
{
/* Pass the handle out in an anonymous way. The handle can be used to
* change the created task's priority, delete the created task, etc.*/
*pxCreatedTask = ( TaskHandle_t ) pxNewTCB;
}
else
{
while(1);
}
}
接口 p x P o r t I n i t i a l i s e S t a c k pxPortInitialiseStack pxPortInitialiseStack在 p o r t . c port.c port.c(对特定的架构来说是特定的,这个文件的位置在 F r e e R T O S FreeRTOS FreeRTOS源码的位置如图3所示。对这一块不熟悉的在阅读接下来的内容之前建议先阅读 《 T h e D e f i n i t i v e G u i d e t o A R M ® C o r t e x ® − M 3 a n d C o r t e x ® − M 4 P r o c e s s o r s 》 《The\quad Definitive\quad Guide\quad to\quad ARM^{®}\quad Cortex^{®}-M3\quad and\quad Cortex^{®}-M4\quad Processors》 《TheDefinitiveGuidetoARMR◯CortexR◯−M3andCortexR◯−M4Processors》的第8章)这个文件中定义。如下所示。参数 p x T o p O f S t a c k pxTopOfStack pxTopOfStack指向当前的任务栈的栈顶,参数 p x C o d e pxCode pxCode指向当前得到任务,参数 p v P a r a m e t e r s pvParameters pvParameters可以先不关心。该接口首先初始化了任务栈的高8个字(这高8个字对应内核中的寄存器如图4中的红色字体所示), x P S R xPSR xPSR初始化为 0 x 01000000 0x01000000 0x01000000,至于这个寄存器的第24位为什么要初始化为1请看我前面提到的 《 T h e D e f i n i t i v e G u i d e t o A R M ® C o r t e x ® − M 3 a n d C o r t e x ® − M 4 P r o c e s s o r s 》 《The\quad Definitive\quad Guide\quad to\quad ARM^{®}\quad Cortex^{®}-M3\quad and\quad Cortex^{®}-M4\quad Processors》 《TheDefinitiveGuidetoARMR◯CortexR◯−M3andCortexR◯−M4Processors》。 R e t u r n A d d r e s s ( R 15 , P C ) Return\ Address(R15,PC) Return Address(R15,PC)初始化为任务对应的函数的指针(在后面的讲解中我们将会看到,系统开始之后第一个任务的运行以及任务的切换都用到了从 S V C _ H a n d l e r SVC\_Handler SVC_Handler或 P e n d S V _ H a n d l e r PendSV\_Handler PendSV_Handler返回,这样从异常中断返回之后 P C PC PC指针就指向了任务对应的函数的地址,因此也就开始了任务的运行), R 14 ( L R ) R14(LR) R14(LR)初始化为一个运行之后就进入死循环的函数的指针,这是因为任务对应的函数都是无限循环的函数,是不可能返回的,如果有返回,那说明出错了。其它的都初始化为0了,在系统中这8个寄存器的进栈和出栈一般都是由 C P U CPU CPU自动去操作的不用我们软件干预。该接口接着初始化了任务栈的低8个字(这低8个字对应内核中的寄存器如图4中的蓝色字体所示),这些寄存器的值全部初始化为0,在系统中这8个寄存器的进栈和出栈一般都需要软件自己操作。到此为止一个任务就算是创建成功了。
StackType_t * pxPortInitialiseStack( StackType_t * pxTopOfStack,
TaskFunction_t pxCode,
void * pvParameters )
{
/* Simulate the stack frame as it would be created by a context switch
* interrupt. */
pxTopOfStack--; /* Offset added to account for the way the MCU uses the stack on entry/exit of interrupts. */
*pxTopOfStack = portINITIAL_XPSR; /* xPSR */
pxTopOfStack--;
*pxTopOfStack = ( ( StackType_t ) pxCode ) & portSTART_ADDRESS_MASK; /* PC */
pxTopOfStack--;
*pxTopOfStack = ( StackType_t ) prvTaskExitError; /* LR */
/*------------------------------------------------------------*/
/***************************Boundary Line*********************/
/*------------------------------------------------------------*/
pxTopOfStack -= 5; /* R12, R3, R2 and R1. */
*pxTopOfStack = ( StackType_t ) pvParameters; /* R0 */
pxTopOfStack -= 8; /* R11, R10, R9, R8, R7, R6, R5 and R4. */
return pxTopOfStack;
}
前面我们提到过任务控制块里面的元素 x S t a t e L i s t I t e m xStateListItem xStateListItem表示当前任务的状态(如果这个链表节点挂载在就绪列表那就表明这个任务已经就绪,如果这个链表节点挂载在阻塞列表那就表明这个任务已经阻塞,如果这个链表节点挂载在暂停列表那就表明这个任务已经暂停,等等),只有处于就绪状态的任务才能被系统调度执行吗,因此只有当任务的任务控制块里面的元素 x S t a t e L i s t I t e m xStateListItem xStateListItem处于就绪列表的时候,任务才有可能被系统调度执行。就绪列表其实就是在跟着野火学FreeRTOS:第一段(基础介绍)提到的链表根节点的数组,数组的每一个元素都是一个链表的根节点,那也就是数组的每一个节点都代表着一个链表,挂在同一个链表的任务的优先级是一样的,这个数组的索引代表着任务的优先级,索引越小优先级越低,因此数组索引为0的链表上挂载的任务的优先级最低(但是野火在这一章节没有实现优先级,优先级的实现会在会面的章节讲到)。就绪列表的初始化接口 p r v I n i t i a l i s e T a s k L i s t s prvInitialiseTaskLists prvInitialiseTaskLists(相对于源码中的定义,这里做了大量精简)也在 t a s k s . c tasks.c tasks.c这个文件中定义,如下所示。这个接口比较简单只是对数组中的每一个链表根节点调用跟着野火学FreeRTOS:第一段(基础介绍)提到的初始化链表根节点的接口 v L i s t I n i t i a l i s e vListInitialise vListInitialise进行初始化操作,就绪列表初始化完成之后如图5所示。
/*
* Utility to ready all the lists used by the scheduler. This is called
* automatically upon the creation of the first task.
*/
void prvInitialiseTaskLists( void )
{
UBaseType_t uxPriority;
for( uxPriority = ( UBaseType_t ) 0U; uxPriority < ( UBaseType_t ) configMAX_PRIORITIES; uxPriority++ )
{
vListInitialise( &( pxReadyTasksLists[ uxPriority ] ) );
}
}
任务就绪列表初始化完成之后,野火这里的操作是调用跟着野火学FreeRTOS:第一段(基础介绍)提到的将一个新的链表节点插入到链表尾部的接口 v L i s t I n s e r t E n d vListInsertEnd vListInsertEnd来实现的,但是 F r e e R T O S FreeRTOS FreeRTOS的官方源码不是这样操作的,感兴趣的可以自行了解一下。因为这里目前只定义了两个任务且没有实现优先级,所以就加单的将任务1挂载到索引为1的数组对应的链表里面,将任务2挂载到索引为2的数组对应的链表里面,这里实际上是将任务的任务控制块里面的元素 x S t a t e L i s t I t e m xStateListItem xStateListItem插入到相应的链表里面,任务插入到就绪列表之后的状态如图6所示。
任务已经建立并初始化完了,也被添加到了就绪列表中了,接下来就接着讲一下任务的调度以及相关的任务切换。任务的调度在实战应用中一般是优先级高的任务最先被调度,任务的切换也是自动的,但是因为这里基于讲解的需要暂时没有实现优先级且任务的调度和切换都是手动执行的。这一部分内容还是比较重要和关键的,我建议大家在阅读之前先把 《 T h e D e f i n i t i v e G u i d e t o A R M ® C o r t e x ® − M 3 a n d C o r t e x ® − M 4 P r o c e s s o r s 》 《The\quad Definitive\quad Guide\quad to\quad ARM^{®}\quad Cortex^{®}-M3\quad and\quad Cortex^{®}-M4\quad Processors》 《TheDefinitiveGuidetoARMR◯CortexR◯−M3andCortexR◯−M4Processors》的第10章阅读一遍,这样一来的话,接下来的内容将会比较好理解,图7的内容就是第10章内容的片段,这段内容基本上总结了接下来要讲的内容所进行的操作。
接口
v
T
a
s
k
S
t
a
r
t
S
c
h
e
d
u
l
e
r
vTaskStartScheduler
vTaskStartScheduler(相对于源码中的定义,这里做了大量精简)也在
t
a
s
k
s
.
c
tasks.c
tasks.c这个文件中定义,它用来启动调度器。如下所示。它只是简单的将全局变量
p
x
C
u
r
r
e
n
t
T
C
B
pxCurrentTCB
pxCurrentTCB(这个变量指向当前正
在运行或者即将要运行的任务的任务控制块)的值赋值为第一个即将运行的任务(这里选择
T
a
s
k
1
Task1
Task1)的任务控制块的地址,然后调用接口
x
P
o
r
t
S
t
a
r
t
S
c
h
e
d
u
l
e
r
xPortStartScheduler
xPortStartScheduler启动调度器。
void vTaskStartScheduler( void )
{
pxCurrentTCB = &Task1TCB;
if( xPortStartScheduler() != pdFALSE )
{
}
}
接口 x P o r t S t a r t S c h e d u l e r xPortStartScheduler xPortStartScheduler(相对于源码中的定义,这里做了大量精简)在 p o r t . c port.c port.c这个文件中定义,它用来启动调度器。如下所示。它先将 P e n d S V PendSV PendSV和 S y s T i c k SysTick SysTick这两个系统的中断优先级设置为最低(这里将优先级设置为最低的原因是上下文的切换是放到 P e n d S V PendSV PendSV中断中的,因为优先级最低因此这样其它中断不会打断上下文切换操作),然后调用接口 p r v S t a r t F i r s t T a s k prvStartFirstTask prvStartFirstTask开始执行第一个任务 T a s k 1 Task1 Task1。
BaseType_t xPortStartScheduler( void )
{
/* Make PendSV and SysTick the lowest priority interrupts. */
portNVIC_SHPR3_REG |= portNVIC_PENDSV_PRI;
portNVIC_SHPR3_REG |= portNVIC_SYSTICK_PRI;
/* Start the first task. */
prvStartFirstTask();
/* Should not get here! */
return 0;
}
接口 p r v S t a r t F i r s t T a s k prvStartFirstTask prvStartFirstTask在 p o r t . c port.c port.c这个文件中定义,它用来执行第一个任务,这里用汇编编写。如下所示。它的主要操作是使能了中断,然后使用 S V C SVC SVC指令触发调用 S V C a l l SVCall SVCall这个系统中断去执行第一个任务。
__asm void prvStartFirstTask( void )
{
/* *INDENT-OFF* */
PRESERVE8
/* Use the NVIC offset register to locate the stack. */
ldr r0, =0xE000ED08
ldr r0, [ r0 ]
ldr r0, [ r0 ]
/* Set the msp back to the start of the stack. */
msr msp, r0
/* Globally enable interrupts. */
cpsie i
cpsie f
dsb
isb
/* Call SVC to start the first task. */
svc 0
nop
nop
/* *INDENT-ON* */
}
S V C a l l SVCall SVCall这个系统中断函数定义如下,使用汇编编写。中断中首先获取任务1的任务控制块的地址,然后获取任务控制块的第一个元素,我们知道任务控制块的第一个元素就是任务栈的栈顶指针,任务初始化完成之后这个指针指向的位置如图4所示,然后从图4所示堆栈指针指向的位置将任务1的任务栈中的蓝色字体的内容出栈(也就是任务1初始化时寄存器 R 4 R4 R4, R 5 R5 R5, R 6 R6 R6, R 7 R7 R7, R 8 R8 R8, R 9 R9 R9, R 10 R10 R10, R 11 R11 R11的初始值)并重新赋值给这些寄存器,操作完成之后堆栈指针此时应该指向图4中红色字体的 R 0 R0 R0所在的位置,此时将 P r o c e s s o r S t a c k P o i n t e r , P S P Processor\quad Stack\quad Pointer,PSP ProcessorStackPointer,PSP(在 C o r t e x − M Cortex-M Cortex−M系列的处理器中有两个堆栈指针, P r o c e s s o r S t a c k P o i n t e r , P S P Processor\quad Stack\quad Pointer,PSP ProcessorStackPointer,PSP和 M a i n S t a c k P o i n t e r , M S P Main\quad Stack\quad Pointer,MSP MainStackPointer,MSP,详细请参考 《 T h e D e f i n i t i v e G u i d e t o A R M ® C o r t e x ® − M 3 a n d C o r t e x ® − M 4 P r o c e s s o r s 》 《The\quad Definitive\quad Guide\quad to\quad ARM^{®}\quad Cortex^{®}-M3\quad and\quad Cortex^{®}-M4\quad Processors》 《TheDefinitiveGuidetoARMR◯CortexR◯−M3andCortexR◯−M4Processors》。在有实时操作系统的环境中,任务使用 P r o c e s s o r S t a c k P o i n t e r , P S P Processor\quad Stack\quad Pointer,PSP ProcessorStackPointer,PSP)设置为也指向图4中红色字体的 R 0 R0 R0所在的位置。最后将 R 14 ( L R ) R14(LR) R14(LR)寄存器的值设置为0xFFFFFFD并触发异常返回将任务1的任务栈中的红色字体的内容出栈(也就是任务1初始化时寄存器 R 0 R0 R0, R 1 R1 R1, R 2 R2 R2, R 3 R3 R3, R 12 R12 R12, R 14 R14 R14, R 15 R15 R15(对应任务1对应的任务函数的指针), x P S R xPSR xPSR的初始值)并重新赋值给这些寄存器,此时最关键的是出栈完成之后 R 15 , P C , P r o g r a m C o u n t e r R15,PC,Program\quad Counter R15,PC,ProgramCounter值为任务1对应的任务函数的指针,那就相当于此时就开始运行任务1了且此时使用的栈是任务一的任务栈(由前面设置的 R 14 ( L R ) R14(LR) R14(LR)寄存器的值 0 x F F F F F F D 0xFFFFFFD 0xFFFFFFD决定)。
__asm void vPortSVCHandler( void )
{
/* *INDENT-OFF* */
PRESERVE8
ldr r3, = pxCurrentTCB /* Restore the context. */
ldr r1, [ r3 ] /* Use pxCurrentTCBConst to get the pxCurrentTCB address. */
ldr r0, [ r1 ] /* The first item in pxCurrentTCB is the task top of stack. */
ldmia r0 !, { r4 - r11 } /* Pop the registers that are not automatically saved on exception entry and the critical nesting count. */
msr psp, r0 /* Restore the task stack pointer. */
isb
mov r0, # 0
msr basepri, r0
orr r14, # 0xd
bx r14
/* *INDENT-ON* */
}
/*-----------------------------------------------------------*/
野火这里为了内容讲解从易到难的循序渐进,因此没有实现优先级而且任务的切换(自动切换后面的章节会讲解和实现)也是手动完成的,并不是自动完成的。从前面定义的任务的函数看一个任务每执行一轮就会调用任务切换的接口 t a s k Y I E L D taskYIELD taskYIELD,也就是 p o r t Y I E L D portYIELD portYIELD,如下所示。这个接口也没有做太多的动作,只是通过将 S y s t e m C o n t r o l B l o c k System\quad Control\quad Block SystemControlBlock模块的 I C S R ICSR ICSR寄存器的相应位置1来触发进入 P e n d S V PendSV PendSV中断来进行任务的切换。
#define portYIELD() \
{ \
/* Set a PendSV to request a context switch. */ \
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; \
\
/* Barriers are normally not required but do ensure the code is completely \
* within the specified behaviour for the architecture. */ \
__dsb( portSY_FULL_READ_WRITE ); \
__isb( portSY_FULL_READ_WRITE ); \
}
P e n d S V PendSV PendSV中断函数的定义如下(用汇编编写),当进入 P e n d S V PendSV PendSV中断之后,系统会自动的将图7中红色字体所示的当前正在执行的任务的相应寄存器的值内容压入当前正在执行的任务的任务栈中,此时当前任务的 P r o c e s s o r S t a c k P o i n t e r , P S P Processor\quad Stack\quad Pointer,PSP ProcessorStackPointer,PSP指向的位置如图7所示。然后再手动的将 R 0 R0 R0, R 1 R1 R1, R 2 R2 R2, R 3 R3 R3, R 12 R12 R12, R 14 R14 R14, R 15 R15 R15等寄存器的内容压入当前正在执行的任务的任务栈中,且将当前执行的任务的任务控制块的 p x T o p O f S t a c k pxTopOfStack pxTopOfStack(也就是堆栈指针)元素指向的位置设置为图4的位置。任务的切换伴随着上下文的切换,上文就是当前正在执行的任务的一些环境变量,就是图4或图7中那些红色和蓝色字体的寄存器的值,那此时上文的操作(保存当前正在执行的任务的环境变量到对应的任务栈中)算是完成了。下文的操作就是将下一个将要执行的任务的环境变量恢复到系统的图4或图7中那些红色和蓝色字体对应的寄存器中。在进行下文的操作之前这里进行了一个关中断的操作,这是为了突发的中断会影响到下文的操作(这一点我目前也不是太清楚)。接下来就是调用接口 v T a s k S w i t c h C o n t e x t vTaskSwitchContext vTaskSwitchContext将全局变量 p x C u r r e n t T C B pxCurrentTCB pxCurrentTCB(这个变量指向当前正在运行或者即将要运行的任务的任务控制块)的值赋值为下一个任务的任务控制块的指针并重新开启了中断。这时即将执行的任务的任务控制块里面的 p x T o p O f S t a c k pxTopOfStack pxTopOfStack(也就是堆栈指针)元素应该指向图4所示的位置。接着将即将执行的任务的任务栈中的蓝色字体的内容出栈(也就是任务初始化时寄存器 R 4 R4 R4, R 5 R5 R5, R 6 R6 R6, R 7 R7 R7, R 8 R8 R8, R 9 R9 R9, R 10 R10 R10, R 11 R11 R11的初始值或者任务切换时保存入栈的值)并重新赋值给这些寄存器,操作完成之后将 P r o c e s s o r S t a c k P o i n t e r , P S P Processor\quad Stack\quad Pointer,PSP ProcessorStackPointer,PSP指向图7的位置。这时 R 14 ( L R ) R14(LR) R14(LR)寄存器的值应该和进入 P e n d S V PendSV PendSV中断函数时的值一样,应该是 0 x F F F F F F D 0xFFFFFFD 0xFFFFFFD,利用该值触发异常返回就会将即将执行的任务的任务栈中红色字体的内容出栈(也就是即将执行的任务初始化时或任务切换时保存的寄存器 R 0 R0 R0, R 1 R1 R1, R 2 R2 R2, R 3 R3 R3, R 12 R12 R12, R 14 R14 R14, R 15 R15 R15(对应即将执行的任务的任务函数的指针), x P S R xPSR xPSR的初始值)并重新赋值给这些寄存器,此时最关键的是出栈完成之后 R 15 , P C , P r o g r a m C o u n t e r R15,PC,Program\quad Counter R15,PC,ProgramCounter值为即将执行的任务的任务函数的指针,那就相当于此时就开始运行即将执行的任务了且此时使用的栈是任务一的任务栈(由 R 14 ( L R ) R14(LR) R14(LR)寄存器的值 0 x F F F F F F D 0xFFFFFFD 0xFFFFFFD决定)。那此时任务切换就完成了,也开始了新的任务的执行。
__asm void xPortPendSVHandler( void )
{
extern pxCurrentTCB;
extern vTaskSwitchContext;
/* *INDENT-OFF* */
PRESERVE8
mrs r0, psp
isb
ldr r3, =pxCurrentTCB /* Get the location of the current TCB. */
ldr r2, [ r3 ]
stmdb r0 !, { r4 - r11 } /* Save the remaining registers. */
str r0, [ r2 ] /* Save the new top of stack into the first member of the TCB. */
stmdb sp !, { r3, r14 }
mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY
msr basepri, r0
dsb
isb
bl vTaskSwitchContext
mov r0, #0
msr basepri, r0
ldmia sp !, { r3, r14 }
ldr r1, [ r3 ]
ldr r0, [ r1 ] /* The first item in pxCurrentTCB is the task top of stack. */
ldmia r0 !, { r4 - r11 } /* Pop the registers and the critical nesting count. */
msr psp, r0
isb
bx r14
nop
/* *INDENT-ON* */
}
void vTaskSwitchContext( void )
{
if( pxCurrentTCB == &Task1TCB )
{
pxCurrentTCB = &Task2TCB;
}
else
{
pxCurrentTCB = &Task1TCB;
}
}
到这里为止关键的代码都已经介绍完了,下面来实际运行并仿真看看,前面说过这里的示例不依赖于任何的硬件板子,而是采用 K E I L − M D K KEIL-MDK KEIL−MDK自带的软件模拟仿真,在配置的时候勾选图7中绿色的位置就可以了。 m a i n main main函数如下所示。
int main(void)
{
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 ) );
vTaskStartScheduler();
while(1);
}
进入 D E B U G DEBUG DEBUG模式之后,将 K E I L − M D K KEIL-MDK KEIL−MDK自带的 L o g i c A n a l y z e r Logic\quad Analyzer LogicAnalyzer调出来,如图9所示。然后将 f l a g 1 flag1 flag1和 f l a g 2 flag2 flag2这两个全局变量添加进 L o g i c A n a l y z e r Logic\quad Analyzer LogicAnalyzer,如图10所示,并设置 f l a g 1 flag1 flag1和 f l a g 2 flag2 flag2这两个全局变量的显示属性为 B i t Bit Bit,如图11所示。然后全速运行我们就可以看到图12所示的现象,这也是符合预期的。
对于临界段的话,我这里没有太多要讲的,主要可以看一下野火的讲解还有就是参考一下 《 T h e D e f i n i t i v e G u i d e t o A R M ® C o r t e x ® − M 3 a n d C o r t e x ® − M 4 P r o c e s s o r s 》 《The\quad Definitive\quad Guide\quad to\quad ARM^{®}\quad Cortex^{®}-M3\quad and\quad Cortex^{®}-M4\quad Processors》 《TheDefinitiveGuidetoARMR◯CortexR◯−M3andCortexR◯−M4Processors》。所谓的临界段可以简单的理解为是一段不能被打断的代码,比如系统调度和上下文切换,如果被打断的话可能会导致当前环境中的变量(例如前面提到的栈中的那些需要入栈保存或出栈恢复的寄存器)的值改变,从而导致系统调度或上下文切换发生异常或错误。前面第一个任务的执行和任务的切换都是在 P e n d S V PendSV PendSV中断和 S V C a l l SVCall SVCall中断中进行的,那有可能出现打断的情况是在这两个中断执行期间有了更高优先级别的中断到来了,转而去执行其它中断。这里的办法是在 P e n d S V PendSV PendSV中断和 S V C a l l SVCall SVCall中断中进行相应临界段的操作的时候先关闭其它中断的响应,等相应临界段的操作完成之后再打开中断响应以此来杜绝其它中断对临界段代码的干扰。这一部分的工程代码以及M3&M4 Guide在这里。