/* 数据类型重定义 */
#define portCHAR char
#define portFLOAT float
#define portDOUBLE double
#define portLONG long
#define portSHORT short
#define portSTACK_TYPE uint32_t
#define portBASE_TYPE long
typedef portSTACK_TYPE StackType_t;
typedef long BaseType_t;
typedef unsigned long UBaseType_t;
#if( configUSE_16_BIT_TICKS == 1 )
typedef uint16_t TickType_t;
#define portMAX_DELAY ( TickType_t ) 0xffff
#else
typedef uint32_t TickType_t;
#define portMAX_DELAY ( TickType_t ) 0xffffffffUL
#endif
均储存在list.c中
预先定义好的全局数据,数据类型为StackType_t [uint32_t]
大小由 TASK1_STACK_SIZE 这个宏来定义, 默认为 128,单位为字,即 512字节 [最小的任务栈]
#define TASKn_STACK_SIZE 128
StackType_t TasknStack[TASKn_STACK_SIZE];
任务是一个独立的函数,函数主体无限循环且不能返回
void Taskn_Entry( void *p_arg )
{
for( ;; )
{
//不能返回
}
}
系统为了顺利的调度任务,为每个任务都额外定义了一个任务控制块,
这个任务控制块就相当于任务的身份证,里面存有任务的所有信息,
比如任务的栈指针,任务名称, 任务的形参等
typedef struct tskTaskControlBlock
{
volatile StackType_t *pxTopOfStack; /* 栈顶 */
ListItem_t xStateListItem; /* 任务节点 */
StackType_t *pxStack; /* 任务栈起始地址 */
char pcTaskName[ configMAX_TASK_NAME_LEN ]; /* 任务名称,字符串形式 */
TickType_t xTicksToDelay; /* 用于阻塞延时 */
UBaseType_t uxPriority; /* 任务优先级 */
} tskTCB;
typedef tskTCB TCB_t;
指向任务控制块的指针
任务创建好之后,我们需要把任务添加到就绪列表里面, 表示任务已经就绪,系统随时可以调度 。
就绪列表实际上就是一个 List_t 类型的数组,数组的大小由最大任务优先级的宏configMAX_PRIORITIES 决 定, configMAX_PRIORITIES 在FreeRTOSConfig.h 中默认定义为 5,最大支持 256 个优先级
调度器是操作系统的核心,主要功能是实现任务切换,即从就绪列表里找到优先级最高的任务,再去执行该任务。
从代码上来看,调度器是由几个全局变量和一些实现任务切换的函数组成,全部都在 task.c 文件中实现。
TaskHandle_t xTaskCreateStatic(
TaskFunction_t pxTaskCode, /* 任务入口 */
const char * const pcName, /* 任务名称,字符串形式 */
const uint32_t ulStackDepth, /* 任务栈大小,单位为字 */
void * const pvParameters, /* 任务形参 */
UBaseType_t uxPriority, /* 任务优先级,数值越大,优先级越高 */
StackType_t * const puxStackBuffer, /* 任务栈起始地址 */
TCB_t * const pxTaskBuffer ) /* 任务控制块指针 */
返回任务句柄,如果任务创建成功,此时xReturn应该指向任务控制块
此函数在xTaskCreateStatic()中被调用,初始化新任务函数
static void prvInitialiseNewTask(
TaskFunction_t pxTaskCode, /* 任务入口 */
const char * const pcName, /* 任务名称,字符串形式 */
const uint32_t ulStackDepth, /* 任务栈大小,单位为字 */
void * const pvParameters, /* 任务形参 */
UBaseType_t uxPriority, /* 任务优先级,数值越大,优先级越高 */
TaskHandle_t * const pxCreatedTask, /* 任务句柄 */
TCB_t *pxNewTCB ) /* 任务控制块 */
无返回值
此函数在prvInitialiseNewTask() 中被调用,初始化任务栈函数
StackType_t *pxPortInitialiseStack( StackType_t *pxTopOfStack, /* 栈顶指针 */
TaskFunction_t pxCode, /* 任务入口 */
void *pvParameters ) /* 任务形参 */
返回栈顶指针
/* 任务就绪列表 */
List_t pxReadyTasksLists[ configMAX_PRIORITIES ];
/* 任务延时列表 */
static List_t xDelayedTaskList1;
static List_t xDelayedTaskList2;
static List_t * volatile pxDelayedTaskList;
static List_t * volatile pxOverflowDelayedTaskList;
FreeRTOS 定义了两个任务延时列表,
当系统时基计数器xTickCount 没有溢出时,用一条列表,当 xTickCount 溢出后, 用另外一条列表。
pxDelayedTaskList指向 xTickCount 没有溢出时使用的那条列表。
pxOverflowDelayedTaskList指向 xTickCount 溢出时使用的那条列表。
创建空闲任务,插入就绪列表。
调用函数 xPortStartScheduler() 启动调度器, 调度器启动成功, 则不会返回。
初始化xNextTaskUnblockTime 【延时任务解锁时刻】
配置 PendSV 和 SysTick 的中断优先级为最低, 即优先相应系统中的外部硬件中断。
调用函数 prvStartFirstTask()启动第一个任务, 启动成功后, 则不再返回。
更新 MSP 的值,产生 SVC 系统调用
在SVC 的中断服务函数里面真正切换到第一个任务
SVC 中断要想被成功响应,其函数名必须与向量表注册的名称一致,
在启动文件的向量表中, SVC 的中断服务函数注册的名称是 SVC_Handler,
所以 SVC 中断服务函数的名称我们应该写成 SVC_Handler
#define xPortPendSVHandler PendSV_Handler
#define xPortSysTickHandler SysTick_Handler
#define vPortSVCHandler SVC_Handler
在PendSV中断中去实现任务的切换
将 PendSV 的悬起位置 1,当没有其它中断运行的时候响应 PendSV 中断
真正实现任务切换的地方
选择优先级最高的任务,然后更新 pxCurrentTCB。
pxCurrentTCB为指向当前正在执行的任务的指针。
一段在执行的时候不能被中断的代码段,临界段最常出现的就是对全局变量的操作。
有两种情况会造成临界段被打断,是任务调度和外部中断,保护临界段要避免这两种情况。
任务调度在FreeRTOS中本质为PendSV中断,所以对FreeRTOS 对临界段的保护本质是对中断的开和关的控制。
名字 | 功能描述 |
---|---|
PRIMASK | 被置1后,关掉所有可屏蔽中断,只响应NMI(非可屏蔽中断)和HARDFAULT(硬件中断) |
FAULTMASK | 被置1后,只有NMI(非可屏蔽中断)可以被响应 |
BASEPRI | 最多9位寄存器,中断优先级号大于此寄存器的中断都将并屏蔽 (优先级号越大,优先级越低) |
分为带返回值和不带返回值两种,
共同点:
两个函数中写入BASEPRI寄存器的均为宏值configMAX_SYSCALL_INTERRUPT_PRIORITY
该宏默认定义为 191,高四位有效,即等于 0xb0,或者是 11,优先级大于等于 11 的中断都会被屏蔽, 11 以内的中断则不受 FreeRTOS 管理 。
不同点:
不带返回值的关中断函数,不能在中断里面使用
带返回值的关中断函数,可以在中断里面使用,与带中断保护的开中断配合,可以保护正在运行的中断
在往 BASEPRI 写入新的值的时候,先将 BASEPRI 的值保存起来,在更新完BASEPRI 的值的时候,将之前保存好的 BASEPRI 的值返回,返回的值作为形参传入开中断函数。
这样的话,可以避免在中断时调用临界段代码后,导致当前中断因为关、开函数参数失误而无法运行。
分为带中断保护和不带中断保护两种,
底层函数相同,为:
void vPortSetBASEPRI( uint32_t ulBASEPRI )
不带中断保护的开中断函数, 直接将 BASEPRI 的值设置为 0,与portDISABLE_INTERRUPTS()成对使用。
带中断保护的开中断函数, 将上一次关中断时保存的 BASEPRI 的值作为形参 ,与portSET_INTERRUPT_MASK_FROM_ISR()成对使用。
#define taskENTER_CRITICAL() portENTER_CRITICAL()
#define taskENTER_CRITICAL_FROM_ISR() portSET_INTERRUPT_MASK_FROM_ISR()
#define taskEXIT_CRITICAL() portEXIT_CRITICAL()
#define taskEXIT_CRITICAL_FROM_ISR( x ) portCLEAR_INTERRUPT_MASK_FROM_ISR( x )
vPortRaiseBASEPRI() 无返回值—关中断函数
ulPortRaiseBASEPRI() 有返回值—关中断函数
vPortSetBASEPRI( 0 ) 不带中断保护—开中断函数
vPortSetBASEPRI(x) 带中断保护—开中断函数
#define portDISABLE_INTERRUPTS() vPortRaiseBASEPRI()
#define portSET_INTERRUPT_MASK_FROM_ISR() ulPortRaiseBASEPRI()
portDISABLE_INTERRUPTS() 无返回值—关中断函数
portSET_INTERRUPT_MASK_FROM_ISR() 有返回值—关中断函数
带FROM_ISR的一般是有返回值的数
#define portENABLE_INTERRUPTS() vPortSetBASEPRI( 0 )
#define portCLEAR_INTERRUPT_MASK_FROM_ISR(x) vPortSetBASEPRI(x)
portENABLE_INTERRUPTS() 不带中断保护—开中断函数
portCLEAR_INTERRUPT_MASK_FROM_ISR(x) 带中断保护—开中断函数
/* ==========进入临界段, 不带中断保护版本,不能嵌套=============== */
#define taskENTER_CRITICAL() portENTER_CRITICAL()
#define portENTER_CRITICAL() vPortEnterCritical()
/* ==========退出临界段,不带中断保护版本,不能嵌套=============== */
#define taskEXIT_CRITICAL() portEXIT_CRITICAL()
#define portEXIT_CRITICAL() vPortExitCritical()
/* ==========进入临界段,带中断保护版本,可以嵌套=============== */
#define taskENTER_CRITICAL_FROM_ISR() portSET_INTERRUPT_MASK_FROM_ISR()
#define portSET_INTERRUPT_MASK_FROM_ISR() ulPortRaiseBASEPRI()
/* ==========退出临界段,带中断保护版本,可以嵌套=============== */
#define taskEXIT_CRITICAL_FROM_ISR( x ) portCLEAR_INTERRUPT_MASK_FROM_ISR( x )
#define portCLEAR_INTERRUPT_MASK_FROM_ISR(x) vPortSetBASEPRI(x)
/* 在中断场合,临界段可以嵌套 */
{
uint32_t ulReturn;
/* 进入临界段,临界段可以嵌套 */
ulReturn = taskENTER_CRITICAL_FROM_ISR();
/* 临界段代码 */
/* 退出临界段 */
taskEXIT_CRITICAL_FROM_ISR( ulReturn );
}
/*在非中断场合,临界段不能嵌套 */
{
/* 进入临界段 */
taskENTER_CRITICAL();
/* 临界段代码 */
/* 退出临界段*/
taskEXIT_CRITICAL();
}
RTOS 都会为 CPU 创建一个空闲任务,这个时候 CPU 就运行空闲任务。 在FreeRTOS 中,空闲任务是系统在
【启动调度器】 的时候创建的优先级最低的任务,空闲任务主体主要是做一些系统内存的清理工作。
在实际应用中,当系统进入空闲任务的时候, 可在空闲任务中让单片机进入休眠或者低功耗等操作
阻塞延时的阻塞是指任务调用该延时函数后, 任务会被剥离 CPU 使用权,然后进入阻塞状态。
直到延时结束, 任务重新获取 CPU 使用权才可以继续运行。
在任务阻塞的这段时间, CPU 可以去执行其它的任务。
如果其它的任务也在延时状态,那么 CPU 就将运行空闲任务。
在任务切换函数 vTaskSwitchContext()中,会判断每个任务的任务控制块中的延时成员 xTicksToDelay 的值是否为 0,如果为 0就要将对应的任务就绪, 如果不为 0 就继续延时。
在FreeRTOS 中, xTicksToDelay周期由 SysTick 中断提供,操作系统里面的最小的时间单位就是SysTick 的中断周期,我们称之为一个 tick。
void vTaskDelay( const TickType_t xTicksToDelay );
更新系统时基计数器 xTickCount 【+1操作】
扫描就绪列表中所有任务的 xTicksToDelay ,如果不为 0,则减 1
如果系统时基计数器 xTickCount 溢出,则切换延时列表
SysTick初始化函 数vPortSetupTimerInterrupt() ,在xPortStartScheduler()中被调用
SysTick 的时钟与内核时钟一致
#define configCPU_CLOCK_HZ (( unsigned long ) 25000000)
#define configTICK_RATE_HZ (( TickType_t ) 100)
/* 设置重装载寄存器的值 */
portNVIC_SYSTICK_LOAD_REG = ( configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ ) - 1UL;
pxCurrenTCB 是一个全局的 TCB 指针,用于指向优先级最高的就绪任务的 TCB,即当前正在运行的 TCB
利用前导零计算指令(__clz)可以很快计算出就绪任务中的最高优先级为:
( 31UL - ( uint32_t ) __clz( ( uxReadyPriorities ) ) ) = ( 31UL - ( uint32_t )6 ) = 25
将优先级高的任务就绪标志,存在uxReadyPriorities的高位上
uxTopReadyPriority 是一个在 task.c 中定义的静态变量,默认初始化为 0
#define portRECORD_READY_PRIORITY( uxPriority, uxReadyPriorities )\
( uxReadyPriorities ) |= ( 1UL << ( uxPriority ) )
#define portRESET_READY_PRIORITY( uxPriority, uxReadyPriorities )\
( uxReadyPriorities ) &= ~( 1UL << ( uxPriority ) )
portRECORD_READY_PRIORITY( uxPriority, uxReadyPriorities )
用于根据传进来的形参(通常形参就是任务的优先级) 将变量uxTopReadyPriority 的某个位置 1
portRESET_READY_PRIORITY( uxPriority, uxReadyPriorities )
用于根据传进来的形参(通常形参就是任务的优先级)将变量 uxTopReadyPriority 的某个位清零
taskSELECT_HIGHEST_PRIORITY_TASK()
用于寻找优先级最高的就绪任务, 实质就是更新 uxTopReadyPriority 和 pxCurrentTCB 的值
portGET_HIGHEST_PRIORITY()
根据 uxTopReadyPriority 的值, 找到最高优先级, 然后更新到uxTopPriority 这个局部变量中
#define portGET_HIGHEST_PRIORITY( uxTopPriority, uxReadyPriorities )\
uxTopPriority = ( 31UL - ( uint32_t ) __clz( ( uxReadyPriorities ) ) )
任务延时后,将处于延时期间的任务从就绪列表中删除,添加到延时列表,避免在每个时基中断中需要对所有任务都扫描一遍 。在 FreeRTOS 中, 有延时列表和溢出延时列表,当延时列表被延时任务填满后,之后的延时任务将填入溢出延时列表中。
当任务需要延时的时候, 则先将任务挂起,即先将任务从就绪列表删除,然后插入到任务延时列表,同时更新下一个任务的解锁时刻变量: xNextTaskUnblockTime 的值。
xNextTaskUnblockTime 的值等于系统时基计数器的值 xTickCount 加上任务需要延时的值 xTicksToDelay
当xTickCount 与 xNextTaskUnblockTime 相等时,表示有任务延时到期,需要将该任务就绪。
任务延时列表维护着一条双向链表,每个节点代表了正在延时的任务,节点按照延时时间大小做升序排列。
/* 任务延时列表 */
static List_t xDelayedTaskList1;
static List_t xDelayedTaskList2;
static List_t * volatile pxDelayedTaskList;
static List_t * volatile pxOverflowDelayedTaskList;
FreeRTOS 定义了两个任务延时列表,当系统时基计数器xTickCount 没有溢出时,用一条列表,当 xTickCount 溢出后, 用另外一条列表。
pxDelayedTaskList指向 xTickCount 没有溢出时使用的那条列表。
pxOverflowDelayedTaskList指向 xTickCount 溢出时使用的那条列表。
xNextTaskUnblockTime 在 vTaskStartScheduler()函数中初始化为 portMAX_DELAY
portMAX_DELAY 是一个 portmacro.h 中定义的宏,默认为 0xffffffffUL
#define taskSWITCH_DELAYED_LISTS()\
{\
List_t *pxTemp;\ (1)
pxTemp = pxDelayedTaskList;\
pxDelayedTaskList = pxOverflowDelayedTaskList;\
pxOverflowDelayedTaskList = pxTemp;\
xNumOfOverflows++;\
prvResetNextTaskUnblockTime();\ (2)
}
切换延时列表 ,实 际 就 是 更 换 pxDelayedTaskList 和pxOverflowDelayedTaskList 这两个指针的指向 。
在 同 一 个 优 先 级 下 可 以 有 多 个 任 务 , 最 终 还 是 得 益 于**taskRESET_READY_PRIORITY()**和 **taskSELECT_HIGHEST_PRIORITY_TASK()**这两个函函数的实现方法。
每个时间片tick只能运行一个任务,即任务切换的周期是1个tick。
在**taskSELECT_HIGHEST_PRIORITY_TASK()中被调用的的listGET_OWNER_OF_NEXT_ENTRY()**函数,每被调用一次,就绪序列的 节点遍历指针 pxIndex 则会向后移动一次,用于指向下一个节点,就实现了同一优先级下不同就绪任务之间的切换。
比如,优先级 2 下有两个任务,当系统第一次切换到优先级为 2 的任务(包含了任务 1 和任务 2,因为它们的优先级相同) 时, pxIndex 指向任务 1, 任务 1 得到执行。 当任务 1 执行完毕,系统重新切换到优先级为 2 的任务时, 这个时候 pxIndex 指向任务 2,任务 2 得到执行, 任务 1 和任务 2 轮流执行,享有相同的 CPU 时间, 即所谓的时间片。
此清除优先级位图表uxTopReadyPriority 中相应的位时候,会先判断当前优先级链表下是否还有其它任务,如果有则不清零。
比如任务 1 会调用 vTaskDelay(),会将自己挂起,只能是将任务 1 从就绪列表删除,不能将任务 1 在优先级位图表 uxTopReadyPriority 中对应的位清 0,因为该优先级下还有任务 2,否则任务 2 将得不到执行
程序(program)只是一组指令的有序集合。
**任务(task)是最抽象的,是一个一般性的术语,指由软件完成的一个活动。一个任务既可以是一个进程,也可以是一个线程。**简而言之,它指的是一系列共同达到某一目的的操作。例如,读取数据并将数据放入内存中。这个任务可以作为一个进程来实现,也可以作为一个线程(或作为一个中断任务)来实现。
**进程(process)常常被定义为程序的执行。**可以把一个进程看成是一个独立的程序,在内存中有其完备的数据空间和代码空间。一个进程所拥有的数据和变量只属于它自己。
进程是表示资源分配的基本单位,又是调度运行的基本单位。例如,用户运行自己的程序,系统就创建一个进程,并为它分配资源,包括各种表格、内存空间、磁盘空间、I/O设备等。然后,把该进程放人进程的就绪队列。进程调度程序选中它,为它分配CPU以及其它有关资源,该进程才真正运行。所以,进程是系统中的并发执行的单位。
**一个进程和一个线程最显著的区别是:线程有自己的全局数据。线程存在于进程中,因此一个进程的全局变量由所有的线程共享。**由于线程共享同样的系统区域,操作系统分配给一个进程的资源对该进程的所有线程都是可用的,正如全局数据可供所有线程使用一样
线程是进程中执行运算的最小单位,亦即执行处理机调度的基本单位。如果把进程理解为在逻辑上操作系统所完成的任务,那么线程表示完成该任务的许多可能的子任务之一。
例如,假设用户启动了一个窗口中的数据库应用程序,操作系统就将对数据库的调用表示为一个进程。假设用户要从数据库中产生一份工资单报表,并传到一个文件中,这是一个子任务;在产生工资单报表的过程中,用户又可以输人数据库查询请求,这又是一个子任务。这样,操作系统则把每一个请求――工资单报表和新输人的数据查询表示为数据库进程中的独立的线程。线程可以在处理器上独立调度执行,这样,在多处理器环境下就允许几个线程各自在单独处理器上进行。操作系统提供线程就是为了方便而有效地实现这种并发性。
在FreeRTOS中对线程、进程没有明显区分,统一称为任务。
一般来说Cortex-M系列有两种工作模式:
Thread Mode (线程模式):程序按照编译好的代码顺序执行
Handler Mode(中断模式):收到中断信号并执行中断处理函数
Cortex-M系列内核使用了双堆栈,即MSP和PSP
MSP : Main_Stack_Pointer 主栈
PSP : Process_Stack_Pointer 任务栈
SP : 堆栈指针,指向最后一个被压入元素的地址
压栈:SP先自减4,然后将待压入的数据存放到SP所指的地址
弹栈:从SP指针所指的地址读出数据,然后SP指针自增4
任何时刻只能使用MSP和PSP其中的一个。
根据CONTROL寄存器的bit1来决定的。
CONTROL寄存器bit1 | SP寄存器 |
---|---|
0 | MSP(默认方式) |
1 | PSP |
CONTROL的bit1为0,SP = MSP
CONTROL的bit1为1,SP = PSP
复位后处于线程模式特权级,默认使用MSP。
在裸机开发中,CONTROL的bit1始终是0,也就是说裸机开发中全程使用程MSP,并没有使用PSP。在执行后台程序(大循环程序)SP使用的是MSP,在执行前台程序(中断服务程序)SP使用的是MSP。
在OS开发中,当运行中断服务程序的时候CONTROL的bit1是0,SP使用的是MSP;当运行线程程序的时候CONTROL的bit1是1,SP使用的是PSP。
FreeRTOS并没有使用SVC异常输入不同的参数,做不同的功能处理。FreeRTOS只是在首次进入任务时调用了SVC异常,且只使用了一次。
PendSV主要用于任务切换,即保存当前任务状态保存和提取下一个任务的状态。在每次SYSTICK异常发生时都会使用指令触发PendSV。当然,也可以在线程模式下主动触发PendSV,进行任务切换。
ARM架构中有一些通用寄存器R0-R15等,C编译器对C函数编译,编译后的汇编会使用到这些通用寄存器。
这些寄存器分成了两类
调用者保存寄存器(caller saved registers):R0-R3,R12,LR,PSR
被调用者保存寄存器(callee-saved registers):R4-R11
调用者调用函数之前,程序随意使用R4-R11,但要在进入函数之前将R0-R3,R12,LR,PSR保存起来,在函数返回后将调用者保存的寄存器R0-R3,R12,LR,PSR恢复还原。
被调用函数中,函数中程序随意使用R0-R3,R12,LR,PSR,但在函数开始是时要将R4-R11保存起来,在函数返回之前将调用者保存的寄存器R4-R11恢复还原。
带浮点型运算单元的Cortex-M4内核还有额外寄存器需要处理。
调用者保存的寄存器包括:S0-S15
被调用者保存的寄存器包括:S16-S31
R13(SP)和R15(PC):
SP在常规的C函数中当然是保存当前的栈地址,不管是调用函数前还是在被调用函数中都要用到SP。使用的栈空间没有变化,所以SP也不用保存了。
在函数调用前,把LR先压入栈中,然后把当前PC传给LR。这样在函数返回时,就会把LR赋值给PC,进而恢复到函数调用前的运行位置。
处理方式和一般C函数的处理过程类似。
进入中断前,要把(R0-R3,R12,LR,PSR)保存起来,然后在中断结束后恢复它们,这都是通过硬件完成的。
不同的是,中断的返回地址并没有存储在LR中。也就是说,中断过程中不但要保存(R0-R3,R12,LR,PSR),还要保存中断返回地址(return address)。中断的硬件机制会把EXC_RETURN放进LR,在中断返回时返回。
LR在进入中断后通过硬件更新为EXC_RETURN
EXC_RETURN为中断返回提供了更多的必要信息,如上表所示。
图8.8是嵌套压栈的过程。
**第一步:**程序在线程模式(Thread Mode)下运行,并使用程序栈(PSP)。
**第二步:**中断来临后,把寄存器压入栈中。但是使用的是哪个栈,以及压入的是8个字还是26个字,返回的是中断模式(Handler mode)还是线程模式(Thread mode)这部分硬件自动回检测相应寄存器,并生成对应EXC_RETURN填入到LR中。因为使用的是程序栈(PSP),PSP先自减,然后把相应的寄存器压栈。这部分是硬件完成的。
**第三步:**执行中断服务函数,中断服务函数使用的是主栈(main stack)
**第四步:**有了更高优先级的任务后,第三步的中断服务函数也会被打断。此时,使用主栈保存寄存器的状态。也会依据当前的状态来生成相应的EXC_RETURN,以便下次返回
**第五步:**执行嵌套的中断服务函数
中断过程中设置LR为EXC_RETURN
出栈操作
出栈操作,其实和入栈是个相反的过程。在退出中断时,使用哪里的数据(MSP或者PSP)恢复寄存器,返回的栈帧模式(带不带FPU,即8个字还是26个字),返回的是线程模式(Thread Mode)还是中断模式(Handler Mode),都是由LR寄存器中的EXC_RETURN决定的。
所以在FreeRTOS的任务切换过程中修改了LR中的EXC_RETURN,以切换处理器到期望的状态。
部分参考:https://www.jianshu.com/p/52841b514868
部分参考:野火教程