Carsten Kelling 1997 (); letzte Änderung: 17. September 1997 |
Die meisten Klassen des Rechner-Baukastens simulieren Rechnerkomponenten, welche eine speichernde Funktion haben. Diesen Klassen läßt sich per setValue() mindestens ein Wert zuweisen und per getValue() später wieder abfragen. Komponenten, die mehr als einen Wert speichern, müssen vorher entsprechend adressiert werden: EditableMemory und TagMemory bieten hierzu die Methode setAddress(), bei TagMemory jedoch empfiehlt der Autor die Verwendung der Methoden readRam() und writeRam() (siehe 4.5.1.4 Simulation eines Cache mit TagMemory). Register16Split übernimmt den Parameter von setValue() zwar stets in seine beiden Bytes, gibt aber, sollte es nur teilweise aktiviert sein (siehe hierzu 4.6 Visualisierung), bei getValue() auch nur den Wert des entsprechenden Bytes aus.
Die verarbeitenden Komponenten ALU und Adder bieten verschiedenen Varianten von calculate(), um neue Werte aus bestehenden zu berechnen:
Der Addierer kennt, wie der Name schon sagt, als Operation nur die Addition, calculatingMode muß Rechner.ADD_MEM sein; als Abkürzung und um beliebig viele Operanden zuzulassen, existiert deswegen die Methode add() in einigen Varianten:
Für die ALU kommen als Werte für calculatingMode in Betracht:
Konstante aus Rechner |
Operation mit zwei Operanden |
ADD_MEM, SUB_MEM, MUL_MEM, DIV_MEM | Addition, Subtraktion, Multiplikation, Division |
SHL, SHR | Schieben von erstem Operand um <zweiter Operand> Stellen nach links/rechts mit Verlust der "hinausgeschobenen" Bits. |
AND_MEM, OR_MEM, XOR_MEM | logisches UND, ODER, exklusiv-ODER |
Konstante aus Rechner |
Operation mit einem Operand |
INC, DEC | Erhöhen/Vermindern des Operanden um 1 |
NOT | logisches NICHT |
Das Ergebnis der Berechnung wird von calculate() als Rückgabewert geliefert, kann aber auch später über getValue() abgefragt werden.
Mit den Methoden getValue(), setValue() und calculate() sowie einigen zusätzlichen Berechnungen, die der Rolle des Steuerwerks entsprechen, lassen sich bereits alle Vorgänge in einem Rechner simulieren:
Beispiel: RAM
ram.setAddress(address.getValue()); register.setValue(ram.getValue());
Eine Variante mit etwas Arbeit für das Steuerwerk:
int address = instructionRegister.getValue(); address = (address >> 5) & 255; // Rolle des Steuerwerks: Adresse liegt in // den Bits 5 bis 12 des Befehlswortes ram.setAddress(address); targetRegister.setValue(ram.getValue());
Beispiel: RAM
ram.setAddress(address.getValue()); ram.setValue(register.getValue());
alu.calculate(Rechner.ADD_MEM, register1.getValue(), register2.getValue()); targetRegister.setValue(alu.getValue());
Eine Instanz von TagMemory bekommt bei der Initialisierung Verweise auf zwei Instanzen von EditableMemory übergeben; mit der einen zusammen bildet es einen kompletten Cache (Tag- und Datenspeicher) für die andere. Außerdem kennt TagMemory Einstellmöglichkeiten für alle relevanten Parameter eines Cache (Größe, Assoziativität, linesize, Schreibverhalten, Ersetzungsstrategie). Damit sind sehr mächtige Methoden readRam() und writeRam() möglich - readRam() überprüft, ob der angeforderte Wert sich im Cache befindet, holt ihn dann aus Cache oder RAM, legt den Wert gegebenenfalls im Cache ab und aktualisiert die Verwaltungsinformationen entsprechend der eingestellten Cache-Strategie; writeRam() verfährt analog bei Schreibzugriffen. Somit läßt sich Simulationscode einfach an einen Cache anpassen - aus
ram.setAddress(address.getValue()); register.setValue(ram.getValue());
wird die sogar kürzere Anweisung
tag.readRam(address, register);
die den richtigen Wert in register ablegt und sich um die Cache-Verwaltung kümmert; etwas ähnlicher dem Vorgehen ohne Cache kann man auch formulieren:
register.setValue(tag.readRam(address));
Nota bene: Die gezeigten Codebeispiele gehen offenbar davon aus, daß zwischen den einzelnen Komponenten dort, wo es notwendig ist, Verbindungen durch Busse bestehen, und daß die (durch den Rechner-Baukasten simulierten) Busse Binärwerte auch korrekt weitergeben. Wenn man bereit ist, dieses zuzugestehen, erspart man sich auf dieser Stufe der Simulation einiges an Arbeit bei dem Codieren.
Caveat: Weder getValue()/setValue() noch calculate() verändern das Erscheinungsbild der Komponenten am Bildschirm, was sich vorteilhaft auf die Simulationsgeschwindigkeit auswirkt, aber natürlich zusätzliche Methoden bedingt, um dem Betrachter die Abläufe klarzumachen. Diese Methoden beschreibt der nächste Abschnitt.
Die Methoden getValue()/setValue(), calculate() und Verwandte ermöglichen die Simulation eines Rechners ohne optische Darstellung der Vorgänge; um das Geschehene deutlich zu machen, bedarf es Methoden zur Visualisierung. Für die Zwecke dieser Anleitung ist es wichtig, sich den Unterschied zwischen Simulation und Demonstration bewußt zu machen.
Wichtigste Methode zur Visualisierung/Demonstration ist activate() (s. 4.6.1), mit dem Gegenstück deactivate() (s. 4.6.2). Durch activate() und einen anschließenden Aufruf an paint() (s. 4.6.4.1), oder besser paintActivated() (s. 4.6.4.2), wird die betreffende Komponente optisch hervorgehoben, mit der Bedeutung "bitte hierher sehen, hier geschieht gerade etwas". Die nachstehende Tabelle zeigt die unterstützten
Aufruf |
unterstützt von |
Wirkung |
activate() | Adder ALU EditableLabel Misc Mux Register16 Register16Split Register8 |
Der Rahmen der Komponente und angezeigte Werte (nicht die Überschrift, diese erscheint immer schwarz) werden
statt beispielsweise in Rechner.ALU_COLOR in Rechner.ALU_COLOR_ACTIVATED dargestellt (siehe Klasse
Rechner für weitere Farben). Die Klasse EditableLabel zeigt keinen Rahmen an. Ein eventuell zwischenzeitlich zugewiesener neuer Wert wird sichtbar (update() wird aufgerufen), bei ALU und Adder bedeutet dieses, daß das Ergebnis der letzten Berechnung und die dadurch angefallenen flags angezeigt werden. |
activate() | EditableMemory | Der Wert in der zuletzt durch setAdress() bezeichneten Speicherzelle erscheint statt in Rechner.MEM_COLOR
in MEM_COLOR_ACTIVATED und diese Zelle erscheint im sichtbaren Speicherbereich. Sollte der Speicher eine
linesize (Größe des kleinsten adressierbaren Elements in Zellen) größer 1 besitzen,
erscheinen die übrigen Werte der line in MEM_COLOR_ACTIVATED_LINE. Eventuell zwischenzeitlich den Zellen der aktuellen line zugewiesene neue Werte werden sichtbar (update() wird aufgerufen). Durch sukzessives Aufrufen von setAddress() und activate() ist es möglich, mehr als eine line einzufärben. |
activate() | PipelineRegister | Abkürzung für activate(PipelineRegister.BOTH). |
activate() | PipelineStageLabel | Ein PipelineStageLabel erscheint nur, falls es aktiviert ist (löscht aber auf jeden Fall den von ihm belegten Platz). Oberhalb seiner Mittellinie erscheint der bei der Initialisierung oder dem letzten activate(String) übergebene Text. |
activate() | TagMemory | Siehe zunächst 4.5.1.4 Simulation eines Cache mit TagMemory. Hervorgehoben
wird die Zelle, die bei dem letzten Aufruf von readRam()/writeRam() einen neuen Wert aufgenommen
hat (eventuell also keine). Der neue Wert dieser Zelle wird sichtbar. |
activate(int act) | PipelineRegister | Mögliche Werte für act: PipelineRegister.BOTH, WRITE, READ; dieser
Wert wird mit dem vorherigen Aktivierungszustand per ODER verknüpft. Bei BOTH als Ergebnis erscheint der gesamte Rahmen in REG_COLOR_ACTIVATED, dessen Inneres in MIPS_COLOR_LIGHT; bei WRITE/READ wird nur die linke/rechte Hälfte von Rahmen und Innerem eingefärbt. |
activate(int act) | Register16Split | Mögliche Werte für act: Register16Split.BOTH, HIGH_BYTE, LOW_BYTE. Bei letzteren beiden wird nur das linke/rechte angezeigte Byte (nicht der Rahmen) in REG_COLOR_BYTE_ACTIVATED eingefärbt; nur bei diesem Byte wird ein eventuell vorher zugewiesener neuer Wert sichtbar. |
activate() | SimpleBus | Abkürzung für activate("start", "end") |
activate(Color restColor) activate(Color selectColor, Color restColor) |
EditableMemory TagMemory |
Wie activate(), nur wird statt MEM_COLOR_ACTIVATED_LINE restColor und ggf. statt MEM_COLOR_ACTIVATED selectColor verwendet. |
activateCompared() | EditableMemory TagMemory |
Bei EditableMemory: Abkürzung für activate(MEM_COLOR_COMPARED, <nicht benötigt>),
wobei die linesize als 1 angenommen wird. Bei TagMemory: Alle die Zellen werden in MEM_COLOR_COMPARED hervorgehoben, die der Tag-Speicher untersuchen muß, um festzustellen, ob sich ein Wert bereits im Cache befindet - bei einem Cache mit Assoziativität n also n Zellen. |
activate(String str) | PipelineStageLabel | Ein PipelineStageLabel erscheint nur, falls es aktiviert ist (löscht aber auf jeden Fall den von ihm belegten Platz). Oberhalb seiner Mittellinie erscheint der Text str. |
activate(String src, String dst) activate(String src, String dst1, String dst2) |
SimpleBus | Auf dem Bus laufen von der durch src bezeichneten Stelle Punkte zu dst bzw. dst1 und
dst2. Diese Zeichenketten können entweder "start", "end" oder
der Bezeichner einer Abzweigung des Busses sein. Wurde setMoveDotMode(true) (ein Punkt als Symbol der relevanten Taktflanke soll sich bewegen) aufgerufen, wird der Hauptstrang des Busses zunächst nur in Rechner.MOVEDOT_COLOR_BACKGROUND eingefärbt, Abzweigungen werden wie üblich aktiviert (siehe moveDot). Sofern dem Bus durch setConnection() mitgeteilt wurde, welche Komponente an der durch src bezeichneten Stelle angeschlossen ist, wird deren Wert übernommen. |
activateTo(String dst) activateFrom(String dst) |
SimpleBus | Nur auf der durch dst bezeichneten Abzweigung des Busses laufen Punkte zu dem Hauptstrang hin (activateTo()) oder von ihm weg (activateFrom()); activateTo() kann zusammen mit activate() benutzt werden, um Punkte zu mehr als zwei Datensenken laufen zu lassen. |
boolean moveDot() | SimpleBus | Wurde setMoveDotMode(true) zuvor aufgerufen, bewegt sich ein Punkt als Symbol der relevanten Taktflanke
bei jedem Aufruf um 2 Pixel weiter. Dieser Punkt kann nur auf dem Hauptstrang des Busses bewegt werden und wird in Rechner.MOVEDOT_COLOR_FOREGROUND gezeichnet. Liefert true zurück, nachdem die "Taktflanke" am Zielpunkt verschwunden ist. |
Caveat: Auch activate() zeichnet keine Komponente neu; um diese mit verändertem Aussehen zu bewundern, ist immer noch ein Aufruf an deren paint()- oder besser paintActivated()-Methode notwendig.
Alle Komponenten unterstützen von deactivate() nur diese eine Variante ohne Parameter. Alle Hervorhebungen an der Komponente werden dadurch beseitigt, sichtbare Werte nicht verändert.
Weitere Auswirkungen:
Da deactivate() das Aussehen der Komponente verändert, wird diese dafür vorgesehen, bei dem nächsten Aufruf von paintActivated() noch einmal neu gezeichnet zu werden.
activate() hebt eine Komponente nicht nur optisch hervor, sondern ersetzt in vielen Fällen auch angezeigte Werte durch neue, intern bereits enthaltene. Diese Aktualisierung kann bei einigen Komponenten auch losgelöst von activate() durch die Methode update() erfolgen, deren Varianten Tabelle 15 nennt. Man beachte, daß das hier gemeinte update() nichts mit java.awt.Component.update() zu tun hat.
Die Kenntnis der Methoden zur Simulation und Demonstration reicht bereits zur Verständnis des nachfolgenden Codeausschnitts aus dem Von-Neumann-Rechner-Applet. Gezeigt ist der (leicht modifizierte) Code, der einen Maschinenbefehl LDA absol. (load accumulator absolutely) durchführt und am Bildschirm darstellt; dieser Befehl überträgt das niederwertige (rechte) Byte des Befehlsregisters (Variable ireg) über den Datenbus (dbus) in den Akkumulator (akku). Die weiter nach rechts eingerückten Zeilen führen die Simulation durch, die anderen die Demonstration. Letztere erfolgt in zwei Schritten durch Hochzählen von demonstrationStep; wenn im Zuge eines Demonstrationsschrittes demonstrationReady auf true gesetzt wird, kann zum nächsten Simulationsschritt fortgeschritten werden (in diesem Fall FETCH, um den nächsten Maschinenbefehl aus dem Hauptspeicher zu laden).
Aufruf |
unterstützt von |
Wirkung |
update() | Register16 Register16Split Register8 |
Der angezeigte Wert des Registers wird auf den Stand des intern gespeicherten Wertes gebracht. |
update() | EditableMemory TagMemory |
Ruft erst updateText(), dann updateBackground() auf. |
update(int adr) | EditableMemory TagMemory |
Ändert die aktuelle Adresse auf adr, ruft danach erst updateText() und dann updateBackground() auf und stellt die alte Adresse wieder her. |
updateText() updateBackground() updateForeground() |
EditableMemory TagMemory |
Gilt nur für die durch den letzten Aufruf von setAddress() bezeichnete Speicherzelle und ggf. die Zellen in derselben line: Der Inhalt (Wert), die Hintergrund- bzw. die Vordergrundfarbe werden auf den intern gespeicherten Stand gebracht. |
update() | ALU | Aktualisiert Operanden, Operator, Ergebnis und flags. |
case LDA_ABSOL: switch (demonstrationStep) // LDA absol. wird in zwei Schritten visualisiert. { case 1: ireg.activate(1); // zu ladender Wert ist das niederwertige Byte des // Befehlswortes dbus.activate("ireg", "akku"); break; case 2: akku.setValue(ireg.getValue); // getValue() liefert nach einem // activate(1) nur das gewünschte Byte n_state = FETCH; akku.activate(); demonstrationReady = true; } break;
Abschnitt 4.8 Der Simulationsansatz widmet sich den Fragen, wie man das gegebene Verhalten einer Hardware am besten mit dem Rechner-Baukasten simuliert und ob man Simulation und Demonstration, anders als im obigen Beispiel, in getrennten Methoden durchführen soll.
Alle Komponenten kennen eine paint()-Methode, die das Bild der jeweiligen Komponente unbedingt, vollständig und sofort zeichnet. Um Flackern während des Neuzeichnens zu vermeiden, ist es auf jeden Fall ratsam, double buffering zu verwenden, was von dem Rechner-Baukasten durch einige vordefinierte und -initialisierte Variablen unterstützt wird. Die paint()-Methode eines von Rechner abgeleiteten Applets, die den gesamten Rechneraufbau neu zeichnet, sollte damit strukturell so aussehen:
Die paint()-Methode des Rechner-Objekts wird vom Browser automatisch immer dann aufgerufen, wenn Teile der Fläche des Applets des Neuzeichnens bedürfen, weil sie z.B. durch ein Fenster verdeckt waren. Es sollte kaum notwendig sein, paint(), abgesehen vom Start des Applets, selbst aufzurufen.
Um die Änderungen durch Demonstrationsmethoden sichtbar zu machen, gibt es die Methode
Ebenfalls alle Komponenten besitzen die Methode paintActivated(), die das Bild der Komponente nur dann neu aufbaut, wenn sich dieses durch einen Aufruf von activate() oder deactivate() verändert hat. Für die meisten Komponenten bedeutet dieses, daß nur der erste Aufruf von paintActivated() nach activate() und deactivate() eine Wirkung hat; die von Instanzen der Klasse SimpleBus dargestellten Busse jedoch zeichnen sich, sofern aktiviert, bei jedem Aufruf von paintActivated() neu. Durch dieses Verhalten sind folgende Vorgehensweisen möglich:
Wie in 4.4.4 Die zwei Arten von Komponenten erklärt, gibt es Rechnerkomponenten, die sich von java.awt.Panel ableiten, und solche, die das nicht tun. Erstere stellen sowieso schon die Methode
reshape(int x, int y, int width, int height)
bereit, letztere bekamen eine solche geschrieben. Diese Methode ist bei Panel die Verbindung eines move() und eines resize(), und genauso funktioniert sie auch in dem Rechner-Baukasten: Die jeweilige Komponente, d.h. die linke obere Ecke des kleinsten umfassenden Rechtecks, wird an den Punkt (x; y) verschoben und Breite und Höhe dieses Rechtecks werden angepaßt.
Da die Rechnerkomponenten aber statt eines move() das mächtigere setCoordinates() kennen, gibt es auch eine entsprechende Variante von reshape():
reshape(int x, int y, int width, int height, String grabMode)
Mögliche Werte für grabMode sind natürlich die in 4.4.2.1 getCoordinates() genannten.
Die beiden folgenden Abschnitte erläutern die Anwendung von unterstützenden Funktionen des Rechner-Baukastens.
Alle Komponenten des Rechner-Baukastens erlauben es, erläuternden Text anzuzeigen. Bei den meisten erscheint dieser als "echte" Überschrift oberhalb der graphischen Repräsentation der Komponente, bei einigen jedoch innerhalb oder gar beides. Dieser Text kann eine oder auch mehrere Zeilen umfassen.
Alle von RechnerKomponentePanel abgeleiteten Klassen kennen nur eine einzeilige Überschrift. Um keine nicht vorhandenen Fähigkeiten vorzutäuschen, kann diese lediglich mit dem Methodenpaar setLabel(String)/String getLabel() eingestellt und überprüft werden. RechnerKomponente jedoch fordert die Implementation von setLabel(String[]) und String[] getLabel(), die also mit einer Reihung (array) von Zeichenketten arbeiten; nicht alle hiervon abgeleiteten Klassen aber benutzen mehr als die erste Zeile des arrays. Als Abkürzung, um nur eine Zeile Text zuzuweisen, kennt auch RechnerKomponente ein setLabel(String), welches einfach auf setLabel(String[]) umgesetzt wird (eine naheliegende analoge Implementation von String getLabel() ist nicht möglich, da eine Methode in Java nur einen Rückgabetyp haben darf).
Es ergibt sich also folgendes Bild:
Unterstützte Zeilen |
Klasse |
Zugriffsmethoden |
1 (Überschrift) | EditableLabel PipelineRegister Register16 Register16Split Register8 |
void setLabel(String) String getLabel() |
1 (Überschrift) | Adder ALU EditableMemory TagMemory |
void setLabel(String) void setLabel(String[]) String[] getLabel() |
2 (Opcode des in der Pipelinestufe bearbeiteten Befehls und Name der Stufe selbst) | PipelineStageLabel | void setLabel(String) (nur Opcode ändern) void setLabel(String[]) String[] getLabel() |
3 (Überschrift, Bezeichnung des oberen und unteren Einganges) | Mux | void setLabel(String) (nur Überschrift ändern) void setLabel(String[]) String[] getLabel() |
beliebig viele (Überschrift) | SimpleBus | void setLabel(String) |
beliebig viele (zentrierter Text innerhalb der Ellipse) | Misc | void setLabel(String[]) String[] getLabel() |
Man beachte, daß sich die Position der Überschrift eines Busses mit Hilfe von setLabelPosition() verändern läßt:
verticalBus1.setLabelPosition("left", "start"); // links oben verticalBus2.setLabelPosition("right", "end"); // rechts unten // Standard: "right", "start" horizontalBus1.setLabelPosition("start", "bottom"); // links unten horizontalBus2.setLabelPosition("end", "top"); // rechts oben //Standard: "start", "top"
Der Rechner-Baukasten unterstützt kontextabhängige kurze Hilfetexte, die neben dem Mauszeiger erscheinen, falls dieser eine bestimmte Zeit lang nicht bewegt wurde (sogenannte info tips). Um ein Applet mit info tips zu versehen, genügt es, die Methode showInfoTip(Point p) zu implementieren - dazu ist man bei Ableitung des Applets von ckelling.baukasten.Rechner auch gezwungen.
Eine beispielhafte Implementation könnte so aussehen:
public void showInfoTip(Point p) // Mauszeiger steht am Punkt p { if (infoTipLabel == null) // initialize() noch nicht aufgerufen, abbrechen return; String itt = ""; // info tip text Color bg = Color.yellow; // Hintergrundfarbe für info tip if (abus.intersectsWith(p)) 1. itt = "Adreßbus: " + abus.getInfoTipText(p); 2. else if (dbus.intersectsWith(p)) itt = "Datenbus: " + dbus.getInfoTipText(p); else if (pc.intersectsWith(p)) itt = pc.getLabel() + ": " + pc.getInfoTipText(p); else if (ireg.intersectsWith(p)) itt = ireg.getLabel() + ": " + ireg.getInfoTipText(p, true); 3. if (itt.equals("")) { bg = Color.white; itt = "Zeigen Sie auf eine Komponente"; }// Kopieren Sie das folgende bis zum Schluß in ihr showInfoTip() 4. int stringWidth = stringWidth(SMALLFONT, itt); int gap = 16; if (p.x + gap + stringWidth > bounds().width) p = new Point(p.x - stringWidth - (3*gap)/2, p.y); if (p.y + SMALLFONTHEIGHT > bounds().height) p = new Point(p.x, p.y - SMALLFONTHEIGHT); symantec.itools.awt.InfoTipManager.draw(p.x + gap, p.y, itt, SMALLFONTMETRICS, bg, Color.black); Rectangle b = infoTipPanel.bounds(); infoTipLabel.setText(itt); infoTipLabel.reshape(0, 0, b.width, b.height); infoTipLabel.setBackground(bg); } /* end showInfoTip */
Zu den Anmerkungen:
Einige Informationen benötigt man noch, um info tips mit dem Rechner-Baukasten kontrolliert einzusetzen:
Carsten Kelling 1997 (); letzte Änderung: 17. September 1997 |