Hello, Guest the thread was called528 times and contains 20 replays

last post from andi6510 at the

Verilog always Block aus dem Block heraus triggern

  • Hallo!


    Ich hab die Tage einen Blick auf die Tiny RISC V Specs werfen können, und fand das Beispiel recht interessant:


    http://www.csl.cornell.edu/cou…ts/ece5745-tinyrv-isa.txt


    Nun grüble ich, wie einfach man so eine CPU in Verilog implementieren könnte, und hab deshalb einfach mal angefangen, bisserl Verilog zusammen zu schreiben.


    Nun überleg ich gerade, wie man die Schiebeoperation recht einfach in der ALU implementieren könnte, und hab mich mal für einen Kompromiss aus Performance und Schaltungsaufwand entschieden, indem in alle Schiebeoperationen in max 5 Schritten durchführe, also 1,2,4,8 und 16 Bit schiebem was ja zusammen max 31 Bit ergibt.


    Ich wollte diese Operationen möglichst asynchron zum Takt in der ALU durchführen indem ich die ALU Operationen einfach durch jede Änderungen an den Eingängen triggere. Da die Schiebeoperationen ja in mehreren Schritten erfolgen, muss ich nach jedem Schieben die nächste ALU Operation triggern. Einfach indem ich eine ALU Eingangsvariable ändere. Nun frag ich mich, ob so eine Schaltung funktionieren kann, wie ich es erwarte.


    Als Anhang mal ein paar unfertige Sources, wo man die Änderung z.B. ab Zeile 72 sehen kann. Weiss jemand, ob so etwas funktionieren kann?


    Ciao,
    Andreas

  • Wie wäre es denn mit dem Verfahren, welches Wirth anwendet bei der RISC-Architektur für sein "Project Oberon"?

    Das Ergebnis ist recht kurz, aber erledigt trotzdem einen Shift in einem Takt. An gleicher Stelle findet sich übrigens auch der Code für Rechtsshift (als LSR und ROR).

  • Wenn ich das recht sehe, ist das aber quasi auch ein 5 Bit Shifter, der aber die Bits 0+1, sowie 2+3 zu jeweils einer Abfrage zusammenfasst?

    Ja, genau. Wirth schreibt hierzu: "A basic element on our FPGA are “lookup tables” with 4 inputs and a single output resulting from any function of the inputs. Hence, a 4-input multiplexer is the preferred element." Aus dem Grunde hat er das Shiften aufgespalten auf drei hintereinandergeschaltete Multiplexer, was einerseits LEs einspart gegenüber einer umfangreichen casex-Anweisung unter Einbeziehung aller Shiftwerte, als auch dafür sorgt, daß die Schaltung einigermaßen schnell genug wird.

  • Ja, aber ist das wirklich so sicher, dass es in einem Takt durchläuft?


    Meine Idee war ja, dass das ALU Ergebnis sofort einen neuen ALU Durchlauf triggert, d.h. theoretisch könnte das mit dem maximalen Takt laufen, den die Schaltung eben erlaubt. Je nach dem Takt der CPU könnte das also auch in 1 Takt durchlaufen. Müsste man vielleicht einfach mal probieren. Hab eh noch keinen Plan, ob die ALU wirklich schön looped bis das Ergebis steht.

  • Ja, aber ist das wirklich so sicher, dass es in einem Takt durchläuft?

    Da hilft nur austesten.


    Grundsätzlich gilt aber, daß das Ergebnis der ALU synchron zum Grundtakt der CPU in der Store-Phase in das Zielregister geschrieben werden muß. Es hilft also nichts, die ALU asynchron irgendwie vor sich hinwerkeln zu lassen, da das Ergebnis am Ende doch wieder zu einem ganz bestimmten Zeitpunkt vorhanden sein muß, denn sonst wird Müll geschrieben. Daher sollte man nach Möglichkeit allgemein diese Vorgänge synchron konzipieren, damit sie sich in das Taktgefüge der CPU einfügen.
    Eine 32-Bit-Multiplikation z. B. kann das Miniboard innerhalb eines 50 Mhz-Taktes erledigen, d. h. Werte kommen aus den Quellregistern, werden multipliziert, und das Produkt wird ins Zielregister geschrieben. Blöd ist halt, daß man die Indizes der betreffenden Register im Takt vorher setzen muß, da das Blockram, in dem die Registerwerte abgelegt sind, mit einem Takt Verzögerung die Werte bereitstellt, wohingegen beim Schreiben der Registerindex zusammen mit dem Schreibwert gesetzt werden kann. Bei einer 3-Adresscodierung (RiscV) heißt das also, daß im ersten Takt (Dekodierung) die Registerindizes der beiden Quellregister an das Blockram geschickt werden, während in der Aluphase das Zielregister übermittelt wird.
    Ähnliches ergibt sich übrigens auch für das Lesen aus dem Speicher. Ein einfaches "opcode <= memory[pc]" wird nicht funktionieren, Das benötigt mindestens zwei Takte: 1.) Legen der Adresse (PC) auf den Blockram-"Bus", 2.) Holen des Wertes. Aus diesem Grunde ist es besser, das Dekodieren mit dem Werteholen zusammenzufassen, da man sich dadurch diesen Schritt sparen kann.
    Insgesamt ergibt sich dadurch folgendes Taktverhalten:
    1.) PC-Adresse auf Bus legen.
    2.) Opcode holen, zerlegen, dabei die beiden Quellregister bestimmen und an das Blockram für den Registersatz schicken.
    3.) Registerwerte holen, verarbeiten und Ergebnis ans Registersatz-Blockram zurückschicken.
    Dann kommt es noch darauf an, wie man diese Schritte überlappen kann, um die Programmausführung zu beschleunigen. Außerdem muß man darauf achten, wie breit der Datenbus zum Speicher ist. Ist er kleiner als die Breite des Opcodes, muß man das Holen des Opcodes auf weitere Takte verteilen. Beim RiscV handelt es sich um 32 Bit-Befehlswörter. Bei einem Speicher mit 8 Bit-Datenbusbreite braucht man folglich vier Zugriffe, bevor man mit der Befehlsverarbeitung loslegen kann. Das kostet sehr viel Zeit. :(

  • Das mit dem opcode <= memory[pc] soll nur ein Platzhalter für den eigentlichen Ram-Zugriff sein, der später nochmal ja mehrere CPU Takte brauchen wird.


    Das Schreiben des ALU-Ergebnis in das Register sollte ja wieder synchron zum CPU-Takt erfolgen, weil ja die ALU den CPU-State auf Store ändert, wenn sie fertig ist. Erst beim nächsten CPU-Takt wird dann das Ergebnis aus der ALU übernommen.
    Aber bis dahin soll die ALU asynchron (schneller) als die CPU laufen können. Gerade bei einer Multiplikation mit 32 Takten müsste das ja ordentlich was bringen, wenn diese 32 Takte eben schneller ablaufen als 32 CPU Takte. So ist zumindest mal die Idee...


    Ich hatte ganz am Anfang auch eine Pipeline drin, die ich aber für den Moment mal wieder rausgeschmissen habm weil ich mal möglichst einfachen Code haben wollte, bis die ganze Funktionalität da ist. Es soll ja primär zum Testen von Ideen sein, und da würde im Moment zuviel Optimierung den Code nur unübersichtlicher machen, dachte ich...

  • Gerade bei einer Multiplikation mit 32 Takten müsste das ja ordentlich was bringen, wenn diese 32 Takte eben schneller ablaufen als 32 CPU Takte.

    Das Miniboard verfügt über eingebaute Multiplikatoren, die das Multplizieren in einem Takt erledigen. Eine Aufspaltung auf mehrere Takte wäre nur bei einer Division oder bei Fließkommaoperationen notwendig.
    Ob es eine gute Idee ist, eine Schleife asynchron zu organisieren, weiß ich nicht. Ich befürchte, daß es da schnell zu race-Conditions kommen kann, d. h. ein Teil der Schleife arbeitet noch, während der andere Teil bereits neu gestartet wird. Diese parallellen Abläufe innerhalb eines Schleifendurchgangs synchron hinzukriegen ohne einen externen Taktgeber, dürfte sehr schwierig und eine große Fehlerquelle sein.

    weil ja die ALU den CPU-State auf Store ändert

    Das Problem dürfte auch hier sein, daß Schreibzugriffe auf Variablen aus verschiedenen Zeitzonen getätigt werden. Zum einen wird der Zustand gesetzt in der synchronen Blockanweisung, zum anderen asynchron am Ende der Schleife. Das könnte zu einem Konflikt führen. Normalerweise wird der Wert der Variablen in jedem Takt neu geschrieben. Steht im Code keine Anweisung zum Neuschreiben der Variablen, wird ein Latch eingefügt, d. h. es wird automatisch die Anweisung "variable <= variable" ergänzt. Dies beißt sich aber mit dem Setzen der Variable im asynchronen Teil. Daher bezweifle ich, daß diese Vorgehensweise funktionieren wird. Um wirklich sicherzugehen, daß kein Müll produziert wird, sollte man Variablen daher immer nur in der gleichen Zeitzone setzen, sofern nicht der Compiler ohnehin schon meckert.

  • Ich wollte diese Operationen möglichst asynchron zum Takt in der ALU durchführen indem ich die ALU Operationen einfach durch jede Änderungen an den Eingängen triggere.

    FPGAs sind für synchrone Schaltungen entworfen(*) und die Designtools sind auch darauf ausgerichtet, dass sie ein synchrones Design verarbeiten. Man kann zwar auch asynchrone Designs in einem FPGA umsetzen, aber meines Wissens rennt man dabei dann häufiger mal in Bugs in den Synthesetools.


    (*) Obskuritäten wie Achronix ignoriere ich hier mal einfach, aber selbst die sind IIRC mit ihrer zweiten Chipgeneration auf teilsynchrone Designs umgestiegen.


    Quote

    Da die Schiebeoperationen ja in mehreren Schritten erfolgen, muss ich nach jedem Schieben die nächste ALU Operation triggern. Einfach indem ich eine ALU Eingangsvariable ändere.

    (Disclaimer: Ich kann fast kein Verilog weil ich VHDL bevorzuge)


    Ich habe nicht in deinen Code geschaut, aber wenn du Variablen in Verilog in ein always reinschreibst ohne dass es Takte sind (d.h. sowas wie "posedge clk"), dann ist das eher ein Hinweis an den Simulator wie er die Ausführung der Simulation optimieren kann und nicht wirklich wichtig für die Synthese. Um eine zuverlässige asynchrone Schaltung zu designen verwendet man üblicherweise mehrere Handshake-Signale. Dabei signalisiert zuerst das ausgebende Modul, dass jetzt ein gültiger Wert bereitsteht und danach das annehmende Modul in Gegenrichtung, dass es den Wert übernommen hat, woraufhin das ausgebende Modul mit der nächsten Berechnung weitermachen kann. In dem Wikipedia-Artikel zu asynchronen Schaltungen gibts einen kleinen Überblick darüber.


    Für einen Barrel-Shifter würde ich mir aber den ganzen Aufwand sparen, den kann man auf einem FPGA auch einfach mit den oft eh in grosser Zahl vorhandenen Hardware-Multiplizierern umsetzen.

  • Danke für Deinen Beitrag!


    Ich simuliere noch herum, wie die Variablen korrekt übergeben werden. Das Gefährliche an dieser ALU ist ja, dass sie die Eingangsvariablen ändert. Über gibt man also 3 Werte, so ist der 1. Wert evtl schon geändert, wenn der 3. übergeben wird. Man müsste mit Kopien arbeiten, aber ich weiss noch nicht, wie man die bei der Übergabe erzeugen könnte.

  • Man müsste mit Kopien arbeiten

    Kopien im Sinne von Speicher wird nicht gehen, da solch ein Vorgang einer Synchronisierung bedarf (im Grunde genommen für jedes einzelne Bit).
    Vielmehr werden ALUs häufig als combinational logic definiert, d. h. ohne Speicher nur mit Input, Verarbeitung und Output. (Die Register, die die Eingänge füttern, sind nicht direkter Teil der ALU.) Praktisch entspricht die Weiterleitung des Eingangssignals einer "Kopie". So bilden beim Bitshiten-Beispiel die wire t1, t2 und t3 quasi die "Kopien". Wichtig dabei ist, daß es zu keiner Rückkopplung kommt, und die Signale stets nur weitergeleitet werden. In Deinem Code wird das Ausgangssignal der ALU jedoch direkt wieder in den Eingang eingespeist, was unkontrollierbare Auswirkungen haben kann. Nun kann man zwar versuchen, solche Konstruktionen mit Quartus zu übersetzen, aber letztendlich wird Quartus wahrscheinlich nur sehr, sehr lange herumrechnen und als Ergebnis einen immens hohen LE-Verbrauch ausspucken, was in der Regel ein ziemlich deutliches Zeichen dafür ist, daß die Vorgehensweise nicht funktioniert.
    Selber hatte ich mal den Versuch gemacht, zwei Zeitzonen zu verwenden, wobei eine Zone wie bei Dir durch die Werte getriggert wurde. Das führte dazu, daß größtenteils die richtigen Werte herauskamen, aber bei ganz bestimmten Konstellationen dann plötzlich falsche. Anders gesagt: Man ist gut beraten, auf asynchrone Wege sowie die Verwendung von verschiedenen Zeitzonen zu verzichten, da man sich damit jede Menge Fehler einhandeln kann, die nur sehr schwer aufzuspüren und zu beseitigen sind. Im konkreten Fall gibt es für die Verwendung von asynchronen Schaltungen eigentlich auch gar keinen Grund, denn das Miniboard ist ja in der Lage, innerhalb eines Taktes den kompletten Bitshift durchzuführen. (Vor einiger Zeit hatte ich Dir ja schon gezeigt, daß es möglich ist.) Von daher würde ich mich auch mit einer asynchronen Organisation gar nicht lange aufhalten, sondern lieber das Augenmerk auf die eigentliche Umsetzung des Prozessors richten.

  • Will man das Shiften in mehreren Takten machen, um halt die LEs für das Shiften mit 32 möglichen Eingangswerte zu sparen, läuft es halt immer drauf raus, dass man in der ALU einen Status speichern muss, wie weit schon geschoben wurde. Diesen Status muss man zwangsläufig bei der Übergabe der Werte initialisieren. Das ginge über ein Handshake-Signal, was ich halt gerne vermieden hätte.


    Die andere Lösung wäre halt, die ALU mit dem CPU Takt anzusteuern, wodurch die Shift Operation max 5 Takte brauchen würde (oder man fasst halt die Bits zusammen, wie bei Deinem Vorschlag).


    Schade, ich hatte gehofft, eine elegante asynchrone Lösung zu finden. Muss ich wohl mal weiter grübeln.

  • Will man das Shiften in mehreren Takten machen, um halt die LEs für das Shiften mit 32 möglichen Eingangswerte zu sparen

    Wenn du LEs sparen willst liegt doch die Nutzung der Hardware-Multiplizierer nahe.

  • um halt die LEs für das Shiften mit 32 möglichen Eingangswerte zu sparen

    Was das Sparen von LEs anbelangt, so würde ich mir da keine großen Gedanken drum machen. Die CPU ist mit Abstand der größte Brocken, alles andere wie die Ansteuerung von irgendwelchen seriellen Anschlüssen verbraucht hingegen nur ganz wenig. Von daher wette ich, Du schaffst es nicht einmal, alle LEs des Miniboards zu verbrauchen.
    Viel wichtiger als die Anzahl der verbleibenden (und damit auch ungenutzten) LEs ist, wieviel (Block)Ram später benötigt wird, um ein bestimmtes Programm ausführen zu können. An diesem Punkt macht es sich nämlich durchaus bezahlt, in LEs zu investieren, denn das bedeutet, anschließend viel wichtigen Speicherplatz einzusparen.
    Wenn Du z. B. eine CPU hast, die wenig LEs verbraucht, aber dafür nur über einen geringen Befehlssatz verfügt, führt das dazu, daß komplizierte Anweisungen aus vielen Einzelbefehlen zusammengesetzt werden müssen, was jede Menge Platz im Speicher verbraucht (ganz abgesehen von der Ausführungsgeschwindigkeit). Außerdem: Wenn das Befehlswort der CPU immer mindestens 32 Bit groß ist (wie beim RiscV), kann man es zwar mit wenig LEs dekodieren, aber dafür passen in 12 kb Blockram auch nur 3072 Befehle. Sind die Befehle hingegen 16 Bit breit, kommt man auf 6144 Befehle. Sowas macht sich in den Anwendungsmöglichkeiten später deutlich bemerkbar. Merke: Ungenutzte LEs sind nur noch zu gebrauchen, um darin vielleicht ein Miniminimini-Rom unterzubringen. Aber was nutzen die paar LE-Speicherbits, wenn der Programmcode das zigfache schluckt..?
    Mein Tip daher: Sorge Dich nicht um die LEs. Sieh lieber zu, daß Du Deine CPU generell ans Laufen kriegst. Optimieren kannst Du später immer noch, wenn Du willst, aber - wie gesagt - ich bezweifle, daß es überhaupt notwendig sein wird.

  • Außerdem: Wenn das Befehlswort der CPU immer mindestens 32 Bit groß ist (wie beim RiscV), kann man es zwar mit wenig LEs dekodieren, aber dafür passen in 12 kb Blockram auch nur 3072 Befehle. Sind die Befehle hingegen 16 Bit breit, kommt man auf 6144 Befehle.

    Es soll ja ggf eh nur bisserl Code ins Rom, um von der SD-Karte booten zu können. Das sollte nicht soooviel Befehle brauchen, denk ich mal?


    Aber bis jetzt ist es ja eh nur ein Spielprojekt und noch nix ernsthaftes. Mal schauen, ob das irgendwo hinführt...


    Was macht Deine CPU denn so?

  • SD-Karte

    Ja, die SD-Karte... Hast Du da schon irgendwie Code, so daß man mit der Karte kommunizieren kann? Also Kommando übertragen und dann Antwort lesen? Die meisten Codeschnipsel, die ich gefunden habe, taugen in dieser Hinsicht nicht wirklich was. :(

    Was macht Deine CPU denn so?

    Hatte in den letzten Monaten so gut wie gar keine Zeit (und war auch kaum im Forum). ;( Konnte daher nur Kleinigkeiten zwischendurch erledigen: Befehlssatz kürzen, Assembler und Disassembler anpassen. Zur Zeit bastel ich an einem neuen Emulator für den PC und kümmere mich gleichzeitig um ein paar OS-Programmbibliotheken für die CPU, wobei das System aus zwei Teilen bestehen soll/muß: Einerseits einen Minimal-Kernel zum Booten im Blockram, andererseits ein recht umfangreiches OS, welches alle möglichen Grundfunktionen für Apps bereitstellt. Auf dem FPGA habe ich mangels Zeit leider noch nichts machen können, muß also die Änderungen des Befehlssatzes erst noch in den Verilogcode für die CPU übertragen. Das ist technisch zwar nicht so schwierig, aber bislang fehlte halt die Möglichkeit dazu. Nichtsdestoweniger kann man für die CPU jetzt schon problemlos Code schreiben, um von der SD-Karte zu booten. Es hapert allein an der Datenübertragung zwischen FPGA und SD-Karte.

  • M.J. und ich basteln doch mit dem ep2c5t144 mini dev Board rum. Das hat nicht soviel Multiplizierer, dass man damit verschwenderisch umgehen könnte...

    Welche Pläne hast du denn für die 13(/26) Hardware-Multiplizierer?