Hallo Leute!
Ich versuche mich gerade daran, eine kleine CPU mit VHDL zu entwickeln.
Leider habe ich habe zugegebenermaßen nach meinen Maßstäben überhaupt
keine Ahnung von der Materie, ich nutze diese Spielerei gerade dazu, mir
ein mentales Modell der Problemstellung zu bilden, um dann beim Lesen
von einschlägiger Literatur einen besseren Kontext zu haben.
Der Ansatz, den ich gerade verfolge, besitzt sowieso schon einige
Probleme, aber auch wenn ich die erstmal so hinnehme (ein schlechtes
Ergebnis macht glücklicher als gar keines =)), habe ich gerade ein arges
umsetzungstechnisches Problem.
Es ist ja nun so, dass gewisse ALU-Blöcke auch mal mehr als einen Takt
brauchen können, um ein Ergebnis auszuspucken. Daher bin ich der
Meinung, dass auch ein anständiger CPU-Ansatz das Problem hat, dass die
Schreibleistung in den Registersatz größer sein muss als 1 Element pro
Takzyklus, denn ansonsten würde es doch zu schlimmen "Hiccups" kommen
können, wenn mehrere unabhängige ALU-Einheiten gleichzeitig fertig
werden und alles darauf wartet, dass die Ergebnisse zurück in die
Zielregister geschrieben werden.
Eine wohlgeformte Befehlsausführung wird wohl selbstverständlich auch
garantieren, dass keinesfalls mehrere dieser Ergebnisse in demselben
Register landen sollen.
Nun setze ich in meinem Möchtegern-Design (für Nexys3 - Spartan6)
Block-RAM für den Registersatz (Die erste Umsetzung mit Logikgattern
führte schnell zu der Erkenntnis, dass man das wohl eher sein lassen
sollte =)).
Nun meine Frage, falls nicht schon die Prämissen wackeln: Gibt es einen
Weg, solche konkurrierenden Schreibzugriffe auf Block-RAM
ressourcenschonend umzusetzen, wenn man nur garantiert, dass niemals
dieselben Block-RAM-Elemente gleichzeitig beschrieben werden sollen?
Ich habe mir überlegt, dass man natürlich die zu schreibenden Werte in
std_logic_vector'en mit Registersatzbreite aufblähen könnte, aber dann
habe ich ja praktisch wieder das Problem mit den Ressourcen, das ich mit
dem Block-RAM zu beheben gedachte.
Also gedanklich so etwas:
process ...
...
register_set( write_dest1 ) <= result_mux1_data;
register_set( write_dest2 ) <= result_mux2_data;
...
end process;
Ich möchte also 2 Muxer hinter dem ALU-Einheiten-Satz haben (von denen
einer nur im Bedarfsfall aushilft), die die anfallenden Ergebnisse auf 2
Kanäle runtermuxen und diese dann in die Zielregister schreiben. Geht
das irgendwie?
Viele Grüße,
Deci
PS: Mir fällt gerade auf, dass ich stillschweigend voraussetze, dass da
mehrere unabhängige ALU-Einheiten vorhanden sind. Damit meine ich nicht
vollwertige allround ALUs, sondern deren Bestandteile (Also Addierer,
Shifter usw.). Durch die Unabhängigkeit möchte ich erreichen, dass ich
recht einfach weitere Befehle hinzufügen kann, so wie ich es gerade
brauche (in etwa so "übersichtlich" und modular gedacht, wie man das
halt in der Software-Entwicklung kennt, auch wenn es vielleicht nicht
das sinnigste ist, das auf Hardwaresynthese übertragen zu wollen).
Wahrscheinlich wäre ein monolithischer ALU-Ansatz einfacher umzusetzen,
oder?
Erst mal: Respekt vor der eigenen CPU, das ist ein Haufen Arbeit.
Register sind per se Register und KEIN RAM oder BlockRAM. Ich hab noch
nicht so genau verstanden warum du das also nicht in Registern
realisieren willst? Ist dir bewusst, dass ein typischer Prozessor schon
allein deshalb Register (=FF) benutzt, weil diese das Ergebnis mit 1
Takt übernehmen? RAM speichert in den meisten Fällen mit schnellen
Taktraten das Ergebnis nicht innerhalb 1 Taktes sondern braucht evtl. 4
oder 8 Takte, je nach Taktrate natürlich, das ist der Grund warum das so
gemacht wird.
Solltest du trotz allem BlockRAM verwenden: Dieser besitzt bei Xilinx
(Virtex 5, Spartan 6 glaub ich auch) 1 oder auch 2 Ports, das lässt sich
mit dem Generator einstellen. Wenn du 1 Port erzeugst, dann kann man mit
Muxer natürlich mehrere Eingänge auf den Port schalten und das BlockRAM
beschreiben und auslesen. Durch die Einstellung vom Muxer ist dann
sichergestellt, dass nur 1 Ergebnis gleichzeitig gespeichert wird.
Wenn du das BlockRAM mit 2 Ports generierst musst du dir halt das
Datenblatt des Xilinx BlockRAM-Generators ansehen, das erzählt dir dann
ob ein Adresskonflikt beim Schreiben zu einem Problem führt oder nicht.
Wenn du übrigens tatsächlich eine ALU bauen willst die pro Taktzyklus
mehrere Ergebnisse liefern kann, dann nennt man das "superskalar" und du
möchtest also sofort in die "modernste" Prozessorarchitektur einsteigen.
Nochmals herzlichen Glückwunsch, ich an deiner Stelle würde eher eine
skalare Struktur anstreben, und selbst die wirst du wahrscheinlich nicht
erreichen. Das Problem sind die höchst unterschiedlich komplizierten
Prozessorbefehle, die eben auch unterschiedlich lang benötigen für die
Ausführung. Wenn du tatsächlich superskalar werden möchtest brauchst du
am Anfang der Pipeline eine Logik, die die anstehenden Befehle sinnvoll
auf deine Prozessoreinheiten verteilt, sicherstellt, dass sich nichts
überholt usw... Das kann extrem kompliziert werden.
LG
Bernd
Hallo Bernd!
Vielen Dank für Deine Antwort! Also das größte Problem an der Situation,
dass man keine Ahnung von dem hat was man tut, ist natürlich immer, dass
man auch nicht so gut ausdrücken kann, was man tut :D Dieser Situation
falle ich hier zum Opfer.
Ich habe mir ein schönes buntes Bild (Signalflussplan) der (ersteinmal
unabhängigen) Blöcke aufgemalt, die ich gedenke miteinander zu
verschalten, um etwas CPU-artiges zu erhalten. Die arbeite ich jetzt so
nach und nach ab (und stoße erwartungsgemäß auf überall auf
Schwierigkeiten, aber wie sonst soll man auch etwas lernen :D).
Also in meiner ersten Version des Registersatz-Moduls hatte ich so etwas
wie:
Also num_registers 16-Bit-Werte.
Als ich den herauskommenden Block inklusive dahinterligenden Lese-Mux
einmal synthetisieren lassen habe und mir das Resultat anschaute, kam
die Ernüchterung.
Im nächsten Versuch ging es folgendermaßen:
Im Internet fand ich die Information, dass solcherart Signaldeklaration
von Xilinx in eine Blockramumsetzung synthetisiert wird. Nun hatte ich
vor Deiner Bemerkung eigentlich nicht damit gerechnet, dass das zu
Problemen führen würde. Ich ging jetzt davon aus, dass dieser RAM
imstande wäre, die ganzen Operationen innerhalb eines Taktzyklusses
durchzuführen, der HDL-Synthesizer hat mich bislang auch noch nicht
gewarnt, dass es da zu irgendwelchen Taktproblemen kommen würde und ich
habe diese Syntax auch schon in einem anderen Miniprojekt erfolgreich
eingesetzt, ohne dass mir da Taktverzögerungen aufgefallen wären (ich
beginne stark zu erröten, offenbar habe ich also nicht aufmerskam
simuliert).
Selbstversändlich möchte ich lieber Speicher ohne Taktverzögerung
verwenden, danke dass Du mich darauf hinweist. Jetzt muss ich nur noch
herausfinden, wie ich dafür sorge, dass Register synthetisiert werden.
Zu der Sache mit Superskalar:
Ich habe das eben mal auf Wikipedia nachgeschlagen und muss hier ganz
klar etwas besser beschreiben. Ich möchte keinesfalls so etwas
abgefahrenes machen! :D
Was ich mir vorstellte waren die einzelnen ALU-Operationen als diskrete
Bausteine hinter einem Quellenmuxer (Der Daten aus den Registern nimmt,
oder Konstanten anlegen kann) an einen "Bus" mit 4 Operandenkanälen
anzuschließen. Dann wird der dem Befehl entsprechende ALU-Block Block1
"getriggert", der sich die Daten vom Bus nimmt und seine Sache erledigt.
Diese Operation kann natürlich länger dauern, als ein Taktzyklus.
Solange der nächste Maschinenbefehl aber diese ALU-Einheit nicht
benötigt, soll das egal sein und eine andere Einheit Block2 erledigt
ihren Job. Soweit so gut. Nun kommt es aber zwangsläufig zu der
Situation, dass zu einem Takt t1 sowohl Block1 als auch ein anderer
Block2 ein Ergebnis fertiggestellt haben.
Jetzt stehe ich vor der Möglichkeit, die gesamte Pipeline anzuhalten, um
eine zusätzliche Register-Schreib-Operation reinzuquetschen, oder ich
finde irgendwie einen Weg, wie ich zwei Register in einem Takt
beschreiben kann.
Pipeline ist auch wieder etwas zu hoch gegriffen. Mit der "Pipeline"
verfolge ich kein anderes Ziel, als die einzelnen Funktionsbausteine in
einer solchen Form voneinander zu trennen, dass ich da überhaupt noch
durchsteige, das hat hier überhaupt nichts damit zu tun, etwas
abgefahrenes machen zu wollen :D
Hoffentlich klingt das ganze jetzt weniger stark super-überambitioniert
:D
Viele Grüße,
Deci
Bernd S. schrieb:> Erst mal: Respekt vor der eigenen CPU, das ist ein Haufen Arbeit.>> Register sind per se Register und KEIN RAM oder BlockRAM. Ich hab noch> nicht so genau verstanden warum du das also nicht in Registern> realisieren willst?
Z.b. weil das nicht zu knapp an Resourcen spart?
Wobei ich distributed RAM (oder wie immer das bei Xilinx heisst)
verwenden würde.
> Ist dir bewusst, dass ein typischer Prozessor schon> allein deshalb Register (=FF) benutzt, weil diese das Ergebnis mit 1> Takt übernehmen? RAM speichert in den meisten Fällen mit schnellen> Taktraten das Ergebnis nicht innerhalb 1 Taktes sondern braucht evtl. 4> oder 8 Takte, je nach Taktrate natürlich, das ist der Grund warum das so> gemacht wird.>
Was so nicht stimmt. BlockRAM kann sehr wohl in einem Takt geschrieben
bzw gelesen werden. Auch bei hohen Taktraten.
Hallo nochmal!
Danke, dass Du mich auf dual port ram gestoßen hast, ich habe mich eben
mal durch die Dokumentation des Vendor-Blocks gekämpft. Zumindest mal
der Blockram unterstützt TDP, also 2 komplett unabhängige
Schrebeingänge, die im gleichen Takt schreiben. Wenn ich jetzt noch die
Anmerkung von Lattice User dazunehme, dass es keine Taktlatenzen gibt,
scheint es genau das zu sein, was ich brauche. Wer hätte gedacht, dass
Xilinx das so vorausschauend für mich vorbereitet hat! \o/
Jetzt mal schauen, ob das auch irgendwie mit dem Distributed-Ram geht
(der jetzt meinem ersten Verständnis nach den Vorteil bietet, dass er
näher an die Logik platziert werden kann, wenn ich das richtig
verstehe?) Außerdem kann man da im gleichen Takt noch kombinatorisches
hinterschalten, weil das Lesen asynchron vonstatten geht, ja? Das klingt
natürlich nach größerem Spielraum!
Danke euch erstmal, über weitere Anmerkungen und Zurechtweisungen freue
ich mich natürlich noch immer ;)
Viele Grüße,
Deci
> Im nächsten Versuch ging es folgendermaßen: type registers_t is array (0 to
15) of std_logic_vector(15 downto 0);
> signal registers : registers_t := (others => (others => '0'));> Im Internet fand ich die Information, dass solcherart Signaldeklaration> von Xilinx in eine Blockramumsetzung synthetisiert wird.
Ja, ich kann mir vorstellen, dass man Blockram auch implizit deklarieren
kann. Wenn das dann schnell genug ist (schreibt und liest in 1 Takt) und
der Syntheseprozess also das Timing implizit sicherstellt, dann ist das
auch kein Problem. In meiner Vorstellung war BlockRAM jetzt aber eine
Black Box, welche man generiert und dann als COMPONENT einbindet. Und
hier ist man dann selber verantwortlich, dass man diese black box mit
dem richtigen Timing (Wartezyklen usw.) anspricht. Diese Denke kommt bei
mir natürlich auch davon, dass ich bei solchen Konstrukten den RAM
einfach durch einen anderen austauschen kann, z.B. wenn man als
Zieltechnologie ein ASIC hat. Dann bindet man anstatt das Xilinx
BlockRAM dann halt das ASIC-RAM in die Simulation ein. Und wenn man das
RAM als black box betrachtet, dann können Prozessor-Register fast
zwingend nur als FF ausgeführt werden. Entweder richtige FF im ASIC oder
halt als LUT im FPGA.
> Jetzt stehe ich vor der Möglichkeit, die gesamte Pipeline anzuhalten, um> eine zusätzliche Register-Schreib-Operation reinzuquetschen, oder ich> finde irgendwie einen Weg, wie ich zwei Register in einem Takt> beschreiben kann.
Ja, verstehe, hat natürlich Ansätze von superskalarität. Hier muss also
geprüft werden ob die beiden Blöcke in die gleichen Zielregister
schreiben, und wenn nicht können sie gleichzeitig schreiben, zumindest
dann wenn es für jeden Block eine Datenleitung/Mux zu allen Registern
gibt. Die Überprüfung ob sie auf das gleiche Register schreiben ist bei
Prozessorbefehlen die das Zielregister direkt als Operand in sich tragen
einfach, bei indirekter Adressierung wirds natürlich schon aufwändiger.
Das Anhalten der Pipeline oder wenn du keine Pipeline hast, das Anhalten
des Prozessors um etwas bestimmtes herauszufinden ist Standard, da wirst
du wohl kaum herumkommen.
decimad schrieb:> also 2 komplett unabhängige> Schrebeingänge, die im gleichen Takt schreiben.
ja, aber gleichzeitig auf die gleiche Adresse zu schreiben, kann ja
nicht gut gehen....
Distributed RAM ist übrigens im Endeffekt nichts anderes als die LUTs
als Speicher herzunehmen und nicht die Standard RAM-Zellen die im FPGA
zusätzlich zu den LUTS auf dem Die vorhanden sind. Ist natürlich
schneller weil hier kein Lese-/Schreibvorgang auf die Standard-RAM-Zelle
gemacht werden muss. Verbraucht so weit ich weiß halt eine Menge der
LUTs für relativ wenig Speicher.
Aus meinem Verständnis würde ich alle Prozessorregister als FF
realisieren und nicht versuchen dafür BlockRAM herzunehmen. Aber ich
lasse mich auch gern eines Besseren belehren, man lernt ja ständig dazu.
Wenn du einen getakteten Prozess machst, dann sind alle Zielsignale
schon FFs.
z.B.
process (clk)
begin
register1 := hilfsregister;
end process;
Und schon ist register1 als FF realisiert. Wozu also sich mit Blockram
herumschlagen? Wenn der Syntheseprozess aus irgendwelchen Gründen das
nicht in den LUTs speichert sondern intern im BlockRAM-Block auf dem Die
speichert und sich automatisch um die dazu notwenigen
Lese-/Schreibzyklen kümmert, kann es dem geneigten VHDL-Coder völlig
egal sein wo das Datum gelandet ist. In der RTL-Beschreibung ist es eben
als FF realisiert...
Der BlockRAM ist in der Regel so schnell wie auch das interne
Taktnetzwerk, jedenfalls bei Xilinx. Bringt also gegenüber dem LUT RAM
da keinen Nachteil. LUT RAM kann sinnvoll sein, wenn man den BRAM für
was anderes braucht, oder die gewünschte RAM Größe weniger als 1k ist,
denn dann geht jedesmal auch ein ganzer BRAM Block drauf. Die nötigen
Schreibzugriffe beschreibt man ja in VHDL selber, wenn man die
Lese-Adresse getaktet anlegt, wird ein BlockRAM generiert, ansonsten ein
LUT-RAM. Beim BLock RAM mit 2 Ports muss man natürlich aufpassen, was
man tut, aber das ist ja klar. Im XST User Guide oder auch in den
Templates in der ISE ist das eigentlich sehr gut erklärt...
Hallo Leute,
danke für die tollen Anregungen! Die Register-Bank meiner CPU ist jetzt
bei mir sozusagen als Blackbox mit 2 Schreibeingängen und zwei Register
-> Ausgangsbus Multiplexer ausgelegt. Für's erste werde ich weiter
Block-Ram verwenden, denke ich, einfach weil ich im Moment keine andere
Verwendung dafür habe (außer dann vielleicht einen LUT für Sinus/Kosinus
oder andere Funktionen, aber es sind ja noch ein paar übrig). Wenn es
dann mal eng wird und ich den RAM brauche, aber noch Logik übrig habe,
kann ich die Blackbox dann ja einfach anders umsetzen, so ist das doch
gedacht, oder? :) Und so kann ich einen ziemlich großen Satz an
Registern vorhalten, was mangels Speichercontroller und Compiler erstmal
verlockend klingt (Speichercontroller wär' natürlich was tolles,
irgendwann einmal, wenn ich weniger wenig weiß, was ich da tue).
So wie ich das im Moment umsetze, wird ein einfacher Befehl dann 3 Takte
benötigen: Auf Bus lesen -> Berechnen -> Zurückschreiben. Aber das läuft
dann wenigstens ohne Probleme beim Referenztakt, was ganz gut ist, weil
ich mich im Moment noch nicht damit herumschlagen möchte, eine weitere
Taktdomäne mit erforderlicher Synchronisierung einzuführen. Denn dann
habe ich noch weniger Ahnung, was ich da eigentlich tue :D Erstmal
kleine Brötchen backen, oder wie sagt man? :)
Aber eine "33MHz" CPU ist ja auch erstmal nicht schlecht, hehe.
So, jetzt erstmal weitermachen hier, so ein Wochenende ist ja nicht
endlos lang.
Viele Grüße,
Deci
Anmerkung... (ich sollte mich einmal registrieren!)
Wobei der Durchsatz in meinem, bei entsprechender Aneinanderreihung von
Befehlen natürlich 1 Befehl pro Takt sein kann, wenn man eben keinen
Register als Quelle referenziert, der als "busy" markiert ist, also auf
ein Berechnungsergebnis wartet. Der Steuer-Multiplexer für die
ALU-Einheiten könnte eigentlich auf dem Rückschreibe-Bus mitlauschen und
die Berechnungsergebnisse dort "abfangen", das würde wieder einen Takt
sparen. Hrmmm, aber erstmal einfach machen... :)