Verilogでミニプロセッサを実装してみた

この記事はCAMPHORアドベントカレンダー22日目の記事です.

 

こんには,びいのです.普段は研究室でVerilogを書いていて,その勉強がてら小さいプロセッサ(RISC-VのI命令,R命令)を実装してみました.FPGA上で動くことを想定しています.

 

【想定する読者】

  • 計算機についてある程度の知識がある(パタヘネを読んだことがあるぐらい)

  • Verilog についての知識(ソースコードが読めるぐらい)

 

【やったこと】

  • RISC-VのI命令,R命令を実装してみた.

  • 実装をテストするために,ソースコードコンパイルしてバイナリを生成し,シミュレーションをして動くことを確認した.  

Q:RISC-Vってなに?

A:オープンソースの命令セットアーキテクチャです.

https://riscv.org/specifications/

で仕様が定義されていて,誰でもこの仕様に基づいて無料で開発を行えます.

IntelとかARMのプロセッサはライセンス料が高いので,オープンソースで開発が行えるというのは革命的.

すばらしいですね.

 

ミニCPUの全体像

教科書に書いてそうなCPUのアーキテクチャをそのまま参考にしてつくりました.

今回はI命令とR命令のみ実装するので,データメモリは無いです.ジャンプ命令も簡単そうだったので実装しました.

 

おおざっぱな理解をしてもらうために図を置いときます(参考文献3より引用).データメモリは今回無いことに注意. 

  f:id:kaz7890:20191221214727p:plain

 

じゃあ,Verilogでどうやって書いたのか紹介していきます

なお,今回実装したもののソースコードはオープンに使って良いものとしてgithubにあげておきます. 

github.com

プログラムカウンタ

プログラムカウンタを32ビットレジスタとして用意しておきます.毎サイクルごとに4バイトずつ増えていき,命令メモリから次の命令を取り出してくれます.

 

   reg [31:0] pc; 
    
    // pc is 0 at start
    initial begin
        pc <= 32'd0;
    end 
    
    /// always
    always @(posedge clk) begin
        if(pc==`PC_LIMIT)
            pc <= 32'd0;
        else if(optype==`OP_JAL)
            pc <= pc + jal_offset <<2;
      else
            pc <= pc + 4;
    end 

 

命令メモリ

実装する 命令が入っているメモリですね. 実装したものはFPGAで動かすことを想定しているので,FPGAのブロックRAM(BRAM)を使うつもりです. なので自分で実装することは何もないですね.楽ちんですね.

シミュレーションするときにはこのメモリの中に命令をいれなければいけないのですが,これは後述...

 

ALU

加算,減算,AND演算など,プロセッサの機能のメインとなる場所です. I命令,R命令それぞれでの動作を記述していたら長くなっちゃったので割愛.詳しくはgithubみてね.

 

レジスタファイル

演算結果を格納するレジスタ群です. レジスタの値を読み出す機能と,データの値をレジスタに書き込む機能が必要です.

module Register(readRegister1,readRegister2,writeRegister,writeData,readData1,readData2,clk,writeEnable);
// input and output
input [4:0] readRegister1,readRegister2,writeRegister;
input [31:0] writeData;
input writeEnable,clk;
output [31:0] readData1,readData2;
// 32 set of 32-bit registers
reg [31:0] registers [31:0];
// initialize memory to zero
integer i;
initial begin
    for(i=0;i<32;i=i+1)
        registers[i] = 0;
end 

assign readData1 = registers[readRegister1];
assign readData2 = registers[readRegister2];

// write data to register on clk
always @(posedge clk) begin
    if(writeEnable==1'b1 && writeRegister != 0)
        registers[writeRegister] = writeData;
end

endmodule

 

ソースコードコンパイル

ちゃんとプロセッサが動いているか確認するために命令メモリに0,1で書かれた命令を入れなきゃいけないんですが,いちいち0,1入れるのはとてもめんどくさい.

そこでテスト用のアセンブリを自分で書いてみて,アセンブリコンパイルして出てきたバイナリを命令メモリにぶち込んでチェックしたいと思った.

そこで神のタイミングでトランジスタ技術12月号がFPGARISC-Vを実装する内容だったので,めちゃくちゃ参考になりました.(トラ技が特集してたから今回実装したわけじゃないよ!プロセッサの中身もトラ技の実装見ないで書いてるよ!)

詳しくはトラ技を読んでね....というのが手っ取り早い回答なんですが,それもつまらないので,少しだけ解説します.

1,RISC-Vのクロスコンパイラ,ツール群を準備

  [https://github.com/riscv/riscv-gnu-toolchain:embed:cite]

これがアセンブリRISC-Vで動くバイナリにするためのツール群になります.

git clone --recursive で取ってきて,ビルドします.

2,自分で書いたアセンブリをビルドする環境を整える

poyo-vのソフトウェアツールを使わせてもらいました.

[https://www.tartetrat.site/poyo-v/:embed:cite]

poyo-v/software/ 以下でc言語risc-v用にクロスコンパイルするMakefileがあるので,それをいじってアセンブリコンパイルするようにしました.

最適化で追加した命令が消されないように,gccオプションを -O0にするなど,少し変更を加えて自分のアセンブリでも無事にコンパイルされるようになりました.

リンカスクリプトを見てみると,命令メモリの中で最初の命令は32Kiバイトから始まっているようである.プログラムカウンタの初期値もこの値に変更した.

.text
.globl main
main:
    addi x1,x1,1
    addi x2,x2,2

みたいなコードを用意して,

Makefileを実行して出てきた

code.hex,data.hexをそれぞれ命令メモリ,データメモリに入れれば実行してくれるはず.

さあ,シミュレーションだ!

シミュレーションの結果,I命令,R命令が機能していることを確認した.実験は成功だ!

 

 

今後やりたいこと

今回はプロセッサの本当に基礎的な部分だけの実装だったんで,他の部分にも手をつけていく.

メモリロード/ストア命令,乗算器,除算器の追加

パイプライン化,アウトオブオーダー化

OSサポート命令の追加

感想

正直クロスコンパイラ関係で知らないことが多かったので,そこで時間を取られてた.

しかし,objdump,リトルエンディアン,リンカスクリプトについて自分の手を動かしながら理解できたのは良かった.  

みんなも自作プロセッサ,やってみよう!

参考文献

1,RISC-V specification

https://riscv.org/specifications/

 

2,トランジスタ技術 2019年12月号

https://toragi.cqpub.co.jp/tabid/887/Default.aspx

 

3, プロセッサの中身の図はこの論文から引用

Joseph, Michael & Ravi, Selvaraj. (2016). FPGA Implementation for Low Power Self Testable MIPS processor.

4,poyo-v github

https://github.com/ourfool/poyo-v

5,riscv-gnu-toolchain

https://github.com/riscv/riscv-gnu-toolchain