目录:
十四、 FreeRTOS任务通知
14.1 任务通知的简介(了解)
任务通知是 FreeRTOS 中一种用于任务间通信的机制,它允许一个任务向其他任务发送简单的通知或信号,以实现任务间的同步和协作。任务通知通常用于替代二值信号量或事件标志组,提供了更轻量级的任务间通信方式。
大多数任务间通信方法通过中间对象,如队列、信号量或事件组。发送任务写入通信对象,接收任务从通信对象读取。当使用直接任务通知时,顾名思义,发送任务直接向接收任务发送通知,而无需中间对象。
每个 RTOS 任务都有一个任务通知组,每条通知均独立运行,都有“挂起”或“非挂起”的通知状态,以及一个 32 位通知值。常量 configTASK_NOTIFICATION_ARRAY_ENTRIES 可设置任务通知组中的索引数量。在 FreeRTOS V10.4.0 版本前,任务只有单条任务通知(即只能一对一),没有任务通知组。
向任务发送“任务通知” 会将目标任务通知设为“挂起”状态。 正如任务可以阻塞中间对象 (如等待信号量可用的信号量),任务也可以阻塞任务通知, 以等待通知状态变为“挂起”。向任务发送“任务通知”也可以更新目标通知的值(可选),可使用下列任一方法:
- 覆盖原值,无论接收任务是否已读取被覆盖的值。
- 覆盖原值(仅当接收任务已读取被覆盖的值时)。
- 在值中设置一个或多个位。
- 对值进行增量(添加 1)。
RTOS 任务通知功能默认为启用状态,将configUSE_TASK_NOTIFICATIONS 设为0可以禁用。
14.2 任务通知相关API函数介绍(熟悉)
任务通知相关函数如下:
| 函数 | 描述 |
| xTaskNotify() | 发送通知,带有通知值 |
| xTaskNotifyAndQuery() | 发送通知,带有通知值并且保留接收任务的原通知值 |
| xTaskNotifyGive() | 发送通知,不带通知值 |
| xTaskNotifyFromISR() | 在中断中发送任务通知 |
| xTaskNotifyAndQueryFromISR() | |
| vTaskNotifyGiveFromISR() | |
| ulTaskNotifyTake() | 获取任务通知,可选退出函数时对通知值清零或减1 |
| xTaskNotifyWait() | 获取任务通知,可获取通知值和清除通知值的指定位 |
注意:发送通知有相关ISR函数,接收通知没有ISR函数,不能在ISR中接收任务通知。
14.3 任务通知模拟信号量实验
将任务通知用作轻量级二进制信号量:
- start_task:用来创建其他2个任务。
- task1:用于按键扫描,当检测到按键KEY1被按下时,将发送任务通知。
- task2:用于接收任务通知,并打印相关提示信息。
freertos_demo.c代码清单:
//1)任务配置
/* 启动任务函数 */
#define START_TASK_PRIORITY 1
#define START_TASK_STACK_DEPTH 128
TaskHandle_t start_task_handler;
void Start_Task(void *pvParameters);
/* Task1 任务 配置 */
#define TASK1_PRIORITY 2
#define TASK1_STACK_DEPTH 128
TaskHandle_t task1_handler;
void Task1(void *pvParameters);
/* Task2 任务 配置 */
#define TASK2_PRIORITY 3
#define TASK2_STACK_DEPTH 128
TaskHandle_t task2_handler;
void Task2(void *pvParameters);
//2)入口函数
/**
* @description: FreeRTOS入口函数:创建任务函数并开始调度
* @return {*}
*/
void FreeRTOS_Start(void)
{
xTaskCreate((TaskFunction_t)Start_Task,
(char *)"Start_Task",
(configSTACK_DEPTH_TYPE)START_TASK_STACK_DEPTH,
(void *)NULL,
(UBaseType_t)START_TASK_PRIORITY,
(TaskHandle_t *)&start_task_handler);
vTaskStartScheduler();
}
//3)初始任务函数
void Start_Task( void * pvParameters )
{
taskENTER_CRITICAL(); /* 进入临界区 */
xTaskCreate((TaskFunction_t ) Task1,
(char * ) "Task1",
(configSTACK_DEPTH_TYPE ) TASK1_STACK_DEPTH,
(void * ) NULL,
(UBaseType_t ) TASK1_PRIORITY,
(TaskHandle_t * ) &task1_handler );
xTaskCreate((TaskFunction_t ) Task2,
(char * ) "Task2",
(configSTACK_DEPTH_TYPE ) TASK2_STACK_DEPTH,
(void * ) NULL,
(UBaseType_t ) TASK2_PRIORITY,
(TaskHandle_t * ) &task2_handler );
vTaskDelete(NULL);
taskEXIT_CRITICAL(); /* 退出临界区 */
}
//4)task1任务函数
/**
* @description: 发送任务通知值
* @param {void *} pvParameters
* @return {*}
*/
void Task1(void * pvParameters)
{
uint8_t key = 0;
while(1)
{
key = Key_Detect();
if(key == KEY1_PRESS)
{
printf("任务通知模拟二值信号量释放\r\n");
xTaskNotifyGive(task2_handler);
}
vTaskDelay(10);
}
}
//5)task2任务函数
/**
* @description: 接收任务通知值
* @param {void *} pvParameters
* @return {*}
*/
void Task2(void * pvParameters)
{
uint32_t rev = 0;
while(1)
{
rev = ulTaskNotifyTake(pdTRUE , portMAX_DELAY);
if(rev != 0)
{
printf("接收任务通知成功,模拟获取二值信号量\r\n");
}
}
}
14.4 任务通知模拟消息邮箱实验
将任务通知用作轻量级邮箱:
- start_task:用来创建其他2个任务。
- task1:用于按键扫描,将按下的按键键值通过任务通知发送给指定任务。
- task2:用于接收任务通知,并根据接收到的数据做相应动作。
freertos_demo.c代码清单:
//1)任务配置
/* 启动任务函数 */
#define START_TASK_PRIORITY 1
#define START_TASK_STACK_DEPTH 128
TaskHandle_t start_task_handler;
void Start_Task(void *pvParameters);
/* Task1 任务 配置 */
#define TASK1_PRIORITY 2
#define TASK1_STACK_DEPTH 128
TaskHandle_t task1_handler;
void Task1(void *pvParameters);
/* Task2 任务 配置 */
#define TASK2_PRIORITY 3
#define TASK2_STACK_DEPTH 128
TaskHandle_t task2_handler;
void Task2(void *pvParameters);
//2)入口函数
/**
* @description: FreeRTOS入口函数:创建任务函数并开始调度
* @return {*}
*/
void FreeRTOS_Start(void)
{
xTaskCreate((TaskFunction_t)Start_Task,
(char *)"Start_Task",
(configSTACK_DEPTH_TYPE)START_TASK_STACK_DEPTH,
(void *)NULL,
(UBaseType_t)START_TASK_PRIORITY,
(TaskHandle_t *)&start_task_handler);
vTaskStartScheduler();
}
//3)初始任务函数
void Start_Task( void * pvParameters )
{
taskENTER_CRITICAL(); /* 进入临界区 */
xTaskCreate((TaskFunction_t ) Task1,
(char * ) "Task1",
(configSTACK_DEPTH_TYPE ) TASK1_STACK_DEPTH,
(void * ) NULL,
(UBaseType_t ) TASK1_PRIORITY,
(TaskHandle_t * ) &task1_handler );
xTaskCreate((TaskFunction_t ) Task2,
(char * ) "Task2",
(configSTACK_DEPTH_TYPE ) TASK2_STACK_DEPTH,
(void * ) NULL,
(UBaseType_t ) TASK2_PRIORITY,
(TaskHandle_t * ) &task2_handler );
vTaskDelete(NULL);
taskEXIT_CRITICAL(); /* 退出临界区 */
}
//4)task1任务函数
/**
* @description: 发送任务通知值
* @param {void *} pvParameters
* @return {*}
*/
void Task1(void *pvParameters)
{
uint8_t key = 0;
while (1)
{
key = Key_Detect();
if ((key != 0) && (task2_handler != NULL))
{
printf("任务通知模拟消息邮箱发送,发送的键值为:%d\r\n", key);
xTaskNotify(task2_handler, key, eSetValueWithOverwrite);
}
vTaskDelay(10);
}
}
//5)task2任务函数
/**
* @description: 接收任务通知值
* @param {void *} pvParameters
* @return {*}
*/
void Task2(void *pvParameters)
{
uint32_t noyify_val = 0;
while (1)
{
xTaskNotifyWait(0, 0xFFFFFFFF, &noyify_val, portMAX_DELAY);
switch (noyify_val)
{
case KEY1_PRESS:
{
printf("接收到的通知值为:%d\r\n", noyify_val);
LED_Toggle(LED1_Pin);
break;
}
case KEY2_PRESS:
{
printf("接收到的通知值为:%d\r\n", noyify_val);
LED_Toggle(LED2_Pin);
break;
}
default:
break;
}
}
}
14.5 任务通知模拟事件标志组实验
将任务通知用作轻量级事件标志组:
- start_task:用来创建其他2个任务。
- task1:用于按键扫描,当检测到按键按下时,发送任务通知设置不同标志位。
- task2:用于接收任务通知,并打印相关提示信息。
freertos_demo.c代码:
//1)任务配置
/* 启动任务函数 */
#define START_TASK_PRIORITY 1
#define START_TASK_STACK_DEPTH 128
TaskHandle_t start_task_handler;
void Start_Task(void *pvParameters);
/* Task1 任务 配置 */
#define TASK1_PRIORITY 2
#define TASK1_STACK_DEPTH 128
TaskHandle_t task1_handler;
void Task1(void *pvParameters);
/* Task2 任务 配置 */
#define TASK2_PRIORITY 3
#define TASK2_STACK_DEPTH 128
TaskHandle_t task2_handler;
void Task2(void *pvParameters);
#define EVENTBIT_0 (1 << 0)
#define EVENTBIT_1 (1 << 1)
//2)入口函数
/**
* @description: FreeRTOS入口函数:创建任务函数并开始调度
* @return {*}
*/
void FreeRTOS_Start(void)
{
xTaskCreate((TaskFunction_t)Start_Task,
(char *)"Start_Task",
(configSTACK_DEPTH_TYPE)START_TASK_STACK_DEPTH,
(void *)NULL,
(UBaseType_t)START_TASK_PRIORITY,
(TaskHandle_t *)&start_task_handler);
vTaskStartScheduler();
}
//3)初始任务函数
void Start_Task( void * pvParameters )
{
taskENTER_CRITICAL(); /* 进入临界区 */
xTaskCreate((TaskFunction_t ) Task1,
(char * ) "Task1",
(configSTACK_DEPTH_TYPE ) TASK1_STACK_DEPTH,
(void * ) NULL,
(UBaseType_t ) TASK1_PRIORITY,
(TaskHandle_t * ) &task1_handler );
xTaskCreate((TaskFunction_t ) Task2,
(char * ) "Task2",
(configSTACK_DEPTH_TYPE ) TASK2_STACK_DEPTH,
(void * ) NULL,
(UBaseType_t ) TASK2_PRIORITY,
(TaskHandle_t * ) &task2_handler );
vTaskDelete(NULL);
taskEXIT_CRITICAL(); /* 退出临界区 */
}
//4)task1任务函数
/**
* @description: 发送任务通知值
* @param {void *} pvParameters
* @return {*}
*/
void Task1(void *pvParameters)
{
uint8_t key = 0;
while (1)
{
key = Key_Detect();
if (key == KEY1_PRESS)
{
printf("将bit0位置1\r\n");
xTaskNotify(task2_handler, EVENTBIT_0, eSetBits);
}
else if (key == KEY2_PRESS)
{
printf("将bit1位置1\r\n");
xTaskNotify(task2_handler, EVENTBIT_1, eSetBits);
}
vTaskDelay(10);
}
}
//5)task2任务函数
/**
* @description: 接收任务通知值
* @param {void *} pvParameters
* @return {*}
*/
void Task2(void *pvParameters)
{
uint32_t notify_val = 0,event_bit = 0;
while(1)
{
xTaskNotifyWait( 0, 0xFFFFFFFF, ¬ify_val, portMAX_DELAY );
if(notify_val & EVENTBIT_0)
{
event_bit |= EVENTBIT_0;
}
if(notify_val & EVENTBIT_1)
{
event_bit |= EVENTBIT_1;
}
if(event_bit == (EVENTBIT_0|EVENTBIT_1))
{
printf("任务通知模拟事件标志组接收成功\r\n");
event_bit = 0;
}
}
}
十五、软件定时器
15.1 简介
FreeRTOS 中的软件定时器是一种轻量级的时间管理工具,用于在任务中创建和管理定时器。软件定时器是基于 FreeRTOS 内核提供的时间管理功能实现的,允许开发者创建、启动、停止、删除和管理定时器,从而实现在任务中对时间的灵活控制。
软件定时器与硬件定时器的主要区别如下:
| 软件定时器 | 硬件定时器 |
| FreeRTOS提供的功能来模拟定时器,依赖系统的任务调度器来进行计时和任务调度 | 由芯片或微控制器提供,独立于 CPU,可以在后台运行,不受任务调度器的影响 |
| 精度和分辨率可能受到任务调度的影响 | 具有更高的精度和分辨率 |
| 不需要额外的硬件资源,但可能会增加系统的负载 | 占用硬件资源,不会增加 CPU 的负载 |
软件定时器能够让函数在未来的设定时间执行。由定时器执行的函数称为定时器的回调函数。从定时器启动到其回调函数执行之间的时间被称为定时器的周期。简而言之,当定时器的周期到期时,定时器的回调函数会被执行。
定时器回调函数在定时器服务任务的上下文中执行,在定时器回调函数中不能调用导致阻塞的API函数。
软件定时器服务任务是任务调度器中的一个特殊任务,专门用于管理和维护软件定时器的正常运行。如果configUSE_TIMERS 设置为1,在开启任务调度器的时候,会自动创建软件定时器服务的任务。它主要负责软件定时器超时的逻辑判断、调用超时软件定时器的超时回调函数、处理软件定时器命令队列。
15.2 软件定时器状态
FreeRTOS 中的软件定时器有三种状态,分别是:
- 未创建(Uncreated):软件定时器被创建之前的状态。在这个状态下,定时器的数据结构已经被定义,但尚未通过 xTimerCreate() 函数创建。
- 已创建(Created):软件定时器已被成功创建,但尚未启动。在这个状态下,可以对定时器进行配置,如设置定时器的周期、回调函数等,但定时器并未开始计时。
- 已运行(Running):软件定时器已经被启动,正在运行中。在这个状态下,定时器会按照预定的周期定时触发超时事件,执行注册的回调函数。
15.3 单次定时器和周期定时器
在 FreeRTOS 中,软件定时器主要有两种类型:一次性定时器和周期性定时器。
- 一次性定时器(One-shot Timer): 这种定时器在触发一次超时后就会停止,不再执行。适用于只需在特定时间执行一次任务或动作的场景。
- 周期性定时器(Periodic Timer): 这种定时器会在每个超时周期都触发一次,循环执行。适用于需要在固定的时间间隔内重复执行任务或动作的场景。
15.4 相关API函数
| 函数 | 描述 |
| xTimerCreate() | 动态方式创建软件定时器 |
| xTimerCreateStatic() | 静态方式创建软件定时器 |
| xTimerStart() | 开启软件定时器定时 |
| xTimerStartFromISR() | 在中断中开启软件定时器定时 |
| xTimerStop() | 停止软件定时器定时 |
| xTimerStopFromISR() | 在中断中停止软件定时器定时 |
| xTimerReset() | 复位软件定时器定时 |
| xTimerResetFromISR() | 在中断中复位软件定时器定时 |
| xTimerChangePeriod() | 更改软件定时器的定时超时时间 |
| xTimerChangePeriodFromISR() | 在中断中更改定时超时时间 |
15.5 实验
学习使用FreeRTOS软件定时器的函数:
- start_task:用来创建task1任务,并创建一次性定时器和周期性定时器。
- task1:用于按键扫描,并对软件定时器进行开启、停止操作。
代码清单:
FreeRTOSConfig.h文件中:
/* 软件定时器相关定义 */
#define configUSE_TIMERS 1 /* 1: 使能软件定时器, 默认: 0。使能后需指定下面3个 */
#define configTIMER_TASK_PRIORITY (configMAX_PRIORITIES - 1) /* 定义软件定时器任务的优先级 */
#define configTIMER_QUEUE_LENGTH 5 /* 定义软件定时器命令队列的长度*/
#define configTIMER_TASK_STACK_DEPTH (configMINIMAL_STACK_SIZE * 2) /* 定义软件定时器任务的栈空间大小*/
freertos_demo.c:
//1)引入头文件
#include "timers.h"
//2)任务配置
/* 启动任务函数 */
#define START_TASK_PRIORITY 1
#define START_TASK_STACK_DEPTH 128
TaskHandle_t start_task_handler;
void Start_Task(void *pvParameters);
/* Task1 任务 配置 */
#define TASK1_PRIORITY 2
#define TASK1_STACK_DEPTH 128
TaskHandle_t task1_handler;
void Task1(void *pvParameters);
void timer1_callback( TimerHandle_t pxTimer );
void timer2_callback( TimerHandle_t pxTimer );
//3)入口函数
/**
* @description: FreeRTOS入口函数:创建任务函数并开始调度
* @return {*}
*/
void FreeRTOS_Start(void)
{
xTaskCreate((TaskFunction_t)Start_Task,
(char *)"Start_Task",
(configSTACK_DEPTH_TYPE)START_TASK_STACK_DEPTH,
(void *)NULL,
(UBaseType_t)START_TASK_PRIORITY,
(TaskHandle_t *)&start_task_handler);
vTaskStartScheduler();
}
//4)初始任务函数
TimerHandle_t timer1_handle = 0; /* 单次定时器 */
TimerHandle_t timer2_handle = 0; /* 周期定时器 */
void Start_Task(void *pvParameters)
{
taskENTER_CRITICAL(); /* 进入临界区 */
/* 创建单次定时器 */
timer1_handle = xTimerCreate("timer1",
500,
pdFALSE,
(void *)1,
timer1_callback);
/* 创建周期定时器 */
timer2_handle = xTimerCreate("timer2",
2000,
pdTRUE,
(void *)2,
timer2_callback);
xTaskCreate((TaskFunction_t)Task1,
(char *)"Task1",
(configSTACK_DEPTH_TYPE)TASK1_STACK_DEPTH,
(void *)NULL,
(UBaseType_t)TASK1_PRIORITY,
(TaskHandle_t *)&task1_handler);
vTaskDelete(NULL);
taskEXIT_CRITICAL(); /* 退出临界区 */
}
//5)task1任务函数
/**
* @description: 根据按键控制软件定时器
* @param {void *} pvParameters
* @return {*}
*/
void Task1(void *pvParameters)
{
uint8_t key = 0;
while (1)
{
key = Key_Detect();
if (key == KEY1_PRESS)
{
xTimerStart(timer1_handle, portMAX_DELAY);
xTimerStart(timer2_handle, portMAX_DELAY);
}
else if (key == KEY2_PRESS)
{
xTimerStop(timer1_handle, portMAX_DELAY);
xTimerStop(timer2_handle, portMAX_DELAY);
}
vTaskDelay(10);
}
}
//6)超时回调函数
/**
* @description: timer1的超时回调函数
* @param {TimerHandle_t} pxTimer 定时器句柄
* @return {*}
*/
void timer1_callback(TimerHandle_t pxTimer)
{
static uint32_t timer = 0;
printf("timer1的运行次数=%d\r\n", ++timer);
}
/**
* @description: timer2的超时回调函数
* @param {TimerHandle_t} pxTimer 定时器句柄
* @return {*}
*/
void timer2_callback(TimerHandle_t pxTimer)
{
static uint32_t timer = 0;
printf("timer2的运行次数=%d\r\n", ++timer);
}
十六、Tickless低功耗模式
16.1 简介
FreeRTOS 的 Tickless 模式是一种特殊的运行模式,用于最小化系统的时钟中断频率,以降低功耗。在 Tickless 模式下,系统只在有需要时才会启动时钟中断,而在无任务要运行时则完全进入休眠状态,从而降低功耗。在滴答中断重启时,会对 RTOS 滴答计数值进行校正调整。
Tickless模式的实现方式通常依赖于微控制器的硬件特性,尤其是低功耗定时器或实时时钟单元。以下是 Tickless 模式的一般工作原理:
- 空闲任务检测:FreeRTOS 会通过空闲任务(Idle Task)来检测系统是否有任务需要执行。如果没有任务需要执行,系统可以进入休眠状态。
- 时钟中断:当有任务需要执行时,系统会启动时钟中断,唤醒处理器。
- 时钟中断处理:在时钟中断处理函数中,FreeRTOS 将检查任务的状态并决定是否继续执行。
- 休眠状态:如果没有任务需要执行,系统可以进入休眠状态,关闭时钟中断。在休眠状态下,处理器可以进入更低功耗的模式。
- 任务唤醒:当有任务需要执行时,系统会再次启动时钟中断,唤醒处理器,然后执行相应的任务。
在 Tickless 模式下,系统的时钟中断频率明显降低,从而降低了系统的平均功耗。Tickless 模式适用于那些对功耗要求较高、需要长时间运行在低功耗状态的嵌入式系统。比如:电池驱动设备、物联网(IoT)设备、低功耗传感器节点、无线通信模块等。
16.2 详解
STM32F103xC、STM32F103xD和STM32F103xE增强型产品支持三种低功耗模式,可以在要求低功耗、短启动时间和多种唤醒事件之间达到最佳的平衡。
1)睡眠模式(Sleep Mode)
只有CPU停止,所有外设处于工作状态并可在发生中断/事件时唤醒CPU。
2)停机模式(Stop Mode)
在保持SRAM和寄存器内容不丢失的情况下,停机模式可以达到最低的电能消耗。在停机模式下,停止所有内部1.8V部分的供电,PLL、HSI的RC振荡器和HSE晶体振荡器被关闭,调压器可以被置于普通模式或低功耗模式。可以通过任一配置成EXTI的信号把微控制器从停机模式中唤醒,EXTI信号可以是16个外部I/O 口之一、PVD的输出、RTC闹钟或USB的唤醒信号。
3)待机模式(Standby Mode)
在待机模式下可以达到最低的电能消耗。内部的电压调压器被关闭,因此所有内部1.8V部分的供电被切断;PLL、HSI的RC振荡器和HSE晶体振荡器也被关闭;进入待机模式后,SRAM和寄存器的内容将消失,但后备寄存器的内容仍然保留,待机电路仍工作。从待机模式退出的条件是:NRST上的外部复位信号、IWDG复位、WKUP引脚上的一个上升边 沿或RTC的闹钟到时。
注意:在进入停机或待机模式时,RTC、IWDG和对应的时钟不会被停止。

主要使用睡眠模式,任何中断或事件都可以唤醒睡眠模式。Tickless低功耗模式通过调用指令 __WFI 实现睡眠模式
FreeRTOS系统中的所有其它任务都不在运行时(处于阻塞或挂起),会运行空闲任务。所以想不影响系统运行又降低功耗,可以在空闲任务执行的期间,让MCU 进入相应的低功耗模式。
由于滴答定时器频繁中断则会影响低功耗,所以FreeRTOS的Tickless低功耗模式会自动把滴答定时器的中断周期修改为低功耗运行时间,退出低功耗后再补上系统时钟节拍数。
16.3 Tickless模式相关配置项
| 配置项 | 说明 |
| configUSE_TICKLESS_IDLE | 使能低功耗 Tickless 模式,默认0 |
| configEXPECTED_IDLE_TIME_BEFORE_SLEEP | 系统进入相应低功耗模式的最短时长,默认2 |
| configPRE_SLEEP_PROCESSING(x) | 在系统进入低功耗模式前执行的事务,比如关闭外设时钟 |
| configPOST_SLEEP_PROCESSING(x) | 系统退出低功耗模式后执行的事务,比如开启之前关闭的外设时钟 |
16.4 实验
使用 FreeRTOS 中的Tickless低功耗模式:在11.3二值信号量实验案例中,加入低功耗模式,对比功耗结果,观察是否降低功耗。
FreeRTOSConfig.h代码清单:
#define configUSE_TICKLESS_IDLE 1
#include "freertos_demo.h"
#define configPRE_SLEEP_PROCESSING( x ) PRE_SLEEP_PROCESSING()
#define configPOST_SLEEP_PROCESSING( x ) POST_SLEEP_PROCESSING()
freertos_demo.c代码清单
//1)引入信号量头文件
#include "semphr.h"
//2)低功耗处理函数
/* 进入低功耗前所需要执行的操作 */
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();
}
//3)任务配置
/* 启动任务函数 */
#define START_TASK_PRIORITY 1
#define START_TASK_STACK_DEPTH 128
TaskHandle_t start_task_handler;
void Start_Task(void *pvParameters);
/* Task1 任务 配置 */
#define TASK1_PRIORITY 2
#define TASK1_STACK_DEPTH 128
TaskHandle_t task1_handler;
void Task1(void *pvParameters);
/* Task2 任务 配置 */
#define TASK2_PRIORITY 3
#define TASK2_STACK_DEPTH 128
TaskHandle_t task2_handler;
void Task2(void *pvParameters);
//4)入口函数
QueueHandle_t semphore_handle;
/**
* @description: FreeRTOS入口函数:创建任务函数并开始调度
* @return {*}
*/
void FreeRTOS_Start(void)
{
semphore_handle = xSemaphoreCreateBinary();
if (semphore_handle != NULL)
{
printf("二值信号量创建成功\r\n");
}
xTaskCreate((TaskFunction_t)Start_Task,
(char *)"Start_Task",
(configSTACK_DEPTH_TYPE)START_TASK_STACK_DEPTH,
(void *)NULL,
(UBaseType_t)START_TASK_PRIORITY,
(TaskHandle_t *)&start_task_handler);
vTaskStartScheduler();
}
//5)初始任务函数
void Start_Task(void *pvParameters)
{
taskENTER_CRITICAL(); /* 进入临界区 */
xTaskCreate((TaskFunction_t)Task1,
(char *)"Task1",
(configSTACK_DEPTH_TYPE)TASK1_STACK_DEPTH,
(void *)NULL,
(UBaseType_t)TASK1_PRIORITY,
(TaskHandle_t *)&task1_handler);
xTaskCreate((TaskFunction_t)Task2,
(char *)"Task2",
(configSTACK_DEPTH_TYPE)TASK2_STACK_DEPTH,
(void *)NULL,
(UBaseType_t)TASK2_PRIORITY,
(TaskHandle_t *)&task2_handler);
vTaskDelete(NULL);
taskEXIT_CRITICAL(); /* 退出临界区 */
}
//6)task1任务函数
/**
* @description: 释放二值信号量
* @param {void *} pvParameters
* @return {*}
*/
void Task1(void *pvParameters)
{
uint8_t key = 0;
BaseType_t err;
while (1)
{
key = Key_Detect();
if (key == KEY1_PRESS)
{
if (semphore_handle != NULL)
{
err = xSemaphoreGive(semphore_handle);
if (err == pdPASS)
{
printf("信号量释放成功\r\n");
}
else
printf("信号量释放失败\r\n");
}
}
vTaskDelay(10);
}
}
//7)task2任务函数
/**
* @description: 获取二值信号量
* @param {void *} pvParameters
* @return {*}
*/
void Task2(void *pvParameters)
{
uint32_t i = 0;
BaseType_t err;
while (1)
{
/* 一直等待获取信号量 */
err = xSemaphoreTake(semphore_handle, portMAX_DELAY);
if (err == pdTRUE)
{
printf("获取信号量成功\r\n");
}
else
{
printf("已超时%d\r\n", ++i);
}
}
}
十七、 FreeRTOS内存管理
17.1 简介
在使用 FreeRTOS 创建任务、队列、信号量等对象时,通常都有动态创建和静态创建的方式。动态方式提供了更灵活的内存管理,而静态方式则更注重内存的静态分配和控制。
如果是动态创建的,那么标准 C 库 malloc() 和 free() 函数有时可用于此目的,但是有以下缺点:
- 它们在嵌入式系统上并不总是可用。
- 它们占用了宝贵的代码空间。
- 它们不是线程安全的。
- 它们不是确定性的 (执行函数所需时间将因调用而异)。
- ...
所以更多的时候需要的不是一个替代的内存分配实现。一个嵌入式/实时系统的 RAM 和定时要求可能与另一个非常不同,所以单一的 RAM 分配算法将永远只适用于一个应用程序子集。为了避免此问题,FreeRTOS 将内存分配 API 保留在其可移植层,提供了五种内存管理算法:
- heap_1:最简单,不允许释放内存。
- heap_2:允许释放内存,但不会合并相邻的空闲块。
- heap_3:简单包装了标准 malloc() 和 free(),以保证线程安全。
- heap_4:合并相邻的空闲块以避免碎片化。包含绝对地址放置选项。
- heap_5:如同 heap_4,能够跨越多个不相邻内存区域的堆。
17.2 内存管理算法
17.2.1 heap_1 算法
heap_1 是最简单的实现方式。内存一经分配,它不允许内存再被释放。尽管如此,heap_1.c 还是适用于大量嵌入式应用程序。这是因为许多小型和深度嵌入的应用程序在系统启动时创建了所需的所有任务、队列、信号量等,并在程序的生命周期内使用所有这些对象(直到应用程序再次关闭或重新启动)。任何内容都不会被删除。

17.2.2 heap_2 算法
heap_2 使用最佳适应算法,并且与方案 1 不同,它允许释放先前分配的块,它不将相邻的空闲块组合成一个大块。
heap_2.c 适用于许多必须动态创建对象的小型实时系统 。
- 如果动态地创建和删除任务,且分配给正在创建任务的堆栈大小总是相同的,那么 heap2.c 可以在大多数情况下使用。
- 但是,如果分配给正在创建任务的堆栈的大小不是总相同,那么可用的空闲内存可能会被碎片化成许多小块,最终导致分配失败。
heap_2 使用最佳适应算法,该算法在空闲内存中选择与请求的内存大小最接近的块来分配内存。下面是一个简单的例子来说明最佳适应算法:

假设有一个空闲内存,其中包含以下块:
- 大小为 20 字节的空闲块。
- 大小为 15 字节的空闲块。
- 大小为 25 字节的空闲块。
现在有一个任务请求分配 18 字节的内存。最佳适应算法将选择大小为 20 字节的块,因为它与请求的大小最接近。在选择这个块后,分配器可能会将该块分割为两部分,一部分大小为 18 字节,用于任务的内存,另一部分大小为 2 字节,留作未分配的块。
17.2.3 heap_3 算法
heap_3使用 C 库的 malloc 和 free 函数来进行内存分配和释放。它通过分配固定大小的块来管理内存,这些块的大小在配置 FreeRTOS 时进行定义,不会动态改变。
假设我们使用 Heap_3 管理内存,其中块的大小固定为 32 字节。初始时,整个内存被分割成大小为 32 字节的块:
- 块 1(32 字节)。
- 块 2(32 字节)。
- 块 3(32 字节)。
现在,有一个任务请求分配 20 字节的内存。Heap_3 算法将选择块 1,并将其分割成两部分:
- 分配给任务的内存块(20 字节)。
- 剩余未分配的块(12 字节)。
再假设另一个任务请求分配 40 字节的内存。由于没有足够大的块可供分配,heap_3 将返回分配失败的状态。
heap_3 的特点是块大小固定,这样可以简化内存管理。然而,也因为块大小不可变,可能导致内存碎片问题,即一些块可能无法完全被利用,从而浪费了一些内存。
17.2.4 heap_4 算法
heap_4使用第一适应算法,并且会将相邻的空闲内存块合并成大内存块,减少内存碎片。
第一适应算法会在可用内存块中选择第一个足够大的内存块进行分配。

假设有一个内存块链表,其中包含以下顺序的内存块:
- 大小为 40 字节的块。
- 大小为 30 字节的块。
- 大小为 15 字节的块。
- 大小为 20 字节的块。
如果一个任务需要申请 25 字节的内存,第一适应算法将选择大小为 40 字节的块,因为它是第一个足够大以容纳任务需求的内存块。(如果是heap_2的最佳适应算法,会选择30字节的块)
17.2.5 heap_5 算法
heap_5使用与 heap_4 相同的第一适应和内存合并算法,允许堆跨越多个不相邻(非连续)内存区域。适用于内存地址不连续的复杂场景。
17.3 相关API函数介绍
| 函数 | 描述 |
| void * pvPortMalloc( size_t xWantedSize ); | 申请内存 |
| void vPortFree( void * pv ); | 释放内存 |
| size_t xPortGetFreeHeapSize( void ); | 获取当前空闲内存的大小 |
17.4 实验
学习FreeRTOS 内存管理的函数,观察内存变化情况:
- start_task:用来创建其他1个任务。
- task1:用于按键扫描,当KEY1按下则申请内存,当KEY2按下则释放内存,并打印剩余内存信息。
freertos_demo.c代码清单:
//1)任务配置
/* 启动任务函数 */
#define START_TASK_PRIORITY 1
#define START_TASK_STACK_DEPTH 128
TaskHandle_t start_task_handler;
void Start_Task(void *pvParameters);
/* Task1 任务 配置 */
#define TASK1_PRIORITY 2
#define TASK1_STACK_DEPTH 128
TaskHandle_t task1_handler;
void Task1(void *pvParameters);
//2)入口函数
/**
* @description: FreeRTOS入口函数:创建任务函数并开始调度
* @return {*}
*/
void FreeRTOS_Start(void)
{
xTaskCreate((TaskFunction_t)Start_Task,
(char *)"Start_Task",
(configSTACK_DEPTH_TYPE)START_TASK_STACK_DEPTH,
(void *)NULL,
(UBaseType_t)START_TASK_PRIORITY,
(TaskHandle_t *)&start_task_handler);
vTaskStartScheduler();
}
//3)初始任务函数
void Start_Task( void * pvParameters )
{
taskENTER_CRITICAL(); /* 进入临界区 */
xTaskCreate((TaskFunction_t ) Task1,
(char * ) "Task1",
(configSTACK_DEPTH_TYPE ) TASK1_STACK_DEPTH,
(void * ) NULL,
(UBaseType_t ) TASK1_PRIORITY,
(TaskHandle_t * ) &task1_handler );
vTaskDelete(NULL);
taskEXIT_CRITICAL(); /* 退出临界区 */
}
//4)task1任务函数
/**
* @description: 申请及释放内存,并显示空闲内存大小
* @param {void *} pvParameters
* @return {*}
*/
void Task1(void * pvParameters)
{
uint8_t key = 0, t = 0;
uint8_t * buf = NULL;
while(1)
{
key = Key_Detect();
if(key == KEY1_PRESS)
{
/* 申请内存 */
buf = pvPortMalloc(30);
if(buf != NULL)
{
printf("申请内存成功\r\n");
}
else
{
printf("申请内存失败\r\n");
}
}
else if(key == KEY2_PRESS)
{
if(buf != NULL)
{
/* 释放内存 */
vPortFree(buf);
printf("释放内存\r\n");
}
}
if(t++ > 50)
{
t = 0;
printf("剩余的空闲内存大小=%d\r\n",xPortGetFreeHeapSize());
}
vTaskDelay(10);
}
}
1万+



被折叠的 条评论
为什么被折叠?



