关于FreeRTOS的底层实现和基础认识

数据类型定义

/* 数据类型重定义 */
#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

前缀类型

关于FreeRTOS的底层实现和基础认识_第1张图片

链表操作

均储存在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( ;; )
	{
		//不能返回        
	}
}

任务控制块TCB

系统为了顺利的调度任务,为每个任务都额外定义了一个任务控制块,

这个任务控制块就相当于任务的身份证,里面存有任务的所有信息

比如任务的栈指针,任务名称, 任务的形参等

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 文件中实现。

相关函数

1-1 xTaskCreateStatic() 任务创建函数

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应该指向任务控制块

1-2 prvInitialiseNewTask() 初始化任务函数

此函数在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 )                       /* 任务控制块 */

无返回值

1-3 pxPortInitialiseStack() 初始化任务栈函数

此函数在prvInitialiseNewTask() 中被调用,初始化任务栈函数

StackType_t *pxPortInitialiseStack( StackType_t *pxTopOfStack,		/* 栈顶指针 */
									TaskFunction_t pxCode,			/* 任务入口 */
                                   	void *pvParameters )			/* 任务形参 */

返回栈顶指针

2-1 prvInitialiseTaskLists() 任务列表初始化函数

/* 任务就绪列表 */
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 溢出时使用的那条列表。

3-1 vTaskStartScheduler() 启动调度器函数

创建空闲任务,插入就绪列表。

调用函数 xPortStartScheduler() 启动调度器, 调度器启动成功, 则不会返回。

初始化xNextTaskUnblockTime 【延时任务解锁时刻】

3-2 xPortStartScheduler() 启动调度器函数

配置 PendSV 和 SysTick 的中断优先级为最低, 即优先相应系统中的外部硬件中断。

调用函数 prvStartFirstTask()启动第一个任务, 启动成功后, 则不再返回。

3-3 prvStartFirstTask() 开始首个任务函数

更新 MSP 的值,产生 SVC 系统调用

在SVC 的中断服务函数里面真正切换到第一个任务

3-4 vPortSVCHandler() SVC中断服务函数

SVC 中断要想被成功响应,其函数名必须与向量表注册的名称一致,

在启动文件的向量表中, SVC 的中断服务函数注册的名称是 SVC_Handler,

所以 SVC 中断服务函数的名称我们应该写成 SVC_Handler

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

4-1 taskYIELD() 任务切换函数

在PendSV中断中去实现任务的切换

将 PendSV 的悬起位置 1,当没有其它中断运行的时候响应 PendSV 中断

4-2 xPortPendSVHandler() PendSV中断服务函数

真正实现任务切换的地方

5-1 vTaskSwitchContext() 最高优先级任务选择函数

选择优先级最高的任务,然后更新 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 )  

相关函数

关(Raise)中断底层(BASEPRI)

vPortRaiseBASEPRI() 无返回值—关中断函数

ulPortRaiseBASEPRI() 有返回值—关中断函数

开(Set)中断底层(BASEPRI)

vPortSetBASEPRI( 0 ) 不带中断保护—开中断函数

vPortSetBASEPRI(x) 带中断保护—开中断函数

关中断port层

#define portDISABLE_INTERRUPTS() vPortRaiseBASEPRI()
#define portSET_INTERRUPT_MASK_FROM_ISR() ulPortRaiseBASEPRI()

portDISABLE_INTERRUPTS() 无返回值—关中断函数

portSET_INTERRUPT_MASK_FROM_ISR() 有返回值—关中断函数

FROM_ISR的一般是有返回值的数

开中断port层

#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 就将运行空闲任务。

SysTick 中断

​ 在任务切换函数 vTaskSwitchContext()中,会判断每个任务的任务控制块中的延时成员 xTicksToDelay 的值是否为 0,如果为 0就要将对应的任务就绪, 如果不为 0 就继续延时。

​ 在FreeRTOS 中, xTicksToDelay周期由 SysTick 中断提供,操作系统里面的最小的时间单位就是SysTick 的中断周期,我们称之为一个 tick。

相关函数

vTaskDelay() 延时设置函数

void vTaskDelay( const TickType_t xTicksToDelay );

xTaskIncrementTick() 系统时基更新函数

​ 更新系统时基计数器 xTickCount 【+1操作】

​ 扫描就绪列表中所有任务的 xTicksToDelay ,如果不为 0,则减 1

​ 如果系统时基计数器 xTickCount 溢出,则切换延时列表

vPortSetupTimerInterrupt() SysTick 初始化函数

​ 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

关于FreeRTOS的底层实现和基础认识_第2张图片

利用前导零计算指令(__clz)可以很快计算出就绪任务中的最高优先级为:

( 31UL - ( uint32_t ) __clz( ( uxReadyPriorities ) ) ) = ( 31UL - ( uint32_t )6 ) = 25

将优先级高的任务就绪标志,存在uxReadyPriorities的高位上

uxTopReadyPriority 是一个在 task.c 中定义的静态变量,默认初始化为 0

相关函数

uxTopReadyPriority置位

#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

prvAddCurrentTaskToDelayedList() 就绪移动到延时列表函数

taskSWITCH_DELAYED_LISTS() 延时列表切换函数

#define taskSWITCH_DELAYED_LISTS()\
{\
List_t *pxTemp;\ (1)
pxTemp = pxDelayedTaskList;\
pxDelayedTaskList = pxOverflowDelayedTaskList;\
pxOverflowDelayedTaskList = pxTemp;\
xNumOfOverflows++;\
prvResetNextTaskUnblockTime();\ (2)
}

切换延时列表 ,实 际 就 是 更 换 pxDelayedTaskList 和pxOverflowDelayedTaskList 这两个指针的指向 。

prvResetNextTaskUnblockTime() 解锁时刻值复位函数

时间片

在 同 一 个 优 先 级 下 可 以 有 多 个 任 务 , 最 终 还 是 得 益 于**taskRESET_READY_PRIORITY()**和 **taskSELECT_HIGHEST_PRIORITY_TASK()**这两个函函数的实现方法。

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 时间, 即所谓的时间片。

taskRESET_READY_PRIORITY()

此清除优先级位图表uxTopReadyPriority 中相应的位时候,会先判断当前优先级链表下是否还有其它任务,如果有则不清零。

比如任务 1 会调用 vTaskDelay(),会将自己挂起,只能是将任务 1 从就绪列表删除,不能将任务 1 在优先级位图表 uxTopReadyPriority 中对应的位清 0,因为该优先级下还有任务 2,否则任务 2 将得不到执行

基础概念

进程和线程

​ 程序(program)只是一组指令的有序集合。

​ **任务(task)是最抽象的,是一个一般性的术语,指由软件完成的一个活动。一个任务既可以是一个进程,也可以是一个线程。**简而言之,它指的是一系列共同达到某一目的的操作。例如,读取数据并将数据放入内存中。这个任务可以作为一个进程来实现,也可以作为一个线程(或作为一个中断任务)来实现。

​ **进程(process)常常被定义为程序的执行。**可以把一个进程看成是一个独立的程序,在内存中有其完备的数据空间和代码空间。一个进程所拥有的数据和变量只属于它自己。

​ 进程是表示资源分配的基本单位,又是调度运行的基本单位。例如,用户运行自己的程序,系统就创建一个进程,并为它分配资源,包括各种表格、内存空间、磁盘空间、I/O设备等。然后,把该进程放人进程的就绪队列。进程调度程序选中它,为它分配CPU以及其它有关资源,该进程才真正运行。所以,进程是系统中的并发执行的单位。

	**一个进程和一个线程最显著的区别是:线程有自己的全局数据。线程存在于进程中,因此一个进程的全局变量由所有的线程共享。**由于线程共享同样的系统区域,操作系统分配给一个进程的资源对该进程的所有线程都是可用的,正如全局数据可供所有线程使用一样

线程是进程中执行运算的最小单位,亦即执行处理机调度的基本单位。如果把进程理解为在逻辑上操作系统所完成的任务,那么线程表示完成该任务的许多可能的子任务之一。

例如,假设用户启动了一个窗口中的数据库应用程序,操作系统就将对数据库的调用表示为一个进程。假设用户要从数据库中产生一份工资单报表,并传到一个文件中,这是一个子任务;在产生工资单报表的过程中,用户又可以输人数据库查询请求,这又是一个子任务。这样,操作系统则把每一个请求――工资单报表和新输人的数据查询表示为数据库进程中的独立的线程。线程可以在处理器上独立调度执行,这样,在多处理器环境下就允许几个线程各自在单独处理器上进行。操作系统提供线程就是为了方便而有效地实现这种并发性。

在FreeRTOS中对线程、进程没有明显区分,统一称为任务。

关于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

关于FreeRTOS的底层实现和基础认识_第3张图片

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。

SVC异常和PendSV异常

​ FreeRTOS并没有使用SVC异常输入不同的参数,做不同的功能处理。FreeRTOS只是在首次进入任务时调用了SVC异常,且只使用了一次。
​ PendSV主要用于任务切换,即保存当前任务状态保存和提取下一个任务的状态。在每次SYSTICK异常发生时都会使用指令触发PendSV。当然,也可以在线程模式下主动触发PendSV,进行任务切换。

栈帧(stack frame)

子函数调用

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

关于FreeRTOS的底层实现和基础认识_第4张图片

EXC_RETURN为中断返回提供了更多的必要信息,如上表所示。

  • bit4,表明了压入的是8个字,还是26个字。因为带浮点运算单元和不带浮点运算单元是有区别的。
  • bit3,表明是返回到Thread模式还是Handler模式。也就是该中断之前是从线程模式进入的,还是从中断中进入的(中断嵌套)。
  • bit2 返回到哪个栈,是程序栈(Process Stack)还是主栈(Main Stack)。

进入中断和入栈

关于FreeRTOS的底层实现和基础认识_第5张图片

图8.8是嵌套压栈的过程。
**第一步:**程序在线程模式(Thread Mode)下运行,并使用程序栈(PSP)。
**第二步:**中断来临后,把寄存器压入栈中。但是使用的是哪个栈,以及压入的是8个字还是26个字,返回的是中断模式(Handler mode)还是线程模式(Thread mode)这部分硬件自动回检测相应寄存器,并生成对应EXC_RETURN填入到LR中。因为使用的是程序栈(PSP),PSP先自减,然后把相应的寄存器压栈。这部分是硬件完成的。
**第三步:**执行中断服务函数,中断服务函数使用的是主栈(main stack)
**第四步:**有了更高优先级的任务后,第三步的中断服务函数也会被打断。此时,使用主栈保存寄存器的状态。也会依据当前的状态来生成相应的EXC_RETURN,以便下次返回
**第五步:**执行嵌套的中断服务函数

中断返回和出栈

关于FreeRTOS的底层实现和基础认识_第6张图片

中断过程中设置LR为EXC_RETURN

关于FreeRTOS的底层实现和基础认识_第7张图片

出栈操作

出栈操作,其实和入栈是个相反的过程。在退出中断时,使用哪里的数据(MSP或者PSP)恢复寄存器,返回的栈帧模式(带不带FPU,即8个字还是26个字),返回的是线程模式(Thread Mode)还是中断模式(Handler Mode),都是由LR寄存器中的EXC_RETURN决定的。
所以在FreeRTOS的任务切换过程中修改了LR中的EXC_RETURN,以切换处理器到期望的状态。

部分参考:https://www.jianshu.com/p/52841b514868
部分参考:野火教程

你可能感兴趣的:(STM32,FreeRTOS)