Verilogを書くことについて
はじめに
うちの研究室はVeilogを書くことが多い。今年も新人がTrainingと称してVerilogを書いているが、ぶっちゃけ教員からの指導も何もないので激ヤバなコードを書いてたりする。(Verilogをコードって言っていいのか未だに良く分かっていないが...) 先輩が苦労したことを後輩が苦労する必要はないので先にインプットしておけば良いのにと毎年思っている。後輩にはもっと本質の部分に時間をかけてほしいという思い。
VerilogはHardware Description Languageなのでもちろんハードウェアを意識して書く必要がある。つまり、Verilogというテキストベースで書いたRTLをツールで合成して(普通はSynopsysのDC Compilerとかで)回路にするので、どういう風な記述をすればどう回路になるかを考えて書くべきである、と思う。研究として回路の性能を上げたいとかいうことをやろうとしたときに、その性能はRTLを書く"練度"が大きく影響する気がする。
"良いRTL"を書くには知識とか経験とかが必要になる(?)のだが、正直上から下へ1から10まで指導したりしていると時間がもったいないし面倒なので、読めばわかる、みたいなことはとりあえず文章にしてしまおうと思って書いてみようと思う。まあ多分間違っていることもあるのであしからず。
ちなみに文法とかは把握しているものとして話を進める。ググれば出るし。
性能の高い回路とは?
さっき回路性能の向上の向上の話をしたが、ここで言う性能の高いとは面積が小さく、かつ動作周波数の高いものを指すことにする。
組み合わせ回路と順序回路
ご存じのように回路にはデータを保持しない組み合わせ回路とレジスタを使ってデータを保持する順序回路がある。Verilogとしては
組み合わせ回路: assign文、wire型
順序回路: always文、reg型
が対応する。
同期回路を前提にした場合、組み合わせ回路は
長所
信号がすぐ伝わるので上位モジュールでの操作が楽。レジスタを使わないのでその部分の面積と消費電力が削減できる。
短所
短所というかしょうがないことだが、演算を1クロックで行うので計算が長くなるとその分1クロックの時間が長くなり結果として動作周波数が下がる。最も長いパスで周波数に影響を及ぼす部分をクリティカルパスと言う。また、assign文を大量に書くとそれだけ回路が生成されるので単純に回路面積が大きくなる。
あとすっきりしたRTLを書くコツとして同じ演算記述はassign文でくくりだしてまとめた方が良い、みたいなところもある。
次に順序回路だが、
長所
クロックを複数回使って演算させることができるので回路面積を小さくできる。レジスタを挟むことでパスを短くして動作周波数を上げられる。(サイクル数が増えるので演算時間の議論はここではできない。) パイプライン化ができる。
短所
上位モジュールでの操作がめんどい。サイクル数はかかる。
結局短くしたいのは(1クロックあたりの時間)×(サイクル数)なので、おおざっぱな言い方をすればどの程度組み合わせ回路にして順序回路にして...などの兼ね合いはケースバイケース、である。ただ片方をたくさん使ったりすると回路面積がいたずらに大きくなったりサイクル数がとてもかかったりする。
ブロッキング代入とノンブロッキング代入
verilogで特徴的なものにノンブロッキング代入というものがある。記号で言うと"<="である。これは"小なりイコール"ではなく左側の変数にぶち込め!という代入の記号である。ちなみに代入先の左側の信号はreg型に限る。右側はどっちでもOK。
回路記述なので書き手が代入したいだけ並列に代入を行うことができる。まあ実はこれにももちろん制限があり、シミュレーション段階では1クロックに何千bitでも何万bitでも同時に代入できるが、実際チップにしたとき配線が物理的に収まらないとかで配置配線の段階でDesign Rule Check(DRC)エラーが出る。めちゃくちゃ出る。
話が少し逸れたが、ここで言いたいのはコードの上から順に実行されるのではなく同時に実行されることに注意してほしいということである。
なので例えば
always @(posedge CLK) begin if(!RST) begin a <= 2'd1; b <= 2'd2; c <= 2'd3; end else begin b <= a; c <= b; end end
と書くとRST==1の1クロック目はcは2'd2が入る。cに2'd1を入れたいときは何か信号を用意して
always @(posedge CLK) begin if(!RST) begin a <= 2'd1; b <= 2'd2; c <= 2'd3; flag <= 1'b0; end else begin if(!flag) begin b <= a; flag <= 1'b1; end else begin c <= b; end end end
みたいに場合分けする必要がある。always文の中で"="を使えはするが、通常使わない。(同期回路を作る場合など特に混乱するため)
少なくともこのような記述方式は一般的なソフトウェア記述き比べて圧倒的にバグを生みやすいのでできるだけ簡潔かつ明示的な記述をすべきで、どうなるか良く分からない記述はそもそも避けるべきである。
下位モジュールの呼び出し
verilogでは上位モジュールの中で下位モジュールを呼び出す。回路のイメージとしてはこんな感じ。
上位モジュールのinputは下位モジュールに入るかもしれないが、上位モジュールの出力はその中の下位モジュールには基本的には入らない。入れたい、という仕様の場合はモジュール構成を変更した方が良いかもしれない。
上位モジュールから下位モジュールへの入力は上位モジュールで信号を用意するのでreg型で用意する。下位モジュールの出力は信号が出てくるだけなので上位モジュール側ではwire型でOK。
例えば、
module ALU(inA,inB,outC); parameter WIDTH = 6'd32; input [WIDTH-1:0] inA; input [WIDTH-1:0] inB; output [WIDTH:0] outC; reg [WIDTH-1:0] a; reg [WIDTH-1:0] b; wire [WIDTH:0] c; add add(a,b,c); ...
テストベンチについて
次にテストベンチだが、これは.vという拡張子がついてるだけのシミュレーション用の記述であることに注意。これは回路ではなくいわゆるテスト用のソフトウェアである。先ほどのモジュール間の信号伝達で書いたようにテストするモジュールの入力をレジスタで作り、出力をwireで受ける。ひな形はこんな感じ
module testbench(); reg CLK,RST reg [31:0] a; reg [31:0] b; wire [31:0] out; wire sync_o; hoge hoge(CLK,RST,a,b,out,sync_o); always #5 clk = ~clk; initial begin $dumpfile("hoge.vcd"); $dumpvars(); $monitor("%t : out = %d",$time/10,out); #0 RST <= 0; CLK <= 1; #10 a <= fuga; b <= piyo; ... #1000 $finish; end endmodule
.vcdファイルをダンプしてgtkwaveで見るとデバッグしやすい。
for文はやめよう
for文はやめてくれ。マジで。always文で代用しよう。まあ文法としてfor文があるので回路生成は出来るのだが、少なくとも今までfor文を使わないと記述できない例を見たことがない。よく見るfor文を使ってしまった記述はこんな感じ
initial begin for (i=0; i<n; i=i+1'b1) begin a = a + b; end end
テストベンチなどのソフトウェア的なシミュレーションでは使っていいと思うが、回路記述では避けた方が良い。他の処理もしたい場合こんな感じで書いてる例もある。
always @(posedge CLK) begin for (i=0; i<n; i=i+1'b1) begin a = a + b; end c <= c + d; end
もうブロッキングかノンブロッキングかもごちゃごちゃだし大変汚い。ダメ。
そもそも同期回路を作ることが多い気がするので1番目の例の中のinitialは回路記述ではあまり使わないと思われる。2番目の例で言うと以下の理由でダメ。
回路面積が大きくなる
クロックが立つ所で中身をつくっているが、1クロックで処理を完了させるために同じ回路をn個連続でつなげた構造を作る。
こうなるとn個同じ回路ができるので回路面積が大きくなり、無駄が大きい。
周波数が下がる
n回の処理を1クロックで行おうとするため、パスが非常に長くなる。上の図を左から右まで信号伝達させようとしているので時間がかかりそうなのはわかると思う。
じゃあどう書けばええねんって話だが、1例としてはこんな感じ
reg [9:0] i; //ここでのbit数は適当 always @(posedge CLK) begin if(!RST) begin i <= 10'b0; end else begin if(state==2'b00) begin a <= a + b; i <= i + 1'b1; if(i[9]) begin //ここも適当 state <= 2'b01; end end else if(state==2'b01) begin ... end
alwaysの部分はCLKごとに実行するのでその辺の感覚が多分大事。
比較するとき
if文で比較するときは不等式ではなく、"=="か"!="で指定した方が良い。例えば、
if(i<n) begin (hogehoge) end else begin (fugafuga) end
よりも、
if(i==n) begin (fugafuga) end else begin (hogehoge) end
の方が良い。論理的な場合分けは同じだが、ifの中は比較する回路ができるのでひとつの値と比べるようにする方が面積が小さくなる。
また、bitを意識して
if(i==10'd512) begin
よりも
if(i[9]) begin
の方が明示的でDC Compilerに優しい。
integer型ではなくreg型で
先ほどのコードの中にも書いているがカウンタの変数はきちんとreg型でbit数を指定して宣言しよう。integer型というやつもあるがこいつは中身がわからないので使わない方が良い。少なくともintegerの方が良いという理由が見当たらない。
bit数は指定しよう
具体的な数字を使うときはそれが2進数なのか10進数なのか16進数なのか、また何bitなのか明示すべき。インクリメントひとつにしても
i <= i + 1;
ではなく
i <= i + 1'b1;
とすべき。上の例では32bitの1になってしまう場合がある、と聞いたことがある。
また、適当に"32"などの数でも何進数かで絶対値が変わるのできちんと示そう。
RSTは負論理で
細かい話になってくるがRSTは負論理で基本的に書く。FPGAボードのボタンなどもRSTを"押す"と論理が"RST=0"になる仕様が多い。
よって、最初の初期化などの段階では
if(!RST) begin ... end else begin ... end
で書こう。
parameterとdefine文
定数を宣言する手法としてmoduleの中に書くparameter記述とmoduleの上に書くdefine文がある。個人的にparameter記述の方が好き。某によるとdefine文は名前空間が汚れる?とかいうのがあるらしい。あとparameterのいいところは下位モジュールの定数も一気に変更できるところである。上位モジュールで
module hogehoge(...); parameter WIDTH = 6'd32; input [WIDTH-1:0] a; ... subm #(WIDTH) subm(fugafuga); endmodule
って書いておけば下位モジュールで
module subm(...); parameter WIDTH = 7'd64; ... endmodule
と違うparameterで書いていても下位モジュール側が書き換えられて上位モジュールのparameterに変更される。論理合成などの段階でいろいろ値を変えながら実験するときなどに便利。
研究におけるASIC実装とFPGA実装の違いとか
ASICを作って研究を行っているが、FPGAを使っているところも多い。ASICとFPGAの違いはそもそもこんな感じ
ASIC(Apprication Specific Integrated Circuit)
いわゆる専用回路というもの。ロジックの場合回路規模が大きいこともあって自動配線が多いが、専用の配線とモジュール配置をできるだけ最適化して行うので速い。ただGHz帯の周波数が通らないとか聞いたことがあるのでそもそも周波数には限界がある。試作に金はかかる気がする。あくまでも量産体制になったところで比較的安価になってくる手法。
FPGA(Field Programmable Gate Array)
これも買うと高い。ただ結構先端のプロセスを用いているので普通に周波数は高い。サイクル数も削減していくと演算時間は結構短い。量産する前の試作段階だとこちらの方が安価。非常に大きいbit数のデータの演算はFPGAだと面倒だという話はあるが、Programmableというだけあって、設計の汎用性の高さは非常に便利である。
ASICはほどほどのプロセスしか使えない場合があるので普通に実装してもFPGAに速さでは勝てない場合がある。