Ein weiterer einfacher Verilog-Prozessor

Der Artikel beschreibt den nächsten primitiven Prozessor und Assembler dafür.
Anstelle des üblichen RISC / CISC verfügt der Prozessor nicht über einen Befehlssatz an sich, sondern nur über einen Kopierbefehl.


Ähnliche Prozessoren haben die Maxim MAXQ- Serie.


Beschreiben wir zunächst ROM und Programmspeicher


module rom1r(addr_r, data_r); parameter ADDR_WIDTH = 8; parameter DATA_WIDTH = 8; input [ADDR_WIDTH - 1 : 0] addr_r; output [DATA_WIDTH - 1 : 0] data_r; reg [DATA_WIDTH - 1 : 0] mem [0 : (1<<ADDR_WIDTH) - 1]; initial $readmemh("rom.txt", mem, 0, (1<<ADDR_WIDTH) - 1); assign data_r = mem[addr_r]; endmodule 

Dual-Port-RAM für Datenspeicher


 module ram1r1w(clk_wr, addr_w, data_w, addr_r, data_r); parameter ADDR_WIDTH = 8; parameter DATA_WIDTH = 8; input clk_wr; input [ADDR_WIDTH - 1 : 0] addr_r, addr_w; output [DATA_WIDTH - 1 : 0] data_r; input [DATA_WIDTH - 1 : 0] data_w; reg [DATA_WIDTH - 1 : 0] mem [0 : (1<<ADDR_WIDTH) - 1]; assign data_r = mem[addr_r]; always @ (posedge clk_wr) mem[addr_w] <= data_w; endmodule 

und der Prozessor selbst


 module cpu(clk, reset, port); parameter WIDTH = 8; parameter RAM_SIZE = WIDTH; parameter ROM_SIZE = WIDTH; input clk, reset; output [WIDTH-1 : 0] port; 

Zumindest benötigt er das Register des Befehlszählers sowie ein Hilfsregister und das IO-Port-Register, damit unser Prozessor etwas hervorheben kann.


  reg [WIDTH-1 : 0] reg_pc; reg [WIDTH-1 : 0] reg_reg; reg [WIDTH-1 : 0] reg_port; assign port = reg_port; 

Der Befehlszähler ist die Adresse für den Programmspeicher.


  wire [WIDTH-1 : 0] addr_w, addr_r, data_r, data_w, data; rom1r rom (reg_pc, {addr_w, addr_r}); defparam rom.ADDR_WIDTH = ROM_SIZE; defparam rom.DATA_WIDTH = RAM_SIZE * 2; 

Der Programmspeicher mit doppelter Breite enthält zwei Adressen: Wo und von wo werden Daten in den Dual-Port-Datenspeicher kopiert?


  ram1r1w ram (clk, addr_w, data_w, addr_r, data_r); defparam ram.ADDR_WIDTH = RAM_SIZE; defparam ram.DATA_WIDTH = WIDTH; 

Wir bezeichnen spezielle Adressen: einen Befehlszähler, einen Konstantengenerator, eine Prüfung auf 0 (für bedingte Sprünge), Additions- / Subtraktionsoperationen und einen Eingabe-Ausgabe-Port, in diesem Fall nur Ausgabe.


  parameter PC = 0; parameter CG = 1; parameter TST = 2; parameter ADD = 3; parameter SUB = 4; parameter PORT = 5; 

Die Datenbusse der beiden Speicherports sind nicht nur miteinander verbunden, sondern über Multiplexer, die gleichzeitig als ALU dienen.


Ein Multiplexer - auf dem Datenbus des Leseports, so dass anstelle des Speichers an bestimmten Adressen der Befehlszähler (für relative Übergänge), E / A usw. gelesen wird.


Der zweite befindet sich auf dem Datenbus des Aufzeichnungsports, um nicht nur Daten im Speicher zu übertragen, sondern auch, wenn zu bestimmten Adressen gewechselt wird, um diese zu ändern.


  assign data = (addr_r == PC) ? reg_pc : (addr_r == PORT) ? reg_port : data_r; assign data_w = (addr_w == CG) ? addr_r : (addr_w == TST) ? |data : (addr_w == ADD) ? data + reg_reg : (addr_w == SUB) ? data - reg_reg : data; 

Das Hilfsregister reg_reg, das für arithmetische Operationen verwendet wird, ist nicht direkt zugänglich, aber das Ergebnis jeder Anweisung wird in dieses Register kopiert.


Um also zwei Werte aus dem Speicher hinzuzufügen, müssen Sie zuerst einen von ihnen an einer beliebigen Stelle lesen, z. B. in sich selbst kopieren (und gleichzeitig in reg_reg), und der nächste Schreibbefehl an der Addiereradresse zeichnet die Summe mit dem vorherigen Wert dort auf.


Der Konstantengenerator schreibt die Adresse und nicht den Speicherwert in diese Adresse.


Für bedingungslose Sprünge müssen Sie nur die Adresse kopieren, die Sie reg_pc möchten, und für bedingte Sprünge eine andere TST-Adresse reservieren, die einen Wert ungleich Null in 1 umwandelt, und gleichzeitig den Befehlszähler um 2 anstelle von 1 erhöhen, um den nächsten Befehl zu überspringen, wenn das Ergebnis nicht 0 ist.


  always @ (posedge clk) begin if (reset) begin reg_pc <= 0; end else begin reg_reg <= data_w; if (addr_w == PC) begin reg_pc <= data_w; end else begin reg_pc <= reg_pc + (((addr_w == TST) && data_w[0]) ? 2 : 1); case (addr_w) PORT: reg_port <= data_w; endcase end end end endmodule 

cpu.v.
 module rom1r(addr_r, data_r); parameter ADDR_WIDTH = 8; parameter DATA_WIDTH = 8; input [ADDR_WIDTH - 1 : 0] addr_r; output [DATA_WIDTH - 1 : 0] data_r; reg [DATA_WIDTH - 1 : 0] mem [0 : (1<<ADDR_WIDTH) - 1]; initial $readmemh("rom.txt", mem, 0, (1<<ADDR_WIDTH) - 1); assign data_r = mem[addr_r]; endmodule module ram1r1w(write, addr_w, data_w, addr_r, data_r); parameter ADDR_WIDTH = 8; parameter DATA_WIDTH = 8; input write; input [ADDR_WIDTH - 1 : 0] addr_r, addr_w; output [DATA_WIDTH - 1 : 0] data_r; input [DATA_WIDTH - 1 : 0] data_w; reg [DATA_WIDTH - 1 : 0] mem [0 : (1<<ADDR_WIDTH) - 1]; assign data_r = mem[addr_r]; always @ (posedge write) mem[addr_w] <= data_w; endmodule module cpu(clk, reset, port); parameter WIDTH = 8; parameter RAM_SIZE = 8; parameter ROM_SIZE = 8; parameter PC = 0; parameter CG = 1; parameter TST = 2; parameter ADD = 3; parameter SUB = 4; parameter PORT = 5; input clk, reset; output [WIDTH-1 : 0] port; wire [WIDTH-1 : 0] addr_r, addr_w, data_r, data_w, data; reg [WIDTH-1 : 0] reg_pc; reg [WIDTH-1 : 0] reg_reg; reg [WIDTH-1 : 0] reg_port; assign port = reg_port; rom1r rom(reg_pc, {addr_w, addr_r}); defparam rom.ADDR_WIDTH = ROM_SIZE; defparam rom.DATA_WIDTH = RAM_SIZE * 2; ram1r1w ram (clk, addr_w, data_w, addr_r, data_r); defparam ram.ADDR_WIDTH = RAM_SIZE; defparam ram.DATA_WIDTH = WIDTH; assign data = (addr_r == PC) ? reg_pc : (addr_r == PORT) ? reg_port : data_r; assign data_w = (addr_w == CG) ? addr_r : (addr_w == TST) ? |data : (addr_w == ADD) ? data + reg_reg : (addr_w == SUB) ? data - reg_reg : data; always @ (posedge clk) begin if (reset) begin reg_pc <= 0; end else begin reg_reg <= data_w; if (addr_w == PC) begin reg_pc <= data_w; end else begin reg_pc <= reg_pc + (((addr_w == TST) && data_w[0]) ? 2 : 1); case (addr_w) PORT: reg_port <= data_w; endcase end end end endmodule 

Das ist der ganze Prozessor.


Assembler


Jetzt schreiben wir für ihn ein einfaches Programm, das die Werte einfach nacheinander an den Port ausgibt und bei 5 stoppt.


Es war zu faul, selbst einen Assembler zu schreiben, selbst einen so einfachen (die gesamte Syntax A = B). Stattdessen wurde die vorgefertigte Lua-Sprache als Basis verwendet, die sich sehr gut zum Erstellen verschiedener domänenspezifischer Sprachen eignet. Gleichzeitig erhalten wir einen vorgefertigten Lua-Präprozessor kostenlos .


Zunächst die Ankündigung von Sonderadressen, deren Eintrag die Daten und die Zählervariable an Adresse 7 ändert


 require ("asm") PC = mem(0) CG = mem(1) TST = mem(2) ADD = mem(3) SUB = mem(4) PORT = mem(5) cnt = mem(7) 

Anstelle von Makros können Sie die üblichen Lua-Funktionen verwenden. Aufgrund der Tatsache, dass die umgebungsmetatable _G geändert wurde, um Zuweisungen abzufangen (siehe unten), fielen gleichzeitig globale Variablen ab: Die Deklaration der nichtlokalen Variablen some_variable = 0xAA betrachtet unser Assembler als seine eigene und versucht zu analysieren Um eine globale Präprozessorvariable zu deklarieren, müssen Sie stattdessen rawset (_G, some_variable, 0xAA) verwenden, das keine Metamethoden berührt.


 function jmp(l) CG = l PC = CG end 

Beschriftungen werden durch die Wortbezeichnung und die Zeichenfolgenkonstanten gekennzeichnet. In Lua können bei einem einzelnen Zeichenfolgenargument für die Funktion die Klammern weggelassen werden.


 label "start" 

Stellen Sie den Portzähler auf Null und registrieren Sie:


 CG = 0 cnt = CG PORT = CG 

Laden Sie in der Schleife die Konstante 1, fügen Sie sie der Zählervariablen hinzu und zeigen Sie sie im Port an:


 label "loop" CG = 1 ADD = cnt -- add = cnt + 1 cnt = ADD PORT = ADD 

Fügen Sie die fehlende zum Überlauf bei 0 hinzu und gehen Sie, wenn es keine Null gibt, zum Anfang und überspringen Sie CG = "exit", andernfalls enden wir in einer Endlosschleife "exit".


 CG = -5 ADD = ADD --add = add + 251 CG = "loop" TST = ADD --skip "exit" if not 0 CG = "exit" PC = CG label "exit" jmp "exit" 

test.lua
 require ("asm") PC = mem(0) CG = mem(1) TST = mem(2) ADD = mem(3) SUB = mem(4) PORT = mem(5) cnt = mem(7) function jmp(l) CG = l PC = CG end label "start" CG = 0 cnt = CG PORT = CG label "loop" CG = 1 ADD = cnt -- add = cnt + 1 cnt = ADD PORT = ADD CG = -5 ADD = ADD --add = add + 256 - 5 CG = "loop" TST = ADD --skip "exit" if not 0 CG = "exit" PC = CG label "exit" jmp "exit" 

Und jetzt der Assembler asm.lua selbst, wie es in 20 Zeilen sein sollte:


In der mem-Funktion (zum Deklarieren spezieller Adressen) würde man auch die automatische Zuweisung der nächsten freien Adresse hinzufügen, wenn man nicht als Argument angegeben ist.
Und für Tags müssten Sie überprüfen, ob ein vorhandenes Tag erneut deklariert wurde


 local output = {} local labels = {} function mem(addr) return addr end function label(name) labels[name] = #output end 

Lua verfügt nicht über eine Metamethode für die Zuweisung, es gibt jedoch Metamethoden zum Indizieren vorhandener Werte und zum Hinzufügen neuer Werte, einschließlich der globalen Umgebungstabelle _G.
Da __newindex nur für Werte funktioniert, die nicht in der Tabelle vorhanden sind, anstatt neue Elemente zu _G hinzuzufügen, müssen Sie sie irgendwo ausblenden, ohne sie zu _G hinzuzufügen, und sie entsprechend herausholen, wenn auf sie über __index zugegriffen wird.


Wenn der Name bereits vorhanden ist, fügen Sie diese Anweisung dem Rest hinzu.


 local g = {} setmetatable(_G, { __index = function(t, k, v) return g[k] end, __newindex = function(t, k, v) if g[k] then table.insert(output, {g[k], v}) else g[k]=v end end }) 

Nun, nachdem der Assembler-Programm ausgeführt wurde und der Garbage Collector schließlich mit unserem Ausgabeprogramm für das Array ankommt, drucken wir ihn einfach aus und ersetzen gleichzeitig die Textbeschriftungen durch die richtigen Adressen.


 setmetatable(output, { __gc = function(o) for i,v in ipairs(o) do if type(v[2]) == "string" then v[2] = labels[v[2]] or print("error: ", v[2]) end print(string.format("%02X%02X", v[1] & 0xFF, v[2] & 0xFF)) end end }) 

asm.lua
 local output = {} local labels = {} function mem(addr) return addr end function label(name) labels[name] = #output end local g = {} setmetatable(_G, { __index = function(t, k, v) return g[k] end, __newindex = function(t, k, v) if g[k] then table.insert(output, {g[k], v}) else g[k]=v end end }) setmetatable(output, { __gc = function(o) for i,v in ipairs(o) do if type(v[2]) == "string" then v[2] = labels[v[2]] or print("error: ", v[2]) end print(string.format("%02X%02X", v[1] & 0xFF, v[2] & 0xFF)) --FIX for WIDTH > 8 end end }) 

Durch Ausführen von lua53 test.lua> rom.txt ( oder online ) erhalten wir ein Programm für den Prozessor in Maschinencodes.


rom.txt
 0100 0701 0501 0101 0307 0703 0503 01FB 0303 0103 0203 010D 0001 010D 0001 

Zur Simulation erstellen wir eine einfache Testbench, die nur den Reset freigibt und die Fetzen zieht.


test.v
 `include "cpu.v" module test(); reg clk; reg reset; wire [7:0] port; cpu c(clk, reset, port); initial begin $dumpfile("test.vcd"); reset <= 1; clk <= 0; #4 reset <= 0; #150 $finish; end always #1 clk <= !clk; endmodule 

Nachdem Sie mit iverilog -o test.vvp test.v simuliert haben, öffnen Sie die resultierende test.vcd in GTKWave:

Der Port zählt bis fünf, und dann schleift der Prozessor.


Jetzt, da es einen minimal arbeitenden Prozessor gibt, können Sie den Rest der arithmetischen, logischen Operationen, Multiplikation, Division, Gleitkomma, Trigonometrie, Register für indirekten Speicherzugriff, Stapel, Hardwarezyklen, verschiedene Peripheriegeräte nach Bedarf hinzufügen ... und mit dem Sägen beginnen Backend für llvm.

Source: https://habr.com/ru/post/de433342/


All Articles