3D-Grafikprojekt

Karsten Becker und Tim Böscke

Intro

Contents

Ziele

Unser Ziel ist es für dieses System on a Chip (SOC) Seminar einen 3D-Core für einen Xilinx-FPGA zu schreiben. Dieser sollte aus einem in C programmierbaren Prozessor und einer 3D-Einheit bestehen.

Der Prozessor hat dabei die Aufgabe die Dreiecksdaten zu berechnen und sie an die 3D-Einheit weiterzugeben. Diese sorgt dann dafür, dass die Dreiecke gezeichnet und texturiert werden.

Zur Realisierung des Prozessorkerns wurde der OpenIP-core Plasma von openCores.org benutzt. Zur Realisierung steht uns das XSV Board von Xess zur Verfügung.

Übersicht

Es gab viele Überlegungen wie die Gesamtarchitektur aussehen sollte. Das grösste Problem bei der Realisierung waren die konkurrierenden Speicherzugriffe. Jede Einheit benötigt Speicher. Die erste Überlegung war für alle Einheiten einen eigenen Speicher auf dem Board vorzusehen. Das brachte jedoch Probleme mit der Anzahl der verfügbaren Pins und der Frage wie denn Daten zwischen zwei Modulen hätten ausgetauscht werden sollen. Deshalb, und weil man einfacher auf andere Speichertypen adaptieren kann, haben wir uns für einen zentralen DMA-Controller entschieden, an dem alle Module als Master dranhängen.

OverView

Die Module

Alle Module verfügen über eine direkte oder indirekte Anbindung über den DMA-Controller an den Speicher.

CPU-Modul

Bei dem CPU-Modul handelt es sich um einen Mips-Prozessor, der von Opencores.org stammt und dort Plasma heißt. Er versteht die meisten Mips-1 Befehle.

CPU-Interface

Das CPU-Interface beinhaltet den Daten- und Befehlscache und stellt außerdem den Zugriff auf die anderen Module per Memory-mapping bereit. Außerdem enthält es noch das Bootstrap.

3D-Control

3D-Control bekommt seine Konfigurationsdaten über den 3D-Bus. Seine Aufgabe ist es die Trapezoide Zeilenweise zu zerlegen und die gewonnen Parameter an den Rasterizer zu übergeben.

Rasterizer

Der Rasterizer füllt die ihm übergebenen Parameter mit einer Textur und Schattierung.

VGA-Modul

Das VGA-Modul bekommt die Startadresse des Framebuffers als Parameter und sorgt für eine dem Monitor angenehme Umsetzung des Pixels. Außerdem sind hier die verschiedenen Dithering Methoden und der XOR-Filler integriert.

DMA-Modul

Das DMA-Modul ist die zentrale Stelle zum Speicher. Jedes Modul, mit Ausnahme von 3D-Control, ist über mindestens einen Bus daran gekoppelt. Hier ist das RAM direkt angebunden.

UART-Modul

Das UART Modul dient der Übertragung von Daten in beliebige Bereiche des Speichers.

Die CPU

Bei der CPU handelt es sich um den Plasma Core, der auf www.opencores.org verfügbar ist. Er versteht die meisten Mips-1 Befehle. Außerdem wird eine Toolchain mitgeliefert, mit der man einfache Programme compilieren kann. Ich erspare mir hier die detailierte Dokumentation der MIPS-1 Architektur, da sie zum einen auf der Homepage von MIPS frei verfügbar ist und außerdem auch von einem Partnerprojekt nochmals detailierter beschrieben wird.

Gefundene Bugs

Bei dem Prozessor handelte es sich leider um einen nicht besonders gut getesteten Core. Zwei Bugs waren besonders schwerwiegend.

Operand bypassing Logik

Der Operand Bypass wird benötigt, wenn das Resultat einer ALU-Operation direkt im nächsten Befehl wieder verwendet wird. Da das Schreiben/Lesen aus dem Registerfile durch das Pipelining um einen Taktzyklus versetzt erfolgt, wird für diesen Fall eine Sonderbehandlung benötigt. Diese hat im Zusammenhang mit dem von uns benutzen "`Pause"' nicht richtig funktioniert.

Falsche Umsetzung des Stack Pointers

Sehr interessante Fehler besorgte uns die etwas merkwürdige Toolchain. Anstatt komplett die GNU-Toolchain zu verwenden, setzte der Programmierer auf ein selbstgeschriebenes Tool, das an einer festen Stelle im Elf-File den Operator und Argument ersetzte. Leider schienen auf dem Prozessor aber noch nie größere Programme gelaufen zu sein, denn die ersten 16-Bit des Stack-Pointers (SP) wurden nicht initialisiert. Das führte dazu, dass der SP mitten in wichtigen Programmteilen saß und so zu unkontrollierbaren Abstürzen führte.

Wie benutze ich den Prozessor

Der Prozessor selber muß sicherlich erst einmal in das VHDL Design eingebaut werden. Die Schnittstellen sind relativ gut erklärt. Es empfiehlt sich jedoch das Registerfile von den Generics zu befreien und durch eine dem FPGA entsprechende Implementation zu ersetzen. Außerdem hat sich die Verwendung des DMAs als sehr nützlich erwiesen, so dass man erwägen sollte, sie auch bei eigenen Designs weiter zu verwenden. Das CPU-Interface sorgt außerdem dafür, dass man den Prozessor komplett Asynchron zum System-Bus takten kann, was bei der nicht gerade überragenden Geschwindigkeit des Prozessors von Vorteil ist.

Die neue Toolchain für das Hauptprogramm

Um den oben genannten Bug mit der alten Toolchain zu umgehen, wurde eine Neue entwickelt, die komplett auf den etablierten GNU-Tools aufsetzt. Die Benutzung der neuen Toolchain wird ausführlich in QuickOverview.html beschrieben.

Um die Toolchain nutzen zu können, muss man sich einen Crosscompiler für das mips-elf-target bauen. Zum Compilieren benötigt man 3 Dateien die unter Umständen etwas angepasst werden müssen.

Um dem Problem der fehlerhaften Toolchain zu umgehen, wurde ein Linkerscript geschrieben, das alle zur Initialisierung nötigen Symbole setzt. Dazu wurden am Anfang erst einmal die Speicherbereiche definiert. In unserem Fall begann das Programm memory bei der Adresse 0x800 und war 0x3F800 lang.

Die nachfolgenden Symboldefinitionen sind mit Sicherheit nicht der eleganteste Weg, funktionieren aber wie sie sollen. Man wird sie später mit nm wiederfinden. Der Sinn dieser Symbole liegt darin, Zahlenwerte bereitzustellen, auf die man im boot.asm später wieder zugreifen kann. Der Linker ersetzt nämlich die im .asm vorhandenen Symbole durch den entsprechenden Zahlenwert.

Das Makefile ist größtenteil generisch. Man benötigt nur die zu kompilierenden c Dateien unter SOURCES.c anzuhängen und evtl. vorhandene Assemblerstücke bei SOURCES.asm. Hierbei muss dringend beachtet werden, dass boot.asm immer das erste Assemblerfile ist, weil hier wichtige Variablen und Speicherbereich initialisiert werden.

Die Toolchain für das Bootstrap

Die Toolchain für das Bootstrap sieht etwas anders aus. Um das Bootstrap nicht immer neu einzukompilieren wird ein Tool genommen, das den Speicherinhalt des Bootstraps direkt in den Configurationsbitstream einfügt. Allerdings wird hier noch das etwas abenteuerliche convert.exe benutzt, da die Datei bootstrap_be.txt benötigt wird, die von diesem Programm erzeugt wird.

Zuerst wird das Programm mit dem Befehl Makefile kompiliert und anschließend wir das Bootstrap mit dem Befehl Makebit in den Bitstream eingefügt. Dazu werden die Dateien block_bd.bmm und System.bit benötigt, die als Compilat vom ISE abfallen. Diese müssen also nach jedem Erzeugen des Programmingfiles neu in das Verzeichnis, indem Makebit liegt, kopiert werden.

Das CPU-Interface

Da wir mit der CPU nicht auf einen angemessenen Takt hoch kamen, mussten wir uns Möglichkeiten überlegen wie wir die Effizienz der CPU anheben konnten. Dazu konstruierten wir ein CPU-Interface, das die CPU komplett über Caches und FIFOs von der restlichen Clockdomain abgrenzt. Die CPU kann also komplett Asynchron getaktet werden. Außerdem bot sich dadurch eine gute Stelle, in der wir die verschiedenen Busse memory mapped anbringen konnten.

Damit die CPU weiterarbeiten konnte, während das 3D-Modul die Daten gezeichnet hatte, gab es einen FIFO der von der CPU beschrieben und von dem 3D-Modul gelesen wurde. Der von uns eingesetzte Cache erreichte einen Wirkungsgrad von über 80%,was den DMA doch spürbar entlastete und so mehr Speicherzugriffe für den Rest der Hardware bereitstanden.

Desweiteren enthält das CPU-Interface auch noch das Bootstrap, das immer als erstes ausgeführt wird, bevor es dann auf User-Interaktion in den echten Speicher springt.

Das 3D-Modul

Das 3D-Modul teilt sich in zwei Teile auf: Dem 3D-Control und dem Rasterizer.

3D-Control

3D-Control zerlegt die vom CPU-Treiber übergebenen Trapezoide weiter in einzelne Zeilen. Da diese Aufgabe relativ Zeitkritisch ist, wird sie von einer dedizierten Hardware übernommen, die parallel zur Haupt-CPU arbeiten kann. Die Positions- und Texturdaten der Zeilen werden über einen eigenen Bus direkt an den Rasterizer übergeben. Auch die sogenannte "`Subtexelkorrektur"' erfolgt in dieser Einheit.

Die 3D-Control Einheit wurde durch einen kleinen RISC-Prozessor in Harvard- Architektur implementiert. Diese CPU verfügt über 16 Register und ist in der Lage, Additionen, Multiplikationen, Sprünge und I/O Befehle auszu- führen. Die Programmdaten sind in einem kleinen ROM mit max. 32 Einträgen abgelegt.

Pro Trapezoidzeile müssen ca. 7 Additionen, 2 Multiplikationen und diverse weitere Befehle ausgeführt werden.

Rasterizer

Der Rasterizer bekommt Positions- und Steigungsdaten von der 3D-Control Einheit.Er interpoliert die Texturkoordinaten, liest die Texturdaten aus dem Speicher und schreibt die einzelnen Pixel an die angegebenen Positionen des Bildschirmspeichers.

Nach dem Zeichnen einer Zeile geht der Rasterizer in den Idle-Zustand und erwartet neue Daten.

Das VGA-Modul

Dieses Modul sorgt dafür, dass die Daten, die im Framebuffer gespeichert sind, auf dem Monitor dargestellt werden. Dazu liest es die Daten in einen Buffer während der Blankzeit. Aus diesem Buffer erzeugt das Modul dann das korrekte timing für den Monitor. Dabei werden die 16-Bit Farbwerte in die 8-Bit Werte umgewandelt, die später auf dem Monitor zu sehen sind. Damit es nicht allzu kümmerlich aussieht, wird ein Dithering der Farben vorgenommen. Dazu sind zwei Verfahren implementiert, die sich über Steuerleitungen ein und ausschalten lassen.

Außerdem enthält das VGA-Modul auch den XOR-Filler, der für die einfachste Art der Texturierung vorgesehen ist. Bei dem Xorfiller werden die Pixel mit dem vorherigen Pixel gexored und auf diese Weise ganze Flächen zwischen Linien mit einer Farbe gefüllt. Der Nachteil dieses Verfahrens ist eindeutig. Linien, die sich eigentlich auf der Objekt-Hinterseite befinden, werden auch verxored und so entstehen Farbwechsel in Bereichen, in denen keine sein sollten. Zudem lassen sich nur einfarbige Flächen darstellen.

Der DMA-Controller

Vorraussetzung

Um einen DMA-Controller implementieren zu können, muss man sich zunächst um die Strukturierung des Speichers Gedanken machen. Auf dem demnächst verfügbaren Prototypenboard wäre ein DDR-Speicher ansprechbar. Leider wissen wir aber zu wenig über die dazu nötigen IP-Cores. Außerdem hat der DDR-Speicher zwar einen enorm hohen Datendurchsatz, den er aber nur bei großen Datenmengen erreichen kann. Bei kleineren Datenmengen ist die Latenz zu hoch um einen einigermaßen sinnvollen Durchsatz zu erreichen. Hinzukommt auch, dass der DDR-Speicher ganz anders angesprochen wird als der SRAM-Speicher, der auf dem "`alten"' Prototypenboard verfügbar ist.

Die sinnvollste Variante ist also ein SRAM zu benutzen, da die Programmierung ausgesprochen einfach ist und es sich gut mit kleinen Datenmengen versteht. Außerdem ist schon SRAM auf dem Board vorhanden. Die Speicheraufteilung ist 2*(16*512).

Bandbreitenschätzung

Um herauszufinden ob wir über eine Priorisierungstabelle oder einen Zeitmultiplex den Zugriff aufs Ram verteilen, haben wir probiert den Bandbreitenbedarf der einzelnen Module zu schätzen.

CPU-Modul

Bei dem CPU-Modul können wir mit einem Takt von vorraussichtlich 33MHz rechnen. In jedem Takt werden Daten gelesen und geschrieben. Wir würden hier also 132MByte/sek (32-Bit Zugriff)brauchen. Allerdings können wir die CPU zu jedem beliebigen Zeitpunkt anhalten, so das wir keinen konstanten Datenstrom brauchen.

3D-Modul (Textur)

Das Textur-Modul benötigt eigentlich einen konstanten Datenstrom. Man kann hier jedoch hervorragend Cachen. Die zu erwartende Datenrate wir gleich dem Takt sein. Das Modul hängt von den Daten des Pixel-Moduls ab. Wenn das Pixel-Modul keine Daten mehr liefert, benötigt auch das Textur-Modul keine Daten mehr.

3D-Modul (Pixel)

Das Pixel-Modul braucht auch einen konstanten Datenstrom; Allerdings nur solange wie es Daten von der CPU zu bearbeiten hat.

VGA-Modul

Dieses Modul ist das Kritischste, denn es benötigt eine hohe Bandbreite von 1024*393*61=23,41MByte/sek. Dadurch, dass es einen integrierten Cache von 1024 Byte hat, greift es nur alle 16000 Taktzyklen auf den Speicher zu. Allerdings müssen diese Daten dann auch sehr schnell gelesen werden, da sie in der Blank-Zeit des Monitors gelesen werden.

Ergebnis

Wir sind zu dem Ergebnis gekommen, dass eine Verteilung über Prioritäten am Einfachsten ist. Auf Grund der Abhängigkeiten bekommt die CPU die geringste Priorität, da ihr Aussetzen das Terminieren der anderen Module zur Folge hat und so irgendwann dann doch wieder zum Zuge kommt. Die einzige vergeudete Zeit entsteht, wenn die CPU gerade erst den nächsten Befehl lesen konnte und noch keine neuen Dreiecksdaten an die 3D-Einheit weitergeben konnte. Ein Zeit-Multiplex hätte den Nachteil, dass man sehr viel Zeit mit der Versorgung des VGA-Moduls vergeudet, auch wenn das nicht nötig gewesen wäre. Insgesamt ist der Vorteil der Priorisierung so einleuchtend, dass wir uns dafür entschieden haben.

Der Bus

Der Bus besteht aus 32 Daten-,32 Adress-,3 Burst- sowie einer Request-, einer Clock-, einer Busfree- und einer Acknowledgeleitung. Alle Einheiten sind über solch einen Bus mit dem DMA-Controller verbunden. Der DMA fungiert immer als Slave. Die vorgesehenen Burstleitungen sind derzeit nicht in Benutzung und sollten immer auf 0 stehen.

Wenn jetzt ein Master Daten aus dem RAM lesen will, muss er RW auf low, Request auf high und an dem Adressbus die Adresse anlegen. Es kann dann 1 bis n Zyklen dauern bis der DMA-Controller den Request behandelt. Wenn die Daten aus dem Ram gelesen sind, setzt der DMA-Controller Acknowledge auf high und im nächsten Takt können die Daten dann gelesen werden. Im selbem Takt muss der Master request auf low setzen, damit Daten nicht mehrfach gelesen werden. BusFree bestätigt, dass der Aktuelle Request angenommen wurde. Im Zyklus, nach dem BusFree high ging, kann ein neuer Request gestartet werden.

Das Schreiben erfolgt analog, nur das RW auf high gesetzt wird und dann auch die zu schreibenden Daten an DataOut anliegen müssen.

Funktionsweise des Controllers

Der Controller entscheidet anhand einer Priorisierung der Master über den nächsten Request. Die Module mit der kleinsten Priorität werden immer zuerst behandelt.

Priorität Modul Begründung
1 VGA Ist auf einen konstanten Strom von Daten angewiesen.
2 Texture Nur texturierte Dreiecke kann man sehen.
3 Pixel Wird dringend benötigt um Dreiecke zeichnen zu können.
4 CPU Kann man problemlos anhalten.

Man könnte jetzt denken das die CPU zu wenig Zugriffe zugeteilt bekommt. Aber wenn die CPU keine neuen Daten bekommt, können auch die anderen Module keine neuen Daten bekommen und werden deshalb von selbst terminieren.

Schnittstelledeklaration

Die Schnittstelle eines Masters ist in VHDL folgendermaßen deklariert:
   adr_mastername       : in std_logic_vector(31 downto 0);
datain_mastername : out std_logic_vector(31 downto 0);
dataout_mastername : in std_logic_vector(31 downto 0);
req_mastername : in std_logic;
ack_mastername : out std_logic;
bfree_mastername : out std_logic;
rw_mastername : in std_logic;
burst_mastername : in std_logic_vector(1 downto 0);

Das UART Modul

Am Anfang haben wir darüber nachgedacht, die UART über "`Register"' an die CPU anzubinden und ein Programm die Daten von der UART in den Speicher kopieren zu lassen. Wenn die CPU wider erwarten nicht funktioniert, hat das jedoch den Nachteil, dass das Hauptprogramm nicht hochgeladen werden kann und so das debuggen erschwert wird. Deshalb haben wir uns entschieden, die UART direkt an den DMA anzuschließen.

Anfangs hatten wir sehr große Probleme mit der fehlerfreien Übertragung der Daten, deshalb haben wir uns entschlossen für die Verifikation eine CRC32 anzuhängen. Die ankommenden Datenpakete bestehen also derzeit aus 16 Datenbytes und 4 CRC bytes. Um auch noch andere Befehle über die UART senden zu können, wird außerdem bei dem ersten Byte unterschieden, ob es sich um Daten oder einen Befehl handelt. Alle Bytes gehen in die CRC32 ein.

Das Protokoll ist denkbar einfach. Wenn das erste Byte den Wert 0xFF hat, handelt es sich um einen Adressierungsbefehl. Die Nachfolgenden vier Bytes werden dann als Addresse verstanden. Bei der Adresse werden die untersten 2 bit weggelassen, weil sie nur unalignte Zugriffe erzeugen würden.

Wenn jedoch das erste Byte 0xAA ist, werden die Daten in den aufsteigenden Adressbereich geschrieben. Nach jeder Übertragung wird kontrolliert, ob es einen Fehler zwischen übertragener und berechneter CRC gab. Wenn es einen Unterschied gab, wird ein Byte mit dem Wert 0x48 gesendet. Ansonsten wird 0x45 gesendet wenn Daten gesendet wurden und 0x46 wenn ein Adressierungsbefehl gesendet wurde.

Bei einer nicht richtigen CRC wird die Adresse wieder auf die Stelle zurückgesetzt, die als Letztes gültig übertragen wurden. Die CRC32 ist die Gleiche, die auch bei ZIP Archiven benutzt wird. Für die Implementierung wurde der CRC Generator von http://www.easics.com benutzt. Um jedoch das gleiche Ergebnis zu erzielen, müssen die Daten "`verdreht"' werden. Man muß also aus der Variable D:std_logic_vector(7 downto 0) ein std_logic_vector(0 to 7); machen. Die Ausgangssignale bleiben unverändert.

Der Emulator

Weil es vorkam, dass unsere geschriebene Software sich nicht immer so verhalten hat wie wir es von ihr erwartet haben, wurde ein Emulator geschrieben, der die CPU nachbildet. Dieser allokiert 2 MB Speicher und die Daten die normalerweise per UART eingespielt würden werden aus einer Datei gelesen und anschließend alles so ausgeführt wie es auch in unserem Projekt gemacht wurde. Um den Prozessor zu simulieren, haben wir die in VHDL beschriebenen State Machines in C++-Quellcode umgesetzt. Auf diese Weise konnten wir einige der Software Fehler schon beheben, bevor die Hardware überhaupt richtig lief.

Natürlich musste auch die dedizierte Hardware nachgebildet werden. Dazu wird nach jedem simulierten Zugriff auf ein entsprechendes Register, der sonst in Hardware in einen Fifo geschoben würde, ausgeführt. Hier liegt dann auch eines der Probleme, die man im Emulator nicht nachbilden kann. Nämlich die Bestimmung der Zeitpunkte wann die Operationen der restlichen Einheiten fertig sind, und wann welche Einheit einen Speicherzugriff bekommt.

Zur Visualisierung des Framebuffers wurde die GDI Library benutzt.

About this document ...

3D-Grafikprojekt

This document was generated using the LaTeX2HTML translator Version 2K.1beta (1.48)

Copyright © 1993, 1994, 1995, 1996, Nikos Drakos, Computer Based Learning Unit, University of Leeds.
Copyright © 1997, 1998, 1999, Ross Moore, Mathematics Department, Macquarie University, Sydney.

The command line arguments were:
latex2html 3dProjektKorr.tex -split 0 -ps_images

The translation was initiated by root on 2003-07-16