AVR Cross Compiler gcc: allgemeine Betrachtungen

Mit dem AVR-Controller zum Mond?
AVR on moon

Der AVR-Cross-Compiler gcc - wer effizient Programme für die AVR-Familie erstellen will, wird nicht umhin kommen, sich ein wenig mit dem erzeugten Assembler-Code zu befassen. Hier betrachten wir den Startup-Code und eine Standard-Verzögerungsschleife.



Was erzeugt der gcc?

Grundsätzlich empfiehlt es sich, ein allgemeines Makefile zu schreiben, dass man für sämtliche Projekte einsetzen kann. Danach kompilieren wir ein einfaches Test-Programm, disassemblieren und betrachten es.

Es wird neben einer AVR-Cross-Compiler-Suite der AVR-Disassembler vavrdisasm benötigt.



Makefile

# ---
# device family, clock rate
DEVICE  = attiny85
CLOCK   = 16500000      # 16.5MHz

# source files
OBJ = main.o

# Optimization level, can be [0, 1, 2, 3, s].
#     0 = turn off optimization. s = optimize for size.
#     (Note: 3 is not always the best optimization level. See avr-libc FAQ.)
#OPT = s
OPT = 1

CFLAGS  = -Wall -Os -DF_CPU=$(CLOCK) -mmcu=$(DEVICE)
CFLAGS += -O$(OPT)
AVRGCC  = avr-gcc $(CFLAGS)
AVRGPP  = avr-g++ $(CFLAGS)
AVROBJ  = avr-objcopy
AVRSIZE = avr-size

LOADER  = micronucleus

# ---

all: main.hex

.c.o:
        $(AVRGCC) -c $< -o $@

.cpp.o:
        $(AVRGPP) -c $< -o $@

.PHONY: clean
clean:
        rm -f main.hex main.elf *.o

main.elf: $(OBJ)
        $(AVRGCC) -o main.elf $(OBJ)

main.hex: main.elf
        rm -f main.hex
        $(AVROBJ) -j .text -j .data -O ihex main.elf main.hex
        $(AVRSIZE) --format=avr --mcu=$(DEVICE) main.elf

flash: all
        $(LOADER) --run main.hex

main.c

#include <avr/io.h>
#include <util/delay.h>

// Digispark LED is on Pin 1 for newer versions
#define LED             PB1
#define DELAY_MS        1000

int main(void)
{
        DDRB    |=   (1 << LED);     // Set pin to output
        PORTB   |=   (1 << LED);     // Set pin to high

        for (;;) {
                PORTB ^= (1 << LED);
                _delay_ms(DELAY_MS);
        }
        return 0;
}

kompilieren von main.c

make
        avr-gcc -Wall -Os -DF_CPU=16500000       -mmcu=attiny85 -O1 -c main.c -o main.o
        avr-gcc -Wall -Os -DF_CPU=16500000       -mmcu=attiny85 -O1 -o main.elf main.o
        rm -f main.hex
        avr-objcopy -j .text -j .data -O ihex main.elf main.hex
        avr-size --format=avr --mcu=attiny85 main.elf
AVR Memory Usage
----------------
Device: attiny85

Program:      84 bytes (1.0% Full)
(.text + .data + .bootloader)

Data:          0 bytes (0.0% Full)
(.data + .bss + .noinit)

Disassemblieren mit Erläuterungen

vavrdisasm main.hex
  • Ganz am Anfang stehen die Reset- und Interrupt-Vektoren, welche ein relativer Sprung sein müssen
  • Nach dem Reset wird an die Adresse 0 gesprungen, hier geht es dann weiter nach $1e
   0:   c0 0e           rjmp    .+28    ; 0x1e
   2:   c0 15           rjmp    .+42    ; 0x2e
   4:   c0 14           rjmp    .+40    ; 0x2e
   6:   c0 13           rjmp    .+38    ; 0x2e
   8:   c0 12           rjmp    .+36    ; 0x2e
   a:   c0 11           rjmp    .+34    ; 0x2e
   c:   c0 10           rjmp    .+32    ; 0x2e
   e:   c0 0f           rjmp    .+30    ; 0x2e
  10:   c0 0e           rjmp    .+28    ; 0x2e
  12:   c0 0d           rjmp    .+26    ; 0x2e
  14:   c0 0c           rjmp    .+24    ; 0x2e
  16:   c0 0b           rjmp    .+22    ; 0x2e
  18:   c0 0a           rjmp    .+20    ; 0x2e
  1a:   c0 09           rjmp    .+18    ; 0x2e
  1c:   c0 08           rjmp    .+16    ; 0x2e
  • Es folgt der Standard-GCC-Initialisierungscode für AVR
  • das SREG $3f wird gelöscht
  • der Stack-Pointer wird auf das Ende des Speichers, hier $025f gesetzt
  • die Routine main() ab 0x30 wird mit einem rcall aufgerufen
  • Sollte sie zurückkehren, wird zur Adresse 0x50 gesprungen
  1e:   24 11           eor     R1, R1
  20:   be 1f           out     $3f, R1
  22:   e5 cf           ldi     R28, 0x5f
  24:   e0 d2           ldi     R29, 0x02
  26:   bf de           out     $3e, R29
  28:   bf cd           out     $3d, R28
  2a:   d0 02           rcall   .+4     ; 0x30
  2c:   c0 11           rjmp    .+34    ; 0x50
  2e:   cf e8           rjmp    .-48    ; 0x0
  • die Routine main()
  • das Bit 1 wird im DDR und Port B gsetzt
  30:   9a b9           sbi     $17, 1
  32:   9a c1           sbi     $18, 1
  • es folgt Code zum toggeln des Bits am Port B via eor
  34:   e0 92           ldi     R25, 0x02
  36:   b3 88           in      R24, 0x18
  38:   27 89           eor     R24, R25
  3a:   bb 88           out     $18, R24
  • Die Warteschleife, die hier 3 Register verwendet, wird inline einkompiliert
  3c:   e9 2f           ldi     R18, 0x9f
  3e:   e5 3a           ldi     R19, 0x5a
  40:   e3 82           ldi     R24, 0x32
  42:   50 21           subi    R18, 0x01
  44:   40 30           sbci    R19, 0x00
  46:   40 80           sbci    R24, 0x00
  48:   f7 e1           brne    .-8     ; 0x42
  • Das Programm beginnt von vorne: hier die Endlosschleife
  4a:   c0 00           rjmp    .+0     ; 0x4c
  4c:   00 00           nop
  4e:   cf f3           rjmp    .-26    ; 0x36
  • eventuelles Programmende, wenn main() keine Endlos-Schleife beinhaltet
  50:   94 f8           cli
  52:   cf ff           rjmp    .-2     ; 0x52

weitere Mini-Beispiele

  • hier auf Github, wurde zum Teil hier verwendet
  • 1-blink und 2-button wurden von mir getestet

https://github.com/matthew-macgregor/digispark-attiny85-experiments/

Artikel erstellt am: 10 February 2025 , aktualisiert am 11 February 2025