FreeRTOS
作者:解琛
时间:2020 年 8 月 18 日
二、任务的定义与切换
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() 函数用于开始第一个任务。
主要做了两个动作:
- 更新 MSP 的值;
- 产生 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()
#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(;;)
{
/* 系统启动成功不会到达这里; */
}
}