QEMU-Arm Archive on lore.kernel.org
 help / color / mirror / Atom feed
* [PATCH v3 0/4] hw/usb: Add ASPEED USB Device Controller (UDC)
@ 2026-08-31  9:21 Jamin Lin
  2026-08-31  9:21 ` [PATCH v3 1/4] hw/usb/aspeed-udc: Add ASPEED UDC device controller Jamin Lin
                   ` (3 more replies)
  0 siblings, 4 replies; 8+ messages in thread
From: Jamin Lin @ 2026-08-31  9:21 UTC (permalink / raw)
  To: Paolo Bonzini, Peter Maydell, Cédric Le Goater, Steven Lee,
	Troy Lee, Kane Chen, Andrew Jeffery, Joel Stanley,
	open list:ARM TCG CPUs, open list:All patches CC here
  Cc: Jamin Lin, Troy Lee

This series adds a QEMU model for the ASPEED USB Device Controller (UDC),
driven by the Linux "aspeed_udc" gadget driver.

It is the first step of a larger plan to model USB device-side support on
ASPEED BMC/BIC SoCs, which has three goals:

1. Model the ASPEED UDC (AST2600 / AST1030). The AST2600 also has USB host
   (EHCI) controllers, so this series targets the AST2600 UDC: its gadget can
   be attached to the SoC's own EHCI bus, letting the guest enumerate its own
   gadget and exercise the UDC end-to-end.  [this series]

2. AST1030 UDC. The AST1030 has no USB host controller, so testing its UDC
   needs a second QEMU instance. The plan is to redirect the UDC gadget out
   of the guest using libusbredir and attach it to another QEMU that runs a
   USB host (a VMM, or an AST2600 / AST2700 guest).  [on-going]
   
3. ASPEED vHub, as a longer-term goal towards BMC KVM / Virtual Media support
   in QEMU.  [future]

This series implements goal 1 only.

Design
------
The UDC is modelled as two QOM objects, because a single object cannot be
both a SysBusDevice and a USBDevice:

  - "aspeed.udc": the sysbus device (MMIO register map, IRQ and DMA engine)
    that the guest gadget driver programs.

  - "aspeed.udc-gadget": a user-creatable USB device presented on a USB host
    controller's bus. It links back to its controller through the "udc"
    property.

The SoC creates the controller; the gadget is added on the command line, e.g.

  -device aspeed.udc-gadget,udc=/machine/soc/udc
     
Test result:
-----------
The default ASPEED SDK prebuilt image does not enable the UDC driver, so
build a kernel with CONFIG_USB_ASPEED_UDC=y first.

Start QEMU with the gadget attached to the on-SoC EHCI:

  qemu-system-arm -machine ast2600-evb -drive file=<image>,if=mtd,format=raw \
      -device aspeed.udc-gadget,udc=/machine/soc/udc -nographic

In the guest, bring up a mass-storage gadget and verify enumeration and I/O:

1. Before: only the host controllers are present
root@ast2600-default:~# lsusb
unable to initialize usb specBus 001 Device 001: ID 1d6b:0002 Linux 6.18.20-dirty-32e49fb4a22b-g32e49fb4a22b-dirty ehci_hcd EHCI Host Controller
Bus 002 Device 001: ID 1d6b:0001 Linux 6.18.20-dirty-32e49fb4a22b-g32e49fb4a22b-dirty uhci_hcd Generic UHCI Host Controller

2. Enable the gadget
root@ast2600-default:~# ./usb-storage.sh
Using UDC: 1e6a2000.usb
[  598.582205] Mass Storage Function, version: 2009/09/11
[  598.582635] LUN: removable file: (no medium)
USB Mass Storage gadget is enabled.
Backing file: /home/root/jamin
root@ast2600-default:~# [  598.876395] usb 1-1: new high-speed USB device number 2 using ehci-platform
[  599.070035] usb 1-1: New USB device found, idVendor=1d6b, idProduct=0104, bcdDevice= 1.00
[  599.070821] usb 1-1: New USB device strings: Mfr=1, Product=2, SerialNumber=3
[  599.071448] usb 1-1: Product: ASPEED USB Storage
[  599.072687] usb 1-1: Manufacturer: ASPEED
[  599.073384] usb 1-1: SerialNumber: 1234567890
[  599.103625] usb-storage 1-1:1.0: USB Mass Storage device detected
[  599.136326] scsi host0: usb-storage 1-1:1.0
[  600.230921] scsi 0:0:0:0: Direct-Access     Linux    File-Stor Gadget 0618 PQ: 0 ANSI: 2
[  600.243758] sd 0:0:0:0: Attached scsi generic sg0 type 0
[  600.263452] sd 0:0:0:0: Power-on or device reset occurred
[  600.289381] sd 0:0:0:0: [sda] 2048 512-byte logical blocks: (1.05 MB/1.00 MiB)
[  600.297896] sd 0:0:0:0: [sda] Write Protect is off
[  600.306379] sd 0:0:0:0: [sda] Write cache: enabled, read cache: enabled, doesn't support DPO or FUA
[  600.424608]  sda:
[  600.425526] sd 0:0:0:0: [sda] Attached SCSI removable disk

3. After: the gadget enumerates on the EHCI bus
root@ast2600-default:~# lsusb
unable to initialize usb specBus 001 Device 001: ID 1d6b:0002 Linux 6.18.20-dirty-32e49fb4a22b-g32e49fb4a22b-dirty ehci_hcd EHCI Host Controller
Bus 001 Device 002: ID 1d6b:0104 ASPEED ASPEED USB Storage
Bus 002 Device 001: ID 1d6b:0001 Linux 6.18.20-dirty-32e49fb4a22b-g32e49fb4a22b-dirty uhci_hcd Generic UHCI Host Controller

4. Mount and verify data
root@ast2600-default:~# mount /dev/sda /mnt/
root@ast2600-default:~# sha256sum /mnt/testfile
3308890a1289f6a327c014537523e8ebd0833301af3d25a7c5893dc7050d2c70  /mnt/testfile
root@ast2600-default:~# umount /mnt

5. Stop the gadget -> clean disconnect
root@ast2600-default:~# ./usb-storage.sh stop
Stopping USB gadget...
[  669.866439] usb 1-1: USB disconnect, device number 2
Stopped.
root@ast2600-default:~# [  669.968156] sd 0:0:0:0: [sda] Synchronizing SCSI cache
[  669.969307] sd 0:0:0:0: [sda] Synchronize Cache(10) failed: Result: hostbyte=0x01 driverbyte=DRIVER_OK

root@ast2600-default:~# lsusb
unable to initialize usb specBus 001 Device 001: ID 1d6b:0002 Linux 6.18.20-dirty-32e49fb4a22b-g32e49fb4a22b-dirty ehci_hcd EHCI Host Controller
Bus 002 Device 001: ID 1d6b:0001 Linux 6.18.20-dirty-32e49fb4a22b-g32e49fb4a22b-dirty uhci_hcd Generic UHCI Host Controller


The contents of this scripts
---------------------------
root@ast2600-default:~# cat  usb-storage.sh
#!/bin/sh
set -e

G=/sys/kernel/config/usb_gadget/g1
IMG=/home/root/jamin
SIZE_MB=64

mount_configfs()
{
    mountpoint -q /sys/kernel/config || mount -t configfs none /sys/kernel/config
}

get_udc()
{
    ls /sys/class/udc | head -n 1
}

stop_gadget()
{
    if [ ! -d "$G" ]; then
        return
    fi

    echo "Stopping USB gadget..."

    # Unbind UDC first
    if [ -f "$G/UDC" ]; then
        echo "" > "$G/UDC" 2>/dev/null || true
    fi

    # Clear backing file to release image
    if [ -f "$G/functions/mass_storage.0/lun.0/file" ]; then
        echo "" > "$G/functions/mass_storage.0/lun.0/file" 2>/dev/null || true
    fi

    # Remove function link from config
    rm -f "$G/configs/c.1/mass_storage.0" 2>/dev/null || true

    echo "Stopped."
}

start_gadget()
{
    mount_configfs

    UDC="$(get_udc)"
    if [ -z "$UDC" ]; then
        echo "ERROR: No UDC found in /sys/class/udc"
        exit 1
    fi

    stop_gadget

    echo "Using UDC: $UDC"

    modprobe libcomposite 2>/dev/null || true

    mkdir -p "$G"
    cd "$G"

    echo 0x1d6b > idVendor
    echo 0x0104 > idProduct
    echo 0x0200 > bcdUSB
    echo 0x0100 > bcdDevice

    mkdir -p strings/0x409
    echo "1234567890" > strings/0x409/serialnumber
    echo "ASPEED" > strings/0x409/manufacturer
    echo "ASPEED USB Storage" > strings/0x409/product

    mkdir -p configs/c.1/strings/0x409
    echo "Mass Storage" > configs/c.1/strings/0x409/configuration
    echo 120 > configs/c.1/MaxPower

    if [ ! -f "$IMG" ]; then
        echo "Creating backing image: $IMG"
        dd if=/dev/zero of="$IMG" bs=1M count="$SIZE_MB"
        mkfs.vfat "$IMG"
    fi

    mkdir -p functions/mass_storage.0

    # Make sure old LUN is detached before changing attributes
    echo "" > functions/mass_storage.0/lun.0/file 2>/dev/null || true

    echo 0 > functions/mass_storage.0/stall
    echo 0 > functions/mass_storage.0/lun.0/cdrom
    echo 0 > functions/mass_storage.0/lun.0/ro
    echo 1 > functions/mass_storage.0/lun.0/removable
    echo "$IMG" > functions/mass_storage.0/lun.0/file

    ln -sf functions/mass_storage.0 configs/c.1/mass_storage.0

    echo "$UDC" > UDC

    echo "USB Mass Storage gadget is enabled."
    echo "Backing file: $IMG"
}

case "$1" in
    start|"")
        start_gadget
        ;;
    stop)
        stop_gadget
        ;;
    restart)
        stop_gadget
        sleep 1
        start_gadget
        ;;
    *)
        echo "Usage: $0 {start|stop|restart}"
        exit 1
        ;;
esac


v1:
  1. Add ASPEED UDC device controller
  2. Add ASPEED UDC gadget USB device

v2:
  1. Replace dynamic memory allocation with fixed-size arrays.
  2. Use an OR gate to route the interrupt signals from the UDC and EHCI2.
  3. Split the "Add ASPEED UDC gadget USB device" patch into two patches:
    a. Add ASPEED UDC gadget USB device
      Implement EP0 control transfers.
    b. Add programmable endpoint DMA transfers
      Implement DMA transfers for programmable endpoints.
  4. Remove unused macros.
  
v3:
  1. Remove unused macros.
  2. Bind the gadget to the controller via a QOM link property
   ("usbgadget", weak) instead of writing the back-pointer directly

Jamin Lin (4):
  hw/usb/aspeed-udc: Add ASPEED UDC device controller
  hw/usb/aspeed-udc: Add ASPEED UDC gadget USB device
  hw/usb/aspeed-udc: Add programmable endpoint DMA transfers
  hw/arm/aspeed_ast2600: Wire up the UDC

 include/hw/arm/aspeed_soc.h |    4 +
 include/hw/usb/aspeed-udc.h |   85 +++
 hw/arm/aspeed_ast2600.c     |   37 +-
 hw/usb/aspeed-udc.c         | 1059 +++++++++++++++++++++++++++++++++++
 hw/arm/Kconfig              |    1 +
 hw/usb/Kconfig              |    4 +
 hw/usb/meson.build          |    1 +
 hw/usb/trace-events         |   16 +
 8 files changed, 1204 insertions(+), 3 deletions(-)
 create mode 100644 include/hw/usb/aspeed-udc.h
 create mode 100644 hw/usb/aspeed-udc.c

-- 
2.53.0


^ permalink raw reply	[flat|nested] 8+ messages in thread

* [PATCH v3 1/4] hw/usb/aspeed-udc: Add ASPEED UDC device controller
  2026-08-31  9:21 [PATCH v3 0/4] hw/usb: Add ASPEED USB Device Controller (UDC) Jamin Lin
@ 2026-08-31  9:21 ` Jamin Lin
  2026-08-31 10:03   ` Philippe Mathieu-Daudé
  2026-08-31  9:21 ` [PATCH v3 2/4] hw/usb/aspeed-udc: Add ASPEED UDC gadget USB device Jamin Lin
                   ` (2 subsequent siblings)
  3 siblings, 1 reply; 8+ messages in thread
From: Jamin Lin @ 2026-08-31  9:21 UTC (permalink / raw)
  To: Paolo Bonzini, Peter Maydell, Cédric Le Goater, Steven Lee,
	Troy Lee, Kane Chen, Andrew Jeffery, Joel Stanley,
	open list:ARM TCG CPUs, open list:All patches CC here
  Cc: Jamin Lin, Troy Lee

The AST2600 has a USB 2.0 Device Controller (UDC) at 0x1e6a2000 with one
control endpoint and four programmable endpoints.

Add the controller as a sysbus (system) device: the MMIO register map
described with the registerfields macros, the interrupt line and the
soft reset. This is only the register/system side.

Note: this "device controller" is the system-bus device (TYPE_ASPEED_UDC).
It is not the gadget USB device (TYPE_ASPEED_UDC_GADGET) that a host
controller enumerates, which is added in the next patch.

Signed-off-by: Jamin Lin <jamin_lin@aspeedtech.com>
---
 include/hw/usb/aspeed-udc.h |  49 ++++++++
 hw/usb/aspeed-udc.c         | 236 ++++++++++++++++++++++++++++++++++++
 hw/arm/Kconfig              |   1 +
 hw/usb/Kconfig              |   4 +
 hw/usb/meson.build          |   1 +
 hw/usb/trace-events         |   7 ++
 6 files changed, 298 insertions(+)
 create mode 100644 include/hw/usb/aspeed-udc.h
 create mode 100644 hw/usb/aspeed-udc.c

diff --git a/include/hw/usb/aspeed-udc.h b/include/hw/usb/aspeed-udc.h
new file mode 100644
index 0000000000..58fed5f9a2
--- /dev/null
+++ b/include/hw/usb/aspeed-udc.h
@@ -0,0 +1,49 @@
+/*
+ * ASPEED USB Device Controller (UDC)
+ *
+ * Copyright (c) 2026 ASPEED Technology Inc.
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#ifndef HW_USB_ASPEED_UDC_H
+#define HW_USB_ASPEED_UDC_H
+
+#include "hw/core/sysbus.h"
+#include "qom/object.h"
+
+#define TYPE_ASPEED_UDC "aspeed.udc"
+OBJECT_DECLARE_SIMPLE_TYPE(AspeedUDCState, ASPEED_UDC)
+
+/*
+ * Register map: root/global block at 0x000 - 0x087, then one 0x10 byte bank
+ * per programmable endpoint from 0x200.
+ */
+#define ASPEED_UDC_MEM_SIZE     0x300
+#define ASPEED_UDC_ROOT_NR_REGS (0x88 >> 2)
+#define ASPEED_UDC_EP_REG_BASE  0x200
+#define ASPEED_UDC_EP_NR_REGS   (0x10 >> 2)
+
+/*
+ * EP0 (control) is served through the root registers (UDC_EP0_*), so only
+ * the 4 programmable endpoints get their own register bank / ep[] entry.
+ */
+#define ASPEED_UDC_NUM_EP   4
+
+typedef struct AspeedUDCEP {
+    MemoryRegion mr;
+    uint32_t regs[ASPEED_UDC_EP_NR_REGS];
+    int index;
+} AspeedUDCEP;
+
+struct AspeedUDCState {
+    SysBusDevice parent_obj;
+
+    MemoryRegion udc_container;
+    MemoryRegion root_mr;
+    uint32_t regs[ASPEED_UDC_ROOT_NR_REGS];
+    AspeedUDCEP ep[ASPEED_UDC_NUM_EP];
+    qemu_irq irq;
+};
+
+#endif /* HW_USB_ASPEED_UDC_H */
diff --git a/hw/usb/aspeed-udc.c b/hw/usb/aspeed-udc.c
new file mode 100644
index 0000000000..cd773e979b
--- /dev/null
+++ b/hw/usb/aspeed-udc.c
@@ -0,0 +1,236 @@
+/*
+ * ASPEED USB Device Controller (UDC)
+ *
+ * Copyright (c) 2026 ASPEED Technology Inc.
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ * Models the ASPEED USB Device Controller (UDC). It implements one control
+ * endpoint (EP0) and 4 programmable endpoints.
+ *
+ * This file is the system-bus side of the controller: the MMIO register map,
+ * the interrupt and the soft reset. The gadget USB device presented to a host
+ * controller (and the endpoint data path) is added on top of this.
+ */
+
+#include "qemu/osdep.h"
+#include "hw/core/irq.h"
+#include "hw/core/registerfields.h"
+#include "hw/usb/aspeed-udc.h"
+#include "qemu/module.h"
+#include "trace.h"
+
+/* Root / Global registers (offset from the controller base) */
+REG32(UDC_FUNC_CTRL, 0x00)
+    FIELD(UDC_FUNC_CTRL, UPSTREAM_EN,       0, 1)
+REG32(UDC_IER, 0x08)
+REG32(UDC_ISR, 0x0C)
+    FIELD(UDC_ISR, EP_POOL_ACK,            16, 1)
+    FIELD(UDC_ISR, BUS_RESET,               6, 1)
+    FIELD(UDC_ISR, EP0_IN_ACK,              3, 1)
+    FIELD(UDC_ISR, EP0_OUT_ACK,             1, 1)
+    FIELD(UDC_ISR, EP0_SETUP,               0, 1)
+REG32(UDC_EP_ACK_IER, 0x10)
+REG32(UDC_EP_NAK_IER, 0x14)
+REG32(UDC_EP_ACK_ISR, 0x18)
+REG32(UDC_EP_NAK_ISR, 0x1C)
+REG32(UDC_DEV_RESET, 0x20)
+    FIELD(UDC_DEV_RESET, EP_POOL,           9, 1)
+    FIELD(UDC_DEV_RESET, DMA,               8, 1)
+    FIELD(UDC_DEV_RESET, ROOT,              0, 1)
+REG32(UDC_STS, 0x24)
+    FIELD(UDC_STS, HIGHSPEED,              27, 1)
+REG32(UDC_EP0_CTRL, 0x30)
+    FIELD(UDC_EP0_CTRL, RX_LEN,            16, 7)
+    FIELD(UDC_EP0_CTRL, TX_LEN,             8, 7)
+    FIELD(UDC_EP0_CTRL, RX_RDY,             2, 1)
+    FIELD(UDC_EP0_CTRL, TX_RDY,             1, 1)
+    FIELD(UDC_EP0_CTRL, STALL,              0, 1)
+REG32(UDC_EP0_DATA_BUFF, 0x34)
+/* EP0 SETUP packet buffer: SETUP0 = bytes 0...3, SETUP1 = bytes 4...7 */
+REG32(UDC_SETUP0, 0x80)
+REG32(UDC_SETUP1, 0x84)
+
+/* Per programmable-endpoint registers (offset from the EP register base) */
+REG32(EP_CONFIG, 0x00)
+    FIELD(EP_CONFIG, MAX_PKT,              16, 10)
+    FIELD(EP_CONFIG, EP_NUM,                8, 4)
+    FIELD(EP_CONFIG, DIR_OUT,               4, 1)
+    FIELD(EP_CONFIG, ENABLE,                0, 1)
+REG32(EP_DMA_CTRL, 0x04)
+    FIELD(EP_DMA_CTRL, PROC_STS,            4, 4)
+    FIELD(EP_DMA_CTRL, DESC_OP_EN,          0, 1)
+REG32(EP_DMA_BUFF, 0x08)
+REG32(EP_DMA_STS, 0x0C)
+    FIELD(EP_DMA_STS, PKT_SIZE,            16, 11)
+    FIELD(EP_DMA_STS, RPTR,                 8, 8)
+    FIELD(EP_DMA_STS, WPTR,                 0, 8)
+
+static void aspeed_udc_update_irq(AspeedUDCState *s)
+{
+    bool level;
+
+    level = (s->regs[R_UDC_ISR] & s->regs[R_UDC_IER]) ||
+            (s->regs[R_UDC_EP_ACK_ISR] & s->regs[R_UDC_EP_ACK_IER]) ||
+            (s->regs[R_UDC_EP_NAK_ISR] & s->regs[R_UDC_EP_NAK_IER]);
+
+    trace_aspeed_udc_irq(s->regs[R_UDC_ISR], s->regs[R_UDC_IER], level);
+    qemu_set_irq(s->irq, level);
+}
+
+static uint64_t aspeed_udc_read(void *opaque, hwaddr offset, unsigned size)
+{
+    AspeedUDCState *s = ASPEED_UDC(opaque);
+    uint32_t reg = offset >> 2;
+    uint32_t val;
+
+    val = s->regs[reg];
+    trace_aspeed_udc_read(offset, val);
+
+    return val;
+}
+
+static void aspeed_udc_write(void *opaque, hwaddr offset, uint64_t data,
+                             unsigned size)
+{
+    AspeedUDCState *s = ASPEED_UDC(opaque);
+    uint32_t reg = offset >> 2;
+    uint32_t val = data;
+
+    trace_aspeed_udc_write(offset, val);
+
+    switch (reg) {
+    case R_UDC_IER:
+    case R_UDC_EP_ACK_IER:
+    case R_UDC_EP_NAK_IER:
+        s->regs[reg] = val;
+        aspeed_udc_update_irq(s);
+        break;
+    case R_UDC_ISR:
+    case R_UDC_EP_ACK_ISR:
+    case R_UDC_EP_NAK_ISR:
+        s->regs[reg] &= ~val;
+        aspeed_udc_update_irq(s);
+        break;
+    default:
+        s->regs[reg] = val;
+        break;
+    }
+}
+
+static const MemoryRegionOps aspeed_udc_ops = {
+    .read = aspeed_udc_read,
+    .write = aspeed_udc_write,
+    .endianness = DEVICE_LITTLE_ENDIAN,
+    .valid = {
+        .min_access_size = 1,
+        .max_access_size = 4,
+    },
+    .impl = {
+        .min_access_size = 4,
+        .max_access_size = 4,
+    },
+};
+
+static uint64_t aspeed_udc_ep_read(void *opaque, hwaddr offset, unsigned size)
+{
+    AspeedUDCEP *e = opaque;
+    uint32_t reg = offset >> 2;
+    uint32_t val;
+
+    val = e->regs[reg];
+    trace_aspeed_udc_ep_read(e->index, offset, val);
+
+    return val;
+}
+
+static void aspeed_udc_ep_write(void *opaque, hwaddr offset, uint64_t data,
+                                unsigned size)
+{
+    AspeedUDCEP *e = opaque;
+    uint32_t reg = offset >> 2;
+
+    trace_aspeed_udc_ep_write(e->index, offset, data);
+    e->regs[reg] = data;
+}
+
+static const MemoryRegionOps aspeed_udc_ep_ops = {
+    .read = aspeed_udc_ep_read,
+    .write = aspeed_udc_ep_write,
+    .endianness = DEVICE_LITTLE_ENDIAN,
+    .valid = {
+        .min_access_size = 1,
+        .max_access_size = 4,
+    },
+    .impl = {
+        .min_access_size = 4,
+        .max_access_size = 4,
+    },
+};
+
+static void aspeed_udc_reset_hold(Object *obj, ResetType type)
+{
+    AspeedUDCState *s = ASPEED_UDC(obj);
+    int i;
+
+    memset(s->regs, 0, sizeof(s->regs));
+    for (i = 0; i < ASPEED_UDC_NUM_EP; i++) {
+        memset(s->ep[i].regs, 0, sizeof(s->ep[i].regs));
+    }
+
+    /* Device-reset default: root, DMA and EP-pool soft-reset bits set */
+    s->regs[R_UDC_DEV_RESET] = (R_UDC_DEV_RESET_ROOT_MASK |
+                                R_UDC_DEV_RESET_DMA_MASK |
+                                R_UDC_DEV_RESET_EP_POOL_MASK);
+}
+
+static void aspeed_udc_realize(DeviceState *dev, Error **errp)
+{
+    SysBusDevice *sbd = SYS_BUS_DEVICE(dev);
+    AspeedUDCState *s = ASPEED_UDC(dev);
+    int i;
+
+    memory_region_init(&s->udc_container, OBJECT(s), TYPE_ASPEED_UDC,
+                       ASPEED_UDC_MEM_SIZE);
+    memory_region_init_io(&s->root_mr, OBJECT(s), &aspeed_udc_ops, s,
+                          TYPE_ASPEED_UDC ".root",
+                          ASPEED_UDC_ROOT_NR_REGS << 2);
+    memory_region_add_subregion(&s->udc_container, 0, &s->root_mr);
+
+    /* Each programmable endpoint has its own register bank */
+    for (i = 0; i < ASPEED_UDC_NUM_EP; i++) {
+        g_autofree char *name = g_strdup_printf(TYPE_ASPEED_UDC ".ep%d", i);
+
+        s->ep[i].index = i;
+        memory_region_init_io(&s->ep[i].mr, OBJECT(s), &aspeed_udc_ep_ops,
+                              &s->ep[i], name, ASPEED_UDC_EP_NR_REGS << 2);
+        memory_region_add_subregion(&s->udc_container,
+                                    ASPEED_UDC_EP_REG_BASE +
+                                    i * (ASPEED_UDC_EP_NR_REGS << 2),
+                                    &s->ep[i].mr);
+    }
+
+    sysbus_init_mmio(sbd, &s->udc_container);
+    sysbus_init_irq(sbd, &s->irq);
+}
+
+static void aspeed_udc_class_init(ObjectClass *klass, const void *data)
+{
+    DeviceClass *dc = DEVICE_CLASS(klass);
+    ResettableClass *rc = RESETTABLE_CLASS(klass);
+
+    dc->desc = "ASPEED USB Device Controller";
+    dc->realize = aspeed_udc_realize;
+    rc->phases.hold = aspeed_udc_reset_hold;
+}
+
+static const TypeInfo aspeed_udc_types[] = {
+    {
+        .name          = TYPE_ASPEED_UDC,
+        .parent        = TYPE_SYS_BUS_DEVICE,
+        .instance_size = sizeof(AspeedUDCState),
+        .class_init    = aspeed_udc_class_init,
+    },
+};
+
+DEFINE_TYPES(aspeed_udc_types)
diff --git a/hw/arm/Kconfig b/hw/arm/Kconfig
index 260d2f0751..146ed1a0be 100644
--- a/hw/arm/Kconfig
+++ b/hw/arm/Kconfig
@@ -536,6 +536,7 @@ config ASPEED_SOC
     imply GENERIC_LOADER
     imply PCI_DEVICES
     imply E1000E_PCI_EXPRESS
+    select ASPEED_UDC
     select DS1338
     select FTGMAC100
     select I2C
diff --git a/hw/usb/Kconfig b/hw/usb/Kconfig
index de95686720..e8c00f813a 100644
--- a/hw/usb/Kconfig
+++ b/hw/usb/Kconfig
@@ -146,3 +146,7 @@ config XLNX_USB_SUBSYS
 config USB_CHIPIDEA
     bool
     select USB_EHCI_SYSBUS
+
+config ASPEED_UDC
+    bool
+    select USB
diff --git a/hw/usb/meson.build b/hw/usb/meson.build
index ba55c28ef6..d4ba60a91c 100644
--- a/hw/usb/meson.build
+++ b/hw/usb/meson.build
@@ -27,6 +27,7 @@ system_ss.add(when: 'CONFIG_USB_XHCI_NEC', if_true: files('hcd-xhci-nec.c'))
 system_ss.add(when: 'CONFIG_USB_DWC2', if_true: files('hcd-dwc2.c'))
 system_ss.add(when: 'CONFIG_USB_DWC3', if_true: files('hcd-dwc3.c'))
 system_ss.add(when: 'CONFIG_USB_CHIPIDEA', if_true: files('chipidea.c'))
+system_ss.add(when: 'CONFIG_ASPEED_UDC', if_true: files('aspeed-udc.c'))
 
 system_ss.add(when: 'CONFIG_IMX_USBPHY', if_true: files('imx-usb-phy.c'))
 system_ss.add(when: 'CONFIG_VT82C686', if_true: files('vt82c686-uhci-pci.c'))
diff --git a/hw/usb/trace-events b/hw/usb/trace-events
index 67249d69c2..ed05304520 100644
--- a/hw/usb/trace-events
+++ b/hw/usb/trace-events
@@ -377,3 +377,10 @@ canokey_handle_data_out(uint8_t ep_out, uint32_t out_len) "ep %d len %d"
 canokey_handle_data_in(uint8_t ep_in, uint32_t in_len) "ep %d len %d"
 canokey_realize(void)
 canokey_unrealize(void)
+
+# aspeed-udc.c
+aspeed_udc_read(uint64_t offset, uint32_t value) "offset 0x%" PRIx64 " value 0x%x"
+aspeed_udc_write(uint64_t offset, uint32_t value) "offset 0x%" PRIx64 " value 0x%x"
+aspeed_udc_ep_read(int ep, uint64_t offset, uint32_t value) "ep %d, offset 0x%" PRIx64 " value 0x%x"
+aspeed_udc_ep_write(int ep, uint64_t offset, uint32_t value) "ep %d, offset 0x%" PRIx64 " value 0x%x"
+aspeed_udc_irq(uint32_t isr, uint32_t ier, int level) "isr 0x%x, ier 0x%x, level %d"
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 8+ messages in thread

* [PATCH v3 2/4] hw/usb/aspeed-udc: Add ASPEED UDC gadget USB device
  2026-08-31  9:21 [PATCH v3 0/4] hw/usb: Add ASPEED USB Device Controller (UDC) Jamin Lin
  2026-08-31  9:21 ` [PATCH v3 1/4] hw/usb/aspeed-udc: Add ASPEED UDC device controller Jamin Lin
@ 2026-08-31  9:21 ` Jamin Lin
  2026-08-31 10:04   ` Philippe Mathieu-Daudé
  2026-08-31  9:21 ` [PATCH v3 3/4] hw/usb/aspeed-udc: Add programmable endpoint DMA transfers Jamin Lin
  2026-08-31  9:21 ` [PATCH v3 4/4] hw/arm/aspeed_ast2600: Wire up the UDC Jamin Lin
  3 siblings, 1 reply; 8+ messages in thread
From: Jamin Lin @ 2026-08-31  9:21 UTC (permalink / raw)
  To: Paolo Bonzini, Peter Maydell, Cédric Le Goater, Steven Lee,
	Troy Lee, Kane Chen, Andrew Jeffery, Joel Stanley,
	open list:ARM TCG CPUs, open list:All patches CC here
  Cc: Jamin Lin, Troy Lee

Present the UDC gadget side to a USB host controller as a USB device
(TYPE_ASPEED_UDC_GADGET). This is a normal QEMU USB device, so it can be
attached to any USB host controller bus, not only the BMC's own EHCI. It
links back to its controller through the "udc" property.

This patch implements the control endpoint (EP0), which is enough for the
host to enumerate the gadget. Host control transfers are handled
asynchronously: the SETUP packet is mirrored into the SETUP data buffer,
the EP0 interrupt is raised and the host packet is parked (USB_RET_ASYNC).
The guest gadget driver then drives the data and status stages by writing
UDC_EP0_CTRL; that moves data to/from the driver's DMA buffer and completes
the parked packet back to the host.

SET_ADDRESS is the exception: it is applied synchronously, because the host
controller keeps the transfer bound to address 0 until it completes.

The gadget connects to / disconnects from the host bus when the driver
sets or clears the upstream-enable (pull-up) bit, and is detached on reset.

Signed-off-by: Jamin Lin <jamin_lin@aspeedtech.com>
---
 include/hw/usb/aspeed-udc.h |  28 +++
 hw/usb/aspeed-udc.c         | 382 +++++++++++++++++++++++++++++++++++-
 hw/usb/trace-events         |   5 +
 3 files changed, 412 insertions(+), 3 deletions(-)

diff --git a/include/hw/usb/aspeed-udc.h b/include/hw/usb/aspeed-udc.h
index 58fed5f9a2..ab9d016c61 100644
--- a/include/hw/usb/aspeed-udc.h
+++ b/include/hw/usb/aspeed-udc.h
@@ -10,11 +10,19 @@
 #define HW_USB_ASPEED_UDC_H
 
 #include "hw/core/sysbus.h"
+#include "hw/usb/usb.h"
 #include "qom/object.h"
 
 #define TYPE_ASPEED_UDC "aspeed.udc"
 OBJECT_DECLARE_SIMPLE_TYPE(AspeedUDCState, ASPEED_UDC)
 
+/*
+ * The gadget side of the controller is presented to a USB host controller's
+ * bus as a single USB device that delegates back to the AspeedUDCState.
+ */
+#define TYPE_ASPEED_UDC_GADGET "aspeed.udc-gadget"
+OBJECT_DECLARE_SIMPLE_TYPE(AspeedUDCGadget, ASPEED_UDC_GADGET)
+
 /*
  * Register map: root/global block at 0x000 - 0x087, then one 0x10 byte bank
  * per programmable endpoint from 0x200.
@@ -36,14 +44,34 @@ typedef struct AspeedUDCEP {
     int index;
 } AspeedUDCEP;
 
+struct AspeedUDCGadget {
+    USBDevice parent_obj;
+    AspeedUDCState *udc;
+};
+
 struct AspeedUDCState {
     SysBusDevice parent_obj;
 
     MemoryRegion udc_container;
     MemoryRegion root_mr;
+    MemoryRegion *dram_mr;
+    AddressSpace dram_as;
     uint32_t regs[ASPEED_UDC_ROOT_NR_REGS];
     AspeedUDCEP ep[ASPEED_UDC_NUM_EP];
     qemu_irq irq;
+
+    /* gadget USB device bound to this controller (set at its realize) */
+    AspeedUDCGadget *usbgadget;
+
+    /*
+     * In-flight EP0 control transfer (host side), deferred until the guest
+     * gadget driver responds via MMIO.
+     */
+    USBPacket *ep0_packet;
+    uint32_t ep0_setup_len;
+    uint32_t ep0_offset;
+    uint8_t *ep0_data;
+    bool ep0_dir_in;
 };
 
 #endif /* HW_USB_ASPEED_UDC_H */
diff --git a/hw/usb/aspeed-udc.c b/hw/usb/aspeed-udc.c
index cd773e979b..70a2296062 100644
--- a/hw/usb/aspeed-udc.c
+++ b/hw/usb/aspeed-udc.c
@@ -8,16 +8,26 @@
  * Models the ASPEED USB Device Controller (UDC). It implements one control
  * endpoint (EP0) and 4 programmable endpoints.
  *
- * This file is the system-bus side of the controller: the MMIO register map,
- * the interrupt and the soft reset. The gadget USB device presented to a host
- * controller (and the endpoint data path) is added on top of this.
+ * The model has two faces:
+ *   - a SysBus device exposing the MMIO register interface, the interrupt and
+ *     the integrated DMA engine to the guest gadget driver;
+ *   - a USBDevice presented on a host controller's bus, which forwards host
+ *     transactions to the guest gadget driver by raising the matching
+ *     controller interrupts and completes them once the guest gadget driver
+ *     responds via MMIO.
  */
 
 #include "qemu/osdep.h"
+#include "qemu/error-report.h"
+#include "qemu/log.h"
 #include "hw/core/irq.h"
 #include "hw/core/registerfields.h"
+#include "hw/core/qdev-properties.h"
 #include "hw/usb/aspeed-udc.h"
 #include "qemu/module.h"
+#include "qapi/error.h"
+#include "system/dma.h"
+#include "system/address-spaces.h"
 #include "trace.h"
 
 /* Root / Global registers (offset from the controller base) */
@@ -47,6 +57,7 @@ REG32(UDC_EP0_CTRL, 0x30)
     FIELD(UDC_EP0_CTRL, TX_RDY,             1, 1)
     FIELD(UDC_EP0_CTRL, STALL,              0, 1)
 REG32(UDC_EP0_DATA_BUFF, 0x34)
+    FIELD(UDC_EP0_DATA_BUFF, BASE_ADDR,     0, 31)
 /* EP0 SETUP packet buffer: SETUP0 = bytes 0...3, SETUP1 = bytes 4...7 */
 REG32(UDC_SETUP0, 0x80)
 REG32(UDC_SETUP1, 0x84)
@@ -66,6 +77,8 @@ REG32(EP_DMA_STS, 0x0C)
     FIELD(EP_DMA_STS, RPTR,                 8, 8)
     FIELD(EP_DMA_STS, WPTR,                 0, 8)
 
+#define ASPEED_UDC_EP0_MAXPKT      64
+
 static void aspeed_udc_update_irq(AspeedUDCState *s)
 {
     bool level;
@@ -78,6 +91,176 @@ static void aspeed_udc_update_irq(AspeedUDCState *s)
     qemu_set_irq(s->irq, level);
 }
 
+static void aspeed_udc_raise_isr(AspeedUDCState *s, uint32_t mask)
+{
+    s->regs[R_UDC_ISR] |= mask;
+    aspeed_udc_update_irq(s);
+}
+
+/*
+ * System bus device: MMIO register interface (guest gadget-driver facing)
+ */
+
+/* Connect/disconnect the gadget device from the host bus */
+static void aspeed_udc_set_pullup(AspeedUDCState *s, bool on)
+{
+    USBDevice *udev;
+    Error *err = NULL;
+
+    if (!s->usbgadget) {
+        /* no gadget device bound to this controller */
+        return;
+    }
+
+    udev = USB_DEVICE(s->usbgadget);
+    if (!udev->port) {
+        /* not attached to a host controller bus */
+        return;
+    }
+
+    trace_aspeed_udc_pullup(on, udev->attached);
+    if (on && !udev->attached) {
+        usb_device_attach(udev, &err);
+        if (err) {
+            warn_report_err(err);
+        }
+    } else if (!on && udev->attached) {
+        usb_device_detach(udev);
+    }
+}
+
+/* Complete the in-flight EP0 control transfer back to the host */
+static void aspeed_udc_ep0_complete(AspeedUDCState *s, uint32_t len)
+{
+    USBPacket *p = s->ep0_packet;
+
+    if (!p) {
+        return;
+    }
+
+    s->ep0_packet = NULL;
+    p->actual_length = s->ep0_dir_in ? MIN(len, s->ep0_setup_len)
+                                     : s->ep0_setup_len;
+    p->status = USB_RET_SUCCESS;
+    trace_aspeed_udc_ep0_complete(s->ep0_dir_in, p->actual_length);
+    usb_generic_async_ctrl_complete(USB_DEVICE(s->usbgadget), p);
+}
+
+static void aspeed_udc_ep0_tx_ready(AspeedUDCState *s, uint32_t val)
+{
+    uint32_t txlen = FIELD_EX32(val, UDC_EP0_CTRL, TX_LEN);
+    uint32_t data_buf_addr = s->regs[R_UDC_EP0_DATA_BUFF];
+    USBPacket *p;
+    uint32_t n;
+
+    if (!s->ep0_dir_in) {
+        /* Status stage IN (zero length) for an OUT / no-data transfer */
+        aspeed_udc_raise_isr(s, R_UDC_ISR_EP0_IN_ACK_MASK);
+        aspeed_udc_ep0_complete(s, s->ep0_offset);
+        return;
+    }
+    /* no control transfer is waiting: nothing to send */
+    if (!s->ep0_packet) {
+        return;
+    }
+
+    /* IN data stage: copy from the guest gadget driver's DMA buffer */
+    n = MIN(txlen, s->ep0_setup_len - s->ep0_offset);
+    if (n && address_space_read(&s->dram_as, data_buf_addr,
+                                MEMTXATTRS_UNSPECIFIED,
+                                s->ep0_data + s->ep0_offset,
+                                n) != MEMTX_OK) {
+        qemu_log_mask(LOG_GUEST_ERROR,
+                      "%s: EP0 IN DMA read failed\n", __func__);
+        p = s->ep0_packet;
+        s->ep0_packet = NULL;
+        p->status = USB_RET_IOERROR;
+        usb_generic_async_ctrl_complete(USB_DEVICE(s->usbgadget), p);
+        return;
+    }
+    s->ep0_offset += n;
+    aspeed_udc_raise_isr(s, R_UDC_ISR_EP0_IN_ACK_MASK);
+    if (txlen < ASPEED_UDC_EP0_MAXPKT || s->ep0_offset >= s->ep0_setup_len) {
+        aspeed_udc_ep0_complete(s, s->ep0_offset);
+    }
+}
+
+static void aspeed_udc_ep0_rx_ready(AspeedUDCState *s)
+{
+    uint32_t data_buf_addr = s->regs[R_UDC_EP0_DATA_BUFF];
+    USBPacket *p;
+    uint32_t n;
+
+    if (s->ep0_dir_in) {
+        /* Status stage OUT (zero length) for an IN transfer */
+        aspeed_udc_raise_isr(s, R_UDC_ISR_EP0_OUT_ACK_MASK);
+        return;
+    }
+    /* no control transfer is waiting: nothing to receive */
+    if (!s->ep0_packet) {
+        return;
+    }
+
+    /* OUT data stage: hand host data to the guest gadget driver */
+    n = MIN(s->ep0_setup_len - s->ep0_offset, ASPEED_UDC_EP0_MAXPKT);
+    if (n && address_space_write(&s->dram_as, data_buf_addr,
+                                 MEMTXATTRS_UNSPECIFIED,
+                                 s->ep0_data + s->ep0_offset,
+                                 n) != MEMTX_OK) {
+        qemu_log_mask(LOG_GUEST_ERROR,
+                      "%s: EP0 OUT DMA write failed\n", __func__);
+        p = s->ep0_packet;
+        s->ep0_packet = NULL;
+        p->status = USB_RET_IOERROR;
+        usb_generic_async_ctrl_complete(USB_DEVICE(s->usbgadget), p);
+        return;
+    }
+    s->ep0_offset += n;
+    s->regs[R_UDC_EP0_CTRL] = FIELD_DP32(s->regs[R_UDC_EP0_CTRL],
+                                         UDC_EP0_CTRL, RX_LEN, n);
+    aspeed_udc_raise_isr(s, R_UDC_ISR_EP0_OUT_ACK_MASK);
+}
+
+/*
+ * The guest gadget driver drives EP0 by writing UDC_EP0_CTRL. Translate
+ * those writes into data movement to/from the deferred host control packet
+ * plus the matching ACK interrupts the guest gadget driver expects.
+ */
+static void aspeed_udc_ep0_ctrl_write(AspeedUDCState *s, uint32_t val)
+{
+    USBPacket *p;
+
+    trace_aspeed_udc_ep0_ctrl_write(val, s->ep0_dir_in, s->ep0_offset);
+
+    if (val & R_UDC_EP0_CTRL_STALL_MASK) {
+        /* Gadget stalled EP0: fail the pending control transfer */
+        if (s->ep0_packet) {
+            p = s->ep0_packet;
+            s->ep0_packet = NULL;
+            p->status = USB_RET_STALL;
+            usb_generic_async_ctrl_complete(USB_DEVICE(s->usbgadget), p);
+        }
+    } else if (val & R_UDC_EP0_CTRL_TX_RDY_MASK) {
+        s->regs[R_UDC_EP0_CTRL] &= ~R_UDC_EP0_CTRL_TX_RDY_MASK;
+        aspeed_udc_ep0_tx_ready(s, val);
+    } else if (val & R_UDC_EP0_CTRL_RX_RDY_MASK) {
+        s->regs[R_UDC_EP0_CTRL] &= ~R_UDC_EP0_CTRL_RX_RDY_MASK;
+        aspeed_udc_ep0_rx_ready(s);
+    }
+}
+
+/* The upstream-enable bit connects/disconnects the gadget device */
+static void aspeed_udc_func_ctrl_write(AspeedUDCState *s, uint32_t val)
+{
+    bool was_on = FIELD_EX32(s->regs[R_UDC_FUNC_CTRL],
+                             UDC_FUNC_CTRL, UPSTREAM_EN);
+    bool now_on = FIELD_EX32(val, UDC_FUNC_CTRL, UPSTREAM_EN);
+
+    if (now_on != was_on) {
+        aspeed_udc_set_pullup(s, now_on);
+    }
+}
+
 static uint64_t aspeed_udc_read(void *opaque, hwaddr offset, unsigned size)
 {
     AspeedUDCState *s = ASPEED_UDC(opaque);
@@ -100,6 +283,11 @@ static void aspeed_udc_write(void *opaque, hwaddr offset, uint64_t data,
     trace_aspeed_udc_write(offset, val);
 
     switch (reg) {
+    case R_UDC_FUNC_CTRL:
+        val &= 0x000e1fff;
+        aspeed_udc_func_ctrl_write(s, val);
+        s->regs[R_UDC_FUNC_CTRL] = val;
+        break;
     case R_UDC_IER:
     case R_UDC_EP_ACK_IER:
     case R_UDC_EP_NAK_IER:
@@ -112,6 +300,16 @@ static void aspeed_udc_write(void *opaque, hwaddr offset, uint64_t data,
         s->regs[reg] &= ~val;
         aspeed_udc_update_irq(s);
         break;
+    case R_UDC_EP0_CTRL:
+        s->regs[reg] = val & (R_UDC_EP0_CTRL_STALL_MASK |
+                              R_UDC_EP0_CTRL_TX_RDY_MASK |
+                              R_UDC_EP0_CTRL_RX_RDY_MASK |
+                              R_UDC_EP0_CTRL_TX_LEN_MASK);
+        aspeed_udc_ep0_ctrl_write(s, val);
+        break;
+    case R_UDC_EP0_DATA_BUFF:
+        s->regs[reg] = val & R_UDC_EP0_DATA_BUFF_BASE_ADDR_MASK;
+        break;
     default:
         s->regs[reg] = val;
         break;
@@ -171,6 +369,7 @@ static const MemoryRegionOps aspeed_udc_ep_ops = {
 static void aspeed_udc_reset_hold(Object *obj, ResetType type)
 {
     AspeedUDCState *s = ASPEED_UDC(obj);
+    USBDevice *udev;
     int i;
 
     memset(s->regs, 0, sizeof(s->regs));
@@ -182,6 +381,21 @@ static void aspeed_udc_reset_hold(Object *obj, ResetType type)
     s->regs[R_UDC_DEV_RESET] = (R_UDC_DEV_RESET_ROOT_MASK |
                                 R_UDC_DEV_RESET_DMA_MASK |
                                 R_UDC_DEV_RESET_EP_POOL_MASK);
+    s->ep0_packet = NULL;
+
+    /*
+     * A guest reboot resets the controller but leaves the USB device
+     * attached to the host bus with no guest gadget driver behind it.
+     * Detach it, otherwise the rebooted host fails to re-enumerate the
+     * driverless gadget device; it re-attaches when the new driver asserts
+     * pull-up.
+     */
+    if (s->usbgadget) {
+        udev = USB_DEVICE(s->usbgadget);
+        if (udev->attached) {
+            usb_device_detach(udev);
+        }
+    }
 }
 
 static void aspeed_udc_realize(DeviceState *dev, Error **errp)
@@ -190,6 +404,12 @@ static void aspeed_udc_realize(DeviceState *dev, Error **errp)
     AspeedUDCState *s = ASPEED_UDC(dev);
     int i;
 
+    if (!s->dram_mr) {
+        error_setg(errp, TYPE_ASPEED_UDC ": 'dram' link not set");
+        return;
+    }
+    address_space_init(&s->dram_as, s->dram_mr, "dram");
+
     memory_region_init(&s->udc_container, OBJECT(s), TYPE_ASPEED_UDC,
                        ASPEED_UDC_MEM_SIZE);
     memory_region_init_io(&s->root_mr, OBJECT(s), &aspeed_udc_ops, s,
@@ -214,6 +434,20 @@ static void aspeed_udc_realize(DeviceState *dev, Error **errp)
     sysbus_init_irq(sbd, &s->irq);
 }
 
+static const Property aspeed_udc_properties[] = {
+    DEFINE_PROP_LINK("dram", AspeedUDCState, dram_mr,
+                     TYPE_MEMORY_REGION, MemoryRegion *),
+};
+
+static void aspeed_udc_init(Object *obj)
+{
+    AspeedUDCState *s = ASPEED_UDC(obj);
+
+    object_property_add_link(obj, "usbgadget", TYPE_ASPEED_UDC_GADGET,
+                             (Object **)&s->usbgadget,
+                             object_property_allow_set_link, 0);
+}
+
 static void aspeed_udc_class_init(ObjectClass *klass, const void *data)
 {
     DeviceClass *dc = DEVICE_CLASS(klass);
@@ -222,6 +456,141 @@ static void aspeed_udc_class_init(ObjectClass *klass, const void *data)
     dc->desc = "ASPEED USB Device Controller";
     dc->realize = aspeed_udc_realize;
     rc->phases.hold = aspeed_udc_reset_hold;
+    device_class_set_props(dc, aspeed_udc_properties);
+}
+
+/*
+ * USB device: gadget device presented on a host controller's bus
+ *
+ * These callbacks run in the context of the host controller. They translate
+ * host transactions into the controller interrupts/state the guest gadget
+ * driver expects, then defer (USB_RET_ASYNC) until the driver responds
+ * through the MMIO register interface above.
+ */
+
+static void aspeed_udc_gadget_handle_reset(USBDevice *udev)
+{
+    AspeedUDCState *s = ASPEED_UDC_GADGET(udev)->udc;
+
+    s->ep0_packet = NULL;
+    s->ep0_offset = 0;
+    /* The EHCI host is High-Speed; advertise it to the guest gadget driver */
+    s->regs[R_UDC_STS] = R_UDC_STS_HIGHSPEED_MASK;
+    trace_aspeed_udc_reset(s->regs[R_UDC_IER]);
+    aspeed_udc_raise_isr(s, R_UDC_ISR_BUS_RESET_MASK);
+}
+
+static void aspeed_udc_gadget_handle_control(USBDevice *udev, USBPacket *p,
+                                          int request, int value, int index,
+                                          int length, uint8_t *data)
+{
+    AspeedUDCState *s = ASPEED_UDC_GADGET(udev)->udc;
+    uint8_t req = request & 0xff;
+    uint8_t type = request >> 8;
+
+    /*
+     * Reconstruct the 8-byte SETUP packet into the SETUP data buffer where
+     * the guest gadget driver reads it from.
+     */
+    s->regs[R_UDC_SETUP0] = type | (req << 8) | ((value & 0xffff) << 16);
+    s->regs[R_UDC_SETUP1] = (index & 0xffff) | ((length & 0xffff) << 16);
+
+    /* A new SETUP clears the EP0 STALL condition */
+    s->regs[R_UDC_EP0_CTRL] &= ~R_UDC_EP0_CTRL_STALL_MASK;
+
+    s->ep0_packet = p;
+    s->ep0_data = data;
+    s->ep0_setup_len = length;
+    s->ep0_offset = 0;
+    s->ep0_dir_in = (type & USB_DIR_IN);
+
+    trace_aspeed_udc_ep0_setup(type, req, value, index, length,
+                               s->ep0_dir_in, udev->addr);
+
+    /*
+     * SET_ADDRESS is delivered while the device still answers at the default
+     * address 0 and carries the new address in wValue. The host controller
+     * keeps this transfer's queue bound to address 0 until it completes, so
+     * apply the new address synchronously as the transfer completes.
+     * Completing it asynchronously (USB_RET_ASYNC) would change udev->addr
+     * while the queue is still bound to 0; the host controller sees the
+     * mismatch, tears the queue down and enumeration breaks. The guest gadget
+     * driver is still notified so its state machine advances.
+     */
+    if (type == 0 && req == USB_REQ_SET_ADDRESS) {
+        udev->addr = value;
+        s->ep0_packet = NULL;
+        aspeed_udc_raise_isr(s, R_UDC_ISR_EP0_SETUP_MASK);
+        p->status = USB_RET_SUCCESS;
+        return;
+    }
+
+    aspeed_udc_raise_isr(s, R_UDC_ISR_EP0_SETUP_MASK);
+    p->status = USB_RET_ASYNC;
+}
+
+static void aspeed_udc_gadget_handle_data(USBDevice *udev, USBPacket *p)
+{
+    /* Programmable endpoint (bulk) transfers are added in a later patch. */
+    p->status = USB_RET_STALL;
+}
+
+static void aspeed_udc_gadget_cancel_packet(USBDevice *udev, USBPacket *p)
+{
+    AspeedUDCState *s = ASPEED_UDC_GADGET(udev)->udc;
+
+    if (s->ep0_packet == p) {
+        s->ep0_packet = NULL;
+    }
+}
+
+static void aspeed_udc_gadget_realize(USBDevice *udev, Error **errp)
+{
+    AspeedUDCGadget *dev = ASPEED_UDC_GADGET(udev);
+
+    if (!dev->udc) {
+        error_setg(errp, TYPE_ASPEED_UDC_GADGET ": 'udc' link is not set");
+        return;
+    }
+    /* Bind this gadget to its controller through the link property */
+    object_property_set_link(OBJECT(dev->udc), "usbgadget", OBJECT(dev),
+                             &error_abort);
+
+    udev->auto_attach = 0;
+    /* The ASPEED UDC is USB 2.0, so it only runs at High-Speed for now */
+    udev->speed = USB_SPEED_HIGH;
+    udev->speedmask = USB_SPEED_MASK_HIGH;
+}
+
+static void aspeed_udc_gadget_unrealize(USBDevice *udev)
+{
+    AspeedUDCGadget *dev = ASPEED_UDC_GADGET(udev);
+
+    if (dev->udc && dev->udc->usbgadget == dev) {
+        object_property_set_link(OBJECT(dev->udc), "usbgadget", NULL,
+                                 &error_abort);
+    }
+}
+
+static const Property aspeed_udc_gadget_props[] = {
+    DEFINE_PROP_LINK("udc", AspeedUDCGadget, udc, TYPE_ASPEED_UDC,
+                     AspeedUDCState *),
+};
+
+static void aspeed_udc_gadget_class_init(ObjectClass *klass, const void *data)
+{
+    DeviceClass *dc = DEVICE_CLASS(klass);
+    USBDeviceClass *uc = USB_DEVICE_CLASS(klass);
+
+    dc->desc           = "ASPEED UDC gadget device";
+    uc->product_desc   = "ASPEED UDC gadget";
+    uc->realize        = aspeed_udc_gadget_realize;
+    uc->unrealize      = aspeed_udc_gadget_unrealize;
+    uc->handle_reset   = aspeed_udc_gadget_handle_reset;
+    uc->handle_control = aspeed_udc_gadget_handle_control;
+    uc->handle_data    = aspeed_udc_gadget_handle_data;
+    uc->cancel_packet  = aspeed_udc_gadget_cancel_packet;
+    device_class_set_props(dc, aspeed_udc_gadget_props);
 }
 
 static const TypeInfo aspeed_udc_types[] = {
@@ -229,8 +598,15 @@ static const TypeInfo aspeed_udc_types[] = {
         .name          = TYPE_ASPEED_UDC,
         .parent        = TYPE_SYS_BUS_DEVICE,
         .instance_size = sizeof(AspeedUDCState),
+        .instance_init = aspeed_udc_init,
         .class_init    = aspeed_udc_class_init,
     },
+    {
+        .name          = TYPE_ASPEED_UDC_GADGET,
+        .parent        = TYPE_USB_DEVICE,
+        .instance_size = sizeof(AspeedUDCGadget),
+        .class_init    = aspeed_udc_gadget_class_init,
+    },
 };
 
 DEFINE_TYPES(aspeed_udc_types)
diff --git a/hw/usb/trace-events b/hw/usb/trace-events
index ed05304520..098c3d6179 100644
--- a/hw/usb/trace-events
+++ b/hw/usb/trace-events
@@ -383,4 +383,9 @@ aspeed_udc_read(uint64_t offset, uint32_t value) "offset 0x%" PRIx64 " value 0x%
 aspeed_udc_write(uint64_t offset, uint32_t value) "offset 0x%" PRIx64 " value 0x%x"
 aspeed_udc_ep_read(int ep, uint64_t offset, uint32_t value) "ep %d, offset 0x%" PRIx64 " value 0x%x"
 aspeed_udc_ep_write(int ep, uint64_t offset, uint32_t value) "ep %d, offset 0x%" PRIx64 " value 0x%x"
+aspeed_udc_pullup(int on, int attached) "on %d, attached %d"
 aspeed_udc_irq(uint32_t isr, uint32_t ier, int level) "isr 0x%x, ier 0x%x, level %d"
+aspeed_udc_reset(uint32_t ier) "bus reset, ier 0x%x"
+aspeed_udc_ep0_setup(uint8_t type, uint8_t req, uint16_t value, uint16_t index, uint16_t length, int dir_in, int addr) "bmRequestType 0x%02x, bRequest 0x%02x, wValue 0x%04x, wIndex 0x%04x, wLength %d, dir_in %d, addr %d"
+aspeed_udc_ep0_ctrl_write(uint32_t val, int dir_in, uint32_t offset) "val 0x%x, dir_in %d, off %u"
+aspeed_udc_ep0_complete(int dir_in, int actual) "dir_in %d, actual %d"
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 8+ messages in thread

* [PATCH v3 3/4] hw/usb/aspeed-udc: Add programmable endpoint DMA transfers
  2026-08-31  9:21 [PATCH v3 0/4] hw/usb: Add ASPEED USB Device Controller (UDC) Jamin Lin
  2026-08-31  9:21 ` [PATCH v3 1/4] hw/usb/aspeed-udc: Add ASPEED UDC device controller Jamin Lin
  2026-08-31  9:21 ` [PATCH v3 2/4] hw/usb/aspeed-udc: Add ASPEED UDC gadget USB device Jamin Lin
@ 2026-08-31  9:21 ` Jamin Lin
  2026-08-31  9:21 ` [PATCH v3 4/4] hw/arm/aspeed_ast2600: Wire up the UDC Jamin Lin
  3 siblings, 0 replies; 8+ messages in thread
From: Jamin Lin @ 2026-08-31  9:21 UTC (permalink / raw)
  To: Paolo Bonzini, Peter Maydell, Cédric Le Goater, Steven Lee,
	Troy Lee, Kane Chen, Andrew Jeffery, Joel Stanley,
	open list:ARM TCG CPUs, open list:All patches CC here
  Cc: Jamin Lin, Troy Lee

Add the bulk data plane for the four programmable endpoints. The gadget
driver queues IN data through the descriptor-list DMA ring and arms OUT
buffers through the single-stage DMA registers; host bulk transactions
are served from / delivered into those.

The DMA mode is taken from EP_DMA_CTRL.DESC_OP_EN: IN endpoints use the
descriptor-list ring, OUT endpoints use single-stage buffers.

A transfer larger than one host packet is served across several polls,
with the host packet parked (USB_RET_ASYNC) until the gadget queues (IN)
or arms (OUT) more data, then completed from the matching DMA kick.

With this the gadget data endpoints work, e.g. a mass-storage gadget can
be enumerated and read/written end to end.

Signed-off-by: Jamin Lin <jamin_lin@aspeedtech.com>
---
 include/hw/usb/aspeed-udc.h |   8 +
 hw/usb/aspeed-udc.c         | 457 +++++++++++++++++++++++++++++++++++-
 hw/usb/trace-events         |   4 +
 3 files changed, 464 insertions(+), 5 deletions(-)

diff --git a/include/hw/usb/aspeed-udc.h b/include/hw/usb/aspeed-udc.h
index ab9d016c61..7701c1aa34 100644
--- a/include/hw/usb/aspeed-udc.h
+++ b/include/hw/usb/aspeed-udc.h
@@ -42,6 +42,14 @@ typedef struct AspeedUDCEP {
     MemoryRegion mr;
     uint32_t regs[ASPEED_UDC_EP_NR_REGS];
     int index;
+
+    /*
+     * host packet parked until the guest gadget driver queues (IN) or
+     * arms (OUT) data
+     */
+    USBPacket *pkt;
+    /* bytes of the current IN descriptor already served */
+    uint32_t desc_off;
 } AspeedUDCEP;
 
 struct AspeedUDCGadget {
diff --git a/hw/usb/aspeed-udc.c b/hw/usb/aspeed-udc.c
index 70a2296062..a6eaf6f97b 100644
--- a/hw/usb/aspeed-udc.c
+++ b/hw/usb/aspeed-udc.c
@@ -72,12 +72,37 @@ REG32(EP_DMA_CTRL, 0x04)
     FIELD(EP_DMA_CTRL, PROC_STS,            4, 4)
     FIELD(EP_DMA_CTRL, DESC_OP_EN,          0, 1)
 REG32(EP_DMA_BUFF, 0x08)
+    FIELD(EP_DMA_BUFF, BASE_ADDR,           0, 31)
 REG32(EP_DMA_STS, 0x0C)
     FIELD(EP_DMA_STS, PKT_SIZE,            16, 11)
     FIELD(EP_DMA_STS, RPTR,                 8, 8)
     FIELD(EP_DMA_STS, WPTR,                 0, 8)
 
-#define ASPEED_UDC_EP0_MAXPKT      64
+#define ASPEED_UDC_EP0_MAXPKT   64
+#define ASPEED_UDC_EP_MAXPKT    1024
+
+/* DMA descriptor ring (256-stage mode) and descriptor data limits */
+#define ASPEED_UDC_DESCS_COUNT  256
+#define ASPEED_UDC_DESC_MAX_LEN 4096
+
+/* DMA processing-status idle codes */
+#define EP_DMA_CTRL_STS_RX_IDLE 0x0
+#define EP_DMA_CTRL_STS_TX_IDLE 0x8
+
+/* DMA descriptor (DES1) fields, in guest memory */
+#define ASPEED_EP_DESC1_IN_LEN(ctrl)   ((ctrl) & 0x1fff)
+/* interrupt-on-completion */
+#define ASPEED_EP_DESC1_INTR           BIT(31)
+
+/* Result of moving a host data packet through an endpoint's DMA */
+typedef enum {
+    /* whole packet transferred */
+    ASPEED_UDC_XFER_DONE,
+    /* not finished, keep parked */
+    ASPEED_UDC_XFER_MORE,
+    /* DMA failed */
+    ASPEED_UDC_XFER_ERROR,
+} AspeedUDCXferResult;
 
 static void aspeed_udc_update_irq(AspeedUDCState *s)
 {
@@ -97,6 +122,14 @@ static void aspeed_udc_raise_isr(AspeedUDCState *s, uint32_t mask)
     aspeed_udc_update_irq(s);
 }
 
+static void aspeed_udc_raise_ep_ack(AspeedUDCState *s, int ep)
+{
+    trace_aspeed_udc_ep_ack(ep);
+    s->regs[R_UDC_EP_ACK_ISR] |= BIT(ep);
+    s->regs[R_UDC_ISR] |= R_UDC_ISR_EP_POOL_ACK_MASK;
+    aspeed_udc_update_irq(s);
+}
+
 /*
  * System bus device: MMIO register interface (guest gadget-driver facing)
  */
@@ -330,6 +363,288 @@ static const MemoryRegionOps aspeed_udc_ops = {
     },
 };
 
+/*
+ * Copy len bytes from guest memory at addr into the IN packet, going through
+ * a bounce buffer one buf-full at a time. Returns false on DMA failure.
+ */
+static bool aspeed_udc_ep_copy_to_pkt(AspeedUDCState *s, int ep, uint32_t addr,
+                                      uint32_t len, USBPacket *p)
+{
+    uint8_t buf[ASPEED_UDC_EP_MAXPKT];
+    uint32_t copied = 0;
+    uint32_t seg;
+
+    while (copied < len) {
+        seg = MIN(len - copied, sizeof(buf));
+        if (address_space_read(&s->dram_as, addr + copied,
+                               MEMTXATTRS_UNSPECIFIED, buf, seg) != MEMTX_OK) {
+            qemu_log_mask(LOG_GUEST_ERROR,
+                          "%s: ep %d IN data DMA read failed\n", __func__,
+                          ep);
+            return false;
+        }
+        usb_packet_copy(p, buf, seg);
+        copied += seg;
+    }
+
+    return true;
+}
+
+/*
+ * IN transfer: send data to the host by filling its IN packet from the
+ * buffers the guest gadget driver queued in the descriptor ring (from the
+ * read pointer to the write pointer).
+ *
+ * One host packet can be bigger than one descriptor's buffer, so we copy from
+ * several descriptors in a row until the packet is full or the ring is empty.
+ * If a descriptor is too big for the space left in the packet, we copy only
+ * part of it now and copy the rest on the next call; desc_off remembers how
+ * far we got. We move the read pointer to the next descriptor only after a
+ * descriptor is fully copied, so the guest gadget driver can read the pointer
+ * and see how much was sent.
+ *
+ * This function raises the endpoint ACK by itself when the ring becomes empty
+ * or when a descriptor asks for an interrupt.
+ */
+static AspeedUDCXferResult aspeed_udc_ep_xfer_in(AspeedUDCState *s, int ep,
+                                                 USBPacket *p)
+{
+    QEMUIOVector *pktiov = p->combined ? &p->combined->iov : &p->iov;
+    AspeedUDCEP *e = &s->ep[ep];
+    uint32_t mps = FIELD_EX32(e->regs[R_EP_CONFIG], EP_CONFIG, MAX_PKT);
+    uint32_t wptr = FIELD_EX32(e->regs[R_EP_DMA_STS], EP_DMA_STS, WPTR);
+    uint32_t rptr = FIELD_EX32(e->regs[R_EP_DMA_STS], EP_DMA_STS, RPTR);
+    uint32_t desc_base = e->regs[R_EP_DMA_BUFF];
+    uint32_t desc_addr;
+    uint32_t remaining;
+    uint32_t desc_ctrl;
+    uint32_t pkt_space;
+    /* des_0: data buffer base address, des_1: control/status */
+    uint32_t desc[2];
+    uint32_t offset;
+    uint32_t chunk;
+    uint32_t dlen;
+    bool done = false;
+    bool ack = false;
+
+    if (mps == 0) {
+        /* a MAX_PKT field of 0 means the maximum packet size */
+        mps = ASPEED_UDC_EP_MAXPKT;
+    }
+
+    trace_aspeed_udc_ep_data_in(ep, rptr, wptr, pktiov->size);
+
+    /* walk the queued descriptors, filling the packet */
+    while (rptr != wptr) {
+        if (address_space_read(&s->dram_as, desc_base + rptr * sizeof(desc),
+                               MEMTXATTRS_UNSPECIFIED, desc,
+                               sizeof(desc)) != MEMTX_OK) {
+            qemu_log_mask(LOG_GUEST_ERROR,
+                          "%s: ep %d descriptor DMA read failed\n",
+                          __func__, ep);
+            return ASPEED_UDC_XFER_ERROR;
+        }
+        desc_addr = le32_to_cpu(desc[0]) & R_EP_DMA_BUFF_BASE_ADDR_MASK;
+        desc_ctrl = le32_to_cpu(desc[1]);
+        dlen = ASPEED_EP_DESC1_IN_LEN(desc_ctrl);
+        offset = e->desc_off;
+        /* how much to copy: min(descriptor bytes left, packet space left) */
+        remaining = dlen > offset ? dlen - offset : 0;
+        pkt_space = pktiov->size > (uint32_t)p->actual_length ?
+                    pktiov->size - (uint32_t)p->actual_length : 0;
+        chunk = MIN(remaining, pkt_space);
+
+        if (!aspeed_udc_ep_copy_to_pkt(s, ep, desc_addr + offset, chunk, p)) {
+            return ASPEED_UDC_XFER_ERROR;
+        }
+        e->desc_off += chunk;
+
+        if (e->desc_off < dlen) {
+            /*
+             * The packet ran out of space in the middle of this descriptor,
+             * so only part of it was copied. Stop here, and leave the read
+             * pointer on this descriptor: the next call resumes copying the
+             * rest (desc_off remembers how far we got).
+             */
+            done = true;
+            break;
+        }
+
+        /*
+         * This descriptor was copied in full. Advance the read pointer to the
+         * next descriptor and reset desc_off so it starts from the beginning.
+         */
+        rptr = (rptr + 1) % ASPEED_UDC_DESCS_COUNT;
+        e->desc_off = 0;
+        if (desc_ctrl & ASPEED_EP_DESC1_INTR) {
+            ack = true;
+        }
+        /*
+         * This descriptor is shorter than the max packet size, i.e. a short
+         * (or zero-length) packet. In USB that marks the end of the transfer,
+         * so stop here.
+         */
+        if (dlen < mps) {
+            done = true;
+            break;
+        }
+        /*
+         * The packet is now completely full, so the host has received all the
+         * data it asked for. Stop here.
+         */
+        if ((uint32_t)p->actual_length >= pktiov->size) {
+            done = true;
+            break;
+        }
+    }
+
+    e->regs[R_EP_DMA_STS] = FIELD_DP32(e->regs[R_EP_DMA_STS], EP_DMA_STS,
+                                       RPTR, rptr);
+    e->regs[R_EP_DMA_CTRL] = FIELD_DP32(e->regs[R_EP_DMA_CTRL], EP_DMA_CTRL,
+                                        PROC_STS, EP_DMA_CTRL_STS_TX_IDLE);
+    /* The guest gadget driver completes its request when the ring drains */
+    if (rptr == wptr) {
+        ack = true;
+    }
+    if (ack) {
+        aspeed_udc_raise_ep_ack(s, ep);
+    }
+
+    return done ? ASPEED_UDC_XFER_DONE : ASPEED_UDC_XFER_MORE;
+}
+
+/*
+ * OUT transfer: receive data from the host by copying its OUT packet into the
+ * buffer the guest gadget driver set up (single-stage mode).
+ *
+ * A host packet can be bigger than one buffer, so we copy at most PKT_SIZE
+ * bytes per call, continuing from where the last call stopped
+ * (p->actual_length). The caller keeps the packet parked until it is fully
+ * copied.
+ */
+static AspeedUDCXferResult aspeed_udc_ep_xfer_out(AspeedUDCState *s, int ep,
+                                                  USBPacket *p)
+{
+    AspeedUDCEP *e = &s->ep[ep];
+    uint32_t chunk = FIELD_EX32(e->regs[R_EP_DMA_STS], EP_DMA_STS, PKT_SIZE);
+    uint32_t remaining = p->iov.size - (uint32_t)p->actual_length;
+    uint32_t data_buf_addr = e->regs[R_EP_DMA_BUFF];
+    uint32_t len = MIN(remaining, chunk);
+    uint8_t buf[ASPEED_UDC_DESC_MAX_LEN];
+
+    if (data_buf_addr && len) {
+        len = MIN(len, sizeof(buf));
+        usb_packet_copy(p, buf, len);
+        if (address_space_write(&s->dram_as, data_buf_addr,
+                                MEMTXATTRS_UNSPECIFIED, buf,
+                                len) != MEMTX_OK) {
+            qemu_log_mask(LOG_GUEST_ERROR,
+                          "%s: ep %d OUT data DMA write failed\n",
+                          __func__, ep);
+            return ASPEED_UDC_XFER_ERROR;
+        }
+    }
+
+    e->regs[R_EP_DMA_STS] = FIELD_DP32(e->regs[R_EP_DMA_STS],
+                                       EP_DMA_STS, PKT_SIZE, len);
+    e->regs[R_EP_DMA_STS] = FIELD_DP32(e->regs[R_EP_DMA_STS],
+                                       EP_DMA_STS, WPTR, 0);
+    e->regs[R_EP_DMA_CTRL] = FIELD_DP32(e->regs[R_EP_DMA_CTRL], EP_DMA_CTRL,
+                                        PROC_STS, EP_DMA_CTRL_STS_RX_IDLE);
+    aspeed_udc_raise_ep_ack(s, ep);
+
+    if ((uint32_t)p->actual_length >= p->iov.size) {
+        return ASPEED_UDC_XFER_DONE;
+    }
+
+    return ASPEED_UDC_XFER_MORE;
+}
+
+/*
+ * IN kick: the guest gadget driver wrote EP_DMA_STS to tell us it queued more
+ * IN data to send to the host. If a host IN request is already waiting
+ * (parked because there was no data before), send the data now and finish it.
+ * If the request needs more data than was queued, keep it parked and wait for
+ * the next kick.
+ */
+static void aspeed_udc_ep_in_kick(AspeedUDCState *s, int ep, uint32_t val)
+{
+    AspeedUDCEP *e = &s->ep[ep];
+    uint32_t cur_rptr = FIELD_EX32(e->regs[R_EP_DMA_STS], EP_DMA_STS, RPTR);
+    uint32_t new_rptr = FIELD_EX32(val, EP_DMA_STS, RPTR);
+    uint32_t new_wptr = FIELD_EX32(val, EP_DMA_STS, WPTR);
+    USBPacket *p = e->pkt;
+
+    /*
+     * A normal kick only sets the write pointer and leaves the read-pointer
+     * field 0 (the read pointer is ours to advance). The guest resets the ring
+     * by writing a read pointer that is non-zero and equal to the write
+     * pointer.
+     *
+     * We check non-zero as well as equal: on a normal kick whose write pointer
+     * just wrapped back to 0, both fields would be 0, so an "equal" test alone
+     * would look like a reset by mistake.
+     */
+    if (new_rptr != 0 && new_rptr == new_wptr) {
+        cur_rptr = new_rptr;
+        e->desc_off = 0;
+    }
+    /* store the guest's write, but keep our own read pointer */
+    e->regs[R_EP_DMA_STS] = FIELD_DP32(val, EP_DMA_STS, RPTR, cur_rptr);
+
+    /* nothing to do unless an IN packet is waiting and the ring has data */
+    if (!p || cur_rptr == new_wptr) {
+        return;
+    }
+
+    switch (aspeed_udc_ep_xfer_in(s, ep, p)) {
+    case ASPEED_UDC_XFER_DONE:
+        e->pkt = NULL;
+        p->status = USB_RET_SUCCESS;
+        usb_packet_complete(USB_DEVICE(s->usbgadget), p);
+        break;
+    case ASPEED_UDC_XFER_ERROR:
+        e->pkt = NULL;
+        p->status = USB_RET_IOERROR;
+        usb_packet_complete(USB_DEVICE(s->usbgadget), p);
+        break;
+    case ASPEED_UDC_XFER_MORE:
+        break;
+    }
+}
+
+/*
+ * OUT kick: the guest gadget driver wrote EP_DMA_STS to give us a buffer for
+ * OUT data. If an OUT packet is already waiting (parked because there was no
+ * buffer before), copy its data into the buffer now and finish it. If the
+ * packet has more data than fits, keep it parked and wait for the next buffer.
+ */
+static void aspeed_udc_ep_out_kick(AspeedUDCState *s, int ep)
+{
+    AspeedUDCEP *e = &s->ep[ep];
+    USBPacket *p = e->pkt;
+
+    /* nothing to do unless an OUT packet is waiting and a buffer is ready */
+    if (!p || !FIELD_EX32(e->regs[R_EP_DMA_STS], EP_DMA_STS, WPTR)) {
+        return;
+    }
+
+    switch (aspeed_udc_ep_xfer_out(s, ep, p)) {
+    case ASPEED_UDC_XFER_DONE:
+        e->pkt = NULL;
+        p->status = USB_RET_SUCCESS;
+        usb_packet_complete(USB_DEVICE(s->usbgadget), p);
+        break;
+    case ASPEED_UDC_XFER_ERROR:
+        e->pkt = NULL;
+        p->status = USB_RET_IOERROR;
+        usb_packet_complete(USB_DEVICE(s->usbgadget), p);
+        break;
+    case ASPEED_UDC_XFER_MORE:
+        break;
+    }
+}
+
 static uint64_t aspeed_udc_ep_read(void *opaque, hwaddr offset, unsigned size)
 {
     AspeedUDCEP *e = opaque;
@@ -346,10 +661,31 @@ static void aspeed_udc_ep_write(void *opaque, hwaddr offset, uint64_t data,
                                 unsigned size)
 {
     AspeedUDCEP *e = opaque;
+    AspeedUDCState *s = container_of(e - e->index, AspeedUDCState, ep[0]);
     uint32_t reg = offset >> 2;
+    uint32_t val = data;
 
-    trace_aspeed_udc_ep_write(e->index, offset, data);
-    e->regs[reg] = data;
+    trace_aspeed_udc_ep_write(e->index, offset, val);
+
+    switch (reg) {
+    case R_EP_DMA_BUFF:
+        e->regs[reg] = val & R_EP_DMA_BUFF_BASE_ADDR_MASK;
+        break;
+    case R_EP_DMA_STS:
+        val &= 0x77ffffff;
+        if (FIELD_EX32(e->regs[R_EP_DMA_CTRL], EP_DMA_CTRL, DESC_OP_EN)) {
+            /* IN, descriptor-list mode */
+            aspeed_udc_ep_in_kick(s, e->index, val);
+        } else {
+            /* OUT, single-stage mode */
+            e->regs[reg] = val;
+            aspeed_udc_ep_out_kick(s, e->index);
+        }
+        break;
+    default:
+        e->regs[reg] = val;
+        break;
+    }
 }
 
 static const MemoryRegionOps aspeed_udc_ep_ops = {
@@ -375,6 +711,8 @@ static void aspeed_udc_reset_hold(Object *obj, ResetType type)
     memset(s->regs, 0, sizeof(s->regs));
     for (i = 0; i < ASPEED_UDC_NUM_EP; i++) {
         memset(s->ep[i].regs, 0, sizeof(s->ep[i].regs));
+        s->ep[i].pkt = NULL;
+        s->ep[i].desc_off = 0;
     }
 
     /* Device-reset default: root, DMA and EP-pool soft-reset bits set */
@@ -468,6 +806,95 @@ static void aspeed_udc_class_init(ObjectClass *klass, const void *data)
  * through the MMIO register interface above.
  */
 
+static int aspeed_udc_find_ep(AspeedUDCState *s, int ep_nr, bool is_out)
+{
+    uint32_t cfg;
+    int i;
+
+    for (i = 0; i < ASPEED_UDC_NUM_EP; i++) {
+        cfg = s->ep[i].regs[R_EP_CONFIG];
+
+        if (!FIELD_EX32(cfg, EP_CONFIG, ENABLE) ||
+            FIELD_EX32(cfg, EP_CONFIG, EP_NUM) != ep_nr) {
+            continue;
+        }
+        if (FIELD_EX32(cfg, EP_CONFIG, DIR_OUT) == is_out) {
+            return i;
+        }
+    }
+
+    return -1;
+}
+
+static void aspeed_udc_ep_data_in(AspeedUDCState *s, int ep, USBPacket *p)
+{
+    AspeedUDCEP *e = &s->ep[ep];
+    uint32_t rptr = FIELD_EX32(e->regs[R_EP_DMA_STS], EP_DMA_STS, RPTR);
+    uint32_t wptr = FIELD_EX32(e->regs[R_EP_DMA_STS], EP_DMA_STS, WPTR);
+
+    if (rptr == wptr) {
+        /*
+         * No IN data is queued yet. Save the packet and return ASYNC
+         * instead of NAK. A NAK would make the host retry slowly.
+         * aspeed_udc_ep_in_kick() serves and completes this packet later,
+         * once the guest gadget driver queues descriptors.
+         */
+        e->pkt = p;
+        p->status = USB_RET_ASYNC;
+        return;
+    }
+
+    switch (aspeed_udc_ep_xfer_in(s, ep, p)) {
+    case ASPEED_UDC_XFER_DONE:
+        p->status = USB_RET_SUCCESS;
+        break;
+    case ASPEED_UDC_XFER_MORE:
+        /* not fully sent yet: save the packet, wait for more descriptors */
+        e->pkt = p;
+        p->status = USB_RET_ASYNC;
+        break;
+    case ASPEED_UDC_XFER_ERROR:
+        p->status = USB_RET_IOERROR;
+        break;
+    }
+}
+
+static void aspeed_udc_ep_data_out(AspeedUDCState *s, int ep, USBPacket *p)
+{
+    AspeedUDCEP *e = &s->ep[ep];
+    uint32_t sts = e->regs[R_EP_DMA_STS];
+
+    trace_aspeed_udc_ep_data_out(ep, FIELD_EX32(sts, EP_DMA_STS, WPTR),
+                                 FIELD_EX32(sts, EP_DMA_STS, PKT_SIZE),
+                                 p->iov.size);
+    if (!FIELD_EX32(sts, EP_DMA_STS, WPTR)) {
+        /*
+         * No OUT buffer is ready yet. Save the packet and return ASYNC
+         * instead of NAK. Writing now could use an old buffer address and
+         * lose the data (for example a mass-storage CBW). A NAK would make
+         * the host retry slowly. aspeed_udc_ep_out_kick() delivers this
+         * packet later, once the guest gadget driver sets up a buffer.
+         */
+        e->pkt = p;
+        p->status = USB_RET_ASYNC;
+        return;
+    }
+
+    switch (aspeed_udc_ep_xfer_out(s, ep, p)) {
+    case ASPEED_UDC_XFER_DONE:
+        p->status = USB_RET_SUCCESS;
+        break;
+    case ASPEED_UDC_XFER_MORE:
+        /* not fully received yet: save the packet, wait for the next buffer */
+        e->pkt = p;
+        p->status = USB_RET_ASYNC;
+        break;
+    case ASPEED_UDC_XFER_ERROR:
+        p->status = USB_RET_IOERROR;
+        break;
+    }
+}
+
 static void aspeed_udc_gadget_handle_reset(USBDevice *udev)
 {
     AspeedUDCState *s = ASPEED_UDC_GADGET(udev)->udc;
@@ -531,17 +958,37 @@ static void aspeed_udc_gadget_handle_control(USBDevice *udev, USBPacket *p,
 
 static void aspeed_udc_gadget_handle_data(USBDevice *udev, USBPacket *p)
 {
-    /* Programmable endpoint (bulk) transfers are added in a later patch. */
-    p->status = USB_RET_STALL;
+    AspeedUDCState *s = ASPEED_UDC_GADGET(udev)->udc;
+    bool is_out = (p->pid == USB_TOKEN_OUT);
+    int ep = aspeed_udc_find_ep(s, p->ep->nr, is_out);
+
+    trace_aspeed_udc_handle_data(p->ep->nr, is_out ? "OUT" : "IN",
+                                 p->iov.size, ep);
+    if (ep < 0) {
+        p->status = USB_RET_STALL;
+        return;
+    }
+
+    if (is_out) {
+        aspeed_udc_ep_data_out(s, ep, p);
+    } else {
+        aspeed_udc_ep_data_in(s, ep, p);
+    }
 }
 
 static void aspeed_udc_gadget_cancel_packet(USBDevice *udev, USBPacket *p)
 {
     AspeedUDCState *s = ASPEED_UDC_GADGET(udev)->udc;
+    int i;
 
     if (s->ep0_packet == p) {
         s->ep0_packet = NULL;
     }
+    for (i = 0; i < ASPEED_UDC_NUM_EP; i++) {
+        if (s->ep[i].pkt == p) {
+            s->ep[i].pkt = NULL;
+        }
+    }
 }
 
 static void aspeed_udc_gadget_realize(USBDevice *udev, Error **errp)
diff --git a/hw/usb/trace-events b/hw/usb/trace-events
index 098c3d6179..80ead23358 100644
--- a/hw/usb/trace-events
+++ b/hw/usb/trace-events
@@ -389,3 +389,7 @@ aspeed_udc_reset(uint32_t ier) "bus reset, ier 0x%x"
 aspeed_udc_ep0_setup(uint8_t type, uint8_t req, uint16_t value, uint16_t index, uint16_t length, int dir_in, int addr) "bmRequestType 0x%02x, bRequest 0x%02x, wValue 0x%04x, wIndex 0x%04x, wLength %d, dir_in %d, addr %d"
 aspeed_udc_ep0_ctrl_write(uint32_t val, int dir_in, uint32_t offset) "val 0x%x, dir_in %d, off %u"
 aspeed_udc_ep0_complete(int dir_in, int actual) "dir_in %d, actual %d"
+aspeed_udc_handle_data(int ep_nr, const char *dir, uint32_t iov, int ep_idx) "ep_nr %d, %s, iov %u, ep_idx %d"
+aspeed_udc_ep_data_in(unsigned ep, uint32_t rptr, uint32_t wptr, uint32_t iov) "ep %u, rptr %u, wptr %u, iov %u"
+aspeed_udc_ep_data_out(unsigned ep, uint32_t wptr, uint32_t avail, uint32_t iov) "ep %u, wptr %u, avail %u, iov %u"
+aspeed_udc_ep_ack(unsigned ep) "ep %u"
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 8+ messages in thread

* [PATCH v3 4/4] hw/arm/aspeed_ast2600: Wire up the UDC
  2026-08-31  9:21 [PATCH v3 0/4] hw/usb: Add ASPEED USB Device Controller (UDC) Jamin Lin
                   ` (2 preceding siblings ...)
  2026-08-31  9:21 ` [PATCH v3 3/4] hw/usb/aspeed-udc: Add programmable endpoint DMA transfers Jamin Lin
@ 2026-08-31  9:21 ` Jamin Lin
  3 siblings, 0 replies; 8+ messages in thread
From: Jamin Lin @ 2026-08-31  9:21 UTC (permalink / raw)
  To: Paolo Bonzini, Peter Maydell, Cédric Le Goater, Steven Lee,
	Troy Lee, Kane Chen, Andrew Jeffery, Joel Stanley,
	open list:ARM TCG CPUs, open list:All patches CC here
  Cc: Jamin Lin, Troy Lee, Philippe Mathieu-Daudé

Create the USB Device Controller (UDC) at 0x1e6a2000 on the AST2600 SoC
and map its registers.

The UDC and the second EHCI host controller (EHCI2) share one SoC
interrupt line. Route both through an OR gate whose output drives that
GIC input, so either device can raise the interrupt without the other
clobbering the line.

The gadget USB device is not created by the SoC. It is a separate,
user-creatable "aspeed.udc-gadget" USB device that the user plugs onto a
USB host controller's bus; it finds its controller through the "udc"
link property, e.g.

  -device aspeed.udc-gadget,udc=/machine/soc/udc

Signed-off-by: Jamin Lin <jamin_lin@aspeedtech.com>
Reviewed-by: Philippe Mathieu-Daudé <philmd@oss.qualcomm.com>
---
 include/hw/arm/aspeed_soc.h |  4 ++++
 hw/arm/aspeed_ast2600.c     | 37 ++++++++++++++++++++++++++++++++++---
 2 files changed, 38 insertions(+), 3 deletions(-)

diff --git a/include/hw/arm/aspeed_soc.h b/include/hw/arm/aspeed_soc.h
index cd68c7f1ca..fe7b31e42c 100644
--- a/include/hw/arm/aspeed_soc.h
+++ b/include/hw/arm/aspeed_soc.h
@@ -36,6 +36,8 @@
 #include "hw/gpio/aspeed_sgpio.h"
 #include "hw/sd/aspeed_sdhci.h"
 #include "hw/usb/hcd-ehci.h"
+#include "hw/usb/aspeed-udc.h"
+#include "hw/core/or-irq.h"
 #include "qom/object.h"
 #include "hw/misc/aspeed_lpc.h"
 #include "hw/misc/unimp.h"
@@ -138,6 +140,8 @@ struct Aspeed2600SoCState {
 
     A15MPPrivState a7mpcore;
     ARMCPU cpu[ASPEED_CPUS_NUM]; /* XXX belong to a7mpcore */
+    AspeedUDCState udc;
+    OrIRQState ehci2_udc_orgate;
 };
 
 #define TYPE_ASPEED2600_SOC "aspeed2600-soc"
diff --git a/hw/arm/aspeed_ast2600.c b/hw/arm/aspeed_ast2600.c
index d1f18e471a..21db95f5ae 100644
--- a/hw/arm/aspeed_ast2600.c
+++ b/hw/arm/aspeed_ast2600.c
@@ -32,6 +32,7 @@ static const hwaddr aspeed_soc_ast2600_memmap[] = {
     [ASPEED_DEV_SPI1]      = 0x1E630000,
     [ASPEED_DEV_SPI2]      = 0x1E631000,
     [ASPEED_DEV_EHCI1]     = 0x1E6A1000,
+    [ASPEED_DEV_UDC]       = 0x1E6A2000,
     [ASPEED_DEV_EHCI2]     = 0x1E6A3000,
     [ASPEED_DEV_MII1]      = 0x1E650000,
     [ASPEED_DEV_MII2]      = 0x1E650008,
@@ -214,6 +215,10 @@ static void aspeed_soc_ast2600_init(Object *obj)
                                 TYPE_PLATFORM_EHCI);
     }
 
+    object_initialize_child(obj, "udc", &a->udc, TYPE_ASPEED_UDC);
+    object_initialize_child(obj, "ehci2-udc-orgate", &a->ehci2_udc_orgate,
+                            TYPE_OR_IRQ);
+
     snprintf(typename, sizeof(typename), "aspeed.sdmc-%s", socname);
     object_initialize_child(obj, "sdmc", &s->sdmc, typename);
     object_property_add_alias(obj, "ram-size", OBJECT(&s->sdmc),
@@ -561,6 +566,16 @@ static void aspeed_soc_ast2600_realize(DeviceState *dev, Error **errp)
                            aspeed_soc_ast2600_get_irq(s, ASPEED_DEV_SPI1 + i));
     }
 
+    /*
+     * EHCI2 and the UDC share one SoC interrupt line, so OR their outputs
+     * together and drive that GIC input from the OR gate.
+     */
+    object_property_set_int(OBJECT(&a->ehci2_udc_orgate), "num-lines", 2,
+                            &error_abort);
+    qdev_realize(DEVICE(&a->ehci2_udc_orgate), NULL, &error_abort);
+    qdev_connect_gpio_out(DEVICE(&a->ehci2_udc_orgate), 0,
+                          aspeed_soc_ast2600_get_irq(s, ASPEED_DEV_EHCI2));
+
     /* EHCI */
     for (i = 0; i < sc->ehcis_num; i++) {
         if (!sysbus_realize(SYS_BUS_DEVICE(&s->ehci[i]), errp)) {
@@ -568,10 +583,26 @@ static void aspeed_soc_ast2600_realize(DeviceState *dev, Error **errp)
         }
         aspeed_mmio_map(s->memory, SYS_BUS_DEVICE(&s->ehci[i]), 0,
                         sc->memmap[ASPEED_DEV_EHCI1 + i]);
-        sysbus_connect_irq(SYS_BUS_DEVICE(&s->ehci[i]), 0,
-                           aspeed_soc_ast2600_get_irq(s,
-                                                      ASPEED_DEV_EHCI1 + i));
     }
+    /*
+     * EHCI1 has its own IRQ; EHCI2 shares the UDC's IRQ, so route it through
+     * the OR gate.
+     */
+    sysbus_connect_irq(SYS_BUS_DEVICE(&s->ehci[0]), 0,
+                       aspeed_soc_ast2600_get_irq(s, ASPEED_DEV_EHCI1));
+    sysbus_connect_irq(SYS_BUS_DEVICE(&s->ehci[1]), 0,
+                       qdev_get_gpio_in(DEVICE(&a->ehci2_udc_orgate), 0));
+
+    /* UDC - USB 2.0 Device Controller */
+    object_property_set_link(OBJECT(&a->udc), "dram", OBJECT(s->dram_mr),
+                             &error_abort);
+    if (!sysbus_realize(SYS_BUS_DEVICE(&a->udc), errp)) {
+        return;
+    }
+    aspeed_mmio_map(s->memory, SYS_BUS_DEVICE(&a->udc), 0,
+                    sc->memmap[ASPEED_DEV_UDC]);
+    sysbus_connect_irq(SYS_BUS_DEVICE(&a->udc), 0,
+                       qdev_get_gpio_in(DEVICE(&a->ehci2_udc_orgate), 1));
 
     /* SDMC - SDRAM Memory Controller */
     if (!sysbus_realize(SYS_BUS_DEVICE(&s->sdmc), errp)) {
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 8+ messages in thread

* Re: [PATCH v3 1/4] hw/usb/aspeed-udc: Add ASPEED UDC device controller
  2026-08-31  9:21 ` [PATCH v3 1/4] hw/usb/aspeed-udc: Add ASPEED UDC device controller Jamin Lin
@ 2026-08-31 10:03   ` Philippe Mathieu-Daudé
  2026-09-01  2:02     ` Jamin Lin
  0 siblings, 1 reply; 8+ messages in thread
From: Philippe Mathieu-Daudé @ 2026-08-31 10:03 UTC (permalink / raw)
  To: Jamin Lin, Paolo Bonzini, Peter Maydell, Cédric Le Goater,
	Steven Lee, Troy Lee, Kane Chen, Andrew Jeffery, Joel Stanley,
	open list:ARM TCG CPUs, open list:All patches CC here
  Cc: Troy Lee

On 31/8/26 11:21, Jamin Lin wrote:
> The AST2600 has a USB 2.0 Device Controller (UDC) at 0x1e6a2000 with one
> control endpoint and four programmable endpoints.
> 
> Add the controller as a sysbus (system) device: the MMIO register map
> described with the registerfields macros, the interrupt line and the
> soft reset. This is only the register/system side.
> 
> Note: this "device controller" is the system-bus device (TYPE_ASPEED_UDC).
> It is not the gadget USB device (TYPE_ASPEED_UDC_GADGET) that a host
> controller enumerates, which is added in the next patch.
> 
> Signed-off-by: Jamin Lin <jamin_lin@aspeedtech.com>
> ---
>   include/hw/usb/aspeed-udc.h |  49 ++++++++
>   hw/usb/aspeed-udc.c         | 236 ++++++++++++++++++++++++++++++++++++
>   hw/arm/Kconfig              |   1 +
>   hw/usb/Kconfig              |   4 +
>   hw/usb/meson.build          |   1 +
>   hw/usb/trace-events         |   7 ++
>   6 files changed, 298 insertions(+)
>   create mode 100644 include/hw/usb/aspeed-udc.h
>   create mode 100644 hw/usb/aspeed-udc.c


> diff --git a/hw/usb/Kconfig b/hw/usb/Kconfig
> index de95686720..e8c00f813a 100644
> --- a/hw/usb/Kconfig
> +++ b/hw/usb/Kconfig
> @@ -146,3 +146,7 @@ config XLNX_USB_SUBSYS
>   config USB_CHIPIDEA
>       bool
>       select USB_EHCI_SYSBUS
> +
> +config ASPEED_UDC
> +    bool
> +    select USB
Nothing depends on USB so far, so "select USB" would be
better added in the following patch. Otherwise QOM/QDev:

Reviewed-by: Philippe Mathieu-Daudé <philmd@oss.qualcomm.com>



^ permalink raw reply	[flat|nested] 8+ messages in thread

* Re: [PATCH v3 2/4] hw/usb/aspeed-udc: Add ASPEED UDC gadget USB device
  2026-08-31  9:21 ` [PATCH v3 2/4] hw/usb/aspeed-udc: Add ASPEED UDC gadget USB device Jamin Lin
@ 2026-08-31 10:04   ` Philippe Mathieu-Daudé
  0 siblings, 0 replies; 8+ messages in thread
From: Philippe Mathieu-Daudé @ 2026-08-31 10:04 UTC (permalink / raw)
  To: Jamin Lin, Paolo Bonzini, Peter Maydell, Cédric Le Goater,
	Steven Lee, Troy Lee, Kane Chen, Andrew Jeffery, Joel Stanley,
	open list:ARM TCG CPUs, open list:All patches CC here
  Cc: Troy Lee

On 31/8/26 11:21, Jamin Lin wrote:
> Present the UDC gadget side to a USB host controller as a USB device
> (TYPE_ASPEED_UDC_GADGET). This is a normal QEMU USB device, so it can be
> attached to any USB host controller bus, not only the BMC's own EHCI. It
> links back to its controller through the "udc" property.
> 
> This patch implements the control endpoint (EP0), which is enough for the
> host to enumerate the gadget. Host control transfers are handled
> asynchronously: the SETUP packet is mirrored into the SETUP data buffer,
> the EP0 interrupt is raised and the host packet is parked (USB_RET_ASYNC).
> The guest gadget driver then drives the data and status stages by writing
> UDC_EP0_CTRL; that moves data to/from the driver's DMA buffer and completes
> the parked packet back to the host.
> 
> SET_ADDRESS is the exception: it is applied synchronously, because the host
> controller keeps the transfer bound to address 0 until it completes.
> 
> The gadget connects to / disconnects from the host bus when the driver
> sets or clears the upstream-enable (pull-up) bit, and is detached on reset.
> 
> Signed-off-by: Jamin Lin <jamin_lin@aspeedtech.com>
> ---
>   include/hw/usb/aspeed-udc.h |  28 +++
>   hw/usb/aspeed-udc.c         | 382 +++++++++++++++++++++++++++++++++++-
>   hw/usb/trace-events         |   5 +
>   3 files changed, 412 insertions(+), 3 deletions(-)

QOM/QDev:
Reviewed-by: Philippe Mathieu-Daudé <philmd@oss.qualcomm.com>


^ permalink raw reply	[flat|nested] 8+ messages in thread

* RE: [PATCH v3 1/4] hw/usb/aspeed-udc: Add ASPEED UDC device controller
  2026-08-31 10:03   ` Philippe Mathieu-Daudé
@ 2026-09-01  2:02     ` Jamin Lin
  0 siblings, 0 replies; 8+ messages in thread
From: Jamin Lin @ 2026-09-01  2:02 UTC (permalink / raw)
  To: Philippe Mathieu-Daudé, Paolo Bonzini, Peter Maydell,
	Cédric Le Goater, Steven Lee, Troy Lee, Kane Chen,
	Andrew Jeffery, Joel Stanley, open list:ARM TCG CPUs,
	open list:All patches CC here
  Cc: Troy Lee

> Subject: Re: [PATCH v3 1/4] hw/usb/aspeed-udc: Add ASPEED UDC device
> controller
> 
> On 31/8/26 11:21, Jamin Lin wrote:
> > The AST2600 has a USB 2.0 Device Controller (UDC) at 0x1e6a2000 with
> > one control endpoint and four programmable endpoints.
> >
> > Add the controller as a sysbus (system) device: the MMIO register map
> > described with the registerfields macros, the interrupt line and the
> > soft reset. This is only the register/system side.
> >
> > Note: this "device controller" is the system-bus device (TYPE_ASPEED_UDC).
> > It is not the gadget USB device (TYPE_ASPEED_UDC_GADGET) that a host
> > controller enumerates, which is added in the next patch.
> >
> > Signed-off-by: Jamin Lin <jamin_lin@aspeedtech.com>
> > ---
> >   include/hw/usb/aspeed-udc.h |  49 ++++++++
> >   hw/usb/aspeed-udc.c         | 236
> ++++++++++++++++++++++++++++++++++++
> >   hw/arm/Kconfig              |   1 +
> >   hw/usb/Kconfig              |   4 +
> >   hw/usb/meson.build          |   1 +
> >   hw/usb/trace-events         |   7 ++
> >   6 files changed, 298 insertions(+)
> >   create mode 100644 include/hw/usb/aspeed-udc.h
> >   create mode 100644 hw/usb/aspeed-udc.c
> 
> 
> > diff --git a/hw/usb/Kconfig b/hw/usb/Kconfig index
> > de95686720..e8c00f813a 100644
> > --- a/hw/usb/Kconfig
> > +++ b/hw/usb/Kconfig
> > @@ -146,3 +146,7 @@ config XLNX_USB_SUBSYS
> >   config USB_CHIPIDEA
> >       bool
> >       select USB_EHCI_SYSBUS
> > +
> > +config ASPEED_UDC
> > +    bool
> > +    select USB
> Nothing depends on USB so far, so "select USB" would be better added in the
> following patch. Otherwise QOM/QDev:
> 
Thanks for the review and suggestion.
Will send v4
Jamin
> Reviewed-by: Philippe Mathieu-Daudé <philmd@oss.qualcomm.com>


^ permalink raw reply	[flat|nested] 8+ messages in thread

end of thread, other threads:[~2026-09-01  2:02 UTC | newest]

Thread overview: 8+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-31  9:21 [PATCH v3 0/4] hw/usb: Add ASPEED USB Device Controller (UDC) Jamin Lin
2026-08-31  9:21 ` [PATCH v3 1/4] hw/usb/aspeed-udc: Add ASPEED UDC device controller Jamin Lin
2026-08-31 10:03   ` Philippe Mathieu-Daudé
2026-09-01  2:02     ` Jamin Lin
2026-08-31  9:21 ` [PATCH v3 2/4] hw/usb/aspeed-udc: Add ASPEED UDC gadget USB device Jamin Lin
2026-08-31 10:04   ` Philippe Mathieu-Daudé
2026-08-31  9:21 ` [PATCH v3 3/4] hw/usb/aspeed-udc: Add programmable endpoint DMA transfers Jamin Lin
2026-08-31  9:21 ` [PATCH v3 4/4] hw/arm/aspeed_ast2600: Wire up the UDC Jamin Lin

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox