Gerätetreiber zum "Cache-fähigen" Zugriff auf FPGA-Speicher von Linux

Einführung

Ablehnung

Der in diesem Artikel vorgestellte Gerätetreiber implementiert CPU-Datencache-Operationen in der Assemblersprache arm64 / arm. Daten-Cache-Operationen sollten normalerweise die Linux-Kernel-API verwenden, aber leider konnte nichts verwendet werden ("So" cache-fähiger "Zugriff auf FPGA-Speicher von Linux" @Qiita) ]).

Bitte beachten Sie, dass dieser Artikel nur ein Testartikel ist, den ich ein wenig ausprobiert habe.

Was ich machen wollte

Bereiten Sie beim Datenaustausch mit dem Abschnitt PS (Processing System) und dem Abschnitt PL (Programmable Logic) mit ZynqMP (ARM64) oder Zynq (ARM) Speicher wie BRAM auf der PL-Seite vor und greifen Sie von der CPU des PS-Abschnitts aus zu. Da ist ein Weg. Zu diesem Zeitpunkt ist es zweckmäßig, die folgenden Bedingungen zu erfüllen.

  1. Sie können den Datencache der CPU aktivieren.
  2. Sie können den Datencache der CPU manuell bedienen (Flush oder Invalidiate).
  3. Es kann nach dem Starten von Linux mit Device Tree Overlay usw. frei angehängt und getrennt werden.

Wenn Sie nur normal darauf zugreifen möchten, können Sie uio verwenden. Uio kann jedoch den Datencache der CPU unter Bedingung 1 nicht aktivieren, was hinsichtlich der Leistung beim Übertragen einer großen Datenmenge nachteilig ist.

Mit der in der Referenz ["Zugriff auf BRAM unter Linux"] gezeigten Methode using / dev / mem und reserved_memory kann der Cache-Vorgang zwar manuell aktiviert werden, der Cache-Vorgang kann jedoch nicht manuell ausgeführt werden, sodass die Daten auf der PL-Seite liegen Nicht für die Interaktion geeignet. Außerdem kann reserved_memory nur beim Booten von Linux angegeben werden, sodass es nach dem Booten von Linux nicht frei angehängt oder getrennt werden kann.

Was ich getan habe

Ich habe einen Prototyp eines Gerätetreibers erstellt, um unter Linux mit aktiviertem Cache auf den Speicher auf der PL-Seite zuzugreifen. Der Name ist uiomem. Es wird unter der folgenden URL veröffentlicht. (Es ist immer noch ein Prototyp der Alpha-Version.)

Dieser Artikel beschreibt Folgendes:

Auswirkung des Datencaches

In diesem Kapitel werden wir tatsächlich messen und zeigen, welche Auswirkungen der Datencache beim Zugriff auf den Speicher auf der PL-Seite von der PS-Seite hat.

Messumgebung

Die für die Messung verwendete Umgebung ist wie folgt.

Implementieren Sie das folgende Design auf der PL-Seite. 256 KByte Speicher werden auf der PL-Seite mit BRAM bereitgestellt, und der AXI BRAM-Controller von Xilinx wird für die Schnittstelle verwendet. Die Betriebsfrequenz beträgt 100MHz. ILA (Integrated Logic Analyzer) ist angeschlossen, um die AXI I / F des AXI BRAM Controllers und die Wellenform der BRAM I / F zu beobachten.

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

Abb.1 Blockdiagramm von PLBRAM-Ultra96


Diese Umgebungen werden auf Github veröffentlicht.

Speicher schreiben, wenn der Datencache ausgeschaltet ist

Es dauerte 0,496 ms, um den Datencache auszuschalten und mit memcpy () 256 KByte Daten in das BRAM auf der PL-Seite zu schreiben. Die Schreibgeschwindigkeit beträgt ca. 528 MByte / s.

Die AXI I / F-Wellenform war zu diesem Zeitpunkt wie folgt.

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

Abb.2 AXI-ZF-Wellenform des Speicherschreibens bei ausgeschaltetem Datencache


Wie Sie der Wellenform entnehmen können, erfolgt keine Burst-Übertragung (AWLEN = 00). Sie können sehen, dass jeweils ein Wort (16 Byte) übertragen wird.

Speicher schreiben, wenn der Datencache aktiviert ist

Es dauerte 0,317 ms, um den Datencache einzuschalten und mit memcpy () 256 KByte Daten in das BRAM auf der PL-Seite zu schreiben. Die Schreibgeschwindigkeit beträgt ca. 827 MByte / Sek.

Die AXI I / F-Wellenform war zu diesem Zeitpunkt wie folgt.

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

Fig. 3 AXI-ZF-Wellenform des Speicherschreibens, wenn der Datencache eingeschaltet ist


Wie Sie der Wellenform entnehmen können, wird eine Burst-Übertragung von 4 Wörtern (64 Bytes) in einem Schreibvorgang durchgeführt (AWLEN = 03).

Schreibvorgänge in den BRAM treten nicht auf, wenn die CPU schreibt. Wenn die CPU schreibt, werden die Daten zuerst in den Datencache und noch nicht in den BRAM geschrieben. Das Schreiben in BRAM erfolgt dann nur, wenn der Befehl zum Leeren des Datencaches manuell ausgeführt wird oder wenn der Datencache voll ist und der nicht verwendete Cache freigegeben wird. Zu diesem Zeitpunkt wird das Schreiben gemeinsam für jede Cache-Zeilengröße des Datencaches durchgeführt (64 Bytes für arm64).

Speicher gelesen, wenn der Datencache ausgeschaltet ist

Es dauerte 3,485 ms, um den Datencache auszuschalten und mit memcpy () 256 KByte Daten aus dem BRAM auf der PL-Seite zu lesen. Die Lesegeschwindigkeit beträgt ca. 75 MByte / s.

Die AXI I / F-Wellenform war zu diesem Zeitpunkt wie folgt.

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

Abb.4 AXI-ZF-Wellenform des gelesenen Speichers bei ausgeschaltetem Datencache


Wie Sie der Wellenform entnehmen können, erfolgt keine Burst-Übertragung (ARLEN = 00). Sie können sehen, dass jeweils ein Wort (16 Byte) übertragen wird.

Speicher gelesen, wenn der Datencache aktiviert ist

Es dauerte 0,409 ms, um den Datencache einzuschalten und mit memcpy () 256 KByte Daten aus dem BRAM auf der PL-Seite zu lesen. Die Lesegeschwindigkeit beträgt ca. 641 MByte / s.

Die AXI I / F-Wellenform war zu diesem Zeitpunkt wie folgt.

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

Abb.5 AXI-ZF-Wellenform des gelesenen Speichers, wenn der Datencache aktiviert ist


Wie Sie der Wellenform entnehmen können, wird eine Burst-Übertragung von 4 Wörtern (64 Bytes) in einem Lesevorgang durchgeführt (ARLEN = 03).

Wenn die CPU den Speicher liest und keine Daten im Datencache vorhanden sind, liest sie die Daten aus dem BRAM und füllt den Cache. Zu diesem Zeitpunkt wird die Cache-Zeilengröße des Datencaches (64 Bytes für arm64) gemeinsam aus dem BRAM gelesen. Danach werden die Daten aus dem Datencache an die CPU geliefert, solange sich Daten im Datencache befinden, und es erfolgt kein Zugriff auf BRAM. Daher wird der Speicherlesevorgang schneller ausgeführt als bei ausgeschaltetem Datencache. In dieser Umgebung wird die Leistung bei ausgeschaltetem Datencache erheblich auf 641 MByte / s verbessert, wenn der Datencache für 75 MByte / s eingeschaltet wird.

Wir stellen vor: uiomem

Was ist uiomem?

uiomem ist ein Linux-Gerätetreiber für den Zugriff auf Speicherbereiche, die nicht vom Linux-Kernel verwaltet werden, aus dem Benutzerbereich. uiomem hat die folgenden Funktionen.

Sie können Gerätedateien (wie / dev / uiomem0) verwenden, um sie dem Benutzerspeicher zuzuordnen, oder die Funktionen read () / write () verwenden, um vom Benutzerbereich aus auf den Speicher zuzugreifen.

Die Startadresse und Größe des Speicherplatzes können im Gerätebaum oder im Argument beim Laden des Gerätetreibers mit dem Befehl insmod angegeben werden.

Unterstützte Plattformen

Installation

Lade uiomem mit insmod. Zu diesem Zeitpunkt kann ein Speicherbereich, der nicht vom Linux-Kernel verwaltet wird, als Argument angegeben werden.

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

Einstellungen nach Gerätebaum

Zusätzlich zur Angabe eines Speicherbereichs, der nicht vom Linux-Kernel mit dem Argument insmod verwaltet wird, kann uiomem den Speicherbereich durch die Gerätebaumdatei angeben, die der Linux-Kernel beim Booten lädt. Wenn Sie der Gerätebaumdatei den folgenden Eintrag hinzufügen, wird / dev / uiomem0 automatisch erstellt, wenn Sie es mit insmod laden.

devicetree.dts


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

Der Speicherbereich wird durch die Eigenschaft reg angezeigt. Das erste Element der reg-Eigenschaft (zwei Elemente, wenn # address-cells 2 ist) gibt die Startadresse des Speicherbereichs an. Die verbleibenden Elemente der reg-Eigenschaft (2 Elemente, wenn # size-cells 2 ist) geben die Größe des Speicherbereichs in Bytes an. Im obigen Beispiel lautet die Startadresse des Speicherbereichs 0x04_0000_0000 und die Größe des Speicherbereichs 0x40000.

Geben Sie den Gerätenamen in der Eigenschaft Gerätename an.

Geben Sie die untergeordnete Nummer von uiomem in der Eigenschaft für die untergeordnete Nummer an. Kleinere Zahlen können zwischen 0 und 255 liegen. Das insmod-Argument hat jedoch Priorität, und wenn die kleinen Zahlen in Konflikt stehen, schlägt die im Gerätebaum angegebene fehl. Wenn die Minor-Number-Eigenschaft weggelassen wird, wird eine freie Minor-Nummer zugewiesen.

Der Gerätename wird wie folgt bestimmt.

  1. Wenn Gerätename angegeben wurde, Gerätename.
  2. Wenn der Gerätename weggelassen und die Minor-Nummer angegeben wird, sprintf ("uiomem% d", Minor-Nummer).
  3. Wenn der Gerätename weggelassen wird und auch die Nebennummer weggelassen wird, der Eintragsname des Gerätebaums (im Beispiel uiomem_plbram).

Gerätedatei

Wenn Sie uiomem in den Kernel laden, wird eine Gerätedatei erstellt, die der folgenden ähnelt: \ <Gerätename > ist der im vorherigen Abschnitt beschriebene Gerätename.

/dev/<device-name>

/ dev / \ <Gerätename > wird verwendet, um den Speicherbereich mit mmap () dem Benutzerbereich zuzuordnen oder um mit read () und write () auf den Speicherbereich zuzugreifen.

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();
		/*Prozess, um hier auf iomem zuzugreifen*/
		uiomem_sync_for_device();
		close(fd);
	}

Möglicherweise müssen Sie den Datencache steuern, wenn Sie mit mmap () dem Benutzerbereich zuordnen. Der Datencache wird von sync_for_cpu und sync_for_device gesteuert. Diese werden später beschrieben.

Sie können auch direkt aus der Shell lesen / schreiben, indem Sie die Gerätedatei mit dem Befehl dd oder ähnlichem angeben.

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 / \ <Gerätename > / phys_addr kann die Startadresse des Speicherbereichs lesen.

size

/ sys / class / uiomem / \ <Gerätename > / size kann die Größe des Speicherbereichs lesen.

sync_direction

/ sys / class / uiomem / \ <Gerätename > / sync_direction gibt die Zugriffsrichtung an, wenn der Cache von uiomem manuell gesteuert wird.

sync_offset

/ sys / class / uiomem / \ <Gerätename > / sync_offset gibt den Beginn des Bereichs an, wenn die Cache-Steuerung manuell als Offset-Wert aus dem Speicherbereich ausgeführt wird.

sync_size

/ sys / class / uiomem / \ <Gerätename > / sync_size gibt die Größe des Bereichs für die manuelle Cache-Steuerung an.

sync_for_cpu

/ sys / class / uiomem / \ <Gerätename > / sync_for_cpu macht den CPU-Cache ungültig, indem bei manueller Steuerung des Caches ein Wert ungleich Null in diese Gerätedatei geschrieben wird. Diese Gerätedatei ist schreibgeschützt.

Wenn Sie 1 in diese Gerätedatei schreiben, / sys / class / uiomem / \ <Gerätename > / sync_offset und / sys, wenn sync_direction 2 (= schreibgeschützt) oder 0 (= bidirektional lesen / schreiben) ist. Der durch / class / uiomem / \ <Gerätename > / sync_size angegebene Bereich des CPU-Cache ist ungültig.

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

Die in diese Gerätedatei geschriebenen Werte können sync_offset, sync_size und sync_direction wie folgt umfassen:

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

Die von dieser Methode angegebenen sync_offset, sync_size und sync_direction sind temporär und die Gerätedateien / sys / class / uiomem / \ <Gerätename > / sync_offset, / sys / class / uiomem / \ <Gerätename Die Werte von > / sync_size und / sys / class / uiomem / \ <Gerätename > / sync_direction sind davon nicht betroffen.

Aus Formatgründen ist der Bereich, der mit sync_offset und sync_size angegeben werden kann, nur der Bereich, der durch 32 Bit angegeben werden kann.

sync_for_device

/ sys / class / uiomem / \ <Gerätename > / sync_for_device leert den CPU-Cache, indem bei manueller Steuerung des Caches ein Wert ungleich Null in diese Gerätedatei geschrieben wird. Diese Gerätedatei ist schreibgeschützt.

Wenn Sie 1 in diese Gerätedatei schreiben, wenn sync_direction 1 (= nur Schreiben) oder 0 (= bidirektional lesen / schreiben) ist, / sys / class / uiomem / \ <Gerätename > / sync_offset und / sys Der CPU-Cache in dem durch / class / uiomem / \ <Gerätename > / sync_size angegebenen Bereich wird geleert.

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

Die in diese Gerätedatei geschriebenen Werte können sync_offset, sync_size und sync_direction wie folgt umfassen:

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

Die von dieser Methode angegebenen sync_offset, sync_size und sync_direction sind temporär und die Gerätedateien / sys / class / uiomem / \ <Gerätename > / sync_offset, / sys / class / uiomem / \ <Gerätename Die Werte von > / sync_size und / sys / class / uiomem / \ <Gerätename > / sync_direction sind davon nicht betroffen.

Aus Formatgründen ist der Bereich, der mit sync_offset und sync_size angegeben werden kann, nur der Bereich, der durch 32 Bit angegeben werden kann.

Datencache-Steuerung

Wenn Sie den Speicher auf der PL-Seite nur als Speicher verwenden möchten, auf den nur von der CPU aus zugegriffen werden kann, müssen Sie nur den Datencache aktivieren. Das Aktivieren des Datencaches reicht jedoch nicht aus, um den PL-seitigen Speicher zu aktivieren oder zu deaktivieren, wenn andere Geräte als die CPU darauf zugreifen, oder um den PL-seitigen Speicher nach dem Start von Linux zu aktivieren oder zu deaktivieren. Da Datenfehlanpassungen zwischen dem Datencache und dem Speicher auf der PL-Seite auftreten können, ist es erforderlich, den Inhalt des Datencaches und des Speichers auf der PL-Seite in irgendeiner Weise abzugleichen.

uiomem implementiert die Datencache-Steuerung direkt mithilfe der arm64 / arm-Datencache-Anweisungen.

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 und sync_for_device rufen arch_sync_for_cpu () und arch_sync_for_device () auf, die jeweils von der Architektur abhängig sind.

Referenz

["So" cache-fähiger "Zugriff auf FPGA-Speicher von Linux" @Qiita]: https://qiita.com/ikwzm/items/1580e89ecdb9cf9392eb "" Cache von Linux auf FPGA-Speicher aktiviert " "So greifen Sie auf" @Qiita "zu [「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

Gerätetreiber zum "Cache-fähigen" Zugriff auf FPGA-Speicher von Linux
So "cache-fähiger" Zugriff auf FPGA-Speicher von Linux
ODBC-Zugriff auf SQL Server von Linux mit Python
So greifen Sie über Python auf Wikipedia zu
Gerätetreiber (NumPy-kompatibel) für Programme und Hardware, die unter Linux im Benutzerbereich ausgeführt werden, um Speicher gemeinsam zu nutzen