Aus RN-Wissen.de
Wechseln zu: Navigation, Suche
Laderegler Test Tueftler Seite

Was hier folgt, ist nichts für Profis und Power-User, die mögen weiterblättern. Ich versuche hier, absolute Neueinsteiger nach und nach mit ein paar Grundinformationen zu versorgen.

Assembler Einführung für Bascom-User

Wieso Bascom ?

Eine der einfachsten Möglichkeiten, sich an Assembler heranzutasten, ist es, den Bascom-Compiler als Workbench zu benutzen.

Die Vorteile:

  • Das Drumherum mit der richtigen Initialisierung, auch der Perpipherie, kann man bequem von Bascom machen lassen, bis man sich halt auskennt.
  • Wenn irgendeine Berechnung oder Teil-Funktion nervt oder nicht gleich richtig hinhaut, schreibt man halt doch ein paar Bascom-Statements.
  • fürs Erste reicht die Demo-Version allemal

Die Nachteile:

  • Gott-weiß-wie komfortabel ist der Bascom-Assembler natürlich nicht, aber es reicht.
  • Bei manchen Befehlen ist es nicht klar, ob das ein Assembler oder ein Bascom-Befehl ist. In diesem Fall muß man ein "!" Rufzeichen davor setzen. Man erkennt das aber sofort, denn diese reservierten Bascom-Wort mach er sofort in Fettschrift. Trotzdem aufpassen !

Ein Grund-Programm

Nicht lachen, auch das ist ein Bascom-Programm:

$regfile = "m32def.dat"

$asm
   
$end Asm

End

Das Programm macht natürlich überhaupt nix. Aber durch die paar Zeilen hat Bascom alle notwendigen Initialisierungen schon erledigt und wir brauchen uns um nichts zu kümmern. Zwischen "$asm" und "$end asm" kann man nun nach Herzenslust irgendwas Assemblermäßiges reinschreiben und mit dem Simulator rumprobieren.

Auch "REGFILE" müßte man nicht hinschreiben, dann gilt eben das, was man in "OPTIONS/COMPILER/CHIP" eingestellt hat.

Der Zentral-Prozessor (CPU)

Das ist der Kollege, dem man mit "Assembler-Instruktionen" davon überzeugen muß, irgendwas zu tun. Ohne den läuft garnix. Der hat als Hilfe einen "Befehlszähler" (PC), der immer auf den nächsten Befehl zeigt, der drankommt. Und dann hat er noch eine Reihe "Register", das sind kleine Zwischenspeicher, mit denen er arbeiten kann. Die heissen einfach "R0", "R1",...."R31", also 32 Stück, in jedes paßt genau ein Byte, und ein Byte, das wissen wir, besteht wiederum aus 8 Bits.

Registerverwendung

An sich sind wir zwischen "$asm" und "$end asm" alleiniger Herrscher über den Mikrokontroller. Damit aber auch dem Bascom ein bißchen was zu Leben bleibt, sollten wir einige Register entweder in Ruhe lassen oder erst sichern und dann wieder herstellen.

 R4, R5         ' die beiden verwendet Bascom für temporäre Sachen. 
 R6             ' da speichert er einige Schalter (Bits)
 R8, R9         ' verwendet es für  "READ" und "RESTORE" etc. 
                ' haben wir sowas aber garnicht, ist es egal
 R28, R29       ' braucht Bascom aber nur, wenn wir Funktionen und Subs aufrufen. 

In den folgenden Beispielen brauchen wir uns aber darum nicht zu kümmern, es wird nix davon gebraucht. Ich wollt' es nur gesagt haben.

Daten-Transfer Operationen

Bevor wir mit diesen Registern irgendetwas ausprobieren können, müssen wir erstmal gezielt bestimmte Werte reinschreiben können. Sowas heißt eben "Transfer". Da wir ja erst am Anfang sind, reicht uns zum Beispiel:

LDI   R24, 14

Damit wird in das Register R24 der Binärwert von "14" reingestellt, das sind die Bits "00001110". Der maximale Wert, da es ja nur ein Byte ist, wäre "255", also "11111111". Für den Befehl "LDI" können wir übrigens leider nur die Register R16 - R31 setzen, das ist so eine Einschränkung von wegen "RISC" Architektur.

MOV   R3, R24

Deswegen auch der zweite Befehl "MOV", damit wird im Beispiel der Inhalt von R24 in das Register R3 kopiert. Somit können wir mit maximal zwei Befehlen also jeder beliebige Register von R0 bis R31 mit beliebigen Werten laden. Natürlich gibt es noch eine Menge mehr an Transferbefehlen, aber Listen von Assembler-Befehlen gibt es schon genug, da brauchen wir hier nicht auch noch eine.

Arithmetisch-Logische Operationen

Laden wir mal zwei Register:

LDI   R25, 17
LDI   R24, 14

Und jetzt die Grund-Befehle, Varianten später:

  • Arithmetisch
ADD   R25, R24       addieren      R25 + R24, Ergebnis nach R25
!SUB   R25, R24       subtrahieren
  • Logisch
!AND   R25, R24       "UND"
!OR    R25, R24       "ODER"
EOR   R25, R24       "Exklusiv-ODER"

Das Ergebnis steht immer in Operand-1

Gleich mal ausprobieren

$regfile = "m32def.dat"

$asm
 LDI   R25, 17        ' Laden
 LDI   R24, 14        ' Laden
 ADD   R25, R24       'addieren  17 + 14, Ergebnis in R25

 LDI   R25, 17        'Nachladen, da R25 durch "ADD" ja verändert wurde
 !SUB   R25, R24       'subtrahieren  17 - 14

 LDI   R25, 17        ' Laden
 LDI   R24, 14        ' Laden
 !AND   R25, R24       ' Es kommt überall dort "1" raus, wo sowohl r25 als auch R24 eine 1 haben
 
 LDI   R25, 17        ' Laden
 LDI   R24, 14        ' Laden
 !OR    R25, R24       ' Es kommt überall dort "1" raus, wo r25 oder R24 eine 1 haben
                      '  (ODER BEIDE !)

 LDI   R25, 17        ' Laden
 LDI   R24, 14        ' Laden
 EOR   R25, R24       ' Es kommt überall dort "1" raus, wo ENTWEDER  r25 oder R24 eine 1 haben
                      '  (ABER NICHT BEIDE !)


$end Asm

End

Zum Probieren ist das am besten mit dem Simulator. (Register-Fenster öffnen und Einzelschritte)

Ergebnis prüfen

Normalerweise ist es ja nicht so, daß vor solchen Operationen die Rechenwerte direkt geladen werden, sondern die kommen ja von irgendwo aussen her. Und da muß man ja dann anders reagieren, je nachdem, ob die Werte gleich waren, ob r25 größer oder kleiner als r24 war, und so weiter.

Da helfen die "Flags" im Status-Register (SREG). Das ist zwar auch ein normales Byte, nur haben die einzelnen Bits darin eine spezielle Bedeutung und geben eben nähere Auskunft über die gerade abgelaufenen Operation. Nur das Wichtigste:

  • ZERO-Bit Es wird automatisch gesetzt, wenn das Ergebnis genau NULL ergeben hat.
  • CARRY-Bit Es wird automatisch gesetzt, wenn es einen "Übertrag" gegeben hat

Man kann diese (und noch andere) Flags sehen, wenn man im Simulator auf "µP" drückt.

Z = ZERO
C = CARRY

Beispiele:

LDI   R25, 17       
LDI   R24, 14       
!SUB   R25, R24       

Zero & Carry sind nicht gesetzt, denn das Ergebnis ist ungleich NULL, und "17" ist außerdem größer als "14"

LDI   R25, 17       
LDI   R24, 17       
!SUB   R25, R24       

Jetzt ist Zero gesetzt, denn das Ergebnis ist gleich NULL

LDI   R25, 12       
LDI   R24, 44       
!SUB   R25, R24       

Jetzt ist das Carry-Bit gesetzt, denn "12" ist ja kleiner als "44", das Ergebnis ist also negativ, und ein "Übertrag" ist auch aufgetreten.

Vergleichen

"Vergleichen" ist für die ALU (Recheneinheit) das Gleiche wie Subtrahieren (SUB), nur daß das eigentliche Rechenergebnis nirgends hingeschrieben wird und NUR DIE FLAGS gesetzt werden.

CP  R25, R24       

Verzweigen

Wir haben ja gesagt, es wird verglichen, damit der Rechner je nach Vergleichs- der Rechenergebnis was anderes tut. "Was anderes tun" heißt anderer Code, also muß der "Befehlszähler" einen anderen Wert bekommen, damit der Programmablauf dort fortgesetzt wird. Dazu gibt es natürlich die "unbedingten" Varianten

JMP  Zieladresse  ' oder
RJMP Zieladresse  ' das nimmt man, wenn das Ziel in der Nähe ist

Oder eben die "Verzweigung unter bestimmten Bedingungen" (conditional branch)

BRxxx Zieladresse 

Für "xxx" (Bedingung) gibt es nun eine ganze Reihe Möglichkeiten. Es gibt im Prinzip für jedes Bit im Status-Register (s.o) eine Abfrage "wenn gesetzt" und "wenn nicht gesetzt".

Die wohl wichtigsten sind die Möglichkeiten, die sich aus dem "ZERO"- und dem "CARRY"-Flag ergeben:

BREQ Zieladresse   ' Verzweigen, wenn "GLEICH"  (equal)                      Zero  = 1
BRNE Zieladresse   ' Verzweigen, wenn "NICHT GLEICH"  (not equal)            Zero  = 0
BRLO Zieladresse   ' Verzweigen, wenn "KLEINER"  (lower)                     Carry = 1
BRSH Zieladresse   ' Verzweigen, wenn "GLEICH ODER GRÖSSER" (same or higher) Carry = 0

Und, die Überraschung, ausgerechnet sowas Häufiges wie

Verzweigen, wenn "GRÖSSER"

gibt's überhaupt nicht. Nun, dazu müßten ja eigentlich zwei Flags abgefragt werden. "Größer" heißt nämlich CARRY = 0 UND ZERO = 0. Und das ist in der "RISC" Welt nicht drin, da wird gespart.

Beispiel

Lieber gleich ein Beispiel zum Ausprobieren und Festigen, das war ja doch etwas gebündelt. Aber davor gleich noch eins drauf: Eine "Zieladresse" ist der (im ganzen Programm) eindeutige Name eines Befehls (ein "Label"), der in der Zeile ganz links beginnt und mit Doppelpunkt abgeschlossen wird

Flußdiagramm

  • Theoretisch sieht das ja so aus:

Compare1.png

  • Da es aber keiner Programmierspache möglich ist, alternativen Code nebeneinander zu schreiben, muß dieser Teil auf "Spaghetti"-Code umstrukturiert werden.

"Hochsprachen" machen das versteckt im Maschinencode, beim Assembler müssen wir selbst machen. Und natürlich auch "GOTO" (=JMP) verwenden, ein sonst in allen Büchern als "no, no" (=pfui) beschriebener Befehl.

Die Praxis

Compare2.png


Programm_Beginn:                            ' das ist zum Beispiel gleich ein "Label"
          LDI         R25, 12       ' R25 = 12
          LDI         R24, 44       ' R24 = 44
'--------------------------------------
'  nun der Vergleich   
'--------------------------------------
          CP          R25, R24       
          BREQ        Label_1    ' Verzweigen nach "Ziel", wenn R25 = R24

          LDI         R16, 1      ' das machen wir (zum Beispiel), wenn R25 NICHT= r24 ist 
          RJMP        Label_2    'wir müssen unbedingt springen, sonst laufen wir ja 
                                         ' in den Zweig "ist_gleich"  rein
Label_1:
          LDI         R16, 0      ' das machen wir (zum Beispiel), wenn R25 = r24 ist 

'----------------------------     ' da treffen wir uns wieder
Label_2:            
         da geht er wieder gemeinsam weiter 


Ich kann nur dringend empfehlen, sich mit diesem Beispiel zu beschäftigen und auch mit anderen Werten rumzuprobieren, das "bedingte Verzweigen" in allen Varianten ist das A und O der Programmiererei, beim Assembler eben auch ein bißchen verschärft.

Eine Alternative: Bedingtes "Skip"

Was der AVR noch anbietet, ist eine Reihe von "SKIP IF" Befehlen. Für unseren Registervergleich gibt es aber nur den

CPSE Register, Register 

Befehl. Er bedeutet:

"Vergleiche die Register, und wenn die Inhalte gleich sind, überspringe den nächsten Befehl"

Das wird uns das Herumspringen und das Verwenden von Labeln erspart. Allerdings kann immer nur EIN Befehl übersprungen werden

Skip.png


eine Besonderheit hat der Befehl noch: Da er ja Vergleich und Bedingungsabfrage in Einem ist, werden auch keine Flags im Statusregister (SREG) verändert. Das ist praktisch, wenn man diese Flags durch eine andere Operation vorher gesetzt hat, und sie über diesen Vergleichs + Sprung - Befehl darüber-retten will. Das ist aber im Moment schon etwas fortgeschritten.

Kurze Zusammenfassung

  • Wir können also beliebige Register mit beliebigen Werten laden,
  • Wir können mit diesen Werten rechnen oder sie vergleichen
  • Und je nach Vergleichs- oder Rechenergebnis unterschiedlichen Code durchlaufen.
  • Man könnte aber auch ein paar Lehren daraus ziehen:
    • die Register R16 - R31 braucht man unter Umständen für Zwischenschritte, um Werte in die Register R0 - R15 laden zu können. Man sollte also diese Register nicht zu schnell fest belegen und vollräumen, damit man dafür noch Spielraum behält.
    • Auch doch recht simple IF .. ELSE Konstrukte können ein gewisses vorher überlegtes Konzept brauchen, sonst verliert man schnell den Überblick. Ein Blatt Papier und ein Bleistift sind also recht hilfreich. Assembler schreibt man nicht einfach in den Bildschirm rein.

Schleifen

Eigentlich ist das ja nichts speziell Assembler-spezifisches, aber was soll's.

Flußdiagramme

Es gibt zwei Grundmuster für Schleifen (Befehlswiederholungen).

  • WHILE "solange Bedingung erfüllt ist, mache was"

While.png

  • DO...LOOP WHILE "mache was, solange Bedingung erfüllt ist"

DoWhile.png

Der Unterschied ist wichtig: Bei "WHILE" wird nur was gemacht, wenn die Bedingung schon zutrifft, Bei "DO..WHILE" werden die Befehle auf jeden Fall wenigstens einmal ausgeführt, erst dann wird gecheckt, ob wiederholt werden soll.

Praxis

Theoretisch sieht das ja gut aus, und mit Hochsprachen kann man das auch meist so formulieren. Beim Assembler geht das aber nur so schön übersichtlich, wenn man nur eine einzelne Bedingung hat. Eine einfache Zähl-Schleife in der "WHILE" Version:

$regfile = "m32def.dat"

$asm
   LDI   r25, 0         ' R25 = 0
   LDI   r24, 1         ' R24 = 1
SchleifenBeginn:
   CPI   R25, 12           ' Der Befehl ist neu: vergleiche R25 mit dem festen Wert "12" 
   BREQ  SchleifenAusgang  ' Wenn R25 = 12, verlassen wir die Schleife
   ADD   R25, R24          ' auf R25 den Wert von R24 draufaddieren 
   RJMP  SchleifenBeginn   ' und wieder rauf zur Prüfung
SchleifenAusgang:
   ...
$end Asm

End

Was geschieht, ist klar: R25 beginnt mit Null. Wenn der R25 NICHT= "12", addieren wir "1" auf R25 und wiederholen das Ganze. Wenn R25 = "12", verlassen wir die Schleife.


Nehmen wir aber an, wir hätten zwei Bedingungen (es geht hier nicht um Sinn oder Unsinn der Abfrage):

  • WHILE R25 NICHT= "12 UND R24 = "1"
SchleifenBeginn:
   CPI   R25, 12           ' Der Befehl ist neu: vergleiche R25 mit dem festen Wert "12" 
   BREQ  SchleifenAusgang  ' Wenn R25 = 12, verlassen wir die Schleife
   CPI   R24, 1            ' s.o
   BRNE  SchleifenAusgang  ' Wenn R24 NICHT= 1, verlassen wir die Schleife
   ADD   R25, R24          ' auf R25 den Wert von R24 draufaddieren 
   RJMP  SchleifenBeginn   ' und wieder rauf zur Prüfung
SchleifenAusgang:
  • WHILE R25 NICHT= "12 ODER R24 < R25
SchleifenBeginn:
   CPI   R25, 12            
   BREQ  R25_ist_12                 
SchleifenBody:
   ADD   R25, R24           
   RJMP  SchleifenBeginn  
R25_ist_12:
   CP    R24, R25
   BRLO  SchleifenBody
SchleifenAusgang:           
   ...

Wenn wir da nicht im Kommentar dazuschreiben, worum es geht, kennt sich ein Fremder erst nach einiger Überlegung aus.

Tips

Mehrere Bedingungen in eine UND-ODER Beziehung sind immer fehleranfällig und leicht unübersichtlich

  • Als Erstes immer die RICHTIGE (und am besten verständliche) Lösung suchen, und erst dann durch Umformungen die "SCHÖNE" Lösung.
  • Also nochmal das obige "ODER" Beispiel, erst in der vollen Grundform
WHILE  ( R25 NICHT= "12 ) ODER  ( R24 < R25 )
SchleifenBeginn:

   CPI   R25, 12           ' R25 <=> 12
   BREQ  R25_ist_12
   JMP   R25_ist_nicht_12

   CP    R24, R25
   BRLO  r24_ist_kleiner_r25
   JMP   r24_ist_nicht_kleiner_r25


   ADD   R25, R24          ' der "BODY" steht ja fest 
   RJMP  SchleifenBeginn   ' das ist auch sicher

SchleifenAusgang:          ' ausgang gibt es (eigentlich) immer
   ...

Das fehlt was ? Ja, denn jetzt erst sollten wir die Ziele auch hinschreiben


1. Wir machen den "body" immer, wenn r25 nicht gleich 12

also schreiben wir das hin

SchleifenBeginn:

   CPI   R25, 12           ' 
   BREQ  R25_ist_12
   JMP   R25_ist_nicht_12  ' abgehakt

   CP    R24, R25
   BRLO  r24_ist_kleiner_r25
   JMP   r24_ist_nicht_kleiner_r25

R25_ist_nicht_12:          ' 
   ADD   R25, R24          ' 
   RJMP  SchleifenBeginn   ' 

SchleifenAusgang:          ' 
   ...

2. Wir machen den "body" immer, wenn r24 kleiner als r25

SchleifenBeginn:

   CPI   R25, 12            
   BREQ  R25_ist_12
   JMP   R25_ist_nicht_12           ' abgehakt

   CP    R24, R25
   BRLO  r24_ist_kleiner_r25        ' abgehakt
   JMP   r24_ist_nicht_kleiner_r25  ' 

r24_ist_kleiner_r25:
R25_ist_nicht_12:           
   ADD   R25, R24           
   RJMP  SchleifenBeginn    

SchleifenAusgang:           
   ...

Anmerkung: wir können an der selben Stelle beliebig viele Label vergeben

3. Was ist, wenn r25 = 12 ? dann müssen wir die zweite Bedingung prüfen (ist ja ein ODER)

SchleifenBeginn:

   CPI   R25, 12            
   BREQ  R25_ist_12                 ' abgehakt
   JMP   R25_ist_nicht_12           ' abgehakt
R25_ist_12:
   CP    R24, R25
   BRLO  r24_ist_kleiner_r25        ' abgehakt
   JMP   r24_ist_nicht_kleiner_r25  ' 

r24_ist_kleiner_r25:
R25_ist_nicht_12:           
   ADD   R25, R24           
   RJMP  SchleifenBeginn    

SchleifenAusgang:           
   ...

4. Bleibt nurmehr "r24 ist nicht kleiner r25". Da geht's offenbar dann hin, wenn KEINE der Bedingungen erfüllt ist, also: raus aus der Schleife

SchleifenBeginn:

   CPI   R25, 12            
   BREQ  R25_ist_12                 ' abgehakt 
   JMP   R25_ist_nicht_12           ' abgehakt 

R25_ist_12:
   CP    R24, R25
   BRLO  r24_ist_kleiner_r25        ' abgehakt
   JMP   r24_ist_nicht_kleiner_r25  ' abgehakt

r24_ist_kleiner_r25:
R25_ist_nicht_12:           
   ADD   R25, R24           
   RJMP  SchleifenBeginn  
  
r24_ist_nicht_kleiner_r25:
SchleifenAusgang:           
   ...

Jetzt ist das Ganze zwar nicht elegant, aber richtig und leicht nachvollziehbar.

Wenn der Sprungbefehl und das Ziel unmittelbar hintereinander stehen, können wir uns den Sprung sparen. Also bauen wir etwas um, damit das auch so ist:

SchleifenBeginn:

   CPI   R25, 12            
   BREQ  R25_ist_12                 '
   JMP   R25_ist_nicht_12           ' steht jetzt direkt dahinter

r24_ist_kleiner_r25:                'den ganzen Block raufgeschoben 
R25_ist_nicht_12:           
   ADD   R25, R24           
   RJMP  SchleifenBeginn  

R25_ist_12:
   CP    R24, R25
   BRLO  r24_ist_kleiner_r25        '
   JMP   r24_ist_nicht_kleiner_r25  ' steht jetzt direkt dahinter
 
r24_ist_nicht_kleiner_r25:
SchleifenAusgang:           
   ...

Und kürzen:

SchleifenBeginn:
   CPI   R25, 12            
   BREQ  R25_ist_12                 
r24_ist_kleiner_r25:      
   ADD   R25, R24           
   RJMP  SchleifenBeginn  
R25_ist_12:
   CP    R24, R25
   BRLO  r24_ist_kleiner_r25        
SchleifenAusgang:           
   ...


Ob das nun ein "WHILE" oder ein "DO..WHILE" wird, hängt nurmehr davon ab, wo wir zu Beginn in die Befehlsfolge reinspringen.

  • Von oben weg, wie es dort steht, ist es eine "WHILE" Schleife
  • Eine "DO..WHILE" Schleife (Bedingung am Ende prüfen) wird es, wenn wir zuerst mit dem "Body" beginnen. Also
   JMP   r24_ist_kleiner_r25     ' Erst die Aktion, DANN die Bedingung prüfen 
SchleifenBeginn:
   CPI   R25, 12            
   BREQ  R25_ist_12                 
r24_ist_kleiner_r25:      
   ADD   R25, R24           
   RJMP  SchleifenBeginn  
R25_ist_12:
   CP    R24, R25
   BRLO  r24_ist_kleiner_r25        
SchleifenAusgang:           
   ...

Ist doch praktisch ?

Assembler-mäßig ist das nun ok und erträglich. Aber mit dem theoretischen WHILE-Flußdiagramm hat das nun nicht mehr viel gemeinsam.

Weg vom Simulator auf den µC

Jetzt wird's Zeit, die ersten Schritte in die AVR-Realität zu machen, immer Simulator ist ja langweilig, fast so, als würden wir im Trockenen schwimmen lernen müssen.

Bis jetzt haben wir vom Bascom ja nur die äussere Programmhülle verwendet, jetzt soll er doch auch wirklich was tun.

Datenaustausch mit BasCom

Das funktioniert über Datenfelder, die wir irgendwie definieren müssen. Weil's so einfach ist, lassen wir das erstmal Bascom übernehmen. Das ist kein Rückschritt auf dem Weg zum Assemblerprogrammierer, denn das Definieren von Daten ist ja nichts anderes, als Felderen im SRAM (also im eigentlichen Arbeitsspeicher) Namen zuzuweisen. Über den Namen kann man diese Felder dann auch im Assembler ansprechen.

  • Bascom-->Assembler

Damit Bascom ein Feld für uns definiert, sagen wir einfach (aber außerhalb der "$asm" / "$end asm" Bereiches)

DIM Meinfeld AS BYTE 

Im Bascom, das weiß der Leser vielleicht ja schon, kann man da Werte reinschreiben, so wie wir das mit dem "LDI" Befehl bei Registern gemacht haben

 Meinfeld = 85 

Jetzt steht an irgendeiner Speicherstelle (egal wo, wir sprechen es eh' nur über den Namen an) der Binärwert von 85 (Hexadezimal &H55 oder in Bits &B01010101). Dieses Feld können wir aber nun auch mit dem Assembler lesen:

$regfile = "m32def.dat"

Dim Meinfeld As Byte           ' definition

   Meinfeld = 85               'Wertzuweisung

   $asm
   lds   r24, {Meinfeld}        '(das sind zwei geschwungene Klammern)
                                ' das ist wieder ein neuer Befehl: "LDS"
   $end Asm
End

"LDS" lädt in ein beliebiges Register den Wert von der Speicherstelle, die "Meinfeld" heißt.

  • Assembler-->Bascom

Das geht auch umgekehrt. Wenn wir den Bascomteil noch etwas vervollständigen

$regfile = "m32def.dat"
$crystal = 8000000               ' der Quartz, den wir verwenden
$baud = 9600                     ' die Baudrate für das Terminal
                                 ' dadurch macht Bascom alle Einstellungen, die wir für das 
                                 ' Terminal brauchen

Dim Meinfeld As Byte      ' definition

   $asm
   LDI   R24, 85
   STS   {Meinfeld}, R24  ' "STS" ist das Gegenstück zu "LDS", also vom Register zum Speicher
   $end Asm

   PRINT  str(Meinfeld)   ' Und schon gibt uns Bascom den Wert (dezimal) auf dem Terminal aus

End


  • Das ist der Vorteil den wir haben, wenn wir Assembler mit Bascom anfangen und nicht mit einem "richtigen" Assembler wie das AVR-Studio. Denn das Gefummel mit der Terminalausgabe erledigt alles Bascom, sonst müßten wir es selbst erst wo abschreiben oder lernen, bis wir auch nur einen Pieps auf dem Terminal sehen.


  • Wenn es interessiert: Das, was wir zuletzt im Assembler geschrieben haben, ist auch genau das, was Bascom an Maschinencode produziert, wenn wir
Meinfeld = 85

hinschreiben

  • Bascom-->Assembler-->Bascom

Noch immer können wir nur mit fest einprogrammierten Werten arbeiten, d.h. für was anderes als "85" müssen wir ändern, übersetzen und brennen.

Wir wollen nun versuchen, etwas über das Terminal einzugeben, bearbeiten und dann wieder ausgeben.

Flußdiagramm

InOut.png

Den Input lassen wir Bascom übernehmen. INKEY() holt einen Wert von der Tastatur. Er wartet aber nicht, bis was gedrückt wird, sondern gibt einfach NULL aus, wenn nix da ist. Wir arbeiten also nur etwas, wenn ein Wert > NULL da ist.

  • Zuerst übernehmen wir nur den Part "Bearbeitung"
$regfile = "m32def.dat"
$crystal = 8000000               ' der Quartz, den wir verwenden
$baud = 9600                     ' die Baudrate für das Terminal

Dim Meinfeld As Byte             
DO
   Meinfeld = INKEY()            '"EINGABE"
   IF Meinfeld <> 0 THEN         '"BEDINGUNG"
       $asm                      '"BEARBEITUNG"
       lds   r24, {Meinfeld}
       '-------- Da kommt dann Code rein
       STS   {Meinfeld}, R24  
       $end Asm
       PRINT  str(Meinfeld)      '"AUSGABE"
   END IF

   LOOP                          'Wiederholung
End

So, wie das Beispiel jetzt ist, ändern wir aber nichts wirklich, sondern schauen mal, was passiert.

Und es passiert Merkwürdiges:

Wenn wir das laufen lassen und in vielleicht gewohnter Manier "85" tippen und <ENTER> drücken, erscheint auf dem Terminal

56
53
13

Einerseits ist es ja klar, wenn wir drei Tasten drücken, kriegen wir auch dreimal was zurück, aber wie bekommen wir dann in EIN Byte EINE Zahl "85" ?

Also ein kurzer Side-Step.

Das Byte und seine vielen Bedeutungen

Nach wie vor ist klar: Ein Byte besteht aus 8 Bit, die in 256 Möglichkeiten kombiniert werden können.

Das sagt aber nicht aus, was irgendeine dieser Bit-Kombinationen eigentlich bedeutet.

  • Das Byte als Bit-Container

Da hat jedes Bit seine eigene Bedeutung, unabhängig von den anderen. Also einfach "Schalter" und/oder JA-Nein Anzeigen.

Typisch: Status-Register (SREG).

  • Das Byte als (binär) Zahl. Hier repräsentieren die 256 Möglichkeiten wirklich die Zahlen von 0-255. Mit diesen Zahlen kann nach den binären Regeln gerechnet werden.
  • Das Byte als zwei BCDZahlen. Da besteht das Byte eigentlich aus zweimal 4 Bit ("Nibbles"). Jedes Nibble stellt die Zahlen 0 - 9 dar. Ist inzwischen eine recht exotische Verwendung, überhaupt als "gepacktes" Format, wo in einem Nibble auch noch ein Vorzeichen reinkodiert wurde.
  • Das Byte als (binär) Zahl mit Vorzeichen. Das ist eine Kombination von Zahl und Schalter. Das Bit 2^^7 zeigt das Vorzeichen an. Solange es NULL ist, können die restlichen 7 Bit für Zahlen von (+) 0 - 127 normal verwendet werden. Ist das Vorzeichenbit = "1", gelten die anderen Bit als Negativ-Zahl. Und sie werden als 2- Komplement dargestellt, d.h. 11111111 = -1. Dadurch geht das Bereich von -128 (10000000) bis +127 (01111111).
  • Das Byte als Zahl, die Zahl ist aber ein Tabellenwert in einer standardisierten Tabelle.

Typisch: ASCII und seine Varianten. Was heißt das ? Nun, gespeichert und hin-und hergeschickt wird zum Beispiel 01000001, das bedeutet aber nicht &H41 oder dezimal 65, sondern das, was in der ASCII-Tabelle an der Stelle 65 steht, und das ist ein großes "A".

Ich werd' mich hüten, hier eine ASCII-Tablle reinzustellen, die gibt es im Internet in Massen. Festgelegt sind die Werte 0 - 127. Wichtig ist nur die Gruppierung:

  • 0-31 sind "Steuerzeichen" für die Kommunikation. ("13" stellt das berühmte <ENTER> dar)
  • 32 bezeichnet eine Leerstelle ("blank")
  • 48 - 57 für die Ziffern 0 - 9
  • 65 - 90 Großbuchstaben "A" bis "Z"
  • 97 - 122 Kleinbuchstaben "a" bis "z"

Numerischer Input vom Terminal

Nachdem sich offenbar unterscheidet, was wir direkt vom Terminal bekommen und was wir davon brauchen können, brauchen wir ein zweites Datenfeld.

$regfile = "m32def.dat"
$crystal = 8000000               ' der Quartz, den wir verwenden
$baud = 9600                     ' die Baudrate für das Terminal

Dim Meinfeld As Byte             ' das bekommen wir vom Terminal
Dim Meinezahl As Byte            ' das soll die eigentliche Zahl werden

DO
   Meinfeld = INKEY()            '"EINGABE"
   IF Meinfeld <> 0 THEN         '"BEDINGUNG"
       $asm                      '"BEARBEITUNG"
       '....   CODE .... (s.u.)
       $end Asm
       PRINT  str(Meinezahl)     'die zeigt er nach jeder Taste an
   END IF
   LOOP                          'Wiederholung
End

Soweit das Gerüst bzw. der Rahmen. In "Meinfeld" stellt der Bascom rein, was getippt wurde und wir müssen das verarbeiten und stellen das jeweilige Ergebnis nach "Meinezahl" rein.

Was ist zu tun ?

  • Prüfen, was getippt wurde.
  • davon abhängig
    • Meinfeld=13 (<ENTER>). Die bisher bekommene Zahl ist komplett, jetzt könnten wir mit ihr irgendetwas rechnen oder sonstwas tun.
    • Meinfeld= 48-57 ('0'-'9').
      • Das, was bisher in "Meinezahl" steht, multiplizieren wir mit 10, an der Einerstelle steht nun auf jeden Fall einen NULL, die bisherigen Zahlen links davon.
      • Und dann müssen wir das, was reingekommen ist, auf binär 0-9 umwandeln und draufaddieren.
    • Alles Andere ignorieren wir einfach.
$regfile = "m32def.dat"
$crystal = 8000000                                          ' der Quartz, den wir verwenden
$baud = 9600                                                ' die Baudrate für das Terminal

Dim Meinfeld As Byte
Dim Meinezahl As Byte
Do
   Meinfeld = Inkey()                                       '"EINGABE"
   If Meinfeld <> 0 Then                                    '"BEDINGUNG"
       $asm                                                 '"BEARBEITUNG"
       lds     r22, {Meinezahl}                             'die bisherige Zahl
       lds     r24, {Meinfeld}                              'die neue Ziffer
       cpi     r24, 13                                      '<ENTER> ?
       brne    Is_numerisch
       ' ENTER Gedrückt
       ' Zahl verarbeiten
       ' dann löschen für ein Neues
       clr   r22
       rjmp    Fertig
Is_numerisch:
       cpi     r24, 48                                      '48 = '0'
       brlo    Fertig                                       'keine Ziffer-->ignorieren
       cpi     r24, 57 + 1                                  '57 = '9'
       brsh    Fertig                                       'keine Ziffer-->ignorieren
Einfügen:
' Bisherige Zahl * 10   Methode:  n * 10 => n (8 + 2) => n * 8 + n * 2
       lsl     r22                                          ' (n * 2)
       mov     r23, r22                                     ' kopieren
       lsl     r23                                          ' (n * 2) * 2
       lsl     r23                                          ' (n * 2 * 2) * 2
       add     r22, r23                                     'n * 8 + n * 2
' neue Ziffer umwandeln
       andi    r24, 15
' und addieren
       add     r22, r24
Fertig:
       STS   {Meinezahl}, R22
       $end Asm
       Print Str(meinezahl)                                 '"AUSGABE"
   End If
   Loop                                                     'Wiederholung
End

Da sind ein paar Sachen dabei, die hatten wir noch nicht

  • Ich beginne mal von hinten: Bei "Fertig" wird das Register R22 nach "Meinezahl" geschrieben, Bascom zeigt es dann her. Wir müssen also sehen, das in R22 immer das Richtige drin steht.
  • Also, jetzt wieder von vorn, wir holen den bisherigen Wert von "Meinezahl" gleich mal ins R22.

(Das ist am Anfang einfach NULL)

  • Dann die neue Eingabe in R24
  • Die vergleichen wir mir "13" == <ENTER>
    • ist es eine andere Eingabe (BRNE), schauen wir bei "Is_numerisch" weiter
    • Wenn aber ja, könnten wir jetzt was tun. Wir machen aber weiter nichts, löschen aber R22 und damit "Meinezahl" (s.o) auf NULL, damit wir für eine neue Eingabe bereit sind.
  • Is_numerisch:
    • Die Eingabe ist kleiner als 48 ('0'), keine Ziffer, wir sind auch schon fertig
    • Jetzt müßten wir vergleichen, ob die Eingabe größer als 57 ('9') ist. Das geht nicht (siehe weiter oben bei den Verzweigungen), also vergleichen wir mit 57+1, bei gleich oder größer ist die Eingabe auch keine Ziffer, also --> fertig.
  • Einfügen:
    • Bisheriger Wert * 10: Bei manchen µC gibt es einen Multiplikationsbefehl, aber diese einfache Rechnung machen wir zu Fuß, ist einfach mehr Spaß und mehr neue Befehle.

n * 10 kann man ja zerlegen in n*(8+2). Und sowohl 8 als auch 2 sind ja 2-er Potenzen, da kann einfach Bit-weise nach links schieben.

"LSL  R22"  ist gleich  R22 * 2   "LSL  Logical Shift Left"

damit haben wir schon mal Meinezahl * 2 gerechnet. Das kopieren wir nach R23 und schieben dort noch zweimal nach links. In R23 steht nun also insgesamt Meinezahl * 8. Das addieren wir nun.

  • Umwandeln: Die Ziffern '0'-'9', also 48 - 57 schauen hexadezimal ja so aus:
0x30 = dezimal 48 = ASCII '0' bis 
0x39 = dezimal 57 = ASCII '9'

Wir brauchen also nur die '3' irgendwie zu löschen, dann haben wir sofort unsere 0-9. Das geschieht durch

ANDI     R24, 15          ' "AND" mit einer Konstanten 

In R24 bleiben nur die 1-er übrig, wo auch in '15' ein 1-er ist. Damit ist die '3' weg.

  • Addieren
ADD  R22, r24           'Meinezahl + neuerWert

und fertig.


Eine Anmerkung: Wir haben noch keine Prüfung, ob die Zahl für ein Byte nicht zu groß wird. Wenn wir also 9999 eintippen, kommt Schrott heraus.

Mehr als ein Byte

Mit einzelnen Bytes geht es ja jetzt schon recht gut. Aber es gibt ja auch größere Zahlen.

16 Bit Zahlen

Das nächstgrößere ist eine 16-Bit Zahl. Bei Bascom ist das dann ein

  • WORD für den Zahlenbereich 0 - 65535 und
  • INTEGER für -32768 bis +32767

Beides wird in zwei hintereinanderliegenden Bytes gespeichert, also

MeinByte und 
MeinByte + 1

Und zwar so, daß erst das niederwertigere und dann das höherwertige Byte gespeichert werden. Also die 16-Bit Binärzahl

0001001000110100 = Hexadezimal &H1234 

steht im Speicher in zwei Bytes als

00110100 00010010 =  &H34 &H12

Genauso ist es mit den Registern, üblicherweise steht das erste Byte in einem geradzahligen Register und das zweite im nächsthöherem. z.B.:

R24: 00110100 =  &H34
R25: 00010010 =  &H12

Sowas nennt man dann "Register-Paar".

Der AVR µC kann einige seiner 32 Register als Registerpaar verwenden. Das sind

R0:R1       (aber nur bei den Multiplikationsbefehlen für das Ergebnis)
R24:R25     (für 16-Bit Berechnungen)
R26:R27     (für 16-Bit Berechnungen und als "Pointer" X)
R28:R29     (für 16-Bit Berechnungen und als "Pointer" Y)
R30:R31     (für 16-Bit Berechnungen und als "Pointer" Z)

Wobei es aber nur zwei solche Berechnungen gibt

ADIW     R24 , zahl       ' addieren zahl auf r24:r25
SBIW     R24 , zahl       ' subtrahieren zahl von r24:r25

Die anderen 16-Bit Befehle sind eher Adress-Arithmetik.

  • Datenaustausch Bascom <=> Assembler

Um ein WORD (oder INTEGER) mit Bascom auszutauschen, geht z.B. folgendes

 DIM word_1 AS WORD
     $asm
     LDS      R24, {word_1}    'holen Byte 1  (bits 0-7)
     LDS      R25, {word_1+1}  'holen Byte 2  (bits 8-15)
     ADIW     R24, 42          'addieren von 42 auf das Paar r24:r25
     STS      {word_1}, R24    'speichern Byte 1
     STS      {word_1+1}, R25  'speichern Byte 2
     $end asm
  • Arithmetik mit 16 Bit

Der Unterschied zur 8-Bit Verarbeitung ist eigentlich nicht groß. Es gibt zu allen Arithmetik Befehlen des AVR eine "Carry"-Variante, die einen Übertrag von der Aktion vorher berücksichtigt.

Angenommen zwei 16-Bit Werte in den Registerpaaren R24:R25 und R22:R23

 ADD    R24, R22           'Addieren der niederwertigen Bytes
 ADC    R25, R23           'Addieren der höherwertigen Bytes + ev. Überlauf

 SUB    R24, R22           'Subtrahieren der niederwertigen Bytes
 SBC    R25, R23           'Subtrahieren der höherwertigen Bytes + ev. Überlauf
  • Vergleichen mit 16 Bit
 CP     R24, R22           'Vergleich der niederwertigen Bytes
 CPC    R25, R23           'Vergleich der höherwertigen Bytes + ev. Überlauf
           ' jetzt können die normalen "bedingte Verzweigung"-Befehle verwendet werden

32 Bit Zahlen

Bei Bascom heissen die LONG und die sind immer Vorzeichenbehaftet. Im AVR Instructionset gibt es keine 32-Bit Befehle. Die physische Speicherung ist wie bei den 16-Bit Feldern

 DIM long_1 AS LONG
     $asm
     LDS      R24, {long_1}    'holen Byte 1  (bits 0-7)
     LDS      R25, {long_1+1}  'holen Byte 2  (bits 8-15)
     LDS      R26, {long_1+2}  'holen Byte 3  (bits 16-23)
     LDS      R27, {long_1+3}  'holen Byte 4  (bits 24-31)
   ...


  • Arithmetik mit 32 Bit

Eigentlich läuft das ab 16-Bit immer gleich ab, und man kann genauso 24-Bit wie 40 oder 48 Bit rechnen. Angenommen zwei 32-Bit Werte in den Registern R16->R19 und R20->R23

 ADD    R16, R20           'Addieren der niederwertigen Bytes
 ADC    R17, R21           'Addieren des nächst-höherwertigen Bytes + ev. Überlauf
 ADC    R18, R22           'Addieren des nächst-höherwertigen Bytes + ev. Überlauf
 ADC    R19, R23           'Addieren des nächst-höherwertigen Bytes + ev. Überlauf

Single und Double Zahlen

das sind 32- bzw. 64-Bitzahlen, allerdings haben die eine eigene Float-Struktur. Das ist dann schon ein eigenes Thema, mit denen was zu machen.

Strings

Strings sind Bytefolgen, in denen ASCII-Zeichen einfach von links nach rechts gespeichert werden können. Das Ende-Kennzeichen der immer variabel langen Texte ist ein NULL-Byte. Daher braucht in Bascom ein String für (max) 20 Zeichen lange Strings auch in Wirklichkeit 21 Byte Speicher.

Strings sind gut geeignet, eine andere Art vorzustellen, wie man Daten lesen oder speichern kann: Über POINTER-Register

Gleich ganz in die Praxis: Wir wollen die Länge eines Bascom-Strings festellen, natürlich im Assembler

$regfile = "m32def.dat"
$crystal = 8000000                                          ' der Quartz, den wir verwenden
$baud = 9600                                                ' die Baudrate für das Terminal

Dim Text As String * 20                                     ' das ist der String
Dim Strlen As Byte                                          ' da sol die Länge rein

      Text = "Hello, world !"                               ' wir befüllen den String

      $asm
      Loadadr Text , X            '"adresse von Text nach R26:r27" 
      clr      r24                'löschen vom Zähler
Schleife: 
      ld       r22, X+            ' s.u.
      cpi      r22, 0             'Vergleichen mit NULL
      breq     Fertig             'fertig
      inc      r24                'Zähler erhöhen
      rjmp     Schleife           'weiter
Fertig:
      sts   {strlen}, r24         'ergebnis ablegen
      $end Asm

      Print Str(strlen)           'herzeigen

End
  • Loadadr ist ein Bascom-Statement, mit dem das Registerpaar R26:R27 mit der Adresse von "Text" geladen wird. In einem "echten" Assembler kann man das auch assemblermäßiger formulieren, aber in Bascom muß man das so machen
  • LD
     ld       r22, X+        'Das Zeichen, wo X hinzeigt, nach R22 und X um eins erhöhen

Den Rest kennen wir eigentlich schon.


ST ist übrigens das Gegenstück zu LD

     st       X+, r22        'R22 dorhin, wo X hinzeigt, und dann X um eins erhöhen

Bascom als Messlatte

Eine sehr gute Möglichkeite, Assembler zu trainieren, ist es, irgendwelche Bascom-Statements durch eigene Inline-Assembler-Blöcke zu ersetzen. Dabei kann man immer auch Varianten ausprobieren.

  • Beispiel
DIM  Wert1 AS WORD
DIM  Wert2 AS WORD
        Wert1 = Wert2   ' Bascom-Statement

'als Inline-Assembler  EINE Möglichkeit
        $asm
        LDS     R24, {Wert1}
        STS     {Wert2}, R24
        LDS     R24, {Wert1+1}
        STS     {Wert2+1}, R24
        $end asm
'als Inline-Assembler  ANDERE Möglichkeit
        $asm
        LOADADR Wert1, X
        LD     R24, X+
        LD     R25, X+
        LOADADR Wert2, X
        ST     X+, R24
        ST     X+, R25
        $end asm

Es gibt hier ein paar Beispiele, wie Bascom dieses oder jenes in Maschinencode umsetzt, dabei ist es auch sehr lehrreich, erstmal zu überlegen, wie man es selbst machen würde, und dann erst nachzusehen, wie es Bacom gelöst hat.


Autor

PicNick

Siehe auch


LiFePO4 Speicher Test