Pardon The Interruption
The previous tutorial covered the basics of writing inline functions. A close relative of the function is the Interrupt Service Routine (ISR), which is the topic here. Portions of this tutorial may pertain to functions as well.
As a warning, this tutorial assumes an understanding of the basic concepts of interrupts in general, and specifically interrupt handlers on the arduino (AVR μC). Hopefully, you have already written a few arduino interrupts in C, using the internal arduino functionality. If not, you may want to study some of the links given in the reference section of this tutorial before continuing.
The Deck is Stacked
Basic knowledge of the stack is essential to understanding functions and interrupt handlers. The basic purpose of the stack is to support function calls and interrupts. Whenever a program makes a function call or whenever an interrupt occurs, the stack is used to store critical information which will be restored upon completion of the function or interrupt. Additional information on the stack can be found here and here.
First and primary, during a function call or interrupt, the hardware places the return address on the stack. The saving and restoration of the return address is accomplished transparently by the CALL and RET instructions. It is not necessary to perform any special instruction(s) to make this occur.
Second, if any “call-saved” registers will be “clobbered” inside the function, these registers are “pushed” onto the stack. In the case of an interrupt service routine, all of the registers used inside the ISR (and always the temporary and zero registers, r0 and r1) get pushed onto the stack. Additionally, during an ISR the SREG is saved and restored.
Finally, if the compiler deems it necessary, space is reserved for any local variables on the stack. Many times the compiler will place local variables into specific registers, and therefore doesn’t use the stack for temporary storage.
Here is an example of how the compiler uses the stack to store local variables inside of a function. This is sometimes referred to as “setting up a stack frame.” We will reserve 16 bytes for a character array (note: unrelated code has been removed for the purpose of clarity). The compiler performs all of this stack manipulation for us behind the scenes, so-to-speak:
void example(void) { char buffer[16]; //space will be reserved on the stack // //do something here. . . // }
Result in this machine code:
;prologue PUSH r28 ;save registers on stack PUSH r29 IN r28, SPL ;get stack pointer IN r29, SPH SBIW r28, 16 ;reserve 16 bytes space on stack ;the stack grows downward, hence the subtraction OUT SPH, r29 ;update new stack pointer OUT SPL, r28 ; ;do something here. . . ; ;epilogue ADIW r28, 16 ;remove the 16 bytes from the stack OUT SPH, r29 ;restore stack pointer OUT SPL, r28 POP r29 ;restore registers from stack POP r28 RET }
Upon return from the interrupt or function, all the preserved values are restored, or “popped” from the stack. Obviously, during the pro and epilogue code, the order of the push and pop instructions is very critical.
Interrupt Before and After
Below, I wrote a very basic interrupt routine that simply increments a byte so we can examine the prologue and epilogue code generated by the compiler:
//here is an example ISR coded in C: volatile uint8_t a; ISR(INT0_vect) { a++; } //this is the generated assembly code: ;prologue 0000027F 1f.92 PUSH r1 ;save r1 register 00000280 0f.92 PUSH r0 ;save r0 register 00000281 0f.b6 IN r0, SREG ;get status register 00000282 0f.92 PUSH r0 ;save sreg 00000283 11.24 CLR r1 00000284 8f.93 PUSH r24 ;save r24 register ;increment byte (a) here 00000285 80.91.c3.01 LDS r24, (a) 00000287 8f.5f SUBI r24, 0xFF 00000288 80.93.c3.01 STS (a), r24 ;epilogue 0000028A 8f.91 POP r24 ;restore r24 register 0000028B 0f.90 POP r0 ;restore status register 0000028C 0f.be OUT SREG, r0 0000028D 0f.90 POP r0 ;restore r0 register 0000028E 1f.90 POP r1 ;restore r1 register 0000028F 18.95 RETI ;return from interrupt
As you can see, the meat of the ISR is only 10 bytes long. However, together the prologue and epilogue add another 24 bytes, for a total of 34. It might be possible to save a few bytes and program cycles by tightly writing your own ISR pro and epilogue. GCC has a provision which allows writing your own pro and epilogues, which will be covered later.
We Interrupt This Program to Blink
It is now time to write an interrupt handler, or ISR in inline assembler. I can’t think of a better example than to adapt the basic Blink sketch to use the Timer #1 Overflow interrupt. Please note, because this code alters the Timer #1 registers, it will render any use of the arduino Timer #1 as nonfunctional (i.e. analogWrite pins 9 & 10, the Servo Library, etc.).
Handle It
The first order of business is to write the interrupt handler for the Timer #1 Overflow. This is the routine that is called when the Timer #1 counter (TCNT1) rolls over from 0xffff to zero. Our the ISR is very basic, and as always, it should be kept as short as possible. Inside the handler we perform two functions:
- Reset the counter (TCNT1) allowing the next overflow to reoccur at 1 second intervals.
- Toggle the LED.
An ISR can be coded using inline assembler just as in a “C Stub Function”, relying upon the compiler to insert the necessary prologue and epilogue code. I suggest you use this stub technique at first before graduating to writing the entire “naked” ISR. Here is a stub version of our ISR:
#define TCNT_BASE 0x0bdc #define TCNT_BASE_H (((TCNT_BASE)>>8)&0xff) #define TCNT_BASE_L ((TCNT_BASE)&0xff) ISR(TIMER1_OVF_vect) { asm ( //reload TCNT1 counter for 1sec interrupt "ldi r24, %3 \n" "st Z+, r24 \n" //TCNT1L "ldi r24, %4 \n" "st Z, r24 \n" //TCNT1H //toggle LED "in __tmp_reg__, %0 \n" //read port "ldi r24, %1 \n" //LED bit mask "eor __tmp_reg__, r24 \n" //toggle LED bit "out %0, __tmp_reg__ \n" //write port : : "I" (_SFR_IO_ADDR(PORTB)), "I" (_BV(PORTB5)), "z" (_SFR_MEM_ADDR(TCNT1)), "M" (TCNT_BASE_L), "M" (TCNT_BASE_H) : "r24" ); }
Having said all that, the boilerplate code the compiler inserts is not always the most efficient, and many times inadequate. For these reasons, and for the academic exercise, we will also select the “ISR_NAKED” attribute when defining the ISR. This gives us full control over all of the code inside the ISR. Full control is a good thing:
ISR(TIMER1_OVF_vect, ISR_NAKED)
Eleven instructions encompass the prologue and epilogue, which is more than the code required for the main purpose of the interrupt. Notice inside the handler, we utilize 3 registers, r24, r30 and r31. This means we need to preserve the content of these registers since the interrupt could be triggered at any time, even precisely when these registers may be in use. Additionally we need to preserve the status register (SREG). The SREG holds critical information on the state of the program when the interrupt fired. Neglecting to reserve any of this information would probably cause the program to crash.
Don’t forget to include the terminating RETI instruction also. By comparison, this ISR_NAKED version is 10 bytes shorter than the “Stub” version:
#include "k328p.h" #define TCNT_BASE 0x0bdc #define TCNT_BASE_H (((TCNT_BASE)>>8)&0xff) #define TCNT_BASE_L ((TCNT_BASE)&0xff) ISR(TIMER1_OVF_vect, ISR_NAKED) { asm ( "push r31 \n" //save r30, r31 contents "push r30 \n" "push r24 \n" //preserve SREG "in r24, __SREG__ \n" "push r24 \n" //reload TCNT1 counter for 1sec interrupt "clr r31 \n" "ldi r30, %2 \n" "ldi r24, %3 \n" "st Z+, r24 \n" //TCNT1L "ldi r24, %4 \n" "st Z, r24 \n" //TCNT1H //toggle LED "in r30, %0 \n" //read port "ldi r31, %1 \n" //LED bit mask "eor r30, r31 \n" //toggle LED bit "out %0, r30 \n" //write port //restore old SREG "pop r24 \n" "out __SREG__, r24 \n" //restore r30, r31 "pop r24 \n" "pop r30 \n" "pop r31 \n" "reti \n" : : "I" (kPORTB), "I" (_BV(PORTB5)), "M" (kTCNT1), "M" (TCNT_BASE_L), "M" (TCNT_BASE_H) ); }
The initiation code required for the Timer #1 interrupt (setting the prescaler, loading the counter and enabling the overflow interrupt) is completely contained inside the Setup function. Obviously, it is not necessary to write this in inline assembly, it’s just good practice:
#include "k328p.h" #define TCNT_BASE 0x0bdc #define TCNT_BASE_H (((TCNT_BASE)>>8)&0xff) #define TCNT_BASE_L ((TCNT_BASE)&0xff) void setup() { uint16_t TNCTBase = TCNT_BASE; asm ( "cli \n" //disable gloal interrupts "sbi %0, %1 \n" //pinMode(13, OUTPUT); //set 256 prescale (CS12) "st Z+, __zero_reg__ \n" //TCCR1A "ldi r24, %3 \n" "st Z+, r24 \n" //zero TCCR1B "st Z, __zero_reg__ \n" //zero TCCR1C //load counter for 1sec interrupt "ldi r30, %4 \n" "st Z+, %A5 \n" //TCNT1L "st Z, %B5 \n" //TCNT1H //enable overflow interrupt "ldi r30, %6 \n" "ldi r24, %7 \n" "st Z, r24 \n" //TIMSK1 "sei \n" //enable global interrupts : : "I" (_SFR_IO_ADDR(DDRB)), "I" (PORTB5), "z" (_SFR_MEM_ADDR(TCCR1A)), "I" (_BV(CS12)), "M" (kTCNT1), "r" (TNCTBase), "M" (kTIMSK1), "I" (_BV(TOIE1)) : "r24", "memory" ); } void loop() { }
Finally, we are introducing a new header file “k328p.h” (contents listed below) which contains all of the IO register defines in such a way that we can use them inside our inline assembly routines. The definitions in this file use the same standard ATMEL mnemonics for the IO registers with the letter ‘k’ pre-pended. They are the LSB of the IO register address, and allow greater flexibility in inline assembler code when referring to the IO registers (when using pointer registers with the LD/ST instructions). A close examination of the above code will reveal the method of use.
Arduino IO Register Defines
//k328p.h - definitions for ATmega328P //4.4.2016 #ifndef _k328P_H_ #define _k328P_H_ //standard registers //0-0x1f: bit addressable //0-0x3f: IN/OUT compatible //0-0x3f: add 0x20 when using LD/ST #define kPINB 0x03 #define kDDRB 0x04 #define kPORTB 0x05 #define kPINC 0x06 #define kDDRC 0x07 #define kPORTC 0x08 #define kPIND 0x09 #define kDDRD 0x0A #define kPORTD 0x0B #define kTIFR0 0x15 #define kTIFR1 0x16 #define kTIFR2 0x17 #define kPCIFR 0x1B #define kEIFR 0x1C #define kEIMSK 0x1D #define kGPIOR0 0x1E #define kEECR 0x1F //end bit addressable #define kEEDR 0x20 #define kEEAR 0x21 #define kEEARL 0x21 #define kEEARH 0x22 #define kGTCCR 0x23 #define kTCCR0A 0x24 #define kTCCR0B 0x25 #define kTCNT0 0x26 #define kOCR0A 0x27 #define kOCR0B 0x28 #define kGPIOR1 0x2A #define kGPIOR2 0x2B #define kSPCR 0x2C #define kSPSR 0x2D #define kSPDR 0x2E #define kACSR 0x30 #define kMCUSR 0x34 #define kMCUCR 0x35 #define kSPMCSR 0x37 #define kSPL 0x3D #define kSPH 0x3E #define kSREG 0x3F //end IN/OUT compatible //extended registers begin #define kWDTCSR 0x60 #define kCLKPR 0x61 #define kPRR 0x64 #define kOSCCAL 0x66 #define kPCICR 0x68 #define kEICRA 0x69 #define kPCMSK0 0x6B #define kPCMSK1 0x6C #define kPCMSK2 0x6D #define kTIMSK0 0x6E #define kTIMSK1 0x6F #define kTIMSK2 0x70 #define kADC 0x78 #define kADCW 0x78 #define kADCL 0x78 #define kADCH 0x79 #define kADCSRA 0x7A #define kADCSRB 0x7B #define kADMUX 0x7C #define kDIDR0 0x7E #define kDIDR1 0x7F #define kTCCR1A 0x80 #define kTCCR1B 0x81 #define kTCCR1C 0x82 #define kTCNT1 0x84 #define kTCNT1L 0x84 #define kTCNT1H 0x85 #define kICR1 0x86 #define kICR1L 0x86 #define kICR1H 0x87 #define kOCR1A 0x88 #define kOCR1AL 0x88 #define kOCR1AH 0x89 #define kOCR1B 0x8A #define kOCR1BL 0x8A #define kOCR1BH 0x8B #define kTCCR2A 0xB0 #define kTCCR2B 0xB1 #define kTCNT2 0xB2 #define kOCR2A 0xB3 #define kOCR2B 0xB4 #define kASSR 0xB6 #define kTWBR 0xB8 #define kTWSR 0xB9 #define kTWAR 0xBA #define kTWDR 0xBB #define kTWCR 0xBC #define kTWAMR 0xBD #define kUCSR0A 0xC0 #define kUCSR0B 0xC1 #define kUCSR0C 0xC2 #define kUBRR0 0xC4 #define kUBRR0L 0xC4 #define kUBRR0H 0xC5 #define kUDR0 0xC6 //end extended registers //0-0x3f for LD/ST instructions #define k2PINB 0x23 #define k2DDRB 0x24 #define k2PORTB 0x25 #define k2PINC 0x26 #define k2DDRC 0x27 #define k2PORTC 0x28 #define k2PIND 0x29 #define k2DDRD 0x2A #define k2PORTD 0x2B #define k2TIFR0 0x35 #define k2TIFR1 0x36 #define k2TIFR2 0x37 #define k2PCIFR 0x3B #define k2EIFR 0x3C #define k2EIMSK 0x3D #define k2GPIOR0 0x3E #define k2EECR 0x3F #define k2EEDR 0x40 #define k2EEAR 0x41 #define k2EEARL 0x41 #define k2EEARH 0x42 #define k2GTCCR 0x43 #define k2TCCR0A 0x44 #define k2TCCR0B 0x45 #define k2TCNT0 0x46 #define k2OCR0A 0x47 #define k2OCR0B 0x48 #define k2GPIOR1 0x4A #define k2GPIOR2 0x4B #define k2SPCR 0x4C #define k2SPSR 0x4D #define k2SPDR 0x4E #define k2ACSR 0x50 #define k2MCUSR 0x54 #define k2MCUCR 0x55 #define k2SPMCSR 0x57 #define k2SPL 0x5D #define k2SPH 0x5E #define k2SREG 0x5F #endif //_k328P_H_
References
Arduino Interrupts
Newbie’s Guide to AVR Interrupts
PJRC Guide to Interrupts
AVR Libc Information on Interrupts
University of Maryland, BC, C Programming and Embedded Systems Course, Interrupt Information
AVR 8-bit Instruction Set
AVR-GCC Inline Assembler Cookbook
Extended Asm – Assembler Instructions with C Expression Operands
Mixing C and Assembly Language
ATMEL ATmega328P Datasheet
Also available as a book, with greatly expanded coverage!