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

ハードウェアシミュレータの最終目標は、これを使って動作するCPUを作ることである。
前にアセンブラを作ったことはあるからそれで独自命令セットでも何かしらのコードを書くことはできそうだが、俺には高級言語コンパイラを作る技術は無いので、いずれそのへんを勉強して実装するか、もしくは既存のアーキテクチャの命令セットを実装してgccなどでクロスコンパイルして動かすか、のどちらかとなる。
まあ、なんにせよ現時点ではCPUと呼べる何かは全く無いので、地味に実装していこう。今回はレジスタ

4bitレジスタ

現時点では作るアーキテクチャの目標は無いので、適当に実装してみる。とりあえずは「CPUの創りかた」という本のTD4のようなモノになりそうではあるが、こちらは物理的な制約も無いのでもっと自由にテキトーでよい。まずは4bitレジスタである。
レジスタは値を保持するのでDフリップフロップで作り、値を書き込むときの入力と、値を取得するときの出力が得られるように構成する。現在シミュレータで作ってあるDフリップフロップは入力DとクロックCLK、クリア信号のCLRB、それから出力のQとQBがある。エッジトリガ形で、クロックの立ち上がりに値を取り込む。
問題はレジスタの値を書き込むタイミングで、例えば書き込みたく無い場合にクロックを強引にLに落とす為にAND回路をCLKの前に入れる、という手を考えてみたが、次のクロックで取り込みたい場合はANDにHを入力することになり、そのタイミングでまだCLKがHだったら変なデータを取り込んでしまうことになる。これを回避する細工をフリップフロップに追加するのは大変そうである。
なので、レジスタは出力を入力にまわしておいて、書き込みをしない場合は自分自身を書き込むという動作にする。そこにマルチプレクサを挟んでおいて、書き込みしたい場合は外からの値のほうを選択する。

1bitに見えるが4個のDフリップフロップと4bitマルチプレクサの図である。
このレジスタのコードは以下のようになる。

# 4bitレジスタ
class Register4
  def initialize
    @dff3 = DFF.new
    @dff2 = DFF.new
    @dff1 = DFF.new
    @dff0 = DFF.new
    @mux = Mux1_4.new
    @mux.d0 = self.o
    @dff3.d = @mux.o[0]
    @dff2.d = @mux.o[1]
    @dff1.d = @mux.o[2]
    @dff0.d = @mux.o[3]
  end
 
  def clk=(v)
    @dff3.clk = v
    @dff2.clk = v
    @dff1.clk = v
    @dff0.clk = v
  end
 
  def d=(v)
    @mux.d1 = v
  end
 
  # writeが1
  def rw=(v)
    @mux.s = v
  end
 
  def clrb=(v)
    @dff3.clrb = v
    @dff2.clrb = v
    @dff1.clrb = v
    @dff0.clrb = v
  end
 
  def o
    [
      @dff3.q,
      @dff2.q,
      @dff1.q,
      @dff0.q,
    ]
  end
 
  def inspect
    "#{HLHASH[@dff3.q.get]}"+
    "#{HLHASH[@dff2.q.get]}"+
    "#{HLHASH[@dff1.q.get]}"+
    "#{HLHASH[@dff0.q.get]}"
  end
end

rwを1っていうかHにするとクロックの立ち上がりで値が更新される。

ROMの拡張

レジスタを扱うにはそれを指示するための何かしらの命令が必要になる。幸い既にROMはあるので、これを8bit幅に拡張して、書き込む値とレジスタ番号を指定できるよう適当な命令フォーマットを決めておこう。
レジスタは4個作るとすると、番号は0〜3で2bit必要になる。4bitレジスタなのであわせて6bit使い、残りの2bitはとりあえず使わない。他の命令を追加する際にはここの値で制御することになるだろう。まあ、ここは適当に上位2bitを使わないことにして、bit5〜bit2までを値、bit1〜bit0でレジスタを選択することにしてみる。問題があるようなら後で変える。
ROMのbit幅を拡張するには単純にデータ線を増やしてマルチプレクサの幅を広げるだけでよい。例えばマルチプレクサは以下のようになる。

# 8bitデータ2つを1bitで選択するマルチプレクサ
class Mux1_8
  def initialize
    @mux7 = Mux1_1.new
    @mux6 = Mux1_1.new
    @mux5 = Mux1_1.new
    @mux4 = Mux1_1.new
    @mux3 = Mux1_1.new
    @mux2 = Mux1_1.new
    @mux1 = Mux1_1.new
    @mux0 = Mux1_1.new
  end
 
  def s=(v)
    @mux7.s = v
    @mux6.s = v
    @mux5.s = v
    @mux4.s = v
    @mux3.s = v
    @mux2.s = v
    @mux1.s = v
    @mux0.s = v
  end
 
  def d0=(v)
    @mux7.d0 = v[0]
    @mux6.d0 = v[1]
    @mux5.d0 = v[2]
    @mux4.d0 = v[3]
    @mux3.d0 = v[4]
    @mux2.d0 = v[5]
    @mux1.d0 = v[6]
    @mux0.d0 = v[7]
  end
 
  def d1=(v)
    @mux7.d1 = v[0]
    @mux6.d1 = v[1]
    @mux5.d1 = v[2]
    @mux4.d1 = v[3]
    @mux3.d1 = v[4]
    @mux2.d1 = v[5]
    @mux1.d1 = v[6]
    @mux0.d1 = v[7]
  end
 
  def o
    [
      @mux7.o,
      @mux6.o,
      @mux5.o,
      @mux4.o,
      @mux3.o,
      @mux2.o,
      @mux1.o,
      @mux0.o
    ]
  end
 
  def inspect
    "#{HLHASH[@mux7.o.get]}"+
    "#{HLHASH[@mux6.o.get]}"+
    "#{HLHASH[@mux5.o.get]}"+
    "#{HLHASH[@mux4.o.get]}"+
    "#{HLHASH[@mux3.o.get]}"+
    "#{HLHASH[@mux2.o.get]}"+
    "#{HLHASH[@mux1.o.get]}"+
    "#{HLHASH[@mux0.o.get]}"
  end
end

非常にシンプルに数が増えただけであるのだが、このような調子で32bitCPUを作ろうなどと思うとシンプルにめんどくさい。せっかくRubyで書いているのでMuxオブジェクトを配列に詰めるようにして、生成時に指定したbit幅のマルチプレクサが作れるとよさげである。これは今後の課題。

セレクタ

レジスタに値を書き込む場合、命令の2bitからどのレジスタに書き込むかの選択が必要になる。命令の中にレジスタの数だけのbitを用意して、書き込み対象のbitを立てるとかの命令フォーマットにすると回路は簡単になるが、複数レジスタに同時書き込みする用途はあまり無く、また、一般的には命令フォーマットのbitに余裕は無い。今回は余ってるんだけど。
まず、1bitの入力で出力Y0とY1のどちらかをHにするという回路は以下のように簡単である。

んで、これを2bit入力して出力を4つにする場合は、AND回路を追加して以下のようになる。

いやーこういうの書いたことないから下手すぎて申し訳ない。
コードはこんな感じ。

# 2bitの入力から4bitのどれかを選択してHにするセレクタ
class Selector2
  def initialize
    @and3 = AND.new
    @and2 = AND.new
    @and1 = AND.new
    @and0 = AND.new
    @not0 = NOT.new
    @not1 = NOT.new
    @and2.a = @not0.o
    @and1.b = @not1.o
    @and0.a = @not0.o
    @and0.b = @not1.o
  end
 
  def s=(v)
    @not0.a = v[1]
    @not1.a = v[0]
    @and3.a = v[1]
    @and3.b = v[0]
    @and1.a = v[1]
    @and2.b = v[0]
  end
 
  def o
    [
      @and3.o,
      @and2.o,
      @and1.o,
      @and0.o
    ]
  end
 
  def inspect
    "#{HLHASH[@and3.o.get]}"+
    "#{HLHASH[@and2.o.get]}"+
    "#{HLHASH[@and1.o.get]}"+
    "#{HLHASH[@and0.o.get]}"
  end
end

これの入力をROMのbit1〜0に接続して出力を各レジスタのrwに接続して、ROMのbit5〜bit2をデータに接続してやれば、ROM内の命令に従ってレジスタの値が書き換わるようになる。

deadbeef書き込み命令列

上のほうに書いたレジスタ書き込み命令を実際にROMのデータとして用意するわけだが、書き込む値のネタも無いのでdeadbeefにする。A〜Dにdeadを書いてその後もう一度A〜Dにbeefを書く、という8つのMOV命令である。

MOV A, 0x0d
MOV B, 0x0e
MOV C, 0x0a
MOV D, 0x0d
MOV A, 0x0b
MOV B, 0x0e
MOV C, 0x0e
MOV D, 0x0f

と、こんな感じ。こんなふうに書くといかにも命令を実行するCPUができたように思えてくるが、やってることは単純極まりない。っていうかこれしか機能が無い。アセンブラもまだ無いからハンドアセンブルするわけだ。

34 39 2a 37 2c 39 3a 3f

と16進数にしてみてもどうせ回路を構成するときは2進数になるのであまり意味は無い。
また、現時点ではレジスタから読み出す命令も、外に値を出力するためのポートも無いので、ロジックアナライザレジスタの中身を直接覗くことでしか確認することができない。

コード全体と実行例

今回のコードはこちらになる。
https://gist.github.com/mirichi/233fc39c2f4e971aa86b
実行結果はこんな感じ。見にくいけど、それぞれのレジスタの出力が順番にdeadbeefに書き換わっているのがわかる。

おしまい

現時点ではRAMが無いのでわかりにくいが、RAMを追加するとしてもそれは1アドレスに4bitという形になり、命令用ROMとはアドレス空間もサイズも異なるハーバードアーキテクチャになることが予想できる。プログラムをRAMからも実行できるノイマンアーキテクチャにするとRAMのデータバスを命令とデータの両方が使うことになるので、命令フェッチとデータのロード/ストアを別サイクルにする必要がでてきて、すると実行状態を保持して切り替えるような回路が追加で必要になる。いずれはそういう形にしていきたいところだが、まあ、まだ先だろう。