RISC-Vを目指す(6)

やってみたいことが無いわけではないのだけども、なかなか具体的なやり方がイメージできないので、とりあえずRV32Iの命令を一気に実装してしまおう。と言ってもCSR系の制御命令は今のところサッパリわからないので保留。なので残りの命令はlui、auipc、条件分岐、演算である。
lui/auipcは即値20bitを指定レジスタの上位20bitに設定(下位12bitは0固定)するもので、oriと組み合わせて32bit値を作ることができる。luiは指定値そのまま、auipcはPCの値を加算する。PCの値をレジスタに持ってくる命令はauipcだけだと思うが、jalrとセットにすれば遠いアドレスに相対ジャンプすることができる。これら2つの命令は即値には12bitシフトした値を指定することになるので非常に使いづらいが、実際にはアセンブラの擬似命令li/callとして他の命令との組み合わせで使われるので大きな問題は無い。
条件分岐命令はbeq/bne/blt/bge/bltu/bgeuの6種類。それぞれ、=、!=、<(符号付)、>=(符号付)、<(符号無)、>=(符号無)となる。>と<=が無いがオペランドを入れ替えればいいだけなので問題は無い。
演算命令はadd/addiは既にあるので、sub/and/or/xor/sll/srl/sra/slt/sltuとsub以外のi付きが残っている。算術演算、論理演算、シフトで、最後の2つは比較命令である。<だったら1がレジスタに書き込まれる。
では作る。

デコーダ

まずは命令のデコードから。つってもここはもう愚直にやる感じで。効率良い回路とかも作れるんだろうけどとりあえずは動けば。

## 命令デコーダ(RISC-V/RV32I)
insn = bus.o

insn_bit_funct7   = insn[0..6]
insn_bit_imm11_5  = insn[0..6]
insn_bit_imm4_0   = insn[20..24]
insn_bit_imm11_0  = insn[0..11]
insn_bit_imm20_1  = [insn[0]] + insn[12..19] + [insn[11]] + insn[1..10]
insn_bit_imm12_1  = [insn[0]] + [insn[24]] + insn[1..6] + insn[20..23]
insn_bit_imm31_12 = insn[0..19]
insn_bit_rs2      = insn[7..11]
insn_bit_rs1      = insn[12..16]
insn_bit_funct3   = insn[17..19]
insn_bit_rd       = insn[20..24]
insn_bit_opcode   = insn[25..31]

# 即値
imm_I_Type = [insn_bit_imm11_0[0]]*20 + insn_bit_imm11_0
imm_S_Type = [insn_bit_imm11_5[0]]*20 + insn_bit_imm11_5 + insn_bit_imm4_0
imm_B_Type = [insn_bit_imm12_1[0]]*19 + insn_bit_imm12_1 + [gnd]
imm_U_Type = insn_bit_imm31_12 + [gnd]*12
imm_J_Type = [insn_bit_imm20_1[0]]*11 + insn_bit_imm20_1 + [gnd]

即値パターンのB(条件分岐用)とU(lui/auipc用)が増えたのでそれに対応して増やした。
次にビットパターンも全種類用意。

# デコード用ビットパターン
opcode_jalr   = _to_pin("1100111")
opcode_jal    = _to_pin("1101111")
opcode_load   = _to_pin("0000011")
opcode_store  = _to_pin("0100011")
opcode_opimm  = _to_pin("0010011")
opcode_op     = _to_pin("0110011")
opcode_lui    = _to_pin("0110111")
opcode_auipc  = _to_pin("0010111")
opcode_branch = _to_pin("1100011")

funct3_jalr  = _to_pin("000")
funct3_lb    = _to_pin("000")
funct3_lh    = _to_pin("001")
funct3_lw    = _to_pin("010")
funct3_lbu   = _to_pin("100")
funct3_lhu   = _to_pin("101")
funct3_sb    = _to_pin("000")
funct3_sh    = _to_pin("001")
funct3_sw    = _to_pin("010")

# 演算系funct3(sub以外はi付きと共通)
funct3_add   = _to_pin("000")
funct3_sub   = _to_pin("000")
funct3_sll   = _to_pin("001")
funct3_slt   = _to_pin("010")
funct3_sltu  = _to_pin("011")
funct3_xor   = _to_pin("100")
funct3_srl   = _to_pin("101")
funct3_sra   = _to_pin("101")
funct3_or    = _to_pin("110")
funct3_and   = _to_pin("111")

# 条件分岐系funct3
funct3_beq   = _to_pin("000")
funct3_bne   = _to_pin("001")
funct3_blt   = _to_pin("100")
funct3_bge   = _to_pin("101")
funct3_bltu  = _to_pin("110")
funct3_bgeu  = _to_pin("111")

funct7_add   = _to_pin("0000000")
funct7_srl   = _to_pin("0000000")
funct7_sub   = _to_pin("0100000")
funct7_sra   = _to_pin("0100000")

レジスタ演算と即値演算でそれぞれopcodeが共通なので、opとopimmとしておいた。条件分岐はbranch。funct3と7も同じものが出てきているので、そのへんも共通化すれば回路は減るんじゃないかと思う。
んで各命令判定用ピン。

# OP_IMM系命令でH
insn_opimm = _equal(insn_bit_opcode, opcode_opimm)
insn_addi  = insn_opimm & _equal(insn_bit_funct3, funct3_add)
insn_slli  = insn_opimm & _equal(insn_bit_funct3, funct3_sll)
insn_slti  = insn_opimm & _equal(insn_bit_funct3, funct3_slt)
insn_sltui = insn_opimm & _equal(insn_bit_funct3, funct3_sltu)
insn_xori  = insn_opimm & _equal(insn_bit_funct3, funct3_xor)
insn_srli  = insn_opimm & _equal(insn_bit_funct3, funct3_srl) & _equal(insn_bit_funct7, funct7_srl)
insn_srai  = insn_opimm & _equal(insn_bit_funct3, funct3_sra) & _equal(insn_bit_funct7, funct7_sra)
insn_ori   = insn_opimm & _equal(insn_bit_funct3, funct3_or)
insn_andi  = insn_opimm & _equal(insn_bit_funct3, funct3_and)

# OP系命令でH
insn_op    = _equal(insn_bit_opcode, opcode_op)
insn_add   = insn_op & _equal(insn_bit_funct3, funct3_add) & _equal(insn_bit_funct7, funct7_add)
insn_sub   = insn_op & _equal(insn_bit_funct3, funct3_sub) & _equal(insn_bit_funct7, funct7_sub)
insn_sll   = insn_op & _equal(insn_bit_funct3, funct3_sll)
insn_slt   = insn_op & _equal(insn_bit_funct3, funct3_slt)
insn_sltu  = insn_op & _equal(insn_bit_funct3, funct3_sltu)
insn_xor   = insn_op & _equal(insn_bit_funct3, funct3_xor)
insn_srl   = insn_op & _equal(insn_bit_funct3, funct3_srl) & _equal(insn_bit_funct7, funct7_srl)
insn_sra   = insn_op & _equal(insn_bit_funct3, funct3_sra) & _equal(insn_bit_funct7, funct7_sra)
insn_or    = insn_op & _equal(insn_bit_funct3, funct3_or)
insn_and   = insn_op & _equal(insn_bit_funct3, funct3_and)

# jalr命令でH
insn_jalr  = _equal(insn_bit_opcode, opcode_jalr) & _equal(insn_bit_funct3, funct3_jalr)

# jal命令でH
insn_jal   = _equal(insn_bit_opcode, opcode_jal)

# 各ロード命令でH
insn_load  = _equal(insn_bit_opcode, opcode_load)
insn_lb    = insn_load & _equal(insn_bit_funct3, funct3_lb)
insn_lh    = insn_load & _equal(insn_bit_funct3, funct3_lh)
insn_lw    = insn_load & _equal(insn_bit_funct3, funct3_lw)
insn_lbu   = insn_load & _equal(insn_bit_funct3, funct3_lbu)
insn_lhu   = insn_load & _equal(insn_bit_funct3, funct3_lhu)

# 各ストア命令でH
insn_store = _equal(insn_bit_opcode, opcode_store)
insn_sb    = insn_store & _equal(insn_bit_funct3, funct3_sb)
insn_sh    = insn_store & _equal(insn_bit_funct3, funct3_sh)
insn_sw    = insn_store & _equal(insn_bit_funct3, funct3_sw)

# lui命令でH
insn_lui   = _equal(insn_bit_opcode, opcode_lui)

# auipc命令でH
insn_auipc = _equal(insn_bit_opcode, opcode_auipc)

# 各条件分岐命令でH
insn_branch = _equal(insn_bit_opcode, opcode_branch)
insn_beq    = insn_branch & _equal(insn_bit_funct3, funct3_beq)
insn_bne    = insn_branch & _equal(insn_bit_funct3, funct3_bne)
insn_blt    = insn_branch & _equal(insn_bit_funct3, funct3_blt)
insn_bge    = insn_branch & _equal(insn_bit_funct3, funct3_bge)
insn_bltu   = insn_branch & _equal(insn_bit_funct3, funct3_bltu)
insn_bgeu   = insn_branch & _equal(insn_bit_funct3, funct3_bgeu)

# レジスタ書き込み条件
write_reg = insn_opimm | insn_op | insn_jalr | insn_jal | insn_load | insn_lui | insn_auipc

演算系の命令で一部しかfunct7を見ていないが、ほんとは'000000'である必要があって、そこはまあ、手抜きである。

ALU

今までは加算しかなかったのでAdderに色々突っ込んでいたが、この部分をALU(演算回路)に置き換えて、色んな演算をこの中でやるようにしたい。色んな演算を最上位の階層に配置するとごちゃごちゃになってしまうので、演算部分だけ別クラスに分離するということだ。
やりたい演算はadd/sub/and/or/xor/sll/srl/sra/slt/sltuの10種類、i付きの命令は即値、付かない命令はレジスタを使うが、この区別はALUに入れる値を選ぶところで行うので、ALUとして区別はしない。んで、どうやって10種類の演算を選択するかなのだが、あまり難しいことは考えず、それぞれの演算をしたいときにHにするピンを10本用意することにした。こういうところもうまく作っていけば回路は減るのだろうなと思う。

# ALU
class ALU
  def initialize(dsize)
    h = OutputPin.new(H)
    l = OutputPin.new(L)
    @o = Array.new(dsize){OutputPin.new}
    @buf_add  = Buffer1.new
    @buf_sub  = Buffer1.new
    @buf_sll  = Buffer1.new
    @buf_slt  = Buffer1.new
    @buf_sltu = Buffer1.new
    @buf_xor  = Buffer1.new
    @buf_srl  = Buffer1.new
    @buf_sra  = Buffer1.new
    @buf_or   = Buffer1.new
    @buf_and  = Buffer1.new

    # 入力データ
    @buf_d1   = Buffer.new(dsize)
    @buf_d0   = Buffer.new(dsize)

    # 加算器
    @adder = Adder.new(dsize)
    @adder.d = [@buf_d1.o, @buf_d0.o]
    @adder.x = l

    # 減算器(加算器と共有できると思うが面倒だったので)
    @not = NOTn.new(dsize)
    @subtractor = Adder.new(dsize)
    @not.a = @buf_d1.o
    @subtractor.d = [@not.o, @buf_d0.o]
    @subtractor.x = OutputPin.new(H)

    # and
    @and = ANDn.new(dsize)
    @and.a = @buf_d1.o
    @and.b = @buf_d0.o

    # or
    @or = ORn.new(dsize)
    @or.a = @buf_d1.o
    @or.b = @buf_d0.o

    # xor
    @xor = XORn.new(dsize)
    @xor.a = @buf_d1.o
    @xor.b = @buf_d0.o

    # バレルシフタ
    ssize = Math.log2(dsize).to_i
    @shifter = BarrelShifter.new(ssize, dsize)
    @shifter.d = @buf_d0.o
    @shifter.shift = @buf_d1.o[-ssize..-1]
    @shifter.sign = _if(@buf_sra.o, [@buf_d0.o[0]], [l])[0]
    @shifter.direction = ~@buf_sll.o

    # 出力
    @o = _if(@buf_add.o,  @adder.o,
         _if(@buf_sub.o,  @subtractor.o,
         _if(@buf_sltu.o, [l]*(dsize-1) + [~@subtractor.c],
         _if(@buf_slt.o,  [l]*(dsize-1) + _if(@buf_d0.o[0] & ~@buf_d1.o[0], [h],
                                          _if(~@buf_d0.o[0] & @buf_d1.o[0], [l],
                                                                            [~@subtractor.c]
                                          )),
         _if(@buf_and.o,  @and.o,
         _if(@buf_or.o,   @or.o,
         _if(@buf_xor.o,  @xor.o,
         _if(@buf_sll.o | @buf_srl.o | @buf_sra.o, @shifter.o,
                         [l]*dsize
         ))))))))
  end

  def d=(v)
    @buf_d1.d = v[0]
    @buf_d0.d = v[1]
  end

  def o
    @o
  end

  def add=(v)
    @buf_add.d = v
  end

  def sub=(v)
    @buf_sub.d = v
  end
  
  def sll=(v)
    @buf_sll.d = v
  end
  
  def slt=(v)
    @buf_slt.d = v
  end
  
  def sltu=(v)
    @buf_sltu.d = v
  end
  
  def xor=(v)
    @buf_xor.d = v
  end
  
  def srl=(v)
    @buf_srl.d = v
  end
  
  def sra=(v)
    @buf_sra.d = v
  end
  
  def or=(v)
    @buf_or.d = v
  end
  
  def and=(v)
    @buf_and.d = v
  end
end

ANDn/ORn/XORn/NOTnの各クラスは論理演算を束ねただけのクラスである。無かったので作った。BarrelShifterは以前作ったバレルシフタをそのまま使った。微妙に厄介だったのはslt(符号付比較命令)で、単純に減算しただけでは判定できないので悩んだ。符号が違う場合を切り離して判定してみたが、果たしてこれでOKなのかどうかはよくわからない(頭悪いので)。

条件分岐

条件分岐命令は
beq rs1, rs2, imm
という形式で、オペランドとしてレジスタを2つ渡し、それを比較して、真の場合にpc+immにジャンプするという相対ジャンプ命令である。なので、この命令1つで比較と加算を実行する必要がある。ALUはそのどちらか片方しかできないので、もう片方の回路を追加しなければならない。どっちにしようかな〜って悩んでみたが、相対ジャンプ用の加算は既にALUを使ってjal命令でやっているのでそれを活用することにして、比較回路を追加することにしてみた。

# 条件分岐用の減算器
_not = NOTn.new(32)
subtractor = Adder.new(32)
_not.a = rs2
subtractor.d = [_not.o, rs1]
subtractor.x = vcc

# 条件分岐用の比較器
eq = _equal(rs1, rs2)

lt = _if(rs1[0] & ~rs2[0], [vcc],
     _if(~rs1[0] & rs2[0], [gnd],
                           [~subtractor.c]
     ))[0]

# 分岐する場合にH
branch = _if(insn_beq,  [eq],
         _if(insn_bne,  [~eq],
         _if(insn_blt,  [lt],
         _if(insn_bge,  [~lt],
         _if(insn_bltu, [~subtractor.c],
         _if(insn_bgeu, [subtractor.c],
                        [gnd]
         ))))))[0]

作ってはみたがALUの中にも比較回路はあるのでやっぱりそっちに比較させればよかったかしらん。などと思った。どっちがいいのかはよくわからん。

その他

ここまでで部品は揃ったので、後はそれらを繋ぐ。まず条件分岐命令が増えたのでpcを操作するところに条件を追加。

# pcとインクリメンタ接続
pc.d = _if(insn_jalr | insn_jal | branch, alu.o, inc.o + [gnd, gnd])
inc.d = pc.o[0, 30]

lui命令はALUを使わないレジスタ書き込み命令なので、レジスタファイルの入力にコードを追加。

# レジスタファイルの入力ポート接続
regf.d = _if(insn_jalr | insn_jal,  inc.o + [gnd, gnd],             # jalr or jalの場合はrdに戻りアドレスを格納する
         _if(insn_load,             ram_read_data,                  # ロード命令の場合はramの出力
         _if(insn_lui,              imm_U_Type,                     # lui命令の場合は即値
                                    alu.o                           # それ以外の命令はALUの結果を格納する
         )))

最後にALUを接続。

# 32bitALU接続
alu.d = [_if(insn_jal,    imm_J_Type, # jal命令
         _if(insn_op,     rs2,        # op系命令
         _if(insn_slli | insn_srli | insn_srai, [gnd]*27 + insn_bit_rs2, # shamt使う系命令
         _if(insn_branch, imm_B_Type, # 条件分岐系命令
         _if(insn_store,  imm_S_Type, # ストア命令
         _if(insn_auipc,  imm_U_Type, # auipc命令
                          imm_I_Type  # それ以外
         )))))),
         _if(insn_jal | insn_auipc | insn_branch, pc.o, # jal/auipc/条件分岐命令
                          rs1   # それ以外
         )
        ]
alu.add = insn_jal | insn_jalr | insn_add | insn_addi | insn_load | insn_store | insn_auipc | insn_branch
alu.sub = insn_sub
alu.and = insn_and | insn_andi
alu.or  = insn_or | insn_ori
alu.xor = insn_xor | insn_xori
alu.sll = insn_sll | insn_slli
alu.srl = insn_srl | insn_srli
alu.sra = insn_sra | insn_srai
alu.slt = insn_slt | insn_slti
alu.sltu = insn_sltu | insn_sltui

Adderは入力の順番はどっちでもよかったが、ALUになったら順番が気になったので入れ替えておいた。

おしまい

アセンブラこちら
シミュレータライブラリはこちら
RISC-Vの回路はこちら
そういえばRISC-Vのレジスタはrじゃなくてxだったのでアセンブラに手を入れてある。各種命令と擬似命令も少し追加した。もっと機能拡張しないと使いづらいのだが、まあそれはいずれ。andとorがRubyの文法上使えないので_を付けることになってしまったのが屈辱である。ロード/ストア命令のフォーマットもそうだが、なかなかうまいこといかないもんである。Parserとか作らなくていいのは楽なんだけども。
さて、命令が揃ったので何か面白いものが作れればいいのだが。やってみたいことはあるがその前にやらねばならないこともあり、やり方も考えなければならないので、少し悩むことにする。