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

回路図と基板だけ渡されてコード書けって言われる仕事を2回ほどやったことがあるので回路図を読むことはある程度できるが、まともに動作する回路図を書いたことは難しい部分を抽象化したデジタル回路ですら経験が無い。
ということで、適当にハードウェアシミュレータを作ってみてはいても、果たしてこれでちゃんと動作する回路が作れるのかどうかは微妙に怪しい。まあ適当だしな。

今回はDフリップフロップについて。

Dフリップフロップを作ってみる

とりあえずコードから。

class DFF
  def initialize
    @not1 = NOT.new
    @not2 = NOT.new
    @nand1 = NAND.new
    @nand2 = NAND.new
    @nand3 = NAND.new
    @nand4 = NAND.new
    @nand5 = NAND.new
    @nand6 = NAND.new
    @nand7 = NAND.new
    @nand8 = NAND.new

    @not2.a = @not1.o
    @nand1.b = @not1.o
    @nand2.a = @nand1.o
    @nand2.b = @not1.o
    @nand3.a = @nand1.o
    @nand3.b = @nand4.o
    @nand4.a = @nand3.o
    @nand4.b = @nand2.o
    @nand5.a = @nand3.o
    @nand5.b = @not2.o
    @nand6.a = @nand5.o
    @nand6.b = @not2.o
    @nand7.a = @nand5.o
    @nand7.b = @nand8.o
    @nand8.a = @nand7.o
    @nand8.b = @nand6.o
  end

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

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

  def q
    @nand7.o
  end

  def qb
    @nand8.o
  end

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

NANDがいっぱいあって何がどう繋がっているのか見てもさっぱりわからないが、これはWikipediaのDフリップフロップをそのままコード化したものである。
DフリップフロップはRSフリップフロップをベースに構築されるが、違いは入力がRSじゃなくてデータDとクロックCになるところで、RSで言うところのSがDになっていて、Cが0の時にはRSで言うところの保持動作をするようになっている。WikipediaのやつはCにNOTを挟みつつRSフリップフロップが2段になった構成だ。
動かしてみよう。フリップフロップの動作は横軸に時間を取る形のタイムチャートにすると見やすいのでそのように出力してみる。

timechart = ["D  ","C  ","Q  ","QB "]
_dff = DFF.new

_dff.d = gnd
_dff.c = gnd
timechart[0] << HLHASH[gnd.get]
timechart[1] << HLHASH[gnd.get]
timechart[2] << HLHASH[_dff.q.get]
timechart[3] << HLHASH[_dff.qb.get]

_dff.c = vcc
timechart[0] << HLHASH[gnd.get]
timechart[1] << HLHASH[vcc.get]
timechart[2] << HLHASH[_dff.q.get]
timechart[3] << HLHASH[_dff.qb.get]

_dff.c = gnd
timechart[0] << HLHASH[gnd.get]
timechart[1] << HLHASH[gnd.get]
timechart[2] << HLHASH[_dff.q.get]
timechart[3] << HLHASH[_dff.qb.get]

_dff.d = vcc
timechart[0] << HLHASH[vcc.get]
timechart[1] << HLHASH[gnd.get]
timechart[2] << HLHASH[_dff.q.get]
timechart[3] << HLHASH[_dff.qb.get]

_dff.c = vcc
timechart[0] << HLHASH[vcc.get]
timechart[1] << HLHASH[vcc.get]
timechart[2] << HLHASH[_dff.q.get]
timechart[3] << HLHASH[_dff.qb.get]

_dff.d = gnd
timechart[0] << HLHASH[gnd.get]
timechart[1] << HLHASH[vcc.get]
timechart[2] << HLHASH[_dff.q.get]
timechart[3] << HLHASH[_dff.qb.get]

_dff.c = gnd
timechart[0] << HLHASH[gnd.get]
timechart[1] << HLHASH[gnd.get]
timechart[2] << HLHASH[_dff.q.get]
timechart[3] << HLHASH[_dff.qb.get]

_dff.c = vcc
timechart[0] << HLHASH[gnd.get]
timechart[1] << HLHASH[vcc.get]
timechart[2] << HLHASH[_dff.q.get]
timechart[3] << HLHASH[_dff.qb.get]

_dff.c = gnd
timechart[0] << HLHASH[gnd.get]
timechart[1] << HLHASH[gnd.get]
timechart[2] << HLHASH[_dff.q.get]
timechart[3] << HLHASH[_dff.qb.get]

puts timechart
#=> D  LLLHHLLLL
    C  LHLLHHLHL
    Q  LLLLHHHLL
    QB HHHHLLLHH

QとQBは反転してるだけだからいいとして、これらの値が変化するタイミングはCがLからHになったときだけである。その瞬間にDの値がセットされて、次のCの立ち上がりまでDの値に関わらず保持される。

ハマったポイント

Wikipedia版のDフリップフロップを最初に作ってみたとき、実はちゃんと動作しなかった。原因はソフトウェア処理なので値を順番に計算していて、2段目のフリップフロップのCにあたる2個目のNOTの出力よりも、1段目のフリップフロップの計算を先にやっていたことだった。
2段構成のDフリップフロップは、
・Cが0の時、1段目がDを取り込み、2段目は保持
・Cが1の時、1段目は保持、2段目がDを取り込む
という動作になっていて、つまり入力のCが1→0になったタイミングでは、1段目の出力が変わる前に2段目が保持動作に入らなければならない。これは物理的な信号伝達のタイミングの話であり、例えば2個目のNOTゲートが極端に遅い素子で作られていたら、シミュレータじゃなくても同様にちゃんと動かない、ということである。たぶん。
また、Dフリップフロップが作れたらレジスタSRAMが作れるという話にもなるが、これについてもクロック入力で値を取り込むよりも先に、他の回路がクロックに反応して入力データを変更してしまうと書き込みが正常にできないわけなので、同期回路と言えど抽象化で死ぬほど単純になるってことも無いらしい。
ハードウェアシミュレータでは出力の変更を出力先のゲートに通知することで計算を連鎖させているが、これだと信号伝達の時間軸による順序の計算ではないので、出力先の登録順(=通知順)をうまいこと細工することで回路が誤動作しないようにしてやる必要がある。と言ってもそれだけで回避できる話ばかりでもないだろうが、とりあえずはシンプルなシミュレータにするためにここは目をつぶっておく。ダメそうだったら修正を検討することにしよう。

他のDフリップフロップ

検索すると色々出てくる。とりあえず日本電気技術者協会のサイトをネタにしてみよう。なお、この協会が何なのかは知らない。
ここに出てくるDフリップフロップWikipedia版と比較すると異様に簡単である。ゲートの数が半分以下。でもどっちもDフリップフロップを名乗っているので、ひょっとして等価回路なんですかね?って思うがそんなことは無く、こちらの簡単なほうはクロックの立ち上がりじゃなくてクロックが1の間ずーっと取り込み続けるタイプのDフリップフロップである。いずれにしてもクロックが1になるまで取り込みを遅延するのでDelayFFってなわけでどっちも間違いではない。クロックの立ち上がりの瞬間だけ取り込むようにすると倍以上にややこしくなる、ということだ。
さて、んじゃこの簡単なほうの回路を作ってみよう。

class DFF
  def initialize
    @nand1 = NAND.new
    @nand2 = NAND.new
    @nand3 = NAND.new
    @nand4 = NAND.new
    @nand2.a = @nand1.o
    @nand3.a = @nand1.o
    @nand3.b = @nand4.o
    @nand4.a = @nand3.o
    @nand4.b = @nand2.o
  end
  def d=(v)
    @nand1.a = v
  end
  def c=(v)
    @nand1.b = v
    @nand2.b = v
  end
  def q
    @nand3.o
  end
  def qb
    @nand4.o
  end
  def inspect
    "q=#{HLHASH[q.get]}, qb=#{HLHASH[qb.get]}"
  end
end

さっきと同様のテストを走らせると結果は以下のようになる。

puts timechart
#=> D  LLLHHLLLL
    C  LHLLHHLHL
    Q  LLLLHLLLL
    QB HHHHLHHHH

出力が違う。クロックがHの間にDがLになると、QもLになる。取り込みタイミングがエッジに限定されない。ここがWikipedia版との違いである。
このサイトには第3図としてタイムチャートが載っているのだが、このチャートはWikipedia版のDフリップフロップのチャートであって、ここに載っている回路のチャートでは無い。ということがわかる。
まあ、Dフリップフロップについて調べているとだいたいのサイトは中身の詳細を解説していないので、フリップフロップの動作原理を理解する必要性からして怪しく、実際のところどうでもいいのかもしれない。

おしまい

でも「NANDゲートだけで地道に構築したDフリップフロップがそれっぽく動作するハードウェアシミュレータですよ」って言えるとちょっとかっこいいよね。