かずの不定期便ブログ

備忘録代わりに書きます

Arduino向けILI9486 LCDパネルを使ってラズパイで動画再生(ソフト作成編その1)

前回の仕様確認編からの続きになります。
spend-carefree.hatenablog.com

今回は:

  • ラズパイを使ってどのようにLCDモジュール(ILI9486)を駆動すればよいのか?
  • ArduinoLCDシールド→ラズパイ変換基板の作成
  • ラズパイGPIOの性能
  • LCDモジュールに画像を表示させるのに利用するコマンド
  • LCDモジュールの駆動する関数
  • bmp画像を表示するプログラム

を見ていきたいと思います

目次

  • LCDモジュール(ILI9486)の駆動方法
  • LCDモジュール <-> ラズパイ 変換基板の作成
    • ラズパイと変換基板を合体
    • 変換基板とLCDモジュールとラズパイ
  • GPIOの性能
    • ラズパイのGPIOの駆動の仕方
    • 性能測定プログラム
    • ロジアナで取得した波形
  • LCDへの画像表示方法
    • コマンドリストの選定
    • 分からない部分はarduinoのドライバのソースコードから設定値を引用
    • 画像データの送信
  • LCDモジュール駆動関数(c++)
  • LCDモジュール初期化部分~1pix分だけライト(c++)
  • BMP画像表示プログラム
  • 参考
続きを読む

Arduino向けILI9486 LCDパネルを使ってラズパイで動画再生(仕様確認編)

目次

  • 目的
  • 購入したモジュール
  • LCD駆動マイコンの選定
  • 購入したLCDモジュール
  • LCDモジュールの動作確認
  • LCDモジュールの仕様確認
  • ILI9486のデータシート
  • 端子一覧(抜粋)
  • タイミング

目的

オリジナルドライバでLCDに動画を表示させること
下記の記事に触発され、自分ドライバで動画を再生したくなり、Arduino向けとして安価で売られているLCDシールドを買いました。(Aliexpressにて1000円程度でした)
【マイコンの限界に挑戦】小型液晶に動画を表示してみた【16bit】 - ニコニコ動画

続きを読む

X68K DMAC(HD63450)がバスマスタの時の格安ロジアナによるバス観測 その2


目次

  • ASX信号をソケット側でプローブする事で見えます
  • DTACKX、ASX信号はソケット側とMPU側でタイミングに違いがあるのか

 第二弾になるのですが、前回、DMAがマスター時のASXが観測できていなかったので、再取得版です。

ASX信号をソケット側でプローブする事で見えます

 前回、下記記事にてDMAC(HD63450)がバス権を取得した際の波形をロジアナで観測したのですが、DMACにバス権が移動した後のASXが"L"になってませんでした。これはXellent30sボード側でMPUの信号を一旦バッファしているので、"H"になってるのではないかと予想しました。
spend-carefree.hatenablog.com

続きを読む

X68K DMAC(HD63450)がバスマスタの時の格安ロジアナによるバス観測


目次

  • X68K DMAC(HD63450)のバス権取得の様子
  • 波形説明
  • DMACがバスマスタ時の区間の説明
  • DMACのアクセス間隔
  • 信号の説明

X68K DMAC(HD63450)のバス権取得の様子

格安ロジアナを使って68KコアへDMAC(HD63450)がバス権を取得する様子を見てみました。
24MHzスペックのロジアナなので10MHzクロックのバス(Clockは20MHzレートになる)を観測するには足りない性能ですが、思っていたよりも分かるように取得できています。

f:id:spend-carefree:20220130233647p:plain
DMACがバス権取得しているところ
続きを読む

高位合成ブロックを手書きのRTLから制御する


目次

高位合成について

xilinx社のVitis HLSではC言語からRTLを生成する高位合成が可能です。
#2020.1版の場合です。これまで僕が使ってきた2016.4版ではvivado HLSで可能です。
ちょっと複雑な演算を行う場合には、RTLを手書きするより、C言語で記載した方が簡単ですし、検証が非常に楽ちんになります。

 しかし、良い面ばかりではなくどのような回路になるかを想像しないでCプログラミング♪って感じで記述すると、出来上がりRTLが思っていたより回路サイズが大きい、パイプライン段数が深いとか、そもそも合成できないという羽目になります。ただこの辺りはちょっと慣れればほぼ解消出来ると思います。

 Vitis HLSを使った設計では、ハードウェアをCの関数として作成します。このハード化された関数を簡単に呼び出せる仕組みをXilinx社は用意してくれています。今回はこのフローは使わないで、高位合成エンジン部分だけを拝借して、出来上がったRTLと直接インターフェースする方法を記載したいと思います。

高位合成ブロックのインターフェースについて

「vitis 高位合成ユーザー ガイド ug1399」 (ug1399で検索するとヒットします)
本ドキュメントを読めば分かるようになってはいるのですが、実際に作ってSIMした波形を見てからドキュメントを読む方がよくわかりました。
簡単なものを例に挙げてSIM波形と共に記載します。

 インターフェースは、いくつかの種類が用意されています。C言語の段階でdirectiveとしてどのようなインターフェースで繋ぐかを指示します。実際に合成し、SIMを行う事で波形を観測してプロトコル仕様を確認しました。
 尚、ug1399ドキュメント以外にFPGAプログラム大全に代表的な動作が記載されていて大変参考になりました。

 インターフェースは、ブロック制御用の信号とポート制御用の信号に分かれます。前者は回路全体の動き(開始、終了)を制御、後者はポートに対する制御になります。

 プロトコルは有効区間を示すvalid信号が存在するもの(標準)、有効区間が特に示されないもの(無手順)、SRAMとのインターフェースがあるようです。ここでは標準のインターフェースについて記載します。(手設計のRTLとの結合には標準を使うのがやりやすそうだったためです。)

 まずは最もシンプルなIOになるように、ポートに対しdirectiveを施さない場合の波形を載せます。入力ポートは構造体St_AffineMAT型とap_unit<16>型です。出力ポートはap_int<32>型のポインタです。出力側は、sx_ap_vldやsy_ap_vld信号で出力有効期間を示す信号がありますが、入力側のdxなどの信号にはvld信号はありません。
 Block-level IO Handshakeを見るとap_startに同期して、dx信号が入力されているのが分かります。ポート(引数)に対しなんのdirectiveを与えない場合には回路全体の信号であるBlock-level信号でやり取りされています。テストベンチはCのコードですが、vitisが論理SIMを行えるように勝手にテストベンチを作成してくれます。なので、ここでは"入力されている"という書き方をしています。実際に手設計のRTLと繋ぐ際にはdx信号を作らないといけません。

高位合成ブロックのインターフェースのSIM

 Cソースコードと波形を示します。テスト階層は(長いので)省略します。テスト階層の方がずっと複雑です。

// ソース1
void affine_transform (

    St_AffineMAT affine_mat,
    ap_uint<16> dx,     // dest x
    ap_uint<16> dy,     // dest y
    
    // source座標
    ap_int<32> *sx,
    ap_int<32> *sy
    )
{
#pragma HLS PIPELINE

    *sx = (affine_mat.mat_a * dx + affine_mat.mat_b * dy + affine_mat.mat_c );
    *sy = (affine_mat.mat_d * dx + affine_mat.mat_e * dy + affine_mat.mat_f );
}
f:id:spend-carefree:20210708234058p:plain
directive無しの場合のポート波形

 ap_start="H"入力で入力ポートの信号が確定されています。同時にap_idleが"L"出力になります。(組み合わせ回路のみでap_idle信号は作られているようなので,RTLで駆動する事になるap_startはFF出力で作成する方がタイミングviolationに悩まされないと思います)本回路はlatency=4cycで作成されたので4cyc後にap_doneが出力され、出力信号sx,syが確定しています。
 なので、後段側のRTLを作る際には(ap_done&sx_ap_vld)="H"時にデータを取り込めばよさそうです。
 またdirectiveでPIPELINE指示をしています(ソースコードのpragma行を参照)ので、毎サイクルデータの入力が可能で、ap_doneが一度"H"になれば毎サイクル演算結果が得られます。
 本回路を組み込む際に気を付けないといけないのは、毎サイクル有効データが出力されるというところです。後段のRTL側の都合で、受け取れないタイミングの時には本回路(HLS)を停止させないと演算結果を取りこぼしますし、また毎サイクル入力信号を供給できるとも限らないので、やはり本回路(HLS)を停止させたいです。
 Block-level信号のap_start="0"入力にすれば回路は停止しそうです。実際にSIMして動作を確認します。ap_startを任意のタイミングで信号を入力したいので、今度はvitisは使用しないで、テスト階層のRTLを作成し、HLS合成されたverilogを読み込んで、vivadoでSIMを行います。

f:id:spend-carefree:20210710231743p:plain
SIM結果1 ap_start="0"入力

この様にap_start="0"を入力すると4cyc(latency cyc)後にap_doneが"L"へ変化し、HLS回路は停止します(出力が止まります)。

f:id:spend-carefree:20210710231829p:plain
SIM結果2 再度ap_start="1"入力

再びap_start="1"入力すると、4cyc後にap_doneは"H"出力へ変化し、有効データが出力されました。

 これらの波形から分かることはSIM結果1に示すようにap_start="0"入力させることで停止はしますが、即停止ではなく、レイテンシ4cyc遅延の後に停止しています。つまり、後段側の回路から停止要求(ap_start=0)を出しても,4cyc分は出力値をロストしないようにfifoのような受け皿が必要です。そして後段回路は何らかの理由(例えばAXIのreadyがネゲートされたなどの理由)で処理が続行ができない為に停止指示を出しているので、処理が再開できるようになった時点で、ここで受けたfifoに入っているデータをまず処理しないといけません(fifoの処理を行った後で、ようやく前段HLS部に再開指示が出せます)。
 次に再開時の下段側の波形を見ると、ap_start="1"入力後、レイテンシ4cyc遅延後に演算結果が出力されます。つまり後段回路側からHLS部へ停止指示を出すたびにレイテンシcyc分、HLSブロックを遊ばせることになるので注意が必要です。たとえば、停止指示はレイテンシより十分長い時間発生しないというようにし、レイテンシ分の停止時間が全体の処理時間に対し無視できるように設計する事が望ましいです。
 まとめると、以下の3点の注意が必要だと思います。

  1. ap_start=0入力した際に、レイテンシcyc後まで、演算結果が出力されるので、演算結果をロストしないように最低、レイテンシサイクル数分バッファ受けする。
  2. HLSブロックの再開は、上記バッファ受けしたデータを処理した後で行う。
  3. "HLSレイテンシサイクル" x "後段回路からHLSブロックへの停止指示回数サイクル分"の時間、HLSブロックから処理結果を受けられないので、この時間が全体処理時間と比べて十分小さい事、もしくは長くても問題のない性質の処理に適用すること。

高位合成ブロックのインターフェースの改善

 ちゃんと作るには割と面倒な気がしますが、実は上記で上げた注意点を気にしなくても良いインターフェースも用意されています。
#最初から説明しろと思うかと思いますが、シンプルなところから記載したかったのです。
 ap_ctrl_chainというブロックレベルインターフェースです。ug1399ドキュメントによると「ap_ctrl_hs と似ていますが、バック プレッシャーを適用するために入力信号ap_continue が追加されている」とあります。上記で挙げた3点の注意を気にしなくてもよくなりそうです。
 下記図のようにDirectiveのタブで関数名で右クリック→Insert Directive...でDirectiveをINTERFACEとしOptionsのmode(optional):でap_ctrl_chainを指定します。
f:id:spend-carefree:20210711231837p:plain
ソースコードは以下の様に「#pragma HLS INTERFACE ap_ctrl_chain port=return」が追加されます。

// ソース2
void affine_transform (

    St_AffineMAT affine_mat,
    ap_uint<16> dx,     // dest x
    ap_uint<16> dy,     // dest y
    
    // source座標
    ap_int<32> *sx,
    ap_int<32> *sy
    )
{
#pragma HLS INTERFACE ap_ctrl_chain port=return  // ←追加
#pragma HLS PIPELINE

    *sx = (affine_mat.mat_a * dx + affine_mat.mat_b * dy + affine_mat.mat_c );
    *sy = (affine_mat.mat_d * dx + affine_mat.mat_e * dy + affine_mat.mat_f );
}

これでvitisによるCOSIMで生成される波形を見るとBlock-level信号にap_continueが追加されています。

f:id:spend-carefree:20210711232735p:plain
SIM結果3 ap_continue付き

ap_doneのタイミングでap_continue="1"が入力されています。これを任意のタイミングで"0"入力をしてみてどのようなタイミングで出力が変化するか確認します。(vivadoへもっていきます)

f:id:spend-carefree:20210712215055p:plain
SIM結果4 ap_continue=0入力によるバックプレッシャ

本SIM波形から、ap_continue="0"入力で即座に回路が停止していることが分かります。これを利用すれば、上記で挙げた3つの注意は不要になります。

 次に、入力ポートと出力ポートにフリップフロップを付けるように修正します。繋げるRTLとの境界のタイミングが緩くなります。
 ソースコードのDirectiveタブで各ポートに対し右クリック→Insert Directive...でDirectiveをINTERFACE、modeをap_hs、registerのチェックボックスをONにします。
 Latencyが2cyc長くなりますが、ap_continueの動作が変わってないか念のために確認します。pragmaを載せます。

#pragma HLS INTERFACE ap_hs port=sy register
#pragma HLS INTERFACE ap_hs port=sx register
#pragma HLS INTERFACE ap_hs port=dy register
#pragma HLS INTERFACE ap_hs port=dx register
#pragma HLS INTERFACE ap_hs port=affine_mat register
#pragma HLS INTERFACE ap_ctrl_chain port=return
#pragma HLS PIPELINE
f:id:spend-carefree:20210712232817p:plain
SIM5 portにregisterを追加

 先ほどとは異なりap_continue="0"入力後、停止までのサイクル数が1cyc増加しています(タイミングが変わるとは思っていなかった)。バッファレスとはいかないので気を付けないといけないです。
 また、入力側にregisterを追加した為か、入力ポートにインターフェース信号xxx_ap_vld,xxx_ap_ack信号が追加されました。
 xxx_ap_ack信号が"H"出力されるまで、入力信号を受けてくれませんが、リセット解除と同時に"H"アサートされ、ap_continue="0"入力でデアサートされる信号なので、ポート毎に制御する事がないのであれば(ほとんどないと思うが、非同期に動く回路がそれぞれの入力ポートを制御する場合に活用?)、Bloclk-level信号の活用のみで特に問題はなさそうです。xxx_ap_vldはap_startと同じ信号を繋いでおけば問題はないと思います。

構造体

 突然話が変わりますが、本ソースの入力は構造体"St_AffineMAT"を利用しています。高位合成を行ってRTLを生成すると、構造体affine_matは、
input [127:0] affine_mat;
とフラットになってたりするのですが、構造体は以下の様に定義しています。

typedef struct {
    // 固定小数8bit 行列式
    ap_int<16> mat_a;
    ap_int<16> mat_b;
    ap_int<16> mat_d;
    ap_int<16> mat_e;
    ap_int<24> mat_c;
    ap_int<24> mat_f;

} St_AffineMAT;

 不思議ですよね?定義している構造体の合計ビット数は108bitなのに128bit化されているので。。。
 RTLと接続するためには、どのビットにどの信号が割り付けられているか分からないと結線が出来ません。このあたりも「ug1399ドキュメント」しっかり記載されています。基本的に8bit,16bit,32bitに構造体の変数は切り上げられパディングされます。また構造体内の変数定義の上から順にLSBから埋められる様です。更に構造体全体のサイズは32bit単位になります。本例の場合は以下の様なassignになるようです。今回は16bit変数が偶数個だったので128bitにきれいに収まりましたが、もし16bit変数が5個だった場合は32bitの倍数になるようにMSBに16bitが追加されて96bitになるようです。

assign affine_mat = {
    {8'h00,mat_f},  // 8bitパディングして32bit化
    {8'h00,mat_c},
    mat_e,
    mat_d,
    mat_b,
    mat_a
    };

全体ブロック図

 今までの事を踏まえると以下の様な構成にすればよいかと思います。

f:id:spend-carefree:20210718173915p:plain
ブロック図

以上で、HLSブロックにRTLを接続する最低限必要な情報は揃いました。

HLSブロックとRTLの結線

 vitis HLSからExport-RTLでIP化して、vivadoのblock-diagramでIP読み込みをして結線するのが王道ではあると思います。
 しかしながら手書きRTLとは結線が出来ませんでした。IPの方は接続相手もインターフェースであることを期待しているようで、端子むき出しのRTLとは結線が出来なかったのです。もしかしたらAXIインターフェースの様にネーミングルールがあってそれに従ってRTLの端子名を決めれば出来るのかもしれません。
 なので、IP化は行わずHLS合成で生成されたRTLを直接取り扱うという方法を取りました。合成も問題なく出来ているので方法としてはありなのかと思っていますが、本当のところは分かりません。実はFPGAへ焼く段階までは進んでいなくてSIM&合成が可能なところを確認して作業を止めています。
 vitis HLSでIP化した方が、vivadoでの合成時間短縮にはなると思います。

最後に

 一応、これでなんとなくHLSを使えるようになりました。本当は王道でHLSで作ったものだけで、FPGAへ焼いてpythonから制御というやり方が、色々環境が整備されていて便利で高度な事が出来そうですが、このような環境を使うだけのネタがないといいますか。。。AIエンジンネタが巷にあふれているのですが、そもそもAIをよく分かってないので何か試すこともできずでして。AIの勉強をしたいなと思っているところです。
 いきなり最後は説明が雑になった感がしますが、個人備忘録としてはよいのではないかと思っています。


以上です。
久しぶりの更新でした。

PYNQしてみる。動きました!

目次

PYNQ-Z1ボードなのでpythonからPL部を動作させたい

 pythonからPL部のロジックを動かす事が目的ですが、ただ動かすだけではなく、DDRメモリをPL<->PS間で共有メモリとして使えるようになることが最終目標です。
 前回記事で動作しないと悩んでましたが、AXIのバス幅を32bitから64bit幅とすることで動作しました。
前回記事↓
spend-carefree.hatenablog.com

AXIバス幅を64bitへ改造する

 前回記事で、sampleで使われているDMAと今回作成したDMAの動作の違いは以下の2点であると書きました。

  1. バス幅が32bitである
  2. AXI Streamではなくバースト転送32byteの普通のAXI転送である

1.側を変更してトライです。sampleで使われているAXIのバス幅と同じにして64bitにして再設計しました。
 修正点は以下の通りです。

  1. AxSIZEを4byte→8byteにするため、3'b010→3'b011へ修正
  2. WDATA,RDATAを64bitへ。本信号に繋がる信号をすべて64bit化
  3. AxADDRの生成部をトランザクションのたびに+0x20していたのを+0x40へ変更
  4. transaction数を数えるカウンタのreg名をcnt32→cnt64
  5. 転送終了条件を(cnt32==size[23:5])→(cnt64==size[23:6])とLSBを見ないように修正(バースト単位が32byte→64byteと倍化したので)

ソフトウェア面(Pythonコード)の変更は不要です。
修正したRTLは以下からダウンロードできます。

  • DMA本体

 https://drive.google.com/file/d/1DQAhD0ZNwG3PW8Mic23b8pt18zvgPyXu/view?usp=sharing

  • vivadoから読み込むためのラッパー回路

 https://drive.google.com/file/d/1KueoX3HntIu8GhKW6FFT49CnrgyuOKVI/view?usp=sharing

Pythonから動かしてみる

 vivadoを起動し、RTLを更新します。Block diagramからZYNQをダブルクリックしてHP0ポートのバス幅を64bitへ変更します。
 Validate design → Generate Bitstream します。
 PYNQボードを動かし、sambaマウントして、先ほど作成した「axi_copy.bit」、「axi_copy.hwh」の2ファイルを更新します。
 同じPythonコードを動作させます。
 なんということでしょう!今回は不一致Errが出ません。

ILAでAXI信号を観測する

 一応、正常状態になったかを確認します。

f:id:spend-carefree:20201027233127p:plain
AXIバス幅を64bit化してPythonから起動

 ILAの波形を見ると分かるように普通に動いてます。

考察

 32bitがダメというのは考えづらいので、やはり自分の何かが悪いのだろうと思います。直接的な原因は64bitではない気がしてて、
 一番怪しいのはAxCACHEだと思います。現設定は4'b0011です。
 AxCACHE[1]はアップサイズ許可なので内部が64bitで構成されてても、"H"設定にしていればバス幅変換されるので特に問題はないはずです。
 AxCACHE[0]は"H"にしてるのでbufferableになって問題ない気がします。
 後はAxCACHE[3:2]の設定ぐらいしか思いつかず、とりあえずAxCACHE=4'b1111で動作させましたが状況は変わらずでした。
 色々サイトを探したら、以下の様な記事が見つかりましたがACPポートのお話のようです。
Zynq の ACP を使う時の AxCACHE 信号とAxUSER 信号の値 - Qiita
 原因、つかめずです。とりあえず64bitで作ればよいということで追及はこの辺りでやめにします。

以上です。

参考にしたドキュメントやweb

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になるようにコンフィグします。

f:id:spend-carefree:20201025181030p:plain
ZYNQ HP0コンフィグ

 次に先ほど読み込んだ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の自動配線を間違うかもしれません。まぁ修正は簡単ですが、若干パニクるかもです。

 以上で出来上がったブロック図が以下です。

f:id:spend-carefree:20201025181724p:plain
全体ブロック図

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します。(以前のように自動でやってくれなくなった)

f:id:spend-carefree:20201025202745p:plainf:id:spend-carefree:20201025202759p:plain
xsaファイル、Build

 次にCプロジェクトを作成します。File→New→Apllication project (正直、何を作らされてるのかさっぱり分からないです)
 先ほど作成した時と同じようなGUIが出ます。こちらはCのプロジェクトです。C_debugとしました。

f:id:spend-carefree:20201025204043p:plain
C_debug

platform選択が出るので、先ほど作成したproject nameを選択します。

f:id:spend-carefree:20201025204203p:plain
platform選択

Domainを聞かれるのでStandaloneを選択します。するとようやくデバッガの画面が出ます。

f:id:spend-carefree:20201025204614p:plainf:id:spend-carefree:20201025204634p:plain
domain&IDE

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が生成されます。

f:id:spend-carefree:20201025234827p:plainf:id:spend-carefree:20201025234838p:plain
ILAインスタンス

後はvalidate designを行いGenerate Bistreamします。
 DMA回路の起動直前に、Open Hardware Managerをクリック。Triger Setupのwindowが出てきたらトリガとなる条件(今回はARVALIDの立下りとした)を入力、runさせるとトリガ待ち状態になるので、DMAを起動させればよい。

f:id:spend-carefree:20201025235221p:plain
ilaトリガ設定

 ベアメタルで動作させてAXI信号を取得した場合の波形(上)と、Pythonから起動させた場合の波形(下)を下記に示す。

f:id:spend-carefree:20201026230017p:plain
ベアメタル ILA波形
f:id:spend-carefree:20201026230726p:plain
Python ILA波形

 図の通り、PythonからDMAを動かした場合のRDATAが2cycに1回しか変化していない。ただ、この図の通りDMAがRDATAを受けたとしたら、読み出しデータは0x03020100→0x03020100→0x0a0a0908→0x0a0a0908→…と2回づつ同一データを取り込むはずであるが、pythonコードで得られたデータは0x03020100→0x00000000→0x0a0a0908→0x00000000→…であったので、波形とは動作が異なっている。Pythonで得たような値になるにはWSTRB=0xf→0x0→0xf→0xf...とならなければならない。ライトchの波形を示す。

f:id:spend-carefree:20201026232337p:plain
Python ILA波形

 WSTRBは0xf固定でした。WSTRBが波形通り動いているなら0x03020100→0x03020100→0x0a0a0908→0x0a0a0908→…になるはずなんですが。
 最後にインターコネクトを通過した後のWSTRBも見てみます

f:id:spend-carefree:20201026233705p:plain
Python ILA波形(インターコネクト側)

 WSTRBは変わらずずっと0xfですね。WDATAはDMAマスタ側の波形と同じです。インターコネクタ側で信号をいじられているわけでもなさそうです。
 そもそもベアメタルで動作させた場合と、Pythonから動作させた場合で同一bitstreamにも関わらず動きが変わっていることが謎です。やっぱりキャッシュが悪さしているのかなぁ...それが一番怪しいですよねぇ。でもcacheble=0でメモリを確保しているのですが。

 以上、pythonからPLを動作させて、DDRlinuxで共有するという目的は果たせませんでした。MMIOレジスタを設定したりは出来る様になったのですが。おしいです。後一歩です。
 尚、この後、sampleにあるDMAを動作させましたが正常に動作したことをご報告しておきます。gitに上げられているtclから回路を合成させました。
 キャッシュ以外でsampleにあるDMAとの違いは以下だと思います。

  • バス幅
  • AXIの使用プロトコル種(sampleのはAXIstreamらしい)

 この辺をsampleのDMAと合わせてトライすることくらいしか現状思いつかずです。
→バス幅を64bitとすることで回避できました。次回の記事に記載します。

以上です。

参考にしたドキュメントやweb