Haskellでいってみよう

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

CPUの創りかた(5): 4 bitレジスタ

今回は、前回作ったD Flip Flop(以下D-FF)を使ってレジスタを作ろう。 対象のCPU(TD4)は4 bitのレジスタを2つ持っている。これを実装したいわけだ。

1 bitレジスタ

最初から複雑なものを作るのは大変なので、まず簡単なところから1 bitレジスタを 考えることにする。今回作るレジスタの基本構造は次の通り。

f:id:eijian:20160228154439p:plain

レジスタは基本的に値を保持し続けるものだ。しかし前回作ったD-FFはクロックの 立ち上がり時点の入力値がそのまま出てくるだけだ。 だからD-FFの出力をぐるりと回してきて入力に戻してやる。そうすれば、 クロックが入るたびに出てきたものをまた入力するので同じ値がぐるぐる回る、 というわけだ(入力A)。

でもそれだけだと値が変わらず何の役にも立たないので、外からの入力値(入力B)も 受け入れられるようにする。その値と保持している値のどちらを新しい値とするかを スイッチで切り替えるのだ。スイッチには、以前作ったMultiplexerが使える。

なお、前回のD-FFは出力値を強制的にHIにセットするPR入力と反転した出力 $\overline{Q}$を持っているが、使わないので割愛する(PRには常にHI(無効)を 入力、出力は$Q$だけ取り出す)。早速コードにしてみよう。

-- c: !CLR
-- l: !LD (LO -> select a, HI -> select b)
-- a,b: input value (A and B)

lc_register1 :: LogicCircuit
lc_register1 (c:l:a:b:_) = take 1 $ lc_dff_cp ([c, sHI] ++ d)
  where
    d = lc_multiplexer2ch [l, a, b]

リセットしたい時はCLRをLOにする。LDは負論理なのでHIのときは値を保持(Aを採用)、 LOで外からの入力値をセット(Bを採用)だ。前回書いたように、値を内部的に 保持することはせず、回路の外で管理することにした。なので保持したい値 (上記ではA)を毎回入力してやる必要があるのだ。

テストコードは以下。

>>> lc_register1 [sLO, sLO, sHI, sHI] == [sLO]  -- clear
True
>>> lc_register1 [sHI, sHI, sHI, sLO] == [sHI]  -- select A
True
>>> lc_register1 [sHI, sHI, sLO, sHI] == [sLO]  -- select A
True
>>> lc_register1 [sHI, sLO, sHI, sLO] == [sLO]  -- select B
True
>>> lc_register1 [sHI, sLO, sLO, sHI] == [sHI]  -- select B
True

4 bitレジスタ

1 bitレジスタができれば4 bitも簡単だ。4つ並べればよい。ただし、CLRと LDは4つの1 bitレジスタで共通に使う。早速コードにしてみよう。

lc_register4 :: LogicCircuit
lc_register4 (c:l:ds)
  where
    a = take 4 ds
    b = take 4 $ drop 4 ds
    procReg1 :: Bin -> Bin -> (Bin, Bin) -> [Bin]
    procReg1 c l (a, b) = lc_register1 [c, l, a, b]

入力データ部分(ds)から、4 bitずつ2つの値A,Bを取り出している。 A,Bのリストから同じ桁の要素を組みにして先の1 bitレジスタに入れている だけだ。とてもシンプルだ。

下記のような簡単なテストもしてみた。大丈夫そう。

>>> let d0 = toBits "0000"
>>> let d1 = toBits "1111"
>>> let d2 = toBits "0101"
>>> let d3 = toBits "0011"
>>> lc_register4 ([sLO, sLO] ++ d1 ++ d2) == d0  -- when CLR = ON
True
>>> lc_register4 ([sLO, sHI] ++ d1 ++ d2) == d0  -- when CLR = ON
True
>>> lc_register4 ([sHI, sHI] ++ d1 ++ d2) == d1  -- when LD = OFF
True
>>> lc_register4 ([sHI, sLO] ++ d1 ++ d2) == d2  -- when LD = ON
True

CLRをLOにすればレジスタはリセットされるので入力によらず出力は"0000"。 HIにすればLDのOFF/ONによって、現在値Aか新規入力値Bが選択されている。

D-FFさえ用意してあれば、なんともあっけないものだ :-)

$n$ bitレジスタ

さて、先の4 bitレジスタのコードをあらためて見てみると'4'が散見される。 このようなマジックナンバーはいただけない。定数にしてまとめたらどうか? いや、このコードであれば別に4に限定する必要すらなさそうだ。ならばビット数を 引数で与えてやれば汎用的になるのではないか?

lc_register :: Int -> LogicCircuit
lc_register w (c:l:ds) = concat $ map (procReg1 c l) $ zip a b
  where
    a = take w ds
    b = take w $ drop w ds
    procReg1 :: Bin -> Bin -> (Bin, Bin) -> [Bin]
    procReg1 c l (a, b) = lc_register1 [c, l, a, b]

これなら4 bitと言わず、8 bitでも64 bitでも行けそうだ!今回は使い道がないが…。 これを使って先のlc_register4を置き換えよう(部分適用だな)。

lc_register4 :: LogicCircuit
lc_register4 = lc_register 4

さっきと同じテストを実行し、エラーがないことを確認。

ところで引数のwはビット数を表すが、チェックしなくていいのだろうか? これまでは面倒くさいので目をつむっていたが、やはり入力値チェックは しておいたほうがいいのだろうなと思う。さてwの条件は何だろう? ビット数だから1以上?でも内部で1 bitレジスタを呼んでいるから2以上で ないと意味がない?

1だと無駄はあるが、結果は正しいので許容できる。0だと結果は0個=空リストに なってしまうが、これはこれでありかもしれないと思い始めた。さすがに負の ビット数はないので、wは0以上にしよう。となると入力データのサイズも 気になる。現状値と新規入力値が必要なのでビット数x2だ。これらの条件を 満たさないならどうするか?HaskellならMaybe型で返すのが真っ当な気がする。 が、あとあと大変な予感がするので、へなちょこながらとりあえず今の所は errorで逃げることにする。

lc_register w (c:l:ds)
  | w < 0       = error ("bit width is invalid (" ++ show w ++ ")")
  | len < w * 2 = error ("no enough input (" ++ show len ++ ")")
  | otherwise   = concat $ map (procReg1 c l) $ zip a b
  where
    len = length ds
      :

これで、入力値がまともでなければ処理が「止まる」。まあシステムがPANICを 起こしたと考えればいいか。

まとめ

前回のD-FFを用いてレジスタが準備できた。「状態」は別途管理しないと 行けないので完璧とは言えないが、まあよしとしよう。

次は何にするか? 加算器あたりかな?

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