mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [PATCH] platform/x86: Add Goodix fingerprint EC mailbox transport
@ 2026-07-27 20:58 Ertuğrul Topçu
  2026-08-03  7:44 ` [PATCH v2] " Ertuğrul Topçu
  2026-08-16  1:26 ` [PATCH] " kernel test robot
  0 siblings, 2 replies; 6+ messages in thread
From: Ertuğrul Topçu @ 2026-07-27 20:58 UTC (permalink / raw)
  To: hansg, ilpo.jarvinen
  Cc: platform-driver-x86, linux-kernel, Ertuğrul Topçu

Some Goodix fingerprint sensors are connected through an ACPI-described
embedded-controller shared-memory mailbox rather than directly through USB
or SPI.

Add a transport driver for the GXFP5130 platform interface. The driver
owns the MMIO mailbox, GPIO handshakes and IRQ. It exposes opaque mailbox
records through /dev/gxfp. Keep sensor initialization, TLS, capture and
image processing in userspace.

Restrict the raw transport to CAP_SYS_RAWIO and document the UAPI. Handle
device removal and system suspend without invalidating open file
descriptors.

Signed-off-by: Ertuğrul Topçu <ertugtopcu0@gmail.com>
---
 .../ABI/testing/dev-goodix-ec-mailbox         |  22 +
 MAINTAINERS                                   |   8 +
 drivers/platform/x86/Kconfig                  |   2 +
 drivers/platform/x86/Makefile                 |   3 +
 .../platform/x86/goodix-ec-mailbox/Kconfig    |  15 +
 .../platform/x86/goodix-ec-mailbox/Makefile   |   2 +
 .../x86/goodix-ec-mailbox/goodix_ec_mailbox.h | 115 +++
 .../x86/goodix-ec-mailbox/goodix_ec_main.c    | 740 ++++++++++++++++++
 .../x86/goodix-ec-mailbox/goodix_ec_uapi.c    | 360 +++++++++
 include/uapi/linux/goodix_ec.h                |  37 +
 10 files changed, 1304 insertions(+)
 create mode 100644 Documentation/ABI/testing/dev-goodix-ec-mailbox
 create mode 100644 drivers/platform/x86/goodix-ec-mailbox/Kconfig
 create mode 100644 drivers/platform/x86/goodix-ec-mailbox/Makefile
 create mode 100644 drivers/platform/x86/goodix-ec-mailbox/goodix_ec_mailbox.h
 create mode 100644 drivers/platform/x86/goodix-ec-mailbox/goodix_ec_main.c
 create mode 100644 drivers/platform/x86/goodix-ec-mailbox/goodix_ec_uapi.c
 create mode 100644 include/uapi/linux/goodix_ec.h

diff --git a/Documentation/ABI/testing/dev-goodix-ec-mailbox b/Documentation/ABI/testing/dev-goodix-ec-mailbox
new file mode 100644
index 000000000000..11ca80a41ca7
--- /dev/null
+++ b/Documentation/ABI/testing/dev-goodix-ec-mailbox
@@ -0,0 +1,22 @@
+What:		/dev/gxfp
+Date:		July 2026
+KernelVersion:	TBD
+Contact:	Ertugrul Topcu <ertugtopcu0@gmail.com>
+Description:
+		Binary userspace interface for the ACPI Goodix EC mailbox
+		fingerprint transport. Only callers with CAP_SYS_RAWIO may open
+		the device. At most one reader may be open at a time.
+
+		A write consists of struct goodix_ec_tx_header followed by exactly
+		payload_len opaque MP payload bytes. reserved and flags must be zero.
+
+		A read returns one struct goodix_ec_record_header followed by exactly
+		len bytes. mp_type is the normalized MP type and timestamp_ns is a
+		CLOCK_MONOTONIC timestamp captured when the record entered the RX
+		queue. Records are never split across reads.
+
+		poll(2) reports POLLIN while a complete record is queued. Device
+		removal reports POLLERR | POLLHUP. System suspend reports POLLERR.
+
+		GOODIX_EC_IOCTL_FLUSH_RX discards all queued receive records.
+Users:		libfprint Goodix GXFP5130 userspace driver
diff --git a/MAINTAINERS b/MAINTAINERS
index 92a2167f1eb8..0c62071bbc1b 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -11109,6 +11109,14 @@ M:	Maud Spierings <maudspierings@gocontroll.com>
 S:	Maintained
 F:	Documentation/devicetree/bindings/connector/gocontroll,moduline-module-slot.yaml
 
+GOODIX EC MAILBOX FINGERPRINT TRANSPORT DRIVER
+M:	Ertugrul Topcu <ertugtopcu0@gmail.com>
+L:	platform-driver-x86@vger.kernel.org
+S:	Maintained
+F:	Documentation/ABI/testing/dev-goodix-ec-mailbox
+F:	drivers/platform/x86/goodix-ec-mailbox/
+F:	include/uapi/linux/goodix_ec.h
+
 GOODIX TOUCHSCREEN
 M:	Hans de Goede <hansg@kernel.org>
 L:	linux-input@vger.kernel.org
diff --git a/drivers/platform/x86/Kconfig b/drivers/platform/x86/Kconfig
index 957034f39e4e..57d1311f8670 100644
--- a/drivers/platform/x86/Kconfig
+++ b/drivers/platform/x86/Kconfig
@@ -440,6 +440,8 @@ config FUJITSU_TABLET
 
          If you have a Fujitsu convertible or slate, say Y or M here.
 
+source "drivers/platform/x86/goodix-ec-mailbox/Kconfig"
+
 config GPD_POCKET_FAN
 	tristate "GPD Pocket Fan Controller support"
 	depends on ACPI
diff --git a/drivers/platform/x86/Makefile b/drivers/platform/x86/Makefile
index 872ac3842391..650ab67f7902 100644
--- a/drivers/platform/x86/Makefile
+++ b/drivers/platform/x86/Makefile
@@ -54,6 +54,9 @@ obj-$(CONFIG_AMILO_RFKILL)	+= amilo-rfkill.o
 obj-$(CONFIG_FUJITSU_LAPTOP)	+= fujitsu-laptop.o
 obj-$(CONFIG_FUJITSU_TABLET)	+= fujitsu-tablet.o
 
+# Goodix
+obj-$(CONFIG_GOODIX_EC_MAILBOX)	+= goodix-ec-mailbox/
+
 # GPD
 obj-$(CONFIG_GPD_POCKET_FAN)	+= gpd-pocket-fan.o
 
diff --git a/drivers/platform/x86/goodix-ec-mailbox/Kconfig b/drivers/platform/x86/goodix-ec-mailbox/Kconfig
new file mode 100644
index 000000000000..6f43b1323fa4
--- /dev/null
+++ b/drivers/platform/x86/goodix-ec-mailbox/Kconfig
@@ -0,0 +1,15 @@
+config GOODIX_EC_MAILBOX
+	tristate "Goodix fingerprint EC mailbox transport"
+	depends on X86
+	depends on ACPI
+	depends on GPIOLIB
+	help
+	  Enable transport support for Goodix fingerprint sensors connected
+	  through an ACPI-described embedded-controller shared-memory mailbox.
+
+	  The driver exposes the opaque mailbox transport through /dev/gxfp.
+	  Sensor configuration, TLS, capture and image processing remain in
+	  userspace.
+
+	  To compile this driver as a module, choose M here. The module will be
+	  called goodix_ec_mailbox.
diff --git a/drivers/platform/x86/goodix-ec-mailbox/Makefile b/drivers/platform/x86/goodix-ec-mailbox/Makefile
new file mode 100644
index 000000000000..52e5516eefb0
--- /dev/null
+++ b/drivers/platform/x86/goodix-ec-mailbox/Makefile
@@ -0,0 +1,2 @@
+obj-$(CONFIG_GOODIX_EC_MAILBOX) += goodix_ec_mailbox.o
+goodix_ec_mailbox-y := goodix_ec_main.o goodix_ec_uapi.o
diff --git a/drivers/platform/x86/goodix-ec-mailbox/goodix_ec_mailbox.h b/drivers/platform/x86/goodix-ec-mailbox/goodix_ec_mailbox.h
new file mode 100644
index 000000000000..26fae8b5ec64
--- /dev/null
+++ b/drivers/platform/x86/goodix-ec-mailbox/goodix_ec_mailbox.h
@@ -0,0 +1,115 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef _GOODIX_EC_H_
+#define _GOODIX_EC_H_
+
+#include <linux/gpio/consumer.h>
+#include <linux/ioport.h>
+#include <linux/kfifo.h>
+#include <linux/kref.h>
+#include <linux/miscdevice.h>
+#include <linux/mutex.h>
+#include <linux/spinlock.h>
+#include <linux/wait.h>
+#include <linux/stddef.h>
+#include <linux/types.h>
+
+#define GOODIX_EC_DRIVER_NAME		"goodix-ec-mailbox"
+
+/* Shared-memory mailbox layout. */
+#define GOODIX_EC_MMIO_SIZE		0x1000
+#define GOODIX_EC_TX_OFFSET		0x000
+#define GOODIX_EC_TX_SIZE		0x0200
+#define GOODIX_EC_RX_OFFSET		0x0200
+#define GOODIX_EC_RX_SIZE		0x0e00
+#define GOODIX_EC_PACKET_ALIGNMENT	8
+
+/* Nuvoton EC mailbox framing. */
+#define GOODIX_EC_PACKET_TYPE		0xf0
+#define GOODIX_EC_SEQUENCE_SEED		0x8881
+
+/* MP framing. The payload is opaque to the kernel. */
+#define GOODIX_MP_HEADER_SIZE		4
+
+/* Synchronous startup transaction timing. */
+#define GOODIX_WRITE_DONE_PRE_US	50
+#define GOODIX_WRITE_DONE_HIGH_US	4000
+#define GOODIX_WRITE_DONE_POST_US	200
+#define GOODIX_READ_DONE_HIGH_US	50
+#define GOODIX_READ_DONE_POST_US	200
+#define GOODIX_SYNC_RX_DELAY_US		1000
+
+struct device;
+
+struct goodix_ec_header {
+	u8 type;
+	__le16 payload_len;
+	u8 checksum;
+	__le16 sequence;
+	u8 reserved[2];
+} __packed;
+
+struct goodix_mp_header {
+	u8 type;
+	__le16 payload_len;
+	u8 checksum;
+} __packed;
+
+struct goodix_device;
+
+/* Add platform-specific transport quirks here only after hardware validation. */
+struct goodix_model_data {
+	const char *name;
+	const char *acpi_hid;
+};
+
+struct goodix_device {
+	struct device *dev;
+	struct kref refcount;
+	bool disconnected;
+	bool suspended;
+	bool irq_enabled;
+
+	void __iomem *mailbox;
+	resource_size_t mailbox_phys;
+	resource_size_t mailbox_size;
+
+	struct gpio_desc *write_done_gpio;
+	struct gpio_desc *read_done_gpio;
+	struct gpio_desc *irq_gpio;
+	int irq;
+
+	u8 *tx_buf;
+	u8 *rx_buf;
+
+	u8 *rx_reassembly;
+	size_t rx_reassembly_len;
+	size_t rx_reassembly_received;
+	u8 rx_reassembly_mp_type;
+	bool rx_reassembly_active;
+
+	u16 tx_sequence;
+	/* Serializes mailbox TX with threaded-IRQ RX access. */
+	struct mutex transfer_lock;
+
+	struct miscdevice miscdev;
+	struct kfifo rx_fifo;
+	/* Protects the RX FIFO and reader ownership state. */
+	spinlock_t rx_fifo_lock;
+	/* Serializes record-oriented read operations. */
+	struct mutex rx_read_lock;
+	wait_queue_head_t rx_wait;
+	bool rx_fifo_ready;
+	bool rx_reader_open;
+	bool misc_registered;
+
+	const struct goodix_model_data *model;
+};
+
+int goodix_ec_sync_send(struct goodix_device *gdev,
+			const u8 *tx, size_t tx_len);
+bool goodix_ec_device_get(struct goodix_device *gdev);
+void goodix_ec_device_put(struct goodix_device *gdev);
+int goodix_ec_uapi_register(struct goodix_device *gdev);
+void goodix_ec_uapi_unregister(struct goodix_device *gdev);
+
+#endif /* _GOODIX_EC_H_ */
diff --git a/drivers/platform/x86/goodix-ec-mailbox/goodix_ec_main.c b/drivers/platform/x86/goodix-ec-mailbox/goodix_ec_main.c
new file mode 100644
index 000000000000..c799f6175a34
--- /dev/null
+++ b/drivers/platform/x86/goodix-ec-mailbox/goodix_ec_main.c
@@ -0,0 +1,740 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Goodix fingerprint sensors behind an EC shared-memory mailbox.
+ *
+ * The tested GXFP5130 platform exposes no native Linux SPI device. The host
+ * talks to a Nuvoton EC through ACPI-described MMIO and GPIO handshakes; the EC
+ * forwards opaque MP payloads to the fingerprint sensor over its private bus.
+ *
+ * The source intentionally remains a single translation unit. Its internal
+ * order still preserves clear boundaries:
+ *
+ *   1. EC mailbox transport (MMIO/GPIO)
+ *   2. MP packet transport and RX reassembly
+ *   3. Userspace interface
+ *   4. ACPI platform-driver lifecycle
+ *
+ * Sensor protocol policy deliberately stays in userspace. This driver does
+ * not know about Goodix commands, checksums, TLS, configuration, capture, or
+ * sensor power states.
+ */
+
+#include <linux/acpi.h>
+#include <linux/align.h>
+#include <linux/delay.h>
+#include <linux/errno.h>
+#include <linux/gpio/consumer.h>
+#include <linux/io.h>
+#include <linux/interrupt.h>
+#include <linux/irq.h>
+#include <linux/capability.h>
+#include <linux/fs.h>
+#include <linux/kernel.h>
+#include <linux/kfifo.h>
+#include <linux/miscdevice.h>
+#include <linux/poll.h>
+#include <linux/module.h>
+#include <linux/platform_device.h>
+#include <linux/pm.h>
+#include <linux/slab.h>
+#include <linux/string.h>
+#include <linux/timekeeping.h>
+#include <linux/uaccess.h>
+#include <linux/unaligned.h>
+
+#include "goodix_ec_mailbox.h"
+#include <linux/goodix_ec.h>
+
+#define GXFP5130_ACPI_HID "GXFP5130"
+
+static void goodix_ec_device_release(struct kref *refcount)
+{
+	struct goodix_device *gdev =
+		container_of(refcount, struct goodix_device, refcount);
+
+	kfree(gdev);
+}
+
+bool goodix_ec_device_get(struct goodix_device *gdev)
+{
+	return kref_get_unless_zero(&gdev->refcount);
+}
+
+void goodix_ec_device_put(struct goodix_device *gdev)
+{
+	kref_put(&gdev->refcount, goodix_ec_device_release);
+}
+
+static void goodix_ec_device_put_action(void *data)
+{
+	goodix_ec_device_put(data);
+}
+
+/* -------------------------------------------------------------------------- */
+/* EC mailbox transport                                                       */
+/* -------------------------------------------------------------------------- */
+
+struct goodix_ec_acpi_gpio_state {
+	unsigned int gpio_count;
+	int irq_index;
+	int done_index[2];
+	u8 done_polarity[2];
+	unsigned int done_count;
+};
+
+static int goodix_ec_acpi_gpio_resource(struct acpi_resource *resource,
+					void *context)
+{
+	struct goodix_ec_acpi_gpio_state *state = context;
+	struct acpi_resource_gpio *gpio;
+	unsigned int index;
+
+	if (resource->type != ACPI_RESOURCE_TYPE_GPIO)
+		return 0;
+
+	gpio = &resource->data.gpio;
+	index = state->gpio_count++;
+	if (!gpio->pin_table_length)
+		return 0;
+
+	if (gpio->connection_type == ACPI_RESOURCE_GPIO_TYPE_INT) {
+		if (state->irq_index < 0)
+			state->irq_index = index;
+	} else if (gpio->connection_type == ACPI_RESOURCE_GPIO_TYPE_IO &&
+		   state->done_count < ARRAY_SIZE(state->done_index)) {
+		state->done_index[state->done_count] = index;
+		state->done_polarity[state->done_count] = gpio->polarity;
+		state->done_count++;
+	}
+
+	return 0;
+}
+
+static int goodix_ec_add_gpio_mappings(struct device *dev)
+{
+	struct goodix_ec_acpi_gpio_state state = {
+		.irq_index = -1,
+		.done_index = { -1, -1 },
+	};
+	struct acpi_gpio_mapping *mappings;
+	struct acpi_gpio_params *params;
+	struct acpi_device *adev = ACPI_COMPANION(dev);
+	LIST_HEAD(resources);
+	bool active_low;
+	int ret;
+
+	if (!adev)
+		return -ENODEV;
+
+	ret = acpi_dev_get_resources(adev, &resources,
+				     goodix_ec_acpi_gpio_resource, &state);
+	acpi_dev_free_resource_list(&resources);
+	if (ret <= 0)
+		return ret < 0 ? ret : -ENOENT;
+	if (state.irq_index < 0 || state.done_count != 2)
+		return dev_err_probe(dev, -ENOENT,
+				     "ACPI _CRS does not describe one IRQ and two done GPIOs\n");
+	if (state.done_polarity[0] != state.done_polarity[1])
+		return dev_err_probe(dev, -EINVAL,
+				     "ACPI done GPIO polarities differ\n");
+
+	active_low = state.done_polarity[0] != 0;
+	params = devm_kcalloc(dev, 3, sizeof(*params), GFP_KERNEL);
+	mappings = devm_kcalloc(dev, 4, sizeof(*mappings), GFP_KERNEL);
+	if (!params || !mappings)
+		return -ENOMEM;
+
+	params[0].crs_entry_index = state.irq_index;
+	params[0].line_index = 0;
+	params[0].active_low = false;
+	mappings[0].name = "irq-gpios";
+	mappings[0].data = &params[0];
+	mappings[0].size = 1;
+
+	params[1].crs_entry_index = state.done_index[0];
+	params[1].line_index = 0;
+	params[1].active_low = active_low;
+	mappings[1].name = "write-done-gpios";
+	mappings[1].data = &params[1];
+	mappings[1].size = 1;
+
+	params[2].crs_entry_index = state.done_index[1];
+	params[2].line_index = 0;
+	params[2].active_low = active_low;
+	mappings[2].name = "read-done-gpios";
+	mappings[2].data = &params[2];
+	mappings[2].size = 1;
+
+	ret = devm_acpi_dev_add_driver_gpios(dev, mappings);
+	if (ret)
+		return dev_err_probe(dev, ret,
+				     "failed to install ACPI GPIO mappings\n");
+
+	dev_dbg(dev,
+		"ACPI GPIO mappings: irq=%d write-done=%d read-done=%d active-low=%u\n",
+		state.irq_index, state.done_index[0], state.done_index[1],
+		active_low);
+	return 0;
+}
+
+static int goodix_ec_get_gpio(struct goodix_device *gdev,
+			      struct gpio_desc **gpio,
+			      const char *name,
+			      enum gpiod_flags flags)
+{
+	*gpio = devm_gpiod_get(gdev->dev, name, flags);
+	if (IS_ERR(*gpio))
+		return dev_err_probe(gdev->dev, PTR_ERR(*gpio),
+				     "failed to acquire %s GPIO\n", name);
+
+	if (!*gpio)
+		return dev_err_probe(gdev->dev, -ENODEV,
+				     "%s GPIO missing\n", name);
+
+	return 0;
+}
+
+static void goodix_ec_mmio_write_tx(struct goodix_device *gdev, size_t len)
+{
+	size_t offset;
+
+	for (offset = 0; offset < len; offset += sizeof(u64)) {
+		u64 value = get_unaligned_le64(gdev->tx_buf + offset);
+
+		writeq(value, gdev->mailbox + GOODIX_EC_TX_OFFSET + offset);
+	}
+
+	/* Publish every qword before asserting the write-done doorbell. */
+	wmb();
+}
+
+static void goodix_ec_mmio_read_rx(struct goodix_device *gdev)
+{
+	size_t offset;
+
+	for (offset = 0; offset < GOODIX_EC_RX_SIZE; offset += sizeof(u64)) {
+		u64 value = readq(gdev->mailbox + GOODIX_EC_RX_OFFSET + offset);
+
+		put_unaligned_le64(value, gdev->rx_buf + offset);
+	}
+
+	/* Complete the mailbox snapshot before parsing its headers. */
+	rmb();
+}
+
+static int goodix_ec_pulse_write_done(struct goodix_device *gdev)
+{
+	if (!gdev->write_done_gpio)
+		return -ENODEV;
+
+	gpiod_set_value_cansleep(gdev->write_done_gpio, 0);
+	udelay(GOODIX_WRITE_DONE_PRE_US);
+
+	gpiod_set_value_cansleep(gdev->write_done_gpio, 1);
+	udelay(GOODIX_WRITE_DONE_HIGH_US);
+
+	gpiod_set_value_cansleep(gdev->write_done_gpio, 0);
+	udelay(GOODIX_WRITE_DONE_POST_US);
+
+	return 0;
+}
+
+static int goodix_ec_pulse_read_done(struct goodix_device *gdev)
+{
+	if (!gdev->read_done_gpio)
+		return -ENODEV;
+
+	/* RX must be copied before acknowledging that the host consumed it. */
+	gpiod_set_value_cansleep(gdev->read_done_gpio, 1);
+	udelay(GOODIX_READ_DONE_HIGH_US);
+
+	gpiod_set_value_cansleep(gdev->read_done_gpio, 0);
+	udelay(GOODIX_READ_DONE_POST_US);
+
+	return 0;
+}
+
+static int goodix_ec_build_packet(struct goodix_device *gdev,
+				  const u8 *payload, size_t payload_len,
+				  size_t *packet_len)
+{
+	struct goodix_ec_header *header;
+	size_t total_len;
+
+	if (!gdev || !payload || !payload_len || !packet_len)
+		return -EINVAL;
+
+	if (payload_len > U16_MAX)
+		return -EOVERFLOW;
+
+	total_len = ALIGN(sizeof(*header) + payload_len,
+			  GOODIX_EC_PACKET_ALIGNMENT);
+	if (total_len > GOODIX_EC_TX_SIZE)
+		return -EMSGSIZE;
+
+	memset(gdev->tx_buf, 0, total_len);
+
+	header = (struct goodix_ec_header *)gdev->tx_buf;
+	header->type = GOODIX_EC_PACKET_TYPE;
+	header->payload_len = cpu_to_le16(payload_len);
+	header->checksum = header->type + (payload_len & 0xff) +
+			   ((payload_len >> 8) & 0xff);
+	header->sequence = cpu_to_le16(++gdev->tx_sequence);
+
+	memcpy(gdev->tx_buf + sizeof(*header), payload, payload_len);
+	*packet_len = total_len;
+
+	return 0;
+}
+
+int goodix_ec_sync_send(struct goodix_device *gdev,
+			const u8 *tx, size_t tx_len)
+{
+	size_t packet_len;
+	int ret;
+
+	ret = goodix_ec_build_packet(gdev, tx, tx_len, &packet_len);
+	if (ret)
+		return ret;
+
+	goodix_ec_mmio_write_tx(gdev, packet_len);
+
+	ret = goodix_ec_pulse_write_done(gdev);
+	if (ret)
+		return ret;
+
+	udelay(GOODIX_SYNC_RX_DELAY_US);
+	return 0;
+}
+
+/* -------------------------------------------------------------------------- */
+/* MP packet transport */
+/* -------------------------------------------------------------------------- */
+static void goodix_ec_rx_push(struct goodix_device *gdev, u8 mp_type,
+			      const u8 *payload, size_t payload_len)
+{
+	struct goodix_ec_record_header header;
+	unsigned long flags;
+	size_t required;
+
+	if (!gdev || !payload || !payload_len ||
+	    payload_len > GOODIX_EC_UAPI_RX_MAX)
+		return;
+
+	memset(&header, 0, sizeof(header));
+	header.len = payload_len;
+	header.mp_type = mp_type >> 4;
+	header.timestamp_ns = ktime_get_ns();
+	required = sizeof(header) + payload_len;
+
+	spin_lock_irqsave(&gdev->rx_fifo_lock, flags);
+	if (!gdev->rx_fifo_ready || kfifo_avail(&gdev->rx_fifo) < required) {
+		spin_unlock_irqrestore(&gdev->rx_fifo_lock, flags);
+		dev_warn_ratelimited(gdev->dev,
+				     "RX queue full; dropping %zu-byte packet\n",
+				     payload_len);
+		return;
+	}
+
+	kfifo_in(&gdev->rx_fifo, &header, sizeof(header));
+	kfifo_in(&gdev->rx_fifo, payload, payload_len);
+	spin_unlock_irqrestore(&gdev->rx_fifo_lock, flags);
+
+	wake_up_interruptible(&gdev->rx_wait);
+}
+
+static irqreturn_t goodix_ec_irq_thread(int irq, void *data)
+{
+	struct goodix_device *gdev = data;
+	size_t chunk_len;
+	size_t remaining;
+	u16 declared_len;
+	u8 mp_type;
+	int ret = 0;
+	int ack_ret;
+
+	mutex_lock(&gdev->transfer_lock);
+
+	goodix_ec_mmio_read_rx(gdev);
+
+	/*
+	 * Continuation IRQs contain raw payload bytes without another
+	 * MP header.
+	 */
+	if (gdev->rx_reassembly_active) {
+		remaining = gdev->rx_reassembly_len -
+			    gdev->rx_reassembly_received;
+
+		chunk_len = min_t(size_t,
+				  remaining,
+				  GOODIX_EC_RX_SIZE);
+
+		memcpy(gdev->rx_reassembly +
+		       gdev->rx_reassembly_received,
+		       gdev->rx_buf,
+		       chunk_len);
+
+		gdev->rx_reassembly_received += chunk_len;
+
+		ack_ret = goodix_ec_pulse_read_done(gdev);
+		if (ack_ret)
+			ret = ack_ret;
+
+		if (!ret &&
+		    gdev->rx_reassembly_received ==
+		    gdev->rx_reassembly_len) {
+			dev_dbg(gdev->dev,
+				"RX reassembly complete: MP=0x%02x payload=%zu bytes\n",
+				 gdev->rx_reassembly_mp_type,
+				 gdev->rx_reassembly_len);
+
+			goodix_ec_rx_push(gdev,
+					  gdev->rx_reassembly_mp_type,
+					  gdev->rx_reassembly,
+					  gdev->rx_reassembly_len);
+
+			kfree(gdev->rx_reassembly);
+			gdev->rx_reassembly = NULL;
+			gdev->rx_reassembly_len = 0;
+			gdev->rx_reassembly_received = 0;
+			gdev->rx_reassembly_mp_type = 0;
+			gdev->rx_reassembly_active = false;
+		}
+
+		mutex_unlock(&gdev->transfer_lock);
+
+		if (ret)
+			dev_warn_ratelimited(gdev->dev,
+					     "IRQ %d continuation ACK failed: %d\n",
+					     irq, ret);
+
+		return IRQ_HANDLED;
+	}
+
+	/*
+	 * Beginning of a normal MP packet.
+	 */
+	mp_type = gdev->rx_buf[0];
+
+	if ((mp_type >> 4) != 0x0a &&
+	    (mp_type >> 4) != 0x0b &&
+	    (mp_type >> 4) != 0x0c) {
+		ret = -EBADMSG;
+		goto acknowledge;
+	}
+
+	if (gdev->rx_buf[3] !=
+	    gdev->rx_buf[0] +
+	    gdev->rx_buf[1] +
+	    gdev->rx_buf[2]) {
+		ret = -EBADMSG;
+		goto acknowledge;
+	}
+
+	declared_len = get_unaligned_le16(gdev->rx_buf + 1);
+
+	/*
+	 * Whole packet fits in the mailbox: use the normal path.
+	 */
+	if (declared_len <=
+	    GOODIX_EC_RX_SIZE -
+	    sizeof(struct goodix_mp_header)) {
+		goodix_ec_rx_push(gdev,
+				  mp_type,
+				  gdev->rx_buf +
+				  sizeof(struct goodix_mp_header),
+				  declared_len);
+
+		dev_dbg(gdev->dev,
+			"IRQ %d RX queued: MP=0x%02x payload=%u bytes\n",
+			irq,
+			mp_type >> 4,
+			declared_len);
+
+		goto acknowledge;
+	}
+
+	/*
+	 * The MP payload spans multiple mailbox IRQs.
+	 */
+	gdev->rx_reassembly = kmalloc(declared_len, GFP_KERNEL);
+	if (!gdev->rx_reassembly) {
+		ret = -ENOMEM;
+		goto acknowledge;
+	}
+
+	chunk_len = GOODIX_EC_RX_SIZE -
+		    sizeof(struct goodix_mp_header);
+
+	memcpy(gdev->rx_reassembly,
+	       gdev->rx_buf +
+	       sizeof(struct goodix_mp_header),
+	       chunk_len);
+
+	gdev->rx_reassembly_len = declared_len;
+	gdev->rx_reassembly_received = chunk_len;
+	gdev->rx_reassembly_mp_type = mp_type;
+	gdev->rx_reassembly_active = true;
+
+	dev_dbg(gdev->dev,
+		"RX reassembly started: MP=0x%02x total=%u first=%zu remaining=%zu\n",
+		 mp_type >> 4,
+		 declared_len,
+		 chunk_len,
+		 (size_t)declared_len - chunk_len);
+
+acknowledge:
+	ack_ret = goodix_ec_pulse_read_done(gdev);
+	if (!ret && ack_ret)
+		ret = ack_ret;
+
+	mutex_unlock(&gdev->transfer_lock);
+
+	if (ret)
+		dev_warn_ratelimited(gdev->dev,
+				     "IRQ %d RX handling failed: %d\n",
+				     irq, ret);
+
+	return IRQ_HANDLED;
+}
+
+/* -------------------------------------------------------------------------- */
+
+static const struct goodix_model_data gxfp5130_model = {
+	.name = "GXFP5130",
+	.acpi_hid = "GXFP5130",
+};
+
+/* -------------------------------------------------------------------------- */
+/* ACPI platform driver and model selection                                   */
+/* -------------------------------------------------------------------------- */
+
+static int goodix_ec_probe(struct platform_device *pdev)
+{
+	struct device *dev = &pdev->dev;
+	const struct acpi_device_id *match;
+	struct goodix_device *gdev;
+	struct resource *resource;
+	resource_size_t mailbox_size;
+	unsigned long irq_flags;
+	unsigned int irq_type;
+	int ret;
+
+	match = acpi_match_device(dev->driver->acpi_match_table, dev);
+	if (!match || !match->driver_data)
+		return -ENODEV;
+
+	gdev = kzalloc_obj(*gdev);
+	if (!gdev)
+		return -ENOMEM;
+	kref_init(&gdev->refcount);
+	ret = devm_add_action_or_reset(dev, goodix_ec_device_put_action, gdev);
+	if (ret)
+		return ret;
+
+	gdev->dev = dev;
+	gdev->model = (const struct goodix_model_data *)match->driver_data;
+	gdev->tx_sequence = GOODIX_EC_SEQUENCE_SEED;
+	mutex_init(&gdev->transfer_lock);
+	platform_set_drvdata(pdev, gdev);
+
+	gdev->mailbox = devm_platform_get_and_ioremap_resource(pdev, 0,
+							       &resource);
+	if (IS_ERR(gdev->mailbox))
+		return dev_err_probe(dev, PTR_ERR(gdev->mailbox),
+				     "failed to map EC mailbox MMIO resource\n");
+
+	mailbox_size = resource_size(resource);
+	if (mailbox_size < GOODIX_EC_MMIO_SIZE)
+		return dev_err_probe(dev, -EINVAL,
+				     "EC mailbox resource too small: %#llx\n",
+				     (unsigned long long)mailbox_size);
+
+	gdev->mailbox_phys = resource->start;
+	gdev->mailbox_size = mailbox_size;
+
+	gdev->tx_buf = devm_kzalloc(dev, GOODIX_EC_TX_SIZE, GFP_KERNEL);
+	if (!gdev->tx_buf)
+		return -ENOMEM;
+
+	gdev->rx_buf = devm_kzalloc(dev, GOODIX_EC_RX_SIZE, GFP_KERNEL);
+	if (!gdev->rx_buf)
+		return -ENOMEM;
+
+	ret = goodix_ec_add_gpio_mappings(dev);
+	if (ret)
+		return ret;
+
+	ret = goodix_ec_get_gpio(gdev, &gdev->write_done_gpio,
+				 "write-done", GPIOD_OUT_LOW);
+	if (ret)
+		return ret;
+
+	ret = goodix_ec_get_gpio(gdev, &gdev->read_done_gpio,
+				 "read-done", GPIOD_OUT_LOW);
+	if (ret)
+		return ret;
+
+	ret = goodix_ec_get_gpio(gdev, &gdev->irq_gpio,
+				 "irq", GPIOD_IN);
+	if (ret)
+		return ret;
+
+	gdev->irq = gpiod_to_irq(gdev->irq_gpio);
+	if (gdev->irq < 0)
+		return dev_err_probe(dev, gdev->irq,
+				     "failed to map IRQ GPIO to an IRQ\n");
+
+	irq_type = irq_get_trigger_type(gdev->irq);
+	if (irq_type == IRQ_TYPE_NONE) {
+		ret = irq_set_irq_type(gdev->irq, IRQ_TYPE_LEVEL_HIGH);
+		if (ret)
+			return dev_err_probe(dev, ret,
+					     "failed to set level-high IRQ trigger\n");
+		irq_type = IRQ_TYPE_LEVEL_HIGH;
+	}
+
+	dev_info(dev, "bound %s: mailbox=%pa size=%#llx irq=%d\n",
+		 gdev->model->name, &gdev->mailbox_phys,
+		 (unsigned long long)gdev->mailbox_size, gdev->irq);
+
+	if (gdev->irq > 0) {
+		irq_flags = IRQF_ONESHOT | IRQF_NO_AUTOEN;
+		ret = devm_request_threaded_irq(dev,
+						gdev->irq,
+						NULL,
+						goodix_ec_irq_thread,
+						irq_flags,
+						dev_name(dev),
+						gdev);
+		if (ret)
+			return dev_err_probe(dev, ret,
+					     "failed to request IRQ %d\n",
+					     gdev->irq);
+
+		dev_dbg(dev, "IRQ %d registered: flags=%#lx trigger=%#x\n",
+			gdev->irq, irq_flags, irq_type);
+	}
+
+	ret = goodix_ec_uapi_register(gdev);
+	if (ret)
+		return dev_err_probe(dev, ret,
+				     "failed to register userspace interface\n");
+
+	enable_irq(gdev->irq);
+	gdev->irq_enabled = true;
+	dev_info(dev, "IRQ %d armed\n", gdev->irq);
+
+	dev_info(dev, "EC mailbox transport ready\n");
+	return 0;
+}
+
+static void goodix_ec_remove(struct platform_device *pdev)
+{
+	struct goodix_device *gdev = platform_get_drvdata(pdev);
+
+	if (!gdev)
+		return;
+
+	WRITE_ONCE(gdev->disconnected, true);
+	if (gdev->irq_enabled) {
+		disable_irq(gdev->irq);
+		synchronize_irq(gdev->irq);
+		gdev->irq_enabled = false;
+	}
+
+	goodix_ec_uapi_unregister(gdev);
+	mutex_lock(&gdev->transfer_lock);
+	kfree(gdev->rx_reassembly);
+	gdev->rx_reassembly = NULL;
+	gdev->rx_reassembly_len = 0;
+	gdev->rx_reassembly_received = 0;
+	gdev->rx_reassembly_active = false;
+
+	if (gdev->write_done_gpio)
+		gpiod_set_value_cansleep(gdev->write_done_gpio, 0);
+	if (gdev->read_done_gpio)
+		gpiod_set_value_cansleep(gdev->read_done_gpio, 0);
+	mutex_unlock(&gdev->transfer_lock);
+
+	dev_info(gdev->dev, "%s detached\n", gdev->model->name);
+}
+
+static int goodix_ec_suspend(struct device *dev)
+{
+	struct goodix_device *gdev = dev_get_drvdata(dev);
+	unsigned long flags;
+
+	if (!gdev || READ_ONCE(gdev->disconnected))
+		return 0;
+
+	WRITE_ONCE(gdev->suspended, true);
+	wake_up_interruptible_all(&gdev->rx_wait);
+	if (gdev->irq_enabled) {
+		disable_irq(gdev->irq);
+		synchronize_irq(gdev->irq);
+		gdev->irq_enabled = false;
+	}
+
+	mutex_lock(&gdev->transfer_lock);
+	gpiod_set_value_cansleep(gdev->write_done_gpio, 0);
+	gpiod_set_value_cansleep(gdev->read_done_gpio, 0);
+	kfree(gdev->rx_reassembly);
+	gdev->rx_reassembly = NULL;
+	gdev->rx_reassembly_len = 0;
+	gdev->rx_reassembly_received = 0;
+	gdev->rx_reassembly_active = false;
+	mutex_unlock(&gdev->transfer_lock);
+
+	spin_lock_irqsave(&gdev->rx_fifo_lock, flags);
+	if (gdev->rx_fifo_ready)
+		kfifo_reset(&gdev->rx_fifo);
+	spin_unlock_irqrestore(&gdev->rx_fifo_lock, flags);
+	return 0;
+}
+
+static int goodix_ec_resume(struct device *dev)
+{
+	struct goodix_device *gdev = dev_get_drvdata(dev);
+
+	if (!gdev || READ_ONCE(gdev->disconnected))
+		return 0;
+
+	gpiod_set_value_cansleep(gdev->write_done_gpio, 0);
+	gpiod_set_value_cansleep(gdev->read_done_gpio, 0);
+	if (!gdev->irq_enabled) {
+		enable_irq(gdev->irq);
+		gdev->irq_enabled = true;
+	}
+	/* Permit new writes only after the receive path is armed. */
+	WRITE_ONCE(gdev->suspended, false);
+	wake_up_interruptible_all(&gdev->rx_wait);
+	return 0;
+}
+
+static DEFINE_SIMPLE_DEV_PM_OPS(goodix_ec_pm_ops, goodix_ec_suspend,
+				goodix_ec_resume);
+
+static const struct acpi_device_id goodix_ec_acpi_match[] = {
+	{
+		.id = GXFP5130_ACPI_HID,
+		.driver_data = (kernel_ulong_t)&gxfp5130_model,
+	},
+	{ }
+};
+MODULE_DEVICE_TABLE(acpi, goodix_ec_acpi_match);
+
+static struct platform_driver goodix_ec_driver = {
+	.probe = goodix_ec_probe,
+	.remove = goodix_ec_remove,
+	.driver = {
+		.name = GOODIX_EC_DRIVER_NAME,
+		.acpi_match_table = ACPI_PTR(goodix_ec_acpi_match),
+		.pm = pm_sleep_ptr(&goodix_ec_pm_ops),
+	},
+};
+module_platform_driver(goodix_ec_driver);
+
+MODULE_AUTHOR("Ertugrul Topcu <ertugtopcu0@gmail.com>");
+MODULE_DESCRIPTION("Goodix fingerprint sensors over an EC mailbox");
+MODULE_LICENSE("GPL");
diff --git a/drivers/platform/x86/goodix-ec-mailbox/goodix_ec_uapi.c b/drivers/platform/x86/goodix-ec-mailbox/goodix_ec_uapi.c
new file mode 100644
index 000000000000..d2f075acb481
--- /dev/null
+++ b/drivers/platform/x86/goodix-ec-mailbox/goodix_ec_uapi.c
@@ -0,0 +1,360 @@
+// SPDX-License-Identifier: GPL-2.0-only
+#include <linux/capability.h>
+#include <linux/errno.h>
+#include <linux/fs.h>
+#include <linux/kernel.h>
+#include <linux/kfifo.h>
+#include <linux/miscdevice.h>
+#include <linux/module.h>
+#include <linux/poll.h>
+#include <linux/slab.h>
+#include <linux/uaccess.h>
+#include <linux/unaligned.h>
+
+#include "goodix_ec_mailbox.h"
+#include <linux/goodix_ec.h>
+
+#define GOODIX_EC_RX_FIFO_BYTES (256u * 1024u)
+
+static bool goodix_ec_rx_ready(struct goodix_device *gdev)
+{
+	unsigned long flags;
+	bool ready;
+
+	spin_lock_irqsave(&gdev->rx_fifo_lock, flags);
+	ready = gdev->rx_fifo_ready && !kfifo_is_empty(&gdev->rx_fifo);
+	spin_unlock_irqrestore(&gdev->rx_fifo_lock, flags);
+
+	return ready;
+}
+
+static int goodix_ec_uapi_open(struct inode *inode, struct file *file)
+{
+	struct miscdevice *misc = file->private_data;
+	struct goodix_device *gdev =
+		container_of(misc, struct goodix_device, miscdev);
+	unsigned long flags;
+
+	if (!capable(CAP_SYS_RAWIO))
+		return -EPERM;
+	if (!goodix_ec_device_get(gdev))
+		return -ENODEV;
+	if (READ_ONCE(gdev->disconnected)) {
+		goodix_ec_device_put(gdev);
+		return -ENODEV;
+	}
+
+	if (file->f_mode & FMODE_READ) {
+		spin_lock_irqsave(&gdev->rx_fifo_lock, flags);
+		if (!gdev->rx_fifo_ready) {
+			spin_unlock_irqrestore(&gdev->rx_fifo_lock, flags);
+			goodix_ec_device_put(gdev);
+			return -ENODEV;
+		}
+
+		if (gdev->rx_reader_open) {
+			spin_unlock_irqrestore(&gdev->rx_fifo_lock, flags);
+			goodix_ec_device_put(gdev);
+			return -EBUSY;
+		}
+
+		gdev->rx_reader_open = true;
+		spin_unlock_irqrestore(&gdev->rx_fifo_lock, flags);
+	}
+
+	file->private_data = gdev;
+	return 0;
+}
+
+static int goodix_ec_uapi_release(struct inode *inode, struct file *file)
+{
+	struct goodix_device *gdev = file->private_data;
+	unsigned long flags;
+
+	if (gdev && (file->f_mode & FMODE_READ)) {
+		spin_lock_irqsave(&gdev->rx_fifo_lock, flags);
+		gdev->rx_reader_open = false;
+		spin_unlock_irqrestore(&gdev->rx_fifo_lock, flags);
+	}
+	if (gdev)
+		goodix_ec_device_put(gdev);
+
+	return 0;
+}
+
+static ssize_t goodix_ec_uapi_write(struct file *file,
+				    const char __user *user_buffer,
+				    size_t count, loff_t *position)
+{
+	struct goodix_device *gdev = file->private_data;
+	struct goodix_ec_tx_header header;
+	u8 *mp_packet;
+	size_t required;
+	int ret;
+
+	if (!gdev || !user_buffer || READ_ONCE(gdev->disconnected))
+		return -ENODEV;
+
+	if (count < sizeof(header))
+		return -EINVAL;
+
+	if (copy_from_user(&header, user_buffer, sizeof(header)))
+		return -EFAULT;
+
+	if (header.payload_len > GOODIX_EC_UAPI_TX_MAX)
+		return -EMSGSIZE;
+	if (header.reserved || header.flags)
+		return -EINVAL;
+
+	required = sizeof(header) + header.payload_len;
+	if (count != required)
+		return -EINVAL;
+
+	mp_packet = kmalloc(GOODIX_MP_HEADER_SIZE +
+			    header.payload_len, GFP_KERNEL);
+	if (!mp_packet)
+		return -ENOMEM;
+
+	mp_packet[0] = header.mp_flags;
+	put_unaligned_le16(header.payload_len, mp_packet + 1);
+	mp_packet[3] = mp_packet[0] + mp_packet[1] + mp_packet[2];
+
+	if (header.payload_len &&
+	    copy_from_user(mp_packet + sizeof(struct goodix_mp_header),
+			   user_buffer + sizeof(header),
+			   header.payload_len)) {
+		kfree(mp_packet);
+		return -EFAULT;
+	}
+
+	mutex_lock(&gdev->transfer_lock);
+	if (READ_ONCE(gdev->disconnected))
+		ret = -ENODEV;
+	else if (READ_ONCE(gdev->suspended))
+		ret = -EHOSTDOWN;
+	else
+		ret = goodix_ec_sync_send(gdev, mp_packet,
+					  sizeof(struct goodix_mp_header) +
+					  header.payload_len);
+	mutex_unlock(&gdev->transfer_lock);
+
+	kfree(mp_packet);
+
+	if (ret)
+		return ret;
+
+	return count;
+}
+
+static ssize_t goodix_ec_uapi_read(struct file *file, char __user *user_buffer,
+				   size_t count, loff_t *position)
+{
+	struct goodix_device *gdev = file->private_data;
+	struct goodix_ec_record_header header;
+	unsigned long flags;
+	u8 *record;
+	size_t required;
+	int ret;
+
+	if (!gdev || !user_buffer || READ_ONCE(gdev->disconnected))
+		return -ENODEV;
+
+	ret = mutex_lock_interruptible(&gdev->rx_read_lock);
+	if (ret)
+		return ret;
+
+	for (;;) {
+		spin_lock_irqsave(&gdev->rx_fifo_lock, flags);
+
+		if (!gdev->rx_fifo_ready) {
+			spin_unlock_irqrestore(&gdev->rx_fifo_lock, flags);
+			ret = -ENODEV;
+			goto out_unlock;
+		}
+		if (READ_ONCE(gdev->suspended)) {
+			spin_unlock_irqrestore(&gdev->rx_fifo_lock, flags);
+			ret = -EHOSTDOWN;
+			goto out_unlock;
+		}
+
+		if (kfifo_len(&gdev->rx_fifo) >= sizeof(header))
+			break;
+
+		spin_unlock_irqrestore(&gdev->rx_fifo_lock, flags);
+
+		if (file->f_flags & O_NONBLOCK) {
+			ret = -EAGAIN;
+			goto out_unlock;
+		}
+
+		ret = wait_event_interruptible(gdev->rx_wait,
+					       goodix_ec_rx_ready(gdev) ||
+					       !READ_ONCE(gdev->rx_fifo_ready) ||
+					       READ_ONCE(gdev->suspended));
+		if (ret)
+			goto out_unlock;
+	}
+
+	if (kfifo_out_peek(&gdev->rx_fifo, &header,
+			   sizeof(header)) != sizeof(header)) {
+		spin_unlock_irqrestore(&gdev->rx_fifo_lock, flags);
+		ret = -EIO;
+		goto out_unlock;
+	}
+
+	required = sizeof(header) + header.len;
+	if (kfifo_len(&gdev->rx_fifo) < required) {
+		spin_unlock_irqrestore(&gdev->rx_fifo_lock, flags);
+		ret = -EIO;
+		goto out_unlock;
+	}
+
+	if (count < required) {
+		spin_unlock_irqrestore(&gdev->rx_fifo_lock, flags);
+		ret = -EMSGSIZE;
+		goto out_unlock;
+	}
+
+	record = kmalloc(required, GFP_ATOMIC);
+	if (!record) {
+		spin_unlock_irqrestore(&gdev->rx_fifo_lock, flags);
+		ret = -ENOMEM;
+		goto out_unlock;
+	}
+
+	if (kfifo_out(&gdev->rx_fifo, record, required) != required) {
+		spin_unlock_irqrestore(&gdev->rx_fifo_lock, flags);
+		kfree(record);
+		ret = -EIO;
+		goto out_unlock;
+	}
+
+	spin_unlock_irqrestore(&gdev->rx_fifo_lock, flags);
+
+	if (copy_to_user(user_buffer, record, required)) {
+		kfree(record);
+		ret = -EFAULT;
+		goto out_unlock;
+	}
+
+	kfree(record);
+	ret = required;
+
+out_unlock:
+	mutex_unlock(&gdev->rx_read_lock);
+	return ret;
+}
+
+static __poll_t goodix_ec_uapi_poll(struct file *file, poll_table *wait)
+{
+	struct goodix_device *gdev = file->private_data;
+	unsigned long flags;
+	__poll_t mask = 0;
+
+	if (!gdev || READ_ONCE(gdev->disconnected))
+		return EPOLLERR;
+
+	poll_wait(file, &gdev->rx_wait, wait);
+
+	spin_lock_irqsave(&gdev->rx_fifo_lock, flags);
+	if (!gdev->rx_fifo_ready || READ_ONCE(gdev->disconnected))
+		mask = EPOLLERR | EPOLLHUP;
+	else if (READ_ONCE(gdev->suspended))
+		mask = EPOLLERR;
+	else if (!kfifo_is_empty(&gdev->rx_fifo))
+		mask = EPOLLIN | EPOLLRDNORM;
+	spin_unlock_irqrestore(&gdev->rx_fifo_lock, flags);
+
+	return mask;
+}
+
+static long goodix_ec_uapi_ioctl(struct file *file,
+				 unsigned int command,
+				 unsigned long argument)
+{
+	struct goodix_device *gdev = file->private_data;
+	unsigned long flags;
+
+	if (!gdev || READ_ONCE(gdev->disconnected))
+		return -ENODEV;
+
+	switch (command) {
+	case GOODIX_EC_IOCTL_FLUSH_RX:
+		spin_lock_irqsave(&gdev->rx_fifo_lock, flags);
+		if (gdev->rx_fifo_ready)
+			kfifo_reset(&gdev->rx_fifo);
+		spin_unlock_irqrestore(&gdev->rx_fifo_lock, flags);
+		return 0;
+	default:
+		return -ENOTTY;
+	}
+}
+
+static const struct file_operations goodix_ec_uapi_fops = {
+	.owner = THIS_MODULE,
+	.open = goodix_ec_uapi_open,
+	.release = goodix_ec_uapi_release,
+	.read = goodix_ec_uapi_read,
+	.write = goodix_ec_uapi_write,
+	.poll = goodix_ec_uapi_poll,
+	.unlocked_ioctl = goodix_ec_uapi_ioctl,
+	.compat_ioctl = compat_ptr_ioctl,
+	.llseek = noop_llseek,
+};
+
+int goodix_ec_uapi_register(struct goodix_device *gdev)
+{
+	int ret;
+
+	spin_lock_init(&gdev->rx_fifo_lock);
+	mutex_init(&gdev->rx_read_lock);
+	init_waitqueue_head(&gdev->rx_wait);
+
+	ret = kfifo_alloc(&gdev->rx_fifo, GOODIX_EC_RX_FIFO_BYTES, GFP_KERNEL);
+	if (ret)
+		return ret;
+
+	gdev->rx_fifo_ready = true;
+	gdev->rx_reader_open = false;
+
+	gdev->miscdev.minor = MISC_DYNAMIC_MINOR;
+	gdev->miscdev.name = "gxfp";
+	gdev->miscdev.fops = &goodix_ec_uapi_fops;
+	gdev->miscdev.parent = gdev->dev;
+
+	ret = misc_register(&gdev->miscdev);
+	if (ret) {
+		gdev->rx_fifo_ready = false;
+		kfifo_free(&gdev->rx_fifo);
+		return ret;
+	}
+
+	gdev->misc_registered = true;
+	dev_info(gdev->dev, "userspace interface registered: /dev/gxfp\n");
+	return 0;
+}
+
+void goodix_ec_uapi_unregister(struct goodix_device *gdev)
+{
+	unsigned long flags;
+
+	if (!gdev)
+		return;
+
+	if (gdev->misc_registered) {
+		misc_deregister(&gdev->miscdev);
+		gdev->misc_registered = false;
+	}
+
+	if (!gdev->rx_fifo_ready)
+		return;
+
+	spin_lock_irqsave(&gdev->rx_fifo_lock, flags);
+	gdev->rx_fifo_ready = false;
+	gdev->rx_reader_open = false;
+	kfifo_reset(&gdev->rx_fifo);
+	spin_unlock_irqrestore(&gdev->rx_fifo_lock, flags);
+
+	wake_up_interruptible_all(&gdev->rx_wait);
+	kfifo_free(&gdev->rx_fifo);
+}
diff --git a/include/uapi/linux/goodix_ec.h b/include/uapi/linux/goodix_ec.h
new file mode 100644
index 000000000000..ff8d0d505ae7
--- /dev/null
+++ b/include/uapi/linux/goodix_ec.h
@@ -0,0 +1,37 @@
+/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
+#ifndef _UAPI_GOODIX_EC_H_
+#define _UAPI_GOODIX_EC_H_
+
+#include <linux/ioctl.h>
+#include <linux/types.h>
+
+#define GOODIX_EC_UAPI_MAGIC		'G'
+#define GOODIX_EC_UAPI_TX_MAX		500u
+#define GOODIX_EC_UAPI_RX_MAX		(128u * 1024u)
+
+/*
+ * read(2) returns one record:
+ *   struct goodix_ec_record_header
+ *   followed by len bytes of MP payload (normally one Goodix frame).
+ */
+struct goodix_ec_record_header {
+	__u32 len;
+	__u32 mp_type;
+	__u64 timestamp_ns;
+};
+
+/*
+ * write(2) accepts:
+ *   struct goodix_ec_tx_header
+ *   followed by payload_len bytes used as the MP payload.
+ */
+struct goodix_ec_tx_header {
+	__u8 mp_flags;
+	__u8 reserved;
+	__u16 payload_len;
+	__u32 flags;
+};
+
+#define GOODIX_EC_IOCTL_FLUSH_RX	_IO(GOODIX_EC_UAPI_MAGIC, 0x11)
+
+#endif /* _UAPI_GOODIX_EC_H_ */
-- 
2.55.0


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

* [PATCH v2] platform/x86: Add Goodix fingerprint EC mailbox transport
  2026-07-27 20:58 [PATCH] platform/x86: Add Goodix fingerprint EC mailbox transport Ertuğrul Topçu
@ 2026-08-03  7:44 ` Ertuğrul Topçu
  2026-08-16 19:49   ` [PATCH v3] " Ertuğrul Topçu
  2026-08-16  1:26 ` [PATCH] " kernel test robot
  1 sibling, 1 reply; 6+ messages in thread
From: Ertuğrul Topçu @ 2026-08-03  7:44 UTC (permalink / raw)
  To: hansg, ilpo.jarvinen
  Cc: platform-driver-x86, linux-kernel, Ertuğrul Topçu

Some Goodix fingerprint sensors are connected through an ACPI-described
embedded-controller shared-memory mailbox rather than directly through USB
or SPI.

Add a transport driver for the GXFP5130 platform interface. The driver
owns the MMIO mailbox, GPIO handshakes and IRQ. It exposes opaque mailbox
records through /dev/gxfp. Keep sensor initialization, TLS, capture and
image processing in userspace.

Restrict the raw transport to CAP_SYS_RAWIO and document the UAPI. Handle
device removal and system suspend without invalidating open file
descriptors.

Signed-off-by: Ertuğrul Topçu <ertugtopcu0@gmail.com>
---

Notes:
    Changes in v2:
    - Treat IRQ 0 as valid and always register a successfully resolved IRQ.
    - Replace mailbox handshake busy waits with fsleep().
    - Remove unverified Nuvoton-specific wording, stale comments and unused model data.

 .../ABI/testing/dev-goodix-ec-mailbox         |  22 +
 MAINTAINERS                                   |   8 +
 drivers/platform/x86/Kconfig                  |   2 +
 drivers/platform/x86/Makefile                 |   3 +
 .../platform/x86/goodix-ec-mailbox/Kconfig    |  15 +
 .../platform/x86/goodix-ec-mailbox/Makefile   |   2 +
 .../x86/goodix-ec-mailbox/goodix_ec_mailbox.h | 113 +++
 .../x86/goodix-ec-mailbox/goodix_ec_main.c    | 714 ++++++++++++++++++
 .../x86/goodix-ec-mailbox/goodix_ec_uapi.c    | 360 +++++++++
 include/uapi/linux/goodix_ec.h                |  37 +
 10 files changed, 1276 insertions(+)
 create mode 100644 Documentation/ABI/testing/dev-goodix-ec-mailbox
 create mode 100644 drivers/platform/x86/goodix-ec-mailbox/Kconfig
 create mode 100644 drivers/platform/x86/goodix-ec-mailbox/Makefile
 create mode 100644 drivers/platform/x86/goodix-ec-mailbox/goodix_ec_mailbox.h
 create mode 100644 drivers/platform/x86/goodix-ec-mailbox/goodix_ec_main.c
 create mode 100644 drivers/platform/x86/goodix-ec-mailbox/goodix_ec_uapi.c
 create mode 100644 include/uapi/linux/goodix_ec.h

diff --git a/Documentation/ABI/testing/dev-goodix-ec-mailbox b/Documentation/ABI/testing/dev-goodix-ec-mailbox
new file mode 100644
index 000000000000..11ca80a41ca7
--- /dev/null
+++ b/Documentation/ABI/testing/dev-goodix-ec-mailbox
@@ -0,0 +1,22 @@
+What:		/dev/gxfp
+Date:		July 2026
+KernelVersion:	TBD
+Contact:	Ertugrul Topcu <ertugtopcu0@gmail.com>
+Description:
+		Binary userspace interface for the ACPI Goodix EC mailbox
+		fingerprint transport. Only callers with CAP_SYS_RAWIO may open
+		the device. At most one reader may be open at a time.
+
+		A write consists of struct goodix_ec_tx_header followed by exactly
+		payload_len opaque MP payload bytes. reserved and flags must be zero.
+
+		A read returns one struct goodix_ec_record_header followed by exactly
+		len bytes. mp_type is the normalized MP type and timestamp_ns is a
+		CLOCK_MONOTONIC timestamp captured when the record entered the RX
+		queue. Records are never split across reads.
+
+		poll(2) reports POLLIN while a complete record is queued. Device
+		removal reports POLLERR | POLLHUP. System suspend reports POLLERR.
+
+		GOODIX_EC_IOCTL_FLUSH_RX discards all queued receive records.
+Users:		libfprint Goodix GXFP5130 userspace driver
diff --git a/MAINTAINERS b/MAINTAINERS
index 92a2167f1eb8..0c62071bbc1b 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -11109,6 +11109,14 @@ M:	Maud Spierings <maudspierings@gocontroll.com>
 S:	Maintained
 F:	Documentation/devicetree/bindings/connector/gocontroll,moduline-module-slot.yaml
 
+GOODIX EC MAILBOX FINGERPRINT TRANSPORT DRIVER
+M:	Ertugrul Topcu <ertugtopcu0@gmail.com>
+L:	platform-driver-x86@vger.kernel.org
+S:	Maintained
+F:	Documentation/ABI/testing/dev-goodix-ec-mailbox
+F:	drivers/platform/x86/goodix-ec-mailbox/
+F:	include/uapi/linux/goodix_ec.h
+
 GOODIX TOUCHSCREEN
 M:	Hans de Goede <hansg@kernel.org>
 L:	linux-input@vger.kernel.org
diff --git a/drivers/platform/x86/Kconfig b/drivers/platform/x86/Kconfig
index 957034f39e4e..57d1311f8670 100644
--- a/drivers/platform/x86/Kconfig
+++ b/drivers/platform/x86/Kconfig
@@ -440,6 +440,8 @@ config FUJITSU_TABLET
 
          If you have a Fujitsu convertible or slate, say Y or M here.
 
+source "drivers/platform/x86/goodix-ec-mailbox/Kconfig"
+
 config GPD_POCKET_FAN
 	tristate "GPD Pocket Fan Controller support"
 	depends on ACPI
diff --git a/drivers/platform/x86/Makefile b/drivers/platform/x86/Makefile
index 872ac3842391..650ab67f7902 100644
--- a/drivers/platform/x86/Makefile
+++ b/drivers/platform/x86/Makefile
@@ -54,6 +54,9 @@ obj-$(CONFIG_AMILO_RFKILL)	+= amilo-rfkill.o
 obj-$(CONFIG_FUJITSU_LAPTOP)	+= fujitsu-laptop.o
 obj-$(CONFIG_FUJITSU_TABLET)	+= fujitsu-tablet.o
 
+# Goodix
+obj-$(CONFIG_GOODIX_EC_MAILBOX)	+= goodix-ec-mailbox/
+
 # GPD
 obj-$(CONFIG_GPD_POCKET_FAN)	+= gpd-pocket-fan.o
 
diff --git a/drivers/platform/x86/goodix-ec-mailbox/Kconfig b/drivers/platform/x86/goodix-ec-mailbox/Kconfig
new file mode 100644
index 000000000000..6f43b1323fa4
--- /dev/null
+++ b/drivers/platform/x86/goodix-ec-mailbox/Kconfig
@@ -0,0 +1,15 @@
+config GOODIX_EC_MAILBOX
+	tristate "Goodix fingerprint EC mailbox transport"
+	depends on X86
+	depends on ACPI
+	depends on GPIOLIB
+	help
+	  Enable transport support for Goodix fingerprint sensors connected
+	  through an ACPI-described embedded-controller shared-memory mailbox.
+
+	  The driver exposes the opaque mailbox transport through /dev/gxfp.
+	  Sensor configuration, TLS, capture and image processing remain in
+	  userspace.
+
+	  To compile this driver as a module, choose M here. The module will be
+	  called goodix_ec_mailbox.
diff --git a/drivers/platform/x86/goodix-ec-mailbox/Makefile b/drivers/platform/x86/goodix-ec-mailbox/Makefile
new file mode 100644
index 000000000000..52e5516eefb0
--- /dev/null
+++ b/drivers/platform/x86/goodix-ec-mailbox/Makefile
@@ -0,0 +1,2 @@
+obj-$(CONFIG_GOODIX_EC_MAILBOX) += goodix_ec_mailbox.o
+goodix_ec_mailbox-y := goodix_ec_main.o goodix_ec_uapi.o
diff --git a/drivers/platform/x86/goodix-ec-mailbox/goodix_ec_mailbox.h b/drivers/platform/x86/goodix-ec-mailbox/goodix_ec_mailbox.h
new file mode 100644
index 000000000000..6f412c46159b
--- /dev/null
+++ b/drivers/platform/x86/goodix-ec-mailbox/goodix_ec_mailbox.h
@@ -0,0 +1,113 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef _GOODIX_EC_H_
+#define _GOODIX_EC_H_
+
+#include <linux/gpio/consumer.h>
+#include <linux/ioport.h>
+#include <linux/kfifo.h>
+#include <linux/kref.h>
+#include <linux/miscdevice.h>
+#include <linux/mutex.h>
+#include <linux/spinlock.h>
+#include <linux/wait.h>
+#include <linux/stddef.h>
+#include <linux/types.h>
+
+#define GOODIX_EC_DRIVER_NAME		"goodix-ec-mailbox"
+
+/* Shared-memory mailbox layout. */
+#define GOODIX_EC_MMIO_SIZE		0x1000
+#define GOODIX_EC_TX_OFFSET		0x000
+#define GOODIX_EC_TX_SIZE		0x0200
+#define GOODIX_EC_RX_OFFSET		0x0200
+#define GOODIX_EC_RX_SIZE		0x0e00
+#define GOODIX_EC_PACKET_ALIGNMENT	8
+
+/* EC mailbox framing. */
+#define GOODIX_EC_PACKET_TYPE		0xf0
+#define GOODIX_EC_SEQUENCE_SEED		0x8881
+
+/* MP framing. The payload is opaque to the kernel. */
+#define GOODIX_MP_HEADER_SIZE		4
+
+/* Mailbox handshake timing. */
+#define GOODIX_WRITE_DONE_PRE_US	50
+#define GOODIX_WRITE_DONE_HIGH_US	4000
+#define GOODIX_WRITE_DONE_POST_US	200
+#define GOODIX_READ_DONE_HIGH_US	50
+#define GOODIX_READ_DONE_POST_US	200
+#define GOODIX_SYNC_RX_DELAY_US		1000
+
+struct device;
+
+struct goodix_ec_header {
+	u8 type;
+	__le16 payload_len;
+	u8 checksum;
+	__le16 sequence;
+	u8 reserved[2];
+} __packed;
+
+struct goodix_mp_header {
+	u8 type;
+	__le16 payload_len;
+	u8 checksum;
+} __packed;
+
+struct goodix_device;
+
+struct goodix_model_data {
+	const char *name;
+};
+
+struct goodix_device {
+	struct device *dev;
+	struct kref refcount;
+	bool disconnected;
+	bool suspended;
+	bool irq_enabled;
+
+	void __iomem *mailbox;
+	resource_size_t mailbox_phys;
+	resource_size_t mailbox_size;
+
+	struct gpio_desc *write_done_gpio;
+	struct gpio_desc *read_done_gpio;
+	struct gpio_desc *irq_gpio;
+	int irq;
+
+	u8 *tx_buf;
+	u8 *rx_buf;
+
+	u8 *rx_reassembly;
+	size_t rx_reassembly_len;
+	size_t rx_reassembly_received;
+	u8 rx_reassembly_mp_type;
+	bool rx_reassembly_active;
+
+	u16 tx_sequence;
+	/* Serializes mailbox TX with threaded-IRQ RX access. */
+	struct mutex transfer_lock;
+
+	struct miscdevice miscdev;
+	struct kfifo rx_fifo;
+	/* Protects the RX FIFO and reader ownership state. */
+	spinlock_t rx_fifo_lock;
+	/* Serializes record-oriented read operations. */
+	struct mutex rx_read_lock;
+	wait_queue_head_t rx_wait;
+	bool rx_fifo_ready;
+	bool rx_reader_open;
+	bool misc_registered;
+
+	const struct goodix_model_data *model;
+};
+
+int goodix_ec_sync_send(struct goodix_device *gdev,
+			const u8 *tx, size_t tx_len);
+bool goodix_ec_device_get(struct goodix_device *gdev);
+void goodix_ec_device_put(struct goodix_device *gdev);
+int goodix_ec_uapi_register(struct goodix_device *gdev);
+void goodix_ec_uapi_unregister(struct goodix_device *gdev);
+
+#endif /* _GOODIX_EC_H_ */
diff --git a/drivers/platform/x86/goodix-ec-mailbox/goodix_ec_main.c b/drivers/platform/x86/goodix-ec-mailbox/goodix_ec_main.c
new file mode 100644
index 000000000000..92f1c6f36b19
--- /dev/null
+++ b/drivers/platform/x86/goodix-ec-mailbox/goodix_ec_main.c
@@ -0,0 +1,714 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Goodix fingerprint sensors behind an EC shared-memory mailbox.
+ *
+ * The tested GXFP5130 platform exposes the fingerprint transport through
+ * ACPI-described MMIO and GPIO handshakes. The host exchanges opaque MP
+ * payloads with the device through this mailbox.
+ *
+ * Sensor protocol policy deliberately stays in userspace. This driver does
+ * not know about Goodix commands, checksums, TLS, configuration, capture, or
+ * sensor power states.
+ */
+
+#include <linux/acpi.h>
+#include <linux/align.h>
+#include <linux/delay.h>
+#include <linux/errno.h>
+#include <linux/gpio/consumer.h>
+#include <linux/io.h>
+#include <linux/interrupt.h>
+#include <linux/irq.h>
+#include <linux/capability.h>
+#include <linux/fs.h>
+#include <linux/kernel.h>
+#include <linux/kfifo.h>
+#include <linux/miscdevice.h>
+#include <linux/poll.h>
+#include <linux/module.h>
+#include <linux/platform_device.h>
+#include <linux/pm.h>
+#include <linux/slab.h>
+#include <linux/string.h>
+#include <linux/timekeeping.h>
+#include <linux/uaccess.h>
+#include <linux/unaligned.h>
+
+#include "goodix_ec_mailbox.h"
+#include <linux/goodix_ec.h>
+
+#define GXFP5130_ACPI_HID "GXFP5130"
+
+static void goodix_ec_device_release(struct kref *refcount)
+{
+	struct goodix_device *gdev =
+		container_of(refcount, struct goodix_device, refcount);
+
+	kfree(gdev);
+}
+
+bool goodix_ec_device_get(struct goodix_device *gdev)
+{
+	return kref_get_unless_zero(&gdev->refcount);
+}
+
+void goodix_ec_device_put(struct goodix_device *gdev)
+{
+	kref_put(&gdev->refcount, goodix_ec_device_release);
+}
+
+static void goodix_ec_device_put_action(void *data)
+{
+	goodix_ec_device_put(data);
+}
+
+/* EC mailbox transport. */
+
+struct goodix_ec_acpi_gpio_state {
+	unsigned int gpio_count;
+	int irq_index;
+	int done_index[2];
+	u8 done_polarity[2];
+	unsigned int done_count;
+};
+
+static int goodix_ec_acpi_gpio_resource(struct acpi_resource *resource,
+					void *context)
+{
+	struct goodix_ec_acpi_gpio_state *state = context;
+	struct acpi_resource_gpio *gpio;
+	unsigned int index;
+
+	if (resource->type != ACPI_RESOURCE_TYPE_GPIO)
+		return 0;
+
+	gpio = &resource->data.gpio;
+	index = state->gpio_count++;
+	if (!gpio->pin_table_length)
+		return 0;
+
+	if (gpio->connection_type == ACPI_RESOURCE_GPIO_TYPE_INT) {
+		if (state->irq_index < 0)
+			state->irq_index = index;
+	} else if (gpio->connection_type == ACPI_RESOURCE_GPIO_TYPE_IO &&
+		   state->done_count < ARRAY_SIZE(state->done_index)) {
+		state->done_index[state->done_count] = index;
+		state->done_polarity[state->done_count] = gpio->polarity;
+		state->done_count++;
+	}
+
+	return 0;
+}
+
+static int goodix_ec_add_gpio_mappings(struct device *dev)
+{
+	struct goodix_ec_acpi_gpio_state state = {
+		.irq_index = -1,
+		.done_index = { -1, -1 },
+	};
+	struct acpi_gpio_mapping *mappings;
+	struct acpi_gpio_params *params;
+	struct acpi_device *adev = ACPI_COMPANION(dev);
+	LIST_HEAD(resources);
+	bool active_low;
+	int ret;
+
+	if (!adev)
+		return -ENODEV;
+
+	ret = acpi_dev_get_resources(adev, &resources,
+				     goodix_ec_acpi_gpio_resource, &state);
+	acpi_dev_free_resource_list(&resources);
+	if (ret <= 0)
+		return ret < 0 ? ret : -ENOENT;
+	if (state.irq_index < 0 || state.done_count != 2)
+		return dev_err_probe(dev, -ENOENT,
+				     "ACPI _CRS does not describe one IRQ and two done GPIOs\n");
+	if (state.done_polarity[0] != state.done_polarity[1])
+		return dev_err_probe(dev, -EINVAL,
+				     "ACPI done GPIO polarities differ\n");
+
+	active_low = state.done_polarity[0] != 0;
+	params = devm_kcalloc(dev, 3, sizeof(*params), GFP_KERNEL);
+	mappings = devm_kcalloc(dev, 4, sizeof(*mappings), GFP_KERNEL);
+	if (!params || !mappings)
+		return -ENOMEM;
+
+	params[0].crs_entry_index = state.irq_index;
+	params[0].line_index = 0;
+	params[0].active_low = false;
+	mappings[0].name = "irq-gpios";
+	mappings[0].data = &params[0];
+	mappings[0].size = 1;
+
+	params[1].crs_entry_index = state.done_index[0];
+	params[1].line_index = 0;
+	params[1].active_low = active_low;
+	mappings[1].name = "write-done-gpios";
+	mappings[1].data = &params[1];
+	mappings[1].size = 1;
+
+	params[2].crs_entry_index = state.done_index[1];
+	params[2].line_index = 0;
+	params[2].active_low = active_low;
+	mappings[2].name = "read-done-gpios";
+	mappings[2].data = &params[2];
+	mappings[2].size = 1;
+
+	ret = devm_acpi_dev_add_driver_gpios(dev, mappings);
+	if (ret)
+		return dev_err_probe(dev, ret,
+				     "failed to install ACPI GPIO mappings\n");
+
+	dev_dbg(dev,
+		"ACPI GPIO mappings: irq=%d write-done=%d read-done=%d active-low=%u\n",
+		state.irq_index, state.done_index[0], state.done_index[1],
+		active_low);
+	return 0;
+}
+
+static int goodix_ec_get_gpio(struct goodix_device *gdev,
+			      struct gpio_desc **gpio,
+			      const char *name,
+			      enum gpiod_flags flags)
+{
+	*gpio = devm_gpiod_get(gdev->dev, name, flags);
+	if (IS_ERR(*gpio))
+		return dev_err_probe(gdev->dev, PTR_ERR(*gpio),
+				     "failed to acquire %s GPIO\n", name);
+
+	if (!*gpio)
+		return dev_err_probe(gdev->dev, -ENODEV,
+				     "%s GPIO missing\n", name);
+
+	return 0;
+}
+
+static void goodix_ec_mmio_write_tx(struct goodix_device *gdev, size_t len)
+{
+	size_t offset;
+
+	for (offset = 0; offset < len; offset += sizeof(u64)) {
+		u64 value = get_unaligned_le64(gdev->tx_buf + offset);
+
+		writeq(value, gdev->mailbox + GOODIX_EC_TX_OFFSET + offset);
+	}
+
+	/* Publish every qword before asserting the write-done doorbell. */
+	wmb();
+}
+
+static void goodix_ec_mmio_read_rx(struct goodix_device *gdev)
+{
+	size_t offset;
+
+	for (offset = 0; offset < GOODIX_EC_RX_SIZE; offset += sizeof(u64)) {
+		u64 value = readq(gdev->mailbox + GOODIX_EC_RX_OFFSET + offset);
+
+		put_unaligned_le64(value, gdev->rx_buf + offset);
+	}
+
+	/* Complete the mailbox snapshot before parsing its headers. */
+	rmb();
+}
+
+static int goodix_ec_pulse_write_done(struct goodix_device *gdev)
+{
+	if (!gdev->write_done_gpio)
+		return -ENODEV;
+
+	gpiod_set_value_cansleep(gdev->write_done_gpio, 0);
+	fsleep(GOODIX_WRITE_DONE_PRE_US);
+
+	gpiod_set_value_cansleep(gdev->write_done_gpio, 1);
+	fsleep(GOODIX_WRITE_DONE_HIGH_US);
+
+	gpiod_set_value_cansleep(gdev->write_done_gpio, 0);
+	fsleep(GOODIX_WRITE_DONE_POST_US);
+
+	return 0;
+}
+
+static int goodix_ec_pulse_read_done(struct goodix_device *gdev)
+{
+	if (!gdev->read_done_gpio)
+		return -ENODEV;
+
+	/* RX must be copied before acknowledging that the host consumed it. */
+	gpiod_set_value_cansleep(gdev->read_done_gpio, 1);
+	fsleep(GOODIX_READ_DONE_HIGH_US);
+
+	gpiod_set_value_cansleep(gdev->read_done_gpio, 0);
+	fsleep(GOODIX_READ_DONE_POST_US);
+
+	return 0;
+}
+
+static int goodix_ec_build_packet(struct goodix_device *gdev,
+				  const u8 *payload, size_t payload_len,
+				  size_t *packet_len)
+{
+	struct goodix_ec_header *header;
+	size_t total_len;
+
+	if (!gdev || !payload || !payload_len || !packet_len)
+		return -EINVAL;
+
+	if (payload_len > U16_MAX)
+		return -EOVERFLOW;
+
+	total_len = ALIGN(sizeof(*header) + payload_len,
+			  GOODIX_EC_PACKET_ALIGNMENT);
+	if (total_len > GOODIX_EC_TX_SIZE)
+		return -EMSGSIZE;
+
+	memset(gdev->tx_buf, 0, total_len);
+
+	header = (struct goodix_ec_header *)gdev->tx_buf;
+	header->type = GOODIX_EC_PACKET_TYPE;
+	header->payload_len = cpu_to_le16(payload_len);
+	header->checksum = header->type + (payload_len & 0xff) +
+			   ((payload_len >> 8) & 0xff);
+	header->sequence = cpu_to_le16(++gdev->tx_sequence);
+
+	memcpy(gdev->tx_buf + sizeof(*header), payload, payload_len);
+	*packet_len = total_len;
+
+	return 0;
+}
+
+int goodix_ec_sync_send(struct goodix_device *gdev,
+			const u8 *tx, size_t tx_len)
+{
+	size_t packet_len;
+	int ret;
+
+	ret = goodix_ec_build_packet(gdev, tx, tx_len, &packet_len);
+	if (ret)
+		return ret;
+
+	goodix_ec_mmio_write_tx(gdev, packet_len);
+
+	ret = goodix_ec_pulse_write_done(gdev);
+	if (ret)
+		return ret;
+
+	fsleep(GOODIX_SYNC_RX_DELAY_US);
+	return 0;
+}
+
+/* MP packet transport. */
+static void goodix_ec_rx_push(struct goodix_device *gdev, u8 mp_type,
+			      const u8 *payload, size_t payload_len)
+{
+	struct goodix_ec_record_header header;
+	unsigned long flags;
+	size_t required;
+
+	if (!gdev || !payload || !payload_len ||
+	    payload_len > GOODIX_EC_UAPI_RX_MAX)
+		return;
+
+	memset(&header, 0, sizeof(header));
+	header.len = payload_len;
+	header.mp_type = mp_type >> 4;
+	header.timestamp_ns = ktime_get_ns();
+	required = sizeof(header) + payload_len;
+
+	spin_lock_irqsave(&gdev->rx_fifo_lock, flags);
+	if (!gdev->rx_fifo_ready || kfifo_avail(&gdev->rx_fifo) < required) {
+		spin_unlock_irqrestore(&gdev->rx_fifo_lock, flags);
+		dev_warn_ratelimited(gdev->dev,
+				     "RX queue full; dropping %zu-byte packet\n",
+				     payload_len);
+		return;
+	}
+
+	kfifo_in(&gdev->rx_fifo, &header, sizeof(header));
+	kfifo_in(&gdev->rx_fifo, payload, payload_len);
+	spin_unlock_irqrestore(&gdev->rx_fifo_lock, flags);
+
+	wake_up_interruptible(&gdev->rx_wait);
+}
+
+static irqreturn_t goodix_ec_irq_thread(int irq, void *data)
+{
+	struct goodix_device *gdev = data;
+	size_t chunk_len;
+	size_t remaining;
+	u16 declared_len;
+	u8 mp_type;
+	int ret = 0;
+	int ack_ret;
+
+	mutex_lock(&gdev->transfer_lock);
+
+	goodix_ec_mmio_read_rx(gdev);
+
+	/*
+	 * Continuation IRQs contain raw payload bytes without another
+	 * MP header.
+	 */
+	if (gdev->rx_reassembly_active) {
+		remaining = gdev->rx_reassembly_len -
+			    gdev->rx_reassembly_received;
+
+		chunk_len = min_t(size_t,
+				  remaining,
+				  GOODIX_EC_RX_SIZE);
+
+		memcpy(gdev->rx_reassembly +
+		       gdev->rx_reassembly_received,
+		       gdev->rx_buf,
+		       chunk_len);
+
+		gdev->rx_reassembly_received += chunk_len;
+
+		ack_ret = goodix_ec_pulse_read_done(gdev);
+		if (ack_ret)
+			ret = ack_ret;
+
+		if (!ret &&
+		    gdev->rx_reassembly_received ==
+		    gdev->rx_reassembly_len) {
+			dev_dbg(gdev->dev,
+				"RX reassembly complete: MP=0x%02x payload=%zu bytes\n",
+				 gdev->rx_reassembly_mp_type,
+				 gdev->rx_reassembly_len);
+
+			goodix_ec_rx_push(gdev,
+					  gdev->rx_reassembly_mp_type,
+					  gdev->rx_reassembly,
+					  gdev->rx_reassembly_len);
+
+			kfree(gdev->rx_reassembly);
+			gdev->rx_reassembly = NULL;
+			gdev->rx_reassembly_len = 0;
+			gdev->rx_reassembly_received = 0;
+			gdev->rx_reassembly_mp_type = 0;
+			gdev->rx_reassembly_active = false;
+		}
+
+		mutex_unlock(&gdev->transfer_lock);
+
+		if (ret)
+			dev_warn_ratelimited(gdev->dev,
+					     "IRQ %d continuation ACK failed: %d\n",
+					     irq, ret);
+
+		return IRQ_HANDLED;
+	}
+
+	mp_type = gdev->rx_buf[0];
+
+	if ((mp_type >> 4) != 0x0a &&
+	    (mp_type >> 4) != 0x0b &&
+	    (mp_type >> 4) != 0x0c) {
+		ret = -EBADMSG;
+		goto acknowledge;
+	}
+
+	if (gdev->rx_buf[3] !=
+	    gdev->rx_buf[0] +
+	    gdev->rx_buf[1] +
+	    gdev->rx_buf[2]) {
+		ret = -EBADMSG;
+		goto acknowledge;
+	}
+
+	declared_len = get_unaligned_le16(gdev->rx_buf + 1);
+
+	/* The complete payload fits in one mailbox transaction. */
+	if (declared_len <=
+	    GOODIX_EC_RX_SIZE -
+	    sizeof(struct goodix_mp_header)) {
+		goodix_ec_rx_push(gdev,
+				  mp_type,
+				  gdev->rx_buf +
+				  sizeof(struct goodix_mp_header),
+				  declared_len);
+
+		dev_dbg(gdev->dev,
+			"IRQ %d RX queued: MP=0x%02x payload=%u bytes\n",
+			irq,
+			mp_type >> 4,
+			declared_len);
+
+		goto acknowledge;
+	}
+
+	/* Reassemble payloads delivered across multiple IRQs. */
+	gdev->rx_reassembly = kmalloc(declared_len, GFP_KERNEL);
+	if (!gdev->rx_reassembly) {
+		ret = -ENOMEM;
+		goto acknowledge;
+	}
+
+	chunk_len = GOODIX_EC_RX_SIZE -
+		    sizeof(struct goodix_mp_header);
+
+	memcpy(gdev->rx_reassembly,
+	       gdev->rx_buf +
+	       sizeof(struct goodix_mp_header),
+	       chunk_len);
+
+	gdev->rx_reassembly_len = declared_len;
+	gdev->rx_reassembly_received = chunk_len;
+	gdev->rx_reassembly_mp_type = mp_type;
+	gdev->rx_reassembly_active = true;
+
+	dev_dbg(gdev->dev,
+		"RX reassembly started: MP=0x%02x total=%u first=%zu remaining=%zu\n",
+		 mp_type >> 4,
+		 declared_len,
+		 chunk_len,
+		 (size_t)declared_len - chunk_len);
+
+acknowledge:
+	ack_ret = goodix_ec_pulse_read_done(gdev);
+	if (!ret && ack_ret)
+		ret = ack_ret;
+
+	mutex_unlock(&gdev->transfer_lock);
+
+	if (ret)
+		dev_warn_ratelimited(gdev->dev,
+				     "IRQ %d RX handling failed: %d\n",
+				     irq, ret);
+
+	return IRQ_HANDLED;
+}
+
+static const struct goodix_model_data gxfp5130_model = {
+	.name = "GXFP5130",
+};
+
+/* ACPI platform driver and model selection. */
+
+static int goodix_ec_probe(struct platform_device *pdev)
+{
+	struct device *dev = &pdev->dev;
+	const struct acpi_device_id *match;
+	struct goodix_device *gdev;
+	struct resource *resource;
+	resource_size_t mailbox_size;
+	unsigned long irq_flags;
+	unsigned int irq_type;
+	int ret;
+
+	match = acpi_match_device(dev->driver->acpi_match_table, dev);
+	if (!match || !match->driver_data)
+		return -ENODEV;
+
+	gdev = kzalloc_obj(*gdev);
+	if (!gdev)
+		return -ENOMEM;
+	kref_init(&gdev->refcount);
+	ret = devm_add_action_or_reset(dev, goodix_ec_device_put_action, gdev);
+	if (ret)
+		return ret;
+
+	gdev->dev = dev;
+	gdev->model = (const struct goodix_model_data *)match->driver_data;
+	gdev->tx_sequence = GOODIX_EC_SEQUENCE_SEED;
+	mutex_init(&gdev->transfer_lock);
+	platform_set_drvdata(pdev, gdev);
+
+	gdev->mailbox = devm_platform_get_and_ioremap_resource(pdev, 0,
+							       &resource);
+	if (IS_ERR(gdev->mailbox))
+		return dev_err_probe(dev, PTR_ERR(gdev->mailbox),
+				     "failed to map EC mailbox MMIO resource\n");
+
+	mailbox_size = resource_size(resource);
+	if (mailbox_size < GOODIX_EC_MMIO_SIZE)
+		return dev_err_probe(dev, -EINVAL,
+				     "EC mailbox resource too small: %#llx\n",
+				     (unsigned long long)mailbox_size);
+
+	gdev->mailbox_phys = resource->start;
+	gdev->mailbox_size = mailbox_size;
+
+	gdev->tx_buf = devm_kzalloc(dev, GOODIX_EC_TX_SIZE, GFP_KERNEL);
+	if (!gdev->tx_buf)
+		return -ENOMEM;
+
+	gdev->rx_buf = devm_kzalloc(dev, GOODIX_EC_RX_SIZE, GFP_KERNEL);
+	if (!gdev->rx_buf)
+		return -ENOMEM;
+
+	ret = goodix_ec_add_gpio_mappings(dev);
+	if (ret)
+		return ret;
+
+	ret = goodix_ec_get_gpio(gdev, &gdev->write_done_gpio,
+				 "write-done", GPIOD_OUT_LOW);
+	if (ret)
+		return ret;
+
+	ret = goodix_ec_get_gpio(gdev, &gdev->read_done_gpio,
+				 "read-done", GPIOD_OUT_LOW);
+	if (ret)
+		return ret;
+
+	ret = goodix_ec_get_gpio(gdev, &gdev->irq_gpio,
+				 "irq", GPIOD_IN);
+	if (ret)
+		return ret;
+
+	gdev->irq = gpiod_to_irq(gdev->irq_gpio);
+	if (gdev->irq < 0)
+		return dev_err_probe(dev, gdev->irq,
+				     "failed to map IRQ GPIO to an IRQ\n");
+
+	irq_type = irq_get_trigger_type(gdev->irq);
+	if (irq_type == IRQ_TYPE_NONE) {
+		ret = irq_set_irq_type(gdev->irq, IRQ_TYPE_LEVEL_HIGH);
+		if (ret)
+			return dev_err_probe(dev, ret,
+					     "failed to set level-high IRQ trigger\n");
+		irq_type = IRQ_TYPE_LEVEL_HIGH;
+	}
+
+	dev_info(dev, "bound %s: mailbox=%pa size=%#llx irq=%d\n",
+		 gdev->model->name, &gdev->mailbox_phys,
+		 (unsigned long long)gdev->mailbox_size, gdev->irq);
+
+	irq_flags = IRQF_ONESHOT | IRQF_NO_AUTOEN;
+	ret = devm_request_threaded_irq(dev,
+					gdev->irq,
+					NULL,
+					goodix_ec_irq_thread,
+					irq_flags,
+					dev_name(dev),
+					gdev);
+	if (ret)
+		return dev_err_probe(dev, ret,
+				     "failed to request IRQ %d\n",
+				     gdev->irq);
+
+	dev_dbg(dev, "IRQ %d registered: flags=%#lx trigger=%#x\n",
+		gdev->irq, irq_flags, irq_type);
+
+	ret = goodix_ec_uapi_register(gdev);
+	if (ret)
+		return dev_err_probe(dev, ret,
+				     "failed to register userspace interface\n");
+
+	enable_irq(gdev->irq);
+	gdev->irq_enabled = true;
+	dev_info(dev, "IRQ %d armed\n", gdev->irq);
+
+	dev_info(dev, "EC mailbox transport ready\n");
+	return 0;
+}
+
+static void goodix_ec_remove(struct platform_device *pdev)
+{
+	struct goodix_device *gdev = platform_get_drvdata(pdev);
+
+	if (!gdev)
+		return;
+
+	WRITE_ONCE(gdev->disconnected, true);
+	if (gdev->irq_enabled) {
+		disable_irq(gdev->irq);
+		synchronize_irq(gdev->irq);
+		gdev->irq_enabled = false;
+	}
+
+	goodix_ec_uapi_unregister(gdev);
+	mutex_lock(&gdev->transfer_lock);
+	kfree(gdev->rx_reassembly);
+	gdev->rx_reassembly = NULL;
+	gdev->rx_reassembly_len = 0;
+	gdev->rx_reassembly_received = 0;
+	gdev->rx_reassembly_active = false;
+
+	if (gdev->write_done_gpio)
+		gpiod_set_value_cansleep(gdev->write_done_gpio, 0);
+	if (gdev->read_done_gpio)
+		gpiod_set_value_cansleep(gdev->read_done_gpio, 0);
+	mutex_unlock(&gdev->transfer_lock);
+
+	dev_info(gdev->dev, "%s detached\n", gdev->model->name);
+}
+
+static int goodix_ec_suspend(struct device *dev)
+{
+	struct goodix_device *gdev = dev_get_drvdata(dev);
+	unsigned long flags;
+
+	if (!gdev || READ_ONCE(gdev->disconnected))
+		return 0;
+
+	WRITE_ONCE(gdev->suspended, true);
+	wake_up_interruptible_all(&gdev->rx_wait);
+	if (gdev->irq_enabled) {
+		disable_irq(gdev->irq);
+		synchronize_irq(gdev->irq);
+		gdev->irq_enabled = false;
+	}
+
+	mutex_lock(&gdev->transfer_lock);
+	gpiod_set_value_cansleep(gdev->write_done_gpio, 0);
+	gpiod_set_value_cansleep(gdev->read_done_gpio, 0);
+	kfree(gdev->rx_reassembly);
+	gdev->rx_reassembly = NULL;
+	gdev->rx_reassembly_len = 0;
+	gdev->rx_reassembly_received = 0;
+	gdev->rx_reassembly_active = false;
+	mutex_unlock(&gdev->transfer_lock);
+
+	spin_lock_irqsave(&gdev->rx_fifo_lock, flags);
+	if (gdev->rx_fifo_ready)
+		kfifo_reset(&gdev->rx_fifo);
+	spin_unlock_irqrestore(&gdev->rx_fifo_lock, flags);
+	return 0;
+}
+
+static int goodix_ec_resume(struct device *dev)
+{
+	struct goodix_device *gdev = dev_get_drvdata(dev);
+
+	if (!gdev || READ_ONCE(gdev->disconnected))
+		return 0;
+
+	gpiod_set_value_cansleep(gdev->write_done_gpio, 0);
+	gpiod_set_value_cansleep(gdev->read_done_gpio, 0);
+	if (!gdev->irq_enabled) {
+		enable_irq(gdev->irq);
+		gdev->irq_enabled = true;
+	}
+	/* Permit new writes only after the receive path is armed. */
+	WRITE_ONCE(gdev->suspended, false);
+	wake_up_interruptible_all(&gdev->rx_wait);
+	return 0;
+}
+
+static DEFINE_SIMPLE_DEV_PM_OPS(goodix_ec_pm_ops, goodix_ec_suspend,
+				goodix_ec_resume);
+
+static const struct acpi_device_id goodix_ec_acpi_match[] = {
+	{
+		.id = GXFP5130_ACPI_HID,
+		.driver_data = (kernel_ulong_t)&gxfp5130_model,
+	},
+	{ }
+};
+MODULE_DEVICE_TABLE(acpi, goodix_ec_acpi_match);
+
+static struct platform_driver goodix_ec_driver = {
+	.probe = goodix_ec_probe,
+	.remove = goodix_ec_remove,
+	.driver = {
+		.name = GOODIX_EC_DRIVER_NAME,
+		.acpi_match_table = ACPI_PTR(goodix_ec_acpi_match),
+		.pm = pm_sleep_ptr(&goodix_ec_pm_ops),
+	},
+};
+module_platform_driver(goodix_ec_driver);
+
+MODULE_AUTHOR("Ertugrul Topcu <ertugtopcu0@gmail.com>");
+MODULE_DESCRIPTION("Goodix fingerprint sensors over an EC mailbox");
+MODULE_LICENSE("GPL");
diff --git a/drivers/platform/x86/goodix-ec-mailbox/goodix_ec_uapi.c b/drivers/platform/x86/goodix-ec-mailbox/goodix_ec_uapi.c
new file mode 100644
index 000000000000..d2f075acb481
--- /dev/null
+++ b/drivers/platform/x86/goodix-ec-mailbox/goodix_ec_uapi.c
@@ -0,0 +1,360 @@
+// SPDX-License-Identifier: GPL-2.0-only
+#include <linux/capability.h>
+#include <linux/errno.h>
+#include <linux/fs.h>
+#include <linux/kernel.h>
+#include <linux/kfifo.h>
+#include <linux/miscdevice.h>
+#include <linux/module.h>
+#include <linux/poll.h>
+#include <linux/slab.h>
+#include <linux/uaccess.h>
+#include <linux/unaligned.h>
+
+#include "goodix_ec_mailbox.h"
+#include <linux/goodix_ec.h>
+
+#define GOODIX_EC_RX_FIFO_BYTES (256u * 1024u)
+
+static bool goodix_ec_rx_ready(struct goodix_device *gdev)
+{
+	unsigned long flags;
+	bool ready;
+
+	spin_lock_irqsave(&gdev->rx_fifo_lock, flags);
+	ready = gdev->rx_fifo_ready && !kfifo_is_empty(&gdev->rx_fifo);
+	spin_unlock_irqrestore(&gdev->rx_fifo_lock, flags);
+
+	return ready;
+}
+
+static int goodix_ec_uapi_open(struct inode *inode, struct file *file)
+{
+	struct miscdevice *misc = file->private_data;
+	struct goodix_device *gdev =
+		container_of(misc, struct goodix_device, miscdev);
+	unsigned long flags;
+
+	if (!capable(CAP_SYS_RAWIO))
+		return -EPERM;
+	if (!goodix_ec_device_get(gdev))
+		return -ENODEV;
+	if (READ_ONCE(gdev->disconnected)) {
+		goodix_ec_device_put(gdev);
+		return -ENODEV;
+	}
+
+	if (file->f_mode & FMODE_READ) {
+		spin_lock_irqsave(&gdev->rx_fifo_lock, flags);
+		if (!gdev->rx_fifo_ready) {
+			spin_unlock_irqrestore(&gdev->rx_fifo_lock, flags);
+			goodix_ec_device_put(gdev);
+			return -ENODEV;
+		}
+
+		if (gdev->rx_reader_open) {
+			spin_unlock_irqrestore(&gdev->rx_fifo_lock, flags);
+			goodix_ec_device_put(gdev);
+			return -EBUSY;
+		}
+
+		gdev->rx_reader_open = true;
+		spin_unlock_irqrestore(&gdev->rx_fifo_lock, flags);
+	}
+
+	file->private_data = gdev;
+	return 0;
+}
+
+static int goodix_ec_uapi_release(struct inode *inode, struct file *file)
+{
+	struct goodix_device *gdev = file->private_data;
+	unsigned long flags;
+
+	if (gdev && (file->f_mode & FMODE_READ)) {
+		spin_lock_irqsave(&gdev->rx_fifo_lock, flags);
+		gdev->rx_reader_open = false;
+		spin_unlock_irqrestore(&gdev->rx_fifo_lock, flags);
+	}
+	if (gdev)
+		goodix_ec_device_put(gdev);
+
+	return 0;
+}
+
+static ssize_t goodix_ec_uapi_write(struct file *file,
+				    const char __user *user_buffer,
+				    size_t count, loff_t *position)
+{
+	struct goodix_device *gdev = file->private_data;
+	struct goodix_ec_tx_header header;
+	u8 *mp_packet;
+	size_t required;
+	int ret;
+
+	if (!gdev || !user_buffer || READ_ONCE(gdev->disconnected))
+		return -ENODEV;
+
+	if (count < sizeof(header))
+		return -EINVAL;
+
+	if (copy_from_user(&header, user_buffer, sizeof(header)))
+		return -EFAULT;
+
+	if (header.payload_len > GOODIX_EC_UAPI_TX_MAX)
+		return -EMSGSIZE;
+	if (header.reserved || header.flags)
+		return -EINVAL;
+
+	required = sizeof(header) + header.payload_len;
+	if (count != required)
+		return -EINVAL;
+
+	mp_packet = kmalloc(GOODIX_MP_HEADER_SIZE +
+			    header.payload_len, GFP_KERNEL);
+	if (!mp_packet)
+		return -ENOMEM;
+
+	mp_packet[0] = header.mp_flags;
+	put_unaligned_le16(header.payload_len, mp_packet + 1);
+	mp_packet[3] = mp_packet[0] + mp_packet[1] + mp_packet[2];
+
+	if (header.payload_len &&
+	    copy_from_user(mp_packet + sizeof(struct goodix_mp_header),
+			   user_buffer + sizeof(header),
+			   header.payload_len)) {
+		kfree(mp_packet);
+		return -EFAULT;
+	}
+
+	mutex_lock(&gdev->transfer_lock);
+	if (READ_ONCE(gdev->disconnected))
+		ret = -ENODEV;
+	else if (READ_ONCE(gdev->suspended))
+		ret = -EHOSTDOWN;
+	else
+		ret = goodix_ec_sync_send(gdev, mp_packet,
+					  sizeof(struct goodix_mp_header) +
+					  header.payload_len);
+	mutex_unlock(&gdev->transfer_lock);
+
+	kfree(mp_packet);
+
+	if (ret)
+		return ret;
+
+	return count;
+}
+
+static ssize_t goodix_ec_uapi_read(struct file *file, char __user *user_buffer,
+				   size_t count, loff_t *position)
+{
+	struct goodix_device *gdev = file->private_data;
+	struct goodix_ec_record_header header;
+	unsigned long flags;
+	u8 *record;
+	size_t required;
+	int ret;
+
+	if (!gdev || !user_buffer || READ_ONCE(gdev->disconnected))
+		return -ENODEV;
+
+	ret = mutex_lock_interruptible(&gdev->rx_read_lock);
+	if (ret)
+		return ret;
+
+	for (;;) {
+		spin_lock_irqsave(&gdev->rx_fifo_lock, flags);
+
+		if (!gdev->rx_fifo_ready) {
+			spin_unlock_irqrestore(&gdev->rx_fifo_lock, flags);
+			ret = -ENODEV;
+			goto out_unlock;
+		}
+		if (READ_ONCE(gdev->suspended)) {
+			spin_unlock_irqrestore(&gdev->rx_fifo_lock, flags);
+			ret = -EHOSTDOWN;
+			goto out_unlock;
+		}
+
+		if (kfifo_len(&gdev->rx_fifo) >= sizeof(header))
+			break;
+
+		spin_unlock_irqrestore(&gdev->rx_fifo_lock, flags);
+
+		if (file->f_flags & O_NONBLOCK) {
+			ret = -EAGAIN;
+			goto out_unlock;
+		}
+
+		ret = wait_event_interruptible(gdev->rx_wait,
+					       goodix_ec_rx_ready(gdev) ||
+					       !READ_ONCE(gdev->rx_fifo_ready) ||
+					       READ_ONCE(gdev->suspended));
+		if (ret)
+			goto out_unlock;
+	}
+
+	if (kfifo_out_peek(&gdev->rx_fifo, &header,
+			   sizeof(header)) != sizeof(header)) {
+		spin_unlock_irqrestore(&gdev->rx_fifo_lock, flags);
+		ret = -EIO;
+		goto out_unlock;
+	}
+
+	required = sizeof(header) + header.len;
+	if (kfifo_len(&gdev->rx_fifo) < required) {
+		spin_unlock_irqrestore(&gdev->rx_fifo_lock, flags);
+		ret = -EIO;
+		goto out_unlock;
+	}
+
+	if (count < required) {
+		spin_unlock_irqrestore(&gdev->rx_fifo_lock, flags);
+		ret = -EMSGSIZE;
+		goto out_unlock;
+	}
+
+	record = kmalloc(required, GFP_ATOMIC);
+	if (!record) {
+		spin_unlock_irqrestore(&gdev->rx_fifo_lock, flags);
+		ret = -ENOMEM;
+		goto out_unlock;
+	}
+
+	if (kfifo_out(&gdev->rx_fifo, record, required) != required) {
+		spin_unlock_irqrestore(&gdev->rx_fifo_lock, flags);
+		kfree(record);
+		ret = -EIO;
+		goto out_unlock;
+	}
+
+	spin_unlock_irqrestore(&gdev->rx_fifo_lock, flags);
+
+	if (copy_to_user(user_buffer, record, required)) {
+		kfree(record);
+		ret = -EFAULT;
+		goto out_unlock;
+	}
+
+	kfree(record);
+	ret = required;
+
+out_unlock:
+	mutex_unlock(&gdev->rx_read_lock);
+	return ret;
+}
+
+static __poll_t goodix_ec_uapi_poll(struct file *file, poll_table *wait)
+{
+	struct goodix_device *gdev = file->private_data;
+	unsigned long flags;
+	__poll_t mask = 0;
+
+	if (!gdev || READ_ONCE(gdev->disconnected))
+		return EPOLLERR;
+
+	poll_wait(file, &gdev->rx_wait, wait);
+
+	spin_lock_irqsave(&gdev->rx_fifo_lock, flags);
+	if (!gdev->rx_fifo_ready || READ_ONCE(gdev->disconnected))
+		mask = EPOLLERR | EPOLLHUP;
+	else if (READ_ONCE(gdev->suspended))
+		mask = EPOLLERR;
+	else if (!kfifo_is_empty(&gdev->rx_fifo))
+		mask = EPOLLIN | EPOLLRDNORM;
+	spin_unlock_irqrestore(&gdev->rx_fifo_lock, flags);
+
+	return mask;
+}
+
+static long goodix_ec_uapi_ioctl(struct file *file,
+				 unsigned int command,
+				 unsigned long argument)
+{
+	struct goodix_device *gdev = file->private_data;
+	unsigned long flags;
+
+	if (!gdev || READ_ONCE(gdev->disconnected))
+		return -ENODEV;
+
+	switch (command) {
+	case GOODIX_EC_IOCTL_FLUSH_RX:
+		spin_lock_irqsave(&gdev->rx_fifo_lock, flags);
+		if (gdev->rx_fifo_ready)
+			kfifo_reset(&gdev->rx_fifo);
+		spin_unlock_irqrestore(&gdev->rx_fifo_lock, flags);
+		return 0;
+	default:
+		return -ENOTTY;
+	}
+}
+
+static const struct file_operations goodix_ec_uapi_fops = {
+	.owner = THIS_MODULE,
+	.open = goodix_ec_uapi_open,
+	.release = goodix_ec_uapi_release,
+	.read = goodix_ec_uapi_read,
+	.write = goodix_ec_uapi_write,
+	.poll = goodix_ec_uapi_poll,
+	.unlocked_ioctl = goodix_ec_uapi_ioctl,
+	.compat_ioctl = compat_ptr_ioctl,
+	.llseek = noop_llseek,
+};
+
+int goodix_ec_uapi_register(struct goodix_device *gdev)
+{
+	int ret;
+
+	spin_lock_init(&gdev->rx_fifo_lock);
+	mutex_init(&gdev->rx_read_lock);
+	init_waitqueue_head(&gdev->rx_wait);
+
+	ret = kfifo_alloc(&gdev->rx_fifo, GOODIX_EC_RX_FIFO_BYTES, GFP_KERNEL);
+	if (ret)
+		return ret;
+
+	gdev->rx_fifo_ready = true;
+	gdev->rx_reader_open = false;
+
+	gdev->miscdev.minor = MISC_DYNAMIC_MINOR;
+	gdev->miscdev.name = "gxfp";
+	gdev->miscdev.fops = &goodix_ec_uapi_fops;
+	gdev->miscdev.parent = gdev->dev;
+
+	ret = misc_register(&gdev->miscdev);
+	if (ret) {
+		gdev->rx_fifo_ready = false;
+		kfifo_free(&gdev->rx_fifo);
+		return ret;
+	}
+
+	gdev->misc_registered = true;
+	dev_info(gdev->dev, "userspace interface registered: /dev/gxfp\n");
+	return 0;
+}
+
+void goodix_ec_uapi_unregister(struct goodix_device *gdev)
+{
+	unsigned long flags;
+
+	if (!gdev)
+		return;
+
+	if (gdev->misc_registered) {
+		misc_deregister(&gdev->miscdev);
+		gdev->misc_registered = false;
+	}
+
+	if (!gdev->rx_fifo_ready)
+		return;
+
+	spin_lock_irqsave(&gdev->rx_fifo_lock, flags);
+	gdev->rx_fifo_ready = false;
+	gdev->rx_reader_open = false;
+	kfifo_reset(&gdev->rx_fifo);
+	spin_unlock_irqrestore(&gdev->rx_fifo_lock, flags);
+
+	wake_up_interruptible_all(&gdev->rx_wait);
+	kfifo_free(&gdev->rx_fifo);
+}
diff --git a/include/uapi/linux/goodix_ec.h b/include/uapi/linux/goodix_ec.h
new file mode 100644
index 000000000000..ff8d0d505ae7
--- /dev/null
+++ b/include/uapi/linux/goodix_ec.h
@@ -0,0 +1,37 @@
+/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
+#ifndef _UAPI_GOODIX_EC_H_
+#define _UAPI_GOODIX_EC_H_
+
+#include <linux/ioctl.h>
+#include <linux/types.h>
+
+#define GOODIX_EC_UAPI_MAGIC		'G'
+#define GOODIX_EC_UAPI_TX_MAX		500u
+#define GOODIX_EC_UAPI_RX_MAX		(128u * 1024u)
+
+/*
+ * read(2) returns one record:
+ *   struct goodix_ec_record_header
+ *   followed by len bytes of MP payload (normally one Goodix frame).
+ */
+struct goodix_ec_record_header {
+	__u32 len;
+	__u32 mp_type;
+	__u64 timestamp_ns;
+};
+
+/*
+ * write(2) accepts:
+ *   struct goodix_ec_tx_header
+ *   followed by payload_len bytes used as the MP payload.
+ */
+struct goodix_ec_tx_header {
+	__u8 mp_flags;
+	__u8 reserved;
+	__u16 payload_len;
+	__u32 flags;
+};
+
+#define GOODIX_EC_IOCTL_FLUSH_RX	_IO(GOODIX_EC_UAPI_MAGIC, 0x11)
+
+#endif /* _UAPI_GOODIX_EC_H_ */
-- 
2.55.0


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

* Re: [PATCH] platform/x86: Add Goodix fingerprint EC mailbox transport
  2026-07-27 20:58 [PATCH] platform/x86: Add Goodix fingerprint EC mailbox transport Ertuğrul Topçu
  2026-08-03  7:44 ` [PATCH v2] " Ertuğrul Topçu
@ 2026-08-16  1:26 ` kernel test robot
  1 sibling, 0 replies; 6+ messages in thread
From: kernel test robot @ 2026-08-16  1:26 UTC (permalink / raw)
  To: Ertuğrul Topçu, hansg, ilpo.jarvinen
  Cc: oe-kbuild-all, platform-driver-x86, linux-kernel,
	Ertuğrul Topçu

Hi Ertuğrul,

kernel test robot noticed the following build errors:

[auto build test ERROR on linus/master]
[also build test ERROR on v7.2-rc7 next-20260814]
[If your patch is applied to the wrong git tree, kindly drop us a note.
And when submitting patch, we suggest to use '--base' as documented in
https://git-scm.com/docs/git-format-patch#_base_tree_information]

url:    https://github.com/intel-lab-lkp/linux/commits/Ertu-rul-Top-u/platform-x86-Add-Goodix-fingerprint-EC-mailbox-transport/20260815-121223
base:   linus/master
patch link:    https://lore.kernel.org/r/20260727205837.91413-1-ertugtopcu0%40gmail.com
patch subject: [PATCH] platform/x86: Add Goodix fingerprint EC mailbox transport
config: i386-allmodconfig (https://download.01.org/0day-ci/archive/20260816/202608160824.RMzAY05S-lkp@intel.com/config)
compiler: gcc-14 (Debian 14.2.0-19) 14.2.0
reproduce (this is a W=1 build): (https://download.01.org/0day-ci/archive/20260816/202608160824.RMzAY05S-lkp@intel.com/reproduce)

If you fix the issue in a separate patch/commit (i.e. not just a new version of
the same patch/commit), kindly add following tags
| Reported-by: kernel test robot <lkp@intel.com>
| Closes: https://lore.kernel.org/oe-kbuild-all/202608160824.RMzAY05S-lkp@intel.com/

All errors (new ones prefixed by >>):

   drivers/platform/x86/goodix-ec-mailbox/goodix_ec_main.c: In function 'goodix_ec_mmio_write_tx':
>> drivers/platform/x86/goodix-ec-mailbox/goodix_ec_main.c:204:17: error: implicit declaration of function 'writeq'; did you mean 'writel'? [-Wimplicit-function-declaration]
     204 |                 writeq(value, gdev->mailbox + GOODIX_EC_TX_OFFSET + offset);
         |                 ^~~~~~
         |                 writel
   drivers/platform/x86/goodix-ec-mailbox/goodix_ec_main.c: In function 'goodix_ec_mmio_read_rx':
>> drivers/platform/x86/goodix-ec-mailbox/goodix_ec_main.c:216:29: error: implicit declaration of function 'readq'; did you mean 'readl'? [-Wimplicit-function-declaration]
     216 |                 u64 value = readq(gdev->mailbox + GOODIX_EC_RX_OFFSET + offset);
         |                             ^~~~~
         |                             readl


vim +204 drivers/platform/x86/goodix-ec-mailbox/goodix_ec_main.c

   196	
   197	static void goodix_ec_mmio_write_tx(struct goodix_device *gdev, size_t len)
   198	{
   199		size_t offset;
   200	
   201		for (offset = 0; offset < len; offset += sizeof(u64)) {
   202			u64 value = get_unaligned_le64(gdev->tx_buf + offset);
   203	
 > 204			writeq(value, gdev->mailbox + GOODIX_EC_TX_OFFSET + offset);
   205		}
   206	
   207		/* Publish every qword before asserting the write-done doorbell. */
   208		wmb();
   209	}
   210	
   211	static void goodix_ec_mmio_read_rx(struct goodix_device *gdev)
   212	{
   213		size_t offset;
   214	
   215		for (offset = 0; offset < GOODIX_EC_RX_SIZE; offset += sizeof(u64)) {
 > 216			u64 value = readq(gdev->mailbox + GOODIX_EC_RX_OFFSET + offset);
   217	
   218			put_unaligned_le64(value, gdev->rx_buf + offset);
   219		}
   220	
   221		/* Complete the mailbox snapshot before parsing its headers. */
   222		rmb();
   223	}
   224	

--
0-DAY CI Kernel Test Service
https://github.com/intel/lkp-tests/wiki

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

* [PATCH v3] platform/x86: Add Goodix fingerprint EC mailbox transport
  2026-08-03  7:44 ` [PATCH v2] " Ertuğrul Topçu
@ 2026-08-16 19:49   ` Ertuğrul Topçu
  2026-09-01 14:12     ` Ertuğrul Topçu
  2026-09-17 13:59     ` Ilpo Järvinen
  0 siblings, 2 replies; 6+ messages in thread
From: Ertuğrul Topçu @ 2026-08-16 19:49 UTC (permalink / raw)
  To: hansg, ilpo.jarvinen
  Cc: platform-driver-x86, linux-kernel, oe-kbuild-all,
	Ertuğrul Topçu

Some Goodix fingerprint sensors are connected through an ACPI-described
embedded-controller shared-memory mailbox rather than directly through USB
or SPI.

Add a transport driver for the GXFP5130 platform interface. The driver
owns the MMIO mailbox, GPIO handshakes and IRQ. It exposes opaque mailbox
records through /dev/gxfp. Keep sensor initialization, TLS, capture and
image processing in userspace.

Restrict the raw transport to CAP_SYS_RAWIO and document the UAPI. Handle
device removal and system suspend without invalidating open file
descriptors.

Signed-off-by: Ertuğrul Topçu <ertugtopcu0@gmail.com>
---

Notes:
    Changes in v3:
    - Include the non-atomic low/high 64-bit MMIO helpers so readq() and
      writeq() are available on 32-bit builds.
    - Verify the driver with the kernel test robot's i386 W=1 configuration.

    Changes in v2:
    - Treat IRQ 0 as valid and always register a successfully resolved IRQ.
    - Replace mailbox handshake busy waits with fsleep().
    - Remove unverified Nuvoton-specific wording, stale comments and unused model data.

 .../ABI/testing/dev-goodix-ec-mailbox         |  22 +
 MAINTAINERS                                   |   8 +
 drivers/platform/x86/Kconfig                  |   2 +
 drivers/platform/x86/Makefile                 |   3 +
 .../platform/x86/goodix-ec-mailbox/Kconfig    |  15 +
 .../platform/x86/goodix-ec-mailbox/Makefile   |   2 +
 .../x86/goodix-ec-mailbox/goodix_ec_mailbox.h | 113 +++
 .../x86/goodix-ec-mailbox/goodix_ec_main.c    | 715 ++++++++++++++++++
 .../x86/goodix-ec-mailbox/goodix_ec_uapi.c    | 360 +++++++++
 include/uapi/linux/goodix_ec.h                |  37 +
 10 files changed, 1277 insertions(+)
 create mode 100644 Documentation/ABI/testing/dev-goodix-ec-mailbox
 create mode 100644 drivers/platform/x86/goodix-ec-mailbox/Kconfig
 create mode 100644 drivers/platform/x86/goodix-ec-mailbox/Makefile
 create mode 100644 drivers/platform/x86/goodix-ec-mailbox/goodix_ec_mailbox.h
 create mode 100644 drivers/platform/x86/goodix-ec-mailbox/goodix_ec_main.c
 create mode 100644 drivers/platform/x86/goodix-ec-mailbox/goodix_ec_uapi.c
 create mode 100644 include/uapi/linux/goodix_ec.h

diff --git a/Documentation/ABI/testing/dev-goodix-ec-mailbox b/Documentation/ABI/testing/dev-goodix-ec-mailbox
new file mode 100644
index 000000000000..11ca80a41ca7
--- /dev/null
+++ b/Documentation/ABI/testing/dev-goodix-ec-mailbox
@@ -0,0 +1,22 @@
+What:		/dev/gxfp
+Date:		July 2026
+KernelVersion:	TBD
+Contact:	Ertugrul Topcu <ertugtopcu0@gmail.com>
+Description:
+		Binary userspace interface for the ACPI Goodix EC mailbox
+		fingerprint transport. Only callers with CAP_SYS_RAWIO may open
+		the device. At most one reader may be open at a time.
+
+		A write consists of struct goodix_ec_tx_header followed by exactly
+		payload_len opaque MP payload bytes. reserved and flags must be zero.
+
+		A read returns one struct goodix_ec_record_header followed by exactly
+		len bytes. mp_type is the normalized MP type and timestamp_ns is a
+		CLOCK_MONOTONIC timestamp captured when the record entered the RX
+		queue. Records are never split across reads.
+
+		poll(2) reports POLLIN while a complete record is queued. Device
+		removal reports POLLERR | POLLHUP. System suspend reports POLLERR.
+
+		GOODIX_EC_IOCTL_FLUSH_RX discards all queued receive records.
+Users:		libfprint Goodix GXFP5130 userspace driver
diff --git a/MAINTAINERS b/MAINTAINERS
index 92a2167f1eb8..0c62071bbc1b 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -11109,6 +11109,14 @@ M:	Maud Spierings <maudspierings@gocontroll.com>
 S:	Maintained
 F:	Documentation/devicetree/bindings/connector/gocontroll,moduline-module-slot.yaml
 
+GOODIX EC MAILBOX FINGERPRINT TRANSPORT DRIVER
+M:	Ertugrul Topcu <ertugtopcu0@gmail.com>
+L:	platform-driver-x86@vger.kernel.org
+S:	Maintained
+F:	Documentation/ABI/testing/dev-goodix-ec-mailbox
+F:	drivers/platform/x86/goodix-ec-mailbox/
+F:	include/uapi/linux/goodix_ec.h
+
 GOODIX TOUCHSCREEN
 M:	Hans de Goede <hansg@kernel.org>
 L:	linux-input@vger.kernel.org
diff --git a/drivers/platform/x86/Kconfig b/drivers/platform/x86/Kconfig
index 957034f39e4e..57d1311f8670 100644
--- a/drivers/platform/x86/Kconfig
+++ b/drivers/platform/x86/Kconfig
@@ -440,6 +440,8 @@ config FUJITSU_TABLET
 
          If you have a Fujitsu convertible or slate, say Y or M here.
 
+source "drivers/platform/x86/goodix-ec-mailbox/Kconfig"
+
 config GPD_POCKET_FAN
 	tristate "GPD Pocket Fan Controller support"
 	depends on ACPI
diff --git a/drivers/platform/x86/Makefile b/drivers/platform/x86/Makefile
index 872ac3842391..650ab67f7902 100644
--- a/drivers/platform/x86/Makefile
+++ b/drivers/platform/x86/Makefile
@@ -54,6 +54,9 @@ obj-$(CONFIG_AMILO_RFKILL)	+= amilo-rfkill.o
 obj-$(CONFIG_FUJITSU_LAPTOP)	+= fujitsu-laptop.o
 obj-$(CONFIG_FUJITSU_TABLET)	+= fujitsu-tablet.o
 
+# Goodix
+obj-$(CONFIG_GOODIX_EC_MAILBOX)	+= goodix-ec-mailbox/
+
 # GPD
 obj-$(CONFIG_GPD_POCKET_FAN)	+= gpd-pocket-fan.o
 
diff --git a/drivers/platform/x86/goodix-ec-mailbox/Kconfig b/drivers/platform/x86/goodix-ec-mailbox/Kconfig
new file mode 100644
index 000000000000..6f43b1323fa4
--- /dev/null
+++ b/drivers/platform/x86/goodix-ec-mailbox/Kconfig
@@ -0,0 +1,15 @@
+config GOODIX_EC_MAILBOX
+	tristate "Goodix fingerprint EC mailbox transport"
+	depends on X86
+	depends on ACPI
+	depends on GPIOLIB
+	help
+	  Enable transport support for Goodix fingerprint sensors connected
+	  through an ACPI-described embedded-controller shared-memory mailbox.
+
+	  The driver exposes the opaque mailbox transport through /dev/gxfp.
+	  Sensor configuration, TLS, capture and image processing remain in
+	  userspace.
+
+	  To compile this driver as a module, choose M here. The module will be
+	  called goodix_ec_mailbox.
diff --git a/drivers/platform/x86/goodix-ec-mailbox/Makefile b/drivers/platform/x86/goodix-ec-mailbox/Makefile
new file mode 100644
index 000000000000..52e5516eefb0
--- /dev/null
+++ b/drivers/platform/x86/goodix-ec-mailbox/Makefile
@@ -0,0 +1,2 @@
+obj-$(CONFIG_GOODIX_EC_MAILBOX) += goodix_ec_mailbox.o
+goodix_ec_mailbox-y := goodix_ec_main.o goodix_ec_uapi.o
diff --git a/drivers/platform/x86/goodix-ec-mailbox/goodix_ec_mailbox.h b/drivers/platform/x86/goodix-ec-mailbox/goodix_ec_mailbox.h
new file mode 100644
index 000000000000..6f412c46159b
--- /dev/null
+++ b/drivers/platform/x86/goodix-ec-mailbox/goodix_ec_mailbox.h
@@ -0,0 +1,113 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef _GOODIX_EC_H_
+#define _GOODIX_EC_H_
+
+#include <linux/gpio/consumer.h>
+#include <linux/ioport.h>
+#include <linux/kfifo.h>
+#include <linux/kref.h>
+#include <linux/miscdevice.h>
+#include <linux/mutex.h>
+#include <linux/spinlock.h>
+#include <linux/wait.h>
+#include <linux/stddef.h>
+#include <linux/types.h>
+
+#define GOODIX_EC_DRIVER_NAME		"goodix-ec-mailbox"
+
+/* Shared-memory mailbox layout. */
+#define GOODIX_EC_MMIO_SIZE		0x1000
+#define GOODIX_EC_TX_OFFSET		0x000
+#define GOODIX_EC_TX_SIZE		0x0200
+#define GOODIX_EC_RX_OFFSET		0x0200
+#define GOODIX_EC_RX_SIZE		0x0e00
+#define GOODIX_EC_PACKET_ALIGNMENT	8
+
+/* EC mailbox framing. */
+#define GOODIX_EC_PACKET_TYPE		0xf0
+#define GOODIX_EC_SEQUENCE_SEED		0x8881
+
+/* MP framing. The payload is opaque to the kernel. */
+#define GOODIX_MP_HEADER_SIZE		4
+
+/* Mailbox handshake timing. */
+#define GOODIX_WRITE_DONE_PRE_US	50
+#define GOODIX_WRITE_DONE_HIGH_US	4000
+#define GOODIX_WRITE_DONE_POST_US	200
+#define GOODIX_READ_DONE_HIGH_US	50
+#define GOODIX_READ_DONE_POST_US	200
+#define GOODIX_SYNC_RX_DELAY_US		1000
+
+struct device;
+
+struct goodix_ec_header {
+	u8 type;
+	__le16 payload_len;
+	u8 checksum;
+	__le16 sequence;
+	u8 reserved[2];
+} __packed;
+
+struct goodix_mp_header {
+	u8 type;
+	__le16 payload_len;
+	u8 checksum;
+} __packed;
+
+struct goodix_device;
+
+struct goodix_model_data {
+	const char *name;
+};
+
+struct goodix_device {
+	struct device *dev;
+	struct kref refcount;
+	bool disconnected;
+	bool suspended;
+	bool irq_enabled;
+
+	void __iomem *mailbox;
+	resource_size_t mailbox_phys;
+	resource_size_t mailbox_size;
+
+	struct gpio_desc *write_done_gpio;
+	struct gpio_desc *read_done_gpio;
+	struct gpio_desc *irq_gpio;
+	int irq;
+
+	u8 *tx_buf;
+	u8 *rx_buf;
+
+	u8 *rx_reassembly;
+	size_t rx_reassembly_len;
+	size_t rx_reassembly_received;
+	u8 rx_reassembly_mp_type;
+	bool rx_reassembly_active;
+
+	u16 tx_sequence;
+	/* Serializes mailbox TX with threaded-IRQ RX access. */
+	struct mutex transfer_lock;
+
+	struct miscdevice miscdev;
+	struct kfifo rx_fifo;
+	/* Protects the RX FIFO and reader ownership state. */
+	spinlock_t rx_fifo_lock;
+	/* Serializes record-oriented read operations. */
+	struct mutex rx_read_lock;
+	wait_queue_head_t rx_wait;
+	bool rx_fifo_ready;
+	bool rx_reader_open;
+	bool misc_registered;
+
+	const struct goodix_model_data *model;
+};
+
+int goodix_ec_sync_send(struct goodix_device *gdev,
+			const u8 *tx, size_t tx_len);
+bool goodix_ec_device_get(struct goodix_device *gdev);
+void goodix_ec_device_put(struct goodix_device *gdev);
+int goodix_ec_uapi_register(struct goodix_device *gdev);
+void goodix_ec_uapi_unregister(struct goodix_device *gdev);
+
+#endif /* _GOODIX_EC_H_ */
diff --git a/drivers/platform/x86/goodix-ec-mailbox/goodix_ec_main.c b/drivers/platform/x86/goodix-ec-mailbox/goodix_ec_main.c
new file mode 100644
index 000000000000..67e73aa3c6df
--- /dev/null
+++ b/drivers/platform/x86/goodix-ec-mailbox/goodix_ec_main.c
@@ -0,0 +1,715 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Goodix fingerprint sensors behind an EC shared-memory mailbox.
+ *
+ * The tested GXFP5130 platform exposes the fingerprint transport through
+ * ACPI-described MMIO and GPIO handshakes. The host exchanges opaque MP
+ * payloads with the device through this mailbox.
+ *
+ * Sensor protocol policy deliberately stays in userspace. This driver does
+ * not know about Goodix commands, checksums, TLS, configuration, capture, or
+ * sensor power states.
+ */
+
+#include <linux/acpi.h>
+#include <linux/align.h>
+#include <linux/delay.h>
+#include <linux/errno.h>
+#include <linux/gpio/consumer.h>
+#include <linux/io.h>
+#include <linux/io-64-nonatomic-lo-hi.h>
+#include <linux/interrupt.h>
+#include <linux/irq.h>
+#include <linux/capability.h>
+#include <linux/fs.h>
+#include <linux/kernel.h>
+#include <linux/kfifo.h>
+#include <linux/miscdevice.h>
+#include <linux/poll.h>
+#include <linux/module.h>
+#include <linux/platform_device.h>
+#include <linux/pm.h>
+#include <linux/slab.h>
+#include <linux/string.h>
+#include <linux/timekeeping.h>
+#include <linux/uaccess.h>
+#include <linux/unaligned.h>
+
+#include "goodix_ec_mailbox.h"
+#include <linux/goodix_ec.h>
+
+#define GXFP5130_ACPI_HID "GXFP5130"
+
+static void goodix_ec_device_release(struct kref *refcount)
+{
+	struct goodix_device *gdev =
+		container_of(refcount, struct goodix_device, refcount);
+
+	kfree(gdev);
+}
+
+bool goodix_ec_device_get(struct goodix_device *gdev)
+{
+	return kref_get_unless_zero(&gdev->refcount);
+}
+
+void goodix_ec_device_put(struct goodix_device *gdev)
+{
+	kref_put(&gdev->refcount, goodix_ec_device_release);
+}
+
+static void goodix_ec_device_put_action(void *data)
+{
+	goodix_ec_device_put(data);
+}
+
+/* EC mailbox transport. */
+
+struct goodix_ec_acpi_gpio_state {
+	unsigned int gpio_count;
+	int irq_index;
+	int done_index[2];
+	u8 done_polarity[2];
+	unsigned int done_count;
+};
+
+static int goodix_ec_acpi_gpio_resource(struct acpi_resource *resource,
+					void *context)
+{
+	struct goodix_ec_acpi_gpio_state *state = context;
+	struct acpi_resource_gpio *gpio;
+	unsigned int index;
+
+	if (resource->type != ACPI_RESOURCE_TYPE_GPIO)
+		return 0;
+
+	gpio = &resource->data.gpio;
+	index = state->gpio_count++;
+	if (!gpio->pin_table_length)
+		return 0;
+
+	if (gpio->connection_type == ACPI_RESOURCE_GPIO_TYPE_INT) {
+		if (state->irq_index < 0)
+			state->irq_index = index;
+	} else if (gpio->connection_type == ACPI_RESOURCE_GPIO_TYPE_IO &&
+		   state->done_count < ARRAY_SIZE(state->done_index)) {
+		state->done_index[state->done_count] = index;
+		state->done_polarity[state->done_count] = gpio->polarity;
+		state->done_count++;
+	}
+
+	return 0;
+}
+
+static int goodix_ec_add_gpio_mappings(struct device *dev)
+{
+	struct goodix_ec_acpi_gpio_state state = {
+		.irq_index = -1,
+		.done_index = { -1, -1 },
+	};
+	struct acpi_gpio_mapping *mappings;
+	struct acpi_gpio_params *params;
+	struct acpi_device *adev = ACPI_COMPANION(dev);
+	LIST_HEAD(resources);
+	bool active_low;
+	int ret;
+
+	if (!adev)
+		return -ENODEV;
+
+	ret = acpi_dev_get_resources(adev, &resources,
+				     goodix_ec_acpi_gpio_resource, &state);
+	acpi_dev_free_resource_list(&resources);
+	if (ret <= 0)
+		return ret < 0 ? ret : -ENOENT;
+	if (state.irq_index < 0 || state.done_count != 2)
+		return dev_err_probe(dev, -ENOENT,
+				     "ACPI _CRS does not describe one IRQ and two done GPIOs\n");
+	if (state.done_polarity[0] != state.done_polarity[1])
+		return dev_err_probe(dev, -EINVAL,
+				     "ACPI done GPIO polarities differ\n");
+
+	active_low = state.done_polarity[0] != 0;
+	params = devm_kcalloc(dev, 3, sizeof(*params), GFP_KERNEL);
+	mappings = devm_kcalloc(dev, 4, sizeof(*mappings), GFP_KERNEL);
+	if (!params || !mappings)
+		return -ENOMEM;
+
+	params[0].crs_entry_index = state.irq_index;
+	params[0].line_index = 0;
+	params[0].active_low = false;
+	mappings[0].name = "irq-gpios";
+	mappings[0].data = &params[0];
+	mappings[0].size = 1;
+
+	params[1].crs_entry_index = state.done_index[0];
+	params[1].line_index = 0;
+	params[1].active_low = active_low;
+	mappings[1].name = "write-done-gpios";
+	mappings[1].data = &params[1];
+	mappings[1].size = 1;
+
+	params[2].crs_entry_index = state.done_index[1];
+	params[2].line_index = 0;
+	params[2].active_low = active_low;
+	mappings[2].name = "read-done-gpios";
+	mappings[2].data = &params[2];
+	mappings[2].size = 1;
+
+	ret = devm_acpi_dev_add_driver_gpios(dev, mappings);
+	if (ret)
+		return dev_err_probe(dev, ret,
+				     "failed to install ACPI GPIO mappings\n");
+
+	dev_dbg(dev,
+		"ACPI GPIO mappings: irq=%d write-done=%d read-done=%d active-low=%u\n",
+		state.irq_index, state.done_index[0], state.done_index[1],
+		active_low);
+	return 0;
+}
+
+static int goodix_ec_get_gpio(struct goodix_device *gdev,
+			      struct gpio_desc **gpio,
+			      const char *name,
+			      enum gpiod_flags flags)
+{
+	*gpio = devm_gpiod_get(gdev->dev, name, flags);
+	if (IS_ERR(*gpio))
+		return dev_err_probe(gdev->dev, PTR_ERR(*gpio),
+				     "failed to acquire %s GPIO\n", name);
+
+	if (!*gpio)
+		return dev_err_probe(gdev->dev, -ENODEV,
+				     "%s GPIO missing\n", name);
+
+	return 0;
+}
+
+static void goodix_ec_mmio_write_tx(struct goodix_device *gdev, size_t len)
+{
+	size_t offset;
+
+	for (offset = 0; offset < len; offset += sizeof(u64)) {
+		u64 value = get_unaligned_le64(gdev->tx_buf + offset);
+
+		writeq(value, gdev->mailbox + GOODIX_EC_TX_OFFSET + offset);
+	}
+
+	/* Publish every qword before asserting the write-done doorbell. */
+	wmb();
+}
+
+static void goodix_ec_mmio_read_rx(struct goodix_device *gdev)
+{
+	size_t offset;
+
+	for (offset = 0; offset < GOODIX_EC_RX_SIZE; offset += sizeof(u64)) {
+		u64 value = readq(gdev->mailbox + GOODIX_EC_RX_OFFSET + offset);
+
+		put_unaligned_le64(value, gdev->rx_buf + offset);
+	}
+
+	/* Complete the mailbox snapshot before parsing its headers. */
+	rmb();
+}
+
+static int goodix_ec_pulse_write_done(struct goodix_device *gdev)
+{
+	if (!gdev->write_done_gpio)
+		return -ENODEV;
+
+	gpiod_set_value_cansleep(gdev->write_done_gpio, 0);
+	fsleep(GOODIX_WRITE_DONE_PRE_US);
+
+	gpiod_set_value_cansleep(gdev->write_done_gpio, 1);
+	fsleep(GOODIX_WRITE_DONE_HIGH_US);
+
+	gpiod_set_value_cansleep(gdev->write_done_gpio, 0);
+	fsleep(GOODIX_WRITE_DONE_POST_US);
+
+	return 0;
+}
+
+static int goodix_ec_pulse_read_done(struct goodix_device *gdev)
+{
+	if (!gdev->read_done_gpio)
+		return -ENODEV;
+
+	/* RX must be copied before acknowledging that the host consumed it. */
+	gpiod_set_value_cansleep(gdev->read_done_gpio, 1);
+	fsleep(GOODIX_READ_DONE_HIGH_US);
+
+	gpiod_set_value_cansleep(gdev->read_done_gpio, 0);
+	fsleep(GOODIX_READ_DONE_POST_US);
+
+	return 0;
+}
+
+static int goodix_ec_build_packet(struct goodix_device *gdev,
+				  const u8 *payload, size_t payload_len,
+				  size_t *packet_len)
+{
+	struct goodix_ec_header *header;
+	size_t total_len;
+
+	if (!gdev || !payload || !payload_len || !packet_len)
+		return -EINVAL;
+
+	if (payload_len > U16_MAX)
+		return -EOVERFLOW;
+
+	total_len = ALIGN(sizeof(*header) + payload_len,
+			  GOODIX_EC_PACKET_ALIGNMENT);
+	if (total_len > GOODIX_EC_TX_SIZE)
+		return -EMSGSIZE;
+
+	memset(gdev->tx_buf, 0, total_len);
+
+	header = (struct goodix_ec_header *)gdev->tx_buf;
+	header->type = GOODIX_EC_PACKET_TYPE;
+	header->payload_len = cpu_to_le16(payload_len);
+	header->checksum = header->type + (payload_len & 0xff) +
+			   ((payload_len >> 8) & 0xff);
+	header->sequence = cpu_to_le16(++gdev->tx_sequence);
+
+	memcpy(gdev->tx_buf + sizeof(*header), payload, payload_len);
+	*packet_len = total_len;
+
+	return 0;
+}
+
+int goodix_ec_sync_send(struct goodix_device *gdev,
+			const u8 *tx, size_t tx_len)
+{
+	size_t packet_len;
+	int ret;
+
+	ret = goodix_ec_build_packet(gdev, tx, tx_len, &packet_len);
+	if (ret)
+		return ret;
+
+	goodix_ec_mmio_write_tx(gdev, packet_len);
+
+	ret = goodix_ec_pulse_write_done(gdev);
+	if (ret)
+		return ret;
+
+	fsleep(GOODIX_SYNC_RX_DELAY_US);
+	return 0;
+}
+
+/* MP packet transport. */
+static void goodix_ec_rx_push(struct goodix_device *gdev, u8 mp_type,
+			      const u8 *payload, size_t payload_len)
+{
+	struct goodix_ec_record_header header;
+	unsigned long flags;
+	size_t required;
+
+	if (!gdev || !payload || !payload_len ||
+	    payload_len > GOODIX_EC_UAPI_RX_MAX)
+		return;
+
+	memset(&header, 0, sizeof(header));
+	header.len = payload_len;
+	header.mp_type = mp_type >> 4;
+	header.timestamp_ns = ktime_get_ns();
+	required = sizeof(header) + payload_len;
+
+	spin_lock_irqsave(&gdev->rx_fifo_lock, flags);
+	if (!gdev->rx_fifo_ready || kfifo_avail(&gdev->rx_fifo) < required) {
+		spin_unlock_irqrestore(&gdev->rx_fifo_lock, flags);
+		dev_warn_ratelimited(gdev->dev,
+				     "RX queue full; dropping %zu-byte packet\n",
+				     payload_len);
+		return;
+	}
+
+	kfifo_in(&gdev->rx_fifo, &header, sizeof(header));
+	kfifo_in(&gdev->rx_fifo, payload, payload_len);
+	spin_unlock_irqrestore(&gdev->rx_fifo_lock, flags);
+
+	wake_up_interruptible(&gdev->rx_wait);
+}
+
+static irqreturn_t goodix_ec_irq_thread(int irq, void *data)
+{
+	struct goodix_device *gdev = data;
+	size_t chunk_len;
+	size_t remaining;
+	u16 declared_len;
+	u8 mp_type;
+	int ret = 0;
+	int ack_ret;
+
+	mutex_lock(&gdev->transfer_lock);
+
+	goodix_ec_mmio_read_rx(gdev);
+
+	/*
+	 * Continuation IRQs contain raw payload bytes without another
+	 * MP header.
+	 */
+	if (gdev->rx_reassembly_active) {
+		remaining = gdev->rx_reassembly_len -
+			    gdev->rx_reassembly_received;
+
+		chunk_len = min_t(size_t,
+				  remaining,
+				  GOODIX_EC_RX_SIZE);
+
+		memcpy(gdev->rx_reassembly +
+		       gdev->rx_reassembly_received,
+		       gdev->rx_buf,
+		       chunk_len);
+
+		gdev->rx_reassembly_received += chunk_len;
+
+		ack_ret = goodix_ec_pulse_read_done(gdev);
+		if (ack_ret)
+			ret = ack_ret;
+
+		if (!ret &&
+		    gdev->rx_reassembly_received ==
+		    gdev->rx_reassembly_len) {
+			dev_dbg(gdev->dev,
+				"RX reassembly complete: MP=0x%02x payload=%zu bytes\n",
+				 gdev->rx_reassembly_mp_type,
+				 gdev->rx_reassembly_len);
+
+			goodix_ec_rx_push(gdev,
+					  gdev->rx_reassembly_mp_type,
+					  gdev->rx_reassembly,
+					  gdev->rx_reassembly_len);
+
+			kfree(gdev->rx_reassembly);
+			gdev->rx_reassembly = NULL;
+			gdev->rx_reassembly_len = 0;
+			gdev->rx_reassembly_received = 0;
+			gdev->rx_reassembly_mp_type = 0;
+			gdev->rx_reassembly_active = false;
+		}
+
+		mutex_unlock(&gdev->transfer_lock);
+
+		if (ret)
+			dev_warn_ratelimited(gdev->dev,
+					     "IRQ %d continuation ACK failed: %d\n",
+					     irq, ret);
+
+		return IRQ_HANDLED;
+	}
+
+	mp_type = gdev->rx_buf[0];
+
+	if ((mp_type >> 4) != 0x0a &&
+	    (mp_type >> 4) != 0x0b &&
+	    (mp_type >> 4) != 0x0c) {
+		ret = -EBADMSG;
+		goto acknowledge;
+	}
+
+	if (gdev->rx_buf[3] !=
+	    gdev->rx_buf[0] +
+	    gdev->rx_buf[1] +
+	    gdev->rx_buf[2]) {
+		ret = -EBADMSG;
+		goto acknowledge;
+	}
+
+	declared_len = get_unaligned_le16(gdev->rx_buf + 1);
+
+	/* The complete payload fits in one mailbox transaction. */
+	if (declared_len <=
+	    GOODIX_EC_RX_SIZE -
+	    sizeof(struct goodix_mp_header)) {
+		goodix_ec_rx_push(gdev,
+				  mp_type,
+				  gdev->rx_buf +
+				  sizeof(struct goodix_mp_header),
+				  declared_len);
+
+		dev_dbg(gdev->dev,
+			"IRQ %d RX queued: MP=0x%02x payload=%u bytes\n",
+			irq,
+			mp_type >> 4,
+			declared_len);
+
+		goto acknowledge;
+	}
+
+	/* Reassemble payloads delivered across multiple IRQs. */
+	gdev->rx_reassembly = kmalloc(declared_len, GFP_KERNEL);
+	if (!gdev->rx_reassembly) {
+		ret = -ENOMEM;
+		goto acknowledge;
+	}
+
+	chunk_len = GOODIX_EC_RX_SIZE -
+		    sizeof(struct goodix_mp_header);
+
+	memcpy(gdev->rx_reassembly,
+	       gdev->rx_buf +
+	       sizeof(struct goodix_mp_header),
+	       chunk_len);
+
+	gdev->rx_reassembly_len = declared_len;
+	gdev->rx_reassembly_received = chunk_len;
+	gdev->rx_reassembly_mp_type = mp_type;
+	gdev->rx_reassembly_active = true;
+
+	dev_dbg(gdev->dev,
+		"RX reassembly started: MP=0x%02x total=%u first=%zu remaining=%zu\n",
+		 mp_type >> 4,
+		 declared_len,
+		 chunk_len,
+		 (size_t)declared_len - chunk_len);
+
+acknowledge:
+	ack_ret = goodix_ec_pulse_read_done(gdev);
+	if (!ret && ack_ret)
+		ret = ack_ret;
+
+	mutex_unlock(&gdev->transfer_lock);
+
+	if (ret)
+		dev_warn_ratelimited(gdev->dev,
+				     "IRQ %d RX handling failed: %d\n",
+				     irq, ret);
+
+	return IRQ_HANDLED;
+}
+
+static const struct goodix_model_data gxfp5130_model = {
+	.name = "GXFP5130",
+};
+
+/* ACPI platform driver and model selection. */
+
+static int goodix_ec_probe(struct platform_device *pdev)
+{
+	struct device *dev = &pdev->dev;
+	const struct acpi_device_id *match;
+	struct goodix_device *gdev;
+	struct resource *resource;
+	resource_size_t mailbox_size;
+	unsigned long irq_flags;
+	unsigned int irq_type;
+	int ret;
+
+	match = acpi_match_device(dev->driver->acpi_match_table, dev);
+	if (!match || !match->driver_data)
+		return -ENODEV;
+
+	gdev = kzalloc_obj(*gdev);
+	if (!gdev)
+		return -ENOMEM;
+	kref_init(&gdev->refcount);
+	ret = devm_add_action_or_reset(dev, goodix_ec_device_put_action, gdev);
+	if (ret)
+		return ret;
+
+	gdev->dev = dev;
+	gdev->model = (const struct goodix_model_data *)match->driver_data;
+	gdev->tx_sequence = GOODIX_EC_SEQUENCE_SEED;
+	mutex_init(&gdev->transfer_lock);
+	platform_set_drvdata(pdev, gdev);
+
+	gdev->mailbox = devm_platform_get_and_ioremap_resource(pdev, 0,
+							       &resource);
+	if (IS_ERR(gdev->mailbox))
+		return dev_err_probe(dev, PTR_ERR(gdev->mailbox),
+				     "failed to map EC mailbox MMIO resource\n");
+
+	mailbox_size = resource_size(resource);
+	if (mailbox_size < GOODIX_EC_MMIO_SIZE)
+		return dev_err_probe(dev, -EINVAL,
+				     "EC mailbox resource too small: %#llx\n",
+				     (unsigned long long)mailbox_size);
+
+	gdev->mailbox_phys = resource->start;
+	gdev->mailbox_size = mailbox_size;
+
+	gdev->tx_buf = devm_kzalloc(dev, GOODIX_EC_TX_SIZE, GFP_KERNEL);
+	if (!gdev->tx_buf)
+		return -ENOMEM;
+
+	gdev->rx_buf = devm_kzalloc(dev, GOODIX_EC_RX_SIZE, GFP_KERNEL);
+	if (!gdev->rx_buf)
+		return -ENOMEM;
+
+	ret = goodix_ec_add_gpio_mappings(dev);
+	if (ret)
+		return ret;
+
+	ret = goodix_ec_get_gpio(gdev, &gdev->write_done_gpio,
+				 "write-done", GPIOD_OUT_LOW);
+	if (ret)
+		return ret;
+
+	ret = goodix_ec_get_gpio(gdev, &gdev->read_done_gpio,
+				 "read-done", GPIOD_OUT_LOW);
+	if (ret)
+		return ret;
+
+	ret = goodix_ec_get_gpio(gdev, &gdev->irq_gpio,
+				 "irq", GPIOD_IN);
+	if (ret)
+		return ret;
+
+	gdev->irq = gpiod_to_irq(gdev->irq_gpio);
+	if (gdev->irq < 0)
+		return dev_err_probe(dev, gdev->irq,
+				     "failed to map IRQ GPIO to an IRQ\n");
+
+	irq_type = irq_get_trigger_type(gdev->irq);
+	if (irq_type == IRQ_TYPE_NONE) {
+		ret = irq_set_irq_type(gdev->irq, IRQ_TYPE_LEVEL_HIGH);
+		if (ret)
+			return dev_err_probe(dev, ret,
+					     "failed to set level-high IRQ trigger\n");
+		irq_type = IRQ_TYPE_LEVEL_HIGH;
+	}
+
+	dev_info(dev, "bound %s: mailbox=%pa size=%#llx irq=%d\n",
+		 gdev->model->name, &gdev->mailbox_phys,
+		 (unsigned long long)gdev->mailbox_size, gdev->irq);
+
+	irq_flags = IRQF_ONESHOT | IRQF_NO_AUTOEN;
+	ret = devm_request_threaded_irq(dev,
+					gdev->irq,
+					NULL,
+					goodix_ec_irq_thread,
+					irq_flags,
+					dev_name(dev),
+					gdev);
+	if (ret)
+		return dev_err_probe(dev, ret,
+				     "failed to request IRQ %d\n",
+				     gdev->irq);
+
+	dev_dbg(dev, "IRQ %d registered: flags=%#lx trigger=%#x\n",
+		gdev->irq, irq_flags, irq_type);
+
+	ret = goodix_ec_uapi_register(gdev);
+	if (ret)
+		return dev_err_probe(dev, ret,
+				     "failed to register userspace interface\n");
+
+	enable_irq(gdev->irq);
+	gdev->irq_enabled = true;
+	dev_info(dev, "IRQ %d armed\n", gdev->irq);
+
+	dev_info(dev, "EC mailbox transport ready\n");
+	return 0;
+}
+
+static void goodix_ec_remove(struct platform_device *pdev)
+{
+	struct goodix_device *gdev = platform_get_drvdata(pdev);
+
+	if (!gdev)
+		return;
+
+	WRITE_ONCE(gdev->disconnected, true);
+	if (gdev->irq_enabled) {
+		disable_irq(gdev->irq);
+		synchronize_irq(gdev->irq);
+		gdev->irq_enabled = false;
+	}
+
+	goodix_ec_uapi_unregister(gdev);
+	mutex_lock(&gdev->transfer_lock);
+	kfree(gdev->rx_reassembly);
+	gdev->rx_reassembly = NULL;
+	gdev->rx_reassembly_len = 0;
+	gdev->rx_reassembly_received = 0;
+	gdev->rx_reassembly_active = false;
+
+	if (gdev->write_done_gpio)
+		gpiod_set_value_cansleep(gdev->write_done_gpio, 0);
+	if (gdev->read_done_gpio)
+		gpiod_set_value_cansleep(gdev->read_done_gpio, 0);
+	mutex_unlock(&gdev->transfer_lock);
+
+	dev_info(gdev->dev, "%s detached\n", gdev->model->name);
+}
+
+static int goodix_ec_suspend(struct device *dev)
+{
+	struct goodix_device *gdev = dev_get_drvdata(dev);
+	unsigned long flags;
+
+	if (!gdev || READ_ONCE(gdev->disconnected))
+		return 0;
+
+	WRITE_ONCE(gdev->suspended, true);
+	wake_up_interruptible_all(&gdev->rx_wait);
+	if (gdev->irq_enabled) {
+		disable_irq(gdev->irq);
+		synchronize_irq(gdev->irq);
+		gdev->irq_enabled = false;
+	}
+
+	mutex_lock(&gdev->transfer_lock);
+	gpiod_set_value_cansleep(gdev->write_done_gpio, 0);
+	gpiod_set_value_cansleep(gdev->read_done_gpio, 0);
+	kfree(gdev->rx_reassembly);
+	gdev->rx_reassembly = NULL;
+	gdev->rx_reassembly_len = 0;
+	gdev->rx_reassembly_received = 0;
+	gdev->rx_reassembly_active = false;
+	mutex_unlock(&gdev->transfer_lock);
+
+	spin_lock_irqsave(&gdev->rx_fifo_lock, flags);
+	if (gdev->rx_fifo_ready)
+		kfifo_reset(&gdev->rx_fifo);
+	spin_unlock_irqrestore(&gdev->rx_fifo_lock, flags);
+	return 0;
+}
+
+static int goodix_ec_resume(struct device *dev)
+{
+	struct goodix_device *gdev = dev_get_drvdata(dev);
+
+	if (!gdev || READ_ONCE(gdev->disconnected))
+		return 0;
+
+	gpiod_set_value_cansleep(gdev->write_done_gpio, 0);
+	gpiod_set_value_cansleep(gdev->read_done_gpio, 0);
+	if (!gdev->irq_enabled) {
+		enable_irq(gdev->irq);
+		gdev->irq_enabled = true;
+	}
+	/* Permit new writes only after the receive path is armed. */
+	WRITE_ONCE(gdev->suspended, false);
+	wake_up_interruptible_all(&gdev->rx_wait);
+	return 0;
+}
+
+static DEFINE_SIMPLE_DEV_PM_OPS(goodix_ec_pm_ops, goodix_ec_suspend,
+				goodix_ec_resume);
+
+static const struct acpi_device_id goodix_ec_acpi_match[] = {
+	{
+		.id = GXFP5130_ACPI_HID,
+		.driver_data = (kernel_ulong_t)&gxfp5130_model,
+	},
+	{ }
+};
+MODULE_DEVICE_TABLE(acpi, goodix_ec_acpi_match);
+
+static struct platform_driver goodix_ec_driver = {
+	.probe = goodix_ec_probe,
+	.remove = goodix_ec_remove,
+	.driver = {
+		.name = GOODIX_EC_DRIVER_NAME,
+		.acpi_match_table = ACPI_PTR(goodix_ec_acpi_match),
+		.pm = pm_sleep_ptr(&goodix_ec_pm_ops),
+	},
+};
+module_platform_driver(goodix_ec_driver);
+
+MODULE_AUTHOR("Ertugrul Topcu <ertugtopcu0@gmail.com>");
+MODULE_DESCRIPTION("Goodix fingerprint sensors over an EC mailbox");
+MODULE_LICENSE("GPL");
diff --git a/drivers/platform/x86/goodix-ec-mailbox/goodix_ec_uapi.c b/drivers/platform/x86/goodix-ec-mailbox/goodix_ec_uapi.c
new file mode 100644
index 000000000000..d2f075acb481
--- /dev/null
+++ b/drivers/platform/x86/goodix-ec-mailbox/goodix_ec_uapi.c
@@ -0,0 +1,360 @@
+// SPDX-License-Identifier: GPL-2.0-only
+#include <linux/capability.h>
+#include <linux/errno.h>
+#include <linux/fs.h>
+#include <linux/kernel.h>
+#include <linux/kfifo.h>
+#include <linux/miscdevice.h>
+#include <linux/module.h>
+#include <linux/poll.h>
+#include <linux/slab.h>
+#include <linux/uaccess.h>
+#include <linux/unaligned.h>
+
+#include "goodix_ec_mailbox.h"
+#include <linux/goodix_ec.h>
+
+#define GOODIX_EC_RX_FIFO_BYTES (256u * 1024u)
+
+static bool goodix_ec_rx_ready(struct goodix_device *gdev)
+{
+	unsigned long flags;
+	bool ready;
+
+	spin_lock_irqsave(&gdev->rx_fifo_lock, flags);
+	ready = gdev->rx_fifo_ready && !kfifo_is_empty(&gdev->rx_fifo);
+	spin_unlock_irqrestore(&gdev->rx_fifo_lock, flags);
+
+	return ready;
+}
+
+static int goodix_ec_uapi_open(struct inode *inode, struct file *file)
+{
+	struct miscdevice *misc = file->private_data;
+	struct goodix_device *gdev =
+		container_of(misc, struct goodix_device, miscdev);
+	unsigned long flags;
+
+	if (!capable(CAP_SYS_RAWIO))
+		return -EPERM;
+	if (!goodix_ec_device_get(gdev))
+		return -ENODEV;
+	if (READ_ONCE(gdev->disconnected)) {
+		goodix_ec_device_put(gdev);
+		return -ENODEV;
+	}
+
+	if (file->f_mode & FMODE_READ) {
+		spin_lock_irqsave(&gdev->rx_fifo_lock, flags);
+		if (!gdev->rx_fifo_ready) {
+			spin_unlock_irqrestore(&gdev->rx_fifo_lock, flags);
+			goodix_ec_device_put(gdev);
+			return -ENODEV;
+		}
+
+		if (gdev->rx_reader_open) {
+			spin_unlock_irqrestore(&gdev->rx_fifo_lock, flags);
+			goodix_ec_device_put(gdev);
+			return -EBUSY;
+		}
+
+		gdev->rx_reader_open = true;
+		spin_unlock_irqrestore(&gdev->rx_fifo_lock, flags);
+	}
+
+	file->private_data = gdev;
+	return 0;
+}
+
+static int goodix_ec_uapi_release(struct inode *inode, struct file *file)
+{
+	struct goodix_device *gdev = file->private_data;
+	unsigned long flags;
+
+	if (gdev && (file->f_mode & FMODE_READ)) {
+		spin_lock_irqsave(&gdev->rx_fifo_lock, flags);
+		gdev->rx_reader_open = false;
+		spin_unlock_irqrestore(&gdev->rx_fifo_lock, flags);
+	}
+	if (gdev)
+		goodix_ec_device_put(gdev);
+
+	return 0;
+}
+
+static ssize_t goodix_ec_uapi_write(struct file *file,
+				    const char __user *user_buffer,
+				    size_t count, loff_t *position)
+{
+	struct goodix_device *gdev = file->private_data;
+	struct goodix_ec_tx_header header;
+	u8 *mp_packet;
+	size_t required;
+	int ret;
+
+	if (!gdev || !user_buffer || READ_ONCE(gdev->disconnected))
+		return -ENODEV;
+
+	if (count < sizeof(header))
+		return -EINVAL;
+
+	if (copy_from_user(&header, user_buffer, sizeof(header)))
+		return -EFAULT;
+
+	if (header.payload_len > GOODIX_EC_UAPI_TX_MAX)
+		return -EMSGSIZE;
+	if (header.reserved || header.flags)
+		return -EINVAL;
+
+	required = sizeof(header) + header.payload_len;
+	if (count != required)
+		return -EINVAL;
+
+	mp_packet = kmalloc(GOODIX_MP_HEADER_SIZE +
+			    header.payload_len, GFP_KERNEL);
+	if (!mp_packet)
+		return -ENOMEM;
+
+	mp_packet[0] = header.mp_flags;
+	put_unaligned_le16(header.payload_len, mp_packet + 1);
+	mp_packet[3] = mp_packet[0] + mp_packet[1] + mp_packet[2];
+
+	if (header.payload_len &&
+	    copy_from_user(mp_packet + sizeof(struct goodix_mp_header),
+			   user_buffer + sizeof(header),
+			   header.payload_len)) {
+		kfree(mp_packet);
+		return -EFAULT;
+	}
+
+	mutex_lock(&gdev->transfer_lock);
+	if (READ_ONCE(gdev->disconnected))
+		ret = -ENODEV;
+	else if (READ_ONCE(gdev->suspended))
+		ret = -EHOSTDOWN;
+	else
+		ret = goodix_ec_sync_send(gdev, mp_packet,
+					  sizeof(struct goodix_mp_header) +
+					  header.payload_len);
+	mutex_unlock(&gdev->transfer_lock);
+
+	kfree(mp_packet);
+
+	if (ret)
+		return ret;
+
+	return count;
+}
+
+static ssize_t goodix_ec_uapi_read(struct file *file, char __user *user_buffer,
+				   size_t count, loff_t *position)
+{
+	struct goodix_device *gdev = file->private_data;
+	struct goodix_ec_record_header header;
+	unsigned long flags;
+	u8 *record;
+	size_t required;
+	int ret;
+
+	if (!gdev || !user_buffer || READ_ONCE(gdev->disconnected))
+		return -ENODEV;
+
+	ret = mutex_lock_interruptible(&gdev->rx_read_lock);
+	if (ret)
+		return ret;
+
+	for (;;) {
+		spin_lock_irqsave(&gdev->rx_fifo_lock, flags);
+
+		if (!gdev->rx_fifo_ready) {
+			spin_unlock_irqrestore(&gdev->rx_fifo_lock, flags);
+			ret = -ENODEV;
+			goto out_unlock;
+		}
+		if (READ_ONCE(gdev->suspended)) {
+			spin_unlock_irqrestore(&gdev->rx_fifo_lock, flags);
+			ret = -EHOSTDOWN;
+			goto out_unlock;
+		}
+
+		if (kfifo_len(&gdev->rx_fifo) >= sizeof(header))
+			break;
+
+		spin_unlock_irqrestore(&gdev->rx_fifo_lock, flags);
+
+		if (file->f_flags & O_NONBLOCK) {
+			ret = -EAGAIN;
+			goto out_unlock;
+		}
+
+		ret = wait_event_interruptible(gdev->rx_wait,
+					       goodix_ec_rx_ready(gdev) ||
+					       !READ_ONCE(gdev->rx_fifo_ready) ||
+					       READ_ONCE(gdev->suspended));
+		if (ret)
+			goto out_unlock;
+	}
+
+	if (kfifo_out_peek(&gdev->rx_fifo, &header,
+			   sizeof(header)) != sizeof(header)) {
+		spin_unlock_irqrestore(&gdev->rx_fifo_lock, flags);
+		ret = -EIO;
+		goto out_unlock;
+	}
+
+	required = sizeof(header) + header.len;
+	if (kfifo_len(&gdev->rx_fifo) < required) {
+		spin_unlock_irqrestore(&gdev->rx_fifo_lock, flags);
+		ret = -EIO;
+		goto out_unlock;
+	}
+
+	if (count < required) {
+		spin_unlock_irqrestore(&gdev->rx_fifo_lock, flags);
+		ret = -EMSGSIZE;
+		goto out_unlock;
+	}
+
+	record = kmalloc(required, GFP_ATOMIC);
+	if (!record) {
+		spin_unlock_irqrestore(&gdev->rx_fifo_lock, flags);
+		ret = -ENOMEM;
+		goto out_unlock;
+	}
+
+	if (kfifo_out(&gdev->rx_fifo, record, required) != required) {
+		spin_unlock_irqrestore(&gdev->rx_fifo_lock, flags);
+		kfree(record);
+		ret = -EIO;
+		goto out_unlock;
+	}
+
+	spin_unlock_irqrestore(&gdev->rx_fifo_lock, flags);
+
+	if (copy_to_user(user_buffer, record, required)) {
+		kfree(record);
+		ret = -EFAULT;
+		goto out_unlock;
+	}
+
+	kfree(record);
+	ret = required;
+
+out_unlock:
+	mutex_unlock(&gdev->rx_read_lock);
+	return ret;
+}
+
+static __poll_t goodix_ec_uapi_poll(struct file *file, poll_table *wait)
+{
+	struct goodix_device *gdev = file->private_data;
+	unsigned long flags;
+	__poll_t mask = 0;
+
+	if (!gdev || READ_ONCE(gdev->disconnected))
+		return EPOLLERR;
+
+	poll_wait(file, &gdev->rx_wait, wait);
+
+	spin_lock_irqsave(&gdev->rx_fifo_lock, flags);
+	if (!gdev->rx_fifo_ready || READ_ONCE(gdev->disconnected))
+		mask = EPOLLERR | EPOLLHUP;
+	else if (READ_ONCE(gdev->suspended))
+		mask = EPOLLERR;
+	else if (!kfifo_is_empty(&gdev->rx_fifo))
+		mask = EPOLLIN | EPOLLRDNORM;
+	spin_unlock_irqrestore(&gdev->rx_fifo_lock, flags);
+
+	return mask;
+}
+
+static long goodix_ec_uapi_ioctl(struct file *file,
+				 unsigned int command,
+				 unsigned long argument)
+{
+	struct goodix_device *gdev = file->private_data;
+	unsigned long flags;
+
+	if (!gdev || READ_ONCE(gdev->disconnected))
+		return -ENODEV;
+
+	switch (command) {
+	case GOODIX_EC_IOCTL_FLUSH_RX:
+		spin_lock_irqsave(&gdev->rx_fifo_lock, flags);
+		if (gdev->rx_fifo_ready)
+			kfifo_reset(&gdev->rx_fifo);
+		spin_unlock_irqrestore(&gdev->rx_fifo_lock, flags);
+		return 0;
+	default:
+		return -ENOTTY;
+	}
+}
+
+static const struct file_operations goodix_ec_uapi_fops = {
+	.owner = THIS_MODULE,
+	.open = goodix_ec_uapi_open,
+	.release = goodix_ec_uapi_release,
+	.read = goodix_ec_uapi_read,
+	.write = goodix_ec_uapi_write,
+	.poll = goodix_ec_uapi_poll,
+	.unlocked_ioctl = goodix_ec_uapi_ioctl,
+	.compat_ioctl = compat_ptr_ioctl,
+	.llseek = noop_llseek,
+};
+
+int goodix_ec_uapi_register(struct goodix_device *gdev)
+{
+	int ret;
+
+	spin_lock_init(&gdev->rx_fifo_lock);
+	mutex_init(&gdev->rx_read_lock);
+	init_waitqueue_head(&gdev->rx_wait);
+
+	ret = kfifo_alloc(&gdev->rx_fifo, GOODIX_EC_RX_FIFO_BYTES, GFP_KERNEL);
+	if (ret)
+		return ret;
+
+	gdev->rx_fifo_ready = true;
+	gdev->rx_reader_open = false;
+
+	gdev->miscdev.minor = MISC_DYNAMIC_MINOR;
+	gdev->miscdev.name = "gxfp";
+	gdev->miscdev.fops = &goodix_ec_uapi_fops;
+	gdev->miscdev.parent = gdev->dev;
+
+	ret = misc_register(&gdev->miscdev);
+	if (ret) {
+		gdev->rx_fifo_ready = false;
+		kfifo_free(&gdev->rx_fifo);
+		return ret;
+	}
+
+	gdev->misc_registered = true;
+	dev_info(gdev->dev, "userspace interface registered: /dev/gxfp\n");
+	return 0;
+}
+
+void goodix_ec_uapi_unregister(struct goodix_device *gdev)
+{
+	unsigned long flags;
+
+	if (!gdev)
+		return;
+
+	if (gdev->misc_registered) {
+		misc_deregister(&gdev->miscdev);
+		gdev->misc_registered = false;
+	}
+
+	if (!gdev->rx_fifo_ready)
+		return;
+
+	spin_lock_irqsave(&gdev->rx_fifo_lock, flags);
+	gdev->rx_fifo_ready = false;
+	gdev->rx_reader_open = false;
+	kfifo_reset(&gdev->rx_fifo);
+	spin_unlock_irqrestore(&gdev->rx_fifo_lock, flags);
+
+	wake_up_interruptible_all(&gdev->rx_wait);
+	kfifo_free(&gdev->rx_fifo);
+}
diff --git a/include/uapi/linux/goodix_ec.h b/include/uapi/linux/goodix_ec.h
new file mode 100644
index 000000000000..ff8d0d505ae7
--- /dev/null
+++ b/include/uapi/linux/goodix_ec.h
@@ -0,0 +1,37 @@
+/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
+#ifndef _UAPI_GOODIX_EC_H_
+#define _UAPI_GOODIX_EC_H_
+
+#include <linux/ioctl.h>
+#include <linux/types.h>
+
+#define GOODIX_EC_UAPI_MAGIC		'G'
+#define GOODIX_EC_UAPI_TX_MAX		500u
+#define GOODIX_EC_UAPI_RX_MAX		(128u * 1024u)
+
+/*
+ * read(2) returns one record:
+ *   struct goodix_ec_record_header
+ *   followed by len bytes of MP payload (normally one Goodix frame).
+ */
+struct goodix_ec_record_header {
+	__u32 len;
+	__u32 mp_type;
+	__u64 timestamp_ns;
+};
+
+/*
+ * write(2) accepts:
+ *   struct goodix_ec_tx_header
+ *   followed by payload_len bytes used as the MP payload.
+ */
+struct goodix_ec_tx_header {
+	__u8 mp_flags;
+	__u8 reserved;
+	__u16 payload_len;
+	__u32 flags;
+};
+
+#define GOODIX_EC_IOCTL_FLUSH_RX	_IO(GOODIX_EC_UAPI_MAGIC, 0x11)
+
+#endif /* _UAPI_GOODIX_EC_H_ */

base-commit: 0d33d21e47d9dc66f91e44da3fc9220c74d93df7
-- 
2.55.0

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

* Re: [PATCH v3] platform/x86: Add Goodix fingerprint EC mailbox transport
  2026-08-16 19:49   ` [PATCH v3] " Ertuğrul Topçu
@ 2026-09-01 14:12     ` Ertuğrul Topçu
  2026-09-17 13:59     ` Ilpo Järvinen
  1 sibling, 0 replies; 6+ messages in thread
From: Ertuğrul Topçu @ 2026-09-01 14:12 UTC (permalink / raw)
  To: hansg, ilpo.jarvinen
  Cc: platform-driver-x86, linux-kernel, Ertuğrul Topçu

Hi,

Gentle ping on this patch. v3 fixes the i386 build failure reported by
the kernel test robot and has been verified with the reported i386 W=1
configuration. The driver has also been tested on the target hardware.

I would appreciate any review or feedback when convenient.

Thanks,
Ertuğrul

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

* Re: [PATCH v3] platform/x86: Add Goodix fingerprint EC mailbox transport
  2026-08-16 19:49   ` [PATCH v3] " Ertuğrul Topçu
  2026-09-01 14:12     ` Ertuğrul Topçu
@ 2026-09-17 13:59     ` Ilpo Järvinen
  1 sibling, 0 replies; 6+ messages in thread
From: Ilpo Järvinen @ 2026-09-17 13:59 UTC (permalink / raw)
  To: Ertuğrul Topçu
  Cc: Hans de Goede, platform-driver-x86, LKML, oe-kbuild-all

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

On Sun, 16 Aug 2026, Ertuğrul Topçu wrote:

> Some Goodix fingerprint sensors are connected through an ACPI-described
> embedded-controller shared-memory mailbox rather than directly through USB
> or SPI.
> 
> Add a transport driver for the GXFP5130 platform interface. The driver
> owns the MMIO mailbox, GPIO handshakes and IRQ. It exposes opaque mailbox
> records through /dev/gxfp.


> Keep sensor initialization, TLS, capture and image processing in userspace.

Is the part of the interface documented somewhere?

> Restrict the raw transport to CAP_SYS_RAWIO and document the UAPI. Handle
> device removal and system suspend without invalidating open file
> descriptors.
> 
> Signed-off-by: Ertuğrul Topçu <ertugtopcu0@gmail.com>
> ---
> 
> Notes:
>     Changes in v3:
>     - Include the non-atomic low/high 64-bit MMIO helpers so readq() and
>       writeq() are available on 32-bit builds.
>     - Verify the driver with the kernel test robot's i386 W=1 configuration.
> 
>     Changes in v2:
>     - Treat IRQ 0 as valid and always register a successfully resolved IRQ.
>     - Replace mailbox handshake busy waits with fsleep().
>     - Remove unverified Nuvoton-specific wording, stale comments and unused model data.
> 
>  .../ABI/testing/dev-goodix-ec-mailbox         |  22 +
>  MAINTAINERS                                   |   8 +
>  drivers/platform/x86/Kconfig                  |   2 +
>  drivers/platform/x86/Makefile                 |   3 +
>  .../platform/x86/goodix-ec-mailbox/Kconfig    |  15 +
>  .../platform/x86/goodix-ec-mailbox/Makefile   |   2 +
>  .../x86/goodix-ec-mailbox/goodix_ec_mailbox.h | 113 +++
>  .../x86/goodix-ec-mailbox/goodix_ec_main.c    | 715 ++++++++++++++++++
>  .../x86/goodix-ec-mailbox/goodix_ec_uapi.c    | 360 +++++++++
>  include/uapi/linux/goodix_ec.h                |  37 +
>  10 files changed, 1277 insertions(+)
>  create mode 100644 Documentation/ABI/testing/dev-goodix-ec-mailbox
>  create mode 100644 drivers/platform/x86/goodix-ec-mailbox/Kconfig
>  create mode 100644 drivers/platform/x86/goodix-ec-mailbox/Makefile
>  create mode 100644 drivers/platform/x86/goodix-ec-mailbox/goodix_ec_mailbox.h
>  create mode 100644 drivers/platform/x86/goodix-ec-mailbox/goodix_ec_main.c
>  create mode 100644 drivers/platform/x86/goodix-ec-mailbox/goodix_ec_uapi.c
>  create mode 100644 include/uapi/linux/goodix_ec.h
> 
> diff --git a/Documentation/ABI/testing/dev-goodix-ec-mailbox b/Documentation/ABI/testing/dev-goodix-ec-mailbox
> new file mode 100644
> index 000000000000..11ca80a41ca7
> --- /dev/null
> +++ b/Documentation/ABI/testing/dev-goodix-ec-mailbox
> @@ -0,0 +1,22 @@
> +What:		/dev/gxfp
> +Date:		July 2026
> +KernelVersion:	TBD
> +Contact:	Ertugrul Topcu <ertugtopcu0@gmail.com>
> +Description:
> +		Binary userspace interface for the ACPI Goodix EC mailbox
> +		fingerprint transport. Only callers with CAP_SYS_RAWIO may open
> +		the device. At most one reader may be open at a time.
> +
> +		A write consists of struct goodix_ec_tx_header followed by exactly
> +		payload_len opaque MP payload bytes. reserved and flags must be zero.
> +
> +		A read returns one struct goodix_ec_record_header followed by exactly
> +		len bytes. mp_type is the normalized MP type and timestamp_ns is a
> +		CLOCK_MONOTONIC timestamp captured when the record entered the RX
> +		queue. Records are never split across reads.
> +
> +		poll(2) reports POLLIN while a complete record is queued. Device
> +		removal reports POLLERR | POLLHUP. System suspend reports POLLERR.
> +
> +		GOODIX_EC_IOCTL_FLUSH_RX discards all queued receive records.
> +Users:		libfprint Goodix GXFP5130 userspace driver
> diff --git a/MAINTAINERS b/MAINTAINERS
> index 92a2167f1eb8..0c62071bbc1b 100644
> --- a/MAINTAINERS
> +++ b/MAINTAINERS
> @@ -11109,6 +11109,14 @@ M:	Maud Spierings <maudspierings@gocontroll.com>
>  S:	Maintained
>  F:	Documentation/devicetree/bindings/connector/gocontroll,moduline-module-slot.yaml
>  
> +GOODIX EC MAILBOX FINGERPRINT TRANSPORT DRIVER
> +M:	Ertugrul Topcu <ertugtopcu0@gmail.com>
> +L:	platform-driver-x86@vger.kernel.org
> +S:	Maintained
> +F:	Documentation/ABI/testing/dev-goodix-ec-mailbox
> +F:	drivers/platform/x86/goodix-ec-mailbox/
> +F:	include/uapi/linux/goodix_ec.h
> +
>  GOODIX TOUCHSCREEN
>  M:	Hans de Goede <hansg@kernel.org>
>  L:	linux-input@vger.kernel.org
> diff --git a/drivers/platform/x86/Kconfig b/drivers/platform/x86/Kconfig
> index 957034f39e4e..57d1311f8670 100644
> --- a/drivers/platform/x86/Kconfig
> +++ b/drivers/platform/x86/Kconfig
> @@ -440,6 +440,8 @@ config FUJITSU_TABLET
>  
>           If you have a Fujitsu convertible or slate, say Y or M here.
>  
> +source "drivers/platform/x86/goodix-ec-mailbox/Kconfig"
> +
>  config GPD_POCKET_FAN
>  	tristate "GPD Pocket Fan Controller support"
>  	depends on ACPI
> diff --git a/drivers/platform/x86/Makefile b/drivers/platform/x86/Makefile
> index 872ac3842391..650ab67f7902 100644
> --- a/drivers/platform/x86/Makefile
> +++ b/drivers/platform/x86/Makefile
> @@ -54,6 +54,9 @@ obj-$(CONFIG_AMILO_RFKILL)	+= amilo-rfkill.o
>  obj-$(CONFIG_FUJITSU_LAPTOP)	+= fujitsu-laptop.o
>  obj-$(CONFIG_FUJITSU_TABLET)	+= fujitsu-tablet.o
>  
> +# Goodix
> +obj-$(CONFIG_GOODIX_EC_MAILBOX)	+= goodix-ec-mailbox/
> +
>  # GPD
>  obj-$(CONFIG_GPD_POCKET_FAN)	+= gpd-pocket-fan.o
>  
> diff --git a/drivers/platform/x86/goodix-ec-mailbox/Kconfig b/drivers/platform/x86/goodix-ec-mailbox/Kconfig
> new file mode 100644
> index 000000000000..6f43b1323fa4
> --- /dev/null
> +++ b/drivers/platform/x86/goodix-ec-mailbox/Kconfig
> @@ -0,0 +1,15 @@
> +config GOODIX_EC_MAILBOX
> +	tristate "Goodix fingerprint EC mailbox transport"
> +	depends on X86
> +	depends on ACPI
> +	depends on GPIOLIB
> +	help
> +	  Enable transport support for Goodix fingerprint sensors connected
> +	  through an ACPI-described embedded-controller shared-memory mailbox.
> +
> +	  The driver exposes the opaque mailbox transport through /dev/gxfp.
> +	  Sensor configuration, TLS, capture and image processing remain in
> +	  userspace.
> +
> +	  To compile this driver as a module, choose M here. The module will be
> +	  called goodix_ec_mailbox.
> diff --git a/drivers/platform/x86/goodix-ec-mailbox/Makefile b/drivers/platform/x86/goodix-ec-mailbox/Makefile
> new file mode 100644
> index 000000000000..52e5516eefb0
> --- /dev/null
> +++ b/drivers/platform/x86/goodix-ec-mailbox/Makefile
> @@ -0,0 +1,2 @@
> +obj-$(CONFIG_GOODIX_EC_MAILBOX) += goodix_ec_mailbox.o
> +goodix_ec_mailbox-y := goodix_ec_main.o goodix_ec_uapi.o
> diff --git a/drivers/platform/x86/goodix-ec-mailbox/goodix_ec_mailbox.h b/drivers/platform/x86/goodix-ec-mailbox/goodix_ec_mailbox.h
> new file mode 100644
> index 000000000000..6f412c46159b
> --- /dev/null
> +++ b/drivers/platform/x86/goodix-ec-mailbox/goodix_ec_mailbox.h
> @@ -0,0 +1,113 @@
> +/* SPDX-License-Identifier: GPL-2.0-only */
> +#ifndef _GOODIX_EC_H_
> +#define _GOODIX_EC_H_
> +
> +#include <linux/gpio/consumer.h>
> +#include <linux/ioport.h>
> +#include <linux/kfifo.h>
> +#include <linux/kref.h>
> +#include <linux/miscdevice.h>
> +#include <linux/mutex.h>
> +#include <linux/spinlock.h>
> +#include <linux/wait.h>
> +#include <linux/stddef.h>
> +#include <linux/types.h>
> +
> +#define GOODIX_EC_DRIVER_NAME		"goodix-ec-mailbox"
> +
> +/* Shared-memory mailbox layout. */
> +#define GOODIX_EC_MMIO_SIZE		0x1000
> +#define GOODIX_EC_TX_OFFSET		0x000
> +#define GOODIX_EC_TX_SIZE		0x0200
> +#define GOODIX_EC_RX_OFFSET		0x0200
> +#define GOODIX_EC_RX_SIZE		0x0e00
> +#define GOODIX_EC_PACKET_ALIGNMENT	8
> +
> +/* EC mailbox framing. */
> +#define GOODIX_EC_PACKET_TYPE		0xf0
> +#define GOODIX_EC_SEQUENCE_SEED		0x8881

You start filling ->sequence from this +1 ?
 
IMO, it would would be better to change from the pre-increment to
post-increment.

> +
> +/* MP framing. The payload is opaque to the kernel. */
> +#define GOODIX_MP_HEADER_SIZE		4

Is this same as sizeof(struct goodix_mp_header), if yes, why a separate 
define necessary?

> +
> +/* Mailbox handshake timing. */
> +#define GOODIX_WRITE_DONE_PRE_US	50
> +#define GOODIX_WRITE_DONE_HIGH_US	4000
> +#define GOODIX_WRITE_DONE_POST_US	200
> +#define GOODIX_READ_DONE_HIGH_US	50
> +#define GOODIX_READ_DONE_POST_US	200
> +#define GOODIX_SYNC_RX_DELAY_US		1000
> +
> +struct device;
> +
> +struct goodix_ec_header {
> +	u8 type;
> +	__le16 payload_len;
> +	u8 checksum;
> +	__le16 sequence;
> +	u8 reserved[2];
> +} __packed;

Missing include for __packed.

> +
> +struct goodix_mp_header {
> +	u8 type;
> +	__le16 payload_len;
> +	u8 checksum;
> +} __packed;
> +
> +struct goodix_device;
> +
> +struct goodix_model_data {
> +	const char *name;
> +};
> +
> +struct goodix_device {
> +	struct device *dev;
> +	struct kref refcount;
> +	bool disconnected;
> +	bool suspended;
> +	bool irq_enabled;
> +
> +	void __iomem *mailbox;
> +	resource_size_t mailbox_phys;
> +	resource_size_t mailbox_size;
> +
> +	struct gpio_desc *write_done_gpio;
> +	struct gpio_desc *read_done_gpio;
> +	struct gpio_desc *irq_gpio;
> +	int irq;
> +
> +	u8 *tx_buf;
> +	u8 *rx_buf;
> +
> +	u8 *rx_reassembly;
> +	size_t rx_reassembly_len;
> +	size_t rx_reassembly_received;
> +	u8 rx_reassembly_mp_type;
> +	bool rx_reassembly_active;
> +
> +	u16 tx_sequence;
> +	/* Serializes mailbox TX with threaded-IRQ RX access. */
> +	struct mutex transfer_lock;
> +
> +	struct miscdevice miscdev;
> +	struct kfifo rx_fifo;
> +	/* Protects the RX FIFO and reader ownership state. */
> +	spinlock_t rx_fifo_lock;
> +	/* Serializes record-oriented read operations. */
> +	struct mutex rx_read_lock;
> +	wait_queue_head_t rx_wait;
> +	bool rx_fifo_ready;
> +	bool rx_reader_open;
> +	bool misc_registered;
> +
> +	const struct goodix_model_data *model;
> +};
> +
> +int goodix_ec_sync_send(struct goodix_device *gdev,
> +			const u8 *tx, size_t tx_len);
> +bool goodix_ec_device_get(struct goodix_device *gdev);
> +void goodix_ec_device_put(struct goodix_device *gdev);
> +int goodix_ec_uapi_register(struct goodix_device *gdev);
> +void goodix_ec_uapi_unregister(struct goodix_device *gdev);
> +
> +#endif /* _GOODIX_EC_H_ */
> diff --git a/drivers/platform/x86/goodix-ec-mailbox/goodix_ec_main.c b/drivers/platform/x86/goodix-ec-mailbox/goodix_ec_main.c
> new file mode 100644
> index 000000000000..67e73aa3c6df
> --- /dev/null
> +++ b/drivers/platform/x86/goodix-ec-mailbox/goodix_ec_main.c
> @@ -0,0 +1,715 @@
> +// SPDX-License-Identifier: GPL-2.0-only
> +/*
> + * Goodix fingerprint sensors behind an EC shared-memory mailbox.
> + *
> + * The tested GXFP5130 platform exposes the fingerprint transport through
> + * ACPI-described MMIO and GPIO handshakes. The host exchanges opaque MP
> + * payloads with the device through this mailbox.
> + *
> + * Sensor protocol policy deliberately stays in userspace. This driver does
> + * not know about Goodix commands, checksums, TLS, configuration, capture, or
> + * sensor power states.
> + */
> +
> +#include <linux/acpi.h>
> +#include <linux/align.h>
> +#include <linux/delay.h>
> +#include <linux/errno.h>
> +#include <linux/gpio/consumer.h>
> +#include <linux/io.h>
> +#include <linux/io-64-nonatomic-lo-hi.h>
> +#include <linux/interrupt.h>
> +#include <linux/irq.h>
> +#include <linux/capability.h>
> +#include <linux/fs.h>
> +#include <linux/kernel.h>
> +#include <linux/kfifo.h>
> +#include <linux/miscdevice.h>
> +#include <linux/poll.h>
> +#include <linux/module.h>
> +#include <linux/platform_device.h>
> +#include <linux/pm.h>
> +#include <linux/slab.h>
> +#include <linux/string.h>
> +#include <linux/timekeeping.h>
> +#include <linux/uaccess.h>
> +#include <linux/unaligned.h>
> +
> +#include "goodix_ec_mailbox.h"
> +#include <linux/goodix_ec.h>

Please group linux/ ones in one single group.

> +#define GXFP5130_ACPI_HID "GXFP5130"
> +
> +static void goodix_ec_device_release(struct kref *refcount)
> +{
> +	struct goodix_device *gdev =
> +		container_of(refcount, struct goodix_device, refcount);

Add include.

> +
> +	kfree(gdev);
> +}
> +
> +bool goodix_ec_device_get(struct goodix_device *gdev)
> +{
> +	return kref_get_unless_zero(&gdev->refcount);
> +}
> +
> +void goodix_ec_device_put(struct goodix_device *gdev)
> +{
> +	kref_put(&gdev->refcount, goodix_ec_device_release);
> +}
> +
> +static void goodix_ec_device_put_action(void *data)
> +{
> +	goodix_ec_device_put(data);
> +}
> +
> +/* EC mailbox transport. */
> +
> +struct goodix_ec_acpi_gpio_state {
> +	unsigned int gpio_count;
> +	int irq_index;
> +	int done_index[2];
> +	u8 done_polarity[2];
> +	unsigned int done_count;
> +};
> +
> +static int goodix_ec_acpi_gpio_resource(struct acpi_resource *resource,
> +					void *context)
> +{
> +	struct goodix_ec_acpi_gpio_state *state = context;
> +	struct acpi_resource_gpio *gpio;
> +	unsigned int index;
> +
> +	if (resource->type != ACPI_RESOURCE_TYPE_GPIO)
> +		return 0;
> +
> +	gpio = &resource->data.gpio;
> +	index = state->gpio_count++;
> +	if (!gpio->pin_table_length)
> +		return 0;
> +
> +	if (gpio->connection_type == ACPI_RESOURCE_GPIO_TYPE_INT) {
> +		if (state->irq_index < 0)
> +			state->irq_index = index;
> +	} else if (gpio->connection_type == ACPI_RESOURCE_GPIO_TYPE_IO &&
> +		   state->done_count < ARRAY_SIZE(state->done_index)) {

Add include.

> +		state->done_index[state->done_count] = index;
> +		state->done_polarity[state->done_count] = gpio->polarity;
> +		state->done_count++;
> +	}
> +
> +	return 0;
> +}
> +
> +static int goodix_ec_add_gpio_mappings(struct device *dev)
> +{
> +	struct goodix_ec_acpi_gpio_state state = {
> +		.irq_index = -1,
> +		.done_index = { -1, -1 },
> +	};
> +	struct acpi_gpio_mapping *mappings;
> +	struct acpi_gpio_params *params;
> +	struct acpi_device *adev = ACPI_COMPANION(dev);
> +	LIST_HEAD(resources);

Include?

> +	bool active_low;
> +	int ret;
> +
> +	if (!adev)
> +		return -ENODEV;

Please move adev assignment right before this line as this is its error
handling.


> +	ret = acpi_dev_get_resources(adev, &resources,
> +				     goodix_ec_acpi_gpio_resource, &state);
> +	acpi_dev_free_resource_list(&resources);
> +	if (ret <= 0)
> +		return ret < 0 ? ret : -ENOENT;

Split the if to two for clarity.

> +	if (state.irq_index < 0 || state.done_count != 2)
> +		return dev_err_probe(dev, -ENOENT,
> +				     "ACPI _CRS does not describe one IRQ and two done GPIOs\n");
> +	if (state.done_polarity[0] != state.done_polarity[1])
> +		return dev_err_probe(dev, -EINVAL,
> +				     "ACPI done GPIO polarities differ\n");
> +
> +	active_low = state.done_polarity[0] != 0;
> +	params = devm_kcalloc(dev, 3, sizeof(*params), GFP_KERNEL);
> +	mappings = devm_kcalloc(dev, 4, sizeof(*mappings), GFP_KERNEL);
> +	if (!params || !mappings)
> +		return -ENOMEM;
> +
> +	params[0].crs_entry_index = state.irq_index;
> +	params[0].line_index = 0;
> +	params[0].active_low = false;
> +	mappings[0].name = "irq-gpios";
> +	mappings[0].data = &params[0];
> +	mappings[0].size = 1;
> +
> +	params[1].crs_entry_index = state.done_index[0];
> +	params[1].line_index = 0;
> +	params[1].active_low = active_low;
> +	mappings[1].name = "write-done-gpios";
> +	mappings[1].data = &params[1];
> +	mappings[1].size = 1;
> +
> +	params[2].crs_entry_index = state.done_index[1];
> +	params[2].line_index = 0;
> +	params[2].active_low = active_low;
> +	mappings[2].name = "read-done-gpios";
> +	mappings[2].data = &params[2];
> +	mappings[2].size = 1;
> +
> +	ret = devm_acpi_dev_add_driver_gpios(dev, mappings);
> +	if (ret)
> +		return dev_err_probe(dev, ret,
> +				     "failed to install ACPI GPIO mappings\n");
> +
> +	dev_dbg(dev,

Please add include.

> +		"ACPI GPIO mappings: irq=%d write-done=%d read-done=%d active-low=%u\n",
> +		state.irq_index, state.done_index[0], state.done_index[1],
> +		active_low);
> +	return 0;
> +}
> +
> +static int goodix_ec_get_gpio(struct goodix_device *gdev,
> +			      struct gpio_desc **gpio,
> +			      const char *name,
> +			      enum gpiod_flags flags)
> +{
> +	*gpio = devm_gpiod_get(gdev->dev, name, flags);
> +	if (IS_ERR(*gpio))

Add linux/err.h

> +		return dev_err_probe(gdev->dev, PTR_ERR(*gpio),
> +				     "failed to acquire %s GPIO\n", name);

Please add braces to multiline blocks.

> +
> +	if (!*gpio)
> +		return dev_err_probe(gdev->dev, -ENODEV,
> +				     "%s GPIO missing\n", name);
> +
> +	return 0;
> +}
> +
> +static void goodix_ec_mmio_write_tx(struct goodix_device *gdev, size_t len)
> +{
> +	size_t offset;
> +
> +	for (offset = 0; offset < len; offset += sizeof(u64)) {
> +		u64 value = get_unaligned_le64(gdev->tx_buf + offset);
> +
> +		writeq(value, gdev->mailbox + GOODIX_EC_TX_OFFSET + offset);
> +	}
> +
> +	/* Publish every qword before asserting the write-done doorbell. */
> +	wmb();
> +}
> +
> +static void goodix_ec_mmio_read_rx(struct goodix_device *gdev)
> +{
> +	size_t offset;
> +
> +	for (offset = 0; offset < GOODIX_EC_RX_SIZE; offset += sizeof(u64)) {
> +		u64 value = readq(gdev->mailbox + GOODIX_EC_RX_OFFSET + offset);
> +
> +		put_unaligned_le64(value, gdev->rx_buf + offset);
> +	}
> +
> +	/* Complete the mailbox snapshot before parsing its headers. */
> +	rmb();
> +}
> +
> +static int goodix_ec_pulse_write_done(struct goodix_device *gdev)
> +{
> +	if (!gdev->write_done_gpio)
> +		return -ENODEV;
> +
> +	gpiod_set_value_cansleep(gdev->write_done_gpio, 0);
> +	fsleep(GOODIX_WRITE_DONE_PRE_US);
> +
> +	gpiod_set_value_cansleep(gdev->write_done_gpio, 1);
> +	fsleep(GOODIX_WRITE_DONE_HIGH_US);
> +
> +	gpiod_set_value_cansleep(gdev->write_done_gpio, 0);
> +	fsleep(GOODIX_WRITE_DONE_POST_US);
> +
> +	return 0;
> +}
> +
> +static int goodix_ec_pulse_read_done(struct goodix_device *gdev)
> +{
> +	if (!gdev->read_done_gpio)
> +		return -ENODEV;
> +
> +	/* RX must be copied before acknowledging that the host consumed it. */
> +	gpiod_set_value_cansleep(gdev->read_done_gpio, 1);
> +	fsleep(GOODIX_READ_DONE_HIGH_US);
> +
> +	gpiod_set_value_cansleep(gdev->read_done_gpio, 0);
> +	fsleep(GOODIX_READ_DONE_POST_US);
> +
> +	return 0;
> +}
> +
> +static int goodix_ec_build_packet(struct goodix_device *gdev,
> +				  const u8 *payload, size_t payload_len,
> +				  size_t *packet_len)
> +{
> +	struct goodix_ec_header *header;
> +	size_t total_len;
> +
> +	if (!gdev || !payload || !payload_len || !packet_len)
> +		return -EINVAL;
> +
> +	if (payload_len > U16_MAX)

+ limits.h

> +		return -EOVERFLOW;
> +
> +	total_len = ALIGN(sizeof(*header) + payload_len,
> +			  GOODIX_EC_PACKET_ALIGNMENT);
> +	if (total_len > GOODIX_EC_TX_SIZE)
> +		return -EMSGSIZE;
> +
> +	memset(gdev->tx_buf, 0, total_len);
> +
> +	header = (struct goodix_ec_header *)gdev->tx_buf;
> +	header->type = GOODIX_EC_PACKET_TYPE;
> +	header->payload_len = cpu_to_le16(payload_len);
> +	header->checksum = header->type + (payload_len & 0xff) +
> +			   ((payload_len >> 8) & 0xff);
> +	header->sequence = cpu_to_le16(++gdev->tx_sequence);
> +
> +	memcpy(gdev->tx_buf + sizeof(*header), payload, payload_len);
> +	*packet_len = total_len;
> +
> +	return 0;
> +}
> +
> +int goodix_ec_sync_send(struct goodix_device *gdev,
> +			const u8 *tx, size_t tx_len)
> +{
> +	size_t packet_len;
> +	int ret;
> +
> +	ret = goodix_ec_build_packet(gdev, tx, tx_len, &packet_len);
> +	if (ret)
> +		return ret;
> +
> +	goodix_ec_mmio_write_tx(gdev, packet_len);
> +
> +	ret = goodix_ec_pulse_write_done(gdev);
> +	if (ret)
> +		return ret;
> +
> +	fsleep(GOODIX_SYNC_RX_DELAY_US);
> +	return 0;
> +}
> +
> +/* MP packet transport. */
> +static void goodix_ec_rx_push(struct goodix_device *gdev, u8 mp_type,
> +			      const u8 *payload, size_t payload_len)
> +{
> +	struct goodix_ec_record_header header;
> +	unsigned long flags;
> +	size_t required;
> +
> +	if (!gdev || !payload || !payload_len ||
> +	    payload_len > GOODIX_EC_UAPI_RX_MAX)
> +		return;
> +
> +	memset(&header, 0, sizeof(header));
> +	header.len = payload_len;
> +	header.mp_type = mp_type >> 4;

Is this some field that starts at that bit index? In that case, naming it 
wit GENMASK() + FIELD_PREP() would be better (likely at the caller already
as it too seems to do right shifting).

You may have to rework some of the code to do FIELD_PREP/GET() only when 
encoding/decoding the field.

> +	header.timestamp_ns = ktime_get_ns();
> +	required = sizeof(header) + payload_len;
> +
> +	spin_lock_irqsave(&gdev->rx_fifo_lock, flags);
> +	if (!gdev->rx_fifo_ready || kfifo_avail(&gdev->rx_fifo) < required) {
> +		spin_unlock_irqrestore(&gdev->rx_fifo_lock, flags);
> +		dev_warn_ratelimited(gdev->dev,
> +				     "RX queue full; dropping %zu-byte packet\n",
> +				     payload_len);
> +		return;
> +	}
> +
> +	kfifo_in(&gdev->rx_fifo, &header, sizeof(header));
> +	kfifo_in(&gdev->rx_fifo, payload, payload_len);
> +	spin_unlock_irqrestore(&gdev->rx_fifo_lock, flags);
> +
> +	wake_up_interruptible(&gdev->rx_wait);
> +}
> +
> +static irqreturn_t goodix_ec_irq_thread(int irq, void *data)
> +{
> +	struct goodix_device *gdev = data;
> +	size_t chunk_len;
> +	size_t remaining;
> +	u16 declared_len;
> +	u8 mp_type;
> +	int ret = 0;
> +	int ack_ret;
> +
> +	mutex_lock(&gdev->transfer_lock);

Please use guard()

> +
> +	goodix_ec_mmio_read_rx(gdev);
> +
> +	/*
> +	 * Continuation IRQs contain raw payload bytes without another
> +	 * MP header.
> +	 */
> +	if (gdev->rx_reassembly_active) {
> +		remaining = gdev->rx_reassembly_len -
> +			    gdev->rx_reassembly_received;
> +
> +		chunk_len = min_t(size_t,
> +				  remaining,
> +				  GOODIX_EC_RX_SIZE);

One line.

As both are unsigned(?), min() would probably work too.

> +
> +		memcpy(gdev->rx_reassembly +
> +		       gdev->rx_reassembly_received,
> +		       gdev->rx_buf,
> +		       chunk_len);

Fits easily to 2 lines.

> +
> +		gdev->rx_reassembly_received += chunk_len;
> +
> +		ack_ret = goodix_ec_pulse_read_done(gdev);
> +		if (ack_ret)
> +			ret = ack_ret;
> +
> +		if (!ret &&
> +		    gdev->rx_reassembly_received ==
> +		    gdev->rx_reassembly_len) {

Fits to two lines.

Consider shortening "reassembly" a bit and "received" -> "rcvd".

> +			dev_dbg(gdev->dev,
> +				"RX reassembly complete: MP=0x%02x payload=%zu bytes\n",
> +				 gdev->rx_reassembly_mp_type,
> +				 gdev->rx_reassembly_len);
> +
> +			goodix_ec_rx_push(gdev,
> +					  gdev->rx_reassembly_mp_type,
> +					  gdev->rx_reassembly,
> +					  gdev->rx_reassembly_len);
> +
> +			kfree(gdev->rx_reassembly);
> +			gdev->rx_reassembly = NULL;
> +			gdev->rx_reassembly_len = 0;
> +			gdev->rx_reassembly_received = 0;
> +			gdev->rx_reassembly_mp_type = 0;
> +			gdev->rx_reassembly_active = false;
> +		}
> +
> +		mutex_unlock(&gdev->transfer_lock);
> +
> +		if (ret)
> +			dev_warn_ratelimited(gdev->dev,
> +					     "IRQ %d continuation ACK failed: %d\n",
> +					     irq, ret);



> +
> +		return IRQ_HANDLED;
> +	}
> +
> +	mp_type = gdev->rx_buf[0];
> +
> +	if ((mp_type >> 4) != 0x0a &&
> +	    (mp_type >> 4) != 0x0b &&
> +	    (mp_type >> 4) != 0x0c) {

As mentioned above, probably you should be naming a field with GENMASK() 
and using FIELD_GET() to extract it.

Also, name those literals with defines.

> +		ret = -EBADMSG;
> +		goto acknowledge;
> +	}
> +
> +	if (gdev->rx_buf[3] !=
> +	    gdev->rx_buf[0] +
> +	    gdev->rx_buf[1] +
> +	    gdev->rx_buf[2]) {

Is the right-hand side some kind of checksum calculation? I suggest you 
do that first into a temporary variable which, with good naming, explains 
the intent of this code much better than random additions together. Or add 
a helper.

> +		ret = -EBADMSG;
> +		goto acknowledge;
> +	}
> +
> +	declared_len = get_unaligned_le16(gdev->rx_buf + 1);
> +
> +	/* The complete payload fits in one mailbox transaction. */
> +	if (declared_len <=
> +	    GOODIX_EC_RX_SIZE -
> +	    sizeof(struct goodix_mp_header)) {

Don't break the line at <=.

> +		goodix_ec_rx_push(gdev,
> +				  mp_type,
> +				  gdev->rx_buf +
> +				  sizeof(struct goodix_mp_header),
> +				  declared_len);
> +
> +		dev_dbg(gdev->dev,
> +			"IRQ %d RX queued: MP=0x%02x payload=%u bytes\n",
> +			irq,
> +			mp_type >> 4,
> +			declared_len);
> +
> +		goto acknowledge;
> +	}
> +
> +	/* Reassemble payloads delivered across multiple IRQs. */
> +	gdev->rx_reassembly = kmalloc(declared_len, GFP_KERNEL);
> +	if (!gdev->rx_reassembly) {
> +		ret = -ENOMEM;
> +		goto acknowledge;
> +	}
> +
> +	chunk_len = GOODIX_EC_RX_SIZE -
> +		    sizeof(struct goodix_mp_header);

Unnecessary linesplit, you seem to have many of these...
 
Please go through your entire file with this problem in mind, I won't 
be marking them from this point on.

> +	memcpy(gdev->rx_reassembly,
> +	       gdev->rx_buf +
> +	       sizeof(struct goodix_mp_header),
> +	       chunk_len);
> +
> +	gdev->rx_reassembly_len = declared_len;
> +	gdev->rx_reassembly_received = chunk_len;
> +	gdev->rx_reassembly_mp_type = mp_type;
> +	gdev->rx_reassembly_active = true;
> +
> +	dev_dbg(gdev->dev,
> +		"RX reassembly started: MP=0x%02x total=%u first=%zu remaining=%zu\n",
> +		 mp_type >> 4,
> +		 declared_len,
> +		 chunk_len,
> +		 (size_t)declared_len - chunk_len);
> +
> +acknowledge:
> +	ack_ret = goodix_ec_pulse_read_done(gdev);
> +	if (!ret && ack_ret)
> +		ret = ack_ret;
> +
> +	mutex_unlock(&gdev->transfer_lock);
> +
> +	if (ret)
> +		dev_warn_ratelimited(gdev->dev,
> +				     "IRQ %d RX handling failed: %d\n",
> +				     irq, ret);

Please use braces for multiline blocks even if there's just one statement.

But I also don't like how you have "ret" named variable which truly isn't 
return value for the function but just printing it here. The entire play 
with ret and ack_ret is also ugly.

> +
> +	return IRQ_HANDLED;
> +}

This function is very hard to read and should be restructured if the line
combining won't make it more readable.
 
Preferrably, it should use guard/scoped_guard() too.

> +static const struct goodix_model_data gxfp5130_model = {
> +	.name = "GXFP5130",
> +};
> +
> +/* ACPI platform driver and model selection. */
> +
> +static int goodix_ec_probe(struct platform_device *pdev)
> +{
> +	struct device *dev = &pdev->dev;
> +	const struct acpi_device_id *match;
> +	struct goodix_device *gdev;
> +	struct resource *resource;
> +	resource_size_t mailbox_size;
> +	unsigned long irq_flags;
> +	unsigned int irq_type;
> +	int ret;
> +
> +	match = acpi_match_device(dev->driver->acpi_match_table, dev);
> +	if (!match || !match->driver_data)
> +		return -ENODEV;
> +
> +	gdev = kzalloc_obj(*gdev);
> +	if (!gdev)
> +		return -ENOMEM;
> +	kref_init(&gdev->refcount);
> +	ret = devm_add_action_or_reset(dev, goodix_ec_device_put_action, gdev);
> +	if (ret)
> +		return ret;
> +
> +	gdev->dev = dev;
> +	gdev->model = (const struct goodix_model_data *)match->driver_data;
> +	gdev->tx_sequence = GOODIX_EC_SEQUENCE_SEED;
> +	mutex_init(&gdev->transfer_lock);

Where's pairing mutex_destroy()?
 
If devm_mutex_init() could be used, use that instead as it handles it for 
you. You seem to use devm below but I'm not sure about that either.

> +	platform_set_drvdata(pdev, gdev);
> +
> +	gdev->mailbox = devm_platform_get_and_ioremap_resource(pdev, 0,
> +							       &resource);

gdev seems to be managed by kref, is it safe to use plain devm for some 
of its members?

> +	if (IS_ERR(gdev->mailbox))
> +		return dev_err_probe(dev, PTR_ERR(gdev->mailbox),
> +				     "failed to map EC mailbox MMIO resource\n");
> +
> +	mailbox_size = resource_size(resource);
> +	if (mailbox_size < GOODIX_EC_MMIO_SIZE)
> +		return dev_err_probe(dev, -EINVAL,

-EINVAL is wrong code to return in this case. If the expected resource
isn't there, isn't that -ENODEV.

> +				     "EC mailbox resource too small: %#llx\n",
> +				     (unsigned long long)mailbox_size);
> +
> +	gdev->mailbox_phys = resource->start;
> +	gdev->mailbox_size = mailbox_size;

Why are you storing these? They're only used for the dev_info() print=20
below AFAICT. More about that print below...

> +	gdev->tx_buf = devm_kzalloc(dev, GOODIX_EC_TX_SIZE, GFP_KERNEL);
> +	if (!gdev->tx_buf)
> +		return -ENOMEM;
> +
> +	gdev->rx_buf = devm_kzalloc(dev, GOODIX_EC_RX_SIZE, GFP_KERNEL);
> +	if (!gdev->rx_buf)
> +		return -ENOMEM;

More devm allocs to kref managed gdev, are they safe?
 
Or could gdev be directly managed with devm?

> +	ret = goodix_ec_add_gpio_mappings(dev);
> +	if (ret)
> +		return ret;
> +
> +	ret = goodix_ec_get_gpio(gdev, &gdev->write_done_gpio,
> +				 "write-done", GPIOD_OUT_LOW);
> +	if (ret)
> +		return ret;
> +
> +	ret = goodix_ec_get_gpio(gdev, &gdev->read_done_gpio,
> +				 "read-done", GPIOD_OUT_LOW);
> +	if (ret)
> +		return ret;
> +
> +	ret = goodix_ec_get_gpio(gdev, &gdev->irq_gpio,
> +				 "irq", GPIOD_IN);
> +	if (ret)
> +		return ret;
> +
> +	gdev->irq = gpiod_to_irq(gdev->irq_gpio);
> +	if (gdev->irq < 0)
> +		return dev_err_probe(dev, gdev->irq,
> +				     "failed to map IRQ GPIO to an IRQ\n");
> +
> +	irq_type = irq_get_trigger_type(gdev->irq);
> +	if (irq_type == IRQ_TYPE_NONE) {
> +		ret = irq_set_irq_type(gdev->irq, IRQ_TYPE_LEVEL_HIGH);
> +		if (ret)
> +			return dev_err_probe(dev, ret,
> +					     "failed to set level-high IRQ trigger\n");
> +		irq_type = IRQ_TYPE_LEVEL_HIGH;
> +	}
> +
> +	dev_info(dev, "bound %s: mailbox=%pa size=%#llx irq=%d\n",
> +		 gdev->model->name, &gdev->mailbox_phys,
> +		 (unsigned long long)gdev->mailbox_size, gdev->irq);

Probe's success path should be quiet.
 
Also, this is way too technical to be presented to a normal user on 
info level. What is user supposed to benefit from this information?
 
Either change to dev_dbg() or drop.
 
Also, you don't need the intermediate mailbox variables (not in struct 
nor in local variables but can get them from resource directly).


> +	irq_flags = IRQF_ONESHOT | IRQF_NO_AUTOEN;
> +	ret = devm_request_threaded_irq(dev,
> +					gdev->irq,
> +					NULL,
> +					goodix_ec_irq_thread,
> +					irq_flags,
> +					dev_name(dev),
> +					gdev);

Fits to less lines.

> +	if (ret)
> +		return dev_err_probe(dev, ret,
> +				     "failed to request IRQ %d\n",
> +				     gdev->irq);
> +
> +	dev_dbg(dev, "IRQ %d registered: flags=%#lx trigger=%#x\n",
> +		gdev->irq, irq_flags, irq_type);
> +
> +	ret = goodix_ec_uapi_register(gdev);
> +	if (ret)
> +		return dev_err_probe(dev, ret,
> +				     "failed to register userspace interface\n");
> +
> +	enable_irq(gdev->irq);
> +	gdev->irq_enabled = true;

Does this attempt some kind of synchronization without barriers?

> +	dev_info(dev, "IRQ %d armed\n", gdev->irq);
> +
> +	dev_info(dev, "EC mailbox transport ready\n");

Success path should be quiet.

> +	return 0;
> +}
> +
> +static void goodix_ec_remove(struct platform_device *pdev)
> +{
> +	struct goodix_device *gdev = platform_get_drvdata(pdev);
> +
> +	if (!gdev)
> +		return;
> +
> +	WRITE_ONCE(gdev->disconnected, true);
> +	if (gdev->irq_enabled) {
> +		disable_irq(gdev->irq);
> +		synchronize_irq(gdev->irq);
> +		gdev->irq_enabled = false;

There's something odd going on here, why you need to do this?

> +	}
> +
> +	goodix_ec_uapi_unregister(gdev);

Add a blank line to separate the critical section clearly.

> +	mutex_lock(&gdev->transfer_lock);
> +	kfree(gdev->rx_reassembly);
> +	gdev->rx_reassembly = NULL;
> +	gdev->rx_reassembly_len = 0;
> +	gdev->rx_reassembly_received = 0;
> +	gdev->rx_reassembly_active = false;

What can race with these if you need them under a lock?

> +	if (gdev->write_done_gpio)
> +		gpiod_set_value_cansleep(gdev->write_done_gpio, 0);
> +	if (gdev->read_done_gpio)
> +		gpiod_set_value_cansleep(gdev->read_done_gpio, 0);
> +	mutex_unlock(&gdev->transfer_lock);
> +
> +	dev_info(gdev->dev, "%s detached\n", gdev->model->name);

Please remove or change to _dbg() level.

> +}
> +
> +static int goodix_ec_suspend(struct device *dev)
> +{
> +	struct goodix_device *gdev = dev_get_drvdata(dev);
> +	unsigned long flags;
> +
> +	if (!gdev || READ_ONCE(gdev->disconnected))
> +		return 0;
> +
> +	WRITE_ONCE(gdev->suspended, true);

Are you duplicating core's PM functionality? Why you need this?

> +	wake_up_interruptible_all(&gdev->rx_wait);
> +	if (gdev->irq_enabled) {

What is the scenario you need this for?

> +		disable_irq(gdev->irq);
> +		synchronize_irq(gdev->irq);

/**
 * disable_irq - disable an irq and wait for completion
 * @irq: Interrupt to disable
 *
 * Disable the selected interrupt line.  Enables and Disables are nested.
 *
 * This function waits for any pending IRQ handlers for this interrupt to
 * complete before returning.

> +		gdev->irq_enabled = false;
> +	}
> +
> +	mutex_lock(&gdev->transfer_lock);
> +	gpiod_set_value_cansleep(gdev->write_done_gpio, 0);
> +	gpiod_set_value_cansleep(gdev->read_done_gpio, 0);
> +	kfree(gdev->rx_reassembly);
> +	gdev->rx_reassembly = NULL;
> +	gdev->rx_reassembly_len = 0;
> +	gdev->rx_reassembly_received = 0;
> +	gdev->rx_reassembly_active = false;
> +	mutex_unlock(&gdev->transfer_lock);

You've disabled irq already so why you need all this clearing?? What's 
the concurrent activity you're protecting against with these??
 
In any case, it seems to be duplicated to multiple places so should
definitely be in a helper instead (if it's presence is justified at all).

> +	spin_lock_irqsave(&gdev->rx_fifo_lock, flags);
> +	if (gdev->rx_fifo_ready)
> +		kfifo_reset(&gdev->rx_fifo);
> +	spin_unlock_irqrestore(&gdev->rx_fifo_lock, flags);
> +	return 0;
> +}
> +
> +static int goodix_ec_resume(struct device *dev)
> +{
> +	struct goodix_device *gdev = dev_get_drvdata(dev);
> +
> +	if (!gdev || READ_ONCE(gdev->disconnected))

Probably missing header for READ_ONCE() though I'm far from convinced
about most of these booleans you've in place.
 
You've like mix kref, devm, and a large set of booleans. It's very
confusing to follow and to understand why each is required (and I
suspect most are either duplicating kernel core's functionality or
entirely unnecessary).

> +		return 0;
> +
> +	gpiod_set_value_cansleep(gdev->write_done_gpio, 0);
> +	gpiod_set_value_cansleep(gdev->read_done_gpio, 0);
> +	if (!gdev->irq_enabled) {

Can it ever be true here? How?

> +		enable_irq(gdev->irq);
> +		gdev->irq_enabled = true;
> +	}
> +	/* Permit new writes only after the receive path is armed. */
> +	WRITE_ONCE(gdev->suspended, false);

Is this doing some kind of adhoc PM counting?

> +	wake_up_interruptible_all(&gdev->rx_wait);
> +	return 0;
> +}
> +
> +static DEFINE_SIMPLE_DEV_PM_OPS(goodix_ec_pm_ops, goodix_ec_suspend,
> +				goodix_ec_resume);
> +
> +static const struct acpi_device_id goodix_ec_acpi_match[] = {
> +	{
> +		.id = GXFP5130_ACPI_HID,
> +		.driver_data = (kernel_ulong_t)&gxfp5130_model,
> +	},
> +	{ }
> +};
> +MODULE_DEVICE_TABLE(acpi, goodix_ec_acpi_match);
> +
> +static struct platform_driver goodix_ec_driver = {
> +	.probe = goodix_ec_probe,
> +	.remove = goodix_ec_remove,
> +	.driver = {
> +		.name = GOODIX_EC_DRIVER_NAME,
> +		.acpi_match_table = ACPI_PTR(goodix_ec_acpi_match),
> +		.pm = pm_sleep_ptr(&goodix_ec_pm_ops),
> +	},
> +};
> +module_platform_driver(goodix_ec_driver);
> +
> +MODULE_AUTHOR("Ertugrul Topcu <ertugtopcu0@gmail.com>");
> +MODULE_DESCRIPTION("Goodix fingerprint sensors over an EC mailbox");
> +MODULE_LICENSE("GPL");
> diff --git a/drivers/platform/x86/goodix-ec-mailbox/goodix_ec_uapi.c b/drivers/platform/x86/goodix-ec-mailbox/goodix_ec_uapi.c
> new file mode 100644
> index 000000000000..d2f075acb481
> --- /dev/null
> +++ b/drivers/platform/x86/goodix-ec-mailbox/goodix_ec_uapi.c
> @@ -0,0 +1,360 @@
> +// SPDX-License-Identifier: GPL-2.0-only
> +#include <linux/capability.h>
> +#include <linux/errno.h>
> +#include <linux/fs.h>
> +#include <linux/kernel.h>
> +#include <linux/kfifo.h>
> +#include <linux/miscdevice.h>
> +#include <linux/module.h>
> +#include <linux/poll.h>
> +#include <linux/slab.h>
> +#include <linux/uaccess.h>
> +#include <linux/unaligned.h>
> +
> +#include "goodix_ec_mailbox.h"
> +#include <linux/goodix_ec.h>
> +
> +#define GOODIX_EC_RX_FIFO_BYTES (256u * 1024u)

SZ_xx + don't forget include.

> +
> +static bool goodix_ec_rx_ready(struct goodix_device *gdev)
> +{
> +	unsigned long flags;
> +	bool ready;
> +
> +	spin_lock_irqsave(&gdev->rx_fifo_lock, flags);
> +	ready = gdev->rx_fifo_ready && !kfifo_is_empty(&gdev->rx_fifo);
> +	spin_unlock_irqrestore(&gdev->rx_fifo_lock, flags);
> +
> +	return ready;
> +}
> +
> +static int goodix_ec_uapi_open(struct inode *inode, struct file *file)
> +{
> +	struct miscdevice *misc = file->private_data;
> +	struct goodix_device *gdev =
> +		container_of(misc, struct goodix_device, miscdev);
> +	unsigned long flags;
> +
> +	if (!capable(CAP_SYS_RAWIO))
> +		return -EPERM;
> +	if (!goodix_ec_device_get(gdev))
> +		return -ENODEV;
> +	if (READ_ONCE(gdev->disconnected)) {
> +		goodix_ec_device_put(gdev);
> +		return -ENODEV;
> +	}
> +
> +	if (file->f_mode & FMODE_READ) {
> +		spin_lock_irqsave(&gdev->rx_fifo_lock, flags);
> +		if (!gdev->rx_fifo_ready) {
> +			spin_unlock_irqrestore(&gdev->rx_fifo_lock, flags);
> +			goodix_ec_device_put(gdev);
> +			return -ENODEV;
> +		}
> +
> +		if (gdev->rx_reader_open) {
> +			spin_unlock_irqrestore(&gdev->rx_fifo_lock, flags);
> +			goodix_ec_device_put(gdev);
> +			return -EBUSY;
> +		}
> +
> +		gdev->rx_reader_open = true;
> +		spin_unlock_irqrestore(&gdev->rx_fifo_lock, flags);
> +	}
> +
> +	file->private_data = gdev;
> +	return 0;
> +}
> +
> +static int goodix_ec_uapi_release(struct inode *inode, struct file *file)
> +{
> +	struct goodix_device *gdev = file->private_data;
> +	unsigned long flags;
> +
> +	if (gdev && (file->f_mode & FMODE_READ)) {
> +		spin_lock_irqsave(&gdev->rx_fifo_lock, flags);
> +		gdev->rx_reader_open = false;
> +		spin_unlock_irqrestore(&gdev->rx_fifo_lock, flags);
> +	}
> +	if (gdev)
> +		goodix_ec_device_put(gdev);
> +
> +	return 0;
> +}
> +
> +static ssize_t goodix_ec_uapi_write(struct file *file,
> +				    const char __user *user_buffer,
> +				    size_t count, loff_t *position)
> +{
> +	struct goodix_device *gdev = file->private_data;
> +	struct goodix_ec_tx_header header;
> +	u8 *mp_packet;
> +	size_t required;
> +	int ret;
> +
> +	if (!gdev || !user_buffer || READ_ONCE(gdev->disconnected))
> +		return -ENODEV;
> +
> +	if (count < sizeof(header))
> +		return -EINVAL;
> +
> +	if (copy_from_user(&header, user_buffer, sizeof(header)))
> +		return -EFAULT;
> +
> +	if (header.payload_len > GOODIX_EC_UAPI_TX_MAX)
> +		return -EMSGSIZE;
> +	if (header.reserved || header.flags)
> +		return -EINVAL;
> +
> +	required = sizeof(header) + header.payload_len;
> +	if (count != required)
> +		return -EINVAL;
> +
> +	mp_packet = kmalloc(GOODIX_MP_HEADER_SIZE +
> +			    header.payload_len, GFP_KERNEL);

Why you can't use sizeof(header) + ...?

> +	if (!mp_packet)
> +		return -ENOMEM;
> +
> +	mp_packet[0] = header.mp_flags;
> +	put_unaligned_le16(header.payload_len, mp_packet + 1);
> +	mp_packet[3] = mp_packet[0] + mp_packet[1] + mp_packet[2];

An unannotated checksum calculation? Add a helper to name the 
functionality?

> +	if (header.payload_len &&
> +	    copy_from_user(mp_packet + sizeof(struct goodix_mp_header),
> +			   user_buffer + sizeof(header),
> +			   header.payload_len)) {
> +		kfree(mp_packet);

Please use __free() and put the variable declaration on the line where
you do the alloc (as per instruction in the long comment in cleanup.h).

> +		return -EFAULT;
> +	}
> +
> +	mutex_lock(&gdev->transfer_lock);
> +	if (READ_ONCE(gdev->disconnected))
> +		ret = -ENODEV;
> +	else if (READ_ONCE(gdev->suspended))
> +		ret = -EHOSTDOWN;
> +	else
> +		ret = goodix_ec_sync_send(gdev, mp_packet,
> +					  sizeof(struct goodix_mp_header) +
> +					  header.payload_len);
> +	mutex_unlock(&gdev->transfer_lock);

Please use scoped_guard() and return immediately.

> +
> +	kfree(mp_packet);
> +
> +	if (ret)
> +		return ret;
> +
> +	return count;
> +}
> +
> +static ssize_t goodix_ec_uapi_read(struct file *file, char __user *user_buffer,
> +				   size_t count, loff_t *position)
> +{
> +	struct goodix_device *gdev = file->private_data;
> +	struct goodix_ec_record_header header;
> +	unsigned long flags;
> +	u8 *record;
> +	size_t required;
> +	int ret;
> +
> +	if (!gdev || !user_buffer || READ_ONCE(gdev->disconnected))
> +		return -ENODEV;
> +
> +	ret = mutex_lock_interruptible(&gdev->rx_read_lock);

Please try ACQUIRE/ACQUIRE_ERR() to avoid need to manually do the 
unlock so you can convert those gotos below to direct returns.

> +	if (ret)
> +		return ret;
> +
> +	for (;;) {
> +		spin_lock_irqsave(&gdev->rx_fifo_lock, flags);
> +
> +		if (!gdev->rx_fifo_ready) {
> +			spin_unlock_irqrestore(&gdev->rx_fifo_lock, flags);
> +			ret = -ENODEV;
> +			goto out_unlock;
> +		}
> +		if (READ_ONCE(gdev->suspended)) {
> +			spin_unlock_irqrestore(&gdev->rx_fifo_lock, flags);
> +			ret = -EHOSTDOWN;
> +			goto out_unlock;
> +		}
> +
> +		if (kfifo_len(&gdev->rx_fifo) >= sizeof(header))
> +			break;
> +
> +		spin_unlock_irqrestore(&gdev->rx_fifo_lock, flags);

And what exactly are you trying to protect here? Can't those check 
results be invalid right after you release the lock?

> +
> +		if (file->f_flags & O_NONBLOCK) {
> +			ret = -EAGAIN;
> +			goto out_unlock;
> +		}
> +
> +		ret = wait_event_interruptible(gdev->rx_wait,
> +					       goodix_ec_rx_ready(gdev) ||
> +					       !READ_ONCE(gdev->rx_fifo_ready) ||
> +					       READ_ONCE(gdev->suspended));
> +		if (ret)
> +			goto out_unlock;
> +	}
> +
> +	if (kfifo_out_peek(&gdev->rx_fifo, &header,
> +			   sizeof(header)) != sizeof(header)) {
> +		spin_unlock_irqrestore(&gdev->rx_fifo_lock, flags);

I'm totally totally lost now.
 
Well, technically was lost. I had to go back and figure out you came from
break here but yeah, the code flow handling in your code is way too
complex to be followable for mere mortals such as me (who unfortunately
also happens to be the acting maintainer of the subsystem this code
falls under... :-( ).
 
I suggest you use cleanup.h in this function. It will force you to
architect sane code flow and lock scoping.

> +		ret = -EIO;
> +		goto out_unlock;
> +	}
> +
> +	required = sizeof(header) + header.len;
> +	if (kfifo_len(&gdev->rx_fifo) < required) {
> +		spin_unlock_irqrestore(&gdev->rx_fifo_lock, flags);
> +		ret = -EIO;
> +		goto out_unlock;
> +	}
> +
> +	if (count < required) {
> +		spin_unlock_irqrestore(&gdev->rx_fifo_lock, flags);
> +		ret = -EMSGSIZE;
> +		goto out_unlock;
> +	}
> +
> +	record = kmalloc(required, GFP_ATOMIC);

Please use __free() and move the variable declaration to this lines.

> +	if (!record) {
> +		spin_unlock_irqrestore(&gdev->rx_fifo_lock, flags);
> +		ret = -ENOMEM;
> +		goto out_unlock;
> +	}
> +
> +	if (kfifo_out(&gdev->rx_fifo, record, required) != required) {
> +		spin_unlock_irqrestore(&gdev->rx_fifo_lock, flags);
> +		kfree(record);
> +		ret = -EIO;
> +		goto out_unlock;
> +	}
> +
> +	spin_unlock_irqrestore(&gdev->rx_fifo_lock, flags);
> +
> +	if (copy_to_user(user_buffer, record, required)) {
> +		kfree(record);
> +		ret = -EFAULT;
> +		goto out_unlock;
> +	}
> +
> +	kfree(record);
> +	ret = required;
> +
> +out_unlock:
> +	mutex_unlock(&gdev->rx_read_lock);
> +	return ret;
> +}
> +
> +static __poll_t goodix_ec_uapi_poll(struct file *file, poll_table *wait)
> +{
> +	struct goodix_device *gdev = file->private_data;
> +	unsigned long flags;
> +	__poll_t mask = 0;
> +
> +	if (!gdev || READ_ONCE(gdev->disconnected))
> +		return EPOLLERR;
> +
> +	poll_wait(file, &gdev->rx_wait, wait);
> +
> +	spin_lock_irqsave(&gdev->rx_fifo_lock, flags);
> +	if (!gdev->rx_fifo_ready || READ_ONCE(gdev->disconnected))
> +		mask = EPOLLERR | EPOLLHUP;
> +	else if (READ_ONCE(gdev->suspended))
> +		mask = EPOLLERR;
> +	else if (!kfifo_is_empty(&gdev->rx_fifo))
> +		mask = EPOLLIN | EPOLLRDNORM;
> +	spin_unlock_irqrestore(&gdev->rx_fifo_lock, flags);
> +
> +	return mask;
> +}
> +
> +static long goodix_ec_uapi_ioctl(struct file *file,
> +				 unsigned int command,
> +				 unsigned long argument)
> +{
> +	struct goodix_device *gdev = file->private_data;
> +	unsigned long flags;
> +
> +	if (!gdev || READ_ONCE(gdev->disconnected))
> +		return -ENODEV;
> +
> +	switch (command) {
> +	case GOODIX_EC_IOCTL_FLUSH_RX:
> +		spin_lock_irqsave(&gdev->rx_fifo_lock, flags);
> +		if (gdev->rx_fifo_ready)
> +			kfifo_reset(&gdev->rx_fifo);
> +		spin_unlock_irqrestore(&gdev->rx_fifo_lock, flags);
> +		return 0;
> +	default:
> +		return -ENOTTY;
> +	}
> +}
> +
> +static const struct file_operations goodix_ec_uapi_fops = {
> +	.owner = THIS_MODULE,
> +	.open = goodix_ec_uapi_open,
> +	.release = goodix_ec_uapi_release,
> +	.read = goodix_ec_uapi_read,
> +	.write = goodix_ec_uapi_write,
> +	.poll = goodix_ec_uapi_poll,
> +	.unlocked_ioctl = goodix_ec_uapi_ioctl,
> +	.compat_ioctl = compat_ptr_ioctl,
> +	.llseek = noop_llseek,
> +};
> +
> +int goodix_ec_uapi_register(struct goodix_device *gdev)
> +{
> +	int ret;
> +
> +	spin_lock_init(&gdev->rx_fifo_lock);
> +	mutex_init(&gdev->rx_read_lock);

Pairing destroy()?

> +	init_waitqueue_head(&gdev->rx_wait);
> +
> +	ret = kfifo_alloc(&gdev->rx_fifo, GOODIX_EC_RX_FIFO_BYTES, GFP_KERNEL);
> +	if (ret)
> +		return ret;
> +
> +	gdev->rx_fifo_ready = true;
> +	gdev->rx_reader_open = false;
> +
> +	gdev->miscdev.minor = MISC_DYNAMIC_MINOR;
> +	gdev->miscdev.name = "gxfp";
> +	gdev->miscdev.fops = &goodix_ec_uapi_fops;
> +	gdev->miscdev.parent = gdev->dev;
> +
> +	ret = misc_register(&gdev->miscdev);
> +	if (ret) {
> +		gdev->rx_fifo_ready = false;
> +		kfifo_free(&gdev->rx_fifo);
> +		return ret;
> +	}
> +
> +	gdev->misc_registered = true;
> +	dev_info(gdev->dev, "userspace interface registered: /dev/gxfp\n");

Success path should be quiet.

> +	return 0;
> +}
> +
> +void goodix_ec_uapi_unregister(struct goodix_device *gdev)
> +{
> +	unsigned long flags;
> +
> +	if (!gdev)
> +		return;
> +
> +	if (gdev->misc_registered) {

Can this ever be false?

> +		misc_deregister(&gdev->miscdev);
> +		gdev->misc_registered = false;
> +	}
> +
> +	if (!gdev->rx_fifo_ready)
> +		return;
> +
> +	spin_lock_irqsave(&gdev->rx_fifo_lock, flags);
> +	gdev->rx_fifo_ready = false;
> +	gdev->rx_reader_open = false;
> +	kfifo_reset(&gdev->rx_fifo);
> +	spin_unlock_irqrestore(&gdev->rx_fifo_lock, flags);
> +
> +	wake_up_interruptible_all(&gdev->rx_wait);
> +	kfifo_free(&gdev->rx_fifo);
> +}
> diff --git a/include/uapi/linux/goodix_ec.h b/include/uapi/linux/goodix_ec.h
> new file mode 100644
> index 000000000000..ff8d0d505ae7
> --- /dev/null
> +++ b/include/uapi/linux/goodix_ec.h
> @@ -0,0 +1,37 @@
> +/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
> +#ifndef _UAPI_GOODIX_EC_H_
> +#define _UAPI_GOODIX_EC_H_
> +
> +#include <linux/ioctl.h>
> +#include <linux/types.h>
> +
> +#define GOODIX_EC_UAPI_MAGIC		'G'
> +#define GOODIX_EC_UAPI_TX_MAX		500u
> +#define GOODIX_EC_UAPI_RX_MAX		(128u * 1024u)
> +
> +/*
> + * read(2) returns one record:
> + *   struct goodix_ec_record_header
> + *   followed by len bytes of MP payload (normally one Goodix frame).
> + */
> +struct goodix_ec_record_header {
> +	__u32 len;
> +	__u32 mp_type;
> +	__u64 timestamp_ns;
> +};
> +
> +/*
> + * write(2) accepts:
> + *   struct goodix_ec_tx_header
> + *   followed by payload_len bytes used as the MP payload.
> + */
> +struct goodix_ec_tx_header {
> +	__u8 mp_flags;
> +	__u8 reserved;
> +	__u16 payload_len;
> +	__u32 flags;
> +};
> +
> +#define GOODIX_EC_IOCTL_FLUSH_RX	_IO(GOODIX_EC_UAPI_MAGIC, 0x11)
> +
> +#endif /* _UAPI_GOODIX_EC_H_ */

Well, this was quite messy read but I believe it will get much more 
readable once you eliminate the excessive linebreaks, replace the adhoc 
synchronization things with what is available from core, and convert to 
use cleanup.h.

-- 
 i.

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

end of thread, other threads:[~2026-09-17 13:59 UTC | newest]

Thread overview: 6+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-07-27 20:58 [PATCH] platform/x86: Add Goodix fingerprint EC mailbox transport Ertuğrul Topçu
2026-08-03  7:44 ` [PATCH v2] " Ertuğrul Topçu
2026-08-16 19:49   ` [PATCH v3] " Ertuğrul Topçu
2026-09-01 14:12     ` Ertuğrul Topçu
2026-09-17 13:59     ` Ilpo Järvinen
2026-08-16  1:26 ` [PATCH] " kernel test robot

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®