任务间通信
- 任务间通信是需要通过某些机制进行数据交换和同步,常见的任务间通信有以下几种方式
- 共享内存:读写效率高,但是需要轮询访问
- 消息队列:用于任务之间的数据传递。
- 信号量(二值信号量、计数信号量):用于任务间的同步或资源管理。
- 互斥锁:用于保护共享资源,防止任务竞争。
- 任务通知:轻量级的任务间通信,用于信号量或事件同步的替代方案。 - 多个任务可以共享系统中的一些资源(如全局变量、内存、硬件外设等),这些资源对所有任务都是可见的。但如果多个任务同时访问同一资源,可能会出现竞态条件,因此需要通过同步机制(如互斥锁、信号量等)来确保共享资源的正确访问。
一、消息队列
- 在FreeRTOS中,消息队列(Queue)是一种任务间通信的主要机制。它允许一个任务或中断将消息发送到队列中,另一个任务可以从该队列中读取并处理消息。
- 使用场景:任务间通信,任务与中断的通信
1.xQueueCreate() 创建消息队列
原型:
QueueHandle_t xQueueCreate(UBaseType_t uxQueueLength, UBaseType_t uxItemSize);
参数:
uxQueueLength:队列的长度,即队列可以容纳的消息数量。
uxItemSize:每条消息的大小(字节数)。
返回值:
返回一个 QueueHandle_t,即队列句柄。如果返回值为 NULL,表示队列创建失败。
使用范例
QueueHandle_t xQueue;
xQueue = xQueueCreate(10, sizeof(int));
2.xQueueSend() 将消息发送到队列。
原型:
BaseType_t xQueueSend(QueueHandle_t xQueue, const void *pvItemToQueue, TickType_t xTicksToWait);
参数:
xQueue:消息队列的句柄。
pvItemToQueue:指向需要发送到队列的消息的指针。
xTicksToWait:阻塞的最大时钟节拍数。如果队列已满,任务将在此时间内等待。如果设置为 0,任务不会等待。
返回值:
pdPASS 表示消息成功发送。
errQUEUE_FULL 表示队列已满,消息发送失败。
使用范例:
int value = 100;
if (xQueueSend(xQueue, &value, pdMS_TO_TICKS(100)) == pdPASS) {
}
3.xQueueSendFromISR() 在中断服务程序(ISR)中将消息发送到队列。
参数:
xQueue:消息队列的句柄。
pvItemToQueue:指向需要发送到队列的消息的指针。
pxHigherPriorityTaskWoken:指向一个标志变量,该变量指示如果这个发送操作使得某个更高优先级的任务应该立即被切换执行。
返回值:
pdPASS 表示消息发送成功。
errQUEUE_FULL 表示队列已满,消息发送失败。
4.xQueueReceive() 从队列中接收消息。
原型:
BaseType_t xQueueReceive(QueueHandle_t xQueue, void *pvBuffer, TickType_t xTicksToWait);
参数:
xQueue:消息队列的句柄。
pvBuffer:接收消息的缓冲区。
xTicksToWait:阻塞的最大时钟节拍数。如果设置为 0,任务不会等待,直接返回。
返回值:
pdPASS 表示成功接收消息。
pdFALSE 表示队列为空,未接收到消息。
使用范例
int receivedValue;
if (xQueueReceive(xQueue, &receivedValue, pdMS_TO_TICKS(100)) == pdPASS) {
}
5. vQueueDelete() 删除队列并释放相关的内存。
函数原型:
void vQueueDelete(QueueHandle_t xQueue);
参数:
xQueue:需要删除的消息队列的句柄。
使用范例:
vQueueDelete(xQueue);
6. xQueuePeek() 查看队列中消息的副本,但不移除消息。
函数原型:
BaseType_t xQueuePeek(QueueHandle_t xQueue, void *pvBuffer, TickType_t xTicksToWait);
参数:
xQueue:消息队列的句柄。
pvBuffer:存放消息的缓冲区。
xTicksToWait:等待队列中有消息的时间,类似 xQueueReceive() 的阻塞参数。
返回值:
pdPASS 表示查看消息成功。
pdFALSE 表示队列为空,没有消息可查看。
使用示例:
int peekedValue;
if (xQueuePeek(xQueue, &peekedValue, pdMS_TO_TICKS(100)) == pdPASS) {
}
7.消息队列使用举例
#include "main.h"
#include "cmsis_os.h"
#include "stdio.h"
TIM_HandleTypeDef htim5;
UART_HandleTypeDef huart1;
osThreadId defaultTaskHandle;
void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_TIM5_Init(void);
static void MX_USART1_UART_Init(void);
void StartDefaultTask(void const * argument);
int __io_putchar(int ch)
{
UBaseType_t vxx;
vxx = portSET_INTERRUPT_MASK_FROM_ISR();
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 1);
portCLEAR_INTERRUPT_MASK_FROM_ISR(vxx);
return ch;
}
struct timex {
int hour;
int min;
int sec;
};
xQueueHandle timeQueueHandle = NULL;
TaskHandle_t taskCustomHandle;
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
static struct timex t = {10, 59, 28};
t.sec++;
if (t.sec == 60) {
t.sec = 0;
t.min++;
if (t.min == 60) {
t.min = 0;
t.hour++;
}
}
BaseType_t ret = xQueueSendToBackFromISR(timeQueueHandle, &t, NULL);
if (ret == pdFAIL) {
printf("timer int send error, peer task may be kill\r\n");
}
}
void task_fun_custom(void *args)
{
BaseType_t ret;
struct timex t;
while (1) {
ret = xQueueReceive(timeQueueHandle, &t, 2000);
if (ret == pdFALSE) {
printf("tsk custom push err\r\n");
continue;
}
printf("task recv:%02d:%02d:%02d\r\n", t.hour, t.min, t.sec);
}
}
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_TIM5_Init();
MX_USART1_UART_Init();
timeQueueHandle = xQueueCreate(3, sizeof(struct timex));
if (timeQueueHandle == NULL) {
printf("xQueueCreate err\r\n");
return -34;
}
HAL_TIM_Base_Start_IT(&htim5);
osThreadDef(defaultTask, StartDefaultTask, osPriorityNormal, 0, 128);
defaultTaskHandle = osThreadCreate(osThread(defaultTask), NULL);
BaseType_t ret;
ret = xTaskCreate(task_fun_custom, "taskCustom", 128, NULL, osPriorityNormal, &taskCustomHandle);
if (ret == pdFALSE) {
printf("xTaskCreate for custom err\r\n");
return -34;
}
osKernelStart();
while (1) {
}
}
二、互斥锁
- 在FreeRTOS中,互斥锁是一种专门为任务同步设计的机制,确保在任何时刻,只有一个任务能够访问被互斥锁保护的共享资源。
- 使用场景:共享资源访问、临界区保护、任务同步
1.xSemaphoreCreateMutex(): 创建一个互斥锁。
函数原型:
SemaphoreHandle_t xSemaphoreCreateMutex(void);
使用示例
SemaphoreHandle_t xMutex = xSemaphoreCreateMutex();
if (xMutex == NULL) {
}
2. xSemaphoreTake(): 获取(上锁)互斥锁。
BaseType_t xSemaphoreTake(SemaphoreHandle_t xSemaphore, TickType_t xTicksToWait);
参数:
xSemaphore: 互斥锁句柄。
xTicksToWait: 等待时间(以tick为单位)。如果设置为0,则任务不会等待。
返回值:
pdPASS: 成功获取互斥锁。
pdFAIL: 任务未能获取到互斥锁(超时或其他错误)。
使用范例:
if (xSemaphoreTake(xMutex, portMAX_DELAY) == pdPASS) {
}
- xSemaphoreGive()用于释放(解锁)互斥锁。
函数原型:
BaseType_t xSemaphoreGive(SemaphoreHandle_t xSemaphore);
参数:
xSemaphore: 互斥锁句柄。
返回值:
pdPASS: 成功释放互斥锁。
pdFAIL: 释放锁失败(通常不太会发生)。
4.互斥锁代码举例
#include "main.h"
#include "cmsis_os.h"
#include "stdio.h"
SemaphoreHandle_t xMutex;
void task1(void *pvParameters);
void task2(void *pvParameters);
int sharedVariable = 0;
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USART1_UART_Init();
xMutex = xSemaphoreCreateMutex();
if (xMutex == NULL) {
printf("Mutex creation failed\r\n");
return -1;
}
xTaskCreate(task1, "Task 1", 128, NULL, 1, NULL);
xTaskCreate(task2, "Task 2", 128, NULL, 1, NULL);
vTaskStartScheduler();
while (1) {}
}
void task1(void *pvParameters)
{
while (1) {
if (xSemaphoreTake(xMutex, portMAX_DELAY) == pdPASS) {
sharedVariable++;
printf("Task 1: Shared Variable = %d\r\n", sharedVariable);
vTaskDelay(500 / portTICK_PERIOD_MS);
xSemaphoreGive(xMutex);
}
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}
void task2(void *pvParameters)
{
while (1) {
if (xSemaphoreTake(xMutex, portMAX_DELAY) == pdPASS) {
sharedVariable += 2;
printf("Task 2: Shared Variable = %d\r\n", sharedVariable);
vTaskDelay(500 / portTICK_PERIOD_MS);
xSemaphoreGive(xMutex);
}
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}
三、二值信号量
- 与互斥有很多相同点
- 基本操作
xSemaphoreCreateBinary(): 创建二值信号量。
xSemaphoreTake(): 获取二值信号量(等待信号量可用)。
xSemaphoreGive(): 释放二值信号量(释放信号)。
- 与互斥锁的区别
- 优先级反转问题:二值信号量没有优先级继承机制,因此可能出现优先级反转问题,而互斥锁提供了优先级继承机制来避免这个问题。
- 目的不同:二值信号量用于任务或中断之间的同步,而互斥锁主要用于保护共享资源。
- 行为不同:二值信号量在释放后,任何一个等待的任务都可以获取,而互斥锁则是用来保护任务之间对共享资源的独占访问。
- 代码示例
#include "main.h"
#include "cmsis_os.h"
#include "stdio.h"
SemaphoreHandle_t xBinarySemaphore;
void task1(void *pvParameters);
void task2(void *pvParameters);
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USART1_UART_Init();
xBinarySemaphore = xSemaphoreCreateBinary();
if (xBinarySemaphore == NULL) {
printf("Binary Semaphore creation failed\r\n");
return -1;
}
xTaskCreate(task1, "Task 1", 128, NULL, 1, NULL);
xTaskCreate(task2, "Task 2", 128, NULL, 1, NULL);
vTaskStartScheduler();
while (1) {
}
}
void task1(void *pvParameters)
{
while (1) {
if (xSemaphoreTake(xBinarySemaphore, portMAX_DELAY) == pdPASS) {
printf("Task 1: Received signal from Task 2\r\n");
}
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}
void task2(void *pvParameters)
{
while (1) {
printf("Task 2: Performing some work\r\n");
xSemaphoreGive(xBinarySemaphore);
vTaskDelay(2000 / portTICK_PERIOD_MS);
}
}
优先级翻转问题
## 先级翻转问题可以发生在多任务系统中,当高优先级任务 taskHigh 和低优先级任务 taskLow 需要访问同一个共享资源时。假设这个资源由一个锁 lock 来保护。
如果 taskLow 先获得了锁并开始使用共享资源,而这时 taskHigh 想要访问该资源,taskHigh 就会被阻塞,等待 taskLow 释放锁。与此同时,系统中还有一些中等优先级的任务(taskMid1、taskMid2 等),它们的优先级比 taskLow 高,但比 taskHigh 低。由于这些中等优先级任务可以打断 taskLow,就会导致 taskLow 的执行被延迟,进而延误了 taskHigh 的执行。这个现象称为优先级翻转。
如何解决优先级翻转?—— 优先级继承
为了避免这种情况,使用了优先级继承机制。当 taskHigh 被阻塞时,系统会临时提升 taskLow 的优先级到与 taskHigh 相同的级别。这样,taskLow 可以快速完成它的任务,因为它的优先级现在足够高,不会被中等优先级任务打断。一旦 taskLow 完成并释放了锁,taskHigh 会继续执行,taskLow 的优先级也恢复到原来的值。
互斥锁(Mutex):支持优先级继承机制,因此在发生优先级翻转时,能够自动提升低优先级任务的优先级,确保它能尽快释放资源。
信号量(Semaphore):没有优先级继承机制,这意味着在使用信号量时,如果发生优先级翻转,系统不会自动调整任务的优先级,因此高优先级任务可能被更长时间地阻塞。
总结:优先级继承是一种解决高优先级任务因为低优先级任务被阻塞的问题,通过临时提升低优先级任务的优先级,使它能更快释放锁,恢复系统的正常执行顺序。
4.任务通知
- 任务通知(Task Notification)是FreeRTOS中用于任务间通信和同步的一种轻量级机制。与信号量、队列等其他同步工具相比,任务通知的效率更高,且占用更少的内存资源。
- 每个任务在FreeRTOS中都有一个专用的通知值,它可以用来发送简单的通知或传递小量数据。
基本概念
- 通知值:每个任务在FreeRTOS中都有一个与之关联的32位的通知值,这个值可以由其他任务或中断服务程序(ISR)设置或修改。
- 通知行为:一个任务可以等待它的通知值被设置。通知值可以表示一个事件(通知发生)或是传递实际的数据。
- 同步和通信:任务通知可以用于实现任务间的同步和事件传递,例如某个任务执行完成后通知另一个任务继续执行,或者中断通知任务处理外部事件。
常用函数
- xTaskNotifyGive()
用于从任务或中断中向另一个任务发送通知。它的功能类似于信号量的“给”,可以唤醒目标任务。
函数原型:
void xTaskNotifyGive(TaskHandle_t xTaskToNotify);
使用示例:
xTaskNotifyGive(xTaskHandle);
- ulTaskNotifyTake()
用于任务等待通知。任务在调用该函数时,会进入阻塞状态,直到收到通知或者超时。
函数原型:
uint32_t ulTaskNotifyTake(BaseType_t xClearCountOnExit, TickType_t xTicksToWait);
参数:
xClearCountOnExit:如果为 pdTRUE,在接收到通知时会将通知计数清零。只处理一次
xClearCountOnExit:当 pdFALSE 作为参数传入时,通知计数在任务处理后不会被清零(val--)。
这意味着如果任务在等待期间接收了多次通知,每次调用 ulTaskNotifyTake() 后,通知计数会递减,直到处理完所有通知为止。
这种情况适用于任务需要处理每个通知,而不是只处理一次通知。
xTicksToWait:等待通知的时间,单位为时钟节拍。可以设置为 portMAX_DELAY 来表示无限期等待。
返回值:
返回收到通知的计数。
使用示例:
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
- xTaskNotify()
用于向任务发送通知,并可以选择性更新任务的通知值。可以用于传递事件或数值。
函数原型:
BaseType_t xTaskNotify(TaskHandle_t xTaskToNotify, uint32_t ulValue, eNotifyAction eAction);
参数:
xTaskToNotify:要通知的任务句柄。
ulValue:要传递的值。
eAction:定义如何处理通知值的操作。常见选项包括:
eSetBits:设置某些位。
eIncrement:对通知值进行递增。
eSetValueWithOverwrite:用新值覆盖通知值。
eSetValueWithoutOverwrite:如果通知值未被清除,则不覆盖现有值。
返回值:
pdPASS:表示通知成功。
pdFAIL:表示通知失败。
使用示例:
xTaskNotify(xTaskHandle, 0x01, eSetBits);
- xTaskNotifyWait()
用于任务等待通知并检索通知值。任务可以进入阻塞状态,直到收到通知。
函数原型:
BaseType_t xTaskNotifyWait(uint32_t ulBitsToClearOnEntry, uint32_t ulBitsToClearOnExit, uint32_t *pulNotificationValue, TickType_t xTicksToWait);
参数:
ulBitsToClearOnEntry:在等待前要清除的通知值中的某些位。
ulBitsToClearOnExit:在退出等待时要清除的通知值中的某些位。
pulNotificationValue:用于存储接收到的通知值。
xTicksToWait:等待通知的时间,单位为时钟节拍。portMAX_DELAY 表示无限期等待。
返回值:
pdPASS:接收到通知。
pdFAIL:超时未收到通知。
使用示例:
uint32_t ulNotificationValue;
xTaskNotifyWait(0, 0, &ulNotificationValue, portMAX_DELAY);
- 使用举例
#include "main.h"
#include "cmsis_os.h"
#include "stdio.h"
TaskHandle_t xTask1Handle, xTask2Handle;
void task1(void *pvParameters);
void task2(void *pvParameters);
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USART1_UART_Init();
xTaskCreate(task1, "Task 1", 128, NULL, 1, &xTask1Handle);
xTaskCreate(task2, "Task 2", 128, NULL, 1, &xTask2Handle);
vTaskStartScheduler();
while (1) {
}
}
void task1(void *pvParameters)
{
while (1) {
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
printf("Task 1: Received notification from Task 2\r\n");
}
}
void task2(void *pvParameters)
{
while (1) {
printf("Task 2: Performing some work\r\n");
vTaskDelay(2000 / portTICK_PERIOD_MS);
xTaskNotifyGive(xTask1Handle);
}
}