1 avr-libc and assembler programs
Introduction
There might be several reasons to write code for AVR microcontrollers using plain assembler source code. Among them are:
-
Code for devices that do not have RAM and are thus not supported by the C compiler.
-
Code for very time-critical applications.
-
Special tweaks that cannot be done in C.
Usually, all but the first could probably be done easily using the inline assembler facility of the compiler.
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:
-
Use of the C preprocessor and thus the ability to use the same symbolic constants that are available to C programs, as well as a flexible macro concept that can use any valid C identifier as a macro (whereas the assembler's macro concept is basically targeted to use a macro in place of an assembler instruction).
-
Use of the runtime framework like automatically assigning interrupt vectors. For devices that have RAM, initializing the RAM variables can also be utilized.
Invoking the compiler
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:
-
There is basically only one program to be called directly,
avr-gcc
, regardless of the actual source language used. -
The invokation of the C preprocessor will be automatic, and will include the appropriate options to locate required include files in the filesystem.
-
The invokation of the linker will be automatic, and will include the appropriate options to locate additional libraries as well as the application start-up code (
crt
XXX.o
) and linker script.
Note that the invokation of the C preprocessor will be automatic when the filename
provided for the assembler file ends in .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.
As an alternative to using .S, the suffix
.sx is recognized for
this purpose (starting with GCC 4.3.0). This is primarily meant to be compatible with other
compiler environments that have been providing this variant before in order to cope with
operating systems where filenames are case-insensitive (and, with some versions of
make
that could not distinguish between .s and
.S on such
systems).
Alternatively, the language can explicitly be specified using the -x
assembler-with-cpp
option.
Example program
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.
- Note [1]
- Note [2]
- Note [3]
- Note [4]
int
by default in order to calculate constant integer expressions. In order to
get a 100 kHz output, we need to toggle the PD6 line 200000 times per second. Since we use timer
0 without any prescaling options in order to get the desired frequency and accuracy, we already
run into serious timing considerations: while accepting and processing the timer overflow
interrupt, the timer already continues to count. When pre-loading the 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.- Note [5]
.global. main
is
the application entry point that will be jumped to from the ininitalization routine in
crts1200.o
.- Note [6]
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.- Note [7]
.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.)- Note [8]
_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.) Since the operation to
reload 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
.- Note [9]
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.) Also, it must be made sure
that registers used inside the interrupt routine do not conflict with those used outside. In the
case of a RAM-less device like the AT90S1200, this can only be done by agreeing on a set of
registers to be used exclusively inside the interrupt routine; there would not be any other
chance to "save" a register anywhere. If the interrupt routine is to be linked together with C
modules, care must be taken to follow the register usage guidelines imposed by the C compiler. Also, any register modified inside
the interrupt sevice needs to be saved, usually on the stack.- Note [10]
__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.)Pseudo-ops and operators
The available pseudo-ops in the assembler are described in the GNU assembler (gas) manual. The manual can be found online as part of the current binutils release under http://sources.redhat.com/binutils/.
As gas comes from a Unix origin, its pseudo-op and overall assembler syntax is
slightly different than the one being used by other assemblers. Numeric constants follow the C
notation (prefix 0x
for hexadecimal constants), expressions use a C-like
syntax.
Some common pseudo-ops include:
-
.byte allocates single byte constants
-
.ascii allocates a non-terminated string of characters
-
.asciz allocates a \0-terminated string of characters (C string)
-
.data switches to the .data section (initialized RAM variables)
-
.text switches to the .text section (code and ROM constants)
-
.set declares a symbol as a constant expression (identical to
.equ)
-
.global (or
.globl) declares a public symbol that is visible to the linker (e. g. function entry point, global variable)
-
.extern declares a symbol to be externally defined; this is effectively a comment only, as gas treats all undefined symbols it encounters as globally undefined anyway
Note that .org is available in gas as well, but is a fairly pointless
pseudo-op in an assembler environment that uses relocatable object files, as it is the linker
that determines the final position of some object in ROM or RAM.
Along with the architecture-independent standard operators, there are some AVR-specific operators available which are unfortunately not yet described in the official documentation. The most notable operators are:
-
lo8
Takes the least significant 8 bits of a 16-bit integer
-
hi8
Takes the most significant 8 bits of a 16-bit integer
-
pm
Takes a program-memory (ROM) address, and converts it into a RAM address. This implies a division by 2 as the AVR handles ROM addresses as 16-bit words (e.g. in anIJMP
orICALL
instruction), and can also handle relocatable symbols on the right-hand side.
ldi r24, lo8(pm(somefunc)) ldi r25, hi8(pm(somefunc)) call something
This passes the address of function somefunc
as the first
parameter to function something
.