Cet article implémente les opérations de cache de données du processeur dans le langage d'assemblage arm64. Les opérations de cache de données devraient normalement utiliser l'API Linux Kernel, mais malheureusement aucune n'était disponible (nous en parlerons plus tard). Veuillez noter que cet article n'est qu'un article d'essai que j'ai un peu essayé.
Lors de l'échange de données entre la section PS (Processing System) et la section PL (Programmable Logic) avec ZynqMP (ARM64), il existe une méthode pour préparer une mémoire telle que BRAM côté PL et y accéder à partir du CPU de la section PS.
Cet article décrit comment accéder à la mémoire côté PL à partir de Linux pour remplir les conditions suivantes:
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.
L'auteur publie [udmabuf] en open source.
J'ai essayé d'ajouter une fonction à udmabuf à titre d'essai afin que la mémoire du côté PL soit accessible à partir de Linux (cela semble être de la merde, mais ce n'est qu'un essai). Ensuite, j'ai implémenté un exemple de conception en utilisant BRAM dans la mémoire du côté PL et j'ai confirmé l'effet du cache de données.
Cet article décrit les éléments suivants:
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.
L'environnement utilisé pour la mesure est le suivant.
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]
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 Schéma fonctionnel de PLBRAM-Ultra96
Ces environnements sont publiés sur github.
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 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.
Il a fallu 0,317 ms pour activer le cache de données et utiliser memcpy () pour écrire 256 Ko de données dans la 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 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 que 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).
Il a fallu 3,485 ms pour désactiver 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 75 Mo / s.
La forme d'onde AXI I / F à ce moment-là était la suivante.
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.
Il a fallu 0,409 ms pour activer le cache de données et utiliser memcpy () pour lire 256 Ko de données 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 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.
Linux a un framework appelé Dynamic DMA mapping (dma-mapping). Le noyau Linux utilise généralement des adresses virtuelles. D'un autre côté, si votre appareil prend en charge DMA, vous avez généralement besoin d'une adresse physique. dma-mapping est un cadre de pontage entre les périphériques prenant en charge DMA et le noyau Linux. Il alloue et gère les tampons DMA, effectue la traduction entre les adresses physiques et virtuelles et gère les caches de données si nécessaire. .. Pour plus de détails, reportez-vous au DMA-API-HOWTO du noyau Linux.
Dans le dma-mapping, généralement lorsque dma_alloc_coherent () alloue un tampon DMA, il alloue un tampon DMA en mémoire dans le noyau Linux. Puisque la mémoire du côté PL n'est pas sur la mémoire du noyau Linux, il est nécessaire de concevoir un moyen de sécuriser un tampon DMA dans la mémoire du côté PL.
Une façon d'allouer un tampon DMA dans la mémoire du côté PL consiste à utiliser la mémoire réservée dans l'arborescence des périphériques. Voir ci-dessous pour savoir comment utiliser la mémoire réservée.
La méthode utilisant la mémoire réservée ne prend pas en charge la superposition de l'arborescence des périphériques. Ceci est dû au fait que la mémoire réservée ne peut pas être reconfigurée une fois qu'elle est configurée au démarrage du noyau Linux. reserve-memory n'est pas couvert par la superposition de l'arborescence des périphériques.
Normalement, lorsque dma_alloc_coherent () alloue un tampon DMA, il alloue un tampon DMA en mémoire dans le noyau Linux. Cependant, un mécanisme appelé pool cohérent de périphériques peut être utilisé pour allouer un tampon DMA à partir de la mémoire en dehors du noyau Linux. Le code source du pool cohérent de périphériques se trouve dans kernel / dma / cohérent.c.
dma_declare_coherent_memory()
Pour utiliser le pool cohérent de périphériques, utilisez d'abord la fonction dma_declare_coherent_memory (). dma_declare_coherent_memory () ressemble à ceci:
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);
Spécifiez l'adresse physique de la mémoire que vous souhaitez allouer à phys_addr, l'adresse sur le périphérique pour device_addr et la taille de la mémoire que vous souhaitez allouer pour la taille.
Initialisez le tampon avec dma_init_cohrent_memory () et affectez-le à dev dans la structure du périphérique avec dma_assign_coherent_memory ().
Maintenant, si vous utilisez dma_alloc_coherent () pour allouer un tampon DMA pour dev, le tampon DMA sera alloué à partir de la mémoire spécifiée par dma_declare_coherent_memory ().
dma_alloc_coherent()
Plus précisément, le mécanisme par lequel dma_alloc_coherent () alloue le tampon DMA à partir de la zone mémoire allouée par dma_declare_coherent_memory () est expliqué. dma_alloc_coherent () est défini dans include / linux / dma-mapping.h comme suit:
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;
}
(Omission)
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);
}
Le dma_alloc_attrs () appelé par dma_alloc_coherent () appelle dma_alloc_from_dev_coherent () plus tôt. dma_alloc_from_dev_coherent () est défini dans kernel / dma / cohérent.c comme suit:
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;
}
(Omission)
/**
* 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 () appelle d'abord dev_get_coherent_memory () pour vérifier dma_mem dans la structure du périphérique. Lorsque dma_mem est NULL, il retourne sans rien faire, mais si le pool cohérent de périphérique est affecté à dma_mem par dma_declare_coherent_memory (), \ _dma_alloc_from_coherent () alloue un tampon DMA à partir du pool cohérent de périphérique.
dma_release_declared_memory()
Le pool cohérent de périphériques affecté à la structure de périphériques avec dma_declare_coherent_memory () est libéré avec dma_release_declared_memory ().
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()
Utilisez dma_mmap_from_dev_coherent () pour mapper le tampon DMA alloué à l'espace utilisateur. Normalement, dma_mmap_cohrent () est utilisé pour mapper le tampon DMA à l'espace utilisateur, mais dma_mmap_from_dev_cohrent () peut être utilisé pour mapper avec le cache de données activé.
dma_mmap_from_dev_coherent () est défini comme suit: Notez que dma_mmap_from_dev_coherent () n'a apporté aucune modification à vma-> vm_page_prot.
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);
D'autre part, vous utilisez généralement dma_mmap_cohrent () pour mapper le tampon DMA à l'espace utilisateur. dma_mmap_coherent () est défini dans include / linux / dma-mapping.h comme suit:
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 () appelle mmap () sur les dma_map_ops dépendant de l'architecture. Le mmap () pour arm64 ressemble à ceci:
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,
};
(Omis)
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);
}
Notez que \ _swiotlb_mmap () appelle également dma_mmap_from_dev_cohrent () une fois, mais avant cela, il utilise \ _get_dma_pgprot () pour écraser vma-> vm_page_prot.
Et \ _get_dma_pgprot () ressemble à ceci:
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;
}
La mise en cache des données a été désactivée par pgprot_writecombine ().
Autrement dit, lors du mappage d'un tampon DMA à l'espace utilisateur, appelez directement dma_mmap_from_dev_cohrent () au lieu d'appeler dma_mmap_coherent (), ce qui force la désactivation 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.
include/linux/dma-mapping.h
dma-mapping fournit une API pour forcer une correspondance entre le contenu de ce cache de données et de la mémoire. Ce sont dma_sync_single_for_cpu () et 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);
}
Malheureusement, lorsque j'exécute cette fonction sur un tampon DMA alloué par le pool cohérent de périphériques, Linux Kenrel déclenche une panique avec un message similaire au suivant: On dit que l'adresse virtuelle est étrange.
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 ]---
Pour des raisons historiques, l'API dma-mapping gère l'adresse physique sur le périphérique DMA appelé dma_addr_t comme adresse passée en argument.
D'autre part, arm64 a un jeu d'instructions pour gérer le cache de données, mais l'adresse gérée par cette instruction est une adresse virtuelle.
dma_sync_single_for_cpu () et dma_sync_single_for_device () appellent chacun des fonctions subordonnées dépendant de l'architecture. Pour arm64, \ _swiotlb_sync_single_for_cpu () et \ _swiotlb_sync_single_for_device () sont finalement appelés.
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);
}
Les \ _dma_unmap_area () et \ _dma_map_area () que chaque fonction appelle sont des programmes de contrôle de cache écrits en langage d'assemblage dans arch / arm64 / mm / cache.S et exécutent les instructions de contrôle de cache de données d'arm64.
Comme expliqué précédemment, l'adresse gérée par l'instruction de cache de données arm64 est une adresse virtuelle, donc phys_to_virt () est appelée dans _swiotlb_sync_single_for_cpu () et \ _swiotlb_sync_single_for_device () pour traduire d'une adresse physique en une adresse virtuelle.
phys_to_virt () est défini dans arch / arm64 / include / asm / memory.h.
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));
}
Comme vous pouvez le voir, pour traduire une adresse physique en une adresse virtuelle, nous soustrayons simplement PHYS_OFFSET et additionnons logiquement PAGE_OFFSET.
En fait, cette conversion fonctionne bien pour l'espace mémoire que le noyau Linux a d'abord chargé en mémoire et alloué lors de l'initialisation. Cependant, dans d'autres espaces mémoire (par exemple, lorsque la mémoire côté PL est utilisée comme tampon DMA comme dans cet exemple), cette conversion ne fonctionne pas. Par conséquent, il semble que le processeur ait soulevé une exception en spécifiant la mauvaise adresse virtuelle dans l'instruction d'opération du cache de données.
Il a été constaté que l'API dma-mapping ne peut pas contrôler le cache de données lorsque la mémoire du côté PL est allouée en tant que tampon DMA. J'ai recherché diverses méthodes, mais je n'ai pas trouvé de bonne méthode. Puisqu'il n'y a aucune aide pour cela, udmabuf v2.2.0-rc2 implémente le contrôle du cache de données directement en utilisant l'instruction de cache de données arm64.
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
Si la mémoire du côté PL est allouée en tant que tampon DMA, sync_for_cpu et sync_for_device appellent respectivement mem_sync_single_for_cpu () et mem_sync_single_for_device ().
Je pensais qu'il était assez courant d'implémenter une mémoire (BRAM dans cet exemple) ou un contrôleur DRAM du côté PL et d'utiliser cette mémoire de Linux, mais j'ai essayé de l'implémenter en considérant sérieusement le cache de données. Puis j'ai senti que c'était étonnamment difficile.
Surtout Kernel Panic m'a fait pleurer. Je ne pensais pas qu'il y avait un tel piège à traduire d'une adresse physique à une adresse virtuelle. L'API dma-mapping a une longue histoire et peut ne plus s'intégrer dans l'architecture actuelle.
Personnellement, j'aurais aimé publier une API pour les opérations de cache de données utilisant des adresses virtuelles. Il devrait également y avoir d'autres utilisations. Par exemple, à la fin de l'arc / arm64 / mm / flush.c, il y a la description suivante.
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
Avec le CONFIG_ARCH_HAS_PMEM_API défini, la fonction que je voulais pour l'opération de cache de données est EXPORTée. Cette API semble être fournie pour la mémoire non volatile (Persistent MEMory).
["Pilote de périphérique pour les programmes s'exécutant dans l'espace utilisateur sur Linux et la mémoire de partage matériel" @Qiita]: https://qiita.com/ikwzm/items/cc1bb33ff43a491440ea "" Programmes s'exécutant dans l'espace utilisateur sous Linux Pilote de périphérique pour le matériel pour partager la mémoire avec @Qiita " ["Pilote de périphérique pour les programmes et le matériel s'exécutant dans l'espace utilisateur pour partager la mémoire sous Linux (mémoire réservée)" @Qiita]: https://qiita.com/ikwzm/items/9b5fac2c1332147e76a8 "" Sous Linux Pilote de périphérique pour les programmes et le matériel s'exécutant dans l'espace utilisateur pour partager la mémoire (édition à mémoire réservée) "@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