diff mbox series

[6/7] Input: touchscreen: add Synaptics TCM oncell S3908

Message ID 20240624-oneplus8-v1-6-388eecf2dff7@postmarketos.org
State New
Headers show
Series qcom: initial support for the OnePlus 8T | expand

Commit Message

Caleb Connolly June 24, 2024, 1:30 a.m. UTC
The TCM oncell is the next generation of Synaptics touchscreen ICs.
These run a very featured firmware with a reasonably well defined API.
It is however entirely incompatible with the existing RMI4 interface.

Unfortunately, no public datasheet for the interface seems to be
available, instead this driver was created through a combination of
vendor drivers and trial and error.

The firmware interface implies support for defining the exact bit
encoding of the touch reports, however on the S3908 chip + firmware
found in the OnePlus 8t the TCM_SET_TOUCH_REPORT_CONFIG command appears
to be unsupported.

Co-developed-by: Frieder Hannenheim <frieder.hannenheim@proton.me>
Signed-off-by: Frieder Hannenheim <frieder.hannenheim@proton.me>
Signed-off-by: Caleb Connolly <caleb@postmarketos.org>
---
 MAINTAINERS                                      |   7 +
 drivers/input/touchscreen/Kconfig                |  11 +
 drivers/input/touchscreen/Makefile               |   1 +
 drivers/input/touchscreen/synaptics_tcm_oncell.c | 617 +++++++++++++++++++++++
 4 files changed, 636 insertions(+)

Comments

Caleb Connolly June 27, 2024, 11:35 p.m. UTC | #1
On 24/06/2024 09:42, Dmitry Torokhov wrote:
> Hi Caleb,
> 
> On Mon, Jun 24, 2024 at 03:30:30AM +0200, Caleb Connolly wrote:
>> The TCM oncell is the next generation of Synaptics touchscreen ICs.
>> These run a very featured firmware with a reasonably well defined API.
>> It is however entirely incompatible with the existing RMI4 interface.
>>
>> Unfortunately, no public datasheet for the interface seems to be
>> available, instead this driver was created through a combination of
>> vendor drivers and trial and error.
>>
>> The firmware interface implies support for defining the exact bit
>> encoding of the touch reports, however on the S3908 chip + firmware
>> found in the OnePlus 8t the TCM_SET_TOUCH_REPORT_CONFIG command appears
>> to be unsupported.
>>
>> Co-developed-by: Frieder Hannenheim <frieder.hannenheim@proton.me>
>> Signed-off-by: Frieder Hannenheim <frieder.hannenheim@proton.me>
>> Signed-off-by: Caleb Connolly <caleb@postmarketos.org>
>> ---
>>   MAINTAINERS                                      |   7 +
>>   drivers/input/touchscreen/Kconfig                |  11 +
>>   drivers/input/touchscreen/Makefile               |   1 +
>>   drivers/input/touchscreen/synaptics_tcm_oncell.c | 617 +++++++++++++++++++++++
>>   4 files changed, 636 insertions(+)
>>
>> diff --git a/MAINTAINERS b/MAINTAINERS
>> index 2b9cfbf92d7a..db589c841d8c 100644
>> --- a/MAINTAINERS
>> +++ b/MAINTAINERS
>> @@ -21826,8 +21826,15 @@ M:	Icenowy Zheng <icenowy@aosc.io>
>>   S:	Maintained
>>   F:	Documentation/devicetree/bindings/regulator/silergy,sy8106a.yaml
>>   F:	drivers/regulator/sy8106a-regulator.c
>>   
>> +SYNAPTICS TCM ONCELL TOUCHSCREEN DRIVER
>> +M:	Caleb Connolly <caleb@postmarketos.org>
>> +L:	linux-input@vger.kernel.org
>> +S:	Maintained
>> +F:	Documentation/devicetree/bindings/input/touchscreen/syna,tcm-oncell.yaml
>> +F:	drivers/input/touchscreen/synaptics_tcm_oncell.c
>> +
>>   SYNC FILE FRAMEWORK
>>   M:	Sumit Semwal <sumit.semwal@linaro.org>
>>   R:	Gustavo Padovan <gustavo@padovan.org>
>>   L:	linux-media@vger.kernel.org
>> diff --git a/drivers/input/touchscreen/Kconfig b/drivers/input/touchscreen/Kconfig
>> index c821fe3ee794..43c4fd80601c 100644
>> --- a/drivers/input/touchscreen/Kconfig
>> +++ b/drivers/input/touchscreen/Kconfig
>> @@ -531,8 +531,19 @@ config TOUCHSCREEN_S6SY761
>>   
>>   	  To compile this driver as module, choose M here: the
>>   	  module will be called s6sy761.
>>   
>> +config TOUCHSCREEN_SYNAPTICS_TCM_ONCELL
>> +	tristate "Synaptics TCM Oncell Touchscreen driver"
>> +	depends on I2C
>> +	help
>> +	  Say Y if you have the Synaptics S3908 TCM Oncell
>> +
>> +	  If unsure, say N
>> +
>> +	  To compile this driver as module, choose M here: the
>> +	  module will be called synaptics_tcm_oncell.
>> +
>>   config TOUCHSCREEN_GUNZE
>>   	tristate "Gunze AHL-51S touchscreen"
>>   	select SERIO
>>   	help
>> diff --git a/drivers/input/touchscreen/Makefile b/drivers/input/touchscreen/Makefile
>> index a81cb5aa21a5..6a2b85050c3a 100644
>> --- a/drivers/input/touchscreen/Makefile
>> +++ b/drivers/input/touchscreen/Makefile
>> @@ -88,8 +88,9 @@ obj-$(CONFIG_TOUCHSCREEN_STMFTS)	+= stmfts.o
>>   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_ONCELL)	+= synaptics_tcm_oncell.o
>>   obj-$(CONFIG_TOUCHSCREEN_TI_AM335X_TSC)	+= ti_am335x_tsc.o
>>   obj-$(CONFIG_TOUCHSCREEN_TOUCHIT213)	+= touchit213.o
>>   obj-$(CONFIG_TOUCHSCREEN_TOUCHRIGHT)	+= touchright.o
>>   obj-$(CONFIG_TOUCHSCREEN_TOUCHWIN)	+= touchwin.o
>> diff --git a/drivers/input/touchscreen/synaptics_tcm_oncell.c b/drivers/input/touchscreen/synaptics_tcm_oncell.c
>> new file mode 100644
>> index 000000000000..c1ba9a3a93c0
>> --- /dev/null
>> +++ b/drivers/input/touchscreen/synaptics_tcm_oncell.c
>> @@ -0,0 +1,617 @@
>> +// SPDX-License-Identifier: GPL-2.0-only
>> +/*
>> + *  Driver for Synaptics TCM Oncell Touchscreens
>> + *
>> + *  Copyright (c) 2024 Frieder Hannenheim <frieder.hannenheim@proton.me>
>> + *  Copyright (c) 2024 Caleb Connolly <caleb@postmarketos.org>
>> + */
>> +
>> +#include <linux/i2c.h>
>> +#include <linux/input.h>
>> +#include <linux/input/touchscreen.h>
>> +#include <linux/mod_devicetable.h>
>> +#include <linux/module.h>
>> +#include <linux/property.h>
>> +#include <asm/unaligned.h>
>> +#include <linux/delay.h>
>> +#include <linux/input/mt.h>
>> +#include <linux/input/touchscreen.h>
>> +#include <linux/interrupt.h>
>> +#include <linux/irq.h>
>> +#include <linux/of_gpio.h>
>> +#include <linux/module.h>
>> +#include <linux/pm_runtime.h>
>> +#include <linux/regulator/consumer.h>
>> +
>> +/*
>> + * The TCM oncell interface uses a command byte, which may be followed by additional
>> + * data. The packet format is defined in the tcm_cmd struct.
>> + *
>> + * The following list only defines commands that are used in this driver (and their
>> + * counterparts for context). Vendor reference implementations can be found at
>> + * https://github.com/LineageOS/android_kernel_oneplus_sm8250/tree/ee0a7ee1939ffd53000e42051caf8f0800defb27/drivers/input/touchscreen/synaptics_tcm
>> + */
>> +
>> +/*
>> + * Request information about the chip. We don't send this command explicitly as
>> + * the controller automatically sends this information when starting up.
>> + */
>> +#define TCM_IDENTIFY 0x02
>> +
>> +/* Enable/disable reporting touch inputs */
>> +#define TCM_ENABLE_REPORT 0x05
>> +#define TCM_DISABLE_REPORT 0x06
>> +
>> +/*
>> + * After powering on, we send this to exit the bootloader mode and run the main
>> + * firmware.
>> + */
>> +#define TCM_RUN_APPLICATION_FIRMWARE 0x14
>> +
>> +/*
>> + * Reports information about the vendor provided application firmware. This is
>> + * also used to determine when the firmware has finished booting.
>> + */
>> +#define TCM_GET_APPLICATION_INFO 0x20
>> +
>> +#define MODE_APPLICATION 0x01
>> +
>> +#define APP_STATUS_OK 0x00
>> +#define APP_STATUS_BOOTING 0x01
>> +#define APP_STATUS_UPDATING 0x02
>> +#define APP_STATUS_BAD_APP_CONFIG 0xff
>> +
>> +/* status codes */
>> +#define REPORT_IDLE 0x00
>> +#define REPORT_OK 0x01
>> +#define REPORT_BUSY 0x02
>> +#define REPORT_CONTINUED_READ 0x03
>> +#define REPORT_RECEIVE_BUFFER_OVERFLOW 0x0c
>> +#define REPORT_PREVIOUS_COMMAND_PENDING 0x0d
>> +#define REPORT_NOT_IMPLEMENTED 0x0e
>> +#define REPORT_ERROR 0x0f
>> +
>> +/* report types */
>> +#define REPORT_IDENTIFY 0x10
>> +#define REPORT_TOUCH 0x11
>> +#define REPORT_DELTA 0x12
>> +#define REPORT_RAW 0x13
>> +#define REPORT_DEBUG 0x14
>> +#define REPORT_LOG 0x1d
>> +#define REPORT_TOUCH_HOLD 0x20
>> +#define REPORT_INVALID 0xff
>> +
>> +/* Touch report codes */
>> +#define TOUCH_END 0
>> +#define TOUCH_FOREACH_ACTIVE_OBJECT 1
>> +#define TOUCH_FOREACH_OBJECT 2
>> +#define TOUCH_FOREACH_END 3
>> +#define TOUCH_PAD_TO_NEXT_BYTE 4
>> +#define TOUCH_TIMESTAMP 5
>> +#define TOUCH_OBJECT_N_INDEX 6
>> +#define TOUCH_OBJECT_N_CLASSIFICATION 7
>> +#define TOUCH_OBJECT_N_X_POSITION 8
>> +#define TOUCH_OBJECT_N_Y_POSITION 9
>> +#define TOUCH_OBJECT_N_Z 10
>> +#define TOUCH_OBJECT_N_X_WIDTH 11
>> +#define TOUCH_OBJECT_N_Y_WIDTH 12
>> +#define TOUCH_OBJECT_N_TX_POSITION_TIXELS 13
>> +#define TOUCH_OBJECT_N_RX_POSITION_TIXELS 14
>> +#define TOUCH_0D_BUTTONS_STATE 15
>> +#define TOUCH_GESTURE_DOUBLE_TAP 16
>> +#define TOUCH_FRAME_RATE 17 /* Normally 80hz */
>> +#define TOUCH_POWER_IM 18
>> +#define TOUCH_CID_IM 19
>> +#define TOUCH_RAIL_IM 20
>> +#define TOUCH_CID_VARIANCE_IM 21
>> +#define TOUCH_NSM_FREQUENCY 22
>> +#define TOUCH_NSM_STATE 23
>> +#define TOUCH_NUM_OF_ACTIVE_OBJECTS 23
>> +#define TOUCH_NUM_OF_CPU_CYCLES_USED_SINCE_LAST_FRAME 24
>> +#define TOUCH_TUNING_GAUSSIAN_WIDTHS 0x80
>> +#define TOUCH_TUNING_SMALL_OBJECT_PARAMS 0x81
>> +#define TOUCH_TUNING_0D_BUTTONS_VARIANCE 0x82
>> +#define TOUCH_REPORT_GESTURE_SWIPE 193
>> +#define TOUCH_REPORT_GESTURE_CIRCLE 194
>> +#define TOUCH_REPORT_GESTURE_UNICODE 195
>> +#define TOUCH_REPORT_GESTURE_VEE 196
>> +#define TOUCH_REPORT_GESTURE_TRIANGLE 197
>> +#define TOUCH_REPORT_GESTURE_INFO 198
>> +#define TOUCH_REPORT_GESTURE_COORDINATE 199
>> +#define TOUCH_REPORT_CUSTOMER_GRIP_INFO 203
> 
> I don't think all these are used. Also, could we align values on the
> same column please?

Yes will do
> 
>> +
>> +struct tcm_message_header {
>> +	u8 marker;
>> +	u8 code;
>> +} __packed;
> 
> I don't think this needs to be packed.

Ack
> 
>> +
>> +/* header + 2 bytes (which are length of data depending on report code) */
>> +#define REPORT_PEAK_LEN (sizeof(struct tcm_message_header) + 2)
>> +
>> +struct tcm_cmd {
>> +	u8 cmd;
>> +	u16 length;
>> +	u8 data[];
>> +};
>> +
>> +struct tcm_identification {
>> +	struct tcm_message_header header;
>> +	u16 length;
>> +	u8 version;
>> +	u8 mode;
>> +	char part_number[16];
>> +	u8 build_id[4];
>> +	u8 max_write_size[2];
>> +} __packed;
>> +
>> +struct tcm_app_info {
>> +	struct tcm_message_header header;
>> +	u16 length;
> 
> On-wire data needs to be annotated with proper endiannes (__le16 or
> __be16) and converted to CPU-endianness before use.

Ah, ok, will do.
> 
>> +	u8 version[2];
>> +	u16 status;
>> +	u8 static_config_size[2];
> 
> Should this be __le16 or __be16.
> 
>> +	u8 dynamic_config_size[2];
>> +	u8 app_config_start_write_block[2];
>> +	u8 app_config_size[2];
>> +	u8 max_touch_report_config_size[2];
>> +	u8 max_touch_report_payload_size[2];
>> +	char customer_config_id[16];
>> +	u16 max_x;
>> +	u16 max_y;
>> +	u8 max_objects[2];
>> +	u8 num_of_buttons[2];
>> +	u8 num_of_image_rows[2];
>> +	u8 num_of_image_cols[2];
>> +	u8 has_hybrid_data[2];
>> +} __packed;
>> +
>> +struct tcm_report_config_prop {
>> +	u8 id; /* TOUCH_OBJECT_* */
>> +	u8 bits; /* Size of the field in bits */
>> +};
>> +
>> +struct tcm_report_config_entry {
>> +	u8 foreach; /* TOUCH_FOREACH_* (and maybe other things?) */
>> +	int n_props;
>> +	const struct tcm_report_config_prop *props;
>> +};
>> +
>> +struct tcm_report_config {
>> +	int n_entries;
>> +	const struct tcm_report_config_entry *entries;
>> +};
>> +
>> +struct tcm_data {
>> +	struct i2c_client *client;
>> +	struct regmap *regmap;
>> +	struct input_dev *input;
>> +	struct touchscreen_properties prop;
>> +	struct gpio_desc *reset_gpio;
>> +	struct completion response;
>> +	struct regulator_bulk_data supplies[2];
>> +
>> +	/* annoying state */
>> +	u16 buf_size;
>> +	char buf[256];
>> +};
>> +
>> +static int tcm_send_cmd(struct tcm_data *tcm, struct tcm_cmd *cmd)
>> +{
>> +	struct i2c_client *client = tcm->client;
>> +	struct i2c_msg msg;
>> +	int ret;
>> +
>> +	dev_dbg(&client->dev, "sending command %#x\n", cmd->cmd);
>> +
>> +	msg.addr = client->addr;
>> +	msg.flags = 0;
>> +	msg.len = 1 + cmd->length;
>> +	msg.buf = (u8 *)cmd;
>> +
>> +	ret = i2c_transfer(client->adapter, &msg, 1);
>> +	if (ret == 1)
>> +		return 0;
>> +	else if (ret < 0)
>> +		return ret;
>> +	else
>> +		return -EIO;
>> +}
>> +
>> +static int tcm_send_cmd_noargs(struct tcm_data *tcm, u8 cmd)
>> +{
>> +	struct tcm_cmd c = {
>> +		.cmd = cmd,
>> +		.length = 0,
>> +	};
>> +
>> +	return tcm_send_cmd(tcm, &c);
>> +}
>> +
>> +static int tcm_recv_report(struct tcm_data *tcm,
>> +			   void *buf, size_t length)
>> +{
>> +	struct i2c_client *client = tcm->client;
>> +	struct i2c_msg msg;
>> +	int ret;
>> +
>> +	msg.addr = client->addr;
>> +	msg.flags = I2C_M_RD;
>> +	msg.len = length;
>> +	msg.buf = buf;
>> +
>> +	ret = i2c_transfer(client->adapter, &msg, 1);
>> +	if (ret == 1)
>> +		return 0;
>> +	else if (ret < 0)
>> +		return ret;
>> +	else
>> +		return -EIO;
>> +}
>> +
>> +static int tcm_read_message(struct tcm_data *tcm, u8 cmd, void *buf, size_t length)
>> +{
>> +	int ret;
>> +
>> +	reinit_completion(&tcm->response);
>> +	ret = tcm_send_cmd_noargs(tcm, cmd);
>> +	if (ret)
>> +		return ret;
>> +
>> +	ret = wait_for_completion_timeout(&tcm->response, msecs_to_jiffies(1000));
>> +	if (ret == 0)
>> +		return -ETIMEDOUT;
>> +
>> +	if (buf) {
>> +		if (length > tcm->buf_size) {
>> +			dev_warn(&tcm->client->dev, "expected %zu bytes, got %u\n",
>> +				 length, tcm->buf_size);
>> +		}
>> +		length = min(tcm->buf_size, length);
>> +		memcpy(buf, tcm->buf, length);
>> +	}
>> +
>> +	return 0;
>> +}
>> +
>> +static void tcm_power_off(void *data)
>> +{
>> +	struct tcm_data *tcm = data;
>> +
>> +	disable_irq(tcm->client->irq);
>> +	regulator_bulk_disable(ARRAY_SIZE(tcm->supplies), tcm->supplies);
>> +}
>> +
>> +static int tcm_input_open(struct input_dev *dev)
>> +{
>> +	struct tcm_data *tcm = input_get_drvdata(dev);
>> +
>> +	return i2c_smbus_write_byte(tcm->client, TCM_ENABLE_REPORT);
>> +}
>> +
>> +static void tcm_input_close(struct input_dev *dev)
>> +{
>> +	struct tcm_data *tcm = input_get_drvdata(dev);
>> +	int ret;
>> +
>> +	ret = i2c_smbus_write_byte(tcm->client, TCM_DISABLE_REPORT);
>> +	if (ret)
>> +		dev_err(&tcm->client->dev, "failed to turn off sensing\n");
>> +}
>> +
>> +/*
>> + * The default report config looks like this:
>> + *
>> + * a5 01 80 00 11 08 1e 08 0f 01 04 01 06 04 07 04
>> + * 08 0c 09 0c 0a 08 0b 08 0c 08 0d 10 0e 10 03 00
>> + * 00 00
>> + *
>> + * a5 01 80 00 - HEADER + length
>> + *
>> + * 11 08 - TOUCH_FRAME_RATE (8 bits)
>> + * 30 08 - UNKNOWN (8 bits)
>> + * 0f 01 - TOUCH_0D_BUTTONS_STATE (1 bit)
>> + * 04 01 - TOUCH_PAD_TO_NEXT_BYTE (7 bits - padding)
>> + * 06 04 - TOUCH_OBJECT_N_INDEX (4 bits)
>> + * 07 04 - TOUCH_OBJECT_N_CLASSIFICATION (4 bits)
>> + * 08 0c - TOUCH_OBJECT_N_X_POSITION (12 bits)
>> + * 09 0c - TOUCH_OBJECT_N_Y_POSITION (12 bits)
>> + * 0a 08 - TOUCH_OBJECT_N_Z (8 bits)
>> + * 0b 08 - TOUCH_OBJECT_N_X_WIDTH (8 bits)
>> + * 0c 08 - TOUCH_OBJECT_N_Y_WIDTH (8 bits)
>> + * 0d 10 - TOUCH_OBJECT_N_TX_POSITION_TIXELS (16 bits) ??
>> + * 0e 10 - TOUCH_OBJECT_N_RX_POSITION_TIXELS (16 bits) ??
>> + * 03 00 - TOUCH_FOREACH_END (0 bits)
>> + * 00 00 - TOUCH_END (0 bits)
>> + *
>> + * Since we only support this report config, we just hardcode the format below.
>> + * To support additional report configs, we would need to parse the config and
>> + * use it to parse the reports dynamically.
>> + */
>> +
>> +struct tcm_default_report_data {
>> +	u8 fps;
>> +	struct {
>> +		u8 unknown;
>> +		u8 buttons;
>> +		u8 idx : 4;
>> +		u8 classification : 4;
>> +		u16 x : 12;
>> +		u16 y : 12;
> 
> This will be a mess on big endian. Please no bitfields. Use normally
> sized fields (u8, u16, etc) and masks/shifts to get data.

argh! yeah, I'll go do this properly XD
> 
>> +		u8 z;
>> +		u8 width_x;
>> +		u8 width_y;
>> +		u8 tx;
>> +		u8 rx;
>> +	} __packed points[];
>> +} __packed;
>> +
>> +static int tcm_handle_touch_report(struct tcm_data *tcm, char *buf, size_t len)
>> +{
>> +	struct tcm_default_report_data *data;
>> +
>> +	buf += REPORT_PEAK_LEN;
>> +	len -= REPORT_PEAK_LEN;
>> +
>> +	dev_dbg(&tcm->client->dev, "touch report len %zu\n", len);
>> +	if ((len - 1) % 11)
>> +		dev_err(&tcm->client->dev, "invalid touch report length\n");
>> +
>> +	data = (struct tcm_default_report_data *)buf;
>> +
>> +	/* We don't need to report releases because we have INPUT_MT_DROP_UNUSED */
>> +	for (int i = 0; i < (len - 1) / 11; i++) {
>> +		u8 major_width, minor_width;
>> +
>> +		minor_width = data->points[i].width_x;
>> +		major_width = data->points[i].width_y;
>> +
>> +		if (minor_width > major_width)
>> +			swap(major_width, minor_width);
>> +
>> +		dev_dbg(&tcm->client->dev, "touch report: idx %u x %u y %u\n",
>> +			data->points[i].idx, data->points[i].x, data->points[i].y);
>> +		input_mt_slot(tcm->input, data->points[i].idx);
>> +		input_mt_report_slot_state(tcm->input, MT_TOOL_FINGER, true);
>> +
>> +		input_report_abs(tcm->input, ABS_MT_POSITION_X, data->points[i].x);
>> +		input_report_abs(tcm->input, ABS_MT_POSITION_Y, data->points[i].y);
> 
> touchscreen_report_pos(...) instead of the 2 above so that rotation/axis
> swap will be handled properly.

Oh nice, thannks
> 
>> +		input_report_abs(tcm->input, ABS_MT_TOUCH_MAJOR, major_width);
>> +		input_report_abs(tcm->input, ABS_MT_TOUCH_MINOR, minor_width);
>> +		input_report_abs(tcm->input, ABS_MT_PRESSURE, data->points[i].z);
>> +	}
>> +
>> +	input_mt_sync_frame(tcm->input);
>> +	input_sync(tcm->input);
>> +
>> +	return 0;
>> +}
>> +
>> +static irqreturn_t tcm_report_irq(int irq, void *data)
>> +{
>> +	struct tcm_data *tcm = data;
>> +	struct tcm_message_header *header;
>> +	char buf[256];
>> +	u16 len;
>> +	int ret;
>> +
>> +	header = (struct tcm_message_header *)buf;
>> +	ret = tcm_recv_report(tcm, buf, sizeof(buf));
>> +	if (ret) {
>> +		dev_err(&tcm->client->dev, "failed to read report: %d\n", ret);
>> +		return IRQ_HANDLED;
>> +	}
>> +
>> +	switch (header->code) {
>> +	case REPORT_OK:
>> +	case REPORT_IDENTIFY:
>> +	case REPORT_TOUCH:
>> +	case REPORT_DELTA:
>> +	case REPORT_RAW:
>> +	case REPORT_DEBUG:
>> +	case REPORT_TOUCH_HOLD:
>> +		break;
>> +	default:
>> +		dev_dbg(&tcm->client->dev, "Ignoring report %#x\n", header->code);
>> +		return IRQ_HANDLED;
>> +	}
>> +
>> +	/* Not present for REPORT_CONTINUED_READ */
>> +	len = get_unaligned_le16(buf + sizeof(*header));
>> +
>> +	dev_dbg(&tcm->client->dev, "report %#x len %u\n", header->code, len);
>> +	print_hex_dump_bytes("report: ", DUMP_PREFIX_OFFSET, buf,
>> +			     min(sizeof(buf), len + sizeof(*header)));
>> +
>> +	if (len > sizeof(buf) - sizeof(*header)) {
>> +		dev_err(&tcm->client->dev, "report too long\n");
>> +		return IRQ_HANDLED;
>> +	}
>> +
>> +	/* Check if this is a read response or an indication. For indications
>> +	 * (user touched the screen) we just parse the report directly.
>> +	 */
>> +	if (completion_done(&tcm->response) && header->code == REPORT_TOUCH) {
>> +		tcm_handle_touch_report(tcm, buf, len + sizeof(*header));
>> +		return IRQ_HANDLED;
>> +	}
>> +
>> +	tcm->buf_size = len + sizeof(*header);
>> +	memcpy(tcm->buf, buf, len + sizeof(*header));
>> +	complete(&tcm->response);
>> +
>> +	return IRQ_HANDLED;
>> +}
>> +
>> +static int tcm_hw_init(struct tcm_data *tcm, u16 *max_x, u16 *max_y)
>> +{
>> +	int ret;
>> +	struct tcm_identification id = { 0 };
>> +	struct tcm_app_info app_info = { 0 };
>> +
>> +	/*
>> +	 * Tell the firmware to start up. After starting it sends an IDENTIFY report, which
>> +	 * we treat like a response to this message even though it's technically a new report.
>> +	 */
>> +	ret = tcm_read_message(tcm, TCM_RUN_APPLICATION_FIRMWARE, &id, sizeof(id));
>> +	if (ret) {
>> +		dev_err(&tcm->client->dev, "failed to identify device: %d\n", ret);
>> +		return ret;
>> +	}
>> +
>> +	dev_dbg(&tcm->client->dev, "Synaptics TCM %s v%d mode %d\n",
>> +		id.part_number, id.version, id.mode);
>> +	if (id.mode != MODE_APPLICATION) {
>> +		/* We don't support firmware updates or anything else */
>> +		dev_err(&tcm->client->dev, "Device is not in application mode\n");
>> +		return -ENODEV;
>> +	}
>> +
>> +	do {
>> +		msleep(20);
>> +		ret = tcm_read_message(tcm, TCM_GET_APPLICATION_INFO, &app_info, sizeof(app_info));
>> +		if (ret) {
>> +			dev_err(&tcm->client->dev, "failed to get application info: %d\n", ret);
>> +			return ret;
>> +		}
>> +	} while (app_info.status == APP_STATUS_BOOTING || app_info.status == APP_STATUS_UPDATING);
>> +
>> +	dev_dbg(&tcm->client->dev, "Application firmware v%d.%d (customer '%s') status %d\n",
>> +		 app_info.version[0], app_info.version[1], app_info.customer_config_id,
>> +		 app_info.status);
>> +
>> +	*max_x = app_info.max_x;
>> +	*max_y = app_info.max_y;
>> +
>> +	return 0;
>> +}
>> +
>> +static int tcm_power_on(struct tcm_data *tcm)
>> +{
>> +	int ret;
>> +
>> +	ret = regulator_bulk_enable(ARRAY_SIZE(tcm->supplies),
>> +				    tcm->supplies);
>> +	if (ret)
>> +		return ret;
>> +
>> +	gpiod_set_value(tcm->reset_gpio, false);
>> +	usleep_range(10000, 11000);
>> +	gpiod_set_value(tcm->reset_gpio, true);
> 
> Here you have reset GPIO asserted, which means that the controller will
> stay in reset state indefinitely.
> 
> gpiod API operates on logical states (active/inactive) and performs
> conversion to physical state (LOW/HIGH) internally.
> 
> Also we should use gpiod_set_value_cansleep() unless we are in atomic
> context.

Ahh yes, this is all inverted, thanks.
> 
>> +	usleep_range(80000, 81000);
>> +
>> +	return 0;
>> +}
>> +
>> +static int tcm_probe(struct i2c_client *client)
>> +{
>> +	struct tcm_data *tcm;
>> +	struct tcm_report_config __maybe_unused report_config;
> 
> It is definitely not used, there no "maybe" about it.
> 
>> +	u16 max_x, max_y;
>> +	int ret;
> 
> Call this and similar variables holding error code "error" please.
> 
>> +
>> +	if (!i2c_check_functionality(client->adapter, I2C_FUNC_I2C |
>> +						I2C_FUNC_SMBUS_BYTE_DATA |
>> +						I2C_FUNC_SMBUS_I2C_BLOCK))
>> +		return -ENODEV;
>> +
>> +	tcm = devm_kzalloc(&client->dev, sizeof(struct tcm_data), GFP_KERNEL);
> 
> sizeof(*tcm)
> 
>> +	if (!tcm)
>> +		return -ENOMEM;
>> +
>> +	i2c_set_clientdata(client, tcm);
>> +	tcm->client = client;
>> +
>> +	init_completion(&tcm->response);
>> +
>> +	tcm->supplies[0].supply = "vdd";
>> +	tcm->supplies[1].supply = "vcc";
>> +	ret = devm_regulator_bulk_get(&client->dev, ARRAY_SIZE(tcm->supplies),
>> +				      tcm->supplies);
>> +	if (ret)
>> +		return ret;
>> +
>> +	tcm->reset_gpio = devm_gpiod_get(&client->dev, "reset", GPIOD_OUT_HIGH);
> 
> Do all designs require GPIO line be specified? Can it be made optional?

I have no idea, no access to docs or much prior art. Maybe best to wait 
until someone shows up with a device that doesn't use it?
> 
>> +
>> +	ret = devm_add_action_or_reset(&client->dev, tcm_power_off,
>> +				       tcm);
>> +	if (ret)
>> +		return ret;
>> +
>> +	ret = tcm_power_on(tcm);
>> +	if (ret)
>> +		return ret;
>> +
>> +	ret = devm_request_threaded_irq(&client->dev, client->irq, NULL,
>> +					tcm_report_irq,
>> +					IRQF_TRIGGER_LOW | IRQF_ONESHOT,
> 
> Please do not hardcode trigger (besides IRQF_ONESHOT), let platform
> decide.
> 

Right, this is a leftover from downstream code I guess, will fix.

> Also interrupt is hot there, are you sure we will not try to report
> input events for not-yet-allocated input device if someone is thouching
> the device at this point?

Hmm, I guess this could happen after tcm_hw_init() and before everything 
is initialised, but it won't happen until we send the 
TCM_RUN_APPLICATION_FIRMWARE command. I think deferring the 
tcm_hw_init() call until after we set things up should be acceptable here.
> 
>> +					"synaptics_tcm_report", tcm);
>> +	if (ret < 0)
>> +		return ret;
>> +
>> +	ret = tcm_hw_init(tcm, &max_x, &max_y);
>> +	if (ret) {
>> +		dev_err(&client->dev, "failed to initialize hardware\n");
>> +		return ret;
>> +	}
>> +
>> +	tcm->input = devm_input_allocate_device(&client->dev);
>> +	if (!tcm->input)
>> +		return -ENOMEM;
>> +
>> +	tcm->input->name = "Synaptics TCM Oncell Touchscreen";
>> +	tcm->input->id.bustype = BUS_I2C;
>> +	tcm->input->open = tcm_input_open;
>> +	tcm->input->close = tcm_input_close;
>> +
>> +	input_set_abs_params(tcm->input, ABS_MT_POSITION_X, 0, max_x, 0, 0);
>> +	input_set_abs_params(tcm->input, ABS_MT_POSITION_Y, 0, max_y, 0, 0);
>> +	input_set_abs_params(tcm->input, ABS_MT_TOUCH_MAJOR, 0, 255, 0, 0);
>> +	input_set_abs_params(tcm->input, ABS_MT_TOUCH_MINOR, 0, 255, 0, 0);
>> +	input_set_abs_params(tcm->input, ABS_MT_PRESSURE, 0, 255, 0, 0);
>> +
>> +	touchscreen_parse_properties(tcm->input, true, &tcm->prop);
>> +
>> +	ret = input_mt_init_slots(tcm->input, 10, INPUT_MT_DIRECT | INPUT_MT_DROP_UNUSED);
>> +	if (ret)
>> +		return ret;
>> +
>> +	input_set_drvdata(tcm->input, tcm);
>> +
>> +	ret = input_register_device(tcm->input);
>> +	if (ret)
>> +		return ret;
>> +
>> +	return 0;
>> +}
>> +
>> +static const struct of_device_id syna_driver_ids[] = {
>> +	{
>> +		.compatible = "syna,s3908",
>> +	},
>> +	{}
>> +};
>> +MODULE_DEVICE_TABLE(of, syna_driver_ids);
>> +
>> +static const struct i2c_device_id syna_i2c_ids[] = {
>> +	{ "synaptics-tcm", 0 },
>> +	{ }
>> +};
>> +
>> +MODULE_DEVICE_TABLE(i2c, syna_i2c_ids);
>> +
>> +static struct i2c_driver syna_i2c_driver = {
>> +	.probe	= tcm_probe,
>> +	.id_table	= syna_i2c_ids,
>> +	.driver	= {
>> +	.name	= "synaptics-tcm",
>> +	.of_match_table = syna_driver_ids,
>> +	},
>> +};
>> +
>> +module_i2c_driver(syna_i2c_driver);
>> +
>> +MODULE_LICENSE("GPL");
>> +MODULE_AUTHOR("Frieder Hannenheim <frieder.hannenheim@proton.me>");
>> +MODULE_AUTHOR("Caleb Connolly <caleb@postmarketos.org>");
>> +MODULE_DESCRIPTION("A driver for Synaptics TCM Oncell Touchpanels");
>> +
>>
>> -- 
>> 2.45.0
>>
>
diff mbox series

Patch

diff --git a/MAINTAINERS b/MAINTAINERS
index 2b9cfbf92d7a..db589c841d8c 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -21826,8 +21826,15 @@  M:	Icenowy Zheng <icenowy@aosc.io>
 S:	Maintained
 F:	Documentation/devicetree/bindings/regulator/silergy,sy8106a.yaml
 F:	drivers/regulator/sy8106a-regulator.c
 
+SYNAPTICS TCM ONCELL TOUCHSCREEN DRIVER
+M:	Caleb Connolly <caleb@postmarketos.org>
+L:	linux-input@vger.kernel.org
+S:	Maintained
+F:	Documentation/devicetree/bindings/input/touchscreen/syna,tcm-oncell.yaml
+F:	drivers/input/touchscreen/synaptics_tcm_oncell.c
+
 SYNC FILE FRAMEWORK
 M:	Sumit Semwal <sumit.semwal@linaro.org>
 R:	Gustavo Padovan <gustavo@padovan.org>
 L:	linux-media@vger.kernel.org
diff --git a/drivers/input/touchscreen/Kconfig b/drivers/input/touchscreen/Kconfig
index c821fe3ee794..43c4fd80601c 100644
--- a/drivers/input/touchscreen/Kconfig
+++ b/drivers/input/touchscreen/Kconfig
@@ -531,8 +531,19 @@  config TOUCHSCREEN_S6SY761
 
 	  To compile this driver as module, choose M here: the
 	  module will be called s6sy761.
 
+config TOUCHSCREEN_SYNAPTICS_TCM_ONCELL
+	tristate "Synaptics TCM Oncell Touchscreen driver"
+	depends on I2C
+	help
+	  Say Y if you have the Synaptics S3908 TCM Oncell
+
+	  If unsure, say N
+
+	  To compile this driver as module, choose M here: the
+	  module will be called synaptics_tcm_oncell.
+
 config TOUCHSCREEN_GUNZE
 	tristate "Gunze AHL-51S touchscreen"
 	select SERIO
 	help
diff --git a/drivers/input/touchscreen/Makefile b/drivers/input/touchscreen/Makefile
index a81cb5aa21a5..6a2b85050c3a 100644
--- a/drivers/input/touchscreen/Makefile
+++ b/drivers/input/touchscreen/Makefile
@@ -88,8 +88,9 @@  obj-$(CONFIG_TOUCHSCREEN_STMFTS)	+= stmfts.o
 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_ONCELL)	+= synaptics_tcm_oncell.o
 obj-$(CONFIG_TOUCHSCREEN_TI_AM335X_TSC)	+= ti_am335x_tsc.o
 obj-$(CONFIG_TOUCHSCREEN_TOUCHIT213)	+= touchit213.o
 obj-$(CONFIG_TOUCHSCREEN_TOUCHRIGHT)	+= touchright.o
 obj-$(CONFIG_TOUCHSCREEN_TOUCHWIN)	+= touchwin.o
diff --git a/drivers/input/touchscreen/synaptics_tcm_oncell.c b/drivers/input/touchscreen/synaptics_tcm_oncell.c
new file mode 100644
index 000000000000..c1ba9a3a93c0
--- /dev/null
+++ b/drivers/input/touchscreen/synaptics_tcm_oncell.c
@@ -0,0 +1,617 @@ 
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ *  Driver for Synaptics TCM Oncell Touchscreens
+ *
+ *  Copyright (c) 2024 Frieder Hannenheim <frieder.hannenheim@proton.me>
+ *  Copyright (c) 2024 Caleb Connolly <caleb@postmarketos.org>
+ */
+
+#include <linux/i2c.h>
+#include <linux/input.h>
+#include <linux/input/touchscreen.h>
+#include <linux/mod_devicetable.h>
+#include <linux/module.h>
+#include <linux/property.h>
+#include <asm/unaligned.h>
+#include <linux/delay.h>
+#include <linux/input/mt.h>
+#include <linux/input/touchscreen.h>
+#include <linux/interrupt.h>
+#include <linux/irq.h>
+#include <linux/of_gpio.h>
+#include <linux/module.h>
+#include <linux/pm_runtime.h>
+#include <linux/regulator/consumer.h>
+
+/*
+ * The TCM oncell interface uses a command byte, which may be followed by additional
+ * data. The packet format is defined in the tcm_cmd struct.
+ *
+ * The following list only defines commands that are used in this driver (and their
+ * counterparts for context). Vendor reference implementations can be found at
+ * https://github.com/LineageOS/android_kernel_oneplus_sm8250/tree/ee0a7ee1939ffd53000e42051caf8f0800defb27/drivers/input/touchscreen/synaptics_tcm
+ */
+
+/*
+ * Request information about the chip. We don't send this command explicitly as
+ * the controller automatically sends this information when starting up.
+ */
+#define TCM_IDENTIFY 0x02
+
+/* Enable/disable reporting touch inputs */
+#define TCM_ENABLE_REPORT 0x05
+#define TCM_DISABLE_REPORT 0x06
+
+/*
+ * After powering on, we send this to exit the bootloader mode and run the main
+ * firmware.
+ */
+#define TCM_RUN_APPLICATION_FIRMWARE 0x14
+
+/*
+ * Reports information about the vendor provided application firmware. This is
+ * also used to determine when the firmware has finished booting.
+ */
+#define TCM_GET_APPLICATION_INFO 0x20
+
+#define MODE_APPLICATION 0x01
+
+#define APP_STATUS_OK 0x00
+#define APP_STATUS_BOOTING 0x01
+#define APP_STATUS_UPDATING 0x02
+#define APP_STATUS_BAD_APP_CONFIG 0xff
+
+/* status codes */
+#define REPORT_IDLE 0x00
+#define REPORT_OK 0x01
+#define REPORT_BUSY 0x02
+#define REPORT_CONTINUED_READ 0x03
+#define REPORT_RECEIVE_BUFFER_OVERFLOW 0x0c
+#define REPORT_PREVIOUS_COMMAND_PENDING 0x0d
+#define REPORT_NOT_IMPLEMENTED 0x0e
+#define REPORT_ERROR 0x0f
+
+/* report types */
+#define REPORT_IDENTIFY 0x10
+#define REPORT_TOUCH 0x11
+#define REPORT_DELTA 0x12
+#define REPORT_RAW 0x13
+#define REPORT_DEBUG 0x14
+#define REPORT_LOG 0x1d
+#define REPORT_TOUCH_HOLD 0x20
+#define REPORT_INVALID 0xff
+
+/* Touch report codes */
+#define TOUCH_END 0
+#define TOUCH_FOREACH_ACTIVE_OBJECT 1
+#define TOUCH_FOREACH_OBJECT 2
+#define TOUCH_FOREACH_END 3
+#define TOUCH_PAD_TO_NEXT_BYTE 4
+#define TOUCH_TIMESTAMP 5
+#define TOUCH_OBJECT_N_INDEX 6
+#define TOUCH_OBJECT_N_CLASSIFICATION 7
+#define TOUCH_OBJECT_N_X_POSITION 8
+#define TOUCH_OBJECT_N_Y_POSITION 9
+#define TOUCH_OBJECT_N_Z 10
+#define TOUCH_OBJECT_N_X_WIDTH 11
+#define TOUCH_OBJECT_N_Y_WIDTH 12
+#define TOUCH_OBJECT_N_TX_POSITION_TIXELS 13
+#define TOUCH_OBJECT_N_RX_POSITION_TIXELS 14
+#define TOUCH_0D_BUTTONS_STATE 15
+#define TOUCH_GESTURE_DOUBLE_TAP 16
+#define TOUCH_FRAME_RATE 17 /* Normally 80hz */
+#define TOUCH_POWER_IM 18
+#define TOUCH_CID_IM 19
+#define TOUCH_RAIL_IM 20
+#define TOUCH_CID_VARIANCE_IM 21
+#define TOUCH_NSM_FREQUENCY 22
+#define TOUCH_NSM_STATE 23
+#define TOUCH_NUM_OF_ACTIVE_OBJECTS 23
+#define TOUCH_NUM_OF_CPU_CYCLES_USED_SINCE_LAST_FRAME 24
+#define TOUCH_TUNING_GAUSSIAN_WIDTHS 0x80
+#define TOUCH_TUNING_SMALL_OBJECT_PARAMS 0x81
+#define TOUCH_TUNING_0D_BUTTONS_VARIANCE 0x82
+#define TOUCH_REPORT_GESTURE_SWIPE 193
+#define TOUCH_REPORT_GESTURE_CIRCLE 194
+#define TOUCH_REPORT_GESTURE_UNICODE 195
+#define TOUCH_REPORT_GESTURE_VEE 196
+#define TOUCH_REPORT_GESTURE_TRIANGLE 197
+#define TOUCH_REPORT_GESTURE_INFO 198
+#define TOUCH_REPORT_GESTURE_COORDINATE 199
+#define TOUCH_REPORT_CUSTOMER_GRIP_INFO 203
+
+struct tcm_message_header {
+	u8 marker;
+	u8 code;
+} __packed;
+
+/* header + 2 bytes (which are length of data depending on report code) */
+#define REPORT_PEAK_LEN (sizeof(struct tcm_message_header) + 2)
+
+struct tcm_cmd {
+	u8 cmd;
+	u16 length;
+	u8 data[];
+};
+
+struct tcm_identification {
+	struct tcm_message_header header;
+	u16 length;
+	u8 version;
+	u8 mode;
+	char part_number[16];
+	u8 build_id[4];
+	u8 max_write_size[2];
+} __packed;
+
+struct tcm_app_info {
+	struct tcm_message_header header;
+	u16 length;
+	u8 version[2];
+	u16 status;
+	u8 static_config_size[2];
+	u8 dynamic_config_size[2];
+	u8 app_config_start_write_block[2];
+	u8 app_config_size[2];
+	u8 max_touch_report_config_size[2];
+	u8 max_touch_report_payload_size[2];
+	char customer_config_id[16];
+	u16 max_x;
+	u16 max_y;
+	u8 max_objects[2];
+	u8 num_of_buttons[2];
+	u8 num_of_image_rows[2];
+	u8 num_of_image_cols[2];
+	u8 has_hybrid_data[2];
+} __packed;
+
+struct tcm_report_config_prop {
+	u8 id; /* TOUCH_OBJECT_* */
+	u8 bits; /* Size of the field in bits */
+};
+
+struct tcm_report_config_entry {
+	u8 foreach; /* TOUCH_FOREACH_* (and maybe other things?) */
+	int n_props;
+	const struct tcm_report_config_prop *props;
+};
+
+struct tcm_report_config {
+	int n_entries;
+	const struct tcm_report_config_entry *entries;
+};
+
+struct tcm_data {
+	struct i2c_client *client;
+	struct regmap *regmap;
+	struct input_dev *input;
+	struct touchscreen_properties prop;
+	struct gpio_desc *reset_gpio;
+	struct completion response;
+	struct regulator_bulk_data supplies[2];
+
+	/* annoying state */
+	u16 buf_size;
+	char buf[256];
+};
+
+static int tcm_send_cmd(struct tcm_data *tcm, struct tcm_cmd *cmd)
+{
+	struct i2c_client *client = tcm->client;
+	struct i2c_msg msg;
+	int ret;
+
+	dev_dbg(&client->dev, "sending command %#x\n", cmd->cmd);
+
+	msg.addr = client->addr;
+	msg.flags = 0;
+	msg.len = 1 + cmd->length;
+	msg.buf = (u8 *)cmd;
+
+	ret = i2c_transfer(client->adapter, &msg, 1);
+	if (ret == 1)
+		return 0;
+	else if (ret < 0)
+		return ret;
+	else
+		return -EIO;
+}
+
+static int tcm_send_cmd_noargs(struct tcm_data *tcm, u8 cmd)
+{
+	struct tcm_cmd c = {
+		.cmd = cmd,
+		.length = 0,
+	};
+
+	return tcm_send_cmd(tcm, &c);
+}
+
+static int tcm_recv_report(struct tcm_data *tcm,
+			   void *buf, size_t length)
+{
+	struct i2c_client *client = tcm->client;
+	struct i2c_msg msg;
+	int ret;
+
+	msg.addr = client->addr;
+	msg.flags = I2C_M_RD;
+	msg.len = length;
+	msg.buf = buf;
+
+	ret = i2c_transfer(client->adapter, &msg, 1);
+	if (ret == 1)
+		return 0;
+	else if (ret < 0)
+		return ret;
+	else
+		return -EIO;
+}
+
+static int tcm_read_message(struct tcm_data *tcm, u8 cmd, void *buf, size_t length)
+{
+	int ret;
+
+	reinit_completion(&tcm->response);
+	ret = tcm_send_cmd_noargs(tcm, cmd);
+	if (ret)
+		return ret;
+
+	ret = wait_for_completion_timeout(&tcm->response, msecs_to_jiffies(1000));
+	if (ret == 0)
+		return -ETIMEDOUT;
+
+	if (buf) {
+		if (length > tcm->buf_size) {
+			dev_warn(&tcm->client->dev, "expected %zu bytes, got %u\n",
+				 length, tcm->buf_size);
+		}
+		length = min(tcm->buf_size, length);
+		memcpy(buf, tcm->buf, length);
+	}
+
+	return 0;
+}
+
+static void tcm_power_off(void *data)
+{
+	struct tcm_data *tcm = data;
+
+	disable_irq(tcm->client->irq);
+	regulator_bulk_disable(ARRAY_SIZE(tcm->supplies), tcm->supplies);
+}
+
+static int tcm_input_open(struct input_dev *dev)
+{
+	struct tcm_data *tcm = input_get_drvdata(dev);
+
+	return i2c_smbus_write_byte(tcm->client, TCM_ENABLE_REPORT);
+}
+
+static void tcm_input_close(struct input_dev *dev)
+{
+	struct tcm_data *tcm = input_get_drvdata(dev);
+	int ret;
+
+	ret = i2c_smbus_write_byte(tcm->client, TCM_DISABLE_REPORT);
+	if (ret)
+		dev_err(&tcm->client->dev, "failed to turn off sensing\n");
+}
+
+/*
+ * The default report config looks like this:
+ *
+ * a5 01 80 00 11 08 1e 08 0f 01 04 01 06 04 07 04
+ * 08 0c 09 0c 0a 08 0b 08 0c 08 0d 10 0e 10 03 00
+ * 00 00
+ *
+ * a5 01 80 00 - HEADER + length
+ *
+ * 11 08 - TOUCH_FRAME_RATE (8 bits)
+ * 30 08 - UNKNOWN (8 bits)
+ * 0f 01 - TOUCH_0D_BUTTONS_STATE (1 bit)
+ * 04 01 - TOUCH_PAD_TO_NEXT_BYTE (7 bits - padding)
+ * 06 04 - TOUCH_OBJECT_N_INDEX (4 bits)
+ * 07 04 - TOUCH_OBJECT_N_CLASSIFICATION (4 bits)
+ * 08 0c - TOUCH_OBJECT_N_X_POSITION (12 bits)
+ * 09 0c - TOUCH_OBJECT_N_Y_POSITION (12 bits)
+ * 0a 08 - TOUCH_OBJECT_N_Z (8 bits)
+ * 0b 08 - TOUCH_OBJECT_N_X_WIDTH (8 bits)
+ * 0c 08 - TOUCH_OBJECT_N_Y_WIDTH (8 bits)
+ * 0d 10 - TOUCH_OBJECT_N_TX_POSITION_TIXELS (16 bits) ??
+ * 0e 10 - TOUCH_OBJECT_N_RX_POSITION_TIXELS (16 bits) ??
+ * 03 00 - TOUCH_FOREACH_END (0 bits)
+ * 00 00 - TOUCH_END (0 bits)
+ *
+ * Since we only support this report config, we just hardcode the format below.
+ * To support additional report configs, we would need to parse the config and
+ * use it to parse the reports dynamically.
+ */
+
+struct tcm_default_report_data {
+	u8 fps;
+	struct {
+		u8 unknown;
+		u8 buttons;
+		u8 idx : 4;
+		u8 classification : 4;
+		u16 x : 12;
+		u16 y : 12;
+		u8 z;
+		u8 width_x;
+		u8 width_y;
+		u8 tx;
+		u8 rx;
+	} __packed points[];
+} __packed;
+
+static int tcm_handle_touch_report(struct tcm_data *tcm, char *buf, size_t len)
+{
+	struct tcm_default_report_data *data;
+
+	buf += REPORT_PEAK_LEN;
+	len -= REPORT_PEAK_LEN;
+
+	dev_dbg(&tcm->client->dev, "touch report len %zu\n", len);
+	if ((len - 1) % 11)
+		dev_err(&tcm->client->dev, "invalid touch report length\n");
+
+	data = (struct tcm_default_report_data *)buf;
+
+	/* We don't need to report releases because we have INPUT_MT_DROP_UNUSED */
+	for (int i = 0; i < (len - 1) / 11; i++) {
+		u8 major_width, minor_width;
+
+		minor_width = data->points[i].width_x;
+		major_width = data->points[i].width_y;
+
+		if (minor_width > major_width)
+			swap(major_width, minor_width);
+
+		dev_dbg(&tcm->client->dev, "touch report: idx %u x %u y %u\n",
+			data->points[i].idx, data->points[i].x, data->points[i].y);
+		input_mt_slot(tcm->input, data->points[i].idx);
+		input_mt_report_slot_state(tcm->input, MT_TOOL_FINGER, true);
+
+		input_report_abs(tcm->input, ABS_MT_POSITION_X, data->points[i].x);
+		input_report_abs(tcm->input, ABS_MT_POSITION_Y, data->points[i].y);
+		input_report_abs(tcm->input, ABS_MT_TOUCH_MAJOR, major_width);
+		input_report_abs(tcm->input, ABS_MT_TOUCH_MINOR, minor_width);
+		input_report_abs(tcm->input, ABS_MT_PRESSURE, data->points[i].z);
+	}
+
+	input_mt_sync_frame(tcm->input);
+	input_sync(tcm->input);
+
+	return 0;
+}
+
+static irqreturn_t tcm_report_irq(int irq, void *data)
+{
+	struct tcm_data *tcm = data;
+	struct tcm_message_header *header;
+	char buf[256];
+	u16 len;
+	int ret;
+
+	header = (struct tcm_message_header *)buf;
+	ret = tcm_recv_report(tcm, buf, sizeof(buf));
+	if (ret) {
+		dev_err(&tcm->client->dev, "failed to read report: %d\n", ret);
+		return IRQ_HANDLED;
+	}
+
+	switch (header->code) {
+	case REPORT_OK:
+	case REPORT_IDENTIFY:
+	case REPORT_TOUCH:
+	case REPORT_DELTA:
+	case REPORT_RAW:
+	case REPORT_DEBUG:
+	case REPORT_TOUCH_HOLD:
+		break;
+	default:
+		dev_dbg(&tcm->client->dev, "Ignoring report %#x\n", header->code);
+		return IRQ_HANDLED;
+	}
+
+	/* Not present for REPORT_CONTINUED_READ */
+	len = get_unaligned_le16(buf + sizeof(*header));
+
+	dev_dbg(&tcm->client->dev, "report %#x len %u\n", header->code, len);
+	print_hex_dump_bytes("report: ", DUMP_PREFIX_OFFSET, buf,
+			     min(sizeof(buf), len + sizeof(*header)));
+
+	if (len > sizeof(buf) - sizeof(*header)) {
+		dev_err(&tcm->client->dev, "report too long\n");
+		return IRQ_HANDLED;
+	}
+
+	/* Check if this is a read response or an indication. For indications
+	 * (user touched the screen) we just parse the report directly.
+	 */
+	if (completion_done(&tcm->response) && header->code == REPORT_TOUCH) {
+		tcm_handle_touch_report(tcm, buf, len + sizeof(*header));
+		return IRQ_HANDLED;
+	}
+
+	tcm->buf_size = len + sizeof(*header);
+	memcpy(tcm->buf, buf, len + sizeof(*header));
+	complete(&tcm->response);
+
+	return IRQ_HANDLED;
+}
+
+static int tcm_hw_init(struct tcm_data *tcm, u16 *max_x, u16 *max_y)
+{
+	int ret;
+	struct tcm_identification id = { 0 };
+	struct tcm_app_info app_info = { 0 };
+
+	/*
+	 * Tell the firmware to start up. After starting it sends an IDENTIFY report, which
+	 * we treat like a response to this message even though it's technically a new report.
+	 */
+	ret = tcm_read_message(tcm, TCM_RUN_APPLICATION_FIRMWARE, &id, sizeof(id));
+	if (ret) {
+		dev_err(&tcm->client->dev, "failed to identify device: %d\n", ret);
+		return ret;
+	}
+
+	dev_dbg(&tcm->client->dev, "Synaptics TCM %s v%d mode %d\n",
+		id.part_number, id.version, id.mode);
+	if (id.mode != MODE_APPLICATION) {
+		/* We don't support firmware updates or anything else */
+		dev_err(&tcm->client->dev, "Device is not in application mode\n");
+		return -ENODEV;
+	}
+
+	do {
+		msleep(20);
+		ret = tcm_read_message(tcm, TCM_GET_APPLICATION_INFO, &app_info, sizeof(app_info));
+		if (ret) {
+			dev_err(&tcm->client->dev, "failed to get application info: %d\n", ret);
+			return ret;
+		}
+	} while (app_info.status == APP_STATUS_BOOTING || app_info.status == APP_STATUS_UPDATING);
+
+	dev_dbg(&tcm->client->dev, "Application firmware v%d.%d (customer '%s') status %d\n",
+		 app_info.version[0], app_info.version[1], app_info.customer_config_id,
+		 app_info.status);
+
+	*max_x = app_info.max_x;
+	*max_y = app_info.max_y;
+
+	return 0;
+}
+
+static int tcm_power_on(struct tcm_data *tcm)
+{
+	int ret;
+
+	ret = regulator_bulk_enable(ARRAY_SIZE(tcm->supplies),
+				    tcm->supplies);
+	if (ret)
+		return ret;
+
+	gpiod_set_value(tcm->reset_gpio, false);
+	usleep_range(10000, 11000);
+	gpiod_set_value(tcm->reset_gpio, true);
+	usleep_range(80000, 81000);
+
+	return 0;
+}
+
+static int tcm_probe(struct i2c_client *client)
+{
+	struct tcm_data *tcm;
+	struct tcm_report_config __maybe_unused report_config;
+	u16 max_x, max_y;
+	int ret;
+
+	if (!i2c_check_functionality(client->adapter, I2C_FUNC_I2C |
+						I2C_FUNC_SMBUS_BYTE_DATA |
+						I2C_FUNC_SMBUS_I2C_BLOCK))
+		return -ENODEV;
+
+	tcm = devm_kzalloc(&client->dev, sizeof(struct tcm_data), GFP_KERNEL);
+	if (!tcm)
+		return -ENOMEM;
+
+	i2c_set_clientdata(client, tcm);
+	tcm->client = client;
+
+	init_completion(&tcm->response);
+
+	tcm->supplies[0].supply = "vdd";
+	tcm->supplies[1].supply = "vcc";
+	ret = devm_regulator_bulk_get(&client->dev, ARRAY_SIZE(tcm->supplies),
+				      tcm->supplies);
+	if (ret)
+		return ret;
+
+	tcm->reset_gpio = devm_gpiod_get(&client->dev, "reset", GPIOD_OUT_HIGH);
+
+	ret = devm_add_action_or_reset(&client->dev, tcm_power_off,
+				       tcm);
+	if (ret)
+		return ret;
+
+	ret = tcm_power_on(tcm);
+	if (ret)
+		return ret;
+
+	ret = devm_request_threaded_irq(&client->dev, client->irq, NULL,
+					tcm_report_irq,
+					IRQF_TRIGGER_LOW | IRQF_ONESHOT,
+					"synaptics_tcm_report", tcm);
+	if (ret < 0)
+		return ret;
+
+	ret = tcm_hw_init(tcm, &max_x, &max_y);
+	if (ret) {
+		dev_err(&client->dev, "failed to initialize hardware\n");
+		return ret;
+	}
+
+	tcm->input = devm_input_allocate_device(&client->dev);
+	if (!tcm->input)
+		return -ENOMEM;
+
+	tcm->input->name = "Synaptics TCM Oncell Touchscreen";
+	tcm->input->id.bustype = BUS_I2C;
+	tcm->input->open = tcm_input_open;
+	tcm->input->close = tcm_input_close;
+
+	input_set_abs_params(tcm->input, ABS_MT_POSITION_X, 0, max_x, 0, 0);
+	input_set_abs_params(tcm->input, ABS_MT_POSITION_Y, 0, max_y, 0, 0);
+	input_set_abs_params(tcm->input, ABS_MT_TOUCH_MAJOR, 0, 255, 0, 0);
+	input_set_abs_params(tcm->input, ABS_MT_TOUCH_MINOR, 0, 255, 0, 0);
+	input_set_abs_params(tcm->input, ABS_MT_PRESSURE, 0, 255, 0, 0);
+
+	touchscreen_parse_properties(tcm->input, true, &tcm->prop);
+
+	ret = input_mt_init_slots(tcm->input, 10, INPUT_MT_DIRECT | INPUT_MT_DROP_UNUSED);
+	if (ret)
+		return ret;
+
+	input_set_drvdata(tcm->input, tcm);
+
+	ret = input_register_device(tcm->input);
+	if (ret)
+		return ret;
+
+	return 0;
+}
+
+static const struct of_device_id syna_driver_ids[] = {
+	{
+		.compatible = "syna,s3908",
+	},
+	{}
+};
+MODULE_DEVICE_TABLE(of, syna_driver_ids);
+
+static const struct i2c_device_id syna_i2c_ids[] = {
+	{ "synaptics-tcm", 0 },
+	{ }
+};
+
+MODULE_DEVICE_TABLE(i2c, syna_i2c_ids);
+
+static struct i2c_driver syna_i2c_driver = {
+	.probe	= tcm_probe,
+	.id_table	= syna_i2c_ids,
+	.driver	= {
+	.name	= "synaptics-tcm",
+	.of_match_table = syna_driver_ids,
+	},
+};
+
+module_i2c_driver(syna_i2c_driver);
+
+MODULE_LICENSE("GPL");
+MODULE_AUTHOR("Frieder Hannenheim <frieder.hannenheim@proton.me>");
+MODULE_AUTHOR("Caleb Connolly <caleb@postmarketos.org>");
+MODULE_DESCRIPTION("A driver for Synaptics TCM Oncell Touchpanels");
+