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.