互斥:同一时刻,共享资源只能被一个任务所使用。
同步:控制不同任务访问共享资源的顺序。
其实,就可以把通知理解为同步,它们的作用都是控制任务的执行顺序。
任务之间的通信,称为 IPC(Inner-Process Communication)
FreeRTOS中提供了队列、信号量、事件组、任务通知用于任务间的通信、互斥与同步,还是非常灵活的。
很乱很乱,无法发理解。
数据传递
队列
创建队列
// 动态创建队列
QueueHandle_t xQueueCreate( UBaseType_t uxQueueLength, // 队列长度,最多能存放多少个数据
UBaseType_t uxItemSize); // 一个元素占用的空间(字节数)
// 静态创建队列,静态队列一旦创建无法删除
QueueHandle_t xQueueCreateStatic( UBaseType_t uxQueueLength, // 队列长度,最多能存放多少个数据
UBaseType_t uxItemSize, // 一个元素占用的空间(字节数)
uint8_t *pucQueueStorageBuffer, // 队列存储区域的指针
StaticQueue_t *pxQueueBuffer ); // 队列控制块(像任务控制块那样用于管理队列)动态创建:
QueueHandle_t xQueue = xQueueCreate(10, sizeof(int));
if (xQueue == NULL) {
// 队列创建失败
}静态创建:
static uint8_t ucQueueStorage[5 * sizeof(int)];
static StaticQueue_t xStaticQueue;
QueueHandle_t xQueue;
xQueue = xQueueCreateStatic(5, sizeof(int), ucQueueStorage, &xStaticQueue);
if (xQueue == NULL) {
// 队列创建失败
}删除队列
仅能删除动态方式创建的队列。
void vQueueDelete(QueueHandle_t xQueue);清空队列
不会释放队列的内存,只是清除队列中的所有元素。
BaseType_t xQueueReset(QueueHandle_t xQueue);写队列
(1)写入到队列尾部
BaseType_t xQueueSend( QueueHandle_t xQueue, // 队列句柄
const void *pvItemToQueue, // 数据指针
TickType_t xTicksToWait ); // 超时时间:0 不等待;portMAX_DELAY 无限等待
BaseType_t xQueueSendToBack( QueueHandle_t xQueue,
const void *pvItemToQueue,
TickType_t xTicksToWait );
BaseType_t xQueueOverwrite(QueueHandle_t xQueue, const void *pvItemToQueue);xQueueSend 和 xQueueSendToBack 这俩是一个函数。
xQueueOverwrite 向队列尾部写入数据,如果队列满了,就覆盖最旧的数据(队列头部的数据)。
xQueueOverwrite 适用场景:队列中只需要保留最新的数据,过时的数据可以被覆盖。
(2)写入到队列头部,优先处理紧急数据。
BaseType_t xQueueSendToFront( QueueHandle_t xQueue,
const void *pvItemToQueue,
TickType_t xTicksToWait );读队列
BaseType_t xQueueReceive( QueueHandle_t xQueue, // 队列句柄
void * const pvBuffer, // 接收数据缓冲区
TickType_t xTicksToWait ); // 超时时间:0 不等待;portMAX_DELAY 无限等待中断安全版本:
BaseType_t xQueueReceiveFromISR(
QueueHandle_t xQueue,
void *pvBuffer,
BaseType_t *pxTaskWoken //
)查询队列
队列中还有多少个元素未处理,即队列中元素的个数:
UBaseType_t uxQueueMessagesWaiting(QueueHandle_t xQueue);返回队列的剩余空间:
UBaseType_t uxQueueSpacesAvailable( const QueueHandle_t xQueue );
// 0:满了
// >0:有剩余空间偷看队列
从队列中获取数据,但是不移除它。
BaseType_t xQueuePeek(QueueHandle_t xQueue, void *pvBuffer, TickType_t xTicksToWait);共享资源互斥
互斥量
互斥量专门用于解决共享资源的互斥,同时只能由一个任务获取,并且只能由获取它的任务释放。
互斥量不能在中断程序中释放,因为中断中没有任务上下文,无法参与优先级继承逻辑。
互斥量默认支持优先级继承,不用担心优先级反转的问题。
信号量也能实现互斥,但可能发生优先级反转。
(1)互斥量和临界区函数的区别:
创建互斥量
SemaphoreHandle_t xSemaphoreCreateMutex( void );递归互斥量
中断安全函数:
优先级反转
假设有三个任务:
Task_H(高优先级)
Task_M(中优先级)
Task_L(低优先级)
优先级反转过程:
Task_L 获取了某个共享资源(如互斥量)。
Task_H 就绪,抢占 Task_L,但尝试获取同一资源时被阻塞(因为资源被 Task_L 占用)。
Task_M 就绪(不需要该资源),抢占 Task_L 并执行。
Task_L 因被 Task_M 抢占,无法继续执行并释放资源,导致 Task_H 长期阻塞。
在这个过程中,Task_M 优先级比 Task_H 低,却比 Task_H 先执行,违反了优先级调度原则,称之为优先级反转。
优先级反转,增加了高优先级任务的响应时间,导致系统不能及时处理高优先级任务,系统的实时性变差。
优先级反转解决办法:
(一)优先级继承
当一个低优先级任务持有资源并被中等优先级任务抢占时,其优先级会被暂时提升到高优先级任务的优先级,直到它释放资源为止。
只有互斥量支持优先级继承,且创建时默认开启。
SemaphoreHandle_t xMutex; // 互斥量句柄
void vTask1(void *pvParameters)
{
while (1)
{
// 获取互斥量
if (xSemaphoreTake(xMutex, portMAX_DELAY))
{
// 执行任务操作
printf("Task 1 is accessing shared resource\n");
vTaskDelay(pdMS_TO_TICKS(1000));
xSemaphoreGive(xMutex); // 释放互斥量
}
}
}
void vTask2(void *pvParameters)
{
while (1)
{
if (xSemaphoreTake(xMutex, portMAX_DELAY))
{
// 执行任务操作
printf("Task 2 is accessing shared resource\n");
vTaskDelay(pdMS_TO_TICKS(1000));
xSemaphoreGive(xMutex); // 释放互斥量
}
}
}
int main(void)
{
// 创建互斥量并启用优先级继承
xMutex = xSemaphoreCreateMutex();
if (xMutex != NULL)
{
// 创建任务
xTaskCreate(vTask1, "Task 1", configMINIMAL_STACK_SIZE, NULL, 1, NULL);
xTaskCreate(vTask2, "Task 2", configMINIMAL_STACK_SIZE, NULL, 2, NULL);
// 启动调度器
vTaskStartScheduler();
}
// 如果调度器启动失败,则进入死循环
for (;;);
return 0;
}
(二)优先级天花板
任务在获取资源时,自动提升自身优先级至预设的“天花板优先级”(所有可能访问该资源的任务中的最高优先级)。
xSemaphoreCreateMutexStatic()总结:
优先级反转提醒我们使用锁的代码段应尽量短
如果锁保护的代码段很短,直接使用原子锁忙等也是一个选择
任务同步
信号量
信号量分为计数信号量和二进制信号量,计数信号量值任意,二进制信号量只有0和1两个值。
信号量可用于互斥和同步。
创建信号量
(1)动态创建信号量
// 动态创建二进制信号量,创建后就可take
SemaphoreHandle_t xSemaphoreCreateBinary(void);
// 动态创建计数信号量
SemaphoreHandle_t xSemaphoreCreateCounting(UBaseType_t uxMaxCount, // 资源总数
UBaseType_t uxInitialCount); // 初始计数值中断安全函数:
(2)静态创建信号量
// 静态创建二进制信号量,创建后必须先give才能take
SemaphoreHandle_t xSemaphoreCreateBinaryStatic(StaticSemaphore_t *pxSemaphoreBuffer);
// 静态创建计数信号量
SemaphoreHandle_t xSemaphoreCreateCountingStatic(
UBaseType_t uxMaxCount, // 资源总数
UBaseType_t uxInitialCount, // 初始计数值
StaticSemaphore_t *pxSemaphoreBuffer // 静态信号量的控制块
);
删除信号量
void vSemaphoreDelete(SemaphoreHandle_t xSemaphore);中断安全函数:
P 操作
P操作即获取信号量,表示使用共享资源。
BaseType_t xSemaphoreTake(SemaphoreHandle_t xSemaphore,
TickType_t xTicksToWait);中断安全函数:
BaseType_t xSemaphoreTakeFromISR(SemaphoreHandle_t xSemaphore,
BaseType_t *pxHigherPriorityTaskWoken);V 操作
V操作即释放信号量,表示释放共享资源。
BaseType_t xSemaphoreGive(SemaphoreHandle_t xSemaphore);中断安全函数:
BaseType_t xSemaphoreGiveFromISR( SemaphoreHandle_t xSemaphore, BaseType_t *pxHigherPriorityTaskWoken );信号量应用于通知示例:
SemaphoreHandle_t xSemaphore = xSemaphoreCreateBinary();
void vTask(void *pvParameters)
{
for(;;)
{
if (xSemaphoreTake(xSemaphore, portMAX_DELAY) == pdTRUE)
{
// 被中断通知了,开始工作
}
}
}
void ISR_Handler(void)
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xSemaphoreGiveFromISR(xSemaphore, &xHigherPriorityTaskWoken); // 释放信号量
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
事件组
事件组就是一组二进制位,一位表示一个事件状态。
事件组用于任务间同步:
多个任务可等待同一个事件
一个任务可等待多个事件
#include "event_groups.h"创建事件组
// 动态创建事件组
EventGroupHandle_t xEventGroupCreate( void );
// 静态创建事件组
EventGroupHandle_t xEventGroupCreateStatic( StaticEventGroup_t * pxEventGroupBuffer );
示例:
// 动态创建
EventGroupHandle_t xEventGroup = xEventGroupCreate();
// 静态创建
删除事件组
void vEventGroupDelete( EventGroupHandle_t xEventGroup );设置事件位
定义事件位:
#define BIT_TASK_A (1 << 0)
#define BIT_TASK_B (1 << 1)
#define BIT_TASK_C (1 << 2)
#define ALL_READY_BITS (BIT_TASK_A | BIT_TASK_B | BIT_TASK_C)EventBits_t xEventGroupSetBits( EventGroupHandle_t xEventGroup, // 哪个事件组
const EventBits_t uxBitsToSet ); // 设置哪些位中断安全函数:
BaseType_t xEventGroupSetBitsFromISR( EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToSet,
BaseType_t * pxHigherPriorityTaskWoken );等待事件
EventBits_t xEventGroupWaitBits( EventGroupHandle_t xEventGroup, // 事件组句柄
const EventBits_t uxBitsToWaitFor, // 等待的事件位
const BaseType_t xClearOnExit, // 事件到达后是否自动清除这些位
const BaseType_t xWaitForAllBits, // pdTRUE:所有条件都得成立;pdFALSE:任何一个条件成立就行
TickType_t xTicksToWait); // 超时时间,portMAX_DELAY 永久,pdMS_TO_TICKS(n) 有超时时间
使用示例:
void ledc_task(void* param)
{
EventBits_t ev;
while(1)
{
ev = xEventGroupWaitBits(s_ledc_ev,LEDC_ON_EV|LEDC_OFF_EV,pdTRUE,pdFALSE,pdMS_TO_TICKS(5000));
if(ev)
{
if(ev & LEDC_OFF_EV)
{
}
else if(ev & LEDC_ON_EV)
{
}
}
}
}
中断安全函数:
清除事件位
手动清除事件位
xEventGroupClearBits(xEventGroup, BIT_0 | BIT_1);
事件同步
该函数结合了上面的设置函数和等待函数,用于简化代码编写。
EventBits_t xEventGroupSync( EventGroupHandle_t xEventGroup, // 事件组句柄
const EventBits_t uxBitsToSet, // 要设置的事件位
const EventBits_t uxBitsToWaitFor, // 要等待的事件位
TickType_t xTicksToWait ); // 等待时间xEventGroupSync 可实现复杂的同步,虽然用其它的方法也能实现,但是没这个简单。
(1)多任务协同启动
所有任务就绪后继续执行。
void vTaskA(void *pvParams) {
xEventGroupSync(xEventGroup, TASK_A_READY_BIT, // 任务各自设置
(TASK_A_READY_BIT | TASK_B_READY_BIT | TASK_C_READY_BIT),
portMAX_DELAY);
}(2)分阶段启动
任务A采集数据后设置 DATA_COLLECTED_BIT,任务B处理数据后设置 DATA_PROCESSED_BIT,双方同步推进。
// 阶段1:等待数据采集完成
xEventGroupSync(xEventGroup, DATA_COLLECTED_BIT,
(DATA_COLLECTED_BIT | DATA_PROCESSED_BIT),
pdMS_TO_TICKS(100));
// 阶段2:继续后续操作中断安全函数:
任务通知
指定任务通知
中断中可以发送通知,但不能接收通知。
发送任务通知
// 发送通知,可传输32位整数数据
BaseType_t xTaskNotify(TaskHandle_t xTask, uint32_t ulValue, eNotifyAction eAction);
// 简化版发送通知(类似二进制信号量)
BaseType_t xTaskNotifyGive(TaskHandle_t xTask);
eAction 对值的处理:
中断安全函数:
BaseType_t xTaskNotifyFromISR( TaskHandle_t xTaskToNotify,
uint32_t ulValue,
eNotifyAction eAction,
BaseType_t *pxHigherPriorityTaskWoken );
接收任务通知
// 等待通知并获取数值
BaseType_t xTaskNotifyWait(
uint32_t ulBitsToClearOnEntry, // 阻塞前清除哪些位
uint32_t ulBitsToWaitFor, // 退出后清除哪些位
uint32_t *pulNotificationValue, // 通知值
TickType_t xTicksToWait); // 等待时间
// 仅等待通知,类似于二值信号量
BaseType_t ulTaskNotifyTake()uint32_t notifyValue;
xTaskNotifyWait(
0x00, // 进入前不清,保留事件
0xFFFFFFFF, // 退出后清所有
¬ifyValue,
portMAX_DELAY
);
清除的是内核中tcb中的任务通知的值,原来的值已经拷贝到 notifyValue 中了,所以后面的判断依然有效。
清除任务的通知状态
清除某个任务的通知状态,但不清除通知值。一般用不到。
BaseType_t xTaskNotifyStateClear(TaskHandle_t xTask);
// 传入NULL表示清除当前任务最大的作用,就是确保任务只执行一次。
void vTaskProcess(void *pvParams) {
uint32_t ulNotifiedValue;
while (1) {
xTaskNotifyWait(0x00, 0xFFFFFFFF, &ulNotifiedValue, portMAX_DELAY);
// 处理通知值
if (ulNotifiedValue & 0x01) {
}
if (ulNotifiedValue & 0x02) {
}
// xTaskNotifyStateClear(NULL);
}
}
void vISR_Handler(void) {
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xTaskNotifyFromISR(xTaskProcessHandle, 0x03, eSetBits, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}使用:仅通知
使用:事件组
#define NOTIFY_UART_RX (1UL << 0)
#define NOTIFY_TIMER (1UL << 1)
#define NOTIFY_KEY (1UL << 2)
xTaskNotify(AppTaskHandle,
NOTIFY_UART_RX,
eSetBits);
相当于 taskNotifyValue |= ulBitsToSet; 把指定位置1
void AppTask(void *arg)
{
uint32_t notify;
for (;;) {
xTaskNotifyWait(
0,
0xFFFFFFFF,
¬ify,
portMAX_DELAY
);
if (notify & NOTIFY_UART_RX) {
Uart_Process();
}
if (notify & NOTIFY_TIMER) {
Timer_Process();
}
if (notify & NOTIFY_KEY) {
Key_Process();
}
}
}