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)?