Linux GPIO subsystem development
 help / color / mirror / Atom feed
* [PATCH v2 00/15] HID: ft260: add UART and GPIO support, plus I2C fixes
@ 2026-10-30 22:36 Michael Zaidman
  2026-10-30 22:36 ` [PATCH v2 01/15] HID: ft260: add serial driver Michael Zaidman
                   ` (14 more replies)
  0 siblings, 15 replies; 25+ messages in thread
From: Michael Zaidman @ 2026-10-30 22:36 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: Linus Walleij, Bartosz Golaszewski, Greg Kroah-Hartman,
	Jiri Slaby, Raman Varabets, Germain Hebert, Christina Quast,
	Rio Liu, Bruno Giacomazzi, linux-input, linux-serial, 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 [1].

Patch 1 is Christina Quast's v4 serial driver, ported to the current
I2C code. Patch 2 holds the fixes it needed on hardware. They stay
two patches, as agreed with Jiri, so that each has its own author.
Patches 7 and 8 add flow control and the modem pins.

The v1 thread:
https://lore.kernel.org/all/20260822213941.98882-1-michael.zaidman@gmail.com/T/#t

As said in the v1 thread, this series lands the current driver first.
The MFD split follows as its own series once this one is in the tree.

Changes in v2:

- Patch 1: the UART config enums are split per field and use the
  UART_ infix from the start (Linus Walleij). Lock, read_lock and
  wait are initialised before hid_hw_open(). Lifetime and TTY fixes
  (reported by Sashiko).
- Patch 2: subject now says what it fixes. FT260_UART_RI_DCD_STATUS
  corrected to 0xE2, as in AN_394 section 4.3. 0xB1 length check and
  wake-up timer re-arm (Sashiko).
- Patch 3: subject drops "on top of UART". linux/bits.h, GENMASK()
  and BIT() for the GPIO masks, FT260_GPIO_MAX renamed (Linus
  Walleij). GPIO shadow, combined direction/value report, uart_mode
  OFF (Sashiko).
- Patch 4: timeout of the last I2C read chunk from its length
  (Sashiko).
- Patch 5: a failed feature report is no longer masked; word stores
  use cpu_to_le16() (Sashiko).
- Patch 6: __func__ is printed once (Sashiko).
- Patch 8: v1 patch 10, Rio Liu's RTS/CTS/DTR/DSR polarity fix, is
  folded in, with Co-developed-by. Error paths of tiocmget and
  tiocmset (Sashiko). The TIOCM ioctls stay; the switch to the
  serial_mctrl_gpio helpers needs serial core changes first, as
  discussed with Linus Walleij.
- Patch 9: subject drops "gpio:". i2c_enable moves to the interface
  that owns the gpiochip, so it changes the mask the gpiochip uses.
- Patch 11: the reset work is flushed on unload, and -ENOMEM does not
  trigger a reset (Sashiko).
- Patches 13 to 15 are new: reject an I2C clock outside 60-3400 kHz,
  return the byte count from the i2c_reset store, and recover a bus
  that a slave holds low.

Testing: two FT260 boards, one in chip mode 0x03 with a 24LC512 and
an FT2232 wired to its UART, one with a 24C02. A pytest suite of 202
tests [2] drives sysfs, the gpiochip, the I2C adapter and the tty. It
ran on 7.3.0-rc4 with this series applied, built with PROVE_LOCKING
and DEBUG_ATOMIC_SLEEP, loading the in-tree module. The second board
was strapped to 0x00, 0x01 and 0x02 in turn; each of the 202 tests
passed in at least one of the three runs, with no lockdep report.
Every patch builds with W=1 without warnings.
checkpatch --strict reports no errors and one warning, for a quoted
kernel log line in patch 15.

[1] https://github.com/MichaelZaidman/hid-ft260
[2] https://github.com/MichaelZaidman/hid-ft260/tree/main/tests

Christina Quast (1):
  HID: ft260: add serial driver

Michael Zaidman (14):
  HID: ft260: uart: fix the serial driver
  HID: ft260: add GPIO support
  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: 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
  HID: ft260: i2c: reject an I2C clock outside the supported range
  HID: ft260: i2c: fix i2c_reset sysfs store return value
  HID: ft260: i2c: recover a bus held by a slave

 drivers/hid/Kconfig     |   10 +-
 drivers/hid/hid-ft260.c | 2435 ++++++++++++++++++++++++++++++++++-----
 2 files changed, 2175 insertions(+), 270 deletions(-)


base-commit: 720806515cb6921ab05caaa6f470f7054b56385e
-- 
2.43.0


^ permalink raw reply	[flat|nested] 25+ messages in thread

* [PATCH v2 01/15] HID: ft260: add serial driver
  2026-10-30 22:36 [PATCH v2 00/15] HID: ft260: add UART and GPIO support, plus I2C fixes Michael Zaidman
@ 2026-10-30 22:36 ` Michael Zaidman
  2026-10-30 23:00   ` sashiko-bot
  2026-10-30 22:36 ` [PATCH v2 02/15] HID: ft260: uart: fix the " Michael Zaidman
                   ` (13 subsequent siblings)
  14 siblings, 1 reply; 25+ messages in thread
From: Michael Zaidman @ 2026-10-30 22:36 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: Linus Walleij, Bartosz Golaszewski, Greg Kroah-Hartman,
	Jiri Slaby, Raman Varabets, Germain Hebert, Christina Quast,
	Rio Liu, Bruno Giacomazzi, linux-input, linux-serial, 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 the v4 patch:
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/

Michael Zaidman ported it to the current driver:

- Adapt it to the I2C hardening merged upstream since v4: input-report
  length validation, the read_buf/raw_event race fix with read_lock,
  and SMBus block-read START/STOP handling.
- Make HID_FT260 depend on TTY, because the UART code is part of this
  driver.
- Stop the report dispatch after an I2C input report is consumed.
  Routing UART reports through ft260_raw_event() made v4 also report
  every I2C input report as unhandled.
- Correct the UART probe/remove lifetime, the TTY install, index and
  write return, the baud endianness, and the lock initialisation
  before hid_hw_open() (reported by Sashiko).

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>

---
v2: fold Sashiko-reported lifetime and TTY fixes into this patch.
---
 drivers/hid/Kconfig     |  10 +-
 drivers/hid/hid-ft260.c | 853 +++++++++++++++++++++++++++++++++++++---
 2 files changed, 802 insertions(+), 61 deletions(-)

diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig
index 2481bc01c435..670bce481c42 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..982e2ca7a2de 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,71 @@ 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 flow control */
+enum {
+	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,
+};
+
+/* UART data bits */
+enum {
+	FT260_UART_CFG_DATA_BITS_7		= 0x07,
+	FT260_UART_CFG_DATA_BITS_8		= 0x08,
+};
+
+/* UART parity */
+enum {
+	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,
+};
+
+/* UART stop bits */
+enum {
+	FT260_UART_CFG_STOP_ONE_BIT		= 0x00,
+	FT260_UART_CFG_STOP_TWO_BIT		= 0x02,
+};
+
+/* UART break */
+enum {
+	FT260_UART_CFG_BREAKING_NO		= 0x00,
+	FT260_UART_CFG_BREAKING_YES		= 0x01,
+};
+
+#define FT260_UART_CFG_BAUD_MIN 1200
+#define FT260_UART_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 +314,22 @@ 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 +473,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 +703,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 +920,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 +937,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,51 +1101,478 @@ static const struct attribute_group ft260_attr_group = {
 	}
 };
 
-static int ft260_probe(struct hid_device *hdev, const struct hid_device_id *id)
+/***
+ * 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;
-	struct ft260_get_chip_version_report version;
+
+	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_tail(&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);
+
+	tty_port_tty_vhangup(&port->port);
+
+	spin_lock(&port->write_lock);
+	kfifo_free(&port->xmit_fifo);
+	spin_unlock(&port->write_lock);
+}
+
+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;
 
-	if (!hid_is_usb(hdev))
-		return -EINVAL;
+	ret = tty_port_open(&port->port, tty, filp);
 
-	dev = devm_kzalloc(&hdev->dev, sizeof(*dev), GFP_KERNEL);
-	if (!dev)
-		return -ENOMEM;
+	return ret;
+}
 
-	ret = hid_parse(hdev);
-	if (ret) {
-		hid_err(hdev, "failed to parse HID\n");
-		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;
 	}
 
-	ret = hid_hw_start(hdev, 0);
+	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 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_UART_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_UART_CFG_DATA_BITS_8;
+		termios->c_cflag &= ~CSIZE;
+		termios->c_cflag |= CS8;
+		break;
+	default:
+	case CS8:
+		req.data_bit = FT260_UART_CFG_DATA_BITS_8;
+		break;
+	}
+
+	req.stop_bit = (termios->c_cflag & CSTOPB) ?
+		FT260_UART_CFG_STOP_TWO_BIT : FT260_UART_CFG_STOP_ONE_BIT;
+
+	if (termios->c_cflag & PARENB) {
+		req.parity = (termios->c_cflag & PARODD) ?
+			FT260_UART_CFG_PAR_ODD : FT260_UART_CFG_PAR_EVEN;
+	} else {
+		req.parity = FT260_UART_CFG_PAR_NO;
+	}
+
+	baud = tty_termios_baud_rate(termios);
+	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);
+		baud = 9600;
+		if (tty) {
+			tty_encode_baud_rate(tty, baud, baud);
+			tty_kref_put(tty);
+		}
+	}
+	put_unaligned_le32(baud, &req.baudrate);
+
+	if (termios->c_cflag & CRTSCTS)
+		req.flow_ctrl = FT260_UART_CFG_FLOW_CTRL_RTS_CTS;
+	else
+		req.flow_ctrl = FT260_UART_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_UART_CFG_FLOW_CTRL_NONE;
+	req.breaking = FT260_UART_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;
+
+	if (!port)
+		return -ENODEV;
+
+	ret = tty_standard_install(driver, tty);
 	if (ret) {
-		hid_err(hdev, "failed to start HID HW\n");
+		ft260_uart_port_put(port);
 		return ret;
 	}
 
-	ret = hid_hw_open(hdev);
-	if (ret) {
-		hid_err(hdev, "failed to open HID HW\n");
-		goto err_hid_stop;
+	tty->driver_data = port;
+	return 0;
+}
+
+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;
+}
 
-	ret = ft260_hid_feature_report_get(hdev, FT260_CHIP_VERSION,
-					   (u8 *)&version, sizeof(version));
-	if (ret < 0) {
-		hid_err(hdev, "failed to retrieve chip version\n");
-		goto err_hid_close;
+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));
 	}
+}
 
-	hid_info(hdev, "chip code: %02x%02x %02x%02x\n",
-		 version.chip_code[0], version.chip_code[1],
-		 version.chip_code[2], version.chip_code[3]);
+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;
 
-	ret = ft260_is_interface_enabled(hdev);
-	if (ret <= 0)
-		goto err_hid_close;
+	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,
@@ -1061,10 +1588,6 @@ static int ft260_probe(struct hid_device *hdev, const struct hid_device_id *id)
 	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);
@@ -1073,7 +1596,7 @@ static int ft260_probe(struct hid_device *hdev, const struct hid_device_id *id)
 	ret = i2c_add_adapter(&dev->adap);
 	if (ret) {
 		hid_err(hdev, "failed to add i2c adapter\n");
-		goto err_hid_close;
+		return ret;
 	}
 
 	ret = sysfs_create_group(&hdev->dev.kobj, &ft260_attr_group);
@@ -1081,15 +1604,157 @@ static int ft260_probe(struct hid_device *hdev, const struct hid_device_id *id)
 		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");
+		goto err_port;
+	}
+	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_UART_CFG_FLOW_CTRL_NONE;
+	put_unaligned_le32(9600, &req.baudrate);
+	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) {
+		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);
+err_port:
+	timer_delete_sync(&dev->wakeup_timer);
+	return ret;
+}
+
+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;
+	int ret;
+
+	if (!hid_is_usb(hdev))
+		return -EINVAL;
+
+	/* 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);
+	mutex_init(&dev->lock);
+	spin_lock_init(&dev->read_lock);
+	init_completion(&dev->wait);
+
+	ret = hid_parse(hdev);
+	if (ret) {
+		hid_err(hdev, "failed to parse HID\n");
+		goto hid_fail;
+	}
+
+	ret = hid_hw_start(hdev, 0);
+	if (ret) {
+		hid_err(hdev, "failed to start HID HW\n");
+		goto hid_fail;
+	}
+
+	ret = hid_hw_open(hdev);
+	if (ret) {
+		hid_err(hdev, "failed to open HID HW\n");
+		goto err_hid_stop;
+	}
+
+	ret = ft260_hid_feature_report_get(hdev, FT260_CHIP_VERSION,
+					   (u8 *)&version, sizeof(version));
+	if (ret < 0) {
+		hid_err(hdev, "failed to retrieve chip version\n");
+		goto err_hid_close;
+	}
+
+	hid_info(hdev, "chip code: %02x%02x %02x%02x\n",
+		 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;
+
+	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_uart;
+	}
+
+	return 0;
+
+err_uart:
+	hid_hw_close(hdev);
+	hid_hw_stop(hdev);
+	tty_port_put(&dev->port);
+	return ret;
+
 err_hid_close:
 	hid_hw_close(hdev);
 err_hid_stop:
 	hid_hw_stop(hdev);
+hid_fail:
+	kfree(dev);
+alloc_fail:
 	return ret;
 }
 
@@ -1100,21 +1765,34 @@ 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) {
+		timer_delete_sync(&dev->wakeup_timer);
+		cancel_work_sync(&dev->wakeup_work);
+		tty_port_unregister_device(&dev->port, ft260_tty_driver,
+					   dev->index);
+		ft260_uart_port_remove(dev);
+	} else {
+		sysfs_remove_group(&hdev->dev.kobj, &ft260_attr_group);
+		i2c_del_adapter(&dev->adap);
+	}
 
 	hid_hw_close(hdev);
 	hid_hw_stop(hdev);
+
+	if (dev->ft260_is_serial)
+		ft260_uart_port_put(dev);
+	else
+		kfree(dev);
 }
 
 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 +1805,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 +1835,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 +1857,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] 25+ messages in thread

* [PATCH v2 02/15] HID: ft260: uart: fix the serial driver
  2026-10-30 22:36 [PATCH v2 00/15] HID: ft260: add UART and GPIO support, plus I2C fixes Michael Zaidman
  2026-10-30 22:36 ` [PATCH v2 01/15] HID: ft260: add serial driver Michael Zaidman
@ 2026-10-30 22:36 ` Michael Zaidman
  2026-10-30 23:03   ` sashiko-bot
  2026-10-30 22:36 ` [PATCH v2 03/15] HID: ft260: add GPIO support Michael Zaidman
                   ` (12 subsequent siblings)
  14 siblings, 1 reply; 25+ messages in thread
From: Michael Zaidman @ 2026-10-30 22:36 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: Linus Walleij, Bartosz Golaszewski, Greg Kroah-Hartman,
	Jiri Slaby, Raman Varabets, Germain Hebert, Christina Quast,
	Rio Liu, Bruno Giacomazzi, linux-input, linux-serial, linux-gpio,
	linux-i2c, linux-kernel, Michael Zaidman

Fix the serial driver added by the previous patch, a port of
Christina Quast's v4, so that it works reliably on hardware.

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: replace the kfifo_*_locked aliases with
kfifo_*_spinlocked, and drop the parentheses around single-value
defines, which coding-style asks for only when the value is an
expression.

This replaces the UART bring-up fix series:
https://lore.kernel.org/all/20240210215147.77629-1-michael.zaidman@gmail.com/

Do not treat UART interrupt status (0xB1) as a payload length. When
re-enabling the power-save wakeup workaround after a baud change,
re-arm the timer (reported by Sashiko).

Report 0xE1 is defined as FT260_UART_RI_DCD_STATUS, but AN_394 section
4.3 lists 0xE2 for "UART RI and DCD Status". Correct the value. The
macro has no user, because the DCD and RI levels arrive in the 0xB1
interrupt report, which ft260_raw_event() handles.

Signed-off-by: Michael Zaidman <michael.zaidman@gmail.com>

---
v2: 0xB1 length check and wakeup timer re-arm (reported by Sashiko).
---
 drivers/hid/hid-ft260.c | 332 +++++++++++++++++++++++-----------------
 1 file changed, 189 insertions(+), 143 deletions(-)

diff --git a/drivers/hid/hid-ft260.c b/drivers/hid/hid-ft260.c
index 982e2ca7a2de..c859486cb28c 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,11 +34,14 @@ 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 */
+#define FT260_WAKEUP_NEEDED_AFTER_MS 4800 /* 5s minus 200ms margin */
 
 /*
  * The ft260 input report format defines 62 bytes for the data payload, but
@@ -55,8 +54,9 @@ MODULE_PARM_DESC(debug, "Toggle FT260 debugging messages");
  * 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)
+#define FT260_RD_DATA_MAX 180
+#define FT260_WR_I2C_DATA_MAX 60
+#define FT260_WR_UART_DATA_MAX 62
 
 /*
  * Device interface configuration.
@@ -90,8 +90,8 @@ enum {
 	FT260_I2C_REPORT_MAX		= 0xDE,
 	FT260_GPIO			= 0xB0,
 	FT260_UART_INTERRUPT_STATUS	= 0xB1,
-	FT260_UART_STATUS		= 0xE0,
-	FT260_UART_RI_DCD_STATUS	= 0xE1,
+	FT260_UART_SETTINGS		= 0xE0,
+	FT260_UART_RI_DCD_STATUS	= 0xE2,
 	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 */
@@ -304,6 +317,11 @@ enum {
 #define FT260_UART_CFG_BAUD_MIN 1200
 #define FT260_UART_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) },
@@ -314,25 +332,23 @@ 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;
+	/* Protects xmit_fifo */
+	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;
@@ -343,8 +359,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;
@@ -473,8 +488,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;
 	}
@@ -516,7 +529,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;
@@ -524,12 +537,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);
@@ -565,7 +578,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;
@@ -585,6 +598,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;
 }
@@ -703,8 +718,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,
@@ -937,27 +951,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;
 }
 
@@ -1101,12 +1113,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;
@@ -1123,8 +1159,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);
@@ -1156,12 +1192,12 @@ static void ft260_uart_port_remove(struct ft260_device *port)
 
 	tty_port_tty_vhangup(&port->port);
 
-	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);
 }
 
-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;
 
@@ -1217,30 +1253,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:
@@ -1248,20 +1278,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)
@@ -1271,18 +1295,18 @@ 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;
 
-	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 len;
 }
@@ -1291,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)
@@ -1307,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));
@@ -1321,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_UART_CFG_DATA_BITS_8;
 		termios->c_cflag &= ~CSIZE;
 		termios->c_cflag |= CS8;
@@ -1346,13 +1371,21 @@ static int ft260_uart_change_speed(struct ft260_device *port,
 	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);
+		hid_err(hdev, "invalid baud rate %d\n", baud);
 		baud = 9600;
 		if (tty) {
 			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);
+	if (port->reschedule_work)
+		mod_timer(&port->wakeup_timer, jiffies +
+			  msecs_to_jiffies(FT260_WAKEUP_NEEDED_AFTER_MS));
+
 	put_unaligned_le32(baud, &req.baudrate);
 
 	if (termios->c_cflag & CRTSCTS)
@@ -1360,7 +1393,7 @@ static int ft260_uart_change_speed(struct ft260_device *port,
 	else
 		req.flow_ctrl = FT260_UART_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,
@@ -1371,7 +1404,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;
 }
@@ -1425,8 +1458,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);
 
@@ -1481,15 +1514,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)
 {
@@ -1503,53 +1537,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 *port =
-		container_of(tport, struct ft260_device, port);
+	struct ft260_device *dev =
+		container_of(work, struct ft260_device, wakeup_work);
 
-	port->reschedule_work = false;
+	ft260_uart_wakeup(dev);
 }
 
-static int ft260_uart_activate(struct tty_port *tport, struct tty_struct *tty)
+static void ft260_uart_port_shutdown(struct tty_port *tport)
 {
 	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.
-	 */
+	ft260_uart_wakeup_workaraund_enable(port, false);
+}
+
+static int ft260_uart_port_activate(struct tty_port *tport, struct tty_struct *tty)
+{
+	int ret;
+	int baudrate;
+	struct ft260_get_uart_settings_report cfg;
+	struct ft260_device *port = container_of(tport, struct ft260_device, 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;
 }
@@ -1563,8 +1612,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,
 };
 
@@ -1613,14 +1662,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);
 
@@ -1641,10 +1689,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_UART_CFG_FLOW_CTRL_NONE;
@@ -1656,8 +1704,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;
 	}
 
@@ -1680,9 +1727,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) {
@@ -1730,7 +1777,7 @@ static int ft260_probe(struct hid_device *hdev, const struct hid_device_id *id)
 	hid_set_drvdata(hdev, dev);
 	dev->hdev = hdev;
 
-	if (!dev->ft260_is_serial) {
+	if (dev->iface_type == FT260_IFACE_I2C) {
 		ret = ft260_i2c_probe(hdev, dev);
 		if (ret)
 			goto err_hid_close;
@@ -1765,7 +1812,7 @@ static void ft260_remove(struct hid_device *hdev)
 	if (!dev)
 		return;
 
-	if (dev->ft260_is_serial) {
+	if (dev->iface_type == FT260_IFACE_UART) {
 		timer_delete_sync(&dev->wakeup_timer);
 		cancel_work_sync(&dev->wakeup_work);
 		tty_port_unregister_device(&dev->port, ft260_tty_driver,
@@ -1779,7 +1826,7 @@ static void ft260_remove(struct hid_device *hdev)
 	hid_hw_close(hdev);
 	hid_hw_stop(hdev);
 
-	if (dev->ft260_is_serial)
+	if (dev->iface_type == FT260_IFACE_UART)
 		ft260_uart_port_put(dev);
 	else
 		kfree(dev);
@@ -1797,6 +1844,13 @@ static int ft260_raw_event(struct hid_device *hdev, struct hid_report *report,
 		return -1;
 	}
 
+	if (xfer->report != FT260_UART_INTERRUPT_STATUS &&
+	    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;
@@ -1804,13 +1858,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.
@@ -1837,12 +1884,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);
 
@@ -1887,7 +1933,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;
@@ -1905,7 +1951,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] 25+ messages in thread

* [PATCH v2 03/15] HID: ft260: add GPIO support
  2026-10-30 22:36 [PATCH v2 00/15] HID: ft260: add UART and GPIO support, plus I2C fixes Michael Zaidman
  2026-10-30 22:36 ` [PATCH v2 01/15] HID: ft260: add serial driver Michael Zaidman
  2026-10-30 22:36 ` [PATCH v2 02/15] HID: ft260: uart: fix the " Michael Zaidman
@ 2026-10-30 22:36 ` Michael Zaidman
  2026-10-30 22:53   ` sashiko-bot
  2026-10-30 22:36 ` [PATCH v2 04/15] HID: ft260: i2c: reduce driver module loading time Michael Zaidman
                   ` (11 subsequent siblings)
  14 siblings, 1 reply; 25+ messages in thread
From: Michael Zaidman @ 2026-10-30 22:36 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: Linus Walleij, Bartosz Golaszewski, Greg Kroah-Hartman,
	Jiri Slaby, Raman Varabets, Germain Hebert, Christina Quast,
	Rio Liu, Bruno Giacomazzi, linux-input, linux-serial, 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>.

Read the current GPIO state before writing so a set does not clobber
other pins with a zero shadow. Apply direction and value in one
feature report, and return a failed value write to gpiolib. UART mode
OFF clears only UART-owned bits. Reject an out-of-range uart_mode
index (reported by Sashiko).

Signed-off-by: Michael Zaidman <michael.zaidman@gmail.com>

---
v2: GPIO shadow, combined dir/value report, uart_mode OFF/index
(reported by Sashiko).
---
 drivers/hid/Kconfig     |  10 +-
 drivers/hid/hid-ft260.c | 607 +++++++++++++++++++++++++++++++++++++---
 2 files changed, 570 insertions(+), 47 deletions(-)

diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig
index 670bce481c42..122a5b816d2c 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 c859486cb28c..e24f3151fdaf 100644
--- a/drivers/hid/hid-ft260.c
+++ b/drivers/hid/hid-ft260.c
@@ -19,6 +19,8 @@
 #include <linux/tty_flip.h>
 #include <linux/minmax.h>
 #include <linux/unaligned.h>
+#include <linux/bits.h>
+#include <linux/gpio/driver.h>
 
 #ifdef DEBUG
 static int ft260_debug = 1;
@@ -57,6 +59,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_0_5_COUNT 6
+#define FT260_GPIO_A_H_COUNT 8
+#define FT260_GPIO_TOTAL (FT260_GPIO_0_5_COUNT + FT260_GPIO_A_H_COUNT)
+#define FT260_GPIO_MASK GENMASK(FT260_GPIO_TOTAL - 1, 0)
 
 /*
  * Device interface configuration.
@@ -151,7 +158,65 @@ 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 */
+#define FT260_GPIO_0		BIT(0)
+#define FT260_GPIO_1		BIT(1)
+#define FT260_GPIO_2		BIT(2)
+#define FT260_GPIO_3		BIT(3)
+#define FT260_GPIO_4		BIT(4)
+#define FT260_GPIO_5		BIT(5)
+#define FT260_GPIO_A		BIT(FT260_GPIO_0_5_COUNT + 0)
+#define FT260_GPIO_B		BIT(FT260_GPIO_0_5_COUNT + 1)
+#define FT260_GPIO_C		BIT(FT260_GPIO_0_5_COUNT + 2)
+#define FT260_GPIO_D		BIT(FT260_GPIO_0_5_COUNT + 3)
+#define FT260_GPIO_E		BIT(FT260_GPIO_0_5_COUNT + 4)
+#define FT260_GPIO_F		BIT(FT260_GPIO_0_5_COUNT + 5)
+#define FT260_GPIO_G		BIT(FT260_GPIO_0_5_COUNT + 6)
+#define FT260_GPIO_H		BIT(FT260_GPIO_0_5_COUNT + 7)
+
+/* GPIO groups */
+#define FT260_GPIO_WAKEUP		FT260_GPIO_3
+#define FT260_GPIO_I2C_DEFAULT		(FT260_GPIO_0 | FT260_GPIO_1)
+#define FT260_GPIO_UART_RX_TX		(FT260_GPIO_C | FT260_GPIO_D)
+#define FT260_GPIO_UART_DCD_RI		(FT260_GPIO_4 | FT260_GPIO_5)
+#define FT260_GPIO_UART_RTS_CTS		(FT260_GPIO_B | FT260_GPIO_E)
+#define FT260_GPIO_UART_DTR_DSR		(FT260_GPIO_F | FT260_GPIO_H)
+#define 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)
+#define FT260_GPIO_UART_MODE_1_SET	FT260_GPIO_UART_DTR_DSR
+#define FT260_GPIO_UART_MODE_2_SET	FT260_GPIO_UART_RTS_CTS
+#define FT260_GPIO_UART_MODE_3_SET	(FT260_GPIO_UART_RTS_CTS | \
+					 FT260_GPIO_UART_DTR_DSR)
+#define FT260_GPIO_UART_MODE_4_SET	FT260_GPIO_UART_MODE_3_SET
+#define FT260_GPIO_UART_DEFAULT		FT260_GPIO_UART_MODE_0_SET
+#define FT260_GPIO_UART_MODE_1_CLR	(FT260_GPIO_UART_RX_TX | \
+					 FT260_GPIO_UART_RTS_CTS)
+#define FT260_GPIO_UART_MODE_2_CLR	(FT260_GPIO_UART_RX_TX | \
+					 FT260_GPIO_UART_DTR_DSR)
+#define FT260_GPIO_UART_MODE_3_CLR	FT260_GPIO_UART_RX_TX
+#define FT260_GPIO_UART_MODES		5
+
+#define FT260_SET_REQUEST_VALUE(report_id) ((FT260_FEATURE << 8) | (report_id))
 
 /* Feature In reports */
 
@@ -171,10 +236,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 +267,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 +300,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 +317,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 {
@@ -333,6 +443,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 */
@@ -356,6 +467,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,
@@ -388,8 +503,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);
 
@@ -402,6 +515,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));
@@ -919,6 +1033,347 @@ 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 = FT260_GPIO_UART_MODE_0_SET;
+			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_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\n", offset, value);
+
+	mutex_lock(&dev->lock);
+
+	if (!(dev->gpio_en & BIT(offset))) {
+		hid_err(hdev, "%s: wrong pin function %d\n", __func__, offset);
+		ret = -EINVAL;
+		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.report = FT260_GPIO;
+	rep.gpio = buf.gpio;
+
+	if (offset < FT260_GPIO_0_5_COUNT) {
+		if (value)
+			rep.gpio.vals |= !!value << offset;
+		else
+			rep.gpio.vals &= ~BIT(offset);
+	} else {
+		offset = offset - FT260_GPIO_0_5_COUNT;
+		if (value)
+			rep.gpio.ex_vals |= !!value << offset;
+		else
+			rep.gpio.ex_vals &= ~BIT(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 & BIT(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_0_5_COUNT) {
+			rep->gpio.dirs |= BIT(offset);
+			if (value)
+				rep->gpio.vals |= BIT(offset);
+			else
+				rep->gpio.vals &= ~BIT(offset);
+		} else {
+			u32 ex = offset - FT260_GPIO_0_5_COUNT;
+
+			rep->gpio.ex_dirs |= BIT(ex);
+			if (value)
+				rep->gpio.ex_vals |= BIT(ex);
+			else
+				rep->gpio.ex_vals &= ~BIT(ex);
+		}
+	} else if (offset < FT260_GPIO_0_5_COUNT) {
+		rep->gpio.dirs &= ~BIT(offset);
+	} else {
+		rep->gpio.ex_dirs &= ~BIT(offset - FT260_GPIO_0_5_COUNT);
+	}
+
+	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;
+	ret = 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_0_5_COUNT) | rep.gpio.vals;
+	else
+		ret = (rep.gpio.ex_dirs << FT260_GPIO_0_5_COUNT) | 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 ft260_gpio_read_request_report gbuf;
+	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) {
+		if (cfg->uart_mode < FT260_GPIO_UART_MODES)
+			dev->gpio_en |= dev->gpio_uart_mode[cfg->uart_mode];
+		else
+			dev->gpio_en |= FT260_GPIO_UART_DEFAULT;
+	} 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 = ft260_hid_feature_report_get(hdev, FT260_GPIO, (u8 *)&gbuf,
+					   sizeof(gbuf));
+	if (ret < 0)
+		hid_warn(hdev, "cannot read GPIO state: %d\n", ret);
+	else
+		dev->gpio = gbuf.gpio;
+
+	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)
 {
@@ -934,29 +1389,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);
+
+	dev->iface_id = usbif->cur_altsetting->desc.bInterfaceNumber;
 
-	ret = ft260_get_system_config(hdev, &cfg);
+	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;
@@ -997,6 +1459,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)   \
@@ -1017,37 +1483,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);
@@ -1061,27 +1532,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,
@@ -1105,7 +1595,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,
@@ -1402,9 +1896,15 @@ static int ft260_uart_change_speed(struct ft260_device *port,
 	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;
 }
@@ -1619,9 +2119,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,
@@ -1648,11 +2150,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:
@@ -1660,14 +2167,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);
@@ -1708,6 +2216,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:
@@ -1723,6 +2245,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))
@@ -1737,6 +2260,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;
 	mutex_init(&dev->lock);
 	spin_lock_init(&dev->read_lock);
 	init_completion(&dev->wait);
@@ -1770,19 +2294,16 @@ 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);
+	ret = ft260_get_interface_type(dev, &cfg);
 	if (ret <= FT260_IFACE_NONE)
 		goto err_hid_close;
 
-	hid_set_drvdata(hdev, dev);
-	dev->hdev = hdev;
-
-	if (dev->iface_type == FT260_IFACE_I2C) {
-		ret = ft260_i2c_probe(hdev, dev);
+	if (ret == FT260_IFACE_I2C) {
+		ret = ft260_i2c_probe(dev, &cfg);
 		if (ret)
 			goto err_hid_close;
 	} else {
-		ret = ft260_uart_probe(hdev, dev);
+		ret = ft260_uart_probe(dev, &cfg);
 		if (ret)
 			goto err_uart;
 	}
@@ -1818,6 +2339,8 @@ static void ft260_remove(struct hid_device *hdev)
 		tty_port_unregister_device(&dev->port, ft260_tty_driver,
 					   dev->index);
 		ft260_uart_port_remove(dev);
+		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] 25+ messages in thread

* [PATCH v2 04/15] HID: ft260: i2c: reduce driver module loading time
  2026-10-30 22:36 [PATCH v2 00/15] HID: ft260: add UART and GPIO support, plus I2C fixes Michael Zaidman
                   ` (2 preceding siblings ...)
  2026-10-30 22:36 ` [PATCH v2 03/15] HID: ft260: add GPIO support Michael Zaidman
@ 2026-10-30 22:36 ` Michael Zaidman
  2026-10-30 22:36 ` [PATCH v2 05/15] HID: ft260: i2c: silence sysfs store big-numbers Michael Zaidman
                   ` (10 subsequent siblings)
  14 siblings, 0 replies; 25+ messages in thread
From: Michael Zaidman @ 2026-10-30 22:36 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: Linus Walleij, Bartosz Golaszewski, Greg Kroah-Hartman,
	Jiri Slaby, Raman Varabets, Germain Hebert, Christina Quast,
	Rio Liu, Bruno Giacomazzi, linux-input, linux-serial, 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 a 180-byte chunk about 27 ms. Scale
the wait by 25 ms per 60-byte HID payload so a last chunk of 61..180
bytes gets 50..75 ms, not the single-report 25 ms budget (reported by
Sashiko).

Signed-off-by: Michael Zaidman <michael.zaidman@gmail.com>

---
v2: last I2C read chunk timeout from rd_len (reported by Sashiko).
---
 drivers/hid/hid-ft260.c | 19 ++++++++++++++-----
 1 file changed, 14 insertions(+), 5 deletions(-)

diff --git a/drivers/hid/hid-ft260.c b/drivers/hid/hid-ft260.c
index e24f3151fdaf..f134fab08285 100644
--- a/drivers/hid/hid-ft260.c
+++ b/drivers/hid/hid-ft260.c
@@ -17,6 +17,7 @@
 #include <linux/serial_core.h>
 #include <linux/kfifo.h>
 #include <linux/tty_flip.h>
+#include <linux/math.h>
 #include <linux/minmax.h>
 #include <linux/unaligned.h>
 #include <linux/bits.h>
@@ -49,14 +50,20 @@ MODULE_PARM_DESC(debug, "Toggle FT260 debugging messages");
  * The ft260 input report format defines 62 bytes for the data payload, but
  * when requested 62 bytes, the controller returns 60 and 2 in separate input
  * reports. To achieve better performance with the multi-report read data
- * transfers, we set the maximum read payload length to a multiple of 60.
+ * transfers, we set the maximum read payload length to a multiple of
+ * FT260_RD_ONE_REPORT_MAX.
  * With a 100 kHz I2C clock, one 240 bytes read takes about 1/27 second,
  * which is excessive; On the other hand, some higher layer drivers like at24
  * 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_ONE_REPORT_MAX 60
 #define FT260_RD_DATA_MAX 180
+
+/* Time in ms to wait for a single HID input report at ONE_REPORT_MAX */
+#define FT260_RD_ONE_REPORT_TO 25
+
 #define FT260_WR_I2C_DATA_MAX 60
 #define FT260_WR_UART_DATA_MAX 62
 #define FT260_GPIOCHIP "ft260_gpio"
@@ -722,8 +729,8 @@ static int ft260_i2c_read(struct ft260_device *dev, u8 addr, u8 *data,
 			  u16 len, u8 flag)
 {
 	u16 rd_len;
-	u16 rd_data_max = 60;
-	int timeout, ret = 0;
+	u16 rd_data_max = FT260_RD_ONE_REPORT_MAX;
+	int timeout, timeout_jiffies, ret = 0;
 	struct ft260_i2c_read_request_report rep;
 	struct hid_device *hdev = dev->hdev;
 	unsigned long irqflags;
@@ -748,6 +755,8 @@ static int ft260_i2c_read(struct ft260_device *dev, u8 addr, u8 *data,
 		} else {
 			rd_len = rd_data_max;
 		}
+		timeout = FT260_RD_ONE_REPORT_TO *
+			  DIV_ROUND_UP(rd_len, FT260_RD_ONE_REPORT_MAX);
 		rd_data_max = FT260_RD_DATA_MAX;
 
 		rep.report = FT260_I2C_READ_REQ;
@@ -772,8 +781,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] 25+ messages in thread

* [PATCH v2 05/15] HID: ft260: i2c: silence sysfs store big-numbers
  2026-10-30 22:36 [PATCH v2 00/15] HID: ft260: add UART and GPIO support, plus I2C fixes Michael Zaidman
                   ` (3 preceding siblings ...)
  2026-10-30 22:36 ` [PATCH v2 04/15] HID: ft260: i2c: reduce driver module loading time Michael Zaidman
@ 2026-10-30 22:36 ` Michael Zaidman
  2026-10-30 22:47   ` sashiko-bot
  2026-10-30 22:36 ` [PATCH v2 06/15] HID: ft260: i2c: reduce bus-error message severity Michael Zaidman
                   ` (9 subsequent siblings)
  14 siblings, 1 reply; 25+ messages in thread
From: Michael Zaidman @ 2026-10-30 22:36 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: Linus Walleij, Bartosz Golaszewski, Greg Kroah-Hartman,
	Jiri Slaby, Raman Varabets, Germain Hebert, Christina Quast,
	Rio Liu, Bruno Giacomazzi, linux-input, linux-serial, 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.

Do not report success when the feature report fails. Store 16-bit
attributes in little endian (reported by Sashiko).

Signed-off-by: Michael Zaidman <michael.zaidman@gmail.com>

---
v2: do not mask a failed feature report; cpu_to_le16 on word store
(reported by Sashiko).
---
 drivers/hid/hid-ft260.c | 16 ++++++++++------
 1 file changed, 10 insertions(+), 6 deletions(-)

diff --git a/drivers/hid/hid-ft260.c b/drivers/hid/hid-ft260.c
index f134fab08285..d2c5c9ed7927 100644
--- a/drivers/hid/hid-ft260.c
+++ b/drivers/hid/hid-ft260.c
@@ -1471,63 +1471,67 @@ static int ft260_word_show(struct hid_device *hdev, int id, u8 *cfg, int len,
 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)   \
 	{								       \
 		struct reptype rep;					       \
 		struct hid_device *hdev = to_hid_device(kdev);		       \
 		type *field = &rep.name;				       \
 		int len = sizeof(rep);					       \
 									       \
 		return func(hdev, id, (u8 *)&rep, len, field, buf);	       \
 	}
 
 #define FT260_SSTAT_ATTR_SHOW(name)					       \
 		FT260_ATTR_SHOW(name, ft260_get_system_status_report,	       \
 				FT260_SYSTEM_SETTINGS, u8, ft260_byte_show)
 
 #define FT260_I2CST_ATTR_SHOW(name)					       \
 		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, strtou, func)    \
+#define FT260_STORE_NATIVE(v) (v)
+
+#define FT260_ATTR_STORE(name, reptype, id, req, type, strtou, func, conv)     \
 	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 (!strtou(buf, 10, (ctype *)&name)) {			       \
-			rep.name = name;				       \
+		if (!strtou(buf, 10, &name)) {				       \
+			rep.name = conv(name);				       \
 			rep.report = id;				       \
 			rep.request = req;				       \
 			mutex_lock(&dev->lock);				       \
 			ret = ft260_hid_feature_report_set(hdev, (u8 *)&rep,   \
 							   sizeof(rep));       \
 			if (ret < 0)					       \
 				hid_err(hdev, "%s: failed!\n", __func__);      \
-			else						       \
+			else {						       \
 				func(hdev, req, name);			       \
+				ret = count;				       \
+			}						       \
 			mutex_unlock(&dev->lock);			       \
 		} else {						       \
 			ret = -EINVAL;					       \
 		}							       \
 		return ret;						       \
 	}
 
 #define FT260_BYTE_ATTR_STORE(name, reptype, req, func)			       \
 		FT260_ATTR_STORE(name, reptype, FT260_SYSTEM_SETTINGS, req,    \
-				 u8, u8, kstrtou8, func)
+				 u8, kstrtou8, func, FT260_STORE_NATIVE)
 
 #define FT260_WORD_ATTR_STORE(name, reptype, req, func)			       \
 		FT260_ATTR_STORE(name, reptype, FT260_SYSTEM_SETTINGS, req,    \
-				 __le16, u16, kstrtou16, func)
+				 u16, kstrtou16, func, cpu_to_le16)
 
 FT260_SSTAT_ATTR_SHOW(chip_mode);
 static DEVICE_ATTR_RO(chip_mode);
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 25+ messages in thread

* [PATCH v2 06/15] HID: ft260: i2c: reduce bus-error message severity
  2026-10-30 22:36 [PATCH v2 00/15] HID: ft260: add UART and GPIO support, plus I2C fixes Michael Zaidman
                   ` (4 preceding siblings ...)
  2026-10-30 22:36 ` [PATCH v2 05/15] HID: ft260: i2c: silence sysfs store big-numbers Michael Zaidman
@ 2026-10-30 22:36 ` Michael Zaidman
  2026-10-30 22:44   ` sashiko-bot
  2026-10-30 22:36 ` [PATCH v2 07/15] HID: ft260: uart: enable flow control Michael Zaidman
                   ` (8 subsequent siblings)
  14 siblings, 1 reply; 25+ messages in thread
From: Michael Zaidman @ 2026-10-30 22:36 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: Linus Walleij, Bartosz Golaszewski, Greg Kroah-Hartman,
	Jiri Slaby, Raman Varabets, Germain Hebert, Christina Quast,
	Rio Liu, Bruno Giacomazzi, linux-input, linux-serial, 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.

Drop the extra __func__ from the write-path ft260_dbg calls; the helper
already prints the function name (reported by Sashiko).

Signed-off-by: Michael Zaidman <michael.zaidman@gmail.com>

---
v2: do not print __func__ twice through ft260_dbg (reported by Sashiko).
---
 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 d2c5c9ed7927..6f8d72938702 100644
--- a/drivers/hid/hid-ft260.c
+++ b/drivers/hid/hid-ft260.c
@@ -575,7 +575,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;
 	}
 
@@ -609,6 +609,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;
 	}
@@ -648,7 +649,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;
 
@@ -679,7 +679,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("failed with %d\n", ret);
 			return ret;
 		}
 
@@ -695,7 +695,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 =
@@ -720,7 +720,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("failed with %d\n", ret);
 
 	return ret;
 }
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 25+ messages in thread

* [PATCH v2 07/15] HID: ft260: uart: enable flow control
  2026-10-30 22:36 [PATCH v2 00/15] HID: ft260: add UART and GPIO support, plus I2C fixes Michael Zaidman
                   ` (5 preceding siblings ...)
  2026-10-30 22:36 ` [PATCH v2 06/15] HID: ft260: i2c: reduce bus-error message severity Michael Zaidman
@ 2026-10-30 22:36 ` Michael Zaidman
  2026-10-30 22:36 ` [PATCH v2 08/15] HID: ft260: uart: add modem pins control via ioctl Michael Zaidman
                   ` (7 subsequent siblings)
  14 siblings, 0 replies; 25+ messages in thread
From: Michael Zaidman @ 2026-10-30 22:36 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: Linus Walleij, Bartosz Golaszewski, Greg Kroah-Hartman,
	Jiri Slaby, Raman Varabets, Germain Hebert, Christina Quast,
	Rio Liu, Bruno Giacomazzi, linux-input, linux-serial, 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 6f8d72938702..4b0b55c98f90 100644
--- a/drivers/hid/hid-ft260.c
+++ b/drivers/hid/hid-ft260.c
@@ -1897,8 +1897,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);
@@ -1906,9 +1910,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] 25+ messages in thread

* [PATCH v2 08/15] HID: ft260: uart: add modem pins control via ioctl
  2026-10-30 22:36 [PATCH v2 00/15] HID: ft260: add UART and GPIO support, plus I2C fixes Michael Zaidman
                   ` (6 preceding siblings ...)
  2026-10-30 22:36 ` [PATCH v2 07/15] HID: ft260: uart: enable flow control Michael Zaidman
@ 2026-10-30 22:36 ` Michael Zaidman
  2026-10-30 22:55   ` sashiko-bot
  2026-10-30 22:36 ` [PATCH v2 09/15] HID: ft260: group sysfs attrs per HID interface Michael Zaidman
                   ` (6 subsequent siblings)
  14 siblings, 1 reply; 25+ messages in thread
From: Michael Zaidman @ 2026-10-30 22:36 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: Linus Walleij, Bartosz Golaszewski, Greg Kroah-Hartman,
	Jiri Slaby, Raman Varabets, Germain Hebert, Christina Quast,
	Rio Liu, Bruno Giacomazzi, linux-input, linux-serial, 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.

The RTS, CTS, DTR and DSR pins are active-low, so TIOCMGET inverts the
sense of those four lines and TIOCMSET drives 0 to assert them. DCD and
RI are not inverted. Rio Liu found and fixed the polarity.

Keep the errno from ft260_get_interface_type() so a failed status read
does not probe-succeed after teardown. Return -EINVAL when the modem
pins are not in GPIO mode, propagate gpio_get_all() errors from
TIOCMGET, and copy the GPIO shadow under the port lock for TIOCMSET
(reported by Sashiko).

Co-developed-by: Rio Liu <rio@r26.me>
Signed-off-by: Rio Liu <rio@r26.me>
Signed-off-by: Michael Zaidman <michael.zaidman@gmail.com>

---
v2: polarity fix folded in (Rio Liu); preserve get_interface_type
errno; tiocmget/tiocmset error paths and locked shadow copy (reported
by Sashiko).
---
 drivers/hid/hid-ft260.c | 150 +++++++++++++++++++++++++++++++++++-----
 1 file changed, 131 insertions(+), 19 deletions(-)

diff --git a/drivers/hid/hid-ft260.c b/drivers/hid/hid-ft260.c
index 4b0b55c98f90..4106066b6a89 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/math.h>
 #include <linux/minmax.h>
@@ -183,7 +184,7 @@ enum {
 	FT260_GPIO_DIR_OUTPUT		= 0x01,
 };
 
-/* GPIO offsets */
+/* GPIO bit masks */
 #define FT260_GPIO_0		BIT(0)
 #define FT260_GPIO_1		BIT(1)
 #define FT260_GPIO_2		BIT(2)
@@ -213,7 +214,8 @@ enum {
 #define FT260_GPIO_UART_MODE_1_SET	FT260_GPIO_UART_DTR_DSR
 #define FT260_GPIO_UART_MODE_2_SET	FT260_GPIO_UART_RTS_CTS
 #define 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)
 #define FT260_GPIO_UART_MODE_4_SET	FT260_GPIO_UART_MODE_3_SET
 #define FT260_GPIO_UART_DEFAULT		FT260_GPIO_UART_MODE_0_SET
 #define FT260_GPIO_UART_MODE_1_CLR	(FT260_GPIO_UART_RX_TX | \
@@ -223,6 +225,16 @@ enum {
 #define FT260_GPIO_UART_MODE_3_CLR	FT260_GPIO_UART_RX_TX
 #define 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 */
@@ -449,6 +461,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;
@@ -1106,7 +1119,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_0_5_COUNT) {
+		gpio->dirs |= BIT(offset);
+		if (value)
+			gpio->vals |= !!value << offset;
+		else
+			gpio->vals &= ~BIT(offset);
+	} else {
+		offset = offset - FT260_GPIO_0_5_COUNT;
+		gpio->ex_dirs |= BIT(offset);
+		if (value)
+			gpio->ex_vals |= !!value << offset;
+		else
+			gpio->ex_vals &= ~BIT(offset);
+	}
 }
 
 static int ft260_gpio_set(struct gpio_chip *gc, u32 offset, int value)
@@ -1155,7 +1188,7 @@ static int ft260_gpio_set(struct gpio_chip *gc, u32 offset, int value)
 			rep.gpio.ex_vals &= ~BIT(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);
 
@@ -1307,8 +1340,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;
@@ -1352,7 +1383,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;
@@ -1423,6 +1453,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:
@@ -1440,7 +1471,6 @@ static int ft260_get_interface_type(struct ft260_device *dev,
 		break;
 	}
 
-	dev->iface_type = ret;
 	return ret;
 }
 
@@ -1941,6 +1971,80 @@ 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 -EINVAL;
+	}
+
+	gpios = ft260_gpio_get_all(port->gc, FT260_GPIO_VALUE);
+	if (gpios < 0)
+		return gpios;
+
+	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 -EINVAL;
+	}
+	rep.report = FT260_GPIO;
+
+	mutex_lock(&port->lock);
+	rep.gpio = port->gpio;
+
+	if (set & TIOCM_RTS)
+		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, 0);
+	if (clear & TIOCM_RTS)
+		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, 1);
+
+	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;
@@ -2021,6 +2125,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,
@@ -2164,14 +2270,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;
@@ -2232,7 +2340,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;
@@ -2311,8 +2420,9 @@ static int ft260_probe(struct hid_device *hdev, const struct hid_device_id *id)
 	ret = ft260_get_interface_type(dev, &cfg);
 	if (ret <= FT260_IFACE_NONE)
 		goto err_hid_close;
+	dev->iface_type = ret;
 
-	if (ret == FT260_IFACE_I2C) {
+	if (dev->iface_type == FT260_IFACE_I2C) {
 		ret = ft260_i2c_probe(dev, &cfg);
 		if (ret)
 			goto err_hid_close;
@@ -2353,10 +2463,12 @@ static void ft260_remove(struct hid_device *hdev)
 		tty_port_unregister_device(&dev->port, ft260_tty_driver,
 					   dev->index);
 		ft260_uart_port_remove(dev);
-		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);
 	}
 
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 25+ messages in thread

* [PATCH v2 09/15] HID: ft260: group sysfs attrs per HID interface
  2026-10-30 22:36 [PATCH v2 00/15] HID: ft260: add UART and GPIO support, plus I2C fixes Michael Zaidman
                   ` (7 preceding siblings ...)
  2026-10-30 22:36 ` [PATCH v2 08/15] HID: ft260: uart: add modem pins control via ioctl Michael Zaidman
@ 2026-10-30 22:36 ` Michael Zaidman
  2026-10-30 22:48   ` sashiko-bot
  2026-10-30 22:36 ` [PATCH v2 10/15] HID: ft260: i2c: fix large write transaction failure Michael Zaidman
                   ` (5 subsequent siblings)
  14 siblings, 1 reply; 25+ messages in thread
From: Michael Zaidman @ 2026-10-30 22:36 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: Linus Walleij, Bartosz Golaszewski, Greg Kroah-Hartman,
	Jiri Slaby, Raman Varabets, Germain Hebert, Christina Quast,
	Rio Liu, Bruno Giacomazzi, linux-input, linux-serial, 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): I2C and GPIO attributes on the I2C HID interface
- Dual mode (0/3): hid_over_i2c_en, i2c_reset and clock on the I2C
  interface; GPIO and UART attrs on the UART interface
- UART-only (mode 2): GPIO and UART attrs on the UART interface

Put i2c_enable on the UART group. In modes 0 and 3 ft260_gpio_init()
runs from the UART probe, so that interface registers the gpiochip. A
write to i2c_enable updates dev->gpio_en of the interface that owns
the attribute. On the I2C interface the write would re-mux GPIO0 and
GPIO1 on the chip and leave the gpiochip's mask unchanged, so the two
lines would stay unavailable.

Do not expose i2c_reset on the UART group (reported by Sashiko).

Signed-off-by: Michael Zaidman <michael.zaidman@gmail.com>

---
v2: i2c_enable moves to the UART group, which owns the gpiochip; UART
group drops i2c_reset (reported by Sashiko).
---
 drivers/hid/hid-ft260.c | 60 ++++++++++++++++++++++++++++++++++-------
 1 file changed, 51 insertions(+), 9 deletions(-)

diff --git a/drivers/hid/hid-ft260.c b/drivers/hid/hid-ft260.c
index 4106066b6a89..dca0250f2c25 100644
--- a/drivers/hid/hid-ft260.c
+++ b/drivers/hid/hid-ft260.c
@@ -1630,7 +1630,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,
@@ -1641,8 +1641,6 @@ 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_uart_mode.attr,
-		  &dev_attr_uart_dcd_ri.attr,
 		  &dev_attr_clock_ctl.attr,
 		  &dev_attr_i2c_reset.attr,
 		  &dev_attr_clock.attr,
@@ -1650,6 +1648,32 @@ static const struct attribute_group ft260_attr_group = {
 	}
 };
 
+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_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,
+		  NULL
+	}
+};
+
 static DEFINE_MUTEX(ft260_uart_list_lock);
 static LIST_HEAD(ft260_uart_device_list);
 
@@ -2275,7 +2299,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;
@@ -2341,12 +2374,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;
@@ -2464,11 +2499,18 @@ static void ft260_remove(struct hid_device *hdev)
 					   dev->index);
 		ft260_uart_port_remove(dev);
 		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);
 	}
 
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 25+ messages in thread

* [PATCH v2 10/15] HID: ft260: i2c: fix large write transaction failure
  2026-10-30 22:36 [PATCH v2 00/15] HID: ft260: add UART and GPIO support, plus I2C fixes Michael Zaidman
                   ` (8 preceding siblings ...)
  2026-10-30 22:36 ` [PATCH v2 09/15] HID: ft260: group sysfs attrs per HID interface Michael Zaidman
@ 2026-10-30 22:36 ` Michael Zaidman
  2026-10-30 22:36 ` [PATCH v2 11/15] HID: ft260: workaround for TN_189 errata endpoint STALL after enumeration Michael Zaidman
                   ` (4 subsequent siblings)
  14 siblings, 0 replies; 25+ messages in thread
From: Michael Zaidman @ 2026-10-30 22:36 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: Linus Walleij, Bartosz Golaszewski, Greg Kroah-Hartman,
	Jiri Slaby, Raman Varabets, Germain Hebert, Christina Quast,
	Rio Liu, Bruno Giacomazzi, linux-input, linux-serial, 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 dca0250f2c25..65835e13fa2a 100644
--- a/drivers/hid/hid-ft260.c
+++ b/drivers/hid/hid-ft260.c
@@ -576,9 +576,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;
 
@@ -612,13 +617,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) {
@@ -635,16 +637,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)
@@ -661,6 +653,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;
@@ -673,8 +666,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;
 		}
@@ -690,7 +685,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("failed with %d\n", ret);
 			return ret;
@@ -731,7 +726,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("failed with %d\n", ret);
 
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 25+ messages in thread

* [PATCH v2 11/15] HID: ft260: workaround for TN_189 errata endpoint STALL after enumeration
  2026-10-30 22:36 [PATCH v2 00/15] HID: ft260: add UART and GPIO support, plus I2C fixes Michael Zaidman
                   ` (9 preceding siblings ...)
  2026-10-30 22:36 ` [PATCH v2 10/15] HID: ft260: i2c: fix large write transaction failure Michael Zaidman
@ 2026-10-30 22:36 ` Michael Zaidman
  2026-10-30 22:59   ` sashiko-bot
  2026-10-30 22:36 ` [PATCH v2 12/15] HID: ft260: i2c: abort in-flight transfers with STOP before reset Michael Zaidman
                   ` (3 subsequent siblings)
  14 siblings, 1 reply; 25+ messages in thread
From: Michael Zaidman @ 2026-10-30 22:36 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: Linus Walleij, Bartosz Golaszewski, Greg Kroah-Hartman,
	Jiri Slaby, Raman Varabets, Germain Hebert, Christina Quast,
	Rio Liu, Bruno Giacomazzi, linux-input, linux-serial, 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.

Queue the work on a driver-owned ordered workqueue flushed on module
exit. Do not treat a GFP failure as a TN_189 stall (reported by
Sashiko). Leave the reset requeue uncapped: that is the recovery FTDI
tested.

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>

---
v2: flush reset work on unload; do not reset on -ENOMEM (reported by
Sashiko). Reset requeue left uncapped (FTDI recovery path).
---
 drivers/hid/hid-ft260.c | 245 ++++++++++++++++++++++++++++++++++++++++
 1 file changed, 245 insertions(+)

diff --git a/drivers/hid/hid-ft260.c b/drivers/hid/hid-ft260.c
index 65835e13fa2a..923ce6ee9dcf 100644
--- a/drivers/hid/hid-ft260.c
+++ b/drivers/hid/hid-ft260.c
@@ -12,6 +12,7 @@
 #include <linux/hidraw.h>
 #include <linux/i2c.h>
 #include <linux/module.h>
+#include <linux/workqueue.h>
 #include <linux/usb.h>
 #include <linux/serial.h>
 #include <linux/serial_core.h>
@@ -2396,15 +2397,229 @@ 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 struct workqueue_struct *ft260_reset_wq;
+
+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);
+	queue_work(ft260_reset_wq, &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.
@@ -2432,6 +2647,27 @@ 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 == -ENODEV) {
+		ft260_schedule_reset(usbif);
+		goto err_hid_stop;
+	}
+	if (ret)
+		goto err_hid_stop;
+
+	ret = ft260_check_dev_responsive(hdev);
+	if (ret == -ENODEV) {
+		ft260_schedule_reset(usbif);
+		goto err_hid_stop;
+	}
+	if (ret)
+		goto err_hid_stop;
+
 	ret = hid_hw_open(hdev);
 	if (ret) {
 		hid_err(hdev, "failed to open HID HW\n");
@@ -2621,6 +2857,12 @@ static int __init ft260_driver_init(void)
 		goto err_reg_driver;
 	}
 
+	ft260_reset_wq = alloc_ordered_workqueue("hid-ft260-reset", 0);
+	if (!ft260_reset_wq) {
+		ret = -ENOMEM;
+		goto err_reg_driver_unreg;
+	}
+
 	ret = hid_register_driver(&ft260_driver);
 	if (ret) {
 		pr_err("hid_register_driver failed: %d\n", ret);
@@ -2630,6 +2872,8 @@ static int __init ft260_driver_init(void)
 	return 0;
 
 err_reg_hid:
+	destroy_workqueue(ft260_reset_wq);
+err_reg_driver_unreg:
 	tty_unregister_driver(ft260_tty_driver);
 err_reg_driver:
 	tty_driver_kref_put(ft260_tty_driver);
@@ -2640,6 +2884,7 @@ static int __init ft260_driver_init(void)
 static void __exit ft260_driver_exit(void)
 {
 	hid_unregister_driver(&ft260_driver);
+	destroy_workqueue(ft260_reset_wq);
 	tty_unregister_driver(ft260_tty_driver);
 	tty_driver_kref_put(ft260_tty_driver);
 }
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 25+ messages in thread

* [PATCH v2 12/15] HID: ft260: i2c: abort in-flight transfers with STOP before reset
  2026-10-30 22:36 [PATCH v2 00/15] HID: ft260: add UART and GPIO support, plus I2C fixes Michael Zaidman
                   ` (10 preceding siblings ...)
  2026-10-30 22:36 ` [PATCH v2 11/15] HID: ft260: workaround for TN_189 errata endpoint STALL after enumeration Michael Zaidman
@ 2026-10-30 22:36 ` Michael Zaidman
  2026-10-30 22:36 ` [PATCH v2 13/15] HID: ft260: i2c: reject an I2C clock outside the supported range Michael Zaidman
                   ` (2 subsequent siblings)
  14 siblings, 0 replies; 25+ messages in thread
From: Michael Zaidman @ 2026-10-30 22:36 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: Linus Walleij, Bartosz Golaszewski, Greg Kroah-Hartman,
	Jiri Slaby, Raman Varabets, Germain Hebert, Christina Quast,
	Rio Liu, Bruno Giacomazzi, linux-input, linux-serial, 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 923ce6ee9dcf..50f68790c04d 100644
--- a/drivers/hid/hid-ft260.c
+++ b/drivers/hid/hid-ft260.c
@@ -549,6 +549,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;
@@ -618,7 +645,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;
@@ -647,7 +675,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;
 }
 
@@ -686,7 +714,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("failed with %d\n", ret);
 			return ret;
@@ -727,7 +756,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)
@@ -795,7 +824,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;
 		}
 
@@ -809,7 +838,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;
 		}
 
@@ -993,7 +1022,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] 25+ messages in thread

* [PATCH v2 13/15] HID: ft260: i2c: reject an I2C clock outside the supported range
  2026-10-30 22:36 [PATCH v2 00/15] HID: ft260: add UART and GPIO support, plus I2C fixes Michael Zaidman
                   ` (11 preceding siblings ...)
  2026-10-30 22:36 ` [PATCH v2 12/15] HID: ft260: i2c: abort in-flight transfers with STOP before reset Michael Zaidman
@ 2026-10-30 22:36 ` Michael Zaidman
  2026-10-30 22:36 ` [PATCH v2 14/15] HID: ft260: i2c: fix i2c_reset sysfs store return value Michael Zaidman
  2026-10-30 22:36 ` [PATCH v2 15/15] HID: ft260: i2c: recover a bus held by a slave Michael Zaidman
  14 siblings, 0 replies; 25+ messages in thread
From: Michael Zaidman @ 2026-10-30 22:36 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: Linus Walleij, Bartosz Golaszewski, Greg Kroah-Hartman,
	Jiri Slaby, Raman Varabets, Germain Hebert, Christina Quast,
	Rio Liu, Bruno Giacomazzi, linux-input, linux-serial, linux-gpio,
	linux-i2c, linux-kernel, Michael Zaidman

The clock sysfs attribute passes any 16-bit value to the chip. The chip
accepts it, reports it back in the I2C status report that clock_show
reads, and runs the bus at the 100 kHz default. A read of 2048 bytes
takes the same 0.298 s after writing 50, 3500 or 65535 as it does after
writing 100, while 60 and 3400 give 0.387 s and 0.058 s. Userspace has
no way to see that the value was not applied.

Refuse a value outside the 60-3400 kHz range named in the datasheet
section 4.4.16. ft260_uart_change_speed() checks the baud rate against
FT260_UART_CFG_BAUD_MIN and FT260_UART_CFG_BAUD_MAX, but termios has no
error return, so it falls back to 9600. A sysfs store can return an
error, so the clock store refuses the value.

FT260_WORD_ATTR_STORE() takes the parse function as a parameter now, so
a word attribute with no range of its own keeps passing kstrtou16.

Signed-off-by: Michael Zaidman <michael.zaidman@gmail.com>
---
 drivers/hid/hid-ft260.c | 31 ++++++++++++++++++++++++++++---
 1 file changed, 28 insertions(+), 3 deletions(-)

diff --git a/drivers/hid/hid-ft260.c b/drivers/hid/hid-ft260.c
index 50f68790c04d..f7d01d9e07f7 100644
--- a/drivers/hid/hid-ft260.c
+++ b/drivers/hid/hid-ft260.c
@@ -68,6 +68,11 @@ MODULE_PARM_DESC(debug, "Toggle FT260 debugging messages");
 
 #define FT260_WR_I2C_DATA_MAX 60
 #define FT260_WR_UART_DATA_MAX 62
+
+/* I2C bus clock range in kHz, see the FT260 datasheet section 4.4.16 */
+#define FT260_I2C_CLOCK_MIN 60
+#define FT260_I2C_CLOCK_MAX 3400
+
 #define FT260_GPIOCHIP "ft260_gpio"
 #define FT260_GPIO_0_5_COUNT 6
 #define FT260_GPIO_A_H_COUNT 8
@@ -1529,6 +1534,25 @@ static void ft260_attr_dummy_func(struct hid_device *hdev, u8 req, u16 value)
 {
 }
 
+/*
+ * The chip takes any 16-bit value and reports it back in the I2C status
+ * report, but runs the bus at the 100 kHz default when the value is
+ * outside the documented range. The report gives no way to tell the two
+ * cases apart, so refuse the value here.
+ */
+static int ft260_i2c_clock_parse(const char *buf, unsigned int base, u16 *clock)
+{
+	int ret = kstrtou16(buf, base, clock);
+
+	if (ret)
+		return ret;
+
+	if (*clock < FT260_I2C_CLOCK_MIN || *clock > FT260_I2C_CLOCK_MAX)
+		return -EINVAL;
+
+	return 0;
+}
+
 #define FT260_ATTR_SHOW(name, reptype, id, type, func)			       \
 	static ssize_t name##_show(struct device *kdev,			       \
 				   struct device_attribute *attr, char *buf)   \
@@ -1586,9 +1610,9 @@ static void ft260_attr_dummy_func(struct hid_device *hdev, u8 req, u16 value)
 		FT260_ATTR_STORE(name, reptype, FT260_SYSTEM_SETTINGS, req,    \
 				 u8, kstrtou8, func, FT260_STORE_NATIVE)
 
-#define FT260_WORD_ATTR_STORE(name, reptype, req, func)			       \
+#define FT260_WORD_ATTR_STORE(name, reptype, req, strtou, func)		       \
 		FT260_ATTR_STORE(name, reptype, FT260_SYSTEM_SETTINGS, req,    \
-				 u16, kstrtou16, func, cpu_to_le16)
+				 u16, strtou, func, cpu_to_le16)
 
 FT260_SSTAT_ATTR_SHOW(chip_mode);
 static DEVICE_ATTR_RO(chip_mode);
@@ -1641,7 +1665,8 @@ 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_attr_dummy_func);
+		      FT260_SET_I2C_CLOCK_SPEED, ft260_i2c_clock_parse,
+		      ft260_attr_dummy_func);
 static DEVICE_ATTR_RW(clock);
 
 static ssize_t i2c_reset_store(struct device *kdev,
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 25+ messages in thread

* [PATCH v2 14/15] HID: ft260: i2c: fix i2c_reset sysfs store return value
  2026-10-30 22:36 [PATCH v2 00/15] HID: ft260: add UART and GPIO support, plus I2C fixes Michael Zaidman
                   ` (12 preceding siblings ...)
  2026-10-30 22:36 ` [PATCH v2 13/15] HID: ft260: i2c: reject an I2C clock outside the supported range Michael Zaidman
@ 2026-10-30 22:36 ` Michael Zaidman
  2026-10-30 22:36 ` [PATCH v2 15/15] HID: ft260: i2c: recover a bus held by a slave Michael Zaidman
  14 siblings, 0 replies; 25+ messages in thread
From: Michael Zaidman @ 2026-10-30 22:36 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: Linus Walleij, Bartosz Golaszewski, Greg Kroah-Hartman,
	Jiri Slaby, Raman Varabets, Germain Hebert, Christina Quast,
	Rio Liu, Bruno Giacomazzi, linux-input, linux-serial, linux-gpio,
	linux-i2c, linux-kernel, Michael Zaidman

A sysfs store must return the number of bytes it consumed. kernfs
hands that value straight to write(2). i2c_reset_store() returns
whatever ft260_i2c_reset() returns, which is the length of the
feature report it sent, 2, not 0. The "return count" below it never
runs, so every write to i2c_reset returns 2 whatever its size.

A write of two bytes is right by coincidence, because the report is
two bytes long. A caller that leaves out the newline, as C and Python
code usually does, writes one byte and gets 2 back, which is more
than it asked for:

  echo -n 1 > i2c_reset   1 byte, write() returns 2, bash ignores it
  printf 1  > i2c_reset   1 byte, dash reports an I/O error
  echo 1    > i2c_reset   2 bytes, correct by coincidence

Return 0 from ft260_i2c_reset() on success. The value is discarded by
its other callers, ft260_hid_output_report_check_status(),
ft260_i2c_probe() and ft260_i2c_abort(), whose own four callers
discard it as well.

Fixes: 6a82582d9fa4 ("HID: ft260: add usb hid to i2c host bridge driver")
Signed-off-by: Michael Zaidman <michael.zaidman@gmail.com>
---
 drivers/hid/hid-ft260.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/drivers/hid/hid-ft260.c b/drivers/hid/hid-ft260.c
index f7d01d9e07f7..cd1117c170e1 100644
--- a/drivers/hid/hid-ft260.c
+++ b/drivers/hid/hid-ft260.c
@@ -551,7 +551,7 @@ static int ft260_i2c_reset(struct hid_device *hdev)
 	}
 
 	ft260_dbg("done\n");
-	return ret;
+	return 0;
 }
 
 static int ft260_hid_output_report(struct hid_device *hdev, u8 *data,
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 25+ messages in thread

* [PATCH v2 15/15] HID: ft260: i2c: recover a bus held by a slave
  2026-10-30 22:36 [PATCH v2 00/15] HID: ft260: add UART and GPIO support, plus I2C fixes Michael Zaidman
                   ` (13 preceding siblings ...)
  2026-10-30 22:36 ` [PATCH v2 14/15] HID: ft260: i2c: fix i2c_reset sysfs store return value Michael Zaidman
@ 2026-10-30 22:36 ` Michael Zaidman
  2026-10-30 22:59   ` sashiko-bot
  14 siblings, 1 reply; 25+ messages in thread
From: Michael Zaidman @ 2026-10-30 22:36 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: Linus Walleij, Bartosz Golaszewski, Greg Kroah-Hartman,
	Jiri Slaby, Raman Varabets, Germain Hebert, Christina Quast,
	Rio Liu, Bruno Giacomazzi, linux-input, linux-serial, linux-gpio,
	linux-i2c, linux-kernel, Michael Zaidman

An SMBus block read takes the first byte the slave sends as the block
length. An EEPROM has no length byte, it answers with stored data, and
ft260_smbus_xfer() rejects a value above I2C_SMBUS_BLOCK_MAX. That
count phase carries no STOP, so the slave keeps SDA low while it waits
for the next clock. FT260_SET_I2C_RESET restarts the controller but
does not clock the bus, the chip cannot put a STOP on the line, and
every later START ends in arbitration lost.

A 24c02 holds 0x68 at offset 0x80, so on that bus

  i2cget -y 0 0x50 0x80 s
  ft260 0003:0403:6030.0011: smbus block read: invalid count 104 from slave 0x50

killed the bus: every transfer after it failed with bus_status 0x72.
The sysfs i2c_reset, unbind and bind of the HID interface, and rmmod
followed by insmod all left the status at 0x72.

Nine SCL pulses free the slave. Switching the I2C controller off gives
GPIO0 and GPIO1 back their GPIO function, so i2c_generic_scl_recovery()
can clock the line and make a STOP. ft260_i2c_abort() reads the status
after the reset and calls i2c_recover_bus() when BUS_BUSY is still set.
The error bit is no use there: the chip reports 0x60 right after the
reset and only shows 0x72 at the next START.

ft260_gpio_pin_set() and ft260_gpio_read_all() are the bodies of
ft260_gpio_direction_set() and ft260_gpio_get_all(), split out because
the recovery needs them without the gpio_en check, SCL and SDA are no
GPIOs while I2C runs, and without dev->lock, which the abort path
already holds.

With the patch the same i2cget still fails with -EPROTO and the next
i2ctransfer succeeds. A full i2cdetect scan of 112 addresses logs no
recovery, so an address NAK does not take this path.

Fixes: 2d68d67151eb ("HID: ft260: fix SMBus block read protocol handling")
Signed-off-by: Michael Zaidman <michael.zaidman@gmail.com>
---
 drivers/hid/hid-ft260.c | 193 +++++++++++++++++++++++++++++++++-------
 1 file changed, 162 insertions(+), 31 deletions(-)

diff --git a/drivers/hid/hid-ft260.c b/drivers/hid/hid-ft260.c
index cd1117c170e1..faa1e12cd2d5 100644
--- a/drivers/hid/hid-ft260.c
+++ b/drivers/hid/hid-ft260.c
@@ -188,6 +188,8 @@ enum {
 	FT260_GPIO_DIRECTION		= 0x01,
 	FT260_GPIO_DIR_INPUT		= 0x00,
 	FT260_GPIO_DIR_OUTPUT		= 0x01,
+	FT260_GPIO_SCL_OFFSET		= 0,	/* GPIO0 */
+	FT260_GPIO_SDA_OFFSET		= 1,	/* GPIO1 */
 };
 
 /* GPIO bit masks */
@@ -554,6 +556,22 @@ static int ft260_i2c_reset(struct hid_device *hdev)
 	return 0;
 }
 
+/*
+ * Enable or disable the I2C controller. With the controller disabled
+ * GPIO0 and GPIO1 carry their GPIO function instead of SCL and SDA.
+ */
+static int ft260_i2c_set_mode(struct hid_device *hdev, u8 enable)
+{
+	struct ft260_set_i2c_mode_report report;
+
+	report.report = FT260_SYSTEM_SETTINGS;
+	report.request = FT260_SET_I2C_MODE;
+	report.i2c_enable = enable;
+
+	return ft260_hid_feature_report_set(hdev, (u8 *)&report,
+					    sizeof(report));
+}
+
 static int ft260_hid_output_report(struct hid_device *hdev, u8 *data,
 				   size_t len);
 
@@ -575,10 +593,46 @@ static int ft260_i2c_stop(struct hid_device *hdev, u8 addr)
 	return ft260_hid_output_report(hdev, buf, sizeof(buf));
 }
 
-static int ft260_i2c_abort(struct hid_device *hdev, u8 addr)
+static int ft260_i2c_bus_status(struct ft260_device *dev)
 {
-	ft260_i2c_stop(hdev, addr);
-	return ft260_i2c_reset(hdev);
+	struct ft260_get_i2c_status_report report;
+	int ret;
+
+	ret = ft260_hid_feature_report_get(dev->hdev, FT260_I2C_STATUS,
+					   (u8 *)&report, sizeof(report));
+	if (ret < 0)
+		return ret;
+
+	return report.bus_status;
+}
+
+/*
+ * I2C reset does not clock the bus. A slave left in the middle of a
+ * byte keeps SDA low, the chip cannot put a STOP on the bus, and every
+ * later START fails with arbitration lost. The chip reports this as
+ * BUS_BUSY once the reset is done; only driving SCL as a GPIO frees the
+ * slave, so fall back to bus recovery.
+ */
+static int ft260_i2c_abort(struct ft260_device *dev, u8 addr)
+{
+	int ret;
+
+	ft260_i2c_stop(dev->hdev, addr);
+
+	ret = ft260_i2c_reset(dev->hdev);
+	if (ret < 0)
+		return ret;
+
+	ret = ft260_i2c_bus_status(dev);
+	if (ret < 0)
+		return ret;
+
+	if (ret & FT260_I2C_STATUS_BUS_BUSY) {
+		ft260_dbg("bus_status %#02x, recovering\n", ret);
+		return i2c_recover_bus(&dev->adap);
+	}
+
+	return 0;
 }
 
 static int ft260_xfer_status(struct ft260_device *dev, u8 bus_busy)
@@ -680,7 +734,7 @@ static int ft260_hid_output_report_check_status(struct ft260_device *dev,
 	if (ret == 0)
 		return 0;
 
-	ft260_i2c_abort(hdev, addr);
+	ft260_i2c_abort(dev, addr);
 	return -EIO;
 }
 
@@ -829,7 +883,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_abort(hdev, addr);
+			ft260_i2c_abort(dev, addr);
 			goto ft260_i2c_read_exit;
 		}
 
@@ -843,7 +897,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_abort(hdev, addr);
+			ft260_i2c_abort(dev, addr);
 			goto ft260_i2c_read_exit;
 		}
 
@@ -1027,7 +1081,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_abort(hdev, addr);
+				ft260_i2c_abort(dev, addr);
 				ret = -EPROTO;
 				goto smbus_exit;
 			}
@@ -1237,34 +1291,23 @@ static int ft260_gpio_set(struct gpio_chip *gc, u32 offset, int value)
 	return ret;
 }
 
-static int ft260_gpio_direction_set(struct gpio_chip *gc, u32 offset,
-				    int value, int direction)
+/*
+ * Set the direction and the value of one pin. Takes no lock and does
+ * not look at gpio_en, so the I2C bus recovery can drive SCL and SDA
+ * while it holds dev->lock and while the pins are not GPIOs.
+ */
+static int ft260_gpio_pin_set(struct ft260_device *dev, 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 & BIT(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;
+		return ret;
 	}
 
 	rep = (struct ft260_gpio_write_request_report *)&buf;
@@ -1298,12 +1341,36 @@ static int ft260_gpio_direction_set(struct gpio_chip *gc, u32 offset,
 	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;
+		return ret;
 	}
 
 	dev->gpio = rep->gpio;
-	ret = 0;
-exit:
+	return 0;
+}
+
+static int ft260_gpio_direction_set(struct gpio_chip *gc, u32 offset,
+				    int value, int direction)
+{
+	int ret;
+	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 & BIT(offset))) {
+		hid_err(hdev, "%s: wrong pin function %d\n", __func__, offset);
+		ret = -EIO;
+	} else {
+		ret = ft260_gpio_pin_set(dev, offset, value, direction);
+	}
+
 	mutex_unlock(&dev->lock);
 	return ret;
 }
@@ -1321,11 +1388,10 @@ static int ft260_gpio_direction_input(struct gpio_chip *gc, u32 offset)
 					FT260_GPIO_DIR_INPUT);
 }
 
-static int ft260_gpio_get_all(struct gpio_chip *gc, int item)
+static int ft260_gpio_read_all(struct ft260_device *dev, 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));
@@ -1342,6 +1408,11 @@ static int ft260_gpio_get_all(struct gpio_chip *gc, int item)
 	return ret;
 }
 
+static int ft260_gpio_get_all(struct gpio_chip *gc, int item)
+{
+	return ft260_gpio_read_all(gpiochip_get_data(gc), item);
+}
+
 static int ft260_gpio_get_direction(struct gpio_chip *gc, u32 offset)
 {
 	int ret = ft260_gpio_get_all(gc, FT260_GPIO_DIRECTION);
@@ -1360,6 +1431,65 @@ static int ft260_gpio_get(struct gpio_chip *gc, u32 offset)
 	return (ret >> offset) & 1;
 }
 
+/*
+ * I2C bus recovery. Turning the I2C controller off gives GPIO0 and
+ * GPIO1 back their GPIO function, so i2c_generic_scl_recovery() can
+ * clock the slave out of the byte it holds and put a STOP on the bus.
+ * A pin is released by making it an input, the bus pull-ups then drive
+ * it high.
+ */
+static void ft260_i2c_set_pin(struct i2c_adapter *adap, u32 offset, int val)
+{
+	struct ft260_device *dev = i2c_get_adapdata(adap);
+
+	ft260_gpio_pin_set(dev, offset, 0,
+			   val ? FT260_GPIO_DIR_INPUT : FT260_GPIO_DIR_OUTPUT);
+}
+
+static void ft260_i2c_set_scl(struct i2c_adapter *adap, int val)
+{
+	ft260_i2c_set_pin(adap, FT260_GPIO_SCL_OFFSET, val);
+}
+
+static void ft260_i2c_set_sda(struct i2c_adapter *adap, int val)
+{
+	ft260_i2c_set_pin(adap, FT260_GPIO_SDA_OFFSET, val);
+}
+
+static int ft260_i2c_get_scl(struct i2c_adapter *adap)
+{
+	struct ft260_device *dev = i2c_get_adapdata(adap);
+	int ret = ft260_gpio_read_all(dev, FT260_GPIO_VALUE);
+
+	if (ret < 0)
+		return ret;
+
+	return (ret >> FT260_GPIO_SCL_OFFSET) & 1;
+}
+
+static void ft260_i2c_prepare_recovery(struct i2c_adapter *adap)
+{
+	struct ft260_device *dev = i2c_get_adapdata(adap);
+
+	ft260_i2c_set_mode(dev->hdev, 0);
+}
+
+static void ft260_i2c_unprepare_recovery(struct i2c_adapter *adap)
+{
+	struct ft260_device *dev = i2c_get_adapdata(adap);
+
+	ft260_i2c_set_mode(dev->hdev, 1);
+}
+
+static struct i2c_bus_recovery_info ft260_i2c_recovery = {
+	.recover_bus		= i2c_generic_scl_recovery,
+	.set_scl		= ft260_i2c_set_scl,
+	.get_scl		= ft260_i2c_get_scl,
+	.set_sda		= ft260_i2c_set_sda,
+	.prepare_recovery	= ft260_i2c_prepare_recovery,
+	.unprepare_recovery	= ft260_i2c_unprepare_recovery,
+};
+
 static int ft260_gpio_init(struct ft260_device *dev,
 			   struct ft260_get_system_status_report *cfg)
 {
@@ -2331,6 +2461,7 @@ static int ft260_i2c_probe(struct ft260_device *dev,
 	dev->adap.class = I2C_CLASS_HWMON;
 	dev->adap.algo = &ft260_i2c_algo;
 	dev->adap.quirks = &ft260_i2c_quirks;
+	dev->adap.bus_recovery_info = &ft260_i2c_recovery;
 	dev->adap.dev.parent = &hdev->dev;
 	snprintf(dev->adap.name, sizeof(dev->adap.name),
 		 "FT260 usb-i2c bridge");
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 25+ messages in thread

* Re: [PATCH v2 06/15] HID: ft260: i2c: reduce bus-error message severity
  2026-10-30 22:36 ` [PATCH v2 06/15] HID: ft260: i2c: reduce bus-error message severity Michael Zaidman
@ 2026-10-30 22:44   ` sashiko-bot
  0 siblings, 0 replies; 25+ messages in thread
From: sashiko-bot @ 2026-10-30 22:44 UTC (permalink / raw)
  To: Michael Zaidman; +Cc: linux-input, linux-serial, linux-gpio, dmitry.torokhov

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] HID: ft260: use-after-free of dev in devres-managed gpio_chip on remove
--

commit 1fae4f26d8271d00c6ed7c7d5f16ef4ae51d0f21
Author: Michael Zaidman <michael.zaidman@gmail.com>

HID: ft260: i2c: reduce bus-error message severity

This commit lowers the log level of I2C bus error messages in
ft260_xfer_status(), ft260_i2c_write(), and ft260_smbus_write() from
hid_err() to ft260_dbg() to avoid spamming dmesg during bus scans, while
keeping hid_err() in ft260_hid_output_report_check_status() for actual HID
transport failures.

> diff --git a/drivers/hid/hid-ft260.c b/drivers/hid/hid-ft260.c
> index d2c5c9ed79276..6f8d729387025 100644
> --- a/drivers/hid/hid-ft260.c
> +++ b/drivers/hid/hid-ft260.c
> @@ -575,7 +575,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;
>  	}

[Severity: High]
As reported by Sashiko on v1 [1], in drivers/hid/hid-ft260.c, ft260_probe()
allocates dev manually with kzalloc_obj(), while ft260_gpio_init() registers
the GPIO chip via devres with dev as its private data:

drivers/hid/hid-ft260.c:ft260_gpio_init() {
	...
	ret = devm_gpiochip_add_data(&hdev->dev, dev->gc, dev);
	if (ret < 0)
		hid_err(hdev, "cannot add GPIO chip %d\n", ret);
}

During unbind, hid_device_remove() calls ft260_remove() before releasing
hdev->dev's devres group via devres_release_group():

drivers/hid/hid-ft260.c:ft260_remove() {
	...
	hid_hw_close(hdev);
	hid_hw_stop(hdev);

	if (dev->iface_type == FT260_IFACE_UART)
		ft260_uart_port_put(dev);
	else
		kfree(dev);
}

Similarly, if sysfs_create_group() fails after ft260_gpio_init() in
ft260_i2c_probe() or ft260_uart_probe(), ft260_probe() frees dev before
__hid_device_probe() releases the devres group.

Because dev->gc remains registered until devres_release_group() runs, can a
concurrent GPIO callback such as ft260_gpio_get(), ft260_gpio_set(),
ft260_gpio_direction_input(), ft260_gpio_direction_output(), or
ft260_gpio_get_direction() obtain the freed dev pointer via
gpiochip_get_data(gc) and trigger a use-after-free?

[1] https://lore.kernel.org/r/20260822213941.98882-7-michael.zaidman@gmail.com

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261030223637.86907-1-michael.zaidman@gmail.com?part=6

^ permalink raw reply	[flat|nested] 25+ messages in thread

* Re: [PATCH v2 05/15] HID: ft260: i2c: silence sysfs store big-numbers
  2026-10-30 22:36 ` [PATCH v2 05/15] HID: ft260: i2c: silence sysfs store big-numbers Michael Zaidman
@ 2026-10-30 22:47   ` sashiko-bot
  0 siblings, 0 replies; 25+ messages in thread
From: sashiko-bot @ 2026-10-30 22:47 UTC (permalink / raw)
  To: Michael Zaidman; +Cc: linux-input, linux-serial, linux-gpio, dmitry.torokhov

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] HID: ft260: kfree() of dev before devres unregisters gpio_chip
--

commit 6921079604110581f3bdc063a11efc4ea28f578a
Author: Michael Zaidman <michael.zaidman@gmail.com>

HID: ft260: i2c: silence sysfs store big-numbers

This series adds UART and GPIO support to the FT260 HID driver, adjusts the
I2C read timeout, and updates FT260_ATTR_STORE to return the sysfs write byte
count and convert 16-bit attributes to little endian.

> diff --git a/drivers/hid/hid-ft260.c b/drivers/hid/hid-ft260.c
> index 64c9c940db46e..d2c5c9ed79276 100644
> --- a/drivers/hid/hid-ft260.c
> +++ b/drivers/hid/hid-ft260.c

[ ... ]

> @@ -1043,53 +2307,35 @@ 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(dev, &cfg);
> +	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 (ret == FT260_IFACE_I2C) {
> +		ret = ft260_i2c_probe(dev, &cfg);
> +		if (ret)
> +			goto err_hid_close;
> +	} else {
> +		ret = ft260_uart_probe(dev, &cfg);
> +		if (ret)
> +			goto err_uart;
>  	}
>  
>  	return 0;
>  
> -err_i2c_free:
> -	i2c_del_adapter(&dev->adap);
> +err_uart:
> +	hid_hw_close(hdev);
> +	hid_hw_stop(hdev);
> +	tty_port_put(&dev->port);
> +	return ret;
> +
>  err_hid_close:
>  	hid_hw_close(hdev);
>  err_hid_stop:
>  	hid_hw_stop(hdev);
> +hid_fail:
> +	kfree(dev);
> +alloc_fail:
>  	return ret;
>  }
>  
> @@ -1100,25 +2346,47 @@ 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->iface_type == FT260_IFACE_UART) {
> +		timer_delete_sync(&dev->wakeup_timer);
> +		cancel_work_sync(&dev->wakeup_work);
> +		tty_port_unregister_device(&dev->port, ft260_tty_driver,
> +					   dev->index);
> +		ft260_uart_port_remove(dev);
> +		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);
> +	}
>  
>  	hid_hw_close(hdev);
>  	hid_hw_stop(hdev);
> +
> +	if (dev->iface_type == FT260_IFACE_UART)
> +		ft260_uart_port_put(dev);
> +	else
> +		kfree(dev);
>  }

[Severity: High]
As reported by Sashiko on v1 [1], in drivers/hid/hid-ft260.c, ft260_probe()
allocates struct ft260_device *dev using kzalloc_obj(*dev, GFP_KERNEL), while
ft260_gpio_init() registers the GPIO chip with devres on hdev->dev:

drivers/hid/hid-ft260.c:ft260_gpio_init() {
	...
	ret = devm_gpiochip_add_data(&hdev->dev, dev->gc, dev);
	...
}

In the HID core (drivers/hid/hid-core.c), __hid_device_probe() and
hid_device_remove() only release the devres group via devres_release_group()
after hdrv->probe() or hdrv->remove() returns.

When ft260_remove() runs (or when sysfs_create_group() fails after
ft260_gpio_init() in ft260_i2c_probe() or ft260_uart_probe() and unwinds
through ft260_probe()), dev is freed via kfree(dev) or tty_port_put() /
ft260_uart_port_put() before the devm-managed gpio_chip is unregistered.

Can a concurrent GPIO operation (such as ft260_gpio_get(),
ft260_gpio_get_direction(), ft260_gpio_set(), or ft260_gpio_direction_set())
retrieve the freed dev pointer via gpiochip_get_data(gc) during this window
and cause a use-after-free?

[1] https://lore.kernel.org/r/20260822213941.98882-6-michael.zaidman@gmail.com

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261030223637.86907-1-michael.zaidman@gmail.com?part=5

^ permalink raw reply	[flat|nested] 25+ messages in thread

* Re: [PATCH v2 09/15] HID: ft260: group sysfs attrs per HID interface
  2026-10-30 22:36 ` [PATCH v2 09/15] HID: ft260: group sysfs attrs per HID interface Michael Zaidman
@ 2026-10-30 22:48   ` sashiko-bot
  0 siblings, 0 replies; 25+ messages in thread
From: sashiko-bot @ 2026-10-30 22:48 UTC (permalink / raw)
  To: Michael Zaidman; +Cc: linux-input, linux-serial, linux-gpio, dmitry.torokhov

Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [High] HID: ft260: use-after-free of dev before devres removes gpio_chip
- [High] HID: ft260: ft260_probe() returns 0 after freeing dev on unknown chip_mode
- [High] HID: ft260: missing cancel_work_sync() on error path in ft260_uart_probe()
--

commit cc1ccb7f6680b5d859734f13042a3297303e344a
Author: Michael Zaidman <michael.zaidman@gmail.com>

HID: ft260: group sysfs attrs per HID interface

This commit splits the single FT260 sysfs attribute group into separate
per-interface attribute groups based on the active chip mode (I2C-only,
dual-mode, or UART-only) so each HID interface only exposes relevant
attributes.

> diff --git a/drivers/hid/hid-ft260.c b/drivers/hid/hid-ft260.c
> index 4106066b6a893..dca0250f2c259 100644
> --- a/drivers/hid/hid-ft260.c
> +++ b/drivers/hid/hid-ft260.c

[ ... ]

> @@ -2275,7 +2299,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;

[Severity: High]
What happens in ft260_probe() -> ft260_get_interface_type() if the device
reports a cfg->chip_mode value outside 0..3 in the FT260_SYSTEM_SETTINGS
feature report?

In drivers/hid/hid-ft260.c, ft260_get_interface_type() initializes ret to 0
via ft260_get_system_config() and switches on cfg->chip_mode without a
default case:

drivers/hid/hid-ft260.c:ft260_get_interface_type() {
	ret = ft260_get_system_config(hdev, cfg);
	if (ret < 0)
		return ret;
	...
	switch (cfg->chip_mode) {
	case FT260_MODE_ALL:
	case FT260_MODE_BOTH:
		if (dev->iface_id == 1)
			ret = FT260_IFACE_UART;
		else
			ret = FT260_IFACE_I2C;
		break;
	case FT260_MODE_UART:
		ret = FT260_IFACE_UART;
		break;
	case FT260_MODE_I2C:
		ret = FT260_IFACE_I2C;
		break;
	}

	return ret;
}

When cfg->chip_mode does not match any case, ft260_get_interface_type()
returns 0 (FT260_IFACE_NONE). In ft260_probe(), hid_set_drvdata(hdev, dev)
has already been called, and the check:

	ret = ft260_get_interface_type(dev, &cfg);
	if (ret <= FT260_IFACE_NONE)
		goto err_hid_close;

jumps to err_hid_close with ret == 0. That unwind path calls kfree(dev) and
returns 0 to the HID core while leaving hid_get_drvdata(hdev) pointing to
the freed struct ft260_device, which can then be dereferenced and freed a
second time in ft260_remove().

Should ft260_get_interface_type() return -EINVAL in a default branch when
cfg->chip_mode is unrecognized?

> @@ -2341,12 +2374,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]
As reported by Sashiko on v1 [1], if ft260_hid_feature_report_set(),
ft260_gpio_init(), or sysfs_create_group() fails in ft260_uart_probe() and
jumps to err_hid_report, can dev->wakeup_work still be running or pending
when dev is torn down?

In ft260_uart_probe(), tty_port_register_device_attr() exposes the tty
device before those calls complete. If userspace opens the tty port during
that window, ft260_uart_port_activate() or ft260_uart_change_speed() arms
dev->wakeup_timer, and ft260_uart_start_wakeup() schedules dev->wakeup_work.

However, the error path in ft260_uart_probe() only deletes the timer without
cancelling the work item:

err_hid_report:
	tty_port_unregister_device(&dev->port, ft260_tty_driver, dev->index);
err_register_tty:
	ft260_uart_port_remove(dev);
err_port:
	timer_delete_sync(&dev->wakeup_timer);
	return ret;

ft260_probe() then stops the HID hardware and drops the port reference via
tty_port_put(&dev->port), which can free dev while ft260_uart_do_wakeup() is
still executing. Should the error unwind also call
cancel_work_sync(&dev->wakeup_work), matching ft260_remove()?

> @@ -2464,11 +2499,18 @@ static void ft260_remove(struct hid_device *hdev)
>  					   dev->index);
>  		ft260_uart_port_remove(dev);
>  		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);
>  	}

[Severity: High]
As reported by Sashiko on v1 [2], can freeing dev before devres unregisters
dev->gc cause a use-after-free in the GPIO callbacks?

In ft260_gpio_init(), the GPIO chip is registered with devres on &hdev->dev
and given dev as its driver data:

	ret = devm_gpiochip_add_data(&hdev->dev, dev->gc, dev);

However, dev itself is manually allocated with kzalloc_obj(*dev, GFP_KERNEL)
in ft260_probe(). In the HID core (__hid_device_probe() and
hid_device_remove()), devres_release_group() only runs after the driver's
probe or remove callback returns:

1. In ft260_i2c_probe() and ft260_uart_probe(), if sysfs_create_group()
   fails after ft260_gpio_init() succeeds, ft260_probe() closes and stops
   hdev and frees dev via kfree(dev) or tty_port_put(&dev->port) before
   returning to __hid_device_probe().

2. In ft260_remove(), hid_hw_close(hdev), hid_hw_stop(hdev), and
   ft260_uart_port_put(dev) or kfree(dev) all execute before ft260_remove()
   returns to hid_device_remove().

Until devres_release_group() unregisters dev->gc, any concurrent GPIO
access (such as ft260_gpio_get(), ft260_gpio_set(), or
ft260_gpio_direction_output()) can retrieve the already-freed struct
ft260_device pointer via gpiochip_get_data(gc). Should the gpio_chip be
explicitly unregistered before stopping the HID hardware and freeing dev in
both ft260_remove() and the probe error paths?

[1] https://lore.kernel.org/r/20260822213941.98882-10-michael.zaidman@gmail.com
[2] https://lore.kernel.org/r/20260822213941.98882-10-michael.zaidman@gmail.com

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261030223637.86907-1-michael.zaidman@gmail.com?part=9

^ permalink raw reply	[flat|nested] 25+ messages in thread

* Re: [PATCH v2 03/15] HID: ft260: add GPIO support
  2026-10-30 22:36 ` [PATCH v2 03/15] HID: ft260: add GPIO support Michael Zaidman
@ 2026-10-30 22:53   ` sashiko-bot
  0 siblings, 0 replies; 25+ messages in thread
From: sashiko-bot @ 2026-10-30 22:53 UTC (permalink / raw)
  To: Michael Zaidman; +Cc: linux-input, linux-serial, linux-gpio, dmitry.torokhov

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] HID: ft260: use-after-free from devm_gpiochip_add_data() outliving dev
--

commit 0ee92e34686aff73d8b504af1979dbbb02897cf0
Author: Michael Zaidman <michael.zaidman@gmail.com>

HID: ft260: add GPIO support

This commit adds gpiochip support for the FT260 multifunctional pins, exposes
pin function selection attributes via sysfs, and tracks enabled GPIO pins
based on the active I2C and UART configuration.

> diff --git a/drivers/hid/hid-ft260.c b/drivers/hid/hid-ft260.c
> index c859486cb28c8..e24f3151fdaf3 100644
> --- a/drivers/hid/hid-ft260.c
> +++ b/drivers/hid/hid-ft260.c

[ ... ]

> @@ -919,6 +1033,347 @@ static const struct i2c_algorithm ft260_i2c_algo = {

[ ... ]

> +static int ft260_gpio_init(struct ft260_device *dev,
> +			   struct ft260_get_system_status_report *cfg)
> +{

[ ... ]

> +	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;
> +}

[Severity: High]
Can registering dev->gc with devm_gpiochip_add_data() in ft260_gpio_init() in
drivers/hid/hid-ft260.c cause a use-after-free on dev during device removal or
probe error rollback?

In drivers/hid/hid-core.c, hid_device_remove() calls hdrv->remove(hdev) before
releasing the device's devres group:

  hid_device_remove()
    ft260_remove()
      hid_hw_close(hdev);
      hid_hw_stop(hdev);
      if (dev->iface_type == FT260_IFACE_UART)
          ft260_uart_port_put(dev);
      else
          kfree(dev);
    devres_release_group(&hdev->dev, hdev->devres_group_id)
      devm_gpio_chip_release()
        gpiochip_remove(dev->gc)

Similarly, if sysfs_create_group() fails after ft260_gpio_init() succeeds in
ft260_i2c_probe() or ft260_uart_probe(), ft260_probe() closes and stops hdev
and frees dev (via kfree(dev) in err_hid_close, or via tty_port_put(&dev->port)
in err_uart) before __hid_device_probe() calls devres_release_group().

Until gpiochip_remove() runs, the GPIO chip remains live and registered with
dev as its private data pointer. If a gpiolib consumer or /dev/gpiochip
userspace ioctl invokes ft260_gpio_get(), ft260_gpio_set(),
ft260_gpio_direction_set(), or ft260_gpio_get_direction() during that window,
gpiochip_get_data(gc) will return the freed dev pointer, dereferencing freed
memory (such as dev->lock, dev->hdev, and dev->gpio_en) and issuing HID
transfers on a stopped hid_device.

Would it be safer to manage the gpio_chip registration explicitly with
gpiochip_add_data() and gpiochip_remove() before stopping hdev and freeing dev
in ft260_remove() and the probe error paths?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261030223637.86907-1-michael.zaidman@gmail.com?part=3

^ permalink raw reply	[flat|nested] 25+ messages in thread

* Re: [PATCH v2 08/15] HID: ft260: uart: add modem pins control via ioctl
  2026-10-30 22:36 ` [PATCH v2 08/15] HID: ft260: uart: add modem pins control via ioctl Michael Zaidman
@ 2026-10-30 22:55   ` sashiko-bot
  0 siblings, 0 replies; 25+ messages in thread
From: sashiko-bot @ 2026-10-30 22:55 UTC (permalink / raw)
  To: Michael Zaidman; +Cc: linux-input, linux-serial, linux-gpio, dmitry.torokhov

Thank you for your contribution! Sashiko AI review found 5 potential issue(s) to consider:
- [High] HID: ft260: NULL deref in ft260_uart_tiocmget() before ft260_gpio_init() ends
- [High] HID: ft260: UAF from devm_gpiochip_add_data() vs manual dev free on teardown
- [High] HID: ft260: UAF in ft260_probe() when ft260_get_interface_type() returns 0
- [Medium] HID: ft260: ft260_gpio_en_update() clobbers DCD/RI pin state on mode change
- [Medium] HID: ft260: unlocked gpio_en check in ft260_uart_tiocmset() and tiocmget()
--

commit d2fce903f266a48f35e3fe9c315e488bfef07c36
Author: Michael Zaidman <michael.zaidman@gmail.com>

HID: ft260: uart: add modem pins control via ioctl

This commit adds TIOCMGET and TIOCMSET tty ioctl support to control FT260
UART modem pins when configured as GPIOs, updates GPIO and sysfs
registration to check chip_mode instead of USB interface 0, and preserves
the return value of ft260_get_interface_type() during probe.

> diff --git a/drivers/hid/hid-ft260.c b/drivers/hid/hid-ft260.c
> index 4b0b55c98f900..4106066b6a893 100644
> --- a/drivers/hid/hid-ft260.c
> +++ b/drivers/hid/hid-ft260.c
[ ... ]
> @@ -213,7 +214,8 @@ enum {
>  #define FT260_GPIO_UART_MODE_1_SET	FT260_GPIO_UART_DTR_DSR
>  #define FT260_GPIO_UART_MODE_2_SET	FT260_GPIO_UART_RTS_CTS
>  #define 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)
>  #define FT260_GPIO_UART_MODE_4_SET	FT260_GPIO_UART_MODE_3_SET

[Severity: Medium]
Does adding FT260_GPIO_UART_DCD_RI unconditionally to
FT260_GPIO_UART_MODE_3_SET and FT260_GPIO_UART_MODE_4_SET cause
ft260_gpio_en_update() to clobber the DCD/RI pin state on subsequent UART
mode changes?

When a user enables UART DCD/RI via the uart_dcd_ri sysfs attribute,
ft260_byte_store() sends FT260_ENABLE_UART_DCD_RI and calls
ft260_gpio_en_update(hdev, FT260_ENABLE_UART_DCD_RI, 1), which clears
FT260_GPIO_UART_DCD_RI from dev->gpio_en without updating
dev->gpio_uart_mode[3] or dev->gpio_uart_mode[4].

When ft260_uart_change_speed() (or a write to the uart_mode sysfs attribute)
later configures XON_XOFF (3) or NONE (4) flow control,
ft260_gpio_en_update() executes:

	case FT260_SET_UART_MODE:
		...
		ft260_gpio_en_clr(dev, bitmap);
		bitmap = dev->gpio_uart_mode[value];
		ft260_gpio_en_set(dev, bitmap);
		goto exit;

Because dev->gpio_uart_mode[value] still contains FT260_GPIO_UART_DCD_RI,
ft260_gpio_en_set() sets the GPIO4 and GPIO5 bits back in dev->gpio_en even
though the hardware pins remain configured as UART DCD/RI.

[ ... ]
> @@ -1941,6 +1971,80 @@ 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 -EINVAL;
> +	}
> +
> +	gpios = ft260_gpio_get_all(port->gc, FT260_GPIO_VALUE);
> +	if (gpios < 0)
> +		return gpios;

[Severity: High]
As reported by Sashiko on v1 [1], can dereferencing port->gc here race with
device removal and cause a use-after-free or NULL pointer dereference?

In ft260_gpio_init(), dev->gc is allocated with devm_kzalloc() and registered
via devm_gpiochip_add_data(&hdev->dev, dev->gc, dev), whereas dev (struct
ft260_device) is manually allocated with kzalloc_obj() and reference-counted
by dev->port in UART mode. This creates two lifetime mismatches during
teardown:

1. If a tty file descriptor is open when the USB device disconnects, struct
ft260_device remains alive until ft260_uart_port_destroy(), but
hid_device_remove() runs devres_release_group() as soon as ft260_remove()
returns. That unregisters and frees port->gc (setting gc->gpiodev->data to
NULL). Because tty_tiocmget() does not acquire tty_lock(tty) and
ft260_uart_tiocmget() passes port->gc into ft260_gpio_get_all() ->
gpiochip_get_data(gc), a concurrent TIOCMGET call during disconnect can
dereference freed port->gc or a NULL dev pointer returned by
gpiochip_get_data().

2. Conversely, when no tty is open (or in I2C mode, or when
sysfs_create_group() fails in probe after ft260_gpio_init()), ft260_remove()
and the ft260_probe() error paths call hid_hw_close(), hid_hw_stop(), and
free dev (via ft260_uart_port_put(), tty_port_put(), or kfree()) before
returning to the HID core, where devres_release_group() later unregisters
dev->gc. While dev->gc is still registered during that window,
gpiochip_get_data(gc) points to the freed struct ft260_device.

> +
> +	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 -EINVAL;
> +	}
> +	rep.report = FT260_GPIO;
> +
> +	mutex_lock(&port->lock);
> +	rep.gpio = port->gpio;

[Severity: Medium]
Should the (port->gpio_en & mask) != mask check in ft260_uart_tiocmset() and
ft260_uart_tiocmget() be performed while holding port->lock, matching
ft260_gpio_set() and ft260_gpio_direction_set()?

Updates to port->gpio_en in ft260_uart_change_speed() and FT260_ATTR_STORE()
occur under port->lock after a blocking ft260_hid_feature_report_set() call.
Checking port->gpio_en before acquiring mutex_lock(&port->lock) in
ft260_uart_tiocmset() leaves a window where a concurrent termios or sysfs
change can switch the pins to hardware flow control (clearing RTS/CTS or
DTR/DSR in port->gpio_en), after which ft260_uart_tiocmset() acquires
port->lock and sends an FT260_GPIO report driving those pins as outputs.

[ ... ]
> @@ -2232,7 +2340,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;

[Severity: High]
Can userspace open the tty device and invoke ft260_uart_tiocmget(),
ft260_uart_tiocmset(), or ft260_uart_set_termios() before ft260_gpio_init()
finishes initializing dev->gc, dev->gpio, and dev->gpio_uart_mode[]?

In ft260_uart_probe(), ft260_uart_add_port() and
tty_port_register_device_attr() expose /dev/ttyFTx to userspace before the
blocking ft260_hid_feature_report_set() call and before ft260_gpio_init():

	devt = tty_port_register_device_attr(&dev->port,
					     ft260_tty_driver,
					     dev->index, &hdev->dev,
					     dev, NULL);
	...
	ret = ft260_hid_feature_report_set(hdev, (u8 *)&req, sizeof(req));
	...
	if (dev->chip_mode & FT260_MODE_UART ||
	    dev->chip_mode == FT260_MODE_ALL) {
		ret = ft260_gpio_init(dev, cfg);

Inside ft260_gpio_init(), dev->gpio_en is populated with
FT260_GPIO_UART_MODE_4_SET before dev->gc is allocated, before two sleeping
ft260_hid_feature_report_get() calls populate dev->gpio (without holding
dev->lock), and before devm_gpiochip_add_data() initializes dev->gc->gpiodev:

	if (mode & FT260_MODE_UART) {
		if (cfg->uart_mode < FT260_GPIO_UART_MODES)
			dev->gpio_en |= dev->gpio_uart_mode[cfg->uart_mode];
		...
	}
	...
	dev->gc = devm_kzalloc(&hdev->dev, sizeof(*dev->gc), GFP_KERNEL);
	...
	(void)ft260_hid_feature_report_get(dev->hdev, FT260_CHIP_VERSION,
					(u8 *)&ver, sizeof(ver));

	ret = ft260_hid_feature_report_get(hdev, FT260_GPIO, (u8 *)&gbuf,
					   sizeof(gbuf));
	if (ret < 0)
		hid_warn(hdev, "cannot read GPIO state: %d\n", ret);
	else
		dev->gpio = gbuf.gpio;

	ret = devm_gpiochip_add_data(&hdev->dev, dev->gc, dev);

If userspace (such as udev or ModemManager) opens the tty and calls TIOCMGET
after dev->gpio_en is set but before devm_gpiochip_add_data() completes, the
(port->gpio_en & mask) != mask check in ft260_uart_tiocmget() succeeds while
port->gc or port->gc->gpiodev is still NULL, causing gpiochip_get_data() in
ft260_gpio_get_all() to dereference a NULL pointer. A concurrent TIOCMSET
in this window also sees an uninitialized zeroed port->gpio shadow, and a
concurrent set_termios call before ft260_gpio_init() populates
dev->gpio_uart_mode[] reads zeroed mode masks.

> @@ -2311,8 +2420,9 @@ static int ft260_probe(struct hid_device *hdev, const struct hid_device_id *id)
>  	ret = ft260_get_interface_type(dev, &cfg);
>  	if (ret <= FT260_IFACE_NONE)
>  		goto err_hid_close;
> +	dev->iface_type = ret;

[Severity: High]
As reported by Sashiko on v1 [2], what happens if ft260_get_interface_type()
returns 0 (FT260_IFACE_NONE)?

In ft260_get_interface_type(), ft260_get_system_config() returns 0 on
success, and the switch on cfg->chip_mode has no default case:

	switch (cfg->chip_mode) {
	case FT260_MODE_ALL:
	case FT260_MODE_BOTH:
		if (dev->iface_id == 1)
			ret = FT260_IFACE_UART;
		else
			ret = FT260_IFACE_I2C;
		break;
	case FT260_MODE_UART:
		ret = FT260_IFACE_UART;
		break;
	case FT260_MODE_I2C:
		ret = FT260_IFACE_I2C;
		break;
	}

	return ret;

If the device reports a cfg->chip_mode value outside 0..3,
ft260_get_interface_type() returns ret == 0. In ft260_probe(),
if (ret <= FT260_IFACE_NONE) jumps to err_hid_close with ret == 0, which
calls kfree(dev) while leaving hdev->driver_data pointing to dev, and then
returns 0 (success) from ft260_probe(). Subsequent removal in ft260_remove()
will then dereference and double-free dev.

[1] https://lore.kernel.org/r/20260822213941.98882-9-michael.zaidman@gmail.com
[2] https://lore.kernel.org/r/20260822213941.98882-9-michael.zaidman@gmail.com

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261030223637.86907-1-michael.zaidman@gmail.com?part=8

^ permalink raw reply	[flat|nested] 25+ messages in thread

* Re: [PATCH v2 15/15] HID: ft260: i2c: recover a bus held by a slave
  2026-10-30 22:36 ` [PATCH v2 15/15] HID: ft260: i2c: recover a bus held by a slave Michael Zaidman
@ 2026-10-30 22:59   ` sashiko-bot
  0 siblings, 0 replies; 25+ messages in thread
From: sashiko-bot @ 2026-10-30 22:59 UTC (permalink / raw)
  To: Michael Zaidman; +Cc: linux-input, linux-serial, linux-gpio, dmitry.torokhov

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] HID: ft260: unsynchronized FT260_GPIO access across interfaces in recovery
--

commit 1e0d001c06a246dc407bfd48dc94ce59eae220c5
Author: Michael Zaidman <michael.zaidman@gmail.com>

HID: ft260: i2c: recover a bus held by a slave

This commit adds I2C bus recovery using i2c_generic_scl_recovery() when the
I2C bus remains busy after resetting the controller in ft260_i2c_abort(). It
temporarily disables the I2C controller so GPIO0 and GPIO1 can be driven as
GPIOs via ft260_gpio_pin_set() and ft260_gpio_read_all().

> diff --git a/drivers/hid/hid-ft260.c b/drivers/hid/hid-ft260.c
> index cd1117c170e1d..faa1e12cd2d54 100644
> --- a/drivers/hid/hid-ft260.c
> +++ b/drivers/hid/hid-ft260.c

[ ... ]

> @@ -1237,34 +1291,23 @@ static int ft260_gpio_set(struct gpio_chip *gc, u32 offset, int value)
>  	return ret;
>  }
>  
> -static int ft260_gpio_direction_set(struct gpio_chip *gc, u32 offset,
> -				    int value, int direction)
> +/*
> + * Set the direction and the value of one pin. Takes no lock and does
> + * not look at gpio_en, so the I2C bus recovery can drive SCL and SDA
> + * while it holds dev->lock and while the pins are not GPIOs.
> + */
> +static int ft260_gpio_pin_set(struct ft260_device *dev, 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 & BIT(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;
> +		return ret;
>  	}
>  
>  	rep = (struct ft260_gpio_write_request_report *)&buf;
> @@ -1298,12 +1341,36 @@ static int ft260_gpio_direction_set(struct gpio_chip *gc, u32 offset,
>  	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;
> +		return ret;
>  	}
>  
>  	dev->gpio = rep->gpio;
> -	ret = 0;
> -exit:
> +	return 0;
> +}

[ ... ]

> @@ -1360,6 +1431,65 @@ static int ft260_gpio_get(struct gpio_chip *gc, u32 offset)
>  	return (ret >> offset) & 1;
>  }
>  
> +/*
> + * I2C bus recovery. Turning the I2C controller off gives GPIO0 and
> + * GPIO1 back their GPIO function, so i2c_generic_scl_recovery() can
> + * clock the slave out of the byte it holds and put a STOP on the bus.
> + * A pin is released by making it an input, the bus pull-ups then drive
> + * it high.
> + */
> +static void ft260_i2c_set_pin(struct i2c_adapter *adap, u32 offset, int val)
> +{
> +	struct ft260_device *dev = i2c_get_adapdata(adap);
> +
> +	ft260_gpio_pin_set(dev, offset, 0,
> +			   val ? FT260_GPIO_DIR_INPUT : FT260_GPIO_DIR_OUTPUT);
> +}

[Severity: Medium]
When the chip is in dual-interface mode (FT260_MODE_ALL or FT260_MODE_BOTH),
ft260_probe() allocates separate ft260_device instances for the I2C and UART
USB interfaces, each with its own dev->lock mutex and dev->gpio shadow state.

In those modes, ft260_uart_probe() registers the gpio_chip on the UART
interface's ft260_device:

drivers/hid/hid-ft260.c:ft260_uart_probe()
	if (dev->chip_mode & FT260_MODE_UART ||
	    dev->chip_mode == FT260_MODE_ALL ||
	    dev->chip_mode == FT260_MODE_BOTH) {
		ret = ft260_gpio_init(dev, cfg);

Meanwhile, ft260_i2c_abort() -> i2c_recover_bus() -> ft260_i2c_set_pin() ->
ft260_gpio_pin_set() runs on the I2C interface's ft260_device while holding
only the I2C interface's dev->lock.

Because both interfaces access the same chip-wide FT260_GPIO feature report,
can ft260_gpio_pin_set() race with concurrent FT260_GPIO operations on the
UART interface?

1. ft260_gpio_set() and ft260_gpio_direction_set() perform a non-atomic
   ft260_hid_feature_report_get() and ft260_hid_feature_report_set() cycle on
   FT260_GPIO while holding the UART interface's dev->lock. If this interleaves
   with ft260_gpio_pin_set() during bus recovery, one side's pin updates can
   be overwritten, or the UART interface's dev->gpio can record the transient
   output direction of GPIO0 (SCL) or GPIO1 (SDA).

2. ft260_uart_tiocmset() on the UART interface does not read FT260_GPIO from
   the chip before writing; it takes port->lock, copies the cached port->gpio,
   modifies RTS/DTR, and writes the full FT260_GPIO report:

drivers/hid/hid-ft260.c:ft260_uart_tiocmset()
	mutex_lock(&port->lock);
	rep.gpio = port->gpio;
	...
	ret = ft260_hid_feature_report_set(hdev, (u8 *)&rep, sizeof(rep));

   If called during recovery (or after the UART interface's port->gpio cached
   a transient SCL/SDA output direction), wouldn't ft260_uart_tiocmset()
   overwrite the SCL and SDA directions in FT260_GPIO?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261030223637.86907-1-michael.zaidman@gmail.com?part=15

^ permalink raw reply	[flat|nested] 25+ messages in thread

* Re: [PATCH v2 11/15] HID: ft260: workaround for TN_189 errata endpoint STALL after enumeration
  2026-10-30 22:36 ` [PATCH v2 11/15] HID: ft260: workaround for TN_189 errata endpoint STALL after enumeration Michael Zaidman
@ 2026-10-30 22:59   ` sashiko-bot
  0 siblings, 0 replies; 25+ messages in thread
From: sashiko-bot @ 2026-10-30 22:59 UTC (permalink / raw)
  To: Michael Zaidman; +Cc: linux-input, linux-serial, linux-gpio, dmitry.torokhov

Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [High] HID: ft260: duplicate per-interface resets in ft260_schedule_reset()
- [Medium] HID: ft260: race between ft260_reset_and_rebind() and sibling ft260_probe()
- [Low] HID: ft260: ft260_check_dev_responsive() masks -ENOMEM as -ENODEV
--

commit d1a110e7b9317417bd055dbbded2a9eda4b1e754
Author: Michael Zaidman <michael.zaidman@gmail.com>

HID: ft260: workaround for TN_189 errata endpoint STALL after enumeration

This patch adds an in-driver workaround for FTDI errata TN_189 where the
FT260's USB interrupt endpoints are occasionally halted after enumeration.
During probe, the driver checks interrupt endpoint health and device
responsiveness, and on failure schedules deferred work on an ordered
workqueue to reset the USB device and rebind all of its interfaces.

Link: https://ftdichip.com/wp-content/uploads/2026/05/TN_189-FT260-Errata-Technical-Note.pdf

> diff --git a/drivers/hid/hid-ft260.c b/drivers/hid/hid-ft260.c
> index 65835e13fa2ad..923ce6ee9dcfd 100644
> --- a/drivers/hid/hid-ft260.c
> +++ b/drivers/hid/hid-ft260.c

[ ... ]

> @@ -2396,15 +2397,229 @@ 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 struct workqueue_struct *ft260_reset_wq;
> +
> +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;

[Severity: Medium]
Can ft260_reset_and_rebind() race with a concurrent ft260_probe() on the
sibling HID interface when the driver is loaded after USB enumeration has
already finished?

When an FT260 device in the TN_189 errata state is enumerated before
hid-ft260 is loaded, usbhid_probe() registers both hid_device instances and
hub_event releases usb_lock_device(usbdev). When udev later loads hid-ft260
(or the driver is bound via sysfs), __hid_register_driver() probes the HID
devices sequentially on hid_bus_type without holding usb_lock_device(usbdev).

When interface 0 fails ft260_check_intr_ep_health() or
ft260_check_dev_responsive() in ft260_probe(), it queues
ft260_reset_and_rebind() on ft260_reset_wq and returns -ENODEV. While the
module init thread moves on to probe interface 1, the worker thread on
another CPU can immediately acquire usb_lock_device_for_reset(usbdev, NULL)
and run usb_reset_device(usbdev) and device_release_driver(&intf->dev)
concurrently with interface 1 running usbhid_start(), USB transfers, and
usbhid_stop() in ft260_probe().

In particular, usb_reset_device() invokes hid_pre_reset() -> hid_cease_io()
-> usb_kill_urb() without holding usbhid->mutex while interface 1's error
path in ft260_probe() -> hid_hw_stop() -> usbhid_stop() calls usb_free_urb(),
and interface 1's failing probe will also queue a redundant reset work item.

> +		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);
> +	queue_work(ft260_reset_wq, &rw->work);
> +
> +	return 0;
> +}

[Severity: High]
As reported by Sashiko on v1 [1], since the FT260 in composite modes
(FT260_MODE_ALL and FT260_MODE_BOTH) exposes two USB HID interfaces on the
same struct usb_device, won't scheduling an independent ft260_reset_work
item per failing interface cause a redundant reset that tears down the
already-recovered device and multiply pending work items on repeated
failures?

During enumeration under hub_event, usb_set_configuration() probes both
interface 0 and interface 1 while holding the USB device lock. When the
chip hits the TN_189 state, ft260_probe() fails on both interfaces and calls
ft260_schedule_reset(usbif) twice, queueing two ft260_reset_work items on
ft260_reset_wq for the same usb_device.

Once hub_event releases the USB device lock:

1. The first work item runs ft260_reset_and_rebind(), calls
   usb_reset_device(usbdev), and loops over all interfaces in
   actconfig->desc.bNumInterfaces calling device_release_driver(&intf->dev)
   and device_attach(&intf->dev). When the first reset recovers the chip,
   device_attach() synchronously runs usbhid_probe() and ft260_probe() on
   both interfaces, registering the live I2C adapter, GPIO chip, and
   /dev/ttyFT* port.

2. Immediately after the first work item unlocks usbdev, the second work
   item queued during initial enumeration runs ft260_reset_and_rebind().
   Because ft260_schedule_reset() does not deduplicate per usb_device and
   ft260_reset_and_rebind() does not check whether the interfaces are
   already bound and healthy, it unconditionally resets the healthy USB
   device a second time and calls device_release_driver(&intf->dev) on all
   interfaces, invoking ft260_remove() to tear down the active I2C adapter,
   GPIO chip, and TTY device that were just registered.

3. If a reset does not recover the device on the first attempt (or the
   device is persistently unresponsive), each execution of
   ft260_reset_and_rebind() consumes one work item from ft260_reset_wq
   while its device_attach() loop synchronously triggers failing
   ft260_probe() calls on both interfaces that queue two new
   ft260_reset_work items, causing the workqueue backlog to grow on every
   reset cycle.

[ ... ]

> +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;
> +	}

[Severity: Low]
In ft260_check_dev_responsive(), usb_control_msg() allocates a struct
usb_ctrlrequest and a struct urb (via usb_internal_control_msg() ->
usb_alloc_urb()) with GFP_NOIO and returns -ENOMEM if either allocation
fails.

Because the check afterward converts any negative return value (ret < 0) to
-ENODEV, an -ENOMEM failure inside usb_control_msg() will log the TN_189
warning and return -ENODEV to ft260_probe(), which then calls
ft260_schedule_reset(usbif). Should ft260_check_dev_responsive() propagate
ret directly when ret == -ENOMEM before converting other negative errors to
-ENODEV?

[1] https://lore.kernel.org/r/20260822213941.98882-13-michael.zaidman@gmail.com

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261030223637.86907-1-michael.zaidman@gmail.com?part=11

^ permalink raw reply	[flat|nested] 25+ messages in thread

* Re: [PATCH v2 01/15] HID: ft260: add serial driver
  2026-10-30 22:36 ` [PATCH v2 01/15] HID: ft260: add serial driver Michael Zaidman
@ 2026-10-30 23:00   ` sashiko-bot
  0 siblings, 0 replies; 25+ messages in thread
From: sashiko-bot @ 2026-10-30 23:00 UTC (permalink / raw)
  To: Michael Zaidman; +Cc: linux-input, linux-serial, linux-gpio, dmitry.torokhov

Thank you for your contribution! Sashiko AI review found 7 potential issue(s) to consider:
- [High] HID: ft260: broken minor index allocation and bounds check in ft260_uart_add_port()
- [High] HID: ft260: use-after-free of hdev and xmit_fifo across ft260_remove()
- [High] HID: ft260: unsynchronized transmit loop and infinite loop in ft260_uart_transmit_chars()
- [High] HID: ft260: use-after-free and double free in ft260_probe() on FT260_IFACE_NONE
- [High] HID: ft260: unguarded UART report in ft260_raw_event() on I2C interface
- [High] HID: ft260: sleeping in atomic context in ft260_uart_write()
- [Medium] HID: ft260: race in ft260_uart_probe() registering tty before hw config
--

commit bd086b64065aa177dfdec81fc03e141758ca2307
Author: Christina Quast <contact@christina-quast.de>

HID: ft260: add serial driver

This commit adds a TTY serial driver (/dev/ttyFTx) for the UART interface
of FTDI FT260 USB HID devices, supporting termios configuration and serial
data transfers alongside the existing I2C interface support.

> diff --git a/drivers/hid/hid-ft260.c b/drivers/hid/hid-ft260.c
> index 64c9c940db46e..982e2ca7a2de6 100644
> --- a/drivers/hid/hid-ft260.c
> +++ b/drivers/hid/hid-ft260.c

[ ... ]

> @@ -1001,51 +1101,478 @@ static const struct attribute_group ft260_attr_group = {
>  	}
>  };
>  
> -static int ft260_probe(struct hid_device *hdev, const struct hid_device_id *id)
> +/***
> + * 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;
> -	struct ft260_get_chip_version_report version;
> +
> +	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_tail(&port->device_list, &ft260_uart_device_list);
> +	mutex_unlock(&ft260_uart_list_lock);
> +
> +	return ret;
> +}

[Severity: High]
As reported by Sashiko on v1 [1], can ft260_uart_add_port() in
drivers/hid/hid-ft260.c assign duplicate minor indices or exceed
UART_COUNT_MAX?

The loop in ft260_uart_add_port() searches for the lowest unused index by
breaking as soon as dev->index != index, which assumes ft260_uart_device_list
is kept sorted in ascending order of index. However, the new port is always
appended at the end of the list with list_add_tail(&port->device_list,
&ft260_uart_device_list) instead of being inserted before dev
(&dev->device_list).

If port 0 and port 1 are probed (list: [0, 1]), port 0 is removed (list:
[1]), and a third UART device is probed, it gets index 0 and is appended to
the tail (list: [1, 0]). When a fourth UART device is probed, the loop sees
dev->index == 1 != 0 on the first entry, breaks immediately, and assigns a
duplicate index 0.

In ft260_uart_probe(), tty_port_register_device_attr() then calls
tty_port_link_device(), which overwrites ft260_tty_driver->ports[0] with the
new port before device_register() fails with -EEXIST. The error path in
ft260_probe() frees the new port via tty_port_put(&dev->port), leaving
ft260_tty_driver->ports[0] as a dangling pointer to freed memory while the
first /dev/ttyFT0 remains registered.

In addition, ft260_uart_add_port() does not check whether index >=
UART_COUNT_MAX (4), so probing a fifth UART device passes index == 4 to
tty_port_register_device_attr() -> tty_port_link_device() and triggers
WARN_ON(index >= driver->num).

> +
> +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);
> +
> +	tty_port_tty_vhangup(&port->port);
> +
> +	spin_lock(&port->write_lock);
> +	kfifo_free(&port->xmit_fifo);
> +	spin_unlock(&port->write_lock);
> +}

[Severity: High]
As reported by Sashiko on v1 [2], does freeing port->xmit_fifo in
ft260_uart_port_remove() and stopping port->hdev in ft260_remove() cause a
use-after-free when TTY operations race with or follow device removal?

Although struct ft260_device lives until ft260_uart_port_destroy(),
ft260_uart_port_remove() frees port->xmit_fifo during ft260_remove(), and the
HID core frees port->hdev once ft260_remove() returns:

drivers/hid/hid-ft260.c:ft260_remove() {
	if (dev->ft260_is_serial) {
		timer_delete_sync(&dev->wakeup_timer);
		cancel_work_sync(&dev->wakeup_work);
		tty_port_unregister_device(&dev->port, ft260_tty_driver,
					   dev->index);
		ft260_uart_port_remove(dev);
	}
	...
	hid_hw_close(hdev);
	hid_hw_stop(hdev);
}

If tty_open() has completed ft260_uart_install() (obtaining a port reference
from ft260_uart_device_list and dropping tty_mutex) before
ft260_uart_port_remove() runs, but has not yet reached tty_port_tty_set() in
tty_port_open(), port->port.tty is still NULL when tty_port_tty_vhangup() is
called. When tty_open() continues into ft260_uart_open() -> tty_port_open()
-> ft260_uart_activate():

drivers/hid/hid-ft260.c:ft260_uart_activate() {
	...
	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);
}

it resets the freed xmit_fifo, calls ft260_uart_change_speed() on the
stopped/freed port->hdev, ignores the error return, clears TTY_IO_ERROR, and
lets subsequent writes and termios changes proceed on a disconnected device.

Even for an already-opened TTY, concurrent or subsequent calls to
ft260_uart_set_termios() (which does not check tty_io_error()) or in-flight
ft260_uart_write() / ft260_uart_transmit_chars() calls can dereference the
freed port->hdev and port->xmit_fifo without any disconnect check or
synchronization against ft260_remove(). Because __kfifo_free() sets
fifo->data = NULL and fifo->mask = 0, kfifo_unused() evaluates to 1 when
in == out, causing a racing kfifo_in_locked() in ft260_uart_write() to
memcpy() into a NULL fifo->data pointer.

Should kfifo_free(&port->xmit_fifo) be deferred to ft260_uart_port_destroy(),
with a disconnect flag and lock protecting all port->hdev accesses in the
TTY callbacks?

[ ... ]

> +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);

[Severity: High]
As reported by Sashiko on v1 [3], what happens in ft260_uart_transmit_chars()
if two threads transmit concurrently, or if xmit_fifo is drained or reset
while ft260_hid_output_report() is blocking?

First, port->write_buf is shared across the port without any lock protecting
it across the blocking ft260_hid_output_report() call, so concurrent callers
(for example, normal write vs line discipline echo) will corrupt rep->report,
rep->length, and rep->data.

Second, rep->report and rep->length are computed from data_len before
kfifo_out_locked() dequeues the data. If another thread consumes bytes from
xmit_fifo (or ft260_uart_activate() resets the fifo, or
ft260_uart_port_remove() frees it), kfifo_out_locked() can return fewer bytes
than rep->length, including 0.

When kfifo_out_locked() returns len == 0:
1. rep->length still holds the stale non-zero length, violating the
   __counted_by(length) annotation on rep->data and sending a malformed
   2-byte report header with a non-zero length field.
2. If ft260_hid_output_report() succeeds, data_len -= 0 leaves data_len
   greater than 0, causing the do-while (data_len > 0) loop to spin
   indefinitely.

Also, if ft260_hid_output_report() fails mid-loop, the chunk already removed
by kfifo_out_locked() is lost and any remaining bytes stay in xmit_fifo
while ft260_uart_write() still returns the full queued count. Note that while
a subsequent patch in the series separates i2c_wr_buf and uart_wr_buf, the
unlocked uart_wr_buf use, pre-dequeue rep->length assignment, and infinite
loop when kfifo_out_spinlocked() returns 0 remain present.

[ ... ]

> +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 len;
> +}

[Severity: High]
Can ft260_uart_write() sleep in atomic context here?

The struct tty_operations write() callback contract in
include/linux/tty_driver.h specifies that write() may not sleep, and standard
line disciplines (such as N_PPP, N_SLIP, N_GSM0710, N_SLCAN, and N_MCTP)
invoke tty->ops->write() while holding spinlocks or running in softirq
context.

However, ft260_uart_write() synchronously calls:

ft260_uart_transmit_chars()
  -> ft260_hid_output_report()
       -> kmemdup(..., GFP_KERNEL)
       -> hid_hw_output_report()

which performs a GFP_KERNEL allocation and blocks waiting for synchronous USB
URB completion. Should transmission from xmit_fifo be deferred to a
workqueue item instead of running synchronously in ft260_uart_write()?

[ ... ]

> +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");
> +		goto err_port;
> +	}
> +	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_UART_CFG_FLOW_CTRL_NONE;
> +	put_unaligned_le32(9600, &req.baudrate);
> +	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));

[Severity: Medium]
In ft260_uart_probe(), the port is added to ft260_uart_device_list and
registered via tty_port_register_device_attr() before the initial 9600-8-N-1
FT260_SET_UART_CONFIG feature report is sent to the hardware.

If userspace (such as udev or ModemManager reacting to the KOBJ_ADD uevent)
opens /dev/ttyFTx and configures termios before ft260_hid_feature_report_set()
runs here, won't ft260_uart_probe() overwrite the userspace UART settings in
hardware with 9600-8-N-1? Should the initial hardware configuration happen
before publishing and registering the TTY device?

> +	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);
> +err_port:
> +	timer_delete_sync(&dev->wakeup_timer);
> +	return ret;
> +}
> +
> +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;
> +	int ret;
> +
> +	if (!hid_is_usb(hdev))
> +		return -EINVAL;
> +
> +	/* 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);
> +	mutex_init(&dev->lock);
> +	spin_lock_init(&dev->read_lock);
> +	init_completion(&dev->wait);
> +
> +	ret = hid_parse(hdev);
> +	if (ret) {
> +		hid_err(hdev, "failed to parse HID\n");
> +		goto hid_fail;
> +	}
> +
> +	ret = hid_hw_start(hdev, 0);
> +	if (ret) {
> +		hid_err(hdev, "failed to start HID HW\n");
> +		goto hid_fail;
> +	}
> +
> +	ret = hid_hw_open(hdev);
> +	if (ret) {
> +		hid_err(hdev, "failed to open HID HW\n");
> +		goto err_hid_stop;
> +	}
> +
> +	ret = ft260_hid_feature_report_get(hdev, FT260_CHIP_VERSION,
> +					   (u8 *)&version, sizeof(version));
> +	if (ret < 0) {
> +		hid_err(hdev, "failed to retrieve chip version\n");
> +		goto err_hid_close;
> +	}
> +
> +	hid_info(hdev, "chip code: %02x%02x %02x%02x\n",
> +		 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;

[Severity: High]
What happens in ft260_probe() if ft260_get_interface_type() returns
FT260_IFACE_NONE (0)?

In ft260_get_interface_type(), ret is initialized to the return value of
ft260_get_system_config(hdev, &cfg), which is 0 on success:

drivers/hid/hid-ft260.c:ft260_get_interface_type() {
	...
	ret = ft260_get_system_config(hdev, &cfg);
	if (ret < 0)
		return ret;
	...
	switch (cfg.chip_mode) {
	case FT260_MODE_ALL:
	case FT260_MODE_BOTH:
		...
		break;
	case FT260_MODE_UART:
		...
		break;
	case FT260_MODE_I2C:
		...
		break;
	}

	return ret;
}

If cfg.chip_mode does not match any of the switch cases,
ft260_get_interface_type() returns 0 (FT260_IFACE_NONE). In ft260_probe(),
the check if (ret <= FT260_IFACE_NONE) jumps to err_hid_close with ret == 0:

err_hid_close:
	hid_hw_close(hdev);
err_hid_stop:
	hid_hw_stop(hdev);
hid_fail:
	kfree(dev);
alloc_fail:
	return ret;

This frees dev while leaving hdev drvdata pointing to it, and returns 0
(probe success) to the HID core. When ft260_remove() is later called on
device unbind or disconnect, it retrieves the dangling dev pointer from
hid_get_drvdata(hdev), accesses freed memory, and calls kfree(dev) a second
time.

[ ... ]

> @@ -1157,9 +1835,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);
>  	}

[Severity: High]
Can ft260_raw_event() dereference a NULL pointer if a UART report (ID
0xF0..0xFE) arrives on an I2C interface?

When an interface is probed as I2C (dev->ft260_is_serial is false),
ft260_uart_probe() is not called, so tty_port_init(&dev->port) never runs
and dev->port.buf.tail remains NULL. However, ft260_raw_event() dispatches
reports in FT260_UART_REPORT_MIN..FT260_UART_REPORT_MAX to
ft260_uart_receive_chars() without checking dev->ft260_is_serial:

ft260_raw_event()
  -> ft260_uart_receive_chars()
       -> tty_insert_flip_string(&port->port, data, length)
            -> __tty_insert_flip_string_flags()
                 -> __tty_buffer_request_room()

If a malformed or malicious USB device (or HID-BPF program) sends a UART
input report on the I2C interface, __tty_buffer_request_room() will
dereference NULL port->buf.tail. Should ft260_raw_event() verify
dev->ft260_is_serial before calling ft260_uart_receive_chars()?

[1] https://lore.kernel.org/r/20260822213941.98882-2-michael.zaidman@gmail.com
[2] https://lore.kernel.org/r/20260822215615.2D8541F000E9@smtp.kernel.org
[3] https://lore.kernel.org/r/20260822215615.2D8541F000E9@smtp.kernel.org

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261030223637.86907-1-michael.zaidman@gmail.com?part=1

^ permalink raw reply	[flat|nested] 25+ messages in thread

* Re: [PATCH v2 02/15] HID: ft260: uart: fix the serial driver
  2026-10-30 22:36 ` [PATCH v2 02/15] HID: ft260: uart: fix the " Michael Zaidman
@ 2026-10-30 23:03   ` sashiko-bot
  0 siblings, 0 replies; 25+ messages in thread
From: sashiko-bot @ 2026-10-30 23:03 UTC (permalink / raw)
  To: Michael Zaidman; +Cc: linux-input, linux-serial, linux-gpio, dmitry.torokhov

Thank you for your contribution! Sashiko AI review found 9 potential issue(s) to consider:
- [High] HID: ft260: invalid report ID 0xFF in ft260_uart_transmit_chars()
- [High] HID: ft260: race and infinite loop in ft260_uart_transmit_chars()
- [High] HID: ft260: wakeup timer and work teardown race in ft260_remove()
- [High] HID: ft260: broken index allocation and bounds check in ft260_uart_add_port()
- [High] HID: ft260: UAF and double free when ft260_get_interface_type() returns 0
- [High] HID: ft260: missing iface_type check for UART reports in ft260_raw_event()
- [Medium] HID: ft260: unconditional wakeup timer in ft260_uart_port_activate()
- [Medium] HID: ft260: ft260_uart_probe() registers TTY device before configuring UART
- [Low] HID: ft260: unneeded ft260_uart_wakeup() prototype and changelog mismatches
--

commit 120d1c2c1f711fdf1a272089095a3cbf1a2d1d34
Author: Michael Zaidman <michael.zaidman@gmail.com>

HID: ft260: uart: fix the serial driver

This commit updates the FT260 serial driver to separate the UART and I2C TX
buffers, adjust the power-save wakeup timer logic, validate HID input report
lengths against the received report size, and clean up interface detection and
FIFO handling.

>     HID: ft260: uart: fix the serial driver
>     
>     Fix the serial driver added by the previous patch, a port of
>     Christina Quast's v4, so that it works reliably on hardware.
>     
>     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: replace the kfifo_*_locked aliases with
>     kfifo_*_spinlocked, and drop the parentheses around single-value
>     defines, which coding-style asks for only when the value is an
>     expression.
>     
>     This replaces the UART bring-up fix series:
>     https://lore.kernel.org/all/20240210215147.77629-1-michael.zaidman@gmail.com/
>     
>     Do not treat UART interrupt status (0xB1) as a payload length. When
>     re-enabling the power-save wakeup workaround after a baud change,
>     re-arm the timer (reported by Sashiko).
>     
>     Report 0xE1 is defined as FT260_UART_RI_DCD_STATUS, but AN_394 section
>     4.3 lists 0xE2 for "UART RI and DCD Status". Correct the value. The
>     macro has no user, because the DCD and RI levels arrive in the 0xB1
>     interrupt report, which ft260_raw_event() handles.
>     
>     Signed-off-by: Michael Zaidman <michael.zaidman@gmail.com>

[Severity: Low]
This isn't a bug in the code, but does the commit message match what the patch
does here?

The changelog mentions waking earlier before RX, but
FT260_WAKEUP_NEEDED_AFTER_MS remains 4800 ms and ft260_uart_wakeup() is only
called from the periodic ft260_uart_do_wakeup() work handler.

The last paragraph also states that DCD and RI levels arrive in the 0xB1
interrupt report, which ft260_raw_event() handles, whereas ft260_raw_event()
ignores FT260_UART_INTERRUPT_STATUS (0xB1) and returns 0.

> diff --git a/drivers/hid/hid-ft260.c b/drivers/hid/hid-ft260.c
> index 982e2ca7a2de6..c859486cb28c8 100644
> --- a/drivers/hid/hid-ft260.c
> +++ b/drivers/hid/hid-ft260.c
[ ... ]
> @@ -38,11 +34,14 @@ 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 */
> +#define FT260_WAKEUP_NEEDED_AFTER_MS 4800 /* 5s minus 200ms margin */
>  
>  /*
>   * The ft260 input report format defines 62 bytes for the data payload, but
> @@ -55,8 +54,9 @@ MODULE_PARM_DESC(debug, "Toggle FT260 debugging messages");
>   * 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)
> +#define FT260_RD_DATA_MAX 180
> +#define FT260_WR_I2C_DATA_MAX 60
> +#define FT260_WR_UART_DATA_MAX 62

[Severity: High]
When ft260_uart_write() calls ft260_uart_transmit_chars() with 61 or more
bytes in xmit_fifo, len is clamped to FT260_WR_UART_DATA_MAX (62):

    len = min(data_len, FT260_WR_UART_DATA_MAX);
    rep->report = FT260_UART_DATA_REPORT_ID(len);

For len = 61 or 62, (len - 1) / 4 evaluates to 15, so
FT260_UART_DATA_REPORT_ID(len) produces 0xF0 + 15 = 0xFF.

Since the valid UART data report IDs end at FT260_UART_REPORT_MAX (0xFE, which
carries 57 to 62 payload bytes), won't setting rep->report to 0xFF cause
transfers of 61 or 62 bytes to fail with an invalid HID report ID?

[ ... ]
> @@ -937,27 +951,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;
>  }

[Severity: High]
If cfg.chip_mode does not match any of the four cases in
ft260_get_interface_type(), ret remains 0 (from ft260_get_system_config()),
which equals FT260_IFACE_NONE.

In ft260_probe(), hid_set_drvdata(hdev, dev) is called before
ft260_get_interface_type(), and the check:

    ret = ft260_get_interface_type(hdev, dev);
    if (ret <= FT260_IFACE_NONE)
        goto err_hid_close;

jumps to err_hid_close with ret == 0. That error path frees dev via kfree(dev)
and returns 0 from ft260_probe().

Won't returning 0 cause the driver core to treat probe as successful, leading
to a use-after-free and double free of dev when ft260_remove() later retrieves
dev from hid_get_drvdata(hdev)?

[ ... ]
> @@ -1101,12 +1113,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);

[Severity: Low]
This isn't a bug, but is this forward declaration of ft260_uart_wakeup()
needed? Its only caller is ft260_uart_do_wakeup(), which is defined right
below ft260_uart_wakeup().

[ ... ]
> @@ -1123,8 +1159,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);

[Severity: High]
In ft260_uart_add_port(), the minor index search assumes
ft260_uart_device_list is sorted by index, but new ports are appended to the
tail of the list without checking UART_COUNT_MAX:

    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_tail(&port->device_list, &ft260_uart_device_list);
    mutex_unlock(&ft260_uart_list_lock);

If ports 0 and 1 are added, port 0 is removed, and a new port A is probed,
port A gets index 0 and is placed at the tail (making the list order [1, 0]).
If another port B is then probed, the first entry has dev->index == 1 != 0, so
the loop breaks immediately and assigns index 0 to port B as well.

When ft260_uart_probe() then calls tty_port_register_device_attr(),
tty_port_link_device() overwrites ft260_tty_driver->ports[0] with &devB->port
before tty_register_device_attr() fails with -EEXIST and frees devB, leaving
ft260_tty_driver->ports[0] dangling for ft260_uart_install() ->
tty_standard_install(). Also, if more than UART_COUNT_MAX (4) ports are
probed, index >= UART_COUNT_MAX triggers WARN_ON(index >= driver->num) in
tty_port_link_device().

Should ft260_uart_add_port() insert before dev->device_list to keep the list
sorted and return an error when index >= UART_COUNT_MAX?

[ ... ]
> @@ -1217,30 +1253,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:

[Severity: High]
As reported by Sashiko on v1 [1], ft260_uart_transmit_chars() builds outgoing
reports in the shared per-port buffer port->uart_wr_buf without holding a lock
across the loop while ft260_hid_output_report() sleeps.

If two threads enter ft260_uart_write() -> ft260_uart_transmit_chars()
concurrently (or if kfifo_reset() runs in ft260_uart_port_activate()):

1. Both threads can overwrite port->uart_wr_buf concurrently before kmemdup()
   in ft260_hid_output_report() copies it.
2. Because rep->report and rep->length are set from data_len before
   kfifo_out_spinlocked(), a racing thread draining xmit_fifo can cause
   kfifo_out_spinlocked() to return fewer bytes than rep->length, or 0 bytes.
3. If kfifo_out_spinlocked() returns 0, ft260_hid_output_report() sends a
   2-byte report with a non-zero rep->length and data_len -= 0 never
   terminates the while (data_len > 0) loop.

Also, with tty_wakeup(tty) removed here, what wakes up a writer waiting for
write room after xmit_fifo fills up?

[ ... ]
> @@ -1503,53 +1537,68 @@ static void ft260_uart_start_wakeup(struct timer_list *t)
[ ... ]
> +static int ft260_uart_port_activate(struct tty_port *tport, struct tty_struct *tty)
> +{
> +	int ret;
> +	int baudrate;
> +	struct ft260_get_uart_settings_report cfg;
> +	struct ft260_device *port = container_of(tport, struct ft260_device, 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;
>  }

[Severity: Medium]
In ft260_uart_port_activate(), mod_timer(&port->wakeup_timer, ...) is now
called unconditionally rather than only when port->reschedule_work is true.
Also, if baudrate <= FT260_UART_EN_PW_SAVE_BAUD on the first open after
ft260_uart_probe() enabled the workaround, port->reschedule_work remains true.

Should ft260_uart_port_activate() pass baudrate > FT260_UART_EN_PW_SAVE_BAUD
directly to ft260_uart_wakeup_workaraund_enable() and guard mod_timer() with
if (port->reschedule_work), as in ft260_uart_change_speed()?

[ ... ]
> @@ -1641,10 +1689,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_UART_CFG_FLOW_CTRL_NONE;
> @@ -1656,8 +1704,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;
>  	}

[Severity: Medium]
In ft260_uart_probe(), tty_port_register_device_attr() exposes /dev/ttyFT* to
userspace before ft260_hid_feature_report_set() configures the UART to 9600n8.

Since ft260_uart_port_activate() now reads the active UART settings from the
chip via ft260_get_uart_settings() instead of calling
ft260_uart_change_speed(), can a userspace process opening the port
immediately after registration read uninitialized UART settings or have its
termios changes overwritten by the 9600n8 feature report in
ft260_uart_probe()?

[ ... ]
> @@ -1765,7 +1812,7 @@ static void ft260_remove(struct hid_device *hdev)
>  	if (!dev)
>  		return;
>  
> -	if (dev->ft260_is_serial) {
> +	if (dev->iface_type == FT260_IFACE_UART) {
>  		timer_delete_sync(&dev->wakeup_timer);
>  		cancel_work_sync(&dev->wakeup_work);
>  		tty_port_unregister_device(&dev->port, ft260_tty_driver,
> @@ -1779,7 +1826,7 @@ static void ft260_remove(struct hid_device *hdev)
>  	hid_hw_close(hdev);
>  	hid_hw_stop(hdev);
>  
> -	if (dev->ft260_is_serial)
> +	if (dev->iface_type == FT260_IFACE_UART)
>  		ft260_uart_port_put(dev);
>  	else
>  		kfree(dev);

[Severity: High]
As reported by Sashiko on v1 [2], is the teardown ordering of wakeup_timer and
wakeup_work safe against concurrent TTY operations during removal?

In ft260_remove() and ft260_uart_port_remove():

    ft260_remove()
        timer_delete_sync(&dev->wakeup_timer);
        cancel_work_sync(&dev->wakeup_work);
        tty_port_unregister_device(&dev->port, ft260_tty_driver, dev->index);
        ft260_uart_port_remove(dev)
            timer_delete_sync(&port->wakeup_timer);
            list_del(&port->device_list);
            tty_port_tty_vhangup(&port->port);

Because port->reschedule_work is only cleared when tty_port_tty_vhangup()
invokes ft260_uart_port_shutdown():

1. If wakeup_timer fires between cancel_work_sync() in ft260_remove() and
   timer_delete_sync() in ft260_uart_port_remove(), ft260_uart_start_wakeup()
   schedules wakeup_work, which is never cancelled afterwards (and the
   err_hid_report error path in ft260_uart_probe() never calls
   cancel_work_sync() at all).
2. Until list_del() and tty_port_tty_vhangup() complete, a concurrent
   ft260_uart_change_speed() or ft260_uart_port_activate() can set
   port->reschedule_work = true and re-arm wakeup_timer via mod_timer() after
   timer_delete_sync() has already run, leaving the timer or work active after
   hid_hw_stop() and ft260_uart_port_put() free dev.

Should timer_delete_sync(&port->wakeup_timer) and
cancel_work_sync(&port->wakeup_work) happen after list_del() and
tty_port_tty_vhangup() have finished shutting down the port?

> @@ -1797,6 +1844,13 @@ static int ft260_raw_event(struct hid_device *hdev, struct hid_report *report,
[ ... ]
> @@ -1837,12 +1884,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);

[Severity: High]
In ft260_probe(), tty_port_init(&dev->port) is only called on the UART path in
ft260_uart_probe(). When dev->iface_type == FT260_IFACE_I2C, dev->port is left
zero-initialized with dev->port.buf.tail == NULL.

If an input report in the range FT260_UART_REPORT_MIN..FT260_UART_REPORT_MAX
arrives on an I2C interface, ft260_raw_event() calls
ft260_uart_receive_chars() -> tty_insert_flip_string(&port->port, ...), which
dereferences port->port.buf.tail in __tty_buffer_request_room().

Should ft260_raw_event() check that dev->iface_type == FT260_IFACE_UART before
dispatching UART reports to ft260_uart_receive_chars()?

[1] https://lore.kernel.org/r/20260822215615.2D8541F000E9@smtp.kernel.org
[2] https://lore.kernel.org/r/20260822215615.2D8541F000E9@smtp.kernel.org

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261030223637.86907-1-michael.zaidman@gmail.com?part=2

^ permalink raw reply	[flat|nested] 25+ messages in thread

end of thread, other threads:[~2026-10-30 23:03 UTC | newest]

Thread overview: 25+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-30 22:36 [PATCH v2 00/15] HID: ft260: add UART and GPIO support, plus I2C fixes Michael Zaidman
2026-10-30 22:36 ` [PATCH v2 01/15] HID: ft260: add serial driver Michael Zaidman
2026-10-30 23:00   ` sashiko-bot
2026-10-30 22:36 ` [PATCH v2 02/15] HID: ft260: uart: fix the " Michael Zaidman
2026-10-30 23:03   ` sashiko-bot
2026-10-30 22:36 ` [PATCH v2 03/15] HID: ft260: add GPIO support Michael Zaidman
2026-10-30 22:53   ` sashiko-bot
2026-10-30 22:36 ` [PATCH v2 04/15] HID: ft260: i2c: reduce driver module loading time Michael Zaidman
2026-10-30 22:36 ` [PATCH v2 05/15] HID: ft260: i2c: silence sysfs store big-numbers Michael Zaidman
2026-10-30 22:47   ` sashiko-bot
2026-10-30 22:36 ` [PATCH v2 06/15] HID: ft260: i2c: reduce bus-error message severity Michael Zaidman
2026-10-30 22:44   ` sashiko-bot
2026-10-30 22:36 ` [PATCH v2 07/15] HID: ft260: uart: enable flow control Michael Zaidman
2026-10-30 22:36 ` [PATCH v2 08/15] HID: ft260: uart: add modem pins control via ioctl Michael Zaidman
2026-10-30 22:55   ` sashiko-bot
2026-10-30 22:36 ` [PATCH v2 09/15] HID: ft260: group sysfs attrs per HID interface Michael Zaidman
2026-10-30 22:48   ` sashiko-bot
2026-10-30 22:36 ` [PATCH v2 10/15] HID: ft260: i2c: fix large write transaction failure Michael Zaidman
2026-10-30 22:36 ` [PATCH v2 11/15] HID: ft260: workaround for TN_189 errata endpoint STALL after enumeration Michael Zaidman
2026-10-30 22:59   ` sashiko-bot
2026-10-30 22:36 ` [PATCH v2 12/15] HID: ft260: i2c: abort in-flight transfers with STOP before reset Michael Zaidman
2026-10-30 22:36 ` [PATCH v2 13/15] HID: ft260: i2c: reject an I2C clock outside the supported range Michael Zaidman
2026-10-30 22:36 ` [PATCH v2 14/15] HID: ft260: i2c: fix i2c_reset sysfs store return value Michael Zaidman
2026-10-30 22:36 ` [PATCH v2 15/15] HID: ft260: i2c: recover a bus held by a slave Michael Zaidman
2026-10-30 22:59   ` sashiko-bot

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox