Linux Input/HID development
 help / color / mirror / Atom feed
From: "YİĞİTCAN KAVAKLI via B4 Relay" <devnull+kavakliyigitcan.gmail.com@kernel.org>
To: Lee Jones <lee@kernel.org>, Daniel Thompson <danielt@kernel.org>,
	 Jingoo Han <jingoohan1@gmail.com>,
	Pavel Machek <pavel@kernel.org>,  Rob Herring <robh@kernel.org>,
	Krzysztof Kozlowski <krzk+dt@kernel.org>,
	 Conor Dooley <conor+dt@kernel.org>, Helge Deller <deller@gmx.de>,
	 Liam Girdwood <lgirdwood@gmail.com>,
	Mark Brown <broonie@kernel.org>,
	 Sumit Semwal <sumit.semwal@linaro.org>,
	 Neil Armstrong <neil.armstrong@linaro.org>,
	 Jessica Zhang <jesszhan0024@gmail.com>,
	 Maarten Lankhorst <maarten.lankhorst@linux.intel.com>,
	 Maxime Ripard <mripard@kernel.org>,
	Thomas Zimmermann <tzimmermann@suse.de>,
	 David Airlie <airlied@gmail.com>,
	Simona Vetter <simona@ffwll.ch>,
	 Dmitry Torokhov <dmitry.torokhov@gmail.com>,
	 Henrik Rydberg <rydberg@bitmath.org>,
	 Rob Clark <robin.clark@oss.qualcomm.com>,
	 Dmitry Baryshkov <lumag@kernel.org>,
	 Abhinav Kumar <abhinav.kumar@linux.dev>,
	Sean Paul <sean@poorly.run>,
	 Marijn Suijten <marijn.suijten@somainline.org>,
	 Bjorn Andersson <andersson@kernel.org>,
	 Konrad Dybcio <konradybcio@kernel.org>
Cc: dri-devel@lists.freedesktop.org, linux-leds@vger.kernel.org,
	devicetree@vger.kernel.org, linux-kernel@vger.kernel.org,
	linux-fbdev@vger.kernel.org, linux-input@vger.kernel.org,
	linux-arm-msm@vger.kernel.org, freedreno@lists.freedesktop.org,
	"YİĞİTCAN KAVAKLI" <kavakliyigitcan@gmail.com>
Subject: [PATCH RFC 06/10] Input: novatek-nt36672a-spi: add driver for Novatek NT36672A SPI touchscreen
Date: Fri, 11 Sep 2026 11:51:03 +0300	[thread overview]
Message-ID: <20260911-ginkgo-submission-final-v1-6-0e68e63a18d4@gmail.com> (raw)
In-Reply-To: <20260911-ginkgo-submission-final-v1-0-0e68e63a18d4@gmail.com>

From: YİĞİTCAN KAVAKLI <kavakliyigitcan@gmail.com>

Add support for the Novatek NT36672A TDDI touchscreen controller over SPI.

The NT36672A is connected over SPI (QUPv3 SE2) on SM6125 and requires
dynamic host-download firmware loaded into controller volatile SRAM.
A dedicated SPI driver is introduced (Option B) to avoid breaking or
complicating the existing I2C-only driver (novatek-nvt-ts.c).

Features:
- Dynamic firmware host-download into SRAM via request_firmware()
- Multi-partition binary parsing with hardware CRC verification
- Threaded interrupt handler supporting Linux MT Protocol Type B
  (up to 10 fingers)
- Standard touchscreen_properties coordinate reporting
- DRM panel follower integration via devm_drm_panel_add_follower()
  to follow panel sleep/wake transitions cleanly
- Pure interrupt-driven architecture with zero polling loops

Signed-off-by: YİĞİTCAN KAVAKLI <kavakliyigitcan@gmail.com>
---
 drivers/input/touchscreen/Kconfig                |  11 +
 drivers/input/touchscreen/Makefile               |   1 +
 drivers/input/touchscreen/novatek-nt36672a-spi.c | 674 +++++++++++++++++++++++
 3 files changed, 686 insertions(+)

diff --git a/drivers/input/touchscreen/Kconfig b/drivers/input/touchscreen/Kconfig
index 9b9ae8ac3f7fd3b9e948083fcd05afabdcd69bca..fb9a34d3f983084af75e85aad2d9740d066e85fb 100644
--- a/drivers/input/touchscreen/Kconfig
+++ b/drivers/input/touchscreen/Kconfig
@@ -701,6 +701,17 @@ config TOUCHSCREEN_NOVATEK_NVT_TS
 	  To compile this driver as a module, choose M here: the
 	  module will be called novatek-nvt-ts.
 
+config TOUCHSCREEN_NOVATEK_NT36672A_SPI
+	tristate "Novatek NT36672A SPI touchscreen support"
+	depends on SPI_MASTER
+	select FW_LOADER
+	help
+	  Say Y here if you have a Novatek NT36672A touchscreen
+	  connected over SPI.
+
+	  To compile this driver as a module, choose M here: the
+	  module will be called novatek-nt36672a-spi.
+
 config TOUCHSCREEN_IMAGIS
 	tristate "Imagis touchscreen support"
 	depends on I2C
diff --git a/drivers/input/touchscreen/Makefile b/drivers/input/touchscreen/Makefile
index bfd9de83389d8833dbe7cc2641cb24493cdf3807..aca74a0afd141356c7b5d3d3393a882e0530be17 100644
--- a/drivers/input/touchscreen/Makefile
+++ b/drivers/input/touchscreen/Makefile
@@ -69,6 +69,7 @@ obj-$(CONFIG_TOUCHSCREEN_MMS114)	+= mms114.o
 obj-$(CONFIG_TOUCHSCREEN_MSG2638)	+= msg2638.o
 obj-$(CONFIG_TOUCHSCREEN_MTOUCH)	+= mtouch.o
 obj-$(CONFIG_TOUCHSCREEN_NOVATEK_NVT_TS)	+= novatek-nvt-ts.o
+obj-$(CONFIG_TOUCHSCREEN_NOVATEK_NT36672A_SPI)	+= novatek-nt36672a-spi.o
 obj-$(CONFIG_TOUCHSCREEN_HP600)		+= hp680_ts_input.o
 obj-$(CONFIG_TOUCHSCREEN_HP7XX)		+= jornada720_ts.o
 obj-$(CONFIG_TOUCHSCREEN_IPAQ_MICRO)	+= ipaq-micro-ts.o
diff --git a/drivers/input/touchscreen/novatek-nt36672a-spi.c b/drivers/input/touchscreen/novatek-nt36672a-spi.c
new file mode 100644
index 0000000000000000000000000000000000000000..03d2042b0bb30d3420379497fd822153bc90171e
--- /dev/null
+++ b/drivers/input/touchscreen/novatek-nt36672a-spi.c
@@ -0,0 +1,674 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Novatek NT36672A SPI Touchscreen Driver
+ *
+ * Copyright (c) 2026
+ *
+ * Hardware:
+ *   Novatek NT36672A TDDI controller connected over SPI.
+ *   Firmware is loaded dynamically into controller SRAM via request_firmware().
+ *   Panel lifecycle synchronization uses the DRM panel follower API.
+ */
+
+#include <linux/delay.h>
+#include <linux/firmware.h>
+#include <linux/init.h>
+#include <linux/input.h>
+#include <linux/input/mt.h>
+#include <linux/input/touchscreen.h>
+#include <linux/interrupt.h>
+#include <linux/kernel.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/of.h>
+#include <linux/pm.h>
+#include <linux/slab.h>
+#include <linux/spi/spi.h>
+#include <linux/workqueue.h>
+
+#include <drm/drm_panel.h>
+
+#define NVT_MAX_TOUCHES			10
+#define NVT_POINT_DATA_LEN		65
+#define NVT_TRANSFER_LEN		1024
+#define NVT_DEFAULT_FW_NAME		"novatek/nt36672a_tianma.bin"
+
+/* NT36672A Memory Map & Registers */
+#define EVENT_BUF_ADDR			0x21c00
+#define EVENT_MAP_HOST_CMD		0x50
+#define EVENT_MAP_RESET_COMPLETE	0x60
+#define EVENT_MAP_FWINFO		0x78
+
+#define BOOT_RDY_ADDR			0x3f10d
+#define BLD_CRC_EN_ADDR			0x3f30e
+#define ILM_DES_ADDR			0x3f128
+#define ILM_LENGTH_ADDR			0x3f118
+#define G_ILM_CHECKSUM_ADDR		0x3f100
+#define DLM_DES_ADDR			0x3f12c
+#define DLM_LENGTH_ADDR			0x3f130
+#define G_DLM_CHECKSUM_ADDR		0x3f104
+#define SPI_RD_FAST_ADDR		0x03f310
+
+struct nvt_ts_bin_map {
+	u32 bin_addr;
+	u32 sram_addr;
+	u32 size;
+	u32 crc;
+};
+
+struct novatek_ts_data {
+	struct spi_device *spi;
+	struct input_dev *input_dev;
+	struct touchscreen_properties prop;
+	/* Protects SPI bus access, firmware loading and controller state */
+	struct mutex lock;
+	struct drm_panel_follower panel_follower;
+	struct workqueue_struct *fw_wq;
+	struct delayed_work fw_reload_work;
+	const char *fw_name;
+
+	u16 abs_x_max;
+	u16 abs_y_max;
+	u8 fw_ver;
+	u8 fw_reset_state;
+	bool fw_running;
+	bool slot_active[NVT_MAX_TOUCHES];
+};
+
+static inline u32 byte_to_word(const u8 *b)
+{
+	return b[0] | (b[1] << 8) | (b[2] << 16) | (b[3] << 24);
+}
+
+static int nvt_spi_write(struct spi_device *spi, const u8 *buf, size_t len)
+{
+	struct spi_transfer t = {0};
+	struct spi_message m;
+	u8 *xbuf;
+	int ret;
+
+	xbuf = kmemdup(buf, len, GFP_KERNEL);
+	if (!xbuf)
+		return -ENOMEM;
+
+	xbuf[0] |= 0x80; /* SPI_WRITE_MASK */
+
+	t.tx_buf = xbuf;
+	t.len = len;
+
+	spi_message_init(&m);
+	spi_message_add_tail(&t, &m);
+	ret = spi_sync(spi, &m);
+	kfree(xbuf);
+
+	return ret;
+}
+
+static int nvt_spi_read(struct spi_device *spi, u8 *buf, size_t len)
+{
+	struct spi_transfer t = {0};
+	struct spi_message m;
+	u8 *xbuf, *rbuf;
+	int ret;
+
+	xbuf = kzalloc(len + 1, GFP_KERNEL);
+	rbuf = kzalloc(len + 1, GFP_KERNEL);
+	if (!xbuf || !rbuf) {
+		kfree(xbuf);
+		kfree(rbuf);
+		return -ENOMEM;
+	}
+
+	xbuf[0] = buf[0] & 0x7f; /* SPI_READ_MASK */
+
+	t.tx_buf = xbuf;
+	t.rx_buf = rbuf;
+	t.len = len + 1; /* 1 dummy byte */
+
+	spi_message_init(&m);
+	spi_message_add_tail(&t, &m);
+	ret = spi_sync(spi, &m);
+	if (!ret)
+		memcpy(buf + 1, rbuf + 2, len - 1);
+
+	kfree(xbuf);
+	kfree(rbuf);
+
+	return ret;
+}
+
+static int nvt_set_page(struct spi_device *spi, u32 addr)
+{
+	u8 buf[3];
+
+	buf[0] = 0xff; /* page command */
+	buf[1] = (addr >> 15) & 0xff;
+	buf[2] = (addr >> 7) & 0xff;
+
+	return nvt_spi_write(spi, buf, sizeof(buf));
+}
+
+static int nvt_write_addr(struct spi_device *spi, u32 addr, u8 data)
+{
+	u8 buf[2];
+	int ret;
+
+	ret = nvt_set_page(spi, addr);
+	if (ret)
+		return ret;
+
+	buf[0] = addr & 0x7f;
+	buf[1] = data;
+
+	return nvt_spi_write(spi, buf, sizeof(buf));
+}
+
+static void nvt_bootloader_reset(struct spi_device *spi)
+{
+	/*
+	 * Configure SPI Fast Read to 0x00 so bootloader SRAM writes succeed.
+	 * Do NOT write 0x69 to SWRST_N8_ADDR because NT36672A is a TDDI chip:
+	 * writing 0x69 resets the entire IC (display timing generator, DCS registers, PWM)
+	 * and causes the physical LCD to go completely black.
+	 * The controller is already reset into ROM bootloader mode by hardware panel reset.
+	 */
+	nvt_write_addr(spi, SPI_RD_FAST_ADDR, 0x00);
+}
+
+static void nvt_set_bld_crc_bank(struct spi_device *spi, u32 des_addr, u32 sram_addr,
+				 u32 len_addr, u32 size, u32 g_chk_addr, u32 crc)
+{
+	u8 buf[5];
+
+	/* Destination address */
+	nvt_set_page(spi, des_addr);
+	buf[0] = des_addr & 0x7f;
+	buf[1] = sram_addr & 0xff;
+	buf[2] = (sram_addr >> 8) & 0xff;
+	buf[3] = (sram_addr >> 16) & 0xff;
+	nvt_spi_write(spi, buf, 4);
+
+	/* Length (for NT36672A hw_crc=1, 3 bytes) */
+	buf[0] = len_addr & 0x7f;
+	buf[1] = size & 0xff;
+	buf[2] = (size >> 8) & 0xff;
+	nvt_spi_write(spi, buf, 3);
+
+	/* Golden Checksum (4 bytes) */
+	buf[0] = g_chk_addr & 0x7f;
+	buf[1] = crc & 0xff;
+	buf[2] = (crc >> 8) & 0xff;
+	buf[3] = (crc >> 16) & 0xff;
+	buf[4] = (crc >> 24) & 0xff;
+	nvt_spi_write(spi, buf, 5);
+}
+
+static int nvt_write_sram(struct spi_device *spi, const u8 *fwdata,
+			  u32 sram_addr, u32 size, u32 bin_addr)
+{
+	u32 count, i, len;
+	int ret = 0;
+	u8 *fwbuf;
+
+	fwbuf = kmalloc(NVT_TRANSFER_LEN + 2, GFP_KERNEL);
+	if (!fwbuf)
+		return -ENOMEM;
+
+	count = (size + NVT_TRANSFER_LEN - 1) / NVT_TRANSFER_LEN;
+
+	for (i = 0; i < count; i++) {
+		len = (size < NVT_TRANSFER_LEN) ? size : NVT_TRANSFER_LEN;
+
+		ret = nvt_set_page(spi, sram_addr);
+		if (ret)
+			goto out;
+
+		fwbuf[0] = sram_addr & 0x7f;
+		memcpy(fwbuf + 1, &fwdata[bin_addr], len);
+		ret = nvt_spi_write(spi, fwbuf, len + 1);
+		if (ret)
+			goto out;
+
+		sram_addr += NVT_TRANSFER_LEN;
+		bin_addr += NVT_TRANSFER_LEN;
+		size -= len;
+	}
+
+out:
+	kfree(fwbuf);
+	return ret;
+}
+
+static int nvt_parse_and_download(struct novatek_ts_data *ts, const u8 *fwdata, size_t fwsize)
+{
+	struct spi_device *spi = ts->spi;
+	struct nvt_ts_bin_map bin_map[16];
+	u8 info_sec_num = 0;
+	u8 ovly_sec_num = 0;
+	u32 partition, list;
+	u32 header_end, pos;
+	u8 buf[18];
+	int ret, retry;
+
+	if (fwsize < 0x40)
+		return -EINVAL;
+
+	header_end = byte_to_word(&fwdata[0]);
+	pos = 0x30;
+	while (pos < header_end && info_sec_num < 14) {
+		info_sec_num++;
+		pos += 0x10;
+	}
+
+	ovly_sec_num = (fwdata[0x28] & 0x10) ? (fwdata[0x28] & 0x0f) : 0;
+	partition = 2 + ovly_sec_num + info_sec_num;
+	if (partition > 16)
+		partition = 16;
+
+	for (list = 0; list < partition; list++) {
+		if (list < 2) {
+			bin_map[list].bin_addr = byte_to_word(&fwdata[0 + list * 12]);
+			bin_map[list].sram_addr = byte_to_word(&fwdata[4 + list * 12]);
+			bin_map[list].size = byte_to_word(&fwdata[8 + list * 12]);
+			bin_map[list].crc = byte_to_word(&fwdata[0x18 + list * 4]);
+		} else {
+			pos = 0x30 + (0x10 * (list - 2));
+			bin_map[list].sram_addr = byte_to_word(&fwdata[pos]);
+			bin_map[list].size = byte_to_word(&fwdata[pos + 4]);
+			bin_map[list].bin_addr = byte_to_word(&fwdata[pos + 8]);
+			bin_map[list].crc = byte_to_word(&fwdata[pos + 12]);
+		}
+	}
+
+	nvt_bootloader_reset(spi);
+
+	for (list = 0; list < partition; list++) {
+		u32 size = bin_map[list].size;
+
+		if (!size)
+			continue;
+		size += 1;
+
+		ret = nvt_write_sram(spi, fwdata, bin_map[list].sram_addr, size,
+				     bin_map[list].bin_addr);
+		if (ret) {
+			dev_err(&spi->dev, "SRAM write partition %u failed: %d\n", list, ret);
+			return ret;
+		}
+	}
+
+	nvt_set_bld_crc_bank(spi, ILM_DES_ADDR, bin_map[0].sram_addr,
+			     ILM_LENGTH_ADDR, bin_map[0].size,
+			     G_ILM_CHECKSUM_ADDR, bin_map[0].crc);
+	nvt_set_bld_crc_bank(spi, DLM_DES_ADDR, bin_map[1].sram_addr,
+			     DLM_LENGTH_ADDR, bin_map[1].size,
+			     G_DLM_CHECKSUM_ADDR, bin_map[1].crc);
+
+	/* Enable BLD CRC (bit 7 of 0x3F30E) */
+	nvt_set_page(spi, BLD_CRC_EN_ADDR);
+	buf[0] = BLD_CRC_EN_ADDR & 0x7f;
+	buf[1] = 0xff;
+	nvt_spi_read(spi, buf, 2);
+	buf[0] = BLD_CRC_EN_ADDR & 0x7f;
+	buf[1] = buf[1] | 0x80;
+	nvt_spi_write(spi, buf, 2);
+
+	/* Clear reset status and enable firmware CRC */
+	nvt_set_page(spi, EVENT_BUF_ADDR);
+	buf[0] = EVENT_MAP_RESET_COMPLETE & 0x7f;
+	buf[1] = 0x00;
+	nvt_spi_write(spi, buf, 2);
+
+	buf[0] = EVENT_MAP_HOST_CMD & 0x7f;
+	buf[1] = 0xae;
+	nvt_spi_write(spi, buf, 2);
+
+	/* Boot Ready */
+	nvt_write_addr(spi, BOOT_RDY_ADDR, 1);
+	msleep(10);
+
+	/* Verify Firmware Reset State (>= 0xA0) */
+	ts->fw_running = false;
+	nvt_set_page(spi, EVENT_BUF_ADDR | EVENT_MAP_RESET_COMPLETE);
+	for (retry = 0; retry < 20; retry++) {
+		buf[0] = EVENT_MAP_RESET_COMPLETE;
+		buf[1] = 0x00;
+		ret = nvt_spi_read(spi, buf, 6);
+		if (ret == 0 && buf[1] >= 0xa0 && buf[1] <= 0xaf) {
+			ts->fw_reset_state = buf[1];
+			ts->fw_running = true;
+			break;
+		}
+		msleep(10);
+	}
+
+	/* Wait for baseline calibration */
+	for (retry = 0; retry < 50; retry++) {
+		buf[0] = EVENT_MAP_RESET_COMPLETE;
+		buf[1] = 0x00;
+		ret = nvt_spi_read(spi, buf, 6);
+		if (ret == 0 && buf[1] >= 0xa1 && buf[1] <= 0xaf) {
+			ts->fw_reset_state = buf[1];
+			break;
+		}
+		msleep(10);
+	}
+
+	if (!ts->fw_running) {
+		dev_err(&spi->dev, "firmware boot timeout: state=0x%02x\n", buf[1]);
+		return -EIO;
+	}
+
+	/* Read FW Information */
+	nvt_set_page(spi, EVENT_BUF_ADDR | EVENT_MAP_FWINFO);
+	buf[0] = EVENT_MAP_FWINFO;
+	ret = nvt_spi_read(spi, buf, 17);
+	if (ret == 0) {
+		ts->fw_ver = buf[1];
+		ts->abs_x_max = (u16)((buf[5] << 8) | buf[6]);
+		ts->abs_y_max = (u16)((buf[7] << 8) | buf[8]);
+	}
+
+	if (!ts->abs_x_max || ts->abs_x_max > 4096)
+		ts->abs_x_max = 1080;
+	if (!ts->abs_y_max || ts->abs_y_max > 4096)
+		ts->abs_y_max = 2340;
+
+	nvt_set_page(spi, EVENT_BUF_ADDR);
+
+	return 0;
+}
+
+static int novatek_ts_load_firmware(struct novatek_ts_data *ts)
+{
+	const struct firmware *fw;
+	int ret;
+
+	ret = request_firmware(&fw, ts->fw_name, &ts->spi->dev);
+	if (ret) {
+		dev_err(&ts->spi->dev, "failed to request firmware '%s': %d\n",
+			ts->fw_name, ret);
+		return ret;
+	}
+
+	ret = nvt_parse_and_download(ts, fw->data, fw->size);
+	release_firmware(fw);
+
+	return ret;
+}
+
+static void novatek_ts_fw_work(struct work_struct *work)
+{
+	struct delayed_work *dwork = to_delayed_work(work);
+	struct novatek_ts_data *ts = container_of(dwork, struct novatek_ts_data, fw_reload_work);
+	u8 rst_chk[6] = {EVENT_MAP_RESET_COMPLETE, 0};
+	int ret;
+
+	mutex_lock(&ts->lock);
+
+	nvt_set_page(ts->spi, EVENT_BUF_ADDR);
+	ret = nvt_spi_read(ts->spi, rst_chk, 2);
+	if (ret == 0 && rst_chk[1] >= 0xa0 && rst_chk[1] <= 0xaf) {
+		/* Controller is already running valid firmware */
+		mutex_unlock(&ts->lock);
+		return;
+	}
+
+	ret = novatek_ts_load_firmware(ts);
+	if (ret)
+		dev_err(&ts->spi->dev, "firmware load failed: %d\n", ret);
+
+	nvt_set_page(ts->spi, EVENT_BUF_ADDR);
+	mutex_unlock(&ts->lock);
+}
+
+static int novatek_ts_panel_prepared(struct drm_panel_follower *follower)
+{
+	struct novatek_ts_data *ts = container_of(follower, struct novatek_ts_data, panel_follower);
+
+	if (ts->fw_wq) {
+		cancel_delayed_work_sync(&ts->fw_reload_work);
+		queue_delayed_work(ts->fw_wq, &ts->fw_reload_work, msecs_to_jiffies(1500));
+	}
+	return 0;
+}
+
+static int novatek_ts_panel_unpreparing(struct drm_panel_follower *follower)
+{
+	struct novatek_ts_data *ts = container_of(follower, struct novatek_ts_data, panel_follower);
+
+	if (ts->fw_wq)
+		cancel_delayed_work_sync(&ts->fw_reload_work);
+	return 0;
+}
+
+static const struct drm_panel_follower_funcs novatek_ts_panel_follower_funcs = {
+	.panel_prepared = novatek_ts_panel_prepared,
+	.panel_unpreparing = novatek_ts_panel_unpreparing,
+};
+
+static irqreturn_t novatek_ts_threaded_irq(int irq, void *dev_id)
+{
+	struct novatek_ts_data *ts = dev_id;
+	u8 point_data[NVT_POINT_DATA_LEN + 2] = {0};
+	bool slot_pressed[NVT_MAX_TOUCHES] = {false};
+	int finger_cnt = 0;
+	int ret, i;
+
+	mutex_lock(&ts->lock);
+
+	nvt_set_page(ts->spi, EVENT_BUF_ADDR);
+
+	point_data[0] = 0x00;
+	ret = nvt_spi_read(ts->spi, point_data, NVT_POINT_DATA_LEN + 1);
+	if (ret < 0) {
+		dev_err_ratelimited(&ts->spi->dev, "failed to read touch points: %d\n", ret);
+		mutex_unlock(&ts->lock);
+		return IRQ_HANDLED;
+	}
+
+	for (i = 0; i < NVT_MAX_TOUCHES; i++) {
+		int pos = 1 + 6 * i;
+		u8 raw0 = point_data[pos];
+		u8 input_id = raw0 >> 3;
+		u8 status = raw0 & 0x07;
+
+		if (input_id < 1 || input_id > NVT_MAX_TOUCHES)
+			continue;
+
+		if (status == 0x01 || status == 0x02) { /* 1: down, 2: move */
+			u32 x = ((u32)point_data[pos + 1] << 4) | ((u32)point_data[pos + 3] >> 4);
+			u32 y = ((u32)point_data[pos + 2] << 4) | ((u32)point_data[pos + 3] & 0x0f);
+			u32 w = point_data[pos + 4] ? point_data[pos + 4] : 1;
+			u32 p = point_data[pos + 5] ? point_data[pos + 5] : 1;
+
+			if (x <= ts->abs_x_max && y <= ts->abs_y_max) {
+				int slot = input_id - 1;
+
+				slot_pressed[slot] = true;
+				finger_cnt++;
+
+				input_mt_slot(ts->input_dev, slot);
+				input_mt_report_slot_state(ts->input_dev, MT_TOOL_FINGER, true);
+				input_report_abs(ts->input_dev, ABS_MT_POSITION_X, x);
+				input_report_abs(ts->input_dev, ABS_MT_POSITION_Y, y);
+				input_report_abs(ts->input_dev, ABS_MT_TOUCH_MAJOR, w);
+				input_report_abs(ts->input_dev, ABS_MT_PRESSURE, p);
+			}
+		}
+	}
+
+	for (i = 0; i < NVT_MAX_TOUCHES; i++) {
+		if (!slot_pressed[i] && ts->slot_active[i]) {
+			input_mt_slot(ts->input_dev, i);
+			input_mt_report_slot_state(ts->input_dev, MT_TOOL_FINGER, false);
+			ts->slot_active[i] = false;
+		} else if (slot_pressed[i]) {
+			ts->slot_active[i] = true;
+		}
+	}
+
+	input_report_key(ts->input_dev, BTN_TOUCH, finger_cnt > 0);
+	input_mt_sync_frame(ts->input_dev);
+	input_sync(ts->input_dev);
+
+	mutex_unlock(&ts->lock);
+
+	return IRQ_HANDLED;
+}
+
+static int novatek_ts_init_input(struct novatek_ts_data *ts)
+{
+	struct spi_device *spi = ts->spi;
+	struct input_dev *input;
+	int ret;
+
+	input = devm_input_allocate_device(&spi->dev);
+	if (!input)
+		return -ENOMEM;
+
+	input->name = "Novatek NT36672A Touchscreen";
+	input->id.bustype = BUS_SPI;
+	input->dev.parent = &spi->dev;
+
+	__set_bit(EV_SYN, input->evbit);
+	__set_bit(EV_KEY, input->evbit);
+	__set_bit(EV_ABS, input->evbit);
+	__set_bit(INPUT_PROP_DIRECT, input->propbit);
+
+	input_set_capability(input, EV_KEY, BTN_TOUCH);
+	input_set_abs_params(input, ABS_MT_POSITION_X, 0, ts->abs_x_max, 0, 0);
+	input_set_abs_params(input, ABS_MT_POSITION_Y, 0, ts->abs_y_max, 0, 0);
+	input_set_abs_params(input, ABS_MT_TOUCH_MAJOR, 0, 255, 0, 0);
+	input_set_abs_params(input, ABS_MT_PRESSURE, 0, 255, 0, 0);
+
+	ret = input_mt_init_slots(input, NVT_MAX_TOUCHES,
+				  INPUT_MT_DIRECT | INPUT_MT_DROP_UNUSED);
+	if (ret)
+		return ret;
+
+	ret = input_register_device(input);
+	if (ret)
+		return ret;
+
+	ts->input_dev = input;
+
+	return 0;
+}
+
+static int novatek_ts_probe(struct spi_device *spi)
+{
+	struct device *dev = &spi->dev;
+	struct novatek_ts_data *ts;
+	int ret;
+
+	ts = devm_kzalloc(dev, sizeof(*ts), GFP_KERNEL);
+	if (!ts)
+		return -ENOMEM;
+
+	ts->spi = spi;
+	mutex_init(&ts->lock);
+
+	ts->fw_wq = create_singlethread_workqueue("novatek_ts_fw");
+	if (!ts->fw_wq)
+		return -ENOMEM;
+
+	INIT_DELAYED_WORK(&ts->fw_reload_work, novatek_ts_fw_work);
+
+	ret = device_property_read_string(dev, "firmware-name", &ts->fw_name);
+	if (ret)
+		ts->fw_name = NVT_DEFAULT_FW_NAME;
+
+	spi->mode = SPI_MODE_0;
+	spi->bits_per_word = 8;
+	if (!spi->max_speed_hz)
+		spi->max_speed_hz = 4000000;
+
+	ret = spi_setup(spi);
+	if (ret)
+		goto err_destroy_wq;
+
+	spi_set_drvdata(spi, ts);
+
+	ts->abs_x_max = 1080;
+	ts->abs_y_max = 2340;
+
+	ret = novatek_ts_load_firmware(ts);
+	if (ret) {
+		dev_err(dev, "failed to download firmware during probe: %d\n", ret);
+		goto err_destroy_wq;
+	}
+
+	dev_info(dev, "Novatek NT36672A initialized (fw_ver=0x%02x, reset_state=0x%02x)\n",
+		 ts->fw_ver, ts->fw_reset_state);
+
+	ret = novatek_ts_init_input(ts);
+	if (ret)
+		goto err_destroy_wq;
+
+	if (spi->irq <= 0) {
+		ret = dev_err_probe(dev, -EINVAL, "no IRQ specified\n");
+		goto err_destroy_wq;
+	}
+
+	ret = devm_request_threaded_irq(dev, spi->irq, NULL,
+					novatek_ts_threaded_irq,
+					IRQF_TRIGGER_RISING | IRQF_ONESHOT,
+					"novatek-nt36672a-ts", ts);
+	if (ret) {
+		dev_err_probe(dev, ret, "failed to request threaded IRQ\n");
+		goto err_destroy_wq;
+	}
+
+	if (drm_is_panel_follower(dev)) {
+		ts->panel_follower.funcs = &novatek_ts_panel_follower_funcs;
+		ret = devm_drm_panel_add_follower(dev, &ts->panel_follower);
+		if (ret) {
+			dev_err_probe(dev, ret, "failed to add panel follower\n");
+			goto err_destroy_wq;
+		}
+	}
+
+	return 0;
+
+err_destroy_wq:
+	cancel_delayed_work_sync(&ts->fw_reload_work);
+	destroy_workqueue(ts->fw_wq);
+	return ret;
+}
+
+static void novatek_ts_remove(struct spi_device *spi)
+{
+	struct novatek_ts_data *ts = spi_get_drvdata(spi);
+
+	cancel_delayed_work_sync(&ts->fw_reload_work);
+	destroy_workqueue(ts->fw_wq);
+}
+
+static const struct of_device_id novatek_ts_of_match[] = {
+	{ .compatible = "novatek,nt36672a-spi" },
+	{ .compatible = "novatek,nt36672a-ts" },
+	{ }
+};
+MODULE_DEVICE_TABLE(of, novatek_ts_of_match);
+
+static const struct spi_device_id novatek_ts_spi_id[] = {
+	{ "nt36672a-ts" },
+	{ }
+};
+MODULE_DEVICE_TABLE(spi, novatek_ts_spi_id);
+
+static struct spi_driver novatek_ts_driver = {
+	.driver = {
+		.name = "novatek-nt36672a-spi",
+		.of_match_table = novatek_ts_of_match,
+	},
+	.id_table = novatek_ts_spi_id,
+	.probe = novatek_ts_probe,
+	.remove = novatek_ts_remove,
+};
+module_spi_driver(novatek_ts_driver);
+
+MODULE_AUTHOR("Antigravity Pair Programmer");
+MODULE_DESCRIPTION("Novatek NT36672A SPI Touchscreen Driver");
+MODULE_LICENSE("GPL");

-- 
2.47.3



  parent reply	other threads:[~2026-09-11  8:51 UTC|newest]

Thread overview: 21+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-09-11  8:50 [PATCH RFC 00/10] arm64: qcom: sm6125-ginkgo: display, backlight, and touch support YİĞİTCAN KAVAKLI via B4 Relay
2026-09-11  8:50 ` [PATCH RFC 01/10] dt-bindings: backlight: add Kinetic KTD3136 YİĞİTCAN KAVAKLI via B4 Relay
2026-09-11  8:56   ` sashiko-bot
2026-09-11  8:50 ` [PATCH RFC 02/10] backlight: add Kinetic KTD3136 driver YİĞİTCAN KAVAKLI via B4 Relay
2026-09-11  8:59   ` sashiko-bot
2026-09-11  8:51 ` [PATCH RFC 03/10] dt-bindings: display: panel: novatek,nt36672a: add Tianma FHD+ video mode variant YİĞİTCAN KAVAKLI via B4 Relay
2026-09-11  8:53   ` sashiko-bot
2026-09-11  8:51 ` [PATCH RFC 04/10] drm/panel: novatek-nt36672a: add Tianma FHD+ video mode panel YİĞİTCAN KAVAKLI via B4 Relay
2026-09-11  9:09   ` sashiko-bot
2026-09-11  8:51 ` [PATCH RFC 05/10] dt-bindings: input: touchscreen: add Novatek NT36672A SPI touchscreen YİĞİTCAN KAVAKLI via B4 Relay
2026-09-11  8:58   ` sashiko-bot
2026-09-11  8:51 ` YİĞİTCAN KAVAKLI via B4 Relay [this message]
2026-09-11  9:02   ` [PATCH RFC 06/10] Input: novatek-nt36672a-spi: add driver for " sashiko-bot
2026-09-11  8:51 ` [PATCH RFC 07/10] drm/msm/dpu: describe SM6125 programmable-fetch delayed-start quirk YİĞİTCAN KAVAKLI via B4 Relay
2026-09-11  9:01   ` sashiko-bot
2026-09-11  8:51 ` [PATCH RFC 08/10] drm/msm/dsi: make command and video mode configuration mutually exclusive YİĞİTCAN KAVAKLI via B4 Relay
2026-09-11  9:00   ` sashiko-bot
2026-09-11  8:51 ` [PATCH RFC 09/10] drm/msm/dsi: separate host link enable from video stream enable YİĞİTCAN KAVAKLI via B4 Relay
2026-09-11  9:04   ` sashiko-bot
2026-09-11  8:51 ` [PATCH RFC 10/10] arm64: dts: qcom: sm6125-xiaomi-ginkgo: enable display, backlight and touchscreen YİĞİTCAN KAVAKLI via B4 Relay
2026-09-11  9:04   ` 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=20260911-ginkgo-submission-final-v1-6-0e68e63a18d4@gmail.com \
    --to=devnull+kavakliyigitcan.gmail.com@kernel.org \
    --cc=abhinav.kumar@linux.dev \
    --cc=airlied@gmail.com \
    --cc=andersson@kernel.org \
    --cc=broonie@kernel.org \
    --cc=conor+dt@kernel.org \
    --cc=danielt@kernel.org \
    --cc=deller@gmx.de \
    --cc=devicetree@vger.kernel.org \
    --cc=dmitry.torokhov@gmail.com \
    --cc=dri-devel@lists.freedesktop.org \
    --cc=freedreno@lists.freedesktop.org \
    --cc=jesszhan0024@gmail.com \
    --cc=jingoohan1@gmail.com \
    --cc=kavakliyigitcan@gmail.com \
    --cc=konradybcio@kernel.org \
    --cc=krzk+dt@kernel.org \
    --cc=lee@kernel.org \
    --cc=lgirdwood@gmail.com \
    --cc=linux-arm-msm@vger.kernel.org \
    --cc=linux-fbdev@vger.kernel.org \
    --cc=linux-input@vger.kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-leds@vger.kernel.org \
    --cc=lumag@kernel.org \
    --cc=maarten.lankhorst@linux.intel.com \
    --cc=marijn.suijten@somainline.org \
    --cc=mripard@kernel.org \
    --cc=neil.armstrong@linaro.org \
    --cc=pavel@kernel.org \
    --cc=robh@kernel.org \
    --cc=robin.clark@oss.qualcomm.com \
    --cc=rydberg@bitmath.org \
    --cc=sean@poorly.run \
    --cc=simona@ffwll.ch \
    --cc=sumit.semwal@linaro.org \
    --cc=tzimmermann@suse.de \
    /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