第十一章 FreeRTOS事件标志组
1. 事件标志组简介
事件标志组与信号量一样属于任务间同步的机制,但是信号量一般用于任务间的单事件同步,对于任务间的多事件同步,仅使用信号量就显得力不从心了。 FreeRTOS 提供的事件标志组可以很好的处理多事件情况下的任务同步。
- 事件标志
事件标志是一个用于指示事件是否发生的布尔值,一个事件标志只有 0 或 1 两种状态, FreeRTOS 将多个事件标志储存在一个变量类型为 EventBits_t 变量中, 这个变量就是事件组。
- 事件组
事件组是一组事件标志的集合,一个事件组就包含了一个 EventBites_t 数据类型的变量,变量类型 EventBits_t 的定义如下所示:
typedef TickType_t EventBits_t;
#if ( configUSE_16_BIT_TICKS == 1 )
typedef uint16_t TickType_t;
#else
typedef uint32_t TickType_t;
#endif
#define configUSE_16_BIT_TICKS 0
从上面可以看出, EventBits_t 实际上是一个 16 位或 32 位无符号的数据类型。当configUSE_16_BIT_TICKS 配置为 0 时, EventBits_t 是一个 32 位无符号的数据类型;当configUSE_16_BIT_TICKS 配置为 1 时, EventBits_t 是一个 16 为无符号的数据类型。在本套教程的所有配套例程中,都将配置项 configUSE_16_BIT_TICKS 配置为 0,因此本文就以EventBits_t 为 32 位无符号数据类型为例进行讲解,对于另外一种情况,也是大同小异的。
虽然说使用了 32 位无符号的数据类型变量来存储事件标志,但这并不意味着,一个EventBits_t 数据类型的变量能够存储 32 个事件标志, FreeRTOS 将这个 EventBits_t 数据类型的变量拆分成两部分,其中低 24 位[23:0] (configUSE_16_BIT_TICKS 配置位 1 时,是低 8 位[7:0])用于存储事件标志,而高 8 位[31:24](configUSE_16_BIT_TICKS 配置位 1 时,依然是高 8 位[15:8]) 用作存储事件标志组的一些控制信息,也就是说一个事件组最多可以存储 24 个事件标志。 EventBits_t 数据类型变量的位使用情况如下图所示:
从上图中可以看到,变量中低 24 位中的每一位都是一个事件标志,当某一位被置一时,就表示这一位对应的事件发生了。
2. 事件标志组相关函数
函数 | 描述 |
---|---|
xEventGroupCreate() |
使用动态方式创建事件标志组 |
xEventGroupCreateStatic() |
使用静态方式创建事件标志组 |
vEventGroupDelete() |
删除事件标志组 |
xEventGroupWaitBits() |
等待事件标志位 |
xEventGroupSetBits() |
设置事件标志位 |
xEventGroupSetBitsFromISR() |
在中断中设置事件标志位 |
xEventGroupClearBits() |
清零事件标志位 |
xEventGroupClearBitsFromISR() |
在中断中清零事件标志位 |
xEventGroupGetBits() |
获取事件组中各事件标志位的值 |
xEventGroupGetBitsFromISR() |
在中断中获取事件组中各事件标志位的值 |
xEventGroupSync() |
设置事件标志位,并等待事件标志位 |
2.1 创建事件标志组
xEventGroupCreate()
- 描述: 动态方式创建事件标志组。事件标志组所需的内存从 FreeRTOS 堆中分配。创建时,所有事件标志位都被初始化为 0。
- 函数原型:
EventGroupHandle_t xEventGroupCreate( void );
- 返回值: 如果事件标志组成功创建,则返回一个句柄(
EventGroupHandle_t
类型);否则返回NULL
。 - 注意事项: 要使此函数可用,
configSUPPORT_DYNAMIC_ALLOCATION
需在FreeRTOSConfig.h
中定义为 1。
xEventGroupCreateStatic()
- 描述: 静态方式创建事件标志组。事件标志组所需的内存由用户在编译时或运行时提供。创建时,所有事件标志位都被初始化为 0。
- 函数原型:
EventGroupHandle_t xEventGroupCreateStatic( StaticEventGroup_t *pxEventGroupBuffer );
- 参数:
pxEventGroupBuffer
- 指向用户提供的StaticEventGroup_t
结构体变量的指针,该结构体将用作事件标志组的控制块。 - 返回值: 如果事件标志组成功创建,则返回一个句柄;否则返回
NULL
。 - 注意事项: 要使此函数可用,
configSUPPORT_STATIC_ALLOCATION
需在FreeRTOSConfig.h
中定义为 1。用户需要自行管理pxEventGroupBuffer
所指向的内存生命周期。
2.2 等待事件标志位
xEventGroupWaitBits()
- 描述: 任务阻塞等待一个或多个事件标志位被设置。可以配置为等待所有指定位(逻辑 AND)或任意一个位(逻辑 OR)。还可以选择在成功接收后自动清除这些位。
- 函数原型:
EventBits_t xEventGroupWaitBits( EventGroupHandle_t xEventGroup, const EventBits_t uxBitsToWaitFor, const BaseType_t xClearOnExit, const BaseType_t xWaitForAllBits, TickType_t xTicksToWait );
- 参数:
xEventGroup
: 目标事件标志组的句柄。uxBitsToWaitFor
: 要等待的事件标志位的掩码(例如(1 << 0) | (1 << 2)
)。xClearOnExit
: 如果设置为pdTRUE
,当等待的位被满足后,它们会被自动清除;如果设置为pdFALSE
,则不会自动清除。xWaitForAllBits
: 如果设置为pdTRUE
,任务将等待所有uxBitsToWaitFor
中的位都设置;如果设置为pdFALSE
,任务将等待uxBitsToWaitFor
中任意一个位设置。xTicksToWait
: 如果事件标志位未被设置,任务愿意阻塞等待的节拍数。
- 返回值: 调用函数时事件组中所有标志位的当前值。可以通过检查返回值来确定哪些位被设置。
2.3 设置事件标志位
xEventGroupSetBits()
- 描述: 设置事件标志组中的一个或多个标志位。设置位可能会导致一个或多个等待该事件组的任务解除阻塞。
- 函数原型:
EventBits_t xEventGroupSetBits( EventGroupHandle_t xEventGroup, const EventBits_t uxBitsToSet );
- 参数:
xEventGroup
: 目标事件标志组的句柄。uxBitsToSet
: 要设置的事件标志位的掩码。
- 返回值: 调用函数后事件组中所有标志位的当前值。
xEventGroupSetBitsFromISR()
- 描述: 在中断中设置事件标志组中的一个或多个标志位。此函数不会阻塞。
- 函数原型:
BaseType_t xEventGroupSetBitsFromISR( EventGroupHandle_t xEventGroup, const EventBits_t uxBitsToSet, BaseType_t *pxHigherPriorityTaskWoken );
- 参数:
xEventGroup
: 目标事件标志组的句柄。uxBitsToSet
: 要设置的事件标志位的掩码。pxHigherPriorityTaskWoken
: 指向BaseType_t
变量的指针。如果调用此函数导致更高优先级的任务解除阻塞,则该变量将被设置为pdTRUE
。
- 注意事项: 在 ISR 结束时,应检查
*pxHigherPriorityTaskWoken
的值。如果为pdTRUE
,则调用portYIELD_FROM_ISR()
。
2.4 清零事件标志位
xEventGroupClearBits()
- 描述: 清零(设置为 0)事件标志组中的一个或多个标志位。
- 函数原型:
EventBits_t xEventGroupClearBits( EventGroupHandle_t xEventGroup, const EventBits_t uxBitsToClear );
- 参数:
xEventGroup
: 目标事件标志组的句柄。uxBitsToClear
: 要清零的事件标志位的掩码。
- 返回值: 调用函数后事件组中所有标志位的当前值。
xEventGroupClearBitsFromISR()
- 描述: 在中断中清零事件标志组中的一个或多个标志位。
- 函数原型:
BaseType_t xEventGroupClearBitsFromISR( EventGroupHandle_t xEventGroup, const EventBits_t uxBitsToClear );
- 参数:
xEventGroup
: 目标事件标志组的句柄。uxBitsToClear
: 要清零的事件标志位的掩码。
- 返回值: 总是返回
pdPASS
。此函数不会导致任务切换,因此没有pxHigherPriorityTaskWoken
参数。
2.5 获取事件标志位的值
xEventGroupGetBits()
- 描述: 获取事件组中所有事件标志位的当前值。此函数不会阻塞。
- 函数原型:
EventBits_t xEventGroupGetBits( EventGroupHandle_t xEventGroup );
- 参数:
xEventGroup
- 目标事件标志组的句柄。 - 返回值: 事件组中所有标志位的当前值。
xEventGroupGetBitsFromISR()
- 描述: 在中断中获取事件组中所有事件标志位的当前值。
- 函数原型:
EventBits_t xEventGroupGetBitsFromISR( EventGroupHandle_t xEventGroup );
- 参数:
xEventGroup
- 目标事件标志组的句柄。 - 返回值: 事件组中所有标志位的当前值。
2.6 同步任务:xEventGroupSync()
- 描述:
xEventGroupSync()
是一个同步原语,它结合了设置位和等待位的功能。一个或多个任务可以调用此函数来同步它们的执行。当所有参与的任务都设置了各自指定的位后,它们才会被解除阻塞。- 函数原型:
EventBits_t xEventGroupSync( EventGroupHandle_t xEventGroup, const EventBits_t uxBitsToSet, const EventBits_t uxBitsToWaitFor, TickType_t xTicksToWait );
- 参数:
xEventGroup
: 目标事件标志组的句柄。uxBitsToSet
: 调用任务要设置的标志位。uxBitsToWaitFor
: 要等待的所有任务共同设置的标志位。xTicksToWait
: 如果等待的位未被满足,任务愿意阻塞等待的节拍数。
- 返回值: 成功返回
uxBitsToWaitFor
中所有位都被设置时的事件组值;超时则返回 0。 - 注意事项: 这是一个非常强大的同步机制,可以用于任务屏障(Task Barrier)的实现。
- 函数原型:
2.7 删除事件标志组
vEventGroupDelete()
- 描述: 删除一个事件标志组,并释放其占用的内存(如果它是动态创建的)。
- 函数原型:
void vEventGroupDelete( EventGroupHandle_t xEventGroup );
- 参数:
xEventGroup
- 要删除的事件标志组句柄。 - 注意事项: 只能删除动态创建的事件标志组。删除后,不应再使用该句柄。
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 4
#define TASK2_STK_SIZE 128
TaskHandle_t Task2Task_Handler;
void task2(void *pvParameters);
// TASK3配置
#define TASK3_PRIO 3
#define TASK3_STK_SIZE 128
TaskHandle_t Task3Task_Handler;
void task3(void *pvParameters);
EventGroupHandle_t Event_handle;
#define EVENT0_BIT (1 << 0)
#define EVENT1_BIT (1 << 1)
#define EVENTALL_BIT (EVENT0_BIT | EVENT1_BIT)
/*---------------------------------------------*/
3.2 任务实现
/*------------------任务实现区------------------*/
void freertos_demo(void)
{
lcd_show_string(10, 47, 220, 24, 24, "Event Group", RED);
lcd_draw_rectangle(5, 130, 234, 314, BLACK);
lcd_show_string(30, 110, 220, 16, 16, "Event Group Value: 0", 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(); // 进入临界区
// 创建事件标志组
Event_handle = xEventGroupCreate();
// 创建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(); // 退出临界区
}
void task1(void *pvParameters)
{
uint8_t key_value;
while(1)
{
key_value = key_scan(0);
switch(key_value)
{
case KEY0_PRES:
{
xEventGroupSetBits((EventGroupHandle_t)Event_handle, (EventBits_t)EVENT0_BIT);
break;
}
case KEY1_PRES:
{
xEventGroupSetBits((EventGroupHandle_t)Event_handle, (EventBits_t)EVENT1_BIT);
break;
}
default:break;
}
vTaskDelay(10);
}
}
// task2函数实现
void task2(void *pvParameters)
{
uint32_t task2_value;
while(1)
{
xEventGroupWaitBits((EventGroupHandle_t)Event_handle,// 等待的事件标志组
(EventBits_t)EVENTALL_BIT, // 等待的事件标志
(BaseType_t)pdTRUE, // 函数退出时清零等待的事件
(BaseType_t)pdTRUE, // 等待 等待事件中的所有事件
(TickType_t)portMAX_DELAY); // 等待事件
lcd_fill(6,131,233,313,lcd_discolor[++task2_value%11]);
vTaskDelay(10);
}
}
// task3函数实现
void task3(void *pvParameters)
{
EventBits_t event_value;
while(1)
{
event_value = xEventGroupGetBits((EventGroupHandle_t)Event_handle); /* 获取的事件标志组句柄 */
lcd_show_xnum(132,110,event_value,1,16,0,BLUE);
vTaskDelay(10);
}
}
/*---------------------------------------------*/
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例程 */
}
第十二章 FreeRTOS任务通知
1. 任务通知
在 FreeRTOS 中,每一个任务都有两个用于任务通知功能的数组,分别为任务通知数组和任务通知状态数组。其中任务通知数组中的每一个元素都是一个 32 位无符号类型的通知值;而任务通知状态数组中的元素则表示与之对应的任务通知的状态。
任务通知数组中的 32 位无符号通知值,用于任务到任务或中断到任务发送通知的“媒介”。当通知值为 0 时,表示没有任务通知;当通知值不为 0 时,表示有任务通知,并且通知值就是通知的内容。
任务通知状态数组中的元素,用于标记任务通知数组中通知的状态,任务通知有三种状态,分别为未等待通知状态、等待通知状态和等待接收通知状态。其中未等待通知状态为任务通知的复位状态;当任务在没有通知的时候接收通知时,在任务阻塞等待任务通知的这段时间内,任务所等待的任务通知就处于等待通知状态;当有其他任务向任务发送通知,但任务还未接收这一通知的这段期间内,任务通知就处于等待接收通知状态。
任务通知功能所使用到的任务通知数组和任务通知状态数组为任务控制块中的成员变量,因此任务通知的传输是直接传出到任务中的,不同通过任务的通讯对象(队列、事件标志组和信号量就属于通讯对象) 这个间接的方式。 间接通讯示意图如下所示:
任务通知则是直接地往任务中发送通知,直接通讯示意图如下所示:
1.1 任务通知的优势
使用任务通知向任务发送事件或数据比使用队列、事件标志组或信号量快得多;并且使用任务通知代替队列、事件标志组或信号量,可以节省大量的内存,这是因为每个通讯对象在使用之前都需要被创建,而任务通知功能中的每个通知只需要在每个任务中占用固定的 5 字节内存
1.2 任务通知的缺点
虽然任务通知功能相比通讯对象,有着更快、占用内存少的优点,但是任务通知功能并不能适用于所有情况,例如以下列出的几种情况:
- 发送事件或数据到中断
通讯对象可以发送事件或数据从中断到任务,或从任务到中断,但是由于任务通知依赖于任务控制块中的两个成员变量,并且中断不是任务,因此任务通知功能并不适用于从任务往中断发送事件或数据的这种情况,但是任务通知功能可以在任务之间或从中断到任务发送事件或数据。
- 存在多个接收任务
通讯对象可以被已知通讯对象句柄的任意多个任务或中断访问(发送或接收),但任务通知是直接发送事件或数据到指定接收任务的,因传输的事件或数据只能由接收任务处理。然而在实际中很少受到这种情况的限制,因为,虽然多个任务和中断发送事件或数据到一个通讯对象是很常见的,但很少出现多个任务或中断接收同一个通讯对象的情况。
- 缓冲多个数据项
通讯对象中的队列是可以一次性保存多个已经被发送到队列,但还未被接收的事件或数据的,也就是说,通讯对象有着一定的缓冲多个数据的能力,但是任务通知是通过更新任务通知值来发送事件或数据的,一个任务通知值只能保存一次。
- 广播到多个任务
通讯对象中的事件标志组是可以将一个事件同时发送到多个任务中的,但任务通知只能是被指定的一个接收任务接收并处理。
- 阻塞等待接收任务
当通讯对象处于暂时无法写入的状态(例如队列已满,此时无法再向队列写入消息)时,发送任务是可以选择阻塞等待接收任务接收,但是任务因尝试发送任务通知到已有任务通知但还未处理的任务而进行阻塞等待的。但是任务通知也很少在实际情况中收到这种情况的限制。
3. 任务通知相关函数
3.1 基本任务通知相关函数
这些函数操作任务的默认(索引为 0)通知值。
函数 | 描述 |
---|---|
xTaskNotify() |
发送任务通知 |
xTaskNotifyAndQuery() |
发送任务通知,并获取旧的通知值 |
xTaskNotifyGive() |
递增接收任务的通知值 |
xTaskNotifyFromISR() |
在中断中发送任务通知 |
xTaskNotifyAndQueryFromISR() |
在中断中发送任务通知,并获取旧的通知值 |
vTaskNotifyGiveFromISR() |
在中断中递增接收任务的通知值 |
ulTaskNotifyTake() |
接收任务通知 (作为二值信号量或计数信号量) |
xTaskNotifyWait() |
接收任务通知 (作为事件标志组或直接值) |
1. 发送任务通知
xTaskNotify()
- 描述: 向指定任务发送通知,并可以对接收任务的通知值执行不同的操作(如覆盖、设置位、递增)。
- 函数原型:
BaseType_t xTaskNotify( TaskHandle_t xTaskToNotify, uint32_t ulValue, eNotifyAction eAction );
- 参数:
xTaskToNotify
: 接收通知的任务句柄。ulValue
: 要发送的值,其具体含义取决于eAction
。eAction
: 指定如何使用ulValue
更新接收任务的通知值(例如:eSetBits
设置位,eIncrement
递增,eSetValueWithOverwrite
覆盖等)。
- 返回值:
pdPASS
: 成功发送通知。pdFAIL
: 通常不会失败,除非xTaskToNotify
为NULL
或通知机制未使能。
xTaskNotifyAndQuery()
- 描述: 与
xTaskNotify()
类似,但它会返回接收任务通知值被更新前的值。 - 函数原型:
BaseType_t xTaskNotifyAndQuery( TaskHandle_t xTaskToNotify, uint32_t ulValue, eNotifyAction eAction, uint32_t *pulPreviousNotificationValue );
- 参数:
- 前三个参数与
xTaskNotify()
相同。 pulPreviousNotificationValue
: 指向uint32_t
变量的指针,用于接收通知值被更新前的值。如果不需要旧值,可以传入NULL
。
- 前三个参数与
- 返回值: 同
xTaskNotify()
。
- 描述: 与
xTaskNotifyGive()
- 描述: 递增接收任务的通知值。这模拟了信号量的
Give
操作,每次调用都会使通知值加 1,直到其达到最大值 (portMAX_DELAY
)。 - 函数原型:
BaseType_t xTaskNotifyGive( TaskHandle_t xTaskToNotify );
- 参数:
xTaskToNotify
- 接收通知的任务句柄。 - 返回值:
pdPASS
表示成功递增通知值。
- 描述: 递增接收任务的通知值。这模拟了信号量的
2. 中断中发送任务通知
这些函数是其对应非ISR版本的变体,用于在中断上下文安全地发送通知。它们通过 pxHigherPriorityTaskWoken
参数指示是否需要进行上下文切换。
xTaskNotifyFromISR()
- 描述: 在中断中向指定任务发送通知,并对接收任务的通知值执行操作。
- 函数原型:
BaseType_t xTaskNotifyFromISR( TaskHandle_t xTaskToNotify, uint32_t ulValue, eNotifyAction eAction, BaseType_t *pxHigherPriorityTaskWoken );
- 参数:
xTaskToNotify
,ulValue
,eAction
: 同xTaskNotify()
。pxHigherPriorityTaskWoken
: 指向BaseType_t
变量的指针。如果调用此函数导致更高优先级的任务解除阻塞,则该变量将被设置为pdTRUE
。
- 注意事项: 在 ISR 结束时,应检查
*pxHigherPriorityTaskWoken
的值。如果为pdTRUE
,则调用portYIELD_FROM_ISR()
。
xTaskNotifyAndQueryFromISR()
- 描述: 在中断中向指定任务发送通知,并获取通知值被更新前的值。
- 函数原型:
BaseType_t xTaskNotifyAndQueryFromISR( TaskHandle_t xTaskToNotify, uint32_t ulValue, eNotifyAction eAction, uint32_t *pulPreviousNotificationValue, BaseType_t *pxHigherPriorityTaskWoken );
- 参数、返回值及注意事项 同
xTaskNotifyFromISR()
和xTaskNotifyAndQuery()
的组合。
vTaskNotifyGiveFromISR()
- 描述: 在中断中递增接收任务的通知值。
- 函数原型:
BaseType_t vTaskNotifyGiveFromISR( TaskHandle_t xTaskToNotify, BaseType_t *pxHigherPriorityTaskWoken );
- 参数:
xTaskToNotify
: 接收通知的任务句柄。pxHigherPriorityTaskWoken
: 指向BaseType_t
变量的指针。如果调用此函数导致更高优先级的任务解除阻塞,则该变量将被设置为pdTRUE
。
- 注意事项: 在 ISR 结束时,应检查
*pxHigherPriorityTaskWoken
的值。如果为pdTRUE
,则调用portYIELD_FROM_ISR()
。
3. 接收任务通知
ulTaskNotifyTake()
- 描述: 接收任务通知,并可以使其表现为二值信号量或计数信号量。它会清零通知计数器,并返回其旧值。
- 函数原型:
uint32_t ulTaskNotifyTake( BaseType_t xClearCountOnExit, TickType_t xTicksToWait );
- 参数:
xClearCountOnExit
: 如果设置为pdTRUE
,则在返回前将通知值重置为 0。如果设置为pdFALSE
,则仅将通知值减 1(类似于计数信号量)。xTicksToWait
: 如果没有待处理的通知,任务愿意阻塞等待的节拍数。
- 返回值: 接收通知前任务的通知值。
xTaskNotifyWait()
- 描述: 接收任务通知,并可以使其表现为事件标志组或直接值。
- 函数原型:
BaseType_t xTaskNotifyWait( uint32_t ulBitsToClearOnEntry, uint32_t ulBitsToClearOnExit, uint32_t *pulNotificationValue, TickType_t xTicksToWait );
- 参数:
ulBitsToClearOnEntry
: 进入函数时,哪些位将被清零(设置为 0)。ulBitsToClearOnExit
: 退出函数时(收到通知后),哪些位将被清零。pulNotificationValue
: 指向uint32_t
变量的指针,用于存储接收到的通知值。xTicksToWait
: 如果没有待处理的通知,任务愿意阻塞等待的节拍数。
- 返回值:
pdPASS
表示成功接收通知;pdFALSE
表示超时未收到通知。
3.2 带索引的任务通知相关函数
FreeRTOS v10.0.0 引入了带索引的任务通知,允许每个任务拥有一个通知值数组(由 configTASK_NOTIFICATION_ARRAY_ENTRIES
定义数组大小,默认为 1),而不是单个通知值。这使得一个任务可以同时等待来自多个源的通知,而无需创建多个信号量或队列。
这些函数的命名通常在基本函数名后加上 Indexed
后缀。它们都多了一个 uxIndexToNotify
或 uxIndexToWait
参数,用于指定操作哪个通知值数组元素。
xTaskNotifyIndexed()
xTaskNotifyAndQueryIndexed()
xTaskNotifyGiveIndexed()
xTaskNotifyIndexedFromISR()
xTaskNotifyAndQueryIndexedFromISR()
vTaskNotifyGiveIndexedFromISR()
ulTaskNotifyTakeIndexed()
xTaskNotifyWaitIndexed()
这些带索引的函数的功能和参数与它们对应的基本任务通知函数类似,只是多了一个 uxIndex
参数来指定操作哪个通知槽。
示例:xTaskNotifyIndexed()
- 函数原型:
BaseType_t xTaskNotifyIndexed( TaskHandle_t xTaskToNotify, UBaseType_t uxIndexToNotify, uint32_t ulValue, eNotifyAction eAction );
- 参数:
xTaskToNotify
: 接收通知的任务句柄。uxIndexToNotify
: 要通知的通知值数组的索引。ulValue
: 要发送的值,其具体含义取决于eAction
。eAction
: 指定如何更新通知值。
示例:xTaskNotifyWaitIndexed()
- 函数原型:
BaseType_t xTaskNotifyWaitIndexed( UBaseType_t uxIndexToWait, uint32_t ulBitsToClearOnEntry, uint32_t ulBitsToClearOnExit, uint32_t *pulNotificationValue, TickType_t xTicksToWait );
- 参数:
uxIndexToWait
: 要等待的通知值数组的索引。- 其他参数同
xTaskNotifyWait()
。
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 2
#define TASK1_STK_SIZE 128
TaskHandle_t Task1Task_Handler;
void task1(void *pvParameters);
// TASK2配置
#define TASK2_PRIO 4
#define TASK2_STK_SIZE 128
TaskHandle_t Task2Task_Handler;
void task2(void *pvParameters);
/*---------------------------------------------*/
4.2 任务实现
/*------------------任务实现区------------------*/
void freertos_demo(void)
{
lcd_show_string(10,47,220,24,24,"Task Notify 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(); // 进入临界区
// 创建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(); // 退出临界区
}
void task1(void *pvParameters)
{
uint8_t key_value =0;
while(1)
{
if(Task2Task_Handler != NULL)
{
key_value = key_scan(0);
switch(key_value)
{
case KEY0_PRES:
{
xTaskNotifyGive((TaskHandle_t)Task2Task_Handler); // 发送通知给TASK2
break;
}
default:break;
}
}
vTaskDelay(10);
}
}
// task2函数实现
void task2(void *pvParameters)
{
uint32_t nofify_value = 0;
uint32_t task2_value = 0;
while(1)
{
nofify_value = ulTaskNotifyTake((BaseType_t)pdTRUE, (TickType_t)portMAX_DELAY); // 等待通知
if(nofify_value != 0)
{
lcd_fill(6,131,233,313,lcd_discolor[++task2_value%11]);
}
}
}
/*---------------------------------------------*/
4.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例程 */
}
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);
/*---------------------------------------------*/
5.2 任务实现
/*------------------任务实现区------------------*/
void freertos_demo(void)
{
lcd_show_string(10,47,220,24,24,"Task Notify Demo",RED);
lcd_draw_rectangle(5, 125, 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(); // 进入临界区
// 创建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(); // 退出临界区
}
void task1(void *pvParameters)
{
uint8_t key_value =0;
while(1)
{
key_value = key_scan(0);
if((Task2Task_Handler != NULL) && (key_value != 0))
{
// 接受任务通知的句柄 要更新的bit位 更新方式为覆写
xTaskNotify((TaskHandle_t)Task2Task_Handler, (uint32_t)key_value, (eNotifyAction)eSetValueWithOverwrite);
}
vTaskDelay(10);
}
}
// task2函数实现
void task2(void *pvParameters)
{
uint32_t nofify_value = 0;
uint32_t task2_value = 0;
while(1)
{
xTaskNotifyWait((uint32_t)0x00000000, // 进入函数时,不清除任务通知值
(uint32_t)0xFFFFFFFF, // 函数退出时,清除所有任务通知值
(uint32_t*)&nofify_value, // 等待接收通知值
(TickType_t)portMAX_DELAY);
switch(nofify_value)
{
case KEY0_PRES:
{
lcd_fill(6, 126, 233, 313, lcd_discolor[++task2_value % 11]);
break;
}
case KEY1_PRES:
{
LED0_TOGGLE();
break;
}
case WKUP_PRES:
{
LED1_TOGGLE();
break;
}
default:break;
}
}
}
/*---------------------------------------------*/
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. 任务通知模拟计数信号量测试
6.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);
/*---------------------------------------------*/
6.2 任务实现
/*------------------任务实现区------------------*/
void freertos_demo(void)
{
lcd_show_string(10,47,220,24,24,"Task Notify Demo",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(); // 进入临界区
// 创建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(); // 退出临界区
}
void task1(void *pvParameters)
{
uint8_t key_value =0;
while(1)
{
key_value = key_scan(0);
if(Task2Task_Handler != NULL)
{
switch(key_value)
{
case KEY0_PRES:
{
xTaskNotifyGive((TaskHandle_t)Task2Task_Handler); // 发送通知给TASK2
break;
}
default:break;
}
}
vTaskDelay(10);
}
}
// task2函数实现
void task2(void *pvParameters)
{
uint32_t nofify_value = 0;
uint32_t task2_value = 0;
while(1)
{
nofify_value = ulTaskNotifyTake((BaseType_t)pdTRUE, (TickType_t)portMAX_DELAY); // 等待通知
lcd_show_xnum(166,111,nofify_value-1,2,16,0,BLUE);
lcd_fill(6,131,233,313,lcd_discolor[++task2_value%11]);
vTaskDelay(1000);
}
}
/*---------------------------------------------*/
6.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例程 */
}
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 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);
#define EventBit_0 (1<<0)
#define EventBit_1 (1<<1)
#define EventBit_ALL (EventBit_0 | EventBit_1)
/*---------------------------------------------*/
7.2 任务实现
/*------------------任务实现区------------------*/
void freertos_demo(void)
{
lcd_show_string(10,47,220,24,24,"Task Notify Demo",RED);
lcd_show_string(30,110,220,16,16,"Event Group Value: 0",BLUE);
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(); // 进入临界区
// 创建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(); // 退出临界区
}
void task1(void *pvParameters)
{
uint8_t key_value =0;
while(1)
{
if(Task1Task_Handler != NULL)
{
key_value = key_scan(0);
switch(key_value)
{
case KEY0_PRES:
{
xTaskNotify((TaskHandle_t)Task2Task_Handler, (uint32_t)EventBit_0, (eNotifyAction)eSetBits);
break;
}
case KEY1_PRES:
{
xTaskNotify((TaskHandle_t)Task2Task_Handler, (uint32_t)EventBit_1, (eNotifyAction)eSetBits);
break;
}
default:break;
}
}
vTaskDelay(10);
}
}
// task2函数实现
void task2(void *pvParameters)
{
uint32_t notify_value = 0;
uint32_t event_value = 0;
uint32_t task2_value = 0;
while(1)
{
xTaskNotifyWait((uint32_t)0x00000000, // 进入函数时,不清除任务通知值
(uint32_t)0xFFFFFFFF, // 函数退出时,清除所有任务通知值
(uint32_t*)¬ify_value, // 等待接收通知值
(TickType_t)portMAX_DELAY);
if(notify_value & EventBit_0)
{
event_value |= EventBit_0;
}
else if(notify_value & EventBit_1)
{
event_value |= EventBit_1;
}
lcd_show_xnum(182,110,event_value,1,16,0,BLUE);
if(event_value == EventBit_ALL)
{
event_value = 0;
lcd_fill(6, 131, 233, 313, lcd_discolor[++task2_value % 11]);
}
}
}
/*---------------------------------------------*/
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例程 */
}
第十三章 FreeRTOS低功耗Tickless模式
1. 低功耗 Tickless 模式简介
在整个系统的运行过程中,其实大部分的时间是在执行空闲任务的,而空闲任务之所及叫做空闲任务,是因为空闲任务是在系统中的所有其他都阻塞或被挂起时才运行的,因此可以在本该空闲任务执行的期间,让MCU 进入相应的低功耗模式,接着在其他任务因被解除阻塞或其他原因,而准备运行的时候,让 MCU 退出相应的低功耗模式,去执行相应的任务。 在以上这一过程中,主要的难点在于, MCU 进入相应的低功耗模式后,如何判断有除空闲任务外的其他任务就绪,并退出相应的空闲模式去执行就绪任务,也就是如何计算 MCU 进入相应低功耗模式的时间,而 FreeRTOS 的低功耗 Tickless 模式机制已经处理好了这个问题。
2. 低功耗 Tickless 模式相关配置项
在前面对 FreeRTOS 低功耗 Tickless 模式的简介中提到了 FreeRTOS 中针对该模式的几个配置,如下表所示:
- configUSE_TICKLESS_IDLE
此宏用于使能低功耗 Tickless 模式,当此宏定义为 1 时,系统会在进入空闲任务期间进入相应的低功耗模式大于 configEXPECTED_IDLE_TIME_BEFORE_SLEEP 的时长。
- configEXPECTED_IDLE_TIME_BEFORE_SLEEP
此宏用于定义系统进入相应低功耗模式的最短时长,如果系统在进入相应低功耗模式前,计算出系统将进入相应低功耗的时长小于 configEXPECTED_IDLE_TIME_BEFORE_SLEEP 定义的最小时长,则系统不进入相应的低功耗模式,要注意的是,此宏的值不能小于 2。
- configPRE_SLEEP_PROCESSING(x)
此宏用于定义一些需要在系统进入相应低功耗模式前执行的事务,例如可以在进入低功耗模式前关闭一些 MCU 片上外设的时钟,以达到降低功耗的目的。
- configPOSR_SLEEP_PROCESSING(x)
此宏用于定义一些需要在系统退出相应低功耗模式后执行的事务,例如开启在系统在进入相应低功耗模式前关闭的 MCU 片上外设的时钟,以是系统能够正常运行。
3. 低功耗 Tickless测试
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);
/*---------------------------------------------*/
3.2 任务实现
/*------------------任务实现区------------------*/
void freertos_demo(void)
{
lcd_show_string(10,47,220,24,24,"FreeRTOS Tickless",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(); // 进入临界区
lcd_display_off();
LCD_BL(0);
// 创建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)
{
while(1)
{
LED0(1); // LED0灭,代表退出低功耗模式
delay_ms(3000); // 模拟任务运行,此时不会进入低功耗模式
LED0(0); // LED0亮,代表进入低功耗模式
vTaskDelay(3000);
}
}
3.3 低功耗处理
#if (configUSE_TICKLESS_IDLE == 1)
// 进入低功耗模式执行的操作,只做演示,以实际要求为准
void PRE_SLEEP_PROCESSING(void)
{
__HAL_RCC_GPIOA_CLK_DISABLE();
__HAL_RCC_GPIOB_CLK_DISABLE();
__HAL_RCC_GPIOC_CLK_DISABLE();
__HAL_RCC_GPIOD_CLK_DISABLE();
__HAL_RCC_GPIOE_CLK_DISABLE();
__HAL_RCC_GPIOF_CLK_DISABLE();
__HAL_RCC_GPIOG_CLK_DISABLE();
}
void POST_SLEEP_PROCESSING(void)
{
__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_GPIOB_CLK_ENABLE();
__HAL_RCC_GPIOC_CLK_ENABLE();
__HAL_RCC_GPIOD_CLK_ENABLE();
__HAL_RCC_GPIOE_CLK_ENABLE();
__HAL_RCC_GPIOF_CLK_ENABLE();
__HAL_RCC_GPIOG_CLK_ENABLE();
}
#endif
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. 空闲任务简介
在前面章节分析 vTaskStartScheduler()函数启动任务调度器的时候,就了解到, FreeRTOS 会自动创建一个空闲任务,这样就可以确保系统中永远都至少有一个正在被执行的任务。空闲任务是以系统中最低的任务优先级被创建的,这样可以确保空闲任务不会占用其他就绪态任务的被执行时间。
当有任务被函数 vTaskDelete()函数删除时,如果函数 vTaskDelete()的调用者与被删除任务不是同一个任务,那么被删除任务的删除工作可以由函数 vTaskDelete()的调用者完成。如果vTaskDelete()的调用者和被删除任务为同一个任务,即一个任务调用函数 vTaskDelete()删除了自身,那么删除工作是不能完全由任务本身完成的, 因此这项删除任务自身的工作就交给了空闲任务,也正因如此,如果在任务中调用了函数 vTaskDelete()删除了自身,那么就必须要保证最低任务优先级的空闲任务能够被分配到运行时间。
2. 空闲任务钩子函数
2.1 FreeRTOS 中的钩子函数
FreeRTOS 提供了多种钩子函数,当系统运行到某个功能或函数的某个位置时,就会调用相应的钩子函数,至于钩子函数具体要实现什么功能,可有由用户自行编写。当然,钩子函数是一项可选功能,用户如果不需要使用相应的钩子函数,那就无需编写相应的钩子函数。在FreeRTOSConfig.h 文件中就可以看到启用钩子函数的相关配置项,具体的代码如下所示:
/* 钩子函数相关定义 */
#define configUSE_IDLE_HOOK 0 /* 空闲任务钩子函数 */
#define configUSE_TICK_HOOK 0 /* 系统时钟节拍中断钩子函数 */
#define configUSE_MALLOC_FAILED_HOOK 0 /* 动态内存申请失败钩子函数 */
#define configUSE_DAEMON_TASK_STARTUP_HOOK 0 /* 首次执行定时器服务任务钩子函数 */
如果要启用相应的钩子函数,只需将对应的配置项配置为 1 即可,当然也不要忘了编写相应的钩子函数。
2.2 FreeRTOS 空闲任务钩子函数
如果将宏 configUSE_IDLE_HOOK 配置为1,那么在空闲任务的每一个运行周期中,都会调用一次函数 vApplicationIdleHook(),此函数就是空闲任务的钩子函数。
如果想在空闲任务相同的优先级中处理某些事务,那么有两种选择:
- 在空闲任务的钩子函数中处理需要处理的任务
在这种情况下,需要特别注意,因为不论在什么时候,都应该保证系统中有一个正在被执行的任务,因此在空闲任务的钩子函数中,不能够调用会到时空闲任务被阻塞或挂起的函数,例如函数 vTaskDelay()。
- 在和空闲任务相同任务优先级的任务中处理需要处理的事务
创建一个和空闲任务相同优先级的任务来处理需要处理的事务是一个比较好的方法,但是这会导致消耗更多的 RAM。
通常在空闲任务的钩子函数中设置处理器进入相应的低功耗模式,以达到降低整体功率的目的,为了与 FreeRTOS 自带的低功耗 Tickless 模式做区分,这里暂且将这种使用空闲任务钩子函数的低功耗模式成为通用低功耗模式,这是因为,几乎所有的 RTOS 都可以使用这种方式实现低功耗。
通用的低功耗模式会使处理器在每次进入空闲任务函数时,进入相应的低功耗模式,并且在每次 SysTick 中断发生的时候都会被唤醒, 可见通用方式实现的低功耗效果远不如 FreeRTOS自带的低功耗 Tickless 模式,但是这种方式更加通用。
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);
/*---------------------------------------------*/
3.2 任务实现
/*------------------任务实现区------------------*/
void freertos_demo(void)
{
lcd_show_string(10,47,220,24,24,"HOOK",RED);
delay_ms(2000);
// 创建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(); // 进入临界区
lcd_display_off();
LCD_BL(0);
// 创建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)
{
while(1)
{
LED0(1); // LED0灭,代表退出低功耗模式
delay_ms(3000); // 模拟任务运行,此时不会进入低功耗模式
LED0(0); // LED0亮,代表进入低功耗模式
vTaskDelay(3000);
}
}
3.3 钩子函数
void before_sleep(void)
{
__HAL_RCC_GPIOA_CLK_DISABLE();
__HAL_RCC_GPIOB_CLK_DISABLE();
__HAL_RCC_GPIOC_CLK_DISABLE();
__HAL_RCC_GPIOD_CLK_DISABLE();
__HAL_RCC_GPIOE_CLK_DISABLE();
__HAL_RCC_GPIOF_CLK_DISABLE();
__HAL_RCC_GPIOG_CLK_DISABLE();
}
void after_wakeup(void)
{
__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_GPIOB_CLK_ENABLE();
__HAL_RCC_GPIOC_CLK_ENABLE();
__HAL_RCC_GPIOD_CLK_ENABLE();
__HAL_RCC_GPIOE_CLK_ENABLE();
__HAL_RCC_GPIOF_CLK_ENABLE();
__HAL_RCC_GPIOG_CLK_ENABLE();
}
// 空闲任务钩子函数
void vApplicationIdleHook(void)
{
__disable_irq();
__dsb(0);
__isb(0);
before_sleep();
// 进入睡眠模式(等待中断唤醒)
__WFI();
after_wakeup();
__dsb(0);
__isb(0);
__enable_irq();
}
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 创建任务、队列、信号量等对象的时候, FreeRTOS 一般都提供了两种方法,一种是动态地申请创建对象时所需要的内存,这种方法也叫动态方法;一种是由用户自定义对象,在编译器编译程序的时候,会为已经在程序中定义好的对象分配一定的内存空间,这种方法也叫静态方法。
静态方法创建任务、队列、信号量等对象的 API 函数一般是以“Static”结尾的,例如静态创建任务的 API 函数 xTaskCreateStatic()。使用静态方式创建各种对象时,需要用户提供各种内存空间,例如任务的栈空间、任务控制块所用内存空间等等, 并且使用静态方式占用的内存空间一般固定下来了,即使任务、队列等被删除后,这些被占用的内存空间也没有其他用途。
在使用动态方式管理内存的时候, FreeRTOS 就能够在创建任务、队列、信号量等对象的时候,自动地从 FreeRTOS 管理的内存堆中申请所创建对象所需的内存,在对象被删除后,又可以将这块内存释放会 FreeRTOS 管理的内存堆,这样看来,动态方式管理内存相比与静态方式,显得灵活许多。
除了 FreeRTOS 提供的动态内存管理方法,标准的 C 库也提供了函数 malloc()和函数 free()来实现动态地申请和释放内存,但是标准 C 库的动态内存管理方法有如下几个缺点:
并不适用于所有嵌入式系统。
占用大量的代码空间。
没有线程安全的相关机制。
具有不确定性,体现在每次执行的时间不同。
……
为此, FreeRTOS 提供了动态方式管理内存的方法。不同的嵌入式系统对于动态内存管理的需求不同,因此 FreeRTOS 提供了多种内存管理算法选项,并将其作为 FreeRTOS 移植层的一部分,这样一来, FreeRTOS 的使用者就能够根据自己实际的需求选的何时的动态内存管理算法,并将其移植到系统中。
FreeRTOS 一共提供了 5 种动态内存管理算法,这 5 种动态内存管理算法本别对应了 5 个C 源文件,分别为: heap_1.c、 heap_2.c、 heap_3.c、 heap_4.c、 heap_5.c, 在后面小节中将会讲解这 5 种动态内存管理算法的异同。
2. FreeRTOS 内存管理算法
FreeRTOS提供了5种动态内存管理算法,分别为heap_1、heap_2、heap_3、heap_4和heap_5,这 5 种动态内存管理算法各自的特点如下所示:
2.1 heap_1.c
- 特点:
- 最简单的实现。
- 只支持内存分配 (
pvPortMalloc()
),不支持内存释放 (vPortFree()
)。 - 因此,不会产生内存碎片。
- 分配速度快,是确定性的。
- 适用场景:
- 内存分配只在系统初始化阶段进行,或者应用程序中分配的内存永远不会被释放。
- 适用于资源非常有限、对内存管理开销要求极高且可以预先确定所有内存需求的简单嵌入式系统。
2.2 heap_2.c
- 特点:
- 支持内存分配 (
pvPortMalloc()
) 和内存释放 (vPortFree()
)。 - 内存分配是确定性的,但内存释放不是确定性的,因为涉及搜索最佳匹配块。
- 使用“最佳适配”(best fit)算法来查找分配块。
- 释放内存时,不会自动合并相邻的空闲块,因此可能导致内存碎片。
- 支持内存分配 (
- 适用场景:
- 早期的 FreeRTOS 版本中经常使用。
- 适用于分配和释放操作数量有限,并且对内存碎片化容忍度较高,或者可以通过重启来解决碎片化问题的系统。
- 相对于
heap_4.c
,它在某些特定场景下可能会更快,但通常不作为首选。
2.3 heap_3.c
- 特点:
- 支持内存分配 (
pvPortMalloc()
) 和内存释放 (vPortFree()
)。 - 包装了标准库的
malloc()
和free()
函数。 - 线程安全:通过互斥量(Mutex)保护对
malloc()
和free()
的调用,使其在多任务环境中是安全的。 - 性能和碎片化特性取决于底层标准库的实现。
- 支持内存分配 (
- 适用场景:
- 当你的嵌入式系统有充足的 RAM 并且已经集成了标准 C 库的
malloc()
和free()
,并且希望利用这些成熟的实现时。 - 开发者不希望自己管理 FreeRTOS 堆的复杂性。
- 需要注意:标准库的
malloc()
和free()
通常不是为实时系统设计的,可能存在非确定性行为和性能瓶颈。
- 当你的嵌入式系统有充足的 RAM 并且已经集成了标准 C 库的
2.4 heap_4.c
- 特点:
- 支持内存分配 (
pvPortMalloc()
) 和内存释放 (vPortFree()
)。 - 最常用且推荐的通用内存管理方案。
- 使用“首次适配”(first fit)算法来查找分配块。
- 释放内存时,会自动合并相邻的空闲块,从而有效减少内存碎片。
- 分配和释放操作都不是严格确定性的(因为需要遍历空闲列表),但通常在大多数应用中性能良好。
- 支持内存分配 (
- 适用场景:
- 大多数需要动态内存分配和释放的 FreeRTOS 应用程序。
- 对内存碎片化有一定关注,希望堆空间能够高效重用的系统。
- 需要相对平衡的性能和内存效率。
2.5 heap_5.c
- 特点:
- 支持内存分配 (
pvPortMalloc()
) 和内存释放 (vPortFree()
)。 - 与
heap_4.c
类似,也支持合并相邻空闲块以减少碎片。 - 主要区别在于,
heap_5.c
允许将堆空间分布在多个不连续的内存区域。
- 支持内存分配 (
- 适用场景:
- 当硬件架构中 RAM 是非连续的,例如,部分 RAM 在内部,部分 RAM 在外部,或者 RAM 分布在不同的地址空间中。
- 需要整合多个独立的内存块形成一个逻辑上的大堆。
2.6 总结选择指南
特性 / 算法 | heap_1.c |
heap_2.c |
heap_3.c |
heap_4.c |
heap_5.c |
---|---|---|---|---|---|
内存分配 | 支持 | 支持 | 支持 | 支持 | 支持 |
内存释放 | 不支持 | 支持 | 支持 | 支持 | 支持 |
碎片化 | 无 | 存在可能 | 取决于StdLib | 较少(合并) | 较少(合并) |
确定性 | 分配确定 | 分配确定 | 取决于StdLib | 非确定 | 非确定 |
自动合并 | N/A | 否 | 取决于StdLib | 是 | 是 |
不连续内存 | 否 | 否 | 否 | 否 | 是 |
推荐度 | 特殊场景 | 不推荐 | 仅限特定场景 | 最常用 | 特殊硬件场景 |
在大多数 FreeRTOS 应用程序中,heap_4.c
是最常用且推荐的通用内存管理算法,因为它在内存碎片化和效率之间提供了很好的平衡。只有当有非常特定的需求(如不连续内存、绝对零碎片化或利用现有标准库)时,才考虑其他选项。
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);
/*---------------------------------------------*/
3.2 任务实现
/*------------------任务实现区------------------*/
void freertos_demo(void)
{
lcd_show_string(10,47,220,24,24,"Memory Management",RED);
lcd_show_string(30, 118, 200, 16, 16, "Total Mem: Bytes", RED);
lcd_show_string(30, 139, 200, 16, 16, "Free Mem: Bytes", RED);
lcd_show_string(30, 160, 200, 16, 16, "Malloc Addr:", 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(); // 进入临界区
// 创建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;
uint8_t *buf = NULL;
size_t free_size = 0;
while(1)
{
key_value = key_scan(0);
switch(key_value)
{
case KEY0_PRES:
{
buf = pvPortMalloc(30); // 分配30字节内存
sprintf((char*)buf, "0x%p",buf);
lcd_show_string(130,160,200,16,16,(char*)buf,BLUE);
break;
}
case KEY1_PRES:
{
if(NULL != buf)
{
vPortFree(buf); // 释放内存
buf = NULL;
}
break;
}
default:break;
}
lcd_show_xnum(114,118,configTOTAL_HEAP_SIZE,5,16,0,BLUE); // 显示总内存大小
free_size = xPortGetFreeHeapSize(); // 获取空闲内存大小
lcd_show_xnum(114,139,free_size,5,16,0,BLUE);
vTaskDelay(10);
}
}
/*---------------------------------------------*/
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例程 */
}
文中完整工程下载:https://github.com/hazy1k/FreeRTOS-Quick-Start-Guide/tree/main/2.code