Linux
FPGA
arm64
zynq
Vivado

UltraZed 向け Debian GNU/Linux (v2017.3版) で Vivado-HLS を使って合成した回路を動かす

はじめに

次の記事で UltraZed 向け Debian GNU/Linux (v2017.3版) の構築をしました。

また、次の記事で、上の記事で構築したシステムのイメージを紹介しました。

この記事では、上の記事で構築したシステムを使って、Vivado-HLS で合成した回路を Linux から動かす例を示します。この記事で紹介する FPGA のデザインやプログラムは次の URL に公開しています。

Sample FPGA Design

今回動かす回路は 次のように C で記述したプログラムを Vivado-HLS で合成します。見ての通り、in から 32bit integer を読んで、符号反転して out 書き込むだけの簡単な回路です。

negative.c
int negative(volatile int *in, volatile int *out, int size){
#pragma HLS INTERFACE m_axi depth=10 port=out offset=slave
#pragma HLS INTERFACE m_axi depth=10 port=in  offset=slave
#pragma HLS INTERFACE s_axilite port=size
#pragma HLS INTERFACE s_axilite port=return
    int i;

    for (i = 0; i < size; i++){
        out[i] = -in[i];
    }

    return(0);
}

in と out は AXI Master Interface を経由して外部のメモリにアクセスします。また、この回路の制御は AXI-Lite Slave Interface を経由して外部プロセッサからレジスタアクセスすることによって行います。

デザインのブロック図は次のようになっています。

Fig.1 ZynqMP Sample Design

Fig.1 ZynqMP Sample Design

このデザインでは、Vivado-HLS で合成した回路の AXI Master Interface を ZynqMP の HP0 ポートに接続しています。HP0 は キャッシュのコヒーレンシを行わないので Linux で動くアプリケーション側でキャッシュの成魚をする必要があります。

準備

UltraZed の準備

次の記事を参考に UltraZed に Debian GNU/Linux をインストールしてください。

fpga でログイン

Linux が起動したら ユーザー名 fpga でログインします。パスワードは fpga になっています。

debian-fpga login: fpga
Password:
fpga@debian-fpga:~$

リポジトリのダウンロード

サンプルとなる FPGA デザインやプログラムを下記の URL よりダウンロードします。

ここでは v2017.3 版を examples/negative としてダウンロードします。

fpga@debian-fpga:~$ mkdir examples
fpga@debian-fpga:~$ cd examples
fpga@debian-fpga:~/examples$ git clone https://github.com/ikwzm/ZynqMP-FPGA-Linux-Example-2-UltraZed negative
fpga@debian-fpga:~/examples$ cd negative
fpga@debian-fpga:~/examples/negative$
fpga@debian-fpga:~/examples/gpio$ git checkout v2017.3

FPGA のコンフィギュレーション

Binary File への変換

今回構築した Debian GNU/Linux には FPGA Reagion が組み込まれています。残念ながら、次の記事のとおり FPGA Regsion と FPGA Manager の組み合わせでは Xilinx のビットストリームを直接読めません。したがって、まずはビットストリームファイルを変換する必要があります。

fpga@debian-fpga:~/examples/negative$ python3 fpga-bit-to-bin.py -f negative.bit negative.bin
Design name: b'negative;UserID=0XFFFFFFFF;Version=2017.2.1\x00'
Full bitstream
Partname b'xczu3eg-sfva625-1-i\x00'
Date b'2017/12/24\x00'
Time b'10:39:58\x00'
Found binary data: 5568668
Flipping data...
Writing data...

Binary File を /lib/firmware へコピー

FPGA Region では /lib/firmware にコンフィギュレーションするファイルを置いておく必要があります。

fpga@debian-fpga:~/examples/negative$ sudo cp negative.bin /lib/firmware

Device Tree Overlay による FPGA のコンフィギュレーション

FPGA Region では Device Tree を使って FPGA をコンフィギュレーションします。そこで次のような Device Tree Overlay 用のソースファイルを用意します。

fpga-load.dts
/dts-v1/;
/ {
    fragment@0 {
        target-path = "/fpga-full";
        __overlay__ {
            firmware-name = "negative.bin";
        };
    };
};

target-path が "/fpga-full" であることに注意してください。これは Linux 起動時に読み込んだ Device Tree で指定した fpga-region のシンボル名です。

次の手順で Device Tree Overlay による FPGA のコンフィギュレーションを行います。

  1. Device Tree Overlay 用ソースファイル(ここでは fpga-load.dts)を dtc(Device Tree Compiler) を使って dtb (ここでは fpga-load.dtb) に変換します。
  2. /config/device-tree/overlays 下に Device Tree Overlay 用のディレクトリ(ここでは fpga) を作ります。
  3. 2.で作ったディレクトリ下の dtbo に 1. で作った dtb を書き込みます。

これで次のようなカーネルメッセージが出れば、FPGA のコンフィギュレーションは成功です。

fpga@debian-fpga:~/examples/negative$ dtc -I dts -O dtb -o fpga-load.dtb fpga-load.dts
fpga@debian-fpga:~/examples/negative$ sudo mkdir /config/device-tree/overlays/fpga
fpga@debian-fpga:~/examples/negative$ sudo cp fpga-load.dtb /config/device-tree/overlays/fpga/dtbo
[   56.218477] fpga_manager fpga0: writing negative.bin to Xilinx ZynqMP FPGA Manager

FPGA のクロックの設定

FPGA をコンフィギュレーションするだけでは FPGA 側の回路は動きません。なぜなら FPGA へ供給するクロックの設定が終わっていないからです。

ここでは fclkcfg を使ってクロックを設定します。fclkcfg は筆者が作ったデバイスドライバで、次の URL に公開しています。

「UltraZed 向け Debian GNU/Linux ブートイメージの提供」をインストールしてあれば、すでに fclkcfg が組み込まれています。

クロックを設定するには、次のような Device Tree Overlay 用のソースファイルを用意します。

fclk0-zynqmp.dts
/dts-v1/;/plugin/;
/ {
    fragment@0 {
        target-path = "/amba";
        __overlay__ {
            fclk0 {
                compatible    = "ikwzm,fclkcfg-0.10.a";
                clocks        = <&clkc 0x47>;
                insert-rate   = "100000000";
                insert-enable = <1>;
                remove-rate   = "1000000";
                remove-enable = <0>;
            };
        };
    };
};

ここでは clocks で ZynqMP の PL CLOCK[0] を指定しています。&clkc はクロック制御ドライバのシンボル名、0x47 は PL CLOCK[0] のインデックス番号です。また、この Device Tree が挿入されたとき PL CLOCK[0] は 周波数 100MHz のクロックを出力するように設定しています。

次の手順で Device Tree Overlay を使ってクロックを設定します。

  1. Device Tree Overlay 用ソースファイル(ここでは fclk0-zynqmp.dts)を dtc(Device Tree Compiler) を使って dtb (ここでは fclk0-zynqmp.dtb) に変換します。
  2. /config/device-tree/overlays 下に Device Tree Overlay 用のディレクトリ(ここでは fclk0) を作ります。
  3. 2.で作ったディレクトリ下の dtbo に 1. で作った dtb を書き込みます。

これで次のようなカーネルメッセージが出れば、FPGA クロックの設定は成功です。

fpga@debian-fpga:~/examples/negative$ dtc -I dts -O dtb -o fclk0-zynqmp.dtb fclk0-zynqmp.dts
fpga@debian-fpga:~/examples/negative$ sudo mkdir /config/device-tree/overlays/fclk0
fpga@debian-fpga:~/examples/negative$ sudo cp fclk0-zynqmp.dtb /config/device-tree/overlays/fclk0/dtbo
[  111.238976] fclkcfg amba:fclk0: driver installed.
[  111.243617] fclkcfg amba:fclk0: device name    : fclk0
[  111.248737] fclkcfg amba:fclk0: clock  name    : pl0
[  111.253678] fclkcfg amba:fclk0: clock  rate    : 99999999
[  111.259085] fclkcfg amba:fclk0: clock  enabled : 1
[  111.263833] fclkcfg amba:fclk0: remove rate    : 1000000
[  111.269125] fclkcfg amba:fclk0: remove enable  : 0

Uio と Udmabuf の準備

FPGA にコンフィギュレーションした回路にアクセスするには uio を使います。また、データのやりとりは udmabuf を使います。そのために次のような Device Tree Overlay 用のソースファイルを用意します。

negative.dts
/dts-v1/;/plugin/;
/ {
    fragment@0 {
        target-path = "/amba_pl@0";
        #address-cells = <2>;
        #size-cells = <2>;

        __overlay__ {
            #address-cells = <2>;
            #size-cells = <2>;

            negative-uio {
                compatible = "generic-uio";
                reg = <0x0 0x80010000 0x0 0x10000>;
                interrupt-parent = <&gic>;
                interrupts = <0 89 4>;
            };

            negative-udmabuf4 {
                compatible  = "ikwzm,udmabuf-0.10.a";
                device-name = "udmabuf4";
                size = <0x00100000>;
            };

            negative-udmabuf5 {
                compatible = "ikwzm,udmabuf-0.10.a";
                device-name = "udmabuf5";
                size = <0x00100000>;
            };
                };
    } ;
} ;

uio の レジスタのアドレスは 0x80010000、サイズは 0x10000 です。これは Vivado でデザインしたときに割り当てたアドレスを指定します。

割り込み番号は < 0 89 4> を指定します。これは Vivado でデザインしたときに、割り込み信号を ZynqMP の割り込みポート(PL_PS_IRQ0)に接続してますが、その割り込み番号です。詳細は『Zynq UltraScale+ Device Technical Reference Manual UG1085 (v1.7) December 22, 2017』 の「Table 11-3 Interrupts Map」を参照してください。 『FPGAの部屋』の marsee101 さんの指摘で UG1085(v1.7) には 「Table 11-3 Interrupt Map」の記述は無いことが判りました。「Table 11-3 Interrupt Map」があるのは古い UG1085(v1.0)です。

割り込み番号は < 0 89 4> を指定します。これは Vivado でデザインしたときに、割り込み信号を ZynqMP の割り込みポート(PL_PS_IRQ0)に接続してますが、その割り込みポートを示します。『Zynq UltraScale+ Device Technical Reference Manual UG1085 (v1.7) December 22, 2017』 の「Table 13-1 System Interrupts」によれば、PL_PS_IRQ0 は割り込み番号(IRQ Number) は 121 です。Device Tree の interrupts プロパティの第2引数 には、この割り込み番号 121 から 32 を引いた値(=89)を指定するようです。(ちなみに、Zynq の場合も Device Tree の interrupts プロパティの第二引数に割り込み番号から32を引いた値を指定しています。ARM の 割り込みコントローラー(GIC)のデバイスドライバの規則のようなものかもしれません。)

次の手順で Device Tree Overlay を使って uio と udmabuf を設定します。

  1. Device Tree Overlay 用ソースファイル(ここでは negative.dts)を dtc(Device Tree Compiler) を使って dtb (ここでは negative.dtb) に変換します。
  2. /config/device-tree/overlays 下に Device Tree Overlay 用のディレクトリ(ここでは negative) を作ります。
  3. 2.で作ったディレクトリ下の dtbo に 1. で作った dtb を書き込みます。

これで次のように /dev/uio0、/dev/udmabuf4、/dev/udmabuf5 が出来れば成功です。

fpga@debian-fpga:~/examples/negative$ dtc -I dts -O dtb -o negative.dtb negative.dts
fpga@debian-fpga:~/examples/negative$ sudo mkdir /config/device-tree/overlays/negative
fpga@debian-fpga:~/examples/negative$ sudo cp negative.dtb /config/device-tree/overlays/negative/dtbo
[  164.123998] udmabuf amba_pl@0:negative-udmabuf4: driver probe start.
[  164.131871] udmabuf udmabuf4: driver installed
[  164.136254] udmabuf udmabuf4: major number   = 244
[  164.141021] udmabuf udmabuf4: minor number   = 0
[  164.145619] udmabuf udmabuf4: phys address   = 0x0000000070400000
[  164.151689] udmabuf udmabuf4: buffer size    = 1048576
[  164.156811] udmabuf udmabuf4: dma coherent   = 0
[  164.161412] udmabuf amba_pl@0:negative-udmabuf4: driver installed.
[  164.167998] udmabuf amba_pl@0:negative-udmabuf5: driver probe start.
[  164.175758] udmabuf udmabuf5: driver installed
[  164.180142] udmabuf udmabuf5: major number   = 244
[  164.184917] udmabuf udmabuf5: minor number   = 1
[  164.189505] udmabuf udmabuf5: phys address   = 0x0000000070500000
[  164.195576] udmabuf udmabuf5: buffer size    = 1048576
[  164.200698] udmabuf udmabuf5: dma coherent   = 0
[  164.205298] udmabuf amba_pl@0:negative-udmabuf5: driver installed.
fpga@debian-fpga:~/examples/negative$ ls -la /dev/uio*
crw------- 1 root root 245, 0 Jan  8 18:07 /dev/uio0
fpga@debian-fpga:~/examples/negative$ ls -la /dev/udmabuf*
crw------- 1 root root 244, 0 Jan  8 18:07 /dev/udmabuf4
crw------- 1 root root 244, 1 Jan  8 18:07 /dev/udmabuf5

実際に走らせてみよう

negative.py

FPGA にロードした回路を動かすために次のようなサンプルプログラムを用意しています。

negative.py
from udmabuf import Udmabuf
from uio     import Uio
import numpy as np
import time

if __name__ == '__main__':
    uio0       = Uio('uio0')
    regs       = uio0.regs()
    udmabuf4   = Udmabuf('udmabuf4')
    udmabuf5   = Udmabuf('udmabuf5')
    test_dtype = np.uint32
    test_size  = min(int(udmabuf4.buf_size/(np.dtype(test_dtype).itemsize)),
                     int(udmabuf5.buf_size/(np.dtype(test_dtype).itemsize)))

    udmabuf4_array    = udmabuf4.memmap(dtype=test_dtype, shape=(test_size))
    udmabuf4_array[:] = np.random.randint(-21474836478,2147483647,(test_size))
    udmabuf4.set_sync_to_device(0, test_size*(np.dtype(test_dtype).itemsize))

    udmabuf5_array    = udmabuf5.memmap(dtype=test_dtype, shape=(test_size))
    udmabuf5_array[:] = np.random.randint(-21474836478,2147483647,(test_size))
    udmabuf5.set_sync_to_cpu(   0, test_size*(np.dtype(test_dtype).itemsize))

    total_setup_time   = 0
    total_cleanup_time = 0
    total_xfer_time    = 0
    total_xfer_size    = 0
    count              = 0

    for i in range (0,9):

        start_time  = time.time()
        udmabuf4.sync_for_device()
        udmabuf5.sync_for_device()
        regs.write_word(0x18, udmabuf4.phys_addr & 0xFFFFFFFF)
        regs.write_word(0x20, udmabuf5.phys_addr & 0xFFFFFFFF)
        regs.write_word(0x28, test_size)
        regs.write_word(0x04, 0x000000001)
        regs.write_word(0x08, 0x000000001)
        regs.write_word(0x0C, 0x000000001)
        uio0.irq_on()
        phase0_time = time.time()
        regs.write_word(0x00, 0x000000001)
        uio0.wait_irq()

        phase1_time = time.time()
        regs.write_word(0x0C, 0x000000001)
        udmabuf4.sync_for_cpu()
        udmabuf5.sync_for_cpu()

        end_time     = time.time()
        setup_time   = phase0_time - start_time
        xfer_time    = phase1_time - phase0_time
        cleanup_time = end_time    - phase1_time
        total_time   = end_time    - start_time

        total_setup_time   = total_setup_time   + setup_time
        total_cleanup_time = total_cleanup_time + cleanup_time
        total_xfer_time    = total_xfer_time    + xfer_time
        total_xfer_size    = total_xfer_size    + test_size
        count              = count              + 1
        print ("total:{0:.3f}[msec] setup:{1:.3f}[msec] xfer:{2:.3f}[msec] cleanup:{3:.3f}[msec]".format(round(total_time*1000.0,3), round(setup_time*1000.0,3), round(xfer_time*1000.0,3), round(cleanup_time*1000.0,3)))


    print ("average_setup_time  :{0:.3f}".format(round((total_setup_time  /count)*1000.0,3)) + "[msec]")
    print ("average_cleanup_time:{0:.3f}".format(round((total_cleanup_time/count)*1000.0,3)) + "[msec]")
    print ("average_xfer_time   :{0:.3f}".format(round((total_xfer_time   /count)*1000.0,3)) + "[msec]")
    print ("thougput            :{0:.3f}".format(round(((total_xfer_size/total_xfer_time)/(1000*1000)),3)) + "[MByte/sec]")

    udmabuf4_negative_array = np.negative(udmabuf4_array)
    if np.array_equal(udmabuf4_negative_array, udmabuf5_array):
         print("np.negative(udmabuf4) == udmabuf5 : OK")
    else:
         print("np.negative(udmabuf4) == udmabuf5 : NG")
         count = 0
         for i in range(test_size):
             if udmabuf4_negative_array[i] != udmabuf5_array[i] :
                 count = count + 1
                 if count < 16:
                     print("udmabuf4_negative_array[0x{0:08X}] = 0x{1:08X} udmabuf5_array[0x{0:08X}] = 0x{2:08X}".format(i, udmabuf4_negative_array[i], udmabuf5_array[i]))
         print("NG Count:{0}".format(count))


単純にバッファの大きさ分の 32bit 整数を np.random.randint() で生成して udmabuf4 に用意しておき、回路を起動して、終わるのを割り込みで待ちます。終わったら udmabuf5 に符号変換された結果が格納されているはずなので、negative.py 側で計算しておいた結果と比較して OK / NG を判定しています。

今回は回路の実行時間の他に、キャッシュのフラッシュと無効化にかかった時間も測定しています。

uio.py

negative.py で import している uio.py は、「Python と Numpy で UIO を制御」で紹介したものです。

udmabuf.py

negative.py で import している udmabuf.py は次のようなものです。

udmabuf.py
import numpy as np

class Udmabuf:
    """A simple udmabuf class"""

    def __init__(self, name):
        self.name           = name
        self.device_name    = '/dev/%s'               % self.name
        self.class_path     = '/sys/class/udmabuf/%s' % self.name
        self.phys_addr      = self.get_value('phys_addr', 16)
        self.buf_size       = self.get_value('size')
        self.sync_offset    = None
        self.sync_size      = None
        self.sync_direction = None

    def get_value(self, name, radix=10):
        value = None
        for line in open(self.class_path + '/' + name):
            value = int(line, radix)
            break
        return value

    def set_value(self, name, value):
        f = open(self.class_path + '/' + name, 'w')
        f.write(str(value))
        f.close

    def memmap(self, dtype, shape):
        self.item_size = np.dtype(dtype).itemsize
        self.array     = np.memmap(self.device_name, dtype=dtype, mode='r+', shape=shape)
        return self.array

    def set_sync_area(self, direction=None, offset=None, size=None):
        if offset is None:
            self.sync_offset    = self.get_value('sync_offset')
        else:
            self.set_value('sync_offset', offset)
            self.sync_offset    = offset

        if size   is None:
            self.sync_size      = self.get_value('sync_size')
        else:
            self.set_value('sync_size', size)
            self.sync_size      = size

        if direction is None:
            self.sync_direction = self.get_value('sync_direction')
        else:
            self.set_value('sync_direction', direction)
            self.sync_direction = direction

    def set_sync_to_device(self, offset=None, size=None):
        self.set_sync_area(1, offset, size)

    def set_sync_to_cpu(self, offset=None, size=None):
        self.set_sync_area(2, offset, size)

    def set_sync_to_bidirectional(self, offset=None, size=None):
        self.set_sync_area(3, offset, size)

    def sync_for_cpu(self):
        self.set_value('sync_for_cpu', 1)

    def sync_for_device(self):
        self.set_value('sync_for_device', 1)

基本的には、udmabuf で持っている機能を単純に python のメソッドに割り当てたものです。udmabuf の詳細は以下の URL を参照してください。

実行結果

うまくいくと次のような結果が得られます。

fpga@debian-fpga:~/examples/negative$ sudo python3 negative.py
total:9.228[msec] setup:0.803[msec] xfer:7.894[msec] cleanup:0.532[msec]
total:1.093[msec] setup:0.618[msec] xfer:0.020[msec] cleanup:0.455[msec]
total:1.071[msec] setup:0.597[msec] xfer:0.020[msec] cleanup:0.454[msec]
total:1.071[msec] setup:0.599[msec] xfer:0.019[msec] cleanup:0.453[msec]
total:1.083[msec] setup:0.596[msec] xfer:0.020[msec] cleanup:0.468[msec]
total:1.071[msec] setup:0.600[msec] xfer:0.020[msec] cleanup:0.452[msec]
total:1.069[msec] setup:0.596[msec] xfer:0.020[msec] cleanup:0.453[msec]
total:1.071[msec] setup:0.597[msec] xfer:0.020[msec] cleanup:0.454[msec]
total:8.956[msec] setup:0.608[msec] xfer:7.889[msec] cleanup:0.459[msec]
average_setup_time  :0.624[msec]
average_cleanup_time:0.464[msec]
average_xfer_time   :1.769[msec]
thougput            :148.187[MByte/sec]
np.negative(udmabuf4) == udmabuf5 : OK

時々、実行時間がやたらに遅かったりすることがありますが、これに関してはもう少し調査する予定です。

また、キャッシュのフラッシュの時間(setup time)や無効化の時間(cleanup time)がけっこうバカにならないくらいかかっていることが判ります。キャッシュのコヒーレンシをハードウェアで行わずにソフトウェアで行う場合は、この点に注意する必要があります。

後始末

遊び終わったら、Device Tree Overlay で追加したデバイスツリーを、後から追加した順に削除します。

fpga@debian-fpga:~/examples/negative$ sudo rmdir /config/device-tree/overlays/netagive
[  749.266776] udmabuf udmabuf5: driver uninstalled
[  749.271568] udmabuf amba_pl@0:negative-udmabuf5: driver unloaded
[  749.277680] udmabuf udmabuf4: driver uninstalled
[  749.282536] udmabuf amba_pl@0:negative-udmabuf4: driver unloaded
fpga@debian-fpga:~/examples/negative$ sudo rmdir /config/device-tree/overlays/fclk0
[  760.491074] fclkcfg amba:fclk0: change rate    : 992064
[  760.496344] fclkcfg amba:fclk0: change enable  : 0
[  760.501388] fclkcfg amba:fclk0: driver unloaded
fpga@debian-fpga:~/examples/negative$ sudo rmdir /config/device-tree/overlays/fpga

参考