LoginSignup
81
70

デザイン向け(論理合成可能)SystemVerilog記述

Last updated at Posted at 2019-11-16

■ はじめに

VerilogはSystemVerilogへとアップグレードされ、多くの機能改善と強化がなされた。Verilogでの弱点はSystemVerilogでほぼ解消されたと言っても過言ではない。しかし、検証面での強化、改善が多く、ネットや本でも圧倒的に検証向けの情報が多い。中にはSystemVerilogを検証言語だと壮大な勘違いしている人もいるぐらいである。デザイン向けのSystemVerilogの情報が不足している様に思う。特に日本語での情報が少ないせいなのか、日本でのデザイン分野での浸透がまだいまいちな様に思う。なのでデザイン向けのSystemVerilogの記事を書いてみようと思った。
上述の様にデザイン向けの改善、強化項目は数える程ではあるのだが、それらは非常に強力で、少し知っていると言うだけで開発効率に雲泥の差が出る。なので今までVerilogしか使って来なかった人はこれを機に是非SystemVerilogを覚えて日々の開発業務に取り入れて役立ててもらいたい。なにも既に存在するVerilogのコードを全ていっぺんに置き換える必要はなく、SystemVerilogは完全に上位互換なので、使えそうな部分から少しずつ取り入れて改善して行けば良いのである。
以下、実用上よく使われ、有用だと思われるSystemVerilogのデザイン向けの記述方法を順次少しずつ書いて行くので是非参考にしてもらいたい。尚、一部Verilog 2001/2005の時点で既に導入されていた物も入れた。

■ logic

logicはassignでもalwaysでの代入でもどちらでも使える。要するにwireとregの両対応版。VHDLのsignalと同等。Verilogではassignの時にはwire,alwaysの時にはregといちいち気にしなくてはいけなかったが、System Verilogではlogicにしておけば一切気にする必要ない。なので今日からはlogicを使ってみよう。今までVerilogしか使っていなくてSystemVerilogを導入したいが何から手を付けていいか分からないという人はまずlogicを使ってみると良いと思う。

logicの使用例
logic a, b, c, d, e;

assign c = a & b;

always_comb
  if (d) e = c;
  else   e = 0;

ただ、wireでできる、宣言と同時にassignを省略しての代入は対応していないようなので注意。その場合はwireを使おう。

OK
wire c = a & b;
NG
logic c = a & b;

■ always_ff, always_comb, always_latch

SystemVerilogでは設計者の意図を明確にする為に順序回路、組み合わせ回路、ラッチそれぞれのalways文が追加された。logicの次はこの辺りを使ってみよう。
それぞれ、always_ffなのに組み合わせ回路の記述、always_combなのにラッチの記述の様に意図しない記述をするとエラーとなるので事前にバグの検出ができる。組み込みのアサーションとも言える。是非積極的に使ってみてもらいたい。

  • always_ff

always_ffはVerilogの順序回路のalwaysと同様に記述する。

always_ffの使用例
always_ff@(posedge clk or negedge rst_n)
  if(!rst_n)    data <= '0;
  else if(en)   data <= in;
  • always_comb

always_combはVerilogのalways@(xxx, yyy, ...)での組み合わせ回路記述のようなセンシティビティリストを記述しなくてもよい。

always_combの使用例1
always_comb
  if(a) b = c;
  else  b = 0;
always_combの使用例2
always_comb begin
  b = 0;
  if(a) b = c;
end

Verilog2001でもalways@*と同等の記述ができるが、always@*では

  • functionの中から参照される信号センシティビティリストに含まれない
  • シミュレーション開始時時刻0で実行されない
    という問題があり、always_combではそれらが解消されているのでこちらを使うようにしよう。
    以下、always@*とalways_combの違いについて「SystemVerilog Logic Specific Processes for Synthesis - Benefits and Proper Usage」 by Sunburst Designより抜粋。
    Capture2.PNG
    又、上述のSunburst Designのpaparにも紹介されているが、System Verilogで導入された返り値がvoidのvoid functionと組み合わせて以下の様な記述ができる。
alwas_combとfunctionの組み合わせ
always_comb begin
  step_a();
  step_b();
  step_c();
end

function void function a();
...
endfunction

function void function b();
...
endfunction

function void function c();
...
endfunction

この記述はalways_combがfunction内で参照する信号もきちんとセンシティビティリストに入れてくれるから可能となる。こうすると何が良いかと言うと、always_combで行う組み合わせ回路ロジックの処理の抽象度を階層化できるのである。上位のalways_combで全体の処理フローが分かり、細かい処理はそれぞれのfunction内で行っている。一部のロジックの変更があった場合は対象のfunction内だけをいじれば良いのである。
この事に限らずまた後々キーワードとして出てくるが、なるべく「抽象度」を上げて、開発効率を上げ人間によるミスを少なくしたいのである。

  • always_latch

ラッチは殆ど使う機会はないと思われるのでここでは省略。

■ .portname, .* module接続

SystemVerilogではmoduleの信号接続の記述をできる限り簡略化できるようになった。Verilogではポート名、ネット名で接続する場合
module_name instance_name ( .portname(netname) );
の様な記述が必要だったが、SystemVerilogではportnameとnetnameが同じであれば(netname)を書かなくてもよくなった。接続情報としては冗長なので。
module_name instance_name ( .portname );
さらに.portnameさえ記述しなくてもよく
module_name instance_name ( .* );
とすればポート名と同じ名前のネットを自動で接続してくれる。
これらは従来の接続方法とも混在可能で、ポート名と違う名前のネットを繋ぎたい所にだけ従来通りの接続方法にすることができる。

block  i_block ( .clk, .rst_n, .port_a(signal_x) );
block  i_block ( .*          , .port_a(signal_x) );

しかし、.*での接続はどの信号が繋がっているかぱっと見分からないのでデザインで使用はあまりお勧めしない。今は亡き懐かしのSTARCの設計スタイルガイドでも.*での接続は禁止しているようだ。

これらの接続方法はポートの数が少ないうちは大して効果はないが、数が膨大になってくるとかなり大きな効果を発揮してくる。

■ interface

SystemVerilogではmodule間接続を簡単にする為に更にinterfaceと言う物が導入された。これはmoduleの入出力信号をまとめた物である。バス信号などは同じ入出力信号の定義がいくつものモジュールで何回もされるので冗長である。なので1度定義しておいて、それをいろいろなモジュールで使い回わせば良いと言う発想である。
バスの信号をまとめて1つのinterfaceで表すと、モジュールの接続にバスの個々の信号を考えなくても良くなる。例えば、バスの信号の種類やビット幅等が変わったとしてもモジュール間の接続は一切変える必要はない。要するに抽象度を1段上げているのである。
以下に実際の使用例を示す。
SV_Interface_1_1.png

bus_if.sv
//---- Definition of the interface ----//
interface #(parameter width=32) bus_if(input logic clk, rst_n);
  logic[ 7:0]      addr;
  logic            wr_rq;
  logic            rd_rq;
  logic[width-1:0] wr_data;
  logic[width-1:0] rd_data;
  logic            rd_en;
  modport mst_port(                  output addr, wr_rq, wr_data, rd_rq, input  rd_en, rd_data);
  modport slv_port(input clk, rst_n, input  addr, wr_rq, wr_data, rd_rq, output rd_en, rd_data);
endinterface
top.sv
module top(input logic clk, rst_n, logic[7:0] cfg_reg, output logic[3:0] flag);

  bus_if #(32) bus1(.clk, .rst_n);  // <-- Instanciate the interface as bus1
  bus_if #(16) bus2(.clk, .rst_n);  // <-- Instanciate the interface as bus2

  master  mst( .clk, .rst_n, .cfg_reg, .flag, .bus1, .bus2 );
  slave  slv1( .bus(bus1) );
  slave  slv2( .bus(bus2) );
  /*------- Another Way --------------------
  master  mst( .clk, .rst_n, .cfg_reg, .flag, .bus1(bus1.mst_port), .bus2(bus2.mst_port) );
  slave  slv1( .bus(bus1.slv_port) );
  slave  slv2( .bus(bus2.slv_port) );
  ------------------------------------------*/

endmodule
master.sv
module master (
 input  logic      clk, rst_n,
 input  logic[7:0] cfg_reg,
 output logic[3:0] flg,
 bus_if.mst_port   bus1,  // <-- Use mst_port of bus_if as port (interface) name bus1
 bus_if.mst_port   bus2   // <-- Use mst_port of bus_if as port (interface) name bus2
/*------- Another Way --------------------
 bus_if   bus1,  // <-- modport mst_port must be assigned in the upper module
 bus_if   bus2   // <-- modport mst_port must be assigned in the upper module
------------------------------------------*/
);

  assign  bus1.addr    = ...
  assign  bus1.wr_data = ...
  assign  bus1.wr_rq   = ...
  assign  bus1.rd_rq   = ...

  assign  bus2.addr    = ...
  assign  bus2.wr_data = ...
  assign  bus2.wr_rq   = ...
  assign  bus2.rd_rq   = ...

  always_ff@(posedge clk or negedge rst_n)
    if(!rst_n)          rd_data <= '0;
    else if(bus1.rd_en) rd_data <= bus1.rd_data;
    else if(bus2.rd_en) rd_data <= bus2.rd_data;
slave.sv
module slave (
  bus_if.slv_port  bus   // <-- Use mst_port of bus_if as port (interface) name bus
/*------- Another Way --------------------
  bus_if bus   // <-- Modport mst_port must be assigned in the upper module
-----------------------------------------*/
);
  slave_sub  sub(.bus);  // <-- Pass the interface to a lower module (implicit port connection)
endmodule
slave_sub.sv
module slave_sub (
  bus_if.slv_port  bus   // <-- Use mst_port of bus_if as port (interface) name bus
);
  assign  bus.rd_en   = ...
  assign  bus.rd_data = ...

  always_ff@(posedge bus.clk or negedge bus.rst_n)
    if(!bus.rst_n)     wdata <= '0;
    else if(bus.wr_rq) wdata <= bus.wr_data;
    else if(bus.rd_rq) ...

bus_ifでバスに使用される各信号を定義している。modportで各moduleで使用される際の方向を指定している。この例ではmodport mst_portでmaster用の信号の入出力の方向を決めている。それと対向するslave用にはmodport slv_portで信号の方向を決めていて、当然mst_portとは反対方向となる。modportはmodule portの事でその名の通りmoduleでのポートの入出力方向を記述している。時々interface自体の入出力と勘違いしている人がいるので注意。interfaceの入出力ではなくinterfaceを繋げる先のmoduleの入出力である。
topでbus_ifで定義されたinterfaceをインスタンス化している(bus1, bus2)。インスタンス化の方法はmoduleと同様でparameterも使える。
こうしてインスタンス化されたinterface(bus1, bus2)をmasterとslaveの各moduleに接続している。接続の方法は通常の信号と同様だが、その方法に2通りある。1つはinterfaceのインスタンス名(bus1, bus2)をそのままmoduleにつなげる方法。もう1つはinterfaceのインスタンス名に続けてmodportを指定して接続する方法。この例のコメントアウトで示したの部分ではmaster向けには.mst_portを付けて、slave向けには.slv_portを付けて接続している。
下位モジュールのmaster及びslaveではinput, outputを使う代わりに定義したinterface名 bus_ifを用いる。その際に上述の上位モジュールでの接続方法によってそれぞれbus_if.mst_port, bus_if.slv_portとmodportを指定するか、modport名なしでbus_ifだけをしていする。要するに上位モジュールか下位モジュールのどちらかでmodportを指定していれば良い。又、両方で指定しても良い。
interfaceが繋がったmodule内では<interface port name>.<signal name>で信号にアクセスできる。この例では例えばmaster側ではbus1.rd_dataやbus1.wr_dataの様に信号にアクセスしている。そしてassignやalwaysで代入や参照する事ができる。
slave側ではさらにもう1階層下のmoduleであるslave_subにbusを渡している。このように下位階層にバスをあたかも1つの信号の様に渡すことができる。階層が深くなってくるとバス信号を渡すのも一苦労なので大変便利である。この事を説明している日本語のページをあまり見かけないので是非覚えておくと良いと思う。
上の例ではinterface内にMasterとSlaveそれぞれに対し1つの一対のmodporを持っていた。しかし、必ずしも一対である必要はなく、modportをいくつ持っても良い。例えば以下の例では一つのinterface内に2つのスレーブポートを持つようにした。それぞれ別名である必要があるのでslv_port1,slv_port2とした。
SV_Interface_2_1.png

bus_if.sv
//---- Definition of the interface ----//
interface #(parameter width=32) bus_if(input logic clk, rst_n);
  logic[ 7:0]      addr1   , addr2   ;
  logic            wr_rq1  , wr_rq2  ;
  logic            rd_rq1  , rd_rq2  ;
  logic[width-1:0] wr_data1, wr_data2;
  logic[width-1:0] rd_data1, rd_data2;
  logic            rd_en1  , rd_en1  ;
  modport mst_port(
    output addr1, wr_rq1, wr_data1, rd_rq1, input  rd_en1, rd_data1,
    output addr2, wr_rq2, wr_data2, rd_rq2, input  rd_en2, rd_data2
  );
  modport slv_port1(input clk, rst_n, input  addr1, wr_rq1, wr_data1, rd_rq1, output rd_en1, rd_data1);
  modport slv_port2(input clk, rst_n, input  addr2, wr_rq2, wr_data2, rd_rq2, output rd_en2, rd_data2);
endinterface

又、interfaceは内部にロジックを持つ事もできる。以下の例ではセレクターを持ってマスタ側のポートの信号を一組にした。
SV_Interface_3_1.png

bus_if.sv
//---- Definition of the interface ----//
interface #(parameter width=32) bus_if(input logic clk, rst_n);
  logic            sel2;
  logic[ 7:0]      addr   ;
  logic[width-1:0] wr_data, wr_data1, wr_data2;
  logic            wr_rq  , wr_rq1  , wr_rq2  ;
  logic            rd_rq  , rd_rq1  , rd_rq2  ;
  logic[width-1:0] rd_data, rd_data1, rd_data2;
  logic            rd_en  , rd_en1  , rd_en1  ;

  assign addr1    = addr   ;
  assign wr_data1 = wr_data;
  assign addr2    = addr   ;
  assign wr_data2 = wr_data;

  always_comb begin
    wr_rq1   = wr_rq  ; 
    rd_rq1   = rd_rq  ;
    if(sel2) begin
      wr_rq1   = 'b0; 
      rd_rq1   = 'b0;
    end
  end 
  always_comb begin
    wr_rq2   = 'b0; 
    rd_rq2   = 'b0;
    if(sel2) begin
      wr_rq2   = wr_rq  ; 
      rd_rq2   = rd_rq  ;
    end
  end 

  assign rd_data  = !sel2 ? rd_data1 : rd_data2;
  assign rd_en    = !sel2 ? rd_en    : rd_en2  ;

  modport mst_port (                  output  sel2 , addr , wr_rq , wr_data , rd_rq , input  rd_en , rd_data );
  modport slv_port1(input clk, rst_n, input          addr1, wr_rq1, wr_data1, rd_rq1, output rd_en1, rd_data1);
  modport slv_port2(input clk, rst_n, input          addr2, wr_rq2, wr_data2, rd_rq2, output rd_en2, rd_data2);
endinterface

・modport expression

interface内で定義した信号名がそのinterfaceが使われるmodule内でも参照信号名として使われるが、interface内の信号名とmodule内での参照信号名とで変えたい場合がある。その場合modeportで信号名を変換する事ができる。又、単に信号名を変えるだけでなく同時にビット選択や連結等の操作や簡単なロジック演算を行う事が出来る。
上記のマスターポート1つスレーブポート2つのinterfaceは以下の様にする事もできる。

modport_expression
interface #(parameter width=32) bus_if(input logic clk, rst_n);
  logic                 sel2;
  logic     [ 7:0]      addr;
  logic     [width-1:0] wr_data;
  logic                 wr_rq  ;
  logic                 rd_rq  ;
  logic[1:2][width-1:0] rd_data; // <-- 2D Array
  logic[1:2]            rd_en  ;
  modport mst_port (output sel2, addr, wr_rq, wr_data, rd_rq, input  .rd_en(!sel2 ? rd_en[1] : rd_en[2]), .rd_ata(!sel2 ? rd_data[1] : rd_data[2]));
  modport slv_port1(input clk, rst_n, input  addr, .wr_rq(!sel2 ? wr_rq : 'b0), wr_data, .rd_rq(!sel2 ? rd_rq : 'b0), output .rd_en(rd_en[1]), .rd_data(rd_data[1]));
  modport slv_port2(input clk, rst_n, input  addr, .wr_rq( sel2 ? wr_rq : 'b0), wr_data, .rd_rq( sel2 ? rd_rq : 'b0), output .rd_en(rd_en[2]), .rd_data(rd_data[2]));
endinterface

と、この様にmodule側で使用したい信号名の前に.を付けてinterface側の信号は()の中に入れる。
この記事とか参照。

なおこの例ではrd_dataのビット分割に分かり易い様に2次元配列を使った。下の方に出てくる多次元Vector/Arrayを参照。

・generic interface

interfaceのmoduleでの宣言方法としてgeneric interfaceと言う物もある。generic interfaceではmoduleのポートの入出力宣言にbus_ifの様な自分で定義したinterface名を使う代わりにinterfaceキーワードそのものを使用する。

generic_interface
module slave_sub ( interface  bus );

この記事とか参照
この様にするとinterfaceを使用する下位moduleではinterfaceの種類を意識しなくて済む。interfaceの種類はinterfaceをインスタンス化する上位のmoduleがインスタンス時に指定する。この様に下位のmoduleがinterfaceの種類を意識しなくて済むと何が良いかと言うとinterfaceの種類を変えたい時に下位moduleは一切変えなくて良いと言う事である。ここでもやはり更に抽象度を上げて柔軟性が上げていると考えられる。電子回路日和ではgeneric interfacceはあまり利点が無いのではと述べているが、それは大いに違うと思う。特にmodule階層が深い大規模な開発でinterfaceを変更したい場合、通常のinterfaceでは下位のmoduleでのポート宣言のinterface名を全て変えなくてはならずとても大変である。それに対してgeneric interfaceであれば下位moduleのポートでは全てinterfaceで宣言されているので変えなくて良いのである。むしろ通常のinterface名を使ったポート宣言方法の方が不要なんじゃないだろうかと思う。一体なぜそんな物作ったのだろう。せっかくの抽象度を上げる手段なのに台無しである。moduleのポート宣言もgeneric interfaceの方が理解し易いし、generic interfaceだけで良いのではないかと思う。なのでinterface名を使用した通常のポート宣言よりこのgeneric interfaceの方の使用を強くおススメする。

因みに以下の記事ではこのgeneric interfaceを使用して上位からの下位moduleの機能の変更方法を紹介している。

generic interfaceの高抽象度に起因する柔軟性を利用しているのである。

・Assertion

interface内にはアサーションを書く事ができる。プロトコルが決まったバスをinterfaceでまとめた場合等は非常に有効なので是非interfaceにアサーションも入れてみてもらいたい。アサーションに関しては下で紹介しようと思う。

■ package

packageは複数のmodule間で定数や定義などのリソースを共有する為に使用する。VHDLでは同じくpackageというのがありお馴染みだが、Verilogでは無かった概念で、SystemVerilogでようやく導入された。なのでVerilogしか知らない人には最初理解に戸惑う人もいる様だ。要するにいろんな所(module)で共通で利用できるライブラリだ。C++でのnamespaceと同等。
あまり知られてない様だがデザイン向けにもpackageを使う事ができる。デザイン向けには主にはparameter, localparam, function等をpackageに入れる事ができる。それから後述する typedef struct, typedef enumなんかも入れる事ができる。 Assertionを使用する場合にはpropertyの定義なんかもpackageに含めることができる。デザイン向けには値が変わる変数は入れることはできない。ただしあまり必要性はないかもしれないが、const設定された変数は使用可能ではないかと思われる。固定値は通常parameterかlocalparamを使用する。以下、packageの例。

common_lib_pkg.sv
`ifndef COMMON_LIB_PKG_SV
`define COMMON_LIB_PKG_SV
package common_lib_pkg;

  parameter  p_NUM_CHANNEL = 8;

  function automatic logic[3:0]  calc (input logic[7:0] a,b);
    if(a[7])    calc = a + b;
    else        calc = 0;
  endfunction

endpackage
`endif

この例ではパラメータp_NUM_CHANNELとcalcというfunctionをpackageに入れている。デザインに使用するpackage内のfunctionにはautomaticが必須。コールされる度に別インスタンスとなるので。
因みにここで使用している`ifndef, `define, `endifはインクルードガードと言ってC/C++や他ソフトウエア言語ではよく用いられる。ヘッダファイルの様に複数のファイルでincludeされた場合に、重複コンパイルを避けるために最初にincludeされた記述のみ有効にして他は無効にする為だ。packageは上述のようにライブラリとして用いられるので、Cでのライブラリの使用方法と同様に、使用するmoduleファイルの先頭でincludeされたりする場合がある。そのような使われ方をした場合に重複コンパイルでトラブルとなる事を避ける為に入れておくと無難である。includeせず、コンパイルリストにpackageのファイルも追加して、他moduleのファイルと一緒にコンパイルする場合にはインクルードガードは必要無いが、一応入れておいた方が良いだろう。又、他moduleと一緒にコンパイルする時にはそのpackageを使用するmoduleより先にコンパイルされている必要があるので要注意。
packageの使用方法については以下で説明する。

・package内リソースへのアクセス方法

packageの使用方法(リソースへの参照方法)は以下の2通りがある。

  1. import <package name>::<resource name>で暗黙的に参照
  2. その都度明示的に<package name>::<resource name>で参照

どちらもスコープ演算子::を用いてpackage内のリソースを指定している。

 1. importで暗黙的に参照

一度import <package name>::<resource name>を宣言するとそのpackageの指定したリソースへは以降特に何も指定しなくても参照が可能となる。リソースの指定にはワイルドカード*を使ってすべてにアクセス可能とする方法と、リソース名を個別に指定する方法の2つがある。

  • import <package name>::*;
  • import <package name>::<resource name>;

module内のどこでもimport宣言できるが、通常はmoduleの先頭で行い、そのmodule内であればどこでもそのpackageのリソースへアクセス可能にしておく。

import宣言で暗黙的にpackageリソースへアクセス
module  example1  (input logic in_a, in_b, output out_z);
  import common_lib_pkg::*;         // All resouces inside common_lib_pkg are available after this in this module.
  import special_pkg::p_BUS_WIDTH;  // Only p_BUS_WIDTH inside special_pkg is available after this in this module.

  logic[p_BUS_WIDTH-1:0] data;

ポートの信号にもpackage内で定義したリソースを使いたい場合がある。その場合にはmodule名宣言の直後、パラメータ、ポート宣言の前にimport宣言を置く事もできる。

module宣言の直後でimport宣言
module  example2
  import common_lib_pkg::*;
  import special_pkg::p_BUS_WIDTH;  // p_BUS_WIDTH is used for output bus width.
  #(parameter p_ID = 0)
  (input logic in_a, in_b, output  logic[p_BUS_WIDTH-1:0] out_z);

  logic[p_BUS_WIDTH-1:0] data;

importはmodule内だけでなく、package内で使用してpackageから更に別のpackageのリソースへアクセスする事も可能。
更にmoduleの外のグローバル領域でもimport宣言が可能ではあるがそれはお勧めしない。何故なら全てのmoduleでpackage内のリソース名との名前の競合を気にしなくてはならないからである。せっかく名前の競合を避けたくてpackageを使ったのにそれでは本末転倒である。だったら初めからpackageなんか使わずに裸でグローバル領域に置けばいいのである。やはりimportを使うのはmoduleやpackageの中でのみにした方が良いだろう。
import <package name>::*;は丁度C++でのusing namespace <space name>;と同等なのである。packageの意味もnamespaceと捉えた方がが分かりやすいのではないだろうか。

 2. ::で明示的に参照

importを使わずともスコープ演算子::を使用してpackage内のリソースに直接アクセスする事ができる。以下その例。

明示的packageリソース参照
module  example2
 #(parameter p_ID = 0)
  (input logic in_a, in_b,
   output  logic[special_pkg::p_BUS_WIDTH-1:0] out_z  // p_BUS_WIDTH is referred without import
  );

  logic[special_pkg::p_BUS_WIDTH-1:0] data;  // p_BUS_WIDTH is referred without import

この例の様に、当然ポートリスト宣言の信号でもmoduleの内部の信号でもどこでも使える。importに対する利点としてはmodule内の名前との競合が起きない点がある。ただしimportと違い、参照する度にその都度<package name>::を書く必要があるので参照する回数が多いと面倒になってくるという欠点がある。
::も丁度C++でのnamespace内の変数へのアクセスに使うそれと全く同じである。

・export

importの他にexportと言う物ある。これはpackageの中で使い、他のpackageのリソースをそのpackage内に展開して、あたかもそのpackagegのリソースの様に扱いたい場合に使用する。なのでexportしたリソースは展開された先のpackage名で外からも参照する事ができるのである。importではそのpackage内でのみしか参照できない。exportを使うとpackageを階層構造にすることができる。

top_pkg.sv
package top_pkg;
  parameter p_WIDTH = 8;
  parameter p_DEPTH = 256;
endpackage
top.sv
module top import top_pkg::*;
  (input logic[p_WIDTH-1:0] a,b, output logic z);

  sub i_sub (.a, .b, .z);
sub_pkg.sv
package sub_pkg;
  export  top_pkg::p_WIDTH; // <---- Borrow p_WIDTH from top_pkg
  parameter  p_NUM_CHANNEL = 8;
endpackage
sub.sv
module sub
//import top_pkg::*; // <---- This is not necessary.
  import sub_pkg::*;
  (input logic[p_WIDTH-1:0] a,b, output logic z);

この様にするとsub以下ではtop_pkgを参照せずともsub_pkgの参照のみで良くなる。
exportを説明している日本語のページをあまり見かた事がないので是非参考にしてみてもらいたい。
尚、とあるLint Checkerでexport::*;としたら何故かexportされた筈のリソースをきちんと認識してくれなかったのでexortでワイルドカードを使ってはいけないのかもしれない。その場合はexportではワイルドカードを使わず、個別でexportをしよう。

■ typedef

typedefはC言語などではお馴染みだがSystemVerilogにも導入された。ユーザーがデータのタイプ名を新たに定義でき、使うことができる。VHDLではsubtypeが同様の物。例えば

typedefの例
typedef  logic[15:0]  byte2_t;

byte2_t  signal_A;
assign singal_A = 'hAABB;

の様にtypedefでlogic[15:0]と言うタイプにbyte2_tと言う別名を定義する。この定義したbyte2_tと言うタイプでsignal_Aを宣言した。signal_Aにはlogic[15:0]で宣言したのと同様に代入、参照ができる。
実用上はlogic[15:0]をtypedefでユーザータイプ定義することは殆ど意味がない。実際には以下で紹介するstructenumと一緒に使う事が殆どだろう。C言語ではよくunsigned intをuintとかuint32とか定義したりする。

■ struct

structもC/C++ではお馴染み。VHDLではrecordに相当。いくつかの信号をまとめた物。

structの例
  struct {
    logic[7:0]  data;
    logic[3:0]  addr;
    logic[7:0]  bit_en;
    logic       write_en;
  }  bus_signals;

この例ではdata, addr, bit_en, write_enと言う信号をひとまとめにしてbus_signalsというひと塊のstruct信号にした。structのメンバへの信号の代入と参照には.でアクセス可能。

structアクセス例
 assign  bus_singals.data     = 'hFF;
 assign  bus_signals.addr     = 'hA;
 assign  bus_signals.bit_en   = 'hF;
 assign  bus_signals.write_en = signal_A; 

 assign  write_data = bus_signals.data;

又、以下の様な代入方法も可能である。

structへの一括代入
 assign  bus_signals = '{ data : 'hFF, addr : 'hA, bit_en : 'hF, write_en : signal_A};

又、この代入方法ではすべて同じ値を入れるdefaultが使える。

structへのdefaultでの代入
always_ff@(posedge clk or negedge rst_n)
  if(!rest_n)
    bus_signals <= '{ default : 0, addr : 'hF};

この例の様にdefaultの値以外を入れたい場合はそのメンバだけ別の値を指定する事もできる。
同じstruct同士であれば一括で直接代入も可能。

struct一括直接代入
  struct {
    logic[7:0]  data;
    logic[3:0]  addr;
    logic[7:0]  bit_en:
    logic       write_en;
  }  bus_signals, bus_signals_pre;

always_ff@(posedge clk or negedge rst_n)
  if(!rest_n)    bus_signals <= '{ default : 0};
  else           bus_signals <= bus_signals_pre;

ここでpackedと言う概念を紹介する。packedを指定するとstructのそれぞれのメンバーはメモリ上、隣り合って配置される事になる。なのでreg/wire/logcのベクターと同じ扱いになりビット位置の番号でアクセスもできる。

structのpacked
struct packed {logic[7:0] data; logic[3:0] addr; logic[7:0] bit_en; logic write_en;}  bus_signals;

 assign  bus_signals[20:13] = 'hFF;      // data
 assign  bus_signals[12: 9] = 'hA;       // addr
 assign  bus_signals[ 8: 1] = 'hFF;      // bit_en
 assign  bus_signals[    0] = signal_A;  // write_en

この例では分かりやすいようにstructを改行せず一行で書いてみた。左側のメンバが上位ビットに割り付けられる。改行して書く場合には上に書かれるメンバが上位ビットになる。ベクターと同じ扱いなので、structをベクターに代入したり、ベクターをstructに代入する事も可能になる。

structのvector代入参照
struct packed {logic[7:0] data; logic[3:0] addr; logic[7:0] bit_en; logic write_en;}  bus_signals;
logic [20:0]  a, b;

 assign  bus_signals = a; 
 assign  b = bus_signals;

このようにpackedにする方ができる事が多く何かと便利なのでデザインでは特に理由がなければpackedを使うようにしよう。STARCの設計スタイルガイドでもunpackedの使用を禁止している(参照)
さて、このstructだが、複数の箇所で宣言する場合いちいちその宣言ごとにメンバーの定義をしなくていはならない。それは非常に面倒である。なので実際にはtypedefと合わせて使う事が殆どである。以下の例の様にmodule内の一か所でstructをtypedefでbus_strとして定義しておけばそのmodule内のどこででもbus_strを複数回宣言する事ができる。

typedefでstructを定義
typedef struct packed {logic[7:0] data; logic[3:0] addr; logic[7:0] bit_en; logic write_en;}  bus_str;

bus_str  signals_a;
bus_str  signals_b;
bus_str  signals_c;

そしてさらに、module間でも同じstructを共有したい。そこですでに紹介したpackageを使うのである。以下の様にpackage内でtypedefを使ってstructの定義をする。

common_pkg.sv
package common_pkg;
  typedef struct packed {logic[7:0] data; logic[3:0] addr; logic[7:0] bit_en; logic write_en;}  bus_str;
endpackage

そしてその定義したタイプを各moduleで共通して参照するのである。参照の仕方は既に紹介したようにimport宣言1回で以降は暗黙的に参照可能にしても<package name>::で毎回個別に明示的に参照してもどちらでも良い。

block_a.sv
module(input clk, rst_n, input  common_pkg::bus_str bus_signals);
block_b.sv
module(input clk, rst_n, output common_pkg::bus_str bus_signals);
top.sv
module(input clk, rst_n);
  import  common_pkg::*;
  bus_str bus_signals;
  block_a a(.clk, .rst_n, .bus_signals);
  block_b b(.clk, .rst_n, .bus_signals);

この例の様にmoduleの内部信号だけでなく当然入出力信号のタイプにも使えるのである。つまりinterfaceで信号をまとめたのと同様にstructでも入出力信号をまとめる事が出来るのである。interfaceと違いstructは単にまとめるだけなのでポートの入出力の方向はinputかoutputの一方向だけである。そして当然moduleのportに使用する場合にはinputかoutputを付ける必要がある。このようにstructをpackageと合わせてmoduleの入出力信号に使うと簡易的なinterfaceっぽい使い方ができる。
interfaceと同様にstructで同じ仲間の信号をグループとしてまとめると言う事はやはり「抽象度」を上げていると言う事なのである。そのまとめた信号グループ単位で一括処理できたり、名前でアクセスできるからである。信号に限らず処理なども同じ仲間でまとめると言う事はやはり「抽象度」を上げているのである。因みに余談だがSystemVerilogにもあるオブジェクト指向プログラミングのclassも関数や変数を仲間でまとめて抽象度を上げているのである。

■ union

unionはC/C++のそれと同様。structと似ているがstruct程使用頻度は高くないだろう。unionもstruct同様にいくつかの信号を一つにまとめるのだが、structが別々の信号としてまとめるのに対しunionでは同じ信号線を共有する。信号を共有するので、同じ信号を違う名前で参照できるのである。例えば信号DATA_AとDATA_Bがあったとして以下の様にunionを定義してみる。

unionの例(合成不可)
union {
  logic[31:0]  DATA_A;
  logic[16:0]  DATA_B;
}  united_data;

メンバへのアクセスはstructと同様にunited_data.DATA_A、united_data.DATA_Bと.と名前でアクセスするがstructとの違い、このunited_dataは32ビットの信号となり、united_data.DATA_Aはその32ビットのすべてビット、united_data.DATA_Bはその32ビットのうちの16ビットの信号線を表す。
ただし、この例では合成は不可で、合成可能なデザイン向け記述にするにはunionの各メンバが同じビット幅でかつpackedで宣言する必要がある。

unionの例(合成可能)
union packed {
  logic[31:0]  DATA_A;
  logic[31:0]  DATA_B;
}  united_data;

しかし、この例では実用的ではない。同じlogic[31:0]の信号にDATA_Aと参照しようがDATA_Bと参照しようがどちらも大した違いが無いからである。unionを使う意味が出てくるのは各メンバのタイプが違う場合、特にstructで定義されたタイプを使う場合である。

unionの実用的な例
typedef union packed {
  logic     [31:0]  DATA_32 ;
  logic[0:3][ 7:0]  DATA_8x4;
  struct packed {logic[15:0]  opcode; logic[7:0]  data1; logic[7:0]  data2;    }  CMD;
  struct packed {logic[15:0]  addr  ; logic[15:0]  data;                       }  ADD_DATA;
  struct packed {logic[ 7:0]  mode  ; logic[7:0] R; logic[7:0] G; logic[7:0] B;}  COLOR;  
  struct packed {logic[ 7:0]  mode  ; logic[7:0] Y; logic[7:0] U; logic[7:0] V;}  COMPONENT;  
}  data_set_32bit_t

この例の様にunionもstruct同様typedefで定義できる。そして以下の様に用いる。

union使用例
data_set_32bit_t  data_set;

always_comb
  if(...)
    data_set.DATA_32       = 'hABCD;
  else if(...) begin
    data_set.DATA_8x4[0]   = 'hA;
    data_set.DATA_8x4[1]   = 'hB;
    data_set.DATA_8x4[2]   = 'hC;
    data_set.DATA_8x4[3]   = 'hD;
  end
  else if(...) begin
    data_set.CMD.opcode    = 'b1111_1111_0000_0001; 
    data_set.CMD.data1     = 100; 
    data_set.CMD.data2     = 200;
  else if(...) begin
    data_set.ADD_DATA.addr = 'hB5;
    data_set.ADD_DATA.data = 'h33;
  end
  else if (...)
    data_set.COLOR         = '{mode : 0, R : 255, G :  0, G : 255};
  else if (...)
    data_set.COMPONENT     = '{mode : 1, Y : 100, U : 30, V :  50};

これらはすべて同じ32ビットの信号線のdata_setに対して、場合によって違う名前や方法で代入しているのである。
ここでは使用しなかったが以下で説明するenumも入れる事もできる。opcodeやmodeにはenumを使用すべきであろう。そして当然struct同様packageに入れる事ができる。

■ enum

enumは列挙(enumeration)タイプの事である。C言語やVHDLにもありSystemVerilogでも導入された。信号の値を数値で表すのではなく名前で表したいときに使う。具体的には何かのモードやステートマシーンのステートを記述するのが典型的な使い方だ。

enumの記述例1
enum logic[1:0] {READ, WRITE, SLEEP, NOP}  command_mode;

always_comb begin
  command_mode = NOP;
  if     (xxx) command_mode = READ;
  else if(xxx) command_mode = WRITE;

このようにenumの後にその信号に必要なビット数をlogicタイプで書いてその後にそのenumで識別する任意の名前を列挙していく。そしてその後にこれを使う信号名を書く。この例ではREAD, WRITE, SLEEP, NOPの4つの状態を識別する信号command_modeをenumで宣言した。4つの状態なので2ビットのlogic[1:0]とした。このlogic[1:0]を省略するとint(2値,32ビット)になるのでテストベンチではそのような記述でも良いがデザイン向けには必ず指定するようにしよう。
enumで信号を宣言すると、その信号への代入や参照には数値ではなく列挙した名前で行う。この例ではcommand_modeにNOP,READ,WRITEを代入している。
これらの列挙名には実際の数値が左から0,1,2,3...と自動的に割り当てられる。もしその値の割り当てを変えたい場合には以下の様にする。

マニュアルでの数値割り当て
enum logic[1:0] {READ = 'b10, WRITE = 'b01, SLEEP = 'b11, NOP = 'b00}  command_mode;

特に決まった数値があるわけでは無ければあえてマニュアルで割り当てる必要はない。
以下はステートマシーンの記述にenumを使用した例。

ステートマシーンでのenum
enum logic[2:0] {READY, WAIT, LOARD, EXEC, ABNORMAL } state;

always_ff@(posedge clk or negedge rst_n)
  if      (rst_n)                state <= READY;
  else if (clr)                  state <= READY;
  else
    case (state)
      READY    : if      (run)   state <= WAIT;
      WAIT     : if      (start) state <= LOAD;
      LOAD     :                 state <= EXEC;
      EXEC     : if      (term)  state <= WAIT;
                 else if (done)  state <= WAIT;
                 else if (abnml) state <= ABNORMAL;
      ABNORMAL :                 state <= READY;
      default  :                 state <= READY;
    endcase

どうだろうか。このようにモードやステートを数値で書くより意味ある文字で表した方が意図が分かりやすいのではないのだろうか。丁度0/1の機械語の代わりに人間にとって意味の分かりやすいアセンブリ言語で書くのと同様である。やはり、ここでもenumで人間にとって意味のない数値を文字列にと言う意味のあるものに置き換えて「抽象度」を上げているのである。parameter/localpramを使っても同様に文字表示的にする事はできるが、数値の割り当て記述は人間が行わなくてはならず、いろいろとミスを誘発する原因になるのでenumを使用する事をお勧めする。機械が得意な事は機械にやらせておけば良いのである。
このenumもstructと同様に複数個所で宣言するにはtypedefが使われる。ステートマシーンの様な物では一か所での宣言で済むだろうからあえてtypedefをする必要性もないが、モードの様な信号ではtypedefが必要となってくる場合が多いのではないだろうか。特にmodule間にまたがってモードを知らせるような場合も多いだろう。その場合もstructと同様にやはりpackageに入れて定義することができる。

common_pkg.sv
package common_pkg;

  typedef enum logic[1:0] {READ, WRITE, SLEEP, NOP}  command_e; // <-- enum type definition inside package

  typedef struct packed { command_mode_e cmd_md; logic[3:0] addr; logic[7:0] bit_en; logic write_en; }  bus_str;

endpackage

そして、この例の様にenum自体をstructに入れる事もできる。packageで定義されたenumのmoduleでの使い方もstructと同様にimportや::でアクセスできる。

package内で定義されたenumの使い方
import  common_pkg::*;
command_mode_e  md;

always_comb
  if     (rd)        md = READ ;
  else if(md == NOP) md = SLEEP;

又、package内でexportで他のpackageからenumを取り込んでいる場合には、列挙メンバ名の参照、代入はexportされた先のpackageではなくexport元のpackageをスコープ演算子で指定する必要がある。

common_pkgからenumをexport
package  another_pkg;
  export  common_pkg::command_mode_e;  // <-- export from common_pkg
endpackage
他のpackage内で定義されたenumのをexportされた場合の使い方
import  another_pkg::*;  // import another_pkg
command_mode_e  md;

always_comb
  if     (rd)                    md = common_pkg::READ ;
  else if(md == common_pkg::NOP) md = common_pkg::SLEEP;.

因みにデザインでは関係ないが、テストベンチで使うclass内で定義されたenumの他の場所(moduleやclass)からのアクセスにもスコープ演算子::を使う。

class内でのenum
class pkt_ctrl;
  typedef  enum {SEND, RECEIVE, DISCARD, HOLD}  pkt_ctrl_e;
他のclassからのenum参照
class flow_control;
  pkt_ctrl::pkt_ctrl_e  cmd_1, cmd_2;  // <-- Use enum defined in another class with "::".
  
  function  void copy_pkg();
    if(cmd_1 == pkt_ctrl::DISCARD) cmd_2 = pkt_ctrl::SEND;  // <-- "::" is used to assing and refer enumerated names.
    else                           cmd_2 = cmd_1;

あともう1点余談だが、テストベンチでenumで宣言した変数をプリントしたい場合には%pで表示する方法と、.name()を使ってstringにして出力する方法2つある。

module tb;
  enum  {RED, GREEN, BLUE} color;

  initial  begin
    color = RED   ;  $display( "%p  %s", color, color.name() );
    color = GREEN ;  $display( "%p  %s", color, color.name() );
    color = BLUE  ;  $display( "%p  %s", color, color.name() );
  end
endmodule
実行結果
# RED  RED
# GREEN  GREEN
# BLUE  BLUE

■ 多次元Vector/Array

System Verilogになって多次元配列がサポートされた。これもUnpackedとPackedの2種類ある。UnpackedとPackedの違いはstructのそれと同様で、メモリ上に一続きのデータとして配置されるか、一続きになっていないかの違い。
以下、それぞれ8ビットのベクター信号が4つある場合、つまりの8x4の2次元配列の例。

Unpacked
logic[7:0]  data0,data1,data2,data3;
logic[7:0]  data_8x4[4];    // <- 8x4 array, [0:3] is same.

always_comb
  if(valid) begin
    data_8x4[0] = data0;
    data_8x4[1] = data1;
    data_8x4[2] = data2;
    data_8x4[3] = data3;
  end
  else
    data_8x4 = '{default : 8'h00, 3 : 8'hFF};
Packed
logic     [7:0]  data0,data1,data2,data3;
logic[3:0][7:0]  data_8x4;  // <-- 8x4 array

always_comb
  if(valid) begin
    data_8x4[0] = data0;
    data_8x4[1] = data1;
    data_8x4[2] = data2;
    data_8x4[3] = data3;
  end
  else
    data_8x4 = '{default : 8'h00, 3 : 8'hFF};
  //data_8x4 = {8'hFF, {3{8'h00}} };  // This is also O.K.

Unpackedはベクターで宣言された信号名の右に配列の要素数を[4]と書く。要素数ではなくてベクター表記[0:3]の様に書いても同様だが要素数で書いた方が分かりやすいだろう。
PackedはVerilogでやっていたベクター表記[7:0]の左側に更にベクター表記[3:0]をする。
ビットの参照、代入にはPacke/Unpackedとも同様に、data_8x4[0][7:0]の様に信号名[要素番号]の右にカッコを付けて表す。一番右のカッコがVerilogでのベクターと同様。その左側のカッコ[要素番号]はそれが何番目のベクターなのかを表す。例で示したようにそのベクターのカッコを省略するとそのベクターの全てのビットである。なのでdata_8x4[0][7:0]はdata_8x4[0]と書いたのと同じである。これもVerilogのベクターと同様。
何番目のベクターかを表すカッコも省略し、dataの様に書くとその配列名全体となる。
配列全体への代入はpackedとunpacked共に'{}を使う。その時defalut :で基本の値を指定できる。要素番号 :で基本以外の値を設定できる。
packedは切れ目ない一続きのデータなので、代入に連接演算{}も使える。当然、1次元の大きなベクターへの直接代入やその逆も可能。又、data_8x4[1:0]の様にすべてのベクターではなくて一部のベクターを選択することも可能。

Packed配列のVectorへの代入
logic     [ 7:0]  data0, data1, data2, data3;
logic[3:0][ 7:0]  data_8x4;
logic     [31:0]  data_4byte_all;
logic     [15:0]  data_2byte_lsb, data_2byte_msb;

assign data_8x4 = {data3, data2, data1, data0};
assign data_4byte_all = data_8x4;       // <-- data3, data2, data1, data0
assign data_2byte_lsb = data_8x4[1:0];  // <-- data1, data0
assign data_2byte_msb = data_8x4[3:2];  // <-- data3, data2

このようにpackedの方ができることが多く使い勝手が良いので、デザインではなるべくpackedの多次元配列を使うようにしよう。お馴染みSTARCの設計スタイルガイドでもunpackedでの使用を禁止している(参照)
この多次元配列だが、思っている以上に有用で、役に立つ事が多い。例えばデータセレクトの様な事もこの多次元配列をうまく使うと簡潔に書ける。

2次元ベクターによるデータセレクト
logic[3:0][ 7:0]  data_8x4;
logic     [ 1:0]  sel;
logic     [ 7:0]  data_out;

assign  data_out = data[sel];  

これをlogic[31:0] data_all;logic[7:0] data0, data1, data2, data3;の様な1次元ベクターでやろうとするとcase文ですべて羅列とベクターのビット選択を書かなくてはならないので結構大変で、当然記述ミスが多くなる。このような人間によるエラーはなるべく避けて、面倒な事は機械にやらせたい。

又、インデックスの割り当てを[3:0]の様な0から始まる右から左への昇順だけでなく[4:1]の様に1から始めたり、[0:3]の様に降順のインデックスを振る事もできる。

Packed_1から4へのインデックス割り当て
logic     [7:0]  data1, data2, data3, data4;
logic[4:1][7:0]  data_8x4;  // <-- 8x4 array

assign  data_8x4 = {data4, data3, data2, data1};
// data_8x4[4] <- data4
// data_8x4[3] <- data3
// data_8x4[2] <- data2
// data_8x4[1] <- data1

Packed_3から0へのインデックス割り当て
logic     [7:0]  data1, data2, data3, data4;
logic[0:3][7:0]  data_8x4;  // <-- 8x4 array

assign  data_8x4 = {data0, data1, data2, data3};
// data_8x4[0] <- data0
// data_8x4[1] <- data1
// data_8x4[2] <- data2
// data_8x4[3] <- data3

これをうまく使うとマッピングの変換等も簡潔に行う事もできる。

Packed_マッピングの変換
logic     [7:0]  data0, data1, data2, data3;
logic[3:0][7:0]  data_3_0;  // <-- 8x4 array
logic[0:3][7:0]  data_0_3_reverse_index; //  <-- 8x4 array
logic[0:3][7:0]  data_0_3_reverse_order;  // <-- 8x4 array

assign  data_3_0 = {data3, data2, data1, data0};
// data_3_0[3] <- data3
// data_3_0[2] <- data2
// data_3_0[1] <- data1
// data_3_0[0] <- data0

assign data_0_3_reverse_index = data_3_0;
// Index number is reversed. Order is kept. 
// data_0_3_reverse_index -> {data3, data2, data1, data0}
// data_0_3_reverse_index[0] <- data3
// data_0_3_reverse_index[1] <- data2
// data_0_3_reverse_index[2] <- data1
// data_0_3_reverse_index[3] <- data0

always_comb  for(int i=0; i<4; i++) data_0_3_reverse_order[i] = data_3_0[i];
//  Index number is kept. Order is reversed.
// data_0_3_reverse_order -> {data0, data1, data2, data3}
// data_0_3_reverse_order[0] <- data0
// data_0_3_reverse_order[1] <- data1
// data_0_3_reverse_order[2] <- data2
// data_0_3_reverse_order[3] <- data3

この多次元配列はpackedでもunpackedでも当然moduleの入出力信号タイプにも使える。interfaceやstructenumと合わせて使って行ってもらいたい。特にこの多次元配列はinterfaceやstruct, enumと違い別途何か用意する必要もなく、信号を配列で宣言するだけなので、より気軽にどんどん使って行けばよい思う。
又、上記では2次元配の例で説明したが、3次元以上の配列も同様に可能。

■ $clog2(), $bits()

$clog2(), $bits()はそれぞれ2の底のlogの整数への切り上げ値と変数のビット幅を返すシステムタスク。$clog2()はVerilog2005の時点で導入された。
$clog2()は数値の最大値から必要なビット数を求めたり、FIFO段数などメモリの深さから必要なアドレスのビット幅を求めたりするのに役立つ。「$clog2によるビット幅算出のよくある間違い」も参考にされたし。
$bits()は信号名の他にタイプ名も引数にすることができる。なのでtypedefで定義したタイプのビット数算出にも使う事ができる。

使用例
parameter  MAX_VAL = 10;
parameter  WIDTH = $clog2(MAX_VAL+1);  // Bit Width Calculation
typedef struct packed
  {
    logic[7:0]  data;
    logic[4:0]  addr;
    logic[4:0]  kind;
  }  my_st;

my_st  my_packet;

logic[     WIDTH-1:0]       val;
logic[$bits(val)-1:0]         a;  // Bit Width of Signal val
logic[$bits(my_packet)-1:0]   b;  // Bit Width of Struct Name my_packet
logic[$bits(my_st)    -1:0]   c;  // Bit Width of Defined Struct type my_st

assign a = val;
assign b = my_packet;
assign c = my_packet;

どちらもシステムタスクなので回路の合成自体に使用するのではなく、パラメータの値の計算や変数のビット幅の指定の様にコンパイル時に値が確定している所に使う。値が確定している所ならどこでも使えるのでキャスティングで信号のビット幅を変更する場合にも使える。

■ generate

generate文はVerilog 2001の段階で取り入れられた。VHDLにも同じくgenerateがある。これは基本的にはmodule等のインスタンス化の制御を行う時に使う。具体的にはfor文でparameterで指定した数だけmoduleをインスタンス化したり、if文/case文でmoduleをインスタンス化したり、しなかったりやインスタンス化するmoduleを変えたりを行う事が出来る。moduleの他assign, always, assert等にも使う事が出来る。

generateの例
parameter  TYPE = 0;
parameter  NUM = 10;

logic [NUM-1:0][7:0]  in_data, out_data;
generate

if(TYPE == 0) begin : GEN_TYPE0
  for(genvar i; i<NUM; i++) begin : GEN_NUM
    block_a  i_block(.clk, .rst, .in_data(in_data[i]), .out_data(out_data[i]) );
    a_check_out_data : assert ( out_data[i] == $past(in_data[i]) );
  end
end

else if (TYPE == 1) begin : GEN_TYPE1
  assign out_data = in_data;
end

else if (TYPE == 2) begin : GEN_TYPE2
  always_ff@(posedge clk or negedge rst_n)
    if(!rst_n)  out_data <= '0;
    else        out_data <= in_data;
end

endgenerate

このように、通常if文、forループ文にラベル名を付ける。この例ではGEN_TYPE0/1/2, GEN_NUMとした。そうする事によりgenerateでインスタンス化されたmoduleのスコープ名が明示的にGEN_TYPE0.GEN_NUM[i].i_blockとなる。ラベル名を省略するとgenblk1.genblk1[i].となる事が言語仕様で決められている。generate内のラベルブロックの順番にgenblkの様に1から順番に番号が振られる。generate内のforループの変数には通常のintではなくgenvarを用いる。System Verilogではforループのかっこ内でループ変数を定義する事が出来るようになった。なのでこの例ではそうしている。genvarをfor文のかっこの外に出して変数を定義しても構わない。
因みに実はgenerate, endgenerateただのお飾りで、省略する事もできる。しかし、分かりやすくするためになるべく書くようにしよう。他人の書いた記述でmodule内にいきなりif文やfor文があるのを見たらgenerateが省略されていると理解しよう。
generateを使うとこの様にパラメータによって回路構成を変えることができる。なのでRTLコードを使い回すことができ、回路のIPとしての価値がより高める事ができる。
尚、for文での複数インスタンス化以外、条件によるインスタンス化の制御はdefineやifdefを使用すればVerilogでも同様な事は可能である。

■ 数値のビット幅省略

System Verilogでは数値を表す時にビット幅を省略できるようになった。又、区切り記号"_"を使えるようになった。

Verilogでの数値
logic[ 7:0] valA;
logic[31:0] valB;
logic[15:0] ValC;
always_comb begin
  valA =  7'd100;
  valB = 32'hAAAABBBB;
  valC = 16'b1010111100001010;
SystemVerilogでの数値
logic[ 7:0] valA;
logic[31:0] valB;
logic[15:0] ValC;
always_comb begin
  valA = 'd100;
  valB = 'hAAAA_BBBB;
  valC = 'b1010_1111_0000_1010;

小技だが覚えておくと意外に便利な事もあるだろう。

■ 可変長リテラル

System Verilogでは可変長リテラルと言う物が導入された。これは一言でいうと信号のビット幅によらずall 1, all 0, all x, all zの代入を行う方法である。以下その記述。

可変長リテラルでの代入
logic[ 7:0] all_0;
logic[ 8:0] all_1;
logic[ 9:0] all_x;
logic[10:0] all_z;

assign all_0 = '0;  //      8'b0000_0000
assign all_1 = '1;  //    9'b1_1111_1111
assign all_x = 'x;  //  10'bxx_xxxx_xxxx
assign all_z = 'z;  // 11'bzzz_zzzz_zzzz

デザインで使用するのは主にall 0かall 1であろう。特にall 0はFFのリセットの記述になんかに使うと便利である。この可変長リテラル地味だがとても便利で使う機会も非常に多いので是非覚えて積極的に使って行こう。何が便利かと言うと、0や最大値を代入する際に、信号のビット幅や、タイプを一切気にする必要が無いと言う事である。ここでもやはり0とか最大値と言う少し抽象度が高まった概念を使っているのである。

FFリセット0
logic[WIDTH-1:0]  data;
always_ff@(posedge clk or negedge rst_n)
  if(!rst_n)  data <= '0;  // All 0 for any width of signal.

この例の様に信号のビット幅がパラメータで可変であってもなにも考えずに'0としておけば良いので大変便利である。
代入だけなく比較演算にも使用できる。

比較での使用
logic[WIDTH-1:0]  data;
wire all_0 = (data == '0);
wire all_a = (data == '1);

これらを利用すると例えば最大値で止まるカウンターなんかが非常に簡潔に書ける。

最大値で止まるカウンター
logic[WIDTH-1:0]  cnt;
always_ff@(posedge clk or negedge rst_n)
  if(!rst_n)          cnt <= '0;
  else if(clr)        cnt <= '0;
  else if(cnt != '1)  cnt <= cnt + 'b1;

又、structの初期化等の代入にも有効。

structの初期化
typedef  struct packed {
  logic[      7:0]  data_A;
  logic[     15:0]  data_B;
  logic[     31:0]  data_C;
  logic[WIDTH-1:0]  data_D;
} my_struct_st;

my_struct_st  values;
always_ff@(posedge clk or negedge rst_n)
  if(!rst_n)     values <= '0;                           // Clear to all 0
  else if(init)  values <= '{default : '0, data_D = '1}; // Clear to all 0 except data_D
  .
  .
  .

この様に可変長リテラルはちょっとした事なのだが、非常に有用で、使用する頻度も高いと思う。

■ キャスティング('

キャスティングとは信号の種類を変換する事で、通常はタイプの変換の事を指す。Cや他の言語でもあるが、Verilogでは代入や比較演算の際の信号のタイプが一致していなくて良いのでタイプキャスト自体が不要であった。基本的にSystemVerilogでもそれは変わらないのだが、SystemVerilogではタイプキャストに加えてビット幅、符合のキャストができるようになった。キャスト演算子には'を用いる。

・タイプキャスト

上述のようにSystemVerilogでは通常タイプのキャスティングは使用する機会は殆ど無いが、typedef enumで定義したタイプの信号に数値を直接代入する必要がある場合には使用される。enumで宣言された信号は通常は列挙した文字しか代入できないが、タイプキャストにより数値を直接代入する事ができるようになる。

enumへのタイプキャスティング
typedef enum logic[1:0]  {SLEEP, RUN, WAIT, READY}  mode_e;
mode_e      mode;
always_comb
  mode = mode_e'(2'b11);
//mode = 2'b11;  // NG

この様に<type name>'()で変換する。この例では2'b11と言う数値をmode_eと言うタイプに無理やり変換した。enumで宣言された信号をlogicで宣言された信号に代入する場合にはタイプキャストは必要ない。

タイプキャスティング不要
typedef enum logic[1:0]  {SLEEP, RUN, WAIT, READY}  mode_e;
mode_e      mode;
logic[1:0]  mode_val;
always_comb
  mode_val = mode    ; // OK

・ビット幅キャスト

ビット幅のキャスティングは代入や比較演算での左右の数値のビット幅をそろえたりするのに使える。

数値のビット幅のキャスティング例
logic[7:0] a, b, c;

assign a = 8'(b + c);

この様に<bit width>'()でビット幅を変換する。この例の様に8ビットの足し算では本来オバーフローが1ビットあり、結果は9ビットだが、8ビットのままで良い場合がある。その場合8ビットの変数で結果を受ければ上の1ビットが捨てられるが、Lintチェッカによっては左右のビット幅が合っていないとErrorやWarningをレポートする場合がある。その場合ビット幅キャスティングで明示的にビット幅をそろえる事でError, Warningを回避できる。
ビット幅の指定にはパラメータや$bits()等のシステムタスクの結果など整数を指定できる。又、ビット幅を変換するのは演算結果だけでなく当然数値そのものでも良い。

数値のビット幅キャスティング例2
parameter WIDTH = 16;
logic[WIDTH-1:0]  a;
logic[      7:0]  b, c;
always_comb begin
  a = '0
  case(case_sel_e)
    CASE1 : a = WIDTH'(100);
    CASE2 : a = $bits(a)'(200);
    CASE3 : a = $bits(a)'(b + c);
  endcase
end

・符合キャスト

符合のキャスティングにはsinged'(), unsigned'()を用いる。

符合のキャスティング
logic signed [7:0]   s_valA, s_valB;
logic signed [8:0]   s_sum;
logic        [7:0]   u_valA, u_valB;
logic        [8:0]   u_sum;

assign s_sum =   signed'(u_valA) +   signed'(u_valB);
assign u_sum = unsigned'(s_valA) + unsigned'(s_valB);

の様に符合の変換を行う。符合ビット拡張している訳でなくビット幅は変わらずキャスティングにより数値の認識を変えているに過ぎないので注意。
尚、同様の事がシステムタスク$singned(), $unsigned()を使ってもできる。

符合のキャスティング(システムタスク使用)
logic signed [7:0]   s_valA, s_valB;
logic signed [8:0]   s_sum;
logic        [7:0]   u_valA, u_valB;
logic        [8:0]   u_sum;

assign s_sum =   $signed(u_valA) +   $signed(u_valB);
assign u_sum = $unsigned(s_valA) + $unsigned(s_valB);

■ parameterのタイプ指定

確かVerilogではパラメーターのタイプの指定はできなくて32ビットinteger扱いで、System Verilogでパラメータのタイプを指定できるようになった様な気がする。間違っていたらすみません。省略すると32bit(おそらくint)になる。
タイプを指定できるので当然packed多次元配列やstructenumも指定できる。Unpacked配列もparameter宣言できるが、その場合にはタイプの指定を省略せずきちんとする必要がある。

parameterのタイプ指定
typedef  enum logic[1:0] {RED, GREEN, YELLOW, BLACK} color_e;
typedef  struct packed {logic[15:0] addr; logic[31:0] payload; logic[3:0] kind;}  packet_st

parameter  int              p_INT_0123[4] = '{  0,   1,   2,   3}; // int Unpacked Array
parameter  logic[3:0][3:0]  p_2D_ABCD     =  {'hA, 'hB, 'hC, 'hD}; // 2D Packed Array
parameter  color_e          p_MY_COLOR    =  RED;                  // enum
parameter  packet_st        p_ZERO_PKT    = '{default : '0};       // struct

■ localparam

localparamもVerilog2001で既に導入されていた。基本的にparameterと同じだが、違いはparameterが上位から書き換え可能なのに対しlocalparamはそのmodule内のみで参照が可能で上位から書き換えはできない。なのでむしろparameterは#(parameter ...)のみでの使用にしてmoduleおよびpackage内の定数にはparameterではなくlocalparamを使うべきなのかもしれない。

■ Variable Bit Part Select(+:, -:)

+:-:を使ってベクター内の一部のビットを可変で選択する事が出来る。選択するビット幅は固定である必要がある。選択してくる位置が可変。これはVerilog2001の時点で既に導入された。以下も参照。

例えば32ビットのビット列の中から8ビットを可変的に取り出す場合を考える。
以下ビットの位置に可変な値を指定した以下の記述はNGである。

NGな記述
logic[31:0]  data_4B;
logic[ 7:0]   sel_1B;
logic[ 4:0]        i; // Variable, 0~24

assign  sel_1B = data_4B[(i+7):i];

これをやるにはcase文で全部のケースを場合分けして書く必要があった。

OKな記述
always_comb
  case(i)
    0 : sel_1B = data_4B[ 7:0];
    1 : sel_1B = data_4B[ 8:1];
    2 : sel_1B = data_4B[ 9:2];
    3 : sel_1B = data_4B[10:3];
    .
    .
    .
  endcase

あるいはビットシフト>>でも記述できる。

OKな記述(>>を使用)
assign sel_1B = data_4B >> i;

これをBit Part Selectでは以下のように記述する。

Bit_Part_Selectでの記述
assign  sel_1B = data_4B[i +: 8];  // [(i+7):i], 8bit plus from bit position i(0~24) 

[<Start Index> +: <Bit Width> ]の様に指定する。Bit Widthは固定値である必要がある。数値でなくてもパラメータの様な固定値であればOK。+:は起点の位置からインデックスのプラス方向への選択であったが、-:はインデックスのマイナス方向への選択。

Bit_Part_Selectでの記述(起点からマイナス方向)
assign  sel_1B = data_4B[i -: 8];  // [i:(i-7)], 8bit minus from bit position i(31~7) 

尚、2次元Packed Arrayを使用しても類似な事は可能。

Packed_Arrayを使用
logic[31:0]     data_4B;
logic[3:0][7:0] data_4B_array;
logic[ 7:0]     sel_1B;
logic[ 1:0]           i; // Variable, 0~3

assign  data_4B_array = data_4B;
assign  sel_1B = data_4B_array[i];

ただし、この場合Bit Part Selectと違い取り出してくる位置が8bitの単位に限られる。

又、Bit part Selectはベクター(1次元配列)のインデックスにだけでなく多次元配列のインデックスにも適用可能。
以下は4bit毎に0~7および7~0のインデックスで区切られた2次元配列から2インデックス分の8bitをselで選択する例。

2次元配列のインデックスに適用
module top;

  localparam bit [0:7][3:0] data_0_7 = 'h01234567;
  localparam bit [7:0][3:0] data_7_0 = 'h76543210;

  bit[2:0]  sel;

  initial begin
    $display("data[0:7], [%h] ( (sel) --> + )", data_0_7); for (sel=0; sel<8; sel++) $display("  [%d +: 2] = [%h]", sel, data_0_7[sel+:2]);
    $display("data[0:7], [%h] ( - <-- (sel) )", data_0_7); for (sel=0; sel<8; sel++) $display("  [%d -: 2] = [%h]", sel, data_0_7[sel-:2]);
    $display("data[7:0], [%h] ( + <-- (sel) )", data_7_0); for (sel=0; sel<8; sel++) $display("  [%d +: 2] = [%h]", sel, data_7_0[sel+:2]);
    $display("data[7:0], [%h] ( (sel) --> - )", data_7_0); for (sel=0; sel<8; sel++) $display("  [%d -: 2] = [%h]", sel, data_7_0[sel-:2]);
  end

endmodule
Log
data[0:7], [01234567] ( (sel) --> + )
  [0 +: 2] = [01]
  [1 +: 2] = [12]
  [2 +: 2] = [23]
  [3 +: 2] = [34]
  [4 +: 2] = [45]
  [5 +: 2] = [56]
  [6 +: 2] = [67]
  [7 +: 2] = [7x]
data[0:7], [01234567] ( - <-- (sel))
  [0 -: 2] = [x0]
  [1 -: 2] = [01]
  [2 -: 2] = [12]
  [3 -: 2] = [23]
  [4 -: 2] = [34]
  [5 -: 2] = [45]
  [6 -: 2] = [56]
  [7 -: 2] = [67]
data[7:0], [76543210] ( + <-- (sel))
  [0 +: 2] = [10]
  [1 +: 2] = [21]
  [2 +: 2] = [32]
  [3 +: 2] = [43]
  [4 +: 2] = [54]
  [5 +: 2] = [65]
  [6 +: 2] = [76]
  [7 +: 2] = [x7]
data[7:0], [76543210] ( (sel) --> - )
  [0 -: 2] = [0x]
  [1 -: 2] = [10]
  [2 -: 2] = [21]
  [3 -: 2] = [32]
  [4 -: 2] = [43]
  [5 -: 2] = [54]
  [6 -: 2] = [65]
  [7 -: 2] = [76]

インデックスが左から右に大きくなっている[0:7]では[sel+:2]で[sel:sel+1]が選ばれ[sel-:2]では[sel-1:sel]が選ばれる。逆にインデックスが右から左に大きくなっている[7:0]では[sel+:2]で[sel+1:sel]が選ばれ[sel-:2]では[sel:sel-1]が選ばれる。この様に同じBit Part Selectの表記でもインデックスの数字の増減の方向が逆だと指定される箇所が異なるので注意。+/-はインデックスの数字の増減に対する方向の意味であって、コードのテキスト記述上の左右の方向に対しての意味ではない。1次元ベクターのBit Part Selectでも同じである。

■ Bit Stream Operator(<<

ベクターのビット順を入れ替え反転されるにビットストリームオペレータ<<が使える。1ビット単位の入れ替えだけでなく任意の単位での入れ替えを指定できる。<<自体はビット列を右から順に読み出すという意味。右から順に読み出して(左から順に)置くのでビットの入れ替えができる。
以下を参考。

これを使えばエンディアン変換等も簡潔に記述できる。ただし、やはり2次元Packed Arrayを使っても同様な事は可能。
2次元Packed Arrayで順序を入れ替える場合はforループを回していたが、<<を使えばfor文を書かなくて済む。<<自体がfor文と同等の事をしているからである。

2次元Packed_Arrayのマッピングの変換(Bit_Strem_Operator使用)
  logic[31:0]      data_4B;

  wire[3:0][7:0]  data_4B_array = data_4B; // <-- 8x4 array
//logic[0:3][7:0]  data_4B_array_reverse;  // <-- 8x4 array
//always_comb  for(int i=0; i<4; i++) data_4B_array_reverse[i] = data_4B_array[i];
// Order are reversed.
// data_4B_array_reverse[0] <- data_array[0] <- data_4B[ 7: 0]
// data_4B_array_reverse[1] <- data_array[1] <- data_4B[15: 8]
// data_4B_array_reverse[2] <- data_array[2] <- data_4B[23:16]
// data_4B_array_reverse[3] <- data_array[3] <- data_4B[31:24]
  wire[0:3][7:0]  data_4B_array_reverse = { <<8 {data_4B_array} };

//wire[31:0]  data_4B_reverse = {data_4B[ 7: 0], data_4B[15: 8], data_4B[23:16],data_4B[31:24]};
//wire[31:0]  data_4B_reverse = data_4B_array_reverse;
  wire[31:0]  data_4B_reverse = { <<8 {data_4B} };

■ 比較演算でのワイルドカード(==?, !=?

SystemVerilogでは信号の値との等号比較演算でビットの一部の比較をマスクしたい場合に、ワイルドカードが使えるようになり記述を簡単にする事ができるようになった。

比較演算のワイルドカード
logic[15:0] val;
always_comb
  if      (val ==? 'hF??F) ...
  else if (val !=? 'h0??0) ...
  else if (val ==? 'b0000_????_0000_????) ...
  else if (val !=? 'b1111_????_????_1111) ...
  else ...

比較対象から外すビット位置の値をワイルドカード記号の"?"或いは"x"にする。
Verilogではワイルドカード比較はcasex, casezでのみ記述ができた。SystemVerilogではこのように==?, !=?でcaseに限らずどこでも使える。ただしcasex, casezでのワイルドカード比較と==?, !=?でのワイルドカード比較に違いがある。==?, !=?では左側の変数に含まれるxやzはワイルドカードではない。なので例えば以下のようになる。

   logic[3:0] val;
   if(val ==? 4'b0???) // 4'bx111 ==> x, 4'b0xxx ==> True

比較演算の違いについては

も参照。

■ inside

insideはある信号の値がとある範囲や、値の集合の中にマッチる値があるかを判定する演算子。SystemVerilogのランダム検証のランダム制約の部分でよく見るかと思うが検証専用と言う訳ではなく、実はデザインでも使える。以下の様に使用する。

inside使用例
logic[7:0]  val;
logic       flag;
always_comb
  if(val inside {[0:100],254,255}) flag = 'b1;
  else                             flag = 'b0;

この例ではvalが0~100か254か255の値に入っているかを判定している。ひとつひとつ比較演算で書く事もできるが、範囲が複雑になってきたり集合の数が多くなると大変。
insideでは==?での等号比較演算なので、比較する値にワイルドカードも使える。

insideでワイルドカード使用例
logic[7:0]  val;
logic       flag;
always_comb
  if(val inside {8'b00??_????,[64:100], 8'b1111_111?}) flag = 'b1;
  else                                                 flag = 'b0;

{8'b00??_????}は{[0:63]}と、{8'1111_111?}は{254,255}と同じである。

このinsideはcase文でも使える。

insideのcase文での使用
logic[7:0]  val;
logic       flag;
always_comb
  case(val) inside
    [0:100],254,255 : flag = 'b1;
    default         : flag = 'b0;
  endcase  

なので当然case文でワイルドカードが使える。

case文でのワイルドカードの使用
logic[7:0]  val;
logic       flag;
always_comb
  case(val) inside
    8'b00??_????, [0:100],8'b1111_111? : flag = 'b1;
    default                            : flag = 'b0;
  endcase  

case文でのワイルドカードはVerilogではcasex,casezでできた。

Verilogでのワイルドカード
reg[3:0] code;
reg[1:0] out;
always@*
  casex (code)
    4'b1??? : out = 2'd3;  // <---- 1xxx or xxxx
    4'b01?? : out = 2'd2;
    4'b001? : out = 2'd1;
    4'b0001 : out = 2'd0;
    default : out = 2'bxx;
  endcase

しかしこのcasexおよびcasezでのワイルドカードによるビットマスク比較には問題がある。ビット比較のマスクが両方に適用されるので、入力信号のビットにx或いはzが含まれるとそのビットの比較演算がマスクされてしまうのである。なので上記の記述では例えばcode = 4'bx000の場合、最初の条件がTrueとなりout=2'd3;が実行されてしまう。しかし合成後のGate Simではそうならず、RTL SimとGate Simで結果が異なってしまう。
一方case insideでのワイルドカードによるビット比較のマスクではRTL SimとGate Simで動きが一致する。なぜならcase insdeでは==?での比較で、ワイルドカードによるビットマスクは==?の右側にのみ適用だからで、入力信号のビットにxが入ったとしてもそのビットの比較はマスクされないからである。

case()insideでxの一致
logic[3:0] code;
logic[1:0] out;
always_comb
  case (code) inside
    4'b1??? : out = 2'd3;  // <---- Only 1xxx
    4'b01?? : out = 2'd2;
    4'b001? : out = 2'd1;
    4'b0001 : out = 2'd0;
    default : out = 2'bxx; // <---- xxxx
  endcase

なのでワイルドカードのcaseはcasex,casezは使わずこれからはcase() insdeを使うようにしよう。

insideに関しては以下Qiita記事も参考に。

■ Assertion

アサーションはSystemVerilogの検証向けの強化項目として導入された。「な~んだ検証向けならRTL設計しかしないし関係ないや。」とか思ってしまったそこのあなた。給料の半分を会社に返納してください。確かにアサーション自体は検証向けの記述なのだが、むしろRTL設計者が積極的に取り組むべきものである。その辺に関しては参考資料で紹介しているWho Put Assertions In My RTL Code? And Why?とか是非見てもらいたい。設計者がアサーションを記述する意味がないとか言う人を時々見受けるが、そのような人は以下の事を理解できていない。設計者がアサーションを記述する利点は以下である。

  • 設計者の意図を記述
  • 曖昧さなく仕様を記述
  • 設計ブロックへの入力の前提条件を記述
  • 異常動作の検出の局所化と早期発見

アサーションはこれまでこのページで紹介してきたSystemVerilogの記述の中でも最も開発効率の向上が期待できるのではないかと思う。それぐらい重要なのである。

アサーション自体は元々ソフトウェア開発で用いられてきのだが、それをHDLでのハードウェア開発にも取り入れたのである。SystemVerilogではSVA(SystemVerilog Assertion)と呼ばれたりする。因みにVHDLでは別のアサーション言語であるPSLを統合している。余談だがアサーションに限らず大概のHDLでのハードウェア開発手法はソフトウェア開発からの転用である。

アサーションは期待する信号の挙動を言語で表現し、シミュレーション中に期待動作と違った動きが観測されたらエラーメッセージが表示される。要するに局所領域での期待動作モデルと考える事ができる。RTL自体文字通りRegister Transfer Levelでの動作モデリングなので、アサーションではそれより高い抽象度レベルでの表現でないとあまり意味がない。なので別の言い方をするとアサーションは回路の動作仕様を記述言語で表現した物なのである。アサーションとは何なのかを良く分かっていない人は「アサーション活用の手引き」等の記事を参考にすると良いだろう。
アサーション記述自体の情報は割と世の中に溢れているので、ここでは主にデザインへの適用方法に関してのみ紹介する。以下の様な単純な回路の具体的な例で見て行く。

仕様:入力信号in_aの1クロック後に出力信号out_bにin_aの値が出力される。
RTL記述は以下のようになるだろう。

RTL記述
module block (  input logic  clk, rst_n, in_a, output logic  out_b);

  always_ff@(posedge clk or negedge rst_n)
    if(!rst_n)  out_b <= 'b0;
    else        out_b <= in_a;

endmodule

これにいくつかアサーションを追加してみる。

アサーション追加
module block (  input logic  clk, rst_n, in_a, output logic  out_b);

  always_ff@(posedge clk or negedge rst_n) begin
    `ifndef ASSERTION_OFF
    //------------------ Immediate Assertions ----------------------------
    asm_rst_n_x : assume ( !$isunknown(rst_n) ) else $error("rst_n is X!");
    asm_in_a_x  : assume ( !$isunknown( in_a) ) else $error("in_a is X!" );
    //--------------------------------------------------------------------
    `endif
    if(!rst_n)  out_b <= 'b0;
    else        out_b <= out_a;
  end

  `ifndef ASSERTION_OFF
  //-----------------------------Concurrent Assertions ----------------------------------
  asm_a_pulse : assume property ( @(posedge clk) !in_a ##1 $rose(in_a) |=> $fell(in_a) )
                else $error("in_a is not a single pulse!");

  ast_a_to_b  : assert property ( @(posedge clk)  1 |-> ##1 (out_b == $past(in_a)) )
                else $error("out_b is not equal to in_a!");

  ast_b_pulse : assert property ( @(posedge clk) !out_b ##1 $rose(out_b) |=> $fell(out_b) )
                else $error("out_b is not a single pulse!");
  //------------------------------------------------------------------------------------
  `endif
endmodule

と、この様にRTL記述のmoduleの中に直接アサーションを書くことができる。always_ffの中にimmediateアサーションを書き、moduleの直下にはconcurrentアサーションを書いた。assertの他にassumeを使っているがシミュレーション上は特に違いは無い。フォーマル検証と言う物をやる場合にはassumeは制約として扱われる。ここでは入力信号に対する仮定をしていると言う事を示す為に使い分けてみた。RTLファイルに直接アサーション書いてしまってちゃんと合成できるのか?と思うかもしれないが、合成ツールはきちんとassert,assumeの記述を無視して回路合成してくれる。合成ツールやLintツールが対応していない場合にはこの例の様に`ifdefや`ifndefを使ってアサーションの部分を記述から除くように制御できるようにしておくと良いだろう。
ここではasm_a_pulseのように入力信号の仮定をアサーションで記述した。実はこれはかなり重要である。このモジュールでは実は入力信号in_aを1パルスで来ることを想定していたのである。初めの仕様ではそのような事は一言も言及されていない。バグと言うのは大体このような仕様の曖昧さや担当者間の解釈や認識の相違で起こる。アサーションはコンピュータ言語なので自然言語と違い仕様や設計者の意図を曖昧さなく表現するのである。こんな簡単な回路でさえこんな所にバグが入り込む隙があるのである。アサーションはそれに対抗する強力な手段だと言う事がよく分かると思う。
ast_b_pulseの方はどうだろうか。これは出力信号out_bがやはりパルスだというアサーションである。これも初めの仕様には言及されていない。やはり設計者はout_bもパルスで出す事を意図していたのである。
ast_a_to_bはどうだろうか。これらはin_aの値が1クロック後にout_bに出ると言う仕様を表したアサーションだがこれくらい簡単な回路では殆ど意味がない。しかし、これが1クロック後ではなくて何クロックか後と言う仕様だったらどうだろうか。回路はカウンターやステートマシーンが入ってもう少し複雑になる筈である。それに対してアサーションはin_aの値が何クロック後かにout_bに出ると言う仕様の動作レベルで表現するので入れる価値が十分出てくる。
アサーションとはこの様に、設計者の想定や設計意図、仕様を整理し曖昧さなく表現し、設計情報であるRTL記述自体の中に組み込んだ物だと言う事が分かったと思う。「アサーション活用の手引き」のメリット・デメリットでも以下の様に述べられている。

アサーションの記述は、仕様をブレークダウンする作業である。アサーション言語はコンピュータ言語なので、あいまいな記述は許されない。アサーションを記述/検討する工程において、仕様に対する深い理解、あいまいな点の把握、問題の指摘が行える

アサーションをRTLファイルに組入れる際に便利な方法をもう少し紹介しておこうと思う。Concurrent Assertionsの各propertyでは@(posedge clk)を使ってアサーション評価の基準のクロックを指定していた。実はこれは一回default clockingで宣言すれば以降省略が可能となる。propertyの数が多くなってくると便利だと思う。propertyの抽象度が少し高くできたとも見る事ができる。

clockingの使用
  default clocking def_clk @(posedge clk); endclocking

  asm_a_pulse : assume property ( !in_a ##1 $rose(in_a) |=> $fell(in_a) )
                else $error("in_a is not a single pulse!");

  ast_a_to_b  : assert property ( 1 |-> ##1 (out_b == $past(in_a)) )
                else $error("out_b is not equal to in_a!");

  ast_b_pulse : assert property ( !out_b ##1 $rose(out_b) |=> $fell(in_b) )
                else $error("out_b is not a single pulse!");

尚、このdefault clockingは某ASICでの市場独占合成ツールでは問題なく受け付けるが、同じ会社の某有名Lintツールでは対応していなかった。別の会社のLintツールは対応してたのに。ツールが対応していない場合は諦めて@(posedge clk)を使うかLintの時にifndef/ifdefでアサーション記述を除くようにしよう。
因みに某会社にLintツールでも対応するよう改善要望をしたが却下となってしまった。ユーザーの要望が多くなれば対応すると思うのでこれに限らずツールベンダーへのユーザー要望はどんどん出してもらいたい。
asm_a_pulseとast_b_pulseは両方同じパルスである事をチェックするアサーションである。なのでそのpropertyは同じである。このように同じpropertyを何度も書くのは手間なのでまとめて一回書くだけにしたい。その場合にはpropertyを別宣言してまとめる事ができる。さらにast_a_to_bのpropertyも含めて他のmoduleでも使えるようにしたい。module間で共有したいと言えばそう、既に紹介したpackageに入れて使う事ができるのである。

propertyのpackage化
package prop_pkg;

  property p_pulse(sig);
    !sig ##1 $rose(sig) |=> $fell(sig)
  endproperty

  property prop_1clk(siga, sigb);
    1 |-> ##1 (sigb == $past(siga))
  endproperty
  
endpackage
propertyの使用
  import prop_pkg::*;
  default clocking def_clk @(posedge clk); endclocking

  asm_a_pulse : assume prop_pulse(in_a)        else $error("in_a is not a single pulse!");
  ast_a_to_b  : assert prop_1clk (in_a, out_b) else $error("out_b is not equal to in_a!");
  ast_b_pulse : assert prop_pulse(out_b)       else $error("out_b is not a single pulse!");

この様に良く使うpropertyはpackageに入れてライブラリ化しておくと何度も使い回しができて便利である。
propertyは基準クロックを含めて書く事もできる。

propertyのpackage化(clk含む)
package prop_pkg;

  property p_pulse(clk, sig);
    @(posedge clk)  !sig ##1 $rose(sig) |=> $fell(sig)
  endproperty

  property prop_1clk(clk, siga, sigb);
    @(posedge clk)  1 |-> ##1 (sigb == $past(siga))
  endproperty
  
endpackage
propertyの使用(clkをpropertyに含む)
  import prop_pkg::*;

  asm_a_pulse : assume prop_pulse(clk, in_a)        else $error("in_a is not a single pulse!");
  ast_a_to_b  : assert prop_1clk (clk, in_a, out_b) else $error("out_b is not equal to in_a!");
  ast_b_pulse : assert prop_pulse(clk, in_b)        else $error("in_b is not a single pulse!");

この場合はdefault clockは書かずにassert/assumeでproperty()にclk信号を渡す。
アサーションの効果的な適用箇所としてすでに紹介したinterfaceがある。アサーションはmoduleに限らずinterface内にもを書く事ができるのである。使用方法もmoduleでの使用と同じである。

interfaceでのアサーション使用例
interface #(parameter width=32) bus_if(input logic clk, rst_n);
  logic[ 7:0]      addr;
  logic            wr_rq  , rd_rq  , rd_en;
  logic[width-1:0] wr_data, rd_data;
  modport mst_port(                  output addr, wr_rq, wr_data, rd_rq, input  rd_en, rd_data);
  modport slv_port(input clk, rst_n, input  addr, wr_rq, wr_data, rd_rq, output rd_en, rd_data);

  //---------------- Assertions --------------------------//
  ast_wr_rq_pulse : assert prop_pkg::prop_pulse(clk, wr_rq);
  ast_rd_rq_pulse : assert prop_pkg::prop_pulse(clk, rd_rq);
  ast_rd_en_pulse : assert prop_pkg::prop_pulse(clk, rd_en);
  //------------------------------------------------------//

endinterface

いろんな所で使う汎用バスの様な物は信号とそのプロトコルが決まっているので、入出力信号定義とそのアサーションチェックをinterfaceに入れておいてどこでも使い回しができるようにしておくと非常に効果的である。

■ 最後に

以上、デザイン向けのSystemVerilogの記述を紹介してきた。最初にも書いたが、開発効率が大幅に向上するので是非取り入れて行ってもらいたい。これらの記述を全て覚える必要もないし、いっぺんに適用する必要もないので少しづつ使ってみてもらいたい。又、ここで紹介した記述方法を単体で使うのではなく是非複数組み合わせてより高い効果発揮させてほしい。
最後に以下に参考資料2のSutherland HDLのプレゼン資料に載っているいろいろな合成ツールのSystemVerilog対応状況の表を引用する。2014年時点の情報なので少し古いが、各合成ツールがどれくらいサポートしているか分かると思う。
Synthesis_Support.PNG
若干ツールによってはサポートしていない物もあるが、その当時でもほぼ対応している。これから数年経った現在ではさらにサポート状況が改善されていると思われる。


■ 参考資料

1. SystemVerilog from Zero to One (フル版) (ダイジェスト版) by アートグラフィックス
検証向け含めたSystemVerilogの記述について細かいところまでかなり網羅的に載っている。量が多いが良くまとまっていて読みやすい。既にSystemVerilogをよく知っている人でも知らない事とか載っていると思うので一読をお勧めする。

2. Can My Synthesis Compiler Do That? (Slides) (Paper) by Sutherland HDL
デザイン向けの合成可能なSystemVerilog記述をまとめたプレゼン資料。ほぼこのページの元ネタ。

3. Who Put Assertions In My RTL Code? And Why? (Slides) (Paper) by Sutherland HDL
設計者向けのアサーションに関するプレゼン資料。これを見ると何故設計者がアサーションを使うべきか理解できると思う。
81
70
5

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
81
70