PYNQしてみる。そして動かず
目次
PYNQ-Z1ボードなのでpythonからPL部を動作させたい
pythonからPL部のロジックを動かす事が目的ですが、ただ動かすだけではなく、DDRメモリをPL<->PS間で共有メモリとして使えるようになることが最終目標です。
が、いきなり結論を記載すると、8byte単位で後半4byteがPLからうまく読みだせないというバグに悩まさせています。
例えば連続した64KBを読みだすと、アドレス4-7,12-15,20-23 ...という具合に8アドレスの後半4byteがうまくアクセス出来てないという結果です。転送方法は32bit幅でlen=7なのですが。
[2020/10/28追記]AXIのバス幅を64bitとすることで本現象は回避出来ました。次回の記事に記載します。
DMAを作成する
前回作成したjt51+68Kコアをpythonから起動したかったのですが、うまく動かない為、DMAするだけの回路を作成しました。
PYNQのworkshopにはDMAを動作させるsampleがありますが、多分普通に動いてしまいそうなので自作しました。しかしながらworkshopを行う事で今回の最終目標を達成するのに必要な知識、以下の事について学習が出来るので一通りworkshopをこなすことは必要です。またworkshopを通してjupyter notebookの使い方もなんとなく分かるようになると思います。
- MMIO Classを使ってPLで作成したgpioレジスタへのアクセス方法
- DDRメモリーへのアクセスはLinux配下にあるので、PL部からは普通にはアクセスできないので、PS、PL両側からアクセスが可能なXlnk Classを使っての連続したメモリ領域の確保方法及び物理アドレスの取得方法
お試しで作成したDMAの仕様は以下の様になります。
- AXI4(Liteではない)でHP slave へアクセスしてDDRをリード
- 32bit幅、length=7(32byte)のバースト転送を利用
- リードした32byteデータを一旦内部SRAMへ取り込む
- 内部SRAMへ取り込んだデータをAXI経由でDDRへ書き出す
- リードとライトは同時に発生しません。逐次動作とします。
- GPIOレジスタで、source address, destination address,転送サイズを設定
- GPIOレジスタでDMA転送開始
- GPIOレジスタで転送中かどうかのStatusを読むことが出来る
という簡単なRTLを作成する事にします。
大したRTLでもないので公開しちゃいます。
DMA本体
https://drive.google.com/file/d/1iKDQd205HI9D8DelE-Ia9br8vVK1xhOg/view?usp=sharing
vivadoから読み込むためのラッパー回路
https://drive.google.com/file/d/1y-BZAzj5Ypcmk0yzMCl0GTTlWeMgmlCe/view?usp=sharing
vivadoでの作業
pythonから制御するために、xilinxツールのversionを上げてます。2019.2を使いました。2016.4と比べて大分見た目が変更になっていますが、vivadoを使う分には作業は迷わないでしょう。
デバッガはSDKではなく、Vitis IDEに変更になってます。こちらは大分違う感じがしますが、2016を使った経験があれば大丈夫です。
いつも通り新規にプロジェクトを作成して、上記のRTL2つを読み込みます。
Create Block designでZYNQを置いて、自動configさせた後、AXI slaveとして使うHP0を使えるようにします。今回作成したRTLのバス幅は32bitなので32bitになるようにコンフィグします。
次に先ほど読み込んだRTLをインスタンスします。右クリック→Add Module
Run Connection AutomationをクリックするとHP0に、このRTLのAXImasterがインターコネクトを通して自動結線されます。ちなみに自動結線させるにはport名にネーミングルールがあり、マスターIFの場合、Mx_AXI_xxxx となってないといけないようです。
gpioは3個インスタンスしました。すべてdual構成です。内訳は、以下の通りです。
source_adrs, // gpio_0[31:0] - ch1 dest_adrs, // gpio_0[31:0] - ch2 size, // gpio_1[24:0] - ch1 start, // gpio_1[0] - ch2 0 -> 1 でSTART r_status, // gpio_2[0] - ch1 trans_cnt, // gpio_2[31:0] - ch2 転送バイト数/32 を示すはず
gpio_2は回路のstatus読みなのでall inputs 設定としました。後は回路制御用の為all outputsとしてます。
gpioインスタンス後、ダブルクリックでビット数、方向を設定したら自動配線ボタンを押す前にgpioとRTLモジュールを結線した方が良いです。AXIの自動配線を間違うかもしれません。まぁ修正は簡単ですが、若干パニクるかもです。
以上で出来上がったブロック図が以下です。
LED[2:0]で状態が分かるようにしてます。
- LED[0]:動作中で点灯
- LED[1]:RESPエラーなどがあった場合点灯
- LED[2]:デバッグ用にARVALID|AWVALIDを出したですが、視認は無理でした
LEDを使ったのでxdcファイルを用意してます。(PYNQ-Z1用です。他のボードは分かりません)
set_property -dict {PACKAGE_PIN R14 IOSTANDARD LVCMOS33} [get_ports {LED[0]}] set_property -dict {PACKAGE_PIN P14 IOSTANDARD LVCMOS33} [get_ports {LED[1]}] set_property -dict {PACKAGE_PIN N16 IOSTANDARD LVCMOS33} [get_ports {LED[2]}]
後はいつものように右クリック→Validate design
create HDL Wrapper します。
sources でデザインの最上位階層が違ってたら、Set as Topしましょう。(僕はよく忘れる)
Generate Bitstream します。
終わったらFile→Export→Export Hardware
2016.4と作法は同じです。
ファイル名は変わっていて*.xsa という形式です。実はこれzip形式で、中身はbit,hwh,tclなどを固めたファイルの様です。
次に2016.4でしたらFile →launch SDKでしたが、、、ありません。Vitisで変わりました。
Tools→Launch Vitis です。
Vitisでの作業
workplaceをどこにするか?を聞かれます。以前はvivadoのdirへ勝手にxxx.sdkなるフォルダが作られましたが、vitisからは指定するようになりました。以前と同じようにvivadoのdirをworkspaceとしました。そしてLaunchボタンを押します。ここから2016.4とはだいぶ違うので戸惑います。
Create Platform Project
Project nameは何でもよいがaxi_copy_projectとした。Use default locationはチェックしたままとする。
Next →Create from hardware specification(XSA)にチェックを入れたまま、Next
XSAファイルの在りかを聞かれるので、先ほどvivadoで生成したxsa(design_1_wrapper.xsa)を指定する。
standalone, ps7_cortexa9_0、Generate boot componetsはチェックした状態でFinishボタン
IDEが開くのでトンカチボタンを押してBuildします。(以前のように自動でやってくれなくなった)
次にCプロジェクトを作成します。File→New→Apllication project (正直、何を作らされてるのかさっぱり分からないです)
先ほど作成した時と同じようなGUIが出ます。こちらはCのプロジェクトです。C_debugとしました。
platform選択が出るので、先ほど作成したproject nameを選択します。
Domainを聞かれるのでStandaloneを選択します。するとようやくデバッガの画面が出ます。
srcフォルダへデバッグ用Cプログラムを置けばデバッグが出来る様になります。
トンカチボタンを押してbuildし、虫アイコンを押してDebug開始します。
launch on Hardware(Single Application Debug)
一回Disconnectして、もう一度虫アイコンを押してDebug configurationを選択します。その後にDebugボタンを押します。今度はProgram FPGAが実行されてbitファイルが転送されているようです。
DMAの機能を確認するため、コピー元のデータをデバッガからDDRへ書き込みます。Xilinx→Dump/Restore Memoryからファイルを選択してメモリへロードできますが、Xsctコンソールから簡単に出来ます。
xsct% mwr -bin -file c:/xxx/xxx/a.bin 0x10000000 0x1aea
とすると、a.binファイルを0x10000000を先頭アドレスとする0x1aea分メモリへ書き込みます。
これをファイル化して,
xsct% source c:/xxx/xxx/matome.tcl
matome.tclに複数のmwrコマンドを並べておけばいくつかのバイナリファイルを一気にロード可能です。
2016.4でも可能です。
デバッグ用Cソースを置いておきます。
https://drive.google.com/file/d/1yMRgeJrCkL21GzEhMkzcPWrFW_lRYqqU/view?usp=sharing
やっていることは、0x1000_0000以降に置いたtransfer_size = 0x60000バイトデータを 0x1006_0000以降にコピーします。コピーするのが今回作成したDMAです。
XGpio_DiscreteWrite(&Gpio1Addr, start, 0x1);
でDMAを起動させています。
転送終了後、比較しています。比較した結果エラーだとSerialコンソールにErr :を出力します。転送先のアドレス0x1006_0000以降のメモリウィンドウを開けながら実行すると、メモリウィンドウを開けているアドレス部分が不一致してしまうので、DMA時は、別のアドレス(コピー元の0x1000_0000等)領域を開けておくと良いです。
これでベアメタルでハードの確からしさは証明できたかと思います。次に本PLをpythonから呼び出します。Disconnectしてvitisは終了させます。PYNQの電源はOFFにします。
pythonから呼び出し
まず、PYNQをLinuxのモードでブートさせる必要があるのでジャンパーをJTAGからSDへ切り替え、LANケーブルを繋いで電源を入れます。暫くするとLEDがピカピカ、緑LEDが全灯します。
Overlay呼び出しを行うため、ハード情報をコピーします。僕はwindowsからpynqをマウントしてコピーしました。
vivadoフォルダ\vits_axi_copy2.srcs\sources_1\bd\design_1\hw_handoff\design_1.hwh → P:\pynq\overlays\axi_copy\axi_copy.hwh
vivadoフォルダ\vits_axi_copy2.runs\impl_1\design_1_wrapper.bit →P:\pynq\overlays\axi_copy\axi_copy.bit
P:\はwindowsからマウントしたpynqです。
hwhとbitファイルを同じfile名でoverlays配下にコピーしておきます。次にjupyterへアクセスします。
http://pynq
パスワードはdefaultから変更してなければxilinxです。
適当なDirへ移動してopen→Python 3でpythonを起動させます。
なんと期待値が一致しません。同じハードなのになぜ?って感じなのですが。以下にpythonコードを書きます。
#OverlayでPL呼び出し。これでPL部分がリセットされて起動される。LEDの全灯が消灯するので分かる from pynq import Overlay from pynq import PL axi_copy_OL = Overlay("/home/xilinx/pynq/overlays/axi_copy/axi_copy.bit") #16MBの連続アドレス空間 Xlnkクラスを使って取得 from pynq import allocate import numpy as np buffer = allocate(shape=(16*1024*1024,), dtype=np.uint8, cacheable=0) #物理アドレスを表示してみる。0x18100000と表示される pl_buffer_address = hex(buffer.physical_address) print(pl_buffer_address) #MMIOを使ってgpio0-2のアクセスする準備をする。dual構成としたのでrangeをいずれも16とした。 from pynq import MMIO #mmio_0=MMIO(int(PL.ip_dict["axi_gpio_0"]['phys_addr']),16,True) #port2を使っているので16を設定 #mmio_1=MMIO(int(PL.ip_dict["axi_gpio_1"]['phys_addr']),16,True) #port2を使っているので16を設定 #mmio_2=MMIO(int(PL.ip_dict["axi_gpio_2"]['phys_addr']),16,True) #port2を使っているので16を設定 mmio_0=MMIO(int(PL.ip_dict["axi_gpio_0"]['phys_addr']),16) #port2を使っているので16を設定 mmio_1=MMIO(int(PL.ip_dict["axi_gpio_1"]['phys_addr']),16) #port2を使っているので16を設定 mmio_2=MMIO(int(PL.ip_dict["axi_gpio_2"]['phys_addr']),16) #port2を使っているので16を設定 #バイナリファイルを確保した領域へロード(0x1aeaサイズ) a = np.fromfile("/home/xilinx/try_python/0x1aea.bin", np.uint8).reshape([0x1aea, ]) buffer[0:0x1aea] = a #レジスタの設定 tranfer_size=0x60000 mmio_0.write(0,buffer.physical_address) #source mmio_0.write(8,(tranfer_size+(buffer.physical_address))) #dest mmio_1.write(0,tranfer_size) #size #起動前のstatusリード。両方とも0である status = mmio_2.read(0) print(status) rlast_cnt = mmio_2.read(8) print(rlast_cnt) #DMA起動 startの立ち上がりエッジで動作を開始するように設計している #LED[0]が一瞬点灯する(statusと繋げている) mmio_1.write(8,1) #start=1 mmio_1.write(8,0) #start=0 次回起動の為に初期状態に戻しておく #statusを再度、読む。rlast_cnt=12288 となる。これはRLASTを計数したもの。12288*32=0x60000で設計通りの値 status = mmio_2.read(0) print(status) rlast_cnt = mmio_2.read(8) print(rlast_cnt) #書き込めたかチェック for i in range(0x1aea): if buffer[i] != buffer[tranfer_size+i]: print("Err adr="+hex(i)+", source="+hex(buffer[i])+", dest="+hex(buffer[tranfer_size+i])) #上記の結果。bit2="H"を示すアドレスに書き込めていない。 #Err adr=0x4, source=0x4, dest=0x0 #Err adr=0x5, source=0x5, dest=0x0 #Err adr=0x6, source=0x6, dest=0x0 #Err adr=0x7, source=0x7, dest=0x0 #Err adr=0xc, source=0xc, dest=0x0 #Err adr=0xd, source=0xd, dest=0x0 #Err adr=0xe, source=0xe, dest=0x0 #Err adr=0xf, source=0xf, dest=0x0 #Err adr=0x14, source=0x14, dest=0x0 #...以下同様 #
と、こういう感じで期待値と合いません。
ILAを使ってAXI信号をモニタしてみる
一体何が起きてるんだ。という事で、DMA回路のAXIマスターI/FとZYNQのHP0にAXIデバッグモニタ(ILA)を仕込んでベアメタル時の動作とpythonから呼び出した時の信号の違いを確認する事にします。
ILAの仕込みは簡単で、vivadoに戻ってblock diagramを再度開き、DMA回路のAXIのネットを選択、右クリックでDebugをチェックします。緑の虫マークがついて、Run Connection Automationがクリック出来る様になるので指示に従いクリックします。するとILAが自動インスタンスされます。この要領でHP0側も行います。system_ilaはSLOT_0とSLOT_1が生成されます。
後はvalidate designを行いGenerate Bistreamします。
DMA回路の起動直前に、Open Hardware Managerをクリック。Triger Setupのwindowが出てきたらトリガとなる条件(今回はARVALIDの立下りとした)を入力、runさせるとトリガ待ち状態になるので、DMAを起動させればよい。
ベアメタルで動作させてAXI信号を取得した場合の波形(上)と、Pythonから起動させた場合の波形(下)を下記に示す。
図の通り、PythonからDMAを動かした場合のRDATAが2cycに1回しか変化していない。ただ、この図の通りDMAがRDATAを受けたとしたら、読み出しデータは0x03020100→0x03020100→0x0a0a0908→0x0a0a0908→…と2回づつ同一データを取り込むはずであるが、pythonコードで得られたデータは0x03020100→0x00000000→0x0a0a0908→0x00000000→…であったので、波形とは動作が異なっている。Pythonで得たような値になるにはWSTRB=0xf→0x0→0xf→0xf...とならなければならない。ライトchの波形を示す。
WSTRBは0xf固定でした。WSTRBが波形通り動いているなら0x03020100→0x03020100→0x0a0a0908→0x0a0a0908→…になるはずなんですが。
最後にインターコネクトを通過した後のWSTRBも見てみます
WSTRBは変わらずずっと0xfですね。WDATAはDMAマスタ側の波形と同じです。インターコネクタ側で信号をいじられているわけでもなさそうです。
そもそもベアメタルで動作させた場合と、Pythonから動作させた場合で同一bitstreamにも関わらず動きが変わっていることが謎です。やっぱりキャッシュが悪さしているのかなぁ...それが一番怪しいですよねぇ。でもcacheble=0でメモリを確保しているのですが。
以上、pythonからPLを動作させて、DDRをlinuxで共有するという目的は果たせませんでした。MMIOでレジスタを設定したりは出来る様になったのですが。おしいです。後一歩です。
尚、この後、sampleにあるDMAを動作させましたが正常に動作したことをご報告しておきます。gitに上げられているtclから回路を合成させました。
キャッシュ以外でsampleにあるDMAとの違いは以下だと思います。
- バス幅
- AXIの使用プロトコル種(sampleのはAXIstreamらしい)
この辺をsampleのDMAと合わせてトライすることくらいしか現状思いつかずです。
→バス幅を64bitとすることで回避できました。次回の記事に記載します。
以上です。
参考にしたドキュメントやweb