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 <[email protected]>
---
 include/hw/usb/aspeed-udc.h |  49 ++++++++
 hw/usb/aspeed-udc.c         | 240 ++++++++++++++++++++++++++++++++++++
 hw/arm/Kconfig              |   1 +
 hw/usb/Kconfig              |   4 +
 hw/usb/meson.build          |   1 +
 hw/usb/trace-events         |   7 ++
 6 files changed, 302 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..85786a2e12
--- /dev/null
+++ b/hw/usb/aspeed-udc.c
@@ -0,0 +1,240 @@
+/*
+ * 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_CONFIG, 0x04)
+    FIELD(UDC_CONFIG, DEV_ADDR,             0, 7)
+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_EP_DATA, 0x28)
+REG32(UDC_ISO_TX_FAIL, 0x2C)
+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

Reply via email to