目的
CPUってなんか難しそうで敷居が高いと思いつつX年
東大のCPU実験で自作コア上の自作OS上で自作シェルを動かした話
http://yamaguchi-1024.hatenablog.com/entry/2018/02/27/172417
を読んでCPU設計カッコイイと思い自分もつくってみた。
せっかくVivadoが入っているPCもあるし。
マイクロプロセッサの設計と実装
http://www.mtl.t.u-tokyo.ac.jp/~jikken/cpu/wiki/
と思ってもとっかかりが必要なので上記でも触れられていた東大の名物授業であるCPU実験の内容をなぞってみる。
坂井研のCPU実験のリンクが切れてますね。。(2021/9)
https://qiita.com/JugglerShu@github/items/1c0cd220a9d106ca0240
こちらとか参考になります。
環境
Vivado 2016/4を使用。
ザイリンクスFPGAを買うとライセンスが付いてくる。
無料のRTLコンパイラでもコンパイルできるはず。(Verilogに対応していれば)
例えば下記のソフトは無料。
Altera社model sim
http://www.altera.co.jp/products/software/quartus-ii/modelsim/qts-modelsim-index.html
設計目標
とりあえずはMIPS式の足し算しかできないCPUを作るのが目標!!!
足し算とメモリアクセスするだけでも立派なCPUなハズ。
具体的なアルゴリズムは東大演習のSample1をまずは動かす。
Sample1
#0の準備
XOR r0, r0, r0
#1項めの計算
ADDI r8, r0, 1
ADD r9, r0, r8
#2項めの計算(以下繰り返し)
ADDI r8, r8, 1
ADD r9, r8, r9
ADDI r8, r8, 1
ADD r9, r8, r9
ADDI r8, r8, 1
ADD r9, r8, r9
ADDI r8, r8, 1
ADD r9, r8, r9
ADDI r8, r8, 1
ADD r9, r8, r9
ADDI r8, r8, 1
ADD r9, r8, r9
ADDI r8, r8, 1
ADD r9, r8, r9
ADDI r8, r8, 1
ADD r9, r8, r9
#10項めの計算
ADDI r8, r8, 1
ADD r9, r8, r9
HALT
http://www.mtl.t.u-tokyo.ac.jp/~jikken/cpu/wiki/%E5%85%A5%E5%8A%9B%E7%94%A8%E3%82%A2%E3%82%BB%E3%83%B3%E3%83%96%E3%83%AA%E3%81%AE%E4%BE%8B%E3%81%A8%E6%9B%B8%E3%81%8D%E6%96%B9/
アルゴリズム的には:
1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 = 55
を計算しているだけ。ただ上記を単純に書くと10種の直値を使うことになる(そうしてもいいんだけど・・・)
ここでは一種類の直値(ADDI r8, r0, 1)を使って上記アルゴリズムを実現する。
ちなみにADDIというのは直値加算
r8 = r0 + 1
が計算され、 結果保存レジスタ(r8) = 加算するレジスタ値(r0) + 定数(1)
の形になっている。この辺りは教科書読んだほうが良い。気力があったらMIPS命令の解説をする。。?
命令セット
基本的にはMIPSに従うが東大演習のは割当が本家と微妙に違う。
今回は東大演習に書いてあるものに従って作る。
ISA の仕様
http://www.mtl.t.u-tokyo.ac.jp/~jikken/cpu/wiki/ISA%20%E3%81%AE%E4%BB%95%E6%A7%98/
命令セット形式
これは本家MIPSと同等。
今回使うのはRegとImmediateのみ。
実装するALU命令
Add ImmがADDIに当たる。
Subは使わないけどなんとなく実装してみた。書くだけなら簡単だし。
Funct割当が本家MIPSと違う東大MIPS。まずはフローを捉えてからALU命令は足していくとする。
コンピュータアーキテクチャ
Verilog書く前にその前にCPUアーキテクチャを考える。
といっても素人なもんで教科書通りにしか作れない.
素人CPU回路図
基本的な単一サイクルプロセッサ。でも回路図にするとなんかコンピュータアーキテクチャぽい。
大きくはプログラムカウンタ、レジスタ、ALUと3ブロックを作成し、配線でデコーダを作る。
メリット:分岐や別メモリもないので凄く作りやすい!
デメリット:素人丸出し。動けばいいのだ。
東大演習に当てはめるとstep 4に近い?命令分岐はまだない。
(http://www.mtl.t.u-tokyo.ac.jp/~jikken/cpu/wiki/%E9%96%8B%E7%99%BA%E6%89%8B%E9%A0%86/ より引用)
設計
ブロックの設計を示す。初Verilogでドキドキ。。
文法については
が参考になったので共有。本とかは読まなくてもいけた。
## プログラムカウンタとデコーダ
プログラムカウンタ本体はシングルポートレジスタで記述。
そのアドレスをカウンタで指定するようにする。
module PC(
input CLK,
input RST,
output [31:0] INST
);
wire write = 1'b0; // dont write..
wire [31:0] data_in = 32'b0;
wire [31:0] data_out;
assign INST = data_out;
wire [4:0] addr;
// call program counter
COUNTER5B COUNTER(
.CLK(CLK), .addr(addr), .RST(RST)
);
// call program memory
CPU_REG PMEM(
.addr(addr), .data_in(data_in), .data_out(data_out), .write(write));
endmodule
module CPU_REG(
input [4:0] addr, //32-words MIPS like
input [31:0] data_in,
output [31:0] data_out,
input CLK,
input write
);
reg [31:0] PROGRAM[31:0];
initial $readmemb("バイナリファイルの置き場所", PROGRAM);
assign data_out = PROGRAM[addr];
always @(posedge CLK) if(write) PROGRAM[addr] <= data_in;
endmodule
initial $readmemb("バイナリファイルの置き場所", PROGRAM);
ここでバイナリファイルをロードすることで命令を読み込んでいる。
詳しくは
http://www.mtl.t.u-tokyo.ac.jp/~jikken/cpu/wiki/RAM%E3%81%AE%E6%9B%B8%E3%81%8D%E6%96%B9/
のRAMの初期化を参照。
ちなみにカウンタ回路
グローバルリセットじゃないと上手く動かなかった。
module COUNTER5B(
input CLK,
input RST,
output [4:0] addr);
// 2^5 (32-state) counter
reg [4:0] count;
assign addr = count;
always @(posedge CLK) begin
if(RST == 1) begin
count = 5'b0;
end
else begin
count = count + 1;
end
end
endmodule
## レジスタ
2R1Wレジスタを
Verilogでプロセッサを作ろう
https://qiita.com/thtitech/items/78d6ac9ef48d242d873d#instruction-fetch%E3%82%B9%E3%83%86%E3%83%BC%E3%82%B8
を参考に作成。
デジタル設計では自動生成してくれるの有り難い。手で設計するの面倒くさい
参考設計よろしくREGアドレス0を読み出そうとすると(r0)ゼロが読み出されるようにしている。
そうすると何かと有用だ。
書き込みはCLK rise edgeで起きる。
module GPR(
input CLK,
input [4:0] REGNUM0, REGNUM1, REGNUM2,
input [31:0] DIN0,
input WE0,
output [31:0] DOUT0, DOUT1
);
reg [31:0] r[15:0];
assign DOUT0 = (REGNUM0==0) ? 0 : r[REGNUM0];
assign DOUT1 = (REGNUM1==0) ? 0 : r[REGNUM1];
always @(posedge CLK) if (WE0) r[REGNUM2] <= DIN0;
endmodule
## ALU
ALUsrc: 直値を使うかレジスタ値を読み出すか選択。
命令から得る直値は16bしかないため、ALU内で32bに拡張している。
もっと綺麗に書ける気はする
module ALU(
input [5:0] opcode,
input [31:0] ina,
input [31:0] inb,
input ALUsrc,
input [5:0] funct,
input [31:0] INST,
output [31:0] out, ALUA, ALUB,
output flag
);
// 直値を使うかレジスタ値を使うかALUsrcによってswitch
always @(*) begin
if(ALUsrc) begin
ALUB2 = imm_i;
end
else begin
ALUB2 = inb;
end
end
assign flag = tout[32]; //Overflow Flag
assign out = tout[31:0];
// ALU function
always@(*) begin
if(opcode==6'd0) begin
case(funct)
6'd0: // add
tout = ALUA + ALUB;
6'd2: // sub
tout = ALUA - ALUB;
6'd10: // XOR
tout = ALUA ^ ALUB;
default:
tout = ALUA + ALUB;
endcase
end
else begin //直値の場合
tout = ALUA + ALUB;
end
end
endmodule
テストベンチ
結果:
綺麗に出るね!!
そしてVivadoめちゃ使いやすい。デバッグもやりやすいっす。
FPGAに書いてみたいけど今は手元にない。。
目標であるレジスタ9に55が書き込まれ、正常動作を確認。
やったね。
module tb_mips_v2();
// CLK
reg clk;
reg RST;
// REG
reg write;
// for INST
//reg [31:0] INST;
wire [31:0] INST;
// monitor
wire flag;
wire [31:0] out, reg_out, ina, inb, ALUA, ALUB;
wire [31:0] DOUT0;
wire [4:0] addr;
// top circuit
top_module_mips_v2 t0(
.CLK(clk), .write(write), .flag(flag), .out(out),
.ina(ina), .inb(inb),.ALUA(ALUA),.ALUB(ALUB), .INST(INST), .addr(addr), .RST(RST)
);
initial begin
clk <= 1'b0;
write <= 1'b1;
RST <= 1'b1;
end
always #5 begin
clk <= ~clk;
end
task wait_posedge_clk;
input n;
integer n;
begin
for(n=n; n>0; n=n-1) begin
@(posedge clk)
;
end
end
endtask
initial begin
wait_posedge_clk(2);
RST <= 1'b0;
wait_posedge_clk(50);
$finish;
end
endmodule
命令セットは東大演習のsimple1.binをそのまま書き込んでます。
#Github code
https://github.com/arutema47/mips_cpudesign/tree/developing/v1
tb_mips_v2.vがテストベンチファイル。
## 完走した感想
足し算しているだけだけど動作した時はかなり感動した。
CPUに及び腰になってる人は是非設計してみてほしい。
こんなCPUだけど書いてみるとアーキテクチャの基本を理解するのに役立つ。
でもARM CPUも命令セットは基本的にはこれなのでもうちょっと発展させて理解を深めたい。
なんか設計報告書書いてる気分
参考図書
デジタル回路設計とコンピュータ・アーキテクチャ 7章
コンピュータの構成と設計 上
TODO
単サイクルプロセッサ完成バージョンについて記事執筆
パイプラインプロセッサの実装