10
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

インテル® FPGA設計に関する記事を投稿しよう! by インテルAdvent Calendar 2022

Day 2

外部からPlatformDesignerモジュールにアクセスしよう~Avalon-MMブリッジの使い方あれこれ

Last updated at Posted at 2022-12-01

はじめに

こんにちは。J-7SYSTEM WORKSの長船です。

今回のアドベントカレンダーではPlatform Designerで設計したモジュールに、外部からアクセスする手法を紹介したいと思います。
本記事ではPlatform Designerで何度かデザインを行ったことがある中級以降向けの内容となります。

また今回の記事内容は、QuartusPrime 20.1以降であれば共通に使える内容となっています。文献などは記事の最後に紹介していますので、そちらも参照していただればと思います。


UART経由でアクセスする

さて、Platform Designerを使うとメモリバスやストリームを持つシステムを簡単に構築することができるのはご承知の通りです。

元々はNiosプロセッサ(NiosIIの前)による組み込みSoCを実現するビルドツールであったため、複数のメモリバスマスタをもつ複雑なシステムを作ることに主眼が置かれていました。
そういう背景もあり、基本的にはNiosIIプロセッサのような能動的に動作するバスマスタの存在を前提としてモジュールが構築されることが多いようです。

一方で、最近ではBluetoothやWiFiといったFPGAに内蔵できないペリフェラルを持つMCUの普及で外部にプロセッサを持つシーンが多くなり、Platform Designerで単体のSoCを構築して外部デバイスを集約するといった方向とマッチしない状況も増えてきていると感じています。
また、試作や開発などでPC上から手っ取り早くFPGAデバイス内部にアクセスしたいという状況もあるでしょう。

そこで、PC上のアプリケーションからFPGA内部(Avalon-MMやAvalon-STのデータ)に中間のソフトウェアなしでアクセスする方法、つまりPC上のアプリケーション環境とQuartus Prime以外に開発環境を必要としないブリッジの実装について説明をしていきます。

まず今回の構成として、クライアントになるPCとFPGAの間はUART(USBシリアル)で接続することにします。

構成図_usb.png

このように、FPGA側のPlatform Designerモジュールにはプロセッサは入らず、いわゆるI/Oエクステンダーのような状態です。
大量のデータをクライアントから読み書きするようなシーンには向きませんが、レジスタ設定を行ったりステータスを逐次確認するような低速のアクセスしかしない、あるいはFPGA側の処理を待たせても問題ないような場合には充分です。

こういった外部からAvalon-MMバスにアクセスするためのコンポーネントがPlatform Designerには用意されています。

トランザクションコンバーター

Avalon Packets to Transactions Converterコアは、パケットストリーム(startofpacket信号,endofpacket信号が付加されたAvalon-ST信号)のデータを処理して、Avalon-MMバスファブリックのアクセスに変換するコアです。

transaction_converter.png

このコアはトランザクションパケットを受け取るAvalon-ST Sink、応答パケットを出力するAvalon-ST Source、バスファブリックにアクセスするAvalon-MM Hostのポートを持ちます。
規定のパケットを組み立ててパケットストリームで渡してやれば、所望の動作を行った結果がまたパケットストリームで返されます。クライアント側はこのパケットのやりとりの部分を実装すればよいわけです。
トランザクションパケットは以下のようなフォーマットになっています。

バイト フィールド 内容
0 Transaction code トランザクションのタイプ。
  • 0x00:シングルライト
  • 0x04:インクリメントライト
  • 0x10:シングルリード
  • 0x14:インクリメントリード
  • 0x7F:No transaction
1 Reserved 予約(常に0x00)
[2:3] Size トランザクションデータサイズ。バイト数をネットワークバイトオーダーで指定します。ライトの場合、サイズはdataフィールドのバイト数を示します。リードの場合、サイズは読み出す合計バイト数を指示します。
[4:7] Address トランザクション開始32bitアドレス。ネットワークバイトオーダーで格納します。
[8:n] Data トランザクションデータ。ライトトランザクションで書き込まれるデータをバイト順で格納します。

シングルライト(非インクリメントライト)、シングルリード(非インクリメントリード)は、指定のアドレスに32bit幅以下のデータのアクセスを行います。
Avalon Packets to Transactions Converterコアは32bit幅のデータバスのAvalon-MMホストを持ち、アトミックな32bitアクセスを行う事ができます。逆に言うと、32bit境界を跨ぐようなアクセスはできません。
シングルアクセスのみのため、指定できるデータサイズは1,2,4のいずれかです。

インクリメントライト、インクリメントリードは、指定のアドレスから指定のサイズのバイトデータのアクセスを行います。
アクセスサイクルごとにアドレスはインクリメントされます。もし開始アドレスが非境界の場合は、自動的にデータ分割が行われます。

トランザクションパケットでは多バイト長のデータ(SizeフィールドおよびAddressフィールド)はネットワークバイトオーダーで格納します。ただし、Dataフィールドはバイト順で処理されるため、このフィールドはリトルエンディアン(Platform Designerでは通常リトルエンディアンで解釈される)になります。

トランザクション要求に対して、Avalon Packets to Transactions Converterコアは以下の応答を返します。
シングルライト、インクリメントライトおよびNo transactionの場合は次のような固定長のパケットを返します。

バイト フィールド 内容
0 Transaction code 応答トランザクションのタイプ。MSBが反転したTransaction codeが格納されます。
1 Reserved 予約(常に0x00)
[2:3] Size 書き込んだデータサイズ(No transactionでは常に0)。

シングルリード、インクリメントリードでは、読み出したデータを次のような可変長のパケットで返します。

バイト フィールド 内容
[0:n] Data トランザクションデータ。リードトランザクションで読み出したデータがバイト順で返されます。

パケットデータコンバーター

さて、パケットデータを組み立てればAvalon-MMバスアクセスができるわけですが、PCやMCUとのやりとりをするには、もう一つ考えなくてはならないことがあります。
Avalon Packets to Transactions Converterコアはstartofpacket信号を基準にしてパケットデータを解釈するので、バイトデータ列の中にパケットの先頭・末端のマーカーをどうにかして埋め込んでやらなくてはなりません。
もちろん、startofpacket信号とendofpacket信号とdata信号をまとめて10bit幅のデータストリームにする方法でも良いのですが、汎用的な通信路はほぼ8bit単位のデータ幅であり、そこを外れると取り扱いが難しくなります。

そこで、パケットストリームを単純なバイト列にコード化してしまいます。これらの変換・復元の処理をおこなうのが「Avalon-ST Bytes to Packets Converterコア」と「Avalon-ST Packets to Bytes Converterコア」です。

packet_converter.png

Avalon-ST Bytes to Packets Converterコアは、エンコードされたバイトストリームを受け取り、パケットストリームに復元します。
Avalon-ST Packets to Bytes Converterコアは逆に、パケットストリームを受け取り、エンコードされたバイトストリームに変換します。

パケットストリームのエンコードには以下の4つの特殊なマーカーコードが使用されます。これらのコードはデータ列の中には出現しません。復元時はこれらのコードを元にデータのデコードを行います。

コード 指示 内容
0x7A SOP 次のバイトがパケットの先頭であることを示します。
0x7B EOP 次のバイトがパケットの終端であることを示します。
0x7C CNI 次のバイトがチャネル番号であることを示します。
Avalon Packets to Transactions Converterコアでは使用されないため、常に0を指定します。
0x7D ESC 次のバイトがエスケープされたバイトである事を示します。
エスケープ処理は元の値に0x20がXORされます。

コードはパケットの開始を示すSOP(0x7A)、パケットの終了を示すEOP(0x7B)、チャネルを指示するCNI(0x7C)、それにこれらの値がデータに表れた場合のエスケープ指示子ESC(0x7D)の4つです。
パケットデータ中にこれらの値が出てきた場合は該当バイトを0x20でXORしたあと、ESC(0x7D)を先行バイトとして追加します。
全てのコードは、データ列からは取り除かれ後続のバイトを修飾するように動作をします。そのためEOPは最終バイトの1つ前に挿入する必要があります

実際の変換動作を見てみましょう。以下は、アドレス0x023A7A00にデータ0x44332211をシングルライトで書き込んだときの例です。

transaction_iowr.png

  1. クライアントは最初にAvalon Packets to Transactions Converterコアに向けたトランザクションパケットを作成します。
  2. 次にトランザクションパケットのデータに対してエンコード処理(SOP,EOP,CNI付加、およびデータバイトのエスケープ)を行います。
    例ではデータ中に0x7Aが含まれているため、該当バイトにエスケープ処理が行われて0x5Aに変換されています。
  3. エンコードされたバイト列はAvalon-ST Bytes to Packets Converterコアで復元されAvalon Packets to Transactions Converterコアに渡されます。
  4. 応答パケットがAvalon-ST Packets to Bytes Converterコアでエンコードされ、クライアント側に送られます。
  5. クライアントは戻ってきたバイトストリームから応答パケットを復元します。

もうひとつ、今度はアドレス0x0100007Cから8バイトをインクリメントリードで読み出したときの例を見てみましょう。

transaction_memrd.png

  1. クライアントはトランザクションパケット作成します。
  2. 同様にエンコード処理を行います。今回はパケットに最後にエスケープすべきバイト0x7Cがあることに注目してください。
    SOP,EOP,CNIの次のバイトがエスケープすべきデータだった場合、そこにはさらにエスケープコードが重なった状態になります。ここではEOPの後ろにESC、その後ろにエスケープされたデータ0x5Cが繋がります。
  3. 読み出したデータが応答パケットとして返されます。ここでは先頭にエスケーブすべきバイトがある例です。
  4. 応答パケットでも同様にエスケープ処理がされてます。SOPの後にESCが重なっているのがわかります。

これらの変換・復元処理を挟むことで、通信部分は単純なバイトストリーム(順序が保証されたバイト単位の通信)でパケットを送受信することができるわけです。
この、Avalon Packets to Transactions Converterコア、Avalon-ST Bytes to Packets Converterコア、Avalon-ST Packets to Bytes Converterコアの3つの組み合わせがAvalon-MMブリッジの基本構成になります。

Avalon-MMブリッジの作成

以上を踏まえて、UART to Avalon-MM Bridgeの実装をしていきましょう。
とはいえ、今回もいつもの通り出来合のコンポーネントを組み合わせて実装するため、HDLはPlatform Designerモジュールをインスタンスするトップの部分のみになります。

まず、UARTからAvalon-STに変換するコンポーネントが必要になります。これはPlatform Designerの標準コンポーネントには存在しないため、ユーザーで追加しなければなりません。
下記のリポジトリからClone(またはダウンロード)して、プロジェクトフォルダ直下にipという名前のフォルダを作成してコピーしておきます。

Github : osafune - misc_hdl_module / MISCモジュール集

フォルダ構成
<プロジェクトフォルダ>
  ├─ foo_top.qpf
  ├─ foo_top.qsf
  ├─ foo_top.qws
  ├─ <ip>
  │   └─ <misc_hdl_module> ← ここに格納
  │       └─ <hdl>
  :
  :

この状態でPlatform Designerを呼び出すと、IP Catalogにuart_to_bytesというコンポーネントが追加されます。これをAddして、パラメータは以下のように設定します。

パラメータ
Bitrate 115200bps
Stopbit length 1bit

このコンポーネントは外部との通信を行うため、uartポートはExport名を入れて外部に出しておきます。

次に、altera_avalon_st_bytes_to_packetsaltera_avalon_st_packets_to_bytesaltera_avalon_packets_to_master の3つのコンポーネントをAddします。

これらのAvalon-STコンポーネントを接続していくわけですが、PacketストリームとByteストリームが混ざらないように注意してください。

接続元 接続先
uart_to_bytes
ポート名:source
altera_avalon_st_bytes_to_packets
ポート名:in_bytes_stream
altera_avalon_st_bytes_to_packets
ポート名:out_packet_stream
altera_avalon_packets_to_master
ポート名:in_stream
altera_avalon_packets_to_master
ポート名:out_stream
altera_avalon_st_packets_to_bytes
ポート名:in_packet_stream
altera_avalon_st_packets_to_bytes
ポート名:out_bytes_stream
uart_to_bytes
ポート名:sink

その後は、Avalon-MMコンポーネントにSystemIDPIOをAddします。PIOはオンボードのLEDに接続するので、出力をExportしておきます。
最終的な構成を以下に示します。

pd_uart_bridge.png

これをGenerateして、トップのHDLでインスタンスします。

air_uart_top.v
module air_uart_top(
    input wire          CLOCK_50,
    input wire          RESET_N,

    input wire          UART_RXD,
    output wire         UART_TXD,

    output wire [1:0]   USER_LED
);

    uart_bridge_core u0 (
        .clk_clk       (CLOCK_50),
        .reset_reset_n (RESET_N),
        .uart_txd      (UART_TXD),
        .uart_rxd      (UART_RXD),
        .uart_cts      (1'b1),
        .uart_rts      (),
        .led_export    (USER_LED)
    );

endmodule

今回、ターゲットボードは手持ちのCycloneIV E搭載のPERIDOT-Airボード、UARTには秋月電子のAE-FT234Xを利用しました。
AE-FT234Xに載っているチップ(FTDI社製FT234XD)自体はCTS/RTSのハードフローをサポートしていますが、このモジュールはCTS/RTSはループバックされて端子にはでていないので、HDLでctsポートに1'b1を設定してフローなし動作にしています。

クライアント側のソフトウェアと実行結果

FPGA側は作成できたので、クライアント側のツールを作っていきます。
今回はクライアント側はPC(Windows10)、USBシリアル変換でUARTを接続しています。言語環境はシリアルポートがアクセスできれば何でも構いません。ここでは手っ取り早くPythonを使用しました。

Pythonでは標準ではシリアルポートアクセスができるモジュールが入っていないため、pipコマンドでpySerialモジュールをインストールしておきます。

> pip install pyserial

後はトランザクションパケットを組み立ててバイトストリームにエンコードし、シリアルポートにwriteすればFPGA側で解釈されてAvalon-MMのアクセス結果が返されます。
注意しないといけないのは、返されるデータ長はエスケープされている可能性があるので、バイトストリームの全長は受信してみないと分からないという点です。

このため、シリアルポートから1バイトずつ取得しながら、バイトストリームをデコードしていく必要があります。今回の実装ではbytes_decode関数にローカルの静的変数を付けて、内部ステートを持たせるようにしました。

uart_access.py
from serial import *
from struct import *
from datetime import *

SERIAL_PORT = "COM5"
SERIAL_BITRATE = 115200

def print_bin(label, bin):
	print(label, end=" : ")
	for b in bin: print(format(b, "02x"), end=" ")
	print("")

# Packetデータのデコードとエンコード

def bytes_decode(byte):
	if byte == 0x7a:
		bytes_decode.status |= 1<<0
		bytes_decode.result = b""
		return
	elif byte == 0x7b:
		bytes_decode.status |= 1<<1
		return
	elif byte == 0x7c:
		bytes_decode.status |= 1<<2
		return
	elif byte == 0x7d:
		bytes_decode.status |= 1<<3
		return

	if (bytes_decode.status & (1<<2)) != 0:
		bytes_decode.status &= ~(1<<2)
		return

	if (bytes_decode.status & (1<<3)) != 0:
		bytes_decode.status &= ~(1<<3)
		byte ^= 0x20

	if (bytes_decode.status & (1<<0)) != 0:
		bytes_decode.result += pack("B", byte)

	if (bytes_decode.status & (1<<1)) != 0:
		bytes_decode.status = 0
		return bytes_decode.result

bytes_decode.status = 0
bytes_decode.result = b""

def bytes_encode(bin):
	bytes = b"\x7c\x00\x7a"
	for idx,c in enumerate(bin):
		if idx == len(bin) - 1:
			bytes += b"\x7b"

		if c >= 0x7a and c <= 0x7d:
			bytes += pack("BB", 0x7d, c ^ 0x20)
		else:
			bytes += pack("B", c)
	return bytes

# Avalon-MMのライトとリード

def avm_memwr(ser, addr, wrbytes):
	send_data = bytes_encode(pack("!BxHL", 0x04, len(wrbytes), addr) + wrbytes)
	print_bin("-> MEMWR", send_data)
	ser.write(send_data)
	res = None
	while res is None: res = bytes_decode(ser.read()[0])
	print_bin("<-   RES", res)
	resp, size = unpack("!BxH", res[0:4])
	return size

def avm_memrd(ser, addr, readnum):
	send_data = bytes_encode(pack("!BxHL", 0x14, readnum, addr))
	print_bin("-> MEMRD", send_data)
	ser.write(send_data)
	res = None
	while res is None: res = bytes_decode(ser.read()[0])
	print_bin("<-   RES", res)
	return res

# Platform Designerモジュールのアクセス 

with Serial(SERIAL_PORT, SERIAL_BITRATE) as ser:
	ser.reset_input_buffer()

	# SystemIDペリフェラルのリード

	rdbytes = avm_memrd(ser, 0x10000000, 8)
	sysid, timestamp = unpack('LL', rdbytes[0:8])
	dt = datetime.fromtimestamp(timestamp)
	print("System ID =", hex(sysid), ", Build", dt)

	# PIOペリフェラルのライト(LED点灯)

	avm_memwr(ser, 0x10000020, b"\x01\x00\x00\x00")
	print("UserLED[0] on")

FPGAをコンフィグレーションした状態でこれを実行すると、Avalon-MMバスファブリックに接続されたSystemIDペリフェラルのデータを読み出し、PIOペリフェラルに繋がったLEDが点灯します。
パケットの組み立てとエンコード・デコードが正しく行われているのが確認できます。

実行結果
-> MEMRD : 7c 00 7a 14 00 00 08 10 00 00 7b 00 
<-   RES : 01 00 a0 72 47 99 87 63 
System ID = 0x72a00001 , Build 2022-12-01 02:56:23
-> MEMWR : 7c 00 7a 04 00 00 04 10 00 00 20 01 00 00 7b 00 
<-   RES : 84 00 00 04 
UserLED[0] on

DSC_2512_thumb.jpg

利用のヒント💡

一般的にシリアル通信は115200bpsを上限としているものの、USBシリアル変換チップを使う場合はもっと高速のビットレートを指定することができるものもあります。
今回使ったFT234XDであれば、最大3Mbpsまでのビットレートを指定できます。相手がFPGA側であれば、ビットレートと動作クロックが整数比になるようにしておけば、これぐらいの速度は問題ありません。
むしろボード上の信号品質の方が問題になることが多いので、最短結線やダンピング抵抗の配置なとに注意を払う必要があるでしょう。

他の使い方として、BLEモジュールを接続してクライアントと無線で接続する方法もあります。BLEを使うとスマートフォンやタブレットからFPGAにアクセスすることも容易になります。

構成図_ble.png

ゼロコードでBLEシリアルを使うことができるモジュールは各種でていますが、Lyra Series Bluetooth 5.3 Modules はアンテナ内蔵で技術適合済みで扱いやすいモジュールの一つです。
Google ChromeやMicrosoft Edgeといったブラウザでは Web Bluetooth API を利用して Webページ上のJavaScriptからBLEモジュールにアクセスすることができます。


Ethernet経由でアクセスする

ここまでで、PCからUART(USBシリアル)を使ってFPGA内部へアクセスすることができました。USBシリアルであれはそこそこの速度でアクセスも可能なため、PCからのアクセスとしてはそれなりに役に立ちます。

が、前章でpySerialをインストールしたように、シリアルポートを扱うのは意外と環境に依存し、特にOSが変わると動作も大きく変わる事が多々あります。ポート名などはその良い例です。

また、現在のPCでは事実上USBで接続することになるため、多数のデバイスを繋いだ場合のリソース管理が煩雑になるという問題もあります。距離が問題になることもあるでしょう。USBの仕様から、ハブを使って延長しても30m程度が限界です。単純にもっと速度が欲しい、というのが要望としては大きいでしょうか。

現在のPCに標準的に搭載されており、これらの要望を解決できるものとしてEthernetを利用する方法があります。

構成図_eth.png

Ethernetを利用すると言っても、一昔前のトレンドのようにインターネットへアクセスできるクライアントや公開されたサーバーというわけではありません。PC近傍のローカルなネットワークドメインの中で動作する極小のローカルサーバーをFPGAに組み込もうというわけです。

PC近傍のローカルネットワークとはいえケーブル長は最大100m、スイッチングハブを利用すればもっと延伸することもできますし、10BASE-T1Lでは1kmを越える距離に設置することも可能です。またネットワーク内に百個以上のデバイスを接続することもできます。

これらはUSB接続には無い、Ethernet利用のメリットです。
しかしEthernetを利用するにはUARTよりもずっと複雑な処理・プロトコルを実装しなければなりません。
最近ではEthernetに接続できる機能やプロトコルスタックを実装したワンチップマイコンやネットワークモジュールが増えてきましたが、ネットワーク部分に手間が割かれるのは避けたいところです。

そういうネットワーク部分に手間をかけたくないがEthernet接続は使いたいという、わがままな要望に対応したのが次に紹介するPERIDOT ETHIOコンポーネントです。

PERIDOT ETHIOコンポーネント

PERIDOT ETHIOはEthernet経由でAvalonスイッチファブリックへのアクセスを提供するPlatform Designer用コンポーネントです。単体でARP/ICMP/IP/UDPのプロトコルをハードウェアで実装しているため、Platform Designerドロップオンで即使えるのが特徴です。

通信速度は100Mbit全二重/10Mbit全二重/10Mbit半二重に対応していて、一般的な100BASE-TXのネットワークの他、10BASE-T1L/10BASE-T1Sといった1ペアEthernetのネットワークにも使えます。

極小のフットプリントになるよう、PHYインターフェースはRMII専用、汎用的なEthernet MACの機能は切り捨てUDPサーバー動作に特化した構造になっています。Avalonスイッチファブリック側からの能動的な動作は一切行わず、常にクライアントからのリクエストパケットを元に動作します。
クライアントからはUDPポートを1つ持つ固定IPアドレスのサーバーとして認識され、AvalonスイッチファブリックへはUDP/IPでアクセスを行います。

このように仕様についてはだいぶ割切っていますが、最小構成であれば1800LE/8M9kで実装が可能です。

それではこれを使ってAvalon-MMブリッジを作っていきましょう。
下記のリポジトリからClone(またはダウンロード)して、プロジェクトフォルダ直下にipという名前のフォルダを作成してperidot_ethioフォルダをコピーしておきます。

Github : osafune - peridot_peripherals / PERIDOTペリフェラル

フォルダ構成
<プロジェクトフォルダ>
  ├─ foo_top.qpf
  ├─ foo_top.qsf
  ├─ foo_top.qws
  ├─ <ip>
  │   └─ <peridot_ethio> ← ここに格納
  │       └─ <hdl>
  :
  :

この状態でPlatform Designerを呼び出すと、IP CatalogにPERIDOT Peripheralsというグループと、その下にPERIDOT Ethernet I/O Bridgeというコンポーネントが追加されます。これをAddして、パラメータは以下のように設定します。

pd_ethio_config.png

後はUART to Avalon-MM bridgeの時と同様に、SystemIDとPIOをAddします。PERIDOT ETHIOのethioポート、macaddrポート、ipaddrポート、rmiiポート、およびPIOのexternal_connectionポートはExport名を記入して外部に引き出しておきます。

pd_udp_bridge.png

これをGenerateして、トップのHDLでインスタンスします。
今回はスタンドアロン動作のサーバーとし、速度は100BASE-TX全二重のみ、MACアドレスもIPアドレスも固定で指定します。

air_udp_top.v
module air_udp_top(
    input wire          CLOCK_50,
    input wire          RESET_N,

    input wire          RMII_CLK,
    input wire  [1:0]   RMII_RXD,
    input wire          RMII_CRSDV,
    output wire [1:0]   RMII_TXD,
    output wire         RMII_TXEN,

    output wire [1:0]   USER_LED
);

	parameter MAC_ADDRESS	= 48'hfeffff000001;                // FE-FF-FF-00-00-01
	parameter IP_ADDRESS	= {8'd192, 8'd168, 8'd1, 8'd203};  // 192.168.1.203

    udp_bridge_core u0 (
        .clk_clk       (CLOCK_50),
        .reset_reset_n (RESET_N),
        .ethio_enable  (1'b1),
        .ethio_status  (),
        .macaddr_value (MAC_ADDRESS),
        .ipaddr_value  (IP_ADDRESS),
        .rmii_clk      (RMII_CLK),
        .rmii_rxd      (RMII_RXD),
        .rmii_crs_dv   (RMII_CRSDV),
        .rmii_txd      (RMII_TXD),
        .rmii_tx_en    (RMII_TXEN),
        .led_export    (USER_LED)
    );

endmodule

ターゲットボードはUARTの時とおなじPERIDOT-Airボードを使い、EthernetPHYにはMicrochipの LAN8720A を接続しました。ボードモジュールがネット通販で安く入手できます。
LAN8720AはRMIIインターフェースのみで、デフォルト状態で100BASE-TX全二重のネゴシエーションを行いEthernetに接続します。またリセット時のRXD[1:0]およびCRS_DVの信号レベルで動作モードを指定でき、MIIM(MDIO)を使わなくてもよいので実に組み込み向きなEthernetPHYです。

PERIDOT ETHIOはUDPサーバーの動作をHDLで実装しているため、FPGAがコンフィグレーションされてPHYがリンクアップした状態で、クライアントからの要求に応答することができます。
試しにコンソールからpingを送ってみましょう。

icmp_arp.png

するとこのようにpingに応答し、ARPテーブルにはHDLで指定したMACアドレスが登録されているのが確認できます。

クライアント側のソフトウェアと実行結果

クライアント側のツールはUARTと同じくPythonを使いました。
ネットワークアクセスは標準で用意されていますので、シリアルポートを使う場合よりも扱いやすくなります。

また、Ethernetは最初からパケットありきなので、UDPペイロードに格納するデータについては先頭と終端が明確に分かっています。そのためUART to Avalon-MM Bridgeで使ったバイトストリームへのエンコード・デコードも必要ありません。

そのリクエストパケットはAvalon Packets to Transactions Converterコアと同様‥‥と言いたいのですが、USBシリアルでクライアントと一対一で接続するUART to Avalon-MM Bridgeと異なり、Ethernet上のサーバーとして振る舞うPERIDOT ETHIOでは複数のクライアントがアクセスできることになります。
偶然、あるいは故意にサーバーのポートにパケットを投げるような状況は予め想定しておかなければなりません。

そこでPERIDOT ETHIOではリクエスト・応答ともにFOURCCの識別ヘッダを付加するようにしています。ヘッダの識別ができなかった場合、PERIDOT ETHIOは該当パケットを破棄して応答も返しません。パケットの終端にはコマンド列が正しく処理されたかどうかのチェック用にエンドコマンド/ステータスのフッタが付きます。

ethio_packet.png

さらにAvalon-MMだけでなく、Avalon-STのバイトストリームの対応や、Ethernetパケットを効率的に使うために複数のトランザクションをまとめて発行できるよう、トランザクションコードにも少し手をいれています。
詳細はPERIDOT ETHIOのフォルダにある packet_command.pdf を参照してください。

その他の制限として、IP層のパケットフラグメントには対応していないため、リクエストパケットも応答パケットもMTUサイズ(通常1500バイト)からIP/UDPヘッダ長をのぞいた1472バイト以下にしなければなりません。

ライトトランザクションの場合は送出時にデータ長が分かりますが、リードトランザクションの場合はリクエストパケット中で要求した読み出しサイズおよびそのヘッダ分の合計が1472バイトを越えないよう、クライアント側でコントロールする必要があります。

前述のとおりパケットのフラグメンテーションには対応しないため、応答パケットの全長が1472バイト(IP/UDPヘッダ込みで1500バイト)以上になるとパケットそのものが破棄されてしまいます。

以上を元に、パケット1つにトランザクション1つを格納した最もシンプルなパケットでAvalon-MMバスのアクセスを行う例を示します(なおここではパケット長制限の処理は省略している)。

udp_access.py
from socket import socket, AF_INET, SOCK_DGRAM
from struct import *
from datetime import *

SOCKET_BUFFSIZE = 2048
ETHIO_SERVER = ("192.168.1.203", 16241)

def print_bin(label, bin):
	print(label, end=" : ")
	for b in bin: print(format(b, "02x"), end=" ")
	print("")

# Avalon-MMのライトパケットとリードパケットの作成

ETHIO_PACKET_FOURCC = b"AVMM"
ETHIO_PACKET_ENDCMD = b"\x7f\x00\xff\xff"

def data_padding(data):
	return data + b"\x00" * ((4 - len(data)) & 3)

def avm_memwr(sock, addr, wrbyte):
	send_data = ETHIO_PACKET_FOURCC
	send_data += pack("!BxHL", 0x44, len(wrbyte), addr) + data_padding(wrbyte)
	send_data += ETHIO_PACKET_ENDCMD
	print_bin("-> MEMWR", send_data)
	sock.sendto(send_data, ETHIO_SERVER)
	res, addr = sock.recvfrom(SOCKET_BUFFSIZE)
	print_bin("<-   RES", res)
	resp, size = unpack("!BxH", res[4:8])
	return size

def avm_memrd(sock, addr, readnum):
	send_data = ETHIO_PACKET_FOURCC
	send_data += pack("!BxHL", 0x54, readnum, addr)
	send_data += ETHIO_PACKET_ENDCMD
	print_bin("-> MEMWR", send_data)
	sock.sendto(send_data, ETHIO_SERVER)
	res, addr = sock.recvfrom(SOCKET_BUFFSIZE)
	print_bin("<-   RES", res)
	resp, size = unpack("!BxH", res[4:8])
	return res[8:8+size]

# Platform Designerモジュールのアクセス 

with socket(AF_INET, SOCK_DGRAM) as sock:

	# SystemIDペリフェラルのリード

	rdbytes = avm_memrd(sock, 0x10000000, 8)
	sysid, timestamp = unpack('LL', rdbytes[0:8])
	dt = datetime.fromtimestamp(timestamp)
	print("System ID =", hex(sysid), ", Build", dt)

	# PIOペリフェラルのライト(LED点灯)

	avm_memwr(sock, 0x10000020, b"\x01\x00\x00\x00")
	print("UserLED[0] on")

pingに応答する状態、つまりFPGAをコンフィグレーションしてEthernetにリンクアップした状態で実行すると、UARTの時と同様にAvalon-MMバスファブリックに接続されたSystemIDペリフェラルとPIOペリフェラルにアクセスします。UART to Avalon-MM Bridgeとはパケットデータが異なっているのが分かると思います。

実行結果
-> MEMWR : 41 56 4d 4d 54 00 00 08 10 00 00 00 7f 00 ff ff 
<-   RES : 41 56 4d 4d d4 00 00 08 02 00 a0 72 6d b3 87 63 ff 00 00 00 
System ID = 0x72a00002 , Build 2022-12-01 04:47:57
-> MEMWR : 41 56 4d 4d 44 00 00 04 10 00 00 20 01 00 00 00 7f 00 ff ff 
<-   RES : 41 56 4d 4d c4 00 00 04 ff 00 00 00 
UserLED[0] on

DSC_2514_thumb.jpg


まとめ

今回はPCからFPGAの内部へアクセスする手法を2つ紹介しました。

クライアントがPCではなくワンチップマイコンであるなら、I2C経由でAvalon-MMにアクセスする「I2C Slave to Avalon-MM Master Bridgeコア」やSPI経由でアクセスする「SPI Slave to Avalon Master Bridgeコア」があります。ペリフェラル拡張として使うなら、I2Cバスを共有できるI2C Slave to Avalon-MM Master Bridgeコアを使うのもオススメです。

FPGAの使用シーンは以前より画像処理や信号処理のI/Oエッジ側のプロセッサとして使うことはよくありましたが、ここ数年で素朴なフィルター処理からAIのように複雑な非線形処理のオフロードプロセッサとしての利用にも広がってきています。

アルゴリズムの開発も、ツールで検証した後にHDLで落とし込むというものから、演算処理部の実装とそれに渡すパラメータセットを別々に開発するというパターンも増えました。
PCの汎用的なインターフェースであるシリアルポート(USBシリアル)やEthernetを使うことは、それらのアルゴリズム開発ツール上でFPGAをより密に取り扱うことができます。

例えばデータ解析やアルゴリズム開発でよく使われるMATLABやJupyter notebookではシリアルポートやUDP/IPのアクセス関数が用意されています。
それらを使ってAvalon-MMバスへ自由にアクセスできれば、FPGAのメモリにテスト用の画像データを配置して処理結果を評価したり、リアルタイムでパラメーターを制御して検討するといったことがこれまでよりもずっと楽にできるでしょう。

そういったシーンでもFPGAの利用が増えていったらいいなと思います。

それでは皆さま、よいPlatform Designerライフを!


参考

エンベデッド・ペリフェラルIPユーザーガイド
https://www.intel.co.jp/content/www/jp/ja/docs/programmable/683130/20-2/introduction.html

Intel Quartus Prime Standard Edition User Guide : Platform Designer
https://www.intel.com/content/www/us/en/programmable/documentation/jrw1529444674987.html

この記事で作成したプロジェクト
https://github.com/osafune/transaction_sample

PERIDOT-Air プロトタイピングFPGAボード
https://peridotcraft.buyshop.jp/items/63179915

FT234X 超小型USBシリアル変換モジュール
https://akizukidenshi.com/catalog/g/gM-08461/

LAN8720イーサネットボードモジュール
Amazon


10
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?