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アクセスができるのでコピーできる文字数が増えて、地獄から脱出できる。