Stand 27.07.2007 - Dr. Erhard Henkes

Assembler-Programmierung für AVR Microcontroller (Atmel)


1. Einstieg in das STK500 - Entwicklungssystem

Wird Assembler heute überhaupt noch in größerem Umfang für PCs verwendet? Ich möchte für interessierte Einsteiger hier eine Alternative aufzeigen, da ich denke, es macht praktisch viel Sinn, wenn man sich mit der Assembler-Programmierung von Microcontrollern - die uns überall versteckt umgeben - beschäftigt, z.B. der Atmel ATmega- oder ATtiny-Serie. Man benötigt dazu das kostenlose Atmel AVR Studio, das Atmel STK500 Starter Kit  (ca. 70 Euro, es gibt auch billigere Boards, die sind aber nicht vergleichbar) und eine 10-15 Volt Gleichspannungsversorgung.

Zum Einlesen in die AVR-Programmierung und -Entwicklung bieten sich z.B. folgende Links an:
http://de.wikipedia.org/wiki/Atmel_AVR
http://s-huehn.de/elektronik/avr-prog/avr-prog.htm
http://www.rowalt.de/mc/avr/avrboard/01/avrb01.htm
http://www.mikrocontroller.net/articles/AVR
http://www.avr-asm-tutorial.net/

Das Atmel STK500 Starter Kit Evaluation Board in Verbindung mit der Software AVR Studio (z.Z. ist die Version 4.13 aktuell) unterstützt AT90S- (überholt), ATmega- und ATtiny-Controller im 8 bis 40poligen DIP-Gehäuse sowie künftige Entwicklungen von ATMEL. 



Atmel STK500 Starter Kit Evaluation Board  (Foto: Dr. Erhard Henkes)

Folgende Kabel findet man im Atmel Starter Kit STK500:

Zum Lieferumfang gehört weiterhin das Softwarepaket "AVR Studio", das Assembler, Simulator, Emulator und Programmiersoftware umfasst, so wie man es von einer modernen integrierten Entwicklungsumgebung erwartet ("AVR Studio provides a project management tool, source file editor, simulator, incircuit
emulator interface and programming interface for STK500").

Die Spannungsversorgung, z.B. 12 Volt Gleichspannung, schließt man mit dem
zweiadrigen Gleichspannungskabel an den Spannungseingang (auf dem Bild rechts oben) an. Das RS232-Kabel verbindet die mittlere RS232-Buchse mit einem COM-Port des Computers. Nach Installation des AVR Studio kann man starten. Die bei dem STK500 beiliegende Version 4.12 des AVR Studio (aktuell ist 4.13, siehe AVR Studio) verlangte ein "Downgrade" der Firmware des Boards STK500. Hierzu muss man beim Einschalten der Spannung den Programm-Knopf gedrückt halten. Für die Version 4.12 existiert inzwischen ein Service Pack. Ich empfehle unbedingt die neue Version 4.13, die man bei Atmel im kostenlosen Download erhalten kann (Registrierung ist erforderlich; FlashGet funktioniert und wird von mir diesbezüglich empfohlen!).

Damit man sofort beginnen kann, liegen bereits Atmel Microcontroller dem Starter Kit bei, klassisch ein AT90S8515-8PC (veraltet,
8 kB Flash) oder neuerdings ein ATmega8515 (8 kB Flash) und ATmega162 (16 kB Flash). Datenblätter der Atmel Microcontroller sind auf der beiliegenden CD ebenfalls enthalten. Auf dem STK500 Evaluation Board können gemäß Atmel momentan folgende Microcontroller programmiert und getestet werden:

AT90CAN128, AT90CAN128 Automotive, AT90CAN32, AT90CAN32 Automotive, AT90CAN64, AT90CAN64 Automotive, AT90PWM1, AT90PWM2, AT90PWM3, AT90S1200, AT90S2313, AT90S2323, AT90S2343, AT90S4433, AT90S8515, AT90S8535,
ATmega128, ATmega1280, ATmega1281, ATmega128RZAV, ATmega128RZBV, ATmega16, ATmega161, ATmega162, ATmega163, ATmega164P, ATmega164P Automotive, ATmega165, ATmega165P, ATmega168, ATmega168 Automotive, ATmega169, ATmega169P, ATmega2560, ATmega2561, ATmega256RZAV, ATmega256RZBV, ATmega32, ATmega323, ATmega324P, ATmega324P automotive, ATmega325, ATmega3250, ATmega3250P, ATmega325P, ATmega329, ATmega3290, ATmega3290P, ATmega329P, ATmega48, ATmega48 Automotive, ATmega64, ATmega640, ATmega644, ATmega644P, ATmega644P Automotive, ATmega645, ATmega6450, ATmega649, ATmega6490, ATmega64RZAPV, ATmega64RZAV, ATmega8, ATmega8515, ATmega8535, ATmega88, ATmega88 Automotive,
ATtiny11, ATtiny12, ATtiny13, ATtiny15L, ATtiny24, ATtiny24 Automotive, ATtiny25, ATtiny25 Automotive, ATtiny26, ATtiny261, ATtiny28L, ATtiny44, ATtiny44 Automotive, ATtiny45, ATtiny45 Automotive, ATtiny461, ATtiny84, ATtiny84 Automotive, ATtiny85, ATtiny85 Automotive, ATtiny861.



vgl. Blockbild aus dem "Atmel STK500 User Guide"


2. Programmieren in Assembler

Wir verwenden zum Programmieren in Assembler das aktuelle AVR Studio. Damit können wir unseren Sourcecode schreiben, diesen "debuggen" und anschließend auf den Ziel-µC übertragen. Wir starten AVR Studio und eröffnen ein neues Projekt. Hierbei können wir wählen zwischen Assembler und C/C++-Programmierung (mit AVR-GCC). Wir entscheiden uns für Assembler.

 

2.1. Debugger und blinkende LEDs


Nun geben wir den untenstehenden Sourcecode ein und richten AVR Studio nach dem Assemblieren (Build, F7) über das Menü "View" so ein, dass wir unseren Sourcecode, Port B, die Prozessordaten und die Register im Überblick haben. Wir starten im Menü "Debug" den Degugger und tasten uns schrittweise voran (F11). Dabei ergeben sich folgende Einzelschritt-Zustände, die wir in den nächsten Abbildungen detailliert analysieren wollen. Nachstehendes Beispiel ist aus einer Variation des folgenden Beispiels http://www.avr-asm-tutorial.net/avr_de/quellen/test1.asm entstanden:

.NOLIST
.INCLUDE "8515def.inc"
.LIST

.DEF mp = R16

       rjmp main

main:
            ldi mp,0xFF
            out DDRB, mp
loop:
            ldi mp, 0b01010101
            out PORTB, mp
            ldi mp, 0b10101010
            out PORTB, mp
            clr mp
            out PORTB, mp
            ser mp
            out PORTB, mp
            rjmp loop

Unser Zielprozessor ist ein ATmega8515 (Vorgänger: AT90S8515), der beim aktuellen Atmel STK500 Starter Kit mitgeliefert wird.

Fügen Sie nach dem INCLUDE die für Ihren Prozessor zutreffende inc-Datei ein. In dieser Datei stehen Festlegungen, die uns das Programmieren durch sprechende Bezeichnungen anstelle Zahlen erleichtern, hier einige Beispiele im Zusammenhang mit Port B:

.equ    PORTB   =$18
.equ    DDRB    =$17
.equ    PINB    =$16

;PORTB
.equ    PB7     =7
.equ    PB6     =6
.equ    PB5     =5
.equ    PB4     =4
.equ    PB3     =3
.equ    PB2     =2
.equ    PB1     =1
.equ    PB0     =0

;DDRB
.equ    DDB7    =7
.equ    DDB6    =6
.equ    DDB5    =5
.equ    DDB4    =4
.equ    DDB3    =3
.equ    DDB2    =2
.equ    DDB1    =1
.equ    DDB0    =0

;PINB
.equ    PINB7   =7
.equ    PINB6   =6
.equ    PINB5   =5
.equ    PINB4   =4
.equ    PINB3   =3
.equ    PINB2   =2
.equ    PINB1   =1
.equ    PINB0   =0

Am besten werfen Sie selbst mal einen Blick in diese Datei. Dann verstehen Sie die Zusammenhänge und Festlegungen besser.
Sie finden diese im Pfad: C:\Programme\Atmel\AVR Tools\AvrAssembler2\Appnotes


Port B des Microcontrollers kann mittels Flachbandkabel mit den LEDs des STK500 verbunden werden (siehe Bild oben). Daher sprechen wir in unserem Programm genau die acht Bits dieses Ports an. Jedes Bit ist mit einer LED verbunden. Die include-Datei soll nicht gelistet werden. Unser Programm startet mit der Festlegung des Namens mp für das Register 16. Dieses Register ist von unten gezählt das erste, das man mit dem Befehl LDI (Load Immediate) ansprechen kann. Daher beginnen wir mit R16 und nicht mit R00.



Wir springen mit RJMP (Relative Jump) zum Label "main". RJMP kann man im plus/minus 4 KByte Umkreis verwenden, für unsere kleinen Programme und für Prozessoren mit 8 KB Flashspeicher, z.B. ATmega8..., kein Problem. Die Taktfrequenz beträgt hier 4 MHz, d.h. ein Taktschritt (cycle) benötigt 0,25 Mikrosekunden.

Port B umfasst folgende drei Adressen:
PORTB (Daten), DDRB (Festlegung der Richtung: In / Out) und PINB (Ein-/Ausgang).

Der Program Counter (PC) startet bei 0x000000.  
 


Wie man sieht, benötigt RJMP zwei Taktschritte. Diese benötigen bei 4 MHz genau 0,5 Mikrosekunden.



LDI kann eine 8-Bit-Zahl (0 ... 255) direkt in eines der Register 16 ... 31 laden. Das Statusregister (SREG) wird dabei nicht beeinflusst.
Der Debugger zeigt, dass anschließend im Register R16 die Zahl 0xFF (255 bzw. 0b11111111) gespeichert ist.



Im nächsten Schritt wird der Inhalt des Registers 16 nach DDRB kopiert. Damit sind alle acht Bit des Port B auf Ausgang geschaltet.
OUT kann Daten der Register 0 ... 31 transferieren. SREG bleibt unbeeinflusst.



Nun laden wir 0x55 nach R16 ...



... und kopieren diesen Wert mit OUT nach PORTB. PINB ist noch unbeeinflusst.



Nun wird 0xAA nach R16 geladen. PINB synchronisiert sich nun mit PORTB auf 0x55.



PORTB hat nun mittels OUT den Wert von R16 (0xAA) erhalten.



PINB zieht wieder erst einen Takt später mit. R16 wurde mit CLR (clear register) auf 0x00 gesetzt. Dabei wird das Z-Flag von SREG gesetzt.
CLR führt logisch ein Exclusives ODER (XOR) mit sich selbst durch. Damit werden alle Bits gelöscht.
CLR kann auf die Register 0 ... 31 angewendet werden.



Ausgabe auf PORTB mit OUT.



SER (Set all Bits in Register) kann wie LDI nur auf die Register 16 ... 31 angewendet werden. SREG bleibt wie bei LDI unbeeinflusst.
Damit werden alle Bits von R16 auf 1 gesetzt.



Ausgabe auf PORTB mit OUT.



Rücksprung mit RJMP (Relative Jump) zum Label "loop". Das kostet wieder zwei Taktschritte. Die Schleife läuft nun "endlos".
Der Program Counter (PC) springt zurück auf 0x000003. PINB synchronisiert mit PORTB.



In R16 wird der Wert 0x55 gespeichert.



OUT kopiert den Wert nach PORTB.



Sie sehen an diesem winzigen Beispiel, wie man Sourcecode mit dem AVR Studio schreibt, assembliert (nicht compiliert!) und anschließend im Einzelschritt-Modus mit dem "Debugger" untersucht.

Interessant ist auch die erzeugte Listing-Datei: ASM001.lst
Diese beinhaltet neben dem Code einige interessante Statistiken.

AVRASM ver. 2.1.12  C:\Atmel_ASM\ASM001\ASM001.asm Fri Jul 27 17:46:24 2007

C:\Atmel_ASM\ASM001\ASM001.asm(3): Including file 'C:\Programme\Atmel\AVR Tools\AvrAssembler2\Appnotes\8515def.inc'
                
                
                 .LIST
                
                 .DEF mp = R16
                
000000 c000            rjmp main
                
                 main:
000001 ef0f                  ldi mp,0xFF
000002 bb07                  out DDRB, mp
                 loop:
000003 e505                  ldi mp, 0b01010101
000004 bb08                  out PORTB, mp
000005 ea0a                  ldi mp, 0b10101010
000006 bb08                  out PORTB, mp
000007 2700                  clr mp
000008 bb08                  out PORTB, mp
000009 ef0f                  ser mp
00000a bb08                  out PORTB, mp
00000b cff7                  rjmp loop


RESOURCE USE INFORMATION
------------------------

Notice:
The register and instruction counts are symbol table hit counts,
and hence implicitly used resources are not counted, eg, the
'lpm' instruction without operands implicitly uses r0 and z,
none of which are counted.

x,y,z are separate entities in the symbol table and are
counted separately from r26..r31 here.

.dseg memory usage only counts static data declared with .byte

AT90S8515 register use summary:
r0 :   0 r1 :   0 r2 :   0 r3 :   0 r4 :   0 r5 :   0 r6 :   0 r7 :   0
r8 :   0 r9 :   0 r10:   0 r11:   0 r12:   0 r13:   0 r14:   0 r15:   0
r16:  10 r17:   0 r18:   0 r19:   0 r20:   0 r21:   0 r22:   0 r23:   0
r24:   0 r25:   0 r26:   0 r27:   0 r28:   0 r29:   0 r30:   0 r31:   0
x  :   0 y  :   0 z  :   0
Registers used: 1 out of 35 (2.9%)

AT90S8515 instruction use summary:
adc   :   0 add   :   0 adiw  :   0 and   :   0 andi  :   0 asr   :   0
bclr  :   0 bld   :   0 brbc  :   0 brbs  :   0 brcc  :   0 brcs  :   0
breq  :   0 brge  :   0 brhc  :   0 brhs  :   0 brid  :   0 brie  :   0
brlo  :   0 brlt  :   0 brmi  :   0 brne  :   0 brpl  :   0 brsh  :   0
brtc  :   0 brts  :   0 brvc  :   0 brvs  :   0 bset  :   0 bst   :   0
cbi   :   0 cbr   :   0 clc   :   0 clh   :   0 cli   :   0 cln   :   0
clr   :   1 cls   :   0 clt   :   0 clv   :   0 clz   :   0 com   :   0
cp    :   0 cpc   :   0 cpi   :   0 cpse  :   0 dec   :   0 eor   :   0
icall :   0 ijmp  :   0 in    :   0 inc   :   0 ld    :   0 ldd   :   0
ldi   :   3 lds   :   0 lpm   :   0 lsl   :   0 lsr   :   0 mov   :   0
neg   :   0 nop   :   0 or    :   0 ori   :   0 out   :   5 pop   :   0
push  :   0 rcall :   0 ret   :   0 reti  :   0 rjmp  :   2 rol   :   0
ror   :   0 sbc   :   0 sbci  :   0 sbi   :   0 sbic  :   0 sbis  :   0
sbiw  :   0 sbr   :   0 sbrc  :   0 sbrs  :   0 sec   :   0 seh   :   0
sei   :   0 sen   :   0 ser   :   1 ses   :   0 set   :   0 sev   :   0
sez   :   0 sleep :   0 st    :   0 std   :   0 sts   :   0 sub   :   0
subi  :   0 swap  :   0 tst   :   0 wdr   :   0
Instructions used: 5 out of 100 (5.0%)

AT90S8515 memory use summary [bytes]:
Segment   Begin    End      Code   Data   Used    Size   Use%
---------------------------------------------------------------
[.cseg] 0x000000 0x000018     24      0     24    8192   0.3%
[.dseg] 0x000060 0x000060      0      0      0     512   0.0%
[.eseg] 0x000000 0x000000      0      0      0     512   0.0%

Assembly complete, 0 errors, 0 warnings


Wie Sie sehen, haben Sie damit schon 5 von 100 Befehlen kennen gelernt und 1 von 35 Register genutzt. Wenn dies kein gelungener Anfang ist!

Die Ausführung in der Realität macht in dieser Form selbstverständlich keinen Sinn, da die Ablaufgeschwindigkeit viel zu hoch ist. Daher begnügen wir uns in dieser Version mit der Simulation im Debugger.

Um einen optischen Eindruck von diesem Programm zu gewinnen, muss man zunächst eine Warteschleife einbauen. Wir müssten den Ablauf um etwa um den Faktor 100000 verlangsamen, damit wir das Beispiel optisch verfolgen können. Daher kommt die Weisheit, dass ein Prozessor in einer Sekunde mehr Fehler machen kann als ein Mensch in seinem ganzen Leben.


2.2. Warteschleifen in Assembler

Wie realisiert man Warteschleifen? Man könnte z.B. in einem Unterprogramm zwei verschachtelte Zählschleifen aufbauen, die in verschiedenen Registern jeweils von 0 bis 255 zählen (256 * 256 = 65536). Das sollte doch klappen? Frisch ans Werk! Dabei müssen wir allerdings zuerst den Stackpointer initialisieren, sprich auf das Ende des RAM setzen, damit dieser dort von oben nach unten wandern kann. Hier wird das ausführlich erklärt.

.NOLIST
.INCLUDE "8515def.inc"
.LIST

.DEF temp = R20 

; Initialisierung Stackpointer:
; Der Stack wird im RAM angelegt.
; Der Stack wächst von oben nach unten.
; Zu Beginn muss man den Stackpointer auf das Ende des RAM setzen.

            ldi temp, LOW(RAMEND)  ; Low  Byte der höchsten RAM-Adresse           
            out SPL, temp

            ldi temp, HIGH(RAMEND)
; High Byte der höchsten RAM-Adresse
            out SPH, temp

; Sprung zum Hauptprogramm
            rjmp main

; Hauptprogramm
main:
            ldi R16,0xFF
            out DDRB, R16
loop:
            ldi R16, 0b01010101
            out PORTB, R16
            rcall delay;

            ldi R16, 0b10101010
            out PORTB, R16
            rcall delay;

            clr R16
            out PORTB, R16
            rcall delay;

            ser R16
            out PORTB, R16
            rcall delay;

            rjmp loop

; Unterprogramm Warteschleife
delay:      clr  R17

M2:         clr  R18

M1:         dec  R18   ; erniedrigt R18 um 1 (Decrement)
            nop        ; No Operation (1 Takt)
            nop
            nop
            nop
            nop
            nop
            nop
            nop
            nop
            nop
            nop
            nop
            nop
            nop

            brne M1    ; Branch if Not Equal

            dec  R17
            brne M2
            ret        ; Return from Subroutine



Nun kann man diesen Programmablauf optisch bereits gut verfolgen. Die LEDs verändern ca. jede 0,3 Sekunden ihren Zustand.
Flashen Sie Ihren ATmega8515 und genießen Sie die Früchte dieser kleinen Übung. Mit blinkenden LEDs fängt man zumeist an.

Aufgabe: Tauschen Sie zur Übung die NOP-Befehle gegen eine dritte innere Schleife aus.

Aber warum soll man sich lange quälen, wenn es bereits professionelle Lösungen für solche Routineaufgaben gibt?
Ein interessantes Tool für die Praxis findet man mit dem AVR delay loop generator von Tjabo Kloppenburg:


Setzen wir dieses Beispiel mit einer Warteschleife von 0,5 Sekunden in unserem Code ein:


.NOLIST
.INCLUDE "8515def.inc"
.LIST

.DEF temp = R20 

; Initialisierung Stackpointer:
; Der Stack wird im RAM angelegt.
; Der Stack wächst von oben nach unten.
; Zu Beginn muss man den Stackpointer auf das Ende des RAM setzen.

            ldi temp, LOW(RAMEND)  ; Low  Byte der höchsten RAM-Adresse           
            out SPL, temp

            ldi temp, HIGH(RAMEND)
; High Byte der höchsten RAM-Adresse
            out SPH, temp

; Sprung zum Hauptprogramm
            rjmp main

; Hauptprogramm
main:
            ldi R16,0xFF
            out DDRB, R16
loop:
            ldi R16, 0b01010101
            out PORTB, R16
            rcall delay;

            ldi R16, 0b10101010
            out PORTB, R16
            rcall delay;

            clr R16
            out PORTB, R16
            rcall delay;

            ser R16
            out PORTB, R16
            rcall delay;

            rjmp loop

; Unterprogramm Warteschleife
delay:

; =============================
;    delay loop generator
;     2000000 cycles:
; -----------------------------
; delaying 1999998 cycles:
          ldi  R17, $12
WGLOOP0:  ldi  R18, $BC
WGLOOP1:  ldi  R19, $C4
WGLOOP2:  dec  R19
          brne WGLOOP2
          dec  R18
          brne WGLOOP1
          dec  R17
          brne WGLOOP0
; -----------------------------
; delaying 2 cycles:
          nop
          nop
; =============================
ret   ; Return from Subroutine

Assemblieren, Flashen, funktioniert bestens! Was sagt eigentlich unser Debugger/Simulator zu dem Zeitbedarf der Warteschleife? Im Fenster "Processor" kann man die Gesamtzeit hervorragend verfolgen. Mit der Einzelschrittmethode (F11) kann man richtig alt werden bei so einer tief geschachtelten Schleife. Das geht richtig mit "Step out" (Shift F11). Das Ergebnis ist präzise:



Zieht man 3,25 Mikrosekunden für den Programmteil bis zum Einstieg in die erste Warteschleife ab, so ergeben sich 0,500001 Sekunden. Dies ist wahrhaft akzeptabel. Das Thema "präzise Warteschleifen" in Assembler ist damit für Sie gelöst!


2.3. Eingaben

Auf dem STK500 findet man nicht nur LEDs, sondern auch Taster. Diese wollen wir nun für Eingaben nutzen.




















wird fortgesetzt