ハードウェアシミュレータ

久しぶりの更新。
最近なんとなくデジタル回路などを勉強してて、しかし目的と手段が入れ替わっているので(いつものこと)、勉強した結果として何をするかという目標は無い。
そのうちDE0とかのFPGA学習ボードを買ってなんか作ってみるかもしれないし、何もしないかもしれない。わからん。
まあ、とりあえず何かやってみよう、という感じで、Rubyでハードウェアシミュレータを作ってみることにした。ひょっとしたらそのうちこれの上でCPUが動くようになるかもしれない。ならないかもしれない。わからん。


ハードウェアのシミュレータと言ってもどこまでシミュレーションするのかは大きな問題である。抵抗とかコンデンサなども計算しようと思えばできないこともないだろうが、アナログ的なところを計算するのは何かと骨が折れるのでそこはバッサリやっちゃって、電気的な部分を抽象化した単純なデジタル回路を想定する。
CPUなどの同期回路を考えると、1クロック単位で状態を計算してやるというのもアリな気がしないでもないが、論理ゲートレベルでモノを考えるならクロックは入力信号の1つでしかないので、同期回路を前提にはしない。
難しいことがやりたいわけではないので状態はHighとLowだけにして、組み込みでNANDゲートだけ作ってやれば、後はそれの組み合わせでそれっぽくできるんじゃなかろうか、などと妄想しつつ、とりあえずNANDだけ作ってみたソースが以下。

HLHASH = {true=>"H", false=>"L", nil=>"nil"}
H = true
L = false

# 出力ピン
class OutputPin
  attr_accessor :output_objects

  def initialize(v=nil)
    @value = nil
    @output_objects = []
    self.set(v) if v != nil
  end

  # 出力をセットする
  def set(v)
    tmp = @value
    @value = v

    if tmp != v
      @output_objects.each do |i|
        i.update
      end
    end
    self
  end

  # 出力の取得
  def get
    @value
  end
end

# NANDゲート試作
class NAND
  def initialize
    @a = @b = OutputPin.new(L)
    @o = OutputPin.new
    update
  end

  def a=(v)
    v.output_objects << self
    tmp = @a.get
    @a = v
    update if tmp != v.get
  end

  def b=(v)
    v.output_objects << self
    tmp = @b.get
    @b = v
    update if tmp != v.get
  end

  def o
    @o
  end

  def update
    @o.set(!(@a.get and @b.get))
  end

  def inspect
    "#{HLHASH[o.get]}"
  end
end

OutputPinクラスがゲートの出力ピンを表す。基本的にはNANDの出力ピンなのだが、なぜこれがNANDクラスから分離しているかと言うと、デジタル回路の出力と入力は1対nになりえるので、出力の更新を通知する対象が複数ありえて、そのロジックをNANDに組み込んでしまうと、NANDを使って他の論理ゲート(例えばNOTなど)を作ったときにまたそれを書かないといけなくなってしまうからである。クラス構成でどうにかできないかなーとも思ったが、組み合わせたものは出力が2つ以上になったりもするからなかなかうまいこと行かず、とりあえず出力ピンだけ分離して対応した。
OutputPinはついでに5V固定とかGND固定のピン単体も作れるのでテスト用の入力などはこれでよさげだ。クロックもこれで再現することができる。
NANDのほうはab2つの入力と、出力が1つある。入力にOutputPinオブジェクトを設定するとNAND#oで取得できるOutputPinオブジェクトからOutputPin#getで出力を取得できる、という寸法だ。

んで、これをどう使うかと言うと、以下のような感じ。

vcc = OutputPin.new(H)
gnd = OutputPin.new(L)

_nand = NAND.new
_nand.a = vcc
_nand.b = gnd
p _nand #=> H

5VとGNDの出力ピンを作って、NANDオブジェクトにそれぞれ繋いでやると、出力としてHが出てくる。
全パターン試してみよう。

vcc = OutputPin.new(H)
gnd = OutputPin.new(L)

_nand = NAND.new
_nand.a = gnd
_nand.b = gnd
p _nand #=> H

_nand.a = vcc
_nand.b = gnd
p _nand #=> H

_nand.a = gnd
_nand.b = vcc
p _nand #=> H

_nand.a = vcc
_nand.b = vcc
p _nand #=> L

ちゃんと動いているようだ。NANDゲートは両方の入力に同じ信号を入れるとNOTとして扱えるのだが、さっきの結果を見ればそのようになっているので別に試す必要は無い。
しかしNOTなのに入力が2本あるのは気持ち悪いので、NOTクラスとして作ってみる。NOTはNANDを使えば作れるので、クラスの内部でNANDオブジェクトを持つようにするとコードがシンプルになる。

# NOTゲート試作
class NOT
  def initialize
    @nand = NAND.new
  end

  def a=(v)
    @nand.a = v
    @nand.b = v
  end

  def o
    @nand.o
  end

  def inspect
    "#{HLHASH[o.get]}"
  end
end

vcc = OutputPin.new(H)
gnd = OutputPin.new(L)

_not = NOT.new
_not.a = vcc
p _not #=> L
_not.a = gnd
p _not #=> H

めんどくさいロジックはNANDのほうに既にあるのでNANDと比べるとずいぶん簡単になった。
じゃあNANDとNOTをつないでみよう。

vcc = OutputPin.new(H)
gnd = OutputPin.new(L)

_nand = NAND.new
_not = NOT.new
_not.a = _nand.o # 接続

_nand.a = vcc
_nand.b = gnd
p _nand #=> H
p _not #=> L

_nand.a = vcc
_nand.b = vcc
p _nand #=> L
p _not #=> H

うむ。ちゃんと動いているっぽい。更にもうちょい難しいものにチャレンジだ。

class RSFF
  def initialize
    @not1 = NOT.new
    @not2 = NOT.new
    @nand1 = NAND.new
    @nand2 = NAND.new
    @nand1.a = @not1.o
    @nand1.b = @nand2.o
    @nand2.a = @not2.o
    @nand2.b = @nand1.o
  end

  def s=(v)
    @not1.a = v
  end

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

  def q
    @nand1.o
  end

  def qb
    @nand2.o
  end

  def inspect
    "q=#{HLHASH[q.get]}, qb=#{HLHASH[qb.get]}"
  end
end

NANDとNOTで構成したRSフリップフロップである。nand1の出力がnand2の入力に、nand2の出力がnand1の入力になるという回路だが、これはどうか。

vcc = OutputPin.new(H)
gnd = OutputPin.new(L)

rsff = RSFF.new
rsff.r = gnd
rsff.s = vcc # セット
p rsff #=> q=H, qb=L
rsff.s = gnd # 保持
p rsff #=> q=H, qb=L
rsff.s = vcc # 念のためもう一度セット
p rsff #=> q=H, qb=L
rsff.s = gnd # 保持
p rsff #=> q=H, qb=L
rsff.r = vcc # リセット
p rsff #=> q=L, qb=H
rsff.r = gnd # 保持
p rsff #=> q=L, qb=H

素晴らしいデスネ。
今日はここまで。