Hallo Besucher, der Thread wurde 1,9k mal aufgerufen und enthält 10 Antworten

letzter Beitrag von daybyter am

Minimale 32 Bit CPU für FPGA

  • Hallo!


    Ich suche eine minimale 32 Bit CPU z.B. für fpga Hardware Initialisierungen. Ich habe bereits eine ZPU Implementierung geschrieben, hätte allerdings lieber etwas, dass etwas 'konventioneller' zu programmieren ist (also eher Register als Stackmachine).


    Der Focus soll auf einer recht kleinen Anzahl von LEs sein. Da keine umfangreichen Programme laufen sollen, kann der Code auch ruhig etwas länger werden, wenn die CPU dafür etwas einfacher wird.


    Nun simuliere ich also, wie etwa eine recht minimale 32 Bit CPU aussehen könnte, die trotzdem halbwegs komfortabel zu programmieren ist. Vielleicht entdeckt eure Schwarmintelligenz ja offensichtliche Dinge, die ich übersehen hab... :schande:


    Hier mal der aktuelle Stand meines Brainstormings:


    ==============================
    Register



    a,b Universalregister 32 bit
    s Status 8 bit
    sp Stackpointer 32 bit
    pc Programmzähler 32 bit



    Instruktionen:



    1-Register
    ==========



    inc_a inc_b Inkrementiere a bzw b
    dec_b dec_b Dekrementiere a bzw b
    not_a not_b Negiere
    shl_a shl_b Shift left
    shr_a shr_b Shift right
    rol_a rol_b Rotiere links
    ror_a ror_b Rotiere rechts
    copy_a_b copy_b_a Kopiere a nach b bzw b nach a
    load_a <32-bit adr> load_b <32-bit adr> Lade a bzw b von einer 32 bit Adresse
    store_a <32 bit adr> store_b <32 bit adr> Speichere a bzw b in einer 32 bit Adresse
    set_a <32 bit value> set_b <32 bit value> Setze a bzw b auf einen 32 bit Wert
    push_a push_b Speichere a oder b auf dem Stack
    pop_a pop_b Hole a oder b vom Stack
    load_a_from_b load_b_from_a Lade den 32 Bit Wert an der Adresse <b> nach a bzw lade den Wert in <a> nach b



    2-Register
    ==========
    add_b a = a + b (Sollte man hier auch die andere Richtung b = b + a einbauen?
    sub_b a = a - b
    and_b a = a & b
    or_b a = a | b
    xor_b a = a ^ b



    Sprung Instruktionen
    ====================
    jmp <32 bit adr>
    jpz <32 bit adr>
    jnz <32 bit adr>
    jsr <32 bit adr>
    ret



    Sonstiges
    =========
    brk Stoppe cpu
    ==============================


    Die Mnemonics shl_a hab ich mal so gewählt, weil sie mir einfacher zu parsen scheinen, also ein shl a.


    Vielen Dank schonmal für das Lesen meiner Ideen.


    Ciao,
    Andreas

  • Ich sehe keine bedingten Sprünge für größer als oder kleiner als. WIe würdest Du die implementieren?

  • Shameless Plug: https://github.com/maikmerten/spu32 <-- vielleicht kannst Du da was verwenden. Bisher einfach meine kleine Spielwiese, läuft aber. Führt den 32-bit RISC-V Basisbefehlssatz aus, d.h. Du hast sofort vernünftige Assembler und Compiler startbereit. Alternativ wird gerne https://github.com/cliffordwolf/picorv32 eingesetzt. Beide CPUs liegen irgendwo im Bereich von ca. 1000 LUT4s auf iCE40 FPGAs, synthetisieren aber natürlich auch in anderen FPGAs, da stinknormales Verilog.



    (Ich sage ja schonmal scherzhaft, dass es einfacher ist, eine funktionierende CPU zu basteln, als den ganzen Toolstack drum herum)

  • Für den Fall, dass Du doch lieber einen eigenen Befehlssatz machen möchtest (weil das macht Spass und ist lehrreich!), hier mal ein bisschen Feedback von mir:



    Die Register möchtest Du vermutlich im BRAM des FPGAs realisieren und nicht aus dem Fabric heraus synthetisieren, da es dann deine LUTs frisst. Gute Nachrichten: Dann kannst Du auch mehr Register haben, schließlich ist mindestens ein Block BRAM eh verwendet - den würde ich dann auch versuchen, besser zu nutzen.


    Ich würde also darüber nachdenken, diesen BRAM-Block nicht mit zu wenigen Registern "zu verschwenden", also ein paar Register mehr vorzusehen:
    - mindestens 8 Universalregister, das wären "nur" 256 Bits. Mal zum Vergleich beim Asbach-Uralt Cyclone ist jeder BRAM-Block 4096 Bit gross.
    - ich würde keinen eigenen Stackpointer vorsehen, sondern schlicht ein Universalregister dafür verwenden. Flexibler und und Du brauchst ja eh load/store in die Universalregister.
    - In RISC-V verzichtet man auf das Statusregister. Wen Over- bzw. Underflow interessieren, der macht ensprechende Checks in Software (bei C kommt man ja eh nicht an die entsprechenden Flags). Es gibt Sprünge für größer/unsigned größer/kleiner/unsigned kleiner/gleich - jeweils wird explizit verglichen, ohne ein Statusregister zu konsultierien. Vereinfacht das Pipelining. Wenn Du kein Pipelining machen willst (Du brauchst die Performance nicht), dann spricht nichts gegen ein Statusregister.






    Auf die inc und dec-Instruktionen würde ich verzichten, Du hast ja Reg-Reg Instruktionen und kannst ja eine Konstante in ein Register packen (Du hast ja jetzt mehr Register, da gratis per BRAM). Ist flexibler.


    Möglicherweise willst Du shiften zu einer Reg-Reg Op machen, damit Du auch mehr als ein Bit pro Instruktion shiften kannst. RISC-V sieht kein rotieren vor, da gängiger Code das sehr selten einsetzt und man das mit Shiften, Verundung und Veroderung hinbekommt. Wenn Du weisst, das das eine häufige Operation ist: Kann nicht schaden, das drinzulassen - möglicherweise aber als Reg-Reg, um mehr als ein Bit zu rotieren.


    Rechts-Shiften sollte es in einer logischen und arithmetischen Variante geben (0 bzw. Vorzeichen reinschiften).



    Dein load und store sehen so aus, als würdest Du da ein 32-Bit immediate voraussetzen. Damit könnten load und store ja natürlich als Instruktion nicht weniger als 32 Bit haben. Das würde ich versuchen, zu vermeiden, sondern alle Instruktionen entweder feste 16 oder 32 Bit lang sein lassen. RISC-V kennt 20-Bit und 12-Bit immediates. Man kann auch einfach ein Instruktionsformat mit einem 16-Bit immediate wählen.


    Ich schlage vor: load <reg>, <reg-basis>, <imm-offset>, wobei die Speicheraddresse dann die Summe aus Basisregister und immediate wäre. Immediate als Vorzeichenbehaftet annehmen, dann kann man mit dem offset in "beide" Richtungen greifen.



    Bei 16-Bit immediates würde ich die set-Operation in ein "seth" und "set" aufsplitten (also "set high" und "set"). Um die ganzen 32 Bit zu laden bräuchte man zwei Instruktionen - aber man braucht "selten" "große" Zahlenwerte. Wenn dein Decoder Dein 16-Bit immediate mit Vorzeichen auf 32-Bit aufpustet, dann kann man die meisten benötigten Werte (-32768 bis 32767) mit einem einzigen "set" abhandeln und braucht "set high" nur, wenn man in den obigen 16 Bit noch andere Werte braucht.



    Passt. Je nach Instruktionsencoding kannst Du natürlich auch mit nicht-implizitem Zielregister arbeiten, also <Operation> <Zielregister> <Operand1> <Operand2>. Bei 32-Bit Instruktionen passt das "immer" und ist "gratis", bei 16-Bit Instruktionen nutzt man i.d.R. ein implizites Zielregister (also Ziel = Operand1).




    Auch hier: Die 32-Bit immediates, die Du hier brauchst, sprengen Dein Instruktionsencoding. Zur Inspiration:


    - jmp <16 bit offset>: Springen zu PC + offset (immediate wieder vorzeichenbehaftet, damit Du vor- und zurückspringen kannst)
    - jmpr <Register>: Springen zur 32-Bit Adresse im Register. Mit dieser Operation kannst Du auch auf "jmp" verzichten, wenn Du die Sprungaddresse mit normalen Register-Ops berechnest.


    Ähnlich die anderen Ops. Insgesamt braucht "man" wohl Sprung-{kleiner, unsigned kleiner, größer, unsigned größer, gleich}.


    edit: Vermutlich möchtest Du auch ein "jump and link": Also ein Sprung, der die Rücksprungaddresse in ein Register schreibt. Sonst werden Funktionsaufrufe schwer, da Dein "ret" nicht weiss, wohin. Ret brauchst Du übrigens nicht, wenn Du ein "jmpr" hast.



    Sonstiges
    =========
    brk Stoppe cpu
    ==============================


    Endlosschleife mit jmp ;-)




    Maik

  • Vielen Dank an Alle für eure hilfreichen Kommentare!


    Maik: Du scheinst ja im Bereich cpu Entwicklung recht aktiv zu sein. Vermutlich bist Du einfach sehr viel weiter als wir, aber in der mini dev Board Konversation basteln wir ja auch an eigenen CPUs und evtl. hättest Du da ja Interesse, einfach mal bisserl mit zu sprechen?


    Paar weitere Sachen: ich hab den cmp Befehl und die 2 Jmp Befehle einfach vergessen. Mach ich noch rein.


    Ich wollte die Befehle nicht 32 Bit alignen, weil ich die Hoffnung hab, dass viele Befehle eh kürzer sein werden. Meine Hoffnung waren 8-Bit Befehle, aber andererseits will ich die Befehle so encodieren, dass ich den Alu-Befehl direkt aus dem Asm Befehl extrahieren und in die Alu kopieren kann. Das kostet halt Platz in der Asm-Instruktion, aber der Befehls-Dekoder sollte so ein ganzes Stück einfacher werden, dachte ich.


    Zu einem grösseren Register-File in dem Blockram: ja, das würde dort quasi 0 Platz kosten, aber ich brauch ja dann auch die ganzen Befehle, um diese Register in die ALU zu kopieren, man muss in der Asm-Instruktion enkodieren, auf welche(s) Register sich die Operation bezieht usw. Bei 2 Registerm ist das gerade mal ein Bit. Ursprünglich wollte ich 4 Register nehmen, aber eigentlich ist mir aktuell die Performance quasi wurscht, da ich hauptsächlich Hardware damit initialisieren wollte, und das passiert ja nicht oft.


    Ich denk dann mal weiter, was ich noch so machen kann.


    Vielen Dank nochmal,
    Andreas

  • Hi daybyter :-)


    Na, bin nur Hobby-CPUler, der hin und wieder etwas herumbastelt. Mir ist keine "mini dev Board Konversation" bekannt... gibt es da einen Link?


    Wenn Du variable Instruktionslänge haben möchtest, dann sind ja auch Deine 32-bit immediates kein unmittelbares Problem. Allerdings machen variable Befehlslängen das Dekodieren nicht unbedingt kompakter ;-)


    8-Bit-Befehle klingen natürlich toll und kompakt und wir alle mögen ja auch den 6502 - aber man müsste sich genau angucken, ob man durch das kleine Register-Set nicht viele load/store-Ops benötigt, die in die Kompaktheit wieder reinfressen.


    Zum Thema "kopieren in die ALU": In den meisten Designs hängen die Ausgänge der Register (über Muxer) an den Operanden-Eingängen der ALU (wie z.B. im beigefügten Diagramm, welches für einen älteren meiner RISC-V Designs gilt - ich erkläre das Ding einem älteren mäßig guten und viel zu langen Talk - bei https://media.ccc.de mal nach "CPU-Design für Einsteiger" suchen, ab Minute 65). Die Register haben also zwei Ausgänge, die fest den Operanden-Eingängen der ALU zugeordnet sind. Da muss nichts kopiert werden, die Daten "fließen" von den Registern in die ALU (sofern die Muxer passend gesetzt wurden). Woher weiss die Register-Einheit, welche Daten sie an den Ausgängen präsentieren soll? Klar, der Decoder liefert die "Registeraddresse" (bei RISC-V sind die Bits für die Registerwahl sogar *immer* an derselben Stelle, so dass der Decoder einfach nur die entsprechenden Bits durchreicht - die Instruktion steuert also "direkt" die Registerwahl). Dann ist es halt auch "egal", wie viele Register man hat, sofern man die nötigen Bits in der Instruktion unterbringen kann.


    Es hat einigen Reiz, wenn man die ALU-Operation direkt in den Opcode backen kann.



    Viel Spass beim Knobeln! Manchmal muss man auch mal Spaghetti an die Wand werfen, um zu gucken, was für schöne Muster enstehen - insofern viel Mut zur Kreativität!



    Maik

  • Hallo!


    Die Konversation ist nicht öffentlich. Ich muss Dich da hinzufügen, damit Du Zugriff bekommst. Einen Link gibt es da leider nicht. Es gibt auch paar Discord Chats und einen IRC Channel für Hobby CPU Bastler. Wobei ich da noch der blutige Anfänger bin.


    Hier mal mein zpu Versuch:


    https://github.com/daybyter/zpura/blob/master/zpura/zpura.sv


    Ja, das Dekoden ist einfacher mit festen Befehlslängen. Ist klar. Aber wenn es nur 2 Längen gibt, nämlich 8 Bit Befehl und 32 Wert, dann hoffe ich das auch mit wenig Code hinzubekommen.


    Mir ist die Kompaktheit gar nicht soo wichtig. Meine Vorstellung ist, dass der Code von der SD Karte ins Ram geht, dort läuft und das System initialisiert. Danach kann das Meiste entfernt werden. Dann darf der Code auch länger und langsamer sein. Hauptsache die CPU frisst mir nicht so viele LE im fpga weg, die mir dann für die eigentliche Schaltung fehlen.



    Vielen Dank mal wieder für Deine Tipps!


    Ciao,
    Andreas


  • Hallo,


    klar kann ich in die Konversation mal reingucke, wenn Du mich hinufügen magst :-)


    Nur zwei Befehlslängen machen die Sache natürlich deutlich handhabbarer! Wäre gut, wenn alle Befehle, die 5 Byte sind, "zufällig" immer dasselbe Bit gesetzt haben, während alle anderen Befehle "zufällig" dieses Bit nicht gesetzt haben ;-)



    Maik