目录
FreeRTOS 提供了丰富的任务间同步和通信机制,包括队列、二值信号量、计数信号量、互斥信号量,以及队列集等。以下内容将对这些功能逐一介绍,并提供示例帮助理解。
一、消息队列
消息队列(Queue)用于任务之间传递数据或事件。队列可以存储多个固定大小的消息,并以 FIFO(先进先出)的方式处理。
比喻:
想象一个“共享邮箱”。
每个任务就像一个人,他们通过这个邮箱交换信息。邮箱容量有限,当邮箱满了,发送者必须等到空间空出来才能放新的信件;当邮箱空了,接收者需要等待新信件到达。
使用场景:
用于任务之间或中断与任务之间的数据传递。
适合传递小量数据(如状态值、传感器数据等)。
实际场景:
任务 A 需要向任务 B 发送一个传感器数据,比如温度值,就可以用消息队列。
优点:
线程安全:消息队列内部使用互斥机制,避免并发访问问题。
支持多生产者、多消费者:可以让多个任务同时发送或接收消息。
FIFO机制:消息按照发送顺序处理,避免乱序。
内存消耗较大:需要分配足够的队列缓冲区。
性能受限:频繁的拷贝操作可能增加处理时间。
适合传递小数据:不适合传递大块数据。
相关 API
创建队列:
QueueHandle_t xQueueCreate(UBaseType_t uxQueueLength, UBaseType_t uxItemSize);
uxQueueLength
:队列可容纳的消息数量。
uxItemSize
:每条消息的大小(字节)。
发送消息:
BaseType_t xQueueSend(QueueHandle_t xQueue, const void * pvItemToQueue, TickType_t xTicksToWait);
从中断中发送消息:
BaseType_t xQueueSendFromISR(QueueHandle_t xQueue, const void * pvItemToQueue, BaseType_t * pxHigherPriorityTaskWoken);
接收消息:
BaseType_t xQueueReceive(QueueHandle_t xQueue, void * pvBuffer, TickType_t xTicksToWait);
示例
QueueHandle_t xQueue;
void SenderTask(void *pvParameters) {
int data = 0;
while (1) {
data++;
xQueueSend(xQueue, &data, portMAX_DELAY); // 向队列发送数据
printf("Sent: %d\n", data);
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
void ReceiverTask(void *pvParameters) {
int receivedData;
while (1) {
if (xQueueReceive(xQueue, &receivedData, portMAX_DELAY)) { // 从队列接收数据
printf("Received: %d\n", receivedData);
}
}
}
int main(void) {
xQueue = xQueueCreate(10, sizeof(int)); // 创建队列,长度为10,存储int类型
xTaskCreate(SenderTask, "Sender", 128, NULL, 1, NULL);
xTaskCreate(ReceiverTask, "Receiver", 128, NULL, 1, NULL);
vTaskStartScheduler();
return 0;
}
二、二值信号量
二值信号量(Binary Semaphore)只有两种状态:可用和不可用。它用于任务同步或事件通知。
比喻:
想象一个“电梯通行灯”。
当灯是绿灯时,表示电梯可以使用(信号量被释放)。当灯是红灯时,表示电梯被占用(信号量被获取)。只有一个人能通过该通道(最多有一个任务持有信号量)。
使用场景:
用于简单的任务同步,例如中断通知任务完成某个操作。
用于控制对某个资源的独占访问。
实际场景:
某任务等待中断事件触发,中断服务函数通过释放二值信号量通知任务。
优点:
- 轻量级:占用内存少,操作简单。
- 事件通知:适用于中断触发的事件处理。
- 二选一:只有两种状态(已释放/已获取),逻辑清晰。
缺点:
- 功能单一:不能记录多个事件发生的情况。
- 没有优先级继承:当任务竞争资源时,可能发生优先级翻转问题。
相关 API
创建信号量:
SemaphoreHandle_t xSemaphoreCreateBinary(void);
释放信号量:BaseType_t xSemaphoreGive(SemaphoreHandle_t xSemaphore);
从中断中释放信号量:BaseType_t xSemaphoreGiveFromISR(SemaphoreHandle_t xSemaphore, BaseType_t * pxHigherPriorityTaskWoken);
获取信号量:BaseType_t xSemaphoreTake(SemaphoreHandle_t xSemaphore, TickType_t xTicksToWait);
示例
SemaphoreHandle_t xBinarySemaphore;
void ISR_Handler(void) {
xSemaphoreGiveFromISR(xBinarySemaphore, NULL); // 在中断中释放信号量
}
void Task(void *pvParameters) {
while (1) {
if (xSemaphoreTake(xBinarySemaphore, portMAX_DELAY)) { // 等待信号量
printf("Semaphore taken, performing task\n");
}
}
}
int main(void) {
xBinarySemaphore = xSemaphoreCreateBinary();
xTaskCreate(Task, "Task", 128, NULL, 1, NULL);
vTaskStartScheduler();
return 0;
}
三、计数信号量
计数信号量(Counting Semaphore)维护一个计数值,用于控制对共享资源的访问或记录多个事件。
比喻:
想象一个“停车场计数器”。
停车场有多个车位,每次一辆车进入,就减少一个车位(获取信号量)。每次有一辆车离开,就增加一个车位(释放信号量)。车位数量不能超过停车场的总容量(信号量的最大计数值)。
使用场景:
用于管理多个相同资源的访问,例如限制对多通道资源的并发访问。
用于计数事件发生的次数。
实际场景:
一个任务控制对共享资源(如硬件模块)的访问,资源数量有限(如多通道 ADC 的多个输入通道)。
优点:
- 灵活性高:可以计数多个事件或管理多资源。
- 并发性支持:支持多任务对多个资源的访问。
- 适合事件计数:能够记录事件未被处理的次数。
缺点:
- 复杂性增加:需要仔细设计初始计数值和最大计数值。
- 不支持优先级继承:与二值信号量一样,可能发生优先级翻转问题。
相关 API
创建计数信号量:
SemaphoreHandle_t xSemaphoreCreateCounting(UBaseType_t uxMaxCount, UBaseType_t uxInitialCount);
示例
SemaphoreHandle_t xCountingSemaphore;
void ProducerTask(void *pvParameters) {
while (1) {
xSemaphoreGive(xCountingSemaphore); // 增加计数
printf("Produced an item\n");
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
void ConsumerTask(void *pvParameters) {
while (1) {
if (xSemaphoreTake(xCountingSemaphore, portMAX_DELAY)) { // 消费资源
printf("Consumed an item\n");
}
}
}
int main(void) {
xCountingSemaphore = xSemaphoreCreateCounting(5, 0); // 最大计数为5,初始为0
xTaskCreate(ProducerTask, "Producer", 128, NULL, 1, NULL);
xTaskCreate(ConsumerTask, "Consumer", 128, NULL, 1, NULL);
vTaskStartScheduler();
return 0;
}
四、优先级翻转与互斥信号量
优先级翻转
当高优先级任务等待低优先级任务释放资源时,中间优先级任务可能阻塞高优先级任务的运行,导致优先级翻转问题。
优先级翻转(Priority Inversion)是实时系统中一种常见的问题。当高优先级任务(Task H)等待低优先级任务(Task L)释放某个共享资源时,如果有一个中等优先级任务(Task M)占用CPU时间片,并且没有参与资源争夺,Task M 会阻塞 Task H 的运行。结果就是,高优先级任务的执行被间接推迟,从而违背了实时系统优先级的初衷。
优先级翻转的发生条件
优先级翻转一般在以下条件下发生:
- 共享资源:多个任务需要访问同一个共享资源(如全局变量、硬件设备)。
- 任务优先级不同:系统中存在不同优先级的任务。
- 不正确的资源管理机制:没有使用合适的同步工具(如互斥信号量)来保护资源
比喻:
想象一个“超市收银队伍”。
高优先级任务就像 VIP 顾客,他们应该先结账。低优先级任务是普通顾客,按顺序结账。假如收银员(资源)正在服务低优先级顾客,而中优先级顾客不断插队,高优先级顾客就会被无限阻塞(优先级翻转)。
实际场景:
任务 A(高优先级)需要使用某资源,而任务 B(低优先级)持有该资源,这时任务 C(中优先级)阻塞了任务 B 的运行,导致 A 被阻塞。
优点:
- 暴露潜在问题:帮助设计者意识到系统中可能存在的任务竞争问题。
- 通过优先级继承机制解决:互斥信号量可以解决优先级翻转问题。
缺点:
- 高优先级任务可能被长时间阻塞:对实时系统性能有负面影响。
- 调试难度较大:发生优先级翻转的场景可能隐藏较深。
互斥信号量简介
互斥信号量(Mutex)是一种特殊的二值信号量,附带优先级继承机制,用于解决优先级翻转问题。
比喻:
想象一个“餐馆单人卫生间”。
卫生间一次只能容纳一个人(信号量被获取)。如果一个 VIP 需要使用卫生间,而普通顾客占用了卫生间,VIP 的优先级会暂时提升,直到他完成后优先级恢复正常(优先级继承)。
使用场景:
用于保护共享资源(如全局变量)的访问,防止多个任务同时修改资源。
可用于防止优先级翻转问题。
实际场景:
保护共享资源(如全局变量)免受任务并发访问,并防止优先级翻转。
优点:
- 支持优先级继承:防止优先级翻转,确保系统高效运行。
- 保护共享资源:提供线程安全的资源访问。
- 灵活性高:可以应用于任务之间和任务-中断之间的同步。
缺点:
- 性能开销:优先级继承机制增加了处理开销。
- 死锁风险:不正确的使用可能导致死锁问题。
相关 API
创建互斥信号量:
SemaphoreHandle_t xSemaphoreCreateMutex(void);
释放互斥信号量:BaseType_t xSemaphoreGive(SemaphoreHandle_t xSemaphore);
获取互斥信号量:BaseType_t xSemaphoreTake(SemaphoreHandle_t xSemaphore, TickType_t xTicksToWait);
示例
#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"
#include <stdio.h>
/* 互斥信号量句柄 */
SemaphoreHandle_t xMutex;
/* 共享资源(模拟打印) */
void SharedResource(const char *message) {
for (int i = 0; i < 3; i++) {
printf("%s: %d\n", message, i);
vTaskDelay(pdMS_TO_TICKS(100)); // 模拟操作耗时
}
}
/* 任务1 */
void Task1(void *pvParameters) {
while (1) {
/* 获取互斥信号量 */
if (xSemaphoreTake(xMutex, portMAX_DELAY) == pdTRUE) {
printf("Task1 accessing shared resource...\n");
SharedResource("Task1");
printf("Task1 releasing shared resource...\n");
/* 释放互斥信号量 */
xSemaphoreGive(xMutex);
}
vTaskDelay(pdMS_TO_TICKS(500)); // 模拟任务间隔
}
}
/* 任务2 */
void Task2(void *pvParameters) {
while (1) {
/* 获取互斥信号量 */
if (xSemaphoreTake(xMutex, portMAX_DELAY) == pdTRUE) {
printf("Task2 accessing shared resource...\n");
SharedResource("Task2");
printf("Task2 releasing shared resource...\n");
/* 释放互斥信号量 */
xSemaphoreGive(xMutex);
}
vTaskDelay(pdMS_TO_TICKS(700)); // 模拟任务间隔
}
}
int main(void) {
/* 创建互斥信号量 */
xMutex = xSemaphoreCreateMutex();
/* 检查互斥信号量是否创建成功 */
if (xMutex == NULL) {
printf("Failed to create mutex!\n");
return -1;
}
/* 创建任务 */
xTaskCreate(Task1, "Task1", 128, NULL, 2, NULL);
xTaskCreate(Task2, "Task2", 128, NULL, 1, NULL);
/* 启动调度器 */
vTaskStartScheduler();
/* 如果程序运行到这里,说明调度器启动失败 */
for (;;);
return 0;
}
五、队列集
队列集(Queue Set)允许多个队列或信号量共享一个等待点,用于处理多个事件来源的场景。
比喻:
想象一个“快递收发中心”。
多个快递员(队列或信号量)会把包裹送到收发中心。收发中心的工作人员从中选择最早到达的包裹进行处理。不用每次检查所有快递员,而是集中处理。
使用场景:
用于监控多个消息队列或信号量,处理最早到达的事件。
一个任务需要从多个数据源中选择事件。
实际场景:
一个任务需要处理来自多个队列或信号量的事件。
优点:
- 集中管理:可以通过一个 API 监控多个队列或信号量。
- 提高效率:不需要轮询多个队列,节省 CPU 时间。
- 支持多种同步机制:既支持消息队列,也支持信号量。
缺点:
- 内存消耗较大:需要额外的内存来存储队列集信息。
- 复杂性高:使用和调试队列集比单一队列或信号量复杂。
相关 API
创建队列集:
QueueSetHandle_t xQueueCreateSet(UBaseType_t uxEventQueueLength);
将队列添加到队列集:BaseType_t xQueueAddToSet(QueueSetMemberHandle_t xQueueOrSemaphore, QueueSetHandle_t xQueueSet);
从队列集中获取事件:QueueSetMemberHandle_t xQueueSelectFromSet(QueueSetHandle_t xQueueSet, TickType_t xTicksToWait);
示例
QueueSetHandle_t xQueueSet;
QueueHandle_t xQueue1, xQueue2;
void Task(void *pvParameters) {
QueueSetMemberHandle_t xActivatedMember;
int receivedData;
for (;;) {
xActivatedMember = xQueueSelectFromSet(xQueueSet, portMAX_DELAY); // 等待事件
if (xActivatedMember == xQueue1) {
xQueueReceive(xQueue1, &receivedData, 0);
printf("Received from Queue1: %d\n", receivedData);
} else if (xActivatedMember == xQueue2) {
xQueueReceive(xQueue2, &receivedData, 0);
printf("Received from Queue2: %d\n", receivedData);
}
}
}
int main(void) {
xQueue1 = xQueueCreate(5, sizeof(int));
xQueue2 = xQueueCreate(5, sizeof(int));
xQueueSet = xQueueCreateSet(10);
xQueueAddToSet(xQueue1, xQueueSet);
xQueueAddToSet(xQueue2, xQueueSet);
xTaskCreate(Task, "Task", 128, NULL, 1, NULL);
vTaskStartScheduler();
return 0;
}
六、总结
总结
消息队列:任务间数据传递。
二值信号量:事件同步。
计数信号量:资源计数。
互斥信号量:解决优先级翻转。
队列集:管理多个事件来源。
这些功能可以灵活组合,满足多种实时应用需求。
形象化比喻一览表
总结对比表:通过这些对比可以更好地选择合适的机制,解决具体应用中的问题。