(→Ecken, Kanten und Tücken) |
(→SBI/CBI bei Interrupt-Flags) |
||
(27 dazwischenliegende Versionen von 4 Benutzern werden nicht angezeigt) | |||
Zeile 7: | Zeile 7: | ||
Die Vorteile: | Die Vorteile: | ||
− | *Das Drumherum mit der richtigen Initialisierung, auch der | + | *Das Drumherum mit der richtigen Initialisierung, auch der Peripherie, 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. | *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 | *fürs Erste reicht die Demo-Version allemal | ||
Zeile 26: | Zeile 26: | ||
End | End | ||
</pre> | </pre> | ||
− | 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. | + | Das Programm macht natürlich überhaupt nix. Aber durch die paar Zeilen hat Bascom alle |
+ | [[AVR_Assembler_Einf%C3%BChrung#Struktur_eines_AVR-Maschinenprogrammes|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. | Zwischen "$asm" und "$end asm" kann man nun nach Herzenslust irgendwas Assemblermäßiges reinschreiben und mit dem Simulator rumprobieren. | ||
Zeile 33: | Zeile 34: | ||
==Der Zentral-Prozessor (CPU)== | ==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. | 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. | ||
+ | |||
+ | [[Atmel_Controller_Mega16_und_Mega32#CPU|Hübsche Zeichnungen von Atmega32-CPU und einige weiterführende Information]] | ||
===Registerverwendung=== | ===Registerverwendung=== | ||
Zeile 764: | Zeile 767: | ||
CPC R25, R23 'Vergleich der höherwertigen Bytes + ev. Überlauf | CPC R25, R23 'Vergleich der höherwertigen Bytes + ev. Überlauf | ||
' jetzt können die normalen "bedingte Verzweigung"-Befehle verwendet werden | ' jetzt können die normalen "bedingte Verzweigung"-Befehle verwendet werden | ||
+ | </pre> | ||
+ | |||
+ | Der obige Vergleich prüft nur auf ein kleiner. Für einen Test auf Gleichheit muss man zu Fuß das Zero-Flag nach jeden einzelnen Vergleich berücksichtigen: | ||
+ | <pre> | ||
+ | CP R24, R22 'Vergleich der niederwertigen Bytes | ||
+ | BRNE ungleich: 'Prüfen ob die unteren Bytes gleich sind | ||
+ | CP R25, R23 'Vergleich der höherwertigen Bytes | ||
+ | BRNE ungleich: 'Prüfen ob die obere Bytes gleich sind | ||
+ | ' hier geht es weiter,wenn R24:R25 = R22:R23 | ||
</pre> | </pre> | ||
Zeile 1.147: | Zeile 1.159: | ||
</pre> | </pre> | ||
− | Wen die vielen "-1" wundern: Wir brauchen für eine 16 Bit Zahl (R16:R17) natürlich auch ein 16-Bit-tiges | + | Wen die vielen "-1" wundern: Wir brauchen für eine 16 Bit Zahl (R16:R17) natürlich auch ein 16-Bit-tiges "-1" Literal. Und bei den Vorzeichen-Zahlen haben wir ja festgestellt, daß -1 ja so aussieht: |
+ | bei 8-Bit 0b11111111 &HFF | ||
+ | bei 16-Bit 0b1111111111111111 &HFFFF | ||
+ | bei 32-Bit 0b11111111111111111111111111111111 &HFFFFFFFF | ||
+ | Und diese vielen Bits müssen wir auf die einzelnen Subtraktionsbefehlt eben verteilen | ||
+ | |||
+ | *Hinaufzählen mit z.B. 32 Bit: | ||
+ | <pre> | ||
+ | .... | ||
+ | SUBI r16, -1 | ||
+ | SBCI r17, -1 | ||
+ | SBCI r18, -1 | ||
+ | SBCI r19, -1 | ||
+ | </pre> | ||
+ | |||
*Beim Dekrementieren ist eigentlich nur das Literal ein anderes: statt -1 eben +1, d.h., wir subtrahieren "wirklich". | *Beim Dekrementieren ist eigentlich nur das Literal ein anderes: statt -1 eben +1, d.h., wir subtrahieren "wirklich". | ||
Zeile 1.154: | Zeile 1.180: | ||
SUBI r16, 1 | SUBI r16, 1 | ||
SBCI r17, 0 | SBCI r17, 0 | ||
+ | </pre> | ||
+ | |||
+ | *32 Bit ? Richtig: | ||
+ | <pre> | ||
+ | .... | ||
+ | SUBI r16, 1 | ||
+ | SBCI r17, 0 | ||
+ | SBCI r18, 0 | ||
+ | SBCI r19, 0 | ||
</pre> | </pre> | ||
Zeile 1.168: | Zeile 1.203: | ||
'''Bleibt eigentlich nur R24:R25 für solche 16-Bit Befehle''' | '''Bleibt eigentlich nur R24:R25 für solche 16-Bit Befehle''' | ||
+ | |||
+ | === begrenzte Reichweite von IN / OUT === | ||
+ | Für das Ansprechen der Peripherie-Geräte und PORTs gibt es die Befehle IN und OUT. Diese sind ganz ähnlich den Befehlen LDS bzw. STS. Bei IN und OUT gehen die erlaubten Adresse nur bis 63 und damit man damit etwas weiter kommt wird noch 32 dazu addiert. Ein | ||
+ | IN Register, ioadr | ||
+ | |||
+ | entspricht also einem | ||
+ | LDS Register, ioadr+32 | ||
+ | |||
+ | Durch die relativ lange Adresse, von immerhin 16 Bits (auch wenn die selten alle wirklich gebraucht werden) ist der Befehl LDS doppelt so lang und braucht auch doppelt so lange wie die kurze Form IN. | ||
+ | |||
+ | Bei den älteren µCs wie Mega8 oder Mega32 reichten die Befehle IN und OUT gerade aus um alle SFRs zu erreichen. Viele neuere µCs wie Mega88 oder Mega644 haben mehr als 64-IO Register. Damit sind nicht mehr alle SFRs über die Befehle IN und OUT zu erreichen, sondern müssen wie das RAM über LDS bzw. STS angesprochen werden. | ||
+ | |||
+ | Selbst in den Datenblättern finden sich ein paar fehlerhafte Beispiele (z.B. für die USART0), die dies nicht berücksichtigen. Das sind aber kleine Fehler, die der Compiler / Assembler erkennt und die leicht zu berichtigen sind. | ||
+ | |||
+ | === SBI/CBI bei Interrupt-Flags === | ||
+ | Für das Setzen und Löschen einzelner Bits gibt es für die ersten 32 IO Register extra Befehle SBI und CBI. Diese Befehle fallen etwas aus dem Rahmen, weil hier kleines der 32 GPRs benutzt wird. Eine mögliche Anwendung ist zum Beispiel das Ausgeben einer 1 auf PortA, Bit 3 mit dem Befehl | ||
+ | SBI PortA,3 | ||
+ | statt des längeren | ||
+ | in r16, PortA | ||
+ | ori r16, 8 ' bei der 8 ist Bit 3 gesetzt | ||
+ | out PortA, r16 | ||
+ | |||
+ | Bei den Interrupt-Flags ist besondere Vorsicht mit den Befehlen CBI und SBI geboten. Da kommen sogar gleich 2 Tücken zusammen. | ||
+ | |||
+ | Die Interrupt-Flags werden von der Hardware auf 1 gesetzt wenn ein Interrupt ausgelöst werden könnte, z.B. bei einem Timer Überlauf. Wieder zurück auf 0 geht es durch das Auslösen des Interrupts oder halt von Hand durch das Programm. Das schreiben in diese IO-Register hat einen ungewöhnlichen Effekt: Die Bits wo man eine 1 hinschreibt werden gelöscht, und bei einer 0 passiert einfach nichts. Dadurch ist es möglich gezielt ausgewählte Bits zu löschen, unabhängig davon was die Hardware gleichzeitig mit den anderen Bits anstellt. Von Hand setzen kann man die Bits in der Regel gar nicht. Eine gewisse Logic hat das Löschen mit einer 1 schon, denn oft will man nach dem Aktivieren von Interrupts über die Interrupt-Masken die dazugehörigen Interrupt-Flags löschen. Dazu kann man so den selben Wert einfach in beide Register schreiben. | ||
+ | |||
+ | Die eigentliche Tücke kommt, wenn man bei einem etwas älteren µC (z.B. Mega8, Mega32) jetzt auf die Idee kommt zum löschen eines Interrupt-falgs den Befehl SBI (oder ggf. noch schlechter CBI) zu nutzen. Intern wird da der Befehl SBI so ähnlich umgesetzt wie oben gezeigt, nur das Register r16 und das Status Register bleiben unverändert. Für die normalen Register ist es kein Problem den Inhalt auszulesen und wieder zu schreiben. Aber bei den speziellen Registern mit den Interrupt-Flags werden so alle Flags gelöscht, weil genau für die gesetzten Flags eine 1 geschrieben wird und eine 1 Schreiben bedeutet hier halt gerade Bit löschen. | ||
+ | |||
+ | Bei einigen (neueren, z.B. Mega88, Tiny2313, Mega324) µCs wurde das Verhalten des SBI Befehls so geändert, dass wirklich nur das eine Bit geschrieben wird - es geht dann mit SBI, aber halt nur bei diesen neueren Chips und natürlich nur den ersten 32 IO Registern. So viel komplizierter ist das Löschen eines Interrupt Flags über die Befehle LDI und OUT auch nicht. Die spezielle Logik zum löschen einzelner Bits hat das Register ja schon von sich aus. | ||
+ | |||
+ | ==Unterprogramme== | ||
+ | Man sieht ja schon an den bisherigen Beispielen, daß gewisse Befehlsfolgen immer wieder benötigt werden. | ||
+ | *Typisches Beispiel, was man ja schon beim ersten Lauflicht braucht, sind Verzögerungsroutinen, damit die LEDs nicht gar so schnell funzeln. | ||
+ | |||
+ | Natürlich kann man überall Zählschleifen einbauen, die auf diese Art das Programm gewissermaßen ausbremsen. Aber das verbraucht dann doch mächtig Platz, und auch nur die geringste Änderung in der Befehlsfolge muß man x-mal machen. | ||
+ | |||
+ | Wenn man also diese Befehlsfolge einmal zusammenfaßt und irgendwo außerhalb der Haupt-Programm-Schleife deponiert und mit einem Namen versieht, könnte man doch immer, wenn man sie braucht, diese Folge aufrufen. Ja, aber wie findet man dann wieder zurück, um dort weiterzumachen, wo man grad war? | ||
+ | |||
+ | *Ganz einfach: | ||
+ | ===CALL & RET=== | ||
+ | <pre> | ||
+ | Hauptschleife: | ||
+ | ..... | ||
+ | ..... | ||
+ | CALL Befehlsfolge | ||
+ | ..... | ||
+ | CALL Befehlsfolge | ||
+ | ..... | ||
+ | ..... | ||
+ | JMP Hauptschleife | ||
+ | |||
+ | Befehlsfolge: | ||
+ | .... | ||
+ | RET | ||
+ | </pre> | ||
+ | Das sind zwei listige Erweiterungen von "JMP": | ||
+ | *CALL heißt: Erst merken, wo wir grad sind, und dann JMP irgendwohin | ||
+ | *RET heißt: Mach "JMP" auf die Stelle, die man sich gemerkt hat (und vergiß sie dann) | ||
+ | |||
+ | Super, kann man gut brauchen, aber wie macht der AVR das ? | ||
+ | |||
+ | Dazu braucht der AVR zwei Dinge: Einen "Stapel" und einen "Stapelzeiger". | ||
+ | *Den "Stapelzeiger" (englisch "Stackpointer") hat er schon in der CPU eingebaut. | ||
+ | *Der "Stapel" ("Stack") ist ein Stück Speicher, den uns Bascom bei seiner Initialisierung vom oberen Ende des SRAM abgeknöpft hat. | ||
+ | **Am Anfang enthält der Stackpointer die oberste Adresse vom Stack | ||
+ | **Jedesmal, wenn sich der AVR eine Rücksprungadresse merken soll, schreibt er sie dorthin, wo der Stackpointer grad hinzeigt, und zieht dann 2 (so lange ist eine solche Adresse) vom Stackpointer ab. | ||
+ | **Geht es jetzt ans "RET", addiert er jetzt diese 2 wieder auf den Pointer, und springt zu der Adresse, die dort steht. | ||
+ | **Sagt man nun aber nicht "RET", sondern nochmal "CALL", schreibt er diese (neue) Adresse nun UNTER die vorherige (und zieht wieder 2 vom Pointer ab). Im Stack stehen jetzt also beide Adressen untereinander, der Stack ist größer geworden | ||
+ | |||
+ | *Der Speicher ist also ein armer Hund: Mit jedem "CALL" knabbern wir von oben was weg, und mit jedem "DIM" (im Bascom) von unten. | ||
+ | Erschütternde Tragödien spielen sich ab, wenn sich der Stack und das "DIM" Bereich | ||
+ | mal irgendwo in der Mitte treffen, die beiden Bereiche sind sich nämlich spinnefeind. | ||
+ | Beim Bascom selbst wird es noch viel früher dramatisch: Der verlangt nämlich, daß wir schon beim Programmieren schätzen, wie groß der Stack wohl werden wird ($HWSTACK=nn). | ||
+ | |||
+ | [[Bascom_Inside#Stacks_.26_Frame|Da gibt's ein paar Details dazu]] | ||
+ | |||
+ | ===PUSH & POP=== | ||
+ | Mit diesen Befehlen wird der Stack und der Stackpointer direkt angesprochen. Denn "CALL & RET" sind nicht die einzigen, die das brauchen. | ||
+ | <pre> | ||
+ | PUSH register 'Den Inhalt von register an die Adresse schreiben, an die der Stackpointer | ||
+ | 'grad hinzeigt, und dann 1 vom Pointer abziehen | ||
+ | POP register 'Auf den Stackpointer 1 addieren, und das Byte dort ins register laden | ||
+ | </pre> | ||
+ | *Typische Anwendung: | ||
+ | In unserem "Unterprogramm" müssen wir irgendwelche Register verändern, wir wollen aber nicht, daß das Hauptprogramm, das uns aufgerufen hat, was davon merkt | ||
+ | <pre> | ||
+ | Befehlsfolge: | ||
+ | PUSH R24 | ||
+ | LDI R24, 127 | ||
+ | _Schleife: | ||
+ | DEC R24 | ||
+ | BRNE _Schleife | ||
+ | POP R24 | ||
+ | RET | ||
+ | </pre> | ||
+ | Nach dem "RET" hat R24 also wieder den Inhalt, den es vorher hatte. | ||
+ | ==Interrupt-Routinen (ISR)== | ||
+ | ISR = "Interrupt Service Request" = "Unterbrechungs-Behandlung-Anforderung" | ||
+ | |||
+ | Ein AVR besteht ja nicht nur aus CPU und Speicher, sondern auch aus einer Reihe von "Geräten", die ihre Funktion völlig unabhängig durchführen, wenn man sie mal konfiguriert hat. | ||
+ | |||
+ | Aber immer, wenn sie grad ihre Meß- oder Zählfunktion erledigt haben, wollen sie ihr Ergebnis irgendwie mitteilen und dafür wissen, wie's weitergehen soll. | ||
+ | |||
+ | Wir können dazu mit dem AVR einen Deal machen: | ||
+ | *Wir schreiben ein Unterprogramm, daß seine dahingehenden Ansprüche erfüllt und sagen dem AVR, wo es im Programmspeicher steht. | ||
+ | *Und dafür paßt der AVR auf das "Gerät" auf und macht den "Call" auf dieses Unterprogramm völlig automatisch genau dann, wenn es soweit ist. Wir brauchen uns im "normalen" Programm nun nicht mehr darum zu kümmern | ||
+ | |||
+ | Um so einen Handel einzufädeln, ist es wirklich praktisch, wenn wir Bascom hinzuziehen. Der kennt nämlich die meisten AVR-Controllertypen gewissermaßen persönlich und weiß am besten, an welchen Strippen man da bei irgendeinem Controller ziehen muß. | ||
+ | ===Beispiel Timer=== | ||
+ | Sagen wir, wir wolle jede Millisekunde ein Unterprogramm durchführen (lassen). | ||
+ | <pre> | ||
+ | $regfile = "m32def.dat" | ||
+ | $crystal = 8000000 | ||
+ | $hwstack = 48 | ||
+ | </pre> | ||
+ | Kennen wir an sich schon. Aber wenn wir Interrupts verwenden, sollten wir bei "$HWSTACK=" schon ein bißchen was spendieren. | ||
+ | |||
+ | *Jetzt lassen wir Bascom den Timer konfigurieren | ||
+ | <pre> | ||
+ | Config Timer0 = Timer , Prescale = 64 'Timer Setup | ||
+ | </pre> | ||
+ | Lassen wir das Bascom machen. Für uns ist es dann nämlich egal, ob das auf einem Tiny oder einem ATmega128 laufen wird, es schaut immer gleich aus. | ||
+ | |||
+ | *Jetzt der Deal: | ||
+ | <pre> | ||
+ | On Timer0 Interrupt_ticker ' Das ist das Unterprogramm (heißt jetzt aber ISR-Routine) | ||
+ | |||
+ | Enable Timer0 ' jetzt machen wir den Timer scharf | ||
+ | |||
+ | Enable Interrupts ' Und das ist sozusagen der Hauptschalter für sowas | ||
+ | </pre> | ||
+ | |||
+ | *Das "normale" Programm: | ||
+ | <pre> | ||
+ | |||
+ | $asm | ||
+ | Hauptschleife: | ||
+ | '------------------------------ | ||
+ | ' Irgendein (Assembler?) Programm, dem der Timer völlig schnuppe ist | ||
+ | '------------------------------ | ||
+ | JMP Hauptschleife | ||
+ | $end asm | ||
+ | |||
+ | END | ||
+ | </pre> | ||
+ | |||
+ | *Und nun die ISR-Routine | ||
+ | <pre> | ||
+ | Interrupt_ticker: | ||
+ | |||
+ | Timer0 = 131 ' Damit der arme Counter nicht immer von Null weg zählen muß | ||
+ | ' er darf schon mit 131 anfangen | ||
+ | |||
+ | $asm | ||
+ | '------------------------------ | ||
+ | ' Die befehle, die wir jede mS ausführen wollen | ||
+ | '------------------------------ | ||
+ | |||
+ | $end asm | ||
+ | |||
+ | Return ' wieder zurück und weiter im normalen Programm | ||
+ | </pre> | ||
+ | |||
+ | Die hier verwendeten Werte von "64" für den Prescaler und die "131" für den Counter sind aus der Angabe "$crystal = 8000000" berechnet worden und ergeben Interrupts im Abstand von 1 mS. | ||
+ | |||
+ | Gerade bei einer ISR bietet es sich an die ganze ISR in ASM zu schreiben, denn BASCOM rettet für die ISR fast alle Register ([[Bascom_Debounce_ISR_in_Assembler#Warum_eine_ISR_in_Assembler_programmieren.3F|Details]]). Die ISR ganz in ASM ist entsprechend ein Beispiel wo der ASM Code deutlich schneller und kürzer werden kann als der Code vom Compiler. | ||
==Autor== | ==Autor== | ||
Zeile 1.173: | Zeile 1.374: | ||
==Siehe auch== | ==Siehe auch== | ||
− | *[[AVR_Assembler_Einf%C3%BChrung]] | + | *[[AVR_Assembler_Einf%C3%BChrung|AVR Assembler Einführung]] |
*[[Bascom]] | *[[Bascom]] | ||
+ | *[[Bascom Debounce ISR in Assembler]] | ||
[[Kategorie:Software]] | [[Kategorie:Software]] | ||
+ | [[Kategorie:Quellcode Bascom]] |
Aktuelle Version vom 26. Juli 2011, 17:57 Uhr
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.
Inhaltsverzeichnis
- 1 Assembler Einführung für Bascom-User
- 2 Ein Grund-Programm
- 3 Der Zentral-Prozessor (CPU)
- 4 Kurze Zusammenfassung
- 5 Schleifen
- 6 Weg vom Simulator auf den µC
- 7 Das Byte und seine vielen Bedeutungen
- 8 Numerischer Input vom Terminal
- 9 Mehr als ein Byte
- 10 Bascom als Messlatte
- 11 ALU und Status-Register (SREG)
- 12 Ecken, Kanten und Tücken
- 13 Unterprogramme
- 14 Interrupt-Routinen (ISR)
- 15 Autor
- 16 Siehe auch
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 Peripherie, 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.
Hübsche Zeichnungen von Atmega32-CPU und einige weiterführende Information
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:
- 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
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
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"
- DO...LOOP WHILE "mache was, solange Bedingung erfüllt ist"
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
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
Der obige Vergleich prüft nur auf ein kleiner. Für einen Test auf Gleichheit muss man zu Fuß das Zero-Flag nach jeden einzelnen Vergleich berücksichtigen:
CP R24, R22 'Vergleich der niederwertigen Bytes BRNE ungleich: 'Prüfen ob die unteren Bytes gleich sind CP R25, R23 'Vergleich der höherwertigen Bytes BRNE ungleich: 'Prüfen ob die obere Bytes gleich sind ' hier geht es weiter,wenn R24:R25 = R22:R23
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.
ALU und Status-Register (SREG)
Bisher haben wir nur von Zero-Bits und "Oberflow" bzw. Carry-Bit gesprochen und uns da ein wenig drübergeschwindelt. Es ist ja auch tatsächlich so, daß man mit dem bisher erworbenen Wissen ganze Legionen von Robotern programmieren kann.
Aber ganz kommt man an den diversen Flags im Statusregister ja doch nicht vorbei, schon garnicht, wenn man ernsthafte Berechnungen anstellen will.
Zwei Bit im SREG brauchen wir an dieser Stelle aber nicht weiter zu beachten
- T-Bit oder Transfer-Bit. Das dient für einige Befehle als "Zwischendepot" für einzelne Bits
- Global Interrupt Enable-Bit. Wie der Name sagt, werden damit einfach alle Interrupts zugelassen oder eben nicht. Von Interrupts haben wir aber noch garnicht gesprochen
- Die andern Bits dienen der ALU (Arithmetic-Logical-Unit) als Ergebnis-Anzeige bei den ganzen arithmetisch-logischen Befehlen
ALU (Recheneinheit)
Immer, wenn zwei Bytes zu vermangeln sind, geht das mit der Recheneinheit. Die hat 4 Byte zur Verfügung:
- Input-Byte A
- Input-Byte B
- Output-Byte R
- Statusregister SREG
(Daß er dabei Input A gleichzeitig als Output verwendet, ignorieren wir mal einfach, das verwirrt nur und ändert nix grundsätzliches)
Ein-Bit-Volladdierer
Wir wissen ja alle noch, wie das mit dem Addieren im 2-er System funktioniert ?
A B R 0 + 0 = 0 0 + 1 = 1 1 + 0 = 1 1 + 1 = 0 und Überlauf
Diesen Überlauf muß man nun an das nächsthöhere Bit weitergeben. Andererseits kommt von dem niedrigeren Bit ja auch ein Überlauf daher. Also sieht das für ein Bit dann so aus
Mit 8-Bit
Für alle 8 Bit wird es etwas größer, ein paar direkte Status-Bit hab ich gleich eingezeichnet
- Der Schalter "mit od. ohne Carry" ist da, ob man einen vorherigen Überlauf miteinbeziehen will
ADD register, register OHNE Carry ADC register, register MIT Carry
- N-Bit: Ist einfach der Wert (0 oder 1), den das Bit 2^^7 im Ergebnis hat
- Z-Bit: Steht im Ergebnis alles auf 0, wird das gesetzt
- Half-Carry: Das wird gesetzt, wenn es vom Bit 2^^3 einen Überlauf in Richtung 2^^4 gegeben hat. (Das braucht man, wenn man mit BCD-Zahlen, also nur von 0-9 rechnen will. Dazu später, wenn Interesse da ist)
- Carry-Bit: Ist nun klar, das ist einfach der Überlauf vom 2^^7 Bit.
"S" und "V" Bit
8 Bit mit Vorzeichen
Kurze Wiederholung *gähn*, wie das mit den Vorzeichen ist: Man "spiegelt" die Zahlen an der Null-Stelle. Nehmen wir an, wir haben grad eine NULL im Byte. Ziehen wir jetzt 1 ab, soll ja offenbar -1 rauskommen. Was steht im Byte tatsächlich drin ? Richtig, Hexadezimal FF. Und genauso schaut auch -1 eben aus. Alle diese negativen Zahlen haben gemeinsam, daß zumindest das Bit 2^^7 eine 1 zeigt. Daran erkennt man, daß die Zahl negativ ist. Das funktioniert aber nur so bis -128 (hex 80) und +127 (hex 7F).
Negative Zahlen werden also dargestellt als "2-er Komplement".
- Man dreht 0-er und 1-er im Byte um
- und addiert "1" drauf.
0b01111000 hex 78 = dezimal 120 umdrehen: 0b10000111 hex 87 +1 : 0b10001000 hex 88 Das stellt jetzt -120 dar.
Rechnen mit vorzeichenbehafteten 8 Bit
Der Sinn und Zweck des "S" und "V" Bit ist nun leicht verständlich.
- Bisher wurde ja addiert
0b01111000 hex 78 = dezimal 120 + 0b00001010 hex 0A = dezimal 10 ------------------------------- 0b10000010 hex 82 = dezimal 130
- Addiert wird jetzt immer noch genauso, aber jetzt hat das Bit 2^^7 eine andere Bedeutung, weil es das Vorzeichen darstellt
- Ein Beispiel:
0b10000000 hex 80 = dezimal -128 + 0b00000010 hex 02 = dezimal +2 ------------------------------- 0b10000010 hex 82 (Vorzeichenbit ist gesetzt)
Vorzeichenbit gesetzt-> hex 82 ist also ein zweier-Komplement, und damit ist es -126.
- Aber nun:
0b01111000 hex 78 = dezimal +120 + 0b00001010 hex 0A = dezimal +10 ------------------------------- 0b10000010 hex 82 (Vorzeichenbit ist gesetzt)
Vorzeichenbit gesetzt-> hex 82 ist also ein zweier-Komplement, und damit ist es -126.
Bitte ???
Das kann ja wohl nicht sein, es muß ja +130 rauskommen.
Das Carry-Bit hilft nichts, das ist immer NULL geblieben.
- Genau da hilft das "V" Bit
Das V-Bit = 1,
- wenn entweder die beiden Input-Bytes positiv waren (2^^7=0) UND das Ergebnis aber 2^^7=1 zeigt.
- wenn beiden Input-Bytes negativ waren (2^^7=1) UND das Ergebnis aber 2^^7=0 zeigt.
Im Prinzip sagt also das V-Bit aus, ob das Vorzeichenbit im Ergebnis durch einen Überlauf zustandegekommen ist.
- Und das "S"-Bit ? Das kombiniert das Vorzeichen im Ergebnis mit diesem "V"-Bit.
- S=1, wenn das Vorzeichenbit im Ergebnis zwar NULL ist, aber nur, weil ein Überlauf stattgefunden hat. Das Ergebnis ist also trotzdem negativ und stellt ein 2-er Komplement dar.
- S=1, wenn das Vorzeichenbit im Ergebnis EINS ist, und zwar OHNE, daß ein Überlauf stattgefunden hätte. Das Ergebnis ist also korrekt negativ und stellt auch ein 2-er Komplement dar
Ob an das nächsthöhere Byte ein Überlauf weiterzugeben ist, sagt bei Vorzeichen-Bytes nun das "V" Bit.
Nochmal die obigen Vorzeichen-Beispiele zum mitsingen, aber diesmal mit den N, V und S Flags
0b10000000 hex 80 = dezimal -128 + 0b00000010 hex 02 = dezimal +2 ------------------------------- 0b10000010 hex 82 N=1 V=0 S=1 Ist also negativ, 2-er Kompl. kein Überlauf --> -126 0b01111000 hex 78 = dezimal +120 + 0b00001010 hex 0A = dezimal +10 ------------------------------- 0b10000010 hex 82 N=1 V=1 S=0 Ist also positiv, 2^^7 löschen, der Rest bleibt (dezimal 2), hat aber Überlauf, der ist +128 wert, insgesamt also --> 128 + 2 = +130
Mehr als 8 Bit mit Vorzeichen
Keine Sorge, das Vorzeichenbit gibt es bei allen binärzahlen immer nur einmal, an der höchsten Stelle. Wir haben also auch bei eine 8-Byte / 64-Bit Variablen nur ein einziges Mal das Problem.
- Das Vorzeichenbit ist immer das MSB (höchst aussagekräftige Bit = most significant Bit). Zwei 32 Bit sieht das so aus:
0b00000000000000000000000111111111 = hex 000001FF = dezimal +511 0b11111111111111111111110000000001 = hex FFFFFC01 = dezimal -1023
Das Vorzeichenbit ist das ganz links (2^^31)
Addiert werden solche Zahlen erstmal ganz normal
Dim Vala As Long Dim Valb As Long Dim Stat As Byte Vala = 511 Valb = -1023 Print Hex(vala) ; "+" ; Hex(valb) ; "="; $asm lds r16, {vala} lds r17, {vala+1} lds r18, {vala+2} lds r19, {vala+3} lds r20, {valb} lds r21, {valb+1} lds r22, {valb+2} lds r23, {valb+3} add r16, r20 'das erste Byte noch ohne Carry adc r17, r21 'der Rest mit Carry adc r18, r22 adc r19, r23 in r20, sreg ' Wir holen uns die Status-Bits sts {stat}, r20 ' speichern sts {Vala}, r16 ' speichern Ergebnis sts {Vala+1}, r17 sts {Vala+2}, r18 sts {Vala+3}, r19 $end Asm Print Hex(vala) ; " " ; Bin(stat) ; " " ; Str(vala) ' Zum Anschauen
Wir sehen, gesetzt wird von der ALU letzlich:
S=1 und N=1
Also ist die Zahl negativ (S=1) und muß als 2-er Komplement verstanden werden.
Das Carry-Bit beim Bit-Verschieben
Häufig wird das Carry-Bit auch als Zwischenlager und Anzeige bei den Schiebe- und Rotationsbefehlen.
Es verhält sich da wie ein 9.Bit zu den 8-Bits im Register.
Die Links- und Rechts-Schiebebefehle sind völlig symmetrisch
LSL & ROL
LSL register ROL register
Die Bits im Register werden bei beiden Befehlen eine Stelle nach links verschoben, das höchste Bit wandert in das Carry-Bit.
- Bei LSL wird eine NULL in das niederwertigste Bit des Registers reingeschoben
- Bei ROL wird das Carry-Bit (von vorher) in dieses Bit gestellt.
LSR & ROR
LSR register ROR register
Die Bits im Register werden bei beiden Befehlen eine Stelle nach rechts verschoben, das niederste Bit wandert in das Carry-Bit.
- Bei LSR wird eine NULL in das höchste Bit des Registers reingeschoben
- Bei ROR wird das Carry-Bit (von vorher) in dieses Bit gestellt.
Anwendungen
- Multiplizieren mit 2, 4, 8,...
Bei einem Byte ist da wohl nicht viel dazu zu sagen.
LDI register, &H05 ' register = &H05 = dezimal 5 LSL register ' register = &H0A = dezimal 10
Bei mehreren Bytes, also Variablen mit mehr als 8 Bit, beginnt man immer mit dem niederwertigsten Byte, der erste Befehl OHNE Carry, damit nicht irgendetwas ungewolltes reinrutscht, und dann weiter mit den anderen Bytes
LDS register, {long_variable } ' einlesen LSL register ' verschieben, 2^^7 kommt ins Carry, bleibt ' 0 kommt nach 2^^0 STS {long_variable }, register ' speichern (ändert nix im SREG) LDS register, {long_variable +1 } ' einlesen (ändert auch nix im SREG) ROL register ' verschieben, ' carry von vorher nach 2^^0 ' 2^^7 ins Carry, bleibt STS {long_variable + 1}, register ' speichern .usw.
- Dividieren durch 2, 4, 8,...
Da ist eben alles genau umgekehrt. Beginnen mit dem höchsten, sonst wie gehabt
LDS register, {long_variable + 3} LSR register ' verschieben, 2^^0 kommt ins Carry und bleibt ' 0 kommt nach 2^^7 STS {long_variable +3}, register LDS register, {long_variable + 2 } ROR register ' verschieben, ' carry von vorher nach 2^^7 ' 2^^0 ins Carry STS {long_variable + 2}, register .usw.
Anwendung Lauflicht
Ist auch beliebt. Angenommen, wir haben auf PORTB 8 LED hängen und möchten da ein Lauflicht haben.
$asm LDI r24, &HFF !OUT DDRB, R24 ' wir setzen das Port auf OUTPUT LDI r16, &H00 ' das "SchiebeByte" auf NULL SEC ' wir setzen das Carry Bit, damit ein 1-er da ist Dauerschleife: ROL r16 ' Beim ersten Mal rutscht das carry-Bit rein ' danach geht der 1-er automatisch im Kreis !OUT PORTB, R16 ' wir legen das Byte mit dem einen 1-er an das Port '--------------------------------------------------- ' Hier muß aber eine Verzögerung rein, sonst ist das ' viel zu schnell '--------------------------------------------------- RJMP Dauerschleife ' und immer weiter $end asm
Schreiben wir
ROR r16
geht's in die andere Richtung
Ecken, Kanten und Tücken
Das AVR-Instruction-Set weist einige Feinheiten auf, die schon manchen Assembler-Roboter auf Kollisionskurs gebracht haben.
Grad' dieses Kapitel kann ich nur nach und nach auffüllen, weil mir auch nicht immer gleich alles so einfällt. Vielleicht gibt's aber auch ein paar andere Assembleure, die ihren Erfahrungsschatz hier einbringen.
Ich kann nur raten, beim Programmieren immer zumindest das "Complete Instruction Set Summary" bei der Hand zu haben. Immer wieder nachsehen, ob ein gerade ausgewählter Befehl das macht, was man sich vorstellt, und vor allem, ob er auch alle die Status-Register-Flags so setzt oder löscht, die man braucht.
INC/DEC
Für Schleifenzähler etc. werden ja gerne "INC" (register +1) oder "DEC" (register -1) verwendet. Mit einem Vergleich auf einen Wert kontrolliert man dann diese Schleife. Ist ja auch ok.
Muß man den Schleifenzähler aber nun so erweitern, daß man mehr als ein Byte braucht, muß man aufpassen.
INC od. DEC kümmern sich NICHT um das Carry-Bit !
- wer also schreibt
clr r0 (manche nehmen das gerne als Register, in dem immer NULL steht. Ist praktisch) .... INC r16 adc r17, r0
kann sein blaues Wunder erleben.
Da INC bei einem Überlauf einfach wieder bei NULL weitermacht, das Carry-Bit aber nicht anrührt, addieren wir im "ADC" Befehl ein Carry-Bit dazu, das von irgendeinem Befehl vorher übriggeblieben ist.
Für ein sauberes Carry-Bit muß man also auf einen "echten" Additionsbefehl umsteigen. Und was sehen wir ?
Es gibt keinen "ADDI" Befehl ( für sowas wie "addi register, 1" )
Wenn wir also kein Register zur Hand haben, in das wir einen 1-er reinschreiben können, müssen wir
SUBI register, -1
schreiben. Ist klar: register - (-1) = register + 1, also das, was wir brauchen.
.... SUBI r16, -1 SBCI r17, -1 (abziehen festen Wert von Register MIT CARRY)
Wen die vielen "-1" wundern: Wir brauchen für eine 16 Bit Zahl (R16:R17) natürlich auch ein 16-Bit-tiges "-1" Literal. Und bei den Vorzeichen-Zahlen haben wir ja festgestellt, daß -1 ja so aussieht:
bei 8-Bit 0b11111111 &HFF bei 16-Bit 0b1111111111111111 &HFFFF bei 32-Bit 0b11111111111111111111111111111111 &HFFFFFFFF
Und diese vielen Bits müssen wir auf die einzelnen Subtraktionsbefehlt eben verteilen
- Hinaufzählen mit z.B. 32 Bit:
.... SUBI r16, -1 SBCI r17, -1 SBCI r18, -1 SBCI r19, -1
- Beim Dekrementieren ist eigentlich nur das Literal ein anderes: statt -1 eben +1, d.h., wir subtrahieren "wirklich".
.... SUBI r16, 1 SBCI r17, 0
- 32 Bit ? Richtig:
.... SUBI r16, 1 SBCI r17, 0 SBCI r18, 0 SBCI r19, 0
Ja, es gibt aber doch was für 16 Bit ?
ADIW register:register+1 , nn ' nn auf die 16-bit von register:register+1 addieren SBIW register:register+1 , nn ' nn von den 16-bit von register:register+1 subtrahieren
(nn kann sein 0 - 63)
Ja, aber das geht nur mit den Registerpaaren R24:R25, R26:R27, R28:R29, R30:R31
Da die Paare R26:R27, R28:R29, R30:R31 aber gleichzeitig die einzigen Pointer-Register sind (dazu kommen wir noch, aber grad in Schleifen braucht man die meist für was anderes)
Bleibt eigentlich nur R24:R25 für solche 16-Bit Befehle
begrenzte Reichweite von IN / OUT
Für das Ansprechen der Peripherie-Geräte und PORTs gibt es die Befehle IN und OUT. Diese sind ganz ähnlich den Befehlen LDS bzw. STS. Bei IN und OUT gehen die erlaubten Adresse nur bis 63 und damit man damit etwas weiter kommt wird noch 32 dazu addiert. Ein
IN Register, ioadr
entspricht also einem
LDS Register, ioadr+32
Durch die relativ lange Adresse, von immerhin 16 Bits (auch wenn die selten alle wirklich gebraucht werden) ist der Befehl LDS doppelt so lang und braucht auch doppelt so lange wie die kurze Form IN.
Bei den älteren µCs wie Mega8 oder Mega32 reichten die Befehle IN und OUT gerade aus um alle SFRs zu erreichen. Viele neuere µCs wie Mega88 oder Mega644 haben mehr als 64-IO Register. Damit sind nicht mehr alle SFRs über die Befehle IN und OUT zu erreichen, sondern müssen wie das RAM über LDS bzw. STS angesprochen werden.
Selbst in den Datenblättern finden sich ein paar fehlerhafte Beispiele (z.B. für die USART0), die dies nicht berücksichtigen. Das sind aber kleine Fehler, die der Compiler / Assembler erkennt und die leicht zu berichtigen sind.
SBI/CBI bei Interrupt-Flags
Für das Setzen und Löschen einzelner Bits gibt es für die ersten 32 IO Register extra Befehle SBI und CBI. Diese Befehle fallen etwas aus dem Rahmen, weil hier kleines der 32 GPRs benutzt wird. Eine mögliche Anwendung ist zum Beispiel das Ausgeben einer 1 auf PortA, Bit 3 mit dem Befehl
SBI PortA,3
statt des längeren
in r16, PortA ori r16, 8 ' bei der 8 ist Bit 3 gesetzt out PortA, r16
Bei den Interrupt-Flags ist besondere Vorsicht mit den Befehlen CBI und SBI geboten. Da kommen sogar gleich 2 Tücken zusammen.
Die Interrupt-Flags werden von der Hardware auf 1 gesetzt wenn ein Interrupt ausgelöst werden könnte, z.B. bei einem Timer Überlauf. Wieder zurück auf 0 geht es durch das Auslösen des Interrupts oder halt von Hand durch das Programm. Das schreiben in diese IO-Register hat einen ungewöhnlichen Effekt: Die Bits wo man eine 1 hinschreibt werden gelöscht, und bei einer 0 passiert einfach nichts. Dadurch ist es möglich gezielt ausgewählte Bits zu löschen, unabhängig davon was die Hardware gleichzeitig mit den anderen Bits anstellt. Von Hand setzen kann man die Bits in der Regel gar nicht. Eine gewisse Logic hat das Löschen mit einer 1 schon, denn oft will man nach dem Aktivieren von Interrupts über die Interrupt-Masken die dazugehörigen Interrupt-Flags löschen. Dazu kann man so den selben Wert einfach in beide Register schreiben.
Die eigentliche Tücke kommt, wenn man bei einem etwas älteren µC (z.B. Mega8, Mega32) jetzt auf die Idee kommt zum löschen eines Interrupt-falgs den Befehl SBI (oder ggf. noch schlechter CBI) zu nutzen. Intern wird da der Befehl SBI so ähnlich umgesetzt wie oben gezeigt, nur das Register r16 und das Status Register bleiben unverändert. Für die normalen Register ist es kein Problem den Inhalt auszulesen und wieder zu schreiben. Aber bei den speziellen Registern mit den Interrupt-Flags werden so alle Flags gelöscht, weil genau für die gesetzten Flags eine 1 geschrieben wird und eine 1 Schreiben bedeutet hier halt gerade Bit löschen.
Bei einigen (neueren, z.B. Mega88, Tiny2313, Mega324) µCs wurde das Verhalten des SBI Befehls so geändert, dass wirklich nur das eine Bit geschrieben wird - es geht dann mit SBI, aber halt nur bei diesen neueren Chips und natürlich nur den ersten 32 IO Registern. So viel komplizierter ist das Löschen eines Interrupt Flags über die Befehle LDI und OUT auch nicht. Die spezielle Logik zum löschen einzelner Bits hat das Register ja schon von sich aus.
Unterprogramme
Man sieht ja schon an den bisherigen Beispielen, daß gewisse Befehlsfolgen immer wieder benötigt werden.
- Typisches Beispiel, was man ja schon beim ersten Lauflicht braucht, sind Verzögerungsroutinen, damit die LEDs nicht gar so schnell funzeln.
Natürlich kann man überall Zählschleifen einbauen, die auf diese Art das Programm gewissermaßen ausbremsen. Aber das verbraucht dann doch mächtig Platz, und auch nur die geringste Änderung in der Befehlsfolge muß man x-mal machen.
Wenn man also diese Befehlsfolge einmal zusammenfaßt und irgendwo außerhalb der Haupt-Programm-Schleife deponiert und mit einem Namen versieht, könnte man doch immer, wenn man sie braucht, diese Folge aufrufen. Ja, aber wie findet man dann wieder zurück, um dort weiterzumachen, wo man grad war?
- Ganz einfach:
CALL & RET
Hauptschleife: ..... ..... CALL Befehlsfolge ..... CALL Befehlsfolge ..... ..... JMP Hauptschleife Befehlsfolge: .... RET
Das sind zwei listige Erweiterungen von "JMP":
- CALL heißt: Erst merken, wo wir grad sind, und dann JMP irgendwohin
- RET heißt: Mach "JMP" auf die Stelle, die man sich gemerkt hat (und vergiß sie dann)
Super, kann man gut brauchen, aber wie macht der AVR das ?
Dazu braucht der AVR zwei Dinge: Einen "Stapel" und einen "Stapelzeiger".
- Den "Stapelzeiger" (englisch "Stackpointer") hat er schon in der CPU eingebaut.
- Der "Stapel" ("Stack") ist ein Stück Speicher, den uns Bascom bei seiner Initialisierung vom oberen Ende des SRAM abgeknöpft hat.
- Am Anfang enthält der Stackpointer die oberste Adresse vom Stack
- Jedesmal, wenn sich der AVR eine Rücksprungadresse merken soll, schreibt er sie dorthin, wo der Stackpointer grad hinzeigt, und zieht dann 2 (so lange ist eine solche Adresse) vom Stackpointer ab.
- Geht es jetzt ans "RET", addiert er jetzt diese 2 wieder auf den Pointer, und springt zu der Adresse, die dort steht.
- Sagt man nun aber nicht "RET", sondern nochmal "CALL", schreibt er diese (neue) Adresse nun UNTER die vorherige (und zieht wieder 2 vom Pointer ab). Im Stack stehen jetzt also beide Adressen untereinander, der Stack ist größer geworden
- Der Speicher ist also ein armer Hund: Mit jedem "CALL" knabbern wir von oben was weg, und mit jedem "DIM" (im Bascom) von unten.
Erschütternde Tragödien spielen sich ab, wenn sich der Stack und das "DIM" Bereich mal irgendwo in der Mitte treffen, die beiden Bereiche sind sich nämlich spinnefeind.
Beim Bascom selbst wird es noch viel früher dramatisch: Der verlangt nämlich, daß wir schon beim Programmieren schätzen, wie groß der Stack wohl werden wird ($HWSTACK=nn).
Da gibt's ein paar Details dazu
PUSH & POP
Mit diesen Befehlen wird der Stack und der Stackpointer direkt angesprochen. Denn "CALL & RET" sind nicht die einzigen, die das brauchen.
PUSH register 'Den Inhalt von register an die Adresse schreiben, an die der Stackpointer 'grad hinzeigt, und dann 1 vom Pointer abziehen POP register 'Auf den Stackpointer 1 addieren, und das Byte dort ins register laden
- Typische Anwendung:
In unserem "Unterprogramm" müssen wir irgendwelche Register verändern, wir wollen aber nicht, daß das Hauptprogramm, das uns aufgerufen hat, was davon merkt
Befehlsfolge: PUSH R24 LDI R24, 127 _Schleife: DEC R24 BRNE _Schleife POP R24 RET
Nach dem "RET" hat R24 also wieder den Inhalt, den es vorher hatte.
Interrupt-Routinen (ISR)
ISR = "Interrupt Service Request" = "Unterbrechungs-Behandlung-Anforderung"
Ein AVR besteht ja nicht nur aus CPU und Speicher, sondern auch aus einer Reihe von "Geräten", die ihre Funktion völlig unabhängig durchführen, wenn man sie mal konfiguriert hat.
Aber immer, wenn sie grad ihre Meß- oder Zählfunktion erledigt haben, wollen sie ihr Ergebnis irgendwie mitteilen und dafür wissen, wie's weitergehen soll.
Wir können dazu mit dem AVR einen Deal machen:
- Wir schreiben ein Unterprogramm, daß seine dahingehenden Ansprüche erfüllt und sagen dem AVR, wo es im Programmspeicher steht.
- Und dafür paßt der AVR auf das "Gerät" auf und macht den "Call" auf dieses Unterprogramm völlig automatisch genau dann, wenn es soweit ist. Wir brauchen uns im "normalen" Programm nun nicht mehr darum zu kümmern
Um so einen Handel einzufädeln, ist es wirklich praktisch, wenn wir Bascom hinzuziehen. Der kennt nämlich die meisten AVR-Controllertypen gewissermaßen persönlich und weiß am besten, an welchen Strippen man da bei irgendeinem Controller ziehen muß.
Beispiel Timer
Sagen wir, wir wolle jede Millisekunde ein Unterprogramm durchführen (lassen).
$regfile = "m32def.dat" $crystal = 8000000 $hwstack = 48
Kennen wir an sich schon. Aber wenn wir Interrupts verwenden, sollten wir bei "$HWSTACK=" schon ein bißchen was spendieren.
- Jetzt lassen wir Bascom den Timer konfigurieren
Config Timer0 = Timer , Prescale = 64 'Timer Setup
Lassen wir das Bascom machen. Für uns ist es dann nämlich egal, ob das auf einem Tiny oder einem ATmega128 laufen wird, es schaut immer gleich aus.
- Jetzt der Deal:
On Timer0 Interrupt_ticker ' Das ist das Unterprogramm (heißt jetzt aber ISR-Routine) Enable Timer0 ' jetzt machen wir den Timer scharf Enable Interrupts ' Und das ist sozusagen der Hauptschalter für sowas
- Das "normale" Programm:
$asm Hauptschleife: '------------------------------ ' Irgendein (Assembler?) Programm, dem der Timer völlig schnuppe ist '------------------------------ JMP Hauptschleife $end asm END
- Und nun die ISR-Routine
Interrupt_ticker: Timer0 = 131 ' Damit der arme Counter nicht immer von Null weg zählen muß ' er darf schon mit 131 anfangen $asm '------------------------------ ' Die befehle, die wir jede mS ausführen wollen '------------------------------ $end asm Return ' wieder zurück und weiter im normalen Programm
Die hier verwendeten Werte von "64" für den Prescaler und die "131" für den Counter sind aus der Angabe "$crystal = 8000000" berechnet worden und ergeben Interrupts im Abstand von 1 mS.
Gerade bei einer ISR bietet es sich an die ganze ISR in ASM zu schreiben, denn BASCOM rettet für die ISR fast alle Register (Details). Die ISR ganz in ASM ist entsprechend ein Beispiel wo der ASM Code deutlich schneller und kürzer werden kann als der Code vom Compiler.