How do I determine if my interrupt callback function execution time is acceptable?

samm_flynn

Active member
Hope everyone is doing well.

I come from a non programming background and never was formally taught any programming.

Now I am working on a project with a teensy 4.1, where I am trying to most of my work event based.

I'm running a system at 2KHz sampling rate (control bandwidth is 1 KHz) and have 10 interrupt function calls (I know each function's execution time).

In other words, how do I decide if the interrupt function calls are taking too long?

From the internet(I didn't go too deep) I found in many articles suggests to keep interrupt function execution times short.


How do i determine, what long is short/fast enough.

what should my thought process be like.

Any example /resources / opinions are welcome, I don't have anything concrete to post yet. Asking for general guidance.
 
Here are a couple of quick possibilities to guage how long your interrupt function is taking to execute:

1) Toggle a GPIO pin on at the beginning of your interrupt routine and toggle it off at the end, then monitor that GPIO pin with an oscilloscope. If you see some idle time from the time that the GPIO pin goes low (the end of one interrupt process) to the time that the GPIO goes back high (the start of another interrupt process), then your interrupt function is short enough. If, instead, you see just a very short duration on the time that the GPIO is low, then your interrupt function is too long. The time from when the GPIO pin goes low to the time that your GPIO pin goes hign is the time that your interrupt function took to run. If you have multiple interrupt functions, then allocate a unique GPIO pin to each of them.

2) Declare three variables (say, volatile unsigned long start_of_int, volatile unsigned long end_of_int, & volatile boolean int_in_progress (all volatile because they are modified inside an interrupt). At the beginning of your interrupt function, set int_in_progress to true, & store the current millis() value into start_of_int. At the end of your interrupt function, store the current millis() value into end_of_int, then set int_in_progress back to false. In your loop() function, check int_in_progress & when it is false, subtract start_of_int from end_of_int, then print that difference value to the Serial Console. That difference value is how long your interrupt function took to run.

Hope that helps . . .

Mark J Culross
KD5RXT
 
Continuing with suggestion #2 above, if you also declare an additional variable volatile unsigned long previous_end_of_int, then inside your interrupt function right after setting start_of_int to the current millis() value, copy end_of_int to previous_end_of_int.

In your loop() function, when the int_in_progress flag is false, subtract start_of_int from previous_end_of_int to get the number of milliseconds of idle time between interrupts.

Mark J Culross
KD5RXT
 
Here's an example program that computes execution time in microseconds for an IntervalTimer handler. The timer is configured for 1 kHz. Execution time is computed using the ARM cycle counter and converting to microseconds for printing.

Code:
#include <IntervalTimer.h>

// macro to convert CPU cycles to microseconds
#define CYC_TO_US(cycles) (cycles*(1E6/F_CPU_ACTUAL)) 

IntervalTimer t1;
elapsedMillis ms;

typedef struct {
  volatile uint32_t start;
  volatile uint32_t count;
  volatile uint32_t cycles;
  volatile uint32_t max;
} isr_stats;

isr_stats t1_stats;
volatile uint16_t adc_value;

void t1_handler(void) {
  t1_stats.start = ARM_DWT_CYCCNT;
  /////////////////////////////////////////////////
  // replace line below with your interrupt code
  adc_value = analogRead( 0 );
  /////////////////////////////////////////////////
  t1_stats.cycles = ARM_DWT_CYCCNT - t1_stats.start;
  if (t1_stats.cycles > t1_stats.max)
    t1_stats.max = t1_stats.cycles;
  t1_stats.count++;
}

void setup() {
  Serial.begin(9600);
  while (!Serial && millis()<3000) {} // wait for Serial ready but not forever
  t1.begin( t1_handler, 1000 ); // timer1 period 1000 us (1 kHz)
  ms = 0;
}

void loop() {
  // print stats once per second
  if (ms >= 1000) {
    ms -= 1000;
    Serial.print( "adc = " );
    Serial.print( adc_value );
    Serial.print( "   t1_count = " );
    Serial.print( t1_stats.count );
    Serial.print( "   t1_max = " );
    Serial.print( CYC_TO_US(t1_stats.max) );
    Serial.println();
    t1_stats.max = 0; // reset max (or not)
  }
}
 
ARM cycle counter
Good using that, it is 12+ times faster to read - than micros() that is under 40 cycles.
Should ' isr_stats t1_stats; ' also be volatile?

Watching ' t1_stats.count ' should always be incrementing evenly.

If the interrupt ends up consuming too much time the numbers will show that - except when it never leaves cycles to return to loop() and print.

Another easy way is to keep track of ' count of loop() cycles per second ' it will run from millions downward as loop() is called less frequently:
Code:
void loop() {
  // print stats once per second
  static uint32_t lpCnt=0;
  lpCnt++;
  if (ms >= 1000) {
    ms -= 1000;
    Serial.print( "adc = " );
    Serial.print( adc_value );
    Serial.print( "   t1_count = " );
    Serial.print( t1_stats.count );
    Serial.print( "   t1_max = " );
    Serial.print( CYC_TO_US(t1_stats.max) );
    Serial.print( "\tloops per seconds = " );
    Serial.print( lpCnt );
    Serial.println();
    t1_stats.max = 0; // reset max (or not)
    lpCnt=0;
  }
 
It shouldn't be necessary to use volatile at all in this case since the variables aren't local to the loop() function.
 
It shouldn't be necessary to use volatile at all in this case since the variables aren't local to the loop() function.
@jmarsh, I'm sure you have a better understanding of this than I do, so can you please correct me where I'm wrong in my thinking? I thought volatile was needed because these are globals that are written in the ISR, but read in loop(), and without the volatile, the compiler might optimize out loop's reading of the variables, since the variables are not written from loop.
 
Unless LTO is used the compiler can't "unroll" loop() into the equivalent of a while() or do() construct; it's still going to be a general function. So it has to assume global variables may be modified (by other code) between each time it gets executed.

"volatile" is only required when you have a repeating loop (while / do / for) that checks for modification of a variable, and the compiler can safely assume that variable can't be modified inside the loop (either because it is function-local or there are no intervening calls to functions outside the current source file).
 
Last edited:
Unless LTO is used the compiler can't "unroll" loop() into the equivalent of a while() or do() construct; it's still going to be a general function. So it has to assume global variables may be modified (by other code) between each time it gets executed.

"volatile" is only required when you have a repeating loop (while / do / for) that checks for modification of a variable, and the compiler can safely assume that variable can't be modified inside the loop (either because it is function-local or there are no intervening calls to functions outside the current source file).
Okay, thanks very much. That's clear. I wonder if this was a consideration in Arduino's decision to use the setup/loop construct, which I'll never get used to...
 
Volatile is required whenever a variable may change in a way the compiler cannot see (interrupts and hardware registers being the classic examples). If you don't turn on any optimizations it might not matter, but your code behaviour is strictly speaking not defined without the volatile declarations.

The superloop approach as used in Arduino libraries suits small resource-constrained microcontrollers where an RTOS might not even fit in memory!!
 
Thanks every one for contribution, I guess I will try all the suggestions mentioned here and see whats happening.

Thanks , much appreciated.
 
Back
Top