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

vTaskDelay on nRF52 FreeRTOS port wastes CPU/power

While doing power profiling on Adafruit's nRF52 Arduino port, I have noticed that when using delay(500), the CPU power consumption goes up about 1ms before actually returning from delay() call (see https://github.com/adafruit/Adafruit_nRF52_Arduino/issues/537 for details).

Since their code is based on your FreeRTOS port, I have (successfully) tried to reproduce the issue on bare Nordic SDK by using a slightly modified examples/peripheral/blinky_freertos sample: removed timer, only used the led_toggle_task_function (akin to Arduino loop() {} paradigm) which uses vTaskDelay().

My setup is following:

  • Linux, arm-none-eabi-gcc-7.2.1
  • nRF5_SDK_16.0.0_98a08e2
  • stock pca10059 with p0.22 wired to a logan
  • modified blinky_freertos to compile for pca10059 with mbr
    • also modified to make p0.22 output and to toggle it inside led_toggle_task_function() instead of the LED
    • Experimented with various TASK_DELAY constants
  • LiPo with 10R current sense resistor wired to a standalone USB socket to measure the current

Observations (with the FreeRTOS port from the SDK):

  • at 0ms delay, p0.22 toggles every 3us, the power consumption is ~5.7mA (expected, busy loop)
  • at 1ms delay, p0.22 toggles every about every 970us, the power consumption is ~6.6mA (still expected, the configEXPECTED_IDLE_TIME_BEFORE_SLEEP is set to 2)
  • at 2ms delay, p0.22 toggles every 2.93ms, consumption is 2.42mA - unexpected - delay is 1ms too long, consumption is way too high given that the CPU should (and does) enter sleep and does pretty much nothing else
  • at 5ms delay, p0.22 rate is 5.86ms, consumption is still 1.22mA (i.e. the CPU must be active about 20% of the time)
  • at 200ms delay (for reference, to rule out other effects), consumption is down to about 60uA

For the periodic 500ms wakeup, the extra power wasted in this 1ms between wakeup and resume of my code represents about 10uA of extra drain.

I have debugged this issue into the scheduler and here are my findings:

  • vTaskDelay() will ultimately cause the idle task to enter sleep.
  • after the CPU wakes up (by RTC), it spends almost 1ms spinning inside portTASK_FUNCTION()
  • it's actually the next RTC tick that makes my task runnable again 1ms later

Upon further inspection of FreeRTOS, I have realized that normal timer ticks (that would unblock my waiting task) call xTaskIncrementTick(), which has all the rescheduling logic built in, while after sleep, vTaskStepTick() is called instead. vTaskStepTick updates the time, but doesn't care about task queues, so the idle task has to spin till the next timer tick to wake up my task.

Without enough understanding of the FreeRTOS internals, I have modified (read: hacked) vTaskStepTick() to read:

void vTaskStepTick( const TickType_t xTicksToJump )
{
    /* Correct the tick count value after a period during which the tick
    was suppressed.  Note this does *not* call the tick hook function for
    each stepped tick. */
    configASSERT( ( xTickCount + xTicksToJump ) <= xNextTaskUnblockTime );
#if 1 // PN
    xTickCount += xTicksToJump - 1;
    xTaskIncrementTick();
#else
    xTickCount += xTicksToJump;
#endif
    traceINCREASE_TICK_COUNT( xTicksToJump );
}

This causes the normal timer tick handling to proceed immediately after wakeup (discounting that one tick it will perform from the step update amount).

With FreeRTOS modified like that, I am getting much better behavior:

  • 0ms: no change, 3us tick, 5.7mA consumption
  • 1ms: no change, 0.97ms, 6.6mA
  • 2ms: 1.97ms, 350uA (yay!)
  • 4ms: 3.91ms, 180uA (cool!)
  • 20ms: about 60uA

Obviously, my change is but a hack and not a proper FreeRTOS fix. Perhaps a better fix would be implemented inside the port code (vPortSuppressTicksAndSleep)?

  • Hi Petr,

    Very impressive homework and great to see how you attempted to attack the problem. 

    Upon further inspection of FreeRTOS, I have realized that normal timer ticks (that would unblock my waiting task) call xTaskIncrementTick(), which has all the rescheduling logic built in, while after sleep, vTaskStepTick() is called instead. vTaskStepTick updates the time, but doesn't care about task queues, so the idle task has to spin till the next timer tick to wake up my task.

    I think you have found a side effect in the change of the logic from xTaskIncrementTick to vTaskStepTick in the idle wakeup in the code. I see your point very clearly and thanks for taking the time for this clear description. But I do not think that this is a bug, but rather a side effect of something else i tried to fix.

    After reading your symptoms, I seem to have introduced a side effect in an attempt to fix something.

    I would rather suggest you a different fix in the portable files of FreeRTOS.

    In portcmsis_systick.c you can change

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

    to 

    BaseType_t switch_req = pdFALSE;
    
    if (diff > 1)
    {
        vTaskStepTick(diff - 1);
        switch_req = xTaskIncrementTick();
    }
    else if (diff == 1)
    {
        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;
        __SEV();
    }

    This should give you the same results as you got with your fixes, its just that my suggestions have changes in the port files rather than hacking the RTOS files.

    Please let me know if this makes sense to you.

  • I am creating an internal ticket to fix our port. Thanks again for your thoughts and contributions

  • Thank you for quick feedback.

    Yes, I was thinking about moving the change into the port for more forward compatibility, though I think other FreeRTOS tickless ports might be running into the same issue. Perhaps other FreeRTOS use cases aren't as power sensitive...

    Either way, looking forward to the official fix!

    I have a followup question: With the fix and a minimal "blinky" example, I have verified that even for 2ms delay, the CPU is only active about 20us (from leaving _WFE to entering it again in the vPortSuppressTicksAndSleep()), yet I see about 1ms of slightly elevated supply current tail after entering the _WFE. My guess from the scope trace (I don't have the PPK) is about 300uA of battery current (quite immaterial, <1uA average current impact on 2Hz wakeups).

    What would be the source of that load? Is the oscillator kept running a little longer? Or maybe DC/DC?

  • Sorry for the late reply Petr,

    I was on summer holiday for two weeks and we were very few people in Norway office.

    I am not sure about where that load is coming from,I am not seeing this with the PPK, Can you please upload a snapshot of the elevated power spikes after entering __WFE()

Related