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: 20+ 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
2026-10-12 1:28 ` 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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox