一个简易PCIe DMA engine设备的定义
内存拷贝比较消耗CPU资源,定义一个专用的DMA设备帮助CPU做内存拷贝,CPU把数据的地址
和需要拷贝到的目的地址,配置到这个设备的相关寄存器里,然后启动数据拷贝,数据拷贝
完成后,设备的相关寄存器的值改变,从而向软件报告任务完成。也可以通过设备中断的
方式向软件报告任务完成。
本文选DMA engine设备,完全和具体业务没有关系,只是因为这个业务模型比较简单,容易
说明问题。
具体数据搬移的实现也很简单:先把数据搬移到这个设备内部的buffer里,然后再把buffer
里的数据搬移到目的地址。
这个搬移的过程可以过IOMMU设备,也可以不过IOMMU,我们可以控制qemu系统,使得被模拟
的平台有IOMMU设备或者没有IOMMU设备。
如下是这个设备MMIO寄存器的具体定义:
这个设备有一个32bit non-prefetch BAR, BAR base + 0x1000的位置是我们定义的寄存器。
offset | 定义 |
---|---|
0x0(只写) | 原数据的地址。 |
0x8(只写) | 搬移的目的地址。 |
0x10(只写) | 0-31 bit保留,32-63 bit表示搬移数据的长度。 |
0x18(读写) | bit0 置1表示开始拷贝数据。bit32 置1表示数据拷贝完成。 |
qemu设备的实现
加入qemu编译系统
本文用来arm64平台的qemu虚拟机测试。首先我们要保证整个平台的编译运行。测试使用
的qemu的版本是6.1.0我们用如下的命令编译和启动基础的qemu虚拟机:
1
2
3
4
5
6
7
8
9
10configure --target-list=aarch64-softmmu --enable-kvm
make -j64
qemu-system-aarch64 -cpu cortex-a57 \
-smp 1 -m 1024M \
-nographic \
-machine virt,iommu=smmuv3 \
-kernel ~/Image \
-append "console=ttyAMA0 earlycon root=/dev/ram rdinit=/init" \
-initrd ~/rootfs.cpio.gz \其中,如果想去掉smmuv3这个设备可以把machine那一行改成-machine virt \
qemu使用meson来编译,我们只要在对应的meson.build文件中加入我们要编译的文件就好。
把这个DMA engine设备的代码放到qemu/hw/misc/dma_engine.c里,所以在qemu/hw/misc/meson.build
里加入如下的代码:1
softmmu_ss.add(when: 'CONFIG_DMA_ENGINE', if_true: files('dma_engine.c'))
表示如果配置CONFIG_DMA_ENGINE打开,就把dma_engine.c编译进来。
在qemu/hw/misc/Kconfig里加入CONFIG_DMA_ENGINE,并配置把他直接编译到qemu里:
1
2
3config DMA_ENGINE
bool
default y完成如上的配置后,我们在hw/misc目录加的dma_engine.c就可以参加到qemu编译里来。
定义一个PCIe设备
如下[1]中是DMA engine PCIe设备的代码,我们可以套用这个模板创建其他的PCIe设备。
改变PCIDeviceClass里的vendor_id/device_id/revision/class_id的数值,虚拟设备
PCIe配置空间中的对应域段的值可以被改变。class中的realize函数和instance_init函数是主要要实现函数。可以使用pci_register_bar
来给这个PCIe设备增加BAR,通过pcie_xxxxx_cap_init来给这个设备增加PCIe的各种capability。qemu里对PCI和PCIe设备是分开模拟的,如果你要加PCIe设备相关的capability,需要
创建一个PCIe设备,这个需要interfaces定义成 INTERFACE_PCIE_DEVICE,以及为这个
设备加上PCIe extend capability,使用pcie_endpoint_cap_init就可以了。我们这里的
DMA engine就是一个PCIe设备。在qemu里需要通过一个PCIe RP把一个PCIe设备接入到系统
里。下面的章节会提到具体的qemu命令。模拟设备的代码里,用DmaEngState表示被模拟的设备,因为他是一个PCIe设备,所以在
这个结构体一开始的位置放一个PCIDevice的结构,后面的DmaRawState的结构用来放和具体
业务相关的东西。使用lspci可以看到我们模拟出的是这样一个PCIe设备:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26# ./lspci -s 00:04.0 -vvv
00:04.0 Class 00ff: Device 1234:3456 (rev 10)
Subsystem: Device 1af4:1100
Control: I/O- Mem+ BusMaster+ SpecCycle- MemWINV- VGASnoop- ParErr- Stepping- SERR- FastB2B- DisINTx+
Status: Cap+ 66MHz- UDF- FastB2B- ParErr- DEVSEL=fast >TAbort- <TAbort- <MAbort- >SERR- <PERR- INTx-
Latency: 0
Interrupt: pin A routed to IRQ 55
IOMMU group: 0
Region 0: Memory at 10260000 (32-bit, non-prefetchable) [size=16K]
Capabilities: [e0] Express (v2) Root Complex Integrated Endpoint, MSI 00
DevCap: MaxPayload 128 bytes, PhantFunc 0
ExtTag- RBE+ FLReset-
DevCtl: CorrErr- NonFatalErr- FatalErr- UnsupReq-
RlxdOrd- ExtTag- PhantFunc- AuxPwr- NoSnoop-
MaxPayload 128 bytes, MaxReadReq 128 bytes
DevSta: CorrErr- NonFatalErr- FatalErr- UnsupReq- AuxPwr- TransPend-
DevCap2: Completion Timeout: Not Supported, TimeoutDis- NROPrPrP- LTR-
10BitTagComp- 10BitTagReq- OBFF Not Supported, ExtFmt+ EETLPPrefix+, MaxEETLPPrefixes 4
EmergencyPowerReduction Not Supported, EmergencyPowerReductionInit-
FRS-
AtomicOpsCap: 32bit- 64bit- 128bitCAS-
DevCtl2: Completion Timeout: 50us to 50ms, TimeoutDis- LTR- 10BitTagReq- OBFF Disabled,
AtomicOpsCtl: ReqEn-
Capabilities: [40] MSI: Enable+ Count=1/1 Maskable- 64bit+
Address: 00000000fffff040 Data: 0050
Kernel driver in use: dma_engineMMIO
DmaRawState里的MemoryRegion用来表示这个模拟设备上BAR对应的MMIO。在realize函数里
把这个地址空间和BAR0绑定,在dma_engine_state_init函数里为这段MMIO挂上read/write
处理函数。read/write的回调函数定义在dma_engine_io_ops, 软件读写相关的寄存器最终
都会在这些回调函数中处理。可以看到我们BAR size配置成了16KB。DMA
软件写0x1018 bit0为1触发一个DMA数据拷贝,相关实现代码在dma_engine_do_dma_copy里。
这个函数直接调用pci_dma_read/pci_dma_write做设备buffer和内存之间的数据搬移,这里
在使能smmuv3的时候,以上两个函数的内部实现会先调用smmuv3的translation函数做地址
翻译,然后再做数据搬移,在不使能smmuv3的时候,直接使用内存地址做数据搬移。如果只是模拟一个PCIe EP设备,可以不用管和iommu相关的内部实现。如果要做iommu的模拟,
可以参考这里中断
realize函数中使用pci_config_set_interrupt_pin给设备加一个INTx中断。使用msi_init
给设备加MSI中断。可以使用pci_irq_assert触发一个电平中断,通过msi_notify触发一个
MSI中断,通过qemu_irq_pulse触发一个边沿中断。
Linux内核驱动的实现
DMA engine设备的内核驱动比较简单,源码在[2]。这个驱动同时也通过sysfs接口向用户态
暴露了一个叫copy_size的文件,向这个文件写入一个数值将触发一次该数值大小的DMA数据
拷贝,为了方便起见,我们在这个驱动的内部生成需要拷贝的数据。
编译运行
qemu的编译运行在上面有提及。使用的Linux内核的基础版本是5.15-rc1,编译的时候打开
ARM_SMMU_V3和DMA_ENGINE_DEMO的配置即可。
我们可以在启动qemu的时候带上–trace “smmuv3_*”,这样可以打开qemu smmuv3的trace,
观察到smmuv3这个模拟设备内部的详细运行情况。我们也可以给DMA engine这个设备用类似
的方法加上trace。
1 | # echo 128 > copy_size |
如上的日志,smmuv3前缀的是qemu的trace,时间戳开头的是内核驱动的打印。
[1]https://github.com/wangzhou/qemu/blob/4612113da02716e8c56930e88ca8a142e180f175/hw/misc/dma_engine.c
[2]https://github.com/wangzhou/linux/blob/87695695e4d3ea72e60d9c5da5fc5804ae71fb48/drivers/misc/dma_engine/dma_engine.c