SoCFPGAのハードマクロをFPGA側I/Oから使用する場合の注意点

# 概要
　SoCFPGAのI/Oは，HPS側I/OとFPGA側I/Oに二分されます。SoCFPGAにはEMAC，I2C，QSPI等を制御する種々のハードマクロが搭載されており，これらはHPS側I/Oを介して接続することができます。一方，HPS側I/Oの数は限られており，ハードマクロすべてを有効にしてHPS側I/Oから接続することはできません。
　本制約に対処可能な手段として，ハードマクロの信号線をFPGA側にルーティングし，FPGA側のI/Oを利用する方法があります。
![Screenshot from 2020-09-12 22-43-08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/709057/696b8c33-6788-fc30-2e5c-ee0ad7099671.png)
　【CycloneV HPS Technical Reference Manual　抜粋】
　EMACについてはHPS側I/OまたはFPGA側I/Oいずれかが利用可能である旨，記載があります。

　このようなモジュールについてFPGA側のI/Oを使用する場合，当然FPGA側でタイミング制約を記述し，Setup/Holdが要件を満たすように合成する必要があります。単にI/Oをそのまま出力するだけのように感じますが，うまくタイミングが収束しないパターンがあります。
　本記事ではその一例を挙げたいと思います。

# 検証環境
- Linux ubuntu20 5.4.0-42-generic
- Quartus Prime Lite Edition 20.1.0.711

# 検証用プロジェクトの用意　
　今回はCycloneV SoC（5CSTFD6D5F31I7）を使用します。5CSTFD6D5F31I7を選択してプロジェクトを作成します。
![Screenshot from 2020-09-12 20-00-38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/709057/cea1ba78-d37f-b6bd-1695-9a9fa8d8d072.png)

　続いて，Platform Designerを用いてHPSモジュールを作成します。この際，EMAC0をFPGA側に出力するよう設定します。![Screenshot from 2020-09-12 20-03-11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/709057/9e722894-47c4-f002-a85e-cf98c1beede8.png)
　作成したモジュールの名前を変更します（hps_0 => hps）。必要な信号をダブルクリックでExportし，Generateボタンを押下します。![CUTScreenshot from 2020-09-12 20-11-37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/709057/19c8a273-2acb-4b0a-17db-0d11b6e2cc60.png)
　
　次にトップモジュール（gmiitest.v）を作成します。内容は先ほどPlatform DesignerでGenerateしたIPをインスタンス化し，各入出力信号をそのまま外部入出力として接続するものです。

```verilog:gmiitest.v
module gmiitest
  (
   output wire [7:0]  hps_0_emac0_phy_txd_o,
   output wire        hps_0_emac0_phy_txen_o,
   output wire        hps_0_emac0_phy_txer_o,
   input wire         hps_0_emac0_phy_rxdv_i,
   input wire         hps_0_emac0_phy_rxer_i,
   input wire [7:0]   hps_0_emac0_phy_rxd_i,
   input wire         hps_0_emac0_phy_col_i, 
   input wire         hps_0_emac0_phy_crs_i, 
   output wire        hps_0_emac0_gmii_mdo_o,
   output wire        hps_0_emac0_gmii_mdo_o_e,
   input wire         hps_0_emac0_gmii_mdi_i,
   output wire        hps_0_emac0_ptp_pps_o, 
   input wire         hps_0_emac0_ptp_aux_ts_trig_i,
   output wire        hps_0_emac0_gtx_clk_clk,
   output wire        hps_0_emac0_md_clk_clk, 
   input wire         hps_0_emac0_rx_clk_in_clk,
   input wire         hps_0_emac0_tx_clk_in_clk,
   input wire         hps_0_emac_ptp_ref_clock_clk,
   input wire         hps_f2h_axi_clock_clk,
   input wire         hps_f2h_sdram0_clock_clk,
   input wire         hps_h2f_axi_clock_clk,
   input wire         hps_h2f_lw_axi_clock_clk,
   output wire [12:0] memory_mem_a,
   output wire [2:0]  memory_mem_ba,
   output wire        memory_mem_ck,
   output wire        memory_mem_ck_n,
   output wire        memory_mem_cke,
   output wire        memory_mem_cs_n,
   output wire        memory_mem_ras_n,
   output wire        memory_mem_cas_n,
   output wire        memory_mem_we_n,
   output wire        memory_mem_reset_n,
   inout wire [7:0]   memory_mem_dq,
   inout wire         memory_mem_dqs,
   inout wire         memory_mem_dqs_n,
   output wire        memory_mem_odt,
   output wire        memory_mem_dm,
   input wire         memory_oct_rzqin
   );

    hps u0 (
        .hps_0_emac0_phy_txd_o         (hps_0_emac0_phy_txd_o),
        .hps_0_emac0_phy_txen_o        (hps_0_emac0_phy_txen_o),
        .hps_0_emac0_phy_txer_o        (hps_0_emac0_phy_txer_o),
        .hps_0_emac0_phy_rxdv_i        (hps_0_emac0_phy_rxdv_i),
        .hps_0_emac0_phy_rxer_i        (hps_0_emac0_phy_rxer_i),
        .hps_0_emac0_phy_rxd_i         (hps_0_emac0_phy_rxd_i),
        .hps_0_emac0_phy_col_i         (hps_0_emac0_phy_col_i),
        .hps_0_emac0_phy_crs_i         (hps_0_emac0_phy_crs_i),
        .hps_0_emac0_gmii_mdo_o        (hps_0_emac0_gmii_mdo_o),
        .hps_0_emac0_gmii_mdo_o_e      (hps_0_emac0_gmii_mdo_o_e),
        .hps_0_emac0_gmii_mdi_i        (hps_0_emac0_gmii_mdi_i),
        .hps_0_emac0_ptp_pps_o         (hps_0_emac0_ptp_pps_o),
        .hps_0_emac0_ptp_aux_ts_trig_i (hps_0_emac0_ptp_aux_ts_trig_i),
        .hps_0_emac0_gtx_clk_clk       (hps_0_emac0_gtx_clk_clk),
        .hps_0_emac0_md_clk_clk        (hps_0_emac0_md_clk_clk),
        .hps_0_emac0_rx_clk_in_clk     (hps_0_emac0_rx_clk_in_clk),
        .hps_0_emac0_tx_clk_in_clk     (hps_0_emac0_tx_clk_in_clk),
        .hps_0_emac_ptp_ref_clock_clk  (hps_0_emac_ptp_ref_clock_clk),
        .hps_f2h_axi_clock_clk         (hps_f2h_axi_clock_clk),
        .hps_f2h_sdram0_clock_clk      (hps_f2h_sdram0_clock_clk),
        .hps_h2f_axi_clock_clk         (hps_h2f_axi_clock_clk),
        .hps_h2f_lw_axi_clock_clk      (hps_h2f_lw_axi_clock_clk),
        .memory_mem_a                  (memory_mem_a),
        .memory_mem_ba                 (memory_mem_ba),
        .memory_mem_ck                 (memory_mem_ck),
        .memory_mem_ck_n               (memory_mem_ck_n),
        .memory_mem_cke                (memory_mem_cke),
        .memory_mem_cs_n               (memory_mem_cs_n),
        .memory_mem_ras_n              (memory_mem_ras_n),
        .memory_mem_cas_n              (memory_mem_cas_n),
        .memory_mem_we_n               (memory_mem_we_n),
        .memory_mem_reset_n            (memory_mem_reset_n),
        .memory_mem_dq                 (memory_mem_dq),
        .memory_mem_dqs                (memory_mem_dqs),
        .memory_mem_dqs_n              (memory_mem_dqs_n),
        .memory_mem_odt                (memory_mem_odt),
        .memory_mem_dm                 (memory_mem_dm),
        .memory_oct_rzqin              (memory_oct_rzqin)
    );
endmodule
```

　Platform Designerで作成したqipファイルとトップモジュールファイルをプロジェクトに追加します。続いてFitterでエラーが出ないよう，Assignment EditorからDDR3 I/Fの設定を適当に行います（本記事末尾の付録参照）。なお，このときFPGA側I/Oの位置は特に指定しません。指定しない場合はFitterがタイミング制約などを元に位置を決定します。設定後，一通りコンパイル（論理合成〜タイミング解析）を行い，エラーが発生しないことを確認します。

　続いてタイミング制約を記述します。その前に，Quartusの設定を変更し，Timing AnalyzerからEMACの内部信号が見えるようにする必要があります（そうしないとTX系のタイミング解析ができない）。
　以下ファイルをプロジェクトファイル（.qpf）を配置しているディレクトリに配置し，Quartusを再起動します。

```ini:quartus.ini
b2t_enable_hps_emac_internal_clock_arcs=on
```

　タイミング制約は以下のように記述します。今回は接続するEthernet PHYとしてKSZ9021GQを想定し，簡単のため基板上の配線遅延，クロックジッタ・スキュー等は無視します。

```sdc:gmiitest.sdc
# GTX Clock
create_generated_clock -name GTXCLK -master_clock emac0_tx_clk \
-source [get_keepers {u0|hps|fpga_interfaces|peripheral_emac0~internal_clock}] \
[get_ports {hps_0_emac0_gtx_clk_clk}]

# Setup/Hold
set_output_delay -clock GTXCLK -max 2.0 \
[get_ports {hps_0_emac0_phy_txd_o[*] hps_0_emac0_phy_txen_o hps_0_emac0_phy_txer_o}]
set_output_delay -clock GTXCLK -min 0.0 \
[get_ports {hps_0_emac0_phy_txd_o[*] hps_0_emac0_phy_txen_o hps_0_emac0_phy_txer_o}]

# RX Clock
create_clock -name RXCLK -period 8.0 [get_ports {hps_0_emac0_rx_clk_in_clk}]
# Setup/Hold
set_input_delay -clock RXCLK -max 5.5 \
[get_ports {hps_0_emac0_phy_rxdv_i hps_0_emac0_phy_rxer_i hps_0_emac0_phy_rxd_i[*]}]
set_input_delay -clock RXCLK -min 0.5 \
[get_ports {hps_0_emac0_phy_rxdv_i hps_0_emac0_phy_rxer_i hps_0_emac0_phy_rxd_i[*]}]

derive_clock_uncertainty
# 注意 #####################################################################
# ここで使用したemac0_tx_clkは，Platform Designerから出力されたsdcに記述されている
# @ hps/synthesis/submodules/hps_hps_fpga_interfaces.sdc
# create_clock -name emac0_tx_clk -period 8.0 \
# [get_keepers {*|fpga_interfaces|peripheral_emac0~internal_clock}] -add
```

# Let's 論理合成
　合成結果がこちらです。TX系は問題ありませんが，RX系でタイミングエラーが出ています。単にハードマクロの出力をFPGA側のI/Oに配線するだけですが，このようにタイミングが収束しないことがあります。
![Screenshot from 2020-09-13 01-08-29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/709057/63434265-126f-1247-db1e-470c95d77a0e.png)

　パスの詳細をTiming Analyzerで見てみましょう。
![Screenshot from 2020-09-19 17-32-49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/709057/ffe104d0-8925-2772-758e-c20d590f3af4.png)
　最初の5.5nsの遅延はEthernet PHY出力の仕様であり，タイミング制約で記載した内容です。このパスの遅延で支配的なのはオレンジ色で示した行であり，入力バッファからFPGA interfaceまでの内部配線（IC：InterConnect）が要因であることがわかります。
　
## 実験その１
　前述のパスにFFを挿入することで内部配線を短くして再度合成をしてみましょう。RX側の信号へFFを挿入するにあたり，当該FFのリセット回路を実装する必要があります。Platform Designerから操作を行い，新たにHPSからEMACのリセット信号をExportし，本信号を使用して実装します。
![Screenshot from 2020-09-19 17-39-21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/709057/0135cdd2-7d7c-1aec-7c53-5a097559f582.png)

　コードの追記・変更内容は以下です。

```verilog:gmiitest.v
  //--- register declaration
  reg                 hps_0_emac0_phy_rxdv_i_r;
  reg                 hps_0_emac0_phy_rxer_i_r;
  reg [7:0]           hps_0_emac0_phy_rxd_i_r;

  //--- wire declaration
  wire                reset_rx_n;
  wire                reset_tx_n;

  //--- FF for timing closure
  always @ (posedge hps_0_emac0_rx_clk_in_clk or negedge reset_rx_n) begin
    if (~reset_rx_n) begin
      hps_0_emac0_phy_rxdv_i_r <= 1'b0;
      hps_0_emac0_phy_rxer_i_r <= 1'b0;
      hps_0_emac0_phy_rxd_i_r  <= 8'b0;
    end else begin
      hps_0_emac0_phy_rxdv_i_r <= hps_0_emac0_phy_rxdv_i;
      hps_0_emac0_phy_rxer_i_r <= hps_0_emac0_phy_rxer_i;
      hps_0_emac0_phy_rxd_i_r  <= hps_0_emac0_phy_rxd_i;
    end
  end

  hps u0 (
          .hps_0_emac0_phy_txd_o         (hps_0_emac0_phy_txd_o),
          //... 省略
          .hps_0_emac0_phy_rxdv_i        (hps_0_emac0_phy_rxdv_i_r),
          .hps_0_emac0_phy_rxer_i        (hps_0_emac0_phy_rxer_i_r),
          .hps_0_emac0_phy_rxd_i         (hps_0_emac0_phy_rxd_i_r),
          //... 省略
          .hps_emac0_tx_reset_reset_n    (reset_tx_n),
          .hps_emac0_rx_reset_reset_n    (reset_rx_n)  // reset for FF
          );
```

　結果がこちらです。
![Screenshot from 2020-09-19 18-05-09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/709057/c083b184-338a-07bf-483d-4b927577f53c.png)
　狙い通りRX系のSetupは改善しましたが，Holdが悪化し，Setup/Hold双方でエラーが出ています。加えてTX系の信号についてもSetup/Holdでエラーが出ています。そう簡単にはいかないようです。
　Timing Analyzerによる確認をしてみましょう。
![Screenshot from 2020-09-19 18-13-50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/709057/88c4465b-e351-cf7b-1c34-84c4bc2a1804.png)
　FF〜FPGA interfaceまでのSetupパスは問題ないようです。省略しますが，Holdパスも問題ありません。一方，入力バッファ〜FFまでのSetupパスはうまく行っていないようです。
　後者のパスをChip PlannerにLocateしてみましょう。
　![Screenshot from 2020-09-19 18-22-17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/709057/f2613e1b-476e-c2cd-95b0-8cfefcbb473f.png)
　青紫の矢印がデータのパスであり，赤紫のパスがクロックのパスです。FFは入力バッファから離れた位置に配置されており，データパスは短くする余地があることがわかります。

## 実験その２
　FF1つではデータパスが長く，タイミング収束しませんでした。そこで，FFの数を2つに増やし，さらなるパスの分割を試みます。追記内容は「実験その１」とほぼ同じであるため，コードは省略します。結果がこちらです。
![Screenshot from 2020-09-19 18-31-46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/709057/c27bd9a9-c922-a68d-7c2e-f23335fa59a8.png)
　ほとんど改善は見られていません。理由は簡単で，1つ目のFF〜2つ目のFFまでの距離がとても短く，入力バッファ〜１つ目のFFまでのパスを短くする役割を果たしていないためでした。確認のため，1つ目のFF〜２つ目のFFまでのパスをChip PlannterにLocateした様子をお見せします。
![Screenshot from 2020-09-19 18-40-45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/709057/ca3cba31-5f32-72d4-2ee9-fa546bd7ab9b.png)
　見づらいですが，画面中央部に赤紫の矢印でデータパスが描かれています。以下が拡大図です。
![Screenshot from 2020-09-19 18-41-02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/709057/6065aab0-54e3-2976-3c65-28c9be0bb4a3.png)
　Logic Lockを使用して，2つのFFの位置をよい塩梅に指定することで解決可能かもしれません。しかしながら，本合成環境はQuartusのLite Editionなので当該機能を使用できません。この記事では本機能を使用せずに解決していきます。
　
　脱線しますが，図らずもTX系のタイミングが収束しています。使用するI/Oの位置によって収束可否が決まるようです。逆説的に考えると，I/Oの位置が先に決まっているプロジェクトなどではTX系のタイミング収束作業が必要となる可能性があります。

## 実験その３
　一般に，入力段のタイミングでエラーが出ている場合，入力バッファのすぐそばに配置されているFFを使用してデータを受けることで改善することがあります。![Screenshot from 2020-09-19 19-02-53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/709057/aeba4e6e-0673-d497-5666-ec79544e5962.png)
　【CycloneV Device Handbook 第５章抜粋：図中，下方に記載がある「Input Register」】
　当該FFを使用するには，Assignment Editorから「Fast Input Register：on」を設定します。
![Screenshot from 2020-09-19 18-58-41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/709057/7efdb376-816e-697e-f773-a72954195b3f.png)
　結果です。
![Screenshot from 2020-09-19 18-57-36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/709057/7cb4b9bf-6fd4-588f-b896-4318ed74fc1a.png)
　無事すべてのパスでタイミングが収束しました。

# 結論
　以上のように，ハードマクロをFPGA側I/Oから使用する場合，考えている以上に煩雑なタイミング収束作業が発生するケースがあります。今回はFPGA側I/Oの設定をデフォルト（2.5V, 12mA, Slew Rate: Fast）かつ位置の制約なし，という条件の下で作業を行いましたが，条件次第で収束作業の難易度が変わります。
　
　作業を可能な限り簡単にするには，以下に気をつけると良いと思います。

+ FPGA側I/Oを使用する場合，早期にI/O規格を決定し，タイミング収束性を検証すること
+ 収束した結果を用いてI/Oピンの位置を検討すること
+ 必要に応じてタイミング収束を実現する配置配線結果をLogic Lock等で固定し，開発を進めること

# 付録：Assignment Editor設定
　"To"フィールドの[\*]は適宜展開してください。

| To | Assignment Name | Value |
|:-:|:-:|:-:|
| memory_mem_a[*] | I/O Standard | SSTL-15 Class I |
| memory_mem_ba[*] | I/O Standard | SSTL-15 Class I |
| memory_mem_cas_n | I/O Standard | SSTL-15 Class I |
| memory_mem_odt | I/O Standard | SSTL-15 Class I |
| memory_mem_ras_n | I/O Standard | SSTL-15 Class I | 
| memory_mem_reset_n | I/O Standard | SSTL-15 Class I |
| memory_mem_we_n | I/O Standard | SSTL-15 Class I |
| memory_oct_rzqin | I/O Standard | SSTL-15 Class I |
| memory_mem_cke | I/O Standard | SSTL-15 Class I |
| memory_mem_cs_n | I/O Standard | SSTL-15 Class I |
| memory_mem_dm | I/O Standard | SSTL-15 Class I
| memory_mem_dm | Output Termination | Series 50 Ohm with Calibration
| memory_mem_dq[*] | I/O Standard | SSTL-15 Class I |
| memory_mem_dq[*] | Input Termination | Parallel 50 Ohm with Calibration |
| memory_mem_dq[*] | Output Termination | Series 50 Ohm with Calibration |
| memory_mem_dqs | I/O Standard | SSTL-15 Class I |
| memory_mem_dqs | Input Termination | Parallel 50 Ohm with Calibration |
| memory_mem_dqs | Output Termination | Series 50 Ohm with Calibration |
| memory_mem_dqs_n | I/O Standard | SSTL-15 Class I |
| memory_mem_dqs_n | Input Termination | Parallel 50 Ohm with Calibration |
| memory_mem_dqs_n | Output Termination | Series 50 Ohm with Calibration |
| memory_mem_dqs | I/O Standard | Differential 1.5-V SSTL Class I |
| memory_mem_dqs | Input Termination | Parallel 50 Ohm with Calibration |
| memory_mem_dqs | Output Termination | Series 50 Ohm with Calibration |
| memory_mem_dqs_n | I/O Standard | Differential 1.5-V SSTL Class I |
| memory_mem_dqs_n | Input Termination | Parallel 50 Ohm with Calibration |
| memory_mem_dqs_n | Output Termination | Series 50 Ohm with Calibration |
| memory_mem_ck | I/O Standard | Differential 1.5-V SSTL Class I |
| memory_mem_ck | Output Termination | Series 50 Ohm <strong>without</strong> Calibration |
| memory_mem_ck_n | I/O Standard | Differential 1.5-V SSTL Class I |
| memory_mem_ck_n | Output Termination | Series 50 Ohm <strong>without</strong> Calibration |