Problems Using millis()

Hi. I'm trying to implement a 50 ms delay in an interrupt function using millis(). My code compiles okay, but execution apparently locks up as soon as the interrupt function calls delayMillis() (below) using "delayMillis(50UL);"

void delayMillis(unsigned long milliseconds)
{
  unsigned long currentTime = millis();
  unsigned long goalTime = currentTime + milliseconds;

  while (millis() <= goalTime);
}

If I comment out the delayMillis() call, the interrupt function appears to work (but without the 50 ms delay).

If I attempt to use the Serial Monitor to display delayMillis() variable values, the letters "cu" are displayed when the function is called (no idea what that means).

Any advice would be appreciated. Thanks!

In the Arduino IDE, use Ctrl T or CMD T to format your code then copy the complete sketch.

Use the </> icon from the ‘reply menu’ to attach the copied sketch.


while (millis() <= goalTime);


Tell us how this differs from delay(X) ?

My understanding is that delay() doesn't work inside interrupt functions. Not true?

Because

... millis() relies on interrupts to count, so it will never increment inside an ISR. Since delay() requires interrupts to work, it will not work if called inside an ISR. micros() works initially, but will start behaving erratically after 1-2 ms. ...

a7

Sorry, but as suggested, In your example, you’ve broken almost every rule of tight, efficient coding…

ISRs should execute as quickly as possible…
Blocking functions of any type should be avoided in the runtime part of your program…

Thanks for your helpful reply, alto777. I'm a bit perplexed though, because after replacing my "delayMillis(50UL)" function call with "delay(50)", my program now seems to yield the expected result.

Yeah, so… that's why we need to see a complete sketch as requested previously.

I am sure you are seeing what you are seeing, but I am not. :expressionless:

a7

you haven't described your complete project.

Writing code,
where executing the code inside of an interrupt service-routine takes more than 1,0 milliseconds (you want 50 ! milliseconds is likely to not work as you are expecting.
If this is really the case can only be distinguished if you post your complete sketch and a detailed project-description.
otherwise you are in danger of

details versus overview:
We are talking about details. Please give an overview over your whole project.
in mimimum 70% of all cases knowing the whole thing offers completely different and much better working solutions.

This is like

here is an analogon that shall show what can happen if just ask for details:

Newbee: "I want to do better cutting please help me sharpening. "
Expert: Sure I can help you. What kind of cutting-tool are you using?
(expert asks back for details the newcomer left out slowing down getting a solution)

Newbee: a scissor.
Expert: OK take this sharpening tool
Newbee: Yea works great Next question How can I make it cut faster I need to finish faster.
expert: Motorised scissors.
newbee Yea works great though still not fast enough.

expert: Ok can you give an overview about what you are cutting.
newbee: the green of a football-arena.
expert: Oha! take a big mowing tractor with a seven boom spindel-mower and GPS-steering

In the beginning the newbee always just told details.
The expert was assuming the newbee knows that his basic approach is well suited.
which turns out to be very bad suited
that's the reason why it is always a good idea to give an overview and to explain what shall happen in the end.

best regards Stefan

StefanL38 & Co.,

Thanks for the explanation. Thanks to you guys, I now understand why millis() can't be used in an ISR. As requested, I'll provide some sketch background which I hope will help this community explain why delay(50) seems to work in my Int0 ISR.

This is a Halloween prop project which uses two Unos. The first reads a Panasonic Grid EYE array device to steer eye-pointing servos toward whoever might be walking by. It also connects to the Int0 pin of the second Uno... which primarily operates the eyelid blinking servos (adapted much-appreciated code from John Strope), a jaw servo, and an MP3 sound board. Since I didn't have enough digital pins available on the second Uno, the first one's two discretes - which select one of four growl sounds - are connected to A0 and A1 of the second Uno - which regenerates the original binary levels. When the first Uno "sees" motion, it outputs the two sound code selection discretes and interrupts the second Uno to output a 50 ms low pulse on one of its four (K1, K2, K3, and K4) MP3 sound playback triggers.

More than one person has stated that delay() doesn't work in ISRs, so I'm trying to understand why I'm perceiving evidence to the contrary.

Thanks to all for your help... and for your coaching to avoid blocking code in ISRs.

#include<Servo.h>

#define topLidsShut  1883        // 1.88 mS
#define topLidsOpen  1233        // 1.23 mS
#define botLidsShut  1978        // 1.98 mS
#define botLidsOpen  1368        // 1.37 mS

int spd = 2;                     // Speed can range from 0(fast) => 15(slow)
int gap = 100;                   // Delay between movements in milliseconds

const int topLidsPin = 10;       // Top lids servo output is on Pin #10
const int botLidsPin = 9;        // Bottom lids servo ouptu is on Pin #9

const int K1 = 12;               // Assign pin numbers to FN-BC04 MP3 Kn inputs
const int K2 = 4;
const int K3 = 7;
const int K4 = 8;

// Interrupt Variables
volatile int gReq0 = 0;             // Growl Request 0 connected to A0
volatile int gReq1 = 1;             // Growl Request 1 connected to A1
volatile int mp3K0;
volatile int mp3K1;

Servo topLidsServo;              // Name the top lids servo
Servo botLidsServo;              // Name the bottom lids servo

/******************* Initializing PWM and Serial *********************/
void setup() {
  topLidsServo.attach(topLidsPin); // Attach the top lids servo to its pin
  botLidsServo.attach(botLidsPin); // Attach the bottom lids servo to its pin
  pinMode(K1, OUTPUT);
  pinMode(K2, OUTPUT);
  pinMode(K3, OUTPUT);
  pinMode(K4, OUTPUT);
  digitalWrite(K1, HIGH);
  digitalWrite(K2, HIGH);
  digitalWrite(K3, HIGH);
  digitalWrite(K4, HIGH);
  
  attachInterrupt (0, growlRequest, FALLING); // Vector to growlRequest upon falling pin #2
  Serial.begin(9600);              // Initialize the serial monitor for troubleshooting
  Serial.println("Ready to Go!\r");
}

/**************************  Growl Request Interrupt  ****************/
void growlRequest()
{
  mp3K0 = analogRead(gReq0);  // map(analogRead(gReq0), 0, 1023, 0, 1);
  if (mp3K0 > 512)
  {
    mp3K0 = 1;
  }
  else
  {
    mp3K0 = 0;
  }
  mp3K1 = analogRead(gReq1);  // map(analogRead(gReq1), 0, 1023, 0, 1);
  if (mp3K1 > 512)
  {
    mp3K1 = 1;
  }
  else
  {
    mp3K1 = 0;
  }
  if((mp3K0 == 0) && (mp3K1 == 0))
  {
    digitalWrite(K1, LOW);
    delay(50);        // delayMillis(50UL);
    digitalWrite(K1, HIGH);
    // Serial.println("Pulsed K1");
  }
  else if((mp3K0 == 1) && (mp3K1 == 0))
  {
    digitalWrite(K2, LOW);
    delay(50);        // delayMillis(50UL);
    digitalWrite(K2, HIGH);
    // Serial.println("Pulsed K2");
  }
  else if((mp3K0 == 0) && (mp3K1 == 1))
  {
    digitalWrite(K3, LOW);
    delay(50);        // delayMillis(50UL);
    digitalWrite(K3, HIGH);
    // Serial.println("Pulsed K3");
  }
  else
  { digitalWrite(K4, LOW);
    delay(50);        // delayMillis(50UL);
    digitalWrite(K4, HIGH);
    // Serial.println("Pulsed K4");
  }
 
  Serial.print("GR Interrupt, mp3K0 = ");
  Serial.print(mp3K0);
  Serial.print(" mp3K1 = ");
  Serial.println(mp3K1);
}

// ***********************  Main Loop   ******************************/
/* Now we wait for pseudo-random blink intervals=10
*/
void loop() {
  blink(random(15), random(100, 500), random(15));   // See 'blink' function for input info
  delay(random(100,10000));
}

// **********************  Blink function  ****************************
/* Expecting input of (openingSpeed, stopDuration, closingSpeed)
   Speed can range from 0(fast) => 15(slow). 
   
   Ex: inputing (0,0,0) => fastest possible.
   
   Gap is in milliseconds. Ex: (3,1000,3) would be slower with a pause 
   of 1 second.
*/
void blink(int spd1, int gap, int spd2) {
  Serial.println("Blink\r");
  closeEye(spd1);
  delay(gap);       // This is the time between closing and reopening
  openEye(spd2);
}
/*************************  Closing Lids  ****************************/
void closeEye(int spd) {
  for (uint16_t pulseLen1 = topLidsOpen, pulseLen2 = botLidsOpen; 
  pulseLen1 < topLidsShut || pulseLen2 < botLidsShut; pulseLen1 += 10, pulseLen2 += 10) {
    if (pulseLen1 < topLidsShut) {
      topLidsServo.writeMicroseconds(pulseLen1);
    }
    if (pulseLen2 < botLidsShut) {
      botLidsServo.writeMicroseconds(pulseLen2);
    }
    delay(spd);
  }
}
/**************************  Opening Lids  ***************************/
void openEye(int spd) {
  for (uint16_t pulseLen1 = topLidsShut, pulseLen2 = botLidsShut; 
  pulseLen1 > topLidsOpen || pulseLen2 > botLidsOpen; pulseLen1 -= 10, pulseLen2 -= 10) {
    if (pulseLen1 > topLidsOpen) {
      topLidsServo.writeMicroseconds(pulseLen1);
    }
    if (pulseLen2 > botLidsOpen) {
      botLidsServo.writeMicroseconds(pulseLen2);
    }
    delay(spd);
  }
}

depending on what else you want to do in your code staying inside a ISR indeed blocks everything else. I'm not familiar with the real inner details of interrupts on Atmega328 (nor any other chips). There might be a priority that an interrupt of an even higher priority can interrupt a lower-priority interrupt. But usually as long as you are inside an ISR and something else would like to react on an interrupt-event like

  • receiving serial data,
  • signal-change on a I2C-bus
  • signal change on a SPI-bus does not occur.

Effect:
your code is missing signals that normally would invoke an interrupt.
And this is the reason why you should be back from in interrupt as soon as possible
to keep everything else working.
creating servopulses is based on a timer-interrupt. Not sure if this servo-pulse-timer-interrupt gets affected by a long execution-time of your ISR.

This 50 millisecond pulse. Would your device malfunction if the pulse would be 100 millisecons or 250 milliseconds long?
How fast does your void loop()
loop?

if it is fast enough you would simply set a flag inside your ISR and the main-code would check for flag set. if flag is set switch HIGH and store a snapshot of time
if your loop () needs 100 milliseconds anyway just switch off with the next iteration.
else if your loop() loops at 20 milliseconds check
if (millis() - StartOfPulse) >= 50) {
switch-off
}

You have some delays() for blinking inside your loop() and functions do have delay()

If all these delays() get replaced by non-blocking timing which is very easy and the most classical thing for non-blocking timing

I estimate that your loop runs very fast.

just in case I had not yet posted a link to this tutorial

best regards Stefan

From time to time I enjoy writing demo-codes. In this case you are the lucky guy that I took your code as the demo-code.

I have re-written the complete functionlity of your code in a non-blocking manner.
Your code is a really beautiful example how for-loops and delay - which are both blocking -
must be replaced by code that works non-blocking

There is a

fundamental difference

between sequential and blocking coding and non-blocking coding.
It is very important to

understand this difference as the first step.

Without understanding this difference most people try to see a sequential-blocking thing in non-blocking code. Which it really isn't. And trying to press non-blocking-code into a blocking-pattern does not work => Makes it very hard to understand.

I will use an everyday example to explain it.
Imagine cooking an egg. Most people use a short-time alarm-clock for beeing remembered at the right time to take the egg out of the cooking water.

If you don't have a short-time-alarm-clock handy you will take any kind of clock or watch.
heat up water until the water is boiling
put eggs into the water and take a look onto your watch.
Let's say it is 6:23 am. You want your egg smooth which means cooking-time is 4 to 5 minutes.
You start reading the newspaper but around every 30 seconds you take a new look onto your watch for checking how much time has passed by.
Your watch shows 6:25 am ehm when did I put the egg in 6:23 so 2 minutes not yet 4 minutes not yet time to take egg out of the water...

Your watch shows 6:26 am ehm when did I put the egg in 6:23 so 3 minutes not yet 4 minutes not yet time to take egg out of the water...

Your watch shows 6:28 am ehm when did I put the egg in 6:23 uups 5 minutes !
this is more than 4 minutes ! really time to take egg out of the water

You took an often repeated look onto your watch and calculated how much time has passed by.
You compared the result of the calculation with your timelimit cooking-time 4 minutes

You calculated
actualTime - CookingStartTime
and compared the result with a timelimit

if (actualTime - CookingStartTime  >= 4_minutes) {
  take_egg_out_of_the_water();
}

You could have looked onto your watch once every three seconds to get a more precise cooking-time. Though for a smooth egg it doesn't matter if the cooking-time is 4:00 or 4:17

Your microcontroller loves calculations. So give your microcontroller more fun through a very fast repeated calculation in combination with compairing

The basic principle is

void loop() {
  if (actualTime - CookingStartTime  >= 4_minutes) {
    take_egg_out_of_the_water();
  }
  read_some_words_in_the_newsPaper();
  take_a_sip_of_coffee();

checking how much time has passed by and only in case the timelimit is overdue
take action. If this is not yet the case do all the other stuff (almost) in parallel.

This is non-blocking timing

Back to your halloween-project:
Your code uses randomised numbers for times and speed of movements to get a more natural look-alike
But your device still does certain things in a well-defined sequence.
This sequence is:
start closing the eye with a certain speed
if eye is closed wait some time
start opening the eye with a certain speed
if eye is opened wait some time before a new blink of the eye starts

closing / opening the eye is a sub-sequence:

set servo to a start-position
wait a short time

set servo to a minimal proceeded position
wait a short time

repeat this until the eye-is_closed-position is reached

This is done by a for-loop

void closeEye(int spd) {
  for (uint16_t pulseLen1 = topLidsOpen, pulseLen2 = botLidsOpen; 
  pulseLen1 < topLidsShut || pulseLen2 < botLidsShut; pulseLen1 += 10, pulseLen2 += 10) {
    if (pulseLen1 < topLidsShut) {
      topLidsServo.writeMicroseconds(pulseLen1);
    }
    if (pulseLen2 < botLidsShut) {
      botLidsServo.writeMicroseconds(pulseLen2);
    }
    delay(spd);
  }
}

The for-loop is blocking.

remember the sequence

step1: set servo to a start-position
step2: wait a short time
step3: set servo to a minimal proceeded position
if (servo-position is not yet in end-position) do again step2
if (servo-position has reached end-position do step4

step4: set servo to a position
step5: wait a short time
step6: set servo to a minimal proceeded position
if (servo-position is not yet in end-position) do again step5
if (servo-position has reached end-position do step7
step7: wait some time before starting over new
if waitingtime is over do step1

executing the steps one after the other could be coded

if (stepNo == 1) {
set servo to a start-position
}

if (stepNo == 2) {
wait a short time
}

if (stepNo == 3) {
set servo to a minimal proceeded position
if (servo position is not yet in end-position) {
stepNo = 2; // do again step2
}
if (servo - position has reached = end-position) {
stepNo = 4;
}
}

if (stepNo == 4) {
etc. etc.

But there is a variant which is more compact and better-suited if you have a lot of if-conditions that are similar but still different.

This is the switch-case-break-statement
The above functionality sketched as a switch-case-break-statemenet looks like this

  switch (stepNo) {
    case 1:
      set servo to a start - position
      break;


    case 2:
      wait a **short** time
      break;


    case 3:
      set servo to a minimal proceeded position
      if (servo position is not yet in end - position) {
        stepNo = 2; // do again step2
      }
      if (servo - position has reached = end - position) {
        stepNo = 4;
      }


    case 4:
      etc. etc.
      break;
  } // end of switch-case-break-statement

This coding-technique is called a

step-chain

there is a chain of steps that gets executed

The more common but harder to understand term is state-machine
The "machine" runs through different "states" hence state-machine

The big advantage of a step-chain is that you need just a minimum of if-conditions
because the switch-variable does a pre-selection to only execute that part of the code that is of interest in this moment.

This pre-selection has another advantage:
you can let repeat the code a certain step over and over again until some special conditions become true (this is used for the non-blocking timing)

and last but not least this enables to do multiples things (almost) in parallel

void loop() {
  if (actualTime - CookingStartTime  >= 4_minutes) {
    take_egg_out_of_the_water();
  }
  read_some_words_in_the_newsPaper();
  take_a_sip_of_coffee();

because a longer actions like

  • reading an 2 pages long article in the newspaper
  • drinking a big cup of coffee
    are all done in small steps with changing to the next action after a short time

if really all actions are divided into small steps this quickly
jump_in / jump_out enables to proceed with all actions in parallel.

And this is the fundamental difference between sequential coding and non-blocking coding.

If you look into the code after reading all this it will still take some time to re-recognise
"yes this is my code" or at least my functionality

If you have any questions about any detail just ask all your questions

#include<Servo.h>

#define topLidsShut  1883        // 1.88 mS
#define topLidsOpen  1233        // 1.23 mS
#define botLidsShut  1978        // 1.98 mS
#define botLidsOpen  1368        // 1.37 mS

int spd1;                        // speed eye-closing
int spd2;                        // speed eye-opening

int spd = 2;                     // Speed can range from 0(fast) => 15(slow)
int gap = 100;                   // non-blocking Delay between movements in milliseconds
int PauseBetweenBlinks;          // non-blocking delay between blinkcycles

const int topLidsPin = 10;       // Top lids servo output is on Pin #10
const int botLidsPin = 9;        // Bottom lids servo ouptu is on Pin #9

const byte K1 = 12;               // Assign pin numbers to FN-BC04 MP3 Kn inputs
const byte K2 = 4;
const byte K3 = 7;
const byte K4 = 8;

// Interrupt Variables
volatile boolean LowPulseActive = false;

volatile int gReq0 = 0;             // Growl Request 0 connected to A0
volatile int gReq1 = 1;             // Growl Request 1 connected to A1
volatile int mp3K0;
volatile int mp3K1;

Servo topLidsServo;              // Name the top lids servo
Servo botLidsServo;              // Name the bottom lids servo

const byte sc_InitClosing      = 1;
const byte sc_CloseWriteServo  = 2;
const byte sc_CloseWait        = 3;
const byte sc_InitOpening      = 4;
const byte sc_OpenWriteServo   = 5;
const byte sc_OpenWait         = 6;
const byte sc_WaitGapTime      = 7;
const byte sc_WaitForNextBlink = 8;

byte BlinkStepNo = sc_InitClosing;

uint16_t pulseLen1;
uint16_t pulseLen2;

unsigned long WaitingTimer;
unsigned long LowPulseTimer;

// easy to use helper-function for non-blocking timing
boolean TimePeriodIsOver (unsigned long &startOfPeriod, unsigned long TimePeriod) {
  unsigned long currentMillis  = millis();
  if ( currentMillis - startOfPeriod >= TimePeriod ) {
    // more time than TimePeriod has elapsed since last time if-condition was true
    startOfPeriod = currentMillis; // a new period starts right here so set new starttime
    return true;
  }
  else return false;            // actual TimePeriod is NOT yet over
}



void stepChainBlink() {
  switch (BlinkStepNo) {

    case sc_InitClosing:
      spd1 = random(15);
      gap  = random(100, 500);
      pulseLen1 = topLidsOpen;
      pulseLen2 = botLidsOpen;
      topLidsServo.writeMicroseconds(pulseLen1);
      botLidsServo.writeMicroseconds(pulseLen2);
      WaitingTimer = millis(); // store snapshot of time when waiting starts
      BlinkStepNo = sc_CloseWait; // do a FIRST waiting
      break; // immidiately jump down to END-OF-SWITCH


    case sc_CloseWriteServo:
      if (pulseLen1 < topLidsShut) {
        topLidsServo.writeMicroseconds(pulseLen1);
      }
      if (pulseLen2 < botLidsShut) {
        botLidsServo.writeMicroseconds(pulseLen2);
      }
      BlinkStepNo = sc_CloseWait; // wait again
      break; // immidiately jump down to END-OF-SWITCH


    case sc_CloseWait:
      if ( TimePeriodIsOver(WaitingTimer, spd1) ) {
        // if the amount of milliseconds stored in spd1 have passed by
        if (pulseLen1 < topLidsShut || pulseLen2 < botLidsShut) {
          // if endpositions are not yet reached
          pulseLen1 += 10; // set new positions
          pulseLen2 += 10;
          BlinkStepNo = sc_CloseWriteServo;
        }
        else { // endpositions ARE reached
          WaitingTimer = millis(); // store snapshot of time when waiting starts
          BlinkStepNo = sc_WaitGapTime;
        }
      }
      break; // immidiately jump down to END-OF-SWITCH


    case sc_WaitGapTime:
      if ( TimePeriodIsOver(WaitingTimer, gap) ) {
        BlinkStepNo = sc_InitOpening;
      }
      break; // immidiately jump down to END-OF-SWITCH


    case sc_InitOpening:
      spd2 = random(15);
      pulseLen1 = topLidsShut;
      pulseLen2 = botLidsShut;
      topLidsServo.writeMicroseconds(pulseLen1);
      botLidsServo.writeMicroseconds(pulseLen2);
      WaitingTimer = millis(); // store snapshot of time when waiting starts
      BlinkStepNo = sc_OpenWait; // do a FIRST waiting
      break; // immidiately jump down to END-OF-SWITCH


    case sc_OpenWait:
      if ( TimePeriodIsOver(WaitingTimer, spd2) ) {
        // if the amount of milliseconds stored in spd2 have passed by
        if (pulseLen1 > topLidsOpen || pulseLen2 > botLidsOpen) {
          // if endposition is not yet reached
          pulseLen1 -= 10; // set new position
          pulseLen2 -= 10;
          BlinkStepNo = sc_OpenWriteServo;
        }
        else { // endpositions ARE reached
          WaitingTimer = millis(); // store snapshot of time when waiting starts
          PauseBetweenBlinks = random(100, 10000);
          BlinkStepNo = sc_WaitForNextBlink; // one blink finished go idling until next blink starts
        }
      }
      break; // immidiately jump down to END-OF-SWITCH


    case sc_OpenWriteServo:
      if (pulseLen1 > topLidsOpen) {
        topLidsServo.writeMicroseconds(pulseLen1);
      }
      if (pulseLen2 > botLidsOpen) {
        botLidsServo.writeMicroseconds(pulseLen2);
      }
      WaitingTimer = millis(); // store snapshot of time when waiting starts
      BlinkStepNo = sc_OpenWait;
      break; // immidiately jump down to END-OF-SWITCH


    case sc_WaitForNextBlink:
      if ( TimePeriodIsOver(WaitingTimer, PauseBetweenBlinks) ) {
        // if the amount of milliseconds stored in PauseBetweenBlinks have passed by
        BlinkStepNo = sc_InitClosing; // start next blink-cycle
      }
      break; // immidiately jump down to END-OF-SWITCH
  } // END-OF-SWITCH
}

/******************* Initializing PWM and Serial *********************/
void setup() {
  topLidsServo.attach(topLidsPin); // Attach the top lids servo to its pin
  botLidsServo.attach(botLidsPin); // Attach the bottom lids servo to its pin
  pinMode(K1, OUTPUT);
  pinMode(K2, OUTPUT);
  pinMode(K3, OUTPUT);
  pinMode(K4, OUTPUT);
  digitalWrite(K1, HIGH);
  digitalWrite(K2, HIGH);
  digitalWrite(K3, HIGH);
  digitalWrite(K4, HIGH);

  attachInterrupt (0, growlRequest, FALLING); // Vector to growlRequest upon falling pin #2
  Serial.begin(115200);              // Initialize the serial monitor for troubleshooting
  Serial.println("Ready to Go!\r");
}


/**************************  Growl Request Interrupt  ****************/
void growlRequest() {
  // millis() does not count up inside ISR but you can store the value it has
  LowPulseTimer = millis();
  LowPulseActive = true; // set flag true to indicate ISR was triggered

  mp3K0 = analogRead(gReq0);  // map(analogRead(gReq0), 0, 1023, 0, 1);
  if (mp3K0 > 512) {
    mp3K0 = 1;
  }
  else {
    mp3K0 = 0;
  }
  mp3K1 = analogRead(gReq1);  // map(analogRead(gReq1), 0, 1023, 0, 1);

  if (mp3K1 > 512) {
    mp3K1 = 1;
  }
  else {
    mp3K1 = 0;
  }

  if ((mp3K0 == 0) && (mp3K1 == 0)) {
    digitalWrite(K1, LOW);
    // delay(50);        // delayMillis(50UL);
  }
  else if ((mp3K0 == 1) && (mp3K1 == 0)) {
    digitalWrite(K2, LOW);
    //delay(50);        // delayMillis(50UL);
  }
  else if ((mp3K0 == 0) && (mp3K1 == 1)) {
    digitalWrite(K3, LOW);
    //delay(50);        // delayMillis(50UL);
  }
  else {
    digitalWrite(K4, LOW);
    //delay(50);        // delayMillis(50UL);
    // Serial.println("Pulsed K4");
  }
}


void terminatePulseAfter50ms() {
  if (LowPulseActive) { // is true if ISR was triggered
    if ( TimePeriodIsOver(LowPulseTimer, 50) ) {
      // if 50 milliseconds have passed by
      digitalWrite(K1, HIGH); // terminate LOW-pulse through switching HIGH
      digitalWrite(K2, HIGH);
      digitalWrite(K3, HIGH);
      digitalWrite(K4, HIGH);
      LowPulseActive = false;
    }
  }
}

// ***********************  Main Loop   ******************************/
void loop() {  
  stepChainBlink(); // all the blinking is done through this function
  terminatePulseAfter50ms(); 
}

best regards Stefan

1 Like

Finally I added toggling an IO-pin to loop()

void loop() {  
  digitalWrite( togglePin,!digitalRead(togglePin) ); // invert state of IO-pin

  stepChainBlink(); // all the blinking is done through this function
  terminatePulseAfter50ms(); 
}

and measured with a digital storage oscilloscope the loop-cycle-time.
Pretty fast 19 microseconds

This is so fast that there is no need for any interrupt

Reading in analog inputs needs a little bit more time
If this reading in is done by a function that is called from loop()
The loop-cycletime is 240 microseconds = 0,24 milliseconds
still very fast.
This kind of reading inputs is called polling.
As long as the cycle-time of loop() is kept short through non-blocking timing
in most cases there is no need for an interrupt.
You could even add polling a rotary-encoder which is used as user-input to this code

So all in all this was interesting for me to write the code and to analyse how fast the non-blocking is.
best regards Stefan

1 Like

Stefan,

I want to thank you for the effort you've put into helping me - and others who might take a look at this thread - understand how to write non-blocking code.

I revisited this thread this morning to share a discovery I made that the delay(50) lines in my ISR actually measured only 1.38 ms with a scope. Through experimentation I found that I had to increase this to delay(10000) to yield a pulse delay of 50 ms in the ISR. I was going to post this to request a possible explanation when, of course, I read your tutorial-by-example.

Yes, I do consider myself very fortunate to have received such customized, project-specific training. It will take me a few days to study and understand your alternative code, but I will definitely do this with the goal of learning how to avoid blocking code.

While I don't have a ton of experience with programming, I've written C-code programs for other (Microchip) microcontrollers. The Arduino environment certainly affords advantages in that it unburdens the user from many of the complications associated with learning how a specific microcontroller's registers, peripherals, interrupts, etc. work. The other edge of that sword also limits one's ability to take full advantage of available options such as setting interrupt priorities, polling (vs vectoring from) interrupt flags, enabling/disabling interrupts, controlling peripherals, etc. Once relevant portions of an alternative (e.g. PIC) microcontroller's data sheet have been studied, it seems pretty straightforward to set a 100 ms interrupt with an ISR that largely sets flags to be monitored for action by the main program's housekeeping loop. The Arduino environment seems to disable all interrupts - without option - upon vectoring to an ISR.

Thanks again for your specific interest and training regarding my timing challenge.

Sorry, old chap, but ISR and delay() DO NOT MIX.

Hi Stefan.

I'm not proud to say that it's taken several days of occasional study for your non-blocking code approach to sink into my brain, but now that the lightbulb has finally come on, I do appreciate its clever efficiency despite the additional complexity.

If you're available, I'd like to take you up on your generous offer to answer questions:

  1. Is it safe to assume that the "&" before "startOfPeriod" was unintended (vs. syntax I'm not familiar with)?

  2. The "if" clauses in the sc_CloseWriteServo: and sc_OpenWriteServo cases strike me as being redundant given that the preceding "if" clauses in the sc_CloseWait and sc_OpenWait cases appear to have already established that the eyelid servos are fully closed or open. Am I missing something?

  1. Am I correct in assuming that the above lines in the sc_InitOpening case are unnecessary since the eyelids hare already been closed by the sc_CloseWriteServo case at this point?

Thanks again very much for taking the time to explain and demonstrate (especially using my specific application as an example) this clever but not so intuitive approach to implementing delays without sacrificing pseudo-multitasking functionality!

Hi @bitsoplenty

You are a very attentive reader. The &-symbol in front of the parameter is fully intentional. It has a special function.
The TimePeriodIsOver-function uses a timer-variable. Non-blocking timing based in function millis() requires to update the snapshot of time each time when a new period starts.

The &-symbol tells the compiler to give back the eventually modified value of the variable the function TimePeriodIsOver was called with
with easy to follow numbers

void myFunc(int &myParameter) {
  myParameter = 10
}

and then calling

myVar = 3;

myFunc(myVar);

after executing function myFunc the variable myVar contains the value 10
because of the &-symbol in the declaring of the function

  1. Yes is redundant. I transferred your original code closely to the stepchain
    if-conditions can be removed

  2. servo-attach() puts the servos on 90 degrees.
    This means on startup the servos are at a different position
    But you can move this initial servowrites to setup

I haven't gone through all details new with your questions. So it might be that there could be a special case I haven't considered. If you test a modified code with servos that can turn freely so the servos can't be blocked mechanically you can test it.

If you like I can add a function that prints to the serial monitor at which step the step-chain is and add two macros that enable comfortable debug-output

best regards Stefan

1 Like

Thanks for the clarifications, Stefan.

In your much-appreciated "&" example, I gather that if the ampersand was missing, the value of myVar would have remained 3 after the function? 'Seems counterintuitive... and unclear to me when the ampersand is needed.

Also, I didn't know that the servo.attach() function defaults to a 90 degree (1.5 ms?) output. Good to know in case powering up would cause problems if the default servo position is not immediately corrected in setup().

Thanks for your generous offer to add serial monitor calls/macros, but I can add these as needed during troubleshooting.

Thanks again!

The ampersand-operator is not needed very often. Just in case you want the value beeng modifyed that you "handover" to a function.
The timer-variable must be updated. Otherwise calculating the difference would result in immidiately true
if (currentTime - StartTime >= period)

first period currentTime 0:05 - StartTime 0:03 = 0:02
first period currentTime 0:06 - StartTime 0:03 = 0:03
first period currentTime 0:07 - StartTime 0:03 = 0:04
first period currentTime 0:08 - StartTime 0:03 = 0:05 >= 5
start new period update StartTime to 0:08
second period currentTime 0:09 - StartTime 0:08 = 0:01
second period currentTime 0:10 - StartTime 0:08 = 0:02
second period currentTime 0:11 - StartTime 0:08 = 0:03
second period currentTime 0:12 - StartTime 0:08 = 0:04
second period currentTime 0:13 - StartTime 0:08 = 0:05

without updating
second period currentTime 0:09 - StartTime 0:03 = 0:06 => immidiately overdue
best regards Stefan

OK So I simply post a link to the macros that I'm using

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.