本次学习采用STM32F407ZG开发板,学习教程来自于百问网《FreeRTOS入门与工程实践-基于STM32F103》教程-基于DShanMCU-103(STM32F103) | 百问网
目录
一.环境配置
下载MDK5、STM32CubeMx,并配置固件库与PACK包
利用STM32CubeMx生成代码调试串口,具体配置可参考利用HAL库实现串口数据回显。
与上述文章中的不同是,该开发板采用UART5进行串口通信,由于配置了FreeRTOS,串口的输出应该在freertos.c中实现,不要直接在main.c中编写代码。
二.任务管理
1.任务的动态/静态创建
动态创建函数:
BaseType_t xTaskCreate( TaskFunction_t pxTaskCode, // 函数指针, 任务函数
const char * const pcName, // 任务的名字
const configSTACK_DEPTH_TYPE usStackDepth, // 栈大小,单位为word,10表示40字节
void * const pvParameters, // 调用任务函数时传入的参数
UBaseType_t uxPriority, // 优先级
TaskHandle_t * const pxCreatedTask ); // 任务句柄, 以后使用它来操作这个任务
静态创建函数:
TaskHandle_t xTaskCreateStatic (
TaskFunction_t pxTaskCode, // 函数指针, 任务函数
const char * const pcName, // 任务的名字
const uint32_t ulStackDepth, // 栈大小,单位为word,10表示40字节
void * const pvParameters, // 调用任务函数时传入的参数
UBaseType_t uxPriority, // 优先级
StackType_t * const puxStackBuffer, // 静态分配的栈,就是一个buffer
StaticTask_t * const pxTaskBuffer // 静态分配的任务结构体的指针,用它来操作这个任务
);
参数 | 描述 |
---|---|
pvTaskCode | 函数指针,可以简单地认为任务就是一个C函数。 它稍微特殊一点:永远不退出,或者退出时要调用"vTaskDelete(NULL)" |
pcName | 任务的名字,FreeRTOS内部不使用它,仅仅起调试作用。 长度为:configMAX_TASK_NAME_LEN |
usStackDepth | 每个任务都有自己的栈,这里指定栈大小。 单位是word,比如传入100,表示栈大小为100 word,也就是400字节。 最大值为uint16_t的最大值。 怎么确定栈的大小,并不容易,很多时候是估计。 精确的办法是看反汇编码。 |
pvParameters | 调用pvTaskCode函数指针时用到:pvTaskCode(pvParameters) |
uxPriority | 优先级范围:0~(configMAX_PRIORITIES – 1) 数值越小优先级越低, 如果传入过大的值,xTaskCreate会把它调整为(configMAX_PRIORITIES – 1) |
puxStackBuffer | 静态分配的栈内存,比如可以传入一个数组, 它的大小是usStackDepth*4。 |
pxTaskBuffer | 静态分配的StaticTask_t结构体的指针 |
返回值 | 成功:返回任务句柄; 失败:NULL |
找到freertos.c,创建三个线程,STMCubeMx生成的默认线程1,动态创建线程2,静态创建线程3。具体函数如下:
#include "string.h"
#include "FreeRTOS.h"
#include "task.h"
#include "main.h"
#include "cmsis_os.h"
extern UART_HandleTypeDef huart5;//串口句柄
static StackType_t g_pucStackOfTestTask2[128];//数组,静态分配空间
static StaticTask_t g_TCBofTestTask2;//静态分配的结构体指针
static TaskHandle_t xTestTaskHandle2;//静态返回值句柄
//自动生成
osThreadId_t defaultTaskHandle;
const osThreadAttr_t defaultTask_attributes = {
.name = "defaultTask",
.priority = (osPriority_t) osPriorityNormal,
.stack_size = 128 * 4
};
void StartDefaultTask(void *argument);
void My_TestTask1(void *argument);
void My_TestTask2(void *argument);
//自动生成
void MX_FREERTOS_Init(void);
void MX_FREERTOS_Init(void) {
TaskHandle_t xTestTaskHandle1;//动态句柄
BaseType_t ret1;//动态返回值
defaultTaskHandle = osThreadNew(StartDefaultTask, NULL, &defaultTask_attributes);
//动态创建
ret1 = xTaskCreate(My_TestTask1, "TestTask1", 128 , NULL , osPriorityNormal , &xTestTaskHandle1);
//静态创建
xTestTaskHandle2 = xTaskCreateStatic(My_TestTask2, "TestTask2", 128 , NULL , osPriorityNormal , g_pucStackOfTestTask2 , &g_TCBofTestTask2);
}
void StartDefaultTask(void *argument)
{
for(;;)
{
printf("线程1\n");
osDelay(100);
}
}
void My_TestTask1(void *argument)
{
for(;;)
{
printf("线程2\n");
osDelay(100);
}
}
void My_TestTask2(void *argument)
{
for(;;)
{
printf("线程3\n");
osDelay(100);
}
}
运行结果显示,后创建的线程3先运行.为什么出现这种状态,请查看2.4任务管理与调度。
2.任务的删除
删除任务时使用的函数:
void vTaskDelete( TaskHandle_t xTaskToDelete );
利用创建任务时的句柄来删除任务,但是频繁的删除、创建任务并不好,会导致内存的碎片化,所以删除任务使用频率不高。
3.任务状态
四种状态:ready(就绪)、running(运行)、blocked(等待)、suspended(暂停)
新创建的任务是起点,一个任务被创建后,必定处于ready,有机会就能随时切换位running。
在running调用某一个阻塞函数时(如vTaskDelay等待一段时间),会进入blocked,触发调节满足后(等待时间完毕后),会回到ready。
进入暂停状态只能调用vTaskSuspended()函数实现,可以是自己在running时调用,或者其他函数在running时调用,此时自己处于ready或blocked。
4.任务管理与调度
优先级高的先运行;相同优先级的轮流运行。若优先级高的不停止,低优先级的就一直没法运行;一旦高优先级的任务进入ready,就会马上运行。
调度机制:FreeRTOS一共具有56个优先级,每个优先级通过ready链表存储。每次系统心跳tick会从高到低遍历这56个ready链表,找到第一个非空链表时,启动全局指针pxcurrentTCB指向的任务,然后pxcurrentTCB指向下一个任务。
但如上所说,如果高优先级的任务存在,低优先级的任务则永远无法运行,使用vTaskDelay可以使任务进入blocked(假设阻塞2tick),进入blocked的任务会从ready链表中移除,放在某一个xDelayedTaskList中。此时触发调度,再次遍历ready链表,低优先级的任务得到运行,与此同时会同步判断xDelayedTaskList中的延迟时间(2tick)是否达到,一旦达到就将任务重新放回ready链表立即执行。
注意,由于每次任务调度,pxcurrent指向的都是下一个任务,因此,即使tick中断产生时当前任务没有执行完毕,下次发起调度的时候,仍会执行下一个任务,不会继续执行未完成的任务。
上述是ready、running、blocked三个状态的切换,suspended切换也是一样的逻辑,当调用suspended函数时,将任务放到suspengded链表中,当调用唤醒函数时再放回ready链表中。
回到2.1中的现象:
创建第一个线程1时,线程1进入ready链表,全局指针pxcurrentTCB指向该线程1;
创建线程2时,pxcurrentTCB指向该线程2;
同样创建线程3时,pxcurrentTCB指向线程3;
最后系统会创建一个空闲任务,但由于空闲任务的优先级低,因此pxcurrentTCB还是指向线程3。当进行任务调度时,遍历ready链表,开始执行pxcurrentTCB指向的任务,即线程3,当tick中断产生时发起任务调度,pxcurrentTCB指向下一个任务,即线程1,当线程1执行完后执行线程2,然后执行线程3。
5.空闲任务
任务执行完毕之后,会返回到prvTaskExitError()函数中,这个函数会关闭所有中断,然后进入for循环。而任务调度主要依赖与Tick中断,所以一旦函数进入到prvTaskExitError中,程序会陷入死循环。因此,结束任务的方式只能是杀死任务,包括自杀和他杀。
A杀死B,A帮B收尸(释放资源);B自杀时,空闲任务帮B收尸,空闲任务的优先级为0。
由于空闲任务的优先级最低,如果一直有任务占用资源,空闲任务没有执行的机会,就无法释放自杀掉的资源,因此,在编程时要养成良好的习惯,当任务执行完毕时及时让出CPU资源。
同时空闲任务永远处于ready或running,即使ready链表中所有的任务都处于阻塞状态,还是有空闲任务可以执行。
6.两个Delay函数
有两个Delay函数:
- vTaskDelay:至少等待指定个数的Tick,才能变为就绪状态
- vTaskDelayUntil:等待到指定的绝对时刻,才能变为就绪态。
这2个函数原型如下:
void vTaskDelay( const TickType_t xTicksToDelay ); /* xTicksToDelay: 等待多少给Tick */
/* pxPreviousWakeTime: 上一次被唤醒的时间
* xTimeIncrement: 要阻塞到(pxPreviousWakeTime + xTimeIncrement)
* 单位都是Tick Count
*/
BaseType_t xTaskDelayUntil( TickType_t * const pxPreviousWakeTime,
const TickType_t xTimeIncrement );
三.同步互斥与通信
能实现同步、互斥的内核方法有:任务通知(task notification)、队列(queue)、事件组(event group)、信号量(semaphoe)、互斥量(mutex)。
它们都有类似的操作方法:获取/释放、阻塞/唤醒、超时。比如:
- 任务A获取资源,用完后任务A释放资源
- 任务A获取不到资源则阻塞,任务B释放资源并把任务A唤醒
- 任务A获取不到资源则阻塞,并定个闹钟;A要么超时返回,要么在这段时间内因为任务B释放资源而被唤醒。
这些内核对象五花八门,通过对比的方法来区分它们。
- 能否传信息?还是只能传递状态?
- 为众生(所有任务都可以使用)?只为你(只能指定任务使用)?
- 我生产,你们消费?
- 我上锁,只能由我开锁
内核对象 | 生产者 | 消费者 | 数据/状态 | 说明 |
---|---|---|---|---|
队列 | ALL | ALL | 数据:若干个数据 谁都可以往队列里扔数据, 谁都可以从队列里读数据 | 用来传递数据, 发送者、接收者无限制, 一个数据只能唤醒一个接收者 |
事件组 | ALL | ALL | 多个位:或、与 谁都可以设置(生产)多个位, 谁都可以等待某个位、若干个位 | 用来传递事件, 可以是N个事件, 发送者、接受者无限制, 可以唤醒多个接收者:像广播 |
信号量 | ALL | ALL | 数量:0~n 谁都可以增加一个数量, 谁都可消耗一个数量 | 用来维持资源的个数, 生产者、消费者无限制, 1个资源只能唤醒1个接收者 |
任务通知 | ALL | 只有我 | 数据、状态都可以传输, 使用任务通知时, 必须指定接受者 | N对1的关系: 发送者无限制, 接收者只能是这个任务 |
互斥量 | 只能A开锁 | A上锁 | 位:0、1 我上锁:1变为0, 只能由我开锁:0变为1 | 就像一个空厕所, 谁使用谁上锁, 也只能由他开锁 |
对比如下:
-
队列:
- 里面可以放任意数据,可以放多个数据
- 任务、ISR都可以放入数据;任务、ISR都可以从中读出数据
-
事件组:
- 一个事件用一bit表示,1表示事件发生了,0表示事件没发生
- 可以用来表示事件、事件的组合发生了,不能传递数据
- 有广播效果:事件或事件的组合发生了,等待它的多个任务都会被唤醒
-
信号量:
- 核心是"计数值"
- 任务、ISR释放信号量时让计数值加1
- 任务、ISR获得信号量时,让计数值减1
-
任务通知:
- 核心是任务的TCB里的数值
- 会被覆盖
- 发通知给谁?必须指定接收任务
- 只能由接收任务本身获取该通知
-
互斥量:
- 数值只有0或1
- 谁获得互斥量,就必须由谁释放同一个互斥量
1.队列
队列的本质是一个环形缓冲区(数组,FIFO),任务A写,任务B读。但是环形缓冲区没有阻塞,效率较低,而队列有阻塞。
对于任务A而言,需要考虑如果缓冲区写满了怎么办,对于任务B而言,需要考虑如果队列没有数据怎么办。
因此队列具有两个链表,对于任务B而言,当队列没有数据时,会将该任务从readyLisy取出放在receiveList和DelayList中,如果任务A写了数据会去receive寻找任务将其放入readyList,同时删除对应的receiveList和DelayList。如果任务B等待了许久,还是没有数据,Tick中断会将任务B放回readyList,同时删除其他两个List中的任务B。
对于任务A而言,如果队列满了,会将该任务从readyList取出放到sendList和DelayList中,逻辑同上。具体结构如下图所示:
综上所述,对于队列而言,任务A跟B有两种唤醒方式,第一彼此唤醒,第二超时唤醒。相比于普通的环形缓冲区,队列具有阻塞功能,大大提高运行效率。
1.创建队列
队列的创建有两种方法:动态分配内存、静态分配内存
- 动态分配内存:xQueueCreate,队列的内存在函数内部动态分配
函数原型如下:
QueueHandle_t xQueueCreate( UBaseType_t uxQueueLength, UBaseType_t uxItemSize );
- 静态分配内存:xQueueCreateStatic,队列的内存要事先分配好
函数原型如下:
QueueHandle_t xQueueCreateStatic(*
UBaseType_t uxQueueLength,*
UBaseType_t uxItemSize,*
uint8_t *pucQueueStorageBuffer,*
StaticQueue_t *pxQueueBuffer*
);
参数 | 说明 |
---|---|
uxQueueLength | 队列长度,最多能存放多少个数据(item) |
uxItemSize | 每个数据(item)的大小:以字节为单位 |
pucQueueStorageBuffer | 如果uxItemSize非0,pucQueueStorageBuffer必须指向一个uint8_t数组, 此数组大小至少为"uxQueueLength * uxItemSize" |
pxQueueBuffer | 必须执行一个StaticQueue_t结构体,用来保存队列的数据结构 |
返回值 | 非0:成功,返回句柄,以后使用句柄来操作队列 NULL:失败,因为pxQueueBuffer为NULL |
2.复位队列
队列刚被创建时,里面没有数据;使用过程中可以调用 xQueueReset() 把队列恢复为初始状态,此函数原型为:
/* pxQueue : 复位哪个队列;
* 返回值: pdPASS(必定成功)
*/
BaseType_t xQueueReset( QueueHandle_t pxQueue);
3.写队列
可以把数据写到队列头部,也可以写到尾部,这些函数有两个版本:在任务中使用、在ISR中使用。函数原型如下:
/* 等同于xQueueSendToBack
* 往队列尾部写入数据,如果没有空间,阻塞时间为xTicksToWait
*/
BaseType_t xQueueSend(
QueueHandle_t xQueue,
const void *pvItemToQueue,
TickType_t xTicksToWait
);
/*
* 往队列尾部写入数据,如果没有空间,阻塞时间为xTicksToWait
*/
BaseType_t xQueueSendToBack(
QueueHandle_t xQueue,
const void *pvItemToQueue,
TickType_t xTicksToWait
);
/*
* 往队列尾部写入数据,此函数可以在中断函数中使用,不可阻塞
*/
BaseType_t xQueueSendToBackFromISR(
QueueHandle_t xQueue,
const void *pvItemToQueue,
BaseType_t *pxHigherPriorityTaskWoken
);
/*
* 往队列头部写入数据,如果没有空间,阻塞时间为xTicksToWait
*/
BaseType_t xQueueSendToFront(
QueueHandle_t xQueue,
const void *pvItemToQueue,
TickType_t xTicksToWait
);
/*
* 往队列头部写入数据,此函数可以在中断函数中使用,不可阻塞
*/
BaseType_t xQueueSendToFrontFromISR(
QueueHandle_t xQueue,
const void *pvItemToQueue,
BaseType_t *pxHigherPriorityTaskWoken
);
这些函数用到的参数是类似的,统一说明如下:
参数 | 说明 |
---|---|
xQueue | 队列句柄,要写哪个队列 |
pvItemToQueue | 数据指针,这个数据的值会被复制进队列, 复制多大的数据?在创建队列时已经指定了数据大小 |
xTicksToWait | 如果队列满则无法写入新数据,可以让任务进入阻塞状态, xTicksToWait表示阻塞的最大时间(Tick Count)。 如果被设为0,无法写入数据时函数会立刻返回; 如果被设为portMAX_DELAY,则会一直阻塞直到有空间可写 |
返回值 | pdPASS:数据成功写入了队列 errQUEUE_FULL:写入失败,因为队列满了。 |
4.读队列
使用 xQueueReceive() 函数读队列,读到一个数据后,队列中该数据会被移除。这个函数有两个版本:在任务中使用、在ISR中使用。函数原型如下:
BaseType_t xQueueReceive( QueueHandle_t xQueue,
void * const pvBuffer,
TickType_t xTicksToWait );
BaseType_t xQueueReceiveFromISR(
QueueHandle_t xQueue,
void *pvBuffer,
BaseType_t *pxTaskWoken
);
参数说明如下:
参数 | 说明 |
---|---|
xQueue | 队列句柄,要读哪个队列 |
pvBuffer | bufer指针,队列的数据会被复制到这个buffer 复制多大的数据?在创建队列时已经指定了数据大小 |
xTicksToWait | 果队列空则无法读出数据,可以让任务进入阻塞状态, xTicksToWait表示阻塞的最大时间(Tick Count)。 如果被设为0,无法读出数据时函数会立刻返回; 如果被设为portMAX_DELAY,则会一直阻塞直到有数据可写 |
返回值 | pdPASS:从队列读出数据入 errQUEUE_EMPTY:读取失败,因为队列空了。 |
5.删除队列
删除队列的函数为 vQueueDelete() ,只能删除使用动态方法创建的队列,它会释放内存。原型如下:
void vQueueDelete( QueueHandle_t xQueue );
6.队列集
写队列一般由中断触发,但不建议在中断中进行数据处理,如果占用时间过长会导致资源浪费,因此一般中断触发后会调用另外一个任务,在该任务中进行数据处理。
如果多个写队列在操作同一个设备,如案例中利用遥控器、旋转编码器、位移传感器控制游戏,一般采用下图的方式进行设计,将三个队列整合起来,这就是队列集的思想,队列集本身也是一种队列,只不过该队列读写的数据是队列的句柄。
如上图所示共有A,B,C三个队列,每次队列在写数据时,同时会将句柄传递给队列集,这样在InputTask任务读数据时只用读取队列集的句柄,再根据句柄去寻找数据即可。InputTask任务将得到的数据进行处理,写入挡球板的队列。
按照这种思路底层与任务完全隔离开来,同时也方便添加设备,只用关心InputTask任务内部的实现即可。
2.信号量
信号量是一个特殊的队列,它只涉及到计数,不涉及到数据的传输。因此它不具备环形缓冲区、读指针、写指针、sendList,具体结构如下图所示:
对于信号量而言,分为give和take,give时cnt++,同时唤醒receiveList中等待的任务;take时,cnt--,如果cnt为0则把自己挂入receiveList进行等待。
1.创建信号量
使用信号量之前,要先创建,得到一个句柄;使用信号量时,要使用句柄来表明使用哪个信号量。 对于二进制信号量(计数值限定最大为1)、计数型信号量,它们的创建函数不一样:
二进制信号量 | 计数型信号量 | |
---|---|---|
动态创建 | xSemaphoreCreateBinary 计数值初始值为0 | xSemaphoreCreateCounting |
vSemaphoreCreateBinary(过时了) 计数值初始值为1 | ||
静态创建 | xSemaphoreCreateBinaryStatic | xSemaphoreCreateCountingStatic |
创建二进制信号量的函数原型如下:
/* 创建一个二进制信号量,返回它的句柄。
* 此函数内部会分配信号量结构体
* 返回值: 返回句柄,非NULL表示成功
*/
SemaphoreHandle_t xSemaphoreCreateBinary( void );
/* 创建一个二进制信号量,返回它的句柄。
* 此函数无需动态分配内存,所以需要先有一个StaticSemaphore_t结构体,并传入它的指针
* 返回值: 返回句柄,非NULL表示成功
*/
SemaphoreHandle_t xSemaphoreCreateBinaryStatic( StaticSemaphore_t *pxSemaphoreBuffer );
创建计数型信号量的函数原型如下:
/* 创建一个计数型信号量,返回它的句柄。
* 此函数内部会分配信号量结构体
* uxMaxCount: 最大计数值
* uxInitialCount: 初始计数值
* 返回值: 返回句柄,非NULL表示成功
*/
SemaphoreHandle_t xSemaphoreCreateCounting(UBaseType_t uxMaxCount, UBaseType_t uxInitialCount);
/* 创建一个计数型信号量,返回它的句柄。
* 此函数无需动态分配内存,所以需要先有一个StaticSemaphore_t结构体,并传入它的指针
* uxMaxCount: 最大计数值
* uxInitialCount: 初始计数值
* pxSemaphoreBuffer: StaticSemaphore_t结构体指针
* 返回值: 返回句柄,非NULL表示成功
*/
SemaphoreHandle_t xSemaphoreCreateCountingStatic( UBaseType_t uxMaxCount,
UBaseType_t uxInitialCount,
StaticSemaphore_t *pxSemaphoreBuffer );
2.删除信号量
对于动态创建的信号量,不再需要它们时,可以删除它们以回收内存。
vSemaphoreDelete可以用来删除二进制信号量、计数型信号量,函数原型如下:
/*
* xSemaphore: 信号量句柄,你要删除哪个信号量
*/
void vSemaphoreDelete( SemaphoreHandle_t xSemaphore );
3.give
这些函数也分为2个版本:给任务使用,给ISR使用。列表如下:
在任务中使用 | 在ISR中使用 | |
---|---|---|
give | xSemaphoreGive | xSemaphoreGiveFromISR |
take | xSemaphoreTake | xSemaphoreTakeFromISR |
xSemaphoreGive的函数原型如下:
BaseType_t xSemaphoreGive( SemaphoreHandle_t xSemaphore );
xSemaphoreGiveFromISR的函数原型如下:
BaseType_t xSemaphoreGiveFromISR(
SemaphoreHandle_t xSemaphore,
BaseType_t *pxHigherPriorityTaskWoken
);
xSemaphoreGiveFromISR函数的参数与返回值列表如下:
参数 | 说明 |
---|---|
xSemaphore | 信号量句柄,释放哪个信号量 |
pxHigherPriorityTaskWoken | 如果释放信号量导致更高优先级的任务变为了就绪态, 则*pxHigherPriorityTaskWoken = pdTRUE |
返回值 | pdTRUE表示成功, 如果二进制信号量的计数值已经是1,再次调用此函数则返回失败; 如果计数型信号量的计数值已经是最大值,再次调用此函数则返回失败 |
4.take
xSemaphoreTake的函数原型如下:
BaseType_t xSemaphoreTake(
SemaphoreHandle_t xSemaphore,
TickType_t xTicksToWait
);
xSemaphoreTakeFromISR的函数原型如下:
BaseType_t xSemaphoreTakeFromISR(
SemaphoreHandle_t xSemaphore,
BaseType_t *pxHigherPriorityTaskWoken
);
xSemaphoreTakeFromISR函数的参数与返回值列表如下:
参数 | 说明 |
---|---|
xSemaphore | 信号量句柄,获取哪个信号量 |
pxHigherPriorityTaskWoken | 如果获取信号量导致更高优先级的任务变为了就绪态, 则*pxHigherPriorityTaskWoken = pdTRUE |
返回值 | pdTRUE表示成功 |
3.互斥量
互斥量是一种特殊的二进制信号量,具有优先级继承与恢复功能。
使用互斥量时,先创建、然后去获得、释放它。使用句柄来表示一个互斥量。
创建互斥量的函数有2种:动态分配内存,静态分配内存,函数原型如下:
/* 创建一个互斥量,返回它的句柄。
* 此函数内部会分配互斥量结构体
* 返回值: 返回句柄,非NULL表示成功
*/
SemaphoreHandle_t xSemaphoreCreateMutex( void );
/* 创建一个互斥量,返回它的句柄。
* 此函数无需动态分配内存,所以需要先有一个StaticSemaphore_t结构体,并传入它的指针
* 返回值: 返回句柄,非NULL表示成功
*/
SemaphoreHandle_t xSemaphoreCreateMutexStatic( StaticSemaphore_t *pxMutexBuffer );
要想使用互斥量,需要在配置文件FreeRTOSConfig.h中定义:
#define configUSE_MUTEXES 1
要注意的是,互斥量不能在ISR中使用。
各类操作函数,比如删除、give/take,跟一般是信号量是一样的。
/*
* xSemaphore: 信号量句柄,你要删除哪个信号量, 互斥量也是一种信号量
*/
void vSemaphoreDelete( SemaphoreHandle_t xSemaphore );
/* 释放 */
BaseType_t xSemaphoreGive( SemaphoreHandle_t xSemaphore );
/* 获得 */
BaseType_t xSemaphoreTake(
SemaphoreHandle_t xSemaphore,
TickType_t xTicksToWait
);