mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [PATCH v6 0/3] HID: Add support for the Himax HX83102J touchscreen
@ 2026-10-11 18:16 Michał Kopeć
  2026-10-11 18:16 ` [PATCH v6 1/3] dt-bindings: input: Add " Michał Kopeć
                   ` (2 more replies)
  0 siblings, 3 replies; 5+ messages in thread
From: Michał Kopeć @ 2026-10-11 18:16 UTC (permalink / raw)
  To: linux-input
  Cc: devicetree, linux-mediatek, linux-arm-kernel, linux-kernel,
	jikos, bentiss, dmitry.torokhov, robh, krzk+dt, conor+dt,
	matthias.bgg, angelogioacchino.delregno, allencl_lin, tylor_yang,
	felix, dianders, Michał Kopeć

This series adds support for the touch function of the Himax HX83102J,
a touch and display driver integrated (TDDI) controller that reports HID
packets over SPI. It is used in the MediaTek MT8188 based Lenovo
Chromebook Duet ("Ciri").

As Krzysztof suggested, I have taken over the series and rewritten the
driver along the lines of hid-goodix-spi instead of cleaning up the
vendor code. Allen Lin and Tylor Yang are credited with Co-developed-by,
since the protocol, the firmware loading sequence and the heatmap
handling come from their code.

On Ciri the controller has no flash, so the firmware is loaded from a
file (himax_i2chid_1002.bin or himax_i2chid_1003.bin, depending on the
panel, the names ChromeOS uses). These files are not in linux-firmware.

Tested on Ciri (sku0, BOE panel): touch, stylus, display off and on,
suspend/resume and reloading the module.

The firmware report descriptor declares feature report 68 (usage
0xff0000c5), which the driver does not serve, so hid-multitouch logs
"failed to fetch feature 68" once per probe. Touch works regardless.

Changes in v6:
- Take over the series and use Allen's full name (Krzysztof).
- dt-bindings: describe how reset-gpios differs from
  display-reset-gpios (Krzysztof).
- dt-bindings: use unevaluatedProperties and drop the properties that
  only repeated the referenced schemas.
- Squash the four driver patches and rewrite the driver: a standard
  module_spi_driver() with devm resources, no kerneldoc for standard
  functions, no __func__ in messages, no probe message, no CONFIG_OF
  conditionals or .owner, dev_err_probe() in probe (Krzysztof).
- Rename the driver to hid-himax-spi (CONFIG_HID_HIMAX_SPI), like
  hid-goodix-spi.
- Power the touch function up from a work item when the panel has been
  prepared, so loading the firmware doesn't delay the panel, and drop
  the unexplained 2 s and 1 s delays of the vendor code.
- Request and validate the firmware once and keep it, so resume doesn't
  need the file system. Check every offset and size taken from the
  image.
- Keep the HID device across power cycles instead of recreating it.
- Request the interrupt in probe, disabled until the HID device exists,
  and take its trigger from the device tree.
- Retry a failed power up when the panel is prepared again, and pass
  -EPROBE_DEFER from the panel lookup through.
- Size the heatmap buffers for an odd number of nodes, and use
  hid_safe_input_report().
- Use crc32c() instead of an open-coded CRC-32C.
- Only reset the IC when reading the event stack fails, not when the
  HID core rejects a report.

v5: https://lore.kernel.org/all/20261003142741.48634-1-michal@nozomi.space/
v4: https://lore.kernel.org/all/TY0PR06MB56110754EB9D827BE3304A259E0F2@TY0PR06MB5611.apcprd06.prod.outlook.com/




Michał Kopeć (3):
  dt-bindings: input: Add Himax HX83102J touchscreen
  HID: Add Himax HX83102J SPI touchscreen driver
  arm64: dts: mediatek: mt8188-geralt-ciri: Add the touchscreen

 .../input/touchscreen/himax,hx83102j.yaml     |   98 ++
 MAINTAINERS                                   |    7 +
 .../dts/mediatek/mt8188-geralt-ciri-sku1.dts  |    4 +
 .../dts/mediatek/mt8188-geralt-ciri-sku3.dts  |    4 +
 .../dts/mediatek/mt8188-geralt-ciri-sku5.dts  |    4 +
 .../dts/mediatek/mt8188-geralt-ciri-sku7.dts  |    4 +
 .../boot/dts/mediatek/mt8188-geralt-ciri.dtsi |   33 +
 drivers/hid/Kconfig                           |   13 +
 drivers/hid/Makefile                          |    1 +
 drivers/hid/hid-himax-spi.c                   | 1300 +++++++++++++++++
 10 files changed, 1468 insertions(+)
 create mode 100644 Documentation/devicetree/bindings/input/touchscreen/himax,hx83102j.yaml
 create mode 100644 drivers/hid/hid-himax-spi.c


base-commit: fd179f8a05be3ccae366b9b96e176b51fbe54aab
-- 
2.56.0


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

* [PATCH v6 1/3] dt-bindings: input: Add Himax HX83102J touchscreen
  2026-10-11 18:16 [PATCH v6 0/3] HID: Add support for the Himax HX83102J touchscreen Michał Kopeć
@ 2026-10-11 18:16 ` Michał Kopeć
  2026-10-11 18:16 ` [PATCH v6 2/3] HID: Add Himax HX83102J SPI touchscreen driver Michał Kopeć
  2026-10-11 18:16 ` [PATCH v6 3/3] arm64: dts: mediatek: mt8188-geralt-ciri: Add the touchscreen Michał Kopeć
  2 siblings, 0 replies; 5+ messages in thread
From: Michał Kopeć @ 2026-10-11 18:16 UTC (permalink / raw)
  To: linux-input
  Cc: devicetree, linux-mediatek, linux-arm-kernel, linux-kernel,
	jikos, bentiss, dmitry.torokhov, robh, krzk+dt, conor+dt,
	matthias.bgg, angelogioacchino.delregno, allencl_lin, tylor_yang,
	felix, dianders, Michał Kopeć

Add the binding for the touch function of the Himax HX83102J, a touch
and display driver integrated (TDDI) controller. The touch function is
connected over SPI and shares the analog supplies and the reset of the
display function, so it is powered and reset together with the panel
and has to follow it.

Co-developed-by: Allen Lin <allencl_lin@hotmail.com>
Signed-off-by: Allen Lin <allencl_lin@hotmail.com>
Signed-off-by: Michał Kopeć <michal@nozomi.space>
---
 .../input/touchscreen/himax,hx83102j.yaml     | 98 +++++++++++++++++++
 MAINTAINERS                                   |  6 ++
 2 files changed, 104 insertions(+)
 create mode 100644 Documentation/devicetree/bindings/input/touchscreen/himax,hx83102j.yaml

diff --git a/Documentation/devicetree/bindings/input/touchscreen/himax,hx83102j.yaml b/Documentation/devicetree/bindings/input/touchscreen/himax,hx83102j.yaml
new file mode 100644
index 0000000000000..f0aca7a6662c9
--- /dev/null
+++ b/Documentation/devicetree/bindings/input/touchscreen/himax,hx83102j.yaml
@@ -0,0 +1,98 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/input/touchscreen/himax,hx83102j.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Himax hx83102j touchscreen
+
+maintainers:
+  - Michał Kopeć <michal@nozomi.space>
+
+description:
+  The Himax HX83102J is a touch and display driver integrated (TDDI)
+  controller. Its touch function is connected over SPI and reports HID
+  packets. The display and touch functions share the analog supplies and
+  the display reset, so the touch function is powered and reset together
+  with the panel it is part of.
+
+allOf:
+  - $ref: /schemas/input/touchscreen/touchscreen.yaml#
+  - $ref: /schemas/spi/spi-peripheral-props.yaml#
+
+properties:
+  compatible:
+    const: himax,hx83102j
+
+  reg:
+    maxItems: 1
+
+  interrupts:
+    maxItems: 1
+
+  reset-gpios:
+    maxItems: 1
+    description:
+      Reset of the touch function. Unlike display-reset-gpios, it only
+      resets the touch MCU and leaves the display function running, so the
+      touch function can be reset without disturbing the panel.
+
+  vccd-supply:
+    description: Digital I/O supply of the touch function.
+
+  vsn-supply:
+    description: Negative analog supply, shared with the display function.
+
+  vsp-supply:
+    description: Positive analog supply, shared with the display function.
+
+  display-reset-gpios:
+    maxItems: 1
+    description:
+      Reset of the display function, usually driven by the panel. Asserting
+      it also resets the touch function.
+
+  spi-cpha: true
+
+  spi-cpol: true
+
+  firmware-name:
+    description: Specify the file name for firmware loading.
+
+required:
+  - compatible
+  - reg
+  - interrupts
+  - reset-gpios
+  - panel
+  - vccd-supply
+  - vsn-supply
+  - vsp-supply
+  - display-reset-gpios
+
+unevaluatedProperties: false
+
+examples:
+  - |
+    #include <dt-bindings/gpio/gpio.h>
+    #include <dt-bindings/interrupt-controller/irq.h>
+    spi {
+      #address-cells = <1>;
+      #size-cells = <0>;
+      ap_ts: touchscreen@0 {
+        compatible = "himax,hx83102j";
+        reg = <0>;
+        pinctrl-names = "default";
+        pinctrl-0 = <&touch_int0 &touch_reset>;
+        reset-gpios = <&gpio1 8 GPIO_ACTIVE_LOW>;
+        spi-cpha;
+        spi-cpol;
+        interrupt-parent = <&gpio1>;
+        interrupts = <7 IRQ_TYPE_LEVEL_LOW>;
+        panel = <&panel>;
+        vccd-supply = <&regulator>;
+        vsn-supply = <&regulator>;
+        vsp-supply = <&regulator>;
+        display-reset-gpios = <&gpio1 9 GPIO_ACTIVE_LOW>;
+      };
+    };
diff --git a/MAINTAINERS b/MAINTAINERS
index 72294ddfa5b75..e209f0d44fa73 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -11729,6 +11729,12 @@ W:	http://www.highpoint-tech.com
 F:	Documentation/scsi/hptiop.rst
 F:	drivers/scsi/hptiop.c
 
+HIMAX HID TOUCHSCREEN
+M:	Michał Kopeć <michal@nozomi.space>
+L:	linux-input@vger.kernel.org
+S:	Maintained
+F:	Documentation/devicetree/bindings/input/touchscreen/himax,hx83102j.yaml
+
 HIMAX HX83112B TOUCHSCREEN SUPPORT
 M:	Job Noorman <job@noorman.info>
 L:	linux-input@vger.kernel.org
-- 
2.56.0


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

* [PATCH v6 2/3] HID: Add Himax HX83102J SPI touchscreen driver
  2026-10-11 18:16 [PATCH v6 0/3] HID: Add support for the Himax HX83102J touchscreen Michał Kopeć
  2026-10-11 18:16 ` [PATCH v6 1/3] dt-bindings: input: Add " Michał Kopeć
@ 2026-10-11 18:16 ` Michał Kopeć
  2026-10-11 23:06   ` Uwe Kleine-König
  2026-10-11 18:16 ` [PATCH v6 3/3] arm64: dts: mediatek: mt8188-geralt-ciri: Add the touchscreen Michał Kopeć
  2 siblings, 1 reply; 5+ messages in thread
From: Michał Kopeć @ 2026-10-11 18:16 UTC (permalink / raw)
  To: linux-input
  Cc: devicetree, linux-mediatek, linux-arm-kernel, linux-kernel,
	jikos, bentiss, dmitry.torokhov, robh, krzk+dt, conor+dt,
	matthias.bgg, angelogioacchino.delregno, allencl_lin, tylor_yang,
	felix, dianders, Michał Kopeć

Add a driver for the touch function of the Himax HX83102J, a touch and
display driver integrated (TDDI) controller. The touch function is
connected over SPI and sends HID reports.

The flashless variant of the IC, used in the Lenovo Chromebook Duet,
loses its firmware when it is powered off. The driver loads it from the
file named by the firmware-name property, keeps it for the next power
up, and takes the HID descriptor and the report descriptor from it.

The touch function shares its power and reset with the display, so the
driver follows the panel: it powers the touch function up when the
panel has been prepared and down before the panel is unprepared. The
firmware is loaded from a work item so that it doesn't delay the panel,
and the HID device is kept across power cycles.

Every interrupt delivers a frame with the touch report, the stylus
report if the IC has a stylus function, and a heatmap. The IC packs the
heatmap as 12-bit values, while the report descriptor declares 16-bit
values, so the driver unpacks it before passing it on.

This is based on the driver that Allen Lin posted [1] and on Tylor
Yang's heatmap support in the ChromeOS kernel [2].

Link: https://lore.kernel.org/all/TY0PR06MB56110754EB9D827BE3304A259E0F2@TY0PR06MB5611.apcprd06.prod.outlook.com/ [1]
Link: https://chromium-review.googlesource.com/c/chromiumos/third_party/kernel/+/5326106 [2]
Co-developed-by: Allen Lin <allencl_lin@hotmail.com>
Signed-off-by: Allen Lin <allencl_lin@hotmail.com>
Co-developed-by: Tylor Yang <tylor_yang@himax.corp-partner.google.com>
Signed-off-by: Tylor Yang <tylor_yang@himax.corp-partner.google.com>
Signed-off-by: Michał Kopeć <michal@nozomi.space>
---
 MAINTAINERS                 |    1 +
 drivers/hid/Kconfig         |   13 +
 drivers/hid/Makefile        |    1 +
 drivers/hid/hid-himax-spi.c | 1300 +++++++++++++++++++++++++++++++++++
 4 files changed, 1315 insertions(+)
 create mode 100644 drivers/hid/hid-himax-spi.c

diff --git a/MAINTAINERS b/MAINTAINERS
index e209f0d44fa73..3893cf3122ecf 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -11734,6 +11734,7 @@ M:	Michał Kopeć <michal@nozomi.space>
 L:	linux-input@vger.kernel.org
 S:	Maintained
 F:	Documentation/devicetree/bindings/input/touchscreen/himax,hx83102j.yaml
+F:	drivers/hid/hid-himax-spi.c
 
 HIMAX HX83112B TOUCHSCREEN SUPPORT
 M:	Job Noorman <job@noorman.info>
diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig
index c43e824428f02..6675e060e993c 100644
--- a/drivers/hid/Kconfig
+++ b/drivers/hid/Kconfig
@@ -412,6 +412,19 @@ config HID_GLORIOUS
 	  Support for Glorious PC Gaming Race mice such as
 	  the Glorious Model O, O- and D.
 
+config HID_HIMAX_SPI
+	tristate "Himax HX83102J SPI HID touchscreen"
+	depends on SPI
+	depends on DRM
+	select CRC32
+	help
+	  Say Y here if you have a Himax HX83102J touch and display driver
+	  integrated (TDDI) controller whose touch function is connected
+	  over SPI.
+
+	  To compile this driver as a module, choose M here: the
+	  module will be called hid-himax-spi.
+
 config HID_HOLTEK
 	tristate "Holtek HID devices"
 	depends on USB_HID
diff --git a/drivers/hid/Makefile b/drivers/hid/Makefile
index 48a863b245eed..fc978c61d5b78 100644
--- a/drivers/hid/Makefile
+++ b/drivers/hid/Makefile
@@ -63,6 +63,7 @@ obj-$(CONFIG_HID_GOOGLE_STADIA_FF)	+= hid-google-stadiaff.o
 obj-$(CONFIG_HID_VIVALDI)	+= hid-vivaldi.o
 obj-$(CONFIG_HID_GT683R)	+= hid-gt683r.o
 obj-$(CONFIG_HID_GYRATION)	+= hid-gyration.o
+obj-$(CONFIG_HID_HIMAX_SPI)	+= hid-himax-spi.o
 obj-$(CONFIG_HID_HOLTEK)	+= hid-holtek-kbd.o
 obj-$(CONFIG_HID_HOLTEK)	+= hid-holtek-mouse.o
 obj-$(CONFIG_HID_HOLTEK)	+= hid-holtekff.o
diff --git a/drivers/hid/hid-himax-spi.c b/drivers/hid/hid-himax-spi.c
new file mode 100644
index 0000000000000..98ee51658b362
--- /dev/null
+++ b/drivers/hid/hid-himax-spi.c
@@ -0,0 +1,1300 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * HID over SPI driver for the touch function of the Himax HX83102J
+ *
+ * Copyright (C) 2024 Himax Corporation.
+ * Copyright (C) 2026 Michał Kopeć <michal@nozomi.space>
+ */
+
+#include <drm/drm_panel.h>
+#include <linux/bitfield.h>
+#include <linux/bits.h>
+#include <linux/cleanup.h>
+#include <linux/crc32.h>
+#include <linux/delay.h>
+#include <linux/device.h>
+#include <linux/firmware.h>
+#include <linux/gpio/consumer.h>
+#include <linux/hid.h>
+#include <linux/interrupt.h>
+#include <linux/minmax.h>
+#include <linux/mod_devicetable.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/property.h>
+#include <linux/regulator/consumer.h>
+#include <linux/sizes.h>
+#include <linux/spi/spi.h>
+#include <linux/string.h>
+#include <linux/unaligned.h>
+#include <linux/workqueue.h>
+
+#define HIMAX_SPI_READ			0xf3
+#define HIMAX_SPI_WRITE			0xf2
+/* Reads have a dummy byte after the register */
+#define HIMAX_SPI_READ_HDR_LEN		3
+#define HIMAX_SPI_WRITE_HDR_LEN		2
+
+/* Bus interface registers */
+#define HIMAX_BUS_AHB_ADDR		0x00
+#define HIMAX_BUS_AHB_RDATA		0x08
+#define HIMAX_BUS_AHB_DIRECTION		0x0c
+#define HIMAX_BUS_AHB_INCR4		0x0d
+#define HIMAX_BUS_AHB_CONTI		0x13
+#define HIMAX_BUS_EVENT_STACK		0x30
+#define HIMAX_BUS_SAFE_MODE		0x31
+
+#define HIMAX_AHB_DIRECTION_READ	0x00
+#define HIMAX_AHB_CONTI			0x31
+#define HIMAX_AHB_INCR4			0x10
+#define HIMAX_AHB_INCR4_AUTO		BIT(0)
+#define HIMAX_AHB_MAX_XFER		SZ_4K
+
+#define HIMAX_SAFE_MODE_PASSWORD	0x9527
+
+/* MCU registers */
+#define HX83102J_REG_HW_CRC		0x80010000
+#define HX83102J_REG_TCON_RST		0x80020004
+#define HIMAX_REG_RELOAD_STATUS		0x80050000
+#define HIMAX_REG_RELOAD_CRC32		0x80050018
+#define HIMAX_REG_RELOAD_ADDR		0x80050020
+#define HIMAX_REG_RELOAD_CMD		0x80050028
+#define HIMAX_REG_SYSTEM_RESET		0x90000018
+#define HIMAX_REG_RELOAD_TO_ACTIVE	0x90000048
+#define HIMAX_REG_CTRL_FW		0x9000005c
+#define HIMAX_REG_FW_STATUS		0x900000a8
+#define HIMAX_REG_ICID			0x900000d0
+
+#define HX83102J_HW_CRC_ENABLE		0xecce
+#define HIMAX_RELOAD_BUSY		BIT(0)
+#define HIMAX_RELOAD_CMD_PASSWORD	GENMASK(31, 16)
+#define HIMAX_RELOAD_CMD_WORDS		GENMASK(15, 0)
+#define HIMAX_RELOAD_PASSWORD		0x99
+#define HIMAX_SYSTEM_RESET		0x55
+#define HIMAX_RELOAD_TO_ACTIVE		0xec
+#define HIMAX_RELOAD_TO_ACTIVE_DONE	0x01ec
+#define HIMAX_FW_RE_INIT		0x00
+#define HIMAX_FW_STATE			GENMASK(7, 0)
+#define HIMAX_FW_STATE_SAFE_MODE	0x0c
+#define HIMAX_ICID_MASK			GENMASK(31, 8)
+#define HX83102J_ICID			0x83102900
+
+/* Data RAM, set up by the driver or filled in by the firmware */
+#define HIMAX_DSRAM_FW_VER		0x10007004
+#define HIMAX_DSRAM_RX_TX_NUM		0x100070f4
+#define HIMAX_DSRAM_MAX_POINTS		0x100070f8
+#define HIMAX_DSRAM_STYLUS		0x1000719c
+#define HIMAX_DSRAM_SET_NFRAME		0x10007294
+#define HIMAX_DSRAM_FW_RELOAD		0x100072c0
+#define HX83102J_DSRAM_RAW_OUT_SEL	0x100072ec
+#define HIMAX_DSRAM_FLASH_RELOAD	0x10007f00
+#define HIMAX_DSRAM_SORTING_MODE	0x10007f04
+#define HIMAX_DSRAM_AP_SUSPEND		0x10007fd0
+
+#define HIMAX_RX_NUM			GENMASK(23, 16)
+#define HIMAX_TX_NUM			GENMASK(31, 24)
+#define HIMAX_MAX_POINTS		GENMASK(7, 0)
+#define HIMAX_STYLUS			GENMASK(31, 24)
+#define HIMAX_FW_RELOAD_DONE		0x72c0
+#define HIMAX_NO_FLASH			0x9aa9
+#define HIMAX_AP_RESUMED		0x00000000
+
+#define HX83102J_DSRAM_SIZE		(SZ_64K + SZ_8K)
+#define HX83102J_MAIN_CODE_MAX		SZ_128K
+#define HX83102J_MAX_RX			50
+#define HX83102J_MAX_TX			32
+
+/* Firmware image: a 1 KiB header with a map of the tables in the image */
+#define HIMAX_FW_HEADER_SIZE		SZ_1K
+#define HIMAX_FW_HEADER_ID_OFFSET	0x0e
+#define HIMAX_FW_HEADER_ID		0x87
+#define HIMAX_FW_MAP_ENTRY_SIZE		16
+#define HIMAX_FW_MAP_CFG_TABLE		0x00000a00
+#define HIMAX_FW_MAP_HID_TABLE		0x30000100
+#define HIMAX_FW_PART_DESC_SIZE		16
+#define HIMAX_FW_PART_COUNT_OFFSET	12
+
+/* Every report in the event stack has a 2 byte header */
+#define HIMAX_REPORT_HDR_SIZE		2
+#define HIMAX_REPORT_MAX_INPUT		128
+/* The heatmap report starts with its ID and 20 bytes of frame information */
+#define HIMAX_HEATMAP_HDR_SIZE		(1 + 20)
+#define HIMAX_REPORT_ID_CONTACT_MAX	0x03
+
+#define HIMAX_XFER_BUF_SIZE		(HIMAX_AHB_MAX_XFER + 8)
+
+struct himax_hid_desc {
+	__le16 desc_length;
+	__le16 bcd_version;
+	__le16 report_desc_length;
+	__le16 max_input_length;
+	__le16 max_output_length;
+	__le16 max_fragment_length;
+	__le16 vendor_id;
+	__le16 product_id;
+	__le16 version_id;
+	__le16 flags;
+	__le32 reserved;
+} __packed;
+
+struct himax_fw_part {
+	u32 sram_addr;
+	u32 offset;
+	u32 size;
+};
+
+struct himax_ts_data {
+	struct device *dev;
+	struct spi_device *spi;
+	struct hid_device *hid;
+	struct gpio_desc *reset_gpio;
+	struct regulator *vccd;
+	struct drm_panel_follower panel_follower;
+	struct work_struct power_on_work;
+
+	/* Serializes power changes, firmware loading and HID registration */
+	struct mutex lock;
+	bool powered;
+	bool irq_enabled;
+
+	const char *fw_name;
+	const struct firmware *fw;
+	const struct himax_hid_desc *hid_desc;
+	const u8 *report_desc;
+	u16 report_desc_len;
+	u16 max_input_len;
+	struct himax_fw_part main_code;
+	u32 cfg_addr;
+	u32 cfg_size;
+	u32 cfg_crc;
+	u8 *cfg_image;
+
+	u32 rx_num;
+	u32 tx_num;
+	u8 max_points;
+	bool has_stylus;
+
+	u8 *event_buf;
+	size_t event_size;
+	u8 *heatmap_buf;
+	size_t heatmap_size;
+
+	/* Serializes bus access, which also uses the transfer buffers */
+	struct mutex xfer_lock;
+	size_t xfer_max;
+	u8 tx_buf[HIMAX_XFER_BUF_SIZE] ____cacheline_aligned;
+	u8 rx_buf[HIMAX_XFER_BUF_SIZE] ____cacheline_aligned;
+};
+
+static int himax_bus_read(struct himax_ts_data *ts, u8 reg, u8 *buf, size_t len)
+{
+	struct spi_transfer xfer = {
+		.tx_buf = ts->tx_buf,
+		.rx_buf = ts->rx_buf,
+		.len = HIMAX_SPI_READ_HDR_LEN + len,
+	};
+	int ret;
+
+	lockdep_assert_held(&ts->xfer_lock);
+
+	ts->tx_buf[0] = HIMAX_SPI_READ;
+	ts->tx_buf[1] = reg;
+	memset(ts->tx_buf + 2, 0, len + 1);
+
+	ret = spi_sync_transfer(ts->spi, &xfer, 1);
+	if (ret)
+		return ret;
+
+	memcpy(buf, ts->rx_buf + HIMAX_SPI_READ_HDR_LEN, len);
+
+	return 0;
+}
+
+/* Sends @len bytes of data, which the caller has put after the header */
+static int himax_bus_send(struct himax_ts_data *ts, u8 reg, size_t len)
+{
+	struct spi_transfer xfer = {
+		.tx_buf = ts->tx_buf,
+		.len = HIMAX_SPI_WRITE_HDR_LEN + len,
+	};
+
+	lockdep_assert_held(&ts->xfer_lock);
+
+	ts->tx_buf[0] = HIMAX_SPI_WRITE;
+	ts->tx_buf[1] = reg;
+
+	return spi_sync_transfer(ts->spi, &xfer, 1);
+}
+
+static int himax_bus_write8(struct himax_ts_data *ts, u8 reg, u8 val)
+{
+	ts->tx_buf[HIMAX_SPI_WRITE_HDR_LEN] = val;
+
+	return himax_bus_send(ts, reg, 1);
+}
+
+static int himax_ahb_set_burst(struct himax_ts_data *ts, bool auto_incr)
+{
+	int ret;
+
+	ret = himax_bus_write8(ts, HIMAX_BUS_AHB_CONTI, HIMAX_AHB_CONTI);
+	if (ret)
+		return ret;
+
+	return himax_bus_write8(ts, HIMAX_BUS_AHB_INCR4,
+				HIMAX_AHB_INCR4 | (auto_incr ? HIMAX_AHB_INCR4_AUTO : 0));
+}
+
+static int himax_ahb_read(struct himax_ts_data *ts, u32 addr, u8 *buf, size_t len)
+{
+	size_t chunk = min(HIMAX_AHB_MAX_XFER, ts->xfer_max - HIMAX_SPI_READ_HDR_LEN);
+	size_t pos, n;
+	int ret;
+
+	guard(mutex)(&ts->xfer_lock);
+
+	ret = himax_ahb_set_burst(ts, len > sizeof(u32));
+	if (ret)
+		return ret;
+
+	for (pos = 0; pos < len; pos += n) {
+		n = min(len - pos, chunk);
+
+		put_unaligned_le32(addr + pos, ts->tx_buf + HIMAX_SPI_WRITE_HDR_LEN);
+		ret = himax_bus_send(ts, HIMAX_BUS_AHB_ADDR, sizeof(u32));
+		if (ret)
+			return ret;
+
+		ret = himax_bus_write8(ts, HIMAX_BUS_AHB_DIRECTION, HIMAX_AHB_DIRECTION_READ);
+		if (ret)
+			return ret;
+
+		ret = himax_bus_read(ts, HIMAX_BUS_AHB_RDATA, buf + pos, n);
+		if (ret)
+			return ret;
+	}
+
+	return 0;
+}
+
+static int himax_ahb_write(struct himax_ts_data *ts, u32 addr, const u8 *buf, size_t len)
+{
+	size_t chunk = min(HIMAX_AHB_MAX_XFER,
+			   ts->xfer_max - HIMAX_SPI_WRITE_HDR_LEN - sizeof(u32));
+	u8 *data = ts->tx_buf + HIMAX_SPI_WRITE_HDR_LEN;
+	size_t pos, n;
+	int ret;
+
+	guard(mutex)(&ts->xfer_lock);
+
+	ret = himax_ahb_set_burst(ts, len > sizeof(u32));
+	if (ret)
+		return ret;
+
+	for (pos = 0; pos < len; pos += n) {
+		n = min(len - pos, chunk);
+
+		put_unaligned_le32(addr + pos, data);
+		memcpy(data + sizeof(u32), buf + pos, n);
+		ret = himax_bus_send(ts, HIMAX_BUS_AHB_ADDR, sizeof(u32) + n);
+		if (ret)
+			return ret;
+	}
+
+	return 0;
+}
+
+static int himax_read32(struct himax_ts_data *ts, u32 addr, u32 *val)
+{
+	__le32 data;
+	int ret;
+
+	ret = himax_ahb_read(ts, addr, (u8 *)&data, sizeof(data));
+	if (ret)
+		return ret;
+
+	*val = le32_to_cpu(data);
+
+	return 0;
+}
+
+static int himax_write32(struct himax_ts_data *ts, u32 addr, u32 val)
+{
+	__le32 data = cpu_to_le32(val);
+
+	return himax_ahb_write(ts, addr, (u8 *)&data, sizeof(data));
+}
+
+static int himax_write32_verify(struct himax_ts_data *ts, u32 addr, u32 val,
+				u32 mask, unsigned int tries)
+{
+	u32 rval;
+	int ret;
+
+	while (tries--) {
+		ret = himax_write32(ts, addr, val);
+		if (ret)
+			return ret;
+
+		fsleep(1000);
+
+		ret = himax_read32(ts, addr, &rval);
+		if (ret)
+			return ret;
+
+		if ((rval & mask) == (val & mask))
+			return 0;
+	}
+
+	return -EIO;
+}
+
+static int himax_enter_safe_mode(struct himax_ts_data *ts)
+{
+	guard(mutex)(&ts->xfer_lock);
+
+	put_unaligned_le16(HIMAX_SAFE_MODE_PASSWORD, ts->tx_buf + HIMAX_SPI_WRITE_HDR_LEN);
+
+	return himax_bus_send(ts, HIMAX_BUS_SAFE_MODE, sizeof(u16));
+}
+
+static void himax_pin_reset(struct himax_ts_data *ts)
+{
+	gpiod_set_value_cansleep(ts->reset_gpio, 1);
+	fsleep(10000);
+	gpiod_set_value_cansleep(ts->reset_gpio, 0);
+	fsleep(20000);
+}
+
+static int himax_interface_on(struct himax_ts_data *ts)
+{
+	unsigned int tries = 10;
+	u8 buf[4], conti, incr4;
+	int ret;
+
+	guard(mutex)(&ts->xfer_lock);
+
+	/* The first access after a reset only wakes up the bus interface */
+	ret = himax_bus_read(ts, HIMAX_BUS_AHB_RDATA, buf, sizeof(buf));
+	if (ret)
+		return ret;
+
+	while (tries--) {
+		ret = himax_ahb_set_burst(ts, false);
+		if (ret)
+			return ret;
+
+		ret = himax_bus_read(ts, HIMAX_BUS_AHB_CONTI, &conti, 1);
+		if (ret)
+			return ret;
+
+		ret = himax_bus_read(ts, HIMAX_BUS_AHB_INCR4, &incr4, 1);
+		if (ret)
+			return ret;
+
+		if (conti == HIMAX_AHB_CONTI && incr4 == HIMAX_AHB_INCR4)
+			return 0;
+
+		fsleep(1000);
+	}
+
+	return -EIO;
+}
+
+static int himax_enable_hw_crc(struct himax_ts_data *ts)
+{
+	return himax_write32_verify(ts, HX83102J_REG_HW_CRC, HX83102J_HW_CRC_ENABLE,
+				    GENMASK(15, 0), 5);
+}
+
+static int himax_reload_to_active(struct himax_ts_data *ts)
+{
+	unsigned int tries = 5;
+	u32 val;
+	int ret;
+
+	while (tries--) {
+		ret = himax_write32(ts, HIMAX_REG_RELOAD_TO_ACTIVE, HIMAX_RELOAD_TO_ACTIVE);
+		if (ret)
+			return ret;
+
+		fsleep(1000);
+
+		ret = himax_read32(ts, HIMAX_REG_RELOAD_TO_ACTIVE, &val);
+		if (ret)
+			return ret;
+
+		if ((val & GENMASK(15, 0)) == HIMAX_RELOAD_TO_ACTIVE_DONE)
+			return 0;
+	}
+
+	return -EIO;
+}
+
+static int himax_sense_off(struct himax_ts_data *ts)
+{
+	unsigned int tries = 5;
+	u32 val;
+	int ret;
+
+	while (tries--) {
+		ret = himax_enter_safe_mode(ts);
+		if (ret)
+			return ret;
+
+		ret = himax_read32(ts, HIMAX_REG_FW_STATUS, &val);
+		if (ret)
+			return ret;
+
+		if (FIELD_GET(HIMAX_FW_STATE, val) == HIMAX_FW_STATE_SAFE_MODE) {
+			/* The touch MCU can't be used before the TCON is reset */
+			ret = himax_write32(ts, HX83102J_REG_TCON_RST, 0);
+			if (ret)
+				return ret;
+
+			fsleep(1000);
+			return 0;
+		}
+
+		fsleep(5000);
+		himax_pin_reset(ts);
+	}
+
+	return -EIO;
+}
+
+static int himax_sense_on(struct himax_ts_data *ts)
+{
+	int ret;
+
+	ret = himax_interface_on(ts);
+	if (ret)
+		return ret;
+
+	ret = himax_write32(ts, HIMAX_REG_CTRL_FW, HIMAX_FW_RE_INIT);
+	if (ret)
+		return ret;
+
+	fsleep(10000);
+	himax_pin_reset(ts);
+
+	ret = himax_enable_hw_crc(ts);
+	if (ret)
+		return ret;
+
+	return himax_reload_to_active(ts);
+}
+
+static int himax_detect(struct himax_ts_data *ts)
+{
+	unsigned int tries = 5;
+	u32 id;
+	int ret;
+
+	himax_pin_reset(ts);
+
+	ret = himax_interface_on(ts);
+	if (ret)
+		return ret;
+
+	ret = himax_sense_off(ts);
+	if (ret)
+		return ret;
+
+	/* The ID may not be readable yet while the display function starts up */
+	while (tries--) {
+		ret = himax_read32(ts, HIMAX_REG_ICID, &id);
+		if (ret)
+			return ret;
+
+		if ((id & HIMAX_ICID_MASK) == HX83102J_ICID)
+			return 0;
+	}
+
+	dev_err(ts->dev, "unexpected IC ID %#010x\n", id);
+
+	return -ENODEV;
+}
+
+static int himax_hw_crc(struct himax_ts_data *ts, u32 addr, u32 len, u32 *crc)
+{
+	u32 words = len / sizeof(u32);
+	unsigned int tries = 100;
+	u32 val;
+	int ret;
+
+	ret = himax_write32(ts, HIMAX_REG_RELOAD_ADDR, addr);
+	if (ret)
+		return ret;
+
+	ret = himax_write32(ts, HIMAX_REG_RELOAD_CMD,
+			    FIELD_PREP(HIMAX_RELOAD_CMD_PASSWORD, HIMAX_RELOAD_PASSWORD) |
+			    FIELD_PREP(HIMAX_RELOAD_CMD_WORDS, words));
+	if (ret)
+		return ret;
+
+	ret = himax_read32(ts, HIMAX_REG_RELOAD_CMD, &val);
+	if (ret)
+		return ret;
+
+	if (FIELD_GET(HIMAX_RELOAD_CMD_WORDS, val) != words)
+		return -EIO;
+
+	while (tries--) {
+		ret = himax_read32(ts, HIMAX_REG_RELOAD_STATUS, &val);
+		if (ret)
+			return ret;
+
+		if (!(val & HIMAX_RELOAD_BUSY))
+			return himax_read32(ts, HIMAX_REG_RELOAD_CRC32, crc);
+
+		fsleep(1000);
+	}
+
+	return -ETIMEDOUT;
+}
+
+static int himax_write_sram(struct himax_ts_data *ts, u32 addr, const u8 *data,
+			    u32 len, u32 expected_crc)
+{
+	unsigned int tries = 3;
+	u32 crc;
+	int ret;
+
+	while (tries--) {
+		ret = himax_ahb_write(ts, addr, data, len);
+		if (ret)
+			return ret;
+
+		ret = himax_hw_crc(ts, addr, len, &crc);
+		if (ret)
+			return ret;
+
+		if (crc == expected_crc)
+			return 0;
+	}
+
+	dev_err(ts->dev, "CRC mismatch at %#010x: %#010x != %#010x\n",
+		addr, crc, expected_crc);
+
+	return -EIO;
+}
+
+static int himax_start_fw(struct himax_ts_data *ts)
+{
+	unsigned int tries = 30;
+	u32 val;
+	int ret;
+
+	ret = himax_write32(ts, HX83102J_DSRAM_RAW_OUT_SEL, 0);
+	if (ret)
+		return ret;
+
+	ret = himax_write32_verify(ts, HIMAX_DSRAM_SORTING_MODE, 0, U32_MAX, 3);
+	if (ret)
+		return ret;
+
+	ret = himax_write32(ts, HIMAX_DSRAM_SET_NFRAME, 1);
+	if (ret)
+		return ret;
+
+	ret = himax_write32(ts, HIMAX_DSRAM_FW_RELOAD, 0);
+	if (ret)
+		return ret;
+
+	ret = himax_sense_on(ts);
+	if (ret)
+		return ret;
+
+	while (tries--) {
+		ret = himax_read32(ts, HIMAX_DSRAM_FW_RELOAD, &val);
+		if (ret)
+			return ret;
+
+		if (val == HIMAX_FW_RELOAD_DONE)
+			return 0;
+
+		fsleep(10000);
+	}
+
+	return -ETIMEDOUT;
+}
+
+/* Only the flashless variant is supported, which loses the firmware at power off */
+static int himax_load_fw(struct himax_ts_data *ts)
+{
+	const struct himax_fw_part *main_code = &ts->main_code;
+	unsigned int tries = 3;
+	int ret;
+
+	while (tries--) {
+		ret = himax_write32(ts, HIMAX_REG_SYSTEM_RESET, HIMAX_SYSTEM_RESET);
+		if (ret)
+			continue;
+
+		ret = himax_sense_off(ts);
+		if (ret)
+			continue;
+
+		ret = himax_enable_hw_crc(ts);
+		if (ret)
+			continue;
+
+		/* The main code includes its own CRC, so the CRC over it is 0 */
+		ret = himax_write_sram(ts, main_code->sram_addr,
+				       ts->fw->data + main_code->offset,
+				       main_code->size, 0);
+		if (ret)
+			continue;
+
+		ret = himax_write_sram(ts, ts->cfg_addr, ts->cfg_image,
+				       ts->cfg_size, ts->cfg_crc);
+		if (!ret)
+			break;
+	}
+	if (ret)
+		return ret;
+
+	ret = himax_write32(ts, HIMAX_DSRAM_FLASH_RELOAD, HIMAX_NO_FLASH);
+	if (ret)
+		return ret;
+
+	ret = himax_start_fw(ts);
+	if (ret)
+		return ret;
+
+	if (himax_write32_verify(ts, HIMAX_DSRAM_AP_SUSPEND, HIMAX_AP_RESUMED, U32_MAX, 10))
+		dev_dbg(ts->dev, "failed to notify the firmware of the resume\n");
+
+	return 0;
+}
+
+static int himax_parse_fw_map(struct himax_ts_data *ts, u32 *cfg_table, u32 *hid_table)
+{
+	const struct firmware *fw = ts->fw;
+	const u8 *entry;
+	size_t pos, i;
+	u8 sum;
+
+	if (fw->size < HIMAX_FW_HEADER_SIZE || memchr_inv(fw->data, 0, 8) ||
+	    fw->data[HIMAX_FW_HEADER_ID_OFFSET] != HIMAX_FW_HEADER_ID)
+		return -EINVAL;
+
+	*cfg_table = 0;
+	*hid_table = 0;
+
+	for (pos = 0; pos < HIMAX_FW_HEADER_SIZE; pos += HIMAX_FW_MAP_ENTRY_SIZE) {
+		entry = fw->data + pos;
+		if (!memchr_inv(entry, 0, HIMAX_FW_MAP_ENTRY_SIZE))
+			break;
+
+		for (sum = 0, i = 0; i < HIMAX_FW_MAP_ENTRY_SIZE; i++)
+			sum += entry[i];
+		if (sum) {
+			dev_warn(ts->dev, "bad checksum of map entry at %#zx\n", pos);
+			continue;
+		}
+
+		switch (get_unaligned_le32(entry)) {
+		case HIMAX_FW_MAP_CFG_TABLE:
+			*cfg_table = get_unaligned_le32(entry + 4);
+			break;
+		case HIMAX_FW_MAP_HID_TABLE:
+			*hid_table = get_unaligned_le32(entry + 4);
+			break;
+		}
+	}
+
+	if (!*cfg_table || !*hid_table)
+		return -EINVAL;
+
+	return 0;
+}
+
+static int himax_parse_hid_desc(struct himax_ts_data *ts, u32 hid_table)
+{
+	const struct firmware *fw = ts->fw;
+	const struct himax_hid_desc *desc;
+	size_t rd_offset;
+
+	if (hid_table > fw->size || fw->size - hid_table < sizeof(*desc))
+		return -EINVAL;
+
+	desc = (const struct himax_hid_desc *)(fw->data + hid_table);
+	ts->max_input_len = le16_to_cpu(desc->max_input_length);
+	if (ts->max_input_len <= HIMAX_REPORT_HDR_SIZE ||
+	    ts->max_input_len > HIMAX_REPORT_MAX_INPUT)
+		return -EINVAL;
+
+	/* The report descriptor follows the HID descriptor */
+	rd_offset = hid_table + sizeof(*desc);
+	ts->report_desc_len = le16_to_cpu(desc->report_desc_length);
+	if (!ts->report_desc_len || ts->report_desc_len > HID_MAX_DESCRIPTOR_SIZE ||
+	    rd_offset > fw->size || fw->size - rd_offset < ts->report_desc_len)
+		return -EINVAL;
+
+	ts->hid_desc = desc;
+	ts->report_desc = fw->data + rd_offset;
+
+	return 0;
+}
+
+static int himax_get_fw_part(struct himax_ts_data *ts, u32 table, unsigned int index,
+			     struct himax_fw_part *part)
+{
+	const u8 *desc = ts->fw->data + table + index * HIMAX_FW_PART_DESC_SIZE;
+	size_t fw_size = ts->fw->size;
+	u32 misalign;
+
+	part->sram_addr = get_unaligned_le32(desc);
+	part->size = get_unaligned_le32(desc + 4);
+	part->offset = get_unaligned_le32(desc + 8);
+
+	/* The config partitions are written in whole words */
+	misalign = index ? part->sram_addr % sizeof(u32) : 0;
+	if (part->offset < misalign)
+		return -EINVAL;
+	part->sram_addr -= misalign;
+	part->offset -= misalign;
+	part->size += misalign;
+
+	if (!part->size || part->offset > fw_size || fw_size - part->offset < part->size)
+		return -EINVAL;
+
+	return 0;
+}
+
+/*
+ * Partition 0 is the main code. The others are pieces of the configuration in
+ * the data RAM, which are merged into one image that is written at once.
+ */
+static int himax_parse_fw_parts(struct himax_ts_data *ts, u32 table)
+{
+	const struct firmware *fw = ts->fw;
+	struct himax_fw_part part;
+	u32 start = U32_MAX;
+	unsigned int count, i;
+	u64 end = 0;
+	int ret;
+
+	if (table >= fw->size || fw->size - table <= HIMAX_FW_PART_COUNT_OFFSET)
+		return -EINVAL;
+
+	count = fw->data[table + HIMAX_FW_PART_COUNT_OFFSET];
+	if (count < 2 || fw->size - table < count * HIMAX_FW_PART_DESC_SIZE)
+		return -EINVAL;
+
+	ret = himax_get_fw_part(ts, table, 0, &ts->main_code);
+	if (ret)
+		return ret;
+
+	if (ts->main_code.size > HX83102J_MAIN_CODE_MAX)
+		return -EINVAL;
+
+	for (i = 1; i < count; i++) {
+		ret = himax_get_fw_part(ts, table, i, &part);
+		if (ret)
+			return ret;
+
+		start = min(start, part.sram_addr);
+		end = max(end, (u64)part.sram_addr + part.size);
+	}
+
+	if (end - start > HX83102J_DSRAM_SIZE)
+		return -EINVAL;
+
+	ts->cfg_addr = start;
+	ts->cfg_size = round_up(end - start, sizeof(u32));
+	ts->cfg_image = devm_kzalloc(ts->dev, ts->cfg_size, GFP_KERNEL);
+	if (!ts->cfg_image)
+		return -ENOMEM;
+
+	for (i = 1; i < count; i++) {
+		himax_get_fw_part(ts, table, i, &part);
+		memcpy(ts->cfg_image + part.sram_addr - start, fw->data + part.offset,
+		       part.size);
+	}
+
+	ts->cfg_crc = crc32c(~0, ts->cfg_image, ts->cfg_size);
+
+	return 0;
+}
+
+static int himax_request_fw(struct himax_ts_data *ts)
+{
+	u32 cfg_table, hid_table;
+	int ret;
+
+	ret = request_firmware(&ts->fw, ts->fw_name, ts->dev);
+	if (ret)
+		return ret;
+
+	ret = himax_parse_fw_map(ts, &cfg_table, &hid_table);
+	if (!ret)
+		ret = himax_parse_hid_desc(ts, hid_table);
+	if (!ret)
+		ret = himax_parse_fw_parts(ts, cfg_table);
+	if (ret) {
+		dev_err(ts->dev, "invalid firmware %s\n", ts->fw_name);
+		release_firmware(ts->fw);
+		ts->fw = NULL;
+	}
+
+	return ret;
+}
+
+static int himax_hid_parse(struct hid_device *hid)
+{
+	struct himax_ts_data *ts = hid->driver_data;
+
+	return hid_parse_report(hid, ts->report_desc, ts->report_desc_len);
+}
+
+static int himax_hid_start(struct hid_device *hid)
+{
+	return 0;
+}
+
+static void himax_hid_stop(struct hid_device *hid)
+{
+}
+
+static int himax_hid_open(struct hid_device *hid)
+{
+	return 0;
+}
+
+static void himax_hid_close(struct hid_device *hid)
+{
+}
+
+static int himax_hid_raw_request(struct hid_device *hid, unsigned char reportnum,
+				 u8 *buf, size_t len, unsigned char rtype, int reqtype)
+{
+	struct himax_ts_data *ts = hid->driver_data;
+
+	/* The only feature report the driver serves is the maximum contact count */
+	if (reqtype != HID_REQ_GET_REPORT || reportnum != HIMAX_REPORT_ID_CONTACT_MAX ||
+	    len < 2)
+		return -EINVAL;
+
+	buf[0] = reportnum;
+	buf[1] = ts->max_points;
+
+	return len;
+}
+
+static const struct hid_ll_driver himax_hid_ll_driver = {
+	.parse = himax_hid_parse,
+	.start = himax_hid_start,
+	.stop = himax_hid_stop,
+	.open = himax_hid_open,
+	.close = himax_hid_close,
+	.raw_request = himax_hid_raw_request,
+};
+
+static int himax_read_ic_info(struct himax_ts_data *ts)
+{
+	u8 ver[4];
+	u32 val;
+	int ret;
+
+	ret = himax_read32(ts, HIMAX_DSRAM_RX_TX_NUM, &val);
+	if (ret)
+		return ret;
+
+	ts->rx_num = FIELD_GET(HIMAX_RX_NUM, val);
+	ts->tx_num = FIELD_GET(HIMAX_TX_NUM, val);
+	if (!ts->rx_num || ts->rx_num > HX83102J_MAX_RX ||
+	    !ts->tx_num || ts->tx_num > HX83102J_MAX_TX) {
+		dev_err(ts->dev, "invalid sensor size %ux%u\n", ts->rx_num, ts->tx_num);
+		return -EINVAL;
+	}
+
+	ret = himax_read32(ts, HIMAX_DSRAM_MAX_POINTS, &val);
+	if (ret)
+		return ret;
+
+	ts->max_points = FIELD_GET(HIMAX_MAX_POINTS, val);
+
+	ret = himax_read32(ts, HIMAX_DSRAM_STYLUS, &val);
+	if (ret)
+		return ret;
+
+	ts->has_stylus = FIELD_GET(HIMAX_STYLUS, val);
+
+	ret = himax_ahb_read(ts, HIMAX_DSRAM_FW_VER, ver, sizeof(ver));
+	if (ret)
+		return ret;
+
+	dev_dbg(ts->dev, "firmware %02x%02x, %ux%u nodes, %u contacts%s\n",
+		ver[1], ver[2], ts->rx_num, ts->tx_num, ts->max_points,
+		ts->has_stylus ? ", stylus" : "");
+
+	return 0;
+}
+
+/*
+ * Every frame in the event stack holds the touch report, the stylus report if
+ * the IC has a stylus function, and a heatmap. The IC packs the heatmap as
+ * 12-bit values, while the report descriptor declares 16-bit values.
+ */
+static int himax_alloc_report_bufs(struct himax_ts_data *ts)
+{
+	u32 nodes = ts->rx_num * ts->tx_num;
+	unsigned int reports = ts->has_stylus ? 2 : 1;
+	size_t event_size, heatmap_size;
+	u8 *buf;
+
+	event_size = reports * ts->max_input_len + HIMAX_REPORT_HDR_SIZE +
+		     HIMAX_HEATMAP_HDR_SIZE + DIV_ROUND_UP(nodes * 3, 2);
+	if (event_size > HIMAX_AHB_MAX_XFER)
+		return -EINVAL;
+
+	heatmap_size = HIMAX_HEATMAP_HDR_SIZE + round_up(nodes, 2) * sizeof(u16);
+
+	/* The heatmap is unpacked in pairs, which may read past an odd count */
+	buf = devm_kzalloc(ts->dev, event_size + 1 + heatmap_size, GFP_KERNEL);
+	if (!buf)
+		return -ENOMEM;
+
+	ts->event_buf = buf;
+	ts->event_size = event_size;
+	ts->heatmap_buf = buf + event_size + 1;
+	ts->heatmap_size = heatmap_size;
+
+	return 0;
+}
+
+static int himax_hid_add(struct himax_ts_data *ts)
+{
+	const struct himax_hid_desc *desc = ts->hid_desc;
+	struct hid_device *hid;
+	int ret;
+
+	if (!ts->event_buf) {
+		ret = himax_read_ic_info(ts);
+		if (ret)
+			return ret;
+
+		ret = himax_alloc_report_bufs(ts);
+		if (ret)
+			return ret;
+	}
+
+	hid = hid_allocate_device();
+	if (IS_ERR(hid))
+		return PTR_ERR(hid);
+
+	hid->driver_data = ts;
+	hid->ll_driver = &himax_hid_ll_driver;
+	hid->bus = BUS_SPI;
+	hid->dev.parent = ts->dev;
+	hid->version = le16_to_cpu(desc->bcd_version);
+	hid->vendor = le16_to_cpu(desc->vendor_id);
+	hid->product = le16_to_cpu(desc->product_id);
+	snprintf(hid->name, sizeof(hid->name), "hid-hxtp %04X:%04X",
+		 hid->vendor, hid->product);
+
+	ret = hid_add_device(hid);
+	if (ret) {
+		hid_destroy_device(hid);
+		return ret;
+	}
+
+	ts->hid = hid;
+
+	return 0;
+}
+
+static int himax_read_events(struct himax_ts_data *ts)
+{
+	size_t chunk = ts->xfer_max - HIMAX_SPI_READ_HDR_LEN;
+	size_t pos, n;
+	int ret;
+
+	guard(mutex)(&ts->xfer_lock);
+
+	for (pos = 0; pos < ts->event_size; pos += n) {
+		n = min(ts->event_size - pos, chunk);
+		ret = himax_bus_read(ts, HIMAX_BUS_EVENT_STACK, ts->event_buf + pos, n);
+		if (ret)
+			return ret;
+	}
+
+	return 0;
+}
+
+static void himax_unpack_heatmap(struct himax_ts_data *ts, const u8 *src)
+{
+	u32 count = round_up(ts->rx_num * ts->tx_num, 2);
+	u8 *dst = ts->heatmap_buf;
+	u32 i;
+
+	memcpy(dst, src, HIMAX_HEATMAP_HDR_SIZE);
+	src += HIMAX_HEATMAP_HDR_SIZE;
+	dst += HIMAX_HEATMAP_HDR_SIZE;
+
+	for (i = 0; i < count; i += 2, src += 3, dst += 4) {
+		dst[0] = src[0];
+		dst[1] = src[2] >> 4;
+		dst[2] = src[1];
+		dst[3] = src[2] & 0x0f;
+	}
+}
+
+static irqreturn_t himax_irq(int irq, void *data)
+{
+	struct himax_ts_data *ts = data;
+	size_t len = ts->max_input_len - HIMAX_REPORT_HDR_SIZE;
+	size_t pos = HIMAX_REPORT_HDR_SIZE;
+	int ret;
+
+	ret = himax_read_events(ts);
+	if (ret) {
+		dev_warn_ratelimited(ts->dev, "failed to read events: %d\n", ret);
+		himax_pin_reset(ts);
+		himax_reload_to_active(ts);
+		return IRQ_HANDLED;
+	}
+
+	hid_safe_input_report(ts->hid, HID_INPUT_REPORT, ts->event_buf + pos,
+			      ts->event_size - pos, len, 1);
+	pos += ts->max_input_len;
+
+	if (ts->has_stylus) {
+		hid_safe_input_report(ts->hid, HID_INPUT_REPORT, ts->event_buf + pos,
+				      ts->event_size - pos, len, 1);
+		pos += ts->max_input_len;
+	}
+
+	himax_unpack_heatmap(ts, ts->event_buf + pos);
+	hid_safe_input_report(ts->hid, HID_INPUT_REPORT, ts->heatmap_buf, ts->heatmap_size,
+			      HIMAX_HEATMAP_HDR_SIZE +
+			      ts->rx_num * ts->tx_num * sizeof(u16), 1);
+
+	return IRQ_HANDLED;
+}
+
+static int himax_power_on(struct himax_ts_data *ts)
+{
+	int ret;
+
+	if (!ts->powered) {
+		gpiod_set_value_cansleep(ts->reset_gpio, 1);
+
+		ret = regulator_enable(ts->vccd);
+		if (ret)
+			return ret;
+
+		ts->powered = true;
+		fsleep(2000);
+	}
+
+	gpiod_set_value_cansleep(ts->reset_gpio, 0);
+	fsleep(20000);
+
+	return 0;
+}
+
+static void himax_power_off(struct himax_ts_data *ts)
+{
+	if (ts->irq_enabled) {
+		disable_irq(ts->spi->irq);
+		ts->irq_enabled = false;
+	}
+
+	gpiod_set_value_cansleep(ts->reset_gpio, 1);
+
+	if (ts->powered) {
+		regulator_disable(ts->vccd);
+		ts->powered = false;
+	}
+}
+
+static int himax_boot(struct himax_ts_data *ts)
+{
+	int ret;
+
+	ret = himax_power_on(ts);
+	if (ret)
+		return ret;
+
+	if (!ts->fw) {
+		ret = himax_detect(ts);
+		if (ret)
+			return ret;
+
+		ret = himax_request_fw(ts);
+		if (ret)
+			return ret;
+	}
+
+	ret = himax_load_fw(ts);
+	if (ret)
+		return ret;
+
+	if (!ts->hid)
+		return himax_hid_add(ts);
+
+	return 0;
+}
+
+static void himax_power_on_work(struct work_struct *work)
+{
+	struct himax_ts_data *ts = container_of(work, struct himax_ts_data, power_on_work);
+	int ret;
+
+	guard(mutex)(&ts->lock);
+
+	if (ts->irq_enabled)
+		return;
+
+	/* A failed power up is retried when the panel is prepared again */
+	ret = himax_boot(ts);
+	if (ret) {
+		dev_err(ts->dev, "failed to power up: %d\n", ret);
+		himax_power_off(ts);
+		return;
+	}
+
+	enable_irq(ts->spi->irq);
+	ts->irq_enabled = true;
+}
+
+static int himax_panel_prepared(struct drm_panel_follower *follower)
+{
+	struct himax_ts_data *ts = container_of(follower, struct himax_ts_data,
+						panel_follower);
+
+	/* Loading the firmware takes a while, don't hold up the panel */
+	schedule_work(&ts->power_on_work);
+
+	return 0;
+}
+
+static int himax_panel_unpreparing(struct drm_panel_follower *follower)
+{
+	struct himax_ts_data *ts = container_of(follower, struct himax_ts_data,
+						panel_follower);
+
+	cancel_work_sync(&ts->power_on_work);
+
+	guard(mutex)(&ts->lock);
+	himax_power_off(ts);
+
+	return 0;
+}
+
+static const struct drm_panel_follower_funcs himax_panel_follower_funcs = {
+	.panel_prepared = himax_panel_prepared,
+	.panel_unpreparing = himax_panel_unpreparing,
+};
+
+static void himax_cleanup(void *data)
+{
+	struct himax_ts_data *ts = data;
+
+	cancel_work_sync(&ts->power_on_work);
+
+	guard(mutex)(&ts->lock);
+	himax_power_off(ts);
+	if (ts->hid)
+		hid_destroy_device(ts->hid);
+	release_firmware(ts->fw);
+}
+
+static int himax_probe(struct spi_device *spi)
+{
+	struct device *dev = &spi->dev;
+	struct himax_ts_data *ts;
+	int ret;
+
+	if (spi->controller->flags & SPI_CONTROLLER_HALF_DUPLEX)
+		return dev_err_probe(dev, -EINVAL, "full duplex SPI controller required\n");
+
+	spi->mode = SPI_MODE_3;
+	spi->bits_per_word = 8;
+	spi->cs_setup.value = 300;
+	spi->cs_setup.unit = SPI_DELAY_UNIT_USECS;
+	ret = spi_setup(spi);
+	if (ret)
+		return ret;
+
+	ts = devm_kzalloc(dev, sizeof(*ts), GFP_KERNEL);
+	if (!ts)
+		return -ENOMEM;
+
+	ts->dev = dev;
+	ts->spi = spi;
+	ts->xfer_max = min(spi_max_transfer_size(spi), sizeof(ts->tx_buf));
+	if (ts->xfer_max < SZ_64)
+		return dev_err_probe(dev, -EINVAL, "SPI transfer size limit too low\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");
+
+	ts->vccd = devm_regulator_get(dev, "vccd");
+	if (IS_ERR(ts->vccd))
+		return dev_err_probe(dev, PTR_ERR(ts->vccd),
+				     "failed to get vccd supply\n");
+
+	ts->fw_name = "himax_i2chid.bin";
+	device_property_read_string(dev, "firmware-name", &ts->fw_name);
+
+	ret = devm_mutex_init(dev, &ts->lock);
+	if (ret)
+		return ret;
+
+	ret = devm_mutex_init(dev, &ts->xfer_lock);
+	if (ret)
+		return ret;
+
+	INIT_WORK(&ts->power_on_work, himax_power_on_work);
+
+	ret = devm_request_threaded_irq(dev, spi->irq, NULL, himax_irq,
+					IRQF_ONESHOT | IRQF_NO_AUTOEN,
+					dev_name(dev), ts);
+	if (ret)
+		return dev_err_probe(dev, ret, "failed to request interrupt\n");
+
+	ret = devm_add_action_or_reset(dev, himax_cleanup, ts);
+	if (ret)
+		return ret;
+
+	/* The touch function is powered and reset together with the panel */
+	ts->panel_follower.funcs = &himax_panel_follower_funcs;
+	ret = devm_drm_panel_add_follower(dev, &ts->panel_follower);
+	if (ret)
+		return dev_err_probe(dev, ret, "failed to follow the panel\n");
+
+	return 0;
+}
+
+static const struct of_device_id himax_of_match[] = {
+	{ .compatible = "himax,hx83102j" },
+	{ }
+};
+MODULE_DEVICE_TABLE(of, himax_of_match);
+
+static const struct spi_device_id himax_spi_ids[] = {
+	{ "hx83102j" },
+	{ }
+};
+MODULE_DEVICE_TABLE(spi, himax_spi_ids);
+
+static struct spi_driver himax_spi_driver = {
+	.driver = {
+		.name = "himax-spi-hid",
+		.of_match_table = himax_of_match,
+	},
+	.probe = himax_probe,
+	.id_table = himax_spi_ids,
+};
+module_spi_driver(himax_spi_driver);
+
+MODULE_DESCRIPTION("Himax HX83102J HID over SPI touchscreen driver");
+MODULE_AUTHOR("Michał Kopeć <michal@nozomi.space>");
+MODULE_LICENSE("GPL");
-- 
2.56.0


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

* [PATCH v6 3/3] arm64: dts: mediatek: mt8188-geralt-ciri: Add the touchscreen
  2026-10-11 18:16 [PATCH v6 0/3] HID: Add support for the Himax HX83102J touchscreen Michał Kopeć
  2026-10-11 18:16 ` [PATCH v6 1/3] dt-bindings: input: Add " Michał Kopeć
  2026-10-11 18:16 ` [PATCH v6 2/3] HID: Add Himax HX83102J SPI touchscreen driver Michał Kopeć
@ 2026-10-11 18:16 ` Michał Kopeć
  2 siblings, 0 replies; 5+ messages in thread
From: Michał Kopeć @ 2026-10-11 18:16 UTC (permalink / raw)
  To: linux-input
  Cc: devicetree, linux-mediatek, linux-arm-kernel, linux-kernel,
	jikos, bentiss, dmitry.torokhov, robh, krzk+dt, conor+dt,
	matthias.bgg, angelogioacchino.delregno, allencl_lin, tylor_yang,
	felix, dianders, Michał Kopeć

Ciri uses the touch function of its Himax HX83102J TDDI controller,
connected over SPI1. It shares the analog supplies and the reset with
the panel, so describe those and follow the panel for power sequencing.

The controller has no flash; the firmware is loaded from a file that
depends on the panel: himax_i2chid_1002.bin for the BOE panel and
himax_i2chid_1003.bin for the IVO panel, as on ChromeOS.

Signed-off-by: Michał Kopeć <michal@nozomi.space>
---
 .../dts/mediatek/mt8188-geralt-ciri-sku1.dts  |  4 +++
 .../dts/mediatek/mt8188-geralt-ciri-sku3.dts  |  4 +++
 .../dts/mediatek/mt8188-geralt-ciri-sku5.dts  |  4 +++
 .../dts/mediatek/mt8188-geralt-ciri-sku7.dts  |  4 +++
 .../boot/dts/mediatek/mt8188-geralt-ciri.dtsi | 33 +++++++++++++++++++
 5 files changed, 49 insertions(+)

diff --git a/arch/arm64/boot/dts/mediatek/mt8188-geralt-ciri-sku1.dts b/arch/arm64/boot/dts/mediatek/mt8188-geralt-ciri-sku1.dts
index ef5ea9d12b1de..6377354440d68 100644
--- a/arch/arm64/boot/dts/mediatek/mt8188-geralt-ciri-sku1.dts
+++ b/arch/arm64/boot/dts/mediatek/mt8188-geralt-ciri-sku1.dts
@@ -57,3 +57,7 @@ codec {
 		};
 	};
 };
+
+&touchscreen {
+	firmware-name = "himax_i2chid_1003.bin";
+};
diff --git a/arch/arm64/boot/dts/mediatek/mt8188-geralt-ciri-sku3.dts b/arch/arm64/boot/dts/mediatek/mt8188-geralt-ciri-sku3.dts
index 524f7f0064c1a..2a51c610758f7 100644
--- a/arch/arm64/boot/dts/mediatek/mt8188-geralt-ciri-sku3.dts
+++ b/arch/arm64/boot/dts/mediatek/mt8188-geralt-ciri-sku3.dts
@@ -30,3 +30,7 @@ &sound {
 		"Left Spk", "Front Left BE_OUT",
 		"Right Spk", "Front Right BE_OUT";
 };
+
+&touchscreen {
+	firmware-name = "himax_i2chid_1003.bin";
+};
diff --git a/arch/arm64/boot/dts/mediatek/mt8188-geralt-ciri-sku5.dts b/arch/arm64/boot/dts/mediatek/mt8188-geralt-ciri-sku5.dts
index bf87201ccf273..8fb0036aa14d0 100644
--- a/arch/arm64/boot/dts/mediatek/mt8188-geralt-ciri-sku5.dts
+++ b/arch/arm64/boot/dts/mediatek/mt8188-geralt-ciri-sku5.dts
@@ -70,3 +70,7 @@ codec {
 		};
 	};
 };
+
+&touchscreen {
+	firmware-name = "himax_i2chid_1003.bin";
+};
diff --git a/arch/arm64/boot/dts/mediatek/mt8188-geralt-ciri-sku7.dts b/arch/arm64/boot/dts/mediatek/mt8188-geralt-ciri-sku7.dts
index 825015b452d59..2bd178116423b 100644
--- a/arch/arm64/boot/dts/mediatek/mt8188-geralt-ciri-sku7.dts
+++ b/arch/arm64/boot/dts/mediatek/mt8188-geralt-ciri-sku7.dts
@@ -46,3 +46,7 @@ codec {
 		};
 	};
 };
+
+&touchscreen {
+	firmware-name = "himax_i2chid_1003.bin";
+};
diff --git a/arch/arm64/boot/dts/mediatek/mt8188-geralt-ciri.dtsi b/arch/arm64/boot/dts/mediatek/mt8188-geralt-ciri.dtsi
index 6815c435a57e1..e611fb73ddb01 100644
--- a/arch/arm64/boot/dts/mediatek/mt8188-geralt-ciri.dtsi
+++ b/arch/arm64/boot/dts/mediatek/mt8188-geralt-ciri.dtsi
@@ -121,6 +121,27 @@ codec {
 	};
 };
 
+&spi1 {
+	#address-cells = <1>;
+	#size-cells = <0>;
+
+	touchscreen: touchscreen@0 {
+		compatible = "himax,hx83102j";
+		reg = <0>;
+		spi-max-frequency = <12500000>;
+		interrupts-extended = <&pio 143 IRQ_TYPE_LEVEL_LOW>;
+		pinctrl-names = "default";
+		pinctrl-0 = <&touchscreen_pins>;
+		reset-gpios = <&pio 5 GPIO_ACTIVE_LOW>;
+		display-reset-gpios = <&pio 25 GPIO_ACTIVE_LOW>;
+		vccd-supply = <&mt6359_vm18_ldo_reg>;
+		vsp-supply = <&ppvar_mipi_disp_avdd>;
+		vsn-supply = <&ppvar_mipi_disp_avee>;
+		panel = <&dsi_panel>;
+		firmware-name = "himax_i2chid_1002.bin";
+	};
+};
+
 &pio {
 	gpio-line-names =
 		"GSC_AP_INT_ODL",
@@ -313,4 +334,16 @@ pins-en-spkr {
 			pinmux = <PINMUX_GPIO111__FUNC_B_GPIO111>;
 		};
 	};
+
+	touchscreen_pins: touchscreen-pins {
+		pins-int-odl {
+			pinmux = <PINMUX_GPIO143__FUNC_B_GPIO143>;
+			input-enable;
+		};
+
+		pins-rst-l {
+			pinmux = <PINMUX_GPIO5__FUNC_B_GPIO5>;
+			output-high;
+		};
+	};
 };
-- 
2.56.0


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

* Re: [PATCH v6 2/3] HID: Add Himax HX83102J SPI touchscreen driver
  2026-10-11 18:16 ` [PATCH v6 2/3] HID: Add Himax HX83102J SPI touchscreen driver Michał Kopeć
@ 2026-10-11 23:06   ` Uwe Kleine-König
  0 siblings, 0 replies; 5+ messages in thread
From: Uwe Kleine-König @ 2026-10-11 23:06 UTC (permalink / raw)
  To: Michał Kopeć
  Cc: linux-input, devicetree, linux-mediatek, linux-arm-kernel,
	linux-kernel, jikos, bentiss, dmitry.torokhov, robh, krzk+dt,
	conor+dt, matthias.bgg, angelogioacchino.delregno, allencl_lin,
	tylor_yang, felix, dianders

[-- Attachment #1: Type: text/plain, Size: 476 bytes --]

Hello,

On Sun, Oct 11, 2026 at 08:16:32PM +0200, Michał Kopeć wrote:
> +#include <linux/mod_devicetable.h>

Please don't include <linux/mod_devicetable.h>. This file introduces a
big amount of unneeded dependencies. You can rely on <linux/spi/spi.h>
to provide of_device_id and spi_device_id (which are the ones you
actually use). Alternatively---if you want to go full iwyu---include
linux/device-id/of.h and linux/device-id/spi.h directly.

Best regards
Uwe

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 488 bytes --]

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

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

Thread overview: 5+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-10-11 18:16 [PATCH v6 0/3] HID: Add support for the Himax HX83102J touchscreen Michał Kopeć
2026-10-11 18:16 ` [PATCH v6 1/3] dt-bindings: input: Add " Michał Kopeć
2026-10-11 18:16 ` [PATCH v6 2/3] HID: Add Himax HX83102J SPI touchscreen driver Michał Kopeć
2026-10-11 23:06   ` Uwe Kleine-König
2026-10-11 18:16 ` [PATCH v6 3/3] arm64: dts: mediatek: mt8188-geralt-ciri: Add the touchscreen Michał Kopeć

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

all inboxes | Powered by JetHome®