Wednesday, July 9, 2014

Writing AVR interrupt service routines in assembler with avr-gcc

For writing AVR assembler code, there are two free compilers to choose from.  The Atmel AVR Assembler and the Gnu assembler.  While they both support the same instruction set, there are some differences in assembler syntax that differ between the two.  The assembler is included with gcc packages like the Atmel AVR toolchain, so if you're already writing AVR programs in C or C++, then you don't need to install anything extra in order to start writing in assembler.

Documents you should refer to for writing interrupts are the avr-libc manual entry for interrupt.h and the data sheet for the AVR MCU you are using.  I'll be using an Arduino Pro Mini clone to write a timer interrupt, which uses the Atmega328p MCU.

The purpose of the ISR I'm wiring is to maintain a system clock that counts each second.  I'll use timer/counter2, which supports clocking off an external 32kHz watch crystal or the system clock oscillator.  For now I'll write the code based on running off the Pro Mini's 16Mhz external crystal.

With a 16Mhz system clock and a 8-bit timer, it's impossible to generate an interrupt every second.  Using a prescaler of 256 and a counter reload at 250, an interrupt will be generated every 4ms, or 250 times a second.  Every 250th time the ISR gets called it will need to increment a seconds counter.  The efficiency of the setup code doesn't matter, so I've written it in C:
    // normal mode, clear counter when count reached
    TCCR2A = (1<<WGM21);
    TCCR2B = CSDIV256;

    OCR2A = 250;                // reset timer when count reached
    TIMSK2 = (1<<OCIE2A);       // enable interrupt
    sei();

The first lines for the assembler source, as I do with all my AVR assembler programs, will be the following:
#define __SFR_OFFSET 0
#include <avr/io.h>

This will avoid having to use the _SFR_IO_ADDR macro when using IO registers.  So instead of having to write:
in r0, _SFR_IO_ADDR(GPIOR0)
I can write:
in r0, GPIOR0

The ISR needs to keep a 1-byte counter to count 250 interrupts before adding a second.  There's almost 32 million seconds in a year, so a 4-byte counter is needed.  These counters could be stored in registers or RAM.  In the avr-libc assembler demo project 3 registers are dedicated to ISR use, making them unavailable to the C compiler.  Instead of tying up 5 registers, the ISR will use the .lcomm directive to reserve space in RAM.  The seconds timer (__system_time) will be marked global so it can be accessed outside the ISR.
; 1 byte variable in RAM
.lcomm ovfl_count, 1

; 4 byte (long) global variable in RAM
.lcomm __system_time, 4
.global __system_time

As an 8-bit processor, the AVR cannot increment a 32-bit second counter in a single operation.  It does have a 16-bit add instruction (adiw), but not 32-bit.  So it will have to be done byte-by byte.  Since it doesn't have an instruction for add immediate, the quickest way to add one to a byte is to subtract -1 from it.  For loading and storing the bytes between RAM and registers, the 4-byte lds and sts instruction could be used.  Loading Z with a pointer allows the 2-byte ld and st instructions to be used, and making use of the auto-increment version of the instructions allows a single load/store combination to be used in a loop.  With that in mind, here's the smallest (and fastest) code I could come up with to increment a 32-bit counter stored in RAM:
    ldi ZL, lo8(__system_time)
    ldi ZH, hi8(__system_time)
loop:
    ld r16, Z
    sbci r16, -1                    ; subtract -1 = add 1
    st Z+, r16
    brcc loop

Since the 8-bit overflow counter and the seconds counter are sequential in memory, a reload of the Z counter can be avoided:
    ldi ZL, lo8(ovfl_count)
    ldi ZH, hi8(ovfl_count)
    ld r16, Z
    cpi r16, 250
    brne loop
    clr r16                    ; reset counter
loop:
    sbci r16, -1                    ; subtract -1 = add 1
    st Z+, r16
    ld r16, Z
    brcc loop

For testing the timer, the low byte of the seconds count is written to PORTB.  If it works, the LED on the Pro Mini's pin 13 (PB5) will toggle every 2^5 = 32 seconds.
    DDRB = 0xff;                // output mode
    while (1) {
        PORTB = __system_time & 0xff;
    }

After compiling, linking, and flashing the code to the Pro Mini, it didn't work - the LED on PB5 never flashed.  I looked at the disassembled code and couldn't find anything wrong.  To make sure the while loop was running, I added a line to toggle PB0 after the write to PORTB.  Flashing the new code and attaching an LED to PB0 confirmed the loop was running.  Adding code to set a pin inside the ISR confirmed it was running.  The problem was the counter wasn't incrementing.  After going over the AVR  instruction set again, I realized the mistake had to do with the inverted math.  When the value of ovfl_count in r16 is less than 250, the branch if not equal is taken, continuing execution at the sbci instruction.  However, since the carry flag is set by the cpi instruction when r16 is less than 250, the sbci instruction subtracts -1 and subtracts carry for  a net result of 0.  The solution I came up with was changing the loop to count from 6 up to 0:
    cpi r16, 0
    brne loop
    ldi r16, 6                    ; skip counts 1-6

With that change, it worked!
I cleaned up the code and posted t32isr.S and timer-asm.c to my google code repository.  In a future post I'll add some code to compute the date and time from the seconds count.

No comments:

Post a Comment