Dieser Artikel implementiert CPU-Datencache-Operationen in der Assemblersprache arm64. Daten-Cache-Operationen sollten normalerweise die Linux-Kernel-API verwenden, aber leider war keine verfügbar (dazu später mehr). Bitte beachten Sie, dass dieser Artikel nur ein Testartikel ist, den ich ein wenig ausprobiert habe.
Beim Datenaustausch zwischen dem Abschnitt PS (Processing System) und dem Abschnitt PL (Programmable Logic) mit ZynqMP (ARM64) gibt es eine Methode, um einen Speicher wie BRAM auf der PL-Seite vorzubereiten und von der CPU des PS-Abschnitts darauf zuzugreifen.
Dieser Artikel beschreibt, wie Sie von Linux aus auf PL-seitigen Speicher zugreifen, um die folgenden Bedingungen zu erfüllen:
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 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.
Der Autor veröffentlicht [udmabuf] in Open Source.
Ich habe versucht, udmabuf probeweise um eine Funktion zu erweitern, damit auf den Speicher auf der PL-Seite von Linux aus zugegriffen werden kann (es scheint Mist zu sein, aber es ist nur ein Test). Dann implementierte ich ein Beispieldesign unter Verwendung von BRAM im Speicher auf der PL-Seite und bestätigte den Effekt des Datencaches.
Dieser Artikel beschreibt Folgendes:
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.
Die für die Messung verwendete Umgebung ist wie folgt.
Ultra96-V2
[ZynqMP-FPGA-Linux v2019.2.1]
linux-xlnx v2019.2 (Linux Kernel 4.19)
Debian10
Xilinx Vivado 2019.2
[udmabuf v2.2.0-rc2]
Implementieren Sie das folgende Design auf der PL-Seite. 256 KByte Speicher werden mit BRAM auf der PL-Seite 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.
Abb.1 Blockdiagramm von PLBRAM-Ultra96
Diese Umgebungen werden auf Github veröffentlicht.
Es dauerte 0,496 ms, um den Datencache auszuschalten und mit memcpy () 256 KByte Daten in den 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.
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.
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.
Abb.3 AXI-ZF-Wellenform des Speicherschreibens, wenn der Datencache aktiviert 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).
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.
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.
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.
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.
Linux hat ein Framework namens Dynamic DMA Mapping (DMA-Mapping). Der Linux-Kernel verwendet normalerweise virtuelle Adressen. Wenn Ihr Gerät DMA unterstützt, benötigen Sie normalerweise eine physische Adresse. DMA-Mapping ist ein Framework zur Überbrückung des Linux-Kernels mit Geräten, die DMA unterstützen, zum Zuweisen und Verwalten von DMA-Puffern, zum Übersetzen physischer und virtueller Adressen und zum Verwalten von Datencaches, falls erforderlich. .. Weitere Informationen finden Sie unter DMA-API-HOWTO des Linux-Kernels.
Bei der DMA-Zuordnung wird normalerweise, wenn dma_alloc_coherent () einen DMA-Puffer zuweist, ein DMA-Puffer im Speicher des Linux-Kernels zugewiesen. Da sich der Speicher auf der PL-Seite nicht im Speicher des Linux-Kernels befindet, muss ein Weg gefunden werden, um einen DMA-Puffer im Speicher auf der PL-Seite zu sichern.
Eine Möglichkeit, einen DMA-Puffer im Speicher auf der PL-Seite zuzuweisen, besteht darin, den reservierten Speicher im Gerätebaum zu verwenden. Im Folgenden erfahren Sie, wie Sie den reservierten Speicher verwenden.
Die Methode mit reserviertem Speicher kann die Gerätebaumüberlagerung nicht unterstützen. Dies liegt daran, dass der reservierte Speicher nicht neu konfiguriert werden kann, sobald er beim Booten des Linux-Kernels konfiguriert wurde. Der reservierte Speicher wird von Device Tree Overlay nicht abgedeckt.
Wenn dma_alloc_coherent () einen DMA-Puffer zuweist, ordnet es normalerweise einen DMA-Puffer im Speicher des Linux-Kernels zu. Ein Mechanismus namens Device Coherent Pool kann jedoch verwendet werden, um einen DMA-Puffer aus dem Speicher außerhalb des Linux-Kernels zuzuweisen. Der Quellcode für den Gerätekohärenzpool befindet sich in kernel / dma / kohärent.c.
dma_declare_coherent_memory()
Verwenden Sie zur Verwendung des Gerätekohärenzpools zunächst die Funktion dma_declare_coherent_memory (). dma_declare_coherent_memory () sieht folgendermaßen aus:
kernel/dma/coherent.c
int dma_declare_coherent_memory(struct device *dev, phys_addr_t phys_addr,
dma_addr_t device_addr, size_t size, int flags)
{
struct dma_coherent_mem *mem;
int ret;
ret = dma_init_coherent_memory(phys_addr, device_addr, size, flags, &mem);
if (ret)
return ret;
ret = dma_assign_coherent_memory(dev, mem);
if (ret)
dma_release_coherent_memory(mem);
return ret;
}
EXPORT_SYMBOL(dma_declare_coherent_memory);
Geben Sie die physische Adresse des Speichers an, den Sie phys_addr zuweisen möchten, die Adresse auf dem Gerät für device_addr und die Größe des Speichers, den Sie für die Größe zuweisen möchten.
Initialisieren Sie den Puffer mit dma_init_cohrent_memory () und weisen Sie ihn in der Gerätestruktur mit dma_assign_coherent_memory () dev zu.
Wenn Sie nun dma_alloc_coherent () verwenden, um einen DMA-Puffer für dev zuzuweisen, wird der DMA-Puffer aus dem durch dma_declare_coherent_memory () angegebenen Speicher zugewiesen.
dma_alloc_coherent()
Insbesondere wird der Mechanismus erläutert, mit dem dma_alloc_coherent () den DMA-Puffer aus dem von dma_declare_coherent_memory () zugewiesenen Speicherbereich zuweist. dma_alloc_coherent () wird in include / linux / dma-Mapping.h wie folgt definiert:
include/linux/dma-mapping.h
static inline void *dma_alloc_attrs(struct device *dev, size_t size,
dma_addr_t *dma_handle, gfp_t flag,
unsigned long attrs)
{
const struct dma_map_ops *ops = get_dma_ops(dev);
void *cpu_addr;
BUG_ON(!ops);
WARN_ON_ONCE(dev && !dev->coherent_dma_mask);
if (dma_alloc_from_dev_coherent(dev, size, dma_handle, &cpu_addr))
return cpu_addr;
/* let the implementation decide on the zone to allocate from: */
flag &= ~(__GFP_DMA | __GFP_DMA32 | __GFP_HIGHMEM);
if (!arch_dma_alloc_attrs(&dev))
return NULL;
if (!ops->alloc)
return NULL;
cpu_addr = ops->alloc(dev, size, dma_handle, flag, attrs);
debug_dma_alloc_coherent(dev, size, *dma_handle, cpu_addr);
return cpu_addr;
}
(Unterlassung)
static inline void *dma_alloc_coherent(struct device *dev, size_t size,
dma_addr_t *dma_handle, gfp_t flag)
{
return dma_alloc_attrs(dev, size, dma_handle, flag, 0);
}
Die von dma_alloc_coherent () aufgerufenen dma_alloc_attrs () rufen früher dma_alloc_from_dev_coherent () auf. dma_alloc_from_dev_coherent () ist in kernel / dma / kohärent.c wie folgt definiert:
kernel/dma/coherent.c
static inline struct dma_coherent_mem *dev_get_coherent_memory(struct device *dev)
{
if (dev && dev->dma_mem)
return dev->dma_mem;
return NULL;
}
(Unterlassung)
/**
* dma_alloc_from_dev_coherent() - allocate memory from device coherent pool
* @dev: device from which we allocate memory
* @size: size of requested memory area
* @dma_handle: This will be filled with the correct dma handle
* @ret: This pointer will be filled with the virtual address
* to allocated area.
*
* This function should be only called from per-arch dma_alloc_coherent()
* to support allocation from per-device coherent memory pools.
*
* Returns 0 if dma_alloc_coherent should continue with allocating from
* generic memory areas, or !0 if dma_alloc_coherent should return @ret.
*/
int dma_alloc_from_dev_coherent(struct device *dev, ssize_t size,
dma_addr_t *dma_handle, void **ret)
{
struct dma_coherent_mem *mem = dev_get_coherent_memory(dev);
if (!mem)
return 0;
*ret = __dma_alloc_from_coherent(mem, size, dma_handle);
if (*ret)
return 1;
/*
* In the case where the allocation can not be satisfied from the
* per-device area, try to fall back to generic memory if the
* constraints allow it.
*/
return mem->flags & DMA_MEMORY_EXCLUSIVE;
}
EXPORT_SYMBOL(dma_alloc_from_dev_coherent);
dma_alloc_from_dev_coherent () ruft zuerst dev_get_coherent_memory () auf, um dma_mem in der Gerätestruktur zu überprüfen. Wenn dma_mem NULL ist, wird es zurückgegeben, ohne etwas zu tun. Wenn jedoch der Gerätekohärenzpool dma_mem von dma_declare_coherent_memory () zugewiesen wird, weist \ _dma_alloc_from_coherent () einen DMA-Puffer aus dem Gerätekohärenzpool zu.
dma_release_declared_memory()
Der der Gerätestruktur mit dma_declare_coherent_memory () zugewiesene Gerätekohärenzpool wird mit dma_release_declared_memory () freigegeben.
kernel/dma/coherent.c
void dma_release_declared_memory(struct device *dev)
{
struct dma_coherent_mem *mem = dev->dma_mem;
if (!mem)
return;
dma_release_coherent_memory(mem);
dev->dma_mem = NULL;
}
EXPORT_SYMBOL(dma_release_declared_memory);
dma_mmap_from_dev_coherent()
Verwenden Sie dma_mmap_from_dev_coherent (), um den zugewiesenen DMA-Puffer dem Benutzerbereich zuzuordnen. Normalerweise wird dma_mmap_cohrent () verwendet, um den DMA-Puffer dem Benutzerbereich zuzuordnen, aber dma_mmap_from_dev_cohrent () kann verwendet werden, um mit aktiviertem Datencache zuzuordnen.
dma_mmap_from_dev_coherent () ist wie folgt definiert: Beachten Sie, dass dma_mmap_from_dev_coherent () keine Änderungen an vma-> vm_page_prot vorgenommen hat.
kernel/dma/coherent.c
static int __dma_mmap_from_coherent(struct dma_coherent_mem *mem,
struct vm_area_struct *vma, void *vaddr, size_t size, int *ret)
{
if (mem && vaddr >= mem->virt_base && vaddr + size <=
(mem->virt_base + (mem->size << PAGE_SHIFT))) {
unsigned long off = vma->vm_pgoff;
int start = (vaddr - mem->virt_base) >> PAGE_SHIFT;
int user_count = vma_pages(vma);
int count = PAGE_ALIGN(size) >> PAGE_SHIFT;
*ret = -ENXIO;
if (off < count && user_count <= count - off) {
unsigned long pfn = mem->pfn_base + start + off;
*ret = remap_pfn_range(vma, vma->vm_start, pfn,
user_count << PAGE_SHIFT,
vma->vm_page_prot);
}
return 1;
}
return 0;
}
/**
* dma_mmap_from_dev_coherent() - mmap memory from the device coherent pool
* @dev: device from which the memory was allocated
* @vma: vm_area for the userspace memory
* @vaddr: cpu address returned by dma_alloc_from_dev_coherent
* @size: size of the memory buffer allocated
* @ret: result from remap_pfn_range()
*
* This checks whether the memory was allocated from the per-device
* coherent memory pool and if so, maps that memory to the provided vma.
*
* Returns 1 if @vaddr belongs to the device coherent pool and the caller
* should return @ret, or 0 if they should proceed with mapping memory from
* generic areas.
*/
int dma_mmap_from_dev_coherent(struct device *dev, struct vm_area_struct *vma,
void *vaddr, size_t size, int *ret)
{
struct dma_coherent_mem *mem = dev_get_coherent_memory(dev);
return __dma_mmap_from_coherent(mem, vma, vaddr, size, ret);
}
EXPORT_SYMBOL(dma_mmap_from_dev_coherent);
Andererseits verwenden Sie normalerweise dma_mmap_cohrent (), um den DMA-Puffer dem Benutzerbereich zuzuordnen. dma_mmap_coherent () wird in include / linux / dma-Mapping.h wie folgt definiert:
include/linux/dma-mapping.h
/**
* dma_mmap_attrs - map a coherent DMA allocation into user space
* @dev: valid struct device pointer, or NULL for ISA and EISA-like devices
* @vma: vm_area_struct describing requested user mapping
* @cpu_addr: kernel CPU-view address returned from dma_alloc_attrs
* @handle: device-view address returned from dma_alloc_attrs
* @size: size of memory originally requested in dma_alloc_attrs
* @attrs: attributes of mapping properties requested in dma_alloc_attrs
*
* Map a coherent DMA buffer previously allocated by dma_alloc_attrs
* into user space. The coherent DMA buffer must not be freed by the
* driver until the user space mapping has been released.
*/
static inline int
dma_mmap_attrs(struct device *dev, struct vm_area_struct *vma, void *cpu_addr,
dma_addr_t dma_addr, size_t size, unsigned long attrs)
{
const struct dma_map_ops *ops = get_dma_ops(dev);
BUG_ON(!ops);
if (ops->mmap)
return ops->mmap(dev, vma, cpu_addr, dma_addr, size, attrs);
return dma_common_mmap(dev, vma, cpu_addr, dma_addr, size);
}
#define dma_mmap_coherent(d, v, c, h, s) dma_mmap_attrs(d, v, c, h, s, 0)
dma_mmap_attrs () ruft mmap () auf dem architekturabhängigen dma_map_ops auf. Die mmap () für arm64 sieht folgendermaßen aus:
arch/arm64/mm/dma-mapping.c
static const struct dma_map_ops arm64_swiotlb_dma_ops = {
.alloc = __dma_alloc,
.free = __dma_free,
.mmap = __swiotlb_mmap,
.get_sgtable = __swiotlb_get_sgtable,
.map_page = __swiotlb_map_page,
.unmap_page = __swiotlb_unmap_page,
.map_sg = __swiotlb_map_sg_attrs,
.unmap_sg = __swiotlb_unmap_sg_attrs,
.sync_single_for_cpu = __swiotlb_sync_single_for_cpu,
.sync_single_for_device = __swiotlb_sync_single_for_device,
.sync_sg_for_cpu = __swiotlb_sync_sg_for_cpu,
.sync_sg_for_device = __swiotlb_sync_sg_for_device,
.dma_supported = __swiotlb_dma_supported,
.mapping_error = __swiotlb_dma_mapping_error,
};
(Ausgelassen)
arch/arm64/mm/dma-mapping.c
static int __swiotlb_mmap(struct device *dev,
struct vm_area_struct *vma,
void *cpu_addr, dma_addr_t dma_addr, size_t size,
unsigned long attrs)
{
int ret;
unsigned long pfn = dma_to_phys(dev, dma_addr) >> PAGE_SHIFT;
vma->vm_page_prot = __get_dma_pgprot(attrs, vma->vm_page_prot,
is_device_dma_coherent(dev));
if (dma_mmap_from_dev_coherent(dev, vma, cpu_addr, size, &ret))
return ret;
return __swiotlb_mmap_pfn(vma, pfn, size);
}
Beachten Sie, dass \ _swiotlb_mmap () auch einmal dma_mmap_from_dev_cohrent () aufruft, aber vorher \ _get_dma_pgprot () verwendet, um vma-> vm_page_prot zu überschreiben.
Und \ _get_dma_pgprot () sieht folgendermaßen aus:
arch/arm64/mm/dma-mapping.c
static pgprot_t __get_dma_pgprot(unsigned long attrs, pgprot_t prot,
bool coherent)
{
if (!coherent || (attrs & DMA_ATTR_WRITE_COMBINE))
return pgprot_writecombine(prot);
return prot;
}
Das Zwischenspeichern von Daten wurde von pgprot_writecombine () deaktiviert.
Wenn Sie also einen DMA-Puffer dem Benutzerbereich zuordnen, rufen Sie dma_mmap_from_dev_cohrent () direkt auf, anstatt dma_mmap_coherent () aufzurufen, wodurch der Datencache deaktiviert wird.
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 ein anderes Gerät als die CPU darauf zugreift, 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.
include/linux/dma-mapping.h
dma-Mapping bietet eine API, um eine Übereinstimmung zwischen dem Inhalt dieses Datencaches und dem Speicher zu erzwingen. Dies sind dma_sync_single_for_cpu () und dma_sync_single_for_device ().
include/linux/dma-mapping.h
static inline void dma_sync_single_for_cpu(struct device *dev, dma_addr_t addr,
size_t size,
enum dma_data_direction dir)
{
const struct dma_map_ops *ops = get_dma_ops(dev);
BUG_ON(!valid_dma_direction(dir));
if (ops->sync_single_for_cpu)
ops->sync_single_for_cpu(dev, addr, size, dir);
debug_dma_sync_single_for_cpu(dev, addr, size, dir);
}
static inline void dma_sync_single_for_device(struct device *dev,
dma_addr_t addr, size_t size,
enum dma_data_direction dir)
{
const struct dma_map_ops *ops = get_dma_ops(dev);
BUG_ON(!valid_dma_direction(dir));
if (ops->sync_single_for_device)
ops->sync_single_for_device(dev, addr, size, dir);
debug_dma_sync_single_for_device(dev, addr, size, dir);
}
Wenn ich diese Funktion auf einem DMA-Puffer ausführe, der vom gerätekohärenten Pool zugewiesen wird, löst Linux Kenrel leider eine Panik mit einer Meldung aus, die der folgenden ähnelt: Es wird gesagt, dass die virtuelle Adresse seltsam ist.
dmesg
[ 141.582982] Unable to handle kernel paging request at virtual address ffffffc400000000
[ 141.590907] Mem abort info:
[ 141.593725] ESR = 0x96000145
[ 141.596767] Exception class = DABT (current EL), IL = 32 bits
[ 141.602686] SET = 0, FnV = 0
[ 141.605741] EA = 0, S1PTW = 0
[ 141.608872] Data abort info:
[ 141.611748] ISV = 0, ISS = 0x00000145
[ 141.615584] CM = 1, WnR = 1
[ 141.618552] swapper pgtable: 4k pages, 39-bit VAs, pgdp = 000000005fbae591
[ 141.627503] [ffffffc400000000] pgd=0000000000000000, pud=0000000000000000
[ 141.634294] Internal error: Oops: 96000145 [#1] SMP
[ 141.642892] Modules linked in: fclkcfg(O) u_dma_buf(O) mali(O) uio_pdrv_genirq
[ 141.650118] CPU: 0 PID: 3888 Comm: plbram_test Tainted: G O 4.19.0-xlnx-v2019.2-zynqmp-fpga #2
[ 141.660017] Hardware name: Avnet Ultra96-V2 Rev1 (DT)
[ 141.665053] pstate: 40000005 (nZcv daif -PAN -UAO)
[ 141.669839] pc : __dma_inv_area+0x40/0x58
[ 141.673838] lr : __swiotlb_sync_single_for_cpu+0x4c/0x70
[ 141.679138] sp : ffffff8010bdbc50
[ 141.682437] x29: ffffff8010bdbc50 x28: ffffffc06d1e2c40
[ 141.691811] x27: 0000000000000000 x26: 0000000000000000
[ 141.697114] x25: 0000000056000000 x24: 0000000000000015
[ 141.702418] x23: 0000000000000013 x22: ffffffc06abb5c80
[ 141.707721] x21: 0000000000040000 x20: 0000000400000000
[ 141.713025] x19: ffffffc06a932c10 x18: 0000000000000000
[ 141.718328] x17: 0000000000000000 x16: 0000000000000000
[ 141.723632] x15: 0000000000000000 x14: 0000000000000000
[ 141.728935] x13: 0000000000000000 x12: 0000000000000000
[ 141.734239] x11: ffffff8010bdbcd0 x10: ffffffc06dba2602
[ 141.739542] x9 : ffffff8008f48648 x8 : 0000000000000010
[ 141.744846] x7 : 00000000ffffffc9 x6 : 0000000000000010
[ 141.750149] x5 : 0000000400000000 x4 : 0000000400000000
[ 141.755452] x3 : 000000000000003f x2 : 0000000000000040
[ 141.760756] x1 : ffffffc400040000 x0 : ffffffc400000000
[ 141.766062] Process plbram_test (pid: 3888, stack limit = 0x0000000037d4fe7f)
[ 141.773187] Call trace:
[ 141.775620] __dma_inv_area+0x40/0x58
[ 141.779280] udmabuf_set_sync_for_cpu+0x10c/0x148 [u_dma_buf]
[ 141.785013] dev_attr_store+0x18/0x28
[ 141.788668] sysfs_kf_write+0x3c/0x50
[ 141.792319] kernfs_fop_write+0x118/0x1e0
[ 141.796313] __vfs_write+0x30/0x168
[ 141.799791] vfs_write+0xa4/0x1a8
[ 141.803090] ksys_write+0x60/0xd8
[ 141.806389] __arm64_sys_write+0x18/0x20
[ 141.810297] el0_svc_common+0x60/0xe8
[ 141.813949] el0_svc_handler+0x68/0x80
[ 141.817683] el0_svc+0x8/0xc
[ 141.820558] Code: 8a230000 54000060 d50b7e20 14000002 (d5087620)
[ 141.826642] ---[ end trace 3084524689d96f4d ]---
Aus historischen Gründen behandelt die dma-Mapping-API die physische Adresse auf dem DMA-Gerät mit dem Namen dma_addr_t als die als Argument übergebene Adresse.
Andererseits verfügt arm64 über einen Befehlssatz zum Behandeln des Datencaches, aber die von diesem Befehl behandelte Adresse ist eine virtuelle Adresse.
dma_sync_single_for_cpu () und dma_sync_single_for_device () rufen jeweils architekturabhängige untergeordnete Funktionen auf. Für arm64 werden schließlich \ _swiotlb_sync_single_for_cpu () und \ _swiotlb_sync_single_for_device () aufgerufen.
arch/arm64/mm/dma-mapping.c
static void __swiotlb_sync_single_for_cpu(struct device *dev,
dma_addr_t dev_addr, size_t size,
enum dma_data_direction dir)
{
if (!is_device_dma_coherent(dev))
__dma_unmap_area(phys_to_virt(dma_to_phys(dev, dev_addr)), size, dir);
swiotlb_sync_single_for_cpu(dev, dev_addr, size, dir);
}
static void __swiotlb_sync_single_for_device(struct device *dev,
dma_addr_t dev_addr, size_t size,
enum dma_data_direction dir)
{
swiotlb_sync_single_for_device(dev, dev_addr, size, dir);
if (!is_device_dma_coherent(dev))
__dma_map_area(phys_to_virt(dma_to_phys(dev, dev_addr)), size, dir);
}
Die \ _dma_unmap_area () und \ _dma_map_area (), die jede Funktion aufruft, sind Cache-Steuerprogramme, die in Assemblersprache in arch / arm64 / mm / cache.S geschrieben sind und die Anweisungen zur Daten-Cache-Steuerung von arm64 ausführen.
Wie bereits erläutert, ist die von der arm64-Datencache-Anweisung behandelte Adresse eine virtuelle Adresse. Daher wird phys_to_virt () in _swiotlb_sync_single_for_cpu () und \ _swiotlb_sync_single_for_device () aufgerufen, um von einer physischen Adresse in eine virtuelle Adresse zu übersetzen.
phys_to_virt () ist in arch / arm64 / include / asm / memory.h definiert.
arch/arm64/include/asm/memory.h
#ifdef CONFIG_DEBUG_VIRTUAL
extern phys_addr_t __virt_to_phys(unsigned long x);
extern phys_addr_t __phys_addr_symbol(unsigned long x);
#else
#define __virt_to_phys(x) __virt_to_phys_nodebug(x)
#define __phys_addr_symbol(x) __pa_symbol_nodebug(x)
#endif
#define __phys_to_virt(x) ((unsigned long)((x) - PHYS_OFFSET) | PAGE_OFFSET)
#define __phys_to_kimg(x) ((unsigned long)((x) + kimage_voffset))
/*
* Convert a page to/from a physical address
*/
#define page_to_phys(page) (__pfn_to_phys(page_to_pfn(page)))
#define phys_to_page(phys) (pfn_to_page(__phys_to_pfn(phys)))
/*
* Note: Drivers should NOT use these. They are the wrong
* translation for translating DMA addresses. Use the driver
* DMA support - see dma-mapping.h.
*/
#define virt_to_phys virt_to_phys
static inline phys_addr_t virt_to_phys(const volatile void *x)
{
return __virt_to_phys((unsigned long)(x));
}
#define phys_to_virt phys_to_virt
static inline void *phys_to_virt(phys_addr_t x)
{
return (void *)(__phys_to_virt(x));
}
Wie Sie sehen können, subtrahieren wir zum Übersetzen einer physischen Adresse in eine virtuelle Adresse einfach PHYS_OFFSET und summieren PAGE_OFFSET logisch.
Tatsächlich funktioniert diese Konvertierung gut für den Speicherplatz, den der Linux-Kernel zuerst in den Speicher geladen und bei der Initialisierung zugewiesen hat. In anderen Speicherbereichen (z. B. wenn der Speicher auf der PL-Seite wie in diesem Beispiel als DMA-Puffer verwendet wird) funktioniert diese Konvertierung jedoch nicht. Daher scheint die CPU eine Ausnahme ausgelöst zu haben, indem sie die falsche virtuelle Adresse in der Datencache-Operationsanweisung angegeben hat.
Es wurde festgestellt, dass die dma-Mapping-API den Datencache nicht steuern kann, wenn der Speicher auf der PL-Seite als DMA-Puffer zugewiesen wird. Ich suchte nach verschiedenen Methoden, konnte aber keine gute finden. Da es keine Hilfe dafür gibt, implementiert udmabuf v2.2.0-rc2 die Datencache-Steuerung direkt mithilfe des arm64-Datencache-Befehls.
u-dma-buf.c
#if ((USE_IORESOURCE_MEM == 1) && defined(CONFIG_ARM64))
/**
* DOC: Data Cache Clean/Invalid for arm64 architecture.
*
* This section defines mem_sync_sinfle_for_cpu() and mem_sync_single_for_device().
*
* * arm64_read_dcache_line_size() - read data cache line size of arm64.
* * arm64_inval_dcache_area() - invalid data cache.
* * arm64_clean_dcache_area() - clean(flush and invalidiate) data cache.
* * mem_sync_single_for_cpu() - sync_single_for_cpu() for mem_resource.
* * mem_sync_single_for_device() - sync_single_for_device() for mem_resource.
*/
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 mem_sync_single_for_cpu(struct device* dev, void* start, size_t size, enum dma_data_direction direction)
{
if (is_device_dma_coherent(dev))
return;
if (direction != DMA_TO_DEVICE)
arm64_inval_dcache_area(start, size);
}
static void mem_sync_single_for_device(struct device* dev, void* start, size_t size, enum dma_data_direction direction)
{
if (is_device_dma_coherent(dev))
return;
if (direction == DMA_FROM_DEVICE)
arm64_inval_dcache_area(start, size);
else
arm64_clean_dcache_area(start, size);
}
#endif
Wenn der Speicher auf der PL-Seite als DMA-Puffer zugewiesen ist, rufen sync_for_cpu und sync_for_device mem_sync_single_for_cpu () bzw. mem_sync_single_for_device () auf.
Ich dachte, dass es ziemlich üblich ist, einen Speicher (in diesem Beispiel BRAM) oder DRAM-Controller auf der PL-Seite zu implementieren und diesen Speicher unter Linux zu verwenden, aber ich habe versucht, ihn unter Berücksichtigung des Datencaches ernsthaft zu implementieren. Dann fühlte ich, dass es unerwartet schwierig war.
Besonders Kernel Panic brachte mich zum Weinen. Ich hätte nicht gedacht, dass es eine solche Falle gibt, wenn man von einer physischen Adresse in eine virtuelle Adresse übersetzt. Die dma-Mapping-API hat eine lange Geschichte und passt möglicherweise nicht mehr in die aktuelle Architektur.
Persönlich wünschte ich, ich hätte eine API für Daten-Cache-Operationen unter Verwendung virtueller Adressen veröffentlicht. Es sollte auch andere Verwendungszwecke geben. Am Ende von arch / arm64 / mm / flush.c befindet sich beispielsweise die folgende Beschreibung.
arch/arm64/mm/flush.c
#ifdef CONFIG_ARCH_HAS_PMEM_API
void arch_wb_cache_pmem(void *addr, size_t size)
{
/* Ensure order against any prior non-cacheable writes */
dmb(osh);
__clean_dcache_area_pop(addr, size);
}
EXPORT_SYMBOL_GPL(arch_wb_cache_pmem);
void arch_invalidate_pmem(void *addr, size_t size)
{
__inval_dcache_area(addr, size);
}
EXPORT_SYMBOL_GPL(arch_invalidate_pmem);
#endif
Mit der definierten CONFIG_ARCH_HAS_PMEM_API wird die Funktion für die von mir gewünschte Datencache-Operation EXPORTIERT. Diese API scheint für nichtflüchtigen Speicher (Persistent MEMory) bereitgestellt zu werden.
["Gerätetreiber für Programme, die unter Linux im Benutzerbereich ausgeführt werden, und Hardware, die Speicher gemeinsam nutzen" @Qiita]: https://qiita.com/ikwzm/items/cc1bb33ff43a491440ea "" Programme, die unter Linux im Benutzerbereich ausgeführt werden Gerätetreiber für Hardware zur gemeinsamen Nutzung des Speichers mit @Qiita " ["Gerätetreiber für Programme und Hardware, die im Benutzerbereich ausgeführt werden, um Speicher unter Linux gemeinsam zu nutzen (reservierter Speicher)" @Qiita]: https://qiita.com/ikwzm/items/9b5fac2c1332147e76a8 "" Unter Linux Gerätetreiber für Programme und Hardware, die im Benutzerbereich ausgeführt werden, um Speicher gemeinsam zu nutzen (Edition mit reserviertem Speicher) "@Qiita" [udmabuf]: https://github.com/ikwzm/udmabuf "udmabuf" [udmabuf v2.2.0-rc2]: https://github.com/ikwzm/udmabuf/tree/v2.2.0-rc2 "udmabuf v2.2.0-rc2" [ZynqMP-FPGA-Linux v2019.2.1]: https://github.com/ikwzm/ZynqMP-FPGA-Linux/tree/v2019.2.1 "ZynqMP-FPGA-Linux v2019.2.1"
Recommended Posts