KirIn 落書き帳

素人がプログラミング, FPGA, LSIをお勉強しているメモ書きです。間違いがあればご指導していただけたら幸いです。

Verilog HDL & VHDLテストベンチ記述の初歩 自分用メモ

Verilog HDL&VHDLテストベンチ記述の初歩 (DESIGN WAVE MOOK)

Verilog HDL&VHDLテストベンチ記述の初歩 (DESIGN WAVE MOOK)

Verilogのテストベンチ記述について言及している日本語の本はあまりないと思います。 いい本です。

以下は自分用メモです。

Testbench実行の流れ

  1. 期待値ファイルを作成します。
    • 期待値ファイルとは、テストを行う要素回路の期待される出力が記述されたファイル
    • 期待値ファイルはソフトウェアで作成することが望ましいが、ない場合は初回のテストベンチ実行出力ファイルの中身をよく観察した後にそのファイルを使用します。
  2. diffコマンドを使って期待値ファイルとの差分を確認して、ソースコードの修正及び確認を行います。

パターン・ファイルによるテスト入力の生成

テストバターンを実行したい場合、テストパターン入力部分と、テストベンチの共通部分は分けて記述するべきです。 テストバターン入力にはreadmemh or readmembを使用します。

taskを用いてテストベンチを構造化

回路のリセット機能や、ある程度クロックサイクルが必要となる処理(ENの切り替えのあとに出力パターン確認)等のテストベンチ上で何回か出る可能性のあるものはtaskを用いて構造化するべきです。

利点は * 記述量の現象 * 間違いの減少、デバック効率上昇 * 読解性の向上

クロック・エッジ・ベースのタイミング制御

テストベンチが複雑及び構造化していくと、思わぬところに遅延を入れてしまいミスが起こってしまう可能性があります。

そこでクロック・エッジ・ベース@(event) statementを利用して遅延の影響を受けないようにするべきだそうです。 一見、記述量が増えてしまい面倒になるかもしれませんが、クロック・エッジと信号の変化のタイミングが固定されるので安全です。

階層化の記述方法

`includeでファイルを分けることによって可読性を向上させます。 例)

tbench.v

module tbench;

parameter DELAY=10;

`include "clk_common.h"
`include "instance_0.h"

initial begin
               RST_N=1'b1; count_on=1'b0;
    #DELAY
    #CYCLE     RST_N=1'b0;     
    #CYCLE     RST_N=1'b1;     
    #CYCLE     count_on=1'b1;     
    #CYCLE     count_on=1'b0;     
    #CYCLE     count_on=1'b1;     
    #CYCLE     count_on=1'b0;     
    ...
    #(5 * CYCLE) $finish;
end

endmodule

clk_common.h

parameter CYCLE = 100;
parameter HALF_CYCLE = 50;
reg CLK;

always begin
                CLK = 1'b1;
    #HALF_CYCLE CLK = 1'b0;
    #HALF_CYCLE;
end

counter_instance.h

reg RST_N, count_on;
wire [3:0] count4;

coutner counter_0(.CLK(CLK), .RST_N(RST_N), .count_on(count_on), .count4(count4));

上記の様な感じで分割して可読性を上げる事が重要です。

実際のテストベンチのディレクトリ構成例

複数バターンの検証の際にどのようにディレクトリを分けるかの例

top directory
├── lib
│   └── dummy_CPU_tsk.h
├── rtl
├── sim1
│   ├── Work
│   └── tb
│       ├── DataGenerator.v
│       ├── dummy_cpu2.v
│       └── param.h
└── sim2
  • lib : 共通タスクや共通して使用するテストベンチ用モジュール
  • rtl : 検証対象モジュール
  • sim1 : パターン1の実行ディレクトリ
    • Work : ワークディレクトリ、Makefileやログ等
    • tb : パターン独自の検証環境
  • sim2 : パターン2の実行ディレクトリ

パラメータ引き渡しの際の注意点

module #(param1, param2) instanceという書き方では`includeで代入したparameterがかわりに書き換わる可能性があるので、必ずmodule #(.param1(param1), .param2(param2)) instanceと記述します。(verilog HDL 2001から導入)

期待値比較自動化の記述方法

期待値ファイルとシミュレーションファイルとのdiffなどで比較するべきと書きましたが、テストパターンが増えてきますと、煩雑 & ケアレスミスが増加します。

期待値比較に必要な部分を記述します。

module tbench;

parameter STEP = 100;
parameter STB = 10;
parameter PATTERN_NUM = 8;

// 観測データ
wire [3:0] q;
//期待値データを入れる配列
reg [3:0] mem2 [0:NUM];

....

initial begin
    // 期待値ファイル読み出し
    $readmemh("expect.hex", mem2);
    for(i=0; i < PATTERN_NUM; i=i+1) begin
        #(STEP-STB) if (mem2[i] !== q) begin
        $display("Mismatch Error!");
        $display("q=%h expect=%h", q, mem2[i]);
        end
        #STB;
    end
end
endmodule

ここで注意すべき点はVerilog等号演算子です。

演算子 A=X B=X A=!X B=X
==
===
!=
!==

以上の表のように不定値が入った場合の判定方法から===!==を期待値判定に使用します。

比較の待機

while(条件式) <ステートメント>を使用して条件がマッチするまで待機

// 例
while(CC_EN != 1) @(posedge CLK);

$display(...

期待値比較の欠点

実際に期待値比較を始めると先ほどのwhileのように必要なタイミングの時の値のみを確認することになります。 しかし期待値比較の範囲を誤っていた場合、回路の誤りが検出できません。

この誤りの研修方法は確立されていないようです。 アサーション(コントロール信号のプロトコルやタイミング関係をチェックし、違反を出力)を行うことが重要です。

つまりどのような工程で検証を行うかの確認項目をしっかり作って検証を行うべきです。

verilog構文のforce, releaseも使えるかも?

  • リファレンスモデル : 期待値ファイルのこと。
  • ビヘイビアモデル : 処理の結果だけを実現するモデル、基本的にリファレンスモデルはこのモデルで記述される。

テストバターン表の作成

P141 第10章 テストパターンの検討はわかっているつもりでも、忘れてしまうことが多い気がします。 たまに読み返したい。

特にコラムパターンが多ければテストが早く終わるは肝に命じるべきです。

入力するバターン及び検証対象回路の制御信号の、最小値、最大値、中間値をチェックします。 最終的にテストパターン表などを作成します。

まず、デフォルトパターンを決定し、波形で接続ミス等のケアレスミスを検出する。 ちなみに、デフォルトパターンは小規模なパターンを選択するように。 次に、単機能検証に入りますが、チェックをしながらバグを修正します。その際に以前のパターンでエラーが出ないか再度確認するのに、時間が非常にかかります。 はじめの数パターンは波形による目視でも良いかもしれませんが、できるだけ早い時期に期待値照合ができる環境を整えるべきです。 最後に複合機能検証パターンを行います。これは期待値照合ができる環境が必須となります。

効率よくテストするには

バグがあった場合の疑う順番

  1. テストパターンの入力を疑う
  2. テストパターンの観測方法を疑う
  3. 検証対象回路を疑う

複雑な回路のテスト

演算部と制御部に分けてテスト、具体的には演算部は単体で、制御部は演算部をパススルーにしてテストを行います。

作業効率の向上

グループ検証とRTLコードのバージョン管理

どこかのサーバの共有リポジトリに上げる際のVersionの付け方。
ある回路Aの機能Xを検証するテストパターンがYまで終了している場合 -> 0.X-1.Y
Versionを上げる際には、これまで正常動作していたパターンを全て確認してから上げます。

// 例 lsft.v
/*================================================================================
| Project name
|---------------------------------------------------------------------------------
| Block name  : lsfr
| Version     : 0.0.2
| Description :
|     - 8bit Fibonacci LSFR
|     - feedback polynomial : x^8 + x^6 + x^5 + x^4 +1
|     - look about LFSR! : Wikipedia or Xilinx Efficient Shift Registers, LFSR...
|---------------------------------------------------------------------------------
| Created by author name : author's email address
\===============================================================================*/

パラメータファイルの自動生成

パターンファイルはEXCEL等に記入して、どのようなテストを行っていくかを管理します。 EXCELからパターンファイルを自動生成できるようにしておくべきです。

コード・カバレッジ

検証もれのないフローを実現するためにコード・カバレッジを導入します。 コード・ガバレッジとは、シミュレーション中に実行されたHDL記述のコードの網羅率のことです。 殆どのシミュレータでサポートされている検証指標みたいです。(ModelSim Altera Web Edition では使えない?)

// 実行例
coverage 56% <- コードの全行に対する実行行の割合

...

begin
    case( DIN )
        4'b0000 : DECODE = 5'b11111; 11 <- 実行回数
        4'b0001 : DECODE = 5'b01100; 8
        4'b0001 : DECODE = 5'b01100; 0  <- 一度も実行されていないのでテストパターン or 検証対象回路の調査を行います。
        ...
        default : DECODE = 5'bxxxxx; 5

sed正規表現で0の部分だけ取り出せるようにしたいところ。。。

ゲートレベルシミュレーション

ここまではRTLでの検証についてでした。 ここからは論理合成後のお話になります。 ゲートレベルシミュレーションは実行に時間が掛かるためRTLで行った全ての検証を行うことがおそらく無理です。 そこで、リグレッションテスト(できる竹機能を組み合わせて、最小のパターン数で機能を網羅できるテスト)とタイミング検証のみ行います。

非同期回路対策

異なるクロック間でデータをやり取りする際に、データ信号線の遅延によって思わぬ動きを見せることが有ります。 問題を早期に発見するために異なるクロック間での信号線に#DLAY等で様々な遅延パラメータで検証を行っておくべきです。

クロックスキューとジッタによってクロックの載せ替えによる問題発生を検出するためにジッタ・モデルを挿入する方法があります。

module jitter(A, Y);
parameter DLY = 5;
input [3:0] A;
output [3:0] Y;

assign #(DLY)     Y[0] <= A[0];
assign #(0)       Y[1] <= A[1];
assign #(DLY/2)   Y[2] <= A[2];
assign #(DLY*7/8) Y[3] <= A[3];
endmodule

このモデルは論理合成時に削除しなくても、遅延の記述は単に無視されるのでそのままでOKです。 ジッタモデルの階層が残ってしまう時は、この階層を破壊するコマンドを論理合成ツールで出します。

タスクによるテスト内容構造化例

まずおおまかにテスト手順を考えます。

  1. テストシナリオ-0
    1. 初期化
    2. 機能1起動
    3. 機能2起動
    4. 評価
    5. 終了

次にテストシナリオの各シーケンスについての内容をフローにまとめます。

  1. 初期化
    1. リセット
    2. レジスタ00へデータ0F書き込み
    3. レジスタ01へデータ83書き込み
    4. レジスタ02へデータ01書き込み

最後に初期化シーケンスで使用されているプロトコルについて記述します。 1. リセット 2. ...の各プロトコルの記述を行います。 基本的にここにのみタイミング情報を記述すべきです。 なぜなら変更等があった場合にプロトコルを修正するのみで大幅な修正が行えるからです。

テストシナリオ記述例

initial begin
    A = 8'hz; D = 8'hz;
    WEB = 1'b1;
    #DELAY;
    @(posedge CLK) #DELAY;

    InitialSeq(4'hF, 3'h3, 3'h9);
    Function1start(...);
    Function2start(...);
    Judgment(...);

    @(posedge CLK) #DELAY;
    $finish;
end

シーケンス記述例

task InitialSeq;
input [3:0] CLKMode, DataMode, DataLength;
begin
    ResetDo();
    CPUWrite(8'h00, {4'h0,CLKMode});
    CPUWrite(8'h01, {DataLength,DataMode});
    CPUWrite(8'h02, 8'h00);
end
endtask
...

プロトコル記述例

task CpuWrite;
input [7:0] addr, data;
begin
    A = addr; D = data;
    WEB = 1'b0;
    @(posedge CLK) #DELAY;
        A = 8'bz; D = 8'bz;
        WEB = 1'b1;
end
endtask
....