はじめに
これまでの連載では、RP2040とFPGAの間の通信にSPIを使ってきました。
SPI通信は扱いやすく、Shrike-Liteにも最初から利用できる仕組みが用意されています。
連載記事の第15回では、MicroPython側の送受信バッファを再利用し、複数byteをまとめて転送しました。
さらにSPIクロックを4MHzまで上げ、現在の構成で安定して利用できる範囲まで通信帯域を広げました。
Shrike-LiteでAtCoder問題を解く(15):RP2040側でSPI転送を高速化する
しかし、ABC469Cのように、RP2040とFPGAの間で大量のデータを受け渡す問題では、まだSPI転送がボトルネックになる可能性があります。
そこで今回は、SPIとは別の通信方式として、4bitパラレル転送を実装してみます。
利用するのは、Shrike-Lite基板上でRP2040とFPGAを接続している4bitの信号線です。
いつも通り、ジャンパ線や外付け回路は使いません。
4bitなら速いのか
SPIは、基本的にデータを1bitずつ転送します。
これに対して4bitパラレル通信なら、1回の転送で4bitを送れます。
同じ速さで信号を切り替えられるなら、単純にはSPIより多くのデータを転送できそうです。
しかし、実際の通信速度は配線の本数だけでは決まりません。
RP2040には内蔵のSPI通信用ハードウェアが組み込まれています。
MicroPythonにも、このSPIハードウェアを利用するためのAPIがあります。
SPIでは、専用ハードウェアとMicroPython APIをそのまま利用するだけで、FIFOやバースト転送を含む最適化済みの通信機構が働きます。
4MHzというクロックだけを見る以上に、初めから得られる実効性能は高いのです。
これに対して、今回の4bitパラレル通信はほぼイチからの自作です。
データ線を4本用意しただけでは通信できません。
通信開始の合図、クロック生成、通信方向の切り替え、byteと4bitデータの変換なども、自分で用意する必要があります。
4bitパラレル通信というアイデアは成立しそうですが、本当に4MHz SPIを超える通信帯域を確保できるかは、実際に作って測定するまで分かりません。
4本のDATA線を交互に使う
Shrike-Liteには、RP2040とFPGAを接続する4bitの信号線があります。
これを送信用と受信用に2本ずつ分けると、一方向あたり2bitしか使えません。
そこで、同じ4本のDATA線を送信と受信で交互に使う半二重通信にしました。
主な信号は次の三つです。
| 信号 | 役割 |
|---|---|
| DATA | 4bitのデータを転送する |
| REQ | RP2040から通信開始を要求する |
| CLK | 4bitデータを受け渡すタイミングを示す |
最初にRP2040がREQを送ります。
その後、DATA線をRP2040からFPGAへの送信に使います。
要求データの送信が終わるとDATA線の方向を切り替え、今度はFPGAからRP2040へ応答を返します。
同じDATA線を双方が同時に駆動すると問題になるため、方向切り替えの間には、どちらもDATA線を駆動しない期間を設けます。
詳細なクロック波形や各信号の切り替え条件については、今回は省略します。
重要なのは、
4本のDATA線を、要求と応答で交互に使う
という基本方針です。
RP2040のPIOを使う
MicroPythonからGPIOを直接操作する方法でも、通信そのものは実現できます。
ただし、Pythonコードで、
- DATA線へ値を設定する
- CLKを切り替える
- 次の4bitを用意する
- 再びCLKを切り替える
という処理を繰り返すと、Python側の処理時間が大きくなります。
これでは、せっかくDATA線を4本にしても、十分な通信帯域を得られません。
そこで利用するのが、RP2040のPIOです。
PIOは、小さな専用プログラムによってGPIOを自動制御できる仕組みです。
CPUがGPIOを1回ずつ操作しなくても、PIO State Machineが一定のタイミングでDATA線やCLKを切り替えられます。
さらに、PIOのFIFOとメモリの間のデータ転送にはDMAを利用できます。
役割分担は、おおまかに次のようになります。
| 処理 | 担当 |
|---|---|
| packetの準備と通信開始 | MicroPython |
| DATAとCLKの制御 | PIO |
| メモリとPIO FIFO間の転送 | DMA |
| データ受信と応答生成 | FPGA |
SPIとは違い、RP2040には、4bitパラレル通信専用の周辺回路があるわけではありません。
しかし、PIOとDMAという汎用ハードウェアを組み合わせれば、自作の通信エンジンとして利用できそうです。
実装Phase 1からPhase 4まで
最初からPIOとDMAを組み合わせたわけではありません。
通信方式を確認しながら、少しずつ構成を変更しました。
| Phase | 試したこと | 確認したこと |
|---|---|---|
| Phase 1 | MicroPythonからGPIOを直接操作 | 4bit半二重通信の基本動作 |
| Phase 2 | 複数byteのburst転送 | packet単位の連続送受信 |
| Phase 3 | PIOを使った信号制御 | GPIO操作をハードウェアへ移せること |
| Phase 4 | PIOとDMAを組み合わせる | MicroPythonから連続通信できること |
それぞれのコードはこの記事には掲載しません。
興味がある方は以下を覗いてみてください。
Phase 1:まずはGPIOで動かす
最初は、MicroPythonからGPIOを直接操作しました。
速度よりも、REQ、CLK、DATAによる半二重通信そのものが成立するかを確認するためです。
FPGAへデータを送り、FPGA側で処理した結果を同じ4本のDATA線から受け取ります。
通信方向の切り替えや、双方がDATA線を駆動しない時間も、この段階で確認しました。
Phase 2:複数byteをまとめて送る
次に、1byteだけではなく、複数byteを連続して送受信できるようにしました。
packetの内容をFPGA側のメモリへ保存し、その後、結果をまとめて返します。
32byteのpacketを1000回繰り返す試験も行い、連続通信できることを確認しました。
Phase 3:PIOへ移す
GPIOをMicroPythonから直接操作する構成では、Python側の処理が通信速度を制限します。
そこで、DATA線とCLKの制御をPIOへ移しました。
RP2040のCPUはpacketの準備と開始を担当し、実際の信号生成はPIOへ任せます。
この段階で、RP2040側のPIOとFPGA側の内部クロックが異なることも考慮する必要がありました。
Phase 4:DMAを組み合わせる
最後に、PIOのFIFOとメモリ間の転送へDMAを使いました。
MicroPythonから1個ずつデータをPIOへ渡すのではなく、送信データをメモリへ準備し、その後の転送をDMAへ任せます。
受信側も同様に、PIOが取り込んだデータをDMAでメモリへ保存します。
これにより、
- MicroPython
- PIO
- DMA
- FPGA
を組み合わせた4bit半二重通信が実機で動作しました。
速度も測ってみる
各Phaseでは、動作確認だけでなく処理時間も測定しました。
32byteのpacketを1000回送受信した結果は、次のようになりました。
| Phase | 構成 | 処理時間 |
|---|---|---|
| Phase 2 | GPIOによるburst転送 | 約28.3秒 |
| Phase 3 | PIO+MicroPythonによるFIFO操作 | 約12.3秒 |
| Phase 4 | PIO+DMA | 約12.7秒 |
GPIO操作をPIOへ移したPhase 3では、Phase 2の約2.3倍まで高速化しました。
しかし、パラレル通信用クロックを100kHzから4MHzまで上げても、通信全体の処理時間はほとんど変わりませんでした。
PIOが信号を高速に生成できても、MicroPythonからPIOのFIFOへデータを出し入れする処理がボトルネックになっていたためです。
そこでPhase 4では、PIOのFIFOとメモリ間の転送をDMAへ任せました。
その結果、DMAが実際にデータを転送している区間だけを見ると、約9.19Mbpsまで高速化できました。
参考として、これまで使用してきた4MHz SPIは、1byteあたり約2.5µs、通信速度に換算すると約3.2Mbpsです。
測定条件は同じではありませんが、PIO+DMAによる通信区間だけなら、4MHz SPIの約3倍に相当する速度が出ています。
ところが、256byteのpacketを1000回処理した通信全体では、約66.8kbpsに留まりました。これは4MHz SPIのおよそ48分の1です。
DMAによる実データ転送が占める時間は通信全体の1%未満で、残りの99%以上が、packetの準備、byteと4bitデータの変換、DMAの設定、応答の確認などに使われていました。
つまり、4bitの通信路そのものは十分に高速化できましたが、その前後に残っているMicroPythonの処理が、通信全体の性能をほぼ決めていたのです。
MicroPython版が完成した
Phase 4では、MicroPythonからPIOとDMAを設定し、4bitパラレル通信を実行しています。
この時点で、目標としていた通信方式そのものは完成しました。
Shrike-Liteの既設配線だけを使い、RP2040からFPGAへpacketを送り、FPGAから応答を受け取れます。
複数packetの連続試験も通りました。
しかし、MicroPython版のままでは、4MHz SPIと勝負できる通信速度にはまったく届きませんでした。
一方で、PIOとDMAによる通信区間の速度測定では、4bitパラレル通信に十分なポテンシャルがあることも確認できました。
次に行うべきことは、MicroPython側に残っている処理を減らすことです。
本当の勝負はここからです。
C SDKへ移れば素直だが
PIOとDMAを本格的に利用するなら、RP2040のC SDKで専用アプリケーションを作る方法があります。
CからPIOやDMAを直接制御すれば、MicroPythonよりも細かい処理を効率よく実装できます。
しかし、この連載では最初に、
Shrike-LiteはThonnyとMicroPythonから操作する
という縛りルールを決めています。
ここでC SDKの専用アプリケーションへ移行すると、このルールから外れてしまいます。
自分で決めたルールですが、守ることにします。
そこで、ユーザーから見える操作方法はMicroPythonのまま残し、高速化が必要な部分だけをCへ移します。
Cで作成した処理は、MicroPythonのUser C Moduleとしてfirmwareへ組み込みます。
そのfirmwareをUF2としてShrike-Liteへ書き込み、Thonnyから通常のMicroPython moduleとして呼び出します。
つまり、
C SDKのアプリケーションへ置き換えるのではなく、MicroPythonの内部へCで書いた専用通信エンジンを追加する
という方針です。
これなら、ThonnyとMicroPythonから操作するという連載のルールを維持できます。
今回のまとめ
今回は、Shrike-Lite基板上の4bit配線を使った半二重通信を設計し、PIOとDMAを組み合わせたMicroPython版まで実装しました。
PIOとDMAによる通信区間は高速化できましたが、MicroPython側の処理を含む通信全体では、まだ4MHz SPIにまったく歯が立ちません。
一方で、ボトルネックが4bitの信号線ではなく、その前後のMicroPython処理にあることも分かりました。
今後、これらの処理を段階的にCへ移し、最終的にSPIを超えられるか確認します。
次回
次回は、公開されているMicroPythonのソースコードから、Shrike-Lite用firmwareを再構築します。
clean環境でfirmwareをbuildし、User C Moduleを追加します。
その後、データのpack/unpackとDMA処理を段階的にCへ移し、MicroPython版との性能差を確認します。
4bitパラレル通信は、本当に4MHz SPIを超えられるのでしょうか。
お楽しみに。
前回:
Shrike-LiteでAtCoder問題を解く(26):Interlude - ABC469の各問題をFPGA目線で見てみる
次回:
Shrike-LiteでAtCoder問題を解く(28):4bitパラレル通信を作る(2) - Cで高速化する
今回の全コード:
第27回コード全文