HAL库版FreeRTOS(中)

news2024/11/25 20:53:50

目录

  • FreeRTOS 任务切换
    • PendSV 异常
    • PendSV 中断服务函数
    • FreeRTOS 确定下一个要运行的任务
      • 函数vTaskSwitchContext()
      • 函数taskSELECT_HIGHEST_PRIORITY_TASK()
    • PendSV 异常何时触发
    • FreeRTOS 时间片调度实验
      • 功能设计
      • 软件设计
      • 下载验证
  • FreeRTOS 内核控制函数
    • FreeRTOS 内核控制函数预览
    • FreeRTOS 内核控制函数详解
  • FreeRTOS 其他任务API 函数
    • FreeRTOS 任务相关API 函数
      • FreeRTOS 任务相关API 函数预览
      • FreeRTOS 任务相关API 函数详解

FreeRTOS 任务切换

RTOS 的核心时任务管理,而任务管理的重中之重任务切换,系统中任务切换的过程决定
了操作系统的运行效率和稳定性,尤其是对于实时操作系统。而对于深入了解和学习FreeRTOS,
FreeRTOS 的任务切换是必须要掌握的一个知识点。本章就来学习FreeRTOS 的任务切换。
本章分为如下几部分:
9.1 PendSV 异常
9.2 PendSV 中断服务函数
9.3 FreeRTOS 确定下一个要运行的任务
9.4 PendSV 异常何时触发
9.5 FreeRTOS 时间片调度实验

PendSV 异常

PendSV(Pended Service Call,可挂起服务调用),是一个对RTOS 非常重要的异常。PendSV
的中断优先级是可以编程的,用户可以根据实际的需求,对其进行配置。PendSV 的中断由将中
断控制状态寄存器(ICSR)中PENDSVSET 为置一触发(中断控制状态寄存器的有关内容,请
查看4.1.5 小节《中断控制状态寄存器》)。PendSV 与SVC 不同,PendSV 的中断是非实时的,
即PendSV 的中断可以在更高优先级的中断中触发,但是在更高优先级中断结束后才执行。
利用PendSV 的这个可挂起特性,在设计RTOS 时,可以将PendSV 的中断优先级设置为
最低的中断优先级(FreeRTOS 就是这么做的,更详细的内容,请查看4.3.1 小节《PendSV 和
SysTick 中断优先级》),这么一来,PendSV 的中断服务函数就会在其他所有中断处理完成后才
执行。任务切换时,就需要用到PendSV 的这个特性。
首先,来看一下任务切换的一些基本概念,在典型的RTOS 中,任务的处理时间被分为多
个时间片,OS 内核的执行可以有两种触发方式,一种是通过在应用任务中通过SVC 指令触发,
例如在应用任务在等待某个时间发生而需要停止的时候,那么就可以通过SVC 指令来触发OS
内核的执行,以切换到其他任务;第二种方式是,SysTick 周期性的中断,来触发OS 内核的执
行。下图演示了只有两个任务的RTOS 中,两个任务交替执行的过程:
在这里插入图片描述
在操作系统中,任务调度器决定是否切换任务。图9.1.1 中的任务及切换都是在SysTick 中
断中完成的,SysTick 的每一次中断都会切换到其他任务。
如果一个中断请求(IRQ)在SysTick 中断产生之前产生,那么SysTick 就可能抢占该中断
请求,这就会导致该中断请求被延迟处理,这在实时操作系统中是不允许的,因为这将会影响
到实时操作系统的实时性,如下图所示:
在这里插入图片描述
并且,当SysTick 完成任务的上下文切换,准备返回任务中运行时,由于存在中断请求,
ARM Cortex-M 不允许返回线程模式,因此,将会产生用法错误异常(Usage Fault)。
在一些RTOS 的设计中,会通过判断是否存在中断请求,来决定是否进行任务切换。虽然
可以通过检查xPSR 或NVIC 中的中断活跃寄存器来判断是否存在中断请求,但是这样可能会
影响系统的性能,甚至可能出现中断源在SysTick 中断前后不断产生中断请求,导致系统无法
进行任务切换的情况。
PendSV 通过延迟执行任务切换,直到处理完所有的中断请求,以解决上述问题。为了达到
这样的效果,必须将PendSV 的中断优先级设置为最低的中断优先等级。如果操作系统决定切
换任务,那么就将PendSV 设置为挂起状态,并在PendSV 的中断服务函数中执行任务切换,如
下图所示:
在这里插入图片描述

  1. 任务一触发SVC 中断以进行任务切换(例如,任务一正等待某个事件发生)。
  2. 系统内核接收到任务切换请求,开始准备任务切换,并挂起PendSV 异常。
  3. 当退出SVC 中断的时候,立刻进入PendSV 异常处理,完成任务切换。
  4. 当PendSV 异常处理完成,返回线程模式,开始执行任务二。
  5. 中断产生,并进入中断处理函数。
  6. 当运行中断处理函数的时候,SysTick 异常(用于内核时钟节拍)产生。
  7. 操作系统执行必要的操作,然后挂起PendSV 异常,准备进行任务切换。
  8. 当SysTick 中断处理完成,返回继续处理中断。
  9. 当中断处理完成,立马进入PendSV 异常处理,完成任务切换。
  10. 当PendSV 异常处理完成,返回线程模式,继续执行任务一。
    PendSV 在RTOS 的任务切换中,起着至关重要的作用,FreeRTOS 的任务切换就是在PendSV
    中完成的。

PendSV 中断服务函数

FreeRTOS 在PendSV 的中断中,完成任务切换,PendSV 的中断服务函数由FreeRTOS 编
写,将PendSV 的中断服务函数定义成函数xPortPendSVHandler()。
针对ARM Cortex-M3 和针对ARM Cortex-M4 和ARM Cortex-M7 内核的函数
xPortPendSVHandler()稍有不同,其主要原因在于ARM Cortex-M4 和ARM Cortex-M7 内核具有
浮点单元,因此在进行任务切换的时候,还需考虑是否保护和恢复浮点寄存器的值。
针对ARM Cortex-M3 内核的函数xPortPendSVHandler(),具体的代码如下所示:

__asm void xPortPendSVHandler(void) {
        /* 导入全局变量及函数*/
        extern uxCriticalNesting;
        extern pxCurrentTCB;
        extern vTaskSwitchContext;
        /* 8字节对齐*/
        PRESERVE8
        /* R0为PSP,即当前运行任务的任务栈指针*/
        mrs r0, psp
        isb
        /* R3为pxCurrentTCB的地址值,即指向当前运行任务控制块的指针*/
        /* R2为pxCurrentTCB的值,即当前运行任务控制块的首地址*/
        ldr r3, = pxCurrentTCB
        ldr r2, [r3]
        /* 将R4~R11入栈到当前运行任务的任务栈中*/
        stmdb r0!, {
                r4 - r11
            }
            /* R2指向的地址为此时的任务栈指针*/
        str r0, [r2]
        /* 将R3、R14入栈到MSP指向的栈中*/
        stmdb sp!, {
                r3, r14
            }
            /* 屏蔽受FreeRTOS管理的所有中断*/
        mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY
        msr basepri, r0
        dsb
        isb
        /* 跳转到函数vTaskSeitchContext
         * 主要用于更新pxCurrentTCB,
         * 使其指向最高优先级的就绪态任务
         */
        bl vTaskSwitchContext
        /* 使能所有中断*/
        mov r0, #0
msr basepri, r0
/* 将R3、R14重新从MSP指向的栈中出栈*/
ldmia sp !, { r3, r14 }
/* 注意:R3为pxCurrentTCB的地址值,
* pxCurrentTCB已经在函数vTaskSwitchContext中更新为最高优先级的就绪态任务
* 因此R1为pxCurrentTCB的值,即当前最高优先级就绪态任务控制块的首地址*/
ldr r1, [ r3 ]
/* R0为最高优先级就绪态任务的任务栈指针*/
ldr r0, [ r1 ]
/* 从最高优先级就绪态任务的任务栈中出栈R4~R11 */
ldmia r0 !, { r4 - r11 }
/* 更新PSP为任务切换后的任务栈指针*/
msr psp, r0
isb
/* 跳转到切换后的任务运行
* 执行此指令,CPU会自动从PSP指向的任务栈中,
* 出栈R0、R1、R2、R3、R12、LR、PC、xPSR寄存器,
* 接着CPU就跳转到PC指向的代码位置运行,
* 也就是任务上次切换时运行到的位置
*/
bx r14
nop
}

针对ARM Cortex-M4 内核的函数xPortPendSVHandler(),具体的代码如下所示(针对ARM Cortex-M7 内核的函数xPortPendSVHandler()与之类似):

__asm void xPortPendSVHandler(void) {
    /* 导入全局变量及函数*/
    extern uxCriticalNesting;
    extern pxCurrentTCB;
    extern vTaskSwitchContext;
    /* 8字节对齐*/
    PRESERVE8
    /* R0为PSP,即当前运行任务的任务栈指针*/
    mrs r0, psp
    isb
    /* R3为pxCurrentTCB的地址值,即指向当前运行任务控制块的指针*/
    /* R2为pxCurrentTCB的值,即当前运行任务控制块的首地址*/
    ldr r3, = pxCurrentTCB
    ldr r2, [r3]
    /* 获取R14寄存器的值,因为处于中断,此时R14为EXC_RETURN
     * 通过判断EXC_RETURN的bit4是否为0,
     * 判断在进入PendSV中断前运行的任务是否使用的浮点单元,
     * 若使用了浮点单元,需要在切换任务时,保存浮点寄存器的值
     */
    tst r14, #0x10
it eq
vstmdbeq r0!, {s16-s31}
/* 将R4~R11和R14寄存器入栈到当前运行任务的任务栈中
* 注意:此时的R14为EXC_RETURN,主要用于指示任务是否使用了浮点单元
*/
stmdb r0!, {r4-r11, r14}
/* R2指向的地址为此时的任务栈指针*/
str r0, [ r2 ]
/* 将R0、R3入栈到MSP指向的栈中*/
stmdb sp!, {r0, r3}
/* 屏蔽受FreeRTOS管理的所有中断*/
mov r0, # configMAX_SYSCALL_INTERRUPT_PRIORITY
    msr basepri, r0
    dsb
    isb
    /* 跳转到函数vTaskSeitchContext
     * 主要用于更新pxCurrentTCB,
     * 使其指向最高优先级的就绪态任务
     */
    bl vTaskSwitchContext
    /* 使能所有中断*/
    mov r0, #0
msr basepri, r0
/* 将R0、R3重新从MSP指向的栈中出栈*/
ldmia sp!, {r0, r3}
/* 注意:R3为pxCurrentTCB的地址值,
* pxCurrentTCB已经在函数vTaskSwitchContext中更新为最高优先级的就绪态任务
* 因此R1为pxCurrentTCB的值,即当前最高优先级就绪态任务控制块的首地址*/
ldr r1, [ r3 ]
/* R0为最高优先级就绪态任务的任务栈指针*/
ldr r0, [ r1 ]
/* 从最高优先级就绪态任务的任务栈中出栈R4~R11和R14
* 注意:这里出栈的R14为EXC_RETURN,其保存了任务是否使用浮点单元的信息
*/
ldmia r0!, {r4-r11, r14}
/* 此时R14为EXC_RETURN,通过判断EXC_RETURN的bit4是否为0,
* 判断任务是否使用的浮点单元,
* 若使用了浮点单元,则需要从任务的任务栈中恢复出浮点寄存器的值
*/
tst r14, # 0x10
    it eq
    vldmiaeq r0!, {
            s16 - s31
        }
        /* 更新PSP为任务切换后的任务栈指针*/
    msr psp, r0
    isb
    /* 用于修改XMC4000的BUG,不用理会*/
    # ifdef WORKAROUND_PMU_CM001#
    if WORKAROUND_PMU_CM001 == 1
    push {
        r14
    }
    pop {
        pc
    }
    nop# endif# endif
    /* 跳转到切换后的任务运行
     * 执行此指令,CPU会自动从PSP指向的任务栈中,
     * 出栈R0、R1、R2、R3、R12、LR、PC、xPSR寄存器,
     * 接着CPU就跳转到PC指向的代码位置运行,
     * 也就是任务上次切换时运行到的位置
     */
    bx r14
}

从上面的代码可以看出,FreeRTOS 在进行任务切换的时候,会将CPU 的运行状态,在当
前任务在进行任务切换前,进行保存,保存到任务的任务栈中,然后从切换后运行任务的任务
栈中恢复切换后运行任务在上一次被切换时保存的CPU 信息。
但是从PendSV 的中断回调函数代码中,只看到程序保存和恢复的CPU 信息中的部分寄存
器信息(R4 寄存器~R11 寄存器),这是因为硬件会自动出栈和入栈其他CPU 寄存器的信息。
在任务运行的时候,CPU 使用PSP 作为栈空间使用,也就是使用运行任务的任务栈。当
SysTick 中断(SysTick 的中断服务函数会判断是否需要进行任务切换,相关内容在后续章节会
进行讲解)发生时,在跳转到SysTick 中断服务函数运行前,硬件会自动将除R4~R11 寄存器的
其他CPU 寄存器入栈,因此就将任务切换前CPU 的部分信息保存到对应任务的任务栈中。当
退出PendSV 时,会自动从栈空间中恢复这部分CPU 信息,以共任务正常运行。
因此在PendSV 中断服务函数中,主要要做的事情就是,保存硬件不会自动入栈的CPU 信
息,已经确定写一个要运行的任务,并将pxCurrentTCB 指向该任务的任务控制块,然后更新
PSP 指针为该任务的任务堆栈指针。

FreeRTOS 确定下一个要运行的任务

从上一小节中可以看到,在PendSV 的中断服务函数中,调用了函数vTaskSwitchContext()
来确定写一个要运行的任务。

函数vTaskSwitchContext()

函数vTaskSwitchContext()在task.c 文件中有定义,具体的代码如下所示:

void vTaskSwitchContext(void) {
    /* 判断任务调度器是否运行*/
    if (uxSchedulerSuspended != (UBaseType_t) pdFALSE) {
        /* 此全局变量用于在系统运行的任意时刻标记需要进行任务切换
         * 会在SysTick的中断服务函数中统一处理
         * 任务任务调度器没有运行,不允许任务切换,
         * 因此将xYieldPending设置为pdTRUE
         * 那么系统会在SysTick的中断服务函数中持续发起任务切换
         * 直到任务调度器运行
         */
        xYieldPending = pdTRUE;
    } else {
        /* 可以执行任务切换,因此将xYieldPending设置为pdFALSE */
        xYieldPending = pdFALSE;
        /* 用于调试,不用理会*/
        traceTASK_SWITCHED_OUT();
        /* 此宏用于使能任务运行时间统计功能,不用理会*/
        #
        if (configGENERATE_RUN_TIME_STATS == 1) {#
            ifdef portALT_GET_RUN_TIME_COUNTER_VALUE
            portALT_GET_RUN_TIME_COUNTER_VALUE(ulTotalRunTime);#
            else
                ulTotalRunTime = portGET_RUN_TIME_COUNTER_VALUE();#
            endif
            if (ulTotalRunTime > ulTaskSwitchedInTime) {
                pxCurrentTCB - > ulRunTimeCounter +=
                    (ulTotalRunTime - ulTaskSwitchedInTime);
            } else {
                mtCOVERAGE_TEST_MARKER();
            }
            ulTaskSwitchedInTime = ulTotalRunTime;
        }#
        endif
        /* 检查任务栈是否溢出,
         * 未定义,不用理会
         */
        taskCHECK_FOR_STACK_OVERFLOW();
        /* 此宏为POSIX相关配置,不用理会*/
        #
        if (configUSE_POSIX_ERRNO == 1) {
            pxCurrentTCB - > iTaskErrno = FreeRTOS_errno;
        }#
        endif
        /* 将pxCurrentTCB指向优先级最高的就绪态任务
         * 有两种方法,由FreeRTOSConfig.h文件配置决定
         */
        taskSELECT_HIGHEST_PRIORITY_TASK();
        /* 用于调试,不用理会*/
        traceTASK_SWITCHED_IN();
        /* 此宏为POSIX相关配置,不用理会*/
        #
        if (configUSE_POSIX_ERRNO == 1) {
            FreeRTOS_errno = pxCurrentTCB - > iTaskErrno;
        }#
        endif
        /* 此宏为Newlib相关配置,不用理会*/
        #
        if (configUSE_NEWLIB_REENTRANT == 1) {
            _impure_ptr = & (pxCurrentTCB - > xNewLib_reent);
        }#
        endif
    }
}

函数vTaskSwitchContext()调用了函数taskSELECT_HIGHEST_PRIORITY_TASK(),来将
pxCurrentTCB 设置为指向优先级最高的就绪态任务。

函数taskSELECT_HIGHEST_PRIORITY_TASK()

函数taskSELECT_HIGHEST_PRIORITY_TASK()用于将pcCurrentTCB 设置为优先级最高
的就绪态任务,因此该函数会使用位图的方式在任务优先级记录中查找优先级最高任务优先等
级,然后根据这个优先等级,到对应的就绪态任务列表在中取任务。
FreeRTOS 提供了两种从任务优先级记录中查找优先级最高任务优先等级的方式,一种是
由纯C 代码实现的,这种方式适用于所有运行FreeRTOS 的MCU;另外一种方式则是使用了硬
件计算前导零的指令,因此这种方式并不适用于所有运行FreeRTOS 的MCU,而仅适用于具有
有相应硬件指令的MCU。正点原子所有板卡所使用的STM32 MCU 都支持以上两种方式。具
体使用哪种方式,用户可以在FreeRTOSConfig.h 文件中进行配置,配置方法,请查看第三章
《FreeRTOS 系统配置》的相关章节。
软件方式实现的函数taskSELECT_HIGHEST_PRIORITY_TASK()是一个宏定义,在task.c
文件中由定义,具体的代码如下所示:

#
define taskSELECT_HIGHEST_PRIORITY_TASK()\ {\
    /* 全局变量uxTopReadyPriority以位图方式记录了系统中存在任务的优先级*/
    \
    /* 将遍历的起始优先级设置为这个全局变量,*/
    \
    /* 而无需从系统支持优先级的最大值开始遍历,*/
    \
    /* 可以节约一定的遍历时间*/
    \
    UBaseType_t uxTopPriority = uxTopReadyPriority;\\
    /* Find the highest priority queue that contains ready tasks. */
    \
    /* 按照优先级从高到低,判断对应的就绪态任务列表中是否由任务,*/
    \
    /* 找到存在任务的最高优先级就绪态任务列表后,退出遍历*/
    \
    while (listLIST_IS_EMPTY( & (pxReadyTasksLists[uxTopPriority])))\ {\
        configASSERT(uxTopPriority);\
        --uxTopPriority;\
    }\\
    /* 从找到了就绪态任务列表中取下一个任务,*/
    \
    /* 让pxCurrentTCB指向这个任务的任务控制块*/
    \
    listGET_OWNER_OF_NEXT_ENTRY(pxCurrentTCB, \ & (pxReadyTasksLists[uxTopPriority]));\
    /* 更新任务优先级记录*/
    \
    uxTopReadyPriority = uxTopPriority;\
}

依靠特定硬件指令实现的函数taskSELECT_HIGHEST_PRIORITY_TASK()是一个宏定义,
在task.c 文件中有定义,具体的代码如下所示:

#define taskSELECT_HIGHEST_PRIORITY_TASK() \
{ \
	UBaseType_t uxTopPriority; \
	\
	/* 使用硬件方式从任务优先级记录中获取最高的任务优先等级*/ \
	portGET_HIGHEST_PRIORITY( uxTopPriority, uxTopReadyPriority ); \
	configASSERT( listCURRENT_LIST_LENGTH( \
	&( pxReadyTasksLists[ uxTopPriority ] ) ) > \
	0 ); \
	/* 从获取的任务优先级对应的就绪态任务列表中取下一个任务*/ \
	/* 让pxCurrentTCB指向这个任务的任务控制块*/ \
	listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, \
	&( pxReadyTasksLists[ uxTopPriority ] ) ); \
}

在使用硬件方式实现的函数taskSELECT_HIGHEST_PRIORITY_TASK()中调用了函数
portGET_HIGHEST_PRIORITY() 来计算任务优先级记录中的最高任务优先级,函数
portGET_HIGHEST_PRIORITY()实际上是一个宏定义,在portmacro.h 文件中有定义,具体的代
码如下所示:

#define portGET_HIGHEST_PRIORITY( uxTopPriority, uxReadyPriorities ) \
uxTopPriority = \
( 31UL - ( uint32_t ) __clz( ( uxReadyPriorities ) ) )

可以看到,宏portGET_HIGHEST_PRIORITY() 使用了__clz 这个硬件指定来计算
uxReadyPriorities 的前导零,然后使用31(变量uxReadyPriorities 的最大比特位)减去得到的前
导零,那么就得到了变量uxReadyPriorities 中,最高位1 的比特位。使用此方法就限制了系统
最大的优先级数量不能超多32,即最高优先等级位31,不过对于绝大多数的应用场合,32 个
任务优先级等级已经足够使用了。

PendSV 异常何时触发

PendSV 异常用于进行任务切换,当需要进行任务切换的时候,FreeRTOS 就会触发PendSV
异常,以进行任务切换。
FreeRTOS 提供了多个用于触发任务切换的宏,如下所示:

#
if (configUSE_PREEMPTION == 0)# define taskYIELD_IF_USING_PREEMPTION()#
else# define taskYIELD_IF_USING_PREEMPTION() portYIELD_WITHIN_API()# endif#
if (configUSE_PREEMPTION == 0)# define queueYIELD_IF_USING_PREEMPTION()#
else# define queueYIELD_IF_USING_PREEMPTION() portYIELD_WITHIN_API()# endif# define portEND_SWITCHING_ISR(xSwitchRequired)\
do\ {\
    if (xSwitchRequired != pdFALSE)\
        portYIELD();\
}\
while (0)# define portYIELD_FROM_ISR(x) portEND_SWITCHING_ISR(x)# define taskYIELD() portYIELD()# define portYIELD_WITHIN_API portYIELD

从上面的代码中可以看到,这些后实际上最终都是调用了函数portYIELD(),函数实际上是
一个宏定义,在portmacro.h 文件中有定于,具体的代码如下所示:

#define portYIELD() \
{ \
	/* 设置中断控制状态寄存器,以触发PendSV异常*/ \
	portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; \
	\
	__dsb( portSY_FULL_READ_WRITE ); \
	__isb( portSY_FULL_READ_WRITE ); \
}

上面代码中宏portNVIC_INT_CTRL_REG 和宏portNVIC_PENDSVSET_BIT 在portmacro.h
文件中有定义,具体的代码如下所示:

#define portNVIC_INT_CTRL_REG ( *( ( volatile uint32_t * ) 0xe000ed04 ) )
#define portNVIC_PENDSVSET_BIT ( 1UL << 28UL )

中断控制状态寄存器的有关内容,请参考第4.1.5 小节《中断控制状态寄存器》。

FreeRTOS 时间片调度实验

功能设计

  1. 例程功能
    本实验主要用于学习FreeRTOS 的时间片调度,了解FreeRTOS 任务切换的结果,时间片调
    度的相关内容,请参考第5.4.2 小节《时间片调度》。本实验设计了三个任务,这三个任务的功
    能如下表所示:
    在这里插入图片描述
    该实验的实验工程,请参考《FreeRTOS 实验例程9 FreeRTOS 时间片调度实验》。

软件设计

  1. 程序流程图
    本实验的程序流程图,如下图所示:
    在这里插入图片描述
  2. FreeRTOS 函数解析
    (1) 函数taskENTER_CRITICAL()
    此函数是一个宏定义,此宏的具体解析,请参考4.3.3 小节《FreeRTOS 进出临界区》。
    (2) 函数taskEXIT_CRITICAL()
    此函数是一个宏定义,此宏的具体解析,请参考4.3.3 小节《FreeRTOS 进出临界区》。
  3. 程序解析
    整体的代码结构,请参考2.1.6 小节,本小节着重讲解本实验相关的部分。
    (1) start_task 任务
    start_task 任务的入口函数代码如下所示:
/**
 * @brief start_task
 * @param pvParameters : 传入参数(未用到)
 * @retval 无
 */
void start_task(void * pvParameters) {
    taskENTER_CRITICAL(); /* 进入临界区*/
    /* 创建任务1 */
    xTaskCreate((TaskFunction_t) task1, (const char * )
        "task1", (uint16_t) TASK1_STK_SIZE, (void * ) NULL, (UBaseType_t) TASK1_PRIO, (TaskHandle_t * ) & Task1Task_Handler);
    /* 创建任务2 */
    xTaskCreate((TaskFunction_t) task2, (const char * )
        "task2", (uint16_t) TASK2_STK_SIZE, (void * ) NULL, (UBaseType_t) TASK2_PRIO, (TaskHandle_t * ) & Task2Task_Handler);
    vTaskDelete(StartTask_Handler); /* 删除开始任务*/
    taskEXIT_CRITICAL(); /* 退出临界区*/
}

start_task 任务主要用于创建task1 和task2 任务,这里要注意的是,由于本实验要展示的是
FreeRTOS 的时间片调度,时间片调度是对于任务优先级相同的多个任务而言的,因此创建用于
测试FreeRTOS 时间片调度的task1 和task2 任务的任务优先级必须相同。
(2) task1 和task2 任务

/**
 * @brief task1
 * @param pvParameters : 传入参数(未用到)
 * @retval 无
 */
void task1(void * pvParameters) {
        uint32_t task1_num = 0;
        while (1) {
            taskENTER_CRITICAL();
            printf("任务1运行次数: %d\r\n", ++task1_num);
            taskEXIT_CRITICAL();
        }
    }
    /**
     * @brief task2
     * @param pvParameters : 传入参数(未用到)
     * @retval 无
     */
void task2(void * pvParameters) {
    uint32_t task2_num = 0;
    while (1) {
        taskENTER_CRITICAL();
        printf("任务2运行次数: %d\r\n", ++task2_num);
        taskEXIT_CRITICAL();
    }
}

可以看到,task1 和task2 任务都是循环打印任务运行的次数,并没有进行任务延时,因此
task1 和task2 任务会由于时间片调度,在任务调度器的调度下轮询运行。值得一提的是,在打
印任务运行次数的时候,需要使用到串口硬件,为了避免多个任务“同时”使用同一个硬件,
因此在使用串口硬件打印任务运行次数之前,进入临界区,在使用串口硬件打印任务运行次数
之后,再退出临界区。
由于task1 和task2 的任务优先级相同,因此可以猜测,在时间片的调度下,task1 和task2
任务应该轮询打印各自的任务运行次数。

下载验证

编译并下载代码,复位后可以看到LCD 屏幕上显示了本次实验的相关信息,如下图所示:
在这里插入图片描述
同时,通过串口调试助手就能看到本次实验的结果,如下图所示:
图9.5.3.2 串口调试助手显示内容

从上图可以看到,task1 和task2 任务的运行情况,与猜测的相同,task1 和task2 任务交替
轮询运行,符合时间片调度下的运行情况。

FreeRTOS 内核控制函数

FreeRTOS 提供了一些用于控制内核的API 函数,这些API 函数主要包含了进出临界区、
开关中断、启停任务调度器等一系列用于控制内核的API 函数。本章就来学习FreeRTOS 的内
核控制函数。

FreeRTOS 内核控制函数预览

在FreeRTOS 官方在线文档的网页页面中,可以看到官方列出的FreeRTOS 内核控制函数,
如下图所示:
在这里插入图片描述
以上FreeRTOS 内核的控制函数描述,如下表所示:
在这里插入图片描述
在这里插入图片描述

FreeRTOS 内核控制函数详解

  1. 函数taskYIELD()
    此函数用于请求切换任务,调用后会触发PendSV 中断,请参考第9.4 小节《PendSV 异常
    何时触发》。
  2. 函数taskENTER_CRITICAL()
    此函数用于在任务中进入临界区,请参考第4.3.3 小节《FreeRTOS 进出临界区》。
  3. 函数taskEXIT_CRITICAL()
    此函数用于在任务中退出临界区,请参考第4.3.3 小节《FreeRTOS 进出临界区》。
  4. 函数taskENTER_CRITICAL_FROM_ISR()
    此函数用于在中断服务函数中进入临界区,请参考第4.3.3 小节《FreeRTOS 进出临界区》。
  5. 函数taskEXIT_CRITICAL_FROM_ISR()
    此函数用于在中断服务函数中退出临界区,请参考第4.3.3 小节《FreeRTOS 进出临界区》。
  6. 函数taskDISABLE_INTERRUPTS()
    此函数用于关闭受FreeRTOS 管理的中断,请参考第4.3.2 小节《FreeRTOS 开关中断》。
  7. 函数taskENABLE_INTERRUPTS()
    此函数用于开启所有中断,请参考第4.3.2 小节《FreeRTOS 开关中断》。
  8. 函数vTaskStartScheduler()
    此函数用于开启任务调度器,请参考第8.1.1 小节《函数vTaskStartScheduler()》。
  9. 函数vTaskEndScheduler()
    此函数用于关闭任务调度器,要注意的是,此函数只适用于X86 架构的PC 端。对于STM32
    平台,调用此函数会关闭受FreeRTOS 管理的中断,并强制触发断言。代码如下所示:
void vTaskEndScheduler(void) {
    /* 关闭受FreeRTOS管理的中断*/
    portDISABLE_INTERRUPTS();
    /* 标记任务调度器未运行*/
    xSchedulerRunning = pdFALSE;
    vPortEndScheduler();
}
void vPortEndScheduler(void) {
    /* 强制断言*/
    configASSERT(uxCriticalNesting == 1000 UL);
}
  1. 函数vTaskSuspendAll()
    此函数用于挂起任务调度器,当任务调度器被挂起后,就不能进行任务切换,直到任务调
    度器恢复运行。此函数的代码如下所示:
void vTaskSuspendAll( void )
{
	/* 未定义,不用理会*/
	portSOFTWARE_BARRIER();
	/* 任务调度器挂起计数器加1 */
	++uxSchedulerSuspended;
	/* 未定义,不用理会*/
	portMEMORY_BARRIER();
}

从上面的代码可以看出,函数vTaskSuspendAll()挂起任务调度器的操作是可以递归的,也
就是说,可以重复多次挂起任务调度器,只要后续调用相同次数的函数xTaskResumeAll()来恢
复任务调度器运行即可。函数vTaskSuspendAll()挂起任务调度器的操作就是将任务调度器挂起
计数器(uxSchedulerSuspended)的值加1。在FreeRTOS 的源码中会通过任务调度器挂起计数
器的值是否为0,来判断任务调度器时候被挂起,如果任务调度器被挂起,FreeRTOS 就不会进
行任务切换等操作,如函数vTaskSwitchContext() ,请参考第9.3.1 小节《函数
vTaskSwitchContext()》。
11. 函数xTaskResumeAll()
此函数用于恢复任务调度器运行,要注意的是,任务调度器的挂起是可递归的,因此需要
使用此函数恢复任务调度器与任务调度器被挂起相同的次数,才能恢复任务调度器运行。此函
数的代码如下所示:

BaseType_t xTaskResumeAll(void) {
    TCB_t * pxTCB = NULL;
    BaseType_t xAlreadyYielded = pdFALSE;
    /* 不会恢复没有被挂起的任务调度器
     * 当uxSchedulerSuspended为0时,
     * 表示任务调度器没有被挂起
     */
    configASSERT(uxSchedulerSuspended);
    /* 进入临界区*/
    taskENTER_CRITICAL(); {
        /* 任务调度器挂起计数器减1 */
        --uxSchedulerSuspended;
        /* 如果任务调度器挂起计数器减到0
         * 说明任务调度器可以恢复运行了
         */
        if (uxSchedulerSuspended == (UBaseType_t) pdFALSE) {
            /* 任务数量计数器大于0
             * 说明系统中有任务,
             * 因此需要作向相应地处理
             */
            if (uxCurrentNumberOfTasks > (UBaseType_t) 0 U) {
                /* 将所有挂起态任务添加到就绪态任务列表中
                 * 同时,如果被恢复的挂起态任务的优先级比当前运行任务的优先级高,
                 * 则标记需要进行任务切换
                 */
                while (listLIST_IS_EMPTY( & xPendingReadyList) == pdFALSE) {
                    pxTCB =
                        listGET_OWNER_OF_HEAD_ENTRY(( & xPendingReadyList));
                    listREMOVE_ITEM( & (pxTCB - > xEventListItem));
                    portMEMORY_BARRIER();
                    listREMOVE_ITEM( & (pxTCB - > xStateListItem));
                    prvAddTaskToReadyList(pxTCB);
                    if (pxTCB - > uxPriority >= pxCurrentTCB - > uxPriority) {
                        xYieldPending = pdTRUE;
                    } else {
                        mtCOVERAGE_TEST_MARKER();
                    }
                }
                /* 如果pxTCB非空,
                 * 则表示在任务调度器挂起期间,
                 * 有阻塞任务超时,
                 * 因此需要重新计算下一个任务阻塞超时的时间
                 */
                if (pxTCB != NULL) {
                    /* 重新计算下一个任务的阻塞超时时间*/
                    prvResetNextTaskUnblockTime();
                }
                /* 处理在任务调度器挂起期间,未处理的系统使用节拍
                 * 这样可以保证正确地计算阻塞任务的阻塞超时时间
                 * 处理方式就是调用相同次数的函数xTaskIncrementTick()
                 */
                {
                    TickType_t xPendedCounts = xPendedTicks;
                    if (xPendedCounts > (TickType_t) 0 U) {
                        do {
                            /* 调用函数xTaskIncrementTick() */
                            if (xTaskIncrementTick() != pdFALSE) {
                                xYieldPending = pdTRUE;
                            } else {
                                mtCOVERAGE_TEST_MARKER();
                            }
                            --xPendedCounts;
                        } while (xPendedCounts > (TickType_t) 0 U);
                        xPendedTicks = 0;
                    } else {
                        mtCOVERAGE_TEST_MARKER();
                    }
                }
                /* 根据需要进行任务切换*/
                if (xYieldPending != pdFALSE) {#
                    if (configUSE_PREEMPTION != 0) {
                        xAlreadyYielded = pdTRUE;
                    }#
                    endif
                    taskYIELD_IF_USING_PREEMPTION();
                } else {
                    mtCOVERAGE_TEST_MARKER();
                }
            }
        } else {
            mtCOVERAGE_TEST_MARKER();
        }
    }
    /* 退出临界区*/
    taskEXIT_CRITICAL();
    return xAlreadyYielded;
}
  1. 函数vTaskStepTick()
    此函数用于设置系统时钟节拍计数器的值,可以设置系统时钟节拍计数器的值为当前值加
    上指定值,不过要注意的值,更新后系统时钟节拍计数器的值,不能超过下一个任务阻塞超时
    时间。具体的代码如下所示:
void vTaskStepTick( const TickType_t xTicksToJump )
{
	/* 系统使用节拍计数器更新后的值
	* 不能超过下一个任务阻塞超时时间
	*/
	configASSERT( ( xTickCount + xTicksToJump ) <= xNextTaskUnblockTime );
	/* 更新系统时钟节拍计数器*/
	xTickCount += xTicksToJump;
	/* 用于调试,不用理会*/
	traceINCREASE_TICK_COUNT( xTicksToJump );
}
  1. 函数xTaskCatchUpTicks()
    此函数用于修正中断后的系统时钟节拍,主要是用过更新全局变量xPendedTicks 实现的,
    全局变量xPendedTicks 用于计数系统使用节拍在任务调度器挂起时被忽略处理的次数。具体的
    代码如下所示:
BaseType_t xTaskCatchUpTicks(TickType_t xTicksToCatchUp) {
    BaseType_t xYieldOccurred;
    /* 该函数不能在任务调度器被挂起期间被调用*/
    configASSERT(uxSchedulerSuspended == 0);
    /* 挂起任务调度器*/
    vTaskSuspendAll();
    /* 更新xPendedTicks */
    xPendedTicks += xTicksToCatchUp;
    /* 恢复任务调度器运行*/
    xYieldOccurred = xTaskResumeAll();
    return xYieldOccurred;
}

FreeRTOS 其他任务API 函数

通过前面几章的学习,了解了FreeRTOS 任务管理的相关内容,但仅涉及了任务创建、删
除、挂起和恢复等几个任务相关的API 函数。除此之外,FreeRTOS 还提供了很多与任务相关的
API 函数,通过这些函数,用户可以更加灵活地使用FreeRTOS。本章就来学习FreeRTOS 中一
些其他的任务API 函数。

FreeRTOS 任务相关API 函数

FreeRTOS 任务相关API 函数预览

在FreeRTOS 官方在线文档的网页页面中,通过查看API 参考,可以看到官方列出的
FreeRTOS 任务相关的API 函数,如下图所示:
在这里插入图片描述
以上部分FreeRTOS 任务相关的API 函数描述,如下表所示:
在这里插入图片描述
在这里插入图片描述

FreeRTOS 任务相关API 函数详解

  1. 函数uxTaskPriorityGet()
    此函数用于获取指定任务的任务优先级,若使用此函数,需在FreeRTOSConfig.h 文件中设
    置配置项INCLUDE_uxTaskPriorityGet 为1,此函数的函数原型如下所示:
    UBaseType_t uxTaskPriorityGet(const TaskHandle_t xTask);
    函数uxTaskPriorityGet()的形参描述,如下表所示:

  2. 函数vTaskPrioritySet()
    此函数用于设置指定任务的优先级,若使用此函数,需在FreeRTOSConfig.h 文件中设置配
    置项INCLUDE_vTaskPrioritySet 为1,此函数的函数原型如下所示:

void vTaskPrioritySet(
TaskHandle_t xTask,
UBaseType_t uxNewPriority);

函数vTaskPrioritySet()的形参描述,如下表所示:
在这里插入图片描述
函数vTaskPrioritySet()无返回值。
3. 函数uxTaskGetSystemState()
此函数用于获取所有任务的状态信息,若使用此函数,需在FreeRTOSConfig.h 文件中设置
配置项configUSE_TRACE_FACILITY 为1,此函数的函数原型如下所示:

UBaseType_t uxTaskGetSystemState(
TaskStatus_t * const pxTaskStatusArray,
const UBaseType_t uxArraySize,
configRUN_TIME_COUNTER_TYPE * const pulTotalRunTime);

函数uxTaskGetSystemState()的形参描述,如下表所示:
在这里插入图片描述
函数uxTaskGetSystemState()的返回值,如下表所示:
在这里插入图片描述
函数uxTaskGetSystemState()的形参pxTaskStatusArray 指向变量类型为TaskStatus_t 的变量
的首地址,可以是一个数组,用来存放多个TaskStatus_t 类型的变量,函数uxTaskGetSystemState()
使用将任务的状态信息,写入到该数组中,形参uxArraySize 指示该数组的大小,其中变量类型
TaskStatus_t 的定义如下所示:

typedef struct xTASK_STATUS {
    TaskHandle_t xHandle; /* 任务句柄*/
    const char * pcTaskName; /* 任务名*/
    UBaseType_t xTaskNumber; /* 任务编号*/
    eTaskState eCurrentState; /* 任务状态*/
    UBaseType_t uxCurrentPriority; /* 任务优先级*/
    UBaseType_t uxBasePriority; /* 任务原始优先级*/
    configRUN_TIME_COUNTER_TYPE ulRunTimeCounter; /* 任务被分配的运行时间*/
    StackType_t * pxStackBase; /* 任务栈的基地址*/
    configSTACK_DEPTH_TYPE usStackHighWaterMark; /* 任务栈历史剩余最小值*/
}
TaskStatus_t;

该结构体变量就包含了任务的一些状态信息,获取到的每个任务都有与之对应的
TaskStatus_t 结构体来保存该任务的状态信息。
4. 函数vTaskGetInfo()
此函数用于获取指定任务的任务信息,若使用此函数,需在FreeRTOSConfig.h 文件中设置
配置项configUSE_TRACE_FACILITY 为1,此函数的函数原型如下所示:

void vTaskGetInfo(
TaskHandle_t xTask,
TaskStatus_t * pxTaskStatus,
BaseType_t xGetFreeStackSpace,
eTaskState eState);

函数vTaskGetInfo()的形参描述,如下表所示:
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/499202.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

蓝牙设备的名称与MAC地址及UUID

每个蓝牙设备都具有各自的地址和名称&#xff0c;他们之间通过唯一通过地址和名称进行数据交互。本文详细讲述了蓝牙设备的名称和地址的格式及作用。 名称 蓝牙设备具有各自的名称&#xff0c;通常为字母与数字的组合. MAC地址 与Ethernet相同&#xff0c;MAC地址为48bit的…

VTK安装路径检查

/usr/include/vtk-7.1——————VTK头文件

番剧更新表及番剧详情数据库

访问【WRITE-BUG数字空间】_[内附完整源码和文档] 该项目立足于目前各大平台网站的番剧信息较为分散&#xff0c;用户需要辗转多个平台才能获取较为完整的番剧信息的背景下&#xff0c;实现了各大平台网站番剧信息的整合。将各大平台网站的番剧更新信息及番剧详情信息整合制表…

MATLAB 之 基本概述

文章目录 一、MATLAB 主要功能1. 数值计算功能2. 符号计算功能3. 绘图功能4. 程序设计语言功能5. 工具箱的扩展功能 二、MATLAB 操作界面1. 主窗口2. 命令行窗口3. 当前文件夹窗口4. 工作区窗口5. 搜索路径 三、MATLAB 基本操作1. 交互式命令操作1.1 命令行1.2 续行符1.3 命令行…

开关电源基础01:电源变换器基础(1)-关于缘起

说在开头 我相信各位胖友们通过对《阻容感基础》&#xff0c;《信号完整性基础》以及《半导体器件基础》艰苦卓绝地钻研&#xff0c;已为 “硬功夫” 这门绝世武功&#xff0c;打下了坚实的入门基础&#xff0c;入门之日简直就是指日可待&#xff08;我xxx&#xff0c;都半年了…

【数据结构】单链表详解

☃️个人主页&#xff1a;fighting小泽 &#x1f338;作者简介&#xff1a;目前正在学习C语言和数据结构 &#x1f33c;博客专栏&#xff1a;数据结构 &#x1f3f5;️欢迎关注&#xff1a;评论&#x1f44a;&#x1f3fb;点赞&#x1f44d;&#x1f3fb;留言&#x1f4aa;&…

阿里云服务器镜像系统怎么选择?超详细教程

阿里云服务器镜像怎么选择&#xff1f;云服务器操作系统镜像分为Linux和Windows两大类&#xff0c;Linux可以选择Alibaba Cloud Linux&#xff0c;Windows可以选择Windows Server 2022数据中心版64位中文版&#xff0c;阿里云百科来详细说下阿里云服务器操作系统有哪些&#xf…

Buf 教程 - 使用 Protobuf 生成 Golang 代码和 Typescript 类型定义

简介 Buf 是一款更高效、开发者友好的 Protobuf API 管理工具&#xff0c;不仅支持代码生成&#xff0c;还支持插件和 Protobuf 格式化。 我们可以使用 Buf 替代原本基于 Protoc 的代码生成流程&#xff0c;一方面可以统一管理团队 Protoc 插件的版本、代码生成配置&#xff…

测试之路,2023年软件测试市场领域有哪些变化?突破走得更远...

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 Python自动化测试&…

Linux - 第12节 - 网络编程套接字

1.预备知识 1.1.理解源IP地址和目的IP地址 因特网上的每台计算机都有一个唯一的IP地址&#xff0c;如果一台主机上的数据要传输到另一台主机&#xff0c;那么对端主机的IP地址就应该作为该数据传输时的目的IP地址。但仅仅知道目的IP地址是不够的&#xff0c;当对端主机收到该数…

【Java校招面试】基础知识(七)——数据库

目录 前言一、数据库索引二、数据库锁三、数据库事务四、数据库连接池后记 前言 本篇主要介绍数据库的相关内容。 “基础知识”是本专栏的第一个部分&#xff0c;本篇博文是第六篇博文&#xff0c;如有需要&#xff0c;可&#xff1a; 点击这里&#xff0c;返回本专栏的索引文…

Sourcetree介绍及使用

Sourcetree是一个操作简单但功能强大的免费Git客户端管理工具&#xff0c;可应用在Windows和Mac平台。 Sourcetree的安装&#xff1a; 1.从Sourcetree | Free Git GUI for Mac and Windows 下载SourceTreeSetup-3.4.12.exe&#xff1b; 2.双击SourceTreeSetup-3.4.12.exe&#…

【C++】动态规划

参考博客&#xff1a;动态规划详解 1. 什么是动态规划 动态规划&#xff08;英语&#xff1a;Dynamic programming&#xff0c;简称 DP&#xff09;&#xff0c;是一种在数学、管理科学、计算机科学、经济学和生物信息学中使用的&#xff0c;通过把原问题分解为相对简单的子问…

Linux LED 驱动开发实验

1、LED 灯驱动原理 Linux 下的任何外设驱动&#xff0c;最终都是要配置相应的硬件寄存器。LED 灯驱动最 终也是对 I.MX6ULL 的 IO 口进行配置&#xff0c;在 Linux 下编写驱动要符合 Linux 的驱动框架。I.MX6U-ALPHA 开发板上的 LED 连接到 I.MX6ULL 的 GPIO1_IO03 这个引脚上&…

Day963.如何拆分数据 -遗留系统现代化实战

如何拆分数据 Hi&#xff0c;我是阿昌&#xff0c;今天学习记录的是关于如何拆分数据的内容。 如何拆分数据&#xff0c;这个场景在建设新老城区&#xff0c;甚至与其他城市&#xff08;外部系统&#xff09;交互时都非常重要。 作为开发人员&#xff0c;理想中的业务数据存…

C++《vector类的使用介绍》

本文主要介绍vector一些常见的接口函数的使用 文章目录 一、vector的介绍二、vector的使用2.1vector构造函数2.2迭代器的使用2.3空间增长问题2.4增删查改问题 一、vector的介绍 vector是表示可变大小数组的序列容器。就像数组一样&#xff0c;vector也采用的连续存储空间来存储…

比赛记录:Codeforces Round 871 (Div. 4) A~H

传送门:CF A题:A. Love Story 简单比对一下即可解决 #include <bits/stdc.h> using namespace std; typedef long long ll; #define root 1,n,1 #define ls rt<<1 #define rs rt<<1|1 #define lson l,mid,rt<<1 #define rson mid1,r,rt<<1|1 …

模拟银行账户转账业务

文章目录 一、需求分析二、核心代码1. 业务层添加 Spring 事务管理2. 配置类中设置事务管理器3. 开启注解式事务驱动 三、相关截图 一、需求分析 需求&#xff1a; 实现任意两个账户间转账操作&#xff0c;要求当转账过程出现异常时&#xff0c;转账方与被转账方的转账操作同时…

操作系统笔记--CPU调度

1--基本概念 CPU调度&#xff1a; 从进程的就绪队列中挑选一个进程/线程作为CPU将要运行的下一个进程/线程&#xff1b; 在下图中&#xff0c;进程产生状态转换时&#xff08;运行→结束、运行→等待&#xff0c;等等&#xff09;都会发生相应的CPU调度&#xff1b; 内核运行调…

2023/5/7周报

目录 摘要 论文阅读 1、标题和现存问题 2、循环神经网络和传统 LSTM 3、堆叠 LSTM和论文模型结构 4、实验准备 5、结果分析 深度学习 1、TGCN 2、公式 3、伪代码 总结 摘要 本周在论文阅读上&#xff0c;阅读了一篇基于注意力机制的堆叠LSTM心电预测算法的论文。模…