FreeRTOS学习记录(四):任务、任务切换(难点)

2022-04-23

依据:[野火]《FreeRTOS内核实现与应用开发实战指南》


目录

一、任务

二、创建任务

1、定义任务栈

2、定义任务函数

3、定义任务控制块

4、实现任务创建函数

xTaskCreateStatic()函数

prvInitialiseNewTask()函数

pxPortInitialiseStack()函数

三、实现就绪列表

1、定义就绪列表

 2、就绪列表初始化

 3、将任务插入就绪列表

 四、实现调度器

1、启动调度器

vTaskStartScheduler()函数

xPortStartScheduler()

prvStartFirstTask()

vPortSVCHandler()函数

 2、任务切换

taskYIELD()

xPortPendSVHandler()函数

vTaskSwitchContext()函数

测试代码

汇编指令参考


任务切换由汇编实现!!

所有函数都有所简化!!不是FreeRTOS的原函数

一、任务

把main死循环中的函数分割成一个个独立且无法返回的函数,这种函数称为任务。

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

二、创建任务

1、定义任务栈

栈是单片机 RAM 里面一段连续的内存空间,栈的大小一般在启动文件或者链接脚本里面指定,最后由 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)任务栈其实就是一个预先定义好的全局数据,默认128,单位为字。即512字节。StackType_t在portmacro.h中定义。学习笔记(一)可查看。

2、定义任务函数

任务是一个独立的函数,函数主体无限循环且不能返回。

delay()函数尽量避免,FreeRTOS提供了延时函数。

// 任务函数
/* 软件延时 */
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、定义任务控制块

在多任务系统中,任务的执行是由系统调度的。系统为了顺利的调度任务,为每个任务都额外定义了一个任务控制块, 这个任务控制块就相当于任务的身份证,里面存有任务的所有信息,比如任务的栈指针, 任务名称,任务的形参等。有了这个任务控制块之后,以后系统对任务的全部操作都可以通过这个任务控制块来实现。定义一个任务控制块需要一个新的数据类型,该数据类型在 task.c 这 C 头文件中声明(为了 tskTCB 这个数据类型能在其它地方使用,讲解的时候我把 这个任务控制块的声明放在了 FreeRTOS.h 这个头文件)

//tasks.c
//复制自FreeRTOS库,删减了一些定义

/*
*任务控制块。为每个任务分配一个任务控制块(TCB),
*并存储任务状态信息,包括指向任务上下文的指针
*(任务的运行时环境,包括寄存器值)
*/ 
typedef struct tskTaskControlBlock       /*旧的命名约定用于防止破坏支持内核的调试器*/ 
{
    volatile StackType_t * pxTopOfStack;  //(1)      /*<当前堆栈的栈顶,必须位于结构体的第一项 */

    ListItem_t xStateListItem;            //(2)       /*< 任务的状态列表项所引用的列表表示该任务的状态(就绪、已阻止、已暂停) */
    ListItem_t xEventListItem;                  /*< 事件列表项,用于将任务以引用的方式挂接到事件列表 */
    UBaseType_t uxPriority;                     /*< 任务的优先级。0是最低优先级*/
    StackType_t * pxStack;                //(3)       /*< 指向堆栈的起始 */
    char pcTaskName[ configMAX_TASK_NAME_LEN ]; //(4) /*< 创建任务时为任务指定的描述性名称。仅用于调试。 */ /*lint !e971 仅允许字符串和单个字符使用非限定字符类型*/

} tskTCB;

typedef tskTCB TCB_t;

(2)任务节点,这是一个内置在 TCB 控制块中的链表节点,通过这个节点,可以将任务控制块挂接到各种链表中。链表参考学习记录(三)

(4)任务名称,字符串形式,长度由宏 configMAX_TASK_NAME_LEN 来控制,该宏在 FreeRTOSConfig.h 中定义,默认为 16。

任务控制块定义

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

4、实现任务创建函数

任务的栈,任务的函数实体,任务的控制块最终需要联系起来才能由系统进行统一调度。那么这个联系的工作就由任务创建函数 xTaskCreateStatic()来实现,该函数在 task.c 中定义,在 task.h 中声明

xTaskCreateStatic()函数

//tasks.c
//删除了部分定义、、
//静态创建为例子
/*-----------------------------------------------------------*/
#if( configSUPPORT_STATIC_ALLOCATION == 1 ) //(1)

    TaskHandle_t xTaskCreateStatic( TaskFunction_t pxTaskCode,      //(2)//指针,函数入口
                        const char * const pcName,          //(3)//任务描述
                        const configSTACK_DEPTH_TYPE usStackDepth,      //(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 ) )
        {
            /*用于任务的TCB和堆栈的内存被传递到这个函数中——使用它们*/
            pxNewTCB = ( TCB_t * ) pxTaskBuffer

            pxNewTCB->pxStack = ( StackType_t * ) puxStackBuffer
                
                      //(9)  //创建新任务
            prvInitialiseNewTask( pxTaskCode, pcName, ulStackDepth, pvParameters,  &xReturn, pxNewTCB );
            //任务入口,名称,栈大小,形参,句柄,栈起始位置
          
        }
        else
        {
            xReturn = NULL;
        }

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


    }
#endif /* SUPPORT_STATIC_ALLOCATION */
/*-----------------------------------------------------------*/

(1)FreeRTOS 中,任务的创建有两种方法,一种是使用动态创建,一 种是使用静态创建。动态创建时,任务控制块和栈的内存是创建任务时动态分配的,任务删除时,内存可以释放。静态创建时,任务控制块和栈的内存需要事先定义好,是静态的内存,任务删除时,内存不 释放 。

(那就是说动态创建的函数比较复杂咯~~)

(2)任务入口,即任务的函数名称。TaskFunction_t 是在 projdefs.h 中重定义的一个数据类型,实际就是空指针。

//projdefs.h


typedef void (* TaskFunction_t)( void * );

(3)任务名称,字符串形式,方便调试。

(4)任务栈大小,单位为字。

(5)任务形参

(6)任务栈起始地址。

(7)任务控制块指针。

(8)定义一个任务句柄 xReturn,任务句柄用于指向任务的 TCB。任务句柄的数据类型为 TaskHandle_t,在 task.h 中定义,实际上就是一个空指针

//任务句柄
 typedef void * TaskHandle_t;

(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,        //(4)//任务形参
                                  TaskHandle_t * const pxCreatedTask,//(5)//任务句柄
                                  TCB_t * pxNewTCB )                //(6)//任务控制块指针
{
    StackType_t * pxTopOfStack;
    UBaseType_t x;

    //* 获取栈顶地址 */ (7)
    pxTopOfStack = pxNewTCB->pxStack + ( ulStackDepth - ( uint32_t ) 1 );
    //* 向下做 8 字节对齐 */ (8)
    pxTopOfStack = ( StackType_t * ) \
                    ( ( ( uint32_t ) pxTopOfStack ) & ( ~( ( uint32_t ) 0x0007 ) ) );

    //* 将任务的名字存储在 TCB 中 */ (9)
    for( x = ( UBaseType_t ) 0; x < ( UBaseType_t ) configMAX_TASK_NAME_LEN; x++ )
    {
        pxNewTCB->pcTaskName[ x ] = pcName[ x ];

        if( pcName[ x ] == ( char ) 0x00 )
        {
            break;
        }
    
    }

    //* 任务名字的长度不能超过 configMAX_TASK_NAME_LEN */(10)
    pxNewTCB->pcTaskName[ configMAX_TASK_NAME_LEN - 1 ] = '\0';
  
    //* 初始化 TCB 中的 xStateListItem 节点 */ (11)
    vListInitialiseItem( &( pxNewTCB->xStateListItem ) );

    //* 设置 xStateListItem 节点的拥有者 */ (12)
    listSET_LIST_ITEM_OWNER( &( pxNewTCB->xStateListItem ), pxNewTCB );

    //* 初始化任务栈 */ (13)
    pxNewTCB->pxTopOfStack = pxPortInitialiseStack( pxTopOfStack,
                                                    pxTaskCode,
                                                    pvParameters );
    /* 让任务句柄指向任务控制块 */(14)
    if ( ( void * ) pxCreatedTask != NULL )
    {
        *pxCreatedTask = ( TaskHandle_t ) pxNewTCB;
    }

}

(8)将栈顶指针向下做 8 字节对齐。在 Cortex-M3(Cortex-M4 或 Cortex-M7)内核的单片机中,因为总线宽度是 32 位的,通常只要栈保持 4 字节对齐就行, 可这样为啥要 8 字节?难道有哪些操作是 64 位的?确实有,那就是浮点运算,所以要 8 字 节对齐(但是目前我们都还没有涉及到浮点运算,只是为了后续兼容浮点运行的考虑)。 如果栈顶指针是 8 字节对齐的,在进行向下 8 字节对齐的时候,指针不会移动,如果不是 8 字节对齐的,在做向下 8 字节对齐的时候,就会空出几个字节,不会使用,比如当 pxTopOfStack 是 33,明显不能整除 8,进行向下 8 字节对齐就是 32,那么就会空出一个字 节不使用。

(12)设置 xStateListItem 节点的拥有者,即拥有这个节点本身的 TCB。

(13)调用 pxPortInitialiseStack()函数初始化任务栈,并更新栈顶指针, 任务第一次运行的环境参数就存在任务栈中。该函数在 port.c中定义。

pxPortInitialiseStack()函数

初始化任务栈

//port.c
#define portINITIAL_XPSR                      ( 0x01000000 )
#define portSTART_ADDRESS_MASK                ( ( StackType_t ) 0xfffffffeUL )

static void prvTaskExitError( void )
{
    configASSERT( uxCriticalNesting == ~0UL );
    portDISABLE_INTERRUPTS();

    for( ; ; )
    {
    }
}
/*-----------------------------------------------------------*/

/*
 * See header file for description.
 */
StackType_t * pxPortInitialiseStack( StackType_t * pxTopOfStack,
                                     TaskFunction_t pxCode,
                                     void * pvParameters )
{
    /* 异常发生时,自动加载到 CPU 寄存器的内容 */        //(1)
    pxTopOfStack--;                                                      
    *pxTopOfStack = portINITIAL_XPSR;                //(2)               /* xPSR */
    pxTopOfStack--;
    *pxTopOfStack = ( ( StackType_t ) pxCode ) & portSTART_ADDRESS_MASK; //(3)/* PC */
    pxTopOfStack--;
    *pxTopOfStack = ( StackType_t ) prvTaskExitError;     //(4)          /* LR */

    pxTopOfStack -= 5;                                                   /* R12, R3, R2 and R1. */
    *pxTopOfStack = ( StackType_t ) pvParameters;         //(5)          /* R0 */
    
    /* 异常发生时,手动加载到 CPU 寄存器的内容 */
    pxTopOfStack -= 8;                                    //(6)          /* R11, R10, R9, R8, R7, R6, R5 and R4. */

    /* 返回栈顶指针,此时 pxTopOfStack 指向空闲栈 */
    return pxTopOfStack;                                  //(7)
}
/*-----------------------------------------------------------*/

(1)异常发生时,CPU 自动从栈中加载到 CPU 寄存器的内容。包括 8 个寄存器,分别为 R0、R1、R2、R3、R12、R14、R15 和 xPSR 的位 24,且顺序不能变。

(2)xPSR 的 bit24 必须置 1,即 0x01000000。

(3)任务的入口地址。

(4)任务的返回地址,通常任务是不会返回的,如果返回了就跳转到 prvTaskExitError,该函数是一个无限循环。

(5)R12, R3, R2 and R1 默认初始化为 0。

(6)异常发生时,需要手动加载到 CPU 寄存器的内容,总共有 8 个, 分别为 R4、R5、R6、R7、R8、R9、R10和 R11,默认初始化为 0。

(7)返回栈顶指针,此时 pxTopOfStack 指向具体见图 。任务第一次 运行时,就是从这个栈指针开始手动加载 8 个字的内容到 CPU 寄存器:R4、R5、R6、R7、 R8、R9、R10 和 R11,当退出异常时,栈中剩下的 8 个字的内容会自动加载到 CPU 寄存器: R0、R1、R2、R3、R12、R14、R15 和 xPSR 的位 24。此时 PC 指针就指向了任务入口地址, 从而成功跳转到第一个任务。

三、实现就绪列表

1、定义就绪列表

任务创建好之后,我们需要把任务添加到就绪列表里面,表示任务已经就绪,系统随 时可以调度。就绪列表在 task.c 中定义

/* 任务就绪列表 */

List_t pxReadyTasksLists[ configMAX_PRIORITIES ];

就绪列表实际上就是一个 List_t 类型的数组,数组的大小由决定 最 大 任 务 优 先 级 的 宏 configMAX_PRIORITIES 决 定 , configMAX_PRIORITIESFreeRTOSConfig.h 中默认定义为 5,最大支持 256 个优先级。数组的下标对应了任务的优 先级,同一优先级的任务统一插入到就绪列表的同一条链表中。

空的就绪列表:

 2、就绪列表初始化

就绪列表在使用前需要先初始化,就绪列表初始化的工作在函数 prvInitialiseTaskLists() 里面实现

//就绪列表初始化
void prvInitialiseTaskLists( void )
{
    UBaseType_t uxPriority;

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

就绪列表初始化完成:

 3、将任务插入就绪列表

任务控制块里面有一个 xStateListItem 成员,数据类型为 ListItem_t,我们将任务插入 到就绪列表里面,就是通过将任务控制块的 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 和 Task2 任务在插入到就绪列表的时 候,可以随便选择插入的位置。在代码清单 7-15 中,我们选择将 Task1 任务插入到就绪列 表下标为 1 的链表中,Task2 任务插入到就绪列表下标为 2 的链表中

任务插入到就绪列表

 四、实现调度器

调度器是操作系统的核心,其主要功能就是实现任务的切换,即从就绪列表里面找到 优先级最高的任务,然后去执行该任务。从代码上来看,调度器无非也就是由几个全局变 量和一些可以实现任务切换的函数组成,全部都在 task.c 文件中实现。

1、启动调度器

调度器的启动由 vTaskStartScheduler()函数来完成,该函数在 task.c 中定义

vTaskStartScheduler()函数

void vTaskStartScheduler( void )
{
    /* 手动指定第一个运行的任务 */
    pxCurrentTCB = &Task1TCB;                      //(1)

    /* 启动调度器 */
    if ( xPortStartScheduler() != pdFALSE )
    {
        /* 调度器启动成功,则不会返回,即不会来到这里 */ //(2)
    }
}

(1)pxCurrentTCB 是一个在 task.c 定义的全局指针,用于指向当前正在运行或者即将要运行的任务的任务控制块。目前我们还不支持优先级,则手动指定第一个要运行的任务。

(2)调用函数 xPortStartScheduler()启动调度器,调度器启动成功,则 不会返回。该函数在 port.c 中实现

xPortStartScheduler()

// xPortStartScheduler()函数
/*
 * 参考资料《STM32F10xxx Cortex-M3 programming manual》4.4.7,百度搜索“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)配置 PendSVSysTick 的中断优先级为最低。SysTick 和 PendSV 都会涉及到系统调度,系统调度的优先级要低于系统的其它硬件中断优先级,即优 先相应系统中的外部硬件中断,所以 SysTick 和 PendSV 的中断优先级配置为最低。

(2)调用函数 prvStartFirstTask()启动第一个任务,启动成功后,则不再返回,该函数由汇编编写,在 port.c 实现

prvStartFirstTask()

prvStartFirstTask()函数用于开始第一个任务,主要做了两个动作,一个是更新 MSP 的值,二是产生 SVC 系统调用,然后去到 SVC 的中断服务函数里面真正切换到第一个任务。

// prvStartFirstTask()函数
/*
 * 参考资料《STM32F10xxx Cortex-M3 programming manual》4.4.3,百度搜索“PM0056”即可找到这个文档
 * 在 Cortex-M 中,内核外设SCB 的地址范围为:0xE000ED00-0xE000ED3F
 * 0xE000ED008 为 SCB 外设中 SCB_VTOR 这个寄存器的地址,里面存放的是向量表的起始地址,即 MSP 的地址
 */
//(1)
__asm void prvStartFirstTask( void )
{
        PRESERVE8 //(2)

        /* 在 Cortex-M 中,0xE000ED08 是 SCB_VTOR 这个寄存器的地址,     //(3)
        里面存放的是向量表的起始地址,即 MSP 的地址 */
        ldr r0, =0xE000ED08                         //(4)
        ldr r0, [r0]                                //(5)
        ldr r0, [r0]                                //(6)

        /* 设置主堆栈指针 msp 的值 */
        msr msp, r0                        //(7)

        /* 使能全局中断 */                  //(8)
        cpsie i
        cpsie f
        dsb
        isb

        /* 调用 SVC 去启动第一个任务 */
        svc 0                              //(9)
        nop
        nop
}

(2)当前栈需按照 8 字节对齐,如果都是 32 位的操作则 4 个字节对齐 即可。在 Cortex-M 中浮点运算是 8字节的。

(3)在 Cortex-M 中,0xE000ED08 是 SCB_VTOR 寄存器的地址,里面 存放的是向量表的起始地址,即 MSP 的地址。向量表通常是从内部 FLASH 的起始地址开 始存放,那么可知 memory:0x00000000 处存放的就是 MSP 的值。

(看不懂懂吗~~~~~~~~~~~~~~)

(4)将 0xE000ED08 这个立即数加载到寄存器 R0。

(5)将 0xE000ED08 这个地址指向的内容加载到寄存器 R0,此时 R0 等于 SCB_VTOR 寄存器的值,等于 0x00000000,即 memory 的起始地址。

(6)将 0x00000000 这个地址指向的内容加载到 R0,此时 R0 等于 0x200008DB

(7)查询到的值吻合。 代码清单 7-18(7):将 R0 的值存储到 MSP,此时 MSP 等于 0x200008DB,这是主堆 栈的栈顶指针。起始这一步操作有点多余,因为当系统启动的时候,执行完 Reset_Handler 的时候,向量表已经初始化完毕,MSP 的值就已经更新为向量表的起始值,即指向主堆栈 的栈顶指针。

(8)使用 CPS 指令把全局中断打开。为了快速地开关中断, Cortex-M 内核 专门设置了一条 CPS 指令,有 4 种用法

CPSID I ;PRIMASK=1 ;关中断
CPSIE I ;PRIMASK=0 ;开中断
CPSID F ;FAULTMASK=1 ;关异常
CPSIE F ;FAULTMASK=0 ;开异常

上面代码中 PRIMASK 和 FAULTMAST 是 Cortex-M内核 里面三个中断屏蔽寄存 器中的两个,还有一个是 BASEPRI

 (9)产生系统调用,服务号 0表示 SVC 中断,接下来将会执行 SVC 中 断服务函数。

vPortSVCHandler()函数

SVC 中断要想被成功响应,其函数名必须与向量表注册的名称一致,在启动文件的向 量表中,SVC 的中断服务函数注册的名称是 SVC_Handler,所以 SVC 中断服务函数的名称 我们应该写成 SVC_Handler,但是在 FreeRTOS 中,官方版本写的是 vPortSVCHandler(), 为了能够顺利的响应 SVC 中断,我们有两个选择,改中断向量表中 SVC 的注册的函数名 称或者改FreeRTOS 中 SVC 的中断服务名称。这里,我们采取第二种方法,即在 FreeRTOSConfig.h 中添加添加宏定义的方法来修改

//修改 FreeRos 中 SVC、PendSV 和 SysTick 中断服务函数的名称
#define xPortPendSVHandler PendSV_Handler
#define xPortSysTickHandler SysTick_Handler
#define vPortSVCHandler SVC_Handler

vPortSVCHandler()函数开始真正启动第一个任务,不再返回

//vPortSVCHandler()函数
__asm void vPortSVCHandler( void )
{
        extern pxCurrentTCB;     //(1)

        PRESERVE8

        ldr r3, =pxCurrentTCB    //(2)
        ldr r1, [r3]             //(3)
        ldr r0, [r1]             //(4)
        ldmia r0!, {r4-r11}      //(5)
        msr psp, r0              //(6)
        isb
        mov r0, #0               //(7)
        msr basepri, r0          //(8)
        orr r14, #0xd            //(9)

        bx r14                   //(10)
}

(1)声明外部变量 pxCurrentTCB,pxCurrentTCB 是一个在 task.c 中定 义的全局指针,用于指向当前正在运行或者即将要运行的任务的任务控制块。

(2)加载 pxCurrentTCB 的地址到 r3。

(3)加载 pxCurrentTCB 到 r3。

(4)加载 pxCurrentTCB 指向的任务控制块到 r0,任务控制块的第一个成员就是栈顶指针,所以此时 r0 等于栈顶指针。

一个刚刚被创建还没有运行过的任务的栈 空间分布具体如图所示,即 r0 等于图的 pxTopOfStack。

(5)以 r0 为基地址,将栈中向上增长的 8 个字的内容加载到 CPU 寄存 器 r4~r11,同时 r0 也会跟着自增。

(6)将新的栈顶指针 r0 更新到 psp,任务执行的时候使用的堆栈指针是 psp。

(7)将寄存器 r0清 0

(8)设置 basepri 寄存器的值为 0,即打开所有中断。basepri 是一个中 断屏蔽寄存器,大于等于此寄存器值的中断都将被屏蔽。

(9)当从 SVC 中断服务退出前,通过向 r14 寄存器最后 4 位按位或上 0x0D,使得硬件在退出时使用进程堆栈指针 PSP 完成出栈操作并返回后进入任务模式、返 回 Thumb 状态。在 SVC 中断服务里面,使用的是 MSP 堆栈指针,是处在 ARM 状态。

当 r14 为 0xFFFFFFFX,执行是中断返回指令,cortext-m3 的做法,X 的 bit0 为 1 表示 返回 thumb 状态,bit1 和 bit2 分别表示返回后 sp 用 msp 还是 psp、以及返回到特权模式还 是用户模式

(10)异常返回,这个时候出栈使用的是 PSP 指针,自动将栈中的剩下 内容加载到 CPU 寄存器: xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0 (任务的形参)同时 PSP 的值也将更新,即指向任务栈的栈顶,具体指向见图

 2、任务切换

任务切换就是在就绪列表中寻找优先级最高的就绪任务,然后去执行该任务。但是目 前我们还不支持优先级,仅实现两个任务轮流切换,任务切换函数 taskYIELD()具体实现见代码清单

taskYIELD()

//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中断服务函数,在 里面实现任务切换。

xPortPendSVHandler()函数

PendSV 中断服务函数是真正实现任务切换的地方。

//xPortPendSVHandler()函数
__asm void xPortPendSVHandler( void )
{
    extern pxCurrentTCB;       //(1)
    extern vTaskSwitchContext; //(2)
 
    PRESERVE8                  //(3)
 
    mrs r0, psp                //(4)
    isb
 
    ldr r3, =pxCurrentTCB      //(5)
    ldr r2, [r3]               //(6)

    stmdb r0!, {r4-r11}        //(7)
    str r0, [r2]               //(8)

    stmdb sp!, {r3, r14}       //(9)
    mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY   // (10)
    msr basepri, r0           //(11)
    dsb
    isb
    bl vTaskSwitchContext     //(12)
    mov r0, #0                //(13)
    msr basepri, r0 
    ldmia sp!, {r3, r14}      //(14)

    ldr r1, [r3]              //(15)
    ldr r0, [r1]              //(16)
    ldmia r0!, {r4-r11}       //(17)
    msr psp, r0               //(18)
    isb
    bx r14                    //(19)
    nop
}

(1)声明外部变量 pxCurrentTCB,pxCurrentTCB 是一个在 task.c 中定 义的全局指针,用于指向当前正在运行或者即将要运行的任务的任务控制块。

(2)声明外部函数 vTaskSwitchContext

(3)当前栈需按照 8 字节对齐,如果都是 32 位的操作则 4 个字节对齐 即可。在 Cortex-M 中浮点运算是 8字节的。

(4)将 PSP 的值存储到 r0。当进入 PendSVC Handler 时,上一个任务 运行的环境即: xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0(任务的形 参)这些 CPU 寄存器的值会自动存储到任务的栈中,剩下的 r4~r11 需要手动保存,同时 PSP 会自动更新(在更新之前 PSP 指向任务栈的栈顶),此时 PSP 具体指向见图

(5)加载 pxCurrentTCB 的地址到 r3。

(6)加载 r3 指向的内容到 r2,即 r2 等于 pxCurrentTCB。

(7)以 r0 作为基址(指针先递减,再操作,STMDB 的 DB 表示 Decrease Befor),将 CPU 寄存器 r4~r11 的值存储到任务栈,同时更新 r0的值,此时 r0 的 指向具体见图。

(8)将 r0 的值存储到 r2 指向的内容,r2 等于 pxCurrentTCB。具体为将 r0 的值存储到上一个任务的栈顶指针 pxTopOfStack,具体指向如图 7-11 的 r0 指向一样。 到此,上下文切换中的上文保存就完成了。

(9)将 R3 和 R14 临时压入堆栈(在整个系统中,中断使用的是主堆栈, 栈指针使用的是 MSP),因为接下来要调用函数 vTaskSwitchContext,调用函数时,返回 地址自动保存到 R14 中,所以一旦调用发生,R14 的值会被覆盖(PendSV 中断服务函数执 行完毕后,返回的时候需要根据 R14 的值来决定返回处理器模式还是任务模式,出栈时使 用的是 PSP 还是 MSP),因此需要入栈保护。R3 保存的是当前正在运行的任务(准确来 说是上文,因为接下来即将要切换到新的任务)的 TCB 指针(pxCurrentTCB)地址,函数调 用后 pxCurrentTCB 的值会被更新,后面我们还需要通过 R3 来操作 pxCurrentTCB,但是运 行函数 vTaskSwitchContext 时不确定会不会使用 R3 寄存器作为中间变量,所以为了保险起 见,R3 也入栈保护起来。

(10)将 configMAX_SYSCALL_INTERRUPT_PRIORITY 的值存储到 r0,该宏在 FreeRTOSConfig.h 中定义,用来配置中断屏蔽寄存器 BASEPRI 的值,高四位 有效。目前配置为 191,因为是高四位有效,所以实际值等于 11,即优先级高于或者等于 11 的中断都将被屏蔽。在关中断方面,FreeRTOS 与其它的 RTOS 关中断不同,而是操作 BASEPRI 寄存器来预留一部分中断,并不像 μC/OS 或者 RT-Thread 那样直接操作 PRIMASK 把所有中断都关闭掉(除了硬 FAULT)。

(11)关中断,进入临界段,因为接下来要更新全局指针 pxCurrentTCB 的值。

(12)调用函数 vTaskSwitchContext。该函数在 task.c 中定义,作用只有 一个,选择优先级最高的任务,然后更新 pxCurrentTCB。目前我们还不支持优先级,则手 动切换,不是任务 1 就是任务 2。

(13)退出临界段,开中断,直接往 BASEPRI 写 0。

(14)从主堆栈中恢复寄存器 r3 和 r14 的值,此时的 sp 使用的是 MSP。

(15)加载 r3 指向的内容到 r1。r3 存放的是 pxCurrentTCB 的地址,即 让 r1 等于 pxCurrentTCB。pxCurrentTCB 在上面的 vTaskSwitchContext 函数中被更新,指 向了下一个将要运行的任务的 TCB。

(16)加载 r1 指向的内容到 r0,即下一个要运行的任务的栈顶指针。

(17)以 r0 作为基地址(先取值,再递增指针,LDMIA 的 IA 表示 Increase After),将下一个要运行的任务的任务栈的内容加载到 CPU 寄存器 r4~r11。

(18)更新 psp 的值,等下异常退出时,会以 psp 作为基地址,将任务 栈中剩下的内容自动加载到 CPU 寄存器。

(19)异常发生时,R14 中保存异常返回标志,包括返回后进入任务模 式还是处理器模式、使用 PSP 堆栈指针还是 MSP 堆栈指针。此时的 r14 等于 0xfffffffd,最 表示异常返回后进入任务模式,SP 以 PSP 作为堆栈指针出栈,出栈完毕后 PSP 指向任务栈 的栈顶。当调用 bx r14 指令后,系统以 PSP 作为 SP 指针出栈,把接下来要运行的新任务 的任务栈中剩下的内容加载到 CPU 寄存器:R0(任务形参)、R1、R2、R3、R12、R14 (LR)、R15(PC)和 xPSR,从而切换到新的任务。

vTaskSwitchContext()函数

//vTaskSwitchContext()函数
void vTaskSwitchContext( void )
{
    /* 两个任务轮流切换 */
    if ( pxCurrentTCB == &Task1TCB )         //(1)
    {
        pxCurrentTCB = &Task2TCB;
    }
    else                 //(2)
    {
        pxCurrentTCB = &Task1TCB;
    }
}

(1)如果当前任务为任务 1,则把下一个要运行的任务改为任务 2。

(2)如果当前任务为任务 2,则把下一个要运行的任务改为任务 1。

测试代码

//main.c 代码
/**
 ***********************************************************************
 * @file main.c
 * @author fire
 * @version V1.0
 * @date 2018-xx-xx
 * @brief 《FreeRTOS 内核实现与应用开发实战指南》书籍例程
 * 任务的定义与任务切换的实现
 ***********************************************************************
 * @attention
 *
 * 实验平台:野火 STM32 系列 开发板
 *
 * 官网 :www.embedfire.com
 * 论坛 :http://www.firebbs.cn
 * 淘宝 :https://fire-stm32.taobao.com
 *
 ***********************************************************************
 */
 
/*
 *************************************************************************
 * 包含的头文件
 *************************************************************************
 */
#include "FreeRTOS.h"
#include "task.h"

/*
 *************************************************************************
 * 全局变量
 *************************************************************************
 */
portCHAR flag1;
portCHAR flag2;
 
extern List_t pxReadyTasksLists[ configMAX_PRIORITIES ];
 

/*
 *************************************************************************
 * 任务控制块 & STACK
 *************************************************************************
 */
TaskHandle_t Task1_Handle;
#define TASK1_STACK_SIZE 128
StackType_t Task1Stack[TASK1_STACK_SIZE];
TCB_t Task1TCB;

TaskHandle_t Task2_Handle;
#define TASK2_STACK_SIZE 128
StackType_t Task2Stack[TASK2_STACK_SIZE];
TCB_t Task2TCB;


/*
 *************************************************************************
 * 函数声明
 *************************************************************************
 */
void delay (uint32_t count);
void Task1_Entry( void *p_arg );
void Task2_Entry( void *p_arg );
 
/*
 ************************************************************************
 * main 函数
 ************************************************************************
 */
/*
 * 注意事项:1、该工程使用软件仿真,debug 需选择 Ude Simulator
 *          2、在 Target 选项卡里面把晶振 Xtal(Mhz)的值改为 25,默认是 12,
 *             改成 25 是为了跟 system_ARMCM3.c 中定义的__SYSTEM_CLOCK 相同,
 *             确保仿真的时候时钟一致
 */
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();

    for (;;)
    {
        /* 系统启动成功不会到达这里 */
    }
}

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

        /* 任务切换,这里是手动切换 */
        taskYIELD(); //(注意) 
    }
}

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

        /* 任务切换,这里是手动切换 */
        taskYIELD(); //(注意) 
    }
}

(注意)还没设置优先级、每个任务执行完毕之后都主动调用 任务切换函数 taskYIELD()来实现任务的切换。

汇编指令参考

 

  • 9
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值