[PATCH v2] platform/x86: Add Goodix fingerprint EC mailbox transport
Ertuğrul Topçu <[email protected]> Mon, 3 Aug 2026 10:44:54 +0300
| Newsgroups | org.kernel.vger.platform-driver-x86,org.kernel.vger.linux-kernel |
|---|---|
| Message-ID | <[email protected]> |
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 <[email protected]> --- 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 <[email protected]> +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 <[email protected]> S: Maintained F: Documentation/devicetree/bindings/connector/gocontroll,moduline-module-slot.yaml +GOODIX EC MAILBOX FINGERPRINT TRANSPORT DRIVER +M: Ertugrul Topcu <[email protected]> +L: [email protected] +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 <[email protected]> L: [email protected] 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 = ¶ms[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 = ¶ms[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 = ¶ms[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 <[email protected]>"); +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