5.シミュレータの内部構造
現実のハードウェアは、並列に進みますが、シミュレータは、シーケンシャルにしか事を進められません。どのようにして、現実のハードウェアを模擬(シミュレート)しているのでしょうか?
その仕組みがイベントドリブンと言われる手法です。これに対する手法としてサイクルベースがありますが、ここでは触れません。
alwaysや、initial 文を実行することでイベントが発生し、イベントキューに値の変化が蓄えられます。
蓄えられたイベントは、順番に実行されていきます。現在時刻のイベントがなくなると次の時刻のイベントキューが実行されていきます。こうしてイベントキューがなくなるか、$finishが実行されるまで、延々とシミュレーションは続きます。イベントドリブンとは、イベントが次のイベントを生むというという意味に筆者は捉えています。
もう少し具体的に見ていきましょう。
以下はサンプルフォルダにあるsample3.v のソースです。
module sample3; reg clock=0; reg [9:0] c10; reg Reset=1; initial begin #55 Reset=0; #1000 $stop; #1000000 $finish; end always #10 clock =~clock; always @ (posedge clock) begin if (Reset) c10<=0; else c10<=c10+1; end endmodule |
Always文から見ていきましょう。
always #10 clock=~clock;
は、
always begin
#10;// 10単位時間待つ
clock=~clock;//clockを反転
end
と等価です。シミュレータが#10を実行すると、
というイベントが発生し、#10後のイベントキューに放り込まれます。そして、#10後、クロックというNetObjectから、値を取り出して、反転させ、それをNetOjectにセットします。alwaysは、無限ループなので#10までシミュレータは間断なく進みます。後は、同じことを繰り返します。実行されたイベントは、キューから取り除かれますが、この例の場合キューがなくなることはありませんので、永久にシミューションは終わりません。どこかに$finishが必要です。
次のAlways文は、clockのPositive Edgeを発見するまで停止せよという意味です。(posedgeは、(0,x,z)から1への遷移です。)この文が実行されて初めてposedge検出可能状態になることに注意してください。文が実行されないで、posedge
を検出することはありえません。
次にInitial文です。clock=0は、次に等価です。
reg clock
initial clock=0;
clock というNetObjectは、時刻0の以前に初期値xを持ちます。したがって、時刻0で、clockは、x=0という変化(イベント)が起きます。もし、このイベントを待っている者がいれば、その者を実行するように
イベントスケジューラは、スケジュールします。(すぐに実行するという意味ではありません。あくまでスケジュールするという意味です。)イベントを待っている者とは、たとえば、
になります。この例では、上記に該当するものはありませんので、この文でキューイングされるイベントはありません。同様にResetも 時刻0で、x=>0になりますが、キューイングされるイベントはありません。次の
initial begin #55 Reset=0; #1000 $stop; #1000000 $finish; end
は、initial begin
#55;
Reset=0;
...
と等価です。
再びalways clockに戻りましょう。 時刻0でのイベントキューはなくなりました。次の時刻は、10で、clock=~clockが実行されます。0=>1のイベントが起きました。これを待っている者@(posedge clock)がいるので、現在時刻10のイベントキュ-に追加されます。
このようにして、alwaysや、Initialで発生したイベントが伝播して、あたかもゲート信号が伝播するかのような動きになります。
イベントの概念が大体理解できたでしょうか?シミュレータの動きを理解するのに、もう二つ重要な要素があります。それは、
です。HDL以前の回路シミュレータは、NetobjectとEventScedulerのみで構成されていたと思います。(中身を見たことはありませんので筆者の想像です。)Netobjectとは、実際の回路素子を模した物です。シミュレータ上で、実際にメモリを食います。たとえば、reg a;という宣言は、1ビットのメモリ素子を表現しています。シミュレータ上でもこのaという物をNetobjectという形でメモリ上に持っています。(ただし、回路素子としては、1ビットでも、シミュレータ上では、50バイト位食っています。)
スレッドとは、Initial や Alwaysで生成されるプログラムの実行単位です。Veritakでは、これを仮想CPU上にインタプリタコードを生成して実行していますが、コンパイル型のシミュレータでも同じ事です。Verilogでは、テストベンチまたは回路をC言語と同じように手続き的に記述できます。ただし、C言語と違い、時間的に並列しないといけませんので、スレッドという概念がどうしても必要になります。
たとえば、上の例では、5つのスレッドを持っています。Always文が二つと明示的Initial文一つ、それに暗黙的Initial文二つの計5つです。(VHDLでは、5つのプロセス文に対応します。)スレッド内では、基本的に中断することなく進みますが、遅延文や、イベント文(下記)に遭遇したとき実行スレッドは、実行を一時中断しイベントスゲジューラに実行権を渡します。
どのスレッドをどういう順序で行うかはVerilogでは規定されておりません。実は、IntialとAlwaysのどちらを先に実行させるかも規定されておりません。そのために、テストベンチの書き方によってはシミュレータにより結果が異なる場合があります。これをVerilogのRace問題と呼び、しばしば悩ましい問題となります。Verilogが悪いのだ、という人もありますが、時間0で、DFFにCLKとDataを同時に変化させて0か1のどちらに転ぶか規定する側面もありますのであくまで記述で避けるべきだと思います。ハードウェアの記述において、シミュレータで結果が違うのは、即記述ミス(意図しない論理合成結果を生む、または、合成できない)を意味するので論外ですが、テストベンチの記述においてもRace的記述(スレッドの順番に依存する記述)は避けるようにしてください。(残念ながら、オープンコアでプロフェッショナルが書いたものにもそういう記述が多々見られます。)
良質な記述の為に、シミュレータの内部の動きを本章と次章で理解されておくことをお勧めします。
Veritakでは、Step実行機能させてみれば、どういう風にスレッドが実行されているか分かりますので是非上記の例でもやってみてください。遅延文やイベント文でスレッドが変わるのが分かると思います。(なお、スレッドの実行順序は、プロジェクトオプションでAlternativeがあります。)
以上を図にまとめたのが下図になります。
実際は、これよりもう少し複雑なのですが、それは次の機会にしましょう。