Patch Detail
get:
Show a patch.
patch:
Update a patch.
put:
Update a patch.
GET /api/patches/2218223/?format=api
{ "id": 2218223, "url": "http://patchwork.ozlabs.org/api/patches/2218223/?format=api", "web_url": "http://patchwork.ozlabs.org/project/linux-gpio/patch/20260331171612.102018-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": "<20260331171612.102018-3-o.rempel@pengutronix.de>", "list_archive_url": null, "date": "2026-03-31T17:16:08", "name": "[v9,2/6] mfd: add NXP MC33978/MC34978 core driver", "commit_ref": null, "pull_url": null, "state": "new", "archived": false, "hash": "abca9ff2415348fee2f751bb1217e3fa3f9c8485", "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/20260331171612.102018-3-o.rempel@pengutronix.de/mbox/", "series": [ { "id": 498233, "url": "http://patchwork.ozlabs.org/api/series/498233/?format=api", "web_url": "http://patchwork.ozlabs.org/project/linux-gpio/list/?series=498233", "date": "2026-03-31T17:16:09", "name": "mfd: Add support for NXP MC33978/MC34978 MSDI", "version": 9, "mbox": "http://patchwork.ozlabs.org/series/498233/mbox/" } ], "comments": "http://patchwork.ozlabs.org/api/patches/2218223/comments/", "check": "pending", "checks": "http://patchwork.ozlabs.org/api/patches/2218223/checks/", "tags": {}, "related": [], "headers": { "Return-Path": "\n <linux-gpio+bounces-34498-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=2600:3c15:e001:75::12fc:5321; helo=sin.lore.kernel.org;\n envelope-from=linux-gpio+bounces-34498-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 sin.lore.kernel.org (sin.lore.kernel.org\n [IPv6:2600:3c15:e001:75::12fc:5321])\n\t(using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)\n\t key-exchange x25519 server-signature ECDSA (secp384r1) server-digest SHA384)\n\t(No client certificate requested)\n\tby legolas.ozlabs.org (Postfix) with ESMTPS id 4flbGD63qwz1y1q\n\tfor <incoming@patchwork.ozlabs.org>; Wed, 01 Apr 2026 04:49:08 +1100 (AEDT)", "from smtp.subspace.kernel.org (conduit.subspace.kernel.org\n [100.90.174.1])\n\tby sin.lore.kernel.org (Postfix) with ESMTP id DC0683077A25\n\tfor <incoming@patchwork.ozlabs.org>; Tue, 31 Mar 2026 17:18:07 +0000 (UTC)", "from localhost.localdomain (localhost.localdomain [127.0.0.1])\n\tby smtp.subspace.kernel.org (Postfix) with ESMTP id B799636AB47;\n\tTue, 31 Mar 2026 17:16:51 +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 AEE8C36829D\n\tfor <linux-gpio@vger.kernel.org>; Tue, 31 Mar 2026 17:16:48 +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 1w7chK-0004mG-Dv; Tue, 31 Mar 2026 19:16:14 +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 1w7chJ-0034jN-1i;\n\tTue, 31 Mar 2026 19:16:13 +0200", "from ore by dude04 with local (Exim 4.98.2)\n\t(envelope-from <ore@pengutronix.de>)\n\tid 1w7chJ-00000000QYv-1uZD;\n\tTue, 31 Mar 2026 19:16:13 +0200" ], "ARC-Seal": "i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116;\n\tt=1774977411; cv=none;\n b=OP8usoe7rMer6wUvtJBJV41MnAOtH8URDhp5PSYRZyJkiydlgy1/DoWrPWzWqEqA16c9d/fOzY6qtfN3P3sul+qgQxXEzVUMeS18I/O/pZatqWRUiD1xQ53k8RvfbcNzMCN9hctQBJ59fcZiyvS3DMaUASmXQ+vdb6IJ5MkI1A4=", "ARC-Message-Signature": "i=1; a=rsa-sha256; d=subspace.kernel.org;\n\ts=arc-20240116; t=1774977411; c=relaxed/simple;\n\tbh=MkHTFOqhBSnAvHCBOJV/berRdoLaFabztIg97GygCug=;\n\th=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References:\n\t MIME-Version;\n b=sb/wz1bDJj2ZbgBJk18YyqfMzWUfdBAb5WCvboIFdwTB5LLS471B8l+vLqdEYSfB0eAj8hkBc2OJIr50/pqHsLuCa0D5y4Mpr4XfrkQHccMWUiCTfFLMM4p2giFHfNLxpvGcgnloCB7MvANbPPwtg5pqKoTcUucUjSQSQMC5q4E=", "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 v9 2/6] mfd: add NXP MC33978/MC34978 core driver", "Date": "Tue, 31 Mar 2026 19:16:08 +0200", "Message-ID": "<20260331171612.102018-3-o.rempel@pengutronix.de>", "X-Mailer": "git-send-email 2.47.3", "In-Reply-To": "<20260331171612.102018-1-o.rempel@pengutronix.de>", "References": "<20260331171612.102018-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 v9:\n- Fix null irq_domain dereference from debugfs race by initializing IRQ domain\n early before regmap initialization.\n- Refactor mc33978_handle_fault_condition() to improve readability by keeping\n variable declarations at the top and adding inline comments.\n- Fix spurious transient fault events caused by redundant STAT_FAULT flags\n during event loop.\n- Fix spurious interrupt loops by explicitly returning -ENODATA in\n mc33978_rx_decode() for registers without status bits.\n- Validate hwirq bounds in mc33978_irq_domain_alloc() to prevent corruption\n of irq_rise/irq_fall bitmasks by malformed device tree inputs.\n- set DOMAIN_BUS_NEXUS\n- Protect work on teardown\n- remove IRQF_SHARED\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 | 1061 +++++++++++++++++++++++++++++++++++\n include/linux/mfd/mc33978.h | 92 +++\n 4 files changed, 1170 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..c017d295503f\n--- /dev/null\n+++ b/drivers/mfd/mc33978.c\n@@ -0,0 +1,1061 @@\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+\t/* Immutable after initialization (no lock needed) */\n+\tstruct spi_device *spi;\n+\tstruct regmap *map;\n+\tstruct regulator *vddq;\n+\tstruct regulator *vbatp;\n+\tstruct irq_domain *domain;\n+\n+\t/* Pre-built SPI messages (immutable after init) */\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+\t/* Protected by event_lock */\n+\tstruct mutex event_lock;\n+\tu32 cached_pin_state;\t\t/* Previous pin state for edge detection */\n+\n+\t/* Protected by irq_lock */\n+\tstruct mutex irq_lock;\n+\tu32 cached_pin_mask;\t\t/* IRQ mask for 22 pins */\n+\tu32 irq_rise;\t\t\t/* Rising edge IRQ enable mask */\n+\tu32 irq_fall;\t\t\t/* Falling edge IRQ enable mask */\n+\n+\t/* Protected by teardown_lock */\n+\tspinlock_t teardown_lock;\n+\tbool tearing_down;\t\t/* Prevents work scheduling during teardown */\n+\n+\t/* Atomic operations (no lock needed) */\n+\tatomic_t harvested_flags;\t/* Status bits from SPI responses */\n+\n+\t/*\n+\t * Cross-context lockless access (READ_ONCE/WRITE_ONCE).\n+\t * Accessed from regmap callbacks (unpredictable context) and event work.\n+\t * Cannot use lock in regmap callback. Benign race acceptable.\n+\t */\n+\tbool bus_fault_active;\t\t/* Latest physical fault state on bus */\n+\tbool cached_fault_active;\t/* Cached fault state from previous event */\n+\n+\t/*\n+\t * Work scheduling protected by teardown_lock.\n+\t * Work execution serialized by workqueue subsystem.\n+\t */\n+\tstruct work_struct event_work;\n+\n+\t/*\n+\t * DMA buffers protected by SPI subsystem + regmap serialization.\n+\t * Modified before spi_sync(), read after it returns.\n+\t * Must be at end for ____cacheline_aligned.\n+\t */\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\tmc->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\tmc->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 = 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+\t/*\n+\t * No locking needed here - irq_bus_lock/irq_bus_sync_unlock\n+\t * already provide serialization via mc->irq_lock mutex.\n+\t */\n+\n+\tif (type & IRQ_TYPE_EDGE_RISING)\n+\t\tmc->irq_rise |= mask;\n+\telse\n+\t\tmc->irq_rise &= ~mask;\n+\n+\tif (type & IRQ_TYPE_EDGE_FALLING)\n+\t\tmc->irq_fall |= mask;\n+\telse\n+\t\tmc->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+\tif (hwirq >= MC33978_HWIRQ_FAULT + 1 ||\n+\t nr_irqs > MC33978_HWIRQ_FAULT + 1 - hwirq)\n+\t\treturn -EINVAL;\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, pin_mask;\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+\n+\tscoped_guard(mutex, &mc->irq_lock) {\n+\t\tpin_mask = mc->cached_pin_mask;\n+\t\trise = mc->irq_rise;\n+\t\tfall = mc->irq_fall;\n+\t}\n+\n+\tchanged_pins &= pin_mask;\n+\n+\tif (!changed_pins)\n+\t\treturn false;\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,\n+\t\t\t\t\t u8 hw_flags)\n+{\n+\tbool fault_active, cached_fault, transient, changed;\n+\tbool handled = false;\n+\tu32 rise, fall;\n+\tint virq;\n+\n+\t/* Read the absolute latest physical state seen on the bus */\n+\tfault_active = READ_ONCE(mc->bus_fault_active);\n+\n+\t/* Read the cached fault state from the previous event loop */\n+\tcached_fault = READ_ONCE(mc->cached_fault_active);\n+\n+\t/* Check if the fault state has changed since the last event loop */\n+\tchanged = fault_active ^ cached_fault;\n+\tif (changed)\n+\t\tWRITE_ONCE(mc->cached_fault_active, fault_active);\n+\n+\t/*\n+\t * A transient fault is a pulse that was caught by the clear-on-read\n+\t * status flags, but is no longer physically active on the bus.\n+\t */\n+\ttransient = !changed && !fault_active &&\n+\t\t (hw_flags & MC33978_HI_BYTE_STAT_FAULT);\n+\n+\tif (!changed && !transient)\n+\t\treturn false;\n+\n+\tscoped_guard(mutex, &mc->irq_lock) {\n+\t\trise = mc->irq_rise;\n+\t\tfall = mc->irq_fall;\n+\t}\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 if ((fault_active && (rise & BIT(MC33978_HWIRQ_FAULT))) ||\n+\t\t (!fault_active && (fall & BIT(MC33978_HWIRQ_FAULT)))) {\n+\t\t/* Normal edge */\n+\t\thandle_nested_irq(virq);\n+\t\thandled = true;\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\n+\t * 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\n+\t * switches) makes this 4-byte transfer relatively costly, it is\n+\t * mathematically necessary to guarantee no edge events are permanently\n+\t * lost when a concurrent regmap access races with the IRQ thread, due\n+\t * to the hardware's 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+\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\tif (mc33978_process_single_event(mc))\n+\t\t\thandled = true;\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_teardown(void *data)\n+{\n+\tstruct mc33978_mfd_priv *mc = data;\n+\n+\t/*\n+\t * During the devres LIFO teardown window, the workqueue is canceled\n+\t * before the regmap is destroyed. A concurrent debugfs regmap read\n+\t * can trigger mc33978_harvest_status() and wrongly reschedule the\n+\t * workqueue after it was already canceled.\n+\t *\n+\t * Flag the teardown state under a lock so the harvester atomically\n+\t * checks and ignores status bits before scheduling new work.\n+\t */\n+\tscoped_guard(spinlock_irqsave, &mc->teardown_lock) {\n+\t\tmc->tearing_down = true;\n+\t}\n+\n+\tcancel_work_sync(&mc->event_work);\n+}\n+\n+static int mc33978_irq_init(struct mc33978_mfd_priv *mc,\n+\t\t\t struct fwnode_handle *fwnode)\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_create_linear(fwnode, 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+\t/*\n+\t * Use DOMAIN_BUS_NEXUS to distinguish this intermediate demux domain\n+\t * from child domains sharing the same fwnode. Matches the pattern used\n+\t * by other MFD drivers (e.g., crystalcove).\n+\t */\n+\tirq_domain_update_bus_token(mc->domain, DOMAIN_BUS_NEXUS);\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+\treturn 0;\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, int 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/*\n+\t * If the bus state changed from what the IRQ thread last evaluated,\n+\t * wake it up.\n+\t */\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 checking teardown state */\n+\tsmp_mb__after_atomic();\n+\n+\tscoped_guard(spinlock_irqsave, &mc->teardown_lock) {\n+\t\tif (!mc->tearing_down && atomic_read(&mc->harvested_flags))\n+\t\t\tschedule_work(&mc->event_work);\n+\t}\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+ * Note: MC33978_REG_CHECK, MC33978_REG_WET_SP, and MC33978_REG_WET_SG0 do not\n+ * contain fault status or interrupt flags.\n+ *\n+ * Return: Status bits if present, negative error code otherwise.\n+ */\n+static int 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\tmemcpy(val_buf, &rx_frame[MC33978_FRAME_DATA_HI],\n+\t\t MC33978_PAYLOAD_LEN);\n+\n+\t\tif (has_status)\n+\t\t\tval_buf[MC33978_PAYLOAD_HI] &= MC33978_HI_BYTE_DATA_MASK;\n+\t}\n+\n+\treturn has_status ? status : -ENODATA;\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+\tint 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+\tif (status >= 0)\n+\t\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+\tint 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+\tif (status_req >= 0)\n+\t\tmc33978_harvest_status(mc, status_req);\n+\tif (status_pipe >= 0)\n+\t\tmc33978_harvest_status(mc, 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+ * Note: MC33978_REG_CHECK does not contain fault status or interrupt flags.\n+ * See mc33978_rx_decode() for details.\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 fwnode_handle *fwnode;\n+\tstruct mc33978_mfd_priv *mc;\n+\tint ret;\n+\n+\tfwnode = dev_fwnode(dev);\n+\tif (!fwnode)\n+\t\treturn dev_err_probe(dev, -ENODEV, \"missing firmware node\\n\");\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+\tspin_lock_init(&mc->teardown_lock);\n+\n+\tINIT_WORK(&mc->event_work, mc33978_event_work);\n+\n+\tatomic_set(&mc->harvested_flags, 0);\n+\n+\tmc33978_prepare_messages(mc);\n+\n+\tret = mc33978_irq_init(mc, fwnode);\n+\tif (ret)\n+\t\treturn ret;\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+\t/*\n+\t * Ensure event_work is canceled before regmap and irq_domain teardown,\n+\t * since the worker dereferences both mc->map and mc->domain.\n+\t */\n+\tret = devm_add_action_or_reset(dev, mc33978_teardown, mc);\n+\tif (ret)\n+\t\treturn ret;\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/* 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+\tscoped_guard(mutex, &mc->event_lock) {\n+\t\tret = regmap_read(mc->map, MC33978_REG_READ_IN,\n+\t\t\t\t &mc->cached_pin_state);\n+\t}\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+\t/*\n+\t * Deliberately not using IRQF_SHARED.\n+\t *\n+\t * MC33978 clear-on-read interrupt status can make shared wiring with\n+\t * another MC33978/MC34978 functionally possible, but this handler runs\n+\t * threaded with IRQF_ONESHOT and may hold the line masked for a long\n+\t * time on slow SPI. The added latency/jitter makes shared operation\n+\t * impractical.\n+\t */\n+\tret = devm_request_threaded_irq(dev, mc->spi->irq,\n+\t\t\t\t\tNULL,\n+\t\t\t\t\tmc33978_irq_thread,\n+\t\t\t\t\tIRQF_ONESHOT,\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": [ "v9", "2/6" ] }