HAL库版FreeRTOS(下)

FreeRTOS 事件标志组

事件标志组与信号量一样属于任务间同步的机制,但是信号量一般用于任务间的单事件同步,对于任务间的多事件同步,仅使用信号量就显得力不从心了。FreeRTOS 提供的事件标志组可以很好的处理多事件情况下的任务同步。本章就来学习FreeRTOS 中事件标志组的相关内容。

FreeRTOS 事件标志组简介

  1. 事件标志
    事件标志是一个用于指示事件是否发生的布尔值,一个事件标志只有0 或1 两种状态,FreeRTOS 将多个事件标志储存在一个变量类型为EventBits_t 变量中,这个变量就是事件组。
  2. 事件组
    事件组是一组事件标志的集合,一个事件组就包含了一个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 位中的每一位都是一个事件标志,当某一位被置一时,就表示这一位对应的事件发生了。

FreeRTOS 事件标志组相关API 函数

FreeRTOS 提供了事件标志组的一些相关操作函数,如下表所示:

在这里插入图片描述

  1. 创建事件标志组
    FreeRTOS 提供了两种创建事件标志组的方式,分别为动态方式创建事件标志组和静态方式创建事件标志组,两者的区别在于静态方式创建事件标志组时,需要用户提供创建事件标志组所需的内存空间,而使用动态方式创建事件标志组时,FreeRTOS 会自动从FreeRTOS 管理的堆中分配创建事件标志组所需的内存空间。
    动态方式创建事件标志组API 函数的函数原型如下所示:
EventGroupHandle_t xEventGroupCreate(void);

函数xEventGroupCreate()的形参描述,如下表所示:

在这里插入图片描述

静态方式创建事件标志组API 函数的函数原型如下所示:

EventGroupHandle_t xEventGroupCreateStatic(
StaticEventGroup_t * pxEventGroupBuffer);

函数xEventGroupCreateStatic()的形参描述,如下表所示:
在这里插入图片描述

  1. 删除事件标志组
    FreeRTOS 提供了用于删除事件标志组的API 函数,函数原型如下所示:
void vEventGroupDelete(EventGroupHandle_t xEventGroup);

在这里插入图片描述
3. 等待事件标志位
等待事件标志位使用的是函数xEventGroupWaitBits(),其函数原型如下所示:

EventBits_t xEventGroupWaitBits(
EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToWaitFor,
const BaseType_t xClearOnExit,
const BaseType_t xWaitForAllBits,
TickType_t xTicksToWait)

在这里插入图片描述

  1. 设置事件标志位
    FreeRTOS 提供了两个用于设置事件标志位的API 函数,这个两个函数分别用于在任务和在中断中设置事件标志位。
    在任务中设置事件标志位API 函数的函数原型如下所示:
EventBits_t xEventGroupSetBits(
EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToSet)

在这里插入图片描述
在中断中设置事件标志位API 函数的函数原型如下所示:

BaseType_t xEventGroupSetBitsFromISR(
EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToSet,
BaseType_t * pxHigherPriorityTaskWoken)

函数xEventGroupSetBitsFromISR()的形参描述,如下表所示:

在这里插入图片描述

  1. 清零事件标志位
    FreeRTOS 提供了两个用于清零事件标志位的API 函数,这个两个函数分别用于在任务和在中断中清零事件标志位。
    在任务中清零事件标志位API 函数的函数原型如下所示:
EventBits_t xEventGroupClearBits(
EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToClear)

函数xEventGroupClearBits()的形参描述,如下表所示:

在这里插入图片描述
函数xEventGroupClearBits()的返回值,如下表所示:
在这里插入图片描述

在中断中清零事件标志位API 函数的函数原型如下所示:

BaseType_t xEventGroupClearBitsFromISR(
EventGroupHandle_t EventGroup,
const EventBits_t uxBitsToClear)

在这里插入图片描述

  1. 获取事件组中事件标志位的值
    FreeRTOS 提供了两个用于获取事件组中事件标志位值的API 函数,这个两个函数分别用于在任务和在中断中获取事件组中事件标志位的值。
    在任务中获取事件组中事件标志位值API 函数的函数原型如下所示:
EventBits_t xEventGroupGetBits(xEventGroup);

在这里插入图片描述

  1. 函数xEventGroupSync()
    此函数一般用于多任务同步,其中每个任务都必须等待其他任务达到同步点,然后才能继续执行。函数xEventGroupSync()的函数原型如下所示:
EventBits_t xEventGroupSync(
EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToSet,
const EventBits_t uxBitsToWaitFor,
TickType_t xTicksToWait)

在这里插入图片描述

FreeRTOS 事件标志组实验

功能设计

  1. 例程功能
    本实验主要用于学习FreeRTOS 事件标志组相关API 函数的使用,本实验设计了四个任务,这四个任务的功能如下表所示:
    在这里插入图片描述
    该实验的实验工程,请参考《FreeRTOS 实验例程16 FreeRTOS 事件标志组实验》。

软件设计

  1. 程序流程图
    本实验的程序流程图,如下图所示:

在这里插入图片描述
2. 程序解析
整体的代码结构,请参考2.1.6 小节,本小节着重讲解本实验相关的部分。
(1) start_task 任务
start_task 任务的入口函数代码如下所示:

/**
 * @brief start_task
 * @param pvParameters : 传入参数(未用到)
 * @retval 无
 */
void start_task(void * pvParameters) {
    taskENTER_CRITICAL(); /* 进入临界区*/
    /* 创建事件标志组*/
    EventGroupHandler = xEventGroupCreate();
    /* 创建任务1 */
    xTaskCreate((TaskFunction_t) task1, (const char * )
        "task1", (uint16_t) TASK1_STK_SIZE, (void * ) NULL, (UBaseType_t) TASK1_PRIO, (TaskHandle_t * ) & Task1Task_Handler);
    /* 创建任务2 */
    xTaskCreate((TaskFunction_t) task2, (const char * )
        "task2", (uint16_t) TASK2_STK_SIZE, (void * ) NULL, (UBaseType_t) TASK2_PRIO, (TaskHandle_t * ) & Task2Task_Handler);
    /* 创建任务3 */
    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(); /* 退出临界区*/
}

start_task 任务主要用于创建事件标志组、task1 任务、task2 任务和task3 任务。
(2) task1 任务

/**
 * @brief task1
 * @param pvParameters : 传入参数(未用到)
 * @retval 无
 */
void task1(void * pvParameters) {
    uint8_t key = 0;
    while (1) {
        key = key_scan(0);
        switch (key) {
            case KEY0_PRES:
                {
                    /* 设置事件组的事件标志0 */
                    xEventGroupSetBits((EventGroupHandle_t) EventGroupHandler, (EventBits_t) EVENTBIT_0);
                    break;
                }
            case KEY1_PRES:
                {
                    /* 设置事件组的事件标志1 */
                    xEventGroupSetBits((EventGroupHandle_t) EventGroupHandler, (EventBits_t) EVENTBIT_1);
                    break;
                }
            default:
                {
                    break;
                }
        }
        vTaskDelay(10);
    }
}

task1 任务主要用于扫描按键,当按下按键0 时,设置事件组的事件标志0,当按下按键1时,设置事件组的事件标志1。
(3) task2 任务

/**
 * @brief task2
 * @param pvParameters : 传入参数(未用到)
 * @retval 无
 */
void task2(void * pvParameters) {
    uint32_t task2_num = 0;
    while (1) {
        /* 等待事件标志0和事件标志1
         * 如果成功等待到了事件标志0和1,则清零事件标志0和1
         * 等待的方式为逻辑与,即事件标志0和1需要被同时设置
         */
        xEventGroupWaitBits((EventGroupHandle_t) EventGroupHandler, (EventBits_t) EVENTBIT_ALL, (BaseType_t) pdTRUE, (BaseType_t) pdTRUE, (TickType_t) portMAX_DELAY);
        /* LCD区域刷新*/
        lcd_fill(6, 131, 233, 313, lcd_discolor[++task2_num % 11]);
        vTaskDelay(10);
    }
}

可以看到task2 任务等待事件组中的事件标志0 和1 同时被设置,只有事件组中的视角标志0 和1 被同时设置,task2 任务才会继续执行,同时如果成功等待到了事件标志0 和1 被同时设置,那么还会自动清零事件组中的事件标志0 和1,无需手动清零。
(4) task3 任务

/**
 * @brief task3
 * @param pvParameters : 传入参数(未用到)
 * @retval 无
 */
void task3(void * pvParameters) {
    EventBits_t event_val = 0;
    while (1) {
        /* 获取事件组的所有事件标志值*/
        event_val = xEventGroupGetBits((EventGroupHandle_t) EventGroupHandler);
        /* 在LCD上显示事件值*/
        lcd_show_xnum(182, 110, event_val, 1, 16, 0, BLUE);
        vTaskDelay(10);
    }
}

从上面的代码可以看出,task3 任务获取了事件组的事件标志值,并将事件组的事件标志值实时显示到LCD 屏幕上。

下载验证

编译并下载代码,复位后可以看到LCD 屏幕上显示了本次实验的相关信息,如下图所示:

在这里插入图片描述
一开始可以看到,事件组的事件标志值为0。此时按键按键0,来设置事件组的事件标志0,可以看到LCD 上显示的事件组事件标志值为1,此值正是事件标志0 被设置后的值,如下图所示:
图16.3.3.2 LCD 显示内容二

接着多次按下按键0,LCD 显示的内容都不会发生变化,因为事件标志0 已经被设置了。接着按键按键1,来设置事件组的事件标志1,可以看到LCD 上显示的事件组事件标志值直接清零,并且LCD 屏幕区域也刷新了,如下图所示:

在这里插入图片描述
这是因为,当事件标志1 被设置后,task2 任务立马就等待到了事件标志0 和1 同时被设置,因此task2 任务就将事件组的事件标志0 和1 清零了,并且task2 解除阻塞得以运行,所以LCD 屏幕区域被刷新了。

在本次实验中,不论按键0 或者按键1 中的哪一个按键先被按下,即不论事件组中的事件标志0 还是事件标志1 中的哪一个先被设置,只要事件标志0 和事件标志1 存在同时处于被设置的状态,那么task2 任务就等待事件组中的事件标志成功,于是task2 任务解除阻塞,得以执行。

FreeRTOS 任务通知

FreeRTOS 内核V8.2.0 版本发布的时候,FreeRTOS 新增了任务通知这个功能,任务通知也是用于任务间进行同步和通讯的一种机制,但是相对于前面章节介绍的队列、事件标志组和信号量等而言,任务通知在内存占用和效率方面都有很大的优势。本章就来学习FreeRTOS 中任务通知的相关内容。

FreeRTOS 任务通知简介

在FreeRTOS 中,每一个任务都有两个用于任务通知功能的数组,分别为任务通知数组和任务通知状态数组。其中任务通知数组中的每一个元素都是一个32 位无符号类型的通知值;而任务通知状态数组中的元素则表示与之对应的任务通知的状态。

任务通知数组中的32 位无符号通知值,用于任务到任务或中断到任务发送通知的“媒介”。
当通知值为0 时,表示没有任务通知;当通知值不为0 时,表示有任务通知,并且通知值就是通知的内容。

任务通知状态数组中的元素,用于标记任务通知数组中通知的状态,任务通知有三种状态,分别为未等待通知状态、等待通知状态和等待接收通知状态。其中未等待通知状态为任务通知的复位状态;当任务在没有通知的时候接收通知时,在任务阻塞等待任务通知的这段时间内,任务所等待的任务通知就处于等待通知状态;当有其他任务向任务发送通知,但任务还未接收这一通知的这段期间内,任务通知就处于等待接收通知状态。

任务通知功能所使用到的任务通知数组和任务通知状态数组为任务控制块中的成员变量,因此任务通知的传输是直接传出到任务中的,不同通过任务的通讯对象(队列、事件标志组和信号量就属于通讯对象)这个间接的方式。间接通讯示意图如下所示:

在这里插入图片描述
任务通知则是直接地往任务中发送通知,直接通讯示意图如下所示:
在这里插入图片描述

任务通知的优势

使用任务通知向任务发送事件或数据比使用队列、事件标志组或信号量快得多;并且使用任务通知代替队列、事件标志组或信号量,可以节省大量的内存,这是因为每个通讯对象在使用之前都需要被创建,而任务通知功能中的每个通知只需要在每个任务中占用固定的5 字节内存。

任务通知的缺点

虽然任务通知功能相比通讯对象,有着更快、占用内存少的优点,但是任务通知功能并不能适用于所有情况,例如以下列出的几种情况:

  1. 发送事件或数据到中断
    通讯对象可以发送事件或数据从中断到任务,或从任务到中断,但是由于任务通知依赖于任务控制块中的两个成员变量,并且中断不是任务,因此任务通知功能并不适用于从任务往中断发送事件或数据的这种情况,但是任务通知功能可以在任务之间或从中断到任务发送事件或数据。
  2. 存在多个接收任务
    通讯对象可以被已知通讯对象句柄的任意多个任务或中断访问(发送或接收),但任务通知是直接发送事件或数据到指定接收任务的,因传输的事件或数据只能由接收任务处理。然而在实际中很少受到这种情况的限制,因为,虽然多个任务和中断发送事件或数据到一个通讯对象是很常见的,但很少出现多个任务或中断接收同一个通讯对象的情况。
  3. 缓冲多个数据项
    通讯对象中的队列是可以一次性保存多个已经被发送到队列,但还未被接收的事件或数据的,也就是说,通讯对象有着一定的缓冲多个数据的能力,但是任务通知是通过更新任务通知值来发送事件或数据的,一个任务通知值只能保存一次。
  4. 广播到多个任务
    通讯对象中的事件标志组是可以将一个事件同时发送到多个任务中的,但任务通知只能是被指定的一个接收任务接收并处理。
  5. 阻塞等待接收任务
    当通讯对象处于暂时无法写入的状态(例如队列已满,此时无法再向队列写入消息)时,发送任务是可以选择阻塞等待接收任务接收,但是任务因尝试发送任务通知到已有任务通知但还未处理的任务而进行阻塞等待的。但是任务通知也很少在实际情况中收到这种情况的限制。

FreeRTOS 任务通知相关API 函数

FreeRTOS 提供了任务通知的一些相关操作函数,其中任务通知相关API 函数,如下两表所示:

在这里插入图片描述
在这里插入图片描述
以上两表列出了FreeRTOS 提供的几个任务通知相关的操作函数,从第17.1 小节《FreeRTOS任务通知简介》中,可以知道任务的任务控制块中,与任务通知功能相关的两个成员变量,任务通知值和任务通知状态,是两个数组,也就是说,一个任务可以有多个任务通知,多个通知就通过数组的下标进行索引。
表17.2.1 所列出的API 函数都是对任务通知相关数组中下标为0 的元素进行操作,而表17.2.2 中列出的API 函数可以指定对任务通知相关数组中的元素进行操作。两表中对应的API函数原理上是一样的,只是表17.2.1 中的API 是固定对任务的任务通知0 进行操作,而表17.2.2中的API 函数可以对任务的指定任务通知进行操作,本文以表17.2.1 中的函数为例进行讲解。

  1. 发送任务通知
    表17.2.1 中发送任务通知的三个API 函数的定义如下所示:
#
define xTaskNotify(xTaskToNotify, \
    ulValue, \
    eAction)\
xTaskGenericNotify((xTaskToNotify), \ (tskDEFAULT_INDEX_TO_NOTIFY), \ (ulValue), \ (eAction), \
    NULL)# define xTaskNotifyAndQuery(xTaskToNotify, \
    ulValue, \
    eAction, \
    pulPreviousNotifyValue)\
xTaskGenericNotify((xTaskToNotify), \ (tskDEFAULT_INDEX_TO_NOTIFY), \ (ulValue), \ (eAction), \ (pulPreviousNotifyValue))# define xTaskNotifyGive(xTaskToNotify)\
xTaskGenericNotify((xTaskToNotify), \ (tskDEFAULT_INDEX_TO_NOTIFY), \ (0), \
    eIncrement, \
    NULL)

从上面的代码中可以看出,三个用于在任务中发送任务通知的函数,实际上都是调用了函数xTaskGenericNotify()来发送任务通知的,只是传入了不同的参数。函数xTaskGenericNotify()的函数原型如下所示:

BaseType_t xTaskGenericNotify(TaskHandle_t xTaskToNotify,
    UBaseType_t uxIndexToNotify,
    uint32_t ulValue,
    eNotifyAction eAction,
    uint32_t * pulPreviousNotificationValue);

函数xTaskGenericNotify()的形参描述,如下表所示:
在这里插入图片描述
函数xTaskGenericNotify()的返回值,如下表所示:
在这里插入图片描述
函数xTaskGenericNotify()的源代码如下所示:
。。。。

结合函数xTaskNotify()、函数xTaskNotifyAndQuery()、函数xTaskNotifyGive()的定义和以上代码,可以知道函数xTaskNotify()、函数xTaskNotifyAndQuery()、函数xTaskNotifyGive()的作用如下所示:
函数xTaskNotify():
此函数用于往指定任务发送任务通知,通知方式可以自由指定,并且不获取发送任务通知前任务通知的通知值。
函数xTaskNotifyAndQuery():
此函数用于往指定任务发送任务通知,通知方式可以自由指定,并且获取发送任务通知前任务通知的通知值。
函数xTaskNotifyGive():
此函数用于往指定任务发送任务通知,通知方式为将通知值加1,并且不获取发送任务通知前任务通知的通知值。

  1. 在中断中发送任务通知
    表17.2.1 中在中断中发送任务通知的三个API 函数的定义如下所示:
#define xTaskNotifyFromISR( xTaskToNotify, \
ulValue, \
eAction, \
pxHigherPriorityTaskWoken) \
xTaskGenericNotifyFromISR( (xTaskToNotify), \
(tskDEFAULT_INDEX_TO_NOTIFY), \
(ulValue), \
(eAction), \
NULL, \
(pxHigherPriorityTaskWoken))
#define xTaskNotifyAndQueryFromISR( xTaskToNotify, \
ulValue, \
eAction, \
pulPreviousNotificationValue, \
pxHigherPriorityTaskWoken) \
xTaskGenericNotifyFromISR( (xTaskToNotify), \
(tskDEFAULT_INDEX_TO_NOTIFY), \
(ulValue), \
(eAction), \
(pulPreviousNotificationValue), \
(pxHigherPriorityTaskWoken))
#define vTaskNotifyGiveFromISR( xTaskToNotify, \
pxHigherPriorityTaskWoken ) \
vTaskGenericNotifyGiveFromISR( (xTaskToNotify), \
(tskDEFAULT_INDEX_TO_NOTIFY), \
( pxHigherPriorityTaskWoken));

从上面的代码可以看出,函数xTaskNotifyFromISR()和函数xTaskNotifyAndQueryFromISR()实际上都是调用了函数xTaskGenericNotifyFromISR(),而函数vTaskNotifyGiveFromISR()实际上则是调用了函数vTaskGenericNotifyGiveFromISR()。下面就分别看一下以上这两个实际被调用的函数。
函数xTaskGenericNotifyFromISR()的函数原型如下所示:

BaseType_t xTaskGenericNotifyFromISR(
TaskHandle_t xTaskToNotify,
UBaseType_t uxIndexToNotify,
uint32_t ulValue,
eNotifyAction eAction,
uint32_t * pulPreviousNotificationValue,
BaseType_t * pxHigherPriorityTaskWoken);

在这里插入图片描述
函数xTaskGenericNotifyFromISR()的源代码如下所示:
。。。。

从上面的代码中可以看出,函数xTaskGenericNotifyFromISR()于函数xTaskNotify()是很相似的,只是多了对中断做了一些相应的处理。
函数vTaskGenericNotifyGiveFromISR()的函数原型如下所示:

void vTaskGenericNotifyGiveFromISR(
TaskHandle_t xTaskToNotify,
UBaseType_t uxIndexToNotify,
BaseType_t * pxHigherPriorityTaskWoken);

在这里插入图片描述
函数vTaskGenericNotifyGiveFromISR()的源代码如下所示:
。。。。

从以上代码中可以看出,函数vTaskGenericNotifyGiveFromISR()就是通知方式为eIncrement并且没有返回值的函数xTaskGenericNotifyFromISR()。
结合以上函数xTaskGenericNotifyFromISR()和函数vTaskGenericNotifyGiveFromISR()的源代码和函数xTaskNotifyFromISR() 和函数xTaskNotifyAndQueryFromISR() 和函数vTaskNotifyGiveFromISR()的定义,表17.2.1 中列出的三个在中断中发送任务通知的API 函数作用如下:
函数xTaskNotifyFromISR()
此函数用于在中断中往指定任务发送任务通知,通知方式可以自由指定,并且不获取发送任务通知前任务通知的通知值,但获取发送通知后是否需要进行任务切换的标志。
函数xTaskNotifyAndQueryFromISR()
此函数用于在中断中往指定任务发送任务通知,通知方式可以自由指定,并且获取发送任务通知前任务通知的通知值,和发送通知后是否需要进行任务切换的标志。
函数vTaskNotifyGiveFromISR()
此函数用于在中断中往指定任务发送任务通知,通知方式为将通知值加1,并且不获取发送任务通知前任务通知的通知值,但获取发送通知后是否需要进行任务切换的标志。

  1. 接收任务通知
    用于获取任务通知的API 函数有两个,分别为函数ulTaskNotifyTake() 和函数xTaskNotifyWait()。
    函数ulTaskNotifyTake()
    此函数用于获取任务通知的通知值,并且在成功获取任务通知的通知值后,可以指定将通知值清零或减1。此函数实际上是一个宏定义,具体的代码如下所示:
#define ulTaskNotifyTake(xClearCountOnExit, \
    xTicksToWait)\
ulTaskGenericNotifyTake((tskDEFAULT_INDEX_TO_NOTIFY), \ (xClearCountOnExit), \ (xTicksToWait))

从上面的代码中可以看出,函数ulTaskNotifyTake() 实际上是调用了函数ulTaskGenericNotifyTake(),函数ulTaskGenericNotifyTake()的函数原型如下所示:

uint32_t ulTaskGenericNotifyTake( UBaseType_t uxIndexToWaitOn,
BaseType_t xClearCountOnExit,
TickType_t xTicksToWait);

在这里插入图片描述
函数ulTaskGenericNotifyTake()的源代码如下所示:
。。。

函数xTaskNotifyWait()
此函数用于等待任务通知通知值中的指定比特位被置一,此函数可以在等待前和成功等待到任务通知通知值中的指定比特位被置一后清零指定比特位,并且还能获取等待超时后任务通知的通知值。此函数实际上是一个宏定义,具体的代码如下所示:

#define xTaskNotifyWait( ulBitsToClearOnEntry, \
ulBitsToClearOnExit, \
pulNotificationValue, \
xTicksToWait) \
xTaskGenericNotifyWait( tskDEFAULT_INDEX_TO_NOTIFY, \
(ulBitsToClearOnEntry), \
(ulBitsToClearOnExit), \
(pulNotificationValue), \
(xTicksToWait))

从上面的代码中可以看出,函数xTaskNotifyWait() 实际上是调用了函数xTaskGenericNotifyWait(),函数xTaskGenericNotifyWait()的函数原型如下所示:

BaseType_t xTaskGenericNotifyWait( UBaseType_t uxIndexToWaitOn,
uint32_t ulBitsToClearOnEntry,
uint32_t ulBitsToClearOnExit,
uint32_t * pulNotificationValue,
TickType_t xTicksToWait);

在这里插入图片描述
函数xTaskGenericNotifyWait()的源代码如下所示:
。。。

FreeRTOS 任务通知模拟二值信号量实验

功能设计

  1. 例程功能
    本实验主要用于学习使用FreeRTOS 中的任务通知功能模拟二值信号量,本实验设计了三个任务,这三个任务的功能如下表所示:
    在这里插入图片描述
    该实验的实验工程,请参考《FreeRTOS 实验例程17-1 FreeRTOS 任务通知模拟二值信号量实验》。

软件设计

  1. 程序流程图
    本实验的程序流程图,如下图所示:
    在这里插入图片描述
  2. 程序解析
    整体的代码结构,请参考2.1.6 小节,本小节着重讲解本实验相关的部分。
    (1) start_task 任务
    start_task 任务的入口函数代码如下所示:
/**
 * @brief start_task
 * @param pvParameters : 传入参数(未用到)
 * @retval 无
 */
void start_task(void * pvParameters) {
    taskENTER_CRITICAL(); /* 进入临界区*/
    /* 创建任务1 */
    xTaskCreate((TaskFunction_t) task1, (const char * )
        "task1", (uint16_t) TASK1_STK_SIZE, (void * ) NULL, (UBaseType_t) TASK1_PRIO, (TaskHandle_t * ) & Task1Task_Handler);
    /* 创建任务2 */
    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(); /* 退出临界区*/
}

start_task 任务主要用于创建task1 任务和task2 任务。
(2) task1 任务

/**
 * @brief task1
 * @param pvParameters : 传入参数(未用到)
 * @retval 无
 */
void task1(void * pvParameters) {
    uint8_t key = 0;
    while (1) {
        if (Task2Task_Handler != NULL) {
            key = key_scan(0);
            switch (key) {
                case KEY0_PRES:
                    {
                        /* 发送任务通知*/
                        xTaskNotifyGive((TaskHandle_t) Task2Task_Handler);
                        break;
                    }
                default:
                    {
                        break;
                    }
            }
        }
        vTaskDelay(10);
    }
}

task1 任务主要用于扫描按键,当按下按键0 时,往task2 任务发送任务通知。
(3) task2 任务

/**
 * @brief task2
 * @param pvParameters : 传入参数(未用到)
 * @retval 无
 */
void task2(void * pvParameters) {
    uint32_t notify_val = 0;
    uint32_t task2_num = 0;
    while (1) {
        /* 接收任务通知,
         * 并在接收到任务通知后,将任务通知的通知值清零,
         * 类似于二值信号量
         */
        notify_val = ulTaskNotifyTake((BaseType_t) pdTRUE, (TickType_t) portMAX_DELAY);
        if (notify_val != 0) {
            /* LCD区域刷新*/
            lcd_fill(6, 131, 233, 313, lcd_discolor[++task2_num % 11]);
        }
    }
}

可以看到task2 任务会接收任务通知,并且在成功接收到任务通知后,会将任务通知的通知值清零,那么任务通知的通知值就只存在两种状态,就是0 和非0,这也就是模拟二值信号量的关键。接着还会判断任务通知的通知值是否为零,如果任务通知的通知值为0,说明接收任务通知失败,只有在任务通知的通知值不为0,即成功地接收地任务通知后,才会执行刷新LCD 区域的操作。

下载验证

编译并下载代码,复位后可以看到LCD 屏幕上显示了本次实验的相关信息,如下图所示:

在这里插入图片描述
此时按下按键0,让task1 任务发送任务通知到task2 任务,可以看到,LCD 发生了变化,如下图所示:
在这里插入图片描述
可以看放LCD 的区域刷新了颜色,这是因为task2 任务成功接收到任务通知后执行的操作。
多次按下按键0,LCD 区域都会刷新颜色,这与预计的实验结果相符。

FreeRTOS 任务通知模拟计数型信号量实验

功能设计

  1. 例程功能
    本实验主要用于学习使用FreeRTOS 中的任务通知功能模拟计数型信号量,本实验设计了三个任务,这三个任务的功能如下表所示:
    在这里插入图片描述
    该实验的实验工程,请参考《FreeRTOS 实验例程17-2 FreeRTOS 任务通知模拟计数型信号量实验》。

软件设计

  1. 程序流程图
    本实验的程序流程图,如下图所示:
    在这里插入图片描述
  2. 程序解析
    整体的代码结构,请参考2.1.6 小节,本小节着重讲解本实验相关的部分。
    (1) start_task 任务
    start_task 任务的入口函数代码如下所示:
/**
 * @brief start_task
 * @param pvParameters : 传入参数(未用到)
 * @retval 无
 */
void start_task(void * pvParameters) {
    taskENTER_CRITICAL(); /* 进入临界区*/
    /* 创建任务1 */
    xTaskCreate((TaskFunction_t) task1, (const char * )
        "task1", (uint16_t) TASK1_STK_SIZE, (void * ) NULL, (UBaseType_t) TASK1_PRIO, (TaskHandle_t * ) & Task1Task_Handler);
    /* 创建任务2 */
    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(); /* 退出临界区*/
}

start_task 任务主要用于创建task1 任务和task2 任务。
(2) task1 任务

/**
 * @brief task1
 * @param pvParameters : 传入参数(未用到)
 * @retval 无
 */
void task1(void * pvParameters) {
    uint8_t key = 0;
    while (1) {
        key = key_scan(0);
        if (Task2Task_Handler != NULL) {
            switch (key) {
                case KEY0_PRES:
                    {
                        /* 发送任务通知*/
                        xTaskNotifyGive((TaskHandle_t) Task2Task_Handler);
                        break;
                    }
                default:
                    {
                        break;
                    }
            }
        }
        vTaskDelay(10);
    }
}

task1 任务主要用于扫描按键,当按下按键0 时,往task2 任务发送任务通知。
(3) task2 任务

/**
 * @brief task2
 * @param pvParameters : 传入参数(未用到)
 * @retval 无
 */
void task2(void * pvParameters) {
    uint32_t notify_val = 0;
    uint32_t task2_num = 0;
    while (1) {
        /* 接收任务通知,
         * 并在接收任务通知后,将任务通知的通知值减1
         * 类似于计数型信号量
         */
        notify_val = ulTaskNotifyTake((BaseType_t) pdFALSE, (TickType_t) portMAX_DELAY);
        /* 在LCD上显示任务通知的通知值*/
        lcd_show_xnum(166, 111, notify_val - 1, 2, 16, 0, BLUE);
        /* LCD区域刷新*/
        lcd_fill(6, 131, 233, 313, lcd_discolor[++task2_num % 11]);
        vTaskDelay(1000);
    }
}

可以看到task2 任务会接收任务通知,并且在成功接收到任务通知后,会将任务通知的通知值减1,这也就是模拟计数型信号量的关键。接着还会在LCD 上显示任务通知的通知值,并刷新LCD 区域的颜色。

下载验证

编译并下载代码,复位后可以看到LCD 屏幕上显示了本次实验的相关信息,如下图所示:
在这里插入图片描述
此时按下按键0,让task1 任务发送任务通知到task2 任务,可以看到,LCD 发生了变化,如下图所示:
在这里插入图片描述
可以看放LCD 的区域刷新了颜色,这是因为task2 任务成功接收到任务通知后执行的操作,同时LCD 屏幕上显示了任务通知的通知值为0,这是因为任务通知的通知值已经被获取而从1减1 为0 了。快速地多次按下按键0,可以看到,任务通知的通知值变大,并且LCD 区域会自动刷新,每刷新一次任务通知的通知值就减1,直到任务通知的通知值减到0 为止,如下图所示:
在这里插入图片描述
LCD 区域会已知刷新颜色,直到任务通知的通知值为0,这与预期的实验结果相符。

FreeRTOS 任务通知模拟消息邮箱实验

功能设计

  1. 例程功能
    本实验主要用于学习使用FreeRTOS 中的任务通知功能模拟消息邮箱,本实验设计了三个任务,这三个任务的功能如下表所示:
    在这里插入图片描述
    该实验的实验工程,请参考《FreeRTOS 实验例程17-3 FreeRTOS 任务通知模拟消息邮箱实验》。

软件设计

  1. 程序流程图
    本实验的程序流程图,如下图所示:
    在这里插入图片描述
  2. 程序解析
    整体的代码结构,请参考2.1.6 小节,本小节着重讲解本实验相关的部分。
    (1) start_task 任务
    start_task 任务的入口函数代码如下所示:
/**
 * @brief start_task
 * @param pvParameters : 传入参数(未用到)
 * @retval 无
 */
void start_task(void * pvParameters) {
    taskENTER_CRITICAL(); /* 进入临界区*/
    /* 创建任务1 */
    xTaskCreate((TaskFunction_t) task1, (const char * )
        "task1", (uint16_t) TASK1_STK_SIZE, (void * ) NULL, (UBaseType_t) TASK1_PRIO, (TaskHandle_t * ) & Task1Task_Handler);
    /* 创建任务2 */
    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(); /* 退出临界区*/
}

start_task 任务主要用于创建task1 任务和task2 任务。
(2) task1 任务

/**
 * @brief task1
 * @param pvParameters : 传入参数(未用到)
 * @retval 无
 */
void task1(void * pvParameters) {
    uint8_t key = 0;
    while (1) {
        key = key_scan(0);
        if ((Task2Task_Handler != NULL) && (key != 0)) {
            /* 以键值作为通知值向task2任务发送任务通知*/
            xTaskNotify((TaskHandle_t) Task2Task_Handler, (uint32_t) key, (eNotifyAction) eSetValueWithOverwrite);
        }
        vTaskDelay(10);
    }
}

task1 任务主要用于扫描按键,当按下按键时,将键值作为通知值向task2 任务发送任务通知。
(3) task2 任务

/**
 * @brief task2
 * @param pvParameters : 传入参数(未用到)
 * @retval 无
 */
void task2(void * pvParameters) {
    uint32_t notify_val = 0;
    uint32_t task2_num = 0;
    while (1) {
        /* 接收任务通知,
         * 并在成功接收到任务通知后,
         * 清零任务通知通知值
         */
        xTaskNotifyWait((uint32_t) 0x00000000, (uint32_t) 0xFFFFFFFF, (uint32_t * ) & notify_val, (TickType_t) portMAX_DELAY);
        switch (notify_val) {
            case KEY0_PRES:
                {
                    /* LCD区域刷新*/
                    lcd_fill(6, 126, 233, 313, lcd_discolor[++task2_num % 11]);
                    break;
                }
            case KEY1_PRES:
                {
                    /* LED0闪烁*/
                    LED0_TOGGLE();
                    break;
                }
            default:
                {
                    break;
                }
        }
    }
}

可以看到task2 任务会接收任务通知,并且在成功接收到任务通知后,会将任务通知的通知值清零,然后将接收到的任务通知通知值作为键值进行解析,做相应的解释。

下载验证

编译并下载代码,复位后可以看到LCD 屏幕上显示了本次实验的相关信息,如下图所示:
在这里插入图片描述

接着按键按键0,可以看到LCD 的区域颜色刷新了,这是因为,当task1 任务扫描到按键 0 被按下后,往task2 任务发送任务通知,任务通知值的通知值就是按键0 的键值,当task2 任务接收到按键0 键值的任务通知后,task2 任务就知道按键0 被按下了,因此就执行了刷新LCD区域颜色的操作,如下图所示:

在这里插入图片描述

接着按下按键1,同样的,task2 任务接收到了通知值为按键1 键值的任务通知,因此执行改变LED0 状态的操作,这与预期的实验结果相符。

FreeRTOS 任务通知模拟事件标志组实验

功能设计

  1. 例程功能
    本实验主要用于学习使用FreeRTOS 中的任务通知功能模拟事件标志组,本实验设计了三
    个任务,这三个任务的功能如下表所示:
    在这里插入图片描述
    该实验的实验工程,请参考《FreeRTOS 实验例程17-4 FreeRTOS 任务通知模拟时间标志组实验》。

软件设计

  1. 程序流程图
    本实验的程序流程图,如下图所示:
    在这里插入图片描述
  2. 程序解析
    整体的代码结构,请参考2.1.6 小节,本小节着重讲解本实验相关的部分。
    (1) start_task 任务
    start_task 任务的入口函数代码如下所示:
/**
 * @brief start_task
 * @param pvParameters : 传入参数(未用到)
 * @retval 无
 */
void start_task(void * pvParameters) {
    taskENTER_CRITICAL(); /* 进入临界区*/
    /* 创建任务1 */
    xTaskCreate((TaskFunction_t) task1, (const char * )
        "task1", (uint16_t) TASK1_STK_SIZE, (void * ) NULL, (UBaseType_t) TASK1_PRIO, (TaskHandle_t * ) & Task1Task_Handler);
    /* 创建任务2 */
    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(); /* 退出临界区*/
}

start_task 任务主要用于创建task1 任务和task2 任务。
(2) task1 任务

/**
 * @brief task1
 * @param pvParameters : 传入参数(未用到)
 * @retval 无
 */
void task1(void * pvParameters) {
    uint8_t key = 0;
    while (1) {
        if (Task2Task_Handler != NULL) {
            key = key_scan(0);
            switch (key) {
                case KEY0_PRES:
                    {
                        /* 发送事件标志0 */
                        xTaskNotify((TaskHandle_t) Task2Task_Handler, (uint32_t) EVENTBIT_0, (eNotifyAction) eSetBits);
                        break;
                    }
                case KEY1_PRES:
                    {
                        /* 发送事件标志1 */
                        xTaskNotify((TaskHandle_t) Task2Task_Handler, (uint32_t) EVENTBIT_1, (eNotifyAction) eSetBits);
                        break;
                    }
                default:
                    {
                        break;
                    }
            }
        }
        vTaskDelay(10);
    }
}

task1 任务主要用于扫描按键,当按下按键时,将事件标志0 或事件标志1 作为通知值向task2 任务发送任务通知。
(3) task2 任务

/**
 * @brief task2
 * @param pvParameters : 传入参数(未用到)
 * @retval 无
 */
void task2(void * pvParameters) {
    uint32_t notify_val = 0;
    uint32_t event_val = 0;
    uint32_t task2_num = 0;
    while (1) {
        /* 阻塞接收任务通知*/
        xTaskNotifyWait((uint32_t) 0x00000000, (uint32_t) 0xFFFFFFFF, (uint32_t * ) & notify_val, (TickType_t) portMAX_DELAY);
        if (notify_val & EVENTBIT_0) {
            /* 标记接收到事件0 */
            event_val |= EVENTBIT_0;
        } else if (notify_val & EVENTBIT_1) {
            /* 标记接收到事件1 */
            event_val |= EVENTBIT_1;
        }
        /* LCD上显示事件值*/
        lcd_show_xnum(182, 110, event_val, 1, 16, 0, BLUE);
        if (event_val == EVENTBIT_ALL) {
            /* 事件标记清零*/
            event_val = 0;
            /* LCD区域刷新*/
            lcd_fill(6, 131, 233, 313, lcd_discolor[++task2_num % 11]);
        }
    }
}

可以看到task2 任务会接收任务通知,并且在成功接收到任务通知后,会将任务通知的通知值清零,然后将接收到的任务通知通知值作为事件存入事件标记中,并在LCD 上实时显示事件值,当事件0 和事件1 通知发生时,刷新LCD 区域显示。

下载验证

编译并下载代码,复位后可以看到LCD 屏幕上显示了本次实验的相关信息,如下图所示:
在这里插入图片描述
一开始可以看到,模拟事件组的事件标志值为0。此时按键按键0,来设置模拟事件组的事件标志0,可以看到LCD 上显示的模拟事件组事件标志值为1,此值正是事件标志0 被设置后的值,如下图所示:
在这里插入图片描述
接着多次按下按键0,LCD 显示的内容都不会发生变化,因为事件标志0 已经被设置了。
接着按键按键1,来设置模拟事件组的事件标志1,可以看到LCD 屏幕区域刷新了,如下图所示:
在这里插入图片描述
这是因为,当事件标志1 被设置后,就满足了task2 任务等待的事件条件(事件标志0 和事件标志1 通知发生),因此就刷新了LCD 区域,这与预期的实验结果相符。

FreeRTOS 低功耗Tickless 模式

在一些特殊场景中,例如长期无人照看的数据采集采集设备、可穿戴设备等,都对设备的功耗有着很严格的要求,为了迎合这种特殊需求,大多数MCU 也都有相应的低功耗模式,以此来降低设备的整体功耗。当然有了相应的低功耗硬件涉设计,软件层面的低功耗设计也得跟上,FreeRTOS 就提供了用于低功耗的Tickless 机制。本章就来学习FreeRTOS 中低功耗Tickless模式的相关内容。

FreeRTOS 低功耗Tickless 模式简介

FreeRTOS 的低功耗Tickless 模式是基于硬件层面的相应低功耗模式实现的,MCU 硬件层面相关的低功耗模式请读者参考正点原子对应开发板开发指南中《低功耗实验》章节,本章主要介绍FreeRTOS 的低功耗Tickless 模式。

空闲任务中的低功耗Tickless 处理

在第11.3 小节《FreeRTOS 任务运行时间实验》中,可以看出,在整个系统的运行过程中,其实大部分的时间是在执行空闲任务的,而空闲任务之所及叫做空闲任务,是因为空闲任务是在系统中的所有其他都阻塞或被挂起时才运行的,因此可以在本该空闲任务执行的期间,让MCU 进入相应的低功耗模式,接着在其他任务因被解除阻塞或其他原因,而准备运行的时候,让MCU 退出相应的低功耗模式,去执行相应的任务。在以上这一过程中,主要的难点在于,MCU 进入相应的低功耗模式后,如何判断有除空闲任务外的其他任务就绪,并退出相应的空闲模式去执行就绪任务,也就是如何计算MCU 进入相应低功耗模式的时间,而FreeRTOS 的低功耗Tickless 模式机制已经处理好了这个问题。

在第8.8 小节《FreeRTOS 空闲任务》中,已经对空闲任务进行了分析,但并没有设计低功耗Tickless 模式的相关内容,接下来看一下在空闲任务中,是如何处理低功耗Tickless 机制的,代码如下所示:

static portTASK_FUNCTION(prvIdleTask, pvParameters) {
    /* 低功耗Tickless模式无关代码,代码省略*/
    /* 此宏用于启用FreeRTOS低功耗Tickless模式*/
    #
    if (configUSE_TICKLESS_IDLE != 0) {
        TickType_t xExpectedIdleTime;
        /* 计算进入相应低功耗模式的时长
         * 本次计算的结果并不一定准确,
         * 因为可能会收到任务调度器的影响
         */
        xExpectedIdleTime = prvGetExpectedIdleTime();
        /* 如果时长大于configEXPECTED_IDLE_TIME_BEFORE_SLEEP,才进入相应的低功耗模式*/
        if (xExpectedIdleTime >= configEXPECTED_IDLE_TIME_BEFORE_SLEEP) {
            /* 挂起任务调度器*/
            vTaskSuspendAll(); {
                configASSERT(xNextTaskUnblockTime >= xTickCount);
                /* 重新计算进入相应低功耗模式的时长
                 * 此时任务调度器已经被挂起,
                 * 因此本次的计算结果就是MCU进入相应低功耗模式的时长
                 */
                xExpectedIdleTime = prvGetExpectedIdleTime();
                /* 如果不希望进入低功耗模式,
                 * 可以定义此宏将xExpectedIdleTime设置为0
                 */
                configPRE_SUPPRESS_TICKS_AND_SLEEP_PROCESSING(xExpectedIdleTime);
                /* 如果时长大于configEXPECTED_IDLE_TIME_BEFORE_SLEEP,
                 * 才进入相应的低功耗模式
                 */
                if (xExpectedIdleTime >= configEXPECTED_IDLE_TIME_BEFORE_SLEEP) {
                    /* 用于调试,不用理会*/
                    traceLOW_POWER_IDLE_BEGIN();
                    /* 此宏就是用来让MCU进入相应的低功耗模式的
                     * 传入MCU需要进入相应低功耗模式的时长
                     */
                    portSUPPRESS_TICKS_AND_SLEEP(xExpectedIdleTime);
                    /* 用于调试,不用理会*/
                    traceLOW_POWER_IDLE_END();
                } else {
                    mtCOVERAGE_TEST_MARKER();
                }
            }
            /* 恢复任务调度器*/
            (void) xTaskResumeAll();
        } else {
            mtCOVERAGE_TEST_MARKER();
        }
    }#
    endif
    /* 低功耗Tickless模式无关代码,代码省略*/
}

从上面的代码中可以看出,FreeRTOS 首先会调用函数prvGetExpectedIdleTime()计算MCU需要进入相应低功耗模式的时长,只有当这个时长大于宏定义configEXPECTED_IDLE_TIME_BEFORE_SLEEP 定义的值时,才会让MCU 进入相应的低功耗模式。接下来还会第二次计算MCU 需要进入相应低功耗模式的时长,这是因为第一次计算的时长可能会收到任务调度器的影响,并不准确,第二次计算的时长是在挂起了任务调度器之后计算的,最终会调用函数portSUPPRESS_TICKS_AND_SLEEP()使得MCU 进入相应的低功耗模式。

函数portSUPPRESS_TICKS_AND_SLEEP()

此函数实际上是一个宏,该宏在portmacro.h 文件中有定义,因为各种不同架构的不同MCU,其进入低功耗模式的方式各有不同,因为FreeRTOS 针对不同架构的不同MCU,为用户提供了此宏,代码如下所示:

#ifndef portSUPPRESS_TICKS_AND_SLEEP
extern void vPortSuppressTicksAndSleep( TickType_t xExpectedIdleTime );
#define portSUPPRESS_TICKS_AND_SLEEP( xExpectedIdleTime ) \
vPortSuppressTicksAndSleep( xExpectedIdleTime )
#endif

可以看出,宏portSUPPRESS_TICKS_AND_SLEEP() 默认是被定义成函数
vPortSuppressTickAndSleep(),此函数中有一段重要的代码,使得CPU 进入睡眠模式,代码如下所示:

__weak void vPortSuppressTicksAndSleep(TickType_t xExpectedIdleTime) 
{
    /* 省略其他代码*/
    /* 此宏用于执行进入相应低功耗模式之前的事务*/
    configPRE_SLEEP_PROCESSING(xModifiableIdleTime);
    /* 判断时长是否大于0 */
    if (xModifiableIdleTime > 0) {
        __dsb(portSY_FULL_READ_WRITE);
        /* 进入睡眠模式*/
        __wfi();
        __isb(portSY_FULL_READ_WRITE);
    }
    /* 此宏用于执行退出相应低功耗模式后的事务*/
    configPOST_SLEEP_PROCESSING(xExpectedIdleTime);
    /* 省略其他代码*/
}

从上面的代码中可以看出,调用了函数__wfi()使得CPU 进入睡眠模式,且在CPU 进入睡眠模式前后,分别调用了函数configPRE_SLEEP_RPOCESSING() 和函数configPost_Sleep_ProCESSING()处理CPU 进入睡眠模式之前和CPU 退出睡眠模式之后要做的事务,在这两个函数中,用户可以自行添加一些优化功耗的相关功能,例如管理MCU 一些片上外设的时钟、修改MCU 的系统时钟或时钟源,使得CPU 在进入睡眠模式后功耗达到最低,在退出睡眠模式后又恢复功能。

FreeRTOS 低功耗Tickless 模式相关配置项

在前面对FreeRTOS 低功耗Tickless 模式的简介中提到了FreeRTOS 中针对该模式的几个配置,如下表所示:
在这里插入图片描述

  1. configUSE_TICKLESS_IDLE
    此宏用于使能低功耗Tickless 模式,当此宏定义为1 时,系统会在进入空闲任务期间进入相应的低功耗模式大于configEXPECTED_IDLE_TIME_BEFORE_SLEEP 的时长。
  2. configEXPECTED_IDLE_TIME_BEFORE_SLEEP
    此宏用于定义系统进入相应低功耗模式的最短时长,如果系统在进入相应低功耗模式前,计算出系统将进入相应低功耗的时长小于configEXPECTED_IDLE_TIME_BEFORE_SLEEP 定义的最小时长,则系统不进入相应的低功耗模式,要注意的是,此宏的值不能小于2。
  3. configPRE_SLEEP_PROCESSING(x)
    此宏用于定义一些需要在系统进入相应低功耗模式前执行的事务,例如可以在进入低功耗模式前关闭一些MCU 片上外设的时钟,以达到降低功耗的目的。
  4. configPOSR_SLEEP_PROCESSING(x)
    此宏用于定义一些需要在系统退出相应低功耗模式后执行的事务,例如开启在系统在进入相应低功耗模式前关闭的MCU 片上外设的时钟,以是系统能够正常运行。

FreeRTOS 低功耗Tickless 实验

功能设计

  1. 例程功能
    本实验主要用于学习使用FreeRTOS 中的低功耗Tickless 模式,本实验设计了两个任务,这两个任务的功能如下表所示:

在这里插入图片描述
该实验的实验工程,请参考《FreeRTOS 实验例程18 FreeRTOS 低功耗Tickless 实验》。

软件设计

  1. 程序流程图
    本实验的程序流程图,如下图所示
    在这里插入图片描述
  2. 程序解析
    整体的代码结构,请参考2.1.6 小节,本小节着重讲解本实验相关的部分。
    (1) FreeRTOS 配置
    本实验需要使用FreeRTOS 的低功耗Tickless 模式功能,因此需要配置FreeRTOSConfig.h文件,具体的配置如下所示:
/* 1: 使能tickless低功耗模式, 默认: 0 */
#define configUSE_TICKLESS_IDLE 1
/* 低功耗相关定义*/
#if (configUSE_TICKLESS_IDLE != 0)
#include "freertos_demo.h"
/* 定义在进入低功耗模式前执行的函数*/
extern void PRE_SLEEP_PROCESSING(void);
#define configPRE_SLEEP_PROCESSING(x) PRE_SLEEP_PROCESSING()
/* 定义在退出低功耗模式后执行的函数*/
extern void POST_SLEEP_PROCESSING(void);
#define configPOST_SLEEP_PROCESSING(x) POST_SLEEP_PROCESSING()
#endif

可以看到将宏configUSE_TICKLESS_IDLE 定义为1,使能了Tickless 模式,接着定义了宏configPRE_SLEEP_PROCESSING(x) 和宏configPOST_SLEEP_PROCESSING(x) ,后续会在freertos_demo.c 文件中实现这两个宏定义对应的函数。在FreeRTOSConfig.h 文件中并没有定义宏configEXPECTED_IDLE_TIME_BEFORE_SLEEP,这是因为,此宏在FreeRTOS.h 文件中已经有默认定义了,具体的定义如下所示:

#ifndef configEXPECTED_IDLE_TIME_BEFORE_SLEEP
#define configEXPECTED_IDLE_TIME_BEFORE_SLEEP 2
#endif
#if configEXPECTED_IDLE_TIME_BEFORE_SLEEP < 2
#error configEXPECTED_IDLE_TIME_BEFORE_SLEEP must not be less than 2
#endif

可以看出,宏configEXPECTED_IDLE_TIME_BEFORE_SLEEP 被默认定义为2,本实验使用默认为值即可,同时可以看出,此宏定义的值不能小于2。
(2) start_task 任务
start_task 任务的入口函数代码如下所示:

/**
 * @brief start_task
 * @param pvParameters : 传入参数(未用到)
 * @retval 无
 */
void start_task(void * pvParameters) {
    taskENTER_CRITICAL(); /* 进入临界区*/
    /* 关闭LCD */
    lcd_display_off();
    LCD_BL(0);
    /* 创建任务1 */
    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(); /* 退出临界区*/
}

start_task 任务主要用于创建task1 任务,和关闭LCD 显示,因为本实验为低功耗实验,需要测量板卡的整体功耗,因此关闭一些功耗较大的板载设备,这样后续能够比较直观地观察实验结果。
(3) task1 任务

/**
 * @brief task1
 * @param pvParameters : 传入参数(未用到)
 * @retval 无
 */
void task1(void * pvParameters) {
    while (1) {
        LED0(1); /* LED0灭,指示退出低功耗模式*/
        delay_ms(3000); /* CPU忙延时,期间不会进入低功耗模式*/
        LED0(0); /* LED0亮,指示进入低功耗模式*/
        vTaskDelay(3000); /* 阻塞延时,期间会进入低功耗模式*/
    }
}

task1 任务比较简单,就是重复地延时并更改LED0 的状态,不过要注意的是,当LED0 熄灭后使用的延时函数是delay_ms(),此函数为CPU 忙延时,并不会触发任务切换,而当LED0亮起时,使用的是阻塞延时,此时会触发任务切换,因此空闲任务就有机会被执行,从而进入相应的低功耗状态。
(4) 函数PRE_SLEEP_PROCESSING()和函数POST_SLEEP_PROCESSING()

/**
 * @brief PRE_SLEEP_PROCESSING
 * @param 无
 * @retval 无
 */
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();
    }
    /**
     * @brief POST_SLEEP_PROCESSING
     * @param 无
     * @retval 无
     */
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();
}

可以看出,在进入和退出相应低功耗模式前后分别关闭和打开了部分GPIO 外设的时钟,这些操作仅在本实验中起演示作用,读者应根据实际情况完成相应的事务。

下载验证

编译并下载代码,复位后可以看到LCD 屏幕上显示了本次实验的相关信息,如下图所示:
在这里插入图片描述

随后LCD 被关闭,接着LED0 闪烁,当LED0 熄灭时,CPU 正常运行,当LED0 亮起时,CPU 进入睡眠模式,接下来测量板卡在CPU 正常运行和CPU 进入睡眠模式时的整体功耗。测量的设备如下图所示:

在这里插入图片描述
CPU 正常运行时板卡的整体功耗如下图所示:
在这里插入图片描述
CPU 进入低功耗模式后板卡的整体功耗如下图所示:
在这里插入图片描述
要注意的时,在不同的测量环境下,则测量出的数据肯定是不同的,本文给出的测量数据仅作参考。
从图18.3.3.3 和图18.3.3.4 中的测量数据可以看到当是能了低功耗Tickless 模式功能后,系统会使CPU 在空闲任务运行期间进入睡眠模式,降低了系统的整体功耗。在实际应用中,应根据实际情况在系统进入Tickless 模式前为降低系统功耗做更多的事务,而绝非仅仅是本实验中的关闭部分GPIO 时钟,这样一来才能保证能够满足特殊场景的低功耗要求。

FreeRTOS 空闲任务

空闲任务是FreeRTOS 必不可少的一个任务,通过名称,不难猜测,空闲任务是在处理器空闲期间会去执行的任务,当系统中没有其他出去就绪态的任务时,空闲任务就会开始运行,因此空闲任务的任务优先级肯定是最低的任务优先级。当然了也不可能在空闲任务中什么都不做,这样太浪费处理器的资源了,因此FreeRTOS 在空闲任务中也会处理一些事务。本章就来学习FreeRTOS 中空闲任务的相关内容。

FreeRTOS 空闲任务详解

FreeRTOS 空闲任务简介

在前面章节分析vTaskStartScheduler()函数启动任务调度器的时候,就了解到,FreeRTOS 会自动创建一个空闲任务,这样就可以确保系统中永远都至少有一个正在被执行的任务。空闲任务是以系统中最低的任务优先级被创建的,这样可以确保空闲任务不会占用其他就绪态任务的被执行时间。

当有任务被函数vTaskDelete()函数删除时,如果函数vTaskDelete()的调用者与被删除任务不是同一个任务,那么被删除任务的删除工作可以由函数vTaskDelete()的调用者完成。如果vTaskDelete()的调用者和被删除任务为同一个任务,即一个任务调用函数vTaskDelete()删除了自身,那么删除工作是不能完全由任务本身完成的,因此这项删除任务自身的工作就交给了空闲任务,也正因如此,如果在任务中调用了函数vTaskDelete()删除了自身,那么就必须要保证最低任务优先级的空闲任务能够被分配到运行时间。

FreeRTOS 空闲任务的创建

前面也说了,空闲任务是在函数vTaskStartScheduler()中被创建的,具体的代码如下所示:

void vTaskStartScheduler(void) {
    BaseType_t xReturn;
    /* 此宏用于启用静态方式管理内存
     * 可以看出,当静态、动态内存管理方式同时被启用时,
     * 优先使用静态方式管理内存
     */
    #
    if (configSUPPORT_STATIC_ALLOCATION == 1) {
        StaticTask_t * pxIdleTaskTCBBuffer = NULL;
        StackType_t * pxIdleTaskStackBuffer = NULL;
        uint32_t ulIdleTaskStackSize;
        /* 获取空闲任务所需内存空间,
         * 此函数由用户实现,
         * 即使用静态方式管理内存,
         * 任务创建时需要的内存空间由用户手动分配并提供
         */
        vApplicationGetIdleTaskMemory( & pxIdleTaskTCBBuffer, & pxIdleTaskStackBuffer, & ulIdleTaskStackSize);
        /* 使用静态方式创建空闲任务*/
        xIdleTaskHandle = xTaskCreateStatic(
            prvIdleTask, /* 任务函数*/
            configIDLE_TASK_NAME, /* 任务名*/
            ulIdleTaskStackSize, /* 任务栈大小*/ (void * ) NULL, /* 任务函数参数*/
            portPRIVILEGE_BIT, /* 任务优先级*/
            pxIdleTaskStackBuffer, /* 任务栈*/
            pxIdleTaskTCBBuffer); /* 任务控制块*/
        if (xIdleTaskHandle != NULL) {
            xReturn = pdPASS;
        } else {
            xReturn = pdFAIL;
        }
    }#
    else {
        /* 使用动态方式创建空闲任务*/
        xReturn = xTaskCreate(prvIdleTask, /* 任务函数*/
            configIDLE_TASK_NAME, /* 任务名*/
            configMINIMAL_STACK_SIZE, /* 任务栈大小*/ (void * ) NULL, /* 任务函数参数*/
            portPRIVILEGE_BIT, /* 任务优先级*/ & xIdleTaskHandle); /* 任务句柄*/
    }#
    endif
    /* 其他代码省略*/
}

从上面的代码中可以看出,空闲任务的优先级为宏portPRIVILEGE_BIT 定义的值,此宏在FreeRTOS.h 文件中由定义,具体的代码如下所示:

#ifndef portPRIVILEGE_BIT
#define portPRIVILEGE_BIT ( ( UBaseType_t ) 0x00 )
#endif

可以看出,空闲任务的任务优先级就是最低的任务优先级0,用户不能随意修改空闲任务的任务优先级。

FreeRTOS 空闲任务的任务函数

从19.1.2小节《FreeRTOS 空闲任务的创建》中,可以看到,空闲任务的任务函数为prvIdleTask,这个函数由宏portTASK_FUNCTION 定义,代码如下所示:

#define portTASK_FUNCTION( vFunction, pvParameters ) \
void vFunction( void * pvParameters )
static portTASK_FUNCTION( prvIdleTask, pvParameters )
{
/* 代码省略*/
}

展开后,如下所示:

static prvIdleTask( void * pvParameters )
{
/* 代码省略*/
}

其中空闲任务任务函数中的具体内容,请参考第8.8 小节《FreeRTOS 空闲任务》和第18.1.1《空闲任务中的低功耗Tickless 处理》。

FreeRTOS 空闲任务钩子函数

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 即可,当然也不要忘了编写相应的钩子函数。

FreeRTOS 空闲任务钩子函数

从8.8 小节《FreeRTOS 空闲任务》中可以看出,如果将宏configUSE_IDLE_HOOK 配置为1,那么在空闲任务的每一个运行周期中,都会调用一次函数vApplicationIdleHook(),此函数就是空闲任务的钩子函数。

如果想在空闲任务相同的优先级中处理某些事务,那么有两种选择:

  1. 在空闲任务的钩子函数中处理需要处理的任务
    在这种情况下,需要特别注意,因为不论在什么时候,都应该保证系统中有一个正在被执行的任务,因此在空闲任务的钩子函数中,不能够调用会到时空闲任务被阻塞或挂起的函数,例如函数vTaskDelay()。
  2. 在和空闲任务相同任务优先级的任务中处理需要处理的事务
    创建一个和空闲任务相同优先级的任务来处理需要处理的事务是一个比较好的方法,但是这会导致消耗更多的RAM。
    通常在空闲任务的钩子函数中设置处理器进入相应的低功耗模式,以达到降低整体功率的目的,为了与FreeRTOS 自带的低功耗Tickless 模式做区分,这里暂且将这种使用空闲任务钩子函数的低功耗模式成为通用低功耗模式,这是因为,几乎所有的RTOS 都可以使用这种方式实现低功耗。
    通用的低功耗模式会使处理器在每次进入空闲任务函数时,进入相应的低功耗模式,并且在每次SysTick 中断发生的时候都会被唤醒,可见通用方式实现的低功耗效果远不如FreeRTOS自带的低功耗Tickless 模式,但是这种方式更加通用。

FreeRTOS 空闲任务钩子函数实验

功能设计

  1. 例程功能
    本实验主要用于学习使用FreeRTOS 中空闲任务钩子函数实现通用的低功耗模式,本实验设计了两个任务,这两个任务的功能如下表所示:
    在这里插入图片描述
    该实验的实验工程,请参考《FreeRTOS 实验例程19 FreeRTOS 空闲任务钩子函数实验》。

软件设计

  1. 程序流程图
    本实验的程序流程图,如下图所示:
    在这里插入图片描述
  2. 程序解析
    整体的代码结构,请参考2.1.6 小节,本小节着重讲解本实验相关的部分。
    (1) FreeRTOS 配置
    本实验需要使用空闲任务的回调函数,因此需要配置FreeRTOSConfig.h 文件,具体的配置如下所示:
/* 1: 使能空闲任务钩子函数, 无默认需定义*/
#define configUSE_IDLE_HOOK 1

将宏configUSE_IDLE_HOOK 配置为1,就使能了空闲任务的钩子函数,因此下面还需添加空闲任务的钩子函数。
(2) start_task 任务
start_task 任务的入口函数代码如下所示:

/**
 * @brief start_task
 * @param pvParameters : 传入参数(未用到)
 * @retval 无
 */
void start_task(void * pvParameters) {
    taskENTER_CRITICAL(); /* 进入临界区*/
    /* 关闭LCD */
    lcd_display_off();
    LCD_BL(0);
    /* 创建任务1 */
    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(); /* 退出临界区*/
}

start_task 任务主要用于创建task1 任务,和关闭LCD 显示,因为本实验为低功耗实验,需要测量板卡的整体功耗,因此关闭一些功耗较大的板载设备,这样后续能够比较直观地观察实验结果。
(3) task1 任务

/**
 * @brief task1
 * @param pvParameters : 传入参数(未用到)
 * @retval 无
 */
void task1(void * pvParameters) {
    while (1) {
        LED0(1); /* LED0灭,指示退出低功耗模式*/
        delay_ms(3000); /* CPU忙延时,期间不会进入低功耗模式*/
        LED0(0); /* LED0亮,指示进入低功耗模式*/
        vTaskDelay(3000); /* 阻塞延时,期间会进入低功耗模式*/
    }
}

task1 任务比较简单,就是重复地延时并更改LED0 的状态,不过要注意的是,当LED0 熄灭后使用的延时函数是delay_ms(),此函数为CPU 忙延时,并不会触发任务切换,而当LED0亮起时,使用的是阻塞延时,此时会触发任务切换,因此空闲任务就有机会被执行,从而进入相应的低功耗状态。
(4) 函数vApplicationIdleHook()

void BeforeEnterSleep(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 AfterExitSleep(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(portSY_FULL_READ_WRITE);
    __isb(portSY_FULL_READ_WRITE);
    BeforeEnterSleep();
    __wfi();
    AfterExitSleep();
    __dsb(portSY_FULL_READ_WRITE);
    __isb(portSY_FULL_READ_WRITE);
    __enable_irq();
}

可以看出,此处编写的空闲任务回调函数与FreeRTOS 低功耗Tickless 模式的部分代码很相似,同样的会在处理器进出睡眠模式的时候做一些相应的处理。

下载验证

编译并下载代码,复位后可以看到LCD 屏幕上显示了本次实验的相关信息,如下图所示:
在这里插入图片描述
随后LCD 被关闭,接着LED0 闪烁,当LED0 熄灭时,CPU 正常运行,当LED0 亮起时,CPU 进入睡眠模式,接下来测量板卡在CPU 空闲任务中的整体功耗,如下如所示:
在这里插入图片描述

要注意的时,在不同的测量环境下,则测量出的数据肯定是不同的,本文给出的测量数据仅作参考。

比较图19.3.3.2、图18.3.3.3 和图18.3.3.4 中的测量数据可以看到,使用通用的低功耗模式也是能起到一定降低功耗的效果,但是还是不如FreeRTOS 提供的低功耗Tickless 模式。

FreeRTOS 内存管理

内存管理是一个系统的基本组成部分,在FreeRTOS 中大量地使用了内存管理,比如创建任务、信号量、队列等对象时,都可以从FreeRTOS 管理的堆中申请内存。FreeRTOS 也向用户提供了应用层的内存申请与释放函数。本章就来学习FreeRTOS 中内存管理的相关内容。

FreeRTOS 内存管理简介

在使用FreeRTOS 创建任务、队列、信号量等对象的时候,FreeRTOS 一般都提供了两种方法,一种是动态地申请创建对象时所需要的内存,这种方法也叫动态方法;一种是由用户自定义对象,在编译器编译程序的时候,会为已经在程序中定义好的对象分配一定的内存空间,这种方法也叫静态方法。
静态方法创建任务、队列、信号量等对象的API 函数一般是以“Static”结尾的,例如静态创建任务的API 函数xTaskCreateStatic()。使用静态方式创建各种对象时,需要用户提供各种内存空间,例如任务的栈空间、任务控制块所用内存空间等等,并且使用静态方式占用的内存空间一般固定下来了,即使任务、队列等被删除后,这些被占用的内存空间也没有其他用途。

在使用动态方式管理内存的时候,FreeRTOS 就能够在创建任务、队列、信号量等对象的时候,自动地从FreeRTOS 管理的内存堆中申请所创建对象所需的内存,在对象被删除后,又可以将这块内存释放会FreeRTOS 管理的内存堆,这样看来,动态方式管理内存相比与静态方式,显得灵活许多。

除了FreeRTOS 提供的动态内存管理方法,标准的C 库也提供了函数malloc()和函数free()来实现动态地申请和释放内存,但是标准C 库的动态内存管理方法有如下几个缺点:

  1. 并不适用于所有嵌入式系统。
  2. 占用大量的代码空间。
  3. 没有线程安全的相关机制。
  4. 具有不确定性,体现在每次执行的时间不同。
  5. ……

为此,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 种动态内存管理算法的异同。

FreeRTOS 内存管理算法

FreeRTOS 提供了5 种动态内存管理算法,分别为heap_1、heap_2、heap_3、heap_4 和heap_5,这5 种动态内存管理算法各自的特点如下所示:

heap_1:最简单,只允许申请内存,不允许释放内存。
heap_2:允许申请和释放内存,但不能合并相邻的空闲内存块。
heap_3:简单封装C 库的函数malloc()和函数free(),以确保线程安全。
heap_4:允许申请和释放内存,并且能够合并相邻的空闲内存块,减少内存碎片的产生。
heap_5:能够管理多个非连续内存区域的heap_4。
读者可根据实际的应用需求选择合适的内存管理算法,如果不想了解FreeRTOS 内存管理算法的实现机制,则可以跳过本20.2 小节的后续内容。

heap_1 内存管理算法

heap_1 内存管理算法是5 种内存管理算法中最实现简单的内存管理算法,但是由heap_1 内存管理算法申请的内存,是无法被释放的。尽管如此,heap_1 内存管理算法依然适用于个别嵌入式应用,这是因为个别嵌入式应用会在系统启动时创建所需的任务、队列、信号量等,接着在整个程序的运行过程中,这些创建好的任务、队列、信号量等都不需要被删除,因此也就无需释放这些任务、队列、信号量等创建时申请的内存。

heap_1 内存管理算法的内存堆:
heap_1 内存管理算法管理的内存堆是一个数组,在申请内存的时候,heap_1 内存管理算法只是简单地从数组中分出合适大小的内存,内存堆数组的定义如下所示:

/* 此宏用于定义FreeRTOS内存堆的定义方式*/
#if ( configAPPLICATION_ALLOCATED_HEAP == 1 )
/* 用户自定义一个大数组作为FreeRTOS管理的内存堆*/
extern uint8_t ucHeap[ configTOTAL_HEAP_SIZE ];
#else
/* 定义一个大数组作为FreeRTOS管理的内存堆*/
static uint8_t ucHeap[ configTOTAL_HEAP_SIZE ];
#endif

从上面的代码中可以看出,heap_1 内存管理算法管理的内存堆实际上是一个大小为configTOTAL_HEAP_SIZE 字节的数组,宏configTOTAL_HEAP_SIZE 可以在FreeRTOSConfig.h文件中进行配置。宏configAPPLICATION_ALLCOATED_HEAP 允许用户将内存堆定义在指定的地址中,常用于将内存堆定义在外扩的RAM 中。

用户可以通过函数xPortGetFreeHeapSize()获取内存堆剩余空间的大小,根据系统运行时内存堆的剩余空间大小,对configTOTAL_HEAP_SIZE 配置项进行优化配置。
heap_1 内存管理算法具有如下特性:

  1. 适用于一旦创建好任务、队列、信号量等就不会删除的应用,实际上大多数的FreeRTOS应用都是这样的。
  2. 具有确定性,体现在每次执行的时间都是一样的,而且不会产生内存碎片。
  3. 实现的方式非常简单,分配的内存都是从一个静态分配的数组中分配的,因此也意味着并不适用于那些真正需要动态申请和释放内存的应用。
    heap_1 内存管理算法内存申请函数详解:
    heap_1.c 文件中用于申请内存的函数为函数pvPortMalloc(),此函数的定义如下所示:
    。。。。

从上面的代码中可以看出,heap_1 内存管理算法的申请内存函数的实现非常简单,就是从内存堆的低地址开始往高地址分配内存,内存堆的结构示意图如下所示:

在这里插入图片描述

从上图可以看出,heap_1 内存管理算法管理下的内存堆利用率是非常高的,除了内存堆起始地址的位置可能会因地址对齐产生一小块无用内存外,内存堆中其余的内存空间都可以用来分配,并且也不会产生内存碎片。
heap_1 内存管理算法内存释放函数详解:
heap_1.c 文件中用于释放内存的函数为函数pvPortFree(),此函数的定义如下所示:

void vPortFree(void * pv) {
    /* 没有实现释放内存的功能*/
    (void) pv;
    configASSERT(pv == NULL);
}

从上面的代码中可以看出,heap_1 内存管理算法的内存释放函数并没有实现,因此使用heap_1 内存管理算法申请的内存,是无法释放的。

heap_2 内存管理算法

相比于heap_1 内存管理算法,heap_2 内存管理算法使用了最适应算法,以支持释放先前申请的内存,但是heap_2 内存管理算法并不能将相邻的空闲内存块合并成一个大的空闲内存块,因此heap_2 内存管理算法不可避免地会产生内存碎片。

内存碎片是由于多次申请和释放内存,但释放的内存无法与相邻的空闲内存合并而产生的,具体的产生过程,如下图所示:

在这里插入图片描述

如上图所示,当内存堆被多次申请和释放后,由于相邻的小空闲内存无法合并成一个大的空闲内存,从而导致即使内存堆中有足够多的空闲内存,也无法再分配出一块大内存。

heap_2 内存管理算法的内存堆:
heap_2 内存管理算法的内存堆与heap_1 内存管理算法的内存堆一样,都是一个数组,定义如下所示:

/* 此宏用于定义FreeRTOS内存堆的定义方式*/ #
if (configAPPLICATION_ALLOCATED_HEAP == 1)
/* 用户自定义一个大数组作为FreeRTOS管理的内存堆*/
    extern uint8_t ucHeap[configTOTAL_HEAP_SIZE];#
else
/* 定义一个大数组作为FreeRTOS管理的内存堆*/
    static uint8_t ucHeap[configTOTAL_HEAP_SIZE];#
endif

从上面的代码中可以看出,heap_2 内存管理算法中定义的内存堆与heap_1 内存管理算法一样,可以在FreeRTOSConfig.h 文件中配置configTOTAL_HEAP_SIZE 配置项,以配置内存堆的字节大小,同样地,也可以用过configAPPLICATION_ALLOCATED_HEAP 配置项将内存堆定义在指定的内存地址中。

用户可以通过函数xPortGetFreeHeapSize()获取内存堆中未分配的内存总量,并根据系统运行时内存堆中剩余内存的大小,针对性地对configTOTAL_HEAP_SIZE 配置项进行优化配置。

heap_2 内存管理算法具有如下特性:

  1. 可以使用在可能会删除已经创建好的任务、队列、信号量等的应用程序中,但是要注意内存碎片的产生。
  2. 不应该被使用在多次申请和释放不固定大小内存的情况,因为这可能会导致内存碎片的情况变得严重,例如多次地创建和删除任务、队列等,并且每次创建的任务栈大小、队列长度等都是不固定的,或需要在应用程序中调用函数pvPortMalloc()和函数vPortFree()来申请和释放不固定大小的内存等,以上这些情况都应该慎用heap_2 内存管理算法。
  3. 具有不确定性,但是执行的效率比标准C 库的内存管理高得多。
    heap_2 内存管理算法内存块详解:
    为了能够实现内存的释放功能,heap_2 内存管理算法引入了内存块的概念。在内存堆中的内存都是以内存块表示的,首先来看一下heap_2 内存管理算法中内存块的定义:
typedef struct A_BLOCK_LINK {
    struct A_BLOCK_LINK * pxNextFreeBlock; /* 指向下一个内存块*/
    size_t xBlockSize; /* 内存块的大小*/
}
BlockLink_t;

从上面的代码中可以看出,每一个内存块都包含了一个用于指向下一个内存块的指针pxNextFreeBlock,并记录了内存块的大小,内存块的大小就包含了内存块的内存块结构体占用的内存空间和内存块中可使用的内存大小,因此内存块的结构如下图所示(下图展示了一段24字节大小的内存作为内存块的示意图):

在这里插入图片描述

heap_2 内存管理算法会通过内存块中的pxNextFreeBlock 指针,将还未分配的内存块链成一个单向链表,这个单向链表就叫做空闲块链表。空闲块链表中的内存块是按照内存块的大小从小到大排序的,因此空闲块链表中相邻的两个内存块,其在内存中也不一定相邻。为了方便管理这个空闲块链表,在heap_2.c 文件中定义了两个内存块来作为空闲块链表的链表头和链表尾,这两个内存块的定义如下:

static BlockLink_t xStart, xEnd;

其中,xStart 作为空闲块链表的链表头,xEnd 作为空闲块链表的链表尾,需要注意的是,xStart 和xEnd 并不是内存堆中的内存块,因此xStart 和xEnd 内存块并不包含可分配的内存。

heap_2 内存管理算法内存堆初始化详解:
heap_2.c 文件中用于初始化内存堆的函数为函数prvHeapInit(),此函数的定义如下所示:

static void prvHeapInit(void) {
    BlockLink_t * pxFirstFreeBlock;
    uint8_t * pucAlignedHeap;
    /* 确保内存堆的起始地址按照portBYTE_ALIGNMENT字节对齐*/
    pucAlignedHeap = (uint8_t * )
        (((portPOINTER_SIZE_TYPE) & ucHeap[portBYTE_ALIGNMENT - 1]) &
            (~((portPOINTER_SIZE_TYPE) portBYTE_ALIGNMENT_MASK)));
    /* xStart内存块的下一个内存块指向内存堆*/
    xStart.pxNextFreeBlock = (void * ) pucAlignedHeap;
    /* xStart内存块的大小固定为0 */
    xStart.xBlockSize = (size_t) 0;
    /* xEnd内存块的大小用于指示内存堆的总大小*/
    xEnd.xBlockSize = configADJUSTED_HEAP_SIZE;
    /* xEnd内存块没有下一个内存块*/
    xEnd.pxNextFreeBlock = NULL;
    /* 将整个内存堆作为一个内存块*/
    pxFirstFreeBlock = (void * ) pucAlignedHeap;
    /* 设置内存块的大小*/
    pxFirstFreeBlock - > xBlockSize = configADJUSTED_HEAP_SIZE;
    /* 内存块的下一个内存块指向xEnd */
    pxFirstFreeBlock - > pxNextFreeBlock = & xEnd;
}

从上面的代码中可以看出,初始化内存堆的时候,同时也初始化了xStart 和xEnd,初始化好后的内存堆和xStart、xEnd 如下图所示:

在这里插入图片描述
heap_2 内存管理算法空闲块链表插入空闲内存块:
heap_2 内存管理算法支持释放已经分配的内存,被释放的内存将被作为空闲内存块添加到空闲块链表,这一操作通过宏prvInsertBlockIntoFreeList()完成,此宏的定义如下所示:

#define prvInsertBlockIntoFreeList( pxBlockToInsert ) \
{ \
BlockLink_t * pxIterator; \
size_t xBlockSize; \
\
/* 获取待插入空闲内存块的大小*/ \
xBlockSize = pxBlockToInsert->xBlockSize; \
\
/* 从xStart开始,遍历整个内存块单向链表*/ \
/* 找到第一个内存大小不小于待插入空闲内存块的空闲内存块的上一个空闲内存块*/ \
for(pxIterator = &xStart; \
pxIterator->pxNextFreeBlock->xBlockSize < xBlockSize; \
pxIterator = pxIterator->pxNextFreeBlock) \
{ \
/* 什么都不做,找到内存块该插入的位置*/ \
} \
\
/* 将待插入的内存块,插入链表中的对应位置*/ \
pxBlockToInsert->pxNextFreeBlock = pxIterator->pxNextFreeBlock; \
pxIterator->pxNextFreeBlock = pxBlockToInsert; \
}

从上面的代码中可以看出,将空闲的内存块插入空闲块链表,首先会从头遍历空闲块链表找到第一个内存大小不小于待插入空闲内存块的空闲内存块的上一个空闲内存块,然后将待插入空闲内存块插入到这个空闲内存块的后面,如下图所示:
在这里插入图片描述

可以看出,heap_2 内存管理算法的空闲内存块插入空闲内存块链表的操作与第7.2.4 小节《函数vListInsert()》是很相似的。
heap_2 内存管理算法内存申请函数详解:
heap_2.c 文件中用于申请内存的函数为函数pvPortMalloc(),此函数的定义如下所示:
。。。

从上面的代码中可以看出,heap_2 内存管理算法申请内存的过程,大致如下:

  1. 因为空闲块链表中的空闲内存块是按照内存块的大小从小到大排序的,因此从头开始遍历空闲块链表,找到第一个大小适合的空闲内存块。
  2. 找到大小适合的空闲内存块后,由于找到的空闲内存块可能比需要申请的内存大,因此需要将整个内存块分为两个小的内存块,其中一个内存块的大小就是需要申请内存的大小,另一个小内存块作为空闲内存块重新插入空闲块链表。
    heap_2 内存管理算法内存释放函数详解:
    heap_2.c 文件中用于释放内存的函数为函数pvPortFree(),此函数的定义如下所示:
void vPortFree(void * pv) {
    uint8_t * puc = (uint8_t * ) pv;
    BlockLink_t * pxLink;
    /* 被释放的对象需不为空*/
    if (pv != NULL) {
        /* 获取内存块的起始地址*/
        puc -= heapSTRUCT_SIZE;
        /* 获取内存块*/
        pxLink = (void * ) puc;
        /* 挂起任务调度器*/
        vTaskSuspendAll(); {
            /* 将被释放的内存块插入空闲块链表*/
            prvInsertBlockIntoFreeList(((BlockLink_t * ) pxLink));
            /* 更新内存堆中可分配的内存大小*/
            xFreeBytesRemaining += pxLink - > xBlockSize;
            /* 用于调式,不用理会*/
            traceFREE(pv, pxLink - > xBlockSize);
        }
        /* 恢复任务调度器*/
        (void) xTaskResumeAll();
    }
}

从上面的代码中可以看出,heap_2 内存管理算法的释放函数含简单,就是将带释放的内存块插入到空闲块链表中。

heap_3 内存管理算法

heap_3 内存管理算法是对标准C 库提供的函数malloc()和函数free()的简单封装,以确保线程安全。
heap_3 内存管理算法的内存堆:
heap_3 内存管理算法本质使用的是调用标准C 库提供的内存管理函数,标准C 库的内存管理需要链接器设置好一个堆,这个堆将作为内存管理的内存堆使用,在启动文件中可以配置这个堆的大小,如下所示:

; 配置堆的大小
Heap_Size EQU 0x00000200
; AREA:开辟一段内存空间
; HEAP:段名为HEAP
; NOINIT:不进行初始化
; READWRITE:可读可写
; ALIGN=3:以823次方)字节对齐
AREA HEAP, NOINIT, READWRITE, ALIGN=3
; 堆的起始地址
__heap_base
; 分配一个Heap_Size大小的内存空间
Heap_Mem SPACE Heap_Size
; 堆的结束地址
__heap_limit

通过修改Heap_Size 的值就可以修改堆的大小,要注意的是,在使用heap_3 内存管理算法的时候,配置项configTOTAL_HEAP_SIZE 是无效的。
heap_3 内存管理算法具有如下特性:

  1. 需要链接器提供一个堆,还需要编译器的库提供用于申请内存的函数malloc()和用于释放内存的函数free()。
  2. 具有不确定性。
  3. 有可能会大大地增减编译后的代码量。
    heap_3 内存管理算法内存申请函数详解:
    heap_3.c 文件中用于申请内存的函数为函数pvPortMalloc(),此函数的定义如下所示:
void * pvPortMalloc(size_t xWantedSize) {
    void * pvReturn;
    /* 挂起任务调度器*/
    vTaskSuspendAll(); {
        /* 调用C库函数申请内存*/
        pvReturn = malloc(xWantedSize);
        /* 用于调式,不用理会*/
        traceMALLOC(pvReturn, xWantedSize);
    }
    /* 恢复任务调度器*/
    (void) xTaskResumeAll();
    /* 此宏用于开启动态内存申请失败钩子函数*/
    #
    if (configUSE_MALLOC_FAILED_HOOK == 1) {
        /* 动态申请内存失败*/
        if (pvReturn == NULL) {
            extern void vApplicationMallocFailedHook(void);
            /* 调用动态内存申请失败钩子函数*/
            vApplicationMallocFailedHook();
        }
    }#
    endif
    /* 返回申请到内存的首地址*/
    return pvReturn;
}

从上面的代码中可以看出,heap_3 内存管理算法实际上是调用了C 库的内存申请函数malloc()申请内存,并且会在申请内存的前后挂起和恢复任务调度器,以确保线程安全。
heap_3 内存管理算法内存释放函数详解:
heap_3.c 文件中用于释放内存的函数为函数pvPortFree(),此函数的定义如下所示:

void vPortFree(void * pv) {
    /* 被释放的对象需不为空*/
    if (pv) {
        /* 挂起任务调度器*/
        vTaskSuspendAll(); {
            /* 调用C库函数释放内存*/
            free(pv);
            /* 用于调试,不用理会*/
            traceFREE(pv, 0);
        }
        /* 恢复任务调度器*/
        (void) xTaskResumeAll();
    }
}

从上面的代码中可以看出,heap_3 内存管理算法同样是简单地调用了C 库的内存释放函数free()来释放内存,同时在释放内存前后挂起和恢复任务调度器,以确保线程安全。

heap_4 内存管理算法

heap_4 内存管理算法使用了首次适应算法,与heap_2 内存管理算法一样,heap_4 内存管理算法也支持内存的申请与释放,并且heap_4 内存管理算法还能够将空闲且相邻的内存进行合并,从而减少内存碎片的现象。
heap_4 内存管理算法的内存堆:
heap_4 内存管理算法的内存堆与heap_1、heap_2 内存管理算法的内存堆一样,都是一个数组,定义如下所示:

/* 此宏用于定义FreeRTOS内存堆的定义方式*/ #
if (configAPPLICATION_ALLOCATED_HEAP == 1)
/* 用户自定义一个大数组作为FreeRTOS管理的内存堆*/
    extern uint8_t ucHeap[configTOTAL_HEAP_SIZE];#
else
/* 定义一个大数组作为FreeRTOS管理的内存堆*/
    PRIVILEGED_DATA static uint8_t ucHeap[configTOTAL_HEAP_SIZE];#
endif

从上面的代码中可以看出,heap_4 内存管理算法中定义的内存堆与heap_1、heap_2 内存管理算法一样,可以在FreeRTOSConfig.h 文件中配置configTOTAL_HEAP_SIZE 配置项,以配置内存堆的字节大小,同样地,也可以用过configAPPLICATION_ALLOCATED_HEAP 配置项将内存堆定义在指定的内存地址中。
用户可以通过函数xPortGetFreeHeapSize()获取内存堆中未分配的内存总量,根据系统运行时内存堆中剩余的内存空间大小,就可以针对性地对configTOTAL_HEAP_SIZE 配置项进行优化配置。
heap_4 内存管理算法具有如下特性:

  1. 适用于在程序中多次创建和删除任务、队列、信号量等的应用。
  2. 与heap_2 内存管理算法相比,即使多次分配和释放随机大小的内存,产生内存碎片的几率也要小得多。
  3. 具有不确定性,但是执行的效率比标准C 库的内存管理高得多。
    heap_4 内存管理算法内存块详解:
    与heap_2 内存管理算法相似,heap_4 内存管理算法也引入了内存块的概念。在内存堆中内存以内存块表示,首先来看一下heap_4 内存管理算法中内存块的定义:
/* 内存块结构体*/
typedef struct A_BLOCK_LINK {
    /* 指向下一个内存块*/
    struct A_BLOCK_LINK * pxNextFreeBlock;
    /* 最高位表示内存块是否已经被分配
     * 其余位表示内存块的大小
     */
    size_t xBlockSize;
}
BlockLink_t;

与heap_2 内存管理算法类似,heap_4 内存管理算法的内存块结构体中都包含了两个成员变量,其中成员变量pxNextFreeBlock 与heap_2 内存管理算法一样,都是用来指向下一个空闲内存块的。再来看一下成员变量xBlockSize,这个成员变量与heap_2 内存管理算法中的有些不同,这个成员变量的数据类型为size_t 对于32 位的STM32 而言,这是一个32 位无符号数,其中xBlockSize 的最高位用来标记内存块是否已经被分配,当内存块被分配后,xBlockSize 的最高位会被置1,反之,则置0,其余位用来表示内存块的大小,因为xBlockSize 是一个32 位无符号数,因此能用第0 位至第30 位来表示内存块的大小,也因此内存块的最大大小被限制在0x80000000,即申请内存的大小不能超过0x80000000 字节。
heap_4 内存管理算法同样会通过内存块中的pxNextFreeBlock 指针,将还未分配的内存块链成一个单向链表,这个单向链表就叫做空闲块链表。与heap_2 内存管理算法不同的是,heap_4内存管理算法中空闲块链表中的内存块并不是按照内存块大小的顺序从小到大排序,而是按照空闲块链表中内存块的起始地址大小从小到大排序,这也是为了后续往空闲块链表中插入内存块时,能够将相邻的内存块合并。为了方便管理这个空闲块链表,在heap_4.c 文件中还定义了一个内存块和一个内存块指针来作为空闲块链表的链表头和指向空闲块链表链表尾,这个两个定义如下:

PRIVILEGED_DATA static BlockLink_t xStart, * pxEnd = NULL;

其中,xStart 作为空闲块链表的链表头,pxEnd 指向空闲块链表的链表尾,需要注意的是,xStart 不是内存堆中的内存块,而pxEnd 所指向的内存块则是占用了内存堆中一个内存块结构体大小内存的,只是pxEnd 指向的链表尾内存块的内存大小为0,因此xStart 内存块和pxEnd指向的内存块并不包含可分配的内存。
heap_4 内存管理算法内存堆初始化详解:
heap_4.c 文件中用于初始化内存堆的函数为函数prvHeapInit(),此函数的定义如下所示:

static void prvHeapInit(void) {
    BlockLink_t * pxFirstFreeBlock;
    uint8_t * pucAlignedHeap;
    size_t uxAddress;
    /* 获取内存堆的大小,
     * 即配置项configTOTAL_HEAP_SIZE的值
     */
    size_t xTotalHeapSize = configTOTAL_HEAP_SIZE;
    /* 获取内存堆的起始地址*/
    uxAddress = (size_t) ucHeap;
    /* 将内存堆的起始地址按portBYTE_ALIGNMENT字节向上对齐
     * 并且重新计算地址对齐后内存堆的大小
     */
    if ((uxAddress & portBYTE_ALIGNMENT_MASK) != 0) {
        uxAddress += (portBYTE_ALIGNMENT - 1);
        uxAddress &= ~((size_t) portBYTE_ALIGNMENT_MASK);
        xTotalHeapSize -= uxAddress - (size_t) ucHeap;
    }
    /* 获取对齐后的地址*/
    pucAlignedHeap = (uint8_t * ) uxAddress;
    /* xStart内存块的下一个内存块指向内存堆*/
    xStart.pxNextFreeBlock = (void * ) pucAlignedHeap;
    /* xStart内存块的大小固定为0 */
    xStart.xBlockSize = (size_t) 0;
    /* 从内存堆的末尾与空出一个内存块结构体的内存
     * 并让pxEnd指向这个内存块
     */
    /* 获取内存堆的结束地址*/
    uxAddress = ((size_t) pucAlignedHeap) + xTotalHeapSize;
    /* 为pxEnd预留内存空间*/
    uxAddress -= xHeapStructSize;
    /* 地址按portBYTE_ALIGNMENT字节向下对齐*/
    uxAddress &= ~((size_t) portBYTE_ALIGNMENT_MASK);
    /* 设置pxEnd */
    pxEnd = (void * ) uxAddress;
    /* pxEnd内存块的大小固定为0 */
    pxEnd - > xBlockSize = 0;
    /* pxEnd指向的内存块没有下一个内存块*/
    pxEnd - > pxNextFreeBlock = NULL;
    /* 将内存堆作为一个空闲内存块*/
    pxFirstFreeBlock = (void * ) pucAlignedHeap;
    /* 设置空闲内存块的大小
     * 空闲内存块的大小为pxEnd指向的地址减内存块结构体的大小
     */
    pxFirstFreeBlock - > xBlockSize = uxAddress - (size_t) pxFirstFreeBlock;
    /* 空闲内存块的下一个内存块指向pxEnd */
    pxFirstFreeBlock - > pxNextFreeBlock = pxEnd;
    /* 此时内存堆中只有一个空闲内存块,
     * 并且这个内存块覆盖了整个内存堆空间
     */
    xMinimumEverFreeBytesRemaining = pxFirstFreeBlock - > xBlockSize;
    xFreeBytesRemaining = pxFirstFreeBlock - > xBlockSize;
    /* 此变量限制了内存块的大小,
     * 在32位系统中,这个值的计算结果为0x80000000,
     * 内存块结构体中的成员变量xBlockSize的最高位
     * 用来标记内存块是否被分配,
     * 其余位用来表示内存块的大小,
     * 因此内存块的大小最大为0x7FFFFFFF,
     * 即内存块的大小小于xBlockAllocatedBit的值
     */
    xBlockAllocatedBit = ((size_t) 1) << ((sizeof(size_t) * heapBITS_PER_BYTE) - 1);
}

从上面的代码中可以看出,初始化内存堆的时候,同时也初始化了xStart 和pxEnd,初始化好后的内存堆和xStart、pxEnd 如下图所示:
在这里插入图片描述
从上图中可以看出,heap_4 内存管理算法初始化后的内存堆被分成了两个内存块,分别被内存块指针pxFirstFreeBlock 和内存块指针pxEnd 所指向,其中内存块指针pxEnd 所指向的内存块就是空闲块链表的链表尾,虽然这个链表尾内存块占用了内存堆中的内存,但是并不能作为空闲内存被分配,而被内存块指针pxFirstFreeBlock 所指向的内存块才是可以被分配的空闲内存块。
heap_4 内存管理算法空闲块链表插入空闲内存块:
heap_4 内存管理算法整体与heap_2 内存管理算法很相似,但是heap_4 内存管理算法相较于heap_2 内存管理算法能够将物理内存空间上相邻的两个空闲内存块合并成一个大的空闲内存块,而这正是在将空闲内存块插入空闲块链表的时候实现的。在heap_4.c 文件中定义了函数prvInsertBlockIntoFreeList(),用于将空闲内存块插入空闲块链表,此函数的定义如下所示:

static void prvInsertBlockIntoFreeList(BlockLink_t * pxBlockToInsert) {
    BlockLink_t * pxIterator;
    uint8_t * puc;
    /* 从头开始遍历空闲块链表,
     * 找到第一个下一个内存块的起始地址比待插入内存块高的内存块
     */
    for (pxIterator = & xStart; pxIterator - > pxNextFreeBlock < pxBlockToInsert; pxIterator = pxIterator - > pxNextFreeBlock) {
        /* 什么都不做*/
    }
    /* 获取找到的内存块的起始地址*/
    puc = (uint8_t * ) pxIterator;
    /* 判断找到的这个内存块是否与待插入内存块的低地址相邻*/
    if ((puc + pxIterator - > xBlockSize) == (uint8_t * ) pxBlockToInsert) {
        /* 将两个相邻的内存块合并*/
        pxIterator - > xBlockSize += pxBlockToInsert - > xBlockSize;
        pxBlockToInsert = pxIterator;
    } else {
        mtCOVERAGE_TEST_MARKER();
    }
    /* 获取待插入内存块的起始地址*/
    puc = (uint8_t * ) pxBlockToInsert;
    /* 判断找到的这个内存块的下一个内存块始于待插入内存块的高地址相邻*/
    if ((puc + pxBlockToInsert - > xBlockSize) ==
        (uint8_t * ) pxIterator - > pxNextFreeBlock) {
        /* 要合并的内存块不能未pxEnd */
        if (pxIterator - > pxNextFreeBlock != pxEnd) {
            /* 将两个内存块合并*/
            pxBlockToInsert - > xBlockSize +=
                pxIterator - > pxNextFreeBlock - > xBlockSize;
            pxBlockToInsert - > pxNextFreeBlock =
                pxIterator - > pxNextFreeBlock - > pxNextFreeBlock;
        } else {
            /* 将待插入内存块插入到pxEnd前面*/
            pxBlockToInsert - > pxNextFreeBlock = pxEnd;
        }
    } else {
        /* 将待插入内存块插入到找到的内存块的下一个内存块前面*/
        pxBlockToInsert - > pxNextFreeBlock = - > pxNextFreeBlock;
    }
    /* 判断找到的内存块是否不因为与待插入内存块的低地址相邻,
     * 而与待插入内存块合并
     */
    if (pxIterator != pxBlockToInsert) {
        /* 将找到的内存块的下一个内存块指向待插入内存块*/
        pxIterator - > pxNextFreeBlock = pxBlockToInsert;
    } else {
        /* 如果已经合并了,
         * 那么找到的内存块指向下一个内存块的指针已经被设置了,
         * 不应该再被设这,否为会指向它本身
         */
        mtCOVERAGE_TEST_MARKER();
    }
}

从上面的代码中可以看出,与heap_2 内存管理算法将空闲块链表中的空闲内存块按照内存块的内存大小从小到大排序的方式不同,heap_4 内存管理算法是将空闲内存块链表中的空闲内存块按照内存块在物理内存上的起始地址从低到高进行排序的,也正是因此,才能够更加方便地找出物理内存地址相邻的空闲内存块,并将其进行合并。
从代码中可以看到,在将空闲内存块插入空闲块链表之前,会先从头开始遍历空闲块链表,按照内存块在物理内存上起始地址从低到高的排序规则,找到空闲块要插入的位置。接着判断待插入空闲内存块的起始地址或结束地址是否分别与该位置前面内存块的结束地址或该位置后面内存块的起始地址相同,如果相同侧表示待插入的空闲内存块在物理地址上与该位置前面的内存块或该位置后面的内存块相邻,那么就将响铃的两个空闲内存块合并成一个大的内存块,再将这个大的内存块插入到空闲块链表中,这个操作的示意图如下所示(以待插入空闲内存块与找到位置的上一个内存块相邻为例):

在这里插入图片描述
heap_4 内存管理算法内存申请函数详解:
heap_4.c 文件中用于申请内存的函数为函数pvPortMalloc(),此函数的定义如下所示:
。。。。
从上面的代码中可以看出,heap_4 内存管理算法申请内存的过程的整个逻辑与heap_2 内存管理算法是大同小异的。
heap_4 内存管理算法内存释放函数详解:
heap_4.c 文件中用于释放内存的函数为函数pvPortFree(),此函数的定义如下所示:

void vPortFree(void * pv) {
    uint8_t * puc = (uint8_t * ) pv;
    BlockLink_t * pxLink;
    /* 被释放的对象需不为空*/
    if (pv != NULL) {
        /* 获取内存块的起始地址*/
        puc -= xHeapStructSize;
        /* 获取内存块*/
        pxLink = (void * ) puc;
        /* 待释放的内存块必须是已经被分配的内存块*/
        configASSERT((pxLink - > xBlockSize & xBlockAllocatedBit) != 0);
        /* 待释放的内存块不能在空闲块链表中*/
        configASSERT(pxLink - > pxNextFreeBlock == NULL);
        /* 判断待释放的内存块是否是已经被分配的内存块*/
        if ((pxLink - > xBlockSize & xBlockAllocatedBit) != 0) {
            /* 判断待释放的内存块是否不在空闲块链表中*/
            if (pxLink - > pxNextFreeBlock == NULL) {
                /* 将待释放的内存块标记为未被分配*/
                pxLink - > xBlockSize &= ~xBlockAllocatedBit;
                /* 挂起任务调度器*/
                vTaskSuspendAll(); {
                    /* 更新内存堆中可分配的内存大小*/
                    xFreeBytesRemaining += pxLink - > xBlockSize;
                    /* 用于调式,不用理会*/
                    traceFREE(pv, pxLink - > xBlockSize);
                    /* 将新的空闲内存块插入到空闲块链表中*/
                    prvInsertBlockIntoFreeList(((BlockLink_t * ) pxLink));
                    /* 更新成功释放内存的次数*/
                    xNumberOfSuccessfulFrees++;
                }
                /* 恢复任务调度器*/
                (void) xTaskResumeAll();
            } else {
                mtCOVERAGE_TEST_MARKER();
            }
        } else {
            mtCOVERAGE_TEST_MARKER();
        }
    }
}

从上面的代码中可以看出,heap_4 内存管理算法释放函数的逻辑与heap_2 内存管理算法依然类似。

heap_5 内存管理算法

heap_5 内存管理算法是在heap_4 内存管理算法的基础上实现的,因为heap_5 内存管理算法使用与heap_4 内存管理算法相同的内存分配、释放和合并算法,但是heap_5 内存管理算法在heap_4 内存管理算法的基础上实现了管理多个非连续内存区域的能力。
heap_5 内存管理算法默认并没有定义内存堆,需要用户手动调用函数vPortDefindHeapRegions(),并传入作为内存堆的内存区域的信息,对其进行初始化。初始化后的内存堆将被作为空闲内存块链接到空闲块链表中,再接下来的内存申请与释放就和heap_4 内存管理算法一致了。
要注意的是,因为heap_5 内存管理算法并不会自动创建好内存堆,因此需要用户手动为heap_5 初始化好作为内存堆的内存区域后,才能够动态创建任务、队列、信号量等对象。
heap_5 内存管理算法内存区域信息结构体
heap_5 内存管理算法定义了一个结构体,用于表示内存区域的信息,该结构体的定义如下所示:

typedef struct HeapRegion {
    uint8_t * pucStartAddress; /* 内存区域的起始地址*/
    size_t xSizeInBytes; /* 内存区域的大小,单位:字节*/
}
HeapRegion_t;

通过这个结构体就能够表示内存区域的信息了,要注意的是系统中有多个内存区域需要由heap_5 内存管理算法管理,切记不能多次调用内存区域初始化函数,需参考以下方式(仅作参考,请根据实际情况编写内存区域信息数组):

const HeapRegion_t xHeapRegions[] = {
    {
        (uint8_t * ) 0x80000000, 0x10000
    }, /* 内存区域1 */ {
        (uint8_t * ) 0x90000000, 0xA0000
    }, /* 内存区域2 */ {
        NULL, 0
    } /* 数组终止标志*/
};
vPortDefineHeapRegions(xHeapRegions);

如以上例子所示,定义了一个内存区域信息结构体HeapRegion_t 类型的数组,数组中包含了两个内存区域的信息,这些内存区域信息必须按照内存区域起始地址的高低,从低到高进行排序,最后以一个起始地址为NULL,大小为0 的“虚假”内存区域信息作为内存区域信息数组的终止标志。

heap_5 内存管理算法初始化内存区域
heap_5.c 文件中用于初始化内存区域的函数为函数vPortDefineHeapRegions(),此函数的定义如下所示:

,,。。。

从上面的代码中可以看出,heap_5 内存管理算法的内存区域初始化与heap_4 内存管理算法的内存堆初始化是有写相似的地方的,heap_5 内存管理算法初始化后的内存区域示意图,如下所示(以两个内存区域为例):

在这里插入图片描述

heap_5 内存管理算法与heap_4 内存管理算法大部分的差异在于初始化,其余的内存块插入空闲块链表、内存申请与释放,都是大同小异的,这里就不在专门分析heap_5 内存管理算法的这个部分了,请读者参考heap_4 内存管理的算法的相关内容。

FreeRTOS 内存管理实验

功能设计

  1. 例程功能
    本实验主要用于学习使用FreeRTOS 中的内存管理,本实验设计了两个任务,这两个任务的功能如下表所示:
    在这里插入图片描述
    该实验的实验工程,请参考《FreeRTOS 实验例程20 FreeRTOS 内存管理实验》。

软件设计

  1. 程序流程图
    本实验的程序流程图,如下图所示:
    在这里插入图片描述
  2. 程序解析
    整体的代码结构,请参考2.1.6 小节,本小节着重讲解本实验相关的部分。
    (1) FreeRTOS 配置
    本实验需要使用动态内存管理,因此需要配置FreeRTOSConfig.h 文件,具体的配置如下所示:
/* 1: 支持动态申请内存, 默认: 1 */
#define configSUPPORT_DYNAMIC_ALLOCATION 1
/* FreeRTOS堆中可用的RAM总量, 单位: Byte, 无默认需定义*/
#define configTOTAL_HEAP_SIZE ((size_t)(10 * 1024))

将宏configSUPPORT_DYNAMIC_ALLOCATION 配置为1,就使能了动态内存管理,因此需要将动态内存管理的算法文件添加到工程中,本实验以heap_4 内存管理算法为例,在上文移植FreeRTOS 的时候,已经往工程添加了内存管理算法文件了,因此无需重复添加,如图2.1.2.5所示。同时还需要设置用于内存管理的对的大小,这里定义为10KBytes,这么一来就配置好了动态内存管理的相关配置

(2) start_task 任务
start_task 任务的入口函数代码如下所示:

/**
 * @brief start_task
 * @param pvParameters : 传入参数(未用到)
 * @retval 无
 */
void start_task(void * pvParameters) {
    taskENTER_CRITICAL(); /* 进入临界区*/
    /* 创建任务1 */
    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(); /* 退出临界区*/
}

start_task 任务主要用于创建task1 任务。
(3) task1 任务

/**
 * @brief task1
 * @param pvParameters : 传入参数(未用到)
 * @retval 无
 */
void task1(void * pvParameters) {
    uint8_t key = 0;
    uint8_t * buf = NULL;
    size_t free_size = 0;
    while (1) {
        key = key_scan(0);
        switch (key) {
            case KEY0_PRES:
                {
                    /* 申请内存和使用内存*/
                    buf = pvPortMalloc(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);
    }
}

task1 任务用于扫描按键,当按键0 被按下时,使用函数pvPortMalloc()申请30 字节的内存,并往内存中写入该内存的首地址,然后将其打印出来;当按键1 被按下时,是释放掉申请的内存。同时会在LCD 上实时显示内存的总大小和剩余大小。

下载验证

编译并下载代码,复位后可以看到LCD 屏幕上显示了本次实验的相关信息,如下图所示:

在这里插入图片描述

可以看到,LCD 上显示了用于动态内存内存管理的总内存大小为1KBytes,由于内存对齐、内存块结构体占用、系统启动等因素,已经使用了一部分内存,此时还剩余7664Bytes 的可分配内存空间。

接着按下按键0,动态地从内存堆中申请30Bytes 内存,LCD 显示的内容如下所示:

图20.3.3.2 LCD 显示内容二

首先可以看到,LCD 上显示了申请到内存的首地址,这说明申请到内存的读取和写入都没有问题,因此成功地申请到了内存。但是与图20.3.3.1 比较,可以发现,剩余的可分配内存减少了40Bytes,但是明明就只申请了30Bytes,这是怎么回事呢?这是因为本实验使用的是heap_4内存管理算法,此内存管理算法引入了内存块的概念,内存块的结构体变量固定占用了8Bytes的空间,因此需要从内存堆中分配的内存就变成了38Bytes ,另外还需要按照portBYTE_ALIGNMENT(对于32 位的STM32,宏定义为8)字节对齐,因此就需要从内存堆中划分出40Bytes 的内存空间了,刚详细地请参考第20.2 小节《FreeRTOS 内存管理算法》。

接着按下按键1,释放刚刚申请的内存,LCD 显示的内容如下所示:

在这里插入图片描述

释放完内存后,可以看到内存堆中剩余的可分配内存又变回了7664Bytes,说明之前申请到的内存被成功释放。

内存管理机制为用户提供了灵活的管理内存方法,用户可以在程序运行过程中,根据需求申请和释放内存,但是这也就要求用户对申请的内存进行管理。对于程序中动态申请的内存,在程序执行完毕后需要进行内存释放,将不用的内存释放回内存堆中。如果没有释放不用且动态申请的内存,将导致内存泄漏,这也是使用内存内存管理的问题之一。因此在一般情况下,临时申请内存时,申请和释放内存的函数都是成对出现的,除非保证申请到的内存,需要一直使用,这样才能尽可能地避免内存泄露问题的发生。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

行稳方能走远

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值