二、FreeRTOS 任务的定义与切换

FreeRTOS

作者:解琛
时间:2020 年 8 月 18 日

[野火®]《FreeRTOS 内核实现与应用开发实战—基于STM32》

二、任务的定义与切换

2.1 定义任务栈

#define TASK1_STACK_SIZE
StackType_t Task1Stack[TASK1_STACK_SIZE];

#define TASK2_STACK_SIZE
StackType_t Task2Stack[TASK2_STACK_SIZE];

2.2 定义任务函数

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

/* 任务 1; */
void Task1_Entry( void *p_arg )
{
    for ( ;; )
    {
        flag1 = 1;
        delay( 100 );
        flag1 = 0;
        delay( 100 );
    }
}

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

2.3 定义任务控制块

多任务系统中,任务的执行是由系统调度的。

系统为了顺利的调度任务,为每个任务都额外定义了一个任务控制块,这个任务控制块就相当于任务的身份证,里面存有任务的所有信息,比如任务的栈指针、任务名称、任务的形参等。

typedef struct tskTaskControlBlock
{
	volatile StackType_t    *pxTopOfStack;    /* 栈顶指针,作为 TCB 的第一个成员; */
	ListItem_t			    xStateListItem;   /* 任务节点,这是一个内置在 TCB 控制块中的链表节点,
                                                 可通过这个节点将任务控制块挂接到各种链表中。 */
    StackType_t             *pxStack;         /* 任务栈起始地址; */
	char                    pcTaskName[ configMAX_TASK_NAME_LEN ];  
	                                          /* 任务名称,字符串形式; */
} tskTCB;
typedef tskTCB TCB_t;

TCB_t Task1TCB;
TCB_t Task2TCB;

2.4 实现任务创建函数

任务的栈,任务的函数实体,任务的控制块最终需要联系起来才能由系统进行统一调度。那么这个联系的工作就由任务创建函数 xTaskCreateStatic() 来实现。

2.4.1 xTaskCreateStatic()

任务的创建有两种方法,一种是使用动态创建,一种是使用静态创建。

动态创建时,任务控制块和栈的内存是创建任务时动态分配任务,删除时,内存可以释放。

静态创建时,任务控制块和栈的内存需要事先定义好,是静态的内存,任务删除时,内存不能释放。

#if( configSUPPORT_STATIC_ALLOCATION == 1 )                          /* 静态创建; */

TaskHandle_t xTaskCreateStatic(	
                                TaskFunction_t pxTaskCode,           /* 任务入口; */
					            const char * const pcName,           /* 任务名称,字符串形式; */
					            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 ) )
	{		
		pxNewTCB = ( TCB_t * ) pxTaskBuffer; 
		pxNewTCB->pxStack = ( StackType_t * ) puxStackBuffer;

		/* 创建新的任务 */
		prvInitialiseNewTask( pxTaskCode,        /* 任务入口; */
                              pcName,            /* 任务名称,字符串形式; */
                              ulStackDepth,      /* 任务栈大小,单位为字; */ 
                              pvParameters,      /* 任务形参; */
                              &xReturn,          /* 任务句柄; */ 
                              pxNewTCB);         /* 任务栈起始地址; */      

        /* xReturn 作为形参传入到 prvInitialiseNewTask 函数; */
	}
	else
	{
		xReturn = NULL;
	}

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

#endif /* configSUPPORT_STATIC_ALLOCATION */

2.4.2 prvInitialiseNewTask()

static void prvInitialiseNewTask( 	
                                    TaskFunction_t pxTaskCode,              /* 任务入口; */
									const char * const pcName,              /* 任务名称,字符串形式; */
									const uint32_t ulStackDepth,            /* 任务栈大小,单位为字; */
									void * const pvParameters,              /* 任务形参; */
									TaskHandle_t * const pxCreatedTask,     /* 任务句柄; */
									TCB_t *pxNewTCB )                       /* 任务控制块指针; */

{
	StackType_t *pxTopOfStack;
	UBaseType_t x;	
	
	/* 获取栈顶地址; */
	pxTopOfStack = pxNewTCB->pxStack + ( ulStackDepth - ( uint32_t ) 1 );
	//pxTopOfStack = ( StackType_t * ) ( ( ( portPOINTER_SIZE_TYPE ) pxTopOfStack ) & ( ~( ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK ) ) );
	
    /* 向下做8字节对齐; 
       在 Cortex-M3(Cortex-M4 或 Cortex-M7)内核的单片机中,因为总线宽度是 32 位的,通常只要栈保持 4 字节对齐就行;
       因为会涉及到浮点运算的操作,所以需要使用 8 字节对齐; */
	pxTopOfStack = ( StackType_t * ) ( ( ( uint32_t ) pxTopOfStack ) & ( ~( ( uint32_t ) 0x0007 ) ) );	

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

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

	/* 任务名字的长度不能超过 configMAX_TASK_NAME_LEN,并以'\0'结尾; */
	pxNewTCB->pcTaskName[ configMAX_TASK_NAME_LEN - 1 ] = '\0';

    /* 初始化 TCB 中的 xStateListItem 节点,即初始化该节点所在的链表为空,表示节点还没有插入任何链表; */
    vListInitialiseItem( &( pxNewTCB->xStateListItem ) );

    /* 设置 xStateListItem 节点的拥有者,即拥有这个节点本身的 TCB; */
	listSET_LIST_ITEM_OWNER( &( pxNewTCB->xStateListItem ), pxNewTCB );
    
    /* 初始化任务栈,并更新栈顶指针,任务第一次运行的环境参数就存在任务栈中; */
	pxNewTCB->pxTopOfStack = pxPortInitialiseStack( pxTopOfStack, pxTaskCode, pvParameters );   

	/* 让任务句柄指向任务控制块; */
    if( ( void * ) pxCreatedTask != NULL )
	{		
		*pxCreatedTask = ( TaskHandle_t ) pxNewTCB;
	}
}

2.4.3 pxPortInitialiseStack()

任务栈初始化完后栈空间分布图

#define portINITIAL_XPSR        ( 0x01000000 )
#define portSTART_ADDRESS_MASK  ( ( StackType_t ) 0xfffffffeUL )

static void prvTaskExitError( void )
{
    /* 函数停止在这里; */
    for (;;);
}

StackType_t *pxPortInitialiseStack( StackType_t *pxTopOfStack, TaskFunction_t pxCode, void *pvParameters )
{
    /* 异常发生时,自动加载到CPU寄存器的内容; */
	pxTopOfStack--;
	*pxTopOfStack = portINITIAL_XPSR;                                       /* xPSR 的 bit24 必须置 1; */
	pxTopOfStack--;
	*pxTopOfStack = ( ( StackType_t ) pxCode ) & portSTART_ADDRESS_MASK;	/* PC,即任务入口函数; */
	pxTopOfStack--;
	*pxTopOfStack = ( StackType_t ) prvTaskExitError;                       /* LR,任务的返回地址,通常任务是不会返回的,
                                                                               如果返回了就跳转到 prvTaskExitError,该函数是一个无限循环; */
	pxTopOfStack -= 5;                                                      /* R12, R3, R2 and R1 默认初始化为0; */
	*pxTopOfStack = ( StackType_t ) pvParameters;                           /* R0,任务形参; */
    
    /* 异常发生时,手动加载到CPU寄存器的内容; */    
	pxTopOfStack -= 8;                  /* R11, R10, R9, R8, R7, R6, R5 and R4 默认初始化为 0; */

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

函数返回时,pxTopOfStack 指向R4,任务第一次运行时,就是从这个栈指针开始手动加载 8 个字的内容到 CPU 寄存器:R4、R5、R6、R7、R8、R9、R10 和 R11。

当退出异常时,栈中剩下的 8 个字的内容会自动加载到 CPU 寄存器:R0、R1、R2、R3、R12、R14、R15 和 xPSR 的位 24。此时 PC 指针就指向了任务入口地址,从而成功跳转到第一个任务。

2.5 实现就绪列表

2.5.1 定义就绪列表

任务创建好之后,需要把任务添加到就绪列表里面,表示任务已经就绪,系统随时可以调度。

/* 任务就绪列表 */
List_t pxReadyTasksLists[ configMAX_PRIORITIES ];

空的就序列表
就绪列表实际上就是一个 List_t 类型的数组,数组的大小由决定最大任务优先级的宏 configMAX_PRIORITIES 决定。

configMAX_PRIORITIES 在 FreeRTOSConfig.h 中默认定义为 5,即最大支持 256 个优先级。

2 5 × 8 = 256 {2^5 \times 8 = 256 } 25×8=256

数组的下标对应了任务的优先级,同一优先级的任务统一插入到就绪列表的同一条链表中。

2.5.2 就序列表的初始化

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

就序列表的初始化

2.5.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 ) );

任务插入到就绪列表

2.5.4 实现调度器

调度器是操作系统的核心,其主要功能就是实现任务的切换,即从就绪列表里面找到优先级最高的任务,然后去执行该任务。

2.5.4.1 启动调度器
2.5.4.1.1 vTaskStartScheduler()
/* 当前正在运行的任务的任务控制块指针,默认初始化为NULL; 
   用于指向当前正在运行或者即将要运行的任务的任务控制块 */
TCB_t * volatile pxCurrentTCB = NULL;

void vTaskStartScheduler( void )
{
    /* 手动指定第一个运行的任务; */
    pxCurrentTCB = &Task1TCB;
    
    /* 启动调度器; */
    if( xPortStartScheduler() != pdFALSE )
    {
        /* 调度器启动成功,则不会返回,不会来到这里; */
    }
}
2.5.4.1.2 xPortStartScheduler()
/*
 * 在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 configKERNEL_INTERRUPT_PRIORITY 255   /* 高四位有效,即等于0xff,或者是15 */

#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 的中断优先级为最低; 
       SysTick 和PendSV 都会涉及到系统调度,系统调度的优先级要低于系统的其它硬件中断优先级,
       即优先使用相应系统中的外部硬件中断,所以 SysTick 和 PendSV 的中断优先级配置为最低。*/
	portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;
	portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;

	/* 启动第一个任务,不再返回; */
	prvStartFirstTask();

	/* 不应该运行到这里; */
	return 0;
}
2.5.4.1.3 prvStartFirstTask()

prvStartFirstTask() 函数用于开始第一个任务。

主要做了两个动作:

  1. 更新 MSP 的值;
  2. 产生 SVC 系统调用,然后去到 SVC 的中断服务函数里面真正切换到第一个任务;
/*
 * 参考资料《STM32F10xxx Cortex-M3 programming manual》4.4.3,百度搜索 “PM0056” 即可找到这个文档
 * 在Cortex-M中,内核外设SCB的地址范围为:0xE000ED00-0xE000ED3F
 * 0xE000ED008为SCB外设中SCB_VTOR这个寄存器的地址,里面存放的是向量表的起始地址,即MSP的地址
 */
__asm void prvStartFirstTask( void )
{
    /* 栈需按照 8 字节对齐,如果都是 32 位的操作则 4 个字节对齐即可;
       在 Cortex-M 中浮点运算是 8 字节的; */
	PRESERVE8

	/* 在 Cortex-M 中,0xE000ED08 是 SCB_VTOR 这个寄存器的地址,
       里面存放的是向量表的起始地址,即 MSP 的地址;
       向量表通常是从内部 FLASH 的起始地址开始存放,
       那么可知 memory:0x00000000 处存放的就是 MSP 的值; */
	ldr r0, =0xE000ED08     /* 将 0xE000ED08 这个立即数加载到寄存器 R0; */
	ldr r0, [r0]            /* 将 0xE000ED08 这个地址指向的内容加载到寄存器 R0,此时 R0
                               等于 SCB_VTOR 寄存器的值,等于 0x00000000,
                               即 memory 的起始地址; */
	ldr r0, [r0]            /* 将 0x00000000 这个地址指向的内容加载到 R0,此时 R0 等于
                               0x20000578; */ 

	/* 设置主堆栈指针msp的值 */
	msr msp, r0             /* 将 R0 的值存储到 MSP,这是主堆栈的栈顶指针;*/
                            /* 起始这一步操作有点多余,因为当系统启动的时候,
                               执行完 Reset_Handler 的时候,向量表已经初始化完毕,
                               MSP 的值就已经更新为向量表的起始值,即指向主堆栈的栈顶指针; */
    
	/* 使能全局中断 */
    /* 为了快速地开关中断,Cortex-M 内核 专门设置了一条 CPS 指令,有 4 种用法:
       CPSID I  ;PRIMASK = 1    关中断;
       CPSIE I  ;PRIMASK = 0    开中断;
       CPSID F  ;FAULTMASK = 1  关异常;
       CPSIE I  ;FAULTMASK = 0  开异常;

       PRIMASK 和 FAULTMAST 是 Cortex-M 内核里面三个中断屏蔽寄存器中的两个,还有一个是 BASEPRI; */
	cpsie i                 /* 使用 CPS 指令把全局中断打开; */
	cpsie f
	dsb
	isb
	
    /* 调用 SVC 去启动第一个任务; */
	svc 0   /* 产生系统调用,服务号 0 表示 SVC 中断,接下来将会执行 SVC 中断服务函数; */
	nop
	nop
}
2.5.4.1.4 vPortSVCHandler()

SVC 中断要想被成功响应,其函数名必须与向量表注册的名称一致,在启动文件的向量表中,SVC 的中断服务函数注册的名称是 SVC_Handler,所以 SVC 中断服务函数的名称应该写成 SVC_Handler,但是在 FreeRTOS 中,官方版本写的是 vPortSVCHandler()。

#define xPortPendSVHandler   PendSV_Handler
#define xPortSysTickHandler  SysTick_Handler
#define vPortSVCHandler      SVC_Handler

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

__asm void vPortSVCHandler( void )
{
    extern pxCurrentTCB;    /* 声明外部变量 pxCurrentTCB,用于指向当前正在运行或者即将要运行的任务的任务控制块;*/<++>
    
    PRESERVE8

	ldr r3, =pxCurrentTCB   /* 加载 pxCurrentTCB 的地址到 r3; */
	ldr r1, [r3]			/* 加载 pxCurrentTCB 到 r1; */
	ldr r0, [r1]			/* 加载 pxCurrentTCB 指向的值到 r0,目前 r0 的值等于第一个任务堆栈的栈顶; */
	ldmia r0!, {r4-r11} 	/* 以 r0 为基地址,将栈里面的内容加载到 r4 ~ r11 寄存器,同时 r0 会递增; */
	msr psp, r0 			/* 将 r0 的值,即任务的栈指针更新到 psp,任务执行的时候使用的堆栈指针是 psp; */
	isb
	mov r0, #0              /* 设置 r0 的值为 0; */
	msr basepri, r0         /* 设置 basepri 寄存器的值为 0,即所有的中断都没有被屏蔽; 
                               basepri 是一个中断屏蔽寄存器,大于等于此寄存器值的中断都将被屏蔽; */
	orr r14, #0xd           /* 当从 SVC 中断服务退出前,通过向 r14 寄存器最后 4 位按位或上 0x0D,
                               使得硬件在退出时使用进程堆栈指针 PSP 完成出栈操作并返回后进入线程模式、返回 Thumb 状态; 
                               在 SVC 中断服务里面,使用的是 MSP 堆栈指针,是处在 ARM 状态; */
    
	bx r14                  /* 异常返回,这个时候栈中的剩下内容将会自动加载到 CPU 寄存器:
                               xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0(任务的形参),
                               同时PSP的值也将更新,即指向任务栈的栈顶; */
}
2.5.4.2 任务切换

任务切换就是在就绪列表中寻找优先级最高的就绪任务,然后去执行该任务。

2.5.4.2.1 taskYIELD()

stm32中响应优先级的设置的数值代表什么

#define taskYIELD() portYIELD()

/* 中断控制状态寄存器: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 );
}

实际就是将 PendSV 的悬起位置 1,当没有其它中断运行的时候响应 PendSV 中断,去执行我们写好的 PendSV 中断服务函数,在里面实现任务切换。

2.5.4.2.2 xPortPendSVHandler()

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

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

    extern vTaskSwitchContext;  /* 声明外部函数 vTaskSwitchContext;*/

	PRESERVE8

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

    /* 当进入PendSVC Handler时,上一个任务运行的环境即:
       xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0(任务的形参)
       这些CPU寄存器的值会自动保存到任务的栈中,剩下的 r4~r11 需要手动保存; */

    /* 获取任务栈指针到 r0; */

	mrs r0, psp                 /* 将 PSP 的值存储到 r0;*/
	isb

	ldr r3, =pxCurrentTCB       /* 加载pxCurrentTCB的地址到 r3; */
	ldr r2, [r3]                /* 加载pxCurrentTCB指向的内容到 r2; */

	stmdb r0!, {r4-r11} 		/* 将CPU寄存器 r4~r11 的值存储到 r0 指向的地址; */

    /* 以 r0 作为基址(指针先递减,再操作,STMDB 的 DB 表示 Decrease Befor),
       将 CPU 寄存器 r4~r11 的值存储到任务栈,同时更新 r0 的值;*/

	str r0, [r2]                /* 将任务栈的新的栈顶指针存储到当前任务 TCB 的第一个成员,即栈顶指针; */				

    /* 将 r0 的值存储到 r2 指向的内容,r2 等于 pxCurrentTCB;
       具体为将 r0 的值存储到上一个任务的栈顶指针 pxTopOfStack;
       到此,上下文切换中的上文保存就完成了;*/

	stmdb sp!, {r3, r14}        /* 将 R3 和 R14 临时压入堆栈,因为即将调用函数 vTaskSwitchContext,
                                   调用函数时,返回地址自动保存到 R14 中,所以一旦调用发生,R14 的值会被覆盖,因此需要入栈保护;
                                   (PendSV 中断服务函数执行完毕后,返回的时候需要根据 R14 的值来决定返回处理器模式还是任务模式,
                                   出栈时使用的是 PSP 还是 MSP)
                                   R3 保存的当前激活的任务(准确来说是上文,因为接下来即将要切换到新的任务) TCB 指针
                                   (pxCurrentTCB)地址,函数调用后会用到,因此也要入栈保护; 
                                   函数调用后 pxCurrentTCB 的值会被更新,后面我们还需要通过 R3 来操作 pxCurrentTCB,但是运行函数
                                   vTaskSwitchContext 时不确定会不会使用 R3 寄存器作为中间变量,所以为了保险起见,R3 也入栈保护
                                   起来;*/
	
    mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY    /* 进入临界段; */
                                
    /* 将 configMAX_SYSCALL_INTERRUPT_PRIORITY 的值存储到 r0,该宏在 FreeRTOSConfig.h 中定义,用来配置中断屏蔽寄存器 BASEPRI 
       的值,高四位有效。目前配置为 191,因为是高四位有效,所以实际值等于 11,即优先级高于或者等于 11 的中断都将被屏蔽。在关中
       断方面,FreeRTOS 与其它的 RTOS 关中断不同,而是操作 BASEPRI 寄存器来预留一部分中断,并不像 μC/OS 或者 RT-Thread 那样直
       接操作 PRIMASK 把所有中断都关闭掉(除了硬 FAULT);*/

	msr basepri, r0             /* 关中断,进入临界段;*/
	dsb
	isb
	bl vTaskSwitchContext       /* 调用函数 vTaskSwitchContext,寻找新的任务运行,通过使变量 pxCurrentTCB 指向新的任务来实现任务切换; */ 
	
    mov r0, #0                  /* 退出临界段; */
	msr basepri, r0             /* 开中断,直接往 BASEPRI 写 0;*/
	ldmia sp!, {r3, r14}        /* 从主堆栈中恢复寄存器 r3 和 r14 的值,此时的 sp 使用的是 MSP; */

	ldr r1, [r3]                /* 加载 r3 指向的内容到 r1;r3 存放的是 pxCurrentTCB 的地址,即让 r1 等于 pxCurrentTCB。pxCurrentTCB 
                                   在上面的 vTaskSwitchContext 函数中被更新,指向了下一个将要运行的任务的 TCB;*/
	ldr r0, [r1]                /* 加载 r1 指向的内容到 r0,即下一个要运行的任务的栈顶指针;
                                   当前激活的任务 TCB 第一项保存了任务堆栈的栈顶,现在栈顶值存入 R0;*/
	ldmia r0!, {r4-r11}         /* 出栈;
                                   以 r0 作为基地址(先取值,再递增指针,LDMIA 的 IA 表示 Increase After),将下一个要运行的任务的任务
                                   栈的内容加载到 CPU 寄存器 r4~r11;*/
	msr psp, r0                 /* 更新 psp 的值,等下异常退出时,会以 psp 作为基地址,将任务栈中剩下的内容自动加载到 CPU 寄存器。*/
	isb
	bx r14                      /* 异常发生时,R14 中保存异常返回标志,包括返回后进入线程模式还是处理器模式、
                                   使用 PSP 堆栈指针还是 MSP 堆栈指针,当调用 bx r14 指令后,硬件会知道要从异常返回,
                                   然后出栈,这个时候堆栈指针 PSP 已经指向了新任务堆栈的正确位置,
                                   当新任务的运行地址被出栈到 PC 寄存器后,新的任务也会被执行。*/
	nop
}
2.5.4.2.3 vTaskSwitchContext()
void vTaskSwitchContext( void )
{    
    /* 两个任务轮流切换; */

    if( pxCurrentTCB == &Task1TCB )
    {
        pxCurrentTCB = &Task2TCB;
    }
    else
    {
        pxCurrentTCB = &Task1TCB;
    }
}

2.6 主函数

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(;;)
	{
		/* 系统启动成功不会到达这里; */
	}
}

2.7 实现现象

任务切换

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

解琛

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值