get:
Show a patch.

patch:
Update a patch.

put:
Update a patch.

GET /api/patches/2217451/?format=api
HTTP 200 OK
Allow: GET, PUT, PATCH, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept

{
    "id": 2217451,
    "url": "http://patchwork.ozlabs.org/api/patches/2217451/?format=api",
    "web_url": "http://patchwork.ozlabs.org/project/linux-gpio/patch/20260329090601.532477-3-o.rempel@pengutronix.de/",
    "project": {
        "id": 42,
        "url": "http://patchwork.ozlabs.org/api/projects/42/?format=api",
        "name": "Linux GPIO development",
        "link_name": "linux-gpio",
        "list_id": "linux-gpio.vger.kernel.org",
        "list_email": "linux-gpio@vger.kernel.org",
        "web_url": "",
        "scm_url": "",
        "webscm_url": "",
        "list_archive_url": "",
        "list_archive_url_format": "",
        "commit_url_format": ""
    },
    "msgid": "<20260329090601.532477-3-o.rempel@pengutronix.de>",
    "list_archive_url": null,
    "date": "2026-03-29T09:05:57",
    "name": "[v8,2/6] mfd: add NXP MC33978/MC34978 core driver",
    "commit_ref": null,
    "pull_url": null,
    "state": "new",
    "archived": false,
    "hash": "d61c337bf3dcbbad5bdd275773f88c5fe36dd635",
    "submitter": {
        "id": 71360,
        "url": "http://patchwork.ozlabs.org/api/people/71360/?format=api",
        "name": "Oleksij Rempel",
        "email": "o.rempel@pengutronix.de"
    },
    "delegate": null,
    "mbox": "http://patchwork.ozlabs.org/project/linux-gpio/patch/20260329090601.532477-3-o.rempel@pengutronix.de/mbox/",
    "series": [
        {
            "id": 497911,
            "url": "http://patchwork.ozlabs.org/api/series/497911/?format=api",
            "web_url": "http://patchwork.ozlabs.org/project/linux-gpio/list/?series=497911",
            "date": "2026-03-29T09:05:56",
            "name": "mfd: Add support for NXP MC33978/MC34978 MSDI",
            "version": 8,
            "mbox": "http://patchwork.ozlabs.org/series/497911/mbox/"
        }
    ],
    "comments": "http://patchwork.ozlabs.org/api/patches/2217451/comments/",
    "check": "pending",
    "checks": "http://patchwork.ozlabs.org/api/patches/2217451/checks/",
    "tags": {},
    "related": [],
    "headers": {
        "Return-Path": "\n <linux-gpio+bounces-34362-incoming=patchwork.ozlabs.org@vger.kernel.org>",
        "X-Original-To": [
            "incoming@patchwork.ozlabs.org",
            "linux-gpio@vger.kernel.org"
        ],
        "Delivered-To": "patchwork-incoming@legolas.ozlabs.org",
        "Authentication-Results": [
            "legolas.ozlabs.org;\n spf=pass (sender SPF authorized) smtp.mailfrom=vger.kernel.org\n (client-ip=172.105.105.114; helo=tor.lore.kernel.org;\n envelope-from=linux-gpio+bounces-34362-incoming=patchwork.ozlabs.org@vger.kernel.org;\n receiver=patchwork.ozlabs.org)",
            "smtp.subspace.kernel.org;\n arc=none smtp.client-ip=185.203.201.7",
            "smtp.subspace.kernel.org;\n dmarc=none (p=none dis=none) header.from=pengutronix.de",
            "smtp.subspace.kernel.org;\n spf=pass smtp.mailfrom=pengutronix.de"
        ],
        "Received": [
            "from tor.lore.kernel.org (tor.lore.kernel.org [172.105.105.114])\n\t(using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)\n\t key-exchange x25519)\n\t(No client certificate requested)\n\tby legolas.ozlabs.org (Postfix) with ESMTPS id 4fk7nD1bxSz1xtJ\n\tfor <incoming@patchwork.ozlabs.org>; Sun, 29 Mar 2026 20:07:28 +1100 (AEDT)",
            "from smtp.subspace.kernel.org (conduit.subspace.kernel.org\n [100.90.174.1])\n\tby tor.lore.kernel.org (Postfix) with ESMTP id A5ECB302F428\n\tfor <incoming@patchwork.ozlabs.org>; Sun, 29 Mar 2026 09:06:41 +0000 (UTC)",
            "from localhost.localdomain (localhost.localdomain [127.0.0.1])\n\tby smtp.subspace.kernel.org (Postfix) with ESMTP id 424CE2F28FC;\n\tSun, 29 Mar 2026 09:06:41 +0000 (UTC)",
            "from metis.whiteo.stw.pengutronix.de\n (metis.whiteo.stw.pengutronix.de [185.203.201.7])\n\t(using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits))\n\t(No client certificate requested)\n\tby smtp.subspace.kernel.org (Postfix) with ESMTPS id E5D432EBBA4\n\tfor <linux-gpio@vger.kernel.org>; Sun, 29 Mar 2026 09:06:37 +0000 (UTC)",
            "from drehscheibe.grey.stw.pengutronix.de ([2a0a:edc0:0:c01:1d::a2])\n\tby metis.whiteo.stw.pengutronix.de with esmtps\n (TLS1.3:ECDHE_RSA_AES_256_GCM_SHA384:256)\n\t(Exim 4.92)\n\t(envelope-from <ore@pengutronix.de>)\n\tid 1w6m5s-00018t-0v; Sun, 29 Mar 2026 11:06:04 +0200",
            "from dude04.red.stw.pengutronix.de ([2a0a:edc0:0:1101:1d::ac]\n helo=dude04)\n\tby drehscheibe.grey.stw.pengutronix.de with esmtps  (TLS1.3) tls\n TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384\n\t(Exim 4.96)\n\t(envelope-from <ore@pengutronix.de>)\n\tid 1w6m5q-002gZS-36;\n\tSun, 29 Mar 2026 11:06:02 +0200",
            "from ore by dude04 with local (Exim 4.98.2)\n\t(envelope-from <ore@pengutronix.de>)\n\tid 1w6m5q-00000002EXp-3jjn;\n\tSun, 29 Mar 2026 11:06:02 +0200"
        ],
        "ARC-Seal": "i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116;\n\tt=1774775201; cv=none;\n b=leQIxB6Qte/AXQcDr1RLtkVm4PVp3lX7bEI3MCJCdvkoCNSl2k+drV7TTYOLIKKF/P71EZZjm8mszf1yIF3yNgLrweVknoT59gbXy1QEXiXyDtEWBe827e7FATiBzXdzesNhG+YZ586F/Cjt6nu9L/B02GvjTfa3OLTTCs6n39g=",
        "ARC-Message-Signature": "i=1; a=rsa-sha256; d=subspace.kernel.org;\n\ts=arc-20240116; t=1774775201; c=relaxed/simple;\n\tbh=HHdVoPXDyMQFDT1uK3lQCK+Jq13vELaPji4udijVnpQ=;\n\th=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References:\n\t MIME-Version;\n b=ZiULQ2FEGYRZ0Ukc0kUwopMugWGApSDlkLghDWER3J3jre+N+DjsGXyiYS1buBZMmJHwO5+60WPXy/Bo9TskqNUmi8SvPZmYZSAG0yc25n2JKSySm1kbCaOWdODkJnm5qOWA+3vatRkEZyKooKEoSqh0blCruYzSf7yEGC+yLHg=",
        "ARC-Authentication-Results": "i=1; smtp.subspace.kernel.org;\n dmarc=none (p=none dis=none) header.from=pengutronix.de;\n spf=pass smtp.mailfrom=pengutronix.de; arc=none smtp.client-ip=185.203.201.7",
        "From": "Oleksij Rempel <o.rempel@pengutronix.de>",
        "To": "Guenter Roeck <linux@roeck-us.net>,\n\tRob Herring <robh@kernel.org>,\n\tKrzysztof Kozlowski <krzk+dt@kernel.org>,\n\tConor Dooley <conor+dt@kernel.org>,\n\tLee Jones <lee@kernel.org>,\n\tPeter Rosin <peda@axentia.se>,\n\tLinus Walleij <linusw@kernel.org>",
        "Cc": "Oleksij Rempel <o.rempel@pengutronix.de>,\n\tkernel@pengutronix.de,\n\tlinux-kernel@vger.kernel.org,\n\tdevicetree@vger.kernel.org,\n\tlinux-hwmon@vger.kernel.org,\n\tlinux-gpio@vger.kernel.org,\n\tDavid Jander <david@protonic.nl>",
        "Subject": "[PATCH v8 2/6] mfd: add NXP MC33978/MC34978 core driver",
        "Date": "Sun, 29 Mar 2026 11:05:57 +0200",
        "Message-ID": "<20260329090601.532477-3-o.rempel@pengutronix.de>",
        "X-Mailer": "git-send-email 2.47.3",
        "In-Reply-To": "<20260329090601.532477-1-o.rempel@pengutronix.de>",
        "References": "<20260329090601.532477-1-o.rempel@pengutronix.de>",
        "Precedence": "bulk",
        "X-Mailing-List": "linux-gpio@vger.kernel.org",
        "List-Id": "<linux-gpio.vger.kernel.org>",
        "List-Subscribe": "<mailto:linux-gpio+subscribe@vger.kernel.org>",
        "List-Unsubscribe": "<mailto:linux-gpio+unsubscribe@vger.kernel.org>",
        "MIME-Version": "1.0",
        "Content-Transfer-Encoding": "8bit",
        "X-SA-Exim-Connect-IP": "2a0a:edc0:0:c01:1d::a2",
        "X-SA-Exim-Mail-From": "ore@pengutronix.de",
        "X-SA-Exim-Scanned": "No (on metis.whiteo.stw.pengutronix.de);\n SAEximRunCond expanded to false",
        "X-PTX-Original-Recipient": "linux-gpio@vger.kernel.org"
    },
    "content": "Add core Multi-Function Device (MFD) driver for the NXP MC33978 and\nMC34978 Multiple Switch Detection Interfaces (MSDI).\n\nThe MC33978/MC34978 devices provide 22 switch detection inputs, analog\nmultiplexing (AMUX), and comprehensive hardware fault detection.\n\nThis core driver handles:\n- SPI communications via a custom regmap bus to support the device's\n  pipelined two-frame MISO response requirement.\n- Power sequencing for the VDDQ (logic) and VBATP (battery) regulators.\n- Interrupt demultiplexing, utilizing an irq_domain to provide 22 virtual\n  IRQs for switch state changes and 1 virtual IRQ for hardware faults.\n- Inline status harvesting from the SPI MSB to detect and trigger events\n  without requiring dedicated status register polling.\n\nChild devices (pinctrl, hwmon, mux) are instantiated by the core driver\nfrom match data.\n\nSigned-off-by: Oleksij Rempel <o.rempel@pengutronix.de>\n---\nchanges v8:\n- Fix TOCTOU race condition in SPI event harvesting loop by grabbing\n  harvested_flags before hardware reads.\n- Fix broken hierarchical IRQ allocation by replacing\n  irq_domain_set_hwirq_and_chip() with irq_domain_set_info() and passing\n  the handle_simple_irq flow handler.\n- Fix out-of-bounds stack read and endianness bug in for_each_set_bit() by\n  typing fired_pins as unsigned long instead of casting u32.\n- Prevent DMA cacheline corruption by explicitly aligning rx_frame with\n  ____cacheline_aligned to separate it from tx_frame.\n- Prevent spurious IRQs by verifying irq_find_mapping() returns non-zero\n  before calling handle_nested_irq().\n- Prevent missed transient hardware faults by explicitly evaluating\n  hw_flags in mc33978_handle_fault_condition().\n- Fix missing memory barrier in mc33978_harvest_status() with\n  smp_mb__after_atomic() to ensure harvested_flags visibility.\n- Fix devres use-after-free teardown race by using INIT_WORK and a custom\n  cancel action after the IRQ domain is destroyed, instead of\n  devm_work_autocancel.\n- Prevent spurious pin interrupts on boot by priming cached_pin_state via\n  a regmap_read() during probe before enabling IRQs.\n- Implement .irq_set_wake callback to support system wake from\n  hardware faults and switch state changes.\nchanges v7:\n- Fix event handling race condition with smp_mb()\n- Replace INIT_WORK() with devm_work_autocancel()\nchanges v6:\n- Remove the hardcoded bypass in irq_set_type to allow child drivers to\n  configure the FAULT line for edge-triggering.\n- Implement software edge-detection for FAULT interrupt.\n- Add MC33978_FAULT_ALARM_MASK to the shared header for child devices\n- Use READ_ONCE() and WRITE_ONCE() for lockless shared state variables\n  (cached_pin_mask, irq_rise, irq_fall, bus_fault_active,\n  cached_fault_active) accessed across the SPI harvesting context and\n  the event worker.\n- Add an if (hwirq < MC33978_NUM_PINS) guard in irq_mask() and\n  irq_unmask() to prevent the FAULT hwirq (22) from altering the\n  physical pin mask registers.\n- Lowercase the error strings in dev_err_probe()\n- Add inline comments explaining the irq_map fallback behavior\nchanges v5:\n- no changes\nchanges v4:\n- Removed .of_compatible strings from the mfd_cell arrays\nchanges v3:\n- Select IRQ_DOMAIN_HIERARCHY in Kconfig\n- Add .alloc and .free callbacks to irq_domain_ops to support hierarchical\n  IRQ domains\n- Set IRQ_DOMAIN_FLAG_HIERARCHY flag on the core MFD irq_domain\n- replace manual lock/unlock with guard()\nchanges v2:\n- Rewrite the driver header comment\n- Explicitly reject IRQ_TYPE_LEVEL_HIGH and IRQ_TYPE_LEVEL_LOW in\n  mc33978_irq_set_type() to correctly reflect the hardware's edge-only\n  interrupt capabilities.\n- Pass the hardware fault IRQ to the hwmon child driver via mfd_cell\n  resources, rather than requiring the child to parse the parent's irq_domain.\n- Ensure the Kconfig strictly depends on OF and SPI\n---\n drivers/mfd/Kconfig         |  15 +\n drivers/mfd/Makefile        |   2 +\n drivers/mfd/mc33978.c       | 988 ++++++++++++++++++++++++++++++++++++\n include/linux/mfd/mc33978.h |  92 ++++\n 4 files changed, 1097 insertions(+)\n create mode 100644 drivers/mfd/mc33978.c\n create mode 100644 include/linux/mfd/mc33978.h",
    "diff": "diff --git a/drivers/mfd/Kconfig b/drivers/mfd/Kconfig\nindex 7192c9d1d268..6dc9554822c9 100644\n--- a/drivers/mfd/Kconfig\n+++ b/drivers/mfd/Kconfig\n@@ -2566,6 +2566,21 @@ config MFD_UPBOARD_FPGA\n \t  To compile this driver as a module, choose M here: the module will be\n \t  called upboard-fpga.\n \n+config MFD_MC33978\n+\ttristate \"NXP MC33978/MC34978 industrial input controller core\"\n+\tdepends on OF\n+\tdepends on SPI\n+\tselect IRQ_DOMAIN_HIERARCHY\n+\tselect MFD_CORE\n+\tselect REGMAP\n+\thelp\n+\t  Support for the NXP MC33978/MC34978 industrial input controllers\n+\t  using the SPI interface.\n+\n+\t  This driver provides common support for accessing the device.\n+\t  Additional drivers must be enabled in order to use the functionality\n+\t  of the device.\n+\n config MFD_MAX7360\n \ttristate \"Maxim MAX7360 I2C IO Expander\"\n \tdepends on I2C\ndiff --git a/drivers/mfd/Makefile b/drivers/mfd/Makefile\nindex e75e8045c28a..dcd99315f683 100644\n--- a/drivers/mfd/Makefile\n+++ b/drivers/mfd/Makefile\n@@ -122,6 +122,8 @@ obj-$(CONFIG_MFD_MC13XXX)\t+= mc13xxx-core.o\n obj-$(CONFIG_MFD_MC13XXX_SPI)\t+= mc13xxx-spi.o\n obj-$(CONFIG_MFD_MC13XXX_I2C)\t+= mc13xxx-i2c.o\n \n+obj-$(CONFIG_MFD_MC33978)\t+= mc33978.o\n+\n obj-$(CONFIG_MFD_PF1550)\t+= pf1550.o\n \n obj-$(CONFIG_MFD_NCT6694)\t+= nct6694.o\ndiff --git a/drivers/mfd/mc33978.c b/drivers/mfd/mc33978.c\nnew file mode 100644\nindex 000000000000..ae8706b40cd4\n--- /dev/null\n+++ b/drivers/mfd/mc33978.c\n@@ -0,0 +1,988 @@\n+// SPDX-License-Identifier: GPL-2.0-only\n+/*\n+ * Copyright (C) 2024 David Jander <david@protonic.nl>, Protonic Holland\n+ * Copyright (C) 2026 Oleksij Rempel <kernel@pengutronix.de>, Pengutronix\n+ *\n+ * MC33978/MC34978 Multiple Switch Detection Interface - MFD Core Driver\n+ *\n+ * Driver Architecture:\n+ * This is the core MFD driver handling the physical SPI interface, power\n+ * management, and central interrupt routing. It instantiates the following\n+ * child devices:\n+ * - pinctrl: For GPIO read/write and wetting current configuration.\n+ * - hwmon:   For hardware fault monitoring (tLIM, over/under-voltage).\n+ * - mux:     For the 24-to-1 analog multiplexer (AMUX).\n+ *\n+ * Custom SPI Regmap & Event Harvesting:\n+ * The device uses a non-standard pipelined SPI protocol where the MISO\n+ * response logically lags the MOSI command by one frame. Furthermore, the\n+ * hardware embeds volatile global status bits (INT_flg, FAULT_STAT) into the\n+ * high byte of almost every SPI response (with specific exceptions handled by\n+ * the decoder). This core implements a custom regmap_bus to handle the\n+ * 2-frame dummy fetches and transparently \"harvests\" these status bits in\n+ * the background to schedule event processing.\n+ *\n+ * Interrupt Quirks & Limitations:\n+ * - Clear-on-Read: The physical INT_B line is directly tied to the INT_flg\n+ * bit. The hardware deasserts INT_B immediately upon *any* SPI transfer\n+ * that returns INT_flg. Harvesting this bit from all SPI traffic is the\n+ * ONLY way to know this device triggered an interrupt (crucial for shared\n+ * IRQ lines).\n+ * - Stateless Pin Edge Detection: The hardware lacks per-pin interrupt status\n+ * registers. To determine which pin triggered an event, the driver must\n+ * read the current pin states and XOR them against a previously cached state.\n+ * - Missed Short Pulses: Because pin interrupts are state-derived rather than\n+ * hardware-latched, very short physical pulses (shorter than the SPI read\n+ * latency) will be missed entirely if the pin reverts to its original state\n+ * before the READ_IN register is sampled by the IRQ thread.\n+ * - Edge-Only Pin Interrupts: The hardware only asserts INT_B on a state\n+ * change. It cannot continuously assert an interrupt while a pin is held at a\n+ * specific logic level. Consequently, the driver strictly emulates edge\n+ * interrupts (RISING/FALLING) and explicitly rejects LEVEL interrupt\n+ * configurations to prevent consumer misalignment.\n+ */\n+\n+#include <linux/array_size.h>\n+#include <linux/atomic.h>\n+#include <linux/bitfield.h>\n+#include <linux/bits.h>\n+#include <linux/cache.h>\n+#include <linux/cleanup.h>\n+#include <linux/device.h>\n+#include <linux/devm-helpers.h>\n+#include <linux/interrupt.h>\n+#include <linux/irq.h>\n+#include <linux/irqdomain.h>\n+#include <linux/mfd/core.h>\n+#include <linux/mod_devicetable.h>\n+#include <linux/module.h>\n+#include <linux/of_platform.h>\n+#include <linux/property.h>\n+#include <linux/regmap.h>\n+#include <linux/regulator/consumer.h>\n+#include <linux/spi/spi.h>\n+#include <linux/string.h>\n+\n+#include <linux/mfd/mc33978.h>\n+\n+#define MC33978_DRV_NAME\t\t\"mc33978\"\n+\n+/* Device identification signature returned by CHECK register */\n+#define MC33978_CHECK_SIGNATURE\t\t0x123456\n+\n+/*\n+ * Pipelined two-frame SPI transfer:\n+ * [REQ]  - Transmits command/write-data, receives dummy/previous response\n+ * [PIPE] - Transmits dummy CHECK, receives actual response to current command\n+ */\n+enum mc33978_frame_index {\n+\tMC33978_FRAME_REQ = 0,\n+\tMC33978_FRAME_PIPE,\n+\tMC33978_FRAME_COUNT\n+};\n+\n+/* SPI frame byte offsets (transmitted MSB first) */\n+enum mc33978_frame_offset {\n+\tMC33978_FRAME_CMD = 0,\n+\tMC33978_FRAME_DATA_HI,\n+\tMC33978_FRAME_DATA_MID,\n+\tMC33978_FRAME_DATA_LO\n+};\n+\n+#define MC33978_FRAME_LEN\t\t4\n+\n+/* Regmap internal value buffer offsets */\n+enum mc33978_payload_offset {\n+\tMC33978_PAYLOAD_HI = 0,\n+\tMC33978_PAYLOAD_MID,\n+\tMC33978_PAYLOAD_LO\n+};\n+\n+#define MC33978_PAYLOAD_LEN\t\t3\n+\n+/*\n+ * SPI Command Byte (FRAME_CMD).\n+ * Maps to frame bit [24] in the datasheet.\n+ */\n+#define MC33978_CMD_BYTE_WRITE\t\tBIT(0)\n+\n+/* High Payload Byte Masks (FRAME_DATA_HI / PAYLOAD_HI). */\n+#define MC33978_HI_BYTE_STAT_FAULT     BIT(7) /* Maps to frame bit [23] */\n+#define MC33978_HI_BYTE_STAT_INT       BIT(6) /* Maps to frame bit [22] */\n+\n+#define MC33978_HI_BYTE_STATUS_MASK    (MC33978_HI_BYTE_STAT_FAULT | \\\n+\t\t\t\t\tMC33978_HI_BYTE_STAT_INT)\n+\n+/* Maps to frame bits [21:16] */\n+#define MC33978_HI_BYTE_DATA_MASK\tGENMASK(5, 0)\n+\n+#define MC33978_CACHE_SG_PIN_MASK\tGENMASK(13, 0)\n+#define MC33978_CACHE_SP_PIN_MASK\tGENMASK(21, 14)\n+\n+#define MC33978_SG_PIN_MASK\t\tGENMASK(13, 0)\n+#define MC33978_SP_PIN_MASK\t\tGENMASK(7, 0)\n+\n+struct mc33978_data {\n+\tconst struct mfd_cell *cells;\n+\tint num_cells;\n+};\n+\n+struct mc33978_mfd_priv {\n+\tstruct spi_device *spi;\n+\tstruct regmap *map;\n+\n+\tstruct regulator *vddq;\n+\tstruct regulator *vbatp;\n+\n+\t/* Protects event processing loop and hardware state machine */\n+\tstruct mutex event_lock;\n+\tstruct work_struct event_work;\n+\tatomic_t is_handling;\n+\tatomic_t harvested_flags;\n+\tu32 cached_pin_state;\n+\tu32 cached_pin_mask;\n+\tu32 irq_rise;\n+\tu32 irq_fall;\n+\n+\t/* Protects IRQ mask registers and cached IRQ state */\n+\tstruct mutex irq_lock;\n+\tstruct irq_domain *domain;\n+\n+\t/* Pre-built SPI messages */\n+\tstruct spi_message msg_read;\n+\tstruct spi_message msg_write;\n+\tstruct spi_transfer xfer_read[MC33978_FRAME_COUNT];\n+\tstruct spi_transfer xfer_write;\n+\n+\tbool cached_fault_active;\n+\tbool bus_fault_active;\n+\n+\t/* DMA buffers at the end */\n+\tu8 tx_frame[MC33978_FRAME_COUNT][MC33978_FRAME_LEN] ____cacheline_aligned;\n+\tu8 rx_frame[MC33978_FRAME_COUNT][MC33978_FRAME_LEN] ____cacheline_aligned;\n+};\n+\n+static void mc33978_irq_mask(struct irq_data *data)\n+{\n+\tstruct mc33978_mfd_priv *mc = irq_data_get_irq_chip_data(data);\n+\tirq_hw_number_t hwirq = irqd_to_hwirq(data);\n+\n+\tif (hwirq < MC33978_NUM_PINS)\n+\t\tWRITE_ONCE(mc->cached_pin_mask,\n+\t\t\t   READ_ONCE(mc->cached_pin_mask) & ~BIT(hwirq));\n+}\n+\n+static void mc33978_irq_unmask(struct irq_data *data)\n+{\n+\tstruct mc33978_mfd_priv *mc = irq_data_get_irq_chip_data(data);\n+\tirq_hw_number_t hwirq = irqd_to_hwirq(data);\n+\n+\tif (hwirq < MC33978_NUM_PINS)\n+\t\tWRITE_ONCE(mc->cached_pin_mask,\n+\t\t\t   READ_ONCE(mc->cached_pin_mask) | BIT(hwirq));\n+}\n+\n+static void mc33978_irq_bus_lock(struct irq_data *data)\n+{\n+\tstruct mc33978_mfd_priv *mc = irq_data_get_irq_chip_data(data);\n+\n+\tmutex_lock(&mc->irq_lock);\n+}\n+\n+/**\n+ * mc33978_irq_bus_sync_unlock() - Sync cached IRQ mask to hardware and unlock\n+ * @data: IRQ data\n+ *\n+ * Writes the cached interrupt mask to the hardware IE_SG and IE_SP registers,\n+ * then releases the IRQ lock. This is where the actual hardware update occurs\n+ * after mask/unmask operations.\n+ */\n+static void mc33978_irq_bus_sync_unlock(struct irq_data *data)\n+{\n+\tstruct mc33978_mfd_priv *mc = irq_data_get_irq_chip_data(data);\n+\tu32 sg_mask, sp_mask, cached_mask;\n+\tint ret;\n+\n+\tcached_mask = READ_ONCE(mc->cached_pin_mask);\n+\n+\t/*\n+\t * Split the cached 22-bit pin mask into hardware register format:\n+\t * - SG pins: bits [13:0] (14 pins, mask 0x3FFF)\n+\t * - SP pins: bits [21:14] (8 pins, mask 0xFF)\n+\t */\n+\tsg_mask = FIELD_GET(MC33978_CACHE_SG_PIN_MASK, cached_mask);\n+\tsp_mask = FIELD_GET(MC33978_CACHE_SP_PIN_MASK, cached_mask);\n+\n+\tret = regmap_update_bits(mc->map, MC33978_REG_IE_SG,\n+\t\t\t\t MC33978_SG_PIN_MASK, sg_mask);\n+\tif (ret)\n+\t\tgoto unlock;\n+\n+\tret = regmap_update_bits(mc->map, MC33978_REG_IE_SP,\n+\t\t\t\t MC33978_SP_PIN_MASK, sp_mask);\n+unlock:\n+\tif (ret)\n+\t\tdev_err(&mc->spi->dev, \"failed to sync IRQ mask to hardware: %d\\n\",\n+\t\t\tret);\n+\n+\tmutex_unlock(&mc->irq_lock);\n+}\n+\n+static int mc33978_irq_set_type(struct irq_data *data, unsigned int type)\n+{\n+\tstruct mc33978_mfd_priv *mc = irq_data_get_irq_chip_data(data);\n+\tirq_hw_number_t hwirq = irqd_to_hwirq(data);\n+\tu32 mask = BIT(hwirq);\n+\n+\tif (type & (IRQ_TYPE_LEVEL_HIGH | IRQ_TYPE_LEVEL_LOW))\n+\t\treturn -EINVAL;\n+\n+\tif (type & IRQ_TYPE_EDGE_RISING)\n+\t\tWRITE_ONCE(mc->irq_rise, READ_ONCE(mc->irq_rise) | mask);\n+\telse\n+\t\tWRITE_ONCE(mc->irq_rise, READ_ONCE(mc->irq_rise) & ~mask);\n+\n+\tif (type & IRQ_TYPE_EDGE_FALLING)\n+\t\tWRITE_ONCE(mc->irq_fall, READ_ONCE(mc->irq_fall) | mask);\n+\telse\n+\t\tWRITE_ONCE(mc->irq_fall, READ_ONCE(mc->irq_fall) & ~mask);\n+\n+\treturn 0;\n+}\n+\n+static int mc33978_irq_set_wake(struct irq_data *data, unsigned int on)\n+{\n+\tstruct mc33978_mfd_priv *mc = irq_data_get_irq_chip_data(data);\n+\n+\treturn irq_set_irq_wake(mc->spi->irq, on);\n+}\n+\n+static struct irq_chip mc33978_irq_chip = {\n+\t.name\t\t\t= MC33978_DRV_NAME,\n+\t.irq_mask\t\t= mc33978_irq_mask,\n+\t.irq_unmask\t\t= mc33978_irq_unmask,\n+\t.irq_bus_lock\t\t= mc33978_irq_bus_lock,\n+\t.irq_bus_sync_unlock\t= mc33978_irq_bus_sync_unlock,\n+\t.irq_set_type\t\t= mc33978_irq_set_type,\n+\t.irq_set_wake\t\t= mc33978_irq_set_wake,\n+};\n+\n+static int mc33978_irq_map(struct irq_domain *d, unsigned int virq,\n+\t\t\t   irq_hw_number_t hw)\n+{\n+\tstruct mc33978_mfd_priv *mc = d->host_data;\n+\n+\tirq_set_chip_data(virq, mc);\n+\tirq_set_chip_and_handler(virq, &mc33978_irq_chip, handle_simple_irq);\n+\n+\tirq_set_nested_thread(virq, 1);\n+\tirq_clear_status_flags(virq, IRQ_NOREQUEST | IRQ_NOPROBE);\n+\n+\treturn 0;\n+}\n+\n+static int mc33978_irq_domain_alloc(struct irq_domain *domain,\n+\t\t\t\t    unsigned int virq,\n+\t\t\t\t    unsigned int nr_irqs, void *arg)\n+{\n+\tstruct mc33978_mfd_priv *mc = domain->host_data;\n+\tstruct irq_fwspec *fwspec = arg;\n+\tirq_hw_number_t hwirq;\n+\tint i;\n+\n+\tif (fwspec->param_count < 1)\n+\t\treturn -EINVAL;\n+\n+\thwirq = fwspec->param[0];\n+\n+\tfor (i = 0; i < nr_irqs; i++) {\n+\t\tirq_domain_set_info(domain, virq + i, hwirq + i,\n+\t\t\t\t    &mc33978_irq_chip, mc,\n+\t\t\t\t    handle_simple_irq, NULL, NULL);\n+\t\tirq_set_nested_thread(virq + i, 1);\n+\t\tirq_clear_status_flags(virq + i, IRQ_NOREQUEST | IRQ_NOPROBE);\n+\t}\n+\n+\treturn 0;\n+}\n+\n+static void mc33978_irq_domain_free(struct irq_domain *domain,\n+\t\t\t\t    unsigned int virq,\n+\t\t\t\t    unsigned int nr_irqs)\n+{\n+\tint i;\n+\n+\tfor (i = 0; i < nr_irqs; i++)\n+\t\tirq_domain_reset_irq_data(irq_domain_get_irq_data(domain,\n+\t\t\t\t\t\t\t\t  virq + i));\n+}\n+\n+static const struct irq_domain_ops mc33978_irq_domain_ops = {\n+\t.map\t= mc33978_irq_map,\n+\t.alloc\t= mc33978_irq_domain_alloc,\n+\t.free\t= mc33978_irq_domain_free,\n+\t.xlate\t= irq_domain_xlate_twocell,\n+};\n+\n+static void mc33978_irq_domain_remove(void *data)\n+{\n+\tstruct irq_domain *domain = data;\n+\n+\tirq_domain_remove(domain);\n+}\n+\n+static bool mc33978_handle_pin_changes(struct mc33978_mfd_priv *mc,\n+\t\t\t\t       unsigned int pin_state)\n+{\n+\tunsigned long fired_pins = 0;\n+\tu32 changed_pins;\n+\tu32 rise, fall;\n+\tint i;\n+\n+\tchanged_pins = pin_state ^ mc->cached_pin_state;\n+\tif (!changed_pins)\n+\t\treturn false;\n+\n+\tmc->cached_pin_state = pin_state;\n+\tchanged_pins &= READ_ONCE(mc->cached_pin_mask);\n+\n+\tif (!changed_pins)\n+\t\treturn false;\n+\n+\trise = READ_ONCE(mc->irq_rise);\n+\tfall = READ_ONCE(mc->irq_fall);\n+\n+\tfired_pins |= (changed_pins & pin_state) & rise;\n+\tfired_pins |= (changed_pins & ~pin_state) & fall;\n+\n+\tfor_each_set_bit(i, &fired_pins, MC33978_NUM_PINS) {\n+\t\tint virq = irq_find_mapping(mc->domain, i);\n+\n+\t\tif (virq)\n+\t\t\thandle_nested_irq(virq);\n+\t}\n+\n+\treturn true;\n+}\n+\n+static bool mc33978_handle_fault_condition(struct mc33978_mfd_priv *mc, u8 hw_flags)\n+{\n+\tbool fault_active = READ_ONCE(mc->bus_fault_active);\n+\tbool cached_fault = READ_ONCE(mc->cached_fault_active);\n+\tbool changed = fault_active ^ cached_fault;\n+\tbool transient = !changed && (hw_flags & MC33978_HI_BYTE_STAT_FAULT);\n+\tu32 rise, fall;\n+\tint virq;\n+\tbool handled = false;\n+\n+\tWRITE_ONCE(mc->cached_fault_active, fault_active);\n+\n+\tif (!changed && !transient)\n+\t\treturn false;\n+\n+\trise = READ_ONCE(mc->irq_rise);\n+\tfall = READ_ONCE(mc->irq_fall);\n+\n+\tvirq = irq_find_mapping(mc->domain, MC33978_HWIRQ_FAULT);\n+\tif (!virq)\n+\t\treturn false;\n+\n+\tif (transient) {\n+\t\t/* Transient pulse: trigger both edges if enabled */\n+\t\tif (rise & BIT(MC33978_HWIRQ_FAULT)) {\n+\t\t\thandle_nested_irq(virq);\n+\t\t\thandled = true;\n+\t\t}\n+\t\tif (fall & BIT(MC33978_HWIRQ_FAULT)) {\n+\t\t\thandle_nested_irq(virq);\n+\t\t\thandled = true;\n+\t\t}\n+\t} else {\n+\t\t/* Normal edge */\n+\t\tif (fault_active && (rise & BIT(MC33978_HWIRQ_FAULT))) {\n+\t\t\thandle_nested_irq(virq);\n+\t\t\thandled = true;\n+\t\t} else if (!fault_active && (fall & BIT(MC33978_HWIRQ_FAULT))) {\n+\t\t\thandle_nested_irq(virq);\n+\t\t\thandled = true;\n+\t\t}\n+\t}\n+\n+\treturn handled;\n+}\n+\n+static bool mc33978_process_single_event(struct mc33978_mfd_priv *mc)\n+{\n+\tunsigned int pin_state;\n+\tbool handled = false;\n+\tu8 hw_flags;\n+\tint ret;\n+\n+\t/*\n+\t * Grab harvested_flags BEFORE reading the hardware. If the read itself\n+\t * or a concurrent SPI transfer harvests new flags, they will remain set\n+\t * in harvested_flags and correctly trigger another pass of the event loop.\n+\t *\n+\t * Note on Performance: This architecture intentionally forces a second\n+\t * (redundant) SPI read of READ_IN during almost every interrupt event.\n+\t * While SPI framework overhead (CS toggling, DMA setup, context switches)\n+\t * makes this 4-byte transfer relatively costly, it is mathematically\n+\t * necessary to guarantee no edge events are permanently lost when a\n+\t * concurrent regmap access races with the IRQ thread, due to the hardware's\n+\t * clear-on-read global INT_flg design.\n+\t */\n+\thw_flags = atomic_xchg(&mc->harvested_flags, 0);\n+\n+\tret = regmap_read(mc->map, MC33978_REG_READ_IN, &pin_state);\n+\tif (ret)\n+\t\treturn false;\n+\n+\tif (mc33978_handle_pin_changes(mc, pin_state))\n+\t\thandled = true;\n+\n+\t/*\n+\t * Pass hw_flags to handle_fault_condition so transient faults\n+\t * can be detected and processed properly.\n+\t */\n+\tif (mc33978_handle_fault_condition(mc, hw_flags))\n+\t\thandled = true;\n+\n+\tif (hw_flags & MC33978_HI_BYTE_STAT_INT)\n+\t\thandled = true;\n+\n+\treturn handled;\n+}\n+\n+static bool mc33978_handle_events(struct mc33978_mfd_priv *mc)\n+{\n+\tbool handled = false;\n+\n+\tguard(mutex)(&mc->event_lock);\n+\n+\tdo {\n+\t\tatomic_set(&mc->is_handling, 1);\n+\t\t/*\n+\t\t * Ensure the is_handling state is globally visible before we\n+\t\t * process the hardware events and re-evaluate harvested_flags.\n+\t\t */\n+\t\tsmp_mb();\n+\n+\t\tif (mc33978_process_single_event(mc))\n+\t\t\thandled = true;\n+\n+\t\tatomic_set(&mc->is_handling, 0);\n+\t\t/*\n+\t\t * Ensure is_handling = 0 is globally visible before we check\n+\t\t * harvested_flags in the while loop, preventing a race where\n+\t\t * the harvester misses scheduling the workqueue.\n+\t\t */\n+\t\tsmp_mb();\n+\n+\t} while (atomic_read(&mc->harvested_flags) != 0);\n+\n+\treturn handled;\n+}\n+\n+static irqreturn_t mc33978_irq_thread(int irq, void *data)\n+{\n+\treturn mc33978_handle_events(data) ? IRQ_HANDLED : IRQ_NONE;\n+}\n+\n+static void mc33978_work_cancel(void *data)\n+{\n+\tstruct work_struct *work = data;\n+\n+\tcancel_work_sync(work);\n+}\n+\n+static int mc33978_irq_init(struct mc33978_mfd_priv *mc)\n+{\n+\tstruct device *dev = &mc->spi->dev;\n+\tint ret;\n+\n+\tmutex_init(&mc->irq_lock);\n+\n+\t/*\n+\t * Create IRQ domain with 23 interrupts:\n+\t * - hwirq 0-21: Pin change interrupts (22 pins)\n+\t * - hwirq 22: Fault interrupt (for hwmon driver)\n+\t */\n+\tmc->domain = irq_domain_add_linear(dev->of_node, MC33978_NUM_PINS + 1,\n+\t\t\t\t\t   &mc33978_irq_domain_ops, mc);\n+\tif (!mc->domain)\n+\t\treturn dev_err_probe(dev, -ENOMEM, \"failed to create IRQ domain\\n\");\n+\n+\tmc->domain->flags |= IRQ_DOMAIN_FLAG_HIERARCHY;\n+\n+\tret = devm_add_action_or_reset(dev, mc33978_irq_domain_remove,\n+\t\t\t\t       mc->domain);\n+\tif (ret)\n+\t\treturn ret;\n+\n+\t/*\n+\t * Cancel the work before the IRQ domain is destroyed to prevent\n+\t * use-after-free during device teardown.\n+\t */\n+\treturn devm_add_action_or_reset(dev, mc33978_work_cancel,\n+\t\t\t\t       &mc->event_work);\n+}\n+\n+static void mc33978_event_work(struct work_struct *work)\n+{\n+\tstruct mc33978_mfd_priv *mc =\n+\t\tcontainer_of(work, struct mc33978_mfd_priv, event_work);\n+\n+\tmc33978_handle_events(mc);\n+}\n+\n+/**\n+ * mc33978_harvest_status() - Collect status flags from SPI responses\n+ * @mc: Device private data\n+ * @status: Status bits (FAULT_STAT and INT_flg) from MISO frame\n+ *\n+ * Accumulates status flags harvested from SPI responses and schedules\n+ * event processing if not already in progress. Called by the SPI\n+ * read/write functions when status bits are detected in responses.\n+ */\n+static void mc33978_harvest_status(struct mc33978_mfd_priv *mc, u8 status)\n+{\n+\tbool fault_active;\n+\n+\tfault_active = !!(status & MC33978_HI_BYTE_STAT_FAULT);\n+\n+\t/* Track the absolute latest physical state seen on the bus */\n+\tWRITE_ONCE(mc->bus_fault_active, fault_active);\n+\n+\t/* If the bus state changed from what the IRQ thread last evaluated, wake it up */\n+\tif (fault_active != READ_ONCE(mc->cached_fault_active))\n+\t\tatomic_or(MC33978_HI_BYTE_STAT_FAULT, &mc->harvested_flags);\n+\n+\tif (status & MC33978_HI_BYTE_STAT_INT)\n+\t\tatomic_or(MC33978_HI_BYTE_STAT_INT, &mc->harvested_flags);\n+\n+\t/* Ensure harvested_flags is visible before reading is_handling */\n+\tsmp_mb__after_atomic();\n+\n+\t/*\n+\t * If the handler is not actively running and there are flags to process,\n+\t * schedule the background worker.\n+\t */\n+\tif (!atomic_read(&mc->is_handling) && atomic_read(&mc->harvested_flags))\n+\t\tschedule_work(&mc->event_work);\n+}\n+\n+/**\n+ * mc33978_prepare_messages() - Initialize the persistent SPI messages\n+ * @mc: Device private data\n+ *\n+ * Hardware pipelining constraints:\n+ * - Write (1 Frame): The device executes write commands immediately upon\n+ * CS de-assertion. No fetch frame is required.\n+ * - Read (2 Frames): The MISO response logically lags by one frame.\n+ * Frame 1 transmits the read request and toggles CS to latch it.\n+ * Frame 2 transmits a dummy CHECK command to fetch the actual payload.\n+ */\n+static void mc33978_prepare_messages(struct mc33978_mfd_priv *mc)\n+{\n+\t/* --- Prepare Write Message (1 Frame) --- */\n+\tspi_message_init(&mc->msg_write);\n+\n+\tmc->xfer_write.tx_buf = mc->tx_frame[MC33978_FRAME_REQ];\n+\tmc->xfer_write.rx_buf = mc->rx_frame[MC33978_FRAME_REQ];\n+\tmc->xfer_write.len = MC33978_FRAME_LEN;\n+\n+\tspi_message_add_tail(&mc->xfer_write, &mc->msg_write);\n+\n+\t/* --- Prepare Read Message (2 Frames) --- */\n+\tspi_message_init(&mc->msg_read);\n+\n+\t/* Frame 1: Request */\n+\tmc->xfer_read[MC33978_FRAME_REQ].tx_buf =\n+\t\tmc->tx_frame[MC33978_FRAME_REQ];\n+\tmc->xfer_read[MC33978_FRAME_REQ].rx_buf =\n+\t\tmc->rx_frame[MC33978_FRAME_REQ];\n+\tmc->xfer_read[MC33978_FRAME_REQ].len = MC33978_FRAME_LEN;\n+\tmc->xfer_read[MC33978_FRAME_REQ].cs_change = 1; /* Latch command */\n+\n+\t/* Frame 2: Fetch (Dummy CHECK) */\n+\tmc->xfer_read[MC33978_FRAME_PIPE].tx_buf =\n+\t\tmc->tx_frame[MC33978_FRAME_PIPE];\n+\tmc->xfer_read[MC33978_FRAME_PIPE].rx_buf =\n+\t\tmc->rx_frame[MC33978_FRAME_PIPE];\n+\tmc->xfer_read[MC33978_FRAME_PIPE].len = MC33978_FRAME_LEN;\n+\n+\t/* Preload the dummy CHECK command statically */\n+\tmc->tx_frame[MC33978_FRAME_PIPE][MC33978_FRAME_CMD] = MC33978_REG_CHECK;\n+\n+\tspi_message_add_tail(&mc->xfer_read[MC33978_FRAME_REQ], &mc->msg_read);\n+\tspi_message_add_tail(&mc->xfer_read[MC33978_FRAME_PIPE], &mc->msg_read);\n+}\n+\n+/**\n+ * mc33978_rx_decode() - Decode MISO response frame and extract status\n+ * @rx_frame: Received SPI frame buffer (4 bytes)\n+ * @val_buf: Output buffer for regmap (exactly 3 bytes, optional)\n+ *\n+ * Translates the 4-byte SPI response into a 3-byte regmap payload.\n+ * Harvests the volatile INTflg and FAULT_STAT bits from the MSB.\n+ *\n+ * Return: Status bits if present, 0 otherwise.\n+ */\n+static u8 mc33978_rx_decode(const u8 *rx_frame, u8 *val_buf)\n+{\n+\tu8 cmd = rx_frame[MC33978_FRAME_CMD] & ~MC33978_CMD_BYTE_WRITE;\n+\tbool has_status;\n+\tu8 status = 0;\n+\n+\tswitch (cmd) {\n+\tcase MC33978_REG_CHECK:\n+\tcase MC33978_REG_WET_SP:\n+\tcase MC33978_REG_WET_SG0:\n+\t\thas_status = false;\n+\t\tbreak;\n+\tdefault:\n+\t\thas_status = true;\n+\t\tbreak;\n+\t}\n+\n+\tif (has_status)\n+\t\tstatus = rx_frame[MC33978_FRAME_DATA_HI] &\n+\t\t\t\t\t\tMC33978_HI_BYTE_STATUS_MASK;\n+\n+\tif (!val_buf)\n+\t\treturn status;\n+\n+\tmemcpy(val_buf, &rx_frame[MC33978_FRAME_DATA_HI], MC33978_PAYLOAD_LEN);\n+\n+\tif (has_status)\n+\t\tval_buf[MC33978_PAYLOAD_HI] &= MC33978_HI_BYTE_DATA_MASK;\n+\n+\treturn status;\n+}\n+\n+static int mc33978_spi_write(void *ctx, const void *data, size_t count)\n+{\n+\tstruct mc33978_mfd_priv *mc = ctx;\n+\tu8 status;\n+\tint ret;\n+\n+\tif (count != MC33978_FRAME_LEN)\n+\t\treturn -EINVAL;\n+\n+\tmemcpy(mc->tx_frame[MC33978_FRAME_REQ], data, MC33978_FRAME_LEN);\n+\n+\tret = spi_sync(mc->spi, &mc->msg_write);\n+\tif (ret)\n+\t\treturn ret;\n+\n+\tstatus = mc33978_rx_decode(mc->rx_frame[MC33978_FRAME_REQ], NULL);\n+\tmc33978_harvest_status(mc, status);\n+\n+\treturn 0;\n+}\n+\n+static int mc33978_spi_read(void *ctx, const void *reg_buf, size_t reg_size,\n+\t\t\t    void *val_buf, size_t val_size)\n+{\n+\tstruct mc33978_mfd_priv *mc = ctx;\n+\tu8 status_req, status_pipe;\n+\tint ret;\n+\n+\tif (reg_size != 1 || val_size != MC33978_PAYLOAD_LEN)\n+\t\treturn -EINVAL;\n+\n+\tmemset(&mc->tx_frame[MC33978_FRAME_REQ][MC33978_FRAME_DATA_HI], 0,\n+\t       MC33978_PAYLOAD_LEN);\n+\tmc->tx_frame[MC33978_FRAME_REQ][MC33978_FRAME_CMD] =\n+\t\t((const u8 *)reg_buf)[0];\n+\n+\tret = spi_sync(mc->spi, &mc->msg_read);\n+\tif (ret)\n+\t\treturn ret;\n+\n+\tstatus_req = mc33978_rx_decode(mc->rx_frame[MC33978_FRAME_REQ], NULL);\n+\tstatus_pipe = mc33978_rx_decode(mc->rx_frame[MC33978_FRAME_PIPE],\n+\t\t\t\t\tval_buf);\n+\n+\tmc33978_harvest_status(mc, status_req | status_pipe);\n+\n+\treturn 0;\n+}\n+\n+static const struct regmap_bus mc33978_regmap_bus = {\n+\t.read = mc33978_spi_read,\n+\t.write = mc33978_spi_write,\n+};\n+\n+static const struct regmap_range mc33978_volatile_range[] = {\n+\tregmap_reg_range(MC33978_REG_READ_IN, MC33978_REG_RESET),\n+};\n+\n+static const struct regmap_access_table mc33978_volatile_table = {\n+\t.yes_ranges = mc33978_volatile_range,\n+\t.n_yes_ranges = ARRAY_SIZE(mc33978_volatile_range),\n+};\n+\n+static const struct regmap_range mc33978_precious_range[] = {\n+\tregmap_reg_range(MC33978_REG_READ_IN, MC33978_REG_RESET),\n+};\n+\n+static const struct regmap_access_table mc33978_precious_table = {\n+\t.yes_ranges = mc33978_precious_range,\n+\t.n_yes_ranges = ARRAY_SIZE(mc33978_precious_range),\n+};\n+\n+/*\n+ * NOTE: Need to fake REG_IRQ and REG_RESET as readable, so that regcache\n+ * will NOT write to them on a cache sync. Sounds counterintuitive, but marking\n+ * a reg as \"precious\" or \"volatile\" is the only way to avoid this, and that\n+ * works only with readable regs.\n+ */\n+static const struct regmap_range mc33978_readable_range[] = {\n+\tregmap_reg_range(MC33978_REG_CHECK, MC33978_REG_WET_SG1),\n+\tregmap_reg_range(MC33978_REG_CWET_SP, MC33978_REG_WDEB_SG),\n+\tregmap_reg_range(MC33978_REG_AMUX_CTRL, MC33978_REG_RESET),\n+};\n+\n+static const struct regmap_access_table mc33978_readable_table = {\n+\t.yes_ranges = mc33978_readable_range,\n+\t.n_yes_ranges = ARRAY_SIZE(mc33978_readable_range),\n+};\n+\n+static const struct regmap_range mc33978_writable_range[] = {\n+\tregmap_reg_range(MC33978_REG_CONFIG, MC33978_REG_WET_SG1),\n+\tregmap_reg_range(MC33978_REG_CWET_SP, MC33978_REG_AMUX_CTRL),\n+\tregmap_reg_range(MC33978_REG_IRQ, MC33978_REG_RESET),\n+};\n+\n+static const struct regmap_access_table mc33978_writable_table = {\n+\t.yes_ranges = mc33978_writable_range,\n+\t.n_yes_ranges = ARRAY_SIZE(mc33978_writable_range),\n+};\n+\n+static const struct regmap_config mc33978_regmap_config = {\n+\t.name = MC33978_DRV_NAME,\n+\t.reg_bits = 8,\n+\t.val_bits = 24,\n+\t.reg_stride = 2,\n+\t.write_flag_mask = MC33978_CMD_BYTE_WRITE,\n+\t.reg_format_endian = REGMAP_ENDIAN_BIG,\n+\t.val_format_endian = REGMAP_ENDIAN_BIG,\n+\t.use_single_read = true,\n+\t.use_single_write = true,\n+\t.volatile_table = &mc33978_volatile_table,\n+\t.precious_table = &mc33978_precious_table,\n+\t.rd_table = &mc33978_readable_table,\n+\t.wr_table = &mc33978_writable_table,\n+\t.cache_type = REGCACHE_MAPLE,\n+\t.max_register = MC33978_REG_RESET,\n+};\n+\n+static int mc33978_power_on(struct mc33978_mfd_priv *mc)\n+{\n+\tstruct device *dev = &mc->spi->dev;\n+\tint ret;\n+\n+\tret = regulator_enable(mc->vddq);\n+\tif (ret)\n+\t\treturn dev_err_probe(dev, ret, \"failed to enable VDDQ supply\\n\");\n+\n+\tret = regulator_enable(mc->vbatp);\n+\tif (ret) {\n+\t\tregulator_disable(mc->vddq);\n+\t\treturn dev_err_probe(dev, ret, \"failed to enable VBATP supply\\n\");\n+\t}\n+\n+\treturn 0;\n+}\n+\n+static void mc33978_power_off(void *data)\n+{\n+\tstruct mc33978_mfd_priv *mc = data;\n+\n+\tregulator_disable(mc->vbatp);\n+\tregulator_disable(mc->vddq);\n+}\n+\n+/**\n+ * mc33978_check_device() - Verify SPI communication with device\n+ * @mc: Device context\n+ *\n+ * Reads the CHECK register which should return a fixed signature (0x123456).\n+ * This verifies that SPI communication is working correctly.\n+ *\n+ * Return: 0 on success, -ENODEV if signature doesn't match\n+ */\n+static int mc33978_check_device(struct mc33978_mfd_priv *mc)\n+{\n+\tstruct device *dev = &mc->spi->dev;\n+\tunsigned int check;\n+\tint ret;\n+\n+\tret = regmap_read(mc->map, MC33978_REG_CHECK, &check);\n+\tif (ret)\n+\t\treturn ret;\n+\n+\tif (check != MC33978_CHECK_SIGNATURE)\n+\t\treturn dev_err_probe(dev, -ENODEV,\n+\t\t\t\t     \"SPI check failed. Expected: 0x%06x, got: 0x%06x\\n\",\n+\t\t\t\t     MC33978_CHECK_SIGNATURE, check);\n+\n+\treturn 0;\n+}\n+\n+static const struct resource mc33978_hwmon_resources[] = {\n+\tDEFINE_RES_IRQ(MC33978_HWIRQ_FAULT),\n+};\n+\n+static const struct mfd_cell mc33978_cells[] = {\n+\t{ .name = \"mc33978-pinctrl\" },\n+\t{\n+\t\t.name = \"mc33978-hwmon\",\n+\t\t.resources = mc33978_hwmon_resources,\n+\t\t.num_resources = ARRAY_SIZE(mc33978_hwmon_resources),\n+\t},\n+\t{ .name = \"mc33978-mux\" },\n+};\n+\n+static const struct mfd_cell mc34978_cells[] = {\n+\t{ .name = \"mc34978-pinctrl\" },\n+\t{\n+\t\t.name = \"mc34978-hwmon\",\n+\t\t.resources = mc33978_hwmon_resources,\n+\t\t.num_resources = ARRAY_SIZE(mc33978_hwmon_resources),\n+\t},\n+\t{ .name = \"mc34978-mux\" },\n+};\n+\n+static const struct mc33978_data mc33978_match_data = {\n+\t.cells = mc33978_cells,\n+\t.num_cells = ARRAY_SIZE(mc33978_cells),\n+};\n+\n+static const struct mc33978_data mc34978_match_data = {\n+\t.cells = mc34978_cells,\n+\t.num_cells = ARRAY_SIZE(mc34978_cells),\n+};\n+\n+static int mc33978_probe(struct spi_device *spi)\n+{\n+\tconst struct mc33978_data *match_data;\n+\tstruct device *dev = &spi->dev;\n+\tstruct mc33978_mfd_priv *mc;\n+\tint ret;\n+\n+\tmatch_data = spi_get_device_match_data(spi);\n+\tif (!match_data)\n+\t\treturn dev_err_probe(dev, -ENODEV, \"no device match data found\\n\");\n+\n+\tmc = devm_kzalloc(dev, sizeof(*mc), GFP_KERNEL);\n+\tif (!mc)\n+\t\treturn -ENOMEM;\n+\n+\tmc->spi = spi;\n+\tspi_set_drvdata(spi, mc);\n+\n+\tmc->vddq = devm_regulator_get(dev, \"vddq\");\n+\tif (IS_ERR(mc->vddq))\n+\t\treturn dev_err_probe(dev, PTR_ERR(mc->vddq),\n+\t\t\t\t     \"failed to get VDDQ regulator\\n\");\n+\n+\tmc->vbatp = devm_regulator_get(dev, \"vbatp\");\n+\tif (IS_ERR(mc->vbatp))\n+\t\treturn dev_err_probe(dev, PTR_ERR(mc->vbatp),\n+\t\t\t\t     \"failed to get VBATP regulator\\n\");\n+\n+\tret = mc33978_power_on(mc);\n+\tif (ret)\n+\t\treturn ret;\n+\n+\tret = devm_add_action_or_reset(dev, mc33978_power_off, mc);\n+\tif (ret)\n+\t\treturn ret;\n+\n+\tmutex_init(&mc->event_lock);\n+\n+\tINIT_WORK(&mc->event_work, mc33978_event_work);\n+\n+\tatomic_set(&mc->harvested_flags, 0);\n+\tatomic_set(&mc->is_handling, 0);\n+\n+\tmc33978_prepare_messages(mc);\n+\n+\tmc->map = devm_regmap_init(dev, &mc33978_regmap_bus, mc,\n+\t\t\t\t   &mc33978_regmap_config);\n+\tif (IS_ERR(mc->map))\n+\t\treturn dev_err_probe(dev, PTR_ERR(mc->map), \"can't init regmap\\n\");\n+\n+\tret = mc33978_check_device(mc);\n+\tif (ret)\n+\t\treturn dev_err_probe(dev, ret, \"can't use SPI bus\\n\");\n+\n+\t/* Initialize IRQ domain early so background worker can safely use it */\n+\tret = mc33978_irq_init(mc);\n+\tif (ret)\n+\t\treturn ret;\n+\n+\t/* Disable interrupts to prevent storms during priming */\n+\tret = regmap_write(mc->map, MC33978_REG_IE_SP, 0);\n+\tif (ret)\n+\t\treturn ret;\n+\n+\tret = regmap_write(mc->map, MC33978_REG_IE_SG, 0);\n+\tif (ret)\n+\t\treturn ret;\n+\n+\t/* Prime the cached pin state under lock to prevent spurious events */\n+\tmutex_lock(&mc->event_lock);\n+\tret = regmap_read(mc->map, MC33978_REG_READ_IN, &mc->cached_pin_state);\n+\tmutex_unlock(&mc->event_lock);\n+\tif (ret)\n+\t\treturn dev_err_probe(dev, ret, \"failed to read initial pin state\\n\");\n+\n+\tif (mc->spi->irq <= 0)\n+\t\treturn dev_err_probe(dev, -EINVAL, \"no valid IRQ provided for INT_B pin\\n\");\n+\n+\tret = devm_request_threaded_irq(dev, mc->spi->irq, NULL,\n+\t\t\t\t\tmc33978_irq_thread,\n+\t\t\t\t\tIRQF_ONESHOT | IRQF_SHARED,\n+\t\t\t\t\tdev_name(dev), mc);\n+\tif (ret)\n+\t\treturn dev_err_probe(dev, ret, \"failed to request IRQ\\n\");\n+\n+\tret = devm_mfd_add_devices(dev, PLATFORM_DEVID_NONE,\n+\t\t\t\t   match_data->cells, match_data->num_cells,\n+\t\t\t\t   NULL, 0, mc->domain);\n+\tif (ret)\n+\t\treturn dev_err_probe(dev, ret, \"failed to add MFD child devices\\n\");\n+\n+\treturn 0;\n+}\n+\n+static const struct of_device_id mc33978_of_match[] = {\n+\t{ .compatible = \"nxp,mc33978\", .data = &mc33978_match_data },\n+\t{ .compatible = \"nxp,mc34978\", .data = &mc34978_match_data },\n+\t{ }\n+};\n+MODULE_DEVICE_TABLE(of, mc33978_of_match);\n+\n+static const struct spi_device_id mc33978_spi_id[] = {\n+\t{ \"mc33978\", (kernel_ulong_t)&mc33978_match_data },\n+\t{ \"mc34978\", (kernel_ulong_t)&mc34978_match_data },\n+\t{ }\n+};\n+MODULE_DEVICE_TABLE(spi, mc33978_spi_id);\n+\n+static struct spi_driver mc33978_driver = {\n+\t.driver = {\n+\t\t.name = MC33978_DRV_NAME,\n+\t\t.of_match_table = mc33978_of_match,\n+\t},\n+\t.probe = mc33978_probe,\n+\t.id_table = mc33978_spi_id,\n+};\n+module_spi_driver(mc33978_driver);\n+\n+MODULE_AUTHOR(\"David Jander <david@protonic.nl>\");\n+MODULE_DESCRIPTION(\"NXP MC33978/MC34978 MFD core driver\");\n+MODULE_LICENSE(\"GPL\");\ndiff --git a/include/linux/mfd/mc33978.h b/include/linux/mfd/mc33978.h\nnew file mode 100644\nindex 000000000000..e8dec678e5a4\n--- /dev/null\n+++ b/include/linux/mfd/mc33978.h\n@@ -0,0 +1,92 @@\n+/* SPDX-License-Identifier: GPL-2.0-only */\n+/*\n+ * Copyright (C) 2024 David Jander <david@protonic.nl>, Protonic Holland\n+ * Copyright (C) 2026 Oleksij Rempel <kernel@pengutronix.de>, Pengutronix\n+ *\n+ * MC34978/MC33978 Multiple Switch Detection Interface - Shared Definitions\n+ */\n+\n+#ifndef _LINUX_MFD_MC33978_H\n+#define _LINUX_MFD_MC33978_H\n+\n+#include <linux/bits.h>\n+\n+/* Register Map - All addresses are base command bytes (R/W bit = 0) */\n+#define MC33978_REG_CHECK\t0x00\t/* SPI communication check */\n+#define MC33978_REG_CONFIG\t0x02\t/* Device configuration */\n+#define MC33978_REG_TRI_SP\t0x04\t/* Tri-state enable SP */\n+#define MC33978_REG_TRI_SG\t0x06\t/* Tri-state enable SG */\n+#define MC33978_REG_WET_SP\t0x08\t/* Wetting current level SP */\n+#define MC33978_REG_WET_SG0\t0x0a\t/* Wetting current level SG0 (SG7-SG0) */\n+#define MC33978_REG_WET_SG1\t0x0c\t/* Wetting current level SG1 (SG13-SG8) */\n+#define MC33978_REG_CWET_SP\t0x16\t/* Continuous wetting current SP */\n+#define MC33978_REG_CWET_SG\t0x18\t/* Continuous wetting current SG */\n+#define MC33978_REG_IE_SP\t0x1a\t/* Interrupt enable SP */\n+#define MC33978_REG_IE_SG\t0x1c\t/* Interrupt enable SG */\n+#define MC33978_REG_LPM_CONFIG\t0x1e\t/* Low-power mode configuration */\n+#define MC33978_REG_WAKE_SP\t0x20\t/* Wake-up enable SP */\n+#define MC33978_REG_WAKE_SG\t0x22\t/* Wake-up enable SG */\n+#define MC33978_REG_COMP_SP\t0x24\t/* Comparator only mode SP */\n+#define MC33978_REG_COMP_SG\t0x26\t/* Comparator only mode SG */\n+#define MC33978_REG_LPM_VT_SP\t0x28\t/* LPM voltage threshold SP */\n+#define MC33978_REG_LPM_VT_SG\t0x2a\t/* LPM voltage threshold SG */\n+#define MC33978_REG_IP_SP\t0x2c\t/* Polling current SP */\n+#define MC33978_REG_IP_SG\t0x2e\t/* Polling current SG */\n+#define MC33978_REG_SPOLL_SP\t0x30\t/* Slow polling SP */\n+#define MC33978_REG_SPOLL_SG\t0x32\t/* Slow polling SG */\n+#define MC33978_REG_WDEB_SP\t0x34\t/* Wake-up debounce SP */\n+#define MC33978_REG_WDEB_SG\t0x36\t/* Wake-up debounce SG */\n+#define MC33978_REG_ENTER_LPM\t0x38\t/* Enter low-power mode (write-only) */\n+#define MC33978_REG_AMUX_CTRL\t0x3a\t/* AMUX control */\n+#define MC33978_REG_READ_IN\t0x3e\t/* Read switch status (READ_SW in datasheet) */\n+#define MC33978_REG_FAULT\t0x42\t/* Fault status register */\n+#define MC33978_REG_IRQ\t\t0x46\t/* Interrupt request (write-only) */\n+#define MC33978_REG_RESET\t0x48\t/* Reset (write-only) */\n+\n+/*\n+ * FAULT Register (0x42) bit definitions\n+ * Reading this register clears most fault flags except persistent conditions\n+ */\n+#define MC33978_FAULT_SPI_ERROR\tBIT(10)\t/* SPI communication error */\n+#define MC33978_FAULT_HASH\tBIT(9)\t/* SPI register hash mismatch */\n+#define MC33978_FAULT_UV\tBIT(7)\t/* VBATP undervoltage */\n+#define MC33978_FAULT_OV\tBIT(6)\t/* VBATP overvoltage */\n+#define MC33978_FAULT_TEMP_WARN\tBIT(5)\t/* Temperature warning threshold */\n+#define MC33978_FAULT_OT\tBIT(4)\t/* Over-temperature */\n+#define MC33978_FAULT_INTB_WAKE\tBIT(3)\t/* Woken by INT_B pin */\n+#define MC33978_FAULT_WAKEB_WAKE BIT(2)\t/* Woken by WAKE_B pin */\n+#define MC33978_FAULT_SPI_WAKE\tBIT(1)\t/* Woken by SPI message */\n+#define MC33978_FAULT_POR\tBIT(0)\t/* Power-on reset occurred */\n+\n+/* Critical faults that need immediate attention */\n+#define MC33978_FAULT_CRITICAL\t(MC33978_FAULT_UV | \\\n+\t\t\t\t MC33978_FAULT_OV | \\\n+\t\t\t\t MC33978_FAULT_OT)\n+\n+/* Bits relevant as hwmon alarms; excludes wake/reset/SPI status bits */\n+#define MC33978_FAULT_ALARM_MASK\t(MC33978_FAULT_UV | \\\n+\t\t\t\t\t MC33978_FAULT_OV | \\\n+\t\t\t\t\t MC33978_FAULT_TEMP_WARN | \\\n+\t\t\t\t\t MC33978_FAULT_OT)\n+\n+#define MC33978_NUM_PINS\t22\n+\n+/*\n+ * Virtual IRQ number for fault handling.\n+ * Using hwirq 22 (beyond the 22 pin IRQs 0-21).\n+ */\n+#define MC33978_HWIRQ_FAULT\t22\n+\n+/*\n+ * AMUX channel definitions\n+ * The AMUX can route one of 24 signals to the external AMUX pin\n+ */\n+#define MC33978_AMUX_CH_SG0\t0\t/* Switch-to-Ground inputs 0-13 */\n+#define MC33978_AMUX_CH_SG13\t13\n+#define MC33978_AMUX_CH_SP0\t14\t/* Programmable switch inputs 0-7 */\n+#define MC33978_AMUX_CH_SP7\t21\n+#define MC33978_AMUX_CH_TEMP\t22\t/* Internal temperature diode */\n+#define MC33978_AMUX_CH_VBATP\t23\t/* Battery voltage sense */\n+#define MC33978_NUM_AMUX_CH\t24\t/* Total number of AMUX channels */\n+\n+#endif /* _LINUX_MFD_MC33978_H */\n",
    "prefixes": [
        "v8",
        "2/6"
    ]
}