System Verilog
はハードウェアを記述するための言語である HDL (Hardware Description Language)
の一つですが、ハードウェアを記述するのはソフトウェアを記述するのと異なる部分も多く、初めて書く人は大きく戸惑うのではないかと思います。
筆者もその1人だったので、その経験を踏まえて System Verilog
の書き方を以下の実装を参考にしながら、雑多に (?) 解説していこうと思います。System Verilog
でハードウェアを記述できるようになると FPGA 上に実際に実装して動かしたりできるようになるので、頑張ってください
正確に書くというよりはお気持ちみたいな部分もあると思うので、違和感などあればコメントいただけると幸いです
1. ハードウェアの記述とは
ハードウェアを記述する時とソフトウェアを記述するときの大きな違いは、並列的に考えるか時系列順に考えるかだと思います。
これがハードウェアを記述しようと思ったときに一番つまるポイントだと思います。
例えば、整数を 2 ビットで表した時に 1 が何個あるかを求める popcount
という関数を計算するプログラムを C
と System Verilog
で書いてみると以下のようになります。(書き方などは後で説明するので、ここでは雰囲気だけ理解してくれれば大丈夫です!)
今回は 4 ビットの整数 x0 = 10
(二進数で書くと 1010
) と x1 = 7
(二進数で書くと 0111
) の popcount
(それぞれ 2
、3
) を求め、その合計 (5
) を求めるプログラムを書いています。
実装は例えば x0
を考えるときは最下位ビットが 1 なら答え ans0
に 1 を足して、x0
を右に 1 論理シフトをするということを繰り返すだけです。
#include <stdio.h>
int main() {
int x0 = 10; // 1010(2)
int ans0 = 0;
for (int i = 0; i < 4; i++) {
ans0 += x0 & 1;
x0 >>= 1;
}
int x1 = 7; // 0111(2)
int ans1 = 0;
for (int i = 0; i < 4; i++) {
ans1 += x1 & 1;
x1 >>= 1;
}
const int ans = ans0 + ans1;
printf("%d\n", ans);
return 0;
}
module popcount
( input wire clk,
output wire [2:0] ans );
reg [3:0] x0;
reg [3:0] x1;
reg [1:0] ans0;
reg [1:0] ans1;
initial begin
x0 <= 4'd10; // 4'b1010
x1 <= 4'd7; // 4'b0111
ans0 <= 2'd0;
ans1 <= 2'd0;
end
assign ans = {1'b0, ans0} + {1'b0, ans1};
always @(posedge clk) begin
ans0 <= ans0 + {1'b0, x0[0]};
x0 <= x0 >> 1;
ans1 <= ans1 + {1'b0, x1[0]};
x1 <= x1 >> 1;
end
endmodule
実行方法は後で説明しますが、これを見るとわかるように、C
のような手続き型言語では動作を順番に上から書いていけばいいですが、System Verilog
では少し異なっています。
実装の方法はもちろんこれだけではないですが、今回の実装では always
文の中で C
でいう for
文の中身が入っているということがわかります。System Verilog
を書くときは並列を意識するという話でしたが、ここでは ans0
と ans1
を求める計算が並列に行われているということになります。
そして、always
文の外で、ans0
と ans1
を足した値を ans
に代入しているとわかります。
ここで、always
文の中はクロックに同期して、1 クロック進むごとに一回実行され、always
の外はクロックに関係なく常に実行されています。(常に実行されているというのは少しイメージしにくいかもしれませんが、ハードウェアでは回路線が繋がっているのでずっと値が出力されているということをイメージしてくれればいいと思います。)
つまり、最終的な結果は 4 クロック後に得られるわけで、それまでの ans
の値は最終的な値とは異なる値になっています。
これを図にしてみると以下の図のような感じでしょうか。C
では三つの処理が順に実行されていますが、System Verilog
では 3 つの処理が並列に 4 回実行されている様子を表してみました。
簡単な例で説明しましたが、並列的に考えるということが少しでもイメージしてもらえればいいと思います。
最後に実行方法について説明しておきます。
まず、C
については説明不要かもしれませんが、以下のようにすればいいです。
$ gcc popcount.c && ./a.out
2
次に、System Verilog
については回路があるだけでは実行ができないので、回路を動かすための テストベンチ
を書いてあげる必要があります。(この辺りは少し面倒なところです…)
少し長くなりそうなので、次の章で説明します。
2. テストベンチ
先程途中で止まってしまった System Verilog
のテストベンチの書き方を紹介します。
とりあえず、筆者が書いたテストベンチは以下のようになりました。
テストベンチも System Verilog
で書くので少しややこしいのですが、回路の記述とテストベンチの記述は少し異なります。
共通する部分については後で説明しますが、ここではテストベンチ特有の部分を説明していきます。
`timescale 1ns / 100ps
`default_nettype none
module test_popcount();
logic clk;
wire [2:0] ans;
int max_clocks = 4;
int i, j;
popcount _popcount(clk, ans);
initial begin
$display("############### start of checking module popcount ###############");
clk = 0;
for (i = 1; i <= max_clocks; i++) begin
for (j = 0; j < 2; j++) begin
#10
clk = ~clk;
end
$display("(clocks, ans) : (%2d, %2d)", i, ans);
end
$display("################# end of checking module popcount ###############");
$finish;
end
endmodule
`default_nettype wire
まず、1 行目のtimescale 1ns / 100ps
では 単位 / 精度
の形でタイムスケールを指定しています。
精度は割り算をするときなどに使うようですが、あまり使用する場面を見たことがない気がします。
単位については、#(時間)
で経過時間を指定する時に使われますが、例えば、18 行目には #10
とあるので、タイムスケールの 10 倍の時間が経つごとに for
の中身を実行するということを意味しています。timescale
の解説は以下の記事も参考になると思います。
次に、initial
の中ですが、ここでは基本的に先程説明した for
と #(時間)
を使用して、クロックを進めながら処理を行っていきます。
全ての処理が終わったら $finish
で処理の終了を通知します。
また、テストベンチでは $display
とすることで、変数の値や文字列を表示することができます。
出力の方法は C
と似ていて、%d
などとしてフォーマットを指定して出力することができます。
例えば、ビット列 x
を単精度浮動小数点 (shortreal
) として出力したいときは $display("%e", $bitstoshortreal(x));
のように型変換をしてから出力するとうまくいきます。
また、%05d
などのように 0 埋め5桁の表示なども指定できます。
ただし、$display
は末尾に自動的に改行が入るので、改行を入れたくない場合などは $write
を使うといいです。
その他のフォーマットや類似の関数は以下のような記事が参考になると思います。
このようにしてテストベンチを書いた後は Vivado
のシミュレータを使って動かしてみましょう。
以下の三つのコマンドを順に実行すれば OK です。xvlog
をするときはテストベンチ、その他の回路を実装したファイル (複数ある場合は順不同) の順で記述してください。xelab
や xsim
の test_popcount
の部分はテストベンチのファイルの名前に合わせておくといいと思います。
$ xvlog --sv test_popcount.sv popcount.sv
INFO: [VRFC 10-2263] Analyzing SystemVerilog file "/path/to/test_popcount.sv" into library work
INFO: [VRFC 10-311] analyzing module test_popcount
INFO: [VRFC 10-2263] Analyzing SystemVerilog file "/path/to/popcount.sv" into library work
INFO: [VRFC 10-311] analyzing module popcount
$ xelab -debug typical test_popcount -s test_popcount.sim
Vivado Simulator v2021.1
Copyright 1986-1999, 2001-2021 Xilinx, Inc. All Rights Reserved.
Running: /gopt/Xilinx/Vivado/2021.1/bin/unwrapped/lnx64.o/xelab -debug typical test_popcount -s test_popcount.sim
Multi-threading is on. Using 126 slave threads.
Starting static elaboration
Pass Through NonSizing Optimizer
Completed static elaboration
Starting simulation data flow analysis
Completed simulation data flow analysis
Time Resolution for simulation is 100ps
Compiling module work.popcount
Compiling module work.test_popcount
Built simulation snapshot test_popcount.sim
$ xsim --runall test_popcount.sim
****** xsim v2021.1 (64-bit)
**** SW Build 3247384 on Thu Jun 10 19:36:07 MDT 2021
**** IP Build 3246043 on Fri Jun 11 00:30:35 MDT 2021
** Copyright 1986-2021 Xilinx, Inc. All Rights Reserved.
source xsim.dir/test_popcount.sim/xsim_script.tcl
# xsim {test_popcount.sim} -autoloadwcfg -runall
Time resolution is 100 ps
run -all
############### start of checking module popcount ###############
(clocks, ans) : ( 1, 1)
(clocks, ans) : ( 2, 3)
(clocks, ans) : ( 3, 4)
(clocks, ans) : ( 4, 5)
################# end of checking module popcount ###############
$finish called at time : 80 ns : File "/path/to/test_popcount.sv" Line 23
exit
INFO: [Common 17-206] Exiting xsim at Sat Jul 31 14:28:52 2021...
結果は 1 クロックずつ表示してみましたが、正しい結果になっていることがわかると思います。
このようにしてテストベンチを作って、テストを行っていきましょう。
いちいちコマンドを三つ実行したりするのは面倒だと思うので Makefile
を作ったりすることをお勧めします。Makefile
の作り方やデバッグの方法などについては以下の記事で説明していますので、ご覧ください。
3. System Verilog の書き方
前置きがかなり長くなってしまいましたが、ここからいよいよ本題である System Verilog
の書き方について説明してきたいと思います。
文法などについては C
に似ているところも多いと思いますし、これまで説明なしにいくつかコードを見せてきたので少しは慣れてきているでしょうか…?
ここからは、一番初めに紹介した GitHub のページを元に説明をしていくので適宜参照してください。
3.1. 型
System Verilog
では主に 3 つの型 reg
、wire
、 logic
があります。
基本的には全て logic
を使えばいいとも言われていますが、個人的には reg
と wire
を使い分けるのがいいのではないかと思っています。
まず、reg
と wire
の違いについて見ていきますが、この違いは明確で、reg
はクロックと同期して値が変化する一方、wire
は常に値が変化しうるということです。
別の言い方をすると wire
は配線に対応し、reg
は値を格納する入れ物 (厳密には違うと思いますが、レジスタをイメージしてもいいと思います) に対応するというイメージでもいいと思います。
使い方の違いについては wire
は定義する時にしか使わない (C
で言う const
のように一度定義したら変更することはない) が、reg
は initial
で初期化を行って、always
文、 function
文、task
文などで代入を行って値を更新していくことができます。wire
は配線なので、一度決めたら実行中に配線が変わることはないが、reg
は入れ物なので、違う値を入れていくことができると考えるとわかりやすいと思います。
ただし、module
の入出力に wire
が使われる場合 (popcount
のときの ans
) は、定義する時に配線を指定できないので、assign
文を使って配線を指定します (popcount
のときの 18 行目の assign ans = {1'b0, ans0} + {1'b0, ans1};
)。
また、wire
の定義の時には =
を使用し、reg
の代入の時には <=
を使用します。reg
については 1 クロックにつき 1 回しか代入できないので、同じものに always
文内などで 2 回代入されているときは思った通りの挙動をしないはずです。
1 回しか代入できない理由は、並列動作とずっと行っていますが、reg
への代入は上から順に起こるわけではなく同時に起こるため、2 回以上代入をしている場合はどちらの値が代入されるかわからないからです。(基本的には下に記述した方が代入されているような気もしますが、その場合上は代入されていない不要な代入になるため、想定とは違う挙動になると思います。)
また、always
文の中の reg
の読み出しと書き込みは読み出しが書き込みより先に起こると考えて、処理を記述すれば OK です。
これまでのことを簡単にまとめると以下のようになると思います。
wire | reg | |
---|---|---|
クロックとの同期 | しない | する |
イメージ | 配線 | 入れ物 |
使い場所 | 定義、assign | always 、function 、task |
使用する記号 | = | <= |
値の更新 | 不可 | 可 |
wire
と reg
の違いreg
とwire
の使い分けについては以下の記事も参考にしてください。
次に、logic
についてですが、簡単にいうと、wire
でもあり reg
でもあるという型です。C++
でいう auto
のように自動で判定してくれるのだと思います。
これは便利なのですが、定義の時以外は wire
と reg
の意識をしっかりして実装をしないと全く動かないので、あまり理解していない人が適当に logic
で定義してしまうのは危険なような気がします。
慣れるまでは wire
と reg
の使い分けをしっかり意識しながら実装していくのがいいと思っています。
また、parameter
や localparam
のようにパラメータを設定するための型 (?) もあります。parameter
は外部の module
からも参照できますが、localparam
は参照できないという違いがあります。
パラメータをモジュール内で定義するときは、parameter hoge = 32'd4;
のようにすれば良いです。
モジュールがパラメータを受け取るときは以下のようにポート宣言の前に #(parameter パラメータ名 = デフォルト値)
の形で書いておき、パラメータを渡すときは module2 #(.hoge(32'd4))
のようにしてパラメータを指定します。(モジュールの定義方法は次の章で解説します。)
module module1 (// ポート宣言);
module2 #(.hoge(32'd4)) _module2(// ポートの指定);
// 他の処理
endmodule
module module2 #(parameter hoge = 32'd0) (// ポート宣言);
// 処理
endmodule
core.sv の register #(.MEM_SIZE(MEM_SIZE)) _register
から register.sv の module register #(parameter MEM_SIZE = 32'd1024)
に対してパラメータを渡したりしているので、参考にして見てください。
パラメータの設定については詳しくはこちらも参考になると思います。
3.2. 定義・初期化・参照
3.2.1. wire
wire
は一度定義したらその後変更したりできないので、一文で必ず定義します。
そのために三項演算子を多用することになると思います。
例えば、32 ビット整数 x
がゼロかどうか判定するには以下のようにします。
wire x_is_zero = (x == 32'b0) ? 1 : 0;
Verilog
では真なら 1、偽なら 0 にするので、(条件) ? 真の時の値 : 偽の時の値
のフォーマットに従って書くとこのようになるとわかります。(もっと簡単に wire x_is_zero = (x == 32'b0);
としてしまえばいいのですが、練習です。)
alu.sv にあるように複数の三項演算子を使って連続して書いたり、入れ子にして書いたりすることももちろんできます。
また、wire
の長さも指定してあげないといけないのですが、wire [31:0] rd_data
のように wire
と 名前
の間に入れてあげます。
この場合は 31 ビット目から 0 ビット目までが左から右に並んでいるので、32 ビットのデータだとわかります。
ただし、1 ビットのデータの場合は先程の wire x_is_zero
のように省略することができます。
さらに、数字を指定する場合は以下のように (ビット数)(クォーテーション)(フォーマット)(値)
の順で並べて書きます。
wire [32:0] zero = 32'b0; // b : ビット列のフォーマット
wire [2:0] six = 3'b110;
wire [7:0] byte = 8'd255; // d: 10 進数のフォーマット
値を参照する時は zero[0] (=1'b0)
、six[2:1] (=2'b11)
のようにして、1 ビットずつでも複数ビット同時にでも参照することができます。
3.2.2. reg
(多次元配列を含む)
定義の時は値を指定しませんが、代入の時は先程の wire
と同じように値を指定したり、別のwire
や reg
などの値を代入したりすることができます。
初期化は register.sv にあるように、initial begin
から始まり end
で終わる部分の内部で行っています。
この中では for
文を使って初期化を行うこともできますが、for
の中身は全て同時に行われることに注意してください。
代入は同様に、always @(posedge clk) begin
から始まり end
で終わる部分の内部で行っています。
同じ reg
に二度以上代入しないように気をつけてください。
この中に書いた処理はクロックと同期して実行されます。
また、多次元配列については例えば、core.sv にあるように reg [1:0] bht [255:0] [3:0];
のようにして定義できます。
これは 256 x 4
個の 2 ビットの値とみなされるので、bht[255][3][1]
のようにすれば、255 行目、3 列目の値の最上位ビットを取り出すことができます。
3.2.3. 構造体
C
と同じように自分で構造体を定義することもできます。
例えば、def.sv にあるように struct
を使うことで以下のように instructions
という構造体を定義することができます。
typedef struct {
// metadata
reg [4:0] rd_addr;
reg [4:0] rs1_addr;
// 中略
reg is_conditional_jump;
reg is_illegal_instr;
} instructions;
これは instructions instr
のようにして定義でき、instr <= '{ default:0 };
とすることで全ての値を 0 に初期化することができます。
アクセスをする際は、instr.rd_addr
のように.
で繋いであげることで、構造体の中の値にアクセスすることができます。
3.2.4. モジュール
memory.sv を参考にすると module
は以下のように定義できます。
module memory #(parameter MEM_SIZE = 32'd1024)
( input wire clk,
input wire rstn,
input wire [31:0] base,
input wire [31:0] offset,
input wire r_enabled,
output wire [31:0] r_data,
input wire w_enabled,
input wire [31:0] w_data );
// 処理を書く
endmodule
モジュールは module
で始まり endmodule
で終わりますが、module
のすぐ後にそのモジュールの名前 memory
を書きます。
その後ろに必要なら parameter
の設定 (#(parameter MEM_SIZE = 32'd1024)
) を書きます。
これはその後ろのポート宣言と同様に ,
で区切って複数個書くこともできます。
さらに、その後ろにポート宣言を書きます。
入力なら input
、出力なら output
から始め、先程の定義方法に従って、定義していきます。wire
に関してはこの時は値の設定はしないので注意してください。input
、output
の順番は入れ替わっていても構いません。
次に、このモジュールを呼び出す部分を見ていきます。
execute.sv にあるように以下のようにして、memory
モジュールを呼び出すことができます。
memory #(.MEM_SIZE(MEM_SIZE)) _memory
( .clk(clk),
.rstn(rstn),
.base(rs1_data),
.offset(instr.imm),
.r_enabled(instr.is_load),
.r_data(r_data),
.w_enabled(instr.is_store),
.w_data(rs2_data) );
ここで、memory
が呼び出したいモジュールの名前、#(.MEM_SIZE(MEM_SIZE))
がパラメータの指定、_memory
が execute.sv の中での memory
モジュールの呼び方 (これが再利用されているところは今の所見たことがないので適当でもいいと思われる)、最後の部分がポートの対応付けを表しています。
パラメータの指定やポートの対応付けについては、.(ポート名)((ネット名))
の形で指定します。.
から始まる部分がポート名 (memory
モジュールの中での呼び方)、括弧の内部がネット名 (execute.sv での呼び方) を表しています。System Verilog
ではポート名とネット名が同じ時は .*
で省略して書くこともできるらしいが、使用したことはないのでわからないです…
(詳しくは、こちらの記事の「(1) 冗長記述の削除」をご覧ください。)
また、順番を保って書いているのであれば、ポート名を省略して、ネット名だけを ,
で繋いで (clk, rstn, rs1_data, ...);
のように書くこともできます。
楽をするという意味では省略記法はいいかもしれないが、ポートが多いと対応関係がわからなくなったりしそうなので、丁寧にポート名とネット名を全て書くようにしています。 (test_core.sv 以外は)
3.2.5. task
、function
task
と function
は似ているが、違っている点としては function
には返り値 (?) があって、task
にはないというところだと思います。
core.sv を見ながら解説していきます。
task flush_ew_reg;
begin
rd_data_ew_in <= 32'b0;
end
endtask
まず、task
はこのように task (タスク名);
から始まって、begin
、end
を挟んで、endtask
までの一塊です。begin
と end
の間に処理を書きます。
これの利点としては、(flush_ew_reg
はあまりいい task
ではないかもしれませんが) 複数の処理をまとめて記述できるというところにあると思います。always
文の分岐の中で何度も同じ代入操作を書くのは面倒なので、task
でまとめてしまおうということです。
ここでは引数は指定していませんが、後で書く function
と同じように引数を指定することも可能です。(module
と違って output
は指定できないので注意してください。)
output
できないんかい!って思ったところで登場するのが function
です。
これは、返り値 (?) を指定できるので、output
と同じような役割を果たしてくれます。(一つしか返せませんが…)
function [32:0] read_csr(input [11:0] r_addr);
begin
case (r_addr)
12'h300: read_csr = {1'b1, csr.mstatus};
12'h304: read_csr = {1'b1, csr.mie};
12'h305: read_csr = {1'b1, csr.mtvec};
12'h341: read_csr = {1'b1, csr.mepc};
12'h342: read_csr = {1'b1, csr.mcause};
12'h343: read_csr = {1'b1, csr.mtval};
12'h344: read_csr = {1'b1, csr.mip};
default: read_csr = {1'b0, 32'b0};
endcase
end
endfunction
このように、function
、返り値のビット数、function
の名前 read_csr
、入力 (wire
や reg
などは入れないことに注意) 、begin
、処理、end
、endfunction
の順で記述していきます。
処理の中に read_csr =
という部分があると思いますが、function
の名前の後ろに =
をつけて値を代入することで、これが返り値となります。(ややこしい…)
ちなみに、function
の中で他の task
を呼び出したりすることはできないようなので、注意してください。
3.3. 演算
基本的には C
と同じ演算子を使うことで演算ができますが、System Verilog
独特の注意が必要な部分があったりするので、そういうところを中心に解説していきます。
演算については alu.sv を参考にしてください。
3.3.1. ビット長
まず、色々な演算で気をつけないといけないのがビット長です。
とりあえず、整数を使う時は 4’d4
のように常にビット数を指定して書くことを意識し、ビット演算をする時は繰り上がりなどを意識して桁数に余裕を持たせることを意識してください。
確かに面倒だし、非効率に見えるかもしれませんが、非自明なバグを踏んでしまうよりはマシだと筆者は思っています。
wire [3:0] a = 2'd4; // 0
wire [3:0] b = 4'd15; // 15
wire [3:0] c = 4'd4; // 4
wire [3:0] d = (b + c) >> 1; // 1
wire [3:0] e = ({1'b0, b} + {1'b0, c}) >> 1; // 9
//
以降が実際に出力された値を示しています。
a
については 4(10) = 100(2)
なので、3 桁必要だが、2'd4
のように 2 ビットで表現しようとしているため、下位 2 ビットだけが格納されて 0
と出力されています。
この時は、xvlog
のコンパイル結果で、WARNING: [VRFC 10-8497] literal value 'd4 truncated to fit in 2 bits
のように WARNING
を吐いてくれるため比較的気付きやすいと思います。
それ以降は、floor((b + c) / 2) (= 9)
を計算しています。e
については正しい結果が出ているとわかるでしょう。d
についてどのようになっているかというと、b = 4'b1111
と c = 4'b0100
を足すと、b + c = 5'b10011
となるはずなのですが、ビットが拡張されていないので、4 ビットしか取らずに、4'b0011
となってしまいます。
これを右に 1 シフトすると、4'b0001 = 4'd1
となり、出力された答えと一致します。
これらのような、非自明なバグを踏まないようにビット長は常に意識して書くようにしましょう。
3.3.2. 符号付きか符号なしか
次は符号付きで演算をしたいのか符号なしで演算をしたいのかを気をつけることです。
結論から言うと、符号付きで計算したい時は常に $signed
で囲むことを意識してください。
wire [3:0] a = 4'b0111; // 7
wire [3:0] b = 4'b1111; // unsigned なら 15、signed なら -1
wire c = a < b; // 1 (7 < 15 -> True)
wire d = $signed(a) < $signed(b); // 0 (7 < -1 -> False)
簡単な例ですが、b
は符号付きで扱うか符号なしで扱うかで値が変わってしまうので、不等号の判定が思った通りにならない可能性があります。
足し算や引き算したりする時も予期せぬバグを踏んでしまうことがあるので、符号付きで計算したいならしっかりと $signed
をつけましょう。
また、ややこしい例として論理シフトと算術シフトがあります。
問題になるのはもちろん右シフトの時で、論理シフトなら左端に 0 を追加していきますが、算術シフトなら 1 を追加していきます。
wire [3:0] a = 4'b1111;
wire [3:0] b = a >> 1; // 4'b0111 (論理シフト)
wire [3:0] c = a >>> 1; // 4'b0111 (論理シフト)
wire [3:0] d = $signed(a) >> 1; // 4'b0111 (論理シフト)
wire [3:0] e = $signed(a) >>> 1; // 4'b1111 (算術シフト)
b
が標準の論理シフトで思った通りの挙動をしています。
算術シフトをするためには $signed
をつければいいとか、>>>
にすればいいとか言う記事も見かけたりするのですが、どちらかだけでは不十分で論理シフトになってしまいます。(なぜ…)e
のように $signed
にした上で、>>>
にしてあげないと算術シフトにならないので気をつけてください。
3.3.3. 演算子の優先順位
演算子の優先順位にも注意しましょう。
これも、常に思い通りになるように括弧をつけることを意識しておくといいと思います。
wire a = 1'b0;
wire b = 1'b0;
wire c = 1'b1;
wire d = 1'b1;
wire e = a == b ? c : d; // 1
wire f = ( a == b ) ? c : d; // 1 (a == b) なので c が返る
wire g = a == ( b ? c : d ); // 0 (b は False なので、 a == d が返る)
例えば、wire e = a == b ? c : d;
は wire f = ( a == b ) ? c : d;
のように解釈されるので注意してください。
どっちか悩んでミスったり、非自明なバグを踏んで困るぐらいなら、初めから括弧をつけておけば安心ですね!
もっと色々書こうとも思ったのですが、この辺りの気をつけることを書き出したらキリがなくなってしまうので、この辺りにしておきます。
ググっている最中にいい記事を見つけたので、こちらも参考にしてください。
4. まとめ
ここまで雑多に System Verilog
を書くときに気をつけるべきポイントを書いてきました。
改めて書き出してみると、もうちょっと使い勝手がよくならないのか…と思う部分もないことはないのですが、仕方ないですね。
高位合成を使って、C++
などの言語から自動で Verilog
のコードを吐いてくれたりする技術も進んできていますが、高位合成も痒いところに手が届かなかったりするので、どっちもどっちですね。
いずれにせよ、正しい知識を持って自分が強くなるしかないようです…
ちなみに、高位合成を使って回路を作る記事も書いていますので、興味ある方はぜひご覧ください!