序号 | 内容 | 链接 |
---|---|---|
1 | 任务状态类型 | 点我访问 |
2 | 任务间通信方式 | 点我访问 |
3 | 任务通信机制的配合使用 | 点我访问 |
FreeRTOS的任务间通信方式提供了多种机制来帮助任务之间交换信息、同步执行和共享资源。以下是几种常见的通信方式及其典型应用场景:
1. 信号量(Semaphores)
应用场景:
资源互斥访问:例如,假设多个任务需要访问一个共享的ADC(模拟数字转换器),可以使用互斥信号量来确保一次只有一个任务可以访问ADC。
事件通知:一个任务完成某项工作后,可以通过信号量通知其他任务,如数据采集完成。
1.1 创建信号量:
#include "freertos/semphr.h"
SemaphoreHandle_t xSemaphore;
void vSemaphoreCreate()
{
xSemaphore = xSemaphoreCreateBinary();
configASSERT(xSemaphore);
}
1.2 使用信号量:
void vSemaphoreTake()
{
/*获取信号量*/
if (xSemaphoreTake(xSemaphore, portMAX_DELAY) == pdTRUE)
{
// 信号量获取成功,执行临界区代码
// ...
/*释放信号量*/
xSemaphoreGive(xSemaphore);
}
}
2. 消息队列(Message Queues)
应用场景:
异步数据传输:一个任务采集传感器数据,并将其放入消息队列,另一个任务负责处理这些数据,从而实现异步通信。
命令响应:一个任务通过消息队列发送命令给另一个任务,接收任务处理命令后,再通过队列返回响应。
2.1 创建消息队列
#include "freertos/queue.h"
QueueHandle_t xQueue;
void vQueueCreate()
{
xQueue = xQueueCreate(10, sizeof(uint32_t));
configASSERT(xQueue);
}
2.2 发送消息队里
void vQueueSend()
{
uint32_t ulValue = 123;
xQueueSend(xQueue, &ulValue, portMAX_DELAY);
}
2.3 接收消息队列
void vQueueReceive()
{
uint32_t ulReceivedValue;
if (xQueueReceive(xQueue, &ulReceivedValue, portMAX_DELAY) == pdTRUE)
{
// 消息接收成功
}
}
3. 事件组(Event Groups)
应用场景:
多事件组合:当一个任务需要等待多个事件中的任何一个或几个事件发生时,可以使用事件组。例如,一个任务可能需要等待网络连接建立或外部存储器准备好,可以使用事件组来同时监听这两种事件。
3.1 创建事件组
#include "freertos/event_groups.h"
EventGroupHandle_t xEventGroup;
void vEventGroupCreate()
{
xEventGroup = xEventGroupCreate();
configASSERT(xEventGroup);
}
3.2 设置事件
void vEventGroupSetBits()
{
xEventGroupSetBits(xEventGroup, 1 << 0);
}
3.3 等待事件
void vEventGroupWaitBits()
{
uint32_t ulBits;
ulBits = xEventGroupWaitBits(xEventGroup, 1 << 0, pdFALSE, pdFALSE, portMAX_DELAY);
if (ulBits & (1 << 0))
{
// 事件已发生
}
}
4. 任务通知(Task Notifications)
应用场景:
轻量级通信:当一个任务需要简单地通知另一个任务某些事件已经发生时,可以使用任务通知,如任务完成、定时器到期或外部事件触发。
4.1 发送通知
void taskF(void *pvParameters)
{
while(1)
{
xTaskNotify(taskGHandle, 0, eIncrement);
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
4.2 接收通知
void taskG(void *pvParameters)
{
while(1)
{
uint32_t ulNotificationValue = ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
if(ulNotificationValue > 0)
{
// Notification received
// ...
}
}
}
5. 互斥量(Mutexes)和递归互斥量(Recursive Mutexes)
应用场景:
保护共享资源:当多个任务需要访问共享内存或硬件资源时,可以使用互斥量来保证资源的独占访问。递归互斥量允许一个任务多次获取同一互斥量,这在任务内部需要多次访问共享资源时非常有用。
5.1 创建互斥量
#include "semphr.h"
SemaphoreHandle_t xMutex = NULL;
void createMutex(void)
{
xMutex = xSemaphoreCreateMutex();
configASSERT(xMutex != NULL);
}
5.2 使用互斥量
void taskA(void *pvParameters)
{
while(1)
{
xSemaphoreTake(xMutex, portMAX_DELAY);
// Critical section
// Access shared resource here
// ...
xSemaphoreGive(xMutex);
}
}
5.3 创建递归互斥量
SemaphoreHandle_t xRecursiveMutex = NULL;
void createRecursiveMutex(void)
{
xRecursiveMutex = xSemaphoreCreateRecursiveMutex();
configASSERT(xRecursiveMutex != NULL);
}
5.4 使用递归互斥量
void taskB(void *pvParameters)
{
while(1)
{
xSemaphoreTakeRecursive(xRecursiveMutex, portMAX_DELAY);
// Critical section 1
// Access shared resource here
// ...
// If this task needs to re-enter the critical section:
xSemaphoreTakeRecursive(xRecursiveMutex, portMAX_DELAY);
// Critical section 2
// Access shared resource again
// ...
// Release lock twice because it was taken twice
xSemaphoreGiveRecursive(xRecursiveMutex);
xSemaphoreGiveRecursive(xRecursiveMutex);
}
}
6. 软件定时器(Software Timers)
虽然软件定时器本身不是直接的通信机制,但它们可以间接用于任务间的通信。
应用场景:
周期性任务执行:一个任务可以配置一个软件定时器,当定时器到期时,它会自动重新加载并执行回调函数,这可以用于定期执行任务,如数据上报、周期性维护等。
每种通信方式都有其优缺点和最适合的场景,选择合适的方法可以提高系统的效率和可靠性。在实际应用中,可能需要根据具体需求组合使用多种通信方式。例如,一个复杂的系统可能同时使用消息队列来传输数据,使用信号量来同步资源访问,以及使用事件组来处理多事件组合的情况。
6.1 定义软件定时器回调函数
static void prvTimerCallback(TimerHandle_t pxTimer)
{
// This function will be called every 1 second (as specified in the timer creation).
// You can put your code here that should run periodically.
// Example: Print something to the console
printf("Timer expired!\n");
// Optionally, you can restart the timer from here if it's a one-shot timer.
// xTimerReset(pxTimer, 0);
}
6.2 创建和配置软件定时器
#include "progtypes.h"
#include "timers.h"
static void prvTimerCallback(TimerHandle_t pxTimer);
void createTimer(void)
{
TimerHandle_t xTimer = xTimerCreate(
"DemoTimer", /* Just a text name, not used by the RTOS kernel. */
pdMS_TO_TICKS(1000), /* The rate at which the timer ticks. */
pdFALSE, /* The timer is not auto-reload (one-shot). */
(void *)0, /* The ID of the timer - not needed. */
prvTimerCallback);/* The function to be called when the timer expires. */
if(xTimer != NULL)
{
// Start the timer.
if(xTimerStart(xTimer, 0) != pdPASS)
{
// Failed to start the timer.
}
}
}
6.3 创建和启动软件定时器
void main_task(void *pvParameters)
{
// Create and start the timer.
createTimer();
// Your other tasks or code...
}
7. 队列集(Queue Sets)
队列集是一种用于管理多个队列和信号量的集合的机制,允许一个任务等待多个事件源上的数据或信号。队列集为任务提供了一种方式,使其能够在多个队列和信号量上同时等待并对任意一个队列或信号量中的事件作出响应。
队列集的主要特性
- 统一的等待接口:一个任务可以同时等待属于同一队列集的多个队列和信号量。这使得任务可以处理多个输入源,而无需为每个输入源单独管理等待逻辑。
- 灵活的事件处理:当队列集上的任一队列或信号量有数据或事件发生时,任务将被唤醒以处理该事件。
- 支持信号量和队列:队列集可以包含普通队列、二值信号量和计数信号量。
使用场景
队列集非常适合于需要从多个数据源接收数据或信号的任务场景,例如:
- 需要同时监视多个传感器的输入。
- 等待来自多个任务的命令或请求。
- 需要在多个信号量或事件标志上同步的复杂任务流程。
注意事项
- 队列集中的队列和信号量在被添加到队列集中后,不应再单独使用或直接操作。
- 队列集中的所有成员必须是空的,并且不能有 任何任务正在等待这些成员上的事件。
- 每个队列集的大小应足够大,以容纳所有可能被同时激活的队列和信号量 。
7.1 创建队列集
QueueSetHandle_t xQueueSet = xQueueCreateSet(10); // 创建一个最大容量为10的队列集
7.2 创建添加队列或信号量到队列集中
QueueHandle_t xQueue1 = xQueueCreate(5, sizeof(int)); // 创建队列
QueueHandle_t xQueue2 = xQueueCreate(5, sizeof(int));
SemaphoreHandle_t xSemaphore = xSemaphoreCreateBinary(); // 创建信号量
// 将队列和信号量添加到队列集
xQueueAddToSet(xQueue1, xQueueSet);
xQueueAddToSet(xQueue2, xQueueSet);
xQueueAddToSet(xSemaphore, xQueueSet);
7.3 使用队列集
void vTaskFunction(void *pvParameters) {
QueueSetMemberHandle_t xActivatedMember;
for (;;) {
// 等待队列集中任何队列或信号量上的事件
xActivatedMember = xQueueSelectFromSet(xQueueSet, portMAX_DELAY);
if (xActivatedMember == xQueue1) {
// 处理来自xQueue1的事件
} else if (xActivatedMember == xQueue2) {
// 处理来自xQueue2的事件
} else if (xActivatedMember == xSemaphore) {
// 处理来自xSemaphore的事件
}
}
}
8. 消息缓冲区(Message Buffers)
消息缓冲区(Message Buffers)用于在任务之间或任务与中断服务程序(ISR)之间传递消息。消息缓冲区支持可变长度消息,并且提供了一种高效且灵活的方式来传输数据。
消息缓冲区的特性
- 可变长度消息:每条消息可以是不同长度,消息缓冲区会在内部管理消息的边界。
- 任务与ISR间通信:消息缓冲区可以用于任务与中断之间的通信,这对于需要快速响应的系统非常有用。
- 单一接收者:消息缓冲区通常有一个接收者,可以有多个发送者。
应用场景
- 事件日志:当需要记录可变长度的日志消息时,消息缓冲区非常有用。
- 数据流处理:适合需要处理不同长度的数据块的场景。
- 实时通信:在任务与中断之间传递数据,确保高效的实时响应。
与队列的比较
- 数据结构:消息缓冲区以字节流形式存储消息,而队列以固定大小的项目存储数据。
- 灵活性:消息缓冲区支持可变长度消息,适用于需要传递多种长度消息的情况。
- 效率:在处理大数据块时,消息缓冲区可能比队列更高效,因为它减少了数据拷贝次数。
8.1 创建消息缓冲区
创建一个消息缓冲区需要指定其总大小(以字节为单位)。这个大小决定了缓冲区可以存储的最大数据量。
#include "FreeRTOS.h"
#include "message_buffer.h"
// 创建一个大小为100字节的消息缓冲区
MessageBufferHandle_t xMessageBuffer = xMessageBufferCreate(100);
8.2 发送消息
可以从任务或中断中发送消息到消息缓冲区。发送操作会将消息的长度信息和消息数据一起存储到缓冲区中。
void vSenderTask(void *pvParameters) {
const char *pcMessage = "Hello";
size_t xBytesSent;
// 发送消息到缓冲区
xBytesSent = xMessageBufferSend(xMessageBuffer, pcMessage, strlen(pcMessage), portMAX_DELAY);
if (xBytesSent != strlen(pcMessage)) {
// 处理发送失败的情况
}
}
8.3 接收消息
接收消息时,会先读取消息的长度,然后读取消息内容。接收操作是从消息缓冲区的开头开始的。
void vReceiverTask(void *pvParameters) {
char pcReceivedMessage[20];
size_t xReceivedBytes;
// 从缓冲区接收消息
xReceivedBytes = xMessageBufferReceive(xMessageBuffer, pcReceivedMessage, sizeof(pcReceivedMessage), portMAX_DELAY);
if (xReceivedBytes > 0) {
// 处理接收到的消息
}
}
9. 流缓冲区(Stream Buffers)
流缓冲区(Stream Buffers)是一种用于任务之间或任务与中断服务程序(ISR)之间传输数据的通信机制。流缓冲区支持字节流传输,适合需要处理连续数据流的应用。
流缓冲区的特性
- 字节流传输:流缓冲区以字节为单位传输数据,适合用于音频、视频流或其他连续数据的传输。
- 单一读写者:每个流缓冲区通常有一个写入者和一个读取者。
- 任务与ISR间通信:支持在任务和中断之间传输数据。
- 高效传输:减少数据拷贝,支持高效的数据流传输。
应用场景
- 音视频流:适合用于音频、视频等连续数据流的传输。
- 传感器数据:适合用于传输从传感器获取的连续数据。
- 数据记录:适用于实时记录和传输数据的场景。
与消息缓冲区的比较
- 数据结构:流缓冲区用于传输字节流,消息缓冲区用于传输可变长度的消息。
- 应用场景:流缓冲区适合用于连续数据传输,消息缓冲区适合用于离散消息传输。
- 单一读写者:流缓冲区通常只有一个写入者和一个读取者,而消息缓冲区可以有多个发送者。
9.1 创建流缓冲区
创建流缓冲区时需要指定其总大小,这决定了可以存储的数据总量。
#include "FreeRTOS.h"
#include "stream_buffer.h"
// 创建一个大小为100字节的流缓冲区
StreamBufferHandle_t xStreamBuffer = xStreamBufferCreate(100, 0);
第二个参数是触发接收任务解阻的触发级别,设置为0表示无触发级别。
9.2 发送数据到流缓冲区
可以从任务或中断中向流缓冲区写入数据。
void vSenderTask(void *pvParameters) {
const char *pcData = "Hello Stream";
size_t xBytesSent;
// 发送数据到流缓冲区
xBytesSent = xStreamBufferSend(xStreamBuffer, pcData, strlen(pcData), portMAX_DELAY);
if (xBytesSent != strlen(pcData)) {
// 处理发送失败的情况
}
}
9.3 从流缓冲区接收数据
可以从流缓冲区读取数据,接收的数据量可能小于请求的大小。
void vReceiverTask(void *pvParameters) {
char pcReceivedData[20];
size_t xReceivedBytes;
// 从流缓冲区接收数据
xReceivedBytes = xStreamBufferReceive(xStreamBuffer, pcReceivedData, sizeof(pcReceivedData), portMAX_DELAY);
if (xReceivedBytes > 0) {
// 处理接收到的数据
}
}
10. 通信方式占用内存大小比较
10.1 任务间通信方式占用内存
10.1.1 任务通知(Task Notification)
特点:最轻量级的任务间通信方式,专为单一任务通信设计,适用于一对一的简单事件通知或数据传递。
内存占用:非常小,因为每个任务都有一个 ulNotifiedValue 变量用于存储通知值,没有额外的缓冲区。
适用场景:任务之间快速的同步或事件通知,一般只用于单个任务的信号传递。
内存开销:任务本身的一个通知变量,开销非常低。
10.1.2 互斥锁(Mutex)
特点:用于保护共享资源,确保同一时刻只有一个任务可以访问特定资源。它类似于信号量,但专门用于互斥访问。FreeRTOS 中有普通的互斥锁(Mutex)和递归互斥锁(Recursive Mutex)。
内存占用:和信号量相似,互斥锁底层也是使用队列机制实现的,但互斥锁需要额外的控制逻辑,因此相比信号量略多一点。
适用场景:保护临界区、共享资源访问,如硬件设备或全局变量的独占访问。
内存开销:和信号量类似,约 80-100 字节,互斥锁的结构与信号量几乎相同,只是增加了一些额外控制。
10.1.3 信号量(Semaphore)
特点:用于同步任务或管理共享资源,通常用于二元信号量或计数信号量。
内存占用:低,因为信号量的底层实现是 FreeRTOS 的 Queue_t 结构,但不需要存储实际数据。信号量本质上是一个没有消息数据的队列。
适用场景:任务同步、资源管理、互斥锁保护临界区等。
内存开销:约 80-100 字节(Queue_t 的结构体),因为不存储数据,所以开销较小。
10.1.4 消息队列(Message Queues)
特点:可以在任务间传递数据,是 FreeRTOS 中非常常用的通信方式。队列用于任务间或任务与中断间的消息传递,支持多个任务或中断同时访问。
内存占用:中等,因为队列不仅需要存储控制结构,还需要存储实际传输的数据。内存开销取决于队列长度和单个消息大小。
内存开销:Queue_t 的大小(约 80-100 字节)+ 消息缓冲区大小(队列长度 × 每条消息的大小)。总内存开销会随着队列长度和消息大小增加。
10.1.5 消息缓冲区(Message Buffer)
特点:用于在任务之间传递可变长度的消息,允许传递较大数据块。消息缓冲区适合一对一通信。
内存占用:较高,除了存储控制结构外,还需要存储整个消息缓冲区的数据块。
适用场景:当需要传输较大的数据块或数据长度不固定时,可以使用消息缓冲区。
内存开销:控制结构的大小 + 消息缓冲区的大小(可以设置为较大的缓冲区以存储可变长度消息)。比队列稍大。
10.1.6 流缓冲区(Stream Buffer)
特点:用于任务之间传递连续的字节流,类似于消息缓冲区,但适用于一对一的流式数据传输。
内存占用:与消息缓冲区类似,但更适合传递连续字节流,如音频数据流。
适用场景:用于任务间传递连续的数据,通常用于音频或传感器数据等实时数据流。
内存开销:控制结构的大小 + 流缓冲区的大小。与消息缓冲区类似,但用于连续字节流。
10.1.7 事件组(Event Group)
特点:允许任务之间设置和等待多个标志位(事件),可以同时控制多个事件。事件组适合多任务同步。
内存占用:较高,事件组内部也使用 Queue_t 结构,但需要额外存储每个事件标志位的状态。
适用场景:当需要多个任务或事件同步时,使用事件组来管理不同的事件。
内存开销:除了控制结构,还需要存储每个事件的标志位。总内存消耗取决于事件数量,但整体开销会比信号量稍大。
10.1.8 消息队列集(Queue Set)
特点:消息队列集允许一个任务等待来自多个队列或信号量中的消息或信号。它相当于多个队列和信号量的组合体。通过队列集,一个任务可以等待从任意一个队列中接收消息,而不必依次检查每个队列。队列集非常适合需要处理多个输入源的场景。
内存占用:较高,因为消息队列集本质上是一个存储了多个队列或信号量的结构体,所以需要额外的内存来保存队列或信号量的引用。
适用场景:当任务需要从多个队列或信号量中获取数据时,使用消息队列集可以简化代码,并提高效率。
内存开销:队列集自身的控制结构(管理队列和信号量的集合) + 队列或信号量的内存开销。队列集占用的内存会随着加入的队列或信号量的数量增加,整体开销相对较高。
10.1.9 软件定时器(Software Timer)
特点:定时器可以用于任务之间的延时通知或周期性触发。每个软件定时器会有单独的任务处理回调。
内存占用:更高,因为每个定时器都有一个控制结构和回调函数。多个软件定时器还需要共享一个定时器服务任务,进一步增加内存消耗。
适用场景:适合周期性任务的触发或延时操作。
内存开销:控制结构 + 定时器任务的栈空间。开销比消息缓冲区更大。
10.2 占用内存大小对比
以下是任务间通信方式占用内存大小从小到大的排序
- 任务通知(Task Notification):极小
- 互斥锁(Mutex):小(与信号量类似)
- 信号量(Semaphore):小
- 消息队列(Message Queue):中等,取决于队列大小和消息大小
- 消息缓冲区(Message Buffer):中等偏大,适用于可变长度数据
- 流缓冲区(Stream Buffer):中等偏大,适用于连续字节流
- 事件组(Event Group):中等偏大,取决于事件数量
- 消息队列集(Queue Set):较大,取决于加入的队列和信号量的数量
- 软件定时器(Software Timer):较大,取决于定时器数量