Emacs Verilog-Mode でVerilog HDL をサクサク書く
モチベーション
ハードウェアの記述の大変なところは色々とありますが、テストを書きづらいところも苦労する点の1つだと思います。
そこで、Emacs Verilog-Mode を利用するとある程度の作業をスキップすることができます。 Verilog HDL を書く際の助けになれば幸いです。 SystemVerilog の記法にも対応しており、初心者から玄人まで使いやすいツールです。
以下、Verilog HDL および SystemVerilog を総称してVerilog とします。
使い方
Emacs にverilog-mode.el
を認識させてEmacs 上でVerilog を記述するのが本来の用途ですが、Emacs を使いづらい環境や使わない環境も多いため、ここはDocker を利用してフィルタ機能のみ抽出します。
手前味噌ですが、自作したDocker image であるstomoki/eda-env_emacs-verilog-mode を利用します。
以下のコマンドを実行することで利用できます。
$ docker run --rm -t -v <path-to-target-dir>:/dat \ stomoki/eda-env_emacs-verilog-mode:7 \ emacs --batch /dat/<path-to-target-file-from-target-dir> -f verilog-batch-auto
オプションごとに解説すると、
--rm : 生成したコンテナを破棄。メモリの節約。
-t : 処理した出力の表示。
-v : ボリュームのマウント。Docker 上でローカルマシンのファイルを参照できるようにする。
stomoki/... : Emacs Verilog-Mode を含むDocker image の指定
emacs ... : 実行されるコマンド
となっています。
なお、各行の末尾の \
は長いコマンドを分割して入力するためのものです。
実際に実行されるコマンドの詳細を解説すると、
--batch : バッチモードでEmacs を実行
/dat/... : Verilog-mode で処理するターゲットのファイル。`-v` で指定したディレクトリをトップとしてファイルのパスを指定。
-f : 実行するファンクション。基本的に `verilog-batch-auto` の指定で十分
となります。
例えば、ローカルマシンの/path/to/hdl
をプロジェクトのトップディレクトリとし、src/target.v
に対してVerilog-mode の処理を実行する場合、以下のようなコマンドの内容となります。
$ docker run --rm -t -v /path/to/hdl:/dat \ stomoki/eda-env_emacs-verilog-mode:7 \ emacs --batch /dat/src/target.v -f verilog-batch-auto
実際に使ってみる
実際に使ってみましょう。 Verilog で記述されたファイルを変更しながら、Emacs Verilog-mode の使い方を見ていきます。
使用する例
定番web ページ ASIC World から拝借します。
ただし、入力 data
は未使用なので削除しています。
//----------------------------------------------------- // Design Name : up_down_counter // File Name : up_down_counter.v // Function : Up down counter // Coder : Deepak Kumar Tala //----------------------------------------------------- module up_down_counter ( out , // Output of the counter up_down , // up_down control for counter clk , // clock input reset // reset input ); //----------Output Ports-------------- output [7:0] out; //------------Input Ports-------------- input [7:0] data; input up_down, clk, reset; //------------Internal Variables-------- reg [7:0] out; //-------------Code Starts Here------- always @(posedge clk) if (reset) begin // active high reset out <= 8'b0 ; end else if (up_down) begin out <= out + 1; end else begin out <= out - 1; end endmodule
このコードに対してVerilog-mode を実行しても何も起きません。
ポート生成を自動化 (AUTOARG)
では、コードを以下のように一部書き換えます。
module up_down_counter ( /*AUTOARG*/ ); //----------Output Ports-------------- output [7:0] out; //------------Input Ports-------------- input up_down, clk, reset; //------------Internal Variables-------- reg [7:0] out; //-------------Code Starts Here------- always @(posedge clk) if (reset) begin // active high reset out <= 8'b0 ; end else if (up_down) begin out <= out + 1; end else begin out <= out - 1; end endmodule
ポートリストを削除し、代わりに/*AUTOARG*/
というコメントを挿入しました。
このコメントはVerilog-mode へ特定の処理を指示するもので、Verilog-mode のキーワードはすべてAUTO
で始まります。
このAUTOARG
はコード中のディレクション指示の記述からポートリストを自動生成します。
詳細はHelp : verilog-auto-argを参照してください。
Verilog-mode の動作結果は以下の通りとなります。
module up_down_counter ( /*AUTOARG*/ // Outputs out, // Inputs up_down, clk, reset ); //----------Output Ports-------------- output [7:0] out; //------------Input Ports-------------- input up_down, clk, reset; //------------Internal Variables-------- reg [7:0] out; //-------------Code Starts Here------- always @(posedge clk) if (reset) begin // active high reset out <= 8'b0 ; end else if (up_down) begin out <= out + 1; end else begin out <= out - 1; end endmodule
このようにポートリストが自動生成されました。
これを利用する利点として、Verilog-1995 の冗長な点であったポートリストとディレクションの指定の重複をスキップできることです。 ディレクションはコード中で明示的に指定する必要があります。
もっとも、Verilog-2001 以降のANSI C 形式の記法ならばAUTOARG
は必要ないように思えます。
インスタンスのポート結線を自動化 (AUTOINST)
では、上記のテストベンチを作成してみましょう。
以下のようなテストベンチtb_up_down_counter.v
を作成しました。
`timescale 1ns/1ns module tb_up_down_counter; localparam CLK_PERIOD = 10; wire [7:0] out; reg up_down, clk, reset; up_down_counter DUT ( .out(out), .up_down(up_down), .clk(clk), .reset(reset) ); initial begin: gen_clk clk = 1'b0; forever #(CLK_PERIOD/2) clk = ~clk; end initial begin: test_count_up $monitor("[%0t] up_down %b | out %h", $time, up_down, out); up_down = 1'b0; reset = 1'b1; #(CLK_PERIOD) reset = 1'b0; up_down = 1'b1; //count-up $display("[%0t] start count-up", $time); repeat(16) @(posedge clk); $finish; end endmodule
このテストベンチを以下のようなコマンドを実行してModelSim でシミュレーションします。
$ vlib work $ vlog up_down_counter.v tb_up_down_counter.v $ vsim -c work.tb_up_down_counter -do "run -all"
以下のような出力が得られます。
# vsim -c work.tb_up_down_counter -do "run -all" # Start time: 11:38:19 on May 17,2020 # Loading work.tb_up_down_counter # Loading work.up_down_counter # run -all # [0] up_down 0 | out xx # [5] up_down 0 | out 00 # [10] start count-up # [10] up_down 1 | out 00 # [15] up_down 1 | out 01 # [25] up_down 1 | out 02 # [35] up_down 1 | out 03 # [45] up_down 1 | out 04 # [55] up_down 1 | out 05 # [65] up_down 1 | out 06 # [75] up_down 1 | out 07 # [85] up_down 1 | out 08 # [95] up_down 1 | out 09 # [105] up_down 1 | out 0a # [115] up_down 1 | out 0b # [125] up_down 1 | out 0c # [135] up_down 1 | out 0d # [145] up_down 1 | out 0e # [155] up_down 1 | out 0f # ** Note: $finish : tb_up_down_counter.v(30) # Time: 165 ns Iteration: 1 Instance: /tb_up_down_counter # End time: 11:38:20 on May 17,2020, Elapsed time: 0:00:01 # Errors: 0, Warnings: 0
ここまでは通常のシミュレーションです。 ここからVerilog-mode を利用して記述量を削減します。
まず、インスタンスを生成する部分を以下のように置換します。
`timescale 1ns/1ns module tb_up_down_counter; localparam CLK_PERIOD = 10; wire [7:0] out; reg up_down, clk, reset; up_down_counter DUT (/*AUTOINST*/); initial begin: gen_clk clk = 1'b0; forever #(CLK_PERIOD/2) clk = ~clk; end initial begin: test_count_up $monitor("[%0t] up_down %b | out %h", $time, up_down, out); up_down = 1'b0; reset = 1'b1; #(CLK_PERIOD) reset = 1'b0; up_down = 1'b1; //count-up $display("[%0t] start count-up", $time); repeat(16) @(posedge clk); $finish; end endmodule
インスタンスのポートマップをAUTOINST
に置換しました。
以下のコマンドでVerilog-mode を実行します。
なお、カレントディレクトリ/path/to/hdl
にtb_up_down_counter.v
および up_down_counter.v
があるとします。
$ docker run --rm -t -v /path/to/hdl:/dat \ stomoki/eda-env_emacs-verilog-mode:7 \ emacs --batch /dat/tb_up_down_counter.v -f verilog-batch-auto
なお、Bash 系では以下の記法も可能です。
$ docker run --rm -t -v $(pwd):/dat \ stomoki/eda-env_emacs-verilog-mode:7 \ emacs --batch /dat/tb_up_down_counter.v -f verilog-batch-auto
上記コマンドを実行したとき、tb_up_down_counter.v
の内容は以下の通りとなります。
`timescale 1ns/1ns module tb_up_down_counter; localparam CLK_PERIOD = 10; wire [7:0] out; reg up_down, clk, reset; up_down_counter DUT ( /*AUTOINST*/ // Outputs .out (out[7:0]), // Inputs .up_down (up_down), .clk (clk), .reset (reset)); initial begin: gen_clk clk = 1'b0; forever #(CLK_PERIOD/2) clk = ~clk; end initial begin: test_count_up $monitor("[%0t] up_down %b | out %h", $time, up_down, out); up_down = 1'b0; reset = 1'b1; #(CLK_PERIOD) reset = 1'b0; up_down = 1'b1; //count-up $display("[%0t] start count-up", $time); repeat(16) @(posedge clk); $finish; end endmodule
上記のように、、インスタンスの結線が自動的に生成されました。
この処理はインスタンスの参照先であるup_down_counter.v
を参照して実行されています。
src
とtb
といったように、もしデザインとテストベンチが保存されている場合は参照元のファイルの末尾に以下のようなコメントを追加してVerilog-mode へ参照先を指示する必要があります。
// Local Variables: // verilog-library-directories:("." "../src") // End:
詳細はHelp: verilog-library-directories を参照してください。
もっとも、SystemVerilog の.*
(ドットスター記述)を利用すればこの機能も不要です。
ただし、EDAツールによっては.*
を上手く解決できなかったり、.*
だと結線されている信号名が分かりづらかったりするため、この機能を使用した方が記述量を削減しつつ可読性を向上できます。
インスタンスの結線用信号の記述を自動化 (AUTOWIRE, AUTOREGINPUT)
AUTOINST
を利用することで、インスタンスのポートリストを自動化することができました。
併せて、ポートリストと接続する結線用の信号の宣言を自動化することで、さらに記述量を削減することができます。
以下のようにtb_up_down_counter.v
を書き換えます。
`timescale 1ns/1ns module tb_up_down_counter; localparam CLK_PERIOD = 10; /*AUTOWIRE*/ /*AUTOREGINPUT*/ up_down_counter DUT (/*AUTOINST*/); initial begin: gen_clk clk = 1'b0; forever #(CLK_PERIOD/2) clk = ~clk; end initial begin: test_count_up $monitor("[%0t] up_down %b | out %h", $time, up_down, out); up_down = 1'b0; reset = 1'b1; #(CLK_PERIOD) reset = 1'b0; up_down = 1'b1; //count-up $display("[%0t] start count-up", $time); repeat(16) @(posedge clk); $finish; end endmodule
このコードをVerilog-mode で変換すると、以下のようになります。
`timescale 1ns/1ns module tb_up_down_counter; localparam CLK_PERIOD = 10; /*AUTOWIRE*/ // Beginning of automatic wires (for undeclared instantiated-module outputs) wire [7:0] out; // From DUT of up_down_counter.v // End of automatics /*AUTOREGINPUT*/ // Beginning of automatic reg inputs (for undeclared instantiated-module inputs) reg clk; // To DUT of up_down_counter.v reg reset; // To DUT of up_down_counter.v reg up_down; // To DUT of up_down_counter.v // End of automatics up_down_counter DUT (/*AUTOINST*/ // Outputs .out (out[7:0]), // Inputs .up_down (up_down), .clk (clk), .reset (reset)); initial begin: gen_clk clk = 1'b0; forever #(CLK_PERIOD/2) clk = ~clk; end initial begin: test_count_up $monitor("[%0t] up_down %b | out %h", $time, up_down, out); up_down = 1'b0; reset = 1'b1; #(CLK_PERIOD) reset = 1'b0; up_down = 1'b1; //count-up $display("[%0t] start count-up", $time); repeat(16) @(posedge clk); $finish; end endmodule
AUTOWIRE
は出力ポートの、AUTOREGINPUT
は入力ポートの結線信号を自動生成しました。
前述の通り、AUTOINST
だけではSystemVerilog の.*
の互換となりますが、AUTOWIRE
およびAUTOREGINPUT
を利用すれば信号宣言なしにテストベンチのひな形を作成することができます。
さらに言えば、テスト対象回路のポートマップを知らずにテストベンチのひな形を作成することができますので、テストベンチ作成の手間を大幅に減らすことができます。
AUTOREGINPUT
はAUTOREG
の派生です。
AUTOREG
はポートマップで指定されていない出力方向のレジスタを内部信号として自動的に生成します。
デザインおよびテストベンチで信号線の宣言を忘れていないかのチェックに有用です。
階層構造の記述を自動化する (AUTOINPUT, AUTOOUTPUT)
前述した結線の自動化は複数のインスタンスが存在するときに大きな効果をもたらします。
例えば、以下のような回路を追加し、カウンタの値をSPIプロトコルのシリアル通信へ変換する、とします。
module count_to_driver ( input wire clk, input wire reset, input wire [7:0] out, output reg sck, output reg cs_n, output reg so ); // control logics are here ... endmodule
また、up_down_counter
と count_to_driver
を組み合わせ、SPIプロトコルに従ったドライバ回路を設計するとします。
このときのコードは以下のように記載できます。
module driver ( /*AUTOARG*/ ); /*AUTOINPUT*/ /*AUTOOUTPUT*/ /*AUTOWIRE*/ /*AUTOREG*/ up_down_counter counter (/*AUTOINST*/); count_to_driver cnt2spi (/*AUTOINST*/); endmodule
初出となるAUTOINPUT
および AUTOOUTPUT
は、インスタンスの入出力信号でコード上に宣言されていない信号をそのまま参照元モジュールの入出力ととして宣言します。
Verilog-mode で変換した結果は以下の通りとなります。
module driver ( /*AUTOARG*/ // Outputs so, sck, cs_n, // Inputs up_down, reset, clk ); /*AUTOINPUT*/ // Beginning of automatic inputs (from unused autoinst inputs) input clk; // To counter of up_down_counter.v, ... input reset; // To counter of up_down_counter.v, ... input up_down; // To counter of up_down_counter.v // End of automatics /*AUTOOUTPUT*/ // Beginning of automatic outputs (from unused autoinst outputs) output cs_n; // From cnt2spi of count_to_driver.v output sck; // From cnt2spi of count_to_driver.v output so; // From cnt2spi of count_to_driver.v // End of automatics /*AUTOWIRE*/ // Beginning of automatic wires (for undeclared instantiated-module outputs) wire [7:0] out; // From counter of up_down_counter.v // End of automatics /*AUTOREG*/ up_down_counter counter (/*AUTOINST*/ // Outputs .out (out[7:0]), // Inputs .up_down (up_down), .clk (clk), .reset (reset)); count_to_driver cnt2spi (/*AUTOINST*/ // Outputs .sck (sck), .cs_n (cs_n), .so (so), // Inputs .clk (clk), .reset (reset), .out (out[7:0])); endmodule
このように、すべてのインスタンスの結線が解決され、必要な入出力ポートが宣言され、ポートマップが補完されています。
clk
やreset
といった共通の入力信号も重複なく解決されています。
また、ANSI C 形式でポートマップを宣言するときは以下のように記述します。
module driver ( /*AUTOINPUT*/ /*AUTOOUTPUT*/ ); /*AUTOWIRE*/ /*AUTOREG*/ up_down_counter counter (/*AUTOINST*/); count_to_driver cnt2spi (/*AUTOINST*/); endmodule
この変換結果は以下のようになります。
module driver ( /*AUTOINPUT*/ // Beginning of automatic inputs (from unused autoinst inputs) input clk, // To counter of up_down_counter.v, ... input reset, // To counter of up_down_counter.v, ... input up_down, // To counter of up_down_counter.v // End of automatics /*AUTOOUTPUT*/ // Beginning of automatic outputs (from unused autoinst outputs) output cs_n, // From cnt2spi of count_to_driver.v output sck, // From cnt2spi of count_to_driver.v output so // From cnt2spi of count_to_driver.v // End of automatics ); /*AUTOWIRE*/ // Beginning of automatic wires (for undeclared instantiated-module outputs) wire [7:0] out; // From counter of up_down_counter.v // End of automatics /*AUTOREG*/ up_down_counter counter (/*AUTOINST*/ // Outputs .out (out[7:0]), // Inputs .up_down (up_down), .clk (clk), .reset (reset)); count_to_driver cnt2spi (/*AUTOINST*/ // Outputs .sck (sck), .cs_n (cs_n), .so (so), // Inputs .clk (clk), .reset (reset), .out (out[7:0])); endmodule
ここで、ポートリストに型宣言が含まれないことに注意すること。
SystemVerilog の場合、AUTOWIRE
の代わりにAUTOLOGIC
の使用を推奨します。
その場合、ポートリストにlogic
として型宣言が含まれるようになります。
テスト用スタブを自動生成する (AUTOINOUTMODULE)
テスト作成時に困るのが外部モジュールのふるまいの記述です。
外部モジュールのふるまいをテストコードに直接記述すると、大抵が複雑化し保守性を低下させる要因となりやすいです。 そのため、ここはモックやスタブとして外部モジュールと同様のインタフェースを持つテスト用モジュールを作成し、必要ならばロジックを増やしていく戦略をとると良いと思います。 このメリットとして、テストコードの行数を減らせることとと作成したテストモジュールを他のテストへ再利用できる点が挙げられます。
上記のdriver
のスタブを作成してみましょう。
このスタブはdriver
モジュールが完成するまで上位モジュールから参照してもコンパイルが通るようにし、driver
モジュールから返答がなくてもよい部分を先に作るために有用です。
Verilog-mode のFAQ を参考にして以下のように記述します。
module stub_driver #( /*AUTOINOUTPARAM("driver")*/ )( /*AUTOINOUTMODULE("driver")*/ ); /*AUTOWIRE*/ /*AUTOREG*/ /*AUTOTIEOFF*/ wire _unused_ok = &{1'b0, /*AUTOUNUSED*/ 1'b0}; endmodule
Verilog-mode で変換すると以下のようになります。
module stub_driver #( /*AUTOINOUTPARAM("driver")*/ )( /*AUTOINOUTMODULE("driver")*/ // Beginning of automatic in/out/inouts (from specific module) output cs_n, output sck, output so, input clk, input reset, input up_down // End of automatics ); /*AUTOWIRE*/ /*AUTOREG*/ /*AUTOTIEOFF*/ // Beginning of automatic tieoffs (for this module's unterminated outputs) wire cs_n = 1'h0; wire sck = 1'h0; wire so = 1'h0; // End of automatics wire _unused_ok = &{1'b0, /*AUTOUNUSED*/ // Beginning of automatic unused inputs clk, reset, up_down, // End of automatics 1'b0}; endmodule
AUTOINOUTPARAM
およびAUTOINOUTMODULE
は引数として指定したモジュールのポートリストを参照し、内容を複製します。
AUTOTIEOF
は未使用の出力信号を1`b0 へ固定し、不定値となることを防ぎます。
AUTOUNUSED
は未使用な入力信号を列挙します。
この_unused_ok
では全ての未使用な入力信号線をまとめ、全てのビットのANDを生成しています。
_unused_ok
は1'b0 とのANDとなっているため、常に1'b0となります。
必要ならばこの信号線をリンタなど他のツールのスコープから除外するのがベターだそうです。
一方で、driver
の開発を考えた場合、SPIの接続先が必要となります。
その場合、stub_spi
として以下のように記述します。
module stub_spi #( /*AUTOINOUTPARAM("driver")*/ ) ( /*AUTOINOUTCOMP("driver", "", "^input.*")*/ ); /*AUTOWIRE*/ /*AUTOREG*/ /*AUTOTIEOFF*/ wire _unused_ok = &{1'b0, /*AUTOUNUSED*/ 1'b0}; endmodule
Verilog-mode で変換した結果は以下の通りです。
module stub_spi #( /*AUTOINOUTPARAM("driver")*/ ) ( /*AUTOINOUTCOMP("driver", "", "^input.*")*/ // Beginning of automatic in/out/inouts (from specific module) input cs_n, input sck, input so // End of automatics ); /*AUTOWIRE*/ /*AUTOREG*/ /*AUTOTIEOFF*/ wire _unused_ok = &{1'b0, /*AUTOUNUSED*/ // Beginning of automatic unused inputs cs_n, sck, so, // End of automatics 1'b0}; endmodule
AUTOINOUTCOMP
はAUTOINOUTMODULE
の派生で、指定したモジュールのポートリストを参照して逆方向のポートリストを生成します。
ここで重要なのが2番目の引数と3番目の引数です。
2番目の引数は「正規表現による参照元のポートリストの信号名に対するフィルタリング設定」で、例えば^spi_.*
のように指定するとspi_
から始まる信号線のみをポートリストの対象とします。
3番目の引数は「正規表現による変換後のポートリストのディレクションおよびデータ型に対するフィルタリング設定」で、この例ではinput
から始まるポートのみを出力する設定となります。
そのため、AUTOINOUTCOMP("driver","","^input.*")
は、
1. driver
モジュールの、
2. すべてのポートに対して、
3. 反対のディレクションへ変換し、
4. input
から始まる全てのポートを出力する
という指示となります。
今回はSPI通信関係の信号線を抽出したかったこととSPI通信に関わる信号線のみが出力だったため上記のような条件設定となりました。
特定のポートのみ出力したい場合は信号名にspi_
のようなプレフィックスを付け、第2引数で指定するのが良いと思います。
詳細はHelp: verilog-auto-inout-module を参照してください。
上記スタブを使用することで、SPI通信関連の信号を内部で処理することができます。 スタブ中に制御ロジックやタスクを追記することでモックとして作り込むこともできます。
実際にスタブを用いたテストベンチを記述してみましょう。
driver
モジュールのテストベンチを以下のように記述します。
module tb_driver; /*AUTOREGINPUT*/ /*AUTOWIRE*/ /*AUTOREG*/ driver DUT (/*AUTOINST*/); stub_spi stub_spi(/*AUTOINST*/); // stimulus are here... endmodule
verilog-mode で変換すると以下のようになります。
module tb_driver; /*AUTOREGINPUT*/ // Beginning of automatic reg inputs (for undeclared instantiated-module inputs) reg clk; // To DUT of driver.v reg reset; // To DUT of driver.v reg up_down; // To DUT of driver.v // End of automatics /*AUTOWIRE*/ // Beginning of automatic wires (for undeclared instantiated-module outputs) wire cs_n; // From DUT of driver.v wire sck; // From DUT of driver.v wire so; // From DUT of driver.v // End of automatics /*AUTOREG*/ driver DUT (/*AUTOINST*/ // Outputs .cs_n (cs_n), .sck (sck), .so (so), // Inputs .clk (clk), .reset (reset), .up_down (up_down)); stub_spi stub_spi(/*AUTOINST*/ // Inputs .cs_n (cs_n), .sck (sck), .so (so)); // stimulus are here ... endmodule
上記の通り、スタブおよびテスト対象回路の記述量を削減しつつテストベンチを構築することができます.
まとめ
Emacs Verilog-mode を利用することでテストベンチの記述量を減らし、作成スピードを向上することができます。 どこかのモジュールで変更があった場合、信号名の書き換えがコマンドによって自動的に書き換えられるのが利点です。
よいHDL設計ライフを!