正点原子FreeRTOS(下)

更多干货推荐可以去牛客网看看,他们现在的IT题库内容很丰富,属于国内做的很好的了,而且是课程+刷题+面经+求职+讨论区分享,一站式求职学习网站,最最最重要的里面的资源全部免费!!!点击进入--------------》跳转接口
在这里插入图片描述

更多干货推荐可以去牛客网看看,他们现在的IT题库内容很丰富,属于国内做的很好的了,而且是课程+刷题+面经+求职+讨论区分享,一站式求职学习网站,最最最重要的里面的资源全部免费!!!点击进入--------------》跳转接口
在这里插入图片描述

目录

  • 第十六章FreeRTOS 事件标志组
    • 16.1 事件标志组简介
    • 16.2 创建事件标志组
    • 16.3 设置事件位
    • 16.4 获取事件标志组值
    • 16.5 等待指定的事件位
    • 16.6 事件标志组实验
      • 16.6.1 实验程序设计
      • 16.6.2 程序运行结果分析
  • 第十七章FreeRTOS 任务通知
    • 17.1 任务通知简介
    • 17.2 发送任务通知
    • 17.3 任务通知通用发送函数
      • 17.3.1 任务级任务通知通用发送函数
      • 17.3.2 中断级任务通知发送函数
    • 17.4 获取任务通知
    • 17.5 任务通知模拟二值信号量实验
      • 17.5.1 实验程序设计
      • 17.5.2 实验程序运行结果
    • 17.6 任务通知模拟计数型信号量实验
      • 17.6.1 实验程序设计
      • 17.6.2 实验程序运行结果
    • 17.7 任务通知模拟消息邮箱实验
      • 17.7.1 实验程序设计
      • 17.7.2 实验程序运行结果
    • 17.8 任务通知模拟事件标志组实验
      • 17.8.1 实验程序设计
      • 17.8.2 实验程序运行结果
  • 第十八章FreeRTOS 低功耗Tickless 模式
    • 18.1 STM32F1 低功耗模式
      • 18.1.1 睡眠(Sleep)模式
      • 18.1.2 停止(Stop)模式
      • 18.1.3 待机(Standby)模式
    • 18.2 Tickless 模式详解
      • 18.2.1 如何降低功耗?
      • 18.2.2 Tickless 具体实现
    • 18.3 低功耗Tickless 模式实验
      • 18.3.1 实验程序设计
      • 18.3.2 实验程序运行结果
  • 第十九章FreeRTOS 空闲任务
    • 19.1 空闲任务详解
      • 19.1.1 空闲任务简介
      • 19.1.2 空闲任务的创建
      • 19.1.3 空闲任务函数
    • 19.2 空闲任务钩子函数详解
      • 19.2.1 钩子函数
      • 19.2.2 空闲任务钩子函数
    • 19.3 空闲任务钩子函数实验
      • 19.3.1 实验程序设计
      • 19.3.2 实验程序运行结果
  • 第二十章FreeRTOS 内存管理
    • 20.1 FreeRTOS 内存管理简介
    • 20.2 内存碎片
    • 20.3 heap_1 内存分配方法
      • 20.3.1 分配方法简介
      • 20.3.2 内存申请函数详解
      • 20.3.3 内存释放函数详解
    • 20.4 heap_2 内存分配方法
      • 20.4.1 分配方法简介
      • 20.4.2 内存块详解
      • 20.4.3 内存堆初始化函数详解
      • 20.4.4 内存块插入函数详解
      • 20.4.5 内存申请函数详解
      • 20.4.6 内存释放函数详解
    • 20.5 heap_3 内存分配方法
    • 20.6 heap_4 内存分配方法
      • 20.6.1 分配方法简介
      • 20.6.2 内存堆初始化函数详解
      • 20.6.3 内存块插入函数详解
      • 20.6.4 内存申请函数详解
      • 20.6.5 内存释放函数详解
    • 20.7 heap_5 内存分配方法
    • 20.8 FreeRTOS 内存管理实验
      • 20.8.1 实验程序设计
      • 20.8.2 实验程序运行结果

第十六章FreeRTOS 事件标志组

前面我们学习了使用信号量来完成同步,但是使用信号量来同步的话任务只能与单个的事
件或任务进行同步。有时候某个任务可能会需要与多个事件或任务进行同步,此时信号量就无
能为力了。FreeRTOS 为此提供了一个可选的解决方法,那就是事件标志组。本章我们就来学习
一下FreeRTOS 中事件标志组的使用,本章分为如下几部分:
16.1 事件标志组简介
16.2 创建事件标志组
16.3 设置事件位
16.4 获取事件标志组值
16.5 等待指定的事件位
16.6 事件标志组实验

16.1 事件标志组简介

1、事件位(事件标志)
事件位用来表明某个事件是否发生,事件位通常用作事件标志,比如下面的几个例子:
●当收到一条消息并且把这条消息处理掉以后就可以将某个位(标志)置1,当队列中没有
消息需要处理的时候就可以将这个位(标志)置0。
●当把队列中的消息通过网络发送输出以后就可以将某个位(标志)置1,当没有数据需要
从网络发送出去的话就将这个位(标志)置0。
●现在需要向网络中发送一个心跳信息,将某个位(标志)置1。现在不需要向网络中发送
心跳信息,这个位(标志)置0。
2、事件组
一个事件组就是一组的事件位,事件组中的事件位通过位编号来访问,同样,以上面列出
的三个例子为例:
●事件标志组的bit0 表示队列中的消息是否处理掉。
●事件标志组的bit1 表示是否有消息需要从网络中发送出去。
●事件标志组的bit2 表示现在是否需要向网络发送心跳信息。
3、事件标志组和事件位的数据类型
事件标志组的数据类型为EventGroupHandle_t,当configUSE_16_BIT_TICKS 为1 的时候
事件标志组可以存储8 个事件位,当configUSE_16_BIT_TICKS 为0 的时候事件标志组存储24
个事件位。
事件标志组中的所有事件位都存储在一个无符号的EventBits_t 类型的变量中,EventBits_t
在event_groups.h 中有如下定义:

typedef TickType_t EventBits_t;

数据类型TickType_t 在文件portmacro.h 中有如下定义:

#if( configUSE_16_BIT_TICKS == 1 )typedef uint16_t TickType_t;#define portMAX_DELAY ( TickType_t ) 0xffff
#elsetypedef uint32_t TickType_t;#define portMAX_DELAY ( TickType_t ) 0xffffffffUL#define portTICK_TYPE_IS_ATOMIC 1
#endif

可以看出当configUSE_16_BIT_TICKS 为0 的时候TickType_t 是个32 位的数据类型,因
此EventBits_t 也是个32 位的数据类型。EventBits_t 类型的变量可以存储24 个事件位,另外的
那高8 位有其他用。事件位0 存放在这个变量的bit0 上,变量的bit1 就是事件位1,以此类推。
对于STM32 来说一个事件标志组最多可以存储24 个事件位,如图16.1.1 所示:
在这里插入图片描述

16.2 创建事件标志组

FreeRTOS 提供了两个用于创建事件标志组的函数,如表16.2.1 所示:
在这里插入图片描述
1、函数xEventGroupCreate()
此函数用于创建一个事件标志组,所需要的内存通过动态内存管理方法分配。由于内部处
理的原因,事件标志组可用的bit 数取决于configUSE_16_BIT_TICKS ,当
configUSE_16_BIT_TICKS1 为1 的时候事件标志组有8 个可用的位(bit0~bit7) ,当
configUSE_16_BIT_TICKS 为0 的时候事件标志组有24 个可用的位(bit0~bit23)。EventBits_t 类
型的变量用来存储事件标志组中的各个事件位,函数原型如下:

EventGroupHandle_t xEventGroupCreate( void )

参数:
无。
返回值:
NULL: 事件标志组创建失败。
其他值: 创建成功的事件标志组句柄。
2、函数xEventGroupCreateStatic()
此函数用于创建一个事件标志组定时器,所需要的内存需要用户自行分配,此函数原型如
下:

EventGroupHandle_t xEventGroupCreateStatic( StaticEventGroup_t *pxEventGroupBuffer )

参数:
pxEventGroupBuffer:参数指向一个StaticEventGroup_t 类型的变量,用来保存事件标志组结
构体。
返回值:
NULL: 事件标志组创建失败。
其他值: 创建成功的事件标志组句柄。

16.3 设置事件位

FreeRTOS 提供了4 个函数用来设置事件标志组中事件位(标志),事件位(标志)的设置包括
清零和置1 两种操作,这4 个函数如表16.3.1 所示:

函数描述
xEventGroupClearBits()将指定的事件位清零,用在任务中。
xEventGroupClearBitsFromISR()将指定的事件位清零,用在中断服务函数中
xEventGroupSetBits()将指定的事件位置1,用在任务中。
xEventGroupSetBitsFromISR()将指定的事件位置1,用在中断服务函数中。

1、函数xEventGroupClearBits()
将事件标志组中的指定事件位清零,此函数只能用在任务中,不能用在中断服务函数中!
中断服务函数有其他的API 函数。函数原型如下:

EventBits_t xEventGroupClearBits( EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToClear );

参数:
xEventGroup:要操作的事件标志组的句柄。
uxBitsToClear:要清零的事件位,比如要清除bit3 的话就设置为0X08。可以同时清除多个
bit,如设置为0X09 的话就是同时清除bit3 和bit0。
返回值:
任何值:将指定事件位清零之前的事件组值。
2、函数xEventGroupClearBitsFromISR()
此函数为函数xEventGroupClearBits()的中断级版本,也是将指定的事件位(标志)清零。此
函数用在中断服务函数中,此函数原型如下:

BaseType_t xEventGroupClearBitsFromISR( EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToSet );

参数:
xEventGroup:要操作的事件标志组的句柄。
uxBitsToClear:要清零的事件位,比如要清除bit3 的话就设置为0X08。可以同时清除多个
bit,如设置为0X09 的话就是同时清除bit3 和bit0。
返回值:
pdPASS:事件位清零成功。
pdFALSE: 事件位清零失败。
2、函数xEventGroupSetBits()
设置指定的事件位为1,此函数只能用在任务中,不能用于中断服务函数,此函数原型如
下:

EventBits_t xEventGroupSetBits( EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToSet );

参数:
xEventGroup:要操作的事件标志组的句柄。
uxBitsToClear:指定要置1 的事件位,比如要将bit3 值1 的话就设置为0X08。可以同时将多个bit 置1,如设置为0X09 的话就是同时将bit3 和bit0 置1。
返回值:
任何值:在将指定事件位置1 后的事件组值。
3、函数xEventGroupSetBitsFromISR()
此函数也用于将指定的事件位置1,此函数是xEventGroupSetBits()的中断版本,用在中断
服务函数中,函数原型如下:

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

参数:
xEventGroup:要操作的事件标志组的句柄。
uxBitsToClear:指定要置1 的事件位,比如要将bit3 值1 的话就设置为0X08。可以同时将
多个bit 置1,如设置为0X09 的话就是同时将bit3 和bit0 置1。
pxHigherPriorityTaskWoken:标记退出此函数以后是否进行任务切换,这个变量的值函数会自
动设置的,用户不用进行设置,用户只需要提供一个变量来保存
这个值就行了。当此值为pdTRUE 的时候在退出中断服务函数之
前一定要进行一次任务切换。
返回值:
pdPASS:事件位置1 成功。
pdFALSE: 事件位置1 失败。

16.4 获取事件标志组值

我们可以通过FreeRTOS 提供的API 函数来查询事件标准组值,FreeRTOS 一共提供了两个
这样的API 函数,如表16.4.1 所示:

在这里插入图片描述
1、函数xEventGroupGetBits()
此函数用于获取当前事件标志组的值,也就是各个事件位的值。此函数用在任务中,不能
用在中断服务函数中。此函数是个宏,真正执行的是函数xEventGroupClearBits(),函数原型如
下:

EventBits_t xEventGroupGetBits( EventGroupHandle_t xEventGroup )

参数:
xEventGroup:要获取的事件标志组的句柄。
返回值:
任何值:当前事件标志组的值。
2、函数xEventGroupGetBitsFromISR()
获取当前事件标志组的值,此函数是xEventGroupGetBits()的中断版本,函数原型如下:

EventBits_t xEventGroupGetBitsFromISR( EventGroupHandle_t xEventGroup )

参数:
xEventGroup:要获取的事件标志组的句柄。
返回值:
任何值:当前事件标志组的值。

16.5 等待指定的事件位

某个任务可能需要与多个事件进行同步,那么这个任务就需要等待并判断多个事件位(标
志),使用函数xEventGroupWaitBits()可以完成这个功能。调用函数以后如果任务要等待的事件
位还没有准备好(置1 或清零)的话任务就会进入阻塞态,直到阻塞时间到达或者所等待的事件
位准备好。函数原型如下:

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

参数:
xEventGroup:指定要等待的事件标志组。
uxBitsToWaitFord:指定要等待的事件位,比如要等待bit0 和(或)bit2 的时候此参数就是0X05,
如果要等待bit0 和(或)bit1 和(或)bit2 的时候此参数就是0X07,以此类推。
xClearOnExit:此参数要是为pdTRUE 的话,那么在退出此函数之前由参数uxBitsToWaitFor
所设置的这些事件位就会清零。如果设置位pdFALSE 的话这些事件位就
不会改变。
xWaitForAllBits:此参数如果设置为pdTRUE 的话,当uxBitsToWaitFor 所设置的这些事件
位都置1,或者指定的阻塞时间到的时候函数xEventGroupWaitBits()才会
返回。当此函数为pdFALSE 的话,只要uxBitsToWaitFor 所设置的这些事
件位其中的任意一个置1 ,或者指定的阻塞时间到的话函数
xEventGroupWaitBits()就会返回。
xTicksToWait:设置阻塞时间,单位为节拍数。
返回值:
任何值:返回当所等待的事件位置1 以后的事件标志组的值,或者阻塞时间到。根
据这个值我们就知道哪些事件位置1 了。如果函数因为阻塞时间到而返回的话那么这个返回值就不代表任何的含义。

16.6 事件标志组实验

16.6.1 实验程序设计

1、实验目的
学习FreeROTS 事件标志组的使用,包括创建事件标志组、将相应的事件位置1、等待相应
的事件位置1 等操作。
2、实验设计
本实验设计四个任务:start_task、eventsetbit_task、eventgroup_task 和eventquery_task 这四
个任务的任务功能如下:
start_task:用来创建其他三个任务和事件标志组。
eventsetbit_task:读取按键值,根据不同的按键值将事件标志组中相应的事件位置1,用来
模拟事件的发生。
eventgroup_task:同时等待事件标志组中的多个事件位,当这些事件位都置1 的话就执行
相应的处理,例程中是刷新LCD 指定区域的背景色。
eventquery_task:查询事件组的值,也就是各个事件位的值。获取到事件组值以后就将其显
示到LCD 上,并且也通过串口打印出来。
实验中还创建了一个事件标志组:EventGroupHandler,实验中用到了这个事件标志组的三
个事件位,分别位bit0,bit1 和bit2。
实验中会用到3 个按键:KEY0、KEY1 和KEY2,其中按键KEY1 和KEY2 为普通的输入
模式。按键KEY0 为中断输入模式,KEY0 用来演示如何在中断服务程序调用事件标志组的API
函数。
3、实验工程
FreeRTOS 实验16-1 FreeRTOS 事件标志组实验。
4、实验程序与分析
●任务设置

#define START_TASK_PRIO 1 //任务优先级
#define START_STK_SIZE 256 //任务堆栈大小
TaskHandle_t StartTask_Handler; //任务句柄
void start_task(void *pvParameters); //任务函数
#define EVENTSETBIT_TASK_PRIO 2 //任务优先级
#define EVENTSETBIT_STK_SIZE 256 //任务堆栈大小
TaskHandle_t EventSetBit_Handler; //任务句柄
void eventsetbit_task(void *pvParameters); //任务函数
#define EVENTGROUP_TASK_PRIO 3 //任务优先级
#define EVENTGROUP_STK_SIZE 256 //任务堆栈大小
TaskHandle_t EventGroupTask_Handler; //任务句柄
void eventgroup_task(void *pvParameters); //任务函数
#define EVENTQUERY_TASK_PRIO 4 //任务优先级
#define EVENTQUERY_STK_SIZE 256 //任务堆栈大小
TaskHandle_t EventQueryTask_Handler; //任务句柄
void eventquery_task(void *pvParameters); //任务函数

EventGroupHandle_t EventGroupHandler; //事件标志组句柄
#define EVENTBIT_0 (1<<0) //事件位
#define EVENTBIT_1 (1<<1)
#define EVENTBIT_2 (1<<2)
#define EVENTBIT_ALL (EVENTBIT_0|EVENTBIT_1|EVENTBIT_2)
//LCD 刷屏时使用的颜色
int lcd_discolor[14]={ WHITE, BLACK, BLUE, BRED,GRED, GBLUE, RED, MAGENTA,GREEN, CYAN, YELLOW, BROWN,BRRED, GRAY };

●main()函数

int main(void)
{NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);//设置系统中断优先级分组4delay_init(); //延时函数初始化uart_init(115200); //初始化串口LED_Init(); //初始化LEDKEY_Init(); //初始化按键EXTIX_Init(); //初始化外部中断BEEP_Init(); //初始化蜂鸣器LCD_Init(); //初始化LCDmy_mem_init(SRAMIN); //初始化内部内存池POINT_COLOR = RED;LCD_ShowString(30,10,200,16,16,"ATK STM32F103/407");LCD_ShowString(30,30,200,16,16,"FreeRTOS Examp 16-1");LCD_ShowString(30,50,200,16,16,"Event Group");LCD_ShowString(30,70,200,16,16,"ATOM@ALIENTEK");LCD_ShowString(30,90,200,16,16,"2016/11/25");POINT_COLOR = BLACK;LCD_DrawRectangle(5,130,234,314); //画矩形POINT_COLOR = BLUE;LCD_ShowString(30,110,220,16,16,"Event Group Value:0");//创建开始任务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(); //开启任务调度
}

●任务函数

//开始任务任务函数
void start_task(void *pvParameters)
{taskENTER_CRITICAL(); //进入临界区//创建事件标志组EventGroupHandler=xEventGroupCreate(); //创建事件标志组(1)//创建设置事件位的任务xTaskCreate((TaskFunction_t )eventsetbit_task,(const char* )"eventsetbit_task",(uint16_t )EVENTSETBIT_STK_SIZE,(void* )NULL,(UBaseType_t )EVENTSETBIT_TASK_PRIO,(TaskHandle_t* )&EventSetBit_Handler);//创建事件标志组处理任务xTaskCreate((TaskFunction_t )eventgroup_task,(const char* )"eventgroup_task",(uint16_t )EVENTGROUP_STK_SIZE,(void* )NULL,(UBaseType_t )EVENTGROUP_TASK_PRIO,(TaskHandle_t* )&EventGroupTask_Handler);//创建事件标志组查询任务xTaskCreate((TaskFunction_t )eventquery_task,(const char* )"eventquery_task",(uint16_t )EVENTQUERY_STK_SIZE,(void* )NULL,(UBaseType_t )EVENTQUERY_TASK_PRIO,(TaskHandle_t* )&EventQueryTask_Handler);vTaskDelete(StartTask_Handler); //删除开始任务taskEXIT_CRITICAL(); //退出临界区
}
//设置事件位的任务
void eventsetbit_task(void *pvParameters)
{u8 key;while(1){if(EventGroupHandler!=NULL){key=KEY_Scan(0);switch(key){case KEY1_PRES:xEventGroupSetBits(EventGroupHandler,EVENTBIT_1); (2)break;case KEY2_PRES:xEventGroupSetBits(EventGroupHandler,EVENTBIT_2); (3)break;}}vTaskDelay(10); //延时10ms,也就是10 个时钟节拍}
}
//事件标志组处理任务
void eventgroup_task(void *pvParameters)
{u8 num;EventBits_t EventValue;while(1){if(EventGroupHandler!=NULL){//等待事件组中的相应事件位EventValue=xEventGroupWaitBits((EventGroupHandle_t )EventGroupHandler, (4)(EventBits_t ) EVENTBIT_ALL,(BaseType_t )pdTRUE,(BaseType_t )pdTRUE,(TickType_t )portMAX_DELAY);printf("事件标志组的值:%d\r\n",EventValue);LCD_ShowxNum(174,110,EventValue,1,16,0);num++;LED1=!LED1;LCD_Fill(6,131,233,313,lcd_discolor[num%14]);}else{vTaskDelay(10); //延时10ms,也就是10 个时钟节拍}}
}
//事件查询任务
void eventquery_task(void *pvParameters)
{u8 num=0;EventBits_t NewValue,LastValue;while(1){if(EventGroupHandler!=NULL){NewValue=xEventGroupGetBits(EventGroupHandler); //获取事件组的(5)if(NewValue!=LastValue){LastValue=NewValue;printf("事件标志组的值:%d\r\n",NewValue);LCD_ShowxNum(174,110,NewValue,1,16,0);}}num++;if(num==0) //每500msLED0 闪烁一次{num=0;LED0=!LED0;}vTaskDelay(50); //延时50ms,也就是50 个时钟节拍}
}

(1)、首先调用函数xEventGroupCreate()创建一个事件标志组EventGroupHandler。
(2)、按下KEY1 键的时候就调用函数xEventGroupSetBits()将事件标志组的bit1 置1。
(3)、按下KEY2 键的时候调用函数xEventGroupSetBits()将事件标志组的bit2 值1。
(4)、调用函数xEventGroupWaitBits()同时等待事件标志组的bit0,bit1 和bit2,只有当这三
个事件都置1 的时候才会执行任务中的其他代码。
(5)、调用函数xEventGroupGetBits()查询事件标志组EventGroupHandler 的值变化,通过查看这些值的变化就可以分析出当前哪个事件位置1 了。
●中断初始化及处理过程
事件标志组EventGroupHandler 的事件位bit0是通过KEY0 的外部中断服务函数来设置的,
注意中断优先级的设置!本例程的中断优先级设置如下:

NVIC_InitStructure.NVIC_IRQChannel = EXTI4_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x06; //抢占优先级6
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x00; //子优先级0
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //使能外部中断通道
NVIC_Init(&NVIC_InitStructure); //初始化外设NVIC 寄存器

KEY0 的外部中断服务函数如下:

//事件标志组句柄        
extern EventGroupHandle_t EventGroupHandler;
//中断服务函数
void EXTI4_IRQHandler (void)
{BaseType_t Result,xHigherPriorityTaskWoken;delay_xms(50); //消抖if(KEY0==0){Result=xEventGroupSetBitsFromISR(EventGroupHandler,EVENTBIT_0,\ (1)&xHigherPriorityTaskWoken);if(Result!=pdFAIL){portYIELD_FROM_ISR(xHigherPriorityTaskWoken);}}EXTI_ClearITPendingBit(EXTI_Line4);//清除LINE4 上的中断标志位
}

(1)、在中断服务函数中通过调用xEventGroupSetBitsFromISR()来将事件标志组的事件位
bit0 置1。

16.6.2 程序运行结果分析

编译并下载实验代码到开发板中,打开串口调试助手,默认情况下LCD 显示如图16.6.2.1
所示:

在这里插入图片描述
通过按下KEY0、KEY1 和KEY2 来观察LCD 的变化和串口调试助手收到的信息,当KEY0、
KEY1 和KEY2 都有按下的时候LCD 指定区域的背景颜色就会刷新,因为三个事件位都置1
了,LCD 显示如图16.6.2.2 所示:

在这里插入图片描述
注意观察,当LCD 背景颜色刷新完成以后事件标志组就会清零,通过串口调试助手可以很
清楚的观察到事件标志组值的变化情况,如图16.6.2.3 所示:
在这里插入图片描述
从图16.6.2.3 中可以看出,每次事件标志组的值为7 的话就会紧接着将事件标志组的值清
零。这是因为当事件标志组的值为7 的话说明事件位bit0,bit1 和bit2 都置1 了,任务
eventgroup_task()所等待的事件满足了,然后函数xEventGroupWaitBits()就会将事件组清零,因
为我们在调用函数xEventGroupWaitBits()的时候就设置的要将事件组值清零的。

第十七章FreeRTOS 任务通知

从v8.2.0 版本开始,FreeRTOS 新增了任务通知(Task Notifictions)这个功能,可以使用任
务通知来代替信号量、消息队列、事件标志组等这些东西。使用任务通知的话效率会更高,本
章我们就来学习一下FreeRTOS 的任务通知功能,本章分为如下几部分:
17.1 任务通知简介
17.2 发送任务通知
17.3 任务通知通用发送函数
17.4 获取任务通知
17.5 任务通知模拟二值信号量实验
17.6 任务通知模拟计数型信号量实验
17.7 任务通知模拟消息邮箱实验
17.8 任务通知模拟事件标志组实验

17.1 任务通知简介

任务通知在FreeRTOS 中是一个可选的功能,要使用任务通知的话就需要将宏
configUSE_TASK_NOTIFICATIONS 定义为1。
FreeRTOS 的每个任务都有一个32 位的通知值,任务控制块中的成员变量ulNotifiedValue
就是这个通知值。任务通知是一个事件,假如某个任务通知的接收任务因为等待任务通知而阻
塞的话,向这个接收任务发送任务通知以后就会解除这个任务的阻塞状态。也可以更新接收任
务的任务通知值,任务通知可以通过如下方法更新接收任务的通知值:
●不覆盖接收任务的通知值(如果上次发送给接收任务的通知还没被处理)。
●覆盖接收任务的通知值。
●更新接收任务通知值的一个或多个bit。
●增加接收任务的通知值。
合理、灵活的使用上面这些更改任务通知值的方法可以在一些场合中替代队列、二值信号
量、计数型信号量和事件标志组。使用任务通知来实现二值信号量功能的时候,解除任务阻塞
的时间比直接使用二值信号量要快45%(FreeRTOS 官方测试结果,使用v8.1.2 版本中的二值信
号量,GCC 编译器,-O2 优化的条件下测试的,没有使能断言函数configASSERT()),并且使用
的RAM 更少!
任务通知的发送使用函数xTaskNotify()或者xTaskNotifyGive()(还有此函数的中断版本)来
完成,这个通知值会一直被保存着,直到接收任务调用函数xTaskNotifyWait() 或者
ulTaskNotifyTake()来获取这个通知值。假如接收任务因为等待任务通知而阻塞的话那么在接收
到任务通知以后就会解除阻塞态。
任务通知虽然可以提高速度,并且减少RAM 的使用,但是任务通知也是有使用限制的:
●FreeRTOS 的任务通知只能有一个接收任务,其实大多数的应用都是这种情况。
●接收任务可以因为接收任务通知而进入阻塞态,但是发送任务不会因为任务通知发送
失败而阻塞。

17.2 发送任务通知

任务通知发送函数有6 个,如表17.2.1 所示:
在这里插入图片描述
1、函数xTaskNotify()
此函数用于发送任务通知,此函数发送任务通知的时候带有通知值,此函数是个宏,真正
执行的函数xTaskGenericNotify(),函数原型如下:

BaseType_t xTaskNotify( TaskHandle_t xTaskToNotify,
uint32_t ulValue,
eNotifyAction eAction )

参数:
xTaskToNotify:任务句柄,指定任务通知是发送给哪个任务的。
ulValue:任务通知值。
eAction:任务通知更新的方法,eNotifyAction 是个枚举类型,在文件task.h 中有如下
定义:

typedef enum
{eNoAction = 0,eSetBits, //更新指定的biteIncrement, //通知值加一eSetValueWithOverwrite, //覆写的方式更新通知值eSetValueWithoutOverwrite //不覆写通知值
} eNotifyAction;

此参数可以选择枚举类型中的任意一个,不同的应用环境其选择也不同。
返回值:
pdFAIL: 当参数eAction 设置为eSetValueWithoutOverwrite 的时候,如果任务通知值没有
更新成功就返回pdFAIL。
pdPASS: eAction 设置为其他选项的时候统一返回pdPASS。
2、函数xTaskNotifyFromISR()
此函数用于发送任务通知,是函数xTaskNotify()的中断版本,此函数是个宏,真正执行的
是函数xTaskGenericNotifyFromISR(),此函数原型如下:

BaseType_t xTaskNotifyFromISR( TaskHandle_t xTaskToNotify,
uint32_t ulValue,
eNotifyAction eAction,
BaseType_t * pxHigherPriorityTaskWoken );

参数:
xTaskToNotify:任务句柄,指定任务通知是发送给哪个任务的。
ulValue:任务通知值。
eAction:任务通知更新的方法。
pxHigherPriorityTaskWoken: 记退出此函数以后是否进行任务切换,这个变量的值函数会自动
设置的,用户不用进行设置,用户只需要提供一个变量来保存这
个值就行了。当此值为pdTRUE 的时候在退出中断服务函数之
前一定要进行一次任务切换。

返回值:
pdFAIL: 当参数eAction 设置为eSetValueWithoutOverwrite 的时候,如果任务通知值没有
更新成功就返回pdFAIL。
pdPASS: eAction 设置为其他选项的时候统一返回pdPASS。
3、函数xTaskNotifyGive()
发送任务通知,相对于函数xTaskNotify(),此函数发送任务通知的时候不带有通知值。此
函数只是将任务通知值简单的加一,此函数是个宏,真正执行的是函数xTaskGenericNotify(),
此函数原型如下:
BaseType_t xTaskNotifyGive( TaskHandle_t xTaskToNotify );
参数:
xTaskToNotify:任务句柄,指定任务通知是发送给哪个任务的。
返回值:
pdPASS: 此函数只会返回pdPASS。
4、函数vTaskNotifyGiveFromISR()
此函数为xTaskNotifyGive()的中断版本,用在中断服务函数中,函数原型如下:

void vTaskNotifyGiveFromISR( TaskHandle_t xTaskHandle,
BaseType_t * pxHigherPriorityTaskWoken );

参数:
xTaskToNotify:任务句柄,指定任务通知是发送给哪个任务的。
pxHigherPriorityTaskWoken: 记退出此函数以后是否进行任务切换,这个变量的值函数会自动
设置的,用户不用进行设置,用户只需要提供一个变量来保存这
个值就行了。当此值为pdTRUE 的时候在退出中断服务函数之
前一定要进行一次任务切换。
返回值:
无。
5、函数xTaskNotifyAndQuery()
此函数和xTaskNotify()很类似,此函数比xTaskNotify()多一个参数,此参数用来保存更新
前的通知值。此函数是个宏,真正执行的是函数xTaskGenericNotify(),此函数原型如下:

BaseType_t xTaskNotifyAndQuery ( TaskHandle_t xTaskToNotify,
uint32_t ulValue,
eNotifyAction eAction
uint32_t * pulPreviousNotificationValue);

参数:
xTaskToNotify:任务句柄,指定任务通知是发送给哪个任务的。
ulValue:任务通知值。
eAction:任务通知更新的方法。
pulPreviousNotificationValue:用来保存更新前的任务通知值。
返回值:
pdFAIL: 当参数eAction 设置为eSetValueWithoutOverwrite 的时候,如果任务通知值没有
更新成功就返回pdFAIL。
pdPASS: eAction 设置为其他选项的时候统一返回pdPASS。
6、函数xTaskNotifyAndQueryFromISR()
此函数为xTaskNorityAndQuery()的中断版本,用在中断服务函数中。此函数同样为宏,真
正执行的是函数xTaskGenericNotifyFromISR(),此函数的原型如下:

BaseType_t xTaskNotifyAndQueryFromISR ( TaskHandle_t xTaskToNotify,
uint32_t ulValue,
eNotifyAction eAction,
uint32_t * pulPreviousNotificationValue
BaseType_t * pxHigherPriorityTaskWoken );

参数:
xTaskToNotify:任务句柄,指定任务通知是发送给哪个任务的。
ulValue:任务通知值。
eAction:任务通知更新的方法。
pulPreviousNotificationValue:用来保存更新前的任务通知值。
pxHigherPriorityTaskWoken: 记退出此函数以后是否进行任务切换,这个变量的值函数会自动
设置的,用户不用进行设置,用户只需要提供一个变量来保存这
个值就行了。当此值为pdTRUE 的时候在退出中断服务函数之
前一定要进行一次任务切换。
返回值:
pdFAIL: 当参数eAction 设置为eSetValueWithoutOverwrite 的时候,如果任务通知值没有
更新成功就返回pdFAIL。
pdPASS: eAction 设置为其他选项的时候统一返回pdPASS。

17.3 任务通知通用发送函数

17.3.1 任务级任务通知通用发送函数

在17.2 小节中我们学习了3 个任务级任务通知发送函数:xTaskNotify()、xTaskNotifyGive()
和xTaskNotifyAndQuery(),这三个函数最终调用的都是函数xTaskGenericNotify()!此函数在文
件tasks.c 中有如下定义,缩减后的函数如下:

BaseType_t xTaskGenericNotify( TaskHandle_t xTaskToNotify, //任务句柄uint32_t ulValue, //任务通知值eNotifyAction eAction, //任务通知更新方式uint32_t * pulPreviousNotificationValue )//保存更新前的//任务通知值
{TCB_t * pxTCB;BaseType_t xReturn = pdPASS;uint8_t ucOriginalNotifyState;configASSERT( xTaskToNotify );pxTCB = ( TCB_t * ) xTaskToNotify;taskENTER_CRITICAL();{if( pulPreviousNotificationValue != NULL ) (1){*pulPreviousNotificationValue = pxTCB->ulNotifiedValue; (2)}ucOriginalNotifyState = pxTCB->ucNotifyState; (3)pxTCB->ucNotifyState = taskNOTIFICATION_RECEIVED; (4)switch( eAction ){case eSetBits : (5)pxTCB->ulNotifiedValue |= ulValue;break;case eIncrement : (6)( pxTCB->ulNotifiedValue )++;break;case eSetValueWithOverwrite : (7)pxTCB->ulNotifiedValue = ulValue;break;case eSetValueWithoutOverwrite : (8)if( ucOriginalNotifyState != taskNOTIFICATION_RECEIVED ){pxTCB->ulNotifiedValue = ulValue;}else{xReturn = pdFAIL;}break;case eNoAction:break;}traceTASK_NOTIFY();//如果任务因为等待任务通知而进入阻塞态的话就需要解除阻塞if( ucOriginalNotifyState == taskWAITING_NOTIFICATION ) (9){( void ) uxListRemove( &( pxTCB->xStateListItem ) ); (10)prvAddTaskToReadyList( pxTCB ); (11)/******************************************************************//********************省略相关的条件编译代码************************//******************************************************************/if( pxTCB->uxPriority > pxCurrentTCB->uxPriority ) (12){//解除阻塞的任务优先级比当前运行的任务优先级高,所以需要进行//任务切换。taskYIELD_IF_USING_PREEMPTION();}else{mtCOVERAGE_TEST_MARKER();}}else{mtCOVERAGE_TEST_MARKER();}}taskEXIT_CRITICAL();return xReturn; (13)
}

(1)、判断参数pulPreviousNotificationValue 是否有效,因为此参数用来保存更新前的任务通
知值。
(2)、如果参数pulPreviousNotificationValue 有效的话就用此参数保存更新前的任务通知值。
(3)、保存任务通知状态,因为下面会修改这个状态,后面我们要根据这个状态来确定是否
将任务从阻塞态解除。
(4)、更新任务通知状态为taskNOTIFICATION_RECEIVED。
(5)、根据不同的更新方式做不同的处理,如果为eSetBits 的话就将指定的bit 置1。也就是
更新接收任务通知值的一个或多个bit。
(6)、如果更新方式为eIncrement 的话就将任务通知值加一。
(7)、如果更新方式为eSetValueWithOverwrite 的话就直接覆写原来的任务通知值。
(8)、如果更新方式为eSetValueWithoutOverwrite 的话就需要判断原来的任务通知值是否被
处理,如果已经被处理了就更新为任务通知值。如果此前的任务通知值话没有被处理的话就标记xReturn 为pdFAIL,后面会返回这个值。
(9)、根据(3)中保存的接收任务之前的状态值来判断是否有任务需要解除阻塞,如果在任务
通知值被更新前任务处于taskWAITING_NOTIFICATION 状态的话就说明有任务因为等待任务
通知值而进入了阻塞态。
(10)、将任务从状态列表中移除。
(11)、将任务重新添加到就绪列表中。
(12)、判断刚刚解除阻塞的任务优先级是否比当前正在运行的任务优先级高,如果是的话需
要进行一次任务切换。
(13)、返回xReturn 的值,pdFAIL 或pdPASS。

17.3.2 中断级任务通知发送函数

中断级任务通知发送函数也有三个,分别为:xTaskNotifyFromISR() 、
xTaskNotifyAndQueryFromISR()和vTaskNotifyGiveFromISR()。其中函数xTaskNotifyFromISR()
和xTaskNotifyAndQueryFromISR()最终调用的都是函数xTaskGenericNotifyFromISR(),此函数
的原型如下:

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

参数:
xTaskToNotify:任务句柄,指定任务通知是发送给哪个任务的。
ulValue:任务通知值。
eAction:任务通知更新的方法。
pulPreviousNotificationValue:用来保存更新前的任务通知值。
pxHigherPriorityTaskWoken: 记退出此函数以后是否进行任务切换,这个变量的值函数会自动
设置的,用户不用进行设置,用户只需要提供一个变量来保存这
个值就行了。当此值为pdTRUE 的时候在退出中断服务函数之
前一定要进行一次任务切换。
返回值:
pdFAIL: 当参数eAction 设置为eSetValueWithoutOverwrite 的时候,如果任务通知值没有
更新成功就返回pdFAIL。
pdPASS: eAction 设置为其他选项的时候统一返回pdPASS。
函数xTaskGenericNotifyFromISR()在文件tasks.c 中有定义,函数源码如下:

BaseType_t xTaskGenericNotifyFromISR( TaskHandle_t xTaskToNotify,uint32_t ulValue,eNotifyAction eAction,uint32_t * pulPreviousNotificationValue,BaseType_t * pxHigherPriorityTaskWoken )
{TCB_t * pxTCB;uint8_t ucOriginalNotifyState;BaseType_t xReturn = pdPASS;UBaseType_t uxSavedInterruptStatus;configASSERT( xTaskToNotify );portASSERT_IF_INTERRUPT_PRIORITY_INVALID();pxTCB = ( TCB_t * ) xTaskToNotify;uxSavedInterruptStatus = portSET_INTERRUPT_MASK_FROM_ISR();{if( pulPreviousNotificationValue != NULL ) (1){*pulPreviousNotificationValue = pxTCB->ulNotifiedValue;}ucOriginalNotifyState = pxTCB->ucNotifyState; (2)pxTCB->ucNotifyState = taskNOTIFICATION_RECEIVED; (3)switch( eAction ) (4){case eSetBits :pxTCB->ulNotifiedValue |= ulValue;break;case eIncrement :( pxTCB->ulNotifiedValue )++;break;case eSetValueWithOverwrite :pxTCB->ulNotifiedValue = ulValue;break;case eSetValueWithoutOverwrite :if( ucOriginalNotifyState != taskNOTIFICATION_RECEIVED ){pxTCB->ulNotifiedValue = ulValue;}else{xReturn = pdFAIL;}break;case eNoAction :break;}traceTASK_NOTIFY_FROM_ISR();//如果任务因为等待任务通知而进入阻塞态的话就需要解除阻塞if( ucOriginalNotifyState == taskWAITING_NOTIFICATION ) (5){configASSERT( listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) ==\NULL );if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE ) (6){( void ) uxListRemove( &( pxTCB->xStateListItem ) );prvAddTaskToReadyList( pxTCB );}else (7){vListInsertEnd( &( xPendingReadyList ), &( pxTCB->xEventListItem ) );}if( pxTCB->uxPriority > pxCurrentTCB->uxPriority ) (8){//解除阻塞的任务优先级比当前运行任务的优先级高,所以需要标记//在退出中断服务函数的时候需要做任务切换。if( pxHigherPriorityTaskWoken != NULL ){*pxHigherPriorityTaskWoken = pdTRUE;}else{xYieldPending = pdTRUE;}}else{mtCOVERAGE_TEST_MARKER();}}}portCLEAR_INTERRUPT_MASK_FROM_ISR( uxSavedInterruptStatus );return xReturn;
}

(1)、判断参数pulPreviousNotificationValue 是否有效,因为此参数用来保存更新前的任务通知值。如果参数pulPreviousNotificationValue 有效的话就用此参数保存更新前的任务通知值。
(2)、保存任务通知状态,因为下面会修改这个状态,后面我们要根据这个状态来确定是否
将任务解除阻塞态。
(3)、更新任务通知状态taskNOTIFICATION_RECEIVED。
(4)、根据不同的通知值更新方式来做不同的处理,与函数xTaskGenericNotify()的处理过程
一样。
(5)、根据(2)中保存的接收任务之前的状态值来判断是否有任务需要解除阻塞,如果在任务
通知值被更新前任务处于taskWAITING_NOTIFICATION 状态的话就说明有任务因为等待任务
通知值而进入了阻塞态。
(6)、判断任务调度器是否上锁,如果调度器没有上锁的话就将任务从状态列表中移除,然
后重新将任务添加到就绪列表中。
(7)、如果任务调度器上锁了的话就将任务添加到列表xPendingReadyList 中。
(8)、判断任务解除阻塞的任务优先级是否比当前任务优先级高,如果是的话就将
pxHigherPriorityTaskWoken 标记pdTRUE。如果参数pxHigherPriorityTaskWoken 无效的话就将
全局变量xYieldPending 标记为pdTRUE。
还有另外一个用于中断服务函数的任务通知发送函数vTaskNotifyGiveFromISR(),此函数
和xTaskGenericNotifyFromISR()极其类似。此函数用于将任务通知值加一,大家可以自行分析
一下此函数。

17.4 获取任务通知

获取任务通知的函数有两个,如表17.4.1 所示:

在这里插入图片描述
1、函数ulTaskNotifyTake()
此函数为获取任务通知函数,当任务通知用作二值信号量或者计数型信号量的时候可以使
用此函数来获取信号量,函数原型如下:

uint32_t ulTaskNotifyTake( BaseType_t xClearCountOnExit,
TickType_t xTicksToWait );

参数:
xClearCountOnExit:参数为pdFALSE 的话在退出函数ulTaskNotifyTake()的时候任务通知值
减一,类似计数型信号量。当此参数为pdTRUE 的话在退出函数的时候
任务任务通知值清零,类似二值信号量。
xTickToWait: 阻塞时间。
返回值:
任何值:任务通知值减少或者清零之前的值。
此函数在文件tasks.c 中有定义,代码如下:

uint32_t ulTaskNotifyTake( BaseType_t xClearCountOnExit, TickType_t xTicksToWait )
{uint32_t ulReturn;taskENTER_CRITICAL();{if( pxCurrentTCB->ulNotifiedValue == 0UL ) (1){pxCurrentTCB->ucNotifyState = taskWAITING_NOTIFICATION; (2)if( xTicksToWait > ( TickType_t ) 0 ) (3){prvAddCurrentTaskToDelayedList( xTicksToWait, pdTRUE );traceTASK_NOTIFY_TAKE_BLOCK();portYIELD_WITHIN_API();}else{mtCOVERAGE_TEST_MARKER();}}else{mtCOVERAGE_TEST_MARKER();}}taskEXIT_CRITICAL();taskENTER_CRITICAL();{traceTASK_NOTIFY_TAKE();ulReturn = pxCurrentTCB->ulNotifiedValue; (4)if( ulReturn != 0UL ) (5){if( xClearCountOnExit != pdFALSE ) (6){pxCurrentTCB->ulNotifiedValue = 0UL;}else{pxCurrentTCB->ulNotifiedValue = ulReturn - 1; (7)}}else{mtCOVERAGE_TEST_MARKER();}pxCurrentTCB->ucNotifyState = taskNOT_WAITING_NOTIFICATION; (8)}taskEXIT_CRITICAL();return ulReturn;
}

(1)、判断任务通知值是否为0,如果为0 的话说明还没有接收到任务通知。
(2)、修改任务通知状态为taskWAITING_NOTIFICATION。
(3)、如果阻塞时间不为0 的话就将任务添加到延时列表中,并且进行一次任务调度。
(4)、如果任务通知值不为0 的话就先获取任务通知值。
(5)、任务通知值大于0。
(6)、参数xClearCountOnExit 不为pdFALSE,那就将任务通知值清零。
(7)、如果参数xClearCountOnExit 为pdFALSE 的话那就将任务通知值减一。
(8)、更新任务通知状态为taskNOT_WAITING_NOTIFICATION。
2、函数xTaskNotifyWait()
此函数也是用来获取任务通知的,不过此函数比ulTaskNotifyTake()更为强大,不管任务通
知用作二值信号量、计数型信号量、队列和事件标志组中的哪一种,都可以使用此函数来获取
任务通知。但是当任务通知用作位置信号量和计数型信号量的时候推荐使用函数
ulTaskNotifyTake()。此函数原型如下:

BaseType_t xTaskNotifyWait( uint32_t ulBitsToClearOnEntry,uint32_t ulBitsToClearOnExit,uint32_t * pulNotificationValue,TickType_t xTicksToWait );

参数:
ulBitsToClearOnEntry:当没有接收到任务通知的时候将任务通知值与此参数的取反值进行按
位与运算,当此参数为0xffffffff 或者ULONG_MAX 的时候就会将任务
通知值清零。
ulBitsToClearOnExit:如果接收到了任务通知,在做完相应的处理退出函数之前将任务通知值
与此参数的取反值进行按位与运算,当此参数为0xffffffff 或者
ULONG_MAX 的时候就会将任务通知值清零。
pulNotificationValue:此参数用来保存任务通知值。
xTickToWait: 阻塞时间。
返回值:
pdTRUE:获取到了任务通知。
pdFALSE:任务通知获取失败。
此函数在文件tasks.c 中有定义,代码如下:

BaseType_t xTaskNotifyWait( uint32_t ulBitsToClearOnEntry,uint32_t ulBitsToClearOnExit,uint32_t * pulNotificationValue,TickType_t xTicksToWait )
{BaseType_t xReturn;taskENTER_CRITICAL();{if( pxCurrentTCB->ucNotifyState != taskNOTIFICATION_RECEIVED ) (1){pxCurrentTCB->ulNotifiedValue &= ~ulBitsToClearOnEntry; (2)pxCurrentTCB->ucNotifyState = taskWAITING_NOTIFICATION; (3)if( xTicksToWait > ( TickType_t ) 0 ) (4){prvAddCurrentTaskToDelayedList( xTicksToWait, pdTRUE );traceTASK_NOTIFY_WAIT_BLOCK();portYIELD_WITHIN_API();}else{mtCOVERAGE_TEST_MARKER();}}else{mtCOVERAGE_TEST_MARKER();}}taskEXIT_CRITICAL();taskENTER_CRITICAL();{traceTASK_NOTIFY_WAIT();if( pulNotificationValue != NULL ) (5){*pulNotificationValue = pxCurrentTCB->ulNotifiedValue;}if( pxCurrentTCB->ucNotifyState == taskWAITING_NOTIFICATION ) (6){xReturn = pdFALSE;}else{pxCurrentTCB->ulNotifiedValue &= ~ulBitsToClearOnExit; (7)xReturn = pdTRUE;}pxCurrentTCB->ucNotifyState = taskNOT_WAITING_NOTIFICATION; (8)}taskEXIT_CRITICAL();return xReturn;
}

(1)、任务同通状态不为taskNOTIFICATION_RECEIVED。
(2)、将任务通知值与参数ulBitsToClearOnEntry 的取反值进行按位与运算。
(3)、任务通知状态改为taskWAITING_NOTIFICATION。
(4)、如果阻塞时间大于0 的话就要将任务添加到延时列表中,并且进行一次任务切换。
(5)、如果任务通知状态为taskNOTIFICATION_RECEIVED,并且参数pulNotificationValue
有效的话就保存任务通知值。
(6)、如果任务通知的状态又变为taskWAITING_NOTIFICATION 的话就标记xRetur 为
pdFALSE。
(7)、如果任务通知的状态一直为taskNOTIFICATION_RECEIVED 的话就将任务通知的值
与参数ulBitsToClearOnExit 的取反值进行按位与运算,并且标记xReturn 为pdTRUE,表示获
取任务通知成功。
(8)、标记任务通知的状态为taskNOT_WAITING_NOTIFICATION。

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

前面说了,根据FreeRTOS 官方的统计,使用任务通知替代二值信号量的时候任务解除阻
塞的时间要快45%,并且需要的RAM 也更少。其实通过我们上面分析任务通知发送和获取函
数的过程可以看出,任务通知的代码量很少,所以执行时间与所需的RAM 也就相应的会减少。
二值信号量就是值最大为1 的信号量,这也是名字中“二值”的来源。当任务通知用于替
代二值信号量的时候任务通知值就会替代信号量值,函数ulTaskNotifyTake()就可以替代信号量
获取函数xSemaphoreTake(),函数ulTaskNotifyTake()的参数xClearCountOnExit 设置为pdTRUE。
这样在每次获取任务通知的时候模拟的信号量值就会清零。函数xTaskNotifyGive() 和
vTaskNotifyGiveFromISR()用于替代函数xSemaphoreGive()和xSemaphoreGiveFromISR()。接下
来我们通过一个实验来演示一下任务通知是如何用作二值信号量的。

17.5.1 实验程序设计

1、实验目的
FreeRTOS 中的任务通知可以用来模拟二值信号量,本实验就来学习如何使用任务通知功能模拟二值信号量。
2、实验设计
本实验是在“FreeRTOS 实验14-1 FreeRTOS 二值信号量实验”的基础上修改而来的,只是
将其中的二值信号量相关API 函数改为任务通知的API 函数。
3、实验工程
FreeRTOS 实验17-1 FreeRTOS 任务通知模拟二值信号量。
4、实验程序与分析
本实验是在“FreeRTOS 实验14-1 FreeRTOS 二值信号量实验”上修改而来的,所以几乎所
有的代码都相同,只是将二值信号量的发送和获取换成了任务通知的发送和获取。我们就挑其
中重要的代码讲解一下。
●任务函数

//DataProcess_task 函数
void DataProcess_task(void *pvParameters)
{u8 len=0;u8 CommandValue=COMMANDERR;u32 NotifyValue;u8 *CommandStr;POINT_COLOR=BLUE;while(1){NotifyValue=ulTaskNotifyTake(pdTRUE,portMAX_DELAY); //获取任务通知(1)if(NotifyValue==1) //清零之前的任务通知值为1,说明任务通知有效(2){len=USART_RX_STA&0x3fff; //得到此次接收到的数据长度CommandStr=mymalloc(SRAMIN,len+1); //申请内存sprintf((char*)CommandStr,"%s",USART_RX_BUF);CommandStr[len]='\0'; //加上字符串结尾符号LowerToCap(CommandStr,len); //将字符串转换为大写CommandValue=CommandProcess(CommandStr); //命令解析if(CommandValue!=COMMANDERR){LCD_Fill(10,90,210,110,WHITE); //清除显示区域LCD_ShowString(10,90,200,16,16,CommandStr);//在LCD 上显示命令printf("命令为:%s\r\n",CommandStr);switch(CommandValue) //处理命令{case LED1ON:LED1=0;break;case LED1OFF:LED1=1;break;case BEEPON:BEEP=1;break;case BEEPOFF:BEEP=0;break;}}else{printf("无效的命令,请重新输入!!\r\n");}USART_RX_STA=0;memset(USART_RX_BUF,0,USART_REC_LEN); //串口接收缓冲区清零myfree(SRAMIN,CommandStr); //释放内存}else{vTaskDelay(10); //延时10ms,也就是10 个时钟节拍}}
}

(1)、调用函数ulTaskNotifyTake()获取本任务的任务通知,函数ulTaskNotifyTake()的第一个
参数设置为pdTRUE。因为本实验是用任务通知模拟二值信号量的,所以需要在获取任务通知
以后将任务通知值清零。
(2)、函数ulTaskNotifyTake()的返回值就是清零之前的任务通知值,如果为1 的话就说明任
务通知值有效,相当于二值信号量有效。
●中断处理
任务通知的发送是在串口1 的接收中断服务函数中完成的,中断服务程序如下:

extern TaskHandle_t DataProcess_Handler;; //接收任务通知的任务句柄
void USART1_IRQHandler(void) //串口1 中断服务程序
{u8 Res;BaseType_t xHigherPriorityTaskWoken;if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET){Res =USART_ReceiveData(USART1);//(USART1->DR); //读取接收到的数据if((USART_RX_STA&0x8000)==0)//接收未完成{if(USART_RX_STA&0x4000)//接收到了0x0d{if(Res!=0x0a)USART_RX_STA=0; //接收错误,重新开始else USART_RX_STA|=0x8000; //接收完成了}else //还没收到0X0D{if(Res==0x0d)USART_RX_STA|=0x4000;else{USART_RX_BUF[USART_RX_STA&0X3FFF]=Res ;USART_RX_STA++;if(USART_RX_STA>(USART_REC_LEN-1))USART_RX_STA=0;}}}}//发送任务通知if((USART_RX_STA&0x8000)&&(DataProcess_Handler!=NULL)){//发送任务通知vTaskNotifyGiveFromISR(DataProcess_Handler,&xHigherPriorityTaskWoken); (1)//如果需要的话进行一次任务切换portYIELD_FROM_ISR(xHigherPriorityTaskWoken);}
}

(1)、调用函数vTaskNotifyGiveFromISR()向任务DataProcess_task 发送任务通知,此处取代
的是二值信号量发送函数。其他部分的代码和“FreeRTOS 实验14-1 FreeRTOS 二值信号量实
验”基本完全相同。
可以看出,使用任务通知替代二值信号量的过程还是非常简单的,只需要替换掉几个API
函数,然后做简单的处理即可!

17.5.2 实验程序运行结果

参考实验“FreeRTOS 实验14-1 FreeRTOS 二值信号量实验”。

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

不同与二值信号量,计数型信号量值可以大1,这个最大值在创建信号量的时候可以设置。
当计数型信号量有效的时候任务可以获取计数型信号量,信号量值只要大于0 就表示计数型信
号量有效。
当任务通知用作计数型信号量的时候获取信号量相当于获取任务通知值,使用函数ulTaskNotifyTake()来替代函数xSemaphoreTake()。函数ulTaskNotifyTake()的参数xClearOnExit
要设置为pdFLASE,这样每次获取任务通知成功以后任务通知值就会减一。使用任务通知发送
函数xTaskNotifyGive() 和vTaskNotifyGiveFromISR() 来替代计数型信号量释放函数
xSemaphoreGive()和xSemaphoreGiveFromISR()。下面通过一个实验来演示一下任务通知是如何
用作计数型信号量。

17.6.1 实验程序设计

1、实验目的
FreeRTOS 中的任务通知可以用来模拟计数型信号量,本实验就来学习如何使用任务通知
功能模拟计数型信号量。
2、实验设计
本实验是在“FreeRTOS 实验14-2 FreeRTOS 计数型信号量实验”的基础上修改而来的,只
是将其中的计数型信号量相关API 函数改为任务通知的API 函数。
3、实验工程
FreeRTOS 实验17-2 FreeRTOS 任务通知模拟计数型信号量。
4、实验程序与分析
本实验是在“FreeRTOS 实验14-2 FreeRTOS 计数型信号量实验”上修改而来的,大部分的
代码都是相同,我们就挑其中重要的代码讲解一下。
●任务函数

//释放计数型信号量任务函数
void SemapGive_task(void *pvParameters)
{u8 key,i=0;while(1){key=KEY_Scan(0); //扫描按键if(SemapTakeTask_Handler!=NULL){switch(key){case WKUP_PRES:xTaskNotifyGive(SemapTakeTask_Handler);//发送任务通知(1)break;}}i++;if(i==50){i=0;LED0=!LED0;}vTaskDelay(10); //延时10ms,也就是10 个时钟节拍}
}
//获取计数型信号量任务函数
void SemapTake_task(void *pvParameters)
{u8 num;uint32_t NotifyValue;while(1){NotifyValue=ulTaskNotifyTake(pdFALSE,portMAX_DELAY);//获取任务通知(2)num++;LCD_ShowxNum(166,111,NotifyValue-1,3,16,0); //显示当前任务通知值(3)LCD_Fill(6,131,233,313,lcd_discolor[num%14]); //刷屏LED1=!LED1;vTaskDelay(1000); //延时1s,也就是1000 个时钟节拍}
}

(1)、以前发送计数型信号量的函数改为发送任务通知的函数xTaskNotifyGive()。
(2)、调用任务通知获取函数ulTaskNotifyTake(),由于我们是用任务通知来模拟计数型信号
量的,所以函数ulTaskNotifyTake()的第一个参数要设置为pdFALSE。这样每获取成功一次任务
通知,任务通知值就会减一,类似技术型信号量。函数ulTaskNotifyTake()的返回值为任务通知
值减少之前的值。
(3)、在LCD 上显示当前任务通知值,注意,NotifyValue 要减一才是当前的任务通知值。

17.6.2 实验程序运行结果

参考实验“FreeRTOS 实验14-1 FreeRTOS 二值信号量实验”。

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

任务通知也可用来向任务发送数据,但是相对于用队列发送消息,任务通知向任务发送消
息会受到很多限制!
1、只能发送32 位的数据值。
2、消息被保存为任务的任务通知值,而且一次只能保存一个任务通知值,相当于队列长度
为1。
因此说任务通知可以模拟一个轻量级的消息邮箱而不是轻量级的消息队列。任务通知值就
是消息邮箱的值。
发送数据可以使用函数xTaskNotify()或者xTaskNotifyFromISR(),函数的参数eAction 设置
eSetValueWithOverwrite 或者eSetValueWithoutOverwrite 。如果参数eAction 为
eSetValueWithOverwrite 的话不管接收任务的通知值是否已经被处理,这个通知值都会被更新。
参数eAction 为eSetValueWithoutOverwrite 的话如果上一个任务通知值话还没有被处理,那么新
的任务通知值就不会更新。如果要读取任务通知值的话就使用函数xTaskNotifyWait()。下面通
过一个实验来演示一下任务通知是如何用作消息邮箱。

17.7.1 实验程序设计

1、实验目的
FreeRTOS 中的任务通知可以用来模拟消息邮箱,本实验就来学习如何使用任务通知功能
模拟消息邮箱。
2、实验设计
本实验设计三个任务:start_task、task1_task 、Keyprocess_task 这三个任务的任务功能如下:
start_task:用来创建其他2 个任务。
task1_task :读取按键的键值,然后将按键值作为任务通知发生给任务Keyprocess_task。
Keyprocess_task:按键处理任务,读取任务通知值,根据不同的通知值做相应的处理。
实验需要三个按键KEY_UP、KEY2 和KEY0,不同的按键对应不同的按键值,任务
task1_task()会将这些值作为任务通知发送给任务Keyprocess_task。
本实验是在“FreeRTOS 实验13-1 FreeRTOS 队列操作实验”的基础上修改而来的。
3、实验工程
FreeRTOS 实验17-3 FreeRTOS 任务通知模拟消息邮箱实验。
4、实验程序与分析
●任务设置

#define START_TASK_PRIO 1 //任务优先级
#define START_STK_SIZE 256 //任务堆栈大小
TaskHandle_t StartTask_Handler; //任务句柄
void start_task(void *pvParameters); //任务函数
#define TASK1_TASK_PRIO 2 //任务优先级
#define TASK1_STK_SIZE 256 //任务堆栈大小
TaskHandle_t Task1Task_Handler; //任务句柄
void task1_task(void *pvParameters); //任务函数
#define KEYPROCESS_TASK_PRIO 3 //任务优先级
#define KEYPROCESS_STK_SIZE 256 //任务堆栈大小
TaskHandle_t Keyprocess_Handler; //任务句柄
void Keyprocess_task(void *pvParameters); //任务函数
//LCD 刷屏时使用的颜色
int lcd_discolor[14]={ WHITE, BLACK, BLUE, BRED,GRED, GBLUE, RED, MAGENTA,GREEN, CYAN, YELLOW, BROWN,BRRED, GRAY };

●main()函数

int main(void)
{NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);//设置系统中断优先级分组4delay_init(); //延时函数初始化uart_init(115200); //初始化串口LED_Init(); //初始化LEDKEY_Init(); //初始化按键BEEP_Init(); //初始化蜂鸣器LCD_Init(); //初始化LCDmy_mem_init(SRAMIN); //初始化内部内存池POINT_COLOR = RED;LCD_ShowString(10,10,200,16,16,"ATK STM32F103/407");LCD_ShowString(10,30,200,16,16,"FreeRTOS Examp 18-3");LCD_ShowString(10,50,200,16,16,"Task Notify Maibox");LCD_ShowString(10,70,220,16,16,"KEY_UP:LED1 KEY2:BEEP");LCD_ShowString(10,90,200,16,16,"KEY0:Refresh LCD");POINT_COLOR = BLACK;LCD_DrawRectangle(5,125,234,314); //画矩形//创建开始任务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(); //开启任务调度
}

●任务函数

//开始任务任务函数
void start_task(void *pvParameters)
{taskENTER_CRITICAL(); //进入临界区//创建TASK1 任务xTaskCreate((TaskFunction_t )task1_task,(const char* )"task1_task",(uint16_t )TASK1_STK_SIZE,(void* )NULL,(UBaseType_t )TASK1_TASK_PRIO,(TaskHandle_t* )&Task1Task_Handler);//创建按键处理任务xTaskCreate((TaskFunction_t )Keyprocess_task,(const char* )"keyprocess_task",(uint16_t )KEYPROCESS_STK_SIZE,(void* )NULL,(UBaseType_t )KEYPROCESS_TASK_PRIO,(TaskHandle_t* )&Keyprocess_Handler);vTaskDelete(StartTask_Handler); //删除开始任务taskEXIT_CRITICAL(); //退出临界区
}
//task1 任务函数
void task1_task(void *pvParameters)
{u8 key,i=0;BaseType_t err;while(1){key=KEY_Scan(0); //扫描按键if((Keyprocess_Handler!=NULL)&&(key)){err=xTaskNotify((TaskHandle_t )Keyprocess_Handler, //任务句柄(1)(uint32_t )key, //任务通知值(eNotifyAction )eSetValueWithOverwrite); //覆写的方式if(err==pdFAIL){printf("任务通知发送失败\r\n");}}i++;if(i==50){i=0;LED0=!LED0;}vTaskDelay(10); //延时10ms,也就是10 个时钟节拍}
}
//Keyprocess_task 函数
void Keyprocess_task(void *pvParameters)
{u8 num,beepsta=1;uint32_t NotifyValue;BaseType_t err;while(1){err=xTaskNotifyWait((uint32_t )0x00, //进入函数的时候不清除任务bit (2)(uint32_t )ULONG_MAX,//退出函数的时候清除所有的bit(uint32_t* )&NotifyValue, //保存任务通知值(TickType_t )portMAX_DELAY); //阻塞时间if(err==pdTRUE) //获取任务通知成功{switch((u8)NotifyValue){case WKUP_PRES: //KEY_UP 控制LED1LED1=!LED1;break;case KEY2_PRES: //KEY2 控制蜂鸣器beepsta=!beepsta;BEEP=!BEEP;break;case KEY0_PRES: //KEY0 刷新LCD 背景num++;LCD_Fill(6,126,233,313,lcd_discolor[num%14]);break;}}}
}

(1)、调用函数xTaskNotify()发送任务通知,任务通知值为按键值,发送方式采用覆写的方
式。
(2)、调用函数xTaskNotifyWait()获取任务通知,获取到的任务通知其实就是(1)中发送的按
键值,然后根据不同的按键值做不同的处理。

17.7.2 实验程序运行结果

编译并下载实验代码到开发板中,通过按键来控制LED1 和蜂鸣器的开关、LCD 指定区域
背景色的刷新。本实验的运行结果和“FreeRTOS 实验13-1 FreeRTOS 队列操作实验”类似,只
是不能接收串口发送过来的数据而已。

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

事件标志组其实就是一组二进制事件标志(位),每个事件标志位的具体意义由应用程序编
写者来决定。当一个任务等待事件标志组中的某几个标志(位)的时候可以进入阻塞态,当任务
因为等待事件标志(位)而进入阻塞态以后这个任务就不会消耗CPU。
当任务通知用作事件标志组的话任务通知值就相当于事件组,这个时候任务通知值的每个
bit 用作事件标志( 位) 。函数xTaskNotifyWait() 替代事件标志组中的API 函数
xEventGroupWaitBits()。函数xTaskNotify()和xTaskNotifyFromISR()(函数的参数eAction 为
eSetBits)替代事件标志组中的API 函数xEventGroupSetBits ()和xEventGroupSetBitsFromISR()。
下面通过一个实验来演示一下任务通知是如何用作事件标志组。

17.8.1 实验程序设计

1、实验目的
FreeRTOS 中的任务通知可以用来模拟事件标志组,本实验就来学习如何使用任务通知功
能模拟事件标志组。
2、实验设计
本实验是在“FreeRTOS 实验16-1 FreeRTOS 事件标志组实验”的基础上修改而来的。
3、实验工程
FreeRTOS 实验17-4 FreeRTOS 任务通知模拟事件标志组实验。
4、实验程序与分析
本实验是在“FreeRTOS 实验16-1 FreeRTOS 事件标志组实验”上修改而来的,大部分的代
码都是相同,我们就挑其中重要的代码讲解一下:
●任务函数

//开始任务任务函数
void start_task(void *pvParameters)
{taskENTER_CRITICAL(); //进入临界区//创建设置事件位的任务xTaskCreate((TaskFunction_t )eventsetbit_task,(const char* )"eventsetbit_task",(uint16_t )EVENTSETBIT_STK_SIZE,(void* )NULL,(UBaseType_t )EVENTSETBIT_TASK_PRIO,(TaskHandle_t* )&EventSetBit_Handler);//创建事件标志组处理任务xTaskCreate((TaskFunction_t )eventgroup_task,(const char* )"eventgroup_task",(uint16_t )EVENTGROUP_STK_SIZE,(void* )NULL,(UBaseType_t )EVENTGROUP_TASK_PRIO,(TaskHandle_t* )&EventGroupTask_Handler);vTaskDelete(StartTask_Handler); //删除开始任务taskEXIT_CRITICAL(); //退出临界区
}
//设置事件位的任务
void eventsetbit_task(void *pvParameters)
{u8 key,i;while(1){if(EventGroupTask_Handler!=NULL){key=KEY_Scan(0);switch(key){case KEY1_PRES:xTaskNotify((TaskHandle_t )EventGroupTask_Handler, (1)(uint32_t )EVENTBIT_1,(eNotifyAction )eSetBits);break;case KEY2_PRES:xTaskNotify((TaskHandle_t )EventGroupTask_Handler, (2)(uint32_t )EVENTBIT_2,(eNotifyAction )eSetBits);break;}}i++;if(i==50){i=0;LED0=!LED0;}vTaskDelay(10); //延时10ms,也就是10 个时钟节拍}
}
//事件标志组处理任务
void eventgroup_task(void *pvParameters)
{u8 num=0,enevtvalue;static u8 event0flag,event1flag,event2flag;uint32_t NotifyValue;BaseType_t err;while(1){//获取任务通知值err=xTaskNotifyWait((uint32_t )0x00, //进入函数的时候不清除任务bit (3)(uint32_t )ULONG_MAX, //退出函数的时候清除所有的bit(uint32_t* )&NotifyValue, //保存任务通知值(TickType_t )portMAX_DELAY);//阻塞时间if(err==pdPASS) //任务通知获取成功{if((NotifyValue&EVENTBIT_0)!=0) //事件0 发生(4){event0flag=1;}else if((NotifyValue&EVENTBIT_1)!=0) //事件1 发生{event1flag=1;}else if((NotifyValue&EVENTBIT_2)!=0) //事件2 发生{event2flag=1;}enevtvalue=event0flag|(event1flag<<1)|(event2flag<<2); //模拟事件标志组值(5)printf("任务通知值为:%d\r\n",enevtvalue);LCD_ShowxNum(174,110,enevtvalue,1,16,0); //在LCD 上显示当前事件值if((event0flag==1)&&(event1flag==1)&&(event2flag==1)) //三个事件都同时发生(6){num++;LED1=!LED1;LCD_Fill(6,131,233,313,lcd_discolor[num%14]);event0flag=0; //标志清零(7)event1flag=0;event2flag=0;}}else{printf("任务通知获取失败\r\n");}}
}

(1)、按下KEY1 键以后调用函数xTaskNotify()发生任务通知,因为是模拟事件标志组的,
所以这里设置的是将任务通知值的bit1 置1。
(2)、按下KEY2 键以后调用函数xTaskNotify()发生任务通知,因为是模拟事件标志组的,
所以这里设置的是将任务通知值的bit2 置1。
(3)、调用函数xTaskNotifyWait()获取任务通知值,相当于获取事件标志组值。
(4)、根据NotifyValue 来判断是哪个事件发生了,如果是事件0 发生的话(也就是任务通知
值的bit0 置一)就给event0flag 赋1,表示事件0 发生了。因为任务通知不像事件标志组那样可以同时等待多个事件位。只要有任何一个事件位置1,函数xTaskNotifyWait()就会因为获取任务
通知成功而在退出函数的时候将这个事件位清零(函数xTaskNotifyWait()的参数设置的)。所以这
里我们要对每个发生的事件做个标志,比如本试验中用到了三个事件标志位:EVENTBIT_0、
EVENTBIT_1、EVENTBIT_2。这三个事件标志位代表三个事件,每个事件都有一个变量来记录
此事件是否已经发生,这三个变量在任务函数eventgroup_task()中有定义,它们分别为:
event0flag、event1flag 和event2flag。当这三个变量都为1 的时候就说明三个事件都发生了,这
不就模拟出来了事件标志组的同时等待多个事件位吗?
(5)、通过这三个事件发生标志来模拟事件标志组值。
(6)、所等待的三个事件都发生了,执行相应的处理。
(7)、处理完成以后将事件发生标志清零。
●中断处理过程

//事件标志组句柄
extern TaskHandle_t EventGroupTask_Handler;
//中断服务函数
void EXTI4_IRQHandler(void)
{BaseType_t xHigherPriorityTaskWoken;delay_xms(50); //消抖if(KEY0==0){xTaskNotifyFromISR((TaskHandle_t )EventGroupTask_Handler, //任务句柄(1)(uint32_t )EVENTBIT_0, //要更新的bit(eNotifyAction )eSetBits, //更新指定的bit(BaseType_t* )xHigherPriorityTaskWoken);portYIELD_FROM_ISR(xHigherPriorityTaskWoken);}EXTI_ClearITPendingBit(EXTI_Line4);//清除LINE4 上的中断标志位
}

(1)、中断服务函数中调用xTaskNotifyFromISR()来发送任务通知。

17.8.2 实验程序运行结果

参考实验“FreeRTOS 实验16-1 FreeRTOS 事件标志组实验”。

第十八章FreeRTOS 低功耗Tickless 模式

很多应用场合对于空耗的要求很严格,比如长期无人照看的数据采集仪器,可穿戴设备等。
其实很多MCU 都有相应的低功耗模式,以此来降低设备运行时的功耗,进行裸机开发的时候
就可以使用这些低功耗模式。但是现在我们要使用操作系统,因此操作系统对于低功耗的支持
也显得尤为重要,这样硬件与软件相结合,可以进一步降低系统的功耗。这样开发也会方便很
多,毕竟系统已经原生支持低功耗了,我们只需要按照系统的要求来做编写相应的应用层代码
即可。FreeRTOS 提供了一个叫做Tickless 的低功耗模式,本章我们就来学习一下如何使用这个
Tickless 模式,本章分为如下几部分:
18.1 STM32F1 低功耗模式
18.2 Tickless 模式详解
18.3 低功耗Tickless 模式实验

18.1 STM32F1 低功耗模式

STM32 本身就支持低功耗模式,以本教程使用的STM32F429 为例,共有三种低功耗模式:
●睡眠(Sleep)模式。
●停止(Stop)模式。
●待机(Standby)模式。
这三种模式对比如表18.1.1 所示:
在这里插入图片描述
这三种低功耗模式对应三种不同的功耗水平,根据实际的应用环境选择相对应的低功耗模
式。接下来我们就详细的看一下这三者有何区别。

18.1.1 睡眠(Sleep)模式

●进入睡眠模式
进入睡眠模式有两种指令:WFI(等待中断)和WFE(等待事件)。根据Cortex-M 内核的SCR(系
统控制)寄存器可以选择使用立即休眠还是退出时休眠,当SCR 寄存器的SLEEPONEXIT(bit1)
位为0 的时候使用立即休眠,当为1 的时候使用退出时休眠。关于立即休眠和退出时休眠的详
细内容请参考《权威指南》“第9 章低功耗和系统控制特性”章节。
CMSIS(Cortex 微控制器软件接口标准)提供了两个函数来操作指令WFI 和WFE,我们可以
直接使用这两个函数:__WFI 和__WFE。FreeRTOS 系统会使用WFI 指令进入休眠模式。
●退出休眠模式
如果使用WFI 指令进入休眠模式的话那么任意一个中断都会将MCU 从休眠模式中唤醒,
如果使用WFE 指令进入休眠模式的话那么当有事件发生的话就会退出休眠模式,比如配置一个EXIT 线作为事件。
当STM32F103 处于休眠模式的时候Cortex-M3 内核停止运行,但是其他外设运行正常,
比如NVIC、SRAM 等。休眠模式的功耗比其他两个高,但是休眠模式没有唤醒延时,应用程
序可以立即运行。

18.1.2 停止(Stop)模式

停止模式基于Cortex-M3 的深度休眠模式与外设时钟门控,在此模式下1.2V 域的所有时钟
都会停止,PLL、HSI 和HSE RC 振荡器会被禁止,但是内部SRAM 的数据会被保留。调压器
可以工作在正常模式,也可配置为低功耗模式。如果有必要的话可以通过将PWR_CR 寄存器的
FPDS 位置1 来使Flash 在停止模式的时候进入掉电状态,当Flash 处于掉电状态的时候MCU
从停止模式唤醒以后需要更多的启动延时。停止模式的进入和退出如表18.1.2.1 所示:

在这里插入图片描述

18.1.3 待机(Standby)模式

相比于前面两种低功耗模式,待机模式的功耗最低。待机模式是基于Cortex-M3 的深度睡
眠模式的,其中调压器被禁止。1.2V 域断电,PLL、HSI 振荡器和HSE 振荡器也被关闭。除了
备份区域和待机电路相关的寄存器外,SRAM 和其他寄存器的内容都将丢失。待机模式的进入
和退出如表18.1.3.1 所示:
在这里插入图片描述
退出待机模式的话会导致STM32F1 重启,所以待机模式的唤醒延时也是最大的。实际应
用中要根据使用环境和要求选择合适的待机模式。关于STM32 低功耗模式的详细介绍和使用
请参考ST 官方的参考手册。

18.2 Tickless 模式详解

18.2.1 如何降低功耗?

在11.4 小节讲解获取任务运行时间信息的时候可以看出,一般的简单应用中处理器大量的
时间都在处理空闲任务,所以我们就可以考虑当处理器处理空闲任务的时候就进入低功耗模式,
当需要处理应用层代码的时候就将处理器从低功耗模式唤醒。FreeRTOS 就是通过在处理器处
理空闲任务的时候将处理器设置为低功耗模式来降低能耗。一般会在空闲任务的钩子函数中执
行低功耗相关处理,比如设置处理器进入低功耗模式、关闭其他外设时钟、降低系统主频等等。
本章后面均以STM32F103 三个低功耗模式中的睡眠模式为例讲解。
我们知道FreeRTOS 的系统时钟是由滴答定时器中断来提供的,系统时钟频率越高,那么
滴答定时器中断频率也就越高。18.1 小节讲过,中断是可以将STM32F03 从睡眠模式中唤醒,
周期性的滴答定时器中断就会导致STM32F4103 周期性的进入和退出睡眠模式。因此,如果滴
答定时器中断频率太高的话会导致大量的能量和时间消耗在进出睡眠模式中,这样导致的结果
就是低功耗模式的作用被大大的削弱。
为此,FreeRTOS 特地提供了一个解决方法——Tickless 模式,当处理器进入空闲任务周期
以后就关闭系统节拍中断(滴答定时器中断),只有当其他中断发生或者其他任务需要处理的时
候处理器才会被从低功耗模式中唤醒。为此我们将面临两个问题:
问题一:关闭系统节拍中断会导致系统节拍计数器停止,系统时钟就会停止。
FreeRTOS 的系统时钟是依赖于系统节拍中断(滴答定时器中断)的,如果关闭了系统节拍中
断的话就会导致系统时钟停止运行,这是绝对不允许的!该如何解决这个问题呢?我们可以记
录下系统节拍中断的关闭时间,当系统节拍中断再次开启运行的时候补上这段时间就行了。这
时候我们就需要另外一个定时器来记录这段该补上的时间,如果使用专用的低功耗处理器的话
基本上都会有一个低功耗定时器,比如STM32L4 系列(L 系列是ST 的低功耗处理器)就有一个
叫做LPTIM(低功耗定时器)的定时器。STM32F103 没有这种定时器那么就接着使用滴答定时器
来完成这个功能,具体实现方法后面会讲解。
问题二:如何保证下一个要运行的任务能被准确的唤醒?
即使处理器进入了低功耗模式,但是我的中断和应用层任务也要保证及时的响应和处理。
中断自然不用说,本身就可以将处理器从低功耗模式中唤醒。但是应用层任务就不行了,它无
法将处理器从低功耗模式唤醒,无法唤醒就无法运行!这个问题看来很棘手,既然应用层任务
无法将处理器从低功耗模式唤醒,那么我们就借助其他的力量来完成这个功能。如果处理器在
进入低功耗模式之前能够获取到还有多长时间运行下一个任务那么问题就迎刃而解了,我们只
需要开一个定时器,定时器的定时周期设置为这个时间值就行了,定时时间到了以后产生定时
中断,处理器不就从低功耗模式唤醒了。这里似乎又引出了一个新的问题,那就是如何知道还
有多长时间执行下一个任务?这个时间也就是低功耗模式的执行时间,值得庆辛的是FreeRTOS
已经帮我们完成了这个工作。

18.2.2 Tickless 具体实现

1、宏configUSE_TICKLESS_IDLE
要想使用Tickless 模式,首先必须将FreeRTOSConfig.h 中的宏configUSE_TICKLESS_IDLE
设置为1,代码如下:

#define configUSE_TICKLESS_IDLE 1 //1 启用低功耗tickless 模式

2、宏portSUPPRESS_TICKS_AND_SLEEP()
使能Tickless 模式以后当下面两种情况都出现的时候FreeRTOS 内核就会调用宏
portSUPPRESS_TICKS_AND_SLEEP()来处理低功耗相关的工作。
●空闲任务是唯一可运行的任务,因为其他所有的任务都处于阻塞态或者挂起态。
●系统处于低功耗模式的时间至少大于configEXPECTED_IDLE_TIME_BEFORE_SLEEP
个时钟节拍,宏configEXPECTED_IDLE_TIME_BEFORE_SLEEP 默认在文件FreeRTOS.h 中定
义为2,我们可以在FreeRTOSConfig.h 中重新定义,此宏必须大于2!
portSUPPRESS_TICKS_AND_SLEEP()有个参数,此参数用来指定还有多长时间将有任务
进入就绪态,其实就是处理器进入低功耗模式的时长(单位为时钟节拍数),因为一旦有其他任
务进入就绪态处理器就必须退出低功耗模式去处理这个任务。
portSUPPRESS_TICKS_AND_SLEEP()应该是由用户根据自己所选择的平台来编写的,此宏会
被空闲任务调用来完成具体的低功耗工作。但是!如果使用STM32 的话编写这个宏的工作就不
用我们来完成了,因为FreeRTOS 已经帮我们做好了,有没有瞬间觉得好幸福啊。当然了你也
可以自己去重新编写,不使用FreeRTOS 提供的,如果自己编写的话需要先将
configUSE_TICKLESS_IDLE 设置为2。
宏portSUPPRESS_TICKS_AND_SLEEP 在文件portmacro.h 中如下定义:

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

从上面的代码可以看出portSUPPRESS_TICKS_AND_SLEEP() 的本质就是函数
vPortSuppressTicksAndSleep(),此函数在文件port.c 中有如下定义:

__weak void vPortSuppressTicksAndSleep( TickType_t xExpectedIdleTime )
{uint32_t ulReloadValue, ulCompleteTickPeriods, ulCompletedSysTickDecrements,\ulSysTickCTRL;TickType_t xModifiableIdleTime;//确保滴答定时器的Reload(重装载)值不会溢出,也就是不能超过滴答定时器最大计数值。if( xExpectedIdleTime > xMaximumPossibleSuppressedTicks ) (1){xExpectedIdleTime = xMaximumPossibleSuppressedTicks;}//停止滴答定时器。portNVIC_SYSTICK_CTRL_REG &= ~portNVIC_SYSTICK_ENABLE_BIT;//根据参数xExpectedIdleTime 来计算滴答定时器的重装载值。ulReloadValue = portNVIC_SYSTICK_CURRENT_VALUE_REG +\ (2)( ulTimerCountsForOneTick * ( xExpectedIdleTime - 1UL ) );if( ulReloadValue > ulStoppedTimerCompensation ) (3){ulReloadValue -= ulStoppedTimerCompensation;}__disable_irq(); (4)__dsb( portSY_FULL_READ_WRITE );__isb( portSY_FULL_READ_WRITE );//确认是否可以进入低功耗模式if( eTaskConfirmSleepModeStatus() == eAbortSleep ) (5){//不能进入低功耗模式,重新启动滴答定时器portNVIC_SYSTICK_LOAD_REG = portNVIC_SYSTICK_CURRENT_VALUE_REG;portNVIC_SYSTICK_CTRL_REG |= portNVIC_SYSTICK_ENABLE_BIT;portNVIC_SYSTICK_LOAD_REG = ulTimerCountsForOneTick - 1UL;__enable_irq(); (6)}else (7){//可以进入低功耗模式,设置滴答定时器portNVIC_SYSTICK_LOAD_REG = ulReloadValue; (8)portNVIC_SYSTICK_CURRENT_VALUE_REG = 0UL;portNVIC_SYSTICK_CTRL_REG |= portNVIC_SYSTICK_ENABLE_BIT;xModifiableIdleTime = xExpectedIdleTime;configPRE_SLEEP_PROCESSING( xModifiableIdleTime ); (9)if( xModifiableIdleTime > 0 ){__dsb( portSY_FULL_READ_WRITE );__wfi(); (10)__isb( portSY_FULL_READ_WRITE );}//当代码执行到这里的时候说明已经退出了低功耗模式!configPOST_SLEEP_PROCESSING( xExpectedIdleTime ); (11)//停止滴答定时器ulSysTickCTRL = portNVIC_SYSTICK_CTRL_REG; (12)portNVIC_SYSTICK_CTRL_REG = ( ulSysTickCTRL &\~portNVIC_SYSTICK_ENABLE_BIT );__enable_irq(); (13)//判断导致退出低功耗的是由外部中断引起的还是滴答定时器计时时间到引起的if( ( ulSysTickCTRL & portNVIC_SYSTICK_COUNT_FLAG_BIT ) != 0 ) (14){uint32_t ulCalculatedLoadValue;ulCalculatedLoadValue = ( ulTimerCountsForOneTick - 1UL ) - ( ulReloadValue -\portNVIC_SYSTICK_CURRENT_VALUE_REG );if( ( ulCalculatedLoadValue < ulStoppedTimerCompensation ) ||\( ulCalculatedLoadValue > ulTimerCountsForOneTick ) ){ulCalculatedLoadValue = ( ulTimerCountsForOneTick - 1UL );}portNVIC_SYSTICK_LOAD_REG = ulCalculatedLoadValue;ulCompleteTickPeriods = xExpectedIdleTime - 1UL;}else //外部中断唤醒的,需要进行时间补偿{ulCompletedSysTickDecrements = ( xExpectedIdleTime * \ulTimerCountsForOneTick ) -\portNVIC_SYSTICK_CURRENT_VALUE_REG;ulCompleteTickPeriods = ulCompletedSysTickDecrements / \ulTimerCountsForOneTick;portNVIC_SYSTICK_LOAD_REG = ( ( ulCompleteTickPeriods + 1UL ) * \ulTimerCountsForOneTick ) - ulCompletedSysTickDecrements;}//重新启动滴答定时器,滴答定时器的重装载值设置为正常值。portNVIC_SYSTICK_CURRENT_VALUE_REG = 0UL;portENTER_CRITICAL();{portNVIC_SYSTICK_CTRL_REG |= portNVIC_SYSTICK_ENABLE_BIT;vTaskStepTick( ulCompleteTickPeriods ); (15)portNVIC_SYSTICK_LOAD_REG = ulTimerCountsForOneTick - 1UL;}portEXIT_CRITICAL();}
}

(1)、参数xExpectedIdleTime 表示处理器将要在低功耗模式运行的时长(单位为时钟节拍数),
这个时间会使用滴答定时器来计时,但是滴答定时器的计数寄存器是24 位的,因此这个时间值
不能超过滴答定时器的最大计数值。xMaximumPossibleSuppressedTicks 是个静态全局变量,在
文件port.c 中有定义,此变量会在函数vPortSetupTimerInterrupt()中被重新赋值,代码如下:

ulTimerCountsForOneTick = ( configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ );
xMaximumPossibleSuppressedTicks = portMAX_24_BIT_NUMBER / ulTimerCountsForOneTick;

经过计算xMaximumPossibleSuppressedTicks=0xffffff/(72000000/1000)≈233,因此可以得出
进入低功耗模式的最大时长为233 个时钟节拍,注意!这个值要根据自己所使用的平台以及
FreeRTOS 的实际配置情况来计算。
(2)、根据参数xExpectedIdleTime 来计算滴答定时器的重装载值,因为处理器进入低功耗模
式以后的计时是由滴答定时器来完成的。
(3)、从滴答定时器停止运行到把统计得到的低功耗模式运行的这段时间补偿给FreeRTOS
系统时钟也是需要时间的,这期间也是有程序在运行的。这段程序运行的时间我们要留出来,
具体的时间没法去统计,因为平台不同、编译器的代码优化水平不同导致了程序的执行时间也
不同。这里只能大概的留出一个时间值,这个时间值由变量ulStoppedTimerCompensation 来确
定,这是一个全局变量,在文件port.c 中有定义。此变量也会在函数vPortSetupTimerInterrupt()
中被重新赋值,代码如下:

#define portMISSED_COUNTS_FACTOR ( 45UL )
ulStoppedTimerCompensation = portMISSED_COUNTS_FACTOR / ( configCPU_CLOCK_HZ / \
configSYSTICK_CLOCK_HZ );

由上面的公式可以得出:ulStoppedTimerCompensation=45/(72000000/72000000)=45。如果要
修改这个时间值的话直接修改宏portMISSED_COUNTS_FACTOR 即可。
(4)、在执行WFI 前设置寄存器PRIMASK 的话处理器可以由中断唤醒但是不会处理这些
中断,退出低功耗模式以后通过清除寄存器PRIMASK 来使ISR 得到执行,其实就是利用
PRIMASK 来延迟ISR 的执行。函数__disable_irq()是用来设置寄存器PRIMASK 的,清除寄存
器PRIMASK 使用函数__enable_irq(),这两个函数是由CMSIS-Core 提供的,如果所使用的例
程添加了CMSIS 相关文件的话就可以直接拿来用,ALIENTEK 的所有例程都添加了。
(5)、调用函数eTaskConfirmSleepModeStatus()来判断是否可以进入低功耗模式,此函数在
文件tasks.c 中有定义。此函数通过检查是否还有就绪任务来决定处理器能不能进入低功耗模式,
如果返回eAbortSleep 的话就表示不能进入低功耗模式,既然不能进入低功耗那就需要重新恢
复滴答定时器的运行。
(6)、调用函数__enable_irq()重新打开中断,因为在(4)中我们调用函数__disable_irq()关闭了
中断。
(7)、可以进入低功耗模式,完成低功耗相关设置。
(8)、进入低功耗模式的时间已经在(2)中计算出来了,这里将这个值写入到滴答定时器的重
装载寄存器中。
(9)、configPRE_SLEEP_PROCESSING()是个宏,在进入低功耗模式之前可能有一些其他的
事情要处理,比如降低系统时钟、关闭外设时钟、关闭板子某些硬件的电源等等,这些操作就
可以在这个宏中完成,后面会讲解这个宏如何使用。
(10)、使用WFI 指令使STM32F429 进入睡眠模式。
(11)、代码执行到这里说明处理器已经退出了低功耗模式,退出低功耗模式以后也可能需要
处理一些事情。比如恢复系统时钟,使能外设时钟,打开板子某些硬件的电源等等,这些操作
在宏configPOST_SLEEP_PROCESSING()中完成,后面会讲解这个宏如何使用。
(12)、读取滴答定时器CTRL(控制和状态)寄存器,后面要用。
(13)、调用函数__enable_irq()打开中断。
(14)、判断退出低功耗模式是由滴答定时器中断引起的还是由其他中断引起的,因为这两种
原因所对应的系统时钟赔偿值的计算方法不同,这个系统时钟补偿值的单位是时钟节拍。
(15)、调用函数vTaskStepTick()补偿系统时钟,函数参数是要补偿的值,此函数在文件tasks.c
中有如下定义:

void vTaskStepTick( const TickType_t xTicksToJump )
{configASSERT( ( xTickCount + xTicksToJump ) <= xNextTaskUnblockTime );xTickCount += xTicksToJump;traceINCREASE_TICK_COUNT( xTicksToJump );
}

可以看出,此函数很简单,只是给FreeRTOS 的系统时钟节拍计数器xTickCount 加上一个
补偿值而已。
3、宏configPRE_SLEEP_PROCESSING ()和configPOST_SLEEP_PROCESSING()
在真正的低功耗设计中不仅仅是将处理器设置到低功耗模式就行了,还需要做一些其他的
处理,比如:
●将处理器降低到合适的频率,因为频率越低功耗越小,甚至可以在进入低功耗模式以后
关闭系统时钟。
●修改时钟源,晶振的功耗肯定比处理器内部的时钟源高,进入低功耗模式以后可以切换
到内部时钟源,比如STM32 的内部RC 振荡器。
●关闭其他外设时钟,比如IO 口的时钟。
●关闭板子上其他功能模块电源,这个需要在产品硬件设计的时候就要处理好,比如可以
通过MOS 管来控制某个模块电源的开关,在处理器进入低功耗模式之前关闭这些模块的电源。
有关产品低功耗设计的方法还有很多,大家可以上网查找一下,上面列举出的这几点在处
理器进入低功耗模式之前就要完成处理。FreeRTOS 为我们提供了一个宏来完成这些操作,它就
是configPRE_SLEEP_PROCESSING(),这个宏的具体实现内容需要用户去编写。如果在进入低
功耗模式之前我们降低了处理器频率、关闭了某些外设时钟等的话,那在退出低功耗模式以后
就需要恢复处理器频率、重新打开外设时钟等,这个操作在宏
configPOST_SLEEP_PROCESSING()中完成,同样的这个宏的具体内容也需要用户去编写。这两
个宏会被函数vPortSuppressTicksAndSleep()调用,我们可以在FreeRTOSConfig.h 定义这两个宏,
如下:

/********************************************************************************/
/* FreeRTOS 与低功耗管理相关配置*/
/********************************************************************************/
extern void PreSleepProcessing(uint32_t ulExpectedIdleTime);
extern void PostSleepProcessing(uint32_t ulExpectedIdleTime);
//进入低功耗模式前要做的处理
#define configPRE_SLEEP_PROCESSING PreSleepProcessing
//退出低功耗模式后要做的处理
#define configPOST_SLEEP_PROCESSING PostSleepProcessing

函数PreSleepProcessing()和PostSleepProcessing()可以在任意一个C 文件中编写,本章对应
的例程是在main.c 文件中,函数的具体内容在下一节详解。
4、宏configEXPECTED_IDLE_TIME_BEFORE_SLEEP
处理器工作在低功耗模式的时间虽说没有任何限制,1 个时钟节拍也行,滴答定时器所能计时的最大值也行。但是时间太短的话意义也不大啊,就1 个时钟节拍,我这刚进去就得出来!
所以我们必须对工作在低功耗模式的时间做个限制,不能太短了,宏
configEXPECTED_IDLE_TIME_BEFORE_SLEEP 就是用来完成这个功能的。此宏默认在文件
FreeRTOS 中有定义,如下:

#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 个时钟节拍。如果要修改这个值的话可以在文件FreeRTOSConfi.h 中对其重新定义。
此宏会在空闲任务函数prvIdleTask()中使用!

18.3 低功耗Tickless 模式实验

18.3.1 实验程序设计

1、实验目的
学习如何使用FreeRTOS 的低功耗Tickless 模式,观察Tickless 模式对于降低系统功耗有无
帮助。
2、实验设计
对于功耗要求严格的场合一般不要求有太大的数据处理量,因为功耗与性能很难兼得。一
般的低功耗场合都是简单的数据采集设备或者小型的终端控制设备。它们的功能都很简单,周
期性的采集数据并且发送给上层,比如服务器,或者接收服务器发送来的指令执行相应的控制
操作,比如开灯关灯、开关电机等。
本实验我们就设计一个通过串口发送指定的指令来控制开发板上的LED1 和BEEP 开关的
实验,本实验就是对“FreeRTOS 实验14-1 FreeRTOS 二值信号量实验”的简单修改,只是在实验
14-1 的基础上增加了低功耗模式。最后我们可以直接通过对这两个实验的结果对比,观察
Tickless 模式对于降低功耗是否有用。
3、实验工程
FreeRTOS 实验18-1 FreeRTOS 低功耗Tickless 模式实验。
4、实验程序与分析
●低功耗相关函数

//进入低功耗模式前需要处理的事情
//ulExpectedIdleTime:低功耗模式运行时间
void PreSleepProcessing(uint32_t ulExpectedIdleTime)
{//关闭某些低功耗模式下不使用的外设时钟,此处只是演示性代码RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,DISABLE); (1)RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC,DISABLE);RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOD,DISABLE);RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOE,DISABLE);RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOF,DISABLE);RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOG,DISABLE);
}
//退出低功耗模式以后需要处理的事情
//ulExpectedIdleTime:低功耗模式运行时间
void PostSleepProcessing(uint32_t ulExpectedIdleTime)
{//退出低功耗模式以后打开那些被关闭的外设时钟,此处只是演示性代码RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE); (2)RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC,ENABLE);RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOD,ENABLE);RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOE,ENABLE);RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOF,ENABLE);RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOG,ENABLE);
}

(1)、进入低功耗模式以后关闭那些低功耗模式中不用的外设时钟,本实验中串口1 需要在
低功耗模式下使用,因此USART1 和GPIOA 的时钟不能被关闭,其他的外设时钟可以关闭。
这个要根据实际使用情况来设置,也可以在此函数中执行一些其他有利于降低功耗的处理。
(2)、退出低功耗模式以后需要打开函数PreSleepProcessing()中关闭的那些外设的时钟。
●任务设置

#define START_TASK_PRIO 1 //任务优先级
#define START_STK_SIZE 256 //任务堆栈大小
TaskHandle_t StartTask_Handler; //任务句柄
void start_task(void *pvParameters); //任务函数
#define TASK1_TASK_PRIO 2 //任务优先级
#define TASK1_STK_SIZE 256 //任务堆栈大小
TaskHandle_t Task1Task_Handler; //任务句柄
void task1_task(void *pvParameters); //任务函数
#define DATAPROCESS_TASK_PRIO 3 //任务优先级
#define DATAPROCESS_STK_SIZE 256 //任务堆栈大小
TaskHandle_t DataProcess_Handler; //任务句柄
void DataProcess_task(void *pvParameters); //任务函数
//二值信号量句柄
SemaphoreHandle_t BinarySemaphore; //二值信号量句柄
//用于命令解析用的命令值
#define LED1ON 1
#define LED1OFF 2
#define BEEPON 3
#define BEEPOFF 4
#define COMMANDERR 0XFF

●其他应用函数

//将字符串中的小写字母转换为大写
//str:要转换的字符串
//len:字符串长度
void LowerToCap(u8 *str,u8 len)
{u8 i;for(i=0;i<len;i++){if((96<str[i])&&(str[i]<123)) //小写字母str[i]=str[i]-32; //转换为大写}
}
//命令处理函数,将字符串命令转换成命令值
//str:命令
//返回值: 0XFF,命令错误;其他值,命令值
u8 CommandProcess(u8 *str)
{u8 CommandValue=COMMANDERR;if(strcmp((char*)str,"LED1ON")==0) CommandValue=LED1ON;else if(strcmp((char*)str,"LED1OFF")==0) CommandValue=LED1OFF;else if(strcmp((char*)str,"BEEPON")==0) CommandValue=BEEPON;else if(strcmp((char*)str,"BEEPOFF")==0) CommandValue=BEEPOFF;return CommandValue;
}

●main()函数

int main(void)
{NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4); //设置系统中断优先级分组4delay_init(); //延时函数初始化uart_init(115200); //初始化串口LED_Init(); //初始化LEDKEY_Init(); //初始化按键BEEP_Init(); //初始化蜂鸣器my_mem_init(SRAMIN); //初始化内部内存池//创建开始任务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(); //开启任务调度
}

●任务函数

//开始任务任务函数
void start_task(void *pvParameters)
{taskENTER_CRITICAL(); //进入临界区//创建二值信号量BinarySemaphore=xSemaphoreCreateBinary();//创建TASK1 任务xTaskCreate((TaskFunction_t )task1_task,(const char* )"task1_task",(uint16_t )TASK1_STK_SIZE,(void* )NULL,(UBaseType_t )TASK1_TASK_PRIO,(TaskHandle_t* )&Task1Task_Handler);//创建TASK2 任务xTaskCreate((TaskFunction_t )DataProcess_task,(const char* )"keyprocess_task",(uint16_t )DATAPROCESS_STK_SIZE,(void* )NULL,(UBaseType_t )DATAPROCESS_TASK_PRIO,(TaskHandle_t* )&DataProcess_Handler);vTaskDelete(StartTask_Handler); //删除开始任务taskEXIT_CRITICAL(); //退出临界区
}
//task1 任务函数
void task1_task(void *pvParameters)
{while(1){LED0=!LED0;vTaskDelay(500); //延时500ms,也就是500 个时钟节拍}
}
//DataProcess_task 函数
void DataProcess_task(void *pvParameters)
{u8 len=0;u8 CommandValue=COMMANDERR;BaseType_t err=pdFALSE;u8 *CommandStr;while(1){err=xSemaphoreTake(BinarySemaphore,portMAX_DELAY); //获取信号量if(err==pdTRUE) //获取信号量成功{len=USART_RX_STA&0x3fff; //得到此次接收到的数据长度CommandStr=mymalloc(SRAMIN,len+1); //申请内存sprintf((char*)CommandStr,"%s",USART_RX_BUF);CommandStr[len]='\0'; //加上字符串结尾符号LowerToCap(CommandStr,len); //将字符串转换为大写CommandValue=CommandProcess(CommandStr); //命令解析if(CommandValue!=COMMANDERR){printf("命令为:%s\r\n",CommandStr);switch(CommandValue) //处理命令{case LED1ON:LED1=0;break;case LED1OFF:LED1=1;break;case BEEPON:BEEP=1;break;case BEEPOFF:BEEP=0;break;}}else{printf("无效的命令,请重新输入!!\r\n");}USART_RX_STA=0;memset(USART_RX_BUF,0,USART_REC_LEN); //串口接收缓冲区清零myfree(SRAMIN,CommandStr); //释放内存}}
}

●中断初始化及处理过程
同实验“FreeRTOS 实验14-1 FreeRTOS 二值信号量实验”一样。
总体来说,本实验代码和“FreeRTOS 实验14-1 FreeRTOS 二值信号量实验”基本一样,只
是有微小的改动。

18.3.2 实验程序运行结果

编译并下载实验代码到开发板中,打开串口调试助手,通过串口调试助手发送命令就可以
控制LED1 或者BEEP 的开关,操作方法和“FreeRTOS 实验14-1 FreeRTOS 二值信号量实验”
一样,这不是本章的重点,本章的重点是观察Tickless 模式是否能够降低系统功耗。
既然要观察功耗肯定需要一个能测量功耗的仪器,仪器的灵敏度尽可能高一点,这里我用
的是网上买来的一个测量手机充电电流和功耗的小仪器,通过USB 线连接到开发板的USB_232
接口为开发板提供电能,仪器的显示界面内容如图19.3.2.1 所示:
在这里插入图片描述
测量注意事项:
●关闭板子其他所有的供电接口,确保板子的供电电路只有一条有效。本实验中笔者通过
开发板的USB_232 接口为开发板供电。
●拔掉板子上的液晶屏、JLINK、STLink 等外接模块,防止对测量结果造成干扰。
●由于开发板硬件并没有做低功耗处理,在通过命令控制LED1 或者BEEP 开关以后功耗
可能会增大,即使进入低功耗模式功耗也没法降低到原来的水平。这是因为外设相关电路开始
工作了,但是进入低功耗模式以后没法去关闭这些电路导致的,属于正常现象。
1、开启Tickless 模式
打开Tickless 模式,也就是将宏configUSE_TICKLESS_IDLE 设置为1,经过测量系统的整
体功耗如图18.3.2.2 所示:
在这里插入图片描述
打开Tickless 模式以后板子的工作电压为:5.13V,工作电流为:0.116A=116mA,功率为:
0.595W=595mW。
2、关闭Tickless 模式
关闭Tickless 模式,将宏configUSE_TICKLESS_IDLE 设置为0,经过测量系统的整体功耗
如图18.3.2.3 所示:
在这里插入图片描述
关闭Tickless 模式以后板子的工作电压为:5.13V,工作电流为:0.163A=163mA,功率为
0.836W=836mW。
3、总结
通过对比可以看出当开启了FreeRTOS 的Tickless 模式以后系统的工作电流降低了163-116=47mA,功率降低了:836-595=241mW。这个还只是STM32F429 芯片自身的睡眠模式带来
的功耗收益,如果再配合硬件上的设计、选择其他低功耗模式那么功耗肯定会压到一个很低的
水平。

第十九章FreeRTOS 空闲任务

空闲任务是FreeRTOS 必不可少的一个任务,其他RTOS 类系统也有空闲任务,比如uC/OS。
看名字就知道,空闲任务是处理器空闲的时候去运行的一个任务,当系统中没有其他就绪任务
的时候空闲任务就会开始运行,空闲任务最重要的作用就是让处理器在无事可做的时候找点事
做,防止处理器无聊,因此,空闲任务的优先级肯定是最低的。当然了,实际上肯定不会这么
浪费宝贵的处理器资源,FreeRTOS 空闲任务中也会执行一些其他的处理。本章我们就来学习一
下FreeRTOS 中的空闲任务,本章分为如下几部分:
19.1 空闲任务详解
19.2 空闲任务钩子函数详解
19.3 空闲任务钩子函数实验

19.1 空闲任务详解

19.1.1 空闲任务简介

当FreeRTOS 的调度器启动以后就会自动的创建一个空闲任务,这样就可以确保至少有一
任务可以运行。但是这个空闲任务使用最低优先级,如果应用中有其他高优先级任务处于就绪
态的话这个空闲任务就不会跟高优先级的任务抢占CPU 资源。空闲任务还有另外一个重要的职
责,如果某个任务要调用函数vTaskDelete()删除自身,那么这个任务的任务控制块TCB 和任务
堆栈等这些由FreeRTOS 系统自动分配的内存需要在空闲任务中释放掉,如果删除的是别的任
务那么相应的内存就会被直接释放掉,不需要在空闲任务中释放。因此,一定要给空闲任务执
行的机会!除此以外空闲任务就没有什么特别重要的功能了,所以可以根据实际情况减少空闲
任务使用CPU 的时间(比如,当CPU 运行空闲任务的时候使处理器进入低功耗模式)。
用户可以创建与空闲任务优先级相同的应用任务,当宏configIDLE_SHOULD_YIELD 为1
的话应用任务就可以使用空闲任务的时间片,也就是说空闲任务会让出时间片给同优先级的应
用任务。这种方法在3.2 节讲解configIDLE_SHOULD_YIELD 的时候就讲过了,这种机制要求
FreeRTOS 使用抢占式内核。

19.1.2 空闲任务的创建

当调用函数vTaskStartScheduler()启动任务调度器的时候此函数就会自动创建空闲任务,代
码如下:

void vTaskStartScheduler( void )
{BaseType_t xReturn;//创建空闲任务,使用最低优先级
#if( configSUPPORT_STATIC_ALLOCATION == 1 ) (1){StaticTask_t *pxIdleTaskTCBBuffer = NULL;StackType_t *pxIdleTaskStackBuffer = NULL;uint32_t ulIdleTaskStackSize;vApplicationGetIdleTaskMemory( &pxIdleTaskTCBBuffer, &pxIdleTaskStackBuffer, \&ulIdleTaskStackSize );xIdleTaskHandle = xTaskCreateStatic( prvIdleTask,"IDLE",ulIdleTaskStackSize,( void * ) NULL,( tskIDLE_PRIORITY | portPRIVILEGE_BIT ),pxIdleTaskStackBuffer,pxIdleTaskTCBBuffer );if( xIdleTaskHandle != NULL ){xReturn = pdPASS;}else{xReturn = pdFAIL;}}
#else (2){xReturn = xTaskCreate( prvIdleTask,"IDLE",configMINIMAL_STACK_SIZE,( void * ) NULL,( tskIDLE_PRIORITY | portPRIVILEGE_BIT ),&xIdleTaskHandle );}
#endif /* configSUPPORT_STATIC_ALLOCATION *//*********************************************************************//**************************省略其他代码*******************************//*********************************************************************/
}

(1)、使用静态方法创建空闲任务。
(2)、使用动态方法创建空闲任务,空闲任务的任务函数为prvIdleTask(),任务堆栈大小为
configMINIMAL_STACK_SIZE,任务堆栈大小可以在FreeRTOSConfig.h 中修改。任务优先级为
tskIDLE_PRIORITY,宏tskIDLE_PRIORITY 为0,说明空闲任务优先级最低,用户不能随意修
改空闲任务的优先级!

19.1.3 空闲任务函数

空闲任务的任务函数为prvIdleTask(),但是实际上是找不到这个函数的,因为它是通过宏
定义来实现的,在文件portmacro.h 中有如下宏定义:

#define portTASK_FUNCTION( vFunction, pvParameters ) void vFunction( void *pvParameters )

其中portTASK_FUNCTION()在文件tasks.c 中有定义,它就是空闲任务的任务函数,源码
如下:

static portTASK_FUNCTION( prvIdleTask, pvParameters ) (1)
{( void ) pvParameters; //防止报错//本函数为FreeRTOS 的空闲任务任务函数,当任务调度器启动以后空闲任务会自动//创建for( ;; ){//检查是否有任务要删除自己,如果有的话就释放这些任务的任务控制块TCB 和//任务堆栈的内存prvCheckTasksWaitingTermination(); (2)
#if ( configUSE_PREEMPTION == 0 ){//如果没有使用抢占式内核的话就强制进行一次任务切换查看是否有其他//任务有效,如果有使用抢占式内核的话就不需要这一步,因为只要有任//何任务有效(就绪)之后都会自动的抢夺CPU 使用权taskYIELD();}
#endif /* configUSE_PREEMPTION */
#if ( ( configUSE_PREEMPTION == 1 ) && ( configIDLE_SHOULD_YIELD == 1 ) ) (3){//如果使用抢占式内核并且使能时间片调度的话,当有任务和空闲任务共享//一个优先级的时候,并且此任务处于就绪态的话空闲任务就应该放弃本时//间片,将本时间片剩余的时间让给这个就绪任务。如果在空闲任务优先级//下的就绪列表中有多个用户任务的话就执行这些任务。if( listCURRENT_LIST_LENGTH( \ (4)&( pxReadyTasksLists[ tskIDLE_PRIORITY ] ) )> ( UBaseType_t ) 1 ){taskYIELD();}else{mtCOVERAGE_TEST_MARKER();}}
#endif
#if ( configUSE_IDLE_HOOK == 1){extern void vApplicationIdleHook( void );//执行用户定义的空闲任务钩子函数,注意!钩子函数里面不能使用任何//可以引起阻塞空闲任务的API 函数。vApplicationIdleHook(); (5)}
#endif /* configUSE_IDLE_HOOK *///如果使能了Tickless 模式的话就执行相关的处理代码
#if ( configUSE_TICKLESS_IDLE != 0 ) (6){TickType_t xExpectedIdleTime;xExpectedIdleTime = prvGetExpectedIdleTime(); (7)if( xExpectedIdleTime >= configEXPECTED_IDLE_TIME_BEFORE_SLEEP ) (8){vTaskSuspendAll(); (9){//调度器已经被挂起,重新采集一次时间值,这次的时间值可以//使用configASSERT( xNextTaskUnblockTime >= xTickCount );xExpectedIdleTime = prvGetExpectedIdleTime(); (10)if( xExpectedIdleTime >=\configEXPECTED_IDLE_TIME_BEFORE_SLEEP ){traceLOW_POWER_IDLE_BEGIN();portSUPPRESS_TICKS_AND_SLEEP( xExpectedIdleTime ); (11)traceLOW_POWER_IDLE_END();}else{mtCOVERAGE_TEST_MARKER();}}( void ) xTaskResumeAll(); (12)}else{mtCOVERAGE_TEST_MARKER();}}
#endif /* configUSE_TICKLESS_IDLE */}
}

(1)、将此行展开就是static void prvIdleTask(void *pvParameters),创建空闲任务的时候任务函数名就是prvIdleTask()。
(2)、调用函数prvCheckTasksWaitingTermination()检查是否有需要释放内存的被删除任务,
当有任务调用函数vTaskDelete() 删除自身的话,此任务就会添加到列表
xTasksWaitingTermination 中。函数prvCheckTasksWaitingTermination() 会检查列表
xTasksWaitingTermination 是否为空,如果不为空的话就依次将列表中所有任务对应的内存释放
掉(任务控制块TCB 和任务堆栈的内存)。
(3)、使用抢占式内核并且configIDLE_SHOULD_YIELD 为1,说明空闲任务需要让出时间
片给同优先级的其他就绪任务。
(4)、检查优先级为tskIDLE_PRIORITY(空闲任务优先级)的就绪任务列表是否为空,如果不
为空的话就调用函数taskYIELD()进行一次任务切换。
(5)、如果使能了空闲任务钩子函数的话就执行这个钩子函数,空闲任务钩子函数的函数名
为vApplicationIdleHook(),这个函数需要用户自行编写!在编写这个这个钩子函数的时候一定
不能调用任何可以阻塞空闲任务的API 函数。
(6)、configUSE_TICKLESS_IDLE 不为0,说明使能了FreeRTOS 的低功耗Tickless 模式。
(7)、调用函数prvGetExpectedIdleTime()获取处理器进入低功耗模式的时长,此值保存在变
量xExpectedIdleTime 中,单位为时钟节拍数。
(8)、xExpectedIdleTime 值要大于configEXPECTED_IDLE_TIME_BEFORE_SLEEP 才有效,
原因在上一章讲解宏configEXPECTED_IDLE_TIME_BEFORE_SLEEP 的时候已经说过了。
(9)、处理Tickless 模式,挂起任务调度器,其实就是起到临界段代码保护功能
(10)、重新获取一次时间值,这次的时间值是直接用于portSUPPRESS_TICKS_AND_SLEEP()
的。
(11)、调用portSUPPRESS_TICKS_AND_SLEEP()进入低功耗Tickless 模式,上一章已经详
细的讲解过Tickless 模式了。
(12)、恢复任务调度器。

19.2 空闲任务钩子函数详解

19.2.1 钩子函数

FreeRTOS 中有多个钩子函数,钩子函数类似回调函数,当某个功能(函数)执行的时候就会
调用钩子函数,至于钩子函数的具体内容那就由用户来编写。如果不需要使用钩子函数的话就
什么也不用管,钩子函数是一个可选功能,可以通过宏定义来选择使用哪个钩子函数,可选的
钩子函数如表19.2.1.1 所示:

在这里插入图片描述
钩子函数的使用方法基本相同,用户使能相应的钩子函数,然后自行根据实际需求编写钩
子函数的内容,下一节我们会以空闲任务钩子函数为例讲解如何使用钩子函数。

19.2.2 空闲任务钩子函数

在每个空闲任务运行周期都会调用空闲任务钩子函数,如果想在空闲任务优先级下处理某
个任务有两种选择:
●在空闲任务钩子函数中处理任务。
不管什么时候都要保证系统中至少有一个任务可以运行,因此绝对不能在空闲任务钩子函
数中调用任何可以阻塞空闲任务的API 函数,比如vTaskDelay(),或者其他带有阻塞时间的信
号量或队列操作函数。
●创建一个与空闲任务优先级相同的任务。
创建一个任务是最好的解决方法,但是这种方法会消耗更多的RAM。
要使用空闲任务钩子函数首先要在FreeRTOSConfig.h 中将宏configUSE_IDLE_HOOK 改
为1,然后编写空闲任务钩子函数vApplicationIdleHook()。通常在空闲任务钩子函数中将处理
器设置为低功耗模式来节省电能,为了与FreeRTOS 自带的Tickless 模式做区分,这里我暂且
将这种低功耗的实现方法称之为通用低功耗模式(因为几乎所有的RTOS 系统都可以使用这种
方法实现低功耗)。这种通用低功耗模式和FreeRTOS 自带的Tickless 模式的区别我们通过下图
19.2.2.1 来对比分析一下。
在这里插入图片描述
图19.2.2.1 两种低功耗模式对比
图19.2.2.1 中有三个任务,它们分别为一个空闲任务(Idle),两个用户任务(Task1 和Task2),
其中空闲任务一共有运行了三次,分别为(1)、(2)、(3),其中T1 到T12 是12 个时刻,下面我
们分别从这两种低功耗的实现方法去分析一下整个过程。
1、通用低功耗模式
如果使用通用低功耗模式的话每个滴答定时器中断都会将处理器从低功耗模式中唤醒,以
(1)为例,再T2 时刻处理器从低功耗模式中唤醒,但是接下来由于没有就绪的其他任务所以处
理器又再一次进入低功耗模式。T2、T3 和T4 这三个时刻都一样,反复的进入低功耗、退出低
功耗,最理想的情况应该是从T1 时刻就进入低功耗,然后在T5 时刻退出。
在(2)中空闲任务只工作了两个时钟节拍,但是也执行了低功耗模式的进入和退出,显然这
个意义不大,因为进出低功耗也是需要时间的。
(3)中空闲任务在T12 时刻被某个外部中断唤醒,中断的具体处理过程在任务2(使用信号量
实现中断与任务之间的同步)。
2、低功耗Tickless 模式
在(1)中的T1 时刻处理器进入低功耗模式,在T5 时刻退出低功耗模式。相比通用低功耗模
式少了3 次进出低功耗模式的操作。
在(2)中由于空闲任务只运行了两个时钟节拍,所以就没必要进入低功耗模式。说明在
Tickless 模式中只有空闲任务要运行时间的超过某个最小阈值的时候才会进入低功耗模式,此
阈值通过configEXPECTED_IDLE_TIME_BEFORE_SLEEP 来设置,上一章已经讲过了。
(3)中的情况和通用低功耗模式一样。
可以看出相对与通用低功耗模式,FreeRTOS 自带的Tickless 模式更加合理有效,所以如果
有低功耗设计需求的话大家尽量使用FreeRTOS 再带的Tickless 模式。当然了,如果对于功耗
要求不严格的话通用低功耗模式也可以使用,下一节将通过一个实验讲解如何在空闲任务钩子
函数中实现低功耗。

19.3 空闲任务钩子函数实验

19.3.1 实验程序设计

1、实验目的
学习如何在FreeRTOS 空闲任务钩子函数中实现低功耗。
2、实验设计
本实验在上一章实验“FreeRTOS 实验18-1 FreeRTOS 低功耗Tickless 模式实验”上做简单
修改,关闭Tickless 模式,在空闲任务钩子函数中使用WFI 指令是处理器进入睡眠模式。
3、实验工程
FreeRTOS 实验19-1 FreeRTOS 空闲任务钩子函数实验。
4、实验程序与分析
●相关宏设置

#define configUSE_TICKLESS_IDLE 0 //关闭低功耗tickless 模式
#define configUSE_IDLE_HOOK 1 //使能空闲任务钩子函数

●空闲任务钩子函数

//进入低功耗模式前需要处理的事情
void BeforeEnterSleep(void)
{//关闭某些低功耗模式下不使用的外设时钟,此处只是演示性代码RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,DISABLE);RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC,DISABLE);RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOD,DISABLE);RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOE,DISABLE);RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOF,DISABLE);RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOG,DISABLE);
}
//退出低功耗模式以后需要处理的事情
void AfterExitSleep(void)
{//退出低功耗模式以后打开那些被关闭的外设时钟,此处只是演示性代码RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC,ENABLE);RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOD,ENABLE);RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOE,ENABLE);RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOF,ENABLE);RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOG,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();
}

空闲任务钩子函数主要目的就是调用WFI 指令使STM32F103 进入睡眠模式,在进入和退
出低功耗模式的时候也可以做一些其他处理,比如关闭外设时钟等等,用法和FreeRTOS 的
Tickless 模式类似。
●其他任务函数和设置
其他有关设置和任务函数的内容同“FreeRTOS 实验18-1 FreeRTOS 低功耗Tickless 模式实
验”一样,这里就不列出来了。

19.3.2 实验程序运行结果

编译并下载实验代码到开发板中,打开串口调试助手,通过串口调试助手发送命令就可以
控制LED1 或者BEEP 的开关。当然了,还有本实验的重点,功耗测量!功耗如图19.3.2.1 所
示:
在这里插入图片描述
当前的系统电压为5.13V,工作电流为0.119A=119mA,功率为0.610W=610mW。跟上一章
测量出的未使用低功耗模式的功耗相比,工作电流减少了163-119=44mA,功率降低了836-610=226mW。

第二十章FreeRTOS 内存管理

内存管理是一个系统基本组成部分,FreeRTOS 中大量使用到了内存管理,比如创建任务、
信号量、队列等会自动从堆中申请内存。用户应用层代码也可以FreeRTOS 提供的内存管理函
数来申请和释放内存,本章就来学习一下FreeRTOS 自带的内存管理,本章分为如下几部分:
20.1 内存管理简介
20.2 内存碎片
20.3 heap_1 内存分配方法
20.4 heap_2 内存分配方法
20.5 heap_3 内存分配方法
20.6 heap_4 内存分配方法
20.7 heap_5 内存分配方法
20.8 内存管理实验

20.1 FreeRTOS 内存管理简介

FreeRTOS 创建任务、队列、信号量等的时候有两种方法,一种是动态的申请所需的RAM。
一种是由用户自行定义所需的RAM,这种方法也叫静态方法,使用静态方法的函数一般以
“Static”结尾,比如任务创建函数xTaskCreateStatic(),使用此函数创建任务的时候需要由用户
定义任务堆栈,本章我们不讨论这种静态方法。
使用动态内存管理的时候FreeRTOS 内核在创建任务、队列、信号量的时候会动态的申请
RAM。标准C 库中的malloc()和free()也可以实现动态内存管理,但是如下原因限制了其使用:
●在小型的嵌入式系统中效率不高。
●会占用很多的代码空间。
●它们不是线程安全的。
●具有不确定性,每次执行的时间不同。
●会导致内存碎片。
●使链接器的配置变得复杂。
不同的嵌入式系统对于内存分配和时间要求不同,因此一个内存分配算法可以作为系统的
可选选项。FreeRTOS 将内存分配作为移植层的一部分,这样FreeRTOS 使用者就可以使用自己
的合适的内存分配方法。
当内核需要RAM 的时候可以使用pvPortMalloc()来替代malloc()申请内存,不使用内存的
时候可以使用vPortFree()函数来替代free()函数释放内存。函数pvPortMalloc()、vPortFree()与函
数malloc()、free()的函数原型类似。
FreeRTOS 提供了5 种内存分配方法,FreeRTOS 使用者可以其中的某一个方法,或者自己
的内存分配方法。这5 种方法是5 个文件,分别为:heap_1.c、heap_2.c、heap_3.c、heap_4.c 和
heap_5.c。这5 个文件再FreeRTOS 源码中,路径:FreeRTOS->Source->portable->MemMang,
后面会详细讲解这5 种方法有何区别。

20.2 内存碎片

在看FreeRTOS 的内存分配方法之前我们先来看一下什么叫做内存碎片,看名字就知道是
小块的、碎片化的内存。那么内存碎片是怎么来的呢?内存碎片是伴随着内存申请和释放而来
的,如图20.2.1 所示。
在这里插入图片描述
(1)、此时内存堆还没有经过任何操作,为全新的。
(2)、此时经过第一次内存分配,一共分出去了4 块内存块,大小分别为80B、80B、10B 和
100B。
(3)、有些应用使用完内存,进行了释放,从左往右第一个80B 和后面的10B 这两个内存块
就是释放的内存。如果此时有个应用需要50B 的内存,那么它可以从两个地方来获取到,一个
是最前面的还没被分配过的剩余内存块,另一个就是刚刚释放出来的80B 的内存块。但是很明
显,刚刚释放出来的这个10B 的内存块就没法用了,除非此时有另外一个应用所需要的内存小
于10B。
(4)、经过很多次的申请和释放以后,内存块被不断的分割、最终导致大量很小的内存块!
也就是图中80B 和50B 这两个内存块之间的小内存块,这些内存块由于太小导致大多数应用无
法使用,这些没法使用的内存块就沦为了内存碎片!
内存碎片是内存管理算法重点解决的一个问题,否则的话会导致实际可用的内存越来越少,
最终应用程序因为分配不到合适的内存而奔溃!FreeRTOS 的heap_4.c 就给我们提供了一个解
决内存碎片的方法,那就是将内存碎片进行合并组成一个新的可用的大内存块。

20.3 heap_1 内存分配方法

20.3.1 分配方法简介

动态内存分配需要一个内存堆,FreeRTOS 中的内存堆为ucHeap[] ,大小为
configTOTAL_HEAP_SIZE,这个前面讲FreeRTOS 配置的时候就讲过了。不管是哪种内存分配
方法,它们的内存堆都为ucHeap[],而且大小都是configTOTAL_HEAP_SIZE。内存堆在文件
heap_x.c(x 为1~5)中定义的,比如heap_1.c 文件就有如下定义:

#if( configAPPLICATION_ALLOCATED_HEAP == 1 )extern uint8_t ucHeap[ configTOTAL_HEAP_SIZE ]; //需要用户自行定义内存堆
#elsestatic uint8_t ucHeap[ configTOTAL_HEAP_SIZE ]; //编译器决定
#endif

当宏configAPPLICATION_ALLOCATED_HEAP 为1 的时候需要用户自行定义内存堆,否
则的话由编译器来决定,默认都是由编译器来决定的。如果自己定义的话就可以将内存堆定义
到外部SRAM 或者SDRAM 中。
heap_1 实现起来就是当需要RAM 的时候就从一个大数组(内存堆)中分一小块出来,大数
组(内存堆)的容量为configTOTAL_HEAP_SIZE,上面已经说了。使用函数xPortGetFreeHeapSize()
可以获取内存堆中剩余内存大小。
heap_1 特性如下:
1、适用于那些一旦创建好任务、信号量和队列就再也不会删除的应用,实际上大多数的
FreeRTOS 应用都是这样的。
2、具有可确定性(执行所花费的时间大多数都是一样的),而且不会导致内存碎片。
3、代码实现和内存分配过程都非常简单,内存是从一个静态数组中分配到的,也就是适合
于那些不需要动态内存分配的应用。

20.3.2 内存申请函数详解

heap_1 的内存申请函数pvPortMalloc()源码如下:

void *pvPortMalloc( size_t xWantedSize )
{void *pvReturn = NULL;static uint8_t *pucAlignedHeap = NULL;//确保字节对齐
#if( portBYTE_ALIGNMENT != 1 ) (1){if( xWantedSize & portBYTE_ALIGNMENT_MASK ) (2){//需要进行字节对齐xWantedSize += ( portBYTE_ALIGNMENT - ( xWantedSize &\ (3)portBYTE_ALIGNMENT_MASK ) );}}
#endifvTaskSuspendAll(); (4){if( pucAlignedHeap == NULL ){//确保内存堆的开始地址是字节对齐的pucAlignedHeap = ( uint8_t * ) ( ( ( portPOINTER_SIZE_TYPE )\ (5)&ucHeap[ portBYTE_ALIGNMENT ] ) &\( ~( ( portPOINTER_SIZE_TYPE )\portBYTE_ALIGNMENT_MASK ) ) );}//检查是否有足够的内存供分配,有的话就分配内存if( ( ( xNextFreeByte + xWantedSize ) < configADJUSTED_HEAP_SIZE ) && (6)( ( xNextFreeByte + xWantedSize ) > xNextFreeByte ) ){pvReturn = pucAlignedHeap + xNextFreeByte; (7)xNextFreeByte += xWantedSize; (8)}traceMALLOC( pvReturn, xWantedSize );}( void ) xTaskResumeAll(); (9)
#if( configUSE_MALLOC_FAILED_HOOK == 1 ) (10){if( pvReturn == NULL ){extern void vApplicationMallocFailedHook( void );vApplicationMallocFailedHook();}}
#endifreturn pvReturn; (11)
}

(1)、是否需要进行字节对齐,宏portBYTE_ALIGNMENT 是需要对齐的字节数,默认为8,
需要进行8 字节对齐,也就是说参数xWantedSize 要为8 的倍数,如果不是的话就需要调整为
8 的倍数。
(2)、参数xWantedSize 与宏portBYTE_ALIGNMENT_MASK 进行与运算来判断xWantedSize
是否为8 字节对齐,如果结果等于0 就说明xWantedSize 是8 字节对齐的,否则的话不为8 字
节对齐,portBYTE_ALIGNMENT_MASK 为0x0007。假如xWantedSize 为13,那么13&0x0007=5,
5 大于0,所以13 不为8 的倍数,需要做字节对齐处理。当xWantedSize 为16 的时候,16&0x0007=0,
所以16 是8 的倍数,无需字节对齐。
(3)、当xWantedSize 不是8 字节对齐的时候就需要调整为8 字节对齐,调整方法就是找出
大于它并且离它最近的那个8 字节对齐的数,对于13 来说就是16。体现在代码中就是本行的
这个公式,同样以xWantedSize 为13 为例,计算公式就是:
xWantedSize=13+(8-(13&0x0007))=13+(8-5)=16;
(4)、调用函数vTaskSuspendAll()挂起任务调度器,因为申请内存过程中要做保护,不能被
其他任务打断。
(5)、确保内存堆的可用起始地址也是8 字节对齐的,内存堆ucHeap 的起始地址是由编译
器分配的,ucHeap 的起始地址不一定是8 字节对齐的。但是我们在使用的时候肯定要使用一个
8 字节对齐的起始地址,这个地址用pucAlignedHeap 表示,同样需要用公式计算一下,公式就
是本行代码,ucHeap 和pucAlignedHeap 如图20.3.2.1 所示:
在这里插入图片描述
图20.3.2.1 中内存堆ucHeap 实际起始地址为0x200006C4,这个地址不是8 字节对齐的,
所以不能拿来使用,经过字节对齐以后可以使用的开始地址是0x200006C8 ,所以
pucAlignedHeap 就为0x200006C8。
(6)、检查一下可用内存是否够分配,分配完成以后是否会产生越界(超出内存堆范围),
xNextFreeByte 是个全局变量,用来保存pucAlignedHeap 到内存堆剩余内存首地址之间的偏移
值,如图20.3.2.2 所示:
在这里插入图片描述
(7)、如果内存够分配并且不会产生越界,那么就将申请到的内存首地址赋给pvReturn,比
如我们要申请30 个字节(字节对齐以后实际需要申请32 字节)的内存,申请过程如图20.3.2.3 所
示:
在这里插入图片描述
(8)、内存申请完成以后更新一下变量xNextFreeByte。
(9)、调用函数xTaskResumeAll()恢复任务调度器。
(10)、宏configUSE_MALLOC_FAILED_HOOK 为1 的话就说明使能了内存申请失败钩子
函数,因此会调用钩子函数vApplicationMallocFailedHook(),此函数需要用户自行编写实现。
(11)、返回pvRerurn 值,如果内存申请成功的话就是申请到的内存首地址,内存申请失败
的话就返回NULL。

20.3.3 内存释放函数详解

heap_1 的内存释放函数为pvFree(),可以看一下pvFree()的源码,如下:

void vPortFree( void *pv )
{( void ) pv;configASSERT( pv == NULL );
}

可以看出vPortFree()并没有具体释放内存的过程。因此如果使用heap_1,一旦申请内存成
功就不允许释放!但是heap_1 的内存分配过程简单,如此看来heap_1 似乎毫无任何使用价值
啊。千万不能这么想,有很多小型的应用在系统一开始就创建好任务、信号量或队列等,在程
序运行的整个过程这些任务和内核对象都不会删除,那么这个时候使用heap_1 就很合适的。

20.4 heap_2 内存分配方法

20.4.1 分配方法简介

heap_2 提供了一个更好的分配算法,不像heap_1 那样,heap_2 提供了内存释放函数。heap_2
不会把释放的内存块合并成一个大块,这样有一个缺点,随着你不断的申请内存,内存堆就会被分为很多个大小不一的内存(块),也就是会导致内存碎片!heap_4 提供了空闲内存块合并的
功能。
heap_2 的特性如下:
1、可以使用在那些可能会重复的删除任务、队列、信号量等的应用中,要注意有内存碎片
产生!
2、如果分配和释放的内存n 大小是随机的,那么就要慎重使用了,比如下面的示例:
●如果一个应用动态的创建和删除任务,而且任务需要分配的堆栈大小都是一样的,
那么heap_2 就非常合适。如果任务所需的堆栈大小每次都是不同,那么heap_2 就
不适合了,因为这样会导致内存碎片产生,最终导致任务分配不到合适的堆栈!不
过heap_4 就很适合这种场景了。
●如果一个应用中所使用的队列存储区域每次都不同,那么heap_2 就不适合了,和上
面一样,此时可以使用heap_4。
●应用需要调用pvPortMalloc()和vPortFree()来申请和释放内存,而不是通过其他
FreeRTOS 的其他API 函数来间接的调用,这种情况下heap_2 不适合。
3、如果应用中的任务、队列、信号量和互斥信号量具有不可预料性(如所需的内存大小不能
确定,每次所需的内存都不相同,或者说大多数情况下所需的内存都是不同的)的话可能会导致
内存碎片。虽然这是小概率事件,但是还是要引起我们的注意!
4、具有不可确定性,但是也远比标准C 中的mallo()和free()效率高!
heap_2 基本上可以适用于大多数的需要动态分配内存的工程中,而heap_4 更是具有将内存
碎片合并成一个大的空闲内存块(就是内存碎片回收)的功能。

20.4.2 内存块详解

同heap_1 一样,heap_2 整个内存堆为ucHeap[],大小为configTOTAL_HEAP_SIZE。可以
通过函数xPortGetFreeHeapSize()来获取剩余的内存大小。
为了实现内存释放,heap_2 引入了内存块的概念,每分出去的一段内存就是一个内存块,
剩下的空闲内存也是一个内存块,内存块大小不定。为了管理内存块又引入了一个链表结构,
链表结构如下:

typedef struct A_BLOCK_LINK
{struct A_BLOCK_LINK *pxNextFreeBlock; //指向链表中下一个空闲内存块size_t xBlockSize; //当前空闲内存块大小
} BlockLink_t;

每个内存块前面都会有一个BlockLink_t 类型的变量来描述此内存块,比如我们现在申请
了一个16 个字节的内存块,那么此内存块结构就如图20.4.2.1 所示:

在这里插入图片描述

图20.4.2.1 中内存块的总大小是24 字节,虽然我们只申请了16 个字节,但是还需要另外8 字节来保存BlockLink_t 类型的结构体变量,xBlockSize 记录的是整个内存块的大小。
为了方便管理,可用的内存块会被全部组织在一个链表内,局部静态变量xStart, xEnd 用来
记录这个链表的头和尾,这两个变量定义如下:

static BlockLink_t xStart, xEnd;

20.4.3 内存堆初始化函数详解

内存堆初始化函数为prvHeapInit(),函数源码如下:

static void prvHeapInit( void )
{BlockLink_t *pxFirstFreeBlock;uint8_t *pucAlignedHeap;//确保内存堆的开始地址是字节对齐的pucAlignedHeap = ( uint8_t * ) ( ( ( portPOINTER_SIZE_TYPE )\ (1)&ucHeap[ portBYTE_ALIGNMENT ] ) & \( ~( ( portPOINTER_SIZE_TYPE )\portBYTE_ALIGNMENT_MASK ) ) );//xStart 指向空闲内存块链表首。xStart.pxNextFreeBlock = ( void * ) pucAlignedHeap; (2)xStart.xBlockSize = ( size_t ) 0;//xEnd 指向空闲内存块链表尾。xEnd.xBlockSize = configADJUSTED_HEAP_SIZE; (3)xEnd.pxNextFreeBlock = NULL;//刚开始只有一个空闲内存块,空闲内存块的总大小就是可用的内存堆大小。pxFirstFreeBlock = ( void * ) pucAlignedHeap; (4)pxFirstFreeBlock->xBlockSize = configADJUSTED_HEAP_SIZE;pxFirstFreeBlock->pxNextFreeBlock = &xEnd;
}

(1)、同heap_1 一样,确保内存堆的可用起始地址为8 字节对齐。
(2)、初始化xStart 变量。
(3)、初始化xEnd 变量。
(4)、每个内存块前面都会保存一个BlockLink_t 类型的结构体变量,这个结构体变量用来
描述此内存块的大小和下一个空闲内存块的地址。
初始化以后的内存堆如图20.4.3.1 所示:

在这里插入图片描述

20.4.4 内存块插入函数详解

heap_2 允许内存释放,释放的内存肯定是要添加到空闲内存链表中的,宏
prvInsertBlockIntoFreeList()用来完成内存块的插入操作,宏定义如下:

#define prvInsertBlockIntoFreeList( pxBlockToInsert )
{BlockLink_t *pxIterator;size_t xBlockSize;xBlockSize = pxBlockToInsert->xBlockSize;//遍历链表,查找插入点for( pxIterator = &xStart; pxIterator->pxNextFreeBlock->xBlockSize < xBlockSize; (1)pxIterator = pxIterator->pxNextFreeBlock ){//不做任何事情}//将内存块插入到插入点pxBlockToInsert->pxNextFreeBlock = pxIterator->pxNextFreeBlock; (2)pxIterator->pxNextFreeBlock = pxBlockToInsert;
}

(1)、寻找内存块的插入点,内存块是按照内存大小从小到大连接起来的,因为只是用来寻
找插入点的,所以for 循环体内没有任何代码。
(2)、找到内存插入点以后就将内存块插入到链表中。
假如我们现在需要将大小为80 字节的内存块插入到链表中,过程如图20.4.4.1 所示:
在这里插入图片描述

20.4.5 内存申请函数详解

heap_2 的内存申请函数源码如下:

void *pvPortMalloc( size_t xWantedSize )
{BlockLink_t *pxBlock, *pxPreviousBlock, *pxNewBlockLink;static BaseType_t xHeapHasBeenInitialised = pdFALSE;void *pvReturn = NULL;vTaskSuspendAll();{//如果是第一次申请内存的话需要初始化内存堆if( xHeapHasBeenInitialised == pdFALSE ) (1){prvHeapInit();xHeapHasBeenInitialised = pdTRUE;}//内存大小字节对齐,实际申请的内存大小还要加上结构体//BlockLink_t 的大小if( xWantedSize > 0 ) (2){xWantedSize += heapSTRUCT_SIZE; (3)//xWantedSize 做字节对齐处理if( ( xWantedSize & portBYTE_ALIGNMENT_MASK ) != 0 ){xWantedSize += ( portBYTE_ALIGNMENT - ( xWantedSize &portBYTE_ALIGNMENT_MASK ) );}}//所申请的内存大小合理,进行内存分配。if( ( xWantedSize > 0 ) && ( xWantedSize < configADJUSTED_HEAP_SIZE ) ){//从xStart(最小内存块)开始,查找大小满足所需要内存的内存块。pxPreviousBlock = &xStart;pxBlock = xStart.pxNextFreeBlock;while( ( pxBlock->xBlockSize < xWantedSize ) &&\ (4)( pxBlock->pxNextFreeBlock != NULL ) ){pxPreviousBlock = pxBlock;pxBlock = pxBlock->pxNextFreeBlock;}if( pxBlock != &xEnd ) (5){//返回申请到的内存首地址pvReturn = ( void * ) ( ( ( uint8_t * ) pxPreviousBlock->pxNextFreeBlock ) +\(6)heapSTRUCT_SIZE );pxPreviousBlock->pxNextFreeBlock = pxBlock->pxNextFreeBlock; (7)if( ( pxBlock->xBlockSize - xWantedSize ) >\ (8)heapMINIMUM_BLOCK_SIZE ){pxNewBlockLink = ( void * ) ( ( ( uint8_t * ) pxBlock ) + xWantedSize );pxNewBlockLink->xBlockSize = pxBlock->xBlockSize - xWantedSize;pxBlock->xBlockSize = xWantedSize;prvInsertBlockIntoFreeList( ( pxNewBlockLink ) ); (9)}xFreeBytesRemaining -= pxBlock->xBlockSize; (10)}}traceMALLOC( pvReturn, xWantedSize );}( void ) xTaskResumeAll();
#if( configUSE_MALLOC_FAILED_HOOK == 1 ) (11){if( pvReturn == NULL ){extern void vApplicationMallocFailedHook( void );vApplicationMallocFailedHook();}}
#endifreturn pvReturn;
}

(1)、如果是第一次调用函数pvPortMalloc()申请内存的话就需要先初始化一次内存堆。
(2)、所申请的内存大小进行字节对齐。
(3)、实际申请的内存大小需要再加上结构体BlockLink_t 的大小,因为每个内存块都会保
存一个BlockLink_t 类型变量,BlockLink_t 结构体的大小为heapSTRUCT_SIZE。
(4)、从空闲内存链表头xStart 开始,查找满足所需内存大小的内存块,pxPreviousBlock 所
指向的下一个内存块就是找到的可用内存块。
(5)、找到的可用内存块不能是链表尾xEnd!
(6)、找到内存块以后就将可用内存首地址保存在pvReturn 中,函数返回的时候返回此值,
这个内存首地址要跳过结构体BlockLink_t,如图20.4.5.1 所示:
在这里插入图片描述
(7)、内存块已经被申请了,所以需要将这个内存块从空闲内存块链表中移除。
(8)、存在这样一种情况(不考虑结构体BlockLink_t 的大小),我需要申请100 个字节的内
存,但是经过上面几步我得到了一个1K 字节的内存块,实际使用中我只需要100 个字节,剩
下的900 个字节就浪费掉了。这个明显是不合理的,所以需要判断,如果申请到的实际内存减
去所需的内存大小(xBlockSize-xWantedSize)大于某个阈值的时候就把多余出来的内存重新组合
成一个新的可用空闲内存块。这个阈值由宏heapMINIMUM_BLOCK_SIZE 来设置,这个阈值
要大于heapSTRUCT_SIZE。
(9)、将新的空闲内存块插入到空闲内存块链表中。
(10)、更新全局变量xFreeBytesRemaining,此变量用来保存内存堆剩余内存大小。
(11)、如果使能了钩子函数的话就调用钩子函数vApplicationMallocFailedHook()。

20.4.6 内存释放函数详解

内存释放函数vPortFree()的源码如下:

void vPortFree( void *pv )
{uint8_t *puc = ( uint8_t * ) pv;BlockLink_t *pxLink;if( pv != NULL ){puc -= heapSTRUCT_SIZE; (1)pxLink = ( void * ) puc; (2)vTaskSuspendAll();{//将内存块添加到空闲内存块链表中prvInsertBlockIntoFreeList( ( ( BlockLink_t * ) pxLink ) ); (3)xFreeBytesRemaining += pxLink->xBlockSize; (4)traceFREE( pv, pxLink->xBlockSize );}( void ) xTaskResumeAll();}
}

(1)、puc 为要释放的内存首地址,这个地址就是图20.3.2.4 中pvReturn 所指向的地址。所
以必须减去heapSTRUCT_SIZE 才是要释放的内存段所在内存块的首地址。
(2)、防止编译器报错。
(3)、将内存块添加到空闲内存块列表中。
(4)、更新变量xFreeBytesRemaining。
内存释放函数vPortFree()还是很简单的,主要目的就是将需要释放的内存所在的内存块添
加到空闲内存块链表中。

20.5 heap_3 内存分配方法

这个分配方法是对标准C 中的函数malloc()和free()的简单封装,FreeRTOS 对这两个函数
做了线程保护,两个函数的源码如下:

void *pvPortMalloc( size_t xWantedSize )
{void *pvReturn;vTaskSuspendAll(); (1){pvReturn = malloc( xWantedSize ); (2)traceMALLOC( pvReturn, xWantedSize );}( void ) xTaskResumeAll(); (3)
#if( configUSE_MALLOC_FAILED_HOOK == 1 ){if( pvReturn == NULL ){extern void vApplicationMallocFailedHook( void );vApplicationMallocFailedHook();}}
#endifreturn pvReturn;
}
void vPortFree( void *pv )
{if( pv ){vTaskSuspendAll(); (4){free( pv ); (5)traceFREE( pv, 0 );}( void ) xTaskResumeAll(); (6)}
}

(1)和(4)、挂起任务调度器,为malloc()和free()提供线程保护
(2)、调用函数malloc()来申请内存。
(3)和(6)、恢复任务调度器。
(5)、调用函数free()释放内存。
heap_3 的特性如下:
1、需要编译器提供一个内存堆,编译器库要提供malloc()和free()函数。比如使用STM32
的话可以通过修改启动文件中的Heap_Size 来修改内存堆的大小,如图20.5.1 所示。
在这里插入图片描述
图20.5.1 内存堆大小
2、具有不确定性
3、可能会增加代码量。
注意,在heap_3 中configTOTAL_HEAP_SIZE 是没用的!

20.6 heap_4 内存分配方法

20.6.1 分配方法简介

heap_4 提供了一个最优的匹配算法,不像heap_2,heap_4 会将内存碎片合并成一个大的可
用内存块,它提供了内存块合并算法。内存堆为ucHeap[],大小同样为configTOTAL_HEAP_SIZE。
可以通过函数xPortGetFreeHeapSize()来获取剩余的内存大小。
heap_4 特性如下:
1、可以用在那些需要重复创建和删除任务、队列、信号量和互斥信号量等的应用中。
2、不会像heap_2 那样产生严重的内存碎片,即使分配的内存大小是随机的。
3、具有不确定性,但是远比C 标准库中的malloc()和free()效率高。
heap_4 非常适合于那些需要直接调用函数pvPortMalloc()和vPortFree()来申请和释放内存
的应用,注意,我们移植FreeRTOS 的时候就选择的heap_4!
heap_4 也使用链表结构来管理空闲内存块,链表结构体与heap_2 一样。heap_4 也定义了
两个局部静态变量xStart 和pxEnd 来表示链表头和尾,其中pxEnd 是指向BlockLink_t 的指针。

20.6.2 内存堆初始化函数详解

内存初始化函数prvHeapInit()源码如下:

static void prvHeapInit( void )
{BlockLink_t *pxFirstFreeBlock;uint8_t *pucAlignedHeap;size_t uxAddress;size_t xTotalHeapSize = configTOTAL_HEAP_SIZE;//起始地址做字节对齐处理uxAddress = ( size_t ) ucHeap;if( ( uxAddress & portBYTE_ALIGNMENT_MASK ) != 0 ) (1){uxAddress += ( portBYTE_ALIGNMENT - 1 );uxAddress &= ~( ( size_t ) portBYTE_ALIGNMENT_MASK );xTotalHeapSize -= uxAddress - ( size_t ) ucHeap; (2)}pucAlignedHeap = ( uint8_t * ) uxAddress; (3)//xStart 为空闲链表头。xStart.pxNextFreeBlock = ( void * ) pucAlignedHeap; (4)xStart.xBlockSize = ( size_t ) 0;//pxEnd 为空闲内存块列表尾,并且将其放到到内存堆的末尾uxAddress = ( ( size_t ) pucAlignedHeap ) + xTotalHeapSize; (5)uxAddress -= xHeapStructSize;uxAddress &= ~( ( size_t ) portBYTE_ALIGNMENT_MASK );pxEnd = ( void * ) uxAddress;pxEnd->xBlockSize = 0;pxEnd->pxNextFreeBlock = NULL;//开始的时候将内存堆整个可用空间看成一个空闲内存块。pxFirstFreeBlock = ( void * ) pucAlignedHeap; (6)pxFirstFreeBlock->xBlockSize = uxAddress - ( size_t ) pxFirstFreeBlock;pxFirstFreeBlock->pxNextFreeBlock = pxEnd;//只有一个内存块,而且这个内存块拥有内存堆的整个可用空间xMinimumEverFreeBytesRemaining = pxFirstFreeBlock->xBlockSize; (7)xFreeBytesRemaining = pxFirstFreeBlock->xBlockSize;xBlockAllocatedBit = ( ( size_t ) 1 ) << ( ( sizeof( size_t ) * heapBITS_PER_BYTE ) - 1 );(8)
}

(1)、可用内存堆起始地址做字节对齐处理。
(2)、可用起始地址做字节对齐处理以后难免会有几个字节被抛弃掉,被抛弃的这几个字节
不能使用,因此内存堆总的可用大小需要重新计算一下。
(3)、pucAlignedHeap 为内存堆字节对齐以后的可用起始地址。
(4)、初始化xStart,xStart 为可用内存块链表头。
(5)、初始化pxEnd,pxEnd 为可用内存块链表尾,pxEnd 放到了内存堆末尾。
(6)、同heap_2 一样,内存块前面会有一个BlockLink_t 类型的变量来描述内存块,这里是
完成这个变量初始化的。
(7) 、xMinimumEverFreeBytesRemaining 记录最小的那个空闲内存块大小,
xFreeBytesRemaining 表示内存堆剩余大小。
(8)、初始化静态变量xBlockAllocatedBit,初始化完成以后此变量值为0X80000000,此变量
是size_t 类型的,其实就是将size_t 类型变量的最高位置1,对于32 位MCU 来说就是0X80000000。
此变量会用来标记某个内存块是被使用,BlockLink_t 中的成员变量xBlockSize 是用来描述内存
块大小的,在heap_4 中其最高位表示此内存块是否被使用,如果为1 的话就表示被使用了,所
以在heap_4 中一个内存块最大只能为0x7FFFFFFF。
假设内存堆ucHeap 的大小为46KB,即configTOTAL_HEAP_SIZE =46*1024,ucHeap 的起
始地址为0X200006D4,经过函数prvHeapInit()初始化以后的内存堆如图20.6.2.1 所示:
在这里插入图片描述
图20.6.2.1 初始化完成以后的内存堆

20.6.3 内存块插入函数详解

内存块插入函数prvInsertBlockIntoFreeList()用来将某个内存块插入到空闲内存块链表中,
函数源码如下:

static void prvInsertBlockIntoFreeList( BlockLink_t *pxBlockToInsert )
{BlockLink_t *pxIterator;uint8_t *puc;//遍历空闲内存块链表,找出内存块插入点,内存块按照地址从低到高连接在一起for( pxIterator = &xStart; pxIterator->pxNextFreeBlock < pxBlockToInsert;\ (1)pxIterator = pxIterator->pxNextFreeBlock ){//不做任何处理}//插入内存块,如果要插入的内存块可以和前一个内存块合并的话就//合并两个内存块puc = ( uint8_t * ) pxIterator;if( ( puc + pxIterator->xBlockSize ) == ( uint8_t * ) pxBlockToInsert ) (2){pxIterator->xBlockSize += pxBlockToInsert->xBlockSize;pxBlockToInsert = pxIterator;}else{mtCOVERAGE_TEST_MARKER();}//检查是否可以和后面的内存块合并,可以的话就合并puc = ( uint8_t * ) pxBlockToInsert;.if( ( puc + pxBlockToInsert->xBlockSize ) == ( uint8_t * ) pxIterator->pxNextFreeBlock ) (3){if( pxIterator->pxNextFreeBlock != pxEnd ){//将两个内存块组合成一个大的内存块pxBlockToInsert->xBlockSize += pxIterator->pxNextFreeBlock->xBlockSize;pxBlockToInsert->pxNextFreeBlock =\pxIterator->pxNextFreeBlock->pxNextFreeBlock;}else{pxBlockToInsert->pxNextFreeBlock = pxEnd;}}else{pxBlockToInsert->pxNextFreeBlock = pxIterator->pxNextFreeBlock; (4)}if( pxIterator != pxBlockToInsert ){pxIterator->pxNextFreeBlock = pxBlockToInsert;}else{mtCOVERAGE_TEST_MARKER();}
}

(1)、遍历空闲内存块链表,找出当前内存块插入点,内存块是按照地址从低到高的顺序链
接在一起的。
(2)、找到插入点以后判断是否可以和要插入的内存块合并,如果可以的话就合并在一起。
如图20.6.3.1 所示:
在这里插入图片描述
在图20.6.3.1 中,右边椭圆圈起来的就是要插入的内存块,其起始地址为0x20009040,该
地址刚好和内存块Block2 的末地址一样,所以这两个内存块可以合并在一起。合并以后Block2
的大小xBlockSize 要更新为最新的内存块大小,即64+80=144。
(3)、再接着检查(2)中合并的新内存块是否可以和下一个内存块合并,也就是Block3,如果
可以的话就再次合并,合并完成以后如图20.6.3.2 所示:
在这里插入图片描述
在图20.6.3.2 中可以看出,最终新插入的内存块和Blokc2、Block3 合并成一个大小为
64+80+80=224 字节的大内存块,这个就是heap_4 解决内存碎片的方法!
(4)、如果不能和Block3 合并的话就将这两个内存块链接起来。
(5)、pxIterator 不等于pxBlockToInsert 就意味着在内存块插入的过程中没有进行过一次内
存合并,这样的话就使用最普通的处理方法。pxIterator 所指向的内存块在前,pxBlockToInsert
所指向的内存块在后,将两个内存块链接起来。

20.6.4 内存申请函数详解

heap_4 的内存申请函数源码如下:

void *pvPortMalloc( size_t xWantedSize )
{BlockLink_t *pxBlock, *pxPreviousBlock, *pxNewBlockLink;void *pvReturn = NULL;vTaskSuspendAll();{//第一次调用,初始化内存堆if( pxEnd == NULL ) (1){prvHeapInit();}else{mtCOVERAGE_TEST_MARKER();}//需要申请的内存块大小的最高位不能为1,因为最高位用来表示内存块有没有//被使用if( ( xWantedSize & xBlockAllocatedBit ) == 0 ) (2){if( xWantedSize > 0 ) (3){xWantedSize += xHeapStructSize;if( ( xWantedSize & portBYTE_ALIGNMENT_MASK ) != 0x00 ){xWantedSize += ( portBYTE_ALIGNMENT - ( xWantedSize &\portBYTE_ALIGNMENT_MASK ) );configASSERT( ( xWantedSize & portBYTE_ALIGNMENT_MASK ) == 0 );}else{mtCOVERAGE_TEST_MARKER();}}else{mtCOVERAGE_TEST_MARKER();}if( ( xWantedSize > 0 ) && ( xWantedSize <= xFreeBytesRemaining ) ){//从xStart(内存块最小)开始,查找大小满足所需要内存的内存块。pxPreviousBlock = &xStart;pxBlock = xStart.pxNextFreeBlock;while( ( pxBlock->xBlockSize < xWantedSize ) &&\ (4)( pxBlock->pxNextFreeBlock != NULL ) ){pxPreviousBlock = pxBlock;pxBlock = pxBlock->pxNextFreeBlock;}//如果找到的内存块是pxEnd 的话就表示没有内存可以分配if( pxBlock != pxEnd ) (5){pvReturn = ( void * ) ( ( ( uint8_t * ) pxPreviousBlock->\ (6)pxNextFreeBlock ) + xHeapStructSize );//将申请到的内存块从空闲内存链表中移除pxPreviousBlock->pxNextFreeBlock = pxBlock->pxNextFreeBlock; (7)//如果申请到的内存块大于所需的内存,就将其分成两块if( ( pxBlock->xBlockSize - xWantedSize ) >\ (8)heapMINIMUM_BLOCK_SIZE ){pxNewBlockLink = ( void * ) ( ( ( uint8_t * ) pxBlock ) + xWantedSize );pxNewBlockLink->xBlockSize = pxBlock->xBlockSize - xWantedSize;pxBlock->xBlockSize = xWantedSize;prvInsertBlockIntoFreeList( pxNewBlockLink ); (9)}else{mtCOVERAGE_TEST_MARKER();}xFreeBytesRemaining -= pxBlock->xBlockSize; (10)if( xFreeBytesRemaining < xMinimumEverFreeBytesRemaining ){xMinimumEverFreeBytesRemaining = xFreeBytesRemaining;}else{mtCOVERAGE_TEST_MARKER();}//内存块申请成功,标记此内存块已经被时候pxBlock->xBlockSize |= xBlockAllocatedBit; (11)pxBlock->pxNextFreeBlock = NULL;}else{mtCOVERAGE_TEST_MARKER();}}else{mtCOVERAGE_TEST_MARKER();}}else{mtCOVERAGE_TEST_MARKER();}traceMALLOC( pvReturn, xWantedSize );}( void ) xTaskResumeAll();
#if( configUSE_MALLOC_FAILED_HOOK == 1 ){if( pvReturn == NULL ){//调用钩子函数extern void vApplicationMallocFailedHook( void );vApplicationMallocFailedHook();}else{mtCOVERAGE_TEST_MARKER();}}
#endifconfigASSERT( ( ( ( size_t ) pvReturn ) & ( size_t ) portBYTE_ALIGNMENT_MASK ) == 0 );return pvReturn;
}

(1)、pxEnd 为NULL,说明内存堆还没初始化,所以需要调用函数prvHeapInit()初始化内
存堆。
(2)、BlockLink_t 中的变量xBlockSize 是来描述内存块大小的,其最高位用来记录内存块
有没有被使用,所以申请的内存块大小最高位不能为1。
(3)、实际所需申请的内存数要加上结构体BlockLink_t 的大小,因为内存块前面需要保存
一个BlockLink_t 类型的变量。最后还需要对最终的大小做字节对齐处理。这里有个疑问,假如
xWantedSize 为0x7FFFFFFF ,那么xWantedSize 加上结构体BlockLink_t 的大小就是
0x7FFFFFFF+8=0x80000007,在做一次8 字节对齐xWantedSize 就是0x80000008,其最高位为
1。前面已经说了,BlockLink_t 中的变量xBlockSize 的最高位是用来标记内存块是否被使用的,
这里明显冲突了,但是FreeRTOS 对此并没有做处理。
(4)、从空闲内存链表头xStart 开始,查找满足所需内存大小的内存块,pxPreviousBlock 的
下一个内存块就是找到的可用内存块。
(5)、找到的可用内存块不能是链表尾pxEnd!
(6)、找到内存块以后就将内存首地址保存在pvReturn 中,函数返回的时候返回此值。
(7)、内存块已经被申请了,所以需要将这个内存块从空闲内存块链表中移除。
(8)、申请到的内存块大于所需的大小,因此要把多余出来的内存重新组合成一个新的可用
空闲内存块。
(9)、将新的空闲内存块插入到空闲内存块链表中。
(10)、更新全局变量xFreeBytesRemaining 和xMinimumEverFreeBytesRemaining。
(11)、xBlockSize 与xBlockAllocatedBit 进行或运算,也就是将xBlockSize 的最高位置1,
表示此内存块被使用。

20.6.5 内存释放函数详解

内存释放函数源码如下:

void vPortFree( void *pv )
{uint8_t *puc = ( uint8_t * ) pv;BlockLink_t *pxLink;if( pv != NULL ){puc -= xHeapStructSize; (1)pxLink = ( void * ) puc; //防止编译器报错configASSERT( ( pxLink->xBlockSize & xBlockAllocatedBit ) != 0 );configASSERT( pxLink->pxNextFreeBlock == NULL );if( ( pxLink->xBlockSize & xBlockAllocatedBit ) != 0 ) (2){if( pxLink->pxNextFreeBlock == NULL ){pxLink->xBlockSize &= ~xBlockAllocatedBit; (3)vTaskSuspendAll();{xFreeBytesRemaining += pxLink->xBlockSize;traceFREE( pv, pxLink->xBlockSize );prvInsertBlockIntoFreeList( ( ( BlockLink_t * ) pxLink ) ); (4)}( void ) xTaskResumeAll();}else{mtCOVERAGE_TEST_MARKER();}}else{mtCOVERAGE_TEST_MARKER();}}
}

(1)、获取内存块的BlockLink_t 类型结构体。
(2)、要释放的内存块肯定是被使用了的,没有被使用的空闲内存块肯定没有释放这一说。
这里通过判断xBlockSize 的最高位是否等于0 来得知要释放的内存块是否被应用使用。前面已
经说了BlockLink_t 中成员变量xBlockSize 的最高位用来表示此内存块有没有被使用。
(3)、xBlockSize 的最高位清零,重新标记此内存块没有使用。xBlockSize 也表示内存块大
小,就跟在pvPortMalloc()函数里面分析的一样,如果最终申请的内存大小是0x80000008,那么
在经过这一行代码处理之后这个大小就变成了0x00000008。所以一定要保证在使用函数
pvPortMalloc()申请内存的时候经过字节对齐等处理以后,最后申请大小不能超过0x7FFFFFFF。
(4)、将内存块插到空闲内存链表中。

20.7 heap_5 内存分配方法

heap_5 使用了和heap_4 相同的合并算法,内存管理实现起来基本相同,但是heap_5 允许
内存堆跨越多个不连续的内存段。比如STM32 的内部RAM 可以作为内存堆,但是STM32 内
部RAM 比较小,遇到那些需要大容量RAM 的应用就不行了,如音视频处理。不过STM32 可
以外接SRAM 甚至大容量的SDRAM,如果使用heap_4 的话你就只能在内部RAM 和外部
SRAM 或SDRAM 之间二选一了,使用heap_5 的话就不存在这个问题,两个都可以一起作为
内存堆来用。
如果使用heap_5 的话,在调用API 函数之前需要先调用函数vPortDefineHeapRegions ()来
对内存堆做初始化处理,在vPortDefineHeapRegions()未执行完之前禁止调用任何可能会调用
pvPortMalloc()的API 函数!比如创建任务、信号量、队列等函数。函数vPortDefineHeapRegions()
只有一个参数,参数是一个HeapRegion_t 类型的数组,HeapRegion 为一个结构体,此结构体在
portable.h 中有定义,定义如下:

typedef struct HeapRegion
{uint8_t *pucStartAddress; //内存块的起始地址size_t xSizeInBytes; //内存段大小
} HeapRegion_t;

上面说了,heap_5 允许内存堆跨越多个不连续的内存段,这些不连续的内存段就是由结构
体HeapRegion_t 来定义的。比如以STM32F103 开发板为例,现在有连个内存段:内部SRAM、
外部SRAM,起始分别为:0X20000000、0x68000000,大小分别为:64KB、1MB,那么数组就
如下:

HeapRegion_t xHeapRegions[] =
{{ ( uint8_t * ) 0X20000000UL, 0x10000 },//内部SRAM 内存,起始地址0X20000000,//大小为64KB{ ( uint8_t * ) 0X68000000UL, 0x100000},//外部SRAM 内存,起始地址0x68000000,//大小为1MB{ NULL, 0 } //数组结尾
};

注意,数组中成员顺序按照地址从低到高的顺序排列,而且最后一个成员必须使用NULL。
heap_5 允许内存堆不连续,说白了就是允许有多个内存堆。在heap_2 和heap_4 中只有一个内
存堆,初始化的时候只也只需要处理一个内存堆。heap_5 有多个内存堆,这些内存堆会被连接
在一起,和空闲内存块链表类似,这个处理过程由函数vPortDefineHeapRegions()完成。
使用heap_5 的时候在一开始就应该先调用函数vPortDefineHeapRegions()完成内存堆的初始化!然后才能创建任务、信号量这些东西,如下示例代码:

int main(void)
{NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4); //设置系统中断优先级分组4delay_init(); //延时函数初始化uart_init(115200); //初始化串口LED_Init(); //初始化LEDKEY_Init(); //初始化按键BEEP_Init(); //初始化蜂鸣器LCD_Init(); //初始化LCDmy_mem_init(SRAMIN); //初始化内部内存池//使用heap_5 的时候在开启任务调度器、创建任务、创建信号量之前一定要先//调用函数vPortDefineHeapRegions()初始化内存堆!vPortDefineHeapRegions((const HeapRegion_t *)xHeapRegions);//创建开始任务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(); //开启任务调度
}

heap_5 的内存申请和释放函数和heap_4 基本一样,这里就不详细讲解了,大家可以对照着
前面heap_4 的相关内容来自行分析。
至此,FreeRTOS 官方提供的5 种内存分配方法已经讲完了,heap_1 最简单,但是只能申请
内存,不能释放。heap_2 提供了内存释放函数,用户代码也可以直接调用函数pvPortMalloc()和
vPortFree()来申请和释放内存,但是heap_2 会导致内存碎片的产生!heap_3 是对标准C 库中的
函数malloc()和free()的简单封装,并且提供了线程保护。heap_4 相对与heap_2 提供了内存合
并功能,可以降低内存碎片的产生,我们移植FreeRTOS 的时候就选择了heap_4。heap_5 基本
上和heap_4 一样,只是heap_5 支持内存堆使用不连续的内存块。

20.8 FreeRTOS 内存管理实验

20.8.1 实验程序设计

1、实验目的
本节我们设计一个小程序来学习使用FreeRTOS 的内存申请和释放函数:pvPortMalloc()、
vPortFree(),并且观察申请和释放的过程中内存大小的变化情况
2、实验设计
本实验设计两个任务:start_task 和malloc_task ,这两个任务的任务功能如下:
start_task:用来创建另外一个任务。
malloc_task :此任务用于完成内存的申请、释放和使用功能。任务会不断的获取按键情
况,当检测到KEY_UP 按下的时候就会申请内存,当KEY0 按下以后就会使用申请到的内存,
如果检测到KEY1 按下的话就会释放申请到的内存。
实验中会用到3 个按键:KEY0、KEY1 和KEY_UP,KEY_UP 用于申请内存,KEY0 使用
申请到的内存,KEY1 释放申请到的内存。
3、实验工程
FreeRTOS 实验20-1 FreeRTOS 内存管理实验。
4、实验程序与分析
●任务设置

#define START_TASK_PRIO 1 //任务优先级
#define START_STK_SIZE 128 //任务堆栈大小
TaskHandle_t StartTask_Handler; //任务句柄void start_task(void *pvParameters); //任务函数
#define MALLOC_TASK_PRIO 2 //任务优先级
#define MALLOC_STK_SIZE 128 //任务堆栈大小
TaskHandle_t MallocTask_Handler; //任务句柄
void malloc_task(void *p_arg); //任务函数

●main()函数

int main(void)
{NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);//设置系统中断优先级分组4delay_init(); //延时函数初始化uart_init(115200); //初始化串口LED_Init(); //初始化LEDKEY_Init(); //初始化按键BEEP_Init(); //初始化蜂鸣器LCD_Init(); //初始化LCDmy_mem_init(SRAMIN); //初始化内部内存池POINT_COLOR = RED;LCD_ShowString(30,10,200,16,16,"ATK STM32F103/407");LCD_ShowString(30,30,200,16,16,"FreeRTOS Examp 20-1");LCD_ShowString(30,50,200,16,16,"Mem Manage");LCD_ShowString(30,70,200,16,16,"KEY_UP:Malloc,KEY1:Free");LCD_ShowString(30,90,200,16,16,"KEY0:Use Mem");LCD_ShowString(30,110,200,16,16,"ATOM@ALIENTEK");LCD_ShowString(30,130,200,16,16,"2016/11/14");LCD_ShowString(30,170,200,16,16,"Total Mem: Bytes");LCD_ShowString(30,190,200,16,16,"Free Mem: Bytes");LCD_ShowString(30,210,200,16,16,"Message: ");POINT_COLOR = BLUE;//创建开始任务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(); //开启任务调度
}

●任务函数

//开始任务任务函数
void start_task(void *pvParameters)
{taskENTER_CRITICAL(); //进入临界区//创建TASK1 任务xTaskCreate((TaskFunction_t )malloc_task,(const char* )"malloc_task",(uint16_t )MALLOC_STK_SIZE,(void* )NULL,(UBaseType_t )MALLOC_TASK_PRIO,(TaskHandle_t* )&MallocTask_Handler);vTaskDelete(StartTask_Handler); //删除开始任务taskEXIT_CRITICAL(); //退出临界区
}
//MALLOC 任务函数
void malloc_task(void *pvParameters)
{u8 *buffer;u8 times,i,key=0;u32 freemem;LCD_ShowxNum(110,170,configTOTAL_HEAP_SIZE,5,16,0);//显示内存总容量(1)while(1){key=KEY_Scan(0);switch(key){case WKUP_PRES:buffer=pvPortMalloc(30); //申请内存,30 个字节(2)printf("申请到的内存地址为:%#x\r\n",(int)buffer);break;case KEY1_PRES:if(buffer!=NULL)vPortFree(buffer); //释放内存(3)buffer=NULL; (4)break;case KEY0_PRES:if(buffer!=NULL) //buffer 可用,使用buffer (5){times++;sprintf((char*)buffer,"User %d Times",times);//向buffer 中填写一些数据LCD_ShowString(94,210,200,16,16,buffer);}break;}freemem=xPortGetFreeHeapSize(); //获取剩余内存大小(6)LCD_ShowxNum(110,190,freemem,5,16,0);//显示内存总容量i++;if(i==50){i=0;LED0=~LED0;}vTaskDelay(10);}
}

(1)、显示内存堆的总容量,内存堆的容量由宏configTOTAL_HEAP_SIZE 来确定的,所以
直接显示configTOTAL_HEAP_SIZE 的值就行了。
(2)、按下KEY_UP 键,调用函数pvPortMalloc()申请内存,大小为30 字节。
(3)、按下KEY1 键,释放(2)中申请到的内存。
(4)、释放内存以后将buffer 设置为NULL,函数vPortFree()释放内存以后并不会将buffer
清零,此时buffer 还是上次申请到的内存地址,如果此时再调用指针buffer 的话你会发现还是
可以使用的!感觉好像没有释放内存,所以这里将buffer 清零!
(5)、判断buffer 是否有效,有效的话就是用buffer。
(6)、调用函数xPortGetFreeHeapSize()获取当前剩余内存大小并且显示到LCD 上。

20.8.2 实验程序运行结果

编译并下载代码到开发板中,LCD 显示如图20.8.2.1 所示:
在这里插入图片描述
可以看出内存堆的总容量为20480 字节,再FreeRTOSConfig.h 文件中我们设置的内存堆
大小如下:

#define configTOTAL_HEAP_SIZE ((size_t)(20*1024)) //20*1024=20480

此时剩余内存大小为17856 字节,因为前面已经创建了一些用户任务和系统任务,所以内
存肯定会被使用掉一部分。按下KEY_UP 键,申请内存,内存申请成功LCD 显示如图20.8.2.2
所示:
在这里插入图片描述
内存申请成功以后会通过串口打印出当前申请到的内存首地址,串口调试助手如图
20.8.2.3 所示:
在这里插入图片描述
从图20.8.2.3 可以看出此时申请到的内存的首地址为0x20005a18。不过此时有的同学可能
注意到了,图7.4.2.2 中剩余内存大小为17816,17856-17816=40,而我们申请的是30 个字节的
内存!内存为什么会少40 个字节呢?多余的10 个字节是怎么回事?前面分析heap_4 内存分配
方法的时候已经说了,这是应用所需的内存大小上加上结构体BlockLink_t 的大小然后做8 字
节对齐后导致的结果。
按下KEY1 键释放内存,此时剩余内存大小又重新变成了17856,说明内存释放成功!
内存泄露:
在使用内存管理的时候最常遇到的一个问题就是内存泄露,内存泄露的原因是没有正确的
释放内存!以本实验为例,连续5 次按下KEY_UP 键,此时LCD 上显示的剩余内存为17656,
如图20.8.2.4 所示:
在这里插入图片描述
图20.8.2.4 连续多次申请内存
17856-17656=200,连续5 次一共申请了200 字节的内存,上面说了一次申请40 个字节
的,5 次肯定就是40*5=200 字节,这个没有错。然后在释放内存,连续按5 次KEY1,此时
LCD 显示剩余内存为17696,如图20.8.2.5 所示:
在这里插入图片描述
图20.8.2.5 连续多次释放内存
17856-17696=160,竟然少了160 个字节的内存,也就是说实际上只释放了一次内存,其他
的4 次是无效的!为什么会这样?这个就是内存泄露,泄露了160 个字节的内存,这个是不正
确的内存申请和释放导致的,内存申请和释放是要成对出现的,在一段内存没有被释放之前绝
对不能再调用一次函数pvPortMalloc()为其再次分配内存!我们连续5 次按KEY_UP 为buffer
申请内存,就犯了这个错误,正确的方法应该是,按一次KEY_UP 按键申请内存成功以后,先
按KEY1 释放掉buffer 的内存,然后再按KEY_UP 为其重新分配内存。初学者很容易犯这样的
错误,忘记释放内存,内存泄露严重的话应用可能因为申请不到合适的内存而导致死机!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/264769.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Solaris 下 Oracle impdp 过程中出现的问题

ORA-39002: invalid operationORA-39070: Unable to open the log file.ORA-29283: invalid file operationORA-06512: at "SYS.UTL_FILE", line 475ORA-29283: invalid file operation解决方法参考1&#xff1a;今天在使用IMPDP完成数据导入的过程中遇到“ORA-39002…

正点原子FreeRTOS(中)

更多干货推荐可以去牛客网看看&#xff0c;他们现在的IT题库内容很丰富&#xff0c;属于国内做的很好的了&#xff0c;而且是课程刷题面经求职讨论区分享&#xff0c;一站式求职学习网站&#xff0c;最最最重要的里面的资源全部免费&#xff01;&#xff01;&#xff01;点击进…

Android应用开发中的风格和主题(style,themes)

越来越多互联网企业都在Android平台上部署其客户端&#xff0c;为了提升用户体验&#xff0c;这些客户端都做得布局合理而且美观.......Android的Style设计就是提升用户体验的关键之一。Android上的Style分为了两个方面&#xff1a; Theme是针对窗体级别的&#xff0c;改变窗体…

windows上安装mysql5.7.24

平时自己做测试的时候&#xff0c;自己安装一个mysql还是很有必要的&#xff0c;网上教程很多&#xff0c;但是自己操作过程中还是遇到了一些问题&#xff0c;这里记录一下安装过程。 一、下载mysql https://downloads.mysql.com/archives/community/ 我使用的是5.7.24的解压版…

gb酱油和gbt酱油哪个好_都是酱油,生抽好还是味极鲜好?老板:两者差别很大,别买错了...

导读&#xff1a;都是酱油&#xff0c;生抽好还是味极鲜好&#xff1f;老板&#xff1a;两者差别很大&#xff0c;别买错了一道美食的完成不只是依赖掌厨的高超技艺&#xff0c;还与炒制的锅具、所用的调料等有关。其中最重要的就是调料&#xff0c;有了调料的辅助&#xff0c;…

数字万用表的使用

参考&#xff1a;连3岁小孩子都能看懂的万用表使用方法 地址&#xff1a;https://www.bilibili.com/video/BV1Gx411z7x2?p1&vd_sourcecc0e43b449de7e8663ca1f89dd5fea7d 目录万用表外观测量电阻测量通断/二极管测量电容测量温度测量电流测量电压测量三极管万用表外观 测量…

在GridView开头插入自动编号的方法

网上看了很多方法&#xff0c;发现都是照抄别人&#xff0c;而且&#xff0c;都是把第一列替换掉了&#xff0c;往往不是我们的理想结果。经过本人的实践&#xff0c;下面方法觉得更好用一些。就是不知道数据量过大时&#xff0c;效率怎么样&#xff0c;不过既然能用&#xff0…

Zookeeper:fsync超时导致实例异常

一、问题描述 2019-02-19 08:44左右&#xff0c;实时计算服务重启&#xff0c;报错显示找不到zk集群的leader节点&#xff0c;同时ZooKeeper集群有告警显示连接超时&#xff1a; 指标[连接耗时(ms)18221]符合告警规则[连接耗时(ms)>3000] 二、排查过程 查看当前集群状态&…

断言(assert)的用法

参考&#xff1a;https://www.runoob.com/w3cnote/c-assert.html 目录作用总结与注意事项Demo作用 assert 是个宏&#xff0c;并且作用并非"报错"。 assert() 的用法像是一种"契约式编程"&#xff0c;程序满足我的假设条件&#xff0c;才能正常良好的运作…

马云语录,非常值得一看(转)

来源:计算机网1999至今 在杭州设立研究开发中心&#xff0c;以香港为总部&#xff0c;创办阿里巴巴网站(Alibaba.com) 孙正义跟我有同一个观点&#xff0c;一个方案是一流的Idea加三流的实施&#xff1b;另外一个方案&#xff0c;一流的实施&#xff0c;三流的Idea&#xff0c;…

centos7 docker安装和使用_入门教程

centos7 docker安装和使用_入门教程 原文:centos7 docker安装和使用_入门教程说明&#xff1a;本文也是参考互联网上的文章写的&#xff0c;感谢相关作者的贡献。 操作系统 64位CentOS Linux release 7.2.1511 (Core) 配置好IP&#xff1a;192.168.1.160 修改yum源 目的是提升对…

C语言中字符串和字符数组的区别

参考&#xff1a;C语言中字符串和字符数组的区别 参考&#xff1a;字符数组和字符串的区别&#xff0c;C语言字符数组和字符串区别详解 这里写目录标题区别代码分析一代码分析二总结区别 &#xff08;1&#xff09;C语言中&#xff0c;没有字符串类型但可以用字符数组模拟字符…

spring in action 读书笔记

IOC 1.几个主要使用的application context. ClassPathXmlApplicationContext 从ClassPath路径加载 FileSystemXmlApplicationContext 从文件系统路径加载XmlWebApplicationContext 配置文件黑夜在/WEB-INF/applicationContext.xml&#xff0c;也可以使用setConfigLocation…

C语言可变参数

参考&#xff1a;https://blog.csdn.net/u013171226/article/details/121445507 目录什么是可变参数可变参数列表构成实现原理(va_list系列变参宏实现变参函数)代码示例函数通过固定参数指定可变参数个数&#xff0c;打印所有变参值函数定义一个结束标记(-1)&#xff0c;调用时…

940mx黑苹果驱动_超详细黑苹果安装图文教程送EFI配置合集及系统

一、准备工作所有工具在&#xff1a;黑苹果资源站可以下载到 网站地址&#xff1a;https://jnzr.ewys.net/1、两张16g的u盘 其中一张安装pe系统 (老毛桃等)这里自行安装2、电脑(废话)这里以小米pro笔记本做教程 其余的本本大同小异3、工具包及镜像以及EFI合集(链接及下载地址在…

python时间减法_干!一张图整理了 Python 所有内置异常

在编写程序时&#xff0c;可能会经常报出一些异常&#xff0c;很大一方面原因是自己的疏忽大意导致程序给出错误信息&#xff0c;另一方面是因为有些异常是程序运行时不可避免的&#xff0c;比如在爬虫时可能有几个网页的结构不一致&#xff0c;这时两种结构的网页用同一套代码…

笔记本电脑频繁自动重启_笔记本电脑自动重启是什么原因

使用电脑很长一段时间就会出现各种各样的问题&#xff0c;但不管出了什么问题&#xff0c;只要电脑能打开有一种方法可以解决的问题&#xff0c;但有时电脑会莫名其妙的重启&#xff0c;电脑爱好者我们有点不知所措。尤其是办公室人员做了很长时间的工作&#xff0c;想要面对以…

新快现类似产品_小米全新折叠屏产品曝光,预计今年还有更多折叠屏产品亮相...

虽然目前小米并未正式推出旗下的折叠屏设备&#xff0c;但这并不意味着小米放弃了这方面的研究。相反&#xff0c;近日的一些爆料显示了小米在折叠屏设备领域有着多种不同的设想和思路。上个月的相关爆料曾提到过&#xff0c;小米2021年有望推出外折型、内折型和翻盖式三种不同…

全虚拟化和半虚拟化的区别 cpu的ring0~ring3又是什么概念?

ring0是指CPU的运行级别&#xff0c;ring0是最高级别&#xff0c;ring1次之&#xff0c;ring2更次之…… 拿Linuxx86来说&#xff0c; 操作系统&#xff08;内核&#xff09;的代码运行在最高运行级别ring0上&#xff0c;可以使用特权指令&#xff0c;控制中断、修改页表、访问…

dual mysql 获取序列_MySQL JDBC客户端反序列化漏洞

标题: MySQL JDBC客户端反序列化漏洞☆ 背景介绍☆ 学习思路☆ 搭建测试环境☆ 恶意MySQL插件 1) 获取MySQL 5.7.28源码 2) 在rewrite_example基础上修改出evilreplace☆ 测试rewriter插件 1) 安装rewriter.so 2) 在服务端替换SQL查询语句 3) 卸载rewriter.so …