Pilote de périphérique pour accéder à la mémoire FPGA à partir de Linux

introduction

Refus

Le pilote de périphérique présenté dans cet article implémente les opérations de cache de données du processeur dans le langage d'assemblage arm64 / arm. Les opérations de cache de données devraient normalement utiliser l'API du noyau Linux, mais malheureusement, rien ne pouvait être utilisé ("Comment" activer le cache "pour accéder à la mémoire FPGA depuis Linux" @Qiita ]).

Veuillez noter que cet article n'est qu'un article d'essai que j'ai un peu essayé.

Ce que je voulais faire

Lors de l'échange de données avec la section PS (Processing System) et la section PL (Programmable Logic) avec ZynqMP (ARM64) ou Zynq (ARM), préparez une mémoire telle que BRAM du côté PL et accédez à partir du CPU de la section PS. Il y a un moyen. À ce moment-là, il convient de remplir les conditions suivantes.

  1. Vous pouvez activer le cache de données de la CPU.
  2. Vous pouvez utiliser manuellement le cache de données de la CPU (Flush ou Invalidiate).
  3. Il peut être librement attaché et détaché après le démarrage de Linux avec la superposition de l'arborescence des périphériques, etc.

Si vous souhaitez simplement y accéder normalement, vous pouvez utiliser uio. Cependant, uio ne peut pas activer le cache de données du CPU sous la condition 1, ce qui est désavantageux en termes de performances lors du transfert d'une grande quantité de données.

De plus, avec la méthode utilisant / dev / mem et reserved_memory indiquée dans la référence ["Accéder à BRAM sous Linux"], bien que le cache de données puisse être activé, l'opération de cache ne peut pas être effectuée manuellement, donc les données avec le côté PL Ne convient pas à l'interaction. De plus, reserved_memory ne peut être spécifié qu'au démarrage de Linux, il ne peut donc pas être librement attaché ou détaché après le démarrage de Linux.

Ce que j'ai fait

J'ai créé un pilote de périphérique prototype pour accéder à la mémoire côté PL depuis Linux avec le cache activé. Le nom est uiomem. Il est publié à l'URL suivante. (Il s'agit toujours d'un prototype de version alpha.)

Cet article décrit les éléments suivants:

Effet du cache de données

Dans ce chapitre, nous mesurerons et montrerons l'effet du cache de données lors de l'accès à la mémoire côté PL depuis le côté PS.

Environnement de mesure

L'environnement utilisé pour la mesure est le suivant.

Implémentez la conception suivante du côté PL. 256 Ko de mémoire sont montés du côté PL avec BRAM et le contrôleur AXI BRAM de Xilinx est utilisé pour l'interface. La fréquence de fonctionnement est de 100 MHz. ILA (Integrated Logic Analyzer) est connecté pour observer l'AXI I / F du contrôleur AXI BRAM et la forme d'onde de BRAM I / F.

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

Fig.1 Schéma fonctionnel du PLBRAM-Ultra96


Ces environnements sont publiés sur github.

Écriture en mémoire lorsque le cache de données est désactivé

Il a fallu 0,496 ms pour désactiver le cache de données et utiliser memcpy () pour écrire 256 Ko de données dans le BRAM du côté PL. La vitesse d'écriture est d'environ 528 Mo / s.

La forme d'onde AXI I / F à ce moment-là était la suivante.

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

Fig.2 Forme d'onde AXI IF de l'écriture en mémoire lorsque le cache de données est désactivé


Comme vous pouvez le voir sur la forme d'onde, il n'y a pas de transfert en rafale (AWLEN = 00). Vous pouvez voir qu'il transfère un mot (16 octets) à la fois.

Écriture en mémoire lorsque le cache de données est activé

Il a fallu 0,317 ms pour activer le cache de données et utiliser memcpy () pour écrire 256 Ko de données dans le BRAM du côté PL. La vitesse d'écriture est d'environ 827 Mo / s.

La forme d'onde AXI I / F à ce moment-là était la suivante.

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

Fig.3 Forme d'onde AXI IF de l'écriture en mémoire lorsque le cache de données est activé


Comme vous pouvez le voir sur la forme d'onde, un transfert en rafale de 4 mots (64 octets) est effectué en une seule écriture (AWLEN = 03).

Les écritures dans le BRAM ne se produisent pas lorsque le CPU écrit. Lorsque le CPU écrit, les données sont d'abord écrites dans le cache de données et pas encore écrites dans le BRAM. Ensuite, l'écriture dans BRAM se produit uniquement lorsque l'instruction de vidage du cache de données est exécutée manuellement ou lorsque le cache de données est plein et le cache inutilisé est libéré. A ce moment, l'écriture est effectuée collectivement pour chaque taille de ligne de cache du cache de données (64 octets pour arm64).

Mémoire lue lorsque le cache de données est désactivé

Il a fallu 3,485 ms pour désactiver le cache de données et utiliser memcpy () pour lire 256 Ko de données depuis le BRAM du côté PL. La vitesse de lecture est d'environ 75 Mo / s.

La forme d'onde AXI I / F à ce moment-là était la suivante.

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

Fig.4 Forme d'onde AXI IF de la mémoire lue lorsque le cache de données est désactivé


Comme vous pouvez le voir sur la forme d'onde, il n'y a pas de transfert en rafale (ARLEN = 00). Vous pouvez voir qu'il transfère un mot (16 octets) à la fois.

Mémoire lue lorsque le cache de données est activé

Il a fallu 0,409 ms pour activer le cache de données et utiliser memcpy () pour lire 256 Ko de données à partir du BRAM du côté PL. La vitesse de lecture est d'environ 641 Mo / s.

La forme d'onde AXI I / F à ce moment-là était la suivante.

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

Fig.5 Forme d'onde AXI IF de la mémoire lue lorsque le cache de données est activé


Comme vous pouvez le voir sur la forme d'onde, un transfert en rafale de 4 mots (64 octets) est effectué en une seule lecture (ARLEN = 03).

Lorsque le CPU lit la mémoire, s'il n'y a pas de données dans le cache de données, il lit les données du BRAM et remplit le cache. A ce moment, la taille de la ligne de cache du cache de données (64 octets pour arm64) est collectivement lue à partir de BRAM. Après cela, tant qu'il y a des données dans le cache de données, les données seront fournies au CPU à partir du cache de données et aucun accès à BRAM n'aura lieu. Par conséquent, la lecture de la mémoire est effectuée plus rapidement que lorsque le cache de données est désactivé. Dans cet environnement, lorsque le cache de données est désactivé, les performances sont considérablement améliorées à 641 Mo / s lorsque le cache de données est activé pendant 75 Mo / s.

Présentation de uiomem

Qu'est-ce que uiomem

uiomem est un pilote de périphérique Linux permettant d'accéder aux zones mémoire non gérées par le noyau Linux depuis l'espace utilisateur. uiomem a les fonctions suivantes.

Vous pouvez utiliser un fichier de périphérique (tel que / dev / uiomem0) pour mapper à l'espace mémoire utilisateur, ou utiliser les fonctions read () / write () pour accéder à la mémoire depuis l'espace utilisateur.

L'adresse de départ et la taille de l'espace mémoire peuvent être spécifiées dans l'arborescence des périphériques ou dans l'argument lors du chargement du pilote de périphérique avec la commande insmod.

Plateformes prises en charge

Installation

Chargez uiomem avec insmod. À ce stade, une zone mémoire non gérée par le noyau Linux peut être spécifiée comme argument.

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

Configuration par arborescence d'appareils

En plus de spécifier une zone mémoire qui n'est pas gérée par le noyau Linux avec l'argument insmod, uiomem peut spécifier la zone mémoire par le fichier d'arborescence de périphériques que le noyau Linux charge au démarrage. Si vous ajoutez l'entrée suivante au fichier de l'arborescence des périphériques, / dev / uiomem0 sera créé automatiquement lorsque vous le chargerez avec insmod.

devicetree.dts


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

La zone mémoire est indiquée par la propriété reg. Le premier élément de la propriété reg (deux éléments si # address-cells vaut 2) indique l'adresse de début de la zone mémoire. Les éléments restants de la propriété reg (2 éléments si # size-cells vaut 2) indiquent la taille de la zone mémoire en octets. Dans l'exemple ci-dessus, l'adresse de début de la zone mémoire est 0x04_0000_0000 et la taille de la zone mémoire est 0x40000.

Spécifiez le nom du périphérique dans la propriété nom du périphérique.

Spécifiez le nombre mineur de uiomem dans la propriété Minor-number. Les nombres mineurs peuvent être compris entre 0 et 255. Cependant, l'argument insmod a la priorité et si les nombres mineurs sont en conflit, celui spécifié dans l'arborescence des périphériques échouera. Si la propriété de numéro mineur est omise, un numéro mineur gratuit sera attribué.

Le nom de l'appareil est déterminé comme suit.

  1. Si le nom du périphérique a été spécifié, nom du périphérique.
  2. Si le nom du périphérique est omis et que le nombre mineur est spécifié, sprintf ("uiomem% d", nombre-mineur).
  3. Si le nom du périphérique est omis et que le numéro mineur est également omis, le nom d'entrée de devicetree (uiomem_plbram dans l'exemple).

Fichier de l'appareil

Le chargement de uiomem dans le noyau créera un fichier de périphérique similaire au suivant: \ <nom-périphérique > est le nom du périphérique décrit dans la section précédente.

/dev/<device-name>

/ dev / \ <nom-du-périphérique > est utilisé pour mapper la zone de mémoire à l'espace utilisateur en utilisant mmap () ou pour accéder à la zone de mémoire en utilisant read () et 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();
		/*Processus pour accéder à iomem ici*/
		uiomem_sync_for_device();
		close(fd);
	}

Vous devrez peut-être contrôler le cache de données lors du mappage vers l'espace utilisateur à l'aide de mmap (). Le cache de données est contrôlé par sync_for_cpu et sync_for_device. Ceux-ci seront décrits plus tard.

Vous pouvez également lire / écrire directement à partir du shell en spécifiant le fichier de périphérique avec la commande dd ou autre.

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 / \ <nom-périphérique > / phys_addr peut lire l'adresse de début de la zone de mémoire.

size

/ sys / class / uiomem / \ <nom-périphérique > / size peut lire la taille de la zone mémoire.

sync_direction

/ sys / class / uiomem / \ <nom-périphérique > / sync_direction spécifie la direction d'accès lors du contrôle manuel du cache de uiomem.

sync_offset

/ sys / class / uiomem / \ <nom-de-périphérique > / sync_offset spécifie le début de la plage lors de l'exécution manuelle du contrôle du cache en tant que valeur de décalage à partir de la zone de mémoire.

sync_size

/ sys / class / uiomem / \ <nom-périphérique > / sync_size spécifie la taille de la plage pour le contrôle manuel du cache.

sync_for_cpu

/ sys / class / uiomem / \ <nom-de-périphérique > / sync_for_cpu invalide le cache du processeur en écrivant une valeur différente de zéro dans ce fichier de périphérique lors du contrôle manuel du cache. Ce fichier de périphérique est en écriture seule.

Si vous écrivez 1 dans ce fichier de périphérique, / sys / class / uiomem / \ <nom-de-périphérique > / sync_offset et / sys lorsque sync_direction est 2 (= lecture seule) ou 0 (= lecture / écriture bidirectionnelle) La plage de cache du processeur spécifiée par / class / uiomem / \ <nom-périphérique > / taille_sync est invalidée.

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);
 	}
}

Les valeurs écrites dans ce fichier de périphérique peuvent inclure sync_offset, sync_size et sync_direction comme suit:

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 et sync_direction spécifiés par cette méthode sont temporaires, et les fichiers de périphérique / sys / class / uiomem / \ <nom-périphérique > / sync_offset, / sys / class / uiomem / \ <nom-périphérique Cela n'affecte pas les valeurs de > / sync_size et / sys / class / uiomem / \ <nom-périphérique > / sync_direction.

De plus, pour des raisons de format, la plage qui peut être spécifiée avec sync_offset et sync_size est uniquement la plage qui peut être indiquée par 32 bits.

sync_for_device

/ sys / class / uiomem / \ <nom-périphérique > / sync_for_device vide le cache du processeur en écrivant une valeur différente de zéro dans ce fichier de périphérique lors du contrôle manuel du cache. Ce fichier de périphérique est en écriture seule.

Si vous écrivez 1 dans ce fichier de périphérique, / sys / class / uiomem / \ <nom-de-périphérique > / sync_offset et / sys lorsque sync_direction est 1 (= écriture seule) ou 0 (= lecture / écriture bidirectionnelle) Le cache du processeur dans la plage spécifiée par / class / uiomem / \ <nom-périphérique > / taille_sync est vidé.

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);
 	}
}

Les valeurs écrites dans ce fichier de périphérique peuvent inclure sync_offset, sync_size et sync_direction comme suit:

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 et sync_direction spécifiés par cette méthode sont temporaires, et les fichiers de périphérique / sys / class / uiomem / \ <nom-périphérique > / sync_offset, / sys / class / uiomem / \ <nom-périphérique Cela n'affecte pas les valeurs de > / sync_size et / sys / class / uiomem / \ <nom-périphérique > / sync_direction.

De plus, pour des raisons de format, la plage qui peut être spécifiée avec sync_offset et sync_size est uniquement la plage qui peut être indiquée par 32 bits.

Contrôle du cache de données

Si vous souhaitez uniquement utiliser la mémoire côté PL comme mémoire accessible uniquement à partir de la CPU, il vous suffit d'activer le cache de données. Cependant, l'activation de la mise en cache des données ne suffit pas pour activer ou désactiver la mémoire côté PL lorsque des périphériques autres que le processeur y accèdent, ou pour activer ou désactiver la mémoire côté PL après le démarrage de Linux. Étant donné qu'une discordance de données entre le cache de données et la mémoire du côté PL peut se produire, il est nécessaire de faire correspondre le contenu du cache de données et la mémoire du côté PL d'une certaine manière.

uiomem implémente le contrôle du cache de données directement en utilisant les instructions de cache de données 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 et sync_for_device appellent respectivement arch_sync_for_cpu () et arch_sync_for_device (), qui dépendent de l'architecture.

référence

["Comment accéder à la mémoire FPGA" avec mise en cache "depuis Linux" @Qiita]: https://qiita.com/ikwzm/items/1580e89ecdb9cf9392eb "" Cache activé de Linux vers la mémoire FPGA " "Comment accéder à" @Qiita " [「Accessing BRAM In Linux」]: https://xilinx-wiki.atlassian.net/wiki/spaces/A/pages/18842412/Accessing+BRAM+In+Linux "「Accessing BRAM In Linux」" [uiomem v1.0.0-alpha.1]: https://github.com/ikwzm/uiomem/tree/v1.0.0-alpha.1 "uiomem v1.0.0-alpha.1" [ZynqMP-FPGA-Linux v2020.1.1]: https://github.com/ikwzm/ZynqMP-FPGA-Linux/tree/v2020.1.1 "ZynqMP-FPGA-Linux v2020.1.1"

Recommended Posts

Pilote de périphérique pour accéder à la mémoire FPGA à partir de Linux
Comment "mettre en cache" l'accès à la mémoire FPGA à partir de Linux
Accès ODBC à SQL Server depuis Linux avec Python
Comment accéder à wikipedia depuis python
Pilote de périphérique (compatible NumPy) pour les programmes et le matériel qui s'exécutent dans l'espace utilisateur sous Linux pour partager la mémoire