* [PATCH v1 0/3] hw/usb: Add ASPEED USB Device Controller (UDC)
@ 2026-07-03 7:43 Jamin Lin
2026-07-03 7:43 ` [PATCH v1 1/3] hw/usb/aspeed-udc: Add ASPEED UDC device controller Jamin Lin
` (2 more replies)
0 siblings, 3 replies; 6+ messages in thread
From: Jamin Lin @ 2026-07-03 7:43 UTC (permalink / raw)
To: Paolo Bonzini, Peter Maydell, Cédric Le Goater, Steven Lee,
Troy Lee, Kane Chen, Andrew Jeffery, Joel Stanley,
open list:ARM TCG CPUs, open list:All patches CC here
Cc: Jamin Lin, Troy Lee
This series adds a QEMU model for the ASPEED USB Device Controller (UDC),
driven by the Linux "aspeed_udc" gadget driver.
It is the first step of a larger plan to model USB device-side support on
ASPEED BMC/BIC SoCs, which has three goals:
1. Model the ASPEED UDC (AST2600 / AST1030). The AST2600 also has USB host
(EHCI) controllers, so this series targets the AST2600 UDC: its gadget can
be attached to the SoC's own EHCI bus, letting the guest enumerate its own
gadget and exercise the UDC end-to-end. [this series]
2. AST1030 UDC. The AST1030 has no USB host controller, so testing its UDC
needs a second QEMU instance. The plan is to redirect the UDC gadget out
of the guest using libusbredir and attach it to another QEMU that runs a
USB host (a VMM, or an AST2600 / AST2700 guest). [on-going]
3. ASPEED vHub, as a longer-term goal towards BMC KVM / Virtual Media support
in QEMU. [future]
This series implements goal 1 only.
Design
------
The UDC is modelled as two QOM objects, because a single object cannot be
both a SysBusDevice and a USBDevice:
- "aspeed.udc": the sysbus device (MMIO register map, IRQ and DMA engine)
that the guest gadget driver programs.
- "aspeed.udc-gadget": a user-creatable USB device presented on a USB host
controller's bus. It links back to its controller through the "udc"
property.
The SoC creates the controller; the gadget is added on the command line, e.g.
-device aspeed.udc-gadget,udc=/machine/soc/udc
Test result:
-----------
The default ASPEED SDK prebuilt image does not enable the UDC driver, so
build a kernel with CONFIG_USB_ASPEED_UDC=y first.
Start QEMU with the gadget attached to the on-SoC EHCI:
qemu-system-arm -machine ast2600-evb -drive file=<image>,if=mtd,format=raw \
-device aspeed.udc-gadget,udc=/machine/soc/udc -nographic
In the guest, bring up a mass-storage gadget and verify enumeration and I/O:
1. Before: only the host controllers are present
root@ast2600-default:~# lsusb
unable to initialize usb specBus 001 Device 001: ID 1d6b:0002 Linux 6.18.20-dirty-32e49fb4a22b-g32e49fb4a22b-dirty ehci_hcd EHCI Host Controller
Bus 002 Device 001: ID 1d6b:0001 Linux 6.18.20-dirty-32e49fb4a22b-g32e49fb4a22b-dirty uhci_hcd Generic UHCI Host Controller
2. Enable the gadget
root@ast2600-default:~# ./usb-storage.sh
Using UDC: 1e6a2000.usb
[ 598.582205] Mass Storage Function, version: 2009/09/11
[ 598.582635] LUN: removable file: (no medium)
USB Mass Storage gadget is enabled.
Backing file: /home/root/jamin
root@ast2600-default:~# [ 598.876395] usb 1-1: new high-speed USB device number 2 using ehci-platform
[ 599.070035] usb 1-1: New USB device found, idVendor=1d6b, idProduct=0104, bcdDevice= 1.00
[ 599.070821] usb 1-1: New USB device strings: Mfr=1, Product=2, SerialNumber=3
[ 599.071448] usb 1-1: Product: ASPEED USB Storage
[ 599.072687] usb 1-1: Manufacturer: ASPEED
[ 599.073384] usb 1-1: SerialNumber: 1234567890
[ 599.103625] usb-storage 1-1:1.0: USB Mass Storage device detected
[ 599.136326] scsi host0: usb-storage 1-1:1.0
[ 600.230921] scsi 0:0:0:0: Direct-Access Linux File-Stor Gadget 0618 PQ: 0 ANSI: 2
[ 600.243758] sd 0:0:0:0: Attached scsi generic sg0 type 0
[ 600.263452] sd 0:0:0:0: Power-on or device reset occurred
[ 600.289381] sd 0:0:0:0: [sda] 2048 512-byte logical blocks: (1.05 MB/1.00 MiB)
[ 600.297896] sd 0:0:0:0: [sda] Write Protect is off
[ 600.306379] sd 0:0:0:0: [sda] Write cache: enabled, read cache: enabled, doesn't support DPO or FUA
[ 600.424608] sda:
[ 600.425526] sd 0:0:0:0: [sda] Attached SCSI removable disk
3. After: the gadget enumerates on the EHCI bus
root@ast2600-default:~# lsusb
unable to initialize usb specBus 001 Device 001: ID 1d6b:0002 Linux 6.18.20-dirty-32e49fb4a22b-g32e49fb4a22b-dirty ehci_hcd EHCI Host Controller
Bus 001 Device 002: ID 1d6b:0104 ASPEED ASPEED USB Storage
Bus 002 Device 001: ID 1d6b:0001 Linux 6.18.20-dirty-32e49fb4a22b-g32e49fb4a22b-dirty uhci_hcd Generic UHCI Host Controller
4. Mount and verify data
root@ast2600-default:~# mount /dev/sda /mnt/
root@ast2600-default:~# sha256sum /mnt/testfile
3308890a1289f6a327c014537523e8ebd0833301af3d25a7c5893dc7050d2c70 /mnt/testfile
root@ast2600-default:~# umount /mnt
5. Stop the gadget -> clean disconnect
root@ast2600-default:~# ./usb-storage.sh stop
Stopping USB gadget...
[ 669.866439] usb 1-1: USB disconnect, device number 2
Stopped.
root@ast2600-default:~# [ 669.968156] sd 0:0:0:0: [sda] Synchronizing SCSI cache
[ 669.969307] sd 0:0:0:0: [sda] Synchronize Cache(10) failed: Result: hostbyte=0x01 driverbyte=DRIVER_OK
root@ast2600-default:~# lsusb
unable to initialize usb specBus 001 Device 001: ID 1d6b:0002 Linux 6.18.20-dirty-32e49fb4a22b-g32e49fb4a22b-dirty ehci_hcd EHCI Host Controller
Bus 002 Device 001: ID 1d6b:0001 Linux 6.18.20-dirty-32e49fb4a22b-g32e49fb4a22b-dirty uhci_hcd Generic UHCI Host Controller
The contents of this scripts
---------------------------
root@ast2600-default:~# cat usb-storage.sh
#!/bin/sh
set -e
G=/sys/kernel/config/usb_gadget/g1
IMG=/home/root/jamin
SIZE_MB=64
mount_configfs()
{
mountpoint -q /sys/kernel/config || mount -t configfs none /sys/kernel/config
}
get_udc()
{
ls /sys/class/udc | head -n 1
}
stop_gadget()
{
if [ ! -d "$G" ]; then
return
fi
echo "Stopping USB gadget..."
# Unbind UDC first
if [ -f "$G/UDC" ]; then
echo "" > "$G/UDC" 2>/dev/null || true
fi
# Clear backing file to release image
if [ -f "$G/functions/mass_storage.0/lun.0/file" ]; then
echo "" > "$G/functions/mass_storage.0/lun.0/file" 2>/dev/null || true
fi
# Remove function link from config
rm -f "$G/configs/c.1/mass_storage.0" 2>/dev/null || true
echo "Stopped."
}
start_gadget()
{
mount_configfs
UDC="$(get_udc)"
if [ -z "$UDC" ]; then
echo "ERROR: No UDC found in /sys/class/udc"
exit 1
fi
stop_gadget
echo "Using UDC: $UDC"
modprobe libcomposite 2>/dev/null || true
mkdir -p "$G"
cd "$G"
echo 0x1d6b > idVendor
echo 0x0104 > idProduct
echo 0x0200 > bcdUSB
echo 0x0100 > bcdDevice
mkdir -p strings/0x409
echo "1234567890" > strings/0x409/serialnumber
echo "ASPEED" > strings/0x409/manufacturer
echo "ASPEED USB Storage" > strings/0x409/product
mkdir -p configs/c.1/strings/0x409
echo "Mass Storage" > configs/c.1/strings/0x409/configuration
echo 120 > configs/c.1/MaxPower
if [ ! -f "$IMG" ]; then
echo "Creating backing image: $IMG"
dd if=/dev/zero of="$IMG" bs=1M count="$SIZE_MB"
mkfs.vfat "$IMG"
fi
mkdir -p functions/mass_storage.0
# Make sure old LUN is detached before changing attributes
echo "" > functions/mass_storage.0/lun.0/file 2>/dev/null || true
echo 0 > functions/mass_storage.0/stall
echo 0 > functions/mass_storage.0/lun.0/cdrom
echo 0 > functions/mass_storage.0/lun.0/ro
echo 1 > functions/mass_storage.0/lun.0/removable
echo "$IMG" > functions/mass_storage.0/lun.0/file
ln -sf functions/mass_storage.0 configs/c.1/mass_storage.0
echo "$UDC" > UDC
echo "USB Mass Storage gadget is enabled."
echo "Backing file: $IMG"
}
case "$1" in
start|"")
start_gadget
;;
stop)
stop_gadget
;;
restart)
stop_gadget
sleep 1
start_gadget
;;
*)
echo "Usage: $0 {start|stop|restart}"
exit 1
;;
esac
v1:
1. Add ASPEED UDC device controller
2. Add ASPEED UDC gadget USB device
Jamin Lin (3):
hw/usb/aspeed-udc: Add ASPEED UDC device controller
hw/usb/aspeed-udc: Add ASPEED UDC gadget USB device
hw/arm/aspeed_ast2600: Wire up the UDC
hw/arm/Kconfig | 1 +
hw/arm/aspeed_ast2600.c | 13 +
hw/usb/Kconfig | 4 +
hw/usb/aspeed-udc.c | 930 ++++++++++++++++++++++++++++++++++++
hw/usb/meson.build | 1 +
hw/usb/trace-events | 16 +
include/hw/arm/aspeed_soc.h | 2 +
include/hw/usb/aspeed-udc.h | 84 ++++
8 files changed, 1051 insertions(+)
create mode 100644 hw/usb/aspeed-udc.c
create mode 100644 include/hw/usb/aspeed-udc.h
--
2.53.0
^ permalink raw reply [flat|nested] 6+ messages in thread
* [PATCH v1 1/3] hw/usb/aspeed-udc: Add ASPEED UDC device controller
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
2026-07-09 21:03 ` 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
2 siblings, 1 reply; 6+ messages in thread
From: Jamin Lin @ 2026-07-03 7:43 UTC (permalink / raw)
To: Paolo Bonzini, Peter Maydell, Cédric Le Goater, Steven Lee,
Troy Lee, Kane Chen, Andrew Jeffery, Joel Stanley,
open list:ARM TCG CPUs, open list:All patches CC here
Cc: Jamin Lin, Troy Lee
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
^ permalink raw reply related [flat|nested] 6+ messages in thread
* [PATCH v1 2/3] hw/usb/aspeed-udc: Add ASPEED UDC gadget USB device
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-03 7:43 ` Jamin Lin
2026-07-03 7:43 ` [PATCH v1 3/3] hw/arm/aspeed_ast2600: Wire up the UDC Jamin Lin
2 siblings, 0 replies; 6+ messages in thread
From: Jamin Lin @ 2026-07-03 7:43 UTC (permalink / raw)
To: Paolo Bonzini, Peter Maydell, Cédric Le Goater, Steven Lee,
Troy Lee, Kane Chen, Andrew Jeffery, Joel Stanley,
open list:ARM TCG CPUs, open list:All patches CC here
Cc: Jamin Lin, Troy Lee
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
^ permalink raw reply related [flat|nested] 6+ messages in thread
* [PATCH v1 3/3] hw/arm/aspeed_ast2600: Wire up the UDC
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-03 7:43 ` [PATCH v1 2/3] hw/usb/aspeed-udc: Add ASPEED UDC gadget USB device Jamin Lin
@ 2026-07-03 7:43 ` Jamin Lin
2026-07-09 21:10 ` Philippe Mathieu-Daudé
2 siblings, 1 reply; 6+ messages in thread
From: Jamin Lin @ 2026-07-03 7:43 UTC (permalink / raw)
To: Paolo Bonzini, Peter Maydell, Cédric Le Goater, Steven Lee,
Troy Lee, Kane Chen, Andrew Jeffery, Joel Stanley,
open list:ARM TCG CPUs, open list:All patches CC here
Cc: Jamin Lin, Troy Lee
Create the USB Device Controller (UDC) at 0x1e6a2000 on the AST2600 SoC:
map its registers and connect its interrupt.
The gadget USB device is not created by the SoC. It is a separate,
user-creatable "aspeed.udc-gadget" USB device that the user plugs onto a
USB host controller's bus; it finds its controller through the "udc"
link property, e.g.
-device aspeed.udc-gadget,udc=/machine/soc/udc
Signed-off-by: Jamin Lin <jamin_lin@aspeedtech.com>
---
hw/arm/aspeed_ast2600.c | 13 +++++++++++++
include/hw/arm/aspeed_soc.h | 2 ++
2 files changed, 15 insertions(+)
diff --git a/hw/arm/aspeed_ast2600.c b/hw/arm/aspeed_ast2600.c
index d1f18e471a..7d97f10448 100644
--- a/hw/arm/aspeed_ast2600.c
+++ b/hw/arm/aspeed_ast2600.c
@@ -32,6 +32,7 @@ static const hwaddr aspeed_soc_ast2600_memmap[] = {
[ASPEED_DEV_SPI1] = 0x1E630000,
[ASPEED_DEV_SPI2] = 0x1E631000,
[ASPEED_DEV_EHCI1] = 0x1E6A1000,
+ [ASPEED_DEV_UDC] = 0x1E6A2000,
[ASPEED_DEV_EHCI2] = 0x1E6A3000,
[ASPEED_DEV_MII1] = 0x1E650000,
[ASPEED_DEV_MII2] = 0x1E650008,
@@ -113,6 +114,7 @@ static const int aspeed_soc_ast2600_irqmap[] = {
[ASPEED_DEV_SDHCI] = 43,
[ASPEED_DEV_EHCI1] = 5,
[ASPEED_DEV_EHCI2] = 9,
+ [ASPEED_DEV_UDC] = 9,
[ASPEED_DEV_EMMC] = 15,
[ASPEED_DEV_GPIO] = 40,
[ASPEED_DEV_GPIO_1_8V] = 11,
@@ -214,6 +216,8 @@ static void aspeed_soc_ast2600_init(Object *obj)
TYPE_PLATFORM_EHCI);
}
+ object_initialize_child(obj, "udc", &a->udc, TYPE_ASPEED_UDC);
+
snprintf(typename, sizeof(typename), "aspeed.sdmc-%s", socname);
object_initialize_child(obj, "sdmc", &s->sdmc, typename);
object_property_add_alias(obj, "ram-size", OBJECT(&s->sdmc),
@@ -573,6 +577,15 @@ static void aspeed_soc_ast2600_realize(DeviceState *dev, Error **errp)
ASPEED_DEV_EHCI1 + i));
}
+ /* UDC - USB 2.0 Device Controller */
+ if (!sysbus_realize(SYS_BUS_DEVICE(&a->udc), errp)) {
+ return;
+ }
+ aspeed_mmio_map(s->memory, SYS_BUS_DEVICE(&a->udc), 0,
+ sc->memmap[ASPEED_DEV_UDC]);
+ sysbus_connect_irq(SYS_BUS_DEVICE(&a->udc), 0,
+ aspeed_soc_ast2600_get_irq(s, ASPEED_DEV_UDC));
+
/* SDMC - SDRAM Memory Controller */
if (!sysbus_realize(SYS_BUS_DEVICE(&s->sdmc), errp)) {
return;
diff --git a/include/hw/arm/aspeed_soc.h b/include/hw/arm/aspeed_soc.h
index 3aac144cd4..74fd83e957 100644
--- a/include/hw/arm/aspeed_soc.h
+++ b/include/hw/arm/aspeed_soc.h
@@ -36,6 +36,7 @@
#include "hw/gpio/aspeed_sgpio.h"
#include "hw/sd/aspeed_sdhci.h"
#include "hw/usb/hcd-ehci.h"
+#include "hw/usb/aspeed-udc.h"
#include "qom/object.h"
#include "hw/misc/aspeed_lpc.h"
#include "hw/misc/unimp.h"
@@ -138,6 +139,7 @@ struct Aspeed2600SoCState {
A15MPPrivState a7mpcore;
ARMCPU cpu[ASPEED_CPUS_NUM]; /* XXX belong to a7mpcore */
+ AspeedUDCState udc;
};
#define TYPE_ASPEED2600_SOC "aspeed2600-soc"
--
2.53.0
^ permalink raw reply related [flat|nested] 6+ messages in thread
* Re: [PATCH v1 1/3] hw/usb/aspeed-udc: Add ASPEED UDC device controller
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é
0 siblings, 0 replies; 6+ messages in thread
From: Philippe Mathieu-Daudé @ 2026-07-09 21:03 UTC (permalink / raw)
To: Jamin Lin, Paolo Bonzini, Peter Maydell, Cédric Le Goater,
Steven Lee, Troy Lee, Kane Chen, Andrew Jeffery, Joel Stanley,
open list:ARM TCG CPUs, open list:All patches CC here
Cc: Troy Lee
Hi Jamin,
On 3/7/26 09:43, Jamin Lin wrote:
> 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
> +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);
Why allocate a fixed size? Otherwise LGTM.
> + 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_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);
> +}
> +/*
> + * 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 */
^ permalink raw reply [flat|nested] 6+ messages in thread
* Re: [PATCH v1 3/3] hw/arm/aspeed_ast2600: Wire up the UDC
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é
0 siblings, 0 replies; 6+ messages in thread
From: Philippe Mathieu-Daudé @ 2026-07-09 21:10 UTC (permalink / raw)
To: Jamin Lin, Paolo Bonzini, Peter Maydell, Cédric Le Goater,
Steven Lee, Troy Lee, Kane Chen, Andrew Jeffery, Joel Stanley,
open list:ARM TCG CPUs, open list:All patches CC here
Cc: Troy Lee
On 3/7/26 09:43, Jamin Lin wrote:
> Create the USB Device Controller (UDC) at 0x1e6a2000 on the AST2600 SoC:
> map its registers and connect its interrupt.
>
> The gadget USB device is not created by the SoC. It is a separate,
> user-creatable "aspeed.udc-gadget" USB device that the user plugs onto a
> USB host controller's bus; it finds its controller through the "udc"
> link property, e.g.
>
> -device aspeed.udc-gadget,udc=/machine/soc/udc
>
> Signed-off-by: Jamin Lin <jamin_lin@aspeedtech.com>
> ---
> hw/arm/aspeed_ast2600.c | 13 +++++++++++++
> include/hw/arm/aspeed_soc.h | 2 ++
> 2 files changed, 15 insertions(+)
>
> diff --git a/hw/arm/aspeed_ast2600.c b/hw/arm/aspeed_ast2600.c
> index d1f18e471a..7d97f10448 100644
> --- a/hw/arm/aspeed_ast2600.c
> +++ b/hw/arm/aspeed_ast2600.c
> @@ -32,6 +32,7 @@ static const hwaddr aspeed_soc_ast2600_memmap[] = {
> [ASPEED_DEV_SPI1] = 0x1E630000,
> [ASPEED_DEV_SPI2] = 0x1E631000,
> [ASPEED_DEV_EHCI1] = 0x1E6A1000,
> + [ASPEED_DEV_UDC] = 0x1E6A2000,
> [ASPEED_DEV_EHCI2] = 0x1E6A3000,
> [ASPEED_DEV_MII1] = 0x1E650000,
> [ASPEED_DEV_MII2] = 0x1E650008,
> @@ -113,6 +114,7 @@ static const int aspeed_soc_ast2600_irqmap[] = {
> [ASPEED_DEV_SDHCI] = 43,
> [ASPEED_DEV_EHCI1] = 5,
> [ASPEED_DEV_EHCI2] = 9,
> + [ASPEED_DEV_UDC] = 9,
> [ASPEED_DEV_EMMC] = 15,
> [ASPEED_DEV_GPIO] = 40,
> [ASPEED_DEV_GPIO_1_8V] = 11,
> @@ -214,6 +216,8 @@ static void aspeed_soc_ast2600_init(Object *obj)
> TYPE_PLATFORM_EHCI);
> }
>
> + object_initialize_child(obj, "udc", &a->udc, TYPE_ASPEED_UDC);
> +
> snprintf(typename, sizeof(typename), "aspeed.sdmc-%s", socname);
> object_initialize_child(obj, "sdmc", &s->sdmc, typename);
> object_property_add_alias(obj, "ram-size", OBJECT(&s->sdmc),
> @@ -573,6 +577,15 @@ static void aspeed_soc_ast2600_realize(DeviceState *dev, Error **errp)
> ASPEED_DEV_EHCI1 + i));
> }
>
> + /* UDC - USB 2.0 Device Controller */
> + if (!sysbus_realize(SYS_BUS_DEVICE(&a->udc), errp)) {
> + return;
> + }
> + aspeed_mmio_map(s->memory, SYS_BUS_DEVICE(&a->udc), 0,
> + sc->memmap[ASPEED_DEV_UDC]);
> + sysbus_connect_irq(SYS_BUS_DEVICE(&a->udc), 0,
> + aspeed_soc_ast2600_get_irq(s, ASPEED_DEV_UDC));
Do we need to mux this IRQ line?
> /* SDMC - SDRAM Memory Controller */
> if (!sysbus_realize(SYS_BUS_DEVICE(&s->sdmc), errp)) {
> return;
> diff --git a/include/hw/arm/aspeed_soc.h b/include/hw/arm/aspeed_soc.h
> index 3aac144cd4..74fd83e957 100644
> --- a/include/hw/arm/aspeed_soc.h
> +++ b/include/hw/arm/aspeed_soc.h
> @@ -36,6 +36,7 @@
> #include "hw/gpio/aspeed_sgpio.h"
> #include "hw/sd/aspeed_sdhci.h"
> #include "hw/usb/hcd-ehci.h"
> +#include "hw/usb/aspeed-udc.h"
> #include "qom/object.h"
> #include "hw/misc/aspeed_lpc.h"
> #include "hw/misc/unimp.h"
> @@ -138,6 +139,7 @@ struct Aspeed2600SoCState {
>
> A15MPPrivState a7mpcore;
> ARMCPU cpu[ASPEED_CPUS_NUM]; /* XXX belong to a7mpcore */
> + AspeedUDCState udc;
> };
>
> #define TYPE_ASPEED2600_SOC "aspeed2600-soc"
^ permalink raw reply [flat|nested] 6+ messages in thread
end of thread, other threads:[~2026-07-09 21:11 UTC | newest]
Thread overview: 6+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
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 ` [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é
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.