Home | Artikel | VCFe Maschinencode und Assembler

VCFe Vortrag vom 2008.04.26 - 8008 8080/8085 und Z80/U880 - Maschinencode und Assembler


Inhalt

Einführung

Dieser Vortrag beschreibt wie die Maschinencodes vom ursprünglichen 8bit Mikroprozessor 8008, sowie den überarbeiteten 8080/8085, wie auch den erweiterten Z80/U880 funktionieren. Ebenso wie man Programme für diese direkt in Maschinencode oder in völlig verschiedenen Assemblern generieren kann.

Abgedeckt werden einerseits die Funktionsweise von Recheneinheit, Registern und Speicher, über die Befehlscodes, bis hin zum Programablauf, und anderseits die Eingabe der Befehlscodes in Binär Oktal und Hex, sowie ihre Generierung mit 8008 Assembler, 8080/8085 Assembler, Z80 Assembler, und in algebraischer Notation.

Dabei soll gezeigt werden, dass Befehlscodes eine grundlegende von der Hardware gegebene Sache sind, die alleine stehen können, während Assembler eine darübergestülpte Software Definition sind, die wie jedes Programm (und dessen Eingabeformate) beliebig ersetzt werden können.

Es wird angenommen, dass der Zuhörer die Hardware Seite von Rechnern bisher nicht gross kennt, ausser Platinen oder gar nur Geräte zusammenzustecken und drauf Software zu installieren, also keine Kenntnis vorhanden ist, was drinnen in den Teilen auf den Platinen passiert.

Es wird lediglich vorausgesetzt, dass der Zuhörer die Grundlagen von programmieren kennt (und sei dies nur Scripte) und Konzepte wie Variablen, Ausdrücke, Zuweisungen, Schleifen und Unterprogramme kennt.

Dieser Vortrag ist der zweite Teil zu einem früheren VCFe Vortrag vom 2006.05.01 - Rechnen mit Bauklötzchen - Rechnergrundlagen und deren Implementationen.

Dieser Vortrag wurde später, für eine Wiederholung am 2008.10.17 in der LUGS, leicht überarbeitet (Tippfehler, Formulierungen verbessert und expandiert, z.T. verständlichere Erklärungen, Formatierung, mehr Links addiert). Er wurde noch später, nach einer Wiederholung am 2009.05.23 am LUG-Camp, aufgrund Zuhöhrer Feedback leicht überarbeitet (Linkregister basierte Unterprogrammaufrufe, Jump/Branch Mnemonics).

Geschichtliche Position dieser Mikroprozessoren

Zuerst einmal ein kurzer historischer Abriss, wo in der Entwicklung und dem Gebrauch der Mikroprozessoren diese Chips auftauchen. Einerseits ist die Firma Intel ja, neben Texas Instruments, einer der beiden gleichzeitigen Erfinder des Mikroprozessors. Anderseits viel wichtiger ist wozu sie verwendet wurden, weil sich erst dort zeigt was für eine Bedeutung etwas hat(te).

Als erstes erschien 1971 der 4bit Prozessor 4004 mit 2300 Transistoren ( gescanntes Handbuch), welcher später (1973 oder 74) noch durch den geringfügig erweiterten 4040 ersetzt wurde. Ursprünglicher Zweck war eine Auftragsarbeit, als zentrale Logik eines BCD Tischrechners der Firma Busicom zu dienen (einem Gerät mit in etwa der Funktionalität eines heutigen Tachenrechners), mit einem einzigen fix eingebautem Programm. Er wurde erst später als allgemeinen programmierbaren Steuerbaustein auf die Welt losgelassen, und war dementsprechend auch nicht geeignet als Prozessor eines Allzweck Rechners zu dienen, vor allem da es ohne aufwendige externe Basteleien (später gab es 2 spezielle 4008 und 4009 Chips zum diese vereinfachen) nicht möglich war, Programme von Band oder Disk zu laden, oder zu Laufzeit eingeben oder generieren, und dann auszuführen, weil eine strikte Trennung von Programm- und Datenspeicher dies verhinderte. Auch die Daten Ein-/Ausgabe war auf Tasten und 7-Segment Anzeigen beschränkt, und nur mit grossem Aufwand (mit dem 4009 Chip) erweiterbar, weil direkt in den ebenfalls speziellen Speicherchips (4001 256x8bit ROM, 4002 64x4bit RAM) eingebaut. Daher erstreckt sich deren Bedeutung praktisch nur im Bereich von kleinen Steuerungen, die flexibel per Computer sein sollen aber die Kosten eines Minicomputers nicht rechtfertigen konnten. Verkehrsampeln werden immer wieder as Beispiel herumgereicht. Auch dabei war er nicht gerade wichtig, er ist einfach nur "der Erste" (bzw einer der allerersten).

Als zweites erschien 1972 der 8bit Prozessor 8008 mit 3500 Transistoren ( gescanntes Handbuch, dessen ursprünglicher Zweck war ebenfalls eine Auftragsarbeit, als günstigere/kleinere Variante der zentralen Logik des 1970er programmierbaren Terminals Datapoint 2200 zu dienen. Der Operations, Register/Speicher und Befehlssatz stammt dementsprechend von Datapoint und nicht etwa von Intel! Eines dessen Haupteigenschaften war die Ersetzbarkeit der Steuerungs Software (von Band ladbar), zum verschiedenste Terminals emulieren, und folglich ein gemeinsamer Program- und Datenspeicher. Dieses Terminal wurde dann auch schon in Form des vor-Mikroprozessor Originales als "unabsichtlichen" einfachen Computer missbraucht, mit eigenen Programmen der User. Folglich waren 8008 Hobbyrechner nur eine Zeitfrage, nachdem dieser Chip auf die restliche Welt losgelassen wurde. Einerseits gab es kommerziell vertriebene Produkte, wie den Micral oder den Scelbi 8H, anderseits auch ein in einer Radio Elektronik Zeitschrift veröffentlichten simplen Selbstbaurechner zum nachbauen, den Titus Mark 8 bzw Titus Mark 8, sowie einige persönliche Einzelstücke verschiedenster Erbauer. Seine grosse Bedeutung liegt aber darin, der Vorläufer des 8080 zu sein.

Die Weiterentwicklung des 8008 in 1974 zum 8080 mit 6000 Transistoren war, im Gegensatz zur Entwicklung vom 4004 zum 4040, eine grössere Überarbeitung und Erweiterung. Und er wurde von Anfang an für beliebige Anwendungen beabsichtigt. Dies machte den 8080 zum ersten vollständigen computertauglichen Mikroprozessor. Insbesondere ermöglichte er beliebig viele verschachtelte Unterprogrammaufrufe, und dazu noch 4 mal mehr Speicher, und komplexere Befehle (v.a. besser adressieren). Er ist daher aber auch nicht 100% Kompatibel zum 8008. Aber die Änderungen an der bestehenden Funktionalität sind gering (zumeist nur andere binäre Nummerierung) und daher problemlos vorhersehbar (und automatisch ersetzbar). Man kann insbesondere 8008 Programme (bis auf die Ein-/Ausgabe!) vollautomatisch 1:1 für 8080 umwandeln lassen. Das Erscheinen des 8080 löste gleich von Anfang an ein Wettrennen aus, damit Hobbyrechner zu bauen. Der erste gross bekannte war ein selbst-zusammenlöt Bausatz, der MITS Altair 8800, der auch gleich den ersten Standardbus (S-100) definierte, und bereits auch als Erster geklont wurde, durch den mechanisch verbesserten IMSAI 8080. Diesen folgte einerseits der erste vollständige Homecomputer mit S-100 Bus und Bildschirm und Tastatur und Tonband zum Daten speichern, der Processor Technology Sol-20, und anderseits die erste Flut an generischen S-100 basierten RS232+Terminal und Floppydisk basierten Mikrocomputern mit dem ersten Standardbetriebsystem (CP/M). Aber auch Terminals (wie das DEC VT100 (rechts in der Mitte)) und Steuerungen (wie der Mikrocode Lader des DEC KS-10 Prozessors des PDP-20 Grossrechners) beinhalten 8080er.

Der 8085 von 1976 dagegen war nur eine sehr leichte Änderung. Vor allem wird durch den Gebrauch neuerer Herstelltechniken der Einsatz einfacher und billiger, wie eine einfachere Stromversorgung (die 5 im Namen steht für nur noch 5V Versorgung, im Gegensatz zu den 5V + 12V + -5V des 8080), sowie weniger externe Beschaltung (nur standard 1 standard 74373 Chip statt 2 speziellen 8224 und 8228 Chips, sowie Interrupts ohne Hilfslogik möglich). Hier konnte man ausser bei teils Interrupts sogar die unveränderten 8080 Programme weiter benutzen. Der 8085 hat es nicht mehr in die Home- und Mikrocomputerwelt geschafft. Er ist daher vor allem in vielen industriellen Geräten verbaut worden, darunter auch in Terminals (wie dem Tektronix LT-ACS (in der Mitte), das ich am letzten VCFe aufgelesen habe).

Ein Teil der Intel 8080 Mitarbeiter waren mit der Situation in der Firma nicht zufrieden, vor allem mit dem Mangel an Plänen zur Weiterentwicklung der Prozessoren, und gründeten dazu ihre eigene Firma Zilog. Das Resultat war ebenfalls in 1976 der Z80. Er braucht auch nur 5V, noch weniger externe Beschaltung (nur einen möglicherweise schon vorhandenen Oszillator, und je nach Modus keine Interrupt Hilfslogik). Anderseits hat er einiges an grösseren Erweiterungen, und ist trotzdem bis auf ein selten benutztes Feature (Parity Flag bei Arithmetik) voll kompatibel. Daher ist es nicht verwunderlich, dass er in der Mikrocomputer Welt sehr schnell dem 8080 den Rang ablief (und den 8085er seine Verbreitung ausbremste), und in der Homecomputer Welt sogar von Anfang an deren Eintritt verhinderte. Er wurde damit zum häufigsten Mikrocomputer Prozessor schlechthin (alle späteren CP/M Rechner, wie Intertec Superbrain, Osborne und Kaypro), bevor der 8088 des IBM PC und seiner Klone die Welt übernahm (und Mikrocomputer in PCs umbenannt wurden). Er war auch neben dem 6502 der häufigste Homecomputer Prozessor aller Zeiten (wie im Radio Shack TRS-80, Sharp MZ-80, NEC PC-8001/8801, allen Sinclair ausser dem QL, allen Amstrad/Schneider CPC, allen MSX, allen Micro-Professor 1, dem Sega Master System, as Zweitprozessor im C128, als CP/M Karte im Apple 2). Auch der Nintendo Gameboy sein Prozessor ist ein modifizierter Z80 (teils Sachen entfernt plus einige Erweiterungen). Dazu kommen viele industrielle Anwendungen (auch Terminals, wie Retro Graphics (rechts unten)), Adaptec 154x PC SCSI Karten, Texas instruments TI-8x Graphik Taschenrechner, Spielhallen Automaten (wie Pac Man). Es wird dem Z80 nachgesagt, der zweithäufigst lizenzierte/geklonte und gebaute Prozessor der Weltgeschichte zu sein (nach dem 8051). Er scheint auch der häufigste Prozessor hier am VCFe zu sein. Daher auch die Wahl dieser Prozessorfamilie für diesen Vortrag. Das ich auf diesem programmieren gelernt habe (ein Z80 Mikrocomputer mit weiterem Z80 im Terminal) kommt noch dazu.

Der U880 schliesslich ist die DDR Ausgabe des Z80. Früher hiess es, dies sei ein Klon (wie auch die NEC uPD780 und Sharp LH0080). Aber am letzten VCFe wurde gemunkelt es sei inzwischen nachgewiesen, dass es ein Lizenznachbau sei, gemacht mit offiziellen Zilog Belichtungsmasken, aber mit eigenem Namen versehen (wie auch der offizielle Lizenznehmer Mostek als MK3880). Anderseits behauptet Wikipedia in U880, dass Inkompatibilitäten nachgewiesen wurden, was für einen Klon spricht. Egal welches der beiden er ist, der U880 hat die DDR Mikro- und Homecomputer Szene total dominiert, sei das in den Robotrons, oder den KC85, oder im LC80.

Kurz nach dem 8085 hat Intel noch 2 weitere inkompatible Prozessorfamilien ins Leben gerufen (und beide später dann erweitert):

Einerseits erschien 1977 der 8048 (nachgezeichnetes Handbuch). Dieser wurde von der 4004/4040 Gruppe gemacht, und ist eine auf 8bit verbreiterte und stark überarbeitete Version davon. Ziel war ein vollständiger kleiner Steuercomputer, aus simplen Prozessor, beiden kleinen Speichern, und den für kleine Steuerungen übliche Ein-/Ausgabe Schaltungen, alles auf einen einzelnen Chip, was eine neue Chipsorte ergab, den Mikrocontroller. Insbesondere sind die 4004/4040-typischen getrennten Programm- und Datenspeicher geblieben. Aber ein dritter separat zu benutzender (und in Fähigkeiten sehr limitierter) "externer Datenspeicher" (für externe Ein-/Ausgabe Erweiterung gedacht) kann mit Komplikationen auch als externe n*256Byte Erweiterung des Datenspeichers benutzt werden, und kann damit mit etwas mehr Aufwand auch beschreiben eines externen Programspeichers ermöglichen. Wegen dieser Herkunft und Schwächen hat der 8048 keinerlei Bedeutung als Mikro- oder Homecomputer CPU. Sein Gebiet ist in allerlei industriellen Anwendungen, aber auch dank geringem Preis eines Einzelchipcomputers auch in Konsumprodukten drin (wie LCD Games). Sowohl IBMs frühe PCs wie auch DECs VT100 und folgende Terminals haben aber auch einen 8048er, in ihren Tastaturen drin! Ebenfalls hat IBM die 8048 Variante 8042 auf das PC/AT Motherboard gestellt, als Tastatur Interface, und ihn damit zum häufigst benutzten Prozessor in PCs gemacht.

Daraus wurde 1980 durch mischen von 8048 und 8085 ein Art "das Beste von Beiden" Microcontroller entwickelt, der 8051. Er ist wie der 8008 zu 8080 Schritt nicht zum 8048 kompatibel (und zwar weitaus mehr), aber alle 8048 Programme können 1:1 umgewandelt werden. Man kann in diesem auch teilweise einen durch Erfahrung aufgeräumten 8080/8085 anschauen, bis auf die extrem nervige Eigenschaft, dass er zwar die Programmspeichergröesse des 8080/8085 hat, aber leider der 4004/4040/8048-typische separate Datenspeicher erhalten blieb, und viel nervender dessen sehr kleine Grösse (256bytes), was weiterhin den Murks mit dem limitiert zu benutzenden separaten externen Datenspeicher nötig macht, der Programme kompliziert. Ohne dieses Fehldesign könnte der 8051 die 8080/8085 voll und besser ersetzen. So aber wurde das nächste zu einem Rechnereinsatz wohl, das DEC ihn in das VT220 (links oben) Terminal verbaut hat. Seine grosse Verbreitung hat er wie 4004/4040 und 8048 in Steuerungen geschafft, dort wo der kleine 8048 nicht ausreichte. Dort wurde er aber schlicht zum häufigsten Prozessor aller Zeiten. Hersteller von ASIC Chips bekommen 8051er heute als kostenlose Dreingabe. Hier am VCFe war er schon mehrmals Subjekt von Vorträgen, und wurde auch im kbdbabel Projekt verbaut.

Anderseits erschien 1978 der 16bit Prozessor 8086 (und 1979 sein kleiner Bruder 8088, mit intern voll 16bit aber extern nur 8bit Leitungen). Diese beiden sind wie der 8048 100% inkompatibel zum 8080, aber nicht wegen allfälligem 4004/4040 Einfluss, sondern weil die Unterstützung von 2 Datenbreiten (16bit und 8bit) ein kompett neues Design verlangte, das aber eindeutig die Handschrift der 8008/8080/8085 Designer trägt. Neben 16bit ist die Speicherverwaltung und -erweiterung die herausragendste Änderung, da man damit 1024k/1M statt nur 64k Speicher benutzen kann. Auch daraus wurden später erweiterte Prozessoren. Einerseits addierten von 1981 an die 80186 und 80188 jede Menge Ein-/Ausgabe auf dem Chip, und wurden damit zum halben (speicherlosen) Mikrocontroller. Anderseits brachte 1982 der 286er eine komplexe Speicherverwaltung die an Grossrechner angelehnt ist und 16M Speicher benutzen kann (aber ohne die 8018x Ein-/Ausgabe). Schliesslich kam in 1986 der 80386, der den 286er massiv erweiterte auf 32bit und 4096M/4G Speicher und viele neue Befehle brachte, und wiederum von den zumeist nur beschleunigten 80486 und Pentiums und Cores gefolgt wurde. Was mit den 8086/8088 und ihren Nachfolgern alles angestellt wurde weiter zu beschreiben ist wohl müssig. Jeder IBM PC hat einen 8088, jeder PC/AT einen 286er oder spätere. Aber auch in in nicht-PC Rechnern kommen sie vor, wie den NEC PC-9800 (8086), Tandy 1000 (80186), Siemens PC-D (80186) oder Fujitsu FM Towns (80386). Ebenso in industriellen Anwendungen, als ausgefallenstes wohl der 80386 im Hubble Weltraum Teleskop.

Neben den Intel 8080/8085 (und Z80/U880) sowie 8048/8051 und 8086/8088/ff sind eigentlich nur noch 3 Prozessorfamilien von dieser Zeit ähnlich relevant geworden:

Eigentlich sind das, wenn man die viel neueren 32bit Prozessoren (wie MIPS, Sparc, ARM und PPC) ignoriert, sowie die neueren PIC und AVR Mikrocontroller, alle verbreiteten Mikroprozessoren der Weltgeschichte.

Binäre Mathematik (Repetition)

Bevor man die Funktionsweise des Rechner anschaut, sollte man zuerst einmal wissen, was der eigentlich anstellen soll. Also zuerst mal eine Einführung in Rechnen in Bits. Für Besucher des Vortrages vor 2 Jahren ist dies eine kurze Repetition, mit leichter Erweiterung.

Wir rechnen normalerweise mit dezimalen Zahlen, mit 10 Ziffern 0..9. Dies ist für Rechner schwierig, da alleine eine 1-stellige Addition von 2 Ziffern 10 mögliche Ziffern bei jedem Operanden hat, und damit 10*10 = 100 mögliche Kombinationen (die grosse Tabelle die man in der Primarschule auswendig lernen musste). Wenn man für Ergebnisse grösser 9 auch noch einen Übertrag will, gibt das eine zweite Einrichtung mit 100 Kombinationen. Will man mehrziffrige Zahlen Addieren hat man wegen dem Übertrag von der vorherigen Stelle, der 0 oder 1 sein kann, ab der zweiten Ziffer sogar 10*10*2 = 200 Möglichkeiten mal 2 Einrichtungen. Multiplikation ergibt mit 0..9 Übertrag sogar 10*10*10 = 1000 Kombinationen mal 2 Einrichtungen. Das machte Dezimalcomputer aufwendig und langsam.

Der Computer wurde erst realistisch möglich, als man auf binäre Zahlen umstellte, also Zahlen mit nur 2 Ziffern. Das vereinfachte die Rechner gewaltig (nur noch 2 Einrichtungen mit je 2*2*2 = 8 Kombinationen, sowohl für Addition wie auch für Multiplikation). Also 200/8=25 mal Weniger Auswand pro Ziffer, zum Preis von ln(10)/ln(2)=3.322 mal mehr Ziffern. Die einzelnen Ziffern von binären Zahlen werden Bits (Binariy digITS) genannt. Die ganzen hier behandelten Chips, und auch alle anderen Mikroprozessoren sind binäre Rechner.

      Beispiel binäres Rechnen mit vorzeichenlosen 8bit Zahlen
      (Bereich von 0..2^n-1, für n = 8: 0..2^8-1 = 0..255)

        C 7654 3210   Nummer n des Bits, n = 7..0

        2 1           Wert des Bits, für n = 7..0:, 2^n = 128..1
        5 2631
        6 8426 8421
        - ---- ----
          0100 1100   Operand1   | =     64      +8+4     =  76
      +   0110 0111   Operand2   | =     64+32     +4+2+1 = 103
      (   .  . .      Übertrag)  |
        - ---- ----              |                          ---
        0 1011 0011   Ergebnis   | = 128   +32+16    +2+1 = 179
    

Weil Binärzahlen sehr schnell sehr lang werden, fasst man oft 4 Bits zusammen, zu Hexadezimalen Zahlen, nach folgender Methode:

         0000 -> 0   0001 -> 1   0010 -> 2   0011 -> 3
         0100 -> 4   0101 -> 5   0110 -> 6   0111 -> 7
         1000 -> 8   1001 -> 9   1010 -> A   1011 -> B
         1100 -> C   1101 -> D   1110 -> E   1111 -> F

         0000 0000 -> 00   0000 1111 -> 0F
         0001 0000 -> 10   0001 1111 -> 1F
         0010 0000 -> 20   ...
         ...               1111 1111 -> FF
    

Etwas das im Vortrag vor 2 Jahren nicht erwähnt wurde (aber inzwischen im Vortragstext nachgetragen wurde) ist, dass man alternativ nach einer älteren Methode 3 Bits zusammenfassen kann, mit nur den Ziffern 0..7, und damit ohne Ziffern A..F. Das ergibt dann Oktalen Zahlen:

         000 -> 0   001 -> 1   010 -> 2   011 -> 3
         100 -> 4   101 -> 5   110 -> 6   111 -> 7

         000 000 -> 00   000 111 -> 07
         001 000 -> 10   ...
         ...             111 111 -> 77

         00 000 000 -> 000   01 000 111 -> 107
         10 111 000 -> 270   11 111 111 -> 377
    

Dies ist aber etwas weniger effizient, es reduziert die Stellen nur um 3 statt um 4. Es passt aber sehr gut zu den Intel 8008/8080/8085 8bit und 8086/8088 16bit Chips, deren Opcodes in 3er Gruppen desingt sind. Früher hatten oktal designte Rechner oft durch 3 teilbare Wortbreiten. Demensprechend kamen hexadezimal designte Rechner auf, nachdem auf 2^n Bits Wortbreite standardisiert wurde, die nicht durch 3 teilbar sind. Intel ist da in einer Zwischenphase von bereits 2^n Bit aber immer noch oktal stehengeblieben.

Vorzeichen und damit negative Zahlen könnte man damit machen, indem man eine Zahl und ein separates Vorzeichen Bit verwendet. Aber dies ist zum Rechnen aufwendig (abhängig von Vorzeichen Addition durch Subtraktion ersetzen).

Vorzeichen und damit negative Zahlen kann man einfacher damit machen, indem man den Zahlenbereich verschiebt: Zählt man 0,1,2,...255,0,... überlauft unsere 8bit Zahl von 255 auf 0, weil der Computer nur eine endliche Menge Zahlen fassen kann. Wenn man nun von 0 einen Schritt zurückgeht, was -1 geben sollte, landet man auf 255. Man definiert nun einfach, dass 255 = -1 ist, 254 = -2, und so weiter. Damit kollidieren die 0..n positiven und -1..-n negativen Zahlen irgendwo in der Mitte. Man spaltet dabei der ganzen Zahlenbereich von 0..(2^n)-1 (hier bei 8bit 0..255) in 2 Hälften, von denen eine zum negativen Bereich -2^(n-1)..(2^(n-1))-1 (hier -128..127) uminterpretiert wird, und der verbleibende Rest von n-1 Bits (hier 7bit) als positiven Bereich 0..(2^(n-1))-1 (hier 0..127) erhalten bleibt, womit auch noch das linke Bit zum Vorzeichen wird, mit 0=positiv und 1=negativ:

       Binär         Dezimal ohne / mit Vorzeichen
       .... ....
       0000 0001                1    +1
       0000 0000                0     0      A 255 -> 0
       1111 1111              255    -1      | = Überlauf ohne Vorzeichen
       1111 1110              254    -2          nicht 256, da nur 8bit Bereich
       .... ....
       1000 0001              129  -127
       1000 0000              128  -128      A +127 -> -128
       0111 1111              127  +127      | = Überlauf mit Vorzeichen
       0111 1110              126  +126          nicht 128, da nur 7bit Bereich
       .... ....
       0000 0010                2    +2
       0000 0001                1    +1
       0000 0000                0    +0      | 0 -> 255
       1111 1111              255    -1      V = Unterlauf ohn Vorzeichen
       .... ....                                 nicht -1, da voll 8bit Bereich
       ^----------- Vorzeichen falls eines definiert wird, 0 = +, 1 = -
    

Dieses Verfahren wird 2er Komplement genannt. Weil die Null damit eine positive Zahl an Platz verbraucht, hat es immer eine negative Zahl mehr, hier die -128.

Operationen: Recheneinheit, Akkumulator und Flags

Ein Rechner ist nutzlos ohne die Fähigkeit zu rechnen. Also schauen wir zuerst mal an, wie gerechnet wird. Wie wir vor 2 Jahren gezeigt haben, kann man mit kombinatorischer Logik die Addition V1+V2 implementieren, und mit V1+(-V2) auch die Subtraktion. Dabei wird pro Bit von V1 bzw V2 ein Resultatbit erzeugt, sowie ein Übertrag der in die Rechnung des nächsthöheren Bits einfliesst. Ebenso wurde dort gezeigt wie so eine Schaltung erweitert wurde zur Arithmetisch Logischen Einheit (ALU), einer universellen Rechneinheit die 32 verschiedene Funktionen (16 arithmetische und 16 logische) beherrscht. Wie zu erwarten haben die hier besprochenen Prozessoren eine derartige ALU Schaltung darin, 8bit breit weil es 8bit Prozessoren sind. Diese ist aber derart verdrahtet, dass nur die 5 Funktionen Addition, Subtraktion, AND, OR und XOR davon nutzbar sind. Man beachte dass es keine Multiplikation oder Division gibt. Zum historischen Vergleich bieten beschreibe ich jeweils in Kursivschrift die Situation einiger anderer Prozessoren. Diesen Teil kann man stets ignorieren ohne Einbussen im Haupttext verstehen. Diese identischen 5 Operationen sind (natürlich 16bit breit) auch in den 8086/8088) drin, wie es sich für Nachfahren gehört (mit zusätzlich Multiplikation und Division). Dagegen haben die 4004/4040 keine ALU, sondern nur einen 4bit Addierer mit Subtraktion. Deren Nachfahre 8048 kann sogar nur Addition (keine Subtraktion) aber wenigstens auch noch AND/OR/XOR. Dessen Nachfahre 8051 kann wiederum alle fünf (und auch Multiplikation und Division). Die 6502 6800/6802 6809 und 68000 haben auch alle fünf, 6809 auch Multiplikation, 68000 auch Multiplikation und Division.

Eine Recheneinheit ohne Daten ist natürlich sinnlos. Dazu brauchen wir zum Variablen speichern Register, wie sie ebenfalls im Vortrag vor 2 Jahren erklärt wurden. Normal würde man für eine Rechnung V3 = V1 OP V2 mit 3 Variablen V1 bis V3 bei jeder Operation 3 Operanden auswählen wollen. Aber solche Dreiadress Rechner sind ineffizient, weil man eben in jedem Befehl 3 Adressen für V3 V1 und V2 auswählen und mitliefern muss, und das kostet Speicherplatz und noch wichtiger Transferzeit, sowie noch Adressauswahllogik. Daher verwendet man oft eine mehrstufige Vorgehensweise, ein Einadress Rechner, mit pro Operation nur einen ausgewählten Operanden: Temp = V1; Temp = Temp + V2; V3 = Temp (oder das mittlere als Temp += V2 in C Schreibweise). Logisch versucht man, solange wie möglich die Rechnung nur in Temp zu halten, so wird für V5 = V1 + V2 + V3 + V4 nur 1 mal Temp = V1 und V5 = Temp benötigt, und dazwischen 3 mal Temp = Temp + V2 bzw + V3 bzw + V4 benutzt. Dieses temporäre Register wird Akkumulator (= Aufsammler) genannt, und mit A benamst. Dies machen auch die 4004/4040 sowie 8048 und 8051 so (und ebenfalls die meisten 1950er Grossrechner, 1960er Minirechner, 1970er Mikroprozessoren (inkl 6502) und 1980er Mikrocontroller). Hingegen verwenden die 8086/8088 eine halbwegs dazwischen liegende Vorgehensweise als Zweiadress Rechner mit V3 = V1; V3 = V3 + V2 (oder V3 += V2 in C Schreibweise) ohne Akkumulator (wie auch die meisten 1960er Grossrechner, 1970er Minis, 1980er und 2000er Mikros und 1990er Controller), welches heute als die effizienteste Methode gilt.

Neben dem A Register werden bei jeder Operation auch ein paar weitere einzelne Bits abgespeichert, namens Flags, die in einem Register F (Kürzel für Flags) gesammelt werden. Diese geben Auskunft über Eigenschaften des Resultates der Operation. Eines davon ist das C (Carry/Übertrag) Flag, das als "neuntes Bit" den Übertrag von den achten Bits aufbewahrt, für Kettenrechnungen von n*8bit. Ein weiteres ist das S (Sign/Vorzeichen) Flag, das eine Kopie des achten Resultatbits bekommt, das ja das Vorzeichen ist, zum Vergleiche if(V1%lt;V2) machen (V1-V2 rechnen ergibt negativ). Ebenso wird ein Test gemacht, ob das Resultat gleich Null ist, und bei 0 das Z (Zero/Null) Flag auf 1 gesetzt, zum Vergleiche if(V1=V2) machen (V1-V2 rechnen ergibt 0). Schliesslich wird noch die Anzahl 1-Bits im Resultat gezählt und bei gerade das P (Parity/Parität) Flag auf 1 gesetzt. Alle 4 Flags kann man später als Kriterien benutzen zum Entscheidungen machen. Was dem 8080/8085 fehlt ist ein O (Overflow/Überlauf) Flag, in dem der siebente Übertrag gespeichert wird, das Vorzeichenwechsel anzeigt. Der Z80 (und U880) benutzt seinen P Flag entweder als P (nach einer AND/OR/XOR Operation) oder als O (nach einer Addition/Subtraktion/Vergleich Operation), seine einzige kleine absichtliche Inkompatibilität zum 8080/8085. Auch hier zeigen die 8086/8088 als Nachfahren das identische Verfahren, ausser das sie ein fünftes separates O (Overflow) Flag haben. Die 4004/4040 sowie 8048 und 8051 haben alle nur ein Carry Flag, sie testen A direkt auf gleich 0, und haben gar keine Vorzeichen oder Parity oder Overflow Tests. Der 6502 hat kein P, aber alle anderen 3 und ein O, ebenso auch die 6800/6802 6809 und 68000. Das P ist eine 100%ige Intel Spezialität

Die ALU ist derart gebaut, dass sie, neben dem &Uuuml;bertrag aus den achten Bits generieren, auch für das erste Resultatbit einen "nullten" &Uuuml;bertrag vom Carry Flag her berücksichtigen kann. Damit kann man mehrstufige Additionen und Subtraktionen als Kettenrechnungen von längeren Zahlen (n*8bit) rechnen, indem das achte Carry von Bits 0..7 via Carry Flag zum nullten Carry von Bits 8..15 wird. Dazu gibt es von den Additions und Subtraktions Befehlen 2 Varianten, eine ohne und eine mit Einbezug des Carry Flags. Insgesammt wird dann FC,A = A + V2 + FC bzw FC,A - V2 - FC gerechnet. Die 8086/8088 machen das auch so. Die 4004/4040 kennen ausschliesslich Rechnen mit Carry, also muss man vor jeder ersten Operation einer Serie das Carry gezielt auf 0 setzen, was als zusätzlichen Aufwand und Fehlerquelle nervt. Der 6502 macht das übrigens auch so. Der 8048 kennt seine nur-Addition ohne und mit, der 8051 kann Addition ohne und mit, Subtraktion aber nur mit. Die 6800/6802 6809 und 68000 können wiederum alles.

Als letztes ist noch eine Besonderheit zu erwähnen: Es gibt neben der normalen Subtraktion noch eine Variante, bei der nur die Flags gesetzt werden, aber der Akkumulator unverändert bleibt. Dieser wird Compare (Vergleich) genannt, was auf seinen Zweck hindeutet. Macht man nämlich A = V1; A ?- V2 zeigt das Z Flag genau dann "gleich Null" wenn V1 gleich V2 war (und V1 - V2 damit 0 ergab), und das S Flag zeigt genau dann negativ wenn V1 kleiner als V2 war (und V1 - V2 damit negativ ergab). Damit hat man die Basis zum Entscheidungen mit kleiner/kleiner-gleich/gleich/ungleich/grösser-gleich/grösser zu machen. Die 8086/8088 machen das auch so. Ebenso der 6502, sowie 6800/6802 6809 und 68000. Die 4004/4040 sowie 8048 und 8051 kennen dies aber nicht, man muss normal subtrahieren und verliert den Inhalt von A, was man mangels Z Flag und mit nur A=0 Test ohnehin tun muss, ebenso fehlt der Vorzeichen Test, dazu muss man mit AND alle Bits in A ausser dem oberen löschen, und dann nochmals A=0 testen (oder A zu sich selber addieren und auf C Flag testen). Man sieht wie freundlich und komfortabel die 8008 8080/8085 und Z80/U880 im Vergleich dazu sind.

Am Schluss hat man im 8008 8080/8085 und Z80/U880 insgesammt 8 Operation/Flag/Akkumulator Kombinationen:

      Nummer        Kürzel               Operation            Name
      Binär  Oktal  Intel  Intel  Zilog
                    alt    neu
      000    0      AD     ADD    ADD    F,A = A +   V2       Add
      001    1      AC     ADC    ADC    F,A = A +   V2 + FC  Add with Carry
      010    2      SU     SUB    SUB    F,A = A -   V2       Subtract
      011    3      SB     SBB    SBC    F,A = A -   V2 - FC  Sub Borrow/Carry
      100    4      ND     ANA    AND    F,A = A AND V2       AND
      101    5      XR     XRA    XOR    F,A = A XOR V2       Exclusive OR
      110    6      OR     ORA    OR     F,A = A OR  V2       (Inclusive) OR
      111    7      CP     CMP    CP     F   = A -   V2       Compare
    
Die Nummer wird überall benutzt wo in einem Befehl eine arithmetische Operation spezifiziert werden muss. Wie man sieht weichen die Namen einiges von einander ab, u.a. Borrow (Intel) vs Carry (Zilog). Worauf es aber ankommt ist was gemacht wird, und das wird von den Nummern ausgelöst. Namen sind auch hier, wie es so schön heisst, nur Schall und Rauch.

Variablen: Registerbank und Speicher

Ein Rechner ist auch nutzlos ohne die Fähigkeit Daten zu haben zum damit rechnen. Bisher haben wir neben den beiden Registern A und F einfach angenommen, dass die Variablen V1 bis V5 "irgendwo" sind. Tatsächlich gibt es 2 Orte dafür, einerseits ein kleiner Satz an Registern im Prozessor, und anderseits den vielfach grösseren Speicher. Die Register haben den Vorteil, einerseits nur kurze Adressen zu benötigen (was neben nur 1 Adresse pro Befehl weiteren Programm Speicherplatz und Transferzeit spart), und anderseits weil sie im Prozessor drin sind die Daten schneller zu liefern (was weiter Zeit spart), und weiterhin den Speicher für Programm holen frei lassen (was Wartezeit spart). In diese Register passt aber nicht viel hinein, also muss man irgendwann einmal auf den Speicher ausweichen. Logisch versucht man, solange wie möglich Variablen in den Registern zu nutzen, das ist typisch für den Intel Prozessor Programmierstil.

Es hat 6 derartige Register, namens B C D E H und L. Wie es die Namen suggerieren sind B bis E schlicht 4 normale Allzweckregister, während H und L zwar auch all dies können, aber zusätzlich speziell sind, und daher nicht F und G heissen. Das ist eine ziemlich geringe Anzahl Register, aber diese kleine Zahl reicht erstaunlich weit. Bei den 4004/4040Es sind 16 4bit Register R0 bis R15, bzw bei den 8048 und 8051 8 8bit Register R0 bis R7, bzw bei den 8086/8088 7 16bit Register AX BX CX DX SI DI und BP, von denen erstere 4 in je 2*8bit unterteilbar sind, als AH+AL=AX, BH+BL=BX, CH+CL=CX und DH+DL=DX. Der 6502 hat neben dem A nur noch 2 8bit Register namens X und Y, die notabene nur halbe Adressen halten können, nicht Daten. Die 6800/6802 haben sogar neben A nur 1 16bit Register namens X, das eine ganze Adresse halten kann. Der 6809 kann hat neben A einen zweiten vollwertigen Akkumulator B sowie 3 16bit generische Register X Y und U. Man sieht hier grosse Differenzen im Motorola zum Datapoint/Intel Design.

Ein Grund für die eigenartige Zahl "6" ist, dass man auch den Akkumulator als zweiten Operanden namens A wählen kann. Also wie in A = A + A (gibt A = 2 * A), oder A = A - A (gibt A = 0). Aber der Hauptgrund liegt bei der Art und Weise wie das Kopieren A = V1 und V3 = A ausgeführt wird: Dazu gibt es nur einen universellen V3 = V1 Befehl (der einzige Zweiadress Befehl!), der für V3 oder V1 eben neben B bis E und H L auch A haben kann. Die 4004/4040 sowie 8048 und 8051 kennen dagegen nur 2 spezifische A = V1 und V3 = A Befehle (reine Einadress), keine V3 = V1, und haben folglich konsequent 8 Register. Die 8086/8088 kennen V3 = V1, aber das ist Teil ihres generellen Zweiadress Verfahrens, das neben V3 = V1 auch V3 = V3 + V1 kann.

Auch 6 + 1 gibt immer noch die komische Zahl 7. Die fehlende achte Kombination ist der Schlüssel zum auf den Speicher zugreifen. Wird dies verwendet, wird der Inhalt der beiden Register H und L als obere und untere Hälften einer 2*8=16bit grossen Speicheradresse benutzt, und die damit adressierte (= ausgewählte) Speicherstelle als Ort für den zweiten Operanden benutzt. Man kann den Speicher als ein einzelnes Array anschauen, dessen Index von H*256+L geliefert wird, also als Speicher[H*256+L] (bzw in Kurzschreibweise Speicher[HL]). Daher auch die komischen Registernamen, H (High) und L (Low) statt F und G. Jede Speichervariable bekommt eine Adresse, als Speicher[Var]. Etwas das niemals in Register passt sind Arrays, die werden in einem zusammenhängenden Block Speicher abgelegt, als Speicher[Array] bis Speicher[Array + (Elemente-1) * Elementgrösse]. H und L sind folglich auch der allererste Ort zum eine Adressvariable hinstellen. Adressvariablen (bzw Zeigervariablen, auch als Pointer bekannt) werden ohnehin soweit möglich in Registern gehalten, weil sie dort gut rein passen, und deren Vorteile maximal ausnutzen können. Bei den 4004/4040 kann der Speicher nur mit von den Registern separaten Befehlen benutzt werden, dafür mit beliebigen R0+R1 oder R2+R3, .. R14+R15 Registerpaaren. Die 8048 macht das auch separat, aber mit nur R0 oder R1. 8051 kann mit R0 oder R1 oder mit einer 8bit Konstante. Die 8086/8088 schliesslich erlauben ein komplexes Verfahren von Speicherzugriffen mit 24 Kombinationen von verschiedenen Konstanten und ev mehreren Registern, zusätzlich zu den 8 Registern gibt das insgesammt 32 Nummern.

Wegen der 2*8=16bit Speicheradresse von H und L ist das Speicher[HL] Array auch auf max 64k Elemente begrenzt, mit Adressen 0 bis (2^16)-1, ausser beim 8008, der sogar nur 6+8=14bit und max 16k erlaubt wegen 2 fehlenden Leitungen im Chip. Die 4004/4040 erlauben mit 4+4=8bit Speicheradressen sogar nur 256*4bit Speicher für Variablen, was für einen Tischrechner auch ausreichte. Die 8048 und 8051 haben diese Grösse (eher eine Kleine) mit 1-Register 8bit Adressen geerbt, also nur 256 Bytes an Platz für Variablen, was beim 8048 (für kleine Steuerungen) auch angebracht war, aber beim 8051 der grobe Designfehler schlechthin ist. Die 8086/8088 können auch direkt nur 1 16bit Register benutzen, was sie auf 64k limitiert (ergibt die berühmte bzw berüchtigte Grösse eines 64k Segmentes), die vollen 1024k/1M sind nur mit der Speicherverwaltung als Adresserweiterung zu machen, als 16bit Segmentregister * 16 + 16bit Adresse. Man könnte auch einen 8080/8085 oder Z80/U880 oder 6800/6802/6809 oder 6502 mit identischer Speicherverwatung auf 1M Speicher aufbohren, oder gar mit * 256 alle inklusive 8086/8088 auf 16384k/16M aufrüsten.

Am Schluss hat man im 8008 8080/8085 und Z80/U880 also insgesammt 8 Register/Akku/Speicher Kombinationen:

      Nummer 8008   Nummer 8080ff  Kürzel/Name   Variable
      Binär  Oktal  Binär  Oktal   Intel  Zilog
      001    1      000    0       B      B      Register B
      010    2      001    1       C      C      Register C
      011    3      010    2       D      D      Register D
      100    4      011    3       E      E      Register E
      101    5      100    4       H      H      Register H
      110    6      101    5       L      L      Register L
      111    7      110    6       M      (HL)   Speicher[HL]
      000    0      111    7       A      A      Akkumulator A
    
Man sieht hier, wie die Nummern vom 8008 zum 8080 reorganisiert wurden, damit die jeweils beiden Register der HL (und BC bzw DE) Registerpaare sich nur im hintersten Bit unterscheiden.

Rechnen: Operationen, Operanden und Befehlscodes

Um mit einem Computer zu Rechnen braucht man ein Program. Dieses ist eigentlich nichts anderes als eine in Zahlen (und damit in Bits) codierte Liste von Befehlen. Diese bestehen aus je einer Operation und den dazu benutzten Operanden (Registernummern und/oder Speicherzellenadressen oder Konstanten). Diese Summe aller möglichen Codes ergibt den Maschinencode des jeweiligen Prozessors. Die sind durch die Verdrahtung im Prozessor gegeben, und sind bei jeder Prozessorfamilie anders. Maschinencode Programme für eine Familie laufen daher nicht auf anderen Familien, man muss sie dazu umschreiben, oder in einem Emulator laufen lassen. Codiert werden die Befehle bei sämtlichen hier behandelten Prozessoren als 8bit Codes. Dies gilt auch für die 8048 und 8051, aber auch für die 4bit 4004/4040, wie auch für die 16bit 8086/8088 (dort aber zumeist als 2er Päärchen von 8bit Codes). Das ist Intel-Design typisch. Es ist aber auch beim 6502 so, sowie den 6800/6802 und dem 6809. Es ist aber nicht beim 68000 der immer 16bit Codes hat). Eigentlich sind 8bit Codes 1970er typisch, selbst die DEC VAX Minicomputer oder teils Xerox Alto Workstations haben das. Auch Java Bytecode ist 8bit.

Zum einen einzelnen Arithmetikbefehl der Sorte A = A OP V2 rechnen zu lassen, wird ein Befehlscode der Form 10'ooo'sss benötigt, bei dem das 10 ein fester Codeteil ist, der eben Arithmetik auslöst, die ooo (= Operation) eine der Varianten 000..111 aus obiger Operationentabelle (Add/AddCarry/Sub/SubCarry/AND/XOR/OR/Vergleich) auswählt, und die sss (= Source) eine der Varianten 000..111 aus obiger Operandentabelle (RegB/RegC/RegD/RegE/RegH/RegL/SpeicherM/AkkuA) für V2 auswählt. Damit können 64 Kombinationen ausgelöst werden, die zusammen 64/256 = 1/4 der mit 8bit möglichen 256 Codes verbrauchen.

Wie wir gesehen haben muss man für eine beliebige Rechnung V3 = V1 OP V2 zuerst A = V1 setzen, und am Schluss das wieder V3 = A. Dazu gibt es den Kopierbefehl der Sorte V3 = V1. Dieser benötigt ein Code der Art 01'ddd'sss (bei 8080 und folgenden, beim 8008 war dies 11'ddd'sss), bei dem wieder das 01 (bzw 11) ein fester Codeteil ist, der eben Kopieren auslöst, und die ddd (= Destination) bzw sss (= Source) je eine der Varianten 000..111 aus obiger Operanden Tabelle (B/C/D/E/H/L/M/A) auswählt, wobei ddd das V3 und sss das V1 vorgibt. Das sind ebenfalls 64 Kombinationen, und damit ein weiteres 1/4 der möglichen Codes. Die Rechnung E = B + C wird dann also zerlegt zu 3 Befehlen, welche dann als 3 Codes repräsentiert werden:

      Codes              Operationen  Befehle
      Binär       Oktal  
      01'111'000  170    ='A'B        A = B
      10'000'001  201    OP'+'C       A = A + C
      01'011'111  137    ='E'A        E = A
    

Man beachte noch, dass die ohnehin sinnlose Kombination M = M, Code 01'110'110 (bei 8080 und folgenden, beim 8008 war dies 11'111'111) nicht ausgeführt wird. Statt dessen wird der Prozessor angehalten! Das war zu 8008 Zeiten der Zahlenwert mit alles-1er, der beim Lesen von unbeschriebenem (oder gar nicht vorhandenen!) Speicher zurückkommt, und ein guter Hinweis ist, dass man nicht mehr im beabsichtigten Programm drin ist. Das war eine nette Debugging Hilfe. Warum beim 8080 nicht 110 für A und 111 für M gewählt wurde, und die 11 für Kopieren nicht erhalten blieben, wird wohl ein Rätsel bleiben.

Spätestens hier sieht man auch warum man beim 2-3-3 bit Codeaufbau der 8008 8080/8085 und Z80/U880 die 3bit Oktalzahlen benutzen will. Dies ist bei den 8086/8088 ihren Codes auch so. im Gegensatz zu den 4004/4040 sowie 8048 und 8051 ihren Codes, die eine 4-4 bit Struktur haben, und daher besser in den 4bit Hexzahlen dargestellt werden.

Aber wie kommen überhaupt einmal Daten in die Register? Dazu braucht man Konstanten. Dazu gibt es für alle 8 Kopierziele und 8 Arithmetik Operationen spezielle Code Varianten, die anstelle von einen der acht sss eine neunte spezielle Form haben, zum eine 8bit Konstante benutzen, die als weiteren 8bit Code im Programm steht. Natürlich passen 9 Varianten nicht in 3 Bits, also sind die beiden vorderen Bits anderst. Für Kopieren ist dies 00'ddd'110, für Arithmetik 11'ooo'110 (8080 und folgende, beim 8008 00'ooo'100), mit ddd und ooo identisch wie oben. Die sss fehlen, weil eben kein Register oder Speicher benutzt wird, sondern die Konstante (strikte sind sie identisch mit Speicher[HL]=110 weil die Konstante aus dem Programmspeicher geladen wird). Die Rechnung D = E - 23 wird also auch zu 3 Befehlen zerlegt, welche dann aber als 4 Codes repräsentiert werden:

      Codes                            Operationen    Befehle
      Binär                 Oktal+Hex  Dezimal        Dezimal
      01'111'011            173        ='A'E          A = E
      11'010'110 0001'0111  326 17     OP'+'Konst 23  A = A - 23
      01'010'111            127        ='D'A          D = A
    

Die 0001'0111 sind die Konstante 23, was in Hex 17 ergibt, hier absichtlich in Hex statt Oktal geschrieben zum die Differenz Code/Konstante sichtbarer zu machen.

Mit Konstanten kann man auch auf beliebige Speicherstellen Zugreifen. Die Rechnung Speicher[4200] = 69 - D wird zu 5 Befehlen zerlegt, welche als 8 Codes repräsentiert werden:

      Codes                            Operationen    Befehle
      Binär                 Oktal+Hex  Dezimal        Dezimal
      00'111'110 0100'0101  076 45     ='A'Konst 69   A = 69
      10'010'010            222        OP'-'D         A = A - D
      00'101'110 1010'1000  056 A8     ='L'Konst 168  L = untere-8bit-von-4200
      00'100'110 0001'0000  046 10     ='H'Konst 16   H = obere-8bit-von-4200
      01'110'111            167        ='Speicher'A   Speicher[HL] = A
    

Man merke, dass ich L= vor H= gesetzt habe, das ist die gebräuchliche Reihenfolge. Wenn man die 16bit Konstante 4200 (Binär 0001'0000'1010'1000 = Hex 10A8, bzw Binär 0'001'000'010'101'000 = Oktal 010250) in 2 mal 8bit (Binär 0001'0000 1010'1000 = Hex 10 AB, bzw Binär 00'010'000 10'101'000 = Oktal 020 250) aufteilt, sieht man warum man bei Adressen (und damit auch andere Konstanten) Hexzahlen benutzen will, damit die Bytegrenze zwischen die (Hex-)Ziffern fällt (Hex 10AB teilt zu 10 AB), statt in eine (Oktal-)Ziffer hinein (Oktal 010250 teilt zu 020 250). Dies ist so weil 8=2*4 aber 8=2.667*3. Und daraus folgt, dass man für Konsistenz auch bei Befehlscodes statt eine Oktal passende 2-3-3 bit Struktur lieber eine Hex passende 1-3-1-3 bit wünschen würde, damit alles in Hexzahlen geht. Es gibt aber noch schlimmeres als 2-3-3 bit, wie z.B. die 3-3-2 bit Codierung beim 6502.

Die Rechnung V3 = V3 +oder- 1 wird so oft benutzt, dass man ihr spezielle Namen gibt: Increment für die +1 Addition, und Decrement für die -1 Subtraktion. Kein Wunder, dass es dafür anstelle der 3 Befehle und 4 Codes für A = V3; A = A +oder- 1; V3 = A eine Kurzform von nur 1 Befehl und 1 Code gibt, 00'ddd'10o (8080 und folgende, beim 8008 00'ddd'00o), bei dem ddd wieder als Variante das Ziel (und zugleich die Quelle) V3 angibt, und o als Variante die Operation (0 für +1 und 1 für -1) vorgibt. B = B - 1 wird also zu 00'000'101. Man beachte, dass beim 8008 ddd 000 (A) und 111 (M) ebenfalls den Prozessor anhielten. A = A + 1 und A = A - 1 wären Codes 00'000'000 und 00'000'001, und damit ersterer auch ein möglicher "kein Speicher" Hinweis. Ab dem 8080 gibt es diese Einschränkungen nicht mehr.

Wie wir gesehen haben, gibt es keine Multiplikation oder Division. Man kann sich nur behelfen, indem man diese in mehrere Additionen bzw Subtraktionen zerlegt. Dazu kann man zum Multiplizieren *2 *4 *8 etc rechnen indem man eine Zahl wiederholt zu sich selber addiert, da A+A = 2*A). Für beliebige Zahlen addiert man dann mehrere von diesen, wie z.B. *10 wird dann zu *8 + *2. Diese Serie kann man mit wiederholt Verdoppeln generieren, mit dem normalen Codes. Es gibt aber hierfür auch 4 Spezialcodes der Form 00'0oo'111 (8080 und folgende, beim 8008 00'0oo'010). oo=10 ist ein Duplikat von A = A + A + FC, bei dem die A Bits 0..6 nach Bits 1..7 wandern, Bit 7 ins Flag C wandert, und das alte Flag C in Bit 0. oo=00 dagegen nimmt das alte Bit 7 sowohl ins FC wie auch direkt in Bit 0, ist also A = A + A mit danach FC dazu addieren. Das ganze Zeugs nennt man rotieren nach links, mit 9 bzw 8 Bits. oo=11 und oo=01 dagegen rotieren nach rechts (was mit A - A gar nicht geht!), also Bits 1..7 wandern nach Bits 0..6, Bit 0 ins Flag C, und Bit 7 bekommt Flag C oder eine Kopie von Bit 0, was wieder 9 oder 8 Bits gibt. Diese benutzt man für /2 /4 /8 etc, zum Divisionen aufbauen, aber auch für hinterstes Bit abknabbern und testen.

Damit haben wir bereits die ganze 8008 Arithmetik und Kopiererei, so wie sie Datapoint definiert und Intel nachimplementiert hat:

      Codes 8008         Codes 8080ff       K  Art          Operation
      Binär       Oktal  Binär       Oktal  C
      10'ooo'sss  2os    10'ooo'sss  2os    0  Arithmetik   FC,A = A OP V2
      11'ddd'sss  3ds    01'ddd'sss  1ds    0  Kopieren     V3 = V1
      11'111'111  377    01'110'110  166    0  Halt         (wäre M = M)

      00'ooo'100  0d4    11'ooo'110  3d6    1  Arith Konst  FC,A = A OP Konst
      00'ddd'110  0o6    00'ddd'110  0o6    1  Kopie Konst  V3 = Konst

      00'ddd'00o  0do    00'ddd'10o  0do    0  Inc/Dec      V3 = V3 +oder- 1
      00'000'00o  00o    00'111'10o  07o    0  8008 Halt    (8080ff A +oder- 1)
      00'111'00o  07o    00'110'10o  06o    0  8008 nix     (8080ff M +oder- 1)

      00'0oo'010  0o2    00'0oo'111  0o7    0  Rotieren     A = A *oder/ 2

        ooo = Add/AddCarry/Sub/SubCarry/AND/XOR/OR/Vergleich
        ddd und sss 8008 = AkkuA/RegB/RegC/RegD/RegE/RegH/RegL/SpeicherM
        ddd und sss 8080ff = RegB/RegC/RegD/RegE/RegH/RegL/SpeicherM/AkkuA

        o = +1/-1, für increment/decrement

        oo = links8/rechts8/links9/rechts9, für rotieren

        K C = Anzahl Konstanten Codes nach dem Befehlscode
    

Rechnen: 8080/8085 Erweiterungen

Im 8080 (und folgenden) addierte(n) Intel (und Zilog) im Alleingang hier einiges mehr, oft wegen Mangel an noch freien (oder freigeschaufelten) Codes nur in Varianten für einzelne oder wenige Register.

Da multipizieren sehr langsam ist, will man bei Arrays nicht in jedem Schleifendurchlauf Arrayindizes in Adressen umrechnen lassen (braucht Adresse des ersten Array Elementes + Index * Grösse eines Arrayelementes und damit langsame Multiplikation). Daher nimmt man als alternative Methode eine Addressvariable, in die man anfangs die Adresse des ersten Arrayelementes lädt (die Multiplikation mit Index Null fällt weg), und dann bei jedem Schleifendurchlauf nur die Grösse eines Arrayelementes dazu addiert, bzw bei auf jedem Byte des Arrayelementes zugreifen 1 dazu addiert. Das Verfahren nennt man Strength Reduction, weil es die "starke" Multiplikation durch die "schwache" Addition ersetzt (und erst noch die sehr schwache +1, also den Increment). Noch besser wird ist, weil zwischen Adresse zu Speicher schicken und Daten transferieren die ALU unbenutzt ist, und man die Adresse daher gleich nebenher incrementieren kann, was als Autoincrement bekannt ist, womit dies Null Zeit braucht. So schnell wie gar nicht vorhanden wäre selbst die beste und teuerste Multiplikation nicht.

Wenn man zwischen mehreren Arrays am kopieren oder kombinieren ist, muss man aber für jede von ihnen eine eigene Adressvariable führen, statt nur einen Index für alle. Und letzteren braucht man auch weiterhin, als Zähler zum die Schleife abbrechen. Das ganze kostet also mehr Register. Dass man auf den Speicher aber nur mit der Adresse in H und L zugreifen kann ist daher sehr limitierend, man hat quasi nur eine Adressvariable. Daher wird diese Beschränkung gleich auf mehrere Arten umgangen, die alle verschiedene Limiten, und damit Vor- und Nachteile, haben:

Aber auch das berechnen von Adressen ist etwas wo sich die 8bit Eigenschaft des Prozessors als limitierend erwies. Und da man dies häufig macht, bei all den Arrayvariablen an den Anfang setzen und später inkrementieren, gibt es speziell zum diese berechnen ein paar 16bit Arithmetik und Kopier Befehle:

Neben vielen Erleichterungen und Beschleunigungen beim Adressen bearbeiten fallen die verbleibenden paar kleinen Erweiterungen der Arithmetik fast nicht mehr auf:

Insgesammt hat der also der 8080 an erweiterter Arithmetik und Kopiererei folgende Befehlscodes addiert:

      8008   Codes 8080ff       K  Art           Operation
             Binär       Oktal  C
      -nix-  11'101'011  353    0  Austausch     DE und HL

      -nix-  00'0ao'010  0o3    0  Kopieren      A von/zu Speicher[BC/DE]
      -nix-  00'10o'010  0o3    2  Kopieren 16b  HL von/zu Speicher[16bKonst]
      -nix-  00'11o'010  0o3    2  Kopieren      A von/zu Speicher[16bKonst]

      -nix-  00'dd0'001  0d1    2  Laden 16b     Paar = 16bit Konst
      -nix-  00'ddo'011  0d3    0  Inc/Dec 16b   Paar = Paar +oder- 1
      -nix-  00'ss1'001  0s1    0  Arithmet 16b  HL = HL + Paar

      -nix-  00'100'111  047    0  spezial       BCD, hier nicht erläutert
      -nix-  00'101'111  057    0  Invertieren   A = A XOR 255
      -nix-  00'110'111  067    0  Carry         FC = 1
      -nix-  00'111'111  077    0  Carry         FC = FC XOR 1

        ao = zuSpeicher[BC]/vonSpeicher[BC]/zuSpeicher[DE]/vonSpeicher[DE]
        o = zuSpeicher[Konst]/vonSpeicher[Konst]

        ss oder dd = BE/DE/HL/-nichtAM-, für Paare

        o = +1/-1, für increment/decrement

        K C = Anzahl Konstanten Codes nach dem Befehlscode
    

Spätestens wenn man die vielen, zum Teil ziemlich spezialisierten und mit Limiten (die man auswendig kennen muss zum sie benutzen können!) versehenen Erweiterungen ansieht, versteht man warum Intel ihre Prozessordesigns oft als komplexe Befehlssätze (CISC = Complex Instruction Set Computer) bezeichnet werden. Man könnte dies der Tatsache anlasten, dass der 8080 ein überarbeiteter 8008 ist, aber da er nicht 100% kompatibel ist, sticht dies nicht wirklich, zumal Intels kompletes Eigendesign 8086/8088 noch einiges komplexer ist, manche sagen auch ein vielfaches komplexer!

Rechnen: Z80 Erweiterungen

Das Zilog, als Gründung durch ex-Intel Mitarbeiter, diesen Trend zu Erweiterungen beim Z80/U880 nur fortsetzte überrascht nicht. Da diese Erweiterungen viele sind, und für das Verstehen der Arbeitsweise eines Prozessors, oder selbst für einfaches Programmieren (auch eines Z80 oder U880!) nicht nötig sind, werden hier nur ein paar besonders nützliche oder sonstwie auffällige Sachen aufgeführt: Es überrascht ansgesichts von Obigem nicht wirklich, dass im Gegensatz zu den 244 definierten Befehlscodes des 8080 der Z80 über 600 hat, und dass eine vollständige Zusammenfassung des Z80 Befehlssatzes etwas über doppelt so gross ist wie eine des 8080 Befehlssatzes und etwa dreimal so gross ist wie eine des 8008 Befehlssatzes.

Programmablauf, Schleifen und Bedingungen

Nun können wir rechnen, und Daten kopieren und speichern, gesteuert von Befehlscodes. Konstanten können wir als Codes dazwischen ablegen. Diese ganzen Codes liegen neben den Daten auch im Speicher und bilden als Ganzes ein Programm, entweder von einem Programmfile auf Disk geholt, oder von Hand eingegeben, oder aber in einem Speicherchip eingebrannt. Wie alles im Speicher, haben diese Codes Adressen, jeder eine andere, in direkt hinter einander liegender Sequenz.

Anfangs wird immer ein Befehlscode erwartet, aber nachdem einer geholt worden ist, der eine 8bit oder 16bit Konstante benutzt, werden 1 oder 2 weitere Speicherzugriffe (auf die jeweils folgenden Adressen) gemacht zum diese Codes holen, womit dann der nächste Befehlscode dann 2 oder 3 Adressen weiter oben abgeholt wird. Beim Z80 sind mit den Doppel-Präfix Codes sogar bis zu 4 weitere Codes möglich, womit der nächste Befehl 5 Adressen weiter anfängt.

Aber wie kommt der Prozessor dazu, die Programcodes zu finden und holen, um sie danach auswerten zu können? Dazu braucht er ein weiteres sehr spezielles Adressregister namens PC (Program Counter = Programzähler), so benamst weil es einerseits eben als Speicher[PC] das Programm adressiert, und anderseits (solange es nicht gestört wird) nach jedem Gebrauch stur PC = PC + 1 nach oben gezählt wird, und damit die Sequenz der Codes abadressiert und so deren abarbeiten zulässt. Damit macht der PC einen Autoincrement, was vermutlich die Herkunft dessen Gebrauches füe Arrays ist. Der PC ist ein 16bit Register (beim 8008 nur 14bit) welches nicht in 2 8bit Teile teilbar ist. Es wird nur mit speziellen 16bit Befehlen bearbeitet (auch beim 8008, dort einfach nur 14bit davon genutzt, 2 ignoriert). Bei 4004/4040 sowie 8048 ist dies ein 12bit Register, bei 8051 wieder 16bit, und bei 8086/8088 ebenfalls 16bit (ergibt die 64k Codesegmentgrösse, wie bei 64k Datensegmentgrösse).

Nach dem Rechner einschalten, bzw Reset auslösen, wird PC = 0 gestellt. Daher müssen die Rechner eine Speicheranordnung haben, bei dem nach dem Einschalten an Adresse 0 ein ROM/PROM/EPROM/Flash Speicher mit einem Bootloader oder BIOS oder gleich das ganze Program drin liegt, damit der Prozessor sofort etwas zum machen vorgesetzt bekommt, sonst ist der Absturz vorprogrammiert (bzw im Prozessor vorverdrahtet!), weil der Prozessor zufällige im Speicher liegende Zahlen als zusammenhangslose Codes liest und ausführt. Es ist daher üblich, Rechner mit ROM von unten und RAM von oben im Adressraum zu bauen. 4004/4040, sowie 8048 und 8051, sowie 8086/8088 fangen auch alle mit PC = 0 an, aber 8086/8088 haben durch die Speicherverwaltung bedingt am Anfang die Adresse Hex FFFF0 (PC = 0 im Segment (16*)FFFF). 6502 6800/6802 und 6809 laden den PC mit einer Konstante die vom Ende des Speichers geholt wird. Daher ist bei 8086/8088 sowie 6502 6800/6802 und 6809 üblich die Rechner umgekehrt zu bauen, RAM von unten und ROM von oben. Der 68000 nimmt ebenfalls eine Konstante, aber vom Anfang des Speichers, also wieder ROM von unten und RAM von oben.

Falls nichts dazwischen kommt (z.B. Reset), wird der PC dann bis maximal 65'535 (Hex FFFF, Oktal 177777) zählen, um dann wieder bei 0 anzufangen (strikte wäre er bei 65'366, Hex 10000, Oktal 200000, aber das 17te Bit geht verloren, mangels 17ten PC Registerbit zum es festhalten). Das resultiert in einem 65'356 Codes langen "Programm", in dem auch alle Daten im Speicher "ausgeführt" werden, das in einer einzigen endlosen Schleife abläuft, was wohl auch einen Absturz gibt.

Das kann man nur verhindern, indem man dem PC ein vernünftiges Programm vorsetzt, mit kontrollierten Schleifen und Bedingungen. Dies kann man nur dadurch erreichen, indem man das Register PC an den gewünschten Stellen gezielt überschreibt, und somit sein stures Zählen unterbricht, und damit aus der Sequenz ausbricht. So ein PC überschreiben nennt man einen Programmsprung bzw den Befehl dazu einen Sprungbefehl. Man findet solche Befehle in jeder Prozessorfamilie. Sie wirken wie ein "goto", aber mit der neuen Adresse statt einer neuen Zeilennummer oder Labelnamen, und ergeben eine Endlosschleife mit kontrolliertem Ende (dort wo der Befehl steht) und Anfang (do wo der neue PC Inhalt hin adressiert), ohne zufällige ausgeführte Daten (nur gewollte Befehle) drin. Der Absturz ist damit abgewehrt.

Der simpelste Befehl dafür hat den Code 11'000'011 (8080 und folgende, beim 8008 01'xxx'100, mit xxx beliebig, üblich ist xxx=000) und macht schlicht PC = 16bit Konstante, gefolgt von 2*8bit untere und obere Hälfte Codes, wie bei Registerpaaren laden. Wenn wir ein Program haben, mit 300 Codes Einleitung in Adressen 0..299, und 500 Codes Schleife in 300..799, ergibt sich:

      Adresse   Codes                  Operation
      Dez Hex   Binär       Oktal+Hex
        0 0000  xx'xxx'xxx  xxx        reset: erster Befehl, 1 Code
        1 0001  xx'xxx'xxx  xxx          zweiter Befehl, auch 1 Code
        2 0002  xx'xxx'xxx  xxx xx       dritter Befehl, mit 2 Codes
        4 0004  xx'xxx'xxx  xxx          vierter Befehl, mit 1 Code
        5 0005  xx'xxx'xxx  xxx xx xx    fünfter Befehl, mit 3 Codes
        6 0008  ..          ...          sechster ...

      299 0127  xx'xxx'xxx  xxx          letzter Befehl vor Schleife
      300 0128  xx'xxx'xxx  xxx        loop: erster Befehl in Schleife
      301 0129  ..          ...          zweiter ...

      799 031F  xx'xxx'xxx  xxx          letzter Befehl in Schleife
      800 0320  11'000'011  303 28 01  end: goto loop
      803 0323  -- --- ---  ---          von Program unbenutzter Speicher
    
Ab den Z80/U880 gibt es auch eine nur 2 Codes lange Kurzform mit Befehlscode 00'011'000 welche PC = PC + 8bit Konstante macht, welche brauchbar ist bei den oft vorkommenden kleinen Distanzen -128 bis +127. Die 4004/4040 haben nur eine 2 Codes lange PC = Form mit (4+8=)12bit Konstante (die können auch nur 4k Speicher benutzen!). Der 8048 hat sogar nur PC = (3+8=)11Bits in 2 Codes, das 12te Bit muss von einem Flag M genommen werden, für ein PC = (1+3+8=)12bit (der kann auch nur 4k Speicher benutzen). Der 8051 kennt eine ähnliche 2 Codes PC = (5+3+8=)16bit Methode, bei der die 5bit vom PC selber genommen werden, und zusätzlich sowohl 3 Codes PC = 16bit und 2 Codes PC = PC + 8bit Formen. Ebenso haben die 8086/8088 beide PC = 16bit und PC = PC + 8bit Formen. Der 6502 und die 6800/6802 kennen nur die PC = 16bit Form, der 6809 dazu auch die PC = PC + 8bit oder 16bit Form.

Eine einzelne Endlosschleife ist nützlich, und in fast jedem Rechner irgendwo drin, sei das in einem Steuerprogramm als einwn "Abfrage, Auswerten, Aktion, wieder von vorne" Ablauf, oder in einem Betriebssystem drin die endlose Abfrage und Ausführen von Kommandos. Aber für die meisten Schleifen will man, dass sie eine Anzahl/Weile wiederholen und dann gemäss einem Abbruchkriterium verlassen werden. Dazu gibt es bedingte Sprünge, bei denen entweder PC = etwas gesprungen oder trotzdem PC = PC + 1 weitergezählt wird. Daher heissen diese auf manchen Rechnern auch Verzweigungen, weil danach 2 Wege weiterführen. Welchen davon man nimmt ist abhängig von einem Kriterium. Als mögliche Kriterien werden die Zustände der Flags benutzt. Bisher haben wir nur den Carry Flag benutzt zum lange Arithmetik machen, nun bekommen alle 4 Flags einen Nutzen, zum Entscheidungen machen. Man kann einen beliebigen von ihnen auswählen und dann mit 0 oder 1 vergleichen, und dann falls Übereinstimmung ist springen (und damit die Schleife weiter wiederholen), oder weiterzählen (und damit die Schleife abbrechen). Es wird also if(Flag=Wert)goto gemacht, und es entsteht somit eine "loop/do .. while (Flag=Wert)" Schleife.

Der Code für diesen Befehl ist 11'ccc'010, bei dem ccc eine der Flag=Wert Varianten 000..111 aus der Tabelle FZ=0/FZ=1/FC=0/FC=1/FP=0/FP=1/FS=0/FS=1 ist (8080 und folgende, beim 8008 01'ccc'000 mit Tabelle FC=0/FZ=0/FS=0/FP=0/FC=1/FZ=1/FS=1/FP=1). Danach kommt wieder 2 Codes mit einer 2*8bit Adresskonstante für PC = 16bit Konstante. Eine Schleife, die 100 mal ablaufen soll, kann man also machen mit:

      Adresse   Codes                  Operation
      Dez Hex   Binär       Oktal+Hex
      400 0190  00'000'010  002 64       B = 100
      402 0192  xx'xxx'xxx  xxx        loop: erster Befehl in Schleife
      403 0193  ..          ...          zweiter ...

      499 01F3  xx'xxx'xxx  xxx          letzter Befehl in Schleife
      500 01F4  00'000'101  005        while: B = B - 1
      501 01F5  11'000'010  302 92 01    if(FZ=0)goto loop   (falls nicht 0)
      504 01F8  xx'xxx'xxx  xxx          nächster Befehl nach Schleife

        Man merke übrigens, dass nur "loop" als zu merkende Adresse
        auftaucht, niemals das "while". Das gilt bei allen Prozessoren.
    
Wieder ab den Z80/U880 gibt es hier die 2 Codes Kurzform bedingte PC = PC + 8bit Konstante mit Codes 00'1cc'000 bei denen wegen Mangel an noch unbenutzten Codes cc nur für 00..11 aus der kurzen Tabelle FZ=0/FZ=1/FC=0/FC=1 geht, was aber ohnehin die häufig benutzten 4 Fälle sind. Die 4004/4040 haben bei bedingt *nur* 8bit Konstante, aber diese nicht addierend, stattdessen werden die unteren 8 PC Bits ersetzt und die oberen 4 gelassen, will man mehr kombiniert man diese mit einem nachfolgenden normalen Sprung. Der 8048 macht dies auch so, erst der 8051 hat normale PC = PC + 8bit Form. Die 8086/8088 haben auch nur PC = PC + 8bit Form, aber ab den 80186/80188 zusätzlich mit PC = 16bit. Der 6502 und die 6800/6802 haben nur PC = PC + 8bit Form, der 6809 aber wiederum auch PC = 16bit.

Diese Art "Herunterzählschleife" ist so häufig, dass es ab den Z80/U880 ausserdem einen ultrakurz Code 00'010'000 gibt, der schlicht B = B - 1 und falls das nicht 0 ergab gleich PC = PC + 8bit macht, und dabei erst noch keine Flags ändert. Alles in ein "Kombibefehl" von 2 Codes statt 1+2. Auch die ganzen 4004/4040, sowie 8048 und 8051, sowie 8086/8088 haben diesen Kombibefehl, teils mit oder ohne Flags ändern. Er fehlt bei Intel nur in 8008 und 8080/8085. Der 6502 sowie 6800/6802 und 6809 sowie 68000 haben ihn aber alle auch nicht.

Neben der sich von selber ergebenden "loop/do .. while" Schleife (und damit auch wie oben als "loop/do .. n-times" Schleife nutzbar) will man auch andere. Die seltenere "loop/do .. until" Schleife kann man einfach machen, indem man den Test umdreht, indem man das Flag mit dem umgekehrten Wert vergleicht. Obiges "while (B <> 0)" kann man ja auch als "until (B = 0)" anschauen. Das Zero Flag verkehrt herum zu benutzen ist übrigens auch ein "beliebter" Anfängerfehler, zumal FZ=1 ein Ergebnis gleich Null und FZ=0 ein Ergebnis ungleich Null anzeigt!

Bei allen obigen Schleifen wird den Code in der Schleife mindestens ein mal ausgeführt, selbst wenn die Bedingung gar nie zutraf. Das ist für "n mal wiederholen" kein Problem, aber stört wenn man ein "solange Zustand noch nicht (oder immer noch) da ist, korrigiere es" haben will, wo der Zustand vor erreichen von der Schleife bereits erreicht (oder längst weg) sein kann. Muss man dieses "immer einmal durch" Verhalten vermeiden, will man eine "while .. loop" oder "until .. loop" Schleife verwenden. Diese kann man machen, indem man vor dem "loop/do" Schleifenanfang einer normalen "loop/do .. while" oder "loop/do .. until" Schleife einen unbedingten Sprung hinsetzt, der in die Schleife hinein springt, und zwar an die Adresse ab der der "while" oder "until" Test gerechnet wird:

      Adresse   Codes                  Operation
      Dez Hex   Binär       Oktal+Hex
      600 0258  00'000'010  002 65       B = 101   (wenn man 100mal will!)
      602 025A  11'000'011  303 BA 02    goto while
      605 025D  xx'xxx'xxx  xxx        loop: erster Befehl in Schleife
      606 025E  ..          ...          zweiter ...

      699 02B9  xx'xxx'xxx  xxx          letzter Befehl in Schleife
      700 02BA  00'000'100  004        while: B = B - 1
      701 02BB  11'000'010  302 5D 02    if(FZ=0)goto loop   (falls nicht 0)
      704 02BE  xx'xxx'xxx  xxx          nächster Befehl nach Schleife

        Man merke, dass hier neben dem "loop" auch das "while" als zu
        merkende Adresse auftaucht, aber mit dem "while" immer noch nach
        dem "loop". Das gilt auch bei allen Prozessoren.
    

Als letztes gibt es noch die beliebte "for .. next" Schleife. Diese wird als normale "loop/do .. while" Schleife implementiert (ein "while .. loop" ist da selten nötig), nur mit dem Unterschied zu oben, dass man die Zählervariable anfangs auf 0 oder 1 setzt, und vor dem Test + 1 (oder + "step") rechnet statt -1, und man dann einen expliziten Vergleich mit dem Endwert machen muss, statt auf Null werden warten. Das ist daher langsamer als herunterzählen, und bringt meistens nix, daher wird dies gemieden, ausser man braucht den Zähler auch als Index. Zumeist will man mit "for .. next" in 0..Ende Richtung ohnehin nur die Indizes generieren zum Arrays durchlaufen, und da ist es besser, für die Arrays zu adressieren die Adressvariablen zu verwenden (und deren Adressen hinaufzählen), und nur um den Abbruch zu erkennen (herunter) zu zählen. Die Standard Speicherblock Kopierschleife wird dadurch zu:

       HL = Anfang Quell-Array; DE = Anfang Ziel-Array; BC = Anzahl-Bytes;
       loop:
         A = Speicher[HL]; HL = HL + 1;
         Speicher[DE] = A; DE = DE + 1;
       while: BC = BC - 1; if(FZ=0)goto loop
    
Das wird so oft benutzt, dass die Z80/U880 dafür einen speziellen "Block Kopieren" Kurzbefehl (Code 11'101'101 10'010'000) drin haben, der das ganze 4 Zeilen von "loop:" bis und mit "while:" in sich vereinigt, mit 2 statt 8 Codes! Kostet dementsprechend pro Schleifendurchlauf (und kopiertes Byte) auch nur 4 statt 10 Speicherzugriffe, und spart auch 50% Zeit, bzw ist 2 mal schneller. Einen der wirklich nützlichen Erweiterungen. Eigentlich hat es sogar 16 Varianten davon, zum Teil auch Block absuchen können, mit Codes 11101101 100rc0oo, aber die restlichen 15 lass ich hier weg. Die 8086/8088 sowie der 68000 Nachfahre 68010 kennen sogar noch ausgefuchstere derartige integrierte Block Operationen. Die restlichen hier erwähnten Rechner haben gar nichts derartiges drin.

Neben Schleifen will man auch Bedingungen machen, die "if .. then .. else .. end" Teile. Da wir bereits bisher if(Flag=Wert)goto benutzen wundert es nicht, dass man dazu die genau gleichen Sprünge und bedingten Sprünge verwendet. Es gibt also keine neuen Befehlescodes zum lernen, nur eine neue Anordnung bestehender. Dazu befinden sich die beiden Programmabschnitte für die "then" und "else" Fälle hinter einander im Speicher. Je nach Fall will man nur das einte oder das andere durchlaufen, und das andere auslassen durch Überspringen. Daher setzt man davor einen bedingten Sprung, danach vereinigt man wieder mit einem normalen Sprung. Für ein "if (A<B) then A = B else B = A end" (setzt A und B beide auf den grösseren Wert) macht man:

      Adresse   Codes                  Operation
      Dez Hex   Binär       Oktal+Hex
      550 0226  10'111'000  270        if: Vergleich A ?- B, nur Flags
      551 0227  11'110'010  332 2E 02    if(FS=0)goto else   (falls positiv)
      554 022A  01'111,000  170        then: A = B           (negativ, A<B)
      555 022B  11'000'011  303 2F 02    goto end
      558 022E  01'000'111  107        else: B = A           (positiv, A>=B)
      559 022F  xx'xxx'xxx  xxx        end: nächster Befehl nach Bedingung

        Man merke, dass auch hier nur "else" und "end" als zu merkende
        Adressen auftauchen, niemals das "if". Das "then" wird nur in
        speziellen Fällen (siehe unten) benutzt. Ohne "else" Fall wird
        einfach zu "end" gesprungen und der unbedingte Sprung fällt
        komplett weg. Das gilt ebenfalls bei allen Prozessoren.
    
Zuerst wird A mit B verglichen, ohne anzugeben was für ein Vergleich (kleiner/kleiner-gleich/gleich/ungleich/grösser-gleich/grösser) man will, mit der speziellen Vergleichs-Subtraktion, damit der Inhalt von A erhalten bleibt (sonst müsste man A wegspeichern und danach wiederherstellen). Was für ein Vergleich man will wird erst danach in der Flag Wahl des bedingten Sprunges festgelegt. Hier wollen wir A < B, also schauen wir ob A - B negativ wird (Flag S, Vorzeichen, wird 1 falls negativ). Wir haben nun aber zuerst unseren "then" Fall, also müssen wir im "else" Fall springen. Also nehmen wir FS=0. Diese Umkehrung weil wir eben für "else" springen kann man leicht vergessen. Das ist eine sehr perfide Fehlerquelle, die nicht nur Anfänger erwischt. Da dies oft verwirrt, hier die generelle Flag und Sprung Merktabelle:
      Bedingung  A-B  Flag(s) für "then" Fall  Befehl(e) für "else" Sprung
      A =  B     =0   FZ=1/Null   (FS egal)    if(FZ=0)goto else
      A <> B     <>0  FZ=0/nichtN (FS egal)    if(FZ=1)goto else

      A <  B     <0   FS=1/negativ (FZ egal)    if(FS=0)goto else
      A >= B     >=0  FS=0/positiv (FZ egal)    if(FS=1)goto else

      A <= B     <1   FS=1 oder FZ=1           if(FS=0)goto else und
                                                 danach if(FZ=0)goto else
      A >  B     >0   FS=0 und FZ=0            if(FZ=0)goto *then* und
                                                 danach if(FS=1)goto else
        *SPEZIALFALL* bei dem man das "then" merken muss
        zum den "Null zu viel" else Fall vorwegzuschnappen
    
Man bemerke, dass es nur 4 Fälle direkt gibt, und man die anderen beiden kombinieren muss. Das ist mit Abstand das mühsamste Problem bei Machinencode und Assembler Programmieren. Daher testet man auch bevorzugt auf kleiner-als oder grösser-gleich, insbesondere macht man anstelle von "kleiner-gleich n" lieber "kleiner-als n+1". Dies alles gilt übrigens auch bei "loop/do .. while" oder "while .. loop" mit kleiner-gleich oder grösser-als Tests. Gerade auch "for .. next" mit 0..n wird mit daher mit kleiner-als n+1 als Endwert getestet. Das ist auch bei den 4004/4040, sowie 8048 und 8051 der Fall, Und auch beim 6502, und vielen anderen. Lediglich die 8086/8088 haben alle 6 Fälle automatisch (weil es dort 16 Bedingungen gibt, mit 5 Flags + 3 Flagkombinationen, gleich 0 oder 1), ebenso haben die 6800/6802 6809 und 68000 alle 16 davon.

Bisher haben wir immer den PC mit einer Konstante geladen oder addiert, weil das der Normalfall ist. Für ein paar spezialisierte Programmtechniken (für "case" Bedingungen, aber auch für Multitasking) will man etwas flexibleres, nämlich beliebig berechnete neue PC Werte (welche oft aus Variablen oder Sprungtabellen ausgelesen werden), was dann berechnete Sprünge ergibt. Erst ab 8080 hat es dafür einen Befehl mit Code 11'101'001, der PC = HL macht, nachdem man den Wert berechnet und in HL geladen hat. Die 4004/4040 kennen ein PC teils (nur untere 8bit) durch ein beliebiges 2*4bit Registerpaar ersetzen, ebenso der 8048, einfach mit 8bit Register. Der 8051 kennt volles Ersetzen durch ein einzelnes 16bit Register. Der 6502 hat mangels Register dafür einen PC = Speicher[16bitKonstante] Befehl. Der 6809 kennt Register und Speicher Varianten. Bei den 6800/6802 weiss ich es nicht, ebenso beim 68000.

Am Schluss hat man im 8008 8080/8085 und Z80/U880 also insgesammt an Sprung- und Verzweigungsbefehlen:

      Codes 8008         Codes 8080ff       K  Art          Operation
      Binär       Oktal  Binär       Oktal  C
      -- kein Code --    -- kein Code --    -  Reset        PC = 0

      01'xxx'100  1x4    11'000'011  303    2  Sprung       PC = 16bitKonst
      01'ccc'000  1c0    11'ccc'010  3c2    2  Bedingt Spr  if(Bed) PC = 16bitK
      -- nix --          11'101'001  351    0  Berechn Spr  PC = HL

                         Codes Z80/U880
                         Binär       Oktal
      -- nur Z80 --      00'011'000  030    1  Kurzsprung   PC = PC + 8bitK
      -- nur Z80 --      00'1cc'000  0c0    1  Bed Kurzspr  if(Bed) PC = PC + K

      -- nur Z80 --      00'010'000  020    1  Zähl Kurzspr B=B-1; if(FZ=0) PC=
                         11'101'101  355
                          +10'0rc'0oo  2co  0  Block Operationen

        xxx 8008 = beliebig 000..111, 000 normal

        ccc 8008 = FC=0/FZ=0/FS=0/FP=0/FC=1/FZ=1/FS=1/FP=1
        ccc 8080ff = FZ=0/FZ=1/FC=0/FC=1/FP=0/FP=1/FS=0/FS=1

        cc nur Z80/U880 = FZ=0/FZ=1/FC=0/FC=1

        K C = Anzahl Konstanten Codes nach dem Befehlscode


      Nummer 8008   Nummer 8080ff  Z80 "+" Form  Kürzel             Bedingung
      Binär  Oktal  Binär  Oktal   Binär  Oktal  Intel Intel Zilog
                                                 alt   neu
      001    1      000    0       (1)00  4      FZ    NZ    NZ     FZ=0
      101    5      001    1       (1)01  5      TZ    Z     Z      FZ=1
      000    0      010    2       (1)10  6      FC    NC    NC     FC=0
      100    4      011    3       (1)11  7      TC    C     C      FC=1
      011    3      100    4       -             FP    PO    PO     FP=0
      111    7      101    5       -             TP    PE    PE     FP=1
      010    2      110    6       -             FS    P     P      FS=0
      110    6      111    7       -             TS    M     M      FS=1
    

Unterprogramme, Stapel und Parameter

Auch mit Schleifen und Bedingungen entsteht letztlich nur ein Programm das geradeaus läuft und sich dann wiederholt, wenn auch kontrolliert und mit längeren und differenzierteren Aktionen drin. Damit ein Rechner wirklich universell wird muss es möglich sein, ganze Programmteile zu verschiedenen Zeiten wiederzuverwerten. Dazu benutzt man Unterprogramme. Auch dies sind Programabschnitte die man nur einmal im Speicher hat, aber mehrmals ausführen will, aber nicht wie bei einer Schleife sofort hintereinander und dann nicht mehr, sondern zu x beliebigen Zeiten ein mal. Man kann das mit dem Refrain eines Liedes vergleichen.

Program und alle Unterprogramme stehen einfach hinter einander im Speicher. Von den verschiedenen Orten her zum Unterprogram springen ist nicht schwierig, das kann man auch mit einem normalen Sprung machen, zu der Adresse vom Anfang des Unterprogrammes. Das Problem macht der Weg zurück zur Fortsetzung, da es jedesmal an einen anderen Ort zurückgehen sollte, also die Adresse dieses Sprunges nicht mehr eine Konstante ist, und das Unterprogram das folglich nicht eincodiert haben kann.

      Adresse   Codes                  Operation
      Dez Hex   Binär       Oktal+Hex
      ... ....  ..'...'...  ...
      250 00FA  11'000'011  303 84 03    goto Unterprogram
      253 00FD  xx'xxx'xxx  xxx        Fortsetzung1: beliebig weiter
      ... ....  ..'...'...  ...          und weiter ...

      350 015E  11'000'011  303 84 03    goto Unterprogram
      353 0161  xx'xxx'xxx  xxx        Fortsetzung2: beliebig weiter
      ... ....  ..'...'...  ...          und weiter ...

      450 01C2  11'000'011  303 84 03    goto Unterprogram
      453 01C5  xx'xxx'xxx  xxx        Fortsetzung3: beliebig weiter
      ... ....  ..'...'...  ...          und weiter ...

      650 028A  11'000'011  303 84 03    goto Unterprogram
      653 028D  xx'xxx'xxx  xxx        Fortsetzung4: beliebig weiter
      ... ....  ..'...'...  ...          und weiter ...


      900 0384  xx'xxx'xxx  xxx        Unterprogramm: erster Befehl Unterprog
      ... ....  ..'...'...  ...          zweiter Befehl ...

      949 03B5  xx'xxx'xxx  xxx          letzter Befehl
      950 03B6  11'000'011  303 ?? ??    goto Fortsetzung, aber wohin bloss?!
    

Wie löst man nun dieses Problem? Dazu gibt es mehrere Ansätze:

Da man Unterprogramme auch innerhalb anderer Unterprogramme benutzen will (das ergibt verschachtelte Unterprogramme), muss man mehrere derartige weggespeicherte Adressen gleichzeitig behalten können, und diese erst noch in der richtigen Reihenfolge (das nächste auszuführende Zurück muss zur neuesten noch unbenutzten Fortsetzung).

Das machen heute alle Prozessoren so, nur die Frage, wo und wie die Adressen weggespeichert werden, wurde sehr verschieden gelöst. Hier ist auch der grösste Unterschied vom 8008 zum 8080 und folgende, dass dies komplett anders gelöst wurde!

Der 8008 hat einfach 8 PC Register. Der Unterprogrammsprung (Code 01'xxx'110 mit üblicher 14bit Adresse in 2 Codes 16bit Konstante) schaltet, nachdem normal PC = PC + 1 gemacht worden ist, zum nächsten PC weiter, bevor er diesen mit der Konstante PC = Unterprogramm lädt. Der Rücksprung (Code 00'xxx'111) tut einfach den vorherigen PC wieder aktivieren. Das limitiert ihn auf Hauptprogramm und maximal 7 verschachtelte Unterprogramme. Der 4004 hat das auch so gemacht, aber mit nur 4 PC Register und 3 Unterprogramme, beim 4040 wurde das auch auf 8 PC Register erweitert. Alle anderen Prozessoren verwenden dieses veraltete Verfahren nicht mehr, mit Ausnahme der sehr primitiven PIC Mikrocontroller.

Ab dem 8080 kommt ein weitaus flexibleres System zum Einsatz. Es gibt nur einen PC (das spart erst noch Chip Platz). Dessen Inhalt wird vom Unterprogrammsprung (Code 11'001'101 mit üblicher 2 Codes 16bit Konstante) in den Speicher kopiert (nach dem 3 mal PC + 1 rechnen), bevor er das PC = Unterprogramm macht. Der Rücksprung (Code 11'001'001) geht mit kopieren PC = vom-Speicher. Dieses System kann so viele Verschachtelungen machen wie man ihm dafür Speicherplatz gibt, und es ist der wichtigste Vorteil des 8080 und folgenden, der grosse Schritt vom Terminal Logikchip zum computertauglichen Allzweck Prozessor. Der 8048 macht das auch so, aber pervertiert es mit max 16Bytes (nur 8 Adressen und damit Unterprogramme) nutzbarem Speicherausschnitt. Beim 8051 ist dies behoben, aber immer noch von dessen max 256Bytes Datenpeicher limitiert. Die 8068/8088 sind unlimitiert, ebenso die 6800/6802 und 6809 sowie der 68000. Der 6502 ist mit einem 256Bytes Speicherausschnitt etwas limitiert, wenigstens wird der nicht mit allen anderen Daten geteilt. Ausserdem hat er noch die Eigenheit, dass die Adresse des letzten Unterprogramsprung Konstantencodes abgepeichert wird, statt der Adresse des folgenden Befehls, der Rücksprung ist daher PC = vom-Speicher + 1.

Damit man hierfür den Speicher schreiben und lesen kann, muss man aber wiederum eine Adresse haben, genauso wie zum Programcodes holen. Dazu gibt es ab dem 8080 ein weiteres spezielles Adressregister namens SP (Stack Pointer = Stapelzeiger), so benamst weil die Rücksprungadressen einen Stapel bilden (neuestes/letztes zuoberst und als erstes entfernt und benutzt, macht älteres darunter sichtbar, wie bei einem Stapel Teller oder Notizzettel), und das Adressregister als Speicher[SP] auf den Stapel (genauer dessen Spitze) zeigt. Dies ist wie der PC ein nicht teilbares 16bit Register, das nur mit 16bit Befehlen bearbeitet werden kann. Weil jede zu speichernde Adresse anderen Speicherplatz braucht wird SP (auch wie der PC) bei jedem Gebrauch um 1 verändert. Es wird vor jedem Schreiben SP = SP - 1 gemacht (also insgesammt SP = SP - 2; 2*8bit Speicher[SP] = PC + 1; PC = 2*8bit Unterprogramm) und nach jedem Lesen SP = SP + 1 gemacht (also insgesammt PC = 2*8bit Speicher[SP]; SP = SP + 2). Letzteres die identische Autoincrement Logik wie bei PC = PC + 1 nach Code Lesen, ersteres ist als Gegenstück ein Autodecrement, genauer sind diese Autopostincrement und Autopräincrement. Der Stapel wird als Seiteneffekt davon von oben nach unten gefüllt, zum nachher von unten nach oben geleert, und SP zeigt auf den neuesten Eintrag. Dies machen die 8086/8088 auch so, ebenso der 6809, sowie viele andere. Die 8048 macht aber nach Schreiben +1 und vor Lesen -1, ihr Nachfolger 8051 dagegen vor Schreiben +1 und nach Lesen -1, füllen also beide von unten nach oben, mit SP bei 8048 auf ersten freien Platz und bei 8051 auf neuestes Element. Die 6502 macht dagegen nach Schreiben -1 und vor Lesen +1, also von oben nach unten, aber mit SP auf ersten freien Platz. Die Rechner mit Link Register (MIPS, ARM und PPC) verwenden was auch immer für Adressregister und +1/-1 Verfahren der Programmierer beim expliziten Abspeichern vorgibt. Konsistenz ist hier das letzte was man findet, daher muss man bei jeder Prozessorfamilie wieder nachschauen gehen, welche der 4 mathematisch möglichen Varianten der Hersteller benutzt, bevor man SP setzt.

Der Gebrauch von einem Stapel ist zwar flexibel, verlangt aber nach etwas Aufwand. SP muss nach jedem Einschalten (oder Reset) des Prozessors auf die erste Adresse des für den Stapel vorgesehenen/reservierten Stückes Speicher gesetzt werden, bevor man irgendwelche Unterprogramme benutzen kann, sonst wird irgendwo Speicher überschrieben! Das nennt man den Stapel Initialisieren, und macht es genau ein mal, zumeist mit den allerersten Befehlen nach dem Reset. Da beim 8080/8085 und Z80 vor dem Schreiben -1 gemacht wird, muss SP auf die erste Adresse nach dem zu benutzenden Speicherbereich gesetzt werden! Da der Stapel in RAM Speicher sein muss, der ja üblicherweise von oben her angeordnet wird, wird oft gleich der letzte Block davon benutzt, also mit der letzten Adresse an Hex FFFF. In dem Fall muss also SP = Hex FFFF+1=0(!) gesetzt werden! Zum SP bearbeiten gibt es ein paar Befehle:

Die beiden letzteren benutzt man eigentlich nur zum Stackframes erzeugen oder auslesen, oder zum bei Multithreading Threads umschalten. Die 8086/8088 können ihr SP einfach als achtes 16bit Register ansprechen, mit spezieller Bedeutung. Die 8048 und 8051 verwenden dafür spezielle Speicheradressen die auf SP "umgebogen" werden. Der 6502 hat einfach nur 2 Befehle zum SP vom und zum Adressregister X kopieren. Der 6809 kann sein SP wie jedes andere Adressregister bearbeiten. Der 68000 setzt SP nach dem Reset gleichzeitig wie PC mit einer weitern Konstante vom Anfang des Speichers, kann es aber auch danach wie jedes andere Adressregister bearbeiten.

Da man den Stapel ohnehin hat, kann man ihn auch gleich für anderes (miss-)brauchen. Wenn man zuwenig Register hat und welche temporär in den Speicher "auslagern" will, und danach zurückholen, kann man dies, anstelle mit 2 langen und langsamen 3 Code Speicher[16bitKonstante] Befehlen, auch schnell und kompakt auf den Stapel machen. Ebenso kann man in Unterprogrammen die Registerinhalte der aufrufenden Programme wegspeichern. Dazu gibt es ebenfalls erst ab dem 8080 1 Code Befehle bei denen Speicher[SP] benutzt wird. Diese sind die Codes 11'aa0'101 (speichern) und 11'aa0'001 (zurückholen), mit aa wie bei anderen 16bit Sachen 00..10 für BC/DE/HL, aber 11 ist weder das nutzlose A+M noch das hier sinnlose SP sondern das sehr nützliche A+F. Daneben gibt es einen weitern Spezialbefehl (Code 11'100'011) der die obersten Daten auf dem Stapel und den Inhalt von HL austauschen, sehr praktisch zum die Rücksprungadresse manipulieren. Natürlich muss man zum diese benutzen auf den Stapel mehr Platz haben als nur die sonst fälligen 2*Aufruftiefe an Bytes. Auf den 4004/4040 (und 8008) gibt es diese Befehle mangels Stapel nicht, ebenso nicht auf dem 8084 mit seinem 8*2Byte Ministapel. Alle anderen Prozessoren können dies.

Bei jedem Rechner mit beschreibbarem Stapel kann man mit Speicher[SP] = gerechnete Daten, gefolgt vom normalen Rücksprung PC = Speicher[SP], den PC mit beliebigen gerechneten Daten laden, und damit einen berechneten Sprung irgendwohin machen. Damit kann man einen fehlenden PC = gerechnete Daten berechneten Sprungbefehl ersetzen. Leider fehlen beide Varianten beim 8008. Bei den 6800/8602 6909 und 6502 ist dies sogar kompakter und schneller als der vorhandene spezielle PC = Speicher[16bitKonstante] Befehl!

Unterprogramme sind sehr schön, aber ohne ihnen Parameter übergeben zu können oder von ihnen Rückgabewerte zurück zu bekommen sind sie nur beschränkt fähig. Dazu gibt es verschiedene Methoden:

Bereits im 8008 hat es für den Fall, dass man eine Bedingung hat, mit nur einem Unterprogramm Aufruf oder einem Rücksprung drin, zwei Kombibefehle. Es sind dies bedingter Aufruf Code 11'ccc'100 (8080 und folgende, beim 8008 01'ccc'010) und Rücksprung bedingter Code 11'ccc'000 (8080 und folgende, beim 8008 00'ccc'011), mit ccc wie bei bedingten Sprüngen). Man beachte, dass man hier nicht wie beim Sprung zum "else" die Bedingung kehren muss. Ausser den 8008 8080/8085 und Z80/U880 hat sonst keiner der hier erwähnten Prozessoren sowas drin.

Am Schluss hat man im 8008 8080/8085 und Z80/U880 also insgesammt an Unterprogramm- und Stapelbefehlen:

      Codes 8008         Codes 8080ff       K  Art           Operation
      Binär       Oktal  Binär       Oktal  C
      01'xxx'110  1x6    11'001'101  315    2  Unterprog     merke PC; PC = Kon
      01'ccc'010  1c2    11'ccc'100  3c4    2  Bedingt Unt   if(Bed) m PC; PC =
      00'xxx'111  0x7    11'001'001  311    0  Rücksprung    PC = gemerkt
      00'ccc'011  0c3    11'ccc'000  3c0    0  Bedingt Rück  if(Bed) PC = gemer

      -- nix --          00'110'001  061    2  Laden 16b     SP = 16bit Konst
                           00'dd0'001  Spezialfall von  Paar = 16bit Konst
      -- nix --          00'11o'011  0o3    0  Inc/Dec 16b   SP = SP +oder- 1
                           00'ddo'011  Spezialfall von  Paar = Paar +oder- 1
      -- nix --          00'111'001  071    0  Arithmet 16b  HL = HL + SP
                           00'ss1'001  Spezialfall von  HL = HL + Paar
      -- nix --          11'111'001  371    0  Kopieren      SP = HL

      -- nix --          11'aa0'101  3a5    0  Kopieren      Speicher[SP] = aa
      -- nix --          11'aa0'001  3a1    0  Kopieren      aa = Speicher[SP]
      -- nix --          11'100'011  343    0  Austausch     HL u Speicher[SP]

        xxx und ccc wie bei Spüngen

        o = +1/-1, für increment/decrement

        ss oder dd = BE/DE/HL/SP (-nichtAM-), für Arithmetik Paare
        aa = BE/DE/HL/AF (anderes -nichtAM-), für Speichern Paare

        K C = Anzahl Konstanten Codes nach dem Befehlscode
    

Damit sind wir alle 8008 und 8080/8085 (und einen grossen Teil Z80/U880) Befehle durch, ausser denen zum Daten ein- und ausgeben, die wir wegen Platzmangel und Aufwendigkeit des Themas ignorieren. Gerade gegen den Schluss waren das doch ziemlich viele beim 8080 dazugekommenen Spezialitäten. Genau hier sieht man wieder Intels CISC Ansatz. Aber man kann zumeist ohne diese neuen Befehle auskommen, schliesslich lief der 8008 auch ohne sie! Nur ein mal SP = 16bit Konstante am Anfang ist unvermeidbar, und die Speicher[SP] = aa und aa = Speicher[SP] sind wirklich nützlich. Es fällt auf, dass bei den Z80/U880 in dieser ganzen Kategorie kein einziger neuer Befehl addiert wurde!

Programme in den Rechner Eingeben

Wir können nun beliebige Programme produzieren. Wir wissen wie Variablen in Register und Speicher benutzen, Arithmetik auf Operationen aufteilen, aus Befehlen Codes generieren, Programmablauf mit Sprüngen steuern, Adressen merken und als Konstanten einsetzen. Wir haben am Schluss ein Papier voller Bits, Papier das nicht rechnen kann. Daneben steht eine Platine mit Chips (oder früher ein grosser Kasten voller Transistoren) die rechnen kann, aber vom Programm nichts weiss. Wie bekommen wir die Bits nun in den Rechner, damit das Program ausgeführt wird? Genau darum geht es im ganzen restlichen Vortrag.

Die älteste Methode überhaupt besteht darin, den Rechner so einzuschalten, dass der Speicher läuft, aber der Prozessor angehalten ist, dann das Programm in den Speicher eingeben, und erst dann den Prozessor starten. Da während der Eingabe der Prozessor mangels Programm nicht benutzt werden kann (ein klassisches Henne-Ei Problem!), muss man den Speicher mit einem programmlosen in Hardware gebauten Editor bearbeitet werden. Das ist das Front Pannel, eine Ansammlung von einigen Reihen Lämpchen und einer Reihe Schaltern, welche früheren Rechnern ihr charakteristisches Aussehen gab. Seien das alte Minicomputer wie die DEC PDP-1 oder PDP-8 oder PDP-11, oder frühe Mikrocomputer wie die Intel Intellec8 (8008 Programentwicklungsrechner) oder Titus Mark 8 oder MITS Altair 8800.

Das Vorgehen war eigentlich immer gleich:

Wie man unschwer sieht, ist ein Rechner so benutzen ziemlich aufwendig, auch wenn die Hersteller im Laufe der Zeit einige Vereinfachungen eingebaut haben, wie:

Aber auch damit blieb das Verfahren mühsam, und wurde daher wenn möglich nur verwendet zum Bootloader eingeben auf Rechnern ohne Boot-ROM. Der Rest der Software holte man sich dann per Bootloader von Lochstreifen, Band oder Disk, sobald man diese hatte. Daher verschwanden Frontpannels auch Mitte der 1970er, als ROMs mit eingebautem Bootlader zum Standard wurden. Bei der PDP-8/E, -8/F und -8/M war dieses optinale ROM eine Platine mit 32*12bit Dioden, bei dem man die Daten "einbaute", indem man unerwünschte Dioden per Kneiffzange den einten Anschluss durchschnitt, Reparatur dann per Lötzinntropfen.

Bereits lange davor wurden aber effizientere Verfahren zur Eingabe entwickelt. Sobald Rechner Lochstreifen hatten, hat man die Möglichkeit eingeführt, den einmal eingegebenen Speicherinhalt auf Streifen zu stanzen und das nächste Mal wieder von Streifen zurückzulesen, womit man viel Arbeit und Tippfehler sparte. Natürlich konnte man danach auch statt Bits mit dem Front Pannel eingeben (und dabei auch den Rechner blockieren ohne zu rechnen!) gleich ein Program off-line auf einen Lochstreifen stanzen und davon einlesen. Nur erzeugen Fernschreiber ASCII Text und keine beliebigen Bitmuster, was man mit einem Code Umwandler beim Einlesen löste, sei das einer der ASCII Oktal oder Hex in Binär umwandelt, oder ein Verfahren das mit dem heutigen base64 ähnlich ist.

Ein Problem ist, dass man dabei nach Stanzfehler nicht mehr zurück kann zum es flicken. Was aber mit verschiedenen Ansätzen behoben wurde:

Wenn man schon ein editiertes Program im Speicher drin hat, kann man es auch gleich aus dem Editor hinaus ausführen lassen, und dann in den Editor zurückkehren zum weiter editieren (statt ausstanzen, Editor beenden, neues Program laden, abstürzen oder beenden, Editor neu starten, Program wieder einlesen), und spart so Zeit (und Lochstreifen). Ebenso kann man die ganzen Register vor Start direkt setzen und nach dem Lauf direkt anschauen, und spart sich so bei jedem Unterprogram zum testen Testcode schreiben (zum die Register vorbesetzen, dann aufrufen und danach Register abspeichern). Ebenso kann man dann auch aus dem Editor nur einen Befehl ausführen lassen, und nach jedem einzelnen Befehl automatisch alle Register ausgeben lassen, und so detailiert zuschauen wie ein Program Befehl für Befehl abläuft, was ein instinktives Verständnis für die Rechnerfunktion aufbaut:

      Beispiel Programm, die Herunterzählschleife von vorhin:

      Adresse  Code       Operation
      Hex      Oktal+Hex
      0190     002 64       B = 100
      0192     xxx        do: erster Befehl in Schleife
      0193     ...          zweiter ...

      01F3     xxx          letzter Befehl in Schleife
      01F4     005        while: B = B - 1
      01F5     302 92 01    if(FZ=0)goto do
      01F8     xxx          nächster Befehl nach Schleife


      Vorgehen Eingeben, > sind Eingaben, rest Ausgaben, hinten Kommentate:

      > d 0190                       Kommando d "display": Daten anzeigen
      0190: xx                         xx ist alter Speicherinhalt
                                       Adresse 0190 wird gemerkt
      > m 002 64 xxx ...             Kommando m "modify": Modifiziere Speicher
                                       ab der gemerkten Adresse
                                       3-stellig automatisch Oktal
                                       2-stellig automatisch Hex
      0190: 02 64 xx ...               alles in Hex angezeigt, mit Adresse
      > m ...                        ich überspringe Schleifeninhalt eingeben
      ....: ..

      > m xxx                        letzter Befehl in Schleife
      01F3: xx
      > m 005 302 0192 ...           4-stellig automatisch 16bit Hex
                                       dito wären 6-stellig 16bit Oktal
      01F4: 05 C2 92 01 ...            automatisch in 2 Codes, richtig herum


      Vorgehen Ausführen/Testen:

      > r PC 0190                    Kommando r "register": setzen PC
      Axx Fxxxx BCxxxx DExxxx HLxxxx Mxx SPxxxx Txxxx PC0190 I002,64
                                       alle Register werden ausgegeben
                                       xx und xxxx = irrelevanter Inhalt
                                       A in Hex, F Flags SZPC klein/gross Binär
                                       M Speicher[HL], T "Top" Speicher[SP]
                                       PC wie gesetzt, I nächster Befehl
                                       der  B = 100  Befehl (002 64) steht an
      > s                            Kommando s "step": Schritt/Einzelbefehl
      Axx FXXXX BC64xx DExxxx HLxxxx Mxx SPxxxx Txxxx PC0192 Ixxx
                                       B wurde auf Hex 64 gesetzt
                                       PC = PC + 2 für erst Befehl in Schleife

      > s                            einige s bis wir bei 01F4 ankommen

      > s
      Axx FXXXX BC64xx DExxxx HLxxxx Mxx SPxxxx Txxxx PC01F4 I005
                                     der  B = B - 1  Befehl (005) steht an
      > s
      Axx FszPc BC63xx DExxxx HLxxxx Mxx SPxxxx Txxxx PC01F5 I302,0192
                                     B wird 63, Flags werden szPc = 0010, z=0
                                     der  if(FZ=0)goto  (302 92 01) steht an
      > s
      Axx FszPc BC63xx DExxxx HLxxxx Mxx SPxxxx Txxxx PC0192 Ixxx
                                     weil z=0 gab es einen Sprung zu 0192

      > s 01F4                       alle Schritte bis 01F4 in einem Zug machen
      Axx FXXXX BC63xx DExxxx HLxxxx Mxx SPxxxx Txxxx PC01F4 I005

      > s 01F4                       einige mehr bis wir 100 mal durch sind
      Axx FXXXX BC01xx DExxxx HLxxxx Mxx SPxxxx Txxxx PC01F4 I005
                                     der  B = B - 1  steht letztmals an
      > s
      Axx FsZpc BC00xx DExxxx HLxxxx Mxx SPxxxx Txxxx PC01F5 I302,0192
                                     B wird 0, Flags werden sZpc = 0100, Z=1
                                     der  if(FZ=0)goto  steht letztmals an
      > s
      Axx FsZpc BC00xx DExxxx HLxxxx Mxx SPxxxx Txxxx PC01F8 Ixxx
                                     weil Z=1 gibt es jetzt keinen Sprung
                                     das Program macht weiter bei 01F8
    
Selbst wenn ein Programm mit einem Fehler abbricht, kann man nachschauen gehen wie die Register aussehen, und was für Unterprogrammaufrufe auf dem Stapel aktiv waren. Man kann sogar Registerinhalte und Speicherinhalt reparieren und das Program weiter laufen lassen. Und am Schluss kann man wie gehabt alles ausstanzen zum das nächste Mal wieder einlesen. Solche erweiterten Editoren wurden unter der Bezeichnung Monitor oder Debugger bekannt. Viele solcher Programme hiessen daher auch etwas-mon oder etwas-bug oder sogar nach bekannten Insektenvernichtungsmitteln, wie DDT (rückübersetzt zu Dynamic Debugging Tape/Technique), da "Bug" Amerikanisch für Käfer bzw Insekt ist.

Mit einem Monitor wird die Rechnernutzung weitaus komfortabler als per Front Pannel, ohne dessen Limitationen durch was man in programmloser Hardware machen kann. Das Front Pannel ist danach nur noch zum Monitor laden da, oder zum Bootloader laden zum Monitor von and holen. Sobald genug grosse ROM Speicher halbwegs billig wurden konnte man neben dem Bootlader auch den Monitor fest einbauen, und dafür die teure Front Pannel Hardware weglassen. Man musste dafür nur noch generische Ein-/Ausgabe Hardware haben, die man ohnehin für die Userprogramme braucht. Gerade bei den frühen Mikrocomputern war das dann auch eine massive Platz- und Kostenreduktion. Dabei dab es einige Hardware Varianten:

Man vergleiche die Kompaktheit der obigen Einplatinenrechner mit den früher gezeigten Front Pannel Mikrocomputer Kisten.

Mit dem sich durchsetzen der Basic Rechner (Gründe siehe VCFe Vortrag vom letzten Jahr/2007) verschwanden aber eingebaute Monitor Programme leider aus den 1980er Mikrocomputer Designs (auch dem IBM PC), zum ROM sparen. Folglich gab es für Leute denen Basic zu langsam oder zu beschränkt geworden war, und die daher auf Maschinencode umsteigen wollten, zwei Wege:

Man merke noch, dass in diesem ganzen Abschnitt zwar einzelne Rechner als Beispiele gezeigt wurde, aber keinerlei Prozessorfamilien erwähnt wurden. Das liegt daran, dass all diese Sachen sich nur nach Speicherinhalt bearbeiten richtet, und Speicher ist bei allen Rechnern gleich. Nur beim Einzelbefehl abarbeiten ist die Registerausgabe von der Prozessorfamile abhängig!

Damit sind wir mit Maschinencode durch. Wir können es nun programmieren und eingeben zum benutzen. Was jetzt noch kommt ist der Assembler obendrauf.

Assembler: Mnemonics, Labels und Pseudo-Ops

Mit den ganzen Schritten vom minimalen Front Pannel, über optimierte Front Pannels, und direkt auf Lochstreifen/Lochkarten, über Editor, bis hin zum komfortabelsten Monitor ist eines konstant geblieben: Wir geben immer noch Binärcodes ein, wenn auch eventuell in Oktal oder Hex oder gar Dezimal, und ev mit in Bytes zerlegen und L vor H wenden. Aber der Vorgang manuell Befehle in Codes umzuwandeln ist geblieben. Und der ist eigentlich eine langwierige, mühsame und fehleranfällige Tätigkeit, entweder aus einer Tabelle auslesen oder auswendig lernen, die der Computer für uns machen kann. Schliesslich ist das nicht anders als Bitmuster für die gewünschten Operationen und Operanden auslesen und Zusammensetzen.

Sobald man dies per Computer machen lassen will, muss man nun ein Format haben zum die Befehle spezifizieren. Dieses besteht aus besser als Zahlen merkbaren Textsymbolen, die man Mnemonics nennt. Diese muss das Programm erkennen, und die Codes dafür aus Tabellen heraussuchen und zusammensetzen, daher wird so ein Program auch ein Assembler (Zusammensetzer, Monteur) genannt. Die resultierenden Programme sind auch weitaus lesbarer als ein Haufen Zahlen.

Im Gegensatz zu den Befehlscodes, die fest von der Prozessorhardware vorgegeben sind, sind die Mnemonics ausschliesslich vom Assembler Program seinen Tabellen vorgegeben, und damit durch Austausch dieses Programmes beliebig änderbar. Man merke, dass dies analog ist zu Bits und Hardware, aber genau umgekehrt: Dort waren oben die Bits durch die Mathematik einmal gegeben und unten die Hardware mit mehreren Bauweisen beliebig ersetzbar. Hier sind unten die Bits einmal gegeben und darüber die Mnemonics beliebig ersetzbar. Und da man für neue Mnemonics benutzen nur ein neues Program schreiben muss, und dies relativ einfach ist (das kann man sogar in Basic), haben viele Leute genau dies gemacht, und dabei viele Varianten des sich merkbar(er) ausdrückens ausprobiert.

Intel hat für den 8008 anfangs einen sehr primitiven und simplen Assembler gemacht, der einfach nur 3-Buchstaben Kürzel (das war früher ein weit verbreiteter Standard) für jeden der 256 Befehlscodes anbietet, sowie Oktalzahlen für alle Konstanten, auch die 16bit-igen:

Damit haben wir alle 8008 Befehle die dieser Vortrag behandelt hat (alle ausser Ein-/Ausgabe) mit Mnemonics versehen. Wir können die ganzen 256 3-Buchstabensequenzen der 256 Befehlscodes als Tabelle zusammenfassen (alle Ein-/Ausgabe ist mit xxx markiert, weil hier nicht behandelt). Man sieht sofort das regelmässige Design von Datapoint:
            Werte                                   Kommentare
      +     Oktal 000 001 002 003 004 005 006 007   Oktal zeigt Struktur
      Oktal Hex00  00  01  02  03  04  05  06  07   Hex zum in nur-Hex Rechner
            Hex08  08  09  0A  0B  0C  0D  0E  0F     Anzeige eingeben
      
      000   00    HLT HLT RLC RFC ADI xxx LAI RET   man sieht die "8 Spalten"
      010   08    INB DCB RRC RFZ ACI xxx LBI RET   - 6+6 erlaubte IN* u DC*
      020   10    INC DCC RAL RFS SUI xxx LCI RET   - 4 Varianten Rotieren
      030   18    IND DCD RAR RFP SBI xxx LDI RET   - 8 bedingte RET
      040   20    INE DCE -   RTC NDI xxx LEI RET   - 8 Arithmetik I + 8b Konst
      050   28    INH DCH -   RTZ XRI xxx LHI RET   - 8 xxx Ein-/Ausgabe
      060   30    INL DCL -   RTS ORI xxx LLI RET   - 8 Load/Kopie I + 8b Konst
      070   38    -   -   -   RTP CPI xxx LMI RET   - 8 wiederholte RET

      100   40    JFC xxx CFC xxx JMP xxx CAL xxx   man sieht auch "8 Spalten"
      110   48    JFZ xxx CFZ xxx JMP xxx CAL xxx   - 8+8 bedingte J* und C*
      120   50    JFS xxx CFS xxx JMP xxx CAL xxx   - 8+8 wiederholte JMP u CAL
      130   58    JFP xxx CFP xxx JMP xxx CAL xxx   - 8+24 xxx Ein-/Ausgabe
      140   60    JTC xxx CTC xxx JMP xxx CAL xxx   alle JMP J* CAL und C*
      150   68    JTZ xxx CTZ xxx JMP xxx CAL xxx     + 2*8bit Adresskonstante
      160   70    JTS xxx CTS xxx JMP xxx CAL xxx
      170   78    JTP xxx CTP xxx JMP xxx CAL xxx

      200   80    ADA ADB ADC ADD ADE ADH ADL ADM   man sieht die 8 * 8 = 64
      210   88    ACA ACB ACC ACD ACE ACH ACL ACM   A = A OP V2
      220   90    SUA SUB SUC SUD SUE SUH SUL SUM   Arithmetikbefehle
      230   98    SBA SBB SBC SBD SBE SBH SBL SBM   64/256 = 1/4 von allem
      240   A0    NDA NDB NDC NDD NDE NDH NDL NDM
      250   A8    XRA XRB XRC XRD XRE XRH XRL XRM
      260   B0    ORA ORB ORC ORD ORE ORH ORL ORM
      270   B8    CPA CPB CPC CPD CPE CPH CPL CPM

      300   C0    LAA LAB LAC LAD LAE LAH LAL LAM   man sieht die 8 * 8 = 64
      310   C8    LBA LBB LBC LBD LBE LBH LBL LBM   V3 = V1
      320   D0    LCA LCB LCC LCD LCE LCH LCL LCM   Load/Kopierbefehle
      330   D8    LDA LDB LDC LDD LDE LDH LDL LDM   64/256 = 1/4 von allem
      340   E0    LEA LEB LEC LED LEE LEH LEL LEM
      350   E8    LHA LHB LHC LHD LHE LHH LHL LHM
      360   F0    LLA LLB LLC LLD LLE LLH LLL LLM
      370   F8    LMA LMB LMC LMD LME LMH LML HLT   HLT wäre LMM, Prozi Anhalt
    
Solche alles wichtige auf einer A4 Seite zusammengefasst Tabellen nannte man Befehlssatz Kurzübersicht. Sie waren eines der wichtigsten Hilfsmittel im vor-Assembler Zeitalter, so wichtig dass DEC bei der PDP-8/E sogar eine sehr kleine davon direkt auf das Front Pannel des Rechners aufdruckte!

Ich habe diese Tabelle neben Oktal auch noch mit Hex angeschrieben. Grund dafür ist zum zeigen, dass zum die Bitmuster der Befehlscodes generieren Oktal ideal ist, aber zum einfach Tabellenkoordinaten auslesen und in Rechner eingeben Hex kein Nachteil mehr ist, dafür aber kürzer zu tippen und damit besser. Also kann man auf diese Art auch ein reines nur-Hex 8008 oder 8080/8085 oder Z80/U880 System ohne Assembler problemlos benutzen, indem man in Assembler schreibt und beim Eingeben nur die Tabelle benutzt und nicht von Kopf Befehlscodes generiert. Oktal ist nur zum generieren hilfreich.

Bereits vor Einführung des 8080 hat Intel diese an sich sehr systematischen Mnemonics nicht mehr gemocht und hat einen neuen Assembler geschrieben, der dies teilweise anders macht. Vor allem wurden die ddd und sss Registerauswahlbits aus den Befehlen herausgenommen. Also wurden:

Beim 8080 gab es für all die neuen Befehle neue Mnemonics, oft 4-Zeichen lang, und zum Teil ziemlich weit hergeholt:

Wie man sieht ist Intels 8080 Assembler ein grosses Durcheinander von 2 bis 4 Zeichen langen Mnemonics. Am Schluss sah das dann so aus. IMHO weitaus weniger lesbar:

      +    000      001      002      003      004      005      006      007

      000  NOP      LXI B,   STAX B   INX B    INR B    DCR B    MVI B,   RLC
      010  -        DAD B    LDAX B   DCX B    INR C    DCR C    MVI C,   RRC
      020  -        LXI D,   STAX D   INX D    INR D    DCR D    MVI D,   RAL
      030  -        DAD D    LDAX D   DCX D    INR E    DCR E    MVI E,   RAR
      040  -        LXI H,   SHLD     INX H    INR H    DCR H    MVI H,   DAA
      050  -        DAD H    LHLD     DCX H    INR L    DCR L    MVI L,   CMA
      060  -        LXI SP,  STA      INX SP   INR M    DCR M    MVI M,   STC
      070  -        DAD SP   LDA      DCX SP   INR A    DCR A    MVI A,   CMC

      100  MOV B,B  MOV B,C  MOV B,D  MOV B,E  MOV B,H  MOV B,L  MOV B,M  MOV B,A
      110  MOV C,B  MOV C,C  MOV C,D  MOV C,E  MOV C,H  MOV C,L  MOV C,M  MOV C,A
      120  MOV D,B  MOV D,C  MOV D,D  MOV D,E  MOV D,H  MOV D,L  MOV D,M  MOV D,A
      130  MOV E,B  MOV E,C  MOV E,D  MOV E,E  MOV E,H  MOV E,L  MOV E,M  MOV E,A
      140  MOV H,B  MOV H,C  MOV H,D  MOV H,E  MOV H,H  MOV H,L  MOV H,M  MOV H,A
      150  MOV L,B  MOV L,C  MOV L,D  MOV L,E  MOV L,H  MOV L,L  MOV L,M  MOV L,A
      160  MOV M,B  MOV M,C  MOV M,D  MOV M,E  MOV M,H  MOV M,L  HLT      MOV M,A
      170  MOV A,B  MOV A,C  MOV A,D  MOV A,E  MOV A,H  MOV A,L  MOV A,M  MOV A,A

      200  ADD B    ADD C    ADD D    ADD E    ADD H    ADD L    ADD M    ADD A
      210  ADC B    ADC C    ADC D    ADC E    ADC H    ADC L    ADC M    ADC A
      220  SUB B    SUB C    SUB D    SUB E    SUB H    SUB L    SUB M    SUB A
      230  SBB B    SBB C    SBB D    SBB E    SBB H    SBB L    SBB M    SBB A
      240  ANA B    ANA C    ANA D    ANA E    ANA H    ANA L    ANA M    ANA A
      250  XRA B    XRA C    XRA D    XRA E    XRA H    XRA L    XRA M    XRA A
      260  ORA B    ORA C    ORA D    ORA E    ORA H    ORA L    ORA M    ORA A
      270  CMP B    CMP C    CMP D    CMP E    CMP H    CMP L    CMP M    CMP A

      300  RNZ      POP B    JNZ      JMP      CNZ      PUSH B   ADI      xxx
      310  RZ       RET      JZ       -        CZ       CALL     ACI      xxx
      320  RNC      POP D    JNC      xxx      CNC      PUSH D   SUI      xxx
      330  RC       -        JC       xxx      CC       -        SBI      xxx
      340  RPO      POP H    JPO      XTHL     CPO      PUSH H   ANI      xxx
      350  RPE      PCHL     JPE      XCHG     CPE      -        XRI      xxx
      360  RP       POP PSW  JP       xxx      CP       PUSH PSW ORI      xxx
      370  RM       SPHL     JM       xxx      CM       -        CPI      xxx
    

Wer nun glaubt die Zilog Leute hätten analog zu Befehlen addieren nur weiter Mnemonics addiert täuscht sich gewaltig. Sie haben wieder einen komplett neuen Assembler geschrieben. Manche munkeln das sei nur gewesen zum Intels Anwälten ausweichen, andere sagen weil dieser viel besser sei. Sicher ist jedenfalls, dass er total anders ist:

Spätestens jetzt wird es klar, wie sehr Assembler Mnemonics Parameter und Syntax beliebig sein können. Wen wundert es, das es auch beliebig viele weitere Assembler Schreibweisen gibt? Einen davon kennt ihr bereits seit dem Anfang des Vortrages: Meine Notation mit der ich die Wirkungen aller Befehle beschrieben habe! Auch diese A = B; A = A + C; E = A oder Speicher[Adresse] = A oder if(FZ=0)goto do sind eine leichter zu merkende textuelle Form, die ein Program durch simple Testmustervergleiche erkennen und dazu die passenden Befehlscodes generieren kann. Also auch eine gültige Assembler Schreibweise, sofern man ein passendes Assemblierprogramm hat. Solch eine an höhere Programmiersprachen angelehnte Form wird HLA (= High Level Assembler) genannt, nach dem Begriff HLL (= High Level Language) für höhere Programmiersprache. Die muss ich euch nicht mal mehr erklären wie sie aussieht, die habt ihr bereits vorzu intuitiv verstanden.

Damit haben wir Mnemonics zum Befehlscodes erzeugen im Griff, aber wie steht es mit den Codes für die Konstanten? Normale Zahlenkonstanten kann man genauso eingeben, als Zahlen. Der alte 8008 Assembler konnte die nur in Oktal entgegennehmen. Aber alle neueren Assembler gehen davon aus, dass Konstanten Dezimal sind, ausser man kennzeichnet sie als Binär oder Oktal oder Hex, entweder mit einem speziellen Zeichen davor oder danach. Einige verwenden ein b/B oder o/O oder h/H am Ende einer Konstante. Andere wollen ein Sonderzeichen davor (wie $ für Hex), andere erachten eine 0 am Anfang als Oktal und 0b/0B als Binär und 0h/0H oder 0x/0X als Hex. Konsistenz gibt es hier nicht.

Es gibt einen Fall wo ein Assembler bei Konstanten mehr machen kann als das. Wenn man einen ASCII Zeichen Code als Konstante einbaut, wie bei einen Test auf ein Kommandozeichen (wie die d m r und s im Monitor oben) oder ein Steuerzeichen (wie "Neue Zeile" in einem Terminalemulator), kann man anstelle von dem ASCII Code in einer Tabelle nachschauen einfach das Zeichen speziell markiert hinschreiben, und der Assembler extrahiert das und setzt es ein. Syntax dazu ist oft das Zeichen ' (oder ein ") davor und danach (oder zum Teil auch nur davor). Also wird das Zeichen D statt ASCII 68 (dezimal) oder $44/0x44/44H (hex) dann zu 'D' (Text). Für nicht druckbare ASCIIs (0..31 und 127) wird dann wieder zu normalen Konstanten gegriffen, ausser es hat dafür eine Spezialform, wie die 2- oder 3-Buchstaben ASCII Namen (die wichtigsten davon: NUL, BEL, TAB/HT, LF, FF, CR, ESC sowie DEL).

Damit haben wir die normalen 8bit Datenkonstanten erledigt, aber wie steht es mit den 16bit Adresskonstanten? Diese sind niemals ASCII, also fällt das mal weg. Aber irgendwie muss man sich bei diesen ja jeweils eine Adresse merken, und dann diese anderswo als Konstante einfügen, was auch mühsam ist. Assembler können einem diese Arbeit abnehmen. Dazu kann man jedesmal wenn man an eine Stelle kommt, deren Adresse zu merken ist, diese mit einem Namen versehen, den man Label (= Zettel) nennt. Genau das hab ich mit den loop: end: while: if: then: und else: Namen bei den Sprungbefehl Beispielen bereits gezeigt. Wenn man in einem Befehl nun diese Adresse als Konstante haben will, schreibt man statt einer Zahl einfach den Labelnamen hin, wie bei goto loop oder if(FZ=0)goto loop. Das selbige gilt aber nicht nur für Sprünge mit PC = 16bitKonstante sondern auch für Variablen mit Speicher[16bitKonstante] und für Arrayanfänge mit Registerpaar = 16bitKonstante. Der Assembler merkt sich alle Name=Wert Paare in einer vorzu aufgebauten Tabelle, die man Symboltabelle nennt, und schlägt die Werte danach dort drin nach. Zum Platz für Variablen reservieren, nach den Label, gibt es Pseudobefehle, die einfach nur Konstanten (Default 0) in den Speicher stellen, oft DB bzw .DB (Define Byte) oder DW bzw .DW genannt.

Aber ein Assembler kann uns noch mehr helfen, indem es uns zusätzliche Befehle gibt! Natürlich kann kein Programm einen Chip erweitern, die Binärcodes sind fix, aber es kann einem zusammengesetzte Pseudobefehle vorgaukeln. Anstelle von der Kopierschleife mit 4 Befehlen A = Speicher[HL]; HL = HL + 1; Speicher[DE] = A; DE = DE + 1 schreiben ist auch A = Speicher[HL++]; Speicher[DE++] = A eine kompaktere Form die der Assembler zusätzlich als 2 Pseudobefehle erkennen kann. Diese generieren halt einfach je 2-Code Befehlscodes, deren Bitmuster "zufällig" identisch sind mit den Befehlscode der jeweiligen beiden ersetzten realen Befehle. Ein sehr guter Assembler kann einem sogar eine Syntax geben, zum des Assemblers eingebaute Liste solcher Pseudobefehle zu erweitern. Diese Definitionen werden Macros genannt, der derart erweiterte Assembler dann als Macroassembler.

Diesen Trick kann man noch viel weiter ziehen. Die ganzen Konstrukte mit mehreren Sprungbefehlen zum Schleifen und Bedingungen machen sind auch fixe Befehlsfolgen. Diese kann man auf die gleiche Weise hinter mehreren Pseudobefehlen (pro Folge einen) verstecken. Dabei kann man auch soweit gehen, dass selbst die ganzen Flagkombinationen (und Fehlerquellen) nicht mehr sichtbar sind, also statt if(FZ=0)goto einfach if(<>)goto oder sogar gleich while(<>)goto .. whend. Einen derart erweiterten Assembler nennt man strukturierten Assembler, und er steht einer einfacheren Hochsprache wie C in fast nichts mehr nach, eigentlich nur noch im Mangel an Portabilität.

Jetzt wissen wir, wie man mit einem Assembler etwas formulieren kann, mit weniger Aufwand und lesbarer. Aber wie kommen die Mnemonics und Labels jetzt in den Rechner, so dass das Assembler Programm sie dann in die nötigen Codes verwandelt kann. Dazu gibt es 2 grundlegend verschiedene Ansätze:

Damit kann man nun problemlos Programme in Befehle umwandeln, sie als Mnemonics Parameter und Labels des jeweiligen Assemblers eingeben, übersetzen lassen und ausführen. Die Welt der Maschinencode und Assembler Programmierung ist nun sowohl verstehbar wie auch komfortabel erreichbar, erst recht wenn man einen Zeilenassembler mit HLA-artigen Syntax hat.
Home | Artikel | VCFe Maschinencode und Assembler

Diese Seite ist von Neil Franklin, letzte Änderung für VCFe 2008.07.16, letzte Änderung für LUGS Wiederholung 2008.10.18, letzte Änderung insgesammt 2009.05.27.