QEMU-Arm Archive on 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 1/3] hw/usb/aspeed-udc: Add ASPEED UDC device controller
Date: Fri, 3 Jul 2026 07:43:34 +0000	[thread overview]
Message-ID: <20260703074332.1049473-2-jamin_lin@aspeedtech.com> (raw)
In-Reply-To: <20260703074332.1049473-1-jamin_lin@aspeedtech.com>

The AST2600 has a USB 2.0 Device Controller (UDC) at 0x1e6a2000 with one
control endpoint and four programmable endpoints, driven by the Linux
"aspeed_udc" gadget driver.

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_DEV) that a host
controller enumerates, which is added in the next patch.

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

diff --git a/hw/arm/Kconfig b/hw/arm/Kconfig
index fb798ccbee..8fcf9f48c9 100644
--- a/hw/arm/Kconfig
+++ b/hw/arm/Kconfig
@@ -532,6 +532,7 @@ config ASPEED_SOC
     default y
     depends on TCG && ARM
     imply PCI_DEVICES
+    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/aspeed-udc.c b/hw/usb/aspeed-udc.c
new file mode 100644
index 0000000000..9be9cfcf13
--- /dev/null
+++ b/hw/usb/aspeed-udc.c
@@ -0,0 +1,264 @@
+/*
+ * 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), driven by the Linux
+ * "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.
+ */
+
+#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)
+    FIELD(UDC_FUNC_CTRL, UPSTREAM_FS,       1, 1)
+    FIELD(UDC_FUNC_CTRL, STOP_CLK_SUSPEND,  2, 1)
+    FIELD(UDC_FUNC_CTRL, AUTO_REMOTE_WKUP,  3, 1)
+    FIELD(UDC_FUNC_CTRL, REMOTE_WKUP_EN,    4, 1)
+    FIELD(UDC_FUNC_CTRL, TEST_MODE,         8, 3)
+    FIELD(UDC_FUNC_CTRL, PHY_RESET_DIS,    11, 1)
+    FIELD(UDC_FUNC_CTRL, EP_LONG_DESC,     18, 1)
+    FIELD(UDC_FUNC_CTRL, PHY_CLK_EN,       31, 1)
+REG32(UDC_CONFIG, 0x04)
+    FIELD(UDC_CONFIG, DEV_ADDR,             0, 7)
+REG32(UDC_IER, 0x08)
+REG32(UDC_ISR, 0x0C)
+    FIELD(UDC_ISR, EP0_SETUP,               0, 1)
+    FIELD(UDC_ISR, EP0_OUT_ACK,             1, 1)
+    FIELD(UDC_ISR, EP0_OUT_NAK,             2, 1)
+    FIELD(UDC_ISR, EP0_IN_ACK,              3, 1)
+    FIELD(UDC_ISR, EP0_IN_NAK,              4, 1)
+    FIELD(UDC_ISR, BUS_RESET,               6, 1)
+    FIELD(UDC_ISR, SUSPEND,                 7, 1)
+    FIELD(UDC_ISR, RESUME,                  8, 1)
+    FIELD(UDC_ISR, EP_POOL_ACK,            16, 1)
+    FIELD(UDC_ISR, EP_POOL_NAK,            17, 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, ROOT,              0, 1)
+    FIELD(UDC_DEV_RESET, DMA,               8, 1)
+    FIELD(UDC_DEV_RESET, EP_POOL,           9, 1)
+REG32(UDC_STS, 0x24)
+    FIELD(UDC_STS, HIGHSPEED,              27, 1)
+REG32(UDC_EP_DATA, 0x28)
+REG32(UDC_ISO_TX_FAIL, 0x2C)
+REG32(UDC_EP0_CTRL, 0x30)
+    FIELD(UDC_EP0_CTRL, STALL,              0, 1)
+    FIELD(UDC_EP0_CTRL, TX_RDY,             1, 1)
+    FIELD(UDC_EP0_CTRL, RX_RDY,             2, 1)
+    FIELD(UDC_EP0_CTRL, TX_LEN,             8, 7)
+    FIELD(UDC_EP0_CTRL, RX_LEN,            16, 7)
+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, ENABLE,                0, 1)
+    FIELD(EP_CONFIG, DIR_OUT,               4, 1)
+    FIELD(EP_CONFIG, TYPE,                  5, 2)
+    FIELD(EP_CONFIG, EP_NUM,                8, 4)
+    FIELD(EP_CONFIG, STALL,                12, 1)
+    FIELD(EP_CONFIG, AUTO_TOGGLE_DIS,      13, 1)
+    FIELD(EP_CONFIG, MAX_PKT,              16, 10)
+REG32(EP_DMA_CTRL, 0x04)
+    FIELD(EP_DMA_CTRL, DESC_OP_EN,          0, 1)
+    FIELD(EP_DMA_CTRL, SINGLE_STAGE,        1, 1)
+    FIELD(EP_DMA_CTRL, RESET,               2, 1)
+    FIELD(EP_DMA_CTRL, IN_LONG_MODE,        3, 1)
+    FIELD(EP_DMA_CTRL, PROC_STS,            4, 4)
+REG32(EP_DMA_BUFF, 0x08)
+REG32(EP_DMA_STS, 0x0C)
+    FIELD(EP_DMA_STS, WPTR,                 0, 8)
+    FIELD(EP_DMA_STS, RPTR,                 8, 8)
+    FIELD(EP_DMA_STS, TX_SIZE,             16, 11)
+
+/* 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 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);
+    uint64_t val = s->regs[offset >> 2];
+
+    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_ISR:
+    case R_UDC_EP_ACK_ISR:
+    case R_UDC_EP_NAK_ISR:
+        /* Status registers are write-1-to-clear */
+        s->regs[reg] &= ~val;
+        break;
+    default:
+        s->regs[reg] = val;
+        break;
+    }
+
+    aspeed_udc_update_irq(s);
+}
+
+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;
+    uint64_t val = e->regs[offset >> 2];
+
+    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;
+
+    trace_aspeed_udc_ep_write(e->index, offset, data);
+    e->regs[offset >> 2] = 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_realize(DeviceState *dev, Error **errp)
+{
+    AspeedUDCState *s = ASPEED_UDC(dev);
+    SysBusDevice *sbd = SYS_BUS_DEVICE(dev);
+    int i;
+
+    s->regs = g_new0(uint32_t, ASPEED_UDC_NR_REGS);
+
+    memory_region_init(&s->iomem, OBJECT(s), TYPE_ASPEED_UDC,
+                       ASPEED_UDC_REG_SIZE);
+
+    /* Root/global registers occupy the low part of the window */
+    memory_region_init_io(&s->reg_mr, OBJECT(s), &aspeed_udc_ops, s,
+                          TYPE_ASPEED_UDC ".regs", ASPEED_UDC_NR_REGS << 2);
+    memory_region_add_subregion(&s->iomem, 0, &s->reg_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;
+        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,
+                              &s->ep[i], name, ASPEED_UDC_EP_NR_REGS << 2);
+        memory_region_add_subregion(&s->iomem, ASPEED_UDC_EP_REG_BASE +
+                                    i * ASPEED_UDC_EP_REG_SIZE, &s->ep[i].mr);
+    }
+
+    sysbus_init_mmio(sbd, &s->iomem);
+    sysbus_init_irq(sbd, &s->irq);
+}
+
+static void aspeed_udc_reset_hold(Object *obj, ResetType type)
+{
+    AspeedUDCState *s = ASPEED_UDC(obj);
+    int i;
+
+    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->regs[R_UDC_DEV_RESET] = UDC_DEV_RESET_DEFAULT;
+}
+
+static void aspeed_udc_unrealize(DeviceState *dev)
+{
+    AspeedUDCState *s = ASPEED_UDC(dev);
+    int i;
+
+    for (i = 0; i < ASPEED_UDC_NUM_EP; i++) {
+        g_free(s->ep[i].regs);
+    }
+    g_free(s->regs);
+}
+
+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;
+    dc->unrealize = aspeed_udc_unrealize;
+    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/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 0d4318dcf1..fb79e24fba 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, uint64_t value) "offset 0x%" PRIx64 " value 0x%" PRIx64
+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_irq(uint32_t isr, uint32_t ier, int level) "isr 0x%x, ier 0x%x, level %d"
diff --git a/include/hw/usb/aspeed-udc.h b/include/hw/usb/aspeed-udc.h
new file mode 100644
index 0000000000..eb279dd9c3
--- /dev/null
+++ b/include/hw/usb/aspeed-udc.h
@@ -0,0 +1,54 @@
+/*
+ * 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)
+
+/*
+ * 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
+/* 32-bit registers per programmable endpoint */
+#define ASPEED_UDC_EP_NR_REGS   4
+
+/*
+ * The root/global register block spans 0x000...0x087: the SETUP data buffer
+ * ends at 0x84. Size the backing array to cover the whole block.
+ */
+#define ASPEED_UDC_NR_REGS      (0x88 >> 2)
+
+/* MMIO window: root registers below EP_REG_BASE, then the per-EP banks */
+#define ASPEED_UDC_REG_SIZE     0x300
+#define ASPEED_UDC_EP_REG_BASE  0x200
+#define ASPEED_UDC_EP_REG_SIZE  0x10
+
+typedef struct AspeedUDCEP {
+    MemoryRegion mr;
+    int index;
+    uint32_t *regs;
+} AspeedUDCEP;
+
+struct AspeedUDCState {
+    SysBusDevice parent_obj;
+
+    /* container: root registers + per-endpoint banks */
+    MemoryRegion iomem;
+    MemoryRegion reg_mr;
+    qemu_irq irq;
+    uint32_t *regs;
+    AspeedUDCEP ep[ASPEED_UDC_NUM_EP];
+};
+
+#endif /* HW_USB_ASPEED_UDC_H */
-- 
2.53.0


  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 ` Jamin Lin [this message]
2026-07-09 21:03   ` [PATCH v1 1/3] hw/usb/aspeed-udc: Add ASPEED UDC device controller Philippe Mathieu-Daudé
2026-07-03  7:43 ` [PATCH v1 2/3] hw/usb/aspeed-udc: Add ASPEED UDC gadget USB device Jamin Lin
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-2-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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox