始めに
ChatGPTを使ってのプログラミングが取りざたされて早くも2年以上が経ちます。初期の頃にプログラムを出力できると聞いて、verilogでもできるかと少し試して、まったく使い物にならなかったのでやめてしまったことがありました。月日は流れて、先日、支持を積み重ねることによりpythonだけでなくCでもSDL2を使ったリアルタイムゲームを作成することができることを体験し、それならverilogでもかなりいけるのでは?と挑戦することにしました。
挑戦するにあたり、やはり作るなら簡単でもCPUを自作できると面白いなと思い、実験の最終目標をCPU上でLチカプログラムを動かし、FPGAで動作させることにしました。
目標仕様
- 8bit CPU 命令セットはお任せ(途中で追加も可)
- プログラムはROMに収める
- パイプラインはやらない(1回目で失敗したため)
- I/Oを1ピン出してLEDを操作する
- FPGAトップ層は無理そうなので、手書きする
- CPUのモジュールには一切手を入れない
- テストベンチも自動で生成する
トライ1回目
以下入力したプロンプトのみの記録
- verilogで8bit命令長の簡単なCPUを作って
- CPU内部でinitial文は使わないで
- ハーバートアーキティクチャで書き直して
- パイプライン化して書き直してください
※ここが失敗のもと・・・ - 1ビットのアウトプット信号を追加してそれを操作できる命令を追加して
- out_flagの先にLEDがついていると仮定して点滅させるプログラムを入れて
- ディレイカウンタを4段にして
- ディレイカウンタを3段に直して
- ディレイカウンタを4段に直して
- このCPUのシミュレーションコードを作って
- initializedをなくして動くようにして
- initialized もinitial文も使わない書き方をして
- $readmemhは使わずにinstr_memをROMとして固定値を書いてください
- pcが複数個所で代入されるバグを直して
- pcが定義されていないとえらーになります
- pcが2サイクルに1回しか更新されないバグを直して
- 修正されていないので、pcの計算はpcだけで閉じるように修正して
- pcが複数のalwaysで変更されているバグを直して
- それでは2クロックに1回しか動かないので、next_pcを使わずに直接ジャンプアドレスをpcに代入して
- pcへの重複代入をやめて、かつnext_pcを使わずに直接ジャンプアドレスをpcに代入して
- if_pcを使わないでpcを直接使って
- 動作前にデータメモリをクリアする回路を追加して
- out_flagが変化しないバグを修正して
- out_flagが交互に同じサイクルで変化するように変更して
- マイクロコードでout_flag が 0x1ffff サイクルごとに交互にトグルされるようコードと回路を構成して
⇒ コードがぐちゃぐちゃになりあきらめる・・・
トライ2回目
プロンプトの記録
- 簡単な8bitCPUをverilogでinitial文を使わずに作って
- このCPUに1ビットのアウトプットピンを追加し、命令で値を操作可能にして
- このCPUでout_pinを0x10000サイクル毎に値を変更するプログラムをROMで同祭して
- このCPUをシミュレーションするテストベンチを作って
- マイクロコードに最初にデータを256バイトの0クリアを行う初期化ルーチンを追加して
- 命令セットを1~2命令だけ拡張して
- initial命令とintegerを使わない形式に直して
- out_pinの値が0x1fffサイクル以上の周期となるようにマイクロコードを変更して
- out_pinが0に固定されるバグがあります。マイクロコードを直してください
- ループが終わらないバグがあります。修正してください。
- まだトグルが観測できないので、CPUのバグを直して
- 動かないので、トグルを128サイクル毎に変更したマイクロコードにして
- マイクロコードを128サイクルから65000サイクルに桁あふれに注意して延ばして
- Bがループとトグル値の両方に使われている間違いがあります。レジスタCを追加してトグル値に使用してください
- Bの値が変化しないバグがあります。修正してください
- Bのループが0xffから0xfeになった後0xffになるバグがあります。修正してください
- バグが修正されていません。マイクロコードに問題があります。
- Dレジスタを追加し、トグルの間隔を0xffffffのマイクロコードにしてください
以上で以下の仕様のCPUの中にLチカコードが入っているverilogを生成できました。
以下仕様になります。
✅ 命令セット一覧(Opcodeベース)
Opcode 命令名 内容 備考
0x01 LOAD A, imm A ← imm
0x02 ADD A, imm A ← A + imm 減算時は imm = 0xFF
0x03 STORE A, [addr] mem[addr] ← A
0x04 JMP addr PC ← addr 無条件ジャンプ
0x05 OUT B[0] out_pin ← B[0] 未使用
0x06 MOV [A], B mem[A] ← B
0x07 JNZ A, addr if A ≠ 0: PC ← addr, else PC ← PC + 2
0x08 LOAD B, imm B ← imm
0x09 XOR B, imm B ← B ^ imm
0x0A OUT C[0] out_pin ← C[0] 実際の出力に使用
0x0B XOR C, imm C ← C ^ imm
0x0C ADD B, imm B ← B + imm 未使用
0x0D SUB B, imm B ← B - imm
0x0E JNZ B, addr if B ≠ 0: PC ← addr
0x10 LOAD D, imm D ← imm ★ 拡張命令(NEW)
0x11 SUB D, imm D ← D - imm ★ 拡張命令(NEW)
0x12 JNZ D, addr if D ≠ 0: PC ← addr ★ 拡張命令(NEW)
🔧 レジスタ構成
A, B, C, D: 汎用8bitレジスタ
C[0] が out_pin に接続され、XORトグルに使われている
以下が生成されたコードになります。
module Simple8bitCPU (
input wire clk,
input wire rst,
output reg out_pin
);
reg [7:0] pc;
reg [7:0] A, B, C, D;
reg [7:0] instr_mem [0:255];
reg [7:0] data_mem [0:255];
wire [7:0] opcode = instr_mem[pc];
wire [7:0] operand = instr_mem[pc + 1];
reg [7:0] rom_init_counter;
reg rom_initialized;
wire [7:0] rom_program [0:25];
// ▼ ROM内容(マイクロコード)
assign rom_program[ 0] = 8'h10; // LOAD D, 0xFF
assign rom_program[ 1] = 8'hFF;
assign rom_program[ 2] = 8'h08; // LOAD B, 0xFF
assign rom_program[ 3] = 8'hFF;
assign rom_program[ 4] = 8'h01; // LOAD A, 0xFF
assign rom_program[ 5] = 8'hFF;
assign rom_program[ 6] = 8'h02; // ADD A, 0xFF (A--)
assign rom_program[ 7] = 8'hFF;
assign rom_program[ 8] = 8'h07; // JNZ A, 0x06
assign rom_program[ 9] = 8'h06;
assign rom_program[10] = 8'h0D; // SUB B, 0x01
assign rom_program[11] = 8'h01;
assign rom_program[12] = 8'h0E; // JNZ_B 0x04
assign rom_program[13] = 8'h04;
assign rom_program[14] = 8'h11; // SUB D, 0x01
assign rom_program[15] = 8'h01;
assign rom_program[16] = 8'h12; // JNZ_D 0x02
assign rom_program[17] = 8'h02;
assign rom_program[18] = 8'h0A; // OUT C[0]
assign rom_program[19] = 8'h00;
assign rom_program[20] = 8'h0B; // XOR C, 0x01
assign rom_program[21] = 8'h01;
assign rom_program[22] = 8'h01; // LOAD A, 0xFF
assign rom_program[23] = 8'hFF;
assign rom_program[24] = 8'h04; // JMP 0x00
assign rom_program[25] = 8'h00;
// ▼ 実行部
always @(posedge clk) begin
if (rst) begin
pc <= 8'd0;
A <= 8'd0;
B <= 8'd0;
C <= 8'd0;
D <= 8'd0;
out_pin <= 1'b0;
rom_init_counter <= 8'd0;
rom_initialized <= 1'b0;
end else if (!rom_initialized) begin
instr_mem[rom_init_counter] <= rom_program[rom_init_counter];
rom_init_counter <= rom_init_counter + 1;
if (rom_init_counter == 8'd25)
rom_initialized <= 1'b1;
end else begin
case (opcode)
8'h01: begin // LOAD A, imm
A <= operand;
pc <= pc + 2;
end
8'h02: begin // ADD A, imm
A <= A + operand;
pc <= pc + 2;
end
8'h03: begin // STORE A -> [addr]
data_mem[operand] <= A;
pc <= pc + 2;
end
8'h04: begin // JMP addr
pc <= operand;
end
8'h05: begin // OUT B[0](未使用)
out_pin <= B[0];
pc <= pc + 2;
end
8'h06: begin // MOV [A], B
data_mem[A] <= B;
pc <= pc + 2;
end
8'h07: begin // JNZ A, addr
if (A != 8'd0)
pc <= operand;
else
pc <= pc + 2;
end
8'h08: begin // LOAD B, imm
B <= operand;
pc <= pc + 2;
end
8'h09: begin // XOR B, imm
B <= B ^ operand;
pc <= pc + 2;
end
8'h0A: begin // OUT C[0]
out_pin <= C[0];
pc <= pc + 2;
end
8'h0B: begin // XOR C, imm
C <= C ^ operand;
pc <= pc + 2;
end
8'h0C: begin // ADD B, imm(未使用)
B <= B + operand;
pc <= pc + 2;
end
8'h0D: begin // SUB B, imm
B <= B - operand;
pc <= pc + 2;
end
8'h0E: begin // JNZ_B addr
if (B != 8'd0)
pc <= operand;
else
pc <= pc + 2;
end
8'h10: begin // LOAD D, imm
D <= operand;
pc <= pc + 2;
end
8'h11: begin // SUB D, imm
D <= D - operand;
pc <= pc + 2;
end
8'h12: begin // JNZ_D addr
if (D != 8'd0)
pc <= operand;
else
pc <= pc + 2;
end
default: begin
pc <= pc + 1;
end
endcase
end
end
endmodule
Arty-A7 35Tに載せてLチカをしました。
使用したファイルを添付します。(これは手動でつくりました)
module fpga_top (
input clkin,
input rst_n,
output out_pin
);
wire clk; // input
wire rst = ~rst_n; // input
wire locked;
// Instantiation of the clocking network
//--------------------------------------
clk_wiz_0 clknetwork
(
// Clock out ports
.clk_out1 (clk),
// Status and control signals
.reset (~rst_n),
.locked (locked),
// Clock in ports
.clk_in1 (clkin)
);
Simple8bitCPU Simple8bitCPU (
.clk(clk),
.rst(rst),
.out_pin(out_pin)
);
endmodule
set_property OFFCHIP_TERM NONE [get_ports out_pin]
set_property IOSTANDARD LVCMOS33 [get_ports out_pin]
set_property DRIVE 12 [get_ports out_pin]
set_property SLEW SLOW [get_ports out_pin]
set_property PACKAGE_PIN G6 [get_ports out_pin]
set_property IOSTANDARD LVCMOS33 [get_ports clkin]
set_property PACKAGE_PIN E3 [get_ports clkin]
set_property IOSTANDARD LVCMOS33 [get_ports rst_n]
set_property PACKAGE_PIN C2 [get_ports rst_n]
create_clock -period 10.000 -name clkin -waveform {0.000 5.000} [get_ports clkin]
#create_generated_clock -name clk [get_pins fpga_top/clknetwork/clk_out1]
set_input_delay -clock [get_clocks -of_objects [get_nets clk]] -min -add_delay 1.000 [get_ports rst_n]
set_input_delay -clock [get_clocks -of_objects [get_nets clk]] -max -add_delay 1.000 [get_ports rst_n]
set_output_delay -clock [get_clocks -of_objects [get_nets clk]] -min -add_delay -3.000 [get_ports out_pin]
set_output_delay -clock [get_clocks -of_objects [get_nets clk]] -max -add_delay -3.000 [get_ports out_pin]
終わりに
途中カウンタがカウントダウンしないバグが出てマイクロコードなのかCPUなのかの切り分けが難しく、シミュレーションしながらのデバッグをしていたら4時間を超えていました。verilogのようなHDLという特殊な言語でもそれなりにプログラミングできることは実証できたのではないかと思っております。皆様もいかがでしょうか?