整理
野火
《FreeRTOS 内核实现与应用开发实战指南
》---基于野火 STM32 全系列(M3/4/7)开发板
文章目录
- [第7章 任务的定义与任务切换的实现](#第7章 任务的定义与任务切换的实现)
-
- [7.1 本章目标](#7.1 本章目标)
- [7.2 什么是任务](#7.2 什么是任务)
- [7.3 创建任务](#7.3 创建任务)
-
- [7.3.1 定义任务栈](#7.3.1 定义任务栈)
- [7.3.2 定义任务函数](#7.3.2 定义任务函数)
- [7.3.3 定义任务控制块](#7.3.3 定义任务控制块)
- [7.3.4 实现任务创建函数](#7.3.4 实现任务创建函数)
-
- [1. xTaskCreateStatic()函数](#1. xTaskCreateStatic()函数)
- [2. prvInitialiseNewTask()函数](#2. prvInitialiseNewTask()函数)
- [3. pxPortInitialiseStack()函数](#3. pxPortInitialiseStack()函数)
- [7.4 实现就绪列表](#7.4 实现就绪列表)
-
- [7.4.1 定义就绪列表](#7.4.1 定义就绪列表)
- [7.4.2 就绪列表初始化](#7.4.2 就绪列表初始化)
- [7.4.3 将任务插入到就绪列表](#7.4.3 将任务插入到就绪列表)
- [7.5 实现调度器](#7.5 实现调度器)
-
- [7.5.1 启动调度器](#7.5.1 启动调度器)
-
- [1. vTaskStartScheduler()函数](#1. vTaskStartScheduler()函数)
- [2. xPortStartScheduler()函数](#2. xPortStartScheduler()函数)
- [PendSV(Pending Supervisor Call)扩展:](#PendSV(Pending Supervisor Call)扩展:)
-
- [1. PendSV 的作用](#1. PendSV 的作用)
- [2. 中断服务函数(ISR)](#2. 中断服务函数(ISR))
- [SysTick(System Tick Timer)扩展](#SysTick(System Tick Timer)扩展)
-
- [1. SysTick 的作用](#1. SysTick 的作用)
- [2. SysTick 的中断服务函数(ISR)](#2. SysTick 的中断服务函数(ISR))
- [3. prvStartFirstTask()函数](#3. prvStartFirstTask()函数)
-
- [`MSP` 和 `SVC` 详解](#
MSP
和SVC
详解)
- [`MSP` 和 `SVC` 详解](#
- [4. vPortSVCHandler()函数](#4. vPortSVCHandler()函数)
- [7.5.2 任务切换](#7.5.2 任务切换)
-
- [#define portYIELD()](#define portYIELD())
- [2. xPortPendSVHandler()函数](#2. xPortPendSVHandler()函数)
- [3. vTaskSwitchContext()函数](#3. vTaskSwitchContext()函数)
- [7.8 本章涉及到的汇编指令讲解](#7.8 本章涉及到的汇编指令讲解)
第7章 任务的定义与任务切换的实现
7.1 本章目标
本章是我们真正从从 0 到 1 写 FreeRTOS 的第一章,属于基础中的基础,必须要学会创建任务,并重点掌握任务是如何切换的。因为任务的切换是由汇编代码来完成的,所以代码看起来比较难懂,但是我会尽力把代码讲得透彻。如果本章内容学不会,后面的内容根本无从下手。
7.2 什么是任务
在裸机系统中,系统的主体就是 main 函数里面顺序执行的无限循环,这个无限循环里面 CPU 按照顺序完成各种事情。在多任务系统中,我们根据功能的不同,把整个系统分割成一个个独立的且无法返回的函数,这个函数我们称为任务。
c
void task_entry (void *parg)
{
/* 任务主体,无限循环且不能返回 */
for (;;)
{
/* 任务主体代码 */
}
}
7.3 创建任务
7.3.1 定义任务栈
在裸机系统
中,如果有全局变量,有子函数调用,有中断发生。那么系统在运行的时候,全局变量放在哪里,子函数调用时,局部变量放在哪里,中断发生时,函数返回地址放哪里。在裸机系统中,他们统统放在一个叫栈的地方,栈是单片机 RAM 里面一段连续的内存空间,栈的大小一般在启动文件或者链接脚本里面指定,最后由 C 库函数_main 进行初始化。
在多任务系统
中,每个任务都是独立的,互不干扰的,所以要为每个任务都分配独立的栈空间,这个栈空间通常是一个预先定义好的全局数组,也可以是动态分配的一段内存空间,但它们都存在于 RAM 中。
定义任务栈:
c
/* 定义任务1的堆栈大小为20个字 */
#define TASK1_STACK_SIZE 20
/* 声明一个大小为TASK1_STACK_SIZE的StackType_t类型的数组Task1Stack,用于存储任务1的堆栈数据 */
StackType_t Task1Stack[TASK1_STACK_SIZE];
/* 定义任务2的堆栈大小为20个字 */
#define TASK2_STACK_SIZE 20
/* 声明一个大小为TASK2_STACK_SIZE的StackType_t类型的数组Task2Stack,用于存储任务2的堆栈数据 */
StackType_t Task2Stack[TASK2_STACK_SIZE];
7.3.2 定义任务函数
任务是一个独立的函数,函数主体无限循环且不能返回。
c
/* 任务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();
}
}
7.3.3 定义任务控制块
在多任务系统
中,任务的执行是由系统调度的。系统为了顺利的调度任务,为每个任务都额外定义了一个任务控制块,这个任务控制块就相当于任务的身份证,里面存有任务的所有信息,比如任务的栈指针,任务名称,任务的形参等。有了这个任务控制块之后,以后系统对任务的全部操作都可以通过这个任务控制块来实现。
c
typedef struct tskTaskControlBlock
{
volatile StackType_t *pxTopOfStack; /* 栈顶 */
/*这是一个内置在 TCB 控制块中的链表节点,通过这个节点,可以将任务控制块挂接到各种链表中。*/
ListItem_t xStateListItem; /* 任务节点 */
StackType_t *pxStack; /* 任务栈起始地址 */
char pcTaskName[ configMAX_TASK_NAME_LEN ]; /* 任务名称,字符串形式 */
} tskTCB;
typedef tskTCB TCB_t;
7.3.4 实现任务创建函数
任务的栈
,任务的函数实体
,任务的控制块
最终需要联系起来才能由系统进行统一调度
。那么这个联系的工作就由任务创建函数来实现
。
1. xTaskCreateStatic()函数
FreeRTOS 中,任务的创建有两种方法,一种是使用动态创建,一种是使用静态创建。动态创建时,任务控制块和栈的内存是创建任务时动态分配的
,任务删除时,内存可以释放。静态创建时,任务控制块和栈的内存需要事先定义好
,是静态的内存,任务删除时,内存不能释放。
c
/*
* 静态创建任务函数
*
* 参数:
* pxTaskCode: 任务入口函数,即任务开始执行的地方
* pcName: 任务名称,以字符串形式表示
* ulStackDepth: 任务栈的大小,以字(通常是4字节)为单位
* pvParameters: 传递给任务的参数
* puxStackBuffer: 任务栈的起始地址
* pxTaskBuffer: 任务控制块(TCB)的指针
*
* 返回值:
* TaskHandle_t: 返回任务句柄,如果任务创建成功,返回的任务句柄指向新创建的任务控制块;如果任务创建失败,返回NULL
*/
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,用于指向新创建的任务控制块
TCB_t *pxNewTCB;
// 声明一个TaskHandle_t类型的变量xReturn,用于返回任务句柄
TaskHandle_t xReturn;
// 检查传入的任务控制块指针和任务栈指针是否为非空,如果其中任何一个为空,则任务创建失败
if( ( pxTaskBuffer != NULL ) && ( puxStackBuffer != NULL ) )
{
// 将传入的任务控制块指针赋值给pxNewTCB
pxNewTCB = ( TCB_t * ) pxTaskBuffer;
// 将传入的任务栈指针赋值给pxNewTCB的pxStack成员
pxNewTCB->pxStack = ( StackType_t * ) puxStackBuffer;
/* 创建新的任务 */
prvInitialiseNewTask( pxTaskCode, /* 任务入口 */
pcName, /* 任务名称,字符串形式 */
ulStackDepth, /* 任务栈大小,单位为字 */
pvParameters, /* 任务形参 */
&xReturn, /* 任务句柄 */
pxNewTCB); /* 任务控制块指针 */
}
else
{
// 如果传入的任务控制块指针或任务栈指针为空,则将xReturn设置为NULL,表示任务创建失败
xReturn = NULL;
}
/* 返回任务句柄,如果任务创建成功,此时xReturn应该指向任务控制块 */
return xReturn;
}
2. prvInitialiseNewTask()函数
c
/*
* 初始化新任务的函数
*
* 参数:
* pxTaskCode: 任务入口函数,即任务开始执行的地方
* pcName: 任务名称,以字符串形式表示
* ulStackDepth: 任务栈的大小,以字(通常是4字节)为单位
* pvParameters: 传递给任务的参数
* pxCreatedTask: 任务句柄的指针,用于返回任务句柄
* pxNewTCB: 任务控制块的指针,指向新创建的任务控制块
*
* 返回值:
* 无
*/
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,用于指向任务栈的栈顶
StackType_t *pxTopOfStack;
// 声明一个UBaseType_t类型的变量x,用于循环计数
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 指针指向的地址是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节点 */
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;
}
}
3. pxPortInitialiseStack()函数
c
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;
}
7.4 实现就绪列表
7.4.1 定义就绪列表
任务创建好之后,我们需要把任务添加到就绪列表里面,表示任务已经就绪,系统随时可以调度。
c
/* 任务就绪列表 */
List_t pxReadyTasksLists[ configMAX_PRIORITIES ];
就绪列表实际上就是一个 List_t 类型的数组,
数组的大小由决定最 大 任 务 优 先 级 的 宏 configMAX_PRIORITIES 决 定 , configMAX_PRIORITIES 在FreeRTOSConfig.h 中默认定义为 5,最大支持 256 个优先级。数组的下标对应了任务的优先级,同一优先级的任务统一插入到就绪列表的同一条链表中。
一个空的就绪列表具体见图 7-4。
7.4.2 就绪列表初始化
就绪列表在使用前需要先初始化,就绪列表初始化的工作在函数 prvInitialiseTaskLists()里面实现
c
/* 初始化任务相关的列表 */
void prvInitialiseTaskLists( void )
{
UBaseType_t uxPriority;
for( uxPriority = ( UBaseType_t ) 0U; uxPriority < ( UBaseType_t ) configMAX_PRIORITIES; uxPriority++ )
{
vListInitialise( &( pxReadyTasksLists[ uxPriority ] ) );
}
}
7.4.3 将任务插入到就绪列表
任务控制块里面有一个 xStateListItem 成员,数据类型为 ListItem_t,我们将任务插入到就绪列表里面,就是通过将任务控制块的 xStateListItem 这个节点插入到就绪列表中来实现的。
如果把就绪列表比作是晾衣架,任务是衣服,那 xStateListItem 就是晾衣架上面的钩子,每个任务都自带晾衣架钩子,就是为了把自己挂在各种不同的链表中。
c
/* 初始化与任务相关的列表,如就绪列表 */
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.5 实现调度器
调度器是操作系统的核心
,其主要功能就是实现任务的切换,即从就绪列表里面找到优先级最高的任务,然后去执行该任务。从代码上来看,调度器无非也就是由几个全局变量和一些可以实现任务切换的函数组成
,全部都在 task.c 文件中实现。
7.5.1 启动调度器
调度器的启动由 vTaskStartScheduler()
函数来完成,该函数在 task.c 中定义。
1. vTaskStartScheduler()函数
c
void vTaskStartScheduler( void )
{
/* 手动指定第一个运行的任务 */
pxCurrentTCB = &Task1TCB;
/* 启动调度器 */
if( xPortStartScheduler() != pdFALSE )
{
/* 调度器启动成功,则不会返回,即不会来到这里 */
}
}
pxCurrentTCB 是一个在 task.c 定义的全局指针,用于指向当前正在运行或者即将要运行的任务的任务控制块。目前我们还不支持优先级,则手动指定第一个要运行的任务。
2. xPortStartScheduler()函数
c
/*
* 启动调度器函数
*
* 返回值:
* BaseType_t: 返回0,表示调度器启动成功
*/
BaseType_t xPortStartScheduler( void )
{
/* 配置PendSV 和 SysTick 的中断优先级为最低 */
portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;
portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;
/* 启动第一个任务,不再返回 */
prvStartFirstTask();
/* 不应该运行到这里 */
return 0;
}
配置 PendSV
和 SysTick
的中断优先级为最低。SysTick
和 PendSV
都会涉及到系统调度,系统调度的优先级要低于系统的其它硬件中断优先级,即优先相应系统中的外部硬件中断,所以 SysTick
和 PendSV
的中断优先级配置为最低。
PendSV(Pending Supervisor Call)扩展:
1. PendSV 的作用
PendSV
的主要作用是触发任务切换。在 FreeRTOS
中,任务切换的时机通常由以下几种情况触发:
- 任务主动放弃 CPU:例如调用 vTaskDelay() 或 vTaskYield()。
- 时间片到期:SysTick 中断触发任务切换。
- 高优先级任务就绪:例如某个高优先级任务从阻塞状态变为就绪状态。
当需要切换任务时,系统会设置 PendSV
的 Pend
位,从而触发 PendSV
异常。
2. 中断服务函数(ISR)
PendSV 的中断服务函数(ISR)负责完成任务切换。它的主要步骤包括:
- 保存当前任务的上下文:将当前任务的寄存器状态保存到任务栈中。
- 选择下一个任务:通过调度器选择下一个要运行的任务。
- 恢复新任务的上下文:从新任务的栈中恢复寄存器状态。
- 切换到新任务:将新任务的栈指针加载到 PSP,然后返回到新任务的执行。
assembly
__asm void xPortPendSVHandler(void)
{
extern pxCurrentTCB;
PRESERVE8
/* 保存当前任务的上下文 */
mrs r0, psp
isb
ldr r3, =pxCurrentTCB
ldr r2, [r3]
stmdb r0!, {r4-r11}
str r0, [r2]
/* 调用调度器选择下一个任务 */
vTaskSwitchContext()
/* 获取新任务的 TCB */
ldr r1, [r3]
ldr r0, [r1]
/* 恢复新任务的上下文 */
ldmia r0!, {r4-r11}
msr psp, r0
isb
/* 切换到新任务 */
bx r14
}
SysTick(System Tick Timer)扩展
1. SysTick 的作用
sysTick
的主要作用包括:
- 提供时间基准:为任务调度提供时间片(tick)信号。
- 任务延时:实现任务的阻塞延时功能。
- 系统时钟更新:维护系统时钟(xTickCount),用于任务调度和时间管理。
2. SysTick 的中断服务函数(ISR)
SysTick
的中断服务函数(ISR)是任务调度的核心,其主要任务包括:
- 更新系统时钟:递增系统时钟 xTickCount。
- 检查任务延时:更新就绪列表,将延时到期的任务移入就绪列表。
- 触发任务切换 :如果需要切换任务,设置
PendSV
的Pend
位。
c
void xPortSysTickHandler(void)
{
/* 关闭中断 */
vPortRaiseBASEPRI();
/* 更新系统时钟 */
xTaskIncrementTick();
/* 检查是否需要任务切换 */
if (xTaskIncrementTick() != pdFALSE)
{
/* 触发 PendSV 中断,进行任务切换 */
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
}
/* 恢复中断 */
vPortClearBASEPRIFromISR();
}
3. prvStartFirstTask()函数
prvStartFirstTask()
函数用于开始第一个任务,主要做了两个动作,一个是更新 MSP
的值,二是产生 SVC
系统调用,然后去到 SVC
的中断服务函数里面真正切换到第一个任务。
c
/*
* 启动第一个任务的函数
*
* 返回值:
* 无
*/
__asm void prvStartFirstTask( void )
{
PRESERVE8
/* 在Cortex-M中,0xE000ED08是SCB_VTOR这个寄存器的地址,
里面存放的是向量表的起始地址,即MSP的地址 */
ldr r0, =0xE000ED08
ldr r0, [r0]
ldr r0, [r0]
/* 设置主堆栈指针msp的值 */
msr msp, r0
/* 使能全局中断 */
cpsie i
cpsie f
dsb
isb
/* 调用SVC去启动第一个任务 */
svc 0
nop
nop
}
代码清单 7-18(1)
:
代码清单 7-18(2)
:当前栈需按照 8 字节对齐,如果都是 32 位的操作则 4 个字节对齐即可。在 Cortex-M 中浮点运算是 8字节的。
代码清单 7-18(3)
:在 Cortex-M
中,0xE000ED08
是 SCB_VTOR
寄存器的地址,里面存放的是向量表的起始地址,即 MSP
的地址。向量表通常是从内部 FLASH
的起始地址开始存放,那么可知 memory:0x00000000
处存放的就是 MSP
的值。这个可以通过仿真时查看内存的值证实,具体见图 7-7。
代码清单 7-18(4)
:将 0xE000ED08
这个立即数加载到寄存器 R0
。
代码清单 7-18(5)
:将 0xE000ED08
这个地址指向的内容加载到寄存器 R0
,此时 R0
等于 SCB_VTOR
寄存器的值,等于0x00000000
,即 memory
的起始地址。
代码清单 7-18(6)
:将 0x00000000
这个地址指向的内容加载到 R0
,此时 R0
等于0x200008DB
,与图7-7 查询到的值吻合。
代码清单 7-18(7)
:将 R0
的值存储到 MSP
,此时 MSP
等于 0x200008DB
,这是主堆栈的栈顶指针。起始这一步操作有点多余,因为当系统启动的时候,执行完Reset_Handler
的时候,向量表已经初始化完毕,MSP
的值就已经更新为向量表的起始值,即指向主堆栈的栈顶指针。
代码清单 7-18(8)
:使用 CPS
指令把全局中断打开。为了快速地开关中断, Cortex-M
内核专门设置了一条 CPS
指令,有 4 种用法,具体见代码清单 7-19。
代码清单 7-19 中 PRIMASK
和 FAULTMAST
是 Cortex-M
内核 里面三个中断屏蔽寄存器中的两个,还有一个是 BASEPRI
,有关这三个寄存器的详细用法见表格8-1。
代码清单 7-18(9)
:产生系统调用,服务号 0
表示 SVC
中断,接下来将会执行 SVC
中断服务函数。
MSP
和 SVC
详解
在嵌入式系统中,MSP
和 SVC
是与任务调度和中断处理密切相关的重要概念。以下是它们的详细解析:
MSP
(Main Stack Pointer
,主堆栈指针)
- 定义:
MSP
是Cortex-M
架构中定义的主堆栈指针,用于系统级操作和中断处理。 - 作用:
- 在系统启动时,
MSP
被初始化为堆栈的起始地址。 - 在任务调度中,
MSP
用于存储任务的上下文信息,包括寄存器值和堆栈帧。 - 在
prvStartFirstTask()
函数中,通过操作MSP
,可以将堆栈指针重置到任务的初始堆栈位置,从而为第一个任务的运行做好准备。
- 在系统启动时,
SVC
(Supervisor Call
,系统调用)
- 定义:
SVC
是Cortex-M
架构中的一种异常类型,用于触发系统调用。 - 作用:
- 在操作系统中,
SVC
用于从用户模式切换到特权模式,执行系统服务。 - 在任务调度中,
SVC
异常被用来触发任务切换。当SVC
被触发时,处理器会跳转到SVC
中断服务函数(SVC_Handler
)。 - 在
prvStartFirstTask()
函数中,通过触发SVC
异常,可以进入SVC_Handler
,从而完成第一个任务的初始化和切换。
- 在操作系统中,
prvStartFirstTask()
函数的工作原理
- 更新
MSP
:- 通过读取向量表中的初始堆栈指针值,重新设置 MSP 的值。
- 这一步确保了任务的堆栈指针指向正确的初始位置。
- 触发
SVC
:- 调用
SVC
异常,进入SVC_Handler
。 - 在
SVC_Handler
中,完成任务的上下文加载和切换。
- 调用
SVC_Handler
的工作
- 在
SVC_Handler
中,操作系统会根据当前的任务控制块(TCB
)加载任务的上下文。 - 具体操作包括:
- 从任务的堆栈中恢复寄存器值。
- 切换到任务的堆栈指针。
- 返回到任务的入口函数。
通过上述机制,prvStartFirstTask()
和 SVC_Handler
协同工作,完成了第一个任务的启动和上下文切换。
4. vPortSVCHandler()函数
SVC
中断要想被成功响应,其函数名必须与向量表注册的名称一致,在启动文件的向量表中,SVC
的中断服务函数注册的名称是 SVC_Handler
,所以 SVC
中断服务函数的名称我们应该写成SVC_Handler
,但是在 FreeRTOS
中,官方版本写的是 vPortSVCHandler()
,为了能够顺利的响应 SVC
中断,我们有两个选择,改中断向量表中 SVC
的注册的函数名称或者改 FreeRTOS
中 SVC
的中断服务名称。这里,我们采取第二种方法,即在FreeRTOSConfig.h
中添加添加宏定义的方法来修改,具体见代码清单 7-20,顺便把PendSV
和 SysTick
的中断服务函数名也改成与向量表的一致。
vPortSVCHandler()
函数开始真正启动第一个任务,不再返回,实现具体见代码清单
7-21。
c
/*
* SVC中断服务程序,用于启动第一个任务
*
* 返回值:
* 无
*/
__asm void vPortSVCHandler( void )
{
// 声明pxCurrentTCB为外部变量,表示当前正在运行的任务的任务控制块(TCB)
extern pxCurrentTCB;
// 确保8字节对齐
PRESERVE8
// 将pxCurrentTCB的地址加载到r3寄存器
ldr r3, =pxCurrentTCB
// 将pxCurrentTCB的值加载到r1寄存器
ldr r1, [r3]
// 将pxCurrentTCB指向的值加载到r0寄存器,此时r0的值等于第一个任务堆栈的栈顶
ldr r0, [r1]
// 以r0为基地址,将栈里面的内容加载到r4~r11寄存器,同时r0会递增
ldmia r0!, {r4-r11}
// 将r0的值,即任务的栈指针更新到psp寄存器
msr psp, r0
// 指令同步屏障,确保前面的内存访问操作完成
isb
// 设置r0的值为0
mov r0, #0
// 设置basepri寄存器的值为0,即所有的中断都没有被屏蔽
msr basepri, r0
// 当从SVC中断服务退出前,通过向r14寄存器最后4位按位或上0x0D,使得硬件在退出时使用进程堆栈指针PSP完成出栈操作并返回后进入线程模式、返回Thumb状态
orr r14, #0xd
// 异常返回,这个时候栈中的剩下内容将会自动加载到CPU寄存器:xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0(任务的形参),同时PSP的值也将更新,即指向任务栈的栈顶
bx r14
}
代码清单 7-21(1)
:声明外部变量 pxCurrentTCB
,pxCurrentTCB
是一个在 task.c
中定义的全局指针,用于指向当前正在运行或者即将要运行的任务的任务控制块。
代码清单 7-21(2)
:加载 pxCurrentTCB
的地址到 r3
。
代码清单 7-21(3)
:加载 pxCurrentTCB
到 r3
。
代码清单 7-21(4)
:加载 pxCurrentTCB
指向的任务控制块到 r0
,任务控制块的第一个成员就是栈顶指针,所以此时 r0
等于栈顶指针。一个刚刚被创建还没有运行过的任务的栈
空间分布具体如图7-8 所示,即 r0
等于图7-8 的 pxTopOfStack
。
代码清单 7-21(5)
:以 r0
为基地址,将栈中向上增长的 8
个字的内容加载到 CPU
寄存器 r4~r11
,同时 r0
也会跟着自增。
代码清单 7-21(6)
:将新的栈顶指针 r0
更新到 psp
,任务执行的时候使用的堆栈指针是psp
。此时 psp
的指向具体见。
代码清单 7-21(7):将寄存器 r0
清 0。
代码清单 7-21(8):设置 basepri
寄存器的值为 0,即打开所有中断。basepri
是一个中断屏蔽寄存器,大于等于此寄存器值的中断都将被屏蔽。
代码清单 7-21(9):当从 SVC
中断服务退出前,通过向 r14
寄存器最后 4 位按位或上0x0D,使得硬件在退出时使用进程堆栈指针 PSP
完成出栈操作并返回后进入任务模式、返
回 Thumb
状态。在 SVC
中断服务里面,使用的是 MSP
堆栈指针,是处在 ARM
状态。
当 r14
为 0xFFFFFFFX
,执行是中断返回指令,cortext-m3
的做法,X
的 bit0
为 1 表示返回 thumb
状态,bit1
和 bit2
分别表示返回后 sp
用 msp
还是 psp
、以及返回到特权模式还是用户模式。
代码清单 7-21(10):异常返回,这个时候出栈使用的是 PSP
指针,自动将栈中的剩下内容加载到 CPU
寄存器: xPSR
,PC
(任务入口地址),R14
,R12
,R3
,R2
,R1
,R0
(任务的形参)同时 PSP 的值也将更新,即指向任务栈的栈顶,具体指向见图 7-9。
总结:
1、 指定当前任务控制块 pxCurrentTCB
。
2、 配置 PendSV
和 SysTick
的中断优先级为最低。
3、 启动第一个任务:两个动作,①更新MSP
的值 ②产生SVC
系统调用,去到 vPortSVCHandler
的中断服务函数里面真正切换到第一个任务。
7.5.2 任务切换
任务切换就是在就绪列表中寻找优先级最高的就绪任务,然后去执行该任务。但是目前我们还不支持优先级,仅实现两个任务轮流切换,任务切换函数 taskYIELD()
具体实现见代码清单 7-22
#define portYIELD()
c
#define portYIELD() \
{ \
/* 触发PendSV,产生上下文切换 */ \
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; \
__dsb( portSY_FULL_READ_WRITE ); \
__isb( portSY_FULL_READ_WRITE ); \
}
portYIELD
的实现很简单,实际就是将 PendSV
的悬起位置 1
,当没有其它中断运行的时候响应 PendSV
中断,去执行我们写好的 PendSV
中断服务函数,在里面实现任务切换。
2. xPortPendSVHandler()函数
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,
调用函数时,返回地址自动保存到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]
ldr r0, [r1] /* 当前激活的任务TCB第一项保存了任务堆栈的栈顶,现在栈顶值存入R0*/
ldmia r0!, {r4-r11} /* 出栈 */
msr psp, r0
isb
bx r14 /* 异常发生时,R14中保存异常返回标志,包括返回后进入线程模式还是处理器模式、
使用PSP堆栈指针还是MSP堆栈指针,当调用 bx r14指令后,硬件会知道要从异常返回,
然后出栈,这个时候堆栈指针PSP已经指向了新任务堆栈的正确位置,
当新任务的运行地址被出栈到PC寄存器后,新的任务也会被执行。*/
nop
}
3. vTaskSwitchContext()函数
c
void vTaskSwitchContext( void )
{
/* 两个任务轮流切换 */
if( pxCurrentTCB == &Task1TCB )
{
pxCurrentTCB = &Task2TCB;
}
else
{
pxCurrentTCB = &Task1TCB;
}
}
7.8 本章涉及到的汇编指令讲解