----------------------- Assembler ab 0 und 1 ----------------------- Gerhard Paulus gp@gnomsoft.de Dieser Text könnte Leuten nützlich sein, die sich für Mikrocontroller und Assembler-Programmierung interessieren und ganz von vorn anfangen (bei 0 und 1 sozusagen). Als konkrete Hardware dient dabei der Mikrocontroller AVR AT90S4433 von Atmel. Für Beispiel-Schaltungen und weiterführende Texte und Beispiele wird das Tutorial auf http://www.mikrocontroller.net und das Tutorial auf http://www.avr-asm-tutorial.net empfohlen. Im Mikrocontroller (MC fürderhin) gibt es Bereiche, die Zahlen speichern. Diese sogenannten Register sind direkt mit der Recheneinheit des MC verbunden, die diese Zahlen addieren, subtrahieren, vergleichen etc. kann. Ein Register kann beim AVR 1 Byte fassen, also 8 Bit an Information. Ein Bit kann als Information eine 1 darstellen (eingeschaltet) oder eine 0 (ausgeschaltet), also eine durchaus überschaubare Informationsfülle. Bit ist übrigens ein Kunstwort, das sich aus "binary digit" ableitet. Die Bits eines Bytes werden von hinten nach vorn gezählt: das letzte Bit (LSB, Least Significant Bit) ist Bit Nummer 0, das erste Bit (MSB, Most Significant Bit) ist Bit Nummer 7. --------------------------------------------------------- 1 Byte : | | | | | | | | | --------------------------------------------------------- Bit 7 Bit 6 Bit 5 Bit 4 Bit 3 Bit 2 Bit 1 Bit 0 (MSB) (LSB) Von besagten Registern gibt es recht viele: 32 davon sind als generell verfügbare Arbeitsregister konzipiert. Diese Arbeitsregister werden in Programmen angesprochen mit den Namen R0 bis R31. 64 Register sind ausgelegt als Register für Eingabe und Ausgabe; diese Input/Output-Register (bzw. I/O-Register) werden auch als "ports" bezeichnet (Anlegestellen, sozusagen). Und wie im reellen Leben gibt es da eine Arbeitsteilung, d.h. manche Register können mehr als andere. Jetzt wollen wir doch mal schauen, was man mit den Bits in den Registern so alles machen kann. Wenn alle Bits auf 0 gesetzt sind dann sieht das so aus 0 0 0 0 0 0 0 0 und damit wird die Zahl 0 dargestellt, klingt irgendwie logisch. Wenn alle Bits auf 1 gesetzt sind dann sieht das so aus 1 1 1 1 1 1 1 1 und damit wird entweder die Zahl 255 oder die (negative) Zahl -1 dargestellt. Schauen wir uns mal die Zahl 255 näher an, die Sache mit den negativen Zahlen kommt später. Das übliche dezimale Zahlensystem kennt 10 Ziffern (0,1,2,3,4,5,6,7,8,9) und hat als Basis die Zahl 10. Die Dezimal-Zahl 255 kann man damit folgendermaßen auseinandernehmen: 2 5 5 | | |_ 5 * 10 hoch 0 = 5 * 1 = 5 | |___ 5 * 10 hoch 1 = 5 * 10 = 50 |_____ 2 * 10 hoch 2 = 2 * 100 = 200 ----- 255 OK, das wußten wir schon, aber wie ist das mit dem Byte und den lauter Einsen ? Antwort: Das ist genauso, aber halt binär, das heißt als Zahlensystem mit Basis 2 und mit nur 2 Ziffern: 0 und 1. 1 1 1 1 1 1 1 1 | | | | | | | |_ 1 * 2 hoch 0 = 1 * 1 = 1 | | | | | | |___ 1 * 2 hoch 1 = 1 * 2 = 2 | | | | | |_____ 1 * 2 hoch 2 = 1 * 4 = 4 | | | | |_______ 1 * 2 hoch 3 = 1 * 8 = 8 | | | |_________ 1 * 2 hoch 4 = 1 * 16 = 16 | | |___________ 1 * 2 hoch 5 = 1 * 32 = 32 | |_____________ 1 * 2 hoch 6 = 1 * 64 = 64 |_______________ 1 * 2 hoch 7 = 1 * 128 = 128 ----- 255 Jetzt könnte man natürlich fragen: Und wieso ist das nicht gleich 2 hoch 8, also 256 ? Antwort: mit den 8 Bits eines Bytes kann man tatsächlich 256 Zahlen darstellen, aber 0 ist ja bereits eine Zahl. Daher ist die höchste mit 8 Bit darstellbare Zahl 256 - 1, also 255. Um die Zahl 256 binär zu speichern sind mindestens 9 Bit erforderlich. Jetzt sind diese binären Zahlen beim Ausdrucken etwas unübersichtlich. Es hat sich in "Assembler-Kreisen" so entwickelt, daß die binären Daten in hexadezimaler Schreibweise ausgegeben werden. Also noch ein Zahlensystem, diesmal zur Basis 16. Im hexadezimalen System gibt es folgende Ziffern : 1, 2, 3, 4, 5, 6, 7, 8, 9, A , B , C , D , E , F (10) (11) (12) (13) (14) (15) Die dezimale Zahl 255 wird hexadezimal als FF dargestellt: F F | |___ F * 16 hoch 0 = 15 * 1 = 15 |_____ F * 16 hoch 1 = 15 * 16 = 240 ----- 255 Unbedingt klarer ist die Schreibweise damit nicht, aber mit 2 Druckzeichen pro Byte sehr kompakt. dezimal: 14 binär: 00001110 hex: 0E Obige Zahlensysteme lassen sich übrigens direkt in Assembler-Programmen benutzen. Um die dezimale Zahl 14 in Register R16 zu speichern, sind die folgenden Anweisungen gleichwertig: ldi R16, 14 ; dezimale Schreibweise ist Standard ldi R16, 0b00001110 ; binäre Zahlen fangen mit 0b an ldi R16, 0x0E ; Hex-Zahlen fangen mit 0x an Und wie funktioniert Addieren von binären Zahlen ? dezimal: binär: 1 0000 0001 1 + 0000 0001 + ----- ----------- 2 0000 0010 9 0000 1001 5 + 0000 0101 + ----- ----------- 14 0000 1110 Im Prinzip wie bei dezimalen Zahlen. Man geht also von rechts nach links, zählt jeweils 2 Ziffern zusammen und wenn die Summe größer ist als die größtmögliche Ziffer wird eine 1 nach links übertragen. Bei binären Zahlen erfolgt dieses Übertragen immer wenn beide zu addierenden Ziffer 1 sind. Als Regel für binäres Addieren: 0 + 0 = 0 kein Übertrag 1 + 0 = 1 kein Übertrag 0 + 1 = 1 kein Übertrag 1 + 1 = 0 mit Übertrag nach links benachbartem Bit Und wie funktioniert Subtrahieren von binären Zahlen ? dezimal: binär: 1 0000 0001 1 - 0000 0001 - ----- ----------- 0 0000 0000 9 0000 1001 5 - 0000 0101 - ----- ----------- 4 0000 0100 Falls von 0 die 1 abgezogen wird, dann wird 1 vom links benachbarten Bit abgezogen bzw. geborgt. Als Regel für binäres Subtrahieren: 0 - 0 = 0 ohne Übertrag 1 - 0 = 1 ohne Übertrag 0 - 1 = 1 mit "Borgen" vom links benachbarten Bit 1 - 1 = 0 ohne Übertrag Obige Rechenoperationen würden mit Registern zB. folgendermaßen ablaufen: ; Addition: ldi r16, 9 ; lade 9 in Register r16 ldi r17, 5 ; lade 5 in Register r17 add r16, r17 ; danach ist das Ergebnis (also 14) in Register 16 ; Subtraktion: ldi r16, 9 ; lade 9 in Register r16 ldi r17, 5 ; lade 5 in Register r17 sub r16, r17 ; danach ist das Ergebnis (also 4) in Register 16 Wenn in einem Byte maximal die Zahl 255 dargestellt werden kann und eine Zahl dazu addiert wird, dann kann es natürlich vorkommen, das das Ergebnis zu groß wird und in das Byte nicht mehr reinpaßt. Was passiert dann ? Im folgenden fängt das Register mit 0 an und dann wird immer wieder 1 addiert (das nennt man Inkrementieren und ist trotzdem eine anständige Sache). Register: --------- 0000 0000 am Anfang ist alles Null + 0000 0001 0000 0001 + 0000 0001 0000 0010 + 0000 0001 0000 0011 + 0000 0001 ... etc. ... 1111 1110 + 0000 0001 1111 1111 + 0000 0001 0000 0000 und jetzt geht es wieder bei Null weiter ! + 0000 0001 0000 0001 Wenn alle Bits gesetzt sind und es wird immer noch dazuaddiert dann bleibt dem MC nichts anderes übrig, als wieder bei Null (alle Bits auf 0) weiterzumachen. Im folgenden fängt das Register mit 2 an und dann wird immer wieder 1 subtrahiert (das nennt man Dekrementieren und ist immer noch eine anständige Sache). Register: --------- 0000 0010 - 0000 0001 0000 0001 - 0000 0001 0000 0000 - 0000 0001 1111 1111 - 0000 0001 1111 1110 Wenn beim Subtrahieren die niedrigste Zahl erreicht ist (also 0) und immer noch weiter abgezogen ist, dann springt der MC zur höchsten Zahl (alle Bits auf 1) und macht dort weiter. Wenn beim Addieren bzw. Subtrahieren zwischen 0 und 255 umgesprungen wird (also von 0000 0000 zu/von 1111 1111), dann weist der MC darauf hin, daß etwas außerordentliches passiert ist. Er setzt in dem Status-Register SREG das unterste Bit (Bit Nummer 0) auf 1. Mit anderen Worten: der Carry-Flag wird gesetzt (Vortrags-Fähnchen klingt etwas seltsam, wäre aber die Übersetzung ins Deutsche). In dem Statusregister gibt es noch weitere Bits, die wichtige Informationen beinhalten. Hier die komplette Flaggen-Parade: Status-Register: Bit 7 I Interrupt Enabled flag (SREG) Bit 6 T Transfer flag Bit 5 H Half-carry flag Bit 4 S Sign flag Bit 3 V Overflow flag Bit 2 N Negativ flag Bit 1 Z Zero flag Bit 0 C Carry flag Im folgenden sind nur die beiden unterwertigsten Flags interessant. Der Carry-flag wird gesetzt, wenn der Wert eines Registers bei Addition durch's Dach geht bzw. bei Subtraktion durch den Boden geht; andernfalls wird er auf 0 gesetzt. Der Zero-Flag wird nach jeder Rechenoperation gesetzt (Bit hat Wert 1) wenn das Ergebnis der Berechnung eine Null ist; andernfalls wird der Zero-Flag rückgesetzt (Bit hat Wert 0). Die meisten der anderen Flags sind relevant für Rechenoperationen für Zahlen mit Vorzeichen, die später im Anhang behandelt werden. Bis jetzt wurden nur Rechenoperationen für Zahlen ohne Vorzeichen behandelt, wo negative Zahlen nicht vorkommen können. Soweit mal zum Thema Zahlen, jetzt schauen wir uns doch mal die Sache mit dem Speicher an. Der MC hat einen Hauptspeicher, der Daten (also Zahlen) speichern kann. Der Hauptspeicher fängt mit den Arbeitsregistern an, hinter denen sich die I/O-Register anschließen und dahinter fängt der SRAM an. Jedes Byte, das in diesem Speicher gehalten wird, kann durch seine Adresse angesprochen werden. Diese Adressen werden üblicherweise in hexadezimaler Schreibweise angegeben. Beim AT90S4433 sieht das so aus (niedrigste Addresse oben, jedes Register belegt ein Byte): Adresse dez: hex: 0 00 Register 0 (R0) + 1 01 Register 1 (R1) | | ... | 32 Arbeitsregister a 1 Byte | 30 1E Register 30 (R30) | 31 1F Register 31 (R31) + 32 20 ??? + | ... | 64 I/O Register a 1 Byte | 95 5F SREG + 96 60 erstes Byte + | ... | 128 Bytes SRAM | 223 DF letztes Byte + Wird ein Byte in diesem Datenspeicher gespeichert, dann "wohnt" es sozusagen in einem Lagerregal. Man kann die Rechenheit via Programm anweisen, an einer bestimmten Addresse ein Byte zu holen, zu ändern und wieder an dieser Addresse zu speichern. In Assemblerprogrammen explizit mit Speicheraddressen zu arbeiten, wäre zu umständlich. Deshalb bekommen bestimmte Adressen im Hauptspeicher Namen, zB. werden die Arbeitsregister generell mit R0 bis R31 bezeichnet. Beim Assemblieren werden diese Namen automatisch in die richtige Adresse umgewandelt. Der Assembler nutzt dazu die Include-Datei (hier 4433def.inc), die mit der Direktive .include in das Assembler-Programm eingebunden wird. Das Status-Register (mit den diversen Flags) hat zum Beispiel im Hauptspeicher die Speicheradresse heximal 5F, die im Programm mit dem Namen SREG spezifiziert werden kann. Ein eigenständiger Speicher ist der Programmspeicher, in dem der Programmcode gespeichert wird, den der AVR zur Laufzeit der Anwendung abarbeitet. Dieser Flash-Speicher ist beim AT90S4433 4096 Bytes groß (4 KB). Der Programmcode ist das Ergebnis des Assemblierens, wobei jeder Programmbefehl in 2 Bytes gepackt wird (also insgesamt 16 Bit, auch "word" genannt). Jetzt schauen wir uns doch mal an, wie die Assembler-Programme vom MC abgearbeitet werden. Dazu muß das assemblierte Programm in den Programmspeicher des MC geladen werden. Wenn der MC dann gestartet wird bzw. zurückgesetzt wird (reset), dann liest er automatisch den Befehl, der im Programmspeicher an der ersten Stelle steht (also in den ersten beiden Bytes) und führt ihn aus. Dann wird der Befehl ausgeführt, der an der darauffolgenden Stelle steht (also in den nächsten 2 Bytes). Der MC merkt sich dabei die laufende Nummer des aktuell ausgeführten Befehls im Register PC (program counter). Standardmäßig wird bei jedem nächsten Befehl der Inhalt des Registers PC um 1 erhöht. Dieses Spielchen geht solange, bis keine Befehle mehr gefunden werden, worauf der MC wieder zum Beginn des Programmspeichers geht und das Programm wieder von Anfang an abarbeitet. Diese Schleife macht der MC automatisch, ohne daß man ihm das sagen muß. Folgendes aufregende Programm NOP ; erster Befehl: no operation = tu nichts NOP ; zweiter Befehl: no operation = tu nichts NOP ; dritter Befehl: no operation = tu nichts würde im Programmspeicher folgendermaßen aussehen (in hexadezimaler Schreibweise mit Zeilenumbruch nach jeweils 2 Bytes): 0000 0000 0000 Obwohl es nicht so aussieht: obiges "Programm" tut tatsächlich etwas. Es beschäftigt den MC 3 Taktzyklen lang (bei 8MHz also 375 Nanosekunden). Das kann sinnvoll sein, wenn der MC in einem bestimmten Zusammenhang zu schnell ist und bei bestimmten Eingaben/Ausgaben Zeit gewonnen werden muß. Die oben beschriebene strikt sukkzessive Programm-Abarbeitung ist aber die Ausnahme. Normalerweise wird im Programm auch gesprungen, damit nicht der nächstfolgende Befehl ausgeführt wird sondern ein anderer, der im Programm entweder weiter unten oder oben steht. In folgendem Programm sind 2 Sprünge eingebaut. Das Programm läßt LED's an Port B leuchten, wenn Taster an Port D gedrückt werden. Dazu muß Port B als "output" und Port C als "input" definiert werden. Vorher eine kurze Bemerkung: um eine Zahl in ein normales Arbeitsregister zu schreiben kann man den Befehl "ldi" benutzen, bei dem als Parameter die Addresse des Registers und die konkret zu speichernde Zahl mitgegeben werden muß. Bei I/O-Registern ist dazu der Befehl "out" notwendig mit Angabe der Addresse des jeweiligen I/O-Registers und der Addresse eines Arbeitsregisters, in dem die Zahl gespeichert ist. Der Befehl "ldi" funktioniert bei I/O-Registern nicht. Das ganze sieht dann zB. so aus: ldi r16, 0xFF out DDRB, r16 ; PortB als output Es wird also zuerst eine Zahl in ein Arbeitsregister geladen und von dort dann in des Register DDRB (Data Direction Register for Port B). Wenn in diesem I/O-Register wie in diesem Fall alle Bits auf 1 gesetzt sind so gelten alle Pins dieses Ports als Ausgänge. .include "4433def.inc" rjmp weiter ; überspringe die nop's nop nop weiter: ldi r16, 0xFF out DDRB, r16 ; PortB als output ldi r16, 0x00 out DDRD, r16 ; PortD als input loop: in r16, PIND ; lies Taster-Stellungen (lies alle Pins an Port D ) out PORTB, r16 ; lasse entsprechend LED's leuchten (schreibe zu Pins an Port B) rjmp loop Das sieht dann im Programmspeicher folgendermaßen aus. Dabei wird hexadezimale Schreibweise benutzt mit Zeilenumbruch nach jeweils 2 Byte. Für jeden Maschinenbefehl (opcode genannt) wird auch der Quellcode des Assemblerprogramms angegeben. opcode: Quellcode: ------- ------------- c002 rjmp weiter 0000 nop 0000 nop ef0f ldi r16, 0xFF bb07 out DDRB, r16 e000 ldi r16, 0x00 bb01 out DDRD, r16 b300 in r16, PIND bb08 out PORTB, r16 cffd rjmp loop Der Assembler generiert also für jede Programmzeile einen Maschinencode, der in 2 Bytes reinpaßt. Leerzeilen/Kommentarzeilen werden übersprungen, das leuchtet ein. Aber für die Anweisung "loop:" gibt es ja gar keinen korrespondierenden Befehl im Programmspeicher. Hat der Assembler da was vergessen ? Scheinbar nicht, denn das Prgramm funktioniert. Diese Anweisung mit dem abschließenden Doppelpunkt wird als "label" benutzt, also wie das Ding, das an Kaufhauswaren hängt und die Ware näher bezeichnet. Im Programm kann damit eine Addresse im Programmspeicher spezifiziert werden, ohne daß man sich als Programmierer diese Addresse mühsam ausrechnen muß. Diese Arbeit übernimmt der Assembler beim Übersetzen des Quellcodes. Schaun wir uns doch mal die erste Anweisung an: rjmp weiter Entsprechend dem Befehlssatz (instruction set) des AVR wird diese Anweisung übersetzt in folgenden 16-bit opcode: 1100 kkkk kkkk kkkk 1100 ist dabei fest vorgegeben und ist die "Kennung" für rjmp (relative jump). Mit den k's wird eine Zahl codiert, die einen relative Sprung definiert. Die Syntax für diesen Befehl ist PC <-- PC + k + 1 PC steht dabei für program counter und bezeichnet die Nummer des aktuell in Bearbeitung stehenden Befehls. Der Maschinenbefehl, der also im Programmspeicher steht, ist in diesem Fall hexadezimal c002 Binär ausgedrückt ist das 1100 0000 0000 0010 und damit ist die relative Sprung-Adresse binär 0000 0000 0010 Damit ist also dezimal die Zahl 2 codiert, die den Sprung vorgibt. Konkret bedeutet dies, daß der MC vom aktuell bearbeiteten Befehl im Programmspeicher 2 Befehle weiterzuspringen und den darauffolgenden Befehl zu lesen und auszuführen. Und das ist die Programmanweisung ef0f, also wieder ganz so wie geplant. Die letzte Anweisung in dem Programm ist auch eine relative Sprung-Anweisung, aber diesmal wird im Programmablauf rückwärts gesprungen. rjmp loop Hex cffd ist binär 1100 1111 1111 1101 und damit ist die relative Sprung-Adresse binär 1111 1111 1101 bzw. dezimal -3. Um das zu verstehen, muß man die Sache mit den Zahlen und den Vorzeichen lesen (das kommt noch). Für den MC ist der Programmcode cffd eine Anweisung, vom aktuell bearbeiteten Befehl im Programmspeicher 3 Befehle zurückspringen und den darauffolgenden Befehl zu lesen und auszuführen. Und das ist die Programmanweisung b300, also auch wieder so wie vorgesehen. Mit diesen Sprüngen ist auch eine Art strukturierte Programmierung möglich. Im folgenden wird eine if-Struktur programmiert, wie sie von Hochsprachen wie C oder Java bekannt sind: if ( ) { // } else { // } Konkret schaltet das Programm zwei LED's an PortB alternierend an und aus und legt dann eine Kunstpause ein. .include "4433def.inc" ldi r16, 50 ldi r17, 0xFF out DDRB, r17 ; port B als Ausgabe (über LED's) loop1: if1: cpi r16, 100 ; compare immediate brne else1 ; branch if not equal ldi r17, 0b11111110 out PORTB, r17 ldi r16, 50 rjmp endif1 else1: ldi r17, 0b11111101 out PORTB, r17 ldi r16, 100 endif1: ldi r17, 255 while1: subi r17, 1 breq endwhile1 ; wenn Zero-Flag gesetzt nop ; Päuschen rjmp while1 endwhile1: rjmp loop1 Der Knackpunkt in obigem Programm sind diese beiden Anweisungen: cpi r16, 100 ; compare immediate brne else1 ; branch if not equal Es wird mit "cpi" der Inhalt von Register 16 verglichen mit der Zahl 100. Der MC macht das so, als wenn er von Register 16 die Zahl 100 abzieht und alle Flags des Status-Registers setzt, aber den Inhalt von Register 16 ansonsten so beläßt, wie er vor der Operation war. Es wird also nicht echt subtrahiert. Die Anweisung "brne" prüft danach den Zero-Flag. Wenn der Inhalt des Registers 16 genauso groß ist wie die Zahl 100, dann steht nach der simulierten Subtraktion der Zero-Flag auf 1. "brne" wird in diesem Fall nicht zur Marke else1 springen sondern der Programmablauf geht direkt bei der nächsten Anweisung weiter. Am Ende dieses Zweiges wird dann zur Marke endif1 gesprungen. Wenn der Inhalt des Registers 16 nicht identisch ist mit der Zahl 100, dann steht nach der simulierten Subtraktion der Zero-Flag auf 0. "brne" wird in diesem Fall direkt zur Marke else1 springen und von dort aus automatisch irgendwann die Marke endif1 erreichen. Die folgende Anweisung funktioniert ähnlich, allerdings wird hier echt subtrahiert: subi r17, 1 breq endwhile1 ; wenn Zero-Flag gesetzt Die Anweisung "breq" springt zur Anweisung "endwhile1" sobald das Ergebnis der Subtraktion 0 ist. Andernfalls arbeitet der MC den nächstfolgenden Befehl ab. Soweit zum Thema Programmieren. Jetzt manipulieren wir mal gezielt Bits in einem Register, wenn eine Zahl in dem Register schon geladen ist. Es werden also gezielt Bits gesetzt (Bit bekommt Wert 1) oder zurückgesetzt (Bit bekommt Wert 0). Das geht und hat recht nützliche Effekte (vorausgesetzt es funktioniert so, wie man sich das vorstellt): Erst mal die logische Operation AND. Dabei werden 2 Bytes Bit für Bit verglichen und das jeweils resultierende Bit ergibt sich nach folgender Regel : AND 1 1 -> 1 1 0 -> 0 0 1 -> 0 0 0 -> 0 Das Ergebnis des logischen Vergleichs ist also nur dann 1 (wahr), wenn beide Bits den Wert 1 hatten. Die entsprechende Assembler-Anweisung lautet "andi" (AND immediate). ldi r16, 0b00001110 ; im Register: 0000 1110 andi r16, 0b00000001 ; im Register: 0000 0000 Das Ergebnis dieser Aktivitäten ist also Zahl 0 im Register r16. Bei der logischen Operation OR (inklusives OR) werden 2 Bytes Bit für Bit verglichen und das jeweils resultierende Bit ergibt sich nach folgender Regel: OR 1 1 -> 1 1 0 -> 1 0 1 -> 1 0 0 -> 0 Das Ergebnis des logischen Vergleichs ist also dann 1 (wahr), wenn irgendeins der beiden Bits den Wert 1 hatte. Die entsprechende Assembler-Anweisung lautet "ori" (OR immediate). Nur wenn beide Bits 0 sind dann ist das Ergebnis des Vergleichs auch 0. ldi r16, 0b00001110 ; im Register: 0000 1110 ori r16, 0b00000001 ; im Register: 0000 1111 Das Ergebnis dieser Aktivitäten ist also Zahl 15 im Register r16. Bei der logischen Operation XOR ("exklusives oder") werden 2 Bytes Bit für Bit verglichen und das jeweils resultierende Bit ergibt sich nach folgender Regel: Das Ergebnis des logischen Vergleichs ist also nur dann 1 (wahr), wenn nur eines der beiden Bits den Wert 1 hatte. Wenn beide Bits den Wert 1 hatten, dann ist das Ergebnis 0. Die entsprechende Assembler-Anweisung lautet "eor". XOR 1 1 -> 0 1 0 -> 1 0 1 -> 1 0 0 -> 0 ldi r16, 0b00001110 ; im Register: 0000 1110 ldi r17, 0b00001000 ; im Register: 0000 1000 eor r16, r17 ; im Register: 0000 0110 Das Ergebnis dieser Aktivitäten ist also Zahl 6 im Register. Mit diesen logischen Operationen lassen sich gezielt bestimmte Bits in einem Byte entweder setzen oder zurücksetzen. Wie setze ich in einem Byte das Bit 3 auf 1 und lasse die restlichen Bits des Byte so wie sie momentan sind ? Dazu braucht man ein Byte, in dem die Bits folgendermaßen aufgebaut sind (das nennt man Bit-Maske) : 0000 1000 nur Bit 3 hat Wert 1, alle anderen Bits haben Wert 0 * Und dann muß das Register-Byte und das Masken-Byte logisch mit OR verknüpft werden. Der passende Befehl lautet: ldi r16, 0b11000011 ; im Register: 1100 0011 ori r16, 0b00001000 ; im Register: 1100 1011 * Danach ist im Byte von Register r16 das Bit Nummer 3 auf jeden Fall auf 1 gesetzt, egal welchen Wert das Bit vorher hatte. Die restlichen Bits bleiben so, wie sie sind. Wie bekomme ich eine 0 in die Bits 3 und 4 in einem Register-Byte und lasse die restlichen Bits des Byte so wie sie momentan sind ? Man nehme dazu ein Byte-Maske, in dem die Bits folgendermaßen aufgebaut sind 1110 0111 nur Bit 3 und 4 haben Wert 0, alle anderen Bits haben Wert 1 * * Und dann muß das Register-Byte und das Masken-Byte logisch mit AND verknüpft werden. Der passende Befehl lautet: ldi r16, 0b01010101 ; im Register: 0101 0101 andi r16, 0b11100111 ; im Register: 0100 0101 * * Danach ist im Byte von Register r16 das Bit Nummer 3 und 4 auf jeden Fall auf 0 gesetzt, egal welche Werte die Bits vorher hatten. Die restlichen Bits bleiben so, wie sie sind. Praktisch sind diese Bit-Masken, weil damit alle Bits eine Byte mit einem einzigen Befehl gesetzt werden könen. Für die unteren I/O-Register (ports) gibt es einen eigenen Befehl, um gezielt ein Bit auf 0 oder 1 zu setzen. Mit folgendem Befehl wird in einem UART Control-Register das Bit RXEN auf 1 gesetzt. sbi UCSRB, RXEN ; set bit immediate Damit wird ein Empfang von Daten über die serielle Schnittstelle ermöglicht. Die Namen UCSRB und RXEN sind definiert in 4433def.inc, die in dem Assembler-Programm eingebunden sein muß. .equ UCSRB =$0a .equ RXEN =4 UCSRB steht also für Adresse Hex 0A und RXEN steht für Bit Nummer 4 Mit folgendem Befehl wird in einem UART Control-Register das Bit RXEN auf 0 zurückgesetzt und der MC liest keine Daten mehr von der Schnittstelle. cbi UCSRB, RXEN ; clear bit immediate OK, und wie kann ich alle Bits eine Byte umdrehen, so daß alle 1 zu 0 werden und umgekehrt ? Antwort: mit dem Einser-Komplement. ldi r16, 0b11000011 ; im Register: 1100 0011 com r16 ; im Register: 0011 1100 So, und was kann man mit diesen Bits noch so alles machen ? Man kann die Bits eines Register-Byte auch nach links und rechts schieben. Die Befehle lauten lsl (logical shift left) und lsr (logical shift right). ldi r16, 0b00001010 ; im Register: 00001010 dezimal: 10 lsl r16 ; im Register: 00010100 <-- links geschoben; dezimal: 20 lsr r16 ; im Register: 00001010 --> rechts geschoben; dezimal: 10 Wenn die Bits eine Stelle nach links geschoben werden, dann entspricht das einer Multiplikation mit 2. Von rechts wird dabei immer eine 0 eingeschoben. Das niederwertigste Bit hat also nach dem Schieben immer den Wert 0. Das nach links rausgeschobene Bit wird im Carry-Flag des Status-Registers gespeichert. Wenn die Bits eine Stelle nach rechts geschoben werden, dann entspricht das einer Division durch 2. Von links wird 0 reingeschoben, das nach rechts rausgeschobene Bit wird zum Carry-Flag. Dann kann man die Bits noch im Kreis rotieren lassen. Die Befehle sind rol (rotate left through carry) und ror (rotate right through carry). Dabei wird der Wert des Carry-Flags jeweils reingeschoben und das rausgeschobene Bit wird im Carry-Flag gespeichert. Carry-Flag Register Carry-Flag zu Beginn am Ende ldi r16, 0b01111110 ; ? 0111 1110 0 rol r16 ; 0 1111 1100 0 rol r16 ; 0 1111 1000 1 rol r16 ; 1 1111 0001 1 Jetzt zu den Zahlen mit Vorzeichen, also den negativen und positiven Zahlen (signed numbers). In diesem Fall muß irgendwo in den 8 Bits eine Information stehen, ob die betreffende Zahl positiv ist (Vorzeichen +) oder negativ ist (Vorzeichen -). Dafür hat man sich das höchstwertige Bit ausgesucht, das Bit ganz links sozusagen. Wenn dieses Bit 0 ist, dann ist die Zahl positiv, wenn dieses Bit 1 ist, dann ist die Zahl negativ. Jetzt gibt es aber ein kleines Problem: wie kann dann noch die Zahl 255 dargestellt werden, bei der ja das Bit Nummer 7 auch auf 1 gesetzt ist ? Antwort: in einem Byte kann eine vorzeichenbehaftete +255 überhaupt nicht dargestellt werden. Die obere Grenze bei 8 Bits und Vorzeichen ist +127, die untere Grenze liegt dafür nicht mehr bei 0 sondern bei -128. Allerdings ist -1 nicht dargestellt mit 1000 0001, wie man ja durchaus vermuten könnte. Sondern die negative Zahl wird durch ein sogenanntes Zweier-Komplement dargestellt. Das ganze funktioniert dann so, als ob man von 0 die jeweilige (absolute) Zahl abzieht. 0000 0011 +3 0000 0010 +2 0000 0001 +1 0000 0000 +0 1111 1111 -1 1111 1110 -2 1111 1101 -3 Dazu gilt eine einfache Regel, um eine positive Zahl in eine negative Zahl zu verwandeln : man drehe alle Bits um (von 0 nach 1 und von 1 nach 0) und addiere am Ende noch 0000 0001 und schon hat man die richtige Bit-Folge für die negative Zahl. Nehmen wir als Beispiel die Zahl +3, die wir in -3 wandeln: dezimal: binär: +3 0000 0011 1111 1100 + 1 -3 = 1111 1101 Es gibt auch einen Befehl, um eine solche Negation zu erreichen (sinnigerweise NEG). ldi r16, 3 ; dezimal +3 Register: 0000 0011 neg r16 ; dezimal -3 Register: 1111 1101 Dumme Frage: wie kann der MC eigentlich feststellen, ob die Zahl in einem Register jetzt positiv ist oder negativ ? Immerhin kann binär 1111 1111 die vorzeichenlose 255 darstellen oder auch -1. Der Programmierer weiß, um was es geht, aber wie kriegt der MC das raus ? Antwort: der MC kann es überhaupt nicht feststellen. Der MC weiß nie, ob die Zahl in dem Register jetzt vorzeichenbehaftet ist oder nicht. Und da der MC nicht weiß was läuft, setzt er bei Rechenoperationen wie Addition und Division immer auch alle Flags, die für vorzeichenbehaftete zahlen notwendig sind. Im ungünstigsten Fall macht der MC etwas, was vom Programmierer in dem jeweiligen Zusammenhang nicht gebraucht wird. Für vorzeichenbehaftete Zahlen sind folgende Flags des Status-Registers relevant: Status-Register: Bit 7 Bit 6 Bit 5 Bit 4 S Sign flag Bit 3 V Overflow flag Bit 2 N Negativ flag Bit 1 Z Zero flag Bit 0 C Carry flag Der Carry-Flag wird gesetzt, wenn die Bit-Folge von 0000 0000 auf 1111 1111 dreht (bzw. umgekehrt). Der Zero-Flag wird gesetzt, wenn das Ergebnis der Rechenoperation Null ist. Der Negativ-Flag wird gesetzt, wenn das Ergebnis der Rechenoperation eine negative Zahl sein *könnte*. Der Negativ-Flag ist 1 bei allen Operationen, bei denen das Ergebnis zwischen folgenden Bit-Folgen liegen: binär: dezimal: 1111 1111 -1 1111 1110 -2 ... 1000 0001 -127 1000 0000 -128 Der Overflow-Flag wird gesetzt, wenn die Zahl im Register wechselt zwischen der höchsten positiven Zahl und der negativsten Zahl (das heißt +127 und minus 128). binär: dezimal: 0111 1111 +127 positivste Zahl 1000 0000 -128 negativste Zahl Beim Inkrementieren sieht das dann so aus : dezimal: Register: Carry Negativ Overflow +126 0111 1110 ? ? ? + 0000 0001 +127 0111 1111 0 0 0 + 0000 0001 -128 1000 0000 0 1 1 !!! + 0000 0001 -127 1000 0001 0 1 0 + 0000 0001 Hmmm ..., wenn in einem Register nur maximal eine Zahl bis 255 dargestellt werden kann, wie kann ich dann die Zahl 444 speichern ? Antwort: man nimmt halt 2 Register. ldi r16, 0b10111100 ; dezimal: 188 = 444 - 256 ldi r17, 0b00000001 ; dezimal: 1 Wenn man jetzt beide Register bei Rechenoperationen zusammen behandelt, dann stellen sie zusammen die Zahl 444 dar. Dem MC ist das übrigens vollkommen egal, der kriegt das gar nicht mit. Daß die Register R16 und R17 logisch zusammengehören, das weiß nur der Programmierer. dezimal: binär: 444 0000 0001 1011 1100 | R17 | R16 | Das ganze sieht zwar aus wie von hinten durch die Brust ins Auge, aber damit können jetzt fast beliebig große Zahlen dargestellt werden, solange wie die Register ausreichen. Diese "Großzahlen" können auch addiert werden. Im folgenden Beispiel wird dezimal 444 addiert zu 444: ldi r16, 0b10111100 ; dezimal: 188 ldi r17, 0b00000001 ; dezimal: 1 ldi r18, 0b10111100 ; dezimal: 188 ldi r19, 0b00000001 ; dezimal: 1 add r16, r18 adc r17, r19 r16 und r17 haben jetzt (zusammen betrachtet) das Resultat der Addition. dezimal: binär: 888 0000 0011 0111 1000 | R17 | R16 | Die Addition erfolgt also hier 2-stufig : 1) mit ADD werden erst die niederwertigen Bytes addiert. Dabei wird vom MC automatisch der Carry-Flag gesetzt, wenn die Summe der beiden Bytes größer als 255 ist. 2) mit ADC (add with carry) werden dann die höherwertigen Bytes addiert. Der Trick besteht hier darin, den Vortrag von der ersten Addition auch noch dazuzuzählen, deswegen ADC. Das funktioniert auch mit Subtraktion, nur lauten die Assembler-Befehle dann "sub" für Subtraktion und "sbc" für Subtraktion durch Carry. ldi r16, 0b10111100 ; dezimal: 188 ldi r17, 0b00000001 ; dezimal: 1 ldi r18, 0b10111100 ; dezimal: 188 ldi r19, 0b00000001 ; dezimal: 1 sub r16, r18 sbc r17, r19 Danach ist der Wert von Register r16 und r17 jeweils 0000 0000.