Devicetree
 help / color / mirror / Atom feed
From: Oleksij Rempel <o.rempel@pengutronix.de>
To: Guenter Roeck <linux@roeck-us.net>, Lee Jones <lee@kernel.org>,
	Linus Walleij <linusw@kernel.org>, Rob Herring <robh@kernel.org>,
	Krzysztof Kozlowski <krzk+dt@kernel.org>,
	Conor Dooley <conor+dt@kernel.org>, Peter Rosin <peda@axentia.se>
Cc: Oleksij Rempel <o.rempel@pengutronix.de>,
	kernel@pengutronix.de, linux-kernel@vger.kernel.org,
	devicetree@vger.kernel.org, linux-hwmon@vger.kernel.org,
	linux-gpio@vger.kernel.org, David Jander <david@protonic.nl>
Subject: [PATCH v15 2/6] platform: misc: add NXP MC33978/MC34978 core driver
Date: Fri, 10 Jul 2026 12:13:51 +0200	[thread overview]
Message-ID: <20260710101358.2606941-3-o.rempel@pengutronix.de> (raw)
In-Reply-To: <20260710101358.2606941-1-o.rempel@pengutronix.de>

Add the core support module for the NXP MC33978 and MC34978 Multiple
Switch Detection Interfaces (MSDI).

The MC33978/MC34978 devices provide 22 switch detection inputs, analog
multiplexing (AMUX), and comprehensive hardware fault detection.

This core module handles:
- SPI communications via a custom regmap bus to support the device's
  pipelined two-frame MISO response requirement.
- Interrupt demultiplexing, utilizing an irq_domain to provide 22 virtual
  IRQs for switch state changes and 1 virtual IRQ for hardware faults.
- Inline status harvesting from the SPI MSB to detect and trigger events
  without requiring dedicated status register polling.

It exports mc33978_core_init(), called by the MFD driver added in the
following patch. CONFIG_MC33978_CORE carries no prompt and is selected
by CONFIG_MFD_MC33978, so this patch alone builds nothing new.

Note: The device currently lacks suspend/resume power management
callbacks. If the system enters a sleep state cutting power to
VDDQ/VBATP, the device will wake up in POR state with hardware interrupt
masks reset. Power management support is intentionally deferred for now.

Signed-off-by: Oleksij Rempel <o.rempel@pengutronix.de>
---
changes v15:
- Split out of the MFD patch, as requested by Lee Jones. The register
  definitions in include/linux/mfd/mc33978.h are carried here rather than
  with the MFD driver because this module includes them, keeping every
  commit individually buildable.
changes v14:
- Fix IRQ domain bounds check: hwirq_max is exclusive, not inclusive
- Add explicit CS inactive delay (1µs) for pipelined SPI frames
- Clear harvested_flags before enabling work scheduling
- Remove irq_set_wake callback: causes lock ordering violations with parent
  IRQ
- Add SPI command echo validation to detect bus faults
- Add time-bounded event processing (10ms bursts) to prevent soft lockups on
  high-frequency signals
- Fix fault IRQ masking: sync cached_fault_active even when IRQ disabled to
  prevent infinite harvesting
- Optimize harvesting lock scope: move atomic ops outside spinlock where safe
- Fix comment: MC34978 temperature range is -40°C to +105°C (not +150°C)
- Add power-on reset delay (450µs) after VBATP enable per datasheet
  t_ACTIVE spec
- Change platform/Makefile: unconditionally compile misc/
  (obj-y instead of CONFIG_MC33978_CORE guard)
- fix pin/irq convertion from ligical to physical state
changes v13:
- Move cached_pin_state protection from event_lock to irq_state_lock
- Gate FAULT_STAT harvesting: only collect when !cached_fault_active
- Change dummy SPI command from REG_CHECK to REG_READ_IN
changes v12:
- Split monolithic MFD driver into two modules per maintainer feedback:
  * drivers/mfd/mc33978.c: Thin wrapper for SPI binding, power sequencing,
    and child device registration
  * drivers/platform/misc/mc33978-core.c: Complex core functionality
    including custom regmap bus, IRQ domain, and event handling
- Add mc33978_core_init() API exported by core module for MFD initialization
- Introduce CONFIG_MC33978_CORE in drivers/platform/misc/Kconfig
- Update CONFIG_MFD_MC33978 to select CONFIG_MC33978_CORE
- Add MODULE_SOFTDEP("pre: mc33978-core") to ensure correct module load order
- Rename struct mc33978_mfd_priv to context-specific names:
  * mc33978_ddata in MFD driver
  * mc33978_core_data in core module
- Rename local variables from 'mc' to 'ddata'/'cdata' for clarity
- Replace MC33978_DRV_NAME macro with string literal "mc33978"
- Convert kerneldoc (/** */) on static functions to regular comments (/* */)
- Adopt 100-character line limit throughout
- Fix misleading cacheline alignment comment ("must be at end" -> "prevent
  DMA corruption from adjacent fields")
- Document IRQ locking strategy explicitly (irq_lock mutex for regmap/SPI,
  irq_state_lock raw spinlock for atomic state access)
- Reformat header file comments from inline style to preceding style for
  improved readability
- Enhance interrupt behavior documentation with detailed datasheet references
  explaining INT_flg clear timing and FAULT_STAT latching behavior
- add .suppress_bind_attrs = true
changes v11:
- Introduce synthetic wakeup bit to prevent hardware bit overloading and
  avoid phantom transient faults.
- Add smp_mb__before_atomic() to ensure memory visibility before
  updating harvested_flags.
- Update event processing to mask synthetic bits before evaluating
  hardware fault conditions.
- Introduce fault_lock spinlock to protect fault state members.
- Replace manual memory barriers and READ_ONCE/WRITE_ONCE with
  spinlock-protected access.
- Introduce irq_state_lock raw spinlock to protect IRQ state fields.
- Replace irq_lock mutex with raw spinlock in IRQ callbacks and event
  handling paths.
- Include MC33978_REG_ENTER_LPM in volatile and precious ranges.
- Mark MC33978_REG_ENTER_LPM as readable to avoid regcache sync writes.
changes v10:
- Refactor IRQ setup into a common helper to unify the .map and .alloc
  code paths.
- Enable hierarchical IRQ support to allow integration with the
  pinctrl/GPIO child domain.
- Simplify event handling by removing redundant boolean return values
  from internal handlers.
- Fix IRQ cleanup by disposing of all active mappings before removing
  the IRQ domain.
- Standardize IRQ constants using MC33978_NUM_IRQS for consistent domain
  sizing and bounds checks.
changes v9:
- Fix null irq_domain dereference from debugfs race by initializing IRQ domain
  early before regmap initialization.
- Refactor mc33978_handle_fault_condition() to improve readability by keeping
  variable declarations at the top and adding inline comments.
- Fix spurious transient fault events caused by redundant STAT_FAULT flags
  during event loop.
- Fix spurious interrupt loops by explicitly returning -ENODATA in
  mc33978_rx_decode() for registers without status bits.
- Validate hwirq bounds in mc33978_irq_domain_alloc() to prevent corruption
  of irq_rise/irq_fall bitmasks by malformed device tree inputs.
- set DOMAIN_BUS_NEXUS
- Protect work on teardown
- remove IRQF_SHARED
changes v8:
- Fix TOCTOU race condition in SPI event harvesting loop by grabbing
  harvested_flags before hardware reads.
- Fix broken hierarchical IRQ allocation by replacing
  irq_domain_set_hwirq_and_chip() with irq_domain_set_info() and passing
  the handle_simple_irq flow handler.
- Fix out-of-bounds stack read and endianness bug in for_each_set_bit() by
  typing fired_pins as unsigned long instead of casting u32.
- Prevent DMA cacheline corruption by explicitly aligning rx_frame with
  ____cacheline_aligned to separate it from tx_frame.
- Prevent spurious IRQs by verifying irq_find_mapping() returns non-zero
  before calling handle_nested_irq().
- Prevent missed transient hardware faults by explicitly evaluating
  hw_flags in mc33978_handle_fault_condition().
- Fix missing memory barrier in mc33978_harvest_status() with
  smp_mb__after_atomic() to ensure harvested_flags visibility.
- Fix devres use-after-free teardown race by using INIT_WORK and a custom
  cancel action after the IRQ domain is destroyed, instead of
  devm_work_autocancel.
- Prevent spurious pin interrupts on boot by priming cached_pin_state via
  a regmap_read() during probe before enabling IRQs.
- Implement .irq_set_wake callback to support system wake from
  hardware faults and switch state changes.
changes v7:
- Fix event handling race condition with smp_mb()
- Replace INIT_WORK() with devm_work_autocancel()
changes v6:
- Remove the hardcoded bypass in irq_set_type to allow child drivers to
  configure the FAULT line for edge-triggering.
- Implement software edge-detection for FAULT interrupt.
- Add MC33978_FAULT_ALARM_MASK to the shared header for child devices
- Use READ_ONCE() and WRITE_ONCE() for lockless shared state variables
  (cached_pin_mask, irq_rise, irq_fall, bus_fault_active,
  cached_fault_active) accessed across the SPI harvesting context and
  the event worker.
- Add an if (hwirq < MC33978_NUM_PINS) guard in irq_mask() and
  irq_unmask() to prevent the FAULT hwirq (22) from altering the
  physical pin mask registers.
- Lowercase the error strings in dev_err_probe()
- Add inline comments explaining the irq_map fallback behavior
changes v5:
- no changes
changes v4:
- Removed .of_compatible strings from the mfd_cell arrays
changes v3:
- Select IRQ_DOMAIN_HIERARCHY in Kconfig
- Add .alloc and .free callbacks to irq_domain_ops to support hierarchical
  IRQ domains
- Set IRQ_DOMAIN_FLAG_HIERARCHY flag on the core MFD irq_domain
- replace manual lock/unlock with guard()
changes v2:
- Rewrite the driver header comment
- Explicitly reject IRQ_TYPE_LEVEL_HIGH and IRQ_TYPE_LEVEL_LOW in
  mc33978_irq_set_type() to correctly reflect the hardware's edge-only
  interrupt capabilities.
- Pass the hardware fault IRQ to the hwmon child driver via mfd_cell
  resources, rather than requiring the child to parse the parent's irq_domain.
- Ensure the Kconfig strictly depends on OF and SPI
---
 drivers/platform/Kconfig             |    2 +
 drivers/platform/Makefile            |    1 +
 drivers/platform/misc/Kconfig        |   10 +
 drivers/platform/misc/Makefile       |    6 +
 drivers/platform/misc/mc33978-core.c | 1285 ++++++++++++++++++++++++++
 include/linux/mfd/mc33978.h          |  196 ++++
 6 files changed, 1500 insertions(+)
 create mode 100644 drivers/platform/misc/Kconfig
 create mode 100644 drivers/platform/misc/Makefile
 create mode 100644 drivers/platform/misc/mc33978-core.c
 create mode 100644 include/linux/mfd/mc33978.h

diff --git a/drivers/platform/Kconfig b/drivers/platform/Kconfig
index 312788f249c9..45e66de36678 100644
--- a/drivers/platform/Kconfig
+++ b/drivers/platform/Kconfig
@@ -11,6 +11,8 @@ source "drivers/platform/cznic/Kconfig"
 
 source "drivers/platform/mellanox/Kconfig"
 
+source "drivers/platform/misc/Kconfig"
+
 source "drivers/platform/olpc/Kconfig"
 
 source "drivers/platform/surface/Kconfig"
diff --git a/drivers/platform/Makefile b/drivers/platform/Makefile
index fa322e7f8716..c247a6826a2d 100644
--- a/drivers/platform/Makefile
+++ b/drivers/platform/Makefile
@@ -6,6 +6,7 @@
 obj-$(CONFIG_X86)		+= x86/
 obj-$(CONFIG_LOONGARCH)		+= loongarch/
 obj-$(CONFIG_MELLANOX_PLATFORM)	+= mellanox/
+obj-y				+= misc/
 obj-$(CONFIG_MIPS)		+= mips/
 obj-$(CONFIG_OLPC_EC)		+= olpc/
 obj-$(CONFIG_GOLDFISH)		+= goldfish/
diff --git a/drivers/platform/misc/Kconfig b/drivers/platform/misc/Kconfig
new file mode 100644
index 000000000000..e5730bfd31e8
--- /dev/null
+++ b/drivers/platform/misc/Kconfig
@@ -0,0 +1,10 @@
+# SPDX-License-Identifier: GPL-2.0-only
+#
+# Platform miscellaneous drivers configuration
+#
+
+config MC33978_CORE
+	tristate
+	depends on SPI
+	select IRQ_DOMAIN_HIERARCHY
+	select REGMAP
diff --git a/drivers/platform/misc/Makefile b/drivers/platform/misc/Makefile
new file mode 100644
index 000000000000..4aa1351a46ad
--- /dev/null
+++ b/drivers/platform/misc/Makefile
@@ -0,0 +1,6 @@
+# SPDX-License-Identifier: GPL-2.0-only
+#
+# Makefile for miscellaneous platform-specific drivers
+#
+
+obj-$(CONFIG_MC33978_CORE)	+= mc33978-core.o
diff --git a/drivers/platform/misc/mc33978-core.c b/drivers/platform/misc/mc33978-core.c
new file mode 100644
index 000000000000..83931e85cedf
--- /dev/null
+++ b/drivers/platform/misc/mc33978-core.c
@@ -0,0 +1,1285 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (C) 2024 David Jander <david@protonic.nl>, Protonic Holland
+ * Copyright (C) 2026 Oleksij Rempel <kernel@pengutronix.de>, Pengutronix
+ *
+ * MC33978/MC34978 - Core functionality module
+ *
+ * This module implements the complex functionality:
+ * - Custom regmap bus (pipelined SPI protocol)
+ * - IRQ chip + IRQ domain (23 virtual IRQs)
+ * - Event processing (pin changes + fault detection)
+ * - Status harvesting from SPI responses
+ *
+ * The MFD driver (mc33978.c) calls mc33978_core_init() to initialize
+ * this functionality, keeping the MFD driver simple for review.
+ *
+ * Custom SPI Regmap & Event Harvesting:
+ * The device uses a non-standard pipelined SPI protocol where the MISO
+ * response logically lags the MOSI command by one frame. Furthermore, the
+ * hardware embeds volatile global status bits (INT_flg, FAULT_STAT) into the
+ * high byte of almost every SPI response (with specific exceptions handled by
+ * the decoder). This core implements a custom regmap_bus to handle the
+ * 2-frame dummy fetches and transparently "harvests" these status bits in
+ * the background to schedule event processing.
+ *
+ * Interrupt Quirks & Limitations (MC33978 Rev. 10, §9.10.27):
+ * - Clear-on-Read INT_flg: The INT_B pin is cleared 1.0ms after CS_B falling
+ * edge. Any SPI message that returns INT_flg will clear this flag. The
+ * INT_flg bit is set on any interrupt event (pin change, fault). Harvesting
+ * INT_flg from all SPI traffic is the ONLY way to know this device triggered
+ * an interrupt (crucial for shared IRQ lines where another driver's regmap
+ * access would silently clear the flag).
+ * - FAULT_STAT Latching (§9.10.27): FAULT_STAT is a sticky summary bit that
+ * latches HIGH when any fault occurs and remains HIGH in all subsequent SPI
+ * responses until the Fault register (0x42) is read. Reading Fault register
+ * clears the latch; hardware immediately re-latches if fault condition still
+ * present. INT_flg clears on any SPI read regardless of persistence. This
+ * allows transient detection: FAULT_STAT can appear in one SPI frame (latched
+ * evidence) but be absent in the next (condition cleared before register read),
+ * proving a transient fault occurred.
+ * - Stateless Pin Edge Detection: Hardware lacks per-pin interrupt status
+ * registers. READ_IN register (§9.10.27) returns instantaneous switch state
+ * (Logic[1]=closed, Logic[0]=open). The driver XORs current state against
+ * cached state to derive which pins changed.
+ * - Missed Short Pulses: Pin interrupts are state-sampled, not edge-latched.
+ * Physical pulses shorter than SPI read latency (~1-2ms) will be missed if
+ * the pin reverts before READ_IN sampling. Inherent hardware limitation.
+ * - Edge-Only Pin Interrupts: Hardware asserts INT_B only on state changes,
+ * never continuously for sustained levels. Driver emulates edge interrupts
+ * (RISING/FALLING) and rejects LEVEL configurations to match hardware.
+ */
+
+#include <linux/array_size.h>
+#include <linux/atomic.h>
+#include <linux/bitfield.h>
+#include <linux/bits.h>
+#include <linux/cache.h>
+#include <linux/cleanup.h>
+#include <linux/device.h>
+#include <linux/dma-mapping.h>
+#include <linux/interrupt.h>
+#include <linux/irq.h>
+#include <linux/irqdomain.h>
+#include <linux/jiffies.h>
+#include <linux/module.h>
+#include <linux/property.h>
+#include <linux/regmap.h>
+#include <linux/spi/spi.h>
+#include <linux/string.h>
+
+#include <linux/mfd/mc33978.h>
+
+/* Device identification signature returned by CHECK register */
+#define MC33978_CHECK_SIGNATURE		0x123456
+
+/*
+ * Pipelined two-frame SPI transfer:
+ * [REQ]  - Transmits command/write-data, receives dummy/previous response
+ * [PIPE] - Transmits dummy READ_IN, receives actual response to current command
+ */
+enum mc33978_frame_index {
+	MC33978_FRAME_REQ = 0,
+	MC33978_FRAME_PIPE,
+	MC33978_FRAME_COUNT
+};
+
+/* SPI frame byte offsets (transmitted MSB first) */
+enum mc33978_frame_offset {
+	MC33978_FRAME_CMD = 0,
+	MC33978_FRAME_DATA_HI,
+	MC33978_FRAME_DATA_MID,
+	MC33978_FRAME_DATA_LO
+};
+
+#define MC33978_FRAME_LEN		4
+
+/* Regmap internal value buffer offsets */
+enum mc33978_payload_offset {
+	MC33978_PAYLOAD_HI = 0,
+	MC33978_PAYLOAD_MID,
+	MC33978_PAYLOAD_LO
+};
+
+#define MC33978_PAYLOAD_LEN		3
+
+/*
+ * SPI Command Byte (FRAME_CMD).
+ * Maps to frame bit [24] in the datasheet.
+ */
+#define MC33978_CMD_BYTE_WRITE		BIT(0)
+
+/*
+ * Sentinel for skipping command echo validation. Uses 0xFF (invalid register
+ * address - all valid registers are even).
+ */
+#define MC33978_CMD_INVALID		0xFF
+
+/* High Payload Byte Masks (FRAME_DATA_HI / PAYLOAD_HI). */
+/* Maps to frame bit [23] */
+#define MC33978_HI_BYTE_STAT_FAULT     BIT(7)
+/* Maps to frame bit [22] */
+#define MC33978_HI_BYTE_STAT_INT       BIT(6)
+
+#define MC33978_HI_BYTE_STATUS_MASK    (MC33978_HI_BYTE_STAT_FAULT | \
+					MC33978_HI_BYTE_STAT_INT)
+
+/* Synthetic wakeup bit for harvested flags */
+#define MC33978_HARVEST_WAKE_BIT	BIT(8)
+
+/* Maps to frame bits [21:16] */
+#define MC33978_HI_BYTE_DATA_MASK	GENMASK(5, 0)
+
+struct mc33978_core_data {
+	struct spi_device *spi;
+	struct regmap *map;
+	struct irq_domain *domain;
+
+	/* Pre-built SPI messages (immutable after init) */
+	struct spi_message msg_read;
+	struct spi_message msg_write;
+	struct spi_transfer xfer_read[MC33978_FRAME_COUNT];
+	struct spi_transfer xfer_write;
+
+	/* Serializes event processing loop (prevents concurrent handle_events) */
+	struct mutex event_lock;
+
+	/*
+	 * IRQ subsystem locking:
+	 * - irq_lock: Mutex for sleeping operations (regmap/SPI access)
+	 * - irq_state_lock: Raw spinlock for atomic state access
+	 *
+	 * Protected by irq_state_lock:
+	 */
+	struct mutex irq_lock;
+	raw_spinlock_t irq_state_lock;
+	/* Previous pin state for edge detection (protected by irq_state_lock) */
+	u32 cached_pin_state;
+	/* IRQ mask for 23 IRQs: bits 0-21 for pins, bit 22 for fault */
+	u32 cached_pin_mask;
+	/* Rising edge IRQ enable mask (23 IRQs) */
+	u32 irq_rise;
+	/* Falling edge IRQ enable mask (23 IRQs) */
+	u32 irq_fall;
+	/* Pins that were unmasked since last sync */
+	u32 unmasked_since_sync;
+
+	/* Protected by state_lock */
+	spinlock_t state_lock;
+	/* Prevents work scheduling during teardown */
+	bool tearing_down;
+	/* Prevents work scheduling before IRQ handler ready */
+	bool irq_ready;
+	/* Latest physical fault state on bus */
+	bool bus_fault_active;
+	/* Cached fault state from previous event */
+	bool cached_fault_active;
+
+	/* Atomic operations (no lock needed) */
+	/* Status bits from SPI responses */
+	atomic_t harvested_flags;
+
+	/*
+	 * Work scheduling protected by state_lock.
+	 * Work execution serialized by workqueue subsystem.
+	 */
+	struct work_struct event_work;
+
+	/*
+	 * DMA-safe buffers for SPI transfers. Must be kept at end of struct.
+	 * devm_kzalloc() returns memory aligned to ARCH_DMA_MINALIGN, and
+	 * __aligned(ARCH_DMA_MINALIGN) prevents false sharing with preceding fields
+	 * as well as false sharing between independent SPI transfers.
+	 */
+	u8 tx_req[MC33978_FRAME_LEN] __aligned(ARCH_DMA_MINALIGN);
+	u8 rx_dummy[MC33978_FRAME_LEN] __aligned(ARCH_DMA_MINALIGN);
+	u8 tx_pipe[MC33978_FRAME_LEN] __aligned(ARCH_DMA_MINALIGN);
+	u8 rx_resp[MC33978_FRAME_LEN] __aligned(ARCH_DMA_MINALIGN);
+};
+
+static void mc33978_irq_mask(struct irq_data *data)
+{
+	struct mc33978_core_data *cdata = irq_data_get_irq_chip_data(data);
+	irq_hw_number_t hwirq = irqd_to_hwirq(data);
+
+	if (hwirq < MC33978_NUM_IRQS) {
+		scoped_guard(raw_spinlock_irqsave, &cdata->irq_state_lock) {
+			cdata->cached_pin_mask &= ~BIT(hwirq);
+			cdata->unmasked_since_sync &= ~BIT(hwirq);
+		}
+	}
+}
+
+static void mc33978_irq_unmask(struct irq_data *data)
+{
+	struct mc33978_core_data *cdata = irq_data_get_irq_chip_data(data);
+	irq_hw_number_t hwirq = irqd_to_hwirq(data);
+
+	if (hwirq < MC33978_NUM_IRQS) {
+		scoped_guard(raw_spinlock_irqsave, &cdata->irq_state_lock) {
+			if (!(cdata->cached_pin_mask & BIT(hwirq))) {
+				/*
+				 * Don't add to cached_pin_mask yet to avoid race:
+				 * If event worker runs before bus_sync_unlock(),
+				 * it would see pin in mask but stale cached_pin_state,
+				 * causing spurious interrupt. Let bus_sync_unlock()
+				 * atomically update both mask and state.
+				 */
+				cdata->unmasked_since_sync |= BIT(hwirq);
+			}
+		}
+	}
+}
+
+static void mc33978_irq_bus_lock(struct irq_data *data)
+{
+	struct mc33978_core_data *cdata = irq_data_get_irq_chip_data(data);
+
+	mutex_lock(&cdata->irq_lock);
+}
+
+/* Sync cached IRQ mask to hardware IE_SG/IE_SP registers, then unlock */
+static void mc33978_irq_bus_sync_unlock(struct irq_data *data)
+{
+	struct mc33978_core_data *cdata = irq_data_get_irq_chip_data(data);
+	u32 sg_mask, sp_mask, cached_mask, unmasked;
+	int ret;
+
+	scoped_guard(raw_spinlock_irqsave, &cdata->irq_state_lock) {
+		cached_mask = cdata->cached_pin_mask;
+		unmasked = cdata->unmasked_since_sync;
+		cdata->unmasked_since_sync = 0;
+	}
+
+	if (unmasked) {
+		u32 val;
+
+		ret = regmap_read(cdata->map, MC33978_REG_READ_IN, &val);
+		if (!ret) {
+			scoped_guard(raw_spinlock_irqsave, &cdata->irq_state_lock) {
+				/*
+				 * Atomically update BOTH cached_pin_state and cached_pin_mask
+				 * to prevent race: if event worker runs after this point, it
+				 * will see consistent state (both mask and baseline updated).
+				 */
+				cdata->cached_pin_state = (cdata->cached_pin_state & ~unmasked) |
+							  (val & unmasked);
+				cdata->cached_pin_mask |= unmasked;
+			}
+		} else {
+			dev_err(&cdata->spi->dev, "failed to read pin state for sync: %d\n", ret);
+			scoped_guard(raw_spinlock_irqsave, &cdata->irq_state_lock) {
+				cdata->cached_pin_mask |= unmasked;
+			}
+		}
+		cached_mask |= unmasked;
+	}
+
+	/*
+	 * Split the cached 22-bit pin mask into hardware register format:
+	 * - SG pins: bits [13:0] (14 pins, mask 0x3FFF)
+	 * - SP pins: bits [21:14] (8 pins, mask 0xFF)
+	 */
+	sg_mask = FIELD_GET(MC33978_SG_PIN_MASK, cached_mask);
+	sp_mask = FIELD_GET(MC33978_SP_PIN_MASK, cached_mask);
+
+	ret = regmap_update_bits(cdata->map, MC33978_REG_IE_SG,
+				 MC33978_IE_SG_MASK, sg_mask);
+	if (ret)
+		dev_err(&cdata->spi->dev, "failed to sync SG IRQ mask: %d\n", ret);
+
+	ret = regmap_update_bits(cdata->map, MC33978_REG_IE_SP,
+				 MC33978_IE_SP_MASK, sp_mask);
+	if (ret)
+		dev_err(&cdata->spi->dev, "failed to sync SP IRQ mask: %d\n", ret);
+
+	mutex_unlock(&cdata->irq_lock);
+}
+
+static int mc33978_irq_set_type(struct irq_data *data, unsigned int type)
+{
+	struct mc33978_core_data *cdata = irq_data_get_irq_chip_data(data);
+	irq_hw_number_t hwirq = irqd_to_hwirq(data);
+	u32 mask;
+
+	if (hwirq >= MC33978_NUM_IRQS)
+		return -EINVAL;
+
+	/*
+	 * Safe to use BIT(hwirq) with u32: we support 23 IRQs (0-22),
+	 * validated by check above. Using u32 for consistency with
+	 * irq_rise/irq_fall/cached_pin_mask fields.
+	 */
+	mask = BIT(hwirq);
+
+	if (type == IRQ_TYPE_NONE)
+		return -EINVAL;
+
+	if (type & (IRQ_TYPE_LEVEL_HIGH | IRQ_TYPE_LEVEL_LOW))
+		return -EINVAL;
+
+	scoped_guard(raw_spinlock_irqsave, &cdata->irq_state_lock) {
+		if (type & IRQ_TYPE_EDGE_RISING)
+			cdata->irq_rise |= mask;
+		else
+			cdata->irq_rise &= ~mask;
+
+		if (type & IRQ_TYPE_EDGE_FALLING)
+			cdata->irq_fall |= mask;
+		else
+			cdata->irq_fall &= ~mask;
+	}
+
+	return 0;
+}
+
+/*
+ * Wake support not implemented: calling irq_set_irq_wake() on the parent SPI
+ * IRQ from this callback causes lock ordering violations (genirq holds
+ * desc->lock while the parent's buslock may sleep). Configure wake on the SPI
+ * controller's interrupt line instead.
+ */
+
+static struct irq_chip mc33978_irq_chip = {
+	.name			= "mc33978",
+	.irq_mask		= mc33978_irq_mask,
+	.irq_unmask		= mc33978_irq_unmask,
+	.irq_bus_lock		= mc33978_irq_bus_lock,
+	.irq_bus_sync_unlock	= mc33978_irq_bus_sync_unlock,
+	.irq_set_type		= mc33978_irq_set_type,
+};
+
+static void mc33978_irq_setup(struct irq_domain *domain, unsigned int virq,
+			      irq_hw_number_t hwirq)
+{
+	struct mc33978_core_data *cdata = domain->host_data;
+
+	irq_domain_set_info(domain, virq, hwirq, &mc33978_irq_chip, cdata,
+			    handle_simple_irq, NULL, NULL);
+	irq_set_nested_thread(virq, 1);
+	irq_clear_status_flags(virq, IRQ_NOREQUEST | IRQ_NOPROBE);
+}
+
+static int mc33978_irq_map(struct irq_domain *d, unsigned int virq,
+			   irq_hw_number_t hw)
+{
+	mc33978_irq_setup(d, virq, hw);
+	return 0;
+}
+
+static int mc33978_irq_domain_alloc(struct irq_domain *domain,
+				    unsigned int virq,
+				    unsigned int nr_irqs, void *arg)
+{
+	struct irq_fwspec *fwspec = arg;
+	irq_hw_number_t hwirq;
+	int i;
+
+	/*
+	 * MFD child devices (e.g. hwmon) are mapped via mfd_add_devices() ->
+	 * irq_create_mapping(), which strictly uses the .map callback
+	 * (mc33978_irq_map) and bypasses .alloc completely.
+	 * This .alloc callback is exclusively invoked by the hierarchical GPIO
+	 * irqchip in the pinctrl driver, which guarantees a valid fwspec in arg.
+	 */
+	if (!fwspec || fwspec->param_count < 1)
+		return -EINVAL;
+
+	hwirq = fwspec->param[0];
+
+	if (hwirq >= MC33978_NUM_IRQS ||
+	    nr_irqs > MC33978_NUM_IRQS - hwirq)
+		return -EINVAL;
+
+	for (i = 0; i < nr_irqs; i++)
+		mc33978_irq_setup(domain, virq + i, hwirq + i);
+
+	return 0;
+}
+
+static void mc33978_irq_domain_free(struct irq_domain *domain,
+				    unsigned int virq,
+				    unsigned int nr_irqs)
+{
+	int i;
+
+	for (i = 0; i < nr_irqs; i++)
+		irq_domain_reset_irq_data(irq_domain_get_irq_data(domain,
+								  virq + i));
+}
+
+/*
+ * Dual-mode IRQ domain: supports both direct MFD child (hwmon via .map)
+ * and hierarchical child (pinctrl GPIO IRQ chip via .alloc). The .xlate
+ * translates DT 2-cell format (hwirq 0-22, type flags).
+ * IRQ_DOMAIN_FLAG_HIERARCHY enables the pinctrl hierarchical chain.
+ */
+static const struct irq_domain_ops mc33978_irq_domain_ops = {
+	.map	= mc33978_irq_map,
+	.alloc	= mc33978_irq_domain_alloc,
+	.free	= mc33978_irq_domain_free,
+	.xlate	= irq_domain_xlate_twocell,
+};
+
+static void mc33978_irq_domain_remove(void *data)
+{
+	struct mc33978_core_data *cdata = data;
+	struct irq_domain *domain = cdata->domain;
+	int hwirq;
+
+	/*
+	 * Must manually dispose mappings before irq_domain_remove().
+	 * Child platform_get_irq() creates mappings via irq_create_of_mapping().
+	 * devm_request_irq() only calls free_irq(), not irq_dispose_mapping().
+	 * irq_domain_remove() expects empty radix tree (has WARN_ON check).
+	 */
+	for (hwirq = 0; hwirq < MC33978_NUM_IRQS; hwirq++) {
+		unsigned int virq;
+
+		virq = irq_find_mapping(domain, hwirq);
+		if (virq)
+			irq_dispose_mapping(virq);
+	}
+
+	irq_domain_remove(domain);
+}
+
+static u32 mc33978_compute_pin_events(u32 old_state, u32 new_state, u32 config_reg,
+				      u32 mask, u32 rise, u32 fall)
+{
+	u32 changed = (old_state ^ new_state) & mask;
+	u32 fired = 0;
+	u32 physical_new;
+
+	if (!changed)
+		return 0;
+
+	physical_new = mc33978_raw_to_physical_state(new_state, config_reg);
+
+	fired |= (changed & physical_new) & rise;
+	fired |= (changed & ~physical_new) & fall;
+
+	return fired;
+}
+
+static unsigned long mc33978_handle_pin_changes(struct mc33978_core_data *cdata,
+					       unsigned int pin_state,
+					       unsigned int config_reg)
+{
+	unsigned long fired_pins = 0;
+
+	scoped_guard(raw_spinlock_irqsave, &cdata->irq_state_lock) {
+		fired_pins = mc33978_compute_pin_events(cdata->cached_pin_state,
+							pin_state,
+							config_reg,
+							cdata->cached_pin_mask,
+							cdata->irq_rise,
+							cdata->irq_fall);
+		cdata->cached_pin_state = pin_state;
+	}
+
+	return fired_pins;
+}
+
+/*
+ * Computes whether the fault interrupt should fire based on hardware state,
+ * transient evidence, and user IRQ configuration.
+ * Updates the cached fault state when a state change is detected.
+ */
+static bool mc33978_compute_fault_event(bool bus_active, bool *cached_active,
+					unsigned int hw_flags, u32 rise, u32 fall)
+{
+	bool changed = bus_active ^ *cached_active;
+	bool transient;
+
+	/*
+	 * Transient fault detection (§9.10.27): FAULT_STAT latches on fault
+	 * occurrence and persists until Fault register (0x42) read clears it.
+	 * Hwmon driver reads Fault register, which triggers regmap SPI read with
+	 * pipelined frames: REQ frame (old response, FAULT_STAT=1 latched) and
+	 * PIPE frame (Fault register response clears latch). If fault condition
+	 * cleared between latch and read, FAULT_STAT won't re-latch (datasheet:
+	 * "immediately set again if condition still present").
+	 *
+	 * Transient detection: hw_flags contains harvested FAULT_STAT bit
+	 * (latched evidence from REQ frame), but bus_active=0 and
+	 * cached_active=0 (PIPE frame showed cleared, condition gone).
+	 * This proves brief fault (VBATP voltage glitch, threshold oscillation)
+	 * occurred and cleared before Fault register read. Edge-triggered
+	 * consumers need this event notification.
+	 *
+	 * WAKE_BIT exclusion: WAKE_BIT is synthetic (bus_active changes),
+	 * not hardware evidence. Including it causes phantom transients when
+	 * normal faults deassert (WAKE_BIT present but no actual transient).
+	 */
+	transient = !changed && !bus_active && !(*cached_active) &&
+		    (hw_flags & MC33978_HI_BYTE_STAT_FAULT);
+
+	if (changed)
+		*cached_active = bus_active;
+
+	if (!changed && !transient)
+		return false;
+
+	if (transient) {
+		/*
+		 * Transient pulse: both edges occurred. Dispatch once if
+		 * any edge is enabled. Dispatching both edges separately
+		 * would incorrectly report two interrupts for one event.
+		 */
+		return (rise | fall) & BIT(MC33978_HWIRQ_FAULT);
+	}
+
+	/* Normal edge */
+	if ((bus_active && (rise & BIT(MC33978_HWIRQ_FAULT))) ||
+	    (!bus_active && (fall & BIT(MC33978_HWIRQ_FAULT))))
+		return true;
+
+	return false;
+}
+
+/*
+ * Fault Signaling Variants (hwirq 22, consumed by hwmon driver):
+ *
+ * The driver distinguishes three fault event types based on timing and
+ * hardware FAULT_STAT latch behavior:
+ *
+ * 1. Sustained Fault Edges (normal operation):
+ *    - Fault persists through detection cycle
+ *    - Rising edge: fault_active=1, cached_fault=0 -> dispatch if rise enabled
+ *    - Falling edge: fault_active=0, cached_fault=1 -> dispatch if fall enabled
+ *    - Example: Overtemperature condition that lasts seconds
+ *
+ * 2. Transient Fault (brief pulse <1ms):
+ *    - Fault occurs and clears before Fault register (0x42) read
+ *    - Detection: hw_flags contains FAULT_STAT bit (latched evidence from
+ *      pipelined SPI REQ frame), but both fault_active=0 and cached_fault=0
+ *      (PIPE frame showed condition cleared)
+ *    - Dispatch: Single IRQ if ANY edge enabled (represents both edges)
+ *    - Example: VBATP voltage glitch during inductive load switching, or fault
+ *      clears coincidentally between SPI frames (threshold boundary oscillation)
+ *    - Rationale: Without preserving FAULT_STAT bit, these events are invisible
+ *
+ * 3. No Event (filtered):
+ *    - Fault state unchanged (fault_active == cached_fault) AND no transient
+ *      evidence in hw_flags
+ *    - Common during polling: hardware still overtemp, state already reflected
+ *    - No IRQ dispatch (prevents duplicate notifications)
+ *
+ * State Tracking:
+ * - bus_fault_active: Live FAULT_STAT from most recent SPI response
+ * - cached_fault_active: Last fault state dispatched to consumer
+ * - hw_flags: Accumulated FAULT_STAT bits from pipelined SPI (transient evidence)
+ *
+ * Edge Configuration:
+ * - Consumers use irq_set_irq_type(virq, IRQ_TYPE_EDGE_RISING/FALLING/BOTH)
+ * - Level types rejected (hardware limitation: edge-only assertion)
+ * - Masking via disable_irq() prevents dispatch but harvesting continues
+ */
+static bool mc33978_handle_fault_condition(struct mc33978_core_data *cdata,
+					   unsigned int hw_flags)
+{
+	bool bus_fault_active, fire_irq;
+	u32 rise, fall, mask;
+
+	scoped_guard(raw_spinlock_irqsave, &cdata->irq_state_lock) {
+		rise = cdata->irq_rise;
+		fall = cdata->irq_fall;
+		mask = cdata->cached_pin_mask;
+	}
+
+	scoped_guard(spinlock_irqsave, &cdata->state_lock) {
+		bus_fault_active = cdata->bus_fault_active;
+
+		/*
+		 * Always sync cached_fault_active with bus state,
+		 * even when IRQ is masked. This prevents infinite harvesting
+		 * loop when fault occurs while IRQ is disabled.
+		 */
+		if (!(mask & BIT(MC33978_HWIRQ_FAULT))) {
+			cdata->cached_fault_active = bus_fault_active;
+			return false;
+		}
+
+		fire_irq = mc33978_compute_fault_event(bus_fault_active,
+						       &cdata->cached_fault_active,
+						       hw_flags, rise, fall);
+	}
+
+	return fire_irq;
+}
+
+static void mc33978_process_single_event(struct mc33978_core_data *cdata)
+{
+	unsigned int harvested;
+	unsigned int pin_state;
+	unsigned long fired_pins = 0;
+	bool fire_fault = false;
+	int ret;
+	int i, virq;
+
+	/*
+	 * Grab harvested_flags BEFORE hardware read. Flags harvested during
+	 * the read trigger another loop pass. This intentionally forces a
+	 * redundant SPI read on most interrupts, but is necessary to prevent
+	 * lost events when concurrent regmap access races with IRQ thread
+	 * (hardware has clear-on-read INT_flg).
+	 */
+	harvested = atomic_xchg(&cdata->harvested_flags, 0);
+
+	scoped_guard(mutex, &cdata->irq_lock) {
+		unsigned int config_reg = 0;
+
+		ret = regmap_read(cdata->map, MC33978_REG_READ_IN, &pin_state);
+		if (ret) {
+			dev_err_ratelimited(&cdata->spi->dev, "failed to read pin state: %d\n",
+					    ret);
+		} else {
+			if (cdata->cached_pin_mask & MC33978_SP_PIN_MASK) {
+				ret = regmap_read(cdata->map, MC33978_REG_CONFIG, &config_reg);
+				if (ret)
+					dev_err_ratelimited(&cdata->spi->dev, "failed to read config reg: %d\n",
+							    ret);
+			}
+			fired_pins = mc33978_handle_pin_changes(cdata, pin_state, config_reg);
+		}
+	}
+
+	fire_fault = mc33978_handle_fault_condition(cdata, harvested);
+
+	/* Dispatch virtual IRQs for any fired events */
+	for_each_set_bit(i, &fired_pins, MC33978_NUM_PINS) {
+		virq = irq_find_mapping(cdata->domain, i);
+		if (virq)
+			handle_nested_irq(virq);
+	}
+
+	if (fire_fault) {
+		virq = irq_find_mapping(cdata->domain, MC33978_HWIRQ_FAULT);
+		if (virq)
+			handle_nested_irq(virq);
+	}
+}
+
+static void mc33978_handle_events(struct mc33978_core_data *cdata)
+{
+	unsigned long burst_end;
+
+	guard(mutex)(&cdata->event_lock);
+
+	/*
+	 * Process events in time-bounded bursts to prevent CPU monopolization
+	 * on high-frequency signals. Allow 10ms of processing per invocation,
+	 * then reschedule if more events remain. This supports kHz square wave
+	 * measurement while preventing infinite loops during module removal.
+	 *
+	 * 10ms @ 1ms SPI latency = ~10 events/burst, adequate for responsiveness
+	 * while preventing soft lockups. Remaining events trigger rescheduling.
+	 */
+	burst_end = jiffies + msecs_to_jiffies(10);
+
+	do {
+		mc33978_process_single_event(cdata);
+
+		/* Check teardown flag: allows module removal to complete */
+		scoped_guard(spinlock_irqsave, &cdata->state_lock) {
+			if (cdata->tearing_down)
+				return;
+		}
+
+		/* Yield CPU if time slice exhausted and reschedule remaining work */
+		if (time_after(jiffies, burst_end) &&
+		    atomic_read(&cdata->harvested_flags)) {
+			schedule_work(&cdata->event_work);
+			return;
+		}
+	} while (atomic_read(&cdata->harvested_flags) != 0);
+}
+
+static irqreturn_t mc33978_irq_thread(int irq, void *data)
+{
+	mc33978_handle_events(data);
+
+	return IRQ_HANDLED;
+}
+
+static void mc33978_teardown(void *data)
+{
+	struct mc33978_core_data *cdata = data;
+
+	/*
+	 * Set teardown flag before cancel_work_sync(). Prevents debugfs
+	 * regmap reads from rescheduling work after cancellation during
+	 * the devres LIFO teardown window.
+	 */
+	scoped_guard(spinlock_irqsave, &cdata->state_lock) {
+		cdata->tearing_down = true;
+	}
+
+	cancel_work_sync(&cdata->event_work);
+}
+
+static int mc33978_irq_init(struct mc33978_core_data *cdata,
+			    struct fwnode_handle *fwnode)
+{
+	struct device *dev = &cdata->spi->dev;
+	int ret;
+
+	mutex_init(&cdata->irq_lock);
+
+	/*
+	 * Create IRQ domain with 23 interrupts:
+	 * - hwirq 0-21: Pin change interrupts (22 pins)
+	 * - hwirq 22: Fault interrupt (for hwmon driver)
+	 */
+	cdata->domain = irq_domain_create_linear(fwnode, MC33978_NUM_IRQS,
+						 &mc33978_irq_domain_ops, cdata);
+	if (!cdata->domain)
+		return dev_err_probe(dev, -ENOMEM, "failed to create IRQ domain\n");
+
+	/*
+	 * Use DOMAIN_BUS_NEXUS to distinguish this intermediate demux domain
+	 * from child domains sharing the same fwnode. Matches the pattern used
+	 * by other MFD drivers (e.g., crystalcove).
+	 */
+	irq_domain_update_bus_token(cdata->domain, DOMAIN_BUS_NEXUS);
+
+	/*
+	 * Enable hierarchical IRQ domain support for pinctrl's GPIO IRQ chip.
+	 * See mc33978_irq_domain_ops for detailed architecture explanation.
+	 */
+	cdata->domain->flags |= IRQ_DOMAIN_FLAG_HIERARCHY;
+
+	ret = devm_add_action_or_reset(dev, mc33978_irq_domain_remove, cdata);
+	if (ret)
+		return ret;
+
+	return 0;
+}
+
+static void mc33978_event_work(struct work_struct *work)
+{
+	struct mc33978_core_data *cdata =
+		container_of(work, struct mc33978_core_data, event_work);
+
+	mc33978_handle_events(cdata);
+}
+
+/*
+ * Status Harvesting: Opportunistic Event Detection
+ *
+ * The hardware embeds volatile status bits (FAULT_STAT, INT_flg) in the high
+ * byte of almost every SPI response. These bits are harvested from all regmap
+ * operations (reads, writes, any register) to detect events regardless of
+ * which code path triggered the SPI transaction.
+ *
+ * Rationale for Harvesting All Traffic:
+ *
+ * The INT_flg bit is clear-on-read: any SPI transaction clears it, even if
+ * unrelated to interrupt handling. On shared IRQ lines, another driver's
+ * regmap access could clear INT_flg before this driver's IRQ thread runs,
+ * making it impossible to determine if this device triggered the interrupt.
+ * Harvesting INT_flg from all traffic ensures we see it before it's cleared.
+ *
+ * Current Usage:
+ *
+ * The driver does NOT use IRQF_SHARED (see mc33978_core_init comment), so
+ * shared IRQ protection is currently defensive/future-proofing. The harvesting
+ * architecture supports shared IRQs if the design changes.
+ *
+ * The Fault register (0x42) is marked volatile+precious in regmap config,
+ * which excludes it from regmap debugfs dumps, so unintended side effects
+ * from debug inspection cannot occur. Harvesting still applies to intentional
+ * Fault register reads from the hwmon driver.
+ *
+ * Harvesting Call Sites:
+ * - mc33978_spi_write(): Single frame (1 harvest from response)
+ * - mc33978_spi_read(): Pipelined (2 harvests: REQ frame + PIPE frame)
+ */
+static void mc33978_harvest_status(struct mc33978_core_data *cdata, int status)
+{
+	bool fault_active;
+	bool should_schedule;
+
+	fault_active = !!(status & MC33978_HI_BYTE_STAT_FAULT);
+
+	guard(spinlock_irqsave)(&cdata->state_lock);
+
+	cdata->bus_fault_active = fault_active;
+
+	/*
+	 * If the bus state changed from what the IRQ thread last
+	 * evaluated, wake it up using a synthetic software bit to avoid
+	 * overloading the hardware STAT_FAULT bit and causing phantom
+	 * transient faults.
+	 */
+	if (fault_active != cdata->cached_fault_active)
+		atomic_or(MC33978_HARVEST_WAKE_BIT,
+			  &cdata->harvested_flags);
+
+	if (status & MC33978_HI_BYTE_STAT_INT)
+		atomic_or(MC33978_HI_BYTE_STAT_INT, &cdata->harvested_flags);
+
+	/*
+	 * Preserve FAULT_STAT bit for transient detection: FAULT_STAT is sticky
+	 * (latched until Fault register read). When hwmon reads Fault register,
+	 * pipelined SPI produces two harvest calls: first with FAULT_STAT=1
+	 * (latched evidence), second with FAULT_STAT=0 (if condition cleared).
+	 * Transient detection needs the harvested FAULT_STAT bit as proof the
+	 * fault occurred, even if condition cleared before work runs.
+	 *
+	 * To prevent infinite soft lockups on sustained faults, we ONLY harvest
+	 * FAULT_STAT if the fault is not yet known to be active by the worker
+	 * (cached_fault_active == false). If cached_fault_active is true, we
+	 * already dispatched the edge and don't need evidence for transients.
+	 */
+	if ((status & MC33978_HI_BYTE_STAT_FAULT) && !cdata->cached_fault_active)
+		atomic_or(MC33978_HI_BYTE_STAT_FAULT, &cdata->harvested_flags);
+
+	should_schedule = cdata->irq_ready && !cdata->tearing_down &&
+			  atomic_read(&cdata->harvested_flags);
+
+	/*
+	 * Schedule work inside lock: prevent TOCTOU race where teardown frees cdata
+	 * after tearing_down check but before schedule_work() executes.
+	 */
+	if (should_schedule)
+		schedule_work(&cdata->event_work);
+}
+
+/*
+ * Initialize persistent SPI messages.
+ * Write: 1 frame. Read: 2 frames (MISO lags by 1 frame, needs dummy fetch).
+ */
+static void mc33978_prepare_messages(struct mc33978_core_data *cdata)
+{
+	/* --- Prepare Write Message (1 Frame) --- */
+	spi_message_init(&cdata->msg_write);
+
+	cdata->xfer_write.tx_buf = cdata->tx_req;
+	cdata->xfer_write.rx_buf = cdata->rx_dummy;
+	cdata->xfer_write.len = MC33978_FRAME_LEN;
+
+	spi_message_add_tail(&cdata->xfer_write, &cdata->msg_write);
+
+	/* --- Prepare Read Message (2 Frames) --- */
+	spi_message_init(&cdata->msg_read);
+
+	/* Frame 1: Request */
+	cdata->xfer_read[MC33978_FRAME_REQ].tx_buf =
+		cdata->tx_req;
+	cdata->xfer_read[MC33978_FRAME_REQ].rx_buf =
+		cdata->rx_dummy;
+	cdata->xfer_read[MC33978_FRAME_REQ].len = MC33978_FRAME_LEN;
+	/*
+	 * Latch command with explicit CS inactive delay. Per datasheet timing
+	 * specification, minimum CS inactive time (t_CSH) is 500ns. Use 1us.
+	 */
+	cdata->xfer_read[MC33978_FRAME_REQ].cs_change = 1;
+	cdata->xfer_read[MC33978_FRAME_REQ].cs_change_delay.value = 1;
+	cdata->xfer_read[MC33978_FRAME_REQ].cs_change_delay.unit = SPI_DELAY_UNIT_USECS;
+
+	/* Frame 2: Fetch (Dummy READ_IN) */
+	cdata->xfer_read[MC33978_FRAME_PIPE].tx_buf =
+		cdata->tx_pipe;
+	cdata->xfer_read[MC33978_FRAME_PIPE].rx_buf =
+		cdata->rx_resp;
+	cdata->xfer_read[MC33978_FRAME_PIPE].len = MC33978_FRAME_LEN;
+
+	/* Preload the dummy READ_IN command statically */
+	cdata->tx_pipe[MC33978_FRAME_CMD] = MC33978_REG_READ_IN;
+
+	spi_message_add_tail(&cdata->xfer_read[MC33978_FRAME_REQ], &cdata->msg_read);
+	spi_message_add_tail(&cdata->xfer_read[MC33978_FRAME_PIPE], &cdata->msg_read);
+}
+
+/**
+ * mc33978_rx_decode() - Decode and validate SPI response frame
+ * @rx_frame: Received SPI frame (4 bytes)
+ * @val_buf: Output buffer for register value (3 bytes), or NULL if not needed
+ * @expected_cmd: Command byte to validate, or MC33978_CMD_INVALID to skip
+ *
+ * Decodes 4-byte SPI frame to 3-byte regmap payload and extracts status bits.
+ * Validates echoed command to detect SPI bus faults (stuck MISO, disconnected
+ * chip). Validation skipped when expected_cmd == MC33978_CMD_INVALID (relaxed
+ * strategy - see mc33978_spi_write/read for rationale).
+ *
+ * Semi-global status flags (§9.10.27): FAULT_STAT and INT_flg bits are
+ * returned in most responses, except REG_CHECK and REG_WET_* registers.
+ *
+ * Return: Status bits (MC33978_HI_BYTE_STATUS_MASK) if present,
+ *         -ENODATA if register has no status bits,
+ *         -EIO if command echo validation failed
+ */
+static int mc33978_rx_decode(const u8 *rx_frame, u8 *val_buf, u8 expected_cmd)
+{
+	u8 cmd = rx_frame[MC33978_FRAME_CMD] & ~MC33978_CMD_BYTE_WRITE;
+	bool has_status;
+	u8 status = 0;
+
+	/* Validate echoed command if caller provided expected value */
+	if (expected_cmd != MC33978_CMD_INVALID) {
+		u8 expected = expected_cmd & ~MC33978_CMD_BYTE_WRITE;
+
+		if (cmd != expected)
+			return -EIO;
+	}
+
+	switch (cmd) {
+	case MC33978_REG_CHECK:
+	case MC33978_REG_WET_SP:
+	case MC33978_REG_WET_SG0:
+	case MC33978_REG_WET_SG1:
+		has_status = false;
+		break;
+	default:
+		has_status = true;
+		break;
+	}
+
+	if (has_status)
+		status = rx_frame[MC33978_FRAME_DATA_HI] &
+						MC33978_HI_BYTE_STATUS_MASK;
+
+	if (val_buf) {
+		memcpy(val_buf, &rx_frame[MC33978_FRAME_DATA_HI],
+		       MC33978_PAYLOAD_LEN);
+
+		if (has_status)
+			val_buf[MC33978_PAYLOAD_HI] &= MC33978_HI_BYTE_DATA_MASK;
+	}
+
+	return has_status ? status : -ENODATA;
+}
+
+static int mc33978_spi_write(void *ctx, const void *data, size_t count)
+{
+	struct mc33978_core_data *cdata = ctx;
+	int status;
+	int ret;
+
+	if (count != MC33978_FRAME_LEN)
+		return -EINVAL;
+
+	memcpy(cdata->tx_req, data, MC33978_FRAME_LEN);
+
+	ret = spi_sync(cdata->spi, &cdata->msg_write);
+	if (ret)
+		return ret;
+
+	/*
+	 * Relaxed validation: Skip echo check (would need prev cmd tracking).
+	 * Pipelining means RX contains previous transaction's response.
+	 * Tradeoff: simplicity/performance vs full validation. Hardware failures
+	 * still caught via read PIPE validation and CHECK register at init.
+	 */
+	status = mc33978_rx_decode(cdata->rx_dummy, NULL,
+				   MC33978_CMD_INVALID);
+
+	if (status >= 0)
+		mc33978_harvest_status(cdata, status);
+
+	return 0;
+}
+
+static int mc33978_spi_read(void *ctx, const void *reg_buf, size_t reg_size,
+			    void *val_buf, size_t val_size)
+{
+	struct mc33978_core_data *cdata = ctx;
+	int status_req, status_pipe;
+	u8 reg;
+	int ret;
+
+	if (reg_size != 1 || val_size != MC33978_PAYLOAD_LEN)
+		return -EINVAL;
+
+	memset(&cdata->tx_req[MC33978_FRAME_DATA_HI], 0,
+	       MC33978_PAYLOAD_LEN);
+	reg = ((const u8 *)reg_buf)[0];
+	cdata->tx_req[MC33978_FRAME_CMD] = reg;
+
+	ret = spi_sync(cdata->spi, &cdata->msg_read);
+	if (ret)
+		return ret;
+
+	/*
+	 * REQ frame validation: Skip echo check because pipelining means this
+	 * frame contains the PREVIOUS transaction's response, not the current
+	 * register being read. Only PIPE frame can be validated (see below).
+	 */
+	status_req = mc33978_rx_decode(cdata->rx_dummy, NULL,
+				       MC33978_CMD_INVALID);
+
+	/*
+	 * PIPE frame validation: Contains response to REQ frame command.
+	 * We transmit dummy READ_IN in PIPE, but receive echoed 'reg' from REQ.
+	 */
+	status_pipe = mc33978_rx_decode(cdata->rx_resp,
+					val_buf, reg);
+	if (status_pipe == -EIO) {
+		dev_err_ratelimited(&cdata->spi->dev, "PIPE frame cmd echo mismatch\n");
+		return -EIO;
+	}
+
+	if (status_req >= 0)
+		mc33978_harvest_status(cdata, status_req);
+	if (status_pipe >= 0)
+		mc33978_harvest_status(cdata, status_pipe);
+
+	return 0;
+}
+
+static const struct regmap_bus mc33978_regmap_bus = {
+	.read = mc33978_spi_read,
+	.write = mc33978_spi_write,
+};
+
+static const struct regmap_range mc33978_volatile_range[] = {
+	regmap_reg_range(MC33978_REG_ENTER_LPM, MC33978_REG_ENTER_LPM),
+	regmap_reg_range(MC33978_REG_READ_IN, MC33978_REG_RESET),
+};
+
+static const struct regmap_access_table mc33978_volatile_table = {
+	.yes_ranges = mc33978_volatile_range,
+	.n_yes_ranges = ARRAY_SIZE(mc33978_volatile_range),
+};
+
+static const struct regmap_range mc33978_precious_range[] = {
+	regmap_reg_range(MC33978_REG_ENTER_LPM, MC33978_REG_ENTER_LPM),
+	regmap_reg_range(MC33978_REG_READ_IN, MC33978_REG_RESET),
+};
+
+static const struct regmap_access_table mc33978_precious_table = {
+	.yes_ranges = mc33978_precious_range,
+	.n_yes_ranges = ARRAY_SIZE(mc33978_precious_range),
+};
+
+/*
+ * NOTE: Need to fake REG_ENTER_LPM, REG_IRQ and REG_RESET as readable, so
+ * regcache will NOT write them on a cache sync. Sounds counterintuitive, but
+ * marking a reg as "precious" or "volatile" is the only way to avoid this,
+ * and that works only with readable regs.
+ */
+static const struct regmap_range mc33978_readable_range[] = {
+	regmap_reg_range(MC33978_REG_CHECK, MC33978_REG_WET_SG1),
+	regmap_reg_range(MC33978_REG_CWET_SP, MC33978_REG_ENTER_LPM),
+	regmap_reg_range(MC33978_REG_AMUX_CTRL, MC33978_REG_RESET),
+};
+
+static const struct regmap_access_table mc33978_readable_table = {
+	.yes_ranges = mc33978_readable_range,
+	.n_yes_ranges = ARRAY_SIZE(mc33978_readable_range),
+};
+
+static const struct regmap_range mc33978_writable_range[] = {
+	regmap_reg_range(MC33978_REG_CONFIG, MC33978_REG_WET_SG1),
+	regmap_reg_range(MC33978_REG_CWET_SP, MC33978_REG_AMUX_CTRL),
+	regmap_reg_range(MC33978_REG_IRQ, MC33978_REG_RESET),
+};
+
+static const struct regmap_access_table mc33978_writable_table = {
+	.yes_ranges = mc33978_writable_range,
+	.n_yes_ranges = ARRAY_SIZE(mc33978_writable_range),
+};
+
+static const struct regmap_config mc33978_regmap_config = {
+	.name = "mc33978",
+	.reg_bits = 8,
+	.val_bits = 24,
+	.reg_stride = 2,
+	.write_flag_mask = MC33978_CMD_BYTE_WRITE,
+	.reg_format_endian = REGMAP_ENDIAN_BIG,
+	.val_format_endian = REGMAP_ENDIAN_BIG,
+	.use_single_read = true,
+	.use_single_write = true,
+	.volatile_table = &mc33978_volatile_table,
+	.precious_table = &mc33978_precious_table,
+	.rd_table = &mc33978_readable_table,
+	.wr_table = &mc33978_writable_table,
+	.cache_type = REGCACHE_MAPLE,
+	.max_register = MC33978_REG_RESET,
+};
+
+/* Verify SPI communication via CHECK register signature (0x123456) */
+static int mc33978_check_device(struct mc33978_core_data *cdata)
+{
+	struct device *dev = &cdata->spi->dev;
+	unsigned int check;
+	int ret;
+
+	ret = regmap_read(cdata->map, MC33978_REG_CHECK, &check);
+	if (ret)
+		return ret;
+
+	if (check != MC33978_CHECK_SIGNATURE)
+		return dev_err_probe(dev, -ENODEV,
+				     "SPI check failed. Expected: 0x%06x, got: 0x%06x\n",
+				     MC33978_CHECK_SIGNATURE, check);
+
+	return 0;
+}
+
+/**
+ * mc33978_core_init() - Initialize core functionality
+ * @dev: Parent device (for devm allocation)
+ * @spi: SPI device (already powered)
+ * @domain_out: Returns created IRQ domain
+ *
+ * Called by MFD driver to initialize regmap, IRQ domain, and event handling.
+ * All resources are devm-managed and tied to @dev lifecycle.
+ *
+ * Return: 0 on success, negative error code on failure
+ */
+int mc33978_core_init(struct device *dev, struct spi_device *spi,
+		      struct irq_domain **domain_out)
+{
+	struct fwnode_handle *fwnode;
+	struct mc33978_core_data *cdata;
+	int ret;
+
+	/* Initialize output parameter to NULL for error paths */
+	*domain_out = NULL;
+
+	fwnode = dev_fwnode(dev);
+	if (!fwnode)
+		return dev_err_probe(dev, -ENODEV, "missing firmware node\n");
+
+	cdata = devm_kzalloc(dev, sizeof(*cdata), GFP_KERNEL);
+	if (!cdata)
+		return -ENOMEM;
+
+	cdata->spi = spi;
+
+	mutex_init(&cdata->event_lock);
+	mutex_init(&cdata->irq_lock);
+	spin_lock_init(&cdata->state_lock);
+	raw_spin_lock_init(&cdata->irq_state_lock);
+
+	INIT_WORK(&cdata->event_work, mc33978_event_work);
+
+	mc33978_prepare_messages(cdata);
+
+	ret = mc33978_irq_init(cdata, fwnode);
+	if (ret)
+		return ret;
+
+	cdata->map = devm_regmap_init(dev, &mc33978_regmap_bus, cdata,
+				      &mc33978_regmap_config);
+	if (IS_ERR(cdata->map))
+		return dev_err_probe(dev, PTR_ERR(cdata->map),
+				     "failed to initialize regmap\n");
+
+	/*
+	 * Register teardown action to cancel event_work before resource cleanup.
+	 * Critical devm LIFO ordering (registered AFTER regmap/IRQ init above):
+	 *
+	 * Teardown sequence:
+	 * 1. MFD: devm_mfd_add_devices() cleanup
+	 *    - Child devices removed, child IRQ handlers freed
+	 * 2. THIS MODULE: devm_request_threaded_irq() cleanup (below)
+	 *    - Parent IRQ handler freed, stops new event triggers
+	 * 3. THIS ACTION: mc33978_teardown() via devm_add_action
+	 *    - Calls cancel_work_sync(&cdata->event_work)
+	 * 4. THIS MODULE: devm_regmap_init() cleanup (above)
+	 *    - Regmap destroyed
+	 * 5. THIS MODULE: devm_add_action(mc33978_irq_domain_remove) in
+	 *    mc33978_irq_init()
+	 *    - IRQ domain removed
+	 *
+	 * event_work (via mc33978_handle_events) accesses both cdata->map and
+	 * cdata->domain. Registering this action AFTER their creation but BEFORE
+	 * devm_request_threaded_irq() ensures LIFO cleanup: work is canceled in
+	 * step 3, guaranteeing no worker is running when resources are destroyed
+	 * in steps 4-5.
+	 *
+	 * Additionally, mc33978_teardown() sets tearing_down flag to prevent
+	 * debugfs regmap operations from rescheduling work after cancellation.
+	 */
+	ret = devm_add_action_or_reset(dev, mc33978_teardown, cdata);
+	if (ret)
+		return ret;
+
+	ret = mc33978_check_device(cdata);
+	if (ret)
+		return ret;
+
+	/*
+	 * POR state (§9.10.27): After power-on reset, both FAULT_STAT and
+	 * INT_flg are set high. These will be harvested during initialization
+	 * but discarded (no child IRQ handlers registered yet). Disable
+	 * interrupts before priming to prevent storms during state setup.
+	 */
+	ret = regmap_write(cdata->map, MC33978_REG_IE_SP, 0);
+	if (ret)
+		return ret;
+
+	ret = regmap_write(cdata->map, MC33978_REG_IE_SG, 0);
+	if (ret)
+		return ret;
+
+	/*
+	 * Prime the cached pin state under lock to prevent spurious events.
+	 * Work scheduling is disabled (irq_ready=false) to prevent the work
+	 * feedback loop that would occur during init: regmap_read() harvests
+	 * status -> schedules work -> work does regmap_read() -> schedules more
+	 * work -> infinite loop on single-core systems where work monopolizes
+	 * CPU before init can complete.
+	 */
+	scoped_guard(mutex, &cdata->event_lock) {
+		ret = regmap_read(cdata->map, MC33978_REG_READ_IN,
+				  &cdata->cached_pin_state);
+	}
+	if (ret)
+		return dev_err_probe(dev, ret, "failed to read initial pin state\n");
+
+	/*
+	 * Clear stale flags harvested during initialization. Regmap operations
+	 * above (IE_* writes, READ_IN read) harvested POR status flags while
+	 * irq_ready was false, preventing work scheduling.
+	 *
+	 * Note: Userspace debugfs reads of regmap registers between
+	 * devm_regmap_init() and this point will also harvest flags, but
+	 * irq_ready=false prevents work scheduling. This is safe and the minor
+	 * CPU load from debugfs access during probe can be ignored.
+	 *
+	 * These flags would be automatically discarded by mask checks (all IRQs
+	 * masked until child drivers probe), but clearing them explicitly avoids
+	 * unnecessary work scheduling and processing overhead.
+	 */
+	atomic_set(&cdata->harvested_flags, 0);
+
+	if (spi->irq <= 0)
+		return dev_err_probe(dev, -EINVAL,
+				     "no valid IRQ provided for INT_B pin\n");
+
+	/*
+	 * Not using IRQF_SHARED: threaded handler with IRQF_ONESHOT may hold
+	 * line masked too long on slow SPI, making shared operation impractical.
+	 */
+	ret = devm_request_threaded_irq(dev, spi->irq,
+					NULL,
+					mc33978_irq_thread,
+					IRQF_ONESHOT,
+					dev_name(dev), cdata);
+	if (ret)
+		return dev_err_probe(dev, ret, "failed to request IRQ\n");
+
+	/*
+	 * Enable work scheduling now that IRQ handler is registered.
+	 * This prevents the work feedback loop during initialization while
+	 * allowing proper event processing after setup completes.
+	 */
+	scoped_guard(spinlock_irqsave, &cdata->state_lock)
+		cdata->irq_ready = true;
+
+	/* Return IRQ domain for MFD to use */
+	*domain_out = cdata->domain;
+
+	return 0;
+}
+EXPORT_SYMBOL_GPL(mc33978_core_init);
+
+MODULE_AUTHOR("David Jander <david@protonic.nl>");
+MODULE_AUTHOR("Oleksij Rempel <o.rempel@pengutronix.de>");
+MODULE_DESCRIPTION("NXP MC33978/MC34978 Core Module");
+MODULE_LICENSE("GPL");
diff --git a/include/linux/mfd/mc33978.h b/include/linux/mfd/mc33978.h
new file mode 100644
index 000000000000..0a314ad29edf
--- /dev/null
+++ b/include/linux/mfd/mc33978.h
@@ -0,0 +1,196 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Copyright (C) 2024 David Jander <david@protonic.nl>, Protonic Holland
+ * Copyright (C) 2026 Oleksij Rempel <kernel@pengutronix.de>, Pengutronix
+ *
+ * MC34978/MC33978 Multiple Switch Detection Interface - Shared Definitions
+ */
+
+#ifndef _LINUX_MFD_MC33978_H
+#define _LINUX_MFD_MC33978_H
+
+#include <linux/bits.h>
+
+/* Register Map - All addresses are base command bytes (R/W bit = 0) */
+/* SPI communication check */
+#define MC33978_REG_CHECK		0x00
+/* Device configuration */
+#define MC33978_REG_CONFIG		0x02
+/* Tri-state enable SP */
+#define MC33978_REG_TRI_SP		0x04
+/* Tri-state enable SG */
+#define MC33978_REG_TRI_SG		0x06
+/* Wetting current level SP */
+#define MC33978_REG_WET_SP		0x08
+/* Wetting current level SG0 (SG7-SG0) */
+#define MC33978_REG_WET_SG0		0x0a
+/* Wetting current level SG1 (SG13-SG8) */
+#define MC33978_REG_WET_SG1		0x0c
+/* Continuous wetting current SP */
+#define MC33978_REG_CWET_SP		0x16
+/* Continuous wetting current SG */
+#define MC33978_REG_CWET_SG		0x18
+/* Interrupt enable SP */
+#define MC33978_REG_IE_SP		0x1a
+/* Interrupt enable SG */
+#define MC33978_REG_IE_SG		0x1c
+/* Low-power mode configuration */
+#define MC33978_REG_LPM_CONFIG		0x1e
+/* Wake-up enable SP */
+#define MC33978_REG_WAKE_SP		0x20
+/* Wake-up enable SG */
+#define MC33978_REG_WAKE_SG		0x22
+/* Comparator only mode SP */
+#define MC33978_REG_COMP_SP		0x24
+/* Comparator only mode SG */
+#define MC33978_REG_COMP_SG		0x26
+/* LPM voltage threshold SP */
+#define MC33978_REG_LPM_VT_SP		0x28
+/* LPM voltage threshold SG */
+#define MC33978_REG_LPM_VT_SG		0x2a
+/* Polling current SP */
+#define MC33978_REG_IP_SP		0x2c
+/* Polling current SG */
+#define MC33978_REG_IP_SG		0x2e
+/* Slow polling SP */
+#define MC33978_REG_SPOLL_SP		0x30
+/* Slow polling SG */
+#define MC33978_REG_SPOLL_SG		0x32
+/* Wake-up debounce SP */
+#define MC33978_REG_WDEB_SP		0x34
+/* Wake-up debounce SG */
+#define MC33978_REG_WDEB_SG		0x36
+/* Enter low-power mode (write-only) */
+#define MC33978_REG_ENTER_LPM		0x38
+/* AMUX control */
+#define MC33978_REG_AMUX_CTRL		0x3a
+/* Read switch status (READ_SW in datasheet) */
+#define MC33978_REG_READ_IN		0x3e
+/* Fault status register */
+#define MC33978_REG_FAULT		0x42
+/* Interrupt request (write-only) */
+#define MC33978_REG_IRQ			0x46
+/* Reset (write-only) */
+#define MC33978_REG_RESET		0x48
+
+/*
+ * FAULT Register (0x42) bit definitions
+ * Reading this register clears most fault flags except persistent conditions
+ */
+/* SPI communication error */
+#define MC33978_FAULT_SPI_ERROR		BIT(10)
+/* SPI register hash mismatch */
+#define MC33978_FAULT_HASH		BIT(9)
+/* VBATP undervoltage */
+#define MC33978_FAULT_UV		BIT(7)
+/* VBATP overvoltage */
+#define MC33978_FAULT_OV		BIT(6)
+/* Temperature warning threshold */
+#define MC33978_FAULT_TEMP_WARN		BIT(5)
+/* Over-temperature */
+#define MC33978_FAULT_OT		BIT(4)
+/* Woken by INT_B pin */
+#define MC33978_FAULT_INTB_WAKE		BIT(3)
+/* Woken by WAKE_B pin */
+#define MC33978_FAULT_WAKEB_WAKE	BIT(2)
+/* Woken by SPI message */
+#define MC33978_FAULT_SPI_WAKE		BIT(1)
+/* Power-on reset occurred */
+#define MC33978_FAULT_POR		BIT(0)
+
+/* Critical faults that need immediate attention */
+#define MC33978_FAULT_CRITICAL		(MC33978_FAULT_UV | \
+					 MC33978_FAULT_OV | \
+					 MC33978_FAULT_OT)
+
+/* Bits relevant as hwmon alarms; excludes wake/reset/SPI status bits */
+#define MC33978_FAULT_ALARM_MASK	(MC33978_FAULT_UV | \
+					 MC33978_FAULT_OV | \
+					 MC33978_FAULT_TEMP_WARN | \
+					 MC33978_FAULT_OT)
+
+#define MC33978_NUM_PINS		22
+#define MC33978_NUM_SG_PINS		14
+#define MC33978_NUM_SP_PINS		8
+
+/*
+ * Bit positions and shifts for pin groups in 22-bit representation.
+ * SG (Switch-to-Ground) pins are in the lower bits, SP (Programmable) in upper.
+ */
+#define MC33978_SG_SHIFT		0
+#define MC33978_SP_SHIFT		MC33978_NUM_SG_PINS
+
+/*
+ * Logical pin position masks (22-bit representation):
+ * - SG pins occupy bits [13:0] (14 pins)
+ * - SP pins occupy bits [21:14] (8 pins)
+ * Use these for manipulating cached state, checking pin types, etc.
+ */
+#define MC33978_SG_PIN_MASK		GENMASK(MC33978_NUM_SG_PINS - 1, 0)
+#define MC33978_SP_PIN_MASK		GENMASK(MC33978_NUM_PINS - 1, MC33978_NUM_SG_PINS)
+
+/*
+ * Hardware interrupt enable register bit masks:
+ * - IE_SG register: 14-bit field [13:0] for SG0-SG13
+ * - IE_SP register: 8-bit field [7:0] for SP0-SP7
+ * Use these when writing to IE_SG/IE_SP registers with regmap_update_bits().
+ * Note: IE_SG mask is identical to SG_PIN_MASK, but IE_SP differs from SP_PIN_MASK.
+ */
+#define MC33978_IE_SG_MASK		GENMASK(MC33978_NUM_SG_PINS - 1, 0)
+#define MC33978_IE_SP_MASK		GENMASK(MC33978_NUM_SP_PINS - 1, 0)
+
+static inline u32 mc33978_raw_to_physical_state(u32 raw_state, u32 config_reg)
+{
+	u32 inv_mask = MC33978_SG_PIN_MASK |
+		       (~(config_reg << MC33978_NUM_SG_PINS) & MC33978_SP_PIN_MASK);
+
+	return raw_state ^ inv_mask;
+}
+
+/*
+ * Virtual IRQ number for fault handling.
+ * Using hwirq 22 (beyond the 22 pin IRQs 0-21).
+ */
+#define MC33978_HWIRQ_FAULT		22
+
+/* Total number of hwirqs exposed by the MFD IRQ domain */
+#define MC33978_NUM_IRQS		(MC33978_HWIRQ_FAULT + 1)
+
+/*
+ * AMUX channel definitions
+ * The AMUX can route one of 24 signals to the external AMUX pin
+ */
+/* Switch-to-Ground inputs 0-13 */
+#define MC33978_AMUX_CH_SG0		0
+#define MC33978_AMUX_CH_SG13		13
+/* Programmable switch inputs 0-7 */
+#define MC33978_AMUX_CH_SP0		14
+#define MC33978_AMUX_CH_SP7		21
+/* Internal temperature diode */
+#define MC33978_AMUX_CH_TEMP		22
+/* Battery voltage sense */
+#define MC33978_AMUX_CH_VBATP		23
+/* Total number of AMUX channels */
+#define MC33978_NUM_AMUX_CH		24
+
+/* Forward declarations */
+struct device;
+struct spi_device;
+struct irq_domain;
+
+/*
+ * Core initialization function (implemented in mc33978-core module)
+ *
+ * This initializes the regmap, IRQ domain, and event handling for the device.
+ * The mc33978-core module must be loaded before the mc33978 MFD driver.
+ *
+ * @dev: Device pointer
+ * @spi: SPI device pointer
+ * @domain_out: Returns the IRQ domain for child device registration
+ *
+ * Returns: 0 on success, negative error code on failure
+ */
+int mc33978_core_init(struct device *dev, struct spi_device *spi,
+		      struct irq_domain **domain_out);
+
+#endif /* _LINUX_MFD_MC33978_H */
-- 
2.47.3


  parent reply	other threads:[~2026-07-10 10:14 UTC|newest]

Thread overview: 16+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-07-10 10:13 [PATCH v15 0/6] mfd: Add support for NXP MC33978/MC34978 MSDI Oleksij Rempel
2026-07-10 10:13 ` [PATCH v15 1/6] dt-bindings: pinctrl: add " Oleksij Rempel
2026-07-10 10:19   ` sashiko-bot
2026-07-10 10:13 ` Oleksij Rempel [this message]
2026-07-10 10:24   ` [PATCH v15 2/6] platform: misc: add NXP MC33978/MC34978 core driver sashiko-bot
2026-07-10 10:13 ` [PATCH v15 3/6] mfd: mc33978: Add NXP MC33978/MC34978 MSDI support Oleksij Rempel
2026-07-10 10:25   ` sashiko-bot
2026-07-10 10:13 ` [PATCH v15 4/6] pinctrl: add NXP MC33978/MC34978 pinctrl driver Oleksij Rempel
2026-07-10 10:27   ` sashiko-bot
2026-07-10 15:22   ` Alvin Šipraga
2026-07-11  5:21     ` Oleksij Rempel
2026-07-10 10:13 ` [PATCH v15 5/6] hwmon: add NXP MC33978/MC34978 driver Oleksij Rempel
2026-07-10 10:23   ` sashiko-bot
2026-07-10 10:13 ` [PATCH v15 6/6] mux: add NXP MC33978/MC34978 AMUX driver Oleksij Rempel
2026-07-10 10:25   ` sashiko-bot
2026-07-10 12:53   ` Alvin Šipraga

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260710101358.2606941-3-o.rempel@pengutronix.de \
    --to=o.rempel@pengutronix.de \
    --cc=conor+dt@kernel.org \
    --cc=david@protonic.nl \
    --cc=devicetree@vger.kernel.org \
    --cc=kernel@pengutronix.de \
    --cc=krzk+dt@kernel.org \
    --cc=lee@kernel.org \
    --cc=linusw@kernel.org \
    --cc=linux-gpio@vger.kernel.org \
    --cc=linux-hwmon@vger.kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux@roeck-us.net \
    --cc=peda@axentia.se \
    --cc=robh@kernel.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox