Interrups and Timers
Interrupts
An interrupt is an event which stops the normal execution of your program, makes the CPU go off and do something else (like respond to the condition that caused the interrupt). When the CPU finishes what it needed to do, your program continues execution whereever it left off, as if the interrupt never happened (except, time has passed, and some memory locations could be changed).
All computers have interrupts. If you've ever installed a peripheral device onto a PC, you may have had to select "IRQ" addresses, or have seen the system select them automatically in the case of plug-n-play. "IRQ" just means "interrupt request". Under Linux, you can look at the information in /proc/interrupts to see the active interrupts.
Interrupts allow a system to be much more efficient, and thus faster. I/O devices are usually much much slower than the CPU. If the CPU had to wait for an I/O device to finish each time it used it, your PC would run much slower. By using interrupts to let the device tell the CPU that it is ready for more data, or it has more data for the CPU, or it is finished, the PC can continue doing something else while the device is completing its task.
How do interrupts work? Well, when an interrupt occurs, the CPU hardware recognizes and figures out which interrupt occurred. The CPU expects that there is some program code that must be executed in response to the interrupt (this is the code that will take care of whatever needs to be done). The CPU finds this code by using a table called an interrupt vector; this table is just an array of addresses, where each entry is the address of the code that should be executed in response to the interrupt that is associated with that array index. The AVR ATmega328P (the version on our Arduino boards) interrupt vector is:
Vector # Entry\ Source Description Address
1 0x0000 RESET External Pin, Power-on Reset,
Brown-out Reset and Watchdog System Reset
2 0x0002 INT0 External Interrupt Request 0
3 0x0004 INT1 External Interrupt Request 1
4 0x0006 PCINT0 Pin Change Interrupt Request 0
5 0x0008 PCINT1 Pin Change Interrupt Request 1
6 0x000A PCINT2 Pin Change Interrupt Request 2
7 0x000C WDT Watchdog Time-out Interrupt
8 0x000E TIMER2 COMPA Timer/Counter2 Compare Match A
9 0x0010 TIMER2 COMPB Timer/Counter2 Compare Match B
10 0x0012 TIMER2 OVF Timer/Counter2 Overflow
11 0x0014 TIMER1 CAPT Timer/Counter1 Capture Event
12 0x0016 TIMER1 COMPA Timer/Counter1 Compare Match A
13 0x0018 TIMER1 COMPB Timer/Coutner1 Compare Match B
14 0x001A TIMER1 OVF Timer/Counter1 Overflow
15 0x001C TIMER0 COMPA Timer/Counter0 Compare Match A
16 0x001E TIMER0 COMPB Timer/Counter0 Compare Match B
17 0x0020 TIMER0 OVF Timer/Counter0 Overflow
18 0x0022 SPI, STC SPI Serial Transfer Complete
19 0x0024 USART, RX USART Rx Complete
20 0x0026 USART, UDRE USART, Data Register Empty
21 0x0028 USART, TX USART, Tx Complete
22 0x002A ADC ADC Conversion Complete
The interrupt vector table is always at a fixed location in memory; for the AVR, as can be seen in the second column, the table begins at address 0x0000 in flash (program) memory, and ends at address 0x002B. Reading down the names of the interrupts, you can see that some are directly triggered by external circuitry connected to external pins, some are communication oriented, to handle the "slow" nature of data transfer, there is one for A/D conversion indication, and quite a few related to timers.
The first entry in the table is a psuedo-interrupt in the sense that it doesn't have a current program to get back to; it is the power up condition. Did you ever wonder how a CPU knows what to do when you first turn it on? Well the first entry is it! This RESET interrupt is triggered whenever the CPU is power-reset or software-reset; this entry contains the address of the code that needs executed first when the CPU starts up. In the Arduino environment this code eventually leads to the function main() being called, which is where all C programs start.
Interrupt Processing
The code that is executed in response to an interrupt is known as an interrupt handler or an interrupt service routine (ISR). In assembly code we would write it much like a function, and indeed it is invoked much like a function, using the stack to store the return address, which is the address in the program that was executing where control must go back to when the interrupt handler is finished.
However, since the handler was NOT called as a normal function, we cannot use the RET instruction to return from it; rather there is a special RETI instruction to use, which means "return from interrupt".
When an interrupt handler returns back to the program that was executing, it is very important that no registers have been changed! The program that was executing must have all its data still in place. Some CPU's accomplish this by automatically pushing all registers onto the stack when an interrupt handler is invoked. The AVR processor does not do this; rather it leaves it up to the programmer to save whichever registers it needs to. For an interrupt handler, the normal register usage conventions DO NOT apply -- ALL registers must be treated as "protect if used". Registers are saved just as in procedures, by pushing them on the stack.
THIS INCLUDES the SREG status register! An interrupt handler could actually be invoked in between any two instructions in a program, and this means that it could execute between a comparison instruction and the conditional branch instruction that will use the results of the comparison. So if the interrupt handler needs to execute any instructions that change any flags in the SREG register, it needs to save the current SREG value.
IMPORTANT: Interrupt handlers must always be kept SHORT and QUICK. Many times students will try to write their whole program as a response to some interrupt. The problem is that further interrupts are disabled until the current handler finishes! Thus, interrupt handlers should always be very short pieces of code which simply do something immediately doable and/or just set a flag for later code to check. Your goal always is to return from the interrupt handler as quickly as possible.
Timers
As seen in the above table, quite a few interrupts are related to timers. This is very typical of small processors meant to be used in embedded systems. Think of your microwave oven; almost everything the processor in that system needs to do is associated with keeping track of time.
Using a timer interrupt is essentially the same as setting your alarm clock; you configure the timer to run at a certain rate and then set a specific trigger point, and the interrupt happens when the timer reaches that trigger point. And just like your alarm clock that is automatically reset for the next day, the timer resets and will cause an interrupt the next time it hits the trigger point (and the next, and the next, etc., until it is turned off). In this manner the timer acts like a metronome, giving you a tick (an interrupt) at a very precise and regular interval.
Timers work as what is known as "free running counters"; that is, they are a self-contained circuit and register that can do a "timer++" action at the rate that you specify. Setting a trigger point means setting a value such that when the timer value reaches that value, an interrupt occurs. One natural point to trigger an interrupt is when the timer value reaches its maximum and "overflows" (it wraps around to 0). In the interrupt vector you can see three overflow interrupts.
The AVR has three timers: 0, 1, and 2. Timer 0 and 2 are 8-bit timers, while timer 1 is a 16 bit timer. There are also quite complex mechanisms available to set up timer interrupts, with various comparator setups. We are going to keep it simple and just use the overflow interrupts.
The memory-mapped I/O addresses associated with the timers are somewhat scattered:
- Timer 0 controls are down in the "pure" I/O space, with memory addresses 0x44 to 0x48 (I/O addresses 0x24 to 0x28); all the rest are only in memory space;
- Timer 1 controls are 0x80 to 0x8B;
- Timer 2 controls are 0xB0 to 0xB4;
- All three timer interrupt controls are at 0x6E to 0x70;
Selecting the Timer Rate
The timer speed is selected by three bits for each timer; these bits are called CSn0, CSn1, CSn2 and are the lowest three bits in their respective TCCRnB control registers. The meaning of these bits are:
CSn2 CSn1 CSn0 Description
0 0 0 No clock source (timer stopped) 0 0 1 Clk-I/O (no prescaling) 0 1 0 Clk-I/O / 8 0 1 1 Clk-I/O / 64 1 0 0 Clk-I/O / 256 1 0 1 Clk-I/O / 1024 1 1 0 External on T0 pin, falling edge 1 1 1 External on T0 pin, rising edge
My only problem right now is that I do not know how Clk-I/O differs from the main clock Clk-CPU. The manual clearly refers to them separately but it seems like a table is missing somewhere. I suspect that clock-I/O is the main CPU clock divided by 256, but I'm not sure.
In any case, the most logical setting for us is 101, selecting a divisor of 1024; because the system clock is fairly fast (16MHz), we want to slow it (even through Clk-I/O) down as much as possible.
The only other thing we need to do is enable the interrupt for the timer overflow. This is done in bit 0 of the TIMSKn control registers. Each names bit 0 as TOIE, meaning "timer overflow interrupt enable".
Installing Our Interrupt Handler
Presently we are just doing this in the C/C++ code environment that Arduino supports. There is a nice macro that is pre-defined which allows you to write an interrupt handler very similar to a function. The code looks like:
ISR(interrupt_name)
{
// body of your interrupt handler here
}
The interupt_name must be a valid pre-defined interrupt name, the table below gives the names for the Atmega328P:
INT0_vect
INT1_vect
PCINT0_vect
PCINT1_vect
PCINT2_vect
WDT_vect
TIMER2_COMPA_vect
TIMER2_COMPB_vect
TIMER2_OVF_vect
TIMER1_CAPT_vect
TIMER1_COMPA_vect
TIMER1_COMPB_vect
TIMER1_OVF_vect
TIMER0_COMPA_vect
TIMER0_COMPB_vect
TIMER0_OVF_vect
SPI_STC_vect
USART_RX_vect
USART_UDRE_vect
USART_TX_vect
ADC_vect
EE_READY_vect
ANALOG_COMP_vect
TWI_vect
SPM_READY_vect