目录
1 Cortex-M4 Systick的重要性
抛出4个问题
Systick是什么?
对应中断和异常向量表SysTick
每一次周期,都会触发中断,进行内核的调度,我们常在FreeRTOS设置时基1000hz,对应Systick周期就是1ms1次中断。每次触发,都会进行对就绪列表遍历,查看优先级。
上下文切换是什么?
即系统内核态转变到任务态的变化,即栈的操作。
对应中断和异常向量表PendSV
怎么启动?
对应中断和异常向量表SVCall
如何切换?
下文第4点
2 Cortex-M4 中断管理
Cortex-M4 内核支持 256 个中断,其中包含了 16 个内核中断和 240 个外部中断,并且具有256 级的可编程中断优先级设置。但 STM32F4 并没有使用 Cortex-M4 内核的全部东西,而是只用了它的一部分。
Cortex-M4处理器中,每一个外部中断都可以被使能或者禁止,并且可以被设置为挂起状态或者清除状态。
学习FreeRTOS,我们需要了解16个内核中断,以及中断优先级设置。FreeRTOS会接管中断优先级、中断是否使能,并且FreeRTOS用了2个中断进行切换和启动。
中断和异常向量表
SVCall异常:启动
PendSV异常:上下文切换
Systick中断:遍历就绪列表优先级
2.1 该如何管理中断?
一上电,系统就回去中断向量表读取MSP的指针
PRIMSK:中断屏蔽特殊寄存器
它的作用是控制系统是否允许处理器处理特定优先级的中断。
PRIMASK寄存器是用于屏蔽(即禁止)或允许(使能)所有中断。当PRIMASK寄存器被设置为1时,将屏蔽所有中断,这意味着任何发生的中断都不会被处理。而当PRIMASK寄存器被清零时,允许中断的处理。
在FreeRTOS中,通常将PRIMASK寄存器与任务调度器配合使用,以确保任务切换的正确性。当任务切换时,需要禁止中断,以防止中断干扰任务切换过程。一旦任务切换完成,再允许中断以便处理其他中断请求。
具体来说,在FreeRTOS中,任务调度器会在任务切换之前禁止中断,这是通过将PRIMASK寄存器设置为1来实现的。这可以确保在任务切换期间不会发生其他中断,从而保证任务切换的原子性。任务切换完成后,调度器会将PRIMASK寄存器恢复为允许中断,从而继续处理其他中断请求。
需要注意的是,PRIMASK寄存器的使用需要谨慎。在禁止中断的情况下,系统将无法响应任何中断请求,包括系统定时器中断和外部设备中断。因此,在使用PRIMASK寄存器进行中断屏蔽时,需要确保能够及时地允许中断,以避免长时间阻塞特定中断的发生。
CONTROL:控制寄存器
【PRIV】0特权级 1非特权级
【SPSEL】0MSP 1PSP(也称影子栈)
在FreeRTOS中,【PRIV】和【SPSEL】是两个与特权级和堆栈选择相关的控制寄存器。
-
特权级(【PRIV】):该控制寄存器用于设置处理器的特权级别。0表示特权级,1表示非特权级。在FreeRTOS中,通常会将操作系统内核的代码设置为特权级(0),而任务代码设置为非特权级(1)。这样可以实现更好的隔离和保护,防止任务之间相互干扰或访问不受其权限限制的资源。
-
堆栈选择(【SPSEL】):该控制寄存器用于选择使用哪个堆栈指针来处理中断。0表示使用主堆栈指针(MSP),1表示使用进程堆栈指针(PSP)。在FreeRTOS中,通常会将主堆栈指针用于操作系统内核和中断处理,并将进程堆栈指针用于任务代码。这样可以在不同的上下文中使用不同的堆栈,确保任务切换和中断处理的正确性和可靠性。
在FreeRTOS的移植实现中,需要合理配置和使用这两个控制寄存器。通常,操作系统内核的启动代码会设置【PRIV】为特权级(0),并初始化主堆栈指针(MSP)。而任务的创建和切换代码则会使用【SPSEL】来选择进程堆栈指针(PSP),并根据需要切换堆栈。这样,在任务执行时,使用的是任务自己的私有堆栈,而不会影响其他任务或操作系统内核的堆栈。
FreeRTOS对中断的管理
中断的优先级永远比任务的优先级高,因为要保证实时性。
如图,中断1比任务task0高,中断2比中断1高,进行了一个嵌套。
但是我们前面介绍过PRIMSK:中断屏蔽特殊寄存器。如果我们把ISR1屏蔽掉,那么只响应ISR2的中断。
FreeRTOS中断管理的作用
1.只响应实时性要求比较高的作用
2.临界段的作用:进入时赋值,屏蔽,退出时响应所有中断。因为我们操作系统内核操作的时候,时不希望有其他中断打断,会影响操作系统正常运行。
3 Cortex-M4 影子栈指针
分析:
异常发生的时候使用MSP栈指针
异常退出的时候可以使用MSP也可以使用PSP
FreeRTOS操作系统,是通过Systick中断,来遍历优先级最高的任务,这个时候,一旦进入中断使用MSP,但是退出的时候可以选择PSP。如果我们把每个任务的栈分配给PSP,那么我们在中断或异常退出的时候恢复的栈就是指向任务的上下文环境。
任务栈
如图,任务均使用的是PSP
4 Cortex-M4 SVC和pendSV异常
4.1 SVC
SVC异常是从ARM7转变过来的:
操作系统屏蔽了一些硬件,只给用户提供一些API接口,当用户需要进行特权操作时,例如访问受保护的资源或执行特殊的操作,必须请求操作系统内核来执行这些操作。在执行系统调用时,应用程序切换到内核模式并执行特权指令。如果在执行特权指令期间发生错误或违反了特权级别的限制,就会引发SVC异常。
在M4中,SVC异常的特点:
- 响应快
- 中断不可内部嵌套
4.2 PendSV异常
上图描述了系统启动一个中断流程,OS启动一上电需要响应快,所以使用的SVC中断,之后使用PendSV中断切换任务b,当Systick中断发生再会调用PendSV切换至优先级高的任务。
其中R0~R3、R12、LR、PC、xPSR、S0~S15(浮点寄存器)由硬件帮我们保存
而上R4-R11(任务内部)、CONTROL(栈选择)、EXC_RETURN(用来表示使用的是哪个栈)和S16~S31(其他浮点寄存器)硬件不会帮我们保存,用户自己软件保存,STMDB是切换指令。切换的是上文是入栈和出栈,以及栈指针改变的操作。
上下文切换的过程可以简单地描述为以下几个步骤:
上文处理:
- a. 当PendSV中断被触发时,处理器会首先保存当前任务的上下文。
- b. 保存的上下文包括当前任务的寄存器值和堆栈指针。
下文处理:
- a. 在保存当前任务的上下文后(EXC_RETURN),处理器会加载下一个要执行的任务的上下文。
- b. 加载的上下文包括下一个任务的寄存器值和堆栈指针。
关于入栈、出栈和栈指针的操作,下面是一个简单的示例:
假设有两个任务 TaskA 和 TaskB,它们通过 PendSV 实现上下文切换。
上文处理(TaskA -> PendSV):
-
a. 保存当前任务(TaskA)的寄存器值和堆栈指针到TaskA的任务控制块中。
-
b. 切换到PendSV的堆栈,并将PendSV的堆栈指针(PSP)加载到处理器堆栈指针寄存器(MSP或PSP)。
-
c. 将PendSV的中断处理程序入栈。
下文处理(PendSV -> TaskB):
-
a. 从TaskB的任务控制块中恢复TaskB的寄存器值和堆栈指针。
-
b. 将TaskB的堆栈指针加载到处理器堆栈指针寄存器。
-
c. 从PendSV的堆栈中弹出PendSV的中断处理程序。
-
d. 返回到TaskB继续执行。
5 多任务启动流程
- 创建空闲任务
- 配置SysTick PendSV为最低优先级
- 配置SysTick寄存器
- 调用SVC
空闲任务
之前讲过当前任务把任务放到删除任务列表里,空闲任务还可以用作调整操作系统使用率、内存使用率、低功耗处理等作用
SydTick PendSV为最低优先级
否则其他任务无法中断
配置SysTick寄存器
保证1ms中断一次
调用SVC
产生一个系统调用,进行任务切换
5.1源码分析
//main.c中开启内核
osKernelStart(); //---->
//任务开始调度
vTaskStartScheduler(); //---->
void vTaskStartScheduler( void )
{
BaseType_t xReturn;
#if( configSUPPORT_STATIC_ALLOCATION == 1 )
{
}
#else //动态分配
{
/*创建空闲任务,最低优先级*/
xReturn = xTaskCreate( prvIdleTask,
"IDLE", configMINIMAL_STACK_SIZE,
( void * ) NULL,
( tskIDLE_PRIORITY | portPRIVILEGE_BIT ),
&xIdleTaskHandle );
}
#endif /* configSUPPORT_STATIC_ALLOCATION */
if( xReturn == pdPASS )
{
/* 关闭中断 */
portDISABLE_INTERRUPTS();
//下一个任务锁定时间赋值最大值,起始时不让时间片调度
xNextTaskUnblockTime = portMAX_DELAY;
//调度器运行状态置位,开始运行了
xSchedulerRunning = pdTRUE;
//初始化系统的节拍值为0
xTickCount = ( TickType_t ) 0U;
/* 启动调度器 */
if( xPortStartScheduler() != pdFALSE )
{
//不会运行到这里
}
else
{
/* Should only reach here if a task calls xTaskEndScheduler(). */
}
}
else
{
}
}
启动调度器源码分析xPortStartScheduer
BaseType_t xPortStartScheduler( void )
{
/* 配置systick pendsv为最低的优先级,为了保证系统的实时性 */
portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;
portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;
/* 1、初始化systick,配置为1ms的中断产生时基
2、开启systick中断
*/
vPortSetupTimerInterrupt();
/* 初始化临界段嵌套值 */
uxCriticalNesting = 0;
/* 启动第一个任务 */
prvStartFirstTask();
return 0;
}
启动第一个任务源码分析prvStartFirstTask
__asm void prvStartFirstTask( void )
{
PRESERVE8 //8字节对齐,AAPCS的标志,ARM特有
/* 0xE000ED08 它是中断向量表的一个地址
它存储的是MSP的指针
最终获取到MSP的RAM的地址
*/
ldr r0, =0xE000ED08
ldr r0, [r0]
ldr r0, [r0]
/* 重新把MSP的地址,赋值为MSP
不是系统一上电就会处理了吗?
起始真正的作用:
1、如果没有在线升级的功能,就可以屏蔽这段代码
2、BSP BOOTLoder 可以选择程序运行的代码段
3、如果在线更新,这个时候,中断向量表会更新,所以要重新赋值MSP
*/
msr msp, r0
/* 开启全局中断 */
cpsie i
cpsie f
dsb
isb
/* 调用SVC */
svc 0
nop
nop
}
SVC异常处理源码分析
__asm void vPortSVCHandler( void )
{
PRESERVE8 //8字节对齐
/* 获取当前任务控制块
任务控制块的第一成员是任务的栈顶
获取到栈顶之后,剩下的就是出栈工作
出栈---任务的堆栈
*/
ldr r3, =pxCurrentTCB
ldr r1, [r3] /* Use pxCurrentTCBConst to get the pxCurrentTCB address. */
ldr r0, [r1] /* The first item in pxCurrentTCB is the task top of stack. */
//出栈内核寄存器
ldmia r0!, {r4-r11} /* Pop the registers that are not automatically saved on exception entry and the critical nesting count. */
//更新栈指针地址到PSP
msr psp, r0 /* Restore the task stack pointer. */
isb
//把basepri赋值为0,打开屏蔽中断
mov r0, #0
msr basepri, r0
orr r14, #0xd
//异常退出
bx r14
//最终跳转到任务的执行函数里面
}
问:为什么出栈r4-r14?
出栈方式是先入后出。结合创建任务代码分析
问:protINITIAL_EXEC_RETRUN是干什么的?
R14就是异常返回值,全局搜索 protINITIAL_EXEC_RETRUN 的值是0xFFFFFFFD
翻阅权威指南第八章升入了解异常。
R14寄存器(也称为LR,Link Register)在ARM Cortex-M4处理器中存在,用于存储函数调用的返回地址。这意味着只有基于Cortex-M4内核的微控制器才会有R14寄存器。
在Cortex-M3架构中,没有专门的寄存器用于存储和管理函数的返回地址,而是通过栈来实现这个功能。
问:为什么没有恢复其他寄存器?
其他再出宅的是很好会自动恢复(由系统处理)
之后下一个PC指针执行pxCode,即执行下个任务
问:为什么异常退出?
r14就是异常返回值,表示异常退出后,使用PSP
6 PendSV
PendSV业务流程
- 读取当前PSP值,获取当前任务栈顶
- 保存s16-s31到栈中,保存r4-r11 r14到当前栈中
- 更新栈顶到当前任务可控制块中,保存r3到栈中关闭中断
- 查找优先级最高的任务,更新当前任务控制块,开启中断出栈r3值
- 出栈r4-r11 r14到当前栈中 出栈s16-s31到栈中 更新栈顶到PSP 调用移除返回指令
__asm void xPortPendSVHandler( void )
{
extern uxCriticalNesting;
extern pxCurrentTCB;
extern vTaskSwitchContext;
PRESERVE8
mrs r0, psp
isb
/*获取当前任务控制块,其实就是获取任务栈顶 */
ldr r3, =pxCurrentTCB
ldr r2, [r3]
//保存内核寄存器---调用者需要做的
stmdb r0!, {r4-r11}
//保存当前任务栈顶 把栈顶指针入栈
str r0, [r2]
stmdb sp!, {r3, r14}
//使能可屏蔽的中断---临界段
mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY
msr basepri, r0
dsb
isb
//执行上下文切换
bl vTaskSwitchContext
//使能可屏蔽的中断
mov r0, #0
msr basepri, r0
/*恢复任务控制块指向的栈顶,其实在上下文切换的时候,
当前的任务控制块的指针,已经指向了优先级最高的
*/
ldmia sp!, {r3, r14}
//获取当前栈顶
ldr r1, [r3]
ldr r0, [r1]
//出栈
ldmia r0!, {r4-r11}
//更新PSP指针
msr psp, r0
isb
//异常返回,下面要执行的代码,就是要切换的任务的代码
bx r14
nop
}
vTaskSwitchContext源码分析
void vTaskSwitchContext( void )
{
if( uxSchedulerSuspended != ( UBaseType_t ) pdFALSE )
{
/* 标记调度器状态 */
xYieldPending = pdTRUE;
}
else
{
/* 标记调度器状态 */
xYieldPending = pdFALSE;
traceTASK_SWITCHED_OUT();
/* 检查任务栈是否溢出 */
taskCHECK_FOR_STACK_OVERFLOW();
/* 选择优先级最高的任务,把当前任务的控制块进行赋值 */
taskSELECT_HIGHEST_PRIORITY_TASK();
traceTASK_SWITCHED_IN();
}
}
7 总结
多任务启动流程
osKernelStart(); //---->
vTaskStartScheduler(); //---->
xPortStartScheduer(); //---->
prvStartFirstTask(); //---->
SVC; //---->出栈的一个工作
//其他任务调度会进行
PendSV(); //---->
vTaskSwitchContext(); //--->