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

news2024/11/15 4:39:18

     在裸机系统中,系统的主体就是 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 CORTEXM3芯片为例进行介绍,最后在实战的时候会基于 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类型。

 
图1.
 
图2.

     在下面的代码中我们定义了两个任务 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》 TheDefinitiveGuidetoARMRCortexRM3andCortexRM4Processors的第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》 TheDefinitiveGuidetoARMRCortexRM3andCortexRM4Processors 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个寄存器的进栈和出栈一般都需要软件自己操作。到此为止一个任务就算是创建成功了。

 
图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 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 ] ) );
    }
}
 
图5.

     任务就绪列表初始化完成之后,野火这里的操作是调用跟着野火学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所示。

 
图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》 TheDefinitiveGuidetoARMRCortexRM3andCortexRM4Processors的第10章阅读一遍,这样一来的话,接下来的内容将会比较好理解,图7的内容就是第10章内容的片段,这段内容基本上总结了接下来要讲的内容所进行的操作。

 
图7.

     接口 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 CortexM系列的处理器中有两个堆栈指针, 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》 TheDefinitiveGuidetoARMRCortexRM3andCortexRM4Processors。在有实时操作系统的环境中,任务使用 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;
    }
}
 
图7.

     到这里为止关键的代码都已经介绍完了,下面来实际运行并仿真看看,前面说过这里的示例不依赖于任何的硬件板子,而是采用 K E I L − M D K KEIL-MDK KEILMDK自带的软件模拟仿真,在配置的时候勾选图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 DEBUG模式之后,将 K E I L − M D K KEIL-MDK KEILMDK自带的 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所示的现象,这也是符合预期的。

 
图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》 TheDefinitiveGuidetoARMRCortexRM3andCortexRM4Processors。所谓的临界段可以简单的理解为是一段不能被打断的代码,比如系统调度和上下文切换,如果被打断的话可能会导致当前环境中的变量(例如前面提到的栈中的那些需要入栈保存或出栈恢复的寄存器)的值改变,从而导致系统调度或上下文切换发生异常或错误。前面第一个任务的执行和任务的切换都是在 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在这里。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1324891.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【C++题目速刷】二分查找

【C题目速刷】二分查找 一、二分查找1、题目链接2、解题3、代码 二、在排序数组中查找元素的第一个和最后一个位置1、题目链接2、解题3、代码4、算法模板 三、x的平方根1、解题链接2、解题3、代码 四、搜索插入位置1、题目链接2、解题3、代码 五、山脉数组的峰顶索引1、题目链接…

Python新闻文本分类系统的设计与实现:基于Flask、贝叶斯算法的B/S架构

Python新闻文本分类系统的设计与实现&#xff1a;基于Flask、贝叶斯算法的B/S架构 引言数据获取与处理数据分析与可视化文本分类模型结论 引言 在信息爆炸的时代&#xff0c;新闻数据的快速获取和准确分类变得尤为重要。本文将介绍一种基于Python语言、Flask技术、B/S架构以及…

智能优化算法应用:基于野狗算法3D无线传感器网络(WSN)覆盖优化 - 附代码

智能优化算法应用&#xff1a;基于野狗算法3D无线传感器网络(WSN)覆盖优化 - 附代码 文章目录 智能优化算法应用&#xff1a;基于野狗算法3D无线传感器网络(WSN)覆盖优化 - 附代码1.无线传感网络节点模型2.覆盖数学模型及分析3.野狗算法4.实验参数设定5.算法结果6.参考文献7.MA…

msvcp120.dll丢失的多种详细有效解决方法

在计算机使用过程中&#xff0c;我们可能会遇到一些错误提示&#xff0c;其中之一就是“msvcp120.dll丢失”。那么&#xff0c;msvcp120.dll到底是什么&#xff1f;为什么会出现丢失的情况&#xff1f;丢失后会对电脑产生什么影响&#xff1f;本文将为您详细解答这些问题&#…

【汇编先导】-- 2

汇编先导 6. 寄存器 存储数据&#xff1a;CPU > 内存 > 硬盘(固态、机械) CPU还可分为&#xff1a; 32位CPU 8 16 32 64位CPU 8 16 32 64(增加了寻址能力) 通用寄存器 # 32位的通用寄存器只有8个 # 可以在任意软件的底层看到 # 通用寄存器可以存储任何值存值的范围…

【动态规划】08路径问题_下降路径最小和_C++(medium)

题目链接&#xff1a;leetcode下降路径最小和 目录 题目解析&#xff1a; 算法原理 1.状态表示 2.状态转移方程 3.初始化 4.填表顺序 5.返回值 编写代码 题目解析&#xff1a; 题目让我们求通过 matrix 的下降路径 的 最小和 由题可得&#xff1a; 在下一行选择的元…

【职言】三年功能测试,一些测试工作的“吐槽”

以下为作者观点&#xff1a; 概述 作为功能测试&#xff0c;我也分享下日常工作中功能测试值得吐槽的问题&#xff0c;由于工作时间不长且未进过大厂&#xff0c;不了解大公司的工作模式和流程&#xff0c;所以自己的方法和理解都是基于中小公司的工作经验总结&#xff0c;应…

【Linux】Linux基础命令

写在前面&#xff1a; 傍晚时分&#xff0c;你坐在屋檐下&#xff0c;看着天慢慢地黑下去&#xff0c;心里寂寞而凄凉&#xff0c;感到自己的生命被剥夺了。当时我是个年轻人&#xff0c;但我害怕这样生活下去&#xff0c;衰老下去。在我看来&#xff0c;这是比死亡更可怕的事…

flask 之上传与下载

from flask import Flask, render_template, request, send_from_directory, redirect, url_for import osapp Flask(__name__)# 上传文件存储路径 UPLOAD_FOLDER uploads app.config[UPLOAD_FOLDER] UPLOAD_FOLDERapp.route(/) def index():# 确保上传文件夹存在if not os.…

小程序中实现长按二维码图片识别

本文使用小程序提供的imae组件实现二维码的识别&#xff0c;在小程序官方文档中给出了该组件一个用于识别图片的属性show-menu-by-longpress。 属性说明&#xff1a;长按图片显示发送给朋友、收藏、保存图片、搜一搜、打开名片/前往群聊/打开小程序&#xff08;若图片中包含对应…

Ubuntu-20.04.2 mate 上安装、配置、测试 qtcreator

一、从repo中安装 Ubuntu-20.04.2的repo中&#xff0c;qtcreator安装包挺全乎的&#xff0c;敲完 sudo apt install qtcreator 看一下同时安装和新软件包将被安装列表&#xff0c;压缩包252MB&#xff0c;解压安装后933MB&#xff0c;集大成的一包。 sudo apt install qtcrea…

【从服务器获取共享列表失败】【无法与设备或资源通信】解决方案!

【从服务器获取共享列表失败】背景&#xff1a; 某项目搭建有samba共享&#xff0c;使用一段时间后&#xff0c;不知何种原因&#xff0c;客户端链接共享时报&#xff1a;从服务器获取共享列表失败&#xff0c;无效的参数。 可参考解决方案A&#xff1a; 银河麒麟samba共享文…

【经典LeetCode算法题目专栏分类】【第7期】快慢指针与链表

《博主简介》 小伙伴们好&#xff0c;我是阿旭。专注于人工智能AI、python、计算机视觉相关分享研究。 ✌更多学习资源&#xff0c;可关注公-仲-hao:【阿旭算法与机器学习】&#xff0c;共同学习交流~ &#x1f44d;感谢小伙伴们点赞、关注&#xff01; 快慢指针 移动零 class…

分段函数1_分支结构 C语言xdoj112

题目描述: 编写程序计算分段函数f(x)的值。 输入格式&#xff1a;输入实数x的值 输出格式&#xff1a;输出f(x)的值&#xff0c;结果保留两位小数。 示例&#xff1a; 输入&#xff1a;4 输出&#xff1a;2.00 #include <stdio.h> #include <math.h>//分段函数1_分…

如何入门 GPT 并快速跟上当前的大语言模型 LLM 进展?

入门GPT 首先说第一个问题&#xff1a;如何入门GPT模型&#xff1f; 最直接的方式当然是去阅读官方的论文。GPT模型从2018年的GPT-1到现在的GPT-4已经迭代了好几个版本&#xff0c;通过官方团队发表的论文是最能准确理清其发展脉络的途径&#xff0c;其中包括GPT模型本身和一…

【3D数据读取】利用JAVA读取GLB(GLTF)文件数据

了解GLB和GLTF&#xff1a; GLB和GLTF是用于共享3D数据的标准化文件格式。GLB是GLTF的二进制格式&#xff0c;而GLTF基于JSON&#xff0c;一种基于文本的数据格式。 GLB文件&#xff1a; 由一个头部和一个二进制数据块组成。头部包含文件的元数据&#xff0c;例如文件版本、文件…

w3af安装(处理python2和3,pip2和3混乱的问题)

git clone --depth 1 https://github.com/andresriancho/w3af.git cd w3af ./w3af_gui报错 打开w3af_gui看一下 要求必须是python2 但我的/usr/bin/env中的python是python3 我们将/usr/bin/env中的python换成python2 which python2 #/usr/bin/python2rm /usr/bin/pythonsud…

网络安全-零信任安全

本文为作者学习文章&#xff0c;按作者习惯写成&#xff0c;如有错误或需要追加内容请留言&#xff08;不喜勿喷&#xff09; 本文为追加文章&#xff0c;后期慢慢追加 零信任的概念 零信任技术体系是一种安全架构和策略&#xff0c;其核心理念是不信任任何用户或设备&#…

科聪控制系统典型应用车型 —— 料箱机器人

料箱机器人即料箱AGV是一种智能化物流搬运设备&#xff0c;它可以代替人力完成出库入库和搬运工作&#xff0c;可根据出入库生产出货需求&#xff0c;将货物从起点运送到终点&#xff0c;自动柔性完成货到人货到点的操作。 提升仓储和物流效率的自动化利器 料箱机器人的投用能…

查询后矩阵的和

说在前面 &#x1f388;不知道大家对于算法的学习是一个怎样的心态呢&#xff1f;为了面试还是因为兴趣&#xff1f;不管是出于什么原因&#xff0c;算法学习需要持续保持。 问题描述 给你一个整数 n 和一个下标从 0 开始的 二维数组 queries &#xff0c;其中 queries[i] [t…