北京昌平网站设计,千万不要做手游推广员,食品加工设备建站方案,网站建设的能力文章目录 0x01 FreeRTOS文件夹FreeRTOSConfig.h文件内容上面定义的宏决定FreeRTOS.h文件中的定义0x02 创建任务创建静态任务过程configSUPPORT_STATIC_ALLOCATION创建动态任务过程configSUPPORT_DYNAMIC_ALLOCATION 0x03 FreeRTOS启动流程启动流程概述 0x04 任务管理任务调度器… 文章目录 0x01 FreeRTOS文件夹FreeRTOSConfig.h文件内容上面定义的宏决定FreeRTOS.h文件中的定义0x02 创建任务创建静态任务过程configSUPPORT_STATIC_ALLOCATION创建动态任务过程configSUPPORT_DYNAMIC_ALLOCATION 0x03 FreeRTOS启动流程启动流程概述 0x04 任务管理任务调度器任务状态迁移vTaskSuspend()vTaskSuspendAll()vTaskResume()xTaskResumeFromISR()xTaskResumeAll()vTaskDelete()vTaskDelay()vTaskDelayUntil()任务设计要点任务执行的时间 0x05 消息队列运作原理阻塞机制消息队列控制块消息队列常用函数xQueueCreate()xQueueGenericCreate()prvInitialiseNewQueue()xQueueGenericReset()xQueueCreateStatic()vQueueDelete()向消息队列发送消息xQueueSend()xQueueSendToBack()xQueueSendFromISR()xQueueSendToBackFromISR()xQueueSendToFront()xQueueSendToFrontFromISR()xQueueGenericSend()xQueueGenericSendFromISR()向消息队列读取消息函数xQueueReceive()xQueuePeek()xQueueReceiveFromISR()、xQueuePeekFromISR()消息队列使用注意对消息队列进行读写实现 0x06 信号量概念二值信号量计数信号量互斥信号量递归信号量二值信号量运作机制计数信号量运作机制信号量控制块信号量函数接口创建信号量函数xSemaphoreCreateBinary()xSemaphoreCreateCounting()xQueueCreateCountingSemaphore()删除信号量函数vSemaphoreDelete()信号量释放函数xSemaphoreGive()xSemaphoreGiveFromISR()信号量获取函数xSemaphoreTake()xSemaphoreTakeFromISR()信号量实现 0x01 FreeRTOS文件夹 FreeRTOS 文件夹下的 Source 文件夹里面包含的是 FreeRTOS内核的源代码Demo 文件夹里面包含了 FreeRTOS 官方为各个单片机移植好的工程代码。从Demo中可以得到FreeRTOSConfig.h。
Source文件夹 include以及各种.c文件包含的是FreeRTOS的通用头文件和C文件这两部分的文件试用于各种编译器和处理器是通用的。 portblle里面很多与编译器相关的文件夹在不同的编译器中使用不同的支持文件。Keil文件与RVDS的文件是一样的其中的MemMang文件是与内存管理相关的。 RVDS这个文件夹包含了各种处理器相关的文件夹FreeRTOS需要软硬结合不同的硬件接口文件是不一样的需要编写代码来进行关联这部分关联则叫关联文件一般由汇编和C联合编写。FreeRTOS 为我们提供了 cortex-m0、m3、m4 和 m7 等内核的单片机的接口文件只要是使用了这些内核的 mcu 都可以使用里面的接口文件。 里面的文件里面只有“port.c”与“portmacro.h”两个文件port.c 文件里面的内容是由 FreeRTOS 官方的技术人员为 Cortex-M3 内核的处理器写的接口文件里面核心的上下文切换代码是由汇编语言编写而成。portmacro.h 则是 port.c 文件对应的头文件主要是一些数据类型和宏定义。 MemMang文件夹下存放的是跟内存管理相关的总共有五个 heap 文件以及一个 readme 说明文件这五个 heap 文件在移植的时候必须使用一个因为 FreeRTOS 在创建内核对象的时候使用的是动态分配内存而这些动态内存分配的函数则在这几个文件里面实现不同的分配算法会导致不同的效率与结果。 Demo各种开发平台完整的Demo。 FreeRTOS-PlusreeRTOS-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.hextern 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基础配置选项//1RTOS使用抢占式调度器 0RTOS使用协作式调度器时间片
//协作式操作系统是任务主动释放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 的时钟周期T1/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 设置为 0FreeRTOS 调度器仍然总是运行处于最高优先级的就 绪任务但是当 RTOS 系统节拍中断发生时相同优先级的多个任务之间不再进行任务切换而是在执行完高优先级的任务之后才进行任务切换。
0x02 创建任务
任务里面的延时函数必须使用 FreeRTOS 里面提供的延时函数并不能使用我们裸机编程中的那种延时。这两种的延时的区别是 FreeRTOS 里面的延时是阻塞延时即调用 vTaskDelay()函数的时候当前任务会被挂起调度器会切换到其它就绪的任务从而实现多任务。如果还是使用裸机编程中的那种延时那么整个任务就成为了一个死循环如果恰好该任务的优先级是最高的那么系统永远都是在这个任务中运行比它优先级更低的任务无法运行根本无法实现多任务。任务必须是一个死循环否则任务将通过 LR 返回如果 LR 指向了非法的内存就会产生 HardFault_Handler而 FreeRTOS 指向一个任务退出函数prvTaskExitError()里面是一个死循环那么任务返回之后就在死循环中执行这样子的任务是不安全的所以避免这种情况任务一般都是死循环并且无返回值的。
创建静态任务过程configSUPPORT_STATIC_ALLOCATION
使用静态创建任务时需要将configSUPPORT_STATIC_ALLOCATION这个宏定义为1并且需要实现函数vApplicationGetIdleTaskMemory()与 vApplicationGetTimerTaskMemory()这两个函数是用户设定的空闲Idle任务与定时器Timer任务的堆栈大小必须由用户自己分配而不能是动态分配。并且需要定义一些全局变量如下 并且创建好任务句柄。
使用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 functionosPriority tpriority; /// initial thread priorityuint32_t instances; /// maximum number of instances of that thread functionuint32_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 allocationosStaticThreadDef_t *controlblock; /// control block to hold threads 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: lowosPriorityBelowNormal -1, /// priority: below normalosPriorityNormal 0, /// priority: normal (default)osPriorityAboveNormal 1, /// priority: above normalosPriorityHigh 2, /// priority: highosPriorityRealtime 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这个函数将任务主体函数、任务栈、任务控制块联合在一起。让任务可以随时被系统启动。 当任务创建好后是处于任务就绪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 functionosPriority tpriority; /// initial thread priorityuint32_t instances; /// maximum number of instances of that thread functionuint32_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 allocationosStaticThreadDef_t *controlblock; /// control block to hold threads 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: lowosPriorityBelowNormal -1, /// priority: below normalosPriorityNormal 0, /// priority: normal (default)osPriorityAboveNormal 1, /// priority: above normalosPriorityHigh 2, /// priority: highosPriorityRealtime 3, /// priority: realtime (highest)osPriorityError 0x84 /// system cannot determine priority or thread has illegal priority
} osPriority;此时调用的是函数xTaskCreate来创建任务并且在如下进行创建 堆内存的大小为 configTOTAL_HEAP_SIZE 在FreeRTOSConfig.h 中由我们自己定义configSUPPORT_DYNAMIC_ALLOCATION 这个宏定义在使用 FreeRTOS 操作系统的时候必须开启。
之后即可启动任务vTaskStartScheduler()。
0x03 FreeRTOS启动流程
第一种启动流程
这种启动方式也就是上面讲解的启动方式。
外设硬件初始化RTOS系统初始化创建各种任务启动RTOS调度器编写函数实体任务实体通常是一个不带返回值的无限循环的 C 函数函数体必须有阻塞的情况出现不然任务如果优先权恰好是最高会一直在 while 循环里面执行导致其它任务没有执行的机会。
第二种启动流程
在 main 函数中将硬件和 RTOS 系统先初始化好然后创建一个启动任务后就启动调度器然后在启动任务里面创建各种应用任务当所有任务都创建成功后启动任务把自己删除。 启动流程概述
系统上电时第一个执行的启动文件是由汇编编写的复位函数Reset_Handler复位函数会调用C库函数__main其函数主要工作是初始化系统的堆栈。
创建任务xTaskCreate()函数
在 main()函数中我们直接可以对 FreeRTOS 进行创建任务操作因为 FreeRTOS 会自动帮我们做初始化的事情比如初始化堆内存。其实也就是对即将使用的堆栈进行初始化根据任务个数来决定。在此函数中进行了堆内存的初始化 其分配内存函数
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 和 SysTickif( 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()函数的调用主要为了启动系统节拍定时器
SVCSVC系统服务调用亦简称系统调用用于任务启动有些操作系统不允许应用程序直接访问硬件而是通过提供一些系统服务函数用户程序使用 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 内核中也允许创建相同优先级的任务。相同优先级的任务采用时间片轮转方式进行调度也就是通常说的分时调度器时间片轮转调度仅在当前系统中无更高优先级就绪任务存在的情况下才有效。为了保证系统的实时性系统尽最大可能地保证高优先级的任务得以运行。任务调度的原则是一旦任务状态发生了改变并且当前运行的任务优先级小于优先级队列组中任务最高优先级时立刻进行任务切换除非当前系统处于中断处理程序中或禁止任务切换的状态。
任务状态迁移 创建任务→就绪态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 isnow 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(i10) 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()函数也只在任务是挂起态的时候才进行恢复。可以创建如下两个任务看到其挂起恢复的效果 void ReceiveTask(void const * argument)
{/* USER CODE BEGIN ReceiveTask *//* Infinite loop */int i 0;for(;;){printf(Task1 count: %d ----------\r\n,i);if(i%100) 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(i15) vTaskResume(ReceiveHandle);osDelay(1000);}/* USER CODE END SendTask */
}xTaskResumeFromISR()
xTaskResumeFromISR()与 vTaskResume()一样都是用于恢复被挂起的任务不一样的是 xTaskResumeFromISR() 专门用在中断服务程序中。无 论 通 过 调 用 一 次 或 多 次vTaskSuspend()函数而被挂起的任务也只需调用一次 xTaskResumeFromISR()函数即可解挂。
这个函数使用前需要把宏INCLUDE_vTaskSuspend 和INCLUDE_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()函数是没有意义的只能等下一次中断。这种情况下可以使用信号量或者任务通知来同步就可以避免这种情况。
使用 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;}#endiftaskYIELD_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 中保存的是上次唤醒时间唤醒后需要一定时间执行任务主体代码如果上次唤醒时间大于当前时间说明节拍计数器溢出了。如果本次任务的唤醒时间小于上次唤醒时间但是大于开始进入延时的时间进入延时的时间与任务唤醒时间都已经溢出了这样子就可以看做没有溢出其实也就是保证了周期性延时时间大于任务主体代码的执行时间。 只是唤醒时间 xTimeToWake 溢出的情况或者是 xTickCount 与xTimeToWake 都没溢出的情况都是符合要求的因为都保证了周期性延时时间大于任务主体代码的执行时间。
只有任务唤醒时间溢出 xTickCount 与 xTimeToWake 都没溢出正常情况 可以看出无论是溢出还是没有溢出都要求在下次唤醒任务之前当前任务主体代码必须被执行完。也就是说任务执行的时间必须小于任务周期时间 xTimeIncrement。每次产生系统节拍中断都会检查这两个延时列表查看延时的任务是否到期如果时间到则将任务从延时列表中删除重新加入就绪列表任务从阻塞态变成就绪态如果此时的任务优先级是最高的则会触发一次上下文切换。绝对延时函数使用如下 在使用的时候要将延时时间转化为系统节拍在任务主体之前要调用延时函数。
任务会先调用 vTaskDelayUntil()使任务进入阻塞态等到时间到了就从阻塞中解除然后执行主体代码任务主体代码执行完毕。会继续调用 vTaskDelayUntil()使任务进入阻塞态然后就是循环这样子执行。即使任务在执行过程中发生中断那么也不会影响这个任务的运行周期仅仅是缩短了阻塞的时间而已到了要唤醒的时间依旧会将任务唤醒。
任务设计要点
FreeRTOS中程序运行的上下文包括
中断服务函数普通任务空闲任务
中断服务函数
中断服务函数是一种需要特别注意的上下文环境它运行在非任务的执行环境下一般为芯片的一种特殊运行模式也被称作特权模式在这个上下文环境中不能使用挂起当前任务的操作不允许调用任何会阻塞运行的 API 函数接口另外需要注意的是中断服务程序最好保持精简短小快进快出一般在中断服务函数中只做标记事件的发生然后通知任务让对应任务去执行相关处理因为中断服务函数的优先级高于任何优先级的任务如果中断处理时间过长将会导致整个系统的任务无法正常运行。
普通任务
做为一个优先级明确的实时系统如果一个任务中的程序出现了死循环操作此处的死循环是指没有阻塞机制的任务循环体那么比这个任务优先级低的任务都将无法执行当然也包括了空闲任务因为死循环的时候任务不会主动让出 CPU低优先级的任务是不可能得到CPU 的使用权的而高优先级的任务就可以抢占 CPU。这个情况在实时操作系统中是必须注意的一点所以在任务中不允许出现死循环。如果一个任务只有就绪态而无阻塞态势必会影响到其他低优先级任务的执行所以在进行任务设计时就应该保证任务在不活跃的时候任务可以进入阻塞态以交出 CPU 使用权这就需要我们自己明确知道什么情况下让任务进入阻塞态保证低优先级任务可以正常运行。
空闲任务
空闲任务idle 任务是 FreeRTOS 系统中没有其他工作进行时自动进入的系统任务。FreeRTOS 为了保证这一点当调用 **vTaskStartScheduler()**时调度器会自动创建一个空闲任务空闲任务是一个非常短小的循环。
用户可以通过空闲任务钩子方式在空闲任务上钩入自己的功能函数。通常这个空闲任务钩子能够完成一些额外的特殊功能例如系统运行状态的指示系统省电模式等。
除了空闲任务钩子FreeRTOS 系统还把空闲任务用于一些其他的功能比如当系统删除一个任务或一个动态任务运行结束时在执行删除任务的时候并不会释放任务的内存空间只会将任务添加到结束列表中真正的系统资源回收工作在空闲任务完成空闲任务是唯一一个不允许出现阻塞情况的任务因为 FreeRTOS 需要保证系统永远都有一个可运行的任务。
对于空闲任务钩子上挂接的空闲钩子函数它应该满足以下的条件
永远不会被挂起不应该陷入死循环需要留出部分时间用于系统处理系统资源回收
任务执行的时间
任务的执行时间一般是指两个方面一是任务从开始到结束的时间二是任务的周期。一般来说处理时间更短的任务优先级应设置更高一些。
0x05 消息队列
队列又称消息队列是一种常用于任务间通信的数据结构队列可以在任务与任务间、中断和任务间传递信息实现了任务接收来自其他任务或中断的不固定长度的消息任务能够从队列里面读取消息当队列中的消息是空时读取消息的任务将被阻塞用户还可以指定阻塞的任务时间 xTicksToWait在这段时间中如果队列为空该任务将保持阻塞状态以等待队列数据有效。
当队列中有新消息时被阻塞的任务会被唤醒并处理新消息当等待的时间超过了指定的阻塞时间即使队列中尚无有效数据任务也会自动从阻塞态转为就绪态。消息队列是一种异步的通信方式。
任务先得到的是最先进入消息队列的消息即先进先出原则FIFO但是也支持后进先出原则LIFO。 运作原理
创建消息队列时 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 ) )
#endifxQueueGenericCreate()
#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, sonote this task was created dynamically in case it is laterdeleted. */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
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 previousversions. */return pdPASS;
}在创建消息队列的时候是需要用户自己定义消息队列的句柄的但是注意了定义了队列的句柄并不等于创建了队列创建队列必须是调用消息队列创建函数进行创建可以是静态也可以是动态创建否则以后根据队列句柄使用消息队列的其它函数的时候会发生错误创建完成会返回消息队列的句柄用户通过句柄就可使用消息队列进行发送与读取消息队列的操作如果返回的是 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 */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()来代替。 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 )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 ) ) );}#endiffor( ;; ){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 );// 将当前任务添加到队列的等待发送列表中以及阻塞延时列表延时时间为用户指定的超时时间 xTicksToWaitvTaskPlaceOnEventList( ( 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_FULLtraceQUEUE_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 );// 将当前任务添加到队列的等待接收列表中,以及阻塞延时列表阻塞时间为用户指定的超时时间 xTicksToWaitvTaskPlaceOnEventList( ( 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_EMPTif( 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 i0;for(;;){memset(Receive_data , 0 , 24);xReturn xQueueReceive( TestQueueHandle, Receive_data,portMAX_DELAY);if(xReturnpdTRUE){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(xReturnpdPASS)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。
二值信号量获取任何任务都可以从创建的二值信号量资源中获取一个二值信号量获取成功则返回正确否则任务会根据用户指定的阻塞超时时间来等待其它任务/中断释放信号量。在等待这段时间系统将任务变成阻塞态任务将被挂到该信号量的阻塞等待列表中。
假如某个时间中断/任务释放了信号量由于获取无效信号量而进入阻塞态的任务将获得信号量并且恢复为就绪态 计数信号量运作机制
计数信号量可以用于资源管理允许多个任务获取信号量访问共享资源但会限制任务的最大数目。访问的任务数达到可支持的最大数目时会阻塞其他试图获取该信号量的任务直到有任务释放了信号量。
信号量控制块
信号量 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, //创建的消息空间队列项大小为0queueQUEUE_TYPE_BINARY_SEMAPHORE ) //创建消息队列的类型
#endifqueueQUEUE_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 ) )
#endifxQueueCreateCountingSemaphore()
/*-----------------------------------------------------------*/#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 );//实则也是调用函数xQueueGenericCreatexHandle xQueueGenericCreate( uxMaxCount, //信号量最大个数queueSEMAPHORE_QUEUE_ITEM_LENGTH, //每个消息空间的大小的宏 0queueQUEUE_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()函数不同它不能用于获取互斥量因为互斥量不可以在中断中使用并且互斥量特有的优先级继承机制只能在任务中起作用而在中断中毫无意义。 信号量实现
实现一个释放信号量另一个线程获取信号量后再接着执行。
创建一个二值信号量
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任务只运行了十次。