There might be several reasons to write code for AVR microcontrollers using plain assembler source code. Among them are:
Although avr-libc is primarily targeted to support programming AVR microcontrollers using the C (and C++) language, there's limited support for direct assembler usage as well. The benefits of it are:
For the purpose described in this document, the assembler and linker are usually not invoked manually, but rather using the C compiler frontend (avr-gcc)
that in turn will call the assembler and linker as required.
This approach has the following advantages:
avr-gcc
, regardless of the actual source language used.crt
XXX.o
) and linker script.S
(the capital letter "s"). This would even apply to operating systems that use case-insensitive filesystems since the actual decision is made based on the case of the filename suffix given on the command-line, not based on the actual filename from the file system.
Alternatively, the language can explicitly be specified using the -x assembler-with-cpp
option.
The following annotated example features a simple 100 kHz square wave generator using an AT90S1200 clocked with a 10.7 MHz crystal. Pin PD6 will be used for the square wave output.
#include <avr/io.h> ; Note [1] work = 16 ; Note [2] tmp = 17 inttmp = 19 intsav = 0 SQUARE = PD6 ; Note [3] ; Note [4]: tmconst= 10700000 / 200000 ; 100 kHz => 200000 edges/s fuzz= 8 ; # clocks in ISR until TCNT0 is set .section .text .global main ; Note [5] main: rcall ioinit 1: rjmp 1b ; Note [6] .global SIG_OVERFLOW0 ; Note [7] SIG_OVERFLOW0: ldi inttmp, 256 - tmconst + fuzz out _SFR_IO_ADDR(TCNT0), inttmp ; Note [8] in intsav, _SFR_IO_ADDR(SREG) ; Note [9] sbic _SFR_IO_ADDR(PORTD), SQUARE rjmp 1f sbi _SFR_IO_ADDR(PORTD), SQUARE rjmp 2f 1: cbi _SFR_IO_ADDR(PORTD), SQUARE 2: out _SFR_IO_ADDR(SREG), intsav reti ioinit: sbi _SFR_IO_ADDR(DDRD), SQUARE ldi work, _BV(TOIE0) out _SFR_IO_ADDR(TIMSK), work ldi work, _BV(CS00) ; tmr0: CK/1 out _SFR_IO_ADDR(TCCR0), work ldi work, 256 - tmconst out _SFR_IO_ADDR(TCNT0), work sei ret .global __vector_default ; Note [10] __vector_default: reti .end
#define work 16
int
by default in order to calculate constant integer expressions. TCCNT0
register, we therefore have to account for the number of clock cycles required for interrupt acknowledge and for the instructions to reload TCCNT0
(4 clock cycles for interrupt acknowledge, 2 cycles for the jump from the interrupt vector, 2 cycles for the 2 instructions that reload TCCNT0)
. This is what the constant fuzz
is for.global
. main
is the application entry point that will be jumped to from the ininitalization routine in crts1200.o
.sleep
instruction (using idle mode) could be used as well, but probably would not conserve much energy anyway since the interrupt service is executed quite frequently.global
in order to be acceptable for this purpose. This will only work if <avr/io.h>
has been included. Note that the assembler or linker have no chance to check the correct spelling of an interrupt function, so it should be double-checked. (When analyzing the resulting object file using avr-objdump
or avr-nm
, a name like __vector_N
should appear, with N being a small integer number.)_SFR_IO_ADDR
. (The AT90S1200 does not have RAM thus the memory-mapped approach to access the IO registers is not available. It would be slower than using in
/ out
instructions anyway.) TCCNT0
is time-critical, it is even performed before saving SREG
. Obviously, this requires that the instructions involved would not change any of the flag bits in SREG
.SREG
. (Note that this serves as an example here only since actually, all the following instructions would not modify SREG
either, but that's not commonly the case.) __vector_default
. This must be .global
, and obviously, should end in a reti
instruction. (By default, a jump to location 0 would be implied instead.)