Abgedeckt werden einerseits die Funktionsweise von Recheneinheit, Registern und Speicher für Variablen, ü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 Eingabe Datenformate) 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 von Zuhöhrer Feedback leicht überarbeitet (Linkregister basierte Unterprogrammaufrufe, sowie Jump/Branch Mnemonics).
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, als vertikalen Mikrocode. 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. Program in 4001 ROMs produzieren lassen kostete $3000 und 3 Wochen Wartezeit. 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.
Die 8008 benutzte normale ROM/PROM/EPROM Chips, was jeder ohne $3000 nutzen kann. 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, was den Durchbruch ergab. Seine grosse Bedeutung liegt aber darin, der Vorläufer des 8080 zu sein.
Die drittes erschien 1974 die Weiterentwicklung zum 8080 mit 6000 Transistoren ( gescanntes Handbuch). Im Gegensatz zur Entwicklung vom 4004 zum 4040, war dies 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 mehr 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 sogar 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. Sowie 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 (Chip INS8080AJ-1 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 der 8080), sowie weniger externe Beschaltung (nur 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 (Chip P8085AH-2 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 ( gescanntes Handbuch). 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 so zum häufigsten Mikrocomputer Prozessor schlechthin, in allen späteren CP/M Rechnern (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 ein paar Erweiterungen). Dazu kommen viele industrielle Anwendungen (auch Terminals, wie Retro Graphics (Chip Z80 CPU 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 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 der Z80 (nach bereits der U808 als DDR Ausgabe der 8008). 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, die U880 hat die DDR Mikro- und Homecomputer Szene total dominiert, sei das in den Robotrons, oder den KC85ern, oder im LC80. (Die U808 wiederum findet man in den Robotron K1000 Serie Tischrechnern.)
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, angeblich Nintendo Game+Watch). 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 ( gescanntes Handbuch). 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 (nur 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 (Chip P8051AH 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) ( gescanntes Handbuch). 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/etc sind eigentlich nur noch 3 Prozessorfamilien von dieser Zeit ähnlich relevant geworden:
Eigentlich sind das, wenn man die viel neueren 32bit RISC Prozessoren (wie MIPS, Sparc, ARM und PPC) ignoriert, sowie die neueren PIC und AVR Mikrocontroller, alle verbreiteten Mikroprozessoren der Weltgeschichte.
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 ohne 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.
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 neben dem ERgebnis 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 vom 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<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 Übertrag aus den achten Bits generieren, auch für das erste Resultatbit einen "nullten" Ü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/U808 8080/8085 und Z80/U880 im Vergleich dazu sind.
Am Schluss hat man im 8008/U808 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. Die 8086/8088 186/188 286 386 ... verwenden die selbigen Nummern, bis zu den heutigen neuesten PCs hinauf, 1969 von Datapoint festgelegt, bis heute unverändert erhalten geblieben.
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[Arrayanfang] bis Speicher[Arrayanfang + (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/U808, 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/U808 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 000 0 111 7 A A Akkumulator A 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]
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.
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
Spätestens hier sieht man warum man beim 2-3-3 bit Codeaufbau der 8008/U808 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.
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.
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 (bei allen), für Arithmetik 11'ooo'110 (bei 8080 und folgenden, beim 8008 00'ooo'100). Dies 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 Daten 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 zerlegt (Binär 0001'0000 1010'1000 = Hex 10 AB, bzw Binär 00'010'000 10'101'000 = Oktal 020 250), 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. Daraus folgt, dass man für Konsistenz auch bei Befehlscodes statt einer Oktal passenden 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 bei der 6502. 4040/4040, 8048 und 8051, sowie 6800/6802 und 6809 sind 4-4 Hex freundlich.
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) ebenfalls den Prozessor anhalten, und 111 (M) nichts machen. A = A + 1 und A = A - 1 wären Codes 00'000'000 und 00'000'001, und damit ersterer als alles-0er 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 = 0 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 A Art Operation Binär Oktal Binär Oktal K 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 ---- (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 A K = Anzahl Daten Konstanten Code Bytes nach dem Befehlscode
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 wäre, weil zwischen Adresse zu Speicher schicken und Daten transferieren die ALU unbenutzt ist, man die Adresse gleich nebenher incrementieren kann, was als Autoincrement bekannt ist. Womit dies Null Zeit braucht,s o schnell wie gar nicht vorhanden wäre selbst die beste und teuerste Multiplikation nicht! Dies können aber keine dieser Prozessoren.)
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 bei der 8008 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 A Art Operation Binär Oktal K -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 (invertieren) -nix- 00'110'111 067 0 Carry FC = 1 -nix- 00'111'111 077 0 Carry FC = FC XOR 1 a = zu-von-Speicher[0=BC/1=DE] o = 0=zuSpeicher[]/1=vonSpeicher[] ss oder dd = BE/DE/HL/-nichtAM-, für Paare o = +1/-1, für increment/decrement A K = Anzahl Daten Konstanten Code Bytes nach dem Befehlscode
Spätestens wenn man die vielen, zum Teil ziemlich spezialisierten und mit Limiten (die man alle 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!
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.
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 Rechner eine Speicheranordnung haben, bei dem nach dem Einschalten an Adresse 0 ein ROM/PROM/EPROM/Flash Speicher mit einem Program drin, sei das einem Bootloader oder BIOS oder gleich das ganze Program, 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). Die 6502 6800/6802 und 6809 laden aber 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) mit +1 durchzä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 und unbenutzer Platz "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 der 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. Das ergibt 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. Diese spart zwar ein Byte (2 statt 3), aber kostet mehr Taktzyklen (12 statt 10). 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 einen "Abfragen, Auswerten, Aktion, wieder von vorne, unendlich" 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 mal/lang wiederholen und dann gemäss einem Abbruchkriterium verlassen werden. Dazu gibt es bedingte Sprünge, bei denen entweder PC = etwas gesprungen oder trotzdem mit PC = PC + 1 weitergezählt wird. Daher heissen diese auf manchen Rechnern auch Verzweigungen (Branches), 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. Diese sparen auch ein Byte (2 statt 3), kosten gesprungen mehr Taktzyklen (12 statt 10), aber nicht gesprungen weniger (7 statt 10) 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. Dieser spart nicht nur ein Byte (2 statt 1+2) sondern auch Taktzyklen (schleife 13 oder ende 8 statt 4+12 bzw 4+7). 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/U808 und 8080/8085. Der 6502 sowie 6800/6802 und 6809 haben ihn aber alle auch nicht, der 68000 aber schon.
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 7oder8 Codes! Kostet dementsprechend pro Schleifendurchlauf (und kopiertes Byte) auch nur 4 statt 9oder10 Speicherzugriffe, und spart auch Zeit (schleife 21 oder ende 16 statt 7+6+7+6+6+(12bzw7)=44bzw39), und ist so 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 per Subtraktion, 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 spezielles 16bit Register. Der 6502 hat mangels Register dafür einen PC = Speicher[16bitKonstante] Befehl. Der 6800/6802 kann PC = Register X plus 8bit Konstante. Der 6809 kennt diverse Register und Speicher Varianten, ebenso der 68000.
Am Schluss hat man im 8008/U808 8080/8085 und Z80/U880 also insgesammt an Sprung- und Verzweigungsbefehlen:
Codes 8008 Codes 8080ff A Art Operation Binär Oktal Binär Oktal K -- 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 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 A K = Anzahl Adress Konstanten Code Bytes 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
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 reversen 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 verwendet, 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 daher 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 Daten 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/6802 6909 und 6502 ist dies sogar kompakter und schneller als die vorhandenen speziellen PC = Speicher[16bitKonstante] oder PC = X + Konstante Befehle!
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 bedingter Rücksprung 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/U808 8080/8085 und Z80/U880 also insgesammt an Unterprogramm- und Stapelbefehlen:
Codes 8008 Codes 8080ff A Art Operation Binär Oktal Binär Oktal K 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 A K = Anzahl Adress Konstanten Code Bytes nach dem Befehlscode
Damit sind wir alle 8008/U808 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/U808 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!
Die älteste Methode überhaupt besteht darin, den Rechner so einzuschalten, dass der (leere) Speicher läuft, aber der Prozessor angehalten ist (sonst gibts ohne Programm Absturz), 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(-karte, Band oder Disk, sobald man diese hatte. Daher verschwanden Front Pannels 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, Editieren Reparatur 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 werden. 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.
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, von Datapoint ihrem abgeleitet, 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 als 2 separate 8bit:
Damit haben wir alle 8008 Befehle die dieser Vortrag behandelt hat (alle ausser Ein-/Ausgabe) mit Mnemonics versehen. Wir können die ganzen 3-Buchstaben Sequenzen der 256 Befehlscodes als Tabelle zusammenfassen (alle Ein-/Ausgabe sind mit xxx markiert, weil hier nicht behandelt). Man sieht sofort das regelmässige Design von Datapoint:
+ 000 001 002 003 004 005 006 007 000 HLT HLT RLC RFC ADI xxx LAI RET man sieht die 8 Spalten, mit: 010 INB DCB RRC RFZ ACI xxx LBI RET - 6+6 erlaubte IN* u DC* 020 INC DCC RAL RFS SUI xxx LCI RET - 4 Varianten Rotieren 030 IND DCD RAR RFP SBI xxx LDI RET - 8 bedingte RET 040 INE DCE - RTC NDI xxx LEI RET - 8 Arithmetik I mit 8b Konst 050 INH DCH - RTZ XRI xxx LHI RET - 8 xxx Ein-/Ausgabe 060 INL DCL - RTS ORI xxx LLI RET - 8 Load/Kopie I mit 8b Konst 070 - - - RTP CPI xxx LMI RET - 8 wiederholte RET 100 JFC xxx CFC xxx JMP xxx CAL xxx man sieht auch 8 Spalten, mit: 110 JFZ xxx CFZ xxx JMP xxx CAL xxx - 8+8 bedingte J* und C* 120 JFS xxx CFS xxx JMP xxx CAL xxx - 8+8 wiederholte JMP u CAL 130 JFP xxx CFP xxx JMP xxx CAL xxx - 4*8 xxx Ein-/Ausgabe 140 JTC xxx CTC xxx JMP xxx CAL xxx alle JMP J* CAL und C* 150 JTZ xxx CTZ xxx JMP xxx CAL xxx mit 2*8bit Adresskonstante 160 JTS xxx CTS xxx JMP xxx CAL xxx 170 JTP xxx CTP xxx JMP xxx CAL xxx 200 ADA ADB ADC ADD ADE ADH ADL ADM Arithmetikbefehle 210 ACA ACB ACC ACD ACE ACH ACL ACM 64/256 = 1/4 von allem 220 SUA SUB SUC SUD SUE SUH SUL SUM man sieht die 8 * 8 = 64 Struktur 230 SBA SBB SBC SBD SBE SBH SBL SBM för A = A (8*)OP (8*)V2 240 NDA NDB NDC NDD NDE NDH NDL NDM 250 XRA XRB XRC XRD XRE XRH XRL XRM 260 ORA ORB ORC ORD ORE ORH ORL ORM 270 CPA CPB CPC CPD CPE CPH CPL CPM 300 LAA LAB LAC LAD LAE LAH LAL LAM Load/Kopierbefehle 310 LBA LBB LBC LBD LBE LBH LBL LBM 64/256 = 1/4 von allem 320 LCA LCB LCC LCD LCE LCH LCL LCM man sieht auch 8 * 8 = 64 Struktur 330 LDA LDB LDC LDD LDE LDH LDL LDM für (8*)V3 = (8*)V1 340 LEA LEB LEC LED LEE LEH LEL LEM 350 LHA LHB LHC LHD LHE LHH LHL LHM 360 LLA LLB LLC LLD LLE LLH LLL LLM 370 LMA LMB LMC LMD LME LMH LML HLT HLT Proz Anhalten, wäre LMM
Solche alles wichtige auf einer A4 Seite zusammengefasste Tabellen nannte man Befehlssatz Kurzübersicht oder Kurzreferenz. Sie waren eines der wichtigsten Hilfsmittel im vor-Assembler Zeitalter (und auch in diesem weiterhin wichtig).
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 sogar 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, mitsammt Codes verschoben, 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 PSWORI xxx 370 RM SPHL JM xxx CM - CPI xxx
Wer nun glaubt die Zilog Leute hätten analog zu Befehlen addieren nur weitere 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:
Wie man sieht ist Zilogs Z80 Assembler ziemlich anderst. Am Schluss sah das dann so aus, systematischer aber verboser, und IMHO damit noch weniger lesbar, sowie regelmässiger erscheinend aber die legalen Kombinationen verschleiernd, und IMHO damit auch weniger helfend:
+ 000 001 002 003 004 005 006 007 000 NOP LD BC, LD (),A INC BC INC B DEC B LD B, RLCA 010 EX AF, ADD HL,BLD A,() DEC BC INC C DEC C LD C, RRCA 020 DJNZ LD DE, LD (),A INC DE INC D DEC D LD D, RLA 030 JR ADD HL,DLD A,() DEC DE INC E DEC E LD E, RRA 040 JR NZ, LD HL, LD (),HLINC HL INC H DEC H LD H, DAA 050 JR Z, ADD HL,HLD HL,()DEC HL INC L DEC L LD L, CPL 060 JR NC, LD SP, LD (),A INC SP INC (HL)DEC (HL)LD (HL),SCF 070 JR C, ADD HL,SLD A,() DEC SP INC A DEC A LD A, CCF 100 LD B,B LD B,C LD B,D LD B,E LD B,H LD B,L LD B,() LD B,A 110 LD C,B LD C,C LD C,D LD C,E LD C,H LD C,L LD C,() LD C,A 120 LD D,B LD D,C LD D,D LD D,E LD D,H LD D,L LD D,() LD D,A 130 LD E,B LD E,C LD E,D LD E,E LD E,H LD E,L LD E,() LD E,A 140 LD H,B LD H,C LD H,D LD H,E LD H,H LD H,L LD H,() LD H,A 150 LD L,B LD L,C LD L,D LD L,E LD L,H LD L,L LD L,() LD L,A 160 LD (HL),LD (HL),LD (HL),LD (HL),LD (HL),LD (HL),HALT LD (HL), 170 LD A,B LD A,C LD A,D LD A,E LD A,H LD A,L LD A,() LD A,A 200 ADD A,B ADD A,C ADD A,D ADD A,E ADD A,H ADD A,L ADD A,()ADD A,A 210 ADC A,B ADC A,C ADC A,D ADC A,E ADC A,H ADC A,L ADC A,()ADC A,A 220 SUB B SUB C SUB D SUB E SUB H SUB L SUB (HL)SUB A 230 SBC A,B SBC A,C SBC A,D SBC A,E SBC A,H SBC A,L SBC A,()SBC A,A 240 AND B AND C AND D AND E AND H AND L AND (HL)AND A 250 XOR B XOR C XOR D XOR E XOR H XOR L XOR (HL)XOR A 260 OR B OR C OR D OR E OR H OR L OR (HL) OR A 270 CP B CP C CP D CP E CP H CP L CP (HL) CP A 300 RET NZ, POP BC JP NZ, JP CALL NZ,PUSH BC ADD A, xxx 310 RET Z, RET JP Z, pfx CB CALL Z, CALL ADC A, xxx 320 RET NC, POP DE JP NC, xxx CALL NC,PUSH DE SUB xxx 330 RET C, EXX JP C, xxx CALL C, pfx DD SBC A, xxx 340 RET PO, POP HL JP PO, EX (SP),CALL PO,PUSH HL AND xxx 350 RET PE, JP HL JP PE, EX DE,HLCALL PE,pfx ED XOR xxx 360 RET P, POP AF JP P, xxx CALL P, PUSH AF XOR xxx 370 RET M, LD SP,HLJP M, xxx CALL M, pfx FD CP xxx
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 ohne dass ich sie erklären musste.
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 diese 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. Das entweder mit einem speziellen Zeichen davor oder danach. Einige verwenden am Ende der Konstante ein b/B für Binär oder o/O (bzw q/Q) für Oktal oder h/H (bzw x/X) für Hex, oder gar ein d/D um Dezimal explizit zu bezeichnen. Andere wollen ein Sonderzeichen davor wie % für Binär oder @ für Oktal oder $ für Hex, oder gar ein & um Dezimal explizit zu bezeichnen. Weitere erachten eine 0 am Anfang als Oktal oder 0b/0B für Binär oder 0h/0H (bzw 0x/0X) für Hex, mit 1-9 am Anfang implizit als Dezimal angenommen. Konsistenz gibt es hier nicht. Das schlimmste was ich kenne ist ohne etwas als Oktal anzunehmen und mit Punkt danach Dezimal, was aber fürchterlich fehleranfällig ist!
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 direkt aus dem Programm und setzt es ein. Syntax dazu ist oft das Zeichen ' (oder ein ") davor und danach (bzw zum Teil auch nur davor). Also wird das Zeichen D statt ASCII 68 (dezimal) oder 44H/$44/0x44 (hex) dann zu 'D oder "D oder 'D' oder "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: 0 NUL, 7 BEL, 9 TAB oder HT, 10 LF, 12 FF, 13 CR, 27 ESC, sowie 127 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 (= Ettikette) nennt. Genau das hab ich mit den loop: end: while: if: then: und else: Namen bei obigen Sprungbefehl Beispielen bereits benutzt. 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 BYTE bzw DB bzw .DB (Define Byte) oder WORD bzw DW bzw .DW (Define Word) genannt. Ebenso kann mna nicht-Adress Konstanten definieren, oft mit EQU bzw .EQ (Equate) 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 ein 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. Solche Definitionen werden als Macros bezeichnet, ein derart erweiterter Assembler 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.
Diese Seite ist von Neil Franklin, letzte Änderung 2019.04.03