CPUよもやま話(機械語の正体)
前回は、マルチプレクサに繋がる7つの配線を、4ビットのオペコードとフラグでどうやって操るの?ってところで終わりました。
この?の部分には、任意のオペコードとフラグが入力されれば、それを解読して必要な信号を出力する装置を入れる必要があります。例えば0001が入力されれば0111001が出力される、といったように。
このように圧縮・暗号化された符号を解読するための装置をデコーダといいます。特にこの場合は、圧縮された命令を解読するためのものなので命令デコーダといいます。
反対に、実際の信号をある規則に従って圧縮・暗号化することをエンコードといいます。多くの配線への信号を、より少ないビットの組み合わせに圧縮したもの。これが、機械語です。
機械語は回路設計者が決定します。とりあえず、LTD4では下のようにしてみました。(アセンブリは動作をイメージしやすくするための参考記載です)
さて、この一覧表どおりに出力される命令デコーダを作る必要があります。
Logisimで命令デコーダを自動生成してみる
Logisimには真理値表から論理回路を自動生成する便利な機能があります。せっかくなのでこれを使ってみたいと思います。
まず、ProjectからAnalyze Circuitを選択します。
次に、Inputsダブで入力ピンの名前を入力し、一覧に追加します。
同様に、Outputsタブで出力ピンの名前を入力し、一覧に追加します。
Tableタブで真理値表を作成します。入力側の組み合わせに応じて、出力側を1、0、X(どちらでもよい)のいずれかに設定します。
なお、入力側は変更できません。X(どちらでもよい)といった指定もできないので、Xの場合は0と1のどちらでも結果が同じになるように真理値表を作成する必要があります。
真理値表を埋めたら、Build Circuitを押します。その後、表示されるダイアログボックスで回路名を入力して了解を押すと、回路が自動生成されます。
で、出来上がった回路がこちら
やけに、縦長じゃないか?( ̄q ̄;)
とりあえずこれが最後のピースなので、?の部分に当てはめます。(入力と出力の向きが逆なので180度回転させます)
これで完成!
とりあえず動かしてみる
特に面白いプログラムを思いつかなかったので(←ぇ)、無意味にループさせて10回目でループを抜けるというのをやってみます。
高水準言語的に書くと
- For i = 1 To 10
- Next
アセンブリ言語で書くと
- MOV B,6 '比較用の6をレジスタBに転送
- ADD A,1 'レジスタAに1を加算した結果をレジスタAに転送
- ADD B,A 'レジスタAとレジスタBを加算
- JNC 0 '直前の加算結果が15以下なら0番地にジャンプ
- JMP 4 '4番地に無条件ジャンプ(つまりこの行で永久ループ)
機械語で書くと
- 0100 0110 [46]
- 0111 0001 [71]
- 0110 0000 [60]
- 1011 0000 [B0]
- 1001 0100 [94]
- ※カッコ内は16進数
です。
これをメモリに0番地から順番に記憶させておいて、0番地から実行させてみたいと思います。(クロックは4Hz)
すごく地味!
まとめ
普段使っているPCも、ビット数・レジスタ数などが違うだけで根本的には内部でこんなことをやっています(1秒間に数十億回という速さですが)。
果てしなく続くオープンワールドのゲームでも、Excelでも、YouTubeの再生でも、人工知能でも、ビット単位で見ればこんなもんです。これらはすべて、根本的な部分は変わらず、ハードウェアの高性能化とソフトウェアの工夫で実現されています。
・・・・・・っていう、どの立場から語っているんだよ!的な、よもやま話でした。
CPUよもやま話(マルチプレクサで転送先と転送元を切り替える)
前回までの記事に登場したモノをすべて繋いでCPUの形にしていきたいと思います。しかし「レジスタとクロック」の回でも書いたように、ただ単純に繋ぐだけでは回路が固定であり、処理内容が永遠に同じものになってしまいます。命令によって回路を切り替えできるようにしなければダメです。
ということで、マルチプレクサにご登場いただきましょう。
マルチプレクサの動作を見てみる
マルチプレクサとはふたつ以上の入力をひとつの信号として出力するためのモノです。文章で説明するよりも、下の図を見てもらったほうが手っ取り早いです。
2チャンネルマルチプレクサ
4チャンネルマルチプレクサ
このように、選択信号の組み合わせによって回路を切り替えることができるのがマルチプレクサです。選択信号=命令です。鉄道の線路がダイヤによって切り替わり、車両の進路を変更するようなもんです(たぶん)
上の図はあくまで概念図です。実際は複数の論理ゲートで構成されています。選択信号のビット数が増えれば、扱えるチャンネルが8、16、・・・・・・というふうに増えます。
転送先を切り替える(レジスタの自己保持・外部データ読み込みを切り替える)
下は、2チャンネルマルチプレクサを使ってレジスタの自己保持・外部データ読み込みを切り替えできるようにしたものです。(MUXはマルチプレクサの略)
~Loadが1なら、クロックの立ち上がりでOutputの値を自己保持
~Loadが0なら、クロックの立ち上がりでDataの値を読み込む
つまり、転送元となる何処かからのデータをレジスタに転送するのなら、該当レジスタの~Loadに0を与え、何もしないのなら~Loadに1を与えます。
転送元を切り替える(ALUの入力を切り替える)
次に、ALUの入力を4チャンネルマルチプレクサ2個で切り替えできるようにします。
例えば、図のマルチプレクサに下の選択信号を与えると、レジスタAとレジスタBの値を加算することができます。
SelectAに0
SelectBに0
SelectCに1
SelectDに0
ALUの出力は各レジスタのDataへ戻すので、このマルチプレクサ2個で実質、転送元の切り替えをしていることになります。(ちなみに4チャンネル目は不使用。GND〔接地記号のアレ〕と、即値をどう使うかは後で説明します)
4ビットCPUの形になるように繋いでみる
ここまでを踏まえ、前回までの記事に登場したモノ。つまり、加算器・メモリ・レジスタ・クロック・Cフラグ・プログラムカウンタをマルチプレクサで切り替えできるように繋いでいきます。
※4ビット/配線(ただし、制御信号とフラグは 1ビット/配線)
※2チャンネルマルチプレクサはレジスタの中に隠した
書籍「CPUの創りかた」を参考にしたので、著者に敬意を表し、この回路をLTD4(Logisimで創ったとりあえず動作する4ビットCPU)と名付けます!(←あまり重要ではない)
処理の内容によって、信号の与え方とデータの流れがどうなるのか、次にまとめてみました。
MOV A,B を行う場合
説明:レジスタBの値をレジスタAに転送する。
ポイント:レジスタの値は必ずALUを通過するので、もう片方は即値(0000)となるようにする。
MOV B,A を行う場合
説明:レジスタAの値をレジスタBに転送する。
ポイント:レジスタの値は必ずALUを通過するので、もう片方は即値(0000)となるようにする。
MOV A,即値 を行う場合
説明:即値をレジスタAに転送する。
ポイント:即値は必ずALUを通過するので、もう片方はGND(つまり0000)となるようにする。
MOV B,即値 を行う場合
説明:即値をレジスタBに転送する。
ポイント:即値は必ずALUを通過するので、もう片方はGND(つまり0000)となるようにする。
ADD A,B を行う場合
説明:レジスタAの値とレジスタBの値を加算して、レジスタAに転送する。
ポイント:即値は使わないので0000にしておく。
ADD B,A を行う場合
説明:レジスタAの値とレジスタBの値を加算して、レジスタBに転送する。
ポイント:即値は使わないので0000にしておく。
ADD A,即値 を行う場合
説明:レジスタAの値と即値を加算して、レジスタAに転送する。
ポイント:即値は任意の値にしておく。
ADD B,即値 を行う場合
説明:レジスタBの値と即値を加算して、レジスタBに転送する。
ポイント:即値は任意の値にしておく。
JMP 即値 を行う場合
説明:Cフラグとは無関係に即値をプログラムカウンタに転送する。
ポイント:即値は必ずALUを通過するので、もう片方はGND(つまり0000)となるようにする。
JC 即値 を行う場合
説明:Cフラグが1の時のみ即値をプログラムカウンタに転送する。
ポイント:即値は必ずALUを通過するので、もう片方はGND(つまり0000)となるようにする。
JNC 即値 を行う場合
説明:Cフラグが0の時のみ即値をプログラムカウンタに転送する。
ポイント:即値は必ずALUを通過するので、もう片方はGND(つまり0000)となるようにする。
プログラムからマルチプレクサを切り替えできるようにしたい
とりあえず、各処理とフラグの状態とマルチプレクサに与える信号を一覧にしたものがこちら。
さあ、ここまでくれば、あとはメモリから取り出したプログラム(8ビット)から上位4ビットを使ってマルチプレクサを切り替えできるようにすればいいだけです。Cフラグも使わないとね。
あれ?マルチプレクサへの信号は7つ必要なのにオペコードは4ビットしかない・・・・・・どうすんのコレ?(@_@;)
ということで次回に続きます。
CPUよもやま話(プログラムカウンタのお仕事)
今回は、プログラムカウンタが動作する様子を見てみましょう。
プログラムカウンタのお仕事は、実行すべきプログラムが格納されているメモリアドレスを指し示すことです。もう少し具体的にいうと、プログラムの開始位置となるアドレスから始まり、クロックに合わせてカウントアップしていく・・・・・・という簡単なお仕事です。1命令が1アドレスに収まっているなら1ずつカウントアップし、1命令が2アドレスにまたがるなら2ずつカウントアップする・・・・・・、といった具合にです。
前回、レジスタについて触れましたが、レジスタにカウントアップ機能を付加したものだと思えばよいと思います。
- ~Loadが1なら、クロックの立ち上がりでOutputの値を1つカウントアップ
- ~Loadが0なら、カウントアップを止め、Dataの値を読み込む
- 再び~Loadが1になれば、読み込んだDataの値からカウントアップしていく
勘のいい人ならピンと来たかもしれませんが、Dataからはプログラムの開始位置となるアドレスやジャンプ先のアドレスが読み込まれることになります。
※本記事中のプログラムカウンタはすべて0000番地からスタートするものとします。
では、上の回路を部品化して、メモリに繋いでみましょう。
このように、プログラムカウンタのOutputをメモリのアドレスバスに接続すれば、該当のアドレスに格納されたプログラムがデータバスから出力されます。(プログラムカウンタが4ビットなので、1111番地の次に0000番地に戻るのは無視して下さい)
しかし、これだけでは面白くないので、好きなタイミングで指定アドレスにジャンプできるような回路を追加してみたいと思います。
追加する回路をアセンブリ言語で表現するならば
- JMP 即値 '即値をプログラムカウンタにセット
です。
即値(すなわちジャンプ先アドレス)に0000を指定しておき、プログラムカウンタが1000まで進んだところで、手動でJMP命令(すなわち~Loadを0にする)を与えてみます。
現在アドレスが1000から0000に変わり、再び0000からカウントを開始していることが分かると思います。
でも、手動で命令を出すなんてのは原始的な発想ですね( ̄q ̄;)。ここは、プログラムから自動的にJMP命令を与えるようにしてみましょう。
っと、その前にプログラムのフォーマットについて説明しとかないと意味が繋がらなくなってしまいます。CPUによって異なりますが、本回路では1命令を8ビットとし、下のような割り振りにしたいと思います。
オペコードはMOV A,BとかJMPとかのアレで、即値はプログラム中に直接書く値です。
メモリの各アドレスには、下のようなプログラムが事前に格納してあるものとします。
んで、メモリのデータバスからプログラムカウンタへ向けて、最上位ビットを~Loadに接続し、下位4ビットをDataに接続します(複数ビット・単数ビットのいずれも単線で表現されているので注意して下さい)。
これにクロックを与えて動作させると
0000番地から1000番地を無限ループします。
1000番地に格納された、01110000というプログラムはアセンブリ言語で表現するならば、
- JMP 0 '0000をプログラムカウンタにセット
です。この回路の場合、オペコードの最上位ビットが0になれば、プログラムカウンタに即値の0000がロードされるので、プログラムは0000番地に戻るという訳です。
上位4ビットのうち残りの3ビットは配線がどこにも繋がっていないので、この場合は無意味です。つまり、この回路で実行できる命令はJMP(オペコード 0XXX)のみです。
次回は、MOVやADDなどの命令も実行できるように配線を繋いでいきたいと思います。
CPUよもやま話(レジスタとクロック)
前回までに「レジスタ」という単語が何回か出てきましたが、レジスタが何者なのかについてはほとんど書いていなかったので、ここらで書いておこうと思います。
なぜCPUの中にレジスタが必要なのか?
CPUの中にはデータを一旦保持するためのレジスタがいくつか入っています。で、なぜこのレジスタが必要なのかということなんですが・・・・・・料理にでも例えてみようと思います。
料理をする際は、食材をまな板の上に置いて包丁で切ったりすると思います(空中でも出来なくはないですが、置く場所がないととても不便です)。
食材は順次、冷蔵庫から取り出します。が、冷蔵庫に無いものはスーパーに買いに行くことになります。
ということで、こんな感じで例えてみました。
- 食材:データ
- まな板:レジスタ
- 冷蔵庫:メモリ
- スーパー:HDD
出来上がった料理は冷蔵庫に入れることもできるし、冷蔵庫からスーパーに売りに行くこともできます。・・・・・・って、そんなスーパーあるかい(@_@;)
この例え話なら
- スーパー:CD-ROMまたはDVD-ROM
のほうがしっくりくるかもしれない。どっちにしても分かりにくかった人は無視して下さい。
レジスタの正体はDフリップフロップ
先ほども書きましたが、レジスタはデータを一旦保持するためのものです。データを保持するもので思い出されるのはフリップフロップですが、レジスタにはDフリップフロップというものが使われていたりします。
Dフリップフロップは、クロックが立ち上がった瞬間の入力Dを出力Qに保持(次にクロックが立ち上がるまで)します。
真理値表
クロックに関係なく出力Qを保持したいのなら、下のように出力Qを入力Dへ直結します。
Dフリップフロップを、必要なbit数並べたものがレジスタです。
ADD A,B はどう実現するのか?
前回、アセンブリ言語について少し触れましたが、例えば ADD A,B のような処理はどのように行われるのでしょうか?
ADD A,B の処理内容は
です。
回路図で書いてみましょう。(仮に、4bitCPUとする)
このように、レジスタA・レジスタBの出力を加算器の入力に接続し、加算器の出力をレジスタAの入力に戻します。レジスタBは値を保持するように、出力を入力に戻します。
次に動作を実験してみます。あらかじめ、レジスタAに1010、レジスタBに0101が保持されている状態から
ADD A,B
ADD A,B
を(つまり、ADD A,Bを2回)、Logisimでシミュレートしてみます。目で追えるように、クロックを0.25Hzで動作させるので刮目してご覧下さい。
解説
- 1行目のADD A,Bで1010+0101=1111となり、レジスタAの入力側に戻ります。
- 次のクロックで1111がレジスタAに保持されます。同時に、2行目のADD A,Bが実行されます。1111+0101=10100となるため、加算器の4ビット目から桁上がりが出力されています。
- 次のクロックで、0100(オーバーフローした出鱈目な値)がレジスタAに保持され、桁上がりはCフラグとして保持されます。
Cフラグを保持するのもDフリップフロップなので、クロックの立ち上がりで値を保持します。このフラグを参照して条件分岐させるなら、目的のフラグが保持されたクロックの立ち上がり(先のソースコードであれば直後の3行目)で実行する必要があります。クロックが進めば目的のフラグではなくなってしまうからです。
さて、少し脱線しましたが、これでADD A,Bを実行できる回路が分かりました。しかし、これでは回路が固定であるため、永遠にADD A,BしかできないCPUになってしまっています。
なので、転送元と転送先を切り替えることができる回路を追加しなければなりません。さらに言えば、その切り替えをプログラムで行えるようにする必要がある訳です。
(例えば、下のような命令ごとに回路を繋ぎかえできるようにしなければならない)
MOV A,B レジスタAのデータをレジスタBに転送
MOV B,A レジスタBのデータをレジスタAに転送
ADD A,B レジスタAとレジスタBの値を加算してから、レジスタAに転送
ADD B,A レジスタAとレジスタBの値を加算してから、レジスタBに転送
MOV A,A レジスタAのデータをレジスタAに転送(要するに自己保持)
MOV B,B レジスタBのデータをレジスタBに転送(要するに自己保持)
Dフリップフロップの中身
Dフリップフロップの実際の内部回路は複雑なので、単純化した図で表します。(本の受け売り)
クロックが0の時は、前段と後段では縁が切れ、後段だけで出力を自己保持しています。入力に変化があっても出力は保持されます。
クロックが1の時は、前段と後段が繋がります。前段で自己保持した出力を後段は素通しします。前段と入力とは縁が切れているので、入力に変化があっても出力は保持されます。
ここで、素朴な疑問・・・・・・。クロックが1になる瞬間の入力を出力に保持する訳ですが、クロックが1になると同時に(加算器などから)逆の値が入力にフィードバックされたらどうなるでしょう?1だと思ってたけどやっぱり0で・・・・・・、でもそれなら逆の逆の値がフィードバックされて・・・・・・堂々巡り?( ̄q ̄;)
という間抜けな考えをいだくのは私だけだと思いますが、この心配は要りません。
Dフリップフロップの入力から出力には遅延があり、さらに加算器の入力から出力にも遅延があり、さらに言えば電子が配線の中を移動する時間もあるので、厳密には同時ではありません。クロックの立ち上がりから少し遅れて、入力へのフィードバックがあると思えば良いのです。この時間は、人間の感覚からすれば限りなくゼロに近いけれど、ゼロではないということですね。
CPUよもやま話(アセンブリ言語で命令のイメージを掴もう)
今回は、CPUに対しての命令がどのように行われるのか見てみましょう。
アセンブリ言語とは?
皆さんご存知のとおりコンピューターの中で情報は、0と1で表されます。0と1で表現されたプログラムは機械語と呼ばれます。しかし、命令の内容が0と1の羅列では人間にはかなり分かりにくいものです。
そんなとき、アセンブリ言語を用いて説明すると比較的分かりやすくすることができます。何故かというと、アセンブリ言語のソースコードの1行は機械語の1命令に相当し、かつ機械語の内容を(人間が理解・記述しやすいように簡略化した英単語や記号の組み合わせで)そのまま表現しているからです。
例えば、こんなのがあります。
MOV A,B
これは、レジスタBのデータをレジスタAに転送せよ という命令です。
オペランドにはレジスタだけではなく、メモリ→レジスタ、レジスタ→メモリ、即値→レジスタ・・・・・・等とすることもできます。(CPUがそのような命令を実装していることが前提)
※即値とはコード中に直接書かれた値のことです。イミディエイトデータとも呼びます。
他にも、こんなのがあります。
ADD A,B レジスタAとレジスタBの値を加算して、レジスタAに転送
NOT A レジスタAの値を論理否定して、レジスタAに転送
JMP [番地] 指定番地へ無条件でジャンプ
IN A I/Oの入力ポートのデータをAレジスタに転送
OUT A Aレジスタの値をI/Oの出力ポートに転送
等々。ただし、繰り返しになりますがCPUがそのような命令を実装していれば、です(例えば、MOVでメモリからメモリへ転送したくても、それに該当する機械語〔回路〕が存在しなければ実行することはできない)。次回以降、機械語の正体を説明する際に、ポイントになるので軽く覚えておいて下さい。
アセンブリ言語で書かれたソースコードは、アセンブラというソフトウェアで機械語に変換して実行します。(今回はイメージを掴むためにアセンブリ言語を持ち出しているだけなので、変換うんぬんは気にしなくてもよいです)
さて、命令がどのようにCPUで実行されているか、高水準言語とアセンブリ言語を比較してイメージを掴んでみましょう。今回は扱う命令やデータがすべて1バイト(8bit)であるものとします。
順次進行の仕組み(プログラムカウンタとは?)
基本的にプログラムはメモリの小さい番地から大きい番地へ順に実行されます。これをプログラムの基本三構造のうち順次進行と呼びます。プログラムカウンタには、次に実行される命令が入ったメモリ番地が格納されています。まず、プログラムの開始位置(メモリ番地)がセットされ、順次、カウントアップしていきます。命令は、プログラムカウンタの値を参照してメモリから命令レジスタに取り出されるという寸法です。
試しに算術演算を順次進行で行ってみましょう。
(高水準言語で表現した場合)
a = 10
b = 6
c = a + b
(アセンブリ言語で表現した場合)
MOV [0],10 即値の10をメモリの0番地に転送
MOV [1],6 即値の6をメモリの1番地に転送
MOV A,[0] メモリの0番地の値をAレジスタに転送
MOV B,[1] メモリの1番地の値をBレジスタに転送
ADD A,B レジスタAとレジスタBの値を加算して、レジスタAに転送
MOV [n],A Aレジスタの値をメモリのn番地に転送
END ソースコードの終わり(OSに戻る)
※大カッコで囲んだ数値はメモリの番地として扱われます。(大カッコがなければ即値として扱われます)
※メモリの番地は説明のために適当に割り当てたものです。
高水準言語であればコンパクトに書けることを、アセンブリ言語では長々とした命令になってしまいます。アセンブリ言語(≒機械語)で複数の命令に渡る処理を、人間に分かりやすく・抽象的に記述できるようにしたものが高水準言語です。って、説明不要ですね(@_@;)
繰り返しの仕組み
次に、基本三構造のうちの繰り返しをやってみましょう。アセンブリ言語でJMPのオペコードを使うと、プログラムカウンタの値を強制的に変更し、プログラムの流れを変更することができます。試しに先の順次進行のコードを無限ループさせてみましょう(処理の内容に意味はありません)。
(高水準言語で表現した場合)
jp:
a = 10
b = 6
c = a + b
GoTo jp
(アセンブリ言語で表現した場合)
MOV [0],10 即値の10をメモリの0番地に転送
MOV [1],6 即値の6をメモリの1番地に転送
MOV A,[0] メモリの0番地の値をAレジスタに転送
MOV B,[1] メモリの1番地の値をBレジスタに転送
ADD A,B レジスタAとレジスタBの値を加算して、レジスタAに転送
MOV [n],A Aレジスタの値をメモリのn番地に転送
JMP [2] 指定アドレスをプログラムカウンタにセット
END ソースコードの終わり(OSに戻る)
条件分岐の仕組み
最後に、基本三構造のうちの条件分岐をやってみましょう。レジスタAとBの加算結果が15を超えるのなら、何もせずに処理を終了します。
(高水準言語で表現した場合)
a = 10
b = 6
If a + b <= 15 Then
c = a + b
End If
(アセンブリ言語で表現した場合)
MOV [0],10 即値の10をメモリの0番地に転送
MOV [1],6 即値の6をメモリの1番地に転送
MOV A,[0] メモリの0番地の値をAレジスタに転送
MOV B,[1] メモリの1番地の値をBレジスタに転送
ADD A,B レジスタAとレジスタBの値を加算して、レジスタAに転送
JC [9] キャリーフラグが1なら指定アドレスをプログラムカウンタにセット
MOV [n],A Aレジスタの値をメモリのn番地に転送
END ソースコードの終わり(OSに戻る)
前々回の加算器を思い出して下さい。ADD A,Bは、10(1010)+6(0110)=16(10000)で、4ビット目が桁上がりするので、これをキャリーフラグにセットしています。
これを応用して、例えばレジスタAがn以上なら条件分岐 という処理を次のようにするができます。
ADD A,9 レジスタAの値に(16-n)の値を加算する(この場合、7以上でキャリーが発生する)
JC [番地] キャリーフラグが1なら指定アドレスをプログラムカウンタにセット
JCは(Jump if Carryの略)はキャリーフラグが1ならジャンプします。逆にキャリーフラグが0ならジャンプするJNC(Jump if Not Carry)というのもあります。
参照するフラグレジスタの種類によって、他にもいろいろな条件付ジャンプがあります。
回数指定のループも条件付ジャンプで作ることができます。やり方は簡単なので、ぜひ各自で考えてみて下さいね!(←手抜き)
記事を書くにあたって、以下の書籍を参考にしています。
- 作者: 渡波郁
- 出版社/メーカー: 毎日コミュニケーションズ
- 発売日: 2003/10/01
- メディア: 単行本(ソフトカバー)
- 購入: 35人 クリック: 445回
- この商品を含むブログ (193件) を見る
プログラムはなぜ動くのか 第2版 知っておきたいプログラムの基礎知識
- 作者: 矢沢久雄
- 出版社/メーカー: 日経ソフトウエア
- 発売日: 2007/04
- メディア: 単行本(ソフトカバー)
- 購入: 45人 クリック: 646回
- この商品を含むブログ (74件) を見る
なお、今回使用したアセンブリ言語は、あくまで説明用の架空のものです。
CPUよもやま話(情報を書き込む・記憶する・読み出す)
今回は、情報がどのようにして記憶(メモリー)されているのかを調べてみたいと思います。
皆さんご存知のとおりコンピューターの中で情報は、0と1で表されます。コンピューターは電気回路なので、電圧が低い状態を0、電圧が高い状態を1としています(回路設計者のさじ加減で逆になることもありますが)。
メインメモリーなどでは、電源が供給されている限りは、書き込まれた情報を記憶していますが、どのようにして0と1の状態を記憶しているのでしょうか?
フリップフロップとは?
下の回路を見て下さい。これはフリップフロップと呼ばれるのもです(正確にはRS型フリップフロップです。専門家の方には当たり前過ぎるシロモノですが・・・・・・)
S(Set)に入力された情報がQとして出力され、入力が無くなっても自己保持するという性質の回路です。自己保持されたQの出力を解除するには、R(Reset)に入力を与えます。
真理値表
表中の「禁止」はそのような使い方をすると壊れる、という意味ではなく、通常そのような使い方をしないという意味です(意図的に使う場面もあるらしいですが)。
もうお気づきだと思いますが、この回路一つで1bitの情報を記憶することができます。
このように1bitの情報を記憶するための回路構成をメモリーセルと呼びます。
言い換えれば、1bitの情報を記憶することができるのであれば、必ずしもフリップフロップである必要もないということです。
(例えば、コンデンサの充電/放電を、1/0として扱うなど)
ただし、今回は基本的(原始的)でかつ分かりやすいフリップフロップを用いて話を進めていきたいと思います。
1bitの情報を記憶する回路を理解できたのであれば、あとはそれを必要なbit数並べればメインメモリーができます。
なんて簡単!
メインメモリっぽいものを作ってみる
メインメモリーは、一般的には1バイト(8bit)ずつに区切って並べられ、区切りごとにアドレス(番地)が割り振られています。
先ほど作ったフリップフロップを部品化(図中のFF)して、Logisimでメインメモリっぽいものを作ってみましょう。
随分ごちゃごちゃしてますね。メモリーセルが縦一列に1バイト分並んでいるので、4GBのSRAMを作るなら、これを約40億列並べる必要がある訳です。
配線は、縦にアドレスバスが共用されており、横にデータバスが共用されています。このように配線したものをマトリクスと呼びます。
配線を共用せずに、各bitごとに配線を引き出すと、8×40億の端子が必要になるので、この方式にたどり着くのはごく自然な流れですね・・・・・・。
実際の製品では書込側と読出側の端子も共用されています。(書込みモード/読出しモードを切り替えて使用する)
回路は若干(大層?)オリジナルな部分がありますが、あくまで原理を説明するためのモデルなので我慢して下さい。
実験として、0番地を指定して01010101を書込み、次に1番地を指定して10101010を書込み、最後にn番地に11111111を書込みします。書込みが終わったら、読出しモードにして0番地を出力、次に1番地を出力、最後にn番地を出力してみます。
見づらいかもしれませんが、FFの右上(出力Q)が明るい緑色であれば、1を記憶している状態です。(0に該当する出力が、ところどころE〔Error:信号がHでもLでもない状態〕になっていますがそこは無視して下さい。)
本当ならアドレスは二進数(00000000~FFFFFFFF等)で指定されるので、これを各アドレスバスへスイッチングする回路も搭載する必要があります。
CPUの片鱗を見てみる
さて、メモリーの原理っぽいものが理解できたところで、最後にメモリーの情報を読み出してCPU内で演算し、結果をメモリーに書き込むという流れを見て今回は終わりたいと思います。
まず、何らかのプログラム言語で
a=85
b=170
c=a+b
と、記述されたものを実行した時、CPUとメモリーの間ではどのようなことが行われるのでしょうか?(変数の型は仮に1バイトとする)
次の図を見て下さい。
※変数aは0番地、変数bは1番地、変数cはn番地に割り当てられたものとします。これは説明のために適当に割り当てたものです。
まず、85と170の二進数値がそれぞれの番地に格納されます。次にこの85と170がレジスタに転送され、ALUで加算が行われます。加算結果は255となり、これがレジスタに上書きされ、その後、メモリーの該当番地に格納されます。(どのレジスタがどのように使用されるかはコンパイラの気分次第です)
見たまんまなので説明の必要はなかったかもしれませんが、ここでレジスタという謎の人物が登場してきましたね(これも中身はフリップフロップです。詳細は次回以降に)。
CPUの片鱗が少しだけ見えた気がしますが、実はまだまだ謎の部分もあります。データの読み書きや、演算の命令は誰が(何が)出すのか?これらはどのようなタイミングで実行されていくのか?などなど。
次回以降、その辺りの謎の部分も少しずつ紐解いていきたいと思います。
CPUよもやま話(論理ゲートを使って加算器を作ってみよう)
前回はトランジスタを利用した基本的な論理ゲートであるAND、OR、NOTの作り方を紹介しました。今回は、その論理ゲートをさらに組み合わせて加算器なるものを作ってみましょう。専門家の方には初歩的過ぎるかもしれませんが、そのあたりはご容赦下さい。
その前に、加算器ってなーに?って言う人もいるかもしれないので簡単に説明しておきます。
加算器とは、二進数と二進数を足し算(算術的加算)して、二進数で計算結果を出力するための回路です。例えば、0101と0101を入力として与えたなら、1010を出力します。(10進数での、5+5=10と同義)
よく混同されがちなのが、論理演算です。論理演算は入力に対して真(1)か偽(0)を出力するだけなので算術的意味はありません。具体的に論理和と算術的加算の違いを見てみましょう。
まずは、論理和から
次に、算術的加算
このように、結果は違います。
しかし両者はまったく1ミリも無関係なのではなく、算術的演算は、論理演算を応用してテクニカルに実装されているのです。
?(゜ρ゜)?
安心して下さい、その辺を順を追って説明してみようと思います。
半加算器とは?
まずは桁上がりをどう実装するか?を考えなくてはいけません。
そこで考案されたのが、この半加算器です。(AとBは同じ桁の入力、Sはその桁の合計値出力、Cは桁上がり出力です)
真理値表
AとBが1ならばこの桁は0になり、桁上がりが発生するというものです。論理回路をシミュレートできる「Logisim」を使って真理値表どおりに動作させてみたので、見比べてみて下さい。
繰り返しになりますが、 AとBの両方が1であった場合Sが0、Cが1になるようになっています。つまりその桁の算術的加算をこのようなAND、OR、NOTの組み合わせで実現することができます。(XORを使う場合もあり)
全加算器とは?
さて、半加算器では、上位桁への桁上がり出力はできるものの、下位桁からの桁上がりを受け付ける回路がありません。そこで考案されたのが、半加算器2つとORを組み合わせて作られた全加算器です。(AとBは同じ桁の入力、Xは下位桁からの桁上がり入力、Sはその桁の合計値出力、Cは桁上がり出力です)
真理値表
Logisimのアニメーションと真理値表を見比べてみて下さい。
このように下位桁からの桁上がりを含めた算術的加算を、半加算器2個とORで実現しています。
4ビットの加算器を作ってみる
勘のいい人なら半加算器と全加算器を組み合わせて、任意ビット(桁)の加算器を作れることに気付いたかもしれません。
最下位ビットには下位ビットからの桁上がり入力がない半加算器を置き、上位ビットには残り必要な個数の全加算器を置きます。半加算器の桁上がり出力(C)を次のビットの全加算器の桁上がり入力(X)に接続し、さらにその全加算器の桁上がり出力(C)を次のビットの全加算器の桁上がり入力(X)に接続し・・・・・・というふうに半加算器1個+全加算器3個を組み合わせれば4ビットの加算器になります。回路図中のHA(Half adder)は半加算器、FA(Full adder)は全加算器を表します。どちらも先ほどLogisimで作った半加算器と全加算器を部品化してLogisimで再利用したものです。(Logisimは一枚絵で回路を作るだけでなく、別途作成した回路を部品化して別の回路で再利用することができます。さらに再利用したものを含む回路をも部品化して別の回路で再利用できる・・・・・・プログラミングで言えばクラスとクラスの継承のような使い方ができるので便利です!)
入力Bを0101で固定し、入力Aを0000→0001→0011→0111→1111というふうに変化させたときの出力Sは次のようになります。
最後のケースは4ビット目が桁上がりしていますが、桁上がりを入力する5ビット目がないのでオーバーフローしています。
任意ビットの加算器を作るのなら、このように半加算器1個+全加算器n個で実現することができます。
さて、今回紹介した加算器は最も原始的な構造のものと言えるでしょう。この他に加算器を応用した減算器、加算器の動作を高速化するためのキャリー先読み・キャリー予測・・・・・・etc.これらの仕組みはCPUの中のALU(演算装置)の基本となるものです。この辺も機会があれば、記事にしてみようと思います。
既に紹介済みですが、論理回路作成と動作確認はフリーソフトの「Logisim」を使用しました。感電や半田ごてによる火傷の心配がないので手軽に遊ぶことができます。