しかくいさんかく

解答略のメモ

4日目: [CPU] 1bitのCPUとHDL

この記事はひとりでCPUとエミュレータとコンパイラを作る Advent Calendar 2017の4日目の記事です。

一昨日はトランジスタでNANDゲートを作り、昨日はDフリップフロップを作った。 今日はいよいよCPUを導入する。

昨日はややこしかったけど、今日は簡単だ。しかし今後1週間の中で最も重要な内容だと思う。

Dフリップフロップの復習

昨日登場したDフリップフロップだが、名前は長いし回路はややこしいしで、馴染めなかったと思う。 回路の詳細はぶっちゃけ理解できなくても問題ないのだが

  • クロックが上がる瞬間に、入力(D)が出力(Q)にコピーされる
  • その瞬間以外は、入力を変えても出力は変わらない

この2つの機能だけはしっかり押さえておく必要がある。

今後は、以下の長方形記号でDフリップフロップを表す。

f:id:kaitou_ryaku:20171203034014p:plain:w400

左図が正式なのだが、クロックに繋ぐのは当然なので省略し、右図のように描くこともある。

1bit不変CPU

ここで、Dフリップフロップの出口を入口に繋いだらどうなるか考えてみよう。 図の左下のクネクネしたやつはクロック生成器を表している。

f:id:kaitou_ryaku:20171203034037g:plain:w300

この回路、実は1bitのCPUなのである

Dフリップフロップの出口(Q)の電圧を、変数aだと思って欲しい。 すると、クロックが上がる毎にaを前回のaと同じ値で塗り替えていることになる。 つまり、a=aという代入命令を毎クロック実行していると解釈できる。 ちなみに上の動画のQの電圧は常にH(緑)なので、aはずっと1のままだと解釈できる。

aがずっと0の状況は以下の動画のようになる

f:id:kaitou_ryaku:20171203034044g:plain:w300

1bit反転CPU

今見た1bit不変CPUの説明を読んでも、分かったような分からないような気分になると思う。そこで次の例を見て欲しい。

さっきと同じくDフリップフロップの出口と入口を繋いだ回路なのだが、途中にNOTゲートを挟んでみた。 さっきより代入してる雰囲気が増したと思う。

f:id:kaitou_ryaku:20171203034104g:plain:w300

クロックが立ち上がる瞬間(つまりクロックが黒から緑に変わる瞬間)に、電圧がぐるーっと回っているが、これは見やすさのためであり実際は即座に伝わる。

この回路の挙動を詳しく説明すると

  1. クロックが黒から緑に上がった瞬間に、Dフリップフロップの出口(Q)が入口(D)と同じ電圧になる
  2. 入口(D)付近の電圧をb,出口(Q)付近の電圧をaとすると、1.の挙動はa=b代入と解釈される
  3. 代入後、電圧がぐるっと周回し、出口(Q)の電圧は入口(D)の電圧の逆になる
  4. これはb=~a計算を行ったと解釈される (~はビット反転を表す。1=~0, 0=~1)
  5. 再びクロックが上がると、代入命令a=bが実行される...

重要な部分だけ抽出すると

  • 代入 : クロックが立ち上がる瞬間に、Dフリップフロップの入口が出口を上書きすること
  • 計算 : クロックが立ち上がったに、Dフリップフロップの出口から出た電圧がNOTゲートを通過すること

CPUとは、このように代入→計算→代入→計算→...を繰り返す回路のことなのだ。

CPUの基礎は以上で尽くされる。代入と計算だけだ。これが説明できて嬉しいぜ~

ちなみにクロック立上りの際に「何も代入しない」という選択肢はない。 元の値を保持したければ、最初の回路のように「前回と同じ値を代入」する必要がある。 電圧は0と1しかない(NULLもundefinedも空文字もない)ので、Dフリップフロップはクロック立上り毎に値を変えるか否かの二択を迫られている。

Dフリップフロップのように、CPUの回路上で変数のように働く(情報を保持する)素子をレジスタという。 レジスタを実現する素子は色々あるのだが、とりあえずはDフリップフロップ=レジスタと思って問題ない。

明日以降はレジスタの数を増やし、またNOTゲートの代わりにもっと複雑なゲートを使うことで、CPUをどんどん高機能化していく予定だ。

1bit反転CPU製作キット

今導入した1bit反転CPUを、ハードウェア記述言語(Hardware Description Language, HDL)のSystem Verilogで記述したい。 HDLは変わった言語で、一見ふつうのプログラミング言語に見えるのだが、実態はかなり違う。 以下では、僕の(不正確で浅い)理解に基いてHDLを説明しようと思う。後で大幅に書き換えるかも。

例として、さっき電圧がぐるっと周回していた、1bit反転CPUの製作キットを秋葉原で買ったとする。箱を開けると以下の3パーツが詰まっていた。

f:id:kaitou_ryaku:20171203034131p:plain:w400

内容物は以下の3つだ

これらの出口を入口に繋いでCPUを組み立てる。 クロックは後で買うとして、組立説明書を見ると

f:id:kaitou_ryaku:20171203034140p:plain:w600

左図では、aの出口をinvの入口に繋ぎ、invの出口をbの入口に繋いでいる。 また説明文にある通り、aの出口の電圧が変われば、その変化はinvを抜けてワイヤーのbまで一瞬で伝わる。 こういう線の繋ぎalways_combというらしい。

右図では、bの出口をaの入口に繋いでいる。 説明文にある通り、bの電圧が変わっても、Dフリップフロップaの出口電圧はすぐには変わらない。 こういう線の繋ぎalways_ffというらしい。

ここまでの話をまとめると、線の繋ぎ方は2パターンあることが分かった。 電圧が即座に反映されるような繋ぎ(always_comb)と、クロック立上りの瞬間だけ電圧が伝わるような繋ぎ(always_ff)の2種類だ。

そんなこんなで製作キットが完成した。

f:id:kaitou_ryaku:20171203034154p:plain:w400

ハードウェア記述言語(HDL)

唐突だが、今の1bit反転CPUをHDL(Hardware Description Language)のSystem Verilogという言語で記述してみる

module cpu( input CLOCK);

  logic a;    // 素子aを用意
  logic b;    // 素子bを用意

  always_comb // 電圧変化が即座に伝わる繋ぎを書く
    b = ~a;   // bの入口に、aの出口を反転して繋いだ
              // bの出口は即座に電圧が変わる。
              // つまりbはワイヤーだ

  always_ff @(posedge CLOCK) // 電圧変化がクロック立上りの瞬間だけ伝わる繋ぎを書く
    a <= b;   // aの入口に、bの出口を繋いだ
              // aの出口はクロック立上りの瞬間だけ電圧が変わる。
              // つまりaはDフリップフロップだ

endmodule

要するにHDL (System Verilog)は

  1. logic宣言で素子をたくさん用意する
  2. 素子間の繋ぎをalways_combalways_ffの2パターンに分ける
  3. =<=を用いて、素子の出口を入口に繋ぐ

という言語だ(と僕は思う)。

ちなみにHDLコードの1行目にinput CLOCKという引数がある。 これはmoduleからendmoduleの中に、外部からCLOCKという線を引っ張ってくるという意味だ。 CLOCKは多くの場合にalways_ffの条件指定子(いつ電圧が伝わるか)的な役割を担うことになる。

またHDLコードをよく見ると

  • always_combの中では=を使って線を繋ぐ
  • always_ffの中では<=を使って線を繋ぐ

となっている。この辺の記号の使い分けはややこしいので、詳細は明日以降に回す。 要するにイコールっぽい記号で線を繋ぐのだなと思って欲しい。

HDLはプログラミング言語ではない

HDLは回路図の絵を描く言語だ。極論すると、有向グラフの絵を描くための言語といえる。 決してプログラミング言語ではないので、注意しないといけない。

たとえばさっきのコードにa <= bがあったけど、これはプログラミング言語の代入文とは全く違う。 つまり変数aに変数bの値を代入するのではない。 素子aの入口に素子bの出口を繋いだだけだ。

最初に1bit不変CPUを導入したとき、CPUは代入と計算を繰り返すと説明したが、それは

  1. HDLに従って回路素子を用意し、出口を入口に繋ぐ
  2. 線をつなぎ終えた後の回路(1bit反転CPU)に電源を入れて、電圧変動をじーっと眺める
  3. フリップフロップ周辺の電圧の時間変化が、プログラミング言語における代入命令のように見える
  4. NOTゲート周辺の電圧の時間変化が、プログラミング言語における計算命令のように見える

つまり記事の最初の方に出てきた 代入→計算→代入→計算→... の代入は、回路が完成した後の動作の様子を言語化したものだ。 一方、HDLのa <= bは回路そのものの作り方(線の繋ぎ方)を表している。 これらを混同してはいけない。

言い換えると、HDLは回路の機能を記述する言語では無いのだ。 もちろん何らかの機能を実現するために回路を作るわけだが、その際はまず自分の頭で機能を回路図に変換し、次に回路図をHDLで記述するという手続きを踏むことになる。 今回の場合は

  1. 「代入できる変数が欲しい。そこにbit反転して代入する機能も欲しい」という欲求があった
  2. その機能は、Dフリップフロップのループ回路で実現できると考えついた
  3. そのループ回路をHDLで記述した

という風に僕は考えている1

どうでもいい話

僕にはHDLを解説する力量が無い。今も、見よう見まねで適当に書いている。

HDL言語の見た目は、プログラミング言語そっくりだ。 if文, for文, case文, 四則演算、代入(のような操作)、変数宣言(のような操作)などもある。親しみ深いだろう。 見た目はとっつきやすい。しかし実際は、、、

恥を忍んで、HDLを触り始めたころの僕と、HDLに詳しい人との実録会話をメモしておく

「HDLはどこから処理が始まるんや?main関数はどれや?」

「質問が的外れ。HDLは回路の満たす性質を列挙するものであり、開始やリターン文といった概念は無い」

always_combとかalways_ffとか意味不明なんやが」

「線を繋ぎ終えた後に満たして欲しい状況を記述する。それを状態と遷移に分ける。前者はalways_comb、後者はalways_ffに書く。簡単なことだ」

always_ffの中でLEDに1を代入したのに光らんかった」

always_ff内でワイヤーに線を繋ぐとエラーが出るのは当然だろう。クロック立ち上がり以外で右辺が変化すると破綻する。あと代入という言葉を使うのはやめろ」

logic a;で変数aを宣言して代入するとき...」

「それはフリップフロップであり変数ではない。そして代入より線を繋ぐという表現の方が適切である」

「for文でレジスタに入ってる値を増やして、LEDに繋いでチカチカさせたいんやが」

「for文は貴様の想像しているものとは違う。あれはコピペ作業を簡略化するための構文である。」

FPGAからPCにUARTで情報を送りたいときは、01の列を送って、情報を送り終えたらNULLにしといたらええんやな?」

「電圧は0か1しかない。NULLなどない。情報を送らない時は常に0を送り続ける。情報を送りたい時は1を何度か送ってセッションを開始する」

コンパイルに時間かかりすぎてクソ辛いんやが」

「論理合成やインプリメントはNP困難な問題や四色問題が絡むので、時間がかかって当然」

「なんかいろいろうまくいかんのやが」

「どういう回路図になるか想像しながらHDLを書いてるか?」

「いやぜんぜん」

「HDLはハードウェアを記述する言語だ。どういう回路を作りたいか考えてないのに、それを記述するのは無理にきまっているだろう」


  1. 初心者が勝手に考えているだけなので、あまり信用してはいけない。