はじめに
前回は、ABC468B「Corridor Watch」の監視状態をForgeFPGAの内蔵BRAMへ置きました。
Shrike-LiteでAtCoder問題を解く(23):ABC468B③ - BRAMに配列を置く
BRAM版は合成・配置と実機試験には成功しましたが、50MHz制約ではTiming違反が残りました。
達成可能周波数は48.202MHzで、50MHzへわずかに届いていません。
今回はこのTiming違反を解消してみます。
回路を変更せず、動作クロックを見直す
タイミング違反の解決は第18回の記事でも扱っています。
Shrike-LiteでAtCoder問題を解く(18):ABC467C - Adjacent Sums (easy)(タイミング違反解消編・後編)
このときは、長い組み合わせ回路へレジスタを追加して、達成可能周波数を向上させました。
しかし、レジスタ追加は、動作タイミングの見直しなどに、結構な手間がかかります。
今回の回路の達成可能周波数は48.202MHzです。
50MHzへあと少し届かないだけなので、回路を複雑にするより、実際の回路に合ったクロックへ下げる方が素直だと考えました。
そこで今回は、ForgeFPGAに内蔵されているPLLを使い、内部クロックを40MHzへ下げてみます。
PLLとは
PLLは、基準となる1つのクロックから、別の周波数のクロックを作るための回路です。
FPGAでは、基板上のクロックや内蔵OSCを入力として、用途に合わせた周波数のクロックを作るために使われます。
たとえば、50MHzのクロックをもとに、40MHzなどの異なる周波数のクロックを作ることができます。
今回使用するForgeFPGAのPLLでは、入力クロックに対する分周値と逓倍値を設定し、その組み合わせから目的の出力周波数を作ります。
PLLの公式サンプルを確認する
PLLは初めて使うため、最初にRenesasのアプリケーションノートとShrikeのサンプルを確認しました。
ForgeFPGAでのPLL自体の設定は、それほど難しくなさそうです。
完成したコードサンプルは見つかりましたが、PLLを既存のプロジェクトへ組み込む手順は、複数の資料や画面に断片化されています。
何を変更する必要があるのか、それらをどの順番で進めればよいのかをまとめた説明は見当たりません。
実際に組み込むには、PLL Configurator、Topモジュール、I/O Planner、Timing制約をそれぞれ変更する必要がありそうです。
そこで今回は、実際に作業しながら組み込み手順を確認してみます。
ForgeFPGA WorkshopでPLLを設定する
まずはメイン画面でPLLブロックをクリックして、PLLのプロパティを確認します。
Asynchronouslyに設定して、FPGA Editor画面に移動します。
PLL Configuratorで40MHzを設定する
FPGA Editor画面の上部メニューバーからPLL Configuratorを開きます。
開いたPLL Configurator画面のPLL Calculatorタブで、Required Output Frequencyの値を40MHzに設定します。
すると、PLL Configuratorが逓倍比や分周比を自動計算してくれます。
今回は素直にこの設定をそのまま利用することにしましょう。
Configuratorが自動生成するVerilogテンプレートを確認する
Verilog PLL Configタブで、設定に合わせて自動生成したVerilogテンプレートを見ることができます。
ただし、ユーザーが記述したmain.vは自動変更されません。
表示されたテンプレートを手動でmain.vに挿入してご利用ください、という形のようです。
I/O PlannerにPLLポートを反映する
さて、設定確認が済んだら、PLL Calculatorタブに戻ってApplyを押してください。
すると、先ほどテンプレートに記述されていたPLL設定端子とTopモジュールのポート名がI/O Plannerへ自動反映されます。
PLL設定端子とI/O Plannerの対応が自動で反映されました。
PLL Configuratorでの作業はここまでです。
ここまでのまとめ
PLL Configuratorでの作業が一段落しましたので、ここで作業手順をまとめておきます。
PLL Configuratorで済ませた作業
├─ PLLの設定値を計算する
├─ Verilogのテンプレートを表示する
└─ I/O PlannerのPLL設定端子を更新する
ここから利用者側で実施する作業
├─ テンプレートをTopモジュールへ追加する
├─ ForgeFPGA用の属性を追加する
├─ PLL出力クロックの接続先を変更する
├─ Timing制約を変更する
└─ (MicroPython) SPI通信速度を変更する
ここからはABC468B BRAM版プロジェクト設定を順次変更していきます。
TopモジュールへPLL設定端子を追加する
PLL Configuratorが自動生成したテンプレートを、ABC468B BRAM版のTopモジュールであるmain.vへ追加します。
ただし、このテンプレートには、ForgeFPGAの外部モジュール接続に必要なiopad_external_pin属性が付いていません。
そこで、公式サンプルに合わせて属性を追加しました。
// PLL設定端子
(* iopad_external_pin *) output pll_en,
(* iopad_external_pin *) output [5:0] pll_refdiv,
(* iopad_external_pin *) output [11:0] pll_fbdiv,
(* iopad_external_pin *) output [2:0] pll_postdiv1,
(* iopad_external_pin *) output [2:0] pll_postdiv2,
(* iopad_external_pin *) output pll_bypass,
(* iopad_external_pin *) output pll_clk_selection
続いて、Configuratorが計算した40MHz用の設定値を追加します。
// PLL設定
assign pll_en = 1'b1;
assign pll_refdiv = 6'b00_0001; // 1
assign pll_fbdiv = 12'b0000_0001_1000; // 24
assign pll_postdiv1 = 3'b110; // 6
assign pll_postdiv2 = 3'b101; // 5
assign pll_bypass = 1'b0;
assign pll_clk_selection = 1'b0;
内蔵OSCはPLLの入力として使用するため、既存の出力有効設定をそのまま残します。
assign clk_en = 1'b1;
I/O Plannerでクロックの接続先を変更する
さて、main.vへPLL設定端子と40MHz用の設定値を追加しました。
しかし、まだmain.vのクロック入力clkはOSC_CLK、つまり内蔵50MHzクロックに接続されています。
これを解除し、PLL出力のPLL_CLKへ付け替えます。
Topモジュール内のクロック信号名は、これまでと同じclkです。
そのため、既存のFSMやBRAMアクセス回路のクロック信号名を変更する必要はありません。
I/O Plannerの接続変更だけで、クロックのソースだけを変更できました。
Timing制約を25nsへ変更する
内部クロックを40MHzへ変更したため、Timing Constraintファイル記述も20ns(50MHz)から25ns(40MHz)へ変更します。
create_clock -name clk {clk} -period 25.000
SPIクロックを3MHzへ下げる
FPGA側の動作クロックを50MHzから40MHzへ下げると、1byteを受信する間に使える内部クロック数が減ります。
FPGAクロック 50MHz / 4MHz SPI:1byteあたり100クロック
FPGAクロック 40MHz / 4MHz SPI:1byteあたり 80クロック
そこで、MicroPython側のSPIクロックも3MHzへ下げました。
SPI_BAUDRATE = 3_000_000
40MHz、3MHz SPIでは、1byteあたり約106.7クロックになります。
40MHz × 8bit ÷ 3MHz ≒ 106.7クロック
変更前の100クロックより、少し多い処理サイクルを確保しておくことにします。
ここまででFPGAクロックを40MHzに変更する作業は完了です。
完成したコード全文は以下にあります。
次は、いつもの通り、合成、Bitstream生成、実機試験へと進みます。
Timing Analysisを確認する
40MHz、25ns制約でBitstreamを生成し、Timing Analysisを確認しました。
結果は次のとおりです。
| 項目 | 50MHz OSC版 | 40MHz PLL版 |
|---|---|---|
| Constrained Period | 20.000ns | 25.000ns |
| Achievable Period | 20.746ns | 21.568ns |
| Achievable Frequency | 48.202MHz | 46.365MHz |
| WNS | -0.747ns | +3.431ns |
| TNS | -12.817ns | 0ns |
| TNS Endpoints | 25 | 0 |
40MHz制約に対して、WNSは+3.431nsとなりました。
これでTiming違反は解消です。
再合成の結果、Achievable Frequency自体は48.202MHzから46.365MHzへ少し下がっています。
PLLによってクロックを回路の実力に合った値へ下げたことで、Timing制約を満たしました。
CLB使用率も下がった
Resource Reportを比較すると、CLB使用量も減っていました。
| 項目 | 50MHz版 | 40MHz+PLL版 |
|---|---|---|
| CLB LUT5s | 572 / 1120(51.07%) | 549 / 1120(49.02%) |
| CLBs | 108 / 140(77.14%) | 102 / 140(72.86%) |
| FFs | 180 | 180 |
| CLB FFs | 150 | 150 |
| PLL | 0 | 1 |
| BRAM | 1 | 1 |
PLLを1個使用しましたが、汎用ロジック側ではLUTが23個、CLBが6個減っています。
40MHz制約ではPnR時に速度を最優先にする必要がなくなり、ForgeFPGA Workshopが面積効率のよい論理最適化と配置を選べるようになったものと考えられます。
50MHz版より達成可能周波数は少し下がりましたが、40MHz動作には十分です。
制約に応じて最適化の方向を変えているようで、ForgeFPGA Workshopもなかなかよい仕事をしています。
実機試験
生成したBitstreamをShrike-Liteへ書き込み、40MHz内部クロック、3MHz SPIで実機試験を行いました。
CONFIG BITSTREAM=abc468b_BRAM_40MHz.bin
SPI_BAUDRATE=3000000
MAX_REPLY_POLLS=16
公式サンプル、左右端、範囲の重複、M=100、D=99、最大負荷など11ケースを実行し、すべてPASSしました。
SUMMARY PASS=11 FAIL=0 RESULT=PASS
全ケースで、RESET ACK、START ACK、データ返信形式、回答VALIDを確認しています。
SPIクロックを下げても時間差は小さい
SPIクロックは4MHzから3MHzへ下がりました。
1byteの純粋な転送時間は次のようになります。
4MHz:8bit ÷ 4MHz = 2.000µs
3MHz:8bit ÷ 3MHz = 2.667µs
差 = 0.667µs
ABC468Bで送る文字列は最大100byteですから、主データの転送時間増分は、0.667µs x 100byte = 約66.7µsです。
主データ部分では70µs以下の差なので、MicroPython上で測定した全体時間では大きな違いになりません。
実際、最大負荷ケースは次の結果でした。
50MHz+4MHz SPI:約7.8ms
40MHz+3MHz SPI:約7.9ms
単発測定には数百µs程度のばらつきが見られたため、細かな差を厳密には比較していません。
Timing違反を解消しながら、実用上の処理時間はほぼ維持できました。
ForgeFPGA WorkshopでPLLを使うときの作法
今回、既存回路へPLLを追加する作業は、次の順番で進めました。
- メイン画面でPLLブロックを選択し、
Enable user clock by PLLをAsynchronouslyに設定する - FPGA EditorからPLL Configuratorを開き、入力50MHz、出力40MHzを設定する
-
Verilog PLL Configタブで、40MHz用のテンプレートと設定値を確認する - PLL Calculatorへ戻って
Applyし、PLL設定端子をI/O Plannerへ反映する - 自動生成テンプレートをTopモジュールへ手作業で追加する
- PLL関連ポートへ
iopad_external_pin属性を追加する - I/O Plannerで
OSC_CLK → clkを解除し、PLL_CLK → clkへ付け替える - Timing制約を20nsから25nsへ変更する
- MicroPython側のSPIクロックを4MHzから3MHzへ変更する
PLL Configuratorが自動で行うのは、分周値と逓倍値の計算、Verilogテンプレートの表示、I/O PlannerへのPLL設定端子の反映までです。
その後は、Topモジュール、I/O Planner、Timing制約を手作業で変更する必要があります。
PLLの設定自体は難しくありませんでしたが、工程数は多めです。今回、手順をまとめたので、次回からは迷わず進められそうです。
今回のまとめ
今回は、ForgeFPGAのPLLを使って、ABC468B BRAM版の内部クロックを50MHzから40MHzへ下げました。
PLL Configuratorで設定値を作成し、Topモジュールへの設定追加、I/O Plannerでのクロック接続変更、Timing制約の更新を行いました。あわせてSPIクロックも4MHzから3MHzへ変更しています。
その結果、WNSは-0.747nsから+3.431ns、TNSは-12.817nsから0nsとなり、Timing違反を解消できました。
CLB使用率も77.14%から72.86%へ下がり、実機試験は11ケースすべてPASSしました。
50MHzへわずかに届かない回路では、回路を変更して高速化するだけでなく、必要な動作周波数を見直す方法も有効ですね。
これでABC468Bの実装は完了です。
次回
次回は、ABC468Cへ進みます。
C++ / Pythonのように全探索を行うわけにはいきませんので、FPGAに向いた計算方法を模索します。
お楽しみに。
前回:
Shrike-LiteでAtCoder問題を解く(23):ABC468B③ - BRAMに配列を置く
次回:
Shrike-LiteでAtCoder問題を解く(25):ABC468C - Between P and Q(桁DPで加算器を使い回す)
今回のコード全文:
第24回コード全文







