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
next prev 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 ` 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