summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJamin Lin <jamin_lin@aspeedtech.com>2026-09-02 02:15:44 +0000
committerCédric Le Goater <clg@redhat.com>2026-09-05 07:05:11 +0200
commita8cbaf13dc7c1289b9768766bc0966d34d5216cb (patch)
tree3d2753e473caa69ab4788ab15952286b097d3c3e
parent05dafe6bfcf646c3fc0d8665b5954c3dfdff9db8 (diff)
downloadqemu-a8cbaf13dc7c1289b9768766bc0966d34d5216cb.tar.gz
qemu-a8cbaf13dc7c1289b9768766bc0966d34d5216cb.zip
hw/usb/aspeed-udc: Add ASPEED UDC device controller
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> Reviewed-by: Philippe Mathieu-Daudé <philmd@oss.qualcomm.com> Reviewed-by: Cédric Le Goater <clg@redhat.com> Link: https://lore.kernel.org/qemu-devel/20260902021542.3194812-2-jamin_lin@aspeedtech.com Signed-off-by: Cédric Le Goater <clg@redhat.com>
-rw-r--r--hw/arm/Kconfig1
-rw-r--r--hw/usb/Kconfig3
-rw-r--r--hw/usb/aspeed-udc.c236
-rw-r--r--hw/usb/meson.build1
-rw-r--r--hw/usb/trace-events7
-rw-r--r--include/hw/usb/aspeed-udc.h49
6 files changed, 297 insertions, 0 deletions
diff --git a/hw/arm/Kconfig b/hw/arm/Kconfig
index 5db7ce057f..eae06369b0 100644
--- a/hw/arm/Kconfig
+++ b/hw/arm/Kconfig
@@ -520,6 +520,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..e4df15fe7c 100644
--- a/hw/usb/Kconfig
+++ b/hw/usb/Kconfig
@@ -146,3 +146,6 @@ config XLNX_USB_SUBSYS
config USB_CHIPIDEA
bool
select USB_EHCI_SYSBUS
+
+config ASPEED_UDC
+ bool
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/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"
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 */