Cleaning up Interrupt Code

When trying to work with interrupts, most example code is written in C, and the compiler takes care of building the interrupt vector jump table that needs to be constructed starting at address 0x0000. When we work with assembly language, asking the compiler to do this setup gets messy.

The C compiler has access to all the required chip definitions by using this include in your code:

#include <avr/io.h>

If we tell the compiler what chip we re using, which we are doing using the Makefile system we set up earlier, then the right chip definition file will automatically be included when you process your code. We do not need to use the atmega328p.def file. This is nice, since we really do not want to manage all the definitions. Fortunately, this trick works in our assembly language files as well. Let’s see if we can get the compiler to build the interrupt table we need for our assembly language projects.

AVR Interrupt code

Here is the smallest test code I could set up to see if we can get the interrupt table set up right.

main.S:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
; main.S - avr-gcc assembly language

#include <avr/io.h>

        .section    .text

        .global main
        .org    0x0000
main:
1:
        rjmp    1b

Notice that I have included the header file shown above in this code. We need that include whenever we access a register other than the standard r0-r31 set.

Here is the timer code:

timer.S:

1
2
3
4
5
6
7
8
; timer.S - Timer1 code for interrupt blink-buzz
#include <avr/io.h>
        .section    .text

        .global TIMER1_COMPA_vect
TIMER1_COMPA_vect:
        reti

Simplified Makefile Set

For this test, I am going to replace the standard Makefile system we use in previous labs with a simplified one for testing. Copy your AVRtools.mak file into the directory where you set up the above two files, then add this file:

AVRmaster.mak:

 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
F_CPU		=	16000000
FORMAT      =   ihex
include     AVRtools.mak

CFLAGS		= -Wall -Os -DFU_CPU=$(F_CPU) -mmcu=$(MCU)
AFLAGS		= $(CFLAGS) 

OBJS        =   $(CSRCS:%.c=%.o) $(ASRCS:%.S=%.o)

all:        $(TARGET).hex

%.hex:      %.elf
			$(OBJCOPY) -O $(FORMAT) \
				-R .eeprom \
				-R .fuse \
				-R lock \
				-R .signature $< $@

%.elf:      $(OBJS)
			$(GCC) $(AFLAGS) $^ -o $@

%.lss:		%.elf
			$(OBJDUMP) -h -S $< > $@

# build objects from C files
%.o:        %.c
			$(GCC) -c $(CFLAGS) $< -o $@

%.o:        %.S
			$(GCC) -c $(AFLAGS) $< -o $@

flash:
	$(AVRDUDE) -C$(AVRDUDE_CONF) -v -p$(MCU) -carduino -P$(PORT) -b115200 -D -Uflash:w:$(TARGET).hex:i

clean:
			$(RM) -f *.o
			$(RM) -f *.hex
			$(RM) -f *.lst
			$(RM) -f *.lss
			$(RM) -f *.map

            

This is a simplified version of the file we have been using, with most of the optimization settings removed. I want to keep this test as clean as possible.

Your project Makefile should look like this:

Makefile:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
TARGET  =   test

MCU		= atmega328p
PORT	= /dev/cu.usbmodem1421

# list AVR assembly language source files here (.S extensions)
ASRCS    =  main.S timer.S

# DO NOT MODIFY ANYTHING BELOW THIS LINE ----------------------------
include AVRmaster.mak

Building the Code

Let’s see what the compiler does with this code.

$ make
$ make test.lss

You should see no errors when you build the program. That second command generates a disassembly file by processing the .elf file using avr-objdump. Here is the file it produced:

 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
62
63
64

test.elf:     file format elf32-avr

Sections:
Idx Name          Size      VMA       LMA       File off  Algn
  0 .data         00000000  00800100  00000088  000000fc  2**0
                  CONTENTS, ALLOC, LOAD, DATA
  1 .text         00000088  00000000  00000000  00000074  2**1
                  CONTENTS, ALLOC, LOAD, READONLY, CODE

Disassembly of section .text:

00000000 <__vectors>:
   0:	0c 94 34 00 	jmp	0x68	; 0x68 <__ctors_end>
   4:	0c 94 3e 00 	jmp	0x7c	; 0x7c <__bad_interrupt>
   8:	0c 94 3e 00 	jmp	0x7c	; 0x7c <__bad_interrupt>
   c:	0c 94 3e 00 	jmp	0x7c	; 0x7c <__bad_interrupt>
  10:	0c 94 3e 00 	jmp	0x7c	; 0x7c <__bad_interrupt>
  14:	0c 94 3e 00 	jmp	0x7c	; 0x7c <__bad_interrupt>
  18:	0c 94 3e 00 	jmp	0x7c	; 0x7c <__bad_interrupt>
  1c:	0c 94 3e 00 	jmp	0x7c	; 0x7c <__bad_interrupt>
  20:	0c 94 3e 00 	jmp	0x7c	; 0x7c <__bad_interrupt>
  24:	0c 94 3e 00 	jmp	0x7c	; 0x7c <__bad_interrupt>
  28:	0c 94 3e 00 	jmp	0x7c	; 0x7c <__bad_interrupt>
  2c:	0c 94 41 00 	jmp	0x82	; 0x82 <__vector_11>
  30:	0c 94 3e 00 	jmp	0x7c	; 0x7c <__bad_interrupt>
  34:	0c 94 3e 00 	jmp	0x7c	; 0x7c <__bad_interrupt>
  38:	0c 94 3e 00 	jmp	0x7c	; 0x7c <__bad_interrupt>
  3c:	0c 94 3e 00 	jmp	0x7c	; 0x7c <__bad_interrupt>
  40:	0c 94 3e 00 	jmp	0x7c	; 0x7c <__bad_interrupt>
  44:	0c 94 3e 00 	jmp	0x7c	; 0x7c <__bad_interrupt>
  48:	0c 94 3e 00 	jmp	0x7c	; 0x7c <__bad_interrupt>
  4c:	0c 94 3e 00 	jmp	0x7c	; 0x7c <__bad_interrupt>
  50:	0c 94 3e 00 	jmp	0x7c	; 0x7c <__bad_interrupt>
  54:	0c 94 3e 00 	jmp	0x7c	; 0x7c <__bad_interrupt>
  58:	0c 94 3e 00 	jmp	0x7c	; 0x7c <__bad_interrupt>
  5c:	0c 94 3e 00 	jmp	0x7c	; 0x7c <__bad_interrupt>
  60:	0c 94 3e 00 	jmp	0x7c	; 0x7c <__bad_interrupt>
  64:	0c 94 3e 00 	jmp	0x7c	; 0x7c <__bad_interrupt>

00000068 <__ctors_end>:
  68:	11 24       	eor	r1, r1
  6a:	1f be       	out	0x3f, r1	; 63
  6c:	cf ef       	ldi	r28, 0xFF	; 255
  6e:	d8 e0       	ldi	r29, 0x08	; 8
  70:	de bf       	out	0x3e, r29	; 62
  72:	cd bf       	out	0x3d, r28	; 61
  74:	0e 94 40 00 	call	0x80	; 0x80 <main>
  78:	0c 94 42 00 	jmp	0x84	; 0x84 <_exit>

0000007c <__bad_interrupt>:
  7c:	0c 94 00 00 	jmp	0	; 0x0 <__vectors>

00000080 <main>:
  80:	ff cf       	rjmp	.-2      	; 0x80 <main>

00000082 <__vector_11>:
  82:	18 95       	reti

00000084 <_exit>:
  84:	f8 94       	cli

00000086 <__stop_program>:
  86:	ff cf       	rjmp	.-2      	; 0x86 <__stop_program>

Compiler Results

Since we let the compiler use its normal include files, the actual assembly language generated includes some of the chip setup work we were doing ourselves in our earlier work. Look at the first chink of code on lines 14-39.

This is the interrupt jump table. Look at line 25. This is the entry that jumps to our TIMER_COMPA_vect routine. All the other entries are filled in with a jump to a dummy handler that simply restarts the code. This will handle any spurious interrupts that might happen (although we do not expect this).

At location 0x0000, we see a jump to a block of code that should look familiar. The code beginning at line 42 sets register r1 to zero, and initializes the stack pointer so subroutines will work properly. All we are left to do to complete initialization is to program the system clock as we wish, then set up any devices we plan to use. Following this compiler generated setup code, we call our original main code and we are off and running!

Writing Interrupt Code

We still need to deal with that weird I/O register issue, though. Since we are letting the compiler work with our code, the register definitions for those registers within that special zone between 0x0020 and 0x005F are not the “corrected” values I placed in our old atmega328p.d.e.f file. We are not using this file in this code, so we are stuck. We we need to use the __SFR_IO_ADDR macro on any registers in the special address region.

Since so much example code on the Internet just uses this macro, we will as well. I am not happy with this since is “clutters” up the code a bit, but I have not found a clean approach I like yet!

Bottom line. Whenever you reference any register whose address is within the range 0x0020 - ox005F, use the macro. (You can figure out what registers are involved by scanning the summary datasheet I provided in an earlier lecture).