低功耗模式

        Tickless模式简介

FreeRTOS的Tickless模式是一种特殊的运行模式,用于最小化系统的时钟中断频率,以降低功耗,在Tickless模式下,系统只有在有需要时才会启动滴答时钟中断(Systick),而在无任务运行时则完全进入休眠状态,从而降低功耗,在滴答时钟中断重启时会对RTOS滴答计数值进行校正调整,即补回缺失的tick(在低功耗模式下SysTick会被关闭,xTickCount不再递增,但是硬件RTC仍在走,等重新唤醒的时候同一加回缺失的xTickCount即可)

        Tickless模式的一般工作原理

  • 空闲任务检测:FreeRTOS会通过空闲任务(idle task)来检测系统是否有任务需要执行,如果没有任务执行,系统可以进入休眠状态
  • 时钟中断:当有任务需要执行时,系统会启动时钟中断,唤醒处理器
  • 时钟中断处理:在时钟中断处理函数中,FreeRTOS将检查任务的状态并决定是否继续执行
  • 休眠状态:如果没有任务需要执行,系统可以进入休眠状态,关闭时钟中断,在休眠状态下,处理器可以进入更低功耗的模式
  • 任务唤醒:当有任务需要执行时,系统会再次启动时钟中断,唤醒处理器,然后执行相应的任务

        Tickless模式详解

一些增强型产品支持三种低功耗模式,可以在要求低功耗、短启动事件和多种唤醒事件之间达到最佳平衡

短空闲进sleep,长空闲进stop,超长空闲并允许系统重启时进standby

  • 睡眠模式(Sleep Mode):只有CPU停止,所有外设处于工作状态并可在发生中断/事件时唤醒CPU
  • 停机模式(Stop Mode):保持SRAM和寄存器内容不丢失的情况下,停机模式可以达到最低的电能消耗,在停机模式下,停止所有内部1.8V部分的供电,PLL、HSI的RC振荡器和HSE晶体振荡器被关闭,调压器可以被置于普通模式或低功耗模式,可以通过任一配置成EXTI的信号把微控制器从停机模式中唤醒,EXTI信号可以是16个外部I/O 口之一、PVD的输出、RTC闹钟或USB的唤醒信号
  • 待机模式(Standby Mode):在待机模式下可以达到最低的电能消耗,内部的电压调压器被关闭,因此所有内部1.8V部分的供电被切断,PLL、HSI的RC振荡器和HSE晶体振荡器也被关闭,进入待机模式后,SRAM和寄存器的内容将消失,但后备寄存器的内容仍然保留,待机电路仍工作,从待机模式退出的条件是:NRST上的外部复位信号、IWDG复位、WKUP引脚上的一个上升边沿或RTC的闹钟到时

注意:在进入停机或待机模式时,RTC、IWDG和对应的时钟不会被停止

FreeRTOS系统中的所有其它任务都不在运行时(处于阻塞或挂起),会运行空闲任务,所以想不影响系统运行又降低功耗,可以在空闲任务执行的期间,让MCU 进入相应的低功耗模式,由于滴答定时器频繁中断则会影响低功耗,所以FreeRTOS的Tickless低功耗模式会自动把滴答定时器的中断周期修改为低功耗运行时间,退出低功耗后再补上系统时钟节拍数

        Tickless模式配置流程

#define configUSE_TICKLESS_IDLE                         1
#include "freertos_demo.h"
#define configPRE_SLEEP_PROCESSING( x )         PRE_SLEEP_PROCESSING()
//进入低功耗之前的操作,这里的是自定义的函数名,可以在这手动关闭外部时钟
#define configPOST_SLEEP_PROCESSING( x )        POST_SLEEP_PROCESSING()
//进入低功耗之后的操作

这里这套配置是无参回调做一个简单的演示,本来的配置是configPRE_SLEEP_PROCESSING( xModifiableIdleTime );这里没有用到参数xModifiableIdleTime,这样子无法阻止后面的__WIFI(),当xModifiableIdleTime>0的时候会执行__WFI(),__WFI()是ARM Cortex-M的指令,执行它之后CPU会进入睡眠状态,直到有中断把他唤醒,如果用户在PRE_SLEEP_PROCESSING()中自定义了进入哪种睡眠状态,但是后面又跑一次__WFI()会再次发起一次睡眠动作,整个时序就变成了先睡一次、醒来、又睡一次,从而打乱用户原来定义好的睡眠流程

进入睡眠模式是按照以下这个流程:

任务都阻塞
-> Idle Task 运行
-> FreeRTOS 判断可以 tickless
-> vPortSuppressTicksAndSleep()
-> PRE_SLEEP_PROCESSING()
-> __WFI()
-> 中断唤醒
-> POST_SLEEP_PROCESSING()
-> 恢复调度

真正实现让CPU睡眠的是在FreeRTOS对口层的port.c中的vPortSuppressTicksAndSleep() 的关键逻辑:如下紧接着的代码,所以前面说需要设置xModifiableIdleTime参数才可以避免打乱用户定义的睡眠模式

  • vPortSuppressTicksAndSleep() 是系统级的钩子函数,专门用于Tickless idle,用来在系统空闲的时候停止SysTick然后进入低功耗,唤醒之后再补回tick
configPRE_SLEEP_PROCESSING( xModifiableIdleTime );

if( xModifiableIdleTime > 0 )
{
    __dsb(...);
    __wfi();
    __isb(...);
}

configPOST_SLEEP_PROCESSING( xExpectedIdleTime );

      Tickless代码例程

#include "freertos_demo.h"
#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"
#include "LED.h"
#include "Key.h"

/* 启动任务的配置 */
#define START_TASK_STACK 128
#define START_TASK_PRIORITY 1
TaskHandle_t start_task_handle;
void start_task(void *pvParameters);

/* 任务1的配置 */
#define TASK1_STACK 128
#define TASK1_PRIORITY 2
TaskHandle_t task1_handle;
void task1(void *pvParameters);

/* 任务2的配置 */
#define TASK2_STACK 128
#define TASK2_PRIORITY 3
TaskHandle_t task2_handle;
void task2(void *pvParameters);

/* 进入睡眠前的处理:可以手动关闭外设时钟 */
void PRE_SLEEP_PROCESSING()
{
    __HAL_RCC_GPIOA_CLK_DISABLE();
    __HAL_RCC_GPIOB_CLK_DISABLE();
    __HAL_RCC_GPIOC_CLK_DISABLE();
    __HAL_RCC_GPIOD_CLK_DISABLE();
    __HAL_RCC_GPIOE_CLK_DISABLE();
    __HAL_RCC_GPIOF_CLK_DISABLE();
    __HAL_RCC_GPIOG_CLK_DISABLE();
}

/* 退出睡眠后的处理:可以手动开启外设时钟 */
void POST_SLEEP_PROCESSING()
{
    __HAL_RCC_GPIOA_CLK_ENABLE();
    __HAL_RCC_GPIOB_CLK_ENABLE();
    __HAL_RCC_GPIOC_CLK_ENABLE();
    __HAL_RCC_GPIOD_CLK_ENABLE();
    __HAL_RCC_GPIOE_CLK_ENABLE();
    __HAL_RCC_GPIOF_CLK_ENABLE();
    __HAL_RCC_GPIOG_CLK_ENABLE();
}

/**
 * @description: 启动FreeRTOS
 * @return {*}
 */
QueueHandle_t sem_handle;
void freertos_start(void)
{

    //xSemaphoreCreateBinaryx开头的,不会主动释放信号量
    vSemaphoreCreateBinary(sem_handle); //v开头的,创建完会主动释放一次信号量
    if (sem_handle == NULL)
    {
        printf("二值信号量创建失败\r\n");
    }


    xTaskCreate((TaskFunction_t)start_task,               // 任务函数的地址
                (char *)"start_task",                     // 任务名字符串
                (configSTACK_DEPTH_TYPE)START_TASK_STACK, // 任务栈大小,默认最小128,单位4字节
                (void *)NULL,                             // 传递给任务的参数
                (UBaseType_t)START_TASK_PRIORITY,         // 任务的优先级
                (TaskHandle_t *)&start_task_handle);      // 任务句柄的地址

    vTaskStartScheduler();
}


void start_task(void *pvParameters)
{

    taskENTER_CRITICAL();

    xTaskCreate((TaskFunction_t)task1,
                (char *)"task1",
                (configSTACK_DEPTH_TYPE)TASK1_STACK,
                (void *)NULL,
                (UBaseType_t)TASK1_PRIORITY,
                (TaskHandle_t *)&task1_handle);
    xTaskCreate((TaskFunction_t)task2,
                (char *)"task2",
                (configSTACK_DEPTH_TYPE)TASK2_STACK,
                (void *)NULL,
                (UBaseType_t)TASK2_PRIORITY,
                (TaskHandle_t *)&task2_handle);

    taskEXIT_CRITICAL();
    vTaskDelete(NULL);
}

/**
 * @description: 任务一:用于按键扫描,当检测到按键KEY1被按下时,释放二值信号量。
 * @param {void} *pvParameters
 * @return {*}
 */
void task1(void *pvParameters)
{

    uint8_t key = 0;
    BaseType_t res = 0;
    while (1)
    {
        key = Key_Detect();
        if (key == KEY1_PRESS)
        {
            /* 释放二值信号量 */
            res = xSemaphoreGive(sem_handle);
            if (res == pdPASS)
            {
                printf("Task1释放二值信号量成功\r\n");
            }
            else
            {
                printf("Task1释放二值信号量失败\r\n");
            }
        }
        vTaskDelay(500);
    }
}

/**
 * @description: 任务二:获取二值信号量,当成功获取后打印提示信息
 * @param {void} *pvParameters
 * @return {*}
 */
void task2(void *pvParameters)
{
    BaseType_t res = 0;
    while (1)
    {
        /* 获取二值信号量 */
        res = xSemaphoreTake(sem_handle, portMAX_DELAY);
        if (res == pdPASS)
        {
            printf("Task2获取信号量成功\r\n");
        }
        else
        {
            printf("Task2获取信号量失败\r\n");
        }
    }
}
  • 这里任务一采用的是轮询式访问,一直轮询检测按键有没有被按下,更到位的低功耗模式设计应该中断驱动唤醒效果更好,和搭配多用阻塞式等待如vTaskDelay()、xSemaphoreTake()、xQueueReceive()、xEventGroupWaitBits()等
  • 当前的代码走的是FreeRTOS端口层默认的__WFI()睡眠路径,而不是手动选择切换到对应的三种模式之一,如果要切换到具体的模式应该调用对应的函数,然后再在函数PRE_SLEEP_PROCESSING()里切换到自己指定的函数
    • Sleep模式:
      • HAL_PWR_EnterSLEEPMode(PWR_MAINREGULATOR_ON, PWR_SLEEPENTRY_WFI);
    • Stop模式:
      • HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI);
      • 或者HAL_PWR_EnterSTOPMode(PWR_MAINREGULATOR_ON, PWR_STOPENTRY_WFI);
    • Standby模式:
      • HAL_PWR_EnterSTANDBYMode();

内存管理

在使用 FreeRTOS 创建任务、队列、信号量等对象时,通常都有动态创建和静态创建的方式,动态方式提供了更灵活的内存管理,而静态方式则更注重内存的静态分配和控制
如果是动态创建的话malloc()和free()有时会用于此目的但是会有一定的缺点

  • 在嵌入式系统中并不通用、占用了宝贵的带啊吗空间、不是线程安全、不是确定性

所以更多的时候需要的不是一个替代的内存分配实现,一个嵌入式/实时系统的 RAM 和定时要求可能与另一个非常不同,所以单一的 RAM 分配算法将永远只适用于一个应用程序子集,为了避免此问题,FreeRTOS 将内存分配 API 保留在其可移植层,提供了五种内存管理算法:

  • heap_1:最简单,不允许释放内存
  • heap_2:允许释放内存,但不会合并相邻的空闲块
  • heap_3:简单包装了标准 malloc() 和 free(),以保证线程安全
  • heap_4:合并相邻的空闲块以避免碎片化。包含绝对地址放置选项
  • heap_5:如同 heap_4,能够跨越多个不相邻内存区域的堆

        heap_1算法

heap_1是最简单的实现方式,内存一经分配,它不允许内存再被释放,适用于小型和深度嵌入的应用程序在系统启动给时创建了所需的所有任务、队列、信号量等,并在程序的生命周期内使用这些所有对象,直到应用程序再洗关闭或重新启动,任何内容都不会被删除

        heap_2算法

heap2使用最佳适应算法,并且允许释放先前分配的块,但是不将相邻的空闲块组合成一个大块,适用于许多必须动态创建对象的小型实时系统,如果动态地创建和删除任务,且分配给正在创建任务的堆栈大小总是相同的,那么 heap2.c 可以在大多数情况下使用,但是,如果分配给正在创建任务的堆栈的大小不是总相同,那么可用的空闲内存可能会被碎片化成许多小块,最终导致分配失败

        heap_3算法

heap_3使用 C 库的 malloc 和 free 函数来进行内存分配和释放,它通过分配固定大小的块来管理内存,这些块的大小在配置 FreeRTOS 时进行定义,不会动态改变
比如一开始用heap_3管理内存,其中块的大小固定为32字节,,初始时整个内存被分割成大小为32块的字节,然后有一个任务请求分配20字节的内存,然后heap_3算法将块1分割成20字节和12字节两个内存块,,再假设另一个任务请求40字节的内存由于没有足够大的内存块heap_3将返回分配失败的状态,heap_3 的特点是块大小固定,这样可以简化内存管理,然而,也因为块大小不可变,可能导致内存碎片问题,即一些块可能无法完全被利用,从而浪费了一些内存

        heap4_算法

heap_4使用第一适应算法,并且会将相邻的空闲内寸块合并成大内存块,减少内存碎片,第一适应算法会在可用内存块中选择第一个足够大的内存块进行分配,是最常用的内存算法

        heap_5算法

heap_5使用和heap_4相同的第一适应和内存合并算法,允许堆跨越多个不相邻(非连续)内寸区域,适用于内寸地址不连续的复杂场景

FreeTOS也提供了一些函数让用户可以自定义申请和释放

函数 描述
void * pvPortMalloc( size_t  xWantedSize ); 申请内存
void  vPortFree( void * pv ); 释放内存
size_t  xPortGetFreeHeapSize( void ); 获取当前空闲内存的大小

#include "freertos_demo.h"
#include "FreeRTOS.h"
#include "task.h"
#include "LED.h"
#include "Key.h"

/* 启动任务的配置 */
#define START_TASK_STACK 128
#define START_TASK_PRIORITY 1
TaskHandle_t start_task_handle;
void start_task(void *pvParameters);

/* 任务1的配置 */
#define TASK1_STACK 128
#define TASK1_PRIORITY 2
TaskHandle_t task1_handle;
void task1(void *pvParameters);

void freertos_start(void)
{
    xTaskCreate((TaskFunction_t)start_task,               // 任务函数的地址
                (char *)"start_task",                     // 任务名字符串
                (configSTACK_DEPTH_TYPE)START_TASK_STACK, // 任务栈大小,默认最小128,单位4字节
                (void *)NULL,                             // 传递给任务的参数
                (UBaseType_t)START_TASK_PRIORITY,         // 任务的优先级
                (TaskHandle_t *)&start_task_handle);      // 任务句柄的地址
    vTaskStartScheduler();
}


void start_task(void *pvParameters)
{

    taskENTER_CRITICAL();

    xTaskCreate((TaskFunction_t)task1,
                (char *)"task1",
                (configSTACK_DEPTH_TYPE)TASK1_STACK,
                (void *)NULL,
                (UBaseType_t)TASK1_PRIORITY,
                (TaskHandle_t *)&task1_handle);
   
    taskEXIT_CRITICAL();
    vTaskDelete(NULL);
}

/**
 * @description: 任务一:当KEY1按下则申请内存,当KEY2按下则释放内存,并打印剩余内存信息
 * @param {void} *pvParameters
 * @return {*}
 */
void task1(void *pvParameters)
{
    uint8_t key = 0;
    void *buff = 0;
    while (1)
    {
        key = Key_Detect();
        if (key == KEY1_PRESS)
        {
            /* 申请内存:实际申请会比传入的参数还大,多出两部分:① 堆结构体的大小;② 字节对齐会浪费一部分 */
            buff = pvPortMalloc(20);
            printf("申请20内存,当前剩余空闲内存=%d\r\n", xPortGetFreeHeapSize());
        }
        else if (key == KEY2_PRESS)
        {
            /* 释放内存 */
            vPortFree(buff);
            printf("释放内存,当前剩余空闲内存=%d\r\n", xPortGetFreeHeapSize());
        }

        vTaskDelay(500);
    }
}

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐