愛知⼯業⼤学の藤枝と申します。 普段は電気系の学⽣さんにディジタル回路や計算機システムを教えつつ、FPGA を使って何か⾯⽩いことができないかと⽇々⼿を動かしています。
このコースでは、実用的な回路の設計・実装と動作確認を通じて、ハードウェア記述言語 (HDL) を使った FPGA 上のディジタル回路設計について学んでいきます。具体的には、PC と FPGA との最もお手軽な通信手段であるシリアル通信 (UART ともいいます) を使って、FPGA から PC に対して 「Hello, FPGA」 と表示させることが、このコースの目標です。
広く扱われている HDL には VHDL と Verilog HDL、そして Verilog HDL の後継の SystemVerilog があります。本コースでは SystemVerilog を扱います。前提となるいくつかの知識は、本ブログで同時に連載されている他のコースで解説されていますので、併せて参照してください。
ディジタル回路の基礎
組合せ回路と順序回路
このコースでは HDL を用いますが、まずはその説明に必要なディジタル回路の基礎について、おさらいしていきましょう。ディジタル回路は、ゼロとイチで情報を処理したり伝達したりする回路のことです。プログラムでは入力と出力は引数と返り値という形で受け渡していたかと思いますが、ディジタル回路はあくまでも回路ですから、回路の外側からは入力と出力の信号 (端子) が見えることになります。つまり、入力と出力はその回路の外見です。それに対して、入力と出力の間にどのような関係が成り立つかが、その回路の中身になります。
例えば、上図に示す2つの1ビットの加算を行う回路のことを半加算器 (Half Adder) といいます。この回路の入力は加算の対象である A と B の2本、出力は和の2の位である C (Carry) と1の位である S (Sum) の2本です。入力と出力の関係は、表を使って表現したり、より小さな回路の組合せ (上図の場合は AND ゲートと XOR ゲート) で表現したりします。
回路の中身を定義するにあたって、出力が現在の入力だけで表せることもあれば、過去の入力やそこから得られた情報も必要なこともあります。ディジタル回路はこの違いによって大きく2つに分けられていて、前者を組合せ回路、後者を順序回路といいます。先に挙げた半加算器は組合せ回路の一種です。
順序回路では、過去の入力やそこから得られた情報を状態として記憶しておく必要があります。上図の赤枠で囲んだ部分が状態を記憶する部分、すなわち記憶回路です。一般的なディジタル回路ではその素子として、D フリップフロップ (D-FF) が使われます。また、D-FF を束ねたものをレジスタとよびます。 記憶された今の状態と今の入力から、次の状態や今の出力を求める (この部分は組合せ回路です) ことで、順序回路が出来上がります。つまり、順序回路は組合せ回路 + 記憶回路で構成されています。
さて、今とか次とか書きましたが、そのタイミングをどこで判断すればいいでしょうか。典型的な順序回路では、時刻を定めるためのクロックとよばれる入力信号を用意しておいて、その信号が ‘0’ から ‘1’ に変化する瞬間 (立上りといいます) を時刻の区切りとしています。また、時刻ゼロのとき、つまり回路が動作を開始した瞬間の状態を定めておくことは、回路の誤動作防止に役立ちます。そのために、リセットとよばれる入力信号を用意しておいて、この信号が ‘1’ であるときには状態を初期値に固定する、という方法がよく使われます。これらの理由から、順序回路は他の入力に加えて、クロック入力とリセット入力をもちます。
状態遷移図
順序回路の設計では、その回路がどのような状態をもつか、またある状態から別の状態へとどのような条件で移るのかを考えることが重要です。これには、状態とその間の移り変わり (遷移) とをグラフィカルに表した図がよく利用されます。 これを状態遷移図といいます。状態遷移図では状態を囲みで表し、状態名は囲みに付記します。また、遷移を囲みの間の矢印で表し、遷移する条件は矢印に付記します。出力は状態名や条件と区別がつけられるよう、スラッシュで区切ってから付記します。
例えば、回路をオン状態にするスイッチ SW_ON とオフ状態にするスイッチ SW_OFF、そして出力 ON をもつ回路を考えます。出力 ON は、この回路がオフ状態からオン状態になったときに限り ‘1’ を出力するとします。ただし、両方のスイッチが同時に押されているときには SW_OFF が優先されるものとします。
この回路の状態遷移図は上図のように表されます。この回路にはオフ状態 (STATE_OFF) とオン状態 (STATE_ON) の2つの状態 (囲み) があります。図の右向きの矢印、つまりオフ状態からオン状態に切り替わる条件は、SW_ON が押されていて、かつ SW_OFF が押されていないことです。このときに限り出力 ON は ‘1’ になります。また、図の右向きの矢印、つまりオン状態からオフ状態に切り替わる条件は、SW_OFF が押されていることです。ON が ‘0’ になる場合や、状態遷移しない場合は、図から省略しています。
このあとに説明しますが、状態遷移図を作っておくと、 特に順序回路の組合せ回路部分を HDL で書くときに便利です。
SystemVerilog 記述の例
それでは、SystemVerilog による回路記述を読み解きながら、ここまで見てきたディジタル回路の基礎が、どのように回路記述と対応しているかを見ていきましょう。 先に挙げたオン・オフの回路を SystemVerilog で記述すると、以下のようになります。
module onoff_circuit (
input logic CLK, RST,
input logic SW_ON, SW_OFF,
output logic ON);
typedef enum {
STATE_OFF,
STATE_ON
} state_type;
state_type state, n_state;
always_comb begin
ON = 1'b0;
n_state = state;
if (state == STATE_OFF) begin
if (SW_ON & ~ SW_OFF) begin
ON = 1'b1;
n_state = STATE_ON;
end
end else if (state == STATE_ON) begin
if (SW_OFF) begin
n_state = STATE_OFF;
end
end
end
always_ff @ (posedge CLK) begin
if (RST) begin
state <= STATE_OFF;
end else begin
state <= n_state;
end
end
endmodule
回路記述は、空行を区切りとして大きく4つに分けらます。順番に見ていきます。
回路の外見
最初の module 文では、回路の入力と出力、つまり外見を定義しています。入力であれば input logic、出力であれば output logic で、入出力の信号をカンマ区切りで宣言していきます。この回路は順序回路なので、2つのスイッチ SW_ON と SW_OFF に加え、クロック入力CLK とリセット入力 RST が宣言されています。
末尾の endmodule 文までの間で、回路の中身を定義していきます。
回路の状態
2つ目の部分では、状態を列挙型 state_type として定義するとともに、今の状態 state と次の状態 n_state をその列挙型をもつ内部信号として定義しています。文法的には C 言語での列挙型と変わりません。
状態を列挙型で定義しておくと、シミュレーションを使ったデバッグ (詳しくは第3回で扱います) のときに便利です。実際のディジタル回路に落とし込むときには具体的なゼロとイチへのあてはめ (論理割当て) が必要ですが、これもツールが最適そうなものを自動的に選んでくれます。
組合せ回路
3つ目の部分は、順序回路のうちの組合せ回路部分を定める部分です。条件分けなどを含む複雑な組合せ回路の記述には、always_comb 文を使用します。begin から end で囲まれた部分が上から順番に評価されていきます。always_comb 文の中での代入には = 演算子を使います。同じ信号に複数回の代入があれば、最後に代入された値が最終的な信号の値として採用されます。
このとき参考になるのが、状態遷移図です。まず、今の状態 state による第1の場合分け (外側の if 文) を行います。これは状態遷移図のそれぞれの囲みに注目することに相当します。そこから、今の入力 SW_ON と SW_OFF による第2の場合分け (内側の if 文) を行っています。これは注目した囲みから伸びている矢印を見ることに相当します。こうして2段階の場合分けができたら、次の状態 (矢印の先の囲み) や出力 (囲みや矢印に付記した) を適切な場所に記載していきます。
この途中の 1’b0 とか 1’b1 といった表現は定数です。定数は、ビット数、アポストロフィ、底を示すアルファベット、値の順に並べることで表現します。底は2であれば b、10であれば d、16であれば x を用います。例えば 4’d9 ならば「10進数で9を表す4ビットの定数」という意味です。
always_comb 文で書いているのは組合せ回路の一部ですから、この中でも入力と出力を意識することが大切です。特に、出力にあたる信号 (always_comb 文の中で代入されている信号) に対しては、少なくとも1回は何らかの値が代入されることに注意してください。そうでないと、出力が今の入力だけで決められなくなってしまい、組合せ回路の定義が満たされません。記述例のように、他の代入文が評価されなかった場合の値を always_comb 文の冒頭で定めておくと、間違いが少ないです。
記憶回路
最後の部分は、順序回路のうちの記憶回路部分、つまり D-FF を記述する部分です。これには always_ff 文を使用します。always_ff 文の中での代入には <= 演算子を使います。 この場合、代入の結果は always_ff 文の終わりでまとめて反映されます (D-FFの値の更新は全て同時に起こる、ということに相当します) 。記述例は、クロック信号 CLK の立上りにおいて、リセット信号 RST が ‘1’ であれば状態を初期値に、そうでなければ状態を次の状態へと更新する、という意味になります。
組合せ回路部分と記憶回路部分をはっきり分離しておけば、この部分は決まりきった書き方になるので、それを覚えておけば良いだけです。 順序回路の設計に慣れるまでは、自分がどちらを書いているのかを意識するために、記憶回路の部分をはっきり分離して書くことをおすすめします。
よく使われる順序回路
次に、実用的な回路の部品としてよく使われる順序回路と、その SystemVerilog による記述例を見ていきましょう。
カウンタ
時間を測ったり、繰り返しの処理を行った回数を数えるのに便利な回路がカウンタです。特に、N – 1 まで数え終わったら0に戻るカウンタを N 進カウンタとよびます。
カウンタにはいくつかのバリエーションがあります。ここでは単にクロック入力の立上りの回数を数え、その回数を2進数で出力する10進カウンタの回路を考えます。 この回路は、レジスタ、加算器、比較器、マルチプレクサを用いて、次のブロック図のように表されます。
この10進カウンタの SystemVerilog での記述例は以下のとおりです。
module counter10 (
input logic CLK, RST,
output logic [3:0] COUNT);
logic [3:0] n_count;
always_comb begin
if (COUNT == 4'd9) begin
n_count = 4'd0;
end else begin
n_count = COUNT + 1'b1;
end
end
always_ff @ (posedge CLK) begin
if (RST) begin
COUNT <= 4'd0;
end else begin
COUNT <= n_count;
end
end
endmodule
カウンタの値は最大で9 (2進数で1001) なので、4ビットが必要です。複数ビットの信号は、添字の最大値と最小値を角カッコで示すことで宣言できます。
ブロック図と記述の対応を確認しておきましょう。組合せ回路の部分 (always_comb) では、if ~ else の構造がマルチプレクサ、定数 4’d9 との比較が比較器、定数 1’b1 の加算が加算器に対応しています。記憶回路の部分 (always_ff) はレジスタに対応しています。
シフトレジスタ(シリアル→パラレル)
複数の D-FF を次の図のように直列に並べた回路をシフトレジスタとよびます。例えば、下図は D-FF を3個並べた、3ビットのシフトレジスタです。
D-FF の出力は、その D-FF の1つ前の入力に対応します。これに注目すると、Q0 は1つ前の D、Q1 は2つ前の D (= 1つ前の Q0)、Q2 は3つ前の D (= 1つ前の Q1) に対応していることがわかります。つまり、N ビットのシフトレジスタを使うと、過去 N 回分の D の系列を、N ビットで出力する、直列 (シリアル) データから並列 (パラレル) データへの変換回路が作れます。
このシフトレジスタの SystemVerilog での記述例は以下のとおりです。
module shiftreg_sp (
input logic CLK, RST,
input logic D,
output logic [2:0] Q);
logic [2:0] data_reg, n_data_reg;
assign Q = data_reg;
assign n_data_reg = {data_reg[1:0], D};
always_ff @ (posedge CLK) begin
if (RST) begin
data_reg <= 3'b000;
end else begin
data_reg <= n_data_reg;
end
end
endmodule
信号同士を単に結線する、あるいは単純な組合せ回路を記述するときには、 assign 文を使います。また、カンマで区切った複数の信号を中括弧で囲むと、それらを束ねて1つの信号として扱えます。例えば記述例の9行目は、次のレジスタの値 (data_reg すなわち Q) は、上位ビットから順に Q1, Q0, D である、という意味になります。
シフトレジスタ(パラレル→シリアル)
シフトレジスタはまた、並列 (パラレル) データから直列 (シリアル) データへの変換にも使えます。先ほどはデータを1ビットずつ入力し、複数ビットまとめて出力しました。今度は、データを複数ビットまとめて入力し、1ビットずつ出力します。その回路図を次に示します。
ここでは、入力を書き込むタイミングを指定する書き込み有効入力 WE が追加されています。書き込みの際には、各 D-FF の手前にあるマルチプレクサによって、D が各 D-FF の入力に与えられます。この後は、クロック入力が立上がるたびにデータが Q の方へと順番に (上図の場合 D の最上位ビットから順に) 押し出され、直列に出力されていきます。
このシフトレジスタを SystemVerilog で記述すると、例えば以下に示す通りになります。
module shiftreg_ps (
input logic CLK, RST,
input logic [2:0] D,
input logic WE,
output logic Q);
logic [2:0] data_reg, n_data_reg;
assign Q = data_reg[2];
always_comb begin
if (WE) begin
n_data_reg = D;
end else begin
n_data_reg = {data_reg[1:0], 1'b0};
end
end
always_ff @ (posedge CLK) begin
if (RST) begin
data_reg <= 3'b000;
end else begin
data_reg <= n_data_reg;
end
end
endmodule
出力 Q の定義や、書き込み有効入力 WE による場合分けに注目してください。一方で、シフトレジスタの基本構造は変わっていないので、WE が0の場合の次のレジスタの値の定義 (15行目) や記憶回路の部分 (19~25行目) には変わりがありません。
まとめ
今回は、ディジタル回路、特に順序回路を HDL で記述するときの基礎事項として、次のことを確認しました。
- 順序回路は組合せ回路 + 記憶回路からなること。
- 組合せ回路部分の HDL 記述にあたっては、状態遷移図が有用であること。
- 記憶回路部分の HDL 記述には一定の書き方があること。
- よく用いられる順序回路にカウンタやシフトレジスタがあること。
次回はこれらをもとにして、シリアル通信による文字送信回路の設計と HDL 記述について解説していきます。
愛知工業大学 藤枝直輝