Devicetree
 help / color / mirror / Atom feed
* [PATCH 0/7] Input: edt-ft5x06 - add SPI transport and FocalTech FT8722 support
@ 2026-10-17 14:07 Luka Panio
  2026-10-17 14:07 ` [PATCH 1/7] Input: edt-ft5x06 - split the bus independent core from the I2C driver Luka Panio
                   ` (6 more replies)
  0 siblings, 7 replies; 11+ messages in thread
From: Luka Panio @ 2026-10-17 14:07 UTC (permalink / raw)
  To: Dmitry Torokhov
  Cc: Rob Herring, Krzysztof Kozlowski, Conor Dooley, Henrik Rydberg,
	Douglas Anderson, linux-input, devicetree, dri-devel,
	linux-kernel, Luka Panio

The FocalTech FT8722 is a touch and display driver (TDDI) found in the
Nexphone (QCM6490). Its touch controller speaks the usual FocalTech
register protocol handled by edt-ft5x06, but it is connected over SPI,
has no flash and runs its firmware from RAM, and shares its power and
reset with the display panel.

This series:

 - splits the bus independent core of edt-ft5x06 from the I2C driver
   (patch 1) and moves the controller bring-up into a helper (patch 2),
 - adds an optional per-chip boot hook, used to download the firmware
   after every reset (patch 3), and a per-chip touch data register
   (patch 4),
 - documents the FT8722 touch controller as focaltech,ft8722-ts, keeping
   it distinct from the display side of the chip (patch 5),
 - adds an SPI transport with the FT8722 firmware loader (patch 6),
 - lets the driver follow a DRM panel, like i2c-hid does, so the touch
   controller is powered down and re-initialised together with the panel
   (patch 7).

Tested on the Nexphone by Nex Computers. A board DTS using it will
follow separately.

Luka Panio (7):
  Input: edt-ft5x06 - split the bus independent core from the I2C driver
  Input: edt-ft5x06 - move the controller bring-up into a helper
  Input: edt-ft5x06 - add an optional controller boot hook
  Input: edt-ft5x06 - allow a chip specific touch data register
  dt-bindings: input: touchscreen: edt-ft5x06: Add FocalTech FT8722
  Input: edt-ft5x06 - add SPI transport and FT8722 support
  Input: edt-ft5x06 - support following a DRM panel

 .../input/touchscreen/edt-ft5x06.yaml         |  37 ++
 drivers/input/touchscreen/Kconfig             |  15 +
 drivers/input/touchscreen/Makefile            |   1 +
 drivers/input/touchscreen/edt-ft5x06-spi.c    | 475 ++++++++++++++
 drivers/input/touchscreen/edt-ft5x06.c        | 585 ++++++++++++------
 drivers/input/touchscreen/edt-ft5x06.h        |  33 +
 6 files changed, 957 insertions(+), 189 deletions(-)
 create mode 100644 drivers/input/touchscreen/edt-ft5x06-spi.c
 create mode 100644 drivers/input/touchscreen/edt-ft5x06.h


base-commit: d419d682257d813f6cb7a28e42b35d0edf4ad038
-- 
2.55.0


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

* [PATCH 1/7] Input: edt-ft5x06 - split the bus independent core from the I2C driver
  2026-10-17 14:07 [PATCH 0/7] Input: edt-ft5x06 - add SPI transport and FocalTech FT8722 support Luka Panio
@ 2026-10-17 14:07 ` Luka Panio
  2026-10-17 14:07 ` [PATCH 2/7] Input: edt-ft5x06 - move the controller bring-up into a helper Luka Panio
                   ` (5 subsequent siblings)
  6 siblings, 0 replies; 11+ messages in thread
From: Luka Panio @ 2026-10-17 14:07 UTC (permalink / raw)
  To: Dmitry Torokhov
  Cc: Rob Herring, Krzysztof Kozlowski, Conor Dooley, Henrik Rydberg,
	Douglas Anderson, linux-input, devicetree, dri-devel,
	linux-kernel, Luka Panio

Some FocalTech controllers handled by this driver, such as the FT8722
TDDI, are connected over SPI. Prepare for an SPI transport by moving
everything that depends on the i2c_client into a small I2C probe.

The only visible change for the I2C devices is that the interrupt is now
requested with the device name (e.g. "1-0038") instead of the I2C client
name, as the core no longer has access to the latter.

Signed-off-by: Luka Panio <lukapanio@gmail.com>
---
 drivers/input/touchscreen/edt-ft5x06.c | 276 +++++++++++++------------
 drivers/input/touchscreen/edt-ft5x06.h |  30 +++
 2 files changed, 179 insertions(+), 127 deletions(-)
 create mode 100644 drivers/input/touchscreen/edt-ft5x06.h

diff --git a/drivers/input/touchscreen/edt-ft5x06.c b/drivers/input/touchscreen/edt-ft5x06.c
index e69d64c3db2d..543cc62088c7 100644
--- a/drivers/input/touchscreen/edt-ft5x06.c
+++ b/drivers/input/touchscreen/edt-ft5x06.c
@@ -34,6 +34,8 @@
 
 #include <linux/unaligned.h>
 
+#include "edt-ft5x06.h"
+
 #define WORK_REGISTER_THRESHOLD		0x00
 #define WORK_REGISTER_REPORT_RATE	0x08
 #define WORK_REGISTER_GAIN		0x30
@@ -106,7 +108,9 @@ struct edt_reg_addr {
 };
 
 struct edt_ft5x06_ts_data {
-	struct i2c_client *client;
+	struct device *dev;
+	int irq;
+	struct dentry *debugfs;
 	struct input_dev *input;
 	struct touchscreen_properties prop;
 	u16 num_x;
@@ -148,15 +152,6 @@ struct edt_ft5x06_ts_data {
 	unsigned int header_errors;
 };
 
-struct edt_i2c_chip_data {
-	int  max_support_points;
-};
-
-static const struct regmap_config edt_ft5x06_i2c_regmap_config = {
-	.reg_bits = 8,
-	.val_bits = 8,
-};
-
 static bool edt_ft5x06_ts_check_crc(struct edt_ft5x06_ts_data *tsdata,
 				    u8 *buf, int buflen)
 {
@@ -168,7 +163,7 @@ static bool edt_ft5x06_ts_check_crc(struct edt_ft5x06_ts_data *tsdata,
 
 	if (crc != buf[buflen - 1]) {
 		tsdata->crc_errors++;
-		dev_err_ratelimited(&tsdata->client->dev,
+		dev_err_ratelimited(tsdata->dev,
 				    "crc error: 0x%02x expected, got 0x%02x\n",
 				    crc, buf[buflen - 1]);
 		return false;
@@ -182,7 +177,7 @@ static int edt_M06_i2c_read(void *context, const void *reg_buf, size_t reg_size,
 {
 	struct device *dev = context;
 	struct i2c_client *i2c = to_i2c_client(dev);
-	struct edt_ft5x06_ts_data *tsdata = i2c_get_clientdata(i2c);
+	struct edt_ft5x06_ts_data *tsdata = dev_get_drvdata(dev);
 	struct i2c_msg xfer[2];
 	bool reg_read = false;
 	u8 addr;
@@ -258,7 +253,7 @@ static int edt_M06_i2c_write(void *context, const void *data, size_t count)
 {
 	struct device *dev = context;
 	struct i2c_client *i2c = to_i2c_client(dev);
-	struct edt_ft5x06_ts_data *tsdata = i2c_get_clientdata(i2c);
+	struct edt_ft5x06_ts_data *tsdata = dev_get_drvdata(dev);
 	u8 addr, val;
 	u8 wbuf[4];
 	struct i2c_msg xfer;
@@ -298,7 +293,7 @@ static const struct regmap_config edt_M06_i2c_regmap_config = {
 static irqreturn_t edt_ft5x06_ts_isr(int irq, void *dev_id)
 {
 	struct edt_ft5x06_ts_data *tsdata = dev_id;
-	struct device *dev = &tsdata->client->dev;
+	struct device *dev = tsdata->dev;
 	u8 rdbuf[63];
 	int i, type, x, y, id;
 	int error;
@@ -376,8 +371,7 @@ static ssize_t edt_ft5x06_setting_show(struct device *dev,
 				       struct device_attribute *dattr,
 				       char *buf)
 {
-	struct i2c_client *client = to_i2c_client(dev);
-	struct edt_ft5x06_ts_data *tsdata = i2c_get_clientdata(client);
+	struct edt_ft5x06_ts_data *tsdata = dev_get_drvdata(dev);
 	struct edt_ft5x06_attribute *attr =
 			container_of(dattr, struct edt_ft5x06_attribute, dattr);
 	u8 *field = (u8 *)tsdata + attr->field_offset;
@@ -412,7 +406,7 @@ static ssize_t edt_ft5x06_setting_show(struct device *dev,
 	if (addr != NO_REGISTER) {
 		error = regmap_read(tsdata->regmap, addr, &val);
 		if (error) {
-			dev_err(&tsdata->client->dev,
+			dev_err(tsdata->dev,
 				"Failed to fetch attribute %s, error %d\n",
 				dattr->attr.name, error);
 			return error;
@@ -422,7 +416,7 @@ static ssize_t edt_ft5x06_setting_show(struct device *dev,
 	}
 
 	if (val != *field) {
-		dev_warn(&tsdata->client->dev,
+		dev_warn(tsdata->dev,
 			 "%s: read (%d) and stored value (%d) differ\n",
 			 dattr->attr.name, val, *field);
 		*field = val;
@@ -435,8 +429,7 @@ static ssize_t edt_ft5x06_setting_store(struct device *dev,
 					struct device_attribute *dattr,
 					const char *buf, size_t count)
 {
-	struct i2c_client *client = to_i2c_client(dev);
-	struct edt_ft5x06_ts_data *tsdata = i2c_get_clientdata(client);
+	struct edt_ft5x06_ts_data *tsdata = dev_get_drvdata(dev);
 	struct edt_ft5x06_attribute *attr =
 			container_of(dattr, struct edt_ft5x06_attribute, dattr);
 	u8 *field = (u8 *)tsdata + attr->field_offset;
@@ -478,7 +471,7 @@ static ssize_t edt_ft5x06_setting_store(struct device *dev,
 	if (addr != NO_REGISTER) {
 		error = regmap_write(tsdata->regmap, addr, val);
 		if (error) {
-			dev_err(&tsdata->client->dev,
+			dev_err(tsdata->dev,
 				"Failed to update attribute %s, error: %d\n",
 				dattr->attr.name, error);
 			return error;
@@ -511,8 +504,7 @@ static EDT_ATTR(report_rate, S_IWUSR | S_IRUGO, WORK_REGISTER_REPORT_RATE,
 static ssize_t model_show(struct device *dev, struct device_attribute *attr,
 			  char *buf)
 {
-	struct i2c_client *client = to_i2c_client(dev);
-	struct edt_ft5x06_ts_data *tsdata = i2c_get_clientdata(client);
+	struct edt_ft5x06_ts_data *tsdata = dev_get_drvdata(dev);
 
 	return sysfs_emit(buf, "%s\n", tsdata->name);
 }
@@ -522,8 +514,7 @@ static DEVICE_ATTR_RO(model);
 static ssize_t fw_version_show(struct device *dev,
 			       struct device_attribute *attr, char *buf)
 {
-	struct i2c_client *client = to_i2c_client(dev);
-	struct edt_ft5x06_ts_data *tsdata = i2c_get_clientdata(client);
+	struct edt_ft5x06_ts_data *tsdata = dev_get_drvdata(dev);
 
 	return sysfs_emit(buf, "%s\n", tsdata->fw_version);
 }
@@ -534,8 +525,7 @@ static DEVICE_ATTR_RO(fw_version);
 static ssize_t header_errors_show(struct device *dev,
 				  struct device_attribute *attr, char *buf)
 {
-	struct i2c_client *client = to_i2c_client(dev);
-	struct edt_ft5x06_ts_data *tsdata = i2c_get_clientdata(client);
+	struct edt_ft5x06_ts_data *tsdata = dev_get_drvdata(dev);
 
 	return sysfs_emit(buf, "%d\n", tsdata->header_errors);
 }
@@ -546,8 +536,7 @@ static DEVICE_ATTR_RO(header_errors);
 static ssize_t crc_errors_show(struct device *dev,
 			       struct device_attribute *attr, char *buf)
 {
-	struct i2c_client *client = to_i2c_client(dev);
-	struct edt_ft5x06_ts_data *tsdata = i2c_get_clientdata(client);
+	struct edt_ft5x06_ts_data *tsdata = dev_get_drvdata(dev);
 
 	return sysfs_emit(buf, "%d\n", tsdata->crc_errors);
 }
@@ -567,7 +556,16 @@ static struct attribute *edt_ft5x06_attrs[] = {
 	&dev_attr_crc_errors.attr,
 	NULL
 };
-ATTRIBUTE_GROUPS(edt_ft5x06);
+
+static const struct attribute_group edt_ft5x06_group = {
+	.attrs = edt_ft5x06_attrs,
+};
+
+const struct attribute_group *edt_ft5x06_groups[] = {
+	&edt_ft5x06_group,
+	NULL
+};
+EXPORT_SYMBOL_NS_GPL(edt_ft5x06_groups, "EDT_FT5X06");
 
 static void edt_ft5x06_restore_reg_parameters(struct edt_ft5x06_ts_data *tsdata)
 {
@@ -590,18 +588,17 @@ static void edt_ft5x06_restore_reg_parameters(struct edt_ft5x06_ts_data *tsdata)
 #ifdef CONFIG_DEBUG_FS
 static int edt_ft5x06_factory_mode(struct edt_ft5x06_ts_data *tsdata)
 {
-	struct i2c_client *client = tsdata->client;
 	int retries = EDT_SWITCH_MODE_RETRIES;
 	unsigned int val;
 	int error;
 
 	if (tsdata->version != EDT_M06) {
-		dev_err(&client->dev,
+		dev_err(tsdata->dev,
 			"No factory mode support for non-M06 devices\n");
 		return -EINVAL;
 	}
 
-	disable_irq(client->irq);
+	disable_irq(tsdata->irq);
 
 	if (!tsdata->raw_buffer) {
 		tsdata->raw_bufsize = tsdata->num_x * tsdata->num_y *
@@ -616,7 +613,7 @@ static int edt_ft5x06_factory_mode(struct edt_ft5x06_ts_data *tsdata)
 	/* mode register is 0x3c when in the work mode */
 	error = regmap_write(tsdata->regmap, WORK_REGISTER_OPMODE, 0x03);
 	if (error) {
-		dev_err(&client->dev,
+		dev_err(tsdata->dev,
 			"failed to switch to factory mode, error %d\n", error);
 		goto err_out;
 	}
@@ -632,7 +629,7 @@ static int edt_ft5x06_factory_mode(struct edt_ft5x06_ts_data *tsdata)
 	} while (--retries > 0);
 
 	if (retries == 0) {
-		dev_err(&client->dev, "not in factory mode after %dms.\n",
+		dev_err(tsdata->dev, "not in factory mode after %dms.\n",
 			EDT_SWITCH_MODE_RETRIES * EDT_SWITCH_MODE_DELAY);
 		error = -EIO;
 		goto err_out;
@@ -644,14 +641,13 @@ static int edt_ft5x06_factory_mode(struct edt_ft5x06_ts_data *tsdata)
 	kfree(tsdata->raw_buffer);
 	tsdata->raw_buffer = NULL;
 	tsdata->factory_mode = false;
-	enable_irq(client->irq);
+	enable_irq(tsdata->irq);
 
 	return error;
 }
 
 static int edt_ft5x06_work_mode(struct edt_ft5x06_ts_data *tsdata)
 {
-	struct i2c_client *client = tsdata->client;
 	int retries = EDT_SWITCH_MODE_RETRIES;
 	unsigned int val;
 	int error;
@@ -659,7 +655,7 @@ static int edt_ft5x06_work_mode(struct edt_ft5x06_ts_data *tsdata)
 	/* mode register is 0x01 when in the factory mode */
 	error = regmap_write(tsdata->regmap, FACTORY_REGISTER_OPMODE, 0x1);
 	if (error) {
-		dev_err(&client->dev,
+		dev_err(tsdata->dev,
 			"failed to switch to work mode, error: %d\n", error);
 		return error;
 	}
@@ -675,7 +671,7 @@ static int edt_ft5x06_work_mode(struct edt_ft5x06_ts_data *tsdata)
 	} while (--retries > 0);
 
 	if (retries == 0) {
-		dev_err(&client->dev, "not in work mode after %dms.\n",
+		dev_err(tsdata->dev, "not in work mode after %dms.\n",
 			EDT_SWITCH_MODE_RETRIES * EDT_SWITCH_MODE_DELAY);
 		tsdata->factory_mode = true;
 		return -EIO;
@@ -685,7 +681,7 @@ static int edt_ft5x06_work_mode(struct edt_ft5x06_ts_data *tsdata)
 	tsdata->raw_buffer = NULL;
 
 	edt_ft5x06_restore_reg_parameters(tsdata);
-	enable_irq(client->irq);
+	enable_irq(tsdata->irq);
 
 	return 0;
 }
@@ -723,7 +719,6 @@ static ssize_t edt_ft5x06_debugfs_raw_data_read(struct file *file,
 						loff_t *off)
 {
 	struct edt_ft5x06_ts_data *tsdata = file->private_data;
-	struct i2c_client *client = tsdata->client;
 	int retries  = EDT_RAW_DATA_RETRIES;
 	unsigned int val;
 	int i, error;
@@ -741,7 +736,7 @@ static ssize_t edt_ft5x06_debugfs_raw_data_read(struct file *file,
 
 	error = regmap_write(tsdata->regmap, 0x08, 0x01);
 	if (error) {
-		dev_err(&client->dev,
+		dev_err(tsdata->dev,
 			"failed to write 0x08 register, error %d\n", error);
 		return error;
 	}
@@ -750,7 +745,7 @@ static ssize_t edt_ft5x06_debugfs_raw_data_read(struct file *file,
 		usleep_range(EDT_RAW_DATA_DELAY, EDT_RAW_DATA_DELAY + 100);
 		error = regmap_read(tsdata->regmap, 0x08, &val);
 		if (error) {
-			dev_err(&client->dev,
+			dev_err(tsdata->dev,
 				"failed to read 0x08 register, error %d\n",
 				error);
 			return error;
@@ -761,7 +756,7 @@ static ssize_t edt_ft5x06_debugfs_raw_data_read(struct file *file,
 	} while (--retries > 0);
 
 	if (retries == 0) {
-		dev_err(&client->dev,
+		dev_err(tsdata->dev,
 			"timed out waiting for register to settle\n");
 		return -ETIMEDOUT;
 	}
@@ -793,7 +788,10 @@ static const struct file_operations debugfs_raw_data_fops = {
 
 static void edt_ft5x06_ts_prepare_debugfs(struct edt_ft5x06_ts_data *tsdata)
 {
-	struct dentry *debug_dir = tsdata->client->debugfs;
+	struct dentry *debug_dir = tsdata->debugfs;
+
+	if (!debug_dir)
+		return;
 
 	debugfs_create_u16("num_x", S_IRUSR, debug_dir, &tsdata->num_x);
 	debugfs_create_u16("num_y", S_IRUSR, debug_dir, &tsdata->num_y);
@@ -829,9 +827,9 @@ static void edt_ft5x06_ts_teardown_debugfs(struct edt_ft5x06_ts_data *tsdata)
 
 #endif /* CONFIG_DEBUGFS */
 
-static int edt_ft5x06_ts_identify(struct i2c_client *client,
-				  struct edt_ft5x06_ts_data *tsdata)
+static int edt_ft5x06_ts_identify(struct edt_ft5x06_ts_data *tsdata)
 {
+	struct i2c_client *client;
 	u8 rdbuf[EDT_NAME_LEN];
 	char *p;
 	int error;
@@ -854,6 +852,11 @@ static int edt_ft5x06_ts_identify(struct i2c_client *client,
 	if (!strncasecmp(rdbuf + 1, "EP0", 3)) {
 		tsdata->version = EDT_M06;
 
+		/* M06 uses its own register protocol, only over I2C */
+		client = i2c_verify_client(tsdata->dev);
+		if (!client)
+			return -ENODEV;
+
 		/* remove last '$' end marker */
 		rdbuf[EDT_NAME_LEN - 1] = '\0';
 		if (rdbuf[EDT_NAME_LEN - 2] == '$')
@@ -870,7 +873,7 @@ static int edt_ft5x06_ts_identify(struct i2c_client *client,
 		tsdata->regmap = regmap_init_i2c(client,
 						 &edt_M06_i2c_regmap_config);
 		if (IS_ERR(tsdata->regmap)) {
-			dev_err(&client->dev, "regmap allocation failed\n");
+			dev_err(tsdata->dev, "regmap allocation failed\n");
 			return PTR_ERR(tsdata->regmap);
 		}
 	} else if (!strncasecmp(rdbuf, "EP0", 3)) {
@@ -1113,10 +1116,10 @@ static void edt_ft5x06_disable_regulators(void *arg)
 	regulator_disable(data->iovcc);
 }
 
-static int edt_ft5x06_ts_probe(struct i2c_client *client)
+int edt_ft5x06_probe(struct device *dev, struct regmap *regmap, int irq,
+		     const struct edt_ft5x06_chip_data *chip_data, u16 bustype,
+		     struct dentry *debugfs)
 {
-	const struct i2c_device_id *id = i2c_client_get_device_id(client);
-	const struct edt_i2c_chip_data *chip_data;
 	struct edt_ft5x06_ts_data *tsdata;
 	unsigned int val;
 	struct input_dev *input;
@@ -1124,57 +1127,50 @@ static int edt_ft5x06_ts_probe(struct i2c_client *client)
 	int error;
 	u32 report_rate;
 
-	dev_dbg(&client->dev, "probing for EDT FT5x06 I2C\n");
-
-	tsdata = devm_kzalloc(&client->dev, sizeof(*tsdata), GFP_KERNEL);
+	tsdata = devm_kzalloc(dev, sizeof(*tsdata), GFP_KERNEL);
 	if (!tsdata) {
-		dev_err(&client->dev, "failed to allocate driver data.\n");
+		regmap_exit(regmap);
 		return -ENOMEM;
 	}
 
-	tsdata->regmap = regmap_init_i2c(client, &edt_ft5x06_i2c_regmap_config);
-	if (IS_ERR(tsdata->regmap)) {
-		dev_err(&client->dev, "regmap allocation failed\n");
-		return PTR_ERR(tsdata->regmap);
-	}
+	tsdata->dev = dev;
+	tsdata->irq = irq;
+	tsdata->debugfs = debugfs;
+	tsdata->regmap = regmap;
 
 	/*
-	 * We are not using devm_regmap_init_i2c() and instead install a
-	 * custom action because we may replace regmap with M06-specific one
-	 * and we need to make sure that it will not be released too early.
+	 * The bus driver hands over a regmap that is not device managed
+	 * because we may replace it with the M06-specific one and we need to
+	 * make sure that it will not be released too early.
 	 */
-	error = devm_add_action_or_reset(&client->dev, edt_ft5x06_exit_regmap,
-					 tsdata);
+	error = devm_add_action_or_reset(dev, edt_ft5x06_exit_regmap, tsdata);
 	if (error)
 		return error;
 
-	chip_data = device_get_match_data(&client->dev);
-	if (!chip_data)
-		chip_data = (const struct edt_i2c_chip_data *)id->driver_data;
 	if (!chip_data || !chip_data->max_support_points) {
-		dev_err(&client->dev, "invalid or missing chip data\n");
+		dev_err(dev, "invalid or missing chip data\n");
 		return -EINVAL;
 	}
 
 	tsdata->max_support_points = chip_data->max_support_points;
 
-	tsdata->vcc = devm_regulator_get(&client->dev, "vcc");
+	tsdata->vcc = devm_regulator_get(dev, "vcc");
 	if (IS_ERR(tsdata->vcc))
-		return dev_err_probe(&client->dev, PTR_ERR(tsdata->vcc),
+		return dev_err_probe(dev, PTR_ERR(tsdata->vcc),
 				     "failed to request regulator\n");
 
-	tsdata->iovcc = devm_regulator_get(&client->dev, "iovcc");
+	tsdata->iovcc = devm_regulator_get(dev, "iovcc");
 	if (IS_ERR(tsdata->iovcc)) {
 		error = PTR_ERR(tsdata->iovcc);
 		if (error != -EPROBE_DEFER)
-			dev_err(&client->dev,
+			dev_err(dev,
 				"failed to request iovcc regulator: %d\n", error);
 		return error;
 	}
 
 	error = regulator_enable(tsdata->iovcc);
 	if (error < 0) {
-		dev_err(&client->dev, "failed to enable iovcc: %d\n", error);
+		dev_err(dev, "failed to enable iovcc: %d\n", error);
 		return error;
 	}
 
@@ -1183,32 +1179,30 @@ static int edt_ft5x06_ts_probe(struct i2c_client *client)
 
 	error = regulator_enable(tsdata->vcc);
 	if (error < 0) {
-		dev_err(&client->dev, "failed to enable vcc: %d\n", error);
+		dev_err(dev, "failed to enable vcc: %d\n", error);
 		regulator_disable(tsdata->iovcc);
 		return error;
 	}
 
-	error = devm_add_action_or_reset(&client->dev,
-					 edt_ft5x06_disable_regulators,
+	error = devm_add_action_or_reset(dev, edt_ft5x06_disable_regulators,
 					 tsdata);
 	if (error)
 		return error;
 
-	tsdata->reset_gpio = devm_gpiod_get_optional(&client->dev,
-						     "reset", GPIOD_OUT_HIGH);
+	tsdata->reset_gpio = devm_gpiod_get_optional(dev, "reset",
+						     GPIOD_OUT_HIGH);
 	if (IS_ERR(tsdata->reset_gpio)) {
 		error = PTR_ERR(tsdata->reset_gpio);
-		dev_err(&client->dev,
-			"Failed to request GPIO reset pin, error %d\n", error);
+		dev_err(dev, "Failed to request GPIO reset pin, error %d\n",
+			error);
 		return error;
 	}
 
-	tsdata->wake_gpio = devm_gpiod_get_optional(&client->dev,
-						    "wake", GPIOD_OUT_LOW);
+	tsdata->wake_gpio = devm_gpiod_get_optional(dev, "wake", GPIOD_OUT_LOW);
 	if (IS_ERR(tsdata->wake_gpio)) {
 		error = PTR_ERR(tsdata->wake_gpio);
-		dev_err(&client->dev,
-			"Failed to request GPIO wake pin, error %d\n", error);
+		dev_err(dev, "Failed to request GPIO wake pin, error %d\n",
+			error);
 		return error;
 	}
 
@@ -1237,21 +1231,20 @@ static int edt_ft5x06_ts_probe(struct i2c_client *client)
 		msleep(300);
 	}
 
-	input = devm_input_allocate_device(&client->dev);
+	input = devm_input_allocate_device(dev);
 	if (!input) {
-		dev_err(&client->dev, "failed to allocate input device.\n");
+		dev_err(dev, "failed to allocate input device.\n");
 		return -ENOMEM;
 	}
 
 	mutex_init(&tsdata->mutex);
-	tsdata->client = client;
 	tsdata->input = input;
 	tsdata->factory_mode = false;
-	i2c_set_clientdata(client, tsdata);
+	dev_set_drvdata(dev, tsdata);
 
-	error = edt_ft5x06_ts_identify(client, tsdata);
+	error = edt_ft5x06_ts_identify(tsdata);
 	if (error) {
-		dev_err(&client->dev, "touchscreen probe failed\n");
+		dev_err(dev, "touchscreen probe failed\n");
 		return error;
 	}
 
@@ -1263,19 +1256,18 @@ static int edt_ft5x06_ts_probe(struct i2c_client *client)
 
 	edt_ft5x06_ts_set_tdata_parameters(tsdata);
 	edt_ft5x06_ts_set_regs(tsdata);
-	edt_ft5x06_ts_get_defaults(&client->dev, tsdata);
+	edt_ft5x06_ts_get_defaults(dev, tsdata);
 	edt_ft5x06_ts_get_parameters(tsdata);
 
 	if (tsdata->reg_addr.reg_report_rate != NO_REGISTER &&
-	    !device_property_read_u32(&client->dev,
-				      "report-rate-hz", &report_rate)) {
+	    !device_property_read_u32(dev, "report-rate-hz", &report_rate)) {
 		if (tsdata->version == EDT_M06)
 			tsdata->report_rate = clamp_val(report_rate, 30, 140);
 		else
 			tsdata->report_rate = clamp_val(report_rate, 1, 255);
 
 		if (report_rate != tsdata->report_rate)
-			dev_warn(&client->dev,
+			dev_warn(dev,
 				 "report-rate %dHz is unsupported, use %dHz\n",
 				 report_rate, tsdata->report_rate);
 
@@ -1286,12 +1278,11 @@ static int edt_ft5x06_ts_probe(struct i2c_client *client)
 			     tsdata->report_rate);
 	}
 
-	dev_dbg(&client->dev,
-		"Model \"%s\", Rev. \"%s\", %dx%d sensors\n",
+	dev_dbg(dev, "Model \"%s\", Rev. \"%s\", %dx%d sensors\n",
 		tsdata->name, tsdata->fw_version, tsdata->num_x, tsdata->num_y);
 
 	input->name = tsdata->name;
-	input->id.bustype = BUS_I2C;
+	input->id.bustype = bustype;
 
 	input_set_abs_params(input, ABS_MT_POSITION_X,
 			     0, tsdata->num_x * 64 - 1, 0, 0);
@@ -1303,20 +1294,19 @@ static int edt_ft5x06_ts_probe(struct i2c_client *client)
 	error = input_mt_init_slots(input, tsdata->max_support_points,
 				    INPUT_MT_DIRECT);
 	if (error) {
-		dev_err(&client->dev, "Unable to init MT slots.\n");
+		dev_err(dev, "Unable to init MT slots.\n");
 		return error;
 	}
 
-	irq_flags = irq_get_trigger_type(client->irq);
+	irq_flags = irq_get_trigger_type(irq);
 	if (irq_flags == IRQF_TRIGGER_NONE)
 		irq_flags = IRQF_TRIGGER_FALLING;
 	irq_flags |= IRQF_ONESHOT;
 
-	error = devm_request_threaded_irq(&client->dev, client->irq,
-					  NULL, edt_ft5x06_ts_isr, irq_flags,
-					  client->name, tsdata);
+	error = devm_request_threaded_irq(dev, irq, NULL, edt_ft5x06_ts_isr,
+					  irq_flags, dev_name(dev), tsdata);
 	if (error) {
-		dev_err(&client->dev, "Unable to request touchscreen IRQ.\n");
+		dev_err(dev, "Unable to request touchscreen IRQ.\n");
 		return error;
 	}
 
@@ -1326,26 +1316,27 @@ static int edt_ft5x06_ts_probe(struct i2c_client *client)
 
 	edt_ft5x06_ts_prepare_debugfs(tsdata);
 
-	dev_dbg(&client->dev,
+	dev_dbg(dev,
 		"EDT FT5x06 initialized: IRQ %d, WAKE pin %d, Reset pin %d.\n",
-		client->irq,
+		irq,
 		tsdata->wake_gpio ? desc_to_gpio(tsdata->wake_gpio) : -1,
 		tsdata->reset_gpio ? desc_to_gpio(tsdata->reset_gpio) : -1);
 
 	return 0;
 }
+EXPORT_SYMBOL_NS_GPL(edt_ft5x06_probe, "EDT_FT5X06");
 
-static void edt_ft5x06_ts_remove(struct i2c_client *client)
+void edt_ft5x06_remove(struct device *dev)
 {
-	struct edt_ft5x06_ts_data *tsdata = i2c_get_clientdata(client);
+	struct edt_ft5x06_ts_data *tsdata = dev_get_drvdata(dev);
 
 	edt_ft5x06_ts_teardown_debugfs(tsdata);
 }
+EXPORT_SYMBOL_NS_GPL(edt_ft5x06_remove, "EDT_FT5X06");
 
 static int edt_ft5x06_ts_suspend(struct device *dev)
 {
-	struct i2c_client *client = to_i2c_client(dev);
-	struct edt_ft5x06_ts_data *tsdata = i2c_get_clientdata(client);
+	struct edt_ft5x06_ts_data *tsdata = dev_get_drvdata(dev);
 	struct gpio_desc *reset_gpio = tsdata->reset_gpio;
 	int ret;
 
@@ -1370,7 +1361,7 @@ static int edt_ft5x06_ts_suspend(struct device *dev)
 	 * settings. Disable the irq to avoid adjusting each host till the
 	 * device is back in a full functional state.
 	 */
-	disable_irq(tsdata->client->irq);
+	disable_irq(tsdata->irq);
 
 	gpiod_set_value_cansleep(reset_gpio, 1);
 	usleep_range(1000, 2000);
@@ -1387,8 +1378,7 @@ static int edt_ft5x06_ts_suspend(struct device *dev)
 
 static int edt_ft5x06_ts_resume(struct device *dev)
 {
-	struct i2c_client *client = to_i2c_client(dev);
-	struct edt_ft5x06_ts_data *tsdata = i2c_get_clientdata(client);
+	struct edt_ft5x06_ts_data *tsdata = dev_get_drvdata(dev);
 	int ret = 0;
 
 	if (device_may_wakeup(dev))
@@ -1432,7 +1422,7 @@ static int edt_ft5x06_ts_resume(struct device *dev)
 		msleep(300);
 
 		edt_ft5x06_restore_reg_parameters(tsdata);
-		enable_irq(tsdata->client->irq);
+		enable_irq(tsdata->irq);
 
 		if (tsdata->factory_mode)
 			ret = edt_ft5x06_factory_mode(tsdata);
@@ -1447,38 +1437,38 @@ static int edt_ft5x06_ts_resume(struct device *dev)
 	return ret;
 }
 
-static DEFINE_SIMPLE_DEV_PM_OPS(edt_ft5x06_ts_pm_ops,
-				edt_ft5x06_ts_suspend, edt_ft5x06_ts_resume);
+EXPORT_NS_GPL_SIMPLE_DEV_PM_OPS(edt_ft5x06_pm_ops, edt_ft5x06_ts_suspend,
+				edt_ft5x06_ts_resume, EDT_FT5X06);
 
-static const struct edt_i2c_chip_data edt_ft5x06_data = {
+static const struct edt_ft5x06_chip_data edt_ft5x06_data = {
 	.max_support_points = 5,
 };
 
-static const struct edt_i2c_chip_data edt_ft3518_data = {
+static const struct edt_ft5x06_chip_data edt_ft3518_data = {
 	.max_support_points = 10,
 };
 
-static const struct edt_i2c_chip_data edt_ft5452_data = {
+static const struct edt_ft5x06_chip_data edt_ft5452_data = {
 	.max_support_points = 5,
 };
 
-static const struct edt_i2c_chip_data edt_ft5506_data = {
+static const struct edt_ft5x06_chip_data edt_ft5506_data = {
 	.max_support_points = 10,
 };
 
-static const struct edt_i2c_chip_data edt_ft6236_data = {
+static const struct edt_ft5x06_chip_data edt_ft6236_data = {
 	.max_support_points = 2,
 };
 
-static const struct edt_i2c_chip_data edt_ft8201_data = {
+static const struct edt_ft5x06_chip_data edt_ft8201_data = {
 	.max_support_points = 10,
 };
 
-static const struct edt_i2c_chip_data edt_ft8716_data = {
+static const struct edt_ft5x06_chip_data edt_ft8716_data = {
 	.max_support_points = 10,
 };
 
-static const struct edt_i2c_chip_data edt_ft8719_data = {
+static const struct edt_ft5x06_chip_data edt_ft8719_data = {
 	.max_support_points = 10,
 };
 
@@ -1515,17 +1505,49 @@ static const struct of_device_id edt_ft5x06_of_match[] = {
 };
 MODULE_DEVICE_TABLE(of, edt_ft5x06_of_match);
 
+static const struct regmap_config edt_ft5x06_i2c_regmap_config = {
+	.reg_bits = 8,
+	.val_bits = 8,
+};
+
+static int edt_ft5x06_i2c_probe(struct i2c_client *client)
+{
+	const struct i2c_device_id *id = i2c_client_get_device_id(client);
+	const struct edt_ft5x06_chip_data *chip_data;
+	struct regmap *regmap;
+
+	dev_dbg(&client->dev, "probing for EDT FT5x06 I2C\n");
+
+	chip_data = device_get_match_data(&client->dev);
+	if (!chip_data && id)
+		chip_data = (const struct edt_ft5x06_chip_data *)id->driver_data;
+
+	regmap = regmap_init_i2c(client, &edt_ft5x06_i2c_regmap_config);
+	if (IS_ERR(regmap)) {
+		dev_err(&client->dev, "regmap allocation failed\n");
+		return PTR_ERR(regmap);
+	}
+
+	return edt_ft5x06_probe(&client->dev, regmap, client->irq, chip_data,
+				BUS_I2C, client->debugfs);
+}
+
+static void edt_ft5x06_i2c_remove(struct i2c_client *client)
+{
+	edt_ft5x06_remove(&client->dev);
+}
+
 static struct i2c_driver edt_ft5x06_ts_driver = {
 	.driver = {
 		.name = "edt_ft5x06",
 		.dev_groups = edt_ft5x06_groups,
 		.of_match_table = edt_ft5x06_of_match,
-		.pm = pm_sleep_ptr(&edt_ft5x06_ts_pm_ops),
+		.pm = pm_sleep_ptr(&edt_ft5x06_pm_ops),
 		.probe_type = PROBE_PREFER_ASYNCHRONOUS,
 	},
 	.id_table = edt_ft5x06_ts_id,
-	.probe    = edt_ft5x06_ts_probe,
-	.remove   = edt_ft5x06_ts_remove,
+	.probe    = edt_ft5x06_i2c_probe,
+	.remove   = edt_ft5x06_i2c_remove,
 };
 
 module_i2c_driver(edt_ft5x06_ts_driver);
diff --git a/drivers/input/touchscreen/edt-ft5x06.h b/drivers/input/touchscreen/edt-ft5x06.h
new file mode 100644
index 000000000000..deadc756cd74
--- /dev/null
+++ b/drivers/input/touchscreen/edt-ft5x06.h
@@ -0,0 +1,30 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Bus independent core of the EDT FT5x06 touchscreen driver
+ *
+ * Copyright (C) 2026 Luka Panio, <lukapanio@gmail.com>
+ */
+#ifndef _EDT_FT5X06_H
+#define _EDT_FT5X06_H
+
+#include <linux/types.h>
+
+struct attribute_group;
+struct dentry;
+struct dev_pm_ops;
+struct device;
+struct regmap;
+
+struct edt_ft5x06_chip_data {
+	int max_support_points;
+};
+
+int edt_ft5x06_probe(struct device *dev, struct regmap *regmap, int irq,
+		     const struct edt_ft5x06_chip_data *chip_data, u16 bustype,
+		     struct dentry *debugfs);
+void edt_ft5x06_remove(struct device *dev);
+
+extern const struct dev_pm_ops edt_ft5x06_pm_ops;
+extern const struct attribute_group *edt_ft5x06_groups[];
+
+#endif
-- 
2.55.0


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

* [PATCH 2/7] Input: edt-ft5x06 - move the controller bring-up into a helper
  2026-10-17 14:07 [PATCH 0/7] Input: edt-ft5x06 - add SPI transport and FocalTech FT8722 support Luka Panio
  2026-10-17 14:07 ` [PATCH 1/7] Input: edt-ft5x06 - split the bus independent core from the I2C driver Luka Panio
@ 2026-10-17 14:07 ` Luka Panio
  2026-10-17 14:16   ` sashiko-bot
  2026-10-17 14:07 ` [PATCH 3/7] Input: edt-ft5x06 - add an optional controller boot hook Luka Panio
                   ` (4 subsequent siblings)
  6 siblings, 1 reply; 11+ messages in thread
From: Luka Panio @ 2026-10-17 14:07 UTC (permalink / raw)
  To: Dmitry Torokhov
  Cc: Rob Herring, Krzysztof Kozlowski, Conor Dooley, Henrik Rydberg,
	Douglas Anderson, linux-input, devicetree, dri-devel,
	linux-kernel, Luka Panio

Move the part of probe that takes the controller out of reset,
identifies it and registers the input device into a helper, so it
can also be run after the probe.

Signed-off-by: Luka Panio <lukapanio@gmail.com>
---
 drivers/input/touchscreen/edt-ft5x06.c | 154 ++++++++++++++-----------
 1 file changed, 86 insertions(+), 68 deletions(-)

diff --git a/drivers/input/touchscreen/edt-ft5x06.c b/drivers/input/touchscreen/edt-ft5x06.c
index 543cc62088c7..536d633d97a7 100644
--- a/drivers/input/touchscreen/edt-ft5x06.c
+++ b/drivers/input/touchscreen/edt-ft5x06.c
@@ -1116,16 +1116,96 @@ static void edt_ft5x06_disable_regulators(void *arg)
 	regulator_disable(data->iovcc);
 }
 
+/*
+ * Take the controller out of reset, identify it and register the input
+ * device.
+ */
+static int edt_ft5x06_ts_init_hw(struct edt_ft5x06_ts_data *tsdata)
+{
+	struct device *dev = tsdata->dev;
+	struct input_dev *input = tsdata->input;
+	unsigned int val;
+	u32 report_rate;
+	int error;
+
+	if (tsdata->reset_gpio) {
+		usleep_range(5000, 6000);
+		gpiod_set_value_cansleep(tsdata->reset_gpio, 0);
+		msleep(300);
+	}
+
+	error = edt_ft5x06_ts_identify(tsdata);
+	if (error) {
+		dev_err(dev, "touchscreen probe failed\n");
+		return error;
+	}
+
+	/*
+	 * Dummy read access. EP0700MLP1 returns bogus data on the first
+	 * register read access and ignores writes.
+	 */
+	regmap_read(tsdata->regmap, 0x00, &val);
+
+	edt_ft5x06_ts_set_tdata_parameters(tsdata);
+	edt_ft5x06_ts_set_regs(tsdata);
+	edt_ft5x06_ts_get_defaults(dev, tsdata);
+	edt_ft5x06_ts_get_parameters(tsdata);
+
+	if (tsdata->reg_addr.reg_report_rate != NO_REGISTER &&
+	    !device_property_read_u32(dev, "report-rate-hz", &report_rate)) {
+		if (tsdata->version == EDT_M06)
+			tsdata->report_rate = clamp_val(report_rate, 30, 140);
+		else
+			tsdata->report_rate = clamp_val(report_rate, 1, 255);
+
+		if (report_rate != tsdata->report_rate)
+			dev_warn(dev,
+				 "report-rate %dHz is unsupported, use %dHz\n",
+				 report_rate, tsdata->report_rate);
+
+		if (tsdata->version == EDT_M06)
+			tsdata->report_rate /= 10;
+
+		regmap_write(tsdata->regmap, tsdata->reg_addr.reg_report_rate,
+			     tsdata->report_rate);
+	}
+
+	dev_dbg(dev, "Model \"%s\", Rev. \"%s\", %dx%d sensors\n",
+		tsdata->name, tsdata->fw_version, tsdata->num_x, tsdata->num_y);
+
+	input->name = tsdata->name;
+
+	input_set_abs_params(input, ABS_MT_POSITION_X,
+			     0, tsdata->num_x * 64 - 1, 0, 0);
+	input_set_abs_params(input, ABS_MT_POSITION_Y,
+			     0, tsdata->num_y * 64 - 1, 0, 0);
+
+	touchscreen_parse_properties(input, true, &tsdata->prop);
+
+	error = input_mt_init_slots(input, tsdata->max_support_points,
+				    INPUT_MT_DIRECT);
+	if (error) {
+		dev_err(dev, "Unable to init MT slots.\n");
+		return error;
+	}
+
+	error = input_register_device(input);
+	if (error)
+		return error;
+
+	edt_ft5x06_ts_prepare_debugfs(tsdata);
+
+	return 0;
+}
+
 int edt_ft5x06_probe(struct device *dev, struct regmap *regmap, int irq,
 		     const struct edt_ft5x06_chip_data *chip_data, u16 bustype,
 		     struct dentry *debugfs)
 {
 	struct edt_ft5x06_ts_data *tsdata;
-	unsigned int val;
 	struct input_dev *input;
 	unsigned long irq_flags;
 	int error;
-	u32 report_rate;
 
 	tsdata = devm_kzalloc(dev, sizeof(*tsdata), GFP_KERNEL);
 	if (!tsdata) {
@@ -1225,78 +1305,22 @@ int edt_ft5x06_probe(struct device *dev, struct regmap *regmap, int irq,
 		usleep_range(5000, 6000);
 	}
 
-	if (tsdata->reset_gpio) {
-		usleep_range(5000, 6000);
-		gpiod_set_value_cansleep(tsdata->reset_gpio, 0);
-		msleep(300);
-	}
-
 	input = devm_input_allocate_device(dev);
 	if (!input) {
 		dev_err(dev, "failed to allocate input device.\n");
 		return -ENOMEM;
 	}
 
+	input->id.bustype = bustype;
+
 	mutex_init(&tsdata->mutex);
 	tsdata->input = input;
 	tsdata->factory_mode = false;
 	dev_set_drvdata(dev, tsdata);
 
-	error = edt_ft5x06_ts_identify(tsdata);
-	if (error) {
-		dev_err(dev, "touchscreen probe failed\n");
-		return error;
-	}
-
-	/*
-	 * Dummy read access. EP0700MLP1 returns bogus data on the first
-	 * register read access and ignores writes.
-	 */
-	regmap_read(tsdata->regmap, 0x00, &val);
-
-	edt_ft5x06_ts_set_tdata_parameters(tsdata);
-	edt_ft5x06_ts_set_regs(tsdata);
-	edt_ft5x06_ts_get_defaults(dev, tsdata);
-	edt_ft5x06_ts_get_parameters(tsdata);
-
-	if (tsdata->reg_addr.reg_report_rate != NO_REGISTER &&
-	    !device_property_read_u32(dev, "report-rate-hz", &report_rate)) {
-		if (tsdata->version == EDT_M06)
-			tsdata->report_rate = clamp_val(report_rate, 30, 140);
-		else
-			tsdata->report_rate = clamp_val(report_rate, 1, 255);
-
-		if (report_rate != tsdata->report_rate)
-			dev_warn(dev,
-				 "report-rate %dHz is unsupported, use %dHz\n",
-				 report_rate, tsdata->report_rate);
-
-		if (tsdata->version == EDT_M06)
-			tsdata->report_rate /= 10;
-
-		regmap_write(tsdata->regmap, tsdata->reg_addr.reg_report_rate,
-			     tsdata->report_rate);
-	}
-
-	dev_dbg(dev, "Model \"%s\", Rev. \"%s\", %dx%d sensors\n",
-		tsdata->name, tsdata->fw_version, tsdata->num_x, tsdata->num_y);
-
-	input->name = tsdata->name;
-	input->id.bustype = bustype;
-
-	input_set_abs_params(input, ABS_MT_POSITION_X,
-			     0, tsdata->num_x * 64 - 1, 0, 0);
-	input_set_abs_params(input, ABS_MT_POSITION_Y,
-			     0, tsdata->num_y * 64 - 1, 0, 0);
-
-	touchscreen_parse_properties(input, true, &tsdata->prop);
-
-	error = input_mt_init_slots(input, tsdata->max_support_points,
-				    INPUT_MT_DIRECT);
-	if (error) {
-		dev_err(dev, "Unable to init MT slots.\n");
+	error = edt_ft5x06_ts_init_hw(tsdata);
+	if (error)
 		return error;
-	}
 
 	irq_flags = irq_get_trigger_type(irq);
 	if (irq_flags == IRQF_TRIGGER_NONE)
@@ -1310,12 +1334,6 @@ int edt_ft5x06_probe(struct device *dev, struct regmap *regmap, int irq,
 		return error;
 	}
 
-	error = input_register_device(input);
-	if (error)
-		return error;
-
-	edt_ft5x06_ts_prepare_debugfs(tsdata);
-
 	dev_dbg(dev,
 		"EDT FT5x06 initialized: IRQ %d, WAKE pin %d, Reset pin %d.\n",
 		irq,
-- 
2.55.0


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

* [PATCH 3/7] Input: edt-ft5x06 - add an optional controller boot hook
  2026-10-17 14:07 [PATCH 0/7] Input: edt-ft5x06 - add SPI transport and FocalTech FT8722 support Luka Panio
  2026-10-17 14:07 ` [PATCH 1/7] Input: edt-ft5x06 - split the bus independent core from the I2C driver Luka Panio
  2026-10-17 14:07 ` [PATCH 2/7] Input: edt-ft5x06 - move the controller bring-up into a helper Luka Panio
@ 2026-10-17 14:07 ` Luka Panio
  2026-10-17 14:07 ` [PATCH 4/7] Input: edt-ft5x06 - allow a chip specific touch data register Luka Panio
                   ` (3 subsequent siblings)
  6 siblings, 0 replies; 11+ messages in thread
From: Luka Panio @ 2026-10-17 14:07 UTC (permalink / raw)
  To: Dmitry Torokhov
  Cc: Rob Herring, Krzysztof Kozlowski, Conor Dooley, Henrik Rydberg,
	Douglas Anderson, linux-input, devicetree, dri-devel,
	linux-kernel, Luka Panio

Some FocalTech TDDI controllers have no flash and run their firmware
from RAM, which is lost whenever the controller is reset or powered
down. Allow the chip data to provide a boot callback that is run right
after the controller is taken out of reset, both at probe time before
the controller is identified and when resuming from the power-off sleep
mode before the register settings are restored.

The boot ROM of these controllers only accepts commands shortly after
reset, so skip the 300 ms settle delay for them and pass the reset GPIO
to the callback, letting it restart the boot ROM when it retries.

Signed-off-by: Luka Panio <lukapanio@gmail.com>
---
 drivers/input/touchscreen/edt-ft5x06.c | 25 +++++++++++++++++++++++--
 drivers/input/touchscreen/edt-ft5x06.h |  2 ++
 2 files changed, 25 insertions(+), 2 deletions(-)

diff --git a/drivers/input/touchscreen/edt-ft5x06.c b/drivers/input/touchscreen/edt-ft5x06.c
index 536d633d97a7..e57611e48217 100644
--- a/drivers/input/touchscreen/edt-ft5x06.c
+++ b/drivers/input/touchscreen/edt-ft5x06.c
@@ -148,6 +148,7 @@ struct edt_ft5x06_ts_data {
 
 	struct edt_reg_addr reg_addr;
 	enum edt_ver version;
+	int (*boot)(struct device *dev, struct gpio_desc *reset_gpio);
 	unsigned int crc_errors;
 	unsigned int header_errors;
 };
@@ -1131,7 +1132,16 @@ static int edt_ft5x06_ts_init_hw(struct edt_ft5x06_ts_data *tsdata)
 	if (tsdata->reset_gpio) {
 		usleep_range(5000, 6000);
 		gpiod_set_value_cansleep(tsdata->reset_gpio, 0);
-		msleep(300);
+		/* Controllers with a boot callback handle their own timing */
+		if (!tsdata->boot)
+			msleep(300);
+	}
+
+	if (tsdata->boot) {
+		error = tsdata->boot(dev, tsdata->reset_gpio);
+		if (error)
+			return dev_err_probe(dev, error,
+					     "failed to boot the controller\n");
 	}
 
 	error = edt_ft5x06_ts_identify(tsdata);
@@ -1233,6 +1243,7 @@ int edt_ft5x06_probe(struct device *dev, struct regmap *regmap, int irq,
 	}
 
 	tsdata->max_support_points = chip_data->max_support_points;
+	tsdata->boot = chip_data->boot;
 
 	tsdata->vcc = devm_regulator_get(dev, "vcc");
 	if (IS_ERR(tsdata->vcc))
@@ -1437,7 +1448,17 @@ static int edt_ft5x06_ts_resume(struct device *dev)
 
 		usleep_range(1000, 2000);
 		gpiod_set_value_cansleep(reset_gpio, 0);
-		msleep(300);
+
+		if (tsdata->boot) {
+			ret = tsdata->boot(dev, reset_gpio);
+			if (ret) {
+				dev_err(dev, "Failed to boot the controller: %d\n",
+					ret);
+				return ret;
+			}
+		} else {
+			msleep(300);
+		}
 
 		edt_ft5x06_restore_reg_parameters(tsdata);
 		enable_irq(tsdata->irq);
diff --git a/drivers/input/touchscreen/edt-ft5x06.h b/drivers/input/touchscreen/edt-ft5x06.h
index deadc756cd74..7eaeb562d11f 100644
--- a/drivers/input/touchscreen/edt-ft5x06.h
+++ b/drivers/input/touchscreen/edt-ft5x06.h
@@ -13,10 +13,12 @@ struct attribute_group;
 struct dentry;
 struct dev_pm_ops;
 struct device;
+struct gpio_desc;
 struct regmap;
 
 struct edt_ft5x06_chip_data {
 	int max_support_points;
+	int (*boot)(struct device *dev, struct gpio_desc *reset_gpio);
 };
 
 int edt_ft5x06_probe(struct device *dev, struct regmap *regmap, int irq,
-- 
2.55.0


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

* [PATCH 4/7] Input: edt-ft5x06 - allow a chip specific touch data register
  2026-10-17 14:07 [PATCH 0/7] Input: edt-ft5x06 - add SPI transport and FocalTech FT8722 support Luka Panio
                   ` (2 preceding siblings ...)
  2026-10-17 14:07 ` [PATCH 3/7] Input: edt-ft5x06 - add an optional controller boot hook Luka Panio
@ 2026-10-17 14:07 ` Luka Panio
  2026-10-17 14:07 ` [PATCH 5/7] dt-bindings: input: touchscreen: edt-ft5x06: Add FocalTech FT8722 Luka Panio
                   ` (2 subsequent siblings)
  6 siblings, 0 replies; 11+ messages in thread
From: Luka Panio @ 2026-10-17 14:07 UTC (permalink / raw)
  To: Dmitry Torokhov
  Cc: Rob Herring, Krzysztof Kozlowski, Conor Dooley, Henrik Rydberg,
	Douglas Anderson, linux-input, devicetree, dri-devel,
	linux-kernel, Luka Panio

The generic FocalTech path reads the touch report starting at register
0x00 and expects the points from register 0x03 onwards. Over SPI the
FT8722 only returns the report when the read starts at register 0x01;
a read from 0x00 returns the static register block instead, the
interrupt is never acknowledged and no further touch events arrive.

Let the chip data select the register the report is read from, keeping
the points at register 0x03.

Signed-off-by: Luka Panio <lukapanio@gmail.com>
---
 drivers/input/touchscreen/edt-ft5x06.c | 6 ++++--
 drivers/input/touchscreen/edt-ft5x06.h | 1 +
 2 files changed, 5 insertions(+), 2 deletions(-)

diff --git a/drivers/input/touchscreen/edt-ft5x06.c b/drivers/input/touchscreen/edt-ft5x06.c
index e57611e48217..7bcc48e07d62 100644
--- a/drivers/input/touchscreen/edt-ft5x06.c
+++ b/drivers/input/touchscreen/edt-ft5x06.c
@@ -149,6 +149,7 @@ struct edt_ft5x06_ts_data {
 	struct edt_reg_addr reg_addr;
 	enum edt_ver version;
 	int (*boot)(struct device *dev, struct gpio_desc *reset_gpio);
+	u8 tdata_reg;
 	unsigned int crc_errors;
 	unsigned int header_errors;
 };
@@ -1037,8 +1038,8 @@ static void edt_ft5x06_ts_set_tdata_parameters(struct edt_ft5x06_ts_data *tsdata
 		tsdata->point_len = 4;
 		crclen = 1;
 	} else {
-		tsdata->tdata_cmd = 0x0;
-		tsdata->tdata_offset = 3;
+		tsdata->tdata_cmd = tsdata->tdata_reg;
+		tsdata->tdata_offset = 3 - tsdata->tdata_reg;
 		tsdata->point_len = 6;
 		crclen = 0;
 	}
@@ -1244,6 +1245,7 @@ int edt_ft5x06_probe(struct device *dev, struct regmap *regmap, int irq,
 
 	tsdata->max_support_points = chip_data->max_support_points;
 	tsdata->boot = chip_data->boot;
+	tsdata->tdata_reg = chip_data->tdata_reg;
 
 	tsdata->vcc = devm_regulator_get(dev, "vcc");
 	if (IS_ERR(tsdata->vcc))
diff --git a/drivers/input/touchscreen/edt-ft5x06.h b/drivers/input/touchscreen/edt-ft5x06.h
index 7eaeb562d11f..c55686e288b6 100644
--- a/drivers/input/touchscreen/edt-ft5x06.h
+++ b/drivers/input/touchscreen/edt-ft5x06.h
@@ -18,6 +18,7 @@ struct regmap;
 
 struct edt_ft5x06_chip_data {
 	int max_support_points;
+	u8 tdata_reg;
 	int (*boot)(struct device *dev, struct gpio_desc *reset_gpio);
 };
 
-- 
2.55.0


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

* [PATCH 5/7] dt-bindings: input: touchscreen: edt-ft5x06: Add FocalTech FT8722
  2026-10-17 14:07 [PATCH 0/7] Input: edt-ft5x06 - add SPI transport and FocalTech FT8722 support Luka Panio
                   ` (3 preceding siblings ...)
  2026-10-17 14:07 ` [PATCH 4/7] Input: edt-ft5x06 - allow a chip specific touch data register Luka Panio
@ 2026-10-17 14:07 ` Luka Panio
  2026-10-17 14:07 ` [PATCH 6/7] Input: edt-ft5x06 - add SPI transport and FT8722 support Luka Panio
  2026-10-17 14:07 ` [PATCH 7/7] Input: edt-ft5x06 - support following a DRM panel Luka Panio
  6 siblings, 0 replies; 11+ messages in thread
From: Luka Panio @ 2026-10-17 14:07 UTC (permalink / raw)
  To: Dmitry Torokhov
  Cc: Rob Herring, Krzysztof Kozlowski, Conor Dooley, Henrik Rydberg,
	Douglas Anderson, linux-input, devicetree, dri-devel,
	linux-kernel, Luka Panio

The FT8722 is a touch and display driver (TDDI) whose touch controller
is connected over SPI and runs its firmware from RAM. Document its
compatible, allow the SPI peripheral properties and add a
firmware-name property for the firmware to download at power up.

The "-ts" suffix keeps the compatible distinct from the one of the
display side of the same chip, which is a separate DSI device. The
reset GPIO is required, as the firmware can only be downloaded while
the boot ROM is running right after a reset.

Signed-off-by: Luka Panio <lukapanio@gmail.com>
---
 .../input/touchscreen/edt-ft5x06.yaml         | 37 +++++++++++++++++++
 1 file changed, 37 insertions(+)

diff --git a/Documentation/devicetree/bindings/input/touchscreen/edt-ft5x06.yaml b/Documentation/devicetree/bindings/input/touchscreen/edt-ft5x06.yaml
index 68b2f1601654..51d22ff42675 100644
--- a/Documentation/devicetree/bindings/input/touchscreen/edt-ft5x06.yaml
+++ b/Documentation/devicetree/bindings/input/touchscreen/edt-ft5x06.yaml
@@ -19,6 +19,7 @@ maintainers:
 
 allOf:
   - $ref: touchscreen.yaml#
+  - $ref: /schemas/spi/spi-peripheral-props.yaml#
   - if:
       properties:
         compatible:
@@ -30,6 +31,17 @@ allOf:
       properties:
         offset-x: true
         offset-y: true
+  - if:
+      properties:
+        compatible:
+          contains:
+            const: focaltech,ft8722-ts
+    then:
+      required:
+        - reset-gpios
+    else:
+      properties:
+        firmware-name: false
 
 properties:
   compatible:
@@ -47,6 +59,7 @@ properties:
           - focaltech,ft8201
           - focaltech,ft8716
           - focaltech,ft8719
+          - focaltech,ft8722-ts
       - items:
           - const: focaltech,ft3519
           - const: focaltech,ft3518
@@ -60,6 +73,12 @@ properties:
   reset-gpios:
     maxItems: 1
 
+  firmware-name:
+    maxItems: 1
+    description:
+      Firmware to download at power up, for controllers that run their
+      firmware from RAM, such as the FT8722.
+
   wake-gpios:
     maxItems: 1
 
@@ -134,5 +153,23 @@ examples:
         wake-gpios = <&gpio4 9 GPIO_ACTIVE_HIGH>;
       };
     };
+  - |
+    #include <dt-bindings/gpio/gpio.h>
+    #include <dt-bindings/interrupt-controller/irq.h>
+    spi {
+      #address-cells = <1>;
+      #size-cells = <0>;
+      touchscreen@0 {
+        compatible = "focaltech,ft8722-ts";
+        reg = <0>;
+        spi-max-frequency = <6000000>;
+        interrupt-parent = <&tlmm>;
+        interrupts = <81 IRQ_TYPE_EDGE_FALLING>;
+        reset-gpios = <&tlmm 105 GPIO_ACTIVE_LOW>;
+        firmware-name = "qcom/qcm6490/nexphone/ft8722.bin";
+        touchscreen-size-x = <1080>;
+        touchscreen-size-y = <2408>;
+      };
+    };
 
 ...
-- 
2.55.0


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

* [PATCH 6/7] Input: edt-ft5x06 - add SPI transport and FT8722 support
  2026-10-17 14:07 [PATCH 0/7] Input: edt-ft5x06 - add SPI transport and FocalTech FT8722 support Luka Panio
                   ` (4 preceding siblings ...)
  2026-10-17 14:07 ` [PATCH 5/7] dt-bindings: input: touchscreen: edt-ft5x06: Add FocalTech FT8722 Luka Panio
@ 2026-10-17 14:07 ` Luka Panio
  2026-10-17 14:29   ` sashiko-bot
  2026-10-17 14:07 ` [PATCH 7/7] Input: edt-ft5x06 - support following a DRM panel Luka Panio
  6 siblings, 1 reply; 11+ messages in thread
From: Luka Panio @ 2026-10-17 14:07 UTC (permalink / raw)
  To: Dmitry Torokhov
  Cc: Rob Herring, Krzysztof Kozlowski, Conor Dooley, Henrik Rydberg,
	Douglas Anderson, linux-input, devicetree, dri-devel,
	linux-kernel, Luka Panio

Add an SPI bus driver and FT8722 support to edt-ft5x06.

The FT8722 TDDI has no flash, so add a boot hook that downloads the
firmware every time the controller comes out of reset: enter the boot
ROM, write the program and data RAM, verify both with the ROM's ECC
and start the application.

Signed-off-by: Luka Panio <lukapanio@gmail.com>
---
 drivers/input/touchscreen/Kconfig          |  14 +
 drivers/input/touchscreen/Makefile         |   1 +
 drivers/input/touchscreen/edt-ft5x06-spi.c | 475 +++++++++++++++++++++
 3 files changed, 490 insertions(+)
 create mode 100644 drivers/input/touchscreen/edt-ft5x06-spi.c

diff --git a/drivers/input/touchscreen/Kconfig b/drivers/input/touchscreen/Kconfig
index 9b9ae8ac3f7f..92840ea9c65f 100644
--- a/drivers/input/touchscreen/Kconfig
+++ b/drivers/input/touchscreen/Kconfig
@@ -805,6 +805,20 @@ config TOUCHSCREEN_EDT_FT5X06
 	  To compile this driver as a module, choose M here: the
 	  module will be called edt-ft5x06.
 
+config TOUCHSCREEN_EDT_FT5X06_SPI
+	tristate "EDT FocalTech FT5x06 SPI Touchscreen support"
+	depends on TOUCHSCREEN_EDT_FT5X06 && SPI
+	select CRC_CCITT
+	help
+	  Say Y here if you have a FocalTech touchscreen controller based
+	  on the FocalTech FT5x06 family of controllers that is connected over
+	  SPI.
+
+	  If unsure, say N.
+
+	  To compile this driver as a module, choose M here: the
+	  module will be called edt-ft5x06-spi.
+
 config TOUCHSCREEN_RASPBERRYPI_FW
 	tristate "Raspberry Pi's firmware base touch screen support"
 	depends on RASPBERRYPI_FIRMWARE || (RASPBERRYPI_FIRMWARE=n && COMPILE_TEST)
diff --git a/drivers/input/touchscreen/Makefile b/drivers/input/touchscreen/Makefile
index bfd9de83389d..263c7e8039af 100644
--- a/drivers/input/touchscreen/Makefile
+++ b/drivers/input/touchscreen/Makefile
@@ -33,6 +33,7 @@ obj-$(CONFIG_TOUCHSCREEN_DA9034)	+= da9034-ts.o
 obj-$(CONFIG_TOUCHSCREEN_DA9052)	+= da9052_tsi.o
 obj-$(CONFIG_TOUCHSCREEN_DYNAPRO)	+= dynapro.o
 obj-$(CONFIG_TOUCHSCREEN_EDT_FT5X06)	+= edt-ft5x06.o
+obj-$(CONFIG_TOUCHSCREEN_EDT_FT5X06_SPI)	+= edt-ft5x06-spi.o
 obj-$(CONFIG_TOUCHSCREEN_HAMPSHIRE)	+= hampshire.o
 obj-$(CONFIG_TOUCHSCREEN_HYCON_HY46XX)	+= hycon-hy46xx.o
 obj-$(CONFIG_TOUCHSCREEN_GUNZE)		+= gunze.o
diff --git a/drivers/input/touchscreen/edt-ft5x06-spi.c b/drivers/input/touchscreen/edt-ft5x06-spi.c
new file mode 100644
index 000000000000..d0ae0d4b621d
--- /dev/null
+++ b/drivers/input/touchscreen/edt-ft5x06-spi.c
@@ -0,0 +1,475 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright (C) 2026 Luka Panio, <lukapanio@gmail.com>
+ *
+ * SPI transport for FocalTech touchscreen controllers handled by the
+ * edt-ft5x06 driver, with firmware download for the RAM-only FT8722.
+ *
+ * Protocol and boot sequence based on the FocalTech vendor driver:
+ * Copyright (c) 2012-2020, FocalTech Systems, Ltd.
+ */
+
+#include <linux/crc-ccitt.h>
+#include <linux/delay.h>
+#include <linux/firmware.h>
+#include <linux/gpio/consumer.h>
+#include <linux/input.h>
+#include <linux/minmax.h>
+#include <linux/mod_devicetable.h>
+#include <linux/module.h>
+#include <linux/property.h>
+#include <linux/regmap.h>
+#include <linux/slab.h>
+#include <linux/spi/spi.h>
+#include <linux/unaligned.h>
+
+#include "edt-ft5x06.h"
+
+#define FTS_SPI_CTRL_WRITE		0x00
+#define FTS_SPI_CTRL_READ		BIT(7)
+#define FTS_SPI_CTRL_CRC		BIT(5)
+#define FTS_SPI_STATUS_ERR		(BIT(7) | BIT(5))
+#define FTS_SPI_HEADER_LEN		4
+#define FTS_SPI_DUMMY_LEN		3
+#define FTS_SPI_CRC_LEN			2
+#define FTS_SPI_RETRIES			3
+#define FTS_SPI_CS_HIGH_US		150
+
+/* Boot ROM commands */
+#define FTS_BOOT_CMD_START		0x55
+#define FTS_BOOT_CMD_READ_ID		0x90
+#define FTS_BOOT_CMD_START_APP		0x08
+#define FTS_BOOT_CMD_SET_ADDR		0xad
+#define FTS_BOOT_CMD_WRITE		0xae
+#define FTS_BOOT_CMD_ECC		0xcc
+#define FTS_BOOT_CMD_ECC_READ		0xcd
+#define FTS_BOOT_CMD_ECC_FINISH		0xce
+#define FTS_BOOT_ECC_FINISH_OK		0xa5
+#define FTS_BOOT_ECC_POLY		0x8408
+
+#define FTS_REG_CHIP_ID			0xa3
+
+/* Firmware image layout */
+#define FTS_FW_INFO_OFFSET		0x100
+#define FTS_FW_MIN_LEN			0x120
+
+#define FTS_BOOT_RETRIES		3
+#define FTS_ECC_RETRIES			100
+
+struct fts_ram_chip {
+	u8 boot_id[2];
+	u8 chip_id;
+	u8 len_unit;
+	u32 pram_max;
+	u32 dram_base;
+	u32 packet_len;
+	unsigned int boot_delay_ms;
+	const char *default_fw;
+};
+
+static int fts_spi_xfer(struct spi_device *spi, const u8 *tx, u8 *rx,
+			size_t len)
+{
+	struct spi_transfer xfer = {
+		.tx_buf = tx,
+		.rx_buf = rx,
+		.len = len,
+	};
+	int ret;
+
+	ret = spi_sync_transfer(spi, &xfer, 1);
+	fsleep(FTS_SPI_CS_HIGH_US);
+
+	return ret;
+}
+
+static int fts_spi_write(struct spi_device *spi, u8 reg, const void *val,
+			 size_t len)
+{
+	size_t xlen = FTS_SPI_HEADER_LEN + (len ? FTS_SPI_DUMMY_LEN + len : 0);
+	u8 *tx, *rx;
+	int ret, i;
+
+	if (len > U16_MAX)
+		return -EINVAL;
+
+	tx = kzalloc(2 * xlen, GFP_KERNEL);
+	if (!tx)
+		return -ENOMEM;
+	rx = tx + xlen;
+
+	tx[0] = reg;
+	tx[1] = FTS_SPI_CTRL_WRITE;
+	put_unaligned_be16(len, &tx[2]);
+	if (len)
+		memcpy(&tx[FTS_SPI_HEADER_LEN + FTS_SPI_DUMMY_LEN], val, len);
+
+	for (i = 0; i < FTS_SPI_RETRIES; i++) {
+		ret = fts_spi_xfer(spi, tx, rx, xlen);
+		if (!ret && !(rx[3] & FTS_SPI_STATUS_ERR))
+			break;
+		if (!ret)
+			ret = -EIO;
+	}
+
+	kfree(tx);
+	return ret;
+}
+
+static int fts_spi_read(struct spi_device *spi, u8 reg, void *val,
+			size_t len)
+{
+	size_t dp = FTS_SPI_HEADER_LEN + FTS_SPI_DUMMY_LEN;
+	size_t xlen = dp + len + FTS_SPI_CRC_LEN;
+	u8 *tx, *rx;
+	int ret, i;
+
+	if (!len || len > U16_MAX)
+		return -EINVAL;
+
+	tx = kzalloc(2 * xlen, GFP_KERNEL);
+	if (!tx)
+		return -ENOMEM;
+	rx = tx + xlen;
+
+	tx[0] = reg;
+	tx[1] = FTS_SPI_CTRL_READ | FTS_SPI_CTRL_CRC;
+	put_unaligned_be16(len, &tx[2]);
+
+	for (i = 0; i < FTS_SPI_RETRIES; i++) {
+		ret = fts_spi_xfer(spi, tx, rx, xlen);
+		if (ret)
+			continue;
+
+		if (rx[3] & FTS_SPI_STATUS_ERR) {
+			ret = -EIO;
+			continue;
+		}
+
+		if (crc_ccitt(0xffff, &rx[dp], len) !=
+		    get_unaligned_le16(&rx[dp + len])) {
+			ret = -EBADMSG;
+			continue;
+		}
+
+		memcpy(val, &rx[dp], len);
+		break;
+	}
+
+	kfree(tx);
+	return ret;
+}
+
+static int fts_spi_regmap_write(void *context, const void *data,
+				size_t count)
+{
+	const u8 *buf = data;
+
+	return fts_spi_write(context, buf[0], buf + 1, count - 1);
+}
+
+static int fts_spi_regmap_read(void *context, const void *reg,
+			       size_t reg_size, void *val, size_t val_size)
+{
+	return fts_spi_read(context, *(const u8 *)reg, val, val_size);
+}
+
+static const struct regmap_bus fts_spi_regmap_bus = {
+	.write = fts_spi_regmap_write,
+	.read = fts_spi_regmap_read,
+};
+
+static const struct regmap_config fts_spi_regmap_config = {
+	.reg_bits = 8,
+	.val_bits = 8,
+};
+
+static int fts_boot_enter(struct spi_device *spi, struct gpio_desc *reset,
+			  const struct fts_ram_chip *chip)
+{
+	u8 id[2] = { };
+	int ret, i;
+
+	for (i = 0; i < FTS_BOOT_RETRIES; i++) {
+		/* The boot ROM only listens shortly after reset */
+		if (i && reset) {
+			gpiod_set_value_cansleep(reset, 1);
+			usleep_range(1000, 2000);
+			gpiod_set_value_cansleep(reset, 0);
+		}
+		fsleep(chip->boot_delay_ms * USEC_PER_MSEC);
+
+		ret = fts_spi_write(spi, FTS_BOOT_CMD_START, NULL, 0);
+		if (ret)
+			continue;
+
+		fsleep(chip->boot_delay_ms * USEC_PER_MSEC);
+
+		ret = fts_spi_read(spi, FTS_BOOT_CMD_READ_ID, id, sizeof(id));
+		if (ret)
+			continue;
+
+		if (!memcmp(id, chip->boot_id, sizeof(id)))
+			return 0;
+
+		ret = -ENODEV;
+	}
+
+	dev_err(&spi->dev, "boot ROM not responding, last id %02x%02x: %d\n",
+		id[0], id[1], ret);
+	return ret;
+}
+
+static int fts_boot_write_ram(struct spi_device *spi,
+			      const struct fts_ram_chip *chip, u32 base,
+			      const u8 *data, u32 len)
+{
+	u8 addr[3];
+	u32 off, n;
+	int ret;
+
+	for (off = 0; off < len; off += n) {
+		n = min(len - off, chip->packet_len);
+
+		put_unaligned_be24(base + off, addr);
+		ret = fts_spi_write(spi, FTS_BOOT_CMD_SET_ADDR, addr,
+				    sizeof(addr));
+		if (ret)
+			return ret;
+
+		ret = fts_spi_write(spi, FTS_BOOT_CMD_WRITE, data + off, n);
+		if (ret)
+			return ret;
+	}
+
+	return 0;
+}
+
+static u16 fts_boot_ecc(const u8 *data, u32 len)
+{
+	u16 ecc = 0;
+	u32 i;
+	int j;
+
+	for (i = 0; i + 1 < len; i += 2) {
+		ecc ^= get_unaligned_be16(data + i);
+		for (j = 0; j < 16; j++)
+			ecc = (ecc & 1) ? (ecc >> 1) ^ FTS_BOOT_ECC_POLY :
+					  ecc >> 1;
+	}
+
+	return ecc;
+}
+
+static int fts_boot_check_ecc(struct spi_device *spi, const u8 *data,
+			      u32 len)
+{
+	u8 cmd[6], val[2];
+	int ret, i;
+
+	put_unaligned_be24(0, &cmd[0]);
+	put_unaligned_be24(len, &cmd[3]);
+	ret = fts_spi_write(spi, FTS_BOOT_CMD_ECC, cmd, sizeof(cmd));
+	if (ret)
+		return ret;
+
+	usleep_range(2000, 3000);
+
+	for (i = 0; i < FTS_ECC_RETRIES; i++) {
+		ret = fts_spi_read(spi, FTS_BOOT_CMD_ECC_FINISH, val, 1);
+		if (ret)
+			return ret;
+		if (val[0] == FTS_BOOT_ECC_FINISH_OK)
+			break;
+		usleep_range(1000, 2000);
+	}
+	if (i == FTS_ECC_RETRIES)
+		return -ETIMEDOUT;
+
+	ret = fts_spi_read(spi, FTS_BOOT_CMD_ECC_READ, val, sizeof(val));
+	if (ret)
+		return ret;
+
+	if (get_unaligned_be16(val) != fts_boot_ecc(data, len)) {
+		dev_err(&spi->dev, "ECC mismatch: %04x != %04x\n",
+			get_unaligned_be16(val), fts_boot_ecc(data, len));
+		return -EIO;
+	}
+
+	return 0;
+}
+
+static u32 fts_fw_len(const struct firmware *fw, unsigned int off,
+		      const struct fts_ram_chip *chip)
+{
+	const u8 *info = fw->data + FTS_FW_INFO_OFFSET + off;
+	u16 len = get_unaligned_be16(info);
+
+	if ((u16)(len + get_unaligned_be16(info + 2)) != 0xffff)
+		return 0;
+
+	return len * chip->len_unit;
+}
+
+static int fts_ram_boot(struct device *dev, struct gpio_desc *reset,
+			const struct fts_ram_chip *chip)
+{
+	struct spi_device *spi = to_spi_device(dev);
+	const struct firmware *fw;
+	const char *fw_name = chip->default_fw;
+	u32 pram_len, dram_len;
+	u8 id;
+	int ret, i;
+
+	device_property_read_string(dev, "firmware-name", &fw_name);
+
+	ret = request_firmware(&fw, fw_name, dev);
+	if (ret)
+		return ret;
+
+	ret = -EINVAL;
+	if (fw->size < FTS_FW_MIN_LEN) {
+		dev_err(dev, "firmware %s too short\n", fw_name);
+		goto out;
+	}
+
+	pram_len = fts_fw_len(fw, 0, chip);
+	dram_len = fts_fw_len(fw, 8, chip);
+	if (!pram_len || pram_len > chip->pram_max ||
+	    pram_len + dram_len > fw->size) {
+		dev_err(dev, "invalid firmware %s\n", fw_name);
+		goto out;
+	}
+
+	ret = fts_boot_enter(spi, reset, chip);
+	if (ret)
+		goto out;
+
+	ret = fts_boot_write_ram(spi, chip, 0, fw->data, pram_len);
+	if (!ret)
+		ret = fts_boot_check_ecc(spi, fw->data, pram_len);
+	if (ret) {
+		dev_err(dev, "failed to load program RAM: %d\n", ret);
+		goto out;
+	}
+
+	if (dram_len) {
+		ret = fts_boot_write_ram(spi, chip, chip->dram_base,
+					 fw->data + pram_len, dram_len);
+		if (!ret)
+			ret = fts_boot_check_ecc(spi, fw->data + pram_len,
+						 dram_len);
+		if (ret) {
+			dev_err(dev, "failed to load data RAM: %d\n", ret);
+			goto out;
+		}
+	}
+
+	ret = fts_spi_write(spi, FTS_BOOT_CMD_START_APP, NULL, 0);
+	if (ret)
+		goto out;
+
+	/* Wait for the application to report the chip ID */
+	for (i = 0; i < 20; i++) {
+		fsleep(10 * USEC_PER_MSEC);
+		if (!fts_spi_read(spi, FTS_REG_CHIP_ID, &id, 1) &&
+		    id == chip->chip_id)
+			break;
+	}
+	if (i == 20) {
+		dev_err(dev, "firmware did not start\n");
+		ret = -EIO;
+		goto out;
+	}
+
+	dev_dbg(dev, "loaded %s (%u + %u bytes)\n", fw_name, pram_len,
+		dram_len);
+out:
+	release_firmware(fw);
+	return ret;
+}
+
+static const struct fts_ram_chip fts_ft8722 = {
+	.boot_id = { 0x87, 0x22 },
+	.chip_id = 0x87,
+	.len_unit = 2,
+	.pram_max = 88 * 1024,
+	.dram_base = 0xd00000,
+	.packet_len = 32 * 1024 - 16,
+	.boot_delay_ms = 8,
+	.default_fw = "focaltech/ft8722.bin",
+};
+
+static int ft8722_boot(struct device *dev, struct gpio_desc *reset)
+{
+	return fts_ram_boot(dev, reset, &fts_ft8722);
+}
+
+static const struct edt_ft5x06_chip_data edt_ft8722_data = {
+	.max_support_points = 10,
+	.tdata_reg = 0x01,
+	.boot = ft8722_boot,
+};
+
+static int edt_ft5x06_spi_probe(struct spi_device *spi)
+{
+	const struct edt_ft5x06_chip_data *chip_data;
+	struct regmap *regmap;
+	int ret;
+
+	chip_data = spi_get_device_match_data(spi);
+	if (!chip_data)
+		return -ENODEV;
+
+	if (!spi->bits_per_word)
+		spi->bits_per_word = 8;
+	ret = spi_setup(spi);
+	if (ret)
+		return ret;
+
+	regmap = regmap_init(&spi->dev, &fts_spi_regmap_bus, spi,
+			     &fts_spi_regmap_config);
+	if (IS_ERR(regmap))
+		return dev_err_probe(&spi->dev, PTR_ERR(regmap),
+				     "failed to init regmap\n");
+
+	return edt_ft5x06_probe(&spi->dev, regmap, spi->irq, chip_data,
+				BUS_SPI, NULL);
+}
+
+static void edt_ft5x06_spi_remove(struct spi_device *spi)
+{
+	edt_ft5x06_remove(&spi->dev);
+}
+
+static const struct of_device_id edt_ft5x06_spi_of_match[] = {
+	{ .compatible = "focaltech,ft8722-ts", .data = &edt_ft8722_data },
+	{ }
+};
+MODULE_DEVICE_TABLE(of, edt_ft5x06_spi_of_match);
+
+static const struct spi_device_id edt_ft5x06_spi_ids[] = {
+	{ "ft8722-ts", (kernel_ulong_t)&edt_ft8722_data },
+	{ }
+};
+MODULE_DEVICE_TABLE(spi, edt_ft5x06_spi_ids);
+
+static struct spi_driver edt_ft5x06_spi_driver = {
+	.driver = {
+		.name = "edt_ft5x06_spi",
+		.dev_groups = edt_ft5x06_groups,
+		.of_match_table = edt_ft5x06_spi_of_match,
+		.pm = pm_sleep_ptr(&edt_ft5x06_pm_ops),
+		.probe_type = PROBE_PREFER_ASYNCHRONOUS,
+	},
+	.id_table = edt_ft5x06_spi_ids,
+	.probe = edt_ft5x06_spi_probe,
+	.remove = edt_ft5x06_spi_remove,
+};
+module_spi_driver(edt_ft5x06_spi_driver);
+
+MODULE_IMPORT_NS("EDT_FT5X06");
+MODULE_AUTHOR("Luka Panio <lukapanio@gmail.com>");
+MODULE_DESCRIPTION("EDT FT5x06 SPI Touchscreen Driver");
+MODULE_LICENSE("GPL");
+MODULE_FIRMWARE("focaltech/ft8722.bin");
-- 
2.55.0


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

* [PATCH 7/7] Input: edt-ft5x06 - support following a DRM panel
  2026-10-17 14:07 [PATCH 0/7] Input: edt-ft5x06 - add SPI transport and FocalTech FT8722 support Luka Panio
                   ` (5 preceding siblings ...)
  2026-10-17 14:07 ` [PATCH 6/7] Input: edt-ft5x06 - add SPI transport and FT8722 support Luka Panio
@ 2026-10-17 14:07 ` Luka Panio
  2026-10-17 14:30   ` sashiko-bot
  6 siblings, 1 reply; 11+ messages in thread
From: Luka Panio @ 2026-10-17 14:07 UTC (permalink / raw)
  To: Dmitry Torokhov
  Cc: Rob Herring, Krzysztof Kozlowski, Conor Dooley, Henrik Rydberg,
	Douglas Anderson, linux-input, devicetree, dri-devel,
	linux-kernel, Luka Panio

On touch and display driver (TDDI) chips such as the FT8722 the touch
controller shares its supply and reset with the display panel, so it
can only be accessed while the panel is powered and loses its state,
and for RAM-only parts its firmware, every time the panel goes off.

When the device tree links the touchscreen to its panel, register as a
DRM panel follower, like i2c-hid does.

Signed-off-by: Luka Panio <lukapanio@gmail.com>
---
 drivers/input/touchscreen/Kconfig      |   1 +
 drivers/input/touchscreen/edt-ft5x06.c | 156 ++++++++++++++++++++++++-
 2 files changed, 151 insertions(+), 6 deletions(-)

diff --git a/drivers/input/touchscreen/Kconfig b/drivers/input/touchscreen/Kconfig
index 92840ea9c65f..b4fd55028a04 100644
--- a/drivers/input/touchscreen/Kconfig
+++ b/drivers/input/touchscreen/Kconfig
@@ -794,6 +794,7 @@ config TOUCHSCREEN_PENMOUNT
 config TOUCHSCREEN_EDT_FT5X06
 	tristate "EDT FocalTech FT5x06 I2C Touchscreen support"
 	depends on I2C
+	depends on DRM_PANEL || !DRM_PANEL
 	select REGMAP_I2C
 	help
 	  Say Y here if you have an EDT "Polytouch" touchscreen based
diff --git a/drivers/input/touchscreen/edt-ft5x06.c b/drivers/input/touchscreen/edt-ft5x06.c
index 7bcc48e07d62..b2fca6c37205 100644
--- a/drivers/input/touchscreen/edt-ft5x06.c
+++ b/drivers/input/touchscreen/edt-ft5x06.c
@@ -4,6 +4,7 @@
  * Daniel Wagener <daniel.wagener@kernelconcepts.de> (M09 firmware support)
  * Lothar Waßmann <LW@KARO-electronics.de> (DT support)
  * Dario Binacchi <dario.binacchi@amarulasolutions.com> (regmap support)
+ * Luka Panio <lukapanio@gmail.com> (SPI and DRM panel support)
  */
 
 /*
@@ -33,6 +34,9 @@
 #include <linux/uaccess.h>
 
 #include <linux/unaligned.h>
+#include <linux/workqueue.h>
+
+#include <drm/drm_panel.h>
 
 #include "edt-ft5x06.h"
 
@@ -150,6 +154,12 @@ struct edt_ft5x06_ts_data {
 	enum edt_ver version;
 	int (*boot)(struct device *dev, struct gpio_desc *reset_gpio);
 	u8 tdata_reg;
+
+	struct drm_panel_follower panel_follower;
+	struct work_struct panel_work;
+	bool is_panel_follower;
+	bool powered_off;
+	bool hw_initialized;
 	unsigned int crc_errors;
 	unsigned int header_errors;
 };
@@ -383,7 +393,7 @@ static ssize_t edt_ft5x06_setting_show(struct device *dev,
 
 	guard(mutex)(&tsdata->mutex);
 
-	if (tsdata->factory_mode)
+	if (tsdata->factory_mode || tsdata->powered_off)
 		return -EIO;
 
 	switch (tsdata->version) {
@@ -441,7 +451,7 @@ static ssize_t edt_ft5x06_setting_store(struct device *dev,
 
 	guard(mutex)(&tsdata->mutex);
 
-	if (tsdata->factory_mode)
+	if (tsdata->factory_mode || tsdata->powered_off)
 		return -EIO;
 
 	error = kstrtouint(buf, 0, &val);
@@ -1120,7 +1130,8 @@ static void edt_ft5x06_disable_regulators(void *arg)
 
 /*
  * Take the controller out of reset, identify it and register the input
- * device.
+ * device. Called from probe, or once the panel is up for controllers
+ * following a panel.
  */
 static int edt_ft5x06_ts_init_hw(struct edt_ft5x06_ts_data *tsdata)
 {
@@ -1206,9 +1217,117 @@ static int edt_ft5x06_ts_init_hw(struct edt_ft5x06_ts_data *tsdata)
 
 	edt_ft5x06_ts_prepare_debugfs(tsdata);
 
+	tsdata->hw_initialized = true;
+
+	return 0;
+}
+
+/*
+ * TDDI controllers share their power and reset with the display panel. When
+ * following a panel, power the touch controller down before the panel goes
+ * off and bring it back once the panel is up again.
+ */
+static void edt_ft5x06_panel_work(struct work_struct *work)
+{
+	struct edt_ft5x06_ts_data *tsdata =
+		container_of(work, struct edt_ft5x06_ts_data, panel_work);
+	int error;
+
+	guard(mutex)(&tsdata->mutex);
+
+	if (!tsdata->powered_off)
+		return;
+
+	if (!tsdata->hw_initialized) {
+		error = edt_ft5x06_ts_init_hw(tsdata);
+		if (error) {
+			/* try again the next time the panel comes up */
+			gpiod_set_value_cansleep(tsdata->reset_gpio, 1);
+			dev_err(tsdata->dev,
+				"Failed to initialize the controller: %d\n",
+				error);
+			return;
+		}
+		goto out;
+	}
+
+	gpiod_set_value_cansleep(tsdata->reset_gpio, 0);
+
+	if (tsdata->boot) {
+		error = tsdata->boot(tsdata->dev, tsdata->reset_gpio);
+		if (error) {
+			dev_err(tsdata->dev,
+				"Failed to boot the controller: %d\n", error);
+			return;
+		}
+	} else {
+		msleep(300);
+	}
+
+	edt_ft5x06_restore_reg_parameters(tsdata);
+out:
+	tsdata->powered_off = false;
+	enable_irq(tsdata->irq);
+}
+
+static int edt_ft5x06_panel_prepared(struct drm_panel_follower *follower)
+{
+	struct edt_ft5x06_ts_data *tsdata =
+		container_of(follower, struct edt_ft5x06_ts_data, panel_follower);
+
+	/* Downloading firmware takes a while, don't hold up the panel */
+	schedule_work(&tsdata->panel_work);
+
 	return 0;
 }
 
+static int edt_ft5x06_panel_unpreparing(struct drm_panel_follower *follower)
+{
+	struct edt_ft5x06_ts_data *tsdata =
+		container_of(follower, struct edt_ft5x06_ts_data, panel_follower);
+
+	cancel_work_sync(&tsdata->panel_work);
+
+	guard(mutex)(&tsdata->mutex);
+
+	if (tsdata->powered_off)
+		return 0;
+
+	disable_irq(tsdata->irq);
+	gpiod_set_value_cansleep(tsdata->reset_gpio, 1);
+	tsdata->powered_off = true;
+
+	return 0;
+}
+
+static const struct drm_panel_follower_funcs edt_ft5x06_panel_follower_funcs = {
+	.panel_prepared = edt_ft5x06_panel_prepared,
+	.panel_unpreparing = edt_ft5x06_panel_unpreparing,
+};
+
+static void edt_ft5x06_cancel_panel_work(void *data)
+{
+	struct edt_ft5x06_ts_data *tsdata = data;
+
+	cancel_work_sync(&tsdata->panel_work);
+}
+
+static int edt_ft5x06_register_panel_follower(struct edt_ft5x06_ts_data *tsdata)
+{
+	struct device *dev = tsdata->dev;
+	int error;
+
+	INIT_WORK(&tsdata->panel_work, edt_ft5x06_panel_work);
+	error = devm_add_action_or_reset(dev, edt_ft5x06_cancel_panel_work,
+					 tsdata);
+	if (error)
+		return error;
+
+	tsdata->panel_follower.funcs = &edt_ft5x06_panel_follower_funcs;
+
+	return devm_drm_panel_add_follower(dev, &tsdata->panel_follower);
+}
+
 int edt_ft5x06_probe(struct device *dev, struct regmap *regmap, int irq,
 		     const struct edt_ft5x06_chip_data *chip_data, u16 bustype,
 		     struct dentry *debugfs)
@@ -1331,14 +1450,24 @@ int edt_ft5x06_probe(struct device *dev, struct regmap *regmap, int irq,
 	tsdata->factory_mode = false;
 	dev_set_drvdata(dev, tsdata);
 
-	error = edt_ft5x06_ts_init_hw(tsdata);
-	if (error)
-		return error;
+	/*
+	 * A controller sharing its power with a panel can only be accessed
+	 * once the panel is up, so leave it in reset and do the rest of the
+	 * initialization from the panel follower callback.
+	 */
+	tsdata->is_panel_follower = drm_is_panel_follower(dev);
+	if (!tsdata->is_panel_follower) {
+		error = edt_ft5x06_ts_init_hw(tsdata);
+		if (error)
+			return error;
+	}
 
 	irq_flags = irq_get_trigger_type(irq);
 	if (irq_flags == IRQF_TRIGGER_NONE)
 		irq_flags = IRQF_TRIGGER_FALLING;
 	irq_flags |= IRQF_ONESHOT;
+	if (tsdata->is_panel_follower)
+		irq_flags |= IRQF_NO_AUTOEN;
 
 	error = devm_request_threaded_irq(dev, irq, NULL, edt_ft5x06_ts_isr,
 					  irq_flags, dev_name(dev), tsdata);
@@ -1347,6 +1476,14 @@ int edt_ft5x06_probe(struct device *dev, struct regmap *regmap, int irq,
 		return error;
 	}
 
+	if (tsdata->is_panel_follower) {
+		tsdata->powered_off = true;
+		error = edt_ft5x06_register_panel_follower(tsdata);
+		if (error)
+			return dev_err_probe(dev, error,
+					     "failed to follow the panel\n");
+	}
+
 	dev_dbg(dev,
 		"EDT FT5x06 initialized: IRQ %d, WAKE pin %d, Reset pin %d.\n",
 		irq,
@@ -1374,6 +1511,10 @@ static int edt_ft5x06_ts_suspend(struct device *dev)
 	if (device_may_wakeup(dev))
 		return 0;
 
+	/* The panel powers us down and up */
+	if (tsdata->is_panel_follower)
+		return 0;
+
 	if (tsdata->suspend_mode == EDT_PMODE_NOT_SUPPORTED)
 		return 0;
 
@@ -1415,6 +1556,9 @@ static int edt_ft5x06_ts_resume(struct device *dev)
 	if (device_may_wakeup(dev))
 		return 0;
 
+	if (tsdata->is_panel_follower)
+		return 0;
+
 	if (tsdata->suspend_mode == EDT_PMODE_NOT_SUPPORTED)
 		return 0;
 
-- 
2.55.0


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

* Re: [PATCH 2/7] Input: edt-ft5x06 - move the controller bring-up into a helper
  2026-10-17 14:07 ` [PATCH 2/7] Input: edt-ft5x06 - move the controller bring-up into a helper Luka Panio
@ 2026-10-17 14:16   ` sashiko-bot
  0 siblings, 0 replies; 11+ messages in thread
From: sashiko-bot @ 2026-10-17 14:16 UTC (permalink / raw)
  To: Luka Panio
  Cc: linux-input, devicetree, dri-devel, robh, conor+dt,
	dmitry.torokhov

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] Input: edt-ft5x06: edt_ft5x06_ts_init_hw() registers device before IRQ
--

commit 5aab9a2f7e5d9f696c6cdfb8f9baf87581af8689
Author: Luka Panio <lukapanio@gmail.com>

Input: edt-ft5x06 - move the controller bring-up into a helper

This commit moves the controller reset release, hardware identification,
input device registration, and debugfs setup out of edt_ft5x06_probe()
into a new edt_ft5x06_ts_init_hw() helper function.

> diff --git a/drivers/input/touchscreen/edt-ft5x06.c b/drivers/input/touchscreen/edt-ft5x06.c
> index 543cc62088c7a..536d633d97a78 100644
> --- a/drivers/input/touchscreen/edt-ft5x06.c
> +++ b/drivers/input/touchscreen/edt-ft5x06.c
> @@ -1116,16 +1116,96 @@ static void edt_ft5x06_disable_regulators(void *arg)
>  	regulator_disable(data->iovcc);
>  }
>  
> +/*
> + * Take the controller out of reset, identify it and register the input
> + * device.
> + */
> +static int edt_ft5x06_ts_init_hw(struct edt_ft5x06_ts_data *tsdata)
> +{

[ ... ]

> +	error = input_register_device(input);
> +	if (error)
> +		return error;
> +
> +	edt_ft5x06_ts_prepare_debugfs(tsdata);
> +
> +	return 0;
> +}
> +
>  int edt_ft5x06_probe(struct device *dev, struct regmap *regmap, int irq,
>  		     const struct edt_ft5x06_chip_data *chip_data, u16 bustype,
>  		     struct dentry *debugfs)
>  {

[ ... ]

> @@ -1225,78 +1305,22 @@ int edt_ft5x06_probe(struct device *dev, struct regmap *regmap, int irq,

[ ... ]

> +	error = edt_ft5x06_ts_init_hw(tsdata);
> +	if (error)
>  		return error;
> -	}
>  
>  	irq_flags = irq_get_trigger_type(irq);
>  	if (irq_flags == IRQF_TRIGGER_NONE)
> @@ -1310,12 +1334,6 @@ int edt_ft5x06_probe(struct device *dev, struct regmap *regmap, int irq,
>  		return error;
>  	}
>  
> -	error = input_register_device(input);
> -	if (error)
> -		return error;
> -
> -	edt_ft5x06_ts_prepare_debugfs(tsdata);
> -
>  	dev_dbg(dev,
>  		"EDT FT5x06 initialized: IRQ %d, WAKE pin %d, Reset pin %d.\n",
>  		irq,

[Severity: Medium]
Previously, edt_ft5x06_probe() called devm_request_threaded_irq() before
calling input_register_device() and edt_ft5x06_ts_prepare_debugfs().

With edt_ft5x06_ts_init_hw() now running before devm_request_threaded_irq(),
the input device and debugfs files are published to userspace before the
touchscreen IRQ is requested.

If devm_request_threaded_irq() fails, the input device is announced to
userspace only to be immediately torn down during devres release.

Additionally, because edt_ft5x06_ts_prepare_debugfs() exposes the debugfs
mode file early, writing 1 to it before or during IRQ setup triggers:

edt_ft5x06_debugfs_mode_set()
  -> edt_ft5x06_factory_mode()
       disable_irq(tsdata->irq);
       if (!tsdata->raw_buffer) {
           tsdata->raw_bufsize = tsdata->num_x * tsdata->num_y *
                                 sizeof(u16);
           tsdata->raw_buffer = kzalloc(tsdata->raw_bufsize, GFP_KERNEL);
           ...
       }

Can this cause disable_irq() (or enable_irq() on error or when switching
back in edt_ft5x06_work_mode()) to be called before the IRQ is requested, or
cause irq_startup() inside devm_request_threaded_irq() to re-enable the
interrupt while tsdata->factory_mode is true?

Also, if devm_request_threaded_irq() fails after tsdata->raw_buffer is
allocated, edt_ft5x06_remove() -> edt_ft5x06_ts_teardown_debugfs() is not
run on probe failure. Does this leak tsdata->raw_buffer?

Should devm_request_threaded_irq() be called before edt_ft5x06_ts_init_hw()
(for example, with IRQF_NO_AUTOEN if needed until the hardware is ready), or
should input_register_device() and edt_ft5x06_ts_prepare_debugfs() happen
after the IRQ is requested?

oggler

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

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

* Re: [PATCH 6/7] Input: edt-ft5x06 - add SPI transport and FT8722 support
  2026-10-17 14:07 ` [PATCH 6/7] Input: edt-ft5x06 - add SPI transport and FT8722 support Luka Panio
@ 2026-10-17 14:29   ` sashiko-bot
  0 siblings, 0 replies; 11+ messages in thread
From: sashiko-bot @ 2026-10-17 14:29 UTC (permalink / raw)
  To: Luka Panio
  Cc: linux-input, devicetree, dri-devel, robh, conor+dt,
	dmitry.torokhov

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] Input: edt-ft5x06 - unaligned DMA rx buffer sharing cacheline with tx
--

commit 6c4cc2125db365d716cb624219a2c7fd67fb674b
Author: Luka Panio <lukapanio@gmail.com>

Input: edt-ft5x06 - add SPI transport and FT8722 support

This commit adds an SPI transport driver for FocalTech touchscreen
controllers handled by the edt-ft5x06 driver, along with firmware
download support for the RAM-only FT8722 controller.

> diff --git a/drivers/input/touchscreen/edt-ft5x06-spi.c b/drivers/input/touchscreen/edt-ft5x06-spi.c
> new file mode 100644
> index 0000000000000..d0ae0d4b621dd
> --- /dev/null
> +++ b/drivers/input/touchscreen/edt-ft5x06-spi.c
> @@ -0,0 +1,475 @@

[ ... ]

> +static int fts_spi_xfer(struct spi_device *spi, const u8 *tx, u8 *rx,
> +			size_t len)
> +{
> +	struct spi_transfer xfer = {
> +		.tx_buf = tx,
> +		.rx_buf = rx,
> +		.len = len,
> +	};
> +	int ret;
> +
> +	ret = spi_sync_transfer(spi, &xfer, 1);
> +	fsleep(FTS_SPI_CS_HIGH_US);
> +
> +	return ret;
> +}
> +
> +static int fts_spi_write(struct spi_device *spi, u8 reg, const void *val,
> +			 size_t len)
> +{
> +	size_t xlen = FTS_SPI_HEADER_LEN + (len ? FTS_SPI_DUMMY_LEN + len : 0);
> +	u8 *tx, *rx;
> +	int ret, i;
> +
> +	if (len > U16_MAX)
> +		return -EINVAL;
> +
> +	tx = kzalloc(2 * xlen, GFP_KERNEL);
> +	if (!tx)
> +		return -ENOMEM;
> +	rx = tx + xlen;
> +
> +	tx[0] = reg;
> +	tx[1] = FTS_SPI_CTRL_WRITE;
> +	put_unaligned_be16(len, &tx[2]);
> +	if (len)
> +		memcpy(&tx[FTS_SPI_HEADER_LEN + FTS_SPI_DUMMY_LEN], val, len);
> +
> +	for (i = 0; i < FTS_SPI_RETRIES; i++) {
> +		ret = fts_spi_xfer(spi, tx, rx, xlen);
> +		if (!ret && !(rx[3] & FTS_SPI_STATUS_ERR))
> +			break;
> +		if (!ret)
> +			ret = -EIO;
> +	}
> +
> +	kfree(tx);
> +	return ret;
> +}
> +
> +static int fts_spi_read(struct spi_device *spi, u8 reg, void *val,
> +			size_t len)
> +{
> +	size_t dp = FTS_SPI_HEADER_LEN + FTS_SPI_DUMMY_LEN;
> +	size_t xlen = dp + len + FTS_SPI_CRC_LEN;
> +	u8 *tx, *rx;
> +	int ret, i;
> +
> +	if (!len || len > U16_MAX)
> +		return -EINVAL;
> +
> +	tx = kzalloc(2 * xlen, GFP_KERNEL);
> +	if (!tx)
> +		return -ENOMEM;
> +	rx = tx + xlen;

[Severity: High]
Does placing rx at tx + xlen in fts_spi_write() and fts_spi_read() cause
tx and rx to share a cacheline during full-duplex SPI DMA transfers?

In both functions, a single buffer of size 2 * xlen is allocated, with tx
at offset 0 and rx = tx + xlen. Both buffers are then passed to
fts_spi_xfer() -> spi_sync_transfer() in a single full-duplex
spi_transfer, where the SPI core maps tx as DMA_TO_DEVICE and rx as
DMA_FROM_DEVICE concurrently.

Because xlen is typically not aligned to ARCH_DMA_MINALIGN (for example,
xlen is 4 or 8 for control writes, 10 or 11 for short reads, 71 for
touch report reads in edt_ft5x06_ts_isr(), and 32759 for firmware RAM
writes in fts_boot_write_ram()), rx starts in the middle of the cacheline
where tx ends.

On systems with DMA-capable SPI controllers, this overlapping mapping in
opposite DMA directions triggers DMA-API debug EEXIST warnings and risks
cacheline corruption between tx and rx during cache maintenance on
non-coherent architectures (particularly when xlen is large enough to
bypass swiotlb bouncing under CONFIG_DMA_BOUNCE_UNALIGNED_KMALLOC, or
across retries in the FTS_SPI_RETRIES loop), as well as presenting an
unaligned rx_buf pointer to SPI controllers that require aligned buffers.

Could tx and rx be allocated separately, or could the offset of rx be
aligned with ALIGN(xlen, ARCH_DMA_MINALIGN)?

[ ... ]

>

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

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

* Re: [PATCH 7/7] Input: edt-ft5x06 - support following a DRM panel
  2026-10-17 14:07 ` [PATCH 7/7] Input: edt-ft5x06 - support following a DRM panel Luka Panio
@ 2026-10-17 14:30   ` sashiko-bot
  0 siblings, 0 replies; 11+ messages in thread
From: sashiko-bot @ 2026-10-17 14:30 UTC (permalink / raw)
  To: Luka Panio
  Cc: linux-input, devicetree, dri-devel, robh, conor+dt,
	dmitry.torokhov

Thank you for your contribution! Sashiko AI review found 5 potential issue(s) to consider:
- [High] Input: edt-ft5x06 - unbind race between panel_work and remove/devres teardown
- [High] Input: edt-ft5x06 - non-idempotent edt_ft5x06_ts_init_hw() retried in panel_work
- [Medium] Input: edt-ft5x06 - reset_gpio left deasserted on boot failure in panel_work
- [Medium] Input: edt-ft5x06 - missing powered_off check and factory_mode restore for panel follower
- [Medium] Input: edt-ft5x06 - unlocked sysfs reads in model_show() and fw_version_show()
--

commit 8d0f54b6a0e4f2aafd88896b2e7280382657e313
Author: Luka Panio <lukapanio@gmail.com>

Input: edt-ft5x06 - support following a DRM panel

This commit adds DRM panel follower support to the edt-ft5x06 touchscreen
driver for TDDI controllers that share power and reset lines with a display
panel. When following a panel, initial hardware probing and subsequent power
transitions are deferred to a workqueue triggered by the panel follower
callbacks.

> diff --git a/drivers/input/touchscreen/edt-ft5x06.c b/drivers/input/touchscreen/edt-ft5x06.c
> index 7bcc48e07d629..b2fca6c372052 100644
> --- a/drivers/input/touchscreen/edt-ft5x06.c
> +++ b/drivers/input/touchscreen/edt-ft5x06.c

[ ... ]

> @@ -1206,9 +1217,117 @@ static int edt_ft5x06_ts_init_hw(struct edt_ft5x06_ts_data *tsdata)
>  
>  	edt_ft5x06_ts_prepare_debugfs(tsdata);
>  
> +	tsdata->hw_initialized = true;
> +
> +	return 0;
> +}
> +
> +/*
> + * TDDI controllers share their power and reset with the display panel. When
> + * following a panel, power the touch controller down before the panel goes
> + * off and bring it back once the panel is up again.
> + */
> +static void edt_ft5x06_panel_work(struct work_struct *work)
> +{
> +	struct edt_ft5x06_ts_data *tsdata =
> +		container_of(work, struct edt_ft5x06_ts_data, panel_work);
> +	int error;
> +
> +	guard(mutex)(&tsdata->mutex);
> +
> +	if (!tsdata->powered_off)
> +		return;
> +
> +	if (!tsdata->hw_initialized) {
> +		error = edt_ft5x06_ts_init_hw(tsdata);
> +		if (error) {
> +			/* try again the next time the panel comes up */
> +			gpiod_set_value_cansleep(tsdata->reset_gpio, 1);
> +			dev_err(tsdata->dev,
> +				"Failed to initialize the controller: %d\n",
> +				error);
> +			return;
> +		}
> +		goto out;
> +	}

[Severity: High]
Is it safe to retry edt_ft5x06_ts_init_hw() on a subsequent panel prepare
cycle if the first attempt fails?

edt_ft5x06_ts_init_hw() performs several one-shot actions that do not roll
back on error:

1. In edt_ft5x06_ts_init_hw() -> edt_ft5x06_ts_identify(), if an EDT_M06
   chip is detected, it destroys tsdata->regmap and replaces it:

	regmap_exit(tsdata->regmap);
	tsdata->regmap = regmap_init_i2c(client,
					 &edt_M06_i2c_regmap_config);
	if (IS_ERR(tsdata->regmap)) {
		dev_err(tsdata->dev, "regmap allocation failed\n");
		return PTR_ERR(tsdata->regmap);
	}

   If regmap_init_i2c() returns an error pointer, the next retry will pass
   that error pointer into regmap_bulk_read() and dereference it. Even if
   regmap_init_i2c() succeeds and a later step in edt_ft5x06_ts_init_hw()
   fails, the next retry will call edt_ft5x06_ts_identify() using
   edt_M06_i2c_regmap_config, which masks the 0xBB identification register
   to 0x3B and only reads a single byte, breaking M06 identification.

2. edt_ft5x06_ts_init_hw() calls input_register_device(tsdata->input) on the
   input_dev allocated once in edt_ft5x06_probe(). If input_register_device()
   fails, struct input_dev cannot be passed to input_register_device() a
   second time on the next retry.

3. In edt_ft5x06_ts_init_hw(), if tsdata->boot() fails, it calls
   dev_err_probe() from the background panel_work workqueue after
   edt_ft5x06_probe() has already returned.

> +
> +	gpiod_set_value_cansleep(tsdata->reset_gpio, 0);
> +
> +	if (tsdata->boot) {
> +		error = tsdata->boot(tsdata->dev, tsdata->reset_gpio);
> +		if (error) {
> +			dev_err(tsdata->dev,
> +				"Failed to boot the controller: %d\n", error);
> +			return;
> +		}
> +	} else {
> +		msleep(300);
> +	}

[Severity: Medium]
Should reset_gpio be re-asserted to 1 if tsdata->boot() fails here, just as
is done in the !tsdata->hw_initialized error path above?

Because tsdata->powered_off remains true on this early return, a subsequent
call to edt_ft5x06_panel_unpreparing() will see tsdata->powered_off == true
and return early without asserting reset_gpio to 1:

	if (tsdata->powered_off)
		return 0;

	disable_irq(tsdata->irq);
	gpiod_set_value_cansleep(tsdata->reset_gpio, 1);
	tsdata->powered_off = true;

That leaves reset_gpio deasserted (0) while the shared TDDI panel power goes
off, and also prevents a 1 -> 0 reset transition on the next panel wake-up.

> +
> +	edt_ft5x06_restore_reg_parameters(tsdata);
> +out:
> +	tsdata->powered_off = false;
> +	enable_irq(tsdata->irq);
> +}

[Severity: Medium]
What happens if tsdata->factory_mode is active across a panel power cycle, or
if the debugfs files are accessed while tsdata->powered_off is true?

In edt_ft5x06_debugfs_mode_set() and edt_ft5x06_debugfs_raw_data_read(),
tsdata->mutex is acquired, but unlike edt_ft5x06_setting_show() and
edt_ft5x06_setting_store(), neither function checks tsdata->powered_off
before accessing the controller over regmap or toggling IRQ state.

In addition, if factory mode was enabled before the panel turned off
(calling disable_irq(tsdata->irq) once and setting tsdata->factory_mode =
true), edt_ft5x06_panel_unpreparing() will call disable_irq(tsdata->irq) a
second time and assert reset_gpio. When edt_ft5x06_panel_work() runs on
resume, the controller comes out of reset in work mode while
tsdata->factory_mode is still true, causing edt_ft5x06_restore_reg_parameters()
on M06 to format writes as factory-mode requests, and leaving tsdata->irq
disabled after the single enable_irq(tsdata->irq) call because
edt_ft5x06_factory_mode() is not called as in edt_ft5x06_ts_resume():

	edt_ft5x06_restore_reg_parameters(tsdata);
	enable_irq(tsdata->irq);

	if (tsdata->factory_mode)
		ret = edt_ft5x06_factory_mode(tsdata);

[ ... ]

> +static void edt_ft5x06_cancel_panel_work(void *data)
> +{
> +	struct edt_ft5x06_ts_data *tsdata = data;
> +
> +	cancel_work_sync(&tsdata->panel_work);
> +}
> +
> +static int edt_ft5x06_register_panel_follower(struct edt_ft5x06_ts_data *tsdata)
> +{
> +	struct device *dev = tsdata->dev;
> +	int error;
> +
> +	INIT_WORK(&tsdata->panel_work, edt_ft5x06_panel_work);
> +	error = devm_add_action_or_reset(dev, edt_ft5x06_cancel_panel_work,
> +					 tsdata);
> +	if (error)
> +		return error;
> +
> +	tsdata->panel_follower.funcs = &edt_ft5x06_panel_follower_funcs;
> +
> +	return devm_drm_panel_add_follower(dev, &tsdata->panel_follower);
> +}

[Severity: High]
Can deferring panel follower removal and cancel_work_sync(&tsdata->panel_work)
to devres actions race with edt_ft5x06_remove() and devres teardown when the
driver is unbound while edt_ft5x06_panel_work() is running?

When unbinding the driver:

1. edt_ft5x06_remove() runs before devres release:

	void edt_ft5x06_remove(struct device *dev)
	{
		struct edt_ft5x06_ts_data *tsdata = dev_get_drvdata(dev);

		edt_ft5x06_ts_teardown_debugfs(tsdata);
	}

   For I2C devices, i2c_device_remove() then immediately calls
   debugfs_remove_recursive(client->debugfs) before devres_release_group().
   Because panel_work has not been cancelled yet, a concurrent execution of
   edt_ft5x06_panel_work() -> edt_ft5x06_ts_init_hw() ->
   edt_ft5x06_ts_prepare_debugfs() can dereference the freed tsdata->debugfs
   dentry and allocate tsdata->raw_buffer after teardown has already run.

2. During devres teardown, remove_nodes() drains dev->devres_head into a
   local todo list before release_nodes() invokes the devm actions for
   drm_panel_remove_follower and edt_ft5x06_cancel_panel_work. If
   edt_ft5x06_panel_work() is mid-flight inside edt_ft5x06_ts_init_hw()
   waiting to finish while edt_ft5x06_cancel_panel_work() blocks in
   cancel_work_sync(), input_register_device(tsdata->input) will append
   devm_input_device_unregister to dev->devres_head after dev->devres_head
   was already drained into todo. release_nodes() will then execute
   devm_input_device_release from todo without having run
   devm_input_device_unregister.

> @@ -1331,14 +1450,24 @@ int edt_ft5x06_probe(struct device *dev, struct regmap *regmap, int irq,
>  	tsdata->factory_mode = false;
>  	dev_set_drvdata(dev, tsdata);
>  
> -	error = edt_ft5x06_ts_init_hw(tsdata);
> -	if (error)
> -		return error;
> +	/*
> +	 * A controller sharing its power with a panel can only be accessed
> +	 * once the panel is up, so leave it in reset and do the rest of the
> +	 * initialization from the panel follower callback.
> +	 */
> +	tsdata->is_panel_follower = drm_is_panel_follower(dev);
> +	if (!tsdata->is_panel_follower) {
> +		error = edt_ft5x06_ts_init_hw(tsdata);
> +		if (error)
> +			return error;
> +	}

[Severity: Medium]
When tsdata->is_panel_follower is true and edt_ft5x06_ts_init_hw() is
deferred to edt_ft5x06_panel_work(), should model_show() and
fw_version_show() hold tsdata->mutex when reading tsdata->name and
tsdata->fw_version?

	static ssize_t model_show(struct device *dev,
				  struct device_attribute *attr, char *buf)
	{
		struct edt_ft5x06_ts_data *tsdata = dev_get_drvdata(dev);

		return sysfs_emit(buf, "%s\n", tsdata->name);
	}

	static ssize_t fw_version_show(struct device *dev,
				       struct device_attribute *attr, char *buf)
	{
		struct edt_ft5x06_ts_data *tsdata = dev_get_drvdata(dev);

		return sysfs_emit(buf, "%s\n", tsdata->fw_version);
	}

Previously, tsdata->name and tsdata->fw_version were populated synchronously
in edt_ft5x06_probe() before the sysfs attributes in edt_ft5x06_groups were
registered. With hardware initialization deferred to edt_ft5x06_panel_work(),
edt_ft5x06_ts_identify() now writes to tsdata->name and tsdata->fw_version
under tsdata->mutex after the sysfs files are already visible to userspace,
so unlocked reads in model_show() and fw_version_show() can race with
edt_ft5x06_ts_identify().

[ ... ]

>
>
>

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261017140827.290666-1-lukapanio@gmail.com?part=7

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

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

Thread overview: 11+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-17 14:07 [PATCH 0/7] Input: edt-ft5x06 - add SPI transport and FocalTech FT8722 support Luka Panio
2026-10-17 14:07 ` [PATCH 1/7] Input: edt-ft5x06 - split the bus independent core from the I2C driver Luka Panio
2026-10-17 14:07 ` [PATCH 2/7] Input: edt-ft5x06 - move the controller bring-up into a helper Luka Panio
2026-10-17 14:16   ` sashiko-bot
2026-10-17 14:07 ` [PATCH 3/7] Input: edt-ft5x06 - add an optional controller boot hook Luka Panio
2026-10-17 14:07 ` [PATCH 4/7] Input: edt-ft5x06 - allow a chip specific touch data register Luka Panio
2026-10-17 14:07 ` [PATCH 5/7] dt-bindings: input: touchscreen: edt-ft5x06: Add FocalTech FT8722 Luka Panio
2026-10-17 14:07 ` [PATCH 6/7] Input: edt-ft5x06 - add SPI transport and FT8722 support Luka Panio
2026-10-17 14:29   ` sashiko-bot
2026-10-17 14:07 ` [PATCH 7/7] Input: edt-ft5x06 - support following a DRM panel Luka Panio
2026-10-17 14:30   ` sashiko-bot

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