verilogのもう1つの単純なプロセッサ

この記事では、別のプリミティブプロセッサとそのためのアセンブラについて説明します。

通常のRISC /ISCの代わりに、プロセッサ自体に命令セットはありません。コピー命令は1つだけです。







同様のプロセッサにはマキシムのMAXQシリーズがあります。







まず、ROM、プログラムメモリについて説明しましょう







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
      
      





データメモリ用のデュアルポートRAM







 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
      
      





プロセッサ自体







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





少なくとも、コマンドカウンターのレジスターと、1つの補助レジスター、およびIOポートレジスターが必要です。そのため、プロセッサーから何かを見せることができます。







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





コマンドカウンターは、プログラムメモリのアドレスになります。







  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;
      
      





倍幅のプログラムメモリには、デュアルポートデータメモリへのデータのコピー元とコピー元の2つのアドレスが含まれています。







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





特別なアドレスを指定します:コマンドカウンター、定数ジェネレーター、0のチェック(条件付きジャンプの場合)、加算/減算演算、および入出力ポート(この場合は出力のみ)。







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





2つのメモリポートのデータバスは相互接続されているだけでなく、マルチプレクサを介して接続されており、同時にALUとして機能します。







1つのマルチプレクサ-特定のアドレスのメモリではなく、コマンドカウンター(相対遷移用)、IOなどを読み取るための読み取りポートのデータバス上







2番目は、メモリ内のデータを転送するだけでなく、特定のアドレスに書き込むときに変更するために、記録ポートのデータバス上にあります。







  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;
      
      





算術演算に使用される補助レジスタreg_regは直接アクセスできませんが、各命令の結果はそこにコピーされます。







したがって、メモリから2つの値を追加するには、最初にそれらのいずれかを読み取る必要があります(たとえば、それを自分自身にコピーし(同時にreg_regで))、加算器アドレスでの次の書き込みコマンドは、前の値との合計をそこに記録します。







定数ジェネレーターは、メモリ値ではなくアドレスをこのアドレスに書き込みます。







無条件ジャンプの場合は、目的のアドレスをreg_pcにコピーするだけです。条件ジャンプの場合は、別のTSTアドレスを予約します。これにより、ゼロ以外の値が1に変わり、同時にコマンドカウンターが1ではなく2増加して、結果が0でない場合は次のコマンドがスキップされます。







  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
      
      





これがプロセッサ全体です。







アセンブラー



ここで、ポートに値を順番に出力し、5で停止する単純なプログラムを彼のために作成します。







このような単純なもの(構文全体はA = B)であっても、自分でアセンブラーを書くのは面倒でした。そのため、代わりに既製のLua言語がベースとして使用され、これに基づいてさまざまなドメイン固有言語を構築するのに非常に適していますが、同時に既製のLuaプリプロセッサーを取得します。







まず、特別なアドレスの発表、データを変更するエントリ、およびアドレス7のカウンタ変数







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





マクロの代わりに、通常のLua関数を使用できますが、環境メタテーブル_Gが割り当てをキャッチするために変更されたという事実により(以下を参照)、グローバル変数は同時に落ちました:非ローカル変数some_variable = 0xAAの宣言はアセンブラーが自分のものであると見なし、解析しようとします代わりに、グローバルプリプロセッサ変数を宣言するには、メタメソッドに触れないrawset(_G、some_variable、0xAA)を使用する必要があります。







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





ラベルは単語ラベルと文字列定数で示されます; Luaでは、関数の単一の文字列引数の場合、括弧は省略できます。







 label "start"
      
      





ポートカウンターをゼロにして登録します。







 CG = 0 cnt = CG PORT = CG
      
      





ループで、定数1をロードし、カウンター変数に追加してポートに表示します。







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





不足しているものを0のオーバーフローに追加し、ゼロがない場合は、CG = "exit"をスキップして先頭に進みます。そうでない場合は、無限ループ "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"
      
      





そして、アセンブラasm.lua自体は、20行になっているはずです。







(特別なアドレスを宣言するための)mem関数では、引数として指定されていない場合、次の空きアドレスの自動割り当ても追加します。

また、タグについては、既存のタグの再宣言を確認する必要があります







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





Luaには割り当てのためのメタメソッドはありませんが、_Gグローバル環境テーブルなど、既存の値にインデックスを付けたり、新しい値を追加したりするためのメタメソッドがあります。

__newindexはテーブルに存在しない値に対してのみ機能するため、_Gに新しい要素を追加する代わりに、_Gに追加せずにどこかで非表示にし、したがって、__ indexを介してアクセスされたときにそれらを取得する必要があります。







名前が既に存在する場合、この命令を残りに追加します。







 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)) 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 })
      
      





lua53 test.lua> rom.txt( またはonline )を実行することにより、マシンコードでプロセッサのプログラムを取得します。







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





シミュレートするために、リセットを解除してシュレッドをプルするだけの簡単なテストベンチを作成します。







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
      
      





iverilog -o test.vvp test.vを使用してシミュレートしたら、GTKWaveで結果のtest.vcdを開きます。



ポートは5にカウントされ、プロセッサーはループします。







プロセッサの動作が最小限になったので、算術演算、論理演算、乗算、除算、浮動小数点、三角法、間接メモリアクセス用のレジスタ、スタック、ハードウェアサイクル、さまざまな周辺機器を必要に応じて追加し、ソーイングを開始できます。 llvmのバックエンド。








All Articles