Interrupts¶
Read time: 31 minutes (7997 words)
Polling to check a signal works, but the processor still needs to check things periodically. This puts a load on the chip we really do not need. What we really need is a way to set an alarm that will go off when we need to react to something, and that is why interrupts were invented!
What is an interrupt?¶
Most computer systems support the idea of an interrupt. Basically, an interrupt is just a signal generated by some device and sent to the processor. These events happen at unpredictable times. The source of the interrupt can be external or internal. The AVR can sense signals though the I/O pins on the chip Internal devices can generate them as well, and that is what we will be examining in this lecture.
We will set up Timer0
so it generates an interrupt when it rolls over.
Asynchronous events¶
Interrupts are asynchronous events, meaning we do not know exactly what we will be doing when they happen. The chip handles interrupts using something like a procedure call. The bad thing is that this call might happen in the middle of your code, not at some point where you would be happy to take a break!
We need to preserve the state of the chip before dealing with the event. When we return, the original code should not know this happened!
Recognizing the interrupt¶
We can turn all interrupts on or off with code. The AVR has a Global
Interrupt Enable
flag which basically shuts off the entire interrupt system.
Every internal device that can generate an interrupt has an enable flag We need to set all these bits correctly for the interrupt system to work.
Controlling the global interrupt system¶
If allowing interrupts might cause problems, we can do this:
CLI
- disable interrupts
SEI
- enable interrupts
The processor is initialized on power-up with interrupts disabled.
Handling the interrupt¶
Basically, the interrupt is handled by a special procedure call. It happens
between two instructions right after the event. We need to set up code for the
procedure at specific addresses associated with the particular interrupt. The
interrupt system will make sure that the correct interrupt procedure is called
for that particular device. We need as many handlers
as we have interrupt
sources.
AVR Interrupt table¶
To handle all potential sources of interrupts, the AVR sets up a jump
table
, also called an interrupt vector table
. This table starts at
address 0x00. If we are not using the interrupt system, we cna eliminate this
table by providing a special flag to the compiler.
Note
I added a way to decide if you want to use the interrupt system in your AVR projects. Just add a line in your Makefile that looks like this:
NOINTS := TRUE
In avr-build.mk
make these changes:
ifeq ($(NOINTS), TRUE)
LFLAGS += -nostartfiles
endif
The original Makefile system always eliminated this table, meaning the interrupt projects will not work properly!
Each entry in this table is just a jump to the actual procedure code needed. We
only need entries at places where we want to handle specific interrupts. The
other jump table locations could be left blank, but the compiler generates a
table with a jump to location zero for any interrupt that you do not use. The
zero address is reserved for the reset interrupt
which directs the
processor to the start of your program!
New style AVR code¶
To get things working with avr-gcc
, we need to change code a bit. The
linker will set up the interrupt vector table
. Unfortunately, some simple
code becomes not so simple We will use macros
to make things work
correctly! avr-gcc
will set up the chip!
Interrupt handler code¶
The actual handler code looks like other procedures, except this one ends with a new instruction:
InterruptHandler:
...
reti
The last instruction is vital. It resets the interrupt system after each interrupt is recognized.
The Reset Vector¶
One special signal related to the interrupts, but is a bit different. This one
happens when powering up of the processor. Some systems have a reset
button, which directs the processor to the reset
handler. avr-gcc
will
set this up so your main
entry point gets called on reset
.
Saving Processor State¶
We need to save the processor state in our handlers. The question is where to save this data! I know, use the stack!
We need to be careful here, not to save too much, or too little. Save any
registers you intend to use. But we need to also pay attention to any other
registers the user might be using. For instance, we need to save the system flag
register SREG
as well. The interrupted code will thank you!
Using interrupts with Timer0¶
Let’s put this all together with a simple example.
Our polling code checked the Timer0 Overflow (TOV0)
interrupt flag. This
flag was being set by the timer, but did not generate an interrupt. In fact, we were
running with interrupts disabled!
To generate an interrupt, we need to reprogram the timer (and chip). We will use the blinking light for this example. Again, we want the LED to blink once per second
Sample program¶
This program will consist of a main routine and timer code in separate files
#include "config.h"
.extern Timer0Setup
.global main
.section .text
main:
; set up the system clock
ldi r24, 0x80 ; set up prescaler
sts CLKPR, r24
sts CLKPR, r1 ; set to full speed
; set up LED port
sbi _(DDRB), 5 ; set up the output port (bit 6)
cbi _(PORTB), 5 ; start off with the LED off
As usual, project configurarion details are defined in config.h
.
This code does the normal processor setup, and configures the LED so we can make it blink.
The interrupt jump table¶
Interrupts are managed using a “jump table” in low memory. This table will be
set up by avr-gcc
during the link step i building youe applicarion. We
need to declare labels defined in the include files for this chip to set things
up properly, something we do in timer.S
.
.global TIMER0_OVF_vect
TIMER0_OVF_vect:
The linker will place a jump to this routine in the table at the right spot
Finishing up¶
Back in our main program, we cna continue to do whatever work we need. In this example, we will do nothing! (All the magic happens in the interrupt code!)
call Timer0Setup ; initialze the timer
1: rjmp 1b
Huh? Where is the work going to happen? In the handler!
In this simple example, we really have no work for the program to do, other than what will happen when interrupts occur. For that reason, we simply put the main code in an infinite lop. The interrupts will happen, and the processor will take care of those events with the code we provide.
Timer code¶
timer.S
starts up with this code
; timer.S - Timer0 code for blink
#include "config.h"
.global Timer0Setup
.section .data
ISRcount: .byte 0
#define MAX_ISR 61
We will discuss the ISRcount
data item later.
Timer setup¶
Set up the timer prescaler value here
.section .text
;----------------------------------------------------------------------
; Initialize Timer 0 for interrupts
;
Timer0Setup:
in r16, _(TCCR0B)
ori r16, (1 << CS02) | (1 << CS00) ; divide by 1024
out _(TCCR0B), r16 ; set timer clock
Enabling interrupts¶
One simple line tells the timer module to generate an interrupt signal when overflow happens. The same flag we watched for the buzzer project is being used, but now the processor will be notified when overflow happens.
out _(TCCR0B), r16 ; set timer clock
sbi _(TIFR0), (1<< TOV0); clear interrupt flag
;
lds r16, TIMSK0 ; get interrupt mask reg
ori r16, (1 << TOIE0) ; enable interrupts
sts TIMSK0, r16
out _(TCNT0), r1 ; zero the timer counter
sts ISRcount, r1 ; and our counter variable
;
sei ; let the fun begin
ret
The sei
line turns on the interrupt system, and we are now ready for the
timer interrups, except we need to show the handler code!
The handler code¶
Finally, we need our handler code:
;----------------------------------------------------------------------
; Timer0 overflow ISR
;
.global TIMER0_OVF_vect
TIMER0_OVF_vect:
; save callers registers
push r1
push r0
in r0, _(SREG)
push r0
eor r1, r1
push r24
push r25
;
This code protects the important registers in the chip, and any registers we plan on using in our code.
Do the work¶
We let the handler toggle the LED on/off
; toggle LED port
in r24, _(PORTB) ; get current PORTD
ldi r25, (1 << LED_PIN) ; LED bit position
eor r24, r25 ; toggle bit
out _(PORTB), r24 ; set back in place
Finishing up¶
Finally, we restore the system state
; recover user's registers
pop r25
pop r24
pop r0
out _(SREG), r0
pop r0
pop r1
reti
Wow - we are blinking fast¶
- The above code blinks about 61 times per second.
Let’s try a simple trick.
Create a simple counter variable. We will have the interrupt handler increment the counter each time it is called. We will let this counter count up to 61, then trigger our LED toggle code. We will then reset the counter as we toggle the LED and start over. With any luck, we will end up with a blink every second.
counter setup¶
We need a counter variable
.section .data
ISRcount: .byte 0
#define MAX_ISR 61
This was shown earlier.
We need to set the counter in the setup code
out _(TCNT0), r1 ; zero the timer counter
sts ISRcount, r1 ; and our counter variable
All we do here is zero the counter using our handy zero in r1
.
Adding the count logic¶
In the handler, add this code to increment the counter
; bump the ISR counter
lds r24, ISRcount ; get current count
inc r24 ; add one
sts ISRcount, r24 ; put it back
;
Blinking only when the count is reached¶
; test the counter to see if we toggle the LED
lds r24, ISRcount ; needed?
cpi r24, MAX_ISR ; one second is 61 interrupts
brcs 1f ; skip if not
;
The label (1
) is after the blink logic, just before we restore all the
registers and end the handler.
Resetting the count on toggle¶
The last thing we do is reset the counter after toggling the LED
out _(PORTB), r24 ; set back in place
sts ISRcount, r1 ; sero the counter
Notice the line that resets the counter for the next pass.
This now blinks (toggles) once per second!
This simple scheme to delay actions until some number of interrupts is seen is a simple mechanism to adjust when events are handled. We will use it later, when we explore a simple multi-tasking kernel for AVR projects.
Single Makefile¶
Since the Makefile
setup we used for the simulator project was really designed
to build example code to run in that simulator, it is not ideal for just
building simple AVR code. So, I am providing a simple Makefile you can drop
into any folder with the required AVR source code files, and it should build
just fine. Be sure to make the needed changes so you are using the right PORT
and chip. The Makefile
is set up to run on my Mac at the moment:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | # source files
TARGET := $(shell basename $(PWD))
CSRCS := $(wildcard *.c)
COBJS := $(CSRCS:.c=.o)
SSRCS := $(wildcard *.S)
SOBJS := $(SSRCS:.S=.o)
OBJS := $(COBJS) $(SOBJS)
LST := $(TARGET).lst
# define the processor here
MCU := atmega328p
FREQ := 16000000L
# define the USB port on your system
#PORT := /dev/ttyACM0
PORT := /dev/tty.usbmodem14101
PGMR := arduino
# tools
GCC := avr-gcc
OBJDUMP := avr-objdump
OBJCOPY := avr-objcopy
DUDE := avrdude
UFLAGS := -v -D -p$(MCU) -c$(PGMR)
UFLAGS += -P$(PORT)
UFLAGS += -b115200
CFLAGS := -c -Os -mmcu=$(MCU)
CFLAGS += -DF_CPU=$(FREQ)
LFLAGS += -mmcu=$(MCU)
.PHONY all:
all: $(TARGET).hex $(LST)
# implicit build rules
%.hex: %.elf
$(OBJCOPY) -O ihex -R .eeprom $< $@
%.elf: $(OBJS)
$(GCC) $(LFLAGS) -o $@ $^
%.o: %.c
$(GCC) -c $(CFLAGS) -o $@ $^
%.o: %.S
$(GCC) -c $(CFLAGS) -o $@ $<
%.lst: %.elf
$(OBJDUMP) -C -d $< > $@
# utility targets
.PHONY: load
load:
$(DUDE) $(DUDECONF) $(UFLAGS) -Uflash:w:$(TARGET).hex:i
.PHONY: clean
clean:
$(RM) *.hex *.lst *.o *.elf
|
Use this Makefile to run this project, and the final project as well. Both of
those use interrupts. It would work for the Blink project if you add the
-nostartfiles
flag to the LFLAGS
variable.