FreeRTOS笔记

FreeRTOS笔记

1.练习问题

  1. FreeRTOS的内核在哪个文件夹?有哪些是FreeRTOS移植不可缺少的文件?
    答:FreeRTOS内核的源代码通常位于FreeRTOS源代码目录中的"FreeRTOS/Source"文件夹中。在这个文件夹中,您将找到与内核本身相关的大部分源代码文件。这些文件通常包括以下一些主要文件:

    1. tasks.c:这是任务管理的核心文件,包括任务的创建、调度和切换等功能。
    2. queue.c:这是队列管理的文件,包括队列的创建、发送和接收等功能。
    3. list.c:这是双向链表数据结构的实现,用于管理任务和其他数据结构。
    4. timers.c:这是定时器服务的文件,用于管理软件定时器。
    5. event_groups.c:这是事件组的文件,用于任务间的事件通信。
    6. semphr.c:这是用于信号量管理的文件。

    这些文件是FreeRTOS内核的核心组成部分,通常是移植FreeRTOS时必不可少的文件。但是,您的具体移植可能还需要包括与硬件相关的文件,例如port.c,它包含了与特定微控制器架构相关的底层代码,以及用于配置FreeRTOS的FreeRTOSConfig.h文件。这些文件通常是根据您的目标平台和编译器来提供的。

  2. 移植FreeRTOS对所使用的基础工程有什么要求?
    答:硬件支持:您的目标平台必须支持FreeRTOS所需的硬件资源,例如时钟、中断控制器、内存等。确保目标平台的硬件符合FreeRTOS的最低要求。

    编译器和工具链:您需要使用与FreeRTOS兼容的编译器和工具链。FreeRTOS通常支持多种编译器,但您需要选择与您的目标平台和编译器版本兼容的FreeRTOS版本。

    移植层:为了将FreeRTOS移植到目标平台,您需要提供适用于该平台的移植层。这包括实现与目标硬件相关的底层功能,如任务切换、中断处理和时钟管理。移植层的核心是port.c文件,您需要根据目标平台的特定需求来实现此文件。

    配置文件:FreeRTOS提供了一个名为FreeRTOSConfig.h的配置文件,您需要根据您的应用程序需求进行配置。这个文件包括任务堆栈大小、空闲任务优先级、最大任务数等参数的设置。确保配置文件正确地反映了您的应用程序要求。

    驱动程序和外设支持:如果您的应用程序需要与外部硬件或外设进行交互,您需要编写相应的驱动程序或库,并确保这些驱动程序与FreeRTOS协同工作。

    内存管理:FreeRTOS需要一些内存来管理任务堆栈、队列、信号量等数据结构。您需要分配足够的内存,并为FreeRTOS提供内存分配函数,以便它可以在运行时动态分配和释放内存。

    中断处理:您需要配置中断控制器以确保FreeRTOS的中断处理机制正常工作。此外,您还需要确保中断处理程序正确地与FreeRTOS任务调度器进行交互。

    启动文件和链接脚本:确保编译工具使用正确的启动文件和链接脚本,以确保生成的可执行文件正确加载到目标硬件上。

    调试工具:在移植和开发过程中,使用合适的调试工具来调试和测试您的应用程序。这包括使用调试器、示波器和仿真器等工具来解决问题。

  3. 简述FreeRTOS移植步骤和注意事项
    答:1. 使用STM32CubeMX快速建立工程
    2. 下载FreeRTOS源码
    3. 在工程文件夹下新建“FreeRTOS”,将源码文件中的Source文件夹全部复制到工程文件夹”FreeROTS“中。
    4. 对“Source”中的“Portable”进行配置,删除除了“keil”“MemMang”“RVDS”的所有文件。
    5. 打开工程,新建两个组“FreeRTOS/Source”,“FreeRTOS/Portable”,添加文件,“Source”添加文件夹“FreeRTOS”中7个.c,“Portable”添加RVDS\ARM\ARM_CM4F的“port.c”,还有MemMang的“heap_4.c”文件,
    6. 配置头文件和路径,.\FreeRTOS\include.\FreeRTOS\portable\RVDS\ARM_CM4F
    7. 配置"FreeRTOSConfig.h"文件,可以在源码文件的Demo中找到。
    8. 删除"stm32f4xxit.c"的SysTick,PendSV,SVC三个中的服务
    注意事项:1. 注意建立工程时,不要使用Sys定时器,裸机开发的时钟源默认是Systick,使用FreeRTOS后,FreeRTOS会占用Systick(用来生成1ms的时间片,用于时间片调度),所以需要另外的时钟源。2. 建议配置USART,便于串口打印。

2.FreeRTOS裁剪与配置

在FreeRTOSconfig.h文件中。

基础配置

  1. configUSE_PREEMPTION:用来定义内核调度方式,宏为1时,使用抢占式,0时,使用合作式,默认1.
  2. configUSE_PORT_OPTIMISED_TASK_SELECION(端口优化任务选择):在FreeRTOS的任务切换时,它用来设置查找下一个要运行的任务方法,宏为1,使用硬件查找,运行速度快,但有优先级数量限制,当宏为0,使用通用方法查找,也就是纯软件方法,没有优先级数量限制,但比硬件查找慢。STM32有一个计算前导指令,支持硬件查找,此宏设置为1.
  3. configCPU_CLOCK_HZ:用于设置系统时钟,单位为Hz,STM32可使用SystemCoreClock这个全局变量来设置系统时钟频率
  4. configTICK_RATE_HZ:用于定义系统时钟节拍,此频率就是嘀嗒定时器的中断频率,其倒数是一个时间片的长度。过高的系统时钟节拍,会使CPU花在任务切换的时间过多,导致性能下降。
  5. configMAX_PRIORITIES(最大优先级):用来定义最大优先级值。freeRTOS本身没有优先级数量限制,但是优先级数量越多,消耗资源越大。建议设置到0-31.
  6. configMINIMAL_STACK_SIZE:用来定义空闲任务使用堆栈大小,单位是word,即4b
  7. configTOTAL_HEAP_SIZE:用来定义FreeRTOS管理的堆大小。如果使用了动态内存管理,则FreeRTOS在创建任务通知、心信号量、队列时会从这个堆内存中分配内存。堆内存其实就是定义好的数组,ucHeap[configTOTAL_HEAP_SIZE],类型为uint8_t,大小为configTOTAL_HEAP_SIZE
  8. configUSE_!^_BIT_TICKS:为系统时钟节拍计数器数据类型,用来定义TickType_t是哪种数据类型的。若为1,则TickType为16位无符号数,为0,则为32位无符号数。对于8位或16位架构处理器,此宏应为1;对于32位,则设置0.此宏还会影响任务最大阻塞时间,阻塞时间为poetMAX_DELAY为无限期阻塞。
  9. configIDLE_SHOULD_YIELD:用于实现空闲任务与空闲任务同优先级的任务之间的切换。只有设置为抢占式调度方法,且创建了与空闲任务同优先级的任务时,此宏才起作用。宏为1,表示不等空闲任务时间片用完,就可立即从空闲任务切换到与空闲任务同优先级的任务。
  10. configMAX_TASK_NAME_LEN:定义任务名最长的字符串大小,字符串的’\0’也考虑。此宏越大,消耗资源越大。
  11. configUSE_TICKLESS_IDLE:用于低功耗tickless模式,宏为1,启用,宏为0,不启用。
关于队列、信号量、任务通知

configUSE_QUEUE_SETS用于定义是否使用队列集,configUSE_MUTEXES用于定义是否使用互斥信号量,configUSE_RECURSIVE_MUTEXES用于定义是否使用递归互斥信号量,configUSE_COUTING_SEMAPHORES用于定义是否使用计数信号量,configUSE_TASK_NOTIFICATIONS用于定义是否使用任务通知。当这些宏为1时,相关功能的API就会被编译。

关于钩子函数

configUSE_IDLE_HOOK用于使能空闲任务钩子函数,configUSE_TICK_HOOK用于使能时间片钩子函数,configUSE_MALLOC_FAILED_HOOK用于使能动态内存分配失败钩子函数,configCHECK_FOR_STACK_OVERFLOW:用于使能堆栈溢出检测钩子函数。当对应宏不为0时,必须提供相应的钩子函数。

FreeRTOS中断配置

ARM Cortex-M内核采用8位来表示可编程的中断优先级,共有256个中断优先级。

configPRIO_BITS

用于设置硬件表达中断优先级的二进制位数。stm32使用了4位,因此宏为4.因为在core_cm4.h头文件中定义了表达中断优先级位数据的宏_NVIC_PRIO_BITS,并且已经将宏设4。

configLIBPARY_LOWSET_INTERRUPT_PRIOPRITY

用于设置最低中断优先级。stm32使用4位二进制表达中断优先级,最多设置16个。中断优先级值越大,优先级越低。0表示最高中断优先级。

configKERNEL_INTERRUPT_PRIOPRITY

用来设置内核中断优先级,即嘀嗒定时器和pendSV的中断优先级。此宏的定义通过configLIBRARY_LOWSET_INTERRUPT_PRIOPRITY<<(8-configPRIO_BITS)替换得到。

configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY

用于设置FreeRTOS可替换的最高中断优先级。可以在硬件中断中任意设置,如设置为5,表示优先级高于5的中断不归FreeRTOS管理。

configMAX_SYSCALL_INTERRUPT_PRIORITY

configMAX_SYSCALL_INTERRUPT_PRIORITY的定义通过configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY <<(8-configPRIO_BITS)替换得到。例如configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY设置为5,表示优先级高于5的中断不归FreeRTOS管理。

可选的API函数配置

通过配置以INCLUDE_开始的宏,可以对FreeRTOS的一些可选API函数进行配置。宏为1,使能

#define INCLUDE_vTaskPrioritySet		1	/*任务优先级设置*/
#define INCLUDE_uxTaskPriorityGet		1	/*任务优先级获取*/
#define INCLUDE_vTaskDelete				1	/*任务删除*/
#define INCLUDE_vTaskSuspend			1	/*任务挂起*/
#define INCLUDE_vTaskDelayUntil			1	/*绝对延时阻塞*/
#define INCLUDE_vTaskDelay				1	/*相对延时阻塞*/
#define INCLUDE_xTaskGetSchedulerState	 1	/*获取调度器状态*/
#define INCLUDE_xTaskGetCurrentTaskHandle	1	/*获取当前任务句柄*/
#define INCLUDE_uxTaskGetStackHighWaterMark		1	/*获取任务堆栈高水位线*/
#define INCLUDE_xTaskGetIdleTaskHandle		1	/*获取软件定时器服务句柄*/
#define INCLUDE_pcTaskGetTaskName		1	/*获取任务名*/

其他配置

协程相关
configUSE_CO_ROUTINES

用于定义是否使用协程。宏为0,不使用。在抢占式调度方式中也可使用协程,使用协程可节约开销,但功能将收到限制。

configMAX_CO_ROUTINE_PRIORITIES

用于定义协程的有效优先级数量,优先级从0到configMAX_CO_ROUTINE_PRIORITIES-1, 0为最低优先级。

任务运行相关
configUSE_TRACE_FACILTY

用于使能可视化跟踪和调试。宏为1,使能。

configUSE_STATS_FORMATTING_FUNCTIONS

当它与configUSE_TRACE_FACILTY勇士为1时,vTaskList()和vTaskGetRunTimeStats()两个函数会参与编译,从而可以通过两个函数获取任务运行信息。

configGENERATE_RUN_TIME_STATS

用于定义是否开启时间统计功能。宏为1,使能。此宏为1,还需要开启两个宏,portCONFIGURE_TIMER_FOR_RUN_TIME_STATS()和portGET_RUN_TIME_COUNTER_VALUE(),前者对应与一个精度是嘀嗒定时器时间精度10倍以上的定时器初始化函数,后者对应于一个获取统计时间值的函数,这两个函数均需要用户打开。

软件定时相关
configUSE_TIMERS

是用于定义是否使用软件定时器的相关功能。为1,打开。当它打开时,调度器开启函数中自动创建软件定时器服务任务,同时需要配置后面的几个宏。

configTIMER_TASK_PRIORITY

用于定义软件定时器服务任务的默认优先级

configTIMER_QUEUE_LENGTH

用于定义软件定时器命令队列长度

configTIMER_TASK_STACK_DEPTH

用于定义软件定时器服务堆栈大小

断言

用于在调试阶段检查传入参数是否合理,调用configASSERT(x),x=0,说明有错误发生。可以使用串口打印,也可以使用停机。

如:

#define configASSERT( x ) if(( x ) == 0)
{
	taskDISABLE_INTERRUPT();
	for(;;);
}
中断服务函数

在FreeRTOS移植层的port.c文件中,分别定义了vPortSVCHandler、xPortSVHandler和xPortSysTickHandle这三个中断服务函数。这三个函数用来实现SVC中断,PendSV中断,嘀嗒定时器中断。

经过下面的宏定义可以进行编译替换

#define vPortSVCHandler	SVC_Handler
#define xPortSVHandler	PendSV_Handle
#define xPortSysTickHandle	SysTick_Handler

FreeRTOSConfig.h头文件配置参考

#ifndef FREERTOS_CONFIG_H
#define FREERTOS_CONFIG_H

#include "sys.h"
#include "usart.h"
//针对不同的编译器调用不同的stdint.h文件
#if defined(__ICCARM__) || defined(__CC_ARM) || defined(__GNUC__)
    #include <stdint.h>
    extern uint32_t SystemCoreClock;
#endif

//断言
#define vAssertCalled(char,int) printf("Error:%s,%d\r\n",char,int)
#define configASSERT(x) if((x)==0) vAssertCalled(__FILE__,__LINE__)

/***************************************************************************************************************/
/*                                        FreeRTOS基础配置配置选项                                              */
/***************************************************************************************************************/
#define configUSE_PREEMPTION                    1                       //1使用抢占式内核,0使用协程
#define configUSE_TIME_SLICING                    1                        //1使能时间片调度(默认式使能的)
#define configUSE_PORT_OPTIMISED_TASK_SELECTION    1                       //1启用特殊方法来选择下一个要运行的任务
                                                                        //一般是硬件计算前导零指令,如果所使用的
                                                                        //MCU没有这些硬件指令的话此宏应该设置为0!
#define configUSE_TICKLESS_IDLE                    0                       //1启用低功耗tickless模式
#define configUSE_QUEUE_SETS                    1                       //为1时启用队列
#define configCPU_CLOCK_HZ                        (SystemCoreClock)       //CPU频率
#define configTICK_RATE_HZ                        (1000)                  //时钟节拍频率,这里设置为1000,周期就是1ms
#define configMAX_PRIORITIES                    (32)                    //可使用的最大优先级
#define configMINIMAL_STACK_SIZE                ((unsigned short)130)   //空闲任务使用的堆栈大小
#define configMAX_TASK_NAME_LEN                    (16)                    //任务名字字符串长度

#define configUSE_16_BIT_TICKS                    0                       //系统节拍计数器变量数据类型,
                                                                        //1表示为16位无符号整形,0表示为32位无符号整形
#define configIDLE_SHOULD_YIELD                    1                       //为1时空闲任务放弃CPU使用权给其他同优先级的用户任务
#define configUSE_TASK_NOTIFICATIONS            1                       //为1时开启任务通知功能,默认开启
#define configUSE_MUTEXES                        1                       //为1时使用互斥信号量
#define configQUEUE_REGISTRY_SIZE                8                       //不为0时表示启用队列记录,具体的值是可以
                                                                        //记录的队列和信号量最大数目。
#define configCHECK_FOR_STACK_OVERFLOW            0                       //大于0时启用堆栈溢出检测功能,如果使用此功能
                                                                        //用户必须提供一个栈溢出钩子函数,如果使用的话
                                                                        //此值可以为1或者2,因为有两种栈溢出检测方法。
#define configUSE_RECURSIVE_MUTEXES                1                       //为1时使用递归互斥信号量
#define configUSE_MALLOC_FAILED_HOOK            0                       //1使用内存申请失败钩子函数
#define configUSE_APPLICATION_TASK_TAG            0                       
#define configUSE_COUNTING_SEMAPHORES            1                       //为1时使用计数信号量

/***************************************************************************************************************/
/*                                FreeRTOS与内存申请有关配置选项                                                */
/***************************************************************************************************************/
#define configSUPPORT_DYNAMIC_ALLOCATION        1                       //支持动态内存申请
#define configTOTAL_HEAP_SIZE                    ((size_t)(20*1024))     //系统所有总的堆大小

/***************************************************************************************************************/
/*                                FreeRTOS与钩子函数有关的配置选项                                              */
/***************************************************************************************************************/
#define configUSE_IDLE_HOOK                        0                       //1,使用空闲钩子;0,不使用
#define configUSE_TICK_HOOK                        0                       //1,使用时间片钩子;0,不使用

/***************************************************************************************************************/
/*                                FreeRTOS与运行时间和任务状态收集有关的配置选项                                 */
/***************************************************************************************************************/
#define configGENERATE_RUN_TIME_STATS            0                       //为1时启用运行时间统计功能
#define configUSE_TRACE_FACILITY                1                       //为1启用可视化跟踪调试
#define configUSE_STATS_FORMATTING_FUNCTIONS    1                       //与宏configUSE_TRACE_FACILITY同时为1时会编译下面3个函数
                                                                        //prvWriteNameToBuffer(),vTaskList(),
                                                                        //vTaskGetRunTimeStats()
                                                                        
/***************************************************************************************************************/
/*                                FreeRTOS与协程有关的配置选项                                                  */
/***************************************************************************************************************/
#define configUSE_CO_ROUTINES                     0                       //为1时启用协程,启用协程以后必须添加文件croutine.c
#define configMAX_CO_ROUTINE_PRIORITIES         ( 2 )                   //协程的有效优先级数目

/***************************************************************************************************************/
/*                                FreeRTOS与软件定时器有关的配置选项                                            */
/***************************************************************************************************************/
#define configUSE_TIMERS                        1                               //为1时启用软件定时器
#define configTIMER_TASK_PRIORITY                (configMAX_PRIORITIES-1)        //软件定时器优先级
#define configTIMER_QUEUE_LENGTH                5                               //软件定时器队列长度
#define configTIMER_TASK_STACK_DEPTH            (configMINIMAL_STACK_SIZE*2)    //软件定时器任务堆栈大小

/***************************************************************************************************************/
/*                                FreeRTOS可选函数配置选项                                                      */
/***************************************************************************************************************/
#define INCLUDE_xTaskGetSchedulerState          1                       
#define INCLUDE_vTaskPrioritySet                1
#define INCLUDE_uxTaskPriorityGet                1
#define INCLUDE_vTaskDelete                        1
#define INCLUDE_vTaskCleanUpResources            1
#define INCLUDE_vTaskSuspend                    1
#define INCLUDE_vTaskDelayUntil                    1
#define INCLUDE_vTaskDelay                        1
#define INCLUDE_eTaskGetState                    1
#define INCLUDE_xTimerPendFunctionCall            1

/***************************************************************************************************************/
/*                                FreeRTOS与中断有关的配置选项                                                  */
/***************************************************************************************************************/
#ifdef __NVIC_PRIO_BITS
    #define configPRIO_BITS               __NVIC_PRIO_BITS
#else
    #define configPRIO_BITS               4                  
#endif

#define configLIBRARY_LOWEST_INTERRUPT_PRIORITY            15                      //中断最低优先级
#define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY    5                       //系统可管理的最高中断优先级
#define configKERNEL_INTERRUPT_PRIORITY         ( configLIBRARY_LOWEST_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) )
#define configMAX_SYSCALL_INTERRUPT_PRIORITY     ( configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) )

/***************************************************************************************************************/
/*                                FreeRTOS与中断服务函数有关的配置选项                                          */
/***************************************************************************************************************/
#define xPortPendSVHandler     PendSV_Handler
#define vPortSVCHandler     SVC_Handler

#endif /* FREERTOS_CONFIG_H */

2.练习问题

1. 若设计的系统需要用到多达40个任务优先级,要修改和设置哪些宏?如何配置?

  1. 配置任务优先级数量

    打开FreeRTOS的配置文件 FreeRTOSConfig.h 并找到以下宏定义:

    #define configMAX_PRIORITIES    (32)
    

    configMAX_PRIORITIES 的值更改为您所需的任务优先级数量,例如40:

    #define configMAX_PRIORITIES    (40)
    
  2. 分配任务优先级

    现在,您可以为任务分配任务优先级。任务的优先级是一个从0到(configMAX_PRIORITIES-1)的整数。较小的数字表示较高的优先级。

    例如,要为任务分配优先级,您可以在任务创建函数 xTaskCreate 中指定任务的优先级,如下所示:

    xTaskCreate(TaskFunction_t pvTaskCode, const char * const pcName, const uint16_t usStackDepth, void * const pvParameters, UBaseType_t uxPriority, TaskHandle_t * const pxCreatedTask);
    

    其中,uxPriority 参数是任务的优先级。您可以将其设置为0到(configMAX_PRIORITIES-1)之间的整数,以确定任务的优先级。

2.若想用vTaskList()函数获取任务运行信息,则应修改和设置哪些宏?如何配置?

  1. 启用任务列表功能

    打开FreeRTOS的配置文件 FreeRTOSConfig.h 并找到以下宏定义:

    #define configUSE_TRACE_FACILITY    0
    

    configUSE_TRACE_FACILITY 的值更改为1以启用任务列表功能:

    #define configUSE_TRACE_FACILITY    1
    

    确保 configUSE_STATS_FORMATTING_FUNCTIONS 宏也启用。通常,这是默认启用的,但您可以检查一下:

    #define configUSE_STATS_FORMATTING_FUNCTIONS    1
    
  2. 包含头文件

    在您的代码中包含以下头文件以使用vTaskList()函数:

    #include "task.h"
    
  3. 调用vTaskList()函数

    您可以在需要的地方调用vTaskList()函数以获取任务列表。例如:

    codechar *pcWriteBuffer;
    vTaskList(pcWriteBuffer);
    

    pcWriteBuffer 将包含任务列表的字符串。您可以通过串口或其他适当的方式将其输出到控制台或记录下来,以查看任务的运行信息。

3. 若要启动软件定时器功能,则应修改和设置哪些宏?如何配置?

  1. 启用软件定时器功能

    打开FreeRTOS的配置文件 FreeRTOSConfig.h 并找到以下宏定义:

    #define configUSE_TIMERS     0
    

    configUSE_TIMERS 的值更改为1以启用软件定时器功能:

    #define configUSE_TIMERS     1
    
  2. 选择软件定时器实现方式

    在同一配置文件中,您可以选择使用标准软件定时器实现方式或POSIX定时器实现方式。这取决于您的需求。以下是两个选项:

    a. 标准软件定时器(默认选项):

    #define configUSE_POSIX_ERRNO 1
    
    
    // 这将启用标准的FreeRTOS软件定时器功能。一般情况下,您可以使用默认选项。
    

    b. POSIX定时器

    #define configUSE_POSIX_ERRNO 0
    
     // 如果您想要使用POSIX定时器API,则将 `configUSE_POSIX_ERRNO` 设置为0,并根据需要配置`configTIMER_TASK_PRIORITY` 和 `configTIMER_QUEUE_LENGTH` 以及其他相关的POSIX定时器选项。
    
  3. 包含头文件

    在您的代码中包含以下头文件以使用软件定时器:

    #include "FreeRTOS.h"
    #include "timers.h"
    
  4. 创建和启动软件定时器

    您可以使用xTimerCreate()函数创建定时器,然后使用xTimerStart()函数启动定时器。例如:

    TimerHandle_t xTimer = xTimerCreate("MyTimer", pdMS_TO_TICKS(1000), pdTRUE, NULL, vTimerCallback);
    if (xTimer != NULL) {
        if (xTimerStart(xTimer, 0) != pdPASS) {
            // 定时器启动失败
        }
    }
    

    在上述示例中,vTimerCallback 是您定义的定时器回调函数。

  5. 处理定时器事件

    在您的回调函数 vTimerCallback 中处理定时器事件。当定时器超时时,此函数将被调用。

  6. 其他配置项

    根据您的应用程序需求,配置其他FreeRTOS选项,例如内存管理、任务堆栈大小、优先级等。

3.任务基础

任务的状态

  1. 运行态
  2. 就绪态
  3. 阻塞态
  4. 挂起态

任务的优先级

当多个任务的优先级相同时,FreeRTOS将采用时间片调度方式,前提是在配置文件中进行了配置。

任务堆栈

堆栈就是指存储器中按照“后进先出”的原则。

任务堆栈是在创建任务时自动创建或指定。对于动态任务,任务堆栈由xTaskCreat()自动创建,但需要传入指定的堆栈大小的参数。

/*
* 创建一个新任务
*
* xTaskCreate()函数用于在FreeRTOS中创建一个新任务
*
* 参数:
*   pvTaskCode: 任务函数,任务将在这个函数中执行 
*   pcName: 任务名称,字符串
*   usStackDepth: 任务栈的大小(单位words,取决于平台)
*   pvParameters: 传给任务函数的参数
*   uxPriority: 任务优先级,数值越大优先级越高  
*
* 返回:
*   pdPASS: 任务创建成功
*   errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY: 内存不足
*/

BaseType_t xTaskCreate( 
    TaskFunction_t pvTaskCode,
    const char * const pcName, 
    uint16_t usStackDepth,
    void *pvParameters,
    UBaseType_t uxPriority,
    TaskHandle_t *pvCreatedTask
);

如果采用静态任务创建函数xTaskCreateStatic(),就要需要用户自定义任务堆栈,然后将任务堆栈首地址作为实参穿软水xTaskCreateStatic()函数

/**
 * 在 FreeRTOS 中用静态分配的栈创建任务
 * 
 * @param pvTaskCode 任务入口函数
 * @param pcName 任务名称字符串 
 * @param usStackDepth 任务栈大小(单位为字)
 * @param pvParameters 传递给任务函数的参数  
 * @param uxPriority 任务优先级,数字越大优先级越高
 * @param puxStackBuffer 任务栈的缓冲区数组
 * @param pxTaskParameter 任务参数结构体,包含栈缓冲区和函数参数
 * @return xTaskHandle 任务句柄,创建成功则返回有效句柄
 */
TaskHandle_t xTaskCreateStatic(  
    TaskFunction_t pvTaskCode,
    const char * const pcName,
    uint32_t usStackDepth,
    void *pvParameters, 
    UBaseType_t uxPriority,
    StackType_t *puxStackBuffer, 
    StaticTask_t *pxTaskParameter
);

/**
 * 任务参数结构体定义
 */
typedef struct {
  StackType_t *pxStackBuffer;  // 任务栈缓冲区指针
  void *pvParameters;          // 传递给任务函数的参数
} StaticTask_t;

任务堆栈的类型为StackType_t,在portmacro.h中定义,实践课程为uint32_t,即任务堆栈大小为word(4B)。

任务控制块

FreeRTOS用来记录任务堆栈指针、当前状态、任务优先级等一些与任务相关的属性叫做任务控制块(TCB)。

任务控制块靠一个结构体类型TCB_t实现。在task.c中定义。

typedef struct tskTaskControlBlock // 使用旧的命名约定,防止破坏内核调试
{

  volatile StackType_t pxTopOfStack; // 指向任务栈中最后一个入栈项的位置,必须是TCB的第一个成员

  #if ( portUSING_MPU_WRAPPERS == 1 )
    xMPU_SETTINGS xMPUSettings; // MPU设置是定义在端口层的一部分,必须是TCB的第二个成员
  #endif

  ListItem_t xStateListItem; // 状态链表项,指向一个链表,该链表反映任务的状态(就绪、阻塞、挂起)

  ListItem_t xEventListItem; // 用于从事件链表引用一个任务,即事件列表项

  UBaseType_t uxPriority; // 任务优先级,0是最低优先级

  StackType_t pxStack; // 指向任务栈的起始地址

  char pcTaskName[configMAX_TASK_NAME_LEN]; // 创建任务时赋予的描述性名称,用于调试

  #if ( ( portSTACK_GROWTH > 0 ) || ( configRECORD_STACK_HIGH_ADDRESS == 1 ) )  
    StackType_t pxEndOfStack; // 指向任务栈的最高有效地址
  #endif

  #if ( portCRITICAL_NESTING_IN_TCB == 1 )
    UBaseType_t uxCriticalNesting; // 在TCB中保存关键抢占计数,用于没有自己的计数的端口
  #endif

  #if ( configUSE_TRACE_FACILITY == 1 )
    UBaseType_t uxTCBNumber; // 在每次创建TCB时自增,允许调试器判断任务是否被删除和重新创建
    UBaseType_t uxTaskNumber; // 专门用于第三方跟踪代码的编号
  #endif

  #if ( configUSE_MUTEXES == 1 )
    UBaseType_t uxBasePriority; // 最近一次被分配给该任务的优先级,用于优先级继承机制
    UBaseType_t uxMutexesHeld; // 该任务持有的互斥锁数
  #endif

  #if ( configUSE_APPLICATION_TASK_TAG == 1 )
    TaskHookFunction_t pxTaskTag; // 任务标签钩子函数
  #endif

  #if ( configNUM_THREAD_LOCAL_STORAGE_POINTERS > 0 )
    void *pvThreadLocalStoragePointers[configNUM_THREAD_LOCAL_STORAGE_POINTERS]; // 线程本地存储指针
  #endif

  #if ( configGENERATE_RUN_TIME_STATS == 1 )  
    configRUN_TIME_COUNTER_TYPE ulRunTimeCounter; // 存储任务在Running状态所消耗的时间
  #endif

  #if ( ( configUSE_NEWLIB_REENTRANT == 1 ) || ( configUSE_C_RUNTIME_TLS_SUPPORT == 1 ) )
    configTLS_BLOCK_TYPE xTLSBlock; // 任务的线程本地存储块
  #endif

  #if ( configUSE_TASK_NOTIFICATIONS == 1 )
    volatile uint32_t ulNotifiedValue[configTASK_NOTIFICATION_ARRAY_ENTRIES]; // 通知值数组
    volatile uint8_t ucNotifyState[configTASK_NOTIFICATION_ARRAY_ENTRIES]; // 通知状态数组
  #endif

  #if ( tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE != 0 )
    uint8_t ucStaticallyAllocated; // 如果任务是静态分配的,设置为pdTRUE以防止尝试释放内存
  #endif

  #if ( INCLUDE_xTaskAbortDelay == 1 ) 
    uint8_t ucDelayAborted; // 用于中止延时的标志
  #endif

  #if ( configUSE_POSIX_ERRNO == 1 )
    int iTaskErrno; // 每个任务的errno
  #endif
  
} tskTCB;

任务控制块在创建任务时初始化,若任务创建不成功,则任务控制块中的pxTopOfStack指向任务堆栈栈顶,该指针随着出入栈操作不断更新,而pxStack指向任务堆栈首地址。FreeRTOS使用列表来处理就绪、挂起、延时等任务,通过任务控制块中的任务状态列表项xStateListItem和事件列表项xEventListItem挂接不同的列表来实现。

比如:当某个任务处于就绪状态时,调度器就会将任务状态列表挂接到就绪列表中。事件列表表项与之类似,如当在队列列满的情况下任务因入队操作而阻塞时,调度器就会将事件列表项挂接到队列的等待入队列表。

列表和列表项

列表与列表项是FreeRTOS中定义的数据结构。FreeRTOS列表使用指针指向列表项。一个定义列表下面可能有多个列表项,每个列表都有一个指针指向列表。定义与代码实现在list.c与list.h中。

列表

列表用于跟踪任务。处于就绪、挂起、阻塞态的任务都会被挂接到格子的列表中。列表类型名为List_t,定义如下:

typedef struct xList
{
	listFIRST_LIST_INTEGITY_CHECK_VALUE			/*完整性检查*/
	volatile UBaseType_t uxNumberOfItems;		/*记录列表项数量*/
    ListItem_t *configLIST_VOLATILE pxIndex;	/*指向某个列表项的指针*/
    MiniListItem_t xListEnd;				/*mini列表项*/
    listSECOND_LIST_INTEGITY_CHECK_VALUE	/*完整性检查*/
} List_t;

列表结构体中的第一个与最后一个成员用来检查列表完整性。在配置文件中configUSE_LIST_DATA_INTEGITY_BYTES设置为1时才会开启完整性检查,默认不开启。

列表项

列表项是用来存放在列表中的具体项目。

struct xLIST_ITEM
{
    listFIRST_LIST_ITEM_INTEGITY_CHECK_VALUE	/*完整性检查*/
    configLIST_VOLATILE TickType_t xItemValue;	/*列表项值*/
    struct xLIST_ITEM *configLIST_VOLATILE pxNext;	/*指向下一个列表项*/
    struct xLIST_ITEM *configLIST_VOLATILE pxPrevious;	/*指向上一个列表项*/
    void *pvOwner;		/*记录列表项属于那个任务控制块*/
    struct xLIST *configLIST_VOLATILE pxContainer;	/*记录列表项属于哪一个列表*/
    listSECOND_LIST_ITEM_INTEFRITY_CHECK_VALUE	/*完整性检查*/
};
typedef strtuct xLIOST_ITEM ListItem_t;	/*取别名*/

xItemValue用来记录列表项值,当在列表中插入列表时,按列表项值从小到大的顺序插入;

同时FreeRTOS提供mini列表项

struct xLIST_ITEM
{
    listFIRST_LIST_ITEM_INTEGITY_CHECK_VALUE	/*完整性检查*/
    configLIST_VOLATILE TickType_t xItemValue;	/*列表项值*/
    struct xLIST_ITEM *configLIST_VOLATILE pxNext;	/*指向下一个列表项*/
    struct xLIST_ITEM *configLIST_VOLATILE pxPrevious;	/*指向上一个列表项*/
};
typedef strtuct xLIOST_ITEM ListItem_t;	/*取别名*/

FreeRTOS使用不同的列表来标识任务的不同状态。

比如:用一个就绪列表来跟踪所有已经准备好运行的任务,因为每个优先级都有可能有就绪任务,FreeRTOS使用就绪列表数组来实现所有优先级就绪任务管理

static List_t pxReadyTasksLists[ configMAX_PRIORITIES ]

pxReadyTasksLists[0]是所有准备好的优先级为0的任务列表。以此类推,直到pxReadyTasksLists[configMAX_PRIORITIES - 1]。

每个优先级的任务就绪列表下挂接不同任务的列表项,其中列表项成员pvOwner指向所属任务的任务控制块,当前任务的任务块保存在局部变量pxCurrentTCB中,它们之间的关系如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

任务创建和删除

任务管理是FreeRTOS核心功能。涉及任务创建、任务删除、任务挂起、任务恢复和任务调度等。任务句柄用来识别一个任务,其类型名为TaskHandle_t。

任务函数
static void TaskFunction(void *pvParameters)
{
    while(1)		/* FreeRTOS是一个死循环*/
    {
        ...
    }
}

FreeRTOS任务不允许以任何方式从实现函数中返回,即不能有一条return语句,也不能执行到函数末尾。如果一个函数不再被需要,可以显式地将其删除。

void TaskFunction(void *pvParameters)
{
    int iVariableEXample = 0;
    /*可以像普通函数一样定义变量。用这个函数创建的每个任务实例都有一个属于自己的iVariableExample变量。单iVariableExample被定义为static时除外,所有的任务实例将共享种鸽变量*/
    while(1)
    {
        。。。
    }
    /*若任务的具体实现会跳出上面的死循环,则此任务必须在函数运行玩之前进行删除。传入NULL参数表示删除的是当前版本*/
    vTaskDelete(NULL);
}
任务创建和删除函数

FreeRTOS提供的任务创建函数有4个,以static结尾表示在创建任务时使用静态内存分配方法,带有Restricted限定词表示使用MPU进行保护限制。

  1. xTaskCreate()

    最常用的任务创建函数,动态分配任务堆栈,支持传入任务参数。

  2. xTaskCreateStatic()

    使用静态分配的任务堆栈创建任务,节省动态内存使用。

  3. xTaskCreateRestricted()

    创建受限制的任务,只能访问限定的资源,增加安全性。

  4. xTaskCreateRestrictedStatic()

    结合静态堆栈分配和访问限制的任务创建。

这4个函数主要的区别在于:

  • 堆栈分配机制:动态分配或静态分配
  • 是否添加访问限制

动态分配灵活方便,静态分配可以节省内存并提高实时性。

受限制的任务访问更安全,但功能较受限。

所以在使用时,需要根据实际需求选择合适的任务创建函数,以获取最佳效果。

xTaskCreate()函数最通用,xTaskCreateStatic()在内存受限的系统中很有用。

受限任务适用于对安全性要求极高的场景。

用动态内存分配方法创建任务

xTaskCreate()用来创建任务并将它将入就绪任务。

使用这个函数需要在FreeRTOSConfig.h中将configSUPPORT_DYNAMICALLOCATION设置1,默认1.

BaseType_t xTaskCreate( /* 返回任务创建结果 */
       TaskFunction_t pvTaskCode, /* 任务入口函数 */
       const char * const pcName, /* 任务名称 */
       const uint16_t usStackDepth, /* 堆栈大小 */ 
       void * const pvParameters, /* 任务函数参数 */
       UBaseType_t uxPriority, /* 任务优先级 */
       TaskHandle_t * const pvCreatedTask ) /* 用于返回创建的任务句柄 */
}

如果任务创建成功,则返回pdPASS(一个定义为1的宏);如果创建失败返回errCOULD_NOT_ALLOWCATE_REQUIRED_MEMORY,原因是堆内存不足,不能进行动态内存分配。

使用例子:

// LED闪烁任务函数
void vLEDTask(void *pvParameters)
{
  bool LedState = false;
    
  while(1) {
    // toggle LED 
    LedState = !LedState;
    // set LED according to state
    SetLED(LedState);

    // delay
    vTaskDelay(500); 
  }
}

// 在main函数中创建LED任务  
int main(void)
{
  // 初始化LED硬件

  // 创建LED闪烁任务
  xTaskCreate(
    vLEDTask,       // 任务入口    
    "LED_TASK",     // 任务名称
    128,            // 任务栈大小 
    NULL,           // 无任务参数
    1,              // 任务优先级 
    NULL );         // 不需要返回任务句柄
    
   // 启动调度器
  vTaskStartScheduler(); 
  
  return 0;
}
使用静态内存分配方法创建任务

xTaskCreateStatic()也用于创建任务并将它将入就绪任务。但是创建任务时不使用动态内存分配,需要由用户指定,使用这个函数需要将configSUPPORT_STATIC_ALLOCATION设置1

TaskHandle_t xTaskCreateStatic(/* 返回任务句柄 */  
       TaskFunction_t pvTaskCode, /* 任务入口函数 */
       const char * const pcName, /* 任务名称 */  
       const uint32_t ulStackDepth, /* 堆栈大小,单位为字 */
       void * const pvParameters, /* 传递给任务函数的参数 */
       UBaseType_t uxPriority, /* 任务优先级 */
       StackType_t * const puxStackBuffer, /* 任务栈缓冲区 */
       StaticTask_t * const pxTaskBuffer /* 任务参数结构体,包含栈缓冲区等 */
     )
  • TaskHandle_t xTaskCreateStatic():函数返回类型,返回任务句柄。
  • TaskFunction_t pvTaskCode:任务入口函数指针。
  • const char * const pcName:任务的名称。
  • const uint32_t ulStackDepth:任务栈的大小,单位为字。
  • void * const pvParameters:传递给任务函数的参数。
  • UBaseType_t uxPriority:任务的优先级。
  • StackType_t * const puxStackBuffer:指向任务栈缓冲区的指针,一般是一个数组,在32位架构中为uint32_t,需要用户实现分配好内存。
  • StaticTask_t * const pxTaskBuffer:任务参数的结构体指针,包含栈缓冲区等信息,需要用户实现分配好内存。

如果创建成功返回句柄。失败则返回NULL。

使用静态内存分配方法创建任务,除要指定待创建任务的任务堆栈及任务控制块内存以外,还要实现vApplicationGetTaskMemory()和vApplicationGetTimerTaskMemory()函数,作用是给空闲任务和软件定时器服务任务分配任务堆栈和任务控制块所需要内存。

任务删除

VTaskDelete()用于删除一个以及创建好的任务,也可以在任务运行时删除任务。由于删除任务之后,使用动态内存分配方法创建的任务会被释放资源,由此要给空闲任务运行的机会,用于资源回收。空闲任务一般设定为最低优先级。

如果是静态内存分配方法创建任务需要手动释放资源。

void vTaskDelete(TaskHandle_t xTask);

xTask是要删除任务的句柄,为NULL时删除自身。

使用vTaskDelete()函数时,需要在FreeRTOSConfig.h中将INCLUDE_vTaskDelete设置1。

如果传入参数为NULL,即数值0,则删除当前正在执行的任务,此任务被删除之后,FreeRTOS会切换到下一个运行的最高优先级的任务。

任务创建与删除实例

本次通过appTask()创建两个FreeRTOS任务。任务1位LED0Task,功能是让LED0每秒闪烁1次,闪烁5次后删除任务2;任务2为LED1Task,功能是使LED1每秒闪烁2次。

组织代码

创建appTask.c与appTask.h并加入到MDK-USER中。

其中.h内容如下

/*用来管理FreeRTOS任务的头文件*/
#ifndef _APPTASK_H_
#define _APPTASK_H_
#include "freertos.h"		//FreeRTOS头文件
#include "task.h"			// FreeRTOS任务实现头文件
static void LED0Task(void *pvParameters);	// 任务1闪烁任务
static void LED1Task(void *pvParameters);	//任务2闪烁任务
void appStartTask(void);		// 用于创建其他任务的函数
#endif
编写LED0任务函数

任务函数LED0Task()在appTask.h中声明,在appTask.c中实现

static void LED0Task (void *pvParameters)
{
    uint16_t cnt=0;		//用于统计闪烁次数的局部变量
    while(1)
    {
        HAL_GPIO_TogglePin(GPIOB, LED0_Pin);// LED闪烁
        vTaskDelay(pdMS_TO_TICKS(500));		//每秒闪烁一次
        printf("LED0正在以1秒的周期闪烁\r\n");
        if(++cnt >= 10)
        {
            if(eTaskGetState (LED1TaskHandle) != eDeleted)	//如果没有被删除
            {
                vTaskDelete(LED1TaskHandle);		// 删除LED1闪烁任务
                printf("LED1任务已经被删除\r\n");
            }
        }
    }
    /*如果在任务的具体实现中会跳出上面的死循环,则此任务必须在函数运行完之前删除。传入NULL参数表示删除的是当前任务*/
    vTaskDelete(NULL);
}

任务被删除之后,将不复存在,不能在对该任务进行操作,否则会造成系统崩溃。因此在删除任务前要调用eTaskGetState()函数,测试该任务是否已被删除。

eTaskGetState(LED1TaskHandle) 返回的枚举值表示任务的状态。FreeRTOS 中的任务状态是通过以下枚举值来表示的:

  1. eRunning: 任务正在运行,也就是当前正在执行的任务。
  2. eReady: 任务处于就绪状态,等待调度器选择运行它。
  3. eBlocked: 任务被阻塞,等待某些事件的发生,如等待信号量或消息队列。
  4. eSuspended: 任务被挂起,它可以通过其他任务恢复运行。
  5. eDeleted: 任务已经被删除,不再存在。
编写LED1任务函数

在appTask.c实现

static void LED1Task(void *pvParameters)
{
    while(1)
    {
        HAL_GPIO_TogglePin(GPIOB, LED1_Pin);
        vTaskDelay(pdMS_TO_TICKS(2500));
        printf("LED1正在以0.5秒周期闪烁\r\n");
        vTaskDelete(NULL);
    }
}
创建任务

任务的创建在appStartTask()函数中完成,先进入临界段,调用xTaskCreate()函数创建LED0闪烁及LED1闪烁任务,在创建任务之前,要先定义这两个任务的任务句柄。appTask()函数在appTask.h中声明,在appTask.c中实现

static TaskHandle_t LED0TaskHandle = NULL;	// 任务1句柄
static TaskHandle_t LED1TaskHandle = NULL;	// 任务2句柄

void appStartTask(void)
{
    taskENTER_CRITICAL();		// 进入临界段,关中断
    xTaskCreate(LED0Task,		/*任务函数*/
               "LED0TASK",		/*任务名*/
               128,				/*任务堆栈大小,单位word,4B*/
               NULL,			/*任务参数*/
               4,				/*优先级大小*/
               &LED0TaskHandle);	/*任务句柄*/
    xTaskCreate(LED1Task,		/*任务函数*/
               "LED1TASK",		/*任务名*/
               128,				/*任务堆栈大小,单位word,4B*/
               NULL,			/*任务参数*/
               4,				/*优先级大小*/
               &LED1TaskHandle);	/*任务句柄*/
    taskEXIT_CRITICAL();	//退出临界段,开中断
    vTaskStartScheduler();	// 开启调度器
}
修改main.c文件

先导入头文件"appTask.h",在/USER CODE BEGIN 2/之间插入 appStartTask(),创建任务并开启调度器.

4.任务调度

开启调度器

通过 vTaskStartScheduler()开启,在调度器开启的过程中会创建空闲任务并启动第一个任务。

1.1调度器开启函数

void vTaskStartScheduler(void)
{
BaseType_t xReturn;

/* 初始化调度器内部数据结构等 */
if (xTaskCreate(prvIdleTask, /* 闲置任务 */
                "IDLE",
                configMINIMAL_STACK_SIZE,
                (void *)NULL,
                (tskIDLE_PRIORITY | portPRIVILEGE_BIT),
                &xIdleTaskHandle) != pdPASS)
{
    /* 无法创建闲置任务,错误处理 */
}

/* 启动调度器 */
xReturn = xPortStartScheduler();

/* xPortStartScheduler()通常不会返回,但在某些错误情况下可能会返回 */

if (xReturn != pdPASS)
{
    /* 错误处理 */
}
1.2调度器开启过程

首先创建一个空闲任务,运行最低优先级(由宏portPRIVILEGE_BIT定义,通常为0),若配置configUSE_TIMERS为1,则创建软件定时器服务任务。若任务创建成功,则调用与移植层硬件相关的xPortStartScheduler()函数来初始化SysTick嘀嗒定时器作为FreeRTOS的心跳时钟,然后启动第一个任务,启动任务后任务将不会退出任务调度。

1.3启动第一个任务

在调度器开启的过程中,需要使用xPortStartSchedule()函数初始化中断,嘀嗒定时器,并启动第一个任务。

BaseType_t xPortStartScheduler(void)
{
    /* 禁用中断,为初始化做准备 */
    taskDISABLE_INTERRUPTS();

    /* 初始化硬件和RTOS内部数据结构 */
    prvSetupHardware();

    /* 启动系统节拍计数器和定时器中断 */
    prvStartTickInterrupt();

    /* 启动第一个任务 */
    vPortRestoreTaskContext();

    /* 恢复中断状态 */
    taskENABLE_INTERRUPTS();

    /* 进入RTOS的调度循环,通常不会返回 */
    vTaskSwitchContext();

    return pdFALSE; /* 通常不会返回 */
}

任务的挂起

2.1任务挂起函数

vTaskSuspend() 用于将某个任务设置为挂起状态,进入挂起状态的任务不会运行,退出挂起状态的唯一方法是调用任务恢复函数。

void vTaskSuspend(TaskHandle_t xTaskToSuspend)
{
    /* 如果传入的任务句柄为空,返回 */
    if (xTaskToSuspend == NULL) {
        return;
    }

    taskENTER_CRITICAL(); /* 进入临界段 */

    /* 设置任务的挂起状态 */
    xTaskToSuspend->uxSuspend = pdSUSPENDED;

    /* 检查任务是否正在运行,如果是,则切换到另一个任务 */
    if (xTaskToSuspend == pxCurrentTCB) {
        portYIELD();
    }

    taskEXIT_CRITICAL(); /* 退出临界段 */
}

xTaskToSuspend要挂起的任务句柄,为NULL时表示该任务本身。

任务挂起该函数使用了一个参数xTaskToSusoend,为挂起任务的任务句柄,创建任务的时候会为每个任务分配一个任务句柄。如果使用的 xTaskCreate()创建的任务,那么该函数的参数pxCreateTask就是此任务的任务句柄;如果使用xTaskCreateStatic()创建,那么该函数的返回值就是它的任务句柄。也可以使用 xTaskGetHandle()来根据任务名获取任务句柄,如果为NULL,表示该挂起任务本身。

2.2应用举例:挂起按键扫描

创建一个appTaskSuspend()函数,在该函数中创建按键扫描任务并将其挂起。

static TaskHandle_t KeyTaskHandle = NULL;		// 任务句柄
static void KeyTask(void *pvParameters)			// 按键扫描任务
{
	while(1)
	{
		KeyScan();
		vTaskDelay(10);			// 按键扫描
	}
}

// 创建按键扫描任务,并将其挂起
static void appTaskSuspend(void)
{
	xTaskCreate(KeyTask,"KeyScan",512,NULL,4,&KeyTaskHandle);
	if(KeyTaskHandle != NULL)
	{
		vTaskSuspend(KeyTaskHandle);
	}
}

任务的恢复

如果需要将一个任务从挂起态恢复到就绪态,那么就需要使用任务恢复函数。恢复函数有两个版本: vTaskResume()xTaskResumeFronISR(),后者是中断版本。

3.1任务恢复函数vTaskResume()
void vTaskResume(TaskHandle_t xTaskToResume)
{
    /* 如果传入的任务句柄为空,返回 */
    if (xTaskToResume == NULL) {
        return;
    }

    taskENTER_CRITICAL(); /* 进入临界段 */

    /* 检查任务是否被挂起,如果是,则将其状态设置为就绪 */
    if (xTaskToResume->uxSuspend == pdSUSPENDED) {
        xTaskToResume->uxSuspend = pdFALSE;

        /* 将任务添加到就绪列表以便后续调度 */
        if (xTaskToResume->uxPriority >= uxTopReadyPriority) {
            uxTopReadyPriority = configMAX_PRIORITIES - 1U;
        }
        else {
            configASSERT(uxTopReadyPriority);
            uxTopReadyPriority--;
        }

        vListInsertEnd(&(pxReadyTasksLists[uxTopReadyPriority]), &(xTaskToResume->xStateListItem));

        /* 如果任务的优先级高于当前运行任务,切换到新任务 */
        if (uxSchedulerSuspended == pdFALSE) {
            if (xTaskToResume->uxPriority >= pxCurrentTCB->uxPriority) {
                portYIELD_WITHIN_API();
            }
        }
    }

    taskEXIT_CRITICAL(); /* 退出临界段 */
}

xTaskToResume是需要恢复的任务句柄。

3.2任务恢复函数xTaskResumeFromISR()
BaseType_t xTaskResumeFromISR(TaskHandle_t xTaskToResume)
{
    BaseType_t xYieldRequired = pdFALSE;

    /* 如果传入的任务句柄为空,返回 */
    if (xTaskToResume == NULL) {
        return pdFALSE;
    }

    /* 检查任务是否被挂起,如果是,则将其状态设置为就绪 */
    if (xTaskToResume->uxSuspend == pdSUSPENDED) {
        xTaskToResume->uxSuspend = pdFALSE;

        /* 将任务添加到就绪列表以便后续调度 */
        if (xTaskToResume->uxPriority >= uxTopReadyPriority) {
            uxTopReadyPriority = configMAX_PRIORITIES - 1U;
        }
        else {
            configASSERT(uxTopReadyPriority);
            uxTopReadyPriority--;
        }

        if (xTaskToResume->uxPriority >= pxCurrentTCB->uxPriority) {
            xYieldRequired = pdTRUE;
        }
    }

    return xYieldRequired;
}

xTaskToResume是需要恢复的任务句柄。

返回值:pdTRUE或pdFALSE。

当要恢复任务的优先级等于或高于正在运行的任务(被打断的任务)时,返回pdTRUE。这意味着退出中断服务函数后必须进行一次上下文切换。

当要恢复任务的优先级低于正在运行的任务(被打断的任务)时,返回pdFALSE。这意味着退出中断服务函数后无需进行一次上下文切换。

3.3应用举例:恢复用vTaskSuspend()函数挂起的按键扫描任务

创建一个appTaskResume()函数,在该函数中来恢复vTaskSuspend()挂起的按键扫描任务。


// 创建扫描任务,用vTaskSuspend()函数挂起,然后恢复
static void appTaskResume(void)
{
	xTaskCreate(KeyTask,"KeyTask",512,NULL,4,&KeyTaskHandle);
	if(KeyTaskHandle != NULL)
	{
		vTaskSuspend(KeyTaskHandle);
	}
	// 恢复此任务
	if(KeyTaskHandle != NULL)
	{
		vTaskResume(KeyTaskHandle);
	}
}

任务调度

FreeRTOS支持3种任务调度方式:抢占式,时间片,合作式调度。

大多时候采用抢占式与时间片,合作式是以前资源很少的MCU准备的,但是由于现在MCU发展很快,所以不在需要。

抢占式调度用于任务有不同优先级的场合。每个任务都有不同的优先级,任务会一直运行,知道被高优先级任务抢占,或者遇到阻塞式API调用,如最简单的 vTaskDelay()函数调用,才会让CPU让出使用权。

时间片调度用于多个任务具有相同优先级的场合。当多个优先级相同时,每个任务在运行一个时间片(一个系统时钟节拍的长度,在STM32微控制器中就是嘀嗒定时器的中断周期)后让出CPU使用权,让其他优先级相同的任务有被运行的机会。

FreeRTOS任务切换场合

FreeRTOS会在下面两种情况执行任务切换

  1. 执行系统调用
  2. 嘀嗒定时器中断

执行系统调用就是执行FreeRTOS提供的API函数,这类函数有很多个,但实际操作任务切换的是taskYIELD()宏,该宏与硬件平台无关,在 task.c中定义。

#define taskYIELD()			portYIELD()

对于不同的平台,任务切换有差异。在硬件移植层portmacro.h中定义。对于STM32,代码如下:

portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;

portYEILD()宏的主要工作就是将中断控制和状态寄存器(ICSR)的bit28置1,触发一个PendSV中断,FreeRTOS在PendSV中断进行任务切换。

FreeRTOS中嘀嗒定时器的中断服务函数如下:

#define xPortSysTickHandle  xPortSysTick
void xPortSysTickHandle(void)
{
    vPortRaiseBASEPRI();
    {
        /* 系统时钟节拍加1,并判断有无需要切换的任务 */
        if( xTaskIncrementTick() != pdFALSE)
        {
            /* 置PendSV中断挂起位以切换上下文 */
            portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
        }
    }
    vPortClearBASEPRIFromISR();
}

可以看出,无论在哪个任务切换场合,FreeRTOS都是通过设置PendSV中断来挂起位触发一次PendSV中断来进行任务切换。

PendSV中断

在STM32中有两个系统中断,SVC与PendSV,前者叫系统服务调用,简称系统调用,后者叫做可挂起的系统调用。

触发PendSV中断的方法就是ICSR的bit28置1。若置1后优先级不够,则将等等更高优先级的中断执行完后才会执行,这使PendSV中断非常适合在操作系统中用于任务切换。

PendSV中断服务函数

FreeRTOS任务切换通过PendSV中断服务函数实现。PendSV中断服务函数的名称是PendSV_Handler。为了方便不同硬件平台之间的移植,FreeRTOS对PendSV中断服务函数重新进行定义,定义为xPortPendSVHandler,在FreeRTOSConfig.h

#define xPortPendSVHandler PendSV_Handler /*方便移植不同不同平台*/

查找下一个要运行的任务

在PendSV中断服务函数中,通过调用 vTaskSwitchContext()函数来查找下一个要运行的、已经就绪且优先级最高的任务,该函数在task.c中

void vTaskSwitchContext( void )
{
    if( uxSchedulerSuspended != pdFALSE )
    {
        /* 如果调度器被挂起,不允许进行上下文切换。 */
        xYieldPending = pdTRUE;
    }
    else
    {
        xYieldPending = pdFALSE;
        traceTASK_SWITCHED_OUT();

        /* 如果启用了运行时间统计,则更新任务的运行时间统计。 */
        #if ( configGENERATE_RUN_TIME_STATS == 1 )
        {
            /* 添加任务运行时间到累积时间中。 */
            if( ulTotalRunTime > ulTaskSwitchedInTime )
            {
                pxCurrentTCB->ulRunTimeCounter += ( ulTotalRunTime - ulTaskSwitchedInTime );
            }
            else
            {
                mtCOVERAGE_TEST_MARKER();
            }

            ulTaskSwitchedInTime = ulTotalRunTime;
        }
        #endif

        /* 检查堆栈溢出,如果已配置。 */
        taskCHECK_FOR_STACK_OVERFLOW();

        /* 在当前运行任务被切换出之前,保存其 errno。 */
        #if ( configUSE_POSIX_ERRNO == 1 )
        {
            pxCurrentTCB->iTaskErrno = FreeRTOS_errno;
        }
        #endif

        /* 选择一个新的任务来运行。 */
        taskSELECT_HIGHEST_PRIORITY_TASK();
        traceTASK_SWITCHED_IN();

        /* 新任务切换进来后,更新全局 errno。 */
        #if ( configUSE_POSIX_ERRNO == 1 )
        {
            FreeRTOS_errno = pxCurrentTCB->iTaskErrno;
        }
        #endif

        /* 切换 C 运行时的 TLS 块以指向当前任务的 TLS 块。 */
        #if ( ( configUSE_NEWLIB_REENTRANT == 1 ) || ( configUSE_C_RUNTIME_TLS_SUPPORT == 1 ) )
        {
            configSET_TLS_BLOCK( pxCurrentTCB->xTLSBlock );
        }
        #endif
    }
}

选择已经就绪且优先级最高的任务有两种方法:通用方法,硬件方法。通过两个宏进行定义。

configUSE_PORT_OPTIMISED_TASK_SELECTION

进行选择,为0则为通用方法,为1则为硬件方法。

通用方法对于所有硬件平台都适用,但是速度比硬件方法慢很多

查找下一个要运行的任务还可以使用特定的硬件的特定指令,即硬件方法。(不想写它的,直接用通用得了)

FreeRTOS时间片调度

当多个任务具有相同的优先级时,每个任务只允许一个系统时钟节拍,然后让出CPU使用权,让另一个任务运行,从而实现同优先级任务的调度,称为时间片调度。

要使用时间片调度需要设置两个宏。

condfigUSE_PREEMPTION		1
configUSE_TIME_SLICING		1

时间片的长度由宏configTICK_RATE_HZ定义的系统时钟节拍决定,即嘀嗒定时器的溢出周期,当宏为1000时,时间片长度就是1ms。

在嘀嗒定时器中断服务函数中,通过XTaskIncrementTick()函数来使系统时钟节拍加1,并判断有无需要切换的任务,当这个函数返回pdTRUE时,表示要进行任务切换。去掉与时间片调度无关的代码之后的XTaskIncrementTick函数如下:

BaseType_t XTaskIncrementTick( void )
{
    BaseType_t xSwitchRequired = pdFALSE;

    taskENTER_CRITICAL();
    {
        /* 增加系统时钟的节拍计数。 */
        if( xTickCount == ( TickType_t ) 0U )
        {
            xTickCount = ( TickType_t ) 1U;
        }
        else
        {
            xTickCount++;
        }

        /* 检查任务的延时等待列表并更新任务的状态。 */
        prvCheckDelayedTaskList( xTickCount, &xSwitchRequired );

        /* 执行其他与时钟节拍相关的操作。 */
        /* ... 这部分代码可能包含与时间片调度相关的内容 ... */
    }
    taskEXIT_CRITICAL();

    /* 返回 pdTRUE 表示需要进行任务切换。 */
    return xSwitchRequired;
}

空闲任务

FreeRTOS开启调度之后,至少有一个是任务处于运行态,为了保证这一点VTaskStartScheduler()开启调度器后,会自动创建一个空闲任务。

空闲任务是一个特别小的循环,且拥有最低优先级(0),可以保证一旦有更高优先级任务进入就绪态,空闲任务就会立即切出运行态。

空闲任务的主要功能有:

  1. 释放内存。如果有任务删除了自身,被删除的任务的TCB和堆栈资源会在空闲任务中释放,单用户自己分配的资源需要手动回收。
  2. 处理空闲优先级任务。当采用抢占式调度方式时,如果有用户任务与空闲任务共享一个优先级,空闲任务可以不必等到时间片耗尽就进行任务切换。当没有采用抢占式调度时,空闲任务总是调用taskYIELD()函数试图切换用户任务,以确保最快响应用户任务。
  3. 执行空闲任务钩子函数。这个函数由用户实现。
  4. 实现低功耗tickless模式。FreeRTOS的tickless模式会在空闲周期停止嘀嗒定时器,从而让微控制器长时间处于低功耗模式。移植层需要配置外部唤醒中断,当唤醒事件到来时,将微控制器从低功耗模式唤醒。MCU在被唤醒后,需要重新使能嘀嗒定时器。

FreeRTOS内核函数

除了VTaskStartScheduler()和taskYIELD()是它的内核函数外,还有一些,如图。

函数功能
taskYIELD()进行任务切换
taskENTER_CRITICAL()进入临界段,关中断
taskEXIT_CRITICAL()退出临界段,开中断
taskENTER_CRITICAL_FROM_ISR()进入临界段中断版本
taskEXIT_CRITICAL_FROM_ISR()退出临界中断版本
taskDISABLE_INTERRUPT()关中断
taskENABLE_INTERRUPT()开中断
vTaskStartScheduler()开启调度器
vTaskEndScheduler()关闭调度器
vTaskSuspendAll()挂起调度器
xTaskResumeAll()恢复调度器
vTaskStepTick()设置系统时钟节拍追加值

临界段操作函数

在程序运行过程中,一些关键代码的执行不能被打断,这一段不能被打断的程序被称为临界段或临界区。能打断程序执行的一般是中断,所以进入、退出临界段需要进行开中断、关中断操作。

进入和退出临界段函数有四个:taskENTER_CRITICAL()taskEXIT_CRITICAL()taskENTER_CRITICAL_FROM_ISR()taskEXIT_CRITICAL_FROM_ISR()。后两个函数在中断服务中使用。

1.1taskENTER_CRITICAL()和taskEXIT_CRITICAL()

进入和退出其实两个宏,最终实现进入临界段功能的是vPortEnterCritical()函数。进入后先用portDISABLE_INTERRUPT()操作寄存器basepri关闭FreeRTOS管理的所有中断,全局变量uxCriticalNesting加1,用来记录临界段的嵌套次数。

/* 这个宏定义会根据不同的架构提供不同的实现方式 */
#define portENTER_CRITICAL()    vPortEnterCritical()
#define taskENTER_CTRITICAL()	portENTER_CRITICAL()

/* 用于追踪临界区嵌套深度的变量 */
UBaseType_t uxCriticalNesting = 0;

/* 具体的实现可以是禁用中断的汇编或其他适合的方式 */
static inline void portDISABLE_INTERRUPTS(void)
{
    /* 禁用中断的代码,具体取决于你的微控制器架构 */
    /* 这里以 Cortex-M 为例,使用汇编指令禁用中断 */
    __asm volatile ("cpsid i" ::: "memory");

    /* 增加临界区嵌套深度计数器 */
    uxCriticalNesting++;
    if( uxCriticalNesting == 1)
    {
        configASSERT((portNVIC_INT_CTRL_REG & portVECTACTIVE_MASK) == 0);
    }
}

与进入临界段类似真正实现退出临界段的是vPortExitCritical()函数,uxCriticalNesting会减1,当他等于0时,也就是所有临界段代码都退出后,才打开中断。这样保证了在有多个临界段时,不会因某个临界段代码执行完成而退出。

/* 这个宏定义会根据不同的架构提供不同的实现方式 */
#define portEXIT_CRITICAL()    vPortExitCritical()
#define taskEXIT_CTRITICAL()	portEXIT_CRITICAL()

/* 用于追踪临界区嵌套深度的变量 */
UBaseType_t uxCriticalNesting = 0;

/* 具体的实现可以是禁用中断的汇编或其他适合的方式 */
static inline void vPortExitCritical(void)
{
    configASSERT(uxCriticalNesting > 0);

    uxCriticalNesting--;


      /* 启用中断的代码,具体取决于你的微控制器架构 */
      /* 这里以 Cortex-M 为例,使用汇编指令启用中断 */
    if( uxCriticalNesting == 1)
    {
        configASSERT((portNVIC_INT_CTRL_REG & portVECTACTIVE_MASK) == 0);
    }
}
1.2taskENTER_CRITICAL_FROM_ISR()和taskEXIT_CRITICAL_FROM_ISR()

这两个是进入、退出临界段函数的中断版本,在中断服务函数中使用。Vge中断必须是FreeRTOS可以管理的中断,即中断优先级低于configMAX_SYSCALL_INTERRUPT所设置的值的中断。

挂起和恢复调度器函数

vTaskSuspendAll()函数用于挂起调度,xTaskResumeAll()函数用于恢复调度器,调度器挂起后,介于vTaskSuspendAll()和vTaskResumeAll()之间的代码不会被更高优先级任务抢占,即任务调度被禁止。挂起调度器不用关中断。

挂起调度器的代码如下:

void vTaskSuspendAll( void )
{
    ++uxSchedulerSuspended;
}

调度器挂起支持嵌套。uxSchedulerSuspended是一个静态全局变量,在使用恢复函数时,次计数器会减1,当变为0时真正恢复调度器。

调用几次vTaskSuspendAll()函数,就需要调用多少次vTaskResumeAll()。

任务切换函数

taskYIELD()是任务切换函数,该变量其实是一个宏,最终由宏portYIELD()实现。通过想 ICSR的bit28位写入1,启动一个PendSV中断,在PendSV中缩中完成任务切换。

系统时钟节拍追加

vTaskStepTick()函数用于设置系统时钟节拍的追加值,在低功耗tickless模式下使用,当宏configUSE_TICKLESS_IDLE为1时,使能低功耗tickless模式。一般情况下,会在空闲任务中让系统时钟节拍停止运行,恢复系统时钟节拍后,系统时钟节拍停止运行的节拍数就可以用vTaskStepTick()补上。

void vTaskStepTick( const TickType_t xTicksToJump)
{
    configASSERT( ( cTickCount + xTicksToJump ) <= xNextTaskUnblockTime );
    xTickCount += xTicksToJump;
    traceINCREASE_TICK_COUNT( xTicksToJump );
}

vTaskStepTick()函数有一个参数xTicksToJump,为要追加的系统时钟节拍值。

内核函数使用示例

static void Test4Task(void *pvParameters)
{
	uint16_t count = 0;
	while(1)
	{
		HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_7);
		count++;
		taskENTER_CRITICAL();
		printf("任务4:LED1闪烁,任务4已运行 %d次\r\n",count);
		taskEXIT_CRITICAL();
		vTaskDelay(pdMS_TO_TICKS(500));		// 每秒闪烁一次
	}
}

static void Test5Task(void *pvParameters)
{
	uint16_t count=0;
	while(1)
	{
		HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_6);
		count++;
		vTaskSuspendAll();		// 挂起调度器
		printf("任务5:LED2闪烁,运行 %3d次\r\n",count);
		if(xTaskResumeAll() == pdTRUE)		// 如果有任务需要切换,则返回函数pdTRUE
		{
			taskYIELD();			// 进行一次任务切换
		}
	}
}

创建启动函数

xTaskCreate(Test4Task,"Test4Task",128,NULL,4,&Test4TaskHandle);
xTaskCreate(Test5Task,"Test5Task",128,NULL,3,&Test5TaskHandle);

FreeRTOS任务函数

任务管理是FreeRTOS的核心功能,处理内核函数中的任务创建、挂起、恢复、删除和任务切换等之外,还有其他函数

函数功能
vTaskDelay()阻塞任务,直到指定的系统时钟节拍后解除阻塞
vTaskDelayUntil任务以指定的系统时钟节拍周期性执行
uxTaskPriorityGet()获取某个任务的优先级
vTaskPrioritySet()设定某个任务的优先级
uxTaskGetSystemState()获取系统任务状态
vTaskGetInfo()获取某个任务信息
xTaskGetCurrentTaskHandle()获取当前任务句柄
xTaskGetIdleTaskHandle()获取空闲任务句柄
uxTaskGetStackHighWaterMark()获取某个任务剩余堆栈历史最小值
eTaskGetState()获取某个任务的状态
pcTaskGetName()获取某个任务的任务名
xTaskGetHandle()根据任务名查找某个任务句柄
xTaskGetTickCount()获取系统时钟节拍值
xTaskGetTickCountFromISR()中断内获取系统时钟节拍值
xTaskGetSchedulerState()获取调度器状态
uxTaskGetNumberOfTasks()获取调度器数量
vTaskList()以列表形式输出所有任务的信息
vTaskGetRunTimeStates()获取所有任务的运行时间信息
vTaskSetApplicationTaskTag()设置任务标签
xTaskGetApplicationTaskTag()获取任务标签

延时函数

FreeRTOS提供了两种延时函数:

  1. 相对延时函数 vTaskDelay()
  2. 绝对延时函数 vTaskDelayUntil()

这两种延时函数都会使调用它们的任务进入阻塞态,带延时结束后再恢复到就绪态,是最优先级任务主动让出CPU使用权的一种有效方法。都是基于系统时钟节拍作为计算依据。

1.1系统时钟节拍

系统时钟节拍被称为心跳,是一个周期性的中断。时钟节拍越快,系统的额外开销越大。

在STM32中,系统时钟节拍由嘀嗒定时器提供。FreeRTOS定义了一个静态全局变量xTickCount,用来对系统时钟节拍技术,嘀嗒计时器每中断一次xTickCount就会加1。系统时钟节拍长短怎么FreeRTOSconfig.h中配置

#define configTICK_RATE_HZ		((TickType_t) 1000)
1.2相对延时

相对延时是指每次延时都从vTaskDelay()函数开始,到指定的系统时钟节拍后结束,任务恢复到就绪态。

void vTaskDelay( const TickType_t xTicksToDelay );

参数说明:

xTicksToDelay:要延时的系统时钟节拍数,范围为1~portMAX_DELAY。对于STM32,portMAX_DELAY的值是0xFFFFFFFF。

返回值:无

在程序设计中,优势使用系统时钟节拍数进行延时不方便。FreeRTOS提供了将延时时间转换为系统时钟节拍的宏 pdMS_TO_TICKS(),可以方便的将要延时的以ms为单位的时间转换为系统时钟节拍。

#define pdMS_TO_TICKS( xTimeMs)		((TickType_t)	\	(	(	(TickType_t)	(xTimeInMs)	*	\	(TickType_t)	configTick_TATE_HZ)	/	(TickType_t)	1000))

vTaskDelay()是相对延时函数,延时时间是从调用这个函数开始计算,任务进入阻塞态,在到达指定的系统时钟节拍数时,将任务加入就绪列表。若任务在运行过冲中发生了中断,则任务的运行时间会变长,所以相对延时时间不精确。

1.3绝对延时

绝对延时是指每隔指定的系统系统时钟节拍运行一次调用vTaskDelayUntil()函数任务。也就是说,任务以固定的频率运行。

void vTaskDelayUntil(TickType_t * const pxPreviousWakeTime, const TickType_t xTimeIncrement)

参数说明:

pxPreviousWakeTime:指向用于保存上次任务退出阻塞态时的变量

xTimeIncrement:周期性的延时值

返回值:无

与相对延时不同,绝对延时函数VTaskDelayUntil()增加了一个参数pxPreviousWakeTime,用于指向一个变量,该变量保存了上次任务退出阻塞态的时间。这个变量在任务开始时必须被设置成当前系统时钟节拍值,此后vTaskDelayUntil()函数在内部会自动更新这个变量。

假设系统中有一个具有最高优先级的任务B,当任务B调用vTaskDelayUntil()函数时,即使发生中断,整个任务的运行时间也不会变长,即绝对延时函数能以固定频率周期性地运行某个任务。

注意:FreeRTOSv202212.01版本之后vTaskDelayUntil()更名xTaskDelayUntil(),并在配置文件中加入

#define INCLUDE_xTaskDelayUntil				1
//#define INCLUDE_vTaskDelayUntil              0 注释之前的

优先级控制

1.1获取任务优先级

uxTaskPriorityGet() 获取指定任务的优先级。使用前配置

INCLUDE_uxTaskPriorityget		1

使用原型为:

UBaseType_t uxTaskPriorityGet(const TaskHandle_t xTask);

参数说明:

xTask:要查询的任务的任务句柄,为NULL表示查询调用本函数的任务的优先级。

1.2设置任务优先级

vTaskPrioritySet()用于设置指定任务的优先级。使用前配置

INCLUDE_vTaskPrioritySet		1

使用原型:

void vTaskPrioritySet( TaskHandle_t xTask, UBaseType_t uxNewPriority);

参数说明:

xTask:设置优先级放入任务的句柄

uxNewPriority:要设置新优先级,范围0~configMAX_PRIORITY - 1

1.3改变任务优先级示例

为便于观察,先设置宏 configUSE_TIME_SLICING置0,禁止时间片调度,并且龙哥任务不加入任何使任务进入阻塞态的函数,这样保证只有一个任务在运行。

同时通过按键使任务2优先级降低。

按键KEY_0引脚为 PE4

获取任务状态信息

1.1任务状态获取函数
1.1.1 uxTaskGetSystemState()

它用来获取所有任务的状态,任务句柄,任务名,任务堆栈,任务优先级等,将信息保存在一个TaskStatus_t类型的结构体数组中,使用前需要配置宏。

configUSE_TRACE_FACILITY = 1

函数原型如下:

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

参数说明:

pxTaskStatusArray:指向TaskStatus_t类型的结构体数组首地址

uxArraySize:保存任务状态信息的数组大小

pulTotalRunTime:当宏 configGENERATE_RUN_TIME_STATS为1时,保存总运行时间。

返回值:

获取到的任务状态个数,即保存到数组pxTaskStatusArray中的任务状态个数。

1.1.2vTaskGetInfo()

用于获取单个任务的状态,任务句柄,任务名,任务堆栈,任务优先级等,将信息保存在一个TaskStatus_t类型的结构体数组中,使用前需要配置宏。

使用前需要配置宏。

configUSE_TRACE_FACILITY = 1

函数原型如下:

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

参数说明:

xTask:要获取任务信息的任务句柄。

pxTaskStatus:指向TaskStatus_t类型的结构体数组首地址。

xGetFreeStackSpace:是否需要获取任务剩余堆栈历史最小值。

eState:要获取信息的任务状态。

返回值:无。

1.1.3uxTaskGetStackHighWaterMark()

每个任务都有自己的堆栈,用于在进行任务切换时保存必要的数据。利用uxTaskGetStackHighWaterMark()可以检测任务从创建运行以来剩余堆栈历史最小值,这个值越小,说明堆栈溢出的可能性越大。FreeRTOS把这个任务剩余堆栈历史最小值称为“高水位线”。

使用前需要进行配置:

INCLUDE_uxTaskGetStackHighWaterMark设置1.

该函数原型为:

configSTACK_DEPTH_TYPE uxTaskGetStackHighWaterMark2(TaskHandle_t xTask);

参数说明:

xTask:要获取高水位线的任务句柄,为NULL时为自身。

返回值:

任务剩余堆栈历史最小值。

1.1.4eTaskGetState()

eTaskGetState()用于查询指定任务的状态,任务状态有运行态、就绪态、阻塞态、挂起态。

函数原型如下:

eTaskGetState eTaskGetState(TaskHandle_t xTask);

参数说明:

xTask:要获取高水位线的任务句柄,为NULL时为自身。

返回值:

eTaskState枚举常量,eRunning、eReady、eBlocked、eSuspend、eDeleted、eInvalid

这些是 FreeRTOS 中任务状态的枚举值,用于表示不同任务的状态。以下是各个状态的含义:

  1. eRunning(运行中):任务当前正在执行,也就是当前正在 CPU 上运行的任务。
  2. eReady(就绪):任务已经准备好运行,但还没有得到 CPU 的时间片或优先级不足以使其运行。
  3. eBlocked(阻塞):任务因为某些原因被阻塞,等待某些事件的发生,如等待信号量、消息队列、事件组等。
  4. eSuspended(挂起):任务被挂起,它不会被调度器选中运行,但可以通过其他任务来恢复运行。
  5. eDeleted(已删除):任务已经被删除,不再存在,其资源已被释放。
  6. eInvalid(无效):无效的任务状态,通常不会被使用。
1.1.5vTaskList()

vTaskList()用于将每个任务的状态信息以字符串的形式存入字符数组或指定的存储区。

任务信息包括:任务名,任务状态,优先级,剩余堆栈大小及任务号,任务原型如下:

void vTaskList(cahr *pcWriteBuffer);

参数说明:

pcWriteBuffer:保存任务信息的存储区。

示例信息:

getTestInfo    	X	1	251	3
IDLE           	R	0	109	4
Test11Task     	B	2	107	2
Test10Task     	B	3	235	1
Tmr Svc        	B	2	231	5
  • getTestInfo 任务处于就绪状态(Ready),拥有优先级 1,堆栈使用 251 字节。
  • IDLE 任务处于运行状态(Running),拥有优先级 4,堆栈使用 109 字节。
  • Test11Task 任务处于阻塞状态(Blocked),拥有优先级 2,堆栈使用 107 字节。
  • Test10Task 任务处于阻塞状态(Blocked),拥有优先级 3,堆栈使用 235 字节。
  • Tmr Svc 任务处于阻塞状态(Blocked),拥有优先级 2,堆栈使用 231 字节。

统计任务运行时间信息

1.1任务运行时间信息统计函数

vTaskGetRunTimeStats()原型如下:

void vTaskGetRunTimeStats(char *pcWriteBuffer)

参数说明:

pcWriteBuffer:保存任务运行时间信息的存储区

使用前需要配置

configGENERATE_RUN_TIME_STATS			1
configUSE_TRACE_FACILITY				1
configUSE_STATS_FORMATTING_FUNCTIONS	 1

portCONFIGURE_TIMER_FOR_RUN_TIME_STATS():用于初始化任务运行时间信息统计功能的时间基准。时间基准一般使用定时器来提供,并且要求这个定时器的精度是系统时钟节拍精度的10倍以上。

portGET_RUN_TIME_COUNTER_VALUE():用于获取统计任务运行时间信息的计数器值,利用这个计数器来计算任务运行时间百分比。

注意:使用vTaskGetRunTImeStats()函数可能会降低系统的实时性。该函数一般在代码调试阶段使用

FreeRTOS队列与消息传递

任务与任务之间,任务与中断之间常常需要进行信息交互与消息传递。

FreeRTOS利用队列来实现任务间通信,队列可以用于任务与任务之间、任务与中断之间传递消息,所以又被称为“消息队列”。

另外,用于资源共享和访问的二值信号量、计数信号量、互斥信号量和递归互斥信号量之间也都是通过队列来实现。

1.FreeRTOS队列及其结构

队列是一种特殊的数据结构,可以保存有限个具有确定长度的数据单元。队列可以保存的最大单元数目被称为队列的深度,在创建队列时需要设定深度和每个单元的大小。

1.1FreeRTOS队列特性
1.1.1存储数据

通常队列作为FIFO结构使用,即数据从队尾写入,队列首读出。

FreeRTOS向队列写入数据是通过字节复制把数据赋值存储到队列中的,而不是通过数据引用(只传递数据指针)的方式实现的。

从队列中读出数据将删除队列中复制的数据。

1.1.2多任务访问

队列不属于某个任务,所有任务都可以操作队列,向队列发送消息,或者从队列中获取消息,前提是队列要在操作前创建好。

1.1.3读队列时阻塞

当某个任务试图读一个队列时,可以指定一个阻塞时间,如果在这段时间中,队列为空,那么该任务将保持阻塞态以等待队列数据有效。

如果其他任务或中断服务示例程序向其等待的队列写入了数据,那么该任务将自动由阻塞态转为就绪态。

当等待时间超过了阻塞超时时间,即使队列中尚有无效数据,任务也会自动从阻塞态转为就绪态。

由于队列可以被多个任务读取,所以对单个队列而言,也可能有多个任务处于阻塞态以等待队列数据有效。在这种情况下,一旦队列数据有效,只会有一个任务解除阻塞态,这个任务就是所有等待任务中优先级最高的。如果优先级都相同,那么被解除阻塞态的任务将是等待最久的任务。

1.1.4写队列时阻塞 {#7114}

1.1.4写队列时阻塞

1.1.5队列读写过程

创建一个队列Queue用于进行任务A和任务B之间的通信,次队列最多可以保存5个字符。队列创建好的时候是空的,无数据。

任务A将一个本地变量的值写入队列(入队),由于队列之前是空的,所以写入的值目前是队列中唯一的数据单元,队列尾和队列首同时这个值。

任务A改变本地变量的值并再次写入队列,由于队列首已经有数值了,新写入的值被插入队列尾,紧跟在第一个值后面,现在队列中还有3个空的数据单元。

任务B从队列中读取(接收)数据到本地变量,读取的值是队列首的数值,即任务A第一次写入的值。

任务B已经读走了一个数据单元的值,现在队列中值剩下任务A第二次写入的值,这个值将在任务2下一次度队列时被读走,目前队列中空数据单元变为4个。

1.2队列结构体

FreeRTOS用结构体Queue_t描述队列,

typedef struct QueueDefinition /* 用于防止破坏内核感知调试器的旧命名约定。 */
{
    int8_t * pcHead;           /*< 指向队列存储区的开头。 */
    int8_t * pcWriteTo;        /*< 指向存储区中的下一个空闲位置。 */

    union
    {
        QueuePointers_t xQueue;     /*< 仅当此结构用作队列时需要的数据。 */
        SemaphoreData_t xSemaphore; /*< 仅当此结构用作信号量时需要的数据。 */
    } u;

    List_t xTasksWaitingToSend;             /*< 阻塞等待向该队列发布的任务列表,按优先级排序。 */
    List_t xTasksWaitingToReceive;          /*< 阻塞等待从该队列读取的任务列表,按优先级排序。 */

    volatile UBaseType_t uxMessagesWaiting; /*< 队列中当前的项目数量。 */
    UBaseType_t uxLength;                   /*< 队列的长度,定义为它将保存的项目数,而不是字节数。 */
    UBaseType_t uxItemSize;                 /*< 队列将保存的每个项目的大小。 */

    volatile int8_t cRxLock;                /*< 存储在锁定队列时从队列中接收的项目数量(从队列中移除)。在队列未锁定时设置为queueUNLOCKED。 */
    volatile int8_t cTxLock;                /*< 存储在锁定队列时传递给队列的项目数量(添加到队列中)。在队列未锁定时设置为queueUNLOCKED。 */

    #if ( ( configSUPPORT_STATIC_ALLOCATION == 1 ) && ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) )
        uint8_t ucStaticallyAllocated; /*< 如果队列的内存是静态分配的以确保不会尝试释放内存,则设置为pdTRUE。 */
    #endif

    #if ( configUSE_QUEUE_SETS == 1 )
        struct QueueDefinition * pxQueueSetContainer;
    #endif

    #if ( configUSE_TRACE_FACILITY == 1 )
        UBaseType_t uxQueueNumber;
        uint8_t ucQueueType;
    #endif
} xQUEUE;

typedef xQUEUE Queue_t;// 新版本队列数据类型名

2.队列操作

队列的创建、入队和出队是最常见的队列操作,除此之外还有队列删除、重置、查询队列中的消息数量等队列操作。

2.1队列创建

有动态、静态两种方式创建。xQueueCreate()实现动态创建,用xQueueCreateStatic()实现静态创建。采用动态时,使用动态内存分配,采用静态时,则需要用户指定相应的存储区。

2.1.1xQueueCreate()

它其实是一个宏,定义如下:

#define xQueueCreate( uxQueueLength, uxItemSize)	\	xQueueGenericCreate( ( uxQueueLength ),	\	( uxItemSize ), \ ( queueQUEUE_TYPE_BASE) )

由此可见,真正用来创建队列的是xQueueGenericCreate(),该函数如下:

QueueHandle_t xQueueGenericCreate( const UBaseType_t uxQueueLength,
                                   const UBaseType_t uxItemSize,
                                   const uint8_t ucQueueType );

参数说明如下:

uxQueueLength:创建队列成都,即队列数量

uxItemSize:队列中每个队列项的长度,单位B

ucQueueType:队列类型,用于指明创建的队列属于哪种用途,一共有6种类型:

queueQUEUE_TYPE_BASE普通消息队列
queueQUEUE_TYPE_SET队列集
queueQUEUE_TYPE_MUTEX互斥信号量
queueQUEUE_TYPE_COUNTING_SEMAPHORE计数信号量
queueQUEUE_TYPE_BINARY_SEMAPHORE二值信号量
queueQUEUE_TYPE_RECURSIVE_MUTEX递归互斥信号量

返回值:创建成功返回所创建队列的队列句柄,创建失败返回NULL。

2.1.2xQueueCreateStatic()

可用静态方法创建队列,创建过程中使用的内存需要由用户事先分配,这是一个宏。

#define xQueueCreateStatic( uxQueueLength, uxItemSize, pucQUeueStorage, pxQueueBuffer )	  \				  												xQueueGenericCreateStatic( ( uxQueueLength ), \
										( uxItemSize ),							    \
										( pucQueueStorage ),					    \
										( pxQueueBuffer ),						    \
										( queueQUEUE_TYPE_BASE ) )

因此创建xQueueCreateStatic()函数原型为:

QueueHandle_t xQueueGenericCreateStatic(const UBaseType_t uxQueueLength,
                                        const UBaseType_t uxItemSize,
                                        uint8_t *pucQueueStorage,
                                        StaticQueue_t *pxStaticQueue,
                                        const uint8_t ucQueueType );

参数说明如下:

uxQueueLength:要创建的队列的长度,即队列数量

uxItemSize:队列中每个队列项的长度,单位为B

pucQueueStorage:指向队列项的存储区,需要用户自行分配内存

pxStaticQueue:指向StaticQueue_t结构体变量,用于保存队列结构体

ucQueueType:队列类型,与动态方法创建类型相同

返回值:创建成功返回所创建队列的队列句柄,创建失败返回NULL。

2.2入队操作

入队操作API有xQueueSend()xQueueSendBack()xQueueSendToFront()xQueueOverwrite()四个,它们都是长得像函数的宏,实际入队操作的是xQueueGenericSend()函数。xQueueSendFromISR()xQueueSendToBackFromISR()xQueueSendToFrontISR()xQueueOverwriteFromISR()是入队操作API的中断版本,实际执行入队操作的是xQueueGenericSendFromISR()函数。

2.2.1xQueueGenericSend()函数

xQueueGenericSend()函数是通用入队函数,该函数原型如下:

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

参数说明:

xQueue:队列句柄,指向要向哪个队列发送数据

pvItemToQueue:发送队列的消息,以数据赋值的形式发送

xTicksToWait:任务阻塞超时时间,该值为0且队列满时不阻塞,函数立即返回。该值为portMAX_DELAY且队列满时将无限期阻塞,但要将宏INCLUDE_vTaskSuspend设置为1.其他值为队列满时任务阻塞的系统时钟节拍。

xCopyPosition:入队方式有三种。

  1. queueSEND_TO_BACK 后向入队
  2. queueSEND_TO_FRONT 前向入队
  3. queueOVERWRITE 覆写入队

返回值:pdTRUE,想队列发送消息成功;errQUEUE_FULL,队列满,向队列发送消息失败。

2.2.2xQueueGenericSendFromISR()

这个函数是中断服务函数中使用的入队操作。原型如下:

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

参数如下:

xQueue:队列句柄,指明要向哪个队列发送数据

pvItemToQueue:发往队列的消息,以数据赋值的形式发送

pxHigherPriorityTaskWoken:指向一个用于保存调用函数后是否进行任务切换的变量,若执行函数后值为pdTRUE,则要在退出中断服务函数后执行一次任务切换

xCopyPosition:入队方式,有三种:

  1. queueSEND_TO_BACK 后向入队
  2. queueSEND_TO_FRONT 前向入队
  3. queueOVERWRITE 覆写入队

返回值:pdTRUE,向队列发送消息成功;errQUEUE_FULL,队列满,向队列发送消息失败。

2.3出队操作

从队列中获取数据,需要用到出队操作。出队操作函数有xQueueReceive()xQueuePeek()两个,它们的中断版本是xQueueReceiveFromISR()xQueuePeekFromISR()

2.3.1xQueueReceive()

它用于从指定的队列中读取数据,读取成功后会将队列中的这个数据删除。原型函数如下:

BaseType_t xQueueReceive( QueueHandle_t xQueue,
                          void * const pvBuffer,
                          TickType_t xTicksToWait);

参数说明:
xQueue:队列句柄,指明要从哪个队列中接收数据。

pvBuffer:指向保存数据的缓存区,以数据复制的形式保存到缓冲区中。

xTicksToWait:任务阻塞超时时间,该值为0且队列空时不阻塞,函数立即返回。该值为portMAX_DELAY且队列满时将无限期阻塞,但要将宏INCLUDE_vTaskSuspend设置为1.其他值为队列空时任务阻塞的系统时钟节拍。

返回值:pdTRUE,成功从指定队列中读取到消息;pdFALSE,从指定队列中读取消息失败。

2.3.2xQueuePeek()

它用于从指定的队列中读取消息,读取成功后并不删除队列中的这个数据。原型如下:

BaseType_t xQueuePeek(QueueHandle_t xQueue,
                      void * const pvBuffer,
                      TickType_t xTicksToWait);

参数说明:

xQueue:队列句柄,指明要从哪个队列中读取数据。

pvBuffer:指向保存数据缓冲区,以数据复制的形式保存到缓冲区中。

xTicksToWait:任务阻塞超时时间,该值为0且队列满时不阻塞,函数立即返回。该值为portMAX_DELAY且队列空时将无限期阻塞,但要将宏INCLUDE_vTaskSuspend设置为1.其他值为队列空时任务阻塞的系统时钟节拍。

返回值:pdTRUE,成功从指定队列中读取到消息;pdFALSE,从指定队列中读取消息失败。

2.3.3xQueueReceiveFromISR()

这个函数用于在中断服务中从指定的队列中读取消息,读取成功后会将队列中的这个数据删除。原型如下:

BaseType_t xQueueReceiveFromISR( QueueHandle_t xQueue,
                                 void * const pvBuffer,
                                 BaseType_t * const pxHigherPriorityTaskWoken);

参数说明:

xQueue:队列句柄,指明要从哪个队列中接收数据。

pvBuffer:指向保存数据缓冲区,以数据复制的形式保存到缓冲区中

pxHigherPriorityTaskWoken:指向一个用于保存调用函数后是否进行任务切换的变量,若执行函数后值为pdTRUE,则要在退出中断服务函数后执行一次任务切换。

返回值:pdTRUE,成功从指定队列中读取到消息;pdFALSE,从指定队列中读取消息失败。

2.3.4xQueuePeekFromISR()

这个函数用于在中断服务函数中从指定的队列中读取消息,读取成功后并不删除队列中这个数据。原型如下:

BaseType_t xQueuePeekFromISR( QueueHandle_t xQueue,
                              void * const pvBuffer);

参数说明:

xQueue:队列句柄,指明要从哪个队列中接收数据

pvBuffer:指明保存数据的缓冲区,以数据复制的形式保存到缓冲区中

返回值:pdTRUE,成功从指定队列中读取到消息;pdFALSE,从指定队列中读取消息失败。

2.4其他队列操作函数
函数功能
vQueueDelete()删除队列中并释放内存
uxQueueMessagesWaiting()查询队列中的消息数量
uxQueueMessagesWaitingFromISR()查询队列中的消息数量中断版本
uxQueueSpacesAvailable()查询队列中空闲空间
xQueueReset()将队列重置为原始的空间状态
vQueueAddToRegistry()为队列分配名称并添加到队列注册表中
pcQueueGetName()通过队列句柄查询队列名
vQueueUnregisterQueue()从队列注册表中删除队列
xQueueIsQueueEmptyFromISR()在中断服务函数中查询队列是否为空
xQueueIsQueueFullFromISR()在中断服务函数中查询队列是否为满

** “ISR”(中断服务程序)**

3.用队列实现串口守护任务

在之前实验过程中常常用到串口发送信息,但当多个任务同时访问串口时,就会发生资源冲突,造成数据混乱。

之前做法,要么限制只有一个任务能运行,要么在访问串口时用临界段代码保护或挂起调度器的方式进行代码保护。这种解决多个任务同时访问某个资源的方法叫做互斥访问。

3.1守护任务

守护任务是对某个资源具有唯一所有权的任务。只有守护任务才可以直接访问其守护的子夜,其他任务要访问该资源只能间接地通过守护任务提供的服务实现。守护任务提供了一种干净利落的方法来实现互斥功能,不用担心会发生优先级反转和死锁问题。

3.2串口守护任务示例

printTask()Test12Task()Test13Task()函数。

其中任务12是LED闪烁,统计任务12运行次数,并将任务12的运行总节拍数通过队列传给串口守护任务打印。

其中任务13是LED闪烁,统计任务13运行次数,并将任务13的运行总节拍数通过队列传给串口守护任务打印。

printTask是串口守护程序,其功能是将通过队列传送过来的字符信息在串口上输出,任何时候只有该守护任务才能访问串口。

xQueueSendToBack(xQueuePrint, pcToPrint,0);

将生成的待打印输出信息 pcToPrint 发送到名为 xQueuePrint 的队列中,其中0是超时参数。

问题

  1. 什么是入队?它有什么特点?

入队是一种数据结构操作,通常用于将元素添加到队列(或者称为先进先出队列,FIFO队列)的末尾。队列是一种线性数据结构,遵循先进先出(First-In-First-Out)的原则,即最先进入队列的元素将首先被移出队列。

入队的特点包括:

1. 添加元素:入队操作将一个新元素添加到队列的末尾。这个元素成为队列的新尾部。
2. 先进先出:队列遵循FIFO原则,即最先入队的元素将首先被移出队列。新元素总是添加到队列的末尾,并且只有在前面的元素都被移出队列后才能被访问。
3. 线性结构:队列是一种线性数据结构,元素之间存在顺序关系。这种顺序性决定了入队和出队的顺序。
4. 常用于任务调度:队列常常用于多任务环境中,用于任务之间的通信和数据传递。任务可以通过将数据入队,然后其他任务可以出队并处理这些数据。
5. 安全性:队列通常具有一定的安全性,即一次只允许一个任务访问队列的入队和出队操作,以防止竞争条件。
  1. 估算创建一个队列长度为1的FreeRTOS队列所需的最小内存?

  2. 队列控制结构内存: 队列需要一些内存来存储其控制结构,包括队列的当前长度、队列的最大长度等信息。这个内存大小通常很小,通常是几个字节。

  3. 队列元素内存: 您需要为队列中存储的每个元素分配内存。如果队列中每个元素的大小是固定的,那么您只需为一个元素分配内存。如果队列中的元素大小不固定,那么您需要为每个元素分配足够的内存以容纳最大可能的元素。

  4. 内存对齐: 在某些体系结构上,内存可能需要按特定的边界对齐,因此您可能需要额外的内存来满足对齐要求。

  5. FreeRTOS版本和配置: 不同版本的FreeRTOS可能会在内部使用不同的数据结构和方法,因此内存需求可能会有所不同。此外,您的FreeRTOS配置也会影响内存需求,例如,是否启用了统计信息、调试功能等。

由于这些因素的影响,难以提供精确的数字。但是,一般来说,一个长度为1的FreeRTOS队列的最小内存需求通常在几十字节到一百字节之间。

FreeRTOS信号量与任务同步

信号量是操作系统用来实现资源管理和任务同步的消息机制。

FreeRTOS信号量分为:二值信号量、计数信号量、互斥信号量和递归信号量。

可以把互斥信号量看作一种特殊的二值信号量,但互斥信号量和二值信号量之间还是有一些区别的。

(1) 使用目的不同:二值信号量用于同步,可实现任务和任务之间及任务和中断之间的同步。互斥信号用于自锁,保证在同一时间只有一个任务访问某个资源。

(2) 操作方法不同:二值信号量在用于同步时,一般是一个任务(或中断)给出信号,另一个任务获取信号。互斥信号量必须在同一个任务中获取并在同一个任务中给出信号。

(3) 使用场所不同:互斥信号量具有优先级继承机制,而二值信号量没有。互斥信号量不能用于在中断服务函数中,二二值信号量可以。

(4) 创建方法不同:用于创建互斥信号量和用于创建二值信号量的API函数不同,但是获取和给出信号的API函数相同。

1二值信号量

二值信号量相当于只有一个队列项的队列,创建二值信号量与创建队列使用的是同一个函数。二值信号量只关心这个特殊的队列状态,要不为空,要不为满,并不关心队列中存放的是什么消息。

二值信号量主要用于同步,可实现任务与任务之间和中断之间的同步。二值信号量用于实现任务和中断之间同步的工作过程如下。

4.任务因请求信号量再次进入阻塞态。

通常任务函数都是一个大循环,在任务处理完相关事务后,又会再次调用xSemaphoreTake()函数试图再次获取信号量,但此时二值信号量已无效,任务在进入阻塞态。

1.1创建二值信号量

创建二值信号量可使用两个宏:xSemaphoreCreateBinary()xSemaphoreCreateBinary()。这两个宏最终被队列创建函数xQueueGenericCreate()替换,使用这两个宏创建二值信号量,需要将宏configSUPPORT_DYNAMIC_ALLOCATION设置为1.

1.1.1vSemaphoreCreateBinary()

该函数是旧版本的FreeRTOS使用的二值信号量创建宏,在semohr.h文件中定义

函数原型如下:

void vSemaphoreCreateBinary(SemaphoreHandle_t xSemaphore)

xSemaphore用来保存创建成功的二值信号量的句柄。实际用于创建二值信号量的是队列创建函数xQueueGenericCreate(),创建成功后会返回这个二值信号量的句柄且二值信号量默认有效,创建失败则返回NULL。

1.1.2xSemaphoreCreateBinary()

这是新版本的FreeRTOS使用的二值信号量创建宏,同样通过调用队列创建函数xQueueGenericCreate()来创建二值信号量,只不过创建成功后并没有释放此二值信号量。

#define xSemaphoreCreateBinary()		  \
	xQueueGenericCreate( ( UBseType_t ) 1, \
	semSEMAPHORE_QUEUE_ITEM_LENGTH,		   \
	queueQUEUE_TYPE_BINARY_SEMAPHORE );	   \

该函数如果成功创建了二值信号量,则返回这个二值信号量的句柄,创建失败返回NULL。

1.2释放二值信号量
1.2.1xSemaphoreGive()

xSemaphoreGive()是一个宏,用于释放二值信号量,最终实现功能的是队列发送函数xQueueGenericSend()

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

该宏有一个参数xSemaphore,指明要释放的信号量句柄。该宏不仅可用于释放二值信号量,还可用于释放技术信号量。释放成功则返回pdTRUE,释放失败则返回pdFALSE.

1.2.2xSemaphoreGiveFromISR()

这是释放信号量的宏的中断版本,在中断服务函数中使用,同样也是一个宏。该宏可用于释放二值信号量和技术信号量,但不能用于释放互斥信号量。最终实现功能的是xQueueGiveFromISR()函数。

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

参数说明:

xSemaphore:信号量句柄

pxHigherPriorityTaskWoken:指向一个用于保存调用函数后是否进行任务切换的变量,若执行函数后值为pdTRUE,则要在退出中断服务函数后执行一次任务切换。

返回值:pdTRUE,成功释放信号量;errQUEUE_FULL,在指定阻塞时间内释放信号量失败。

1.3获取二值信号量
1.3.1xSemaphoreTake()

它是一个宏,用于获取二值信号量。该宏还可用于获取技术信号量和互斥信号量。定义如下:

#define xSemaphoreTake( xSemaphore, xBlockTime )   xQueueSemaphoreTake( ( xSemaphore ), ( xBlockTime ) )

参数说明如下:

xSemaphore:信号量句柄

xBlockTime:任务阻塞超时时间,当信号量无效时,若该值为0,则函数立即返回;若该值为portMAX_DELAY,则任务将无限期阻塞,但要将宏INCLUDE_vTaskSuspend设置为1.其他值为任务阻塞的系统时钟节拍。

返回值:pdTRUE,成功获取信号量;pdFALSE,在指定阻塞时间内获取信号量失败。

最终实现功能的是xQueueSemaphoreTake()函数,原型如下:

# BaseType_t xQueueSemaphoreTake( QueueHandle_t xQueue, TickType_t xTicksToWait );
1.3.2xSemaphoreTakeFromISR()

这是用来获取信号量的宏的中断版本,在中断服务函数使用,同样也是一个宏。该宏可以用来获取二值信号量和计数信号量,但不能用于获取互斥信号量。最终实现功能的是xQueueReceiveFromISR()函数。

#define xSemaphoreFromISR( xSemaphore, pxHigherPriorityTaskWoken )  xQueueReceiveFromISR( ( QueueHandle_t) ( xSemaphore ), NULL, ( pxHigherPriorityTaskWoken ) )

参数说明:

xSemaphore:信号量句柄

pxHigherPriorityTaskWoken:指向一个用于保存调用函数后是否进行任务切换的变量,若执行函数后值为pdTRUE,则要在退出中断服务函数后执行一次任务切换。

返回值:pdTRUE,成功释放信号量;pdFALSE,在指定阻塞时间内释放信号量失败。

1.4用二值信号量进行任务同步

本示例通过对二值信号量的操作,实现任务与任务之间及任务与中断之间的同步。

任务14功能是使LED1闪烁,优先级3,通过二值信号量实现任务与任务之间的同步,输出信息到串口。

任务15功能是使LED2闪烁,优先级3,通过二值信号量实现任务与中断之间的同步,输出信息到串口。

使用上一章的printTask()串口守护任务,功能通过队列传送过来的字符信息在串口上输出,任何时候只有该守护任务能访问串口。

使用keyTask(),按键扫描,优先级4,并根据返回的键值执行释放二值信号量、启动定时器等操作。

  1. WAKEUP按键用于释放任务与任务之间同步的二值信号量
  2. KEY0用于启动TIM3,利用TIM2的更新中断释放任务与终端之间同步的二值信号量
  3. KEY1用于停止TIM2

2计数信号量

计数信号量相当于长度大于1的队列,主要用于时间计数和资源管理。当计数信号量用于事件计数时,初值一般为0,事件处理函数释放一次信号量其值加1,其他任务获取信号其值减1,。当计数信号量用于资源管理时,信号量代码资源可用的数量,初值为可用资源的最大值。

2.1创建计数信号量

xSemaphoreCreateCounting()是用于动态创建计数信号量的宏,其定义如下:

#define xSemaphoreCreateCouting( uxMaxCount, uxInitialCount )  xQueueCreateCountingSwmaphore( ( uxMaxCount ),( uxInitialCount ) )

有两个参数uxMaxCountuxInitialCount,用于给创建的计数信号量指定最大计数值和初值,实例用于创建计数信号量的是xQueueCreateCountingSemaphore()函数,创建成功返回这个信号的句柄,失败返回NULL。

还有一个静态创建计数信号量的宏xSemaphoreCreateCountingStatic(),在使用该宏创建计数信号量时需要由用户分配所需内存。

2.2计数信号量的释放和获取
2.2.1信号量进行任务同步

示例代码如下:

void appStartTask(void)
{
	/*创建一个长度为2,队列项大小足够容纳待输出字符队列*/
    xQueuePrint = xQueueCreate(2,sizeof(pcToPrint));
    /*创建两个二值信号量,一个用于进行任务与任务,另一个用于任务与中断之间的同步*/
    binKeySemaphore = xSemaphoreCreateBinary();
    /*创建一个技术信号量,初值为0,用于进行任务与中断之间的同步*/
    cntIRQSemaphore = xSemaphoreCreateCounting(255,0);
    if(xQueuePrint && binKeySemaphore && cntIRQSemaphore )
    {
		taskENTER_CRITICAL();//关中断
        xTaskCreate(Led0Task,"Led0Task",128,NULL,3,&Led0TaskHandle);
        xTaskCreate(printTask,"printTask",128,NULL,3,&printTaskHandle);
        xTaskCreate(keyTask,"keyTask",128,NULL,4,&keyTaskHandle);\
         taskEXIT_CRITICAL();
        vTaskStartScheduler();
    }
}

3互斥信号量

互斥信号量是一种特殊的二值信号量。用于葵涌在两个或多个任务之间访问共享资源。互斥信号量提供一种优先级继承机制,让持有互斥信号量的任务优先级提升到等待这个互斥信号量的任务优先级。与二值信号量主要用于同步不同,互斥信号量主要用于互斥访问。除优先级继承机制外,二者的区别主要在于信号量被获取后发生的事情。

用于互斥的信号量必须归还。

用于同步的信号量通常在完成同步之后便丢弃,不在归还。

互斥信号量在多个任务资源共享上相当于与共享资源关联的令牌(成为令牌拥有者)。令牌持有者在完成资源使用后,必须马上归还(Give)。只有归还了令牌,其他任务才可能成功持有令牌,也才可能安全的访问该共享资源。一个任务除非持有令牌,否则不允许共享资源。

3.1创建互斥信号量

xSemaphoreCreateMutex()使用动态内存分配方法来创建互斥信号量,其定义如下:

#define xSemaphoreCreateMutex() xQueueCreateMutex( queueQUEUE_TYPE_MUTEX)

创建成功返回任务语柄,失败返回NULL。

还有一个静态创建互斥信号量的宏xSemaphoreCreateMutexStatic()

3.2互斥信号量的释放与获取

因为有优先级继承机制,互斥信号量不能用于在中断服务函数中。在任务中使用互斥信号量,其释放和获取与二值信号量完全相同,使用相同的释放与获取函数。

3.3优先级翻转

优先级翻转就是指在任务的事务处理顺序上,高优先级任务的事务处理反而滞后于低优先级任务的事务处理。

3.4优先级翻转示例

低优先级任务:优先级1,lowTask(),任务运行获取信号量,同时将运行信息送往串口 。

中优先级任务:优先级为2,midTask(),任务简单地将运行信息送往串口。

高优先级任务:优先级3,highTask(),任务运行需要获取与低优先级任务相同的信号量,同时将运行信息送往串口。

串口守护任务:优先级4,printTask(),其功能是将通过队列传送过来的字符信息从串口输出,任何时候只有串口守护任务能访问串口。

4递归互斥信号量

递归互斥信号量是一种特殊的互斥信号量,已经获取了递归互斥信号量的任务可以重复获取这个递归信号量,而且没有次数的限制。

递归互斥信号量操作函数如下:

xSemaphoreCreateRecursiveMutex()用动态方法创建递归互斥信号量

xSemaphoreCreateRecursiveMutexStatic()用静态方法创建递归信号量

xSemaphoreGiveRecursive()释放递归互斥信号量

xSemaphoreTaskRecursive()获取递归互斥信号量

9 FreeRTOS事件标志组

信号量用以同步任务,但使用信号量只能同步单个任务或事件,有时候某个任务可能需要与多个任务或事件同步,信号量无法满足这个要求。因此FreeRTOS提供了事件标志组用来实现多个任务的同步。

9.1事件标志组

用一个二进制位来表示一个事件,这个二进制位为1表明发生了对应事件,为0表明没有发生事件,这样多个二进制位组合在一起就可以用来表示事件标志组。

在FreeRTOS中,事件标志组中的所有事件标志位使用一个EventBits_t数据类型来存储。在FreeRTOS中值使用第24位用来存储事件标志位,故事件标志组最多只能存储24个事件。

9.1.1创建时间标志组

xEventGroupCreate()函数用来创建事件标志组。

EventGoupHandle_t xEventGroupCreate( void )

创建成功返回标志组句柄,失败返回NULL,创建失败往往是由于不能动态申请到内存引起。

还有一个静态创建事件标志组的函数xEventGroupCreateStatic(),需要自行分配所需内存。

9.1.2设置事件标志位

设置事件标志位涉及两种操作:置1和清0。

1. xEventGroupClearBits()

该函数为事件标志位清0函数。

EventBits_t xEventGroupClearBIts(EventGroupHandle_t xEventGroup, const EventBits_t uxBitsToClear );

参数说明:

xEventGroup:事件标志组句柄,指明要操作哪个事件标志组。

uxBitsToClear:要清0的事件标志位,可以多位同时清0

返回值:指定事件标志位清0之前的事件标志组值。

2.xEventGroupClearBitsFromISR()

该函数是中断服务函数中用于事件标志位清0错的函数。

EventBits_t xEventGroupClearBItsFromISR(EventGroupHandle_t xEventGroup, const EventBits_t uxBitsToClear );

参数说明:

xEventGroup:事件标志组句柄,指明要操作哪个事件标志组。

uxBitsToClear:要清0的事件标志位,可以多位同时清0

返回值:pdPASS,指定事件标志位清0成功;pdFALSE,指定事件标志位清0失败

3.xEventGroupSetBits()

该函数是事件标志位 置1函数。

EventBits_t xEventGroupSetBits( EventGroupHandle_t xEventGroup, const EventBits_t uxBitsToSet );

参数说明:

xEventGroup:事件标志组句柄,指明要操作那个事件标志组。

uxBitsToSet:要置1的时间标志位,可以多位同时置1.

返回值:指定事件标志位 置1后的时间标志组值。

4.xEventGroupSetBitsFromISR()

该函数是中断服务函数中用于事件标志位 置1的函数。

EventBits_t xEventGroupSetBitsFromISR( EventGroupHandle_t xEventGroup, const EventBits_t uxBitsToSet, BaseType_t *pxHigherPriorityTaskWoken );

参数说明:

xEventGroup:事件标志组句柄,指明要操作那个事件标志组。

uxBitsToSet:要置1的时间标志位,可以多位同时置1.

pxHigherPriorityTaskWoken:指向一个用于保存调用函数后是否进行任务切换的变量,若执行函数后值为pdTRUE,则要在退出中断服务函数后执行一次任务切换。

返回值:pdPASS,指定事件标志位 置1成功;pdFALSE,指定事件标志位 置1失败。

** 注意: 若使用中断版本的时间标志位 置1或清0函数,则需要将宏configUSE_TRACE_FACILITYINCLUDE_xTimerPendFunctionCallconfigUSE_TIMERS都设置为1,这样函数才会编译。

9.1.3获取事件标志组值
1.xEventGroupGetBits()

该函数是获取事件标志组当前值函数

#define xEventGroupGetBits( xEventGroup )       xEventGroupClearBits( xEventGroup, 0 );

参数说明:

xEventGroup:事件标志组句柄,指明要操作那个事件标志组

返回值:时间标志组当前值。

实际实现功能的是时间标志组指定位清0函数,xEventGroupBits(),利用这个函数返回清0指定事件标志位之前的时间标志组值来完成,传入参数0表示某一哪个事件标志位被清0.

2.xEventGroupGetBitsFromISR()

该函数是中断服务函数用于获取事件标志组当前值的函数。

EventBits_t xEventGroupGetBitsFromISR( EventGroupHandle_t xEventGroup );

参数说明:

xEventGroup:事件标志组句柄,指明要操作哪个事件标志组。

返回值:pdPASS,指定事件标志位 置1;pdFALSE,指定事件标志位 置1.失败

9.1.4等待指定的事件标志位

某个任务可能需要与多个事件进行同步,可使用xEventGroupWaitBits()函数判断多个事件标志。

EventBits_t xEventGroupWaitBits( EventGroupHandle_t xEventGroup, const EventBits_t uxBitsToWaitFor, const BaseType_t xClearOnExit, const BaseType_t xWaitForAllBits, TickType_t xTicksToWait );

参数说明:

xEventGroup:事件标志组句柄,指明要操作哪个事件标志组。

uxBitsToWaitFor:指明要等待的时间标志位,可以多位同时操作

xClearOnExit:退出函数前,对指定要等待的时间标志位进行何种操作,传入pdTRUE清0,传入pdFALSE保持不变。

xWaitForAllBits:指定函数返回时机,传入pdTRUE表示指定要等待的所有事件标志位都为1或阻塞超时才返回,传入pdFALSE表示要等待的事件标志位有一个为1或阻塞超时就返回。

xTicksToWait:设置阻塞超时时间,单位为系统时钟节拍。

返回值:返回要等待时间标志位 置1之后的事件标志组值,若因阻塞超时返回,则返回值将没有任何意义。

9.2用事件标志组进行任务同步

任务1的任务函数为Test16Task(),优先级为3,使LED闪烁,指示程序正常运行。

任务2的任务函数为Test17Task(),优先级为3,检测事件标志位,bit2,bit1和bit0同时置1时点亮LED,并输出信息到串口。

任务3是串口守护函数,优先级3,通过队列传送过来的字符信息在串口显示,任何时候只有该守护任务访问串口。

任务4是按键扫描任务,优先级4.

问题

什么是事件标志组?有什么用?

是一种用于任务间通信和同步的功能。它们提供了一种机制,使任务能够等待、设置和清除特定的事件标志,以便在特定条件发生时进行协调和同步。以下是FreeRTOS中事件标志组的主要概念和用途:

  1. 事件标志组表示多个事件或状态:一个事件标志组是一个32位的变量,可以看作是由32个二进制位组成的。每个位都可以表示一个不同的事件、状态或条件。这意味着一个事件标志组可以表示多达32个不同的事件。
  2. 任务等待事件标志:任务可以等待事件标志组中的一个或多个特定位(事件)。任务可以进入阻塞状态,直到所等待的事件中的任何一个或所有事件都发生为止。
  3. 事件通知:一个任务可以通过设置事件标志组中的一个或多个位来通知其他任务特定的事件已经发生。这是任务之间进行通信和同步的一种方式,而不需要共享全局变量或进行复杂的消息传递。
  4. 等待条件:任务可以等待多个不同的事件,只有当所有条件都满足时才会被唤醒。这可以用于等待多个相关条件的同时发生。
  5. 清除事件标志:一旦任务检查了事件标志,它们可以选择清除特定位的标志,以便其他任务不再等待相同的事件。
  6. 轻量级同步:与互斥量和信号量相比,事件标志组通常更轻量级,因为它们只涉及位操作,而不需要任务切换。
  7. 优先级控制:任务可以与事件标志组一起使用,以便在发生事件时,只有具有足够高优先级的任务被唤醒。这有助于实现任务的优先级调度。

10 FreeRTOS任务通知

任务通知是一个事件。每个TCB中有一个32位的成员变量ulNotifiedValue专门用于任务通知。任务通知可以在某些场合代替信号量,事件标志组等,并且拥有更高的执行频率。

接收任务通知的任务可因等待任务通知而进入阻塞态,在其他任务向这个任务发送任务通知后解除阻塞。根据FreeRTOS文档,使用任务通知相较于使用信号量和事件标志组,唤醒内阻塞任务时间的速度提示45%,并且使用的RAM空间更少。但也有一定的局限:

  1. 智能有一个接收任务通知的任务
  2. 只有接收任务通知的任务你进入阻塞态,发送任务不会因任务通知发送失败而阻塞。

10.1发送和获取任务通知

用于获取任务通知的API:
ulTaskNotifyTake()xTaskNotifyWait()。用于获取任务通知的API函数不能用于中断服务函数,没有对应中断版本。

10.1.1发送任务通知
10.1.1.1xTaskNotify()

它被用于指定任务通知值发送给指定任务,并指定任务通知更新方法,真正实现功能的是xTaskGenericNotify()函数,定义如下:

#define xTaskNotify( xTaskToNotify, ulValue, eAction)			xTaskGenericNotify( ( xTaskToNotify ),	( ulValue ),	( eAction ),	NULL )

参数如下:

xTaskToNotify:任务句柄,指定接收任务通知的任务

ulValue:任务通知值

eAction:任务通知更新方法,是一个枚举类型,取值如下:

  1. eNoAction 无动作
  2. eSetBits 更新指定位
  3. eIncrement 通知值加一
  4. eSetValueWithOverwrite 覆写方式更新通知值
  5. eSetValueWithoutOverwrite 非覆写方式更新通知值

返回值:当eActioneSetValueWithoutOverwrite且任务通知没有更新时返回paFAIL;当eAction设为其他项时,返回pdPASS.

10.1.1.2xTaskNotufyGive()

它用于将任务通知值简单加一后发送给指定任务,真正实现功能的是xTaskGenericNotify(),定义如下:

#define xTaskNotifyGive( xTaskToNotify )		xTaskGenericNotify( ( xTaskToNotify ), ( 0 ), eIncrement, NULL)

参数说明:

xTaskToNotify:任务句柄,指定接收任务通知的任务。

返回值:pdPASS

10.1.1.3xTaskNotifyAndQuery()

它用于将指定的任务通知值发送给指定任务,并且保存接收任务的任务通知原值,真正实现功能的是xTaskGenericNotify()函数,定义如下:

#define xTaskNotifyAndQuery( xTaskToNotify, ulValue, eAction, pulPreviousNotifyValue )		xTaskGenericNotify( ( xTaskToNotify ), ( ulValue ), ( eAction ), )

参数说明:

xTaskToNotify:任务句柄,指定接收通知的任务

ulValue:任务通知值

eAction:任务通知更新方法,是一个枚举类型

pulPreviousNotifyValue:用来保存更新前任务通知值。

返回值:当eActioneSetValueWithoutOverwrite且任务通知没有更新时返回paFAIL;当eAction设为其他项时,返回pdPASS.

除了上面3个发送任务通知的函数之外,还有3个对应的中断版本,都以FromISR结尾,用在服务中断函数中,主要增加了退出后是否需要进行任务切换。

10.1.2获取任务通知
10.1.2.1ulTaskNotifyTake()

它为简单获取任务通知函数,可以指定函数退出时的行为,以及调用次函数任务的阻塞时间。

uint32_t ulTaskNotifyTake( BaseType_t xClearCountOnExit, TickType_t xTicksToWait );

参数说明:

xClearCountOnExit:函数退出时对任务通知值的操作,传入pdTRUE参数退出函数时任务通知值清0,传入pdFALSE参数退出函数时任务通知值减1.

xTicksToWait:获取通知任务的阻塞时间

返回值:任务通知值

10.1.2.2xTaskNotifyWait()

它为获取任务通知函数,可以指定函数退出时的行为,调用次函数得物的阻塞时间,并且保存接收任务的任务通知原值。

BaseType_t xTaskNotifyWait( uint32_t xClearCountOnEntry,
						uint32_t ulBitsToClearOnExit,
						uint32_t *pulNotificationValue,
						TickType_t xTicksToWait)

xClearCountOnEntry:没有收到任务通知,将次参数取反值与任务通知值进行按位与。

ulBitsToClearOnExit:收到任务通知,退出函数前将次参数取反值与任务通知值进行按位与。

pulNotificationValue:指向用于保存任务通知的变量

xTicksToWait:获取通知任务的阻塞时间

返回值:pdTRUE,获取任务通知成功,pdFALSE获取失败

10.2任务通知使用

10.2.1用任务通知模拟二值信号量
static void Test18Task(void *pvParameters)
{
	uint32_t ulNotifyValue;		// 保存任务通知值
	while(1)
	{
		HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_6);
		vTaskDelay(pdMS_TO_TICKS(500));
		
		/*获取通过按键发送的任务通知,调用后任务通知值清0*/
		ulNotifyValue = ulTaskNotifyTake(pdTRUE,10);
		if(ulNotifyValue)
		{
			sprintf(pcToPrint,"按键任务通过任务通知同步任务1\r\n");
			xQueueSendToBack(xQueuePrint,pcToPrint,0);
		}
	}
}

static void Test19Task(void *pvParameters)
{
	uint32_t ulNotifyValue;		// 保存任务通知值
	while(1)
	{
		HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_7);
		vTaskDelay(pdMS_TO_TICKS(500));
		
		/*获取通过中断发送的任务通知,调用函数后任务通知值清0*/
		ulNotifyValue = ulTaskNotifyTake(pdTRUE,10);
		if(ulNotifyValue)
		{
			sprintf(pcToPrint,"TIM6中断通过任务通知同步任务2,中断%3d次\r\n\r\n",uIRQCounter);
			xQueueSendToBack(xQueuePrint, pcToPrint,0);
		}
	}
}

static void key2Task(void *pvParameters)
{
	uint8_t keyValue;			// 键值
	extern TIM_HandleTypeDef htim6;
	while(1)
	{
		keyValue = KeyScan(0);		// 获取键值
		if(keyValue == KEY0_PRES)
		{
			HAL_TIM_Base_Start_IT(&htim6);
			sprintf(pcToPrint,"KEY0按下,启动TIM6中断...\r\n");
			xQueueSendToBack(xQueuePrint, pcToPrint,0);
			
			/*在定时器TIM6中断中,通过发送任务通知给任务2进行同步*/
		}
		else if(keyValue == KEY1_PRES)
		{
			HAL_TIM_Base_Stop_IT(&htim6);
			sprintf(pcToPrint,"KEY1按下,停止TIM6...\r\n");
			xQueueSendToBack(xQueuePrint,pcToPrint,0);
		}
		else if(keyValue == KWUP_PRES)
		{
			sprintf(pcToPrint,"KEY_UP按下,发送任务通知...\r\n");
			xQueueSendToBack(xQueuePrint,pcToPrint,0);
			
			/*通过发送任务通知给任务1进行同步*/
			xTaskNotifyGive(Test18TaskHandle);
		}
		vTaskDelay(pdMS_TO_TICKS(100));
	}
}
			

总结

使用任务通知可以提升运行速度、减少RAM的消耗,可用于在轻量级得到使用场合下替换二值信号量、计数信号量、事件标志组等。

11FreeRTOS软件定时器

软件定时器允许在设定时间到来之后执行指定功能,这个执行指定功能的函数被称为软件定时器回调函数。

从软件定时器启动到执行软件定时器回调函数之间的时间被称为软件定时周期。

简单说:就是定时时间到了,执行回调函数。

111.1软件定时器服务任务

configUSE_TIMERS为1时,在调度器启动的时候会自动创建一个软件定时器服务任务(软件定时器守护任务),软件定时器服务任务的堆栈大小、优先级、命令队列长度等也要在配置文件中配置。

软件定时器的API函数通过队列发送命令给软件定时器私有的,用户不能直接访问,但它是用户任务与软件定时器服务之间沟通的纽带。

111.2软件定时器的操作

11.2.1单次定时与周期定时

单次定时:值用户创建并开启软件定时器后,达到定时时间执行一次软件定时器回调函数,然后就会停止。

周期定时:指此软件定时器会按设置好的定时时间周期性地执行软件定时器回调函数,直到调用软件定时器停止函数为止。

** 注意:软件定时器回调函数不能使用会导致任务阻塞的API函数,如vTaskDelay()

11.2.2创建软件定时器
11.2.2.1xTimerCreate()

用于动态创建一个软件定时器,所需要的内存通过动态内存分配方法获取。

TimerHandle_t xTimerCreateStatic( const char * const pcTimerName, /*lint !e971 Unqualified char types are allowed for strings and single characters only. */
                                          const TickType_t xTimerPeriodInTicks,
                                          const BaseType_t xAutoReload,
                                          void * const pvTimerID,
                                          TimerCallbackFunction_t pxCallbackFunction,
                                          StaticTimer_t * pxTimerBuffer )

参数说明:

pcTimerName:软件定时器的名字

xTimerPeriodInTicks:定时周期,单位是系统时钟节拍

xAutoReload:软件定时模式,传入pdTRUE,是创建周期软定时器,pdFALSE是单次软件定时器

pvTimerID:定时器ID,用于标识当多个软件定时器使用同一个回调函数时是哪个软件定时器引起的回调

pxTimerBuffer:软件定时器回调函数

返回值:创建成功返回软件定时器句柄,失败返回NULL

11.2.2.2xTimerCreateStatic()

用于静态创建一个软件定时器。刚创建好的软件定时器处于未运行状态。

TimerHandle_t xTimerCreateStatic( const char * const pcTimerName, /*lint !e971 Unqualified char types are allowed for strings and single characters only. */
                                          const TickType_t xTimerPeriodInTicks,
                                          const BaseType_t xAutoReload,
                                          void * const pvTimerID,
                                          TimerCallbackFunction_t pxCallbackFunction,
                                          StaticTimer_t * pxTimerBuffer )

参数说明:

pcTimerName:软件定时器的名字

xTimerPeriodInTicks:定时周期,单位是系统时钟节拍

xAutoReload:软件定时模式,传入pdTRUE,是创建周期软定时器,pdFALSE是单次软件定时器

pvTimerID:定时器ID,用于标识当多个软件定时器使用同一个回调函数时是哪个软件定时器引起的回调

pxTimerBuffer:软件定时器回调函数

pxTimerBuffer:指向一个StatciTimer_t类型的结构体

返回值:创建成功返回软件定时器句柄,失败返回NULL

11.2.3启动软件定时器

刚创建好的软件定时器处于未运行状态。

11.2.3.1xTimerStart()

它为软件定时器启动函数。

实际实现功能是xTimerGenericCommand()

创建成功返回pdPASS,失败pdFAIL

11.2.3.2xTimerStartFromISR()

是软件定时器启动的中断版本。

创建成功返回pdPASS,失败pdFAIL

11.2.4停止软件定时器

软件定时器一旦启动就会不断重复运行,知道调用软件定时器停止API为止。

11.2.4.1xTimerStop()

实际实现功能是xTimerGenericCommand()

创建成功返回pdPASS,失败pdFAIL

11.2.4.2xTimerStopISR()

它的中断版本

实际实现功能是xTimerGenericCommand()

创建成功返回pdPASS,失败pdFAIL

12FreeRTOS内存管理

内存管理是FreeRTOS非常重要的功能,前面讲述的任务,消息队列,信号量,事件标志组及软件定时器等在创建时又两种方法:动态内存分配;用户指定内存的静态方法。

使用动态内存分配方法所需要的内存就是从FreeRTOS所管理的内存区进行分配。

12.1内存分配方法

FreeRTOS支持5中方法,分别通过文件heap_1,heap_2,heap_3heap_5实现,在实际使用中,只需要使用其中的一种即可。

heap_3动态内存管理之外,FreeRTOS均通过内存堆ucHeap[]来管理内存,内存堆的大小为宏configTOTAL_HEAP_SIZE所设的的大小。

在程序运行过程中使用xPortGetFreeHeapSize()函数获取动态剩余内存堆大小。

12.1.1 heap_1.c动态内存管理方法

这种发放是5种中最简单的一种,采用这种方法,动态内存一旦申请就不允许释放。

特性:

  1. 项目应用不需要删除任务、信号量、消息队列等已经创建好的资源
  2. 具有时间确定性,即申请动态内存的时间是固定的,并不会产生内存碎片
  3. 代码实现简单
12.1.2 heap_2.c动态内存管理方法

它使用了最适应算法,并支持内存释放。但不支持鸟村碎片整理。

特性:

  1. 在不考虑内存碎片的情况下,heap_2支持重复的任务、信号量、事件标志组、软件定时器等内部资源的创建和删除。
  2. 如果用户申请和释放的动态内存大小是随机的,可变的,那么不要使用此方法。因为容易产生内存碎片
  3. 不具有事件确定性。
12.1.3 heap_3.c动态内存管理方法

heap_3.c动态内存管理方法实现的动态内存管理是对编译器提供的malloc()函数和free()函数进行简单封装,额外做了线程保护

特性:

  1. 需要编译提供malloc()和free()函数。
  2. 不具有事件确定性,即申请动态内存的时间不固定
  3. 可能增加FreeRTOS内核代码量
12.1.4 heap_4.c动态内存管理方法

与heap_2不同,它使用了最优匹配算法,并支持内存碎片的回收,能将零散的内存碎片整理为一个大的内存块

特性:

  1. 可以用于重复第创建,删除任务,信号量。。。。。
  2. 即使随机第调用pvPortMalloc()函数和vPortFree()函数,并未向每次申请的内存大小不同,也不会像heap_2一样产生很多内存碎片。
  3. 不具有事件确定性,即申请动态内存的时间不固定,但是比C库中的malloc()效率高
12.1.5 heap_5.c动态内存管理方法

它使用了与heap_4.c方法相同的合算法。,但heap_5.c允许内存堆跨越多个不连续的内存段。另外,使用heap_5要先通过vPortDefineHeapRegions()函数进行初始化。

特性:

  1. 具有与heap_4一样的特性
  2. 能跨区域管理内存
  3. 使用复杂
    定时器的名字

xTimerPeriodInTicks:定时周期,单位是系统时钟节拍

xAutoReload:软件定时模式,传入pdTRUE,是创建周期软定时器,pdFALSE是单次软件定时器

pvTimerID:定时器ID,用于标识当多个软件定时器使用同一个回调函数时是哪个软件定时器引起的回调

pxTimerBuffer:软件定时器回调函数

返回值:创建成功返回软件定时器句柄,失败返回NULL

11.2.2.2xTimerCreateStatic()

用于静态创建一个软件定时器。刚创建好的软件定时器处于未运行状态。

TimerHandle_t xTimerCreateStatic( const char * const pcTimerName, /*lint !e971 Unqualified char types are allowed for strings and single characters only. */
                                          const TickType_t xTimerPeriodInTicks,
                                          const BaseType_t xAutoReload,
                                          void * const pvTimerID,
                                          TimerCallbackFunction_t pxCallbackFunction,
                                          StaticTimer_t * pxTimerBuffer )

参数说明:

pcTimerName:软件定时器的名字

xTimerPeriodInTicks:定时周期,单位是系统时钟节拍

xAutoReload:软件定时模式,传入pdTRUE,是创建周期软定时器,pdFALSE是单次软件定时器

pvTimerID:定时器ID,用于标识当多个软件定时器使用同一个回调函数时是哪个软件定时器引起的回调

pxTimerBuffer:软件定时器回调函数

pxTimerBuffer:指向一个StatciTimer_t类型的结构体

返回值:创建成功返回软件定时器句柄,失败返回NULL

11.2.3启动软件定时器

刚创建好的软件定时器处于未运行状态。

11.2.3.1xTimerStart()

它为软件定时器启动函数。

实际实现功能是xTimerGenericCommand()

创建成功返回pdPASS,失败pdFAIL

11.2.3.2xTimerStartFromISR()

是软件定时器启动的中断版本。

创建成功返回pdPASS,失败pdFAIL

11.2.4停止软件定时器

软件定时器一旦启动就会不断重复运行,知道调用软件定时器停止API为止。

11.2.4.1xTimerStop()

实际实现功能是xTimerGenericCommand()

创建成功返回pdPASS,失败pdFAIL

11.2.4.2xTimerStopISR()

它的中断版本

实际实现功能是xTimerGenericCommand()

创建成功返回pdPASS,失败pdFAIL

12FreeRTOS内存管理

内存管理是FreeRTOS非常重要的功能,前面讲述的任务,消息队列,信号量,事件标志组及软件定时器等在创建时又两种方法:动态内存分配;用户指定内存的静态方法。

使用动态内存分配方法所需要的内存就是从FreeRTOS所管理的内存区进行分配。

12.1内存分配方法

FreeRTOS支持5中方法,分别通过文件heap_1,heap_2,heap_3heap_5实现,在实际使用中,只需要使用其中的一种即可。

heap_3动态内存管理之外,FreeRTOS均通过内存堆ucHeap[]来管理内存,内存堆的大小为宏configTOTAL_HEAP_SIZE所设的的大小。

在程序运行过程中使用xPortGetFreeHeapSize()函数获取动态剩余内存堆大小。

12.1.1 heap_1.c动态内存管理方法

这种发放是5种中最简单的一种,采用这种方法,动态内存一旦申请就不允许释放。

特性:

  1. 项目应用不需要删除任务、信号量、消息队列等已经创建好的资源
  2. 具有时间确定性,即申请动态内存的时间是固定的,并不会产生内存碎片
  3. 代码实现简单
12.1.2 heap_2.c动态内存管理方法

它使用了最适应算法,并支持内存释放。但不支持鸟村碎片整理。

特性:

  1. 在不考虑内存碎片的情况下,heap_2支持重复的任务、信号量、事件标志组、软件定时器等内部资源的创建和删除。
  2. 如果用户申请和释放的动态内存大小是随机的,可变的,那么不要使用此方法。因为容易产生内存碎片
  3. 不具有事件确定性。
12.1.3 heap_3.c动态内存管理方法

heap_3.c动态内存管理方法实现的动态内存管理是对编译器提供的malloc()函数和free()函数进行简单封装,额外做了线程保护

特性:

  1. 需要编译提供malloc()和free()函数。
  2. 不具有事件确定性,即申请动态内存的时间不固定
  3. 可能增加FreeRTOS内核代码量
12.1.4 heap_4.c动态内存管理方法

与heap_2不同,它使用了最优匹配算法,并支持内存碎片的回收,能将零散的内存碎片整理为一个大的内存块

特性:

  1. 可以用于重复第创建,删除任务,信号量。。。。。
  2. 即使随机第调用pvPortMalloc()函数和vPortFree()函数,并未向每次申请的内存大小不同,也不会像heap_2一样产生很多内存碎片。
  3. 不具有事件确定性,即申请动态内存的时间不固定,但是比C库中的malloc()效率高
12.1.5 heap_5.c动态内存管理方法

它使用了与heap_4.c方法相同的合算法。,但heap_5.c允许内存堆跨越多个不连续的内存段。另外,使用heap_5要先通过vPortDefineHeapRegions()函数进行初始化。

特性:

  1. 具有与heap_4一样的特性
  2. 能跨区域管理内存
  3. 使用复杂
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值