Re: [PATCH v2 3/4] USB: serial: mxuport: handle SEND_NEXT transmit flow control

From: Crescent Hsieh

Date: Mon Jul 27 2026 - 06:31:39 EST


On Tue, Jul 21, 2026 at 05:51:58PM +0200, Johan Hovold wrote:
> On Tue, Jun 23, 2026 at 04:01:38PM +0800, Crescent Hsieh wrote:
> > The device uses the SEND_NEXT event to pace host-to-device transmission.
> > Without waiting for this event, continuous transmission on multiple
> > ports can make the driver submit bulk-out URBs faster than the device
> > can process them, which can result in data errors during burn-in
> > testing.
>
> Why does it matter if we're sending data for other ports? Isn't the
> problem that we're sending more data than the per-port buffer can hold?

You're right. The commit message is incorrect, as the issue can also be
reproduced on a single port. The actual problem is overflow of the
per-port buffer. I will correct this in v3.

>
> And is this an issue with all firmware versions, including the ones used
> for the existing gen1 devices?
>

Yes. This issue is not specific to the G2 and Platform firmware; This
could also be reproduced on existing G1 devices.

> > Stop submitting further write URBs after requesting SEND_NEXT and resume
> > transmission when the matching event is received.
>
> How exactly does SEND_NEXT work? Is it signalled when the per-port
> buffer drops below some threshold?

SEND_NEXT is initiated by the driver on a per-port basis. The driver
sets the SEND_NEXT request bit in a bulk-out frame and then stops
submitting further data for that port. The firmware sends a SEND_NEXT
event when the corresponding per-port buffer becomes empty, The driver
then resumes transmission.

> > @@ -276,22 +280,148 @@ MODULE_DEVICE_TABLE(usb, mxuport_idtable);
> > static int mxuport_prepare_write_buffer(struct usb_serial_port *port,
> > void *dest, size_t size)
> > {
> > + struct mxuport_port *mxport = usb_get_serial_port_data(port);
> > u8 *buf = dest;
> > + unsigned long flags;
> > + bool request_send_next;
> > int count;
> >
> > - count = kfifo_out_locked(&port->write_fifo, buf + HEADER_SIZE,
> > - size - HEADER_SIZE,
> > - &port->lock);
> > + spin_lock_irqsave(&port->lock, flags);
> > + count = kfifo_out(&port->write_fifo, buf + HEADER_SIZE,
> > + size - HEADER_SIZE);
> > + mxport->sent_payload += count;
> > + request_send_next = mxport->sent_payload >= port->bulk_out_size;
>
> How big are the per-port buffers?

The size depends on the device. The G2 and Platform UART firmware have
a 4096-byte buffer per port, while the G1 per-port buffer ranges from
32 KiB to 256 KiB depending on the number of ports.

The bulk_out_size threshold is not derived from the firmware buffer
size. It is used as a simple and conservative pacing interval that
works across the device families without requiring family-specific
buffer sizes in the driver. This results in more frequent
SEND_NEXT waits and may reduce throughput.

>
> > + if (request_send_next)
> > + mxport->hold_reason |= MX_WAIT_FOR_SEND_NEXT;
> > + spin_unlock_irqrestore(&port->lock, flags);
> >
> > put_unaligned_be16(port->port_number, buf);
> > put_unaligned_be16(count, buf + 2);
> >
> > + if (request_send_next)
> > + buf[0] |= UPORT_REQUEST_SEND_NEXT;
> > +
> > dev_dbg(&port->dev, "%s - size %zd count %d\n", __func__,
> > size, count);
> >
> > return count + HEADER_SIZE;
> > }
> >
> > +static int mxuport_write_start(struct usb_serial_port *port, gfp_t mem_flags)
> > +{
> > + struct mxuport_port *mxport = usb_get_serial_port_data(port);
> > + struct urb *urb;
> > + unsigned long flags;
> > + int i;
> > + int count;
> > + int result;
> > +
> > + if (test_and_set_bit_lock(USB_SERIAL_WRITE_BUSY, &port->flags))
> > + return 0;
> > +retry:
> > + spin_lock_irqsave(&port->lock, flags);
> > + if ((mxport->hold_reason & MX_WAIT_FOR_SEND_NEXT) ||
> > + !port->write_urbs_free || !kfifo_len(&port->write_fifo)) {
> > + clear_bit_unlock(USB_SERIAL_WRITE_BUSY, &port->flags);
> > + spin_unlock_irqrestore(&port->lock, flags);
> > + return 0;
> > + }
> > +
> > + i = (int)find_first_bit(&port->write_urbs_free,
> > + ARRAY_SIZE(port->write_urbs));
> > + spin_unlock_irqrestore(&port->lock, flags);
> > +
> > + urb = port->write_urbs[i];
> > + count = mxuport_prepare_write_buffer(port, urb->transfer_buffer,
> > + port->bulk_out_size);
> > + urb->transfer_buffer_length = count;
> > + usb_serial_debug_data(&port->dev, __func__, count,
> > + urb->transfer_buffer);
> > +
> > + spin_lock_irqsave(&port->lock, flags);
> > + port->tx_bytes += count;
> > + spin_unlock_irqrestore(&port->lock, flags);
> > +
> > + clear_bit(i, &port->write_urbs_free);
> > + result = usb_submit_urb(urb, mem_flags);
> > + if (result) {
> > + dev_err_console(port, "%s - error submitting urb: %d\n",
> > + __func__, result);
> > + set_bit(i, &port->write_urbs_free);
> > + spin_lock_irqsave(&port->lock, flags);
> > + port->tx_bytes -= count;
>
> > + if (mxport->hold_reason & MX_WAIT_FOR_SEND_NEXT) {
> > + mxport->hold_reason &= ~MX_WAIT_FOR_SEND_NEXT;
> > + mxport->sent_payload = 0;
> > + }
>
> Shouldn't you undo the effects of prepare_write_buffer() and subtract
> count from sent_payload (and clear the flag) unconditionally?
>
> > + spin_unlock_irqrestore(&port->lock, flags);
> > +
> > + clear_bit_unlock(USB_SERIAL_WRITE_BUSY, &port->flags);
> > + return result;
> > + }
> > +
> > + goto retry;
> > +}
>
> This is a more or less verbatim copy of the generic write
> implementation. If we go this way you should at least mention that you
> copied it in the commit message, but perhaps we should try to find a way
> to generalise it instead.
>
> Also, if you really need a custom implementation to throttle writes,
> then shouldn't using one URB be enough? The other one is essentially
> there to allow for higher throughput which we need to give up for
> correctness here anyway.

Agreed, the custom write path is largely based on the generic write
implementation. In v1, I attempted to implement the throttling in
prepare_write_buffer() to avoid changing the generic path, but
returning zero caused the generic implementation to submit a
zero-length URB. In v2, I therefore added a custom write path so that
submission could be stopped before preparing another URB.

I will investigate whether this can instead be generalised through a
small driver callback that allows the generic write path to check
whether another URB may be submitted. This would avoid duplicating the
generic implementation while preserving the behaviour of other
USB-to-serial drivers.

I will also describe the SEND_NEXT behaviour in more detail in the v3
commit message.

---
Sincerely,
Crescent Hsieh