このブログは、株式会社フィックスターズのエンジニアが、あらゆるテーマについて自由に書いているブログです。
※2020/07/13更新 近日公開としていた10G Ethernet MACのソースコードを公開&追記しました。
Xilinxのデータセンター向けFPGAボードである Alveo U50には、SFP-DDスロット(ES版)もしくはQSFPスロット(Production版)が搭載されており、Ethernetモジュールを接続することにより、他のデバイスとEthernetで通信することができます。
これらのSFP系スロットを使って10G Ethernetの通信を行うには、10G Ethernetの処理を行う論理回路が必要となります。
10G Ethernetの通信処理を行うIPとして、10G/25G Ethernet Subsystem がVivadoに同梱されていますが、合成して使用するには別途ライセンス購入が必要となります。
実際には、10G/25G Ethernet Subsystemは、PCS/PMAと呼ばれる部分と、MACと呼ばれる部分に分かれており、PCS/PMA部分は無償、MAC部分が有償となっています。
調べてみたところ、MACの処理自体は基本的な処理のみであれば、そんなに複雑ではないことがわかりましたので、Alveo U50で動くものを実装して、PCに接続した10G Ethernet NICとの通信ができるかどうか試してみました。
通信を確認する際に用いた、FPGA内部とその周辺のブロック図を図1に示します。
前述の通り、Xilinxの10G Ethernet Subsystemは、PCS/PMAと呼ばれる64b66b変換を行う部分、および66:1のSERDES部分は無償で使用できますので、PCS/PMAの部分のみ使用しています。
SFP-DDスロットに挿入された10G Ethernet CableとPMAは、GTHトランシーバのレーン1つと接続されています。
今回作成した10G MAC部分がxgmac IP (xgmac)です。10G Ethernet SubsystemのPCSとは、XGMIIというインターフェース規格に基づいてEthernetフレームの内容をやりとりします。また、10G Ethernetを使うユーザー・ロジックとは、イーサネット経由で送受信するヘッダ及びペイロードをAXI4 Stream経由でやりとりします。
10G EthernetにおけるPCSとMACの間のインターフェースはXGMII (10G Media Independent Interface)として規格化されており、Xilinxの10G Ethernet Subsystem内のPCSとMACのインターフェースもXGMIIとなっています。
XGMII自体は、データ32bit+制御信号4bit@312.5[MHz]で通信する規格ですが、XilinxのPCSでは、バス幅を倍(データ64bit+制御信号8bit)にするかわりに周波数を半分(156.25[MHz])にした64bit PCSとなっており、動作周波数的に扱いやすくしています。
XGMIIでは、データをオクテット(8bit)単位の塊とそれに対応する制御ビットのペアとして扱います。例えば、データの下位8bit([7:0])と制御信号の0bit目がペアとなります。対応する制御信号のビットが0のデータは、そのままデータとして扱います。対応する制御信号のビットが1のデータは、XGMIIの制御信号として扱います。
XGMIIの制御信号はいくつかありますが、最低限扱う必要があるものは次の通りです。
データの値 | 名称 | 意味 |
---|---|---|
8’h07 | IDLE | 何もない |
8’hfb | START | フレーム開始 |
8’hfd | TERMINATE | フレーム正常終了 |
8’hfe | ERROR | フレーム異常 |
XGMIIではEthernetのフレームをSTARTからTERMINATEで囲まれた区間で表します。 また、START は必ずD[7:0]、つまり0番目のオクテットに現れることが規定されています。ただし、これは32bit PCSの場合で、64bit PCSの場合は0番目及び4番目のオクテットにSTARTが現れることに注意が必要です。
Ethernetフレーム送受信中に何かしら異常が発生した場合は、TERMINATEではなく ERROR でフレームを終了します。
以上より、XGMII経由でEthernetフレームを受信するには、Startを検出して、TerminateかErrorが来るまで間のデータを読み出しつづければよいことがわかります。ただし、前述の通りStartの位置が0オクテット目か4オクテット目か変化するので、どちらなのか検出してその後のデータのずらし方を変えます。
逆にXGMII経由でデータを送信するには、Ethernetフレームの前にStart、Ethernetフレームの後にTerminateもしくはErrorと、パディングとしてIdleを付加すればよいことがわかります。
以上より、XGMIIでEthernetフレームをやり取りする処理は意外と単純であることがわかります。
Xilinxの10G/25G Ethernet Subsystemが提供するMAC IPは、入出力としてEthernetフレームのうちヘッダとペイロードの部分のみをユーザーロジックとの間でやりとりします。よって、Ethernetフレームのヘッダとペイロードを除く、プリアンブル(Preamble)、SOF、FCSはMAC IP側で処理する必要があります。
プリアンブルは値が8'h55
のデータが5~7オクテット続いたものです。通信経路上である程度欠損することが許されているため、5~7オクテットの長さになる可能性があります。
SFD(Start Frame Delimiter)は、フレームの開始を表すマーカーで、その値は8'hd5
となります。
SFD以降はEthernetヘッダ (送信元MACアドレス+送信先MACアドレス+Ethernetフレームタイプ) が14オクテット、ペイロードがMTU(Max Transfer Unit, 大抵1500)オクテットが続きます。
最後にFCS(Frame Check Sequence)と呼ばれるCRC32によるヘッダとペイロードのチェックサム が付加されます。
よって、Ethernetフレームの受信を行うには以下の手順が必要です。
逆に、Ethernetフレームの送信を行うには、以下の手順が必要です。
以上より、Ethernetフレームに関する処理もそんなに複雑なことは行わないでよいことがわかります。
ここまでで、10G Ethernet MACに必要な処理が出そろいました。そこで、以下の通り10G Ethernet MACの受信処理と送信処理の処理ブロックをそれぞれ設計しました。
10G Ethernet MACの受信処理は以下のブロックから成ります。
10G Ethernet MACの送信処理は以下のブロックから成ります。
前述の送受信処理を行う各ブロックをRTLで記述し、送受信処理を1つの 10G Ethernet MAC という名称のIPとしてまとめました。
XGMIIおよびAXI Streamによる入出力インターフェースは、Xilinxの10G/25G Ethernet Subsystemと互換性があるようにしてあります。
Xilinxの10G/25G Ethernet Subsystem (PCS/PMAのみ)との接続は次の通りです。
hier_mac_0, hier_mac_1内部に10G Ethernet MACのインスタンスがそれぞれ1つづつ含まれており、前述の10G Ethernet MACのインターフェースと同名のポートがHierarchyのポートとして外に出ています。ポートの接続関係は次の通りです。
10G/25G Ethernet Subsystemのポート | 10G Ethernet MACのポート | 信号の内容 |
---|---|---|
mii_rx_{0,1} | rx_xgmii | 受信ブロックへのXGMII信号 |
mii_tx_{0,1} | tx_xgmii | 送信ブロックからのXGMII信号 |
tx_mii_clk_{0,1} | rx_clock, tx_clock | 10G/25G Ethernet Subsystemの送受信クロック |
user_rx_reset_{0,1} | rx_reset | 受信ブロックのリセット信号 |
user_tx_reset_{0,1} | tx_reset | 送信ブロックのリセット信号 |
10G/25G Ethernet SubsystemはPCS only (64bit)の設定としています。
10G Ethernet MAC IPをAlveo U50上で操作確認するため、まずは単純に受信したEthernetフレームを送信元にそのまま返すデザインを作成しました。
ループバックのデザインでは、単純に10G Ethernet MACのrx_maxisとtx_saxis をAXI Stream Data FIFO経由で接続しています。これで受信したEthernetフレームの内容がそのまま送信されます。
上記デザインを合成します。
また、Alveo U50 (ES3)のSFP-DDスロットに、10G EthernetのSFP+ケーブルを挿入し、ケーブルの反対側を同じPCに接続した 10G Ethernet NICのSFP+スロットに接続します。
ホスト側の10G Ethernet NICとケーブルの型式は以下の通りです。
まずはデザインを書き込む前の応答を確認します。
ホストPC側の10G Ethernet NICに対応するインターフェースに固定でIP(今回は192.168.1.1/255.255.255.0)を割り当てます。その後、別コンソールでtcpdumpを実行してから、元のコンソールでpingを192.168.1.100当てに送ります。
$ sudo ifconfig enp3s0f1 192.168.1.1 netmask 255.255.255.0
$ ping -I enp3s0f1 192.168.1.100
PING 192.168.1.100 (192.168.1.100) from 192.168.1.1 enp3s0f1: 56(84) bytes of data.
$ sudo tcpdump -i enp3s0f1 -nxe
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on enp3s0f1, link-type EN10MB (Ethernet), capture size 262144 bytes
06:18:32.745340 00:1b:21:bf:31:32 > ff:ff:ff:ff:ff:ff, ethertype ARP (0x0806), length 42: Request who-has 192.168.1.100 tell 192.168.1.1, length 28
0x0000: 0001 0800 0604 0001 001b 21bf 3132 c0a8
0x0010: 0101 0000 0000 0000 c0a8 0164
06:18:33.769484 00:1b:21:bf:31:32 > ff:ff:ff:ff:ff:ff, ethertype ARP (0x0806), length 42: Request who-has 192.168.1.100 tell 192.168.1.1, length 28
0x0000: 0001 0800 0604 0001 001b 21bf 3132 c0a8
0x0010: 0101 0000 0000 0000 c0a8 0164
送信先のMACアドレスがわからないのでARPリクエストが送信されています。
次に、Alveo U50に合成したビットストリームを書き込んで再度同じ操作を行います。
08:50:43.881654 00:1b:21:bf:31:32 > ff:ff:ff:ff:ff:ff, ethertype ARP (0x0806), length 42: Request who-has 192.168.1.100 tell 192.168.1.1, length 28
0x0000: 0001 0800 0604 0001 001b 21bf 3132 c0a8
0x0010: 0101 0000 0000 0000 c0a8 0164
08:50:43.881735 00:1b:21:bf:31:32 > ff:ff:ff:ff:ff:ff, ethertype ARP (0x0806), length 60: Request who-has 192.168.1.100 tell 192.168.1.1, length 46
0x0000: 0001 0800 0604 0001 001b 21bf 3132 c0a8
0x0010: 0101 0000 0000 0000 c0a8 0164 0000 0000
0x0020: 0000 0000 0000 0000 0000 0000 0000
ビットストリームを書き込む前と同様、ARPリクエストを送信している様子が見えます。ただし、一度送信した直後に再度ほぼ同じ内容のリクエストが記録されています。
このときの2つ目のARPリクエストが、FPGAから送信されたEthernetフレームです。内容をそっくりそのまま送り返しているため、Ethernetヘッダーの送信元アドレスと送信先アドレスもそのままとなっており、あたかもホストから送信されたかのよな内容が記録されています。
なお、なぜか末尾に0x00のデータが増えていますが、10G Ethernet Subsystemからの受信側XGMIIの信号を観測してみると、その時点で0x00のデータが増えています。よって、ホストPC側のNICが実際に0x00を末尾に付加した信号を送ってきているようです。
ここまでで、単にホストPCのNICから送信したEthernetフレームをそのまま送り返すだけのループバック動作ができるようになりました。Ethernet MACの動作確認としてはこれでよいですが、このままではあまり面白くありません。せめてpingを打ったら返ってくる、つまりICMP Echo Requestに対してICMP Echo Replyを返すくらいはしたいところです。
幸い、EthernetフレームのデータはすべてAXI4 Stream経由でやり取りするため、Vivado HLSでこの手のロジックを作ることはそんなに難しくありません。
そこで、HLSでこれらのロジックを作ろうかと考えていたところ、なんとオープンソースのTCP/IPオフロード・エンジンがGitHub上で公開されているということを社内の別のメンバーから教えてもらいました。
TCP/IPの処理を行えるということは、当然、ICMPの処理や、IPとMACアドレスを変換するためのARPの処理が含まれています。
せっかくのTCP/IPオフロードエンジンですので、TCP/IPの通信処理もできたら面白かったのですが、今回は時間の都合上、TCP/IPオフロードエンジンからARPとICMPの処理モジュールだけを借りてきてpingに応答するデザインを作ってみました。
図中の各IPのインスタンスの役割を以下に示します。IPベンダーがTOEとなっているIPは、TCP/IPオフロードエンジンから借りてきたモジュールです。
インスタンス名 | IP名 | IPベンダー | 役割 |
---|---|---|---|
axis_dwidth_converter_in | AXI 4 -Stream Data Width Converter | Xilinx | 受信したEthernetフレームの内容を含む64bit幅のAXI Streamを、TOEから借りてきたモジュールが受けられる512bit幅に変換する |
packet_handler_0 | Packet_handler | TOE | EthernetフレームのTypeフィールドから種別を判定し、対応する番号をAXI StreamのTDESTに付加する。 |
axis_switch_in | AXI4-Stream Switch | Xilinx | packet_handler_0で判定した種別にしたがって、ストリームの出力先を分岐する。 |
arp_server_inst | Arp_server | TOE | ARPのEthernetフレームを処理する。自身のMACアドレスやIPはポートから入力した信号で設定する。 |
icmp_server_inst | Icmp_server | TOE | ICMPのEthernetフレームを処理する。 |
ethernet_header_inserter_inst | Ethernet_header_inserter | TOE | 入力されたEthernetのペイロードに対して、現在のARPテーブルの内容に基づいて、適切なEthernetヘッダーを付加して出力する |
axis_switch_out | AXI4-Stream Switch | Xilinx | ethernet_header_inserter_instおよびarp_server_instから出力された送信データをフレーム単位で切り替えて出力する |
axis_dwidth_converter_out | AXI4-Stream Data Width Converter | Xilinx | TOE関連IPの512bit幅AXI4-Streamを64bit幅に変換する |
xlconstant_ip | Constant | Xilinx | 自身のIPアドレスを表す定数信号。TOE IPが扱う信号はビッグエンディアンにする必要があるため、バイト順が逆転することに注意 (他の定数も同様) 例えば、192.168.1.100は0x6401a8c0とする。 |
xlconstant_mac | Constant | Xilinx | 自身のMACアドレスを表す定数信号 |
xlconstant_netmask | Constant | Xilinx | 自身のIPアドレスのネットマスクを表す定数信号 |
xlconstant_gw | Constant | Xilinx | デフォルトゲートウェイのIPアドレスを表す定数信号 |
axis_dispose_udp | axis_dispose | Fixstars | 今回は使用しないUDPのパケットデータを捨てるために、ダミーのTREADYを出力する。 (こうしないと、UDPのフレームを受信したらaxis_switch_inがハングアップする) |
axis_dispose_tcp | axis_dispose | Fixstars | 今回は使用しないTCPのパケットデータを捨てるために、ダミーのTREADYを出力する。 |
TCP/IPオフロードエンジンのIPは一部を除いてVivado HLSで記述されています。公開されているソースコードに付属しているVivado HLS合成スクリプトでは、Virtex Ultrascale+ XCVU9P用のIPを生成します。
このままではAlveo U50用のデザインに組み込めないので、合成スクリプトのターゲットFPGAをAlveo U50に対応する型式 (xcvu35p-fsvh2104-2-e) に変更して高位合成を行っています。
また、もともとはVivado HLS 2018.3用のようですが、Vitis 2019.2に付属しているVivado HLSで問題なく合成できています。
ループバックの時と同様に、合成したビットストリームをAlveo U50に書き込み、ホストからpingを打ってみます。FPGA側のIPは192.168.1.100
、MACアドレスは00:11:22:33:44:55
に設定しています。
$ ifconfig enp3s0f1 # FPGAにつながっているNICはenp3s0f1
enp3s0f1: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 192.168.1.1 netmask 255.255.255.0 broadcast 192.168.1.255
ether xx:xx:xx:xx:xx:xx txqueuelen 1000 (イーサネット)
RX packets 38348 bytes 5858313 (5.8 MB)
RX errors 0 dropped 97 overruns 0 frame 0
TX packets 483783 bytes 69814135 (69.8 MB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
$ sudo arp -i enp3s0f1 -d 192.168.1.100 #ARPエントリを削除
$ arp -i enp3s0f1 #ARPエントリを確認
arp: 29のエントリ中, 一致するものが見つかりません.
$ ping 192.168.1.100 # FPGAにpingを打つ
PING 192.168.1.100 (192.168.1.100) 56(84) bytes of data.
64 bytes from 192.168.1.100: icmp_seq=1 ttl=128 time=0.036 ms
64 bytes from 192.168.1.100: icmp_seq=2 ttl=128 time=0.081 ms
64 bytes from 192.168.1.100: icmp_seq=3 ttl=128 time=0.077 ms
64 bytes from 192.168.1.100: icmp_seq=4 ttl=128 time=0.078 ms
64 bytes from 192.168.1.100: icmp_seq=5 ttl=128 time=0.040 ms
64 bytes from 192.168.1.100: icmp_seq=6 ttl=128 time=0.044 ms
64 bytes from 192.168.1.100: icmp_seq=7 ttl=128 time=0.078 ms
64 bytes from 192.168.1.100: icmp_seq=8 ttl=128 time=0.060 ms
64 bytes from 192.168.1.100: icmp_seq=9 ttl=128 time=0.076 ms
64 bytes from 192.168.1.100: icmp_seq=10 ttl=128 time=0.080 ms
^C
--- 192.168.1.100 ping statistics ---
10 packets transmitted, 10 received, 0% packet loss, time 9217ms
rtt min/avg/max/mdev = 0.036/0.065/0.081/0.017 ms
$ arp -i enp3s0f1 # ARPエントリ確認
アドレス HWタイプ HWアドレス フラグ マスク インタフェース
192.168.1.100 ether 00:11:22:33:44:55 C enp3s0f1
以上で、ホストPCからのpingに応答することが確認できました。また、pingの後にホストPCのARPエントリに、FPGAのIPアドレスとMACアドレスが追加されており、ARPの処理が動作していることが確認できました。
Alveo U50向けに10G Ethernet MAC IPを作って動作させるのは、とりあえず動くものであれば、そんなに難しくないということがわかりました。
製品などに使用するには、検証内容など足りないところがありますが、とりあえず10G Ethernetのフレームに応答して、何かしら処理をして、結果を返すシステムのプロトタイプを作ってみるのには十分かと思います。
今回作成した10G Ethernet MACのソースコードを https://github.com/fixstars/xg_mac に公開しました。使用方法や注意事項などについては README やライセンスファイルを御覧ください。なおファイルを clone する際は以下のように recursive オプションを付けて取得するようにして下さい。
$ git clone --recursive git@github.com:fixstars/xg_mac.git
コンピュータビジョンセミナーvol.2 開催のお知らせ - ニュース一覧 - 株式会社フィックスターズ in Realizing Self-Driving Cars with General-Purpose Processors 日本語版
[…] バージョンアップに伴い、オンラインセミナーを開催します。 本セミナーでは、...
【Docker】NVIDIA SDK Managerでエラー無く環境構築する【Jetson】 | マサキノート in NVIDIA SDK Manager on Dockerで快適なJetsonライフ
[…] 参考:https://proc-cpuinfo.fixstars.com/2019/06/nvidia-sdk-manager-on-docker/ […]...
Windowsカーネルドライバを自作してWinDbgで解析してみる① - かえるのほんだな in Windowsデバイスドライバの基本動作を確認する (1)
[…] 参考:Windowsデバイスドライバの基本動作を確認する (1) - Fixstars Tech Blog /proc/cpuinfo ...
2021年版G検定チートシート | エビワークス in ニューラルネットの共通フォーマット対決! NNEF vs ONNX
[…] ONNX(オニキス):Open Neural Network Exchange formatフレームワーク間のモデル変換ツー...
YOSHIFUJI Naoki in CUDAデバイスメモリもスマートポインタで管理したい
ありがとうございます。別に型にこだわる必要がないので、ユニバーサル参照を受けるよ...