RunEvery Macro vs. interrupt driven processing

Hi Nerds here,

You may want to discuss the results of my comparison between two methods of timing:

  1. timing interrupt driven
  2. timing millis() driven

I am using a quite extensive sketch that needs processing at three timing levels:

a) every 10 ms gathering data and integrating averages, processing the serial menu.
b) every 50 ms computing 10mS averages, calculating maximum and minimum, doing some maths, integrating averages for the next level.
c) every second computing 50 ms averages, doing some maths, more computing, and printing out some results ( which is a part of the process that takes the most time ).

a) Timing interrupt driven.
Since the processing involved can be quite extensive, I do not process it directly within the interrupt, but just set a flag, and process the sketch whenever the flag is set.
I used to use 2 interrupt timers.
-one at 500Hz to set the flags for 10 ms and 50 ms, flags are reset once the job is done.
-another one at 1Hz that sets the flag for one second, flag it is reset once the job is done as well.

As a part of the reporting, the number of times each step is processed within one second is displayed.

Here is the result using the method with interrupts:

22:39:11.721 -> Vcc=4.94 | Vbat=12.50 | 22°C | A0Raw=425 | 10ms=94 | 50mS=20 | Avg=28.08 | Min=25.57 | Max=30.08
22:39:12.724 -> Vcc=4.94 | Vbat=12.56 | 22°C | A0Raw=421 | 10ms=94 | 50mS=20 | Avg=27.70 | Min=25.07 | Max=29.96
22:39:13.727 -> Vcc=4.94 | Vbat=12.48 | 26°C | A0Raw=427 | 10ms=94 | 50mS=20 | Avg=29.33 | Min=27.20 | Max=31.71
22:39:14.731 -> Vcc=4.94 | Vbat=12.48 | 26°C | A0Raw=450 | 10ms=94 | 50mS=20 | Avg=28.95 | Min=27.07 | Max=31.21
22:39:15.734 -> Vcc=4.94 | Vbat=12.40 | 23°C | A0Raw=426 | 10ms=94 | 50mS=20 | Avg=26.95 | Min=24.69 | Max=29.21

The timestamp in the first column of the display shows that:

  • The 1 Sec timing is quite precise with a bit of jitter.
  • The: 10 mS column shows that ( probably during printing) the 10 ms process cannot be 100% granted.
  • The: 50 mS column is executed precisely.

B) timing using indirectly millis()
I made a second version of the program, and eliminating the interrupt and the flags replacing them by the runEvery macro (millis()-based used @10mS, 50mS and 1000mS.

The behavior of the program was roughly the same. The details in reporting gave following results:

22:53:13.147 -> Vcc=4.89 | Vbat=5.32 | 31°C | A0Raw=644 | 10ms=100 | 50mS=19 | Avg=53.77 | Min=51.77 | Max=56.40
22:53:14.151 -> Vcc=4.87 | Vbat=6.64 | 31°C | A0Raw=685 | 10ms=100 | 50mS=20 | Avg=58.53 | Min=56.03 | Max=60.91
22:53:15.154 -> Vcc=4.87 | Vbat=5.56 | 31°C | A0Raw=705 | 10ms=101 | 50mS=21 | Avg=62.79 | Min=60.91 | Max=64.30
22:53:16.158 -> Vcc=4.87 | Vbat=5.64 | 31°C | A0Raw=693 | 10ms=99 | 50mS=19 | Avg=63.05 | Min=60.54 | Max=65.43
22:53:17.161 -> Vcc=4.87 | Vbat=7.00 | 31°C | A0Raw=727 | 10ms=100 | 50mS=20 | Avg=63.67 | Min=61.54 | Max=66.05
22:53:18.164 -> Vcc=4.87 | Vbat=5.76 | 29°C | A0Raw=708 | 10ms=100 | 50mS=21 | Avg=60.91 | Min=59.29 | Max=63.92

Compared to the message with interrupts, we can notice following differences:
The one second cycle has a bit more jitter, but there is no huge difference between the two methods.
The 10 ms cycles are all executed with a jitter, if the cycle could not happen, we will get one cycle more in the next second
The 50mS cycles are also executed with a jitter, if the cycle could not happen, we will get one cycle more in the next second.

My conclusions: both methods are roughly equivalent,
-the interrupt timer driven processing keeps the precise timing, and skips cycles which cannot be processed.
-the millis() driven processing keeps the amount of total cycles, but brings a strong jitter on every level.

Summary: There is no clear performance winner, in every case you need to count the number of really executed cycles until the next level, if you are doing averages.

From the point of view of the user, there is no noticeable difference.

Regarding programming: The runEvery Macro is closer to natural language and is more portable.
The interrupts method must be adapted to the different processors, and requires quite tricky compiler directives if you want a code to be half way portable.

I will give the code used in the next message...

here is the code I have used:

Finally here is the relevant parts of my code used in both cases:

A) interrupt driven method:

void setup(void)
{
...

  //set timer1 interrupt at 1Hz
  TCCR1A = 0;// set entire TCCR1A register to 0
  TCCR1B = 0;// same for TCCR1B
  TCNT1  = 0;//initialize counter value to 0
  // set compare match register for 1hz increments
#if F_CPU == 16000000
  OCR1A = 15624;// = (16*10^6) / (1*1024) - 1 (must be <65536)
#elif F_CPU == 8000000
  OCR1A = 7811;// = (8*10^6) / (1*1024) - 1 (must be <65536)
#endif
  // turn on CTC mode
  TCCR1B |= (1 << WGM12);
  // Set CS12 and CS10 bits for 1024 prescaler
  TCCR1B |= (1 << CS12) | (1 << CS10);
  // enable timer compare interrupt
  TIMSK1 |= (1 << OCIE1A);

#ifndef __AVR_ATmega32U4__
  //set timer2 interrupt at 500Hz
  TCCR2A = 0;// set entire TCCR2A register to 0
  TCCR2B = 0;// same for TCCR2B
  TCNT2  = 0;//initialize counter value to 0
  // set compare match register for 2khz increments
#if F_CPU == 16000000
  OCR2A = 249;// = (16*10^6) / (1*1024) - 1 (must be <512)
#elif F_CPU == 8000000
  OCR2A = 124;// = (8*10^6) / (1*1024) - 1 (must be <512)
#endif

  // turn on CTC mode
  TCCR2A |= (1 << WGM21);
  // Set CS20 , CS 21 and CS22 bit for 128 prescaler
  TCCR2B |= (1 << CS22) | (0 << CS21) | (1 << CS20);
  // enable timer compare interrupt
  TIMSK2 |= (1 << OCIE2A);
#endif

  sei();//allow interrupts
} //end of setup


void loop(void)
{
[b]  if (timer10mS)[/b]
  {
    if (Serial.available())
    {
      inbyte = Serial.read(); //Serial input available
    }
    #ifndef __AVR_ATmega168__      //ATmega 168 lacks ram to run software serial
    if (mySerial.available()) 
    {
      inbyte = mySerial.read();  //Soft Serial input available
    }
#endif
      if (inbyte >= 31 & (inbyte <128)) stopReport = true;
    switch (inbyte)
    {
      //==== (Handshake with Serial Host) ======
      case 0:
        break;
      case 1: //Switch to mode 1
...
if (inputType > 0) // if no simulation
{
  A0Sum10mS += analogRead(pinA0);  A1Sum10mS += analogRead(pinA1); ++nRaw;  //Sum for Averaging
}
++amount10ms;  timer10mS = false;
}   
//end  if (timer10mS)


if (timer50mS) //running every 50mS
{

  if (inputType == 0)        //0  = no input: simulation*
  {
    //==== (Running simulation) ======
    n = random(0 , 80) + l;                      //Create 1% fast Noise + 5% random variation at 1 sec pace
    m = max (x1, 0) + y2 / 6 + y3 / 8 + 120;     //Half the first wave, mix with the second and third to create a pulsed, rectified AM wave type
    z = (m * m) / 2200 + n + 800;                // Square the result, add noise, add bias.
    A0Raw  = z  / 2;
    n = random(0 , 230);                          //Create 6% Noise
    m = max (x1, 0) - y2 / 8 + y3 / 6 + 120;     //Half the first wave, mix with the second and third to create a pulsed, rectified AM wave type
    z = (m * m) / 2200 + n + 780;                // Square the result, add noise, add bias.
    A1Raw  = z  / 2;
  }
  else
  {
    //==== (Computing 50mS Averages) ======
    A0Raw = A0Sum10mS / nRaw;  A1Raw = A1Sum10mS / nRaw; nRaw = 0; A0Sum10mS = 0; A1Sum10mS = 0;//averaging the readings within the 10ms period
  } //end if (inputType)
  //==== (Building 1S Averages, Max, Mins) ======
  A0Sum1S += A0Raw; A1Sum1S += A1Raw; ++n1S;                            //Sum for Averaging
  A0Max = max(A0Max, A0Raw); A0Min = min(A0Min, A0Raw); //computing max,min, storing the last sample
  ++amount50mS;   timer50mS = false;
}
//end if (timer100S)

if (timer1S)
{
  if (inputType == 0)                 //0  = no input: simulation*
  {
...
// Quite a lot of stuff here
...
} //end (timer1S) opened in Data processing

}  //end void(loop) Opend in Menu


//============ Timer relays ============

ISR(TIMER1_COMPA_vect) //timer1 interrupt 1Hz
{
  //every second
  timer1S = true;
  counter50mS = 0;
  if (millis() / 1000 % 60 == 20)   //every minute delayed by 20s
  {
    timer1M = true;
  }
}
//end ISR(TIMER1_COMPA_vect)

ISR(TIMER2_COMPA_vect) //timer1 interrupt 2Khz
{
++counter2KHz;
if (counter2KHz >= 5)   
{
  timer10mS = true;  ++counter10mS;
  if (counter10mS >= 5 )
  {
    counter10mS = 0;
    ++counter50mS;
    timer50mS = true;
  }
  counter2KHz = 0;
}
}
//end ISR(TIMER2_COMPA_vect)

B) millis() based method:

#define runEvery(t) for (static uint16_t _lasttime;\
                         (uint16_t)((uint16_t)millis() - _lasttime) >= (t);\
                         _lasttime += (t))
...
void loop(void)
{
[b]runEvery(10) //10mS[/b]
  {
    if (Serial.available())
    {
      inbyte = Serial.read(); //Serial input available
    }
    #ifndef __AVR_ATmega168__      //ATmega 168 lacks ram to run software serial
    if (mySerial.available()) 
    {
      inbyte = mySerial.read();  //Soft Serial input available
    }
#endif
      if (inbyte >= 31 & (inbyte <128)) stopReport = true;
    switch (inbyte)
    {
      //==== (Handshake with Serial Host) ======
      case 0:
        break;
      case 1: //Switch to mode 1
...
if (inputType > 0) // if no simulation
{
  A0Sum10mS += analogRead(pinA0);  A1Sum10mS += analogRead(pinA1); ++nRaw;  //Sum for Averaging
}
++amount10ms;  timer10mS = false;
}   
//end runEvery(10)


[b]runEvery(50)[/b] //running every 50mS
{

  if (inputType == 0)        //0  = no input: simulation*
  {
    //==== (Running simulation) ======
    n = random(0 , 80) + l;                      //Create 1% fast Noise + 5% random variation at 1 sec pace
    m = max (x1, 0) + y2 / 6 + y3 / 8 + 120;     //Half the first wave, mix with the second and third to create a pulsed, rectified AM wave type
    z = (m * m) / 2200 + n + 800;                // Square the result, add noise, add bias.
    A0Raw  = z  / 2;
    n = random(0 , 230);                          //Create 6% Noise
    m = max (x1, 0) - y2 / 8 + y3 / 6 + 120;     //Half the first wave, mix with the second and third to create a pulsed, rectified AM wave type
    z = (m * m) / 2200 + n + 780;                // Square the result, add noise, add bias.
    A1Raw  = z  / 2;
  }
  else
  {
    //==== (Computing 50mS Averages) ======
    A0Raw = A0Sum10mS / nRaw;  A1Raw = A1Sum10mS / nRaw; nRaw = 0; A0Sum10mS = 0; A1Sum10mS = 0;//averaging the readings within the 10ms period
  } //end if (inputType)
  //==== (Building 1S Averages, Max, Mins) ======
  A0Sum1S += A0Raw; A1Sum1S += A1Raw; ++n1S;                            //Sum for Averaging
  A0Max = max(A0Max, A0Raw); A0Min = min(A0Min, A0Raw); //computing max,min, storing the last sample
  ++amount50mS;   timer50mS = false;
}
//end runEvery(50)

[b]runEvery(1000)[/b] //running every Second
  {
  if (inputType == 0)                 //0  = no input: simulation*
  {
...
// Quite a lot of stuff here
...
  } //end //running every 1S opened in Data processing

}  //end void(loop) opened in Menu

wouldn't it make sense to execute the 10 msec processing along with the 50msec processing at the 50 msec interval and all three steps during the 1 sec interval? of course this assumes you can complete all three steps w/in 10 msec.

Can you not simplify this into something like this:

unsigned long
    time10,
    time50,
    time1000,
    timeNow;

const unsigned long TEN_MS = 10000ul;       //uS    # of uS in 10mS
const unsigned long FIFTY_MS = 50000ul;     //uS    # of uS in 50mS
const unsigned long ONE_SEC = 1000000ul;    //uS    # of uS in 1000mS

void setup() 
{
    //initializations as required...

    pinMode( LED_BUILTIN, OUTPUT );
    timeNow = micros();
    time10 = time50 = time1000 = timeNow;

}//setup

void loop() 
{
    timeNow = micros();

    if( (timeNow - time10) >= TEN_MS )
    {
        time10 = timeNow;
        //do 10mS stuff
        
    }//if

    if( (timeNow - time50) >= FIFTY_MS )
    {
        time50 = timeNow;
        //do 50mS stuff
        
    }//if

    if( (timeNow - time1000) >= ONE_SEC )
    {
        time1000 = timeNow;
        //do 1-second stuff 


        //show something happening
        digitalWrite( LED_BUILTIN, (digitalRead( LED_BUILTIN ) ^ HIGH) );
        
    }//if

}//loop

Creating a Timer interrupt to produce timing flags at specific intervals is no different from what the underlying Arduino system does to increment the value of millis(). I can't imagine any advantage from the extra complexity compared to simple code that queries millis() as in the demo Several Things at a Time

Interrupts are invaluable if you want to detect something that won't wait for the next polling cycle or where you need to identify to the nearest few microseconds when something external happened.

...R

gcjr:
wouldn't it make sense to execute the 10 msec processing along with the 50msec processing at the 50 msec interval and all three steps during the 1 sec interval? of course this assumes you can complete all three steps w/in 10 msec.

No.

why?

The 10 ms cycles are all executed with a jitter, if the cycle could not happen, we will get one cycle more in the next second

it’s not clear what you’re trying to do. is 10 msec processing require each 10 msec interval? why would you get one extra in next second?

gcjr:
why?

it's not clear what you're trying to do. is 10 msec processing require each 10 msec interval? why would you get one extra in next second?

normally you expect to get in 1 seconds 100 times the 10 ms processing.

With the interrupt driven method, during busy time of printing every second, the 10mS process can partly not happen, and the cycles are lost. Else be executed with a precise timing.

With the millis() method the short cycles will be executed, but later.

RIN67630:
With the millis() method the short cycles will be executed, but later.

That suggests that the program design needs to be improved.

...R

that's the typical reason why relatively time consuming printing should be done in the foreground and either triggered with a flag in an interrupt or by simply checking time while critical, non-printing processing is done in an interrupt.

have you considered a mix of interrupt and non-interrupt processing?

gcjr:
that's the typical reason why relatively time consuming printing should be done in the foreground and either triggered with a flag in an interrupt or by simply checking time while critical, non-printing processing is done in an interrupt.

have you considered a mix of interrupt and non-interrupt processing?

You surely mean that printing should be done in the background.
The Arduino doesn't have a concept like foreground/ background.

Indeed I just wanted to point out the effects of the tiniy differences between the millis() method and the interrupt driven flag setting method that I am using.

Both are fine for me. No action required.
It is okay when 95% of the fast sampling actions are done in time, while printing relatively intensively at 9600 Baud.

if it where critical, solutions without need to redesign the whole program exists:
a) improve the Baud rate.
b) split the print task into shorter chunks
c) gather the sampling data within the interrupt and differ the processing.

c) print less...

Robin2:
That suggests that the program design needs to be improved.

...R

No. The behavior is acceptable.

RIN67630:
You surely mean that printing should be done in the background.
The Arduino doesn't have a concept like foreground/ background.

i guess it's not obvious (to me) whether the higher priority interrupt is foreground or background, but because there are interrupts the concept applies to an arduino, perhaps more so than a processor running an OS.

part of the point is that the lower priority prints should be interruptible.

while not critical for all applications, there's not harm in architecting code in such a way that should the need arise, things are easy to support.

gcjr:
...there's not harm in architecting code in such a way that should the need arise, things are easy to support.

once you begin to change things in an interrupt, that are also used in the main program (and that is the very reason of the fast data-gathering), expect a lot of hassle...
That would not be easy to support...

I’m not sure where this is going or why.

In a couple of projects, I have several (30+) timers using millis(), and the only issue I have because i’m too lazy to rewrite), is that I still use a public library function that contains rarely called ‘blocking’ code.

Everything else runs perfectly, all the time, with runtimes well over a year, and once a month, I experience the 5mS blocking.

I’m not saying my code is any better than yours, and it took a while to get there, but structure and precedemce of function calls is a big thing.

lastchancename:
I’m not sure where this is going or why.

It was just a comparison of 2 methods that are both working fine, but have subtle differences in detail.