FreeRTOS学习——Systick中断、SVC中断、PendSV中断

FreeRTOS学习------接口宏portmacro.h,仅用于记录自己阅读与学习源码
FreeRTOS Kernel V10.5.1
port :GCC/ARM_CM7

文章目录


Systick

源码

在Systick中断xPortSysTickHandler中,只做了一件事,那就是挂起PendSV中断。

所以其实切换任务是在PendSV中断执行的。

c 复制代码
void xPortSysTickHandler( void )
{
    /* SysTick以最低的中断优先级运行 */
    portDISABLE_INTERRUPTS();
    {
        /* Increment the RTOS tick. */
        if( xTaskIncrementTick() != pdFALSE )
        {
            /* 上下文切换在 PendSV 中断 中执行
             * 挂起 PendSV 中断. */
            portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
        }
    }
    portENABLE_INTERRUPTS();
}

触发方式

在开启调度器时,会调用定时器中断设置函数vPortSetupTimerInterrupt,以生成 tick 中断,如下:

vTaskStartScheduler →

xPortStartScheduler →

vPortSetupTimerInterrupt

配置系统的滴答定时器,以允许操作系统根据配置的频率生成定时中断,从而支持任务的调度和管理

c 复制代码
__attribute__( ( weak ) ) void vPortSetupTimerInterrupt( void )
{
    /* 低功耗模式,这里先不做分析. */
    #if ( configUSE_TICKLESS_IDLE == 1 )
    {
        ulTimerCountsForOneTick = ( configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ );
        xMaximumPossibleSuppressedTicks = portMAX_24_BIT_NUMBER / ulTimerCountsForOneTick;
        ulStoppedTimerCompensation = portMISSED_COUNTS_FACTOR / ( configCPU_CLOCK_HZ / configSYSTICK_CLOCK_HZ );
    }
    #endif /* configUSE_TICKLESS_IDLE */

    /* 停止和清除SysTick. */
    portNVIC_SYSTICK_CTRL_REG = 0UL;/*将控制寄存器设置为0,停止SysTick*/
    portNVIC_SYSTICK_CURRENT_VALUE_REG = 0UL;/*将当前值寄存器清零。*/

    /* 配置SysTick. */
    /* 设置加载寄存器,将SysTick的计数重加载为目标计数值*/
    portNVIC_SYSTICK_LOAD_REG = ( configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ ) - 1UL;
    /* 设置SysTick的控制寄存器,以启用时钟、启用中断和使能SysTick*/
    portNVIC_SYSTICK_CTRL_REG = ( portNVIC_SYSTICK_CLK_BIT_CONFIG | portNVIC_SYSTICK_INT_BIT | portNVIC_SYSTICK_ENABLE_BIT );
}

这几个值分别是

c 复制代码
#define configSYSTICK_CLOCK_HZ             ( configCPU_CLOCK_HZ )
/* Ensure the SysTick is clocked at the same frequency as the core. */
#define portNVIC_SYSTICK_CLK_BIT_CONFIG    ( portNVIC_SYSTICK_CLK_BIT )

#define configCPU_CLOCK_HZ						( SystemCoreClock )
#define configTICK_RATE_HZ						( 1000 )

#define portNVIC_SYSTICK_CLK_BIT              ( 1UL << 2UL )
#define portNVIC_SYSTICK_INT_BIT              ( 1UL << 1UL )
#define portNVIC_SYSTICK_ENABLE_BIT           ( 1UL << 0UL )

然后就会按照设定的时间(默认1ms)周期性的触发Systick中断


SVC

源码

c 复制代码
void vPortSVCHandler( void )
{
    __asm volatile (                           /* volatile表示不能移除或重排序这段代码. */
        "	ldr	r3, pxCurrentTCBConst2		\n"/* 将pxCurrentTCB的地址加载到r3中. */
        "	ldr r1, [r3]					\n"/* 从r3指向的位置读取当前任务控制块(TCB)的地址并将其存储在r1中. */
        "	ldr r0, [r1]					\n"/* 从r1指向的位置读取数据,即当前任务控制块中的第一个元素,所以r0中保存的就是当前任务栈的栈顶地址. */
        "	ldmia r0!, {r4-r11, r14}		\n"/* 栈中弹出寄存器r4到r11和r14(链接寄存器),这里使用ldmia指令(加载多寄存器),!表示更新指针r0,指向下一个数据. */
        "	msr psp, r0						\n"/* 将恢复后的任务栈指针r0存入psp,这样就恢复了当前任务的执行上下文. */
        "	isb								\n"/* 指令同步屏障,确保之前的指令在接下来的指令执行之前完成*/
        "	mov r0, #0 						\n"/* 将寄存器r0清零,准备设置中断优先级屏蔽*/
        "	msr	basepri, r0					\n"/* 将basepri设置为0,意味着没有设置优先级屏蔽,允许所有中断*/
        "	bx r14							\n"/* 通过bx指令跳转到r14寄存器中保存的地址,通常是返回到被中断的任务执行*/
        "									\n"
        "	.align 4						\n"/* 确保下一条指令或数据在4字节边界对齐*/
        "pxCurrentTCBConst2: .word pxCurrentTCB				\n"/* 定义一个标签pxCurrentTCBConst2,并将pxCurrentTCB的地址存储到这个标签中,以便在前面的ldr指令中使用*/
        );
}
  1. 获取当前任务控制块pxCurrentTCB的栈顶地址
  2. 将r4-r11, r14手动出栈
  3. 将当前栈顶指针存入psp
  4. 开中断

ldr r3, pxCurrentTCBConst2:将pxCurrentTCBConst2的值(即pxCurrentTCB的地址)加载到寄存器r3中。

ldr r1, [r3]:从r3指向的内存地址加载当前任务控制块(TCB)的地址到r1中。

ldr r0, [r1]:从r1指向的TCB地址加载当前任务栈的栈顶地址到r0中。

ldmia r0!, {r4-r11, r14}:从r0指向的栈中弹出寄存器r4到r11和r14,!表示更新r0的值,指向下一个数据。

msr psp, r0:将恢复后的任务栈指针r0存入PSP(进程栈指针)。

mov r0, #0:将寄存器r0清零。

msr basepri, r0:将basepri寄存器设置为0,允许所有中断。

bx r14:跳转到返回地址。

pxCurrentTCBConst2: .word pxCurrentTCB:定义标签pxCurrentTCBConst2并将pxCurrentTCB的地址存储在该位置

触发方式

SVC(Supervisor Call)中断是通过软件触发的中断

SVC指令:软件通过执行 SVC 指令显式地请求中断,svc 0

当SVC指令被执行后,处理器会根据中断向量表中的信息跳转到相应的SVC中断处理函数,执行SVC指令时,处理器会自动保存当前的上下文,并将处理器的模式切换为特权模式,从而允许执行受限的操作

在开启调度器时,会调用开始第一个任务函数,如下

vTaskStartScheduler →

xPortStartScheduler →

prvPortStartFirstTask

c 复制代码
static void prvPortStartFirstTask( void )
{
    /* 开始第一个任务。这也清除了指示FPU正在使用的位,以防FPU在调度器启动之前被使用,
     * 否则会导致SVC堆栈中不必要的空间留下,以延迟保存FPU寄存器。 */
    __asm volatile (
        " ldr r0, =0xE000ED08 	\n"/* 加载NVIC偏移寄存器的地址到r0寄存器,这个寄存器用于访问系统控制块(SCB)的基地址. */
        " ldr r0, [r0] 			\n"
        " ldr r0, [r0] 			\n"/* 读取两次,从SCB中加载系统栈指针(MSP)的值到r0寄存器*/
        " msr msp, r0			\n"/* 将r0中的值设置为主栈指针(MSP),这标志着栈的开始. */
        " mov r0, #0			\n"/* 清除控制寄存器中指示FPU正在使用的位,以确保不保留之前的状态. */
        " msr control, r0		\n"
        " cpsie i				\n"/* 开中断. */
        " cpsie f				\n"
        " dsb					\n"
        " isb					\n"
        " svc 0					\n"/* 触发SVC异常. */
        " nop					\n"
        " .ltorg				\n"
        );
}
  1. 获取系统堆栈指针MSP
  2. 清除FPU正在使用的位
  3. 开中断
  4. 触发SVC异常

PendSV

源码

c 复制代码
void xPortPendSVHandler( void )
{
    /* This is a naked function. */

    __asm volatile
    (
        "	mrs r0, psp							\n"/* 读取当前进程的栈指针(Process Stack Pointer),将其值存入寄存器 r0*/
        "	isb									\n"/* 指令同步屏障*/
        "										\n"
        "	ldr	r3, pxCurrentTCBConst			\n"/* 将pxCurrentTCB的地址加载到寄存器 r3. */
        "	ldr	r2, [r3]						\n"/* 从r3指向的位置读取当前任务控制块(TCB)的地址并将其存储在r2中. */
        "										\n"
        "	tst r14, #0x10						\n"/* 测试链接寄存器 r14 的第 4 位,判断当前任务是否使用浮点单元(FPU). */
        "	it eq								\n"/* 如果第 4 位为 1(即使用 FPU 上下文),则执行接下来的语句*/
        "	vstmdbeq r0!, {s16-s31}				\n"/* 如果使用 FPU 上下文,则将 FPU 的高寄存器(s16 到 s31)压入栈中*/
        "										\n"
        "	stmdb r0!, {r4-r11, r14}			\n"/* 将寄存器 r4 到 r11 及 r14 压入栈中. */
        "	str r0, [r2]						\n"/* 将新的栈顶指针值保存到 TCB 中. */
        "										\n"
        "	stmdb sp!, {r0, r3}					\n"/* 将目前的栈指针和 TCB 地址压入主栈,以便恢复,这里入栈是 MSP*/
        "	mov r0, %0 							\n"/* 将最大可用优先级(configMAX_SYSCALL_INTERRUPT_PRIORITY)加载到 r0 中*/
        "	cpsid i								\n"/* 禁止中断. */
        "	msr basepri, r0						\n"/* 设置优先级屏蔽寄存器(Base Priority),关中断*/
        "	dsb									\n"/* 数据同步屏障*/
        "	isb									\n"/* 指令同步屏障*/
        "	cpsie i								\n"/* 启用中断 */
        "	bl vTaskSwitchContext				\n"/* 调用任务切换的上下文切换函数*/
        "	mov r0, #0							\n"
        "	msr basepri, r0						\n"/* 恢复 Base Priority 为 0,开中断*/
        "	ldmia sp!, {r0, r3}					\n"/* 从主栈中恢复之前保存的 r0 和 r3*/
        "										\n"
        "	ldr r1, [r3]						\n"/* 从r3指向的位置读取当前任务控制块(已经更新过了)的地址并将其存储在r1中. */
        "	ldr r0, [r1]						\n"/* 从r1指向的位置读取数据,即当前任务控制块中的第一个元素,所以r0中保存的就是当前任务栈的栈顶地址*/
        "										\n"
        "	ldmia r0!, {r4-r11, r14}			\n"/* 从栈中弹出保存的核心寄存器r4-r11 和 r14. */
        "										\n"
        "	tst r14, #0x10						\n"/* 再次判断是否使用 FPU 上下文. */
        "	it eq								\n"/* 如果使用 FPU,上下文则执行下一条指令*/
        "	vldmiaeq r0!, {s16-s31}				\n"/* 如果使用 FPU,则从栈中弹出高寄存器(s16 到 s31)*/
        "										\n"
        "	msr psp, r0							\n"/* 将恢复后的栈指针值写入PSP,以便切换到新的任务的栈中. */
        "	isb									\n"/* 指令同步屏障*/
        "										\n"
        #ifdef WORKAROUND_PMU_CM001 /* XMC4000特定勘误表解决方法. 这里不用管*/
            #if WORKAROUND_PMU_CM001 == 1
                "			push { r14 }				\n"
                "			pop { pc }					\n"
            #endif
        #endif
        "										\n"
        "	bx r14								\n"/* 返回到调用该处理程序的位置*/
        "										\n"/* 将后面的数据对齐到 4 字节*/
        "	.align 4							\n"
        "pxCurrentTCBConst: .word pxCurrentTCB	\n"
        ::"i" ( configMAX_SYSCALL_INTERRUPT_PRIORITY )
    );
}

在PendSV中干了两件事
一是保存当前任务的现场

  1. 获取当前任务控制块pxCurrentTCB的栈顶地址
  2. 将核心寄存器入栈(其他寄存器在进入中断之前已经自动入栈)
  3. 保存栈顶指针

二是恢复最新任务的现场

  1. 调用任务切换的上下文切换函数,然后pxCurrentTCBConst中指向的值就已经变成了新的TCB的地址,但是pxCurrentTCBConst本身的内存地址是不变得
  2. 获取最新任务控制块pxCurrentTCB的栈顶地址
  3. 将核心寄存器出栈(其他寄存器在退出中断之后会自动出栈)
  4. 将栈顶指针值写入PSP

这样就完成了上下文的切换, 在进入中断前后,堆栈指针的变化是:

  1. 进入中断前,使用PSP,PSP指向旧任务的栈顶
  2. 在中断中,使用MSP
  3. 退出中断后,使用PSP,PSP指向新任务的栈顶

任务切换函数vTaskSwitchContext →

taskSELECT_HIGHEST_PRIORITY_TASK →

listGET_OWNER_OF_NEXT_ENTRY

经过此函数后,pxCurrentTCBConst已经指向了新的TCB

触发方式

1 在Systick handle中触发

c 复制代码
/* 上下文切换在 PendSV 中断 中执行
             * 挂起 PendSV 中断. */
            portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;

2 taskYIELD();

通过调用taskYIELD来触发

c 复制代码
#define portYIELD()                                 \
    {                                                   \
        /* Set a PendSV to request a context switch. */ \
        portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; \
                                                        \
        /* Barriers are normally not required but do ensure the code is completely \
         * within the specified behaviour for the architecture. */ \
        __asm volatile ( "dsb" ::: "memory" );                     \
        __asm volatile ( "isb" );                                  \
    }

相关汇编指令

  • ldr:加载寄存器指令,用于从内存中加载数据到寄存器中。
  • ldmia:加载多寄存器指令,可以一次性将多个内存单元的数据加载到多个寄存器中。
  • msr:移动至特权寄存器的指令,用于设置某些特定的寄存器。
  • isb:指令同步屏障,用于确保指令执行的顺序,确保之前的指令在接下来的指令执行之前完成
  • dsb:数据同步屏障,该指令确保所有之前的读写操作在继续执行后续指令之前完成
  • mov:数据传送指令,用于将一个常数值或寄存器的值移动到另一个寄存器。
  • bx:分支和交换指令,用于跳转到r14寄存器中保存的地址。
  • .align:伪指令,用于确保下一条指令或数据在指定的字节边界对齐。
  • .word:伪指令,定义一个字(4字节)并将值存储到该位置。
  • nop用于在代码中占用一个执行周期,但不进行任何操作,常常针对时序和代码结构进行调整
  • .ltorg用于生成字面量池,为代码中使用的常量提供存储空间。通过将常量放置在易于访问的位置,它提高了程序的整体性能。
  • 开关中断指令
    CPSID I ;PRIMASK=1 ;关中断 (硬件错误异常 不关)
    CPSIE I ;PRIMASK=0 ;开中断
    CPSID F ;FAULTMASK=1 ;关异常(硬件错误异常 也关)
    CPSIE F ;FAULTMASK=0 ;开异常

本人菜鸟,欢迎大佬们甄误

相关推荐
码农小白13 分钟前
qt学习:linux监听键盘alt+b和鼠标移动事件
学习·计算机外设
MapleLea1f32 分钟前
26届JAVA 学习日记——Day14
java·开发语言·学习·tcp/ip·程序人生·学习方法
小鹿撞出了脑震荡1 小时前
SQLite3语句以及用实现FMDB数据存储的学习
数据库·学习·sqlite
lcintj1 小时前
【WPF】Prism学习(九)
学习·wpf·prism
一只小菜鸡..2 小时前
241121学习日志——[CSDIY] [InternStudio] 大模型训练营 [11]
学习
2402_871321952 小时前
MATLAB方程组
gpt·学习·线性代数·算法·matlab
2301_775281192 小时前
法语旅游常用口语-柯桥学外语到蓝天广场泓畅学校
学习·生活·旅游
SSL_lwz3 小时前
P11290 【MX-S6-T2】「KDOI-11」飞船
c++·学习·算法·动态规划