kivantium活動日記

プログラムを使っていろいろやります

SystemVerilog文法メモ

前回の記事でSystemVerilogをちょこっと書いたので、今度はSystemVerilogの文法について細かく勉強しようと思います。
あくまで自分が勉強するためのメモなので、お気づきの点があったら指摘してもらえると助かります。
理解したところから記述を足していくので永遠に未完成です。

SystemVerilogについて

SystemVerilogはVerilog HDLをベースに記法や検証機能などを追加して作られたハードウェア記述言語です。
最新の規格であるIEEE 1800-2012はIEEE Standard Association - IEEE Get Programからダウンロードできます。

最初の例

前回の記事で書いたコード例を元に文法について書いていきます。
コードを再掲しておきます。

module decoder(
    input [2:0] sw,
    output reg [7:0] led
    );
    
    always @ (sw)
        case (sw)
            3'b000: led = 8'b00000001;
            3'b001: led = 8'b00000010;
            3'b010: led = 8'b00000100;
            3'b011: led = 8'b00001000;
            3'b100: led = 8'b00010000;
            3'b101: led = 8'b00100000;
            3'b110: led = 8'b01000000;
            3'b111: led = 8'b10000000;
        endcase
 endmodule

module構造

SystemVerilogにおいて基本構造となるのがこのmodule構造です。

module <モジュール名> (<ポート宣言>);
    <ネット宣言>
    <変数宣言>
    <パラメータ宣言>

    <モジュール構成要素>
        initial
        always
        assign
        function
        generate
        など
endmodule

という構造をしています。

最初に上げた例ではdecoderという名前のmoduleを記述しています。

ポート宣言

モジュールの入出力を記述する部分です。

例でいうと

    input [2:0] sw,
    output reg [7:0] led

にあたります。

構文は

<モード> [<ネット型> or reg] [signed] [<レンジ>] <ポート名>

のように書き、ポート同士の記述はカンマでつなげます。([]内は省略可能という意味)

  • モードには、入力を表すinput, 出力を表すoutput, 入出力を表すinoutの3種類があります。
  • <ネット型>は後述します。regはoutputのみで使えます。
  • signedは符号付きの場合に指定します。(デフォルトは符号なし)
  • レンジは[MSB:LSB]という形で指定します。指定がなければ1bitになります。
  • ポート名は英字またはアンダーバーから始まり、英数字・アンダーバー・$を含むことができる文字列で、大文字と小文字は区別されます。

ネット宣言

回路記述で使うネット(単なる配線?)を記述します。構文は

<ネット型> [signed] [<レンジ>] [<遅延>] <ネット名リスト>;

です。

wire ENABLE;
wire [7:0] data;
wire #3000 delay; // 遅延の例
wor signed hoge;

のように書きます。

ネット型はいろいろありますが、通常の配線を表すwireとワイヤードORを表すworだけ紹介しておきます。

変数宣言

フリップフロップなどの値を保持する信号を記述します。構文は

<変数型> [signed] [<レンジ>] <変数名リスト>;

です。

reg ff;
reg signed [7:0] counter;
reg [7:0] memory [0:1023]; // アンパック型配列で表した1KBメモリ
reg [7:0] [0:1023] memory; // パック型配列で表した1KBメモリ (SystemVerilogのみ)

のように書きます。
アンパック型配列は個々の要素が別々に存在するため(この場合)8bit単位でしか扱えません。
パック型配列は連続したベクタとして存在するため32bitなどのそれぞれの要素数より長いデータを代入することができます。

変数型には任意bitのregや実数を表すrealなどがあります。

また、SystemVerilogで追加されたデータタイプとして、logicがあります。
logicの使用方法はregとほとんど同じですが、logicを使うことでalways文で組み合わせ回路を記述する際にはレジスタが存在しないにも関わらずregを使わないといけないという紛らわしさを防止するなどの効果があります。
詳しくは該当する文法の説明のところで書きます。

パラメータ宣言

パラメータは定数に名前をつけるために使います。

parameter MAX = 1024;

のように書きます。

このあたりは初めてでも使えるVerilog HDL文法ガイド ―― 記述スタイル編|Tech Village (テックビレッジ) / CQ出版株式会社が参考になります。

数字の表記方法

SystemVerilogの数値は

<size>'<base> <value>

という形で表します。

  • <size>はビット幅を表す10進数です
  • '<base>は基数を表すアルファベットで、dが10進数・hが16進数・bが2進数・oが8進数です。符号付きの場合はsをつけてsd, sh, sb, soのように表します。
  • <value>は値です。不定値を表すxやハイ・インピーダンスを表すzを使うことができます。

ビット幅が省略されると32bitになり、さらに基数も省略すると10進数になります。

10     // 32bit 10進数
3'b010 // 3bit 2進数
8'hff  // 8bit 16進数

コメントの書き方

コメントにはC++と同じように

  • // から始まって行末まで
  • /* 〜 */ で囲まれた部分

の2種類の書き方があります。

initial文

initial文は変数の初期化のような最初に1回だけ行われる処理の記述に使います。

    initial begin
        led <= 0;
        count <= 0;
    end;

begin〜endはブロックを作って、複数の文をまとめて一つの文として扱う構文です。
begin〜endは記述順に実行される順序処理ブロック、fork〜joinは並列に実行される並列処理ブロックを作ります。
(fork〜joinは論理合成で使えないらしいです[要検証])

always文

always文は、入力が変化した時に常に行う処理を記述するための文で、組み合わせ回路の記述にも順序回路の記述にも使えます。
冒頭で示した例は、always文で組み合わせ回路を作っています。

always文の構文は

always @(<入力信号名>,<入力信号名>,...) begin
<処理内容>
end

が基本です。

Verilogの場合はalways文の出力はレジスタ(regなど)で宣言されている必要があります。(SystemVerilogの場合は後述)
組み合わせ回路の場合など、入力信号がたくさんある場合は@*のように記述することで書き忘れを防ぐことができます。

always文の中ではif文やcase文を使うことができます。
if文は

if (<条件式>) <処理内容>
[else <処理内容>]

のように使います。
case文は

case (<>)
    <>: <処理内容>
    <>: <処理内容>
    default: <処理内容>
endcase

のようにして使います。
値が全てのパターンを網羅していない場合、組み合わせ回路と認識されずにラッチが生成されるなどの問題が起きることがあるためdefaultを記述することが推奨されます。
Don't careを使うことのできるcasexcasezというものもあります。

(SystemVerilogに関する注:
SystemVerilogではuniquepriorityというキーワードが追加されました。
unique ifのように、どちらもif, caseの前にキーワードを置いて使います。

  • uniqueは組み合わせ回路が並列回路であることを指定します。条件式には重複がなく全ての条件が記述されている必要があります。
  • priorityは組み合わせ回路が優先順位を持つ回路であることを指定します。条件式には全ての条件が記述されている必要があります。

簡単な例としてシンクロナイザの記述を示します。
シンクロナイザはスイッチなどの非同期入力をクロックに同期させるために使うものでメタステーブル現象の防止に役立ちます。

module sync(
  input clk,
  input sw,
  output logic out
  );

  logic buf;

  always @(posedge clk) begin
    buf <= sw;
    out <= buf;
  end
endmodule


組み合わせ回路・case文の例はデコーダのところで書いたので、順序回路・if文の例としてカウンタのコードを書きます。
論理合成するときにはconstraintのコメントアウトを適宜解除してください。
Basys 3のクロックは100 MHzなので100000000回立ち上がるたびにLEDの示す値が1増えるような回路を設計します。

module counter(
    output reg [15:0] led,
    input clk
    );
    
    reg [32:0] count;
    parameter CLOCK = 100000000;
    
    initial begin
        led <= 0;
        count <= 0;
    end;
    
    always @(posedge clk) begin // posedgeは立ち上がりを表す。立ち下がりはnegedge
        if(count == CLOCK) begin
            count <= 0;
            if(led == 16'b1111111111111111)
                led <= 0;
            else
                led <= led + 1;
        end
        else
            count <= count + 1;
    end
endmodule

SystemVerilogでは新たに3つのalways文が導入されました。

  • always_comb文: 組み合わせ回路を記述するためのalways文です。センシティビティ・リスト(@以下の入力を指定する部分)が不要です。
  • always_ff文: フリップフロップを含む順序回路を記述するためのalways文です。always文と使い方は同じです。
  • always_latch文: ラッチを記述するためのalways文です。これもセンシティビティ・リストが不要です。

これらを使うことで、always文の持つ意味がはっきりし、意図しないラッチなどが生成された時に警告が出るようになります。

ブロッキング代入とノン・ブロッキング代入

ここまで代入の記号として=<=を特に説明なく使ってきましたが、この2つには明確な違いがあります。

=の方はブロッキング代入を表し、順序処理ブロックの中で次のブロッキング代入処理より先に実行されます。
<=の方はノン・ブロッキング代入を表し、ブロッキング代入のような順序はありません。

a = b;
b = a;

はaにbの値を代入してからbにaの値を代入するので2つの値は同じになりますが、

a <= b;
b <= a;

は代入が同時に実行されるので、aとbの値が入れ替わります。

  • 順序回路には<=を使う
  • 組み合わせ回路やテストベンチには=

のが基本となります。

同じ信号に対する代入を複数のalways文で書くとエラーになるようです。

assign文

assign文は論理式1行で書ける組み合わせ回路の記述に使います。

module assign_test(
    input [1:0] sw,
    output [0:0] led
    );
    assign led = ~(sw[0] & sw[1]);    
endmodule

このようにして使います。
このコードはswの0bitと1bitのNANDを取ってLED0の光り方を決定しています。

`select`が10ならin0を、1ならin1をoutに出力する4bitマルチプレクサは

module mux(
    input select,
    input [3:0] in0, in1, 
    output [3:0] out
    );
    assign out = s ? in1 : in0;    
endmodule

のように書きます。?はC言語と同じように条件分岐を表す三項演算子として使えます。

assign文で書かれた代入文は同時に実行されます。ゲートが作られて常に接続されるイメージです。
このため、assign文で生成する信号はネット型(wireなど)である必要があります。

一方、always文で生成する信号はreg型である必要があります。
しかし、always文は組み合わせ回路を生成する場合もあります。レジスタを持たないのにreg型を使うのは混乱の原因となります。
そこで、SystemVerilogではlogicという新しい型が導入されました。
logicはネット型とレジスタ型の両方で使用できるため、使い分けを考える必要がなく混乱を避けることができます。
なお、logicにはネット型・レジスタ型にあった複数の値が代入されたときの最終値決定機能はないため、複数のalways文やassign文からの代入はできません。

function文

function文はassign文で記述できない複雑な組み合わせ回路を書くときに使われます。
function文の中ではif文やcase文などの制御構造を利用できます。

最初のデコーダーの例をfunction文を使って書き直すと

module decoder(
    input [2:0] sw,
    output [7:0] led
    );
    
    function [7:0] DECODER (
        input [2:0] INPUT
    );
    begin
        case (INPUT)
            3'b000: DECODER = 8'b00000001;
            3'b001: DECODER = 8'b00000010;
            3'b010: DECODER = 8'b00000100;
            3'b011: DECODER = 8'b00001000;
            3'b100: DECODER = 8'b00010000;
            3'b101: DECODER = 8'b00100000;
            3'b110: DECODER = 8'b01000000;
            3'b111: DECODER = 8'b10000000;
        endcase
    end
    endfunction
    
    assign led = DECODER(sw);  // functionを使ったassign文
endmodule

のようになります。

function文は

function <戻り値の宣言> (<入力の宣言>);
<処理内容>
endfunction

のように記述します。

処理内容が2文以上になる場合はbegin〜endで囲みます。(この例では1文しかありませんが囲んであります)
function文は処理を定義しているだけなので、実際に使うにはassign文を使って関数を呼び出す必要があります。

generate文

generate文はparameterに応じて回路を切り替えるときや大量のモジュールを自動でつなげるときに使うみたいですが、まだよく分からないので解説できません。
初めてでも使えるVerilog HDL文法ガイド ―― 記述スタイル編|Tech Village (テックビレッジ) / CQ出版株式会社などを参照してください。

モジュールの接続

大規模な回路になると一つのmoduleにすると記述量が膨大になるので、いくつかの下位moduleに分割して記述し上位moduleで結合するという設計をしたくなります。

<下位モジュール名> <インスタンス名> (<接続する信号>, <接続する信号>, ...);

のようにすれば接続できます。
接続する信号は、module定義のポートの順番と揃える必要があります。

.<定義側ポート名> (<接続する信号>)

のようにして、名前でポートを指定することもできます。こうすると順番は任意です。

フルアダーを4つつなげて4bitリップルキャリーアダーを作る例を示します。
理論はコンピュータアーキテクチャの話 (70) 演算器の設計 - 加算器(Adder) | マイナビニュースを参照してください。(フルアダーの回路は少し変えてあります)

module adder(
  input [3:0] A,
  input [3:0] B,
  output [3:0] S,
  output C
  );
  logic [3:0] signal;
  logic zero;
  assign zero = 0;
  
  FA add1(A[0], B[0], zero, S[0], signal[0]);
  FA add2(A[1], B[1], signal[0], S[1], signal[1]);
  FA add3(A[2], B[2], signal[1], S[2], signal[2]);
  FA add4(A[3], B[3], signal[2], S[3], C);
endmodule
module FA(
  input A,
  input B,
  input Cin,
  output S,
  output Cout
  );
  assign S = (A ^ B) ^ Cin;
  assign Cout = ((A ^ B) & Cin) | (A & B);
endmodule

初めてでも使えるVerilog HDL文法ガイド ―― 記述スタイル編|Tech Village (テックビレッジ) / CQ出版株式会社を参照してください。

SystemVerilogだとinterfaceというものを使って簡単に書けるらしいですが、調査中です。

演算子

仕様書のp.221から演算子の優先順位表を引用します。(Verilogにはない演算子も混ざっています)
f:id:kivantium:20160917183111p:plain:w800
基本的にC言語などと同じですが、いくつか馴染みがないものもあるので紹介します。

  • ~&, ~|, ~^: それぞれNAND, NOR, EX-NORを表します。
  • ===, !==: 結果がunknownになる可能性のある==, !=と異なり、xとzも含めて比較します。
  • ==?, !=?: x, zはwildcardと扱った上で比較します。
  • <<<, >>>: 論理シフトを表す<<, >>と異なり、算術シフトを表します。

ビット連接

ビットをつなげるには{}演算子を使います。

assign y = {{16{a[15]}}, a[15:0]};

こうすることで、上位bitからa[15]を16個・a[15:0]と並べていった値がyに入ります。
この記法は即値の符号拡張などで使えます。

{a, b, c} = vectorのような記法でベクトルの分割もできるらしいです。

列挙型(enum)

SystemVerilogで追加された型です。
ステートマシンの状態名を列挙するときに便利です。

typedef enum logic [1:0] {
    START, WAIT, RECEIVE, SEND
} state;
state current_state;

のようにして使います。

データタイプを指定しないとデフォルトではint型になります。
また、各列挙名の値を指定することもできます。

typedef enum logic [3:0] {
    START   = 4'b0001,
    WAIT    = 4'b0010,
    RECEIVE = 4'b0100,
    SEND    = 4'b1000
} state;
state current_state;

SystemVerilogでは構造体・共用体もあるらしいですが、使う機会が思い浮かばないので省略します。

テストベンチ用構文

テストベンチは、

  • ポートのないモジュールを作る
  • 入力を用意する
  • always文でクロックを用意する
  • テスト対象のモジュールをつなぐ
  • initial文の内部で入力を順次与える

というようにして書くらしいです。

まだテストベンチのことがよく分かっていないので調べるためのメモだけ残しておきます。

  • task: サブルーチンにあたるもの
  • ループ構文: for, while, repeat, forever, force, release文などがある
  • システム・タスク: $displayなどの画面表示や時刻を表す$timeがある
  • コンパイラ指示子: `define, `timescale, `include, `ifdef, `else, `endif, `elsifなど
  • アサーション: assert, assume, cover, property, sequenceを使って書けるらしい


先ほどのの4bit加算器に対するテストベンチのコードを書きます。
最初の行は、数字の単位がnsで、シミュレーションはpsの精度で行うことを指定します。

`timescale 1ns/1ps
module testbench;
  logic [3:0] A;
  logic [3:0] B;   
  logic [3:0] out;
  logic carry;

   
   int answer, a, b;
   
   adder adder_instance (A, B, out, carry);
   
   initial begin
     assign A = a;
     assign B = b;
     for (a = 0; a <= 7; a++) begin
	   for (b = 0; b <= 7; b++) begin
	       #10; // delay 10 cycle
	       assert(out == a+b) $display("correct: %0d + %0d = %0d", a, b, out);
	       else $display("wrong: %0d + %0d = %0d", a, b, out);
	   end
	 end
     $finish;      
   end
endmodule

Add Sourcesから「Add or create simulation sources」を選んでtestbenchというファイルを追加し、このコードを追記します。
左側のSimulation画面から「Run Simulation」を実行すると

# run 1000ns
correct: 0 + 0 = 0
correct: 0 + 1 = 1
correct: 0 + 2 = 2
correct: 0 + 3 = 3
correct: 0 + 4 = 4
(中略)
correct: 7 + 5 = 12
correct: 7 + 6 = 13
correct: 7 + 7 = 14
$finish called at time : 640 ns : File "〜" Line 18

のようにしてテストベンチが走り、全ての入力に対して正しい結果を吐く回路が生成されたことが確認できます。

テストベンチを書くときに役立ちそうな文法事項としては$readmemh, $readmemb, $dumpfileなどがあります。
使用例はおいおい……