CPUをつくろう -プログラムカウンタとデータセレクタ編1-

プログラムカウンタ

次に読み出すメモリのアドレスを指定する装置。クロックパルスを入力する度に指し示すアドレスを進めて行く。

今回のCPUは0〜3の計4アドレスなので、クロックを入力する度に0〜3をカウントアップする回路を作る。

フリップフロップ

カウンタはフリップフロップという回路を使って作る。フリップフロップにはいくつかの種類があるが、今回はDフリップフロップを用いる。フリップフロップについての詳細は今後の「レジスタ編」で説明する。

Dフリップフロップとは以下のような回路となる。NOT、AND、ORの論理回路を組み合わせた構成になっている。

入力DとCLK、出力Qがある。この回路のCLKにクロックパルスを入力すると、クロックの立ち上がり(LoからHiに切り替わる瞬間)にDに入力された値を記憶し、Qに出力する。

このDフリップフロップは以下のような記号で表す。

カウンタを作る

Dフリップフロップをつかって2bitのカウンタを作る。2bitのカウンタとは、00,01,10,11の2bitの値をカウントするということ、つまり0〜3をカウントアップするカウンタのこと。

2bitのカウンタは以下の構成で作る。CLKにクロックパルスを入力する度にbit1、bit0の出力が0,0、0,1、1,0、1,1と変化する。

最後にカウンタが動作する様子を示す。クロックの立ち上がりに合わせてbit1、bit0の値が変化するのが分かる。

CPUをつくろう -ROMの設計編2-

まずは1アドレス分

仕様編に書いたとおり今回のCPUに搭載するROMは「5bit × 4アドレス」とする。まずは1アドレス分を作ろう。1アドレス分は5bitなので1bit分の回路を5個並べる。上からbit0、1、2、3、4となる。

複数アドレスを実装するには

2アドレス以上にする場合は、5bit分のスイッチを図の様に増やしていく。そしてアドレス切り替えスイッチを追加する。このスイッチを切り替えることで、どのアドレスをGNDに落とすかを選択する。選択されたアドレスのデータを読み出す事ができる。

下図のように、アドレス0の全bitのスイッチが閉じており、アドレス1の全bitスイッチが開いている回路を考える。出力は、アドレス0は全bitが0、アドレス1は全bitが1となれば良い事になる。

回路の動きを見ていくと、まずアドレス切り替えスイッチがアドレス0側に接続されている場合、アドレス0側のスイッチがGNDに落ちるようになる。よって全bitが0Vとなり、出力は全bitが0となる。

次に、アドレス切り替えスイッチがアドレス1側に接続された場合、全スイッチがGNDに落ちず電源に吊られた状態である。よって全bitが5Vとなり、出力は全bitが1となる。

以上のように、各bitのスイッチを開または閉に設定することでROMに命令コードとデータを入力することが出来る様になる。そして、読み出したいアドレスをアドレス切り替えスイッチで選択する。

ここのアドレス切り替えスイッチは上記の様な物理スイッチを用いるのでは無い。電気的に切り替えることのできるスイッチを使う。電気的なスイッチは論理回路を用いて自作する。論理回路でセレクターというものを作れば、電気的に切り替えらえれるスイッチとなる。論理回路によるセレクターについては事項で解説をする。

最後にシミュレータで作った4アドレス分のROMを示す。bit0〜3が命令コード、bit4はイミディエイトデータとなる。実際の回路には各スイッチの後ろにダイオードを接続する。詳しい説明は割愛するが、電流が別のアドレスの回路に回り込むのを防ぐためである。

CPUをつくろう -ROMの設計編-

ROMとは

Read Only Memoryの略、データを保存しておく装置。Read onlyだから「読み取り専用」のメモリである。プログラムの命令コードやデータをここに書き込んでおき、プログラム実行時にここからデータを読み出す。そしてROMは不揮発性(電源を切ってもデータが消えない)のメモリである。パソコンであればHDDやSSDを指す。他にもフラッシュメモリやCD-ROMと言ったものがある。

まぁ読み取り専用と言いながら書き込みも出来るのだが、ROMの場合は特殊な方法を用いないと書き込むことは出来ないし書き込み時間も遅い。HDDなら磁気で書き込むし、CD-Rならアモルファス合金にレーザー照射して書き込む。SSDやフラッシュメモリーは電気的に書き込めるのだが…

書き込めるのにRead onlyとは良く分かりづらいが、要するにプログラム実行中は読み取り専用として動くということ。プログラム実行中に計算したデータを書き込んだりはしない。プログラムを動かして色々計算して最後に何か保存しておきたい時、このROMに保存しておく。という使い方をする。(OSがSwap領域として使う場合は除く)

ROMをどうやって作るか

先述のとおり、ROMとは「電源を切ってもデータが消えない読み取り専用のメモリ」である。世の中にはHDDやフラッシュメモリもあるが、そんなものは使えないから自作する。

ROMはスイッチを使って作る事ができる。スイッチを閉じたら0、開いたら1という具合に作る。これならスイッチを手動で切り替えて0、1を入力(書き込み)できるし、スイッチを切り替えない限り電源を切っても状態は保存される。(不揮発性)

まずはスイッチとプルアップ抵抗

スイッチを使って0、1を切り替えるにはどうするか?ここではプルアップ抵抗というものを使って実現する。

回路は下記の様になる。スイッチの片側をGND(0V)に接続し、反対側を出力とする。出力は抵抗Rを介して電源(5V)に接続する。出力が抵抗を介して電源に吊られている。この抵抗を「プルアップ抵抗」と言う。

ここでスイッチを開閉するとどうなるだろうか?

まずはスイッチが開いている状態。スイッチが開いていると出力側はどこにも接続されていないため電流が流れない。電流が流れないとプルアップ抵抗Rで電圧降下が起こらない。よって出力は5V、すなわち1となる。

∴スイッチ開で、出力1

つぎにスイッチが閉じた場合を考える。スイッチを閉じると出力側はGNDに接続されるため電流が流れる。そうするとプルアップ抵抗Rで電圧降下が起きて出力は0Vとなる。

∴スイッチ閉で、出力0

これが1bit分のメモリとなる。ROMを作るにはこの回路を複数接続していく。

CPUをつくろう -プログラム編-

これから作るCPUはもちろんプログラムを実装することが出来る。ということでCPU作成を始める前にプログラムについての話をする。

プログラム

プログラムあるいはソフトウェアと言ったらどんなものを思い浮かべるだろうか。まず思い浮かぶのは、こんなのじゃないだろうか。

#include <stdio.h>

#define VALUE1 3
#define VALUE2 5

int main (int argc, char *argv[])
{
  int a = VALUE1;
  int b = VALUE2;
  int sum;

  sum = a + b;

  printf (“sum = %d\n”, sum);

  return 0;
}
  

上記の例はC言語というプログラミング言語のコードである。それに対し、今回作るCPUに書き込むプログラムはこんなのである。

0011 0011
0000 0101
1001 0000

とてもプログラムには見えない只の数字の羅列である。何が書いてあるか分からないと思うが、少し待ってほしい。順に説明する。

高級言語と低級言語

先に説明したC言語というのは高級言語と言われる部類になる。高級言語とは、英単語や記号などを組み合わせて命令を記述することで、人間にとって理解や記述をし易くした言語のことを言う。高級言語にはC言語以外にもBASIC、Java、C#、python、Swift等がある。

しかし、コンピュータは0と1のみで動くはずである。高級言語のような英単語を直接理解できるはずがない。コンピュータは上記のような高級言語で書かれたプログラムを直接実行しているのではなく、機械語という0と1のみで記述された言語に変換して実行している。

コンピュータが直接実行できる言語を機械語、またはマシン語と言う。機械語はちょうど上にもあるように”0011 0011”のようなコードである。

この前半の”0011”の部分はコンピュータに対する命令になっていて、この例では「次に示す値をAレジスタに転送しろ」という命令を表す。そして後半の0011が値となる。これは10進数に直すと3である。よって、“0011 0011”は「Aレジスタに3を転送しろ」という命令となる。

ここで0011という機械語の命令は、人間にとってはあまりに覚えにくい。よってこの0011に人間にとって分かりやすい名前を付ける。今だと「Aレジスタに転送しろ」だから”MOV A”という名前を付ける。これだとMOV A 0011となり、命令の意味が分かり易くなる。

このような機械語に人間に分かり易い名前をつけた言語をアセンブラとかアセンブリ言語という。またMOV Aのような命令自体をニーモニックと呼ぶ事もある。

機械語やアセンブリ言語のことを低級言語という。低級言語はとてもハードウェアに近い言語である。MOV A命令を見ても分かる通りレジスタというハードウェアを直接操作している。他にも入力やメモリといったハードを直接操作する。

低級言語というのはCPUのハードウェア部分を直接触ることのできる言語である。

ところで、この機械語やニーモニックのような命令コードはCPUの種類によって使用できる命令が変わってくる。命令コードは、CPUの使用用途に合わせてCPU毎に設計されているからだ。

CPUと命令コードは一対である。これからCPUを設計するにあたり、命令コードも設計して行く事になる。

高級言語から低級言語への変換

高級言語から低級言語への変換の話をする。今回のCPU作成ではC言語のような高級言語は実装しないため、ここからの話は余談となる。

C言語で考える。C言語は以下の手順を踏んで機械語に変換される。

  • プリプロセス
  • コンパイル
  • アセンブル
  • リンク

この作業はコンパイラと言われるプログラムが行う。具体的に見て行くと

・プリプロセス:C言語の#で始まる#includeや#defineというプリプロセッサ命令を解釈する。

・コンパイル:C言語ソースコードをアセンブラコードに変換する。生成されたファイルの拡張子は「.s」となる。

・アセンブル:アセンブラコードを機械語コードに変換する。変換された後のファイルは「.o」という拡張子になる。oはオブジェクトファイルの意味である。

・リンク:別のファイルやライブラリファイルなど、複数のオブジェクトファイルをまとめてひとつの実行ファイルを作る場合、リンクという作業を行いオブジェクトファイルをまとめて実行ファイルを作成する。Windows用の実行ファイルなら「.exe」という拡張子になる。

コンパイラは有名なものではgccというコンパイラがある。GNUという組織が作っているコンパイラでオープンソースなので無償で利用できる。

ところで、このコンパイラを通すと機械語が生成されるのだが、機械語はCPUの種類によって様々である。なのでコンパイラを通す際、その機械語を動かしたい目的のCPUに合わせてコンパイラを選ぶ必要がある。armコア用ならarm用のコンパイラ、x86系ならx86用コンパイラ、Renesasマイコン用ならRenesas用コンパイラという様にそれぞれのCPUに合わせたコンパイラを使用する。

CPUをつくろう -CPUとは-

CPUとは何だろう

前回まででCPUを作るために必要な基礎的な知識を紹介した。これからCPUを作っていく事になるのだが、まずそのCPUとは何なのか? これから作るものが何なのか分からないと作りようがない。

「CPUとは中央演算装置、コンピュータの頭脳にあたる」という説明が良く見られるが、これからCPUを作るにはそれだけの理解ではたりない。どういう構造で何をしているのか。

まずCPUには主要装置として以下の装置が含まれる。(下図の赤枠の中の部分)

  • ALU(演算装置):計算をする装置
  • レジスタ:計算途中の値を一時的に保存する装置
  • 制御装置:ALUやレジスタ間のデータの流れを制御する装置
  • 命令デコーダ:メモリから読み出した命令を解釈する装置
  • プログラムカウンタ:メモリの番地を指定する装置

加えて以下の周辺装置も必要である。

  • クロック:CPU内の各装置の動作を同期させる信号
  • メモリ:命令とデータを格納する装置
  • 入力装置:計算データ等を入力する装置
  • 出力装置:計算結果のデータ等を出力する装置

クロック

クロックとは、HiとLoを一定周期で繰り返すパルス波形のこと。プログラムカウンタ、制御装置、レジスタ等に入力しCPUを動作させる信号である。

メモリ

CPUに実行させる命令とデータを記憶させておく装置。メモリにはROMとRAMがあるが、今回作るCPUでは、このメモリはROMとする。

メモリは基本的には1区画が8bitになっており、1区画の中に命令やデータを1つずつ格納できる。そして、その何区が何個も連続している。区画は1区画毎に先頭から0、1、2…というアドレス(番地)が割り振られている。

下図の例ではアドレス0に0b0110 1101という値が格納されている。

プログラムカウンタ

メモリには命令が1つずつ格納されているのだが、CPUはこの命令をアドレス0から順々に実行していく事になる。プログラムカウンタとは次に実行するべきメモリのアドレスを指し示す装置である。

クロックが入力されるたびに指し示すアドレスをカウントアップして行く。

命令デコーダ

メモリから読み出した命令はそのままではCPUを動作させることが出来ない。命令デコーダは命令をCPUが解釈できる信号に変換する装置である。

制御装置

CPUはレジスタの値を演算装置に転送して計算したり、演算装置で計算した結果の値をレジスタに格納したりする。前者のデータの流れは”レジスタ→演算装置”となっているし、後者は”演算装置→レジスタ”となっており、それぞれでデータの流れる向きが違う。

制御装置は、メモリから読み取った命令に応じて、このデータの流れを切り替える装置である。

レジスタ

演算装置で計算した値や、入力装置から入力した値を一時的に格納する装置。データ幅はCPUによって様々だが、このレジスタのデータ幅が広いほどCPUで扱える命令やデータの範囲が広くなり、CPUは高性能になる。データ幅が8bitのものを8bitCPUとか、32bitのものを32bitCPUと呼んだりする。

レジスタは通常2つ以上あり、Aレジスタ、Bレジスタという様に個別の名前がつけられている。

データを一時的に格納するものにはRAMというメモリもあるが、レジスタはCPUに内蔵されておりRAMよりも超高速にデータの転送が可能となっている。RAMは計算結果を格納するのに対し、レジスタは更に一時的な計算途中の値などを格納する。

ALU(演算装置)

CPUの心臓部、計算をする装置。具体的にはレジスタや入力装置からデータを入力し、入力した値に対し加算や減算を行う。

入力装置

計算させたい値をCPUに入力する装置。

出力装置

計算した結果等の値を出力する装置。

CPUをつくろう -2進数と16進数編2-

16進数の必要性

コンピュータは数を0と1のみの2進数で表現する。この2進数、人間にとっては弱点がある。桁数が多くなると何が書いてあるか分かりづらいのだ。例えは0b01100011、これはぱっとみて全体の桁数も分かりづらいし、どの桁が1でどの桁が0かもとても見づらい。

そこで人間が理解し易いように16進数を用いる。

16進数

16進数とは、16から1を引いた数”15”で桁上がりをする数の表現方法である。1から15までを1桁で表現するのだが、残念ながら10以降を1桁で表現できる数字は無い。よって10〜15をアルファベットのA〜Fで表す。10進数、2進数、16進数の対比を示す。

10進数2進数16進数
000
111
2102
3113
41004
51015
61106
71117
810008
910019
101010A
111011B
121100C
131101D
141110E
151111F

ここでも16進数は10を”イチゼロ”と言い、1Bは”イチビー”と言う。

2進数と同様に、これが16進数表記だと分かるように頭に”0x”をつけ、0x10や0x1Bという様に記述する。

2進数は4bit毎に間を開けて書いておくと16進数に変換するときに見やすくなる。2進数4bit分で16進数1桁になるからだ。

2進数を4bit毎に区切り、4bitづつ16進数に変換していけば16進数表記になる。0b0000 0000は0x00、0b0110 1100は0x6Cとなる。

16進数を使えば少ない桁数で2進数を表現できるため、数の把握が容易になる。

CPUをつくろう -2進数と16進数編-

2進数

コンピュータは全ての数を0と1のみで表現する。対して人間は0〜9までの数字を使って数を表現する。0〜9をで表現する数を10進数、それに対し0、1のみで表現する数を2進数という。

10進数と2進数の対比を以下に示す。

10進数2進数
00
11
210
311
4100
5101
6110
7111
81000
91001
101010
111011
121100
131101
141110
151111

10進数は、0,1,2…と進んで行き9まで来ると桁上がりして10となる。10進数の10から1を引いた数”9”まで来ると桁上がりする。

2進数は、2から1を引いた数”1”まで来ると桁上がりする。つまり全ての桁が1になると桁上がりをする。例えば1の次は桁上がりして10となる。ちなみに2進数の10は”ジュウ”ではない。10と書いて”イチゼロ”とか”イチマル”と言う。

ここで、10と書いても10進数のジュウなのか、2進数のイチゼロなのか区別がつかないため、普通は10進数の場合は頭に”d”をつけてd10のように書き、2進数の場合は頭に”0b”をつけて0b10と書く。dはデシマルの略、bはバイナリの略である。10進数のdは省略する場合が多い(と思う)。

2進数の場合、1桁を”bit(ビット)”という。bitというのがコンピュータで数を表す最小単位となる。0b10は2bitだし、0b1111は4bitである。

bitが8つ集まった8bitの事を”Byte(バイト)”という。0b11111111は1Byteである。

コンピュータで2進数を扱う場合、表記を1Byte単位に合わせて書く場合がある。0b1なら0b0000 0001、0b11110なら0b0001 1110と書く。この際4bit毎にスペースを入れて書くと読みやすくなる。(理由は後の16進数の項目で説明する)

10進数と2進数の変換

・10進数から2進数へ

10進数を2進数に変換するには、変換したい10進数の数字を商が0になるまで2で割り続け、余りを求めていく。求められた余りが変換後の2進数となる。

例えば35を2進数に変換すると、0b0010 0011となる。

・2進数から10進数へ

2進数から10進数に変換するには、各bitの値にその桁の「重み」を掛ける。重みという言葉が出てきたが、n桁目の重みは2のn-1乗となる。例えば、3桁目の重みは2の2乗となって4である。そして最後に各桁で算出した値を足し合わせる。

先ほどの0b0010 0011を10進数に変換すると、下記の通り35となる。

CPUをつくろう -論理回路編3-

OR回路

ANDと回路来たら次はOR回路。

ORは「または」回路。入力Aが1または入力Bが1の時、出力が1となる。真理値表に表すと以下の通り。

入力A入力B出力Y
000
011
101
111

NOR回路

NOR回路はOR回路の出力を反転させたものである。よって、入力ABのどちらか一方でも1の場合、0を出力する。真理値表表は下記になる。

入力A入力B出力Y
001
010
100
110

NOR回路をトランジスタで

まずはNOR回路をトランジスタで作る。NAND回路のときはNOT回路を縦に2つ接続した。今回はNOT回路を横に2つ接続するイメージで作る。

NOR回路の回路図を以下に示す。2つのNOT回路の出力が重なった形になっている。

回路図記号で示すと下記のようになる。

動作する様子を示す。入力ABの一方でも1となった場合、出力が0となる。

OR回路をトランジスタで

次にOR回路をトランジスタで作ってみよう。OR回路とNOR回路は逆の関係である。OR回路はNOR回路を否定すれば良い。よってNOR回路の出力にNOT回路を接続するとOR回路になる。

回路図を示す。NOR回路の出力にNOT回路が接続されている。

回路記号で表すと以下の通り。

最後にOR回路が動作する様子を示す。入力ABの片方でも1の場合、出力は1となる。

CPUをつくろう -論理回路編2-

AND回路

NOT回路に続いて次はAND回路を紹介しよう。AND回路とは入力が2つあり、2つの入力が両方とも1の場合のみ1を出力する回路である。

表であらわすと以下の通りとなる。ちなみにこのような入出力の関係をまとめた表を真理値表という。

入力A入力B出力Y
000
010
100
111

NAND回路

AND回路の次はNAND回路を示す。NAND回路はAND回路の出力を反転させたものである。よって、入力AB両方が1の時のみ0を出力する。真理値表は下記になる。

入力A入力B出力Y
001
011
101
110

NAND回路をトランジスタで

まずはNAND回路をトランジスタで作ってみよう。まず思いだすとNOTというのは0を入力すると1が出力された。NANDというのはそれを直列に組み合わせる事になる。

回路図を示す。NOT回路が縦に2つ接続されたような回路となっている。

動作する様子を動画で示す。入力が両方とも1(Hi)のときのみ出力が0(Lo)となる。

回路記号は以下のようになる。

AND回路をトランジスタで

次はAND回路。AND回路とNAND回路は逆の関係である。よってAND回路はさっきのNAND回路を否定すれば良い。ということは、NAND回路の出力にNOT回路を接続すれば良い。

下の動画のように入力が両方とも1のときのみ出力が1となる。

回路記号は以下の通り。