CPUの設計
CPU(Central Processing Unit)を設計するにあたって以下の本を参考にした。名著なのでぜひ。
CPUは一連の命令に従って、CPU内の記憶領域(レジスタ)を操作する装置である。
最も簡単なCPUは、計算を実行するALU(Arithmetic and Logic Unit)、記憶を行うレジスタ、命令を格納するバッファで構成される(多分)。
上記CPUでは4本の記憶領域を持つレジスタがあり、各レジスタにはアドレスが割り振られている。
また、4個の命令を格納できる命令バッファがある。
このCPUにおいて命令が実行される様子を次に示す。命令バッファの中には、「アドレス0とアドレス3を足してアドレス2に結果を入れろ」という加算命令、「アドレス0からアドレス3を引いてアドレス3に結果を入れろ」減算命令が入っているとする。また、レジスタの中には、アドレス0、1、2には0、アドレス3には1の値が入っているとする。
各命令の実行は以下の通りとなる。
これがCPUの本質的な動作となる。
しかし、このCPUでは動かすたびにレジスタや命令バッファに書き込む必要がある上に、それぞれ記憶領域が4個しかないため、大規模な計算を実行できない。そこで、命令や値を大量に記憶しておくため、メモリが用いられる。
C言語のコンパイル その2 コンバータのアルゴリズム
当面の目標
まず、簡単なプログラムをModasm形式にコンバートできるコンバータを作成する。その後、徐々に難しいプログラムをコンバートできるように拡張を行っていく。
まず最初に、以下のプログラムをコンパイルできることを目標とする。
#include<stdio.h> int main(void) { return 42; }
これを、以下のコマンドを用いて
clang -S -emit-llvm main.c
LLVM IRファイルmain.llを作成する。
ここら辺の内容はこの記事を参考にした。 itchyny.hatenablog.com
自分の実行環境では以下のようになった。
; ModuleID = 'main.c' target datalayout = "e-m:e-i64:64-f80:128-n8:16:32:64-S128" target triple = "x86_64-pc-linux-gnu" ; Function Attrs: nounwind uwtable define i32 @main() #0 { %1 = alloca i32, align 4 store i32 0, i32* %1, align 4 ret i32 42 } attributes #0 = { nounwind uwtable "disable-tail-calls"="false" "less-precise-fpmad"="false" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-nans-fp-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+fxsr,+mmx,+sse,+sse2" "unsafe-fp-math"="false" "use-soft-float"="false" } !llvm.ident = !{!0} !0 = !{!"clang version 3.8.0-2ubuntu4 (tags/RELEASE_380/final)"}
このうち、コード部分を抜き出す。
define i32 @main() #0 { %1 = alloca i32, align 4 store i32 0, i32* %1, align 4 ret i32 42 }
この記事を参考にし、以下のことがわかった。 LLVMでLLVM-IRを生成して眺める · GitHub
- 関数はdefine [戻り値の型] @ [関数名()] #0 {...}の形式で表す
- レジスタは%1,%2,...で表す
- alloca命令は静的なメモリ領域の確保を行っており、メモリアドレス(ポインタ)を返す。
- align 4によってメモリアドレスは4の倍数となっている必要がある。
- store命令はレジスタ値をメモリに書き込む
- ret命令は戻り値を表す。
- ret [型] [即値]
LLVM IR表現に出てくるすべての命令は、当然ながらLLVMの公式サイトに最も詳しい説明が載っている。 ちょくちょく参考にする必要がありそう。
LLVM Language Reference Manual — LLVM 8 documentation
以上から、コンバータを以下の構成とすることにした。
コンバータの構成
C言語のコンパイル その1 どのような方針で行くか?
どのように変換するか?
C言語をバイナリにどのように変換するか、が自作CPU上に自作OSを乗せる上で、最も大きな課題の1つと考えられる。 すなわち、コンパイラをどのように作るか、である。
レギュレーションとして、コンパイラはとりあえず動けばOKということにする。 そこで、容易さ、実装時間の面から、既存のフレームワークであるLLVMの一部を利用し、とりあえず以下のように実装することとした。
(先日のOSもくもく会で聞いたが、過去に某企業がこのような構成でコンパイラを実装し、LLVMのコミュニティを怒らせたことがあるらしい...。これは趣味なので許していただきたい。) まず、フロントエンドはclangを用いてLLVM IR(中間表現)に変換する。中間表現はテキスト形式によって記述され、可読性が高い。
以下の記事が参考になった。 itchyny.hatenablog.com
次に、LLVM IRをアセンブリ言語に変換する。CPU名はModakioとした(CPUの設計編は時間を見つけていつか書く)。 Modakioのアセンブリ言語なのでModasmとする。 LLVM IRからmodasmへの変換をどのように行うか、が今回の記事の内容となる。
一番の課題は、レジスタへのアサインをどのようなアルゴリズムで行うか、になると考えている。 コンパイラのレジスタアサインはどのようにやっているか全くわからないが、とりあえず動けばいいので素人考えで適当に実装してみることにする。
Modasmから機械語への変換を行うアセンブラ、および動作検証を行うはModakioのエミュレータはすでに作った。(こちらも時間を見つけて書く)。
論理回路の基礎
VerilogHDLを用いたモジュール記述に入る前に、最低限、論理回路はどんな要素からできているのか知っている必要があります。 VerilogHDLで記述するというのは頭の中の論理回路を言語で記述する作業となります。
レジスタ(register)とは?
論理回路において、フリップフロップなどにより状態を保持する装置をレジスタと呼びます。 レジスタは高速で動作できる一方、保持できるデータ量は比較的少量です。 レジスタ (コンピュータ) - Wikipedia
レジスタの動作を(動いたほうがわかりやすいと思ったので)動画にしてみました。 レジスタには入力D、出力Q、クロックclkの3つのポートがあります。 入力Dに信号が入力されると、クロックの立ち上がりタイミングでキャプチャされ、出力Qから出力されます。 これが全てのクロック毎に繰り返されます。
入力D、出力Qのタイミングがクロックの立ち上がりから少し遅れていますが、これは、キャプチャにかかる遅延、電気信号が信号線を伝達するのに遅延が発生するためです。
組み合わせ回路と順序回路
論理回路には、組み合わせ回路と順序回路の2種類があります。 これまでの入力が現在の出力に影響するかどうかによってどちらの回路か決まります。
組み合わせ回路
その時点での入力値のみによって出力値が決まります(過去の入力は現在の出力に影響を与えない)。 そのため、回路内部に状態を持っていません。
組み合わせ回路の例として、以下のような回路が考えられます。
このような組み合わせ回路をVerilogHDLで記述する場合、function文を用いて記述できます。
\\function文テンプレート function [ビット幅-1:0] function_name; input ....; .... endfunction
順序回路
現在の入力値だけでなく、過去の入力値も現在の出力値に影響を与えます。内部に状態があり、状態が現在の出力に影響します。 内部に状態を保持する方法として、レジスタが用いられます。
順序回路の例として、以下のような回路が考えられます。 このような順序回路をVerilogHDLで記述する場合、順序回路はmodule文を使って記述します。
\\module文テンプレート module module_name #( parameter パラメータ名0 = デフォルト値0, parameter パラメータ名1 = デフォルト値1, ..., parameter パラメータ名N = デフォルト値N ) ( input [ポート幅-1:0] ポート名0, input [ポート幅-1:0] ポート名1, ..., output [ポート幅-1:0] ポート名2, output [ポート幅-1:0] ポート名3, ..., output [ポート幅-1:0] ポート名N ); 論理回路記述; endmodule
module文への入力、module文からの出力はポートを介して行われます。 パラメータ部分は省略可能です。
VerilogHDLを用いた1bitレジスタの記述
VerilogHDLでは、レジスタはalways文と呼ばれる文を用いて、記述することができます。 always文の文法は以下となります。
\\always文テンプレート always @ ( [posedge|negedge] [信号1] or [posedge|negedge] [信号1] or ...) begin レジスタの入力信号・条件記述; end
posedge、negedgeはどのタイミングでレジスタへの入力信号の取り込みを行うかを指定します。 上の動画の動作を記述する場合、クロックの立ち上がり時に信号の取り込みを行っているので、posedgeを用いることになります。
1レジスタ毎に1always文にするのが良いと思います。 (2つ以上のレジスタを1always文内に入れることもできますが、見にくいのでやらないほうがいいと思います。) 上図のレジスタをVerilogHDLに直すと下のソースコードのようになります。
module one_bit_register ( input clk, input to_regA, output from_regA ); reg regA; always @ (posedge clk) begin regA <= to_regA; end assign from_regA = regA; endmodule
ソースコード中には宣言文、always文、assign文があります。
宣言文では、使うワイヤやレジスタを宣言します。宣言はソースコード中のどこに書いてもOKです。 宣言ではワイヤ・レジスタの幅も指定できます。(指定がない場合、幅1になります。)
assign文はレジスタ-ワイヤ接続、及びワイヤ-ワイヤ接続や関係を記述するのに用います。 assign文の文法は以下となります。
\\assign文テンプレート assign wire_B = [wire_A|register_A];
4bitレジスタx3の接続
上の論理回路をVerilogHDLを用いて記述すると以下になります。
module four_bit_regx3 ( input clk, input [3:0] to_regA, output [3:0] from_regC ) \\regA reg [3:0] regA; always @ (posedge clk) begin regA <= to_regA; end assign regA_to_regB = regA; \\regB wire [3:0] regA_to_regB; reg [3:0] regB; always @ (posedge clk) begin regB <= regA_to_regB; end assign regC_to_regB = regB; \\regC wire [3:0] regB_to_regC; reg [3:0] regC; always @ (posedge clk) begin regC <= reg_B_to_regC; end assign from_regC = regC; endmodule
イネーブル付きレジスタ
毎サイクル、入力信号をキャプチャしていては使いにくいため、イネーブルポートのついたレジスタがあります。 en=1のときのみ値の取り込みを行います。
これは以下のように書きます。
wire in; wire out; wire en; reg regA; always @ (posedge clk) begin if (en) begin regA <= in; end end assign out = regA;
もちろん、上の左図の通り
wire in; wire out; wire mux_to_regA; wire en; reg regA; always @ (posedge clk) begin regA <= mux_to_regA; end assign mux_to_regA = (en) ? in : out;
と書いても同様の動作します。(が、特別の理由がない限り、このように書く必要は無いでしょう)
次の記事では、実際にシミュレータを用いて動作を動作させてみます。
はじめに
なぜこのブログを書いたか?
FPGAとは論理仕様をプログラムすることによって、論理回路を半導体チップ上に合成してくれるデバイスである。本ブログでは最終的に簡単なCPUを合成することを目的としてFPGA開発を行っていく。本ブログはFPGAを触ったことのない人を対象として図や絵を用いてできるだけわかりやすく書いてみようと思う。ただし、僕は別にFPGAを研究している教授でも、企業の中でFPGAを使っている人でもないため(大学院のとき研究にちょっと使っていた)、間違った情報も多々含まれていると思う。そのため、間違った情報や、分かりにくかった点などは指摘していただけると嬉しい。
何が書いてあるか?
このブログの方針として、あまり理論的なことには触れない。それはとりあえず手を動かしてみてロジックを作ってみるのが面白いと考えたためである(気が向いたらappendixの形で追記するかもしれない)。 最低限ロジックを組むにあたっての要素ごとの振る舞いの説明(何を入れたら何が出てくるのか)はするつもりである。 また、自分の望み通りのロジックを合成するためのテクニカルな書き方、詰まりそうなバグ等も載せていく。 本ブログは後半、Intel社のFPGA(旧Altara社)開発環境を用いて進めていく。Xilinx社のFPGAを使っている人には申し訳ない。 内容自体はどちらのFPGAにも当てはまる抽象的な説明から入る。Xilinx社の開発環境にも同等機能が実装されているので参考にはなると思う。
どのような手順で進めていくか?
本blogではVerilogHDLというハードウェア記述言語を用いてFPGAを開発を行っていく(OpenCL等の高位合成系は使用しない。そちらはweb上にたくさん入門記事がある)。 まず最初に、VerilogHDLと、合成されるロジックの関係を説明する。 その説明を理解してもらった後、実際に複数のサンプルロジックを合成し、入力・出力波形を見て合成できていることを確認する。 実機のFPGAボードがあれば嬉しいが、結構高いので(だいたい安いものでも6000円位する)動作確認は基本的にシミュレータのみを用いて行う。 実機のFPGAを用いた動作確認は最後の最後にちらっと出すかもしれない。
目次
基本ロジック編
- 論理回路の基礎
- カウンタを用いたPWM生成器(いわゆるLチカ)
- カウンタを用いたPWM生成器実機編(ノイズキャンセラ)
- ステートマシンとは
- ステートマシンを用いたなにか
- 他論理要素(FIFO、マルチプレクサ、バレルシフタ)
非同期並列計算編
簡単なMIPSのCPU制作編
...
他テクニカルなネタ
- log_2 xのマクロ
- for文を用いた論理合成
- モジュールへのネーミング
- シミュレータの導入方法
- Intel社の提供する開発環境Quartus導入方法