for beginners - cpu-dev/cpu-dev.github.io GitHub Wiki

自作CPUを始める方たちへ

当ページを訪れていただいて感謝します。自作CPU研究会では, コンピュータの核となるCPUを自作すること、またそれにまつわる事柄について議論したり質問したりするとこが出来る場所として設立されました。 皆様も自作CPUに興味があってこのページを訪れたのだろうと思われます。自作CPUを進める上ではその特性上、情報を手に入れることが難しいのが現状であり、より多くの情報、より多くの人が必要です。もし少しでも自作CPUに興味を持っていたら当研究会に入って頂けると幸いです。

CPUとはなにか

CPU(Central Processing Unit)とは、その名の通りコンピュータの中枢となる「演算」や「制御」を担う装置です。現代のCPUは一般的に、メモリから命令を取得して実行することが基本の動作になっています。

「演算」は想像に難くないでしょう。加減乗除算などのことです。 「制御」は命令をメモリから取ってきたり、命令を解釈したり、命令に合わせて「演算」そのものを実行したり、メモリに実行結果を書き込んだりすることです。

CPUはその「演算」と「制御」を用いて様々な処理を可能とします。

CPU設計の基礎

CPUの設計と聞いて、電子回路を設計することを思いつく方は少なくないと思います。 そういう方には衝撃的かもしれませんが、電子回路としてCPUを設計することはまずないと言っていいでしょう(トランジスタなどを使ってCPUを作る猛者はたまにいますが)。ではどうするかというと、電子回路を抽象化して論理の世界で扱えるようにした論理回路や、論理回路を実現するためのディジタル回路、というものとして設計します。趣味のレベルで自作CPUをするときには論理回路の知識だけで完結することが多いです。

論理回路は、その名の通り論理を実現する回路です。論理素子と呼ばれるものを使って構成します。論理素子には、AND, OR, NOT, XORなどといったものがあります。聞いたことがある方も多いのではないでしょうか。論理回路では、真と偽という2つの状態が存在します。そのため、2進数との親和性が高くコンピュータをはじめとするディジタル回路では2進数をベースに使います。また論理回路には現在の入力のみで出力が決まる組み合わせ回路と過去の入力と現在の入力によって出力が決まる、つまり過去の入力を記憶する順序回路というものがあります。CPUを作るときには、この2つをうまく使い分けて構成していきます。

上で説明したように、CPUはメモリから命令を取得して実行する装置です。 命令は、命令セットアーキテクチャ(ISA)によって決められた2進数の列で構成されています。 ですから、まずCPU設計の基本としてはこの命令を入力として目的の出力を得るための組み合わせ回路を作ればよいのです。実行結果はメモリに書き込みます。

ところで、単にメモリから命令を取得すると言ってもメモリのどこから命令を取得するのか決めなければなりません。そこで、メモリのどこから命令を取得するのかを示すProgram Counter(PC)という機構を用意します。これは「レジスタ」と呼ばれるものの一つでCPU内部に存在する記憶領域です。 PCが今指している命令を実行したら、PCには次の命令を指してもらいたいです。このような回路は、先程でてきた順序回路ですね!

このようにCPUは組み合わせ回路と順序回路をうまく使って構成します。

自作CPU初心者への指針

自作CPUを始めるために必要な情報をここに記します。 何もないところから作るのはとても難しいです。まずは情報源となる書籍を入手しましょう。以下は、自作CPU研究会が薦める入門書です。

書籍

CPU製作に役立つ資料の書籍の項から抜粋します。

  • 動かしてわかる CPUの作り方10講

CPUの動作原理からエミュレータ作成、FPGAを用いたCPU自作の方法までを初心者にもわかりやすく解説しています。

  • CPUの創りかた

論理回路の設計から入門できます。書籍内では秋葉原で入手できる汎用ロジックICを使って実際に基板上にはんだ付けをしてCPUを作るので「ものを作ってる」感があります。組み合わせ回路と順序回路、カルノー図を用いた組み合わせ回路設計と聞いてピンと来ない方はここから読むといいと思います。

  • コンピュータシステムの理論と実装

単純な電気素子であるNANDからCPUを作成し、コンパイラ、バーチャルマシンなどを実装しコンピュータを完成させます。書籍自体には完全な実装は載っておらず、仕様だけが与えられて作っていくスタイルなので自分で考えて自作CPUを楽しむことが出来ます。

  • ディジタル回路設計とコンピュータアーキテクチャ

MIPSアーキテクチャを扱った書籍です。CPU設計にまつわる非常に丁寧な説明と多くの知識、2種類のHDLによる実装が載っています。

FPGA(Field Programmable Gate Array)

自作したCPUを現実世界で使いたい場合にFPGAがよく使われます。FPGAとは内部に大量の論理ブロックを持っており、それらの内部と繋がりを書き換える事によって任意のディジタル回路を内部に生成できるチップです。CPUもディジタル回路の1つであることからFPGAはCPUも内部で構成することができます。 FPGAのプログラムにはVerilog HDLやSystemVerilog等のHDL(ハードウェア記述言語)が使われます。他にもC++等のプログラミング言語を用いてFPGAをプログラムする高位合成(HLS)と呼ばれる技術もありますが、ここでは省略します。

自作CPUをちょっとかじってみる

ここでは、実際にCPUを作るための建設的なステップを紹介します。 この章では前提知識に論理回路の基礎の知識が含まれます。未履修の方は、東京大学Open Cource Wareに登録されている坂井先生の「論理回路基礎」の講義資料で学習することをおすすめします。

坂井先生の「論理回路基礎」の講義資料

Verilog HDL入門

ここではHDLの1つであるVerilog HDLの書き方を解説します。
以下は半加算器の論理式と真理値表です。

C = A and B

S = A xor B

A B C S
0 0 0 0
0 1 0 1
1 0 0 1
1 1 1 0

以下のコードはVerilog HDLで書いた半加算器のコードです。

module HalfAdder(A, B, S, C);
    input wire A, B;
    output wire S, C;

    assign C = A & B;
    assign S = A ^ B;
endmodule

半加算器は1ビット同士の加算を行います。AとBが入力、Sが出力、Cが桁上げ出力です。 1行目から見ていきましょう。

module HalfAdder(A, B, S, C);

1行目はモジュール名とIOポート名の宣言です。 Verilog HDLでは論理回路の塊を1つのモジュールとして扱い、モジュール同士を組み合わせる事で大規模な回路の構成が可能とします。またモジュール同士を接続するにはIOポートが必要な為、モジュール名(ポート1, ポート2, ...);のといった形式でIOのポート名の宣言が可能です。

2~3行目を見てみましょう。

    input wire A, B;
    output wire S, C;

2~3行目ではIOポートの宣言をしています。1行目でポート名の宣言のみを行いましたが、これだけではどれが入力でどれが出力なのかは明示されていません。そこで2~4行目で宣言しています。最初のinputで入力であることの宣言、続けてwire A, BでA, Bが記憶する機能を持たない線であることを示しています。出力もinputをoutputと書き換えてS, Cは出力であると宣言しています。

次に5~6行目を見てみましょう。

    assign C = A & B;
    assign S = A ^ B;

各行の最初に書いてあるassignは、その後ろの式が組み合わせ回路であることを示しています。 C = A & BはAとBのAND結果をCに接続していることを表しています。

7行目のendmoduleはモジュールの宣言の締めくくりに使います。
プログラミングを少しでも齧ったことのある方ならすぐ慣れるはずです。

Verilog HDLではかなり柔軟にいろいろな書き方で同じ論理回路を実現できます。他の書き方でも半加算器を実装してみましょう。

module HalfAdder(A, B, S, C);
    
    input wire A, B;
    output wire S, C;
    
    assign {C, S} = A + B;
    
endmodule
module HalfAdder(A, B, S, C);

    input wire A, B;
    output wire S, C;
    
    assign {C, S} = HA(A, B);
    
    function [1:0] HA;
        input A, B;
        begin
            case({A, B})
                2'b00: sum = 2'b00;
                2'b01: sum = 2'b01;
                2'b10: sum = 2'b01;
                2'b11: sum = 2'b10;
            endcase
        end
    endfunction
endmodule
module HalfAdder(A, B, S, C);

    input wire A, B;
    output wire S, C;
    
    reg[1:0] sum;
    
    assign {C, S} = sum;
    
    always @(*) begin
        case({A, B})
            2'b00: sum = 2'b00;
            2'b01: sum = 2'b01;
            2'b10: sum = 2'b01;
            2'b11: sum = 2'b10;
        endcase
    end
    
endmodule

各部に関して細かく解説はしませんが、always文とfunction文だけ説明します。always文は組み合わせ回路や順序回路を記述でき、function文では組み合わせ回路を記述できます。alwaysでもfunctionでもあまり大差はないのでどちらを使ってもいいと思います。always文では、alwaysのカッコの中に書いた信号に変化があったときにalways文の中を実行するのですがここでは*(asterisk)を書くことで全ての信号を対象としています。

module FullAdder(X, Y, Cin, out, Cout);

    input X,Y;
    input Cin;
    output out;
    output Cout;

    wire s0,c0,c1;

    HalfAdder HalfAdder_0(X,Y,s0,c0);
    HalfAdder HalfAdder_1(s0,Cin,out,c1);

    assign Cout = c0 | c1;
endmodule

Verilog HDLでは、他のmoduleを上のようにして使うことができます。