FreeRTOS的学习(十四)——PendSV异常

23 篇文章 28 订阅

FreeRTOS的学习系列文章目录

FreeRTOS的学习(一)——STM32上的移植问题
FreeRTOS的学习(二)——任务优先级问题
FreeRTOS的学习(三)——中断机制
FreeRTOS的学习(四)——列表
FreeRTOS的学习(五)——系统延时
FreeRTOS的学习(六)——系统时钟
FreeRTOS的学习(七)——1.队列概念
FreeRTOS的学习(七)——2.队列入队源码分析
FreeRTOS的学习(七)——3.队列出队源码分析
FreeRTOS的学习(八)——1.二值信号量
FreeRTOS的学习(八)——2.计数型信号量
FreeRTOS的学习(八)——3.优先级翻转问题
FreeRTOS的学习(八)——4.互斥信号量
FreeRTOS的学习(九)——软件定时器
FreeRTOS的学习(十)——事件标志组
FreeRTOS的学习(十一)——任务通知



前言

本文将从任务切换的角度出发,学习PendSV异常的相关概念。


1 PendSV异常

正常情况下,任务的切换需要具有实时性的作用,并且不能影响到其他优先级更高的任务。因此不难想象,可以使用一个优先级最低的中断来完成任务的切换操作。
PendSV称为可悬起的系统调用,这与SVC不同,SVC异常是必须被立即响应的,如若不然,就会产生硬件错误。而PendSV异常是可以像普通中断一样根据优先级从而判断是否执行。此外,PendSV的异常实现也很简单,只要像地址为0xe000ed04的中断控制和状态寄存器(ICSR)的bit28位置1悬起PendSV,从而引起PendSV异常。


2 FreeRTOS 任务切换场合

2.1 执行系统调用

执行系统调用就是执行FreeRTOS系统提供的相关API函数,比如任务切换函数taskYIELD(),FreeRTOS 有些 API 函数也会调用函数 taskYIELD(),这些 API 函数都会导致任务切换,这些 API 函数和任务切换函数 taskYIELD()都统称为系统调用。

PendSV异常的代码最终都会调用portYIELD,其中主要的操作就是将ICSR的bit28置1,并且对数据和指令进行隔离,保证PendSV的悬起。

#define portYIELD()                                 \
    {                                                   \
        portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; \
        /*数据和指令隔离指令,确保之前指令确实执行*/ \
        __dsb( portSY_FULL_READ_WRITE );                           \
        __isb( portSY_FULL_READ_WRITE );                           \
    }

当然 上述的情况属于任务级的任务切换,如果是中断级的任务切换,首先会根据中断函数中是否存在更高优先级的任务产生,从而有一个返回值,该返回值如果为真,则需要进行任务切换,同样是调用portYILED。

2.2 系统滴答定时器(SysTick)中断

除了PendSV异常进行任务切换外,FreeRTOS 中滴答定时器(SysTick)中断服务函数中也会进行任务切换,滴答定时器中断服务函数如下:

void xPortSysTickHandler( void )
{
    //执行SysTick_Handler中断函数时,为了保证在freertos中属于最低优先级的此中断能顺利执行,
    //故要关闭FreeRTOS的所有可管理中断,保证系统计数时不被打断。
    vPortRaiseBASEPRI();  //关中断
    {
        /* Increment the RTOS tick. */
        if( xTaskIncrementTick() != pdFALSE )  //判断返回值,如果为pdTURE就要进行一次上下文切换
        {
            portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
        }
    }
    vPortClearBASEPRIFromISR();  //开中断
}

xTaskIncrementTick函数:当有阻塞的任务计时时间到了的时候,如果任务优先级更高的话就会需要进行任务切换,其返回值为pdTURE。
此外还有一种情况:抢占功能以及时间片调度功能都开启的话,也会根据情况对xTaskIncrementTick函数的返回值进行操作。
具体关于时间片调度的说明将在下面给出。


3 PendSV中断函数

通过以上的说明,可以发现,FreeRTOS的任务切换都是在PendSV中断函数中进行的。
其源码如下:

__asm void xPortPendSVHandler( void )
{
    extern uxCriticalNesting;
    extern pxCurrentTCB;
    extern vTaskSwitchContext;

/* *INDENT-OFF* */
    PRESERVE8

    mrs r0, psp
    isb

    ldr r3, =pxCurrentTCB /* Get the location of the current TCB. */
    ldr r2, [ r3 ]

    stmdb r0 !, { r4 - r11 } /* Save the remaining registers. */
    str r0, [ r2 ] /* Save the new top of stack into the first member of the TCB. */

    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 ] /* The first item in pxCurrentTCB is the task top of stack. */
    ldmia r0 !, { r4 - r11 } /* Pop the registers and the critical nesting count. */
    msr psp, r0
    isb
    bx r14
    nop
/* *INDENT-ON* */
}

(1)、读取进程栈指针,保存在寄存器 r0 里面。
(2)(3)、获取当前任务的任务控制块,并将任务控制块的地址保存在寄存器 r2 里面。
(4)、保存 r4~r11 和 r14 这几个寄存器的值。
(5)、将寄存器r0 的值写入到寄存器 r2 所保存的地址中去,也就是将新的栈顶保存在任务控制块的第一个字段中。此时的寄存器 r0 保存着最新的堆栈栈顶指针值,所以要将这个最新的栈顶指针写入到当前任务的任务控制块第一个字段,而经过(2)和(3)已经获取到了任务控制块,并将任务控制块的首地址写如到了寄存器 r2 中。
(6)、将寄存器 r3 和 r14 的值临时压栈,寄存器 r3 中保存了当前任务的任务控制块,而接下来要调用函数 vTaskSwitchContext(),为了防止 R3 和R14 的值被改写,所以这里临时将 r3和r14 的值先压栈。
(7)(8)、关闭中断,进入临界区。
(9)、调用函数 vTaskSwitchContext(),此函数用来获取下一个要运行的任务,并将pxCurrentTCB更新为这个要运行的任务。
(10)(11)、打开中断,退出临界区。
(12)、刚刚保存的寄存器 r3 和r14 的值出栈,恢复寄存器 r3 和r14 的值。注意,经过(12)步,此时 pxCurrentTCB的值已经改变了,所以读取 r3 所保存的地址处的数据就会发现其值改变了,成为了下一个要运行的任务的任务控制块。
(13)(14)、获取新的要运行的任务的任务堆栈栈顶,并将栈顶保存在寄存器 r0 中。
(15)、r4~r11,r14 出栈,也就是即将运行的任务的现场。
(16)、更新进程栈指针 PSP 的值。
(17)、执行此行代码以后硬件自动恢复寄存器r0~r3、r12、LR、PC 和 xPSR的值,确定异常返回以后应该进入处理器模式还是进程模式,使用主栈指针(MSP)还是进程栈指针(PSP)。很明显这里会进入进程模式,并且使用进程栈指针(PSP),寄存器PC 值会被恢复为即将运行的任务的任务函数,新的任务开始运行!至此,任务切换成功。

任务切换的大致过程可以表述如下:任务A切换前先将cpu使用过程中的一些寄存器的值保存到堆栈里面(现场保护),过程中操作栈顶指针(保存在TCB中)。当要运行任务B时,调用函数 vTaskSwitchContext(),此函数用来获取下一个要运行的任务,并将 pxCurrentTCB 更新为这个要运行的任务,然后从任务堆栈中取出来这些寄存器值,赋给相应的寄存器,cpu开始执行。


4 时间片调度

在 FreeRTOS 中允许一个任务运行一个时间片(一个时钟节拍的长度)后让出 CPU 的使用权,让拥有同优先级的下一个任务运行。
要使用时间片调度的话宏 configUSE_PREEMPTION和宏 configUSE_TIME_SLICING必须为 1。时间片的长度由宏 configTICK_RATE_HZ 来确定,一个时间片的长度就是滴答定时器的中断周期,比如本教程中 configTICK_RATE_HZ 为 1000,那么一个时间片的长度就是 1ms。
时间片调度发生在滴答定时器的中断服务函数中,即xTaskIncrementTick函数:

#if ( ( configUSE_PREEMPTION == 1 ) && ( configUSE_TIME_SLICING == 1 ) )
        {
            if( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ pxCurrentTCB->uxPriority ] ) ) > ( UBaseType_t ) 1 )
            {
                xSwitchRequired = pdTRUE;
            }
            else
            {
                mtCOVERAGE_TEST_MARKER();
            }
        }
#endif

可以看到,通过listCURRENT_LIST_LENGTH函数判断当前任务所对应的优先级下是否还有其他的任务。如果当前任务所对应的任务优先级下还有其他的任务那么就返回 xSwitchRequired = pdTRUE。
即函数xTaskIncrementTick() 就 会 返 回 pdTURE , 由 于 函 数 返 回 值 为 pdTURE 因 此 函 数
xPortSysTickHandler()就会进行一次任务切换。从而就可以调度到同优先级的其他任务中去了,当然,如果此时有更高的优先级任务要运行,则会执行优先级更高的任务,此为抢占。


关于任务查找的相关文档可以查看FreeRTOS的学习(二)——任务优先级问题

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

LEODWL

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值