前回、といっても一年以上前だがFPGAの評価ボードDE0-CVを買ってちょっとしたプログラムを動かしていた。 当時は、LEDがチカチカするような回路を直接書いてそれを動かすというような所でとまっていた。
それから 本業の事業の方に集中していたのでFPGAはたまに触ってみる程度だったが、 GWでまとまった休みをとれる事になり、FPGAの事を思いだして特定の目的の回路ではなく汎用的に動く自作CPUを何かつくってみようという事にした。
- まずは4bit CPUをつくってみて、CPUの構造に慣れる
- System Verilogへと翻訳しつつテストベンチの書き方を勉強
- ROMの内容をMifファイルとMemファイルで切りかえるようにする
- パタヘネとディジタル回路設計とコンピュータアーキテクチャを読んでMIPS32のサブセットを実装
- Cのフィボナッチプログラムの実装を目標に、必要な命令だけを追加
- 次の目標何にするか...
まずは4bit CPUをつくってみて、CPUの構造に慣れる
CPU自作系は大学時代の授業でもあったので、その中で小さな2サイクルCPUをつくった事はあったが、 とはいえその時は実際のFPGAでやったわけではなくiverilogとgtkwave等をつかってしかやっていなかったので まずはCPUの基礎について思いだせたらという事で、自作CPU界隈では有名な神本『CPUの創り方』を買って、その中で紹介されているTD4を作ってみました。
CPUの創り方本ではベースとなる回路の説明からしっかりしてくれていて、その回路の説明をしてくれたあと その回路が乗っているチップの紹介をしてくれるという構成になっているので、なじみやすかったです。 いつかはFPGAだけではなく実際にチップでTD4も作ってみたいですね。
System Verilogへと翻訳しつつテストベンチの書き方を勉強
最初は学生時代に書いていた時の記憶を頼りにVerilog HDLで書いていたのですが、 System Verilogの方が今後は主流になっていったりlogicやらalways_comb, always_ff等の便利な機能もあるという事で、 このタイミングでSystemVerilogに書きなおしてみました。 regとwireを意識していたのがlogicでよくなったり、色々と意図した通りの回路が生成される安心感がありますね。
TD4のサイズであれば回路を書くだけでもまぁちゃんと動いているかなと規模ですが、 せっかく普段のコーディングでもTDDを意識して書いているので、 今後の事を考えるとVerilogのテストベンチを書く練習もしようという事で回路シミュレーションもしたくなりました。
しかし、System Verilogで使っている記法 input var logic [31:0] a
とかがiverilogで読めないようで(おそらくvar?)
学生時代つかっていたのiverilogだと対応できないとわかり、Quartusに付属しているModelSimの使い方をドキュメントを読んだりして勉強しました。
(といっても、ModelSimの中で編集してRecompile, Waveをついかしてシミュレーションといった基礎的な機能しか使えていませんが...)
ROMの内容をMifファイルとMemファイルで切りかえるようにする
TD4の時は適当なアセンブラをRubyスクリプトで書いていた
TD4の時はRubyで雑なスクリプトを書いてTD4のアセンブリ言語から機械語に翻訳された、System Verilogのコードを吐きだすというような事をやっていました: (ほんとの書き捨てなので非常に雑です)
def is_reg_a(str) str == "A" end def is_reg_b(str) str == "B" end def is_im(str) return false if str == nil (str =~ /\A[0-9]+\z/) != nil end def to_binary(im) im = im.to_i sprintf("%04b", im) end class UnknownAsm < StandardError def initialize(op, arg1, arg2) super("Unknown Asm: '#{op} #{arg1},#{arg2}") end end def asm_op(op, arg1, arg2) case op when "MOV" if is_reg_a(arg1) && is_im(arg2) "0011#{to_binary(arg2)}" elsif is_reg_b(arg1) && is_im(arg2) "0111#{to_binary(arg2)}" elsif is_reg_b(arg1) && is_reg_a(arg2) "01000000" elsif is_reg_a(arg1) && is_reg_b(arg2) "00010000" else raise UnknownAsm.new(op, arg1, arg2) end when "ADD" if is_reg_a(arg1) && is_im(arg2) "0000#{to_binary(arg2)}" elsif is_reg_b(arg1) && is_im(arg2) "0101#{to_binary(arg2)}" else raise UnknownAsm.new(op, arg1, arg2) end when "JMP" if is_im(arg1) "1111#{to_binary(arg1)}" else raise UnknownAsm.new(op, arg1, arg2) end when "JNC" if is_im(arg1) "1111#{to_binary(arg1)}" else raise UnknownAsm.new(op, arg1, arg2) end when "OUT" if is_reg_b(arg1) "1001#{to_binary(0)}" elsif is_im(arg1) "1011#{to_binary(arg1)}" else raise UnknownAsm.new(op, arg1, arg2) end else raise UnknownAsm.new(op, arg1, arg2) end end MAX_LINUM = 16 if __FILE__ == $0 linum = 0 while line = gets if linum >= MAX_LINUM raise "Assembly must be less than #{MAX_LINUM} lines." end line = line.chomp.strip result = /\A([a-zA-Z]+)\s+([^,]+)(,\s*(\w*))?\z/.match(line) unless result raise "Unexpected line: '#{line}'" end op = result.captures[0].upcase arg1 = result.captures[1] arg2 = result.captures[3] asm = asm_op(op, arg1, arg2) puts "4'b#{to_binary(linum)}: out=8'b#{asm}; // #{op} #{arg1},#{arg2}" linum += 1 end end
こんな感じでROMの回路コードにコピペできる出力がでます
❯ ruby main.rb < test.asm 4'b0000: out=8'b10110001; // OUT 1, 4'b0001: out=8'b10110010; // OUT 2, 4'b0010: out=8'b10110011; // OUT 3, 4'b0011: out=8'b10110100; // OUT 4, 4'b0100: out=8'b10110101; // OUT 5, 4'b0101: out=8'b10110110; // OUT 6, 4'b0110: out=8'b10110111; // OUT 7, 4'b0111: out=8'b10111000; // OUT 8, 4'b1000: out=8'b10111001; // OUT 9, 4'b1001: out=8'b10111010; // OUT 10, 4'b1010: out=8'b10111011; // OUT 11, 4'b1011: out=8'b10111100; // OUT 12, 4'b1100: out=8'b10111101; // OUT 13, 4'b1101: out=8'b10111110; // OUT 14, 4'b1110: out=8'b10111111; // OUT 15, 4'b1111: out=8'b11110000; // JMP 0,
MIFファイルとMemファイルを生成する形式に変更
TD4では16行程度のアセンブリしかあつかわないので問題なかったのですが、今後の事を考えるとアドレス空間も大きくなるので 「メモリ」というハードウェアと、そのデータという形で分離したほうがあつかいやすくなると考え、ROMの中身は別のファイルで指定できるような構成に変更する事にしました。
Quartusの方ではMifファイルという構造のファイルを利用する事で回路合成時にlogic [31:0] mem[256:0]
のようなメモリの中身を指定できるようになります。
しかし、このコメントに指定する記法はModelSimの方ではとりあつかってくれません。 ModelSimの方で使えないとプログラムを書きかえる度に回路合成する事になり、また実機でしかチェックできなくなってしまいます。 ModelSimの方はどうやるかというと、$readmemb, $readmemhといったメモリの中身だけを羅列したファイルを読みとれる命令があるので そっちをVerilog側のコードに書いておく形にします。
なのでコードの方はこんな感じになります
logic [31:0] mem[0:1023] /* synthesis ram_init_file = "rom.mif" */; initial begin $display("Loading rom."); $readmemb("rom.mem", mem); end
このmemファイルと、mifファイルをRubyのスクリプトで生成する形に変更しました。
パタヘネとディジタル回路設計とコンピュータアーキテクチャを読んでMIPS32のサブセットを実装
ベースのツールの準備や学習は整ったと見てここから目標をMIPS32のサブセットの実装に切りかえて、そちらの学習と実装に入りました。 この時読んでいたのは、こちらも両方とも定番の以下の2冊です
『ディジタル回路設計とコンピュータアーキテクチャ』の方は学生時代にまさに授業で2サイクルCPUの実装を教えてくださった天野先生が翻訳されているのですが、 非常に読みやすく、パタヘネを並行して読む事で理論と実装の両面から理解しやすいのでハードの実装を視野にいれている方は両方を並行して読む事をおすすめします。
まずは本に沿ってシングルサイクルで add, addi, beq, sub, and, or, slt, jだけをサポートしたMIPS32のサブセットを実装しました。
Cのフィボナッチプログラムの実装を目標に、必要な命令だけを追加
CPU自作でMIPS32のサブセットをつくっていらっしゃる方々の初期の目標としてフィボナッチ数の計算をするというのが定番としてありそうだったので
qiita.com kazunori279.hatenablog.com
僕もそれにのっとってフィボナッチ数の計算をするCのプログラムの稼動を目標としました。
稼動を目標とするプログラムをディスアセンブルしてみると、追加でsll, bne, jr, jal, addi, addiuのサポートが必要だとわかり一つずつ実装していきました。 この時はディジタル回路設計の本の回路図をみながら、一つ一つ「ここにマルチプレクサを置いて..コントローラから信号を追加して」とやっていけば比較的素直に実装できました。
この時期はひたすらModelSimとにらめっこしていました。
そしてようやく稼動!
フィボナッチ数の5番目は5です!
こうして、とりあえずTD4の実装からCでFibをうごかすまでGWの後半の4日で無事完了させられました! フィボナッチ数の稼動は、加減算と、ジャンプレジスタ等のベースとなる機能の実装をすればよいので比較的達成しやすい目標かとおもうのでおすすめです。
この4日は8時におきて朝1時くらいまでずっと本よんだりModelSim上でひたすらシミュレーションしていたので、夢の中でもうなされてました
次の目標何にするか...
GWがおわってしまうので、こんなにガッツリ集中して書いている事は今後はまたできなくなるとおもいますが とはいえ次の目標は考えたいですね。
- 乗算・除算機とHI, LOレジスタをつけて素数判定等
- もっと命令や構造つけてxv6等をうごかせるまでがんばる
- vga出力をして画面に出してみる
- 独自の命令セットでかわったハードの作成
どういうのがよいんだろう...