Custom ISR Prologue in AVR C

I fell into a situation recently where I was relying on pin change (PCINT) interrupts to decode a serial protocol. This meant I needed to be able to detect if the interrupt was triggered by a rising or falling edge. This particular protocol can have pulses as short as 1uS (8 CPU cycles) so in order to tell if the pin was rising or falling I need to sample it within 8 cycles of it actually happening. Adding time for the pin synchroniser and a non-detirministic ISR entry up to 7 cycles-ish, by the time the ISR starts you’re usually already too late… won’t stop me trying though!

Now I’ll skip the part where you tell me I should just use INT0 or poll or whatever and just look at the case where you do actually want to insert some code before the interrupt service routine (ISR) starts its prologue.
A typical ISR is defined like this:

ISR(PCINT0_vect) {
  //do stuff here
}

When the compiler is building this, it checks all registers your ISR modifies (including modifications by other functions you call) and pushes them to the stack. A dissasembled ISR might look something like

ISR(TIMER0_COMPA_vect) {
0000010B  PUSH R1		Push register on stack 
0000010C  PUSH R0		Push register on stack 
0000010D  IN R0,0x3F		In from I/O location 
0000010E  PUSH R0		Push register on stack 
0000010F  CLR R1		Clear Register 
00000110  PUSH R18		Push register on stack 
00000111  PUSH R19		Push register on stack 
00000112  PUSH R20		Push register on stack 
00000113  PUSH R21		Push register on stack 
00000114  PUSH R22		Push register on stack 
00000115  PUSH R23		Push register on stack 
00000116  PUSH R24		Push register on stack

...actual stuff...

00000150  POP R24		Pop register from stack 
00000151  POP R23		Pop register from stack 
00000152  POP R22		Pop register from stack 
00000153  POP R21		Pop register from stack 
00000154  POP R20		Pop register from stack 
00000155  POP R19		Pop register from stack 
00000156  POP R18		Pop register from stack 
00000157  POP R0		Pop register from stack 
00000158  OUT 0x3F,R0		Out to I/O location 
00000159  POP R0		Pop register from stack 
0000015A  POP R1		Pop register from stack 
0000015B  RETI 		Interrupt return 

This is the ISR burning a bunch of CPU cycles storing the state of whatever you just interrupted, and would be about to clobber otherwise, and then restoring it. This is pretty important, but the typical PUSH takes two clock cycles, and there are a lot of them…

It’s possible to define a ‘naked’ ISR where the compiler doesn’t give you the epi/prologue

ISR(PCINT0_vect, ISR_NAKED) {
  //do stuff here
}

however, now you have to worry about any registers you might be stepping on. Really, you should never use a naked ISR unless you are hand coding assembler.

My decoder is completely interrupt driven, which means my ISR actually does quite a lot, and I would very much like to have it preserve the state of whatever I’m interrupting. However it must sample the pin that changed as the very first thing it does.

Enter the naked top half:

ISR(PCINT0_vect, ISR_NAKED) {
  asm (
    "SBIS %[port], 1\t\n" //Check PINB1 and..
    "RJMP __vector_PCINT0_FALLING\t\n"
    "RETI"
    :: [port] "I"(_SFR_IO_ADDR(PINB)) :
  );
}

and the not so naked bottom half:

ISR(__vector_PCINT0_FALLING) {
  //do stuff
}

The top half here is built by the compiler and installed as the ISR for the PCINT interrupt we care about. Being naked, the only code it runs is the assembly you see here. It samples PINB, checks if the pin is high or low and then jumps to the bottom half which performs the rest of the ISR. If we jumped, then we can rely on the bottom half to RETI for us (also why we don’t ‘call’ the bottom half), but if we didn’t jump then we need to clean up the ISR ourselves with a RETI. We could also define another bottom half for the rising edge and replace the RETI with an RJMP to it if we care about both events. Finally, the name of the bottom half should start with “__vector_*” to stop GCC complaining.

This works because you can put any name you like in an ISR() definition. GCC will check all of its clobbering and generate the epi/prologue regardless of wether the ISR is attached to an actual interrupt vector. Once it’s created that for us, we can hand code the top half however we like and JMP knowing the the compiler has taken care of the hard stuff coming later. Though you should be careful not to clobber stuff in the top half.

Leave a Reply

Your email address will not be published. Required fields are marked *