AVR Libc Home Page |  | AVR Libc Development Pages
Main Page | User Manual | Library Reference | FAQ | Alphabetical Index | Example Projects

# 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.

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.

    #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 TIMER0_OVF_vect                 ; Note [7]
    TIMER0_OVF_vect:
            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

Note [1]

As in C programs, this includes the central processor-specific file containing the IO port definitions for the device. Note that not all include files can be included into assembler sources.

Note [2]

Assignment of registers to symbolic names used locally. Another option would be to use a C preprocessor macro instead:

     #define work 16

Note [3]

Our bit number for the square wave output. Note that the right-hand side consists of a CPP macro which will be substituted by its value (6 in this case) before actually being passed to the assembler.

Note [4]

The assembler uses integer operations in the host-defined integer size (32 bits or longer) when evaluating expressions. This is in contrast to the C compiler that uses the C type `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]

External functions need to be declared to be ``.global. `main` is the application entry point that will be jumped to from the ininitalization routine in `crts1200.o`.

Note [6]

The main loop is just a single jump back to itself. Square wave generation itself is completely handled by the timer 0 overflow interrupt service. A `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]

Interrupt functions can get the usual names that are also available to C programs. The linker will then put them into the appropriate interrupt vector slots. Note that they must be declared ``.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]

As explained in the section about special function registers, the actual IO port address should be obtained using the macro `_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]

Interrupt routines must not clobber the global CPU state. Thus, it is usually necessary to save at least the state of the flag bits in `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]

As explained in Interrupts, a global "catch-all" interrupt handler that gets all unassigned interrupt vectors can be installed using the name `__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 an `IJMP` or `ICALL` instruction), and can also handle relocatable symbols on the right-hand side.

Example:

    	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`.

Automatically generated by Doxygen 1.5.2 on 14 Feb 2008.