today 2020-02-02
access_time 10 mins
ただ音をBypassするだけのデザインで小手調べ
最近ふとネットサーフィンをしていたら、PYNQ-Z2にAudio Codecが乗っていることに気がついた。
http://www.tul.com.tw/ProductsPYNQ-Z2.html
PYNQ-Z1が出たときはかなりオーディオはチープというイメージを受けていたので、これには感動してつい購入してしまった。 これを使いこなすために調べた内容と、音をBypassするイメージをPYNQのベースデザインに追加でインプリして動かした備忘録である。 ググれば比較的ある情報にはあまり触れてないので適宜調べるかdocを参照してほしい。
何も伝わらないので概要です pic.twitter.com/TGOYiNxgKF
— かみや (@kamiya_owl) January 30, 2020
実行したJupyter Notebook
Github kamiyaowl/pynq_dsp_hw/dist/bypass/Bypass.ipynb
おおよそは以下の手順通りで行けた。
https://pynq.readthedocs.io/en/latest/getting_started/pynq_z2_setup.html
気になる点は以下の通り。
SanDiskの速そうなパッケージのやつにしたら動いた。Boot後はデフォルトイメージがコンフィグされてそこらへんのLEDが一斉に点滅するので確認に使うと良い。
以下の写真の状態ではコンフィグされていない。
リングフィットアドベンチャー pic.twitter.com/382EZ8S2bQ
— かみや (@kamiya_owl) January 25, 2020
DHCPも標準で探してくれるようになっており、netbiosでの名前解決ができるのでhttp://pynq でもアクセスできた。
おおよそ以下の構成のようだった。
~/pynq
は/usr/local/lib/python3.6/dist-packages/
のpynqからシンボリックリンクがはられているので弄ると反映される。
C++で実装された部分も~/pynq/lib/_pynq
にある。おいてあるmakefileでビルドできるので出来上がった*.so
で既存の*.so
を上書きすれば良い
なるほどね、完全に理解した(なにもわかってない pic.twitter.com/eOJASRo8jr
— かみや (@kamiya_owl) January 25, 2020
Jupyterの自動起動はsystemdに登録されているだけ
/usr/bin/jupyter-notebookが登録されていたか pic.twitter.com/rTorSt9Qa1
— かみや (@kamiya_owl) January 25, 2020
自分でいじったライブラリrepoに差し替えたりが少しやりづらいなぁと感じたり感じなかったり…
ADAU1761というADC/DACの乗った俗に言うCODECが実装されており、I2SとI2CがFPGAと直結されていた。
/boards/ip/audio_codec_ctrl
に実装があるが、AXI4経由で先頭から4byteずつRX_L
, RX_R
, TX_L
, TX_R
, Status
が公開されていた。
Status
には受信データがReadyになっているとビットが立つようだった。
その他I2C経由の設定はC++のaudio_adau1761.cpp
の実装で各種設定しているようだった。
xi_lite_ipifから叩かれているのはここか、やっぱりdata_rdy_bitだけアサインされてそう pic.twitter.com/8O4rQnBC5K
— かみや (@kamiya_owl) January 26, 2020
少し罠なのはpynqライブラリのC++実装にあるaudio_adau1761.cpp
を見ればわかるのだが、Bypass関数を呼んだとき以外はADAU1761にI2Sでデータを送ってもIC内臓のMixer3/4とボリュームによって結局ミュートされてしまうよう実装されていた。
新しい関数I/Fを生やすのも面倒なので、Line入力に設定した時点で上記設定をするように修正した。 これでPythonからでも受信レジスタの値を送信レジスタに書いてあげればループバックが実現できる。
まずは既存のデザインを自力で論理合成してみる。現在時点ではVivado 2019.1向けに書かれたtclなので2019.1を入れた。
適当なprojectを作って/boards/Pynq-Z2/base/base.tcl
を実行するのだが、私の環境かWindowsのせいかわからないが作業Directoryが~/AppData/....
あたりに飛ばされて解決できなかったのでIP Packageの登録だけ手動でやった。
以下の通りbase.tclをいじって、/boards/ip
にいるIPは事前にVivadoのGUIから手動で追加しておいた。
☑base.tclのご機嫌をとった pic.twitter.com/Qhpl487OmB
— かみや (@kamiya_owl) January 26, 2020
あとはBlock Designのwrapperを作って合成を進める正規の手順でbitstreamが生成できた。
今のPynqのOverlayライブラリは.bit
, .hwh
, .dtbo
(DeviceTreeが変わる場合のみ)を必要としているようだった。
<project-root>/<project>.runs/impl_1/<top_file_name>.bit
と<project-root>/<project>.src/source_1/bd/base/hw_handoff/base.hwh
に配置されていたのでこれを利用した。
先の生成物をPynqにコピーして、以下のコードをJupyterあたりで実行すれば無事同じように動作できた。 overlay.pyとか周辺を読む限り、bitファイルのファイルパスをもじってhwhを取得しているようだった。
わーい、自前で生成し直したbitstreamでも昨日のADCの入力を取れるようになった!https://t.co/F985jGegW0 pic.twitter.com/FJYQq5fW34
— かみや (@kamiya_owl) January 26, 2020
割とここからが本題。Verilogを書く気分でもなかったのでVivado HLSで作ったデザインを先のデザインに追加して動作させる。
正直audio_codec_ctrl
に変わるものをまるごと作っても良かったが、既存のものを生かしていくのも大事なので割愛。
先程Pythonで行っていた受信データを送信レジスタに書き戻すだけの処理をHLSで行う。エフェクトではないのでどちらかというとまだオレオレDMAといった感じ。このエフェクトをBypassと呼ぶことにする。
まずはCPUから制御することも考慮して以下の仕様を検討した。
audio_codec_ctrl
のベースアドレスを指定することを想定まずは仕様どおりに動くC++の実装を行う。C++で実装するとap_int
/ap_fixed
などが任意ビット幅で利用できるので便利。
特筆する必要のある処理はないが、physMemPtrが4byte単位で進むことに注意する。
最低限シミュレーションはしておきたいので書いた。気を使ったポイントはstatusが立っていないときはTXに何も書かないこと、basePhysAddrを書き換えるとアドレスオフセットをきちんと考慮できるとか。
ここが悩みポイントだが基本的に使用を満たせるように設定する。
特に2.項だが、関数I/Fをs_axilite
に設定するとoffset=0にap_start
, ap_done
, ap_idle
, ap_ready
, auto_restart
bitを持ったレジスタが生成される。
名前でおおよそ想像がつくが、auto_restart
ビットを立てた状態でap_start
ビットを立ててあげればfreerunしてくれる。
HLSのデザイン上でwhile無限ループを作ったり、Interfaceにap_none
に設定するような小細工は必要なかった。
参考までに、これは最終的に以下のようなI/Fで見えるようになる。s_axilite
はbundleを明示しなければポートがまとめられる。
C/RTL CoSimも動かしてみたが、想像通り動いてました。ぐらいの情報しかないので割愛。
まぁとりあえず波形出しとけばインスタ映えしそうだから意味もなく貼ります pic.twitter.com/BpheVoqcB9
— かみや (@kamiya_owl) January 27, 2020
最後にIP Packageとして出力すればVivado HLSでの作業は終了。
まずは最初に作ったプロジェクトにVivado HLSで出力したIPのディレクトリも設定して、IP Catalogから見えるようにする。 そして配置する。
この際にCPUからもアクセスできるように考慮する必要があるので以下の具合で検討した。 このモチベは出力している波形をPython上で書いたり、先程Pythonで作ったBypass実装を動かしたりするため。
— かみや (@kamiya_owl) January 29, 2020
CPUからは新たにbypassが、bypass_0からはaudio_codec_ctrlが見えるようになっているのでAXI4でのベースアドレスを指定してあげる。 基本的にはCPUから見えるベースアドレスと合わせることにした。これはBlock DesignのAddress Editorから設定できる。
あとは合成して生成物を準備する
bypass_0というペリフェラルを追加したので、FPGAに新しいbitstreamを書き込めば該当する物理アドレスにR/Wをかければ使うことができる。 ベアメタルなら該当のアドレスにアクセスをかければよいのだが、PynqではLinuxが動いているので行儀よくOSに教えてあげる必要がある。
これにはDevice Treeを記述して、これをdtc(Device Tree Compiler)で.dtb
に変換してあげる必要がある。
通常であればBoot時に読み出せる場所に置く必要があるが、Device Tree OverlayをサポートしたOSであれば必ずしも起動時にロードする必要はない。
まずはPYNQ-Z2で動いているDevice Treeを入手する。パット見リポジトリにはないので、PYNQ-Z2のボードにsshして以下コマンドで入手した。
面倒な人はこちら pynq-z2-base-origin.dts
今回作成したbypassデザインは、特に特殊なアクセスが要求されないのでuio(Userspace I/O)のドライバを当てることにした。 なにか特殊な初期化やら設定やら動きが必要であれば、自分でDevice Driverを書くことになる。
reg
には、HLSのAddress Editorで設定した値を参考に記述する。これでLinuxからbypassの存在を知ることができ、uio経由でアクセスが可能になる。
作成したDevice Tree.dts
を.dtb
ファイルにする。Pynqのライブラリ上は厳密に.dtbo
としていたのでこれに合わせた。
.bit
ファイルとファイル名を合わせておく
生成した.bit
, .hwh
, .dtbo
をまとめてPynqに配置する。
†Device Tree Overlayエグゾディア† pic.twitter.com/9lIcPj121R
— かみや (@kamiya_owl) January 30, 2020
あとはPynqのライブラリがかなりいい感じに展開してくれるので、bypassのphysMemAddr
設定を行いap_start
, auto_restart
を書きに行けば動作するはず。
あと、Overlayの引数は.dtbo
ファイルは明示しないと読んでくれなさそうだった。
audio_codec_ctrl
の設定と合わせて以下の通りだった。
これで最初のツイートにある音声Bypassを自作IPから行うことができた。
結構楽しい。
Vivadoの使い方やらLinuxのでのDeviceの扱いなどを最低限知っていないといけないので、「Pythonだけで~」というのはちょっと厳しいというのが本音。(他人のデザインを使うなら話は別) だが、Device Tree OverlayやPynqのライブラリのおかげでZynqでBootさせるOSのconfigurationで試行錯誤する時間がごっそり短縮できる点は本当に素晴らしいと思う。(本当につらい、時間もかかるし)
CPUから制御する必要のあるIPだと残念ながらAXI I/Fを持つ実装を避けることはできない。しかしAXI4のI/Fが複数あるようなデザインをRTLで書くのは初心者でなくてもなかなかつらいものがある。
Vivado HLSは関数の引数にディレクティブを指定するだけで上記を達成できるのがかなり嬉しい。今回は触れなかったが引数の値をそのままGPIOとして外に出したりもできる。
なのでap_uint<N>
の任意ビット幅指定の変数で公開すれば、何かしらの制御に使ったりすることももちろんできる。
(対象のLEがカツカツなデバイスでなければ)とりあえずFPGAに興味がある人などでも試すのは大いにありだと感じる。