| /* |
| * A test device for IOMMU |
| * |
| * Copyright (c) 2026 Phytium Technology |
| * |
| * Author: |
| * Tao Tang <tangtao1634@phytium.com.cn> |
| * |
| * SPDX-License-Identifier: GPL-2.0-or-later |
| */ |
| |
| #include "qemu/osdep.h" |
| #include "system/address-spaces.h" |
| #include "system/memory.h" |
| #include "trace.h" |
| #include "hw/pci/pci_device.h" |
| #include "hw/core/qdev-properties.h" |
| #include "qom/object.h" |
| #include "hw/misc/iommu-testdev.h" |
| |
| #define TYPE_IOMMU_TESTDEV "iommu-testdev" |
| OBJECT_DECLARE_SIMPLE_TYPE(IOMMUTestDevState, IOMMU_TESTDEV) |
| |
| struct IOMMUTestDevState { |
| PCIDevice parent_obj; |
| MemoryRegion bar0; |
| uint64_t dma_vaddr; |
| uint64_t dma_paddr; |
| uint32_t dma_len; |
| uint32_t dma_result; |
| bool dma_armed; /* armed until a trigger consumes the request */ |
| |
| AddressSpace *dma_as; /* IOMMU-mediated DMA AS for this device */ |
| uint32_t dma_attrs_cfg; /* bit0 secure, bits[2:1] space, bit3 valid */ |
| }; |
| |
| static bool iommu_testdev_attrs_inconsistent(uint32_t cfg) |
| { |
| uint32_t space; |
| bool secure; |
| |
| if (!ITD_ATTRS_GET_SPACE_VALID(cfg)) { |
| return false; |
| } |
| |
| space = ITD_ATTRS_GET_SPACE(cfg); |
| secure = ITD_ATTRS_GET_SECURE(cfg); |
| |
| if (space == ITD_ATTRS_SPACE_SECURE || space == ITD_ATTRS_SPACE_NONSECURE) { |
| return secure != (space == ITD_ATTRS_SPACE_SECURE); |
| } |
| |
| return false; |
| } |
| |
| static void iommu_testdev_maybe_run_dma(IOMMUTestDevState *s) |
| { |
| uint32_t expected_val, actual_val; |
| g_autofree uint8_t *write_buf = NULL; |
| g_autofree uint8_t *read_buf = NULL; |
| MemTxResult write_res, read_res; |
| MemTxAttrs attrs = {}; |
| AddressSpace *as; |
| bool space_valid; |
| |
| if (!s->dma_armed) { |
| s->dma_result = ITD_DMA_ERR_NOT_ARMED; |
| trace_iommu_testdev_dma_result(s->dma_result); |
| return; |
| } |
| trace_iommu_testdev_dma_start(); |
| |
| if (!s->dma_len) { |
| s->dma_result = ITD_DMA_ERR_BAD_LEN; |
| goto out; |
| } |
| |
| write_buf = g_malloc(s->dma_len); |
| read_buf = g_malloc(s->dma_len); |
| |
| /* Initialize MemTxAttrs from generic register. */ |
| attrs.secure = ITD_ATTRS_GET_SECURE(s->dma_attrs_cfg); |
| |
| space_valid = ITD_ATTRS_GET_SPACE_VALID(s->dma_attrs_cfg); |
| if (space_valid) { |
| /* The 'space' field in MemTxAttrs is ARM-specific. */ |
| attrs.space = ITD_ATTRS_GET_SPACE(s->dma_attrs_cfg); |
| } else { |
| /* Default to Non-Secure when space is not valid. */ |
| attrs.space = ITD_ATTRS_SPACE_NONSECURE; |
| } |
| |
| if (iommu_testdev_attrs_inconsistent(s->dma_attrs_cfg)) { |
| s->dma_result = ITD_DMA_ERR_BAD_ATTRS; |
| goto out; |
| } |
| |
| as = s->dma_as; |
| |
| /* Step 1: Write ITD_DMA_WRITE_VAL to DMA address */ |
| trace_iommu_testdev_dma_write(s->dma_vaddr, s->dma_len); |
| |
| for (int i = 0; i < s->dma_len; i++) { |
| /* Data is written in little-endian order */ |
| write_buf[i] = (ITD_DMA_WRITE_VAL >> ((i % 4) * 8)) & 0xff; |
| } |
| write_res = dma_memory_write(as, s->dma_vaddr, write_buf, |
| s->dma_len, attrs); |
| |
| if (write_res != MEMTX_OK) { |
| s->dma_result = ITD_DMA_ERR_TX_FAIL; |
| goto out; |
| } |
| |
| /* Step 2: Read back from the same DMA address */ |
| trace_iommu_testdev_dma_read(s->dma_vaddr, s->dma_len); |
| |
| read_res = address_space_read(&address_space_memory, s->dma_paddr, |
| attrs, read_buf, s->dma_len); |
| |
| if (read_res != MEMTX_OK) { |
| s->dma_result = ITD_DMA_ERR_RD_FAIL; |
| goto out; |
| } |
| |
| /* Step 3: Verify the read data matches what we wrote */ |
| for (int i = 0; i < s->dma_len; i += 4) { |
| int remaining_bytes = MIN(4, s->dma_len - i); |
| |
| expected_val = 0; |
| actual_val = 0; |
| |
| for (int j = 0; j < remaining_bytes; j++) { |
| expected_val |= ((uint32_t)write_buf[i + j]) << (j * 8); |
| actual_val |= ((uint32_t)read_buf[i + j]) << (j * 8); |
| } |
| |
| trace_iommu_testdev_dma_verify(expected_val, actual_val); |
| |
| if (expected_val != actual_val) { |
| s->dma_result = ITD_DMA_ERR_MISMATCH; |
| goto out; |
| } |
| } |
| |
| /* All checks passed */ |
| s->dma_result = 0; |
| out: |
| trace_iommu_testdev_dma_result(s->dma_result); |
| s->dma_armed = false; |
| } |
| |
| static uint64_t iommu_testdev_mmio_read(void *opaque, hwaddr addr, |
| unsigned size) |
| { |
| IOMMUTestDevState *s = opaque; |
| uint64_t value = 0; |
| |
| switch (addr) { |
| case ITD_REG_DMA_TRIGGERING: |
| /* |
| * This lets tests poll ITD_REG_DMA_RESULT to observe BUSY before |
| * consuming the DMA. |
| */ |
| iommu_testdev_maybe_run_dma(s); |
| value = 0; |
| break; |
| case ITD_REG_DMA_GVA_LO: |
| value = (uint32_t)(s->dma_vaddr & 0xffffffffu); |
| break; |
| case ITD_REG_DMA_GVA_HI: |
| value = (uint32_t)(s->dma_vaddr >> 32); |
| break; |
| case ITD_REG_DMA_GPA_LO: |
| value = (uint32_t)(s->dma_paddr & 0xffffffffu); |
| break; |
| case ITD_REG_DMA_GPA_HI: |
| value = (uint32_t)(s->dma_paddr >> 32); |
| break; |
| case ITD_REG_DMA_LEN: |
| value = s->dma_len; |
| break; |
| case ITD_REG_DMA_RESULT: |
| value = s->dma_result; |
| break; |
| case ITD_REG_DMA_ATTRS: |
| value = s->dma_attrs_cfg; |
| break; |
| default: |
| value = 0; |
| break; |
| } |
| |
| trace_iommu_testdev_mmio_read(addr, value, size); |
| return value; |
| } |
| |
| static void iommu_testdev_mmio_write(void *opaque, hwaddr addr, uint64_t val, |
| unsigned size) |
| { |
| IOMMUTestDevState *s = opaque; |
| uint32_t data = val; |
| |
| trace_iommu_testdev_mmio_write(addr, val, size); |
| |
| switch (addr) { |
| case ITD_REG_DMA_GVA_LO: |
| s->dma_vaddr = (s->dma_vaddr & ~0xffffffffull) | data; |
| break; |
| case ITD_REG_DMA_GVA_HI: |
| s->dma_vaddr = (s->dma_vaddr & 0xffffffffull) | |
| ((uint64_t)data << 32); |
| break; |
| case ITD_REG_DMA_GPA_LO: |
| s->dma_paddr = (s->dma_paddr & ~0xffffffffull) | data; |
| break; |
| case ITD_REG_DMA_GPA_HI: |
| s->dma_paddr = (s->dma_paddr & 0xffffffffull) | |
| ((uint64_t)data << 32); |
| break; |
| case ITD_REG_DMA_LEN: |
| s->dma_len = data; |
| break; |
| case ITD_REG_DMA_RESULT: |
| s->dma_result = data; |
| break; |
| case ITD_REG_DMA_DBELL: |
| if (data & ITD_DMA_DBELL_ARM) { |
| /* Arm the DMA operation; repeated arm is idempotent. */ |
| s->dma_armed = true; |
| s->dma_result = ITD_DMA_RESULT_BUSY; |
| trace_iommu_testdev_dma_armed(true); |
| } else { |
| /* Disarm the DMA operation */ |
| s->dma_armed = false; |
| s->dma_result = ITD_DMA_RESULT_IDLE; |
| trace_iommu_testdev_dma_armed(false); |
| } |
| break; |
| case ITD_REG_DMA_ATTRS: |
| s->dma_attrs_cfg = data; |
| break; |
| default: |
| break; |
| } |
| } |
| |
| static const MemoryRegionOps iommu_testdev_mmio_ops = { |
| .read = iommu_testdev_mmio_read, |
| .write = iommu_testdev_mmio_write, |
| .endianness = DEVICE_LITTLE_ENDIAN, |
| .valid = { |
| .min_access_size = 4, |
| .max_access_size = 4, |
| }, |
| }; |
| |
| static void iommu_testdev_realize(PCIDevice *pdev, Error **errp) |
| { |
| IOMMUTestDevState *s = IOMMU_TESTDEV(pdev); |
| |
| s->dma_vaddr = 0; |
| s->dma_paddr = 0; |
| s->dma_len = 0; |
| s->dma_result = ITD_DMA_RESULT_IDLE; |
| s->dma_armed = false; |
| s->dma_attrs_cfg = ITD_ATTRS_SET_SPACE(0, ITD_ATTRS_SPACE_NONSECURE); |
| s->dma_as = pci_device_iommu_address_space(pdev); |
| |
| memory_region_init_io(&s->bar0, OBJECT(pdev), &iommu_testdev_mmio_ops, s, |
| TYPE_IOMMU_TESTDEV ".bar0", BAR0_SIZE); |
| pci_register_bar(pdev, 0, PCI_BASE_ADDRESS_SPACE_MEMORY, &s->bar0); |
| } |
| |
| static void iommu_testdev_reset(DeviceState *dev) |
| { |
| IOMMUTestDevState *s = IOMMU_TESTDEV(dev); |
| |
| s->dma_vaddr = 0; |
| s->dma_paddr = 0; |
| s->dma_len = 0; |
| s->dma_result = ITD_DMA_RESULT_IDLE; |
| s->dma_armed = false; |
| s->dma_attrs_cfg = ITD_ATTRS_SET_SPACE(0, ITD_ATTRS_SPACE_NONSECURE); |
| } |
| |
| static void iommu_testdev_class_init(ObjectClass *klass, const void *data) |
| { |
| DeviceClass *dc = DEVICE_CLASS(klass); |
| PCIDeviceClass *pc = PCI_DEVICE_CLASS(klass); |
| |
| pc->realize = iommu_testdev_realize; |
| pc->vendor_id = IOMMU_TESTDEV_VENDOR_ID; |
| pc->device_id = IOMMU_TESTDEV_DEVICE_ID; |
| pc->revision = 0; |
| pc->class_id = PCI_CLASS_OTHERS; |
| dc->desc = "A test device for IOMMU"; |
| set_bit(DEVICE_CATEGORY_MISC, dc->categories); |
| device_class_set_legacy_reset(dc, iommu_testdev_reset); |
| } |
| |
| static const TypeInfo iommu_testdev_info = { |
| .name = TYPE_IOMMU_TESTDEV, |
| .parent = TYPE_PCI_DEVICE, |
| .instance_size = sizeof(IOMMUTestDevState), |
| .class_init = iommu_testdev_class_init, |
| .interfaces = (const InterfaceInfo[]) { |
| { INTERFACE_CONVENTIONAL_PCI_DEVICE }, |
| { } |
| }, |
| }; |
| |
| static void iommu_testdev_register_types(void) |
| { |
| type_register_static(&iommu_testdev_info); |
| } |
| |
| type_init(iommu_testdev_register_types); |