跟着野火学FreeRTOS:第一段(任务定义,切换以及临界段)

2023-12-21

在裸机系统中,系统的主体就是 C P U CPU CP U 按照预先设定的程序逻辑在 m a i n main main 函数里面顺序执行的无限循环。在多任务系统中,根据功能的不同,把整个系统分割成一个个 独立的,无限循环且不能返回的 的函数,这个函数我们称为任务。
在几乎所有的处理器架构中都会用到 S T A C K ,栈 STACK,栈 ST A C K ,栈 这种数据结构,它用来存储函数调用的参数,局部变量。当异常发生的时候它也可以用来存储处理器当前的状态和寄存器值。当发生函数调用的时候可以用来存储当前的原始数据,原始数据一般存储在系统的某些寄存器中,如果在函数调用之前不把它们存储在栈中的话,被调用的函数也会用到这些系统中的存储器,这样就会导致调用函数的某些原始数据会被覆盖而丢失,因而函数调用返回的时候也无法恢复到调用之前的环境。对于裸机系统,也可以被认为是只有一个任务的多任务系统,因此我们可以不用去关心栈数据结构,但是对于多任务系统,每个任务都是独立的,互不干扰的,因此每个任务都需要有独立的栈区域。在 F r e e R T O S FreeRTOS F ree RTOS 中每个任务的栈区域是一个预先定义好的全局数组,也可以是动态分配的一段内存空间。本章实现了两个简单的任务,因此需要定义两个任务栈。这里我们将会以 C O R T E X − M 3 CORTEX-M3 CORTEX M 3 芯片为例进行介绍,最后在实战的时候会基于 S T M 32 F 103 Z E T 6 STM32F103ZET6 STM 32 F 103 ZET 6 芯片的开发板进行实战演示。
任务栈的数组类型的定义位于 p o r t m a c r o . h portmacro.h p or t ma cro . h 文件中,如图1和图2所示。其实就是 u i n t 32 _ t uint32\_t u in t 32_ t 类型。

图1.
图2.

在下面的代码中我们定义了两个任务 T a s k 1 _ E n t r y Task1\_Entry T a s k 1_ E n t ry T a s k 2 _ E n t r y Task2\_Entry T a s k 2_ E n t ry 以及和它们对应的任务控制块(下面会讲到),任务句柄,任务栈。在裸机系统中,程序是按照预先设定的程序逻辑顺序执行的,而在多任务系统中,任务的执行是由系统调度的,为了实现任务的调度功能,每个任务都有一个和其息息相关的任务控制块 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 , T a s k C o n t ro l Bl oc k ,这个任务控制块就相当于任务的身份证,系统对任务的全部操作都通过和它息息相关的任务控制块来实现。

/*
*****************************************************************************
          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 F ree RTOS 源码中的定义放在 t a s k . c task.c t a s k . c 文件中,但是在这里野火将它放到了 F r e e R T O S . h FreeRTOS.h F ree RTOS . h 这个文件中。具体定义如下所示(相对于源码中的定义,这里做了大量精简)。其中元素 p x T o p O f S t a c k pxTopOfStack p x T o pO f St a c k 是栈顶指针,元素 x S t a t e L i s t I t e m xStateListItem x St a t e L i s t I t e m 表示当前任务的状态(如果这个链表节点挂载在就绪列表那就表明这个任务已经就绪,如果这个链表节点挂载在阻塞列表那就表明这个任务已经阻塞,如果这个链表节点挂载在暂停列表那就表明这个任务已经暂停,等等),元素 p c T a s k N a m e pcTaskName p c T a s k N am e 存储任务的名字,元素 p x S t a c k pxStack p x St a c k 存储任务栈的起始地址。

/*
 * 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 x T a s k C re a t e St a t i c (相对于源码中的定义,这里做了大量精简,并且源码中还有很多其它的任务创建接口)这个任务创建接口,它在 t a s k s . c tasks.c t a s k s . c 这个文件中定义。如下所示。参数 p x T a s k C o d e pxTaskCode p x T a s k C o d e 对应任务的函数,参数 p c N a m e pcName p c N am e 对应任务的名字,参数 u l S t a c k D e p t h ulStackDepth u lSt a c k De pt h 对应任务栈的长度(因为这里的任务栈实际就是一个数组,因此这里实际就是数组的长度),参数 p v P a r a m e t e r s pvParameters p v P a r am e t ers 可以先不关心,参数 p v P a r a m e t e r s pvParameters p v P a r am e t ers 对于任务栈的起始地址,参数 p x T a s k B u f f e r pxTaskBuffer p x T a s k B u ff er 对应任务控制块。这个接口的操作比较简单,它将任务控制块和对用的任务栈链接起来之后就调用了接口 p r v I n i t i a l i s e N e w T a s k prvInitialiseNewTask p r v I ni t ia l i se N e wT a s k

    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 p r v I ni t ia l i se N e wT a s k (相对于源码中的定义,这里做了大量精简)也在 t a s k s . c tasks.c t a s k s . c 这个文件中定义,它主要完成对任务控制块中所有未初始化的元素的初始化。如下所示。参数 p x T a s k C o d e pxTaskCode p x T a s k C o d e 对应任务的函数,参数 p c N a m e pcName p c N am e 对应任务的名字,参数 u l S t a c k D e p t h ulStackDepth u lSt a c k De pt h 对应任务栈的长度(因为这里的任务栈实际就是一个数组,因此这里实际就是数组的长度),参数 p v P a r a m e t e r s pvParameters p v P a r am e t ers 可以先不关心,参数 p v P a r a m e t e r s pvParameters p v P a r am e t ers 对于任务栈的起始地址,参数 p x C r e a t e d T a s k pxCreatedTask p x C re a t e d T a s k 用来返回已经完成初始化的任务控制块,参数 p x N e w T C B pxNewTCB p x N e wTCB 对应任务控制块。这个接口首先计算了任务栈的栈顶地址并对栈顶地址做了8字节对齐(处理器架构需要,有需要了解的可以再详细查询资料),然后接着对任务控制块里面的任务名字进行了初始化,接着对任务控制块里面的任务状态节点进行了初始化,最后调用接口 p x P o r t I n i t i a l i s e S t a c k pxPortInitialiseStack p x P or t I ni t ia l i se St a c k 对任务栈进行了初始化并最终初始化了任务控制块的栈顶指针元素。

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 p x P or t I ni t ia l i se St a c k p o r t . c port.c p or t . c (对特定的架构来说是特定的,这个文件的位置在 F r e e R T O S FreeRTOS F ree RTOS 源码的位置如图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》 T h e De f ini t i v e G u i d e t o A R M R C or t e x R M 3 an d C or t e x R M 4 P rocessors 的第8章)这个文件中定义。如下所示。参数 p x T o p O f S t a c k pxTopOfStack p x T o pO f St a c k 指向当前的任务栈的栈顶,参数 p x C o d e pxCode p x C o d e 指向当前得到任务,参数 p v P a r a m e t e r s pvParameters p v P a r am e t ers 可以先不关心。该接口首先初始化了任务栈的高8个字(这高8个字对应内核中的寄存器如图4中的红色字体所示), x P S R xPSR x PSR 初始化为 0 x 01000000 0x01000000 0 x 01000000 ,至于这个寄存器的第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》 T h e De f ini t i v e G u i d e t o A R M R C or t e x R M 3 an d C or t e x R M 4 P rocessors R e t u r n   A d d r e s s ( R 15 , P C ) Return\ Address(R15,PC) R e t u r n A dd ress ( R 15 , PC ) 初始化为任务对应的函数的指针(在后面的讲解中我们将会看到,系统开始之后第一个任务的运行以及任务的切换都用到了从 S V C _ H a n d l e r SVC\_Handler S V C _ H an d l er P e n d S V _ H a n d l e r PendSV\_Handler P e n d S V _ H an d l er 返回,这样从异常中断返回之后 P C PC PC 指针就指向了任务对应的函数的地址,因此也就开始了任务的运行), R 14 ( L R ) R14(LR) R 14 ( L R ) 初始化为一个运行之后就进入死循环的函数的指针,这是因为任务对应的函数都是无限循环的函数,是不可能返回的,如果有返回,那说明出错了。其它的都初始化为0了,在系统中这8个寄存器的进栈和出栈一般都是由 C P U CPU CP U 自动去操作的不用我们软件干预。该接口接着初始化了任务栈的低8个字(这低8个字对应内核中的寄存器如图4中的蓝色字体所示),这些寄存器的值全部初始化为0,在系统中这8个寄存器的进栈和出栈一般都需要软件自己操作。到此为止一个任务就算是创建成功了。

图3.
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;
}
图4.

前面我们提到过任务控制块里面的元素 x S t a t e L i s t I t e m xStateListItem x St a t e L i s t I t e m 表示当前任务的状态(如果这个链表节点挂载在就绪列表那就表明这个任务已经就绪,如果这个链表节点挂载在阻塞列表那就表明这个任务已经阻塞,如果这个链表节点挂载在暂停列表那就表明这个任务已经暂停,等等),只有处于就绪状态的任务才能被系统调度执行吗,因此只有当任务的任务控制块里面的元素 x S t a t e L i s t I t e m xStateListItem x St a t e L i s t I t e m 处于就绪列表的时候,任务才有可能被系统调度执行。就绪列表其实就是在 跟着野火学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 p r v I ni t ia l i se T a s k L i s t s (相对于源码中的定义,这里做了大量精简)也在 t a s k s . c tasks.c t a s k s . c 这个文件中定义,如下所示。这个接口比较简单只是对数组中的每一个链表根节点调用 跟着野火学FreeRTOS:第一段(基础介绍) 提到的初始化链表根节点的接口 v L i s t I n i t i a l i s e vListInitialise vL i s t I ni t ia l i se 进行初始化操作,就绪列表初始化完成之后如图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 ] ) );
    }
}
图5.

任务就绪列表初始化完成之后,野火这里的操作是调用 跟着野火学FreeRTOS:第一段(基础介绍) 提到的将一个新的链表节点插入到链表尾部的接口 v L i s t I n s e r t E n d vListInsertEnd vL i s t I n ser tE n d 来实现的,但是 F r e e R T O S FreeRTOS F ree RTOS 的官方源码不是这样操作的,感兴趣的可以自行了解一下。因为这里目前只定义了两个任务且没有实现优先级,所以就加单的将任务1挂载到索引为1的数组对应的链表里面,将任务2挂载到索引为2的数组对应的链表里面,这里实际上是将任务的任务控制块里面的元素 x S t a t e L i s t I t e m xStateListItem x St a t e L i s t I t e m 插入到相应的链表里面,任务插入到就绪列表之后的状态如图6所示。

图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》 T h e De f ini t i v e G u i d e t o A R M R C or t e x R M 3 an d C or t e x R M 4 P rocessors 的第10章阅读一遍,这样一来的话,接下来的内容将会比较好理解,图7的内容就是第10章内容的片段,这段内容基本上总结了接下来要讲的内容所进行的操作。

图7.

接口 v T a s k S t a r t S c h e d u l e r vTaskStartScheduler v T a s k St a r tS c h e d u l er (相对于源码中的定义,这里做了大量精简)也在 t a s k s . c tasks.c t a s k s . c 这个文件中定义,它用来启动调度器。如下所示。它只是简单的将全局变量 p x C u r r e n t T C B pxCurrentTCB p x C u rre n tTCB (这个变量指向当前正
在运行或者即将要运行的任务的任务控制块)的值赋值为第一个即将运行的任务(这里选择 T a s k 1 Task1 T a s k 1 )的任务控制块的地址,然后调用接口 x P o r t S t a r t S c h e d u l e r xPortStartScheduler x P or tSt a r tS c h e d u l er 启动调度器。

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 x P or tSt a r tS c h e d u l er (相对于源码中的定义,这里做了大量精简)在 p o r t . c port.c p or t . c 这个文件中定义,它用来启动调度器。如下所示。它先将 P e n d S V PendSV P e n d S V S y s T i c k SysTick S ys T i c k 这两个系统的中断优先级设置为最低(这里将优先级设置为最低的原因是上下文的切换是放到 P e n d S V PendSV P e n d S V 中断中的,因为优先级最低因此这样其它中断不会打断上下文切换操作),然后调用接口 p r v S t a r t F i r s t T a s k prvStartFirstTask p r v St a r tF i rs tT a s k 开始执行第一个任务 T a s k 1 Task1 T a s k 1

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 p r v St a r tF i rs tT a s k p o r t . c port.c p or t . c 这个文件中定义,它用来执行第一个任务,这里用汇编编写。如下所示。它的主要操作是使能了中断,然后使用 S V C SVC S V C 指令触发调用 S V C a l l SVCall S V C a ll 这个系统中断去执行第一个任务。

__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 S V C a ll 这个系统中断函数定义如下,使用汇编编写。中断中首先获取任务1的任务控制块的地址,然后获取任务控制块的第一个元素,我们知道任务控制块的第一个元素就是任务栈的栈顶指针,任务初始化完成之后这个指针指向的位置如图4所示,然后从图4所示堆栈指针指向的位置将任务1的任务栈中的蓝色字体的内容出栈(也就是任务1初始化时寄存器 R 4 R4 R 4 , R 5 R5 R 5 , R 6 R6 R 6 , R 7 R7 R 7 , R 8 R8 R 8 , R 9 R9 R 9 , R 10 R10 R 10 , R 11 R11 R 11 的初始值)并重新赋值给这些寄存器,操作完成之后堆栈指针此时应该指向图4中红色字体的 R 0 R0 R 0 所在的位置,此时将 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 P rocessor St a c k P o in t er , PSP (在 C o r t e x − M Cortex-M C or t e x 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 P rocessor St a c k P o in t er , 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 M ain St a c k P o in t er , 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》 T h e De f ini t i v e G u i d e t o A R M R C or t e x R M 3 an d C or t e x R M 4 P rocessors 。在有实时操作系统的环境中,任务使用 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 P rocessor St a c k P o in t er , PSP )设置为也指向图4中红色字体的 R 0 R0 R 0 所在的位置。最后将 R 14 ( L R ) R14(LR) R 14 ( L R ) 寄存器的值设置为0xFFFFFFD并触发异常返回将任务1的任务栈中的红色字体的内容出栈(也就是任务1初始化时寄存器 R 0 R0 R 0 , R 1 R1 R 1 , R 2 R2 R 2 , R 3 R3 R 3 , R 12 R12 R 12 , R 14 R14 R 14 , R 15 R15 R 15 (对应任务1对应的任务函数的指针), x P S R xPSR x PSR 的初始值)并重新赋值给这些寄存器,此时最关键的是出栈完成之后 R 15 , P C , P r o g r a m C o u n t e r R15,PC,Program\quad Counter R 15 , PC , P ro g r am C o u n t er 值为任务1对应的任务函数的指针,那就相当于此时就开始运行任务1了且此时使用的栈是任务一的任务栈(由前面设置的 R 14 ( L R ) R14(LR) R 14 ( L R ) 寄存器的值 0 x F F F F F F D 0xFFFFFFD 0 x FFFFFF D 决定)。

__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 t a s kY I E L D ,也就是 p o r t Y I E L D portYIELD p or t Y I E L D ,如下所示。这个接口也没有做太多的动作,只是通过将 S y s t e m C o n t r o l B l o c k System\quad Control\quad Block S ys t e m C o n t ro l Bl oc k 模块的 I C S R ICSR I CSR 寄存器的相应位置1来触发进入 P e n d S V PendSV P e n d S V 中断来进行任务的切换。

    #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 P e n d S V 中断函数的定义如下(用汇编编写),当进入 P e n d S V PendSV P e n d S V 中断之后,系统会自动的将图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 P rocessor St a c k P o in t er , PSP 指向的位置如图7所示。然后再手动的将 R 0 R0 R 0 , R 1 R1 R 1 , R 2 R2 R 2 , R 3 R3 R 3 , R 12 R12 R 12 , R 14 R14 R 14 , R 15 R15 R 15 等寄存器的内容压入当前正在执行的任务的任务栈中,且将当前执行的任务的任务控制块的 p x T o p O f S t a c k pxTopOfStack p x T o pO f St a c k (也就是堆栈指针)元素指向的位置设置为图4的位置。任务的切换伴随着上下文的切换,上文就是当前正在执行的任务的一些环境变量,就是图4或图7中那些红色和蓝色字体的寄存器的值,那此时上文的操作(保存当前正在执行的任务的环境变量到对应的任务栈中)算是完成了。下文的操作就是将下一个将要执行的任务的环境变量恢复到系统的图4或图7中那些红色和蓝色字体对应的寄存器中。在进行下文的操作之前这里进行了一个关中断的操作,这是为了突发的中断会影响到下文的操作(这一点我目前也不是太清楚)。接下来就是调用接口 v T a s k S w i t c h C o n t e x t vTaskSwitchContext v T a s k Sw i t c h C o n t e x t 将全局变量 p x C u r r e n t T C B pxCurrentTCB p x C u rre n tTCB (这个变量指向当前正在运行或者即将要运行的任务的任务控制块)的值赋值为下一个任务的任务控制块的指针并重新开启了中断。这时即将执行的任务的任务控制块里面的 p x T o p O f S t a c k pxTopOfStack p x T o pO f St a c k (也就是堆栈指针)元素应该指向图4所示的位置。接着将即将执行的任务的任务栈中的蓝色字体的内容出栈(也就是任务初始化时寄存器 R 4 R4 R 4 , R 5 R5 R 5 , R 6 R6 R 6 , R 7 R7 R 7 , R 8 R8 R 8 , R 9 R9 R 9 , R 10 R10 R 10 , R 11 R11 R 11 的初始值或者任务切换时保存入栈的值)并重新赋值给这些寄存器,操作完成之后将 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 P rocessor St a c k P o in t er , PSP 指向图7的位置。这时 R 14 ( L R ) R14(LR) R 14 ( L R ) 寄存器的值应该和进入 P e n d S V PendSV P e n d S V 中断函数时的值一样,应该是 0 x F F F F F F D 0xFFFFFFD 0 x FFFFFF D ,利用该值触发异常返回就会将即将执行的任务的任务栈中红色字体的内容出栈(也就是即将执行的任务初始化时或任务切换时保存的寄存器 R 0 R0 R 0 , R 1 R1 R 1 , R 2 R2 R 2 , R 3 R3 R 3 , R 12 R12 R 12 , R 14 R14 R 14 , R 15 R15 R 15 (对应即将执行的任务的任务函数的指针), x P S R xPSR x PSR 的初始值)并重新赋值给这些寄存器,此时最关键的是出栈完成之后 R 15 , P C , P r o g r a m C o u n t e r R15,PC,Program\quad Counter R 15 , PC , P ro g r am C o u n t er 值为即将执行的任务的任务函数的指针,那就相当于此时就开始运行即将执行的任务了且此时使用的栈是任务一的任务栈(由 R 14 ( L R ) R14(LR) R 14 ( L R ) 寄存器的值 0 x F F F F F F D 0xFFFFFFD 0 x FFFFFF D 决定)。那此时任务切换就完成了,也开始了新的任务的执行。

__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;
    }
}
图7.

到这里为止关键的代码都已经介绍完了,下面来实际运行并仿真看看,前面说过这里的示例不依赖于任何的硬件板子,而是采用 K E I L − M D K KEIL-MDK K E I L M DK 自带的软件模拟仿真,在配置的时候勾选图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);
}
图8.

进入 D E B U G DEBUG D EB U G 模式之后,将 K E I L − M D K KEIL-MDK K E I L M DK 自带的 L o g i c A n a l y z e r Logic\quad Analyzer L o g i c A na l yzer 调出来,如图9所示。然后将 f l a g 1 flag1 f l a g 1 f l a g 2 flag2 f l a g 2 这两个全局变量添加进 L o g i c A n a l y z e r Logic\quad Analyzer L o g i c A na l yzer ,如图10所示,并设置 f l a g 1 flag1 f l a g 1 f l a g 2 flag2 f l a g 2 这两个全局变量的显示属性为 B i t Bit B i t ,如图11所示。然后全速运行我们就可以看到图12所示的现象,这也是符合预期的。

图9.
图10.
图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》 T h e De f ini t i v e G u i d e t o A R M R C or t e x R M 3 an d C or t e x R M 4 P rocessors 。所谓的临界段可以简单的理解为是一段不能被打断的代码,比如系统调度和上下文切换,如果被打断的话可能会导致当前环境中的变量(例如前面提到的栈中的那些需要入栈保存或出栈恢复的寄存器)的值改变,从而导致系统调度或上下文切换发生异常或错误。前面第一个任务的执行和任务的切换都是在 P e n d S V PendSV P e n d S V 中断和 S V C a l l SVCall S V C a ll 中断中进行的,那有可能出现打断的情况是在这两个中断执行期间有了更高优先级别的中断到来了,转而去执行其它中断。这里的办法是在 P e n d S V PendSV P e n d S V 中断和 S V C a l l SVCall S V C a ll 中断中进行相应临界段的操作的时候先关闭其它中断的响应,等相应临界段的操作完成之后再打开中断响应以此来杜绝其它中断对临界段代码的干扰。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

跟着野火学FreeRTOS:第一段(任务定义,切换以及临界段) 的相关文章

随机推荐

  • 什么是DLNA

    DLNA旨在解决个人PC 消费电器 移动设备在内的无线网络和有线网络的互联互通 使得数字媒体和内容服务的无限制的共享和增长成为可能 DLNA的口号是Enjoy your music photos and videos anywhere an
  • LLaVA和LLaVA-Plus视觉指令微调及工具使用构建多模态智能体

    认识和理解视觉内容 以及基于人类指令对视觉世界进行推理 长久以来一直是一个具有挑战性问题 得益于大规模预训练 OpenAI 的 GPT 4V 展示了在自然语言处理任务和复杂视觉理解中令人印象深刻的能力 智源社区邀请到了LLaVA的一作柳昊天
  • 测试开发 | 智能辅助在心理健康治疗中的革新:倾听、理解、支持的新时代

    随着科技的迅速发展 智能辅助技术正在逐渐渗透到心理健康治疗领域 为个体提供更为智能 个性化的支持 这种创新性的结合为心理健康领域带来了新的可能性 使治疗更加灵活 高效 并为患者提供了更全面的关怀 1 虚拟治疗环境 智能辅助技术通过虚拟治疗环
  • 航空港务数据大屏为航空港的可持续发展提供有力支撑!

    随着经济的发展 不断加建与扩建民用机场 空港行业规模不断扩大 在不断引进和消化发达国家先进技术的同时 中国深入开展了对新技术和新材料的研究 极大地丰富和发展了中国的机场建设技术 且各项机场建设计划均已落实推进 行业在经济发展的推动下欣欣向荣
  • 在vue3的setup语法糖中为什么无法直接使用useRouter().currentRoute

    在vue3的setup语法糖中为什么无法直接使用useRouter currentRoute 问题 在setup语法糖中 不能直接使用useRouter xx
  • 测试开发 | 创业与人工智能的密切关系

    随着科技的迅猛发展 人工智能 AI 已经逐渐渗透到各个领域 为创业者带来了前所未有的机遇与挑战 在这个数字化转型的时代 创业与人工智能的密切关系日益凸显 成为推动创新和持续发展的关键因素 1 人工智能为创业提供新的商业模式 人工智能技术的不
  • 在spring boot项目引入mybatis plus后的的案例实践

    前景提要 1 项目背景 一个spring boot mybatis的项目 分页一直是PageHelper 2 为什么要引入mybatis plus 1 简化单表的crud 2 对mybatis plus进行简单的设计 以满足现有系统的规范
  • 龙芯loongarch64服务器编译安装paddlepaddle

    前言 PaddlePaddle Parallel Distributed Deep Learning 中文名飞桨 是百度公司推出的开源 易学习 易使用的分布式深度学习平台 现阶段各行各业均追求国产化 软件行业也一样 所有需要在龙芯服务器上编
  • Python+Pytest接口自动化之测试函数、测试类/测试方法的封装

    前言 在python pytest 接口自动化系列中 我们之前的文章基本都没有将代码进行封装 但实际编写自动化测试脚本中 我们都需要将测试代码进行封装 才能被测试框架识别执行 例如单个接口的请求代码如下 import requests he
  • Amazon Toolkit — CodeWhisperer 使用

    tFragment gt 官网 https aws amazon com cn codewhisperer trk cndc detail 最近学习了亚马逊云科技的 代码工具 感慨颇多 下面是安装 和使用的分享 CodeWhisperer
  • android 13.0 USB连接模式默认设为MTP

    1 概述 在13 0android系统产品开发中 在通过otg连接设备的时候 会弹出usb连接模式这时候会让客户选择当前连接电脑是 哪种模式 在项目开发中 需要以mtp模式 就是可以在电脑查看设备的内部存储的样式来设置otg连接电脑的模式
  • 数据工作者最爱的AI功能,你知道吗~

    在工作中难以避免的一项任务就是各种数据总结和汇报 怎么分析总结 以何种形式汇报 都是具有一定的难点 所以我要推荐的就是具有AI图表解析功能的可视化工具 Easyv数字孪生低代码可视化平台 可实现对数据的可视化展示 通过丰富的图表组件 清晰展
  • CentOS7.9安装Mysql5.7-m14

    简介 本文介绍了Linux CentOS系统下Mysql5 7 m14的下载和安装方法 环境 CentOS Linux release 7 9 2009 Core mysql Ver 14 14 Distrib 5 7 4 m14 for
  • EasyV+UE创造数字孪生可视化新篇章!

    众所周知 UE是UNREAL ENGINE 虚幻引擎 的简写 由Epic开发 是世界知名授权最广的游戏引擎之一 EasyV是一款数据可视化应用平台 用户通过EasyV可以更高效的实现数据可视化项目搭建 产品内有丰富的模版 海量的组件 简单的
  • 智能辅助技术的未来前景:创新与便利的引领者

    在数字化时代 智能辅助技术正迅速崛起 成为改善生活 提升工作效率的关键因素 这些技术通过结合人工智能 机器学习和物联网等前沿技术 为用户提供更加智能 便捷的体验 未来 智能辅助技术有望在多个领域引领创新 改变人们的生活方式 1 智能助理的个
  • 使用 Amazon Fault Injection Service 演示多区域和多可用区应用程序弹性

    文章作者 Jeff Amazon Fault Injection Service FIS 可帮助您将混沌工程大规模付诸实践 今天 我们推出了新的 场景 这些场景可以让您演示在亚马逊云科技可用区完全断电 或从一个亚马逊云科技区域到另一个亚马逊
  • 测试开发 | 物流与供应链中的智能优化

    随着全球化的深入和商业环境的复杂化 物流与供应链管理面临着前所未有的挑战 然而 随着人工智能 AI 和物联网 IoT 等技术的不断发展 智能优化正成为推动物流与供应链行业变革的关键力量 以下是智能优化在物流与供应链领域的一些关键应用 1 实
  • 测试开发 |AI驱动的健康监测与管理:数字化时代的医疗创新

    随着人工智能 AI 技术的飞速发展 其在健康监测与管理领域的应用正日益成为医疗领域的重要变革力量 AI不仅提供了更精准 高效的医学诊断手段 还为个体化的健康管理提供了全新的可能性 以下是AI驱动的健康监测与管理在数字化时代的一些关键应用 1
  • 测试开发 | 游戏开发中的人工智能创新:探索数字娱乐的未来

    随着科技的不断进步 人工智能 AI 在游戏开发领域展现出巨大的潜力 为游戏行业带来了前所未有的创新 以下是游戏开发中人工智能创新的一些关键方面 1 智能敌对角色和游戏对抗性 传统游戏中的敌对角色往往遵循固定的模式和策略 容易被玩家预测 通过
  • 跟着野火学FreeRTOS:第一段(任务定义,切换以及临界段)

    在裸机系统中 系统的主体就是 C P U CPU CP U 按照预先设定的程序逻辑在 m a i n