当前位置: 首页 > news >正文

STM32-FreeRTOS快速入门指南(中)

第七章 FreeRTOS队列

1. 队列简介

队列是一种任务到任务、任务到中断、中断到任务数据交流的一种机制。在队列中可以存储数量有限、大小固定的多个数据,队列中的每一个数据叫做队列项目,队列能够存储队列项目的最大数量称为队列的长度,在创建队列的时候,就需要指定所创建队列的长度及队列项目的大小。因为队列是用来在任务与任务或任务于中断之间传递消息的一种机制,因此队列也叫做消息队列。

基于队列, FreeRTOS 实现了多种功能,其中包括队列集、互斥信号量、计数型信号量、二值信号量、 递归互斥信号量,因此很有必要深入了解 FreeRTOS 的队列。

  1. 数据存储

队列通常采用 FIFO(先进先出)的存储缓冲机制,当有新的数据被写入队列中时,永远都是写入到队列的尾部,而从队列中读取数据时,永远都是读取队列的头部数据。但同时 FreeRTOS的队列也支持将数据写入到队列的头部, 并且还可以指定是否覆盖先前已经在队列头部的数据。

  1. 多任务访问

队列不属于某个特定的任务,可以在任何的任务或中断中往队列中写入消息,或者从队列中读取消息。

  1. 队列读取阻塞

在任务从队列读取消息时,可以指定一个阻塞超时时间。如果任务在读取队列时,队列为空,这时任务将被根据指定的阻塞超时时间添加到阻塞态任务列表中进行阻塞,以等待队列中有可用的消息。当有其他任务或中断将消息写入队列中, 因等待队列而阻塞任务将会被添加到就绪态任务列表中,并读取队列中可用的消息。如果任务因等待队列而阻塞的时间超过指定的阻塞超时时间,那么任务也将自动被转移到就绪态任务列表中,但不再读取队列中的数据。

因为同一个队列可以被多个任务读取,因此可能会有多个任务因等待同一个队列,而被阻塞,在这种情况下,如果队列中有可用的消息,那么也只有一个任务会被解除阻塞并读取到消息,并且会按照阻塞的先后和任务的优先级,决定应该解除哪一个队列读取阻塞任务。

  1. 队列写入阻塞

与队列读取一样,在任务往队列写入消息时,也可以指定一个阻塞超时时间。如果任务在写入队列时,队列已经满了,这时任务将被根据指定的阻塞超时时间添加到阻塞态任务列表中进行阻塞,以等待队列有空闲的位置可以写入消息。指定的阻塞超时时间为任务阻塞的最大时间,如果在阻塞超时时间到达之前,队列有空闲的位置,那么队列写入阻塞任务将会解除阻塞,并往队列中写入消息,如果达到指定的阻塞超时时间,队列依旧没有空闲的位置写入消息,那么队列写入阻塞任务将会自动转移到就绪态任务列表中,但不会往队列中写入消息。

因为同一个队列可以被多个任务写入, 因此可有会有多个任务因等待统一个任务,而被阻塞,在这种情况下,如果队列中有空闲的位置,那么也之后一个任务会被解除阻塞并往队列中写入消息,并且会按照阻塞的先后和任务的优先级,决定应该解除哪一个队列写入阻塞任务。

  1. 队列操作

下面简单介绍一下队列操作的过程,包括创建队列、往队列中写入消息、从队列中读取消息等操作。

1.1 创建队列

在这里插入图片描述

创建了一个用于任务 A 与任务 B 之间“沟通交流”的队列,这个队列最大可容纳 5 个队列项目,即队列的长度为 5。刚创建的队列是不包含内容的,因此这个队列为空。

1.2 往队列写入第一个消息

在这里插入图片描述

任务 A 将一个私有变量写入队列的尾部。由于在写入队列之前,队列是空的,因此新写入的消息,既是是队列的头部,也是队列的尾部。

1.3 往队列中写入第二个消息

在这里插入图片描述

任务 A 改变了私有变量的值,并将新值写入队列。现在队列中包含了队列 A写入的两个值,其中第一个写入的值在队列的头部,而新写入的值在队列的尾部。 这时队列还有 3 个空闲的位置。

1.4 从队列读取第一个消息

在这里插入图片描述

任务 B 从队列中读取消息,任务 B 读取的消息是处于队列头部的消息,这是任务 A 第一次往队列中写入的消息。在任务 B 从队列中读取消息后,队列中任务 A 第二次写入的消息,变成了队列的头部,因此下次任务 B 再次读取消息时,将读取到这个消息。此时队列中剩余 4 个空闲的位置。

2. FreeRTOS队列相关API函数

2.1 创建队列

函数描述
xQueueCreate()动态方式创建队列
xQueueCreateStatic()静态方式创建队列
2.1.1 动态方式创建队列

xQueueCreate() 函数用于动态地创建 FreeRTOS 队列。这意味着队列所需的内存(包括队列结构体和存储数据的缓冲区)将从 FreeRTOS 堆中分配。

函数原型:

QueueHandle_t xQueueCreate( UBaseType_t uxQueueLength, UBaseType_t uxItemSize );

参数说明:

  • uxQueueLength: 队列可以存储的最大项目数量。
  • uxItemSize: 队列中每个项目的大小,以字节为单位。如果队列用于存储指针,则将其设置为 sizeof( Type_t * ),其中 Type_t 是指针指向的数据类型。

返回值:

  • QueueHandle_t: 如果队列成功创建,则返回一个队列句柄(QueueHandle_t 类型),该句柄用于后续的队列操作。
  • NULL: 如果由于内存不足而无法创建队列,则返回 NULL

注意事项:

  • 要使此函数可用,configSUPPORT_DYNAMIC_ALLOCATION 必须在 FreeRTOSConfig.h 中定义为 1。
  • 使用 xQueueCreate() 创建的队列在不再使用时应该通过 vQueueDelete() 函数进行删除,以释放占用的内存,防止内存泄漏。
  • 队列可以存储固定大小的项目(通过值传递),也可以存储指针(通过引用传递)。当存储指针时,用户需要自己管理指针指向的数据的生命周期。
2.1.2 静态方式创建队列

xQueueCreateStatic() 函数用于静态地创建 FreeRTOS 队列。这意味着队列结构体和数据存储缓冲区所需的内存由用户在编译时或运行时提供,而不是从 FreeRTOS 堆中动态分配。这对于对内存管理有严格要求、需要确定性行为或禁止使用动态内存分配的系统非常有用。

函数原型:

QueueHandle_t xQueueCreateStatic( UBaseType_t uxQueueLength,UBaseType_t uxItemSize,uint8_t *pucQueueStorageBuffer,StaticQueue_t *pxQueueBuffer );

参数说明:

  • uxQueueLength: 队列可以存储的最大项目数量。
  • uxItemSize: 队列中每个项目的大小,以字节为单位。
  • pucQueueStorageBuffer: 指向用户提供的 uint8_t 数组的指针,该数组将用作队列的数据存储缓冲区。数组的大小应至少为 ( uxQueueLength * uxItemSize ) 字节。
  • pxQueueBuffer: 指向用户提供的 StaticQueue_t 结构体变量的指针,该结构体将用作队列的控制块。

返回值:

  • QueueHandle_t: 如果队列成功创建,则返回一个队列句柄。
  • NULL: 如果 pucQueueStorageBufferpxQueueBufferNULL,则返回 NULL

注意事项:

  • 要使此函数可用,configSUPPORT_STATIC_ALLOCATION 必须在 FreeRTOSConfig.h 中定义为 1。
  • 用户需要自行管理 pucQueueStorageBufferpxQueueBuffer 所指向的内存生命周期。这些内存通常定义为全局变量或静态变量,以确保它们在队列的整个生命周期内都有效。
  • 静态创建的队列不需要调用 vQueueDelete() 来释放内存,因为内存是由应用程序静态分配的。

2.2 队列写入消息

函数描述
xQueueSend()往队列的尾部写入消息
xQueueSendToBack()xQueueSend()
xQueueSendToFront()往队列的头部写入消息
xQueueOverwrite()覆盖写入队列消息 (只用于队列长度为 1 的情况)
xQueueSendFromISR()在中断中往队列的尾部写入消息
xQueueSendToBackFromISR()xQueueSendFromISR()
xQueueSendToFrontFromISR()在中断中往队列的头部写入消息
xQueueOverwriteFromISR()在中断中覆盖写入队列消息 (只用于队列长度为 1 的情况)
2.2.1 任务上下文中的队列写入函数

这些函数可以在任务中调用,并且可以在队列满时选择阻塞任务等待。

  • xQueueSend()

    • 描述: 往队列的尾部(末端)写入一个消息。
    • 函数原型: BaseType_t xQueueSend( QueueHandle_t xQueue, const void *pvItemToQueue, TickType_t xTicksToWait );
    • 参数:
      • xQueue: 目标队列的句柄。
      • pvItemToQueue: 指向要发送到队列的数据的指针。该数据将被复制到队列中。
      • xTicksToWait: 如果队列已满,任务愿意阻塞等待的节拍数。设置为 portMAX_DELAY 表示无限期等待,设置为 0 表示不等待。
    • 返回值:
      • pdPASS: 消息成功发送到队列。
      • errQUEUE_FULL: 队列已满,且等待时间已到或为 0 未等待。
    • 注意事项: 这是最常用的发送函数。
  • xQueueSendToBack()

    • 描述: 功能与 xQueueSend() 完全相同,也是往队列的尾部写入消息。这是一个为了提高代码可读性而提供的别名。
    • 函数原型: BaseType_t xQueueSendToBack( QueueHandle_t xQueue, const void *pvItemToQueue, TickType_t xTicksToWait );
    • 参数、返回值及注意事项xQueueSend() 相同。
  • xQueueSendToFront()

    • 描述: 往队列的头部(前端)写入一个消息。这使得新发送的消息成为下一个被读取的消息。
    • 函数原型: BaseType_t xQueueSendToFront( QueueHandle_t xQueue, const void *pvItemToQueue, TickType_t xTicksToWait );
    • 参数、返回值及注意事项xQueueSend() 类似,只是消息被放置在队列的头部。
  • xQueueOverwrite()

    • 描述: 仅用于长度为 1 的队列(即邮箱)。如果队列已满,新消息将覆盖旧消息。如果队列为空,则直接写入。此函数不会导致任务阻塞。
    • 函数原型: BaseType_t xQueueOverwrite( QueueHandle_t xQueue, const void *pvItemToQueue );
    • 参数:
      • xQueue: 目标队列的句柄。
      • pvItemToQueue: 指向要发送到队列的数据的指针。
    • 返回值:
      • pdPASS: 消息成功发送或覆盖。
    • 注意事项: 通常用于实现“最新值”机制,例如传感器数据的最新读取值。
2.2.2 中断服务例程 (ISR) 中的队列写入函数

这些函数是任务上下文函数的“FromISR”版本,它们可以在 ISR 中安全地调用。它们不会阻塞,而是通过 pxHigherPriorityTaskWoken 参数指示是否需要进行上下文切换。

  • xQueueSendFromISR()

    • 描述: 在中断中往队列的尾部写入一个消息。
    • 函数原型: BaseType_t xQueueSendFromISR( QueueHandle_t xQueue, const void *pvItemToQueue, BaseType_t *pxHigherPriorityTaskWoken );
    • 参数:
      • xQueue: 目标队列的句柄。
      • pvItemToQueue: 指向要发送到队列的数据的指针。
      • pxHigherPriorityTaskWoken: 指向 BaseType_t 变量的指针。此变量应初始化为 pdFALSE。如果调用此函数导致更高优先级的任务解除阻塞(即需要进行上下文切换),则该变量将被设置为 pdTRUE
    • 返回值:
      • pdPASS: 消息成功发送到队列。
      • errQUEUE_FULL: 队列已满。
    • 注意事项: 在 ISR 结束时,应检查 *pxHigherPriorityTaskWoken 的值。如果为 pdTRUE,则调用 portYIELD_FROM_ISR()(或类似的宏)来触发上下文切换。
  • xQueueSendToBackFromISR()

    • 描述: 功能与 xQueueSendFromISR() 完全相同,也是在中断中往队列的尾部写入消息。
    • 函数原型: BaseType_t xQueueSendToBackFromISR( QueueHandle_t xQueue, const void *pvItemToQueue, BaseType_t *pxHigherPriorityTaskWoken );
    • 参数、返回值及注意事项xQueueSendFromISR() 相同。
  • xQueueSendToFrontFromISR()

    • 描述: 在中断中往队列的头部写入一个消息。
    • 函数原型: BaseType_t xQueueSendToFrontFromISR( QueueHandle_t xQueue, const void *pvItemToQueue, BaseType_t *pxHigherPriorityTaskWoken );
    • 参数、返回值及注意事项xQueueSendFromISR() 类似,只是消息被放置在队列的头部。
  • xQueueOverwriteFromISR()

    • 描述: 仅用于长度为 1 的队列(邮箱),在中断中覆盖写入消息。
    • 函数原型: BaseType_t xQueueOverwriteFromISR( QueueHandle_t xQueue, const void *pvItemToQueue, BaseType_t *pxHigherPriorityTaskWoken );
    • 参数、返回值及注意事项xQueueSendFromISR() 类似,但用于覆盖写入。

2.3 队列读取消息

函数描述
xQueueReceive()从队列头部读取消息,并删除消息
xQueuePeek()从队列头部读取消息
xQueueReceiveFromISR()在中断中从队列头部读取消息,并删除消息
xQueuePeekFromISR()在中断中从队列头部读取消息
2.3.1 任务上下文中的队列读取函数

这些函数可以在任务中调用,并且可以在队列为空时选择阻塞任务等待。

  • xQueueReceive()

    • 描述: 从队列的头部读取一个消息,并将该消息从队列中删除。如果队列为空,调用任务可以选择阻塞,直到有消息可用或超时。
    • 函数原型: BaseType_t xQueueReceive( QueueHandle_t xQueue, void *pvBuffer, TickType_t xTicksToWait );
    • 参数:
      • xQueue: 目标队列的句柄。
      • pvBuffer: 指向一个缓冲区的指针,用于存储从队列中接收到的消息。该缓冲区的大小必须至少与队列创建时指定 uxItemSize 相同。
      • xTicksToWait: 如果队列为空,任务愿意阻塞等待的节拍数。设置为 portMAX_DELAY 表示无限期等待,设置为 0 表示不等待。
    • 返回值:
      • pdPASS: 消息成功从队列接收。
      • errQUEUE_EMPTY: 队列为空,且等待时间已到或为 0 未等待。
    • 注意事项: 这是最常用的队列读取函数。
  • xQueuePeek()

    • 描述: 从队列的头部读取一个消息,但 将其从队列中删除。这意味着消息仍然留在队列中,可以被其他任务或后续调用再次读取。如果队列为空,调用任务可以选择阻塞,直到有消息可用或超时。
    • 函数原型: BaseType_t xQueuePeek( QueueHandle_t xQueue, void *pvBuffer, TickType_t xTicksToWait );
    • 参数、返回值及注意事项xQueueReceive() 类似,但主要区别在于消息不会被删除。
2.3.2 中断服务例程 (ISR) 中的队列读取函数

这些函数是任务上下文函数的“FromISR”版本,它们可以在 ISR 中安全地调用。它们不会阻塞,而是通过 pxHigherPriorityTaskWoken 参数指示是否需要进行上下文切换。

  • xQueueReceiveFromISR()

    • 描述: 在中断中从队列的头部读取一个消息,并将该消息从队列中删除。
    • 函数原型: BaseType_t xQueueReceiveFromISR( QueueHandle_t xQueue, void *pvBuffer, BaseType_t *pxHigherPriorityTaskWoken );
    • 参数:
      • xQueue: 目标队列的句柄。
      • pvBuffer: 指向一个缓冲区的指针,用于存储从队列中接收到的消息。
      • pxHigherPriorityTaskWoken: 指向 BaseType_t 变量的指针。此变量应初始化为 pdFALSE。如果调用此函数导致更高优先级的任务解除阻塞(即需要进行上下文切换),则该变量将被设置为 pdTRUE
    • 返回值:
      • pdPASS: 消息成功从队列接收。
      • pdFALSE: 队列为空。
    • 注意事项: 在 ISR 结束时,应检查 *pxHigherPriorityTaskWoken 的值。如果为 pdTRUE,则调用 portYIELD_FROM_ISR()(或类似的宏)来触发上下文切换。
  • xQueuePeekFromISR()

    • 描述: 在中断中从队列的头部读取一个消息,但 将其从队列中删除。
    • 函数原型: BaseType_t xQueuePeekFromISR( QueueHandle_t xQueue, void *pvBuffer );
    • 参数:
      • xQueue: 目标队列的句柄。
      • pvBuffer: 指向一个缓冲区的指针,用于存储从队列中接收到的消息。
    • 返回值:
      • pdPASS: 消息成功从队列读取。
      • pdFALSE: 队列为空。
    • 注意事项: 此函数不会影响任务的阻塞状态,因此不需要 pxHigherPriorityTaskWoken 参数。

3. FreeRTOS队列操作测试

3.1 队列配置

/*----------------队列配置区-----------------*/
QueueHandle_t xQueue; // 队列句柄
#define QUEUE_LENGTH 1 // 队列长度
#define QUEUE_ITEM_SIZE sizeof(uint8_t) // 队列中每条信息的大小
/*---------------------------------------------*/

3.2 任务配置

/*----------------任务配置区-----------------*/
// START_TASK配置
#define START_TASK_PRIO 1           // 任务优先级 
#define START_STK_SIZE  128         // 任务堆栈大小 
TaskHandle_t StartTask_Handler;     // 任务句柄 
void start_task(void *pvParameters);// 任务函数 // TASK1配置
#define TASK1_PRIO      2                   
#define TASK1_STK_SIZE  128                 
TaskHandle_t Task1Task_Handler;          
void task1(void *pvParameters);// TASK2配置
#define TASK2_PRIO      3                   
#define TASK2_STK_SIZE  128                 
TaskHandle_t Task2Task_Handler;          
void task2(void *pvParameters);
/*---------------------------------------------*/

3.3 任务实现

/*------------------任务实现区------------------*/
void freertos_demo(void)
{lcd_show_string(10, 47, 220, 24, 24, "Message Queue", RED);lcd_draw_rectangle(5,130,234,314,BLACK);// 创建START_TASK任务xTaskCreate((TaskFunction_t)start_task,        // 任务函数(const char*)"start_task",         // 任务名称(uint16_t)START_STK_SIZE,          // 任务堆栈大小(void*)NULL,                       // 传递给任务函数的参数(UBaseType_t)START_TASK_PRIO,      // 任务优先级(TaskHandle_t*)&StartTask_Handler);// 任务句柄// 开始任务调度vTaskStartScheduler();
}// start_task函数实现
void start_task(void *pvParameters)
{taskENTER_CRITICAL(); // 进入临界区// 创建队列xQueue = xQueueCreate(QUEUE_LENGTH, QUEUE_ITEM_SIZE);// 创建TASK1任务xTaskCreate((TaskFunction_t)task1,  (const char*)"task1",       (uint16_t)TASK1_STK_SIZE,   (void*)NULL,                (UBaseType_t)TASK1_PRIO,    (TaskHandle_t*)&Task1Task_Handler);// 创建TASK2任务xTaskCreate((TaskFunction_t)task2,  (const char*)"task2",       (uint16_t)TASK2_STK_SIZE,   (void*)NULL,                (UBaseType_t)TASK2_PRIO,    (TaskHandle_t*)&Task2Task_Handler);vTaskDelete(StartTask_Handler); // 开始任务已经完成自己使命,删除自己taskEXIT_CRITICAL(); // 退出临界区
}// task1函数实现
void task1(void *pvParameters)
{uint8_t key_value =0;while(1){key_value = key_scan(0);if(key_value != 0){xQueueSend(xQueue, &key_value, portMAX_DELAY); // 发送数据到队列}vTaskDelay(10);}
}// task2函数实现
void task2(void *pvParameters)
{uint8_t queue_recv = 0; // 接收队列数据uint32_t task2_num = 0; // 任务2运行次数while(1){xQueueReceive(xQueue, &queue_recv, portMAX_DELAY); // 接收队列数据switch(queue_recv){case KEY0_PRES:{lcd_fill(6,131,233,313,lcd_discolor[++task2_num%11]);break;}case KEY1_PRES:{LED0_TOGGLE();break;}default:break;}}
}
/*---------------------------------------------*/

3.4 主函数

#include "sys.h"
#include "delay.h"
#include "usart.h"
#include "led.h"
#include "mpu.h"
#include "lcd.h"
#include "key.h"
#include "malloc.h"
#include "freertos_demo.h"int main(void)
{sys_cache_enable();                 /* 打开L1-Cache */HAL_Init();                         /* 初始化HAL库 */sys_stm32_clock_init(240, 2, 2, 4); /* 设置时钟, 480Mhz */delay_init(480);                    /* 延时初始化 */usart_init(115200);                 /* 串口初始化为115200 */led_init();                         /* 初始化LED */mpu_memory_protection();            /* 保护相关存储区域 */lcd_init();                         /* 初始化LCD */key_init();                         /* 初始化按键 */my_mem_init(SRAMIN);                /* 初始化内部内存池(AXI) */freertos_demo();                    /* 运行FreeRTOS例程 */
}

第八章 FreeRTOS队列集

1. 队列集简介

在使用队列进行任务之间的“沟通交流”时,一个队列只允许任务间传递的消息为同一种数据类型,如果需要在任务间传递不同数据类型的消息时,那么就可以使用队列集。 FreeRTOS提供的队列集功能可以对多个队列进行“监听”,只要被监听的队列中有一个队列有有效的消息,那么队列集的读取任务都可以读取到消息,如果读取任务因读取队列集而被阻塞,那么队列集将解除读取任务的阻塞。使用队列集的好处在于,队列集可以是的任务可以读取多个队列中的消息,而无需遍历所有待读取的队列,以确定具体读取哪一个队列。

使用队列集功能,需要在 FreeRTOSConfig.h 文件中将配置项 configUSE_QUEUE_SETS 配置为 1,来启用队列集功能。

2. FreeRTOS队列集相关函数

xQueueCreateSet()创建队列集
xQueueAddToSet()队列添加到队列集中
xQueueRemoveFromSet()从队列集中移除队列
xQueueSelectFromSet()获取队列集中有有效消息的队列
xQueueSelectFromSetFromISR()在中断中获取队列集中有有效消息的队列

2.1 创建队列集

xQueueCreateSet() 函数用于创建一个新的 FreeRTOS 队列集。队列集本身不存储数据,它是一个“容器”,可以包含多个队列和/或信号量。

函数原型:

QueueSetHandle_t xQueueCreateSet( const UBaseType_t uxEventQueueLength );

参数说明:

  • uxEventQueueLength: 队列集中可以同时处于“可用”状态(即有数据可读或可以被获取)的事件(队列或信号量)的最大数量。这个参数指定了队列集内部用于管理事件的列表长度。

返回值:

  • QueueSetHandle_t: 如果队列集成功创建,则返回一个队列集句柄(QueueSetHandle_t 类型),该句柄用于后续的队列集操作。
  • NULL: 如果由于内存不足而无法创建队列集,则返回 NULL

注意事项:

  • 要使此函数可用,configUSE_QUEUE_SETS 必须在 FreeRTOSConfig.h 中定义为 1。
  • 队列集是动态分配的,因此需要有可用的 FreeRTOS 堆内存。
  • 创建队列集后,需要使用 xQueueAddToSet() 将队列或信号量添加到其中。

2.2 队列添加到队列集中

xQueueAddToSet() 函数用于将一个队列或信号量添加到已创建的队列集中。一旦添加到队列集,该队列或信号量将成为队列集的一部分,其事件(消息或信号量可用)将可以通过队列集进行监测。

函数原型:

BaseType_t xQueueAddToSet( QueueSetMemberHandle_t xQueueOrSemaphore, QueueSetHandle_t xQueueSet );

参数说明:

  • xQueueOrSemaphore: 要添加到队列集中的队列句柄(QueueHandle_t)或信号量句柄(SemaphoreHandle_t)。由于 QueueHandle_tSemaphoreHandle_t 都是 QueueSetMemberHandle_t 的别名,因此可以直接传入。
  • xQueueSet: 目标队列集的句柄。

返回值:

  • pdPASS: 成功将队列或信号量添加到队列集。
  • pdFAIL: 队列或信号量已在队列集中,或者队列集已满。

注意事项:

  • 要使此函数可用,configUSE_QUEUE_SETS 必须在 FreeRTOSConfig.h 中定义为 1。
  • 一个队列或信号量只能被添加到 一个 队列集中。
  • 在将队列或信号量添加到队列集之后,对该队列或信号量的直接接收/获取函数(例如 xQueueReceive()xSemaphoreTake())将不再正常工作,而应该通过队列集的相关函数来获取数据。

2.3 从队列集中移除队列

xQueueRemoveFromSet() 函数用于将一个队列或信号量从队列集中移除。移除后,可以再次使用其直接的接收/获取函数。

函数原型:

BaseType_t xQueueRemoveFromSet( QueueSetMemberHandle_t xQueueOrSemaphore, QueueSetHandle_t xQueueSet );

参数说明:

  • xQueueOrSemaphore: 要从队列集中移除的队列句柄或信号量句柄。
  • xQueueSet: 目标队列集的句柄。

返回值:

  • pdPASS: 成功从队列集中移除队列或信号量。
  • pdFAIL: 队列或信号量不在队列集中。

注意事项:

  • 要使此函数可用,configUSE_QUEUE_SETS 必须在 FreeRTOSConfig.h 中定义为 1。

2.4 获取队列集中有效消息的队列

xQueueSelectFromSet() 函数用于阻塞任务,直到队列集中有任何成员(队列或信号量)变为“可用”状态。当有成员可用时,该函数返回该可用成员的句柄。

函数原型:

QueueSetMemberHandle_t xQueueSelectFromSet( QueueSetHandle_t xQueueSet, TickType_t xTicksToWait );

参数说明:

  • xQueueSet: 要监测的队列集句柄。
  • xTicksToWait: 如果队列集没有可用成员,任务愿意阻塞等待的节拍数。设置为 portMAX_DELAY 表示无限期等待,设置为 0 表示不等待。

返回值:

  • QueueSetMemberHandle_t: 如果有成员可用,则返回该成员(队列或信号量)的句柄。
  • NULL: 如果等待时间已到且没有成员可用。

注意事项:

  • 要使此函数可用,configUSE_QUEUE_SETS 必须在 FreeRTOSConfig.h 中定义为 1。
  • 一旦 xQueueSelectFromSet() 返回一个句柄,任务就可以对该句柄进行相应的读取操作(例如 xQueueReceive()xSemaphoreTake()

2.5 在中断中获取队列集中有有效消息的队列

xQueueSelectFromSetFromISR() 函数是 xQueueSelectFromSet() 的中断安全版本,用于在 ISR 中查询队列集中是否有可用成员。

函数原型:

QueueSetMemberHandle_t xQueueSelectFromSetFromISR( QueueSetHandle_t xQueueSet );

参数说明:

  • xQueueSet: 要查询的队列集句柄。

返回值:

  • QueueSetMemberHandle_t: 如果有成员可用,则返回该成员的句柄。
  • NULL: 如果没有成员可用。

注意事项:

  • 要使此函数可用,configUSE_QUEUE_SETS 必须在 FreeRTOSConfig.h 中定义为 1。
  • 此函数不会阻塞。如果返回非 NULL 句柄,则表示对应的队列或信号量中有数据,但 不会 自动执行上下文切换。如果需要唤醒等待该队列集的任务,通常需要在 ISR 外部手动处理或通过其他中断安全机制。

3. 队列集操作测试

3.1 队列配置

/*----------------队列配置区-----------------*/
static QueueSetHandle_t xQueueSet; // 队列集
QueueHandle_t xQueuel; // 队列1
SemaphoreHandle_t xSemaphore; // 二值信号量
#define QUEUE_LENGTH    1 // 队列长度
#define QUEUE_ITEM_SIZE sizeof(uint32_t) // 每条信息大小
#define SEMAPHORE_BINARY_LENGTH  1 // 二值信号量有效长度
#define QUEUESET_LENGTH (QUEUE_LENGTH+SEMAPHORE_BINARY_LENGTH) // 队列集长度
/*---------------------------------------------*/

3.2 任务配置

/*----------------任务配置区-----------------*/
// START_TASK配置
#define START_TASK_PRIO 1           // 任务优先级 
#define START_STK_SIZE  128         // 任务堆栈大小 
TaskHandle_t StartTask_Handler;     // 任务句柄 
void start_task(void *pvParameters);// 任务函数 // TASK1配置
#define TASK1_PRIO      2                   
#define TASK1_STK_SIZE  128                 
TaskHandle_t Task1Task_Handler;          
void task1(void *pvParameters);// TASK2配置
#define TASK2_PRIO      3                   
#define TASK2_STK_SIZE  128                 
TaskHandle_t Task2Task_Handler;          
void task2(void *pvParameters);
/*---------------------------------------------*/

3.3 任务实现

/*------------------任务实现区------------------*/
void freertos_demo(void)
{lcd_show_string(10, 47, 220, 24, 24, "Queue Set", RED);// 创建START_TASK任务xTaskCreate((TaskFunction_t)start_task,        // 任务函数(const char*)"start_task",         // 任务名称(uint16_t)START_STK_SIZE,          // 任务堆栈大小(void*)NULL,                       // 传递给任务函数的参数(UBaseType_t)START_TASK_PRIO,      // 任务优先级(TaskHandle_t*)&StartTask_Handler);// 任务句柄// 开始任务调度vTaskStartScheduler();
}// start_task函数实现
void start_task(void *pvParameters)
{taskENTER_CRITICAL(); // 进入临界区// 创建队列集xQueueSet = xQueueCreateSet(QUEUESET_LENGTH);// 创建队列1xQueuel = xQueueCreate(QUEUE_LENGTH, QUEUE_ITEM_SIZE);// 创建二值信号量xSemaphore = xSemaphoreCreateBinary();// 将队列1和二值信号量加入队列集xQueueAddToSet(xQueuel, xQueueSet);xQueueAddToSet(xSemaphore, xQueueSet);// 创建TASK1任务xTaskCreate((TaskFunction_t)task1,  (const char*)"task1",       (uint16_t)TASK1_STK_SIZE,   (void*)NULL,                (UBaseType_t)TASK1_PRIO,    (TaskHandle_t*)&Task1Task_Handler);// 创建TASK2任务xTaskCreate((TaskFunction_t)task2,  (const char*)"task2",       (uint16_t)TASK2_STK_SIZE,   (void*)NULL,                (UBaseType_t)TASK2_PRIO,    (TaskHandle_t*)&Task2Task_Handler);vTaskDelete(StartTask_Handler); // 开始任务已经完成自己使命,删除自己taskEXIT_CRITICAL(); // 退出临界区
}// task1函数实现
void task1(void *pvParameters)
{uint32_t key_value = 0;  // 改成 uint32_twhile(1){uint8_t temp = key_scan(0);  // 临时用 uint8_t 接收switch(temp){case KEY0_PRES:key_value = temp;  // 赋值给 32 位变量xQueueSend(xQueuel, &key_value, portMAX_DELAY);break;case KEY1_PRES:xSemaphoreGive(xSemaphore);break;default:break;}vTaskDelay(10);}
}// task2函数实现
void task2(void *pvParameters)
{QueueSetMemberHandle_t activate_member = NULL;uint32_t queue_recv = 0;while(1){activate_member = xQueueSelectFromSet(xQueueSet, portMAX_DELAY); // 等待队列集中的队列接受到消息if(activate_member == xQueuel){xQueueReceive(activate_member, &queue_recv, portMAX_DELAY);printf("recv from queue1:%d\r\n", queue_recv);}else if(activate_member == xSemaphore){xSemaphoreTake(activate_member, portMAX_DELAY); // 等待二值信号量释放printf("recv from semaphore\r\n");}}
}
/*---------------------------------------------*/

3.4 主函数

#include "sys.h"
#include "delay.h"
#include "usart.h"
#include "led.h"
#include "mpu.h"
#include "lcd.h"
#include "key.h"
#include "malloc.h"
#include "freertos_demo.h"int main(void)
{sys_cache_enable();                 /* 打开L1-Cache */HAL_Init();                         /* 初始化HAL库 */sys_stm32_clock_init(240, 2, 2, 4); /* 设置时钟, 480Mhz */delay_init(480);                    /* 延时初始化 */usart_init(115200);                 /* 串口初始化为115200 */led_init();                         /* 初始化LED */mpu_memory_protection();            /* 保护相关存储区域 */lcd_init();                         /* 初始化LCD */key_init();                         /* 初始化按键 */my_mem_init(SRAMIN);                /* 初始化内部内存池(AXI) */freertos_demo();                    /* 运行FreeRTOS例程 */
}

4. 队列集模拟事件标志位

4.1 队列配置

/*----------------队列配置区-----------------*/
static QueueSetHandle_t xQueueSet; // 队列集
QueueHandle_t xQueue1; // 队列1
QueueHandle_t xQueue2; // 队列2
#define QUEUE_LENGTH 1 // 队列支持消息个数
#define QUEUE_ITEM_SIZE sizeof(uint32_t) // 队列消息大小
#define QUEUESET_LENGTH (2*QUEUE_LENGTH) // 队列集支持队列个数
#define EVENTBIT_0 (1<<0) // 事件位
#define EVENTBIT_1 (1<<1) 
#define EVENTBIT_ALL (EVENTBIT_0 | EVENTBIT_1)
/*---------------------------------------------*/

4.2 任务配置

/*----------------任务配置区-----------------*/
// START_TASK配置
#define START_TASK_PRIO 1           // 任务优先级 
#define START_STK_SIZE  128         // 任务堆栈大小 
TaskHandle_t StartTask_Handler;     // 任务句柄 
void start_task(void *pvParameters);// 任务函数 // TASK1配置
#define TASK1_PRIO      2                   
#define TASK1_STK_SIZE  128                 
TaskHandle_t Task1Task_Handler;          
void task1(void *pvParameters);// TASK2配置
#define TASK2_PRIO      3                   
#define TASK2_STK_SIZE  128                 
TaskHandle_t Task2Task_Handler;          
void task2(void *pvParameters);
/*---------------------------------------------*/

4.3 任务实现

// task1函数实现
void task1(void *pvParameters)
{uint32_t key_value =0;uint32_t eventbit_0 = EVENTBIT_0;uint32_t eventbit_1 = EVENTBIT_1;while(1){key_value = key_scan(0);switch(key_value){case KEY0_PRES:{xQueueSend(xQueue1, &eventbit_0, portMAX_DELAY); // 发送事件到队列1break;}case KEY1_PRES:{xQueueSend(xQueue2, &eventbit_1, portMAX_DELAY);break;}default:break;}vTaskDelay(10); }
}// task2函数实现
void task2(void *pvParameters)
{uint32_t event_val = 0;uint32_t event_recv = 0;QueueSetMemberHandle_t activate_member = NULL;uint32_t task2_num = 0;while(1){activate_member = xQueueSelectFromSet(xQueueSet, portMAX_DELAY); // 选择队列集中的一个队列xQueueReceive(activate_member, &event_recv, portMAX_DELAY);event_val |= event_recv; // 合并事件值lcd_show_xnum(182,110,event_val,1,16,0,BLUE);if(event_val == EVENTBIT_ALL) // 事件集中包含所有事件{event_val = 0; // 清空事件集lcd_fill(6, 131, 233, 313, lcd_discolor[++task2_num % 11]);}}
}
/*---------------------------------------------*/lcd_fill(6, 131, 233, 313, lcd_discolor[++task2_num % 11]);}}
}

4.4 主函数

#include "sys.h"
#include "delay.h"
#include "usart.h"
#include "led.h"
#include "mpu.h"
#include "lcd.h"
#include "key.h"
#include "malloc.h"
#include "freertos_demo.h"int main(void)
{sys_cache_enable();                 /* 打开L1-Cache */HAL_Init();                         /* 初始化HAL库 */sys_stm32_clock_init(240, 2, 2, 4); /* 设置时钟, 480Mhz */delay_init(480);                    /* 延时初始化 */usart_init(115200);                 /* 串口初始化为115200 */led_init();                         /* 初始化LED */mpu_memory_protection();            /* 保护相关存储区域 */lcd_init();                         /* 初始化LCD */key_init();                         /* 初始化按键 */my_mem_init(SRAMIN);                /* 初始化内部内存池(AXI) */freertos_demo();                    /* 运行FreeRTOS例程 */
}

第九章 FreeRTOS信号量

1. 信号量简介

信号量是一种解决同步问题的机制,可以实现对共享资源的有序访问。其中,“同步”指的是任务间的同步,即信号量可以使得一个任务等待另一个任务完成某件事情后,才继续执行;而“有序访问”指的是对被多任务或中断访问的共享资源(如全局变量)的管理,当一个任务在访问(读取或写入)一个共享资源时,信号量可以防止其他任务或中断在这期间访问(读取或写入)这个共享资源。

举一个例子,假设某个停车场有 100 个停车位(共享资源),这个 100 个停车位对所有人(访问共享资源的任务或中断)开放。如果有一个人要在这个停车场停车,那么就需要先判断这个停车场是否还有空车位(判断信号量是否有资源), 如果此时停车场正好有空车位(信号量有资源),那么就可以直接将车开入空车位进行停车(获取信号量成功), 如果此时停车场已经没有空车位了(信号量没有资源),那么这个人可以选择不停车(获取信号量失败),也可以选择等待(任务阻塞)其他人将车开出停车场(释放信号量资源), 让后再将车停入空车位。

在上面的这个例子中,空车位的数量相当于信号量的资源数,获取信号量相当于占用了空车位,而释放信号量就相当于让出了占用的空车位。信号量用于管理共享资源的场景相当于对共享资源上了个锁,只有任务成功获取到了锁的钥匙,才能够访问这个共享资源,访问完共享资源后还得归还钥匙,当然钥匙可以不只一把,即信号量可以有多个资源。

2. 二值信号量

前面说过,信号量是基于队列实现的,二值信号量也不例外,二值信号量实际上就是一个队列长度为 1 的队列,在这种情况下,队列就只有空和满两种情况,这不就是二值情况吗? 二值信号量通常用于互斥访问或任务同步, 与互斥信号量比较类似,但是二值信号量有可能会导

致优先级翻转的问题。优先级翻转问题指的是,当一个高优先级任务因获取一个被低优先级任务获取而处于没有资源状态的二值信号量时,这个高优先级的任务将被阻塞,直到低优先级的任务释放二值信号量,而在这之前,如果有一个优先级介于这个高优先级任务和低优先级任务之间的任务就绪,那么这个中等优先级的任务就会抢占低优先级任务的运行, 这么一来,这三个任务中优先级最高的任务反而要最后才运行,这就是二值信号量带来的优先级翻转问题,用户在实际开发中要注意这种问题。

和队列一样,在获取二值信号量的时候,允许设置一个阻塞超时时间,阻塞超时时间是当任务获取二值信号量时,由于二值信号量处于没有资源的状态,而导致任务进入阻塞状态的最大系统时钟节拍数。如果多个任务同时因获取同一个处于没有资源状态的二值信号量而被阻塞,那么在二值信号量有资源的时候,这些阻塞任务中优先级高的任务将优先获得二值信号量的资源并解除阻塞。

二值信号量是最简单的信号量形式,它只有两种状态:可用(1)和不可用(0),通常用于任务同步或互斥访问。

函数描述
xSemaphoreCreateBinary()使用动态方式创建二值信号量
xSemaphoreCreateBinaryStatic()使用静态方式创建二值信号量
xSemaphoreTake()获取信号量
xSemaphoreTakeFromISR()在中断中获取信号量
xSemaphoreGive()释放信号量
xSemaphoreGiveFromISR()在中断中释放信号量
vSemaphoreDelete()删除信号量

2.1 创建二值信号量

  • xSemaphoreCreateBinary()

    • 描述: 使用动态方式创建二值信号量。信号量所需的内存将从 FreeRTOS 堆中分配。创建时,信号量会被初始化为“空”(不可用)状态。
    • 函数原型: SemaphoreHandle_t xSemaphoreCreateBinary( void );
    • 返回值: 如果信号量成功创建,则返回一个信号量句柄(SemaphoreHandle_t 类型);否则返回 NULL
    • 注意事项: 要使此函数可用,configUSE_COUNTING_SEMAPHORESconfigSUPPORT_DYNAMIC_ALLOCATION 都需在 FreeRTOSConfig.h 中定义为 1。
  • xSemaphoreCreateBinaryStatic()

    • 描述: 使用静态方式创建二值信号量。信号量所需的内存由用户在编译时或运行时提供。创建时,信号量会被初始化为“空”(不可用)状态。
    • 函数原型: SemaphoreHandle_t xSemaphoreCreateBinaryStatic( StaticSemaphore_t *pxSemaphoreBuffer );
    • 参数: pxSemaphoreBuffer - 指向用户提供的 StaticSemaphore_t 结构体变量的指针,该结构体将用作信号量的控制块。
    • 返回值: 如果信号量成功创建,则返回一个信号量句柄;否则返回 NULL
    • 注意事项: 要使此函数可用,configUSE_COUNTING_SEMAPHORESconfigSUPPORT_STATIC_ALLOCATION 都需在 FreeRTOSConfig.h 中定义为 1。

2.2 获取信号量

  • xSemaphoreTake()

    • 描述: 尝试获取一个二值信号量。如果信号量可用,则获取成功,信号量变为不可用状态。如果信号量不可用,调用任务将进入阻塞状态(如果指定了等待时间),直到信号量可用或超时。
    • 函数原型: BaseType_t xSemaphoreTake( SemaphoreHandle_t xSemaphore, TickType_t xTicksToWait );
    • 参数:
      • xSemaphore: 要获取的信号量句柄。
      • xTicksToWait: 如果信号量不可用,任务愿意阻塞等待的节拍数。设置为 portMAX_DELAY 表示无限期等待,设置为 0 表示不等待。
    • 返回值:
      • pdPASS: 成功获取信号量。
      • pdFAIL: 信号量不可用,且等待时间已到或为 0 未等待。
    • 注意事项: 用于任务之间的同步(如事件通知)或实现互斥(如保护共享资源)。
  • xSemaphoreTakeFromISR()

    • 描述: 在中断服务例程 (ISR) 中尝试获取一个二值信号量。此函数不会阻塞。
    • 函数原型: BaseType_t xSemaphoreTakeFromISR( SemaphoreHandle_t xSemaphore, BaseType_t *pxHigherPriorityTaskWoken );
    • 参数:
      • xSemaphore: 要获取的信号量句柄。
      • pxHigherPriorityTaskWoken: 指向 BaseType_t 变量的指针。此变量应初始化为 pdFALSE。如果调用此函数导致更高优先级的任务解除阻塞(即需要进行上下文切换),则该变量将被设置为 pdTRUE
    • 返回值:
      • pdPASS: 成功获取信号量。
      • pdFALSE: 信号量不可用。
    • 注意事项: 在 ISR 结束时,应检查 *pxHigherPriorityTaskWoken 的值。如果为 pdTRUE,则调用 portYIELD_FROM_ISR()(或类似的宏)来触发上下文切换。

2.3 释放信号量

  • xSemaphoreGive()

    • 描述: 释放一个二值信号量,使其变为可用状态。如果当前有任务因等待此信号量而阻塞,则优先级最高的阻塞任务将被解除阻塞。
    • 函数原型: BaseType_t xSemaphoreGive( SemaphoreHandle_t xSemaphore );
    • 参数: xSemaphore - 要释放的信号量句柄。
    • 返回值:
      • pdPASS: 成功释放信号量。
      • pdFAIL: 信号量已处于可用状态(通常表示逻辑错误,二值信号量不能被多次释放)。
    • 注意事项: 当用于互斥时,通常由持有信号量的任务释放。当用于同步时,通常由事件发生者释放。
  • xSemaphoreGiveFromISR()

    • 描述: 在中断服务例程 (ISR) 中释放一个二值信号量。
    • 函数原型: BaseType_t xSemaphoreGiveFromISR( SemaphoreHandle_t xSemaphore, BaseType_t *pxHigherPriorityTaskWoken );
    • 参数:
      • xSemaphore: 要释放的信号量句柄。
      • pxHigherPriorityTaskWoken: 指向 BaseType_t 变量的指针。此变量应初始化为 pdFALSE。如果调用此函数导致更高优先级的任务解除阻塞,则该变量将被设置为 pdTRUE
    • 返回值:
      • pdPASS: 成功释放信号量。
      • pdFALSE: 信号量已处于可用状态。
    • 注意事项: 在 ISR 结束时,应检查 *pxHigherPriorityTaskWoken 的值。如果为 pdTRUE,则调用 portYIELD_FROM_ISR()

2.4 删除信号量

  • vSemaphoreDelete()
    • 描述: 删除一个二值信号量,并释放其占用的内存(如果它是动态创建的)。
    • 函数原型: void vSemaphoreDelete( SemaphoreHandle_t xSemaphore );
    • 参数: xSemaphore - 要删除的信号量句柄。
    • 注意事项: 只能删除动态创建的信号量。删除后,不应再使用该句柄。

3. 二值信号量操作测试

3.1 任务配置

/*----------------任务配置区-----------------*/
// START_TASK配置
#define START_TASK_PRIO 1           // 任务优先级 
#define START_STK_SIZE  128         // 任务堆栈大小 
TaskHandle_t StartTask_Handler;     // 任务句柄 
void start_task(void *pvParameters);// 任务函数 // TASK1配置
#define TASK1_PRIO      2                   
#define TASK1_STK_SIZE  128                 
TaskHandle_t Task1Task_Handler;          
void task1(void *pvParameters);// TASK2配置
#define TASK2_PRIO      3                   
#define TASK2_STK_SIZE  128                 
TaskHandle_t Task2Task_Handler;          
void task2(void *pvParameters);SemaphoreHandle_t BinarySema; // 二值信号量
/*---------------------------------------------*/

3.2 任务实现

/*------------------任务实现区------------------*/
void freertos_demo(void)
{lcd_show_string(10,47,220,24,24,"Binary Demo",RED);lcd_draw_rectangle(5, 130, 234, 314, BLACK);// 创建START_TASK任务xTaskCreate((TaskFunction_t)start_task,        // 任务函数(const char*)"start_task",         // 任务名称(uint16_t)START_STK_SIZE,          // 任务堆栈大小(void*)NULL,                       // 传递给任务函数的参数(UBaseType_t)START_TASK_PRIO,      // 任务优先级(TaskHandle_t*)&StartTask_Handler);// 任务句柄// 开始任务调度vTaskStartScheduler();
}// start_task函数实现
void start_task(void *pvParameters)
{taskENTER_CRITICAL(); // 进入临界区// 创建二值信号量BinarySema = xSemaphoreCreateBinary();// 创建TASK1任务xTaskCreate((TaskFunction_t)task1,  (const char*)"task1",       (uint16_t)TASK1_STK_SIZE,   (void*)NULL,                (UBaseType_t)TASK1_PRIO,    (TaskHandle_t*)&Task1Task_Handler);// 创建TASK2任务xTaskCreate((TaskFunction_t)task2,  (const char*)"task2",       (uint16_t)TASK2_STK_SIZE,   (void*)NULL,                (UBaseType_t)TASK2_PRIO,    (TaskHandle_t*)&Task2Task_Handler);vTaskDelete(StartTask_Handler); // 开始任务已经完成自己使命,删除自己taskEXIT_CRITICAL(); // 退出临界区
}// task1函数实现
void task1(void *pvParameters)
{uint8_t key_value = 0;while(1){key_value = key_scan(0);switch(key_value){case KEY0_PRES:{xSemaphoreGive(BinarySema); // 释放二值信号量break;}default:break;}vTaskDelay(10);}
}// task2函数实现
void task2(void *pvParameters)
{uint32_t task2_num = 0;while(1){xSemaphoreTake(BinarySema, portMAX_DELAY); // 获取二值信号量lcd_fill(6, 131, 233, 313, lcd_discolor[++task2_num % 11]);}
}
/*---------------------------------------------*/

3.3 主函数

#include "sys.h"
#include "delay.h"
#include "usart.h"
#include "led.h"
#include "mpu.h"
#include "lcd.h"
#include "key.h"
#include "malloc.h"
#include "freertos_demo.h"int main(void)
{sys_cache_enable();                 /* 打开L1-Cache */HAL_Init();                         /* 初始化HAL库 */sys_stm32_clock_init(240, 2, 2, 4); /* 设置时钟, 480Mhz */delay_init(480);                    /* 延时初始化 */usart_init(115200);                 /* 串口初始化为115200 */led_init();                         /* 初始化LED */mpu_memory_protection();            /* 保护相关存储区域 */lcd_init();                         /* 初始化LCD */key_init();                         /* 初始化按键 */my_mem_init(SRAMIN);                /* 初始化内部内存池(AXI) */freertos_demo();                    /* 运行FreeRTOS例程 */
}

4. 计数型信号量

计数型信号量与二值信号量类似, 二值信号量相当于队列长度为 1 的队列,因此二值信号量只能容纳一个资源,这也是为什么命名为二值信号量,而计数型信号量相当于队列长度大于0 的队列,因此计数型信号量能够容纳多个资源,这是在计数型信号量被创建的时候确定的。计数型信号量通常用于一下两种场合:

  1. 事件计数

在这种场合下,每次事件发生后,在事件处理函数中释放计数型信号量(计数型信号量的资源数加 1),其他等待事件发生的任务获取计数型信号量(计数型信号量的资源数减 1),这么一来等待事件发生的任务就可以在成功获取到计数型信号量之后执行相应的操作。在这种场合下,计数型信号量的资源数一般在创建时设置为 0。

  1. 资源管理

在这种场合下,计数型信号量的资源数代表着共享资源的可用数量,例如前面举例中停车场中的空车位。一个任务想要访问共享资源,就必须先获取这个共享资源的计数型信号量,之后在成功获取了计数型信号量之后,才可以对这个共享资源进行访问操作,当然,在使用完共享资源后也要释放这个共享资源的计数型信号量。在这种场合下,计数型信号量的资源数一般在创建时设置为受其管理的共享资源的最大可用数量。

函数描述
xSemaphoreCreateCounting()使用动态方式创建计数型信号量
xSemaphoreCreateCountingStatic()使用静态方式创建计数型信号量
xSemaphoreTake()获取信号量
xSemaphoreTakeFromISR()在中断中获取信号量
xSemaphoreGive()释放信号量
xSemaphoreGiveFromISR()在中断中释放信号量
vSemaphoreDelete()删除信号量

4.1 创建计数型信号量

  • xSemaphoreCreateCounting()

    • 描述: 使用动态方式创建计数型信号量。信号量所需的内存将从 FreeRTOS 堆中分配。
    • 函数原型: SemaphoreHandle_t xSemaphoreCreateCounting( UBaseType_t uxMaxCount, UBaseType_t uxInitialCount );
    • 参数:
      • uxMaxCount: 信号量的最大计数值。当信号量计数值达到此值时,不能再进行 Give 操作。
      • uxInitialCount: 信号量的初始计数值。
    • 返回值: 如果信号量成功创建,则返回一个信号量句柄(SemaphoreHandle_t 类型);否则返回 NULL
    • 注意事项: 要使此函数可用,configUSE_COUNTING_SEMAPHORESconfigSUPPORT_DYNAMIC_ALLOCATION 都需在 FreeRTOSConfig.h 中定义为 1。
  • xSemaphoreCreateCountingStatic()

    • 描述: 使用静态方式创建计数型信号量。信号量所需的内存由用户在编译时或运行时提供。
    • 函数原型: SemaphoreHandle_t xSemaphoreCreateCountingStatic( UBaseType_t uxMaxCount, UBaseType_t uxInitialCount, StaticSemaphore_t *pxSemaphoreBuffer );
    • 参数:
      • uxMaxCount: 信号量的最大计数值。
      • uxInitialCount: 信号量的初始计数值。
      • pxSemaphoreBuffer: 指向用户提供的 StaticSemaphore_t 结构体变量的指针,该结构体将用作信号量的控制块。
    • 返回值: 如果信号量成功创建,则返回一个信号量句柄;否则返回 NULL
    • 注意事项: 要使此函数可用,configUSE_COUNTING_SEMAPHORESconfigSUPPORT_STATIC_ALLOCATION 都需在 FreeRTOSConfig.h 中定义为 1。用户需要自行管理 pxSemaphoreBuffer 所指向的内存生命周期。

4.2 获取信号量

  • 描述: 尝试获取一个计数型信号量。如果信号量的计数值大于 0,则计数值减 1,获取成功。如果计数值为 0,调用任务将进入阻塞状态(如果指定了等待时间),直到信号量可用或超时。
  • 函数原型: BaseType_t xSemaphoreTake( SemaphoreHandle_t xSemaphore, TickType_t xTicksToWait );
  • 参数:
    • xSemaphore: 要获取的信号量句柄。
    • xTicksToWait: 如果信号量不可用,任务愿意阻塞等待的节拍数。
  • 返回值:
    • pdPASS: 成功获取信号量。
    • pdFAIL: 信号量不可用,且等待时间已到或为 0 未等待。
  • 注意事项: 通常用于当某个资源有多个实例时,任务在访问资源前先获取信号量。

4.3 在中断中获取信号量

  • 描述: 在中断服务例程 (ISR) 中尝试获取一个计数型信号量。此函数不会阻塞。
  • 函数原型: BaseType_t xSemaphoreTakeFromISR( SemaphoreHandle_t xSemaphore, BaseType_t *pxHigherPriorityTaskWoken );
  • 参数:
    • xSemaphore: 要获取的信号量句柄。
    • pxHigherPriorityTaskWoken: 指向 BaseType_t 变量的指针。如果调用此函数导致更高优先级的任务解除阻塞,则该变量将被设置为 pdTRUE
  • 返回值:
    • pdPASS: 成功获取信号量。
    • pdFALSE: 信号量不可用。
  • 注意事项: 在 ISR 结束时,应检查 *pxHigherPriorityTaskWoken 的值。如果为 pdTRUE,则调用 portYIELD_FROM_ISR()

4.4 释放信号量

  • 描述: 释放一个计数型信号量,使其计数值加 1。如果计数值未达到最大值,且当前有任务因等待此信号量而阻塞,则优先级最高的阻塞任务将被解除阻塞。
  • 函数原型: BaseType_t xSemaphoreGive( SemaphoreHandle_t xSemaphore );
  • 参数: xSemaphore - 要释放的信号量句柄。
  • 返回值:
    • pdPASS: 成功释放信号量。
    • pdFAIL: 信号量计数值已达到最大值。
  • 注意事项: 当用于资源管理时,通常在任务使用完资源后释放信号量。

4.5 在中断中释放信号量

  • 描述: 在中断服务例程 (ISR) 中释放一个计数型信号量。
  • 函数原型: BaseType_t xSemaphoreGiveFromISR( SemaphoreHandle_t xSemaphore, BaseType_t *pxHigherPriorityTaskWoken );
  • 参数:
    • xSemaphore: 要释放的信号量句柄。
    • pxHigherPriorityTaskWoken: 指向 BaseType_t 变量的指针。如果调用此函数导致更高优先级的任务解除阻塞,则该变量将被设置为 pdTRUE
  • 返回值:
    • pdPASS: 成功释放信号量。
    • pdFALSE: 信号量计数值已达到最大值。
  • 注意事项: 在 ISR 结束时,应检查 *pxHigherPriorityTaskWoken 的值。如果为 pdTRUE,则调用 portYIELD_FROM_ISR()

4.6 删除信号量

  • 描述: 删除一个计数型信号量,并释放其占用的内存(如果它是动态创建的)。
  • 函数原型: void vSemaphoreDelete( SemaphoreHandle_t xSemaphore );
  • 参数: xSemaphore - 要删除的信号量句柄。
  • 注意事项: 只能删除动态创建的信号量。删除后,不应再使用该句柄。

5. 计数型信号量操作测试

5.1 任务配置

/*----------------任务配置区-----------------*/
// START_TASK配置
#define START_TASK_PRIO 1           // 任务优先级 
#define START_STK_SIZE  128         // 任务堆栈大小 
TaskHandle_t StartTask_Handler;     // 任务句柄 
void start_task(void *pvParameters);// 任务函数 // TASK1配置
#define TASK1_PRIO      2                   
#define TASK1_STK_SIZE  128                 
TaskHandle_t Task1Task_Handler;          
void task1(void *pvParameters);// TASK2配置
#define TASK2_PRIO      3                   
#define TASK2_STK_SIZE  128                 
TaskHandle_t Task2Task_Handler;          
void task2(void *pvParameters);SemaphoreHandle_t CountSemaphore; // 计数信号量
/*---------------------------------------------*/

5.2 任务实现

/*------------------任务实现区------------------*/
void freertos_demo(void)
{lcd_show_string(10,47,220,24,24,"Counting Semaphore",RED);lcd_show_string(54, 111, 200, 16, 16, "Notify Value:  0", BLUE);lcd_draw_rectangle(5, 110, 234, 314, BLACK);lcd_draw_line(5, 130, 234, 130, BLACK);// 创建START_TASK任务xTaskCreate((TaskFunction_t)start_task,        // 任务函数(const char*)"start_task",         // 任务名称(uint16_t)START_STK_SIZE,          // 任务堆栈大小(void*)NULL,                       // 传递给任务函数的参数(UBaseType_t)START_TASK_PRIO,      // 任务优先级(TaskHandle_t*)&StartTask_Handler);// 任务句柄// 开始任务调度vTaskStartScheduler();
}// start_task函数实现
void start_task(void *pvParameters)
{taskENTER_CRITICAL(); // 进入临界区// 创建计数信号量CountSemaphore = xSemaphoreCreateCounting((UBaseType_t)255, (UBaseType_t)0); // 创建计数信号量,初始值为0,最大值为255  // 创建TASK1任务xTaskCreate((TaskFunction_t)task1,  (const char*)"task1",       (uint16_t)TASK1_STK_SIZE,   (void*)NULL,                (UBaseType_t)TASK1_PRIO,    (TaskHandle_t*)&Task1Task_Handler);// 创建TASK2任务xTaskCreate((TaskFunction_t)task2,  (const char*)"task2",       (uint16_t)TASK2_STK_SIZE,   (void*)NULL,                (UBaseType_t)TASK2_PRIO,    (TaskHandle_t*)&Task2Task_Handler);vTaskDelete(StartTask_Handler); // 开始任务已经完成自己使命,删除自己taskEXIT_CRITICAL(); // 退出临界区
}// task1函数实现
void task1(void *pvParameters)
{uint8_t key_value = 0;UBaseType_t sema_value = 0;while(1){key_value = key_scan(0);switch(key_value){case KEY0_PRES:{xSemaphoreGive(CountSemaphore); // 给计数信号量加1sema_value = uxSemaphoreGetCount(CountSemaphore); // 获取计数信号量的值lcd_show_xnum(166, 111, sema_value, 2, 16, 0, BLUE);}default:break;}vTaskDelay(10);}
}// task2函数实现
void task2(void *pvParameters)
{UBaseType_t sema_value = 0;uint32_t task2_num = 0;while(1){xSemaphoreTake(CountSemaphore, portMAX_DELAY); // 获取计数信号量,阻塞等待sema_value = uxSemaphoreGetCount(CountSemaphore);lcd_show_xnum(166, 111, sema_value, 2, 16, 0, BLUE);             lcd_fill(6, 131, 233, 313, lcd_discolor[++task2_num % 11]);     vTaskDelay(1000);}
}
/*---------------------------------------------*/

5.3 主函数

#include "sys.h"
#include "delay.h"
#include "usart.h"
#include "led.h"
#include "mpu.h"
#include "lcd.h"
#include "key.h"
#include "malloc.h"
#include "freertos_demo.h"int main(void)
{sys_cache_enable();                 /* 打开L1-Cache */HAL_Init();                         /* 初始化HAL库 */sys_stm32_clock_init(240, 2, 2, 4); /* 设置时钟, 480Mhz */delay_init(480);                    /* 延时初始化 */usart_init(115200);                 /* 串口初始化为115200 */led_init();                         /* 初始化LED */mpu_memory_protection();            /* 保护相关存储区域 */lcd_init();                         /* 初始化LCD */key_init();                         /* 初始化按键 */my_mem_init(SRAMIN);                /* 初始化内部内存池(AXI) */freertos_demo();                    /* 运行FreeRTOS例程 */
}

6. 互斥信号量

互斥信号量其实就是一个拥有优先级继承的二值信号量,在同步的应用中(任务与任务或中断与任务之间的同步)二值信号量最适合。互斥信号量适合用于那些需要互斥访问的应用中。在互斥访问中互斥信号量相当于一把钥匙, 当任务想要访问共享资源的时候就必须先获得这把钥匙,当访问完共享资源以后就必须归还这把钥匙,这样其他的任务就可以拿着这把钥匙去访问资源。

互斥信号量使用和二值信号量相同的 API 操作函数,所以互斥信号量也可以设置阻塞时间,不同于二值信号量的是互斥信号量具有优先级继承的机制。当一个互斥信号量正在被一个低优先级的任务持有时, 如果此时有个高优先级的任务也尝试获取这个互斥信号量,那么这个高优先级的任务就会被阻塞。不过这个高优先级的任务会将低优先级任务的优先级提升到与自己相同的优先级,这个过程就是优先级继承。优先级继承尽可能的减少了高优先级任务处于阻塞态的时间,并且将“优先级翻转”的影响降到最低。

优先级继承并不能完全的消除优先级翻转的问题,它只是尽可能的降低优先级翻转带来的影响。实时应用应该在设计之初就要避免优先级翻转的发生。互斥信号量不能用于中断服务函数中,原因如下:

  1. 互斥信号量有任务优先级继承的机制, 但是中断不是任务,没有任务优先级, 所以互斥信号量只能用与任务中,不能用于中断服务函数。

  2. 中断服务函数中不能因为要等待互斥信号量而设置阻塞时间进入阻塞态。

函数描述
xSemaphoreCreateMutex()使用动态方式创建互斥信号量
xSemaphoreCreateMutexStatic()使用静态方式创建互斥信号量
xSemaphoreTake()获取信号量
xSemaphoreGive()释放信号量
vSemaphoreDelete()删除信号量

6.1 创建互斥信号量

  • xSemaphoreCreateMutex()

    • 描述: 使用动态方式创建互斥信号量。互斥量所需的内存将从 FreeRTOS 堆中分配。创建时,互斥量会被初始化为“空”(可用)状态,其计数值为 1。
    • 函数原型: SemaphoreHandle_t xSemaphoreCreateMutex( void );
    • 返回值: 如果互斥量成功创建,则返回一个信号量句柄(SemaphoreHandle_t 类型);否则返回 NULL
    • 注意事项: 要使此函数可用,configUSE_MUTEXESconfigSUPPORT_DYNAMIC_ALLOCATION 都需在 FreeRTOSConfig.h 中定义为 1。互斥量具有优先级继承特性,当一个低优先级任务持有互斥量而高优先级任务试图获取该互斥量时,低优先级任务的优先级会被临时提升到高优先级任务的优先级,直到它释放互斥量。
  • xSemaphoreCreateMutexStatic()

    • 描述: 使用静态方式创建互斥信号量。互斥量所需的内存由用户在编译时或运行时提供。创建时,互斥量会被初始化为“空”(可用)状态。
    • 函数原型: SemaphoreHandle_t xSemaphoreCreateMutexStatic( StaticSemaphore_t *pxSemaphoreBuffer );
    • 参数: pxSemaphoreBuffer - 指向用户提供的 StaticSemaphore_t 结构体变量的指针,该结构体将用作互斥量的控制块。
    • 返回值: 如果互斥量成功创建,则返回一个信号量句柄;否则返回 NULL
    • 注意事项: 要使此函数可用,configUSE_MUTEXESconfigSUPPORT_STATIC_ALLOCATION 都需在 FreeRTOSConfig.h 中定义为 1。用户需要自行管理 pxSemaphoreBuffer 所指向的内存生命周期。

6.2 获取互斥信号量

  • 描述: 尝试获取一个互斥信号量。如果互斥量可用,则获取成功,互斥量变为不可用状态,并记录下获取该互斥量的任务。如果互斥量不可用,调用任务将进入阻塞状态(如果指定了等待时间),直到互斥量可用或超时。
  • 函数原型: BaseType_t xSemaphoreTake( SemaphoreHandle_t xSemaphore, TickType_t xTicksToWait );
  • 参数:
    • xSemaphore: 要获取的互斥量句柄。
    • xTicksToWait: 如果互斥量不可用,任务愿意阻塞等待的节拍数。
  • 返回值:
    • pdPASS: 成功获取互斥量。
    • pdFAIL: 互斥量不可用,且等待时间已到或为 0 未等待。
  • 注意事项: 互斥量应该总是由获取它的任务释放。在获取互斥量时,FreeRTOS 会自动检查是否有更高优先级的任务正在等待该互斥量,并可能触发优先级继承。

6.3 释放互斥信号量

  • 描述: 释放一个互斥信号量,使其变为可用状态。只有持有该互斥量的任务才能成功释放它。如果当前有任务因等待此互斥量而阻塞,则优先级最高的阻塞任务将被解除阻塞。
  • 函数原型: BaseType_t xSemaphoreGive( SemaphoreHandle_t xSemaphore );
  • 参数: xSemaphore - 要释放的互斥量句柄。
  • 返回值:
    • pdPASS: 成功释放互斥量。
    • pdFAIL: 信号量已处于可用状态,或者尝试释放的不是持有信号量的任务。
  • 注意事项: 当任务释放互斥量时,如果它之前因为优先级继承而提升了优先级,其优先级将被恢复到原始值。互斥量不能从 ISR 中释放(因为 ISR 没有优先级继承的概念,并且不允许阻塞),如果需要在 ISR 中释放,应该考虑使用二值信号量作为同步机制。

6.4 删除信号量

  • 描述: 删除一个互斥信号量,并释放其占用的内存(如果它是动态创建的)。
  • 函数原型: void vSemaphoreDelete( SemaphoreHandle_t xSemaphore );
  • 参数: xSemaphore - 要删除的互斥量句柄。
  • 注意事项: 只能删除动态创建的互斥量。删除后,不应再使用该句柄。

7. 互斥信号量操作测试

7.1 任务配置

/*----------------任务配置区-----------------*/
// START_TASK配置
#define START_TASK_PRIO 1           // 任务优先级 
#define START_STK_SIZE  128         // 任务堆栈大小 
TaskHandle_t StartTask_Handler;     // 任务句柄 
void start_task(void *pvParameters);// 任务函数 // TASK1配置
#define TASK1_PRIO      4                   
#define TASK1_STK_SIZE  128                 
TaskHandle_t Task1Task_Handler;          
void task1(void *pvParameters);// TASK2配置
#define TASK2_PRIO      3                   
#define TASK2_STK_SIZE  128                 
TaskHandle_t Task2Task_Handler;          
void task2(void *pvParameters);// TASK3配置
#define TASK3_PRIO      2                   
#define TASK3_STK_SIZE  128                 
TaskHandle_t Task3Task_Handler;          
void task3(void *pvParameters);SemaphoreHandle_t MutexSemaphore; // 互斥信号量
/*---------------------------------------------*/

7.2 任务实现

/*------------------任务实现区------------------*/
void freertos_demo(void)
{lcd_show_string(10,47,220,24,24,"MultiTask Demo",RED);// 创建START_TASK任务xTaskCreate((TaskFunction_t)start_task,        // 任务函数(const char*)"start_task",         // 任务名称(uint16_t)START_STK_SIZE,          // 任务堆栈大小(void*)NULL,                       // 传递给任务函数的参数(UBaseType_t)START_TASK_PRIO,      // 任务优先级(TaskHandle_t*)&StartTask_Handler);// 任务句柄// 开始任务调度vTaskStartScheduler();
}// start_task函数实现
void start_task(void *pvParameters)
{taskENTER_CRITICAL(); // 进入临界区// 创建互斥信号量MutexSemaphore = xSemaphoreCreateMutex();  // 创建TASK1任务xTaskCreate((TaskFunction_t)task1,  (const char*)"task1",       (uint16_t)TASK1_STK_SIZE,   (void*)NULL,                (UBaseType_t)TASK1_PRIO,    (TaskHandle_t*)&Task1Task_Handler);// 创建TASK2任务xTaskCreate((TaskFunction_t)task2,  (const char*)"task2",       (uint16_t)TASK2_STK_SIZE,   (void*)NULL,                (UBaseType_t)TASK2_PRIO,    (TaskHandle_t*)&Task2Task_Handler);// 创建TASK3任务xTaskCreate((TaskFunction_t)task3,  (const char*)"task3",       (uint16_t)TASK3_STK_SIZE,   (void*)NULL,                (UBaseType_t)TASK3_PRIO,    (TaskHandle_t*)&Task3Task_Handler);vTaskDelete(StartTask_Handler); // 开始任务已经完成自己使命,删除自己taskEXIT_CRITICAL(); // 退出临界区
}// task1函数实现-优先级低
void task1(void *pvParameters)
{vTaskDelay(500);while(1){printf("task1 ready to take mutex\r\n");xSemaphoreTake(MutexSemaphore, portMAX_DELAY); // 获取互斥信号量printf("task1 has taked mutex\r\n");printf("task1 running\r\n");printf("task1 give mutex\r\n");xSemaphoreGive(MutexSemaphore); // 释放互斥信号量vTaskDelay(100);}
}// task2函数实现-优先级中
void task2(void *pvParameters)
{uint32_t task2_num = 0;vTaskDelay(200);while(1){for(task2_num = 0; task2_num < 5; task2_num++){printf("task2 running\r\n");delay_ms(100); // 模拟运行,不触发任务调度}vTaskDelay(1000);}
}// task3函数实现-优先级搞
void task3(void *pvParameters)
{uint32_t task3_num = 0;while(1){printf("task3 ready to take mutex\r\n");xSemaphoreTake(MutexSemaphore, portMAX_DELAY); // 获取互斥信号量printf("task3 has taked mutex\r\n");for(task3_num = 0; task3_num < 5; task3_num++){printf("task3 running\r\n");delay_ms(100); // 模拟运行,不触发任务调度}printf("task3 give mutex\r\n");xSemaphoreGive(MutexSemaphore); // 释放互斥信号量vTaskDelay(1000);}
}
// 程序运行流程:task3获取互斥信号量(因为优先级最高)
// task3运行5次后释放互斥信号量
// task2同时运行5次
// task1获取互斥信号量(因为优先级最低)
/*---------------------------------------------*/

7.3 主函数

#include "sys.h"
#include "delay.h"
#include "usart.h"
#include "led.h"
#include "mpu.h"
#include "lcd.h"
#include "key.h"
#include "malloc.h"
#include "freertos_demo.h"int main(void)
{sys_cache_enable();                 /* 打开L1-Cache */HAL_Init();                         /* 初始化HAL库 */sys_stm32_clock_init(240, 2, 2, 4); /* 设置时钟, 480Mhz */delay_init(480);                    /* 延时初始化 */usart_init(115200);                 /* 串口初始化为115200 */led_init();                         /* 初始化LED */mpu_memory_protection();            /* 保护相关存储区域 */lcd_init();                         /* 初始化LCD */key_init();                         /* 初始化按键 */my_mem_init(SRAMIN);                /* 初始化内部内存池(AXI) */freertos_demo();                    /* 运行FreeRTOS例程 */
}

8. 三种信号量应用场景总结

8.1 二值信号量 (Binary Semaphore)

二值信号量只有两种状态:满(可用)和空(不可用),其行为类似于一个只有一个元素的队列。它主要用于任务与任务中断与任务之间的同步

典型应用场景:

  • 事件通知/任务同步:

    • 中断通知任务: 当一个中断发生时,ISR 释放(Give)一个二值信号量,而一个任务则等待(Take)这个信号量。ISR 不会阻塞,而任务在等待期间可以进入阻塞状态以节省CPU。这是中断通知任务最常用的方式。
      • 示例: 一个串口接收中断接收到一个完整的帧数据后,通过 xSemaphoreGiveFromISR() 释放信号量,通知一个处理任务去解析数据。
    • 任务通知任务: 一个任务完成某个操作或达到某个状态后,释放信号量通知另一个任务继续执行。
      • 示例: 一个数据采集任务完成数据采集后,释放信号量通知数据处理任务开始处理数据。
  • 简单的互斥锁:

    • 虽然二值信号量可以用于互斥(例如,保护一个共享资源),但它不具备优先级继承机制。这意味着在高优先级任务尝试获取被低优先级任务持有的二值信号量时,可能会发生优先级反转问题。因此,不推荐使用二值信号量作为互斥锁来保护共享资源,特别是当可能发生优先级反转时。互斥量(Mutex)是更优的选择。

8.2 计数型信号量 (Counting Semaphore)

计数型信号量有一个计数器,其值可以大于 1。它主要用于资源管理事件计数

典型应用场景:

  • 资源管理: 当有多个相同类型的资源实例时,计数型信号量可以用于管理这些资源的使用。计数值代表可用资源的数量。

    • 示例: 系统中有3个打印机,创建初始计数值为3的计数型信号量。每个任务需要打印时先获取信号量,使用完后释放信号量。当所有打印机都被占用时,后续尝试获取信号量的任务将阻塞,直到有打印机被释放。
    • 示例: 一个缓冲区池中有N个空闲缓冲区。每当任务需要一个缓冲区时,它获取信号量;每当任务释放一个缓冲区时,它释放信号量。
  • 事件计数: 用于记录某个事件发生的次数。

    • 示例: 多个中断源都可能触发相同的事件,每次事件发生时,ISR 都释放信号量。一个任务通过获取信号量来处理事件,并知道有多少个事件待处理(通过信号量的当前值)。
    • 示例: 在生产者-消费者模型中,生产者每次生产一个数据项就释放信号量,消费者每次处理一个数据项就获取信号量。信号量的计数值可以表示队列中待处理的数据项数量。

8.3 互斥信号量 (Mutex Semaphore)

互斥信号量是特殊的二值信号量,它增加了 优先级继承(Priority Inheritance) 机制。它专门用于互斥访问共享资源,有效解决了优先级反转问题。

典型应用场景:

  • 保护共享资源(临界区): 这是互斥信号量的最主要用途。当多个任务需要访问同一块共享内存、外设寄存器或其他非可重入资源时,为了避免数据损坏或状态不一致,需要使用互斥量来确保在任何给定时刻只有一个任务能够访问该资源。

    • 示例: 多个任务需要向同一个串口发送数据。为串口设备创建一个互斥量,每个任务在发送数据前先获取互斥量,发送完成后释放互斥量。这确保了数据发送的原子性,避免了数据混淆。
    • 示例: 访问共享全局变量或数据结构时,使用互斥量将其操作包装起来,以保证数据完整性。
  • 解决优先级反转问题: 优先级继承是互斥量最重要的特性。当一个高优先级任务尝试获取一个被低优先级任务持有的互斥量时,低优先级任务的优先级会被临时提升到高优先级任务的优先级,直到它释放互斥量。这确保了低优先级任务能够尽快执行并释放资源,避免了高优先级任务被“饿死”的情况。

    • 示例: 任务A (高优先级) 和任务B (低优先级) 都需要访问共享资源X。如果任务B在获取了互斥量保护资源X后,任务A抢占了任务B并尝试获取互斥量X。由于优先级继承,任务B的优先级会临时提升到任务A的优先级,使其能够尽快执行完临界区并释放互斥量,从而避免了任务A被一个中等优先级任务打断而无限期等待的问题。

总结表格:

特性/信号量类型二值信号量计数型信号量互斥信号量
主要用途任务同步、事件通知资源管理、事件计数保护共享资源(临界区)
计数范围0或10到N(N为最大计数值)0或1
优先级继承
ISR中Give支持 (xSemaphoreGiveFromISR)支持 (xSemaphoreGiveFromISR)不支持
主要优点轻量、简单高效的同步管理多个相同资源实例解决优先级反转,强健的互斥保护
主要限制不适合复杂资源管理;无优先级继承无优先级继承不能从ISR中Give

第十章 FreeRTOS软件定时器

1. 软件定时器简介

软件定时器是指具有定时功能的软件, FreeRTOS 提供的软件定时器允许在创建前设置一个软件定时器定时超时时间,在软件定时器成功创建并启动后,软件定时器开始定时,当软件定时器的定时时间达到或超过先前设置好的软件定时器定时器超时时间时,软件定时器就处于超时状态,此时软件定时器就会调用相应的回调函数,一般这个回调函数的处理的事务就是需要周期处理的事务。

FreeRTOS 提供的软件定时器还能够根据需要设置成单次定时器和周期定时器。当单次定时器定时超时后,不会自动启动下一个周期的定时,而周期定时器在定时超时后,会自动地启动下一个周期的定时。

FreeRTOS 提供的软件定时器功能,属于 FreeRTOS 的中可裁剪可配置的功能, 如果要使能软件定时器功能,那需要在 FreeRTOSConfig.h 文件中将 configUSE_TIMERS 配置项配置成 1。

要注意的是,软件定时器的超时回调函数是由软件定时器服务任务调用的,软件定定时器的超时回调函数本身不是任务,因此不能在该回调函数中使用可能会导致任务阻塞的 API 函数,例如 vTaskDelay()、 vTaskDelayUntil()和一些会到时任务阻塞的等到事件函数,这些函数将会导致软件定时器服务任务阻塞,这是不可以出现的。

1.1 软件定时器服务任务简介

使能了软件定时器功能后,在调用函数 vTaskStartScheduler()开启任务调度器的时候,会创建一个用于管理软件定时器的任务,这个任务就叫做软件定时器服务任务。软件定时器服务任务,主要负责软件定时器超时的逻辑判断、调用超时软件定时器的超时回调函数以及处理软件定时器命令队列。

1.2 软件定时器命令队列

FreeRTOS 提供了许多软件定时器相关的 API 函数,这些 API 函数,大部分都是往定时器的队列中写入消息(发送命令),这个队列叫做软件定时器命令队列,是提供给 FreeRTOS 中的软件定时器使用的,用户是不能直接访问的。软件定时器命令队列的操作过程如下图所示:

在这里插入图片描述

上图中,左侧的代码为应用程序中用户任务的代码,而右侧的代码为软件定时器服务任务的代码。当用户任务需要操作软件定时器时,就需要调用软件定时器相关的 API 函数,例如图中调用了函数 vTaskStart()启动软件定时器的定时,而函数 vTaskStart()实际上会往软件定时器命令队列写入一条消息(发送命令),这条消息就包含了待操作的定时器对象以及操作的命令(启动软件定时器),软件定时器服务任务就会去读取软件定时器命令队列中的消息(接收命令),并处理这些消息(处理命令)。可以看出,用户任务并不会直接操作软件定时器对象,而是发送命令给软件定时器服务任务,软件定时器服务任务接收到命令后,根据命令内容去操作软件定时器。

1.3 软件定时器的状态

软件定时器可以处于一下两种状态中一种:

  1. 休眠态

休眠态软件定时器可以通过其句柄被引用,但是因为没有运行,所以其定时超时回调函数不会被执行。

  1. 运行态

处于运行态或在上次定时超时后再次定时超时的软件定时器,会执行其定时超时回调函数。

1.4 单次定时器和周期定时器

FreeRTOS 提供了两种软件定时器,如下:

  1. 单次定时器

单次定时器的一旦定时超时,只会执行一次其软件定时器超时回调函数,超时后可以被手动重新开启,但单次定时器不会自动重新开启定时。

  1. 周期定时器

周期定时器的一旦被开启,会在每次超时时,自动地重新启动定时器,从而周期地执行其软件定时器回调函数。

单次定时器和周期定时器之间的差异如下图所示:

在这里插入图片描述

上图展示了单次定时器和周期定时器之间的差异,图中的垂直虚线的间隔时间为一个单位时间,可以理解为一个系统时钟节拍。其中 Timer1 为周期定时器,定时超时时间为 2 个单位时间, Timer2 为单次定时器,定时超时时间为 1 个单位时间。可以看到, Timer1 在开启后,一直以 2 个时间单位的时间间隔重复执行,为 Timer2 则在第一个超时后就不在执行了。

1.5 软件定时器的状态转换图

单次定时器的状态转化图,如下图所示:

在这里插入图片描述

周期定时器的状态转换图,如下图所示:

在这里插入图片描述

1.6 复位软件定时器

除了开启和停止软件定时器的定时,还可以对软件定时器进行复位。复位软件定时器会使软件定时器的重新开启定时,复位后的软件定时器以复位时的时刻作为开启时刻重新定时,软件定时器的复位示意图如下图所示:

在这里插入图片描述

上图展示了软件定时器的复位过程,图中在 t0 时刻创建并启动了一个超时时间为 5 个单位时间的软件定时器,接着在 t3 时刻对软件定时器进行了复位,复位后软件定时器的超时时刻以复位时刻为开启时刻重新计算,在 t7 时刻又再次对软件定时器进行了复位,最终计算出软件定时器的超时时刻为最后一次复位的时刻(t7)加上软件定时器的超时时间(5 个单位时间),于是该软件定时器在 t12 时刻超时,并执行其超时回调函数。

2. 软件定时器相关配置

  1. configUSE_TIMERS

此宏用于使能软件定时器功能,如果要使用软件定时器功能,则需要将该宏定义定义为 1。开启软件定时器功能后,系统会系统创建软件定时器服务任务。

  1. configTIMER_TASK_PRIORITY

此宏用于配置软件定时器服务任务的任务优先级,当使能了软件定时器功能后,需要配置该宏定义,此宏定义可以配置为 0~(configMAX_PRIORITY-1)的任意值。

  1. configTIMER_QUEUE_LENGTH

此宏用于配置软件定时器命令队列的队列长度,当使能了软件定时器功能后,需要配置该宏定义,若要正常使用软件定时器功能,此宏定义需定义成一个大于 0 的值。

  1. configTIMER_TASK_STACK_DEPTH

此宏用于配置软件定时器服务任务的栈大小,当使能了软件定时器功能后,需要配置该宏定义,由于所有软件定时器的定时器超时回调函数都是由软件定时器服务任务调用的,因此这些软件定时器超时回调函数运行时使用的都是软件定时器服务任务的栈。

3. 软件定时器相关函数

函数描述
xTimerCreate()动态方式创建软件定时器
xTimerCreateStatic()静态方式创建软件定时器
xTimerStart()开启软件定时器定时
xTimerStartFromISR()在中断中开启软件定时器定时
xTimerStop()停止软件定时器定时
xTimerStopFromISR()在中断中停止软件定时器定时
xTimerReset()复位软件定时器定时
xTimerResetFromISR()在中断中复位软件定时器定时
xTimerChangePeriod()更改软件定时器的定时超时时间
xTimerChangePeriodFromISR()在中断中更改软件定时器的定时超时时间
xTimerDelete()删除软件定时器

3.1 创建软件定时器

  • xTimerCreate()

    • 描述: 动态方式创建软件定时器。定时器所需的内存从 FreeRTOS 堆中分配。
    • 函数原型: TimerHandle_t xTimerCreate( const char * const pcTimerName, TickType_t xTimerPeriodInTicks, UBaseType_t uxAutoReload, void *pvTimerID, TimerCallbackFunction_t pxCallbackFunction );
    • 参数:
      • pcTimerName: 定时器的名称,用于调试。
      • xTimerPeriodInTicks: 定时器的周期(或单次超时时间),以节拍为单位。
      • uxAutoReload: 如果设置为 pdTRUE,定时器将周期性地触发;如果设置为 pdFALSE,定时器将只触发一次。
      • pvTimerID: 用户定义的 ID,可以用于区分不同的定时器或传递上下文信息给回调函数。
      • pxCallbackFunction: 定时器超时时将被调用的回调函数指针。
    • 返回值: 如果定时器成功创建,则返回一个定时器句柄(TimerHandle_t 类型);否则返回 NULL
    • 注意事项: 要使此函数可用,configSUPPORT_DYNAMIC_ALLOCATION 需在 FreeRTOSConfig.h 中定义为 1。
  • xTimerCreateStatic()

    • 描述: 静态方式创建软件定时器。定时器所需的内存由用户在编译时或运行时提供。
    • 函数原型: TimerHandle_t xTimerCreateStatic( const char * const pcTimerName, TickType_t xTimerPeriodInTicks, UBaseType_t uxAutoReload, void *pvTimerID, TimerCallbackFunction_t pxCallbackFunction, StaticTimer_t *pxTimerBuffer );
    • 参数:
      • 前五个参数与 xTimerCreate() 相同。
      • pxTimerBuffer: 指向用户提供的 StaticTimer_t 结构体变量的指针,该结构体将用作定时器的控制块。
    • 返回值: 如果定时器成功创建,则返回一个定时器句柄;否则返回 NULL
    • 注意事项: 要使此函数可用,configSUPPORT_STATIC_ALLOCATION 需在 FreeRTOSConfig.h 中定义为 1。用户需要自行管理 pxTimerBuffer 所指向的内存生命周期。

3.2 控制软件定时器

  • xTimerStart()

    • 描述: 启动一个软件定时器。如果定时器已经是活动状态,此函数会复位定时器(使其从头开始计数)。
    • 函数原型: BaseType_t xTimerStart( TimerHandle_t xTimer, TickType_t xBlockTime );
    • 参数:
      • xTimer: 要启动的定时器句柄。
      • xBlockTime: 如果定时器命令队列已满,调用任务愿意阻塞等待的节拍数。通常设置为 0 或较小值,因为定时器命令是发送给守护任务的,通常不会阻塞太久。
    • 返回值: pdPASS 表示命令成功发送到守护任务;pdFALSE 表示命令发送失败(通常是超时)。
  • xTimerStartFromISR()

    • 描述: 在中断中启动一个软件定时器。此函数不会阻塞。
    • 函数原型: BaseType_t xTimerStartFromISR( TimerHandle_t xTimer, BaseType_t *pxHigherPriorityTaskWoken );
    • 参数:
      • xTimer: 要启动的定时器句柄。
      • pxHigherPriorityTaskWoken: 指向 BaseType_t 变量的指针。如果调用此函数导致更高优先级的任务(通常是守护任务)解除阻塞,则该变量将被设置为 pdTRUE
    • 注意事项: 在 ISR 结束时,应检查 *pxHigherPriorityTaskWoken 的值。如果为 pdTRUE,则调用 portYIELD_FROM_ISR()
  • xTimerStop()

    • 描述: 停止一个软件定时器。
    • 函数原型: BaseType_t xTimerStop( TimerHandle_t xTimer, TickType_t xBlockTime );
    • 参数:
      • xTimer: 要停止的定时器句柄。
      • xBlockTime: 同 xTimerStart()
    • 返回值:xTimerStart()
  • xTimerStopFromISR()

    • 描述: 在中断中停止一个软件定时器。
    • 函数原型: BaseType_t xTimerStopFromISR( TimerHandle_t xTimer, BaseType_t *pxHigherPriorityTaskWoken );
    • 参数、返回值及注意事项xTimerStartFromISR()
  • xTimerReset()

    • 描述: 复位一个软件定时器。无论定时器当前是否活动,此函数都会停止它(如果正在运行),并将其内部计数器重置为零,然后重新启动它。这有效地“重置”了定时器的超时周期。
    • 函数原型: BaseType_t xTimerReset( TimerHandle_t xTimer, TickType_t xBlockTime );
    • 参数、返回值及注意事项xTimerStart()
  • xTimerResetFromISR()

    • 描述: 在中断中复位一个软件定时器。
    • 函数原型: BaseType_t xTimerResetFromISR( TimerHandle_t xTimer, BaseType_t *pxHigherPriorityTaskWoken );
    • 参数、返回值及注意事项xTimerStartFromISR()
  • xTimerChangePeriod()

    • 描述: 更改软件定时器的定时超时时间(周期)。此函数会停止定时器(如果正在运行),更新其周期,然后重新启动它。
    • 函数原型: BaseType_t xTimerChangePeriod( TimerHandle_t xTimer, TickType_t xNewPeriod, TickType_t xBlockTime );
    • 参数:
      • xTimer: 要更改周期的定时器句柄。
      • xNewPeriod: 新的定时器周期,以节拍为单位。
      • xBlockTime: 同 xTimerStart()
    • 返回值:xTimerStart()
  • xTimerChangePeriodFromISR()

    • 描述: 在中断中更改软件定时器的定时超时时间。
    • 函数原型: BaseType_t xTimerChangePeriodFromISR( TimerHandle_t xTimer, TickType_t xNewPeriod, BaseType_t *pxHigherPriorityTaskWoken );
    • 参数、返回值及注意事项xTimerStartFromISR()

3.3 删除软件定时器

  • xTimerDelete()
    • 描述: 删除一个软件定时器,并释放其占用的内存(如果它是动态创建的)。
    • 函数原型: BaseType_t xTimerDelete( TimerHandle_t xTimer, TickType_t xBlockTime );
    • 参数:
      • xTimer: 要删除的定时器句柄。
      • xBlockTime: 同 xTimerStart()
    • 返回值:xTimerStart()
    • 注意事项: 只能删除动态创建的定时器。删除后,不应再使用该句柄。

4. 软件定时器测试

4.1 任务配置

/*----------------任务配置区-----------------*/
// START_TASK配置
#define START_TASK_PRIO 1           // 任务优先级 
#define START_STK_SIZE  128         // 任务堆栈大小 
TaskHandle_t StartTask_Handler;     // 任务句柄 
void start_task(void *pvParameters);// 任务函数 // TASK1配置
#define TASK1_PRIO      4                   
#define TASK1_STK_SIZE  128                 
TaskHandle_t Task1Task_Handler;          
void task1(void *pvParameters);TimerHandle_t TIM1_Handler; // 软件定时器句柄
TimerHandle_t TIM2_Handler;
void Timer1_Callback(TimerHandle_t xTimer); // 软件定时器回调函数
void Timer2_Callback(TimerHandle_t xTimer);
/*---------------------------------------------*/

4.2 任务实现

/*------------------任务实现区------------------*/
void freertos_demo(void)
{lcd_show_string(10,47,220,24,24,"Timer",RED);lcd_draw_rectangle(5, 110, 115, 314, BLACK);lcd_draw_rectangle(125, 110, 234, 314, BLACK);lcd_draw_line(5, 130, 115, 130, BLACK);lcd_draw_line(125, 130, 234, 130, BLACK);lcd_show_string(15, 111, 110, 16, 16, "Timer1: 000", BLUE);lcd_show_string(135, 111, 110, 16, 16, "Timer2: 000", BLUE);// 创建START_TASK任务xTaskCreate((TaskFunction_t)start_task,        // 任务函数(const char*)"start_task",         // 任务名称(uint16_t)START_STK_SIZE,          // 任务堆栈大小(void*)NULL,                       // 传递给任务函数的参数(UBaseType_t)START_TASK_PRIO,      // 任务优先级(TaskHandle_t*)&StartTask_Handler);// 任务句柄// 开始任务调度vTaskStartScheduler();
}// start_task函数实现
void start_task(void *pvParameters)
{taskENTER_CRITICAL(); // 进入临界区// TIM1为周期定时器TIM1_Handler = xTimerCreate((const char*)"TIM1",                      // 定时器名字(TickType_t)1000,                         // 定时器超时时间(UBaseType_t)pdTRUE,                      // 周期性定时器(void*)1,                                 // 定时器ID(TimerCallbackFunction_t)Timer1_Callback);// 定时器回调函数// TIM2为单次定时器TIM2_Handler = xTimerCreate((const char*)"TIM2",                     (TickType_t)1000,                         (UBaseType_t)pdFALSE,                     (void*)2,                                 (TimerCallbackFunction_t)Timer2_Callback);// 创建TASK1任务xTaskCreate((TaskFunction_t)task1,  (const char*)"task1",       (uint16_t)TASK1_STK_SIZE,   (void*)NULL,                (UBaseType_t)TASK1_PRIO,    (TaskHandle_t*)&Task1Task_Handler);vTaskDelete(StartTask_Handler); // 开始任务已经完成自己使命,删除自己taskEXIT_CRITICAL(); // 退出临界区
}void task1(void *pvParameters)
{uint8_t key_value = 0;while(1){if((TIM1_Handler != NULL) && (TIM2_Handler != NULL)){key_value = key_scan(0);switch(key_value){case KEY0_PRES: // KEY0启动软件定时器{xTimerStart((TimerHandle_t)TIM1_Handler,// 待启动的定时器(TickType_t)portMAX_DELAY); // 等待系统启动定时器xTimerStart((TimerHandle_t)TIM2_Handler,(TickType_t)portMAX_DELAY);break;}case KEY1_PRES: // KEY1停止软件定时器{xTimerStop((TimerHandle_t)TIM1_Handler,(TickType_t)portMAX_DELAY);xTimerStop((TimerHandle_t)TIM2_Handler,(TickType_t)portMAX_DELAY);break;}default:break;}}vTaskDelay(10);}
}

4.3 定时器超时回调

// Timer1_Callback函数实现
void Timer1_Callback(TimerHandle_t xTimer)
{static uint32_t tim1_num = 0;lcd_fill(6, 131, 114, 313, lcd_discolor[++tim1_num % 11]);    /* LCD区域刷新 */lcd_show_xnum(79, 111, tim1_num, 3, 16, 0x80, BLUE);          /* 显示定时器1超时次数 */
}// Timer2_Callback函数实现
void Timer2_Callback(TimerHandle_t xTimer)
{static uint32_t tim2_num = 0;lcd_fill(126, 131, 233, 313, lcd_discolor[++tim2_num % 11]);  /* LCD区域刷新 */lcd_show_xnum(199, 111, tim2_num, 3, 16, 0x80, BLUE);         /* 显示定时器2超时次数 */
}

4.4 主函数

#include "sys.h"
#include "delay.h"
#include "usart.h"
#include "led.h"
#include "mpu.h"
#include "lcd.h"
#include "key.h"
#include "malloc.h"
#include "freertos_demo.h"int main(void)
{sys_cache_enable();                 /* 打开L1-Cache */HAL_Init();                         /* 初始化HAL库 */sys_stm32_clock_init(240, 2, 2, 4); /* 设置时钟, 480Mhz */delay_init(480);                    /* 延时初始化 */usart_init(115200);                 /* 串口初始化为115200 */led_init();                         /* 初始化LED */mpu_memory_protection();            /* 保护相关存储区域 */lcd_init();                         /* 初始化LCD */key_init();                         /* 初始化按键 */my_mem_init(SRAMIN);                /* 初始化内部内存池(AXI) */freertos_demo();                    /* 运行FreeRTOS例程 */
}

文中完整工程下载:https://github.com/hazy1k/FreeRTOS-Quick-Start-Guide/tree/main/2.code

http://www.lryc.cn/news/625904.html

相关文章:

  • 【软件安装】VScode介绍安装步骤及中文界面设置方法
  • 从数据孤岛到实时互联:Canal 驱动的系统间数据同步实战指南
  • Java 11中的Collections类详解
  • 正式签约 | OpenLoong 项目正式捐赠至开放原子开源基金会,成为全国首个具身智能方向孵化项目!
  • Vulkan笔记(十三)-帧缓冲区与命令池命令缓冲区
  • 使用 SemanticKernel 连接本地大模型 Ollama
  • 11.Ansible自动化之-内容集管理
  • 快手Klear-Reasoner登顶8B模型榜首,GPPO算法双效强化稳定性与探索能力!
  • 图像增强——灰度变换增强(线性,对数,指数)、空间滤波增强、频域增强、主成分/彩色合成增强(原理解释和代码示例)
  • FPGA 在情绪识别领域的护理应用(一)
  • Spring Boot应用实现图片资源服务
  • 电商数据分析可视化预测系统
  • gitlab、jenkins等应用集成ldap
  • Wireshark获取数据传输的码元速率
  • 【iOS】内存管理
  • implement libtime on Windows
  • 软件系统运维常见问题
  • STM32之beep、多文件、延迟、按键以及呼吸灯
  • 【数据结构】用堆解决TOPK问题
  • 服务器数据恢复—硬盘坏道离线导致raid崩溃的StorNext文件系统数据恢复案例
  • 深度学习-167-MCP技术之工具函数的设计及注册到MCP服务器的两种方式
  • 应用控制技术、内容审计技术、AAA服务器技术
  • Commons-io
  • Syntax Error: Error: PostCSS received undefined instead of CSS string
  • CSS封装大屏自定义组件(标签线)
  • 2025年6月中国电子学会青少年软件编程(图形化)等级考试试卷(一级)答案 + 解析
  • LangChain —多模态 / 多源上下文管理
  • 云原生俱乐部-mysql知识点归纳(3)
  • 【论文阅读】SIMBA: single-cell embedding along with features(1)
  • 《Dual Prompt Personalized Federated Learning in Foundation Models》——论文阅读