Verilogでミニプロセッサを実装してみた
この記事はCAMPHORアドベントカレンダー22日目の記事です.
こんには,びいのです.普段は研究室でVerilogを書いていて,その勉強がてら小さいプロセッサ(RISC-VのI命令,R命令)を実装してみました.FPGA上で動くことを想定しています.
【想定する読者】
【やったこと】
Q:RISC-Vってなに?
https://riscv.org/specifications/
で仕様が定義されていて,誰でもこの仕様に基づいて無料で開発を行えます.
IntelとかARMのプロセッサはライセンス料が高いので,オープンソースで開発が行えるというのは革命的.
すばらしいですね.
ミニCPUの全体像
教科書に書いてそうなCPUのアーキテクチャをそのまま参考にしてつくりました.
今回はI命令とR命令のみ実装するので,データメモリは無いです.ジャンプ命令も簡単そうだったので実装しました.
おおざっぱな理解をしてもらうために図を置いときます(参考文献3より引用).データメモリは今回無いことに注意.
じゃあ,Verilogでどうやって書いたのか紹介していきます
なお,今回実装したもののソースコードはオープンに使って良いものとしてgithubにあげておきます.
プログラムカウンタ
プログラムカウンタを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月号がFPGAでRISC-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