インタフェ−ス仕様の記述
タイマ本体の記述
機能の記述と実現法の記述
SFL記述の補足
リスト2.1は,前述の仕様のクロック・タイマをSFL で記述したものです.この記述がなにを意味しているか,どうやって作ったか をみていきましょう.
<リスト2.1> クロック・タイマの記述(全体)
1: declare DECR8 { 2: input IN<8> ; 3: output OUT<8> ; 4: instrin ENABLE ; 5: instr_arg ENABLE( IN ) ; 6: } 7: 8: module TIMER { 9: instrin START, RESET ; 10: input INIT<8> ; 11: instrout EXPIRE ; 12: reg REMAINED<8> ; 13: DECR8 DECR ; 14: 15: stage_name MAIN { task RUN( REMAINED ) ; } 16: instruct START generate MAIN.RUN( INIT ) ; 17: 18: stage MAIN { 19: state_name DOWN, ASSERT ; 20: first_state DOWN ; 21: state DOWN any{ 22: RESET | START : finish ; 23: else : par{ 24: REMAINED := DECR.ENABLE( REMAINED ).OUT ; 25: if( DECR.OUT == 0x00 ) goto ASSERT ;' 26: } 27: } 28: state ASSERT any{ 29: RESET | START : par{ goto DOWN ; finish ; } 30: else : EXPIRE() ; 31: } 32: } 33: }
では,リスト2.2を見てください.これは,クロック・タイマのインターフェー スの仕様だけをSFLで記述したものです.このインターフェース仕様の宣言は, C言語における関数プロトタイプのようなもので,タイマの設計記述としては 不要ですが,この仕様のタイマをサブモジュールとして後々の設計で使用する ときに必要になります.
<リスト2.2> クロック・タイマのインターフェース仕様
1: declare TIMER { 2: instrin START, RESET ; 3: input INIT<8> ; 4: instrout EXPIRE ; 5: instr_arg START( INIT ) ; 6: }
3行目で「INITが8ビットの入力端子である」と宣言していることは直感的にわ かると思います.ここのinputは「データ用入力端子」を宣言する予約語です. また,2行目,4行目にはinstrinとかinstrout とかいう,見慣れない宣言があります.instrinは「制御用入力端子」を宣言 する予約語(instruction-inの略)で,instroutは「制御用出力端子」を宣言す る予約語(instruction-outの略)です.
つまり,リスト2.2では,INITがデータ用の入力端子,START, RESETが制御用 の入力端子, EXPIREが制御用の出力端子であることを意味しています.
このようにSFLでは,記述の判読性を上げるために,「制御用のオブジェクト」 と「データ用のオブジェクト」を明確に区別しています.一般に,データ用の オブジェクトは「処理されるべき情報の伝達や格納」のために使用され,制御 用のオブジェクトは「情報伝達のタイミングを指示する(同期をとる)」ために 使用されます.ここでは,START, RESET, EXPIREというのは,なんらかの動作 の契機を与える端子ですので,制御用のオブジェクトとして宣言するのです
5行目のinstr_arg(instruction-argumentの略)という文は, 「制御入力端子のSTARTをアサート(有効)にするときには,それにともなうデー タをINITに与えなければならない」ということを表しています.
構成要素の定義
動作の記述(その1)
動作の記述(その2)
動作の記述(その3)
SFL記述の時間的意味
まず,何が必要かを考えます.残り時間を保持しておくレジスタと,その値を 減らすためのデクリメンタが必要でしょう.これらが一緒になったカウンタが すでにライブラリにあれば,それを使えばよいのですが,今回はSFLの記述に 慣れるのが目的ですから,一から始めることにします.
SFLの記述で重要なことは,「どのような構成物を用い,それらをいつどのよ うに使うか」ということです.ですから,実現法がまだ自明でないデクリメン タは,とりあえず別モジュールとしておいて,後で設計することにしましょう (いわゆる階層化設計).目的のタイマの動作から考えれば,デクリメンタの実 現法はさほど本質的ではありせんし,ひょっとすると別の人が作ってくれるか もしれません.
ただ,別のモジュールを使う場合,使用するモジュールのインターフェース仕 様は宣言しておく必要があります(リスト2.3).もし,ここで使用するつもり のデクリメンタが,すでにファイル(ライブラリ)として存在していれば,リス ト2.3の代わりに,
%i "ファイル名"
と書くだけですみます.
<リスト2.3> 8ビット・デクリメンタのインターフェース仕様
1: declare DECR8 { 2: input IN<8> ; 3: output OUT<8> ; 4: instrin ENABLE ; 5: instr_arg ENABLE( IN ) ; 6: }
さて,8ビットのレジスタにREMAINED,デクリメンタに DECRという名前をつけることにして,タイマ本体の記述を始め ましょう(レジスタは,SFLの組み込みタイプとして自由に宣言できる).いま までにあげてきた,すべてのオブジェクトを定義します(リスト2.4).
<リスト2.4> クロック・タイマの宣言部
1: module TIMER { 2: instrin START, RESET ; 3: input INIT<8> ; 4: instrout EXPIRE ; 5: reg REMAINED<8> ; 6: DECR8 DECR ; 7: }
リスト2.2のインターフェース仕様の宣言では,STARTに対して instr_argという記述がありましたが,モジュール本体の定義 にはありません.このinstr_argは,STARTのアサートの仕方を 記述したものですが,タイマ・モジュール内部ではSTARTのアサートをしない (アサートするのはタイマを用いる外部のモジュール)のですから,その記述は 必要ありません.一方,EXPIREは,タイマ・モジュール自身がアサートします からinstr_argの記述をするべきですが,EXPIREのアサートに ともなうべきデータがないので省略してあります.
さて,必要なオブジェクトはそろいました.いよいよ仕様の実現です.この仕 様を満たすには,いくつかの状態に分けて考えるのがよさそうです.
これらの各状態を示すレジスタを設けて,その値で状態を管理してもよいでしょ う.しかし,SFLには,状態遷移の概念を明確に記述するための枠組み(ステー ジ)が用意されています.
ここでステージとは,「内部に状態をもち,状態遷移で表されるような逐次的 な制御を行うためのオブジェクト」です.また,ステージには, タスクという概念が付随しています. ここで呼ぶタスクとは,文字どおり「そのステージでやるべき仕事」と解釈してください. ステージのタスクは,「オン(仕事がある)かオフ(仕事がない)のいずれかの値」 をとり,タスクがオンのステージは「活性状態で状態遷移動作」を行います. オフのステージは「非活性状態となり停止」します.ステージを用いると(状 態0)はタスクがオフであることで表現できるので,実際に記述するのは,状態 1と状態2の二つ状態ということになります.
それぞれの状態の宣言と,その状態でやるべきことを書いてみましょう(リス
ト2.5).
<リスト2.5> クロック・タイマの動作記述(その1)
1: stage_name MAIN { task RUN( REMAINED ) ; } 2: stage MAIN { 3: state_name DOWN, ASSERT ; 4: first_state DOWN ; 5: 6: state DOWN : /* 状態 1 */ 7: REMAINED := DECR.ENABLE( REMAINED ).OUT 8: state ASSERT : /* 状態 2 */ 9: EXPIRE() ; 10: }
最初の行は,MAINというステージは,RUNという仕事を受け付け,そ のときには,その仕事に必要な一つのパラメータがレジスタREMAINEDにセット されるということを宣言しています.二つの状態に,DOWNとASSERTという名前 をつけ(3行目),ステージMAINの初期状態はDOWN(4行目)とします.状態DOWN (6行目〜)では,レジスタREMAINEDの値をデクリメンタに渡し,その減算結果 (DECR の OUTに出力されている)をREMAINEDの次の値にします(7行目). ASSERT状態(8行目〜)では, EXPIRE 出力をアサートします(9行目).
さらに,状態遷移(goto)を加えるとリスト2.6のようになります(9,13行目). 休眠状態は,タスク・オフで表しますから,RESET端子がアサートされたとき には,タスクを終了(finish)させます(6行目,13行目).ここで,
<リスト2.6> クロック・タイマの動作記述(その2)
1: stage_name MAIN { task RUN( REMAINED ) ; } 2: 3: stage MAIN { 4: ............ 5: state DOWN : any{ 6: RESET : finish() ; 7: else : par{ 8: REMAINED := DECR.ENABLE( REMAINED ).OUT ; 9: if( DECR.OUT == 0 ) goto ASSERT ; 10: } 11: } 12: state ASSERT : any{ 13: RESET : par{ goto DOWN ; finish() ; } 14: else : EXPIRE() ; 15: } 16: }
あとはリスト2.7の2行目に示すように,START端子がアサートされたときに, このステージMAINの処理するタスクRUNを,INIT端子に与えられている値をパ ラメータとし生成する(generate)という記述(2行目)を加えると,クロックタ イマの動作記述ができ上がりです.なお,STARTのアサートでもタスクは終了 する(7,14行目)ことに注意してください.すなわち,古いタスクがキャンセ ルされて,新しいタスクが生成が2行目により生成されるというわけです.
<リスト2.7> クロック・タイマの動作記述(その3)
1: stage_name MAIN { task RUN( REMAINED ) ; } 2: instruct START generate MAIN.RUN( INIT ) ; 3: 4: stage MAIN { 5: ............ 6: state DOWN : any{ 7: START | RESET : finish ; 8: else : par{ 9: REMAINED := DECR.ENABLE( REMAINED ).OUT ; 10: if( DECR.OUT == 0x00 ) goto ASSERT ; 11: } 12: } 13: state ASSERT : any{ 14: START | RESET : par{ goto DOWN ; finish ; } 15: else : EXPIRE() ; 16: } 17: }
以上を全部まとめたクロック・タイマの完全な記述が,前出のリスト2.1です. この記述で,シミュレータにかけられますし,論理合成にもかけられます.と はいっても,これが仕様どおりに動作すると納得した人は少ないかいもしれま せん.なにより,クロック・タイマといいながら,クロックなんてどこにも出 てきていませんし,いままでの説明では,タイミングに関する説明は行われて いません.しかし,すでにタイミングに関する部分もリスト2.1には含まれて いるのです.SFLの世界でのタイミングはいたって単純なので,明示的な記述 がないと考えればよいのです.
SFLで記述された回路は,(論理的に)単相のクロック(ほら,クロックが出てき た!)の同期式回路として動作することになっています.そして,SFLの個々の 文は,そのクロック・パルスの間の動作を定義しています.
たとえば,いまEXPIREを出力中に,STARTがアサートされた場合を考えます. 状態ASSERTで, STARTがアサートされた場合,par{goto DOWN; finish;}が指示されています.またSTARTの動作として, generate MAIN.RUN(INIT);となっています.ここをタイム・チャー トで示すと図2.2(a)のようになります(INITには0x40が与えられているとする).
<図2.2> 同期回路のタイミング
この図では,クロック周期内の信号の変化順が意味ありげに描かれていますが, SFL中にそんな記述がどこにもないように,一つのクロック周期内の信号の変 化順には,まったく意味はありません.次のクロックの立ち上がり(または立 ち下がり)直前の値だけが問題なのです.
これが同期式回路の本質であり,たとえば,同じクロック周期内では, STARTとRESETがどんな順でこようと,SFL(すなわち同期式回路)では, 両方がアサートされていると見えるだけです. したがって,SFLの各文にもクロック周期内での時間的な前後関係はありません.
any,altによる排他条件やブロックの包含関係を除けば(これらは時間的な前後関係ではなく, 条件の優先度の違い),SFLの文がどのような順序で書かれてもいても結果は同じで, 各文は並列に動作するものとして扱われます.たとえば,
par{REMAINED:=DECR.ENABLE(REMAINED).OUT;if(DECR.out==0)goto ASSERT;}
は,
par{if(DECR.OUT==0)goto ASSERT;REMAINED:=DECR.ENABLE(REMAINED).OUT;}
でもまったく同じ結果になります.後者のif文中のDECR.OUTが未定義になったり, 1クロック前の値となるようなことはありません.
クロックが入ると,記憶をもつオブジェクトの値は,図2.2(b)のように一斉に変化します. 以上が,SFLのタイミングのすべてです.
これでリスト2.1のSFLが,正しい記述であると納得できたのではないかと思います.
まだ,納得できないときは,図2.3を参考にすべてのパターンのタイム・チャートを記述してみて,
シミュレータ(SECONDS)で動作を確認してください.この例であれば,たいして手間ではありません.
さて,デクリメンタの設計は後回しにすると述べましたが, 何もないとシミュレーションのときに不便ですから, その機能ぐらいは書いておきましょう.
リスト2.8が 8ビットのデクリメンタです. SFLには,'-' (減算)という演算子は(負の数の表し方を決めていないため)用意されていないのですが, '+' (加算)はあります.ただし,符号なしで定義されています. 8ビットで1をひくのは,非負の範囲に限れば 0xff をたすのと等価ですから 記述の意味は明らかでしょう.
<リスト2.8> 8ビット・デクリメンタの機能記述
1: circuit DECR8{ 2: input IN<8> ; 3: output OUT<8> ; 4: instrin ENABLE ; 5: instruct ENABLE OUT = IN + 0xff ; 6: }
ここでのキーワードはmoduleではなく, circuitとなっていることに注意してください. これは,この部分は論理合成の対象としないことを意味します. circuitを使うのは,つぎの二つの場合が考えられます.
デクリメンタも含めて論理合成をしようとする場合は, DECR8をmoduleで書き直す必要があります. moduleでは加算やシフトは使えませんので, これらをどう実現するかを考えなければなりません.
デクリメンタは,機能記述の式から明らかなように, 加算器の一方の入力の全ビットが"1"である場合 を考えればよいわけです.加算器の第iビットの出力Siは(LSBを第0ビットとする),
Si = Ai @ Bi @ Ci-1 : i >= 0ですから,Bi=1とすると,Ci = (Ai & Bi) | (Bi & Ci-1) | (Ci-1 & Ai) : i >= 0
C-1 = 0
(ここで,'^'は否定,'&'論理積,'|'は論理和,'@'は排他的論理和を表す)
Si = ^(Ai @ Ci-1)となります.あとはこれをSFLに翻訳するだけです.Ci = Ai | Ci-1 | (Ci-1 & Ai)
= Ai | Ci-1
= Ai | Ai-1 | .... | A0 | 0
これをSFLで記述すると,リスト2.9のようになります ( "||"はビット列の連結,"<m:n>"は LSBを0とした第mビットから第nビットまでの部分列の切り出し, "/|"は全ビットの論理和をそれぞれ意味している).
<リスト2.9> キャリ・ルック・アヘッド方式の8ビット・デクリメンタリスト2.9は,8ビットのキャリ・ルック・アヘッドでデクリメンタ機能の実現法を記述していますが, もちろん,回路の簡潔さや経済性を考えて,ビットごとにキャリを伝達する方式での実現も有力な候補 ですし,その中間(2ビットや4ビットのキャリ・ルック・アヘッド)も考えられます. 参考までに,キャリを伝達する方式での記述例もリスト2.10に示しておきます.1: module DECR8 { 2: input IN<8> ; 3: output OUT<8> ; 4: sel_v C<8> ; /* temporal terminal */ 5: instrin ENABLE ; 6: 7: instruct ENABLE par{ 8: C = /|( IN<6:0> ) /* IN<6> | ... | IN<0> */ 9: || /|( IN<5:0> ) /* IN<5> | ... | IN<0> */ 10: || /|( IN<4:0> ) /* IN<4> | ... | IN<0> */ 11: || /|( IN<3:0> ) /* IN<3> | ... | IN<0> */ 12: || /|( IN<2:0> ) /* IN<2> | ... | IN<0> */ 13: || /|( IN<1:0> ) /* IN<1> | ... | IN<0> */ 14: || IN<0> || 0b0 ; 15: OUT = ^( IN @ C ) ; 16: } 17: }
<リスト2.10> キャリ伝搬方式の8ビット・デクリメンタ1: declare DECR_CP{ 2: input IN, CI ; 3: output NS, CO ; 4: instrin EN ; 5: instr_arg EN( IN, CI ) ; 6: } 7: 8: module DECR_CP{ 9: input IN, CI ; 10: output NS, CO ; 11: instrin EN ; 12: instruct EN par{ NS = ^(IN @ CI) ; CO = IN | CI ; } 13: } 14: 15: module DECR8 { 16: input IN<8> ; 17: output OUT<8> ; 18: sel_v C<8> ; /* temporal terminal */ 19: instrin ENABLE ; 20: DECR_CP CP6, CP5, CP4, CP3, CP2, CP1 ; 21: 22: instruct ENABLE par{ 23: C = CP6.EN( IN<6>, C<6> ).CO || 24: CP5.EN( IN<5>, C<5> ).CO || 25: CP4.EN( IN<4>, C<4> ).CO || 26: CP3.EN( IN<3>, C<3> ).CO || 27: CP2.EN( IN<2>, C<2> ).CO || 28: CP1.EN( IN<1>, C<1> ).CO || 29: IN<0> || 0b0 ; 30: OUT = ^( IN<7> @ C<7> ) || 31: CP6.NS||CP5.NS||CP4.NS||CP3.NS||CP2.NS||CP1.NS|| 32: ^IN<0> ; 33: } 34: }
SFLのオブジェクトは,
<表A.1> オブジェクトの分類
stage, segment, taskはSFLに特有の概念ですし,SFLのレジスタやメモリ (メモリもクロックに同期している)のモデルは,常識的なイメージのものですから, 記憶付き(レジスタ系)オブジェクトではそれほどの混乱はないと思います. しかし,記憶なし(端子系)オブジェクトのほうは,たんなる信号線(およびその終端)とは 異なっているので,SFL特有のモデルを理解する必要があります.
SFLでは,データ端子が,つねになんらかの値をもっているということは保証されていません. データ端子では,そのオブジェクトに対する代入文が実行された(ドライブされた)ときにのみ その値が定義されます.また,二つ以上の代入文が同じデータ・オブジェクトを異なる値で ドライブすることは許されません.
一方,制御端子は,タイミングを制御する重要なオブジェクトですから,値が定義されない というのでは困ります.このため,つねに"0"または"1"の値をもつこと が保証されていますが,データ端子と同じようには値を代入できません.C言語の関数呼び出し に似た制御端子のアサート文が実行されたときに制御端子がアサートされたことにな り("1"になる),どのアサート文も実行されないときにかぎり,ネゲート(無効化)さ れたことになります("0"になる).したがって,制御端子を積極的にネゲートすること はできません.
これらの特徴から,あえてイメージで表現すると,
これで,なぜRESET端子などをデータ入力端子ではなく,制御入力端子として定義したのかが,わかるので なないかと思います.後の記述で,RESETの値を条件判定に使用しているのですが,RESETをデータ端子と して定義してしまうとその値が未定義となることがあり,条件の真偽が決定できなくなってしまうからで す(TTLではハイ・インピーダンスは"high"と等価となりますが,PARTHENONのツール群はこのような 特定のテクノロジを仮定していない).
デクリメンタのENABLE端子の場合は事情が少し違います.このデクリメンタでは,入力(IN)の値が未定義の ときは,出力(OUT)の値はなんでもよいので("don't care"),ENABLE端子はどうしても必要な オブジェクトではありません.定義されているほうが,SFLらしいというのにすぎません.ただし, シミュレーションでは,ENABLE端子がない場合,INの値が未定義になると 警告メッセージが出されます.
より正確には「タスクは仕事の一部」を意味します. 仕事全体にジョブという言葉を使うとすれば,ステージのタスクは 「そのステージに割り当てられたジョブ・ステップ」を表します.
この例では,ジョブは制御端子STARTの アサートによりパラメータである初期値とともに与えられ,その内容は, 「その初期値をカウント・ダウンし,0になれは次のジョブが入るか (STARTのアサート)か中止命令(RESETのアサート)が入るまで, EXPIREをアサートしつづける」ということで, STARTかRESETがアサートされると一つのジョブは終わります.
これは,本文中の状態遷移を改めて記述しただけのようにみえますが, 状態遷移のような静的な見方に加え,ある一連のジョブという動的な見方 を提供しています.たとえば,この見方では,1回目のタイマ動作と 2回目のタイマ動作とは,異なるジョブと考えます. このジョブのうち,初期値をREMAINEDにセットするまでが, 制御端子STARTによるジョブ・ステップ,その後がステージMAINに 割り当てられたジョブ・ステップで,RUNという名前がつけられています.
一つのステージは,状態遷移という逐次的な制御しか記述できませんが, モジュールには複数のステージを記述することができます.ジョブをいくつかに分割し, それらのジョブ・ステップをいくつかのステージに割り振ると, 自分のジョブ・ステップが終わったステージは,次のジョブの ジョブ・ステップが実行できるので,パイプラインによる並列化が行えることになります.
タスクの宣言と制御端子の宣言,および,タスクの起動と制御端子アサートが 非常によく似ていることに気付かれた人もいると思いますが, 実際,制御端子は多くの場合モジュールを超えてのジョブ 受け渡しの仲介として使われます.
また,制御端子STARTとステージMAINのタスク名RUN, デクリメンタ・モジュールの制御端子ENABLEをすべてTIMERという名前に変えてみると, 制御端子TIMERはこのモジュールのジョブを代表しているように思えますし, デクリメンタ・モジュールの制御端子TIMERは, MAINにおけるジョブ・ステップTIMERのさらに一部を担当していると考える こともできます.
文中に「SFLは同期式回路専用ですから,クロックの立ち上がり(または立ち下 がり)直前の値だけが問題です」と書きましたが, 逆にいえば,SFLではこれ に反するような設計を記述するとはできません.たとえば,クロック信号を操 作することはできませんし,フリップ/フロッのトリガ入力にクロック以外の 信号を導くこともできません.また,レジスタを通さないル−プ・パスも許され ませんし,クロック周期以上の遅れをもつ組み合わせ回路も許されません.
非同期式回路の設計に慣れている人は,この例のSTARTやRESETの信号は非同期 で扱いたいとうかもしれませんが,SFLではそういう回路は設計できないので す(蛇足だが,ハンドシェイクなどの非同期の通信プロトコルのことをいってい るのではない).これは一見不便に思えるかしれませんが,SFLのような同期式 回路では,STARTやRESET信号のハザードの心配は不要ですし,レーシングも起こ りません.また,細かいタイミングを見なくてすむので,シミュレーシンの高 速化が図れますし,論理合成のパラメータは激減し,合成された回路の動作確 率はきわめて高くなります.
同期式回路では,タイミングの制約はクロック・スキューによるレジスタの データ筒抜けがなか(ホールド時間が確保されているか)と,レジスタ間のデータ転送が クロック周期内で終わる(セットアップ時間に余裕があるか)という問題になります.
PARTHENONでは,これらの問題はテクノロジ・マッパ(OPT_MAP)により静的に検 証され,可能かぎり制約を満たすような回路が合成されます. つまり,SFLは, 特定の半導体メーカやそのライブラリを前提とした設計をするための言語では ありません.自動合成によってその設計を的確に実現するためのメーカやその ライブラリを選択するという立場をとります.