HAL库版FreeRTOS(中)

文章探讨了FreeRTOS实时操作系统的任务管理核心,包括PendSV异常触发的任务切换机制,时间片调度,以及互斥与递归互斥信号量在解决优先级翻转问题中的应用。还介绍了队列、软件定时器等组件,强调了这些机制对系统效率与稳定性的关键作用。
摘要由CSDN通过智能技术生成

目录

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()的形参描述,如下表所示:

  1. 函数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()的形参描述,如下表所示:
在这里插入图片描述
函数vTaskGetInfo()无返回值。
函数vTaskGetInfo()的形参eState 用来表示任务的状态,其变量类型为eTaskState,变量类型eTaskState 的定义如下所示:

typedef enum
{
	eRunning = 0, /* 运行态*/
	eReady, /* 就绪态*/
	eBlocked, /* 阻塞态*/
	eSuspended, /* 挂起态*/
	eDeleted, /* 任务被删除*/
	eInvalid /* 非法值*/
} eTaskState;

形参eState 用于决定形参pxTaskStatus 结构体中成员变量eCurrentState 的值,表示任务的状态,如果传入的eState 为eInvalid,那么eCurrentState 为任务当前的状态,否则eCurrentState为eState。
5. 函数xTaskGetApplicationTaskTag()
此函数用于获取指定任务的Tag,若使用此函数,需在FreeRTOSConfig.h 文件中设置配置项configUSE_APPLICATION_TASK_TAG 为1,此函数的函数原型如下所示:

TaskHookFunction_t xTaskGetApplicationTaskTag(TaskHandle_t xTask);

函数xTaskGetApplicationTaskTag()的形参描述,如下表所示:
在这里插入图片描述
函数xTaskGetApplicationTaskTag()的返回值,如下表所示:
在这里插入图片描述
6. 函数xTaskGetCurrentHandle()
此函数用于获取当前系统正在运行的任务的任务句柄,若使用此函数,需在FreeRTOSConfig.h 文件中设置配置项INCLUDE_xTaskGetCurrentTaskHandle 为1,此函数的函数原型如下所示:

TaskHandle_t xTaskGetCurrentTaskHandle(void);

函数xTaskGetCurrentTaskHandle()无形参。
函数xTaskGetCurrentTaskHandle()的返回值,如下表所示:
在这里插入图片描述
7. 函数xTaskGetHandle()
此函数用于通过任务名获取任务句柄,若使用此函数,需在FreeRTOSConfig.h 文件中设置
配置项INCLUDE_xTaskGetHandle 为1,此函数的函数原型如下所示:

TaskHandle_t xTaskGetHandle(const char * pcNameToQuery);

函数xTaskGetHandle()的形参描述,如下表所示:
在这里插入图片描述
函数xTaskGetHandle()的返回值,如下表所示:
在这里插入图片描述
8. 函数xTaskGetIdleTaskHandle()
此函数用于获取空闲任务的任务句柄,若使用此函数,需在FreeRTOSConfig.h 文件中设置
配置项INCLUDE_xTaskGetIdleTaskHandle 为1,此函数的函数原型如下所示:

TaskHandle_t xTaskGetIdleTaskHandle(void);

函数xTaskGetIdleTaskHandle()无形参。
函数xTaskGetIdleTaskHandle()的返回值,如下表所示:
在这里插入图片描述
9. 函数uxTaskGetStackHighWaterMark()
此函数用于获取指定任务的任务栈的历史剩余最小值,若使用此函数,需在
FreeRTOSConfig.h 文件中设置配置项INCLUDE_uxTaskGetStackHighWaterMark 为1,此函数的函数原型如下所示:

UBaseType_t uxTaskGetStackHighWaterMark(TaskHandle_t xTask);

函数uxTaskGetStackHighWaterMark()的形参描述,如下表所示:
在这里插入图片描述
函数uxTaskGetStackHighWaterMark()的返回值,如下表所示:
在这里插入图片描述
10. 函数eTaskGetState()
此函数用于获取指定任务的状态,若使用此函数,需在FreeRTOSConfig.h 文件中设置配置
项INCLUDE_eTaskGetState 为1,此函数的函数原型如下所示:

eTaskState eTaskGetState(TaskHandle_t xTask);

函数eTaskGetState()的形参描述,如下表所示:
在这里插入图片描述
函数eTaskGetState()的返回值,如下表所示:
在这里插入图片描述
11. 函数pcTaskGetName()
此函数用于获取指定任务的任务名,此函数的函数原型如下所示:

char * pcTaskGetName(TaskHandle_t xTaskToQuery);

函数pcTaskGetName()的形参描述,如下表所示:
在这里插入图片描述
函数pcTaskGetName()的返回值,如下表所示:
在这里插入图片描述
12. 函数xTaskGetTickCount()
此函数用于获取系统时钟节拍计数器的值,此函数的函数原型如下所示:

volatile TickType_t xTaskGetTickCount(void);

函数xTaskGetTickCount()无形参。
函数xTaskGetTickCount()的返回值,如下表所示:
在这里插入图片描述
13. 函数xTaskGetTickCountFromISR()
此函数用于在中断中获取系统时钟节拍计数器的值,此函数的函数原型如下所示:

volatile TickType_t xTaskGetTickCountFromISR(void);

函数xTaskGetTickCountFromISR()无形参。
函数xTaskGetTickCountFromISR()的返回值,如下表所示:
在这里插入图片描述
14. 函数xTaskGetSchedulerState()
此函数用于获取任务调度器的运行状态,此函数的函数原型如下所示:

BaseType_t xTaskGetSchedulerState(void);

函数xTaskGetSchedulerState()无形参。
函数xTaskGetSchedulerState()的返回值,如下表所示:
在这里插入图片描述
15. 函数uxTaskGetNumberOfTasks()
此函数用于获取系统中任务的数量,此函数的函数原型如下所示:

UBaseType_t uxTaskGetNumberOfTasks(void);

函数uxTaskGetNumberOfTasks()无形参。
函数uxTaskGetNumberOfTasks()的返回值,如下表所示:
在这里插入图片描述
16. 函数vTaskList()
此函数用于以“表格”的形式获取系统中任务的信息,若使用此函数,需在FreeRTOSConfig.h
文件中同时设置配置项configUSE_TRACE_FACILITY 和配置项
configUSE_STATS_FORMATTING_FUNCTIONS 为1,此函数的函数原型如下所示:

void vTaskList(char * pcWriteBuffer);

函数vTaskList()的形参描述,如下表所示:
在这里插入图片描述
函数vTaskList()无返回值。
函数vTaskList()获取到的任务信息示例,如下图所示:
在这里插入图片描述
17. 函数vTaskGetRunTimeStats()
此函数用于获取指定任务的运行时间、运行状态等信息,若使用此函数,需在
FreeRTOSConfig.h 文件中同时设置配置项configGENERATE_RUN_TIME_STATS 、
configUSE_STATS_FORMATTING_FUNCTIONS、configSUPPORT_DYNAMIC_ALLOCATION为1,此函数的函数原型如下所示:

void vTaskGetRunTimeStats(char * pcWriteBuffer);

函数vTaskGetRunTimeState()的形参描述,如下表所示:
在这里插入图片描述
函数vTaskGetRunTimeState()无返回值。
18. 函数vTaskSetApplicationTaskTag()
此函数用于设置指定任务的Tag,若使用此函数,需在FreeRTOSConfig.h 文件中设置配置
项configUSE_APPLICATION_TASK_TAG 为1,此函数的函数原型如下所示:

void vTaskSetApplicationTaskTag(
TaskHandle_t xTask,
TaskHookFunction_t pxTagValue);

函数vTaskSetApplicationTaskTag()的形参描述,如下表所示:
在这里插入图片描述
函数vTaskSetApplicationTaskTag()无返回值。
19. 函数SetThreadLocalStoragePointer()
此函数用于设置指定任务的独有数据数组指针,此函数的函数原型如下所示:

void vTaskSetThreadLocalStoragePointer(
TaskHandle_t xTaskToSet,
BaseType_t xIndex,
void * pvValue)

函数SetThreadLocalStoragePointer()的形参描述,如下表所示:
在这里插入图片描述
函数SetThreadLocalStoragePointer()无返回值。
20. 函数GetThreadLocalStoragePointer()
此函数用于获取指定任务的独有数据数组指针,此函数的函数原型如下所示:

void *pvTaskGetThreadLocalStoragePointer(
TaskHandle_t xTaskToQuery,
BaseType_t xIndex);

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

FreeRTOS 任务状态与信息查询实验

功能设计

  1. 例程功能
    本实验主要用于学习FreeRTOS 任务状态与信息的查询API 函数,本实验设计了两个任务,
    这两个任务的功能如下表所示:
    在这里插入图片描述
    该实验的实验工程,请参考《FreeRTOS 实验例程11-1 FreeRTOS 任务状态与信息查询实
    验》。

软件设计

  1. 程序流程图
    本实验的程序流程图,如下图所示:
    在这里插入图片描述
  2. FreeRTOS 函数解析
    (1) 函数uxTaskGetNumberOfTasks()
    此函数用于获取系统中任务的数量,请参考第11.1.2 小节《FreeRTOS 任务相关API 函数
    详解》。
    (2) 函数uxTaskGetSystemState()
    此函数用于获取任务状态信息,请参考第11.1.2 小节《FreeRTOS 任务相关API 函数详解》。
    (3) 函数xTaskGetHandle()
    此函数用于通过任务名获取任务句柄,请参考第11.1.2 小节《FreeRTOS 任务相关API 函
    数详解》。
    (4) 函数vTaskGetInfo()
    此函数用于获取指定任务的信息,请参考第11.1.2 小节《FreeRTOS 任务相关API 函数详
    解》。
    (5) 函数eTaskGetState()
    此函数用于获取指定任务的状态,请参考第11.1.2 小节《FreeRTOS 任务相关API 函数详
    解》。
    (6) 函数vTaskList()
    此函数用于以“表格”形式获取系统中任务的信息,请参考第11.1.2 小节《FreeRTOS 任务
    相关API 函数详解》。
  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);
    vTaskDelete(StartTask_Handler); /* 删除开始任务*/
    taskEXIT_CRITICAL(); /* 退出临界区*/
}

start_task 任务主要用于创建task1 任务。
(2) task1 任务

/**
 * @brief task1
 * @param pvParameters : 传入参数(未用到)
 * @retval 无
 */
void task1(void * pvParameters) {
    uint32_t I = 0;
    UBaseType_t task_num = 0;
    TaskStatus_t * status_array = NULL;
    TaskHandle_t task_handle = NULL;
    TaskStatus_t * task_info = NULL;
    eTaskState task_state = eInvalid;
    char * task_state_str = NULL;
    char * task_info_buf = NULL;
    /* 第一步:函数uxTaskGetSystemState()的使用*/
    printf("/********第一步:函数uxTaskGetSystemState()的使用**********/\r\n");
    task_num = uxTaskGetNumberOfTasks(); /* 获取系统任务数量*/
    status_array = mymalloc(SRAMIN, task_num * sizeof(TaskStatus_t));
    task_num = uxTaskGetSystemState(
        (TaskStatus_t * ) status_array, /* 任务状态信息buffer */ (UBaseType_t) task_num, /* buffer大小*/ (uint32_t * ) NULL); /* 不获取任务运行时间信息*/
    printf("任务名\t\t优先级\t\t任务编号\r\n");
    for (i = 0; i < task_num; i++) {
        printf("%s\t%s%ld\t\t%ld\r\n",
            status_array[i].pcTaskName,
            strlen(status_array[i].pcTaskName) > 7 ? "" : "\t",
            status_array[i].uxCurrentPriority,
            status_array[i].xTaskNumber);
    }
    myfree(SRAMIN, status_array);
    printf("/**************************结束***************************/\r\n");
    printf("按下KEY0键继续!\r\n\r\n\r\n");
    while (key_scan(0) != KEY0_PRES) {
        vTaskDelay(10);
    }
    /* 第二步:函数vTaskGetInfo()的使用*/
    printf("/************第二步:函数vTaskGetInfo()的使用**************/\r\n");
    task_info = mymalloc(SRAMIN, sizeof(TaskStatus_t));
    task_handle = xTaskGetHandle("task1"); /* 获取任务句柄*/
    vTaskGetInfo((TaskHandle_t) task_handle, /* 任务句柄*/ (TaskStatus_t * ) task_info, /* 任务信息buffer */ (BaseType_t) pdTRUE, /* 允许统计任务堆栈历史最小值*/ (eTaskState) eInvalid); /* 获取任务运行状态*/
    printf("任务名:\t\t\t%s\r\n", task_info - > pcTaskName);
    printf("任务编号:\t\t%ld\r\n", task_info - > xTaskNumber);
    printf("任务壮态:\t\t%d\r\n", task_info - > eCurrentState);
    printf("任务当前优先级:\t\t%ld\r\n", task_info - > uxCurrentPriority);
    printf("任务基优先级:\t\t%ld\r\n", task_info - > uxBasePriority);
    printf("任务堆栈基地址:\t\t0x%p\r\n", task_info - > pxStackBase);
    printf("任务堆栈历史剩余最小值:\t%d\r\n", task_info - > usStackHighWaterMark);
    myfree(SRAMIN, task_info);
    printf("/**************************结束***************************/\r\n");
    printf("按下KEY0键继续!\r\n\r\n\r\n");
    while (key_scan(0) != KEY0_PRES) {
        vTaskDelay(10);
    }
    /* 第三步:函数eTaskGetState()的使用*/
    printf("/***********第三步:函数eTaskGetState()的使用*************/\r\n");
    task_state_str = mymalloc(SRAMIN, 10);
    task_handle = xTaskGetHandle("task1");
    task_state = eTaskGetState(task_handle); /* 获取任务运行状态*/
    sprintf(task_state_str, task_state == eRunning ? "Runing" :
        task_state == eReady ? "Ready" :
        task_state == eBlocked ? "Blocked" :
        task_state == eSuspended ? "Suspended" :
        task_state == eDeleted ? "Deleted" :
        task_state == eInvalid ? "Invalid" :
        "");
    printf("任务状态值: %d,对应状态为: %s\r\n", task_state, task_state_str);
    myfree(SRAMIN, task_state_str);
    printf("/**************************结束***************************/\r\n");
    printf("按下KEY0键继续!\r\n\r\n\r\n");
    while (key_scan(0) != KEY0_PRES) {
        vTaskDelay(10);
    }
    /* 第四步:函数vTaskList()的使用*/
    printf("/*************第四步:函数vTaskList()的使用*************/\r\n");
    task_info_buf = mymalloc(SRAMIN, 500);
    vTaskList(task_info_buf); /* 获取所有任务的信息*/
    printf("任务名\t\t状态\t优先级\t剩余栈\t任务序号\r\n");
    printf("%s\r\n", task_info_buf);
    myfree(SRAMIN, task_info_buf);
    printf("/************************实验结束***************************/\r\n");
    while (1) {
        vTaskDelay(10);
    }
}

从以上代码中可以看到,task1 分别展示了函数uxTaskGetSystemState()、函数vTaskGetInfo()、函数eTaskGetState()、函数vTaskList()的使用,并将每次得到的结果通过串口输出。

下载验证

编译并下载代码,复位后可以看到LCD 屏幕上显示了本次实验的相关信息,如下图所示:
在这里插入图片描述
同时,通过串口打印了函数uxTaskGetSystemState()获取到的系统任务信息,如下图所示:
在这里插入图片描述
从上图可以得到系统中任务的一些信息,其中包含了任务的任务名、任务优先级以及任务编号。
接着按下KEY0,展示函数vTaskGetInfo()的使用,如下图所示:

在这里插入图片描述
从上图可以看到,函数vTaskGetInfo()获取到了指定task1 任务的部分任务信息,其中就包
含了task1 任务的任务名、任务编号、任务状态、任务优先级等任务信息。
接着按下KEY0,展示函数eTaskGetState()的使用,如下图所示:
在这里插入图片描述
上图中,函数eTaskGetState()获取的task1 任务的任务状态信息。
接着按下KEY0,展示函数vTaskList()的使用,如下图所示:
在这里插入图片描述
从上图可以看到,函数vTaskList()以“表格”的形式获取的系统中任务的信息,其中就包
含了任务的任务名、任务状态、任务优先级等任务信息。
通过合理地使用FreeRTOS 提供的这部分函数,可以大大地提高用户的开发效率,但这部
分函数中,大部分函数进推荐在调试中使用。

FreeRTOS 任务运行时间统计实验

功能设计

  1. 例程功能
    本实验主要用于学习FreeRTOS 任务运行时间统计相关API 函数的使用,本实验设计了四
    个任务,这四个任务的功能如下表所示:
    在这里插入图片描述
    该实验的实验工程,请参考《FreeRTOS 实验例程11-2 FreeRTOS 任务运行时间统计实验》。

软件设计

  1. 程序流程图
    本实验的程序流程图,如下图所示:
    在这里插入图片描述
  2. FreeRTOS 函数解析
    (1) 函数vTaskGetRunTimeStats()
    此函数用于获取指定任务的运行时间、状态等信息,请参考第11.1.2 小节《FreeRTOS 任务
    相关API 函数详解》。
  3. 程序解析
    整体的代码结构,请参考2.1.6 小节,本小节着重讲解本实验相关的部分。
    (1) FreeRTOS 系统配置
    使用FreeRTOS 获取系统任务运行时间信息的API 函数,需要在FreeRTOSConfig.h 文件中
    开启相关配置,如下所示:
/* 1: 使能任务运行时间统计功能, 默认: 0 */
#define configGENERATE_RUN_TIME_STATS 1

#if configGENERATE_RUN_TIME_STATS
#include "./BSP/TIMER/btim.h"
#define portCONFIGURE_TIMER_FOR_RUN_TIME_STATS() ConfigureTimeForRunTimeStats()
extern uint32_t FreeRTOSRunTimeTicks;
#define portGET_RUN_TIME_COUNTER_VALUE() FreeRTOSRunTimeTicks
#endif

要使用FreeRTOS 获取系统任务运行时间信息的API 函数,须在FreeRTOSConfig.h 文件中
将配置项configGENERATE_RUN_TIME_STATS 定义为1,并且如果配置项configGENERATE_RUN_TIME_STATS 被定义为1,还需定义两个宏,分别为portCONFIGURE_TIMER_FOR_RUN_TIME_STATS()、portGET_RUN_TIME_COUNTER_VALUE(),其中,宏portCONFIGURE
_TIMER_FOR_RUNTIME_STATE()用于初始化配置用于任务运行时间统计的时基定时器,因为任务运行时间统计功能需要一个硬件定时器作为时基,这个时基定时器的计时精度需高于系统时钟节拍的精度10 至100 倍数,这样统计的时间才会比较准确;宏
portGET_RUN_TIME_COUNTER_VALUE()用于获取该功能时基硬件定时器计数的计数值。本实验使用函数ConfigureTimeForRunTimeStats()来进行该功能时基硬件定时器的初始化配置,使用全局变量FreeRTOSRunTimeTicks 来传递该功能时基硬件定时器计数的计数值,该函数在btim.c 文件中有定义,具体的代码如下所示(这里以ALIENTEK 的STM32F1 系列开发板为例,其他类型的开发板类似):

uint32_t FreeRTOSRunTimeTicks; /* FreeRTOS时间统计所用的节拍计数器*/
void ConfigureTimeForRunTimeStats(void) {
        FreeRTOSRunTimeTicks = 0; /* 节拍计数器初始化为0 */
        btim_tim3_int_init(10 - 1, 720 - 1); /* 初始化TIM3 */
    }
    /**
     * @brief 定时器更新中断回调函数
     * @param htim:定时器句柄指针
     * @retval 无
     */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef * htim) {
    if (htim == ( & g_tim3_handle)) {
        FreeRTOSRunTimeTicks++;
    }
}

从上面的代码中可以看到,函数ConfigureTimeForRunTimeStats() 将全局变量
FreeRTOSRunTimeTicks 初始化为0,接着初始化了硬件定时器3。在硬件定时器3 的中断服务函数中更新全局变量FreeRTOSRunTimeTicks 的值。
(2) 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);
    /* 创建任务3 */
    xTaskCreate((TaskFunction_t) task3, (const char * )
        "task3", (uint16_t) TASK3_STK_SIZE, (void * ) NULL, (UBaseType_t) TASK3_PRIO, (TaskHandle_t * ) & Task3Task_Handler);
    vTaskDelete(StartTask_Handler); /* 删除开始任务*/
    taskEXIT_CRITICAL(); /* 退出临界区*/
}

start_task 任务主要用于创建task1 任务、task2 任务和task3 任务。
(3) task1 和task2 任务

/**
 * @brief task1
 * @param pvParameters : 传入参数(未用到)
 * @retval 无
 */
void task1(void * pvParameters) {
        uint32_t task1_num = 0;
        while (1) {
            /* LCD区域刷新*/
            lcd_fill(6, 131, 114, 313, lcd_discolor[++task1_num % 11]);
            /* 显示任务1运行次数*/
            lcd_show_xnum(71, 111, task1_num, 3, 16, 0x80, BLUE);
            vTaskDelay(1000);
        }
    }
    /**
     * @brief task2
     * @param pvParameters : 传入参数(未用到)
     * @retval 无
     */
void task2(void * pvParameters) {
    uint32_t task2_num = 0;
    while (1) {
        /* LCD区域刷新*/
        lcd_fill(126, 131, 233, 313, lcd_discolor[11 - (++task2_num % 11)]);
        /* 显示任务2运行次数*/
        lcd_show_xnum(191, 111, task2_num, 3, 16, 0x80, BLUE);
        vTaskDelay(1000);
    }
}

从以上代码中可以看到,task1 和task2 任务分别每间隔500ticks 就区域刷新一次屏幕,task1和task2 任务主要用于辅助演示FreeRTOS 任务运行时间统计API 函数的使用。
(4) task3 任务

/**
 * @brief task3
 * @param pvParameters : 传入参数(未用到)
 * @retval 无
 */
void task3(void * pvParameters) {
    uint8_t key = 0;
    char * runtime_info = NULL;
    while (1) {
        key = key_scan(0);
        switch (key) {
            case KEY0_PRES:
                {
                    runtime_info = mymalloc(SRAMIN, 100);
                    vTaskGetRunTimeStats(runtime_info); /* 获取任务运行时间信息*/
                    printf("任务名\t\t运行时间\t运行所占百分比\r\n");
                    printf("%s\r\n", runtime_info);
                    myfree(SRAMIN, runtime_info);
                    break;
                }
            default:
                {
                    break;
                }
        }
        vTaskDelay(10);
    }
}

从上面的代码中可以看到,task3 任务负责扫描按键,当检测到KEY0 按键被按下时候,调
用函数vTaskGetRunTimeStats()获取并通过串口打印系统任务运行时间信息。

下载验证

编译并下载代码,复位后可以看到LCD 屏幕上显示了本次实验的相关信息,如下图所示:
图11.3.3.1 LCD 显示内容

接着按下按键0,通过串口调试助手就能后看到,输出了系统任务的运行时间统计信息,
如下图所示:
在这里插入图片描述
通过此功能,用户就能够很好的判断,所设计的任务占用CPU 资源的多少,以此作为优化
系统的依据,此功能同样建议仅在调试阶段使用。

FreeRTOS 时间管理

在前面的章节实验例程中,频繁地使用了FreeRTOS 提供的延时函数,使用延时函数会使
得任务进入阻塞态,直至延时完成,任务才会重新进入就绪态。FreeRTOS 是如何对延时任务进行阻塞的,又是如何判断任务延时超时的,这些都是属于FreeRTOS 时间管理的相关内容。本章就来学习FreeRTOS 时间管理的的相关内容。

FreeRTOS 系统时钟节拍

FreeRTOS 系统时钟节拍简介

任务的操作系统都需要时钟节拍,FreeRTOS 也不例外。FreeRTOS 有一个系统时钟节拍计
数器——xTickCount,xTickCount 是一个全局变量,在tasks.c 文件中有定义,具体的代码如下所示:

PRIVILEGED_DATA static volatile TickType_t xTickCount =
( TickType_t ) configINITIAL_TICK_COUNT;

从上面的代码可以看到,xTickCount 在定义时,被赋了初值,初值由宏定义
configINITIAL_TICK_COUNT 定义,在通常情况下系统使用节拍计数器的初值都是设置为0,除非在个别特出场合,读者仅需了解系统时钟节拍计数器的初值是可以由用户手动配置的即可。

FreeRTOS 系统时钟节拍来源

FreeRTOS 的系统时钟节拍计数器为全局变量xTickCount,那么FreeRTOS 又是何时操作这
个系统时钟节拍计数器的呢?本教程的配套例程而言,是在SysTick 的中断服务函数中,一般也推荐使用SysTick 作为RTOS 的时钟节拍,当然啦,用户也可以用其他的硬件定时器为RTOS提供时钟节拍。
既然使用SysTick 为FreeRTOS 提供时钟节拍,那么首先就来看一下SysTick 是如何配置
的。对于本套教程的配套例程,在main()函数中都会调用函数delay_init(),对SysTick 进行初始化,这里的初始化,主要是用于进行阻塞延时的,并且在FreeRTOS 启动任务调度器的过程中也会对SysTick 进行初始化,这里对SysTick 的配置是会覆盖函数delay_init()对SysTick 的配置的,因此最终SysTick 的配置为FreeRTOS 对SysTick 的配置。
ALIENTEK 不同型号的板卡,在函数delay_init()对SysTick 时钟源的配置不同(更详细的
请参考第2.1.3 小节《修改SYSTEM 文件》中对delay.c 文件修改的部分),其中ALIENTEK STM32F1 系列板卡配置SysTick 的时钟源频率为CPU 时钟频率的1/8,依而其他板卡配置
SysTick 的时钟源频率与CPU 的时钟源频率相同,同时为了使delay.c 文件中的阻塞延时延时能够正常使用,因此对于ALIENTEK STM32F1 系列板卡,需在FreeRTOSConfig.h 文件中配置configSYSTICK_CLOCK_HZ,具体的配置如下所示:

/* 定义SysTick时钟频率,
* 当SysTick时钟频率与内核时钟频率不同时才可以定义,
* 单位: Hz, 默认: 不定义
* */
#define configSYSTICK_CLOCK_HZ (configCPU_CLOCK_HZ / 8)

这里要特别注意的是,宏configSYSTICK_CLOCK_HZ 只有在SysTick 的时钟源频率与CPU
时钟源频率不同时才可以定义。
在第8.1.2 小节《函数xPortStartScheduler()》中,提到函数xPortStartScheduler()会对SysTick进行配置,更详细的请参考第8.1.2 小节《函数xPortStartScheduler()》,下面列出函数xPortStartScheduler()中对SysTick 配置的关键代码:

portNVIC_SHPR3_REG |= portNVIC_SYSTICK_PRI;

vPortSetupTimerInterrupt();

其中,第一行代码用于设置SysTick 的中断优先级为最低优先等级。然后是调用函数
vPortSetupTimerInterrupt()对SysTick 进行配置,函数vPortSetupTimerInterrupt()在port.c 文件中有定义,具体的代码如下所示:

__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
    /* 清空SysTick控制状态寄存器*/
    portNVIC_SYSTICK_CTRL_REG = 0 UL;
    /* 清空SysTick当前数值寄存器*/
    portNVIC_SYSTICK_CURRENT_VALUE_REG = 0 UL;
    /* 根据配置的系统时钟节拍频率,
     * 设置SysTick重装载寄存器
     */
    portNVIC_SYSTICK_LOAD_REG =
        (configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ) - 1 UL;
    /* 设置SysTick控制状态寄存器,
     * 设置SysTick时钟源不分频(与CPU时钟源同频率)、
     * 开启SysTick计数清零中断(SysTick为向下计数)、
     * 开启SysTick计数
     */
    portNVIC_SYSTICK_CTRL_REG = (portNVIC_SYSTICK_CLK_BIT |
        portNVIC_SYSTICK_INT_BIT |
        portNVIC_SYSTICK_ENABLE_BIT);
}

FreeRTOS 系统时钟节拍处理

既然FreeRTOS 的系统时钟节拍来自SysTick,那么FreeRTOS 系统时钟节拍的处理,自然
就是在SysTick 的中断服务函数中完成的。在前面第2.1.3 小节《修改SYSTEM 文件》中,修改了SysTick 的中断服务函数,SysTick 的中断服务函数定义在delay.c 文件中,具体的代码如下所示:

/**
 * @brief systick中断服务函数,使用OS时用到
 * @param ticks: 延时的节拍数
 * @retval 无
 */
void SysTick_Handler(void) {
    HAL_IncTick();
    /* OS开始跑了,才执行正常的调度处理*/
    if (xTaskGetSchedulerState() != taskSCHEDULER_NOT_STARTED) {
        xPortSysTickHandler();
    }
}

从上面的代码可以看出,在SysTick 的中断服务函数中,除了调用函数HAL_IncTick()外,
还通过函数xTaskGetSchedulerState()判断任务调度器是否运行,如果任务调度器运行,那么就调用函数xPortSysTickHandler()处理FreeRTOS 的时钟节拍,及相关事务。
函数xPortSysTickHandler()在port.c 文件中有定义,具体的代码如下所示:

/* SyaTick中断服务函数*/
void xPortSysTickHandler(void) {
    /* 屏蔽所有受FreeRTOS管理的中断
     * 因为SysTick的中断优先级设置为最低的中断优先等级,
     * 因此需要屏蔽所有受FreeRTOS管理的中断
     */
    vPortRaiseBASEPRI(); {
        /* 处理系统时钟节拍,
         * 并决定是否进行任务切换
         */
        if (xTaskIncrementTick() != pdFALSE) {
            /* 需要进行任务切换,
             * 这是中断控制状态寄存器,以挂起PendSV异常
             */
            portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
        }
    }
    /* 取消中断屏蔽*/
    vPortClearBASEPRIFromISR();
}

从上面的代码可以看出,函数xPortSysTickHandler()调用了函数xTaskIncrementTick()来处
理系统时钟节拍。在调用函数xTaskIncrementTick()前后分别屏蔽了受FreeRTOS 管理的中断和取消中断屏蔽,这是因为SysTick 的中断优先级设置为最低的中断优先等级,在SysTick 的中断中处理FreeRTOS 的系统时钟节拍时,并不希望收到其他中断的影响。在通过函数
xTaskIncrementTick()处理完系统时钟节拍和相关事务后,再根据函数xTaskIncrementTick 的返回值,决定是否进行任务切换,如果进行任务切换,就触发PendSV 异常,在本次SysTick 中断及其他中断处理完成后,就会进入PendSV 的中断服务函数进行任务切换,使用PendSV 进行切换任务的相关内容,请查看第九章《FreeRTOS 任务切换》。
接下来分析函数xTaskIncrementTick()是如何处理系统时钟节拍及相关事务的,函数
xTaskIncrementTick()在task.c 文件中有定义,具体的代码如下所示:

。。。。。

从上面的代码可以看到,函数xTaskIncrementTick()处理了系统时钟节拍、阻塞任务列表、
时间片调度等。
处理系统时钟节拍,就是在每次SysTick 中断发生的时候,将全局变量xTickCount 的值加
1,也就是将系统时钟节拍计数器的值加1。
处理阻塞任务列表,就是判断阻塞态任务列表中是否有阻塞任务超时,如果有,就将阻塞
时间超时的阻塞态任务移到就绪态任务列表中,准备执行。同时在系统时钟节拍计数器
xTickCount 的加1 溢出后,将两个阻塞态任务列表调换,这是FreeRTOS 处理系统时钟节拍计数器溢出的一种机制。
处理时间片调度,就是在每次系统时钟节拍加1 后,切换到另外一个同等优先级的任务中
运行,要注意的是,此函数只是做了需要进行任务切换的标记,在函数退出后,会统一进行任务切换,因此时间片调度导致的任务切换,也可能因为有更高优先级的阻塞任务就绪导致任务切换,而出现任务切换后运行的任务比任务切换前运行任务的优先级高,而非相等优先级。

FreeRTOS 任务延时函数

FreeRTOS 提供了与任务延时相关的API 函数,入下表所示:
在这里插入图片描述

函数vTaskDelay()

函数vTaskDelay()用于对任务进行延时,延时的时间单位为系统时钟节拍,如需使用子函
数,需要的FreeRTOSConfig.h 文件中将配置项INCLUDE_vTaskDelay 配置为1。此函数在task.c文件中有定义,具体的代码如下所示:

void vTaskDelay(const TickType_t xTicksToDelay) {
    BaseType_t xAlreadyYielded = pdFALSE;
    /* 只有在延时时间大于0的时候,
     * 才需要进行任务阻塞,
     * 否则相当于强制进行任务切换,而不阻塞任务
     */
    if (xTicksToDelay > (TickType_t) 0 U) {
        configASSERT(uxSchedulerSuspended == 0);
        /* 挂起任务调度器*/
        vTaskSuspendAll(); {
            /* 用于调试,不用理会*/
            traceTASK_DELAY();
            /* 将任务添加到阻塞态任务列表中*/
            prvAddCurrentTaskToDelayedList(xTicksToDelay, pdFALSE);
        }
        /* 恢复任务调度器运行,
         * 调用此函数会返回是否需要进行任务切换
         */
        xAlreadyYielded = xTaskResumeAll();
    } else {
        mtCOVERAGE_TEST_MARKER();
    }
    /* 根据标志进行任务切换*/
    if (xAlreadyYielded == pdFALSE) {
        portYIELD_WITHIN_API();
    } else {
        mtCOVERAGE_TEST_MARKER();
    }
}
  1. 使用函数vTaskDelay()进行任务延时时,被延时的任务为调用该函数的任务,及调用该
    函数时,系统中正在运行的任务,此函数无法指定将其他任务进行任务延时。
  2. 函数vTaskDelay()传入的参数xTicksToDelay 是任务被延时的具体延时时间,时间的单
    位为系统时钟节拍,这里要特别注意,很多FreeRTOS 的初学者可能会一会此函数延时的时间单位为微妙、毫秒、秒等物理时间单位,当时FreeRTOS 是以系统时钟节拍作为计量的时间单位的,而系统时钟节拍对应的物理时间长短于FreeRTOSConfig.h 文件中的配置项configTICK_RATE_HZ 有关,配置项configTICK_RATE_HZ 是用于配置系统时钟节拍的频率的,本教程的所有配套例程,将此配置项配置成了1000,即系统时钟节拍的频率为1000,换算过来,一个系统时钟节拍就是1 毫秒。
  3. 在使用此函数进行任务延时时,如果传入的参数为0,那表明不进行任务延时,而是强
    制进行一次任务切换。
  4. 在使用此函数进行任务延时时,会调用函数prvAddCurrentTaskToDelayedList()将被延时
    的任务添加到阻塞态任务列表中进行延时,系统会在每一次SysTick 中断发生时,处理阻塞态任务列表,更详细地请参考第12.1.3 小节《FreeRTOS 系统时钟节拍处理》。其中函数
    prvAddCurrentTaskToDelayedList()在task.c 文件中有定义,具体的代码如下所示:

。。。

  1. 函数prvAddCurrentTaskToDelayedList()是将任务当前任务添加到阻塞态任务列表中,其
    中入参xTicksToWait 就是要任务被阻塞的时间,入参xCanBlockIndefinitely 为当xTicksToWait为最大值时,是否运行将任务无期限阻塞,即将任务挂起,当然,能够这样做的前提是,在FreeRTOSConfig.h 文件中开启了挂起任务功能。
  2. 此函数在将任务添加到阻塞态任务列表中后,还会更新全局变量xNextTaskUnblockTime,
    全局变量xNextTaskUnblockTime 用于记录系统中的所有阻塞态任务中未来最近一个阻塞超时任务的阻塞超时时系统时钟节拍计数器的值,因此,在往阻塞态任务列表添加任务后,就需要更新这个全局变量,因为,新添加的阻塞态任务可能是未来系统中最早阻塞超时的阻塞任务。

函数vTaskDelayUntil()

函数vTaskDelayUntil()用于以一个绝对的时间阻塞任务,适用于需要按照一定频率运行的
任务,函数vTaskDelayUntil()实际上是一个宏,在task.h 文件中有定义,具体的代码如下所示:

#define vTaskDelayUntil( pxPreviousWakeTime, xTimeIncrement ) \
{ \
( void ) xTaskDelayUntil( pxPreviousWakeTime, xTimeIncrement ); \
}

从上面的代码可以看出,宏vTaskDelayUntil()实际上就是函数xTaskDelayUntil(),函数
xTaskDelayUntil()在task.c 文件中有定义,具体的代码如下所示:
。。。
从上面的代码可以看出,函数xTaskDelayUntil()对任务进行延时的操作,是相对于任务上
一次阻塞超时的时间,而不是相对于系统当前的时钟节拍计数器的值,因此,函数能够更准确地以一定的频率进行任务延时,更加适用于需要按照一定频率运行的任务。

函数xTaskAbortDelay()

函数xTaskAbortDelay()用于终止处于阻塞态任务的阻塞,此函数在task.c 文件中有定于,
具体的代码如下所示:
。。。。

  1. 函数xTaskAbortDelay()会将阻塞任务从阻塞态任务列表中移除,并将任务添加到就绪态
    任务列表中。
  2. 因为有任务添加到就绪态任务列表中,因此需要的启用抢占式调度的情况下,判断刚添
    加就绪态任务列表中的任务是否为系统中优先级最高的任务,如果是的话,就需要进行任务切换,这就是抢占式调度的抢占机制。
  3. 任务被阻塞可能不仅仅因为是被延时,还有可能是在等待某个事件的发生,如果任务是
    因为等待事件而被阻塞,那么中断阻塞的时候,需要将任务从所在事件列表中移除。

FreeRTOS 队列

在实际的项目开发中,经常会遇到在任务于任务之间或任务于中断之间需要进行“沟通交流”,这里的“沟通交流”就是消息传递的过程。在不使用操作系统的情况下,函数与函数,或函数与中断之间的“沟通交流”一般使用一个或多个全局变量来完成,但是在操作系统中,因为会涉及“资源管理”的问题,比方说读写冲突,因此使用全局变量在任务于任务或任务于中断之间进行消息传递,并不是很好的解决方案。FreeRTOS 为此提供了“队列”的机制。本章就来学习FreeRTOS 中的队列。

FreeRTOS 队列简介

队列是一种任务到任务、任务到中断、中断到任务数据交流的一种机制。在队列中可以存储数量有限、大小固定的多个数据,队列中的每一个数据叫做队列项目,队列能够存储队列项目的最大数量称为队列的长度,在创建队列的时候,就需要指定所创建队列的长度及队列项目的大小。因为队列是用来在任务与任务或任务于中断之间传递消息的一种机制,因此队列也叫做消息队列。

基于队列,FreeRTOS 实现了多种功能,其中包括队列集、互斥信号量、计数型信号量、二
值信号量、递归互斥信号量,因此很有必要深入了解FreeRTOS 的队列。

  1. 数据存储
    队列通常采用FIFO(先进先出)的存储缓冲机制,当有新的数据被写入队列中时,永远都是写入到队列的尾部,而从队列中读取数据时,永远都是读取队列的头部数据。但同时FreeRTOS的队列也支持将数据写入到队列的头部,并且还可以指定是否覆盖先前已经在队列头部的数据。

  2. 多任务访问
    队列不属于某个特定的任务,可以在任何的任务或中断中往队列中写入消息,或者从队列中读取消息。

  3. 队列读取阻塞
    在任务从队列读取消息时,可以指定一个阻塞超时时间。如果任务在读取队列时,队列为空,这时任务将被根据指定的阻塞超时时间添加到阻塞态任务列表中进行阻塞,以等待队列中有可用的消息。当有其他任务或中断将消息写入队列中,因等待队列而阻塞任务将会被添加到就绪态任务列表中,并读取队列中可用的消息。如果任务因等待队列而阻塞的时间超过指定的阻塞超时时间,那么任务也将自动被转移到就绪态任务列表中,但不再读取队列中的数据。
    因为同一个队列可以被多个任务读取,因此可能会有多个任务因等待同一个队列,而被阻塞,在这种情况下,如果队列中有可用的消息,那么也只有一个任务会被解除阻塞并读取到消息,并且会按照阻塞的先后和任务的优先级,决定应该解除哪一个队列读取阻塞任务。

  4. 队列写入阻塞
    与队列读取一样,在任务往队列写入消息时,也可以指定一个阻塞超时时间。如果任务在写入队列时,队列已经满了,这时任务将被根据指定的阻塞超时时间添加到阻塞态任务列表中进行阻塞,以等待队列有空闲的位置可以写入消息。指定的阻塞超时时间为任务阻塞的最大时间,如果在阻塞超时时间到达之前,队列有空闲的位置,那么队列写入阻塞任务将会解除阻塞,并往队列中写入消息,如果达到指定的阻塞超时时间,队列依旧没有空闲的位置写入消息,那么队列写入阻塞任务将会自动转移到就绪态任务列表中,但不会往队列中写入消息。
    因为同一个队列可以被多个任务写入,因此可有会有多个任务因等待统一个任务,而被阻塞,在这种情况下,如果队列中有空闲的位置,那么也之后一个任务会被解除阻塞并往队列中写入消息,并且会按照阻塞的先后和任务的优先级,决定应该解除哪一个队列写入阻塞任务。

  5. 队列操作
    下面简单介绍一下队列操作的过程,包括创建队列、往队列中写入消息、从队列中读取消息等操作。

(1) 创建队列

在这里插入图片描述

如图13.1.1,创建了一个用于 任务A 与 任务B 之间“沟通交流”的队列,这个队列最大可容纳 5 个队列项目,即队列的长度为5。刚创建的队列是不包含内容的,因此这个队列为空。

(2) 往队列写入第一个消息

在这里插入图片描述

如图13.1.2,任务A 将一个私有变量写入队列的尾部。由于在写入队列之前,队列是空的,因此新写入的消息,既是是队列的头部,也是队列的尾部。

(3) 往队列中写入第二个消息

在这里插入图片描述

如图13.1.3,任务A 改变了私有变量的值,并将新值写入队列。现在队列中包含了队列A写入的两个值,其中第一个写入的值在队列的头部,而新写入的值在队列的尾部。这时队列还有3 个空闲的位置。

(4) 从队列读取第一个消息

在这里插入图片描述

如图13.1.4,任务B 从队列中读取消息,任务B 读取的消息是处于队列头部的消息,这是任务A 第一次往队列中写入的消息。在任务B 从队列中读取消息后,队列中任务A 第二次写入的消息,变成了队列的头部,因此下次任务B 再次读取消息时,将读取到这个消息。此时队列中剩余4 个空闲的位置。

FreeRTOS 队列相关API 函数

队列结构体

队列的结构体为Queue_t,在queue.c 文件中有定义,具体的代码如下所示:

typedef struct QueueDefinition
{
    int8_t *pcHead;    /* 存储区域的起始地址*/
    int8_t *pcWriteTo; /* 下一个写入的位置*/
    /* 信号量是由队列实现的,
     * 此结构体能用于队列和信号量,
     * 当用于队列时,使用联合体中的xQueue,
     * 当用于信号量时,使用联合体中的xSemaphore
     */
    union
    {
        QueuePointers_t xQueue;
        SemaphoreData_t xSemaphore;
    } u;
    List_t xTasksWaitingToSend;             /* 写入阻塞任务列表*/
    List_t xTasksWaitingToReceive;          /* 读取阻塞任务列表*/
    volatile UBaseType_t uxMessagesWaiting; /* 非空闲项目的数量*/
    UBaseType_t uxLength;                   /* 队列的长度*/
    UBaseType_t uxItemSize;                 /* 队列项目的大小*/
    /* 锁用于在任务因队列操作被阻塞前,防止中断或其他任务操作队列。
     * 上锁期间,队列可以写入和读取消息,但不会操作队列阻塞任务列表,
     * 当有消息写入时,cTxLock加1,当有消息被读取时,cRxLock加1,
     * 在解锁时,会统一处理队列的阻塞任务列表
     */
    volatile int8_t cRxLock; /* 读取上锁计数器*/
    volatile int8_t cTxLock; /* 写入上锁计数器*/
/* 同时启用了静态和动态内存管理*/
#if ((configSUPPORT_STATIC_ALLOCATION == 1) &&
        (configSUPPORT_DYNAMIC_ALLOCATION == 1))
        uint8_t ucStaticallyAllocated; /* 静态创建标志*/
#endif
/* 此宏用于使能启用队列集*/
#if (configUSE_QUEUE_SETS == 1) struct QueueDefinition * pxQueueSetContainer; /* 指向队列所在队列集*/
#endif
/* 此宏用于使能可视化跟踪调试*/
#if (configUSE_TRACE_FACILITY == 1)
        /* 仅用于调试,不用理会*/
        UBaseType_t uxQueueNumber;
        /* 队列的类型
         * 0: 队列或队列集
         * 1: 互斥信号量
         * 2: 计数型信号量
         * 3: 二值信号量
         * 4: 可递归信号量
         */
        uint8_t ucQueueType;
#endif
} xQUEUE;
/* 重定义成Queue_t */
typedef xQUEUE Queue_t;

前面说过FreeRTOS 基于队列实现了互斥信号量和递归互斥信号量功能。在队列的结构体中,就包含了一个联合体u,当队列结构体用作队列时,使用联合体u 中的xQueue,其数据类型为QueuePointers_t,在queue.c 文件中有定义,具体的代码如下所示:

typedef struct QueuePointers
{
	int8_t * pcTail; /* 存储区域的结束地址*/
	int8_t * pcReadFrom; /* 最后一次读取队列的位置*/
} QueuePointers_t;

而当队列结构体用于互斥信号量和递归互斥信号量时,则是使用联合体u 中的xSemaphore,其数据类型为SemaphoreData_t,在queue.c 文件中有定义,具体的代码如下所示:

typedef struct SemaphoreData
{
	TaskHandle_t xMutexHolder; /* 互斥信号量的持有者*/
	UBaseType_t uxRecursiveCallCount; /* 递归互斥信号量被递归获取计数器*/
} SemaphoreData_t;

创建队列

FreeRTOS 中用于创建队列的API 函数如下表所示:

在这里插入图片描述

  1. 函数xQueueCreate()
    此函数用于使用动态方式创建队列,队列所需的内存空间由FreeRTOS 从FreeRTOS 管理的堆中分配。函数xQueueCreate()实际上是一个宏定义,在queue.h 文件中有定义,具体的代码如下所示:
#define xQueueCreate( uxQueueLength, \
uxItemSize ) \
xQueueGenericCreate( ( uxQueueLength ), \
( uxItemSize ), \
( queueQUEUE_TYPE_BASE ))

函数xQueueCreate()的形参描述,如下表所示:

在这里插入图片描述

可以看到,函数xQueueCreate() 实际上是调用了函数xQueueGenericCreate() ,函数
xQueueGenericCreate()用于使用动态方式创建指定类型的队列,前面说FreeRTOS 基于队列实现了多种功能,每一种功能对应一种队列类型,队列类型的queue.h 文件中有定义,具体的代码如下所示:

#define queueQUEUE_TYPE_BASE ( ( uint8_t ) 0U ) /* 队列*/
#define queueQUEUE_TYPE_SET ( ( uint8_t ) 0U ) /* 队列集*/
#define queueQUEUE_TYPE_MUTEX ( ( uint8_t ) 1U ) /* 互斥信号量*/
#define queueQUEUE_TYPE_COUNTING_SEMAPHORE ( ( uint8_t ) 2U ) /* 计数型信号量*/
#define queueQUEUE_TYPE_BINARY_SEMAPHORE ( ( uint8_t ) 3U ) /* 二值信号量*/
#define queueQUEUE_TYPE_RECURSIVE_MUTEX ( ( uint8_t ) 4U ) /* 递归互斥信号量*/

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

QueueHandle_t xQueueGenericCreate(
    const UBaseType_t uxQueueLength, /* 队列长度*/
    const UBaseType_t uxItemSize,    /* 队列项目的大小*/
    const uint8_t ucQueueType)       /* 队列类型*/
{
    Queue_t *pxNewQueue = NULL;
    size_t xQueueSizeInBytes;
    uint8_t *pucQueueStorage;
    /* 队列长度大于0才有意义
     * 检查参数设置
     */
    if ((uxQueueLength > (UBaseType_t)0) &&
        ((SIZE_MAX / uxQueueLength) >= uxItemSize) &&
        ((SIZE_MAX - sizeof(Queue_t)) >= (uxQueueLength * uxItemSize)))
    {
        /* 计算队列存储空间需要的字节大小*/
        xQueueSizeInBytes = (size_t)(uxQueueLength * uxItemSize);
        /* 为队列申请内存空间
         * 队列控制块+队列存储区域
         */
        pxNewQueue = (Queue_t *)pvPortMalloc(sizeof(Queue_t) +
                                             xQueueSizeInBytes);
        /* 内存申请成功*/
        if (pxNewQueue != NULL)
        {
            /* 获取队列存储区域的起始地址*/
            pucQueueStorage = (uint8_t *)pxNewQueue;
            pucQueueStorage += sizeof(Queue_t);
/* 此宏用于启用支持静态内存管理*/
#if (configSUPPORT_STATIC_ALLOCATION == 1)
            {
                /* 标记此队列为非静态申请内存*/
                pxNewQueue - > ucStaticallyAllocated = pdFALSE;
            }
#endif
            /* 初始化队列*/
            prvInitialiseNewQueue(uxQueueLength,
                                  uxItemSize,
                                  pucQueueStorage,
                                  ucQueueType,
                                  pxNewQueue);
        }
        else
        {
            /* 用于调试,不用理会*/
            traceQUEUE_CREATE_FAILED(ucQueueType);
            mtCOVERAGE_TEST_MARKER();
        }
    }
    else
    {
        configASSERT(pxNewQueue);
        mtCOVERAGE_TEST_MARKER();
    }
    return pxNewQueue;
}

从上面的代码可以看出,函数xQueueGenericCreate()主要负责为队列申请内存,然后调用函数prvInitialiseNewQueue()对队列进行初始化,函数prvInitialiseNewQueue()在queue.c 文件中有定义,具体的代码如下所示:

static void prvInitialiseNewQueue(
    const UBaseType_t uxQueueLength, /* 队列长度*/
    const UBaseType_t uxItemSize,    /* 队列项目的大小*/
    uint8_t *pucQueueStorage,        /* 队列存储空间的起始地址*/
    const uint8_t ucQueueType,       /* 队列类型*/
    Queue_t *pxNewQueue)             /* 队列结构体*/
{
    /* 防止编译器警告(可能用不到这个入参)*/
    (void)ucQueueType;
    /* 队列存储空间的起始地址*/
    if (uxItemSize == (UBaseType_t)0)
    {
        /* 如果队列项目大小为0(类型为信号量)
         * 那么就不需要存储空间
         */
        pxNewQueue - > pcHead = (int8_t *)pxNewQueue;
    }
    else
    {
        pxNewQueue - > pcHead = (int8_t *)pucQueueStorage;
    }
    /* 队列长度*/
    pxNewQueue - > uxLength = uxQueueLength;
    /* 队列项目的大小*/
    pxNewQueue - > uxItemSize = uxItemSize;
    /* 重置队列*/
    (void)xQueueGenericReset(pxNewQueue, pdTRUE);
/* 此宏用于启用可视化跟踪调试*/
#if (configUSE_TRACE_FACILITY == 1)
    {
        /* 队列的类型*/
        pxNewQueue - > ucQueueType = ucQueueType;
    }
#endif
/* 此宏用于使能使用队列集*/
#if (configUSE_QUEUE_SETS == 1)
    {
        /* 队列所在队列集设为空*/
        pxNewQueue - > pxQueueSetContainer = NULL;
    }
#endif
    /* 用于调试,不用理会*/
    traceQUEUE_CREATE(pxNewQueue);
}

从上面的代码可以看出,函数prvInitialiseNewQueue()主要用于初始化队列结构体中的成员变量,其中还会调用函数xQueueGenericReset()对队列进行重置,函数xQueueGenericReset()在queue.c 文件中有定义,具体的代码如下所示:

static void prvInitialiseNewQueue(
    const UBaseType_t uxQueueLength, /* 队列长度*/
    const UBaseType_t uxItemSize,    /* 队列项目的大小*/
    uint8_t *pucQueueStorage,        /* 队列存储空间的起始地址*/
    const uint8_t ucQueueType,       /* 队列类型*/
    Queue_t *pxNewQueue)             /* 队列结构体*/
{
    /* 防止编译器警告(可能用不到这个入参)*/
    (void)ucQueueType;
    /* 队列存储空间的起始地址*/
    if (uxItemSize == (UBaseType_t)0)
    {
        /* 如果队列项目大小为0(类型为信号量),
         * 那么就不需要存储空间
         */
        pxNewQueue->pcHead = (int8_t *)pxNewQueue;
    }
    else
    {
        pxNewQueue->pcHead = (int8_t *)pucQueueStorage;
    }
    /* 队列长度*/
    pxNewQueue->uxLength = uxQueueLength;
    /* 队列项目的大小*/
    pxNewQueue->uxItemSize = uxItemSize;
    /* 重置队列*/
    (void)xQueueGenericReset(pxNewQueue, pdTRUE);
/* 此宏用于启用可视化跟踪调试*/
#if (configUSE_TRACE_FACILITY == 1)
    {
        /* 队列的类型*/
        pxNewQueue->ucQueueType = ucQueueType;
    }
#endif
/* 此宏用于使能使用队列集*/
#if (configUSE_QUEUE_SETS == 1)
    {
        /* 队列所在队列集设为空*/
        pxNewQueue->pxQueueSetContainer = NULL;
    }
#endif
    /* 用于调试,不用理会*/
    traceQUEUE_CREATE(pxNewQueue);
}

从上面的函数可以看出,函数xQueueGenericReset()复位队列的操作也是复位队列的结构体中的成员变量。
以上就是使用函数xQueueCreate()创建队列的整个流程,大致就是先为队列申请内存空间,然后初始化队列结构体中的成员变量,下面看一下使用静态方式创建队列的函数。

  1. 函数xQueueCreateStatic()
    此函数用于使用静态方式创建队列,队列所需的内存空间需要由用户手动分配并提供。函数xQueueCreateStatic()实际上是一个宏定义,在queue.h 文件中有定义,具体的代码如下所示:
#define xQueueCreateStatic(uxQueueLength, \
    uxItemSize, \
    pucQueueStorage, \
    pxQueueBuffer)\
xQueueGenericCreateStatic((uxQueueLength), \ (uxItemSize), \ (pucQueueStorage), \ (pxQueueBuffer), \ (queueQUEUE_TYPE_BASE))

函数xQueueCreateStatic()的形参描述,如下表所示:

在这里插入图片描述

函数xQueueCreateStatic()的返回值,如下表所示:

在这里插入图片描述

可以看到,函数xQueueCreateStatic()实际上是调用了函数xQueueGenericCreateStatic(),函数xQueueGenericCreateStatic()用于使用静态方式创建指定类型的队列。要注意的是,函数xQueueCreateStatic()的入参pxQueueBuffer 的数据类型为StaticQueue_t*,结构体StaticQueue_t本质上与前面将的队列结构体Queue_t 是一样的,区别在于Queue_t 是在queue.c 文件中定义的,属于FreeRTOS 的内部结构体,对于数据隐藏策略而言,用户在应用程序开发时,是无法访问FreeRTOS 内部使用的结构体的,但是使用静态方式创建队列时,需要根据队列结构体的大小来分配内存,因此用户需要在不访问队列结构体的前提下,确定队列结构体的大小,因此FreeRTOS 在Free RTOS.h 文件中提供了StaticQueue_t 结构体,具体的代码如下所示:

typedef struct xSTATIC_QUEUE
{
    /*
     * int8_t * pcHead
     * int8_t * pcWriteTo
     * int8_t * pcTail(用于队列时)或
     * TaskHandle_t xMutexHolder(用于互斥信号量时)
     */
    void *pvDummy1[3];
    /*
     * int8_t * pcReadFrom(用于队列时)或
     * UBaseType_t uxRecursiveCallCount(用户互斥信号量时)
     */
    union
    {
        void *pvDummy2;
        UBaseType_t uxDummy2;
    } u;
    /*
     * List_t xTasksWaitingToSend
     * List_t xTasksWaitingToReceive
     */
    StaticList_t xDummy3[2];
    /*
     * volatile UBaseType_t uxMessagesWaiting
     * UBaseType_t uxLength
     * UBaseType_t uxItemSize
     */
    UBaseType_t uxDummy4[3];
    /*
     * volatile int8_t cRxLock
     * volatile int8_t cTxLock
     */
    uint8_t ucDummy5[2];
#if ((configSUPPORT_STATIC_ALLOCATION == 1) && \
     (configSUPPORT_DYNAMIC_ALLOCATION == 1))
    /*
     * uint8_t ucStaticallyAllocated
     */
    uint8_t ucDummy6;
#endif
#if (configUSE_QUEUE_SETS == 1)
    /*
     * struct QueueDefinition * pxQueueSetContainer
     */
    void *pvDummy7;
#endif
#if (configUSE_TRACE_FACILITY == 1)
    /*
     * UBaseType_t uxQueueNumber
     */
    UBaseType_t uxDummy8;
    /*
     * uint8_t ucQueueType
     */
    uint8_t ucDummy9;
#endif
} StaticQueue_t;
/* 重定义成StaticSemaphore_t */
typedef StaticQueue_t StaticSemaphore_t;

从上面的代码中可以看出,结构体StaticQueue_t 与结构体Queue_t 是一一对应的,但是结构体StaticQueue_t 的成员变量的命名方式都以ucDummy 开头,这也表明用户应该使用这个结构体来确定队列结构体的大小,而不应该直接访问StaticQueue_t 中的成员变量。

下面来看一下函数xQueueGenericCreateStatic()是如何创建队列的,该函数在queue.c 文件
中有定义,具体的代码如下所示:

QueueHandle_t xQueueGenericCreateStatic(
    const UBaseType_t uxQueueLength, /* 队列长度*/
    const UBaseType_t uxItemSize,    /* 队列项目的大小*/
    uint8_t *pucQueueStorage,        /* 队列存储区域的起始地址*/
    StaticQueue_t *pxStaticQueue,    /* 队列结构体的起始地址*/
    const uint8_t ucQueueType)       /* 队列类型*/
{
    Queue_t *pxNewQueue = NULL;
    configASSERT(pxStaticQueue);
    /* 队列长度大于0才有意义
     * 不许提供队列控制块所需内存
     * 如果提供了存储区域所需内存,那项目大小就不能为0
     * 如果项目大小不为0,那就必须提供存储区域所需内存
     */
    if ((uxQueueLength > (UBaseType_t)0) &&
        (pxStaticQueue != NULL) &&
        (!((pucQueueStorage != NULL) && (uxItemSize == 0))) &&
        (!((pucQueueStorage == NULL) && (uxItemSize != 0))))
    {
/* 用于调试,不用理会*/
#if (configASSERT_DEFINED == 1)
        {
            volatile size_t xSize = sizeof(StaticQueue_t);
            configASSERT(xSize == sizeof(Queue_t));
            (void)xSize;
        }
#endif
        /* 已结构体Queue_t的方式获取pxStaticQueue,
         * 结构体Queue_t中的成员变量与
         * 结构体StaticQueue_t中的成员变量
         * 在内存上是一一对应的
         */
        pxNewQueue = (Queue_t *)pxStaticQueue;
/* 此宏用于启用动态方式管理内存*/
#if (configSUPPORT_DYNAMIC_ALLOCATION == 1)
        {
            /* 标记队列是静态方式创建的*/
            pxNewQueue->ucStaticallyAllocated = pdTRUE;
        }
#endif
        /* 初始化队列结构体中的成员变量*/
        prvInitialiseNewQueue(uxQueueLength,
                              uxItemSize,
                              pucQueueStorage,
                              ucQueueType,
                              pxNewQueue);
    }
    else
    {
        configASSERT(pxNewQueue);
        mtCOVERAGE_TEST_MARKER();
    }
    return pxNewQueue;
}

从上面的代码中可以看出,因为用户已经提供了队列与其内存区域所需的内存,因此函数
xQueueGenericCreateStatic()只需调用函数prvInitialiseNewQueue()初始化队列结构体中的成员变量即可。

队列写入消息

FreeRTOS 中用于往队列中写入消息的API 函数如下表所示:

在这里插入图片描述

  1. 在任务中往队列写入消息的函数
    在任务中往队列写入消息的函数有函数xQueueSend() 、xQueueSendToBack() 、
    xQueueSendToFront()、xQueueOverwrite(),这4 个函数实际上都是宏定义,在queue.h 文件中有定义,具体的代码如下所示:
#define xQueueSend(xQueue,             \
                   pvItemToQueue,      \
                   xTicksToWait)       \
    xQueueGenericSend((xQueue),        \
                      (pvItemToQueue), \
                      (xTicksToWait),  \
                      queueSEND_TO_BACK)
#define xQueueSendToBack(xQueue,        \
                         pvItemToQueue, \
                         xTicksToWait)  \
    xQueueGenericSend((xQueue),         \
                      (pvItemToQueue),  \
                      (xTicksToWait),   \
                      queueSEND_TO_BACK)
#define xQueueSendToFront(xQueue,        \
                          pvItemToQueue, \
                          xTicksToWait)  \
    xQueueGenericSend((xQueue),          \
                      (pvItemToQueue),   \
                      (xTicksToWait),    \
                      queueSEND_TO_FRONT)
#define xQueueOverwrite(xQueue,        \
                        pvItemToQueue) \
    xQueueGenericSend((xQueue),        \
                      (pvItemToQueue), \
                      0,               \
                      queueOVERWRITE)

从上面的代码中可以看到,函数xQueueSend() 、函数xQueueSendToBack() 、函数
xQueueSendToFront()和函数xQueueOverwrite()实际上都是调用了函数xQueueGenericSend(),只是指定了不同的写入位置,队列一共有3 种写入位置,在queue.h 文件中有定义,具体的代码如下所示:

#define queueSEND_TO_BACK ( ( BaseType_t ) 0 ) /* 写入队列尾部*/
#define queueSEND_TO_FRONT ( ( BaseType_t ) 1 ) /* 写入队列头部*/
#define queueOVERWRITE ( ( BaseType_t ) 2 ) /* 覆写队列*/

要注意的是,覆写方式写入队列,只有在队列的队列长度为1 时,才能够使用,这在下文
讲解函数xQueueGenericSend()时,会提到。
函数xQueueGenericSend() 用于在任务中往队列的指定位置写入消息。函数xQueueGenericSend()的函数原型如下所示:

BaseType_t xQueueGenericSend( QueueHandle_t xQueue,
							const void * const pvItemToQueue,
							TickType_t xTicksToWait,
							const BaseType_t xCopyPosition);

函数xQueueGenericSend()的形参描述,如下表所示:

在这里插入图片描述

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

BaseType_t xQueueGenericSend(QueueHandle_t xQueue,
                             const void *const pvItemToQueue,
                             TickType_t xTicksToWait,
                             const BaseType_t xCopyPosition)
{
    BaseType_t xEntryTimeSet = pdFALSE, xYieldRequired;
    TimeOut_t xTimeOut;
    Queue_t *const pxQueue = xQueue;
    configASSERT(pxQueue);
    configASSERT(!((pvItemToQueue == NULL) &&
                   (pxQueue->uxItemSize != (UBaseType_t)0U)));
    /* 这里限制了只有在队列长度为1时,才能使用覆写*/
    configASSERT(!((xCopyPosition == queueOVERWRITE) &&
                   (pxQueue->uxLength != 1)));
#if ((INCLUDE_xTaskGetSchedulerState == 1) || (configUSE_TIMERS == 1))
    {
        configASSERT(!((xTaskGetSchedulerState() == taskSCHEDULER_SUSPENDED) &&
                       (xTicksToWait != 0)));
    }
#endif
    for (;;)
    {
        /* 进入临界区*/
        taskENTER_CRITICAL();
        {
            /* 只有在队列有空闲位置或
             * 为覆写的情况才能写入消息
             */
            if ((pxQueue->uxMessagesWaiting < pxQueue->uxLength) ||
                (xCopyPosition == queueOVERWRITE))
            {
                /* 用于调试,不用理会*/
                traceQUEUE_SEND(pxQueue);
/* 此宏用于使能启用队列集*/
#if (configUSE_QUEUE_SETS == 1)
                {
                    /* 获取队列中非空闲项目的数量*/
                    const UBaseType_t uxPreviousMessagesWaiting =
                        pxQueue->uxMessagesWaiting;
                    /* 将待写入消息按指定写入方式复制到队列中*/
                    xYieldRequired =
                        prvCopyDataToQueue(pxQueue, pvItemToQueue, xCopyPosition);
                    /* 判断队列是否在队列集中*/
                    if (pxQueue->pxQueueSetContainer != NULL)
                    {
                        /* 写入位置为覆写,且队列非空闲项目数量不为0 */
                        if ((xCopyPosition == queueOVERWRITE) &&
                            (uxPreviousMessagesWaiting != (UBaseType_t)0))
                        {
                            mtCOVERAGE_TEST_MARKER();
                        }
                        /* 通知队列集*/
                        else if (prvNotifyQueueSetContainer(pxQueue) != pdFALSE)
                        {
                            /* 根据需要进行任务切换*/
                            queueYIELD_IF_USING_PREEMPTION();
                        }
                        else
                        {
                            mtCOVERAGE_TEST_MARKER();
                        }
                    }
                    /* 队列不在队列集中*/
                    else
                    {
                        /* 队列的读取阻塞任务列表非空*/
                        if (listLIST_IS_EMPTY(
                                &(pxQueue->xTasksWaitingToReceive)) ==
                            pdFALSE)
                        {
                            /* 将队列读取阻塞任务从所在列表移除
                             * 因为此时队列中已有可用消息
                             */
                            if (xTaskRemoveFromEventList(
                                    &(pxQueue->xTasksWaitingToReceive)) !=
                                pdFALSE)
                            {
                                /* 根据需要进行任务切换*/
                                queueYIELD_IF_USING_PREEMPTION();
                            }
                            else
                            {
                                mtCOVERAGE_TEST_MARKER();
                            }
                        }
                        else if (xYieldRequired != pdFALSE)
                        {
                            /* 在互斥信号量释放完且任务优先级恢复后,
                             * 需要进行任务切换
                             */
                            queueYIELD_IF_USING_PREEMPTION();
                        }
                        else
                        {
                            mtCOVERAGE_TEST_MARKER();
                        }
                    }
                }
#else
                {
                    /* 将消息写入到队列存储区域的指定位置*/
                    xYieldRequired =
                        prvCopyDataToQueue(pxQueue, pvItemToQueue, xCopyPosition);
                    /* 队列有阻塞的读取任务*/
                    if (listLIST_IS_EMPTY(&(pxQueue->xTasksWaitingToReceive)) ==
                        pdFALSE)
                    {
                        /* 将读取阻塞任务从队列读取任务阻塞列表中移除,
                         * 因为此时,队列中已经有非空闲的项目了
                         */
                        if (xTaskRemoveFromEventList(
                                &(pxQueue->xTasksWaitingToReceive)) !=
                            pdFALSE)
                        {
                            /* 有任务解除阻塞后,
                             * 需要根据任务的优先级进行任务切换
                             */
                            queueYIELD_IF_USING_PREEMPTION();
                        }
                        else
                        {
                            mtCOVERAGE_TEST_MARKER();
                        }
                    }
                    else if (xYieldRequired != pdFALSE)
                    {
                        /* 在互斥信号量释放完且任务优先级恢复后,
                         * 需要进行任务切换
                         */
                        queueYIELD_IF_USING_PREEMPTION();
                    }
                    else
                    {
                        mtCOVERAGE_TEST_MARKER();
                    }
                }
#endif
                /* 退出临界区*/
                taskEXIT_CRITICAL();
                return pdPASS;
            }
            else
            {
                /* 此时不能写入消息,因此要将任务阻塞*/
                if (xTicksToWait == (TickType_t)0)
                {
                    /* 如果不选则阻塞等待*/
                    /* 退出临界区*/
                    taskEXIT_CRITICAL();
                    /* 用于调试,不用理会*/
                    traceQUEUE_SEND_FAILED(pxQueue);
                    /* 返回队列满错误*/
                    return errQUEUE_FULL;
                }
                else if (xEntryTimeSet == pdFALSE)
                {
                    /* 队列满,任务需要阻塞,
                     * 记录下此时系统节拍计数器的值和溢出次数
                     * 用于下面对阻塞时间进行补偿
                     */
                    vTaskInternalSetTimeOutState(&xTimeOut);
                    xEntryTimeSet = pdTRUE;
                }
                else
                {
                    mtCOVERAGE_TEST_MARKER();
                }
            }
        }
        /* 退出临界区
         * 退出临界区后系统时钟节拍会发生更新,
         * 因此任务如果需要阻塞的话,
         * 需要对阻塞时间进行补偿
         */
        taskEXIT_CRITICAL();
        /* 挂起任务调度器*/
        vTaskSuspendAll();
        /* 队列上锁*/
        prvLockQueue(pxQueue);
        /* 判断阻塞时间补偿后,是否还需要阻塞*/
        if (xTaskCheckForTimeOut(&xTimeOut, &xTicksToWait) == pdFALSE)
        {
            /* 阻塞时间补偿后,还需要进行阻塞*/
            if (prvIsQueueFull(pxQueue) != pdFALSE)
            {
                /* 用于调试,不用理会*/
                traceBLOCKING_ON_QUEUE_SEND(pxQueue);
                /* 将任务添加到队列写入阻塞任务列表中进行阻塞*/
                vTaskPlaceOnEventList(&(pxQueue->xTasksWaitingToSend),
                                      xTicksToWait);
                /* 解锁队列*/
                prvUnlockQueue(pxQueue);
                /* 恢复任务调度器*/
                if (xTaskResumeAll() == pdFALSE)
                {
                    /* 根据需要进行任务切换*/
                    portYIELD_WITHIN_API();
                }
            }
            else
            {
                /* 队列解锁*/
                prvUnlockQueue(pxQueue);
                /* 恢复任务调度器*/
                (void)xTaskResumeAll();
            }
        }
        /* 阻塞时间补偿后,已不需要阻塞*/
        else
        {
            /* 解锁队列*/
            prvUnlockQueue(pxQueue);
            /* 恢复任务调度器*/
            (void)xTaskResumeAll();
            /* 用于调试,不用理会*/
            traceQUEUE_SEND_FAILED(pxQueue);
            /* 返回队列满错误*/
            return errQUEUE_FULL;
        }
    }
}

  1. 在中断中往队列写入消息的函数
    在任务中往队列写入消息的函数有函数xQueueSendFromISR() 、xQueueSendToBackFromISR()、xQueueSendToFrontFromISR()、xQueueOverwriteFromISR(),这4个函数实际上都是宏定义,在queue.h 文件中有定义,具体的代码如下所示:
#define xQueueSendFromISR(xQueue,                         \
                          pvItemToQueue,                  \
                          pxHigherPriorityTaskWoken)      \
    xQueueGenericSendFromISR((xQueue),                    \
                             (pvItemToQueue),             \
                             (pxHigherPriorityTaskWoken), \
                             queueSEND_TO_BACK)
#define xQueueSendToBackFromISR(xQueue,                    \
                                pvItemToQueue,             \
                                pxHigherPriorityTaskWoken) \
    xQueueGenericSendFromISR((xQueue),                     \
                             (pvItemToQueue),              \
                             (pxHigherPriorityTaskWoken),  \
                             queueSEND_TO_BACK)
#define xQueueSendToFrontFromISR(xQueue,                    \
                                 pvItemToQueue,             \
                                 pxHigherPriorityTaskWoken) \
    xQueueGenericSendFromISR((xQueue),                      \
                             (pvItemToQueue),               \
                             (pxHigherPriorityTaskWoken),   \
                             queueSEND_TO_FRONT)
#define xQueueOverwriteFromISR(xQueue,                    \
                               pvItemToQueue,             \
                               pxHigherPriorityTaskWoken) \
    xQueueGenericSendFromISR((xQueue),                    \
                             (pvItemToQueue),             \
                             (pxHigherPriorityTaskWoken), \
                             queueOVERWRITE)

从上面的代码中可以看到,函数xQueueSendFromISR()、函数xQueueSendToBackFromISR()、
函数xQueueSendToFrontFromISR()和函数xQueueOverwriteFromISR()实际上都是调用了函数xQueueGenericSendFromISR(),只是指定了不同的写入位置。
函数xQueueGenericSendFromISR() 用于在中断中往队列的指定位置写入消息。函数
xQueueGenericSendFromISR()的函数原型如下所示:

BaseType_t xQueueGenericSendFromISR(
							QueueHandle_t xQueue,
							const void * const pvItemToQueue,
							BaseType_t * const pxHigherPriorityTaskWoken,
							const BaseType_t xCopyPosition);

函数xQueueGenericSendFromISR()的形参描述,如下表所示:

在这里插入图片描述
在这里插入图片描述

函数xQueueGenericSendFromISR()的返回值,如下表所示:

在这里插入图片描述

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

BaseType_t xQueueGenericSendFromISR(
    QueueHandle_t xQueue,
    const void *const pvItemToQueue,
    BaseType_t *const pxHigherPriorityTaskWoken,
    const BaseType_t xCopyPosition)
{
    BaseType_t xReturn;
    UBaseType_t uxSavedInterruptStatus;
    Queue_t *const pxQueue = xQueue;
    configASSERT(pxQueue);
    configASSERT(!((pvItemToQueue == NULL) &&
                   (pxQueue->uxItemSize != (UBaseType_t)0U)));
    /* 这里限制了只有在队列长度为1时,才能使用覆写*/
    configASSERT(!((xCopyPosition == queueOVERWRITE) &&
                   (pxQueue->uxLength != 1)));
    /* 只有受FreeRTOS管理的中断才能调用该函数*/
    portASSERT_IF_INTERRUPT_PRIORITY_INVALID();
    /* 屏蔽受FreeRTOS管理的中断,
     * 并保存,屏蔽前的状态,用于恢复
     */
    uxSavedInterruptStatus = portSET_INTERRUPT_MASK_FROM_ISR();
    {
        /* 有空闲的写入位置,或为覆写*/
        if ((pxQueue->uxMessagesWaiting < pxQueue->uxLength) ||
            (xCopyPosition == queueOVERWRITE))
        {
            /* 获取任务的写入上锁计数器*/
            const int8_t cTxLock = pxQueue->cTxLock;
            /* 获取队列中非空闲位置的数量*/
            const UBaseType_t uxPreviousMessagesWaiting =
                pxQueue->uxMessagesWaiting;
            /* 用于调试,不用理会*/
            traceQUEUE_SEND_FROM_ISR(pxQueue);
            /* 将待写入消息按指定写入方式复制到队列中*/
            (void)prvCopyDataToQueue(pxQueue,
                                     pvItemToQueue,
                                     xCopyPosition);
            /* 判断队列的写入是否上锁*/
            if (cTxLock == queueUNLOCKED)
            {
/* 此宏用于使能队列集*/
#if (configUSE_QUEUE_SETS == 1)
                {
                    /* 判断队列是否在队列集中*/
                    if (pxQueue->pxQueueSetContainer != NULL)
                    {
                        /* 写入位置为覆写,且队列非空闲项目数量不为0 */
                        if ((xCopyPosition == queueOVERWRITE) &&
                            (uxPreviousMessagesWaiting != (UBaseType_t)0))
                        {
                            mtCOVERAGE_TEST_MARKER();
                        }
                        /* 通知队列集*/
                        else if (prvNotifyQueueSetContainer(pxQueue) != pdFALSE)
                        {
                            /* 判断是否接收需要任务切换标记*/
                            if (pxHigherPriorityTaskWoken != NULL)
                            {
                                /* 标记要进行任务切换*/
                                *pxHigherPriorityTaskWoken = pdTRUE;
                            }
                            else
                            {
                                mtCOVERAGE_TEST_MARKER();
                            }
                        }
                        else
                        {
                            mtCOVERAGE_TEST_MARKER();
                        }
                    }
                    /* 队列不在队列集中*/
                    else
                    {
                        /* 队列的读取阻塞任务列表非空*/
                        if (listLIST_IS_EMPTY(
                                &(pxQueue->xTasksWaitingToReceive)) ==
                            pdFALSE)
                        {
                            /* 将队列读取阻塞任务从所在列表移除
                             * 因为此时队列中已有可用消息
                             */
                            if (xTaskRemoveFromEventList(
                                    &(pxQueue->xTasksWaitingToReceive)) !=
                                pdFALSE)
                            {
                                /* 判断是否接收需要任务切换标记*/
                                if (pxHigherPriorityTaskWoken != NULL)
                                {
                                    /* 标记不要进行任务切换*/
                                    *pxHigherPriorityTaskWoken = pdTRUE;
                                }
                                else
                                {
                                    mtCOVERAGE_TEST_MARKER();
                                }
                            }
                            else
                            {
                                mtCOVERAGE_TEST_MARKER();
                            }
                        }
                        else
                        {
                            mtCOVERAGE_TEST_MARKER();
                        }
                    }
                }
#else
                {
                    /* 队列有阻塞的读取任务*/
                    if (listLIST_IS_EMPTY(
                            &(pxQueue->xTasksWaitingToReceive)) ==
                        pdFALSE)
                    {
                        /* 将读取阻塞任务从队列读取任务阻塞列表中移除,
                         * 因为此时,队列中已经有非空闲的项目了
                         */
                        if (xTaskRemoveFromEventList(
                                &(pxQueue->xTasksWaitingToReceive)) !=
                            pdFALSE)
                        {
                            /* 判断是否接收需要任务切换标记*/
                            if (pxHigherPriorityTaskWoken != NULL)
                            {
                                /* 标记不要进行任务切换*/
                                *pxHigherPriorityTaskWoken = pdTRUE;
                            }
                            else
                            {
                                mtCOVERAGE_TEST_MARKER();
                            }
                        }
                        else
                        {
                            mtCOVERAGE_TEST_MARKER();
                        }
                    }
                    else
                    {
                        mtCOVERAGE_TEST_MARKER();
                    }
                    /* 未其中队列集时未使用,
                     * 防止编译器警告
                     */
                    (void)uxPreviousMessagesWaiting;
                }
#endif
            }
            /* 队列写入已被上锁*/
            else
            {
                configASSERT(cTxLock != queueINT8_MAX);
                /* 上锁次数加1 */
                pxQueue->cTxLock = (int8_t)(cTxLock + 1);
            }
            xReturn = pdPASS;
        }
        /* 无空闲的写入位置,且不覆写*/
        else
        {
            /* 用于调试,不用理会*/
            traceQUEUE_SEND_FROM_ISR_FAILED(pxQueue);
            xReturn = errQUEUE_FULL;
        }
    }
    /* 恢复屏蔽中断前的中断状态*/
    portCLEAR_INTERRUPT_MASK_FROM_ISR(uxSavedInterruptStatus);
    return xReturn;
}

队列读取消息

FreeRTOS 中用于从队列中读取消息的API 函数如下表所示:

在这里插入图片描述

  1. 函数xQueueReceive()
    此函数用于在任务中,从队列中读取消息,并且消息读取成功后,会将消息从队列中移除。
    消息的读取是通过拷贝的形式传递的,具体拷贝数据的大小,为队列项目的大小。该函数的函数原型如下所示:
BaseType_t xQueueReceive( QueueHandle_t xQueue,
						void * const pvBuffer,
						TickType_t xTicksToWait);

函数xQueueReceive()的形参描述,如下表所示:

在这里插入图片描述

函数xQueueReceive()的返回值,如下表所示:

在这里插入图片描述

  1. 函数xQueuePeek()
    此函数用于在任务中,从队列中读取消息,但与函数xQueueReceive()不同,此函数在成功
    读取消息后,并不会移除已读取的消息,这意味着,下次读取队列时,还能够读取到相同的内容。消息的读取是通过拷贝的形式传递的,具体拷贝数据的大小,为队列项目的大小。该函数的函数原型如下所示:
BaseType_t xQueuePeek( QueueHandle_t xQueue,
void * const pvBuffer,
TickType_t xTicksToWait);

函数xQueuePeek()的形参描述,如下表所示:

在这里插入图片描述

函数xQueuePeek()的返回值,如下表所示:

在这里插入图片描述

  1. 函数xQueueReceiveFromISR()
    此函数用于在中断中,从队列中读取消息,并且消息读取成功后,会将消息从队列中移除。
    消息的读取是通过拷贝的形式传递的,具体拷贝数据的大小,为队列项目的大小。该函数的函数原型如下所示:
BaseType_t xQueueReceiveFromISR(
							QueueHandle_t xQueue,
							void * const pvBuffer,
							BaseType_t * const pxHigherPriorityTaskWoken);

函数xQueueReceiveFromISR()的形参描述,如下表所示:

在这里插入图片描述

  1. 函数xQueuePeekFromISR()
    此函数用于在中断中,从队列中读取消息,但与函数xQueueReceiveFromISR()不同,此函数在成功读取消息后,并不会移除已读取的消息,这意味着,下次读取队列时,还能够读取到相同的内容。消息的读取是通过拷贝的形式传递的,具体拷贝数据的大小,为队列项目的大小。
    该函数的函数原型如下所示:
BaseType_t xQueuePeekFromISR( QueueHandle_t xQueue,
							void * const pvBuffer);

函数xQueuePeekFromISR()的形参描述,如下表所示:

在这里插入图片描述

队列锁

在上文讲解队列操作的函数时,提到了队列的上锁与解锁,通过在队列的结构提供,也包含了队列读取上锁计数器和队列写入上锁计数器。在队列被上锁后,可以往队列中写入消息和读取消息,但是队列消息的读取和写入不会影响到队列读取和写入阻塞任务列表中的任务阻塞,队列的写入和读取阻塞任务列表会在队列解锁后,统一处理。
队列上锁的函数为prvLockQueue(),函数prvLockQueue()实际上是一个宏定义,在queue.c
文件中有定义,具体的代码如下所示:

#define prvLockQueue(pxQueue)\
taskENTER_CRITICAL();\ {\
    if ((pxQueue) - > cRxLock == queueUNLOCKED)\ {\
        (pxQueue) - > cRxLock = queueLOCKED_UNMODIFIED;\
    }\
    if ((pxQueue) - > cTxLock == queueUNLOCKED)\ {\
        (pxQueue) - > cTxLock = queueLOCKED_UNMODIFIED;\
    }\
}\
taskEXIT_CRITICAL()

队列结构体中的cRxLock 和cTxLock 成员变量就是队列的读取和写入上锁计数器,这两个成员变量用来表示队列的上锁状态。
队列解锁的函数为prvUnlockQueue(),函数prvUnlockQueue()实际上是一个宏定义,在queue.c 文件中有定义,具体的代码如下所示:

static void prvUnlockQueue(Queue_t *const pxQueue)
{
    /* 进入临界区*/
    taskENTER_CRITICAL();
    {
        /* 获取队列的写入上锁计数器*/
        int8_t cTxLock = pxQueue->cTxLock;
        /* 判断队列在上锁期间是否被写入消息*/
        while (cTxLock > queueLOCKED_UNMODIFIED)
        {
/* 此宏用于使能队列集*/
#if (configUSE_QUEUE_SETS == 1)
            {
                /* 判断队列是否存在队列集*/
                if (pxQueue->pxQueueSetContainer != NULL)
                {
                    /* 通知队列集*/
                    if (prvNotifyQueueSetContainer(pxQueue) != pdFALSE)
                    {
                        /* 根据需要进行任务切换*/
                        vTaskMissedYield();
                    }
                    else
                    {
                        mtCOVERAGE_TEST_MARKER();
                    }
                }
                /* 队列不存在队列集*/
                else
                {
                    /* 判断队列的读取阻塞任务列表是否不为空*/
                    if (listLIST_IS_EMPTY(&(pxQueue->xTasksWaitingToReceive)) ==
                        pdFALSE)
                    {
                        /* 将读取阻塞任务列表中的任务解除阻塞*/
                        if (xTaskRemoveFromEventList(
                                &(pxQueue->xTasksWaitingToReceive)) !=
                            pdFALSE)
                        {
                            /* 根据需要进行任务切换*/
                            vTaskMissedYield();
                        }
                        else
                        {
                            mtCOVERAGE_TEST_MARKER();
                        }
                    }
                    else
                    {
                        break;
                    }
                }
            }
#else
            /* 未使能队列集*/
            {
                /* 判断队列的读取阻塞任务列表是否不为空*/
                if (listLIST_IS_EMPTY(&(pxQueue->xTasksWaitingToReceive)) ==
                    pdFALSE)
                {
                    /* 将读取阻塞任务列表中的任务解除阻塞*/
                    if (xTaskRemoveFromEventList(
                            &(pxQueue->xTasksWaitingToReceive)) !=
                        pdFALSE)
                    {
                        /* 根据需要进行任务切换*/
                        vTaskMissedYield();
                    }
                    else
                    {
                        mtCOVERAGE_TEST_MARKER();
                    }
                }
                else
                {
                    break;
                }
            }
#endif
            /* 处理完一个读取阻塞任务后,
             * 更新队列写入上锁计数器,
             * 直到写入解锁为止
             */
            --cTxLock;
        }
        /* 设置队列写入解锁*/
        pxQueue->cTxLock = queueUNLOCKED;
    }
    /* 退出临界区*/
    taskEXIT_CRITICAL();
    /* 进入临界区*/
    taskENTER_CRITICAL();
    {
        /* 获取队列的读取上锁计数器*/
        int8_t cRxLock = pxQueue->cRxLock;
        /* 判断队列在上锁期间是否被读取消息*/
        while (cRxLock > queueLOCKED_UNMODIFIED)
        {
            /* 判断队列的写入阻塞任务列表是否不为空*/
            if (listLIST_IS_EMPTY(&(pxQueue->xTasksWaitingToSend)) ==
                pdFALSE)
            {
                /* 将写入阻塞任务列表中的任务解除阻塞*/
                if (xTaskRemoveFromEventList(
                        &(pxQueue->xTasksWaitingToSend)) !=
                    pdFALSE)
                {
                    /* 根据需要进行任务切换*/
                    vTaskMissedYield();
                }
                else
                {
                    mtCOVERAGE_TEST_MARKER();
                }
                /* 处理完一个写入阻塞任务后,
                 * 更新队列读取上锁计数器,
                 * 直到读取解锁位置
                 */
                --cRxLock;
            }
            else
            {
                break;
            }
        }
        /* 设置队列读取解锁*/
        pxQueue->cRxLock = queueUNLOCKED;
    }
    /* 退出临界区*/
    taskEXIT_CRITICAL();
}

FreeRTOS 队列操作实验

功能设计

  1. 例程功能
    本实验主要用于学习FreeRTOS 队列操作相关API 函数的使用,本实验设计了三个任务,这三个任务的功能如下表所示:

在这里插入图片描述
该实验的实验工程,请参考《FreeRTOS 实验例程13-1 FreeRTOS 队列操作实验》。

软件设计

  1. 程序流程图
    本实验的程序流程图,如下图所示:

在这里插入图片描述

  1. FreeRTOS 函数解析
    (1) 函数xQueueCreate()
    此函数用于使用动态方式创建队列,请参考第13.2.2 小节《创建队列》。
    (2) 函数xQueueSend()
    此函数用于往队列中写入消息,请参考第13.2.3 小节《队列写入消息》。
    (3) 函数xQueueReceive()
    此函数用于从队列中读取消息,请参考第13.2.4 小节《队列读取消息》。
  2. 程序解析
    整体的代码结构,请参考2.1.6 小节,本小节着重讲解本实验相关的部分。
    (1) start_task 任务
    start_task 任务的入口函数代码如下所示:
/**
 * @brief start_task
 * @param pvParameters : 传入参数(未用到)
 * @retval 无
 */
void start_task(void *pvParameters)
{
    taskENTER_CRITICAL(); /* 进入临界区*/
    /* 创建队列*/
    xQueue = xQueueCreate(QUEUE_LENGTH, QUEUE_ITEM_SIZE);
    /* 创建任务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 任务,并且创建实验所需的队列。
(2) task1 任务

/**
 * @brief task1
 * @param pvParameters : 传入参数(未用到)
 * @retval 无
 */
void task1(void *pvParameters)
{
    uint8_t key = 0;
    while (1)
    {
        key = key_scan(0);
        if (key != 0)
        {
            /* 将键值作为消息发送到队列中*/
            xQueueSend(xQueue, &key, portMAX_DELAY);
        }
        vTaskDelay(10);
    }
}

从以上代码中可以看到,task1 任务主要负责扫描按键,当扫描到有效按键后,将键值作为消息写入到队列中。
(3) task2 任务

/**
 * @brief task2
 * @param pvParameters : 传入参数(未用到)
 * @retval 无
 */
void task2(void *pvParameters)
{
    uint8_t queue_recv = 0;
    uint32_t task2_num = 0;
    while (1)
    {
        xQueueReceive(xQueue, &queue_recv, portMAX_DELAY);
        switch (queue_recv)
        {
	        case KEY0_PRES:
	            /* LCD区域刷新*/ {
	                lcd_fill(6, 131, 233, 313, lcd_discolor[++task2_num % 11]);
	                break;
	            }
	        case KEY1_PRES:
	            /* LED0闪烁*/ {
	                LED0_TOGGLE();
	                break;
	            }
	        default:
	        {
	            break;
	        }
        }
    }
}

从上面的代码中可以看到,task2 任务主要负责从队列中读取消息,如果队列中没有消息,那么task2 任务将会被阻塞,直到队列有消息。在成功读取到消息后,将读取到的消息解析的键值,当消息为按键0 被按下时,LCD 区域刷新,当消息为按键1 被按下时,LED0 闪烁。

下载验证

编译并下载代码,复位后可以看到LCD 屏幕上显示了本次实验的相关信息,如下图所示:

图13.3.3.1 LCD 显示内容

接着按下按键0,可以看到LCD 屏幕区域颜色刷新了,如下图所示:

在这里插入图片描述
多次按下按键0,可以看到,每次按下按键0,LCD 屏幕区域颜色就刷新一次。
接着按下按键1,可以看到,每次按键按键1,LED0 的状态就改变一次。
以上实验结果与预期相符。

FreeRTOS 队列集

在使用队列进行任务之间的“沟通交流”时,一个队列只允许任务间传递的消息为同一种数据类型,如果需要在任务间传递不同数据类型的消息时,那么就可以使用队列集。FreeRTOS提供的队列集功能可以对多个队列进行“监听”,只要被监听的队列中有一个队列有有效的消息,那么队列集的读取任务都可以读取到消息,如果读取任务因读取队列集而被阻塞,那么队列集将解除读取任务的阻塞。使用队列集的好处在于,队列集可以是的任务可以读取多个队列中的消息,而无需遍历所有待读取的队列,以确定具体读取哪一个队列。

使用队列集功能,需要在FreeRTOSConfig.h 文件中将配置项configUSE_QUEUE_SETS 配置为1,来启用队列集功能。

FreeRTOS 队列集相关API 函数

FreeRTOS 中队列集相关的API 函数入下表所示:

在这里插入图片描述

  1. 函数xQueueCreateSet()
    此函数用于创建队列集,该函数在queue.c 文件中有定义,函数的原型如下所示:
QueueSetHandle_t xQueueCreateSet(const UBaseType_t uxEventQueueLength);

函数xQueueCreateSet()的形参描述,如下表所示:

在这里插入图片描述
函数xQueueCreateSet()的具体代码如下所示:

QueueSetHandle_t xQueueCreateSet(const UBaseType_t uxEventQueueLength) {
    QueueSetHandle_t pxQueue;
    /* 创建一个队列作为队列集
     * 队列长度为队列集可容纳的队列数量
     * 队列项目的大小为队列控制块的大小
     * 队列的类型为队列集
     */
    pxQueue = xQueueGenericCreate(uxEventQueueLength, (UBaseType_t) sizeof(Queue_t * ),
        queueQUEUE_TYPE_SET);
    return pxQueue;
}
  1. 函数xQueueAddToSet()
    此函数用于往队列集中添加队列,要注意的时,队列在被添加到队列集之前,队列中不能有有效的消息,该函数在queue.c 文件中有定义,函数的原型如下所示:
BaseType_t xQueueAddToSet( QueueSetMemberHandle_t xQueueOrSemaphore,
QueueSetHandle_t xQueueSet);

函数xQueueAddToSet()的形参描述,如下表所示:

在这里插入图片描述

函数xQueueAddToSet()的具体代码如下所示:
BaseType_t xQueueAddToSet( QueueSetMemberHandle_t xQueueOrSemaphore,

BaseType_t xQueueAddToSet(QueueSetMemberHandle_t xQueueOrSemaphore,
    QueueSetHandle_t xQueueSet) {
    BaseType_t xReturn;
    /* 进入临界区*/
    taskENTER_CRITICAL(); {
        if (((Queue_t * ) xQueueOrSemaphore) - > pxQueueSetContainer != NULL) {
            xReturn = pdFAIL;
        }
        /* 队列中要求没有有效消息*/
        else if (((Queue_t * ) xQueueOrSemaphore) - > uxMessagesWaiting !=
            (UBaseType_t) 0) {
            xReturn = pdFAIL;
        } else {
            /* 将队列所在队列集设为队列集*/
            ((Queue_t * ) xQueueOrSemaphore) - > pxQueueSetContainer =
                xQueueSet;
            xReturn = pdPASS;
        }
    }
    /* 退出临界区*/
    taskEXIT_CRITICAL();
    return xReturn;
}
  1. 函数xQueueRemoveFromSet()
    此函数用于从队列集中移除队列,要注意的时,队列在从队列集移除之前,必须没有有效的消息,该函数在queue.c 文件中有定义,函数的原型如下所示:
BaseType_t xQueueRemoveFromSet( QueueSetMemberHandle_t xQueueOrSemaphore,
QueueSetHandle_t xQueueSet);

函数xQueueRemoveFromSet()的形参描述,如下表所示:

在这里插入图片描述

函数xQueueRemoveFromSet()的返回值,如下表所示:

在这里插入图片描述
在这里插入图片描述
函数xQueueRemoveFromSet()的具体代码如下所示:

BaseType_t xQueueRemoveFromSet(QueueSetMemberHandle_t xQueueOrSemaphore,
    QueueSetHandle_t xQueueSet) {
    BaseType_t xReturn;
    Queue_t *
        const pxQueueOrSemaphore = (Queue_t * ) xQueueOrSemaphore;
    /* 队列需在队列集中,才能移除*/
    if (pxQueueOrSemaphore - > pxQueueSetContainer != xQueueSet) {
        xReturn = pdFAIL;
    }
    /* 队列中没有有效消息时,才能移除*/
    else if (pxQueueOrSemaphore - > uxMessagesWaiting != (UBaseType_t) 0) {
        xReturn = pdFAIL;
    } else {
        /* 进入临界区*/
        taskENTER_CRITICAL(); {
            /* 将队列所在队列集设为空*/
            pxQueueOrSemaphore - > pxQueueSetContainer = NULL;
        }
        /* 对出临界区*/
        taskEXIT_CRITICAL();
        xReturn = pdPASS;
    }
    return xReturn;
}
  1. 函数xQueueSelectFromSet()
    此函数用于在任务中获取队列集中有有效消息的队列,该函数在queue.c 文件中有定义,函数的原型如下所示:
QueueSetMemberHandle_t xQueueSelectFromSet(
QueueSetHandle_t xQueueSet,
TickType_t const xTicksToWait);

函数xQueueSelectFromSet()的形参描述,如下表所示:

在这里插入图片描述
在这里插入图片描述

函数xQueueSelectFromSet()的返回值,如下表所示:

在这里插入图片描述
函数xQueueSelectFromSet()的具体代码如下所示:

QueueSetMemberHandle_t xQueueSelectFromSet(
    QueueSetHandle_t xQueueSet,
    TickType_t
    const xTicksToWait) {
    QueueSetMemberHandle_t xReturn = NULL;
    /* 读取队列集的消息
     * 读取到的消息,
     * 即为队列集中有空闲消息的队列
     */
    (void) xQueueReceive((QueueHandle_t) xQueueSet, & xReturn,
        xTicksToWait);
    return xReturn;
}
  1. 函数xQueueSelectFromSetFromISR()
    此函数用于在中断中获取队列集中有有效消息的队列,该函数在queue.c 文件中有定义,函
    数的原型如下所示:
QueueSetMemberHandle_t xQueueSelectFromSetFromISR(
QueueSetHandle_t xQueueSet );

函数xQueueSelectFromSetFromISR()的形参描述,如下表所示:

在这里插入图片描述

函数xQueueSelectFromSetFromISR()的返回值,如下表所示:

在这里插入图片描述

函数xQueueSelectFromSetFromISR()的具体代码如下所示:

QueueSetMemberHandle_t xQueueSelectFromSetFromISR(
    QueueSetHandle_t xQueueSet) {
    QueueSetMemberHandle_t xReturn = NULL;
    /* 在中断中读取队列集的消息
     * 读取到的消息,
     * 即为队列集中有空闲消息的队列
     */
    (void) xQueueReceiveFromISR((QueueHandle_t) xQueueSet, & xReturn,
        NULL);
    return xReturn;
}

FreeRTOS 队列集操作实验

功能设计

  1. 例程功能
    本实验主要用于学习FreeRTOS 队列集操作相关API 函数的使用,本实验设计了三个任务,这三个任务的功能如下表所示:

在这里插入图片描述
该实验的实验工程,请参考《FreeRTOS 实验例程13-2 FreeRTOS 队列集操作实验》。

软件设计

  1. 程序流程图
    本实验的程序流程图,如下图所示:

在这里插入图片描述
2. FreeRTOS 函数解析
(1) 函数xQueueCreateSet()
此函数用于创建队列集,请参考第13.5 小节《FreeRTOS 队列集相关API 函数》。
(2) 函数xSemaphoreCreateBinary()
此函数用于创建二值信号量,在下文信号量相关章节中讲解。
(3) 函数xQueueAddToSet()
此函数用于将队列添加到队列集中,请参考第13.5 小节《FreeRTOS 队列集相关API 函数》。
(4) 函数xSemaphoreGive()
此函数用于释放二值信号量,在下文信号量相关章节中讲解。
(5) 函数xQueueSelectFromSet()
此函数用于读取队列集,请参考第13.5 小节《FreeRTOS 队列集相关API 函数》。
(6) 函数xSemaphoreTake()
此函数用于获取二值信号量,在下文信号量相关章节中讲解。
3. 程序解析
整体的代码结构,请参考2.1.6 小节,本小节着重讲解本实验相关的部分。
(1) start_task 任务
start_task 任务的入口函数代码如下所示:

/**
 * @brief start_task
 * @param pvParameters : 传入参数(未用到)
 * @retval 无
 */
void start_task(void *pvParameters)
{
    taskENTER_CRITICAL(); /* 进入临界区*/
    /* 创建队列集*/
    xQueueSet = xQueueCreateSet(QUEUESET_LENGTH);
    /* 创建队列*/
    xQueue1 = xQueueCreate(QUEUE_LENGTH, QUEUE_ITEM_SIZE);
    xQueue2 = xQueueCreate(QUEUE_LENGTH, QUEUE_ITEM_SIZE);
    /* 创建二值信号量*/
    xSemaphore = xSemaphoreCreateBinary();
    /* 将队列和二值信号量添加到队列集*/
    xQueueAddToSet(xQueue1, xQueueSet);
    xQueueAddToSet(xQueue2, xQueueSet);
    xQueueAddToSet(xSemaphore, xQueueSet);
    /* 创建任务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 任务。
(2) task1 任务

/**
 * @brief task1
 * @param pvParameters : 传入参数(未用到)
 * @retval 无
 */
void task1(void *pvParameters)
{
    uint8_t key = 0;
    while (1)
    {
        key = key_scan(0);
        switch (key)
        {
            case WKUP_PRES:
                	/* 队列1发送消息*/ {
                    xQueueSend(xQueue1, &key, portMAX_DELAY);
                    break;
                }
            case KEY1_PRES:
                	/* 队列2发送消息*/ {
                    xQueueSend(xQueue2, &key, portMAX_DELAY);
                    break;
                }
            case KEY0_PRES:
                	/* 释放二值信号量*/ {
                    xSemaphoreGive(xSemaphore);
                    break;
                }
            default:
            {
                break;
            }
        }
        vTaskDelay(10);
    }
}

从以上代码中可以看到,task1 任务主要负责扫描按键,当扫描到有效按键后,将键值写入对应的队列或释放信号量。
(3) task2 任务

/**
 * @brief task2
 * @param pvParameters : 传入参数(未用到)
 * @retval 无
 */
void task2(void *pvParameters)
{
    QueueSetMemberHandle_t activate_member = NULL;
    uint32_t queue_recv = 0;
    while (1)
    {
        /* 等待队列集中的队列接收到消息*/
        activate_member = xQueueSelectFromSet(xQueueSet, portMAX_DELAY);
        if (activate_member == xQueue1)
        {
            xQueueReceive(activate_member, &queue_recv, portMAX_DELAY);
            printf("接收到来自xQueue1的消息: %d\r\n", queue_recv);
        }
        else if (activate_member == xQueue2)
        {
            xQueueReceive(activate_member, &queue_recv, portMAX_DELAY);
            printf("接收到来自xQueue2的消息: %d\r\n", queue_recv);
        }
        else if (activate_member == xSemaphore)
        {
            xSemaphoreTake(activate_member, portMAX_DELAY);
            printf("获取到二值信号量: xSemaphore\r\n");
        }
    }
}

从上面的代码中可以看到,task2 任务负责读取队列集中队列的消息,并将读取到的消息,通过串口打印。

下载验证

编译并下载代码,复位后可以看到LCD 屏幕上显示了本次实验的相关信息,如下图所示:

在这里插入图片描述

接着按下按键0,就可以在串口调试助手上看到获取到了二值信号量,如下图所示:

在这里插入图片描述

接着按下按键1,就可以在串口调试助手上看到读取到了来自队列2 的消息,如下图所示:

在这里插入图片描述

其中读取到的消息“2”就是按键1 的键值。接着按下按键UP,就可以在串口调试助手上看到读取到了来自队列1 的消息,如下图所示:

在这里插入图片描述

其中读取到的消息“4”就是按键UP 的键值。可以看出,以上实验结果与预期相符。

FreeRTOS 队列集模拟事件标志位实验

功能设计

  1. 例程功能
    本实验主要用于学习FreeRTOS 队列集操作相关API 函数的使用,本实验设计了三个任务,这三个任务的功能如下表所示:
    在这里插入图片描述
    该实验的实验工程,请参考《FreeRTOS 实验例程13-3 FreeRTOS 队列集模拟事件标志位实验》。

软件设计

  1. 程序流程图
    本实验的程序流程图,如下图所示:

在这里插入图片描述

  1. FreeRTOS 函数解析
    (1) 函数 xQueueCreateSet()
    此函数用于创建队列集,请参考第 13.5 小节《FreeRTOS 队列集相关 API 函数》。
    (2) 函数 xQueueAddToSet()
    此函数用于将队列添加到队列集中,请参考第 13.5 小节《FreeRTOS 队列集相关 API 函数》。
    (3) 函数 xQueueSelectFromSet()
    此函数用于读取队列集,请参考第 13.5 小节《FreeRTOS 队列集相关 API 函数》
  2. 程序解析
    整体的代码结构,请参考 2.1.6 小节,本小节着重讲解本实验相关的部分。
    (1) start_task 任务
    start_task 任务的入口函数代码如下所示:
/**
 * @brief start_task
 * @param pvParameters : 传入参数(未用到)
 * @retval 无
 */
void start_task(void *pvParameters)
{
    taskENTER_CRITICAL(); /* 进入临界区*/
    /* 创建队列集*/
    xQueueSet = xQueueCreateSet(QUEUESET_LENGTH);
    /* 创建队列*/
    xQueue1 = xQueueCreate(QUEUE_LENGTH, QUEUE_ITEM_SIZE);
    xQueue2 = xQueueCreate(QUEUE_LENGTH, QUEUE_ITEM_SIZE);
    /* 创建二值信号量*/
    xSemaphore = xSemaphoreCreateBinary();
    /* 将队列和二值信号量添加到队列集*/
    xQueueAddToSet(xQueue1, xQueueSet);
    xQueueAddToSet(xQueue2, xQueueSet);
    xQueueAddToSet(xSemaphore, xQueueSet);
    /* 创建任务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 任务。
(2) task1 任务

/**
 * @brief task1
 * @param pvParameters : 传入参数(未用到)
 * @retval 无
 */
void task1(void *pvParameters)
{
    uint8_t key = 0;
    while (1)
    {
        key = key_scan(0);
        switch (key)
        {
        case WKUP_PRES: /* 队列1发送消息*/
        {
            xQueueSend(xQueue1, &key, portMAX_DELAY);
            break;
        }
        case KEY1_PRES: /* 队列2发送消息*/
        {
            xQueueSend(xQueue2, &key, portMAX_DELAY);
            break;
        }
        case KEY0_PRES: /* 释放二值信号量*/
        {
            xSemaphoreGive(xSemaphore);
            break;
        }
        default:
        {
            break;
        }
        }
        vTaskDelay(10);
    }
}

从以上代码中可以看到,task1 任务主要负责扫描按键,当扫描到有效按键后,将键值写入
对应的队列或释放信号量。
(3) task2 任务

/**
 * @brief task2
 * @param pvParameters : 传入参数(未用到)
 * @retval 无
 */
void task2(void *pvParameters)
{
    QueueSetMemberHandle_t activate_member = NULL;
    uint32_t queue_recv = 0;
    while (1)
    {
        /* 等待队列集中的队列接收到消息*/
        activate_member = xQueueSelectFromSet(xQueueSet, portMAX_DELAY);
        if (activate_member == xQueue1)
        {
            xQueueReceive(activate_member, &queue_recv, portMAX_DELAY);
            printf("接收到来自xQueue1的消息: %d\r\n", queue_recv);
        }
        else if (activate_member == xQueue2)
        {
            xQueueReceive(activate_member, &queue_recv, portMAX_DELAY);
            printf("接收到来自xQueue2的消息: %d\r\n", queue_recv);
        }
        else if (activate_member == xSemaphore)
        {
            xSemaphoreTake(activate_member, portMAX_DELAY);
            printf("获取到二值信号量: xSemaphore\r\n");
        }
    }
}

从上面的代码中可以看到,task2 任务负责读取队列集中队列的消息,并将读取到的消息,通过串口打印。

下载验证

编译并下载代码,复位后可以看到LCD 屏幕上显示了本次实验的相关信息,如下图所示:

在这里插入图片描述

接着按下按键0,就可以在串口调试助手上看到获取到了二值信号量,如下图所示:

在这里插入图片描述

图13.6.3.2 串口调试助手一
接着按下按键1,就可以在串口调试助手上看到读取到了来自队列2 的消息,如下图所示:

在这里插入图片描述

图13.6.3.3 串口调试助手二
其中读取到的消息“2”就是按键1 的键值。接着按下按键UP,就可以在串口调试助手上看到读取到了来自队列1 的消息,如下图所示:

在这里插入图片描述

图13.6.3.4 串口调试助手三
其中读取到的消息“4”就是按键UP 的键值。可以看出,以上实验结果与预期相符。

FreeRTOS 信号量

信号量是操作系统中重要的一部分,信号量是任务间同步的一种机制,信号量可以用在多
任务访问同一资源时的资源管理。FreeRTOS 提供了多种信号量,按信号量的功能可分为二值信号量、计数型信号量、互斥信号量和递归互斥信号量,不同类型的信号量有其不同的应用场景,合理地使用信号量可以帮助开发者快速开发稳健的系统。本章就来学习FreeRTOS中的信号量。

FreeRTOS 信号量简介

信号量是一种解决同步问题的机制,可以实现对共享资源的有序访问。其中,“同步”指的
是任务间的同步,即信号量可以使得一个任务等待另一个任务完成某件事情后,才继续执行;而“有序访问”指的是对被多任务或中断访问的共享资源(如全局变量)的管理,当一个任务在访问(读取或写入)一个共享资源时,信号量可以防止其他任务或中断在这期间访问(读取或写入)这个共享资源。
举一个例子,假设某个停车场有 100 个停车位(共享资源),这个 100 个停车位对所有人
(访问共享资源的任务或中断)开放。如果有一个人要在这个停车场停车,那么就需要先判断这个停车场是否还有空车位(判断信号量是否有资源),如果此时停车场正好有空车位(信号量有资源),那么就可以直接将车开入空车位进行停车(获取信号量成功),如果此时停车场已经没有空车位了(信号量没有资源),那么这个人可以选择不停车(获取信号量失败),也可以选择等待(任务阻塞)其他人将车开出停车场(释放信号量资源),让后再将车停入空车位。
在上面的这个例子中,空车位的数量相当于信号量的资源数,获取信号量相当于占用了空
车位,而释放信号量就相当于让出了占用的空车位。信号量用于管理共享资源的场景相当于对共享资源上了个锁,只有任务成功获取到了锁的钥匙,才能够访问这个共享资源,访问完共享资源后还得归还钥匙,当然钥匙可以不只一把,即信号量可以有多个资源。

FreeRTOS 二值信号量

FreeRTOS 二值信号量简介

前面说过,信号量是基于队列实现的,二值信号量也不例外,二值信号量实际上就是一个
队列长度为 1 的队列,在这种情况下,队列就只有空和满两种情况,这不就是二值情况吗?二值信号量通常用于互斥访问或任务同步,与互斥信号量比较类似,但是二值信号量有可能会导致优先级翻转的问题。优先级翻转问题指的是,当一个高优先级任务因获取一个被低优先级任务获取而处于没有资源状态的二值信号量时,这个高优先级的任务将被阻塞,直到低优先级的任务释放二值信号量,而在这之前,如果有一个优先级介于这个高优先级任务和低优先级任务之间的任务就绪,那么这个中等优先级的任务就会抢占低优先级任务的运行,这么一来,这三个任务中优先级最高的任务反而要最后才运行,这就是二值信号量带来的优先级翻转问题,用户在实际开发中要注意这种问题。
和队列一样,在获取二值信号量的时候,允许设置一个阻塞超时时间,阻塞超时时间是当
任务获取二值信号量时,由于二值信号量处于没有资源的状态,而导致任务进入阻塞状态的最大系统时钟节拍数。如果多个任务同时因获取同一个处于没有资源状态的二值信号量而被阻塞,那么在二值信号量有资源的时候,这些阻塞任务中优先级高的任务将优先获得二值信号量的资源并解除阻塞。

FreeRTOS 二值信号量相关 API 函数

FreeRTOS 提供了二值信号量的一些相关操作函数,其中常用的二值信号量相关 API 函数,
如下表所示:
在这里插入图片描述

  1. 函数 xSemaphoreCreateBinary()
    此函数用于使用动态方式创建二值信号量,创建二值信号量所需的内存,由 FreeRTOS 从
    FreeRTOS 管理的堆中进行分配。该函数实际上是一个宏定义,在 semphr.h 文件中有定义,具体的代码如下所示:
#define xSemaphoreCreateBinary() \
 xQueueGenericCreate( ( UBaseType_t ) 1, \
 semSEMAPHORE_QUEUE_ITEM_LENGTH, \
 queueQUEUE_TYPE_BINARY_SEMAPHORE)

从上面的代码中可以看出,函数 xSemaphoreCreateBinary() 实 际 上 是 调 用 了 函 数
xQueueGenericCreate()创建了一个队列长度为 1 且队列项目大小为信号量队列项目大小的二值信号量类型队列。
2. 函数 xSemaphoreCreateBinaryStatic()
此函数用于使用静态方式创建二值信号量,创建二值信号量所需的内存,需要由用户手动
分配并提供。该函数实际上是一个宏定义,在 semphr.h 文件中有定义,具体的代码如下所示:

#define xSemaphoreCreateBinaryStatic(pxStaticSemaphore) \
 xQueueGenericCreateStatic( ( UBaseType_t ) 1, \
 semSEMAPHORE_QUEUE_ITEM_LENGTH, \
 NULL, \
 pxStaticSemaphore, \
 queueQUEUE_TYPE_BINARY_SEMAPHORE)

从 上 面 的 代 码 中 可 以 看 出 , 函 数 xSemaphoreCreateStatic() 实 际 上 是 调 用 了 函 数xQueueGenericCreateStatic()创建了一个队列长度为 1 且队列项目大小为信号量队列项目大小的二值信号量类型队列。
3. 函数 xSemaphoreTake()
此函数用于获取信号量,如果信号量处于没有资源的状态,那么此函数可以选择将任务进
行阻塞,如果成功获取了信号量,那信号量的资源数将会减 1。该函数实际上是一个宏定义,在 semphr.h 文件中有定义,具体的代码如下所示:
#define xSemaphoreTake( xSemaphore,
xBlockTime)
xQueueSemaphoreTake( ( xSemaphore ),
( xBlockTime ))
从 上 面 的 代 码 中 可 以 看 出 , 函 数 xSemaphoreTake() 实 际 上 是 调 用 了 函 数
xQueueSemaphoreTake()来获取信号量,函数 xQueueSemaphoreTake()在 queue.c 文件中有定义,具体的代码如下所示:

从上面的代码中可以看出,函数 xQueueSemaphoreTake()不仅仅是用于获取二值信号量,还有计数型信号量,互斥信号量的获取,都是通过宏定义间接地调用了此函数。
4. 函数 xSemaphoreTakeFromISR()
此函数用于在中断中获取信号量。该函数实际上是一个宏定义,在 semphr.h 文件中有定义,具体的代码如下所示:

#define xSemaphoreTakeFromISR( xSemaphore, \
 pxHigherPriorityTaskWoken) \
 xQueueReceiveFromISR( ( QueueHandle_t ) ( xSemaphore ), \
 NULL, \
 ( pxHigherPriorityTaskWoken ))

从 上 面 的 代 码 中 可 以 看 出 , 函 数 xSemaphoreTakeFromISR() 实 际 上 是 调 用 了 函 数xQueueReceiveFromISR()来获取信号量。要特别注意的是,函数 xSemaphoreTakeFromISR()于函
数 xSemaphoreTake()不同,函数 xSemaphoreTakeFromISR()只能用于获取二值信号量和计数型信号量,而不能用于获取互斥信号量。
5. 函数 xSemaphoreGive()
此函数用于释放信号量,如果信号量处于资源满的状态,那么此函数可续选择将任务进行
阻塞,如果成功释放了信号量,那信号量的资源数将会加 1。该函数实际上是一个宏定义,在semphr.h 文件中有定义,具体的代码如下所示:

#define xSemaphoreGive( xSemaphore) \
 xQueueGenericSend( ( QueueHandle_t ) ( xSemaphore ), \
 NULL, \
 semGIVE_BLOCK_TIME, \
 queueSEND_TO_BACK)

从 上 面 的 代 码 中 可 以 看 出 , 函 数 xSemaphoreGive() 实 际 上 是 调 用 了 函 数
xQueueGenericSend()。
6. 函数 xSemaphoreGiveFromISR()
此函数用于在中断中释放信号量。该函数实际上是一个宏定义,在 semphr.h 文件中有定义,具体的代码如下所示:

#define xSemaphoreGiveFromISR( xSemaphore, \
 pxHigherPriorityTaskWoken) \
 xQueueGiveFromISR( ( QueueHandle_t ) ( xSemaphore ), \
 ( pxHigherPriorityTaskWoken ))

从 上 面 的 代 码 中 可 以 看 出 , 函 数 xSemaphoreGiveFromISR() 实 际 上 是 调 用 了 函 数xQueueGiveFromISR()。函数 xQueueGiveFromISR()在 queue.c 文件中有定义,具体的代码如下所示:

要特别注意的是,函数 xQueueGiveFromISR()只能用于释放二值信号量和计数型信号量,
而不能用于获取互斥信号量,因为互斥信号量会有优先级继承的处理,而中断不属于任务,没法进行优先级继承。
7. 函数 vSemaphoreDelete()
此函数用于删除已创建二值信号量。该函数实际上是一个宏定义,在 semphr.h 文件中有定
义,具体的代码如下所示:

#define vSemaphoreDelete(xSemaphore) \
 vQueueDelete ( QueueHandle_t ) \
 ( xSemaphore ))

从上面的代码中可以看出,函数 vSemaphoreDelete()实际上是调用了函数 vQueueDelete()删除已创建的二值信号量队列。

FreeRTOS 二值信号量操作实验

功能设计

  1. 例程功能
    本实验主要用于学习 FreeRTOS 二值信号量操作相关 API 函数的使用,本实验设计了三个
    任务,这三个任务的功能如下表所示:
    在这里插入图片描述
    该实验的实验工程,请参考《FreeRTOS 实验例程 14-1 FreeRTOS 二值信号量操作实验》。

软件设计

  1. 程序流程图
    本实验的程序流程图,如下图所示:

在这里插入图片描述
2. FreeRTOS 函数解析
(1) 函数 xSemaphoreCreateBinary()
此函数用于创建二值信号量,请参考第 14.2.2 小节《FreeRTOS 二值信号量相关 API 函数》。
(2) 函数 xSemaphoreGive()
此函数用于获取信号量,请参考第 14.2.2 小节《FreeRTOS 二值信号量相关 API 函数》。
(3) 函数 xSemaphoreTake()
此函数用于释放信号量,请参考第 14.2.2 小节《FreeRTOS 二值信号量相关 API 函数》。
3. 程序解析
整体的代码结构,请参考 2.1.6 小节,本小节着重讲解本实验相关的部分。
(1) start_task 任务
start_task 任务的入口函数代码如下所示:

start_task 任务主要用于创建二值信号量、task1 任务和 task2 任务。
(2) task1 任务

/**
 * @brief task1
 * @param pvParameters : 传入参数(未用到)
 * @retval 无
 */
void task1(void * pvParameters) {
    uint8_t key = 0;
    while (1) {
        key = key_scan(0);
        switch (key) {
            case KEY0_PRES:
                {
                    xSemaphoreGive(BinarySemaphore); /* 释放二值信号量*/
                    break;
                }
            default:
                {
                    break;
                }
        }
        vTaskDelay(10);
    }
}

从以上代码中可以看到,task1 任务主要负责扫描按键,当扫描到按键0 被按下后,释放二
值信号量。
(3) task2 任务

/**
 * @brief task2
 * @param pvParameters : 传入参数(未用到)
 * @retval 无
 */
void task2(void * pvParameters) {
    uint32_t task2_num = 0;
    while (1) {
        /* 获取二值信号量*/
        xSemaphoreTake(BinarySemaphore, portMAX_DELAY);
        /* LCD区域刷新*/
        lcd_fill(6, 131, 233, 313, lcd_discolor[++task2_num % 11]);
    }
}

从上面的代码中可以看到,task2 任务负责获取二值信号量,当成功获取到二值信号量之后,刷新LCD 屏幕区域显示。

下载验证

编译并下载代码,复位后可以看到LCD 屏幕上显示了本次实验的相关信息,如下图所示:

在这里插入图片描述

接着按下按键0,就可以看到LCD 屏幕的区域的颜色刷新了,如下图所示:

在这里插入图片描述

重复按下按键0,可以看到LCD 屏幕区域不断刷新颜色。可以看出,以上实验结果与预期相符。

FreeRTOS 计数型信号量

FreeRTOS 计数型信号量简介

计数型信号量与二值信号量类似,二值信号量相当于队列长度为1 的队列,因此二值信号
量只能容纳一个资源,这也是为什么命名为二值信号量,而计数型信号量相当于队列长度大于0 的队列,因此计数型信号量能够容纳多个资源,这是在计数型信号量被创建的时候确定的。
计数型信号量通常用于一下两种场合:

  1. 事件计数
    在这种场合下,每次事件发生后,在事件处理函数中释放计数型信号量(计数型信号量的
    资源数加1),其他等待事件发生的任务获取计数型信号量(计数型信号量的资源数减1),这么一来等待事件发生的任务就可以在成功获取到计数型信号量之后执行相应的操作。在这种场合下,计数型信号量的资源数一般在创建时设置为0。
  2. 资源管理
    在这种场合下,计数型信号量的资源数代表着共享资源的可用数量,例如前面举例中停车
    场中的空车位。一个任务想要访问共享资源,就必须先获取这个共享资源的计数型信号量,之后在成功获取了计数型信号量之后,才可以对这个共享资源进行访问操作,当然,在使用完共享资源后也要释放这个共享资源的计数型信号量。在这种场合下,计数型信号量的资源数一般在创建时设置为受其管理的共享资源的最大可用数量。

FreeRTOS 计数型信号量相关API 函数

FreeRTOS 提供了计数型信号量的一些相关操作函数,其中常用的计数型信号量相关API 函
数,如下表所示:
在这里插入图片描述
从上面中可以看出,计数型信号量除了创建函数之外,其余的获取、释放等信号量操作函
数,都与二值信号量相同,因此这里重点讲解计数型信号量的创建函数。

  1. 函数xSemaphoreCreateCounting()
    此函数用于使用动态方式创建计数型信号量,创建计数型信号量所需的内存,由FreeRTOS
    从FreeRTOS 管理的堆中进行分配。该函数实际上是一个宏定义,在semphr.h 中有定义,具体的代码如下所示:
#define xSemaphoreCreateCounting( uxMaxCount, \
uxInitialCount ) \
xQueueCreateCountingSemaphore( ( uxMaxCount ), \
( uxInitialCount ))

从上面的代码中可以看出,函数xSemaphoreCreateCounting() 实际上是调用了函数xQueueCreateCountingSemaphore(),函数xQueueCreateCountingSemaphore()在queue.c 文件中有定义,具体的代码如下所示:

QueueHandle_t xQueueCreateCountingSemaphore(
    const UBaseType_t uxMaxCount,
        const UBaseType_t uxInitialCount) {
    QueueHandle_t xHandle = NULL;
    /* 计数型信号量的最大资源数必须大于0
     * 计数型信号量的初始资源数不能超过最大资源数
     */
    if ((uxMaxCount != 0) && (uxInitialCount <= uxMaxCount)) {
        /* 创建一个队列
         * 队列长度为计数型信号量的最大资源数
         * 队列类型为计数型信号量
         */
        xHandle = xQueueGenericCreate(uxMaxCount,
            queueSEMAPHORE_QUEUE_ITEM_LENGTH,
            queueQUEUE_TYPE_COUNTING_SEMAPHORE);
        /* 判断队列是否创建成功*/
        if (xHandle != NULL) {
            /* 队列的非空闲项目数量即为计数型信号量的资源数*/
            ((Queue_t * ) xHandle) - > uxMessagesWaiting = uxInitialCount;
            /* 用于调试,不用理会*/
            traceCREATE_COUNTING_SEMAPHORE();
        } else {
            /* 用于调试,不用理会*/
            traceCREATE_COUNTING_SEMAPHORE_FAILED();
        }
    } else {
        configASSERT(xHandle);
        mtCOVERAGE_TEST_MARKER();
    }
    return xHandle;
}

从上面的代码中可以看出,计数型信号量的就是一个队列长度为计数型信号量最大资源数
的队列,而队列的非空闲项目数量就是用来记录计数型信号量的可用资源的。
2. 函数xSemaphoreCreateCountingStatic()
此函数用于使用静态方式创建计数型信号量,创建计数型信号量所需的内存,需要由用户
手动分配并提供。该函数实际上是一个宏定义,在semphr.h 中有定义,具体的代码如下所示:

#
define xSemaphoreCreateCountingStatic(uxMaxCount, \
    uxInitialCount, \
    pxSemaphoreBuffer)\
xQueueCreateCountingSemaphoreStatic((uxMaxCount), \ (uxInitialCount), \ (pxSemaphoreBuffer))

从上面的代码中可以看出,函数xSemaphoreCreateCountingStatic()实际上是调用了函数
xQueueCreateCountingSemaphoreStatic(),函数xQueueCreateCountingSemaphoreStatic()在queue.c
文件中有定义,其函数内容与函数xQueueCreateCountingSemaphore()类似,只是动态创建队列的函数替换成了静态创建队列的函数。

FreeRTOS 计数型信号量操作实验

功能设计

  1. 例程功能
    本实验主要用于学习FreeRTOS 计数型信号量操作相关API 函数的使用,本实验设计了三
    个任务,这三个任务的功能如下表所示:
    在这里插入图片描述
    该实验的实验工程,请参考《FreeRTOS 实验例程14-2 FreeRTOS 计数型信号量操作实验》。

软件设计

  1. 程序流程图
    本实验的程序流程图,如下图所示:
    在这里插入图片描述
  2. FreeRTOS 函数解析
    (1) 函数xSemaphoreCreateCounting()
    此函数用于创建计数型信号量,请参考第14.4.2 小节《FreeRTOS 计数型信号量相关API 函
    数》。
  3. 程序解析
    整体的代码结构,请参考2.1.6 小节,本小节着重讲解本实验相关的部分。
    (1) start_task 任务
    start_task 任务的入口函数代码如下所示:
/**
 * @brief start_task
 * @param pvParameters : 传入参数(未用到)
 * @retval 无
 */
void start_task(void * pvParameters) {
    taskENTER_CRITICAL(); /* 进入临界区*/
    /* 创建计数型信号量*/
    CountSemaphore =
        xSemaphoreCreateCounting((UBaseType_t) 255, /* 计数型信号量最大值*/ (UBaseType_t) 0); /* 计数型信号量初始值*/
    /* 创建任务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 任务。
(2) task1 任务

/**
 * @brief task1
 * @param pvParameters : 传入参数(未用到)
 * @retval 无
 */
void task1(void * pvParameters) {
    uint8_t key = 0;
    UBaseType_t semaphore_val = 0;
    while (1) {
        key = key_scan(0);
        switch (key) {
            case KEY0_PRES:
                {
                    /* 释放计数型信号量*/
                    xSemaphoreGive(CountSemaphore);
                    /* 获取计数型信号量资源数*/
                    semaphore_val = uxSemaphoreGetCount(CountSemaphore);
                    /* 在LCD上显示计数型信号量资源数*/
                    lcd_show_xnum(166, 111, semaphore_val, 2, 16, 0, BLUE);
                }
            default:
                {
                    break;
                }
        }
        vTaskDelay(10);
    }
}

从以上代码中可以看到,task1 任务主要负责扫描按键,当扫描到按键0 被按下后,释放计
数型信号量,并且在LCD 屏幕上显示当前计数型信号量现有资源数。
(3) task2 任务

/**
 * @brief task2
 * @param pvParameters : 传入参数(未用到)
 * @retval 无
 */
void task2(void * pvParameters) {
    UBaseType_t semaphore_val = 0;
    uint32_t task2_num = 0;
    while (1) {
        /* 获取计数型信号量*/
        xSemaphoreTake(CountSemaphore, portMAX_DELAY);
        /* 获取计数型信号量资源数*/
        semaphore_val = uxSemaphoreGetCount(CountSemaphore);
        /* 在LCD上显示计数型信号量资源数*/
        lcd_show_xnum(166, 111, semaphore_val, 2, 16, 0, BLUE);
        /* LCD区域刷新*/
        lcd_fill(6, 131, 233, 313, lcd_discolor[++task2_num % 11]);
        vTaskDelay(1000);
    }
}

从上面的代码中可以看到,task2 任务负责获取计数型信号量,当成功获取到计数型信号量
之后,更新LCD 屏幕上的计数型信号量现有资源数,并刷新LCD 屏幕区域显示。

下载验证

编译并下载代码,复位后可以看到LCD 屏幕上显示了本次实验的相关信息,如下图所示:
在这里插入图片描述
接着快速地重复按下按键0,来释放计数型信号量,可以看到,LCD 屏幕上显示实时显示
的计数型信号量的资源数不断增加,但是每间隔1000Ticks 的时间后,LCD 屏幕上显示的计数型信号量资源数就减1,并且LCD 屏幕区域同时刷新,直到计数型信号量的资源数为0,这是因为task2 任务每间隔1000Ticks 的时间就获取一次计数型信号量,只要计数型信号量有资源,task2 任务就可以获取计数型信号量并执行相应操作。可以看出,以上实验结果与预期相符。

优先级翻转

在使用二值信号量和计数型信号量的时候,经常会遇到优先级翻转的问题,优先级在抢占
式内核中是非常常见的,但是在实时操作系统中是不允许出现优先级翻转的,因为优先级翻转会破坏任务的预期顺序,可能会导致未知的严重后果,下面展示了一个优先级翻转的例子,如下图所示:
在这里插入图片描述
优先级翻转示意图,如上图所示,定义:任务H 为优先级最高的任务,任务L 为优先级中
最低的任务,任务M 为优先级在任务H 与任务L 之间的任务。
(1) 任务H 和任务M 为阻塞状态,等待某一事件发生,此时任务L 正在运行。
(2) 此时任务L 要访问共享资源,因此需要获取信号量。
(3) 任务L 成功获取信号量,并且此时信号量已无资源,任务L 开始访问共享资源。
(4) 此时任务H 就绪,抢占任务L 运行。
(5) 任务H 开始运行。
(6) 此时任务H 要访问共享资源,因此需要获取信号量,但信号量已无资源,因此任务H
阻塞等待信号量资源。
(7) 任务L 继续运行。
(8) 此时任务M 就绪,抢占任务L 运行。
(9) 任务M 正在运行。
(10) 任务M 运行完毕,继续阻塞。
(11) 任务L 继续运行。
(12) 此时任务L 对共享资源的访问操作完成,释放信号量,虽有任务H 因成功获取信号
量,解除阻塞并抢占任务L 运行。
(13) 任务H 得以运行。
从上面优先级翻转的示例中,可以看出,任务H 为最高优先级的任务,因此任务H 执行的
操作需要有较高的实时性,但是由于优先级翻转的问题,导致了任务H 需要等到任务L 释放信号量才能够运行,并且,任务L 还会被其他介于任务H 与任务L 任务优先级之间的任务M 抢
占,因此任务H 还需等待任务M 运行完毕,这显然不符合任务H 需要的高实时性要求。

优先级翻转实验

功能设计

  1. 例程功能
    本实验主要用于学习FreeRTOS 信号量带来的优先级翻转问题,本实验设计了四个任务,
    这四个任务的功能如下表所示:
    在这里插入图片描述
    该实验的实验工程,请参考《FreeRTOS 实验例程14-3 FreeRTOS 优先级翻转实验》。

软件设计

  1. 程序流程图
    本实验的程序流程图,如下图所示:
    在这里插入图片描述
  2. 程序解析
    整体的代码结构,请参考2.1.6 小节,本小节着重讲解本实验相关的部分。
    (1) start_task 任务
    start_task 任务的入口函数代码如下所示:
/**
 * @brief start_task
 * @param pvParameters : 传入参数(未用到)
 * @retval 无
 */
void start_task(void * pvParameters) {
    taskENTER_CRITICAL(); /* 进入临界区*/
    /* 创建计数型信号量*/
    Semaphore = xSemaphoreCreateCounting(1, 1);
    /* 创建任务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);
    /* 创建任务3 */
    xTaskCreate((TaskFunction_t) task3, (const char * )
        "task3", (uint16_t) TASK3_STK_SIZE, (void * ) NULL, (UBaseType_t) TASK3_PRIO, (TaskHandle_t * ) & Task3Task_Handler);
    vTaskDelete(StartTask_Handler); /* 删除开始任务*/
    taskEXIT_CRITICAL(); /* 退出临界区*/
}

start_task 任务主要用于创建计数型信号量、task1 任务、task2 任务和task3 任务。
(2) task1 任务、task2 任务和task3 任务

/**
 * @brief task1
 * @param pvParameters : 传入参数(未用到)
 * @retval 无
 */
void task1(void * pvParameters) {
        vTaskDelay(500);
        while (1) {
            printf("task1 ready to take semaphore\r\n");
            xSemaphoreTake(Semaphore, portMAX_DELAY); /* 获取计数型信号量*/
            printf("task1 has taked semaphore\r\n");
            printf("task1 running\r\n");
            printf("task1 give semaphore\r\n");
            xSemaphoreGive(Semaphore); /* 释放计数型信号量*/
            vTaskDelay(100);
        }
    }
    /**
     * @brief task2
     * @param pvParameters : 传入参数(未用到)
     * @retval 无
     */
void task2(void * pvParameters) {
        uint32_t task2_num = 0;
        vTaskDelay(200);
        while (1) {
            for (task2_num = 0; task2_num < 5; task2_num++) {
                printf("task2 running\r\n");
                delay_ms(100); /* 模拟运行,不触发任务调度*/
            }
            vTaskDelay(1000);
        }
    }
    /**
     * @brief task3
     * @param pvParameters : 传入参数(未用到)
     * @retval 无
     */
void task3(void * pvParameters) {
    uint32_t task3_num = 0;
    while (1) {
        printf("task3 ready to take semaphore\r\n");
        xSemaphoreTake(Semaphore, portMAX_DELAY); /* 获取计数型信号量*/
        printf("task3 has taked semaphore\r\n");
        for (task3_num = 0; task3_num < 5; task3_num++) {
            printf("task3 running\r\n");
            delay_ms(100); /* 模拟运行,不触发任务调度*/
        }
        printf("task3 give semaphore\r\n");
        xSemaphoreGive(Semaphore); /* 释放计数型信号量*/
        vTaskDelay(1000);
    }
}

以上task1 任务、task2 任务和task3 任务就是展示了优先级翻转的问题。

下载验证

编译并下载代码,复位后可以看到LCD 屏幕上显示了本次实验的相关信息,如下图所示:
在这里插入图片描述
同时,通过串口打印了本次实验的相关信息,如下图所示:
在这里插入图片描述

  1. task3 成功获取计数型信号量,并运行。
  2. task2 任务抢占task3 任务运行。
  3. task1 任务抢占task2 任务运行,task1 任务获取计数型信号量失败,被阻塞。
  4. task2 任务继续运行。
  5. task2 任务运行完毕,task3 任务运行。
  6. task3 任务释放计数型信号量,task1 任务获取计数型信号量,解除阻塞,得以运行。
    从串口打印的信息就能看出,优先级最高的task1 任务,最后才得到CPU 使用权,这就是
    优先级翻转带来的问题。

FreeRTOS 互斥信号量

FreeRTOS 互斥信号量简介

互斥信号量其实就是一个拥有优先级继承的二值信号量,在同步的应用中(任务与任务或
中断与任务之间的同步)二值信号量最适合。互斥信号量适合用于那些需要互斥访问的应用中。
在互斥访问中互斥信号量相当于一把钥匙,当任务想要访问共享资源的时候就必须先获得这把钥匙,当访问完共享资源以后就必须归还这把钥匙,这样其他的任务就可以拿着这把钥匙去访问资源。
互斥信号量使用和二值信号量相同的API 操作函数,所以互斥信号量也可以设置阻塞时间,
不同于二值信号量的是互斥信号量具有优先级继承的机制。当一个互斥信号量正在被一个低优先级的任务持有时,如果此时有个高优先级的任务也尝试获取这个互斥信号量,那么这个高优先级的任务就会被阻塞。不过这个高优先级的任务会将低优先级任务的优先级提升到与自己相同的优先级,这个过程就是优先级继承。优先级继承尽可能的减少了高优先级任务处于阻塞态的时间,并且将“优先级翻转”的影响降到最低。
优先级继承并不能完全的消除优先级翻转的问题,它只是尽可能的降低优先级翻转带来的
影响。实时应用应该在设计之初就要避免优先级翻转的发生。互斥信号量不能用于中断服务函数中,原因如下:
(1) 互斥信号量有任务优先级继承的机制,但是中断不是任务,没有任务优先级,所以互斥
信号量只能用与任务中,不能用于中断服务函数。
(2) 中断服务函数中不能因为要等待互斥信号量而设置阻塞时间进入阻塞态。

FreeRTOS 互斥信号量相关API 函数

FreeRTOS 提供了互斥信号量的一些相关操作函数,其中常用的互斥信号量相关API 函数,
如下表所示:
在这里插入图片描述
从上面中可以看出,互斥信号量除了创建函数之外,其余的获取、释放等信号量操作函数,
都与二值信号量相同,因此这里重点讲解互斥信号量的创建函数。

  1. 函数xSemaphoreCreateMutex()
    此函数用于使用动态方式创建互斥信号量,创建互斥信号量所需的内存,由FreeRTOS 从
    FreeRTOS 管理的堆中进行分配。该函数实际上是一个宏定义,在semphr.h 中有定义,具体的代码如下所示:
#define xSemaphoreCreateMutex() xQueueCreateMutex(queueQUEUE_TYPE_MUTEX)

从上面的代码中可以看出,函数xSemaphoreCreateMutex() 实际上是调用了函数
xQueueCreateMutex(),函数xQueueCreateMutex()在queue.c 文件中有定义,具体的代码如下所示:

QueueHandle_t xQueueCreateMutex(const uint8_t ucQueueType) {
    QueueHandle_t xNewQueue;
    const UBaseType_t uxMutexLength = (UBaseType_t) 1;
    const UBaseType_t uxMutexSize = (UBaseType_t) 0;
    /* 创建一个队列
     * 队列长度为1
     * 队列项目大小为0
     */
    xNewQueue = xQueueGenericCreate(uxMutexLength, uxMutexSize, ucQueueType);
    /* 初始化互斥信号量*/
    prvInitialiseMutex((Queue_t * ) xNewQueue);
    return xNewQueue;
}

从上面的代码中可以看出,互斥信号量的就是一个队列长度为1 的队列,且队列项目的大
小为0,而队列的非空闲项目数量就是互斥信号量的资源数。函数xQueueCreateMutex()还会调用函数prvInitialiseMutex()对互斥信号量进行初始化,函数prvInitialiseMutex()在queue.c 文件中有定义,具体的代码如下所示:

static void prvInitialiseMutex(Queue_t * pxNewQueue) {
    if (pxNewQueue != NULL) {
        /* 互斥信号量的持有者初始化为空*/
        pxNewQueue - > u.xSemaphore.xMutexHolder = NULL;
        /* 队列类型初始化为互斥信号量*/
        pxNewQueue - > uxQueueType = queueQUEUE_IS_MUTEX;
        /* 互斥信号量的资源数初始化为0 */
        pxNewQueue - > u.xSemaphore.uxRecursiveCallCount = 0;
        /* 用于调试,不用理会*/
        traceCREATE_MUTEX(pxNewQueue);
        /* 新建的互斥信号量是有资源的*/
        (void) xQueueGenericSend(pxNewQueue,
            NULL, (TickType_t) 0 U,
            queueSEND_TO_BACK);
    } else {
        /* 用于调试,不用理会*/
        traceCREATE_MUTEX_FAILED();
    }
}
  1. 函数xSemaphoreCreateMutexStatic()
    此函数用于使用静态方式创建互斥信号量,创建互斥信号量所需的内存,需要由用户手动
    分配并提供。该函数实际上是一个宏定义,在semphr.h 中有定义,具体的代码如下所示:
#define xSemaphoreCreateMutexStatic( pxMutexBuffer) \
xQueueCreateMutexStatic( queueQUEUE_TYPE_MUTEX, \
( pxMutexBuffer ) )

从上面的代码中可以看出,函数xSemaphoreCreateMutexStatic()实际上是调用了函数
xQueueCreateMutexStatic(),而函数xQueueCreateMutexStatic()在queue.c 文件中有定义,其函数内容与函数xQueueCreateMutex()是类似的,只是将动态创建队列的函数替换成了静态创建队列的函数。

FreeRTOS 互斥信号量操作实验

功能设计

  1. 例程功能
    本实验主要用于学习FreeRTOS 互斥信号量操作相关API 函数的使用以及演示使用互斥信
    号量减少优先级翻转带来的影响,本实验设计了四个任务,这四个任务的功能如下表所示:
    在这里插入图片描述
    该实验的实验工程,请参考《FreeRTOS 实验例程14-4 FreeRTOS 互斥信号量操作实验》。

软件设计

  1. 程序流程图
    本实验的程序流程图,如下图所示:
    在这里插入图片描述

  2. 程序解析
    整体的代码结构,请参考2.1.6 小节,本小节着重讲解本实验相关的部分。
    (1) start_task 任务
    start_task 任务的入口函数代码如下所示:

/**
 * @brief start_task
 * @param pvParameters : 传入参数(未用到)
 * @retval 无
 */
void start_task(void * pvParameters) {
    taskENTER_CRITICAL(); /* 进入临界区*/
    /* 创建互斥信号量*/
    MutexSemaphore = xSemaphoreCreateMutex();
    /* 创建任务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);
    /* 创建任务3 */
    xTaskCreate((TaskFunction_t) task3, (const char * )
        "task3", (uint16_t) TASK3_STK_SIZE, (void * ) NULL, (UBaseType_t) TASK3_PRIO, (TaskHandle_t * ) & Task3Task_Handler);
    vTaskDelete(StartTask_Handler); /* 删除开始任务*/
    taskEXIT_CRITICAL(); /* 退出临界区*/
}

start_task 任务主要用于创建互斥信号量、task1 任务、task2 任务和task3 任务。
(2) task1 任务、task2 任务和task3 任务

/**
 * @brief task1
 * @param pvParameters : 传入参数(未用到)
 * @retval 无
 */
void task1(void * pvParameters) {
        vTaskDelay(500);
        while (1) {
            printf("task1 ready to take mutex\r\n");
            xSemaphoreTake(MutexSemaphore, portMAX_DELAY); /* 获取互斥信号量*/
            printf("task1 has taked mutex\r\n");
            printf("task1 running\r\n");
            printf("task1 give mutex\r\n");
            xSemaphoreGive(MutexSemaphore); /* 释放互斥信号量*/
            vTaskDelay(100);
        }
    }
    /**
     * @brief task2
     * @param pvParameters : 传入参数(未用到)
     * @retval 无
     */
void task2(void * pvParameters) {
        uint32_t task2_num = 0;
        vTaskDelay(200);
        while (1) {
            for (task2_num = 0; task2_num < 5; task2_num++) {
                printf("task2 running\r\n");
                delay_ms(100); /* 模拟运行,不触发任务调度*/
            }
            vTaskDelay(1000);
        }
    }
    /**
     * @brief task3
     * @param pvParameters : 传入参数(未用到)
     * @retval 无
     */
void task3(void * pvParameters) {
    uint32_t task3_num = 0;
    while (1) {
        printf("task3 ready to take mutex\r\n");
        xSemaphoreTake(MutexSemaphore, portMAX_DELAY); /* 获取互斥信号量*/
        printf("task3 has taked mutex\r\n");
        for (task3_num = 0; task3_num < 5; task3_num++) {
            printf("task3 running\r\n");
            delay_ms(100); /* 模拟运行,不触发任务调度*/
        }
        printf("task3 give mutex\r\n");
        xSemaphoreGive(MutexSemaphore); /* 释放互斥信号量*/
        vTaskDelay(100);
    }
}

以上task1 任务、task2 任务和task3 任务就是展示使用互斥信号量来减少优先级翻转带来
的影响。

下载验证

编译并下载代码,复位后可以看到LCD 屏幕上显示了本次实验的相关信息,如下图所示:
在这里插入图片描述
同时,通过串口打印了本次实验的相关信息,如下图所示:
在这里插入图片描述

  1. task3 任务获取互斥信号量并运行。
  2. task2 抢占task3 任务运行。
  3. task1 抢占task2 任务运行,task1 任务获取互斥信号量,由于此时互斥信号量被task3 持
    有,因此task1 任务获取互斥信号量失败被阻塞,同时由于互斥信号量的优先级继承机制,task3任务的任务继承了task1 任务的优先级。
  4. task3 任务继承了task1 任务的优先级,因此,此时并非轮到task2 任务运行,而是轮到
    task3 任务运行。
  5. task3 任务释放互斥信号量,同时恢复原有任务优先级,此时task1 任务获取互斥信号量,得以运行。
  6. task1 任务运行完毕,释放互斥信号量,此时task3 任务已恢复原有任务优先级,因此task2任务运行。
    本次实验的实验代码基本与第14.1 章《优先级翻转实验》的实验代码一直,只是改变了计
    数型信号量为互斥信号量,可以看出互斥信号量的优先级继承机制,减少了优先级翻转问题带来的影响。

FreeRTOS 递归互斥信号量

FreeRTOS 递归互斥信号量简介

递归互斥信号量可以看作是特殊的互斥信号量,与互斥信号量不同的是,递归互斥信号量
在被获取后,可以被其持有者重复获取,当然递归互斥信号量的持有者需要释放递归互斥信号量与之获取递归互斥信号量相同的次数,递归互斥信号量才算被释放。
递归互斥信号量与互斥信号量一样,也具备优先级继承机制,因此也不能在中断服务函数
中使用递归互斥信号量。

FreeRTOS 递归互斥信号量相关API 函数

FreeRTOS 提供了互斥信号量的一些相关操作函数,其中常用的互斥信号量相关API 函数,如下表所示:

在这里插入图片描述

  1. 函数xSemaphoreCreateRecursiveMutex()
    此函数用于使用动态方式创建递归互斥信号量,创建递归互斥信号量所需的内存,由
    FreeRTOS 从FreeRTOS 管理的堆中进行分配。该函数实际上是一个宏定义,在semphr.h 文件中有定义,具体的代码如下所示:
#define xSemaphoreCreateRecursiveMutex() \
xQueueCreateMutex(queueQUEUE_TYPE_RECURSIVE_MUTEX)

从上面的代码中可以看出,函数xSemaphoreCreateRecursiveMutex()实际上是调用了函数
xQueueCreateMutex()创建了一个递归互斥信号量,这与互斥信号量是大致相同的。
2. 函数xSemaphoreCreateRecursiveMutexStatic()
此函数用于使用静态方式创建递归互斥信号量,创建递归互斥信号量所需的内存,需要由
用户手动分配并提供。该函数实际上是一个宏定义,在semphr.h 文件中有定义,具体的代码如下所示:

#define xSemaphoreCreateRecursiveMutexStatic(pxStaticSemaphore) \
xQueueCreateMutexStatic( queueQUEUE_TYPE_RECURSIVE_MUTEX, \
pxStaticSemaphore)

从上面的代码中可以看出,函数xSemaphoreCreateRecursiveMutexStatuc()实际上是调用了函数xQueueCreateMutexStatic()创建了一个递归互斥信号量,这与互斥信号量是大致相同的。
3. 函数xSemaphoreTakeRecursive()
此函数用于获取递归互斥信号量,函数xSemaphoreTakeRecursive()实际上是一个宏定义,
在semphr.h 文件中有定义,具体的代码如下所示:

#define xSemaphoreTakeRecursive( xMutex, \
xBlockTime) \
xQueueTakeMutexRecursive( ( xMutex ), \
( xBlockTime ))

从上面的代码中可以看出,函数xSemaphoreTakeRecursive() 实际上是调用了函数
xQueueTakeMutexRecursice(),函数xQueueTakeMutexRecursive()在queue.c 文件中有定义,具体的代码如下所示:

BaseType_t xQueueTakeMutexRecursive(
    QueueHandle_t xMutex,
    TickType_t xTicksToWait) {
    BaseType_t xReturn;
    Queue_t *
        const pxMutex = (Queue_t * ) xMutex;
    configASSERT(pxMutex);
    /* 用于调试,不用理会*/
    traceTAKE_MUTEX_RECURSIVE(pxMutex);
    /* 判断当前递归互斥信号量的获取者是否为持有者*/
    if (pxMutex - > u.xSemaphore.xMutexHolder == xTaskGetCurrentTaskHandle()) {
        /* 更新递归互斥信号量的被递归获取计数器*/
        (pxMutex - > u.xSemaphore.uxRecursiveCallCount) ++;
        xReturn = pdPASS;
    }
    /* 当前递归互斥信号量的获取者不是持有者*/
    else {
        /* 获取信号量,可能发生阻塞*/
        xReturn = xQueueSemaphoreTake(pxMutex, xTicksToWait);
        /* 判断是否获取成功*/
        if (xReturn != pdFAIL) {
            /* 更新递归互斥信号量的被递归获取计数器*/
            (pxMutex - > u.xSemaphore.uxRecursiveCallCount) ++;
        }
        /* 获取失败*/
        else {
            /* 用于调试,不用理会*/
            traceTAKE_MUTEX_RECURSIVE_FAILED(pxMutex);
        }
    }
    return xReturn;
}

. 函数xSemaphoreGiveRecursive()
此函数用于释放递归互斥信号量,函数xSemaphoreGiveRecursive()实际上是一个宏定义,
在semphr.h 文件中有定义,具体的代码如下所示:

#define xSemaphoreGiveRecursive(xMutex) \
xQueueGiveMutexRecursive(( xMutex ))

从上面的代码中可以看出,函数xSemaphoreGiveRecursive() 实际上是调用了函数
xQueueGiveMutexRecursice(),函数xQueueGiveMutexRecursive()在queue.c 文件中有定义,具体的代码如下所示:

BaseType_t xQueueGiveMutexRecursive(QueueHandle_t xMutex) {
    BaseType_t xReturn;
    Queue_t *
        const pxMutex = (Queue_t * ) xMutex;
    configASSERT(pxMutex);
    /* 判断当前递归互斥信号量的释放者是否为持有者*/
    if (pxMutex - > u.xSemaphore.xMutexHolder == xTaskGetCurrentTaskHandle()) {
        /* 用于调试,不用理会*/
        traceGIVE_MUTEX_RECURSIVE(pxMutex);
        /* 更新递归互斥信号量的被递归获取计数器*/
        (pxMutex - > u.xSemaphore.uxRecursiveCallCount) --;
        /* 判断递归互斥信号量的被获取次数是否为0
         * 被获取次数为0,就要释放
         */
        if (pxMutex - > u.xSemaphore.uxRecursiveCallCount == (UBaseType_t) 0) {
            /* 释放信号量*/
            (void) xQueueGenericSend(pxMutex,
                NULL,
                queueMUTEX_GIVE_BLOCK_TIME,
                queueSEND_TO_BACK);
        }
        /* 递归互斥信号量的被获取次数不为0 */
        else {
            mtCOVERAGE_TEST_MARKER();
        }
        xReturn = pdPASS;
    }
    /* 当前递归互斥信号量的释放者不是持有者*/
    else {
        xReturn = pdFAIL;
        /* 用于调试,不用理会*/
        traceGIVE_MUTEX_RECURSIVE_FAILED(pxMutex);
    }
    return xReturn;
}

FreeRTOS 递归互斥信号量的使用示例

互斥信号量与信号量大致相同,就不专门做一个实验了,FreeRTOS 官方提供了一个简单的
示例,示例代码如下所示:

/* 定义一个信号量*/
SemaphoreHandle_t xMutex = NULL;
/* 用于创建互斥信号量的任务*/
void vATask(void * pvParameters) {
        /* 创建一个用于保护共享资源的互斥信号量*/
        xMutex = xSemaphoreCreateRecursiveMutex();
    }
    /* 用于操作互斥信号量的任务*/
void vAnotherTask(void * pvParameters) {
    /* 做一些其他事…… */
    if (xMutex != NULL) {
        /* 尝试获取互斥信号量,
         * 如果获取不到,则等待10ticks
         */
        if (xSemaphoreTakeRecursive(xMutex, (TickType_t) 10) == pdTRUE) {
            /* 成功获取到了互斥信号量,可以访问共享资源了*/
            /* 在真实的应用场景中,不会这么无意义地重复获取互斥信号量,
             * 而是为应用在更复杂的代码结构中,
             * 这里只是为了演示
             */
            xSemaphoreTakeRecursive(xMutex, (TickType_t) 10);
            xSemaphoreTakeRecursive(xMutex, (TickType_t) 10);
            /* 互斥信号量被获取了三次,
             * 因此也需要被释放三次,
             * 才能够被其他任务获取
             */
            xSemaphoreGiveRecursive(xMutex);
            xSemaphoreGiveRecursive(xMutex);
            xSemaphoreGiveRecursive(xMutex);
            /* 现在互斥信号量可以被其他任务获取了*/
        } else {
            /* 获取互斥信号量失败,
             * 因此无法安全的访问共享资源
             */
        }
    }
}

FreeRTOS 软件定时器

定时器可以说是每个MCU 都有的外设,有的MCU 自带的定时器有着十分强大的功能,
能提供PWM、输入捕获等高级功能,但是最常用的还是定时器的基础功能——定时,通过定时器的定时功能,能够完成一些需要周期性处理的事务。MCU 自带的定时器为硬件定时器,本章讲解的定时器为FreeRTOS 提供的软件定时器,软件定时器在定时器精度上肯定是不如硬件定时器的,但是软件定时器的误差范围在对于对定时器精度要求不高的周期性任务而言,都是可以接受的。并且软件定时器也有使用简单、成本低等优点。本章就来学习FreeRTOS 中软件定时器的相关内容。

FreeRTOS 软件定时器简介

软件定时器是指具有定时功能的软件,FreeRTOS 提供的软件定时器允许在创建前设置一个
软件定时器定时超时时间,在软件定时器成功创建并启动后,软件定时器开始定时,当软件定时器的定时时间达到或超过先前设置好的软件定时器定时器超时时间时,软件定时器就处于超时状态,此时软件定时器就会调用相应的回调函数,一般这个回调函数的处理的事务就是需要周期处理的事务。
FreeRTOS 提供的软件定时器还能够根据需要设置成单次定时器和周期定时器。当单次定时
器定时超时后,不会自动启动下一个周期的定时,而周期定时器在定时超时后,会自动地启动下一个周期的定时。
FreeRTOS 提供的软件定时器功能,属于FreeRTOS 的中可裁剪可配置的功能,如果要使能
软件定时器功能,那需要在FreeRTOSConfig.h 文件中将configUSE_TIMERS 配置项配置成1。
要注意的是,软件定时器的超时回调函数是由软件定时器服务任务调用的,软件定定时器
的超时回调函数本身不是任务,因此不能在该回调函数中使用可能会导致任务阻塞的API 函数,例如vTaskDelay()、vTaskDelayUntil()和一些会到时任务阻塞的等到事件函数,这些函数将会导致软件定时器服务任务阻塞,这是不可以出现的。

FreeRTOS 软件定时器服务任务简介

使能了软件定时器功能后,在调用函数vTaskStartScheduler()开启任务调度器的时候,会创
建一个用于管理软件定时器的任务,这个任务就叫做软件定时器服务任务。软件定时器服务任务,主要负责软件定时器超时的逻辑判断、调用超时软件定时器的超时回调函数以及处理软件定时器命令队列。

软件定时器命令队列

FreeRTOS 提供了许多软件定时器相关的API 函数,这些API 函数,大部分都是往定时器
的队列中写入消息(发送命令),这个队列叫做软件定时器命令队列,是提供给FreeRTOS 中的软件定时器使用的,用户是不能直接访问的。软件定时器命令队列的操作过程如下图所示:

在这里插入图片描述
上图中,左侧的代码为应用程序中用户任务的代码,而右侧的代码为软件定时器服务任务
的代码。当用户任务需要操作软件定时器时,就需要调用软件定时器相关的API 函数,例如图中调用了函数vTaskStart()启动软件定时器的定时,而函数vTaskStart()实际上会往软件定时器命令队列写入一条消息(发送命令),这条消息就包含了待操作的定时器对象以及操作的命令(启动软件定时器),软件定时器服务任务就会去读取软件定时器命令队列中的消息(接收命令),并处理这些消息(处理命令)。可以看出,用户任务并不会直接操作软件定时器对象,而是发送命令给软件定时器服务任务,软件定时器服务任务接收到命令后,根据命令内容去操作软件定时器。

软件定时器的状态

软件定时器可以处于一下两种状态中一种:

  1. 休眠态
    休眠态软件定时器可以通过其句柄被引用,但是因为没有运行,所以其定时超时回调函数
    不会被执行。
  2. 运行态
    处于运行态或在上次定时超时后再次定时超时的软件定时器,会执行其定时超时回调函数。

单次定时器和周期定时器

FreeRTOS 提供了两种软件定时器,如下:

  1. 单次定时器
    单次定时器的一旦定时超时,只会执行一次其软件定时器超时回调函数,超时后可以被手
    动重新开启,但单次定时器不会自动重新开启定时。
  2. 周期定时器
    周期定时器的一旦被开启,会在每次超时时,自动地重新启动定时器,从而周期地执行其
    软件定时器回调函数。
    单次定时器和周期定时器之间的差异如下图所示:
    在这里插入图片描述
    上图展示了单次定时器和周期定时器之间的差异,图中的垂直虚线的间隔时间为一个单位
    时间,可以理解为一个系统时钟节拍。其中Timer1 为周期定时器,定时超时时间为2 个单位时间,Timer2 为单次定时器,定时超时时间为1 个单位时间。可以看到,Timer1 在开启后,一直以2 个时间单位的时间间隔重复执行,为Timer2 则在第一个超时后就不在执行了。

软件定时器的状态转换图

单次定时器的状态转化图,如下图所示:
在这里插入图片描述
周期定时器的状态转换图,如下图所示:
在这里插入图片描述

复位软件定时器

除了开启和停止软件定时器的定时,还可以对软件定时器进行复位。复位软件定时器会使
软件定时器的重新开启定时,复位后的软件定时器以复位时的时刻作为开启时刻重新定时,软件定时器的复位示意图如下图所示:
在这里插入图片描述
上图展示了软件定时器的复位过程,图中在t0 时刻创建并启动了一个超时时间为5 个单位
时间的软件定时器,接着在t3 时刻对软件定时器进行了复位,复位后软件定时器的超时时刻以复位时刻为开启时刻重新计算,在t7 时刻又再次对软件定时器进行了复位,最终计算出软件定时器的超时时刻为最后一次复位的时刻(t7)加上软件定时器的超时时间(5 个单位时间),于是该软件定时器在t12 时刻超时,并执行其超时回调函数。

FreeRTOS 软件定时器相关配置

前面说过软件定时器功能是可选的,用户可以根据需要配置FreeRTOSConfig.h 文件中的
configUSE_TIMERS,同时FreeRTOSConfig.h 文件中还有一些其他与软件定时器相关的配置项,这部分在第三章《FreeRTOS 配置项》中都有讲解,但现在结合前面对软件定时器的了解再对这些配置项进行说明。FreeRTOSConfig.h 文件中软件定时器相关的配置项说明如下:

  1. configUSE_TIMERS
    此宏用于使能软件定时器功能,如果要使用软件定时器功能,则需要将该宏定义定义为1。
    开启软件定时器功能后,系统会系统创建软件定时器服务任务。
  2. configTIMER_TASK_PRIORITY
    此宏用于配置软件定时器服务任务的任务优先级,当使能了软件定时器功能后,需要配置
    该宏定义,此宏定义可以配置为0~(configMAX_PRIORITY-1)的任意值。
  3. configTIMER_QUEUE_LENGTH
    此宏用于配置软件定时器命令队列的队列长度,当使能了软件定时器功能后,需要配置该
    宏定义,若要正常使用软件定时器功能,此宏定义需定义成一个大于0 的值。
  4. configTIMER_TASK_STACK_DEPTH
    此宏用于配置软件定时器服务任务的栈大小,当使能了软件定时器功能后,需要配置该宏
    定义,由于所有软件定时器的定时器超时回调函数都是由软件定时器服务任务调用的,因此这些软件定时器超时回调函数运行时使用的都是软件定时器服务任务的栈。

FreeRTOS 软件定时器相关API 函数

FreeRTOS 提供了软件定时器的一些相关操作函数,其中常用的软件定时器相关API 函数,
如下表所示:
在这里插入图片描述

  1. 创建软件定时器
    FreeRTOS 提供了两种创建软件定时器的方式,分别为动态方式创建软件定时器和静态方
    式创建软件定时器,两者的区别在于静态方式创建软件定时器时,需要用户提供创建软件定时器所需的内存空间,而使用动态方式创建软件定时器时,FreeRTOS 会自动从FreeRTOS 管理的堆中分配创建软件定时器所需的内存空间。
    动态方式创建软件定时器API 函数的函数原型如下所示:
TimerHandle_t xTimerCreate(
			const char * const pcTimerName,
			const TickType_t xTimerPeriodInTicks,
			const UBaseType_t uxAutoReload,
			void * const pvTimerID,
TimerCallbackFunction_t pxCallbackFunction);

函数xTimerCreate()的形参描述,如下表所示:
在这里插入图片描述
在这里插入图片描述
函数xTimerCreate()的返回值,如下表所示:
在这里插入图片描述
静态方式创建软件定时器API 函数的函数原型如下所示:

TimerHandle_t xTimerCreateStatic(
	const char * const pcTimerName,
	const TickType_t xTimerPeriodInTicks,
	const UBaseType_t uxAutoReload,
	void * const pvTimerID,
	TimerCallbackFunction_t pxCallbackFunction,
StaticTimer_t * pxTimerBuffer);

函数xTimerCreateStatic()的形参描述,如下表所示:
在这里插入图片描述
函数xTimerCreateStatic()的返回值,如下表所示:
在这里插入图片描述
2. 开启软件定时器定时
FreeRTOS 提供了两个用于开启软件定时器定时的API 函数,这个两个函数分别用于在任
务和在中断中开启软件定时器定时。
在任务中开启软件定时器定时API 函数的函数原型如下所示:

BaseType_t xTimerStart( TimerHandle_t xTimer,
const TickType_t xTicksToWait);

函数xTimerStart()的形参描述,如下表所示:
在这里插入图片描述
函数xTimerStart()的返回值,如下表所示:
在这里插入图片描述
在中断中开启软件定时器定时API 函数的函数原型如下所示:

BaseType_t xTimerStartFromISR( TimerHandle_t xTimer,
BaseType_t * const pxHigherPriorityTaskWoken);

函数xTimerStartFromISR()的形参描述,如下表所示:
在这里插入图片描述
函数xTimerStartFromISR()的返回值,如下表所示:
在这里插入图片描述
3. 停止软件定时器定时
FreeRTOS 提供了两个用于停止软件定时器定时的API 函数,这个两个函数分别用于在任
务和在中断中停止软件定时器定时。
在任务中停止软件定时器定时API 函数的函数原型如下所示:

BaseType_t xTimerStop( TimerHandle_t xTimer,
const TickType_t xTicksToWait);

函数xTimerStop()的形参描述,如下表所示:
在这里插入图片描述
函数xTimerStop()的返回值,如下表所示:
在这里插入图片描述
在中断中停止软件定时器定时API 函数的函数原型如下所示:

BaseType_t xTimerStopFromISR( TimerHandle_t xTimer,
BaseType_t * const pxHigherPriorityTaskWoken);

函数xTimerStopFromISR()的形参描述,如下表所示:
在这里插入图片描述
函数xTimerStopFromISR()的返回值,如下表所示:
在这里插入图片描述
4. 复位软件定时器定时
FreeRTOS 提供了两个用于复位软件定时器定时的API 函数,这个两个函数分别用于在任
务和在中断中复位软件定时器定时。
在任务中复位软件定时器定时API 函数的函数原型如下所示:

BaseType_t xTimerReset( TimerHandle_t xTimer,
const TickType_t xTicksToWait);

函数xTimerReset()的形参描述,如下表所示:
在这里插入图片描述
函数xTimerReset()的返回值,如下表所示:
在这里插入图片描述
在中断中复位软件定时器定时API 函数的函数原型如下所示:

BaseType_t xTimerResetFromISR( TimerHandle_t xTimer,
BaseType_t * const pxHigherPriorityTaskWoken);

函数xTimerResetFromISR()的形参描述,如下表所示:
在这里插入图片描述
函数xTimerResetFromISR()的返回值,如下表所示:
在这里插入图片描述
5. 更改软件定时器的定时超时时间
FreeRTOS 提供了两个分别用于任务和中断的更改软件定时器的定时超时时间的API 函数。
在任务中更改软件定时器的定时超时时间API 函数的函数原型如下所示:

BaseType_t xTimerChangePeriod( TimerHandle_t xTimer,
const TickType_t xNewPeriod,
const TickType_t xTicksToWait);

函数xTimerChangePeriod()的形参描述,如下表所示:
在这里插入图片描述
在这里插入图片描述
函数xTimerChangePeriod()的返回值,如下表所示:
在这里插入图片描述
在中断中更改软件定时器的定时超时时间API 函数的函数原型如下所示:

BaseType_t xTimerChangePeriodFromISR(
TimerHandle_t xTimer,
const TickType_t xNewPeriod,
BaseType_t * const pxHigherPriorityTaskWoken);

函数xTimerChangePeriodFromISR()的形参描述,如下表所示:
在这里插入图片描述
函数xTimerChangePeriodFromISR()的返回值,如下表所示:
在这里插入图片描述
6. 删除软件定时器
FreeRTOS 提供了用于删除软件定时器的API 函数,函数原型如下所示:

BaseType_t xTimerDelete( TimerHandle_t xTimer,
const TickType_t xTicksToWait);

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

FreeRTOS 软件定时器实验

功能设计

  1. 例程功能
    本实验主要用于学习FreeRTOS 软件定时器相关API 函数的使用,本实验设计了两个任务,
    这两个任务的功能如下表所示:
    在这里插入图片描述
    该实验的实验工程,请参考《FreeRTOS 实验例程15 FreeRTOS 软件定时器实验》。

软件设计

  1. 程序流程图
    本实验的程序流程图,如下图所示:
    在这里插入图片描述
  2. 程序解析
    整体的代码结构,请参考2.1.6 小节,本小节着重讲解本实验相关的部分。
    (1) start_task 任务
    start_task 任务的入口函数代码如下所示:
/**
 * @brief start_task
 * @param pvParameters : 传入参数(未用到)
 * @retval 无
 */
void start_task(void * pvParameters) {
    taskENTER_CRITICAL(); /* 进入临界区*/
    /* 定时器1创建为周期定时器*/
    Timer1Timer_Handler = xTimerCreate(
        (const char * )
        "Timer1", /* 定时器名*/ (TickType_t) 1000, /* 定时器超时时间*/ (UBaseType_t) pdTRUE, /* 周期定时器*/ (void * ) 1, /* 定时器ID */ (TimerCallbackFunction_t) Timer1Callback); /* 定时器回调函数*/
    /* 定时器2创建为单次定时器*/
    Timer2Timer_Handler = xTimerCreate(
        (const char * )
        "Timer2", /* 定时器名*/ (TickType_t) 1000, /* 定时器超时时间*/ (UBaseType_t) pdFALSE, /* 单次定时器*/ (void * ) 2, /* 定时器ID */ (TimerCallbackFunction_t) Timer2Callback); /* 定时器回调函数*/
    /* 创建任务1 */
    xTaskCreate((TaskFunction_t) task1, (const char * )
        "task1", (uint16_t) TASK1_STK_SIZE, (void * ) NULL, (UBaseType_t) TASK1_PRIO, (TaskHandle_t * ) & Task1Task_Handler);
    vTaskDelete(StartTask_Handler); /* 删除开始任务*/
    taskEXIT_CRITICAL(); /* 退出临界区*/
}

start_task 任务主要用于创建软件定时器和task1 任务。
(2) task1 任务

/**
 * @brief task1
 * @param pvParameters : 传入参数(未用到)
 * @retval 无
 */
void task1(void * pvParameters) {
    uint8_t key = 0;
    while (1) {
        if ((Timer1Timer_Handler != NULL) && (Timer2Timer_Handler != NULL)) {
            key = key_scan(0);
            switch (key) {
                case KEY0_PRES:
                    {
                        xTimerStart(
                            (TimerHandle_t) Timer1Timer_Handler, /* 待启动的定时器句柄*/ (TickType_t) portMAX_DELAY); /* 等待系统启动定时器的最大时间*/
                        xTimerStart(
                            (TimerHandle_t) Timer2Timer_Handler, /* 待启动的定时器句柄*/ (TickType_t) portMAX_DELAY); /* 等待系统启动定时器的最大时间*/
                        break;
                    }
                case KEY1_PRES:
                    {
                        xTimerStop(
                            (TimerHandle_t) Timer1Timer_Handler, /* 待停止的定时器句柄*/ (TickType_t) portMAX_DELAY); /* 等待系统停止定时器的最大时间*/
                        xTimerStop(
                            (TimerHandle_t) Timer2Timer_Handler, /* 待停止的定时器句柄*/ (TickType_t) portMAX_DELAY); /* 等待系统停止定时器的最大时间*/
                        break;
                    }
                default:
                    {
                        break;
                    }
            }
        }
        vTaskDelay(10);
    }
}

task1 任务主要用于扫描按键,当按下按键0 时,开启所有软件定时器,当按下按键1 时,
停止所有软件定时器。
(3) 软件定时器定时超时回调函数

/**
* @brief Timer1超时回调函数
* @param xTimer : 传入参数(未用到)
* @retval 无
*/
void Timer1Callback(TimerHandle_t xTimer)
{
static uint32_t timer1_num = 0;
/* LCD区域刷新*/
lcd_fill(6, 131, 114, 313, lcd_discolor[++timer1_num % 11]);
/* 显示定时器1超时次数*/
lcd_show_xnum(79, 111, timer1_num, 3, 16, 0x80, BLUE);
}
/**
* @brief Timer2超时回调函数
* @param xTimer : 传入参数(未用到)
* @retval 无
*/
void Timer2Callback(TimerHandle_t xTimer)
{
static uint32_t timer2_num = 0;
/* LCD区域刷新*/
lcd_fill(126, 131, 233, 313, lcd_discolor[++timer2_num % 11]);
/* 显示定时器2超时次数*/
lcd_show_xnum(199, 111, timer2_num, 3, 16, 0x80, BLUE);
}

可以看到两个软件定时器的超时回调函数都是对LCD 屏幕进行区域刷新,并且会在LCD
屏幕上显示对应软件定时器的超时次数。

下载验证

编译并下载代码,复位后可以看到LCD 屏幕上显示了本次实验的相关信息,如下图所示:
在这里插入图片描述
接着按一下按键0 来启动软件定时器1 和软件定时器2,由于软件定时器1 是周期定时器,
软件定时器2 是单次定时器,因此,在开启软件定时器后,软件定时器1 会在定时超时后自动重启,而软件定时器2 只会超时一次,这么一来就能看到LCD 屏幕上软件定时器1 对应的显示区域不断刷新,并且软件定时器的超时次数也在一直增加,而软件定时器2 对应的显示区域只会刷新一次,并且软件定时器2 的超时次数一直为1。如下图所示:
在这里插入图片描述
多次按下按键0,所有软件定时器均会在,按下按键的那一刻重新开始定时。
接着按下按键1 来停止所有软件定时器,不论是周期定时器还是单次定时器,只要被停止了,就不会再继续定时,也就不会再定时超时,从按下按键1 的那一刻起,软件定时器1 和软件定时器2 便不在继续定时,LCD 屏幕也不再刷新,因此所有软件定时器都不再超时了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

行稳方能走远

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

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

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

打赏作者

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

抵扣说明:

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

余额充值