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

前回、ROMを8bit化するのに単純な手を使ったが、色んなものの幅を変えるたびにコードを書いていくのは単純にめんどくさい。
例えば複数の素子が並んでいる状態はRubyの配列に入れておけばよく、配列に入れるのであればそれは繰り返しで処理できるということなので、Rubyで動的に生成することが可能である。
今回のお題は「すべてを可変にする」。

単純なバッファを作る

何かクラスを作ったとき、外からの入力は内部回路のどこかに接続することになるが、回路を動的に生成するのであれば、どこに接続すればいいのかを覚えておく方法が必要になり、手間が多い。
単純なバッファオブジェクトがあれば、生成した内部回路にはバッファを繋いでおき、外からの入力はバッファに繋ぐという手でシンプルにできる。
ということでバッファクラスを作る。1bitバッファなのでBuffer1という名前にしておく。

# 単純なバッファ
class Buffer1
  def initialize
    @not1 = NOT.new
    @not2 = NOT.new
    @not1.a = @not2.o
  end

  def d=(v)
    @not2.a = v
  end

  def o
    @not1.o
  end
end

NOTを2個繋げたものである。NOTのNOTなので元の値が出力される。これ自体に意味は無いが、上記のような状況で使うと入力を一元化できるので便利という話。
ところでこのバッファも、可変幅の何かの入力に使う場合は配列にして並べることになるので、まとめて管理できて入出力を配列化したものがあると便利である。

# 単純なバッファの塊
class Buffer
  def initialize(bit_width)
    @buf = Array.new(bit_width){Buffer1.new}
  end

  def d=(v)
    @buf.zip(v) do |buf, o|
      buf.d = o
    end
  end

  def o
    @buf.map{|b|b.o}
  end
end

内部的にはBuffer1オブジェクトの配列を持ち、入力は配列で受け取って全部繋ぎ、出力は出力ピンの配列となる。複数bitのマルチプレクサやレジスタなどのオブジェクトと配列で直接接続できる。
どうでもいいがArray#zipを便利に使えたのは10年以上のRuby歴にして今回が初めてである。

バスとスリーステートバッファ

バスはデータバスやアドレスバスその他CPU内部ではたくさんのバスが登場するが、基本的に1bitバスということは無いので、これらもまとめて管理できると便利である。

class Bus1
  def initialize
    @o = OutputPin.new
  end

  def o
    @o
  end

  def add(v)
    v.connect_bus(self)
  end
end

class Bus
  def initialize(bit_width)
    @bus = Array.new(bit_width){Bus1.new}
  end

  def o
    @bus.map{|b|b.o}
  end

  def add(v)
    @bus.zip(v) do |b, tsb|
      tsb.connect_bus(b)
    end
  end
end

class ThreeStateBuffer1
  def d=(v)
    v.output_objects << self
    @d = v
  end

  def g=(v)
    v.output_objects << self
    @g = v
  end

  def update
    @bus.o.set(@d.get) if @g and @g.get
  end

  def connect_bus(b)
    @bus = b
  end
end

class ThreeStateBuffer
  def initialize(bit_width)
    @tsb = Array.new(bit_width){ThreeStateBuffer1.new}
  end

  def d=(v)
    @tsb.zip(v) do |tsb, o|
      tsb.d = o
    end
  end

  def g=(v)
    @tsb.each do |tsb|
      tsb.g = v
    end
  end
end

それぞれ1が付かないほうは今までと同じ名前だが、入出力が配列になっているので互換性は無い。
スリーステートバッファのほうは入力gがすべてのバッファに共通となっていて、まとめて管理するのによい。

マルチプレクサ

マルチプレクサの基本は1bit選択信号で1bitデータの2つを選択するものだが、Bufferと同様に並べてやると可変幅となる。

# n bitデータ2つを1bitで選択するマルチプレクサ
class Mux1_n
  def initialize(bit_width)
    @mux = Array.new(bit_width){Mux1_1.new}
  end

  def s=(v)
    @mux.each do |m|
      m.s = v
    end
  end

  def d=(v)
    @mux.zip(*v) do |m, d0, d1|
      m.d0 = d0
      m.d1 = d1
    end
  end

  def o
    @mux.map{|m|m.o}
  end
end

これについては特筆すべきことは無い。
問題はデータ幅ではなく、選択信号の幅を増やす場合である。ツリー状に接続されるマルチプレクサを動的に構成するには、それっぽいアルゴリズムが必要になる。再帰で綺麗に作れそうだったが、別のメソッドを定義したりProcを作ったりするのが面倒だったので適当にざっくり作ってみた。

# n bitデータをn bitで選択するマルチプレクサ
class Mux
  def initialize(select_bit_width, data_bit_width)
    # 作成
    @mux = Array.new(select_bit_width){|i| Array.new(2**i){Mux1_n.new(data_bit_width)}}
    @buf = Buffer.new(select_bit_width)

    # 選択線接続
    @mux.zip(@buf.o).each do |ary, b|
      ary.each do |m|
        m.s = b
      end
    end

    # データ線接続
    if select_bit_width > 1
      (1...select_bit_width).each do |i|
        @mux[i-1].each.with_index do |m, j|
          m.d = [@mux[i][j * 2].o, @mux[i][j * 2 + 1].o]
        end
      end
    end
  end

  def s=(v)
    @buf.d = v
  end

  def d=(v)
    @mux.last.each.with_index do |m, i|
      m.d = [v[i * 2], v[i * 2 + 1]]
    end
  end

  def o
    @mux[0][0].o
  end
end

うん、どう見ても回路を構成するコードには見えない。でも一応これはデジタル回路を作るコードなのである。

ROM

可変マルチプレクサができたらそれを使ってROMが簡単に作れるようになる。bit幅もデータ数も生成時に自由に指定できるようになるのでこいつは超便利だ。ラクをするために16bitROMを2個使うとかする必要も無くなる。

# ROM
class ROM
  def initialize(select_bit_width, data_bit_width)
    @mux = Mux.new(select_bit_width, data_bit_width)
    @tsb = Array.new(data_bit_width){ThreeStateBuffer1.new}
    @not = NOT.new

    @tsb.each.with_index do |tsb, i|
      tsb.d = @mux.o[i]
      tsb.g = @not.o
    end
  end

  # アドレス線
  def a=(v)
    @mux.s = v
  end

  # チップセレクト
  def csb=(v)
    @not.a = v
  end

  # 保持データの接続
  def d=(v)
    @mux.d = v
  end

  # 出力(スリーステートバッファ)
  def o
    @tsb
  end
end

セレクタ

複数のレジスタを選択するのにセレクタを作ったのだが、レジスタの数を変えるとセレクタも作り直しであり、これが面倒なのでセレクタも可変にする。だがしかしこれは非常にややこしい。

# n bitの入力から2**n bitのどれかを選択してHにするセレクタ
class Selector
  def initialize(bit_width)
    @buf = Buffer.new(bit_width)
    tmp = NOT.new
    tmp.a = @buf.o[0]
    @o = [@buf.o[0], tmp.o]

    if bit_width > 1
      (bit_width-1).times do |i|
        tmp = NOT.new
        tmp.a = @buf.o[i+1]
        bary = [@buf.o[i+1]]*(@o.size) + [tmp.o]*(@o.size)

        @o = @o * 2
        @o = @o.zip(bary).map do |o, b|
          a = AND.new
          a.a = o
          a.b = b
          a.o
        end
      end
    end
  end

  def s=(v)
    @buf.d = v.reverse
  end

  def o
    @o
  end
end

試行錯誤したので自分で見ても何をやっているのかよくわからにあ。

レジスタ

そういえばDフリップフロップも複数並べれるようにしたが、面倒なので割愛。これを使ってレジスタの幅を可変にした。

# レジスタ
class Register
  def initialize(bit_width)
    @dff = DFF.new(bit_width)
    @mux = Mux1_n.new(bit_width)
    @dff.d = @mux.o
  end

  def clk=(v)
    @dff.clk = v
  end

  def d=(v)
    @mux.d = [self.o, v]
  end

  # writeが1
  def rw=(v)
    @mux.s = v
  end

  def clrb=(v)
    @dff.clrb = v
  end

  def o
    @dff.q
  end
end

ここまでできればレジスタ数、bit幅が可変なレジスタファイルクラスを作ることができる。それができたら出力側に可変マルチプレクサを繋げて、非常にラクレジスタを拡張できる。
レジスタ数が増えると命令内のレジスタ指定用bitが増えるが、内部ROMなので必要なだけ幅を増やしてやればすぐに対応できる。ROMも可変サイズ対応だし、もうやりたい放題な感じになってきた。

おしまい

bit数が増えたらそのままコードが増える過去のやり方を一掃した。32bit化する場合はnewの引数を32に変えるだけでよいのでレジスタを4bitにする必要ももはや無い。と言ってもだからと言って32bitのMIPSプロセッサがすぐ作れるというわけでもなし、これは寄り道のようなものなので、今後はまたロジックをどう作るかを考える旅に出る。
今回のコードは
シミュレータ本体が
https://gist.github.com/mirichi/df54ed39ec1437d5aa9a
mov命令によるdeadbeef転送が
https://gist.github.com/mirichi/f8c905444f69076db1ee
となっている。