読者です 読者をやめる 読者になる 読者になる

SFLのstageとNSLのseqを攻略する

SFL(NSL)の基本は簡単です。要はネットリスト記述やVerilog HDLのassignのような同時並列の信号の転送を「クロックごとに区切られたものとして」記述するだけです。端子への転送では値はすぐさま変化し、レジスタに転送されている値はハザードがおちついた後で取り込まれ、レジスタの出力する値は次のクロックで変化します。つまりクロック同期設計を前提とすることで、クロック同期設計の利点を自然に享受できるHDL、それがSFL(NSL)です。
メモをウェブページにまとめていますので( http://metanest.jp/sfl/ )そちらも参考にしてください。
この記事では、SFL/NSLで高度な機能とされている(たとえば、ふんが先生の『作りながら学ぶコンピュータアーキテクチャ』の旧版ではSFLを使っていますが、stageは扱っていませんでした)stageとseqについて「攻略」します。

stage

この機能に、SFLで「ステージ」という名前を使っているのは、SFLの「方式DA」すなわちコンピュータ・アーキテクチャ(マイクロアーキテクチャ)の設計を記述し実装を自動化するシステム、という元々の目的からの影響が大きいでしょう。NSLではprocという名前で、NSLには他にfuncがありますので、procとfuncでプログラミング言語における手続きと関数というなじみのある名前ということかもしれませんが、意味的にはその名前からの連想にはあまりなっていないように思います。
さて、プロセッサのパイプライン方式は、フェッチ・デコード・エクセキュート...といったステージ、すなわち実行ユニット中の活動する部分が順次変化してゆくもの、と見ることができます。これをどのようにモデル化するかはパイプライン型のプロセッサを記述する際のキモですが、SFLのステージはそのための、モジュールの中に、起動されると活動を始め必要が終わったら活動を終了するものを記述する言語機能です。
SFLではステージを使うために、ステージの中にタスクを定義しなければなりません。さらに、ジョブというものがマニュアルなどの用語として使われますが、これについて説明、というか「攻略」します。ステージには状態を作ることもできますが、それについては後で述べます。
まずジョブについて。マニュアル( http://www-lab09.kuee.kyoto-u.ac.jp/parthenon/NTT/hajimete/3shou_6.htm )では独特な説明がされています。簡単に言うと、プロセッサのパイプラインをイメージした時、実行すべき命令が各ステージの間で順番に受け渡されてゆくわけですが、それが要するにジョブです。「このステージを終了して、別のステージを起動する」という動作をいっぺんに行う構文糖的な機能(SFLではrelay、NSLでは プロシージャ名.invoke() とせず単に プロシージャ名() としただけで、暗黙のうちにrelayしたことになる(この設計は微妙な気がします))により、ジョブの受け渡し、というイメージが明確化されています。
続いてタスクですが、これは一言で言うと「ステージを起動する端子は複数あって、そのうちのどの端子から起動されたのかをレジスタで覚えている機能」です(タスクレジスタと言い ステージ名.タスク名 という記述で参照することもできる)。ほとんど活用されていない機能と考えられ、NSLのprocではこれに相当する機能は削除されています。従って、SFLでは特に深く考えることはせず、単に t か tsk という名前のタスクを1個だけ宣言し、ステージの起動などではそれを指定すればokです。

さて、続いてステージの癖について説明します。まず、起動のタイミングについてですが、ステージが動作状態(アクティブ)になるのは、起動のアクションがあった「次」のクロックからです。従って、実装においてステージを使いたい機能は必ず一つ前のクロックで起動が確定するものでなければなりません。このため、ステージの起動には引数を指定できますが、値を受ける方、すなわち仮引数としてはレジスタを指定しなければ意味が無いので、レジスタしか指定できません。
続いて、あるステージを複数起動できるのか、という点ですが、(仮にタスクを複数指定したとしても)できません。これはハードウェアとしてはインスタンスが1個しかないと考えれば自然です。パルテノン研究会編の「SFL仕様書」ではp. 18の「ジョブの生成」で「同一のステージに対し、異なるタスクで同時動作させるような指示を行ってはならない。」とありますが、同じタスクであっても単に無視されるだけで、バグの可能性を考えれば「起動してはいけない」として扱うべきでしょう。つまり、そのステージが動作状態にある場合は、動作の終了(finish)と同時の場合以外は新たに起動しようとしてはいけない、ということになります(終了と同時の場合は、それまでの動作はそのクロックで終了して、次のクロックでは新しい動作が始まる、と解釈できる)。同時に複数、同一のタスクを指定しての起動を動作させるのもやめておいたほうが良いかもしれません。
最後に状態について説明します。ステージは状態を持つことができ、ある状態にある時のみ動作する記述、というものを作ることができます。これもいくつか癖がありますので、順番に説明します。
まず、複数の状態からひとつ、初期状態を指定できますが、初期状態に戻るのはリセットがかかった時のみで、ステージの動作の終了では初期状態には戻りません。par { finish; goto st0; } といったように明示的に戻す必要があります。また、状態遷移が状態の中からの goto でしか制御できない、というのも癖のある点で、ソフトリセットのようなものをいつでも即時掛けられるようにしたい場合、全ての状態の記述に alt { reset_state: goto st0; ... } のように書かねばならず面倒です。場合によっては言語の機能を使わず、レジスタを宣言して自分で記述したほうが見通しが良いかもしれません(『作りながら学ぶコンピュータアーキテクチャ』の旧版ではそのように記述されています)。状態とは関係ありませんが、SFLではfinishも当該のステージの中に書かなければいけません(NSLでは プロシージャ名.finish() のようにして外から終了できるようになりました)。
さて、ステージが動作を開始するのは、起動された次のクロックからです。なので、最初の信号と同時に何か動作して、そのあと順番に動作する、といったものを書こうとすると、次のようにせざるをえません。

stage_name stg {
    task tsk();
}
instruct start_it par {
    何かする_0
    generate stg.tsk();
}
stage stg {
    state_name st1, st2, st3;
    state st1 par {
        何かする_1
        goto st2;
    }
    state st2 par {
        何かする_2
        goto st3;
    }
    state st3 par {
        何かする_3
        goto st1;
        finish;
    }
}

これも、うまい方法は無いのではないかと思います。
以上でステージについてはだいたい説明しました。SFLではステージの中にセグメントというものもありますが、これはステージからステージへのように投げっぱなしではなく、元に戻ることができる記述ができる機能です。あまり使われない機能で、相当するものがNSLには無いので省略します。
SFLのステージに相当する機能は、NSLではプロシージャ(proc)です。名前と、いくつかの機能が少しずつ違いますが、基本は同じなので詳しい説明は省略します。SFLに慣れていて、NSLを初めて使う、という人は一通りマニュアルに目を通しておくと良いでしょう。前述の、単に プロシージャ名() としただけで、暗黙のうちにrelayしたことになる、という罠以外は、そう大きな問題は無いと思います。

seq

NSLでは、SFLで instrなんとか という名前だったものが func_なんとか という名前に変わった以外は基本的には同じですが、seqという新しい特殊なブロックが func に対応する動作として記述できる、という点が、SFLと比べて大きな機能拡張になっています。(どうも他の場所でも書けてしまうことがあるようですが)seq は func 制御端子名 seq { ... } のように func に直接対応する場所にしか書けない、としたほうが混乱しないでしょう。なお、funcは(SFLのinstrファミリも)複数の信号がorされるものと規定されています。
SFLはHoareのCSPを参考にしているとされていますが、直接の影響というより、CSPをベースとしているOccamという言語からいくつかの考えを取り入れている、ということのようです。NSLのseqは、SFLが取り入れていないOccamの機能がベースのようです。
Occamでは、逐次実行であると明示的に指定しなければ、文は並列実行しても良い、という意味になります。Occamで逐次実行を指定するものがSEQです。NSLのseqは、クロック毎に逐次実行する、という処理を直接書けるというもので、少し前に挙げた例のような場合に威力を発揮します。NSLでは、さきほどの例を次のように書き直すことができます。

func start_it seq {
    何かする_0
    何かする_1
    何かする_2
    何かする_3
}

各クロックで行いたいアクションが複数ある場合は、

func start_it seq {
    {
        あれこれ0
        ...
    }
    {
        あれこれ1
        ...
    }
    {
        あれこれ2
        ...
    }
    {
        あれこれ3
        ...
    }
}

という感じになります。(内側の波カッコが、SFLでは par { ... } と書くものが、parキーワードがNSLで消えたものであることに注意)
seqの中には、whileやforが(マニュアルでは「whileブロック」「forブロック」。なお、この形式のifは無いので注意)書けますが、癖のあるものもあるので(特にループ制御などが、どこまで1クロック内で行われるかが微妙です)、使う際はサンプルを作ってシミュレーションで動作をよく確認したほうが良いでしょう。
また、NSLにはseqの中から直接プロシージャを起動すると、そのプロシージャが終了するまでそのseqは止まって待つ、という機能があります(NSLのPDF版マニュアル(バージョン1.4)p. 43)ので、seqとプロシージャを併用しようとする場合は注意してください。これを回避するには、funcname.invoke() のように .invoke を入れるか、直接 seq の文脈に現れないよう並列の { } で囲んで { funcname(); } としてください(これはsfl2vlからの仕様変更になっています ref: http://blog.goo.ne.jp/ip-arch/e/e4244388fb551aa97579998fc244ef6c )。
最後に、ステージ(プロシージャ)の状態機能は、ステートマシンを実装するシーケンサの種類で言うとデコード方式に相当する意味になっていますが、それに対してseqはワンホット方式に相当します。ですので、複数起動が可能です。

たとえば(クロックが10hz程度と、十分に遅いことを仮定します)、

func button_i seq {
    led0_o();
    led1_o();
    led2_o();
    led3_o();
}

のようにすると(LED出力のつもりです。値の転送が指定されていない時はoffにしたい端子は制御端子にする必要があります)、LEDの点灯・消灯が流れるように移動してゆくような感じになります。

その他

以上に述べたことに関することで他には、NSLではprocの中だけではなく、moduleでもstateにより状態を持たせることができます。moduleには起動や終了がないので、常に活動状態(すなわち、どれかの状態にある)ということになります。