ISim によるテストの自動化を考える のバックアップ(No.17)

更新


公開メモ

テストを自動化したい

テスト駆動開発

ソフトウェア開発で最近はやりの手法として、 テスト駆動開発(TDD: test-driven development)というのがあります。

Xilinx WebPack の ISim を使って Verilog による回路設計で テスト駆動開発をやりたいというのがこの記事の趣旨になります。

背景

ソフトでも、回路でも、プロジェクトが大きくなり複雑になると、 コードの一部を修正したことが思ってもみない部分に波及して、 新しいバグを生んでしまう(デグレードしてしまう)不幸が頻繁に起こります。

かといって、「動いているコードを変更するなんて愚の骨頂」 などという旧態依然とした態度でいると、 バグつぶしや新機能の導入のたびに場当たり的なつぎはぎが行われ、 コードの見通しが悪くなったり、 構造的なゆがみが蓄積してして、結局どこかで破綻をきたします。

そこで、コードに加えた変更がどこか他に悪影響を与えないことを確信できるようにして、 既存コードの改変を容易にしようというのがテスト駆動開発の原点になっています。

(ソフトの方ではテストを書くことで仕様が明確になるとか、 テストをしやすいモジュール構成にすること自体が結果的に見通しの良い設計を誘発するとか、 他にもいろいろ利点があることが宣伝されていますので、 興味のある方はググってみて下さい。)

gui がからむソフト開発に比べて、 回路は動作を機械的に定義しやすいので、 テスト駆動はとても向いてると思うのです。

テスト駆動開発の流れ

理想的なテスト駆動開発では、 コードを書いたり変更したりするのに先だって、 まずそのコードがクリアすべきテストを記述します。

そうやってテストを記述した後、実際のコードを書いて、 そのコードがちゃんとテストをクリアすることを確認して、 ようやく次のコードに移ります。

次のコードを書くときにはまた、先にテストを書いて、後からコードを書いて、 とやりますが、そのコードをテストする際には、はじめのテストと新しいテストの 両方が問題なく通ることを確認します。

つまり、後から書いたコードがそれ自身の目的を果たすだけでなく、 それまでに通っていたテストの結果を悪化させていないことも確認するわけです。

そのようにしていくと、コードが増えると同時に、テストもどんどん増えていき、 理想的にはすべてのコードがテストで守られている状況を作ることができます。

このようになっていれば、コードにどんな大きな変更を加えても、 その後テストを走らせることで その変更が遠くの方でデグレードを引き起こしたりしていないという 確信を持てる理想的な状況を実現できます。

この安心感があるおかげで、 テスト駆動開発ではすでに動いているコード部分についても、 コードの読みやすさや構造的な改善のため、一部を改変することが容易であり、 常にコードを読みやすく、直しやすい状態に保つことができます。

このようなテストに裏打ちされた既存コードの改善 (動作を変えずに読みやすさや構成を改善する変更)は 専門用語で「リファクタリング」と呼ばれています。

テストの自動化が重要

上記のように、テスト駆動開発ではコードと同じか、 あるいはそれを上回る量のテストが書かれます。

そして、コードの主要な改変のたびに何百、何千もある すべてのテストを走らせる必要があります。

したがって、普通にテストベンチを書いて、その結果の波形を目で追いながら確認する、 というような従来型のテストベンチの使い方は、テスト駆動開発では役に立ちません。

テスト駆動開発でのテストは、次のような自動テストであることが必須です。

  1. テストはかならず「成功する」か「失敗する」かのどちらかを結果とする
  2. すべてのテストを自動的に連続して走らせることができる
  3. どのテストが成功して、どのテストが失敗したかを容易に把握できる

ISim を使った自動テストに必要な技術

上記の目的を果たす自動テストを ISim を使った Verilog コード検証で行うことを考えると、そのためには Xilinx WebPack 上の ISim を使って、

  • 「成功」か「失敗」かを返すテストベンチを書く方法
  • 書きためた多数のテストベンチを一気に走らせる方法
  • テストベンチの結果(成功・失敗)を一覧する方法
  • そのあたりを考慮したプロジェクトフォルダ内のファイル配置

を確立する必要があります。

どこかにまとめられてたりしません?

で、こういう内容を駆け出しの私が一から考えても前途多難なのは目に見えているのですが、 経験に裏付けされたお勧めのプラクティスとか、どこかで紹介されてないでしょうか?

どなたか知っていたら教えて下さい。

二度目に四角い車輪を発明するような愚は犯したくないのです(−−;

見つけられないでいます(TT

とはいえ、自分ではこれまで上記のような内容の記事を見つけられていません。

しかたがないので、以下で苦しむ過程を書きつつ、 何とか良さそうな解を探ってみようと思います。

今更こんなことを調べているのは不毛な気がするので、 どこかに答えがあればどなたかぜひ教えて下さい!

「成功」か「失敗」かを返すテストベンチの書き方

できればコード上でのテスト失敗位置を簡単に見つけられるようにしたいと考えて、 以下のようなマクロを使う方法を試しています。

これを使えば、ログに残ったエラーメッセージを見るだけで、 コードのどの部分でエラーが生じたかが一目瞭然になります。

テストベンチを書く際の手間が非常に小さくて済むのも お勧めできる点です。

テストベンチ記述に役に立ちそうなマクロ文法

LANG:verilog
`define ERROR(msg) \
   $display(`"%s(%0d) ERROR at #%0d: msg.`", `__FILE__, `__LINE__, $time)

としておいて、テストベンチ中で

LANG:verilog
if(variable!=expected)
    `ERROR(思ってたのと値が違う!);

と書くと、

LANG:console
C:/hoge_proj/moge_test.v(60) ERROR at #100: 思ってたのと値が違う!.

などというエラーメッセージを表示できます。

このようなエラー行からは、例えば秀丸のタグジャンプでソースファイルに飛べるのでとても便利。

  • 実は上記の _LINE_ や _FILE_ は verilog2001 には規定されておらず、 ISim のマニュアルにも記述がないのですが、C の流儀で書いてみたら通ってしまいました
  • `"〜`" という不思議なクォーテーションは、マクロ引数を展開してくれる書き方だそうです

  • エラーの発生時刻は $time で得られます
  • マクロが複数行にわたるときは \ で繋ぎます

自動テスト用テストベンチ記述に使うインクルードファイル

LANGUAGE:verilog
`ifndef XILINX_ISIM

  // シミュレーション時以外は何もしない

  `define INFO(msg) 
  `define ERROR(msg) 
  `define DONE 
  `define ASSERT_EQ(variable, value) 
  `define ASSERT_EQ_ALWAYS(variable, value) 
  `define ASSERT_EQ_AT(variable, value, the_event) 

`else

  // ISim 上での動作
  // ファイル名&行番号も表示する
  
  `define INFO(msg)   \
      $display(`../../"%s(%0d) INFO at #%0d msg.`",  `__FILE__, `__LINE__, $time)
  
  `define ERROR(msg)  \
      $display(`"../../%s(%0d) ERROR at #%0d msg.`", `__FILE__, `__LINE__, $time)
  
  `define DONE        \
      $display(`"../../%s(%0d) DONE at #%0d.`",      `__FILE__, `__LINE__, $time)
  
  // variable が value に等しいことを検証
  `define ASSERT_EQ(variable,value)                                             \
      if((variable)!==(value))                                                  \
          $display("../../%s(%0d) ERROR at #%0d: ", `__FILE__, `__LINE__, $time,      \
                   `"Assertion [ variable === value ] failed `",                \
                   variable, " != ", value, ".")
  
  // variable が常に value に等しいことを検証する always に展開される
  `define ASSERT_EQ_ALWAYS(variable, value)                                     \
      always @(variable, value)                                                 \
          if((variable)!==(value))                                              \
              $display("../../%s(%0d) ERROR at #%0d: ", `__FILE__, `__LINE__, $time,  \
                       `"Assertion [ variable === value ] failed `",            \
                       variable, " != ", value, ".")
  
  // variable が @(the_event) のタイミングで常に value に等しいことを検証する
  // the_event の部分は posedge clk とか negedge clk など @(the_event) 
  // としたときにおかしくない形で指定する
  `define ASSERT_EQ_AT(variable, value, the_event)                              \
      always @(the_event)                                                       \
          if((variable)!==(value))                                              \
              $display("../../%s(%0d) ERROR at #%0d: ", `__FILE__, `__LINE__, $time,  \
                       `"Assertion [ variable === value @ the_event ] failed `",\
                       variable, " != ", value, ".")

`endif

テストベンチでこれをインクルードすれば、分かりやすいエラーメッセージを簡単に表示できます。

古い C でマクロを駆使してた頃を思い出しますが(−−;

  • こういう場合、値の比較は != ではなく !== で行うのが良いですね。
  • ファイル名に ../../ を付けて表示しているのは、 下記で検討したフォルダ構成に合わせた物で、 beh/temp/*.log に保存されたログファイルからタグジャンプでソースを開くための工夫です。

波形を比較するというアプローチ

まずは波形を目視で確認する試験を行って、 もしうまく動いたことが確認できたならば、 そのときの波形を記録してしまえば、 信号値の理想波形が得られます。

そうしておいて、 その後のテストは実波形と理想波形との差分を取ることで行う、 という機械的なアプローチは、結構汎用的に使えるかもしれません。

パラメータによって

  • 何もしない(目視のシミュレーション中)
  • 実波形をファイルへ記録する(理想波形の記録)
  • 実波形をファイルに記録された波形と比較して、必要に応じてエラーを吐く(以降のテスト)

の機能を切り替えられるようなデバッグ用のモジュールを作っておいて、 必要な信号線をありったけ繋いでおけば良さそうですね。

LANG:verilog
module wave_tester #(
    parameter FUNCTION  = "none", // "none" / "record" / "compare"
    parameter WAVE_FILE = "",
    parameter BIT_COUNT = 1,
    parameter PERIOD = 0  // >= 1 なら #PERIOD ごと、0 なら posedge clk ごとに検査
) (
    input wire gate,      // gate == 1 の時に限り検査される
    input wire [BIT_COUNT-1:0] data,
    input wire clk,       // PERIOD == 0 のときのみ使われる
    output wire error     // asserted when comparison fails
);

こんな感じでしょうか?

信号波形の記録・読み出しには、アプロさんがコメント欄で教えて下さったように、

$fopen, $fwrite, $fscanf 等が使えそうです。サブモジュールとする場合に、

$fclose を呼び出すタイミングが難しそうですが・・・
http://www.asic-world.com/verilog/verilog2k3.html

通常プログラムが終了する際にはすべてのファイルハンドルが自動で閉じられる ことが多いので、手抜きで閉じなくても大丈夫だったり?

ただ、この形だと繋いだ信号線の信号名は失われてしまうので、 エラーが生じた際の原因究明にはちょっと手間がかかるかもしれません?

もっとうまい方法はあるでしょうか?

tcl で書いた方が良いという可能性も調べてみなければ。

成功と失敗の判別

エラーメッセージをログファイルに溜めておいて、 その中から "ERROR" という文字列が見つかれば失敗、 見つからなければ成功、という単純な見分け方ができるはず。

書きためた多数のテストベンチを一気に走らせる方法

多数のテストがあれば、 そのすべてを走らせるのに時間がかかってしまうのは仕方がないのですが、 それら1つ1つを手動でコンパイルして走らせるのは面倒なので、 バッチファイルあるいはmakefileのような物で一気にコンパイル&実行して、 後から結果だけを確認したいです。

本来なら xUnit 系のツールのようにインターフェースを gui 化して、 そのとき必要な範囲のテストだけを選んで走らせる、 なんてこともできればよいのだけれど、そこまでは望めない・・・かな?

テストのコンパイル&実行の基本コマンド

ISE から Simulate Behavioral Model をダブルクリックしたときの動作を追うことで、 何をしたらよいかが分かります。

ISE の Console にコンパイルからシミュレータの起動までの一連の流れが表示されますので、 それをなぞるスクリプトを書けばよいことになります。

はじめに環境を整える

C:\Xilinx\12.4\ISE_DS\settings32.bat あるいは C:\Xilinx\12.4\ISE_DS\settings64.bat を起動することで、Xilinx のコマンドラインツールを使うのに必要な環境変数が正しくセットされます。

Language:Console
C:\hoge_proj>C:\Xilinx\12.4\ISE_DS\settings64.bat

私はテスト用のツールを cygwin 環境で使いたかったので、
C:\Xilinx\12.4\ISE_DS\settings64.bat を同じディレクトリに
C:\Xilinx\12.4\ISE_DS\settings64-cygwin.bat という名でコピーして、
その中の

REM Execute command if any

という行以降を

C:
chdir C:\cygwin\bin
bash --login -i

に書き換えて、独自のスクリプトを作りました。

この .bat のショートカットを作成して、プロパティから

  • 編集オプション : 簡易編集モード
  • フォント
  • レイアウト
  • アイコン

等の設定をいじって、ISim デバッグ用のコンソール起動アイコンとして利用しています。

ただ、Windows7 64bit での cygwin 環境はたまにおかしな動作をするようで、 まだその再現性や対処法を見つけられないでいます。

テストベンチソースファイルを fuse でコンパイルして *_isim_beh.exe ファイルを作る

hoge モジュールのテストベンチに hoge_test と言う名前を付けた場合、 通常の呼び出しは以下のようになっています。

LANG:console
fuse                                 \
       -intstyle ise                 \
       -incremental                  \
       -lib unisims_ver              \
       -lib unimacro_ver             \
       -lib xilinxcorelib_ver        \
       -o hoge_test_isim_beh.exe     \
       -prj hoge_test_beh.prj        \
       work.hoge_test work.glbl

ここで参照している hoge_test_beh.prj には

verilog work "hoge.v"
verilog work "hoge_test.v"
verilog work "fuba_module.v"
verilog work "C:/Xilinx/12.4/ISE_DS/ISE//verilog/src/glbl.v"

のように、テストベンチ hoge_test が依存するモジュールファイルが記述されています。

.prj ファイルは ISE からテストベンチを起動するときに自動で作られるものですが、 何か別の作業をしているといつの間にか消えてしまうようです?

モジュールの依存関係を自前で解析するのは骨が折れるので、 このファイルを別ディレクトリに取っておいて、 その .prj ファイルを元に自動テストを行うことにしました。

コンパイル結果のシミュレータは .prj ファイルに記述された HDL ファイル以外に、 そこから参照される `include ファイルにも依存することになりますが、 インクルードによる依存関係は HDL ファイルの中を grep すれば解析可能なので、 自前で見つけることにします。

*_isim_beh.exe の実行

できあがったシミュレータを実行するときは、 標準入力に run all を送ってやる必要があります。 (isim.cmd のようなバッチファイルを記述しても良いのですが、 標準入力を使った方が楽だと思います)

標準出力をファイルにリダイレクトすることでログを取ることができます。

LANG:console
$ (echo run all | hoge_test_isim_beh.exe) > hoge_test_isim_beh.log
$ cat hoge_test_isim_beh.log
 ISim log file
 Running: hoge_test_isim_beh.exe
 ISim M.81d (signature 0x12940baa)
 Time resolution is 1 ps
 Simulator is doing circuit initialization process.
 C:/hoge_proj/foba_test.v(66) ERROR Assertion [ value == expected ] not met at #0: 0 != 1
 Finished circuit initialization process.
 C:/hoge_proj/foba_test.v(60) ERROR エラーが起きた!
 C:/hoge_proj/foba_test.v(62) DONE.
 Stopped at time : 100 ns : File "C:/hoge_proj/foba_test.v" Line 63

run コマンドにより、テストベンチは自ら $finish; で終了するまで走り続け、 その間に、もしエラーが生じればログファイルにログを残します。

$finish を書き忘れると戻ってこないわけですが・・・ そこは書く方が気をつけると言うことで。

複数のテストベンチを一括で自動実行するには

例えば、ISE が作ってくれる *_isim_beh.prj ファイルを多数 beh/ フォルダに入れておいて、下記の ruby スクリプトを走らせると、 それらを順番にコンパイルして実行することができます。

LANG:ruby
#!/usr/bin/ruby

# 古いログファイルを消す
system("rm simulate_all.log")

# .prj ファイルすべてをコンパイル&実行する
Dir.glob("beh/*.prj").each do |prj|
  prj =~ /([^\/]+)_beh.prj/
  module_name = $1
  prj = $~                          # 先頭の beh/ を取り除く
  system("cp beh/#{prj} #{prj}")
  system("fuse "+
    		"-intstyle ise "+
      		"-incremental "+
      		"-lib unisims_ver "+
      		"-lib unimacro_ver "+
      		"-lib xilinxcorelib_ver "+
      		"-o #{module_name}_isim_beh.exe "+
      		"-prj #{prj} "+
      		"work.#{module_name} work.glbl")
  system("echo ===== #{module_name}_isim_beh.exe >> simulate_all.log")
  system("echo run | ./#{module_name}_isim_beh.exe >> simulate_all.log")
end

この場合、実行結果は simulate_all.log と言うファイルにログとして残ります。

ただ、まだいろいろ改善したいところも残ってます。

必要なファイルだけコンパイル&実行したい

上記スクリプトではファイルが更新されたかどうかにかかわらず、 すべての .prj をコンパイル&実行しています。 ログファイルとソースファイルのどちらが古いかをみて、 必要な物だけ実行するよう変える必要がありますね。

テストベンチ .exe の挙動がおかしい

上記で、一応動くことは動くんですが、 コンパイルしてできた *_isim_beh.exe ファイルを実行する際に、 私の環境では内部で fork してすぐに返ってきてしまうようで、 シミュレーションの終了を待たずに次のコンパイルが始まってしまいました。

これだとPCに余計な負荷がかかるだけでなく、ログファイルにはき出される ログの順序が乱れてしまうことも考えられます。

この現象、スクリプトからでなく手動で起動した場合にも、 tcl コマンドのプロンプトがバックグラウンドプロセスに 回ってしまっているような動作をしていて、とても不思議です。

Windows7 64bit で動かしているのがいけないとか?

もう少し調査してみますが、最悪は Windows API でプロセス一覧を調べるようなアホなことをしなきゃならないかも。

fuse のソース検索パス

fuse は .prj ファイルのある位置を基準に HDLソースファイルを探しに行くようで、.prj ファイルを一段下の beh/ フォルダに置いたままだとうまくコンパイルすることができませんでした。

今は .prj ファイルをプロジェクトフォルダにコピーしてから fuse を起動しているのですが、中間ファイルがすべてプロジェクトフォルダに できてしまうため、望ましくありません。

このあたりも、もう少し詳しく調べる必要がありそうです。

最悪一時ファイルは自動的に消してしまうと言う対処でも良いんですが・・・

コンパイルエラーが生じたときの対応

そうそう、コンパイルエラーへの対処もちゃんとしなきゃ。

テストベンチの結果(成功・失敗)を一覧する方法

ログファイルを grep あるいは ruby かなにかのスクリプトで 処理すればできるんじゃないかと考え中。

簡単には、

LANG:console
$ grep -E "ERROR|" simulate_all.log | nkf -s

とすることで、実行したテストベンチ名と、 その中でもしあればエラー行のみを一覧表示できるようです。

プロジェクトフォルダ内のフォルダ構成をどうするのが良いか

これ結構重要だと思うんですが、試行錯誤が必要になりそうでもあります。

今の ISE だとプロジェクトフォルダ内は一時ファイル等で ぐちゃぐちゃになるので、ソースファイルを一段下に置くことにして、

(project_home)/.git/
(project_home)/.gitignore
(project_home)/project.xise
(project_home)/ipcore_dir/some_core.xise
(project_home)/ipcore_dir/some_core.v
(project_home)/iseconfig/filter.filter
(project_home)/src/hoge.v
(project_home)/src/fuba.v
(project_home)/src/test/hoge_test.v
(project_home)/src/test/hoge_test2.v
(project_home)/src/test/hoge_test3.v
(project_home)/src/test/fuba_test.v
(project_home)/src/geda/geda.v
(project_home)/src/geda/bopa.v
(project_home)/src/geda/test/geda_test.v
(project_home)/src/geda/test/bopa_test.v
(project_home)/beh/hoge_test_beh.prj
(project_home)/beh/hoge_test_beh.wcfg
(project_home)/beh/hoge_test2_beh.prj
(project_home)/beh/hoge_test2_beh.wcfg
(project_home)/beh/hoge_test3_beh.prj
(project_home)/beh/hoge_test3_beh.wcfg
(project_home)/beh/fuba_test_beh.prj
(project_home)/beh/fuba_test_beh.wcfg
(project_home)/beh/geda_test_beh.prj
(project_home)/beh/geda_test_beh.wcfg
(project_home)/beh/bopa_test_beh.prj
(project_home)/beh/bopa_test_beh.wcfg
(project_home)/beh/temp/hoge_test_beh.exe
(project_home)/beh/temp/hoge_test_beh.log
(project_home)/beh/temp/hoge_test2_beh.exe
(project_home)/beh/temp/hoge_test2_beh.log
(project_home)/beh/temp/hoge_test3_beh.exe
(project_home)/beh/temp/hoge_test3_beh.log
(project_home)/beh/temp/fuba_test_beh.exe
(project_home)/beh/temp/fuba_test_beh.log
(project_home)/beh/temp/geda_test_beh.exe
(project_home)/beh/temp/geda_test_beh.log
(project_home)/beh/temp/bopa_test_beh.exe
(project_home)/beh/temp/bopa_test_beh.log

なんてのを考えています。

テストベンチとソースを全然違うフォルダに置いてしまうと探しにくいので、 ルート直下のフォルダで分離するという形にはしていません。

自動テスト中にコンパイルした .exe ファイルは、 ログファイル生成後は必要ないので、 エラーが出ない限りは消してしまっても良いかも? いや、エラーが出た場合には gui を起動して原因を探るかもしれないので取っておこう。

手動で波形を目視によりデバッグする際には、 gui 上に表示する波形の設定を保存した .wcfg ファイルも重要なんですが、現状ではこのファイルは ISE が管理していないため、 どこに置いても使いにくいことには変わらない状況です。

エクスプローラの拡張子の関連づけでうまくやれば .prj ファイルのダブルクリックで、同じディレクトリにある .wcfg ファイルを使って gui を起動することもできるかも? あ、.wcfg ファイルの方を関連づけした方が良いかな? 後でまた検討します。

rake ファイルを作ってみた

以下のファイルを simulation.rake と言う名前で保存して cygwin で使ってみています。

rake については ソフトウェア/cygwin/rakeを入れた に書きました。

LANG:ruby(linenumber)
#!/usr/bin/rake -f
#
# Usage: ./simulation.rake
#   beh/*.prj を読んで必要なテストを実行し各テストの正否を表示する
#
# Usage: ./simulation.rake errors
#   失敗したテスト結果の詳細を表示する
#
# Usage: ./simulation.rake clean
#   beh/temp/* に作成された一時ファイルを消去する
#

TEMP = "beh/temp"
LOG  = "#{TEMP}/simulation.log"
PRJS = FileList['beh/*.prj']    # プロジェクトファイルのリスト
LOGS = []
                                # ログファイルからエラー行を取り出す正規表現
ERROR_REGEX = /ERROR|^WARNING:.*Timing violation in/
MAXLOGSIZE = 1000000            # ログファイルの最大サイズ(これ以上は切り捨てられる)

task :default => ["show"]

directory TEMP

# モジュール名 name のテストベンチを実行してログを取る
# Window7 64bit + cygwin 環境でのおかしな動作を回避するため
# popen で起動している。そうでないと実行終了を待てない?
# 本当はタイムアウトも自動で行いたいが、プロセスの管理が
# どうしてもうまくいかないのでそのままになっている。
# 例えば、Process.kill :INT, io.pid が pid not found でエラーになる
def executeTestbench(name, maxlogsize)
    open("beh/temp/#{name}.log", "w") do |log|
        trap(:INT) { log.puts "==== EXECUTION ERROR : INTERRUPTED ===="; exit }
        IO.popen("beh/temp/#{name}.exe", "r+") do |io|
            io.puts "run all"
            io.close_write
            io.each_line do |line|
                log.puts line
                if log.pos > maxlogsize
                    log.puts "==== EXECUTION ERROR : TOO LARGE LOG FILE (#{maxlogsize} bytes) ===="
                    break
                end
            end
            io.each_line do |line|
                ;
            end
        end
    end
end

# プロジェクトファイルからログファイルを作成する生成式を登録する
PRJS.each do |prj|

  prj =~ /([^\/]+)_beh.prj/
  mod = $1
  exe = "#{TEMP}/#{$1}.exe"
  log = "#{TEMP}/#{$1}.log"

  # .prj ファイルに書かれた依存関係を読み出す
  deps = []
  IO.readlines(prj).each do |l|
    deps << l.chomp.sub(/^[^"]+"/,'').sub(/".*/,'').gsub(/\\/, "/").sub(/^([A-Z]):/, "/cygdrive/\\1")
  end

  # `include をたどって依存関係を追加する
  def dependences(deps)
    result = []
    deps.each do |dep|
      IO.readlines(dep).each do |line|
        next unless line =~ /^\s*`include\s*"([^"]+)"/
        result << $1.gsub(/\\/, "/").sub(/^([A-Z]):/, "/cygdrive/\\1") 
      end
    end
    result = result.delete_if{|dep| deps.include?(dep)}.sort.uniq
    result << dependences(result) unless result.empty?
    result << deps
    return result.flatten
  end
  deps = dependences(deps) << "#{TEMP}" << prj

  # 依存関係と生成式を登録
  file log => deps  do |t|

    # メッセージ
    print "\n********** #{mod}_beh.exe\n\n"

    # .prj ファイルがプロジェクトフォルダにないと
    # fuse が HDL ソースを探せないのでコピーする
    sh "cp #{prj} #{mod}_beh.prj"

    # テストベンチをコンパイルする
    sh "fuse "+
            "-intstyle ise "+
            "-incremental "+
            "-lib unisims_ver "+
            "-lib unimacro_ver "+
            "-lib xilinxcorelib_ver "+
            "-o #{exe} "+
            "-prj #{mod}_beh.prj "+
            "work.#{mod} work.glbl"

    # テストベンチを起動してログファイルを生成
    puts "executing #{exe}..."
    executeTestbench(mod, MAXLOGSIZE)

    # テストベンチで $finish を忘れると終了しないため注意!
  end

  # ログファイルの一覧に追加する
  LOGS << log
end

# 個々のログファイルからレポート simulation.log を生成する
file LOG => LOGS do |t|
  open(LOG, "w") do |all_log|
    LOGS.each do |log_name|
      # エラー行数をカウント
      nerrors = 0
      execerr = false
      IO.readlines(log_name).each do |line|
        execerr = true        if line =~ /^==== EXECUTION ERROR : /
        nerrors = nerrors + 1 if line =~ ERROR_REGEX
      end
      # レポートに追加
      all_log.write nerrors == 0 ? log_name[/[^\/]+$/] + "\n" : 
                    execerr      ? log_name[/[^\/]+$/] + " (Errors: #{nerrors}+)\n" :
                                   log_name[/[^\/]+$/] + " (Errors: #{nerrors})\n"
    end
  end
end

task "show" => LOG do |t|
  print "\n********** RESULT\n\n"
  # ANSI カラーコードを付加してカラー表示にする
  print IO.readlines(LOG).map { |line|
            line =~ /^(.*?)( \(Errors: .*?(\+)?\))?$/
            $3 ? "y \e[43mYELLOW \e[0m #{$1}#{$2}\n" :
            $2 ? "x \e[41m  RED  \e[0m #{$1}#{$2}\n" :
                 "o \e[42m GREEN \e[0m #{$1}\n"
        }.sort.join
end

task "errors" => LOG do |t|
  IO.readlines(LOG).each do |line|
    next unless line =~ /([^\/]+) \(Errors: .*/
    print "\n********** #{$1}\n\n"
    IO.readlines("beh/temp/#{$1}").each do |l|
      print l if l =~ ERROR_REGEX
    end
  end
end

task "clean" => [] do |t|
  sh "rm #{TEMP}/*" unless Dir["#{TEMP}/*"].empty?
end

プロジェクトフォルダに beh/ というフォルダを掘って、 テストベンチの *_beh.prj というファイルを好きなだけ入れておきます。

プロジェクトフォルダで simulation.rake を起動すると、

  1. すべての beh/*.prj に対して
    • コンパイルしてシミュレータ beh/temp/*.exe を作成
    • 実行結果を beh/temp/*.log として保存
  2. 各テストベンチの吐いたエラー数を集計して beh/temp/simulation.log に保存
  3. 集計結果をカラー表示

を自動実行します。

以下の例では ad7982_test_beh.prj pulser_test_beh.prj の2つだけが入っている状況で試しています。

LANG:console
$ ls beh/*.prj
beh/ad7982_test_beh.prj  beh/pulser_test_beh.prj
$ ./simulation.rake

********** ad7982_test_beh.exe

cp beh/ad7982_test_beh.prj ad7982_test_beh.prj
fuse -intstyle ise -incremental -lib unisims_ver -lib unimacro_ver -lib xilinxcorelib_ver -o beh/tem
p/ad7982_test.exe -prj ad7982_test_beh.prj work.ad7982_test work.glbl

(中略)

********** RESULT

o GREEN : pulser_test.log
x  RED  : ad7982_test.log (Errors: 1000)

実際には最後の結果表示は GREENRED が色分け表示されるので、 多数のテストベンチを走らせるとテスト駆動開発っぽい感じを味わえます。 (テスト駆動ではテストの成功をグリーン、失敗をレッドと呼びます)

rake を使っているので、もう一度同じコマンドを起動しても、 ソースファイルやインクルードファイルが変更されていなければテストは実行せず、 単に過去に実行したログを集計して表示します。

simulation実行結果.png

↑こんな風にカラーになります。

もちろん HDL ソースを変更すれば、 そのソースに依存したテストのみが再コンパイル&実行されます。

詳細なエラーの表示

LANG:console
$ ./simulation.rake errors

とすると、詳細なエラーメッセージを表示できます。

やってるのは .log ファイルからエラーメッセージを grep しているだけです。

LANG:console
$ ./simulation.rake errors | head

********** ad7982_test.log

../../src/components/test/ad7982_test.v(131) ERROR at #1020 conv パルス幅は 20ns 以上必要.
../../src/components/test/ad7982_test.v(131) ERROR at #2020 conv パルス幅は 20ns 以上必要.
../../src/components/test/ad7982_test.v(131) ERROR at #3020 conv パルス幅は 20ns 以上必要.
../../src/components/test/ad7982_test.v(131) ERROR at #4020 conv パルス幅は 20ns 以上必要.
../../src/components/test/ad7982_test.v(131) ERROR at #5020 conv パルス幅は 20ns 以上必要.
../../src/components/test/ad7982_test.v(131) ERROR at #6020 conv パルス幅は 20ns 以上必要.
  • 本当は 10ns あれば良いのですが、わざと失敗させるために 20ns としてみました。

チェック側を 10ns に書き換えてもう一度走らせると、

simulation実行結果green.png

のように「オールグリーン」になりました。

書き換えていない pulser_test.v の方はコンパイルや実行が行われていないことに注目して下さい。 rake を使うことで、更新の必要なテストだけを選択的に行うことができます。

上記スクリーンショットで分かるとおり、レポート表示の際にはエラーが見つけやすいように RED のものが GREEN よりも後に表示されるようになっています。

一時ファイルの消去

LANG:console
$ ./simulation.rake clean

とすると、beh/temp/* をすべて消去します。

`include も可能な限り調べて依存関係を構築していますが、 ソースの編集後、うまくテストが実行されない場合には 一旦 clean して、再実行すれば良いかもしれません。

該当の .log ファイルのみを削除した方がコンパイル時間が かからず楽ですが。

.log ファイルからのタグジャンプ

テストベンチの実行結果は beh/temp/simulation.log と言う名前のファイルにまとめられます。

このファイルの中身は非常に単純で、

ad7982_test.log (Errors: 1000)
pulser_test.log

のように、個々のテストベンチのログファイル名と、 その中で失敗したテストの数が表示されます。

秀丸などでは対応する行から F10 でタグジャンプすれば、 該当のログファイルを開けます。

失敗した個々のログファイルには、エラー発生を示す

../../src/components/test/ad7982_test.v(130) ERROR at #1020 conv パルス幅は 20ns 以上必要.

のような行が含まれています。 秀丸などではこのような行からタグジャンプすれば、 該当する HDL ソースのエラー発生行へ飛ぶことができます。

LANG:verilog
   ...

   real t;
   always @(posedge conv)
       fork
           begin
               t = $realtime;
               @(negedge conv);
               if ( $realtime() - t < 20 ) begin
                   $display($realtime() - t);
                   `ERROR(conv パルス幅は 20ns 以上必要);   // <= こことか
               end
           end
       join

※下記にあるとおり、このようなパルス幅のチェックは、specify ブロックの $width を使えばもっと簡単に書けます。

エラー行の検出

現在、エラー行は /ERROR/ という正規表現で検出しています。

上記 rake ファイルの ERROR_REGEX 定数を変更することにより任意の形式のエラーを検出可能です。

検証に ovl などを使った場合にも対応できるかもしれません?

Windows7 64bit 環境での ISim テストベンチ実行

何が悪いのか分かりませんが、私の環境では非常にバグバグしています。

普通にコマンドラインからテストベンチを起動すると、 すぐさまコマンドプロンプトが表示され、 その後、テストベンチのプロンプトが表示されます。

キー入力は、不思議な具合にシェルとテストベンチとに振り分けられて、 どちらに入力されるか一定しません。テストベンチへの入力はエコーされず、 シェルへの入力はエコーされるので、ようやくどちらに入るか分かる感じ。

この現象は通常のコマンドプロンプトでも cygwin 環境でも変わらないので、 cygwin が悪いとかそういう話ではなく、(少なくとも私の) Windows7 64bit 環境と ISim テストベンチとの相性が悪いみたいに感じられます。

そのせいなのかどうか、ruby スクリプトから system 関数等で テストベンチを起動すると、テストベンチの終了を待たずすぐさま ruby スクリプトに戻ってきてしまうため、 その後すぐにログを読んでしまうと、正しくすべてのログを読めません。

この現象を回避するには、IO.popen でテストベンチの標準入出力へ パイプを繋ぐのが良いようでした。上ではそのようにしてあります。 パイプの readline が空になるまで読み出すことで、 テストベンチの終了を待つことができます。

ただ、このように起動したテストベンチも、 やはり通常の正しいプロセス管理が及ばないようです。

  • テストベンチの終了前に ruby スクリプトがエラーで落ちてもテストベンチが走り続ける
  • IO.close は本来テストベンチ側の終了を待つはずなのに、待たずに戻ってきてしまう
  • IO.pid への Process.kill によるシグナル送信に失敗する
  • popen を呼び出したスレッドやプロセスを kill しても、テストベンチだけ走り続ける

本当なら、テストベンチ実行中にログファイルが大きくなりすぎたり、 非常識に時間がかかったりした場合には、そのテストベンチを強制終了して次のテストベンチへ 移りたいのですが、上記の不具合のためにそれができていません。

現状では、テストベンチ実行中にログファイルが大きくなりすぎた場合には、 それ以上ログを書き込まず、末尾に === EXECUTION ERROR : TOO LARGE LOG FILE === というマーカーが残ります。

例えば $finish を書き忘れたテストベンチなどはいくら待っても終了しないわけですが、 そのようなテストベンチを Ctrl+C で落とした場合、ログの末尾に ==== EXECUTION ERROR : INTERRUPTED ==== というマーカーが残ります。

このように、正しく終了しなかったテストベンチについては、 simulation.log のエラー数が + 付きで表示され、 イエロー マークが付けられます。

simulation-result-yellow.png

specify 中の $setup / $hold / $width 違反を補足

すっかり忘れてたんですが(何!)
セットアップ時間、ホールド時間、パルス幅等は specify ブロックを使ってチェックが可能なんでした。

ISim でも specify ブロック中に $setup 等を使って

LANG:verilog
   specify
       $setup(data, posedge clk, 10);
       $hold(data, posedge clk, 10);
       $width(edge clk, 10);
   endspecify

のように書いて、タイミングチェックを行えます。

このようなチェックに違反すると

WARNING: at 295 ns: Timing violation in /dac8812_test/mock/  $hold( data:265 ns, clk:295 ns,10 ns)

といったメッセージが現れるようです。

そこで、エラー検出の正規表現に /^WARNING:.*Timing violation in/ を追加して、 specify 違反も検出可能にしました。

しばらく上記方針でやってみる

数個のテストベンチを突っ込んでみましたが、 思った通りに動いてくれているようですので、 しばらくこれで続けてみようと思います。

テストベンチを増やしてグリーンの数が増えるのが楽しみになったりすると、 すでにテスト駆動開発の罠にはまってたりするわけです(笑

実際、毎回テストでグリーンを見るのは開発者に安心感とやる気を持たせる 心理的な効果があるそうで、そのあたりまで考えた上での自動テストなんだそうです。

ISim 13.1 ではいろいろ変わるらしい

ISim 12.4 のマニュアルによると、 ISim 13.1 ではシミュレータを落とさずにテストベンチを再コンパイルできるようになるそうです。

ModelSim XE と ISim との比較の記述を抜粋>

シングルクリックコンパイルおよび更新

ModelSim XE では、スタンドアロンGUI にテキストエディタがビルトインされているので、HDL コードの変更、再コンパイル、および再シミュレーションが実行できます。

ISim GUI にはテキストビューアはありますが、テキストエディタはありません。ファイルに変更しても、再コンパイルおよび再シミュレーションは実行できません。既存のシミュレーションを閉じて、再起動する必要があります。この制限は、13.1 のISim で修正される予定です。

そうなると、生成されるのは .exe ではなく .dll になるんでしょうし、 現状でいろいろ工夫しても、すぐにはしごを外されるのかもしれません。

ModelSim XE の時がそうだったし・・・
書きためた数十の .fdo/.udo ファイルとか、消すのももったいないけど、 すでに無用の長物とか、泣けてきます(TT

どうするのが良いのか、迷ってます。

コメント




ModelSimPE キャンペーン中

[アプロ] (2011-02-24 (木) 14:53:01)

2011年3月25日受注まで、
http://www.paltek.co.jp/event/index.htm
または、
http://www.altima.jp/campaigns/modelsim_xe_xse_campaign.html
ModelSimXE のライセンスファイルが必要みたいです

  • かなり安くなっていますね。でもまだ手が出せる値段ではないです orz -- [武内(管理人)]
  • ModelSim PE Student Edition - HDL Simulation は無料みたいですが・・・ http://model.com/content/modelsim-pe-student-edition-hdl-simulation -- [アプロ]
  • 高等教育機関向け製品貸与プログラム(HEP) http://www.mentorg.co.jp/company/higher_ed/index.html -- [アプロ]
  • 情報ありがとうございます! HEP というのもあるのですね。これは非常に魅力的なので調べてみようと思います。 -- [武内(管理人)]
  • \82,800 で、憧れのQuestaでアサーションが使い放題(笑) -- [アプロ]

テストベンチで全部まかなう

[アプロ] (2011-02-16 (水) 18:48:34)

期待値はテストベンチ内で用意し、テストベンチ内で、コンペアして、結果はログ・ファイルに出力する

波形はデバッグ時に見るが、OK・NGはログ・ファイルで確認する

  • やはりその方向ですよね。現在、その方針で自分なりのコーディング規約を作ることと、そういうテストベンチが多数あったときに、自動的にすべて走らせて結果を一覧にまとめる方法を調べています。良い方法があったら教えて下さい。 -- [武内(管理人)]
  • 相手のなんちゃってモデルを作って接続する。または、メーカーのシミュレーション・モデルと接続する。そーすると、CPUアクセスがメインになったり、そのなんちゃってモデルの制御になるため、コンベアが楽になります -- [アプロ]
  • SystemVerilogでテストベンチが書けると、かなりイロイロと効率が上がりますが・・・taskとfunctionの塊になりますよ。テストケース毎にtask化するとデバッグが楽になります。また、共通化taskを作っておいて、それをコールしまくる -- [アプロ]
  • $system(); は使えるかな? シェルをコールするシステムタスクです。Verilogで出来ないことは、外部プログラムで補う。perlとかを呼ぶことができる -- [アプロ]
  • Verilog の規格のドラフト版を探してみてください。どっかに転がっていると思います。内緒ですが、アレは、ヒントが満載なんですよ -- [アプロ]
  • SystemVerilog が使えるという意味でも modelsim は魅力的だったんですよね。ISim ではまだ実装されていない部分が多そうなので、自分でまかなうしかなさそうです(TT -- [武内(管理人)]
  • いろいろ勉強になります。いろいろやりながら試行錯誤になりそうです・・・ -- [武内(管理人)]
  • OVLも使ってみたらどうでしょうか? -- [marsee]
  • OVLも検討中なのですが、エラーが出た時に何をさせればデバッグに役立つかが見えてこなくてペンディングになってしまっています。 -- [武内(管理人)]
  • できると良いのは、最低限エラーが起きたことをログファイルから見分けられることですが、できればログファイルからコードのどの部分で、どのシミュレーション時刻に、どんなエラーが生じたかが分かることなのですが・・・ -- [武内(管理人)]
  • テストケースは、taskにして、for でループさせるとよいですよ。ひとつのテストベンチで済みます -- [アプロ]
  • テストベンチ部品の task 化、便利に使っています。verilog 言語は task や function のスコープがモジュール内限定のグローバルであることや、テストベンチでは下位モジュール内の信号やイベントに直接介入可能であることなど、言語仕様に自由度が大きいので、自分なりのコーディング規約が定めにくく、経験不足を感じています。 -- [武内(管理人)]
  • http://www.asic-world.com/verilog/verilog2k1.html (2k2, 2k3とすると次のページが出ます) task、function に automatic を付けると、コールされるとメモリに動的に生成します。これで、task、function 内でローカル変数をバシバシ使ってください -- [アプロ]
  • ログファイルは、テストベンチ内で、ファイル名を生成することで、いかようにも制御可能になります -- [アプロ]
  • automatic 知りませんでした。付けないと同時に呼び出せないんですね。テストベンチ内からログファイルを開くのも一考の余地がありそうです。 -- [武内(管理人)]
  • Verilog HDL&VHDLテストベンチ記述の初歩 by CQ出版 はどーでしか? 目次を見た限りでは、よさそうな気がします。私も買ってはいないのですが(笑) -- [アプロ]
  • 買って、積んで、忘れてましたw パターンファイルを使った検証あたりなどは上で考えているのとかぶるところも多そうですね。改めて見なおしてみます。 -- [武内(管理人)]

Counter: 27607 (from 2010/06/03), today: 2, yesterday: 0