ハードウェアシミュレータその8

CPUの部品的なものができあがってきたので、ようやく命令を追加する。
とりあえず今は命令に埋め込まれた値をレジスタに書き込むMOV命令しか実装できていないので、これに近い「命令に埋め込まれた値をレジスタに加算するADD命令」を作ってみよう。

レジスタファイルの出力ポート

前回レジスタファイルを作ったが、出力が必要なかったので出力ポートが未実装だった。指定の値をレジスタに加算するためには、まずレジスタの値を取得する必要があるので、レジスタファイルに出力ポートを追加する。
指定の値を加算するのなら出力ポートは2つもいらないのだが、ついでなので2ポート作っておく。

# レジスタファイル(出力2ポート、入力1ポート)
class RegisterFile
  def initialize(select_bit_width, data_bit_width)
    # レジスタの配列
    @reg = Array.new(2**select_bit_width){Register.new(data_bit_width)}

    # 書き込みレジスタを選択するためのセレクタ
    # セレクタの出力はレジスタファイルのW信号入力とANDを取って各レジスタのWに入る
    @sel = Selector.new(select_bit_width)
    @w = Buffer1.new # W入力用バッファ
    @reg.zip(@sel.o).each do |reg, sel|
      reg.w = @w.o & sel # &演算子でANDゲートが生成される。戻りはANDゲートの出力ピン
    end

    # 出力ポート
    @mux0 = Mux.new(select_bit_width, data_bit_width)
    @mux0.d = self.o
    @mux1 = Mux.new(select_bit_width, data_bit_width)
    @mux1.d = self.o
  end

  # クロック入力
  # 書き込みタイミングはクロックの立ち上がり
  def clk=(v)
    @reg.each do |reg|
      reg.clk = v
    end
  end

  # select_bit_width本の書き込み選択信号
  # 内部のセレクタに入力される
  def sin=(v)
    @sel.s = v
  end

  # 出力ポート0用選択信号
  def sout0=(v)
    @mux0.s = v
  end

  # 出力ポート1用選択信号
  def sout1=(v)
    @mux1.s = v
  end

  # 入力データ
  # wがHの場合、クロックの立ち上がりでsで選択されたレジスタにデータが書き込まれる
  def d=(v)
    @reg.each do |reg|
      reg.d = v
    end
  end

  # 入力ポート有効がH
  def w=(v)
    @w.d = v
  end

  # クリア信号
  def clrb=(v)
    @reg.each do |reg|
      reg.clrb = v
    end
  end

  # 出力ポート0の出力
  def o0
    @mux0.o
  end

  # 出力ポート1の出力
  def o1
    @mux1.o
  end

  # 出力(全レジスタ)
  def o
    @reg.map{|r|r.o}
  end
end

特に難しいこともなく、レジスタの全出力ポートをマルチプレクサに入力して、その出力をレジスタファイルから外に出しているだけである。選択信号とマルチプレクサを増やせば出力ポートをいくつでも作ることができるが、たくさん作っても使い道は無いだろう。
入力ポートを複数作るのはちょっと難しい。なぜなら2つの入力が同じレジスタに書き込むパターンをどうするかが問題になるからだ。Z80のようにBレジスタとCレジスタを組み合わせて16bitとして扱えて、1命令で両方同時に書き込むことができるような命令セットの場合、レジスタファイルの構成はよく考える必要がありそうだ。とりあえず今は考えない。

加算器

いままで無かったのが不思議なぐらい基本中の基本。だが非常に奥が深い。説明できるほど詳しくないのでWikipediaの記事を見てくださいな。
シミュレータには一応、HarfAdderクラスとFullAdderクラスが既に用意されてはいる。でもこれらが単体で存在していても繋ぐのが面倒であり、やっぱり指定幅のbitぶん自動生成するようなクラスが欲しいので、作った。

# 可変長加算器
class Adder
  def initialize(bit_width)
    @adder = Array.new(bit_width){FullAdder.new}

    # 2bit以上の加算の場合、加算器同士を接続する
    if bit_width > 1
      @adder[0..-2].each.with_index do |a, i|
        a.x = @adder[i+1].c
      end
    end
  end

  # データ入力
  def d=(v)
    @adder.zip(*v).each do |a, d1, d0|
      a.a = d1
      a.b = d0
    end
  end

  # キャリー入力
  def x=(v)
    @adder.last.x = v
  end

  # キャリー出力
  def c
    @adder[0].c
  end

  # 出力
  def o
    @adder.map{|a|a.s}
  end
end

2つの値を加算して結果を返す。キャリー入力とキャリー出力も作ってはおいたが、今回は使わない。いずれ使うかもしれない。
指定の値をレジスタの値に加算することになるので、加算器にはROMのデータの一部とレジスタファイルの出力を接続する。出力はレジスタファイルの入力となる。

命令フォーマット

命令を追加するのでフォーマットを決めよう。現状は、

X X imm3 imm2 imm1 imm0 reg1 reg0 : mov reg, imm

という感じで、上位2bitは無視されている。このうちの1つを使って、0のときにmov、1のときにaddを表すようにしよう。

X 0 imm3 imm2 imm1 imm0 reg1 reg0 : mov reg, imm
X 1 imm3 imm2 imm1 imm0 reg1 reg0 : add reg, imm

これなら簡単そうだ。

方針

レジスタファイルの出力を加算器に入力する。ROMのimm部分も加算器に入力する。この接続は無条件であり、movだろうがaddだろうが加算は実行される。
マルチプレクサを1つ用意して、加算器の出力とROMのimm部分を接続し、命令フォーマットのmov/add選択bitでどちらかを選ぶようにする。movのときはROMのimm部分が、addのときは加算器の出力がマルチプレクサの出力となるので、これをレジスタファイルに入力する。
こんな感じ。

回路部分のコード

# 電源とグランド
_vcc = Line.new
_gnd = Line.new
vcc = _vcc.o
gnd = _gnd.o

# ROM
rom = ROM.new(3, 8) # アドレス線3本、データ線8本
rom.d = [ # 8アドレスぶんのデータ
  [gnd, gnd, gnd, gnd, gnd, vcc, gnd, gnd], # mov a, 0x1
  [gnd, gnd, gnd, gnd, vcc, gnd, gnd, vcc], # mov b, 0x2
  [gnd, gnd, gnd, gnd, vcc, vcc, vcc, gnd], # mov c, 0x3
  [gnd, gnd, gnd, vcc, gnd, gnd, vcc, vcc], # mov d, 0x4
  [gnd, vcc, gnd, vcc, gnd, vcc, gnd, gnd], # add a, 0x5
  [gnd, vcc, gnd, vcc, gnd, vcc, gnd, vcc], # add b, 0x5
  [gnd, vcc, vcc, gnd, vcc, gnd, vcc, gnd], # add c, 0xa
  [gnd, vcc, vcc, gnd, vcc, gnd, vcc, vcc]  # add d, 0xa
].reverse

# 8bitデータバス
bus = Bus.new(8)

# クロックとクリアの入力線
clk = Line.new
clrb = Line.new

# 4bitカウンタ
c4 = Counter.new(4)
c4.clk = clk.o
c4.clrb = clrb.o

# ROMにカウンタの値を接続
rom.a = [c4.o[1], c4.o[2], c4.o[3]]
rom.csb = c4.o[0]

# ROMの出力はThreeStateBufferなのでBusオブジェクトにaddする
bus.add rom.o
# ROMの出力の値はBusオブジェクトから取り出す

# 循環接続するので先に生成しておく
regf = RegisterFile.new(2, 4) # レジスタファイル
adder = Adder.new(4) # 加算器
muxd = Mux1_n.new(4) # 書き込みデータ選択用マルチプレクサ

# 4bitレジスタ4本のレジスタファイル
regf.clk = clk.o
regf.clrb = clrb.o

# 出力ポートの接続
regf.sout0 = bus.o[6..7]

# 入力ポートの接続
regf.d = muxd.o
regf.sin = bus.o[6..7] # 命令的に出力と同じレジスタが選択される
regf.w = vcc # 書き込まない命令が存在しないので常に書き込む指定

# 4bit全加算器
adder.d = [regf.o0, bus.o[2..5]] # レジスタファイルの出力ポート0とROMのデータを加算する
adder.x = gnd # キャリー入力は今のところ使わないのでL固定としておく
# キャリー出力は使わないので接続しない
# キャリーフラグを追加するときにそこに接続する予定

# movとaddの命令によってROMの値か加算器を通した値かを選択するマルチプレクサ
muxd.d = [adder.o, bus.o[2..5]]
muxd.s = bus.o[1]

# 電源接続
_vcc.d = H
_gnd.d = L

始めの頃と比べて機能は増えたのに回路部分のコードはシンプルになってきた。複雑な部分は部品内に隠蔽してしまったからだ。
現在は計算する部分は加算しかないのでシンプルだが、今後、例えば減算するSUB命令や、AND/ORなどの論理演算、シフト/ローテイト演算他いろいろ増やしていくとまた複雑になってくるだろう。その時はたぶんALUクラスを作って分離することになる。
演算結果によるフラグ管理や、フラグによる条件分岐も複雑化する要因となりそうだ。
そういえばRAMが未実装だ。RAMに対する読み書きの命令を追加すると、アドレッシング関連も必要になってくる。プログラムをRAMに格納できるようにもしたい。
先は長そうだ。

おしまい

実行した結果はこのような感じになる。

movしてaddする命令によりレジスタの値が変化しているのがわかるだろう。
シミュレータの最新コードはこちら。
https://gist.github.com/mirichi/da035924f10ab18db3d7
今回のコードはこちら。
https://gist.github.com/mirichi/9553e68fbec4c90660d2
シミュレータ本体と分けているが、一部の基本的なクラス類は本体のほうに入れちゃってもいい気がするな。どこまで入れるかが悩むところではあるが。