2022年3月16日水曜日

PYNQとVitis HLSで作るFMA演算IP(4):おまけネタ


前回までで、FMA演算IPを作成することができました。今回はおまけ編で、データ型を変えたり並列数を増やしたりしてみます。



1.いろいろなデータ型に対応させる


 せっかくの専用回路だし、いろいろなデータ型に対応させてみましょう。今回は参考例として16bit固定小数点(q4.12)に対応させてみます。
 ...とはいうものの、高位合成用のサンプルコードはテンプレート関数で記述されているうえに、データ型はFMA.hpp内のusing data_type = float;の部分で一元的に管理しています。なので、ここの部分を固定小数点ように書き換えればいいわけです。
 Vitis HLSでは固定小数点用のライブラリとしてap_fixed.hが提供されており、これを使用して記述すればいいわけです(書き換えるとusing data_type = ap_fixed<data_width, 4>;となる)。あとは、データ幅を16bitに下げればOKです(constexpr data_width = 16;)。
  JupyterNotebookのコードについては、最初のせるのarray_typeをnp.int16、fractionを12、enable_fp2fixをTrueにすれば動くはずです。

2.AXI-Streamのデータ幅を増やす


 こちらに関しては、FMA.cppのbus_widthを書き換え、JupyterNotebookのbus_widthを書き換えます。また回路を作成する際に使用したDMA IPのStream Data Widthを指定の値にします。DMA IPの設定値からわかるように、データ幅は64, 128, 256, 512, 1024といった値しかとることができません。注意しましょう。
 データ幅を増やすことで、並列数を増やすことができます。DMAの転送能力次第な部分はありますが、そこそこの速度向上が達成できそうな雰囲気でした(160万個のデータを流して210MFLOP程度)。

3.遭遇したトラブル


  • AXI-Streamのデータ幅について、fp32+1024bitの時やfix16+256bitの場合でHLSのCoSimの段階から結果がおかしくなっていた。CSimの段階では正常な計算結果を返すし、他のデータ幅の場合とはデータ幅とデータ型以外変えていないので、まったくもって謎だった。
  • float配列とデータバスのunionを作成し、型変換を実装する予定だったが、ap_intやap_fixedをunionのメンバに入れるとコンパイルが通らなくなるのでやめた。
  • float配列とCの組み込み整数型のunionを含むコードをVivado HLSで高位合成かけようとしたところ、合成に失敗した。Vitis HLSを使うと、合成がかかった。
  • 回路が動きそうでも動かない場合、Vivadoのプロジェクトを作り直すと動く場合があった。
  • この記事を書くのに、中の人の睡眠時間とGT7のプレイ時間が削られまくった。



©2022 shts All Right Reserved.

PYNQとVitis HLSで作るFMA演算IP(3):実機での実行


 前回前々回で、とりあえ回路情報の入ったbitstreamファイルまで生成できました。今回は実機実装していきます。





1.実機のコードについて


 今回の回路構成の場合、DMAの関数に64bitの配列を渡す必要があるように見えるのですが、実際には計算で使うデータ型(今回の場合はfp32:np.float32)の配列を使用して問題ないです。ただし、配列の総サイズ(bit)が64bitで割り切れる必要があります。割り切れない場合は、ゼロパディングで埋めるといいでしょう。
 今回作成したIPはAXI-Liteで制御するのですが、開始の制御以外にループ回数の指定を行います(0x10にint32でループ回数を書き込む)。また、0x00に1を書き込むことで、IPの開始を制御できます。そのほかとしては、dmaの転送設定を自作IPの開始前に行うあたりが少し特殊かもしれないです。
 一応参考としてIPの実行時間を計測します。具体的にはメモリからデータを読み出し始めてから、書き込みが終わるまでの時間を計測します(自作IPをスタートさせたタイミングで入力データの転送が開始され、受信側のDMA IPが終了するタイミングでIPの実行も終了するため)。

2.実行結果

実行結果。140MFLOPS程度の性能であった。


 サンプルコードではCPUの計算結果との比較を行っているのですが、特段表示が出なければ一致していると思ってください。実行速度に関しては、20万回FMAの計算を実行した場合で大体140MFLOPS程度が出ているようです(実はループ回数をいじると計算回数が変化し、計算速度が変化する)。
 CPUで実行する場合と比べると...まぁ遅いといった感想になるでしょうか。クロック周波数が200MHzしか出ておらず、またFMAの計算に16*5nsかかっていることを踏まえればこの程度かなとは思っています。

3.今後


 自作のFMA演算IPを実機実装することができました。次回はAXI-Streamのデータ幅拡張や異なるデータ型への対応についての話です。




©2022 shts All Right Reserved.

PYNQとVitis HLSで作るFMA演算IP(2):高位合成と回路合成



 前回高位合成用のC++コードを作成しました。今回はそのコードを使って高位合成を行い、その流れで回路まで作成します。




1.高位合成

 今回はVitis HLSを用いてPYNQ-Z2向けに合成を行います。その際の設定は以下の通り。
  • ターゲット周波数:200MHz
  • Uncertainty:12.5%
  • ターゲットデバイス:pynq-z2(xc7z020clg400-1)
  • 言語:C++0x
 今回は2つのソースファイル(FMA.cpp, FMA.hpp)、1つのテストベンチ(FMA_TB.cpp)を登録します。このうちFMA.cppとFMA_TB.cppのCFLAGSに-std=c++0xオプションを付与します。
 合成結果はこんな感じになりました。

高位合成の結果

 Iteration Latencyは16となりましたが、Intervalが1になっていることからいい感じにパイプライン化できたようです。使用率は
  • DSP : 4%(10/220)
  • LUT:3%(1992/53200)
  • FF:1%(1718/106400)
  • BRAM:0%(0/140)
となっています。余裕ありありですね。合成されたデータフローはこんな感じでした。

データフローの一部。2か所ほど"長い棒"がみえるが、
それぞれfmulとfaddの計算ステップとなっていた。

 一応仕様通りに2並列で計算できているようです。また大半のステップがfmulとfaddで占められていることもわかりました。
 この後CoSimを走らせて結果を確認後、IPを出力します(そのまま出力してもいいのだが、Configuration...のボタンからVendor、Library、Description、Display Nameあたりをいじってからいったん出力->Solution Settings内のConfiguration Settingsのconfig_exportにあるipnameをいじってから出力すると、VivadoでIPを読み込んだ際にオリジナルの名前が付き、ベンダー名で検索がかけられる)。

2.回路合成


 高位合成で作成したIPを使って回路を作成します。ZYNQのIPを最初に置いて、Run Connection Automationを走らせるとDDRとFIXED_IOと外部端子がつながるはずです。
 その後、AXI_HPポートの0から3を有効化します。データ幅はすべて64bitにしましょう。

AXI_HPポートの設定。データ幅はすべて64bitとした。

 AXI_HPポートの設定後、IPへ供給するクロックの設定をします。今回は単一のクロックソースかつ周波数が200MHz程度なので、ZYNQのIPから供給します。Clock Configuration->PL Fabric Clocksの順にたどり、FCLK_CLK0を200MHzに設定します。

クロック設定。今回はHLSに合わせて200MHz。

 ZYNQ IPの設定が終わったら、AXI Direct Memory AccessとAXI Interconnectを4つずつ出します。
 AXI Interconnectの方はMasterとSlaveの数をそれぞれ1に設定します。
DMAの方は入力側(W, X, B)と出力側(Y)で少し異なります。入力側のDMA IPについて、Enable Scatter Gather Engineを無効化し、Width of Buffer Legth Registerを26bits、Address Widthを64Bitsにしておきます。データ入力のIPなので、Read Channelのみ有効にしておきます。Memory Map Data WidthとStream Data Widthは64にしておきます。

読み込み側のDMA IPの設定

 出力側のDMA IPについては、入力側で行ったEnable Scatter Gather Enginenの無効化、Width of Buffer Legth Register->26bits、Address Width->64Bitsの操作を同じく行います。その後、Read Channelを無効化し、Write Channelの有効かを行います。Memory Map Data WidthとStream Data WidthはAUTOのままでOKです。

書き込み側のDMA IPの設定

 最後に各IPを接続します。DMAのIPをCPU側から制御するため、個々のIPに割り当てられている名前が必要になります。サンプルコードをそのまま使いたい場合、
  • S_AXI_HP0 -> axi_dma_0(+interconnect) -> w_axi_0
  • S_AXI_HP1 -> axi_dma_1(+interconnect) -> x_axi_1
  • S_AXI_HP2 -> axi_dma_2(+interconnect) -> b_axi_2
  • y_axi_3 -> axi_dma_3(+interconnect) -> S_AXI_HP3
と接続しましょう。

IPの配線図。
Wが0、Xが1、Bが2、Yが3と覚えると分かりやすい。

 この接続を終えると、Run Connection Automationの表示が出るので、すべてにチェックを入れて実行すれば最終的な回路の完成です。この後、アドレスエディタでDMAと自作IPに対してアドレスを割り当てておきましょう。

 Generate Output Productsを実行し、Create HDL WrapperをLet Vivado manage...の方に入れておきます。Generate Bitsteramをクリックすれば回路の合成が始まります(2回に1回ぐらいで失敗するが、Output Productsをリセットしたのち、Generateしなおして再びBitstreamの生成を行えばいけるはず)。
 Bitstreamの生成が終わったら、.bitのファイルと.hwhのファイルを書きだします。画面左上のFile->Export->Export Hardwareを選択し、Platform typeをFixed、Outputを Include bitstreamに設定します。その後、XSAのファイル名を聞かれるので、適当な名前にします(サンプルコードの通りにしたい場合は、design_1_wrapperをdesign_1に変更)。出力先はどこでもよいです。
 .bitと.hwhはこのXSAファイルに入っているので、7zip等を使い取り出します。最終的にこの.bitと.hwhのファイルを.ipynbから読みだしてプログラムを実行することになります。

3.次回

.bitファイルと.hwhファイルが生成できたので、次回はPYNQ側のコードと速度計測を行っていきます。



©2022 shts All Right Reserved.


2022年3月15日火曜日

PYNQとVitis HLSで作るFMA演算IP(1):簡単な仕様策定とHLSコードの作成



 修論の提出まで終わり、無事修士課程を修了できた中の人です。修論ではとある専用計算回路を作成したのですが、時間の都合上PS-PL間の通信部に関してあまり手を入れることができませんでした。そこで今回はその通信部について、PYNQ-Z2へのFMA(積和)演算IPの実装を題材として、いろいろ試してみようと思います。
 今回のコードについてはこちらからどうぞ。



0.開発環境


  • 使用ボード:PYNQ-Z2(PYNQ v2.6)
  • Vivado:2020.1
  • Vitis HLS:2020.1
  • 高位合成言語:C++0x
  • OS:Ubuntu20.04(on WSL2)
 

1.FMA演算IPの簡単な仕様


 今回は、積和演算を同時に複数実行する演算IPを作成します。演算器自体はそこまで重要ではないのですが、今回重要となるのはPS部とIPを接続する通信部です。

 今回使用するPYNQ-Z2において、PS部とIPの通信方法はいくつかあります。どの通信方法を使うかについて少し考えます。
 まずFMAの演算器の個数よりも多くのデータを流すことを考えると、演算器を複数回使いまわす必要があります。その場合にはパイプライン化した方がより高速に実行できるようになります。パイプライン化を行う際に注意すべき点として、パイプラインに流すデータの取得が滞ってしまうとストールすることが挙げられます。
 となると、PS-IP間の通信として連続してデータを送ることができるAXI-Streamを使うとよさげなことがわかります(Xilinx提供のAXI Direct Memory AccessがPL側とAXI-Streamで接続するのも理由の一つ)。

全体のブロック図(W〇Xは要素積を表す)

 転送規格は決まったので、より詳しい部分を決めていきます。AXI-Streamのデータ幅はとりあえず64bitにします(後で広げる)。また実際に計算するデータの型を単精度浮動小数点(FP32)に仮決定しておきます。64bit幅のバスであればFP32のデータを2つ同時に流すことができるので、同時に2のFMA命令を実行できる回路を作成することにします。

自作IPのブロック図

2.HLS用のコード


 さてここからはHLS用のコードについてです(コードについてはこちら)。IP内部のブロック図を基にコードを作成すると、必要になるのが
  • slicer:64bitのデータを32bit x 2個のデータに分割する
  • array_(w, x, b, y):32bit x 2個のデータを保持する
  • fma_unit:y=wx+bを2並列同時に行う
  • packer:32bit x 2個のデータを64bitのデータに分割する
となります。array_(w, x, b, y)に関しては、C言語の配列(C++の生配列)とarray_partitionプラグマを使用します(std::arrayはHLSで合成できないので)。
 以下のコード例では配列を要素ごとに分割して実装しています(data_type=float, field_length=2)。  

data_type w_vec[field_length];
#pragma HLS array_partition variable=w_vec complete dim=0

 fma_unitはfor-loopとloop_unrollプラグマで2並列分(length=field_length=2)の演算器を実装します。
template<typename data_t, uint64_t length>
void fma(data_t w[], data_t x[], data_t b[], data_t y[]){
  for (uint64_t i = 0; i < length; i++){
#pragma HLS UNROLL
    y[i] = w[i] * x[i] + b[i];
  }
}

 slicerとpackerについては、ビットマスクとシフトを使っていい感じにスライス・パックしていくだけです。しかしながら、floatからAXI-Streamで使用するデータ型(ap_axiu)へ単純に代入した場合型キャストが発生し整数部分しか読み取られない問題や、floatのフォーマットでPSから送信されたビット列をintとして解釈し、floatにキャストした際にデータが化ける問題があります。
 これはap_axiu(ap_axis)のデータ部がap_uint(ap_int)で記述されていることに起因しており、c言語でいうところのfloatとunsigned int(int)間のキャストと同じことが発生しています。
 この対策として、変数のポインタをキャストすることで、ビット列の解釈をプログラム上で変更するコードを入れています(floatの内部表現を確認するときにやるやつ)。
inline void slicer(axi_bus_type bit_data, data_type array[]){
#pragma HLS INLINE
  ap_uint<data_bit_width> buffer, bit_mask = -1;
  ap_uint<data_bus_width> bus_buffer;
  bus_buffer = bit_data.data;
  for (uint64_t i = 0; i < field_length; i++){
#pragma HLS UNROLL
    buffer = (bus_buffer >> (data_bit_width * i)) & bit_mask;
    array[i] = *((data_type*) &buffer);
  }
} 

inline void packer(data_type data[], packed_type bus_buffer[]){
#pragma HLS INLINE
  ap_uint<data_bit_width> buffer;
  bus_buffer[0] = 0;
  for (uint64_t i = 0; i < field_length; i++){
#pragma HLS UNROLL
    buffer = *((ap_uint<data_bit_width>*) &data[i]);
    bus_buffer[0] |= ((packed_type)buffer) << (data_bit_width * i);
  }
}

 最後これらの関数をまとめ、HLS向けにラップした関数を準備します(コードはこちら).このラップ関数の実体は.hppに記述するのではなく.cppに記述します(.hppに書くと高位合成のターゲット関数にならない).
 今回はこれに加えてテストベンチを準備します(コードはこちら).基本的には入力データを仕込み,通常のC言語経由とRTL経由で計算を行い比較を行う流れになっています.

3.次回


今回はHLS用のコードまで作成しました。この後は実際の合成をおこないます。
合成して動作確認まで行った後は、
あたりをやっていこうと考えています。


©2022 shts All Right Reserved.

2021年11月19日金曜日

PYNQ-Z2向けのアクリル天板のレーザーカットを頼んでみた話



修論が忙しいのに何も手を付けていない中の人です。修論で自前のFPGAボード(PYNQ-Z2)を使ってるのですが、基本裸運用なのでショートやらなんやらが怖い状態です。今回はその対策としてアクリルの天板を作成してみた話です。

基板には元から足がついているので5mm程度浮いているが,
基本むき出しなので不安が残る





1.設計する


部品の配置図.
配置するエリアのサイズを入れることで,領域内に入りますよアピール.

天板の設計をするにあたり寸法図を探したのですが、ネットの海にはなかったようでした。そこでその辺にノギスが生えていたので各部を測定し寸法を割り出しました。寸法を割り出したのちFusion360で実際の設計を行い、dxfで出力しました。



2.発注する


完成予想図

設計ができたので次は発注です。今回は手間をあまりかけたくなかったので、レーザーカットのサービスを使うこととし、いろいろ探した結果いつも基板を頼んでいるElecrowのサービスを使うことにしました。
基板の発注と異なる点として、領域内であればいくつでも部品を置くことができます。無理に1つの部品にする必要はありませんでした。
先ほど作成したdxfのファイルとともに、カット時の部品の配置図と完成予想図をpdfで添付しました。完成予想図は正直に言っていらないのですが、たまたまあったので入れました。
発送業者に関しては、最近追加された佐川急便を使ってみました。

3.到着と組み立て


届いた天板とFPGA基板.合計5セット届く.

発注が11/10で、到着が11/18だったので1週間ちょっとぐらいでした。おそらく基板と同じ感覚で待てばよさそうです。
実際に組み立てるとこんな感じになりました。PYNQ-Z2に搭載されるチップであればそこまで発熱しないのでファンは不要なのですが、スペースが開いていたので付けました。
スペーサーは秋月、ねじ・ナット・ワッシャーは近所のホームセンターで購入しました。

組み立てた様子.この時はファンが外側についている.
ファンが外に出っ張っていると壊す恐れがあり,この後内側に移設した.

4.まとめ


初めてレーザーカットのサービスを使ってみましたが、特に詰まるところはありませんでした。簡単な形状であればプリント基板を作るよりも楽にできました。印字みたいなこともできるようなので、また機会があれば試してみたいです。


©2021 shts All Right Reserved.


2021年11月12日金曜日

ctypesを使ってPython3からC++の生配列(not std::vector)をいじる


修論がだんだん忙しくなってきた中の人です.研究でPythonからC++のコードをたたく必要が出てきたのでいろいろお試ししていたのですが,その際にいろいろ詰まったので書き残しておきます.今回のコードはこちらに上げてあります.



1.PythonからC++をさわる


Python側からC++のコードを実行する方法はPython.hやSWIG,Boost.Python,pybind11など様々なものがあります(参考1).今回の研究で使用している環境(HLSツールや実行環境)の問題で,以下の制約がありました.

  • 相互呼び出しの機能は不要(PythonからC++を実行できればOK)
  • STLコンテナが使えない(≒std::vectorなどを使う必要がない)
  • g++やCPythonに標準で入っているライブラリのみで行けると非常に楽

いろいろ調べた結果,とりあえずctypesを使ってみることにしました.

2.C++で共有ライブラリを作成する


今回は以下のC++コードをPythonから呼び出します.

// MAC_VEC.hpp
#include <cstdint>
namespace MAC_VEC{
    template<typename data_t>
    void mac_vec(uint32_t size, data_t a[], data_t x[], data_t b[], data_t y[]){
        for(uint32_t i = 0; i < size; i++){
            y[i] = a[i] * x[i] + b[i];
        }
    }
}

ctypesを使う場合テンプレートをそのまま呼び出すことはできないので,型を確定させるラッパーを作っておきます.

// MAC_VEC_WRAPPER.cpp
#include <cstdint>
#include "MAC_VEC.hpp"

extern "C" {
    void MAC_VEC_FP32(uint32_t size, float a[], float x[], float b[], float y[]){
        MAC_VEC::mac_vec<float>(size, a, x, b, y);
    }
    void MAC_VEC_INT16(uint32_t size, int16_t a[], int16_t x[], int16_t b[], int16_t y[]){
        MAC_VEC::mac_vec<int16_t>(size, a, x, b, y);
    }
    void MAC_VEC_UINT32(uint32_t size, uint32_t a[], uint32_t x[], uint32_t b[], uint32_t y[]){
        MAC_VEC::mac_vec<uint32_t>(size, a, x, b, y);
    }
}

とりあえずfloat(fp32),int16, uint32の3パターン準備しました.この時extern "C"をつけないと,コンパイラ側でシンボル名を書き換える操作を行うため,Python側から読めなくなることがあります(参考2).
この二つが準備できたら,g++で共有ライブラリ(.so)を生成します.

// generate_so.sh
g++ MAC_VEC_WRAPPER.cpp -shared -o MAC_VEC.so

コンパイルが通ると,MAC_VEC.soが生成されるはずです.

3.Pythonから動かす


先ほど作成したライブラリをPythonから動かしてみましょう.今回使用するctypesは,Python2.5から標準で入っているライブラリです.
この記事ではMAC_VEC_FP32のみを扱います.残りの二つのに関しては,こちらの実装を確認してください.

// C_API_TEST.py
import ctypes

# load .so file
libc = ctypes.cdll.LoadLibrary("./MAC_VEC.so")

# define params and src/dst data
len_vec = 8
coef = 1.1
vec_x = [i * coef for i in range(len_vec)]
vec_a = [2 * coef for i in range(len_vec)]
vec_b = [(i - 8) * coef for i in range(len_vec)]
vec_y = [0 for i in range(len_vec)]

# define data type and convert method
len_vec_type = ctypes.c_uint32
data_type = ctypes.c_float
vec_type = data_type * len_vec

# convert list-type data
_len_vec = len_vec_type(len_vec)
_vec_x = vec_type(*vec_x)
_vec_a = vec_type(*vec_a)
_vec_b = vec_type(*vec_b)
_vec_y = vec_type(*vec_y)

# execution
libc.MAC_VEC_FP32(_len_vec, _vec_a, _vec_x, _vec_b, _vec_y)

# convert results to list-type
result = list(_vec_y)
print(result)

配列の型を宣言するあたりの作法で詰まりました.配列のデータ型×配列の要素数で表現し,アンパックしたlistを投げ込むことで変換できるようです.
実行すると,こんな感じの結果が得られました.

実行結果.INT16とUINT32の結果も表示している.

とりあえず正しい結果が得られているようです.

4. まとめ


今回はPythonからC++の生配列をいじってみました.ライブラリの追加も最小限で済み,割と簡単に動かすことができました.少し凝ったデータのやり取りをしようとすると大変かもしれないですが,C/C++標準のデータ型であれば十分に使えると思います.


©2021 shts All Right Reserved.

2021年10月13日水曜日

GeekServoで遊んでみた




数か月間ブログの更新をさぼっていた中の人です。今回はLEGOと互換性のあるDCモータ・サーボモータであるGeekServoで遊んでみた話です。



0.GeekServoとは


GeekServoはKittenBot社から発売されているRCサーボモータ・DCモータのシリーズで、日本ではSwitchScienceあたりで購入できます。

1.外見



今回試したのは、GeekServoの9G Motor-Redです。GeekServoのシリーズの中ではこのモデルだけがDCモータで、あとはRCサーボになっています。
出力軸は十字のシャフトになっており、ギアなどを直接接続することができます。また、側面などに丸穴やポッチがあるのでペグやシャフトを接続できます。

2.実際に組んでみる



手元にあった部品を使ってとりあえず組んでみました。


中身はこんな感じです。横軸と縦軸の両方出力軸に設定できるあたりはいいのですが、ギアの段数が多いために終端のガタが大きいのと、伝達できるトルクが少ないのが欠点でしょうか。
ここまで大げさな構造になったのは、軸の位置が縦と横の両方で半ポッチずつずれているためで、縦方向には半ポッチ幅のパーツを挟んで調節し、横方向は12枚歯のベベルギアの位置をハーフブッシュでずらしてかみ合う位置に移動させました。
きれいに0.5ポッチずれているので、半ポッチずらすテクニックがあれば普通に使えそうです。

3.まとめ


今回初めてGeekServoを触ってみたのですが、割と使い勝手がよくびっくりしました。半ポッチずらす必要があるなど多少癖はあるものの、それさえ解消できれば非常に良いものかなと思います。


©2021 shts All Right Reserved.

2021年5月26日水曜日

UnitV2が手に入った



暇な時間が欲しい中の人です.今回はM5Stackから発売されたUnitV2を手に入れたのでテストしていきます.


0.UnitV2とは


UnitV2はM5Stackから発売されたプロトタイピングツールです.
M5Stackのラインナップの中でも最も計算性能が高いものになっています.
UnitV"2"の名前が示すようにUnitVの後継モデルとなっていますが,CPUに互換性はないうえに,2の方はLinuxが動作しています.

1.外観と内容物



UnitV2のサイズはUnitVよりもStickVに近いです.ただUnitV2はバッテリーを含まないのでその分薄くなっています.


内容物はこんな感じです.16GBのSDカードがついてきます.USBはTypeA-TypeC(おそらく2.0)です.


治具(1).GoProのものに似たヒンジになっています.


治具(2).LEGOの部品が付くやつです(赤い部品は付属品ではないです).


手持ちの画像認識系デバイスを並べました(UnitVはレンズ交換済み).UnitVの小ささが際立っていますね.

2.遊ぶ前の儀式


UnitV2とPCをUSBでつなげるとファンが回転し始め,だんだん暖かくなってきます.この時にデバイスマネージャでUnitV2を探すと不明なデバイスとして認識されます.そのためドライバを充てる必要があります.ドライバはチュートリアルのページにあるリンクで配布されているので突っ込んでおきます.


チュートリアルページの通りにドライバを認識させたのち,ブラウザ(Chromeで動作確認済み)を開き,URLのところに UnitV2.py or 10.254.239.1 と入力します.すると最初から入っているでもプログラムを確認できます.

3.デモの様子




こんな感じで動きます.Yoloの実行の様子を見ている感じだと,UnitVやStickVで使われていたK210のアクセラレータ使用時ほどではないにしろ,そこそこの速さで動いている感じです.

4.SSH



UnitV2は初期状態でSSH接続による操作をすることができます.


適当なターミナルソフトで 10.254.239.1:22 にアクセスし,username : m5stack,初期パスワード : 12345678で入れるはずです(本体裏面のシールに書いてある).
組み込みLinuxなので,コマンドポチポチしていくと色々見ることができます.
ただし,スーパーユーザーになれなかったので電源をコマンドラインから切ることができませんでした(中の人がやり方を知らないだけかもしれません).

cpuinfoを読みに行った様子.確かにデュアルコア.

5.その他



Armv7のデュアルコア(1.2GHz)は初期のRPiを凌駕するほどのCPU性能であり,当然のごとく熱がすごいです.この対策として冷却ファンがついているのですが,振動と音が出ます.もしかしたら気になる人がいるかもしれないです.

6.まとめ


UnitV2を初めて動かしてみました.ドライバのインストールと電源をコマンドラインから切れないこと以外は特に気になる点はありませんでした.
今後はncnnやOpenCVを使っていろいろやっていこうと思っています.



cargo run


©2021 shts All Right Reserved.



2021年4月10日土曜日

PYNQ V2.6でDMA転送をする話(手打ちでDMAコントローラを制御する)



前回,前々回ではPYNQのライブラリでDMA転送を行うための回路の作成とプログラミングを行いました.今回は,pynq.lib.dmaの関数ではなく,AXI-Liteのレジスタを直接操作して同じようなことができないかを探る話です.  



1.概要


DMAコントローラはAXI-Liteを用いて制御しており,PYNQのDMAライブラリ内でもAXI-Liteのレジスタを直接操作してDMA転送を行っています.今回はDMAライブラリが何をしてるかを理解するため,この操作を温かみのある手打ちで実装します.
ビットストリームは前回使用したものを転用するため,DMAのモードはシンプルモードのままでいきます.


参考
Python productivity for Zynq (Pynq),"pynq.lib.dma — Python productivity for Zynq (Pynq)"

2.方針



PS-PL間のDMA転送は,PSからPLへのデータの流れ(Memory-Mapped to Stream:MM2S)とPLからPSへのデータの流れ(Stream to Memory-Mapped:S2MM)の二つが存在し,これらを制御する必要があります.
MM2S,S2MMの制御手順はほぼ同じではあるものの独立して制御するため,同じ機能のレジスタがMM2S用,S2MM用の2つ準備されています.

今回はデータ転送直後にデータ読み出しを行うので,S2MMもMM2Sも同じようなタイミングで操作します.

3.方法


実際にAXI4_Liteを用いて,DMAコントローラを操作していきます.アクセスするレジスタとアドレスの関係は以下の通りです(カッコ内はソースコード内での変数名).

MM2S
DMA制御レジスタ(MM2S_DMACR):0x00
DMA状態レジスタ(MM2S_DMASR):0x04
ソースアドレスレジスタ(MM2S_SA):0x18
転送長さレジスタ(MM2S_LENGTH):0x28

S2MM
DMA制御レジスタ(MM2S_DMACR):0x30
DMA状態レジスタ(MM2S_DMASR):0x34
ソースアドレスレジスタ(MM2S_SA):0x48
転送長さレジスタ(MM2S_LENGTH):0x58

スキャッターギャザーモード(SGモード)でのアドレスはAXI DMA IPの製品ガイドを見てください.

参考
Lauri's blog,"AXI Direct Memory Access"

3-1.リセット


1. S2MMのDMA制御レジスタ(0x30)に0x04を書き込む(リセットビットを1にする)  
2. MM2SのDMA制御レジスタ(0x00)に0x04を書き込む(リセットビットを1にする)

(S2MMとMM2Sの状態を確認)  

3. S2MMのDMA制御レジスタに0x00を書き込む(全停止)  
4. MM2SのDMA制御レジスタに0x00を書き込む(全停止)  

(S2MMとMM2Sの状態を確認)  

最初にS2MM/MM2Sのリセットと停止を行います.通信に失敗してDMAのIPが動作したままだと転送が行えなくなります.
S2MMとMM2Sの状態はxxxx_DMASRを読み出して確認します.

3-2.転送


5. S2MMで書き込むメモリ領域の先頭アドレスをデスティネーションアドレスレジスタ(0x48)に書き込む  
6. MM2Sで読み込むメモリ領域の先頭アドレスをソースアドレスレジスタ(0x18)に書き込む  

(S2MMとMM2Sの状態を確認)  

7. S2MMのDMA制御レジスタ(0x30)に0xF001を書き込む(DMAチャンネルの実行&IOC_IrqENとErr_IrqEnの設定)  
8. MM2SのDMA制御レジスタ(0x00)に0xF001を書き込む(DMAチャンネルの実行&IOC_IrqENとErr_IrqEnの設定)  

(S2MMとMM2Sの状態を確認)  

9. S2MMの転送長を転送長さレジスタ(0x58)に書き込む(バイト単位)  
10. MM2Sの転送長を転送長さレジスタ(0x28)に書き込む(バイト単位)

11. MM2Sの同期を待つ
12. S2MMの同期を待つ(ここでフリーズするなら、全メモリの範囲がアドレスエディタで割り当てられているかを確認すること)  
(S2MMとMM2Sの状態を確認)  

転送長を書き込んだ段階で実際にDMAの転送が行われます.
S2MMやMM2Sの同期は、DMAのステータスがErr_IrqかIdleではない間、whileで待つようにしました。

4.まとめ


手打ちでDMAコントローラを制御してみました.マイコンのレジスタの設定とほぼ同じような感じで操作できるようになっており,個人的には比較的わかりやすかったです.
PYNQ V2.6からはSGモード用の関数も準備されており,直に叩くメリットはほぼないのですが,関数内で何をしているかが分かっていただければ幸いです.


©2021 shts All Right Reserved.

2021年4月9日金曜日

PYNQ V2.6でDMA転送をする話(ビットストリーム生成)



絶賛就活中,中の人です.気晴らしがてら進めていた研究の方でPYNQのDMAを使うことがあったのですが,その際に得た知見をまとめようと思います.
ビットストリームはこちらに上げてあります.  



1.環境



ボード:PYNQ-Z2(TUL製,xc7z020clg400-1搭載)
Vivado : Vivado 2020.1
PYNQ : V2.6
OS Ubuntu 20.04LTS

特に変哲のない感じですが,PYNQ V2.6はVivado 2020.1でビルドされているので,それに合わせてバージョンを決めた感じです.

2.どんな回路を作るか



CPUにぶら下がっているDDR3メモリとPL部をつなぎ,メモリ->FIFO(PL部)->メモリというような簡単な回路で動作確認を行います.

3.PS部とPL部の橋渡し.


PYNQ-Z2で使用されているZYNQ 7000シリーズでは,FPGA(PL)とCPU(PS)に接続されたDDR3メモリをAXIバスを介して接続することができます.この機能はZYNQ内に存在するポートを使うことで使用できます.
このポートいくつか存在しており,32b GP AXI Master Ports(M_AXI_GPx),32b GP AXI Slave Ports(S_AXI_GPx),AXI High Performance 32b/64b Slave Ports(S_AXI_HPx),64b AXI ACP Slave Portのように通信の種類に応じて分けられています.


4.AXIバスの通信規格

AXIバスの通信規格にはいくつかの種類があります(AXI4だとAXI4(-Full),AXI-Lite,AXI-Stream).
今回使用するDMAのIPの制約で,FIFOとDMAコントローラ間はAXI-Stream,PS(S_AXI_HP)とDMAコントローラ間はAXI-Fullを使用します.またいくつかのIPでAXI-Liteによる制御が必要なので,そちらも使用します.

参考

5.ブロックダイアグラムを作る

ここからブロックダイアグラムを作っていきます.

5-1.PSの設定



PSとして,Zynq7 Processing System(5.5)を使います.
DDRメモリやFIXIOへの接続ポートを先に生成しておきます。


ここではメモリとPLを接続するため,ポート(M AXI GP0とS AXI HP0)の設定を行います.Vivado 2020.1ではM AXI GP0の方は最初から有効化されているようで,有効化されていないS_AXI_HP0は手動で有効化します.この際に,S_AXI_HP0のデータ幅は64bitにしておきます.

5-2.DMAコントローラの設定



今回使うIPはXilinx提供のAXI Direct Memory Access(7.1)です.
PYNQのv2.6からScatter Gather Engineをサポートするようになったのですが,今回はシンプルモードで試すのでチェックを外しておきましょう.
また,バッファ長を設定するレジスタ(Width of Buffer Length Register)は最大値の26まで引き上げておきましょう.
ここではAddress Widthを32bitとしておきます.
DMAのIPを二つ配置し,それぞれRead-Only, Write-Onlyにすることも可能ですが,今回はR/Wの双方を使えるようにしておきます.
今回の回路ではint16のデータを流すので,Read Channel側のMemory Map Data Widthを32bit,Stream Data Widthを16bitに設定しておきます.

5-3.FIFOの設定



FIFOとして,Xilinx提供のAXI4-Stream Data FIFO(2.0)を使用します.特段設定をかえる必要はないと思います.

5-4.IPの配置と配線



PSのブロックをDiagramに追加し,DDRとFIXED_IOの自動配線を終えたのち,DMAコントローラとFIFOを追加します.
そして,DMAコントローラとFIFOのAXI Streamのバスをつなぎます.
DMAコントローラとPSのS_AXI_HP0との接続にはAXI SmartConnectをかませました(Vivado2020.1の自動配線だと,別のIPが出てきました).
その後、自動接続を使い、他の配線を行いました。

6. 合成とファイルの生成


先程のブロック図をValidate Designにかけると、アドレスが振られていない警告が出てくるので、アドレスエディタでアサインしておきましょう。もう一度チェックを走らせると、CriticalWarningは消えているはずです。

Generate Output Products,Generate HDL Wrapperをし,ビットストリームを生成しました.
PYNQ上で実行する際には,.bit及び.hwhが必要になります(V2.6では.tclは不要みたいです).
以下の二つのフォルダから.bit及び.hwhを探します.

プロジェクト名.runs/impl_1/デザイン名_wrapper.bit
プロジェクト名.src/source_1/bd/デザイン名/hw_handoff/デザイン名.hwh

これらのファイルをPYNQ上のフォルダに投げ込めば,Vivado上での操作は完了です.
(プロジェクト名.src/source_1/bd/デザイン名/hw_handoff/デザイン名.tcl は無くても動くみたいです。)

7.まとめ


ビットストリームの生成まで完成しました.次はPYNQ上での操作に移っていきます



©2021 shts All Right Reserved.

PYNQ V2.6でDMA転送をする話(JupyterNotebook上での実行)



前回に引き続きDMA転送の話です.今回は前回作成したビットストリームを実行していきます.今回のnotebookはこちらに上げています



1.PYNQで動かす


PYNQ上のJupyter Notebookにアクセスする話は割愛します.
とりあえず適当なからフォルダを作り,その中に先ほどの.bitと.hwhを入れ,.ipynbをそのフォルダ内で新規で作成しておきます..bitと.hwhはファイル名をそろえておきます(今後はdesign_1.bit, design_1.hwhとする).

1-1.オーバーレイのロード


from pynq import Overlay
from pynq import PL

OL = Overlay("design_1.bit")
print(OL.ip_dict.keys())
dma = OL.axi_dma_0

ビットストリームの読み込みはこれで完了です..bitのファイル名を使用して.hwhを読み込むようなので,名前はそろえておきましょう. OL.ip_dictには使用したIPのうち,PS側から操作できるものが辞書として登録されています. この辞書内からdmaのIPを探し出し,dmaとしておきましょう.

1-2.xlnkとAllocate


import numpy as np
from pynq import allocate
data_src = allocate((100,), dtype=np.int16)
for i in range(100):
    data_src[i] = i + 1

data_dst = allocate((512,), dtype=np.int16)

print("size of data_src :", data_src.nbytes, "Byte")
print("size of data_dst :", data_dst.nbytes, "Byte")

V2.6のPYNQでは,旧来より使われてきたXlnkに代わり,allocateを使うようになっています.今回はAXI-Streamのデータ幅を16bitにしたので,dtypeも16bitのモノに設定しました.入出力のバッファ領域をこれで確保しました.

1-3.実行


print(data_dst)

dma.sendchannel.transfer(data_src)
dma.sendchannel.wait()
print("send done")

dma.recvchannel.transfer(data_dst)
dma.recvchannel.wait()
print("receive done")

print(data_dst)

dmaのメンバ関数にsendchannel,recvchannelがあり,それぞれにtransferとwaitがあります.この辺は以前のPYNQと変わらずに使えます.

2.まとめ


とりあえずこのような形でPYNQ+DMAの動作を確認出来ました.
そのうちVivado HLSで作成したIPを埋め込み,動作させたいと思います.

参考


©2021 shts All Right Reserved.

2021年4月3日土曜日

スイッチサイエンスのジャンク品買ってみた


絶賛就活終わらない中の人です.先日Twitterを眺めていたところ,このような記事が流れてきました.



たまたまその時間は待機できそうだったので争奪戦に参加しました.結果1セット手に入れることができたので,少し見ていこうかと思います.


1.内容物



中身はこんな感じでした.内容物としては,

M5StickC x 1
M5Gray x 1
M5Faces x 1

となっていました(https://www.switch-science.com/catalog/7081/の例2のパターン).

2.少し動かしてみた


M5Gray.UIFlowのファームが元から書き込まれていた.

最初にUSBケーブルをつなぎ,電源だけを供給させて動作するか試してみました.
StickCとGrayについては,元からUIFlowのファームが書き込まれており,電源を入れただけで動作しました.

FacesにArduinoのサンプルコードを入れたところ.
加速度センサはMPU6886らしい.

Facesに関しては電源を入れても何も表示されませんでした.PCに接続したところCOMポートが割り当てられたので,試しにArduinoでサンプルコードを入れたところ無事動作することがわかりました.3種類のキーボードも動作することを確かめました.


唯一,Facesのベース内部のフレキが根元から断線していたため,グレードルが使えませんでした.今のところは特に使う場面は無いので,当面は放置になりそうです.

3.まとめ


ジャンクということもあり全く動かないことを覚悟していたのですが,あっさり動いてしまいました.返品されたもののうち軽い修正で使えそうなものを選んだとなっていたのですが,出荷前にある程度の修理や動作確認も行ったように見えました.
保証は当然なく,壊れても自己責任なので初心者には絶対にお勧めできませんが,個人的には満足な買い物でした.


©2021 shts All Right Reserved.

2021年2月28日日曜日

RPi Pico向けRustサンプルコードを動かす(Win10 + PowerShell + WSL)




就活解禁数時間前にブログを書き出す中の人です.少し前にRPi PicoをWSL + C/C++ SDKで実装する話を書いたのですが,どうやらRustで動くサンプルコードがあるようだったので,実際に動かしてみました.

今回はEmbedded Rustの話は置いといて,とにかくサンプルコードをRPi Pico上で動かすことにフォーカスします.そのためコードやクレートの解説はほかの資料に譲ることにします.





1.RPi PicoとEmbedded Rust


そもそもRustで組込みプログラミングができるのかという問題があるのですが,Arm Cortex-M系のマイコンであれば動かすことができるようです.RPi PicoのマイコンであるRP2040はCortex-M0+のデュアルコアなのでRustが動かせるというわけです.

参考:

2.何をやるか


今回はデバッガ等も使わずに,ただひたすらにバイナリを吐き出しLチカを
することにフォーカスします.

3.環境


今回は,コードのビルドとELF->UF2の変換で使う環境を変えました.

ビルド:Rust(1.52.0-nightly) + PowerShell
ELF->UF2の変換:WSL2(Ubuntu 20.04)

最初はWSL2一本でやろうと思ったのですが,cargo buildを実行した際に

error: RPC failed; curl 56 GnuTLS recv error (-24): Decryption has failed.

のエラーが出てしまい,ビルドが通らなくなる現象が解決しなかったので今回はPowerShellを使いました(GitHubとの通信で出るようで,git fetchやgit cloneが使えなくなる).
ビルド後の処理でC/C++SDKに含まれるツールが必要になるので,WSL2の方の環境構築もお勧めします.詳細は,こちらを参考にしてください.

ビルド環境の方は,公式の手順通りにRustを突っ込んだのちにarm-none-eabi-gdbを突っ込みます.この辺りはEmbedded Rustでの手順と同じですが,今回はデバッガを使わないので,OpenOCDなどは入れません.
arm-none-eabi-gdbをインストールする際に,必ず環境変数にパスを追加するようにしましょう.インストールの最後に出てくる画面にAdd path to environment variableのオプションが出てくるのでチェックを入れましょう(初期状態だとチェックがないはず).

4.サンプルコードのクローン



を適当な場所にクローンしましょう.ビルド作業はこのsample-project内で行います.

5.ビルド


早速ビルドに移るのですが,そのままではCortex-M0+のELFファイルを吐き出してくれません.そこでおまじない

rustup target add thumbv6m-none-eabi

を実行しておきます.これを実行したうえで

cargo run

すれば./target/thumbv6m-none-eabi/debug内にrp-testというELFファイルが吐き出されているはずです(実態はELFファイルだが,拡張子がない状態で吐き出される).

6.ELF2UF2


.elfも生成出来たのでRPi Picoへ実装したいのですが,Picoへ実装する際に.uf2に変換する必要があります.これを行うツールがRPi PicoのC/C++ SDKに入っています.このツールは事前にビルドする必要があります.今回はこのビルドをWSL2上で行うことにします.

cd (pico-sdkのあるフォルダ)
cd pico-sdk/tools/elf2uf2
mkdir build
cd build
cmake ..
make
sudo cp elf2uf2  /usr/local/bin/

ビルド終了後,WSL2でelf2uf2と打ち込み,

Usage: elf2uf2 (-v) <input ELF file> <output UF2 file>

と出てくればelf2uf2のビルド成功です.ここで先ほどのrp-testをrp-test.elfとリネームしておきましょう.elf2uf2の<input ELF file>は拡張子が.elfのファイルだけを受け入れるので注意しましょう.
最後に

elf2uf2 rp-test.elf rp-test.uf2

を実行すれば,rp-test.uf2が出来ているはずです.

7.実機で動かす




.uf2を実機に入れるところの操作はC/C++ SDKと全く同じなので割愛.
こんな感じでLチカ完了です.

8.感想


Embedded Rustとほぼ同じなので,開発環境についてはある程度ほかのマイコンでの知見が生かせそうです.またデバッガを使う場合は,OpenOCDやらなんやらのインストールが必要みたいです.

9.参考

"The Embedded Rust Book 日本語版",https://tomoyuki-nakabayashi.github.io/book/intro/index.html
"PicoボードにPico_SDKでC言語をビルドする", https://beta-notes.way-nifty.com/blog/2021/02/post-2fff25.html



©2021 shts All Right Reserved.