This post is older than 2 years and might not be relevant anymore
More Info: Consider searching for newer posts

nRF52840, SDK 14.0.0 and tickless idle FreeRTOS - some suggestions

This post is more about some suggestions for review rather than a question.

I've been looking at implementing a project using the nRF52840, SDK 14.0.0 and FreeRTOS in a low power application. The release note for SDK 14.0.0. indicates that FreeRTOS is not supported on the nRF52840, however I see no reason why FreeRTOS shouldn't work on this platform.

I ported the example ble_app_hrs_freertos app for ARM GCC to pca10056 and found that current consumption was very high even with tickless idle enabled. Upon profiling the idle code, I found that sd_app_event_wait() was being called and then exiting immediately on every iteration of the idle loop.

There were what seemed to be a number of improvements that could be made. Please see the attached port_cmsis_systick.c files for my suggested changes.

They include:

  • Don't turn off app interrupts when going into idle. Instead only turn off RTC timing interrupts. This should improve interrupt latency while sleeping since interrupt handlers will run directly after return from WFE events. This also allows interrupt handlers to make FreeRTOS API calls which could potentially set tasks to running.
  • Stay as long as possible in the inner tickless idle loop if there is no reason to exit. That is, the only reason to exit idle is if a task has been scheduled to run, or the maximum time to wait has elapsed.
  • Don't rely solely on RTC capture compare interrupt to wake from WFE. Instead, use a counter comparison check to determine if wait time has elapsed.
  • Clear pending CPU event using SEV/WFE before testing for idle exit and going to sleep. This prevents any event prior to going to idle from waking the CPU immediately on a call to sd_app_event_wait().

There are likely some improvements to make to this code, however I have found that these changes result in a significant reduction in consumed power.

The attached port_cmsis_systick_with_profiling.c contains a set of counters which are ticked at various points of the idle loop to try and gain an understanding of how often events occur. eg We can determine how often the code requests idle, how many times we wake CPU from idle but the conditions for FreeRTOS are not met to exit idle (eg as a result of a BLE interrupt).

I'd be interested in feedback on this approach and to know if there are any holes in my reasoning with the proposed changes.

I've attached the ble_app_hrs_freertos app ported to pca10056. This also includes an FPU_IRQHandler to address issues with CPU not sleeping with FPU interrupt pending.

ble_app_hrs_freertos_pca10056.tgz

port_cmsis_systick_with_profiling.c

port_cmsis_systick.c

UPDATED 16th Oct 2018:

See the updated port_cmsis_systick.c which includes important changes including the following:

  • Fixes a bug which could result in infinite sleep in idle.  Ensure that pending RTC wake interrupt is cleared before entry to sleep.
  • Ensures pending FPU interrupts are cleared before entry to sleep.
  • Support GPIO profiling of time spent asleep.  Use critical section around WFE instruction when profiling to ensure that GPIO pins are set to show CPU asleep/awake state prior to interrupt handlers running.

/*
 * FreeRTOS Kernel V10.0.0
 * Copyright (C) 2017 Amazon.com, Inc. or its affiliates.  All Rights Reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software. If you wish to use our Amazon
 * FreeRTOS name, please do so in a fair use way that does not cause confusion.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 *
 * http://www.FreeRTOS.org
 * http://aws.amazon.com/freertos
 *
 * 1 tab == 4 spaces!
 */

/* Scheduler includes. */
#include "FreeRTOS.h"
#include "task.h"
#include "app_util.h"
#include "nrf_log.h"

#ifdef SOFTDEVICE_PRESENT
#include "nrf_soc.h"
#include "nrf_sdh.h"
#include "app_error.h"
#include "app_util_platform.h"
#endif


#define PROFILE_TICKLESS_IDLE    0

#if PROFILE_TICKLESS_IDLE == 1

#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]))

struct {
    /* Total number of times the idle wait loop is entered. */
    uint32_t n_idle_loops;

    /* The number of time the CPU is woken from sd_app_evt_wait() due to an event
     * where the event hasn't cause a reschedule or the tickless idle time has elapsed.
     */
    uint32_t n_cpu_wake_no_rtc_or_sched;

    /* Number of time an exit from idle occurs during a scheduling event.
     * Also extract number of time this occurs based on how many times the
     * CPU wakes up (loop variables).
     */
    uint32_t n_sched_idle_wakeup;
    uint32_t n_sched_idle_wakeup_loop[10];

    /* Number of time an exit from idle occurs during RTC time elapsed.
     * Also extract number of time this occurs based on how many times the
     * CPU wakes up (loop variables).
     */
    uint32_t n_rtc_idle_wakeup;
    uint32_t n_rtc_idle_wakeup_loop[10];

    /* Number of times sd_app_evt_wait is called. */
    uint32_t n_sd_app_evt_wait;
} profile_tickless_g;

#endif


/*-----------------------------------------------------------
 * Implementation of functions defined in portable.h for the ARM CM4F port.
 * CMSIS compatible layer to menage SysTick ticking source.
 *----------------------------------------------------------*/

#if configTICK_SOURCE == FREERTOS_USE_SYSTICK


#ifndef configSYSTICK_CLOCK_HZ
    #define configSYSTICK_CLOCK_HZ configCPU_CLOCK_HZ
    /* Ensure the SysTick is clocked at the same frequency as the core. */
    #define portNVIC_SYSTICK_CLK_BIT    ( SysTick_CTRL_CLKSOURCE_Msk )
#else
    /* The way the SysTick is clocked is not modified in case it is not the same
    as the core. */
    #define portNVIC_SYSTICK_CLK_BIT    ( 0 )
#endif


#if configUSE_TICKLESS_IDLE == 1
    #error SysTick port for RF52 does not support tickless idle. Use RTC mode instead.
#endif /* configUSE_TICKLESS_IDLE */

/*-----------------------------------------------------------*/

void xPortSysTickHandler( void )
{
    /* The SysTick runs at the lowest interrupt priority, so when this interrupt
    executes all interrupts must be unmasked.  There is therefore no need to
    save and then restore the interrupt mask value as its value is already
    known. */
    ( void ) portSET_INTERRUPT_MASK_FROM_ISR();
    {
        /* Increment the RTOS tick. */
        if ( xTaskIncrementTick() != pdFALSE )
        {
            /* A context switch is required.  Context switching is performed in
            the PendSV interrupt.  Pend the PendSV interrupt. */
            SCB->ICSR = SCB_ICSR_PENDSVSET_Msk;
        }
    }
    portCLEAR_INTERRUPT_MASK_FROM_ISR( 0 );
}

/*-----------------------------------------------------------*/

/*
 * Setup the systick timer to generate the tick interrupts at the required
 * frequency.
 */
void vPortSetupTimerInterrupt( void )
{
    /* Set interrupt priority */
    NVIC_SetPriority(SysTick_IRQn, configKERNEL_INTERRUPT_PRIORITY);
    /* Configure SysTick to interrupt at the requested rate. */
    SysTick->LOAD = ROUNDED_DIV(configSYSTICK_CLOCK_HZ, configTICK_RATE_HZ) - 1UL;
    SysTick->CTRL = ( portNVIC_SYSTICK_CLK_BIT | SysTick_CTRL_TICKINT_Msk | SysTick_CTRL_ENABLE_Msk );
}

/*-----------------------------------------------------------*/

#elif configTICK_SOURCE == FREERTOS_USE_RTC

#if configUSE_16_BIT_TICKS == 1
#error This port does not support 16 bit ticks.
#endif

#include "nrf_rtc.h"
#include "nrf_drv_clock.h"

/*-----------------------------------------------------------*/

void xPortSysTickHandler( void )
{
#if configUSE_TICKLESS_IDLE == 1
    nrf_rtc_event_clear(portNRF_RTC_REG, NRF_RTC_EVENT_COMPARE_0);
#endif

    BaseType_t switch_req = pdFALSE;
    uint32_t isrstate = portSET_INTERRUPT_MASK_FROM_ISR();

    uint32_t systick_counter = nrf_rtc_counter_get(portNRF_RTC_REG);
    nrf_rtc_event_clear(portNRF_RTC_REG, NRF_RTC_EVENT_TICK);

    if (configUSE_DISABLE_TICK_AUTO_CORRECTION_DEBUG == 0)
    {
        /* check FreeRTOSConfig.h file for more details on configUSE_DISABLE_TICK_AUTO_CORRECTION_DEBUG */
        TickType_t diff;
        diff = (systick_counter - xTaskGetTickCount()) & portNRF_RTC_MAXTICKS;

        /* At most 1 step if scheduler is suspended - the xTaskIncrementTick
         * would return the tick state from the moment when suspend function was called. */
        if ((diff > 1) && (xTaskGetSchedulerState() != taskSCHEDULER_RUNNING))
        {
            diff = 1;
        }
        while ((diff--) > 0)
        {
            switch_req |= xTaskIncrementTick();
        }
    }
    else
    {
        switch_req = xTaskIncrementTick();
    }

    /* Increment the RTOS tick as usual which checks if there is a need for rescheduling */
    if ( switch_req != pdFALSE )
    {
        /* A context switch is required.  Context switching is performed in
        the PendSV interrupt.  Pend the PendSV interrupt. */
        SCB->ICSR = SCB_ICSR_PENDSVSET_Msk;
    }

    portCLEAR_INTERRUPT_MASK_FROM_ISR( isrstate );
}

/*
 * Setup the RTC time to generate the tick interrupts at the required
 * frequency.
 */
void vPortSetupTimerInterrupt( void )
{
    /* Request LF clock */
    nrf_drv_clock_lfclk_request(NULL);

    /* Configure SysTick to interrupt at the requested rate. */
    nrf_rtc_prescaler_set(portNRF_RTC_REG, portNRF_RTC_PRESCALER);
    nrf_rtc_int_enable   (portNRF_RTC_REG, RTC_INTENSET_TICK_Msk);
    nrf_rtc_task_trigger (portNRF_RTC_REG, NRF_RTC_TASK_CLEAR);
    nrf_rtc_task_trigger (portNRF_RTC_REG, NRF_RTC_TASK_START);
    nrf_rtc_event_enable(portNRF_RTC_REG, RTC_EVTEN_OVRFLW_Msk);

    NVIC_SetPriority(portNRF_RTC_IRQn, configKERNEL_INTERRUPT_PRIORITY);
    NVIC_EnableIRQ(portNRF_RTC_IRQn);
}

#if configUSE_TICKLESS_IDLE == 1


#ifdef SOFTDEVICE_PRESENT

static inline void waitCpuCoreEvent()
{
#ifdef TRACE_LOW_POWER
    /* When tracing entry/exit to WFE MCU sleep mode use an additional critical section.
     * Interrupts are normally enabled when MCU is sleeping so interrupt handlers run immediately
     * on wake from WFE.  Without an additional critical section the interrupt handler that woke
     * MCU would have already run before the trace pin is changed resulting in an inaccurate
     * representation of the time the MCU spends asleep.
     */
    CRITICAL_REGION_ENTER();
    traceLOW_POWER_WFE_BEGIN();
#endif
    if (nrf_sdh_is_enabled()) {
        uint32_t err_code = sd_app_evt_wait();
        APP_ERROR_CHECK(err_code);
    }
    else {
        __WFE();
    }
#ifdef TRACE_LOW_POWER
    traceLOW_POWER_WFE_END();
    CRITICAL_REGION_EXIT();
#endif
}

#else

static inline void waitCpuCoreEvent()
{
    __WFE();
}

#endif

#if __FPU_PRESENT
    #define PWR_MGMT_FPU_SLEEP_PREPARE()     pwr_mgmt_fpu_sleep_prepare()

     __STATIC_INLINE void pwr_mgmt_fpu_sleep_prepare(void)
     {
        uint32_t fpscr;
        CRITICAL_REGION_ENTER();
        fpscr = __get_FPSCR();
        /*
         * Clear FPU exceptions.
         * Without this step, the FPU interrupt is marked as pending,
         * preventing system from sleeping. Exceptions cleared:
         * - IOC - Invalid Operation cumulative exception bit.
         * - DZC - Division by Zero cumulative exception bit.
         * - OFC - Overflow cumulative exception bit.
         * - UFC - Underflow cumulative exception bit.
         * - IXC - Inexact cumulative exception bit.
         * - IDC - Input Denormal cumulative exception bit.
         */
        __set_FPSCR(fpscr & ~0x9Fu);
        __DMB();
        NVIC_ClearPendingIRQ(FPU_IRQn);
        CRITICAL_REGION_EXIT();
     }
#else
    #define PWR_MGMT_FPU_SLEEP_PREPARE()
#endif


void vPortSuppressTicksAndSleep( TickType_t xExpectedIdleTime )
{
    /* Upon entry the scheduler is suspended, RTC is still generating tick interrupts. */

    /* Choose a maximum idle time much less than available maximum to allow for safe RTC counter
     * bounds checking after wakeup from sleep.
     */
    TickType_t maxIdleTime = portNRF_RTC_MAXTICKS / 2 - configEXPECTED_IDLE_TIME_BEFORE_SLEEP;
    if ( xExpectedIdleTime > maxIdleTime )
    {
        xExpectedIdleTime = maxIdleTime;
    }


    /* Block RTC interrupts only.  All other interrupt remain active so that they
     * can execute directly after WFE and possibly schedule RTOS tasks.
     */
#ifdef SOFTDEVICE_PRESENT
    do{
        uint32_t err_code = sd_nvic_DisableIRQ(portNRF_RTC_IRQn);
        APP_ERROR_CHECK(err_code);
    }while (0);
#else
    NVIC_DisableIRQ(portNRF_RTC_IRQn);
#endif

    /*
     * Implementation note:
     *
     * To help debugging the option configUSE_TICKLESS_IDLE_SIMPLE_DEBUG was presented.
     * This option would make sure that even if program execution was stopped inside
     * this function no more than expected number of ticks would be skipped.
     *
     * Normally RTC works all the time even if firmware execution was stopped
     * and that may lead to skipping too much of ticks.
     */
    TickType_t enterTime;
    enterTime = nrf_rtc_counter_get(portNRF_RTC_REG);


    if ( eTaskConfirmSleepModeStatus() != eAbortSleep )
    {
        TickType_t xModifiableIdleTime;
        TickType_t wakeupTime = (enterTime + xExpectedIdleTime) & portNRF_RTC_MAXTICKS;

        /* Stop tick events */
        nrf_rtc_int_disable(portNRF_RTC_REG, NRF_RTC_INT_TICK_MASK);

        /* Configure CTC interrupt */
        nrf_rtc_cc_set(portNRF_RTC_REG, 0, wakeupTime);
        nrf_rtc_event_clear(portNRF_RTC_REG, NRF_RTC_EVENT_COMPARE_0);
        nrf_rtc_int_enable(portNRF_RTC_REG, NRF_RTC_INT_COMPARE0_MASK);

        /* Ensure NVIC->ISPR[RTC] (Pending interrupt) for RTC is clear before entering wait loop.
         * Exit from WFE relies on generation of an event from NVIC->ISPR[RTC] transitioning from
         * 0->1 as a result of tickless timeout coming from RTC COMPARE0 interrupt event.
         */
#ifdef SOFTDEVICE_PRESENT
        {
            uint32_t err_code = sd_nvic_ClearPendingIRQ(portNRF_RTC_IRQn);
            APP_ERROR_CHECK(err_code);
        }
#else
        NVIC_ClearPendingIRQ(portNRF_RTC_IRQn);
#endif
        __DSB();

        /* 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 )
        {
            {

#if PROFILE_TICKLESS_IDLE == 1
                uint32_t loopCounter = 0;
#endif

                while (1) {
                    uint32_t currentTime;

#if PROFILE_TICKLESS_IDLE == 1
                    ++profile_tickless_g.n_idle_loops;
#endif

                    /* Clear prior pending CPU events to ensure that sd_app_evt_wait() will wait
                     * on useful wakeup event rather than wakeup immediately due to events which
                     * have occurred since we last went to sleep.
                     * The only reasons to exit from idle are:
                     *  - There is a task scheduled to run
                     *  - The expected idle time has elapsed
                     */
                    __SEV();
                    __WFE();

                    if (eTaskConfirmSleepModeStatus() == eAbortSleep) {
#if PROFILE_TICKLESS_IDLE == 1
                        if (loopCounter < ARRAY_SIZE(profile_tickless_g.n_sched_idle_wakeup_loop)) {
                            ++profile_tickless_g.n_sched_idle_wakeup_loop[loopCounter];
                        }
                        ++profile_tickless_g.n_sched_idle_wakeup;
#endif
                        break;
                    }

                    currentTime = nrf_rtc_counter_get(portNRF_RTC_REG);

                    /* Is current RTC count outside of the window that CPU is expected to sleep?
                     * Account for case where RTC wakeup time may overflow.
                     */
                    if (wakeupTime < enterTime) {
                        if (currentTime >= wakeupTime && currentTime < enterTime) {
#if PROFILE_TICKLESS_IDLE == 1
                            if (loopCounter < ARRAY_SIZE(profile_tickless_g.n_rtc_idle_wakeup_loop)) {
                                ++profile_tickless_g.n_rtc_idle_wakeup_loop[loopCounter];
                            }
                            ++profile_tickless_g.n_rtc_idle_wakeup;
#endif
                            break;
                        }
                    }
                    else {
                        if (currentTime < enterTime || currentTime >= wakeupTime) {
#if PROFILE_TICKLESS_IDLE == 1
                            if (loopCounter < ARRAY_SIZE(profile_tickless_g.n_rtc_idle_wakeup_loop)) {
                                ++profile_tickless_g.n_rtc_idle_wakeup_loop[loopCounter];
                            }
                            ++profile_tickless_g.n_rtc_idle_wakeup;
#endif
                            break;
                        }
                    }

#if PROFILE_TICKLESS_IDLE == 1
                    ++profile_tickless_g.n_sd_app_evt_wait;
                    if (loopCounter != 0) {
                        ++profile_tickless_g.n_cpu_wake_no_rtc_or_sched;
                    }
#endif
                    PWR_MGMT_FPU_SLEEP_PREPARE();
                    waitCpuCoreEvent();

#if PROFILE_TICKLESS_IDLE == 1
                    ++loopCounter;
#endif
                }
            }
        }
        configPOST_SLEEP_PROCESSING( xExpectedIdleTime );

        nrf_rtc_int_disable(portNRF_RTC_REG, NRF_RTC_INT_COMPARE0_MASK);
        nrf_rtc_event_clear(portNRF_RTC_REG, NRF_RTC_EVENT_COMPARE_0);

        /* Correct the system ticks */
        {
            TickType_t diff;
            TickType_t exitTime;

            nrf_rtc_event_clear(portNRF_RTC_REG, NRF_RTC_EVENT_TICK);
            nrf_rtc_int_enable (portNRF_RTC_REG, NRF_RTC_INT_TICK_MASK);

            exitTime = nrf_rtc_counter_get(portNRF_RTC_REG);
            diff =  (exitTime - enterTime) & portNRF_RTC_MAXTICKS;

            /* It is important that we clear pending here so that our corrections are latest and in sync with tick_interrupt handler */
#ifdef SOFTDEVICE_PRESENT
            {
              uint32_t err_code = sd_nvic_ClearPendingIRQ(portNRF_RTC_IRQn);
              APP_ERROR_CHECK(err_code);
            }
#else
            NVIC_ClearPendingIRQ(portNRF_RTC_IRQn);
#endif

            if ((configUSE_TICKLESS_IDLE_SIMPLE_DEBUG) && (diff > xExpectedIdleTime))
            {
                diff = xExpectedIdleTime;
            }

            if (diff > 0)
            {
                vTaskStepTick(diff);
            }
        }
    }

#ifdef SOFTDEVICE_PRESENT
    uint32_t err_code = sd_nvic_EnableIRQ(portNRF_RTC_IRQn);
    APP_ERROR_CHECK(err_code);
#else
    NVIC_EnableIRQ(portNRF_RTC_IRQn);
#endif
}

#endif // configUSE_TICKLESS_IDLE

#else // configTICK_SOURCE
    #error  Unsupported configTICK_SOURCE value
#endif // configTICK_SOURCE == FREERTOS_USE_SYSTICK

  • Sorry for nesting this under another comment., but due to the age of this ticket, I'm forced to reply to a comment.

    For any future consumers of 's solution, which I do find to be quite nice, there are a few things to watch out for.

    1. In ISRs that wake the cpu from sleep, the tick count will be stale, meaning any FreeRTOS call that relies on the tick value will be inaccurate. I found out the hard way that my software timer started from an ISR was inaccurate because the RTOS tick count isn't updated until after the ISR completes. Austin hints at this in the description, but I thought it would be useful to explicitly call out.
    2. While it is common practice to minimize time in ISRs, there is a race condition with the RTC tick and ISR completion when waking from sleep. If sleep is exited due to the RTC timeout and then an interrupt occurs prior to reading the RTC counter for performing the vTaskStepTick(diff) operation, if the interrupt's ISR runs long enough for another RTC tick to occur, then the tick correction's diff value will exceed 
      xExpectedIdleTime. If configUSE_TICKLESS_IDLE_SIMPLE_DEBUG is defined, a tick will be lost. If not defined, vTaskStepTick will assert if enabled.
Related