はじめに
お断り
この記事では CPU のデータキャッシュの操作を arm64 のアセンブリ言語で実装しています。データキャッシュ操作は本来なら Linux Kernel のAPI を使うべきところですが、残念ながら使えるものがありませんでした(この点は後述)。この記事はあくまでもちょっとやってみました的なトライアルの記事であることをご了承ください。
2020年8月7日追記
Linux Kernel 5.2 以降はこの方法が使えなくなったことに伴い、新たにデバイスドライバを作りました。詳しくは[『Linux から FPGA のメモリに"キャッシュを有効にして"アクセスするデバイスドライバ』@Qiita]を参照してください。
2020年6月28日追記
ここの記事で紹介した方法は、Linux Kernel 5.2 以降は使えなくなりました。dma-mapping API から dma_declare_coherent_memory() が削除されたからです。削除された理由は「使われてないから」だそうです。詳細はこちら。https://lore.kernel.org/patchwork/patch/1123030/
やりたかったこと
ZynqMP(ARM64) でPS(Processing System)部と PL(Programmable Logic) 部とデータをやりとりする際に、PL 側にBRAM 等のメモリを用意して、PS部のCPUからアクセスする方法があります。
この記事では、次の条件を満たすように Linux から PL 側のメモリをアクセスする方法について説明します。
- CPU のデータキャッシュを有効に出来る。
- CPU のデータキャッシュの操作(Flush or Invalidiate) が手動でできる。
- Device Tree Overlay 等で Linux 起動後に自由に着脱できる。
普通にアクセスするだけならば uio を使えば可能です。しかし uio では条件1のCPU のデータキャッシュを有効に出来ないため、大量のデータを転送する場合は性能的に不利です。
また、参考「Accessing BRAM In Linux」で示す /dev/mem と reserved_memory を使う方法では、データキャッシュを有効にすることは出来ても、手動でキャッシュ操作ができないため、PL側とのデータのやりとりには向いていません。また、reserved_memory はLinux がブートする際にのみ指定できるので、Linux 起動後に自由に着脱できません。
やったこと
筆者は udmabuf をオープンソースで公開しています。
- 『Linuxでユーザー空間で動作するプログラムとハードウェアがメモリを共有するためのデバイスドライバ』@Qiita
- [https://github.com/ikwzm/udmabuf]
udmabuf を試験的に機能を追加して Linux から PL 側のメモリをアクセスできるようにしてみました(クドいようですがあくまでもトライアルです)。そして PL 側のメモリにBRAM を使ったサンプルデザインを実装して、データキャッシュの効果を確認しました。
この記事では以下のことについて説明します。
- サンプルデザインによるデータキャッシュの効果確認
- PL 側のメモリを dma-mapping API で管理する方法
- データキャッシュ制御
データキャッシュの効果
この章では、PS 側から PL 側のメモリをアクセスする際にデータキャッシュがどのていどの効果があるのかを実際に計測して示します。
測定環境
計測に使用した環境は次の通りです。
-
Ultra96-V2
-
linux-xlnx v2019.2 (Linux Kernel 4.19)
-
Debian10
-
Xilinx Vivado 2019.2
PL 側には次のようなデザインを実装します。PL側に256KByte分のメモリを BRAM で実装し、そのインターフェースには Xilinx 社の AXI BRAM Controller を使っています。動作周波数は100MHz です。AXI BRAM Controller の AXI I/F と BRAM I/F の波形を観測するためにILA (Integrated Logic Analyzer) を接続しています。
Fig.1 PLBRAM-Ultra96 のブロック図
これらの環境は github で公開しています。
データキャッシュオフ時のメモリライト
データキャッシュをオフにして memcpy() を使って PL 側の BRAM に 256KByte のデータを書き込むのに要した時間は 0.496 msec でした。約528MByte/sec の書き込み速度です。
そのときの AXI I/F の波形はつぎのようになりました。
Fig.2 データキャッシュオフ時のメモリライトの AXI IF 波形
波形をみてわかるとおり、バースト転送が行われていません(AWLEN=00)。1ワード(16bytes) ずつ転送していることがわかります。
データキャッシュオン時のメモリライト
データキャッシュをオンにして memcpy() を使って PL 側の BRAM に 256KByte のデータを書き込むのに要した時間は 0.317 msec でした。約827MByte/sec の書き込み速度です。
そのときの AXI I/F の波形はつぎのようになりました。
Fig.3 データキャッシュオン時のメモリライトの AXI IF 波形
波形をみてわかるとおり、一回の書き込みで4ワード(64byte)のバースト転送が行われています(AWLEN=03)。
BRAM へのライトが発生するのは、CPU がライトした時ではありません。CPU がライトしたとき、まずデータキャッシュにデータが書き込まれて BRAM へはまだ書き込まれません。そして、マニュアルでデータキャッシュフラッシュ命令が実行されたときか、データキャッシュが一杯になって使われていないキャッシュを空けるときに初めて BRAM へライトが発生します。その際にデータキャッシュのキャッシュラインサイズ(arm64 では64byte) ごとにまとめてライトが行われます。
データキャッシュオフ時のメモリリード
データキャッシュをオフにして memcpy() を使って PL 側の BRAM から 256KByte のデータを読み込むのに要した時間は 3.485 msec でした。約75MByte/sec の読み込み速度です。
そのときの AXI I/F の波形はつぎのようになりました。
Fig.4 データキャッシュオフ時のメモリリードの AXI IF 波形
波形をみてわかるとおり、バースト転送が行われていません(ARLEN=00)。1ワード(16bytes) ずつ転送していることがわかります。
データキャッシュオン時のメモリリード
データキャッシュをオンにして memcpy() を使って PL 側の BRAM から 256KByte のデータを読み込むのに要した時間は 0.409 msec でした。約641MByte/sec の読み込み速度です。
そのときの AXI I/F の波形はつぎのようになりました。
Fig.5 データキャッシュオン時のメモリリードの AXI IF 波形
波形をみてわかるとおり、一回の読み込みで4ワード(64byte)のバースト転送が行われています(ARLEN=03)。
CPU がメモリリードを行った際、データキャッシュにデータが無ければ、BRAM からデータを読んでキャッシュに充填します。その際にデータキャッシュのキャッシュラインサイズ(arm64 では 64byte)分をまとめて BRAM から読み出します。それ以降はデータキャッシュにデータがある限りデータキャッシュからデータがCPUに提供されるので BRAM へのアクセスは発生しません。そのため、データキャッシュがオフの時よりも高速にメモリリードが行われます。この環境ではデータキャッシュオフ時が75MByte/sec に対してデータキャッシュをオンにすると 641MByte/sec と大幅に性能が向上しました。
PL 側のメモリを dma-mapping API で管理する方法
dma-mapping とは
Linux には Dynamic DMA mapping(dma-mapping) というフレームワークがあります。Linux Kernel は通常、仮想アドレスを使用します。一方、デバイスが DMA をサポートしている場合、通常は物理アドレスが必要です。dma-mapping はDMA をサポートしているデバイスと Linux Kernelの橋渡しを行うためのフレームワークで、DMA バッファの確保と管理、物理アドレスと仮想アドレスの相互変換、必要によってはデータキャッシュの管理を行います。詳しくは Linux Kernel の DMA-API-HOWTO 等を参照してください。
dma-mapping では、通常、dma_alloc_coherent() で DMA バッファを確保した場合、Linux Kernel 内のメモリ上にDMA バッファを確保します。PL 側のメモリは Linux Kernel 内のメモリ上には無いので、PL 側のメモリに DMA バッファを確保するのには一工夫必要です。
reserved-memory を使って PL 側のメモリを管理する
PL 側のメモリに DMA バッファを確保する方法の一つとして、Device Tree のreserved-memory を使う方法があります。reserved-memory を使う方法については以下を参照してください。
reserved-memory を使う方法は、Device Tree Overlay に対応できません。何故なら、reserved-memory は Linux Kernel のブート時に一度設定されたら最後、再設定できません。reserved-memory は Device Tree Overlay の対象外なのです。
device coherent pool を使って PL 側のメモリを管理する
通常、dma_alloc_coherent() で DMA バッファを確保した場合、Linux Kernel 内のメモリ上にDMA バッファを確保します。しかしdevice coherent pool という機構を使うと Linux Kernel 外のメモリから DMA バッファを確保することが出来ます。device coherent pool のソースコードは kernel/dma/coherent.c にあります。
dma_declare_coherent_memory()
device coherent pool を使うには、まず dma_declare_coherent_memory() という関数を使います。dma_declare_coherent_memory() は次のようになっています。
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);
phys_addr に割り当てたいメモリの物理アドレス、device_addr にはデバイス上のアドレス、size には割り当てたいメモリのサイズを指定します。
dma_init_cohrent_memory() でバッファの初期化を行い、dma_assign_coherent_memory() で device 構造体の dev にアサインします。
これで、dev に対して dma_alloc_coherent() を使って DMA バッファを確保した場合、dma_declare_coherent_memory() で指定されたメモリから DMA バッファが確保されます。
dma_alloc_coherent()
具体的に dma_alloc_coherent() が dma_declare_coherent_memory() で確保したメモリ領域から DMA バッファを確保するメカニズムについて説明します。dma_alloc_coherent() は 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;
}
(中略)
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);
}
dma_alloc_coherent() が呼び出している dma_alloc_attrs() は最初の方で dma_alloc_from_dev_coherent() を呼び出しています。dma_alloc_from_dev_coherent() は 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;
}
(中略)
/**
* 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() はまず、dev_get_coherent_memory() を呼び出して、device 構造体の dma_mem をチェックします。dma_mem が NULL の時は何もせずに戻りますが、dma_declare_coherent_memory() によってdevice coherent pool が dma_mem にアサインされていた場合は _dma_alloc_from_coherent() によって device coherent pool から DMA バッファが確保されます。
dma_release_declared_memory()
dma_declare_coherent_memory() で device 構造体にアサインした device coherent pool は dma_release_declared_memory() で解放します。
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()
確保した DMA バッファをユーザー空間にマッピングする時は dma_mmap_from_dev_coherent() を使います。通常、DMA バッファをユーザー空間にマッピングする時は dma_mmap_cohrent() を使いますが、dma_mmap_from_dev_cohrent() を使うことによって、データキャッシュを有効にしたままマッピングすることが出来ます。
dma_mmap_from_dev_coherent() は次のように定義されています。dma_mmap_from_dev_coherent() は vma->vm_page_prot に変更を加えていないことに注目してください。
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);
一方、通常、DMA バッファをユーザー空間にマッピングする時は dma_mmap_cohrent() を使います。dma_mmap_coherent() は 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() はアーキテクチャに依存した dma_map_ops の mmap() を呼び出します。arm64 の mmap() は次のようになっています。
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,
};
(中略)
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);
}
_swiotlb_mmap() も一度 dma_mmap_from_dev_cohrent() を呼び出していますが、その前に _get_dma_pgprot() を使って vma->vm_page_prot を上書きしていることに注意してください。
そして _get_dma_pgprot() は次のようになっています。
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;
}
pgprot_writecombine() によってデータキャッシュはオフにされています。
つまり、DMA バッファをユーザー空間にマッピングする時は、データキャッシュを強制的に無効にする dma_mmap_coherent() を呼び出すのではなく、直接 dma_mmap_from_dev_cohrent() を呼び出します。
データキャッシュ制御
PL 側のメモリを CPU からのみアクセス出来るメモリとして使うだけならば、データキャッシュを有効にするだけで済みます。しかし、PL 側のメモリを CPU 以外のデバイスがアクセスする場合や、PL 側のメモリを Linux 起動後に有効にしたり無効にしたりするには、データキャッシュを有効にするだけでは不十分です。データキャッシュと PL 側のメモリとのデータの不一致が起こりうるので、なんらかの方法でデータキャッシュと PL 側のメモリの内容を一致させる必要があります。
dma-mapping API によるデータキャッシュ制御
include/linux/dma-mapping.h
dma-mapping には、このデータキャッシュとメモリとの内容を強制的に一致させるための API が用意されています。それが dma_sync_single_for_cpu() および dma_sync_single_for_device() です。
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);
}
Kernel Panic が発生
残念ながら device coherent pool で確保した DMA バッファに対してこの関数を実行すると、Linux Kenrel が次のようなメッセージを出して Panic を起こします。なにやら仮想アドレスがおかしいと言われています。
[ 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 ]---
Kernel Panic の原因
dma-mapping API は、歴史的な経緯から、引数で渡すアドレスは dma_addr_t というDMA デバイス上の物理アドレスを扱います。
一方で、arm64 はデータキャッシュを扱うための命令セットを持っていますが、その命令で扱うアドレスは、仮想アドレスです。
dma_sync_single_for_cpu() および dma_sync_single_for_device() はそれぞれアーキテクチャ依存の下位関数を呼び出しています。arm64 の場合、最終的に _swiotlb_sync_single_for_cpu() および _swiotlb_sync_single_for_device() が呼び出されます。
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);
}
それぞれの関数が呼び出している _dma_unmap_area() および _dma_map_area() は arch/arm64/mm/cache.S にアセンブリ言語で記述されたキャッシュ制御プログラムで、arm64 のデータキャッシュ制御命令を実行しています。
先ほど説明したとおり、arm64 のデータキャッシュ命令で扱うアドレスは仮想アドレスなため、_swiotlb_sync_single_for_cpu() および _swiotlb_sync_single_for_device() 内で物理アドレスから仮想アドレスに変換するために phys_to_virt() が呼び出されています。
phys_to_virt() は 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));
}
見ての通り、物理アドレスを仮想アドレスに変換するのに、単純に PHYS_OFFSET を引いて PAGE_OFFSET を論理和しているだけです。
実はこの変換は Linux Kernel が最初にメモリにロードされて初期化時に確保したメモリ空間に関してはうまく働きます。しかし、それ以外のメモリ空間(例えば今回の例のように PL 側のメモリをDMA バッファとして使うような場合)、この変換では上手くいきません。そのためデーターキャッシュ操作命令に間違った仮想アドレスを指定してしまって CPU が例外を発生したようです。
udmabuf の対処方法
dma-mapping API では PL 側のメモリを DMA バッファとして確保した場合にデータキャッシュの制御ができないことがわかりました。いろいろ方法を模索しましたが、上手い方法が見つかりませんでした。仕方が無いので、udmabuf v2.2.0-rc2 ではデータキャッシュの制御を直接 arm64 のデータキャッシュ命令を使って実装しています。
#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
PL 側のメモリを DMA バッファとして確保した場合、sync_for_cpu および sync_for_device はそれぞれ mem_sync_single_for_cpu() および mem_sync_single_for_device() を呼び出します。
所感
PL 側にメモリ(この例ではBRAM) あるいは DRAM コントローラーを実装して、そのメモリを Linux から使うという用途はわりと一般的だと思ったのですが、本格的にデータキャッシュまで考えて実装しようとすると意外に難しかったという感じです。
特に Kernel Panic には泣かされました。まさか物理アドレスから仮想アドレスに変換するのにあんな罠があるとは思っていませんでした。 dma-mapping API の歴史は古く、今のアーキテクチャには合わなくなってきているのかもしれません。
個人的には、仮想アドレスによるデータキャッシュ操作用の API が公開されていれば良かったのにと残念です。他にも用途があるはずですので。例えば、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
CONFIG_ARCH_HAS_PMEM_API が定義されていると、私が望んでいたデータキャッシュ操作用の関数が公開(EXPORT)されます。このAPI は不揮発性メモリ(Persistent MEMory)用に用意されているようです。
参考
- [『Linux から FPGA のメモリに"キャッシュを有効にして"アクセスするデバイスドライバ』@Qiita]
- 『Linuxでユーザー空間で動作するプログラムとハードウェアがメモリを共有するためのデバイスドライバ』@Qiita
- 『Linuxでユーザー空間で動作するプログラムとハードウェアがメモリを共有するためのデバイスドライバ(reserved-memory編)』 @Qiita
-
「Accessing BRAM In Linux」 @Xilinx Wiki
https://xilinx-wiki.atlassian.net/wiki/spaces/A/pages/18842412/Accessing+BRAM+In+Linux - DMA-API-HOWTO
- ZynqMP-FPGA-Linux v2019.2.1
- udmabuf v2.2.0-rc2
- https://github.com/ikwzm/PLBRAM-Ultra96
[『Linux から FPGA のメモリに"キャッシュを有効にして"アクセスするデバイスドライバ』@Qiita]: https://qiita.com/ikwzm/items/26fcbb078023a36e4ae2 "『Linux から FPGA のメモリに"キャッシュを有効にして"アクセスするデバイスドライバ』@Qiita"