Praktikum Grundlagen von Hardwaresystemen Wintersemester 2013/2014 Versuch 5: Assembler und Computergrafik Eingebettete Systeme 15. Dezember 2013 Fachbereich 12: Informatik und Mathematik Institut für Informatik Professur für Eingebettete Systeme Prof. Dr. Uwe Brinkschulte uunter Mitarbeit von Daniel Lohn Michael Bauer Benjamin Betting Goethe-Universität Frankfurt am Main Inhaltsverzeichnis 1 Einleitung 2 2 Grundlagen 2.1 Pseudo-Zufallszahlen . . . . . . . . . . . . . . . . . . . 2.2 Assemblersprache . . . . . . . . . . . . . . . . . . . . . 2.2.1 Eigenschaften einer Assemblersprache . . . . . 2.2.2 Register . . . . . . . . . . . . . . . . . . . . . . 2.2.3 Sprungbefehle . . . . . . . . . . . . . . . . . . . 2.2.4 Sprungadressen . . . . . . . . . . . . . . . . . . 2.2.5 Instruktionsformate . . . . . . . . . . . . . . . 2.2.6 Datenspeicher . . . . . . . . . . . . . . . . . . . 2.2.7 Grundlegende Befehle und Konstrukte . . . . . 2.3 Grafikprogrammierung . . . . . . . . . . . . . . . . . . 2.3.1 Sprites - bewegliche Grafiken . . . . . . . . . . 2.3.2 Funktionsweise eines Sprites . . . . . . . . . . . 2.3.3 Sprite-Implementierung im Praktikumssystem . 2.4 Entwicklungsumgebung und Compiler . . . . . . . . . 2.4.1 Menüleiste . . . . . . . . . . . . . . . . . . . . 2.4.2 Toolbar . . . . . . . . . . . . . . . . . . . . . . 2.4.3 Eingabefeld, Zwischencode und Ausgabe . . . . 2.4.4 Interaktive Boardgrafik . . . . . . . . . . . . . 2.4.5 VGA-Memory Output . . . . . . . . . . . . . . 2.4.6 Interaktive Anzeige interner Werte . . . . . . . 2.4.7 Putty-Konsole . . . . . . . . . . . . . . . . . . 2.4.8 Status-Konsole . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 3 4 5 6 6 7 8 11 11 13 15 15 16 17 17 19 21 21 21 22 22 22 3 Anmerkungen und Tipps 23 4 Vorbereitungsaufgaben 25 5 Praktikumsaufgaben 28 1 Kapitel 1 Einleitung In diesem Abschlussversuch des Praktikums wird die Programmierung eines Prozessors mittels Assembler vermittelt. Verwendet wird eine an der Professur für Eingebettete Systeme entwickelte 16-Bit RISC-CPU, deren Design Anleihen bei verschiedenen RISC-Architekturen aufweist und in VHDL implementiert wurde. Hauptziel dieses Versuches ist es die Assemblerprogrammierung der CPU mit verschiedenen Anwendungen aus dem I/O- und Grafikbereich. Neben den wesentlichen Assemblergrundlagen werden unter anderem auch 2D-Grafiktechniken betrachtet. Im Grundlagenteil werden die Konzepte einer Assemblersprache sowie deren Funktionsweise erläutert. Des Weiteren wird die Umsetzung von 2D-Grafikprogrammierung mittels Assembler vermittelt. Anschließend werden die folgenden Aufgaben bearbeitet: Umsetzung eines Pseudo-Zufallszahlengenerators in Assembler Erstellen und Zeichnen von 2D-Grafikobjekten mittels Assembler Bewegung von Grafikobjekten mittels externer I/O-Steuereingaben durch den Benutzer Umsetzung eines rudimentären Spiel-Automaten in Assembler 2 Kapitel 2 Grundlagen 2.1 Pseudo-Zufallszahlen Zufallszahlen haben ein weites Anwendungsfeld in der Informatik. Sie werden für Simulationen und Datenverschlüsselung (Kryptographie), aber auch für Computerspiele gebraucht, um in die Reaktion des Computers, bzw. des Programms einen nicht vorhersagbaren Aspekt einzubauen. In typischen, industriellen Microcontroller-Anwendungen, wie z. B. einer Heizungssteuerung wird man Zufallszahlen hingegen eher nicht benötigen. Ein einfaches und unmittelbar einleuchtendes Beispiel für ein Mikrocontroller-Programm, das Zufallszahlen verwendet, ist eine Würfelsimulation. Auch im Audio-Bereich werden Zufallszahlen genutzt, um akustisches Rauschen zu erzeugen, das Bestandteil vieler komplexer Geräusche ist. Eine Folge echter Zufallszahlen zeichnet sich dadurch aus, dass sie auch bei perfekter Kenntnis des erzeugenden Vorgangs nicht vorhersagbar und nicht reproduzierbar (identisch wiederholbar) ist. Echte Zufallszahlen lassen sich daher nur mit einem gewissen HardwareAufwand erzeugen, indem man zum Beispiel thermisches Rauschen nutzt, das sich in Halbleiterbauelementen als elektrisches Rauschen äußert, also in winzigen Spannungsschwankungen, die zur Auswertung hoch verstärkt werden müssen. Vielfach braucht man jedoch überhaupt keinen Hardwareaufwand zu treiben, denn in den meisten Anwendungen dürften so genannte Pseudo-Zufallszahlen völlig ausreichen und sind evtl. sogar vorteilhaft. Ein Generator für Pseudo-Zufallszahlen erzeugt eine Folge von Zahlen, die auf den ersten Blick ohne Zusammenhang, eben zufällig, zu sein scheinen. Tatsächlich steckt aber ein mathematisches Prinzip hinter der Erzeugung und unter gleichen Voraussetzungen entsteht immer wieder dieselbe, identische Abfolge. Diese Reproduzierbarkeit ist von Vorteil, wenn etwa eine Simulation mit identischen zufälligen Einflussgrößen wiederholt werden soll. Es gibt mehrere Methoden, um Pseudo-Zufallszahlen zu erzeugen. In Computern, die über leistungsfähige mathematische Co-Prozessoren verfügen, lassen sich problemlos mathematisch aufwendige Algorithmen programmieren, die beispielsweise Divisionen durch sehr große Primzahlen oder ähnliches durchführen. Bei der Realisierung mit einem viel niedriger getakteten Mikrocontroller ohne allzu große Rechenfähigkeit und vielleicht knappem Programmspeicherplatz greift man besser auf andere Methoden zurück. Bewährt sind so genannte linear rückgekoppelte Schieberegister, kurz LFSR (für linear feedback shift register). Die mathematische Beschreibung der Funktion, die ein LFSR ausdrückt, findet sich bei Wikipedia. Die technische Umsetzung ist mit Hilfe eines Schieberegisters und einem oder mehrerer 3 KAPITEL 2. GRUNDLAGEN 4 Abbildung 2.1: Beispiel eines LFSR mit 8-Bit Exklusiv-Oder-Gatter (EOR, XOR) direkt in Hardware sehr effizient möglich. Aber auch ein Microcontroller-Programm benötigt nur einige Zeilen (Assembler-) Code zur Realisierung in Software. Das Prinzip des LFSR besteht darin, den Ausgang eines Schieberegisters auf seinen Eingang rückzukoppeln und mindestens eine XOR-Operation durchzuführen. Die nebenstehende Abbildung stellt eine besonders einfache Realisierung mit einem 8-Bit-Schieberegister und nur einem XOR-Gatter dar. Bei geeigneter Wahl einer Kombination aus Registerlänge n und den Einkopplungsstellen durchläuft das LFSR beim Shiften (bitweises Weiterschieben) aus einem beliebigen Anfangszustand ungleich Null sämtliche 2n − 1 möglichen Zustände, bis der Startzustand wieder erreicht ist. Durch Abgreifen eines Bits, z. B. Bit 0, nach jedem Shift-Schritt oder Verwendung größerer Registeranteile nach mehreren Schritten lassen sich Pseudo-Zufallszahlen gewinnen. Komplexere LFSR enthalten mehrere XOR-Gatter, die zwischen einzelnen Registerzellen angeordnet sind und das umlaufende“ Bit mehrfach mit ” dem internen Bit-Strom verknüpfen. Auf diese Weise findet eine stärkere Veränderung des Registerinhalts pro Shift-Schritt statt. Eine Zwischenstufe zwischen den echten und Pseudo-Zufallszahlen stellen Zahlenwerte dar, die auf besondere Weise gewonnen werden und als scheinbar echte Zufallszahlen bezeichnet werden können. Eine solche Zahl ist beispielsweise die exakte Uhrzeit zu einem bestimmten Zeitpunkt oder die möglichst genau gemessene Zeitdauer eines Tastendrucks. Wird eine scheinbar echte Zufallszahl zur Initialisierung eines LFSR verwendet, so kann man durch das Shiften eine Folge scheinbar echter Zufallszahlen generieren. Dies soll hier aber nicht weiter verfolgt werden. 2.2 Assemblersprache Eine Assemblersprache ist eine spezielle Programmiersprache, welche die Maschinensprache einer spezifischen Prozessorarchitektur in einer für den Menschen lesbaren Form repräsentiert. Daraus folgt, dass jede Computerarchitektur ihre eigene Assemblersprache hat. Ein zugehöriger Assembler ist genaugenommen ein Compiler, der den Code eines Assem” blerprogramms“ in Maschinensprache, d. h. Binärwörter übersetzt. Programmbefehle in Maschinensprache sind einfache Bitmuster, die aus den Opcodes und den zugehörigen Daten gebildet werden. Zur symbolischen Darstellung von Maschinenbefehlen werden Abkürzungen (so genannte mnemonische Symbole) benutzt, die sich ein Programmierer leichter einprägen kann als eine Funktionsbeschreibung oder gar den binären Opcode eines Befehls. Das Assemblerprogramm ist also nur eine für Menschen etwas komfortablere Darstellung des Maschinenprogramms: Anstatt KAPITEL 2. GRUNDLAGEN 5 0x4022 in der hexadezimalen Darstellung schreiben zu müssen, kann der Programmierer die Assembleranweisung ADD R1, R2 verwenden, die für den Prozessor, welcher im Praktikum benutzt wird, genau dasselbe bedeutet. 2.2.1 Eigenschaften einer Assemblersprache Die Fähigkeiten einer Assemblersprache hängen direkt von der Zielarchitektur ab. Besitzt die Ziel-CPU nicht den entsprechenden Befehl, so kann auch der Assemblerprogrammierer keinen solchen Befehl ohne weiteres benutzen. Die ganzen Konstrukte höherer Programmiersprachen, die dem Programmierer erlauben seine Algorithmen in verständliche Programme zu übertragen, fehlen: keine komfortablen Schleifen (for, while, repeat-until) keine strukturierten Datentypen keine Unterprogramme mit typisierter Parameterübergabe keine automatische Speicherverwaltun Beispiel: summe = a + b + c + d; Der Ausdruck für die obige Summe kann für die Praktikums-CPU nicht in einem einzigen Maschinensprachebefehl kodiert werden und muss daher in mehreren Anweisungen aufgeteilt werden. Die CPU kann immer nur zwei Register addieren und das Ergebnis in einem der Register speichern. Das folgende C-Programm entspricht daher eher dem Assemblerprogramm: summe = a + b; summe = summe + c; summe = summe + d; und würde in der Assemblersprache der Praktikums-CPU wiefolgt lauten: LD R1, [a] LD R2, [b] LD R3, [c] LD R4, [d] ADD R1, R2 ADD R1, R3 ADD R1, R4 Im Allgemeinen passiert hier folgendes: KAPITEL 2. GRUNDLAGEN 6 4 Variablen werden aus dem Speicher in 4 Register mit Hilfe des LD-Befehls (Load) geschrieben mit Hilfe des ADD-Befehls werden die Inhalte zweier Register addiert und das Ergebnis in das erste der beiden Register geschrieben. zum Schluss enthält R1 das Ergebnis von a + b + c + d Alle obenstehenden Befehle werden später ausführlicher erklärt. 2.2.2 Register In dem bisher genannten Beispiel wurden anstelle der Variablennamen des C-Programms stets die Namen von Registern verwendet. Ein Register ist ein Hardware-Element innerhalb des Prozessors, das der schnellen Datenspeicherung dient. In Prozessoren eines Computers sind Register direkt eingebaut, um zum Zwischenspeichern von Speicheradressen und Rechenoperanden benutzt zu werden. Die Registersätze (die Gesamtheit aller Register eines Prozessors) von Prozessoren unterscheiden sich in der Art, der Anzahl und der Größe der zur Verfügung stehenden Register. Bei unserer CPU stehen 32 x 16-Bit Register (R0-R31) zur Verfügung. Das Besondere an den Registern der Praktikums-CPU (im Gegensatz zu anderen Speichern, wie RAM oder Festplatte) ist, dass sie direkt in Befehlen verwendet werden können Operationen mit ihrem Inhalt mit nur einem Befehlswort ausgeführt werden können direkt an das Rechenwerk angeschlossen sind sowohl Quelle von Daten, als auch Ziel des Ergebnisses der Operation sein können. 2.2.3 Sprungbefehle Mit Hilfe von Sprungbefehlen können Sie den Ablauf eines Programmes an einer anderen Stelle im Programmspeicher fortführen. Grundsätzlich lassen sich Sprungbefehle nach zwei Kriterien unterscheiden: Unter welchen Umständen wird der Sprung durchgeführt und auf welche Art wird das Sprungziel bestimmt. unbedingter Sprung - das GOTO in Assembler Mit diesem Befehl wird die Ausführung des Programms an der durch die Sprunginstruktion bestimmten Stelle fortgesetzt. bedingter Sprung - das IF-THEN in Assembler Mit diesem Befehl wird die Ausführung des Programms an der durch die Sprunginstruktion bestimmten Stelle fortgesetzt, wenn die Bedingung wahr ist. Andernfalls wird die auf den Sprungbefehl folgende Instruktion ausgeführt. relativer Sprung Die Berechnung des Sprungziels erfolg relativ zur aktuellen Position im Programm. So kann beispielsweise am Ende einer Schleife zum Anfang zurückgesprungen werden ohne KAPITEL 2. GRUNDLAGEN 7 die absolute Adresse der ersten Schleifeninstruktion kennen zu müssen. Die Sprungdistanz wird häufig als Immediate“ direkt im Sprungbefehl kodiert. Sinnvoll sind derar” tige relative Sprünge, da in Programmen häufig über kurze Distanzen gesprungen wird und so der Code übersichtlicher bleiben kann. absoluter Sprung Das Sprungziel ist ein absoluter Wert. Der Programmzähler wird durch die Sprunginstruktion auf diesen Wert gesetzt. In der Regel werten alle bedingten Sprungbefehle das Statusregister des Prozessors aus. Im Statusregister speichert der Prozessor wichtige Informationen über seine generelle Betriebsart (zum Beispiel ob Interrupts zugelassen sind oder nicht) und Informationen über die Ergebnisse der letzen durchgeführten Operation der ALU. Bestimmte Befehle, zum Beispiel ADD können beim Überlauf des Rechenergebnisses dort das entsprechende Carry-Flag setzten oder, falls das Ergebnis der Operation Null war das Zero-Flag setzten. Bevor Sprungbefehle bestimmte Informationen aus dem Statusregister und den darin befindlichen Statusflags entnehmen können, müssen die Statusflags durch einen vorausgehenden Befehl erst richtig gesetzt werden. Bei unserer Praktikums-CPU benötigen wir immer den CP-Befehl (CP = Compare = Vergleich zweier Registerwerte) vor der Ausführung eines Sprungbefehls. Dieser vergleicht die angegebenen Operanden miteinander und teilt das Ergebnis dem Statusregister mit. Bedingte Sprungbefehle reagieren auf Flags, beeinflussen sie jedoch nicht. Dies ist sehr wichtig, wenn man verschiedene Flags nacheinander durch Sprungbefehle nutzen möchte. 2.2.4 Sprungadressen Sprungadressen sind im eigentlichen Sinne Parameter der Sprungbefehle(Abschnitt 2.2.3). Sie stellen die Weite oder auch Distanz eines Sprungs dar, also die Anzahl an Instruktionen, die ausgelassen werden sollen, bis der nächste abzuarbeitende Befehl folgt. Da es zwei Arten von Sprungrichtungen geben kann (vor- oder rückwärts), würde dies im mathematischen Sinn über das Vorzeichen entschieden werden. Allerdings wird dies nur sehr selten verwendet, da die meisten Programmzähler die gesamte Breite der möglichen Adressen ausschöpfen und somit intern nur vorzeichenlose (absolute) Werte verwenden. So auch der Programmzähler der Praktikums-CPU. Wenn also gesprungen wird, dann nur vorwärts, wobei ein Rücksprung durch einen Vorwärtssprung mit Zählerüberlauf realisiert ist. In den meisten Assemblersprachen können Sprungadressen in den folgenden zwei Arten dargestellt werden. als direkte Adresse in Form eines konkreten Zahlenwertes als indirekte Adresse durch Sprungmarken Ein konkretes Beispiel beider Varianten mit äquivalentem Ergebnis ist in Listening 2.3.5 zu sehen. Die Erklärung zu den verwendeten Assemblerbefehlen ist in der RISC INSSET.PDF Datei zu finden. Listening 2.3.5 # Die f o l g e n d e n b e i d e n Assemblercode A b s c h n i t t e # b e i n h a l t e n d i e s e l b e Sprunganweisung mit # ä q u i v a l e n t e r A d r e s s e in d i r e k t e r und i n d i r e k t e r Form KAPITEL 2. GRUNDLAGEN 8 # S p r u n g b e f e h l mit d i r e k t e r A d r e s s e ADD R2 , R10 CP R2 , R3 IMM 0 x000 BRE 0 x3 MUL R2 , R5 XOR R2 , R7 ST R2 , R11 LD R2 , R12 # S p r u n g b e f e h l mit i n d i r e k t e r A d r e s s e durch Sprungmarke JMP MARK ADD R2 , R10 CP R2 , R3 BRE JMP MARK MUL R2 , R5 XOR R2 , R7 ST R2 , R11 JMP MARK: LD R2 , R12 Direkte Adressen stellen die ursprüngliche Form dar, die auch direkt von der Hardware interpretiert werden kann. Aufgrund der Tatsache, dass aber in der Regel jedem Assemblerbefehl ein Maschinenbefehl im Verhältnis 1:1 zugrundeliegt, ist die Umsetzung von Sprungmarken hardwaretechnisch mit reinem Assemblerkonzept nur schwer möglich, da Marken im Grunde Synonyme für Zahlenwerte sind. Die Abbildung dieser Synonyme“ auf konkrete Zahlenwerte ” müsste somit zur Laufzeit erfolgen, wodurch alleine für die Verwendung des Konzepts von Sprungmarken zusätzliche Hardware notwendig wäre. Die meisten Assemblersprachen nutzen deshalb den Einsatz von mitgelieferten Compilern um dieses Problem zu umgehen. Diese ermöglichen es, eine virtuelle Erweiterung des ursprünglichen Befehlsspektrums der Hardware (hier CPU) zu gewährleisten. Für Sprungmarken würde dies konkret bedeuten, dass die Abbildung der Marke auf den jeweiligen Zahlenwert durch den Compiler erfolgt, der das Assemblerprogramm in den ursprünglichen Befehlssatz der Hardware überführt. So werden zum Beispiel oft auch Engpässe bei der Verwaltung von Opcodes umgangen, indem z.B. einem virtuellen Assemblerbefehl ein oder mehrere Befehle der Hardware zugrundeliegen. Innerhalb des Praktikums ist die Verwendung von Sprungadressen in beiden Varianten möglich, da Sie in späteren Versuchen Ihre entwickelten Programme mittels eines bereitgestellten Compilers in die Maschinensprache der Praktikums-CPU überführen. 2.2.5 Instruktionsformate Rechner besitzen eine feste Anzahl von Maschinenbefehlen, die sich in mehrere verschiedenen Befehlsformatgruppen einteilen lassen. Diese Befehlsformatgruppen werden beim Entwurf eines Prozessors definiert. Die Programmierung eines Rechners erfolgt letztlich (nach dem Übersetzen des Hochsprachenprogramms) durch Maschinenbefehlsfolgen, die im Speicher ab- KAPITEL 2. GRUNDLAGEN 9 gelegt sind und nach einem festen Algorithmus abgearbeitet werden. Vor Festlegung der Prozessorstruktur muss erst das Verhalten des Prozessors in Form des Maschinenbefehlssatzes definiert werden. Dabei sind festzulegen: Befehlsformate Befehlsvorrat (Maschinenbefehlsmenge) Wirkung der Befehl Ein Maschinenbefehl wird als binäres Wort (Befehlsformat) repräsentiert, wobei allerdings die Wortlänge nicht konstant sein muss. Das Befehlsformat legt fest, wie die Stellen eines Befehlswortes zu interpretieren sind, wobei oft jeweils mehrere Stellen zu Feldern zusammengefasst werden. Das Befehlsformat definiert also die Einteilung von Befehlswörtern in Felder und legt deren Bedeutung fest. Im Allgemeinen reicht ein einziges Format für alle Befehle des Maschinenbefehlssatzes nicht aus. Daher werden mehrere Formate definiert. Der Befehlsvorrat definiert die Menge aller syntaktisch korrekten Befehle. Der Maschinenbefehlsvorrat und die Interpretation der Befehle beschreiben die Funktion eines Prozessors. Aus diesen Informationen lassen sich Hinweise auf die interne Struktur des Prozessors ableiten. Für unsere CPU wurden folgende Architektur-Entscheidungen getroffen: Alle Maschinenbefehle haben eine identische Länge von 16-Bit und bestehen aus einem 6-Bit-Feld für den Operationscode (siehe Anmerkungen und Tipps) sowie einem 10-Bit Adressfeld. Diese letzten 10 Bit werden in bis zu zwei Felder aufgeteilt. Das Adressfeld dient zur Adressierung eines Datums im Hauptspeicher, der Register oder eines Sprungziels. Die einzelnen Befehlsformat werden jetzt genauer betrachtet: Befehlsformat 1 – Aufteilung der Bits IN[15:14] => Opcode A (Befehlsklasse) IN[13:10] => Opcode B (eigentlicher Befehl/Instruktion der Klasse) IN[9:5] => Operand A (Register) IN[4:0] => Operand B (Register) – Opcode Der Opcode kennzeichnet die auszuführende Operation. Unterschieden wird zwischen Opcode A und B, wobei A die Befehlsklasse (Arithmetisch, Sprung oder Datentransfer) und B die Instruktion der Klasse darstellt. Jeder Befehl hat einen eigenen Opcode, etwa die Addition, Subtraktion, das Kopieren von Registern oder Laden und Speichern von Registern aus dem Arbeitsspeicher. Jedem Opcode wird ein kurzes Wort, ein so genanntes Mnemonic (AssemblerbefehlBezeichner), zugeordnet. Auf die meisten Opcodes müssen Adressen, Konstanten oder ähnliches folgen, die dann zusammen mit dem Opcode einen Maschinenbefehl bilden. – Beispiel Befehl: Ergebnis: ADD R1, R2 R1 = R1 + R2 KAPITEL 2. GRUNDLAGEN 10 Befehlswort: 0x4022 Opcode A: 0x1 (arithmetische Instruktion) Opcode B: 0x0 (Addition zweier Register) Im Befehlswort kennzeichnet 0x die hexadezimale Darstellung. Des Weiteren sind neben der hexadezimalen auch die dezimale (0d) und die binäre (0b) Darstellung von Zahlen möglich. Befehlsformat 2 – Aufteilung der Bits IN[15:14] => Opcode A (Befehlsklasse) IN[13:10] => Opcode B (eigentlicher Befehl/Instruktion der Klasse) IN[9:5] => Operand A (Register) IN[3:0] => Immediate[3:0] (konstanter 4-Bit Wert [0:15]) IN[4] => nicht definiert – Beispiel Befehl: IMM 0d0 ADDI R1, 0d14 Ergebnis: R1 ← R1 + 14 Befehlsworte: 0xF000 0x442E Befehlsformat 3 – Aufteilung der Bits IN[15:12] => Opcode (Immediate, IMM) IN[11:0] => Immediate[15:4] (konstanter 12-Bit Wert [0:4095]) – Opcode 0xF => IMM // RIM M ← immediate Der IMM-Befehl ist nur sinnvoll, wenn er direkt vor einem Befehl steht, der ebenfalls ein Immediate enthält wie z.B. ADDI. In jedem Fall wird der Immediate Wert auf 16-Bit erweitert. Die höherwertigen 12-Bits liefern die IMM-Instruktion, die restlichen vier die Folgeinstruktion. Die höherwertigen 12-Bits der IMM-Instruktion bleiben auch nach der Instruktion im IMM-Register erhalten. Deshalb ist darauf zu achten, dass dieser Wert je nach Bedarf explizit auf Wert 0 (default) rückgesetzt wird. – Beispiel Befehle: IMM 0d123 ADDI R1, 0d14 Ergebnis: R1 ← R1 + (123 ∗ 24 + 14) Befehlsworte: 0xF07B 0x442E KAPITEL 2. GRUNDLAGEN 2.2.6 11 Datenspeicher Meistens reichen die Register nicht aus, um ein Problem zu lösen. In diesem Fall muss auf den Hauptspeicher des Computers zugegriffen werden, der wesentlich mehr Information speichern kann. Für den Assemblerpogrammierer sieht der Hauptspeicher wie ein riesiges Array von Registern aus, deren Einträge je nach Wunsch eine Länge von 8, 16 oder 32 Bit aufweisen. Die kleinste adressierbare Einheit ist also ein Byte (= 8 Bit). Daher wird auch die Größe des Speichers in Bytes gemessen. Um auf einen bestimmten Eintrag des Arrays ”Hauptspeicherßugreifen zu können, muss der Programmierer den Index, d. h. die Adresse des Eintrages kennen. Das erste Byte des Hauptspeichers bekommt dabei die Adresse 0x0000, das zweite die Adresse 1 usw. In einem Assemblerprogramm können Variablen angelegt werden, indem einer Speicheradresse ein Label zugeordnet und dabei Speicherplatz in der gewünschten Größe reserviert wird. 2.2.7 Grundlegende Befehle und Konstrukte Für die Bearbeitung der Aufgaben dieses Versuches werden Sie die zur Verfügung gestellte Praktikums-CPU und die in das System eingebundene VGA-Grafikeinheit programmieren. Für den Fall, dass Ihre Kenntnisse in Assemblerprogrammierung nicht ausreichend sein sollten, werden im Folgenden die wesentlichen Programmierkonstrukte betrachtet, bzw. rekapituliert. Kontrollpfade: IF-ELSE-Statements IF-ELSE-Kontrollpfade werden auch in Assembler dazu benötigt, um die verschiedenen Befehlssequenzen gemäß der Programmlogik deterministisch auszuführen. Obwohl dieses Konstrukt bei Hochsprachen direkt als eigenständige Direktive eingebettet ist, wird in Assembler dazu eine Kombination aus mehreren Befehlen benötigt. Zunächst muss eine Abfrage formuliert werden, dem ein Vergleich mit einer entsprechenden Bedingung voraus geht. Anschließend wird eine Sprungdirektive festgelegt, an welcher Stelle das Programm entsprechend dem Vergleichsergebniss weitergeführt werden soll. Das folgende Beispiel zeigt den Asemblercode einer allgemeinen IF-ELSE-Abfrage. Listing 2.1: Assemblercode-Beispiel für IF-ELSE Statement # Abfrage : IF (R5 == R6) { R5 ++} ELSE { R5=0} CP R5 , R6 # S p r i n g e zum ELSE−Zweig s o f e r n Bedingung n i c h t e r f ü l l t BRNE ELSE CLAUSE INC R5 JMP END IF # ELSE−Zweig ELSE CLAUSE : CLR R5 END IF : Shiften und Rotieren von Bits BIT-SHIFT-Befehle verschieben alle Bit in einem Register um eine Position nach links oder KAPITEL 2. GRUNDLAGEN 12 rechts. Beim Shiften wird eine Null eingefügt und das Bit, das rausgeschoben wird geht verloren. Beim Rotieren wird das Bit, das rausgeschoben wird, wieder eingefügt. Listing 2.2: Shiften und Rotieren von Bits in Assembler # Eine P o s i t i o n nach l i n k s SHIFTEN # R0 [ 1 5 : 1 ] = R0 [ 1 4 : 0 ] , R0 [ 0 ] = 0 LSL R0 # Eine P o s i t i o n nach r e c h t s SHIFTEN # R1 [ 1 4 : 0 ] = R1 [ 1 5 : 1 ] , R1 [ 1 5 ] = 0 LSR R1 # Eine P o s i t i o n nach l i n k s ROTIEREN # R2 [ 1 5 : 1 ] = R2 [ 1 4 : 0 ] , R2 [ 0 ] = R2 [ 1 5 ] ROL R2 # Eine P o s i t i o n nach r e c h t s ROTIEREN # R3 [ 1 4 : 0 ] = R3 [ 1 5 : 1 ] , R3 [ 1 5 ] = R3 [ 0 ] ROR R3 Schleifen Schleifen werden in der Regel dazu genutzt, um eine Sequenz von Befehlen mehrfach auszuführen. In Assembler werden Schleifen mit bedingten Sprunganweisungen realisiert. Am Anfang der Schleife benötigen wir hierzu ein Sprungziel, welches den Kopf der Schleife darstellt. Danach folgt die Befehlssequenz, die durch die Schleife mehrfach wiederholt werden soll. Das Ende der Schleife wird durch den Schleifenbefehl festgelegt. Listing 2.3: Assemblercode-Beispiel für eine Schleife in Assembler # S c h l e i f e n k o n s t r u k t : f o r { i =0; i < 5 ; i++} {R5 = R5 * 2} IMM 0 x000 LDI R2 , 0 x2 # D e f i n i t i o n d e s A b b r u c h k r i t e r i u m s 5 in R2 LDI R1 , 0 x5 # S c h l e i f e n z ä h l e r i in R0 a u f Null s e t z e n CLR R0 # Schleifenrumpf LOOP: MUL R5 , R2 # S c h l e i f e n z ä h l e r erhöhen INC R0 # Check d e r Abbruchbedingung i < 5 CP R0 , R1 # Rücksprung s o l a n g e i < 5 BRL LOOP KAPITEL 2. GRUNDLAGEN 2.3 13 Grafikprogrammierung Bei der Grafikprogrammierung ist vor allem die Geschwindigkeit sehr wichtig. Im Allgemeinen sind Grafikoperationen sehr rechenaufwendig und benötigen daher viel Prozessorzeit. Um diese Geschwindigkeit zu erreichen, wurden die entsprechenden Programmroutinen früher oft in Assembler implementiert. Noch heute kann man Unterprogramme mit Assemblerbefehlen im Quellcode vieler höherer Programmiersprachen sehen. Die Grafikkarte eines PCs kennt verschiedene Bildschirmmodi, in denen die Anzahl der Pixel und Farben unterschiedlich sind. In unserem Fall wird der Bildschirm in der StandardVGA-Auflösung von 640 x 480 geometrischen (physikalischen) Bildpunkten angesteuert. Aufgrund der Adressierungskomplexität der Pixelmenge, sieht die interne Darstellung der PraktikumsCPU ein Down-Sampling um den Faktor 4 vor. Somit ist es möglich eine geringere Auflösung mit 160 x 120 logischen Bildpunkten zu betreiben, wobei 1 logischer Pixel einem Quadrat aus 4x4 geometrischen Pixeln entspricht. Der Bild- bzw. Grafikspeicher ist im SRAM des FPGA-Boards abgelegt. Beginnend ab Adresse 0x0000 gibt es zwei Arten der Pixel-Adressierung. Flush Adressierung, d.h. jede Adresse adressiert immer alle Pixel zugleich, wodurch jeder Pixel den gleichen Farbwert zugewiesen bekommt (löschen des Bildspeichers mit einer einheitlichen Farbe). Den Pixeln können somit nicht unterschiedliche Farbwerte zugewiesen werden. Des Weiteren muss berücksichtigt werden, dass die Zeitspanne die für einen vollständigen Flush benötigt wird (ca. 3,1 Millisekunden), deutlich länger ist, als die Ausführung einer einzigen Assemblerinstruktion durch die CPU (ca. 167 Nanosekunden). Während dieser Phase ist es nicht möglich den Bildspeicher zu adressieren, noch die Art der Pixel-Adressierung zu ändern. Indirekte Adressierung über ein 2D kart. Koordinatensystem, d.h. jede Adresse adressiert genau einen Pixel und unterteilt sich in Lower-Byte (Adresse[7:0]) für die YKoordinate und Upper-Byte (Adresse[15:8]) für die X-Koordinate. Jedem Pixel kann somit ein individueller Farbwert zugewiesen werden. Die Art der Adressierung wird über einen Konfigurationswert des Video-Memory-Controllers (VMC) im Hauptspeicher (RAM) der CPU an der Adresse 0x0007 festgelegt, wobei der Wert 0x0002 für Flush und 0x0001 für die Indirekte Adressierung steht. Da die Grafikeinheit lediglich 8 Farben darstellen kann, stehen für jeden Pixel 3-Bit für die Farbkodierung (RGB) zur Verfügung, d.h. bei einem Schreibzugriff auf den Bildspeicher werden nur die 3 niederstwertigen Bits gespeichert und die restlichen Bits des Datums ignoriert. Bei einem Lesezugriff auf den Bildspeicher werden die nicht vorhandenen Bits des gelesenen Wortes mit Nullen aufgefüllt. Das Koordinatensystem hat seinen Ursprung in der linken oberen Ecke des Bildschirms. Dort befindet sich der Punkt (0, 0) mit der Adresse 0x0000. Wird der Bytewert an dieser Position verändert, so ändert sich automatisch die Farbe des Pixels auf dem Bildschirm. Um nun jeden Punkt einzeln zu erreichen (indirekte Adressierung), kann man die Adresse auch so formulieren: Basisadresse ist 0x0000, die Verschiebung zum aktuellen Pixel wird durch die Angabe der X-([0:159]) und Y-Koordinate ([0:119]) intern automatisch durch den VMC berechnet. Um die jeweiligen Koordinaten an die richtige Bit-Position der Adresse zu setzen, können Sie BIT- KAPITEL 2. GRUNDLAGEN 14 SHIFTING verwenden. Das folgende Assemblerprogramm realisiert das Setzen des Pixels (23, 67) mit der Farbe Blau, unter Verwendung von Bit-Shifting kombiniert mit Schleifen: Listing 2.4: Setzen eines blauen Pixels auf dem Monitor # a k t i v i e r e Flush Modus IMM 0 x000 LDI R0 , 0 x2 LDI R1 , 0 x7 ST R0 , R1 LDI R0 , 0 x7 CLRV R3 # W a r t e s c h l e i f e b i s Flush b e e n d e t i s t ( 3 . 1 M i l l i s e k u n d e n ) CLR R0 LDI R1 , 0 x4883 AWAIT FLUSH : INC R0 CP R0 , R1 BRNE AWAIT FLUSH # a k t i v i e r e n d e r i n d i r e k t e n A d r e s s i e r u n g von P i x e l n IMM 0 x000 LDI R0 , 0 x1 LDI R1 , 0 x7 ST R0 , R1 # s e t z e n d e r X−K o o r d i n a t e in R1=0d23=0x0017 IMM 0 x001 LDI R1 , 0 x7 # s e t z e n d e r Y−K o o r d i n a t e in R2=0d67=0x0043 IMM 0 x004 LDI R2 , 0 x3 # S c h l e i f e um X−K o o r d i n a t e d e s P i x e l s an P o s i t i o n [ 1 5 : 8 ] # d e r A d r e s s e zu s h i f t e n SHIFT X COORD : # X−K o o r d i n a t e um e i n B i t nach l i n k s s c h i e b e n LSL R1 # Z ä h l e r i n k r e m e n t i e r e n INC R3 # V e r g l e i c h Abruchsbedingung IMM 0 x000 LDI R4 , 0 x8 CP R3 , R4 BRNE SHIFT X COORD KAPITEL 2. GRUNDLAGEN 15 # Verkn üpfen d e r f e r t i g e n Z i e l a d r e s s e aus b e i d e n Koordinaten # X [ 1 5 : 8 ] und Y [ 7 : 0 ] in R1 OR R1 , R2 # s e t z e n d e s P i x e l s ( 2 3 , 6 7 ) mit d e r Farbe Blau IMM 0 x000 # s e t z e n d e r Farbe Blau in R3 LDI R3 , 0 x1 STV R3 , R1 2.3.1 Sprites - bewegliche Grafiken Ein Sprite ist ein zweidimensionales Grafikobjekt. Bei einem Sprite handelt es sich um einen kleinen rechteckigen Speicherbereich, der als Bildschirmausschnitt den Bildschirminhalt partiell verdeckt oder sich mit ihm mischt. Ein Sprite wird von der Grafikhardware über das Hintergrundbild, bzw. den restlichen Inhalt der Bildschirmanzeige eingeblendet. Die Positionierung wird dabei komplett von der Grafikhardware erledigt. Die aktuelle Position des Sprites wird in einem Registersatz gehalten, so dass eine Änderung der Registereinträge zu einer Bewegung des Sprites führt. Als Beispiel kann ein Mauszeiger betrachtet werden, der heutzutage von den meisten Grafikkarten als Hardware-Sprite zur Verfügung gestellt wird. In vergangenen Zeiten waren Sprites vor allem in Videospielen und Homecomputern verbreitet. Der C64 beispielsweise verdankt einen Großteil seiner Grafikfähigkeiten der Unterstützung von Sprites. 2.3.2 Funktionsweise eines Sprites Das Sprite wird von der Grafikhardware an der gewünschten Position im Bild eingefügt. Weil dadurch das restliche Bild im Grafikspeicher nicht beeinflusst wird, muss dieses nicht immer wieder neu dorthin kopiert werden. Durch diese Entlastung des Hauptprozessors sind Sprites sehr schnell und gleichzeitig einfach zu programmieren, erfordern allerdings zusätzliche Hardwareressourcen. Die Daten für die Sprite-Grafik werden dabei entweder direkt in Registern der Grafikhardware vorgehalten oder in speziellen RAM-Bereichen, auf die diese Hardware einen genügend schnellen Zugriff hat. Zur Bewegung eines Sprites reicht es aus, lediglich dessen i und j Koordinate zu ändern. Die aktuelle Position (i, j) des Sprites wird in einem Register gehalten, so dass eine Änderung des Registereintrags (i+1, j+1) zu einer Bewegung des Sprites führt. In den drei Abbildungen 2.2, 2.3 und 2.4 sehen Sie, wie die Bewegung realisiert wird. Bestimmung der Koordinaten bei der Bewegung des Sprites über den Bildschirm Die komplizierte Berechnung der Adresse im Grafikspeicher und das Umkopieren des Inhalts entfällt, was ebenfalls den Hauptprozessor entlastet. Der Grafikprozessor fügt selbständig an der vorgegebenen Koordinate das Sprite beim Aufbau des nächsten Bildes im Vordergrund ein. Auch animierte Sprites sind möglich. KAPITEL 2. GRUNDLAGEN 16 Abbildung 2.2: Graphische Darstellung eines Sprites auf der Position (i, j) Abbildung 2.3: Graphische Darstellung eines Sprites auf der Position (i, j+1) Abbildung 2.4: Graphische Darstellung eines Sprites auf der Position (i+1, j+1) 2.3.3 Sprite-Implementierung im Praktikumssystem Das VGA-Modul des Praktikumsprozessorsystems unterstützt genau ein Sprite mit 8x8 Pixeln. Jedes Pixel des Sprites kann entweder weiß oder durchsichtig sein, abhängig vom zugehörigen Bitwert. Ist das entsprechende Bit gesetzt, so ist das Pixel des Sprites weiß, ansonsten durchsichtig. Jede der acht Zeilen des Sprites wird in einem 8-Bit-Register gespeichert, die an den Adressen 0xFFF0 - 0xFFF7 liegen. Die Position des Sprites wird in zwei weiteren Registern an den Adressen 0xFFF8 und 0xFFF9 gespeichert. Die Positionsregister sind jeweils 8 Bit breit. Beschreibung der Hardware-Register KAPITEL 2. GRUNDLAGEN 17 Adresse 0xFFF0, 8-Bit Register - oberste Zeile des Sprites Adresse 0xFFF1, 8-Bit Register - zweite Zeile des Sprites Adresse 0xFFF2, 8-Bit Register - dritte Zeile des Sprites Adresse 0xFFF3, 8-Bit Register - ... Adresse 0xFFF4, 8-Bit Register - ... Adresse 0xFFF5, 8-Bit Register - ... Adresse 0xFFF6, 8-Bit Register - ... Adresse 0xFFF7, 8-Bit Register - unterste Zeile des Sprites Adresse 0xFFF8, 8-Bit Register - horizontale Position des Sprites Adresse 0xFFF9, 8-Bit Register - vertikale Position des Sprites 2.4 Entwicklungsumgebung und Compiler Das TINY RISC STUDIO v1.0 ist eine Entwicklunsgumgebung, welche Ihnen das Implementieren, Kompilieren, Debuggen und Emulieren von Assemblerprogrammen der PraktikumsCPU ermöglicht. Die fertige Ausgabe des Compilers wird direkt durch Pfadangabe in das Instruktionsregister (ROM) der CPU geschrieben. Der Aufbau und die Nutzung der GUI (Abbildung 2.5) ist einfach gehalten und besteht im wesentlichen aus 8 Komponenten, deren Nutzung im Folgenden erläutert wird. 1. Menüleiste 2. Toolbar 3. Eingabefeld, Zwischencode und Ausgabe 4. Interaktive Boardgrafik 5. VGA-Memory Output 6. Interaktive Anzeige interner Werte 7. Putty-Konsole 8. Status Konsole 2.4.1 Menüleiste Die Menüleiste, in Abbildung 2.5 als 1. gekennzeichnet, bietet Standardoptionen zum Compilieren, zum Laden und zum Speichern des Quellcodes. Die Menüunterpunkte sind folgend aufgelistet und beschrieben. hen (beispielsweise wenn der VGA-Memory geflusht werden soll, obwohl der Modus nicht auf flush gesetzt wurde). Zur Orientierung des Benutzers befindet sich bei pausierter Emulation ein roter Pfeil im Codefenster, welcher die nächste auszuführende Codezeile angibt, wodurch der interne Stand des Emulators erkennbar ist. KAPITEL GRUNDLAGEN 6.2.2. Komponenten 18 Abbildung 6.1.: Emulator mit Einteilung der Komponenten Abbildung 2.5: GUI des TINY RISC STUDIO v1.0 6.2.1. Menüleiste File Die Menüleiste, in Abbildung 6.1 als 1. gekennzeichnet, bietet Standardoptionen zum Compilieren, zum Laden zum Speichern des Quellcodes. Die MenüunterpunkteCtrl sind+folgend New: Per Klick aufund diesen Button oder mit der Tastenkombination N, wird ein aufgelistet und beschrieben. neues Dokument angelegt. Als Folge wird das komplette Eingabefeld gelöscht und der Titel zu Unnamed Document“ geändert. Bevor ein neues Dokument angelegt wird, wird ” der Benutzer gefragt, ob das aktuelle Dokument gespeichert werden soll. Bei Bestätigung wird das Speichern-Fenster zur Auswahl37des Dateipfades und -namen geöffnet. Open: Per Klick auf diesen Button oder mit der Tastenkombination Ctrl + O, wird ein neues Dokument geöffnet. Bevor ein Dokument geöffnet wird, erscheint jedoch die Meldung, ob das aktuelle Dokument gespeichert werden soll. Anschließend kann die zu ladende Datei ausgewählt und ihr Inhalt im Eingabefeld angezeigt werden. Pfad und Dateiname erscheinen weiterhin am Anfang des Eingabefeldes. Zu beachten ist, dass nur txt-Dateien korrekt geladen werden können. Save: Per Klick auf diesen Button oder mit der Tastenkombination Ctrl + S, wird das aktuelle Dokument gespeichert. Speicherpfad und Dateiname entsprechen dabei denen, die am oberen Ende des Eingabefeldes angezeigt werden. Sollte jedoch kein Speicherpfad angegeben worden sein, d.h. ein Unnamed Document“ vorliegen, so wird stattdessen ” Save as“ ausgeführt. ” Save as: Per Klick auf diesen Button oder mit der Tastenkombination Ctrl + Shift + S, wird das aktuelle Dokument an der ausgewählten Stelle unter dem ausgewähltem Namen abgespeichert. Zu beachten ist, dass eine Dateiendung nicht automatisch angehängt wird, d.h. soll eine txt-Datei erstellt werden, muss der Dateiname mit .txt“ enden. ” KAPITEL 2. GRUNDLAGEN 19 Options Compiler output directory: An dieser Stelle lassen sich Pfad und Dateinamen des Compiler-Outputs bestimmen. Über den Compile-Button der Toolbar kann anschliessend die angegebene Datei erzeugt werden. Zu beachten ist, dass das Anhängen der Dateiendung nicht automatisch erfolgt, d.h. zur Erstellung einer mem-Datei muss der Dateiname mit .mem“ enden. ” Compiler output format: Hier kann angegeben werden, ob das Compilat binär oder hexadezimal ausgegeben werden soll. Als Default-Wert ist hexadezimal eingestellt. Commentary Symbol: Hier kann das zu verwendende Kommentarzeichen bestimmt werden. Zur Auswahl stehen Raute und Doppelslash. Der Default-Wert ist auf die Raute eingestellt. Das Symbol ist jeder Zeit änderbar, der Compiler wird jedoch ausschließlich das aktuell als Kommentarsymbol ausgewählte Zeichen erkennen. Desweiteren richtet sich die Funktion des Popup-Menüs zum Setzen und Entfernen von Kommentarzeichen nach der aktuell getroffenen Auswahl. 2.4.2 Toolbar Die in Abbildung 2.5 als 2. gekennzeichnete Toolbar ermöglicht die schnelle und einfache Steuerung des Emulators und Compilers. Compile: Über den Compile-Button wird der eingegebene Programmcode compiliert und am Compiler output directory“ angegebenen Pfad und Namen gespeichert. Soll” ten unter Compiler output directory“ keine Angaben vorliegen, wird der Benutzer ” weiterhin aufgefordert diese Einstellungen zu treffen, bevor anschließend das Compilat erzeugt wird. Sollten im Quellcode Syntaxfehler vorhanden sein, wird stattdessen eine entsprechende Fehlermeldung mit Beschreibung auf der Konsole ausgegeben. Max/Min: Die Anzeige des VGA-Memory Output ist ein wichtiger Bestandteil bei der Arbeit mit grafischen Aspekten. Dennoch kann die Anzeige für eine genaue Überprüfung der einzelnen Pixel zu klein sein. Für einen solchen Fall ist die Anzeige über diesen Knopf vergrößerbar. Zu diesem Zweck werden die interaktive Boardgrafik, die interaktive Anzeige der internen Werte und die Putty-Konsole ausgeblendet und die Anzeige des VGA-Memory Outputs entsprechend vergrößert. Bei erneuter Benutzung dieses Buttons werden die Veränderungen der Anzeige rückgängig gemacht und der alte Zustand wiederhergestellt. Emulation New: Vor dem Start einer Emulation muss diese zunächst initialisiert werden. Zu diesem Zweck wird bei Verwendung dieses Buttons der Assemblercode intern compiliert, wodurch das Zwischencode- und Ausgabefenster aktualisiert, jedoch kein Compilat als externe Datei erzeugt wird. Desweiteren werden alle Werte der interaktiven Anzeige der internen Werte auf ihren default-Zustand zurück gesetzt. Sollten im Code noch Fehler existieren oder kein Code vorhanden sein, so schlägt die Initialisierung fehl und eine entsprechende Fehlermeldung wird auf der Konsole ausgegeben. Da die Emulation auf KAPITEL 2. GRUNDLAGEN 20 dem Stand der Initialisierung arbeitet, werden Veränderungen im Code für die laufende Emulation nicht berücksichtigt. Soll neuer Code oder aktualisierter Code emuliert werden, muss über diesen Button erneut eine Initialisierung vorgenommen werden. Next Stop: Über diesen Button ist exakt die nächste Codezeile ausführbar. Welche dies aktuell ist, gibt der rote Pfeil beim Eingabefeld an. Sollte kein roter Pfeil zu sehen sein, beendet sich die Emulation im Initialisierungszustand, d.h. es wird bei der ersten Codezeile begonnen, da der Programmzähler noch Null ist. Der Next Step“-Button kann ” beliebig oft nacheinander ausgeführt werden. Er bietet sich besonders gut zum Debuggen an, da bei dieser Variante der Emulation in der Konsole zusätzliche Warnhinweise angezeigt werden. Diese Warnhinweise machen auf ein Verhalten aufmerksam, welches zwar keinen Programmfehler hervorruft, jedoch wahrscheinlich nicht beabsichtigt war, beispielsweise bei Werten außerhalb des Wertebereichs. Start: Durch diesen Button wird eine Emulation gestartet, bei welcher der vollständige Code ausgeführt wird. Zu diesem Zweck wird stets bei der ersten Codezeile mit DefaultWerten gestartet. Die Emulation wird gestoppt, sobald ein Breakpoint erreicht wird oder der Benutzer den Stop“-Button betätigt. Sollte der Code nicht initialisiert sein ” oder verändert worden sein, muss die Emulation zunächst über den New“-Button neu ” initialisiert werden, bevor die Start“-Funktion erneut genutzt werden kann. ” Resume: Dieser Button dient dem Fortsetzen einer Emulation. Die Emulation startet an der zuletzt ausgeführten Codezeile oder bei der ersten Codezeile, sollte der Initialisierungszustand vorliegen und läuft bis zum Erreichen eines Breakpoints oder dem Stop“ ” durch den Benutzer. Die Resume“-Funktion läuft dabei nicht auf Default-Werten, son” dern nutzt die zuletzt aktuellen Werte. Als Folge dessen lässt sich eine Emulation über diese Funktion fortsetzen und alle vorher getätigten Änderungen berücksichtigen. Zur effektiven Benutzung wird hierzu ein eventueller Breakpoint in der Codezeile, die als erstes ausgeführt wird, ignoriert. Sollte der Code nicht initialisiert sein oder verändert worden sein, muss die Emulation zunächst über den New“-Button neu initialisiert wer” den, bevor die Resume“-Funktion genutzt werden kann. ” Stop: Dieser Button bricht eine laufende Emulation ab. Sollte keine Emulation laufen, dann bleibt die Aktion funktionslos und es wird eine Meldung auf der Konsole ausgegeben. Beim Beenden der Emulation wird im Eingabefenster ein roter Pfeil auf Höhe der Codezeile gesetzt, die als nächstes auszuführen gewesen wäre. Running: Hinter diesem Schriftzug ist ein roter oder grüner Punkt zu finden, welcher angibt, ob die Emulation läuft oder gestoppt ist. Ein grüner Punkt bedeutet dabei, dass die Emulation arbeitet und ein roter Punkt, dass sie nicht arbeitet. Zu beachten ist die Verwendung von Breakpoints sowie dem Next Step“-Button. Da letzterer nur ” eine Codezeile ausführt und bei Breakpoints in der Regel nur eine begrenzte Anzahl an Codezeilen ausgeführt werden, werden diese in der Praxis so schnell verarbeitet werden, dass ein Umschalten von Rot-Grün-Rot nicht zu erkennen ist, sondern der Punkt konstant auf Rot bleibt. KAPITEL 2. GRUNDLAGEN 2.4.3 21 Eingabefeld, Zwischencode und Ausgabe Der in Abbildung 2.5 als 3. gekennzeichnete Bereich beinhaltet das Eingabefeld, den Zwischencode und die Ausgabe. Über den Reiter am oberen Ende kann jeweils auf den entsprechenden Bereich zugegriffen werden, wobei das Eingabefeld als Default dient. Eingabefeld: Das Eingabefeld dient als Hauptarbeitsbereich. In diesem wird der Quellcode geladen, erstellt und bearbeitet. Am oberen Bereich des Eingabefelds wird weiterhin der Dateipfad und -name zum aktuell geöffneten Dokument angegeben, sofern vorhanden. Im großen, weißen Bereich kann der Code beliebig editiert werden. Eine Hilfe bietet dafür das Popup-Menü. Eine Bedienhilfe des Eingabefeldes stellt dabei die Navigation am linken Rand dar. Sie zeigt nicht nur dynamisch die Nummerierung der Codezeilen an, sondern bietet auch Interaktionsmöglichkeiten. Ein Klick auf die Navigation der entsprechenden Codezeile setzt einen Breakpoint, ein weiterer Klick entfernt diesen wieder. Auf diese Weise können beliebig viele Breakpoints gesetzt werden. Die Navigationszeile beinhaltet weiterhin Raum für die Anzeige des Programmzeigers, also den aktuellen Stand der Emulation. Sollte eine Emulation ausgeführt worden sein, wird ein roter Pfeil die entsprechende Zeile markieren. Zwischencode: Das Zwischencodefenster ist nicht editierbar. Nach Nutzung des Com” pile“-Buttons oder des New“-Buttons wird der Zwischencode erzeugt und angezeigt. ” Der Zwischencode entspricht dem Quellcode, abgesehen davon, dass jegliche Kommentare und leere Bereiche entfernt wurden und IMM-Befehle eingefügt wurden. Zum Debuggen ist ein Blick in den Zwischencode hilfreich, da durch das automatische Einfügen der IMM-Befehle vor bestimmten Operationen Programmierfehler entstehen können, wenn der Benutzer von einem anderen IMM-Wert ausgeht, als dies real der Fall ist. Weiterhin basiert die Emulation auf dem Zwischencode. Ausgabe: Das Ausgabefenster ist nicht editierbar und der Inhalt wird nach Nutzung des ”Compile“-Buttons oder des ”New“-Buttons erzeugt. Das Ausgabefenster zeigt an, wie die exportierte ”.mem“-Datei aussieht oder aussehen würde. 2.4.4 Interaktive Boardgrafik Die interaktive Boardgrafik, in Abbildung 2.5 als 4. gekennzeichnet, ermöglicht die Nutzung der Peripherie des Boards, als wäre dieses real vorhanden. Die LEDs und 7-Segment-Anzeige werden automatisch verändert und eingeschaltet, sobald intern der entsprechende Wert gesetzt ist. Auf diese Weise ist das Ergebnis der Emulation direkt sichtbar. Weiterhin sind sowohl Buttons (unten links) als auch Switches (unten rechts) anklickbar und ermöglichen so eine Beeinflussung der Emulation. Im Code abgefragte Memory-Werte der Buttons und Switches beziehen sich direkt auf den Status der Buttons und Switches dieser Boardgrafik. Ihr Status ist weiterhin auch in der interaktiven Anzeige der internen Werte einsehbar. 2.4.5 VGA-Memory Output Über diese Anzeige, in Abbildung 2.5 als 5. gekennzeichnet, kann das Ausgabebild des VGAMemory betrachtet werden. Die Anzeige zeigt alle physikalischen Pixel des VGA-Memory mit den dazugehörigen Farbwerten sowie den Sprite an, sofern dieser definiert wurde. Während in Realität die Pixel des VGA-Memory ohne vorherige Definition einen beliebigen Farbwert KAPITEL 2. GRUNDLAGEN 22 annehmen ( Pixelchaos“), ist dieses Verhalten durch das bunte Default-Bild des Miniaturbild” schirms dargestellt. Über die interaktive Anzeige der internen Werte können Pixel zusätzlich sowohl gelesen, als auch gesetzt werden. Die Anzeige wird automatisch aktualisiert und auf dem Stand der internen Verarbeitung gehalten. 2.4.6 Interaktive Anzeige interner Werte Diese Anzeige, in Abbildung 2.5 als 6. gekennzeichnet, beinhaltet verschiedene Tabellen, die über die dazugehörigen Reiter wechselbar sind und alle relevanten Werte des internen Boardzustandes besitzen. Das Aussehen der einzelnen Tabellen unterscheidet sich je nach Darstellungsgebiet leicht voneinander, um dadurch zusätzliche Informationen geben zu können. Alle Tabellenbesitzen eine dezimale, hexadezimale und binäre Darstellung des ausgewählten Wertes. Da diese drei Darstellungen in einer Tabellenzeile jeweils den selben Wert abbilden, sind diese folglich äquivalent. Bis auf wenige Ausnahmen (Switches, Buttons, SREG und RXDREG) lassen sich alle Werte editieren (Hinweis: Nur die Werte selbst, nicht die Namen oder sonstigen Informationen), jedoch ausschließlich bei pausierter Emulation. Bei Änderung eines Wertes werden alle dazugehörigen Äquivalenzen aktualisiert und der neue Wert intern verarbeitet. Beispielsweise lassen sich über diese Anzeige LEDs setzen oder der VGA-Memory flushen. Durch Modifikation der Werte bei pausierter Emulation eignet sich diese Anzeige neben dem Lesen vor allem zur Manipulation der Emulation. Alle Tabellen sind bezüglich der Benutzereingaben sicher und erlauben nur eine Eingabe im entsprechenden Format (z.B. Binärzahl in die Tabellenspalte für binäre Werte). In der Status-Konsole wird jedoch immer eine Mitteilung erscheinen, wenn ein Wert erfolgreich gesetzt wurde oder wenn ein Fehler aufgetreten ist. Während alle Tabelleneinträge automatisch beim Pausieren oder Initialisieren einer Emulation aktualisiert werden, sind die VGA-Memory Werte aus Gründen der Performanz nur manuell über den Button am Anfang der Tabelle aktualisierbar. 2.4.7 Putty-Konsole Die unter Abbildung 2.5 als 7. gekennzeichnete Putty-Konsole visualisiert die Kommunikation zwischen dem Board und dem PC. Da diese Komponente für die diesigen Praktikumsaufgaben irrelevant ist, wird diese hier nicht weiter betrachtet. 2.4.8 Status-Konsole Die Status-Konsole, unter Abbildung 2.5 als 8. gekennzeichnet, ist das wichtigste Werkzeug zum Umgang mit Problemen und Fehlermeldungen im Code sowie mit dem Emulator allgemein. Zu fast allen Aktionen werden in dieser Statusmitteilungen ausgegeben, welche entweder eine erfolgreiche Aktion bestätigen oder den Fehler der Nichtausführung näher beschreiben. Allgemeine Fehlermeldungen werden dabei durch drei rote Ausrufezeichen in der Navigationszeile der Konsole gekennzeichnet, während spezielle Codefehler mit einer roten Zahl in der Navigationszeile versehen werden. Diese rote Zahl ist mit der Codezeile des Eingabefeldes gleichzusetzen, in welcher der Fehler aufgetreten ist. Wird eine neue Emulation über den Button New“ initialisiert, wird der Inhalt der Konsole gelöscht. ” Kapitel 3 Anmerkungen und Tipps In einer Aufgabe werden Sie ein Assemblerprogramm schreiben, um das Sprite auf dem Bildschirm ausgeben zu können. Um den Inhalt des Sprites zu definieren, sollten Sie den Bitvektor für jede Zeile hexadezimal kodieren und die Zahl als Immediate in ein Register laden. Der Registerinhalt kann dann an die entsprechende Speicherstelle geschrieben werden. Für einen Vektor sieht geht dies wie folgt: # Hexadezimalekodierung des e r s t e n Vektors LDI R2 , 0xFFF0 # R2 e n t h ä l t A d r e s s e d e r e r s t e n S p r i t e z e i l e , # R3 d i e Bit−Kodierung d e r S p r i t e −Z e i l e ST R3 , R2 Bei der Aufgabenbearbeitung gehen Sie davon aus, dass das Sprite nur um ein Pixel pro Richtungseingabe verschoben wird. Bei einem vollständigen FLUSH des Bildspeichers dauert es ca 3,1 Millisekunden bis der Bildspeicher wieder adressiert werden kann. Innerhalb dieser Zeitspanne ist die CPU jedoch in der Lage 18.563 aufeinanderfolgende Instruktionen auszuführen. Somit sollte nach jeder FLUSH-ANWEISUNG (CLRV) ein entsprechender Warteschleifenzyklus eingelegt werden, bevor die nächste Instruktion des eigentlichen Programms ausgeführt werden soll. Ansonsten besteht die Gefahr der inkorrekten Ausführung von Bilsdpeicheroperationen innerhalb des Programms. Programme, die selbst keine Endlosschleife enthalten, sollten am Programmende mit einer solchen abgeschlossen werden, damit das Programm nur genau einmal ausgeführt wird. # Sprungmarke MARKE: # Sprung zum S c h l e i f e n a n f a n g JMP MARKE In der Assemblersprache unserer Praktikums-CPU muss vor jedem bedingtem Sprungbefehl der Compare Befehl ausgeführt werden. 23 KAPITEL 3. ANMERKUNGEN UND TIPPS 24 Kommentare werden bei der Praktikums-CPU mit dem Symbol # eingeleitet. Das FPGA-Board bietet neben dem FPGA-Chip zusätzliche IO-Peripherie (Schalter,LEDS, Buttons, etc.), auf welche die Praktikums-CPU wie folgt Zugriff hat: Im Datenspeicher der CPU ist für jede IO-Komponente eine Adresse reserviert, in der deren aktueller Wert/Zustand abgelegt ist. Die entsprechenden Adressen sind in der nachfolgenden Memory-Map des Datenspeichers aufgelistet. Memory-Map des Datenspeichers: Adresse (HEX) 0x0000 0x0001 0x0002 0x0003 0x0007 0x0004 - 0x07FF 0x0800 - 0xFFFF Belegung 7-Segment Anzeige LEDS SWITCHES BUTTONS VGA-ADDRESS-MODE[1 = indirekt,2 = flush] USER DATA nicht Verfügbar Die genaue Bitbelegung der Adressen ist wie folgt: 7-Segment Anzeige [10:0] AN3 AN2 AN1 AN0 LEDS [7:0] LD7 LD6 LD5 Switches [7:0] SW7 SW6 SW5 Buttons [3:0] BT3 BT2 BT1 LD4 SW4 BT0 g f LD3 SW3 e d LD2 c b LD1 SW2 a LD0 SW1 SW0 Kapitel 4 Vorbereitungsaufgaben Mit den folgenden Aufgaben werden Sie die Assembler-Programme vorbereiten, die Sie im Praktikum fertig implementieren und testen werden. Es ist nicht notwendig, allerdings hilfreich, den Assembler-Code schon komplett zu erstellen. Definieren Sie für alle zu schreibenden Programme die interne Struktur (Pesudo-Code) und den Ablaufplan. Da einige der Programme im späteren Verlauf mit anderen kombiniert werden, sollten Sie darauf achten möglichst sauber“ zu Arbeiten, was die Verwendung von Registern betrifft. Dokumentieren Sie deshalb ” genau welche Register wo und wie verwendet werden, um spätere Inkosistenzen zur Laufzeit zu vermeiden. Aufgabe 1. Verinnerlichen Sie die grundlegenden Konzepte der Assemblerprogrammierung, so dass Sie in der Lage sind, komplexere Programme mit Schleifen, Abfragen und arithmetischen Ausdrücken zu implementieren. Hierzu sollten Ihnen die in Abschnitt 2.2.7 vorgestellten Konstrukte genügen. Des Weiteren sollten Sie sich mit dem Befehlssatz der PraktikumsCPU vetraut machen. Lesen Sie hierzu die zur Verfügung gestellte Befehlstabelle (Befehlstabelle.pdf) und versuchen Sie dabei die Funktion und Anwendung der einzelnen Befehle zu verstehen. Insbesondere die der Sprung-, Daten- und Transferbefehle. Aufgabe 2. Stellen Sie den Pseudo-Assembler-Code für den in Abschnitt 2.1 vorgestellten PseudoZufallszahlengenerator auf. Verwenden Sie hierfür den vorgestellten Ansatz mittels eines linear rückgekoppelten Schieberegisters. Verwenden Sie dabei die volle 16 Bit Registerlänge als Schieberegister um damit Zufallszahlen mit einer möglichst langen Periode zu generieren. Für die Einkopplungsstellen sollten zwei XOR-Operationen an jeweils unterschiedlichen Bitpositionen genügen. Des Weiteren soll dem Generator eine Parameterübergabe möglich sein, so dass die obere Grenze des Zahlenintervalls dynamisch zur Laufzeit übergeben werden kann. Die Berechnung einer Pseudo-Zufallszahl dauert somit solange an, bis eine gewünschte Zahl in dem entsprechenden Intervall berechnet wurde. Die Rückgabe der Zufallszahl erfolgt ebenfalls in einem von Ihnen frei wählbaren Register. Aufgabe 3. Überlegen Sie sich den Ablaufplan eines Assemblerprogrammes, welches ein Quadrat mit der Größe 10 x 10 Pixel in einer beliebigen Farbe an einer beliebigen Stelle des Bildschirms zeichnet. Der Code sollte dabei so modular gehalten werden, dass dieser problemlos später in einem anderen Programm wieder verwendet werden kann. Verwenden Sie daher ein Register, welches als Übergabeparameter für die Ursprungskoordinate des Quadrats dient. Der Ursprung des Quadrats soll seine oberen linke Ecke sein. 25 KAPITEL 4. VORBEREITUNGSAUFGABEN 26 Aufgabe 4. Bereiten Sie ein Assemblerprogramm vor, das ein Sprite Ihrer Wahl an einer beliebigen Stelle des Bildschirms ausgibt. Überlegen Sie sich zunächst wie es aussehen soll. Ein Sprite besteht aus 8 Vektoren, die jeweils 8 Bit breit sind. Ihnen steht somit ein 8x8 Feld zur Verfügung. Füllen Sie das Feld mit Einsen und Nullen, entsprechend dem gewünschten Aussehen. Wenn im Vektor eine 0 vorkommt, dann ist diese Stelle transparent, sonst weiß. Um das Sprite letztendlich zeichnen zu können, müssen Sie es wie in Abschnitt 3 gezeigt, zeilenweise als Konstante in Register laden und an die entsprechenden Spritespeicherstellen speichern. Aufgabe 5. Überlegen Sie sich eine Fusion der beiden Assemblerprogramme aus Aufgabe 2. und 3. Das resultierende Programm sollte in der Lage sein, ein Quadrat mit 10 x 10 Pixeln an einer durch den Pseudo-Zufallsgenerator bestimmten X,Y-Koordinate des Bildschirms zu zeichnen. Berücksichtigen Sie als zulässigen Wertebereich der Ursprunkskoordinate des Quadrats die obere linke Ecke mit X = [0, 150] und Y = [0, 110]. Aufgabe 6. Wie könnte ein Assemblerprogramm aussehen, mit dem man das Sprite mit Hilfe der Tastereingaben der Nexys-Platine am Bildschirm in vier Richtungen verschieben kann, so dass es auf eine zufällig positionierte Hitbox (Quadrat 10 x 10 Pixeln) hinzu bewegt werden kann. Ist das Sprite vollständig innerhalb der Hitbox-Hülle positioniert, so wird die Box gelöscht, an einer zufälligen Stelle des Bildschirms neu gezeichnet und das Spiel“ beginnt von vorne. Der jeweils entsprechende Taster soll dabei das Sprite ” nach links, rechts, oben oder unten bewegen. Sie brauchen nicht zu berücksichtigen, dass mehr als einer der Taster gleichzeitig gedrückt wird. Als Kollisionserkennung, bzw. Hitbox-Überdeckung durch den Sprite sollte der folgende Koordinatenabgleich nach jeder eigehenden Bewegung ausreichen: (X, Y )Sprite == (X + 1, Y + 1)Hitbox Für die spätere Programmumsetzung soll der folgende Automat mit insgesamt drei Zuständen dienen. Abbildung 4.1: Hit The Box“ Automaten Programmablauf ” KAPITEL 4. VORBEREITUNGSAUFGABEN 27 Startzustand 00: Initialisierung Da nach dem Einschalten der CPU zunächst einmal alle internen Werte und Zustände mit Null initialisert sind, müssen zunächst einmal die verschiedenen Konfigurationen, wie Adressmodi, Spritedeklarationen und andere Default-Initialiserungen durchgeführt werden. Hierzu gehört das Laden und Speichern des Sprites sowie die Definition der Positionskoordinaten beider Objekte. Damit die Hitbox und das Sprite erstmalig von der VGA-Hardware korrekt gezeichnet werden können, müssen die Koordinaten beider Objekte mit der obigen Bedingung gemäß einer vollständigen Überdeckung initialisiert werden, d.h. (X, Y )Sprite = (X + 1, Y + 1)Hitbox . Anschließend wird ohne jedwede Eingabe in den Folgezustand 01 gewechselt. Nach dem Verlassen ist ein erneuter Wechsel in den Startzustand nicht mehr möglich. Zustand 01: Kollisionserkennung In diesem Zustand erfolgt die Kollisions-/Überdeckungserkennung. Wird eine Kollision mittels Koordinatenabgleich erkannt, so wird der aktuelle Bildschirm geflusht (gelöscht), die Koordinate der Hitbox mittels Pseudo-Zufallszahlengenerator neu berechnet und anschließend an einer zufälligen Position neu gezeichnet. Da es sich bei der Hitbox nicht um ein Sprite-Objekt handelt, ist ein vollständiges neuzeichnen der Hitbox in diesem Zustand notwendig. Nach dem Beenden dieser Routine oder im Falle einer Nichtüberdeckung wird in den Folgezustand 10 gewechselt. Zustand 10: Eingabekontrolle Im Zustand 10 erfolgt die Überwachung der Eingabeperipherie. Hierzu werden die Taster mittels Polling“ permanent auf Knopfdruck überprüft. Sofern das Drücken eines ” Tasters erkannt wurde, wird die Spritekoordinate in der entsprechenden Richtung manipuliert. Da es sich um ein Sprite handelt, welches mit dem aktuellen Inhalt des Bildspeichers lediglich überblendet wird, entfällt an dieser Stelle das Neuzeichnen. Anschließend wird in den Zustand 01 gewechselt. Wird bei keinem der Taster ein Knopfdruck erkannt, verbleibt der Automat in diesem Zustand (10). Kapitel 5 Praktikumsaufgaben Für die folgenden Aufgaben entpacken Sie die ZIP-Datei für Versuch 5 von der Praktikumswebsite und erstellen ein neues Projekt mit allen darin enthaltenen VHDL-Dateien sowie mit den Dateien irom.bmm“ und irom.mem“. Verwenden Sie als Editor, Compiler und Debugger ” ” das zur Verfügung gestellte TINY RISC STUDIO v1.0“. ” Aufgabe 1. Implementieren Sie den Pseudo-Zufallszahlengenerator aus Vorbereitungsaufgabe 2. Verwenden Sie für Rück- und Übergabeparameter entsprechende Register der CPU. Anschließend simulieren Sie Ihr Programm zunächst mit dem Debugger um logische Fehler in der Programmstruktur zu erkennen. Aufgabe 2. Implementieren Sie Ihr Assemblerprogramm zum Zeichnen eines Quadrats mit 10 x 10 Pixeln aus Vorbereitungsaufgabe 3. Simulieren Sie es zunächst mit Hilfe des Debuggers und testen Sie es abschließend auf der FPGA-Platine. Aufgabe 3. Stellen Sie Ihr Assemblerprogramm zum Zeichnen eines Sprites aus Vorbereitungsaufgabe 4 fertig und testen Sie es anschließend auf der FPGA-Platine. Aufgabe 4. Hit The Box“: Implementieren Sie den in Vorbereitungsaufgabe 6 vorgestellten Auto” maten vollständig in Assembler. Nutzen Sie dazu Ihre bereits vorhandenen Programme aus den Aufgaben 1, 2 und 3 und binden Sie diese in den jeweiligen entsprechenden Zuständen ein. Um Ihnen den Einstieg zu erleichtern, haben wir eine Datei mit dem Namen HitTheBox Aufgabe 4.txt“ vorbereitet, in der Sie den Code des Automaten ” vervollständigen müssen. Diese beinhaltet bereits die Struktur des Automaten (Sprungmarken der Zustände und deren Reihenfolge im Programm) sowie den Code für den Zustand 10 (Eingabeüberprüfung der Taster). Die zu editierenden Stellen sind im Code mit TODO“ und einem zugehörigen Hinweis gekennzeichnet. Nachdem Sie den Auto” maten vollständig implementiert haben, simulieren Sie ihn zunächst mit dem Debugger. Abschließend testen Sie ihn auf der FPGA-Platine. 28