Cornell University
Electrical and Computer Engineering 4760 AVR mega644/1284
Mixing assembler with GCC

Introduction

It is possible to mix assembler and GCC in different ways:

  • Separate file. Write a pure assembler *.S file and link it with the main C file.
    This approach has the advantage of simpler syntax in a separate file. Another advantage is that C saves/restores registers for you (see below).
    It has a runtime speed disadvantage for short assembler routines due to the function linkage generated by C.
  • Inline assembler. Write inline assembler directly into the C code.
    You have to save/restore any registers you use. You get exactly what you write unless you use register constraints.
    If you use constraints the compiler can play with the register assignments, but specifiying the constraints is bewildering.
    The only runtime speed penality is the save/restore overhead, but you may be able to avoid the overhead with register constraints.
    Inline assembler seems to be the only way to write a NAKED interrupt service routine.
  • Assembler macro. Write an assembler macro and instantiate it in C (reusable inline assembler).
    This is useful if you need to use a short chunk of inline assembler many times in a program.
    The runtime speed is very good and register constraints allow the GCC optimizer to play with the code.

Before you actually write any assembly code you will need to read the instruction set architecture and description of AVR opcodes, and look at a bunch of assembler examples. Some examples are below. There are some tutorials, for instance scienceprog and Mixing C and asm. I find that the best way to learn assembler is to look at the assembler output of the compiler. In AVRstudio projects, the *.lss assembler listing file is in the default folder (in the project folder). Code up a few lines of C, open the lss file, search for a line of C (included as comment by the compiler), and see what the compiler did. There is an example below of compiler output from a video generator I wrote in C (assembler comments added for this page). The code line is in a function with x the first (char) parameter and y the second.

;C comment: int i = (x >> 3) + (int)y * bytes_per_line ;
ldi	r24, 0x14	; bytes_per_line=20=0x14
mul	r22, r24 	; since y was the second (char) parameter of a function call, it is in r22
movw	r26, r0		; 2-byte move to get product at r27:r26
eor	r1, r1		; MUST clear r1 after a mult 	
mov	r24, r30	; The compiler had moved the first parameter from r24 to r30	
lsr	r24		; 3x lsr for the >>3 
lsr	r24
lsr	r24
add	r26, r24	; do the add
adc	r27, r1 	; and add the carry to the high byte using r1=0 register

The lss file can tell you other stuff also. If you search for __vectors, you will get the interrupt service routine entry points. You will see by following the undefined interrupt vectors, that GCC defaults to resetting the MCU for any undefined interrupt. The zero entry point is the RESET vector where program execution starts. Searching for that address will lead you to the MCU and C initialization code. The first few lines of the reset code are shown below with my comments added:

 eor	r1, r1 		; clear r1 (C assumes r1 equals zero)
 out	0x3f, r1	; zero the SREG which is i/o register 0x3f
 ldi	r28, 0xFF	; load the low byte of the top-of-memory address
 ldi	r29, 0x40	; load the top byte of the top-of-memory address
 out	0x3e, r29	; store the top byte in top byte of stack pointer (i/o register 0x3e)
 out	0x3d, r28	; store the low byte in low byte of stack pointer (i/o register 0x3d)

The next few lines shown in the lss file clear memory and set up the C environment, then jump to main. The map file in the same folder will show you where the variables are stored in RAM.

Syntax and registers

Global variables defined in C are available to the assembler. For a global variable defined in C as volatile char vname;
Using the declared C variable name in a load/store command like those below loads/stores the value of the variable...

Read more »