Haskellでいってみよう

日曜プログラマにも満たないレベルでもHaskellで何かソフトウェアを作りたい!

CPUの創りかた(4): Flip Flop

今回こそ、CPU本体の製作に入ろう。本「CPUの創りかた」では、ROMの次に レジスタを作る流れになっている。

ただ、いきなりレジスタはしんどいので、今回はその手前のFlip Flopの話だ。

Flip Flopと状態保持

Flip Flopは順序回路の一つらしいが、状態を記憶することができるらしい (習った気はするがとっくに忘れた)。 だから「レジスタ」は値を保持できるというわけだ。

ではHaskellで「状態を記憶する」にはどうしたらいいのか? オブジェクト指向言語であれば、オブジェクト内に状態を持つなんてのは 普通にできるので簡単だが、Haskellである。「状態」って無いのでは?無理?

調べると、HaskellにはStateモナドなるものがあり、これを使うと何かできそうな 匂いはする。が、解説を読んでもちっともわからないのであきらめた。 仕方がないので全ての「状態」は外で管理することにしてしまおう。 ROMの時と同じく、毎回「状態」も入力し、出力には「状態」を含める。 その「状態」を次の入力にする。 面倒だがそうすることで「状態」は管理できるだろう。

という方針(?)に基づき、Flip Flopを作っていく。

S-R Flip Flop

まずは基本中の基本(?)のS-R Flip Flop(以下、SR-FFとする)から。 Flip Flopについては「CPUの創りかた」にも多少説明はあるのだがいまいち ピンとこない。もう少し詳しいことが知りたかったのでこの本も買った。

これによると、SR-FFの回路図は次のとおりである。

f:id:eijian:20160221100443p:plain

回路自体は単純だが、出力($Q$、$\overline{Q}$)がフィードバックしているところを どう処置するかである。ただ、上述の通り今回は「状態」を関数などの内部に持たず 毎回入力するので、それほど悩まなくてもいいかもしれない。 なお、コード中では$\overline{Q}$のように上線が書けないため、'で 代用している。

{- |
SR-FlipFlop

  IN : [S,R,Q,!Q]
  OUT: [Q,!Q]

-}

lc_srff :: LogicCircuit
lc_srff (s:r:q0:q0':_) = [q, q']
  where
    q_ = head $ lc_nand [s, q0']
    q' = head $ lc_nand [r, q_]
    q  = head $ lc_nand [s, q']

一旦、$Q$について仮の出力を得て、それを$\overline{Q}$の方に回して それを得る。 さらにそれを入力として、あらためて$Q$を得るわけだ。なおソース中の q'は$\overline{Q}$である。ちなみにこのSR-FFは負論理だ(セット/リセットの 時は$S$、$R$をLOにする)。

SR-FFの真理値表は次のようである。

$S$ $R$ $Q$ $\overline{Q}$ remarks
L L - - 禁止
L H H L セット
H L L H リセット
H H ? ? 変化しない

幾つかのWebサイトで真理値表を確認したが、$S$と$R$を両方LOにするのは 「禁止」らしい。 実際にそうしたらどうなるか回路図で追いかけてみたら、$Q$も$\overline{Q}$も HIになり$Q=\overline{Q}$となってわけがわからなくなるようだ。 だが上記コードでは$S$、$R$ともにLOが入力されたらエラーとはせず、 回路図の通り$Q$も$\overline{Q}$もHIを出力することにした。入力・出力を どう扱うかは使う人が対応することだ(自分だが…・)。

以下はdoctestのコードだ。

>>> lc_srff [sLO, sLO, sLO, sLO] == [sHI,sHI]
True
>>> lc_srff [sLO, sHI, sHI, sLO] == [sHI,sLO]
True
>>> lc_srff [sHI, sLO, sHI, sLO] == [sLO,sHI]
True
>>> lc_srff [sHI, sHI, sHI, sLO] == [sHI,sLO]
True
>>> lc_srff [sHI, sHI, sLO, sHI] == [sLO,sHI]
True
-- あってはいけない(Q=!Q)状態を入力
>>> lc_srff [sHI, sHI, sLO, sLO] == [sHI,sLO]
True
>>> lc_srff [sHI, sHI, sHI, sHI] == [sLO,sHI]
True

最後に、ここで作ったSR-FFの真理値表を示す。テストコードの最後の二つも 本来あってはいけないものだが、なんとなく値を返しているのはコードでの 処理の仕方によるものなので物理的な回路とは異なるかもしれないがご愛嬌。

$S$ $R$ old $Q$ old $\overline{Q}$ new $Q$ new $\overline{Q}$
L L ? ? H H
L H ? ? H L
H L ? ? L H
H H L H L H
H H H L H L
H H L L H L
H H H H L H

('?'はLO/HIどちらでもよいという意味)

エッジトリガ式 D Flip Flop

解説書などではS-Rの次は同期式S-RとかJK Flip Flopとか色々話が続くのだが、 どうやらCPUで実際に使われるのは「エッジトリガ式 D Flip Flop」だそうだ。 これは、clockの立ち上がり(or 立ち下がり)の瞬間の$D$の値を保持するらしい。 エッジトリガ式D Flip-Flopは長いので以後D-FFとし、値はクロックの 立ち上がり時に変化することとする。

ということで、D-FFはクロックの立ち上がり時の$D$の値を保持する=出力するの だから、真理値表は次のようになるだろう。

$D$ $Q$ $\overline{Q}$
L L H
H H L

なんと単純な!これなら超簡単にコードにできる。

lc_dff :: LogicCircuit
lc_dff (d:_) = [d, not d]

テストはこう。

>>> lc_dff [sLO] == [sLO,sHI]
True
>>> lc_dff [sHI] == [sHI,sLO]
True

テストもちゃんと通る! 難関と思っていたD-FFがいとも簡単に実装できてしまった! いやいやいやいや、そういうわけにはいかない。これだと以前のdecorderと同じだ。 ここはやはり、ちゃんと論理回路をコードで表現して結果を得ないと。

D-FFの回路として、先の本(ゼロから学ぶディジタル論理回路)に記載されている 6NAND型 D Flip-Flopを採用しよう。回路図は以下の通り。

f:id:eijian:20160221100436p:plain

これをコードにしたいわけだがここで問題がある。クロックの立ち上がりなので $C$=HIであるから、$X_1$は$X_0$がわからないと決まらない。$X_0$は$X_1$と $X_3$が決まらないとわからない。ということで、結局入力$D$がHIかLOか 分かっていても$X_n$を決められない。さてどうするか。

今回、論理回路を表す関数はその呼出しの瞬間がクロックの立ち上がりと決めた。 であれば、その「直前」はクロック入力$C$=LOということだ。 そう考えて上の回路図を見直すと、$C$=LOだから$X_1$と$X_2$はHIと決まる。 $X_3$は$D$と$X_2$から決まる。$X_0$も$X_1$と$X_3$から決まるわけだ。 これで直前の状態が決まった。

次に$C$=HIとして各値を求めよう。すでに$X_n$は求めているからそれを使えば 新しい$X_1$と$X_2$が得られる。回路図の右半分はSR-FFそのままだから そいつに入力してやればよい。。。いや、本当にそうか? $\overline{S}$と$\overline{R}$がともにHIだったら「現状維持」となり、 そのためには「現状」がわからないとやっぱりダメだ。

うーん、とりあえずどういう状態になるか調べてみることにする。 $D$と$X_n$の真理値表を書いてみた。クロックの立ち上がり前後があれば わかりやすいので$C$もつけた。

$D$ $C$ $X_0$ $X_1$ $X_2$ $X_3$
L L L H H H
L H L H L H
H L H H H L
H H H L H L

$D$がLOのときにクロックが立ち上がる前と後の$X_n$を並べた。 SR-FFの入力は、$S=X_1$、$R=X_2$であるから、クロックの立ち上がり ($C$=HI)時のそれぞれの値を見てみると、$D$がHIでもLOでも $X_1=\overline{X_2}$である。であればSR-FFへの入力は常に値のセットもしくは リセットとなるため、過去の状態に関わらず必ず$Q$と$\overline{Q}$が 決まることになる!悩む必要はないわけだ。

上記を踏まえてコードに落としてみたのが以下。

lc_dff :: LogicCircuit
lc_dff (d:_) = lc_srff (x1' ++ x2' ++ [sLO, sHI])
  where
    -- before rising (C = LO)
    x1 = [sHI]  -- because C = LO
    x2 = [sHI]  -- because C = LO
    x3 = lc_nand (d  ++ x2)
    x0 = lc_nand (x1 ++ x3)
    -- rising edge (C = HI)
    x1' = lc_nand ([sHI] ++ x0)
    x2' = lc_nand ([sHI] ++ x1' ++ x3)

先の論理回路を使わない方をlc_dff'と改名して比較テスト。

>>> lc_dff [sLO] == lc_dff' [sLO]
True
>>> lc_dff [sHI] == lc_dff' [sHI]
True

ちゃんとテストも通る!

ClearとPresetの追加

さて、D-FFを実際に使うとしたらClear(CLR)やPreset(PR)ができないと だめだろう。特にClearはシステムの起動時のリセット処理で必要となる。 ということで、次のようなモジュールにしよう。ClearとPresetは ともに負論理とする。

f:id:eijian:20160221100431p:plain

このようなモジュールが入ったICもあるようだが、その論理回路はややこしい みたいなので勝手に仕様を決めてしまうことにする。

  • ClearしたいときはCLR=LOにする。その時はPresetや$D$の値によらず $Q$はLOにする。
  • PresetしたいときはCLR=HIかつPR=LOにする。その時は$D$の値によらず $Q$はHIにする。
  • CLR=PR=HIなら、$D$の値を$Q$に出力する。

これを満足するようにCLRPR、$D$の3つの値から、新たに$D'$を作ろう。 D-FFなので結果的に$D'=Q$である。上の仕様を真理値表にすると以下のようになる。 '?'はHI/LO両方を表す。

CLR PR $D$ $D’$(=$Q$)
L ? ? L
H L ? H
H H L L
H H H H

これを論理回路にしよう。CLRPR、$D$の3入力から$D’$を得る式を検討する。 結論を書くと次のようになる。ちなみに(超単純ではあるが)カルノー図を描いたのは 20年ぶりぐらいかな?

 {
    D' = CLR \cdot (\overline{PR} + D)
  }

これを踏まえ、CLRPR付きのD-FFを次のように定義しよう。

lc_dff_cp :: LogicCircuit
lc_dff_cp (c:p:d:_) = lc_dff d'
  where
    d' = lc_and ([c] ++ lc_or (lc_not [p] ++ [d]))

まあ、そのまんまなんだが。ついでにテストは以下。うまく動いているようだ。

>>> lc_dff_cp [sLO, sLO, sLO] == [sLO,sHI]
True
>>> lc_dff_cp [sLO, sLO, sHI] == [sLO,sHI]
True
>>> lc_dff_cp [sLO, sHI, sLO] == [sLO,sHI]
True
>>> lc_dff_cp [sLO, sHI, sHI] == [sLO,sHI]
True
>>> lc_dff_cp [sHI, sLO, sLO] == [sHI,sLO]
True
>>> lc_dff_cp [sHI, sLO, sHI] == [sHI,sLO]
True
>>> lc_dff_cp [sHI, sHI, sLO] == [sLO,sHI]
True
>>> lc_dff_cp [sHI, sHI, sHI] == [sHI,sLO]
True

これで、やっとレジスタに使えるD-FFが得られた。

まとめ

今回はFlip Flop、特にCPUで使うエッジトリガ式D Flip Flopを作った。 次回はこれを多数並べて、いよいよレジスタを作ることにしよう。

(ここまでのソースはこちら)