RISC-Vを目指す(5)

ぼっちの引きこもりはお正月ヒマなので。っていうか年末に大阪行く予定だったのが食あたりでダウンしていたのでそのままって感じ。
ともあれ、今回は今までに作ったロード/ストア系命令を揃えることにしてみる。ちなみに前回のlb命令は符号拡張するべきだったがゼロ拡張してしまっていたのでバグということになる。ゼロ拡張する命令はlbu。

RV32I命令セットのロード/ストア命令

ロードがlb/lh/lw/lbu/lhuの5つ、ストアがsb/sh/swの3つとなる。opcodeはロードとストアで1つずつ、内訳はfunct3で区別する。ロードのうちuが付くやつがゼロ拡張(符号無し)で、bは1byte、hが2byte、wが4byteの読み書きである。

RAM

1/2/4byteの読み書きができるRAMをどうするかという問題だったが、とりあえず手っ取り早くサイズ指定用ピンを用意することにした。世の中のRAMがどうなっているのかは知らないが、どうせリアルなRAMチップのインターフェイスなんて死ぬほど難しいので参考にしてもしょうがない。

# 外付けRAM
class RAM
  attr_reader :memory

  def initialize(a)
    @memory = Array.new(80*30){0}
    @o = Array.new(32){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 size1=(v)
    @size1 = v
  end

  def size2=(v)
    @size2 = v
  end

  def size4=(v)
    @size4 = v
  end

  def update
    # クロックがHになったら処理する
    if @clk.get
      address = ("0b" + @addr.map{|o|o.get ? "1" : "0"}.join).to_i(2)

      if @w.get # RAMへの書き込み
        if @size4.get
          @memory[address  ] = ("0b" + @data[24..31].map{|o|o.get ? "1" : "0"}.join).to_i(2)
          @memory[address+1] = ("0b" + @data[16..23].map{|o|o.get ? "1" : "0"}.join).to_i(2)
          @memory[address+2] = ("0b" + @data[ 8..15].map{|o|o.get ? "1" : "0"}.join).to_i(2)
          @memory[address+3] = ("0b" + @data[ 0.. 7].map{|o|o.get ? "1" : "0"}.join).to_i(2)
        elsif @size2.get
          @memory[address  ] = ("0b" + @data[24..31].map{|o|o.get ? "1" : "0"}.join).to_i(2)
          @memory[address+1] = ("0b" + @data[16..23].map{|o|o.get ? "1" : "0"}.join).to_i(2)
        elsif @size1.get
          @memory[address  ] = ("0b" + @data[24..31].map{|o|o.get ? "1" : "0"}.join).to_i(2)
        end
      end

      if @r.get # RAMからの読み込み
        memory = @memory[address+3].to_s(2).rjust(8, "0") +
                 @memory[address+2].to_s(2).rjust(8, "0") +
                 @memory[address+1].to_s(2).rjust(8, "0") +
                 @memory[address  ].to_s(2).rjust(8, "0")
        @o.each.with_index do |t, i|
          t.set(memory[i] == "1")
        end
      end
    end
  end
end

size1/2/4というピンを作った。これの指定を見ながら書き込み時はメモリに何byte書き込むかの動作を分ける。RISC-Vはリトルエンディアンなのでレジスタの下位byteがアドレスの小さいほうに書き込まれる。回路的には右のほうが数字が小さいのでリトルエンディアンのほうが自然だが、ソフトウェア的には数字が小さいのは左に来るので逆転するという訳だ。それのせいでbitの番号と配列の添え字が一致しなくて苦労しているのだがな。
読み込みのほうはサイズ指定ピンを無視してアドレスから4byteを返すようにしてある。これは上位を0で埋めても符号拡張する命令の場合は意味が無いし、符号拡張するかどうかはRAM側の仕事ではないからで、どのみちCPU側で命令ごとに処理をわけることになるのでどうでもいいという話。こういう感じに作った場合、何byteアクセスでもリトルエンディアンだと下位1byteは常に同じ位置に来る(ビッグエンディアンだと1byteアクセス時のデータは最上位byteに来てしまう)ので、全ロード命令で回路が共通化できて節約になる。俺は面倒なのでそういう努力はしないが。

命令デコーダ

opcodeがロード/ストアでそれぞれ1つずつとなるので、大きくロード命令/ストア命令を表すピンも作っておく。

# デコード用ビットパターン
opcode_load  = _to_pin("0000011", vcc, gnd)
opcode_store = _to_pin("0100011", vcc, gnd)
funct3_sb    = _to_pin("000"    , vcc, gnd)
funct3_sh    = _to_pin("001"    , vcc, gnd)
funct3_sw    = _to_pin("010"    , vcc, gnd)
funct3_lb    = _to_pin("000"    , vcc, gnd)
funct3_lh    = _to_pin("001"    , vcc, gnd)
funct3_lw    = _to_pin("010"    , vcc, gnd)
funct3_lbu   = _to_pin("100"    , vcc, gnd)
funct3_lhu   = _to_pin("101"    , vcc, gnd)

# 各ロード命令で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)

# レジスタ書き込み条件
write_reg = insn_addi | insn_add | insn_jalr | insn_jal | insn_load

RAM接続

size1/2/4はストア系命令の種類によってどれかをHにする。RAMから出てくるデータはロード系命令の種類によって取得サイズと拡張方式を選択する。ついでにアドレス線を16本に増やしておいた。メモリ64kB搭載である。

# でっちあげRAM
ram = RAM.new(16)
ram.a = adder.o[16..31]
ram.d = rs2
ram.clk = clk.o
ram.w = insn_store
ram.r = st
ram.size1 = insn_sb
ram.size2 = insn_sh
ram.size4 = insn_sw
ram_read_data = _if(insn_lb,              [ram.o[24]]*24 + ram.o[24..31],
                _if(insn_lh,              [ram.o[16]]*16 + ram.o[16..31],
                _if(insn_lbu,             [gnd]*24       + ram.o[24..31],
                _if(insn_lhu,             [gnd]*16       + ram.o[16..31],
                                          ram.o
                ))))

レジスタの書き込みにはこのram_read_dataを使う。

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

プログラム

# 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
  lh r2, 0,  r0
  sh r2, 80, r0
  lw r2, 2,  r0
  sw r2, 82, r0
  lb r2, 6,  r0
  sb r2, 86, r0
  lbu r2, 7,  r0
  sb r2, 87, r0
end.map{|t|_to_pin(t, vcc, gnd)}.reverse

最後のところを変えて新しく実装した命令を使ってみた。プログラムサイズは同じだが2byte/4byteアクセスができるのでコピーできる文字数が増えて、地獄から脱出できる。

おしまい

このような結果になる。

えらくまた中途半端ではあるが、まあ、結果の美しさに意味は無いのでこれはこれでよい。ところでコードをgistにぺたぺたするのが面倒になってきた。このシリーズの記事を楽しみにしてる人とかがいるとも思えないのでコードを貼るのは時々にしようかなと面倒くさがりの俺は思ってみた次第。
さて次は何をやろうかな。いい加減、足し算だけてのもあれだし演算命令作っていくか、条件分岐を追加するか。それとも命令をRAMから読むようにするか。まあ、そのうち。どうせどれもやることになるし。