Changing ISR's return address to a function's memory location?

Is it possible modify the return address of an ISR?

Browsing the forum I've found this:

ISR (XXX_vect, ISR_NAKED) {
  asm (
    "pop r0\n"   // remove PC value from stack
    "pop r0\n"
    // load new PC into regs 30 and 31 (I can't remember how to reference an external C function address)
    "push r30\n"
    "push r31\n"
    "reti\n"
  );

However, how do I set the memory location of the function I wish to return to?

For example, If I wanted to return from the ISR to the function read_voltage below, how do I find the memory address for read_voltage??

void loop(){}

void read_voltage() 
{
  //code removed for simplicity
}

ISR(INT0_vect) //Interrupt function
{
//Want to change the return address here
}

It isn't that simple - how do you recover the function's stack frame?

It is possible, and it's basically how proper multi-threading works.

It is best to work directly in assembly for this kind of thing as it's then possible to have complete control over what is done.

As mentioned previously, you have to be careful of stack pointers.

If you are permanently changing the flow of the program though this kind of mechanism then you will break it, and that is guaranteed.

Take the following scenario:

  1. You call function A.
  2. Function A moves the stack pointer to allocate space for its local variables etc.
  3. Function A does some work
  4. Function A resets the stack pointer
  5. Function A returns.

Now you introduce your interrupt:

  1. You call function A.
  2. Function A moves the stack pointer to allocate space for its local variables etc.
  3. Function A does some work
  4. Your interrupt fires
  5. The ISR returns to the start of function B
  6. Function B moves the stack pointer more to allocate space for its local variables etc.
  7. Function B does some work
  8. Function B resets the stack pointer to where it thinks it should be
  9. Function B returns.

What happened to the space allocated by Function A? It's lost! Gone!

If that's what you're trying to do then you are almost certainly trying to do the wrong thing.

The only time when this kind of return address tinkering is valid is when implementing a context switch, where you maintain a record of things like stack pointers etc and switch between different threads with each thread having its own local stack. In that kind of scenario you will be returning back to where you left off in Function A at some point in the future, thus reclaiming its stack space when the function completes.

Thanks for such a detailed response majenko!

My application will have an empty loop function causing the program to be idle until the interrupt is triggered. I trigger the interrupt from another device and have control over its implementation and timing.

My function B, (read_voltage) wants to be called on each external interrupt. I'm doing it this way as my read_voltage function takes ADC reads using the timer compare match functionality which relies on interrupts of lower priority.

So, given that my program just sits idle in loop() until the interrupt is triggered, is modifying the return address to Function B still an approach I can take?

given that my program just sits idle in loop() until the interrupt is triggered

So why not just call your function when the interrupt is triggered or flag tha it has been triggered and call it from loop() ?

So why not just call your function when the interrupt is triggered or flag tha it has been triggered and call it from loop() ?

I've omitted a lot of background info around the project as its rather complicated.
I'm looking at a novel way of taking simultaneous voltage and current samples from a single phase power lines using separate Arduinos. The same code will run on both devices.

Both the voltage and current sampling boards have their interrupt pins wired together. They receive the same short pulse, triggering an interrupt in each device.

I can't call my function from the ISR directly as my read_voltage function relies on lower level interrupt priorities such as the Timer compare register match interrupt and ADC conversion complete interrupt to complete it's work.

I need the loop function to be empty to ensure both devices (voltage and current sampling boards) enter the ISR at the same instant. If the loop function had an 'If statement' set up checking the status of a flag set in the ISR, each device could be a number of machine cycles through the if statement operation before jumping to the ISR and hence would not be simultaneous.

I hope this makes sense :~

I hope this makes sense

Not entirely

each device could be a number of machine cycles through the if statement operation before jumping to the ISR and hence would not be simultaneous.

How simultaneous do they have to be ? Can you quote any figures ?

newb91:
I can't call my function from the ISR directly as my read_voltage function relies on lower level interrupt priorities such as the Timer compare register match interrupt and ADC conversion complete interrupt to complete it's work.

If you enable interrupts while in that ISR, your timer events will still fire.

newb91:
I need the loop function to be empty to ensure both devices (voltage and current sampling boards) enter the ISR at the same instant. If the loop function had an 'If statement' set up checking the status of a flag set in the ISR, each device could be a number of machine cycles through the if statement operation before jumping to the ISR and hence would not be simultaneous.

When an interrupt is polled it is called after the current machine cycle finishes, not the entire operation or if statement.

If you enable interrupts while in that ISR, your timer events will still fire.

I can't believe I overlooked this! Thank you!!

newb91:
... my read_voltage function relies on lower level interrupt priorities such as the Timer compare register match interrupt and ADC conversion complete interrupt to complete it's work.

Can you tell us more about how that works?

Insert Quote
Quote from: newb91 on Today at 11:22:33 am
... my read_voltage function relies on lower level interrupt priorities such as the Timer compare register match interrupt and ADC conversion complete interrupt to complete it's work.
Can you tell us more about how that works?

I have setup timer0 to interrupt every time it reaches the value stored in the OCROA register. The timer will interrupt, reset to zero and continue counting until stopped. This allows you to implement timer interrupts at a specific frequency.

The ADC is set up to perform a conversion when the timer interrupts. This is done by manipulating the ADC's ADCSRB bits.

Here is my code:

void setup()
{
 cli();//stop interrupts

  //SETUP TIMER (10.4kHz interrupt freq)
  TCCR0A = 0;// set entire TCCR0A register to 0
  TCCR0B = 0;// same for TCCR0B
  TCNT0  = 0;//initialize counter value to 0
  OCR0A = 23; // set compare match register to 10.4khz. 23 = (16*10^6) / (10400*64) - 1 [23 = 10.4kHz)
  TCCR0A |= (1 << WGM01);// turn on CTC mode
  TCCR0B |= (1 << CS01) | (1 << CS00);   // Set CS01 and CS00 bits for 64 prescaler
  TIMSK0 |= (1 << OCIE0A); // enable timer0 compare  match A interrupt
  //SETUP ADC
  ADMUX = 0x00;   //configure for A0 pin, use AREF as reference.
  ADCSRA = 0x00;
  ADCSRB = 0x00;
  
  //ADMUX = (1 << REFS0); // Setting reference voltage (Vcc). ////comment out to use Aref from master to both slaves
  
  ADCSRA = ((1 << ADEN)  | // Enable ADC. 
  (1 << ADATE) |          // Auto Trigger enable.
  (1 << ADIE)  |           // ADC Interrupt enable.
  
  (1 << ADPS2) | (1 << ADPS1) | (0 << ADPS0));  // Setting prescaler 64 -> 250Khz ADC clock -> Fs = ~192305Hz
                                                // (as 13ADC clocks per conversion) -> T = 52us per sample
                                                
  ADCSRB |= ((0 << ADTS2) | (1 << ADTS1) | (1 << ADTS0)); // ADC auto trigger source selection -> Timer 0 Compare Match A selected.
  DIDR0 = 0x00; // Turn Off Digital Input Buffer.
  
  //SETUP EXTERNAL INTERRUPT 
  EICRA = (1<< ISC00) | (1<<ISC01); //External interrupt control register set up for interrupt on falling edge int0 (pin2)
  EIMSK = (1<<INT0); //enable global interrupt 0 
  sei();//allow interrupts
}

I don't quite follow.

newb91:
... (read_voltage) wants to be called on each external interrupt. I'm doing it this way as my read_voltage function takes ADC reads using the timer compare match functionality which relies on interrupts of lower priority.

It looks like you've set up free-running analog conversions, controlled by Timer0. At the same time, it sounds like you want to start a conversion when external interrupt 0 fires.

Have I got that right?

Sorry for such a late reply.

It looks like you've set up free-running analog conversions, controlled by Timer0. At the same time, it sounds like you want to start a conversion when external interrupt 0 fires.

Have I got that right?

No. The ADC is configured to perform analogue conversions on the Timer/Counter0 Compare Match A. This is done on this line

ADCSRB |= ((0 << ADTS2) | (1 << ADTS1) | (1 << ADTS0)); // ADC auto trigger source selection -> Timer 0 Compare Match A selected.

So the Timer interrupts every time the value stored in the register is reached, thus starting a conversion.

You have said that you want two separate Arduinos to react to the same timer signal so that one reads voltage and the other reads current at the same moment.

So let's have some useful data ...

How many microseconds (or millisecs) between samples?

How much variation (in microsecs) is acceptable between the instants at which current and voltage are sampled for a single data point?

...R

I need the loop function to be empty to ensure both devices (voltage and current sampling boards) enter the ISR at the same instant. If the loop function had an 'If statement' set up checking the status of a flag set in the ISR, each device could be a number of machine cycles through the if statement operation before jumping to the ISR and hence would not be simultaneous.

At 60 Hz there are 16,666 microseconds in an AC cycle. At 16 MHz the Arduino can execute 16 instructions in one microsecond. It is hard to see how an if statement could introduce any significant jitter in your data. If you really have incredibly tight coherency requirements for your data, Arduino might not be the way to go.

DavidOConnor:
At 60 Hz there are 16,666 microseconds in an AC cycle.

Another way of looking at this is to wonder why one Arduino wouldn't be plenty fast enough to read voltage and current with successive analogRead()s.

...R

I see this as a solution to the problem of synchronizing ADC readings. I admit, though, that my understanding of the OP's goal is kind of fuzzy.

  • Set the ADC to auto-trigger on external interrupt 0
  • Set a timer to run at the appropriate frequency
  • Set that timer to deliver a PWM output
  • Connect that PWM pin to external interrupt 0 on both Arduinos

The datasheet says that the ADC prescaler is reset when the ADC is auto-triggered by an event, to assure a fixed delay from the trigger event to the start of conversion. That will ensure that the samples taken by the two Arduinos are in sync to within what I'd expect to be a couple of system clock cycles, without intervention from the program. Without some kind of active synchronization of ADC readings, they'll drift apart quite quickly, due to the difference in the frequencies of the two ceramic oscillators.

If the intent is to make power measurements on a single-phase circuit, I think that taking readings of current and voltage alternately on a single Arduino would be accurate enough. Fiddling with a unity power factor calculation, close to full scale, in Excel, I get an error of about 0.1% by reacting alternate readings as if they were taken simultaneously. Interpolating one set, I get about the same results. That seems to be plenty accurate for a metering application, but there's no guarantee that's what the OP is up to.