[PATCH 4/6] USB: serial: ftdi_sio: add per-port low_latency sysfs attribute

From: Chinna Mopurigari Naveen Kumar Reddy

Date: Mon Jun 22 2026 - 03:43:03 EST


When the inter-batch read-URB defer added by the previous patch is
enabled globally (urb_defer_timer_ns != 0), every port served by
this driver pays a small read-side latency. In multi-channel
products it is common for one or more channels to be
latency-critical while others tolerate the defer; today there is
no way to express that on a per-port basis.

Add a new writable boolean sysfs attribute, low_latency, on every
ftdi_sio port:

/sys/bus/usb-serial/devices/ttyUSBx/low_latency
0 (default): inter-batch defer applies to this port when the
defer is globally enabled.
1 : defer is bypassed on this port; the generic
read-bulk callback handles URB completions
directly with no throttle / hrtimer interaction.

The implementation adds a bool low_latency field to struct
ftdi_private (zero-initialised by kzalloc), settable via the new
attribute. ftdi_read_bulk_callback() short-circuits to
usb_serial_generic_read_bulk_callback() at the top of the function
when the flag is set, skipping the throttle/hrtimer path entirely.

Writing 1 also cancels any in-flight defer hrtimer and clears the
timer_active bookkeeping so the next URB completion takes the
direct path immediately.

A udev rule typically sets the flag at device-add for the
latency-critical port; other ports are unaffected.

Signed-off-by: Chinna Mopurigari Naveen Kumar Reddy <naveen.reddy@xxxxxxxxxxxx>
---
drivers/usb/serial/ftdi_sio.c | 66 +++++++++++++++++++++++++++++++++++
1 file changed, 66 insertions(+)

diff --git a/drivers/usb/serial/ftdi_sio.c b/drivers/usb/serial/ftdi_sio.c
index c0cda19f074a..8faa073f1383 100644
--- a/drivers/usb/serial/ftdi_sio.c
+++ b/drivers/usb/serial/ftdi_sio.c
@@ -110,6 +110,15 @@ struct ftdi_private {
struct hrtimer urb_defer_timer;
struct tty_struct *urb_defer_tty;
bool urb_defer_timer_active;
+ /*
+ * Per-port opt-out of the inter-batch defer. When true, set via
+ * /sys/bus/usb-serial/devices/ttyUSBx/low_latency, the read-bulk
+ * callback short-circuits to the generic upstream callback and
+ * skips the hrtimer-based throttle. Defaults to false: the
+ * defer applies to every port unless this port is explicitly
+ * opted out (and the defer is globally enabled to begin with).
+ */
+ bool low_latency;
#ifdef CONFIG_GPIOLIB
struct gpio_chip gc;
struct mutex gpio_lock; /* protects GPIO state */
@@ -1728,6 +1737,51 @@ static ssize_t latency_timer_store(struct device *dev,
}
static DEVICE_ATTR_RW(latency_timer);

+/*
+ * /sys/bus/usb-serial/devices/ttyUSBx/low_latency
+ * 0 (default): inter-batch defer applies to this port (only
+ * meaningful when the defer is globally enabled
+ * via urb_defer_timer_ns).
+ * 1 : defer bypassed on this port; the upstream
+ * read-bulk callback handles URB completions with
+ * no throttle / hrtimer interaction.
+ * Typically written by a udev rule on the latency-critical port at
+ * device-add. Writing 1 also cancels any in-flight defer timer so
+ * the next URB submits immediately.
+ */
+static ssize_t low_latency_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct usb_serial_port *port = to_usb_serial_port(dev);
+ struct ftdi_private *priv = usb_get_serial_port_data(port);
+
+ return sprintf(buf, "%u\n", priv->low_latency ? 1 : 0);
+}
+
+static ssize_t low_latency_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *valbuf, size_t count)
+{
+ struct usb_serial_port *port = to_usb_serial_port(dev);
+ struct ftdi_private *priv = usb_get_serial_port_data(port);
+ unsigned long flags;
+ u8 v;
+
+ if (kstrtou8(valbuf, 10, &v))
+ return -EINVAL;
+
+ priv->low_latency = !!v;
+
+ if (priv->low_latency) {
+ hrtimer_cancel(&priv->urb_defer_timer);
+ spin_lock_irqsave(&priv->urb_defer_lock, flags);
+ priv->urb_defer_timer_active = false;
+ spin_unlock_irqrestore(&priv->urb_defer_lock, flags);
+ }
+ return count;
+}
+static DEVICE_ATTR_RW(low_latency);
+
/* Write an event character directly to the FTDI register. The ASCII
value is in the low 8 bits, with the enable bit in the 9th bit. */
static ssize_t event_char_store(struct device *dev,
@@ -1762,6 +1816,7 @@ static DEVICE_ATTR_WO(event_char);
static struct attribute *ftdi_attrs[] = {
&dev_attr_event_char.attr,
&dev_attr_latency_timer.attr,
+ &dev_attr_low_latency.attr,
NULL
};

@@ -2763,6 +2818,17 @@ static void ftdi_read_bulk_callback(struct urb *urb)
return;
}

+ /*
+ * Latency-critical ports opt out of the inter-batch defer via
+ * /sys/.../low_latency. Delegate straight to the generic
+ * callback so the data stream sees no added latency on this
+ * port even when the defer is globally enabled.
+ */
+ if (priv->low_latency) {
+ usb_serial_generic_read_bulk_callback(urb);
+ return;
+ }
+
for (i = 0; i < ARRAY_SIZE(port->read_urbs); ++i) {
if (urb == port->read_urbs[i])
break;
--
2.43.0