1. FreeRTOS 消息队列
1.1 简介
队列是 任务间通信的主要形式,可用于在任务之间以及中断与任务之间传递消息。队列在 FreeRTOS 中具有以下关键特点:
- 队列默认采用 先进先出 FIFO 方式,也可以使用
xQueueSendToFront()
实现 LIFO。 - FreeRTOS 确保队列操作是原子的,不会因为任务切换而导致数据损坏。
- 如果队列 满了(写入时)或 空了(读取时),任务 可以选择阻塞(等待消息) 或 非阻塞(立即返回)。
- 高优先级任务更快地执行队列操作,如果多个任务在相同优先级,按照 时间片轮转 处理队列。
- 消息大小固定,但可以存储指向可变数据的指针。
1.2 队列相关 API 函数介绍
FreeRTOS 队列管理 API 速览
1. 队列的创建 API
API 函数 | 描述 |
---|---|
xQueueCreate() | 动态创建队列 |
xQueueCreateStatic() | 静态创建队列 |
2. 队列消息的写入 API
API 函数 | 描述 |
---|---|
xQueueSend() | 往队列尾部写入消息 |
xQueueSendToBack() | 同 xQueueSend() ,往队列尾部写入消息 |
xQueueSendToFront() | 往队列头部写入消息 |
xQueueOverwrite() | 覆写队列消息(仅限队列长度为 1) |
xQueueSendFromISR() | 在中断中往队列尾部写入消息 |
xQueueSendToBackFromISR() | 同 xQueueSendFromISR() ,往队列尾部写入 |
xQueueSendToFrontFromISR() | 在中断中往队列头部写入消息 |
xQueueOverwriteFromISR() | 在中断中覆写队列消息(仅限队列长度为 1) |
3. 队列消息的读取 API
API 函数 | 描述 |
---|---|
xQueueReceive() | 从队列头部读取消息,并删除消息 |
xQueuePeek() | 从队列头部读取消息(但不删除) |
xQueueReceiveFromISR() | 在中断中从队列头部读取消息,并删除消息 |
xQueuePeekFromISR() | 在中断中从队列头部读取消息(但不删除) |
1.3 实验
- start_task:用来创建其他的 3 个任务。
- task1:当按键 key1 或 key2 按下,将键值拷贝到队列 queue1(入队);当按键 key3 按下,将传输大数据,这里拷贝大数据的地址到队列 big_queue 中。
- task2:读取队列 queue1 中的消息(出队),打印出接收到的键值。
- task3:从队列 big_queue 读取大数据地址,通过地址访问大数据。
1 ) 创建队列程序:
QueueHandle_t queue1; /* 小数据句柄 */
QueueHandle_t big_queue; /* 大数据句柄 */
char buff[100] = {"dioajdiaj fdahjk324hjkhfjksdahjk#$@!@#jfaskdfhjka"};void task_start(void *pvParameters)
{/* 进入临界区:保护临界区里的代码不会被打断 */taskENTER_CRITICAL();/* 创建 queue1 队列 */queue1 = xQueueCreate(2, sizeof(uint8_t));if (queue1 != NULL){printf("queue1 队列创建成功\r\n");}else{printf("queue1 队列创建失败\r\n");}/* 创建 big_queue 队列 */big_queue = xQueueCreate(1, sizeof(char *));if (big_queue != NULL){printf("big_queue 队列创建成功\r\n");}else{printf("big_queue 队列创建失败\r\n");}/* 创建三个任务 */xTaskCreate((TaskFunction_t)task1,(char *)"task1",(configSTACK_DEPTH_TYPE)TASK1_STACK_SIZE,(void *)NULL,(UBaseType_t)TASK1_PRIORITY,(TaskHandle_t *)&Task1_Handler);xTaskCreate((TaskFunction_t)task2,(char *)"task2",(configSTACK_DEPTH_TYPE)TASK2_STACK_SIZE,(void *)NULL,(UBaseType_t)TASK2_PRIORITY,(TaskHandle_t *)&Task2_Handler);xTaskCreate((TaskFunction_t)task3,(char *)"task3",(configSTACK_DEPTH_TYPE)TASK3_STACK_SIZE,(void *)NULL,(UBaseType_t)TASK3_PRIORITY,(TaskHandle_t *)&Task3_Handler);/* 启动任务只需要执行一次即可,用完就删除自己 */vTaskDelete(NULL);/* 退出临界区 */taskEXIT_CRITICAL();
}
2 ) task1:
void task1(void *pvParameters)
{uint8_t key_value = 0;char *buf;BaseType_t err = 0;buf = &buff[0];char task_info[200];while (1){if (key[0].flag == 1 || key[1].flag == 1){key_value = key[0].flag;err = xQueueSendToBack(queue1, &key_value, portMAX_DELAY);if (err != pdTRUE){printf("queue1 队列发送失败\r\n");}key[0].flag = 0;key[1].flag = 0;}else if (key[2].flag == 1){err = xQueueSendToBack(big_queue, &buf, portMAX_DELAY);if (err != pdTRUE){printf("big_queue 队列发送失败\r\n");}key[2].flag = 0;}}
}
3 ) task2:
void task2(void *pvParameters)
{uint8_t key = 0;BaseType_t err = 0;while (1){err = xQueueReceive(queue1, &key, portMAX_DELAY);if (err != pdTRUE){printf("queue1 队列读取失败\r\n");}else{printf("queue1 读取队列成功,数据:%d\r\n", key);}}
}
4 ) task3:
void task3(void *pvParameters)
{char *buf;BaseType_t err = 0;while (1){for (int i = 0; i < 16; i++){if (key[i].flag == 1){err = xQueueReceive(big_queue, &buf, portMAX_DELAY);if (err != pdTRUE){printf("big_queue 队列读取失败\r\n");}else{printf("data:%s\r\n", buf);}}}}
}
2. FreeRTOS 队列集
队列集(Queue Set)是 FreeRTOS 中的一种数据结构,用于管理多个队列。它提供了一种有效的方式,通过单个 API 调用来操作和访问一组相关的队列。在多任务系统中,任务之间可能需要共享数据,而这些数据可能存储在不同的队列中。队列集的作用就是为了更方便地管理这些相关队列,使得任务能够轻松地访问和处理多个队列的数据。
2.1 队列集相关 API 函数介绍
函数 | 描述 |
---|---|
xQueueCreateSet() | 创建队列集,用于管理多个队列或信号量 |
xQueueAddToSet() | 向队列集添加队列或信号量 |
xQueueRemoveFromSet() | 从队列集中移除队列或信号量 |
xQueueSelectFromSet() | 获取队列集中有消息的队列(阻塞) |
xQueueSelectFromSetFromISR() | 在中断中获取队列集中有消息的队列(非阻塞) |
2.2 实验
- start_task:用来创建其他 2 个任务,并创建队列集、俩个队列,将这俩个队列添加到队列集中。
- task1:用于扫描按键,当 KEY1 按下,往队列1中写入数据,当 KEY2 按下,往队列2中写入数据。
- task2:读取队列集中的消息,并打印。
1 ) 创建队列集
QueueHandle_t queue1, queue2;
QueueSetHandle_t queue_set;void task_start(void *pvParameters)
{/* 进入临界区:保护临界区里的代码不会被打断 */taskENTER_CRITICAL();/* 创建 queue1 队列 */queue1 = xQueueCreate(1, sizeof(uint8_t));if (queue1 != NULL){printf("queue1 队列创建成功\r\n");}else{printf("queue1 队列创建失败\r\n");}/* 创建 queue2 队列 */queue2 = xQueueCreate(1, sizeof(char *));if (queue2 != NULL){printf("queue2 队列创建成功\r\n");}else{printf("queue2 队列创建失败\r\n");}/* 创建 queue2 队列 */queue_set = xQueueCreateSet(2);if (queue_set != NULL){printf("queue_set 队列创建成功\r\n");}else{printf("queue_set 队列创建失败\r\n");}xQueueAddToSet(queue1, queue_set);xQueueAddToSet(queue2, queue_set);/* 创建任务 */xTaskCreate((TaskFunction_t)task1,(char *)"task1",(configSTACK_DEPTH_TYPE)TASK1_STACK_SIZE,(void *)NULL,(UBaseType_t)TASK1_PRIORITY,(TaskHandle_t *)&Task1_Handler);xTaskCreate((TaskFunction_t)task2,(char *)"task2",(configSTACK_DEPTH_TYPE)TASK2_STACK_SIZE,(void *)NULL,(UBaseType_t)TASK2_PRIORITY,(TaskHandle_t *)&Task2_Handler);/* 启动任务只需要执行一次即可,用完就删除自己 */vTaskDelete(NULL);/* 退出临界区 */taskEXIT_CRITICAL();
}
2 ) task1:
void task1(void *pvParameters)
{char *buf;char text[20];BaseType_t err = 0;while (1){if (key[0].flag == 1){sprintf((char *)text, "queue1 数据");buf = &text[0];err = xQueueSendToBack(queue1, &buf, portMAX_DELAY);if (err != pdTRUE){printf("queue1 队列写入数据错误\r\n");}key[0].flag = 0;}else if (key[1].flag == 1){sprintf((char *)text, "queue2 数据");buf = &text[0];err = xQueueSendToBack(queue2, &buf, portMAX_DELAY);if (err != pdTRUE){printf("queue2 队列写入数据错误\r\n");}key[1].flag = 0;}vTaskDelay(100);}
}
3 ) task2:
void task2(void *pvParameters)
{char *buf;QueueSetMemberHandle_t queue = NULL;while (1){queue = xQueueSelectFromSet(queue_set, portMAX_DELAY);if (queue == queue1){xQueueReceive(queue, &buf, portMAX_DELAY);printf("queue1 读取到数据为: %s\r\n", buf);}if (queue == queue2){xQueueReceive(queue, &buf, portMAX_DELAY);printf("queue2 读取到数据为: %s\r\n", buf);}vTaskDelay(100);}
}
3. FreeRTOS 信号量
3.1 简介
信号量(Semaphore)是一种用于 任务同步 或 任务间资源互斥 的机制,主要用于:
- 任务同步:协调任务之间的执行顺序,如等待某个事件发生后再执行任务。
- 互斥访问:保护共享资源,防止多个任务同时访问而引起竞争问题。
FreeRTOS 提供了 二值信号量、计数信号量 和 互斥信号量 三种类型:
信号量类型 | 作用 |
---|---|
二值信号量(Binary Semaphore) | 只能取 0 或 1,常用于任务同步或简单的互斥访问 |
计数信号量(Counting Semaphore) | 可取多个值,适用于多个资源的同步管理 |
互斥信号量(Mutex) | 具备 优先级继承 机制,主要用于任务间的互斥访问 |
3.2 常用 API 函数
API 函数 | 作用 |
---|---|
xSemaphoreCreateBinary() | 创建二值信号量(初始值为 0,需要 xSemaphoreGive() 释放一次后才能使用) |
xSemaphoreCreateBinaryStatic() | 使用静态方式创建二值信号量 |
xSemaphoreCreateCounting() | 创建计数信号量 |
xSemaphoreCreateMutex() | 创建互斥信号量 |
xSemaphoreGive() | 释放信号量(+1) |
xSemaphoreTake() | 获取信号量(-1),如果没有可用信号量则阻塞 |
uxSemaphoreGetCount() | 获取信号量的计数值 |
xSemaphoreGiveFromISR() | 在中断中释放信号量 |
xSemaphoreTakeFromISR() | 在中断中获取信号量 |
信号量 API 函数允许指定阻塞时间。 阻塞时间表示当一个任务试图“获取”信号量时,如果信号不是立即可用,那么该任务进入阻塞状态的最大 “tick” 数。 如果多个任务在同一个信号量上阻塞,那么具有最高优先级的任务将在下次信号量可用时最先解除阻塞 。
在 FreeRTOS 中,信号量 实际上是一个特殊的队列,本质上 创建信号量就是创建一个只存储一个元素的队列,区别在于:
- 二值信号量(Binary Semaphore):队列长度固定为 1,每次只能存储 一个项目,用于 任务同步。
- 计数信号量(Counting Semaphore):队列长度可以大于 1,用于 多个资源的管理。
- 互斥信号量(Mutex):基于二值信号量,但增加了 优先级继承 机制,用于资源互斥
对于二值信号量 FreeRTOS 通过 xQueueGenericCreate() 这个通用队列创建函数来创建信号量,但是这个函数在 semphr.h 被宏定义为:
/* (UBaseType_t) 1 → 队列长度 = 1(二值信号量只能存储一个信号量) semSEMAPHORE_QUEUE_ITEM_LENGTH → 项目大小 = 0(不存储实际数据,仅表示可用信号量) queueQUEUE_TYPE_BINARY_SEMAPHORE → 队列类型 = 信号量队列 */ #define xSemaphoreCreateBinary() xQueueGenericCreate( ( UBaseType_t ) 1, semSEMAPHORE_QUEUE_ITEM_LENGTH, queueQUEUE_TYPE_BINARY_SEMAPHORE )
信号量的作用本质是管理这个队列是否为空。
而计数信号量创建函数等效为:
/* uxMaxCount → 队列长度 (计数信号量可以为任何整数) queueSEMAPHORE_QUEUE_ITEM_LENGTH → 项目大小 = 0(不存储实际数据,仅表示可用信号量) queueQUEUE_TYPE_BINARY_SEMAPHORE → 队列类型 = 信号量队列 */ xQueueGenericCreate( uxMaxCount, queueSEMAPHORE_QUEUE_ITEM_LENGTH, queueQUEUE_TYPE_COUNTING_SEMAPHORE );
互斥信号量创建函数等效为:
/* uxMutexLength 1 → 队列长度 = 1(互斥信号量只能存储一个信号量) uxMutexSize → 项目大小 = 0(不存储实际数据,仅表示可用信号量) queueQUEUE_TYPE_MUTEX → 队列类型 = 信号量队列 */ xQueueGenericCreate( uxMutexLength, uxMutexSize, queueQUEUE_TYPE_MUTEX )
3.3 实验
3.3.1 二值信号量实验
- start_task:用来创建其他的 2 个任务。
- task1:用于按键扫描,当检测到按键 KEY1 被按下时,释放二值信号量。
- task2:获取二值信号量,当成功获取后打印提示信息。
1 ) 创建二值信号量:
QueueHandle_t semaphore_handle;void task_start(void *pvParameters)
{/* 进入临界区:保护临界区里的代码不会被打断 */taskENTER_CRITICAL();/* 创建信号量 */semaphore_handle = xSemaphoreCreateBinary();if (semaphore_handle != NULL){printf("二值信号量创建成功\r\n");}else{printf("二值信号量创建失败\r\n");}/* 创建任务 */xTaskCreate((TaskFunction_t)task1,(char *)"task1",(configSTACK_DEPTH_TYPE)TASK1_STACK_SIZE,(void *)NULL,(UBaseType_t)TASK1_PRIORITY,(TaskHandle_t *)&Task1_Handler);xTaskCreate((TaskFunction_t)task2,(char *)"task2",(configSTACK_DEPTH_TYPE)TASK2_STACK_SIZE,(void *)NULL,(UBaseType_t)TASK2_PRIORITY,(TaskHandle_t *)&Task2_Handler);/* 启动任务只需要执行一次即可,用完就删除自己 */vTaskDelete(NULL);/* 退出临界区 */taskEXIT_CRITICAL();
}
2 ) task1:
void task1(void *pvParameters)
{BaseType_t err;while (1){if (key[0].flag == 1){err = xSemaphoreGive(semaphore_handle);if (err != pdTRUE){printf("释放信号量失败\r\n");}led[0].state = !led[0].state;HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, (GPIO_PinState)led[0].state);key[0].flag = 0;}}
}
3 ) task2:
void task2(void *pvParameters)
{BaseType_t err = 0;while (1){err = xSemaphoreTake(semaphore_handle,portMAX_DELAY);if (err != pdTRUE){printf("获取信号量失败\r\n");}else{printf("成功获取信号量\r\n");}}
}
3.3 .2 计数信号量实验
- start_task:用来创建其他的 2 个任务。
- task1:用于按键扫描,当检测到按键 KEY1 被按下时,释放计数型信号量。
- task2:每过一秒获取一次计数型信号量,当成功获取后打印信号量计数值。
1 ) 创建计数信号量:
QueueHandle_t semaphore_handle;void task_start(void *pvParameters)
{/* 进入临界区:保护临界区里的代码不会被打断 */taskENTER_CRITICAL();/* 创建计数信号量 */UBaseType_t uxMaxCount = 4;UBaseType_t uxInitialCount = 0;semaphore_handle = xSemaphoreCreateCounting(uxMaxCount,uxInitialCount);if (semaphore_handle != NULL){printf("计数信号量创建成功\r\n");}else{printf("计数信号量创建失败\r\n");}/* 创建任务 */xTaskCreate((TaskFunction_t)task1,(char *)"task1",(configSTACK_DEPTH_TYPE)TASK1_STACK_SIZE,(void *)NULL,(UBaseType_t)TASK1_PRIORITY,(TaskHandle_t *)&Task1_Handler);xTaskCreate((TaskFunction_t)task2,(char *)"task2",(configSTACK_DEPTH_TYPE)TASK2_STACK_SIZE,(void *)NULL,(UBaseType_t)TASK2_PRIORITY,(TaskHandle_t *)&Task2_Handler);/* 启动任务只需要执行一次即可,用完就删除自己 */vTaskDelete(NULL);/* 退出临界区 */taskEXIT_CRITICAL();
}
2 ) task1:
void task1(void *pvParameters)
{BaseType_t err;while (1){if (key[0].flag == 1){err = xSemaphoreGive(semaphore_handle);if (err != pdTRUE){printf("释放信号量失败\r\n");}else{printf("释放信号量\r\n");}led[0].state = !led[0].state;HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, (GPIO_PinState)led[0].state);key[0].flag = 0;}}
}
3 ) task2:
void task2(void *pvParameters)
{BaseType_t err = 0;while (1){if(uxSemaphoreGetCount(semaphore_handle) != 0){err = xSemaphoreTake(semaphore_handle,portMAX_DELAY);if (err != pdTRUE){printf("获取信号量失败\r\n");}else{printf("成功获取信号量\r\n");}}else{printf("无空闲资源分配\r\n");}vTaskDelay(1000);}
}
3.4 任务优先级翻转 bug 问题
优先级翻转是一个在实时系统中可能出现的问题,特别是在多任务环境中。该问题指的是一个较低优先级的任务阻塞了一个较高优先级任务的执行,从而导致高优先级任务无法及时完成。
典型的优先级翻转场景如下:
- 任务 A(高优先级):拥有高优先级,需要访问共享资源,比如一个关键数据结构。
- 任务 B(低优先级):拥有低优先级,目前正在访问该共享资源。
- 任务 C(中优先级):位于任务 A 和任务 B 之间,具有介于两者之间的优先级。
具体流程如下:
(1)任务 A 开始执行,但由于任务 B 正在访问共享资源,任务 A 被阻塞等待。
(2)任务 C 获得执行权,由于优先级高于任务 B,它可以抢占任务 B。
(3)任务 C 执行完成后,任务 B 被解除阻塞,开始执行,完成后释放了共享资源。
(4)任务 A 重新获取执行权,继续执行。
这个过程中,任务 A 因为资源被占用而被阻塞,而任务 B 却被中优先级的任务 C 抢占,导致任务 B 无法及时完成。这种情况称为优先级翻转,因为任务 C 的介入翻转了高优先级任务 A 的执行顺序。
实验模拟
模拟优先级翻转,观察对抢占式内核的影响:
- start_task:用来创建其他的 3 个任务。
- task1:低优先级任务,同高优先级一样的操作,不同的是低优先级任务占用信号
- task2:中等优先级任务,简单的应用任务。
- task3:高优先级任务,会获取二值信号量,获取成功以后打印提示信息,处理完后释放信号量。
1 ) 创建队列程序:
QueueHandle_t semaphore_handle;/*** @brief : 启动任务函数,创建三个任务,并删除自己** @param pvParameters*/
void task_start(void *pvParameters)
{/* 进入临界区:保护临界区里的代码不会被打断 */taskENTER_CRITICAL();/* 创建计数信号量 */UBaseType_t uxMaxCount = 1;UBaseType_t uxInitialCount = 1;semaphore_handle = xSemaphoreCreateCounting(uxMaxCount, uxInitialCount);if (semaphore_handle != NULL){printf("计数信号量创建成功\r\n");}else{printf("计数信号量创建失败\r\n");}/* 创建三个任务 */xTaskCreate((TaskFunction_t)task1,(char *)"task1",(configSTACK_DEPTH_TYPE)TASK1_STACK_SIZE,(void *)NULL,(UBaseType_t)TASK1_PRIORITY,(TaskHandle_t *)&Task1_Handler);xTaskCreate((TaskFunction_t)task2,(char *)"task2",(configSTACK_DEPTH_TYPE)TASK2_STACK_SIZE,(void *)NULL,(UBaseType_t)TASK2_PRIORITY,(TaskHandle_t *)&Task2_Handler);xTaskCreate((TaskFunction_t)task3,(char *)"task3",(configSTACK_DEPTH_TYPE)TASK3_STACK_SIZE,(void *)NULL,(UBaseType_t)TASK3_PRIORITY,(TaskHandle_t *)&Task3_Handler);/* 启动任务只需要执行一次即可,用完就删除自己 */vTaskDelete(NULL);/* 退出临界区 */taskEXIT_CRITICAL();
}
2 ) task1:
void task1(void *pvParameters)
{while (1){printf("低优先级 Task1 获取信号量\r\n");xSemaphoreTake(semaphore_handle, portMAX_DELAY);printf("低优先级 Task1 正在运行\r\n");HAL_Delay(3000);printf("低优先级 Task1 释放信号量\r\n");xSemaphoreGive(semaphore_handle);vTaskDelay(1000);}
}
3 ) task2:
void task2(void *pvParameters)
{while (1){printf("中优先级的 Task2 正在执行\r\n");HAL_Delay(1500);printf("Task2 执行完成一次.....\r\n");vTaskDelay(1000);}
}
4 ) task3:
void task3(void *pvParameters)
{while (1){printf("高优先级 Task3 获取信号量\r\n");xSemaphoreTake(semaphore_handle, portMAX_DELAY);printf("高优先级 Task3 正在运行\r\n");HAL_Delay(1000);printf("高优先级 Task3 释放信号量\r\n");xSemaphoreGive(semaphore_handle);vTaskDelay(1000);}
}
5 ) 实验结果
可以观察到 Task1 获取信号量后,Task2 通过抢占 Task1 实现了翻转优先级,实现了优于 Task3 的执行优先。
3.5 互斥信号量
互斥信号量是包含优先级继承机制的二进制信号量。二进制信号量能更好实现同步(任务间或任务与中断之间), 而互斥信号量有助于更好实现简单互斥(即相互排斥)。优先级继承是一种解决实时系统中任务调度引起的优先级翻转问题的机制。在具体的任务调度中,当一个高优先级任务等待一个低优先级任务所持有的资源时,系统会提升低优先级任务的优先级,以避免高优先级任务长时间等待的情况。
互斥信号量的获取和释放函数与二值信号量的相应函数相似,但有一个重要的区别:互斥信号量不支持在中断服务程序中直接调用。注意,当创建互斥信号量时,系统会自动进行一次信号量的释放操作。
通过互斥信号量来解决优先级翻转实验
1 ) 将二值信号量改为互斥信号量:
semaphore_handle = xSemaphoreCreateMutex();if (semaphore_handle != NULL){printf("互斥信号量创建成功\r\n");}else{printf("互斥信号量创建失败\r\n");}
2 ) 实验结果:
可以观察到 Task2 只能在 Task3 信号量释放的后才可以抢占运行,不会发生任务优先级的翻转(在 Task1 释放信号量后 Task2 立马抢占执行,而是 Task3 立刻占用信号量,开始执行 Task3 再执行 Task2)
4. FreeRTOS 事件标志组
4.1 简介
事件标志组(Event Groups)是 FreeRTOS 提供的一种轻量级任务间同步机制,允许任务或中断通过 设置或清除 “事件标志(Event Bits)” 来实现 事件通知、同步和状态监控。事件标志组类似于 二进制标志位集合,每个标志位可以单独操作,多个任务可以等待多个标志位满足特定条件后再执行。
事件标志组特点
- 每个事件标志组是一个 8/24 位独立的二进制标志位(Event Bits)。
- 任务可以等待多个事件标志位,并指定所有满足或任意一个满足时触发任务执行。
- 事件标志位可由任务或中断设置/清除,支持
ISR
操作。 - 比队列(Queue)和信号量(Semaphore)更高效,适用于简单事件同步。
事件标志组的适用场景
应用场景 | 适合事件标志组 | 适合队列或信号量 |
---|---|---|
多任务同步 | ✔️ 多任务需要等待某些事件发生 | ❌ |
中断通知任务 | ✔️ 事件触发后可直接通知多个任务 | ✔️ 但队列或信号量只能通知一个任务 |
状态监测 | ✔️ 可使用不同事件位表示不同状态 | ❌ |
任务间数据传输 | ❌ | ✔️ 队列适合传输数据 |
事件标志组和信号量都是 FreeRTOS 中用于任务同步和通信的机制,但它们适用于不同的场景,主要区别如下:
对比项 | 事件标志组(Event Flags Group) | 信号量(Semaphore) |
---|---|---|
用途 | 用于任务间事件通知和同步,标志位代表某个事件状态 | 用于任务间资源控制和同步,确保安全访问共享资源 |
状态表示 | 每个标志位只有 已设置/未设置 两种状态 | 信号量是一个计数器,可用于 计数、互斥、同步 |
任务等待 | 任务可等待 多个事件同时满足 或 任意一个事件发生 | 任务等待信号量计数变为 非零,然后继续执行 |
适用场景 | 适用于任务间 事件同步,如 数据准备完成、状态变更通知 | 适用于 资源访问控制、同步、互斥,防止多个任务同时访问共享资源 |
信号传递 | 可存储多个事件状态,即使任务不在等待,事件状态仍然保留 | 不可存储状态,如果任务未在等待,信号量会直接丢失 |
示例 | 任务 A 设置事件标志 → 任务 B 等待该事件标志并继续执行 | 任务 A 释放信号量 → 任务 B 获取信号量并访问共享资源 |
4.2 事件标志组和事件位数据类型
可以将事件组视为一个二进制标志集合,其中的每一位表示一个事件。任务可以设置(Set)、清(Clear)或等待(Wait)某些特定的事件位,从而实现任务间的 同步与信号触发。事件组的大小(即支持的 事件位数)受 FreeRTOS 配置项的影响,由 configUSE_16_BIT_TICKS
控制:
#define configUSE_16_BIT_TICKS 1 // 事件组内可用标志数位为8
#define configUSE_16_BIT_TICKS 0 // 事件组内可用标志数位为24
在 FreeRTOS 中,事件组的最大位数 由 EventBits_t
变量的大小决定,而 EventBits_t
的大小受 TickType_t
(用于计时的变量类型)影响。所以最高为32位,但是 FreeRTOS 保留了 8 位 供系统内部使用
4.3 事件标志组相关 API 函数介绍
API 函数 | 功能 |
---|---|
xEventGroupCreate() | 创建事件标志组 |
xEventGroupCreateStatic() | 使用静态地创建事件标志组 |
xEventGroupSetBits() | 设置一个或多个事件标志位 |
xEventGroupClearBits() | 清除一个或多个事件标志位 |
xEventGroupWaitBits() | 等待一个或多个事件标志位被置位 |
xEventGroupGetBits() | 获取当前事件标志状态 |
xEventGroupSetBitsFromISR() | 在中断中设置事件标志位 |
vEventGroupDelete() | 删除事件标志组 |
xEventGroupSync() | 多个任务同步等待所有事件标志 |
4.4 实验
- start_task:用来创建其他 3 个任务,并创建事件标志组。
- task1:读取按键按下键值,根据不同键值将事件标志组相应事件位置一,模拟事件发生。
- task2:同时等待事件标志组中的多个事件位,当这些事件位都置 1 的话就执行相应的处理。
- task3:等待事件标志组中的多个任一事件位,当这些事件位任意置 1 的话就执行相应的处理。
1 ) 创建事件标志组程序:
EventGroupHandle_t event_group;/*** @brief : 启动任务函数,创建三个任务,并删除自己** @param pvParameters*/
void task_start(void *pvParameters)
{/* 进入临界区:保护临界区里的代码不会被打断 */taskENTER_CRITICAL();/* 创建事件标志组 */event_group = xEventGroupCreate();if (event_group == NULL){printf("事件标志组创建失败\r\n");}else{printf("事件标志组创建成功\r\n");}/* 创建任务 */xTaskCreate((TaskFunction_t)task1,(char *)"task1",(configSTACK_DEPTH_TYPE)TASK1_STACK_SIZE,(void *)NULL,(UBaseType_t)TASK1_PRIORITY,(TaskHandle_t *)&Task1_Handler);xTaskCreate((TaskFunction_t)task2,(char *)"task2",(configSTACK_DEPTH_TYPE)TASK2_STACK_SIZE,(void *)NULL,(UBaseType_t)TASK2_PRIORITY,(TaskHandle_t *)&Task2_Handler);xTaskCreate((TaskFunction_t)task3,(char *)"task3",(configSTACK_DEPTH_TYPE)TASK3_STACK_SIZE,(void *)NULL,(UBaseType_t)TASK3_PRIORITY,(TaskHandle_t *)&Task3_Handler);/* 启动任务只需要执行一次即可,用完就删除自己 */vTaskDelete(NULL);/* 退出临界区 */taskEXIT_CRITICAL();
}
2 ) task1:
void task1(void *pvParameters)
{while (1){if (key[0].flag == 1){xEventGroupSetBits(event_group, 0x01 << 0);printf("事件0置位\r\n");key[0].flag = 0;}else if (key[1].flag == 1){xEventGroupSetBits(event_group, 0x01 << 1);printf("事件1置位\r\n");key[1].flag = 0;}else if (key[2].flag == 1){xEventGroupSetBits(event_group, 0x01 << 2);printf("事件2置位\r\n");key[2].flag = 0;}else if (key[3].flag == 1){xEventGroupSetBits(event_group, 0x01 << 3);printf("事件3置位\r\n");key[3].flag = 0;}vTaskDelay(100);}
}
3 ) task2:
void task2(void *pvParameters)
{while (1){xEventGroupWaitBits(event_group, 1 << 0 | 1 << 1, pdTRUE, pdTRUE, portMAX_DELAY);printf("事件0和1同时有效,执行task2...\r\n");vTaskDelay(100);}
}
4 ) task3:
void task3(void *pvParameters)
{while (1){xEventGroupWaitBits(event_group, 1 << 2 | 1 << 3, pdTRUE, pdFALSE, portMAX_DELAY);printf("事件2和3任一有效,执行task3...\r\n");vTaskDelay(100);}
}