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

更新


公開メモ

テストを自動化したい

テスト駆動開発

ソフトウェア開発で最近はやりの手法として、 テスト駆動開発(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 msg.`", `__FILE__, `__LINE__)

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

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

と書くと、

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

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

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

実は上記の _LINE_ や _FILE_ は verilog2001 には規定されておらず、 ISim のマニュアルにも記述がないのですが、C の流儀で書いてみたら通ってしまいました。

`"〜`" という不思議なクォーテーションは、マクロ引数を展開してくれる書き方だそうです。

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

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, clk) 

`else

// ISim 上での動作
// ファイル名&行番号も表示する

`define INFO(msg) \
    $display(`"%s(%0d) INFO msg.`", `__FILE__, `__LINE__)

`define ERROR(msg) \
    $display(`"%s(%0d) ERROR msg.`", `__FILE__, `__LINE__)

`define DONE \
    $display(`"%s(%0d) DONE.`", `__FILE__, `__LINE__)

`define ASSERT_EQ(variable,value) \
    if((variable)!=(value)) \
        $display("%s(%0d) ERROR ", `__FILE__, `__LINE__, \
                 `"Assertion [ variable == value ] not met: `", variable, " != ", value)

`define ASSERT_EQ_ALWAYS(variable,value) \
    always @(variable) \
        if((variable)!=(value)) \
            $display("%s(%0d) ERROR ", `__FILE__, `__LINE__, \
                     `"Assertion [ variable == value ] not met at #%0d: `", $time, \
                     variable, " != ", value)

`define ASSERT_EQ_AT(variable,value,clk) \
    always @(clk) \
        if((variable)!=(value)) \
            $display("%s(%0d) ERROR ", `__FILE__, `__LINE__, \
                     `"Assertion [ variable == value @ clk ] not met at #%0d: `", $time, \
                     variable, " != ", value)

`endif

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

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

波形比較のアプローチ

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

そうしておいて、 その後のテストは実波形と理想波形との差分を取ることで行う、 という機械的なアプローチもありかもしれないですね。

パラメータによって

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

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

LANG:verilog
module wave_tester #(
    parameter FUNCTION  = "none", // "none" / "record" / "compare"
    parameter WAVE_FILE = "",
    parameter BIT_COUNT = 1,
    parameter PERIOD = 0 // 0 : clk あるいは >= 1 : ns
) (
    input wire start,
    input wire [BIT_COUNT-1:0] data,
    input wire clk,       // used when PERIOD == 0
    output wire error     // asserted when comparison fails
);

こんな感じでしょうか?

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

さらにうまい方法はあるでしょうか?

成功と失敗の判別

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

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

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

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

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

ISE から Simulate Behavioral Model をダブルクリックしたときの動作を追うことで、 コンパイルからシミュレータの起動までの一連の流れが 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 デバッグ用のコンソール起動アイコンとして利用しています。

fuse でコンパイルして *_isim_beh.exe ファイルを作る

通常の呼び出しは以下の通りです。

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 が依存するモジュールファイルが記述されています。

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

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

*_isim_beh.exe の実行

できあがったシミュレータを実行するときは、 標準入力に run を送ってやる必要があります。

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

LANG:console
C:\hoge_proj>(echo run | hoge_test_isim_beh.exe) > hoge_test_isim_beh.log
C:\hoge_proj>cat hoge_test_isim_beh.log
 ISim log file
 Running: hoge_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

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

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

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

LANG:ruby
#!/usr/bin/ruby

system("rm simulate_all.log")
Dir.glob("beh/*.prj").each do |prj|
  prj =~ /([^\/]+)_beh.prj/
  module_name = $1
  prj = $~
  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 と言うファイルにログとして残ります。

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

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

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

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

もう少し調査してみます。

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

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

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

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

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

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

簡単には、

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

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

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

これ結構重要なんだけど、試行錯誤が必要になりそう。

コメント




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

[アプロ] (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も検討中なのですが、エラーが出た時に何をさせればデバッグに役立つかが見えてこなくてペンディングになってしまっています。 -- [武内(管理人)]
  • できると良いのは、最低限エラーが起きたことをログファイルから見分けられることですが、できればログファイルからコードのどの部分で、どのシミュレーション時刻に、どんなエラーが生じたかが分かることなのですが・・・ -- [武内(管理人)]

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