From: Jamin Lin <jamin_lin@aspeedtech.com>
To: "clg@kaod.org" <clg@kaod.org>,
Peter Maydell <peter.maydell@linaro.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:ASPEED BMCs" <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 v2 7/8] hw/usb/redirect-server: Stream interrupt IN endpoints
Date: Mon, 5 Oct 2026 03:20:33 +0000 [thread overview]
Message-ID: <20261005032022.3980903-8-jamin_lin@aspeedtech.com> (raw)
In-Reply-To: <20261005032022.3980903-1-jamin_lin@aspeedtech.com>
Interrupt IN is not a request and an answer in usbredir. The host asks
once with start_interrupt_receiving. After that it expects a packet
every time the device has data.
Park one IN packet on the device for each streaming endpoint. A device
that answers USB_RET_ASYNC holds the packet and completes it when it
has data. A device that answers USB_RET_NAK gives the packet back at
once, so park a new one when the bus reports a wakeup on that endpoint.
A slow timer parks one too, for a device that does not report a wakeup.
Signed-off-by: Jamin Lin <jamin_lin@aspeedtech.com>
---
include/hw/usb/redirect-server.h | 15 ++
hw/usb/redirect-server.c | 248 +++++++++++++++++++++++++++++++
hw/usb/trace-events | 4 +
3 files changed, 267 insertions(+)
diff --git a/include/hw/usb/redirect-server.h b/include/hw/usb/redirect-server.h
index f7b2261db4..1fb2a8a7a4 100644
--- a/include/hw/usb/redirect-server.h
+++ b/include/hw/usb/redirect-server.h
@@ -28,16 +28,23 @@ OBJECT_DECLARE_SIMPLE_TYPE(USBRedirServer, USB_REDIR_SERVER)
#define USBREDIR_SERVER_MAX_EP 32
#define USBREDIR_SERVER_EP_IN_BASE 16
+/* An endpoint number is 4 bits, so 0 to 15. */
+#define USBREDIR_SERVER_MAX_EP_NR 16
+
/*
* The bulk length field is 32 bits, so the host can ask for up to 4 GB.
* This is the largest transfer accepted.
*/
#define USBREDIR_SERVER_MAX_BULK (1 * MiB)
+/* Buffer size for an interrupt IN endpoint before its descriptor is seen. */
+#define USBREDIR_SERVER_INTR_DEFAULT_LEN 64
+
#define USBREDIR_SERVER_CTRL_SETUP 0
#define USBREDIR_SERVER_CTRL_STATUS 1
#define USBREDIR_SERVER_BULK 2
#define USBREDIR_SERVER_INTR 3
+#define USBREDIR_SERVER_INTR_STREAM 4
/* Which message answers the host when a control transfer ends. */
typedef enum {
@@ -87,6 +94,14 @@ struct USBRedirServer {
bool host_connected;
bool device_announced;
+ /*
+ * Interrupt IN streaming, indexed by endpoint number. intr_bh asks the
+ * device again; intr_retry does the same after a delay on NAK.
+ */
+ bool intr_in_started[USBREDIR_SERVER_MAX_EP_NR];
+ QEMUBH *intr_bh;
+ QEMUTimer *intr_retry;
+
/* In-flight packet tracking */
QTAILQ_HEAD(, USBRedirServerPkt) inflight;
uint64_t next_id;
diff --git a/hw/usb/redirect-server.c b/hw/usb/redirect-server.c
index a733a007e3..f84ece02e4 100644
--- a/hw/usb/redirect-server.c
+++ b/hw/usb/redirect-server.c
@@ -69,6 +69,13 @@
/* Wait this long after attach before we announce the device. */
#define USBREDIR_SERVER_ANNOUNCE_DEBOUNCE_MS 10
+/*
+ * Ask again this often after an endpoint answered NAK. Most devices wake
+ * the bus when data arrives, and then the next ask happens at once. This
+ * timer is for the devices that do not wake the bus.
+ */
+#define USBREDIR_SERVER_INTR_RETRY_MS 1000
+
static void usbredir_server_pkt_free(USBRedirServerPkt *rp);
static void usbredir_server_stop_transfers(USBRedirServer *s);
static void usbredir_server_send_cancelled(USBRedirServer *s,
@@ -402,6 +409,63 @@ static void usbredir_server_intr_complete(USBRedirServer *s,
usbredirparser_do_write(s->parser);
}
+/*
+ * The device answered a parked request. Send the answer to the host, then
+ * ask again, because the host still wants more. Do not ask from here: the
+ * device may answer at once, and this function would call itself over and
+ * over. Let the BH ask, or the retry timer after a NAK.
+ */
+static void usbredir_server_intr_stream_complete(USBRedirServer *s,
+ USBRedirServerPkt *rp)
+{
+ struct usb_redir_interrupt_packet_header resp = rp->intr_hdr;
+ struct usb_redir_interrupt_receiving_status_header st;
+ int ep_nr = resp.endpoint & 0x0f;
+ USBPacket *p = &rp->pkt;
+ int actual = p->actual_length;
+ bool retry = false;
+
+ trace_usbredir_server_intr_stream_complete(ep_nr, p->status, actual);
+
+ switch (p->status) {
+ case USB_RET_SUCCESS:
+ resp.status = usb_redir_success;
+ resp.length = actual;
+ usbredirparser_send_interrupt_packet(s->parser, 0, &resp,
+ actual ? rp->data : NULL,
+ actual);
+ usbredirparser_do_write(s->parser);
+ break;
+ case USB_RET_NAK:
+ /* nothing to report yet; ask again shortly */
+ retry = true;
+ break;
+ default:
+ /*
+ * The endpoint stalled or failed. There is no data to send, so
+ * send a status message. The host keeps that status and gives it
+ * to its guest. On a stall the host also ends the stream, so end
+ * it here too. A later request from the guest starts a new one.
+ */
+ st.endpoint = resp.endpoint;
+ st.status = usbredir_server_status(p->status);
+ usbredirparser_send_interrupt_receiving_status(s->parser, 0, &st);
+ usbredirparser_do_write(s->parser);
+ s->intr_in_started[ep_nr] = false;
+ break;
+ }
+
+ if (s->intr_in_started[ep_nr]) {
+ if (retry) {
+ timer_mod(s->intr_retry,
+ qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL) +
+ (int64_t)USBREDIR_SERVER_INTR_RETRY_MS * SCALE_MS);
+ } else {
+ qemu_bh_schedule(s->intr_bh);
+ }
+ }
+}
+
/*
* USB port ops
*/
@@ -504,6 +568,9 @@ static void usbredir_server_packet_complete(USBPort *port, USBPacket *p)
case USBREDIR_SERVER_INTR:
usbredir_server_intr_complete(s, rp);
break;
+ case USBREDIR_SERVER_INTR_STREAM:
+ usbredir_server_intr_stream_complete(s, rp);
+ break;
}
usbredir_server_pkt_free(rp);
@@ -521,7 +588,25 @@ static USBPortOps usbredir_server_port_ops = {
* USB bus ops
*/
+static void usbredir_server_wakeup_ep(USBBus *bus, USBEndpoint *ep,
+ unsigned int stream)
+{
+ USBRedirServer *s = container_of(bus, USBRedirServer, bus);
+ int ep_nr = ep->nr;
+
+ /*
+ * The device has data. A streaming interrupt IN endpoint may have
+ * answered NAK. Ask now instead of waiting for the retry timer.
+ * Every other transfer already has its request on the device.
+ */
+ if (ep->pid == USB_TOKEN_IN && ep_nr < USBREDIR_SERVER_MAX_EP_NR &&
+ s->intr_in_started[ep_nr]) {
+ qemu_bh_schedule(s->intr_bh);
+ }
+}
+
static USBBusOps usbredir_server_bus_ops = {
+ .wakeup_endpoint = usbredir_server_wakeup_ep,
};
/*
@@ -572,6 +657,91 @@ static void usbredir_server_drop_pkt(USBRedirServer *s,
usbredir_server_pkt_free(rp);
}
+/*
+ * Interrupt IN streaming
+ */
+
+static bool usbredir_server_intr_inflight(USBRedirServer *s, int ep_nr)
+{
+ USBRedirServerPkt *rp;
+
+ QTAILQ_FOREACH(rp, &s->inflight, next) {
+ if (rp->type == USBREDIR_SERVER_INTR_STREAM &&
+ (rp->intr_hdr.endpoint & 0x0f) == ep_nr) {
+ return true;
+ }
+ }
+ return false;
+}
+
+/*
+ * Ask the device for data on @ep_nr and leave the request waiting. A device
+ * sends data only when asked. Do nothing if a request on @ep_nr is still
+ * unanswered.
+ */
+static void usbredir_server_intr_park(USBRedirServer *s, int ep_nr)
+{
+ USBDevice *device = usbredir_server_device(s);
+ USBRedirServerPkt *rp;
+ USBEndpoint *ep;
+ int len;
+
+ /* No device to ask. */
+ if (!device || !device->attached) {
+ return;
+ }
+
+ /* The host did not ask for this endpoint, or a request is unanswered. */
+ if (!s->intr_in_started[ep_nr] ||
+ usbredir_server_intr_inflight(s, ep_nr)) {
+ return;
+ }
+
+ len = s->ep_max_packet[ep_nr + USBREDIR_SERVER_EP_IN_BASE];
+ if (len == 0) {
+ len = USBREDIR_SERVER_INTR_DEFAULT_LEN;
+ }
+ ep = usb_ep_get(device, USB_TOKEN_IN, ep_nr);
+ rp = usbredir_server_pkt_alloc(len);
+ rp->type = USBREDIR_SERVER_INTR_STREAM;
+ /* streamed data carries no host id */
+ rp->redir_id = 0;
+ rp->intr_hdr.endpoint = ep_nr | USB_DIR_IN;
+
+ usb_packet_setup(&rp->pkt, USB_TOKEN_IN, ep, 0, s->next_id++,
+ false, false);
+ usb_packet_addbuf(&rp->pkt, rp->data, len);
+
+ trace_usbredir_server_intr_park(ep_nr, len);
+ usbredir_server_submit_to_device(s, rp);
+}
+
+/* BH and timer callback: ask again on every streaming endpoint. */
+static void usbredir_server_intr_kick(void *opaque)
+{
+ USBRedirServer *s = opaque;
+ int i;
+
+ /* Endpoint 0 is the control endpoint. It never streams. */
+ for (i = 1; i < USBREDIR_SERVER_MAX_EP_NR; i++) {
+ usbredir_server_intr_park(s, i);
+ }
+}
+
+static void usbredir_server_intr_cancel(USBRedirServer *s, int ep_nr)
+{
+ USBRedirServerPkt *tmp;
+ USBRedirServerPkt *rp;
+
+ QTAILQ_FOREACH_SAFE(rp, &s->inflight, next, tmp) {
+ if (rp->type != USBREDIR_SERVER_INTR_STREAM ||
+ (rp->intr_hdr.endpoint & 0x0f) != ep_nr) {
+ continue;
+ }
+ usbredir_server_drop_pkt(s, rp);
+ }
+}
+
/*
* usbredirparser I/O and logging callbacks
*/
@@ -1002,6 +1172,58 @@ static void usbredir_server_interface_info(void *priv,
/* The host should not send this to a device. Nothing to do. */
}
+/*
+ * The host asks to stream an interrupt IN endpoint. Send the receiving
+ * status first. Without that status the host throws the interrupt
+ * packet away. Then ask the device once.
+ */
+static void usbredir_server_start_interrupt_receiving(void *priv,
+ uint64_t id, struct usb_redir_start_interrupt_receiving_header *hdr)
+{
+ struct usb_redir_interrupt_receiving_status_header st = {
+ .endpoint = hdr->endpoint,
+ .status = usb_redir_success,
+ };
+ USBRedirServer *s = priv;
+ USBDevice *device = usbredir_server_device(s);
+ int ep_nr = hdr->endpoint & 0x0f;
+
+ /*
+ * Endpoint 0 is control and an OUT endpoint never streams. There also
+ * has to be a host to send to and a device to ask.
+ */
+ if (ep_nr == 0 || !(hdr->endpoint & USB_DIR_IN) ||
+ !s->host_connected || !device || !device->attached) {
+ st.status = usb_redir_ioerror;
+ } else {
+ s->intr_in_started[ep_nr] = true;
+ }
+
+ trace_usbredir_server_intr_start(hdr->endpoint, st.status);
+ usbredirparser_send_interrupt_receiving_status(s->parser, id, &st);
+ usbredirparser_do_write(s->parser);
+
+ if (st.status == usb_redir_success) {
+ usbredir_server_intr_park(s, ep_nr);
+ }
+}
+
+static void usbredir_server_stop_interrupt_receiving(void *priv, uint64_t id,
+ struct usb_redir_stop_interrupt_receiving_header *hdr)
+{
+ int ep_nr = hdr->endpoint & 0x0f;
+ USBRedirServer *s = priv;
+
+ /* Endpoint 0 is control, and an OUT endpoint never streams. */
+ if (ep_nr == 0 || !(hdr->endpoint & USB_DIR_IN)) {
+ return;
+ }
+
+ trace_usbredir_server_intr_stop(hdr->endpoint);
+ s->intr_in_started[ep_nr] = false;
+ usbredir_server_intr_cancel(s, ep_nr);
+}
+
static void usbredir_server_alloc_bulk_streams(void *priv, uint64_t id,
struct usb_redir_alloc_bulk_streams_header *hdr)
{
@@ -1106,6 +1328,14 @@ static void usbredir_server_stop_transfers(USBRedirServer *s)
{
USBRedirServerPkt *rp;
+ memset(s->intr_in_started, 0, sizeof(s->intr_in_started));
+ if (s->intr_bh) {
+ qemu_bh_cancel(s->intr_bh);
+ }
+ if (s->intr_retry) {
+ timer_del(s->intr_retry);
+ }
+
/*
* No "cancelled" response here. This runs on a bus reset, a detach or
* a closed chardev, and the host has dropped its own queues already.
@@ -1151,6 +1381,10 @@ static void usbredir_server_create_parser(USBRedirServer *s)
s->parser->device_disconnect_ack_func =
usbredir_server_device_disconnect_ack;
s->parser->interface_info_func = usbredir_server_interface_info;
+ s->parser->start_interrupt_receiving_func =
+ usbredir_server_start_interrupt_receiving;
+ s->parser->stop_interrupt_receiving_func =
+ usbredir_server_stop_interrupt_receiving;
s->parser->alloc_bulk_streams_func = usbredir_server_alloc_bulk_streams;
s->parser->cancel_data_packet_func = usbredir_server_cancel_data_packet;
s->parser->start_bulk_receiving_func =
@@ -1292,6 +1526,10 @@ static void usbredir_server_realize(DeviceState *dev, Error **errp)
s->announce_timer = timer_new_ms(QEMU_CLOCK_VIRTUAL,
usbredir_server_do_announce, s);
+ s->intr_retry = timer_new_ns(QEMU_CLOCK_VIRTUAL,
+ usbredir_server_intr_kick, s);
+ s->intr_bh = qemu_bh_new_guarded(usbredir_server_intr_kick, s,
+ &dev->mem_reentrancy_guard);
s->chardev_close_bh = qemu_bh_new_guarded(usbredir_server_chardev_close_bh,
s, &dev->mem_reentrancy_guard);
@@ -1311,6 +1549,16 @@ static void usbredir_server_unrealize(DeviceState *dev)
timer_free(s->announce_timer);
+ if (s->intr_retry) {
+ timer_free(s->intr_retry);
+ s->intr_retry = NULL;
+ }
+
+ if (s->intr_bh) {
+ qemu_bh_delete(s->intr_bh);
+ s->intr_bh = NULL;
+ }
+
if (s->chardev_close_bh) {
qemu_bh_delete(s->chardev_close_bh);
s->chardev_close_bh = NULL;
diff --git a/hw/usb/trace-events b/hw/usb/trace-events
index c9ab80c6ba..931996e8cd 100644
--- a/hw/usb/trace-events
+++ b/hw/usb/trace-events
@@ -416,3 +416,7 @@ usbredir_server_bulk_complete(uint64_t id, uint8_t ep, int status, int actual) "
usbredir_server_bulk_too_big(uint64_t id, uint8_t ep, uint32_t len) "id %" PRIu64 " ep 0x%02x len %u"
usbredir_server_interrupt(uint64_t id, uint8_t ep, size_t len) "id %" PRIu64 " ep 0x%02x len %zu"
usbredir_server_intr_complete(uint64_t id, uint8_t ep, int status, int actual) "id %" PRIu64 " ep 0x%02x status %d actual %d"
+usbredir_server_intr_start(uint8_t ep, int status) "start_interrupt_receiving ep 0x%02x -> status %d"
+usbredir_server_intr_stop(uint8_t ep) "stop_interrupt_receiving ep 0x%02x"
+usbredir_server_intr_park(unsigned ep_nr, int len) "parked IN packet ep %u len %d"
+usbredir_server_intr_stream_complete(unsigned ep_nr, int status, int actual) "ep %u status %d actual %d"
--
2.43.0
next prev parent reply other threads:[~2026-10-05 3:22 UTC|newest]
Thread overview: 19+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-05 3:20 [PATCH v2 0/8] hw/usb: Add a usbredir server transport Jamin Lin
2026-10-05 3:20 ` [PATCH v2 1/8] hw/usb/bus: Let a bus opt out of automatic hub insertion Jamin Lin
2026-10-05 8:59 ` marcandre.lureau
2026-10-05 3:20 ` [PATCH v2 2/8] hw/usb/redirect-server: Add a usbredir server device Jamin Lin
2026-10-05 8:59 ` marcandre.lureau
2026-10-07 2:15 ` Jamin Lin
2026-10-07 6:11 ` Marc-André Lureau
2026-10-07 8:43 ` Jamin Lin
2026-10-05 3:20 ` [PATCH v2 3/8] hw/usb/redirect-server: Connect usbredirparser to a chardev Jamin Lin
2026-10-05 8:59 ` marcandre.lureau
2026-10-05 3:20 ` [PATCH v2 4/8] hw/usb/redirect-server: Announce the exported device Jamin Lin
2026-10-05 8:59 ` marcandre.lureau
2026-10-07 7:32 ` Jamin Lin
2026-10-05 3:20 ` [PATCH v2 5/8] hw/usb/redirect-server: Implement control transfers Jamin Lin
2026-10-05 3:20 ` [PATCH v2 6/8] hw/usb/redirect-server: Implement bulk and interrupt transfers Jamin Lin
2026-10-05 3:20 ` Jamin Lin [this message]
2026-10-05 3:20 ` [PATCH v2 8/8] hw/arm/aspeed: Enable the usbredir server transport Jamin Lin
2026-10-05 8:59 ` [PATCH v2 0/8] hw/usb: Add a " marcandre.lureau
2026-10-07 8:42 ` Jamin Lin
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=20261005032022.3980903-8-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=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 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.