FreeRTOS(三)——应用开发(一)

文章目录

0x01 FreeRTOS文件夹

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jfXPQ13P-1682496278374)(C:\Users\user\AppData\Roaming\Typora\typora-user-images\image-20230419103423058.png)]

FreeRTOS 文件夹下的 Source 文件夹里面包含的是 FreeRTOS内核的源代码,Demo 文件夹里面包含了 FreeRTOS 官方为各个单片机移植好的工程代码。从Demo中可以得到FreeRTOSConfig.h

  • Source文件夹

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xnS8IfMB-1682496278374)(C:\Users\user\AppData\Roaming\Typora\typora-user-images\image-20230419105110294.png)]

  • include以及各种.c文件包含的是FreeRTOS的通用头文件和C文件,这两部分的文件试用于各种编译器和处理器,是通用的。

  • portblle:里面很多与编译器相关的文件夹,在不同的编译器中使用不同的支持文件。Keil文件与RVDS的文件是一样的,其中的MemMang文件是与内存管理相关的。
    在这里插入图片描述

  • RVDS:这个文件夹包含了各种处理器相关的文件夹,FreeRTOS需要软硬结合,不同的硬件接口文件是不一样的,需要编写代码来进行关联,这部分关联则叫关联文件,一般由汇编和C联合编写。FreeRTOS 为我们提供了 cortex-m0、m3、m4 和 m7 等内核的单片机的接口文件,只要是使用了这些内核的 mcu 都可以使用里面的接口文件。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-x5kbnxWQ-1682496278375)(C:\Users\user\AppData\Roaming\Typora\typora-user-images\image-20230419105704894.png)]

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aTaRMmrn-1682496278375)(C:\Users\user\AppData\Roaming\Typora\typora-user-images\image-20230419110709569.png)]

    里面的文件,里面只有“port.c”与“portmacro.h”两个文件,port.c 文件里面的内容是由 FreeRTOS 官方的技术人员为 Cortex-M3 内核的处理器写的接口文件,里面核心的上下文切换代码是由汇编语言编写而成。portmacro.h 则是 port.c 文件对应的头文件,主要是一些数据类型和宏定义。

  • MemMang:文件夹下存放的是跟内存管理相关的,总共有五个 heap 文件以及一个 readme 说明文件,这五个 heap 文件在移植的时候必须使用一个,因为 FreeRTOS 在创建内核对象的时候使用的是动态分配内存,而这些动态内存分配的函数则在这几个文件里面实现,不同的分配算法会导致不同的效率与结果。

  • Demo:各种开发平台完整的Demo。

  • FreeRTOS-Plus:reeRTOS-Plus 文件夹里面包含的是第三方的产品,一般我们不需要使用,FreeRTOSPlus 的预配置演示项目组件(组件大多数都要收费),大多数演示项目都是在 Windows 环境中运行的,使用 FreeRTOS windows 模拟器。

FreeRTOSConfig.h文件内容

#ifndef FREERTOS_CONFIG_H
#define FREERTOS_CONFIG_H

// 针对不同的编译器调用不同的stdint.h文件
// 针对不同的编译器调用不同的 stdint.h 文件,在 MDK 中,我们默
认的是__CC_ARM。
#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基础配置选项

//1:RTOS使用抢占式调度器 0:RTOS使用协作式调度器(时间片)
//协作式操作系统是任务主动释放CPU后,切换到下一个任务,任务切换的时机完全取决于正在运行的任务。
#define configUSE_PREEMPTION                     1
//支持静态内存
#define configSUPPORT_STATIC_ALLOCATION          1
//支持动态内存申请
#define configSUPPORT_DYNAMIC_ALLOCATION         1

// 置 1:使用空闲钩子(Idle Hook 类似于回调函数);置 0:忽略空闲钩子
// 空闲任务钩子是一个函数,这个函数由用户来实现, FreeRTOS 规定了函数的名字和参数:void vApplicationIdleHook(void ),
// 这个函数在每个空闲任务周期都会被调用
// 对于已经删除的 RTOS 任务,空闲任务可以释放分配给它们的堆栈内存。
// 因此必须保证空闲任务可以被 CPU 执行,使用空闲钩子函数设置 CPU 进入省电模式是很常见的,不可以调用会引起空闲任务阻塞的 API 函数
#define configUSE_IDLE_HOOK                      0

// 时间片钩子是一个函数,这个函数由用户来实现, FreeRTOS 规定了函数的名字和参数:void vApplicationTickHook(void )
// 时间片中断可以周期性的调用, 函数必须非常短小,不能大量使用堆栈,不能调用以”FromISR" 或 "FROM_ISR”结尾的 API 函数
#define configUSE_TICK_HOOK                      0
//置 1:使用时间片钩子(Tick Hook);置 0:忽略时间片钩子
//写入实际的 CPU 内核时钟频率,也就是 CPU 指令执行频率,通常称为 Fclk, Fclk 为供给 CPU 内核的时钟信号,我们所说的 cpu 主频为 XX MHz,就是指的这个时钟信号,相应的,1/Fclk 即为 cpu 时钟周期。
#define configCPU_CLOCK_HZ                       ( SystemCoreClock )

//RTOS 系统节拍中断的频率。即一秒中断的次数,每次中断 RTOS 都会进行任务调度
//在 FreeRTOS 中,系统延时和阻塞时间都是以 tick 为单位,配置 configTICK_RATE_HZ 的值可以改变中断的频率,从而间接改变了 FreeRTOS 的时钟周期(T=1/f)
#define configTICK_RATE_HZ                       ((TickType_t)1000)

//可使用的最大优先级
//低优先级数值表示低优先级任务
#define configMAX_PRIORITIES                     ( 16 )

//空闲任务使用的堆栈大小
#define configMINIMAL_STACK_SIZE                 ((uint16_t)128)
///系统所有总的堆大小
//FreeRTOS 内核总计可用的有效的 RAM 大小
#define configTOTAL_HEAP_SIZE                    ((size_t)3072)

//任务名字字符串长度
//这里定义的长度包括字符串结束符’\0’
#define configMAX_TASK_NAME_LEN                  ( 16 )

//启用可视化跟踪调试
#define configUSE_TRACE_FACILITY                 1
//与宏 configUSE_TRACE_FACILITY 同时为 1 时会编译下面 3 个函数
// prvWriteNameToBuffer()\ vTaskList()\ vTaskGetRunTimeStats()
#define configUSE_STATS_FORMATTING_FUNCTIONS     1

//系统节拍计数器变量数据类型,1 表示为 16 位无符号整形,0 表示为 32 位无符号整形
//这个值位数的大小决定了能计算多少个 tick
#define configUSE_16_BIT_TICKS                   0

// 互斥量以及队列长度设置
#define configUSE_MUTEXES                        1
#define configQUEUE_REGISTRY_SIZE                8

//某些运行 FreeRTOS 的硬件有两种方法选择下一个要执行的任务:通用方法和特定于硬件的方法(以下简称“特殊方法”)。
//一般是硬件计算前导零指令,如果所使用的,MCU 没有这些硬件指令的话此宏应该设置为 0
//通用方法:
//1.configUSE_PORT_OPTIMISED_TASK_SELECTION 为 0 或者硬件不支持这种特殊方法。
//2.可以用于所有 FreeRTOS 支持的硬件
//3.完全用 C 实现,效率略低于特殊方法。
//4.不强制要求限制最大可用优先级数目
//特殊方法:
//1.必须将 configUSE_PORT_OPTIMISED_TASK_SELECTION 设置为 1。
//2.依赖一个或多个特定架构的汇编指令(一般是类似计算前导零[CLZ]指令)。
//3.比通用方法更高效
//4.一般强制限定最大可用优先级数目为 32
#define configUSE_PORT_OPTIMISED_TASK_SELECTION  1
//启用协程,启用协程以后必须添加文件 croutine.c
#define configUSE_CO_ROUTINES                    0
//协程的有效优先级数目
#define configMAX_CO_ROUTINE_PRIORITIES          ( 2 )

//可选函数配置选项
#define INCLUDE_vTaskPrioritySet            1
#define INCLUDE_uxTaskPriorityGet           1
#define INCLUDE_vTaskDelete                 1
#define INCLUDE_vTaskCleanUpResources       0
#define INCLUDE_vTaskSuspend                1
#define INCLUDE_vTaskDelayUntil             0
#define INCLUDE_vTaskDelay                  1
#define INCLUDE_xTaskGetSchedulerState      1

//与中断有关选项
#ifdef __NVIC_PRIO_BITS
 /* __BVIC_PRIO_BITS will be specified when CMSIS is being used. */
 #define configPRIO_BITS         __NVIC_PRIO_BITS
#else
 #define configPRIO_BITS         4
#endif

//中断最低优先级
//这里是中断优先级,中断优先级的数值越小,优先级越高。而 FreeRTOS 的任务优先级是,任务优先级数值越小,任务优先级越低
#define configLIBRARY_LOWEST_INTERRUPT_PRIORITY   15

//系统可管理的最高中断优先级
//中断优先级数值在 0、1、2、3、4 的这些中断是不受 FreeRTOS 管理的,不可被屏蔽,也不能调用 FreeRTOS 中的 API 函数接口,而中断优先级在 5 到 15的这些中断是受到系统管理,可以被屏蔽的。
#define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 5
//用于配置 basepri 寄存器的,当 basepri 设置为某个值的时候,会让系统不响应比该优先级低的中断,而优先级比之更高的中断则不受影响。
#define configKERNEL_INTERRUPT_PRIORITY 		( configLIBRARY_LOWEST_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) )
//对需要配置的 SysTick 与 PendSV 进行偏移
//在 port.c 中会用到 configKERNEL_INTERRUPT_PRIORITY 这个宏定义来配置SCB_SHPR3(系统处理优先级寄存器,地址为:0xE000 ED20)
//中断优先级 0(具有最高的逻辑优先级)不能被 basepri 寄存器屏蔽,因此,configMAX_SYSCALL_INTERRUPT_PRIORITY 绝不可以设置成 0。
#define configMAX_SYSCALL_INTERRUPT_PRIORITY 	( configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) )

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

#endif

上面定义的宏决定FreeRTOS.h文件中的定义

// FreeRTOS基础配置
// 1使能时间片调度(默认式使能)
#define configUSE_TIME_SLICING					1
// 置 1:使能低功耗 tickless 模式;置 0:保持系统节拍(tick)中断一直运行
#define configUSE_TICKLESS_IDLE 				0
// 空闲任务放弃 CPU 使用权给其他同优先级的用户任务
//满足条件才会起作用:1:启用抢占式调度;2:用户任务优先级与空闲任务优先级相等。一般不建议使用这个功能,能避免尽量避免,1:设置用户任务优先级比空闲任务优先级高,2:这个宏定义配置为 0。
#define configIDLE_SHOULD_YIELD					1
// 是否启动队列
#define configUSE_QUEUE_SETS 					0
// 开启任务通知功能,默认开启
#define configUSE_TASK_NOTIFICATIONS 			1
// 使用互斥信号量
#define configUSE_MUTEXES 						0
// 使用递归互斥信号量
#define configUSE_RECURSIVE_MUTEXES 			0
// 1 使用计数信号量
#define configUSE_COUNTING_SEMAPHORES 			0
// 设置可以注册的信号量和消息队列个数
#define configQUEUE_REGISTRY_SIZE 				0U
#define configUSE_APPLICATION_TASK_TAG 			0
//使用内存申请失败钩子函数
#define configUSE_MALLOC_FAILED_HOOK 			0
//大于 0 时启用堆栈溢出检测功能,如果使用此功能用户必须提供一个栈溢出钩子函数,如果使用的话,此值可以为 1 或者 2,因为有两种栈溢出检测方法
#define configCHECK_FOR_STACK_OVERFLOW 			0
//启用运行时间统计功能
#define configGENERATE_RUN_TIME_STATS 			0
//启用软件定时器
#define configUSE_TIMERS 						0
  • 抢占式调度:在这种调度方式中,系统总是选择优先级最高的任务进行调度,并且 一旦高优先级的任务准备就绪之后,它就会马上被调度而不等待低优先级的任务主动放弃 CPU,高优先级的任务抢占了低优先级任务的 CPU 使用权,这就是抢占。在实时操作系统中,这样子的方式往往是最适用的。而协作式调度则是由任务主动放弃CPU,然后才进行任务调度。
  • 当优先级相同的时候,就会采用时间片调度,这意味着 RTOS 调度器总是运行处于最高优先级的就绪任务,在每个FreeRTOS 系统节拍中断时在相同优先级的多个任务间进行任务切换。如果宏configUSE_TIME_SLICING 设置为 0,FreeRTOS 调度器仍然总是运行处于最高优先级的就 绪任务,但是当 RTOS 系统节拍中断发生时,相同优先级的多个任务之间不再进行任务切换,而是在执行完高优先级的任务之后才进行任务切换。

0x02 创建任务

  • 任务里面的延时函数必须使用 FreeRTOS 里面提供的延时函数,并不能使用我们裸机编程中的那种延时。
  • 这两种的延时的区别是 FreeRTOS 里面的延时是阻塞延时,即调用 vTaskDelay()函数的时候,当前任务会被挂起,调度器会切换到其它就绪的任务,从而实现多任务。
  • 如果还是使用裸机编程中的那种延时,那么整个任务就成为了一个死循环,如果恰好该任务的优先级是最高的,那么系统永远都是在这个任务中运行,比它优先级更低的任务无法运行,根本无法实现多任务。
  • 任务必须是一个死循环,否则任务将通过 LR 返回,如果 LR 指向了非法的内存就会产生 HardFault_Handler,而 FreeRTOS 指向一个任务退出函数prvTaskExitError(),里面是一个死循环,那么任务返回之后就在死循环中执行,这样子的任务是不安全的,所以避免这种情况,任务一般都是死循环并且无返回值的。

创建静态任务过程configSUPPORT_STATIC_ALLOCATION

使用静态创建任务时,需要将configSUPPORT_STATIC_ALLOCATION这个宏定义为1,并且需要实现函数vApplicationGetIdleTaskMemory()与 vApplicationGetTimerTaskMemory(),这两个函数是用户设定的空闲(Idle)任务与定时器(Timer)任务的堆栈大小,必须由用户自己分配,而不能是动态分配。并且需要定义一些全局变量如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MX66AbiK-1682496278376)(C:\Users\user\AppData\Roaming\Typora\typora-user-images\image-20230419161953387.png)]

并且创建好任务句柄。

使用STM32CubeMX生成的代码中,使用的是如下的宏定义进行线程的创建:

// 静态创建
#define osThreadStaticDef(name, thread, priority, instances, stacksz, buffer, control)  \
const osThreadDef_t os_thread_def_##name = \
{
      
       #name, (thread), (priority), (instances), (stacksz), (buffer), (control) }

宏定义中,##的作用就是把2个宏参数连接为1个数,或实现字符串的连接,#的作用就是将#后面的宏参数进行字符串的操作,也就是将#后面的参数两边加上一对双引号使其成为字符串。

所以

osThreadDef(Display, DisLCD_Task,osPriorityNormal, 0, 128);
//相当于
const   osThreadDef_t   os_thread_def_Display = {
    
     "Display", (DisLCD_Task), (osPriorityNormal), (0), (128)  }

对于osThreadDef_t为一个结构体,并且具有一个函数指针,

typedef struct os_thread_def  {
    
    
  char                   *name;        ///< Thread name 
  os_pthread             pthread;      ///< start address of thread function
  osPriority             tpriority;    ///< initial thread priority
  uint32_t               instances;    ///< maximum number of instances of that thread function
  uint32_t               stacksize;    ///< stack size requirements in bytes; 0 is default stack size
#if( configSUPPORT_STATIC_ALLOCATION == 1 )
  uint32_t               *buffer;      ///< stack buffer for static allocation; NULL for dynamic allocation
  osStaticThreadDef_t    *controlblock;     ///< control block to hold thread's data for static allocation; NULL for dynamic allocation
#endif
} osThreadDef_t;
typedef void (*os_pthread) (void const *argument);
typedef enum  {
    
    
  osPriorityIdle          = -3,          ///< priority: idle (lowest)
  osPriorityLow           = -2,          ///< priority: low
  osPriorityBelowNormal   = -1,          ///< priority: below normal
  osPriorityNormal        =  0,          ///< priority: normal (default)
  osPriorityAboveNormal   = +1,          ///< priority: above normal
  osPriorityHigh          = +2,          ///< priority: high
  osPriorityRealtime      = +3,          ///< priority: realtime (highest)
  osPriorityError         =  0x84        ///< system cannot determine priority or thread has illegal priority
} osPriority;

通过以上我们就相当于使用任务名创建了一个结构体变量,之后传入函数osThreadCreate中进行任务创建:

#define osThread(name)  \
&os_thread_def_##name
osThreadDef(defaultTask, StartDefaultTask, osPriorityNormal, 0, 128);
defaultTaskHandle = osThreadCreate(osThread(defaultTask), NULL);

osThreadCreate函数中调用了xTaskCreateStatic函数进行创建任务。

  • 在 FreeRTOS 系统中,每一个任务都是独立的,他们的运行环境都单独的保存在他们 的栈空间当中。那么在定义好任务函数之后,我们还要为任务定义一个栈,目前我们使用的是静态内存,所以任务栈是一个独立的全局变量。
  • 在大多数系统中需要做栈空间地址对齐,在 FreeRTOS 中是以 8 字节大小对齐,并且会检查堆栈是否已经对齐,其中 portBYTE_ALIGNMENT 是在 portmacro.h 里面定义的一个宏,其值为 8,就是配置为按 8 字节对齐,当然用户可以选择按 1、2、4、8、16、32 等字节对齐。
  • xTaskCreateStatic这个函数将任务主体函数、任务栈、任务控制块联合在一起。让任务可以随时被系统启动。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wsftFxot-1682496278376)(C:\Users\user\AppData\Roaming\Typora\typora-user-images\image-20230419160059259.png)]

当任务创建好后,是处于任务就绪(Ready),在就绪态的任务可以参与操作系统的调度。但是此时任务仅仅是创建了,还未开启任务调度器,也没创建空闲任务与定时器任务(如果使能了 configUSE_TIMERS 这个宏定义),那这两个任务就是在启动任务调度器中实现,每个操作系统,任务调度器只启动一次,之后就不会再次执行了。开启调度使用函数vTaskStartScheduler()

创建动态任务过程configSUPPORT_DYNAMIC_ALLOCATION

该任务任务使用的栈和任务控制块是在创建任务的时候FreeRTOS 动态分配的,并不是预先定义好的全局变量。在上面的任务中,任务控制块和任务栈的内存空间都是从内部的 SRAM 里面分配的,具体分配到哪个地址由编译器决定。

现在我们开始使用动态内存,即堆,其实堆也是内存,也属于 SRAM。FreeRTOS 做法是在 SRAM 里面定义一个大数组,也就是堆内存,供 FreeRTOS 的动态内存分配函数使用,在第一次使用的时候,系统会将定义的堆内存进行初始化,这些代码在 FreeRTOS 提供的内存管理方案中实现。

使用动态内存时候,不用跟使用静态内存那样要预先定义好一个全局的静态的任务控制块空间任务控制块是在任务创建的时候分配内存空间创建,任务创建函数会返回一个指针,用于指向任务控制块,所以要预先为任务栈定义一个任务控制块指针,也是我们常说的任务句柄

//任务句柄是一个指针,用于指向一个任务,当任务创建好之后,它就具有了一个任务句柄以后我们要想操作这个任务都需要通过这个任务句柄,如果是自身的任务操作自己,那么这个句柄可以为 NULL。
typedef TaskHandle_t osThreadId;
osThreadId ReceiveHandle;
osThreadId SendHandle;
/* definition and creation of Receive */
osThreadDef(Receive, ReceiveTask, osPriorityIdle, 0, 128);
ReceiveHandle = osThreadCreate(osThread(Receive), NULL);

/* definition and creation of Send */
osThreadDef(Send, SendTask, osPriorityIdle, 0, 128);
SendHandle = osThreadCreate(osThread(Send), NULL);


// 动态创建
#define osThreadDef(name, thread, priority, instances, stacksz)  \
const osThreadDef_t os_thread_def_##name = \
{
      
       #name, (thread), (priority), (instances), (stacksz), NULL, NULL }

typedef struct os_thread_def  {
    
    
  char                   *name;        ///< Thread name 
  os_pthread             pthread;      ///< start address of thread function
  osPriority             tpriority;    ///< initial thread priority
  uint32_t               instances;    ///< maximum number of instances of that thread function
  uint32_t               stacksize;    ///< stack size requirements in bytes; 0 is default stack size
#if( configSUPPORT_STATIC_ALLOCATION == 1 )
  uint32_t               *buffer;      ///< stack buffer for static allocation; NULL for dynamic allocation
  osStaticThreadDef_t    *controlblock;     ///< control block to hold thread's data for static allocation; NULL for dynamic allocation
#endif
} osThreadDef_t;
typedef void (*os_pthread) (void const *argument);
typedef enum  {
    
    
  osPriorityIdle          = -3,          ///< priority: idle (lowest)
  osPriorityLow           = -2,          ///< priority: low
  osPriorityBelowNormal   = -1,          ///< priority: below normal
  osPriorityNormal        =  0,          ///< priority: normal (default)
  osPriorityAboveNormal   = +1,          ///< priority: above normal
  osPriorityHigh          = +2,          ///< priority: high
  osPriorityRealtime      = +3,          ///< priority: realtime (highest)
  osPriorityError         =  0x84        ///< system cannot determine priority or thread has illegal priority
} osPriority;

此时调用的是函数xTaskCreate来创建任务,并且在如下进行创建:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tO0hj60h-1682496278377)(C:\Users\user\AppData\Roaming\Typora\typora-user-images\image-20230419162728074.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-b5L00ICw-1682496278377)(C:\Users\user\AppData\Roaming\Typora\typora-user-images\image-20230419162334029.png)]

堆内存的大小为 configTOTAL_HEAP_SIZE , 在FreeRTOSConfig.h 中由我们自己定义,configSUPPORT_DYNAMIC_ALLOCATION 这个宏定义在使用 FreeRTOS 操作系统的时候必须开启。

之后即可启动任务:vTaskStartScheduler()。

0x03 FreeRTOS启动流程

第一种启动流程:

这种启动方式也就是上面讲解的启动方式。

  • 外设硬件初始化
  • RTOS系统初始化
  • 创建各种任务
  • 启动RTOS调度器
  • 编写函数实体:
  • 任务实体通常是一个不带返回值的无限循环的 C 函数,函数体必须有阻塞的情况出现,不然任务(如果优先权恰好是最高)会一直在 while 循环里面执行,导致其它任务没有执行的机会。

第二种启动流程:

在 main 函数中将硬件和 RTOS 系统先初始化好,然后创建一个启动任务后就启动调度器,然后在启动任务里面创建各种应用任务,当所有任务都创建成功后,启动任务把自己删除。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-v9JEI2xQ-1682496278378)(C:\Users\user\AppData\Roaming\Typora\typora-user-images\image-20230419163739600.png)]

启动流程概述

系统上电时第一个执行的启动文件是由汇编编写的复位函数Reset_Handler,复位函数会调用C库函数__main,其函数主要工作是初始化系统的堆栈。

创建任务xTaskCreate()函数

在 main()函数中,我们直接可以对 FreeRTOS 进行创建任务操作,因为 FreeRTOS 会自动帮我们做初始化的事情,比如初始化堆内存。其实也就是对即将使用的堆栈进行初始化,根据任务个数来决定。在此函数中,进行了堆内存的初始化:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tO6j9wwM-1682496278378)(C:\Users\user\AppData\Roaming\Typora\typora-user-images\image-20230419164409024.png)]

其分配内存函数:

void *pvPortMalloc( size_t xWantedSize )
{
    
    
BlockLink_t *pxBlock, *pxPreviousBlock, *pxNewBlockLink;
void *pvReturn = NULL;

	vTaskSuspendAll();
	{
    
    
		/*如果这是对 malloc 的第一次调用,那么堆将需要初始化来设置空闲块列表。 */
		if( pxEnd == NULL )
		{
    
    
			prvHeapInit();
		}
		else
		{
    
    
			mtCOVERAGE_TEST_MARKER();
		}
        ....
    }
}

其堆的初始化函数:

static void prvHeapInit( void )
{
    
    
    BlockLink_t *pxFirstFreeBlock;
    uint8_t *pucAlignedHeap;
    size_t uxAddress;
    size_t xTotalHeapSize = configTOTAL_HEAP_SIZE;

	/* 确保堆在正确对齐的边界上启动。 */
	uxAddress = ( size_t ) ucHeap;

	if( ( uxAddress & portBYTE_ALIGNMENT_MASK ) != 0 )
	{
    
    
		uxAddress += ( portBYTE_ALIGNMENT - 1 );
		uxAddress &= ~( ( size_t ) portBYTE_ALIGNMENT_MASK );
		xTotalHeapSize -= uxAddress - ( size_t ) ucHeap;
	}

	pucAlignedHeap = ( uint8_t * ) uxAddress;

	/*xStart 用于保存指向空闲块列表中第一个项目的指针。 void 用于防止编译器警告 */
	xStart.pxNextFreeBlock = ( void * ) pucAlignedHeap;
	xStart.xBlockSize = ( size_t ) 0;

	/*pxEnd 用于标记空闲块列表的末尾,并插入堆空间的末尾 */
	uxAddress = ( ( size_t ) pucAlignedHeap ) + xTotalHeapSize;
	uxAddress -= xHeapStructSize;
	uxAddress &= ~( ( size_t ) portBYTE_ALIGNMENT_MASK );
	pxEnd = ( void * ) uxAddress;
	pxEnd->xBlockSize = 0;
	pxEnd->pxNextFreeBlock = NULL;

	/*首先,有一个空闲块,其大小可以占用整个堆空间,减去 pxEnd 占用的空间*/
	pxFirstFreeBlock = ( void * ) pucAlignedHeap;
	pxFirstFreeBlock->xBlockSize = uxAddress - ( size_t ) pxFirstFreeBlock;
	pxFirstFreeBlock->pxNextFreeBlock = pxEnd;

	/*只存在一个块 - 它覆盖整个可用堆空间。因为是刚初始化的堆内存 */
	xMinimumEverFreeBytesRemaining = pxFirstFreeBlock->xBlockSize;
	xFreeBytesRemaining = pxFirstFreeBlock->xBlockSize;

	/* 计算出size_t变量的顶部位的位置。 */
	xBlockAllocatedBit = ( ( size_t ) 1 ) << ( ( sizeof( size_t ) * heapBITS_PER_BYTE ) - 1 );
}

开启调度器函数vTaskStartScheduler()

在创建完任务的时候,我们需要开启调度器,因为创建仅仅是把任务添加到系统中,还没真正调度,并且空闲任务也没实现,定时器任务也没实现,这些都是在开启调度函数vTaskStartScheduler()中实现的。为什么要空闲任务?因为 FreeRTOS 一旦启动,就必须要保证系统中每时每刻都有一个任务处于运行态(Runing)并且空闲任务不可以被挂起与删除,空闲任务的优先级是最低的,以便系统中其他任务能随时抢占空闲任务的 CPU 使用权

void vTaskStartScheduler( void )
{
    
    
	BaseType_t xReturn;

	/* 创建的是静态的空闲任务 */
	#if( configSUPPORT_STATIC_ALLOCATION == 1 )
	{
    
    
		StaticTask_t *pxIdleTaskTCBBuffer = NULL;
		StackType_t *pxIdleTaskStackBuffer = NULL;
		uint32_t ulIdleTaskStackSize;

		/* 空闲任务是使用用户提供的 RAM 创建的 - 获取
		然后 RAM 的地址创建空闲任务。这是静态创建任务,我们不用管 */
		vApplicationGetIdleTaskMemory( &pxIdleTaskTCBBuffer, &pxIdleTaskStackBuffer, &ulIdleTaskStackSize );
		xIdleTaskHandle = xTaskCreateStatic(	prvIdleTask,
												configIDLE_TASK_NAME,
												ulIdleTaskStackSize,
												( void * ) NULL, /*lint !e961.  The cast is not redundant for all compilers. */
												( tskIDLE_PRIORITY | portPRIVILEGE_BIT ),
												pxIdleTaskStackBuffer,
												pxIdleTaskTCBBuffer ); /*lint !e961 MISRA exception, justified as it is not a redundant explicit cast to all supported compilers. */

		if( xIdleTaskHandle != NULL )
		{
    
    
			xReturn = pdPASS;
		}
		else
		{
    
    
			xReturn = pdFAIL;
		}
	}
	#else
	{
    
    
		/* 使用动态分配的 RAM 创建空闲任务。 */
		xReturn = xTaskCreate(	prvIdleTask,
								configIDLE_TASK_NAME,
								configMINIMAL_STACK_SIZE,
								( void * ) NULL,
								( tskIDLE_PRIORITY | portPRIVILEGE_BIT ),
								&xIdleTaskHandle ); /*lint !e961 MISRA exception, justified as it is not a redundant explicit cast to all supported compilers. */
	}
	#endif /* configSUPPORT_STATIC_ALLOCATION */

	#if ( configUSE_TIMERS == 1 )
	{
    
    
        /* 如果使能了 configUSE_TIMERS 宏定义 表明使用定时器,需要创建定时器任务*/
		if( xReturn == pdPASS )
		{
    
    
			xReturn = xTimerCreateTimerTask();
		}
		else
		{
    
    
			mtCOVERAGE_TEST_MARKER();
		}
	}
	#endif /* configUSE_TIMERS */

	if( xReturn == pdPASS )
	{
    
    
		
		#ifdef FREERTOS_TASKS_C_ADDITIONS_INIT
		{
    
    
			freertos_tasks_c_additions_init();
		}
		#endif

       /* 此处关闭中断,以确保不会发生中断
        在调用 xPortStartScheduler()之前或期间。 堆栈的
        创建的任务包含打开中断的状态
        因此,当第一个任务时,中断将自动重新启用
        开始运行。 */
		portDISABLE_INTERRUPTS();

		#if ( configUSE_NEWLIB_REENTRANT == 1 )
		{
    
    
			/* 不需要理会,这个宏定义没打开 */
			_impure_ptr = &( pxCurrentTCB->xNewLib_reent );
		}
		#endif /* configUSE_NEWLIB_REENTRANT */

		xNextTaskUnblockTime = portMAX_DELAY;
        //xSchedulerRunning 等于 pdTRUE,表示调度器开始运行了,而xTickCount 初始化需要初始化为 0,这个 xTickCount 变量用于记录系统的时间,在节拍定时器(SysTick)中断服务函数中进行自加。
		xSchedulerRunning = pdTRUE;
		xTickCount = ( TickType_t ) 0U;

		/* 如果定义了 configGENERATE_RUN_TIME_STATS,则以下内容
		必须定义宏以配置用于生成的计时器/计数器运行时计数器时基。目前没启用该宏定义 */
		portCONFIGURE_TIMER_FOR_RUN_TIME_STATS();

		/* 调用 xPortStartScheduler 函数配置相关硬件,如滴答定时器、FPU、pendsv 等 */
        //调用函数 xPortStartScheduler()来启动系统节拍定时器(一般都是使用 SysTick)并启动第一个任务。因为设置系统节拍定时器涉及到硬件特性,因此函数xPortStartScheduler()由移植层提供(在 port.c 文件实现),不同的硬件架构,这个函数的代码也不相同
        //在 Cortex-M3 架构中,FreeRTOS 为了任务启动和任务切换使用了三个异常:SVC、PendSV 和 SysTick
		if( xPortStartScheduler() != pdFALSE )
		{
    
    
			/* 如果 xPortStartScheduler 函数启动成功,则不会运行到这里*/
		}
		else
		{
    
    
			/* 不会运行到这里,除非调用 xTaskEndScheduler() 函数 */
		}
	}
	else
	{
    
    
			/* 只有在内核无法启动时才会到达此行,
            因为没有足够的堆内存来创建空闲任务或计时器任务。
            此处使用了断言,会输出错误信息,方便错误定位 */
		configASSERT( xReturn != errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY );
	}

	/* 如果 INCLUDE_xTaskGetIdleTaskHandle 设置为 0,则防止编译器警告,这意味着在其他任何地方都不使用 xIdleTaskHandle。暂时不用理会 */
	( void ) xIdleTaskHandle;
}

如果在 FreeRTOSConfig.h 中使能了 configUSE_TIMERS 这个宏定义,那么需要创建一个定时器任务,这个定时器任务也是调用 xTaskCreate()函数完成创建,过程十分简单,这也是系统的初始化内容,在调度器启动的过程中发现必要初始化的东西,FreeRTOS 就会帮我们完成:

xTimerCreateTimerTask()函数

BaseType_t xTimerCreateTimerTask( void )
{
    
    
	BaseType_t xReturn = pdFAIL;

	/* 检查使用了哪些活动计时器的列表,以及用于与计时器服务通信的队列,已经初始化。 */
	prvCheckForValidListAndQueue();

	if( xTimerQueue != NULL )
	{
    
    
        //静态创建
		#if( configSUPPORT_STATIC_ALLOCATION == 1 )
		{
    
    
			StaticTask_t *pxTimerTaskTCBBuffer = NULL;
			StackType_t *pxTimerTaskStackBuffer = NULL;
			uint32_t ulTimerTaskStackSize;

			vApplicationGetTimerTaskMemory( &pxTimerTaskTCBBuffer, &pxTimerTaskStackBuffer, &ulTimerTaskStackSize );
			xTimerTaskHandle = xTaskCreateStatic(	prvTimerTask,
													configTIMER_SERVICE_TASK_NAME,
													ulTimerTaskStackSize,
													NULL,
													( ( UBaseType_t ) configTIMER_TASK_PRIORITY ) | portPRIVILEGE_BIT,
													pxTimerTaskStackBuffer,
													pxTimerTaskTCBBuffer );

			if( xTimerTaskHandle != NULL )
			{
    
    
				xReturn = pdPASS;
			}
		}
        // 动态创建
		#else
		{
    
    
			xReturn = xTaskCreate(	prvTimerTask,
									configTIMER_SERVICE_TASK_NAME,
									configTIMER_TASK_STACK_DEPTH,
									NULL,
									( ( UBaseType_t ) configTIMER_TASK_PRIORITY ) | portPRIVILEGE_BIT,
									&xTimerTaskHandle );
		}
		#endif /* configSUPPORT_STATIC_ALLOCATION */
	}
	else
	{
    
    
		mtCOVERAGE_TEST_MARKER();
	}

	configASSERT( xReturn );
	return xReturn;
}

xPortStartScheduler()函数的调用,主要为了启动系统节拍定时器:

  • SVC:SVC(系统服务调用,亦简称系统调用)用于任务启动,有些操作系统不允许应用程序直接访问硬件,而是通过提供一些系统服务函数,用户程序使用 SVC 发出对系统服务函数的呼叫请求,以这种方法调用它们来间接访问硬件,它就会产生一个SVC 异常
  • **PendSV(可挂起系统调用)**用于完成任务切换,它是可以像普通的中断一样被挂起的,它的最大特性是如果当前有优先级比它高的中断在运行,PendSV 会延迟执行,直到高优先级中断执行完毕,这样子产生的 PendSV 中断就不会打断其他中断的运行。
  • SysTick 用于产生系统节拍时钟,提供一个时间片,如果多个任务共享同一个优先级,则每次 SysTick 中断,下一个任务将获得一个时间片。

这里将 PendSV 和 SysTick 异常优先级设置为最低,这样任务切换不会打断某个中断服务程序,中断服务程序也不会被延迟,这样简化了设计,有利于系统稳定。这样系统时间也不会有偏差,因为 SysTick 只是当次响应中断被延迟了,而 SysTick 是硬件定时器,它一直在计时,这一次的溢出产生中断与下一次的溢出产生中断的时间间隔是一样的,至于系统是否响应还是延迟响应,这个与 SysTick 无关,它照样在计时。

如果在使用第二种启动流程,也就是:

在 main 函数中将硬件和 RTOS 系统先初始化好,然后创建一个启动任务后就启动调度器,然后在启动任务里面创建各种应用任务,当所有任务都创建成功后,启动任务把自己删除。

如果我们创建的任务比第一个任务的优先级高的时候怎么办。假设是在临界区创建任务,任务只能在退出临界区的时候才执行最高优先级任务。假设如果当前没有临界区,就会分为以下三种情况:

1、应用任务的优先级比初始任务的优先级高,那创建完后立马去执行刚刚创建的应用任务,当应用任务被阻塞时,继续回到初始任务被打断的地方继续往下执行,直到所有应用任务创建完成,最后初始任务把自己删除,完成自己的使命;

2、应用任务的优先级与初始任务的优先级一样,那创建完后根据任务的时间片来执行,直到所有应用任务创建完成,最后初始任务把自己删除,完成自己的使命;

3、应用任务的优先级比初始任务的优先级低,那创建完后任务不会被执行,如果还有应用任务紧接着创建应用任务,如果应用任务的优先级出现了比初始任务高或者相等的情况,请参考 1 和 2 的处理方式,直到所有应用任务创建完成,最后初始任务把自己删除,完成自己的使命。

0x04 任务管理

FreeRTOS 的任务可认为是一系列独立任务的集合。每个任务在自己的环境中运行。在任何时刻,只有一个任务得到运行,FreeRTOS 调度器决定运行哪个任务。调度器会不断的启动、停止每一个任务,宏观看上去所有的任务都在同时在执行

作为任务,不需要对调度器的活动有所了解,**在任务切入切出时保存上下文环境(寄存器值、堆栈内容)是调度器主要的职责。**为了实现这点,每个 FreeRTOS 任务都需要有自己的栈空间。当任务切出时,它的执行环境会被保存在该任务的栈空间中,这样当任务再次运行时,就能从堆栈中正确的恢复上次的运行环境,任务越多,需要的堆栈空间就越大,而一个系统能运行多少个任务,取决于系统的可用的 SRAM。

FreeRTOS 中的任务是抢占式调度机制,高优先级的任务可打断低优先级任务,低优先级任务必须在高优先级任务阻塞或结束后才能得到调度。。同时 FreeRTOS 也支持时间片轮转调度方式,只不过时间片的调度是不允许抢占任务的 CPU 使用权。任务通常会运行在一个死循环中,也不会退出,如果一个任务不再需要,可以调用 FreeRTOS 中的任务删除 API 函数接口显式地将其删除。

任务调度器

在系统中除了中断处理函数、调度器上锁部分的代码和禁止中断的代码是不可抢占的之外,系统的其他部分都是可以抢占的。

系统理论上可以支持无数个优先级(0 ~ N,优先级数值越小的任务优先级越低,0 为最低优先级,分配给空闲任务使用,一般不建议用户来使用这个优先级。假如使能了 configUSE_PORT_OPTIMISED_TASK_SELECTION 这个宏(在 FreeRTOSConfig.h 文件定义),一般强制限定最大可用优先级数目为 32。

在系统中,当有比当前任务优先级更高的任务就绪时,当前任务将立刻被换出,高优先级任务抢占处理器运行。

**一个操作系统如果只是具备了高优先级任务能够“立即”获得处理器并得到执行的特点,那么它仍然不算是实时操作系统。**例如一个包含 n 个就绪任务的系统中,如果仅仅从头找到尾,那么这个时间将直接和 n 相关,而下一个就绪任务抉择时间的长短将会极大的影响系统的实时性。

FreeRTOS 内核中采用两种方法寻找最高优先级的任务:

第一种是通用的方法,在就绪链表中查找从高优先级往低查找 uxTopPriority,因为在创建任务的时候已经将优先级进行排序,查找到的第一个 uxTopPriority 就是我们需要的任务,然后通过 uxTopPriority 获取对应的任务控制块。

第二种方法则是特殊方法,利用计算前导零指令 CLZ,直接在uxTopReadyPriority 这个 32 位的变量中直接得出 uxTopPriority,这样子就知道哪一个优先级任务能够运行,这种调度算法比普通方法更快捷,但受限于平台(在 STM32 中我们就使用这种方法)。

FreeRTOS 内核中也允许创建相同优先级的任务。相同优先级的任务采用时间片轮转方式进行调度(也就是通常说的分时调度器),时间片轮转调度仅在当前系统中无更高优先级就绪任务存在的情况下才有效。为了保证系统的实时性,系统尽最大可能地保证高优先级的任务得以运行。任务调度的原则是一旦任务状态发生了改变,并且当前运行的任务优先级小于优先级队列组中任务最高优先级时,立刻进行任务切换(除非当前系统处于中断处理程序中或禁止任务切换的状态)。

任务状态迁移

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IUr744f7-1682496278380)(C:\Users\user\AppData\Roaming\Typora\typora-user-images\image-20230419175227771.png)]

  • 创建任务→就绪态(Ready):任务创建完成后进入就绪态,表明任务已准备就绪,随时可以运行,只等待调度器进行调度。
  • 就绪态→运行态(Running):发生任务切换时,就绪列表中最高优先级的任务被执行,从而进入运行态。
  • 运行态→就绪态:有更高优先级任务创建或者恢复后,会发生任务调度,此刻就绪列表中最高优先级任务变为运行态,那么原先运行的任务由运行态变为就绪态,依然在就绪列表中,等待最高优先级的任务运行完毕继续运行原来的任务(此处可以看做是 CPU 使用权被更高优先级的任务抢占了)。
  • 运行态→阻塞态(Blocked):正在运行的任务发生阻塞(挂起、延时、读信号量等待)时,该任务会从就绪列表中删除,任务状态由运行态变成阻塞态,然后发生任务切换,运行就绪列表中当前最高优先级任务
  • 阻塞态→就绪态:阻塞的任务被恢复后(任务恢复、延时时间超时、读信号量超时或读到信号量等),此时被恢复的任务会被加入就绪列表,从而由阻塞态变成就绪态;如果此时被恢复任务的优先级高于正在运行任务的优先级,则会发生任务切换,将该任务将再次转换任务状态,由就绪态变成运行态。

就绪态、阻塞态、运行态→挂起态(Suspended):任务可以通过调用 vTaskSuspend() API 函数都可以将处于任何状态的任务挂起,被挂起的任务得不到CPU 的使用权,也不会参与调度,除非它从挂起态中解除。而把一个挂起状态的任务回复的唯一途径是调用vTaskResume()或者是vTaskResumeFromISR()

挂起态与阻塞态的区别,当任务有较长的时间不允许运行的时候,我们可以挂起任务,这样子调度器就不会管这个任务的任何信息,直到我们调用恢复任务的 API 函数;而任务处于阻塞态的时候,系统还需要判断阻塞态的任务是否超时,是否可以解除阻塞。

  • 阻塞(Blocked):如果任务当前正在等待某个时序或外部中断,我们就说这个任务处于阻塞状态,该任务不在就绪列表中。
  • 挂起态(Suspended):处于挂起态的任务对调度器而言是不可见的,让一个任务进入挂起状态的唯一办法就是调用 vTaskSuspend()函数。

vTaskSuspend()

挂起指定任务。被挂起的任务绝不会得到 CPU 的使用权,不管该任务具有什么优先级,也无法参与调度,除非他从挂起态中解除。

如果想要使任务进行挂起,需要设置宏INCLUDE_vTaskSuspend配置为1.

	void vTaskSuspend( TaskHandle_t xTaskToSuspend )
	{
    
    
	TCB_t *pxTCB;

		taskENTER_CRITICAL();
		{
    
    
			/* 如果在此处传递 null,那么它正在被挂起的正在运行的任务 */
			pxTCB = prvGetTCBFromHandle( xTaskToSuspend );

			traceTASK_SUSPEND( pxTCB );

			/* 从就绪/阻塞列表中删除任务并放入挂起列表中 */
			if( uxListRemove( &( pxTCB->xStateListItem ) ) == ( UBaseType_t ) 0 )
			{
    
    
                //更新最高优先级
                //在使用通用方法找到最高优先级任务时,它用来记录最高优先级任务的优先级。
                //在使用硬件方法找到最高优先级任务时,它的每一位(共 32bit)的状态代表这个优先级上边,有没有就绪的任务
				taskRESET_READY_PRIORITY( pxTCB->uxPriority );
			}
			else
			{
    
    
				mtCOVERAGE_TEST_MARKER();
			}

			/* 如果任务在等待事件,也从等待事件列表中移除 */
			if( listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) != NULL )
			{
    
    
				( void ) uxListRemove( &( pxTCB->xEventListItem ) );
			}
			else
			{
    
    
				mtCOVERAGE_TEST_MARKER();
			}
			//  将任务状态添加到挂起列表中
			vListInsertEnd( &xSuspendedTaskList, &( pxTCB->xStateListItem ) );

			#if( configUSE_TASK_NOTIFICATIONS == 1 )
			{
    
    
				if( pxTCB->ucNotifyState == taskWAITING_NOTIFICATION )
				{
    
    
					/* The task was blocked to wait for a notification, but is
					now suspended, so no notification was received. */
					pxTCB->ucNotifyState = taskNOT_WAITING_NOTIFICATION;
				}
			}
			#endif
		}
		taskEXIT_CRITICAL();

		if( xSchedulerRunning != pdFALSE )
		{
    
    
			/* 重置下一个预期的解除阻塞时间,重新计算一下还要多长时间执行下一个任务。如果下个任务的解锁,刚好是被挂起的那个任务,那么变量 NextTaskUnblockTime 就不对了,所以要重新从延时列表中获取一下。 */
			taskENTER_CRITICAL();
			{
    
    
				prvResetNextTaskUnblockTime();
			}
			taskEXIT_CRITICAL();
		}
		else
		{
    
    
			mtCOVERAGE_TEST_MARKER();
		}

		if( pxTCB == pxCurrentTCB )
		{
    
    
			if( xSchedulerRunning != pdFALSE )
			{
    
    
				/* 当前的任务已经被挂起。 */
				configASSERT( uxSchedulerSuspended == 0 );
                /*调度器在运行时,如果这个挂起的任务是当前任务,立即切换任务。*/
				portYIELD_WITHIN_API();
			}
			else
			{
    
    
				/* 调度器未运行(xSchedulerRunning == pdFALSE ),
				但 pxCurrentTCB 指向的任务刚刚被暂停,
				所以必须调整 pxCurrentTCB 以指向其他任务。
				首先调用函数 listCURRENT_LIST_LENGTH()
				判断一下系统中所有的任务是不是都被挂起了,
				也就是查看列表 xSuspendedTaskList
				的长度是不是等于 uxCurrentNumberOfTasks,
				事实上并不会发生这种情况,
				因为空闲任务是不允许被挂起和阻塞的,
				必须保证系统中无论如何都有一个任务可以运行
				 */
				if( listCURRENT_LIST_LENGTH( &xSuspendedTaskList ) == uxCurrentNumberOfTasks )
				{
    
    
					/* 没有其他任务准备就绪,因此将 pxCurrentTCB 设置回 NULL,
					以便在创建下一个任务时 pxCurrentTCB 将被设置为指向它,
					实际上并不会执行到这里 */
					pxCurrentTCB = NULL;
				}
				else
				{
    
    
                    /*有其他任务,则切换到其他任务*/
					vTaskSwitchContext();
				}
			}
		}
		else
		{
    
    
			mtCOVERAGE_TEST_MARKER();
		}
	}

任务的挂起与恢复函数在很多时候都是很有用的,比如我们想暂停某个任务运行一段时间,但是我们又需要在其恢复的时候继续工作,那么删除任务是不可能的,因为删除了任务的话,任务的所有的信息都是不可能恢复的了,删除是完完全全删除了,里面的资源都被系统释放掉,但是挂起任务就不会这样子,调用挂起任务函数,仅仅是将任务进入挂起态,其内部的资源都会保留下来,同时也不会参与系统中任务的调度,当调用恢复函数的时候,整个任务立即从挂起态进入就绪态,并且参与任务的调度,如果该任务的优先级是当前就绪态优先级最高的任务,那么立即会按照挂起前的任务状态继续执行该任务,从而达到我们需要的效果,注意,是继续执行,也就是说,挂起任务之前是什么状态,都会被系统保留下来,在恢复的瞬间,继续执行。

其挂起代码可以如下:

void ReceiveTask(void const * argument)
{
    
    
  /* USER CODE BEGIN ReceiveTask */
  /* Infinite loop */
	int i = 0;
  for(;;)
  {
    
    
		printf("Task1 count: %d ----------\r\n",i++);
		if(i==10)			vTaskSuspend(NULL);
    osDelay(1000);
  }
  /* USER CODE END ReceiveTask */
}

vTaskSuspendAll()

将所有的任务进行挂起,这个函数是可以进行嵌套的,其实就是在挂起调度器。

调度器被挂起后则不能进行上下文切换,但是中断还是使能的。 当调度器被挂起的时候,如果有中断需要进行上下文切换, 那么这个任务将会被挂起,在调度器恢复之后才执行切换任务。

void vTaskSuspendAll( void )
{
    
    
	//uxSchedulerSuspended 用于记录调度器是否被挂起,该变量默认初始值为 pdFALSE,表明调度器是没被挂起的,每调用一次 vTaskSuspendAll()函数就将变量加一,用于记录调用了多少次 vTaskSuspendAll()函数。
	++uxSchedulerSuspended;
}

调度器恢复可以调用 xTaskResumeAll() 函数,调用了多少次的 vTaskSuspendAll() 就要调用多少次xTaskResumeAll()进行恢复

vTaskResume()

任务恢复就是让挂起的任务重新进入就绪状态,恢复的任务会保留挂起前的状态信息,在恢复的时候根据挂起时的状态继续运行。如果被恢复任务在所有就绪态任务中,处于最高优先级列表的第一位,那么系统将进行任务上下文的切换。如果想使用这个函数这个时候需要将宏INCLUDE_vTaskSuspend配置为1.

void vTaskResume( TaskHandle_t xTaskToResume )
{
    
    
    /* 根据 xTaskToResume 获取对应的任务控制块 */
	TCB_t * const pxTCB = ( TCB_t * ) xTaskToResume;

    /* 检查要恢复的任务是否被挂起,
    如果没被挂起,恢复调用任务没有意义 */
    configASSERT( xTaskToResume );

    //该参数不能为 NULL,同时也无法恢复当前正在执行的任务.
    if( ( pxTCB != NULL ) && ( pxTCB != pxCurrentTCB ) )
    {
    
    
        //进入临界区
        taskENTER_CRITICAL();
        {
    
    
            if( prvTaskIsTaskSuspended( pxTCB ) != pdFALSE )
            {
    
    
                traceTASK_RESUME( pxTCB );

                /* 由于我们处于临界区,即使任务被挂起,我们也可以访问任务的状态列表。将要恢复的任务从挂起列表中删除*/
                ( void ) uxListRemove(  &( pxTCB->xStateListItem ) );
                /* 将要恢复的任务添加到就绪列表中去*/
                prvAddTaskToReadyList( pxTCB );

                /*如果刚刚恢复的任务优先级比当前任务优先级更高,则需要进行任务的切换*/
                if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority )
                {
    
    
                    /* 因为恢复的任务在当前情况下的优先级最高,调用 taskYIELD_IF_USING_PREEMPTION()进行一次任务切换 */
                    taskYIELD_IF_USING_PREEMPTION();
                }
                else
                {
    
    
                    mtCOVERAGE_TEST_MARKER();
                }
            }
            else
            {
    
    
                mtCOVERAGE_TEST_MARKER();
            }
        }
        /* 退出临界区 */
        taskEXIT_CRITICAL();
    }
    else
    {
    
    
        mtCOVERAGE_TEST_MARKER();
    }
}

vTaskResume()函数用于恢复挂起的任务。无论任务在挂起时候调用过多少次这个vTaskSuspend()函数,也只需调用一次 vTaskResume ()函数即可将任务恢复运行,当然,无论调用多少次的 vTaskResume()函数,也只在任务是挂起态的时候才进行恢复。可以创建如下两个任务看到其挂起,恢复的效果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZCXqfXn3-1682496278381)(C:\Users\user\AppData\Roaming\Typora\typora-user-images\image-20230419194745450.png)]

void ReceiveTask(void const * argument)
{
    
    
  /* USER CODE BEGIN ReceiveTask */
  /* Infinite loop */
	int i = 0;
  for(;;)
  {
    
    
		printf("Task1 count: %d ----------\r\n",i++);
		if(i%10==0)			vTaskSuspend(NULL);
    osDelay(1000);
  }
  /* USER CODE END ReceiveTask */
}


void SendTask(void const * argument)
{
    
    
  /* USER CODE BEGIN SendTask */
  /* Infinite loop */
	int i = 0;
  for(;;)
  {
    
    
		printf("Task2 count: %d\r\n",i++);
		if(i==15)				vTaskResume(ReceiveHandle);
    osDelay(1000);
  }
  /* USER CODE END SendTask */
}

xTaskResumeFromISR()

xTaskResumeFromISR()与 vTaskResume()一样都是用于恢复被挂起的任务,不一样的是 xTaskResumeFromISR() 专门用在中断服务程序中。无 论 通 过 调 用 一 次 或 多 次vTaskSuspend()函数而被挂起的任务,也只需调用一次 xTaskResumeFromISR()函数即可解挂

这个函数使用前需要把宏INCLUDE_vTaskSuspendINCLUDE_vTaskResumeFromISR 都定义为 1 才有效。

BaseType_t xTaskResumeFromISR( TaskHandle_t xTaskToResume )
{
    
    
    //定义一个是否需要进行任务切换的变量 xYieldRequired,默认为pdFALSE,当任务恢复成功并且需要任务切换的话则重置为 pdTRUE,以表示需要进行任务切换。
    BaseType_t xYieldRequired = pdFALSE;
    //根据 xTaskToResume 任务句柄获取对应的任务控制块。
    TCB_t * const pxTCB = ( TCB_t * ) xTaskToResume;
    //用于保存关闭中断的状态
    UBaseType_t uxSavedInterruptStatus;
	//检查要恢复的任务是存在,如果不存在,调用恢复任务函数没有任何意义。
    configASSERT( xTaskToResume );

    portASSERT_IF_INTERRUPT_PRIORITY_INVALID();
	
    //调用 portSET_INTERRUPT_MASK_FROM_ISR()函数设置 basepri寄存器用于屏蔽系统可管理的中断,防止被处理被其他中断打断,当 basepri 设置为configMAX_SYSCALL_INTERRUPT_PRIORITY 的时候(该宏在 FreeRTOSConfig.h 中定义,现在配置为 5),会让系统不响应比该优先级低的中断,而优先级比之更高的中断则不受影响。
    uxSavedInterruptStatus = portSET_INTERRUPT_MASK_FROM_ISR();
    {
    
    
        if( prvTaskIsTaskSuspended( pxTCB ) != pdFALSE )
        {
    
    
            traceTASK_RESUME_FROM_ISR( pxTCB );

            /*  检查可以访问的就绪列表,检查调度器是否被挂起 */
            if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE )
            {
    
    
                /* 如果刚刚恢复的任务优先级比当前任务优先级更高需要进行一次任务的切换
                xYieldRequired = pdTRUE 表示需要进行任务切换 */
                if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority )
                {
    
    
                    xYieldRequired = pdTRUE;
                }
                else
                {
    
    
                    mtCOVERAGE_TEST_MARKER();
                }
				//可以访问就绪列表,因此可以将任务从挂起列表删除,然后添加到就绪列表中。
                ( void ) uxListRemove( &( pxTCB->xStateListItem ) );
                prvAddTaskToReadyList( pxTCB );
            }
            else
            {
    
    
                /*  无法访问就绪列表,因此任务将被添加到待处理的就绪列表中,直到调度器被恢复再进行任务的处理。*/
                //因为 uxSchedulerSuspended 调度器被挂起,无法访问就绪列表,因此任务将被添加到待处理的就绪列表中,直到调度器被恢复再进行任务的处理。
                vListInsertEnd( &( xPendingReadyList ), &( pxTCB->xEventListItem ) );
            }
        }
        else
        {
    
    
            mtCOVERAGE_TEST_MARKER();
        }
    }
    //调用 portCLEAR_INTERRUPT_MASK_FROM_ISR()函数清除basepri 的设置,恢复屏蔽的中断。
    portCLEAR_INTERRUPT_MASK_FROM_ISR( uxSavedInterruptStatus );
	//返回 xYieldRequired 结果,在外部选择是否进行任务切换。
    return xYieldRequired;
}

使用此函数需要注意:

  • 当函数的返回值为 pdTRUE 时:恢复运行的任务的优先级等于或高于正在运行的任 务 , 表 明 在 中 断 服 务 函 数 退 出 后 必 须 进 行 一 次 上 下 文 切 换 , 使 用portYIELD_FROM_ISR()进行上下文切换。当函数的返回值为 pdFALSE 时:恢复运行的任务的优先级低于当前正在运行的任务,表明在中断服务函数退出后不需要进行上下文切换。
  • xTaskResumeFromISR() 通常被认为是一个危险的函数,因为它的调用并非是固定的,中断可能随时来来临。所以,xTaskResumeFromISR()不能用于任务和中断间的同步,如果中断恰巧在任务被挂起之前到达,这就会导致一次中断丢失(任务还没有挂起,调用 xTaskResumeFromISR()函数是没有意义的,只能等下一次中断)。这种情况下,可以使用信号量或者任务通知来同步就可以避免这种情况。

使用:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FSPM7wI1-1682496278383)(C:\Users\user\AppData\Roaming\Typora\typora-user-images\image-20230419200514966.png)]

xTaskResumeAll()

BaseType_t xTaskResumeAll( void )
{
    
    
    TCB_t *pxTCB = NULL;
    BaseType_t xAlreadyYielded = pdFALSE;

	/* 如果 uxSchedulerSuspended 为 0,则此函数与先前对 vTaskSuspendAll()的调用不匹配,不需要调用 xTaskResumeAll()恢复调度器。 */
	configASSERT( uxSchedulerSuspended );

	/*进入临界区*/
	taskENTER_CRITICAL();
	{
    
    
		--uxSchedulerSuspended;

		if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE )
		{
    
    
			if( uxCurrentNumberOfTasks > ( UBaseType_t ) 0U )
			{
    
    
				/* 将任何准备好的任务从待处理就绪列表移动到相应的就绪列表中。 */
				while( listLIST_IS_EMPTY( &xPendingReadyList ) == pdFALSE )
				{
    
    
					pxTCB = ( TCB_t * ) listGET_OWNER_OF_HEAD_ENTRY( ( &xPendingReadyList ) );
					( void ) uxListRemove( &( pxTCB->xEventListItem ) );
					( void ) uxListRemove( &( pxTCB->xStateListItem ) );
					prvAddTaskToReadyList( pxTCB );

					/*  如果移动的任务的优先级高于当前任务,
					需要进行一次任务的切换 */
					if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority )
					{
    
    
                        //任务切换
						xYieldPending = pdTRUE;
					}
					else
					{
    
    
						mtCOVERAGE_TEST_MARKER();
					}
				}

				if( pxTCB != NULL )
				{
    
    
					/*在调度器被挂起时,任务被解除阻塞,这可能阻止了重新计算下一个解除阻塞时间,在这种情况下,重置下一个任务的解除阻塞时间*/
					prvResetNextTaskUnblockTime();
				}

				/*在挂起时有滴答定时器的计时,并且在这段时间有任务解除阻塞,由于调度器的挂起导致
				没法切换任务,当恢复调度器的时候应立即处理这些任务。
				这样确保了滴答定时器的计数不会滑动,
				并且任何在延时的任务都会在正确的时间恢复。*/
				{
    
    
					UBaseType_t uxPendedCounts = uxPendedTicks; /* Non-volatile copy. */

					if( uxPendedCounts > ( UBaseType_t ) 0U )
					{
    
    
						do
						{
    
    
                            // 调用 xTaskIncrementTick()函数查找是否有待进行切换的任务,如果有则应该进行任务切换
							if( xTaskIncrementTick() != pdFALSE )
							{
    
    
								xYieldPending = pdTRUE;
							}
							else
							{
    
    
								mtCOVERAGE_TEST_MARKER();
							}
							--uxPendedCounts;
						} while( uxPendedCounts > ( UBaseType_t ) 0U );

						uxPendedTicks = 0;
					}
					else
					{
    
    
						mtCOVERAGE_TEST_MARKER();
					}
				}

				if( xYieldPending != pdFALSE )
				{
    
    
					#if( configUSE_PREEMPTION != 0 )
					{
    
    
                        //如果需要任务切换,则调用taskYIELD_IF_USING_PREEMPTION()函数发起一次任务切换。
						xAlreadyYielded = pdTRUE;
					}
					#endif
					taskYIELD_IF_USING_PREEMPTION();
				}
				else
				{
    
    
					mtCOVERAGE_TEST_MARKER();
				}
			}
		}
		else
		{
    
    
			mtCOVERAGE_TEST_MARKER();
		}
	}
	taskEXIT_CRITICAL();

	return xAlreadyYielded;
}

xTaskResumeAll 函数的使用方法很简单,但是要注意,调用了多少次vTaskSuspendAll()函数就必须同样调用多少次 xTaskResumeAll()函数。

vTaskDelete()

vTaskDelete()用于删除一个任务。当一个任务删除另外一个任务时,形参为要删除任务创建时返回的任务句柄,如果是删除自身, 则形参为 NULL。 若要使用这个函数,这个时候可以将宏 INCLUDE_vTaskDelete设定为1。删除的任务将从所有就绪,阻塞,挂起和事件列表中删除。

void vTaskDelete( TaskHandle_t xTaskToDelete )
{
    
    
	TCB_t *pxTCB;

    taskENTER_CRITICAL();
    {
    
    
        /* 获取任务控制块,如果 xTaskToDelete 为 null
       则删除任务自身 */
        pxTCB = prvGetTCBFromHandle( xTaskToDelete );

        /*  将任务从就绪列表中移除 */
        if( uxListRemove( &( pxTCB->xStateListItem ) ) == ( UBaseType_t ) 0 )
        {
    
    
            /*清除任务的就绪优先级变量中的标志位*/
            taskRESET_READY_PRIORITY( pxTCB->uxPriority );
        }
        else
        {
    
    
            mtCOVERAGE_TEST_MARKER();
        }

        /* 如果当前任务在等待事件,那么将任务从事件列表中移除 */
        if( listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) != NULL )
        {
    
    
            ( void ) uxListRemove( &( pxTCB->xEventListItem ) );
        }
        else
        {
    
    
            mtCOVERAGE_TEST_MARKER();
        }

        /* 增加uxTaskNumber也使内核调试器可以检测到任务列表需要重新生成。 */
        uxTaskNumber++;

        if( pxTCB == pxCurrentTCB )
        {
    
    
            /* 任务正在删除自己。 这不能在任务本身内完成,
            因为需要上下文切换到另一个任务。
            将任务放在结束列表中。空闲任务会检查结束
            列表并释放掉删除的任务控制块
            和已删除任务的堆栈的任何内存 */
            vListInsertEnd( &xTasksWaitingTermination, &( pxTCB->xStateListItem ) );

            /* 增加 uxDeletedTasksWaitingCleanUp 变量,
            记录有多少个任务需要释放内存,
           以便空闲任务知道有一个已删除的任务,然后进行内存释放,
           空闲任务会检查结束列表 xTasksWaitingTermination*/
            ++uxDeletedTasksWaitingCleanUp;

            /*任务删除钩子函数 */
            portPRE_TASK_DELETE_HOOK( pxTCB, &xYieldPending );
        }
        else
        {
    
    
            // 当前任务数减一,uxCurrentNumberOfTasks 是全局变量,用于记录当前的任务数量
            --uxCurrentNumberOfTasks;
            // 删除任务控制块
            prvDeleteTCB( pxTCB );

            /* 重置下一个任务的解除阻塞时间。重新计算一下
            还要多长时间执行下一个任务,如果下个任务的解锁,
            刚好是被删除的任务,那么这就是不正确的,
            因为删除的任务对调度器而言是不可见的,
            所以调度器是无法对删除的任务进行调度,
            所以要重新从延时列表中获取下一个要解除阻塞的任务。
            它是从延时列表的头部来获取的任务 TCB,延时列表是按延时时间排序的 */
            prvResetNextTaskUnblockTime();
        }

        traceTASK_DELETE( pxTCB );
    }
    taskEXIT_CRITICAL();

    /* 如删除的是当前的任务,则需要发起一次任务切换 */
    if( xSchedulerRunning != pdFALSE )
    {
    
    
        if( pxTCB == pxCurrentTCB )
        {
    
    
            configASSERT( uxSchedulerSuspended == 0 );
            portYIELD_WITHIN_API();
        }
        else
        {
    
    
            mtCOVERAGE_TEST_MARKER();
        }
    }
}

对于增加变量uxDeletedTasksWaitingCleanUp的值,该变量需要用于记录有多少个任务需要释放内存,以便空闲任务知道有多少个已删除的任务需要进行内存释放,空闲任务会检查结束列表xTasksWaitingTermination 并且释放对应删除任务的内存空间,主要操作函数在于**prvCheckTasksWaitingTermination()**来进行如下的操作,该函数在prvIdleTask中调用:

static void prvCheckTasksWaitingTermination( void )
{
    
    

	/** 这个函数是被空闲任务调用的 prvIdleTask **/

	#if ( INCLUDE_vTaskDelete == 1 )
	{
    
    
		TCB_t *pxTCB;

		/* uxDeletedTasksWaitingCleanUp 这个变量的值用于记录需要进行内存释放的任务个数,
		防止在空闲任务中过于频繁地调用 vTaskSuspendAll(). */
		while( uxDeletedTasksWaitingCleanUp > ( UBaseType_t ) 0U )
		{
    
    
			taskENTER_CRITICAL();
			{
    
    
                //获取对应任务控制块
				pxTCB = ( TCB_t * ) listGET_OWNER_OF_HEAD_ENTRY( ( &xTasksWaitingTermination ) );
                //将任务从状态列表中删除
				( void ) uxListRemove( &( pxTCB->xStateListItem ) );
                // 当前任务个数减一
				--uxCurrentNumberOfTasks;
                //  uxDeletedTasksWaitingCleanUp 的值减一,直到为 0 退出循环
				--uxDeletedTasksWaitingCleanUp;
			}
			taskEXIT_CRITICAL();
			//删除堆栈
            //这个函数的作用是在任务删除自身的时候才起作用,删除其他任务的时候是直接在删除函数中将其他任务的内存释放掉,不需要在空闲任务中释放。
			prvDeleteTCB( pxTCB );
		}
	}
	#endif /* INCLUDE_vTaskDelete */
}

删除任务时,只会自动释放内核本身分配给任务的内存。应用程序(而不是内核)分配给任务的内存或任何其他资源必须是删除任务时由应用程序显式释放,否则会导致内存泄漏。

vTaskDelay()

使用此函数需要将宏INCLUDE_vTaskDelay置为1,在任务中,每个任务需要是死循环,并且是必须要有阻塞的情况,否则低优先级的任务则无法执行。此函数用于阻塞延时,调用该函数后任务将进入阻塞态,让出CPU资源。延时的时长由形参 xTicksToDelay 决定,单位为系统节拍周期。vTaskDelay()并不适用与周期性执行任务的场合。此外,其它任务和中断活动, 也会影响到 vTaskDelay()的调用(比如调用前高优先级任务抢占了当前任务),进而影响到任务的下一次执行的时间。(相对性延时)

void vTaskDelay( const TickType_t xTicksToDelay )
{
    
    
BaseType_t xAlreadyYielded = pdFALSE;

    /*  延时时间要大于 0 个 tick,否则会进行强制切换任务 */
    if( xTicksToDelay > ( TickType_t ) 0U )
    {
    
    
        configASSERT( uxSchedulerSuspended == 0 );
        // 挂起任务调度器
        vTaskSuspendAll();
        {
    
    
            traceTASK_DELAY();

            /* 将任务添加到延时列表*/
            prvAddCurrentTaskToDelayedList( xTicksToDelay, pdFALSE );
        }
        xAlreadyYielded = xTaskResumeAll();
    }
    else
    {
    
    
        mtCOVERAGE_TEST_MARKER();
    }

    /*  强制切换任务,将 PendSV 的 bit28 置 1. */
    if( xAlreadyYielded == pdFALSE )
    {
    
    
        portYIELD_WITHIN_API();
    }
    else
    {
    
    
        mtCOVERAGE_TEST_MARKER();
    }
}

对于将任务添加到延时列表使用的函数为prvAddCurrentTaskToDelayedList:

static void prvAddCurrentTaskToDelayedList( TickType_t xTicksToWait, const BaseType_t xCanBlockIndefinitely )
{
    
    
    TickType_t xTimeToWake;
    const TickType_t xConstTickCount = xTickCount;

	/* 在将任务添加到阻止列表之前,从就绪列表中删除任务,
	因为两个列表都使用相同的列表项。 */
	if( uxListRemove( &( pxCurrentTCB->xStateListItem ) ) == ( UBaseType_t ) 0 )
	{
    
    
		portRESET_READY_PRIORITY( pxCurrentTCB->uxPriority, uxTopReadyPriority );
	}
	else
	{
    
    
		mtCOVERAGE_TEST_MARKER();
	}

	#if ( INCLUDE_vTaskSuspend == 1 )
	{
    
    
		if( ( xTicksToWait == portMAX_DELAY ) && ( xCanBlockIndefinitely != pdFALSE ) )
		{
    
    
			/* 支持挂起,则当前任务挂起,直接将任务添加到挂起列表,而不是延时列表 */
			vListInsertEnd( &xSuspendedTaskList, &( pxCurrentTCB->xStateListItem ) );
		}
		else
		{
    
    
			/* 计算唤醒任务时间 */
			xTimeToWake = xConstTickCount + xTicksToWait;

			/* 列表项按照唤醒时间顺序插入. */
			listSET_LIST_ITEM_VALUE( &( pxCurrentTCB->xStateListItem ), xTimeToWake );

			if( xTimeToWake < xConstTickCount )
			{
    
    
				/* 唤醒时间如果溢出则加入到溢出列表中 */
				vListInsert( pxOverflowDelayedTaskList, &( pxCurrentTCB->xStateListItem ) );
			}
			else
			{
    
    
				/* 没有溢出,添加到延时列表中 */
				vListInsert( pxDelayedTaskList, &( pxCurrentTCB->xStateListItem ) );

				/* 如果进入阻塞状态的任务被放置在被阻止任务列表的头部,也就是下一个要唤醒的任务就是当前任务,那么就需要更新xNextTaskUnblockTime 的值 */
				if( xTimeToWake < xNextTaskUnblockTime )
				{
    
    
					xNextTaskUnblockTime = xTimeToWake;
				}
				else
				{
    
    
					mtCOVERAGE_TEST_MARKER();
				}
			}
		}
	}
}

任务的延时在实际中运用特别多,因为需要暂停一个任务,让任务放弃 CPU,延时结束后再继续运行该任务,如果任务中没有阻塞的话,比该任务优先级低的任务则无法得到CPU 的使用权,就无法运行。

vTaskDelayUntil()

这个函数是绝对延时函数,这个绝对延时常用于较精确的周期运行任务,比如我有一个任务,希望它以固定频率定期执行,而不受外部的影响,任务从上一次运行开始到下一次运行开始的时间间隔是绝对的,而不是相对的。需要设置宏INCLUDE_vTaskDelayUntil

与vTaskDelay()的区别在于:

vTaskDelay ()的延时是相对的,是不确定的,它的延时是等 vTaskDelay ()调用完毕后开始计算的。并且 vTaskDelay ()延时的时间到了之后,如果有高优先级的任务或者中断正在执行,被延时阻塞的任务并不会马上解除阻塞,所有每次执行任务的周期并不完全确定。

而vTaskDelayUntil()延时是绝对的,适用于周期性执行的任务。当(*pxPreviousWakeTime + xTimeIncrement)时间到达后,vTaskDelayUntil()函数立刻返回,如果任务是最高优先级的,那么任务会立马解除阻塞。

void vTaskDelayUntil( TickType_t * const pxPreviousWakeTime, const TickType_t xTimeIncrement )
{
    
    
    TickType_t xTimeToWake;
    BaseType_t xAlreadyYielded, xShouldDelay = pdFALSE;

    configASSERT( pxPreviousWakeTime );
    configASSERT( ( xTimeIncrement > 0U ) );
    configASSERT( uxSchedulerSuspended == 0 );

    vTaskSuspendAll();
    {
    
    
        /* 获取开始进行延时的时间点*/
        const TickType_t xConstTickCount = xTickCount;

        /* 计算延时到达的时间,也就是唤醒任务的时间 */
        //周期循环时间
        xTimeToWake = *pxPreviousWakeTime + xTimeIncrement;

        /*pxPreviousWakeTime 中保存的是上次唤醒时间,唤醒后需要一定时间执行任务主体代码,
        如果上次唤醒时间大于当前时间,说明节拍计数器溢出了*/
        if( xConstTickCount < *pxPreviousWakeTime )
        {
    
    
            /* 如果唤醒的时间小于上次唤醒时间,
            并且唤醒时间大于开始计时的时间,
            这样子就是相当于没有溢出,
            也就是保了证周期性延时时间大于任务主体代码的执行时间 */
            if( ( xTimeToWake < *pxPreviousWakeTime ) && ( xTimeToWake > xConstTickCount ) )
            {
    
    
                xShouldDelay = pdTRUE;
            }
            else
            {
    
    
                mtCOVERAGE_TEST_MARKER();
            }
        }
        else
        {
    
    
            /* 只是唤醒时间溢出的情况
           	或者都没有溢出,
            保证了延时时间大于任务主体代码的执行时间. */
            if( ( xTimeToWake < *pxPreviousWakeTime ) || ( xTimeToWake > xConstTickCount ) )
            {
    
    
                xShouldDelay = pdTRUE;
            }
            else
            {
    
    
                mtCOVERAGE_TEST_MARKER();
            }
        }

        /*  更新上一次的唤醒时间 */
        *pxPreviousWakeTime = xTimeToWake;

        if( xShouldDelay != pdFALSE )
        {
    
    
            traceTASK_DELAY_UNTIL( xTimeToWake );

            /* prvAddCurrentTaskToDelayedList()函数需要的是阻塞时间
            而不是唤醒时间,因此需要减去当前的滴答计数 */
            prvAddCurrentTaskToDelayedList( xTimeToWake - xConstTickCount, pdFALSE );
        }
        else
        {
    
    
            mtCOVERAGE_TEST_MARKER();
        }
    }
    xAlreadyYielded = xTaskResumeAll();

    /* 强制执行一次上下文切换 */
    if( xAlreadyYielded == pdFALSE )
    {
    
    
        portYIELD_WITHIN_API();
    }
    else
    {
    
    
        mtCOVERAGE_TEST_MARKER();
    }
}

其实两个不同在于调用prvAddCurrentTaskToDelayedList函数中的形参不同,计算延时到达的时间,也就是唤醒任务的时间,由于变量xTickCount 与 xTimeToWake 可能会溢出,所以程序必须检测各种溢出情况,并且要保证延时周期不得小于任务主体代码执行时间,才能保证绝对延时的正确性:

xTimeIncrement:任务周期时间。

pxPreviousWakeTime:上一次唤醒任务的时间点。

xTimeToWake:本次要唤醒任务的时间点。

xConstTickCount:进入延时的时间点。

  • pxPreviousWakeTime 中保存的是上次唤醒时间,唤醒后需要一定时间执行任务主体代码,如果上次唤醒时间大于当前时间,说明节拍计数器溢出了
  • 如果本次任务的唤醒时间小于上次唤醒时间,但是大于开始进入延时的时间,进入延时的时间与任务唤醒时间都已经溢出了,这样子就可以看做没有溢出,其实也就是保证了周期性延时时间大于任务主体代码的执行时间

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Q6X4yqDa-1682496278384)(C:\Users\user\AppData\Roaming\Typora\typora-user-images\image-20230420095329567.png)]

  • 只是唤醒时间 xTimeToWake 溢出的情况,或者是 xTickCount 与xTimeToWake 都没溢出的情况,都是符合要求的,因为都保证了周期性延时时间大于任务主体代码的执行时间。

只有任务唤醒时间溢出:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zaeFKIol-1682496278385)(C:\Users\user\AppData\Roaming\Typora\typora-user-images\image-20230420095619908.png)]

xTickCount 与 xTimeToWake 都没溢出(正常情况):

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VFxlbp39-1682496278387)(C:\Users\user\AppData\Roaming\Typora\typora-user-images\image-20230420095705658.png)]

可以看出无论是溢出还是没有溢出,都要求在下次唤醒任务之前,当前任务主体代码必须被执行完。也就是说任务执行的时间必须小于任务周期时间 xTimeIncrement。每次产生系统节拍中断,都会检查这两个延时列表,查看延时的任务是否到期,如果时间到,则将任务从延时列表中删除,重新加入就绪列表,任务从阻塞态变成就绪态,如果此时的任务优先级是最高的,则会触发一次上下文切换。绝对延时函数使用如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7feMIiw3-1682496278390)(C:\Users\user\AppData\Roaming\Typora\typora-user-images\image-20230420100711890.png)]

在使用的时候要将延时时间转化为系统节拍,在任务主体之前要调用延时函数

任务会先调用 vTaskDelayUntil()使任务进入阻塞态,等到时间到了就从阻塞中解除,然后执行主体代码,任务主体代码执行完毕。会继续调用 vTaskDelayUntil()使任务进入阻塞态,然后就是循环这样子执行。即使任务在执行过程中发生中断,那么也不会影响这个任务的运行周期,仅仅是缩短了阻塞的时间而已,到了要唤醒的时间依旧会将任务唤醒。

任务设计要点

FreeRTOS中程序运行的上下文包括:

  • 中断服务函数
  • 普通任务
  • 空闲任务

中断服务函数

中断服务函数是一种需要特别注意的上下文环境,它运行在非任务的执行环境下(一般为芯片的一种特殊运行模式(也被称作特权模式)),在这个上下文环境中不能使用挂起当前任务的操作,不允许调用任何会阻塞运行的 API 函数接口,另外需要注意的是,中断服务程序最好保持精简短小,快进快出,一般在中断服务函数中只做标记事件的发生,然后通知任务,让对应任务去执行相关处理,因为中断服务函数的优先级高于任何优先级的任务,如果中断处理时间过长,将会导致整个系统的任务无法正常运行。

普通任务

做为一个优先级明确的实时系统,如果一个任务中的程序出现了死循环操作(此处的死循环是指没有阻塞机制的任务循环体),那么比这个任务优先级低的任务都将无法执行,当然也包括了空闲任务,因为死循环的时候,任务不会主动让出 CPU,低优先级的任务是不可能得到CPU 的使用权的,而高优先级的任务就可以抢占 CPU。这个情况在实时操作系统中是必须注意的一点,所以在任务中不允许出现死循环。如果一个任务只有就绪态而无阻塞态,势必会影响到其他低优先级任务的执行,所以在进行任务设计时,就应该保证任务在不活跃的时候,任务可以进入阻塞态以交出 CPU 使用权,这就需要我们自己明确知道什么情况下让任务进入阻塞态,保证低优先级任务可以正常运行。

空闲任务

空闲任务(idle 任务)是 FreeRTOS 系统中没有其他工作进行时自动进入的系统任务。FreeRTOS 为了保证这一点,当调用 **vTaskStartScheduler()**时,调度器会自动创建一个空闲任务,空闲任务是一个非常短小的循环。

用户可以通过空闲任务钩子方式,在空闲任务上钩入自己的功能函数。通常这个空闲任务钩子能够完成一些额外的特殊功能,例如系统运行状态的指示,系统省电模式等。

除了空闲任务钩子,FreeRTOS 系统还把空闲任务用于一些其他的功能,比如当系统删除一个任务或一个动态任务运行结束时,在执行删除任务的时候,并不会释放任务的内存空间,只会将任务添加到结束列表中,真正的系统资源回收工作在空闲任务完成,空闲任务是唯一一个不允许出现阻塞情况的任务,因为 FreeRTOS 需要保证系统永远都有一个可运行的任务。

对于空闲任务钩子上挂接的空闲钩子函数,它应该满足以下的条件:

  • 永远不会被挂起
  • 不应该陷入死循环,需要留出部分时间用于系统处理系统资源回收

任务执行的时间

任务的执行时间一般是指两个方面,一是任务从开始到结束的时间,二是任务的周期。一般来说处理时间更短的任务优先级应设置更高一些

0x05 消息队列

队列又称消息队列,是一种常用于任务间通信的数据结构,队列可以在任务与任务间、中断和任务间传递信息,实现了任务接收来自其他任务或中断的不固定长度的消息,任务能够从队列里面读取消息,当队列中的消息是空时,读取消息的任务将被阻塞,用户还可以指定阻塞的任务时间 xTicksToWait,在这段时间中,如果队列为空,该任务将保持阻塞状态以等待队列数据有效。

当队列中有新消息时,被阻塞的任务会被唤醒并处理新消息;当等待的时间超过了指定的阻塞时间,即使队列中尚无有效数据,任务也会自动从阻塞态转为就绪态。消息队列是一种异步的通信方式

任务先得到的是最先进入消息队列的消息,即先进先出原则(FIFO),但是也支持后进先出原则(LIFO)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rNI6bxDq-1682496278393)(C:\Users\user\AppData\Roaming\Typora\typora-user-images\image-20230420103555563.png)]

运作原理

创建消息队列时 FreeRTOS 会先给消息队列分配一块内存空间,这块内存的大小等于消息队列控制块大小加上(单个消息空间大小与消息队列长度的乘积),接着再初始化消息队列,此时消息队列为空。

FreeRTOS 的消息队列控制块由多个元素组成,当消息队列被创建时,系统会为控制块分配对应的内存空间,用于保存消息队列的一些信息如消息的存储位置,头指针 pcHead、尾指针 pcTail、消息大小 uxItemSize 以及队列长度 uxLength 等。

同时每个消息队列都与消息空间在同一段连续的内存空间中,在创建成功的时候,这些内存就被占用了,只有删除了消息队列的时候,这段内存才会被释放掉,创建成功的时候就已经分配好每个消息空间与消息队列的容量,无法更改,每个消息空间可以存放不大于消息大小 uxItemSize 的任意类型的数据,所有消息队列中的消息空间总数即是消息队列的长度,这个长度可在消息队列创建时指定。

任务或者中断服务程序都可以给消息队列发送消息,当发送消息时,如果队列未满或者允许覆盖入队,FreeRTOS 会将消息拷贝到消息队列队尾,否则,会根据用户指定的阻塞超时时间进行阻塞,在这段时间中,如果队列一直不允许入队,该任务将保持阻塞状态以等待队列允许入队。(一直阻塞直到有消息到来)

当其它任务从其等待的队列中读取入了数据(队列未满),该任务将自动由阻塞态转移为就绪态。当等待的时间超过了指定的阻塞时间,即使队列中还不允许入队,任务也会自动从阻塞态转移为就绪态,此时发送消息的任务或者中断程序会收到一个错误码errQUEUE_FULL

紧急消息,发送的位置是消息队列队头而非队尾。

当某个任务试图读一个队列时,其可以指定一个阻塞超时时间。在这段时间中,如果队列为空,该任务将保持阻塞状态以等待队列数据有效。当其它任务或中断服务程序往其等待的队列中写入了数据,该任务将自动由阻塞态转移为就绪态。当等待的时间超过了指定的阻塞时间,即使队列中尚无有效数据,任务也会自动从阻塞态转移为就绪态。

阻塞机制

  • 只有在任务中发送消息才允许进行阻塞状态,而在中断中发送消息不允许带有阻塞机制的,需要调用在中断中发送消息的 API 函数接口,因为发送消息的上下文环境是在中断中,不允许有阻塞的情况。
  • 队列中无可用消息空间时,说明消息队列已满,此时,系统会根据用户指定的阻塞超时时间将任务阻塞,在指定的超时时间内如果还不能完成入队操作,发送消息的任务或者中断服务程序会收到一个错误码 errQUEUE_FULL,然后解除阻塞状态。
  • 假如有多个任务阻塞在一个消息队列中,那么这些阻塞的任务将按照任务优先级进行排序,优先级高的任务将优先获得队列的访问权。

消息队列控制块

typedef struct QueueDefinition
{
    
    
	int8_t *pcHead;					/*pcHead 指向队列消息存储区起始位置,即第一个消息空间。 */
	int8_t *pcTail;					/*< pcTail 指向队列消息存储区结束位置地址 */
	int8_t *pcWriteTo;				/*pcWriteTo 指向队列消息存储区下一个可用消息空间 */

	union							/* 使用联合体用来确保两个互斥的结构体成员不会同时出现 */
	{
    
    
		int8_t *pcReadFrom;			/*当结构体用于队列时,pcReadFrom 指向出队消息空间的最后一个,就是读取消息时候是从 pcReadFrom 指向的空间读取消息内容*/
		UBaseType_t uxRecursiveCallCount;/*用于计数,记录递归互斥量被“调用”的次数。 */
	} u;

	List_t xTasksWaitingToSend;		/*是一个发送消息阻塞列表,用于保存阻塞在此队列的任务,任务按照优先级进行排序,由于队列已满,想要发送消息的任务无法发送消息。*/
	List_t xTasksWaitingToReceive;	/*是一个获取消息阻塞列表,用于保存阻塞在此队列的任务,任务按照优先级进行排序,由于队列是空的,想要获取消息的任务无法获取到消息。 */

	volatile UBaseType_t uxMessagesWaiting;/*用于记录当前消息队列的消息个数,如果消息队列被用于信号量的时候,这个值就表示有效信号量个数。*/
	UBaseType_t uxLength;			/*表示队列的长度,也就是能存放多少消息。*/
	UBaseType_t uxItemSize;			/*表示单个消息的大小。 */

    //这两个成员变量为 queueUNLOCKED 时,表示队列未上锁;当这两个成员变量为queueLOCKED_UNMODIFIED 时,表示队列上锁。
	volatile int8_t cRxLock;		/*队列上锁后,储存从队列收到的列表项数目,也就是出队的数量,如果队列没有上锁,设置为 queueUNLOCKED。*/
	volatile int8_t cTxLock;		/*队列上锁后,储存发送到队列的列表项数目,也就是入队的数量,如果队列没有上锁,设置为 queueUNLOCKED。 */

	#if( ( configSUPPORT_STATIC_ALLOCATION == 1 ) && ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) )
		uint8_t ucStaticallyAllocated;	/*< Set to pdTRUE if the memory used by the queue was statically allocated to ensure no attempt is made to free the memory. */
	#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;

消息队列常用函数

流程:创建消息队列、写队列操作、读队列操作、删除队列。

xQueueCreate()

xQueueCreate()用于创建一个新的队列并返回可用于访问这个队列的队列句柄。队列句柄其实就是一个指向队列数据结构类型的指针

使用xQueueCreate()创建队列时,使用的是动态内存分配,所以要想使用该函数必须在FreeRTOSConfig.h 中把 configSUPPORT_DYNAMIC_ALLOCATION 定义为 1 来使能,这是个用于使能动态内存分配的宏,通常情况下,在 FreeRTOS 中,凡是创建任务,队列,信号量和互斥量等内核对象都需要使用动态内存分配,所以这个宏默认在 FreeRTOS.h 头文件中已经使能(即定义为 1)。

如果想使用静态内存,则可以使用 xQueueCreateStatic() 函数来创建一个队列。使用静态创建消息队列函数创建队列时需要的形参更多,需要的内存由编译的时候预先分配好,一般很少使用这种方法。

#if( configSUPPORT_DYNAMIC_ALLOCATION == 1 )
	#define xQueueCreate( uxQueueLength, uxItemSize ) xQueueGenericCreate( ( uxQueueLength ), ( uxItemSize ), ( queueQUEUE_TYPE_BASE ) )
#endif

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-td9jOcZG-1682496278395)(C:\Users\user\AppData\Roaming\Typora\typora-user-images\image-20230420110650981.png)]

xQueueGenericCreate()

#if( configSUPPORT_DYNAMIC_ALLOCATION == 1 )

	QueueHandle_t xQueueGenericCreate( const UBaseType_t uxQueueLength, const UBaseType_t uxItemSize, const uint8_t ucQueueType )
	{
    
    
	Queue_t *pxNewQueue;
	size_t xQueueSizeInBytes;
	uint8_t *pucQueueStorage;

		configASSERT( uxQueueLength > ( UBaseType_t ) 0 );

		if( uxItemSize == ( UBaseType_t ) 0 )
		{
    
    
			/* 消息空间大小为 0 */
			xQueueSizeInBytes = ( size_t ) 0;
		}
		else
		{
    
    
			/* 分配足够消息存储空间,空间的大小为队列长度*单个消息大小 */
			xQueueSizeInBytes = ( size_t ) ( uxQueueLength * uxItemSize ); 
		}
		//  向系统申请内存,内存大小为消息队列控制块大小+消息存储空间大小
		pxNewQueue = ( Queue_t * ) pvPortMalloc( sizeof( Queue_t ) + xQueueSizeInBytes );

		if( pxNewQueue != NULL )
		{
    
    
			/* 计算出消息存储空间的起始地址 */
			pucQueueStorage = ( ( uint8_t * ) pxNewQueue ) + sizeof( Queue_t );

			#if( configSUPPORT_STATIC_ALLOCATION == 1 )
			{
    
    
				/* Queues can be created either statically or dynamically, so
				note this task was created dynamically in case it is later
				deleted. */
				pxNewQueue->ucStaticallyAllocated = pdFALSE;
			}
			#endif /* configSUPPORT_STATIC_ALLOCATION */

			prvInitialiseNewQueue( uxQueueLength, uxItemSize, pucQueueStorage, ucQueueType, pxNewQueue );
		}
		else
		{
    
    
			traceQUEUE_CREATE_FAILED( ucQueueType );
		}

		return pxNewQueue;
	}

#endif /* configSUPPORT_STATIC_ALLOCATION */

prvInitialiseNewQueue()

static void prvInitialiseNewQueue( const UBaseType_t uxQueueLength, //消息队列长度。
                                  const UBaseType_t uxItemSize,  	//单个消息大小。
                                  uint8_t *pucQueueStorage, 		//存储消息起始地址。
                                  const uint8_t ucQueueType, 		//消息队列类型
                                  Queue_t *pxNewQueue )				//消息队列控制块
{
    
    
	/* 如果configUSE_TRACE_FACILITY未设置为1,则删除编译器关于未使用参数的警告。 */
	( void ) ucQueueType;

	if( uxItemSize == ( UBaseType_t ) 0 )
	{
    
    
		/* 没有为消息存储分配内存,但是 pcHead 指针不能设置为 NULL,
		因为队列用作互斥量时,pcHead 要设置成 NULL。
		这里只是将 pcHead 指向一个已知的区域*/
		pxNewQueue->pcHead = ( int8_t * ) pxNewQueue;
	}
	else
	{
    
    
		/*设置 pcHead 指向存储消息的起始地址 */
		pxNewQueue->pcHead = ( int8_t * ) pucQueueStorage;
	}

	/* 初始化消息队列控制块的其他成员 */
	pxNewQueue->uxLength = uxQueueLength;
	pxNewQueue->uxItemSize = uxItemSize;
    /*重置消息队列*/
	( void ) xQueueGenericReset( pxNewQueue, pdTRUE );

	#if ( configUSE_TRACE_FACILITY == 1 )
	{
    
    
		pxNewQueue->ucQueueType = ucQueueType;
	}
	#endif /* configUSE_TRACE_FACILITY */

	#if( configUSE_QUEUE_SETS == 1 )
	{
    
    
		pxNewQueue->pxQueueSetContainer = NULL;
	}
	#endif /* configUSE_QUEUE_SETS */

	traceQUEUE_CREATE( pxNewQueue );
}
  • ucQueueType:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YDDnP2Rd-1682496278396)(C:\Users\user\AppData\Roaming\Typora\typora-user-images\image-20230420112701755.png)]

xQueueGenericReset()

BaseType_t xQueueGenericReset( QueueHandle_t xQueue, BaseType_t xNewQueue )
{
    
    
	Queue_t * const pxQueue = ( Queue_t * ) xQueue;

	configASSERT( pxQueue );
	//进入临界段
	taskENTER_CRITICAL();
	{
    
    
        //重置消息队列的成员变量,pcTail 指向存储消息内存空间的结束地址。
		pxQueue->pcTail = pxQueue->pcHead + ( pxQueue->uxLength * pxQueue->uxItemSize );
        //当前消息队列中的消息个数 uxMessagesWaiting 为 0。
		pxQueue->uxMessagesWaiting = ( UBaseType_t ) 0U;
        //pcWriteTo 指向队列消息存储区下一个可用消息空间,因为是重置消息队列,就指向消息队列的第一个消息空间,也就是 pcHead 指向的空间。
		pxQueue->pcWriteTo = pxQueue->pcHead;
        //pcReadFrom 指向消息队列最后一个消息空间。
		pxQueue->u.pcReadFrom = pxQueue->pcHead + ( ( pxQueue->uxLength - ( UBaseType_t ) 1U ) * pxQueue->uxItemSize );
        //消息队列没有上锁,设置为 queueUNLOCKED。
		pxQueue->cRxLock = queueUNLOCKED;
		pxQueue->cTxLock = queueUNLOCKED;

		if( xNewQueue == pdFALSE )
		{
    
    
			/* 如果不是新建一个消息队列,那么之前的消息队列可能阻塞了一些任务,需要将其解除阻塞。如果有发送消息任务被阻塞,那么需要将它恢复,而如果任务是因为读取消息而阻塞,那么重置之后的消息队列也是空的,则无需被恢复。 */
			if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToSend ) ) == pdFALSE )
			{
    
    
				if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToSend ) ) != pdFALSE )
				{
    
    
					queueYIELD_IF_USING_PREEMPTION();
				}
				else
				{
    
    
					mtCOVERAGE_TEST_MARKER();
				}
			}
			else
			{
    
    
				mtCOVERAGE_TEST_MARKER();
			}
		}
		else
		{
    
    
			/* 如果是新创建一个消息队列,则需要将 xTasksWaitingToSend 列表与 xTasksWaitingToReceive 列表初始化 */
			vListInitialise( &( pxQueue->xTasksWaitingToSend ) );
			vListInitialise( &( pxQueue->xTasksWaitingToReceive ) );
		}
	}
    //退出临界段
	taskEXIT_CRITICAL();

	/* A value is returned for calling semantic consistency with previous
	versions. */
	return pdPASS;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eLFrHjpy-1682496278398)(C:\Users\user\AppData\Roaming\Typora\typora-user-images\image-20230420114050573.png)]

在创建消息队列的时候,是需要用户自己定义消息队列的句柄的,但是注意了,定义了队列的句柄并不等于创建了队列,创建队列必须是调用消息队列创建函数进行创建(可以是静态也可以是动态创建),否则,以后根据队列句柄使用消息队列的其它函数的时候会发生错误,创建完成会返回消息队列的句柄,用户通过句柄就可使用消息队列进行发送与读取消息队列的操作,如果返回的是 NULL 则表示创建失败。

xQueueCreateStatic()

此函数为消息队列静态创建函数,队列就是一个数据结构,用于任务间的数据的传递。每创建一个新的队列都需要为其分 配 RAM , 一 部 分 用 于 存 储 队 列 的 状 态 , 剩 下 的 作 为 队 列 的 存 储 区 。 使 用xQueueCreateStatic()创建队列时,使用的是静态内存分配,所以要想使用该函数必须在FreeRTOSConfig.h 中把 configSUPPORT_STATIC_ALLOCATION 定义为 1 来使能。

#if( configSUPPORT_STATIC_ALLOCATION == 1 )
	#define xQueueCreateStatic( uxQueueLength, uxItemSize, pucQueueStorage, pxQueueBuffer ) xQueueGenericCreateStatic( ( uxQueueLength ), ( uxItemSize ), ( pucQueueStorage ), ( pxQueueBuffer ), ( queueQUEUE_TYPE_BASE ) )
#endif /* configSUPPORT_STATIC_ALLOCATION */

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qVLthqT3-1682496278400)(C:\Users\user\AppData\Roaming\Typora\typora-user-images\image-20230420142159021.png)]

vQueueDelete()

队列删除函数是根据消息队列句柄直接删除的,删除之后这个消息队列的所有信息都会被系统回收清空,而且不能再次使用这个消息队列了,但是需要注意的是,如果某个消息队列没有被创建,那也是无法被删除的。xQueue 是 vQueueDelete()函数的形参,是消息队列句柄,表示的是要删除哪个想队列。

void vQueueDelete( QueueHandle_t xQueue )
{
    
    
	Queue_t * const pxQueue = ( Queue_t * ) xQueue;
	// 断言
	configASSERT( pxQueue );
	traceQUEUE_DELETE( pxQueue );

	#if ( configQUEUE_REGISTRY_SIZE > 0 )
	{
    
    
        // 将消息队列从注册表中删除
		vQueueUnregisterQueue( pxQueue );
	}
	#endif

	#if( ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) && ( configSUPPORT_STATIC_ALLOCATION == 0 ) )
	{
    
    
		/*  因为用的消息队列是动态分配内存的,所以需要调用vPortFree 来释放消息队列的内存*/
		vPortFree( pxQueue );
	}
	#elif( ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) && ( configSUPPORT_STATIC_ALLOCATION == 1 ) )
	{
    
    
		/* 队列可以是静态分配的,也可以是动态分配的,因此在尝试释放内存之前请进行检查。 */
		if( pxQueue->ucStaticallyAllocated == ( uint8_t ) pdFALSE )
		{
    
    
			vPortFree( pxQueue );
		}
		else
		{
    
    
			mtCOVERAGE_TEST_MARKER();
		}
	}
	#else
	{
    
    
		/* 队列必须是静态分配的,因此不会被删除。避免编译器对未使用的参数发出警告。 */
		( void ) pxQueue;
	}
	#endif /* configSUPPORT_DYNAMIC_ALLOCATION */
}

需要注意的是调用删除消息队列函数前,系统应存在 xQueueCreate()或 xQueueCreateStatic()函数创建的消息队列。此外vQueueDelete()也可用于删除信号量。如果删除消息队列时,有任务正在等待消息,则不应该进行删除操作(官方说的是不允许进行删除操作,但是源码并没有禁止删除的操作,使用的时候注意一下就行了)。

向消息队列发送消息

任务或者中断服务程序都可以给消息队列发送消息,当发送消息时,如果队列未满或者允许覆盖入队,FreeRTOS 会将消息拷贝到消息队列队尾,否则,会根据用户指定的阻塞超时时间进行阻塞,在这段时间中,如果队列一直不允许入队,该任务将保持阻塞状态以等待队列允许入队。当其它任务从其等待的队列中读取入了数据(队列未满),该任务将自动由阻塞态转为就绪态。当任务等待的时间超过了指定的阻塞时间,即使队列中还不允许入队,任务也会自动从阻塞态转移为就绪态,此时发送消息的任务或者中断程序会收到一个错误码errQUEUE_FULL。

发送紧急消息则是加入到队列队头。

xQueueSend()

#define xQueueSend( xQueue, pvItemToQueue, xTicksToWait ) xQueueGenericSend( ( xQueue ), ( pvItemToQueue ), ( xTicksToWait ), queueSEND_TO_BACK )

调用函数 xQueueGenericSend(),该 宏 是 为 了 向 后 兼 容 没 有 包 含 xQueueSendToFront()xQueueSendToBack() 这 两 个 宏 的 FreeRTOS 版 本 。 xQueueSend() 等 同 于xQueueSendToBack()

xQueueSend()用于向队列尾部发送一个队列消息,消息以拷贝的形式入队,而不是以引用的形式。该函数绝对不能在中断服务程序里面被调用,中断中必须使用带有中断保护功能的 xQueueSendFromISR()来代替。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-G5aCYBoa-1682496278402)(C:\Users\user\AppData\Roaming\Typora\typora-user-images\image-20230420144828380.png)]

xQueueSendToBack()

#define xQueueSendToBack( xQueue, pvItemToQueue, xTicksToWait ) xQueueGenericSend( ( xQueue ), ( pvItemToQueue ), ( xTicksToWait ), queueSEND_TO_BACK )

xQueueSendFromISR()

#define xQueueSendFromISR( xQueue, pvItemToQueue, pxHigherPriorityTaskWoken ) xQueueGenericSendFromISR( ( xQueue ), ( pvItemToQueue ), ( pxHigherPriorityTaskWoken ), queueSEND_TO_BACK )

该宏是 xQueueSend()的中断保护版本,用于在中断服务程序中向队列尾部发送一个队列消息,等价于 xQueueSendToBackFromISR()。

xQueueSendToBackFromISR()

#define xQueueSendToBackFromISR( xQueue, pvItemToQueue, pxHigherPriorityTaskWoken ) xQueueGenericSendFromISR( ( xQueue ), ( pvItemToQueue ), ( pxHigherPriorityTaskWoken ), queueSEND_TO_BACK )

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vxALEhCH-1682496278403)(C:\Users\user\AppData\Roaming\Typora\typora-user-images\image-20230420145325873.png)]

xQueueSendToFront()

#define xQueueSendToFront( xQueue, pvItemToQueue, xTicksToWait ) xQueueGenericSend( ( xQueue ), ( pvItemToQueue ), ( xTicksToWait ), queueSEND_TO_FRONT )

宏 展 开 也 是 调 用 函 数 xQueueGenericSend() 。xQueueSendToFront()用于向队列队首发送一个消息。消息以拷贝的形式入队,而不是以引用的形式。该函数绝不能在中断服务程序里面被调用,而是必须使用带有中断保护功能的xQueueSendToFrontFromISR ()来代替。使用方式同 xQueueSend()。

xQueueSendToFrontFromISR()

#define xQueueSendToFrontFromISR( xQueue, pvItemToQueue, pxHigherPriorityTaskWoken ) xQueueGenericSendFromISR( ( xQueue ), ( pvItemToQueue ), ( pxHigherPriorityTaskWoken ), queueSEND_TO_FRONT )

该宏是 xQueueSendToFront()的中断保护版本,用于在中断服务程序中向消息队列队首发送一个消息。,使用方式与 xQueueSendFromISR()函数一致。

xQueueGenericSend()

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 = ( Queue_t * ) xQueue;
	//断言
	configASSERT( pxQueue );
	configASSERT( !( ( pvItemToQueue == NULL ) && ( pxQueue->uxItemSize != ( UBaseType_t ) 0U ) ) );
	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 );
				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 /* configUSE_QUEUE_SETS */

				taskEXIT_CRITICAL();
				return pdPASS;
			}
        	// 队列已满
			else
			{
    
    
				if( xTicksToWait == ( TickType_t ) 0 )
				{
    
    
					/*  如果用户不指定阻塞超时时间,退出 */
					taskEXIT_CRITICAL();
					traceQUEUE_SEND_FAILED( pxQueue );
					return errQUEUE_FULL;
				}
				else if( xEntryTimeSet == pdFALSE )
				{
    
    
					/* 初始化阻塞超时结构体变量,初始化进入
					阻塞的时间 xTickCount 和溢出次数 xNumOfOverflows */
					vTaskInternalSetTimeOutState( &xTimeOut );
					xEntryTimeSet = pdTRUE;
				}
				else
				{
    
    
					/* Entry time was already set. */
					mtCOVERAGE_TEST_MARKER();
				}
			}
		}
		taskEXIT_CRITICAL();
    	//因为接下来的操作系统不允许其他任务访问队列,简单粗暴挂起调度器就不会进行任务切换,但是挂起调度器并不会禁止中断的发生,所以还需给队列上锁
		/* 挂起调度器 */
		vTaskSuspendAll();
    	/* 队列上锁 */
		prvLockQueue( pxQueue );

		/* 检查超时时间是否已经过去了. */
		if( xTaskCheckForTimeOut( &xTimeOut, &xTicksToWait ) == pdFALSE )
		{
    
    
            /* 如果队列还是满的 */
			if( prvIsQueueFull( pxQueue ) != pdFALSE )
			{
    
    
				traceBLOCKING_ON_QUEUE_SEND( pxQueue );
                //  将当前任务添加到队列的等待发送列表中以及阻塞延时列表,延时时间为用户指定的超时时间 xTicksToWait
				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;
		}
	}
}
  • xCopyPosition:具有三个位置:queueSEND_TO_BACK:发送到队尾;queueSEND_TO_FRONT:发送到队头;queueOVERWRITE:以覆盖的方式发送。

从消息队列的入队操作我们可以看出:如果阻塞时间不为 0,则任务会因为等待入队而进入阻塞,在将任务设置为阻塞的过程中,系统不希望有其它任务和中断操作这个队列的 xTasksWaitingToReceive 列表和 xTasksWaitingToSend 列表,因为可能引起其它任务解除阻塞,这可能会发生优先级翻转。比如任务 A 的优先级低于当前任务,但是在当前任务进入阻塞的过程中,任务 A 却因为其它原因解除阻塞了,这显然是要绝对禁止的。因此FreeRTOS 使用挂起调度器禁止其它任务操作队列,因为挂起调度器意味着任务不能切换并且不准调用可能引起任务切换的 API 函数。但挂起调度器并不会禁止中断,中断服务函数仍然可以操作队列事件列表,可能会解除任务阻塞、可能会进行上下文切换,这也是不允许的。于是,解决办法是不但挂起调度器,还要给队列上锁,禁止任何中断来操作队列

xQueueGenericSendFromISR()

这个函数跟 xQueueGenericSend() 函数很像,只不过是 执行的上下文环境是不一样的,xQueueGenericSendFromISR()函数只能用于中断中执行,是不带阻塞机制的。

BaseType_t xQueueGenericSendFromISR( QueueHandle_t xQueue, 							//消息队列句柄
                                    const void * const pvItemToQueue, 				//指针,指向要发送的消息
                                    BaseType_t * const pxHigherPriorityTaskWoken, 	//pxHigherPriorityTaskWoken 称为一个可选参数,并可以设置为 NULL,判断其是否要上下文切换
                                    const BaseType_t xCopyPosition )				//消息队列位置
{
    
    
    BaseType_t xReturn;
    UBaseType_t uxSavedInterruptStatus;
    Queue_t * const pxQueue = ( Queue_t * ) xQueue;

	configASSERT( pxQueue );
	configASSERT( !( ( pvItemToQueue == NULL ) && ( pxQueue->uxItemSize != ( UBaseType_t ) 0U ) ) );
	configASSERT( !( ( xCopyPosition == queueOVERWRITE ) && ( pxQueue->uxLength != 1 ) ) );

	portASSERT_IF_INTERRUPT_PRIORITY_INVALID();

	uxSavedInterruptStatus = portSET_INTERRUPT_MASK_FROM_ISR();
	{
    
    
        //队列未满
		if( ( pxQueue->uxMessagesWaiting < pxQueue->uxLength ) || ( xCopyPosition == queueOVERWRITE ) )
		{
    
    
			const int8_t cTxLock = pxQueue->cTxLock;

			traceQUEUE_SEND_FROM_ISR( pxQueue );

			/* 完成消息拷贝 */
			( void ) prvCopyDataToQueue( pxQueue, pvItemToQueue, xCopyPosition );

			/* 判断队列是否上锁 */
			if( cTxLock == queueUNLOCKED )
			{
    
    
				
				{
    
    
                    //如果有任务再等待获取此消息队列
					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();
					}
				}
				#endif /* configUSE_QUEUE_SETS */
			}
			else
			{
    
    
				/*队列上锁,记录上锁次数,等到任务解除队列锁时,
				使用这个计录数就可以知道有多少数据入队 */
				pxQueue->cTxLock = ( int8_t ) ( cTxLock + 1 );
			}

			xReturn = pdPASS;
		}
		else
		{
    
    
            // 队列是满的,因为 API 执行的上下文环境是中断,所以不能阻塞,直接返回队列已满错误代码 errQUEUE_FULL
			traceQUEUE_SEND_FROM_ISR_FAILED( pxQueue );
			xReturn = errQUEUE_FULL;
		}
	}
	portCLEAR_INTERRUPT_MASK_FROM_ISR( uxSavedInterruptStatus );

	return xReturn;
}

xQueueGenericSendFromISR()函数没有阻塞机制,只能用于中断中发送消息,代码简单了很多,当成功入队后,如果有因为等待出队而阻塞的任务,系统会将该任务解除阻塞,要注意的是,解除了任务并不是会马上运行的,只是任务会被挂到就绪列表中

在执行解除阻塞操作之前,会判断队列是否上锁。如果没有上锁,则可以解除被阻塞的任务,然后根据任务优先级情况来决定是否需要进行任务切换;如果队列已经上锁,则不能解除被阻塞的任务,只能是记录 xTxLock 的值,表示队列上锁期间消息入队的个数,也用来记录可以解除阻塞任务的个数,在队列解锁中会将任务解除阻塞。

向消息队列读取消息函数

当任务试图读队列中的消息时,可以指定一个阻塞超时时间,当且仅当消息队列中有消息的时候,任务才能读取到消息。

在这段时间中,如果队列为空,该任务将保持阻塞状态以等待队列数据有效。当其它任务或中断服务程序往其等待的队列中写入了数据,该任务将自动由阻塞态转为就绪态。当任务等待的时间超过了指定的阻塞时间,即使队列中尚无有效数据,任务也会自动从阻塞态转移为就绪态。

xQueueReceive()

该函数不可以于中断中使用。用于从一个队列中接收消息并把消息从队列中删除

BaseType_t xQueueReceive( QueueHandle_t xQueue, 			//队列句柄。
                         void * const pvBuffer, 			//指针,指向接收到要保存的数据
                         TickType_t xTicksToWait )			//队列为空时阻塞超时的最大时间
{
    
    
    BaseType_t xEntryTimeSet = pdFALSE;
    TimeOut_t xTimeOut;
    Queue_t * const pxQueue = ( Queue_t * ) xQueue;

	for( ;; )
	{
    
    
		taskENTER_CRITICAL();
		{
    
    
			const UBaseType_t uxMessagesWaiting = pxQueue->uxMessagesWaiting;

			/* 看看队列中有没有消息 */
			if( uxMessagesWaiting > ( UBaseType_t ) 0 )
			{
    
    
				/*  拷贝消息到用户指定存放区域 pvBuffer */
				prvCopyDataFromQueue( pxQueue, pvBuffer );
                //读取消息并且消息出队
				traceQUEUE_RECEIVE( pxQueue );
                //获取了消息,当前消息队列的消息个数需要减一
				pxQueue->uxMessagesWaiting = uxMessagesWaiting - ( UBaseType_t ) 1;

				/*  判断一下消息队列中是否有等待发送消息的任务*/
				if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToSend ) ) == pdFALSE )
				{
    
    
                    /* 将任务从阻塞中恢复 */
					if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToSend ) ) != pdFALSE )
					{
    
    
                        /* 如果被恢复的任务优先级比当前任务高,会进行一次任务切换 */
						queueYIELD_IF_USING_PREEMPTION();
					}
					else
					{
    
    
						mtCOVERAGE_TEST_MARKER();
					}
				}
				else
				{
    
    
					mtCOVERAGE_TEST_MARKER();
				}

				taskEXIT_CRITICAL();
				return pdPASS;
			}
			else
			{
    
    
                /* 消息队列中没有消息可读 */
				if( xTicksToWait == ( TickType_t ) 0 )
				{
    
    
					/* 不等待,直接返回 */
					taskEXIT_CRITICAL();
					traceQUEUE_RECEIVE_FAILED( pxQueue );
					return errQUEUE_EMPTY;
				}
				else if( xEntryTimeSet == pdFALSE )
				{
    
    
					/* 初始化阻塞超时结构体变量,初始化进入
					阻塞的时间 xTickCount 和溢出次数 xNumOfOverflows */
					vTaskInternalSetTimeOutState( &xTimeOut );
					xEntryTimeSet = pdTRUE;
				}
				else
				{
    
    
					/* Entry time was already set. */
					mtCOVERAGE_TEST_MARKER();
				}
			}
		}
		taskEXIT_CRITICAL();
		vTaskSuspendAll();
		prvLockQueue( pxQueue );

		/* 检查超时时间是否已经过去了*/
		if( xTaskCheckForTimeOut( &xTimeOut, &xTicksToWait ) == pdFALSE )
		{
    
    
			/*  如果队列还是空的 */
			if( prvIsQueueEmpty( pxQueue ) != pdFALSE )
			{
    
    
				traceBLOCKING_ON_QUEUE_RECEIVE( pxQueue );
                // 将当前任务添加到队列的等待接收列表中,以及阻塞延时列表,阻塞时间为用户指定的超时时间 xTicksToWait
				vTaskPlaceOnEventList( &( pxQueue->xTasksWaitingToReceive ), xTicksToWait );
				prvUnlockQueue( pxQueue );
				if( xTaskResumeAll() == pdFALSE )
				{
    
    
                    /* 如果有任务优先级比当前任务高,会进行一次任务切换 */
					portYIELD_WITHIN_API();
				}
				else
				{
    
    
					mtCOVERAGE_TEST_MARKER();
				}
			}
			else
			{
    
    
				/* 如果队列有消息了,就再试一次获取消息*/
				prvUnlockQueue( pxQueue );
				( void ) xTaskResumeAll();
			}
		}
		else
		{
    
    
			/* 超时时间已过,退出*/
			prvUnlockQueue( pxQueue );
			( void ) xTaskResumeAll();
			// 如果队列还是空的,返回错误代码 errQUEUE_EMPT
			if( prvIsQueueEmpty( pxQueue ) != pdFALSE )
			{
    
    
				traceQUEUE_RECEIVE_FAILED( pxQueue );
				return errQUEUE_EMPTY;
			}
			else
			{
    
    
				mtCOVERAGE_TEST_MARKER();
			}
		}
	}
}

xQueuePeek()

如果接收了消息不想删除队列中的内容,则调用这个函数。实现方法与xQueueReceive()一样。

xQueueReceiveFromISR()、xQueuePeekFromISR()

xQueueReceiveFromISR()是 xQueueReceive ()的中断版本,用于在中断服务程序中接收一个队列消息并把消息从队列中删除;xQueuePeekFromISR()是 xQueuePeek()的中断版本,用于在中断中从一个队列中接收消息,但并不会把消息从队列中移除。说白了这两个函数只能用于中断,是不带有阻塞机制的,并且是在中断中可以安全调用。

消息队列使用注意

  • 使用 xQueueSend()、xQueueSendFromISR()、xQueueReceive()等这些函数之前应先创建需消息队列,并根据队列句柄进行操作。
  • 队列读取采用的是先进先出(FIFO)模式,会先读取先存储在队列中的数据。当然也 FreeRTOS 也支持后进先出(LIFO)模式,那么读取的时候就会读取到后进队列的数据。
  • 在获取队列中的消息时候,我们必须要定义一个存储读取数据的地方,并且该数据区域大小不小于消息大小,否则,很可能引发地址非法的错误。
  • 无论是发送或者是接收消息都是以拷贝的方式进行,如果消息过于庞大,可以将消息的地址作为消息进行发送、接收。
  • 队列是具有自己独立权限的内核对象,并不属于任何任务。

对消息队列进行读写实现

需要注意的是接收的优先级需要比发送的优先级高。这样才可以做到收发顺序执行。否则会出现发送一直在往队列中写数据,直到队列满了阻塞了,才轮到接收来读取一次。有一个地方需要做修改:

#define osMessageQDef(name, queue_sz, type)   \
const osMessageQDef_t os_messageQ_def_##name = \
{
      
       (queue_sz), sizeof(type), NULL, NULL  }

在RTOS中,queue_sz代表队列深度,也就是我们这个队列可以存放多少个Send进来的的数据,而每次进来的数据长度在这里定义了sizeof(type),我们把sizeof去掉即可,这样子的话就不会限制我们在队列中传输的数据长度。

实现:

  osMessageQDef(TestQueue, 1, 24);
  TestQueueHandle = osMessageCreate(osMessageQ(TestQueue), NULL);
void ReceiveTask(void const * argument)
{
    
    
    /* USER CODE BEGIN ReceiveTask */
    /* Infinite loop */
    BaseType_t xReturn = pdTRUE;
    char Receive_data[24];
    int i=0;
    for(;;)
    {
    
    
        memset(Receive_data , 0 , 24);
        xReturn = xQueueReceive( TestQueueHandle, 
                                                            &Receive_data,
                                                            portMAX_DELAY);

        if(xReturn==pdTRUE)
        {
    
    
            printf("The receive task is: ");
            printf("%s\r\n",Receive_data);
        }

    }
/* USER CODE END ReceiveTask */
}

void SendTask(void const * argument)
{
    
    
  /* USER CODE BEGIN SendTask */
	BaseType_t xReturn = pdPASS;
	char send_data[] = "Hello!";
  /* Infinite loop */
  for(;;)
  {
    
    
		xReturn = xQueueSend( TestQueueHandle, 
													&send_data,
													0 ); 
		if(xReturn==pdPASS)
				printf("Send Success\r\n");
    vTaskDelay(500);
  }
  /* USER CODE END SendTask */
}

0x06 信号量

概念

信号量(Semaphore)是一种实现任务间通信的机制,可以实现任务之间同步或临界资源的互斥访问,常用于协助一组相互竞争的任务来访问临界资源。

抽象的来讲,信号量是一个非负整数,所有获取它的任务都会将该整数减一(获取它当然是为了使用资源),当该整数值为零时,所有试图获取它的任务都将处于阻塞状态。通常一个信号量的计数值用于对应有效的资源数,表示剩下的可被占用的互斥资源数。其值的含义分两种情况:

  • 0:表示没有积累下来的释放信号量操作,且有可能有在此信号量上阻塞的任务。

  • 正值:表示有一个或多个释放信号量操作

二值信号量

二值信号量既可以用于临界资源访问也可以用于同步功能

二值信号量与互斥信号量具有如下差别:互斥量有优先级继承机制,二值信号量则没有这个机制。这使得二值信号量更偏向应用于同步功能(任务与任务间的同步或任务和中断间同步),而互斥量更偏向应用于临界资源的访问。

可以将二值信号量看作只有一个消息的队列,因此这个队列只能为空或满(因此称为二 值),我们在运用的时候只需要知道队列中是否有消息即可,而无需关注消息是什么。二值信号量是有0和1两种状态,信号值为0的时候代表资源被获取,信号量为1时代表信号量被释放。

计数信号量

在实际的使用中,我们常将计数信号量用于事件计数与资源管理

每当某个事件发生时,任务或者中断将释放一个信号量(信号量计数值加 1),当处理被事件时(一般在任务中处理),处理任务会取走该信号量(信号量计数值减 1),信号量的计数值则表示还有多少个事件没被处理

也可以使用计数信号量进行资源管理,信号量的计数值表示系统中可用的资源数目,任务必须先获取到信号量才能获取资源访问权,当信号量的计数值为零时表示系统没有可用的资源,但是要注意,在使用完资源的时候必须归还信号量,否则当计数值为 0的时候任务就无法访问该资源了。

互斥信号量

互斥信号量其实是特殊的二值信号量,由于其特有的优先级继承机制从而使它更适用于简单互锁,也就是保护临界资源。

用作互斥时,信号量创建后可用信号量个数应该是满的,任务在需要使用临界资源时,(临界资源是指任何时刻只能被一个任务访问的资源),先获取互斥信号量,使其变空,这样其他任务需要使用临界资源时就会因为无法获取信号量而进入阻塞,从而保证了临界资源的安全。

在操作系统中,我们使用信号量的很多时候是为了给临界资源建立一个标志,信号量表示了该临界资源被占用情况,有效地保护了临界资源。

递归信号量

可以重复获取调用的信号量,是对于已经获取递归互斥量的任务可以重复获取该递归互斥量,该任务拥有递归信号量的所有权。

任务成功获取几次递归互斥量,就要返还几次,在此之前递归互斥量都处于无效状态,其他任务无法获取,只有持有递归信号量的任务才能获取与释放。

使用信号量的目的是,不需要CPU不断地去查询当前状态并且不断去执行重复的状态,这样会占用CPU的资源,提高CPU的执行效率。

二值信号量运作机制

创建信号量时,系统会为创建的信号量对象分配内存,并把可用信号量初始化为用户自定义的个数,二值信号量的最大可用信号量个数为 1。

二值信号量获取,任何任务都可以从创建的二值信号量资源中获取一个二值信号量,获取成功则返回正确,否则任务会根据用户指定的阻塞超时时间来等待其它任务/中断释放信号量。在等待这段时间,系统将任务变成阻塞态,任务将被挂到该信号量的阻塞等待列表中。

假如某个时间中断/任务释放了信号量,由于获取无效信号量而进入阻塞态的任务将获得信号量并且恢复为就绪态:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qRMnWtv5-1682496278405)(C:\Users\user\AppData\Roaming\Typora\typora-user-images\image-20230426095802320.png)]

计数信号量运作机制

计数信号量可以用于资源管理,允许多个任务获取信号量访问共享资源,但会限制任务的最大数目。访问的任务数达到可支持的最大数目时,会阻塞其他试图获取该信号量的任务,直到有任务释放了信号量。

信号量控制块

信号量 API 函数实际上都是宏,它使用现有的队列机制,这些宏定义在 semphr.h 文件中,如果使用信号量或者互斥量,需要包含 semphr.h 头文件。

volatile UBaseType_t uxMessagesWaiting;
UBaseType_t uxLength;			
UBaseType_t uxItemSize;	

如果控制块结构体是用于消息队列:uxMessagesWaiting 用来记录当前消息队列的消息个数;如果控制块结构体被用于信号量的时候,这个值就表示有效信号量个数,有以下两种情况:

  • 如果信号量是二值信号量、互斥信号量,这个值是 1 则表示有可用信号量,如果是 0 则表示没有可用信号量。
  • 如果是计数信号量,这个值表示可用的信号量个数,在创建计数信号量的时候会被初始化一个可用信号量个数 uxInitialCount,最大不允许超过创建信号量的初始值 uxMaxCount。

如果控制块结构体是用于消息队列uxLength 表示队列的长度,也就是能存放多少消息;如果控制块结构体被用于信号量的时候,uxLength 表示最大的信号量可用个数,会有以下两种情况:

  • 如果信号量是二值信号量、互斥信号量,uxLength 最大为 1,因为信号量要么是有效的,要么是无效的。
  • 如果是计数信号量,这个值表示最大的信号量个数,在创建计数信号量的时候将由用户指定这个值 uxMaxCount。

如果控制块结构体是用于消息队列:uxItemSize 表示单个消息的大小;如果控制块结构体被用于信号量的时候,则无需存储空间,为 0 即可。

信号量函数接口

创建信号量函数

xSemaphoreCreateBinary()

xSemaphoreCreateBinary()用于创建一个二值信号量,并返回一个句柄。其实二值信号量和互斥量都共同使用一个类型 SemaphoreHandle_t 的句柄,该句柄的原型是一个 void 型 的 指 针。 使 用 该 函数 创 建 的 二值信号量是空的 , 在 使 用函 数**xSemaphoreTake()**获取之前必须先调用函数 **xSemaphoreGive()**释放后才可以获取。使用前需要将宏configSUPPORT_DYNAMIC_ALLOCATION 置为1,开启动态内存分配。

#if( configSUPPORT_DYNAMIC_ALLOCATION == 1 )
	#define xSemaphoreCreateBinary() 
		xQueueGenericCreate( ( UBaseType_t ) 1, 					//创建的队列长度为1,表示信号量的最大可用个数
                            semSEMAPHORE_QUEUE_ITEM_LENGTH, 		//创建的消息空间(队列项)大小为0
                            queueQUEUE_TYPE_BINARY_SEMAPHORE )		//创建消息队列的类型
#endif

queueQUEUE_TYPE_BINARY_SEMAPHORE可选类型:

#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 )

创建一个没有消息存储空间的队列,信号量用什么表示?其实二值信号量的释放和获取都是通过操作队列结控制块构体成员 uxMessageWaiting 来实现的,它表示信号量中当前可用的信号量个数

在信号量创建之后,变量 uxMessageWaiting 的值为 0,这说明当前信号量处于无效状态,此时的信号量是无法被获取的,在获取信号之前,应先释放一个信号量

xSemaphoreCreateCounting()

用于创建一个计数信号量,使用前需要将宏configSUPPORT_DYNAMIC_ALLOCATION定义为1,其 实 计 数 信 号 量 跟 二 值 信 号 量 的 创 建 过 程 都 差 不 多 , 其 实 也 是 间 接 调 用xQueueGenericCreate()函数进行创建:

#if( configSUPPORT_DYNAMIC_ALLOCATION == 1 )
	#define xSemaphoreCreateCounting( uxMaxCount, uxInitialCount ) 
				xQueueCreateCountingSemaphore( ( uxMaxCount ), 
                                              ( uxInitialCount ) )
#endif

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-taZNPEss-1682496278406)(C:\Users\user\AppData\Roaming\Typora\typora-user-images\image-20230426103131791.png)]

xQueueCreateCountingSemaphore()

/*-----------------------------------------------------------*/

#if( ( configUSE_COUNTING_SEMAPHORES == 1 ) && ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) )

	QueueHandle_t xQueueCreateCountingSemaphore( 
        										const UBaseType_t uxMaxCount, 
                                                const UBaseType_t uxInitialCount )
	{
    
    
	QueueHandle_t xHandle;

		configASSERT( uxMaxCount != 0 );
		configASSERT( uxInitialCount <= uxMaxCount );
		//实则也是调用函数xQueueGenericCreate
		xHandle = xQueueGenericCreate( uxMaxCount, 							//信号量最大个数
                                      queueSEMAPHORE_QUEUE_ITEM_LENGTH, 	//每个消息空间的大小的宏 0
                                      queueQUEUE_TYPE_COUNTING_SEMAPHORE );	//类型

		if( xHandle != NULL )
		{
    
    
			( ( Queue_t * ) xHandle )->uxMessagesWaiting = uxInitialCount;	//初始为用户指定的可用信号量个数

			traceCREATE_COUNTING_SEMAPHORE();
		}
		else
		{
    
    
			traceCREATE_COUNTING_SEMAPHORE_FAILED();
		}

		return xHandle;
	}

#endif /* ( ( configUSE_COUNTING_SEMAPHORES == 1 ) && ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) ) */
/*-----------------------------------------------------------*/

删除信号量函数

vSemaphoreDelete()

用于信号量删除,包括二值信号量,计数信号量,互斥量和递归互斥量。如果有任务阻塞在该信号量上,那么不要删除该信号量。

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

需要传入信号量句柄。删除信号量过程其实就是删除消息队列过程,因为信号量其实就是消息队列,只不过是无法存储消息的队列而已。

信号量释放函数

与消息队列的操作一样,信号量的释放可以在任务、中断中使用,所以需要有不一样的 API 函数在不一样的上下文环境中调用。

使得函数信号量变得有效:

  • 在创建的时候进行初始化,将它可用的信号量个数设置为一个初始值。
  • 在使用时,需要释放信号量,并且注意释放的次数是否符合信号量的区间。

xSemaphoreGive()

xSemaphoreGive()是一个用于释放信号量的宏,真正的实现过程是调用消息队列通用发送函数,释放的信号量对象必须是已经被创建的,可以用于二值信号量、计数信号量、互斥量的释放,但不能释放由函数xSemaphoreCreateRecursiveMutex()创建的递归互斥量

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

从该宏定义可以看出释放信号量实际上是一次入队操作,并且是不允许入队阻塞,因为阻塞时间为 semGIVE_BLOCK_TIME,该宏的值为 0。通过消息队列入队过程分析,我们可以将释放一个信号量的过程简化:如果信号量未满,控制块结构体成员 uxMessageWaiting 就会加 1,然后判断是否有阻塞的任务,如果有的话就会恢复阻塞的任务,然后返回成功信息(pdPASS);如果信号量已满,则返回错误代码(err_QUEUE_FULL)。

xSemaphoreGiveFromISR()

用于释放一个信号量,带中断保护。被释放的信号量可以是二进制信号量和计数信号量。和普通版本的释放信号量 API 函数有些许不同,它不能释放互斥量,这是因为互斥量不可以在中断中使用,互斥量的优先级继承机制只能在任务中起作用,而在中断中毫无意义。

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

如果可用信号量未满,控制块结构体成员 uxMessageWaiting 就会加 1,然后判断是否有阻塞的任务,如果有的话就会恢复阻塞的任务,然后返回成功信息(pdPASS),如果恢复的任务优先级比当前任务优先级高,那么在退出中断要进行任务切换一次;如果信号量满,则返回错误代码(err_QUEUE_FULL),表示信号量满。

一个或者多个任务有可能阻塞在同一个信号量上,调用函数 xSemaphoreGiveFromISR()可能会唤醒阻塞在该信号量上的任务,如果被唤醒的任务的优先级大于当前任务的优先级,那么形参 pxHigherPriorityTaskWoken 就会被设置为 pdTRUE,然后在中断退出前执行一次上下文切换portYIELD_FROM_ISR

信号量获取函数

与消息队列的操作一样,信号量的获取可以在任务、中断(中断中使用并不常见)中使用,所以需要有不一样的 API 函数在不一样的上下文环境中调用。如果某个信号量中当前拥有 1 个可用的信号量的话,被获取一次就变得无效了,那么此时另外一个任务获取该信号量的时候,就会无法获取成功,该任务便会进入阻塞态,阻塞时间由用户指定

xSemaphoreTake()

#define xSemaphoreTake( xSemaphore, xBlockTime )		
			xQueueSemaphoreTake( ( xSemaphore ), 		//信号量句柄
                                ( xBlockTime ) )		//等待信号量可用的最大超时时间,单位为 tick(即系统节拍周期)。如果宏 INCLUDE_vTaskSuspend 定义为 1 且形参 xTicksToWait 设置为portMAX_DELAY ,则任务将一直阻塞在该信号量上(即没有超时时间)。

xSemaphoreTake()函数用于获取信号量,不带中断保护。获取的信号量对象可以是二值信号量、计数信号量和互斥量,但是递归互斥量并不能使用这个 API 函数获取。其实获取信号量是一个宏,真正调用的函数是 xQueueSemaphoreTake()。

从该宏定义可以看出释放信号量实际上是一次消息出队操作,阻塞时间由用户指定xBlockTime,当有任务试图获取信号量的时候,当且仅当信号量有效的时候,任务才能读获取到信号量。如果信号量无效,在用户指定的阻塞超时时间中,该任务将保持阻塞状态以等待信号量有效。当其它任务或中断释放了有效的信号量,该任务将自动由阻塞态转移为就绪态。当任务等待的时间超过了指定的阻塞时间,即使信号量中还是没有可用信号量,任务也会自动从阻塞态转移为就绪态

如果有可用信号量,控制块结构体成员 uxMessageWaiting 就会减 1,然后返回获取成功信息(pdPASS);如果信号量无效并且阻塞时间为 0,则返回错误代码(errQUEUE_EMPTY);如果信号量无效并且用户指定了阻塞时间,则任务会因为等待信号量而进入阻塞状态,任务会被挂接到延时列表中

xSemaphoreTakeFromISR()

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

xSemaphoreTakeFromISR()是函数 xSemaphoreTake()的中断版本,用于获取信号量,是一个不带阻塞机制获取信号量的函数,获取对象必须由是已经创建的信号量,信号量类型可以是二值信号量计数信号量,它与 xSemaphoreTake()函数不同,它不能用于获取互斥量,因为互斥量不可以在中断中使用,并且互斥量特有的优先级继承机制只能在任务中起作用,而在中断中毫无意义。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qER3LkTL-1682496278407)(C:\Users\user\AppData\Roaming\Typora\typora-user-images\image-20230426113013404.png)]

信号量实现

实现一个释放信号量,另一个线程获取信号量后再接着执行。

创建一个二值信号量:

osSemaphoreDef(BinarySem);
BinarySem_Handle = osSemaphoreCreate(osSemaphore(BinarySem),1);

/* definition and creation of Receive */
osThreadDef(Receive, ReceiveTask, osPriorityLow, 0, 128);
ReceiveHandle = osThreadCreate(osThread(Receive), NULL);

/* definition and creation of Send */
osThreadDef(Send, SendTask, osPriorityIdle, 0, 128);
SendHandle = osThreadCreate(osThread(Send), NULL);

osSemaphoreCreate第二个参数代表的是信号量的类型,1为二值信号量,0则可能是计数信号量。任务函数则如下:

void ReceiveTask(void const * argument)
{
    
    
  /* USER CODE BEGIN ReceiveTask */
  /* Infinite loop */
	BaseType_t xReturn = pdTRUE;
  for(;;)
  {
    
    
		vTaskDelay(5000);
		osSemaphoreRelease(BinarySem_Handle);
		int t1 = osKernelSysTick();
		printf("the release is %d\r\n",t1);
    
  }
  /* USER CODE END ReceiveTask */
}

void SendTask(void const * argument)
{
    
    
  /* USER CODE BEGIN SendTask */
	BaseType_t xReturn = pdPASS;
  for(;;)
  {
    
    
		osSemaphoreWait(BinarySem_Handle,portMAX_DELAY);
		int t1 = osKernelSysTick();
		printf("the wait is %d\r\n",t1);
  }
  /* USER CODE END SendTask */
}

使用计数信号量时,需要将宏configUSE_COUNTING_SEMAPHORES置位。

osSemaphoreDef(CountSem);
CountSem_Handle = osSemaphoreCreate(osSemaphore(CountSem),10);

之后将osSemaphoreRelease(BinarySem_Handle);去掉,即可看到send任务只运行了十次。

猜你喜欢

转载自blog.csdn.net/Alkaid2000/article/details/130388364