* [PATCH 00/13] HID: ft260: add UART and GPIO support, plus I2C fixes
@ 2026-08-22 21:39 Michael Zaidman
2026-08-22 21:39 ` [PATCH 01/13] HID: ft260: add serial driver Michael Zaidman
` (13 more replies)
0 siblings, 14 replies; 43+ messages in thread
From: Michael Zaidman @ 2026-08-22 21:39 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: Linus Walleij, Bartosz Golaszewski, Germain Hebert, Rio Liu,
Bruno Giacomazzi, Christina Quast, linux-input, linux-gpio,
linux-i2c, linux-kernel, Michael Zaidman
Mainline supports only the FT260's I2C bridge. This series adds the
UART/TTY interface and GPIO on top of it, and carries the I2C fixes made
alongside them - two years of out-of-tree work [1], now replayed upstream.
It picks up work that stalled twice: Christina Quast's serial driver,
posted as v4 and v5 and never applied, and my own pre-UART GPIO series
from 2023. Patches 1 and 3 link both threads. This is a new series rather
than a v6, being much more than the serial driver now.
The patches keep the order and the granularity in which the work was
developed, so that authorship stays visible: squashing would fold several
people's changes under one name. The UART and I2C changes also interleave
in the code, so regrouping them by interface would mean a rewrite and a
full retest. Only adjacent commits were squashed. Patch 1 is Christina's,
patch 10 is Rio Liu's.
Tested on a UMFT260EV1A with a 24LC512 EEPROM. I2C - reads and writes
that cross the 60-byte write chunk and the 60/180-byte read chunk caps,
SMBus byte-data and block reads, and the shortened read timeout with the
abort path of patch 13. UART - a 2.8 MB pattern in both directions at
115200 and 1.5 Mbaud against an FT2232 peer, without flow control and
with XON/XOFF and RTS/CTS, plus DTR/RTS toggling over TIOCM*. GPIO -
through libgpiod and legacy sysfs. Every commit builds with W=1, and
checkpatch --strict reports no errors and no warnings across the series.
[1] https://github.com/MichaelZaidman/hid-ft260
Christina Quast (1):
HID: ft260: add serial driver
Michael Zaidman (11):
HID: ft260: uart: bring-up fixes
HID: ft260: add GPIO support on top of UART
HID: ft260: i2c: reduce driver module loading time
HID: ft260: i2c: silence sysfs store big-numbers
HID: ft260: i2c: reduce bus-error message severity
HID: ft260: uart: enable flow control
HID: ft260: uart: add modem pins control via ioctl
HID: ft260: gpio: group sysfs attrs per HID interface
HID: ft260: i2c: fix large write transaction failure
HID: ft260: workaround for TN_189 errata endpoint STALL after
enumeration
HID: ft260: i2c: abort in-flight transfers with STOP before reset
Rio Liu (1):
HID: ft260: uart: fix active-low RTS/CTS/DTR/DSR polarity
drivers/hid/Kconfig | 10 +-
drivers/hid/hid-ft260.c | 1967 ++++++++++++++++++++++++++++++++++++---
2 files changed, 1830 insertions(+), 147 deletions(-)
--
2.43.0
^ permalink raw reply [flat|nested] 43+ messages in thread
* [PATCH 01/13] HID: ft260: add serial driver
2026-08-22 21:39 [PATCH 00/13] HID: ft260: add UART and GPIO support, plus I2C fixes Michael Zaidman
@ 2026-08-22 21:39 ` Michael Zaidman
2026-08-22 22:00 ` sashiko-bot
` (2 more replies)
2026-08-22 21:39 ` [PATCH 02/13] HID: ft260: uart: bring-up fixes Michael Zaidman
` (12 subsequent siblings)
13 siblings, 3 replies; 43+ messages in thread
From: Michael Zaidman @ 2026-08-22 21:39 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: Linus Walleij, Bartosz Golaszewski, Germain Hebert, Rio Liu,
Bruno Giacomazzi, Christina Quast, linux-input, linux-gpio,
linux-i2c, linux-kernel, Daniel Beer, Michael Zaidman
From: Christina Quast <contact@christina-quast.de>
Add a TTY serial interface (/dev/ttyFTx) for FT260 USB HID devices,
providing baud rate configuration, data transfer, and termios settings
without requiring userspace HID report formatting.
The FT260 exposes I2C and UART through separate HID interfaces. Only
I2C was supported previously; UART required FTDI hidraw example code.
This is a port of the v4 patch, adapted to the I2C hardening that has
since been merged upstream: input-report length validation, the
read_buf/raw_event race fix with read_lock, and SMBus block-read
START/STOP handling. HID_FT260 now depends on TTY because the UART code
is part of this driver.
Routing UART reports through ft260_raw_event() also made v4 fall through
the report dispatch after an I2C input report had already been consumed,
so every I2C input report was additionally reported as unhandled. This
regression is fixed here.
Ported from:
https://lore.kernel.org/all/20231218093153.192268-1-contact@christina-quast.de/
The follow-up v5 series stalled without being applied:
https://lore.kernel.org/linux-input/20240216-ft260_review5-v5-1-36db44673ac7@christina-quast.de/
Signed-off-by: Christina Quast <contact@christina-quast.de>
Co-developed-by: Daniel Beer <daniel.beer@igorinstitute.com>
Signed-off-by: Daniel Beer <daniel.beer@igorinstitute.com>
Co-developed-by: Michael Zaidman <michael.zaidman@gmail.com>
Signed-off-by: Michael Zaidman <michael.zaidman@gmail.com>
---
drivers/hid/Kconfig | 10 +-
drivers/hid/hid-ft260.c | 842 +++++++++++++++++++++++++++++++++++++---
2 files changed, 790 insertions(+), 62 deletions(-)
diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig
index aa7fa11a0197..e898adb331ba 100644
--- a/drivers/hid/Kconfig
+++ b/drivers/hid/Kconfig
@@ -386,12 +386,12 @@ config HID_EZKEY
Support for Ezkey BTC 8193 keyboard.
config HID_FT260
- tristate "FTDI FT260 USB HID to I2C host support"
- depends on USB_HID && HIDRAW && I2C
+ tristate "FTDI FT260 USB HID to I2C/UART host support"
+ depends on USB_HID && HIDRAW && I2C && TTY
help
- Provides I2C host adapter functionality over USB-HID through FT260
- device. The customizable USB descriptor fields are exposed as sysfs
- attributes.
+ Provides I2C host adapter and UART/TTY functionality over USB-HID
+ through the FT260 device. The customizable USB descriptor fields
+ are exposed as sysfs attributes.
To compile this driver as a module, choose M here: the module
will be called hid-ft260.
diff --git a/drivers/hid/hid-ft260.c b/drivers/hid/hid-ft260.c
index 64c9c940db46..3faedfc5c413 100644
--- a/drivers/hid/hid-ft260.c
+++ b/drivers/hid/hid-ft260.c
@@ -13,6 +13,16 @@
#include <linux/i2c.h>
#include <linux/module.h>
#include <linux/usb.h>
+#include <linux/serial.h>
+#include <linux/serial_core.h>
+#include <linux/kfifo.h>
+#include <linux/tty_flip.h>
+#include <linux/minmax.h>
+#include <linux/unaligned.h>
+
+#define UART_COUNT_MAX 4 /* Number of UARTs this driver can handle */
+#define FIFO_SIZE 256
+#define TTY_WAKEUP_WATERMARK (FIFO_SIZE / 2)
#ifdef DEBUG
static int ft260_debug = 1;
@@ -30,6 +40,7 @@ MODULE_PARM_DESC(debug, "Toggle FT260 debugging messages");
#define FT260_REPORT_MAX_LENGTH (64)
#define FT260_I2C_DATA_REPORT_ID(len) (FT260_I2C_REPORT_MIN + (len - 1) / 4)
+#define FT260_UART_DATA_REPORT_ID(len) (FT260_UART_REPORT_MIN + (len - 1) / 4)
#define FT260_WAKEUP_NEEDED_AFTER_MS (4800) /* 5s minus 200ms margin */
@@ -43,7 +54,7 @@ MODULE_PARM_DESC(debug, "Toggle FT260 debugging messages");
* or optoe limit the i2c reads to 128 bytes. To not block other drivers out
* of I2C for potentially troublesome amounts of time, we select the maximum
* read payload length to be 180 bytes.
-*/
+ */
#define FT260_RD_DATA_MAX (180)
#define FT260_WR_DATA_MAX (60)
@@ -81,7 +92,8 @@ enum {
FT260_UART_INTERRUPT_STATUS = 0xB1,
FT260_UART_STATUS = 0xE0,
FT260_UART_RI_DCD_STATUS = 0xE1,
- FT260_UART_REPORT = 0xF0,
+ FT260_UART_REPORT_MIN = 0xF0,
+ FT260_UART_REPORT_MAX = 0xFE,
};
/* Feature Out */
@@ -132,6 +144,13 @@ enum {
FT260_FLAG_START_STOP_REPEATED = 0x07,
};
+/* Return values for ft260_get_interface_type func */
+enum {
+ FT260_IFACE_NONE,
+ FT260_IFACE_I2C,
+ FT260_IFACE_UART
+};
+
#define FT260_SET_REQUEST_VALUE(report_id) ((FT260_FEATURE << 8) | report_id)
/* Feature In reports */
@@ -220,12 +239,59 @@ struct ft260_i2c_read_request_report {
__le16 length; /* data payload length */
} __packed;
-struct ft260_i2c_input_report {
- u8 report; /* FT260_I2C_REPORT */
+struct ft260_input_report {
+ u8 report; /* FT260_I2C_REPORT or FT260_UART_REPORT */
u8 length; /* data payload length */
u8 data[2]; /* data payload */
} __packed;
+/* UART reports */
+struct ft260_uart_write_request_report {
+ u8 report; /* FT260_UART_REPORT */
+ u8 length; /* data payload length */
+ u8 data[] __counted_by(length); /* variable data payload */
+} __packed;
+
+struct ft260_configure_uart_request {
+ u8 report; /* FT260_SYSTEM_SETTINGS */
+ u8 request; /* FT260_SET_UART_CONFIG */
+ u8 flow_ctrl; /* 0: OFF, 1: RTS_CTS, 2: DTR_DSR */
+ /* 3: XON_XOFF, 4: No flow ctrl */
+ /* The baudrate field is unaligned: */
+ __le32 baudrate; /* little endian, 9600 = 0x2580, 19200 = 0x4B00 */
+ u8 data_bit; /* 7 or 8 */
+ u8 parity; /* 0: no parity, 1: odd, 2: even, 3: high, 4: low */
+ u8 stop_bit; /* 0: one stop bit, 2: 2 stop bits */
+ u8 breaking; /* 0: no break */
+} __packed;
+
+/* UART interface configuration */
+enum {
+ FT260_CFG_FLOW_CTRL_OFF = 0x00,
+ FT260_CFG_FLOW_CTRL_RTS_CTS = 0x01,
+ FT260_CFG_FLOW_CTRL_DTR_DSR = 0x02,
+ FT260_CFG_FLOW_CTRL_XON_XOFF = 0x03,
+ FT260_CFG_FLOW_CTRL_NONE = 0x04,
+
+ FT260_CFG_DATA_BITS_7 = 0x07,
+ FT260_CFG_DATA_BITS_8 = 0x08,
+
+ FT260_CFG_PAR_NO = 0x00,
+ FT260_CFG_PAR_ODD = 0x01,
+ FT260_CFG_PAR_EVEN = 0x02,
+ FT260_CFG_PAR_HIGH = 0x03,
+ FT260_CFG_PAR_LOW = 0x04,
+
+ FT260_CFG_STOP_ONE_BIT = 0x00,
+ FT260_CFG_STOP_TWO_BIT = 0x02,
+
+ FT260_CFG_BREAKING_NO = 0x00,
+ FT260_CFG_BEAKING_YES = 0x01,
+
+ FT260_CFG_BAUD_MIN = 1200,
+ FT260_CFG_BAUD_MAX = 12000000,
+};
+
static const struct hid_device_id ft260_devices[] = {
{ HID_USB_DEVICE(USB_VENDOR_ID_FUTURE_TECHNOLOGY,
USB_DEVICE_ID_FT260) },
@@ -236,6 +302,23 @@ MODULE_DEVICE_TABLE(hid, ft260_devices);
struct ft260_device {
struct i2c_adapter adap;
struct hid_device *hdev;
+
+ bool ft260_is_serial;
+ struct list_head device_list;
+
+ /* tty_port lifetime is equal to device lifetime */
+ struct tty_port port;
+ unsigned int index;
+ struct kfifo xmit_fifo;
+ /* write_lock: lock to serialize access to xmit fifo */
+ spinlock_t write_lock;
+ struct uart_icount icount;
+
+ struct timer_list wakeup_timer;
+ struct work_struct wakeup_work;
+ bool reschedule_work;
+
+
struct completion wait;
struct mutex lock;
u8 write_buf[FT260_REPORT_MAX_LENGTH];
@@ -379,7 +462,7 @@ static int ft260_hid_output_report_check_status(struct ft260_device *dev,
ret = ft260_hid_output_report(hdev, data, len);
if (ret < 0) {
- hid_err(hdev, "%s: failed to start transfer, ret %d\n",
+ hid_dbg(hdev, "%s: failed to start transfer, ret %d\n",
__func__, ret);
ft260_i2c_reset(hdev);
return ret;
@@ -609,7 +692,7 @@ static int ft260_i2c_write_read(struct ft260_device *dev, struct i2c_msg *msgs)
else
read_off = *msgs[0].buf;
- pr_info("%s: off %#x rlen %d wlen %d\n", __func__,
+ ft260_dbg("%s: off %#x rlen %d wlen %d\n", __func__,
read_off, rd_len, wr_len);
}
@@ -826,7 +909,7 @@ static int ft260_get_system_config(struct hid_device *hdev,
return 0;
}
-static int ft260_is_interface_enabled(struct hid_device *hdev)
+static int ft260_get_interface_type(struct hid_device *hdev, struct ft260_device *dev)
{
struct ft260_get_system_status_report cfg;
struct usb_interface *usbif = to_usb_interface(hdev->dev.parent);
@@ -843,21 +926,27 @@ static int ft260_is_interface_enabled(struct hid_device *hdev)
ft260_dbg("i2c_enable: 0x%02x\n", cfg.i2c_enable);
ft260_dbg("uart_mode: 0x%02x\n", cfg.uart_mode);
+ dev->ft260_is_serial = false;
+
switch (cfg.chip_mode) {
case FT260_MODE_ALL:
case FT260_MODE_BOTH:
- if (interface == 1)
- hid_info(hdev, "uart interface is not supported\n");
- else
- ret = 1;
+ if (interface == 1) {
+ ret = FT260_IFACE_UART;
+ dev->ft260_is_serial = true;
+ } else {
+ ret = FT260_IFACE_I2C;
+ }
break;
case FT260_MODE_UART:
- hid_info(hdev, "uart interface is not supported\n");
+ ret = FT260_IFACE_UART;
+ dev->ft260_is_serial = true;
break;
case FT260_MODE_I2C:
- ret = 1;
+ ret = FT260_IFACE_I2C;
break;
}
+
return ret;
}
@@ -1001,6 +1090,583 @@ static const struct attribute_group ft260_attr_group = {
}
};
+/***
+ * START Serial dev part
+ */
+static DEFINE_MUTEX(ft260_uart_list_lock);
+static LIST_HEAD(ft260_uart_device_list);
+
+static struct ft260_device *ft260_dev_by_index(int index)
+{
+ struct ft260_device *port;
+
+ list_for_each_entry(port, &ft260_uart_device_list, device_list) {
+ if (index == port->index)
+ return port;
+ }
+ return NULL;
+}
+
+static int ft260_uart_add_port(struct ft260_device *port)
+{
+ int index = 0, ret = 0;
+ struct ft260_device *dev;
+
+ spin_lock_init(&port->write_lock);
+ if (kfifo_alloc(&port->xmit_fifo, FIFO_SIZE, GFP_KERNEL))
+ return -ENOMEM;
+
+ mutex_lock(&ft260_uart_list_lock);
+ list_for_each_entry(dev, &ft260_uart_device_list, device_list) {
+ if (dev->index != index)
+ break;
+ index++;
+ }
+
+ port->index = index;
+ list_add(&port->device_list, &ft260_uart_device_list);
+ mutex_unlock(&ft260_uart_list_lock);
+
+ return ret;
+}
+
+static void ft260_uart_port_put(struct ft260_device *port)
+{
+ tty_port_put(&port->port);
+}
+
+static void ft260_uart_port_remove(struct ft260_device *port)
+{
+ timer_delete_sync(&port->wakeup_timer);
+
+ mutex_lock(&ft260_uart_list_lock);
+ list_del(&port->device_list);
+ mutex_unlock(&ft260_uart_list_lock);
+
+ spin_lock(&port->write_lock);
+ kfifo_free(&port->xmit_fifo);
+ spin_unlock(&port->write_lock);
+
+ mutex_lock(&port->port.mutex);
+ port->reschedule_work = false;
+ tty_port_tty_hangup(&port->port, false);
+ mutex_unlock(&port->port.mutex);
+
+ ft260_uart_port_put(port);
+}
+
+static struct ft260_device *ft260_uart_port_get(unsigned int index)
+{
+ struct ft260_device *port;
+
+ if (index >= UART_COUNT_MAX)
+ return NULL;
+
+ mutex_lock(&ft260_uart_list_lock);
+ port = ft260_dev_by_index(index);
+ if (port)
+ tty_port_get(&port->port);
+ mutex_unlock(&ft260_uart_list_lock);
+
+ return port;
+}
+
+static int ft260_uart_open(struct tty_struct *tty, struct file *filp)
+{
+ int ret;
+ struct ft260_device *port = tty->driver_data;
+
+ ret = tty_port_open(&port->port, tty, filp);
+
+ return ret;
+}
+
+static void ft260_uart_close(struct tty_struct *tty, struct file *filp)
+{
+ struct ft260_device *port = tty->driver_data;
+
+ tty_port_close(&port->port, tty, filp);
+}
+
+static void ft260_uart_hangup(struct tty_struct *tty)
+{
+ struct ft260_device *port = tty->driver_data;
+
+ tty_port_hangup(&port->port);
+}
+
+static int ft260_uart_transmit_chars(struct ft260_device *port)
+{
+ struct hid_device *hdev = port->hdev;
+ struct kfifo *xmit = &port->xmit_fifo;
+ struct tty_struct *tty;
+ struct ft260_uart_write_request_report *rep;
+ int len, data_len, ret = 0;
+
+ tty = tty_port_tty_get(&port->port);
+
+ data_len = kfifo_len(xmit);
+ if (!tty || !data_len) {
+ ret = -EINVAL;
+ goto tty_out;
+ }
+
+ rep = (struct ft260_uart_write_request_report *)port->write_buf;
+
+ do {
+ len = min(data_len, FT260_WR_DATA_MAX);
+
+ rep->report = FT260_UART_DATA_REPORT_ID(len);
+ rep->length = len;
+
+ len = kfifo_out_locked(xmit, rep->data, len, &port->write_lock);
+
+ ret = ft260_hid_output_report(hdev, (u8 *)rep, len + sizeof(*rep));
+ if (ret < 0) {
+ hid_err(hdev, "Failed to start transfer, ret %d\n", ret);
+ goto tty_out;
+ }
+
+ data_len -= len;
+ port->icount.tx += len;
+ } while (data_len > 0);
+
+ len = kfifo_len(xmit);
+ if ((FIFO_SIZE - len) > TTY_WAKEUP_WATERMARK)
+ tty_wakeup(tty);
+
+ ret = 0;
+
+tty_out:
+ tty_kref_put(tty);
+ return ret;
+}
+
+static int ft260_uart_receive_chars(struct ft260_device *port,
+ u8 *data, u8 length)
+{
+ struct hid_device *hdev = port->hdev;
+ int ret = 0;
+
+ if (length > FT260_RD_DATA_MAX) {
+ hid_err(hdev, "Received too much data (%d)\n", length);
+ return -EBADR;
+ }
+
+ ret = tty_insert_flip_string(&port->port, data, length);
+ if (ret != length)
+ hid_err(hdev, "%d char not inserted to flip buffer\n", length - ret);
+ port->icount.rx += ret;
+
+ if (ret)
+ tty_flip_buffer_push(&port->port);
+
+ return ret;
+}
+
+static ssize_t ft260_uart_write(struct tty_struct *tty, const u8 *buf,
+ size_t count)
+{
+ struct ft260_device *port = tty->driver_data;
+ struct hid_device *hdev = port->hdev;
+ int len, ret;
+
+ len = kfifo_in_locked(&port->xmit_fifo, buf, count, &port->write_lock);
+ ft260_dbg("count: %zu, len: %d", count, len);
+
+ ret = ft260_uart_transmit_chars(port);
+ if (ret < 0) {
+ hid_dbg(hdev, "Failed to transmit chars: %d\n", ret);
+ return 0;
+ }
+
+ ret = kfifo_len(&port->xmit_fifo);
+ if (ret > 0) {
+ hid_dbg(hdev, "Failed to all kfifo data bytes\n");
+ ft260_dbg("return: %d", len - ret);
+ return len - ret;
+ }
+
+ return len;
+}
+
+static unsigned int ft260_uart_write_room(struct tty_struct *tty)
+{
+ struct ft260_device *port = tty->driver_data;
+
+ return FIFO_SIZE - kfifo_len(&port->xmit_fifo);
+}
+
+static unsigned int ft260_uart_chars_in_buffer(struct tty_struct *tty)
+{
+ struct ft260_device *port = tty->driver_data;
+
+ return kfifo_len(&port->xmit_fifo);
+}
+
+static int ft260_uart_change_speed(struct ft260_device *port,
+ struct ktermios *termios,
+ struct ktermios *old)
+{
+ struct hid_device *hdev = port->hdev;
+ unsigned int baud;
+ struct ft260_configure_uart_request req;
+ int ret;
+
+ memset(&req, 0, sizeof(req));
+
+ req.report = FT260_SYSTEM_SETTINGS;
+ req.request = FT260_SET_UART_CONFIG;
+
+ switch (termios->c_cflag & CSIZE) {
+ case CS7:
+ req.data_bit = FT260_CFG_DATA_BITS_7;
+ break;
+ case CS5:
+ case CS6:
+ hid_err(hdev, "Invalid data bit size, setting to default (8 bit)\n");
+ req.data_bit = FT260_CFG_DATA_BITS_8;
+ termios->c_cflag &= ~CSIZE;
+ termios->c_cflag |= CS8;
+ break;
+ default:
+ case CS8:
+ req.data_bit = FT260_CFG_DATA_BITS_8;
+ break;
+ }
+
+ req.stop_bit = (termios->c_cflag & CSTOPB) ?
+ FT260_CFG_STOP_TWO_BIT : FT260_CFG_STOP_ONE_BIT;
+
+ if (termios->c_cflag & PARENB) {
+ req.parity = (termios->c_cflag & PARODD) ?
+ FT260_CFG_PAR_ODD : FT260_CFG_PAR_EVEN;
+ } else {
+ req.parity = FT260_CFG_PAR_NO;
+ }
+
+ baud = tty_termios_baud_rate(termios);
+ if (baud == 0 || baud < FT260_CFG_BAUD_MIN || baud > FT260_CFG_BAUD_MAX) {
+ struct tty_struct *tty = tty_port_tty_get(&port->port);
+
+ hid_err(hdev, "Invalid baud rate %d\n", baud);
+ baud = 9600;
+ tty_encode_baud_rate(tty, baud, baud);
+ tty_kref_put(tty);
+ }
+ put_unaligned_le32(cpu_to_le32(baud), &req.baudrate);
+
+ if (termios->c_cflag & CRTSCTS)
+ req.flow_ctrl = FT260_CFG_FLOW_CTRL_RTS_CTS;
+ else
+ req.flow_ctrl = FT260_CFG_FLOW_CTRL_OFF;
+
+ ft260_dbg("Configured termios: flow control: %d, baudrate: %d, ",
+ req.flow_ctrl, baud);
+ ft260_dbg("data_bit: %d, parity: %d, stop_bit: %d, breaking: %d\n",
+ req.data_bit, req.parity,
+ req.stop_bit, req.breaking);
+
+ req.flow_ctrl = FT260_CFG_FLOW_CTRL_NONE;
+ req.breaking = FT260_CFG_BREAKING_NO;
+
+ ret = ft260_hid_feature_report_set(hdev, (u8 *)&req, sizeof(req));
+ if (ret < 0)
+ hid_err(hdev, "ft260_hid_feature_report_set failed: %d\n", ret);
+
+ return ret;
+}
+
+static int ft260_uart_get_icount(struct tty_struct *tty,
+ struct serial_icounter_struct *icount)
+{
+ struct ft260_device *port = tty->driver_data;
+
+ memcpy(icount, &port->icount, sizeof(struct uart_icount));
+
+ return 0;
+}
+
+static void ft260_uart_set_termios(struct tty_struct *tty,
+ const struct ktermios *old_termios)
+{
+ struct ft260_device *port = tty->driver_data;
+
+ ft260_uart_change_speed(port, &tty->termios, NULL);
+}
+
+static int ft260_uart_install(struct tty_driver *driver, struct tty_struct *tty)
+{
+ int idx = tty->index;
+ struct ft260_device *port = ft260_uart_port_get(idx);
+ int ret = tty_standard_install(driver, tty);
+
+ if (ret == 0)
+ /* This is the ref ft260_uart_port get provided */
+ tty->driver_data = port;
+ else
+ ft260_uart_port_put(port);
+
+ return ret;
+}
+
+static void ft260_uart_cleanup(struct tty_struct *tty)
+{
+ struct ft260_device *port = tty->driver_data;
+
+ tty->driver_data = NULL; /* Bug trap */
+ ft260_uart_port_put(port);
+}
+
+static int ft260_uart_proc_show(struct seq_file *m, void *v)
+{
+ int i;
+
+ seq_printf(m, "ft260 info:1.0 driver%s%s revision:%s\n",
+ "", "", "");
+ for (i = 0; i < UART_COUNT_MAX; i++) {
+ struct ft260_device *port = ft260_uart_port_get(i);
+
+ if (port) {
+ seq_printf(m, "%d: uart:FT260", i);
+ if (capable(CAP_SYS_ADMIN)) {
+ seq_printf(m, " tx:%d rx:%d",
+ port->icount.tx, port->icount.rx);
+ if (port->icount.frame)
+ seq_printf(m, " fe:%d",
+ port->icount.frame);
+ if (port->icount.parity)
+ seq_printf(m, " pe:%d",
+ port->icount.parity);
+ if (port->icount.brk)
+ seq_printf(m, " brk:%d",
+ port->icount.brk);
+ if (port->icount.overrun)
+ seq_printf(m, " oe:%d",
+ port->icount.overrun);
+ if (port->icount.cts)
+ seq_printf(m, " cts:%d",
+ port->icount.cts);
+ if (port->icount.dsr)
+ seq_printf(m, " dsr:%d",
+ port->icount.dsr);
+ if (port->icount.rng)
+ seq_printf(m, " rng:%d",
+ port->icount.rng);
+ if (port->icount.dcd)
+ seq_printf(m, " dcd:%d",
+ port->icount.dcd);
+ }
+ ft260_uart_port_put(port);
+ seq_putc(m, '\n');
+ }
+ }
+ return 0;
+}
+
+static const struct tty_operations ft260_uart_ops = {
+ .open = ft260_uart_open,
+ .close = ft260_uart_close,
+ .write = ft260_uart_write,
+ .write_room = ft260_uart_write_room,
+ .chars_in_buffer = ft260_uart_chars_in_buffer,
+ .set_termios = ft260_uart_set_termios,
+ .hangup = ft260_uart_hangup,
+ .install = ft260_uart_install,
+ .cleanup = ft260_uart_cleanup,
+ .proc_show = ft260_uart_proc_show,
+ .get_icount = ft260_uart_get_icount,
+};
+
+/* The FT260 has a "power saving mode" that causes the device to switch
+ * to a 30 kHz oscillator if there's no activity for 5 seconds.
+ * Unfortunately this mode can only be disabled by reprogramming
+ * internal fuses, which requires an additional programming voltage.
+ *
+ * One effect of this mode is to cause data loss on a fast UART that
+ * transmits after being idle for longer than 5 seconds. We work around
+ * this by sending a dummy report at least once per 4 seconds if the
+ * UART is in use.
+ */
+static void ft260_uart_start_wakeup(struct timer_list *t)
+{
+ struct ft260_device *dev =
+ container_of(t, struct ft260_device, wakeup_timer);
+
+ if (dev->reschedule_work) {
+ schedule_work(&dev->wakeup_work);
+ mod_timer(&dev->wakeup_timer, jiffies +
+ msecs_to_jiffies(FT260_WAKEUP_NEEDED_AFTER_MS));
+ }
+}
+
+static void ft260_uart_do_wakeup(struct work_struct *work)
+{
+ struct ft260_device *dev =
+ container_of(work, struct ft260_device, wakeup_work);
+ struct ft260_get_chip_version_report version;
+ int ret;
+
+ if (dev->reschedule_work) {
+ ret = ft260_hid_feature_report_get(dev->hdev, FT260_CHIP_VERSION,
+ (u8 *)&version, sizeof(version));
+ if (ret < 0)
+ hid_err(dev->hdev,
+ "%s: failed to start transfer, ret %d\n",
+ __func__, ret);
+ }
+}
+
+static void ft260_uart_shutdown(struct tty_port *tport)
+{
+ struct ft260_device *port =
+ container_of(tport, struct ft260_device, port);
+
+ port->reschedule_work = false;
+}
+
+static int ft260_uart_activate(struct tty_port *tport, struct tty_struct *tty)
+{
+ struct ft260_device *port =
+ container_of(tport, struct ft260_device, port);
+
+ /*
+ * Set the TTY IO error marker - we will only clear this
+ * once we have successfully opened the port.
+ */
+ set_bit(TTY_IO_ERROR, &tty->flags);
+
+ spin_lock(&port->write_lock);
+ kfifo_reset(&port->xmit_fifo);
+ spin_unlock(&port->write_lock);
+
+ ft260_uart_change_speed(port, &tty->termios, NULL);
+ clear_bit(TTY_IO_ERROR, &tty->flags);
+
+ if (port->reschedule_work) {
+ mod_timer(&port->wakeup_timer, jiffies +
+ msecs_to_jiffies(FT260_WAKEUP_NEEDED_AFTER_MS));
+ }
+
+ return 0;
+}
+
+static void ft260_uart_port_destroy(struct tty_port *tport)
+{
+ struct ft260_device *port =
+ container_of(tport, struct ft260_device, port);
+
+ kfree(port);
+}
+
+static const struct tty_port_operations ft260_uart_port_ops = {
+ .shutdown = ft260_uart_shutdown,
+ .activate = ft260_uart_activate,
+ .destruct = ft260_uart_port_destroy,
+};
+
+static struct tty_driver *ft260_tty_driver;
+
+static int ft260_i2c_probe(struct hid_device *hdev, struct ft260_device *dev)
+{
+ int ret;
+
+ hid_info(hdev, "USB HID v%x.%02x Device [%s] on %s\n",
+ hdev->version >> 8, hdev->version & 0xff, hdev->name,
+ hdev->phys);
+
+ hid_set_drvdata(hdev, dev);
+ dev->hdev = hdev;
+ dev->adap.owner = THIS_MODULE;
+ dev->adap.class = I2C_CLASS_HWMON;
+ dev->adap.algo = &ft260_i2c_algo;
+ dev->adap.quirks = &ft260_i2c_quirks;
+ dev->adap.dev.parent = &hdev->dev;
+ snprintf(dev->adap.name, sizeof(dev->adap.name),
+ "FT260 usb-i2c bridge");
+
+ ret = ft260_xfer_status(dev, FT260_I2C_STATUS_BUS_BUSY);
+ if (ret)
+ ft260_i2c_reset(hdev);
+
+ i2c_set_adapdata(&dev->adap, dev);
+ ret = i2c_add_adapter(&dev->adap);
+ if (ret) {
+ hid_err(hdev, "failed to add i2c adapter\n");
+ return ret;
+ }
+
+ ret = sysfs_create_group(&hdev->dev.kobj, &ft260_attr_group);
+ if (ret < 0) {
+ hid_err(hdev, "failed to create sysfs attrs\n");
+ goto err_i2c_free;
+ }
+ return 0;
+
+err_i2c_free:
+ i2c_del_adapter(&dev->adap);
+ return ret;
+}
+
+static int ft260_uart_probe(struct hid_device *hdev, struct ft260_device *dev)
+{
+ struct ft260_configure_uart_request req;
+ int ret;
+ struct device *devt;
+
+ INIT_WORK(&dev->wakeup_work, ft260_uart_do_wakeup);
+ // FIXME: Do I need that if I have cancel_work_sync?
+ // FIXME: are all kfifo access secured by lock? with irq or not?
+ dev->reschedule_work = false;
+ /* Work not started at this point */
+ timer_setup(&dev->wakeup_timer, ft260_uart_start_wakeup, 0);
+
+ tty_port_init(&dev->port);
+ dev->port.ops = &ft260_uart_port_ops;
+
+ ret = ft260_uart_add_port(dev);
+ if (ret) {
+ hid_err(hdev, "failed to add port\n");
+ return ret;
+ }
+ devt = tty_port_register_device_attr(&dev->port,
+ ft260_tty_driver,
+ dev->index, &hdev->dev,
+ dev, NULL);
+ if (IS_ERR(devt)) {
+ hid_err(hdev, "failed to register tty port\n");
+ ret = PTR_ERR(devt);
+ goto err_register_tty;
+ }
+ hid_info(hdev, "Registering device /dev/%s%d\n",
+ ft260_tty_driver->name, dev->index);
+
+ /* Send Feature Report to Configure FT260 as UART 9600-8-N-1 */
+ req.report = FT260_SYSTEM_SETTINGS;
+ req.request = FT260_SET_UART_CONFIG;
+ req.flow_ctrl = FT260_CFG_FLOW_CTRL_NONE;
+ put_unaligned_le32(cpu_to_le32(9600), &req.baudrate);
+ req.data_bit = FT260_CFG_DATA_BITS_8;
+ req.parity = FT260_CFG_PAR_NO;
+ req.stop_bit = FT260_CFG_STOP_ONE_BIT;
+ req.breaking = FT260_CFG_BREAKING_NO;
+
+ ret = ft260_hid_feature_report_set(hdev, (u8 *)&req, sizeof(req));
+ if (ret < 0) {
+ hid_err(hdev, "ft260_hid_feature_report_set failed: %d\n",
+ ret);
+ goto err_hid_report;
+ }
+
+ return 0;
+
+err_hid_report:
+ tty_port_unregister_device(&dev->port, ft260_tty_driver, dev->index);
+err_register_tty:
+ ft260_uart_port_remove(dev);
+ return ret;
+}
+
static int ft260_probe(struct hid_device *hdev, const struct hid_device_id *id)
{
struct ft260_device *dev;
@@ -1010,20 +1676,26 @@ static int ft260_probe(struct hid_device *hdev, const struct hid_device_id *id)
if (!hid_is_usb(hdev))
return -EINVAL;
- dev = devm_kzalloc(&hdev->dev, sizeof(*dev), GFP_KERNEL);
- if (!dev)
- return -ENOMEM;
+ /* We cannot used devm_kzalloc here, because port has to survive until
+ * destroy function call
+ */
+ dev = kzalloc_obj(*dev, GFP_KERNEL);
+ if (!dev) {
+ ret = -ENOMEM;
+ goto alloc_fail;
+ }
+ hid_set_drvdata(hdev, dev);
ret = hid_parse(hdev);
if (ret) {
hid_err(hdev, "failed to parse HID\n");
- return ret;
+ goto hid_fail;
}
ret = hid_hw_start(hdev, 0);
if (ret) {
hid_err(hdev, "failed to start HID HW\n");
- return ret;
+ goto hid_fail;
}
ret = hid_hw_open(hdev);
@@ -1043,53 +1715,36 @@ static int ft260_probe(struct hid_device *hdev, const struct hid_device_id *id)
version.chip_code[0], version.chip_code[1],
version.chip_code[2], version.chip_code[3]);
- ret = ft260_is_interface_enabled(hdev);
- if (ret <= 0)
+ ret = ft260_get_interface_type(hdev, dev);
+ if (ret <= FT260_IFACE_NONE)
goto err_hid_close;
- hid_info(hdev, "USB HID v%x.%02x Device [%s] on %s\n",
- hdev->version >> 8, hdev->version & 0xff, hdev->name,
- hdev->phys);
-
hid_set_drvdata(hdev, dev);
dev->hdev = hdev;
- dev->adap.owner = THIS_MODULE;
- dev->adap.class = I2C_CLASS_HWMON;
- dev->adap.algo = &ft260_i2c_algo;
- dev->adap.quirks = &ft260_i2c_quirks;
- dev->adap.dev.parent = &hdev->dev;
- snprintf(dev->adap.name, sizeof(dev->adap.name),
- "FT260 usb-i2c bridge");
mutex_init(&dev->lock);
spin_lock_init(&dev->read_lock);
init_completion(&dev->wait);
- ret = ft260_xfer_status(dev, FT260_I2C_STATUS_BUS_BUSY);
- if (ret)
- ft260_i2c_reset(hdev);
-
- i2c_set_adapdata(&dev->adap, dev);
- ret = i2c_add_adapter(&dev->adap);
- if (ret) {
- hid_err(hdev, "failed to add i2c adapter\n");
- goto err_hid_close;
- }
-
- ret = sysfs_create_group(&hdev->dev.kobj, &ft260_attr_group);
- if (ret < 0) {
- hid_err(hdev, "failed to create sysfs attrs\n");
- goto err_i2c_free;
+ if (!dev->ft260_is_serial) {
+ ret = ft260_i2c_probe(hdev, dev);
+ if (ret)
+ goto err_hid_close;
+ } else {
+ ret = ft260_uart_probe(hdev, dev);
+ if (ret)
+ goto err_hid_close;
}
return 0;
-err_i2c_free:
- i2c_del_adapter(&dev->adap);
err_hid_close:
hid_hw_close(hdev);
err_hid_stop:
hid_hw_stop(hdev);
+hid_fail:
+ kfree(dev);
+alloc_fail:
return ret;
}
@@ -1100,8 +1755,18 @@ static void ft260_remove(struct hid_device *hdev)
if (!dev)
return;
- sysfs_remove_group(&hdev->dev.kobj, &ft260_attr_group);
- i2c_del_adapter(&dev->adap);
+ if (dev->ft260_is_serial) {
+ // FIXME:
+ cancel_work_sync(&dev->wakeup_work);
+ tty_port_unregister_device(&dev->port, ft260_tty_driver,
+ dev->index);
+ ft260_uart_port_remove(dev);
+ /* dev still needed, so we will free it in _destroy func */
+ } else {
+ sysfs_remove_group(&hdev->dev.kobj, &ft260_attr_group);
+ i2c_del_adapter(&dev->adap);
+ kfree(dev);
+ }
hid_hw_close(hdev);
hid_hw_stop(hdev);
@@ -1111,10 +1776,10 @@ static int ft260_raw_event(struct hid_device *hdev, struct hid_report *report,
u8 *data, int size)
{
struct ft260_device *dev = hid_get_drvdata(hdev);
- struct ft260_i2c_input_report *xfer = (void *)data;
+ struct ft260_input_report *xfer = (void *)data;
unsigned long irqflags;
- if (size < offsetof(struct ft260_i2c_input_report, data)) {
+ if (size < offsetof(struct ft260_input_report, data)) {
hid_err(hdev, "short report %d\n", size);
return -1;
}
@@ -1127,7 +1792,7 @@ static int ft260_raw_event(struct hid_device *hdev, struct hid_report *report,
xfer->report, xfer->length, size);
if (xfer->length > size -
- offsetof(struct ft260_i2c_input_report, data)) {
+ offsetof(struct ft260_input_report, data)) {
hid_err(hdev, "report %#02x: length %d exceeds HID report size\n",
xfer->report, xfer->length);
return -1;
@@ -1157,9 +1822,17 @@ static int ft260_raw_event(struct hid_device *hdev, struct hid_report *report,
if (complete_read)
complete(&dev->wait);
- } else {
- hid_err(hdev, "unhandled report %#02x\n", xfer->report);
+ return 0;
+
+ } else if (xfer->length > FT260_RD_DATA_MAX) {
+ hid_err(hdev, "received data too long (%d)\n", xfer->length);
+ return -EBADR;
+ } else if (xfer->report >= FT260_UART_REPORT_MIN &&
+ xfer->report <= FT260_UART_REPORT_MAX) {
+ return ft260_uart_receive_chars(dev, xfer->data, xfer->length);
}
+ hid_err(hdev, "unhandled report %#02x\n", xfer->report);
+
return 0;
}
@@ -1171,7 +1844,62 @@ static struct hid_driver ft260_driver = {
.raw_event = ft260_raw_event,
};
-module_hid_driver(ft260_driver);
-MODULE_DESCRIPTION("FTDI FT260 USB HID to I2C host bridge");
+static int __init ft260_driver_init(void)
+{
+ int ret;
+
+ ft260_tty_driver = tty_alloc_driver(UART_COUNT_MAX,
+ TTY_DRIVER_REAL_RAW | TTY_DRIVER_DYNAMIC_DEV);
+ if (IS_ERR(ft260_tty_driver)) {
+ pr_err("tty_alloc_driver failed: %d\n",
+ (int)PTR_ERR(ft260_tty_driver));
+ return PTR_ERR(ft260_tty_driver);
+ }
+
+ ft260_tty_driver->driver_name = "ft260_ser";
+ ft260_tty_driver->name = "ttyFT";
+ ft260_tty_driver->major = 0;
+ ft260_tty_driver->minor_start = 0;
+ ft260_tty_driver->type = TTY_DRIVER_TYPE_SERIAL;
+ ft260_tty_driver->subtype = SERIAL_TYPE_NORMAL;
+ ft260_tty_driver->init_termios = tty_std_termios;
+ ft260_tty_driver->init_termios.c_cflag = B9600 | CS8 | CREAD | HUPCL | CLOCAL;
+ ft260_tty_driver->init_termios.c_ispeed = 9600;
+ ft260_tty_driver->init_termios.c_ospeed = 9600;
+ tty_set_operations(ft260_tty_driver, &ft260_uart_ops);
+
+ ret = tty_register_driver(ft260_tty_driver);
+ if (ret) {
+ pr_err("tty_register_driver failed: %d\n", ret);
+ goto err_reg_driver;
+ }
+
+ ret = hid_register_driver(&(ft260_driver));
+ if (ret) {
+ pr_err("hid_register_driver failed: %d\n", ret);
+ goto err_reg_hid;
+ }
+
+ return 0;
+
+err_reg_hid:
+ tty_unregister_driver(ft260_tty_driver);
+err_reg_driver:
+ tty_driver_kref_put(ft260_tty_driver);
+
+ return ret;
+}
+
+static void __exit ft260_driver_exit(void)
+{
+ hid_unregister_driver(&(ft260_driver));
+ tty_unregister_driver(ft260_tty_driver);
+ tty_driver_kref_put(ft260_tty_driver);
+}
+
+module_init(ft260_driver_init);
+module_exit(ft260_driver_exit);
+
+MODULE_DESCRIPTION("FTDI FT260 USB HID to I2C host bridge and TTY driver");
MODULE_AUTHOR("Michael Zaidman <michael.zaidman@gmail.com>");
MODULE_LICENSE("GPL v2");
--
2.43.0
^ permalink raw reply related [flat|nested] 43+ messages in thread
* [PATCH 02/13] HID: ft260: uart: bring-up fixes
2026-08-22 21:39 [PATCH 00/13] HID: ft260: add UART and GPIO support, plus I2C fixes Michael Zaidman
2026-08-22 21:39 ` [PATCH 01/13] HID: ft260: add serial driver Michael Zaidman
@ 2026-08-22 21:39 ` Michael Zaidman
2026-08-22 21:56 ` sashiko-bot
2026-08-22 21:39 ` [PATCH 03/13] HID: ft260: add GPIO support on top of UART Michael Zaidman
` (11 subsequent siblings)
13 siblings, 1 reply; 43+ messages in thread
From: Michael Zaidman @ 2026-08-22 21:39 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: Linus Walleij, Bartosz Golaszewski, Germain Hebert, Rio Liu,
Bruno Giacomazzi, Christina Quast, linux-input, linux-gpio,
linux-i2c, linux-kernel, Michael Zaidman
Fix the UART path so the serial interface added previously works
reliably in practice. This squashes the contiguous UART bring-up
fixes that followed the initial serial-driver addition.
The UART and I2C paths shared write_buf without proper separation,
so concurrent use could corrupt transfers on both channels. Give
UART its own TX buffer. The write request report is now built in
that fixed per-port buffer, so its payload member becomes a fixed
FT260_WR_UART_DATA_MAX (62) array rather than a __counted_by()
flexible array, which describes a runtime-counted allocation this
path no longer makes.
The FT260 enters power-save after ~5s idle, which drops RX data at
higher baud rates. Keep a timed dummy wakeup, but only when needed:
skip it below 4800 baud and when EEPROM disables power-save; wake
earlier before RX; avoid configuring baud or disabling the workaround
twice; and arm the timer when a tty session starts.
The received report length was validated against the actual HID report
size only on the I2C path. The UART path instead bounded the
device-supplied length against FT260_RD_DATA_MAX (180), which is the
maximum I2C read payload, not a bound on the report buffer. As a UART
payload cannot exceed FT260_WR_UART_DATA_MAX (62) bytes, a report
claiming more would make ft260_uart_receive_chars() push data past the
end of the report buffer into the tty flip buffer. Validate the length
against the received report size once, before dispatching on the report
type, and drop the two now-redundant FT260_RD_DATA_MAX checks.
Also fix RX data loss after reopening the device, improve TX
throughput, simplify HID interface-type detection, use kfifo_avail
for write-room, and ignore spurious UART interrupt status (0xb1)
reports instead of treating them as errors.
Also includes UART cleanup and refactoring to match the rest of the
driver style (including replacing kfifo_*_locked aliases with
kfifo_*_spinlocked).
This replaces the UART bring-up fix series:
https://lore.kernel.org/all/20240210215147.77629-1-michael.zaidman@gmail.com/
Signed-off-by: Michael Zaidman <michael.zaidman@gmail.com>
---
drivers/hid/hid-ft260.c | 340 ++++++++++++++++++++++------------------
1 file changed, 188 insertions(+), 152 deletions(-)
diff --git a/drivers/hid/hid-ft260.c b/drivers/hid/hid-ft260.c
index 3faedfc5c413..b6c408267a68 100644
--- a/drivers/hid/hid-ft260.c
+++ b/drivers/hid/hid-ft260.c
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
- * hid-ft260.c - FTDI FT260 USB HID to I2C host bridge
+ * FTDI FT260 USB HID to I2C/UART host bridge
*
* Copyright (c) 2021, Michael Zaidman <michaelz@xsightlabs.com>
*
@@ -20,10 +20,6 @@
#include <linux/minmax.h>
#include <linux/unaligned.h>
-#define UART_COUNT_MAX 4 /* Number of UARTs this driver can handle */
-#define FIFO_SIZE 256
-#define TTY_WAKEUP_WATERMARK (FIFO_SIZE / 2)
-
#ifdef DEBUG
static int ft260_debug = 1;
#else
@@ -38,9 +34,12 @@ MODULE_PARM_DESC(debug, "Toggle FT260 debugging messages");
pr_info("%s: " format, __func__, ##arg); \
} while (0)
-#define FT260_REPORT_MAX_LENGTH (64)
-#define FT260_I2C_DATA_REPORT_ID(len) (FT260_I2C_REPORT_MIN + (len - 1) / 4)
-#define FT260_UART_DATA_REPORT_ID(len) (FT260_UART_REPORT_MIN + (len - 1) / 4)
+#define FT260_REPORT_MAX_LEN (64)
+#define FT260_DATA_REPORT_ID(min, len) (min + (len - 1) / 4)
+#define FT260_I2C_DATA_REPORT_ID(len) \
+ FT260_DATA_REPORT_ID(FT260_I2C_REPORT_MIN, len)
+#define FT260_UART_DATA_REPORT_ID(len) \
+ FT260_DATA_REPORT_ID(FT260_UART_REPORT_MIN, len)
#define FT260_WAKEUP_NEEDED_AFTER_MS (4800) /* 5s minus 200ms margin */
@@ -56,7 +55,8 @@ MODULE_PARM_DESC(debug, "Toggle FT260 debugging messages");
* read payload length to be 180 bytes.
*/
#define FT260_RD_DATA_MAX (180)
-#define FT260_WR_DATA_MAX (60)
+#define FT260_WR_I2C_DATA_MAX (60)
+#define FT260_WR_UART_DATA_MAX (62)
/*
* Device interface configuration.
@@ -90,7 +90,7 @@ enum {
FT260_I2C_REPORT_MAX = 0xDE,
FT260_GPIO = 0xB0,
FT260_UART_INTERRUPT_STATUS = 0xB1,
- FT260_UART_STATUS = 0xE0,
+ FT260_UART_SETTINGS = 0xE0,
FT260_UART_RI_DCD_STATUS = 0xE1,
FT260_UART_REPORT_MIN = 0xF0,
FT260_UART_REPORT_MAX = 0xFE,
@@ -144,7 +144,7 @@ enum {
FT260_FLAG_START_STOP_REPEATED = 0x07,
};
-/* Return values for ft260_get_interface_type func */
+/* USB interface type values */
enum {
FT260_IFACE_NONE,
FT260_IFACE_I2C,
@@ -190,6 +190,18 @@ struct ft260_get_i2c_status_report {
u8 reserved;
} __packed;
+struct ft260_get_uart_settings_report {
+ u8 report; /* FT260_UART_SETTINGS */
+ u8 flow_ctrl; /* 0 - OFF; 1 - RTS_CTS, 2 - DTR_DSR, */
+ /* 3 - XON_XOFF, 4 - No flow control */
+ /* The baudrate field is unaligned */
+ __le32 baudrate; /* little endian, 9600 = 0x2580, 19200 = 0x4B00 */
+ u8 data_bit; /* 7 or 8 */
+ u8 parity; /* 0: no parity, 1: odd, 2: even, 3: high, 4: low */
+ u8 stop_bit; /* 0: one stop bit, 2: 2 stop bits */
+ u8 breaking; /* 0: no break */
+} __packed;
+
/* Feature Out reports */
struct ft260_set_system_clock_report {
@@ -229,7 +241,7 @@ struct ft260_i2c_write_request_report {
u8 address; /* 7-bit I2C address */
u8 flag; /* I2C transaction condition */
u8 length; /* data payload length */
- u8 data[FT260_WR_DATA_MAX]; /* data payload */
+ u8 data[FT260_WR_I2C_DATA_MAX]; /* data payload */
} __packed;
struct ft260_i2c_read_request_report {
@@ -246,18 +258,19 @@ struct ft260_input_report {
} __packed;
/* UART reports */
+
struct ft260_uart_write_request_report {
u8 report; /* FT260_UART_REPORT */
u8 length; /* data payload length */
- u8 data[] __counted_by(length); /* variable data payload */
+ u8 data[FT260_WR_UART_DATA_MAX]; /* data payload */
} __packed;
-struct ft260_configure_uart_request {
+struct ft260_configure_uart_request_report {
u8 report; /* FT260_SYSTEM_SETTINGS */
u8 request; /* FT260_SET_UART_CONFIG */
u8 flow_ctrl; /* 0: OFF, 1: RTS_CTS, 2: DTR_DSR */
/* 3: XON_XOFF, 4: No flow ctrl */
- /* The baudrate field is unaligned: */
+ /* The baudrate field is unaligned */
__le32 baudrate; /* little endian, 9600 = 0x2580, 19200 = 0x4B00 */
u8 data_bit; /* 7 or 8 */
u8 parity; /* 0: no parity, 1: odd, 2: even, 3: high, 4: low */
@@ -292,6 +305,11 @@ enum {
FT260_CFG_BAUD_MAX = 12000000,
};
+#define FT260_UART_EN_PW_SAVE_BAUD (4800)
+
+#define UART_COUNT_MAX (4) /* Number of supported UARTs */
+#define XMIT_FIFO_SIZE (PAGE_SIZE)
+
static const struct hid_device_id ft260_devices[] = {
{ HID_USB_DEVICE(USB_VENDOR_ID_FUTURE_TECHNOLOGY,
USB_DEVICE_ID_FT260) },
@@ -302,26 +320,22 @@ MODULE_DEVICE_TABLE(hid, ft260_devices);
struct ft260_device {
struct i2c_adapter adap;
struct hid_device *hdev;
-
- bool ft260_is_serial;
+ int iface_type;
struct list_head device_list;
-
- /* tty_port lifetime is equal to device lifetime */
struct tty_port port;
+ /* tty port index */
unsigned int index;
struct kfifo xmit_fifo;
- /* write_lock: lock to serialize access to xmit fifo */
- spinlock_t write_lock;
+ spinlock_t xmit_fifo_lock;
struct uart_icount icount;
-
struct timer_list wakeup_timer;
struct work_struct wakeup_work;
bool reschedule_work;
-
-
+ bool power_saving_en;
struct completion wait;
struct mutex lock;
- u8 write_buf[FT260_REPORT_MAX_LENGTH];
+ u8 i2c_wr_buf[FT260_REPORT_MAX_LEN];
+ u8 uart_wr_buf[FT260_REPORT_MAX_LEN];
unsigned long need_wakeup_at;
/* Protects read_buf, read_idx and read_len against ft260_raw_event() */
spinlock_t read_lock;
@@ -332,8 +346,7 @@ struct ft260_device {
};
static int ft260_hid_feature_report_get(struct hid_device *hdev,
- unsigned char report_id, u8 *data,
- size_t len)
+ u8 report_id, u8 *data, size_t len)
{
u8 *buf;
int ret;
@@ -462,8 +475,6 @@ static int ft260_hid_output_report_check_status(struct ft260_device *dev,
ret = ft260_hid_output_report(hdev, data, len);
if (ret < 0) {
- hid_dbg(hdev, "%s: failed to start transfer, ret %d\n",
- __func__, ret);
ft260_i2c_reset(hdev);
return ret;
}
@@ -505,7 +516,7 @@ static int ft260_i2c_write(struct ft260_device *dev, u8 addr, u8 *data,
int ret, wr_len, idx = 0;
struct hid_device *hdev = dev->hdev;
struct ft260_i2c_write_request_report *rep =
- (struct ft260_i2c_write_request_report *)dev->write_buf;
+ (struct ft260_i2c_write_request_report *)dev->i2c_wr_buf;
if (len < 1)
return -EINVAL;
@@ -513,12 +524,12 @@ static int ft260_i2c_write(struct ft260_device *dev, u8 addr, u8 *data,
rep->flag = FT260_FLAG_START;
do {
- if (len <= FT260_WR_DATA_MAX) {
+ if (len <= FT260_WR_I2C_DATA_MAX) {
wr_len = len;
if (flag == FT260_FLAG_START_STOP)
rep->flag |= FT260_FLAG_STOP;
} else {
- wr_len = FT260_WR_DATA_MAX;
+ wr_len = FT260_WR_I2C_DATA_MAX;
}
rep->report = FT260_I2C_DATA_REPORT_ID(wr_len);
@@ -554,7 +565,7 @@ static int ft260_smbus_write(struct ft260_device *dev, u8 addr, u8 cmd,
int len = 4;
struct ft260_i2c_write_request_report *rep =
- (struct ft260_i2c_write_request_report *)dev->write_buf;
+ (struct ft260_i2c_write_request_report *)dev->i2c_wr_buf;
if (data_len >= sizeof(rep->data))
return -EINVAL;
@@ -574,6 +585,8 @@ static int ft260_smbus_write(struct ft260_device *dev, u8 addr, u8 cmd,
rep->report, addr, cmd, rep->length, len);
ret = ft260_hid_output_report_check_status(dev, (u8 *)rep, len);
+ if (ret < 0)
+ hid_err(dev->hdev, "%s: failed with %d\n", __func__, ret);
return ret;
}
@@ -692,8 +705,7 @@ static int ft260_i2c_write_read(struct ft260_device *dev, struct i2c_msg *msgs)
else
read_off = *msgs[0].buf;
- ft260_dbg("%s: off %#x rlen %d wlen %d\n", __func__,
- read_off, rd_len, wr_len);
+ ft260_dbg("off %#x rlen %d wlen %d\n", read_off, rd_len, wr_len);
}
ret = ft260_i2c_write(dev, addr, msgs[0].buf, wr_len,
@@ -926,27 +938,25 @@ static int ft260_get_interface_type(struct hid_device *hdev, struct ft260_device
ft260_dbg("i2c_enable: 0x%02x\n", cfg.i2c_enable);
ft260_dbg("uart_mode: 0x%02x\n", cfg.uart_mode);
- dev->ft260_is_serial = false;
+ dev->power_saving_en = cfg.power_saving_en;
switch (cfg.chip_mode) {
case FT260_MODE_ALL:
case FT260_MODE_BOTH:
- if (interface == 1) {
+ if (interface == 1)
ret = FT260_IFACE_UART;
- dev->ft260_is_serial = true;
- } else {
+ else
ret = FT260_IFACE_I2C;
- }
break;
case FT260_MODE_UART:
ret = FT260_IFACE_UART;
- dev->ft260_is_serial = true;
break;
case FT260_MODE_I2C:
ret = FT260_IFACE_I2C;
break;
}
+ dev->iface_type = ret;
return ret;
}
@@ -1090,12 +1100,36 @@ static const struct attribute_group ft260_attr_group = {
}
};
-/***
- * START Serial dev part
- */
static DEFINE_MUTEX(ft260_uart_list_lock);
static LIST_HEAD(ft260_uart_device_list);
+static void ft260_uart_wakeup(struct ft260_device *dev);
+
+static int ft260_get_uart_settings(struct hid_device *hdev,
+ struct ft260_get_uart_settings_report *cfg)
+{
+ int ret;
+ int len = sizeof(struct ft260_get_uart_settings_report);
+
+ ret = ft260_hid_feature_report_get(hdev, FT260_UART_SETTINGS,
+ (u8 *)cfg, len);
+ if (ret < 0) {
+ hid_err(hdev, "failed to retrieve uart settings\n");
+ return ret;
+ }
+ return 0;
+}
+
+static void ft260_uart_wakeup_workaraund_enable(struct ft260_device *port,
+ bool enable)
+{
+ if (port->power_saving_en) {
+ port->reschedule_work = enable;
+ ft260_dbg("%s wakeup workaround",
+ enable ? "activate" : "deactivate");
+ }
+}
+
static struct ft260_device *ft260_dev_by_index(int index)
{
struct ft260_device *port;
@@ -1112,8 +1146,8 @@ static int ft260_uart_add_port(struct ft260_device *port)
int index = 0, ret = 0;
struct ft260_device *dev;
- spin_lock_init(&port->write_lock);
- if (kfifo_alloc(&port->xmit_fifo, FIFO_SIZE, GFP_KERNEL))
+ spin_lock_init(&port->xmit_fifo_lock);
+ if (kfifo_alloc(&port->xmit_fifo, XMIT_FIFO_SIZE, GFP_KERNEL))
return -ENOMEM;
mutex_lock(&ft260_uart_list_lock);
@@ -1143,19 +1177,18 @@ static void ft260_uart_port_remove(struct ft260_device *port)
list_del(&port->device_list);
mutex_unlock(&ft260_uart_list_lock);
- spin_lock(&port->write_lock);
+ spin_lock(&port->xmit_fifo_lock);
kfifo_free(&port->xmit_fifo);
- spin_unlock(&port->write_lock);
+ spin_unlock(&port->xmit_fifo_lock);
mutex_lock(&port->port.mutex);
- port->reschedule_work = false;
tty_port_tty_hangup(&port->port, false);
mutex_unlock(&port->port.mutex);
ft260_uart_port_put(port);
}
-static struct ft260_device *ft260_uart_port_get(unsigned int index)
+static struct ft260_device *ft260_uart_port_get(int index)
{
struct ft260_device *port;
@@ -1211,30 +1244,24 @@ static int ft260_uart_transmit_chars(struct ft260_device *port)
goto tty_out;
}
- rep = (struct ft260_uart_write_request_report *)port->write_buf;
+ rep = (struct ft260_uart_write_request_report *)port->uart_wr_buf;
do {
- len = min(data_len, FT260_WR_DATA_MAX);
+ len = min(data_len, FT260_WR_UART_DATA_MAX);
rep->report = FT260_UART_DATA_REPORT_ID(len);
rep->length = len;
- len = kfifo_out_locked(xmit, rep->data, len, &port->write_lock);
+ len = kfifo_out_spinlocked(xmit, rep->data, len, &port->xmit_fifo_lock);
- ret = ft260_hid_output_report(hdev, (u8 *)rep, len + sizeof(*rep));
- if (ret < 0) {
- hid_err(hdev, "Failed to start transfer, ret %d\n", ret);
+ ret = ft260_hid_output_report(hdev, (u8 *)rep, len + 2);
+ if (ret < 0)
goto tty_out;
- }
data_len -= len;
port->icount.tx += len;
} while (data_len > 0);
- len = kfifo_len(xmit);
- if ((FIFO_SIZE - len) > TTY_WAKEUP_WATERMARK)
- tty_wakeup(tty);
-
ret = 0;
tty_out:
@@ -1242,20 +1269,14 @@ static int ft260_uart_transmit_chars(struct ft260_device *port)
return ret;
}
-static int ft260_uart_receive_chars(struct ft260_device *port,
- u8 *data, u8 length)
+static int ft260_uart_receive_chars(struct ft260_device *port, u8 *data, u8 length)
{
- struct hid_device *hdev = port->hdev;
- int ret = 0;
-
- if (length > FT260_RD_DATA_MAX) {
- hid_err(hdev, "Received too much data (%d)\n", length);
- return -EBADR;
- }
+ int ret;
ret = tty_insert_flip_string(&port->port, data, length);
if (ret != length)
- hid_err(hdev, "%d char not inserted to flip buffer\n", length - ret);
+ ft260_dbg("%d char not inserted to flip buf\n", length - ret);
+
port->icount.rx += ret;
if (ret)
@@ -1265,26 +1286,26 @@ static int ft260_uart_receive_chars(struct ft260_device *port,
}
static ssize_t ft260_uart_write(struct tty_struct *tty, const u8 *buf,
- size_t count)
+ size_t cnt)
{
struct ft260_device *port = tty->driver_data;
- struct hid_device *hdev = port->hdev;
- int len, ret;
+ ssize_t len, ret, diff;
- len = kfifo_in_locked(&port->xmit_fifo, buf, count, &port->write_lock);
- ft260_dbg("count: %zu, len: %d", count, len);
+ len = kfifo_in_spinlocked(&port->xmit_fifo, buf, cnt,
+ &port->xmit_fifo_lock);
+ ft260_dbg("count: %zu, len: %zd", cnt, len);
ret = ft260_uart_transmit_chars(port);
if (ret < 0) {
- hid_dbg(hdev, "Failed to transmit chars: %d\n", ret);
+ ft260_dbg("failed to transmit %zd\n", ret);
return 0;
}
ret = kfifo_len(&port->xmit_fifo);
if (ret > 0) {
- hid_dbg(hdev, "Failed to all kfifo data bytes\n");
- ft260_dbg("return: %d", len - ret);
- return len - ret;
+ diff = len - ret;
+ ft260_dbg("failed to send %zd out of %zd bytes\n", diff, len);
+ return diff;
}
return len;
@@ -1294,7 +1315,7 @@ static unsigned int ft260_uart_write_room(struct tty_struct *tty)
{
struct ft260_device *port = tty->driver_data;
- return FIFO_SIZE - kfifo_len(&port->xmit_fifo);
+ return kfifo_avail(&port->xmit_fifo);
}
static unsigned int ft260_uart_chars_in_buffer(struct tty_struct *tty)
@@ -1310,7 +1331,8 @@ static int ft260_uart_change_speed(struct ft260_device *port,
{
struct hid_device *hdev = port->hdev;
unsigned int baud;
- struct ft260_configure_uart_request req;
+ struct ft260_configure_uart_request_report req;
+ bool wakeup_workaraund = false;
int ret;
memset(&req, 0, sizeof(req));
@@ -1324,7 +1346,7 @@ static int ft260_uart_change_speed(struct ft260_device *port,
break;
case CS5:
case CS6:
- hid_err(hdev, "Invalid data bit size, setting to default (8 bit)\n");
+ hid_err(hdev, "invalid data bit size, setting a default\n");
req.data_bit = FT260_CFG_DATA_BITS_8;
termios->c_cflag &= ~CSIZE;
termios->c_cflag |= CS8;
@@ -1349,11 +1371,17 @@ static int ft260_uart_change_speed(struct ft260_device *port,
if (baud == 0 || baud < FT260_CFG_BAUD_MIN || baud > FT260_CFG_BAUD_MAX) {
struct tty_struct *tty = tty_port_tty_get(&port->port);
- hid_err(hdev, "Invalid baud rate %d\n", baud);
+ hid_err(hdev, "invalid baud rate %d\n", baud);
baud = 9600;
tty_encode_baud_rate(tty, baud, baud);
tty_kref_put(tty);
}
+
+ if (baud > FT260_UART_EN_PW_SAVE_BAUD)
+ wakeup_workaraund = true;
+
+ ft260_uart_wakeup_workaraund_enable(port, wakeup_workaraund);
+
put_unaligned_le32(cpu_to_le32(baud), &req.baudrate);
if (termios->c_cflag & CRTSCTS)
@@ -1361,7 +1389,7 @@ static int ft260_uart_change_speed(struct ft260_device *port,
else
req.flow_ctrl = FT260_CFG_FLOW_CTRL_OFF;
- ft260_dbg("Configured termios: flow control: %d, baudrate: %d, ",
+ ft260_dbg("configured termios: flow control: %d, baudrate: %d, ",
req.flow_ctrl, baud);
ft260_dbg("data_bit: %d, parity: %d, stop_bit: %d, breaking: %d\n",
req.data_bit, req.parity,
@@ -1372,7 +1400,7 @@ static int ft260_uart_change_speed(struct ft260_device *port,
ret = ft260_hid_feature_report_set(hdev, (u8 *)&req, sizeof(req));
if (ret < 0)
- hid_err(hdev, "ft260_hid_feature_report_set failed: %d\n", ret);
+ hid_err(hdev, "failed to change termios: %d\n", ret);
return ret;
}
@@ -1422,8 +1450,8 @@ static int ft260_uart_proc_show(struct seq_file *m, void *v)
{
int i;
- seq_printf(m, "ft260 info:1.0 driver%s%s revision:%s\n",
- "", "", "");
+ seq_printf(m, "ft260 info:1.0 driver%s%s revision:%s\n", "", "", "");
+
for (i = 0; i < UART_COUNT_MAX; i++) {
struct ft260_device *port = ft260_uart_port_get(i);
@@ -1478,15 +1506,16 @@ static const struct tty_operations ft260_uart_ops = {
.get_icount = ft260_uart_get_icount,
};
-/* The FT260 has a "power saving mode" that causes the device to switch
+/*
+ * The FT260 has a "power saving mode" that causes the device to switch
* to a 30 kHz oscillator if there's no activity for 5 seconds.
- * Unfortunately this mode can only be disabled by reprogramming
+ * Unfortunately, this mode can only be disabled by reprogramming
* internal fuses, which requires an additional programming voltage.
*
- * One effect of this mode is to cause data loss on a fast UART that
- * transmits after being idle for longer than 5 seconds. We work around
- * this by sending a dummy report at least once per 4 seconds if the
- * UART is in use.
+ * One effect of this mode is to cause data loss on an Rx line at baud
+ * rates higher than 4800 after being idle for longer than 5 seconds.
+ * We work around this by sending a dummy report at least once per 4.8
+ * seconds if the UART is in use.
*/
static void ft260_uart_start_wakeup(struct timer_list *t)
{
@@ -1500,53 +1529,68 @@ static void ft260_uart_start_wakeup(struct timer_list *t)
}
}
-static void ft260_uart_do_wakeup(struct work_struct *work)
+static void ft260_uart_wakeup(struct ft260_device *dev)
{
- struct ft260_device *dev =
- container_of(work, struct ft260_device, wakeup_work);
- struct ft260_get_chip_version_report version;
+ struct ft260_get_chip_version_report ver;
int ret;
if (dev->reschedule_work) {
ret = ft260_hid_feature_report_get(dev->hdev, FT260_CHIP_VERSION,
- (u8 *)&version, sizeof(version));
+ (u8 *)&ver, sizeof(ver));
if (ret < 0)
- hid_err(dev->hdev,
- "%s: failed to start transfer, ret %d\n",
- __func__, ret);
+ hid_err(dev->hdev, "%s: failed with %d\n", __func__, ret);
}
}
-static void ft260_uart_shutdown(struct tty_port *tport)
+static void ft260_uart_do_wakeup(struct work_struct *work)
+{
+ struct ft260_device *dev =
+ container_of(work, struct ft260_device, wakeup_work);
+
+ ft260_uart_wakeup(dev);
+}
+
+static void ft260_uart_port_shutdown(struct tty_port *tport)
{
struct ft260_device *port =
container_of(tport, struct ft260_device, port);
- port->reschedule_work = false;
+ ft260_uart_wakeup_workaraund_enable(port, false);
}
-static int ft260_uart_activate(struct tty_port *tport, struct tty_struct *tty)
+static int ft260_uart_port_activate(struct tty_port *tport, struct tty_struct *tty)
{
- struct ft260_device *port =
- container_of(tport, struct ft260_device, port);
+ int ret;
+ int baudrate;
+ struct ft260_get_uart_settings_report cfg;
+ struct ft260_device *port = container_of(tport, struct ft260_device, port);
- /*
- * Set the TTY IO error marker - we will only clear this
- * once we have successfully opened the port.
- */
set_bit(TTY_IO_ERROR, &tty->flags);
- spin_lock(&port->write_lock);
+ spin_lock(&port->xmit_fifo_lock);
kfifo_reset(&port->xmit_fifo);
- spin_unlock(&port->write_lock);
+ spin_unlock(&port->xmit_fifo_lock);
- ft260_uart_change_speed(port, &tty->termios, NULL);
clear_bit(TTY_IO_ERROR, &tty->flags);
- if (port->reschedule_work) {
- mod_timer(&port->wakeup_timer, jiffies +
- msecs_to_jiffies(FT260_WAKEUP_NEEDED_AFTER_MS));
- }
+ /*
+ * The port setting may remain intact after session termination.
+ * Then, when reopening the port without configuring the port
+ * setting, we need to retrieve the baud rate from the device to
+ * reactivate the wakeup workaround if needed.
+ */
+ ret = ft260_get_uart_settings(port->hdev, &cfg);
+ if (ret)
+ return ret;
+
+ baudrate = get_unaligned_le32(&cfg.baudrate);
+ if (baudrate > FT260_UART_EN_PW_SAVE_BAUD)
+ ft260_uart_wakeup_workaraund_enable(port, true);
+
+ ft260_dbg("configured baudrate = %d", baudrate);
+
+ mod_timer(&port->wakeup_timer, jiffies +
+ msecs_to_jiffies(FT260_WAKEUP_NEEDED_AFTER_MS));
return 0;
}
@@ -1560,8 +1604,8 @@ static void ft260_uart_port_destroy(struct tty_port *tport)
}
static const struct tty_port_operations ft260_uart_port_ops = {
- .shutdown = ft260_uart_shutdown,
- .activate = ft260_uart_activate,
+ .shutdown = ft260_uart_port_shutdown,
+ .activate = ft260_uart_port_activate,
.destruct = ft260_uart_port_destroy,
};
@@ -1610,14 +1654,13 @@ static int ft260_i2c_probe(struct hid_device *hdev, struct ft260_device *dev)
static int ft260_uart_probe(struct hid_device *hdev, struct ft260_device *dev)
{
- struct ft260_configure_uart_request req;
+ struct ft260_configure_uart_request_report req;
int ret;
struct device *devt;
INIT_WORK(&dev->wakeup_work, ft260_uart_do_wakeup);
- // FIXME: Do I need that if I have cancel_work_sync?
// FIXME: are all kfifo access secured by lock? with irq or not?
- dev->reschedule_work = false;
+ ft260_uart_wakeup_workaraund_enable(dev, true);
/* Work not started at this point */
timer_setup(&dev->wakeup_timer, ft260_uart_start_wakeup, 0);
@@ -1638,10 +1681,10 @@ static int ft260_uart_probe(struct hid_device *hdev, struct ft260_device *dev)
ret = PTR_ERR(devt);
goto err_register_tty;
}
- hid_info(hdev, "Registering device /dev/%s%d\n",
+ hid_info(hdev, "registering device /dev/%s%d\n",
ft260_tty_driver->name, dev->index);
- /* Send Feature Report to Configure FT260 as UART 9600-8-N-1 */
+ /* Configure UART to 9600n8 */
req.report = FT260_SYSTEM_SETTINGS;
req.request = FT260_SET_UART_CONFIG;
req.flow_ctrl = FT260_CFG_FLOW_CTRL_NONE;
@@ -1653,8 +1696,7 @@ static int ft260_uart_probe(struct hid_device *hdev, struct ft260_device *dev)
ret = ft260_hid_feature_report_set(hdev, (u8 *)&req, sizeof(req));
if (ret < 0) {
- hid_err(hdev, "ft260_hid_feature_report_set failed: %d\n",
- ret);
+ hid_err(hdev, "failed to configure uart: %d\n", ret);
goto err_hid_report;
}
@@ -1675,9 +1717,9 @@ static int ft260_probe(struct hid_device *hdev, const struct hid_device_id *id)
if (!hid_is_usb(hdev))
return -EINVAL;
-
- /* We cannot used devm_kzalloc here, because port has to survive until
- * destroy function call
+ /*
+ * We cannot use devm_kzalloc here because the port has to survive
+ * until destroy function call.
*/
dev = kzalloc_obj(*dev, GFP_KERNEL);
if (!dev) {
@@ -1726,15 +1768,12 @@ static int ft260_probe(struct hid_device *hdev, const struct hid_device_id *id)
spin_lock_init(&dev->read_lock);
init_completion(&dev->wait);
- if (!dev->ft260_is_serial) {
+ if (ret == FT260_IFACE_I2C)
ret = ft260_i2c_probe(hdev, dev);
- if (ret)
- goto err_hid_close;
- } else {
+ else
ret = ft260_uart_probe(hdev, dev);
- if (ret)
- goto err_hid_close;
- }
+ if (ret)
+ goto err_hid_close;
return 0;
@@ -1755,13 +1794,12 @@ static void ft260_remove(struct hid_device *hdev)
if (!dev)
return;
- if (dev->ft260_is_serial) {
- // FIXME:
+ if (dev->iface_type == FT260_IFACE_UART) {
cancel_work_sync(&dev->wakeup_work);
tty_port_unregister_device(&dev->port, ft260_tty_driver,
dev->index);
ft260_uart_port_remove(dev);
- /* dev still needed, so we will free it in _destroy func */
+ /* dev is still needed, so we will free it in _destroy func */
} else {
sysfs_remove_group(&hdev->dev.kobj, &ft260_attr_group);
i2c_del_adapter(&dev->adap);
@@ -1784,6 +1822,12 @@ static int ft260_raw_event(struct hid_device *hdev, struct hid_report *report,
return -1;
}
+ if (xfer->length > size - offsetof(struct ft260_input_report, data)) {
+ hid_err(hdev, "report %#02x: length %d exceeds HID report size\n",
+ xfer->report, xfer->length);
+ return -1;
+ }
+
if (xfer->report >= FT260_I2C_REPORT_MIN &&
xfer->report <= FT260_I2C_REPORT_MAX) {
bool complete_read;
@@ -1791,13 +1835,6 @@ static int ft260_raw_event(struct hid_device *hdev, struct hid_report *report,
ft260_dbg("i2c resp: rep %#02x len %d size %d\n",
xfer->report, xfer->length, size);
- if (xfer->length > size -
- offsetof(struct ft260_input_report, data)) {
- hid_err(hdev, "report %#02x: length %d exceeds HID report size\n",
- xfer->report, xfer->length);
- return -1;
- }
-
/*
* Hold read_lock so a timed-out ft260_i2c_read() cannot
* clear read_buf between the NULL check and the memcpy.
@@ -1824,12 +1861,11 @@ static int ft260_raw_event(struct hid_device *hdev, struct hid_report *report,
return 0;
- } else if (xfer->length > FT260_RD_DATA_MAX) {
- hid_err(hdev, "received data too long (%d)\n", xfer->length);
- return -EBADR;
} else if (xfer->report >= FT260_UART_REPORT_MIN &&
xfer->report <= FT260_UART_REPORT_MAX) {
return ft260_uart_receive_chars(dev, xfer->data, xfer->length);
+ } else if (xfer->report == FT260_UART_INTERRUPT_STATUS) {
+ return 0;
}
hid_err(hdev, "unhandled report %#02x\n", xfer->report);
@@ -1874,7 +1910,7 @@ static int __init ft260_driver_init(void)
goto err_reg_driver;
}
- ret = hid_register_driver(&(ft260_driver));
+ ret = hid_register_driver(&ft260_driver);
if (ret) {
pr_err("hid_register_driver failed: %d\n", ret);
goto err_reg_hid;
@@ -1892,7 +1928,7 @@ static int __init ft260_driver_init(void)
static void __exit ft260_driver_exit(void)
{
- hid_unregister_driver(&(ft260_driver));
+ hid_unregister_driver(&ft260_driver);
tty_unregister_driver(ft260_tty_driver);
tty_driver_kref_put(ft260_tty_driver);
}
--
2.43.0
^ permalink raw reply related [flat|nested] 43+ messages in thread
* [PATCH 03/13] HID: ft260: add GPIO support on top of UART
2026-08-22 21:39 [PATCH 00/13] HID: ft260: add UART and GPIO support, plus I2C fixes Michael Zaidman
2026-08-22 21:39 ` [PATCH 01/13] HID: ft260: add serial driver Michael Zaidman
2026-08-22 21:39 ` [PATCH 02/13] HID: ft260: uart: bring-up fixes Michael Zaidman
@ 2026-08-22 21:39 ` Michael Zaidman
2026-08-22 21:56 ` sashiko-bot
2026-08-25 7:44 ` Linus Walleij
2026-08-22 21:39 ` [PATCH 04/13] HID: ft260: i2c: reduce driver module loading time Michael Zaidman
` (10 subsequent siblings)
13 siblings, 2 replies; 43+ messages in thread
From: Michael Zaidman @ 2026-08-22 21:39 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: Linus Walleij, Bartosz Golaszewski, Germain Hebert, Rio Liu,
Bruno Giacomazzi, Christina Quast, linux-input, linux-gpio,
linux-i2c, linux-kernel, Michael Zaidman
Add gpiochip support for the FT260 multifunctional pins, including
GPIO2/GPIOA/GPIOG function selection via sysfs, and enable the
available GPIOs according to the active UART mode.
This supersedes the earlier pre-UART GPIO series:
https://lore.kernel.org/lkml/20230211115752.26276-2-michael.zaidman@gmail.com/
The gpio_chip.set callback returns int to match the current gpiolib
API, based on the adaptation originally contributed by Rio Liu
<rio@r26.me>.
Signed-off-by: Michael Zaidman <michael.zaidman@gmail.com>
---
drivers/hid/Kconfig | 10 +-
drivers/hid/hid-ft260.c | 660 +++++++++++++++++++++++++++++++++++-----
2 files changed, 586 insertions(+), 84 deletions(-)
diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig
index e898adb331ba..8e161c440346 100644
--- a/drivers/hid/Kconfig
+++ b/drivers/hid/Kconfig
@@ -386,12 +386,12 @@ config HID_EZKEY
Support for Ezkey BTC 8193 keyboard.
config HID_FT260
- tristate "FTDI FT260 USB HID to I2C/UART host support"
- depends on USB_HID && HIDRAW && I2C && TTY
+ tristate "FTDI FT260 USB HID to I2C/UART/GPIO host support"
+ depends on USB_HID && HIDRAW && I2C && TTY && GPIOLIB
help
- Provides I2C host adapter and UART/TTY functionality over USB-HID
- through the FT260 device. The customizable USB descriptor fields
- are exposed as sysfs attributes.
+ Provides I2C host adapter, UART/TTY, and GPIO functionality over
+ USB-HID through the FT260 device. The customizable USB descriptor
+ fields are exposed as sysfs attributes.
To compile this driver as a module, choose M here: the module
will be called hid-ft260.
diff --git a/drivers/hid/hid-ft260.c b/drivers/hid/hid-ft260.c
index b6c408267a68..cc3b8f5791de 100644
--- a/drivers/hid/hid-ft260.c
+++ b/drivers/hid/hid-ft260.c
@@ -19,6 +19,7 @@
#include <linux/tty_flip.h>
#include <linux/minmax.h>
#include <linux/unaligned.h>
+#include <linux/gpio/driver.h>
#ifdef DEBUG
static int ft260_debug = 1;
@@ -57,6 +58,11 @@ MODULE_PARM_DESC(debug, "Toggle FT260 debugging messages");
#define FT260_RD_DATA_MAX (180)
#define FT260_WR_I2C_DATA_MAX (60)
#define FT260_WR_UART_DATA_MAX (62)
+#define FT260_GPIOCHIP "ft260_gpio"
+#define FT260_GPIO_MAX (6)
+#define FT260_GPIO_EX_MAX (8)
+#define FT260_GPIO_TOTAL (FT260_GPIO_MAX + FT260_GPIO_EX_MAX)
+#define FT260_GPIO_MASK (~(0xffff << FT260_GPIO_TOTAL))
/*
* Device interface configuration.
@@ -151,7 +157,69 @@ enum {
FT260_IFACE_UART
};
-#define FT260_SET_REQUEST_VALUE(report_id) ((FT260_FEATURE << 8) | report_id)
+/* Multi-function pin functions */
+enum {
+ FT260_MFPIN_GPIO = 0x00,
+ FT260_MFPIN_SUSPOUT = 0x01,
+ FT260_MFPIN_PWREN = 0x02,
+ FT260_MFPIN_TX_ACTIVE = 0x03,
+ FT260_MFPIN_TX_LED = 0x04,
+ FT260_MFPIN_RX_LED = 0x05,
+ FT260_MFPIN_BCD_DET = 0x06,
+};
+
+enum {
+ FT260_GPIO_VALUE = 0x00,
+ FT260_GPIO_DIRECTION = 0x01,
+ FT260_GPIO_DIR_INPUT = 0x00,
+ FT260_GPIO_DIR_OUTPUT = 0x01,
+};
+
+/* GPIO offsets */
+enum {
+ FT260_GPIO_0 = (1 << 0),
+ FT260_GPIO_1 = (1 << 1),
+ FT260_GPIO_2 = (1 << 2),
+ FT260_GPIO_3 = (1 << 3),
+ FT260_GPIO_4 = (1 << 4),
+ FT260_GPIO_5 = (1 << 5),
+ FT260_GPIO_A = (1 << (FT260_GPIO_MAX + 0)),
+ FT260_GPIO_B = (1 << (FT260_GPIO_MAX + 1)),
+ FT260_GPIO_C = (1 << (FT260_GPIO_MAX + 2)),
+ FT260_GPIO_D = (1 << (FT260_GPIO_MAX + 3)),
+ FT260_GPIO_E = (1 << (FT260_GPIO_MAX + 4)),
+ FT260_GPIO_F = (1 << (FT260_GPIO_MAX + 5)),
+ FT260_GPIO_G = (1 << (FT260_GPIO_MAX + 6)),
+ FT260_GPIO_H = (1 << (FT260_GPIO_MAX + 7)),
+};
+
+/* GPIO groups */
+enum {
+ FT260_GPIO_WAKEUP = (FT260_GPIO_3),
+ FT260_GPIO_I2C_DEFAULT = (FT260_GPIO_0 | FT260_GPIO_1),
+ FT260_GPIO_UART_RX_TX = (FT260_GPIO_C | FT260_GPIO_D),
+ FT260_GPIO_UART_DCD_RI = (FT260_GPIO_4 | FT260_GPIO_5),
+ FT260_GPIO_UART_RTS_CTS = (FT260_GPIO_B | FT260_GPIO_E),
+ FT260_GPIO_UART_DTR_DSR = (FT260_GPIO_F | FT260_GPIO_H),
+ FT260_GPIO_UART_MODE_0_SET = (FT260_GPIO_UART_RX_TX |
+ FT260_GPIO_UART_DCD_RI |
+ FT260_GPIO_UART_RTS_CTS |
+ FT260_GPIO_UART_DTR_DSR),
+ FT260_GPIO_UART_MODE_1_SET = (FT260_GPIO_UART_DTR_DSR),
+ FT260_GPIO_UART_MODE_2_SET = (FT260_GPIO_UART_RTS_CTS),
+ FT260_GPIO_UART_MODE_3_SET = (FT260_GPIO_UART_RTS_CTS |
+ FT260_GPIO_UART_DTR_DSR),
+ FT260_GPIO_UART_MODE_4_SET = (FT260_GPIO_UART_MODE_3_SET),
+ FT260_GPIO_UART_DEFAULT = (FT260_GPIO_UART_MODE_0_SET),
+ FT260_GPIO_UART_MODE_1_CLR = (FT260_GPIO_UART_RX_TX |
+ FT260_GPIO_UART_RTS_CTS),
+ FT260_GPIO_UART_MODE_2_CLR = (FT260_GPIO_UART_RX_TX |
+ FT260_GPIO_UART_DTR_DSR),
+ FT260_GPIO_UART_MODE_3_CLR = (FT260_GPIO_UART_RX_TX),
+ FT260_GPIO_UART_MODES = (5),
+};
+
+#define FT260_SET_REQUEST_VALUE(report_id) ((FT260_FEATURE << 8) | (report_id))
/* Feature In reports */
@@ -171,10 +239,10 @@ struct ft260_get_system_status_report {
u8 uart_mode; /* 0 - OFF; 1 - RTS_CTS, 2 - DTR_DSR, */
/* 3 - XON_XOFF, 4 - No flow control */
u8 hid_over_i2c_en; /* 0 - disabled, 1 - enabled */
- u8 gpio2_function; /* 0 - GPIO, 1 - SUSPOUT, */
+ u8 gpio2_func; /* 0 - GPIO, 1 - SUSPOUT, */
/* 2 - PWREN, 4 - TX_LED */
- u8 gpioA_function; /* 0 - GPIO, 3 - TX_ACTIVE, 4 - TX_LED */
- u8 gpioG_function; /* 0 - GPIO, 2 - PWREN, */
+ u8 gpioa_func; /* 0 - GPIO, 3 - TX_ACTIVE, 4 - TX_LED */
+ u8 gpiog_func; /* 0 - GPIO, 2 - PWREN, */
/* 5 - RX_LED, 6 - BCD_DET */
u8 suspend_out_pol; /* 0 - active-high, 1 - active-low */
u8 enable_wakeup_int; /* 0 - disabled, 1 - enabled */
@@ -202,6 +270,18 @@ struct ft260_get_uart_settings_report {
u8 breaking; /* 0: no break */
} __packed;
+struct ft260_gpio_state {
+ u8 vals; /* GPIO[0-5] values in bits 0 - 5 */
+ u8 dirs; /* GPIO[0-5] directions, 0 - in, 1 - out */
+ u8 ex_vals; /* GPIO[A-H] values in bits 0 - 7 */
+ u8 ex_dirs; /* GPIO[A-H] directions, 0 - in, 1 - out */
+} __packed;
+
+struct ft260_gpio_read_request_report {
+ u8 report; /* FT260_GPIO */
+ struct ft260_gpio_state gpio;
+} __packed;
+
/* Feature Out reports */
struct ft260_set_system_clock_report {
@@ -223,6 +303,12 @@ struct ft260_set_uart_mode_report {
/* 3 - XON_XOFF, 4 - No flow control */
} __packed;
+struct ft260_set_uart_dcd_ri_report {
+ u8 report; /* FT260_SYSTEM_SETTINGS */
+ u8 request; /* FT260_ENABLE_UART_DCD_RI */
+ u8 uart_dcd_ri; /* Pins func: 0 - GPIO4,GPIO5, 1 - DCD,RI */
+} __packed;
+
struct ft260_set_i2c_reset_report {
u8 report; /* FT260_SYSTEM_SETTINGS */
u8 request; /* FT260_SET_I2C_RESET */
@@ -234,6 +320,33 @@ struct ft260_set_i2c_speed_report {
__le16 clock; /* I2C bus clock in range 60-3400 KHz */
} __packed;
+struct ft260_set_gpio2_func_report {
+ u8 report; /* FT260_SYSTEM_SETTINGS */
+ u8 request; /* FT260_SELECT_GPIO2_FUNC */
+ u8 gpio2_func; /* Pin func: 0 - GPIO, 1 - SUSPOUT, */
+ /* 2 - PWREN# (active-low), 4 - TX_LED */
+} __packed;
+
+struct ft260_set_gpioa_func_report {
+ u8 report; /* FT260_SYSTEM_SETTINGS */
+ u8 request; /* FT260_SELECT_GPIOA_FUNC */
+ u8 gpioa_func; /* Pin func: 0 - GPIO, */
+ /* 3 - TX_ACTIVE, 4 - TX_LED */
+} __packed;
+
+struct ft260_set_gpiog_func_report {
+ u8 report; /* FT260_SYSTEM_SETTINGS */
+ u8 request; /* FT260_SELECT_GPIOG_FUNC */
+ u8 gpiog_func; /* Pin func: 0 - GPIO, */
+ /* 2 - PWREN# (active-low), */
+ /* 5 - RX_LED, 6 - BCD_DET */
+} __packed;
+
+struct ft260_gpio_write_request_report {
+ u8 report; /* FT260_GPIO */
+ struct ft260_gpio_state gpio;
+} __packed;
+
/* Data transfer reports */
struct ft260_i2c_write_request_report {
@@ -280,32 +393,32 @@ struct ft260_configure_uart_request_report {
/* UART interface configuration */
enum {
- FT260_CFG_FLOW_CTRL_OFF = 0x00,
- FT260_CFG_FLOW_CTRL_RTS_CTS = 0x01,
- FT260_CFG_FLOW_CTRL_DTR_DSR = 0x02,
- FT260_CFG_FLOW_CTRL_XON_XOFF = 0x03,
- FT260_CFG_FLOW_CTRL_NONE = 0x04,
+ FT260_UART_CFG_FLOW_CTRL_OFF = 0x00,
+ FT260_UART_CFG_FLOW_CTRL_RTS_CTS = 0x01,
+ FT260_UART_CFG_FLOW_CTRL_DTR_DSR = 0x02,
+ FT260_UART_CFG_FLOW_CTRL_XON_XOFF = 0x03,
+ FT260_UART_CFG_FLOW_CTRL_NONE = 0x04,
- FT260_CFG_DATA_BITS_7 = 0x07,
- FT260_CFG_DATA_BITS_8 = 0x08,
+ FT260_UART_CFG_DATA_BITS_7 = 0x07,
+ FT260_UART_CFG_DATA_BITS_8 = 0x08,
- FT260_CFG_PAR_NO = 0x00,
- FT260_CFG_PAR_ODD = 0x01,
- FT260_CFG_PAR_EVEN = 0x02,
- FT260_CFG_PAR_HIGH = 0x03,
- FT260_CFG_PAR_LOW = 0x04,
+ FT260_UART_CFG_PAR_NO = 0x00,
+ FT260_UART_CFG_PAR_ODD = 0x01,
+ FT260_UART_CFG_PAR_EVEN = 0x02,
+ FT260_UART_CFG_PAR_HIGH = 0x03,
+ FT260_UART_CFG_PAR_LOW = 0x04,
- FT260_CFG_STOP_ONE_BIT = 0x00,
- FT260_CFG_STOP_TWO_BIT = 0x02,
+ FT260_UART_CFG_STOP_ONE_BIT = 0x00,
+ FT260_UART_CFG_STOP_TWO_BIT = 0x02,
- FT260_CFG_BREAKING_NO = 0x00,
- FT260_CFG_BEAKING_YES = 0x01,
+ FT260_UART_CFG_BREAKING_NO = 0x00,
+ FT260_UART_CFG_BEAKING_YES = 0x01,
- FT260_CFG_BAUD_MIN = 1200,
- FT260_CFG_BAUD_MAX = 12000000,
+ FT260_UART_CFG_BAUD_MIN = 1200,
+ FT260_UART_CFG_BAUD_MAX = 12000000,
};
-#define FT260_UART_EN_PW_SAVE_BAUD (4800)
+#define FT260_UART_EN_PW_SAVE_BAUD (4800)
#define UART_COUNT_MAX (4) /* Number of supported UARTs */
#define XMIT_FIFO_SIZE (PAGE_SIZE)
@@ -321,6 +434,7 @@ struct ft260_device {
struct i2c_adapter adap;
struct hid_device *hdev;
int iface_type;
+ int iface_id;
struct list_head device_list;
struct tty_port port;
/* tty port index */
@@ -343,6 +457,10 @@ struct ft260_device {
u16 read_idx;
u16 read_len;
u16 clock;
+ u16 gpio_en;
+ struct gpio_chip *gc;
+ struct ft260_gpio_state gpio;
+ u16 gpio_uart_mode[FT260_GPIO_UART_MODES];
};
static int ft260_hid_feature_report_get(struct hid_device *hdev,
@@ -375,8 +493,6 @@ static int ft260_hid_feature_report_set(struct hid_device *hdev, u8 *data,
if (!buf)
return -ENOMEM;
- buf[0] = FT260_SYSTEM_SETTINGS;
-
ret = hid_hw_raw_request(hdev, buf[0], buf, len, HID_FEATURE_REPORT,
HID_REQ_SET_REPORT);
@@ -389,6 +505,7 @@ static int ft260_i2c_reset(struct hid_device *hdev)
struct ft260_set_i2c_reset_report report;
int ret;
+ report.report = FT260_SYSTEM_SETTINGS;
report.request = FT260_SET_I2C_RESET;
ret = ft260_hid_feature_report_set(hdev, (u8 *)&report, sizeof(report));
@@ -906,6 +1023,322 @@ static const struct i2c_algorithm ft260_i2c_algo = {
.functionality = ft260_functionality,
};
+static void ft260_gpio_en_set(struct ft260_device *dev, u16 bitmap)
+{
+ dev->gpio_en |= bitmap & FT260_GPIO_MASK;
+}
+
+static void ft260_gpio_en_clr(struct ft260_device *dev, u16 bitmap)
+{
+ dev->gpio_en &= ~bitmap & FT260_GPIO_MASK;
+}
+
+static void ft260_gpio_en_update(struct hid_device *hdev, u8 req, u8 value)
+{
+ u16 bitmap;
+ struct ft260_device *dev = hid_get_drvdata(hdev);
+
+ switch (req) {
+
+ case FT260_SET_I2C_MODE:
+ bitmap = FT260_GPIO_I2C_DEFAULT;
+ break;
+ case FT260_SET_UART_MODE:
+ switch (value) {
+ case FT260_UART_CFG_FLOW_CTRL_OFF:
+ bitmap = (u16)FT260_GPIO_MASK;
+ break;
+ case FT260_UART_CFG_FLOW_CTRL_RTS_CTS:
+ bitmap = FT260_GPIO_UART_MODE_1_CLR;
+ break;
+ case FT260_UART_CFG_FLOW_CTRL_DTR_DSR:
+ bitmap = FT260_GPIO_UART_MODE_2_CLR;
+ break;
+ case FT260_UART_CFG_FLOW_CTRL_XON_XOFF:
+ case FT260_UART_CFG_FLOW_CTRL_NONE:
+ bitmap = FT260_GPIO_UART_MODE_3_CLR;
+ break;
+ default:
+ return;
+ }
+ ft260_gpio_en_clr(dev, bitmap);
+ bitmap = dev->gpio_uart_mode[value];
+ ft260_gpio_en_set(dev, bitmap);
+ goto exit;
+
+ case FT260_ENABLE_UART_DCD_RI:
+ bitmap = FT260_GPIO_UART_DCD_RI;
+ break;
+
+ case FT260_SELECT_GPIO2_FUNC:
+ bitmap = FT260_GPIO_2;
+ break;
+ case FT260_SELECT_GPIOA_FUNC:
+ bitmap = FT260_GPIO_A;
+ break;
+ case FT260_SELECT_GPIOG_FUNC:
+ bitmap = FT260_GPIO_G;
+ break;
+ default:
+ return;
+ }
+
+ if (value == FT260_MFPIN_GPIO)
+ ft260_gpio_en_set(dev, bitmap);
+ else
+ ft260_gpio_en_clr(dev, bitmap);
+exit:
+ hid_info(hdev, "enabled GPIOs: %04x\n", dev->gpio_en);
+}
+
+static int ft260_gpio_set(struct gpio_chip *gc, u32 offset, int value)
+{
+ int ret = 0;
+ struct ft260_gpio_write_request_report rep;
+ struct ft260_device *dev = gpiochip_get_data(gc);
+ struct hid_device *hdev = dev->hdev;
+
+ if (offset >= FT260_GPIO_TOTAL) {
+ hid_err(hdev, "%s: invalid offset %d\n", __func__, offset);
+ return -EINVAL;
+ }
+
+ ft260_dbg("offset %d val %d\n", offset, value);
+
+ mutex_lock(&dev->lock);
+
+ if (!(dev->gpio_en & (1 << offset))) {
+ hid_err(hdev, "%s: wrong pin function %d\n", __func__, offset);
+ ret = -EINVAL;
+ goto exit;
+ }
+
+ rep.report = FT260_GPIO;
+ rep.gpio = dev->gpio;
+
+ if (offset < FT260_GPIO_MAX) {
+ if (value)
+ rep.gpio.vals |= !!value << offset;
+ else
+ rep.gpio.vals &= ~(1 << offset);
+ } else {
+ offset = offset - FT260_GPIO_MAX;
+ if (value)
+ rep.gpio.ex_vals |= !!value << offset;
+ else
+ rep.gpio.ex_vals &= ~(1 << offset);
+ }
+
+ ft260_dbg("dirs %#02x vals %#02x ex_dir %#02x ex_vals %#02x\n",
+ rep.gpio.dirs, rep.gpio.vals,
+ rep.gpio.ex_dirs, rep.gpio.ex_vals);
+
+ ret = ft260_hid_feature_report_set(hdev, (u8 *)&rep, sizeof(rep));
+ if (unlikely(ret < 0)) {
+ hid_err(hdev, "%s: cannot set GPIO: %d\n", __func__, ret);
+ goto exit;
+ }
+
+ dev->gpio = rep.gpio;
+ ret = 0;
+exit:
+ mutex_unlock(&dev->lock);
+ return ret;
+}
+
+static int ft260_gpio_direction_set(struct gpio_chip *gc, u32 offset,
+ int value, int direction)
+{
+ int ret;
+ struct ft260_gpio_read_request_report buf;
+ struct ft260_gpio_write_request_report *rep;
+ struct ft260_device *dev = gpiochip_get_data(gc);
+ struct hid_device *hdev = dev->hdev;
+
+ if (offset >= FT260_GPIO_TOTAL) {
+ hid_err(hdev, "%s: invalid offset %d\n", __func__, offset);
+ return -EINVAL;
+ }
+
+ ft260_dbg("offset %d val %d direction %d\n", offset, value, direction);
+
+ mutex_lock(&dev->lock);
+
+ if (!(dev->gpio_en & (1 << offset))) {
+ hid_err(hdev, "%s: wrong pin function %d\n", __func__, offset);
+ ret = -EIO;
+ goto exit;
+ }
+
+ ret = ft260_hid_feature_report_get(hdev, FT260_GPIO, (u8 *)&buf, sizeof(buf));
+ if (unlikely(ret < 0)) {
+ hid_err(hdev, "%s: cannot get GPIO: %d\n", __func__, ret);
+ goto exit;
+ }
+
+ rep = (struct ft260_gpio_write_request_report *)&buf;
+
+ if (direction == FT260_GPIO_DIR_OUTPUT)
+ if (offset < FT260_GPIO_MAX)
+ rep->gpio.dirs |= 1 << offset;
+ else
+ rep->gpio.ex_dirs |= 1 << (offset - FT260_GPIO_MAX);
+ else
+ if (offset < FT260_GPIO_MAX)
+ rep->gpio.dirs &= ~(1 << offset);
+ else
+ rep->gpio.ex_dirs &= ~(1 << (offset - FT260_GPIO_MAX));
+
+ ft260_dbg("dirs %#02x val %#02x ex_dirs %#02x ex_vals %#02x\n",
+ rep->gpio.dirs, rep->gpio.vals,
+ rep->gpio.ex_dirs, rep->gpio.ex_vals);
+
+ ret = ft260_hid_feature_report_set(hdev, (u8 *)rep, sizeof(*rep));
+ if (unlikely(ret < 0)) {
+ hid_err(hdev, "%s: cannot set GPIO: %d\n", __func__, ret);
+ goto exit;
+ }
+
+ dev->gpio = rep->gpio;
+ mutex_unlock(&dev->lock);
+
+ if (direction == FT260_GPIO_DIR_OUTPUT)
+ ft260_gpio_set(gc, offset, value);
+
+ return 0;
+exit:
+ mutex_unlock(&dev->lock);
+ return ret;
+}
+
+static int ft260_gpio_direction_output(struct gpio_chip *gc,
+ u32 offset, int value)
+{
+ return ft260_gpio_direction_set(gc, offset, value,
+ FT260_GPIO_DIR_OUTPUT);
+}
+
+static int ft260_gpio_direction_input(struct gpio_chip *gc, u32 offset)
+{
+ return ft260_gpio_direction_set(gc, offset, 0,
+ FT260_GPIO_DIR_INPUT);
+}
+
+static int ft260_gpio_get_all(struct gpio_chip *gc, int item)
+{
+ int ret;
+ struct ft260_gpio_read_request_report rep;
+ struct ft260_device *dev = gpiochip_get_data(gc);
+ struct hid_device *hdev = dev->hdev;
+
+ ret = ft260_hid_feature_report_get(hdev, FT260_GPIO, (u8 *)&rep, sizeof(rep));
+ if (unlikely(ret < 0)) {
+ hid_err(hdev, "%s: cannot get GPIO: %d\n", __func__, ret);
+ goto exit;
+ }
+
+ if (item == FT260_GPIO_VALUE)
+ ret = (rep.gpio.ex_vals << FT260_GPIO_MAX) | rep.gpio.vals;
+ else
+ ret = (rep.gpio.ex_dirs << FT260_GPIO_MAX) | rep.gpio.dirs;
+exit:
+ return ret;
+}
+
+static int ft260_gpio_get_direction(struct gpio_chip *gc, u32 offset)
+{
+ int ret = ft260_gpio_get_all(gc, FT260_GPIO_DIRECTION);
+
+ if (ret < 0)
+ return ret;
+ return !((ret >> offset) & 1);
+}
+
+static int ft260_gpio_get(struct gpio_chip *gc, u32 offset)
+{
+ int ret = ft260_gpio_get_all(gc, FT260_GPIO_VALUE);
+
+ if (ret < 0)
+ return ret;
+ return (ret >> offset) & 1;
+}
+
+static int ft260_gpio_init(struct ft260_device *dev,
+ struct ft260_get_system_status_report *cfg)
+{
+ int ret;
+ int label_sz;
+ char *label;
+ struct ft260_get_chip_version_report ver;
+ struct hid_device *hdev = dev->hdev;
+ char prefix[] = "ft260_";
+ u8 mode = cfg->chip_mode;
+
+ hid_info(hdev, "initialize gpio chip\n");
+
+ dev->gpio_uart_mode[0] = (u16)FT260_GPIO_UART_MODE_0_SET;
+ dev->gpio_uart_mode[1] = (u16)FT260_GPIO_UART_MODE_1_SET;
+ dev->gpio_uart_mode[2] = (u16)FT260_GPIO_UART_MODE_2_SET;
+ dev->gpio_uart_mode[3] = (u16)FT260_GPIO_UART_MODE_3_SET;
+ dev->gpio_uart_mode[4] = (u16)FT260_GPIO_UART_MODE_4_SET;
+
+ if (mode == FT260_MODE_ALL)
+ mode = FT260_MODE_BOTH;
+
+ if (mode & FT260_MODE_UART)
+ dev->gpio_en |= dev->gpio_uart_mode[cfg->uart_mode];
+ else
+ dev->gpio_en |= FT260_GPIO_UART_DEFAULT;
+
+ if (!(mode & FT260_MODE_I2C))
+ dev->gpio_en |= FT260_GPIO_I2C_DEFAULT;
+
+ if (cfg->gpio2_func == FT260_MFPIN_GPIO)
+ dev->gpio_en |= FT260_GPIO_2;
+ if (cfg->enable_wakeup_int == FT260_MFPIN_GPIO)
+ dev->gpio_en |= FT260_GPIO_3;
+ if (cfg->gpioa_func == FT260_MFPIN_GPIO)
+ dev->gpio_en |= FT260_GPIO_A;
+ if (cfg->gpiog_func == FT260_MFPIN_GPIO)
+ dev->gpio_en |= FT260_GPIO_G;
+
+ hid_info(hdev, "enabled GPIOs: %04x\n", dev->gpio_en);
+
+ dev->gc = devm_kzalloc(&hdev->dev, sizeof(*dev->gc), GFP_KERNEL);
+ if (!dev->gc)
+ return -ENOMEM;
+
+ label_sz = strlen(dev_name(&hdev->dev)) + strlen(prefix) + 1;
+ label = devm_kzalloc(&hdev->dev, label_sz, GFP_KERNEL);
+ if (!label) {
+ ret = -ENOMEM;
+ goto exit;
+ }
+ snprintf(label, label_sz, "%s%s", prefix, dev_name(&hdev->dev));
+ hid_info(hdev, "initialize gpio chip on %s\n", label);
+
+ dev->gc->label = label;
+ dev->gc->direction_input = ft260_gpio_direction_input;
+ dev->gc->direction_output = ft260_gpio_direction_output;
+ dev->gc->get_direction = ft260_gpio_get_direction;
+ dev->gc->set = ft260_gpio_set;
+ dev->gc->get = ft260_gpio_get;
+ dev->gc->base = -1;
+ dev->gc->ngpio = FT260_GPIO_TOTAL;
+ dev->gc->can_sleep = true;
+ dev->gc->parent = &hdev->dev;
+
+ /* Wakeup chip */
+ (void)ft260_hid_feature_report_get(dev->hdev, FT260_CHIP_VERSION,
+ (u8 *)&ver, sizeof(ver));
+
+ ret = devm_gpiochip_add_data(&hdev->dev, dev->gc, dev);
+ if (ret < 0)
+ hid_err(hdev, "cannot add GPIO chip %d\n", ret);
+exit:
+ return ret;
+}
+
static int ft260_get_system_config(struct hid_device *hdev,
struct ft260_get_system_status_report *cfg)
{
@@ -921,29 +1354,36 @@ static int ft260_get_system_config(struct hid_device *hdev,
return 0;
}
-static int ft260_get_interface_type(struct hid_device *hdev, struct ft260_device *dev)
+static int ft260_get_interface_type(struct ft260_device *dev,
+ struct ft260_get_system_status_report *cfg)
+
{
- struct ft260_get_system_status_report cfg;
- struct usb_interface *usbif = to_usb_interface(hdev->dev.parent);
- int interface = usbif->cur_altsetting->desc.bInterfaceNumber;
int ret;
+ struct hid_device *hdev = dev->hdev;
+ struct usb_interface *usbif = to_usb_interface(hdev->dev.parent);
- ret = ft260_get_system_config(hdev, &cfg);
+ dev->iface_id = usbif->cur_altsetting->desc.bInterfaceNumber;
+
+ ret = ft260_get_system_config(hdev, cfg);
if (ret < 0)
return ret;
- ft260_dbg("interface: 0x%02x\n", interface);
- ft260_dbg("chip mode: 0x%02x\n", cfg.chip_mode);
- ft260_dbg("clock_ctl: 0x%02x\n", cfg.clock_ctl);
- ft260_dbg("i2c_enable: 0x%02x\n", cfg.i2c_enable);
- ft260_dbg("uart_mode: 0x%02x\n", cfg.uart_mode);
+ ft260_dbg("interface: 0x%02x\n", dev->iface_id);
+ ft260_dbg("chip mode: 0x%02x\n", cfg->chip_mode);
+ ft260_dbg("clock_ctl: 0x%02x\n", cfg->clock_ctl);
+ ft260_dbg("i2c_enable: 0x%02x\n", cfg->i2c_enable);
+ ft260_dbg("uart_mode: 0x%02x\n", cfg->uart_mode);
+ ft260_dbg("gpio2_func: 0x%02x\n", cfg->gpio2_func);
+ ft260_dbg("gpioA_func: 0x%02x\n", cfg->gpioa_func);
+ ft260_dbg("gpioG_func: 0x%02x\n", cfg->gpiog_func);
+ ft260_dbg("wakeup_int: 0x%02x\n", cfg->enable_wakeup_int);
- dev->power_saving_en = cfg.power_saving_en;
+ dev->power_saving_en = cfg->power_saving_en;
- switch (cfg.chip_mode) {
+ switch (cfg->chip_mode) {
case FT260_MODE_ALL:
case FT260_MODE_BOTH:
- if (interface == 1)
+ if (dev->iface_id == 1)
ret = FT260_IFACE_UART;
else
ret = FT260_IFACE_I2C;
@@ -984,6 +1424,10 @@ static int ft260_word_show(struct hid_device *hdev, int id, u8 *cfg, int len,
return scnprintf(buf, PAGE_SIZE, "%d\n", le16_to_cpu(*field));
}
+static void ft260_attr_dummy_func(struct hid_device *hdev, u8 req, u16 value)
+{
+}
+
#define FT260_ATTR_SHOW(name, reptype, id, type, func) \
static ssize_t name##_show(struct device *kdev, \
struct device_attribute *attr, char *buf) \
@@ -1004,37 +1448,42 @@ static int ft260_word_show(struct hid_device *hdev, int id, u8 *cfg, int len,
FT260_ATTR_SHOW(name, ft260_get_i2c_status_report, \
FT260_I2C_STATUS, __le16, ft260_word_show)
-#define FT260_ATTR_STORE(name, reptype, id, req, type, ctype, func) \
+#define FT260_ATTR_STORE(name, reptype, id, req, type, ctype, strtou, func) \
static ssize_t name##_store(struct device *kdev, \
struct device_attribute *attr, \
const char *buf, size_t count) \
{ \
struct reptype rep; \
struct hid_device *hdev = to_hid_device(kdev); \
+ struct ft260_device *dev = hid_get_drvdata(hdev); \
type name; \
int ret; \
\
- if (!func(buf, 10, (ctype *)&name)) { \
+ if (!strtou(buf, 10, (ctype *)&name)) { \
rep.name = name; \
rep.report = id; \
rep.request = req; \
+ mutex_lock(&dev->lock); \
ret = ft260_hid_feature_report_set(hdev, (u8 *)&rep, \
sizeof(rep)); \
- if (!ret) \
- ret = count; \
+ if (ret < 0) \
+ hid_err(hdev, "%s: failed!\n", __func__); \
+ else \
+ func(hdev, req, name); \
+ mutex_unlock(&dev->lock); \
} else { \
ret = -EINVAL; \
} \
return ret; \
}
-#define FT260_BYTE_ATTR_STORE(name, reptype, req) \
+#define FT260_BYTE_ATTR_STORE(name, reptype, req, func) \
FT260_ATTR_STORE(name, reptype, FT260_SYSTEM_SETTINGS, req, \
- u8, u8, kstrtou8)
+ u8, u8, kstrtou8, func)
-#define FT260_WORD_ATTR_STORE(name, reptype, req) \
+#define FT260_WORD_ATTR_STORE(name, reptype, req, func) \
FT260_ATTR_STORE(name, reptype, FT260_SYSTEM_SETTINGS, req, \
- __le16, u16, kstrtou16)
+ __le16, u16, kstrtou16, func)
FT260_SSTAT_ATTR_SHOW(chip_mode);
static DEVICE_ATTR_RO(chip_mode);
@@ -1048,27 +1497,46 @@ static DEVICE_ATTR_RO(suspend_status);
FT260_SSTAT_ATTR_SHOW(hid_over_i2c_en);
static DEVICE_ATTR_RO(hid_over_i2c_en);
+FT260_SSTAT_ATTR_SHOW(gpio2_func);
+FT260_BYTE_ATTR_STORE(gpio2_func, ft260_set_gpio2_func_report,
+ FT260_SELECT_GPIO2_FUNC, ft260_gpio_en_update);
+static DEVICE_ATTR_RW(gpio2_func);
+
+FT260_SSTAT_ATTR_SHOW(gpioa_func);
+FT260_BYTE_ATTR_STORE(gpioa_func, ft260_set_gpioa_func_report,
+ FT260_SELECT_GPIOA_FUNC, ft260_gpio_en_update);
+static DEVICE_ATTR_RW(gpioa_func);
+
+FT260_SSTAT_ATTR_SHOW(gpiog_func);
+FT260_BYTE_ATTR_STORE(gpiog_func, ft260_set_gpiog_func_report,
+ FT260_SELECT_GPIOG_FUNC, ft260_gpio_en_update);
+static DEVICE_ATTR_RW(gpiog_func);
+
FT260_SSTAT_ATTR_SHOW(power_saving_en);
static DEVICE_ATTR_RO(power_saving_en);
FT260_SSTAT_ATTR_SHOW(i2c_enable);
FT260_BYTE_ATTR_STORE(i2c_enable, ft260_set_i2c_mode_report,
- FT260_SET_I2C_MODE);
+ FT260_SET_I2C_MODE, ft260_gpio_en_update);
static DEVICE_ATTR_RW(i2c_enable);
FT260_SSTAT_ATTR_SHOW(uart_mode);
FT260_BYTE_ATTR_STORE(uart_mode, ft260_set_uart_mode_report,
- FT260_SET_UART_MODE);
+ FT260_SET_UART_MODE, ft260_gpio_en_update);
static DEVICE_ATTR_RW(uart_mode);
+FT260_BYTE_ATTR_STORE(uart_dcd_ri, ft260_set_uart_dcd_ri_report,
+ FT260_ENABLE_UART_DCD_RI, ft260_gpio_en_update);
+static DEVICE_ATTR_WO(uart_dcd_ri);
+
FT260_SSTAT_ATTR_SHOW(clock_ctl);
FT260_BYTE_ATTR_STORE(clock_ctl, ft260_set_system_clock_report,
- FT260_SET_CLOCK);
+ FT260_SET_CLOCK, ft260_attr_dummy_func);
static DEVICE_ATTR_RW(clock_ctl);
FT260_I2CST_ATTR_SHOW(clock);
FT260_WORD_ATTR_STORE(clock, ft260_set_i2c_speed_report,
- FT260_SET_I2C_CLOCK_SPEED);
+ FT260_SET_I2C_CLOCK_SPEED, ft260_attr_dummy_func);
static DEVICE_ATTR_RW(clock);
static ssize_t i2c_reset_store(struct device *kdev,
@@ -1092,7 +1560,11 @@ static const struct attribute_group ft260_attr_group = {
&dev_attr_hid_over_i2c_en.attr,
&dev_attr_power_saving_en.attr,
&dev_attr_i2c_enable.attr,
+ &dev_attr_gpio2_func.attr,
+ &dev_attr_gpioa_func.attr,
+ &dev_attr_gpiog_func.attr,
&dev_attr_uart_mode.attr,
+ &dev_attr_uart_dcd_ri.attr,
&dev_attr_clock_ctl.attr,
&dev_attr_i2c_reset.attr,
&dev_attr_clock.attr,
@@ -1342,33 +1814,33 @@ static int ft260_uart_change_speed(struct ft260_device *port,
switch (termios->c_cflag & CSIZE) {
case CS7:
- req.data_bit = FT260_CFG_DATA_BITS_7;
+ req.data_bit = FT260_UART_CFG_DATA_BITS_7;
break;
case CS5:
case CS6:
hid_err(hdev, "invalid data bit size, setting a default\n");
- req.data_bit = FT260_CFG_DATA_BITS_8;
+ req.data_bit = FT260_UART_CFG_DATA_BITS_8;
termios->c_cflag &= ~CSIZE;
termios->c_cflag |= CS8;
break;
default:
case CS8:
- req.data_bit = FT260_CFG_DATA_BITS_8;
+ req.data_bit = FT260_UART_CFG_DATA_BITS_8;
break;
}
req.stop_bit = (termios->c_cflag & CSTOPB) ?
- FT260_CFG_STOP_TWO_BIT : FT260_CFG_STOP_ONE_BIT;
+ FT260_UART_CFG_STOP_TWO_BIT : FT260_UART_CFG_STOP_ONE_BIT;
if (termios->c_cflag & PARENB) {
req.parity = (termios->c_cflag & PARODD) ?
- FT260_CFG_PAR_ODD : FT260_CFG_PAR_EVEN;
+ FT260_UART_CFG_PAR_ODD : FT260_UART_CFG_PAR_EVEN;
} else {
- req.parity = FT260_CFG_PAR_NO;
+ req.parity = FT260_UART_CFG_PAR_NO;
}
baud = tty_termios_baud_rate(termios);
- if (baud == 0 || baud < FT260_CFG_BAUD_MIN || baud > FT260_CFG_BAUD_MAX) {
+ if (baud == 0 || baud < FT260_UART_CFG_BAUD_MIN || baud > FT260_UART_CFG_BAUD_MAX) {
struct tty_struct *tty = tty_port_tty_get(&port->port);
hid_err(hdev, "invalid baud rate %d\n", baud);
@@ -1385,9 +1857,9 @@ static int ft260_uart_change_speed(struct ft260_device *port,
put_unaligned_le32(cpu_to_le32(baud), &req.baudrate);
if (termios->c_cflag & CRTSCTS)
- req.flow_ctrl = FT260_CFG_FLOW_CTRL_RTS_CTS;
+ req.flow_ctrl = FT260_UART_CFG_FLOW_CTRL_RTS_CTS;
else
- req.flow_ctrl = FT260_CFG_FLOW_CTRL_OFF;
+ req.flow_ctrl = FT260_UART_CFG_FLOW_CTRL_OFF;
ft260_dbg("configured termios: flow control: %d, baudrate: %d, ",
req.flow_ctrl, baud);
@@ -1395,12 +1867,18 @@ static int ft260_uart_change_speed(struct ft260_device *port,
req.data_bit, req.parity,
req.stop_bit, req.breaking);
- req.flow_ctrl = FT260_CFG_FLOW_CTRL_NONE;
- req.breaking = FT260_CFG_BREAKING_NO;
+ req.flow_ctrl = FT260_UART_CFG_FLOW_CTRL_NONE;
+ req.breaking = FT260_UART_CFG_BREAKING_NO;
+
+ mutex_lock(&port->lock);
ret = ft260_hid_feature_report_set(hdev, (u8 *)&req, sizeof(req));
if (ret < 0)
hid_err(hdev, "failed to change termios: %d\n", ret);
+ else
+ ft260_gpio_en_update(hdev, FT260_SET_UART_MODE, req.flow_ctrl);
+
+ mutex_unlock(&port->lock);
return ret;
}
@@ -1611,9 +2089,11 @@ static const struct tty_port_operations ft260_uart_port_ops = {
static struct tty_driver *ft260_tty_driver;
-static int ft260_i2c_probe(struct hid_device *hdev, struct ft260_device *dev)
+static int ft260_i2c_probe(struct ft260_device *dev,
+ struct ft260_get_system_status_report *cfg)
{
int ret;
+ struct hid_device *hdev = dev->hdev;
hid_info(hdev, "USB HID v%x.%02x Device [%s] on %s\n",
hdev->version >> 8, hdev->version & 0xff, hdev->name,
@@ -1640,11 +2120,16 @@ static int ft260_i2c_probe(struct hid_device *hdev, struct ft260_device *dev)
return ret;
}
+ ret = ft260_gpio_init(dev, cfg);
+ if (ret)
+ goto err_i2c_free;
+
ret = sysfs_create_group(&hdev->dev.kobj, &ft260_attr_group);
if (ret < 0) {
hid_err(hdev, "failed to create sysfs attrs\n");
goto err_i2c_free;
}
+
return 0;
err_i2c_free:
@@ -1652,14 +2137,15 @@ static int ft260_i2c_probe(struct hid_device *hdev, struct ft260_device *dev)
return ret;
}
-static int ft260_uart_probe(struct hid_device *hdev, struct ft260_device *dev)
+static int ft260_uart_probe(struct ft260_device *dev,
+ struct ft260_get_system_status_report *cfg)
{
struct ft260_configure_uart_request_report req;
- int ret;
+ struct hid_device *hdev = dev->hdev;
struct device *devt;
+ int ret;
INIT_WORK(&dev->wakeup_work, ft260_uart_do_wakeup);
- // FIXME: are all kfifo access secured by lock? with irq or not?
ft260_uart_wakeup_workaraund_enable(dev, true);
/* Work not started at this point */
timer_setup(&dev->wakeup_timer, ft260_uart_start_wakeup, 0);
@@ -1687,12 +2173,12 @@ static int ft260_uart_probe(struct hid_device *hdev, struct ft260_device *dev)
/* Configure UART to 9600n8 */
req.report = FT260_SYSTEM_SETTINGS;
req.request = FT260_SET_UART_CONFIG;
- req.flow_ctrl = FT260_CFG_FLOW_CTRL_NONE;
+ req.flow_ctrl = FT260_UART_CFG_FLOW_CTRL_NONE;
put_unaligned_le32(cpu_to_le32(9600), &req.baudrate);
- req.data_bit = FT260_CFG_DATA_BITS_8;
- req.parity = FT260_CFG_PAR_NO;
- req.stop_bit = FT260_CFG_STOP_ONE_BIT;
- req.breaking = FT260_CFG_BREAKING_NO;
+ req.data_bit = FT260_UART_CFG_DATA_BITS_8;
+ req.parity = FT260_UART_CFG_PAR_NO;
+ req.stop_bit = FT260_UART_CFG_STOP_ONE_BIT;
+ req.breaking = FT260_UART_CFG_BREAKING_NO;
ret = ft260_hid_feature_report_set(hdev, (u8 *)&req, sizeof(req));
if (ret < 0) {
@@ -1700,6 +2186,20 @@ static int ft260_uart_probe(struct hid_device *hdev, struct ft260_device *dev)
goto err_hid_report;
}
+ cfg->uart_mode = FT260_UART_CFG_FLOW_CTRL_NONE;
+
+ if (dev->iface_id == 0) {
+ ret = ft260_gpio_init(dev, cfg);
+ if (ret)
+ goto err_hid_report;
+
+ ret = sysfs_create_group(&hdev->dev.kobj, &ft260_attr_group);
+ if (ret < 0) {
+ hid_err(hdev, "failed to create sysfs attrs\n");
+ goto err_hid_report;
+ }
+ }
+
return 0;
err_hid_report:
@@ -1713,6 +2213,7 @@ static int ft260_probe(struct hid_device *hdev, const struct hid_device_id *id)
{
struct ft260_device *dev;
struct ft260_get_chip_version_report version;
+ struct ft260_get_system_status_report cfg;
int ret;
if (!hid_is_usb(hdev))
@@ -1727,6 +2228,7 @@ static int ft260_probe(struct hid_device *hdev, const struct hid_device_id *id)
goto alloc_fail;
}
hid_set_drvdata(hdev, dev);
+ dev->hdev = hdev;
ret = hid_parse(hdev);
if (ret) {
@@ -1757,21 +2259,18 @@ static int ft260_probe(struct hid_device *hdev, const struct hid_device_id *id)
version.chip_code[0], version.chip_code[1],
version.chip_code[2], version.chip_code[3]);
- ret = ft260_get_interface_type(hdev, dev);
- if (ret <= FT260_IFACE_NONE)
- goto err_hid_close;
-
- hid_set_drvdata(hdev, dev);
- dev->hdev = hdev;
-
mutex_init(&dev->lock);
spin_lock_init(&dev->read_lock);
init_completion(&dev->wait);
+ ret = ft260_get_interface_type(dev, &cfg);
+ if (ret <= FT260_IFACE_NONE)
+ goto err_hid_close;
+
if (ret == FT260_IFACE_I2C)
- ret = ft260_i2c_probe(hdev, dev);
+ ret = ft260_i2c_probe(dev, &cfg);
else
- ret = ft260_uart_probe(hdev, dev);
+ ret = ft260_uart_probe(dev, &cfg);
if (ret)
goto err_hid_close;
@@ -1800,6 +2299,9 @@ static void ft260_remove(struct hid_device *hdev)
dev->index);
ft260_uart_port_remove(dev);
/* dev is still needed, so we will free it in _destroy func */
+ if (dev->iface_id == 0)
+ sysfs_remove_group(&hdev->dev.kobj, &ft260_attr_group);
+
} else {
sysfs_remove_group(&hdev->dev.kobj, &ft260_attr_group);
i2c_del_adapter(&dev->adap);
--
2.43.0
^ permalink raw reply related [flat|nested] 43+ messages in thread
* [PATCH 04/13] HID: ft260: i2c: reduce driver module loading time
2026-08-22 21:39 [PATCH 00/13] HID: ft260: add UART and GPIO support, plus I2C fixes Michael Zaidman
` (2 preceding siblings ...)
2026-08-22 21:39 ` [PATCH 03/13] HID: ft260: add GPIO support on top of UART Michael Zaidman
@ 2026-08-22 21:39 ` Michael Zaidman
2026-08-22 21:51 ` sashiko-bot
2026-08-22 21:39 ` [PATCH 05/13] HID: ft260: i2c: silence sysfs store big-numbers Michael Zaidman
` (9 subsequent siblings)
13 siblings, 1 reply; 43+ messages in thread
From: Michael Zaidman @ 2026-08-22 21:39 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: Linus Walleij, Bartosz Golaszewski, Germain Hebert, Rio Liu,
Bruno Giacomazzi, Christina Quast, linux-input, linux-gpio,
linux-i2c, linux-kernel, Michael Zaidman
The HWMON class I2C client drivers register the address range to scan
during the I2C master module probing. For example, the lm75 module
registers eight bus addresses scanned during the ft260 module loading.
In the case of ft260, the bus is scanned by ft260_i2c_read, but due to
an excessive read completion timeout, it took about 40 seconds for the
ft260 driver to scan the lm75 address range before exiting the probe
function.
Adjust the i2c_read timeout so it depends on the amount of data to
read, reducing the lm75 address range scan time to about 200ms.
The 25 ms per-report budget was checked on a UMFT260EV1A with a 24LC512
EEPROM by shrinking the timeout until reads began to fail: a 60-byte
read still completes at 9 ms and times out at 7 ms, against 5.4 ms of
wire time at 100 kHz, so the fixed USB, HID and scheduling overhead
costs a few milliseconds. At the chip's slowest 60 kHz clock that chunk
needs about 9 ms on the wire and the 180-byte chunk about 27 ms, so both
keep roughly a factor of two under the 25 ms and 75 ms limits and the
timeouts do not need retuning for slower bus clocks.
Signed-off-by: Michael Zaidman <michael.zaidman@gmail.com>
---
drivers/hid/hid-ft260.c | 15 ++++++++++++---
1 file changed, 12 insertions(+), 3 deletions(-)
diff --git a/drivers/hid/hid-ft260.c b/drivers/hid/hid-ft260.c
index cc3b8f5791de..4435a39fce23 100644
--- a/drivers/hid/hid-ft260.c
+++ b/drivers/hid/hid-ft260.c
@@ -56,6 +56,13 @@ MODULE_PARM_DESC(debug, "Toggle FT260 debugging messages");
* read payload length to be 180 bytes.
*/
#define FT260_RD_DATA_MAX (180)
+
+/* Time in ms to wait for a single report read data transfer completion */
+#define FT260_RD_ONE_REPORT_TO (25)
+
+/* Time in ms to wait for a multi-report read data transfer completion */
+#define FT260_RD_MULTI_REPORT_TO (FT260_RD_ONE_REPORT_TO * FT260_RD_DATA_MAX / 60)
+
#define FT260_WR_I2C_DATA_MAX (60)
#define FT260_WR_UART_DATA_MAX (62)
#define FT260_GPIOCHIP "ft260_gpio"
@@ -713,7 +720,7 @@ static int ft260_i2c_read(struct ft260_device *dev, u8 addr, u8 *data,
{
u16 rd_len;
u16 rd_data_max = 60;
- int timeout, ret = 0;
+ int timeout, timeout_jiffies, ret = 0;
struct ft260_i2c_read_request_report rep;
struct hid_device *hdev = dev->hdev;
unsigned long irqflags;
@@ -732,10 +739,12 @@ static int ft260_i2c_read(struct ft260_device *dev, u8 addr, u8 *data,
flag = 0; /* no fresh START - continue current transaction */
do {
if (len <= rd_data_max) {
+ timeout = FT260_RD_ONE_REPORT_TO;
rd_len = len;
if (want_stop)
flag |= FT260_FLAG_STOP;
} else {
+ timeout = FT260_RD_MULTI_REPORT_TO;
rd_len = rd_data_max;
}
rd_data_max = FT260_RD_DATA_MAX;
@@ -762,8 +771,8 @@ static int ft260_i2c_read(struct ft260_device *dev, u8 addr, u8 *data,
goto ft260_i2c_read_exit;
}
- timeout = msecs_to_jiffies(5000);
- if (!wait_for_completion_timeout(&dev->wait, timeout)) {
+ timeout_jiffies = msecs_to_jiffies(timeout);
+ if (!wait_for_completion_timeout(&dev->wait, timeout_jiffies)) {
ret = -ETIMEDOUT;
ft260_i2c_reset(hdev);
goto ft260_i2c_read_exit;
--
2.43.0
^ permalink raw reply related [flat|nested] 43+ messages in thread
* [PATCH 05/13] HID: ft260: i2c: silence sysfs store big-numbers
2026-08-22 21:39 [PATCH 00/13] HID: ft260: add UART and GPIO support, plus I2C fixes Michael Zaidman
` (3 preceding siblings ...)
2026-08-22 21:39 ` [PATCH 04/13] HID: ft260: i2c: reduce driver module loading time Michael Zaidman
@ 2026-08-22 21:39 ` Michael Zaidman
2026-08-22 21:51 ` sashiko-bot
2026-08-22 21:39 ` [PATCH 06/13] HID: ft260: i2c: reduce bus-error message severity Michael Zaidman
` (8 subsequent siblings)
13 siblings, 1 reply; 43+ messages in thread
From: Michael Zaidman @ 2026-08-22 21:39 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: Linus Walleij, Bartosz Golaszewski, Germain Hebert, Rio Liu,
Bruno Giacomazzi, Christina Quast, linux-input, linux-gpio,
linux-i2c, linux-kernel, Michael Zaidman
FT260_ATTR_STORE returned the feature-report transfer length from
ft260_hid_feature_report_set() instead of the sysfs write size. For
values like 1000, userspace writes more bytes than the HID report
length, so the VFS retries the remainder and reports "Invalid argument"
even though the attribute was applied correctly:
$ echo 1000 > .../clock
bash: line 1: echo: write error: Invalid argument
$ cat .../clock
1000
Return count after a successful parse so the full write is consumed.
Signed-off-by: Michael Zaidman <michael.zaidman@gmail.com>
---
drivers/hid/hid-ft260.c | 1 +
1 file changed, 1 insertion(+)
diff --git a/drivers/hid/hid-ft260.c b/drivers/hid/hid-ft260.c
index 4435a39fce23..8db896f164cd 100644
--- a/drivers/hid/hid-ft260.c
+++ b/drivers/hid/hid-ft260.c
@@ -1479,6 +1479,7 @@ static void ft260_attr_dummy_func(struct hid_device *hdev, u8 req, u16 value)
hid_err(hdev, "%s: failed!\n", __func__); \
else \
func(hdev, req, name); \
+ ret = count; \
mutex_unlock(&dev->lock); \
} else { \
ret = -EINVAL; \
--
2.43.0
^ permalink raw reply related [flat|nested] 43+ messages in thread
* [PATCH 06/13] HID: ft260: i2c: reduce bus-error message severity
2026-08-22 21:39 [PATCH 00/13] HID: ft260: add UART and GPIO support, plus I2C fixes Michael Zaidman
` (4 preceding siblings ...)
2026-08-22 21:39 ` [PATCH 05/13] HID: ft260: i2c: silence sysfs store big-numbers Michael Zaidman
@ 2026-08-22 21:39 ` Michael Zaidman
2026-08-22 21:52 ` sashiko-bot
2026-08-22 21:39 ` [PATCH 07/13] HID: ft260: uart: enable flow control Michael Zaidman
` (7 subsequent siblings)
13 siblings, 1 reply; 43+ messages in thread
From: Michael Zaidman @ 2026-08-22 21:39 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: Linus Walleij, Bartosz Golaszewski, Germain Hebert, Rio Liu,
Bruno Giacomazzi, Christina Quast, linux-input, linux-gpio,
linux-i2c, linux-kernel, Michael Zaidman
The driver logged errors when the FT260 reported an I2C bus error or when
ft260_i2c_write / ft260_smbus_write failed after such a status. That was
meant to flag unexpected failures, but bus scans (i2cdetect, HWMON
discovery, transceiver monitoring, and similar) hit missing devices
routinely and should not spam dmesg with either:
i2c bus error: %#02x
ft260_i2c_write: failed with -5
Keep returning -EIO, but print those via ft260_dbg. Still use hid_err in
ft260_hid_output_report_check_status for actual HID/USB transport
failures, which are genuine errors.
Signed-off-by: Michael Zaidman <michael.zaidman@gmail.com>
---
drivers/hid/hid-ft260.c | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/drivers/hid/hid-ft260.c b/drivers/hid/hid-ft260.c
index 8db896f164cd..1ed3701eaaf7 100644
--- a/drivers/hid/hid-ft260.c
+++ b/drivers/hid/hid-ft260.c
@@ -565,7 +565,7 @@ static int ft260_xfer_status(struct ft260_device *dev, u8 bus_busy)
* to 1, bit 1 is also set to 1.
*/
if (report.bus_status & FT260_I2C_STATUS_ERROR) {
- hid_err(hdev, "i2c bus error: %#02x\n", report.bus_status);
+ ft260_dbg("i2c bus error: %#02x\n", report.bus_status);
return -EIO;
}
@@ -599,6 +599,7 @@ static int ft260_hid_output_report_check_status(struct ft260_device *dev,
ret = ft260_hid_output_report(hdev, data, len);
if (ret < 0) {
+ hid_err(hdev, "%s: failed with %d\n", __func__, ret);
ft260_i2c_reset(hdev);
return ret;
}
@@ -638,7 +639,6 @@ static int ft260_i2c_write(struct ft260_device *dev, u8 addr, u8 *data,
int len, u8 flag)
{
int ret, wr_len, idx = 0;
- struct hid_device *hdev = dev->hdev;
struct ft260_i2c_write_request_report *rep =
(struct ft260_i2c_write_request_report *)dev->i2c_wr_buf;
@@ -669,7 +669,7 @@ static int ft260_i2c_write(struct ft260_device *dev, u8 addr, u8 *data,
ret = ft260_hid_output_report_check_status(dev, (u8 *)rep,
wr_len + 4);
if (ret < 0) {
- hid_err(hdev, "%s: failed with %d\n", __func__, ret);
+ ft260_dbg("%s: failed with %d\n", __func__, ret);
return ret;
}
@@ -685,7 +685,7 @@ static int ft260_i2c_write(struct ft260_device *dev, u8 addr, u8 *data,
static int ft260_smbus_write(struct ft260_device *dev, u8 addr, u8 cmd,
u8 *data, u8 data_len, u8 flag)
{
- int ret = 0;
+ int ret;
int len = 4;
struct ft260_i2c_write_request_report *rep =
@@ -710,7 +710,7 @@ static int ft260_smbus_write(struct ft260_device *dev, u8 addr, u8 cmd,
ret = ft260_hid_output_report_check_status(dev, (u8 *)rep, len);
if (ret < 0)
- hid_err(dev->hdev, "%s: failed with %d\n", __func__, ret);
+ ft260_dbg("%s: failed with %d\n", __func__, ret);
return ret;
}
--
2.43.0
^ permalink raw reply related [flat|nested] 43+ messages in thread
* [PATCH 07/13] HID: ft260: uart: enable flow control
2026-08-22 21:39 [PATCH 00/13] HID: ft260: add UART and GPIO support, plus I2C fixes Michael Zaidman
` (5 preceding siblings ...)
2026-08-22 21:39 ` [PATCH 06/13] HID: ft260: i2c: reduce bus-error message severity Michael Zaidman
@ 2026-08-22 21:39 ` Michael Zaidman
2026-08-22 21:52 ` sashiko-bot
2026-08-22 21:39 ` [PATCH 08/13] HID: ft260: uart: add modem pins control via ioctl Michael Zaidman
` (6 subsequent siblings)
13 siblings, 1 reply; 43+ messages in thread
From: Michael Zaidman @ 2026-08-22 21:39 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: Linus Walleij, Bartosz Golaszewski, Germain Hebert, Rio Liu,
Bruno Giacomazzi, Christina Quast, linux-input, linux-gpio,
linux-i2c, linux-kernel, Michael Zaidman
The FT260 is a full-speed USB device. Interrupt transfers poll at 1 ms
with a 64-byte max packet size, so the theoretical UART payload ceiling
is about 60 kB/s (~600 kbaud in 8N1). The chip advertises baud rates
above 1M and small transfers can work there, but large transfers are not
reliable above 460800 baud without flow control.
Stop forcing FT260_UART_CFG_FLOW_CTRL_NONE after termios setup. Honor
CRTSCTS (RTS/CTS) and IXON/IXOFF (XON/XOFF) when configuring the UART.
Tested at 1500000 baud both directions against an FT2232H with XON/XOFF
and with RTS/CTS (~2.9 MB transfers, no data corruption).
Signed-off-by: Michael Zaidman <michael.zaidman@gmail.com>
---
drivers/hid/hid-ft260.c | 9 +++++----
1 file changed, 5 insertions(+), 4 deletions(-)
diff --git a/drivers/hid/hid-ft260.c b/drivers/hid/hid-ft260.c
index 1ed3701eaaf7..1da42b8756d8 100644
--- a/drivers/hid/hid-ft260.c
+++ b/drivers/hid/hid-ft260.c
@@ -1868,8 +1868,12 @@ static int ft260_uart_change_speed(struct ft260_device *port,
if (termios->c_cflag & CRTSCTS)
req.flow_ctrl = FT260_UART_CFG_FLOW_CTRL_RTS_CTS;
+ else if (termios->c_iflag & (IXON | IXOFF))
+ req.flow_ctrl = FT260_UART_CFG_FLOW_CTRL_XON_XOFF;
else
- req.flow_ctrl = FT260_UART_CFG_FLOW_CTRL_OFF;
+ req.flow_ctrl = FT260_UART_CFG_FLOW_CTRL_NONE;
+
+ req.breaking = FT260_UART_CFG_BREAKING_NO;
ft260_dbg("configured termios: flow control: %d, baudrate: %d, ",
req.flow_ctrl, baud);
@@ -1877,9 +1881,6 @@ static int ft260_uart_change_speed(struct ft260_device *port,
req.data_bit, req.parity,
req.stop_bit, req.breaking);
- req.flow_ctrl = FT260_UART_CFG_FLOW_CTRL_NONE;
- req.breaking = FT260_UART_CFG_BREAKING_NO;
-
mutex_lock(&port->lock);
ret = ft260_hid_feature_report_set(hdev, (u8 *)&req, sizeof(req));
--
2.43.0
^ permalink raw reply related [flat|nested] 43+ messages in thread
* [PATCH 08/13] HID: ft260: uart: add modem pins control via ioctl
2026-08-22 21:39 [PATCH 00/13] HID: ft260: add UART and GPIO support, plus I2C fixes Michael Zaidman
` (6 preceding siblings ...)
2026-08-22 21:39 ` [PATCH 07/13] HID: ft260: uart: enable flow control Michael Zaidman
@ 2026-08-22 21:39 ` Michael Zaidman
2026-08-22 21:54 ` sashiko-bot
2026-08-25 8:08 ` Linus Walleij
2026-08-22 21:39 ` [PATCH 09/13] HID: ft260: gpio: group sysfs attrs per HID interface Michael Zaidman
` (5 subsequent siblings)
13 siblings, 2 replies; 43+ messages in thread
From: Michael Zaidman @ 2026-08-22 21:39 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: Linus Walleij, Bartosz Golaszewski, Germain Hebert, Rio Liu,
Bruno Giacomazzi, Christina Quast, linux-input, linux-gpio,
linux-i2c, linux-kernel, Michael Zaidman
Add TIOCMGET/TIOCMSET (and thus TIOCMBIS/TIOCMBIC) support so userspace
can drive DTR/RTS when the modem pins are in GPIO mode. This enables
programming an ESP32 over the FT260 with esptool, which resets the chip
via those lines.
Also key GPIO sysfs registration off chip_mode rather than USB interface
number 0, and include DCD/RI in UART mode 3/4 GPIO sets so the modem
mask matches the pins esptool needs.
Signed-off-by: Michael Zaidman <michael.zaidman@gmail.com>
---
drivers/hid/hid-ft260.c | 151 ++++++++++++++++++++++++++++++++++------
1 file changed, 130 insertions(+), 21 deletions(-)
diff --git a/drivers/hid/hid-ft260.c b/drivers/hid/hid-ft260.c
index 1da42b8756d8..554d8e3ddb6d 100644
--- a/drivers/hid/hid-ft260.c
+++ b/drivers/hid/hid-ft260.c
@@ -16,6 +16,7 @@
#include <linux/serial.h>
#include <linux/serial_core.h>
#include <linux/kfifo.h>
+#include <linux/tty.h>
#include <linux/tty_flip.h>
#include <linux/minmax.h>
#include <linux/unaligned.h>
@@ -182,7 +183,7 @@ enum {
FT260_GPIO_DIR_OUTPUT = 0x01,
};
-/* GPIO offsets */
+/* GPIO bit masks */
enum {
FT260_GPIO_0 = (1 << 0),
FT260_GPIO_1 = (1 << 1),
@@ -215,7 +216,8 @@ enum {
FT260_GPIO_UART_MODE_1_SET = (FT260_GPIO_UART_DTR_DSR),
FT260_GPIO_UART_MODE_2_SET = (FT260_GPIO_UART_RTS_CTS),
FT260_GPIO_UART_MODE_3_SET = (FT260_GPIO_UART_RTS_CTS |
- FT260_GPIO_UART_DTR_DSR),
+ FT260_GPIO_UART_DTR_DSR |
+ FT260_GPIO_UART_DCD_RI),
FT260_GPIO_UART_MODE_4_SET = (FT260_GPIO_UART_MODE_3_SET),
FT260_GPIO_UART_DEFAULT = (FT260_GPIO_UART_MODE_0_SET),
FT260_GPIO_UART_MODE_1_CLR = (FT260_GPIO_UART_RX_TX |
@@ -226,6 +228,16 @@ enum {
FT260_GPIO_UART_MODES = (5),
};
+/* UART modem control GPIO offsets */
+enum {
+ FT260_GPIO_UART_DCD = (4),
+ FT260_GPIO_UART_RI = (5),
+ FT260_GPIO_UART_RTS = (7),
+ FT260_GPIO_UART_CTS = (10),
+ FT260_GPIO_UART_DTR = (11),
+ FT260_GPIO_UART_DSR = (13),
+};
+
#define FT260_SET_REQUEST_VALUE(report_id) ((FT260_FEATURE << 8) | (report_id))
/* Feature In reports */
@@ -440,6 +452,7 @@ MODULE_DEVICE_TABLE(hid, ft260_devices);
struct ft260_device {
struct i2c_adapter adap;
struct hid_device *hdev;
+ int chip_mode;
int iface_type;
int iface_id;
struct list_head device_list;
@@ -1097,7 +1110,27 @@ static void ft260_gpio_en_update(struct hid_device *hdev, u8 req, u8 value)
else
ft260_gpio_en_clr(dev, bitmap);
exit:
- hid_info(hdev, "enabled GPIOs: %04x\n", dev->gpio_en);
+ hid_info(hdev, "enabled GPIOs: %04x, bitmap %04x\n",
+ dev->gpio_en, bitmap);
+}
+
+static void ft260_gpio_output_cfg(struct ft260_gpio_state *gpio,
+ u32 offset, int value)
+{
+ if (offset < FT260_GPIO_MAX) {
+ gpio->dirs |= 1 << offset;
+ if (value)
+ gpio->vals |= !!value << offset;
+ else
+ gpio->vals &= ~(1 << offset);
+ } else {
+ offset = offset - FT260_GPIO_MAX;
+ gpio->ex_dirs |= 1 << offset;
+ if (value)
+ gpio->ex_vals |= !!value << offset;
+ else
+ gpio->ex_vals &= ~(1 << offset);
+ }
}
static int ft260_gpio_set(struct gpio_chip *gc, u32 offset, int value)
@@ -1138,7 +1171,7 @@ static int ft260_gpio_set(struct gpio_chip *gc, u32 offset, int value)
rep.gpio.ex_vals &= ~(1 << offset);
}
- ft260_dbg("dirs %#02x vals %#02x ex_dir %#02x ex_vals %#02x\n",
+ ft260_dbg("dirs %#02x vals %#02x ex_dirs %#02x ex_vals %#02x\n",
rep.gpio.dirs, rep.gpio.vals,
rep.gpio.ex_dirs, rep.gpio.ex_vals);
@@ -1283,8 +1316,6 @@ static int ft260_gpio_init(struct ft260_device *dev,
char prefix[] = "ft260_";
u8 mode = cfg->chip_mode;
- hid_info(hdev, "initialize gpio chip\n");
-
dev->gpio_uart_mode[0] = (u16)FT260_GPIO_UART_MODE_0_SET;
dev->gpio_uart_mode[1] = (u16)FT260_GPIO_UART_MODE_1_SET;
dev->gpio_uart_mode[2] = (u16)FT260_GPIO_UART_MODE_2_SET;
@@ -1324,7 +1355,6 @@ static int ft260_gpio_init(struct ft260_device *dev,
goto exit;
}
snprintf(label, label_sz, "%s%s", prefix, dev_name(&hdev->dev));
- hid_info(hdev, "initialize gpio chip on %s\n", label);
dev->gc->label = label;
dev->gc->direction_input = ft260_gpio_direction_input;
@@ -1388,6 +1418,7 @@ static int ft260_get_interface_type(struct ft260_device *dev,
ft260_dbg("wakeup_int: 0x%02x\n", cfg->enable_wakeup_int);
dev->power_saving_en = cfg->power_saving_en;
+ dev->chip_mode = cfg->chip_mode;
switch (cfg->chip_mode) {
case FT260_MODE_ALL:
@@ -1405,7 +1436,6 @@ static int ft260_get_interface_type(struct ft260_device *dev,
break;
}
- dev->iface_type = ret;
return ret;
}
@@ -1912,6 +1942,78 @@ static void ft260_uart_set_termios(struct tty_struct *tty,
ft260_uart_change_speed(port, &tty->termios, NULL);
}
+static int ft260_uart_tiocmget(struct tty_struct *tty)
+{
+ struct ft260_device *port = tty->driver_data;
+ int gpios;
+
+ /*
+ * The modem lines control via ioctl is allowed only
+ * when the related pins are in the gpio mode.
+ */
+ int mask = FT260_GPIO_UART_MODE_4_SET;
+
+ if ((port->gpio_en & mask) != mask) {
+ ft260_dbg("Illegal gpio config %#02x != mask %#04x\n",
+ port->gpio_en, mask);
+ return 0;
+ }
+
+ gpios = ft260_gpio_get_all(port->gc, FT260_GPIO_VALUE);
+
+ return (((gpios & FT260_GPIO_B) ? TIOCM_RTS : 0) |
+ ((gpios & FT260_GPIO_E) ? TIOCM_CTS : 0) |
+ ((gpios & FT260_GPIO_F) ? TIOCM_DTR : 0) |
+ ((gpios & FT260_GPIO_H) ? TIOCM_DSR : 0) |
+ ((gpios & FT260_GPIO_4) ? TIOCM_CAR : 0) |
+ ((gpios & FT260_GPIO_5) ? TIOCM_RNG : 0));
+}
+
+static int ft260_uart_tiocmset(struct tty_struct *tty,
+ unsigned int set, unsigned int clear)
+{
+ int ret;
+ struct ft260_device *port = tty->driver_data;
+ struct ft260_gpio_write_request_report rep;
+ struct hid_device *hdev = port->hdev;
+ int mask = FT260_GPIO_UART_MODE_4_SET;
+
+ if ((port->gpio_en & mask) != mask) {
+ ft260_dbg("Illegal gpio config %#02x != mask %#04x\n",
+ port->gpio_en, mask);
+ return 0;
+ }
+ rep.report = FT260_GPIO;
+ rep.gpio = port->gpio;
+
+ mutex_lock(&port->lock);
+
+ if (set & TIOCM_RTS)
+ ft260_gpio_output_cfg(&rep.gpio, FT260_GPIO_UART_RTS, 1);
+ if (set & TIOCM_DTR)
+ ft260_gpio_output_cfg(&rep.gpio, FT260_GPIO_UART_DTR, 1);
+ if (clear & TIOCM_RTS)
+ ft260_gpio_output_cfg(&rep.gpio, FT260_GPIO_UART_RTS, 0);
+ if (clear & TIOCM_DTR)
+ ft260_gpio_output_cfg(&rep.gpio, FT260_GPIO_UART_DTR, 0);
+
+ ft260_dbg("dirs %#02x vals %#02x ex_dirs %#02x ex_vals %#02x\n",
+ rep.gpio.dirs, rep.gpio.vals,
+ rep.gpio.ex_dirs, rep.gpio.ex_vals);
+
+ ret = ft260_hid_feature_report_set(hdev, (u8 *)&rep, sizeof(rep));
+ if (unlikely(ret < 0)) {
+ hid_err(hdev, "%s: cannot set GPIO: %d\n", __func__, ret);
+ mutex_unlock(&port->lock);
+ return -EIO;
+ }
+
+ port->gpio = rep.gpio;
+ mutex_unlock(&port->lock);
+
+ return 0;
+}
+
static int ft260_uart_install(struct tty_driver *driver, struct tty_struct *tty)
{
int idx = tty->index;
@@ -1988,6 +2090,8 @@ static const struct tty_operations ft260_uart_ops = {
.write_room = ft260_uart_write_room,
.chars_in_buffer = ft260_uart_chars_in_buffer,
.set_termios = ft260_uart_set_termios,
+ .tiocmget = ft260_uart_tiocmget,
+ .tiocmset = ft260_uart_tiocmset,
.hangup = ft260_uart_hangup,
.install = ft260_uart_install,
.cleanup = ft260_uart_cleanup,
@@ -2131,14 +2235,16 @@ static int ft260_i2c_probe(struct ft260_device *dev,
return ret;
}
- ret = ft260_gpio_init(dev, cfg);
- if (ret)
- goto err_i2c_free;
+ if (cfg->chip_mode == FT260_MODE_I2C) {
+ ret = ft260_gpio_init(dev, cfg);
+ if (ret)
+ goto err_i2c_free;
- ret = sysfs_create_group(&hdev->dev.kobj, &ft260_attr_group);
- if (ret < 0) {
- hid_err(hdev, "failed to create sysfs attrs\n");
- goto err_i2c_free;
+ ret = sysfs_create_group(&hdev->dev.kobj, &ft260_attr_group);
+ if (ret < 0) {
+ hid_err(hdev, "failed to create sysfs attrs\n");
+ goto err_i2c_free;
+ }
}
return 0;
@@ -2199,7 +2305,8 @@ static int ft260_uart_probe(struct ft260_device *dev,
cfg->uart_mode = FT260_UART_CFG_FLOW_CTRL_NONE;
- if (dev->iface_id == 0) {
+ if (dev->chip_mode & FT260_MODE_UART ||
+ dev->chip_mode == FT260_MODE_ALL) {
ret = ft260_gpio_init(dev, cfg);
if (ret)
goto err_hid_report;
@@ -2274,11 +2381,11 @@ static int ft260_probe(struct hid_device *hdev, const struct hid_device_id *id)
spin_lock_init(&dev->read_lock);
init_completion(&dev->wait);
- ret = ft260_get_interface_type(dev, &cfg);
- if (ret <= FT260_IFACE_NONE)
+ dev->iface_type = ft260_get_interface_type(dev, &cfg);
+ if (dev->iface_type <= FT260_IFACE_NONE)
goto err_hid_close;
- if (ret == FT260_IFACE_I2C)
+ if (dev->iface_type == FT260_IFACE_I2C)
ret = ft260_i2c_probe(dev, &cfg);
else
ret = ft260_uart_probe(dev, &cfg);
@@ -2310,11 +2417,13 @@ static void ft260_remove(struct hid_device *hdev)
dev->index);
ft260_uart_port_remove(dev);
/* dev is still needed, so we will free it in _destroy func */
- if (dev->iface_id == 0)
+ if (dev->chip_mode & FT260_MODE_UART ||
+ dev->chip_mode == FT260_MODE_ALL)
sysfs_remove_group(&hdev->dev.kobj, &ft260_attr_group);
} else {
- sysfs_remove_group(&hdev->dev.kobj, &ft260_attr_group);
+ if (dev->chip_mode == FT260_MODE_I2C)
+ sysfs_remove_group(&hdev->dev.kobj, &ft260_attr_group);
i2c_del_adapter(&dev->adap);
kfree(dev);
}
--
2.43.0
^ permalink raw reply related [flat|nested] 43+ messages in thread
* [PATCH 09/13] HID: ft260: gpio: group sysfs attrs per HID interface
2026-08-22 21:39 [PATCH 00/13] HID: ft260: add UART and GPIO support, plus I2C fixes Michael Zaidman
` (7 preceding siblings ...)
2026-08-22 21:39 ` [PATCH 08/13] HID: ft260: uart: add modem pins control via ioctl Michael Zaidman
@ 2026-08-22 21:39 ` Michael Zaidman
2026-08-22 21:54 ` sashiko-bot
2026-08-25 8:13 ` Linus Walleij
2026-08-22 21:39 ` [PATCH 10/13] HID: ft260: uart: fix active-low RTS/CTS/DTR/DSR polarity Michael Zaidman
` (4 subsequent siblings)
13 siblings, 2 replies; 43+ messages in thread
From: Michael Zaidman @ 2026-08-22 21:39 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: Linus Walleij, Bartosz Golaszewski, Germain Hebert, Rio Liu,
Bruno Giacomazzi, Christina Quast, linux-input, linux-gpio,
linux-i2c, linux-kernel, Michael Zaidman
Split the single sysfs attribute group into per-interface groups based
on chip mode (DCNF0/DCNF1):
- I2C-only (mode 1): full I2C + GPIO attributes on the I2C HID iface
- Dual mode (0/3): I2C-facing attrs on the I2C iface; GPIO and UART
attrs on the UART iface
- UART-only (mode 2): GPIO and UART attrs on the UART iface
Signed-off-by: Michael Zaidman <michael.zaidman@gmail.com>
---
drivers/hid/hid-ft260.c | 58 +++++++++++++++++++++++++++++++++++------
1 file changed, 50 insertions(+), 8 deletions(-)
diff --git a/drivers/hid/hid-ft260.c b/drivers/hid/hid-ft260.c
index 554d8e3ddb6d..0a1875a1a7ec 100644
--- a/drivers/hid/hid-ft260.c
+++ b/drivers/hid/hid-ft260.c
@@ -1592,7 +1592,7 @@ static ssize_t i2c_reset_store(struct device *kdev,
}
static DEVICE_ATTR_WO(i2c_reset);
-static const struct attribute_group ft260_attr_group = {
+static const struct attribute_group ft260_i2c_chip_mode_1_attr_group = {
.attrs = (struct attribute *[]) {
&dev_attr_chip_mode.attr,
&dev_attr_pwren_status.attr,
@@ -1603,11 +1603,35 @@ static const struct attribute_group ft260_attr_group = {
&dev_attr_gpio2_func.attr,
&dev_attr_gpioa_func.attr,
&dev_attr_gpiog_func.attr,
+ &dev_attr_clock_ctl.attr,
+ &dev_attr_i2c_reset.attr,
+ &dev_attr_clock.attr,
+ NULL
+ }
+};
+
+static const struct attribute_group ft260_i2c_chip_mode_0_3_attr_group = {
+ .attrs = (struct attribute *[]) {
+ &dev_attr_hid_over_i2c_en.attr,
+ &dev_attr_i2c_reset.attr,
+ &dev_attr_clock.attr,
+ NULL
+ }
+};
+
+static const struct attribute_group ft260_uart_chip_mode_0_2_3_attr_group = {
+ .attrs = (struct attribute *[]) {
+ &dev_attr_chip_mode.attr,
+ &dev_attr_pwren_status.attr,
+ &dev_attr_suspend_status.attr,
+ &dev_attr_power_saving_en.attr,
+ &dev_attr_gpio2_func.attr,
+ &dev_attr_gpioa_func.attr,
+ &dev_attr_gpiog_func.attr,
&dev_attr_uart_mode.attr,
&dev_attr_uart_dcd_ri.attr,
&dev_attr_clock_ctl.attr,
&dev_attr_i2c_reset.attr,
- &dev_attr_clock.attr,
NULL
}
};
@@ -2240,7 +2264,16 @@ static int ft260_i2c_probe(struct ft260_device *dev,
if (ret)
goto err_i2c_free;
- ret = sysfs_create_group(&hdev->dev.kobj, &ft260_attr_group);
+ ret = sysfs_create_group(&hdev->dev.kobj,
+ &ft260_i2c_chip_mode_1_attr_group);
+ if (ret < 0) {
+ hid_err(hdev, "failed to create sysfs attrs\n");
+ goto err_i2c_free;
+ }
+ } else if (cfg->chip_mode == FT260_MODE_ALL ||
+ cfg->chip_mode == FT260_MODE_BOTH) {
+ ret = sysfs_create_group(&hdev->dev.kobj,
+ &ft260_i2c_chip_mode_0_3_attr_group);
if (ret < 0) {
hid_err(hdev, "failed to create sysfs attrs\n");
goto err_i2c_free;
@@ -2306,12 +2339,14 @@ static int ft260_uart_probe(struct ft260_device *dev,
cfg->uart_mode = FT260_UART_CFG_FLOW_CTRL_NONE;
if (dev->chip_mode & FT260_MODE_UART ||
- dev->chip_mode == FT260_MODE_ALL) {
+ dev->chip_mode == FT260_MODE_ALL ||
+ dev->chip_mode == FT260_MODE_BOTH) {
ret = ft260_gpio_init(dev, cfg);
if (ret)
goto err_hid_report;
- ret = sysfs_create_group(&hdev->dev.kobj, &ft260_attr_group);
+ ret = sysfs_create_group(&hdev->dev.kobj,
+ &ft260_uart_chip_mode_0_2_3_attr_group);
if (ret < 0) {
hid_err(hdev, "failed to create sysfs attrs\n");
goto err_hid_report;
@@ -2418,12 +2453,19 @@ static void ft260_remove(struct hid_device *hdev)
ft260_uart_port_remove(dev);
/* dev is still needed, so we will free it in _destroy func */
if (dev->chip_mode & FT260_MODE_UART ||
- dev->chip_mode == FT260_MODE_ALL)
- sysfs_remove_group(&hdev->dev.kobj, &ft260_attr_group);
+ dev->chip_mode == FT260_MODE_ALL ||
+ dev->chip_mode == FT260_MODE_BOTH)
+ sysfs_remove_group(&hdev->dev.kobj,
+ &ft260_uart_chip_mode_0_2_3_attr_group);
} else {
if (dev->chip_mode == FT260_MODE_I2C)
- sysfs_remove_group(&hdev->dev.kobj, &ft260_attr_group);
+ sysfs_remove_group(&hdev->dev.kobj,
+ &ft260_i2c_chip_mode_1_attr_group);
+ else if (dev->chip_mode == FT260_MODE_ALL ||
+ dev->chip_mode == FT260_MODE_BOTH)
+ sysfs_remove_group(&hdev->dev.kobj,
+ &ft260_i2c_chip_mode_0_3_attr_group);
i2c_del_adapter(&dev->adap);
kfree(dev);
}
--
2.43.0
^ permalink raw reply related [flat|nested] 43+ messages in thread
* [PATCH 10/13] HID: ft260: uart: fix active-low RTS/CTS/DTR/DSR polarity
2026-08-22 21:39 [PATCH 00/13] HID: ft260: add UART and GPIO support, plus I2C fixes Michael Zaidman
` (8 preceding siblings ...)
2026-08-22 21:39 ` [PATCH 09/13] HID: ft260: gpio: group sysfs attrs per HID interface Michael Zaidman
@ 2026-08-22 21:39 ` Michael Zaidman
2026-08-22 22:03 ` sashiko-bot
2026-08-25 8:16 ` Linus Walleij
2026-08-22 21:39 ` [PATCH 11/13] HID: ft260: i2c: fix large write transaction failure Michael Zaidman
` (3 subsequent siblings)
13 siblings, 2 replies; 43+ messages in thread
From: Michael Zaidman @ 2026-08-22 21:39 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: Linus Walleij, Bartosz Golaszewski, Germain Hebert, Rio Liu,
Bruno Giacomazzi, Christina Quast, linux-input, linux-gpio,
linux-i2c, linux-kernel, Michael Zaidman
From: Rio Liu <rio@r26.me>
The FT260 modem control GPIOs for RTS, CTS, DTR, and DSR are active-low.
Invert the TIOCMGET sense and the TIOCMSET drive levels for those pins
so userspace ioctl state matches the electrical lines. Leave DCD and RI
unchanged.
Signed-off-by: Rio Liu <rio@r26.me>
Signed-off-by: Michael Zaidman <michael.zaidman@gmail.com>
---
drivers/hid/hid-ft260.c | 16 ++++++++--------
1 file changed, 8 insertions(+), 8 deletions(-)
diff --git a/drivers/hid/hid-ft260.c b/drivers/hid/hid-ft260.c
index 0a1875a1a7ec..a2f4b6e5a16b 100644
--- a/drivers/hid/hid-ft260.c
+++ b/drivers/hid/hid-ft260.c
@@ -1985,10 +1985,10 @@ static int ft260_uart_tiocmget(struct tty_struct *tty)
gpios = ft260_gpio_get_all(port->gc, FT260_GPIO_VALUE);
- return (((gpios & FT260_GPIO_B) ? TIOCM_RTS : 0) |
- ((gpios & FT260_GPIO_E) ? TIOCM_CTS : 0) |
- ((gpios & FT260_GPIO_F) ? TIOCM_DTR : 0) |
- ((gpios & FT260_GPIO_H) ? TIOCM_DSR : 0) |
+ return ((!(gpios & FT260_GPIO_B) ? TIOCM_RTS : 0) |
+ (!(gpios & FT260_GPIO_E) ? TIOCM_CTS : 0) |
+ (!(gpios & FT260_GPIO_F) ? TIOCM_DTR : 0) |
+ (!(gpios & FT260_GPIO_H) ? TIOCM_DSR : 0) |
((gpios & FT260_GPIO_4) ? TIOCM_CAR : 0) |
((gpios & FT260_GPIO_5) ? TIOCM_RNG : 0));
}
@@ -2013,13 +2013,13 @@ static int ft260_uart_tiocmset(struct tty_struct *tty,
mutex_lock(&port->lock);
if (set & TIOCM_RTS)
- ft260_gpio_output_cfg(&rep.gpio, FT260_GPIO_UART_RTS, 1);
+ ft260_gpio_output_cfg(&rep.gpio, FT260_GPIO_UART_RTS, 0);
if (set & TIOCM_DTR)
- ft260_gpio_output_cfg(&rep.gpio, FT260_GPIO_UART_DTR, 1);
+ ft260_gpio_output_cfg(&rep.gpio, FT260_GPIO_UART_DTR, 0);
if (clear & TIOCM_RTS)
- ft260_gpio_output_cfg(&rep.gpio, FT260_GPIO_UART_RTS, 0);
+ ft260_gpio_output_cfg(&rep.gpio, FT260_GPIO_UART_RTS, 1);
if (clear & TIOCM_DTR)
- ft260_gpio_output_cfg(&rep.gpio, FT260_GPIO_UART_DTR, 0);
+ ft260_gpio_output_cfg(&rep.gpio, FT260_GPIO_UART_DTR, 1);
ft260_dbg("dirs %#02x vals %#02x ex_dirs %#02x ex_vals %#02x\n",
rep.gpio.dirs, rep.gpio.vals,
--
2.43.0
^ permalink raw reply related [flat|nested] 43+ messages in thread
* [PATCH 11/13] HID: ft260: i2c: fix large write transaction failure
2026-08-22 21:39 [PATCH 00/13] HID: ft260: add UART and GPIO support, plus I2C fixes Michael Zaidman
` (9 preceding siblings ...)
2026-08-22 21:39 ` [PATCH 10/13] HID: ft260: uart: fix active-low RTS/CTS/DTR/DSR polarity Michael Zaidman
@ 2026-08-22 21:39 ` Michael Zaidman
2026-08-22 22:02 ` sashiko-bot
2026-08-22 21:39 ` [PATCH 12/13] HID: ft260: workaround for TN_189 errata endpoint STALL after enumeration Michael Zaidman
` (2 subsequent siblings)
13 siblings, 1 reply; 43+ messages in thread
From: Michael Zaidman @ 2026-08-22 21:39 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: Linus Walleij, Bartosz Golaszewski, Germain Hebert, Rio Liu,
Bruno Giacomazzi, Christina Quast, linux-input, linux-gpio,
linux-i2c, linux-kernel, Michael Zaidman, Chris Keeser
Fixes a regression from commit 5afac727defa ("HID: ft260: missed NACK
from busy device"), which waited for bus-idle after every HID write
report except when the report flag was exactly FT260_FLAG_START.
Multi-report I2C writes keep the bus busy between chunks for atomicity.
Treating those middle reports like a final STOP caused ft260_xfer_status()
to spin on -EAGAIN after the second HID report and fail large writes.
Write-then-read combined transfers still worked.
Pass the bus-busy check policy from the caller: ft260_i2c_write() and
ft260_smbus_write() wait for idle only when the report they send carries
STOP; intermediate chunks and the command phase of a combined transfer
do not.
The SMBus side matters for register reads. The command phase is written
with FT260_FLAG_START and no STOP, because a repeated START read follows
and the controller deliberately keeps the bus busy to keep the
transaction atomic. Checking the busy bit there makes
ft260_xfer_status() return -EAGAIN on every retry and the command write
fail with -EIO, which breaks probing of clients that read registers:
ads1x19 6-0040: Failed to read config register: -5
leds-pca963x: probe of 6-0008 failed with error -5
Fixes: 5afac727defa ("HID: ft260: missed NACK from busy device")
Reported-by: Chris Keeser <chriskeeser@meta.com>
Closes: https://github.com/MichaelZaidman/hid-ft260/issues/35
Tested-by: Chris Keeser <chriskeeser@meta.com>
Reported-by: Bruno Giacomazzi <brunoceg1@gmail.com>
Closes: https://github.com/MichaelZaidman/hid-ft260/issues/42
Signed-off-by: Michael Zaidman <michael.zaidman@gmail.com>
---
drivers/hid/hid-ft260.c | 35 ++++++++++++++++-------------------
1 file changed, 16 insertions(+), 19 deletions(-)
diff --git a/drivers/hid/hid-ft260.c b/drivers/hid/hid-ft260.c
index a2f4b6e5a16b..36687c086b40 100644
--- a/drivers/hid/hid-ft260.c
+++ b/drivers/hid/hid-ft260.c
@@ -566,9 +566,14 @@ static int ft260_xfer_status(struct ft260_device *dev, u8 bus_busy)
}
dev->clock = le16_to_cpu(report.clock);
- ft260_dbg("bus_status %#02x, clock %u\n", report.bus_status,
- dev->clock);
+ ft260_dbg("bus_status %#02x, clock %u, bus_busy %#02x\n",
+ report.bus_status, dev->clock, bus_busy);
+ /*
+ * Do not check the busy bit for combined transactions
+ * since the controller keeps the bus busy between writing
+ * and reading IOs to ensure an atomic operation.
+ */
if (report.bus_status & (FT260_I2C_STATUS_CTRL_BUSY | bus_busy))
return -EAGAIN;
@@ -602,13 +607,10 @@ static int ft260_hid_output_report(struct hid_device *hdev, u8 *data,
}
static int ft260_hid_output_report_check_status(struct ft260_device *dev,
- u8 *data, int len)
+ u8 *data, int len, u8 bus_busy)
{
- u8 bus_busy;
int ret, usec, try = 100;
struct hid_device *hdev = dev->hdev;
- struct ft260_i2c_write_request_report *rep =
- (struct ft260_i2c_write_request_report *)data;
ret = ft260_hid_output_report(hdev, data, len);
if (ret < 0) {
@@ -625,16 +627,6 @@ static int ft260_hid_output_report_check_status(struct ft260_device *dev,
ft260_dbg("wait %d usec, len %d\n", usec, len);
}
- /*
- * Do not check the busy bit for combined transactions
- * since the controller keeps the bus busy between writing
- * and reading IOs to ensure an atomic operation.
- */
- if (rep->flag == FT260_FLAG_START)
- bus_busy = 0;
- else
- bus_busy = FT260_I2C_STATUS_BUS_BUSY;
-
do {
ret = ft260_xfer_status(dev, bus_busy);
if (ret != -EAGAIN)
@@ -651,6 +643,7 @@ static int ft260_hid_output_report_check_status(struct ft260_device *dev,
static int ft260_i2c_write(struct ft260_device *dev, u8 addr, u8 *data,
int len, u8 flag)
{
+ u8 bus_busy = 0;
int ret, wr_len, idx = 0;
struct ft260_i2c_write_request_report *rep =
(struct ft260_i2c_write_request_report *)dev->i2c_wr_buf;
@@ -663,8 +656,10 @@ static int ft260_i2c_write(struct ft260_device *dev, u8 addr, u8 *data,
do {
if (len <= FT260_WR_I2C_DATA_MAX) {
wr_len = len;
- if (flag == FT260_FLAG_START_STOP)
+ if (flag == FT260_FLAG_START_STOP) {
rep->flag |= FT260_FLAG_STOP;
+ bus_busy = FT260_I2C_STATUS_BUS_BUSY;
+ }
} else {
wr_len = FT260_WR_I2C_DATA_MAX;
}
@@ -680,7 +675,7 @@ static int ft260_i2c_write(struct ft260_device *dev, u8 addr, u8 *data,
rep->flag, data[0]);
ret = ft260_hid_output_report_check_status(dev, (u8 *)rep,
- wr_len + 4);
+ wr_len + 4, bus_busy);
if (ret < 0) {
ft260_dbg("%s: failed with %d\n", __func__, ret);
return ret;
@@ -721,7 +716,9 @@ static int ft260_smbus_write(struct ft260_device *dev, u8 addr, u8 cmd,
ft260_dbg("rep %#02x addr %#02x cmd %#02x datlen %d replen %d\n",
rep->report, addr, cmd, rep->length, len);
- ret = ft260_hid_output_report_check_status(dev, (u8 *)rep, len);
+ ret = ft260_hid_output_report_check_status(dev, (u8 *)rep, len,
+ (flag & FT260_FLAG_STOP) ?
+ FT260_I2C_STATUS_BUS_BUSY : 0);
if (ret < 0)
ft260_dbg("%s: failed with %d\n", __func__, ret);
--
2.43.0
^ permalink raw reply related [flat|nested] 43+ messages in thread
* [PATCH 12/13] HID: ft260: workaround for TN_189 errata endpoint STALL after enumeration
2026-08-22 21:39 [PATCH 00/13] HID: ft260: add UART and GPIO support, plus I2C fixes Michael Zaidman
` (10 preceding siblings ...)
2026-08-22 21:39 ` [PATCH 11/13] HID: ft260: i2c: fix large write transaction failure Michael Zaidman
@ 2026-08-22 21:39 ` Michael Zaidman
2026-08-22 22:03 ` sashiko-bot
2026-08-22 21:39 ` [PATCH 13/13] HID: ft260: i2c: abort in-flight transfers with STOP before reset Michael Zaidman
2026-08-25 8:21 ` [PATCH 00/13] HID: ft260: add UART and GPIO support, plus I2C fixes Linus Walleij
13 siblings, 1 reply; 43+ messages in thread
From: Michael Zaidman @ 2026-08-22 21:39 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: Linus Walleij, Bartosz Golaszewski, Germain Hebert, Rio Liu,
Bruno Giacomazzi, Christina Quast, linux-input, linux-gpio,
linux-i2c, linux-kernel, Michael Zaidman, Andreas Boose
FTDI errata TN_189 (Section 2.1) documents a silicon bug where the
FT260's USB interrupt endpoints are occasionally halted right after
enumeration. When this happens, Clear-Feature ENDPOINT_HALT does not
recover the endpoint and the only known recovery is a USB device reset.
This patch implements an in-driver workaround:
1. ft260_check_intr_ep_health() observes the STALL by attempting an
actual interrupt IN transfer. The FT260 does not honestly report
its halt state via USB_REQ_GET_STATUS (returns 0 even when
STALLed; confirmed separately by FTDI engineering with a USB
analyzer trace), so we cannot rely on it; instead we let the
host controller return -EPIPE when it sees the STALL handshake.
2. ft260_check_dev_responsive() catches the broader broken state
where the interrupt endpoint may look healthy but the device
still fails to respond to control transfers. A USB_REQ_GET_STATUS
to the device with a short 500 ms timeout fails fast on a broken
device, preventing later probe stages from hanging on usbhid's
default 10 s timeouts and starving the usb_hub_wq workqueue.
3. When either check fails, probe schedules a deferred work item
and returns -ENODEV so that hub_event releases the device lock
quickly. The work item retries usb_lock_device_for_reset() up to
10 times (~10 s; each attempt already polls for up to one second)
before giving up, then calls usb_reset_device() and explicitly
unbinds/rebinds all USB interfaces to force usbhid to recreate
the HID devices and trigger a fresh ft260_probe(). The
unbind+rebind step is needed because usbhid's pre_reset and
post_reset both return 0, so usb_reset_device() alone keeps
usbhid bound to stale HID device state.
FTDI engineering tested this on a Raspberry Pi 4 Model B Rev 1.5
running Linux 6.12.62-v8+ on an xhci_hcd host, with the FT260 connected
at full-speed through a downstream USB 2.0 hub. Across 28,684
re-enumeration cycles, 350 cycles triggered the recovery path. Two of
those required two consecutive USB resets before the device returned.
All 28,684 cycles recovered to a fully functional state with I2C and
UART working end-to-end.
Reported-by: Andreas Boose <Andreas.Boose@almex.de>
Closes: https://github.com/MichaelZaidman/hid-ft260/issues/40
Link: https://ftdichip.com/wp-content/uploads/2026/05/TN_189-FT260-Errata-Technical-Note.pdf
Signed-off-by: Michael Zaidman <michael.zaidman@gmail.com>
---
drivers/hid/hid-ft260.c | 229 ++++++++++++++++++++++++++++++++++++++++
1 file changed, 229 insertions(+)
diff --git a/drivers/hid/hid-ft260.c b/drivers/hid/hid-ft260.c
index 36687c086b40..9ae688f6208f 100644
--- a/drivers/hid/hid-ft260.c
+++ b/drivers/hid/hid-ft260.c
@@ -2359,15 +2359,227 @@ static int ft260_uart_probe(struct ft260_device *dev,
return ret;
}
+/*
+ * FT260 errata TN_189 Section 2.1: the USB interrupt endpoints are
+ * occasionally halted right after enumeration. When this happens:
+ * - Standard Clear-Feature ENDPOINT_HALT does not recover the endpoint
+ * - Subsequent communication with the device is dead
+ * - The only known recovery is a USB device reset
+ *
+ * A separate finding from FTDI engineering (confirmed by USB analyzer
+ * trace while testing this workaround) is that the FT260 does NOT
+ * honestly report the halt state via USB_REQ_GET_STATUS: it returns 0
+ * even when the endpoint is STALLed. Detection must therefore observe
+ * the STALL handshake at the host controller level rather than ask
+ * the device.
+ *
+ * Recovery is performed by a deferred work item that resets the USB
+ * device and unbinds/rebinds all interfaces to force usbhid to
+ * destroy stale HID devices and create fresh ones, which triggers a
+ * new ft260_probe() that succeeds.
+ *
+ * https://ftdichip.com/wp-content/uploads/2026/05/TN_189-FT260-Errata-Technical-Note.pdf
+ */
+struct ft260_reset_work {
+ struct work_struct work;
+ struct usb_interface *usbif;
+};
+
+static void ft260_reset_and_rebind(struct work_struct *ws)
+{
+ struct ft260_reset_work *rw =
+ container_of(ws, struct ft260_reset_work, work);
+ struct usb_interface *usbif = rw->usbif;
+ struct usb_device *usbdev = interface_to_usbdev(usbif);
+ struct usb_host_config *actconfig;
+ int ret, i, attempt;
+
+ /*
+ * Retry the device lock for up to ~10 seconds. The lock is held
+ * by hub_event for the duration of device enumeration; with the
+ * fast-fail responsiveness check in probe, both interfaces should
+ * abort within ~1-2 seconds, after which the lock becomes free.
+ * Each usb_lock_device_for_reset() attempt already polls for up to
+ * one second internally.
+ */
+ for (attempt = 0; attempt < 10; attempt++) {
+ ret = usb_lock_device_for_reset(usbdev, NULL);
+ if (ret >= 0)
+ break;
+ if (ret == -ENODEV || ret == -EHOSTUNREACH) {
+ dev_dbg(&usbif->dev,
+ "device gone before reset (%d), abort\n", ret);
+ goto out;
+ }
+ /* -EBUSY: someone else holds the lock; retry. */
+ }
+ if (ret < 0) {
+ dev_err(&usbif->dev,
+ "failed to acquire USB device lock for reset after %d attempts: %d\n",
+ attempt, ret);
+ goto out;
+ }
+
+ ret = usb_reset_device(usbdev);
+ if (ret < 0) {
+ dev_err(&usbif->dev, "USB reset failed: %d\n", ret);
+ usb_unlock_device(usbdev);
+ goto out;
+ }
+
+ /*
+ * usb_reset_device() keeps usbhid bound (its pre_reset/post_reset
+ * both return 0) and does not re-trigger HID-level driver probing.
+ * Unbind and rebind all USB interfaces to force usbhid to destroy
+ * stale HID devices and create new ones, which triggers fresh
+ * ft260_probe() calls.
+ */
+ actconfig = usbdev->actconfig;
+ for (i = 0; actconfig && i < actconfig->desc.bNumInterfaces; i++) {
+ struct usb_interface *intf = actconfig->interface[i];
+
+ if (intf && intf->dev.driver)
+ device_release_driver(&intf->dev);
+ }
+ for (i = 0; actconfig && i < actconfig->desc.bNumInterfaces; i++) {
+ struct usb_interface *intf = actconfig->interface[i];
+
+ if (!intf)
+ continue;
+ ret = device_attach(&intf->dev);
+ if (ret < 0)
+ dev_err(&intf->dev,
+ "failed to rebind USB interface: %d\n", ret);
+ }
+
+ usb_unlock_device(usbdev);
+out:
+ usb_put_intf(usbif);
+ kfree(rw);
+}
+
+static int ft260_schedule_reset(struct usb_interface *usbif)
+{
+ struct ft260_reset_work *rw;
+
+ rw = kmalloc_obj(*rw, GFP_KERNEL);
+ if (!rw)
+ return -ENOMEM;
+
+ usb_get_intf(usbif);
+ rw->usbif = usbif;
+ INIT_WORK(&rw->work, ft260_reset_and_rebind);
+ schedule_work(&rw->work);
+
+ return 0;
+}
+
+/*
+ * Detect whether the device's interrupt IN endpoint is in the STALL
+ * state described by TN_189. GET_STATUS is unreliable on the FT260
+ * (returns 0 even when halted, confirmed by FTDI with a USB analyzer
+ * trace), so observe the STALL handshake by attempting an actual
+ * interrupt IN transfer. The host controller returns -EPIPE when it
+ * receives a STALL handshake.
+ *
+ * Must be called before hid_hw_open() so it does not race against
+ * usbhid's own interrupt IN URB.
+ */
+static int ft260_check_intr_ep_health(struct hid_device *hdev)
+{
+ struct usb_interface *usbif = to_usb_interface(hdev->dev.parent);
+ struct usb_device *usbdev = interface_to_usbdev(usbif);
+ struct usb_host_interface *iface_desc = usbif->cur_altsetting;
+ struct usb_endpoint_descriptor *ep = NULL;
+ unsigned int pipe;
+ u8 *buf;
+ int ret, actual_length, i;
+
+ for (i = 0; i < iface_desc->desc.bNumEndpoints; i++) {
+ if (usb_endpoint_is_int_in(&iface_desc->endpoint[i].desc)) {
+ ep = &iface_desc->endpoint[i].desc;
+ break;
+ }
+ }
+ if (!ep)
+ return 0;
+
+ buf = kmalloc(FT260_REPORT_MAX_LEN, GFP_KERNEL);
+ if (!buf)
+ return -ENOMEM;
+
+ pipe = usb_rcvintpipe(usbdev, ep->bEndpointAddress);
+ ret = usb_interrupt_msg(usbdev, pipe, buf, FT260_REPORT_MAX_LEN,
+ &actual_length, 100);
+ kfree(buf);
+
+ if (ret == -EPIPE) {
+ hid_warn(hdev,
+ "interrupt IN ep %#x halted (TN_189 errata), scheduling USB reset and rebind\n",
+ ep->bEndpointAddress);
+ return -ENODEV;
+ }
+
+ return 0;
+}
+
+/*
+ * Quick check that the device responds to a standard control transfer.
+ * When the FT260 is in the buggy post-enumeration state, control
+ * transfers initiated by later probe stages (chip version retrieval,
+ * UART/I2C configuration, etc.) can hang for very long periods,
+ * starving the usb_hub_wq workqueue and preventing the reset work
+ * from acquiring the device lock.
+ *
+ * Issue USB_REQ_GET_STATUS to the device (any compliant USB device
+ * must answer immediately) with a short explicit timeout. If it
+ * fails, treat the device as broken and bail out before reaching
+ * anything that can block.
+ *
+ * The interrupt-endpoint health check above only catches STALLs on
+ * the interrupt IN path; this check catches the broader broken state
+ * that affects the other interface even when its interrupt endpoint
+ * happens to look healthy.
+ */
+static int ft260_check_dev_responsive(struct hid_device *hdev)
+{
+ struct usb_interface *usbif = to_usb_interface(hdev->dev.parent);
+ struct usb_device *usbdev = interface_to_usbdev(usbif);
+ __le16 *status;
+ int ret;
+
+ status = kmalloc_obj(*status, GFP_KERNEL);
+ if (!status)
+ return -ENOMEM;
+
+ ret = usb_control_msg(usbdev, usb_rcvctrlpipe(usbdev, 0),
+ USB_REQ_GET_STATUS,
+ USB_DIR_IN | USB_RECIP_DEVICE,
+ 0, 0, status, sizeof(*status), 500);
+ kfree(status);
+
+ if (ret < 0) {
+ hid_warn(hdev,
+ "device unresponsive to GET_STATUS (%d), suspected TN_189 errata, scheduling USB reset and rebind\n",
+ ret);
+ return -ENODEV;
+ }
+
+ return 0;
+}
+
static int ft260_probe(struct hid_device *hdev, const struct hid_device_id *id)
{
struct ft260_device *dev;
+ struct usb_interface *usbif;
struct ft260_get_chip_version_report version;
struct ft260_get_system_status_report cfg;
int ret;
if (!hid_is_usb(hdev))
return -EINVAL;
+
+ usbif = to_usb_interface(hdev->dev.parent);
/*
* We cannot use devm_kzalloc here because the port has to survive
* until destroy function call.
@@ -2392,6 +2604,23 @@ static int ft260_probe(struct hid_device *hdev, const struct hid_device_id *id)
goto hid_fail;
}
+ /*
+ * TN_189 errata workaround: bail out fast on a broken device so
+ * that hub_event releases the device lock quickly, allowing the
+ * scheduled reset work to acquire it and recover the device.
+ */
+ ret = ft260_check_intr_ep_health(hdev);
+ if (ret) {
+ ft260_schedule_reset(usbif);
+ goto err_hid_stop;
+ }
+
+ ret = ft260_check_dev_responsive(hdev);
+ if (ret) {
+ ft260_schedule_reset(usbif);
+ goto err_hid_stop;
+ }
+
ret = hid_hw_open(hdev);
if (ret) {
hid_err(hdev, "failed to open HID HW\n");
--
2.43.0
^ permalink raw reply related [flat|nested] 43+ messages in thread
* [PATCH 13/13] HID: ft260: i2c: abort in-flight transfers with STOP before reset
2026-08-22 21:39 [PATCH 00/13] HID: ft260: add UART and GPIO support, plus I2C fixes Michael Zaidman
` (11 preceding siblings ...)
2026-08-22 21:39 ` [PATCH 12/13] HID: ft260: workaround for TN_189 errata endpoint STALL after enumeration Michael Zaidman
@ 2026-08-22 21:39 ` Michael Zaidman
2026-08-22 22:12 ` sashiko-bot
2026-08-25 8:21 ` [PATCH 00/13] HID: ft260: add UART and GPIO support, plus I2C fixes Linus Walleij
13 siblings, 1 reply; 43+ messages in thread
From: Michael Zaidman @ 2026-08-22 21:39 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: Linus Walleij, Bartosz Golaszewski, Germain Hebert, Rio Liu,
Bruno Giacomazzi, Christina Quast, linux-input, linux-gpio,
linux-i2c, linux-kernel, Michael Zaidman
A transfer that ends without STOP leaves the FT260 I2C controller
with BUS_BUSY set. I2C reset (AN_394 section 4.4.15) restarts the
master but, with the driver still bound, status stays 0x60
(CTRL_IDLE | BUS_BUSY) even when SCL and SDA are pulled up. The
next START then fails with 0x72 (arbitration lost + error) and
further resets do not recover; only a USB replug did.
Add ft260_i2c_abort(): a STOP-only write (flag 0x04, zero payload)
then FT260_SET_I2C_RESET. Use it wherever the host tears down an
open transaction:
- read wait timeout
- read status check failure after the HID report arrived
- write status poll failure after the report was sent
- SMBus block read with an invalid count (count phase has no STOP)
Leave ft260_i2c_reset() alone for HID output failure (the request
may never have reached the controller), sysfs i2c_reset, and probe.
Tested on a UMFT260EV1A with a 24LC512 at 0x51. Five 1 ms read
timeouts each followed by STOP+reset left bus_status 0x20; raising
the timeout to 25 ms on the same loaded module then read 16 bytes
successfully.
This matters more with the 25/75 ms completion timeout from
"HID: ft260: i2c: reduce driver module loading time", which makes
read timeouts more likely than mainline's 5000 ms wait.
Signed-off-by: Michael Zaidman <michael.zaidman@gmail.com>
---
drivers/hid/hid-ft260.c | 43 ++++++++++++++++++++++++++++++++++-------
1 file changed, 36 insertions(+), 7 deletions(-)
diff --git a/drivers/hid/hid-ft260.c b/drivers/hid/hid-ft260.c
index 9ae688f6208f..a35a1af2d7e5 100644
--- a/drivers/hid/hid-ft260.c
+++ b/drivers/hid/hid-ft260.c
@@ -538,6 +538,33 @@ static int ft260_i2c_reset(struct hid_device *hdev)
return ret;
}
+static int ft260_hid_output_report(struct hid_device *hdev, u8 *data,
+ size_t len);
+
+/*
+ * STOP with no START and no payload. Used by ft260_i2c_abort() when
+ * tearing down an in-flight transfer: I2C reset alone can leave
+ * BUS_BUSY set even when SCL/SDA are idle, and the next START then
+ * fails with arbitration lost.
+ */
+static int ft260_i2c_stop(struct hid_device *hdev, u8 addr)
+{
+ u8 buf[4] = {
+ FT260_I2C_REPORT_MIN,
+ addr,
+ FT260_FLAG_STOP,
+ 0,
+ };
+
+ return ft260_hid_output_report(hdev, buf, sizeof(buf));
+}
+
+static int ft260_i2c_abort(struct hid_device *hdev, u8 addr)
+{
+ ft260_i2c_stop(hdev, addr);
+ return ft260_i2c_reset(hdev);
+}
+
static int ft260_xfer_status(struct ft260_device *dev, u8 bus_busy)
{
struct hid_device *hdev = dev->hdev;
@@ -607,7 +634,8 @@ static int ft260_hid_output_report(struct hid_device *hdev, u8 *data,
}
static int ft260_hid_output_report_check_status(struct ft260_device *dev,
- u8 *data, int len, u8 bus_busy)
+ u8 *data, int len, u8 addr,
+ u8 bus_busy)
{
int ret, usec, try = 100;
struct hid_device *hdev = dev->hdev;
@@ -636,7 +664,7 @@ static int ft260_hid_output_report_check_status(struct ft260_device *dev,
if (ret == 0)
return 0;
- ft260_i2c_reset(hdev);
+ ft260_i2c_abort(hdev, addr);
return -EIO;
}
@@ -675,7 +703,8 @@ static int ft260_i2c_write(struct ft260_device *dev, u8 addr, u8 *data,
rep->flag, data[0]);
ret = ft260_hid_output_report_check_status(dev, (u8 *)rep,
- wr_len + 4, bus_busy);
+ wr_len + 4, addr,
+ bus_busy);
if (ret < 0) {
ft260_dbg("%s: failed with %d\n", __func__, ret);
return ret;
@@ -716,7 +745,7 @@ static int ft260_smbus_write(struct ft260_device *dev, u8 addr, u8 cmd,
ft260_dbg("rep %#02x addr %#02x cmd %#02x datlen %d replen %d\n",
rep->report, addr, cmd, rep->length, len);
- ret = ft260_hid_output_report_check_status(dev, (u8 *)rep, len,
+ ret = ft260_hid_output_report_check_status(dev, (u8 *)rep, len, addr,
(flag & FT260_FLAG_STOP) ?
FT260_I2C_STATUS_BUS_BUSY : 0);
if (ret < 0)
@@ -784,7 +813,7 @@ static int ft260_i2c_read(struct ft260_device *dev, u8 addr, u8 *data,
timeout_jiffies = msecs_to_jiffies(timeout);
if (!wait_for_completion_timeout(&dev->wait, timeout_jiffies)) {
ret = -ETIMEDOUT;
- ft260_i2c_reset(hdev);
+ ft260_i2c_abort(hdev, addr);
goto ft260_i2c_read_exit;
}
@@ -798,7 +827,7 @@ static int ft260_i2c_read(struct ft260_device *dev, u8 addr, u8 *data,
ret = ft260_xfer_status(dev, bus_busy);
if (ret < 0) {
ret = -EIO;
- ft260_i2c_reset(hdev);
+ ft260_i2c_abort(hdev, addr);
goto ft260_i2c_read_exit;
}
@@ -982,7 +1011,7 @@ static int ft260_smbus_xfer(struct i2c_adapter *adapter, u16 addr, u16 flags,
hid_warn(hdev,
"smbus block read: invalid count %u from slave 0x%02x\n",
count, addr);
- ft260_i2c_reset(hdev);
+ ft260_i2c_abort(hdev, addr);
ret = -EPROTO;
goto smbus_exit;
}
--
2.43.0
^ permalink raw reply related [flat|nested] 43+ messages in thread
* Re: [PATCH 04/13] HID: ft260: i2c: reduce driver module loading time
2026-08-22 21:39 ` [PATCH 04/13] HID: ft260: i2c: reduce driver module loading time Michael Zaidman
@ 2026-08-22 21:51 ` sashiko-bot
0 siblings, 0 replies; 43+ messages in thread
From: sashiko-bot @ 2026-08-22 21:51 UTC (permalink / raw)
To: Michael Zaidman; +Cc: linux-input, dmitry.torokhov
Thank you for your contribution! Sashiko AI review found 6 potential issue(s) to consider:
New issues:
- [High] The dynamic I2C read timeout incorrectly applies a 25ms timeout for last-chunk reads up to 180 bytes, causing spurious timeouts for chunks > 60 bytes.
- [High] Delayed USB HID reports from prematurely timed-out I2C transactions corrupt subsequent I2C transfers.
Pre-existing issues:
- [Critical] Double free of `dev` on UART probe error path.
- [High] The driver frees its private data before stopping hardware I/O, allowing asynchronous incoming HID reports to cause a use-after-free.
- [High] Race condition in teardown sequence of wakeup timer and workqueue leads to Use-After-Free on device unplug.
- [High] A malicious USB device can cause a divide-by-zero kernel panic by reporting a 0 Hz clock.
--
--- Patch [4]: [PATCH 04/13] HID: ft260: i2c: reduce driver module loading time ---
commit ce4834500e105e8a3ed2770b588049de28ffe3cf
Author: Michael Zaidman <michael.zaidman@gmail.com>
HID: ft260: i2c: reduce driver module loading time
The commit adjusts the I2C read timeout to depend on the amount of data
being read. This is intended to reduce the module loading time significantly,
lowering the lm75 address range scan time to about 200ms.
> diff --git a/drivers/hid/hid-ft260.c b/drivers/hid/hid-ft260.c
> index cc3b8f5791dea..4435a39fce239 100644
> --- a/drivers/hid/hid-ft260.c
> +++ b/drivers/hid/hid-ft260.c
[ ... ]
> @@ -732,10 +739,12 @@ static int ft260_i2c_read(struct ft260_device *dev, u8 addr, u8 *data,
> flag = 0; /* no fresh START - continue current transaction */
> do {
> if (len <= rd_data_max) {
> + timeout = FT260_RD_ONE_REPORT_TO;
[Severity: High]
Does this logic aggressively override the timeout to 25ms for the final
chunk of any transfer, even if that chunk is 180 bytes long?
If an I2C read request is made where the final chunk is between 61 and 180
bytes (e.g., a 240-byte read), the condition if (len <= rd_data_max)
correctly matches the final chunk, but erroneously assigns it the
FT260_RD_ONE_REPORT_TO (25ms) timeout instead of the required 75ms.
As noted in the commit message, a 180-byte chunk physically requires about
27ms of wire time at 60kHz. A 25ms timeout is guaranteed to expire before the
hardware finishes, which could lead to a deterministic failure (-ETIMEDOUT)
on any I2C read larger than 60 bytes.
> rd_len = len;
> if (want_stop)
> flag |= FT260_FLAG_STOP;
> } else {
> + timeout = FT260_RD_MULTI_REPORT_TO;
> rd_len = rd_data_max;
> }
> rd_data_max = FT260_RD_DATA_MAX;
[ ... ]
> @@ -762,8 +771,8 @@ static int ft260_i2c_read(struct ft260_device *dev, u8 addr, u8 *data,
> goto ft260_i2c_read_exit;
> }
>
> - timeout = msecs_to_jiffies(5000);
> - if (!wait_for_completion_timeout(&dev->wait, timeout)) {
> + timeout_jiffies = msecs_to_jiffies(timeout);
> + if (!wait_for_completion_timeout(&dev->wait, timeout_jiffies)) {
[Severity: High]
Will the drastic reduction of the timeout bounds without a concurrent
mechanism to purge the HID input queue or use sequence numbers expose the
driver to data corruption?
If a premature timeout occurs in ft260_i2c_read() due to the reduced bounds,
it causes the function to clear dev->read_buf and return. However, the USB
host controller may still receive the delayed response.
When a subsequent I2C read starts, it repopulates dev->read_buf. Since
ft260_raw_event() lacks sequence numbers, could it blindly copy the stale
delayed USB report into the new buffer and call complete(), allowing the new
transaction to complete with invalid data?
> ret = -ETIMEDOUT;
> ft260_i2c_reset(hdev);
> goto ft260_i2c_read_exit;
[Severity: High]
This isn't a bug introduced by this patch, but does the driver free its
private data before stopping hardware I/O, allowing asynchronous incoming HID
reports to cause a use-after-free?
In ft260_remove():
} else {
// ...
kfree(dev);
}
hid_hw_close(hdev);
hid_hw_stop(hdev);
During device unplug, the driver unconditionally frees its private dev
structure before calling hid_hw_stop(hdev). Because the hardware and the USB
interrupt IN endpoint remain active until hid_hw_stop() is called, an
incoming HID report during this window will be dispatched to
ft260_raw_event(), which will attempt to acquire dev->read_lock on the
already freed dev memory.
[Severity: Critical]
This isn't a bug introduced by this patch, but is there a double free of dev
on the UART probe error path?
In ft260_probe():
hid_fail:
kfree(dev);
alloc_fail:
If ft260_uart_probe() fails, it jumps to the error path and calls
ft260_uart_port_remove(dev). This triggers tty_port_put(), which drops the
final reference and calls ft260_uart_port_destroy(), explicitly freeing dev
via kfree(port).
The function then returns a failure code to ft260_probe(), which follows its
own error path to hid_fail where it executes kfree(dev) a second time on the
same pointer.
[Severity: High]
This isn't a bug introduced by this patch, but is there a race condition in
the teardown sequence of the wakeup timer and workqueue that leads to a
use-after-free on device unplug?
In ft260_remove():
cancel_work_sync(&dev->wakeup_work);
tty_port_unregister_device(&dev->port, ft260_tty_driver, dev->index);
ft260_uart_port_remove(dev);
The cancel_work_sync() is executed before timer_delete_sync() (called within
ft260_uart_port_remove()), which fails to guarantee the workqueue will remain
empty.
Because the timer can fire between these two calls, ft260_uart_start_wakeup()
can erroneously call schedule_work() to re-enqueue the work. Once unbind
completes and dev is freed via the TTY destructor, the pending work executes
and accesses freed memory.
[Severity: High]
This isn't a bug introduced by this patch, but can a malicious USB device
cause a divide-by-zero kernel panic by reporting a 0 Hz clock?
In ft260_hid_output_report_check_status():
/* transfer time = 1 / clock(KHz) * 9 bits * bytes */
usec = len * 9000 / dev->clock;
The unvalidated clock value read from the hardware is used directly as a
divisor. If a malicious or malfunctioning USB device responds to the
FT260_I2C_STATUS feature request in ft260_xfer_status() with a report.clock
value of 0, the driver saves this to dev->clock without validation.
Subsequent calls will divide by dev->clock to calculate delay timings,
immediately crashing the kernel.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260822213941.98882-1-michael.zaidman@gmail.com?part=4
^ permalink raw reply [flat|nested] 43+ messages in thread
* Re: [PATCH 05/13] HID: ft260: i2c: silence sysfs store big-numbers
2026-08-22 21:39 ` [PATCH 05/13] HID: ft260: i2c: silence sysfs store big-numbers Michael Zaidman
@ 2026-08-22 21:51 ` sashiko-bot
0 siblings, 0 replies; 43+ messages in thread
From: sashiko-bot @ 2026-08-22 21:51 UTC (permalink / raw)
To: Michael Zaidman; +Cc: dmitry.torokhov, linux-input
Thank you for your contribution! Sashiko AI review found 5 potential issue(s) to consider:
New issues:
- [High] The patch unconditionally overwrites the error code from `ft260_hid_feature_report_set()` with `count`, masking hardware write errors from userspace.
Pre-existing issues:
- [Critical] Double free on UART probe error path and Use-After-Free during device remove.
- [High] Native endianness is incorrectly leaked to the device when parsing multi-byte sysfs attributes.
- [High] Teardown order of the wakeup timer and workqueue during device removal allows a Use-After-Free via a re-scheduled work item.
- [High] Manually freeing `dev` with `kfree()` while a devres-managed GPIO chip is still registered creates a use-after-free window.
--
--- Patch [5]: [PATCH 05/13] HID: ft260: i2c: silence sysfs store big-numbers ---
commit ebf18ef9c09c87f02215b8dbbbe0f60a02ef6565
Author: Michael Zaidman <michael.zaidman@gmail.com>
HID: ft260: i2c: silence sysfs store big-numbers
FT260_ATTR_STORE returned the feature-report transfer length from
ft260_hid_feature_report_set() instead of the sysfs write size. For
values like 1000, userspace writes more bytes than the HID report
length, so the VFS retries the remainder and reports "Invalid argument"
even though the attribute was applied correctly:
$ echo 1000 > .../clock
bash: line 1: echo: write error: Invalid argument
$ cat .../clock
1000
Return count after a successful parse so the full write is consumed.
> diff --git a/drivers/hid/hid-ft260.c b/drivers/hid/hid-ft260.c
> index 4435a39fce239..8db896f164cd0 100644
> --- a/drivers/hid/hid-ft260.c
> +++ b/drivers/hid/hid-ft260.c
> @@ -1479,6 +1479,7 @@ static void ft260_attr_dummy_func(struct hid_device *hdev, u8 req, u16 value)
> hid_err(hdev, "%s: failed!\n", __func__); \
> else \
> func(hdev, req, name); \
> + ret = count; \
> mutex_unlock(&dev->lock); \
> } else { \
> ret = -EINVAL; \
[Severity: High]
Does this unconditionally overwrite the error code from
ft260_hid_feature_report_set()?
Since the else branch lacks curly braces, it appears ret = count executes
even if ret < 0. Could this mask hardware write errors from userspace by
incorrectly telling the VFS that the write succeeded?
[Severity: High]
This isn't a bug introduced by this patch, but does FT260_WORD_ATTR_STORE()
leak native endianness to the device?
drivers/hid/hid-ft260.c:
#define FT260_WORD_ATTR_STORE(name, reptype, req, func) \
FT260_ATTR_STORE(name, reptype, FT260_SYSTEM_SETTINGS, req, \
__le16, u16, kstrtou16, func)
It passes a __le16 type directly to kstrtou16(), which seems to write the value
in native byte order, assigning it to the little-endian report structure
without cpu_to_le16() conversion. Could this cause incorrect device
configuration on big-endian architectures?
[Severity: Critical]
This is a pre-existing issue, but does ft260_probe() have a double free on
the UART probe error path?
drivers/hid/hid-ft260.c:ft260_probe()
hid_fail:
kfree(dev);
return ret;
If ft260_uart_probe() fails after tty_port_init(), ft260_uart_port_remove()
calls tty_port_put(), destroying the port and freeing dev. The error path
then calls kfree(dev).
Similarly, in ft260_remove(), does this sequence cause a use-after-free?
drivers/hid/hid-ft260.c:ft260_remove()
ft260_uart_port_remove(dev);
/* dev is still needed, so we will free it in _destroy func */
if (dev->chip_mode & FT260_MODE_UART ||
This dereferences dev->chip_mode immediately after ft260_uart_port_remove()
has dropped the final port reference and freed dev.
[Severity: High]
This is a pre-existing issue, but could the teardown order of the wakeup timer
and workqueue in ft260_remove() allow a use-after-free?
drivers/hid/hid-ft260.c:ft260_remove()
cancel_work_sync(&dev->wakeup_work);
tty_unregister_device(ft260_tty_driver,
dev->index);
ft260_uart_port_remove(dev);
The workqueue is cancelled before the timer is deleted inside
ft260_uart_port_remove(). If the timer fires in this window, could it check
dev->reschedule_work and reschedule the work item, which would then execute
after dev is freed?
[Severity: High]
This is a pre-existing issue, but does manually freeing dev with kfree()
while a devres-managed GPIO chip is still registered create a use-after-free
window?
drivers/hid/hid-ft260.c:ft260_remove()
i2c_del_adapter(&dev->adap);
kfree(dev);
}
And in the ft260_probe() error path:
drivers/hid/hid-ft260.c:ft260_probe()
hid_fail:
kfree(dev);
return ret;
Since the GPIO chip is managed by devm_gpiochip_add_data(), it remains
registered until the core processes devres actions after the function returns,
leaving a window where the GPIO chip can be accessed while its backing data
(dev) has been freed.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260822213941.98882-1-michael.zaidman@gmail.com?part=5
^ permalink raw reply [flat|nested] 43+ messages in thread
* Re: [PATCH 06/13] HID: ft260: i2c: reduce bus-error message severity
2026-08-22 21:39 ` [PATCH 06/13] HID: ft260: i2c: reduce bus-error message severity Michael Zaidman
@ 2026-08-22 21:52 ` sashiko-bot
0 siblings, 0 replies; 43+ messages in thread
From: sashiko-bot @ 2026-08-22 21:52 UTC (permalink / raw)
To: Michael Zaidman; +Cc: dmitry.torokhov, linux-input
Thank you for your contribution! Sashiko AI review found 6 potential issue(s) to consider:
New issues:
- [Low] Redundant `__func__` prefix in `ft260_dbg` logging.
Pre-existing issues:
- [High] Hardware is stopped after freeing device resources in `ft260_remove`, causing a Use-After-Free if a report arrives.
- [High] Accessing `dev` after it has been synchronously freed by `ft260_uart_port_remove`.
- [High] Double free of `dev` in the `ft260_probe` error path for UART interfaces.
- [High] The devres-managed GPIO chip remains active after the driver manually frees its context.
- [High] Improper cleanup order of workqueue and timer leads to a leaked work item and UAF.
--
--- Patch [6]: [PATCH 06/13] HID: ft260: i2c: reduce bus-error message severity ---
commit 726d7795b8219b2b5c0f6063a18546f73c323838
Author: Michael Zaidman <michael.zaidman@gmail.com>
HID: ft260: i2c: reduce bus-error message severity
This commit reduces the severity of I2C bus error messages during bus scans by
replacing hid_err with ft260_dbg. It keeps returning -EIO but avoids spamming
dmesg with routine failures.
> diff --git a/drivers/hid/hid-ft260.c b/drivers/hid/hid-ft260.c
> index 8db896f164cd0..1ed3701eaaf75 100644
> --- a/drivers/hid/hid-ft260.c
> +++ b/drivers/hid/hid-ft260.c
[ ... ]
> @@ -669,7 +669,7 @@ static int ft260_i2c_write(struct ft260_device *dev, u8 addr, u8 *data,
> ret = ft260_hid_output_report_check_status(dev, (u8 *)rep,
> wr_len + 4);
> if (ret < 0) {
> - hid_err(hdev, "%s: failed with %d\n", __func__, ret);
> + ft260_dbg("%s: failed with %d\n", __func__, ret);
[Severity: Low]
Does ft260_dbg already prepend the function name to the log output? If so,
keeping __func__ in the format string here might cause the function name to be
printed twice.
> return ret;
> }
>
[ ... ]
> @@ -710,7 +710,7 @@ static int ft260_smbus_write(struct ft260_device *dev, u8 addr, u8 cmd,
>
> ret = ft260_hid_output_report_check_status(dev, (u8 *)rep, len);
> if (ret < 0)
> - hid_err(dev->hdev, "%s: failed with %d\n", __func__, ret);
> + ft260_dbg("%s: failed with %d\n", __func__, ret);
[Severity: Low]
Similarly, does this result in a redundant function name in the log output?
>
> return ret;
> }
While reviewing this code, I also noticed a few other items in the driver.
[Severity: High]
This is a pre-existing issue, but is it possible for a use-after-free to occur
during device unbind in ft260_remove()?
drivers/hid/hid-ft260.c:ft260_remove() {
...
kfree(dev);
...
hid_hw_stop(hdev);
}
Because hid_hw_stop() is called at the end of the function, the HID hardware
is still active and can process incoming reports. If a report arrives, it
triggers ft260_raw_event() which accesses the already freed dev:
drivers/hid/hid-ft260.c:ft260_raw_event() {
struct ft260_device *dev = hid_get_drvdata(hdev);
...
}
Can this sequence be triggered by hot-unplugging or receiving interrupts
during removal?
[Severity: High]
This isn't a bug introduced by this patch, but does ft260_remove() access dev
after it has been synchronously freed?
drivers/hid/hid-ft260.c:ft260_remove() {
...
ft260_uart_port_remove(dev);
/* dev is still needed, so we will free it in _destroy func */
if (dev->iface_id == 0)
...
}
If the port is not actively opened by userspace, ft260_uart_port_remove()
drops the tty_port refcount to 0, which invokes the destructor and frees dev.
Is it safe to access dev->iface_id immediately after this call?
[Severity: High]
This is a pre-existing issue, but could the error path in ft260_probe() lead
to a double free for UART interfaces?
drivers/hid/hid-ft260.c:ft260_uart_probe() {
...
err_register_tty:
ft260_uart_port_remove(dev);
return ret;
}
If ft260_uart_probe() fails and drops the port reference, dev is freed. The
failure propagates back to ft260_probe(), which then executes:
drivers/hid/hid-ft260.c:ft260_probe() {
...
hid_fail:
kfree(dev);
}
Does this cause kfree(dev) to be called on an already freed pointer?
[Severity: High]
This isn't a bug introduced by this patch, but does devres leave the GPIO chip
active after its private data is freed?
drivers/hid/hid-ft260.c:ft260_remove() {
...
i2c_del_adapter(&dev->adap);
kfree(dev);
}
The driver registers a GPIO chip using devm_gpiochip_add_data() and assigns
dev as its private data. Since devres automatically cleans up the GPIO chip
only after the remove callback fully completes, can userspace race to invoke
GPIO operations during this window and dereference the freed dev?
[Severity: High]
This is a pre-existing issue, but does the cleanup order of the timer and
workqueue leak a work item and cause a use-after-free?
drivers/hid/hid-ft260.c:ft260_remove() {
...
cancel_work_sync(&dev->wakeup_work);
...
ft260_uart_port_remove(dev);
}
drivers/hid/hid-ft260.c:ft260_uart_port_remove() {
...
timer_delete_sync(&port->wakeup_timer);
}
Since cancel_work_sync() is invoked before the timer is shut down, could the
timer fire concurrently and invoke schedule_work() again? This would leave a
pending work item that executes after the driver is unloaded and its private
data freed. Should timer_delete_sync() occur before cancel_work_sync()?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260822213941.98882-1-michael.zaidman@gmail.com?part=6
^ permalink raw reply [flat|nested] 43+ messages in thread
* Re: [PATCH 07/13] HID: ft260: uart: enable flow control
2026-08-22 21:39 ` [PATCH 07/13] HID: ft260: uart: enable flow control Michael Zaidman
@ 2026-08-22 21:52 ` sashiko-bot
0 siblings, 0 replies; 43+ messages in thread
From: sashiko-bot @ 2026-08-22 21:52 UTC (permalink / raw)
To: Michael Zaidman; +Cc: linux-input, dmitry.torokhov
Thank you for your contribution! Sashiko AI review found 5 potential issue(s) to consider:
Pre-existing issues:
- [Critical] The device structure `dev` is freed prematurely during probe failure and device removal, causing a double-free and use-after-free.
- [Critical] `ft260_remove()` tears down driver data structures before stopping the HID transport, violating safe cleanup order.
- [High] Mismanaged asynchronous worker cleanup causes race conditions and Use-After-Free.
- [High] The shared `uart_wr_buf` buffer is used without locking, leading to data corruption.
- [High] The UART baud rate is double-swapped on big-endian architectures, causing astronomically incorrect values.
--
--- Patch [7]: [PATCH 07/13] HID: ft260: uart: enable flow control ---
commit e6afc26d742478ffb1a839b17d94e5df01b28507
Author: Michael Zaidman <michael.zaidman@gmail.com>
HID: ft260: uart: enable flow control
This patch updates the FT260 driver to honor CRTSCTS and IXON/IXOFF flow
control settings during termios setup. It ensures flow control is not
forced off, which improves reliability for large transfers at high baud rates.
> diff --git a/drivers/hid/hid-ft260.c b/drivers/hid/hid-ft260.c
> index 1ed3701eaaf75..1da42b8756d8c 100644
> --- a/drivers/hid/hid-ft260.c
> +++ b/drivers/hid/hid-ft260.c
> @@ -1868,8 +1868,12 @@ static int ft260_uart_change_speed(struct ft260_device *port,
>
> if (termios->c_cflag & CRTSCTS)
> req.flow_ctrl = FT260_UART_CFG_FLOW_CTRL_RTS_CTS;
> + else if (termios->c_iflag & (IXON | IXOFF))
> + req.flow_ctrl = FT260_UART_CFG_FLOW_CTRL_XON_XOFF;
> else
> - req.flow_ctrl = FT260_UART_CFG_FLOW_CTRL_OFF;
> + req.flow_ctrl = FT260_UART_CFG_FLOW_CTRL_NONE;
> +
> + req.breaking = FT260_UART_CFG_BREAKING_NO;
[Severity: High]
This isn't a bug introduced by this patch, but while reviewing this function,
does the baud rate configuration perform an incorrect byte swap?
Earlier in ft260_uart_change_speed():
put_unaligned_le32(cpu_to_le32(baud), &req.baudrate);
Since put_unaligned_le32() already handles the conversion from CPU-endian to
little-endian natively, does wrapping it in cpu_to_le32() cause a double-swap
on big-endian architectures, resulting in astronomically incorrect baud rates?
> ft260_dbg("configured termios: flow control: %d, baudrate: %d, ",
> req.flow_ctrl, baud);
[ ... ]
[Severity: High]
This is a pre-existing issue, but does the UART transmit path have a data
corruption vulnerability when used concurrently?
Looking at ft260_uart_transmit_chars(), the code writes directly to the shared
buffer without a lock:
rep = (struct ft260_uart_write_request_report *)port->uart_wr_buf;
...
rep->report = FT260_UART_DATA_REPORT_ID(len);
Since ft260_uart_transmit_chars() is called by the TTY write callback
(ft260_uart_write), and the TTY layer can invoke this concurrently from
multiple contexts, will concurrent executions overwrite each other's report
payloads before ft260_hid_output_report() sends them?
[Severity: Critical]
This isn't a bug introduced by this patch, but does the device lifecycle
management contain a double-free and use-after-free vulnerability?
In ft260_probe(), if ft260_uart_probe() fails, it triggers the cleanup:
ft260_probe()
...
ret = ft260_uart_probe(hdev, dev);
if (ret)
goto hid_fail;
...
hid_fail:
kfree(dev);
Inside the ft260_uart_probe() failure path or during ft260_remove(),
ft260_uart_port_remove(dev) drops the TTY port reference to 0, triggering
ft260_uart_port_destroy() which frees dev via kfree(port). When execution
reaches hid_fail, does kfree(dev) cause a double-free?
Similarly, in ft260_remove():
ft260_remove()
...
ft260_uart_port_remove(dev);
/* dev is still needed, so we will free it in _destroy func */
if (dev->chip_mode & FT260_MODE_UART || ...)
Does evaluating dev->chip_mode immediately after ft260_uart_port_remove()
dereference the already freed dev pointer?
[Severity: High]
This is a pre-existing issue, but is there a race condition with the
asynchronous worker cleanup during device removal?
Looking at ft260_remove():
ft260_remove()
...
cancel_work_sync(&dev->wakeup_work);
...
ft260_uart_port_remove(dev);
The cancel_work_sync() is called before timer_delete_sync() which is executed
downstream inside ft260_uart_port_remove().
If the wakeup_timer fires immediately after cancel_work_sync() completes,
could its callback ft260_uart_start_wakeup() re-schedule the worker? The
subsequent timer_delete_sync() would cancel the timer but not the newly queued
work, allowing the pending worker to access the device structure after it has
been freed.
[Severity: Critical]
This isn't a bug introduced by this patch, but does ft260_remove() tear
down driver data structures before stopping the HID transport?
In ft260_remove():
ft260_remove()
...
ft260_uart_port_remove(dev);
...
hid_hw_stop(hdev);
By freeing the dev structure before calling hid_hw_stop(hdev), the USB
interrupt endpoint remains active. If incoming HID reports arrive
concurrently, will the HID core dispatch these to ft260_raw_event()
which dereferences dev = hid_get_drvdata(hdev), accessing memory that
has already been freed?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260822213941.98882-1-michael.zaidman@gmail.com?part=7
^ permalink raw reply [flat|nested] 43+ messages in thread
* Re: [PATCH 09/13] HID: ft260: gpio: group sysfs attrs per HID interface
2026-08-22 21:39 ` [PATCH 09/13] HID: ft260: gpio: group sysfs attrs per HID interface Michael Zaidman
@ 2026-08-22 21:54 ` sashiko-bot
2026-08-25 8:13 ` Linus Walleij
1 sibling, 0 replies; 43+ messages in thread
From: sashiko-bot @ 2026-08-22 21:54 UTC (permalink / raw)
To: Michael Zaidman; +Cc: linux-input, dmitry.torokhov
Thank you for your contribution! Sashiko AI review found 7 potential issue(s) to consider:
New issues:
- [Medium] The `i2c_enable` sysfs attribute is unintentionally removed for dual-mode devices.
- [Medium] The `i2c_reset` sysfs attribute is incorrectly exposed on the UART interface group.
Pre-existing issues:
- [High] Premature free of `dev` in `ft260_remove()` leads to multiple Use-After-Free conditions.
- [High] Double free and Use-After-Free of the `dev` structure in the probe error paths.
- [High] Missing and incorrectly ordered teardown of timer and workqueue leads to Use-After-Free.
- [High] On device hot-unplug, the driver frees critical resources before the TTY port is completely hung up, allowing concurrent TTY operations to trigger Use-After-Free.
- [High] Race condition Use-After-Free due to manual `kfree(dev)` before `devres` cleans up the registered gpiochip.
--
--- Patch [9]: [PATCH 09/13] HID: ft260: gpio: group sysfs attrs per HID interface ---
commit c899807eb1ab1f737fe08c45424f6b6a6de53615
Author: Michael Zaidman <michael.zaidman@gmail.com>
HID: ft260: gpio: group sysfs attrs per HID interface
Split the single sysfs attribute group into per-interface groups based
on chip mode (DCNF0/DCNF1).
> diff --git a/drivers/hid/hid-ft260.c b/drivers/hid/hid-ft260.c
> index 554d8e3ddb6db..0a1875a1a7eca 100644
> --- a/drivers/hid/hid-ft260.c
> +++ b/drivers/hid/hid-ft260.c
[ ... ]
> @@ -1603,11 +1603,35 @@ static const struct attribute_group ft260_attr_group = {
> &dev_attr_gpio2_func.attr,
> &dev_attr_gpioa_func.attr,
> &dev_attr_gpiog_func.attr,
> + &dev_attr_clock_ctl.attr,
> + &dev_attr_i2c_reset.attr,
> + &dev_attr_clock.attr,
> + NULL
> + }
> +};
> +
> +static const struct attribute_group ft260_i2c_chip_mode_0_3_attr_group = {
> + .attrs = (struct attribute *[]) {
> + &dev_attr_hid_over_i2c_en.attr,
> + &dev_attr_i2c_reset.attr,
> + &dev_attr_clock.attr,
> + NULL
> + }
> +};
[Severity: Medium]
Is the omission of the dev_attr_i2c_enable.attr intentional for dual-mode
devices?
The commit message mentions splitting the groups per interface based on chip
mode, but it looks like the i2c_enable attribute was completely dropped from
the dual-mode I2C group here.
Could this silently break userspace applications relying on this sysfs entry?
> +
> +static const struct attribute_group ft260_uart_chip_mode_0_2_3_attr_group = {
> + .attrs = (struct attribute *[]) {
> + &dev_attr_chip_mode.attr,
> + &dev_attr_pwren_status.attr,
> + &dev_attr_suspend_status.attr,
> + &dev_attr_power_saving_en.attr,
> + &dev_attr_gpio2_func.attr,
> + &dev_attr_gpioa_func.attr,
> + &dev_attr_gpiog_func.attr,
> &dev_attr_uart_mode.attr,
> &dev_attr_uart_dcd_ri.attr,
> &dev_attr_clock_ctl.attr,
> &dev_attr_i2c_reset.attr,
[Severity: Medium]
Does this unintentionally expose the I2C reset attribute on the UART
interface?
According to the patch description, the UART interface should only contain
GPIO and UART attributes, but dev_attr_i2c_reset.attr is included here.
> - &dev_attr_clock.attr,
> NULL
> }
> };
[ ... ]
> @@ -2306,12 +2339,14 @@ static int ft260_uart_probe(struct ft260_device *dev,
> cfg->uart_mode = FT260_UART_CFG_FLOW_CTRL_NONE;
>
> if (dev->chip_mode & FT260_MODE_UART ||
> - dev->chip_mode == FT260_MODE_ALL) {
> + dev->chip_mode == FT260_MODE_ALL ||
> + dev->chip_mode == FT260_MODE_BOTH) {
> ret = ft260_gpio_init(dev, cfg);
> if (ret)
> goto err_hid_report;
>
> - ret = sysfs_create_group(&hdev->dev.kobj, &ft260_attr_group);
> + ret = sysfs_create_group(&hdev->dev.kobj,
> + &ft260_uart_chip_mode_0_2_3_attr_group);
> if (ret < 0) {
> hid_err(hdev, "failed to create sysfs attrs\n");
> goto err_hid_report;
[Severity: High]
This is a pre-existing issue, but can jumping to err_hid_report lead to a
double free or use-after-free of the dev structure?
If sysfs_create_group() fails, the error path goes to err_hid_report, which
calls ft260_uart_port_remove(dev). This drops the TTY port reference via
tty_port_put().
If the TTY is not open, the refcount drops to 0, and the port destructor
frees dev immediately. The function then returns the error code back to
ft260_probe(), which unconditionally jumps to hid_fail and executes
kfree(dev).
Also, does this error path bypass workqueue cancellation?
The error path here deletes the timer but appears to completely omit calling
cancel_work_sync(), which could trigger a use-after-free if the timer had
already scheduled the work before dev is freed.
Finally, does the manual kfree(dev) in ft260_probe() race with devres?
The ft260_gpio_init() function registers a gpiochip using
devm_gpiochip_add_data(). The devres cleanup won't run until after the probe
function fully returns, leaving a window where the active gpiochip can be
accessed by userspace while its private data (dev) has already been freed.
> }
[ ... ]
> @@ -2418,12 +2453,19 @@ static void ft260_remove(struct hid_device *hdev)
> ft260_uart_port_remove(dev);
> /* dev is still needed, so we will free it in _destroy func */
> if (dev->chip_mode & FT260_MODE_UART ||
> - dev->chip_mode == FT260_MODE_ALL)
> - sysfs_remove_group(&hdev->dev.kobj, &ft260_attr_group);
> + dev->chip_mode == FT260_MODE_ALL ||
> + dev->chip_mode == FT260_MODE_BOTH)
> + sysfs_remove_group(&hdev->dev.kobj,
> + &ft260_uart_chip_mode_0_2_3_attr_group);
[Severity: High]
This is a pre-existing issue, but does checking dev->chip_mode here risk
dereferencing a freed pointer?
When ft260_uart_port_remove(dev) is called just above this block, it drops
the TTY port reference. If the port isn't open, the port destructor runs
and immediately frees dev. Accessing dev->chip_mode and removing the sysfs
group afterward would then be a use-after-free.
Does the order of operations in ft260_remove() also leave the device
vulnerable to incoming USB events after it is freed?
Because hid_hw_stop() is called much later in this function, there is a
window where incoming HID reports can trigger ft260_raw_event() on a
freed dev.
Are there concurrent teardown risks during device hot-unplug?
Looking at ft260_uart_port_remove(), kfifo_free() is called before the
asynchronous tty_port_tty_hangup(). If userspace writes to the TTY during
this window, ft260_uart_write() could push data into the explicitly
freed xmit_fifo.
Could the workqueue also be incorrectly rescheduled during unbind?
Earlier in ft260_remove(), cancel_work_sync(&dev->wakeup_work) is called
before timer_delete_sync() (which happens inside ft260_uart_port_remove).
This allows the timer to expire after the work is canceled, rescheduling
the work to execute after dev is freed.
>
> } else {
> if (dev->chip_mode == FT260_MODE_I2C)
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260822213941.98882-1-michael.zaidman@gmail.com?part=9
^ permalink raw reply [flat|nested] 43+ messages in thread
* Re: [PATCH 08/13] HID: ft260: uart: add modem pins control via ioctl
2026-08-22 21:39 ` [PATCH 08/13] HID: ft260: uart: add modem pins control via ioctl Michael Zaidman
@ 2026-08-22 21:54 ` sashiko-bot
2026-08-25 8:08 ` Linus Walleij
1 sibling, 0 replies; 43+ messages in thread
From: sashiko-bot @ 2026-08-22 21:54 UTC (permalink / raw)
To: Michael Zaidman; +Cc: dmitry.torokhov, linux-input
Thank you for your contribution! Sashiko AI review found 7 potential issue(s) to consider:
New issues:
- [Critical] tiocmget and tiocmset operations trigger a Use-After-Free if called after the USB device is unbound due to delayed TTY hangup.
- [High] In ft260_probe(), if ft260_get_interface_type() fails, the negative error code is discarded, causing probe to return success.
- [Medium] ft260_uart_tiocmget() fails to check the return value of ft260_gpio_get_all(), leading to corrupted TIOCM state on error.
- [Medium] ft260_uart_tiocmset() copies port->gpio outside of port->lock, creating a race condition that leads to lost GPIO updates.
- [Medium] ft260_uart_tiocmset() and ft260_uart_tiocmget() silently ignore partial modem GPIO configurations and falsely return success.
Pre-existing issues:
- [Critical] Pre-existing memory safety bugs related to tty_port refcounting cause a double-free on probe failure and a use-after-free on device removal.
- [High] Pre-existing double byteswap of baudrate corrupts UART configuration on big-endian hosts.
--
--- Patch [8]: [PATCH 08/13] HID: ft260: uart: add modem pins control via ioctl ---
commit 847b0f46ee2404b8e4d020149c5d880eb06ffe61
Author: Michael Zaidman <michael.zaidman@gmail.com>
HID: ft260: uart: add modem pins control via ioctl
This commit adds TIOCMGET/TIOCMSET support so userspace can drive DTR/RTS
when the modem pins are in GPIO mode. It also modifies the GPIO sysfs
registration to rely on chip_mode instead of USB interface number.
> diff --git a/drivers/hid/hid-ft260.c b/drivers/hid/hid-ft260.c
> index 1da42b8756d8c..554d8e3ddb6db 100644
> --- a/drivers/hid/hid-ft260.c
> +++ b/drivers/hid/hid-ft260.c
[ ... ]
> @@ -1912,6 +1942,78 @@ static void ft260_uart_set_termios(struct tty_struct *tty,
> ft260_uart_change_speed(port, &tty->termios, NULL);
> }
>
> +static int ft260_uart_tiocmget(struct tty_struct *tty)
> +{
> + struct ft260_device *port = tty->driver_data;
> + int gpios;
> +
> + /*
> + * The modem lines control via ioctl is allowed only
> + * when the related pins are in the gpio mode.
> + */
> + int mask = FT260_GPIO_UART_MODE_4_SET;
> +
> + if ((port->gpio_en & mask) != mask) {
> + ft260_dbg("Illegal gpio config %#02x != mask %#04x\n",
> + port->gpio_en, mask);
> + return 0;
> + }
[Severity: Medium]
Does this silently ignore the partial configuration by returning 0
(success) instead of an error code like -EINVAL? If userspace enables
hardware flow control, it might try to toggle DTR/DSR manually, and
it looks like this will silently return success without taking action.
> +
> + gpios = ft260_gpio_get_all(port->gc, FT260_GPIO_VALUE);
[Severity: Critical]
Is there a use-after-free risk here if the USB device is unbound
but the TTY file descriptor is still open? It appears port->gc could be
freed by devres during removal, while the asynchronous TTY hangup might
not have completed yet.
> +
> + return (((gpios & FT260_GPIO_B) ? TIOCM_RTS : 0) |
[Severity: Medium]
If ft260_gpio_get_all() returns a negative error code (e.g., due to a
USB transfer error), will the error value be directly evaluated by the
bitwise operators here instead of being propagated back to userspace?
> + ((gpios & FT260_GPIO_E) ? TIOCM_CTS : 0) |
> + ((gpios & FT260_GPIO_F) ? TIOCM_DTR : 0) |
> + ((gpios & FT260_GPIO_H) ? TIOCM_DSR : 0) |
> + ((gpios & FT260_GPIO_4) ? TIOCM_CAR : 0) |
> + ((gpios & FT260_GPIO_5) ? TIOCM_RNG : 0));
> +}
> +
> +static int ft260_uart_tiocmset(struct tty_struct *tty,
> + unsigned int set, unsigned int clear)
> +{
> + int ret;
> + struct ft260_device *port = tty->driver_data;
> + struct ft260_gpio_write_request_report rep;
> + struct hid_device *hdev = port->hdev;
> + int mask = FT260_GPIO_UART_MODE_4_SET;
> +
> + if ((port->gpio_en & mask) != mask) {
> + ft260_dbg("Illegal gpio config %#02x != mask %#04x\n",
> + port->gpio_en, mask);
> + return 0;
> + }
> + rep.report = FT260_GPIO;
> + rep.gpio = port->gpio;
> +
> + mutex_lock(&port->lock);
[Severity: Medium]
Is there a race condition with concurrent GPIO updates here? Since
port->gpio is copied into rep.gpio before acquiring port->lock, could
concurrent updates to the GPIO lines via sysfs or other ioctls be silently
overwritten when this stale copy is written back at the end of the function?
> +
> + if (set & TIOCM_RTS)
> + ft260_gpio_output_cfg(&rep.gpio, FT260_GPIO_UART_RTS, 1);
> + if (set & TIOCM_DTR)
> + ft260_gpio_output_cfg(&rep.gpio, FT260_GPIO_UART_DTR, 1);
> + if (clear & TIOCM_RTS)
> + ft260_gpio_output_cfg(&rep.gpio, FT260_GPIO_UART_RTS, 0);
> + if (clear & TIOCM_DTR)
> + ft260_gpio_output_cfg(&rep.gpio, FT260_GPIO_UART_DTR, 0);
> +
> + ft260_dbg("dirs %#02x vals %#02x ex_dirs %#02x ex_vals %#02x\n",
> + rep.gpio.dirs, rep.gpio.vals,
> + rep.gpio.ex_dirs, rep.gpio.ex_vals);
> +
> + ret = ft260_hid_feature_report_set(hdev, (u8 *)&rep, sizeof(rep));
[Severity: Critical]
Can this access a freed hdev if the USB device has been unplugged?
If the asynchronous TTY hangup work hasn't completed, a delayed
ioctl could potentially pass a destroyed hid_device pointer to
ft260_hid_feature_report_set().
> + if (unlikely(ret < 0)) {
> + hid_err(hdev, "%s: cannot set GPIO: %d\n", __func__, ret);
> + mutex_unlock(&port->lock);
> + return -EIO;
> + }
> +
> + port->gpio = rep.gpio;
> + mutex_unlock(&port->lock);
> +
> + return 0;
> +}
[ ... ]
> @@ -2199,7 +2305,8 @@ static int ft260_uart_probe(struct ft260_device *dev,
>
> cfg->uart_mode = FT260_UART_CFG_FLOW_CTRL_NONE;
>
> - if (dev->iface_id == 0) {
> + if (dev->chip_mode & FT260_MODE_UART ||
> + dev->chip_mode == FT260_MODE_ALL) {
> ret = ft260_gpio_init(dev, cfg);
[Severity: High]
This is a pre-existing issue, but in ft260_uart_change_speed() and earlier
in ft260_uart_probe(), put_unaligned_le32(cpu_to_le32(baud), ...) is used.
Since put_unaligned_le32() already performs a byte swap to little-endian,
does passing cpu_to_le32() result in a double byteswap that breaks UART
communications on big-endian hosts?
[ ... ]
> @@ -2274,11 +2381,11 @@ static int ft260_probe(struct hid_device *hdev, const struct hid_device_id *id)
> spin_lock_init(&dev->read_lock);
> init_completion(&dev->wait);
>
> - ret = ft260_get_interface_type(dev, &cfg);
> - if (ret <= FT260_IFACE_NONE)
> + dev->iface_type = ft260_get_interface_type(dev, &cfg);
> + if (dev->iface_type <= FT260_IFACE_NONE)
> goto err_hid_close;
[Severity: High]
Does this discard the negative error code by failing to update ret?
If ft260_get_interface_type() fails (for instance, during a USB control
transfer error), jumping to err_hid_close will cause the probe function
to return the previous positive value of ret, incorrectly signaling success
to the driver core.
[ ... ]
> @@ -2310,11 +2417,13 @@ static void ft260_remove(struct hid_device *hdev)
> dev->index);
> ft260_uart_port_remove(dev);
> /* dev is still needed, so we will free it in _destroy func */
> - if (dev->iface_id == 0)
> + if (dev->chip_mode & FT260_MODE_UART ||
> + dev->chip_mode == FT260_MODE_ALL)
[Severity: Critical]
This is a pre-existing issue, but does ft260_uart_port_remove() drop
the final tty kref and free dev? If so, accessing dev->chip_mode right
afterward would be a use-after-free during normal unplug. Furthermore,
in the ft260_probe() error path, does this same kref drop lead to a
double-free when the outer error handling calls kfree(dev)?
> sysfs_remove_group(&hdev->dev.kobj, &ft260_attr_group);
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260822213941.98882-1-michael.zaidman@gmail.com?part=8
^ permalink raw reply [flat|nested] 43+ messages in thread
* Re: [PATCH 03/13] HID: ft260: add GPIO support on top of UART
2026-08-22 21:39 ` [PATCH 03/13] HID: ft260: add GPIO support on top of UART Michael Zaidman
@ 2026-08-22 21:56 ` sashiko-bot
2026-08-25 7:44 ` Linus Walleij
1 sibling, 0 replies; 43+ messages in thread
From: sashiko-bot @ 2026-08-22 21:56 UTC (permalink / raw)
To: Michael Zaidman; +Cc: linux-input, dmitry.torokhov
Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
- [High] Uninitialized GPIO shadow state (`dev->gpio`) corrupts the hardware configuration of unrelated pins during updates.
- [High] Unvalidated `cfg->uart_mode` read from the device is used as an array index, leading to an out-of-bounds read.
- [Medium] Setting a GPIO direction to output introduces a hardware glitch by splitting the operation into two non-atomic USB requests.
- [Medium] Setting the UART mode to OFF unintentionally disables all non-UART GPIOs.
--
--- Patch [3]: [PATCH 03/13] HID: ft260: add GPIO support on top of UART ---
commit d5e8f3410c2633ba90471af57e3193f95d3193d4
Author: Michael Zaidman <michael.zaidman@gmail.com>
HID: ft260: add GPIO support on top of UART
Add gpiochip support for the FT260 multifunctional pins, including
GPIO2/GPIOA/GPIOG function selection via sysfs, and enable the
available GPIOs according to the active UART mode.
Link: https://lore.kernel.org/lkml/20230211115752.26276-2-michael.zaidman@gmail.com/
> diff --git a/drivers/hid/hid-ft260.c b/drivers/hid/hid-ft260.c
> index b6c408267a685..cc3b8f5791dea 100644
> --- a/drivers/hid/hid-ft260.c
> +++ b/drivers/hid/hid-ft260.c
[ ... ]
> @@ -906,6 +1023,322 @@ static const struct i2c_algorithm ft260_i2c_algo = {
> .functionality = ft260_functionality,
> };
>
> +static void ft260_gpio_en_set(struct ft260_device *dev, u16 bitmap)
> +{
> + dev->gpio_en |= bitmap & FT260_GPIO_MASK;
> +}
> +
> +static void ft260_gpio_en_clr(struct ft260_device *dev, u16 bitmap)
> +{
> + dev->gpio_en &= ~bitmap & FT260_GPIO_MASK;
> +}
> +
> +static void ft260_gpio_en_update(struct hid_device *hdev, u8 req, u8 value)
> +{
> + u16 bitmap;
> + struct ft260_device *dev = hid_get_drvdata(hdev);
> +
> + switch (req) {
> +
> + case FT260_SET_I2C_MODE:
> + bitmap = FT260_GPIO_I2C_DEFAULT;
> + break;
> + case FT260_SET_UART_MODE:
> + switch (value) {
> + case FT260_UART_CFG_FLOW_CTRL_OFF:
> + bitmap = (u16)FT260_GPIO_MASK;
> + break;
[ ... ]
> + }
> + ft260_gpio_en_clr(dev, bitmap);
> + bitmap = dev->gpio_uart_mode[value];
> + ft260_gpio_en_set(dev, bitmap);
> + goto exit;
[Severity: Medium]
Does setting the UART flow control to OFF unintentionally disable all non-UART
GPIOs?
When value is FT260_UART_CFG_FLOW_CTRL_OFF (0), bitmap is set to
FT260_GPIO_MASK, which represents all 14 GPIO bits. Calling
ft260_gpio_en_clr() inadvertently clears all of these bits from dev->gpio_en.
Subsequently, only the FT260_GPIO_UART_MODE_0_SET bits are restored, which
permanently disables I2C and other multi-function GPIOs until they are
manually re-enabled.
[ ... ]
> +static int ft260_gpio_set(struct gpio_chip *gc, u32 offset, int value)
> +{
> + int ret = 0;
> + struct ft260_gpio_write_request_report rep;
> + struct ft260_device *dev = gpiochip_get_data(gc);
> + struct hid_device *hdev = dev->hdev;
> +
> + if (offset >= FT260_GPIO_TOTAL) {
> + hid_err(hdev, "%s: invalid offset %d\n", __func__, offset);
> + return -EINVAL;
> + }
> +
> + ft260_dbg("offset %d val %d\n", offset, value);
> +
> + mutex_lock(&dev->lock);
> +
> + if (!(dev->gpio_en & (1 << offset))) {
> + hid_err(hdev, "%s: wrong pin function %d\n", __func__, offset);
> + ret = -EINVAL;
> + goto exit;
> + }
> +
> + rep.report = FT260_GPIO;
> + rep.gpio = dev->gpio;
> +
> + if (offset < FT260_GPIO_MAX) {
[ ... ]
> + ret = ft260_hid_feature_report_set(hdev, (u8 *)&rep, sizeof(rep));
[Severity: High]
Does this corrupt the hardware configuration of unrelated pins?
The shadow state dev->gpio is zero-initialized via kzalloc during device
allocation and is never synced with the actual hardware state during
ft260_gpio_init(). Because the entire dev->gpio struct is written to the
device here, won't all unmodified pins be unintentionally configured to input
mode with value 0?
> + if (unlikely(ret < 0)) {
> + hid_err(hdev, "%s: cannot set GPIO: %d\n", __func__, ret);
> + goto exit;
> + }
> +
> + dev->gpio = rep.gpio;
> + ret = 0;
> +exit:
> + mutex_unlock(&dev->lock);
> + return ret;
> +}
> +
> +static int ft260_gpio_direction_set(struct gpio_chip *gc, u32 offset,
> + int value, int direction)
> +{
> + int ret;
> + struct ft260_gpio_read_request_report buf;
> + struct ft260_gpio_write_request_report *rep;
> + struct ft260_device *dev = gpiochip_get_data(gc);
> + struct hid_device *hdev = dev->hdev;
> +
[ ... ]
> + ret = ft260_hid_feature_report_set(hdev, (u8 *)rep, sizeof(*rep));
> + if (unlikely(ret < 0)) {
> + hid_err(hdev, "%s: cannot set GPIO: %d\n", __func__, ret);
> + goto exit;
> + }
> +
> + dev->gpio = rep->gpio;
> + mutex_unlock(&dev->lock);
> +
> + if (direction == FT260_GPIO_DIR_OUTPUT)
> + ft260_gpio_set(gc, offset, value);
[Severity: Medium]
Could this introduce a hardware glitch when changing a pin's direction to
output?
The code updates the direction bits to output without setting the corresponding
value bits, and writes this immediately to the device via
ft260_hid_feature_report_set(). Then it sends a second USB request via
ft260_gpio_set() to set the desired output value. During the window between
these two USB transfers, will the pin assert an unintended logic level?
> +
> + return 0;
> +exit:
> + mutex_unlock(&dev->lock);
> + return ret;
> +}
[ ... ]
> +static int ft260_gpio_init(struct ft260_device *dev,
> + struct ft260_get_system_status_report *cfg)
> +{
> + int ret;
> + int label_sz;
> + char *label;
> + struct ft260_get_chip_version_report ver;
> + struct hid_device *hdev = dev->hdev;
> + char prefix[] = "ft260_";
> + u8 mode = cfg->chip_mode;
> +
> + hid_info(hdev, "initialize gpio chip\n");
> +
> + dev->gpio_uart_mode[0] = (u16)FT260_GPIO_UART_MODE_0_SET;
> + dev->gpio_uart_mode[1] = (u16)FT260_GPIO_UART_MODE_1_SET;
> + dev->gpio_uart_mode[2] = (u16)FT260_GPIO_UART_MODE_2_SET;
> + dev->gpio_uart_mode[3] = (u16)FT260_GPIO_UART_MODE_3_SET;
> + dev->gpio_uart_mode[4] = (u16)FT260_GPIO_UART_MODE_4_SET;
> +
> + if (mode == FT260_MODE_ALL)
> + mode = FT260_MODE_BOTH;
> +
> + if (mode & FT260_MODE_UART)
> + dev->gpio_en |= dev->gpio_uart_mode[cfg->uart_mode];
[Severity: High]
Can this read out of bounds?
cfg->uart_mode is read directly from the device via a USB feature report in
ft260_get_system_config(). If a malicious or malfunctioning USB device returns
a uart_mode >= 5, won't this leak kernel heap data into dev->gpio_en or crash
since dev->gpio_uart_mode only has 5 elements?
> + else
> + dev->gpio_en |= FT260_GPIO_UART_DEFAULT;
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260822213941.98882-1-michael.zaidman@gmail.com?part=3
^ permalink raw reply [flat|nested] 43+ messages in thread
* Re: [PATCH 02/13] HID: ft260: uart: bring-up fixes
2026-08-22 21:39 ` [PATCH 02/13] HID: ft260: uart: bring-up fixes Michael Zaidman
@ 2026-08-22 21:56 ` sashiko-bot
0 siblings, 0 replies; 43+ messages in thread
From: sashiko-bot @ 2026-08-22 21:56 UTC (permalink / raw)
To: Michael Zaidman; +Cc: dmitry.torokhov, linux-input
Thank you for your contribution! Sashiko AI review found 8 potential issue(s) to consider:
New issues:
- [High] Double free and Use-After-Free of `ft260_device` in the `ft260_uart_probe` error paths.
- [High] Missing synchronous timer cancellation for the wakeup workaround leads to a Use-After-Free in `ft260_remove()`.
- [Medium] The hoisted HID report length check in `ft260_raw_event` incorrectly filters `FT260_UART_INTERRUPT_STATUS` reports, leading to kernel log spam.
- [Medium] The power-saving workaround timer is permanently stopped if the baud rate transitions from <= 4800 to > 4800.
Pre-existing issues:
- [Critical] Incorrect teardown ordering in `ft260_remove()` frees `dev` before calling `hid_hw_stop()`, leading to a Use-After-Free if a HID event arrives.
- [High] Premature `kfifo_free()` leads to a NULL pointer dereference on concurrent TTY writes.
- [High] Lockless use of the shared `port->uart_wr_buf` leads to data corruption on concurrent TTY writes.
- [High] The UART write path incorrectly returns 0 on partial transmission failures, causing the TTY layer to duplicate previously sent data.
--
--- Patch [2]: [PATCH 02/13] HID: ft260: uart: bring-up fixes ---
commit b2841c4c6f0b518f0cd8b8780996e8cb92c70332
Author: Michael Zaidman <michael.zaidman@gmail.com>
HID: ft260: uart: bring-up fixes
Fix the UART path so the serial interface added previously works
reliably in practice. This squashes the contiguous UART bring-up
fixes that followed the initial serial-driver addition.
Link: https://lore.kernel.org/all/20240210215147.77629-1-michael.zaidman@gmail.com/
> diff --git a/drivers/hid/hid-ft260.c b/drivers/hid/hid-ft260.c
> index 3faedfc5c4136..b6c408267a685 100644
> --- a/drivers/hid/hid-ft260.c
> +++ b/drivers/hid/hid-ft260.c
[ ... ]
> @@ -1177,10 +1177,9 @@ static void ft260_uart_port_remove(struct ft260_device *port)
> list_del(&port->device_list);
> mutex_unlock(&ft260_uart_list_lock);
>
> - spin_lock(&port->write_lock);
> + spin_lock(&port->xmit_fifo_lock);
> kfifo_free(&port->xmit_fifo);
> - spin_unlock(&port->write_lock);
> + spin_unlock(&port->xmit_fifo_lock);
>
> mutex_lock(&port->port.mutex);
> - port->reschedule_work = false;
> tty_port_tty_hangup(&port->port, false);
> mutex_unlock(&port->port.mutex);
[Severity: High]
This is a pre-existing issue, but can this lead to a NULL pointer dereference
on concurrent terminal writes?
In ft260_uart_port_remove(), kfifo_free() is executed while the port is
still accessible. The tty_port_tty_hangup() executes asynchronously.
If a concurrent userspace writer is active during this window,
ft260_uart_write() will acquire the spinlock and call kfifo_in_spinlocked().
Since kfifo_free() sets the internal buffer to NULL but leaves the mask
logic at 0, kfifo_avail() will calculate the available space as 1 byte,
causing a memcpy of 1 byte into the NULL buffer.
[ ... ]
> @@ -1244,14 +1244,14 @@ static int ft260_uart_transmit_chars(struct ft260_device *port)
> goto tty_out;
> }
>
> - rep = (struct ft260_uart_write_request_report *)port->write_buf;
> + rep = (struct ft260_uart_write_request_report *)port->uart_wr_buf;
>
> do {
> - len = min(data_len, FT260_WR_DATA_MAX);
> + len = min(data_len, FT260_WR_UART_DATA_MAX);
>
> rep->report = FT260_UART_DATA_REPORT_ID(len);
> rep->length = len;
>
> - len = kfifo_out_locked(xmit, rep->data, len, &port->write_lock);
> + len = kfifo_out_spinlocked(xmit, rep->data, len, &port->xmit_fifo_lock);
>
> - ret = ft260_hid_output_report(hdev, (u8 *)rep, len + sizeof(*rep));
> - if (ret < 0) {
> - hid_err(hdev, "Failed to start transfer, ret %d\n", ret);
> + ret = ft260_hid_output_report(hdev, (u8 *)rep, len + 2);
> + if (ret < 0)
[Severity: High]
This is a pre-existing issue, but does this lockless use of the shared
uart_wr_buf lead to data corruption?
ft260_uart_transmit_chars() writes the report ID, length, and payload
directly into the shared port->uart_wr_buf without holding any driver-level
lock.
Since ft260_uart_write() can be called concurrently by multiple threads,
they could simultaneously overwrite port->uart_wr_buf, interleaving their
payloads before ft260_hid_output_report() is executed.
[ ... ]
> @@ -1286,26 +1286,26 @@ static ssize_t ft260_uart_write(struct tty_struct *tty, const u8 *buf,
> size_t cnt)
> {
> struct ft260_device *port = tty->driver_data;
> - struct hid_device *hdev = port->hdev;
> - int len, ret;
> + ssize_t len, ret, diff;
>
> - len = kfifo_in_locked(&port->xmit_fifo, buf, count, &port->write_lock);
> - ft260_dbg("count: %zu, len: %d", count, len);
> + len = kfifo_in_spinlocked(&port->xmit_fifo, buf, cnt,
> + &port->xmit_fifo_lock);
> + ft260_dbg("count: %zu, len: %zd", cnt, len);
>
> ret = ft260_uart_transmit_chars(port);
> if (ret < 0) {
> - hid_dbg(hdev, "Failed to transmit chars: %d\n", ret);
> + ft260_dbg("failed to transmit %zd\n", ret);
> return 0;
> }
[Severity: High]
This is a pre-existing issue, but does returning 0 here cause the terminal
layer to duplicate previously sent data?
If ft260_hid_output_report() fails halfway through a transmission,
ft260_uart_transmit_chars() returns a negative error code. This function
catches it and returns 0, which the terminal layer interprets as 0 bytes
accepted and retries writing the entire original buffer.
Because prior chunks were already extracted from the FIFO and successfully
transmitted, retrying the entire buffer results in those successful chunks
being added to the FIFO again, duplicating data on the wire.
[ ... ]
> @@ -1371,11 +1371,17 @@ static int ft260_uart_change_speed(struct ft260_device *port,
> if (baud == 0 || baud < FT260_CFG_BAUD_MIN || baud > FT260_CFG_BAUD_MAX) {
> struct tty_struct *tty = tty_port_tty_get(&port->port);
>
> - hid_err(hdev, "Invalid baud rate %d\n", baud);
> + hid_err(hdev, "invalid baud rate %d\n", baud);
> baud = 9600;
> tty_encode_baud_rate(tty, baud, baud);
> tty_kref_put(tty);
> }
> +
> + if (baud > FT260_UART_EN_PW_SAVE_BAUD)
> + wakeup_workaraund = true;
> +
> + ft260_uart_wakeup_workaraund_enable(port, wakeup_workaraund);
> +
> put_unaligned_le32(cpu_to_le32(baud), &req.baudrate);
[Severity: Medium]
Does this permanently stop the power-saving workaround timer if the baud rate
transitions from 4800 or below to a higher rate?
When the baud rate is lowered, ft260_uart_change_speed() sets
reschedule_work to false, and the timer naturally stops when it next fires.
If the baud rate is later raised above 4800, this correctly sets
reschedule_work to true, but lacks a call to mod_timer() to restart the
stopped timer.
[ ... ]
> @@ -1726,15 +1768,12 @@ static int ft260_probe(struct hid_device *hdev, const struct hid_device_id *id)
> spin_lock_init(&dev->read_lock);
> init_completion(&dev->wait);
>
> - if (!dev->ft260_is_serial) {
> + if (ret == FT260_IFACE_I2C)
> ret = ft260_i2c_probe(hdev, dev);
> - if (ret)
> - goto err_hid_close;
> - } else {
> + else
> ret = ft260_uart_probe(hdev, dev);
> - if (ret)
> - goto err_hid_close;
> - }
> + if (ret)
> + goto err_hid_close;
[Severity: High]
Can this error path trigger a double free and use-after-free of the
ft260_device structure?
If ft260_uart_probe() fails (e.g., during ft260_hid_feature_report_set),
it jumps to err_register_tty and calls ft260_uart_port_remove(dev). This
function drops the port refcount by calling ft260_uart_port_put(dev).
The error path then falls through to err_port_put and calls
ft260_uart_port_put(dev) a second time, underflowing the refcount and
freeing dev.
Finally, the error propagates back to ft260_probe(), which unconditionally
jumps to err_hid_close and calls kfree(dev) on the already-freed structure.
[ ... ]
> @@ -1755,13 +1794,12 @@ static void ft260_remove(struct hid_device *hdev)
> if (!dev)
> return;
>
> - if (dev->ft260_is_serial) {
> - // FIXME:
> + if (dev->iface_type == FT260_IFACE_UART) {
> cancel_work_sync(&dev->wakeup_work);
[Severity: High]
Does this teardown path omit cancelling the wakeup timer?
The driver arms dev->wakeup_timer but fails to synchronously stop it.
Here, cancel_work_sync(&dev->wakeup_work) is called, but
timer_delete_sync(&dev->wakeup_timer) is missing.
The port shutdown gracefully sets reschedule_work to false but doesn't
cancel the pending timer. If the timer fires in the window after the port
is destroyed (and dev is freed), the timer handler will dereference
freed memory.
> tty_port_unregister_device(&dev->port, ft260_tty_driver,
> dev->index);
> ft260_uart_port_remove(dev);
[Severity: Critical]
This is a pre-existing issue, but does this teardown ordering free the
device structure while the hardware is still active?
ft260_uart_port_remove(dev) is called to tear down the port. If the port
is not open, this drops the final reference and immediately frees dev.
However, hid_hw_stop(hdev) is not called until after this teardown.
If a device interrupt arrives during this window, the core invokes
ft260_raw_event(), which fetches the now-freed dev and passes it to
ft260_uart_receive_chars(), causing a use-after-free.
> - /* dev still needed, so we will free it in _destroy func */
> + /* dev is still needed, so we will free it in _destroy func */
> } else {
> sysfs_remove_group(&hdev->dev.kobj, &ft260_attr_group);
> i2c_del_adapter(&dev->adap);
[ ... ]
> @@ -1784,6 +1822,12 @@ static int ft260_raw_event(struct hid_device *hdev, struct hid_report *report,
> return -1;
> }
>
> + if (xfer->length > size - offsetof(struct ft260_input_report, data)) {
> + hid_err(hdev, "report %#02x: length %d exceeds HID report size\n",
> + xfer->report, xfer->length);
> + return -1;
> + }
> +
[Severity: Medium]
Does this hoisted boundary check misinterpret the status byte of the UART
interrupt report, causing valid reports to be dropped?
For FT260_UART_INTERRUPT_STATUS (0xB1) reports, the byte at offset 1 is the
actual interrupt status bitmap, not a length.
If an unpadded 2-byte report is received, size is 2, making size minus offset
equal 0. If any status bit is set (status > 0), the check incorrectly
triggers, rejects the report, and spams the kernel log with hid_err
messages whenever the UART receives data.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260822213941.98882-1-michael.zaidman@gmail.com?part=2
^ permalink raw reply [flat|nested] 43+ messages in thread
* Re: [PATCH 01/13] HID: ft260: add serial driver
2026-08-22 21:39 ` [PATCH 01/13] HID: ft260: add serial driver Michael Zaidman
@ 2026-08-22 22:00 ` sashiko-bot
2026-08-25 7:49 ` Linus Walleij
2026-08-25 8:12 ` Linus Walleij
2 siblings, 0 replies; 43+ messages in thread
From: sashiko-bot @ 2026-08-22 22:00 UTC (permalink / raw)
To: Michael Zaidman; +Cc: linux-input, dmitry.torokhov
Thank you for your contribution! Sashiko AI review found 9 potential issue(s) to consider:
- [Critical] Use-After-Free of `hdev` and `xmit_fifo` if TTY operations occur after device removal.
- [High] Double free of the `dev` structure in the `ft260_probe()` error path.
- [High] Incorrect teardown ordering of the wakeup timer and workqueue creates a Use-After-Free race condition.
- [High] Device structure is freed before stopping the HID hardware, causing a Use-After-Free in the input report handler.
- [High] Opening an unregistered or disconnected FT260 TTY node crashes the kernel due to a missing NULL check.
- [Medium] Flawed TTY port index allocator logic generates duplicate indices, leading to sysfs collisions.
- [Medium] TTY data duplication due to incorrect return value in `ft260_uart_write()`.
- [Medium] Crashing dereference of `tty` in `ft260_uart_change_speed()` if the TTY is simultaneously closed.
- [Medium] Baud rate configuration is double-swapped on big-endian architectures, resulting in incorrect hardware configuration.
--
--- Patch [1]: [PATCH 01/13] HID: ft260: add serial driver ---
commit 2a24dbd449641441b22e899b0125b7e38412d4d3
Author: Christina Quast <contact@christina-quast.de>
HID: ft260: add serial driver
Add a TTY serial interface (/dev/ttyFTx) for FT260 USB HID devices,
providing baud rate configuration, data transfer, and termios settings
without requiring userspace HID report formatting.
Ported from: https://lore.kernel.org/all/20231218093153.192268-1-contact@christina-quast.de/
The follow-up v5 series stalled without being applied: https://lore.kernel.org/linux-input/20240216-ft260_review5-v5-1-36db44673ac7@christina-quast.de/
> diff --git a/drivers/hid/hid-ft260.c b/drivers/hid/hid-ft260.c
> index 64c9c940db46e..3faedfc5c4136 100644
> --- a/drivers/hid/hid-ft260.c
> +++ b/drivers/hid/hid-ft260.c
> @@ -1001,6 +1090,583 @@ static const struct attribute_group ft260_attr_group = {
[ ... ]
> +static int ft260_uart_add_port(struct ft260_device *port)
> +{
> + int index = 0, ret = 0;
> + struct ft260_device *dev;
> +
> + spin_lock_init(&port->write_lock);
> + if (kfifo_alloc(&port->xmit_fifo, FIFO_SIZE, GFP_KERNEL))
> + return -ENOMEM;
> +
> + mutex_lock(&ft260_uart_list_lock);
> + list_for_each_entry(dev, &ft260_uart_device_list, device_list) {
> + if (dev->index != index)
> + break;
> + index++;
> + }
> +
> + port->index = index;
> + list_add(&port->device_list, &ft260_uart_device_list);
> + mutex_unlock(&ft260_uart_list_lock);
[Severity: Medium]
Will this allocate duplicate indices when multiple devices are plugged in?
The iteration expects the list to be in ascending order to find a hole, but
list_add() prepends devices to the head, making the list descending. When
plugging in a third device, the loop will encounter index 1 first, mismatch
with index = 0, break early, and assign index 0 again, causing a sysfs
name collision.
[ ... ]
> +static void ft260_uart_port_remove(struct ft260_device *port)
> +{
> + timer_delete_sync(&port->wakeup_timer);
> +
> + mutex_lock(&ft260_uart_list_lock);
> + list_del(&port->device_list);
> + mutex_unlock(&ft260_uart_list_lock);
> +
> + spin_lock(&port->write_lock);
> + kfifo_free(&port->xmit_fifo);
> + spin_unlock(&port->write_lock);
> +
> + mutex_lock(&port->port.mutex);
> + port->reschedule_work = false;
> + tty_port_tty_hangup(&port->port, false);
> + mutex_unlock(&port->port.mutex);
> +
> + ft260_uart_port_put(port);
> +}
[Severity: Critical]
Can a use-after-free occur if userspace continues writing to the TTY after
device removal?
ft260_uart_port_remove() is called during teardown and synchronously frees
xmit_fifo. However, if userspace holds the TTY file open, the dev structure
remains alive. Subsequent writes to the TTY will call ft260_uart_write(),
which writes to the freed xmit_fifo and calls ft260_hid_output_report() using
the freed port->hdev.
[ ... ]
> +static int ft260_uart_open(struct tty_struct *tty, struct file *filp)
> +{
> + int ret;
> + struct ft260_device *port = tty->driver_data;
> +
> + ret = tty_port_open(&port->port, tty, filp);
[Severity: High]
If port was NULL during ft260_uart_install(), won't this dereference NULL when
evaluating &port->port?
[ ... ]
> +static ssize_t ft260_uart_write(struct tty_struct *tty, const u8 *buf,
> + size_t count)
> +{
> + struct ft260_device *port = tty->driver_data;
> + struct hid_device *hdev = port->hdev;
> + int len, ret;
> +
> + len = kfifo_in_locked(&port->xmit_fifo, buf, count, &port->write_lock);
> + ft260_dbg("count: %zu, len: %d", count, len);
> +
> + ret = ft260_uart_transmit_chars(port);
> + if (ret < 0) {
> + hid_dbg(hdev, "Failed to transmit chars: %d\n", ret);
> + return 0;
> + }
> +
> + ret = kfifo_len(&port->xmit_fifo);
> + if (ret > 0) {
> + hid_dbg(hdev, "Failed to all kfifo data bytes\n");
> + ft260_dbg("return: %d", len - ret);
> + return len - ret;
> + }
> +
> + return len;
> +}
[Severity: Medium]
Does this logic silently duplicate data in the byte stream?
If kfifo_in_locked accepts all bytes, but ft260_uart_transmit_chars cannot
transmit them all immediately (leaving some in the FIFO), this function
returns len - kfifo_len(). The TTY layer assumes the difference was rejected
and will re-submit those bytes on the next call, appending them to the FIFO
again.
[ ... ]
> +static int ft260_uart_change_speed(struct ft260_device *port,
> + struct ktermios *termios,
> + struct ktermios *old)
> +{
> + struct hid_device *hdev = port->hdev;
> + unsigned int baud;
> + struct ft260_configure_uart_request req;
> + int ret;
[ ... ]
> + baud = tty_termios_baud_rate(termios);
> + if (baud == 0 || baud < FT260_CFG_BAUD_MIN || baud > FT260_CFG_BAUD_MAX) {
> + struct tty_struct *tty = tty_port_tty_get(&port->port);
> +
> + hid_err(hdev, "Invalid baud rate %d\n", baud);
> + baud = 9600;
> + tty_encode_baud_rate(tty, baud, baud);
> + tty_kref_put(tty);
> + }
[Severity: Medium]
Can tty_port_tty_get(&port->port) return NULL if the TTY is simultaneously
closed?
If it does, passing NULL to tty_encode_baud_rate() will unconditionally
dereference it and crash the kernel.
> + put_unaligned_le32(cpu_to_le32(baud), &req.baudrate);
[Severity: Medium]
Is the baud rate being double-swapped on big-endian architectures?
The put_unaligned_le32() macro implicitly handles CPU-to-LE32 conversion
internally. Calling cpu_to_le32(baud) first will swap the bytes on
big-endian platforms, and then put_unaligned_le32() will swap them a second
time, writing the baud rate to the hardware in big-endian format.
[ ... ]
> +static int ft260_uart_install(struct tty_driver *driver, struct tty_struct *tty)
> +{
> + int idx = tty->index;
> + struct ft260_device *port = ft260_uart_port_get(idx);
> + int ret = tty_standard_install(driver, tty);
> +
> + if (ret == 0)
> + /* This is the ref ft260_uart_port get provided */
> + tty->driver_data = port;
[Severity: High]
Can this assign a NULL pointer if the device is disconnected before being
opened?
If the port was already removed, ft260_uart_port_get(idx) returns NULL.
This code does not check if port is NULL before assigning it to
tty->driver_data, which allows the open sequence to continue with a NULL
device context.
[ ... ]
> +static int ft260_uart_probe(struct hid_device *hdev, struct ft260_device *dev)
> +{
> + struct ft260_configure_uart_request req;
> + int ret;
> + struct device *devt;
[ ... ]
> + /* Send Feature Report to Configure FT260 as UART 9600-8-N-1 */
> + req.report = FT260_SYSTEM_SETTINGS;
> + req.request = FT260_SET_UART_CONFIG;
> + req.flow_ctrl = FT260_CFG_FLOW_CTRL_NONE;
> + put_unaligned_le32(cpu_to_le32(9600), &req.baudrate);
[Severity: Medium]
Is this also double-swapped on big-endian architectures?
[ ... ]
> @@ -1010,20 +1676,26 @@ static int ft260_probe(struct hid_device *hdev, const struct hid_device_id *id)
[ ... ]
> -err_i2c_free:
> - i2c_del_adapter(&dev->adap);
> err_hid_close:
> hid_hw_close(hdev);
> err_hid_stop:
> hid_hw_stop(hdev);
> +hid_fail:
> + kfree(dev);
> +alloc_fail:
> return ret;
> }
[Severity: High]
Is it possible to double free the dev structure here?
If ft260_uart_probe() fails, it calls ft260_uart_port_remove(), which
drops the TTY port's reference count to 0, triggering
ft260_uart_port_destroy() and freeing dev.
The error then propagates back to ft260_probe(), which jumps to
err_hid_close and then unconditionally executes kfree(dev) at the
hid_fail label.
> @@ -1100,8 +1755,18 @@ static void ft260_remove(struct hid_device *hdev)
> if (!dev)
> return;
>
> - sysfs_remove_group(&hdev->dev.kobj, &ft260_attr_group);
> - i2c_del_adapter(&dev->adap);
> + if (dev->ft260_is_serial) {
> + // FIXME:
> + cancel_work_sync(&dev->wakeup_work);
> + tty_port_unregister_device(&dev->port, ft260_tty_driver,
> + dev->index);
> + ft260_uart_port_remove(dev);
> + /* dev still needed, so we will free it in _destroy func */
[Severity: High]
Is there a use-after-free race condition during device unplug?
If the timer fires after cancel_work_sync(&dev->wakeup_work) but before
timer_delete_sync(&port->wakeup_timer) runs in ft260_uart_port_remove(),
the timer callback ft260_uart_start_wakeup() can observe
dev->reschedule_work == true and dynamically re-queue the work.
The work would then remain queued after the device is destroyed.
> + } else {
> + sysfs_remove_group(&hdev->dev.kobj, &ft260_attr_group);
> + i2c_del_adapter(&dev->adap);
> + kfree(dev);
> + }
>
> hid_hw_close(hdev);
> hid_hw_stop(hdev);
[Severity: High]
Does this free dev while the HID hardware is still active?
By calling kfree(dev) before hid_hw_stop(hdev), incoming hardware reports
can still be dispatched to ft260_raw_event(). That handler calls
hid_get_drvdata(hdev) and will dereference the freed dev structure.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260822213941.98882-1-michael.zaidman@gmail.com?part=1
^ permalink raw reply [flat|nested] 43+ messages in thread
* Re: [PATCH 11/13] HID: ft260: i2c: fix large write transaction failure
2026-08-22 21:39 ` [PATCH 11/13] HID: ft260: i2c: fix large write transaction failure Michael Zaidman
@ 2026-08-22 22:02 ` sashiko-bot
0 siblings, 0 replies; 43+ messages in thread
From: sashiko-bot @ 2026-08-22 22:02 UTC (permalink / raw)
To: Michael Zaidman; +Cc: linux-input, dmitry.torokhov
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
Pre-existing issues:
- [High] The `ft260_device` structure is freed before the HID hardware is stopped, creating a use-after-free vulnerability in the event handler.
- [High] A malicious USB device can cause a kernel panic via division by zero by reporting a clock frequency of 0, or by stalling the feature report read.
- [High] A malicious USB device can trigger an out-of-bounds heap read in `ft260_gpio_init` via an unvalidated `uart_mode` value.
--
--- Patch [11]: [PATCH 11/13] HID: ft260: i2c: fix large write transaction failure ---
commit 194ff3edbd401c08d64eb7640df73c9f274ff9ea
Author: Michael Zaidman <michael.zaidman@gmail.com>
HID: ft260: i2c: fix large write transaction failure
Fixes a regression where multi-report I2C writes failed because middle
reports were treated like a final STOP. It passes the bus-busy check
policy from the caller so intermediate chunks and the command phase of
combined transfers do not wait for idle.
Link: https://github.com/MichaelZaidman/hid-ft260/issues/35
Link: https://github.com/MichaelZaidman/hid-ft260/issues/42
> diff --git a/drivers/hid/hid-ft260.c b/drivers/hid/hid-ft260.c
> index a2f4b6e5a16bb..36687c086b408 100644
> --- a/drivers/hid/hid-ft260.c
> +++ b/drivers/hid/hid-ft260.c
> @@ -566,9 +566,14 @@ static int ft260_xfer_status(struct ft260_device *dev, u8 bus_busy)
> }
>
> dev->clock = le16_to_cpu(report.clock);
[Severity: High]
This is a pre-existing issue, but can a malicious USB device cause a
kernel panic here via division by zero?
In ft260_xfer_status(), dev->clock is populated from the device's
FT260_I2C_STATUS feature report without validation against zero.
Later, during an I2C transfer in ft260_hid_output_report_check_status():
drivers/hid/hid-ft260.c:ft260_hid_output_report_check_status() {
...
ret = ft260_hid_output_report(hdev, data, len);
if (ret < 0) {
...
return ret;
}
/* transfer time = 1 / clock(KHz) * 9 bits * bytes */
usec = len * 9000 / dev->clock;
...
}
A malicious device could return a clock frequency of 0, or stall the
initial feature report read (leaving dev->clock at its initialized 0).
When the first I2C payload is processed, does this cause a division by
zero?
> - ft260_dbg("bus_status %#02x, clock %u\n", report.bus_status,
> - dev->clock);
> + ft260_dbg("bus_status %#02x, clock %u, bus_busy %#02x\n",
> + report.bus_status, dev->clock, bus_busy);
>
[ ... ]
[Severity: High]
This isn't a bug introduced by this patch, but is there a use-after-free
vulnerability in the event handler during device removal?
In ft260_remove(), the ft260_device structure is freed before the HID
hardware is stopped:
drivers/hid/hid-ft260.c:ft260_remove() {
...
kfree(dev);
}
hid_hw_close(hdev);
hid_hw_stop(hdev);
}
If an incoming hardware report is received between the memory being freed
and the hardware being stopped, the HID core will call ft260_raw_event().
This function retrieves the freed ft260_device and immediately dereferences
it:
drivers/hid/hid-ft260.c:ft260_raw_event() {
...
spin_lock_irqsave(&dev->read_lock, irqflags);
...
}
Can an attacker time incoming reports to race with remove() and trigger a
use-after-free?
[Severity: High]
This is also a pre-existing issue, but does ft260_gpio_init() have an
out-of-bounds read when parsing the UART mode?
During device initialization, cfg->uart_mode is completely controlled by
the device. In ft260_gpio_init(), this value is used as an index into a
5-element array without bounds checking:
drivers/hid/hid-ft260.c:ft260_gpio_init() {
...
if (mode & FT260_MODE_UART)
dev->gpio_en |= dev->gpio_uart_mode[cfg->uart_mode];
hid_info(hdev, "enabled GPIOs: %04x\n", dev->gpio_en);
...
}
Could a device returning a uart_mode value between 5 and 255 cause an
out-of-bounds read past the end of the ft260_device structure into
adjacent heap memory, and leak that 16-bit OOB value to the kernel log?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260822213941.98882-1-michael.zaidman@gmail.com?part=11
^ permalink raw reply [flat|nested] 43+ messages in thread
* Re: [PATCH 10/13] HID: ft260: uart: fix active-low RTS/CTS/DTR/DSR polarity
2026-08-22 21:39 ` [PATCH 10/13] HID: ft260: uart: fix active-low RTS/CTS/DTR/DSR polarity Michael Zaidman
@ 2026-08-22 22:03 ` sashiko-bot
2026-08-25 8:16 ` Linus Walleij
1 sibling, 0 replies; 43+ messages in thread
From: sashiko-bot @ 2026-08-22 22:03 UTC (permalink / raw)
To: Michael Zaidman; +Cc: dmitry.torokhov, linux-input
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
Pre-existing issues:
- [High] Read-modify-write race condition in ft260_uart_tiocmset() due to reading shared state outside of a mutex.
- [High] ft260_uart_tiocmget() fails to check if ft260_gpio_get_all() returns an error code, silently returning a corrupted modem status bitmask to userspace.
--
--- Patch [10]: [PATCH 10/13] HID: ft260: uart: fix active-low RTS/CTS/DTR/DSR polarity ---
commit 1c4e1ea308d880a16bd5a589b4e5e279a4dec5e8
Author: Rio Liu <rio@r26.me>
HID: ft260: uart: fix active-low RTS/CTS/DTR/DSR polarity
This commit inverts the TIOCMGET sense and the TIOCMSET drive levels for
the active-low FT260 modem control GPIOs (RTS, CTS, DTR, and DSR). This
aligns the userspace ioctl state with the electrical lines, leaving DCD
and RI unchanged.
> diff --git a/drivers/hid/hid-ft260.c b/drivers/hid/hid-ft260.c
> index 0a1875a1a7eca..a2f4b6e5a16bb 100644
> --- a/drivers/hid/hid-ft260.c
> +++ b/drivers/hid/hid-ft260.c
> @@ -1985,10 +1985,10 @@ static int ft260_uart_tiocmget(struct tty_struct *tty)
>
> gpios = ft260_gpio_get_all(port->gc, FT260_GPIO_VALUE);
>
[Severity: High]
This isn't a bug introduced by this patch, but ft260_gpio_get_all() can return
a negative error code (such as -EIO) if a hardware or transport error occurs.
Does evaluating this negative error code as a bitmask in the return statement
below risk masking the hardware error and returning a corrupted modem status
to userspace instead of propagating the error?
> - return (((gpios & FT260_GPIO_B) ? TIOCM_RTS : 0) |
> - ((gpios & FT260_GPIO_E) ? TIOCM_CTS : 0) |
> - ((gpios & FT260_GPIO_F) ? TIOCM_DTR : 0) |
> - ((gpios & FT260_GPIO_H) ? TIOCM_DSR : 0) |
> + return ((!(gpios & FT260_GPIO_B) ? TIOCM_RTS : 0) |
> + (!(gpios & FT260_GPIO_E) ? TIOCM_CTS : 0) |
> + (!(gpios & FT260_GPIO_F) ? TIOCM_DTR : 0) |
> + (!(gpios & FT260_GPIO_H) ? TIOCM_DSR : 0) |
> ((gpios & FT260_GPIO_4) ? TIOCM_CAR : 0) |
> ((gpios & FT260_GPIO_5) ? TIOCM_RNG : 0));
> }
> @@ -2013,13 +2013,13 @@ static int ft260_uart_tiocmset(struct tty_struct *tty,
> mutex_lock(&port->lock);
[Severity: High]
This is also a pre-existing issue, but in ft260_uart_tiocmset(), the local
variable rep.gpio is populated from the shared state port->gpio before
acquiring port->lock:
rep.gpio = port->gpio;
mutex_lock(&port->lock);
Could this result in a read-modify-write race where a concurrent GPIO
modification (e.g., via sysfs) is silently reverted by the TTY ioctl?
Should rep.gpio be populated inside the critical section instead?
>
> if (set & TIOCM_RTS)
> - ft260_gpio_output_cfg(&rep.gpio, FT260_GPIO_UART_RTS, 1);
> + ft260_gpio_output_cfg(&rep.gpio, FT260_GPIO_UART_RTS, 0);
> if (set & TIOCM_DTR)
> - ft260_gpio_output_cfg(&rep.gpio, FT260_GPIO_UART_DTR, 1);
> + ft260_gpio_output_cfg(&rep.gpio, FT260_GPIO_UART_DTR, 0);
> if (clear & TIOCM_RTS)
> - ft260_gpio_output_cfg(&rep.gpio, FT260_GPIO_UART_RTS, 0);
> + ft260_gpio_output_cfg(&rep.gpio, FT260_GPIO_UART_RTS, 1);
> if (clear & TIOCM_DTR)
> - ft260_gpio_output_cfg(&rep.gpio, FT260_GPIO_UART_DTR, 0);
> + ft260_gpio_output_cfg(&rep.gpio, FT260_GPIO_UART_DTR, 1);
>
> ft260_dbg("dirs %#02x vals %#02x ex_dirs %#02x ex_vals %#02x\n",
> rep.gpio.dirs, rep.gpio.vals,
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260822213941.98882-1-michael.zaidman@gmail.com?part=10
^ permalink raw reply [flat|nested] 43+ messages in thread
* Re: [PATCH 12/13] HID: ft260: workaround for TN_189 errata endpoint STALL after enumeration
2026-08-22 21:39 ` [PATCH 12/13] HID: ft260: workaround for TN_189 errata endpoint STALL after enumeration Michael Zaidman
@ 2026-08-22 22:03 ` sashiko-bot
0 siblings, 0 replies; 43+ messages in thread
From: sashiko-bot @ 2026-08-22 22:03 UTC (permalink / raw)
To: Michael Zaidman; +Cc: linux-input, dmitry.torokhov
Thank you for your contribution! Sashiko AI review found 5 potential issue(s) to consider:
New issues:
- [High] Detached reset work item causes a kernel Oops (Use-After-Free) if the driver module is unloaded before the work executes.
- [High] A persistently broken FT260 device will trigger an exponential fork bomb of reset work items, causing unbounded workqueue growth.
- [Medium] Memory allocation failures unintentionally trigger a full USB device reset.
- [Low] Passing endpoint address (with direction bit) to `usb_rcvintpipe` instead of the endpoint number.
Pre-existing issues:
- [High] `hid_hw_open()` is called before driver spinlocks are initialized, allowing incoming HID reports to access uninitialized locks.
--
--- Patch [12]: [PATCH 12/13] HID: ft260: workaround for TN_189 errata endpoint STALL after enumeration ---
commit 8891d1670b0f7ce6179bd30f8809f4b07e8b46dc
Author: Michael Zaidman <michael.zaidman@gmail.com>
HID: ft260: workaround for TN_189 errata endpoint STALL after enumeration
This commit introduces a workaround for the FT260 TN_189 errata where USB
interrupt endpoints stall after enumeration. It schedules a delayed work item
to reset the USB device and rebind interfaces if a STALL or unresponsiveness
is detected during probe.
Closes: https://github.com/MichaelZaidman/hid-ft260/issues/40
Link: https://ftdichip.com/wp-content/uploads/2026/05/TN_189-FT260-Errata-Technical-Note.pdf
> --- a/drivers/hid/hid-ft260.c
> +++ b/drivers/hid/hid-ft260.c
> @@ -2359,15 +2359,227 @@ static int ft260_uart_probe(struct ft260_device *dev,
[ ... ]
> +static void ft260_reset_and_rebind(struct work_struct *ws)
> +{
[ ... ]
> + for (i = 0; actconfig && i < actconfig->desc.bNumInterfaces; i++) {
> + struct usb_interface *intf = actconfig->interface[i];
> +
> + if (!intf)
> + continue;
> + ret = device_attach(&intf->dev);
[Severity: High]
If the device is persistently wedged and continues to fail probe, will this
device_attach() synchronously trigger fresh ft260_probe() calls that fail
and schedule even more work items?
For a 2-interface device, failing probe on both interfaces could initially
schedule two reset work items. If each work item unbinds and rebinds all
interfaces, does this risk creating an unbounded exponential loop (a fork
bomb) of work items being queued?
> + if (ret < 0)
> + dev_err(&intf->dev,
> + "failed to rebind USB interface: %d\n", ret);
> + }
> +
> + usb_unlock_device(usbdev);
> +out:
> + usb_put_intf(usbif);
> + kfree(rw);
> +}
> +
> +static int ft260_schedule_reset(struct usb_interface *usbif)
> +{
> + struct ft260_reset_work *rw;
> +
> + rw = kmalloc_obj(*rw, GFP_KERNEL);
> + if (!rw)
> + return -ENOMEM;
> +
> + usb_get_intf(usbif);
> + rw->usbif = usbif;
> + INIT_WORK(&rw->work, ft260_reset_and_rebind);
> + schedule_work(&rw->work);
[Severity: High]
When returning -ENODEV from ft260_probe() on error, the driver binding
aborts and the module reference count stays at 0.
If an administrator unloads the hid-ft260 module while this scheduled work
item is pending or executing its 10-second retry loop, could the workqueue
execute unmapped memory and trigger a use-after-free panic?
Would it be safer to pin the module with try_module_get()/module_put() or
ensure the work is correctly cancelled on failure?
> +
> + return 0;
> +}
> +
[ ... ]
> +static int ft260_check_intr_ep_health(struct hid_device *hdev)
> +{
[ ... ]
> + buf = kmalloc(FT260_REPORT_MAX_LEN, GFP_KERNEL);
> + if (!buf)
> + return -ENOMEM;
> +
> + pipe = usb_rcvintpipe(usbdev, ep->bEndpointAddress);
[Severity: Low]
Does usb_rcvintpipe() expect just the endpoint number instead of the raw
endpoint address?
Passing ep->bEndpointAddress includes the direction bit, which gets shifted
incorrectly by the pipe creation macro. Could we use
usb_endpoint_num(&ep->desc) here to ensure only the endpoint number is
passed?
> + ret = usb_interrupt_msg(usbdev, pipe, buf, FT260_REPORT_MAX_LEN,
> + &actual_length, 100);
> + kfree(buf);
[ ... ]
> @@ -2392,6 +2604,23 @@ static int ft260_probe(struct hid_device *hdev, const struct hid_device_id *id)
> goto hid_fail;
> }
>
> + /*
> + * TN_189 errata workaround: bail out fast on a broken device so
> + * that hub_event releases the device lock quickly, allowing the
> + * scheduled reset work to acquire it and recover the device.
> + */
> + ret = ft260_check_intr_ep_health(hdev);
> + if (ret) {
> + ft260_schedule_reset(usbif);
[Severity: Medium]
If ft260_check_intr_ep_health() fails due to a memory allocation error
(-ENOMEM), this broad check will catch it. Could this erroneously schedule
a full USB device reset during system memory pressure rather than just
propagating the -ENOMEM failure?
> + goto err_hid_stop;
> + }
> +
> + ret = ft260_check_dev_responsive(hdev);
> + if (ret) {
> + ft260_schedule_reset(usbif);
> + goto err_hid_stop;
> + }
> +
> ret = hid_hw_open(hdev);
[Severity: High]
This is a pre-existing issue, but calling hid_hw_open() here starts the
hardware and allows incoming USB events to trigger the driver's report
callbacks immediately.
If an incoming report causes ft260_raw_event() to attempt to acquire
dev->read_lock before it is initialized further down in ft260_probe()
via spin_lock_init(), could this lead to an uninitialized spinlock splat
or kernel crash?
> if (ret) {
> hid_err(hdev, "failed to open HID HW\n");
> goto err_hid_stop;
> }
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260822213941.98882-1-michael.zaidman@gmail.com?part=12
^ permalink raw reply [flat|nested] 43+ messages in thread
* Re: [PATCH 13/13] HID: ft260: i2c: abort in-flight transfers with STOP before reset
2026-08-22 21:39 ` [PATCH 13/13] HID: ft260: i2c: abort in-flight transfers with STOP before reset Michael Zaidman
@ 2026-08-22 22:12 ` sashiko-bot
0 siblings, 0 replies; 43+ messages in thread
From: sashiko-bot @ 2026-08-22 22:12 UTC (permalink / raw)
To: Michael Zaidman; +Cc: dmitry.torokhov, linux-input
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
New issues:
- [High] Use-After-Free in ft260_remove() due to premature kfree(dev) before stopping hardware.
- [High] Flawed cleanup order in UART device removal leads to Use-After-Free.
Pre-existing issues:
- [High] Divide-by-zero panic in `ft260_hid_output_report_check_status` if the device provides a clock frequency of 0.
--
--- Patch [13]: [PATCH 13/13] HID: ft260: i2c: abort in-flight transfers with STOP before reset ---
commit c3cb7a3558ab0556a46bf87d920daa2c07f916c4
Author: Michael Zaidman <michael.zaidman@gmail.com>
HID: ft260: i2c: abort in-flight transfers with STOP before reset
This commit introduces ft260_i2c_abort() to issue a STOP-only write before
resetting the I2C controller when a transaction needs to be torn down. This
prevents the controller from being stuck in a BUS_BUSY state which can block
subsequent START conditions. It updates various I2C and SMBus read/write
status checks to use this new abort mechanism instead of a simple reset.
> diff --git a/drivers/hid/hid-ft260.c b/drivers/hid/hid-ft260.c
> index 9ae688f6208f2..a35a1af2d7e57 100644
> --- a/drivers/hid/hid-ft260.c
> +++ b/drivers/hid/hid-ft260.c
[ ... ]
> @@ -538,6 +538,33 @@ static int ft260_i2c_reset(struct hid_device *hdev)
[ ... ]
> +static int ft260_i2c_abort(struct hid_device *hdev, u8 addr)
> +{
> + ft260_i2c_stop(hdev, addr);
> + return ft260_i2c_reset(hdev);
> +}
> +
> static int ft260_xfer_status(struct ft260_device *dev, u8 bus_busy)
> {
> struct hid_device *hdev = dev->hdev;
[Severity: High]
This is a pre-existing issue, but can this lead to a divide-by-zero panic if
a misbehaving USB device provides a clock value of 0?
In ft260_xfer_status(), the device-provided clock value is trusted without
bounds checking:
dev->clock = le16_to_cpu(report.clock);
Then in ft260_hid_output_report_check_status(), an unchecked division by
dev->clock occurs:
usec = len * 9000 / dev->clock;
[ ... ]
While reviewing this patch, I also noticed a couple of other issues in code
not directly modified by this commit:
[Severity: High]
Can an incoming HID report access the dev structure after it has been freed?
In ft260_remove(), dev is explicitly freed:
drivers/hid/hid-ft260.c:ft260_remove() {
...
kfree(dev);
...
}
But if hid_hw_stop() is called after this, an incoming report could trigger
ft260_raw_event(), which dereferences the freed dev:
drivers/hid/hid-ft260.c:ft260_raw_event() {
...
spin_lock_irqsave(&dev->read_lock, irqflags);
...
}
[Severity: High]
Does this cleanup order leave a window where the work could be re-queued and
execute after the port is freed?
In ft260_remove(), the work is cancelled:
drivers/hid/hid-ft260.c:ft260_remove() {
...
cancel_work_sync(&dev->wakeup_work);
...
}
However, the timer that queues this work isn't deleted until later in
ft260_uart_port_remove():
drivers/hid/hid-ft260.c:ft260_uart_port_remove() {
...
timer_delete_sync(&port->wakeup_timer);
...
}
If the timer fires in this window, it can re-queue the work. Later, the
structure is freed:
drivers/hid/hid-ft260.c:ft260_uart_port_destroy() {
...
kfree(port);
...
}
If the asynchronously queued work executes after the port is freed, could it
cause a use-after-free in ft260_uart_do_wakeup()?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260822213941.98882-1-michael.zaidman@gmail.com?part=13
^ permalink raw reply [flat|nested] 43+ messages in thread
* Re: [PATCH 03/13] HID: ft260: add GPIO support on top of UART
2026-08-22 21:39 ` [PATCH 03/13] HID: ft260: add GPIO support on top of UART Michael Zaidman
2026-08-22 21:56 ` sashiko-bot
@ 2026-08-25 7:44 ` Linus Walleij
2026-08-27 20:39 ` Michael Zaidman
1 sibling, 1 reply; 43+ messages in thread
From: Linus Walleij @ 2026-08-25 7:44 UTC (permalink / raw)
To: Michael Zaidman
Cc: Jiri Kosina, Benjamin Tissoires, Bartosz Golaszewski,
Germain Hebert, Rio Liu, Bruno Giacomazzi, Christina Quast,
linux-input, linux-gpio, linux-i2c, linux-kernel
Hi Michael,
thanks for your patch!
It seems this patch title should just be
"HID: ft260: add GPIO support"
the "on top of UART" part seems to be more about
which order you did the patches and that is about
development process, not technical content, we don't
put that into Subject.
On Sat, Aug 22, 2026 at 11:40 PM Michael Zaidman
<michael.zaidman@gmail.com> wrote:
> Add gpiochip support for the FT260 multifunctional pins, including
> GPIO2/GPIOA/GPIOG function selection via sysfs, and enable the
> available GPIOs according to the active UART mode.
>
> This supersedes the earlier pre-UART GPIO series:
> https://lore.kernel.org/lkml/20230211115752.26276-2-michael.zaidman@gmail.com/
>
> The gpio_chip.set callback returns int to match the current gpiolib
> API, based on the adaptation originally contributed by Rio Liu
> <rio@r26.me>.
>
> Signed-off-by: Michael Zaidman <michael.zaidman@gmail.com>
You probably want:
#include <linus/bits.h>
(...)
> static int ft260_debug = 1;
> @@ -57,6 +58,11 @@ MODULE_PARM_DESC(debug, "Toggle FT260 debugging messages");
> #define FT260_RD_DATA_MAX (180)
> #define FT260_WR_I2C_DATA_MAX (60)
> #define FT260_WR_UART_DATA_MAX (62)
> +#define FT260_GPIOCHIP "ft260_gpio"
> +#define FT260_GPIO_MAX (6)
> +#define FT260_GPIO_EX_MAX (8)
So 6 GPIOs is "max" and then there are extended GPIOs and then there is
a new max which is 8?
I would give these defines some easier to understand name such as:
#define FT260_BASE_GPIOS 6
#define FT260_EXTENDED_GPIOS 8
> +#define FT260_GPIO_TOTAL (FT260_GPIO_MAX + FT260_GPIO_EX_MAX)
> +#define FT260_GPIO_MASK (~(0xffff << FT260_GPIO_TOTAL))
This macro seems to be using funky arithmetic with bits
rolling out on the left.
What about just:
#define FT260_GPIO_MASK GENMASK(FT260_GPIO_TOTAL, 0)
> +/* GPIO offsets */
> +enum {
Is this *really* an enum? I feel an enum should be something like
consecutive or sparse integer ranges.
What about just using #define for those.
> + FT260_GPIO_0 = (1 << 0),
> + FT260_GPIO_1 = (1 << 1),
#define FT259_GPIO_0 BIT(0)
#define FT259_GPIO_1 BIT(1)
(...)
> + FT260_GPIO_A = (1 << (FT260_GPIO_MAX + 0)),
> + FT260_GPIO_B = (1 << (FT260_GPIO_MAX + 1)),
Those are the extended GPIOs I guess? Mention
that in the macro name? Also you know the bit number.
Do this:
#define FT260_GPIO_EXT_A BIT(7)
#define FT260_GPIO_EXT_B BIT(8)
> +/* GPIO groups */
> +enum {
> + FT260_GPIO_WAKEUP = (FT260_GPIO_3),
> + FT260_GPIO_I2C_DEFAULT = (FT260_GPIO_0 | FT260_GPIO_1),
> + FT260_GPIO_UART_RX_TX = (FT260_GPIO_C | FT260_GPIO_D),
> + FT260_GPIO_UART_DCD_RI = (FT260_GPIO_4 | FT260_GPIO_5),
> + FT260_GPIO_UART_RTS_CTS = (FT260_GPIO_B | FT260_GPIO_E),
> + FT260_GPIO_UART_DTR_DSR = (FT260_GPIO_F | FT260_GPIO_H),
> + FT260_GPIO_UART_MODE_0_SET = (FT260_GPIO_UART_RX_TX |
> + FT260_GPIO_UART_DCD_RI |
> + FT260_GPIO_UART_RTS_CTS |
> + FT260_GPIO_UART_DTR_DSR),
> + FT260_GPIO_UART_MODE_1_SET = (FT260_GPIO_UART_DTR_DSR),
> + FT260_GPIO_UART_MODE_2_SET = (FT260_GPIO_UART_RTS_CTS),
> + FT260_GPIO_UART_MODE_3_SET = (FT260_GPIO_UART_RTS_CTS |
> + FT260_GPIO_UART_DTR_DSR),
> + FT260_GPIO_UART_MODE_4_SET = (FT260_GPIO_UART_MODE_3_SET),
> + FT260_GPIO_UART_DEFAULT = (FT260_GPIO_UART_MODE_0_SET),
> + FT260_GPIO_UART_MODE_1_CLR = (FT260_GPIO_UART_RX_TX |
> + FT260_GPIO_UART_RTS_CTS),
> + FT260_GPIO_UART_MODE_2_CLR = (FT260_GPIO_UART_RX_TX |
> + FT260_GPIO_UART_DTR_DSR),
> + FT260_GPIO_UART_MODE_3_CLR = (FT260_GPIO_UART_RX_TX),
> + FT260_GPIO_UART_MODES = (5),
Use #defines for all of this as well.
> +struct ft260_gpio_state {
> + u8 vals; /* GPIO[0-5] values in bits 0 - 5 */
> + u8 dirs; /* GPIO[0-5] directions, 0 - in, 1 - out */
> + u8 ex_vals; /* GPIO[A-H] values in bits 0 - 7 */
> + u8 ex_dirs; /* GPIO[A-H] directions, 0 - in, 1 - out */
> +} __packed;
So instead of creating your own cache of all registers,
have you considered just using regmap for this?
Regmap is essentially a register cache.
> /* UART interface configuration */
> enum {
> - FT260_CFG_FLOW_CTRL_OFF = 0x00,
> - FT260_CFG_FLOW_CTRL_RTS_CTS = 0x01,
> - FT260_CFG_FLOW_CTRL_DTR_DSR = 0x02,
> - FT260_CFG_FLOW_CTRL_XON_XOFF = 0x03,
> - FT260_CFG_FLOW_CTRL_NONE = 0x04,
> + FT260_UART_CFG_FLOW_CTRL_OFF = 0x00,
> + FT260_UART_CFG_FLOW_CTRL_RTS_CTS = 0x01,
> + FT260_UART_CFG_FLOW_CTRL_DTR_DSR = 0x02,
> + FT260_UART_CFG_FLOW_CTRL_XON_XOFF = 0x03,
> + FT260_UART_CFG_FLOW_CTRL_NONE = 0x04,
This looks like names you could have already assigned
in the UART patch and then the diff would be less noisy.
> - FT260_CFG_DATA_BITS_7 = 0x07,
> - FT260_CFG_DATA_BITS_8 = 0x08,
> + FT260_UART_CFG_DATA_BITS_7 = 0x07,
> + FT260_UART_CFG_DATA_BITS_8 = 0x08,
Dito.
> -#define FT260_UART_EN_PW_SAVE_BAUD (4800)
> +#define FT260_UART_EN_PW_SAVE_BAUD (4800)
Dito.
> +static void ft260_gpio_en_set(struct ft260_device *dev, u16 bitmap)
> +{
> + dev->gpio_en |= bitmap & FT260_GPIO_MASK;
> +}
> +
> +static void ft260_gpio_en_clr(struct ft260_device *dev, u16 bitmap)
> +{
> + dev->gpio_en &= ~bitmap & FT260_GPIO_MASK;
> +}
Linux has bitmap manipulation functions in
<linus/bitmap.h>, use these instead of reinventing the wheel.
Yours,
Linus Walleij
^ permalink raw reply [flat|nested] 43+ messages in thread
* Re: [PATCH 01/13] HID: ft260: add serial driver
2026-08-22 21:39 ` [PATCH 01/13] HID: ft260: add serial driver Michael Zaidman
2026-08-22 22:00 ` sashiko-bot
@ 2026-08-25 7:49 ` Linus Walleij
2026-08-25 8:12 ` Linus Walleij
2 siblings, 0 replies; 43+ messages in thread
From: Linus Walleij @ 2026-08-25 7:49 UTC (permalink / raw)
To: Michael Zaidman
Cc: Jiri Kosina, Benjamin Tissoires, Bartosz Golaszewski,
Germain Hebert, Rio Liu, Bruno Giacomazzi, Christina Quast,
linux-input, linux-gpio, linux-i2c, linux-kernel, Daniel Beer
Hi Michael, Christina,
thanks for your patch!
On Sat, Aug 22, 2026 at 11:40 PM Michael Zaidman
<michael.zaidman@gmail.com> wrote:
> From: Christina Quast <contact@christina-quast.de>
>
> Add a TTY serial interface (/dev/ttyFTx) for FT260 USB HID devices,
> providing baud rate configuration, data transfer, and termios settings
> without requiring userspace HID report formatting.
>
> The FT260 exposes I2C and UART through separate HID interfaces. Only
> I2C was supported previously; UART required FTDI hidraw example code.
>
> This is a port of the v4 patch, adapted to the I2C hardening that has
> since been merged upstream: input-report length validation, the
> read_buf/raw_event race fix with read_lock, and SMBus block-read
> START/STOP handling. HID_FT260 now depends on TTY because the UART code
> is part of this driver.
>
> Routing UART reports through ft260_raw_event() also made v4 fall through
> the report dispatch after an I2C input report had already been consumed,
> so every I2C input report was additionally reported as unhandled. This
> regression is fixed here.
>
> Ported from:
> https://lore.kernel.org/all/20231218093153.192268-1-contact@christina-quast.de/
>
> The follow-up v5 series stalled without being applied:
> https://lore.kernel.org/linux-input/20240216-ft260_review5-v5-1-36db44673ac7@christina-quast.de/
>
> Signed-off-by: Christina Quast <contact@christina-quast.de>
> Co-developed-by: Daniel Beer <daniel.beer@igorinstitute.com>
> Signed-off-by: Daniel Beer <daniel.beer@igorinstitute.com>
> Co-developed-by: Michael Zaidman <michael.zaidman@gmail.com>
> Signed-off-by: Michael Zaidman <michael.zaidman@gmail.com>
(...)
> +/* UART interface configuration */
> +enum {
> + FT260_CFG_FLOW_CTRL_OFF = 0x00,
> + FT260_CFG_FLOW_CTRL_RTS_CTS = 0x01,
> + FT260_CFG_FLOW_CTRL_DTR_DSR = 0x02,
> + FT260_CFG_FLOW_CTRL_XON_XOFF = 0x03,
> + FT260_CFG_FLOW_CTRL_NONE = 0x04,
> +
> + FT260_CFG_DATA_BITS_7 = 0x07,
> + FT260_CFG_DATA_BITS_8 = 0x08,
> +
> + FT260_CFG_PAR_NO = 0x00,
> + FT260_CFG_PAR_ODD = 0x01,
> + FT260_CFG_PAR_EVEN = 0x02,
> + FT260_CFG_PAR_HIGH = 0x03,
> + FT260_CFG_PAR_LOW = 0x04,
> +
> + FT260_CFG_STOP_ONE_BIT = 0x00,
> + FT260_CFG_STOP_TWO_BIT = 0x02,
> +
> + FT260_CFG_BREAKING_NO = 0x00,
> + FT260_CFG_BEAKING_YES = 0x01,
> +
> + FT260_CFG_BAUD_MIN = 1200,
> + FT260_CFG_BAUD_MAX = 12000000,
> +};
What kind of "enum" is this that re-assigns 0x00, 0x01 etc to
several enumerators?
Either this is four different enums or you should just do a
series of #defines instead if they are all really coming from
the same place, because using an enum like this does not
make sense: it's like numering pears, apples, oranges
and peaches in the same enum or something.
Also use the UART_ infix as mentioned in the GPIO patch
so you don't need to immediately change it in the next
patch.
Yours,
Linus Walleij
^ permalink raw reply [flat|nested] 43+ messages in thread
* Re: [PATCH 08/13] HID: ft260: uart: add modem pins control via ioctl
2026-08-22 21:39 ` [PATCH 08/13] HID: ft260: uart: add modem pins control via ioctl Michael Zaidman
2026-08-22 21:54 ` sashiko-bot
@ 2026-08-25 8:08 ` Linus Walleij
2026-08-27 22:08 ` Michael Zaidman
1 sibling, 1 reply; 43+ messages in thread
From: Linus Walleij @ 2026-08-25 8:08 UTC (permalink / raw)
To: Michael Zaidman
Cc: Jiri Kosina, Benjamin Tissoires, Bartosz Golaszewski,
Germain Hebert, Rio Liu, Bruno Giacomazzi, Christina Quast,
linux-input, linux-gpio, linux-i2c, linux-kernel
Hi Michael,
thanks for your patch!
On Sat, Aug 22, 2026 at 11:40 PM Michael Zaidman
<michael.zaidman@gmail.com> wrote:
> Add TIOCMGET/TIOCMSET (and thus TIOCMBIS/TIOCMBIC) support so userspace
> can drive DTR/RTS when the modem pins are in GPIO mode. This enables
> programming an ESP32 over the FT260 with esptool, which resets the chip
> via those lines.
>
> Also key GPIO sysfs registration off chip_mode rather than USB interface
> number 0, and include DCD/RI in UART mode 3/4 GPIO sets so the modem
> mask matches the pins esptool needs.
>
> Signed-off-by: Michael Zaidman <michael.zaidman@gmail.com>
I'm not the authortative expert on modem control using GPIO,
but what I think you should do is to
select GPIOLIB
select SERIAL_MCTRL_GPIO
On Kconfig, so that gpiolib is always available and you can use
the generic modem control helpers for modem control over GPIO.
This can be a bit delicate in this case since the gpiochip that you
use for mctrl is also registered in this driver, so you need to
register the gpiochip *first*, then add a look-up table for the
GPIOs, then register this modem control.
I don't think this has been done before for this type of device
so it could be a bit tricky, but it is probably the right thing to
do.
#include <linux/gpio/machine.h>
#include <linux/gpio/consumer.h>
Then look in e.g. drivers/mfd/sm501.c which is an
MFD device that register a gpiochip and then consume
GPIOs from itself. In the function
sm501_register_gpio_i2c_instance() this driver registers
a table of dynamic gpios associated with a separate subdevice.
A separate subdevice probably isn't needed but
something like this will be needed, albeit with the gpiochip
spawn off the main device for FT260.
The core idea is that the serial modem control should look
up the GPIOs from its own gpiochip and use the MCTRL
library helpers, then this should result in very little and
compact code that is easy to read.
Yours,
Linus Walleij
^ permalink raw reply [flat|nested] 43+ messages in thread
* Re: [PATCH 01/13] HID: ft260: add serial driver
2026-08-22 21:39 ` [PATCH 01/13] HID: ft260: add serial driver Michael Zaidman
2026-08-22 22:00 ` sashiko-bot
2026-08-25 7:49 ` Linus Walleij
@ 2026-08-25 8:12 ` Linus Walleij
2026-08-27 19:16 ` Michael Zaidman
2 siblings, 1 reply; 43+ messages in thread
From: Linus Walleij @ 2026-08-25 8:12 UTC (permalink / raw)
To: Michael Zaidman, Greg Kroah-Hartman, Jiri Slaby, linux-serial
Cc: Jiri Kosina, Benjamin Tissoires, Bartosz Golaszewski,
Germain Hebert, Rio Liu, Bruno Giacomazzi, Christina Quast,
linux-input, linux-gpio, linux-i2c, linux-kernel, Daniel Beer
On Sat, Aug 22, 2026 at 11:40 PM Michael Zaidman
<michael.zaidman@gmail.com> wrote:
>
> From: Christina Quast <contact@christina-quast.de>
>
> Add a TTY serial interface (/dev/ttyFTx) for FT260 USB HID devices,
> providing baud rate configuration, data transfer, and termios settings
> without requiring userspace HID report formatting.
You need to CC the TTY maintainers and linux-serial on patches like this,
just keep in mind when resending, thanks!
Yours,
Linus Walleij
^ permalink raw reply [flat|nested] 43+ messages in thread
* Re: [PATCH 09/13] HID: ft260: gpio: group sysfs attrs per HID interface
2026-08-22 21:39 ` [PATCH 09/13] HID: ft260: gpio: group sysfs attrs per HID interface Michael Zaidman
2026-08-22 21:54 ` sashiko-bot
@ 2026-08-25 8:13 ` Linus Walleij
2026-08-27 20:50 ` Michael Zaidman
1 sibling, 1 reply; 43+ messages in thread
From: Linus Walleij @ 2026-08-25 8:13 UTC (permalink / raw)
To: Michael Zaidman
Cc: Jiri Kosina, Benjamin Tissoires, Bartosz Golaszewski,
Germain Hebert, Rio Liu, Bruno Giacomazzi, Christina Quast,
linux-input, linux-gpio, linux-i2c, linux-kernel
Hi Michael,
thanks for your patch!
On Sat, Aug 22, 2026 at 11:40 PM Michael Zaidman
<michael.zaidman@gmail.com> wrote:
> Split the single sysfs attribute group into per-interface groups based
> on chip mode (DCNF0/DCNF1):
>
> - I2C-only (mode 1): full I2C + GPIO attributes on the I2C HID iface
> - Dual mode (0/3): I2C-facing attrs on the I2C iface; GPIO and UART
> attrs on the UART iface
> - UART-only (mode 2): GPIO and UART attrs on the UART iface
>
> Signed-off-by: Michael Zaidman <michael.zaidman@gmail.com>
Drop "gpio" from the subject of this patch, it's all about sysfs
nothung else.
Yours,
Linus Walleij
^ permalink raw reply [flat|nested] 43+ messages in thread
* Re: [PATCH 10/13] HID: ft260: uart: fix active-low RTS/CTS/DTR/DSR polarity
2026-08-22 21:39 ` [PATCH 10/13] HID: ft260: uart: fix active-low RTS/CTS/DTR/DSR polarity Michael Zaidman
2026-08-22 22:03 ` sashiko-bot
@ 2026-08-25 8:16 ` Linus Walleij
2026-08-27 21:08 ` Michael Zaidman
1 sibling, 1 reply; 43+ messages in thread
From: Linus Walleij @ 2026-08-25 8:16 UTC (permalink / raw)
To: Michael Zaidman
Cc: Jiri Kosina, Benjamin Tissoires, Bartosz Golaszewski,
Germain Hebert, Rio Liu, Bruno Giacomazzi, Christina Quast,
linux-input, linux-gpio, linux-i2c, linux-kernel
Hi Michael,
thanks for your patch!
On Sat, Aug 22, 2026 at 11:40 PM Michael Zaidman
<michael.zaidman@gmail.com> wrote:
> From: Rio Liu <rio@r26.me>
>
> The FT260 modem control GPIOs for RTS, CTS, DTR, and DSR are active-low.
> Invert the TIOCMGET sense and the TIOCMSET drive levels for those pins
> so userspace ioctl state matches the electrical lines. Leave DCD and RI
> unchanged.
>
> Signed-off-by: Rio Liu <rio@r26.me>
> Signed-off-by: Michael Zaidman <michael.zaidman@gmail.com>
Isn't this just fixing a patch earlier in the same series?
Just fold it in with the patch adding this.
Note: GPIOlib has it's own polarity inversion handling, when
you rewrite the serial modem control to use the helpers,
the gpio table will include a flag for polarity inversion, and
then you just put it there.
Yours,
Linus Walleij
^ permalink raw reply [flat|nested] 43+ messages in thread
* Re: [PATCH 00/13] HID: ft260: add UART and GPIO support, plus I2C fixes
2026-08-22 21:39 [PATCH 00/13] HID: ft260: add UART and GPIO support, plus I2C fixes Michael Zaidman
` (12 preceding siblings ...)
2026-08-22 21:39 ` [PATCH 13/13] HID: ft260: i2c: abort in-flight transfers with STOP before reset Michael Zaidman
@ 2026-08-25 8:21 ` Linus Walleij
2026-08-27 13:27 ` Lee Jones
13 siblings, 1 reply; 43+ messages in thread
From: Linus Walleij @ 2026-08-25 8:21 UTC (permalink / raw)
To: Michael Zaidman, Lee Jones
Cc: Jiri Kosina, Benjamin Tissoires, Bartosz Golaszewski,
Germain Hebert, Rio Liu, Bruno Giacomazzi, Christina Quast,
linux-input, linux-gpio, linux-i2c, linux-kernel
Hi Michael,
thanks for your patch!
On Sat, Aug 22, 2026 at 11:40 PM Michael Zaidman
<michael.zaidman@gmail.com> wrote:
> Mainline supports only the FT260's I2C bridge. This series adds the
> UART/TTY interface and GPIO on top of it, and carries the I2C fixes made
> alongside them - two years of out-of-tree work [1], now replayed upstream.
(...)
> HID: ft260: add serial driver
(...)
> HID: ft260: add GPIO support on top of UART
This comment isn't pertaining to this one device in particular.
I'm not a HID maintainer but this trend to make HID devices spawn
serial and GPIO devices sort of turns HID into the new MFD, but
I suppose Jiri and Lee has discussed this phenomenon in the past?
Yours,
Linus Walleij
^ permalink raw reply [flat|nested] 43+ messages in thread
* Re: [PATCH 00/13] HID: ft260: add UART and GPIO support, plus I2C fixes
2026-08-25 8:21 ` [PATCH 00/13] HID: ft260: add UART and GPIO support, plus I2C fixes Linus Walleij
@ 2026-08-27 13:27 ` Lee Jones
2026-08-27 18:53 ` Michael Zaidman
0 siblings, 1 reply; 43+ messages in thread
From: Lee Jones @ 2026-08-27 13:27 UTC (permalink / raw)
To: Linus Walleij
Cc: Michael Zaidman, Jiri Kosina, Benjamin Tissoires,
Bartosz Golaszewski, Germain Hebert, Rio Liu, Bruno Giacomazzi,
Christina Quast, linux-input, linux-gpio, linux-i2c, linux-kernel
On Tue, 25 Aug 2026, Linus Walleij wrote:
> Hi Michael,
>
> thanks for your patch!
>
> On Sat, Aug 22, 2026 at 11:40 PM Michael Zaidman
> <michael.zaidman@gmail.com> wrote:
>
> > Mainline supports only the FT260's I2C bridge. This series adds the
> > UART/TTY interface and GPIO on top of it, and carries the I2C fixes made
> > alongside them - two years of out-of-tree work [1], now replayed upstream.
> (...)
> > HID: ft260: add serial driver
> (...)
> > HID: ft260: add GPIO support on top of UART
>
> This comment isn't pertaining to this one device in particular.
>
> I'm not a HID maintainer but this trend to make HID devices spawn
> serial and GPIO devices sort of turns HID into the new MFD, but
> I suppose Jiri and Lee has discussed this phenomenon in the past?
What is this? 4 device drivers in one, shoved into HID? Each
component; HID, I2C, GPIO, UART, should live in its respective
subsystem, surely?
--
Lee Jones
^ permalink raw reply [flat|nested] 43+ messages in thread
* Re: [PATCH 00/13] HID: ft260: add UART and GPIO support, plus I2C fixes
2026-08-27 13:27 ` Lee Jones
@ 2026-08-27 18:53 ` Michael Zaidman
2026-08-27 20:51 ` Lee Jones
0 siblings, 1 reply; 43+ messages in thread
From: Michael Zaidman @ 2026-08-27 18:53 UTC (permalink / raw)
To: Lee Jones
Cc: Linus Walleij, Jiri Kosina, Benjamin Tissoires,
Bartosz Golaszewski, linux-input, linux-gpio, linux-i2c,
linux-kernel
On Thu, Aug 27, 2026 at 14:27 +0100, Lee Jones wrote:
> What is this? 4 device drivers in one, shoved into HID? Each
> component; HID, I2C, GPIO, UART, should live in its respective
> subsystem, surely?
The FT260 is a USB HID protocol converter, not four MMIO blocks behind
an MFD. I2C, UART and GPIO are HID reports on one chip. Mainline
hid-ft260 already hosts the I2C adapter in HID for that reason. This
series adds GPIO and UART the same way.
hid-cp2112 and hid-mcp2221 already register an i2c_adapter and a
gpiochip from a hid_driver. They are not split into i2c/ and gpio/.
A subsystem split does not give independent drivers here. The control
and pin mux live in one feature report, and input is one raw_event.
System status (HID feature 0xA1) is chip-wide, not per USB interface.
chip_mode (DCNF0/DCNF1), the 12/24/48 MHz clock, i2c_enable, uart_mode,
UART configuration (baud/frame/flow), I2C reset and I2C clock, GPIO2/A/G
function select, DCD/RI enable, and power-save are fields or SET
requests on that same report. Probe on either HID interface reads 0xA1
and then decides I2C vs UART from chip_mode plus bInterfaceNumber.
GPIO is not a third USB function. It is feature report 0xB0. Which pins
are GPIO depends on that 0xA1 map: I2C enable takes GPIO0/1 (SCL/SDA);
uart_mode takes or frees RX/TX, RTS/CTS, DTR/DSR and DCD/RI; GPIO2/A/G
are UART/power LEDs vs GPIO; GPIO3 is wakeup vs GPIO. The gpiochip is
attached to the I2C HID interface in I2C-only mode and to the UART HID
interface in UART or dual mode. TIOCMGET/TIOCMSET use that gpiochip
when the modem pins are in GPIO mode. Changing UART flow control
rewrites 0xA1 and then updates the GPIO enable mask.
Input is one hid_driver.raw_event. It dispatches I2C read payloads
(0xD0-0xDE), UART RX, and UART interrupt status (0xB1) by report ID.
Dual-mode still has two USB HID interfaces, but they are two pipes to
one chip. There is one 0xA1; there are not two register files.
USB reset (the TN_189 workaround) resets the whole device and rebinds
every interface. Idle wakeup uses chip-wide GET reports (0xA0 / 0xC0)
and power_saving_en from 0xA1.
UART is still HID reports on the UART interface (0xE0 / 0xB1 / data
reports), not an 8250-style port. Putting it in drivers/tty would not
remove the 0xA1/0xB0 coupling.
An MFD split would still need a HID core that owns 0xA1, 0xB0 and
raw_event, with I2C/GPIO/UART cells calling back into it, and with GPIO
availability depending on UART/I2C mode and on which interface probed.
That cell-and-core layout is this driver already. Splitting it does
not give I2C, GPIO, or UART their own independent devices; it is
still one HID protocol converter, in four files to keep aligned
across kernel versions.
Thanks,
Michael
^ permalink raw reply [flat|nested] 43+ messages in thread
* Re: [PATCH 01/13] HID: ft260: add serial driver
2026-08-25 8:12 ` Linus Walleij
@ 2026-08-27 19:16 ` Michael Zaidman
0 siblings, 0 replies; 43+ messages in thread
From: Michael Zaidman @ 2026-08-27 19:16 UTC (permalink / raw)
To: linusw
Cc: bentiss, brgl, brunoceg1, contact, daniel.beer, germain.hebert,
gregkh, jikos, jirislaby, linux-gpio, linux-i2c, linux-input,
linux-kernel, linux-serial, michael.zaidman, rio
On Tue, 25 Aug 2026 at 09:49 +0200, Linus Walleij wrote:
> What kind of "enum" is this that re-assigns 0x00, 0x01 etc to
> several enumerators?
>
> Either this is four different enums or you should just do a
> series of #defines instead if they are all really coming from
> the same place, because using an enum like this does not
> make sense: it's like numering pears, apples, oranges
> and peaches in the same enum or something.
>
> Also use the UART_ infix as mentioned in the GPIO patch
> so you don't need to immediately change it in the next
> patch.
Agreed. v2 will use #defines with the UART_ infix in this
patch so the GPIO patch does not rename them.
On Tue, 25 Aug 2026 at 10:12 +0200, Linus Walleij wrote:
> You need to CC the TTY maintainers and linux-serial on patches like this,
> just keep in mind when resending, thanks!
Will CC linux-serial and the TTY maintainers on v2.
Thanks,
Michael
^ permalink raw reply [flat|nested] 43+ messages in thread
* Re: [PATCH 03/13] HID: ft260: add GPIO support on top of UART
2026-08-25 7:44 ` Linus Walleij
@ 2026-08-27 20:39 ` Michael Zaidman
0 siblings, 0 replies; 43+ messages in thread
From: Michael Zaidman @ 2026-08-27 20:39 UTC (permalink / raw)
To: linusw
Cc: jikos, bentiss, brgl, germain.hebert, rio, brunoceg1, contact,
linux-input, linux-gpio, linux-i2c, linux-kernel
On Tue, 25 Aug 2026 at 09:44 +0200, Linus Walleij wrote:
> It seems this patch title should just be
> "HID: ft260: add GPIO support"
> the "on top of UART" part seems to be more about
> which order you did the patches and that is about
> development process, not technical content, we don't
> put that into Subject.
Will drop that from the subject in v2.
> You probably want:
>
> #include <linus/bits.h>
<linux/bits.h>. I'll switch the pin constants to #define + BIT()
and the group masks to #defines.
> So 6 GPIOs is "max" and then there are extended GPIOs and then there is
> a new max which is 8?
>
> #define FT260_BASE_GPIOS 6
> #define FT260_EXTENDED_GPIOS 8
Agreed, the names are misleading. FT260_GPIO_MAX and
FT260_GPIO_EX_MAX are counts of the two GPIO groups the chip
exposes, not bit indices: 6 pins named GPIO0-5 and 8 named
GPIOA-H. v2 will use FT260_BASE_GPIOS and FT260_EXTENDED_GPIOS.
One distinction worth making, since it explains the bit numbers
you suggested. The GPIO report (0xB0) carries the two groups
in separate bytes: GPIO0-5 values and directions in bits 0-5 of
one byte pair, GPIOA-H values and directions in bits 0-7 of the
next pair. So in the report GPIOA is bit 0.
The FT260_GPIO_* masks are not report bits. They are the
gpiochip offset space, where the two groups are concatenated
into one 14-line chip: GPIO0-5 at offsets 0-5, GPIOA-H at
offsets 6-13. There GPIOA is BIT(6) and GPIOB is BIT(7), so
I'll write them as explicit BIT() with the group named:
#define FT260_GPIO_EXT_A BIT(6)
#define FT260_GPIO_EXT_B BIT(7)
> What about just:
>
> #define FT260_GPIO_MASK GENMASK(FT260_GPIO_TOTAL, 0)
Yes, that mask is over the 14 gpiochip offsets, and GENMASK is
clearer than the shift. It needs to stop at the top offset:
#define FT260_GPIO_MASK GENMASK(FT260_GPIO_TOTAL - 1, 0)
which is the 0x3fff the current expression produces.
GENMASK(FT260_GPIO_TOTAL, 0) would add bit 14.
> Is this *really* an enum? I feel an enum should be something like
> consecutive or sparse integer ranges.
> What about just using #define for those.
>
> Use #defines for all of this as well.
Will do.
> So instead of creating your own cache of all registers,
> have you considered just using regmap for this?
> Regmap is essentially a register cache.
There is no register space for regmap to model. 0xB0
is one 4-byte feature report, a value byte and a direction byte
per GPIO group, reached with hid_hw_raw_request(). The mux that
decides which of those bits are GPIO at all is in a different
report, 0xA1, with an unrelated layout.
> This looks like names you could have already assigned
> in the UART patch and then the diff would be less noisy.
v2 will put the UART_ infix in the serial patch, as in my
other mail.
> Linux has bitmap manipulation functions in
> <linus/bitmap.h>, use these instead of reinventing the wheel.
gpio_en is "GPIO enabled": a mask of which of the 14 lines are
muxed to GPIO at all, derived from chip_mode and uart_mode, not
pin state. The setters only OR and mask it, and the only readers
test one line before touching the pin. That is a u16 flag word,
and the bitmap helpers work on unsigned long arrays, so I'd keep
the u16 and use BIT(offset) in those tests instead of 1 << offset.
I can convert it to DECLARE_BITMAP() if you prefer.
Thanks,
Michael
^ permalink raw reply [flat|nested] 43+ messages in thread
* Re: [PATCH 09/13] HID: ft260: gpio: group sysfs attrs per HID interface
2026-08-25 8:13 ` Linus Walleij
@ 2026-08-27 20:50 ` Michael Zaidman
0 siblings, 0 replies; 43+ messages in thread
From: Michael Zaidman @ 2026-08-27 20:50 UTC (permalink / raw)
To: linusw
Cc: jikos, bentiss, brgl, germain.hebert, rio, brunoceg1, contact,
linux-input, linux-gpio, linux-i2c, linux-kernel
On Tue, 25 Aug 2026 at 10:13 +0200, Linus Walleij wrote:
> Drop "gpio" from the subject of this patch, it's all about sysfs
> nothung else.
Agreed, the patch regroups the whole attribute set, I2C and UART
ones included. v2 will use:
HID: ft260: group sysfs attrs per HID interface
Thanks,
Michael
^ permalink raw reply [flat|nested] 43+ messages in thread
* Re: [PATCH 00/13] HID: ft260: add UART and GPIO support, plus I2C fixes
2026-08-27 18:53 ` Michael Zaidman
@ 2026-08-27 20:51 ` Lee Jones
2026-08-27 22:25 ` Michael Zaidman
0 siblings, 1 reply; 43+ messages in thread
From: Lee Jones @ 2026-08-27 20:51 UTC (permalink / raw)
To: Michael Zaidman
Cc: Linus Walleij, Jiri Kosina, Benjamin Tissoires,
Bartosz Golaszewski, linux-input, linux-gpio, linux-i2c,
linux-kernel
On Thu, 27 Aug 2026, Michael Zaidman wrote:
> On Thu, Aug 27, 2026 at 14:27 +0100, Lee Jones wrote:
> > What is this? 4 device drivers in one, shoved into HID? Each
> > component; HID, I2C, GPIO, UART, should live in its respective
> > subsystem, surely?
>
> The FT260 is a USB HID protocol converter, not four MMIO blocks behind
> an MFD. I2C, UART and GPIO are HID reports on one chip. Mainline
> hid-ft260 already hosts the I2C adapter in HID for that reason. This
> series adds GPIO and UART the same way.
>
> hid-cp2112 and hid-mcp2221 already register an i2c_adapter and a
> gpiochip from a hid_driver. They are not split into i2c/ and gpio/.
>
> A subsystem split does not give independent drivers here. The control
> and pin mux live in one feature report, and input is one raw_event.
>
> System status (HID feature 0xA1) is chip-wide, not per USB interface.
> chip_mode (DCNF0/DCNF1), the 12/24/48 MHz clock, i2c_enable, uart_mode,
> UART configuration (baud/frame/flow), I2C reset and I2C clock, GPIO2/A/G
> function select, DCD/RI enable, and power-save are fields or SET
> requests on that same report. Probe on either HID interface reads 0xA1
> and then decides I2C vs UART from chip_mode plus bInterfaceNumber.
>
> GPIO is not a third USB function. It is feature report 0xB0. Which pins
> are GPIO depends on that 0xA1 map: I2C enable takes GPIO0/1 (SCL/SDA);
> uart_mode takes or frees RX/TX, RTS/CTS, DTR/DSR and DCD/RI; GPIO2/A/G
> are UART/power LEDs vs GPIO; GPIO3 is wakeup vs GPIO. The gpiochip is
> attached to the I2C HID interface in I2C-only mode and to the UART HID
> interface in UART or dual mode. TIOCMGET/TIOCMSET use that gpiochip
> when the modem pins are in GPIO mode. Changing UART flow control
> rewrites 0xA1 and then updates the GPIO enable mask.
>
> Input is one hid_driver.raw_event. It dispatches I2C read payloads
> (0xD0-0xDE), UART RX, and UART interrupt status (0xB1) by report ID.
> Dual-mode still has two USB HID interfaces, but they are two pipes to
> one chip. There is one 0xA1; there are not two register files.
>
> USB reset (the TN_189 workaround) resets the whole device and rebinds
> every interface. Idle wakeup uses chip-wide GET reports (0xA0 / 0xC0)
> and power_saving_en from 0xA1.
>
> UART is still HID reports on the UART interface (0xE0 / 0xB1 / data
> reports), not an 8250-style port. Putting it in drivers/tty would not
> remove the 0xA1/0xB0 coupling.
>
> An MFD split would still need a HID core that owns 0xA1, 0xB0 and
> raw_event, with I2C/GPIO/UART cells calling back into it, and with GPIO
> availability depending on UART/I2C mode and on which interface probed.
> That cell-and-core layout is this driver already. Splitting it does
> not give I2C, GPIO, or UART their own independent devices; it is
> still one HID protocol converter, in four files to keep aligned
> across kernel versions.
That's precisely what MFD is. It's one chip, usually with a shared and
overlapping register spaces, that conducts multiple functions. This is
no different to any other single-chip device or SoC.
Shoving everything into a single driver isn't how things are done in
Linux. This should be divided up into the associated sub-systems where
each part can be reviewed and looked after by the appropriate SMEs.
--
Lee Jones
^ permalink raw reply [flat|nested] 43+ messages in thread
* Re: [PATCH 10/13] HID: ft260: uart: fix active-low RTS/CTS/DTR/DSR polarity
2026-08-25 8:16 ` Linus Walleij
@ 2026-08-27 21:08 ` Michael Zaidman
0 siblings, 0 replies; 43+ messages in thread
From: Michael Zaidman @ 2026-08-27 21:08 UTC (permalink / raw)
To: linusw
Cc: jikos, bentiss, brgl, germain.hebert, rio, brunoceg1, contact,
linux-input, linux-gpio, linux-i2c, linux-kernel
On Tue, 25 Aug 2026 at 10:16 +0200, Linus Walleij wrote:
> Isn't this just fixing a patch earlier in the same series?
>
> Just fold it in with the patch adding this.
It is, and I have no objection to folding it into the patch that
adds TIOCMGET/TIOCMSET, but it costs an attribution I would
rather not drop on my own.
This patch is Rio's: From: Rio Liu, signed off by him and then
by me. The layout of this series, including Rio authoring the
polarity fix, is what I agreed with Jiri before posting. Folding
it into my patch 8 turns that From: into a Co-developed-by: Rio
Liu with his sign-off, which is honest but is a demotion from
author to contributor. Jiri, Rio - either is fine by me, so tell
me which you prefer and v2 will do that.
> Note: GPIOlib has it's own polarity inversion handling, when
> you rewrite the serial modem control to use the helpers,
> the gpio table will include a flag for polarity inversion, and
> then you just put it there.
Understood. If the modem control moves to the mctrl-gpio helpers
then the inversion belongs in the lookup table as
GPIO_ACTIVE_LOW and this code goes away entirely. I'm looking
into that on your patch 8 mail and will answer there.
Worth noting that this drops Rio's patch rather than folds it -
none of his lines would survive the rewrite - so the credit
would have to move to the commit message body with a link to
his fix.
Thanks,
Michael
^ permalink raw reply [flat|nested] 43+ messages in thread
* Re: [PATCH 08/13] HID: ft260: uart: add modem pins control via ioctl
2026-08-25 8:08 ` Linus Walleij
@ 2026-08-27 22:08 ` Michael Zaidman
0 siblings, 0 replies; 43+ messages in thread
From: Michael Zaidman @ 2026-08-27 22:08 UTC (permalink / raw)
To: linusw
Cc: jikos, bentiss, brgl, germain.hebert, rio, brunoceg1, contact,
daniel.beer, gregkh, jirislaby, michael.zaidman, linux-serial,
linux-input, linux-gpio, linux-i2c, linux-kernel
On Tue, 25 Aug 2026 at 10:08 +0200, Linus Walleij wrote:
> I'm not the authortative expert on modem control using GPIO,
> but what I think you should do is to
>
> select GPIOLIB
> select SERIAL_MCTRL_GPIO
>
> On Kconfig, so that gpiolib is always available and you can use
> the generic modem control helpers for modem control over GPIO.
I looked into this, and it does not work for the FT260 without
changing serial_mctrl_gpio.c first. Three blockers:
- Every GPIO access on this chip is a USB transfer, so the
gpiochip has can_sleep = true. mctrl_gpio_set() calls
gpiod_set_array_value() and mctrl_gpio_get() calls
gpiod_get_value(), and gpiolib does WARN_ON(can_sleep) in both,
so every TIOCMGET/TIOCMSET would give a WARN backtrace.
- mctrl_gpio_init() takes a struct uart_port and its IRQ handler
needs it: uart_port_lock_irqsave(), uart_handle_dcd_change(),
port->icount, delta_msr_wait. This UART is a plain tty_driver
with a tty_port, so only mctrl_gpio_init_noauto() is left - and
the FT260 GPIO lines have no interrupts anyway.
- mctrl_gpio_init_noauto() only picks up lines that exist as
firmware properties: device_property_present(dev, "cts-gpios")
and friends. A gpiod_add_lookup_table() table is the machine
lookup path, so every line would be skipped, all descriptors
would stay NULL and both helpers would silently do nothing.
Software nodes could satisfy that check, but there is no
PROPERTY_ENTRY_GPIO in the tree to build them with.
serial_mctrl_gpio.h is also private to drivers/tty/serial - all
eleven users are serial_core drivers in that directory.
Registering a uart_port instead was tried for this device and
turned down. Daniel Beer's 2022 FT260 UART patch was built on
serial_core and called uart_add_one_port(); Greg asked for
usb-serial, and Johan Hovold answered that "neither USB-serial or
serial (core) is a good fit for such a HID device", pointing at
Christina Quast's tty driver as the right approach - which patch
1 of this series is a port of.
https://lore.kernel.org/lkml/638c51a2.170a0220.3af16.18f8@mx.google.com/
https://lore.kernel.org/lkml/Y6WNl6+ySy8zcSyg@hovoldconsulting.com/
That patch left set_mctrl empty and get_mctrl returning a
constant, which is this same constraint seen from the other side:
uart_ops.set_mctrl and .get_mctrl must not sleep, while every
FT260 line access is a HID feature report over USB.
> This can be a bit delicate in this case since the gpiochip that you
> use for mctrl is also registered in this driver, so you need to
> register the gpiochip *first*, then add a look-up table for the
> GPIOs, then register this modem control.
>
> Then look in e.g. drivers/mfd/sm501.c which is an
> MFD device that register a gpiochip and then consume
> GPIOs from itself.
Agreed on the ordering, and thanks for the reference. The UART
probe currently registers the tty port before the gpiochip, so
that would have to be inverted, and the gpiochip label is built
from the HID device name, so the table would have to be built at
probe rather than being static. Both are workable; they are not
what blocks this. sm501 does not hit the sleeping problem because
its gpiochip is memory mapped.
> The core idea is that the serial modem control should look
> up the GPIOs from its own gpiochip and use the MCTRL
> library helpers, then this should result in very little and
> compact code that is easy to read.
No argument with the goal - I would rather have that than my own
TIOCM handling. But making it usable here means work inside the
serial helpers: cansleep set/get, a path that does not require a
uart_port, a lookup that works without firmware properties, and
the header moved to include/linux. That is a serial subsystem
series to agree with Greg and Jiri Slaby, so I propose keeping
the ioctl implementation in this series and doing the conversion
as a follow-up.
Even then only the set/get helpers would apply: with no GPIO
interrupts, modem status changes come from the FT260's own
interrupt status input report (0xB1), so that part stays in the
driver either way.
Thanks,
Michael
^ permalink raw reply [flat|nested] 43+ messages in thread
* Re: [PATCH 00/13] HID: ft260: add UART and GPIO support, plus I2C fixes
2026-08-27 20:51 ` Lee Jones
@ 2026-08-27 22:25 ` Michael Zaidman
0 siblings, 0 replies; 43+ messages in thread
From: Michael Zaidman @ 2026-08-27 22:25 UTC (permalink / raw)
To: lee
Cc: linusw, jikos, bentiss, brgl, linux-input, linux-gpio, linux-i2c,
linux-kernel
On Thu, 27 Aug 2026 at 21:51 +0100, Lee Jones wrote:
> That's precisely what MFD is. It's one chip, usually with a shared and
> overlapping register spaces, that conducts multiple functions. This is
> no different to any other single-chip device or SoC.
>
> Shoving everything into a single driver isn't how things are done in
> Linux. This should be divided up into the associated sub-systems where
> each part can be reviewed and looked after by the appropriate SMEs.
Understood, and I am not going to argue MFD scope with you. But this
is not specific to my series, so I would rather not decide it here on
my own.
drivers/hid already registers other subsystems' devices from a
hid_driver: hid-cp2112 adds an i2c_adapter and a gpiochip, hid-mcp2221
adds an i2c_adapter, a gpiochip and an IIO device, and hid-ft260 has
hosted the I2C adapter since v5.13, commit 6a82582d9fa4 ("HID: ft260:
add usb hid to i2c host bridge driver").
So the split you are asking for is not a change to this series. It
means moving code that has been in drivers/hid since v5.13 into an MFD
parent with cells, and the same reasoning would apply to cp2112 and
mcp2221. I am willing to discuss that as its own conversion, but it
needs the HID maintainers to agree on the direction first, and I do
not think the UART and GPIO support should wait behind it.
It would also spread the driver over four trees, so a fix touching the
shared chip state becomes a cross-tree series with coordinated merges
between four maintainers - a cost the single driver does not have.
Jiri, Benjamin - this is your call. Do you want FT260 functionality to
keep growing inside hid-ft260, as cp2112 and mcp2221 do today, or do
you want a drivers/hid to MFD conversion for this class of USB HID
bridge chips?
Thanks,
Michael
^ permalink raw reply [flat|nested] 43+ messages in thread
end of thread, other threads:[~2026-08-27 22:26 UTC | newest]
Thread overview: 43+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-22 21:39 [PATCH 00/13] HID: ft260: add UART and GPIO support, plus I2C fixes Michael Zaidman
2026-08-22 21:39 ` [PATCH 01/13] HID: ft260: add serial driver Michael Zaidman
2026-08-22 22:00 ` sashiko-bot
2026-08-25 7:49 ` Linus Walleij
2026-08-25 8:12 ` Linus Walleij
2026-08-27 19:16 ` Michael Zaidman
2026-08-22 21:39 ` [PATCH 02/13] HID: ft260: uart: bring-up fixes Michael Zaidman
2026-08-22 21:56 ` sashiko-bot
2026-08-22 21:39 ` [PATCH 03/13] HID: ft260: add GPIO support on top of UART Michael Zaidman
2026-08-22 21:56 ` sashiko-bot
2026-08-25 7:44 ` Linus Walleij
2026-08-27 20:39 ` Michael Zaidman
2026-08-22 21:39 ` [PATCH 04/13] HID: ft260: i2c: reduce driver module loading time Michael Zaidman
2026-08-22 21:51 ` sashiko-bot
2026-08-22 21:39 ` [PATCH 05/13] HID: ft260: i2c: silence sysfs store big-numbers Michael Zaidman
2026-08-22 21:51 ` sashiko-bot
2026-08-22 21:39 ` [PATCH 06/13] HID: ft260: i2c: reduce bus-error message severity Michael Zaidman
2026-08-22 21:52 ` sashiko-bot
2026-08-22 21:39 ` [PATCH 07/13] HID: ft260: uart: enable flow control Michael Zaidman
2026-08-22 21:52 ` sashiko-bot
2026-08-22 21:39 ` [PATCH 08/13] HID: ft260: uart: add modem pins control via ioctl Michael Zaidman
2026-08-22 21:54 ` sashiko-bot
2026-08-25 8:08 ` Linus Walleij
2026-08-27 22:08 ` Michael Zaidman
2026-08-22 21:39 ` [PATCH 09/13] HID: ft260: gpio: group sysfs attrs per HID interface Michael Zaidman
2026-08-22 21:54 ` sashiko-bot
2026-08-25 8:13 ` Linus Walleij
2026-08-27 20:50 ` Michael Zaidman
2026-08-22 21:39 ` [PATCH 10/13] HID: ft260: uart: fix active-low RTS/CTS/DTR/DSR polarity Michael Zaidman
2026-08-22 22:03 ` sashiko-bot
2026-08-25 8:16 ` Linus Walleij
2026-08-27 21:08 ` Michael Zaidman
2026-08-22 21:39 ` [PATCH 11/13] HID: ft260: i2c: fix large write transaction failure Michael Zaidman
2026-08-22 22:02 ` sashiko-bot
2026-08-22 21:39 ` [PATCH 12/13] HID: ft260: workaround for TN_189 errata endpoint STALL after enumeration Michael Zaidman
2026-08-22 22:03 ` sashiko-bot
2026-08-22 21:39 ` [PATCH 13/13] HID: ft260: i2c: abort in-flight transfers with STOP before reset Michael Zaidman
2026-08-22 22:12 ` sashiko-bot
2026-08-25 8:21 ` [PATCH 00/13] HID: ft260: add UART and GPIO support, plus I2C fixes Linus Walleij
2026-08-27 13:27 ` Lee Jones
2026-08-27 18:53 ` Michael Zaidman
2026-08-27 20:51 ` Lee Jones
2026-08-27 22:25 ` Michael Zaidman
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox