FreeRTOS笔记(二)—静态任务
一、任务定义
在多任务系统中,我们根据功能的不同,把整个系统分割 成一个个独立的且无法返回的函数,这个函数我们称为任务。
二、任务创建
2.1 定义任务栈
写一个 RTOS,对于全局变量与局部变量这些种种环境参数,我们必须弄清楚他们是如何存储的。
在裸机系统 中,他们统统放在一个叫栈的地方,栈是单片机 RAM 里面一段连续的内存空间,栈的大小一般在启动文件或者链接脚本里面指定,最后由C库函数_main进行初始化。
在多任务系统中,每个任务都是独立的,互不干扰的,所以要为每个任务都分配独立的栈空间,这个栈空间通常是一个预先定义好的全局数组,也可以是动态分配的一段内存空间,但它们都存在于RAM中。
多任务系统中,有多少个任务就需要定义多少个任务栈。
示例 1 定义任务栈
/*
#define portSTACK_TYPE uint32_t
typedef portSTACK_TYPE StackType_t;
*/
#define TASK1_STACK_SIZE 128
StackType_t Task1Stack[TASK1_STACK_SIZE];
#define TASK2_STACK_SIZE 128
StackType_t Task2Stack[TASK2_STACK_SIZE];
2.2 定义任务函数
任务是一个独立的函数,函数主体无限循环且不能返回。
示例 2 任务函数定义
/* 软件延时 */
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 );
}
}
/* 任务2 */
void Task2_Entry( void *p_arg )
{
for( ;; )
{
flag2 = 1;
delay( 100 );
flag2 = 0;
delay( 100 );
}
}
2.3 定义任务控制块
-
在裸机系统中,程序的主体是 CPU 按照顺序执行的。而在多任务系统中,任务的执行是由系统调度的。
-
系统为了顺利的调度任务,为每个任务都额外定义了一个任务控制块, 这个任务控制块就相当于任务的身份证,里面存有任务的所有信息,比如任务的栈指针, 任务名称,任务的形参等。
-
有了这个任务控制块之后,以后系统对任务的全部操作都可以通过这个任务控制块来实现。
示例 3 任务控制块数据类型声明
typedef struct tskTaskControlBlock
{
volatile StackType_t *pxTopOfStack; /* 栈顶 */
ListItem_t xStateListItem; /* 任务节点 一个内置在 TCB 控制块中的链表节点,通过 这个节点,可以将任务控制块挂接到各种链表中。这个节点就类似晾衣架的钩子,TCB 就是衣服。*/
StackType_t *pxStack; /* 任务栈起始地址 */
char pcTaskName[ configMAX_TASK_NAME_LEN ]; /* 任务名称,字符串形式 */
} tskTCB;
typedef tskTCB TCB_t;
2.4 实现任务创建函数
示例 4 任务创建函数xTaskCreateStatic()函数
/*
//任务句柄void指针
typedef void * TaskHandle_t;
//TaskFunction_t类型的pxTaskCode为函数指针,指向任务函数的入口。任务永远不会返回(位于死循环内)。
typedef void (*TaskFunction_t)( void * );//参数为void指针类型并返回void类型。
//
#define portSTACK_TYPE uint32_t
typedef portSTACK_TYPE StackType_t;
*/
TaskHandle_t xTaskCreateStatic( TaskFunction_t pxTaskCode, /* 任务入口 */
const char * const pcName, /* 任务名称,字符串形式 ,字符串的最大长度(包括字符串结束字符)由宏configMAX_TASK_NAME_LEN指定,该宏位于FreeRTOSConfig.h文件中。*/
const uint32_t ulStackDepth, /* 任务栈大小,单位为字,不是字节数,在16位宽度的堆栈下,usStackDepth定义为100,则实际使用200字节堆栈存储空间。1字=2字节=16位 */
void * const pvParameters, /* 任务形参,当任务创建时,作为一个参数传递给任务。*/
StackType_t * const puxStackBuffer, /* 任务栈起始地址 */
TCB_t * const pxTaskBuffer ) /* 任务控制块指针 */
{
TCB_t *pxNewTCB;
TaskHandle_t xReturn;//定义一个任务句柄 xReturn,任务句柄用于指向任务的 TCB。
/*任务控制块指针与任务栈起始地址均非空时,形参赋值*/
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;
}
示例 5 创建新任务prvInitialiseNewTask()函数
/*
typedef unsigned long UBaseType_t;
*/
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 );
/* 向下做8字节对齐 ,在 Cortex-M3(Cortex-M4 或 Cortex-M7)内核的单片机中,因为总线宽度是 32 位的,通常只要栈保持 4 字节对齐就行, 可这样为啥要 8字节?难道有哪些操作是 64位的?确实有,那就是浮点运算,所以要8字节对齐(但是目前都还没有涉及到浮点运算,只是为了后续兼容浮点运行的考虑)。如果栈顶指针是8字节对齐的,在进行向下8字节对齐的时候,指针不会移动,如果不是8字节对齐的,在做向下8字节对齐的时候,就会空出几个字节,不会使用,比如当 pxTopOfStack是 33,明显不能整除 8,进行向下 8字节对齐就是 32,那么就会空出一个字
节不使用。*/
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节点,即初始化该节点所在的链 表为空,表示节点还没有插入任何链表。*/
vListInitialiseItem( &( pxNewTCB->xStateListItem ) );
/* 设置xStateListItem节点的拥有者 */
listSET_LIST_ITEM_OWNER( &( pxNewTCB->xStateListItem ), pxNewTCB );
/* 调用 pxPortInitialiseStack()函数初始化任务栈,并更新栈顶指针, 任务第一次运行的环境参数就存在任务栈中。 */
pxNewTCB->pxTopOfStack = pxPortInitialiseStack( pxTopOfStack, pxTaskCode, pvParameters );
/* 让任务句柄指向任务控制块 */
if( ( void * ) pxCreatedTask != NULL )
{
*pxCreatedTask = ( TaskHandle_t ) pxNewTCB;
}
}
示例 6 初始化任务栈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自动从栈中加载到CPU寄存器的内容,包括 8 个寄存器,分别为R0、R1、R2、R3、R12、R14、R15和 xPSR的位 24,且顺序不能变。 */
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指向空闲栈,此时 pxTopOfStack指向具体见图 1。任务第一次运行时,就是从这个栈指针开始手动加载 8 个字的内容到 CPU 寄存器:R4、R5、R6、R7、 R8、R9、R10和 R11,当退出异常时,栈中剩下的 8个字的内容会自动加载到 CPU寄存器: R0、R1、R2、R3、R12、R14、R15和 xPSR的位 24。此时 PC指针就指向了任务入口地址,从而成功跳转到第一个任务。*/
return pxTopOfStack;
}
图 1 任务栈初始化完后栈空间分布图
三、实现就绪列表
3.1 定义就绪列表
任务创建好之后,我们需要把任务添加到就绪列表里面,表示任务已经就绪,系统随时可以调度。
/* 任务就绪列表 */
List_t pxReadyTasksLists[ configMAX_PRIORITIES ];
/* 就绪列表实际上就是一个 List_t 类型的数组,数组的大小由决定 最大任务优先级的宏 configMAX_PRIORITIES 决定, configMAX_PRIORITIES 在 FreeRTOSConfig.h 中默认定义为 5,最大支持 256 个优先级。数组的下标对应了任务的优 先级,同一优先级的任务统一插入到就绪列表的同一条链表中。一个空的就绪列表具体见图 2。 */
图 2 空的就绪列表
3.2 就绪列表初始化
就绪列表在使用前需要先初始化,就绪列表初始化的工作在函数 prvInitialiseTaskLists() 里面实现。
示例 7 就绪列表初始化prvInitialiseTaskLists()函数
/* 初始化任务相关的列表(遍历初始化链表)*/
void prvInitialiseTaskLists( void )
{
UBaseType_t uxPriority;
for( uxPriority = ( UBaseType_t ) 0U; uxPriority < ( UBaseType_t ) configMAX_PRIORITIES; uxPriority++ )
{
vListInitialise( &( pxReadyTasksLists[ uxPriority ] ) );
}
}
**图 3 就绪列表初始化完毕之后示意图 **
3.3 将任务插入到就绪列表
示例 8 将任务插入到就绪列表
/* 初始化与任务相关的列表,如就绪列表 */
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 任务插入到就绪列表下标为 1 的链表中, Task2 任务插入到就绪列表下标为 2 的链表中,具体的示意图见图 4。 */
图 4 任务插入到就绪列表示意图
四、实现调度器
- 调度器是操作系统的核心,其主要功能就是实现任务的切换,即从就绪列表里面找到优先级最高的任务,然后去执行该任务。从代码上来看,调度器无非也就是由几个全局变量和一些可以实现任务切换的函数组成。
4.1 启动调度器
调度器的启动由 vTaskStartScheduler()函数来完成。
示例 9 启动任务调度vTaskStartScheduler()函数
void vTaskStartScheduler( void )
{
/* 手动指定第一个运行的任务 */
pxCurrentTCB = &Task1TCB;
/* 启动调度器 */
if( xPortStartScheduler() != pdFALSE )
{
/* 调度器启动成功,则不会返回,即不会来到这里 */
}
}
示例 10 启动调度器
/*
*************************************************************************
* 调度器启动函数
*************************************************************************
*/
/*
* 参考资料《STM32F10xxx Cortex-M3 programming manual》4.4.3,百度搜索“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 的中断优先级为最低:SysTick 和PendSV 都会涉及到系统调度,系统调度的优先级要低于系统的其它硬件中断优先级, 即优先相应系统中的外部硬件中断, 所以 SysTick 和 PendSV 的中断优先级配置为最低。*/
portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;
portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;
/* 启动第一个任务,不再返回 */
prvStartFirstTask();
/* 不应该运行到这里 */
return 0;
}
示例 11 启动第一个任务 prvStartFirstTask()函数
/*
* 参考资料《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 的值。 这个可以通过仿真时查看内存的值证实*/
/* 将 0xE000ED08 这个立即数加载到寄存器 R0。 */
ldr r0, =0xE000ED08
/* 将 0xE000ED08 这个地址指向的内容加载到寄存器 R0,此时 R0 等于 SCB_VTOR 寄存器的值, 等于 0x00000000,即 memory 的起始地址。 */
ldr r0, [r0]
/* 将 0x00000000 这个地址指向的内容加载到 R0 (此时R0 等于0x200008DB)*/
ldr r0, [r0]
/* 设置主堆栈指针msp的值:将 R0 的值存储到 MSP,此时 MSP 等于 0x200008DB,这是主堆栈的栈顶指针。起 始这一步操作有点多余,因为当系统启动的时候,执行完 Reset_Handler的时候, 向量表已经初始化完毕, MSP 的值就已经更新为向量表的起始值,即指向主堆栈的栈顶指针。*/
msr msp, r0
/* 使能全局中断:使用 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,详细可见表 1
*/
cpsie i
cpsie f
dsb
isb
/* 调用SVC去启动第一个任务:产生系统调用,服务号 0 表示 SVC 中断,接下来将会执行 SVC 中断服务函数。*/
svc 0
nop
nop
}
表 1 Cortex-M 内核中断屏蔽寄存器组描述
名字 |
功能描述 |
PRIMASK |
这是个只有单一比特的寄存器。 在它被置 1 后,就关掉所有可屏蔽的异常, 只剩下 NMI 和硬 FAULT 可以响应。它的缺省值是 0,表示没有关中断。 |
FAULTMASK |
这是个只有 1 个位的寄存器。当它置 1 时,只有 NMI 才能响应,所有其它的 异常,甚至是硬 FAULT,也通通闭嘴。它的缺省值也是 0,表示没有关异 常。 |
BASEPRI |
这个寄存器最多有 9 位( 由表达优先级的位数决定)。它定义了被屏蔽优先 级的阈值。当它被设成某个值后,所有优先级号大于等于此值的中断都被关 (优先级号越大,优先级越低)。但若被设成 0,则不关闭任何中断, 0 也是 缺省值。 |
示例 12 SVC中断服务vPortSVCHandler()函数
SVC 中断要想被成功响应,其函数名必须与向量表注册的名称一致,在启动文件的向量表中, SVC 的中断服务函数注册的名称是 SVC_Handler, 所以 SVC 中断服务函数的名称我们应该写成 SVC_Handler。
但是在 FreeRTOS 中,官方版本写的是 vPortSVCHandler(),为了能够顺利的响应 SVC 中断,有两个选择:
/* 修改 FreeRos 中 SVC、 PendSV 和 SysTick 中断服务函数的名称 */
#define xPortPendSVHandler PendSV_Handler
#define xPortSysTickHandler SysTick_Handler
#define vPortSVCHandler SVC_Handler
vPortSVCHandler()函数开始真正启动第一个任务,不再返回。
__asm void vPortSVCHandler( void )
{
extern pxCurrentTCB; /* 声明外部变量 pxCurrentTCB, pxCurrentTCB 是一个在 task.c 中定
义的全局指针,用于指向当前正在运行或者即将要运行的任务的任务控制块。 */
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状态 在 SVC 中断服务里面,使用的是 MSP 堆栈指针, 是处在 ARM 状态*/
bx r14 /* 异常返回,这个时候栈使用的是 PSP 指针,栈中的剩下内容将会自动加载到CPU 寄存器:
xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0(任务的形参)
同时PSP的值也将更新,即指向任务栈的栈顶 见图 5*/
}
图 5 第一个任务启动成功后, psp 的指向
4.2 任务切换
任务切换就是在就绪列表中寻找优先级最高的就绪任务,然后去执行该任务。但是目前配置还不支持优先级,因此通过任务切换函数 taskYIELD() 仅实现两个任务轮流切换。
示例 12 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,产生上下文切换:将 PendSV 的悬起位置 1,当没有其它中断运行的时候响应 PendSV 中断,去执行我们写好的 PendSV 中断服务函数,在里面实现任务切换。*/ \
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; \
__dsb( portSY_FULL_READ_WRITE ); \
__isb( portSY_FULL_READ_WRITE ); \
}
示例 13 PendSV中断服务xPortPendSVHandler()函数
__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需要手动保存,同时PSP 会自动更新(在更新之前 PSP 指向任务栈的栈顶) */
/* 获取任务栈指针到r0 ,此时PSP具体指向见图 6*/
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,
调用函数时,返回地址自动保存到R14中,所以一旦调用发生,R14的值会被覆 盖,因此需要入栈保护;
R3保存的当前激活的任务TCB指针(pxCurrentTCB)地址,函数调用后会用 到,因此也要入栈保护 */
mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY /* 进入临界段 */
msr basepri, r0
dsb
isb
bl vTaskSwitchContext /* 调用函数vTaskSwitchContext,寻找新的任务运行,通过使变量 pxCurrentTCB指向新的任务来实现任务切换 这个位置栈顶指针变化*/
mov r0, #0 /* 退出临界段, 开中断,直接往 BASEPRI 写 0。 */
msr basepri, r0
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,即下一个要运行的任务的栈顶指针。 */
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
}
图 6 上一个任务的运行环境自动存储到任务栈后, psp 的指向
示例 14 vTaskSwitchContext()函数
/* 选择优先级最高的任务,然后更新 pxCurrentTCB。 */
void vTaskSwitchContext( void )
{
/* 两个任务轮流切换 */
if( pxCurrentTCB == &Task1TCB )
{
pxCurrentTCB = &Task2TCB;
}
else
{
pxCurrentTCB = &Task1TCB;
}
}
五、main函数
/*
*************************************************************************
* 包含的头文件
*************************************************************************
*/
#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 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;
/*
*************************************************************************
* 函数声明
*************************************************************************
*/
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()来实现任务的切换。 */
六、实验现象
6.1 软件仿真调试:
6.2 过程变量变化:
/* 初始化与任务相关的列表,如就绪列表 */
//prvInitialiseTaskLists();
/* 初始化任务相关的列表 */
void prvInitialiseTaskLists( void )
{
UBaseType_t uxPriority;
for( uxPriority = ( UBaseType_t ) 0U; uxPriority < ( UBaseType_t ) configMAX_PRIORITIES; uxPriority++ )//configMAX_PRIORITIES = 5
{
vListInitialise( &( pxReadyTasksLists[ uxPriority ] ) );
}
}
/* 创建任务 */
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 ) );
/* 将衣服(Task)通过衣架(xStateListItem)挂到衣圈(pxReadyTasksLists)上 */
/* 任务切换 */
#define portYIELD() \
{ \
/* 触发PendSV,产生上下文切换 */ \
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; \
__dsb( portSY_FULL_READ_WRITE ); \
__isb( portSY_FULL_READ_WRITE ); \
}
将栈顶值重新存入R0后出栈,实现任务切换