Does Blink Without Delay Eat Electricity

RE: Program Blink Without Delay

There are some things I feel are poorly explained or clarified for novices and which I would like to open up to your comments...
Considering the 2 loop types blink standard versus blink without delay...
Instead of blinking let's assume some other more useful function e.g. reading a sensor and posting results e.g. over serial or WiFi. Let's assume a loop period of say 2.5 minutes AND let's assume you aspire to battery power / energy efficiency.
Is it fair to state the circuit will use MUCH more power churning through a loop making tests of whether it's time yet, rather than just sitting in a delay statement? Why is this never revealed in posts?
What are the implications for migrating your sketch loop onwards towards OTA updates?
If the frequency of the loop iterations is critical but the body of the work done in the loop is significantly time variable, one method gives accurate looping frequency and one does not?
There must be many other differences too.
What do you think?

if you are on a regular arduino (UNO like) then delay is an active wait so you just eat up power in the same way. On other architectures like a ESP32 it is a sleep so you would actually save a bit and not trigger the watchdog

That sort of comment input is VERY helpful and not at all intuitive for a novice. My assessment with an ESP32 (and no super accurate means of measurement) is 80mA with loop() churning versus 10mA sat in a delay(). I have seen no discussion around this aspect before despite being intrigued by the potential differences.

Don´t hijack threads.

Open new thread to have a common discussion.

Is it fair to state the circuit will use MUCH more power churning through a loop making tests of whether it's time yet, rather than just sitting in a delay statement? Why is this never revealed in posts?

No it’s not fair to think this.

In general, the controller is always doing something.

Why would anyone think one piece of executing code consume different power than another piece of executing code.
:thinking:

Best to raise that with Espressif then, its they that produce the ESP32 Core for the Arduino IDE and its examples, not Arduino.

I reckon it's because of how PC/laptop processors behave. They change their clock speeds, put whole processor cores, or parts of processor cores, into low power states when workload is low, and ramp everything back up again when workload is high.

Beginners imagine that microprocessors have similar capabilities, because that's a model they are familiar with (while probably taking for granted all the complexity that goes into achieving it in PC processors).

since ESP32 runs the arduino core on top of FreeRTOS, the "delay()" function calls the FreeRTOS vTaskDelay(). Among other things, that makes sure that the wireless processes continue to run, and it may (apparently does) switch the CPU to lower power modes if it doesn't have anything else that needs done.
It will likely depend on exactly what sorts of things are going on, and how ESP has configured FreeRTOS, and... who knows what else...

But it's not documented as a feature of delay(), and it's not "typical" behavior.

What do you think the CPU is doing during those times?

In AVR processors the use of millis() instead of delay() gives no current saving. What it does give is the possibility to process other statements while the time is running.
One thing: if your program is very very large, it may take more than 1 millisecond before it passes the time-bit again, in which case the expected action may be a little late.

The only time the CPU is going to use less power is if you put it into a low-power state. Depending on which low-power state you are using the power-savings are different and the functions that the board isn't doing are different. For example, when you are not actively sending data over Wifi, the ESP32 will automatically power down the radio. No sense in keeping it at high-power for nothing, right? So, you will see that the chip as a whole will consume more power when actively using WiFi than when it isn't.

But as far as the cores go, they are either running or completely asleep. If you haven't put them to sleep, they are running and using a delay does not put them to sleep.

But if you only have one task running, it doesn't really make a difference. It's still going to be completely blocking. The only time vTaskDelay ends up helping you is if you have multiple tasks running and there is something for the CPU to do while it is waiting to take up that waiting task again. Even more powerful is vTaskDelayUntil for iterative functions (while or for loops).

I've done some measurements of the current using 'blink' and 'blink without delay' on an Arduino Uno.

Here are the results:
First 'Blink'

and then 'Blink Without Delay'

'Blink' mean current: 42.473mA
'Blink Without Delay' mean current: 42.364mA

'tis a wee bit more complex...

  • ESP32 has 2 cores, core_0 and core_1
  • core_0 handles all the RF protocols and services
  • core_1 handles the Arduino user and IDF wrappers for Arduino core.
  • FreeRTOS as a binary file is linked into all Arduino ESP32 projects

So, if you yield() the Arduino core_1, FreeRTOS will service core_0 threads. It is the RF side of things that eat current during transmit; thus stalling core_1 will save on Arduino processing but do nothing for the RF WiFi/BT side of stuff.

Wouldn't that have to mean that in one the LED is on ever so slightly longer?

Can you measure the duty cycle of both methods with enough precision to test that?

I don't see how it can be different otherwise. See @LarryD #5 above.

a7

More likely, the slight difference is due to sampling and averaging error. The waveforms shown are not the same, and the detailed differences probably should not be taken very seriously.

If one really needs to do some wait time with the ESP32 and during the wait time shut down the main CPU, the one with 2 cores, to save power then use the ULP, the other processor included with the ESP32 to run things while the main processor is shutdown. Note not sleep but shutdown.

The 2nd processor included with the ESP32 called the ULP (Ultra Low Power) can do quite a bit with the main processor operating or with the main processor sleeping or shut down.

Because it is a non-issue from Imaginaryland.

My Uno cpu runs at 16 million cycles per second no matter what it does. What uses more power than that are OUTPUT mode sourcing and sinking of current to run things.

What delay does is watch a timer with total code fixation, Nothing Else Runs Until It Is Finished. There are quite a few commands that force a wait... and during that wait every cycle is wasted.

example uses a led that you don't have to wire since Serial Monitor gets the log. Just run it, it stops itself.

/*
  DelayBlink for expierienced users of delay(). 
  Turns on an LED on for one second, then off for one second, repeatedly.
  Added to tha, blink another led for a short time repeatedly and
  gemerate a time-stamped log of LED actions, hit Enter to stop/start.
  With the log there is no real need to wire in the extra LED.
 
  This example code is in the public domain.
 */
 
// Pin 13 has an LED connected on most Arduino boards.
// give it a name:
byte led = 13;
byte led2 = 7; 

byte loops = 0;
byte limitLoopsTo = 5; // stop scrolling forever

// the setup routine runs once when you press reset:
void setup() 
{   
  Serial.begin( 115200 );  // Serial Monitor must match.
  Serial.println( F( "\n\n**** DelayBlink demo. ****" )); // '\n' is newline char
  Serial.println( F( "Log times are milliseconds since start." )); 
  Serial.println( F( "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" )); 
  

  // initialize the digital pins as outputs.
  pinMode(led, OUTPUT);     
  pinMode(led2, OUTPUT); // if phase 2 is used     
}

// the loop routine runs over and over again forever:
void loop() 
{
  loops++; // loop count
  
  digitalWrite(led, HIGH);   // turn the LED on (HIGH is the voltage level)
  Serial.print( millis()); 
  Serial.println( F( " LED 13 ON" )); 
  delay(1000);               // wait for a second
  digitalWrite(led, LOW);    // turn the LED off by making the voltage LOW
  Serial.print( millis()); 
  Serial.println( F( " LED 13 OFF" )); 
  delay(1000);               // wait for a second

//  you have 2 leds on different pins  
  digitalWrite(led2, HIGH);   // turn the LED on (HIGH is the voltage level)
  Serial.print( millis()); 
  Serial.println( F( " LED 7 ON" )); 
  delay(200);               // wait for 0.2 second
  digitalWrite(led2, LOW);    // turn the LED off by making the voltage LOW
  Serial.print( millis()); 
  Serial.println( F( " LED 7 OFF" )); 
  delay(200);               // wait for 0.2 second
//  what happens?
  if ( loops == limitLoopsTo )
  {
    while( 1 ); // always true, locked loop can be reset
  }
}

Cut&Paste the log works but just note how the short blink only follows a long one.
Execution with delay is serial.

Execution without delay is parallel.

/*
  Modified Arduino Blink Converted to NoDelayBlink
  Blinks two leds at different rates at the same time.
  This is for after you have practice with delay code.
 
  You may have to see it run to know what the deal is,
  but just in case, 
  it means merged sketched can run independently
  if one thing runs with delays alone it can be made to run withs others.
  And if no task hogs cycles an Uno can do > 100 tasks without stuttering.
 
  This example code is in the public domain.
 */
 
// stop the log at millis to linit length  
const unsigned long demoLength = 12000;  
  
// Pin 13 has an LED connected on most Arduino boards.
// give it a name:
byte statusLedPin = 13; // save a byte, Uno has 2048B RAM!
byte statusLedState = 1; // turns ON in setup
byte addedLedPin = 7; // phase 3
byte addedLedState = 1; // turns ON in setup

unsigned long statusLedStart; // gets set at start
unsigned long addedLedStart; // millis!

unsigned long statusLedInterval = 1000; // millis!
unsigned long addedLedInterval = 200; // millis!

void BlinkStatusLight() // shows normal operation
{
  if ( millis() - statusLedStart >= statusLedInterval )
  {
    statusLedState = !statusLedState; // flips 0/1
    digitalWrite(statusLedPin, statusLedState); // turn the LED on (HIGH is the voltage level)
    statusLedStart += statusLedInterval; // self-correcting

    Serial.print( millis()); 
    Serial.print( F( " LED " )); 
    Serial.print( statusLedPin );
    Serial.write( ' ' ); 
    if ( statusLedState == 0 )
    {
      Serial.println( F( "OFF" )); 
    }
    else
    {
      Serial.println( F( "ON" )); 
    }
  } 
} // end of BlinkStatusLight(), it toggles a pin on time 

// blinking the added led is really the same as above.
// pnly 1 task can do both BUT
// blinking a led is a filler for Output in general
// this demo is to show independent tasks running.
void BlinkAddedLed() // shows normal operation
{
  if ( millis() - addedLedStart >= addedLedInterval )
  {
    addedLedState = !addedLedState; // flips 0/1
    digitalWrite(addedLedPin, addedLedState); // turn the LED on (HIGH is the voltage level)
    addedLedStart += addedLedInterval; // self-correcting

    Serial.print( millis()); 
    Serial.print( F( " LED " )); 
    Serial.print( addedLedPin );
    Serial.write( ' ' ); 
    if ( addedLedState == 0 )
    {
      Serial.println( F( "OFF" )); 
    }
    else
    {
      Serial.println( F( "ON" )); 
    }
  } 
} // end of BlinkAddedStatusStatusLight(), it toggles a pin on time 


// One more task that measures the void loop average.
// First time was a shock, loop averaged 20 micros.
// It made 67 time checks per timer before 1 ms.
// LoopCounter  --  lets you know how often void loop() ran last second.
// LoopCounter value
#define microsInOneSecond 1000000UL


void LoopCounter() // tells the void loop runs every second
{ // if you add code, this will show you the load
  // it is a load, converting longs to decimal text
  // takes a lot of cycles but only once/second.
  
  // When you are finished with developing,
  // comment out the void loop call to the counter
  // and your finished code will have less ;atency. 
  
  // inside a function, static variables keep their value from run to run
  static unsigned long count, countStartMicros; // only this function sees these

  count++; // adds 1 to count after any use in an expression, here it just adds 1.
  if ( micros() - countStartMicros >= microsInOneSecond ) // 1 second
  {
    countStartMicros += microsInOneSecond; // for a regular second
    Serial.print( F( "\nLC " ));
    Serial.println( count ); // 32-bit binary into decimal text = many micros
    count = 0; // don't forget to reset the counter 
  }
} // end of LoopCounter(), it shows void loop runs every second.


//  ============= VOID SETUP ==============
void setup() 
{ // easier for old eyes to match aligned braces  

  Serial.begin( 115200 ); // empty the buffer faster!
  // set your Serial Monitor baud rate to match!  
  Serial.println( F( "\n\n**** NoDelayBlink demo. ****" )); 
  Serial.println( F( "Log times are milliseconds since startup." )); 
  Serial.println( F( "Demo limited to 12 seconds originally." )); 
  Serial.println( F( "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" )); 

  // initialize the digital pin as an output.
  pinMode(statusLedPin, OUTPUT); 
  pinMode(addedLedPin, OUTPUT); 
 
  // leds ON, waiting to turn OFF unless I want else.
  digitalWrite(statusLedPin, HIGH); // turn the LED on (HIGH is the voltage level)
  digitalWrite(addedLedPin, HIGH); // turn the LED on (HIGH is the voltage level)
} // the vertically alighned closing brace


//  ============= VOID LOOP ==============
void loop()  // the driving wheel 
{  
  // these tasks will appear to run in parallel
  LoopCounter();
  BlinkStatusLight();
  BlinkAddedLed();
  // every time loop runs they all check the time
  
  if ( millis() >= demoLength )
  {
    while( 1 ); // no escape 
  }
}

This is how you can write tasks that run independent of each other.

I watch a button with one task that maintains 8 bits of pin state history that other tasks can read to know when a state change and debounce just happened by if ( history == 128 ) { code for button press }.
It's tight which runs in fewer cycles, less electricity.

Writing separate tasks reduces your indent levels, makes the logic easier to follow and debug. You can work with a task in one simple sketch in order to include it in a larger sketch and save in your own task toolbox.

Non-blocking code is VERY mix and match.

Well, actually...
The most common ESP32 has two Xtensa processors AND the ULP.
But some ESP32s only have a single main CPU, and some have a RISC-V CPU instead of Xtensa, and some have both a high performance RISC-V and a low-power RISC-V CPU.

I have no idea whether the Arduino/FreeRTOS combination makes any use of the ULP.
12ta's measured power differences (10mA vs 80ma) don't seem big enough for the lowest-power modes available (10 to 150 uA reported...)

freeRTOS is not used to program the ULP. Macro Assembler is used to program the ULP, fun stuff.

Because a blocking delay(xxx ms) is so obviously a candidate action for a very low power CPU state, and it what I think my ESP32 test showed. The reduction appeared significant 80mA v 10mA.

I am not trying to diminish the clever 'multitasking' ability of the non blocking no delay() code examples. It's just that for my sensor monitoring with a 2.5 minute period, there is no need for it, but battery power is the aim.