Linux Input/HID development
 help / color / mirror / Atom feed
From: Vsevolod Nevorotov <sevanevorotov29@gmail.com>
To: Dmitry Torokhov <dmitry.torokhov@gmail.com>,
	Rob Herring <robh@kernel.org>,
	Krzysztof Kozlowski <krzk+dt@kernel.org>,
	Conor Dooley <conor+dt@kernel.org>
Cc: Henrik Rydberg <rydberg@bitmath.org>,
	linux-input@vger.kernel.org, devicetree@vger.kernel.org,
	linux-kernel@vger.kernel.org,
	Vsevolod Nevorotov <sevanevorotov29@gmail.com>
Subject: [PATCH 2/2] Input: synaptics_tcm_i2c - add Synaptics TouchComm I2C touchscreen driver
Date: Thu, 10 Sep 2026 14:07:23 +0700	[thread overview]
Message-ID: <20260910070723.347768-3-sevanevorotov29@gmail.com> (raw)
In-Reply-To: <20260910070723.347768-1-sevanevorotov29@gmail.com>

Add input touchscreen driver for Synaptics TouchComm (TCM) generation-1
controllers connected over I2C that boot into application mode out of
their own flash (such as the Synaptics S3908 fitted to the OnePlus 9RT).

The driver supports:
- Multi-touch reporting via input_mt framework
- Configurable touch report layouts
- Standard touchscreen properties (axis swapping/inversion)
- Low-power wakeup gestures (double-tap to wake)
- Deep sleep mode during system suspend

Signed-off-by: Vsevolod Nevorotov <sevanevorotov29@gmail.com>
---
 MAINTAINERS                                   |    7 +
 drivers/input/touchscreen/Kconfig             |   15 +
 drivers/input/touchscreen/Makefile            |    1 +
 drivers/input/touchscreen/synaptics_tcm_i2c.c | 1033 +++++++++++++++++
 4 files changed, 1056 insertions(+)
 create mode 100644 drivers/input/touchscreen/synaptics_tcm_i2c.c

diff --git a/MAINTAINERS b/MAINTAINERS
index 0d7987278c0..aa20d9153ac 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -26125,6 +26125,13 @@ S:	Maintained
 F:	Documentation/devicetree/bindings/regulator/silergy,sy8106a.yaml
 F:	drivers/regulator/sy8106a-regulator.c
 
+SYNAPTICS TOUCHCOMM I2C TOUCHSCREEN DRIVER
+M:	Vsevolod Nevorotov <sevanevorotov29@gmail.com>
+L:	linux-input@vger.kernel.org
+S:	Maintained
+F:	Documentation/devicetree/bindings/input/touchscreen/syna,s3908.yaml
+F:	drivers/input/touchscreen/synaptics_tcm_i2c.c
+
 SYNC FILE FRAMEWORK
 M:	Sumit Semwal <sumit.semwal@linaro.org>
 L:	linux-media@vger.kernel.org
diff --git a/drivers/input/touchscreen/Kconfig b/drivers/input/touchscreen/Kconfig
index 9b9ae8ac3f7..c58e3e4cc1c 100644
--- a/drivers/input/touchscreen/Kconfig
+++ b/drivers/input/touchscreen/Kconfig
@@ -1310,6 +1310,21 @@ config TOUCHSCREEN_SX8654
 	  To compile this driver as a module, choose M here: the
 	  module will be called sx8654.
 
+config TOUCHSCREEN_SYNAPTICS_TCM_I2C
+	tristate "Synaptics TouchComm I2C touchscreen"
+	depends on I2C
+	help
+	  Say Y here if you have a Synaptics TouchComm (TCM) touch controller
+	  attached over I2C, such as the S3908 found in the OnePlus 9RT.
+
+	  Only controllers that boot into application mode out of their own
+	  flash are supported; there is no host firmware download.
+
+	  If unsure, say N.
+
+	  To compile this driver as a module, choose M here: the module will be
+	  called synaptics_tcm_i2c.
+
 config TOUCHSCREEN_TPS6507X
 	tristate "TPS6507x based touchscreens"
 	depends on I2C
diff --git a/drivers/input/touchscreen/Makefile b/drivers/input/touchscreen/Makefile
index bfd9de83389..a1debe134a0 100644
--- a/drivers/input/touchscreen/Makefile
+++ b/drivers/input/touchscreen/Makefile
@@ -86,6 +86,7 @@ obj-$(CONFIG_TOUCHSCREEN_STMPE)		+= stmpe-ts.o
 obj-$(CONFIG_TOUCHSCREEN_SUN4I)		+= sun4i-ts.o
 obj-$(CONFIG_TOUCHSCREEN_SUR40)		+= sur40.o
 obj-$(CONFIG_TOUCHSCREEN_SURFACE3_SPI)	+= surface3_spi.o
+obj-$(CONFIG_TOUCHSCREEN_SYNAPTICS_TCM_I2C)	+= synaptics_tcm_i2c.o
 obj-$(CONFIG_TOUCHSCREEN_TI_AM335X_TSC)	+= ti_am335x_tsc.o
 obj-$(CONFIG_TOUCHSCREEN_TOUCHIT213)	+= touchit213.o
 obj-$(CONFIG_TOUCHSCREEN_TOUCHRIGHT)	+= touchright.o
diff --git a/drivers/input/touchscreen/synaptics_tcm_i2c.c b/drivers/input/touchscreen/synaptics_tcm_i2c.c
new file mode 100644
index 00000000000..2553dabd392
--- /dev/null
+++ b/drivers/input/touchscreen/synaptics_tcm_i2c.c
@@ -0,0 +1,1033 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Synaptics TouchComm (TCM) I2C touchscreen driver.
+ *
+ * Covers TouchComm generation-1 controllers that boot into application mode out
+ * of their own flash, i.e. the ones that need no host firmware download at
+ * probe time. The Synaptics S3908 fitted to the OnePlus 9RT (oneplus,martini)
+ * is one of those.
+ *
+ * Protocol, as implemented by the vendor driver:
+ *
+ *   A command is one plain I2C write of
+ *
+ *      [ cmd | len_lo | len_hi | payload... ]
+ *
+ *   A message -- both command responses and asynchronous reports -- is one
+ *   plain I2C read of
+ *
+ *      [ 0xa5 | code | len_lo | len_hi | payload... | 0x5a ]
+ *
+ *   code <= 0x0f (and 0xff) is a status, so the message is a command response;
+ *   code >= 0x10 is a report id.
+ *
+ */
+
+#include <linux/bits.h>
+#include <linux/delay.h>
+#include <linux/gpio/consumer.h>
+#include <linux/i2c.h>
+#include <linux/input.h>
+#include <linux/input/mt.h>
+#include <linux/input/touchscreen.h>
+#include <linux/interrupt.h>
+#include <linux/mod_devicetable.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/pm.h>
+#include <linux/regulator/consumer.h>
+#include <linux/unaligned.h>
+
+#define SYNA_MESSAGE_MARKER		0xa5
+#define SYNA_MESSAGE_PADDING		0x5a
+#define SYNA_HEADER_SIZE		4
+
+/*
+ * Read length limit of the controller (RD_CHUNK_SIZE in the vendor driver).
+ * One read may not exceed this, including the header.
+ */
+#define SYNA_RD_CHUNK_SIZE		64
+
+/* Commands. */
+#define SYNA_CMD_IDENTIFY		0x02
+#define SYNA_CMD_ENABLE_REPORT		0x05
+#define SYNA_CMD_DISABLE_REPORT		0x06
+#define SYNA_CMD_GET_APP_INFO		0x20
+#define SYNA_CMD_GET_DYNAMIC_CONFIG	0x23
+#define SYNA_CMD_SET_DYNAMIC_CONFIG	0x24
+#define SYNA_CMD_GET_TOUCH_REPORT_CONFIG 0x25
+#define SYNA_CMD_SET_TOUCH_REPORT_CONFIG 0x26
+#define SYNA_CMD_ENTER_DEEP_SLEEP	0x2c
+#define SYNA_CMD_EXIT_DEEP_SLEEP	0x2d
+
+/* Dynamic config IDs. */
+#define SYNA_DC_IN_WAKEUP_GESTURE_MODE	0x09
+#define SYNA_DC_GESTURE_MASK		0xfe
+
+/* Status codes, i.e. header codes of a command response. */
+#define SYNA_STATUS_IDLE		0x00
+#define SYNA_STATUS_OK			0x01
+#define SYNA_STATUS_BUSY		0x02
+#define SYNA_STATUS_CONTINUED_READ	0x03
+#define SYNA_STATUS_ERROR		0x0f
+#define SYNA_STATUS_INVALID		0xff
+
+/* Report ids. Anything >= SYNA_REPORT_IDENTIFY is a report, not a status. */
+#define SYNA_REPORT_IDENTIFY		0x10
+#define SYNA_REPORT_TOUCH		0x11
+#define SYNA_REPORT_LOG			0x1d
+
+#define SYNA_MODE_APPLICATION		0x01
+
+/* Gesture opcodes & IDs. */
+#define SYNA_GESTURE_DTAP		0x01
+
+/* Opcodes of the touch report config. */
+enum syna_touch_report_code {
+	SYNA_TOUCH_END				= 0,
+	SYNA_TOUCH_FOREACH_ACTIVE_OBJECT	= 1,
+	SYNA_TOUCH_FOREACH_OBJECT		= 2,
+	SYNA_TOUCH_FOREACH_END			= 3,
+	SYNA_TOUCH_PAD_TO_NEXT_BYTE		= 4,
+	SYNA_TOUCH_TIMESTAMP			= 5,
+	SYNA_TOUCH_OBJECT_N_INDEX		= 6,
+	SYNA_TOUCH_OBJECT_N_CLASSIFICATION	= 7,
+	SYNA_TOUCH_OBJECT_N_X_POSITION		= 8,
+	SYNA_TOUCH_OBJECT_N_Y_POSITION		= 9,
+	SYNA_TOUCH_OBJECT_N_Z			= 10,
+	SYNA_TOUCH_OBJECT_N_X_WIDTH		= 11,
+	SYNA_TOUCH_OBJECT_N_Y_WIDTH		= 12,
+	SYNA_TOUCH_GESTURE_DOUBLE_TAP		= 16,
+	SYNA_TOUCH_NUM_OF_ACTIVE_OBJECTS	= 24,
+	SYNA_TOUCH_REPORT_GESTURE_SWIPE		= 193,
+	SYNA_TOUCH_REPORT_GESTURE_CIRCLE	= 194,
+	SYNA_TOUCH_REPORT_GESTURE_UNICODE	= 195,
+	SYNA_TOUCH_REPORT_GESTURE_VEE		= 196,
+	SYNA_TOUCH_REPORT_GESTURE_TRIANGLE	= 197,
+	SYNA_TOUCH_REPORT_GESTURE_INFO		= 198,
+	SYNA_TOUCH_REPORT_GESTURE_COORDINATE	= 199,
+};
+
+/* Object classification. Anything other than LIFT counts as a contact. */
+#define SYNA_OBJECT_LIFT		0
+
+#define SYNA_MAX_OBJECTS		16
+#define SYNA_MAX_CONFIG_SIZE		128
+#define SYNA_MAX_MESSAGE_SIZE		1024
+
+/* Vendor driver: POWERUP_TO_RESET_TIME and RESET_TO_NORMAL_TIME. */
+#define SYNA_POWERUP_TO_RESET_MS	10
+#define SYNA_RESET_TO_NORMAL_MS		1000
+
+#define SYNA_RESPONSE_TIMEOUT_MS	500
+
+struct syna_tcm_app_info {
+	__le16 version;
+	__le16 status;
+	__le16 static_config_size;
+	__le16 dynamic_config_size;
+	__le16 app_config_start_write_block;
+	__le16 app_config_size;
+	__le16 max_touch_report_config_size;
+	__le16 max_touch_report_payload_size;
+	u8 customer_config_id[16];
+	__le16 max_x;
+	__le16 max_y;
+	__le16 max_objects;
+	__le16 num_of_buttons;
+	__le16 num_of_image_rows;
+	__le16 num_of_image_cols;
+	__le16 has_hybrid_data;
+} __packed;
+
+struct syna_object {
+	u32 status;
+	u32 x;
+	u32 y;
+	u32 z;
+	u32 wx;
+	u32 wy;
+};
+
+struct syna_tcm {
+	struct i2c_client *client;
+	struct input_dev *input;
+	struct touchscreen_properties prop;
+	struct gpio_desc *reset_gpio;
+	struct regulator_bulk_data supplies[2];
+
+	/* Protects hardware I2C transfers and internal state */
+	struct mutex lock;
+	bool suspended;
+	bool gesture_enabled;
+	u32 gesture_type;
+
+	/* Scratch for one I2C read, and the message assembled out of them. */
+	u8 rxbuf[SYNA_RD_CHUNK_SIZE];
+	u8 payload[SYNA_MAX_MESSAGE_SIZE];
+	unsigned int payload_len;
+	u8 code;
+
+	u8 cmdbuf[SYNA_MAX_CONFIG_SIZE + 3];
+
+	u8 config[SYNA_MAX_CONFIG_SIZE];
+	unsigned int config_len;
+
+	u8 normal_config[SYNA_MAX_CONFIG_SIZE];
+	unsigned int normal_config_len;
+
+	unsigned int max_objects;
+	struct syna_object objects[SYNA_MAX_OBJECTS];
+};
+
+/*
+ * Low-power gesture report config matching the stock vendor driver:
+ * Double-tap ID, gesture info, coordinates, and active object loop.
+ */
+static const u8 syna_gesture_config[] = {
+	SYNA_TOUCH_GESTURE_DOUBLE_TAP,		8,
+	SYNA_TOUCH_REPORT_GESTURE_INFO,		48,
+	SYNA_TOUCH_REPORT_GESTURE_COORDINATE,	192,
+	SYNA_TOUCH_FOREACH_ACTIVE_OBJECT,
+	SYNA_TOUCH_OBJECT_N_INDEX,		4,
+	SYNA_TOUCH_OBJECT_N_CLASSIFICATION,	4,
+	SYNA_TOUCH_OBJECT_N_X_POSITION,		16,
+	SYNA_TOUCH_OBJECT_N_Y_POSITION,		16,
+	SYNA_TOUCH_FOREACH_END,
+	SYNA_TOUCH_END,
+};
+
+/*
+ * Receive one whole message. On success ts->code holds the header code and the
+ * first ts->payload_len bytes of ts->payload hold its payload.
+ *
+ * Must be called with ts->lock held.
+ */
+static int syna_recv_message(struct syna_tcm *ts)
+{
+	struct device *dev = &ts->client->dev;
+	unsigned int total, copied, remaining;
+	int ret;
+
+	ret = i2c_master_recv(ts->client, ts->rxbuf, SYNA_RD_CHUNK_SIZE);
+	if (ret < 0)
+		return ret;
+
+	if (ret < SYNA_HEADER_SIZE) {
+		dev_warn_ratelimited(dev, "i2c recv short read: %d\n", ret);
+		return -EIO;
+	}
+
+	if (ts->rxbuf[0] != SYNA_MESSAGE_MARKER) {
+		dev_dbg(dev, "bad message marker 0x%02x\n", ts->rxbuf[0]);
+		return -EPROTO;
+	}
+
+	ts->code = ts->rxbuf[1];
+	ts->payload_len = get_unaligned_le16(&ts->rxbuf[2]);
+
+	/*
+	 * An idle or busy controller answers with a header and nothing behind
+	 * it, whatever the length field happens to say.
+	 */
+	if (ts->code == SYNA_STATUS_IDLE || ts->code == SYNA_STATUS_BUSY ||
+	    ts->code == SYNA_STATUS_CONTINUED_READ) {
+		ts->payload_len = 0;
+		return 0;
+	}
+
+	if (ts->payload_len > sizeof(ts->payload)) {
+		dev_err(dev, "message payload of %u bytes is too long\n",
+			ts->payload_len);
+		return -EMSGSIZE;
+	}
+
+	/* Header, payload, and the trailing 0x5a padding byte. */
+	total = SYNA_HEADER_SIZE + ts->payload_len + 1;
+	copied = min(ts->payload_len, SYNA_RD_CHUNK_SIZE - SYNA_HEADER_SIZE);
+	memcpy(ts->payload, &ts->rxbuf[SYNA_HEADER_SIZE], copied);
+
+	if (total <= SYNA_RD_CHUNK_SIZE) {
+		if (ts->rxbuf[total - 1] != SYNA_MESSAGE_PADDING) {
+			dev_dbg(dev, "bad message padding 0x%02x\n",
+				ts->rxbuf[total - 1]);
+			return -EPROTO;
+		}
+		return 0;
+	}
+
+	/*
+	 * Continued read: the rest arrives in chunks that repeat the marker and
+	 * carry code 0x03, so only SYNA_RD_CHUNK_SIZE - 2 bytes of each one are
+	 * payload. The trailing padding byte (0x5a) is read and verified in the
+	 * final chunk.
+	 */
+	remaining = ts->payload_len - copied;
+	while (remaining) {
+		bool last = (remaining <= SYNA_RD_CHUNK_SIZE - 3);
+		unsigned int chunk = last ? remaining :
+					    min(remaining - 1,
+						(unsigned int)(SYNA_RD_CHUNK_SIZE - 3));
+		unsigned int read_len = chunk + 2 + (last ? 1 : 0);
+
+		ret = i2c_master_recv(ts->client, ts->rxbuf, read_len);
+		if (ret < 0)
+			return ret;
+
+		if (ret < read_len)
+			return -EIO;
+
+		if (ts->rxbuf[0] != SYNA_MESSAGE_MARKER ||
+		    ts->rxbuf[1] != SYNA_STATUS_CONTINUED_READ) {
+			dev_err(dev, "bad continued-read header 0x%02x 0x%02x\n",
+				ts->rxbuf[0], ts->rxbuf[1]);
+			return -EPROTO;
+		}
+
+		memcpy(&ts->payload[copied], &ts->rxbuf[2], chunk);
+		copied += chunk;
+		remaining -= chunk;
+
+		if (last && ts->rxbuf[2 + chunk] != SYNA_MESSAGE_PADDING) {
+			dev_dbg(dev, "bad continued-read padding 0x%02x\n",
+				ts->rxbuf[2 + chunk]);
+			return -EPROTO;
+		}
+	}
+
+	return 0;
+}
+
+/**
+ * syna_exec_command() - Issue a command and collect its response
+ * @ts: Synaptics TCM device context
+ * @cmd: Command opcode (SYNA_CMD_*)
+ * @payload: Command payload buffer, or NULL
+ * @len: Length of @payload in bytes
+ *
+ * Issue a command and collect its response into ts->payload. Reports that
+ * arrive while waiting are dropped.
+ *
+ * Context: Must be called with @ts->lock held (except during probe before
+ *          the interrupt handler is registered).
+ * Return: 0 on success, or a negative errno.
+ */
+static int syna_exec_command(struct syna_tcm *ts, u8 cmd,
+			     const u8 *payload, unsigned int len)
+{
+	struct device *dev = &ts->client->dev;
+	unsigned long deadline;
+	u8 *buf = ts->cmdbuf;
+	int ret;
+
+	if (len + 3 > sizeof(ts->cmdbuf))
+		return -EINVAL;
+
+	buf[0] = cmd;
+	put_unaligned_le16(len, &buf[1]);
+	if (len)
+		memcpy(&buf[3], payload, len);
+
+	ret = i2c_master_send(ts->client, buf, len + 3);
+	if (ret < 0) {
+		dev_err(dev, "failed to write command 0x%02x: %d\n", cmd, ret);
+		return ret;
+	}
+
+	deadline = jiffies + msecs_to_jiffies(SYNA_RESPONSE_TIMEOUT_MS);
+	do {
+		usleep_range(15000, 20000);
+
+		ret = syna_recv_message(ts);
+		if (ret == -EPROTO)
+			continue;	/* controller not talking yet */
+		if (ret < 0)
+			return ret;
+
+		if (ts->code == SYNA_REPORT_IDENTIFY && cmd == SYNA_CMD_IDENTIFY)
+			return 0;
+
+		if (ts->code >= SYNA_REPORT_IDENTIFY)
+			continue;	/* a report, not our response */
+
+		switch (ts->code) {
+		case SYNA_STATUS_OK:
+			return 0;
+		case SYNA_STATUS_IDLE:
+		case SYNA_STATUS_BUSY:
+			continue;
+		default:
+			dev_err(dev, "command 0x%02x failed with status 0x%02x\n",
+				cmd, ts->code);
+			return -EIO;
+		}
+	} while (time_before(jiffies, deadline));
+
+	dev_err(dev, "command 0x%02x timed out\n", cmd);
+	return -ETIMEDOUT;
+}
+
+static int syna_set_dynamic_config(struct syna_tcm *ts, u8 id, u16 value)
+{
+	u8 buf[3];
+
+	buf[0] = id;
+	put_unaligned_le16(value, &buf[1]);
+
+	return syna_exec_command(ts, SYNA_CMD_SET_DYNAMIC_CONFIG, buf, sizeof(buf));
+}
+
+/* Extract a big field of @bits from the report's bit stream at @offset. */
+static u32 syna_get_bits(const u8 *buf, unsigned int buf_len,
+			 unsigned int offset, unsigned int bits)
+{
+	unsigned int byte = offset / 8;
+	unsigned int bit = offset % 8;
+	unsigned int done = 0;
+	u32 out = 0;
+
+	if (!bits || bits > 32 || offset + bits > buf_len * 8)
+		return 0;
+
+	while (done < bits) {
+		unsigned int avail = 8 - bit;
+		unsigned int take = min(avail, bits - done);
+		u8 val = (buf[byte] >> bit) & (0xff >> (8 - take));
+
+		out |= (u32)val << done;
+		done += take;
+		bit = 0;
+		byte++;
+	}
+
+	return out;
+}
+
+/*
+ * Walk the touch report config and unpack the report accordingly.
+ */
+static int syna_parse_touch_report(struct syna_tcm *ts)
+{
+	const u8 *cfg = ts->config;
+	unsigned int idx = 0, loop_start = 0, loop_bits = 0;
+	unsigned int offset = 0;	/* bit offset into the payload */
+	unsigned int obj = 0;
+	unsigned int seen = 0, active = 0;
+	bool active_only = false, have_active_count = false;
+	bool done = false;
+	unsigned int i;
+
+	memset(ts->objects, 0, sizeof(ts->objects));
+	ts->gesture_type = 0;
+
+	while (idx < ts->config_len && !done) {
+		u8 code = cfg[idx++];
+		unsigned int bits;
+		u32 data;
+
+		switch (code) {
+		case SYNA_TOUCH_END:
+			done = true;
+			continue;
+
+		case SYNA_TOUCH_FOREACH_ACTIVE_OBJECT:
+		case SYNA_TOUCH_FOREACH_OBJECT:
+			active_only = code == SYNA_TOUCH_FOREACH_ACTIVE_OBJECT;
+			loop_start = idx;
+			obj = 0;
+			seen = 0;
+			loop_bits = 0;
+			for (i = idx; i < ts->config_len; i++) {
+				if (cfg[i] == SYNA_TOUCH_FOREACH_END ||
+				    cfg[i] == SYNA_TOUCH_END)
+					break;
+				if (cfg[i] == SYNA_TOUCH_PAD_TO_NEXT_BYTE)
+					continue;
+				if (++i < ts->config_len)
+					loop_bits += cfg[i];
+			}
+			if (active_only && !have_active_count &&
+			    offset + loop_bits > ts->payload_len * 8) {
+				/* Not enough payload for even one object */
+				if (i < ts->config_len && cfg[i] == SYNA_TOUCH_FOREACH_END)
+					idx = i + 1;
+				else
+					idx = i;
+			}
+			continue;
+
+		case SYNA_TOUCH_FOREACH_END:
+			if (!active_only) {
+				if (++obj < ts->max_objects)
+					idx = loop_start;
+			} else if (have_active_count) {
+				if (++seen < active)
+					idx = loop_start;
+			} else if (++seen < ts->max_objects &&
+				   offset + loop_bits <= ts->payload_len * 8) {
+				idx = loop_start;
+			}
+			continue;
+
+		case SYNA_TOUCH_PAD_TO_NEXT_BYTE:
+			offset = ALIGN(offset, 8);
+			continue;
+		}
+
+		/* Everything else is (opcode, bit width). */
+		if (idx >= ts->config_len)
+			break;
+
+		bits = cfg[idx++];
+		data = syna_get_bits(ts->payload, ts->payload_len, offset, bits);
+		offset += bits;
+
+		switch (code) {
+		case SYNA_TOUCH_OBJECT_N_INDEX:
+			if (data >= ts->max_objects || data >= SYNA_MAX_OBJECTS) {
+				dev_dbg(&ts->client->dev,
+					"object index %u out of range\n", data);
+				return -ERANGE;
+			}
+			obj = data;
+			break;
+		case SYNA_TOUCH_NUM_OF_ACTIVE_OBJECTS:
+			active = data;
+			have_active_count = true;
+			if (!active)
+				done = true;
+			break;
+		case SYNA_TOUCH_OBJECT_N_CLASSIFICATION:
+			ts->objects[obj].status = data;
+			break;
+		case SYNA_TOUCH_OBJECT_N_X_POSITION:
+			ts->objects[obj].x = data;
+			break;
+		case SYNA_TOUCH_OBJECT_N_Y_POSITION:
+			ts->objects[obj].y = data;
+			break;
+		case SYNA_TOUCH_OBJECT_N_Z:
+			ts->objects[obj].z = data;
+			break;
+		case SYNA_TOUCH_OBJECT_N_X_WIDTH:
+			ts->objects[obj].wx = data;
+			break;
+		case SYNA_TOUCH_OBJECT_N_Y_WIDTH:
+			ts->objects[obj].wy = data;
+			break;
+		case SYNA_TOUCH_GESTURE_DOUBLE_TAP:
+		case SYNA_TOUCH_REPORT_GESTURE_SWIPE:
+		case SYNA_TOUCH_REPORT_GESTURE_CIRCLE:
+		case SYNA_TOUCH_REPORT_GESTURE_UNICODE:
+		case SYNA_TOUCH_REPORT_GESTURE_VEE:
+		case SYNA_TOUCH_REPORT_GESTURE_TRIANGLE:
+			ts->gesture_type = data;
+			break;
+		default:
+			/* Timestamp, grip info, coordinates: skipped. */
+			break;
+		}
+	}
+
+	return 0;
+}
+
+static void syna_report_touch(struct syna_tcm *ts)
+{
+	unsigned int i;
+
+	for (i = 0; i < ts->max_objects; i++) {
+		struct syna_object *obj = &ts->objects[i];
+		bool down = obj->status != SYNA_OBJECT_LIFT;
+
+		if (down)
+			dev_dbg(&ts->client->dev,
+				"slot %u: %u,%u z=%u w=%ux%u class=%u\n",
+				i, obj->x, obj->y, obj->z, obj->wx, obj->wy,
+				obj->status);
+
+		input_mt_slot(ts->input, i);
+		if (!input_mt_report_slot_state(ts->input, MT_TOOL_FINGER, down))
+			continue;
+
+		touchscreen_report_pos(ts->input, &ts->prop, obj->x, obj->y, true);
+		input_report_abs(ts->input, ABS_MT_PRESSURE, obj->z);
+		input_report_abs(ts->input, ABS_MT_TOUCH_MAJOR,
+				 max(obj->wx, obj->wy));
+		input_report_abs(ts->input, ABS_MT_TOUCH_MINOR,
+				 min(obj->wx, obj->wy));
+	}
+
+	input_mt_sync_frame(ts->input);
+	input_sync(ts->input);
+}
+
+static irqreturn_t syna_irq(int irq, void *data)
+{
+	struct syna_tcm *ts = data;
+
+	mutex_lock(&ts->lock);
+
+	if (syna_recv_message(ts) < 0) {
+		mutex_unlock(&ts->lock);
+		return IRQ_HANDLED;
+	}
+
+	if (ts->code != SYNA_REPORT_TOUCH) {
+		mutex_unlock(&ts->lock);
+		return IRQ_HANDLED;
+	}
+
+	if (syna_parse_touch_report(ts) == 0) {
+		if (ts->suspended) {
+			if (ts->gesture_type == SYNA_GESTURE_DTAP) {
+				dev_dbg(&ts->client->dev,
+					"wakeup gesture detected (type=0x%02x)\n",
+					ts->gesture_type);
+
+				input_report_key(ts->input, KEY_WAKEUP, 1);
+				input_sync(ts->input);
+				input_report_key(ts->input, KEY_WAKEUP, 0);
+				input_sync(ts->input);
+
+				pm_wakeup_event(&ts->client->dev, 0);
+			}
+		} else {
+			syna_report_touch(ts);
+		}
+	}
+
+	mutex_unlock(&ts->lock);
+	return IRQ_HANDLED;
+}
+
+static int syna_power_up(struct syna_tcm *ts)
+{
+	int ret;
+
+	/* Held in reset across power-up; the DT flag makes this drive low. */
+	gpiod_set_value_cansleep(ts->reset_gpio, 1);
+
+	ret = regulator_bulk_enable(ARRAY_SIZE(ts->supplies), ts->supplies);
+	if (ret)
+		return ret;
+
+	msleep(SYNA_POWERUP_TO_RESET_MS);
+	gpiod_set_value_cansleep(ts->reset_gpio, 0);
+	msleep(SYNA_RESET_TO_NORMAL_MS);
+
+	return 0;
+}
+
+static void syna_power_down(void *data)
+{
+	struct syna_tcm *ts = data;
+
+	gpiod_set_value_cansleep(ts->reset_gpio, 1);
+	regulator_bulk_disable(ARRAY_SIZE(ts->supplies), ts->supplies);
+}
+
+/*
+ * Fallback touch report config installed only if the firmware comes up
+ * without coordinates.
+ */
+static const u8 syna_default_config[] = {
+	SYNA_TOUCH_FOREACH_ACTIVE_OBJECT,
+	SYNA_TOUCH_OBJECT_N_INDEX,		4,
+	SYNA_TOUCH_OBJECT_N_CLASSIFICATION,	4,
+	SYNA_TOUCH_OBJECT_N_X_POSITION,		16,
+	SYNA_TOUCH_OBJECT_N_Y_POSITION,		16,
+	SYNA_TOUCH_OBJECT_N_Z,			8,
+	SYNA_TOUCH_OBJECT_N_X_WIDTH,		12,
+	SYNA_TOUCH_OBJECT_N_Y_WIDTH,		12,
+	SYNA_TOUCH_FOREACH_END,
+	SYNA_TOUCH_END,
+};
+
+static bool syna_config_has_coords(const u8 *cfg, unsigned int len)
+{
+	bool x = false, y = false;
+	unsigned int i;
+
+	for (i = 0; i < len; i++) {
+		switch (cfg[i]) {
+		case SYNA_TOUCH_END:
+			return x && y;
+		case SYNA_TOUCH_FOREACH_ACTIVE_OBJECT:
+		case SYNA_TOUCH_FOREACH_OBJECT:
+		case SYNA_TOUCH_FOREACH_END:
+		case SYNA_TOUCH_PAD_TO_NEXT_BYTE:
+			break;
+		default:
+			if (cfg[i] == SYNA_TOUCH_OBJECT_N_X_POSITION)
+				x = true;
+			else if (cfg[i] == SYNA_TOUCH_OBJECT_N_Y_POSITION)
+				y = true;
+			i++;	/* skip the bit width */
+			break;
+		}
+	}
+
+	return x && y;
+}
+
+static int syna_read_touch_report_config(struct syna_tcm *ts)
+{
+	struct device *dev = &ts->client->dev;
+	int ret;
+
+	ret = syna_exec_command(ts, SYNA_CMD_GET_TOUCH_REPORT_CONFIG, NULL, 0);
+	if (ret)
+		return ret;
+
+	if (!ts->payload_len || ts->payload_len > sizeof(ts->config)) {
+		dev_err(dev, "implausible touch report config size %u\n",
+			ts->payload_len);
+		return -EPROTO;
+	}
+
+	ts->config_len = ts->payload_len;
+	memcpy(ts->config, ts->payload, ts->config_len);
+
+	dev_dbg(dev, "touch report config: %*ph\n", (int)ts->config_len, ts->config);
+
+	return 0;
+}
+
+static int syna_setup(struct syna_tcm *ts)
+{
+	struct device *dev = &ts->client->dev;
+	struct syna_tcm_app_info *app_info;
+	unsigned int max_x = 0, max_y = 0;
+	u8 report = SYNA_REPORT_TOUCH;
+	int ret;
+
+	ret = syna_exec_command(ts, SYNA_CMD_IDENTIFY, NULL, 0);
+	if (ret)
+		return ret;
+
+	if (ts->payload_len < 18)
+		return -EPROTO;
+
+	dev_info(dev, "TouchComm v%u, mode 0x%02x, part %.16s\n",
+		 ts->payload[0], ts->payload[1], &ts->payload[2]);
+
+	if (ts->payload[1] != SYNA_MODE_APPLICATION) {
+		dev_err(dev,
+			"controller is in mode 0x%02x, not application mode; this driver cannot download firmware\n",
+			ts->payload[1]);
+		return -ENODEV;
+	}
+
+	ret = syna_exec_command(ts, SYNA_CMD_GET_APP_INFO, NULL, 0);
+	if (ret)
+		return ret;
+
+	if (ts->payload_len < sizeof(*app_info))
+		return -EPROTO;
+
+	app_info = (struct syna_tcm_app_info *)ts->payload;
+	max_x = le16_to_cpu(app_info->max_x);
+	max_y = le16_to_cpu(app_info->max_y);
+	ts->max_objects = le16_to_cpu(app_info->max_objects);
+
+	if (!ts->max_objects)
+		ts->max_objects = 10;
+
+	if (ts->max_objects > SYNA_MAX_OBJECTS) {
+		dev_warn(dev, "clamping %u objects to %u\n",
+			 ts->max_objects, SYNA_MAX_OBJECTS);
+		ts->max_objects = SYNA_MAX_OBJECTS;
+	}
+
+	if (!max_x || !max_y) {
+		dev_err(dev, "touchscreen size not reported by firmware\n");
+		return -EINVAL;
+	}
+
+	dev_info(dev, "%ux%u, %u touch objects\n", max_x + 1, max_y + 1,
+		 ts->max_objects);
+
+	ret = syna_read_touch_report_config(ts);
+	if (ret)
+		return ret;
+
+	if (!syna_config_has_coords(ts->config, ts->config_len)) {
+		dev_warn(dev,
+			 "firmware touch report config carries no coordinates, installing our own\n");
+
+		ret = syna_exec_command(ts, SYNA_CMD_SET_TOUCH_REPORT_CONFIG,
+					syna_default_config,
+					sizeof(syna_default_config));
+		if (ret)
+			return ret;
+
+		ret = syna_read_touch_report_config(ts);
+		if (ret)
+			return ret;
+
+		if (!syna_config_has_coords(ts->config, ts->config_len)) {
+			dev_err(dev, "controller kept a config we cannot parse\n");
+			return -ENODEV;
+		}
+	}
+
+	/* Cache the normal report config for restoring after gesture wakeup */
+	ts->normal_config_len = ts->config_len;
+	memcpy(ts->normal_config, ts->config, ts->normal_config_len);
+
+	ret = syna_exec_command(ts, SYNA_CMD_ENABLE_REPORT, &report, 1);
+	if (ret)
+		return ret;
+
+	ts->input->name = "Synaptics TouchComm Touchscreen";
+	ts->input->id.bustype = BUS_I2C;
+
+	input_set_capability(ts->input, EV_KEY, KEY_WAKEUP);
+
+	input_set_abs_params(ts->input, ABS_MT_POSITION_X, 0, max_x, 0, 0);
+	input_set_abs_params(ts->input, ABS_MT_POSITION_Y, 0, max_y, 0, 0);
+	/*
+	 * ABS_MT_PRESSURE (8-bit, 0-255) and ABS_MT_TOUCH_MAJOR/MINOR (12-bit,
+	 * 0-4095) ranges are tailored for the S3908 / generation-1 family with
+	 * the default touch report layout (syna_default_config).
+	 */
+	input_set_abs_params(ts->input, ABS_MT_PRESSURE, 0, 255, 0, 0);
+	input_set_abs_params(ts->input, ABS_MT_TOUCH_MAJOR, 0, 4095, 0, 0);
+	input_set_abs_params(ts->input, ABS_MT_TOUCH_MINOR, 0, 4095, 0, 0);
+
+	touchscreen_parse_properties(ts->input, true, &ts->prop);
+
+	return input_mt_init_slots(ts->input, ts->max_objects,
+				   INPUT_MT_DIRECT | INPUT_MT_DROP_UNUSED);
+}
+
+static int syna_reinit_hw(struct syna_tcm *ts)
+{
+	struct device *dev = &ts->client->dev;
+	u8 report = SYNA_REPORT_TOUCH;
+	int ret;
+
+	syna_power_down(ts);
+	ret = syna_power_up(ts);
+	if (ret)
+		return ret;
+
+	ret = syna_exec_command(ts, SYNA_CMD_IDENTIFY, NULL, 0);
+	if (ret)
+		return ret;
+
+	if (ts->payload_len < 18 || ts->payload[1] != SYNA_MODE_APPLICATION) {
+		dev_err(dev, "controller not in application mode after recovery\n");
+		return -ENODEV;
+	}
+
+	ret = syna_exec_command(ts, SYNA_CMD_SET_TOUCH_REPORT_CONFIG,
+				ts->normal_config, ts->normal_config_len);
+	if (ret)
+		return ret;
+
+	ret = syna_read_touch_report_config(ts);
+	if (ret)
+		return ret;
+
+	return syna_exec_command(ts, SYNA_CMD_ENABLE_REPORT, &report, 1);
+}
+
+static int syna_probe(struct i2c_client *client)
+{
+	struct device *dev = &client->dev;
+	struct syna_tcm *ts;
+	int ret;
+
+	ts = devm_kzalloc(dev, sizeof(*ts), GFP_KERNEL);
+	if (!ts)
+		return -ENOMEM;
+
+	ts->client = client;
+	ret = devm_mutex_init(dev, &ts->lock);
+	if (ret)
+		return ret;
+
+	i2c_set_clientdata(client, ts);
+
+	ts->input = devm_input_allocate_device(dev);
+	if (!ts->input)
+		return -ENOMEM;
+
+	ts->supplies[0].supply = "vdd";
+	ts->supplies[1].supply = "avdd";
+	ret = devm_regulator_bulk_get(dev, ARRAY_SIZE(ts->supplies),
+				      ts->supplies);
+	if (ret)
+		return dev_err_probe(dev, ret, "failed to get supplies\n");
+
+	ts->reset_gpio = devm_gpiod_get(dev, "reset", GPIOD_OUT_HIGH);
+	if (IS_ERR(ts->reset_gpio))
+		return dev_err_probe(dev, PTR_ERR(ts->reset_gpio),
+				     "failed to get reset GPIO\n");
+
+	ret = syna_power_up(ts);
+	if (ret)
+		return dev_err_probe(dev, ret, "failed to power up\n");
+
+	ret = devm_add_action_or_reset(dev, syna_power_down, ts);
+	if (ret)
+		return ret;
+
+	ret = syna_setup(ts);
+	if (ret)
+		return ret;
+
+	ret = input_register_device(ts->input);
+	if (ret)
+		return dev_err_probe(dev, ret, "failed to register input device\n");
+
+	if (device_property_read_bool(dev, "wakeup-source")) {
+		ret = devm_device_init_wakeup(dev);
+		if (ret)
+			return ret;
+	}
+
+	ret = devm_request_threaded_irq(dev, client->irq, NULL, syna_irq,
+					IRQF_ONESHOT, "synaptics-tcm", ts);
+	if (ret)
+		return dev_err_probe(dev, ret, "failed to request IRQ %d\n",
+				     client->irq);
+
+	return 0;
+}
+
+static int syna_suspend(struct device *dev)
+{
+	struct i2c_client *client = to_i2c_client(dev);
+	struct syna_tcm *ts = i2c_get_clientdata(client);
+	int ret;
+
+	disable_irq(client->irq);
+	mutex_lock(&ts->lock);
+
+	if (device_may_wakeup(dev)) {
+		/* Exit deep sleep if the controller was in sleep mode */
+		ret = syna_exec_command(ts, SYNA_CMD_EXIT_DEEP_SLEEP, NULL, 0);
+		if (ret && ret != -ETIMEDOUT)
+			dev_dbg(dev, "exit deep sleep status: %d\n", ret);
+
+		/* Switch to gesture touch report layout */
+		ret = syna_exec_command(ts, SYNA_CMD_SET_TOUCH_REPORT_CONFIG,
+					syna_gesture_config,
+					sizeof(syna_gesture_config));
+		if (ret)
+			dev_warn(dev, "failed to set gesture report config: %d\n", ret);
+
+		ret = syna_read_touch_report_config(ts);
+		if (ret)
+			dev_warn(dev, "failed to read touch report config: %d\n", ret);
+
+		/* Enable Low Power Wakeup Gesture Mode */
+		ret = syna_set_dynamic_config(ts, SYNA_DC_IN_WAKEUP_GESTURE_MODE, 1);
+		if (ret)
+			dev_warn(dev, "failed to enable wakeup gesture mode: %d\n", ret);
+
+		/* Set gesture mask: bit 0 = DTAP (double-tap to wake) */
+		ret = syna_set_dynamic_config(ts, SYNA_DC_GESTURE_MASK, BIT(0));
+		if (ret)
+			dev_warn(dev, "failed to set gesture mask: %d\n", ret);
+
+		enable_irq_wake(client->irq);
+		enable_irq(client->irq);
+		ts->gesture_enabled = true;
+	} else {
+		/* Deep sleep mode when wakeup is disabled */
+		ret = syna_exec_command(ts, SYNA_CMD_ENTER_DEEP_SLEEP, NULL, 0);
+		if (ret)
+			dev_warn(dev, "failed to enter deep sleep: %d\n", ret);
+
+		ts->gesture_enabled = false;
+	}
+
+	ts->suspended = true;
+	mutex_unlock(&ts->lock);
+
+	return 0;
+}
+
+static int syna_resume(struct device *dev)
+{
+	struct i2c_client *client = to_i2c_client(dev);
+	struct syna_tcm *ts = i2c_get_clientdata(client);
+	int ret;
+
+	if (ts->gesture_enabled) {
+		disable_irq(client->irq);
+		mutex_lock(&ts->lock);
+		disable_irq_wake(client->irq);
+
+		/* Exit Wakeup Gesture Mode */
+		ret = syna_set_dynamic_config(ts, SYNA_DC_IN_WAKEUP_GESTURE_MODE, 0);
+		if (ret)
+			dev_warn(dev, "failed to disable wakeup gesture mode: %d\n", ret);
+
+		/* Restore normal touch report layout */
+		ret = syna_exec_command(ts, SYNA_CMD_SET_TOUCH_REPORT_CONFIG,
+					ts->normal_config,
+					ts->normal_config_len);
+		if (ret)
+			dev_warn(dev, "failed to restore normal report config: %d\n", ret);
+
+		ret = syna_read_touch_report_config(ts);
+		if (ret)
+			dev_warn(dev, "failed to read touch report config: %d\n", ret);
+
+		ts->gesture_enabled = false;
+		ts->suspended = false;
+		enable_irq(client->irq);
+	} else {
+		mutex_lock(&ts->lock);
+
+		/* Wake up controller from deep sleep */
+		ret = syna_exec_command(ts, SYNA_CMD_EXIT_DEEP_SLEEP, NULL, 0);
+		if (ret) {
+			dev_warn(dev, "failed to exit deep sleep (%d), recovering...\n", ret);
+			ret = syna_reinit_hw(ts);
+			if (ret)
+				dev_err(dev, "failed to recover controller: %d\n", ret);
+		}
+
+		ts->suspended = false;
+		enable_irq(client->irq);
+	}
+
+	mutex_unlock(&ts->lock);
+
+	return 0;
+}
+
+static DEFINE_SIMPLE_DEV_PM_OPS(syna_tcm_pm_ops, syna_suspend, syna_resume);
+
+static const struct i2c_device_id syna_tcm_i2c_id[] = {
+	{ "synaptics-tcm" },
+	{ }
+};
+MODULE_DEVICE_TABLE(i2c, syna_tcm_i2c_id);
+
+static const struct of_device_id syna_tcm_of_match[] = {
+	{ .compatible = "syna,s3908" },
+	{ }
+};
+MODULE_DEVICE_TABLE(of, syna_tcm_of_match);
+
+static struct i2c_driver syna_tcm_i2c_driver = {
+	.driver = {
+		.name = "synaptics-tcm-i2c",
+		.of_match_table = syna_tcm_of_match,
+		.pm = pm_sleep_ptr(&syna_tcm_pm_ops),
+	},
+	.probe = syna_probe,
+	.id_table = syna_tcm_i2c_id,
+};
+module_i2c_driver(syna_tcm_i2c_driver);
+
+MODULE_DESCRIPTION("Synaptics TouchComm I2C touchscreen driver");
+MODULE_AUTHOR("Vsevolod Nevorotov <sevanevorotov29@gmail.com>");
+MODULE_LICENSE("GPL");
-- 
2.55.0


  parent reply	other threads:[~2026-09-10  7:07 UTC|newest]

Thread overview: 4+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-09-10  7:07 [PATCH 0/2] Input: add Synaptics TouchComm I2C touchscreen driver and bindings Vsevolod Nevorotov
2026-09-10  7:07 ` [PATCH 1/2] dt-bindings: input: touchscreen: add Synaptics S3908 controller Vsevolod Nevorotov
2026-09-10  7:07 ` Vsevolod Nevorotov [this message]
2026-09-10  7:21   ` [PATCH 2/2] Input: synaptics_tcm_i2c - add Synaptics TouchComm I2C touchscreen driver sashiko-bot

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260910070723.347768-3-sevanevorotov29@gmail.com \
    --to=sevanevorotov29@gmail.com \
    --cc=conor+dt@kernel.org \
    --cc=devicetree@vger.kernel.org \
    --cc=dmitry.torokhov@gmail.com \
    --cc=krzk+dt@kernel.org \
    --cc=linux-input@vger.kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=robh@kernel.org \
    --cc=rydberg@bitmath.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox