やかんです。
右も左も分かりませんが、頑張ってプロセッサを作ろうとしています。
できることから始める。
前回記事では、全体の計画的なものを考えました。これ↓
- 命令セットを定義しよう
- コンポーネントを設計しよう
- verilogを実際に書こう
- gtkwaveを使ってシミュレーションしよう
とりあえずテキトーに決めたわけですが、本当に何もわからないということで、とにかく最終目標を「プロセッサ完成」に据えて手当たり次第色々やっていこうと思います。
ほんとに暗中模索
だから流れとしては多分
- デコーダ作る
- デコーダを作る中で命令せっとも頑張って定義。
- オペランドいじる。符号拡張とか。
- 演算装置作る
- プログラムカウンタをいじる
etc…
みたいなことになるのではと思っていますが、まあ、やってみないとわからない。
まずはレジスタの演算を作るところから始めてみては?
理想を語れば、プログラムカウンタ、デコーダ、即値延算のための演算器みたいなやつ、、、と色々作りたいものがあります。
ですが、まずは「レジスタを用いつつ、命令を1つ実行する」ことから始めるべきではないか、と思いました。
モジュールに分ける必要すらないのでは?
デコーダとかPCとかを分けて考えると、「困難の分割」ということでいい手を打っているような気がしますが、実際に作ろうとすると障害が多い気がします。
だからまずは分けずにベタ書きしていこう
Verilogの復習
シンプルにまだverilogに慣れていないので、都度都度復習しながら取り組みます。
- ノンブロッキング代入とブロッキング代入の違い
- ブロッキング代入は、 代入が完了するまで次の文の実行をブロックする。なんか、ソフトウェアっぽいイメージだ。
- ノンブロッキング代入の方は、よりハードウェアに近いという理解で正しいか?次の文の実行をブロックすることがない。1つのサイクル内でとりあえず並行で実行される。
- 継続的代入ってなんだっけ?
- ネット型に対して行うもの。ネット型はレジスタを表現しているわけじゃなくて、配線とかその辺のやつを表現している。レジスタなら値を保持可能だけど配線はそうもいかないから、配線に対しては継続的に値を代入する必要がある。
- じゃあ、継続的代入とかしないで値を保持する必要があるならレジスタ型を使用すれば良いのでは?
- まだ理解できないが、ケースに応じてネット型とレジスタ型を使い分けるのが玄人らしい。
とりあえず書いてみた。
GPT-4くん提供のコードをちょいちょい変えつつ写経。
module cpu(
input wire clk, // クロック信号
input wire rst, // リセット信号
output wire[31:0] result
);
reg [7:0]mem[0:255];
reg [7:0] instruction;
reg[31:0] alu_operand_a;
reg[31:0] alu_operand_b;
wire [31:0] alu_result;
reg [7:0] pc;
assign result = alu_result;
always @(posedge clk) begin
if(rst) begin // リセット処理
pc <= 8'b00000000;
alu_operand_a <= 32'b0;
alu_operand_b <= 32'b0;
end else begin // リセットしないで命令実行
instruction <= mem[pc];
pc <= pc + 8'b00000001;
alu_operand_a <= {24'b0, instruction};
alu_operand_b <= {24'b0, instruction} + 32'b1;
end
end
assign alu_result = alu_operand_a + alu_operand_b;
endmodule
疑問とその回答。
- なんでoutputはネット型(wire)なの?
- 実際の「CPU」という演算装置を考えれば、演算結果は「保持される」というより「伝えられる」イメージだよね。ってことで、出力結果は「配線を流れる」ってことでネット型。
テストベンチ作るよ
最近ようやく、かろうじて「テストベンチを作って動作確認」という文化に馴染みつつあります。
また、講義において「HDLにおいてはロギングというものをモジュールの外部で定義することができる」といった内容が扱われていましたが、それについても最近馴染みつつあります。
てなわけで、GPT-4の助けも借りつつ作成したテストベンチがこちら。
module cpu_test;
reg clk;
reg rst;
wire [31:0] result;
cpu uut (
.clk(clk),
.rst(rst),
.result(result)
);
always begin
#10 clk = ~clk;
end
task initialize;
begin
$display("%t: Resetting the CPU...", $time);
rst = 1;
#40;
rst = 0;
$display("%t: CPU Reset complete.", $time);
end
endtask
task init_memory;
begin
$display("%t: Initializing memory...", $time);
uut.mem[0] = 8'h01;
uut.mem[1] = 8'h02;
uut.mem[2] = 8'h03;
$display("%t: Memory initialization complete.", $time);
end
endtask
initial begin
clk = 0;
$display("%t: Starting the simulation", $time);
initialize();
init_memory();
#400;
$display("%t: Simulation ends. Result = %d", $time, result);
$finish;
end
initial begin
$monitor("Time=%t, Result=%d", $time, result); // 結果をリアルタイムでモニタリング
end
endmodule
出力がこちら。
$ vvp cpu_test.vvp
0: Starting the simulation
0: Resetting the CPU...
Time= 0, Result= x
Time= 10, Result= 0
40: CPU Reset complete.
40: Initializing memory...
40: Memory initialization complete.
Time= 50, Result= x
Time= 70, Result= 3
Time= 90, Result= 5
Time= 110, Result= 7
Time= 130, Result= x
440: Simulation ends. Result = x
cpu_test.v:43: $finish called at 440 (1s)
うーん、、うまくいってないな。。何かがおかしいみたいだ。なんでだろう?
とりあえず以下質疑応答。
- taskとfunctionの違いとは?
- 正直よくわかってない。taskには時間の概念があるから今回のケースには合致してそう。functionには時間の概念がない。
今回のまとめ
難しい。
これに尽きるぜ。
でも、テストベンチの扱いとかメモリにモックデータみたいなやつ渡す方法とか、ちょっとずつ分かりつつある。まあ、慣れるしかないよねー、、
ということで、今回は終了。最後までお読みいただき、ありがとうございます。