任务间通信

任务间通信

  1. 任务间通信是需要通过某些机制进行数据交换和同步,常见的任务间通信有以下几种方式
    - 共享内存:读写效率高,但是需要轮询访问
    - 消息队列:用于任务之间的数据传递。
    - 信号量(二值信号量、计数信号量):用于任务间的同步或资源管理。
    - 互斥锁:用于保护共享资源,防止任务竞争。
    - 任务通知:轻量级的任务间通信,用于信号量或事件同步的替代方案。
  2. 多个任务可以共享系统中的一些资源(如全局变量、内存、硬件外设等),这些资源对所有任务都是可见的。但如果多个任务同时访问同一资源,可能会出现竞态条件,因此需要通过同步机制(如互斥锁、信号量等)来确保共享资源的正确访问。

一、消息队列

  1. 在FreeRTOS中,消息队列(Queue)是一种任务间通信的主要机制。它允许一个任务或中断将消息发送到队列中,另一个任务可以从该队列中读取并处理消息。
  2. 使用场景:任务间通信,任务与中断的通信
1.xQueueCreate() 创建消息队列
原型:
	QueueHandle_t xQueueCreate(UBaseType_t uxQueueLength, UBaseType_t uxItemSize);
参数:
	uxQueueLength:队列的长度,即队列可以容纳的消息数量。
	uxItemSize:每条消息的大小(字节数)。
返回值:
	返回一个 QueueHandle_t,即队列句柄。如果返回值为 NULL,表示队列创建失败。

使用范例
	QueueHandle_t xQueue;
	xQueue = xQueueCreate(10, sizeof(int));  // 创建一个可以存储10个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.消息队列使用举例
//功能: 定时器TIM5向队列中发送数据,任务通过队列接收,并通过USART1打印出来

#include "main.h"
#include "cmsis_os.h"
#include "stdio.h"

// 定义定时器和UART句柄
TIM_HandleTypeDef htim5;
UART_HandleTypeDef huart1;

// 定义RTOS任务句柄
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);

// 重定向标准输出函数,用于printf重定向到UART1
int __io_putchar(int ch)
{
	UBaseType_t vxx;
	vxx = portSET_INTERRUPT_MASK_FROM_ISR();  // 禁止中断
	HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 1);  // 通过UART发送字符
	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};  // 初始化时间为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) {
		// 从消息队列中接收时间数据,超时时间为2000ms
		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)
{
	// MCU初始化和系统配置
	HAL_Init();
	SystemClock_Config();
	MX_GPIO_Init();
	MX_TIM5_Init();
	MX_USART1_UART_Init();

	// 创建一个长度为3的消息队列,用于存储timex结构
	timeQueueHandle = xQueueCreate(3, sizeof(struct timex));
	if (timeQueueHandle == NULL) {
		// 如果消息队列创建失败,打印错误信息并返回
		printf("xQueueCreate err\r\n");
		return -34;
	}

	// 启动定时器5的中断模式
	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;
	}

	// 启动FreeRTOS调度器
	osKernelStart();

	// 正常情况下,程序不会到达这里,因为调度器接管了控制权
	while (1) {
		// 主循环,不执行任何操作
	}
}


二、互斥锁

  1. 在FreeRTOS中,互斥锁是一种专门为任务同步设计的机制,确保在任何时刻,只有一个任务能够访问被互斥锁保护的共享资源。
  2. 使用场景:共享资源访问、临界区保护、任务同步

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) {
    // 成功获取锁,执行共享资源的访问操作
}
  1. 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);

    // 启动RTOS调度器
    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);
    }
}

三、二值信号量

  1. 与互斥有很多相同点
  2. 基本操作
//与互斥锁的参数都相同
xSemaphoreCreateBinary(): 创建二值信号量。
xSemaphoreTake(): 获取二值信号量(等待信号量可用)。
xSemaphoreGive(): 释放二值信号量(释放信号)。
  1. 与互斥锁的区别
  • 优先级反转问题:二值信号量没有优先级继承机制,因此可能出现优先级反转问题,而互斥锁提供了优先级继承机制来避免这个问题。
  • 目的不同:二值信号量用于任务或中断之间的同步,而互斥锁主要用于保护共享资源。
  • 行为不同:二值信号量在释放后,任何一个等待的任务都可以获取,而互斥锁则是用来保护任务之间对共享资源的独占访问。
  1. 代码示例
// 一个任务等待另一个任务释放信号量,以此来进行同步。
#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) {
            // 成功获取信号量,执行任务1的操作
            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");

        // 释放信号量,通知任务1
        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.任务通知

  1. 任务通知(Task Notification)是FreeRTOS中用于任务间通信和同步的一种轻量级机制。与信号量、队列等其他同步工具相比,任务通知的效率更高,且占用更少的内存资源。
  2. 每个任务在FreeRTOS中都有一个专用的通知值,它可以用来发送简单的通知或传递小量数据。
基本概念
  • 通知值:每个任务在FreeRTOS中都有一个与之关联的32位的通知值,这个值可以由其他任务或中断服务程序(ISR)设置或修改。
  • 通知行为:一个任务可以等待它的通知值被设置。通知值可以表示一个事件(通知发生)或是传递实际的数据。
  • 同步和通信:任务通知可以用于实现任务间的同步和事件传递,例如某个任务执行完成后通知另一个任务继续执行,或者中断通知任务处理外部事件。
常用函数
  1. xTaskNotifyGive()
用于从任务或中断中向另一个任务发送通知。它的功能类似于信号量的“给”,可以唤醒目标任务。
函数原型:
	void xTaskNotifyGive(TaskHandle_t xTaskToNotify);
使用示例:
	xTaskNotifyGive(xTaskHandle);  // 向任务 xTaskHandle 发送通知
  1. ulTaskNotifyTake()
用于任务等待通知。任务在调用该函数时,会进入阻塞状态,直到收到通知或者超时。
函数原型:
	uint32_t ulTaskNotifyTake(BaseType_t xClearCountOnExit, TickType_t xTicksToWait);
参数:
	xClearCountOnExit:如果为 pdTRUE,在接收到通知时会将通知计数清零。只处理一次
	xClearCountOnExit:当 pdFALSE 作为参数传入时,通知计数在任务处理后不会被清零(val--)。
						这意味着如果任务在等待期间接收了多次通知,每次调用 ulTaskNotifyTake() 后,通知计数会递减,直到处理完所有通知为止。
						这种情况适用于任务需要处理每个通知,而不是只处理一次通知。
	xTicksToWait:等待通知的时间,单位为时钟节拍。可以设置为 portMAX_DELAY 来表示无限期等待。
返回值:
	返回收到通知的计数。
使用示例:
	ulTaskNotifyTake(pdTRUE, portMAX_DELAY);  // 等待通知,接收到后计数清零
  1. 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);  // 设置任务通知值的某些位


  1. 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);  // 等待通知并读取通知值
  1. 使用举例
//task1 等待 task2 发出通知后继续执行
#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);
        // 给任务1发送通知
        xTaskNotifyGive(xTask1Handle);
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值