simavr icon indicating copy to clipboard operation
simavr copied to clipboard

Interrupt handling after sleep

Open Morpheus1822 opened this issue 4 years ago • 5 comments

I came across a bug (?) regarding the interrupt handling after sleeping. My minimal application looks like this:

static volatile uint8_t event = 0;
ISR(INT0_vect) {
    event = 1;
}

void main(void) {
    sleep_enable();
    // interrupt initialization

    while(1) {

        cli();
        while(event == 0) {
            sei();
            sleep_cpu();
            cli();
        }
        sei();

        // handle event
        event = 0;
    }
}

However, sometimes the application is sleeping forever. I looked into it a bit and it seems it occurs, if the compiler decides to put the cli instruction directly after the sleep instruction.

After digging a bit in the simavr source files, I found the following code snippet in the file sim_core.h:

static inline void avr_sreg_set(avr_t * avr, uint8_t flag, uint8_t ival) {
	if (flag == S_I) {
		if (ival) {
			if (!avr->sreg[S_I])
				avr->interrupt_state = -2;
		} else
			avr->interrupt_state = 0;
	}

	avr->sreg[flag] = ival;
}

To my understanding negative values for interrupt_state indicate a latency (in cycles) before the interrupt is handled. Waiting for two cycles leads to the situation that the cli is already executed before the interrupt was handled and handling interrupts with cleared SI bit won't work so the interrupt is never handled and my application stays in it's while loop forever. I never observed this behavior on real hardware.

Did I understand anything wrong? What is not clear to me is, why is there a two cycle delay in handling interrupts?

Morpheus1822 avatar May 27 '20 15:05 Morpheus1822

It is a documented fact about interrupts on the AVR. It is also a feature that is used quite a bit by existing code. For example code that switches the stack pointer use that to make it 'interrupt safe'.

Your code can't work without a 'nop' -- the sei(), sleep_cpu() and cli() are all a single cycle as they are intrinsic on the compiler, so you're missing a cycle to make it safe!

buserror avatar May 27 '20 15:05 buserror

Oh, I wasn't aware of that. I thought sei is delayed only one instruction. This is how I understood the ISA description of sei.

I would appreciate if you could point me to the right documentation?

Morpheus1822 avatar May 27 '20 16:05 Morpheus1822

I don't have it handy, but from the code, it was made to skip 2 cycles, not "one instruction". I remember merging a patch years ago about this and I haven't reviewed the code since, but it seems to work fine at the minute, I'm reasonably sure it would break quite a few things if this was out of kilter.

buserror avatar May 27 '20 20:05 buserror

Hey there, I've confirmed that the bug is there. Here's the code I tested with:

#include <avr/io.h>
#include <util/delay.h>
#include <avr/interrupt.h>
#include <avr/cpufunc.h>

ISR(TIMER0_OVF_vect) {
	PORTB |= 1<<5;
}

int main() {
	DDRB |= 1<<5;
	TCCR0 = 1<<CS00;
	TIMSK = 1<<TOIE0;
	_delay_ms(100);

	sei();
	_NOP();
	cli();
	while(1);
}

On real hardware (ATmega8), the LED connected to PB5 is turned on, indicating the interrupt fired fine (as it should according to the datasheet, given there is a nop between sei and cli). On simavr the code never goes there, unless one more nop is added.

Fun fact: at this year's GoogleCTF I've made a task related to this behaviour but accidentally made the solution work only on simulated hardware, causing much confusion (Ctrl-F "two instructions").

akrasuski1 avatar Sep 08 '20 18:09 akrasuski1

Just want to chime in that I believe we should indeed be setting it to -1 instead of -2.

The Klipper firmware uses a very similar style of IRQ handling and will deadlock on sei() nop() cli() unless either a second nop is inserted after the sei(), or the delay is changed to a single cycle.

Klipper has been proven on many AVR driven printers so I should think that's proof positive the behaviour isn't quite correct.

vintagepc avatar Oct 12 '20 21:10 vintagepc