Help us understand the problem. What is going on with this article?

素人が作る超基本CPU@Verilog w/Vivado

More than 1 year has passed since last update.

環境

Vivado 2016/4を使用。
ザイリンクスFPGAを買うとライセンスが付いてくる。

無料のRTLコンパイラでもコンパイルできるはず。(Verilogに対応していれば)
例えば下記のソフトは無料。

Altera社model sim
http://www.altera.co.jp/products/software/quartus-ii/modelsim/qts-modelsim-index.html

きっかけ

ずっと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/

と思ってもとっかかりが必要なので上記でも触れられていた東大の授業のFPGAで行うCPU設計の内容をなぞってみる。
こんな必修があるなんて東大すごい。

設計目標

とりあえずは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/

命令セット形式

image.png
これは本家MIPSと同等。
今回使うのはRegとImmediateのみ。

実装するALU命令

image.png
Add ImmがADDIに当たる。
Subは使わないけどなんとなく実装してみた。書くだけなら簡単だし。
Funct割当が本家MIPSと違う東大MIPS。まずはフローを捉えてからALU命令は足していくとする。

コンピュータアーキテクチャ

Verilog書く前にその前にCPUアーキテクチャを考える。
といっても素人なもんで教科書通りにしか作れない.

素人CPU回路図

image.png
基本的な単一サイクルプロセッサ。でも回路図にするとなんかコンピュータアーキテクチャぽい。
大きくはプログラムカウンタ、レジスタ、ALUと3ブロックを作成し、配線でデコーダを作る。

メリット:分岐や別メモリもないので凄く作りやすい!
デメリット:素人丸出し。動けばいいのだ。

東大演習に当てはめるとstep 4に近い?命令分岐はまだない。

image.png
http://www.mtl.t.u-tokyo.ac.jp/~jikken/cpu/wiki/%E9%96%8B%E7%99%BA%E6%89%8B%E9%A0%86/ より引用)

設計

ブロックの設計を示す。初Verilogでドキドキ。。

文法については

Verilog入門
https://qiita.com/thtitech/items/8cc898dda7a10780f495#%E3%83%A2%E3%82%B8%E3%83%A5%E3%83%BC%E3%83%AB%E3%81%AE%E4%BD%9C%E6%88%90

が参考になったので共有。本とかは読まなくてもいけた。

 プログラムカウンタとデコーダ

プログラムカウンタ本体はシングルポートレジスタで記述。
そのアドレスをカウンタで指定するようにする。

PC
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の初期化を参照。

ちなみにカウンタ回路

グローバルリセットじゃないと上手く動かなかった。

counter
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で起きる。

Register
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に拡張している。
もっと綺麗に書ける気はする

ALU
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

テストベンチ

結果:
結果.JPG
綺麗に出るね!!
そしてVivadoめちゃ使いやすい。デバッグもやりやすいっす。
FPGAに書いてみたいけど今は手元にない。。

result3.JPG
result2.JPG

目標であるレジスタ9に55が書き込まれ、正常動作を確認。
やったね。

testbench
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

単サイクルプロセッサ完成バージョンについて記事執筆
パイプラインプロセッサの実装

arutema47
Ph.D hardware researcher ASIC design、Pytorch、物体検出、専門モデルとか。 https://aru47.hatenablog.com/ ほぼQiitaに書くことはないのでブログ見て下さい~;)
https://aru47.hatenablog.com/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away