Xilinx ISE におけるの制約の与え方 のバックアップ(No.6)

更新


公開メモ

Xilinx ISE を使った FPGA 開発における制約の書き方と満たし方を勉強する

FPGA や CPLD の開発では、回路の動作を HDL 言語で正しく記述するだけではだめで、 その回路がきちんと要求されるタイミングで動くことが必要になります。

FPGAの部屋 さん曰く、 「XilinxのFPGAはHDLが書けても、まだ半分しかマスターできたことにならないと思っている。」 とのことで、実際後半戦では FPGA 内部の構造まで踏み込んだ理解が必要だったり、 初心者にとってはいろいろと苦労が多いです。。。

ということで、動作クロックを高めなければならなかったり、 高速な周辺機器とやりとりをしなければならなかったりするときに重要になる、 「制約」の使い方について勉強する羽目になりました。

FPGA 回路設計での「制約 (constraint)」は、

  • 回路の動作速度やタイミングに対する要求をツールに伝える
  • ツールが要求を満たすことができないときに、実装のヒント(配置の仕方など)を与える

の2つの目的で使われるようです。

以下では、私が初めて FPGA 開発をする際に使った種々の制約について、 考え方や、ツールの使い方をメモしたものです。

いかんせん初めてなので、間違いなども多いかもしれません。 お気づきの点があればコメントをお願いします。

というか、まだ実機で試してない状況で書いているので、 鵜呑みにしないように(?!)お願いします。。。

情報源

以下の内容は主に、FPGA の部屋 で学んだ内容に、私の経験をちょっとだけ加えたものです。

貴重な情報を公開して下さっている著者の marsee さんに深く感謝します。

ピン配置および入出力タイプの制約

すでに仕様の決まっている基板に対して IP 設計を行う場合には、 始めからピン配置が決まっているので、それをツールに伝えます。

一方で、基板の設計と IP 設計とを同時に行う場合には、 大まかな位置だけを指定しておき、後はツールに任すこともできるようです。

制約を与えるには、

  1. プロジェクトの最上位モジュール (Top Module) に必要な input / output / inout ポートを実装
  2. ISE Project Navigator の Hierarchy ペインで Top Module を選択
  3. Processes ペインで [User Constraints] - [I/O Pin Planning (PlanAhead) - Pre-Synthesis] をダブルクリック

として GUI で行っても良いのですが、
http://marsee101.blog19.fc2.com/blog-entry-25.html
に書かれているとおり、.ucf ファイルをテキストとして編集することで、 一度に複数のピンに対して IO レベルを指定するようなことも可能です。

テキストで制約を与えるのであれば、

  1. プロジェクトの最上位モジュール (Top Module) に必要な input / output / inout ポートを実装
  2. ISE Project Navigator の Hierarchy ペインで Top Module を右クリック
  3. [New Source] から Implementation Constraints File を選んで、適当な名前でファイルを作成
  4. できた .ucf を選択し、Processes ペインで [User Constraints] - [Edit Constraints (Text)]

という手順です。

もちろん、すでにプロジェクトに .ucf ファイルが存在すれば、1.〜3. は必要ありません。

制約の書き方としては、

NET "eth_txd[0]" LOC = J8;
NET "eth_txen" LOC = D3;

というようなピン毎の設定の他、

NET "eth_*" SLEW = FAST;

のように、* や ? などのワイルドカードを使った一括指定が可能です。

「これらのピンの中から適当なものを選んでくれ」という制約を与えるには、 LOC の後ろに可能なピンをカンマ区切りで与えます。

NET "eth_txen" LOC = D3, D4, D5;

クロック周期の制約

次に指定したくなるのがクロック周期に対する制約です。

http://marsee101.blog19.fc2.com/blog-entry-26.html で触れられているように、 Timing Constraints ツールから設定するのが楽なようです。

起動方法は、

  1. Hierarchy ペインで .ucf ファイルをダブルクリック

です。

画面左の Constraint Type から Clock Domains を選択すると、 Unconstrained Clocks の部分にクロックの一覧が出るので、 ダブルクリックして [Clock signal definition] - [Specify time] - [Time] に周期を入力します。

DCM を使う場合のクロック周期制約

ここで注意が必要なこととして、 DCM を使って生成されるクロック信号に対しては クロック周期制約を掛けることができません。

えーと、できない、というのはちょっと語弊があって、 DCM に与えるクロックに制約を掛けておけば、 生成されるクロックに対しても自動的に制約がかかるため、 個別に制約を掛ける必要がない、というのが正しいです。
参照 >> http://marsee101.blog19.fc2.com/blog-entry-1335.html

というわけで、1つだけ制約を掛けておけば、 そこから生成されるすべてのクロックに正しい制約が掛かることになります。

クロックドメインをまたぐ信号を除外

上記のように DCM で生成される複数のクロックに対して制約が掛かっている状況では、 ツールは関連するクロックドメイン間での信号の受け渡しについても、 律儀に遅延解析を行います。

通常、そのようなインタークロックな信号は、 受け渡しに特別な注意が必要になるため、 ロジック側でハンドシェイクその他の同期処理を入れて、 間違いのないデータの受け渡しを行うよう注意して HDL を書きます。

ところがツールはそのような高レベルの同期処理を理解できないため、 同期処理を入れなくても、遅延時間のみで正確な受け渡しのできる程度まで クロック周期を落とすよう指示してきます。 このため、インタークロックな信号に対して遅延解析が行われると、 非常識なほど低いクロック周波数が算出されてしまいます。

そこで、クロックドメインをまたぐ信号について、 「遅延解析を行わない (TIG : Timing Ignore a net)」という制約(?)を 付けることで、正しい周波数解析が行えるようにします。

恐らく一番間違いのない方法は、 インタークロックの同期処理を行っている HDL ソースに TIG 制約を埋め込む方法です。

例えば次のようにして、インタークロックな信号に TIG 制約を掛けることができます。

NLANG:verilog
// クロックを越えて、トリガ信号を確実に伝えるためのモジュール
// itrig に iclk に同期した正論理のトリガを入れると、
// otrig に oclk に同期した1クロック幅のトリガを生じる

module interclock_trig(
    input rst,
    input iclk,
    input itrig,
    input oclk,
    output otrig
);

    (* TIG="TRUE" *) reg itrig_extended;
    (* TIG="TRUE" *) reg otrig0, otrig1;
    always @(posedge iclk)
        if(rst)
            itrig_extended <= 0;
        else
        if(itrig)
            itrig_extended <= 1;
        else
        if(otrig0)
            itrig_extended <= 0;

    always @(posedge oclk) begin
        if(itrig_extended)
            otrig0 <= 1;
        else
            otrig0 <= 0;
        otrig1 <= otrig0;
    end
    assign otrig = !otrig1 & otrig0;

endmodule

あるいは、すべてのインタークロック信号に1つ1つ TIG 制約を掛けるのが面倒、 という場合には、http://marsee101.blog19.fc2.com/blog-entry-27.html のように 一括で TIG 制約を掛ける方法もあって、、、結局こっちを選んじゃったりします。

NLANG:verilog
    BUFG clk_125MHz_bufg (
        .I(clk_125MHz_pre),
        .O(clk_125MHz));

    BUFG clk_100MHz_bufg (
        .I(clk_100MHz_pre),
        .O(clk_100MHz));

    IBUFG clk_125MHz_in_ibufg (
        .I(clk_125MHz_in),
        .O(clk_125MHz_in1));
    
    DCM_SP #(
        .CLKDV_DIVIDE(1.0),                     // Divide by: 1.5,2.0,2.5,3.0,3.5,4.0,4.5,5.0,5.5,6.0,6.5
                                                //   7.0,7.5,8.0,9.0,10.0,11.0,12.0,13.0,14.0,15.0 or 16.0
        .CLKFX_DIVIDE(5),                       // Can be any integer from 1 to 32
        .CLKFX_MULTIPLY(4),                     // Can be any integer from 2 to 32
        .CLKIN_DIVIDE_BY_2("FALSE"),            // TRUE/FALSE to enable CLKIN divide by two feature
        .CLKIN_PERIOD(8.0),                     // Specify period of input clock
        .CLKOUT_PHASE_SHIFT("NONE"),            // Specify phase shift of NONE, FIXED or VARIABLE
        .CLK_FEEDBACK("1X"),                    // Specify clock feedback of NONE, 1X or 2X
        .DESKEW_ADJUST("SYSTEM_SYNCHRONOUS"),   // SOURCE_SYNCHRONOUS, SYSTEM_SYNCHRONOUS or
                                                //   an integer from 0 to 15
        .DLL_FREQUENCY_MODE("HIGH"),            // HIGH or LOW frequency mode for DLL
        .DUTY_CYCLE_CORRECTION("TRUE"),         // Duty cycle correction, TRUE or FALSE
        .PHASE_SHIFT(0),                        // Amount of fixed phase shift from -255 to 255
        .STARTUP_WAIT("FALSE")                  // Delay configuration DONE until DCM LOCK, TRUE/FALSE
    ) DCM_inst (
        .CLK0(clk_125MHz_pre),      // 0 degree DCM CLK output
        .CLKFX(clk_100MHz_pre),     // DCM CLK synthesis out (M/D)
        .CLKFB(clk_125MHz),         // DCM clock feedback
        .CLKIN(clk_125MHz_in1),     // Clock input (from IBUFG, BUFG or DCM)
        .PSEN(1'b0),                // Dynamic phase adjust enable input
        .RST(1'b0)                  // DCM asynchronous reset input
    );

のようにして生成したクロックに対して、.ucf に次のように書きます。

PIN "DCM_inst.CLK0" TNM = "CLK125MHz";
PIN "DCM_inst.CLKFX" TNM = "CLK100MHz";
TIMESPEC TS_IGNORE1 = FROM "CLK125MHz" TO "CLK100MHz" TIG ;
TIMESPEC TS_IGNORE2 = FROM "CLK100MHz" TO "CLK125MHz" TIG ;

ここでは、DCM_inst の、CLK0 および CLKFX ピンにそれぞれ CLK125MHz、 CLK100MHz という名前をつけ、CLK125MHz から CLK100MHz、あるいは逆の CLK100MHz から CLK125MHz への信号すべてに TIG 制約を掛けています。

その他の信号への TIG 制約

インタークロックな信号以外にも、数クロック程度の遅延を許容できる信号、 例えば割り込み信号線や、他モジュールの起動信号線にも TIG 制約を掛けておくと フィッターが無理してその線を短くしようとしなくなるため、 他の部分の性能が向上することが期待できます。

が、そう言った信号にはあらかじめレジスタで遅延を入れておいた方が、 遅延量が予測可能になる、不定値が伝搬しない、など、ずっと良いのかもしれません。

配置制約

クロック周期に対する制約は、 個々のモジュール単位、あるいはそれらを数個繋いだ段階では制約を満たせているのに、 繋ぐモジュールの数が増えていくと、制約を満たせなくなってしまうことが良くあります。

この理由としては、モジュール間の配線が長くなって遅延時間が増加していることが多いようです。

どこがボトルネックになっているかは、Place & Route 後に PlanAhead を起動すれば見ることができ、 同じ画面でボトルネック解消のためのヒントを与えることもできます。

ここで始めに何点か注意:

Synthesize Report の Timing Summary

Synthesize が出してくる Report にも Timing Summary があるのですが、 繋ぐモジュールの数が増えてくると、この値はあまり当てにはならないようです。

特に、上記のように DCM を使った回路では Synthesize が TIG を解釈しないので、 クロック周期などの見積もりがむちゃくちゃになってしまいます。

したがって、コンパイル時間が長く掛かってしまうのですが、 ちゃんとしたタイミング解析をするには Place & Route まで走らせなければなりません。

一方、モジュール単位のクロック周波数見積もりなどの用途では、 Synthesize Report の値も十分に参考になります。

Place & Route でエラーが出る

あまり無茶な制約が掛かっていると、Place & Route がエラーで止まってしまいます。

ERROR:Par:228 - At least one timing constraint is impossible to meet because 
  component delays alone exceed the constraint. A timing constraint summary 
  below shows the failing constraints (preceded with an Asterisk (*)). Please
  use the Timing Analyzer (GUI) or TRCE (command line) with the Mapped NCD and 
  PCF files to identify which constraints and paths are failing because of the 
  component delays alone. If the failing path(s) is mapped to Xilinx components as
  expected, consider relaxing the constraint. If it is not mapped to components as 
  expected, re-evaluate your HDL and how synthesis is optimizing the path. To allow 
  the tools to bypass this error, set the environment variable 
  XIL_TIMING_ALLOW_IMPOSSIBLE to 1. 

信号線の遅延時間は(素子自体の遅延)+(信号伝達の遅延)で決まります。

このうち素子の配置を工夫して減らせるのは(信号伝達の遅延)の方だけですが、 あまりに制約が厳しいと(素子自体の遅延)だけで、 要求される遅延時間を超えてしまいます。

この場合、どんなに配置を工夫しても制約を満たすのは無理ですので、 HDL 記述のレベルで回路を見直す必要があります。

で、どうやって見直すかというと、、、

上記のエラーメッセージの直後に、満たせなかった制約の一覧が出ますが、 それだけだと何が悪いか分かりません。

Processes ペインの [Implement Design] - [Map] - [Generate Post-Map Static Timing] - [Analyze Post-Map Static Timing] をダブルクリックして、Timing Analyzer を起動します。

エラーとなったパスに赤のチェックが付いていますので、 その内容から HDL レベルで回避策を講じることになります。

Trace & Route が完了したが、いくつかの制約が満たされない

Design Summary/Report の [Design Overview] - [Summary] ページの 右上の Timing Constraints のところを見ると、 与えた制約がちゃんと満たされたかどうかが分かります。

うまく行っていれば All Constraint Met となりますが、
そうでなければ 2 Failing Constraints などとなります。

この状況は、「素子の配置を工夫すれば制約を満たせる可能性がある」ということで、 配置制約の出番になります。

PlanAhead の基本的な使い方

Trace & Route でいくつかの制約が満たされないという結果になった時、 PlanAhead というツールでその原因を探り、 解決のヒントとして配置制約を与えることができます。
http://marsee101.web.fc2.com/planahead.html

起動方法は、Processes ペインの [Implement Design] - [Place & Route] - [Analyze Timing / Floorplan Design (PlanAhead)] をダブルクリック、です。

PlanAhead1.png

制約を満たさなかったパスが左下で赤文字で出ているので、 それをクリックすると右上の領域に白い矢印で該当パスが表示されます。

ツールバーの緑で囲った部分を使って拡大すると、 1つ1つの素子の配置まで見ることができます。

遅延の大きいパスは大抵非常に長距離を走っているので、 この距離が小さくなるように素子の配置を変えてやるのが、 ここからの仕事です。

真ん中の青丸で囲ったところに "Create Site Constraint Mode" というのがあるので、 これをクリックしてから素子をドラッグ・ドロップすると、 特定の素子の位置を望みの固定することができます。

「素子を動かして矢印の長さを短くする」というだけなら簡単なんですが、 それらの素子には矢印で表示されたクリティカルパス以外のパスも当然繋がっていて、 あちらを短くするとこちらが長くなる、というようなことも起きるので、 言葉で言うほど簡単ではないです。

一番下のタブで I/O ポート位置なども表示できるので、 それらを参考にしながらうまい配置を思い浮かべたりするのですが、 ここではブロックメモリの位置を固定することで、 全体の配置を調整することを試みました。

PlanAhead2.png

ドラッグ中の素子には、その素子へ、 あるいはその素子からのパスがたくさん表示されるので、 どこに下ろすかの指標になるのですが、、、

ここでは大まかな位置だけ決めて、 残りの細かい素子はおおきく自動で再配置してもらおうという算段なので、 線の長さはあまり気にしていません。

制約により固定された素子はオレンジで表示されています。

気の向くままにいじったら制約を保存して、 もう一度 Place & Route します。

ucf ファイルに追加された制約は次のようになりました。

INST "trimac/rx/rxmem/Mram_data2" LOC = RAMB16_X0Y17;
INST "writer/rxfifo/rxfifo/mem" LOC = RAMB16_X1Y19;
INST "trimac/rx/rxmem/Mram_data1" LOC = RAMB16_X0Y18;
INST "trimac/control/Mrom__varindex0000" LOC = RAMB16_X0Y19;
INST "reader/txfifo/txmem/mem" LOC = RAMB16_X0Y20;
INST "reader/rxfifo/bmem/mem" LOC = RAMB16_X1Y18;
INST "trimac/tx/Mram_mem" LOC = RAMB16_X0Y21;

上記制約を与えてフィッティングした結果です。

PlanAhead3.png

今回はメモリの配置で大まかな各モジュールの位置を指定するだけで、 残りの素子はツールによる自動配置に任せたまま全ての制約をクリアすることができました。

いつもそううまく行くわけではないのでしょうが・・・

配置指定をテキストベースで行う

上記の方法では、要素の位置を完全に一カ所に決定する制約を与えましたが、 ucf ファイルをテキストベースで編集する場合には、

INST "trimac/rx/rxmem/Mram_data2" LOC = RAMB16_X0Y21,RAMB16_X0Y20,RAMB16_X0Y19;

のようにして、列挙された3カ所のどこか、という制約を与えたり、

INST "trimac/rx/rxmem/Mram_data2" LOC = RAMB16_X0Y16:RAMB16_X1Y21;

のようにして、左下と右上とで囲まれる四角形の領域の中、という制約を与えることもできます。

気をつける点

テキストベースで上記のような幅を持った制約を与えた場合、PlanAhead がその制約を解釈してくれないので、 PlanAhead で制約を追加や変更して上書きすると、テキストで与えた制約が失われてしまうようです?

回避策?

ucf ファイルを2つ作って、ツールに任せるものと手で編集するものとに分けて使えば良いんですかね。

main.ucf と main_by_hand.ucf を作っておくと、 PlanAhead の起動時にどちらを読み込むか問い合わせてくるので、 main.ucf を選んでやれば main_by_hand.ucf の記述がいつの間にか消えてしまうという現象を避けられそうです。

入出力のタイミング制約

参照> http://marsee101.blog19.fc2.com/blog-entry-26.html

入力信号のセットアップ&ホールド時間制約

OFFSET = IN 2.5 ns VALID 3.0 ns BEFORE "eth_rxclk" RISING;

の形で、eth_rxclk に同期して入力される全てのピンに対して、 最低限 2.5 ns のセットアップ時間と 3.0 - 2.5 = 0.5 ns のホールド時間で動作するよう制約を掛けることができます。

出力信号のクロックエッジからの遅延時間制約

OFFSET = OUT 6 ns AFTER "clk_125MHz_in" REFERENCE_PIN "eth_gtxclk" RISING;

の形で、clk_125MHz_in に同期して(clk_125MHz_in から DCM で生成されるいずれかのクロックに同期するものも含む) 出力されるすべての信号は eth_gtxclk の RISING EDGE から 6 ns 以内に値が決定しなければならない、という制約を与えます。

同じクロックに同期して出力される信号の中から特定のネットを指定して制約を掛けるには、

NET "eth_tx*" OFFSET = OUT 6 ns AFTER "clk_125MHz_in" REFERENCE_PIN "eth_gtxclk" RISING;

あるいは、

INST "eth_tx*" TNM = PADGROUP_ETH_TX;
TIMEGRP "PADGROUP_ETH_TX" OFFSET = OUT 5 ns AFTER "clk_125MHz_in" REFERENCE_PIN "eth_gtxclk" RISING;

の形を使います。

NET を使った場合と TIMEGRP を使った場合との違いは、 NET を使った場合にはワイルドカードを展開した結果、 複数の制約として認識されるため、Timing Analyzer 上で非常に多くの制約が表示されてしまうのに対して、 TIMEGRP を使った方では1つの制約になることが挙げられます。

手動ではなく GUI の Constraints Editor を使う場合には NET 指定時のワイルドカードを認識してくれないので、

INST "eth_txd[0]" TNM = PADGROUP_ETH_TX;
INST "eth_txd[1]" TNM = PADGROUP_ETH_TX;
INST "eth_txd[2]" TNM = PADGROUP_ETH_TX;
INST "eth_txd[3]" TNM = PADGROUP_ETH_TX;
INST "eth_txd[4]" TNM = PADGROUP_ETH_TX;
INST "eth_txd[5]" TNM = PADGROUP_ETH_TX;
INST "eth_txd[6]" TNM = PADGROUP_ETH_TX;
INST "eth_txd[7]" TNM = PADGROUP_ETH_TX;
INST "eth_txen" TNM = PADGROUP_ETH_TX;
INST "eth_txer" TNM = PADGROUP_ETH_TX;
TIMEGRP "PADGROUP_ETH_TX" OFFSET = OUT 5 ns AFTER "clk_125MHz_in" REFERENCE_PIN "eth_gtxclk" RISING;

のようにすべてのネットを列挙する形で制約を作成することになるようです。

一応 PlanAhead とは違って、対応していない制約も表示だけは可能ですし、 上書き保存時に、ツールが非対応の制約が消えてしまうことも無いようです。

I/O バッファー制約

入出力タイミング制約を満たすには、IBUF あるいは OBUF と呼ばれる入出力ピンに内蔵された(?) フリップフロップを活用するのが近道なようです。このための制約が IOB 制約と呼ばれます。

Verilog では、

NLANG:verilog
(* IOB="FORCE" *) reg some_register;

などと書くことで、特定のレジスタを IBUF あるいは OBUF として実装することができます。

IOB="TRUE" という書き方もできるのですが、"FORCE" としておけばレジスタを IOB に入れることができなかったときにエラーとなるので、IOB を使っているつもりでいたのに実は使えていなかった、というようなミスを防ぐことができます。

他にも遅延制約など、いろいろテクニックがありそうなのですが、 さしあたっては以上の方法でうまく行ってしまったようなので、 また必要になったときに書き足します。

コメント

  • こんにちは。紹介していただいてありがとうございます。私は、筑波大のシス情技術室所属です。 -- [marsee]

Counter: 103039 (from 2010/06/03), today: 3, yesterday: 0