初めての System Verilog

公開日:
初めての System Verilog で気をつけるべきこと

System Verilog はハードウェアを記述するための言語である HDL (Hardware Description Language) の一つですが、ハードウェアを記述するのはソフトウェアを記述するのと異なる部分も多く、初めて書く人は大きく戸惑うのではないかと思います。

筆者もその1人だったので、その経験を踏まえて System Verilogの書き方を以下の実装を参考にしながら、雑多に (?) 解説していこうと思います。
System Verilog でハードウェアを記述できるようになると FPGA 上に実際に実装して動かしたりできるようになるので、頑張ってください 💪

正確に書くというよりはお気持ちみたいな部分もあると思うので、違和感などあればコメントいただけると幸いです 🙏

1. ハードウェアの記述とは

ハードウェアを記述する時とソフトウェアを記述するときの大きな違いは、並列的に考えるか時系列順に考えるかだと思います。
これがハードウェアを記述しようと思ったときに一番つまるポイントだと思います。

例えば、整数を 2 ビットで表した時に 1 が何個あるかを求める popcount という関数を計算するプログラムを CSystem Verilogで書いてみると以下のようになります。(書き方などは後で説明するので、ここでは雰囲気だけ理解してくれれば大丈夫です!)
今回は 4 ビットの整数 x0 = 10 (二進数で書くと 1010) x1 = 7 (二進数で書くと 0111) の popcount (それぞれ 23) を求め、その合計 (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を書くときは並列を意識するという話でしたが、ここでは ans0ans1 を求める計算が並列に行われているということになります。
そして、always 文の外で、ans0ans1を足した値を ans に代入しているとわかります。

ここで、always 文の中はクロックに同期して、1 クロック進むごとに一回実行され、always の外はクロックに関係なく常に実行されています。(常に実行されているというのは少しイメージしにくいかもしれませんが、ハードウェアでは回路線が繋がっているのでずっと値が出力されているということをイメージしてくれればいいと思います。)
つまり、最終的な結果は 4 クロック後に得られるわけで、それまでの ans の値は最終的な値とは異なる値になっています。

これを図にしてみると以下の図のような感じでしょうか。
C では三つの処理が順に実行されていますが、System Verilog では 3 つの処理が並列に 4 回実行されている様子を表してみました。

popcount の実行の様子
popcount の実行の様子

簡単な例で説明しましたが、並列的に考えるということが少しでもイメージしてもらえればいいと思います。

最後に実行方法について説明しておきます。
まず、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 をするときはテストベンチ、その他の回路を実装したファイル (複数ある場合は順不同) の順で記述してください。
xelabxsimtest_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 つの型 regwirelogic があります。
基本的には全て logic を使えばいいとも言われていますが、個人的には regwire を使い分けるのがいいのではないかと思っています。

まず、regwire の違いについて見ていきますが、この違いは明確で、reg はクロックと同期して値が変化する一方、wire は常に値が変化しうるということです。
別の言い方をすると wire は配線に対応し、reg は値を格納する入れ物 (厳密には違うと思いますが、レジスタをイメージしてもいいと思います) に対応するというイメージでもいいと思います。

使い方の違いについては wire は定義する時にしか使わない (C で言う const のように一度定義したら変更することはない) が、reginitial で初期化を行って、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 です。

これまでのことを簡単にまとめると以下のようになると思います。

wirereg
クロックとの同期しないする
イメージ配線入れ物
使い場所定義、assignalwaysfunctiontask
使用する記号=<=
値の更新不可
wirereg の違い

regwire の使い分けについては以下の記事も参考にしてください。

次に、logicについてですが、簡単にいうと、wire でもあり reg でもあるという型です。
C++ でいう auto のように自動で判定してくれるのだと思います。
これは便利なのですが、定義の時以外は wirereg の意識をしっかりして実装をしないと全く動かないので、あまり理解していない人が適当に logic で定義してしまうのは危険なような気がします。
慣れるまでは wirereg の使い分けをしっかり意識しながら実装していくのがいいと思っています。

また、parameterlocalparam のようにパラメータを設定するための型 (?) もあります。
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.svregister #(.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 と同じように値を指定したり、別のwirereg などの値を代入したりすることができます。

初期化は 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 に関してはこの時は値の設定はしないので注意してください。
inputoutput の順番は入れ替わっていても構いません。

次に、このモジュールを呼び出す部分を見ていきます。

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. taskfunction

taskfunction は似ているが、違っている点としては function には返り値 (?) があって、task にはないというところだと思います。
core.sv を見ながら解説していきます。

task flush_ew_reg;
  begin
    rd_data_ew_in <= 32'b0;
  end
endtask

まず、task はこのように task (タスク名); から始まって、beginend を挟んで、endtask までの一塊です。
beginend の間に処理を書きます。
これの利点としては、(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 、入力 (wirereg などは入れないことに注意) 、begin 、処理、endendfunction の順で記述していきます。

処理の中に 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'b1111c = 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 のコードを吐いてくれたりする技術も進んできていますが、高位合成も痒いところに手が届かなかったりするので、どっちもどっちですね。
いずれにせよ、正しい知識を持って自分が強くなるしかないようです… 💪

ちなみに、高位合成を使って回路を作る記事も書いていますので、興味ある方はぜひご覧ください!

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA