18
13

Linux から FPGA のメモリに"キャッシュを有効にして"アクセスするデバイスドライバ

Last updated at Posted at 2020-08-07

はじめに

お断り

この記事で紹介するデバイスドライバは CPU のデータキャッシュの操作を arm64/arm のアセンブリ言語で実装しています。データキャッシュ操作は本来なら Linux Kernel のAPI を使うべきところですが、残念ながら使えるものがありませんでした(この点は『Linux から FPGA のメモリに"キャッシュを有効にして"アクセスする方法』@Qiita を参照)。

この記事はあくまでもちょっとやってみました的なトライアルの記事であることをご了承ください。

やりたかったこと

ZynqMP(ARM64) や Zynq(ARM) でPS(Processing System)部と PL(Programmable Logic) 部とデータをやりとりする際に、PL 側にBRAM 等のメモリを用意して、PS部のCPUからアクセスする方法があります。その際に次の条件を満たすようにすると便利です。

  1. CPU のデータキャッシュを有効に出来る。
  2. CPU のデータキャッシュの操作(Flush or Invalidiate) が手動でできる。
  3. Device Tree Overlay 等で Linux 起動後に自由に着脱できる。

普通にアクセスするだけならば uio を使えば可能です。しかし uio では条件1のCPU のデータキャッシュを有効に出来ないため、大量のデータを転送する場合は性能的に不利です。

また、参考「Accessing BRAM In Linux」で示す /dev/mem と reserved_memory を使う方法では、データキャッシュを有効にすることは出来ても、手動でキャッシュ操作ができないため、PL側とのデータのやりとりには向いていません。また、reserved_memory はLinux がブートする際にのみ指定できるので、Linux 起動後に自由に着脱できません。

やったこと

Linux から PL 側のメモリにキャッシュを有効にしてアクセスするデバイスドライバを試作してみました。名前は uiomem です。以下の URL で公開しています。(まだアルファ版の試作品です。)

この記事では以下のことについて説明します。

  • サンプルデザインによるデータキャッシュの効果確認
  • uiomem の紹介

2023年10月23日追記

この記事では uiomem v1.0.0-alpha.1 を使って性能を測定していますが、2023年10月23日現在、uiomem のバージョンは uiomem v1.0.0-alpha.4 です。

また、この記事では PLBRAM-Ultra96 を使って性能を測定していますが、同様の実験を KV260 で行った PLBRAM-Kv260 を2023年10月23日現在公開しています。 以下は PLBRAM-Kv260 の実験環境です。

データキャッシュの効果

この章では、PS 側から PL 側のメモリをアクセスする際にデータキャッシュがどのていどの効果があるのかを実際に計測して示します。

測定環境

計測に使用した環境は次の通りです。

PL 側には次のようなデザインを実装します。PL側に256KByte分のメモリを BRAM で実装し、そのインターフェースには Xilinx 社の AXI BRAM Controller を使っています。動作周波数は100MHz です。AXI BRAM Controller の AXI I/F と BRAM I/F の波形を観測するためにILA (Integrated Logic Analyzer) を接続しています。

Fig.1 PLBRAM-Ultra96 のブロック図

Fig.1 PLBRAM-Ultra96 のブロック図


これらの環境は github で公開しています。

データキャッシュオフ時のメモリライト

データキャッシュをオフにして memcpy() を使って PL 側の BRAM に 256KByte のデータを書き込むのに要した時間は 0.496 msec でした。約528MByte/sec の書き込み速度です。

そのときの AXI I/F の波形はつぎのようになりました。

Fig.2 データキャッシュオフ時のメモリライトの AXI IF 波形

Fig.2 データキャッシュオフ時のメモリライトの AXI IF 波形


波形をみてわかるとおり、バースト転送が行われていません(AWLEN=00)。1ワード(16bytes) ずつ転送していることがわかります。

データキャッシュオン時のメモリライト

データキャッシュをオンにして memcpy() を使って PL 側の BRAM に 256KByte のデータを書き込むのに要した時間は 0.317 msec でした。約827MByte/sec の書き込み速度です。

そのときの AXI I/F の波形はつぎのようになりました。

Fig.3 データキャッシュオン時のメモリライトの AXI IF 波形

Fig.3 データキャッシュオン時のメモリライトの AXI IF 波形


波形をみてわかるとおり、一回の書き込みで4ワード(64byte)のバースト転送が行われています(AWLEN=03)。

BRAM へのライトが発生するのは、CPU がライトした時ではありません。CPU がライトしたとき、まずデータキャッシュにデータが書き込まれて BRAM へはまだ書き込まれません。そして、マニュアルでデータキャッシュフラッシュ命令が実行されたときか、データキャッシュが一杯になって使われていないキャッシュを空けるときに初めて BRAM へライトが発生します。その際にデータキャッシュのキャッシュラインサイズ(arm64 では64byte) ごとにまとめてライトが行われます。

データキャッシュオフ時のメモリリード

データキャッシュをオフにして memcpy() を使って PL 側の BRAM から 256KByte のデータを読み込むのに要した時間は 3.485 msec でした。約75MByte/sec の読み込み速度です。

そのときの AXI I/F の波形はつぎのようになりました。

Fig.4 データキャッシュオフ時のメモリリードの AXI IF 波形

Fig.4 データキャッシュオフ時のメモリリードの AXI IF 波形


波形をみてわかるとおり、バースト転送が行われていません(ARLEN=00)。1ワード(16bytes) ずつ転送していることがわかります。

データキャッシュオン時のメモリリード

データキャッシュをオンにして memcpy() を使って PL 側の BRAM から 256KByte のデータを読み込むのに要した時間は 0.409 msec でした。約641MByte/sec の読み込み速度です。

そのときの AXI I/F の波形はつぎのようになりました。

Fig.5 データキャッシュオン時のメモリリードの AXI IF 波形

Fig.5 データキャッシュオン時のメモリリードの AXI IF 波形


波形をみてわかるとおり、一回の読み込みで4ワード(64byte)のバースト転送が行われています(ARLEN=03)。

CPU がメモリリードを行った際、データキャッシュにデータが無ければ、BRAM からデータを読んでキャッシュに充填します。その際にデータキャッシュのキャッシュラインサイズ(arm64 では 64byte)分をまとめて BRAM から読み出します。それ以降はデータキャッシュにデータがある限りデータキャッシュからデータがCPUに提供されるので BRAM へのアクセスは発生しません。そのため、データキャッシュがオフの時よりも高速にメモリリードが行われます。この環境ではデータキャッシュオフ時が75MByte/sec に対してデータキャッシュをオンにすると 641MByte/sec と大幅に性能が向上しました。

uiomem の紹介

uiomem とは

uiomemはユーザー空間からLinuxカーネル管理外のメモリ領域にアクセスするためのLinuxデバイスドライバです。uiomemには以下の機能があります。

  • uiomemはCPUキャッシュを有効にできるため、メモリに高速でアクセスできます。
  • uiomemは、CPUキャッシュを手動で無効にしてフラッシュできます。
  • uiomemは、Linuxカーネルから自由にアタッチおよびデタッチできます。

デバイスファイル(/dev/uiomem0など)を使用してユーザーメモリ空間にマッピングするか、read()/write() 関数を使用して、ユーザー空間からメモリにアクセスできます。

メモリ空間の開始アドレスとサイズは、デバイスツリーで指定するか、あるいは insmod コマンドによるデバイスドライバのロード時の引数で指定します。

対応プラットフォーム

  • OS:Linux Kernelバージョン4.19、5.4、6.1(作者は5.4 と 6.1でテスト済み)
  • CPU:ARM64 Cortex-A53(ザイリンクスZYNQ UltraScale + MPSoC)
  • CPU:ARMv7 Cortex-A9(ザイリンクスZYNQ)

インストール

insmod で uiomem をロードします。この際に Linuxカーネル管理外のメモリ領域を引数で指定することが出来ます。

shell$ sudo insmod uiomem.ko uiomem0_addr=0x0400000000 uiomem0_size=0x00040000
[  276.428346] uiomem uiomem0: driver version = 1.0.0-alpha.1
[  276.433903] uiomem uiomem0: major number   = 241
[  276.438534] uiomem uiomem0: minor number   = 0
[  276.442980] uiomem uiomem0: range address  = 0x0000000400000000
[  276.448901] uiomem uiomem0: range size     = 262144
[  276.453775] uiomem uiomem.0: driver installed.
shell$ ls -la /dev/uiomem0
crw------- 1 root root 241, 0 Aug  7 12:51 /dev/uiomem0

デバイスツリーによる設定

uiomem は insmod の引数でLinuxカーネル管理外のメモリ領域を指定する以外に、Linux のカーネルが起動時に読み込む device tree ファイルによってメモリ領域を指定する方法があります。device tree ファイルに次のようなエントリを追加しておけばinsmod でロードする際に自動的に /dev/uiomem0 が作成されます。

devicetree.dts
 		#address-cells = <2>;
		#size-cells = <2>;
		uiomem_plbram {
 			compatible  = "ikwzm,uiomem";
			device-name = "uiomem0";
			minor-number = <0>;
			reg = <0x04 0x00000000 0x0 0x00040000>;
		};

reg プロパティで メモリ領域を示します。reg プロパティの最初の要素(#address-cells が 2 の場合は2要素)がメモリ領域の先頭アドレスを示します。reg プロパティの残りの要素(#size-cells が2の場合は2要素)がメモリ領域の大きさをバイト数で示します。上記の例ではメモリ領域の先頭アドレスは 0x04_0000_0000、メモリ領域のサイズは 0x40000 です。

device-name プロパティでデバイス名を指定します。

minor-number プロパティで uiomem のマイナー番号を指定します。マイナー番号は0から255までつけることができます。ただし、insmodの引数の方が優先され、マイナー番号がかち合うとdevice treeで指定した方が失敗します。minor-number プロパティが省略された場合、空いているマイナー番号が割り当てられます。

デバイス名は次のように決まります。

  1. device-nameが指定されていた場合は、 device-name。
  2. device-nameが省略されていて、かつminor-numberが指定されていた場合は、sprintf("uiomem%d", minor-number)。
  3. device-nameが省略されていて、かつminor-numberも省略されていた場合は、devicetree のエントリー名(例ではuiomem_plbram)。

デバイスファイル

uiomem をカーネルにロードすると、次のようなデバイスファイルが作成されます。<device-name> には、前節で説明したデバイス名が入ります。

  • /dev/<device-name>
  • /sys/class/uiomem/<device-name>/phys_addr
  • /sys/class/uiomem/<device-name>/size
  • /sys/class/uiomem/<device-name>/sync_direction
  • /sys/class/uiomem/<device-name>/sync_offset
  • /sys/class/uiomem/<device-name>/sync_size
  • /sys/class/uiomem/<device-name>/sync_for_cpu
  • /sys/class/uiomem/<device-name>/sync_for_device

/dev/<device-name>

/dev/<device-name> は mmap() を使って、メモリ領域をユーザー空間にマッピングするか、read()、write() を使ってメモリ領域にアクセスする際に使用します。

uiomem_test.c
 	if ((fd  = uiomem_open(uiomem, O_RDWR)) != -1) {
 		iomem = mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
		uiomem_sync_for_cpu();
		/* ここで iomem にアクセスする処理を行う */
		uiomem_sync_for_device();
		close(fd);
	}

mmap() を使ってユーザー空間にマッピングする際は、データキャッシュの制御が必要になる場合があります。データキャッシュの制御は sync_for_cpu および sync_for_device で行います。これらについては後述します。

また、dd コマンド等でデバイスファイルを指定することで、shell から直接リードライトすることも出来ます。

shell$ dd if=/dev/urandom of=/dev/uiomem0 bs=4096 count=64
64+0 records in
64+0 records out
262144 bytes (262 kB, 256 KiB) copied, 0.00746404 s, 35.1 MB/s
shell$ dd if=/dev/uiomem0 of=random.bin bs=4096
64+0 records in
64+0 records out
262144 bytes (262 kB, 256 KiB) copied, 0.00578518 s, 45.3 MB/s

phys_addr

/sys/class/uiomem/<device-name>/phys_addr は メモリ領域の先頭アドレスが読めます。

size

/sys/class/uiomem/<device-name>/size はメモリ領域のサイズが読めます。

sync_direction

/sys/class/uiomem/<device-name>/sync_direction は uiomem のキャッシュ制御を手動で行う際のアクセス方向を指定します。

  • 0: リードライト双方向を指定します。
  • 1: ライトオンリー(PS to PL)であることを指定します。
  • 2: リードオンリー(PS from PL)であることを指定します。

sync_offset

/sys/class/uiomem/<device-name>/sync_offset はキャッシュ制御を手動で行う際の範囲の先頭をメモリ領域からのオフセット値で指定します。

sync_size

/sys/class/uiomem/<device-name>/sync_size はキャッシュ制御を手動で行う際の範囲のサイズを指定します。

sync_for_cpu

/sys/class/uiomem/<device-name>/sync_for_cpu は キャッシュ制御を手動で行う際、このデバイスファイルに0以外の値を書き込むことでCPU キャッシュを無効化(Invalidiate) します。このデバイスファイルは書き込みオンリーです。

このデバイスファイルに1を書いた場合、sync_directionが2(=リードオンリー)または0(=リードライト双方向)だった時、 /sys/class/uiomem/<device-name>/sync_offset と /sys/class/uiomem/<device-name>/sync_size で指定された範囲のCPUキャッシュが無効化されます。

uiomem_test.c
void  uiomem_sync_for_cpu(void)
{
	unsigned char  attr[1024];
	unsigned long  sync_for_cpu   = 1;
	if ((fd  = open("/sys/class/uiomem/uiomem0/sync_for_cpu", O_WRONLY)) != -1) {
		sprintf(attr, "%d",  sync_for_cpu);
		write(fd, attr, strlen(attr));
		close(fd);
 	}
}

このデバイスファイルに書き込む値には、次のようにsync_offset、sync_size および sync_direction を含めることが出来ます。

uiomem_test.c
void uiomem_sync_for_cpu(unsigned long sync_offset, unsigned long sync_size, unsigned int sync_direction)
{
	unsigned char  attr[1024];
	unsigned long  sync_for_cpu   = 1;
	if ((fd  = open("/sys/class/uiomem/uiomem0/sync_for_cpu", O_WRONLY)) != -1) {
		sprintf(attr, "0x%08X%08X", (sync_offset & 0xFFFFFFFF), (sync_size & 0xFFFFFFF0) | (sync_direction << 2) | sync_for_cpu);
		write(fd, attr, strlen(attr));
		close(fd);
 	}
}

この方式で指定した sync_offset、sync_size、sync_direction は一時的なものであり、デバイスファイルの /sys/class/uiomem/<device-name>/sync_offset 、/sys/class/uiomem/<device-name>/sync_size、/sys/class/uiomem/<device-name>/sync_direction の値には影響を与えません。

また、フォーマットの都合上、sync_offset および sync_size で指定できる範囲は32ビットで示せる範囲のみです。

sync_for_device

/sys/class/uiomem/<device-name>/sync_for_device は キャッシュ制御を手動で行う際、このデバイスファイルに0以外の値を書き込むことでCPU キャッシュをフラッシュします。このデバイスファイルは書き込みオンリーです。

このデバイスファイルに1を書いた場合、sync_directionが1(=ライトオンリー)または0(=リードライト双方向)だった時、 /sys/class/uiomem/<device-name>/sync_offset と /sys/class/uiomem/<device-name>/sync_size で指定された範囲のCPUキャッシュがフラッシュされます。

uiomem_test.c
void uiomem_sync_for_device(void)
{
	unsigned char  attr[1024];
	unsigned long  sync_for_device   = 1;
	if ((fd  = open("/sys/class/uiomem/uiomem0/sync_for_cpu", O_WRONLY)) != -1) {
		sprintf(attr, "%d",  sync_for_device);
		write(fd, attr, strlen(attr));
		close(fd);
 	}
}

このデバイスファイルに書き込む値には、次のようにsync_offset、sync_size および sync_direction を含めることが出来ます。

uiomem_test.c
void uiomem_sync_for_device(unsigned long sync_offset, unsigned long sync_size, unsigned int sync_direction)
{
	unsigned char  attr[1024];
	unsigned long  sync_for_device  = 1;
	if ((fd  = open("/sys/class/uiomem/uiomem0/sync_for_device", O_WRONLY)) != -1) {
		sprintf(attr, "0x%08X%08X", (sync_offset & 0xFFFFFFFF), (sync_size & 0xFFFFFFF0) | (sync_direction << 2) | sync_for_device);
		write(fd, attr, strlen(attr));
		close(fd);
 	}
}

この方式で指定した sync_offset、sync_size、sync_direction は一時的なものであり、デバイスファイルの /sys/class/uiomem/<device-name>/sync_offset 、/sys/class/uiomem/<device-name>/sync_size、/sys/class/uiomem/<device-name>/sync_direction の値には影響を与えません。

また、フォーマットの都合上、sync_offset および sync_size で指定できる範囲は32ビットで示せる範囲のみです。

データキャッシュ制御

PL 側のメモリを CPU からのみアクセス出来るメモリとして使うだけならば、データキャッシュを有効にするだけで済みます。しかし、PL 側のメモリを CPU 以外のデバイスがアクセスする場合や、PL 側のメモリを Linux 起動後に有効にしたり無効にしたりするには、データキャッシュを有効にするだけでは不十分です。データキャッシュと PL 側のメモリとのデータの不一致が起こりうるので、なんらかの方法でデータキャッシュと PL 側のメモリの内容を一致させる必要があります。

uiomemではデータキャッシュの制御を直接 arm64/arm のデータキャッシュ命令を使って実装しています。

uiomem.c
#if (defined(CONFIG_ARM64))
static inline u64  arm64_read_dcache_line_size(void)
{
    u64       ctr;
    u64       dcache_line_size;
    const u64 bytes_per_word = 4;
    asm volatile ("mrs %0, ctr_el0" : "=r"(ctr) : : );
    asm volatile ("nop" : : : );
    dcache_line_size = (ctr >> 16) & 0xF;
    return (bytes_per_word << dcache_line_size);
}
static inline void arm64_inval_dcache_area(void* start, size_t size)
{
    u64   vaddr           = (u64)start;
    u64   __end           = (u64)start + size;
    u64   cache_line_size = arm64_read_dcache_line_size();
    u64   cache_line_mask = cache_line_size - 1;
    if ((__end & cache_line_mask) != 0) {
        __end &= ~cache_line_mask;
        asm volatile ("dc civac, %0" :  : "r"(__end) : );
    }
    if ((vaddr & cache_line_mask) != 0) {
        vaddr &= ~cache_line_mask;
        asm volatile ("dc civac, %0" :  : "r"(vaddr) : );
    }
    while (vaddr < __end) {
        asm volatile ("dc ivac, %0"  :  : "r"(vaddr) : );
        vaddr += cache_line_size;
    }
    asm volatile ("dsb	sy"  :  :  : );
}
static inline void arm64_clean_dcache_area(void* start, size_t size)
{
    u64   vaddr           = (u64)start;
    u64   __end           = (u64)start + size;
    u64   cache_line_size = arm64_read_dcache_line_size();
    u64   cache_line_mask = cache_line_size - 1;
    vaddr &= ~cache_line_mask;
    while (vaddr < __end) {
        asm volatile ("dc cvac, %0"  :  : "r"(vaddr) : );
        vaddr += cache_line_size;
    }
    asm volatile ("dsb	sy"  :  :  : );
}
static void arch_sync_for_cpu(void* virt_start, phys_addr_t phys_start, size_t size, enum uiomem_direction direction)
{
    if (direction != UIOMEM_WRITE_ONLY)
        arm64_inval_dcache_area(virt_start, size);
}
static void arch_sync_for_dev(void* virt_start, phys_addr_t phys_start, size_t size, enum uiomem_direction direction)
{
    if (direction == UIOMEM_READ_ONLY)
        arm64_inval_dcache_area(virt_start, size);
    else
        arm64_clean_dcache_area(virt_start, size);
}
#endif

sync_for_cpu および sync_for_device はそれぞれアーキテクチャに依存した arch_sync_for_cpu() および arch_sync_for_device() を呼び出します。

参考

18
13
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
18
13