All of lore.kernel.org
 help / color / mirror / Atom feed
From: Jamin Lin <jamin_lin@aspeedtech.com>
To: "Paolo Bonzini" <pbonzini@redhat.com>,
	"Peter Maydell" <peter.maydell@linaro.org>,
	"Cédric Le Goater" <clg@kaod.org>,
	"Steven Lee" <steven_lee@aspeedtech.com>,
	"Troy Lee" <leetroy@gmail.com>,
	"Kane Chen" <kane_chen@aspeedtech.com>,
	"Andrew Jeffery" <andrew@codeconstruct.com.au>,
	"Joel Stanley" <joel@jms.id.au>,
	"open list:ARM TCG CPUs" <qemu-arm@nongnu.org>,
	"open list:All patches CC here" <qemu-devel@nongnu.org>
Cc: Jamin Lin <jamin_lin@aspeedtech.com>, Troy Lee <troy_lee@aspeedtech.com>
Subject: [PATCH v1 2/3] hw/usb/aspeed-udc: Add ASPEED UDC gadget USB device
Date: Fri, 3 Jul 2026 07:43:36 +0000	[thread overview]
Message-ID: <20260703074332.1049473-3-jamin_lin@aspeedtech.com> (raw)
In-Reply-To: <20260703074332.1049473-1-jamin_lin@aspeedtech.com>

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 forwards host transactions to the guest gadget driver by raising the
matching controller interrupts, and completes them once the driver
answers through the device-controller MMIO registers:

  - EP0 control: the SETUP packet is mirrored to the SETUP data buffer,
    and the driver's IN/OUT data is copied to/from the host packet.
  - Bulk endpoints: IN uses the descriptor-list DMA ring, OUT uses the
    single-stage DMA buffer. A transfer larger than one host packet is
    served across several polls and the packet is parked until the gadget
    queues more data.

The gadget connects to / disconnects from the host bus when the driver
sets or clears the upstream-enable bit (pull-up), and is detached on
reset so a guest reboot does not leave the host enumerating a gadget with
no driver behind it.

Signed-off-by: Jamin Lin <jamin_lin@aspeedtech.com>
---
 hw/usb/aspeed-udc.c         | 678 +++++++++++++++++++++++++++++++++++-
 hw/usb/trace-events         |   9 +
 include/hw/usb/aspeed-udc.h |  30 ++
 3 files changed, 711 insertions(+), 6 deletions(-)

diff --git a/hw/usb/aspeed-udc.c b/hw/usb/aspeed-udc.c
index 9be9cfcf13..3d69e33516 100644
--- a/hw/usb/aspeed-udc.c
+++ b/hw/usb/aspeed-udc.c
@@ -9,18 +9,30 @@
  * "aspeed_udc" gadget driver. 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 (the "system bus
+ *     device" section below);
+ *   - a USBDevice presented on a host controller's bus, which forwards host
+ *     transactions to the gadget by raising the matching controller interrupts
+ *     and completes them once the gadget responds via MMIO (the "USB device"
+ *     section below).
  */
 
 #include "qemu/osdep.h"
+#include "qemu/error-report.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"
 
+#define AST_UDC_EP0_MAXPKT      64
+
 /* Root / Global registers (offset from the controller base) */
 REG32(UDC_FUNC_CTRL, 0x00)
     FIELD(UDC_FUNC_CTRL, UPSTREAM_EN,       0, 1)
@@ -90,11 +102,29 @@ REG32(EP_DMA_STS, 0x0C)
     FIELD(EP_DMA_STS, RPTR,                 8, 8)
     FIELD(EP_DMA_STS, TX_SIZE,             16, 11)
 
+/* 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 (EP_DMA_CTRL PROC_STS field) */
+#define EP_DMA_CTRL_STS_RX_IDLE 0x0
+#define EP_DMA_CTRL_STS_TX_IDLE 0x8
+
+/* DMA descriptor (DES1) fields, in guest memory */
+#define AST_EP_DESC1_IN_LEN(w1)     ((w1) & 0x1fff)
+/* interrupt-on-completion */
+#define AST_EP_DESC1_INTR           BIT(31)
+
 /* Device-reset default: root, DMA and EP-pool soft-reset bits set (0x301) */
 #define UDC_DEV_RESET_DEFAULT \
     (R_UDC_DEV_RESET_ROOT_MASK | R_UDC_DEV_RESET_DMA_MASK | \
      R_UDC_DEV_RESET_EP_POOL_MASK)
 
+static bool aspeed_udc_ep_serve_in(AspeedUDCState *s, unsigned ep,
+                                   USBPacket *p, bool *complete);
+static bool aspeed_udc_ep_deliver_out(AspeedUDCState *s, unsigned ep,
+                                      USBPacket *p);
+
 static void aspeed_udc_update_irq(AspeedUDCState *s)
 {
     bool level;
@@ -107,6 +137,159 @@ 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);
+}
+
+/* Signal completion of a programmable-endpoint transaction to the gadget */
+static void aspeed_udc_raise_ep_ack(AspeedUDCState *s, unsigned 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)
+ */
+
+/* Connect/disconnect the gadget from the host bus (UBD00 upstream enable) */
+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);
+}
+
+/*
+ * The gadget driver drives EP0 by writing UBD30. Translate those writes into
+ * data movement to/from the deferred host control packet plus the matching
+ * ACK interrupts the gadget expects.
+ */
+static void aspeed_udc_ep0_ctrl_write(AspeedUDCState *s, uint32_t val)
+{
+    uint32_t buf_addr = s->regs[R_UDC_EP0_DATA_BUFF];
+    uint32_t store = val & R_UDC_EP0_CTRL_STALL_MASK;
+    uint32_t txlen;
+    USBPacket *p;
+    uint32_t n;
+
+    trace_aspeed_udc_ep0_ctrl_write(val, s->ep0_dir_in, s->ep0_offset);
+
+    /* Gadget stalled EP0: fail the pending control transfer */
+    if (val & R_UDC_EP0_CTRL_STALL_MASK) {
+        p = s->ep0_packet;
+
+        if (p) {
+            s->ep0_packet = NULL;
+            p->status = USB_RET_STALL;
+            usb_generic_async_ctrl_complete(USB_DEVICE(s->usbgadget), p);
+        }
+        s->regs[R_UDC_EP0_CTRL] = store;
+        return;
+    }
+
+    if (val & R_UDC_EP0_CTRL_TX_RDY_MASK) {
+        txlen = FIELD_EX32(val, UDC_EP0_CTRL, TX_LEN);
+
+        if (s->ep0_dir_in && s->ep0_packet) {
+            /* IN data stage: copy a chunk from the gadget's DMA buffer */
+            n = MIN(txlen, s->ep0_setup_len - s->ep0_offset);
+
+            if (n) {
+                dma_memory_read(&address_space_memory, buf_addr,
+                                s->ep0_data + s->ep0_offset, n,
+                                MEMTXATTRS_UNSPECIFIED);
+            }
+            s->ep0_offset += n;
+            aspeed_udc_raise_isr(s, R_UDC_ISR_EP0_IN_ACK_MASK);
+
+            if (txlen < AST_UDC_EP0_MAXPKT ||
+                s->ep0_offset >= s->ep0_setup_len) {
+                aspeed_udc_ep0_complete(s, s->ep0_offset);
+            }
+        } else {
+            /* Zero-length status IN for an OUT / no-data control transfer */
+            aspeed_udc_raise_isr(s, R_UDC_ISR_EP0_IN_ACK_MASK);
+            aspeed_udc_ep0_complete(s, s->ep0_offset);
+        }
+
+    } else if (val & R_UDC_EP0_CTRL_RX_RDY_MASK) {
+        if (s->ep0_dir_in) {
+            /* Status stage OUT (zero length) following IN data */
+            aspeed_udc_raise_isr(s, R_UDC_ISR_EP0_OUT_ACK_MASK);
+        } else if (s->ep0_packet) {
+            /* OUT data stage: hand a chunk of host data to the gadget */
+            n = MIN(s->ep0_setup_len - s->ep0_offset,
+                    AST_UDC_EP0_MAXPKT);
+
+            if (n) {
+                dma_memory_write(&address_space_memory, buf_addr,
+                                 s->ep0_data + s->ep0_offset, n,
+                                 MEMTXATTRS_UNSPECIFIED);
+            }
+            s->ep0_offset += n;
+            store = FIELD_DP32(store, UDC_EP0_CTRL, RX_LEN, n);
+            aspeed_udc_raise_isr(s, R_UDC_ISR_EP0_OUT_ACK_MASK);
+        }
+    }
+
+    s->regs[R_UDC_EP0_CTRL] = store;
+}
+
+/* The upstream-enable bit connects/disconnects the gadget from the host bus */
+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);
+
+    s->regs[R_UDC_FUNC_CTRL] = val;
+    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);
@@ -132,6 +315,12 @@ static void aspeed_udc_write(void *opaque, hwaddr offset, uint64_t data,
         /* Status registers are write-1-to-clear */
         s->regs[reg] &= ~val;
         break;
+    case R_UDC_FUNC_CTRL:
+        aspeed_udc_func_ctrl_write(s, val);
+        break;
+    case R_UDC_EP0_CTRL:
+        aspeed_udc_ep0_ctrl_write(s, val);
+        return;
     default:
         s->regs[reg] = val;
         break;
@@ -154,6 +343,70 @@ static const MemoryRegionOps aspeed_udc_ops = {
     },
 };
 
+/*
+ * Descriptor-mode IN kick: the gadget wrote EP_DMA_STS to advance its write
+ * pointer (queue more IN data). Adopt the write pointer, keep our read pointer
+ * (the DMA engine owns it), then serve and complete a parked IN poll if any.
+ */
+static void aspeed_udc_ep_dma_kick(AspeedUDCState *s, unsigned ep, uint32_t val)
+{
+    AspeedUDCEP *e = &s->ep[ep];
+    uint32_t rptr = FIELD_EX32(e->regs[R_EP_DMA_STS], EP_DMA_STS, RPTR);
+    uint32_t rptr_w = FIELD_EX32(val, EP_DMA_STS, RPTR);
+    uint32_t wptr = FIELD_EX32(val, EP_DMA_STS, WPTR);
+    uint32_t sts = e->regs[R_EP_DMA_STS];
+    USBPacket *p = e->pkt;
+    bool complete;
+
+    sts = FIELD_DP32(sts, EP_DMA_STS, WPTR, wptr);
+
+    /*
+     * A normal kick writes only the write pointer (bits[7:0]); the read-pointer
+     * field is zero and is ours to advance. Only an explicit non-zero read
+     * pointer equal to the write pointer (the driver draining the ring, e.g. on
+     * reset) resets our read pointer. Testing rptr==wptr alone would wrongly
+     * match a normal kick whose write pointer just wrapped to 0, resetting the
+     * ring and stranding a parked packet until the host times out.
+     */
+    if (rptr_w != 0 && rptr_w == wptr) {
+        rptr = rptr_w;
+        e->desc_off = 0;
+    }
+    e->regs[R_EP_DMA_STS] = FIELD_DP32(sts, EP_DMA_STS, RPTR, rptr);
+
+    if (p && rptr != wptr) {
+        if (aspeed_udc_ep_serve_in(s, ep, p, &complete)) {
+            aspeed_udc_raise_ep_ack(s, ep);
+        }
+        if (complete) {
+            e->pkt = NULL;
+            p->status = USB_RET_SUCCESS;
+            usb_packet_complete(USB_DEVICE(s->usbgadget), p);
+        }
+    }
+}
+
+/*
+ * Single-stage OUT arm: the gadget wrote EP_DMA_STS to offer a receive
+ * buffer. If a host OUT packet is parked waiting for it,
+ * deliver into the buffer now and complete it (or keep it parked if the host
+ * data phase is larger than this one armed chunk).
+ */
+static void aspeed_udc_ep_out_kick(AspeedUDCState *s, unsigned ep)
+{
+    AspeedUDCEP *e = &s->ep[ep];
+    USBPacket *p = e->pkt;
+
+    if (!p || !FIELD_EX32(e->regs[R_EP_DMA_STS], EP_DMA_STS, WPTR)) {
+        return;
+    }
+    if (aspeed_udc_ep_deliver_out(s, ep, p)) {
+        e->pkt = NULL;
+        p->status = USB_RET_SUCCESS;
+        usb_packet_complete(USB_DEVICE(s->usbgadget), p);
+    }
+}
+
 static uint64_t aspeed_udc_ep_read(void *opaque, hwaddr offset, unsigned size)
 {
     AspeedUDCEP *e = opaque;
@@ -167,9 +420,33 @@ static void aspeed_udc_ep_write(void *opaque, hwaddr offset, uint64_t data,
                                 unsigned size)
 {
     AspeedUDCEP *e = opaque;
-
-    trace_aspeed_udc_ep_write(e->index, offset, data);
-    e->regs[offset >> 2] = data;
+    AspeedUDCState *s = e->udc;
+    uint32_t reg = offset >> 2;
+    uint32_t val = data;
+    uint32_t ctrl;
+
+    trace_aspeed_udc_ep_write(e->index, offset, val);
+
+    /*
+     * A write to EP_DMA_STS is a DMA kick. In descriptor mode (IN) it advances
+     * the ring and resumes a parked IN poll; in single-stage mode (OUT) it
+     * arms a receive buffer and delivers a parked OUT packet. Every other
+     * EP register is plain storage.
+     */
+    if (reg == R_EP_DMA_STS) {
+        ctrl = e->regs[R_EP_DMA_CTRL];
+
+        if (FIELD_EX32(ctrl, EP_DMA_CTRL, DESC_OP_EN)) {
+            /* descriptor-mode IN */
+            aspeed_udc_ep_dma_kick(s, e->index, val);
+        } else {
+            /* single-stage OUT */
+            e->regs[reg] = val;
+            aspeed_udc_ep_out_kick(s, e->index);
+        }
+    } else {
+        e->regs[reg] = val;
+    }
 }
 
 static const MemoryRegionOps aspeed_udc_ep_ops = {
@@ -206,6 +483,7 @@ static void aspeed_udc_realize(DeviceState *dev, Error **errp)
     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].udc = s;
         s->ep[i].index = i;
         s->ep[i].regs = g_new0(uint32_t, ASPEED_UDC_EP_NR_REGS);
         memory_region_init_io(&s->ep[i].mr, OBJECT(s), &aspeed_udc_ep_ops,
@@ -226,8 +504,26 @@ static void aspeed_udc_reset_hold(Object *obj, ResetType type)
     memset(s->regs, 0, ASPEED_UDC_NR_REGS * sizeof(uint32_t));
     for (i = 0; i < ASPEED_UDC_NUM_EP; i++) {
         memset(s->ep[i].regs, 0, ASPEED_UDC_EP_NR_REGS * sizeof(uint32_t));
+        s->ep[i].pkt = NULL;
+        s->ep[i].desc_off = 0;
     }
     s->regs[R_UDC_DEV_RESET] = UDC_DEV_RESET_DEFAULT;
+    s->ep0_packet = NULL;
+
+    /*
+     * A machine reset (e.g. a guest reboot) wipes the gadget driver, so a
+     * device still attached to the host bus from before the reset is now
+     * dead. Detach it, otherwise the rebooted host tries to re-enumerate a
+     * gadget with no driver behind it and fails with -110. It re-attaches
+     * when the new gadget asserts pull-up (UBD00 upstream-enable).
+     */
+    if (s->usbgadget) {
+        USBDevice *udev = USB_DEVICE(s->usbgadget);
+
+        if (udev->attached) {
+            usb_device_detach(udev);
+        }
+    }
 }
 
 static void aspeed_udc_unrealize(DeviceState *dev)
@@ -252,6 +548,370 @@ static void aspeed_udc_class_init(ObjectClass *klass, const void *data)
     rc->phases.hold = aspeed_udc_reset_hold;
 }
 
+/*
+ * USB device: gadget 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 gadget driver
+ * expects, then defer (USB_RET_ASYNC) until the driver responds through the
+ * MMIO register interface above.
+ */
+
+/* Locate the programmable endpoint matching a host transaction */
+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;
+}
+
+/*
+ * IN endpoint, descriptor-list mode: serve the descriptors the gadget queued
+ * (read pointer .. write pointer) into the host IN packet.
+ *
+ * The host controller coalesces a multi-packet bulk IN into one packet whose
+ * iovec may span several descriptors, so drain as many queued descriptors as
+ * fit. A descriptor bigger than the remaining room is served partially and
+ * resumed on the next poll via desc_off; the read pointer only advances once a
+ * descriptor is fully consumed (so the gadget driver, which reads the read
+ * pointer back, learns exactly what completed).
+ *
+ * *complete is set true when the host transaction is finished (packet full, or
+ * a short/zero-length descriptor ended the gadget's transfer) and false when
+ * the ring ran dry with the packet still short - the caller then keeps the
+ * packet parked and resumes on the next kick, rather than completing it short
+ * (a maxpacket-multiple short read is not seen as end-of-transfer by the host,
+ * which would then wait forever for data the gadget considers already sent).
+ *
+ * Returns true when the gadget should be signalled (EP ACK): the ring drained
+ * (its descriptor handler waits for read==write) or a descriptor asked for an
+ * interrupt.
+ */
+static bool aspeed_udc_ep_serve_in(AspeedUDCState *s, unsigned ep,
+                                   USBPacket *p, bool *complete)
+{
+    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 base = e->regs[R_EP_DMA_BUFF];
+    uint32_t sts = e->regs[R_EP_DMA_STS];
+    uint32_t wptr = FIELD_EX32(sts, EP_DMA_STS, WPTR);
+    uint32_t rptr = FIELD_EX32(sts, EP_DMA_STS, RPTR);
+    uint32_t remaining;
+    uint8_t buf[1024];
+    /* des_0: buffer base, des_1: control/len */
+    uint32_t desc[2];
+    uint32_t copied;
+    uint32_t chunk;
+    uint32_t dlen;
+    uint32_t room;
+    uint32_t seg;
+    uint32_t off;
+    uint32_t w0;
+    uint32_t w1;
+    bool done = false;
+    bool ack = false;
+
+    if (mps == 0) {
+        /* a MAX_PKT field of 0 means 1024 bytes */
+        mps = 1024;
+    }
+
+    trace_aspeed_udc_ep_data_in(ep, rptr, wptr, pktiov->size);
+
+    while (rptr != wptr) {
+        copied = 0;
+        dma_memory_read(&address_space_memory, base + rptr * sizeof(desc),
+                        desc, sizeof(desc), MEMTXATTRS_UNSPECIFIED);
+        w0 = le32_to_cpu(desc[0]);
+        w1 = le32_to_cpu(desc[1]);
+        dlen = AST_EP_DESC1_IN_LEN(w1);
+        off = e->desc_off;
+        remaining = dlen > off ? dlen - off : 0;
+        room = pktiov->size > (uint32_t)p->actual_length ?
+               pktiov->size - (uint32_t)p->actual_length : 0;
+        chunk = MIN(remaining, room);
+
+        while (copied < chunk) {
+            seg = MIN(chunk - copied, sizeof(buf));
+            dma_memory_read(&address_space_memory, w0 + off + copied, buf, seg,
+                            MEMTXATTRS_UNSPECIFIED);
+            usb_packet_copy(p, buf, seg);
+            copied += seg;
+        }
+        e->desc_off = off + chunk;
+
+        if (e->desc_off < dlen) {
+            /* Room exhausted mid-descriptor: the packet is full */
+            done = true;
+            break;
+        }
+
+        /* Descriptor fully served: advance the read pointer */
+        rptr = (rptr + 1) % ASPEED_UDC_DESCS_COUNT;
+        e->desc_off = 0;
+        if (w1 & AST_EP_DESC1_INTR) {
+            ack = true;
+        }
+        /* short/zero-length stage ends the transfer */
+        if (dlen < mps) {
+            done = true;
+            break;
+        }
+        if ((uint32_t)p->actual_length >= pktiov->size) {
+            done = true;
+            break;
+        }
+    }
+
+    e->regs[R_EP_DMA_STS] = FIELD_DP32(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 gadget driver completes its request when the ring drains */
+    if (rptr == wptr) {
+        ack = true;
+    }
+
+    *complete = done;
+    return ack;
+}
+
+static void aspeed_udc_ep_data_in(AspeedUDCState *s, unsigned ep, USBPacket *p)
+{
+    AspeedUDCEP *e = &s->ep[ep];
+    uint32_t sts = e->regs[R_EP_DMA_STS];
+    uint32_t rptr = FIELD_EX32(sts, EP_DMA_STS, RPTR);
+    uint32_t wptr = FIELD_EX32(sts, EP_DMA_STS, WPTR);
+    bool complete;
+
+    if (rptr == wptr) {
+        /*
+         * Nothing queued yet. Park the packet as ASYNC rather than NAKing: the
+         * gadget will kick shortly and the kick handler serves and completes
+         * it, waking the host immediately. NAK would leave the host to re-poll
+         * on its much slower async-schedule timer, collapsing bulk throughput.
+         */
+        e->pkt = p;
+        p->status = USB_RET_ASYNC;
+        return;
+    }
+
+    if (aspeed_udc_ep_serve_in(s, ep, p, &complete)) {
+        aspeed_udc_raise_ep_ack(s, ep);
+    }
+    if (complete) {
+        p->status = USB_RET_SUCCESS;
+    } else {
+        /*
+         * Ring ran dry before the request was satisfied: keep it parked and
+         * resume from the kick that queues the next chunk.
+         */
+        e->pkt = p;
+        p->status = USB_RET_ASYNC;
+    }
+}
+
+/*
+ * OUT endpoint, single-stage mode: deliver host OUT data into the buffer the
+ * gadget armed (EP_DMA_BUFF + EP_DMA_STS). The host controller may
+ * hand the whole OUT data phase as one packet larger than the armed chunk, so
+ * copy up to TX_SIZE bytes starting where the previous call left off
+ * (p->actual_length); the caller keeps the packet parked across re-arms until
+ * it is fully drained. Returns true once the packet is fully delivered.
+ */
+static bool aspeed_udc_ep_deliver_out(AspeedUDCState *s, unsigned ep,
+                                      USBPacket *p)
+{
+    AspeedUDCEP *e = &s->ep[ep];
+    uint32_t buf_addr = e->regs[R_EP_DMA_BUFF];
+    uint32_t chunk = FIELD_EX32(e->regs[R_EP_DMA_STS], EP_DMA_STS, TX_SIZE);
+    uint32_t remaining = p->iov.size - (uint32_t)p->actual_length;
+    uint32_t len = MIN(remaining, chunk);
+    uint8_t buf[ASPEED_UDC_DESC_MAX_LEN];
+
+    if (buf_addr && len) {
+        len = MIN(len, sizeof(buf));
+        usb_packet_copy(p, buf, len);
+        dma_memory_write(&address_space_memory, buf_addr, buf, len,
+                         MEMTXATTRS_UNSPECIFIED);
+    }
+
+    /* Report the received length; clearing WPTR disarms the receive buffer */
+    e->regs[R_EP_DMA_STS] = FIELD_DP32(0, EP_DMA_STS, TX_SIZE, len);
+    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);
+
+    return (uint32_t)p->actual_length >= p->iov.size;
+}
+
+static void aspeed_udc_ep_data_out(AspeedUDCState *s, unsigned 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, TX_SIZE),
+                                 p->iov.size);
+    if (!FIELD_EX32(sts, EP_DMA_STS, WPTR)) {
+        /*
+         * No receive buffer armed yet. Park the packet as ASYNC rather than
+         * NAKing: delivering into a stale buffer would lose the packet (e.g. a
+         * mass-storage CBW), and NAK would fall back to the slow async re-poll.
+         * aspeed_udc_ep_out_kick() delivers it once the gadget arms a buffer.
+         */
+        e->pkt = p;
+        p->status = USB_RET_ASYNC;
+        return;
+    }
+
+    if (aspeed_udc_ep_deliver_out(s, ep, p)) {
+        p->status = USB_RET_SUCCESS;
+    } else {
+        e->pkt = p;
+        p->status = USB_RET_ASYNC;
+    }
+}
+
+static void aspeed_udc_gadget_handle_reset(USBDevice *udev)
+{
+    AspeedUDCState *s = ASPEED_UDC_GADGET(udev)->udc;
+
+    s->ep0_packet = NULL;
+    s->ep0_offset = 0;
+    /* EHCI is high speed; let the gadget read the link speed from UBD24 */
+    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 type = request >> 8;
+    uint8_t req = request & 0xff;
+
+    /*
+     * Reconstruct the 8-byte SETUP packet into the SETUP data buffer where
+     * the gadget driver reads it from (controller offset 0x80).
+     */
+    s->regs[R_UDC_SETUP0] = type | (req << 8) | ((value & 0xffff) << 16);
+    s->regs[R_UDC_SETUP1] = (index & 0xffff) | ((length & 0xffff) << 16);
+
+    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: adopt the address and complete the (zero-length) status
+     * synchronously, exactly like QEMU's generic control handler. The gadget
+     * is still notified so its own state machine advances, but the host
+     * transfer must not depend on that asynchronous round-trip.
+     */
+    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)
+{
+    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)
+{
+    AspeedUDCGadget *dev = ASPEED_UDC_GADGET(udev);
+
+    if (!dev->udc) {
+        error_setg(errp, "aspeed-udc-gadget: 'udc' link is not set");
+        return;
+    }
+    /* Bind this gadget to its controller */
+    dev->udc->usbgadget = dev;
+
+    /* Connection to the host is driven by the gadget via UBD00 upstream-en */
+    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 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->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[] = {
     {
         .name          = TYPE_ASPEED_UDC,
@@ -259,6 +919,12 @@ static const TypeInfo aspeed_udc_types[] = {
         .instance_size = sizeof(AspeedUDCState),
         .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 fb79e24fba..77958fe79a 100644
--- a/hw/usb/trace-events
+++ b/hw/usb/trace-events
@@ -383,4 +383,13 @@ aspeed_udc_read(uint64_t offset, uint64_t value) "offset 0x%" PRIx64 " value 0x%
 aspeed_udc_write(uint64_t offset, uint64_t value) "offset 0x%" PRIx64 " value 0x%" PRIx64
 aspeed_udc_ep_read(int ep, uint64_t offset, uint64_t value) "ep %d, offset 0x%" PRIx64 " value 0x%" PRIx64
 aspeed_udc_ep_write(int ep, uint64_t offset, uint64_t value) "ep %d, offset 0x%" PRIx64 " value 0x%" PRIx64
+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"
+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"
diff --git a/include/hw/usb/aspeed-udc.h b/include/hw/usb/aspeed-udc.h
index eb279dd9c3..cbfd1e30fb 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)
+
 /*
  * EP0 (control) is served through the root registers (UDC_EP0_*), so only
  * the 4 programmable endpoints get their own register bank / ep[] entry.
@@ -36,10 +44,20 @@ OBJECT_DECLARE_SIMPLE_TYPE(AspeedUDCState, ASPEED_UDC)
 
 typedef struct AspeedUDCEP {
     MemoryRegion mr;
+    AspeedUDCState *udc;
     int index;
     uint32_t *regs;
+    /* host packet parked until the gadget queues (IN) or arms (OUT) data */
+    USBPacket *pkt;
+    /* bytes of the current IN descriptor already served */
+    uint32_t desc_off;
 } AspeedUDCEP;
 
+struct AspeedUDCGadget {
+    USBDevice parent_obj;
+    AspeedUDCState *udc;
+};
+
 struct AspeedUDCState {
     SysBusDevice parent_obj;
 
@@ -49,6 +67,18 @@ struct AspeedUDCState {
     qemu_irq irq;
     uint32_t *regs;
     AspeedUDCEP ep[ASPEED_UDC_NUM_EP];
+    /* gadget USB device bound to this controller (set at its realize) */
+    AspeedUDCGadget *usbgadget;
+
+    /*
+     * In-flight EP0 control transfer (host side), deferred until the gadget
+     * driver responds via MMIO.
+     */
+    USBPacket *ep0_packet;
+    uint8_t *ep0_data;
+    uint32_t ep0_setup_len;
+    uint32_t ep0_offset;
+    bool ep0_dir_in;
 };
 
 #endif /* HW_USB_ASPEED_UDC_H */
-- 
2.53.0


  parent reply	other threads:[~2026-07-03  7:45 UTC|newest]

Thread overview: 6+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-07-03  7:43 [PATCH v1 0/3] hw/usb: Add ASPEED USB Device Controller (UDC) Jamin Lin
2026-07-03  7:43 ` [PATCH v1 1/3] hw/usb/aspeed-udc: Add ASPEED UDC device controller Jamin Lin
2026-07-09 21:03   ` Philippe Mathieu-Daudé
2026-07-03  7:43 ` Jamin Lin [this message]
2026-07-03  7:43 ` [PATCH v1 3/3] hw/arm/aspeed_ast2600: Wire up the UDC Jamin Lin
2026-07-09 21:10   ` Philippe Mathieu-Daudé

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260703074332.1049473-3-jamin_lin@aspeedtech.com \
    --to=jamin_lin@aspeedtech.com \
    --cc=andrew@codeconstruct.com.au \
    --cc=clg@kaod.org \
    --cc=joel@jms.id.au \
    --cc=kane_chen@aspeedtech.com \
    --cc=leetroy@gmail.com \
    --cc=pbonzini@redhat.com \
    --cc=peter.maydell@linaro.org \
    --cc=qemu-arm@nongnu.org \
    --cc=qemu-devel@nongnu.org \
    --cc=steven_lee@aspeedtech.com \
    --cc=troy_lee@aspeedtech.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.