Search code examples
cinterruptavr-gcc

avr-gcc: jump to arbitrary address after ISR has been invoked


I'm working with an ATmega168p and compiling with avr-gcc.

Specifically, I have an RS485 slave that receives bytes via UART and writes them to a buffer in an ISR. If an end character is received, a flag is set in the ISR. In my main loop this flag is checked and the input buffer is processed if necessary. However, there is the problem that some time can pass between the arrival of the end byte and the time when the handler in the main loop processes the input buffer, because of the other "stuff". This results in a latency which can be up to several milliseconds, because e.g. sensors are read in every n-th iterations.

ISR(UART_RX_vect) {
  write_byte_to_buffer();
  if (byte==endbyte) // return to <HERE>
}

void main(){
  init();
  for(;;){
    // <HERE> I want my program to continue after the ISR received an end byte
    handle_buffer();
    do_stuff(); // "stuff" may take a while
  }

I want to get rid of this latency, as it is the bottleneck for the higher-level system.

I would like that after the ISR received the end byte, the program returns to the beginning of my main loop, where the input buffer would be processed immediately. I could of course process the input buffer directly in the ISR, but I am aware that this is not a good practice. This would also overwrite packets when the ISR gets invoked while processing a packet.

So, is there a way to overwrite an ISR's return address? Does C include such a feature, maybe something like goto? Or am I completely on the wrong track?

Edit: Below is a reduced version of my code which also causes the described latency.

#define F_CPU 8000000UL
#define BAUD 38400
#define BUFFER_LENGTH 64

#include <util/setbaud.h>
#include <avr/interrupt.h>
#include <stdbool.h>


volatile char input_buffer[BUFFER_LENGTH + 1] = "";
volatile uint8_t input_pointer = 0;
volatile bool packet_started=false;
volatile bool packet_available = false;

ISR (USART_RX_vect) {
    unsigned char nextChar;
    nextChar = UDR0;
    if (nextChar=='<') {
        input_pointer=0;
        packet_started=true;
    }
    else if (nextChar=='>' && packet_started) {
        packet_started=false;
        packet_available=true;
    }
    else {
        if (input_pointer>=BUFFER_LENGTH) {
            input_pointer=0;
            packet_started=false;
            packet_available=false;
        }
        else {
            input_buffer[input_pointer++]=nextChar;
        }
    }
}


bool ADC_handler () {
    ADCSRA = (1<<ADEN)|(1<<ADPS2)|(1<<ADPS1)|(1<<ADPS0);
    ADCSRA |= (1<<ADSC);

    while (ADCSRA & (1<<ADSC)); // this loop blocks and causes latency
    // assigning conversion result to a variable (not shown)
}

void ADC_init(void) {
    ADMUX = (1<<REFS1)|(1<<REFS0)|(1<<MUX3);
    ADCSRA = (1<<ADEN)|(1<<ADPS2)|(1<<ADPS1)|(1<<ADPS0);
}


void process_buffer() {
    // this function does something with the buffer
    // but it takes "no" time and is not causing latency
    return;
}

void UART_handler () {
    if (packet_available) process_buffer();
}

void UART_init (void) {
    UBRR0H = UBRRH_VALUE;
    UBRR0L = UBRRL_VALUE;
    UCSR0B |= (1<<RXCIE0)|(1<<RXEN0)|(1<<TXEN0);
    UCSR0C |= (1<<UCSZ01)|(1<<UCSZ00);
}


int main(void){
    UART_init();
    ADC_init();
    // initializing some other things
    sei();

    for(;;){
        UART_handler();
        ADC_handler();
        // other handlers like the ADC_handler follow
    }
    return 0;
}

I'm aware that the latency is due to blocking code, in this case the while loop in the ADC_handler() that waits for the conversion to finish. I could check for packet_available in the ADC handler and make this funtion return if the flag is set or I could even retrieve the conversion result with an ADC interrupt. That's all nice because I'm the one who implements the ADC_handler(). But if I wanted to use third party libraries (e.g. sensor libraries provided by manufacturers) I would depend on how those libraries are implemented. So what I'm looking for is a way to handle the problem "on my side"/in the UART implementation itself.


Solution

  • Don't try to use setjmp()/longjmp() to re-enter a main-level function from an ISR. This calls for disaster, because the ISR is never finished correctly. You might like to use assembly to work around, but this is really fragile. I'm not sure that this works at all on AVRs.

    Since your baudrate is 38400, one byte needs at least some 250µs to transfer. Assumed that your message has a minimum of 4 bytes, the time to transfer a message is at least 1ms.

    There are multiple possible solutions; your question might be closed because they are opinion-based...

    However, here are some ideas:

    Time-sliced main tasks

    Since a message can arrive only once per millisecond or less, your application don't need to be much faster than that.

    Divide your main tasks into separated steps, each running faster than 1 ms. You might like to use a state machine, for example to allow slower I/O to finish.

    After each step, check for a completed message. Using a loop avoids code duplication.

    Completely interrupt-based application

    Use a timer interrupt to do the repeated work. Divide it in short tasks, a state machine does magic here, too.

    Use an otherwise unused interrupt to signal the end of the message. Its ISR may run a bit longer, because it will not be called often. This ISR can handle the message and change the state of the application.

    You need to think about interrupt priorities with much care.

    The endless loop in main() will effectively be empty, like for (;;) {}.