RISC-Vを目指す(4)
新年明けましておめでとうございます。
さて、去年に引き続きRISC-Vの命令セットを実装してみる試練。前回でっちあげたRAMは書き込みしかできなかったので、lb命令を実装してRAMからの読み込みを作ってみよう。書き込みができたのだから読み込みも簡単かと思いそうだが、実際のところちょっとだけ読み込みのほうがめんどい。
外部RAMの都合
RAMは組み合わせ回路に含んでいないのでクロックの立ち上がりを認識してアドレスとデータをRAMに取り込むようにしていた。書き込みの場合はクロックの立ち上がりの時点でアドレスとデータを出力していればそれで処理は完了していたのだが、読み込みの場合、同時点ではRAMがアドレスを受け取るだけである。つまりRAMからデータが出てくるのは最速で次のクロックとなるので、RAMからのデータをレジスタに書き込むためにlb命令は最速で2クロックかかる。
対策案
最も簡単な対策はlb命令を2クロック命令にすることである。1bitレジスタをステータスとして追加して、通常はL、lb命令実行(1クロック目)時にHにして、次のクロックでLに戻す。このステータスがHの時はCPUの動作を一時停止する。具体的にはレジスタファイルとpcへの書き込みを遮断する。
1命令1クロックじゃないとヤダヤダ、という向きには2段パイプライン化という手段がある。今回の場合ならレジスタ書き込み回路だけを2段目に分離すれば見た目1クロック1命令実行ができるようになる。この場合、2段目の書き込みレジスタと1段目の読み込みレジスタが衝突することが懸念されるので、フォワーディングというテクニックで2段目のデータを1段目に引っ張ってくることでパイプラインストールを防ぐことができる。
無論、現在はハーバードアーキテクチャだからこれで解決するだけで、RAMからプログラムを読み込むノイマンアーキテクチャ化した場合は命令フェッチとデータの読み書きが衝突してしまうので、どうあがいても1クロック1命令にはならない。これをどうにかするにはキャッシュメモリを導入してRAMアクセスを減らすことで1クロック1命令に近づくが、近づくだけで、最終的にはスーパースカラとかで複数命令同時実行などする必要がある。まあ、実のところ1クロック1命令にこだわる理由が無い。
2クロック命令
とりあえずlb命令を追加しよう。
# デコード用ビットパターン opcode_lb = _to_pin("0000011", vcc, gnd) funct3_lb = _to_pin("000" , vcc, gnd) # lb命令でH insn_lb = _equal(insn_bit_opcode, opcode_lb) # レジスタ書き込み条件 write_reg = insn_addi | insn_add | insn_jalr | insn_jal | insn_lb
lb命令はIタイプのフォーマットなので即値デコーダは既にある。次に回路の停止用にレジスタを用意する。
# 回路停止用1bitレジスタ(Hの時に停止) stall = Register.new(1) stall.clk = clk.o stall.clrb = clrb.o stall.w = vcc st = ~stall.o[0] & insn_lb # 実行状態かつlb命令のときにHになる(次のクロックですぐLに戻る) stall.d = [st]
あとはpcとレジスタファイルの書き込み条件を変更する。
# pcの書き込み条件 pc.w = ~st regf.w = write_reg & ~st # レジスタ書き込み条件
これで停止中にフリップフロップが書き変わらなくなったのでlb命令は2クロック命令となった。CPUは停止するがRAMへのアドレスや制御信号は出しているので2クロック目にはRAMからのデータが届く。これを2クロック目のレジスタ書き込み動作で保存するわけだ。
RAM
RAMには出力用のピンなども用意してはあったのだが、Read用信号線とRead動作を追加する。
# 外付けRAM class RAM attr_reader :memory def initialize(a, d) @memory = Array.new(80*30){0} @o = Array.new(d){OutputPin.new} @o.each{|t|t.set(false)} end def clk=(v) v.output_objects << self @clk = v end def a=(v) @addr = v end def d=(v) @data = v end def w=(v) @w = v # HがWrite有効 end def r=(v) @r = v # HがRead有効 end def o @o end def update # クロックがHになったら処理する if @clk.get address = ("0b" + @addr.map{|o|o.get ? "1" : "0"}.join).to_i(2) if @w.get # RAMへの書き込み data = ("0b" + @data.map{|o|o.get ? "1" : "0"}.join).to_i(2) @memory[address] = data end if @r.get # RAMからの読み込み memory = @memory[address].to_s(2).rjust(@o.size, "0") @o.each.with_index do |t, i| t.set(memory[i] == "1") end end end end end
RAMの読み込み信号を繋ぐ。
ram.r = st
レジスタファイルの書き込みにlb命令時の条件を追加してRAMからのデータを書き込めるようにする。
# レジスタファイルの入力ポート接続 regf.d = _if(insn_jalr | insn_jal, inc.o + [gnd, gnd], # jalr or jalの場合はrdに戻りアドレスを格納する _if(insn_lb, [gnd]*24 + ram.o, # lb命令の場合はramの出力 adder.o # それ以外の命令は加算器の結果を格納する ))
全加算器の入力は都合よく今のままでいけた。こういうところはうまいことできてるよね。RISC-V。
実行するプログラム
# 32アドレスぶんの命令 rom.d = RISCVASM.asm do addi r2, r0, 72 sb r2, 0, r0 addi r2, r0, 101 sb r2, 1, r0 addi r2, r0, 108 sb r2, 2, r0 addi r2, r0, 108 sb r2, 3, r0 addi r2, r0, 111 sb r2, 4, r0 addi r2, r0, 32 sb r2, 5, r0 addi r2, r0, 119 sb r2, 6, r0 addi r2, r0, 111 sb r2, 7, r0 addi r2, r0, 114 sb r2, 8, r0 addi r2, r0, 108 sb r2, 9, r0 addi r2, r0, 100 sb r2, 10, r0 addi r2, r0, 33 sb r2, 11, r0 lb r2, 0, r0 sb r2, 80, r0 lb r2, 1, r0 sb r2, 81, r0 lb r2, 2, r0 sb r2, 82, r0 lb r2, 3, r0 sb r2, 83, r0 end.map{|t|_to_pin(t, vcc, gnd)}.reverse
Hello world!の後ろにちょこっと追加して、文字を読み込んで真下に表示するようにしてみた。32命令しか書けないので少しだけ。