Skip to content

任务创建切换

把系统分割为一个个独立无法返回的函数, 函数称之为任务

  • 为每一个任务分配栈空间
c
/***********定义任务栈************/
#define TASK1_STACK_SIZE			20
StackType_t Task1Stack[TASK1_STACK_SIZE];
TCB_t TASK1_TCB;


#define TASK2_STACK_SIZE			20
StackType_t Task2Stack[TASK2_STACK_SIZE];
TCB_t TASK2_TCB;
/*************测试任务用的标志*************/
portCHAR flag1;
portCHAR flag2;

创建任务的PCB

c
typedef struct tskTaskControlBlock
{
	//栈顶
	volatile StackType_t	*pxTopOfStack;		
	
	//任务节点
	ListItem_t				xStateListItem;	
	StackType_t 			*pxStack; 		//任务栈的起始位置
	
	char 					pcTaskName[configMAX_TASK_NAME_LEN];
}tskTCB;

typedef tskTCB TCB_t;
  • 任务的创建-静态创建, 控制块内存事先定义好, 静态内存, 删除任务的时候不能释放
c
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);         /* 任务栈起始地址 */      

	}
	else
	{
		xReturn = NULL;
	}

	/* 返回任务句柄,如果任务创建成功,此时xReturn应该指向任务控制块 */
    return xReturn;
}
  • 任务的初始化
c
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字节对齐, 前三位直接置零 */
	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,最后加上结尾标志 */
	pxNewTCB->pcTaskName[ configMAX_TASK_NAME_LEN - 1 ] = '\0';

    /* 初始化TCB中的xStateListItem节点,这里是把所在的列表指向NULL */
    vListInitialiseItem( &( pxNewTCB->xStateListItem ) );
    /* 设置xStateListItem节点的拥有者 */
	listSET_LIST_ITEM_OWNER( &( pxNewTCB->xStateListItem ), pxNewTCB );
    
    
    /* 初始化任务栈, 填入栈中的CPU的寄存器的值 */
	pxNewTCB->pxTopOfStack = pxPortInitialiseStack( pxTopOfStack, pxTaskCode, pvParameters );   


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

硬件有关的函数初始化在port.c文件中

声明在portable.h文件中

image-20230710103937220

在初始化一个任务的时候, 需要实现的内容

c
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,函数返回地址 */
	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;
}

任务第一次运行的时候, 需要手动加载8个字的内容到CPU寄存器, 剩下的八个会自动加载, 从而完成任务的跳转

  • 创建任务列表
c
List_t pxReadTasksLists[configMAX_PRIORITIES];

这是根节点, 设置优先级

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

之后可以使用插入函数把任务插入列表

实现任务切换

c
extern TCB_t Task1TCB;
extern TCB_t Task2TCB;
void vTaskStartScheduler( void )
{
    /* 手动指定第一个运行的任务,这是一个全局变量指针, 指向正在运行的程序 */
    pxCurrentTCB = &Task1TCB;
    
    /* 启动调度器 */
    if( xPortStartScheduler() != pdFALSE )
    {
        /* 调度器启动成功,则不会返回,即不会来到这里 */
    }
}
c
BaseType_t xPortStartScheduler( void )
{
    /* 配置PendSV 和 SysTick 的中断优先级为最低, 直接控制寄存器实现 */
	portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;
	portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;

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

	/* 不应该运行到这里 */
	return 0;
}

PendSV(可悬起的系统调用),它是一种CPU系统级别的异常,它可以像普通外设中断一样被悬起,而不会像SVC服务那样,因为没有及时响应处理,而触发Fault。

SCV(系统服务调用)

在产生中断的时候, 如果产生了一个任务切换, 会在切换之后返回新的任务而不是中断函数, 导致出错, 还会导致任务处理不及时

如果把Systick优先级放在最低, 在期间处理任务, 会导致时间过长, 实时性降低

采用滴答定时器中断, 制作业务调度之前判断, 不切换, 触发PendSV, PendSV不会立刻执行, 应为优先级最低, 所有中断处理完之后执行PendSV执行任务调度

c
__asm void prvStartFirstTask( void )
{
	PRESERVE8

	/* 在Cortex-M中,0xE000ED08是SCB_VTOR这个寄存器的地址,
       里面存放的是向量表的起始地址,即MSP的地址 */
	ldr r0, =0xE000ED08
	ldr r0, [r0]			//这里加载的是内存的首地址
	ldr r0, [r0]			//首地址的内容加载到r0

	/* 设置主堆栈指针msp的值 */
	msr msp, r0			//设置主栈的栈顶指针
    
	/* 使能全局中断, 异常 */
	cpsie i
	cpsie f
	dsb
	isb
	
    /* 调用SVC去启动第一个任务 */
	svc 0  
	nop
	nop
}

手动启动一个任务

Cortex-M3中为了快速打开中断有四个指令

assembly
CPSID 	I	;关中断
CPSIE	I	;开中断
CPSID	F	;关异常
CPSIE	F	;开异常

使用了三个寄存器实现

PRIMASK: 设置为1以后关闭所有可屏蔽中断, 只剩下NMI和硬FSULT可以响应

FAULTMASK, 设置为1, 只有NMI可以响应

BASEPRI: 最多有九位, 设置为一个数, 优先级比他大的都屏蔽

  • **数据存储屏障(Data Memory Barrier,DMB)指令:**仅当所有在它前面的存储器访问操作都执行完毕后,才提交(commit)在它后面的访问指令。DMB指令保证的是DMB指令之前的所有内存访问指令和DMB指令之后的所有内存访问指令的执行顺序。也就是说,DMB指令之后的内存访问指令不会被处理器重排到DMB指令的前面。DMB指令不会保证内存访问指令在内存屏障指令之前完成,它仅仅保证内存屏障指令前后的内存访问的执行顺序。DMB指令仅仅影响内存访问指令、数据高速缓存指令以及高速缓存管理指令等,并不会影响其他指令(例如算术运算指令等)的顺序。
  • **数据同步屏障(Data Synchronization Barrier,DSB)指令:**比DMB指令要严格一些,仅当所有在它前面的内存访问指令都执行完毕后,才会执行在它后面的指令,即任何指令都要等待DSB指令前面的内存访问指令完成。位于此指令前的所有缓存(如分支预测和TLB维护)操作需要全部完成。
  • **指令同步屏障(Instruction Synchronization Barrier,ISB)指令:**确保所有在ISB指令之后的指令都从指令高速缓存或内存中重新预取。它刷新流水线(flush pipeline)和预取缓冲区后才会从指令高速缓存或者内存中预取ISB指令之后的指令。ISB指令通常用来保证上下文切换(如ASID更改、TLB维护操作等)的效果。
  • 可以使用SVC指令,然后执行操作系统中的软件异常处理程序,并提供用户应用程序请求的服务。
assembly
__asm void vPortSVCHandler( void )
{
    extern 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 */
	isb
	mov r0, #0              /* 设置r0的值为0 */
	msr	basepri, r0         /* 设置basepri寄存器的值为0,即所有的中断都没有被屏蔽 */
	orr r14, #0xd           /* 当从SVC中断服务退出前,通过向r14寄存器最后4位按位或上0x0D,
                               使得硬件在退出时使用进程堆栈指针PSP完成出栈操作并返回后进入线程模式、返回Thumb状态 */
    
	bx r14                  /* 异常返回,这个时候栈中的剩下内容将会自动加载到CPU寄存器:
                               xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0(任务的形参)
                               同时PSP的值也将更新,即指向任务栈的栈顶 */
}

msp: 主栈指针

psp: 进程栈指针

当R14的值为子程序链接寄存器, 保存返回地址(但是在中断中返回的时候使用的是EXC_RETURN数值)

他的bit1设置为0表示使用的栈指针是psp

bit2表示使用用户模式还是特权模式, 设置为1为用户模式

image-20240116190859171

image-20240116190921195

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

在这里声明为stm32的函数名字

实现切换

实际上就是把PandSV中断悬起, 当没有其他中断的时候, 就会响应这个中断

c
//这个是在portmacro.h文件中实现的, 目的是使能PendSV
/* 中断控制状态寄存器: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;								\
	__dsb( portSY_FULL_READ_WRITE );											\
	__isb( portSY_FULL_READ_WRITE );											\
}

__dsb和__isb的作用是

image-20240120184442790

c
//实现在task.h文件
#define taskYIELD()			portYIELD()
c
__asm void xPortPendSVHandler( void )
{
	extern pxCurrentTCB;			//指向现在运行的任务的指针
	extern vTaskSwitchContext;		//切换指向的函数

	PRESERVE8

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

	ldr	r3, =pxCurrentTCB		/* 加载pxCurrentTCB的地址到r3 */
	ldr	r2, [r3]                /* 加载pxCurrentTCB到r2 实际上就是栈顶的指针*/

	stmdb r0!, {r4-r11}			/* 将CPU寄存器r4~r11的值存储到r0指向的地址, 保存在任务的栈中 */
	str r0, [r2]                /* 将任务栈的新的栈顶指针存储到当前任务TCB的第一个成员,即栈顶指针 */				
                               

	stmdb sp!, {r3, r14}        /* 将R3和R14临时压入堆栈,因为即将调用函数vTaskSwitchContext, 这里由于在中断中所以所使用的栈是MSP
                                  调用函数时,返回地址自动保存到R14中,所以一旦调用发生,R14的值会被覆盖,因此需要入栈保护;
                                  R3保存的当前激活的任务TCB指针(pxCurrentTCB)地址,函数调用后会用到,因此也要入栈保护 */
	mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY    /* 进入临界段 */
	msr basepri, r0
	dsb
	isb
	bl vTaskSwitchContext       /* 调用函数vTaskSwitchContext,寻找新的任务运行,通过使变量pxCurrentTCB指向新的任务来实现任务切换 */ 
	mov r0, #0                  /* 退出临界段 */
	msr basepri, r0				//打开中断
	ldmia sp!, {r3, r14}        /* 恢复r3和r14 */

	ldr r1, [r3]				//r1保存当前的TCB, 这里的值已经更新
	ldr r0, [r1] 				/* 当前激活的任务TCB第一项保存了任务堆栈的栈顶,现在栈顶值存入R0*/
	ldmia r0!, {r4-r11}			/* 出栈, 恢复新任务中断之前的状态 */
	msr psp, r0					//栈的指针保存当前的R0的位置
	isb
	bx r14                      /* 异常发生时,R14中保存异常返回标志,包括返回后进入线程模式还是处理器模式、
                                   使用PSP堆栈指针还是MSP堆栈指针,当调用 bx r14指令后,硬件会知道要从异常返回,
                                   然后出栈,这个时候堆栈指针PSP已经指向了新任务堆栈的正确位置,
                                   当新任务的运行地址被出栈到PC寄存器后,新的任务也会被执行。*/
	nop
}

中断服务函数

image-20230710140941934

栈顶指针是会变化的

使用总结

首先初始化函数的TCB, 以及初始化函数的栈空间以及内容

之后在创建任务的时候把他归入列表之中

实现任务的创建, 并且调动第一个任务, 之后开启任务的切换

c
#include "list.h"
#include "task.h"
/***********定义任务栈以及TCB************/
#define TASK1_STACK_SIZE			20
StackType_t Task1Stack[TASK1_STACK_SIZE];
TCB_t TASK1_TCB;
#define TASK2_STACK_SIZE			20
StackType_t Task2Stack[TASK2_STACK_SIZE];
TCB_t TASK2_TCB;
/*************测试任务用的标志*************/
portCHAR flag1;
portCHAR flag2;

extern List_t pxReadyTasksLists[configMAX_PRIORITIES];

/************定义文件句柄***********/
TaskHandle_t Task1_Handle;
TaskHandle_t Task2_Handle;

/**************任务函数声明**************/
void delay(uint32_t count);
void Task1_Entry(void *p_arg);
void Task2_Entry(void *p_arg);

int main(void)
{
	prvInitialiseTaskLists();
	//创建任务
	Task1_Handle = xTaskCreateStatic(Task1_Entry, "Task1_Entry", TASK1_STACK_SIZE, NULL, Task1Stack, &TASK1_TCB);	
	vListInsert(&pxReadyTasksLists[1], &TASK1_TCB.xStateListItem);	
	
	Task1_Handle = xTaskCreateStatic(Task2_Entry, "Task2_Entry", TASK2_STACK_SIZE, NULL, Task2Stack, &TASK2_TCB);
	vListInsert(&pxReadyTasksLists[1], &TASK1_TCB.xStateListItem);	
	vTaskStartScheduler();

	for(;;)
	{
		/*空函数*/
	}
}
//定义一个函数,是一个无限的循环
void Task1_Entry(void *p_arg)
{

	for(;;)
	{
		flag1=1;
		delay(100);
		
		flag1=0;
		delay(100);
		portYIELD();
	}
}
void Task2_Entry(void *p_arg)
{

	for(;;)
	{
		flag2=1;
		delay(100);
		
		flag2=0;
		delay(100);
		//进行切换
		portYIELD();
	}
}
void delay(uint32_t count)
{
	for(;count !=0;count--);
}

实际FreeRTOS分析

c
BaseType_t xPortStartScheduler( void )
{
	#if( configASSERT_DEFINED == 1 )
	{
		volatile uint32_t ulOriginalPriority;
        //获取第一个用户的优先级设置的寄存器地址
		volatile uint8_t * const pucFirstUserPriorityRegister = ( uint8_t * ) ( portNVIC_IP_REGISTERS_OFFSET_16 + portFIRST_USER_INTERRUPT_NUMBER );
		volatile uint8_t ucMaxPriorityValue;
		//保存当前的优先级
		ulOriginalPriority = *pucFirstUserPriorityRegister;
		//向里面写入0xff
		*pucFirstUserPriorityRegister = portMAX_8_BIT_VALUE;
		//读取
		ucMaxPriorityValue = *pucFirstUserPriorityRegister;
		//断言判断一下设置的
		configASSERT( ucMaxPriorityValue == ( configKERNEL_INTERRUPT_PRIORITY & ucMaxPriorityValue ) );
        //计算一下前面的0的位数
		ulMaxPRIGROUPValue = portMAX_PRIGROUP_BITS;
		while( ( ucMaxPriorityValue & portTOP_BIT_OF_BYTE ) == portTOP_BIT_OF_BYTE )
		{
			ulMaxPRIGROUPValue--;
			ucMaxPriorityValue <<= ( uint8_t ) 0x01;
		}
        
		#ifdef configPRIO_BITS
		{
			/* 测试一下实际的设置宏定义对不对 */
			configASSERT( ( portMAX_PRIGROUP_BITS - ulMaxPRIGROUPValue ) == configPRIO_BITS );
		}
		#endif

		/* 记录一下实际的NVIC的优先级需要的的值*/
		ulMaxPRIGROUPValue <<= portPRIGROUP_SHIFT;
		ulMaxPRIGROUPValue &= portPRIORITY_GROUP_MASK;

		/* 返回之前的值 */
		*pucFirstUserPriorityRegister = ulOriginalPriority;
	}
	#endif /* conifgASSERT_DEFINED */

	/* 设置一下优先级 */
	portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;
	portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;

	/* 设置一下使用的时钟, 时钟源, 时钟的装载值等 */
	vPortSetupTimerInterrupt();

	/*`uxCriticalNesting`是临界区嵌套层数,FreeRTOS允许临界区嵌套,这个变量是为了避免在较低级别的临界区内部发生不必要的任务切换或中断处理,从而提高系统的可靠性和响应性。*/
	uxCriticalNesting = 0;

	/* 进入第一个任务 */
	prvStartFirstTask();

	/* Should not get here! */
	return 0;
}