FreeRTOS 自定义Tickless的实现

FreeRTOS的Tickless模式

注:以下内容来自硬汉论坛的FreeRTOS教程。

tickless 低功耗机制是当前小型 RTOS 所采用的通用低功耗方法,比如 embOS,RTX 和 uCOS-III (类似方法)都有这种机制。
FreeRTOS 的低功耗也是采用的这种方式,那么 tickless 又是怎样一种模式呢?仅从字母上看 tick 是滴答时钟的意思,less 是 tick 的后缀,表示较少的,这里的含义可以表示为无滴答时钟。 整体看这个字母就是表示滴答时钟节拍停止运行的情况。反映在 FreeRTOS 上,tickless 又是怎样一种情况呢?我们都知道,当用户任务都被挂起或者阻塞时,最低优先级的空闲任务会得到执行。 那么 STM32 支持的睡眠模式,停机模式就可以放在空闲任务里面实现。 为了实现低功耗最优设计,我们还不能直接把睡眠或者停机模式直接放在空闲任务就可以了。 进入空闲任务后,首先要计算可以执行低功耗的最大时间,也就是求出下一个要执行的高优先级任务还剩多少时间。 然后就是把低功耗的唤醒时间设置为这个求出的时间,到时间后系统会从低功耗模式被唤醒,继续执行多任务。这个就是所谓的 tickless 模式。
对于 Cortex-M3 和 M4 内核来说,FreeRTOS 已经提供了 tickless 低功耗代码的实现,通过调用指令 WFI 实现睡眠模式,具体代码的实现就在 port.c 文件中,用户只需在 FreeRTOSConfig.h 文件中配置宏定义 configUSE_TICKLESS_IDLE 为 1 即可。 如果配置此参数为 2,那么用户可以自定义 tickless 低功耗模式的实现。 当用户将宏定义 configUSE_TICKLESS_IDLE 配置为 1 且系统运行满足以下两个条件时,系统内核会自动的调用低功耗宏定义函数 portSUPPRESS_TICKS_AND_SLEEP()

实现自己的Tickless

本文以华大MCU HC32L190为例。系统默认是使用Systick作为系统滴答时钟,这里保持这个不变,使用一个低功耗定时器将芯片从深度睡眠模式中唤醒。

使能自定义Tickless

FreeRTOSConfig.h下定义如下的宏,如果已存在就修改为2。

#define configUSE_TICKLESS_IDLE         2

定义需要用的变量和宏

#if( configUSE_TICKLESS_IDLE == 2 )

#define configLPTIMER_CLOCK_HZ    32768U

/* The LPTIM is a 16-bit counter. */
#define portMAX_16_BIT_NUMBER               ( 0xffffUL )

/*
* The number of LPTIM increments that make up one tick period.
*/
static uint32_t ulTimerCountsForOneTick = 0;

/*
 * The maximum number of tick periods that can be suppressed is limited by the
 * 16 bit resolution of the lowpower timer.
 */
static uint32_t xMaximumPossibleSuppressedTicks = 0;
#endif

在vPortSetupTimerInterrupt()中增加获取定时相关参数的代码,主要给后面计算定时多少时间用

__attribute__(( weak )) void vPortSetupTimerInterrupt( void )
{
    /* Calculate the constants required to configure the tick interrupt. */
    #if( configUSE_TICKLESS_IDLE == 1 )
    {
        ulTimerCountsForOneTick = ( configCPU_CLOCK_HZ / configTICK_RATE_HZ );
        xMaximumPossibleSuppressedTicks = portMAX_24_BIT_NUMBER / ulTimerCountsForOneTick;
        ulStoppedTimerCompensation = portMISSED_COUNTS_FACTOR;
    }
    #endif /* configUSE_TICKLESS_IDLE */

    #if( configUSE_TICKLESS_IDLE == 2 )
    {
        ulTimerCountsForOneTick = ( configLPTIMER_CLOCK_HZ / configTICK_RATE_HZ );
        xMaximumPossibleSuppressedTicks = portMAX_16_BIT_NUMBER / ulTimerCountsForOneTick;
    }
    #endif /* configUSE_TICKLESS_IDLE */

    /* Stop and reset the SysTick. */
    portNVIC_SYSTICK_CTRL_REG = 0UL;
    portNVIC_SYSTICK_CURRENT_VALUE_REG = 0UL;

    /* Configure SysTick to interrupt at the requested rate. */
    portNVIC_SYSTICK_LOAD_REG = ( configCPU_CLOCK_HZ / configTICK_RATE_HZ ) - 1UL;
    portNVIC_SYSTICK_CTRL_REG = portNVIC_SYSTICK_CLK_BIT | portNVIC_SYSTICK_INT_BIT | portNVIC_SYSTICK_ENABLE_BIT;
}

实现portSUPPRESS_TICKS_AND_SLEEP()

HC32L190的低功耗定时器为16位定时器,为了尽可能保持精度,不进行分频,因此最大定时时长为2秒。当系统空闲时间大于2秒时,需要通过唤醒再休眠的方式实现在系统空闲时完全休眠。

#if configUSE_TICKLESS_IDLE == 2

#include "ddl.h"
#include "lptim.h"
#include "lpm.h"

static volatile bool lptim_timeout;

void vPortSuppressTicksAndSleep( TickType_t xExpectedIdleTime )
{
    uint32_t ulReloadValue, ulCompleteTickPeriods, ulCompletedSysTickDecrements;
    TickType_t xModifiableIdleTime;

    /* Make sure the LPTIM reload value does not overflow the counter. */
    if( xExpectedIdleTime > xMaximumPossibleSuppressedTicks )
    {
        xExpectedIdleTime = xMaximumPossibleSuppressedTicks;
    }

    /* Stop the SysTick momentarily.  The time the SysTick is stopped for
    is accounted for as best it can be, but using the tickless mode will
    inevitably result in some tiny drift of the time maintained by the
    kernel with respect to calendar time. */
    portNVIC_SYSTICK_CTRL_REG &= ~portNVIC_SYSTICK_ENABLE_BIT;

    /* Calculate the reload value required to wait xExpectedIdleTime
    tick periods.  -1 is used because this code will execute part way
    through one of the tick periods. */
    /* 定时时钟周期数为(0XFFFF-ARR+0X0001) */
    ulReloadValue   = (uint16_t)(0x10000 - ulTimerCountsForOneTick * ( xExpectedIdleTime - 1 ));

    /* Enter a critical section but don't use the taskENTER_CRITICAL()
    method as that will mask interrupts that should exit sleep mode. */
//   __asm volatile( "cpsid i" ::: "memory" );
//   __asm volatile( "dsb" );
//   __asm volatile( "isb" );

    /* If a context switch is pending or a task is waiting for the scheduler
    to be unsuspended then abandon the low power entry. */
    if( eTaskConfirmSleepModeStatus() == eAbortSleep )
    {
        /* Restart from whatever is left in the count register to complete
        this tick period. */
        portNVIC_SYSTICK_LOAD_REG = portNVIC_SYSTICK_CURRENT_VALUE_REG;

        /* Restart SysTick. */
        portNVIC_SYSTICK_CTRL_REG |= portNVIC_SYSTICK_ENABLE_BIT;

        /* Reset the reload register to the value required for normal tick
        periods. */
        portNVIC_SYSTICK_LOAD_REG = ( configCPU_CLOCK_HZ / configTICK_RATE_HZ ) - 1UL;

        /* Re-enable interrupts - see comments above the cpsid instruction()
        above. */
//       __asm volatile( "cpsie i" ::: "memory" );
    }
    else
    {
        volatile uint32_t ulCompleteTickPeriods;
        volatile uint32_t sleep_time;
        stc_lptim_cfg_t    lptim_cfg;

        DDL_ZERO_STRUCT(lptim_cfg);

        lptim_cfg.enTcksel = LptimRcl;          // 选择外部低速晶体振荡器作为时钟源
        lptim_cfg.enCt     = LptimTimerFun;     // 定时器功能
        lptim_cfg.enMd     = LptimMode2;        // 工作模式为模式2:自动重装载16位计数器/定时器
        lptim_cfg.enPrs    = LptimPrsDiv1;      // Frcl
        lptim_cfg.enGate   = LptimGateLow;      // 无门控,TR=1时定时器工作

        lptim_cfg.u16Arr   = ulReloadValue;

        Lptim_Init(M0P_LPTIMER0, &lptim_cfg);
        lptim_timeout = false;
        Lptim_Cmd(M0P_LPTIMER0, TRUE);
        /* Sleep until something happens.  configPRE_SLEEP_PROCESSING() can
        set its parameter to 0 to indicate that its implementation contains
        its own wait for interrupt or wait for event instruction, and so wfi
        should not be executed again.  However, the original expected idle
        time variable must remain unmodified, so a copy is taken. */
        xModifiableIdleTime = xExpectedIdleTime;
        configPRE_SLEEP_PROCESSING( xModifiableIdleTime );
        if( xModifiableIdleTime > 0 )
        {
            __asm volatile( "dsb" ::: "memory" );
            __asm volatile( "wfi" );
            __asm volatile( "isb" );
        }
        configPOST_SLEEP_PROCESSING( xExpectedIdleTime );
        
        /* Re-enable interrupts to allow the interrupt that brought the MCU
        out of sleep mode to execute immediately.  see comments above
        __disable_interrupt() call above. */
//      __asm volatile( "cpsie i" ::: "memory" );
//      __asm volatile( "dsb" );
//      __asm volatile( "isb" );

        /* Disable interrupts again because the clock is about to be stopped
        and interrupts that execute while the clock is stopped will increase
        any slippage between the time maintained by the RTOS and calendar
        time. */
//      __asm volatile( "cpsid i" ::: "memory" );
//      __asm volatile( "dsb" );
//      __asm volatile( "isb" );
        
        if (true == lptim_timeout)
        {
            ulCompleteTickPeriods = xExpectedIdleTime - 1UL;
        }
        else
        {
            ulCompleteTickPeriods = (M0P_LPTIMER0->CNT - M0P_LPTIMER0->ARR) / ulTimerCountsForOneTick;
        }

        /* Restart SysTick so it runs from portNVIC_SYSTICK_LOAD_REG
        again, then set portNVIC_SYSTICK_LOAD_REG back to its standard
        value. */
        portNVIC_SYSTICK_CURRENT_VALUE_REG = 0UL;
        portNVIC_SYSTICK_CTRL_REG |= portNVIC_SYSTICK_ENABLE_BIT;
        vTaskStepTick( ulCompleteTickPeriods );
        portNVIC_SYSTICK_LOAD_REG = ( configCPU_CLOCK_HZ / configTICK_RATE_HZ ) - 1UL;

        /* Exit with interrpts enabled. */
//       __asm volatile( "cpsie i" ::: "memory" );
    }
}

/*******************************************************************************
 * 中断服务函数
 ******************************************************************************/
/**
 ******************************************************************************
 ** \brief  LPTIMER中断服务函数
 **
 ** \return 无
 ******************************************************************************/
void LpTim0_IRQHandler(void)
{
    if (TRUE == Lptim_GetItStatus(M0P_LPTIMER0))
    {
        Lptim_ClrItStatus(M0P_LPTIMER0);//清除LPTimer的中断标志位

        lptim_timeout = true;
    }
}
#endif

注意:这里注释掉了控制中断的汇编代码!!!

休眠前后的处理

FreeRTOSConfig.h下定义如下的宏

extern uint32_t enter_deepsleep_handle(uint32_t tick);
extern void exit_deepsleep_handle(uint32_t tick);

#define configPRE_SLEEP_PROCESSING(tick)    do {tick = enter_deepsleep_handle(tick);}while(0)
#define configPOST_SLEEP_PROCESSING(tick)   exit_deepsleep_handle(tick)

在其他地方实现上面两个函数,本文的例子中只使用了SPI这个外设,因此休眠前关闭SPI,并将相关的引脚配置为功耗最低的状态,在休眠结束后再重新配置回来。

uint32_t enter_deepsleep_handle(uint32_t tick)
{
    stc_gpio_cfg_t GpioInitStruct;

    DDL_ZERO_STRUCT(GpioInitStruct);

    /* GPIO Ports Clock Enable */
    Sysctrl_SetPeripheralGate(SysctrlPeripheralGpio, TRUE);

    ///< 不用的引脚全部配置成 数字端口
    // DW用到的引脚都是数字引脚,不需要处理这一步骤

    ///< 不用的引脚全部配置成 输入IO
    ///< 不用的引脚全部配置成 使能下拉
    GpioInitStruct.enDir = GpioDirIn;
    GpioInitStruct.enPd = GpioPdEnable;

    Gpio_Init(BMI_CSn_GPIO_Port, BMI_CSn_Pin, &GpioInitStruct);
    Gpio_Init(BMI_SCK_GPIO_Port, BMI_SCK_Pin, &GpioInitStruct);
    Gpio_Init(BMI_MISO_GPIO_Port, BMI_MISO_Pin, &GpioInitStruct);
    Gpio_Init(BMI_MOSI_GPIO_Port, BMI_MOSI_Pin, &GpioInitStruct);

    ///<复位模块
    Reset_RstPeripheral0(ResetMskSpi0);

    ///< 关闭外设时钟
    Sysctrl_SetPeripheralGate(SysctrlPeripheralSpi0, FALSE);

    /* 进入深度休眠模式 */
    Lpm_GotoDeepSleep(FALSE);
    /* 唤醒后停止定时器 */
    Lptim_Cmd(M0P_LPTIMER0, FALSE);
    /* 返回0,不使用通用的休眠 */
    return 0;
}

void exit_deepsleep_handle(uint32_t tick)
{
    bmi_gpio_init();
    bim_spi_init();
}

你可能感兴趣的:(FreeRTOS 自定义Tickless的实现)