Devicetree
 help / color / mirror / Atom feed
* [PATCH v10 1/6] dt-bindings: pinctrl: add NXP MC33978/MC34978 MSDI
From: Oleksij Rempel @ 2026-04-01 16:25 UTC (permalink / raw)
  To: Guenter Roeck, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
	Lee Jones, Peter Rosin, Linus Walleij
  Cc: Oleksij Rempel, kernel, linux-kernel, devicetree, linux-hwmon,
	linux-gpio, David Jander
In-Reply-To: <20260401162510.1706073-1-o.rempel@pengutronix.de>

Add device tree binding documentation for the NXP MC33978 and MC34978
Multiple Switch Detection Interface (MSDI) devices.

The MC33978 and MC34978 differ primarily in their operating temperature
ranges. While not software-detectable, providing specific compatible
strings allows the hwmon subsystem to correctly interpret thermal
thresholds and hardware faults.

These ICs monitor up to 22 mechanical switch contacts in automotive and
industrial environments. They provide configurable wetting currents to
break through contact oxidation and feature extensive hardware
protection against thermal overload and voltage transients (load
dumps/brown-outs).

The device interfaces via SPI. While it provides multiple functions, its
primary hardware purpose is pin/switch control. To accurately represent
the hardware as a single physical integrated circuit without unnecessary
DT overhead, all functions are flattened into a single pinctrl node:
- pinctrl: Exposing the 22 switch inputs (SG/SP pins) as a GPIO controller
  and managing their pin configurations.
- hwmon: Exposing critical hardware faults (OT, OV, UV) and static
  voltage/temperature thresholds.
- mux: Controlling the 24-to-1 analog multiplexer to route pin voltages,
  internal temperature, or battery voltage to an external SoC ADC.

Signed-off-by: Oleksij Rempel <o.rempel@pengutronix.de>
Reviewed-by: Rob Herring (Arm) <robh@kernel.org>
Reviewed-by: Linus Walleij <linusw@kernel.org>
---
changes v10:
- no changes
changes v9:
- no changes
changes v8:
- Update IRQ_TYPE_* macros include path reference in documentation from
  interrupt-controller.h to dt-bindings/interrupt-controller/irq.h.
- Add bias-disable, drive-open-drain, drive-open-source, and drive-strength
  to the list of supported pin configuration properties.
changes v7:
- no changes
changes v6:
- add Reviewed-by: Rob Herring (Arm) <robh@kernel.org>
- add Reviewed-by: Linus Walleij <linusw@kernel.org>
changes v5:
- Commit Message: Added justification for distinct compatible strings
  based on temperature ranges.
- Restricted pins property to an explicit enum of valid hardware pins
changes v4:
- Drop the standalone mfd/nxp,mc33978.yaml schema entirely.
- Move the unified device binding to bindings/pinctrl/nxp,mc33978.yaml,
- Remove the dedicated child node compatible strings (nxp,mc33978-pinctrl).
- Flatten the pinctrl/gpio properties directly into the main SPI device
  node.
changes v3:
- Drop regular expression pattern from pinctrl child node and define
  it as a standard property
- Reorder required properties list in MFD binding
- Remove stray blank line from the MFD binding devicetree example
- Replace unevaluatedProperties with additionalProperties in the pinctrl
  binding
changes v2:
- Squashed MFD, pinctrl, hwmon, and mux bindings into a single patch
- Removed the empty hwmon child node
- Folded the mux-controller node into the parent MFD node
- Added vbatp-supply and vddq-supply to the required properties block
- Changed the example node name from mc33978@0 to gpio@0
- Removed unnecessary literal block scalars (|) from descriptions
- Documented SG, SP, and SB pin acronyms in the pinctrl description
- Added consumer polarity guidance (GPIO_ACTIVE_LOW/HIGH) for SG/SB
  inputs, with a note on output circuit dependency
- Updated commit message
---
 .../bindings/pinctrl/nxp,mc33978.yaml         | 158 ++++++++++++++++++
 1 file changed, 158 insertions(+)
 create mode 100644 Documentation/devicetree/bindings/pinctrl/nxp,mc33978.yaml

diff --git a/Documentation/devicetree/bindings/pinctrl/nxp,mc33978.yaml b/Documentation/devicetree/bindings/pinctrl/nxp,mc33978.yaml
new file mode 100644
index 000000000000..2a3c565c3c03
--- /dev/null
+++ b/Documentation/devicetree/bindings/pinctrl/nxp,mc33978.yaml
@@ -0,0 +1,158 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/pinctrl/nxp,mc33978.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: NXP MC33978/MC34978 Multiple Switch Detection Interface
+
+maintainers:
+  - David Jander <david@protonic.nl>
+  - Oleksij Rempel <o.rempel@pengutronix.de>
+
+description: |
+  The MC33978 and MC34978 are Multiple Switch Detection Interface (MSDI)
+  devices with 22 switch inputs, integrated fault detection, and analog
+  multiplexer (AMUX) for voltage/temperature monitoring.
+
+  Pin numbering:
+  - Pins 0-13: SG0-SG13 (Switch-to-Ground inputs). These pins monitor
+    contacts closed to ground and typically require GPIO_ACTIVE_LOW
+    flags when used as digital inputs.
+  - Pins 14-21: SP0-SP7 (Programmable inputs). These can be configured
+    as SG (Switch-to-Ground) or SB (Switch-to-Battery) inputs. SB
+    inputs monitor contacts closed to the battery voltage and typically
+    require GPIO_ACTIVE_HIGH flags when used as digital inputs.
+
+  Output Emulation:
+  The hardware lacks standard push-pull output drivers. Outputs are emulated
+  by toggling the programmable wetting current sources (acting as pull-ups
+  or pull-downs) and the hardware tri-state registers. Because of this
+  physical constraint:
+  - Consumers using pins as outputs MUST flag them with GPIO_OPEN_DRAIN or
+    GPIO_OPEN_SOURCE in the device tree.
+  - Push-pull configurations are physically unsupported.
+  - The active polarity depends entirely on the external circuit (e.g., how
+    an LED is wired) and must be flagged accordingly by the consumer.
+
+allOf:
+  - $ref: /schemas/spi/spi-peripheral-props.yaml#
+
+properties:
+  compatible:
+    enum:
+      - nxp,mc33978
+      - nxp,mc34978
+
+  reg:
+    maxItems: 1
+    description: SPI chip select number
+
+  spi-max-frequency:
+    maximum: 8000000
+    description: Maximum SPI clock frequency (up to 8 MHz)
+
+  interrupts:
+    maxItems: 1
+    description:
+      INT_B pin interrupt. Active-low, indicates pin state changes or
+      fault conditions.
+
+  interrupt-controller: true
+
+  '#interrupt-cells':
+    const: 2
+    description:
+      First cell is the IRQ number (0-21 for pins, 22 for faults).
+      Second cell is the trigger type (IRQ_TYPE_* from dt-bindings/interrupt-controller/irq.h).
+
+  '#mux-control-cells':
+    const: 0
+    description:
+      Present if the device AMUX selector is used as a mux provider.
+      Consumers (e.g. io-channel-mux) must provide settle-time-us for the
+      external ADC sampling path.
+
+  vddq-supply:
+    description: Digital supply voltage
+
+  vbatp-supply:
+    description: Battery/power supply
+
+  gpio-controller: true
+
+  '#gpio-cells':
+    const: 2
+
+  ngpios:
+    const: 22
+
+patternProperties:
+  '^.*-grp$':
+    type: object
+    $ref: /schemas/pinctrl/pincfg-node.yaml#
+    additionalProperties: false
+    description: Pin configuration subnodes.
+    properties:
+      pins:
+        items:
+          enum: [sg0, sg1, sg2, sg3, sg4, sg5, sg6, sg7, sg8, sg9,
+                 sg10, sg11, sg12, sg13, sp0, sp1, sp2, sp3,
+                 sp4, sp5, sp6, sp7]
+
+      bias-pull-up: true
+      bias-pull-down: true
+      bias-high-impedance: true
+      bias-disable: true
+      drive-open-drain: true
+      drive-open-source: true
+      drive-strength:
+        enum: [2, 6, 8, 10, 12, 14, 16, 20]
+
+required:
+  - compatible
+  - reg
+  - interrupts
+  - interrupt-controller
+  - '#interrupt-cells'
+  - vddq-supply
+  - vbatp-supply
+  - gpio-controller
+  - '#gpio-cells'
+
+unevaluatedProperties: false
+
+examples:
+  - |
+    #include <dt-bindings/interrupt-controller/irq.h>
+    #include <dt-bindings/gpio/gpio.h>
+
+    spi {
+        #address-cells = <1>;
+        #size-cells = <0>;
+
+        msdi: gpio@0 {
+            compatible = "nxp,mc33978";
+            reg = <0>;
+            spi-max-frequency = <4000000>;
+
+            interrupt-parent = <&gpiog>;
+            interrupts = <9 IRQ_TYPE_LEVEL_LOW>;
+            interrupt-controller;
+            #interrupt-cells = <2>;
+
+            vddq-supply = <&reg_3v3>;
+            vbatp-supply = <&reg_12v>;
+
+            #mux-control-cells = <0>;
+
+            gpio-controller;
+            #gpio-cells = <2>;
+            ngpios = <22>;
+
+            door-grp {
+                pins = "sg0";
+                bias-high-impedance;
+            };
+        };
+    };
-- 
2.47.3


^ permalink raw reply related

* [PATCH v10 6/6] mux: add NXP MC33978/MC34978 AMUX driver
From: Oleksij Rempel @ 2026-04-01 16:25 UTC (permalink / raw)
  To: Guenter Roeck, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
	Lee Jones, Peter Rosin, Linus Walleij
  Cc: Oleksij Rempel, kernel, linux-kernel, devicetree, linux-hwmon,
	linux-gpio, David Jander
In-Reply-To: <20260401162510.1706073-1-o.rempel@pengutronix.de>

Add a mux-control driver for the 24-to-1 analog multiplexer (AMUX)
embedded in the NXP MC33978/MC34978 Multiple Switch Detection
Interface (MSDI) devices.

Signed-off-by: Oleksij Rempel <o.rempel@pengutronix.de>
---
changes v10:
- no chnages
changes v9:
- rename mc33978-mux to mux-mc33978 in the Kconfig help
- fail if fwnode is NULL
changes v8:
- no chnages
changes v7:
- Simplify the return path and local variable assignment in
  mc33978_mux_set().
- Change idle_state to a signed integer to properly handle negative MUX
  subsystem constants.
- Default to MUX_IDLE_AS_IS when the "idle-state" device tree property
  is missing.
- Explicitly reject MUX_IDLE_DISCONNECT since the hardware does not
  support disconnecting the multiplexer.
changes v6:
- parse optional idle-state property
- validate idle-state against available AMUX channels
- lower-case probe error messages
changes v5:
- no changes
changes v4:
- no changes
changes v3:
- no changes
changes v2:
- Add missing <linux/err.h> include.
- Add platform_device_id table
---
 drivers/mux/Kconfig       |  14 ++++
 drivers/mux/Makefile      |   2 +
 drivers/mux/mc33978-mux.c | 141 ++++++++++++++++++++++++++++++++++++++
 3 files changed, 157 insertions(+)
 create mode 100644 drivers/mux/mc33978-mux.c

diff --git a/drivers/mux/Kconfig b/drivers/mux/Kconfig
index c68132e38138..ffe92a714096 100644
--- a/drivers/mux/Kconfig
+++ b/drivers/mux/Kconfig
@@ -45,6 +45,20 @@ config MUX_GPIO
 	  To compile the driver as a module, choose M here: the module will
 	  be called mux-gpio.
 
+config MUX_MC33978
+	tristate "NXP MC33978/MC34978 Analog Multiplexer"
+	depends on MFD_MC33978
+	help
+	  MC33978/MC34978 24-to-1 analog multiplexer (AMUX) driver.
+
+	  This driver provides mux-control for the analog multiplexer,
+	  which can route switch voltages, temperature, and battery voltage
+	  to an external ADC. Typically used with IIO ADC drivers to measure
+	  analog values from the 22 switch inputs plus temperature and VBATP.
+
+	  To compile the driver as a module, choose M here: the module will
+	  be called mux-mc33978.
+
 config MUX_MMIO
 	tristate "MMIO/Regmap register bitfield-controlled Multiplexer"
 	depends on OF
diff --git a/drivers/mux/Makefile b/drivers/mux/Makefile
index 6e9fa47daf56..339c44b4d4f4 100644
--- a/drivers/mux/Makefile
+++ b/drivers/mux/Makefile
@@ -7,10 +7,12 @@ mux-core-objs			:= core.o
 mux-adg792a-objs		:= adg792a.o
 mux-adgs1408-objs		:= adgs1408.o
 mux-gpio-objs			:= gpio.o
+mux-mc33978-objs		:= mc33978-mux.o
 mux-mmio-objs			:= mmio.o
 
 obj-$(CONFIG_MULTIPLEXER)	+= mux-core.o
 obj-$(CONFIG_MUX_ADG792A)	+= mux-adg792a.o
 obj-$(CONFIG_MUX_ADGS1408)	+= mux-adgs1408.o
 obj-$(CONFIG_MUX_GPIO)		+= mux-gpio.o
+obj-$(CONFIG_MUX_MC33978)	+= mux-mc33978.o
 obj-$(CONFIG_MUX_MMIO)		+= mux-mmio.o
diff --git a/drivers/mux/mc33978-mux.c b/drivers/mux/mc33978-mux.c
new file mode 100644
index 000000000000..b44c862f0dbe
--- /dev/null
+++ b/drivers/mux/mc33978-mux.c
@@ -0,0 +1,141 @@
+// SPDX-License-Identifier: GPL-2.0-only
+// Copyright (c) 2026 Pengutronix, Oleksij Rempel <kernel@pengutronix.de>
+/*
+ * MC33978/MC34978 Analog Multiplexer (AMUX) Driver
+ *
+ * This driver provides mux-control for the 24-to-1 analog multiplexer.
+ * The AMUX routes one of the following signals to the external AMUX pin:
+ * - Channels 0-13: SG0-SG13 switch voltages
+ * - Channels 14-21: SP0-SP7 switch voltages
+ * - Channel 22: Internal temperature diode
+ * - Channel 23: Battery voltage (VBATP)
+ *
+ * Consumer drivers (typically IIO ADC drivers) use the mux-control
+ * subsystem to select which signal to measure.
+ *
+ * Architecture:
+ * The MC33978 does not have an internal ADC. Instead, it routes analog
+ * signals to an external AMUX pin that must be connected to an external
+ * ADC (such as the SoC's internal ADC). The IIO subsystem is responsible
+ * for coordinating the mux selection and ADC sampling.
+ */
+
+#include <linux/device.h>
+#include <linux/err.h>
+#include <linux/mod_devicetable.h>
+#include <linux/module.h>
+#include <linux/mux/driver.h>
+#include <linux/platform_device.h>
+#include <linux/property.h>
+#include <linux/regmap.h>
+
+#include <linux/mfd/mc33978.h>
+
+/* AMUX_CTRL register field definitions */
+#define MC33978_AMUX_CTRL_MASK	GENMASK(5, 0)	/* 6-bit channel select */
+
+struct mc33978_mux_priv {
+	struct device *dev;
+	struct regmap *map;
+};
+
+static int mc33978_mux_set(struct mux_control *mux, int state)
+{
+	struct mux_chip *mux_chip = mux->chip;
+	struct mc33978_mux_priv *priv = mux_chip_priv(mux_chip);
+	int ret;
+
+	if (state < 0 || state >= MC33978_NUM_AMUX_CH)
+		return -EINVAL;
+
+	ret = regmap_update_bits(priv->map, MC33978_REG_AMUX_CTRL,
+				 MC33978_AMUX_CTRL_MASK, state);
+	if (ret)
+		dev_err(priv->dev, "failed to set AMUX channel %d: %d\n",
+			state, ret);
+
+	return ret;
+}
+
+static const struct mux_control_ops mc33978_mux_ops = {
+	.set = mc33978_mux_set,
+};
+
+static int mc33978_mux_probe(struct platform_device *pdev)
+{
+	struct device *dev = &pdev->dev;
+	struct mc33978_mux_priv *priv;
+	struct fwnode_handle *fwnode;
+	struct mux_chip *mux_chip;
+	struct mux_control *mux;
+	s32 idle_state;
+	int ret;
+
+	mux_chip = devm_mux_chip_alloc(dev, 1, sizeof(*priv));
+	if (IS_ERR(mux_chip))
+		return dev_err_probe(dev, PTR_ERR(mux_chip), "failed to allocate mux chip\n");
+
+	fwnode = dev_fwnode(dev->parent);
+	if (!fwnode)
+		return dev_err_probe(dev, -ENODEV, "missing parent firmware node\n");
+
+	/* Borrow the parent's firmware node so consumers can find this mux chip */
+	device_set_node(&mux_chip->dev, fwnode);
+
+	priv = mux_chip_priv(mux_chip);
+	priv->dev = dev;
+
+	priv->map = dev_get_regmap(dev->parent, NULL);
+	if (!priv->map)
+		return dev_err_probe(dev, -ENODEV, "failed to get parent regmap\n");
+
+	mux_chip->ops = &mc33978_mux_ops;
+
+	mux = &mux_chip->mux[0];
+	mux->states = MC33978_NUM_AMUX_CH;
+
+	ret = device_property_read_u32(&mux_chip->dev, "idle-state",
+				       (u32 *)&idle_state);
+	if (ret < 0 && ret != -EINVAL) {
+		return dev_err_probe(dev, ret, "failed to parse idle-state\n");
+	} else if (ret == -EINVAL) {
+		mux->idle_state = MUX_IDLE_AS_IS;
+	} else {
+		if (idle_state == MUX_IDLE_DISCONNECT)
+			return dev_err_probe(dev, -EINVAL,
+					     "idle-disconnect not supported by hardware\n");
+		if (idle_state != MUX_IDLE_AS_IS &&
+		    (idle_state < 0 || idle_state >= MC33978_NUM_AMUX_CH))
+			return dev_err_probe(dev, -EINVAL, "invalid idle-state %d\n",
+					     idle_state);
+		mux->idle_state = idle_state;
+	}
+
+	ret = devm_mux_chip_register(dev, mux_chip);
+	if (ret)
+		return dev_err_probe(dev, ret, "failed to register mux chip\n");
+
+	platform_set_drvdata(pdev, mux_chip);
+
+	return 0;
+}
+
+static const struct platform_device_id mc33978_mux_id[] = {
+	{ "mc33978-mux", },
+	{ "mc34978-mux", },
+	{ }
+};
+MODULE_DEVICE_TABLE(platform, mc33978_mux_id);
+
+static struct platform_driver mc33978_mux_driver = {
+	.driver = {
+		.name = "mc33978-mux",
+	},
+	.probe = mc33978_mux_probe,
+	.id_table = mc33978_mux_id,
+};
+module_platform_driver(mc33978_mux_driver);
+
+MODULE_AUTHOR("Oleksij Rempel <kernel@pengutronix.de>");
+MODULE_DESCRIPTION("NXP MC33978/MC34978 Analog Multiplexer Driver");
+MODULE_LICENSE("GPL");
-- 
2.47.3


^ permalink raw reply related

* [PATCH v10 4/6] pinctrl: add NXP MC33978/MC34978 pinctrl driver
From: Oleksij Rempel @ 2026-04-01 16:25 UTC (permalink / raw)
  To: Guenter Roeck, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
	Lee Jones, Peter Rosin, Linus Walleij
  Cc: David Jander, Oleksij Rempel, kernel, linux-kernel, devicetree,
	linux-hwmon, linux-gpio
In-Reply-To: <20260401162510.1706073-1-o.rempel@pengutronix.de>

From: David Jander <david@protonic.nl>

Add pin control and GPIO driver for the NXP MC33978/MC34978 Multiple
Switch Detection Interface (MSDI) devices.

This driver exposes the 22 mechanical switch detection inputs (14
Switch-to-Ground, 8 Programmable) as standard GPIOs.

Key features implemented:
- GPIO read/write: Translates physical switch states (open/closed)
  to logical GPIO levels based on the configured switch topology
  (Switch-to-Ground vs. Switch-to-Battery).
- Emulated Output: Allows setting pins "high" or "low" by manipulating
  the tri-state registers and hardware pull topologies.
- Interrupt routing: Proxies GPIO interrupt requests to the irq_domain
  managed by the parent MFD core driver via a hierarchical irq_chip.

Signed-off-by: David Jander <david@protonic.nl>
Co-developed-by: Oleksij Rempel <o.rempel@pengutronix.de>
Signed-off-by: Oleksij Rempel <o.rempel@pengutronix.de>
Reviewed-by: Linus Walleij <linusw@kernel.org>
---
changes v10:
- Replace state macros with enum mc33978_bias_state and enum
  mc33978_drive_mode for better type safety.
- Add cached_drive array to isolate output drive topology from the input
  bias configuration (cached_bias).
- Decouple set_pull and set_drive functions to fix state leakage when
  switching between input and output modes.
- Add explicit comments explaining how hardware wetting currents are
  used to emulate standard pinctrl drive/pull modes.
- Initialize both bias and drive state caches directly from hardware
  registers during driver probe.
changes v9:
- Resolve probe fwnode directly from parent (`dev_fwnode(dev->parent)`) and
  fail early with `-ENODEV` if the parent firmware node is missing.
- Set child device node from this validated parent fwnode.
- Replace mutex_init() with devm_mutex_init()
- Add gpiochip_disable_irq/enable_irq calls in mask/unmask callbacks
  for proper gpiolib IRQ state tracking with IRQCHIP_IMMUTABLE
- Set DOMAIN_BUS_WIRED token for GPIO IRQ domain to distinguish from
  parent MFD domain sharing same fwnode
- Add explanatory comment about fwnode sharing and bus token isolation
  to prevent domain shadowing concerns
- select GPIOLIB_IRQCHIP and IRQ_DOMAIN_HIERARCHY
changes v8:
- Fix comment documentation to state the driver implements a hierarchical
  irq_chip instead of proxying .to_irq().
- Add missing <linux/irqdomain.h> include.
- Add .irq_set_wake = irq_chip_set_wake_parent to the gpio_irq_chip to
  properly proxy wake-up configuration to the parent domain.
- Replace irq_find_host() with irq_find_matching_fwnode() during probe
  to support parent domain lookup on non-OF platforms.
changes v7:
- Refactor I/O state reading and tri-state updates for SG/SB topologies
- Fix open-drain and open-source pinconf emulation
- Make direction_input a no-op to prevent overriding pinctrl bias
- Add defensive wrappers for IRQ proxying to prevent NULL pointer panics
- Add missing mutex guards to pinconf and get operations
- Convert generic internal variables to u32 and add lockdep assertions
changes v6:
- no changes
changes v5:
- no changes
changes v4:
- add Reviewed-by: Linus Walleij ...
- Replace the of_device_id match table with a platform_device_id table
- Add device_set_node(dev, dev_fwnode(dev->parent)) during probe
- Remove the check for a missing dev->of_node
changes v3:
- replace manual mutex_lock()/mutex_unlock() paths with guard(mutex)
- Unify error checking style by replacing if (ret < 0) with if (ret)
- Migrate from a custom .to_irq callback to a hierarchical gpio_irq_chip
- Implement .irq_bus_lock and .irq_bus_sync_unlock proxies to properly
  cascade SPI bus lock operations to the parent MFD domain
- Set girq->handler to handle_simple_irq
changes v2:
- Translate all remaining German comments to English.
- Remove unnecessary #ifdef CONFIG_OF wrappers around dt_node_to_map.
- Add detailed comments to mc33978_get() and mc33978_get_multiple() explaining
  the hardware comparator logic (1 = closed, 0 = open) and justifying the
  bitwise inversion required to report actual physical voltage levels.
- Add comments to the .set() and .set_config() callbacks explaining why
  gpiolib's standard open-drain emulation (switching to input mode) fails on
  this hardware due to active wetting currents, and why tri-state isolation is
  mandatory.
- Add a comment to mc33978_gpio_to_irq() explaining why it must act as a
  proxy to the parent MFD's irq_domain (shared physical INT_B line with hwmon).
- Drop dummy pin group callbacks (get_groups_count, etc.). This relies on a
  preparatory patch in this series making these callbacks optional in the core.
- Fix debugfs 'pinconf-pins' read errors by correctly returning -ENOTSUPP
  instead of -EOPNOTSUPP for unsupported generic configurations.
- Fix empty 'gpio-ranges' and missing debugfs labels by explicitly calling
  gpiochip_add_pin_range() during probe.
- Eliminate "magic" bitwise math in the wetting current configuration by
  introducing a static lookup array (mc33978_wet_mA).
- Resolve checkpatch.pl strict warnings regarding macro argument reuse by
  converting MC33978_SPSG, MC33978_PINSHIFT, MC33978_WREG, and MC33978_WSHIFT
  to static inline functions.
- Remove artifacts from previous interrupt handling implementations.
- Address minor formatting and whitespace nits.
---
 drivers/pinctrl/Kconfig           |   16 +
 drivers/pinctrl/Makefile          |    1 +
 drivers/pinctrl/pinctrl-mc33978.c | 1008 +++++++++++++++++++++++++++++
 3 files changed, 1025 insertions(+)
 create mode 100644 drivers/pinctrl/pinctrl-mc33978.c

diff --git a/drivers/pinctrl/Kconfig b/drivers/pinctrl/Kconfig
index afecd9407f53..64f9c5b1aacb 100644
--- a/drivers/pinctrl/Kconfig
+++ b/drivers/pinctrl/Kconfig
@@ -388,6 +388,22 @@ config PINCTRL_MAX77620
 	  function in alternate mode. This driver also configure push-pull,
 	  open drain, FPS slots etc.
 
+config PINCTRL_MC33978
+	tristate "MC33978/MC34978 industrial input controller support"
+	depends on MFD_MC33978
+	select GPIOLIB
+	select GPIOLIB_IRQCHIP
+	select IRQ_DOMAIN_HIERARCHY
+	select GENERIC_PINCONF
+	help
+	  Say Y here to enable support for NXP MC33978/MC34978 Multiple
+	  Switch Detection Interface (MSDI) devices. This driver provides
+	  pinctrl and GPIO interfaces for the 22 mechanical switch inputs
+	  (14 Switch-to-Ground, 8 Programmable).
+
+	  It allows reading switch states, configuring hardware pull
+	  topologies, and handling interrupts for state changes.
+
 config PINCTRL_MCP23S08_I2C
 	tristate
 	select REGMAP_I2C
diff --git a/drivers/pinctrl/Makefile b/drivers/pinctrl/Makefile
index f7d5d5f76d0c..afb58fb5a197 100644
--- a/drivers/pinctrl/Makefile
+++ b/drivers/pinctrl/Makefile
@@ -40,6 +40,7 @@ obj-$(CONFIG_PINCTRL_XWAY)	+= pinctrl-xway.o
 obj-$(CONFIG_PINCTRL_LPC18XX)	+= pinctrl-lpc18xx.o
 obj-$(CONFIG_PINCTRL_MAX7360)	+= pinctrl-max7360.o
 obj-$(CONFIG_PINCTRL_MAX77620)	+= pinctrl-max77620.o
+obj-$(CONFIG_PINCTRL_MC33978)	+= pinctrl-mc33978.o
 obj-$(CONFIG_PINCTRL_MCP23S08_I2C)	+= pinctrl-mcp23s08_i2c.o
 obj-$(CONFIG_PINCTRL_MCP23S08_SPI)	+= pinctrl-mcp23s08_spi.o
 obj-$(CONFIG_PINCTRL_MCP23S08)	+= pinctrl-mcp23s08.o
diff --git a/drivers/pinctrl/pinctrl-mc33978.c b/drivers/pinctrl/pinctrl-mc33978.c
new file mode 100644
index 000000000000..9a002feebc48
--- /dev/null
+++ b/drivers/pinctrl/pinctrl-mc33978.c
@@ -0,0 +1,1008 @@
+// 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 Multiple Switch Detection Interface - Pinctrl/GPIO Driver
+ *
+ * Provides GPIO and pinctrl interfaces for the 22 switch detection inputs.
+ * Handles digital input reading and wetting current configuration. Analog AMUX
+ * functionality is handled by a separate mux driver.
+ *
+ * GPIO Mapping:
+ * - GPIO 0-13:  SG0-SG13 (Switch-to-Ground inputs)
+ * - GPIO 14-21: SP0-SP7 (Programmable: Switch-to-Ground or Switch-to-Battery)
+ * This is dictated by the READ_IN register where bits [21:14] = SP[7:0]
+ * and bits [13:0] = SG[13:0].
+ *
+ * Register Organization:
+ * Configuration registers are generally paired. The _SP register at offset N
+ * controls SP0-SP7, and the _SG register at offset N+2 controls SG0-SG13.
+ *
+ * Wetting Currents vs. Pull Resistors:
+ * The hardware physically lacks traditional passive pull-up or pull-down
+ * resistors. Instead, it uses active, controllable current regulators
+ * (wetting currents) to detect switch states and clean mechanical contacts.
+ * - Because these are active current sources, specifying an ohmic value for
+ * pull-up/down biases is physically invalid. The driver ignores ohm arguments.
+ * - 8 selectable current values: 2, 6, 8, 10, 12, 14, 16, 20 mA.
+ * - Exposed via the pinconf PIN_CONFIG_DRIVE_STRENGTH parameter (in mA).
+ *
+ * Emulated Outputs:
+ * The hardware lacks traditional push-pull output drivers; it is strictly an
+ * input device. "Outputs" are simulated by toggling the wetting currents and
+ * physically isolating the pins via hardware tri-state registers. Consequently,
+ * consumers should flag outputs with GPIO_OPEN_DRAIN or GPIO_OPEN_SOURCE in
+ * the Device Tree to match the emulated output model.
+ *
+ * Input Detection Mechanics:
+ * This input mechanism relies on the active current regulators rather than
+ * passive hard resistors. For a Switch-to-Ground (SG) pin, the chip sources
+ * a constant current. When the switch is open, the pin voltage floats up to
+ * the battery voltage. When the switch closes, it creates a path to ground;
+ * because the current is strictly regulated, the pin voltage drops sharply
+ * below the internal 4.0V comparator threshold.
+ * The hardware evaluates this and reports an abstract "contact status"
+ * (1 = closed, 0 = open). For SG pins, a closed switch (~0V) reports as '1'.
+ * To align with gpiolib expectations where ~0V equals a physical logical '0',
+ * this driver explicitly inverts the hardware status for all SG-configured
+ * pins before reporting them.
+ *
+ * Interrupts:
+ * The physical INT_B line and threaded IRQ domain are managed centrally by
+ * the parent MFD core. This driver implements a hierarchical irq_chip
+ * to proxy masking/unmasking and configuration to the parent domain.
+ *
+ * Written by David Jander <david@protonic.nl>
+ *
+ * Datasheet:
+ * https://www.nxp.com/docs/en/data-sheet/MC33978.pdf
+ */
+
+#include <linux/cleanup.h>
+#include <linux/device.h>
+#include <linux/gpio/driver.h>
+#include <linux/interrupt.h>
+#include <linux/irq.h>
+#include <linux/irqdomain.h>
+#include <linux/kernel.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/pinctrl/pinconf-generic.h>
+#include <linux/pinctrl/pinconf.h>
+#include <linux/pinctrl/pinctrl.h>
+#include <linux/platform_device.h>
+#include <linux/regmap.h>
+
+#include <linux/mfd/mc33978.h>
+
+#define MC33978_NGPIO		22
+
+/*
+ * Input numbering is dictated by bit-order of the input register:
+ * Inputs 0-13 -> SG0-SG13
+ * Inputs 14-21 -> SP0-SP7
+ */
+#define MC33978_NUM_SG		14
+#define MC33978_SP_MASK		GENMASK(MC33978_NGPIO - 1, MC33978_NUM_SG)
+#define MC33978_SG_MASK		GENMASK(MC33978_NUM_SG - 1, 0)
+#define MC33978_SG_SHIFT	0
+#define MC33978_SP_SHIFT	MC33978_NUM_SG
+
+enum mc33978_bias_state {
+	MC33978_TRISTATE = 0,
+	MC33978_PU,
+	MC33978_PD,
+};
+
+enum mc33978_drive_mode {
+	MC33978_OPEN_SOURCE = 0,
+	MC33978_OPEN_DRAIN,
+};
+
+struct mc33978_pinctrl {
+	struct device *dev;
+	struct regmap *regmap;
+
+	struct irq_domain *domain;
+
+	struct gpio_chip chip;
+	struct pinctrl_dev *pctldev;
+	struct pinctrl_desc pinctrl_desc;
+
+	/*
+	 * Protects multi-register hardware sequences in .set() and atomic
+	 * READ_IN + CONFIG reads in .get()
+	 */
+	struct mutex lock;
+
+	enum mc33978_bias_state cached_bias[MC33978_NGPIO];
+	enum mc33978_drive_mode cached_drive[MC33978_NGPIO];
+};
+
+static const struct pinctrl_pin_desc mc33978_pins[] = {
+	PINCTRL_PIN(0, "sg0"),
+	PINCTRL_PIN(1, "sg1"),
+	PINCTRL_PIN(2, "sg2"),
+	PINCTRL_PIN(3, "sg3"),
+	PINCTRL_PIN(4, "sg4"),
+	PINCTRL_PIN(5, "sg5"),
+	PINCTRL_PIN(6, "sg6"),
+	PINCTRL_PIN(7, "sg7"),
+	PINCTRL_PIN(8, "sg8"),
+	PINCTRL_PIN(9, "sg9"),
+	PINCTRL_PIN(10, "sg10"),
+	PINCTRL_PIN(11, "sg11"),
+	PINCTRL_PIN(12, "sg12"),
+	PINCTRL_PIN(13, "sg13"),
+	PINCTRL_PIN(14, "sp0"),
+	PINCTRL_PIN(15, "sp1"),
+	PINCTRL_PIN(16, "sp2"),
+	PINCTRL_PIN(17, "sp3"),
+	PINCTRL_PIN(18, "sp4"),
+	PINCTRL_PIN(19, "sp5"),
+	PINCTRL_PIN(20, "sp6"),
+	PINCTRL_PIN(21, "sp7"),
+};
+
+static inline bool mc33978_is_sp(unsigned int pin)
+{
+	return pin >= MC33978_NUM_SG;
+}
+
+/* Choose register offset for _SG/_SP registers. reg is always the _SP addr. */
+static inline u8 mc33978_spsg(u8 reg, unsigned int pin)
+{
+	return mc33978_is_sp(pin) ? reg : reg + 2;
+}
+
+/* Get the bit index into the corresponding register */
+static inline unsigned int mc33978_pinshift(unsigned int pin)
+{
+	return mc33978_is_sp(pin) ? pin - MC33978_NUM_SG : pin;
+}
+
+#define MC33978_PINMASK(pin)	BIT(mc33978_pinshift(pin))
+
+/*
+ * Wetting current registers: 3 in total, each pin uses a 3-bit field,
+ * 8 pins per register, except for the last one.
+ */
+static inline u8 mc33978_wreg(u8 reg, unsigned int pin)
+{
+	return reg + (mc33978_is_sp(pin) ? 0 : 2 + 2 * (pin / 8));
+}
+
+static inline unsigned int mc33978_wshift(unsigned int pin)
+{
+	return mc33978_is_sp(pin) ? 3 * (pin - MC33978_NUM_SG) : 3 * (pin % 8);
+}
+
+#define MC33978_WMASK(pin)	(7 << mc33978_wshift(pin))
+
+static int mc33978_read(struct mc33978_pinctrl *mpc, u8 reg, u32 *val)
+{
+	int ret;
+
+	ret = regmap_read(mpc->regmap, reg, val);
+	if (ret)
+		dev_err_ratelimited(mpc->dev, "Regmap read error %d at reg: %02x.\n",
+				    ret, reg);
+	return ret;
+}
+
+static int mc33978_update_bits(struct mc33978_pinctrl *mpc, u8 reg, u32 mask,
+			       u32 val)
+{
+	int ret;
+
+	ret = regmap_update_bits(mpc->regmap, reg, mask, val);
+	if (ret)
+		dev_err_ratelimited(mpc->dev, "Regmap update bits error %d at reg: %02x.\n",
+				    ret, reg);
+	return ret;
+}
+
+static const struct pinctrl_ops mc33978_pinctrl_ops = {
+	.dt_node_to_map = pinconf_generic_dt_node_to_map_pin,
+	.dt_free_map = pinconf_generic_dt_free_map,
+};
+
+/*
+ * Output Drive Emulation:
+ * The MC33978 is an input device and physically lacks output drivers.
+ * We emulate output drive modes by mapping them to the hardware's wetting
+ * current topologies:
+ * - Open-Source : Maps to Switch-to-Ground (Pull-Up) topology.
+ * - Open-Drain  : Maps to Switch-to-Battery (Pull-Down) topology.
+ *
+ * SG pins (0-13) are physically hardwired to Open-Source.
+ * SP pins (14-21) are configurable via the CONFIG register.
+ */
+static int mc33978_get_drive(struct mc33978_pinctrl *mpc, unsigned int pin,
+			     enum mc33978_drive_mode *mode)
+{
+	u32 data;
+	int ret;
+
+	lockdep_assert_held(&mpc->lock);
+
+	if (!mc33978_is_sp(pin)) {
+		*mode = MC33978_OPEN_SOURCE;
+		return 0;
+	}
+
+	ret = mc33978_read(mpc, MC33978_REG_CONFIG, &data);
+	if (ret)
+		return ret;
+
+	if (data & MC33978_PINMASK(pin))
+		*mode = MC33978_OPEN_DRAIN;
+	else
+		*mode = MC33978_OPEN_SOURCE;
+
+	return 0;
+}
+
+static int mc33978_set_drive(struct mc33978_pinctrl *mpc, unsigned int pin,
+			     enum mc33978_drive_mode mode)
+{
+	u32 mask = MC33978_PINMASK(pin);
+	int ret;
+
+	lockdep_assert_held(&mpc->lock);
+
+	if (!mc33978_is_sp(pin)) {
+		if (mode != MC33978_OPEN_SOURCE)
+			return -EINVAL;
+
+		mpc->cached_drive[pin] = MC33978_OPEN_SOURCE;
+		return 0;
+	}
+
+	ret = mc33978_update_bits(mpc, MC33978_REG_CONFIG, mask,
+				  (mode == MC33978_OPEN_DRAIN) ? mask : 0);
+	if (!ret)
+		mpc->cached_drive[pin] = mode;
+
+	return ret;
+}
+
+/*
+ * Input Bias (Pull) Configuration:
+ * The MC33978 physically lacks passive pull-up/down resistors. Instead,
+ * input bias is achieved using active wetting current sources. We map the
+ * standard pinctrl bias terms to these hardware topologies:
+ * - Pull-Up   : Maps to Switch-to-Ground (SG) topology.
+ * - Pull-Down : Maps to Switch-to-Battery (SB) topology.
+ * - Tri-state : Disables the wetting current (High-Z).
+ *
+ * SG pins (0-13) physically only support Pull-Up.
+ * SP pins (14-21) are configurable via the CONFIG register.
+ * The TRI_SP/TRI_SG registers connect or disconnect these currents.
+ */
+static int mc33978_get_pull(struct mc33978_pinctrl *mpc, unsigned int pin,
+			    enum mc33978_bias_state *val)
+{
+	u32 data;
+	int ret;
+
+	lockdep_assert_held(&mpc->lock);
+
+	ret = mc33978_read(mpc, mc33978_spsg(MC33978_REG_TRI_SP, pin), &data);
+	if (ret)
+		return ret;
+
+	/* Is the pin tri-stated? */
+	if (data & MC33978_PINMASK(pin)) {
+		*val = MC33978_TRISTATE;
+		return 0;
+	}
+
+	/* Pins 0..13 only support pull-up */
+	if (!mc33978_is_sp(pin)) {
+		*val = MC33978_PU;
+		return 0;
+	}
+
+	/* Check pin pull direction for pins 14..21 */
+	ret = mc33978_read(mpc, MC33978_REG_CONFIG, &data);
+	if (ret)
+		return ret;
+
+	if (data & MC33978_PINMASK(pin))
+		*val = MC33978_PD;
+	else
+		*val = MC33978_PU;
+
+	return 0;
+}
+
+static int mc33978_set_pull(struct mc33978_pinctrl *mpc, unsigned int pin,
+			    enum mc33978_bias_state val)
+{
+	u32 mask = MC33978_PINMASK(pin);
+	int ret;
+
+	lockdep_assert_held(&mpc->lock);
+
+	/* SG pins physically lack pull-down current sources */
+	if (val == MC33978_PD && !mc33978_is_sp(pin))
+		return -EINVAL;
+
+	/* Configure direction (Exclusively for SP pins) */
+	if (mc33978_is_sp(pin) && val != MC33978_TRISTATE) {
+		ret = mc33978_update_bits(mpc, MC33978_REG_CONFIG, mask,
+					  (val == MC33978_PD) ? mask : 0);
+		if (ret)
+			return ret;
+	}
+
+	/* Enable current source or set to tri-state  */
+	ret = mc33978_update_bits(mpc, mc33978_spsg(MC33978_REG_TRI_SP, pin),
+				   mask,
+				   (val == MC33978_TRISTATE) ? mask : 0);
+	if (!ret)
+		mpc->cached_bias[pin] = val;
+
+	return ret;
+}
+
+static const unsigned int mc33978_wet_mA[] = { 2, 6, 8, 10, 12, 14, 16, 20 };
+
+static int mc33978_set_ds(struct mc33978_pinctrl *mpc, unsigned int pin,
+			  u32 val)
+{
+	int i;
+
+	for (i = 0; i < ARRAY_SIZE(mc33978_wet_mA); i++) {
+		if (val == mc33978_wet_mA[i]) {
+			return mc33978_update_bits(mpc,
+					mc33978_wreg(MC33978_REG_WET_SP, pin),
+					MC33978_WMASK(pin),
+					i << mc33978_wshift(pin));
+		}
+	}
+
+	return -EINVAL;
+}
+
+static int mc33978_get_ds(struct mc33978_pinctrl *mpc, unsigned int pin,
+			  u32 *val)
+{
+	u32 data;
+	int ret;
+
+	ret = mc33978_read(mpc, mc33978_wreg(MC33978_REG_WET_SP, pin), &data);
+	if (ret)
+		return ret;
+
+	data &= MC33978_WMASK(pin);
+	data >>= mc33978_wshift(pin);
+
+	if (data >= ARRAY_SIZE(mc33978_wet_mA))
+		return -EINVAL;
+
+	*val = mc33978_wet_mA[data];
+
+	return 0;
+}
+
+static int mc33978_pinconf_get(struct pinctrl_dev *pctldev, unsigned int pin,
+			       unsigned long *config)
+{
+	struct mc33978_pinctrl *mpc = pinctrl_dev_get_drvdata(pctldev);
+	enum pin_config_param param = pinconf_to_config_param(*config);
+	enum mc33978_bias_state bias;
+	enum mc33978_drive_mode drive;
+	u32 arg, data;
+	int ret;
+
+	guard(mutex)(&mpc->lock);
+
+	switch (param) {
+	case PIN_CONFIG_BIAS_PULL_UP:
+		ret = mc33978_get_pull(mpc, pin, &bias);
+		if (ret)
+			return ret;
+		if (bias != MC33978_PU)
+			return -EINVAL;
+		arg = 0;
+		break;
+	case PIN_CONFIG_BIAS_PULL_DOWN:
+		ret = mc33978_get_pull(mpc, pin, &bias);
+		if (ret)
+			return ret;
+		if (bias != MC33978_PD)
+			return -EINVAL;
+		arg = 0;
+		break;
+	case PIN_CONFIG_DRIVE_OPEN_DRAIN:
+		ret = mc33978_get_drive(mpc, pin, &drive);
+		if (ret)
+			return ret;
+		if (drive != MC33978_OPEN_DRAIN)
+			return -EINVAL;
+		arg = 1;
+		break;
+	case PIN_CONFIG_DRIVE_OPEN_SOURCE:
+		ret = mc33978_get_drive(mpc, pin, &drive);
+		if (ret)
+			return ret;
+		if (drive != MC33978_OPEN_SOURCE)
+			return -EINVAL;
+		arg = 1;
+		break;
+	case PIN_CONFIG_BIAS_DISABLE:
+	case PIN_CONFIG_BIAS_HIGH_IMPEDANCE:
+		ret = mc33978_get_pull(mpc, pin, &bias);
+		if (ret)
+			return ret;
+		if (bias != MC33978_TRISTATE)
+			return -EINVAL;
+		arg = 1;
+		break;
+	case PIN_CONFIG_DRIVE_STRENGTH:
+		ret = mc33978_get_ds(mpc, pin, &data);
+		if (ret)
+			return ret;
+		arg = data;
+		break;
+	default:
+		/*
+		 * Ignore checkpatch warning: the pinctrl core specifically
+		 * expects -ENOTSUPP to silently skip unsupported generic
+		 * parameters. Using -EOPNOTSUPP causes debugfs read failures.
+		 */
+		return -ENOTSUPP;
+	}
+
+	*config = pinconf_to_config_packed(param, arg);
+
+	return 0;
+}
+
+/*
+ * Hardware constraint regarding PIN_CONFIG_BIAS_PULL_UP/DOWN:
+ * The MC33978 utilizes active constant current sources (wetting currents)
+ * rather than passive pull-resistors. Since the equivalent ohmic resistance
+ * scales dynamically with the fluctuating board voltage (VBATP), computing
+ * a static ohm value is physically invalid.
+ * The driver intentionally ignores resistance arguments during configuration
+ * and continuously reports 0 ohms to the pinctrl framework.
+ */
+static int mc33978_pinconf_set(struct pinctrl_dev *pctldev, unsigned int pin,
+			       unsigned long *configs, unsigned int num_configs)
+{
+	struct mc33978_pinctrl *mpc = pinctrl_dev_get_drvdata(pctldev);
+	enum pin_config_param param;
+	int ret = 0;
+	u32 arg;
+	int i;
+
+	guard(mutex)(&mpc->lock);
+
+	for (i = 0; i < num_configs; i++) {
+		param = pinconf_to_config_param(configs[i]);
+		arg = pinconf_to_config_argument(configs[i]);
+
+		/*
+		 * The hardware physically lacks push-pull output drivers.
+		 * By explicitly handling OPEN_DRAIN and OPEN_SOURCE here, we
+		 * signal to gpiolib that we support these modes natively.
+		 *
+		 * This prevents gpiolib from falling back to its software
+		 * emulation, which attempts to achieve High-Z by switching the
+		 * pin to input mode. Because this driver's .direction_input()
+		 * explicitly restores the cached bias, gpiolib's emulation
+		 * would inadvertently turn the wetting current back on instead
+		 * of achieving a High-Z state.
+		 */
+		switch (param) {
+		case PIN_CONFIG_DRIVE_OPEN_SOURCE:
+			ret = mc33978_set_drive(mpc, pin, MC33978_OPEN_SOURCE);
+			break;
+		case PIN_CONFIG_BIAS_PULL_UP:
+			ret = mc33978_set_pull(mpc, pin, MC33978_PU);
+			break;
+		case PIN_CONFIG_DRIVE_OPEN_DRAIN:
+			ret = mc33978_set_drive(mpc, pin, MC33978_OPEN_DRAIN);
+			break;
+		case PIN_CONFIG_BIAS_PULL_DOWN:
+			ret = mc33978_set_pull(mpc, pin, MC33978_PD);
+			break;
+		/*
+		 * The MC33978 uses active wetting currents rather than passive
+		 * pull-resistors. Disabling the bias (pull-up/down) is
+		 * physically equivalent to putting the pin into a
+		 * high-impedance state. Both actions are achieved by isolating
+		 * the pin via the hardware tri-state registers.
+		 */
+		case PIN_CONFIG_BIAS_DISABLE:
+		case PIN_CONFIG_BIAS_HIGH_IMPEDANCE:
+			ret = mc33978_set_pull(mpc, pin, MC33978_TRISTATE);
+			break;
+		case PIN_CONFIG_DRIVE_STRENGTH_UA:
+			arg /= 1000;
+			fallthrough;
+		case PIN_CONFIG_DRIVE_STRENGTH:
+			ret = mc33978_set_ds(mpc, pin, arg);
+			break;
+		default:
+			/*
+			 * Required by the pinctrl core to safely fall back or
+			 * skip unsupported configs. Do not use -EOPNOTSUPP.
+			 */
+			return -ENOTSUPP;
+		}
+
+		if (ret) {
+			dev_err(mpc->dev, "Failed to set config param %04x for pin %u: %d\n",
+				param, pin, ret);
+			return ret;
+		}
+	}
+
+	return 0;
+}
+
+static const struct pinconf_ops mc33978_pinconf_ops = {
+	.pin_config_get = mc33978_pinconf_get,
+	.pin_config_set = mc33978_pinconf_set,
+	.is_generic = true,
+};
+
+static int mc33978_direction_input(struct gpio_chip *chip, unsigned int offset)
+{
+	struct mc33978_pinctrl *mpc = gpiochip_get_data(chip);
+
+	guard(mutex)(&mpc->lock);
+
+	/*
+	 * The MC33978 lacks physical output drivers; we emulate outputs by
+	 * toggling the hardware tri-state registers to connect or disconnect
+	 * the wetting currents.
+	 *
+	 * A valid input on this chip can be either actively wetted (PU/PD)
+	 * or High-Z (tri-stated). Because our output emulation directly
+	 * hijacks the tri-state register, switching back to input mode requires
+	 * us to explicitly restore the original wetting current topology from
+	 * the cache.
+	 */
+	return mc33978_set_pull(mpc, offset, mpc->cached_bias[offset]);
+}
+
+/*
+ * The hardware evaluates pin voltage against a threshold (default 4.0V)
+ * and reports an abstract contact status (1 = closed, 0 = open):
+ *
+ * SG (Switch-to-Ground) topology (pull-up current source):
+ * - Voltage > Threshold: Switch Open   (HW reports 0) -> Physical High
+ * - Voltage < Threshold: Switch Closed (HW reports 1) -> Physical Low
+ *
+ * SB (Switch-to-Battery) topology (pull-down current source):
+ * - Voltage > Threshold: Switch Closed (HW reports 1) -> Physical High
+ * - Voltage < Threshold: Switch Open   (HW reports 0) -> Physical Low
+ *
+ * We translate this contact status back into physical voltage levels by
+ * inverting the hardware status for all pins operating in SG topology.
+ */
+static int mc33978_read_in_state(struct mc33978_pinctrl *mpc,
+				 unsigned long mask, unsigned long *state)
+{
+	u32 status, inv_mask;
+	u32 config_reg = 0;
+	int ret;
+
+	ret = mc33978_read(mpc, MC33978_REG_READ_IN, &status);
+	if (ret)
+		return ret;
+
+	/* Read CONFIG register only if the requested mask involves SP pins */
+	if (mask & MC33978_SP_MASK) {
+		ret = mc33978_read(mpc, MC33978_REG_CONFIG, &config_reg);
+		if (ret)
+			return ret;
+	}
+
+	/*
+	 * Create an inversion mask for all pins currently operating in
+	 * Switch-to-Ground (SG) topology. SG pins always have pull-ups.
+	 * For SP pins, CONFIG bit value 0 = Switch-to-Ground (PU),
+	 * CONFIG bit value 1 = Switch-to-Battery (PD).
+	 */
+	inv_mask = MC33978_SG_MASK |
+		   (~(config_reg << MC33978_NUM_SG) & MC33978_SP_MASK);
+
+	*state = (status ^ inv_mask) & mask;
+
+	return 0;
+}
+
+static int mc33978_get(struct gpio_chip *chip, unsigned int offset)
+{
+	struct mc33978_pinctrl *mpc = gpiochip_get_data(chip);
+	unsigned long state;
+	int ret;
+
+	guard(mutex)(&mpc->lock);
+
+	ret = mc33978_read_in_state(mpc, BIT(offset), &state);
+	if (ret)
+		return ret;
+
+	return !!(state & BIT(offset));
+}
+
+static int mc33978_get_multiple(struct gpio_chip *chip,
+				unsigned long *mask, unsigned long *bits)
+{
+	struct mc33978_pinctrl *mpc = gpiochip_get_data(chip);
+	unsigned long state;
+	int ret;
+
+	guard(mutex)(&mpc->lock);
+
+	ret = mc33978_read_in_state(mpc, *mask, &state);
+	if (ret)
+		return ret;
+
+	*bits = (*bits & ~*mask) | state;
+
+	return 0;
+}
+
+/*
+ * Emulate output states by routing or isolating active wetting currents.
+ * To turn the line ON, we disable the hardware tri-state (write 0).
+ * To turn the line OFF (High-Z), we enable tri-state (write 1).
+ *
+ * For Open-Source (Pull-Up): value=1 turns it ON, value=0 is High-Z.
+ * For Open-Drain (Pull-Down): value=0 turns it ON, value=1 is High-Z.
+ * We dynamically read the CONFIG register to determine the topology
+ * and invert the bits accordingly for Open-Drain pins.
+ *
+ * Note: The hardware physically lacks push-pull drivers. Toggling outputs
+ * via tri-state isolation may cause transient spikes.
+ */
+static int mc33978_update_tri_state(struct mc33978_pinctrl *mpc, u32 mask,
+				    u32 bits)
+{
+	u32 sgmask = (mask & MC33978_SG_MASK) >> MC33978_SG_SHIFT;
+	u32 sgbits = (bits & MC33978_SG_MASK) >> MC33978_SG_SHIFT;
+	u32 spmask = (mask & MC33978_SP_MASK) >> MC33978_SP_SHIFT;
+	u32 spbits = (bits & MC33978_SP_MASK) >> MC33978_SP_SHIFT;
+	u32 config_reg = 0;
+	int ret = 0;
+
+	if (spmask) {
+		/* Read topology: 1 = PD (Open-Drain), 0 = PU (Open-Source) */
+		ret = mc33978_read(mpc, MC33978_REG_CONFIG, &config_reg);
+		if (ret)
+			return ret;
+
+		/*
+		 * Invert bits for Open-Drain (PD) pins.
+		 * The Open-Drain API contract expects value=1 to be High-Z.
+		 */
+		spbits ^= (config_reg & spmask);
+
+		ret = mc33978_update_bits(mpc, MC33978_REG_TRI_SP, spmask,
+					  ~spbits);
+		if (ret)
+			return ret;
+	}
+
+	/* SG pins are always Pull-Up (Open-Source), no inversion needed */
+	if (sgmask)
+		ret = mc33978_update_bits(mpc, MC33978_REG_TRI_SG, sgmask,
+					  ~sgbits);
+
+	return ret;
+}
+
+static int mc33978_set(struct gpio_chip *chip, unsigned int offset, int value)
+{
+	struct mc33978_pinctrl *mpc = gpiochip_get_data(chip);
+	u32 mask = BIT(offset);
+	u32 bits = value ? mask : 0;
+
+	guard(mutex)(&mpc->lock);
+
+	return mc33978_update_tri_state(mpc, mask, bits);
+}
+
+static int mc33978_set_multiple(struct gpio_chip *chip,
+				unsigned long *mask, unsigned long *bits)
+{
+	struct mc33978_pinctrl *mpc = gpiochip_get_data(chip);
+
+	guard(mutex)(&mpc->lock);
+
+	return mc33978_update_tri_state(mpc, *mask, *bits);
+}
+
+static int mc33978_direction_output(struct gpio_chip *chip, unsigned int offset,
+				    int value)
+{
+	struct mc33978_pinctrl *mpc = gpiochip_get_data(chip);
+	u32 mask = BIT(offset);
+	u32 bits = value ? mask : 0;
+	int ret;
+
+	guard(mutex)(&mpc->lock);
+
+	ret = mc33978_set_drive(mpc, offset, mpc->cached_drive[offset]);
+	if (ret)
+		return ret;
+
+	return mc33978_update_tri_state(mpc, mask, bits);
+}
+
+static int mc33978_gpio_child_to_parent_hwirq(struct gpio_chip *gc,
+					      unsigned int child,
+					      unsigned int child_type,
+					      unsigned int *parent,
+					      unsigned int *parent_type)
+{
+	*parent_type = child_type;
+	*parent = child;
+
+	return 0;
+}
+
+/*
+ * Defensive wrappers for hierarchical IRQ proxying.
+ *
+ * gpiolib's hierarchical allocation exposes a lifecycle gap: the child
+ * descriptor is registered before irq_domain_alloc_irqs_parent() fully
+ * instantiates the parent chip.
+ *
+ * During consumer probe (e.g., gpiod_to_irq()), irq_create_fwspec_mapping()
+ * allocates the hierarchy. As part of this, irq_domain_set_info() initializes
+ * the top-level irq_desc and calls __irq_set_handler(). If the irq_desc
+ * requires locking, __irq_get_desc_lock() will invoke the child's
+ * .irq_bus_lock before the parent allocation is complete.
+ *
+ * Upstream generic helpers (e.g., irq_chip_mask_parent) blindly dereference
+ * data->parent_data->chip, causing an immediate NULL pointer panic during
+ * this gap. These wrappers check for a valid parent chip to safely drop
+ * premature locking or masking events while the legacy subsystem hierarchy
+ * is still assembling itself.
+ */
+static void mc33978_gpio_irq_mask(struct irq_data *data)
+{
+	struct gpio_chip *gc = irq_data_get_irq_chip_data(data);
+	struct irq_data *parent = data->parent_data;
+
+	if (parent && parent->chip && parent->chip->irq_mask)
+		parent->chip->irq_mask(parent);
+	gpiochip_disable_irq(gc, data->hwirq);
+}
+
+static void mc33978_gpio_irq_unmask(struct irq_data *data)
+{
+	struct gpio_chip *gc = irq_data_get_irq_chip_data(data);
+	struct irq_data *parent = data->parent_data;
+
+	gpiochip_enable_irq(gc, data->hwirq);
+	if (parent && parent->chip && parent->chip->irq_unmask)
+		parent->chip->irq_unmask(parent);
+}
+
+static int mc33978_gpio_irq_set_type(struct irq_data *data, unsigned int type)
+{
+	struct irq_data *parent = data->parent_data;
+
+	if (parent && parent->chip && parent->chip->irq_set_type)
+		return parent->chip->irq_set_type(parent, type);
+
+	return -EINVAL;
+}
+
+static void mc33978_gpio_irq_bus_lock(struct irq_data *data)
+{
+	struct irq_data *parent = data->parent_data;
+
+	if (parent && parent->chip && parent->chip->irq_bus_lock)
+		parent->chip->irq_bus_lock(parent);
+}
+
+static void mc33978_gpio_irq_bus_sync_unlock(struct irq_data *data)
+{
+	struct irq_data *parent = data->parent_data;
+
+	if (parent && parent->chip && parent->chip->irq_bus_sync_unlock)
+		parent->chip->irq_bus_sync_unlock(parent);
+}
+
+static const struct irq_chip mc33978_gpio_irqchip = {
+	.name = "mc33978-gpio",
+	.irq_mask = mc33978_gpio_irq_mask,
+	.irq_unmask = mc33978_gpio_irq_unmask,
+	.irq_set_type = mc33978_gpio_irq_set_type,
+	.irq_bus_lock = mc33978_gpio_irq_bus_lock,
+	.irq_bus_sync_unlock = mc33978_gpio_irq_bus_sync_unlock,
+	.irq_set_wake = irq_chip_set_wake_parent,
+	.flags = IRQCHIP_IMMUTABLE,
+	GPIOCHIP_IRQ_RESOURCE_HELPERS,
+};
+
+static void mc33978_init_gpio_chip(struct mc33978_pinctrl *mpc,
+				   struct device *dev)
+{
+	struct gpio_irq_chip *girq;
+
+	mpc->chip.label = dev_name(dev);
+	mpc->chip.direction_input = mc33978_direction_input;
+	mpc->chip.get = mc33978_get;
+	mpc->chip.get_multiple = mc33978_get_multiple;
+	mpc->chip.direction_output = mc33978_direction_output;
+	mpc->chip.set = mc33978_set;
+	mpc->chip.set_multiple = mc33978_set_multiple;
+	mpc->chip.set_config = gpiochip_generic_config;
+
+	mpc->chip.base = -1;
+	mpc->chip.ngpio = MC33978_NGPIO;
+	mpc->chip.can_sleep = true;
+	mpc->chip.parent = dev;
+	mpc->chip.owner = THIS_MODULE;
+
+	girq = &mpc->chip.irq;
+	gpio_irq_chip_set_chip(girq, &mc33978_gpio_irqchip);
+	/*
+	 * Share parent's DT fwnode. This does NOT cause IRQ domain shadowing
+	 * because the parent MFD domain uses DOMAIN_BUS_NEXUS while this GPIO
+	 * domain will use DOMAIN_BUS_WIRED (set after gpiochip registration).
+	 * Domain lookups match on both fwnode AND bus_token, ensuring proper
+	 * domain isolation. See crystalcove GPIO driver for similar pattern.
+	 */
+	girq->fwnode = dev_fwnode(dev);
+	girq->parent_domain = mpc->domain;
+	girq->child_to_parent_hwirq = mc33978_gpio_child_to_parent_hwirq;
+	girq->handler = handle_simple_irq;
+	girq->default_type = IRQ_TYPE_NONE;
+}
+
+static void mc33978_init_pinctrl_desc(struct mc33978_pinctrl *mpc,
+				      struct device *dev)
+{
+	mpc->pinctrl_desc.name = dev_name(dev);
+
+	mpc->pinctrl_desc.pctlops = &mc33978_pinctrl_ops;
+	mpc->pinctrl_desc.confops = &mc33978_pinconf_ops;
+	mpc->pinctrl_desc.pins = mc33978_pins;
+	mpc->pinctrl_desc.npins = MC33978_NGPIO;
+	mpc->pinctrl_desc.owner = THIS_MODULE;
+}
+
+static int mc33978_init_cached_bias(struct mc33978_pinctrl *mpc)
+{
+	int i;
+
+	guard(mutex)(&mpc->lock);
+
+	for (i = 0; i < MC33978_NGPIO; i++) {
+		enum mc33978_bias_state val;
+		int ret;
+
+		ret = mc33978_get_pull(mpc, i, &val);
+		if (ret)
+			return ret;
+
+		mpc->cached_bias[i] = val;
+	}
+
+	return 0;
+}
+
+static int mc33978_init_cached_drive(struct mc33978_pinctrl *mpc)
+{
+	int i;
+
+	guard(mutex)(&mpc->lock);
+
+	for (i = 0; i < MC33978_NGPIO; i++) {
+		enum mc33978_drive_mode mode;
+		int ret;
+
+		ret = mc33978_get_drive(mpc, i, &mode);
+		if (ret)
+			return ret;
+
+		mpc->cached_drive[i] = mode;
+	}
+
+	return 0;
+}
+
+static int mc33978_pinctrl_probe(struct platform_device *pdev)
+{
+	struct device *dev = &pdev->dev;
+	struct fwnode_handle *fwnode;
+	struct mc33978_pinctrl *mpc;
+	int ret;
+
+	fwnode = dev_fwnode(dev->parent);
+	if (!fwnode)
+		return dev_err_probe(dev, -ENODEV,
+				     "Missing parent firmware node\n");
+
+	device_set_node(dev, fwnode);
+
+	mpc = devm_kzalloc(dev, sizeof(*mpc), GFP_KERNEL);
+	if (!mpc)
+		return -ENOMEM;
+
+	mpc->dev = dev;
+
+	mpc->regmap = dev_get_regmap(dev->parent, NULL);
+	if (!mpc->regmap)
+		return dev_err_probe(dev, -ENODEV, "Failed to get parent regmap\n");
+
+	/* Find parent MFD IRQ domain (uses DOMAIN_BUS_NEXUS token) */
+	mpc->domain = irq_find_matching_fwnode(fwnode, DOMAIN_BUS_NEXUS);
+	if (!mpc->domain)
+		return dev_err_probe(dev, -ENODEV, "Failed to find parent IRQ domain\n");
+
+	ret = devm_mutex_init(dev, &mpc->lock);
+	if (ret)
+		return ret;
+
+	ret = mc33978_init_cached_bias(mpc);
+	if (ret)
+		return ret;
+
+	ret = mc33978_init_cached_drive(mpc);
+	if (ret)
+		return ret;
+
+	mc33978_init_gpio_chip(mpc, dev);
+	mc33978_init_pinctrl_desc(mpc, dev);
+
+	mpc->pctldev = devm_pinctrl_register(dev, &mpc->pinctrl_desc, mpc);
+	if (IS_ERR(mpc->pctldev))
+		return dev_err_probe(dev, PTR_ERR(mpc->pctldev),
+				     "can't register pinctrl\n");
+
+	ret = devm_gpiochip_add_data(dev, &mpc->chip, mpc);
+	if (ret)
+		return dev_err_probe(dev, ret, "can't add GPIO chip\n");
+
+	/*
+	 * Distinguish GPIO IRQ domain from parent MFD domain sharing the same
+	 * fwnode. Matches the pattern used by other GPIO drivers (e.g.,
+	 * crystalcove). DOMAIN_BUS_WIRED indicates this domain represents
+	 * actual GPIO pin interrupts (wired lines).
+	 */
+	irq_domain_update_bus_token(mpc->chip.irq.domain, DOMAIN_BUS_WIRED);
+
+	ret = gpiochip_add_pin_range(&mpc->chip, dev_name(dev), 0, 0,
+				     MC33978_NGPIO);
+	if (ret)
+		return dev_err_probe(dev, ret, "failed to add pin range\n");
+
+	platform_set_drvdata(pdev, mpc);
+
+	return 0;
+}
+
+static const struct platform_device_id mc33978_pinctrl_id[] = {
+	{ "mc33978-pinctrl", },
+	{ "mc34978-pinctrl", },
+	{ }
+};
+MODULE_DEVICE_TABLE(platform, mc33978_pinctrl_id);
+
+static struct platform_driver mc33978_pinctrl_driver = {
+	.driver = {
+		.name = "mc33978-pinctrl",
+	},
+	.probe = mc33978_pinctrl_probe,
+	.id_table = mc33978_pinctrl_id,
+};
+module_platform_driver(mc33978_pinctrl_driver);
+
+MODULE_AUTHOR("David Jander <david@protonic.nl>");
+MODULE_DESCRIPTION("NXP MC33978/MC34978 pinctrl driver");
+MODULE_LICENSE("GPL");
-- 
2.47.3


^ permalink raw reply related

* [PATCH v10 5/6] hwmon: add NXP MC33978/MC34978 driver
From: Oleksij Rempel @ 2026-04-01 16:25 UTC (permalink / raw)
  To: Guenter Roeck, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
	Lee Jones, Peter Rosin, Linus Walleij
  Cc: Oleksij Rempel, kernel, linux-kernel, devicetree, linux-hwmon,
	linux-gpio, David Jander
In-Reply-To: <20260401162510.1706073-1-o.rempel@pengutronix.de>

Add hardware monitoring support for the NXP MC33978/MC34978 Multiple
Switch Detection Interface (MSDI).

The hardware utilizes a clear-on-read FAULT register, but physical
faults remain asserted as long as the underlying condition exists. This
asserts a global FAULT_STAT bit on the SPI bus. To handle this without
trapping the CPU in an interrupt storm, this driver implements the
following architecture:
- Requests a rising-edge nested IRQ (IRQF_TRIGGER_RISING) from the MFD
  core to catch the initial 0 -> 1 transition of the global fault state.
- Caches hwmon-specific alarm bits and calculates state edges (XOR) to
  isolate alarm transitions from system integrity faults.
- Implements a 1Hz delayed workqueue that polls the hardware as long as
  any alarm is active. This compensates for the edge-triggered IRQ by
  discovering secondary faults that occur without a rising edge, and
  detecting when the hardware clears.

Signed-off-by: Oleksij Rempel <o.rempel@pengutronix.de>
Acked-by: Guenter Roeck <linux@roeck-us.net>
---
changes v10:
- Use freezable polling
changes v9:
- add Acked-by: Guenter Roeck <linux@roeck-us.net>
changes v8:
- no changes
changes v7:
- Fix fault monitoring stall by unconditionally rearming on SPI read
  errors.
- Fix use-after-free race during unbind by correcting devm registration
  order.
changes v6:
- Protect clear-on-read FAULT register and state updates with hwmon_lock().
- Isolate hwmon alarm bits from system integrity bits to fix edge detection.
- Log system faults (SPI/HASH) as level-triggered and add temperature warning
  logs.
- Refactor sysfs read callback into smaller subsystem-specific helpers.
- Fix probe race condition by calling mc33978_hwmon_update_faults() at the end
  of probe instead of reading raw faults early.
- Expose static datasheet temperature limits via temp1_rated_min and
  temp1_rated_max
- Introduce variant-specific hw_info data to correctly report the max
  temperature
- Add a 1Hz delayed workqueue that polls the SPI bus while any alarm is active.
changes v5:
- no changes
changes v4:
- no changes
changes v3:
- no changes
changes v2:
- Switch from OF match table to platform_device_id
---
 drivers/hwmon/Kconfig         |  10 +
 drivers/hwmon/Makefile        |   1 +
 drivers/hwmon/mc33978-hwmon.c | 549 ++++++++++++++++++++++++++++++++++
 3 files changed, 560 insertions(+)
 create mode 100644 drivers/hwmon/mc33978-hwmon.c

diff --git a/drivers/hwmon/Kconfig b/drivers/hwmon/Kconfig
index 328867242cb3..0c52e8268a20 100644
--- a/drivers/hwmon/Kconfig
+++ b/drivers/hwmon/Kconfig
@@ -700,6 +700,16 @@ config SENSORS_MC13783_ADC
         help
           Support for the A/D converter on MC13783 and MC13892 PMIC.
 
+config SENSORS_MC33978
+	tristate "NXP MC33978/MC34978 fault monitoring"
+	depends on MFD_MC33978
+	help
+	  If you say yes here you get fault monitoring support for the
+	  NXP MC33978/MC34978 Multiple Switch Detection Interface (MSDI).
+
+	  This driver can also be built as a module. If so, the module
+	  will be called mc33978-hwmon.
+
 config SENSORS_MC33XS2410
 	tristate "MC33XS2410 HWMON support"
 	depends on PWM_MC33XS2410
diff --git a/drivers/hwmon/Makefile b/drivers/hwmon/Makefile
index 5833c807c688..4c3db5433a10 100644
--- a/drivers/hwmon/Makefile
+++ b/drivers/hwmon/Makefile
@@ -167,6 +167,7 @@ obj-$(CONFIG_SENSORS_MAX31790)	+= max31790.o
 obj-$(CONFIG_MAX31827) += max31827.o
 obj-$(CONFIG_SENSORS_MAX77705) += max77705-hwmon.o
 obj-$(CONFIG_SENSORS_MC13783_ADC)+= mc13783-adc.o
+obj-$(CONFIG_SENSORS_MC33978)	+= mc33978-hwmon.o
 obj-$(CONFIG_SENSORS_MC33XS2410) += mc33xs2410_hwmon.o
 obj-$(CONFIG_SENSORS_MC34VR500)	+= mc34vr500.o
 obj-$(CONFIG_SENSORS_MCP3021)	+= mcp3021.o
diff --git a/drivers/hwmon/mc33978-hwmon.c b/drivers/hwmon/mc33978-hwmon.c
new file mode 100644
index 000000000000..498ea85064ea
--- /dev/null
+++ b/drivers/hwmon/mc33978-hwmon.c
@@ -0,0 +1,549 @@
+// SPDX-License-Identifier: GPL-2.0-only
+// Copyright (c) 2026 Pengutronix, Oleksij Rempel <kernel@pengutronix.de>
+/*
+ * MC33978/MC34978 Hardware Monitor Driver
+ *
+ * Fault handling model:
+ *
+ * The FAULT register is clear-on-read for most bits, but persistent fault
+ * conditions remain asserted. The MFD core only harvests the aggregate
+ * FAULT_STAT indication from SPI responses and dispatches the hwmon child
+ * IRQ on that basis. Because a persistent fault can keep FAULT_STAT asserted,
+ * secondary fault assertions and fault clear events may not generate a fresh
+ * interrupt edge visible to the hwmon child.
+ *
+ * To provide stable hwmon alarm state, this driver:
+ * - caches only hwmon-relevant alarm bits
+ * - serializes FAULT register reads with cache updates
+ * - polls while any alarm remains active to detect secondary alarms and
+ *   clearing edges
+ *
+ * Raw integrity bits such as SPI_ERROR and HASH are logged, but are not
+ * exported through hwmon alarm attributes.
+ */
+
+#include <linux/device.h>
+#include <linux/err.h>
+#include <linux/hwmon.h>
+#include <linux/interrupt.h>
+#include <linux/mod_devicetable.h>
+#include <linux/module.h>
+#include <linux/platform_device.h>
+#include <linux/regmap.h>
+
+#include <linux/mfd/mc33978.h>
+
+/* Operating Temperature Ranges (Datasheet Rated) */
+#define MC33978_TEMP_MIN_MC		(-40000)
+#define MC33978_TEMP_MAX_MC		125000
+#define MC34978_TEMP_MAX_MC		105000
+
+/* Thermal Warning threshold (~120C) */
+#define MC33978_TEMP_WARN_MC		120000
+
+/* Thermal Limit / tLIM (>155C) - Hardware enters CWET throttling */
+#define MC33978_TEMP_CRIT_MC		155000
+
+/* Hysteresis for tLIM recovery (Silicon must cool to <140C) */
+#define MC33978_TEMP_HYST_MC		15000
+
+/* VBATP (in0) IC Level thresholds */
+#define MC33978_VBATP_OV_MV		36000 /* Overvoltage limit */
+#define MC33978_VBATP_FUNC_MV		28000 /* Functional/Normal boundary */
+#define MC33978_VBATP_DEGRADED_MV	6000 /* Degraded parametrics start */
+#define MC33978_VBATP_UVLO_MV		4500 /* UV Rising Threshold max */
+
+/* VDDQ (in1) Logic Supply thresholds */
+#define MC33978_VDDQ_MAX_MV		5250 /* Operating Condition max */
+#define MC33978_VDDQ_MIN_MV		3000 /* Operating Condition min */
+#define MC33978_VDDQ_UV_MV		2800 /* UV Falling Threshold max */
+
+#define MC33978_FAULT_POLL_INTERVAL_MS	1000
+
+enum mc33978_hwmon_in_channels {
+	MC33978_IN_VBATP,
+	MC33978_IN_VDDQ,
+};
+
+struct mc33978_hwmon_priv {
+	struct device *dev;
+	struct device *hwmon_dev;
+	struct regmap *map;
+
+	const struct mc33978_hwmon_hw_info *hw_info;
+
+	int fault_irq;
+
+	/* Cached hwmon alarm bits, serialized by hwmon_lock(). */
+	u32 last_faults;
+
+	/*
+	 * Background polling worker. Active only when faults are present
+	 * to compensate for the lack of clearing/secondary edge interrupts.
+	 */
+	struct delayed_work poll_work;
+};
+
+struct mc33978_hwmon_hw_info {
+	long rated_max_temp;
+};
+
+static const struct mc33978_hwmon_hw_info hwmon_hwinfo_mc33978 = {
+	.rated_max_temp = MC33978_TEMP_MAX_MC,
+};
+
+static const struct mc33978_hwmon_hw_info hwmon_hwinfo_mc34978 = {
+	.rated_max_temp = MC34978_TEMP_MAX_MC,
+};
+
+static int mc33978_hwmon_read_fault(struct mc33978_hwmon_priv *priv,
+				    u32 *faults)
+{
+	unsigned int val;
+	int ret;
+
+	ret = regmap_read(priv->map, MC33978_REG_FAULT, &val);
+	if (ret)
+		return ret;
+
+	*faults = val;
+
+	return 0;
+}
+
+static void mc33978_hwmon_report_faults(struct mc33978_hwmon_priv *priv,
+					u32 new_faults)
+{
+	if (!new_faults)
+		return;
+
+	if (new_faults & MC33978_FAULT_TEMP_WARN)
+		dev_warn_ratelimited(priv->dev, "Temperature warning threshold reached\n");
+
+	if (new_faults & MC33978_FAULT_OT)
+		dev_crit_ratelimited(priv->dev, "Over-temperature fault detected!\n");
+
+	if (new_faults & MC33978_FAULT_OV)
+		dev_crit_ratelimited(priv->dev, "Over-voltage fault detected!\n");
+
+	if (new_faults & MC33978_FAULT_UV)
+		dev_err_ratelimited(priv->dev, "Under-voltage fault detected!\n");
+}
+
+static int mc33978_hwmon_update_faults(struct mc33978_hwmon_priv *priv)
+{
+	u32 old_faults, new_faults, changed_faults;
+	u32 alarm_faults = 0;
+	u32 faults = 0;
+	bool rearm;
+	int ret;
+
+	/*
+	 * Serialize clear-on-read FAULT register access with cached alarm state
+	 * updates and hwmon sysfs readers.
+	 */
+	hwmon_lock(priv->hwmon_dev);
+	old_faults = priv->last_faults;
+
+	ret = mc33978_hwmon_read_fault(priv, &faults);
+	if (ret) {
+		hwmon_unlock(priv->hwmon_dev);
+		dev_err_ratelimited(priv->dev,
+				    "failed to read fault register: %pe\n",
+				    ERR_PTR(ret));
+		/*
+		 * Always retry on read failure. If we drop the heartbeat during
+		 * the initial fault before caching it, the edge-triggered IRQ
+		 * will never fire again and permanently stall fault monitoring.
+		 */
+		rearm = true;
+		goto out_poll;
+	}
+
+	/* Isolate hwmon alarm bits from system integrity bits */
+	alarm_faults = faults & MC33978_FAULT_ALARM_MASK;
+	changed_faults = alarm_faults ^ old_faults;
+	new_faults = alarm_faults & ~old_faults;
+	priv->last_faults = alarm_faults;
+
+	hwmon_unlock(priv->hwmon_dev);
+
+	if (faults & MC33978_FAULT_SPI_ERROR)
+		dev_err_ratelimited(priv->dev, "SPI communication error detected\n");
+	if (faults & MC33978_FAULT_HASH)
+		dev_err_ratelimited(priv->dev, "SPI register hash mismatch detected\n");
+
+	if (new_faults)
+		mc33978_hwmon_report_faults(priv, new_faults);
+
+	if (changed_faults & MC33978_FAULT_UV)
+		hwmon_notify_event(priv->hwmon_dev, hwmon_in,
+				   hwmon_in_lcrit_alarm, MC33978_IN_VBATP);
+
+	if (changed_faults & MC33978_FAULT_OV)
+		hwmon_notify_event(priv->hwmon_dev, hwmon_in,
+				   hwmon_in_crit_alarm, MC33978_IN_VBATP);
+
+	if (changed_faults & MC33978_FAULT_TEMP_WARN)
+		hwmon_notify_event(priv->hwmon_dev, hwmon_temp,
+				   hwmon_temp_max_alarm, 0);
+
+	if (changed_faults & MC33978_FAULT_OT)
+		hwmon_notify_event(priv->hwmon_dev, hwmon_temp,
+				   hwmon_temp_crit_alarm, 0);
+
+	if (changed_faults)
+		hwmon_notify_event(priv->hwmon_dev, hwmon_chip,
+				   hwmon_chip_alarms, 0);
+
+	rearm = !!alarm_faults;
+
+out_poll:
+	/*
+	 * If any alarms are currently active, the global FAULT_STAT bit remains
+	 * asserted. The hardware will not generate a new rising edge interrupt
+	 * if a secondary fault occurs, nor will it interrupt when faults clear.
+	 * Schedule a poll to detect both clearing edges and secondary alarms.
+	 */
+	if (rearm)
+		/* Use freezable polling to pause while the system is suspended. */
+		mod_delayed_work(system_freezable_wq, &priv->poll_work,
+				 msecs_to_jiffies(MC33978_FAULT_POLL_INTERVAL_MS));
+
+	return ret;
+}
+
+static irqreturn_t mc33978_hwmon_fault_irq(int irq, void *data)
+{
+	struct mc33978_hwmon_priv *priv = data;
+
+	mc33978_hwmon_update_faults(priv);
+
+	return IRQ_HANDLED;
+}
+
+static void mc33978_hwmon_poll_work(struct work_struct *work)
+{
+	struct mc33978_hwmon_priv *priv =
+		container_of(work, struct mc33978_hwmon_priv, poll_work.work);
+
+	mc33978_hwmon_update_faults(priv);
+}
+
+static umode_t mc33978_hwmon_is_visible(const void *data,
+					enum hwmon_sensor_types type,
+					u32 attr, int channel)
+{
+	switch (type) {
+	case hwmon_chip:
+		if (attr == hwmon_chip_alarms)
+			return 0444;
+		break;
+
+	case hwmon_temp:
+		switch (attr) {
+		case hwmon_temp_max:
+		case hwmon_temp_crit:
+		case hwmon_temp_crit_hyst:
+		case hwmon_temp_max_alarm:
+		case hwmon_temp_crit_alarm:
+		case hwmon_temp_rated_min:
+		case hwmon_temp_rated_max:
+			return 0444;
+		default:
+			break;
+		}
+		break;
+
+	case hwmon_in:
+		switch (attr) {
+		case hwmon_in_label:
+		case hwmon_in_max:
+		case hwmon_in_min:
+		case hwmon_in_lcrit:
+			return 0444;
+		case hwmon_in_crit:
+			if (channel == MC33978_IN_VBATP)
+				return 0444;
+			break;
+		case hwmon_in_crit_alarm:
+		case hwmon_in_lcrit_alarm:
+			if (channel == MC33978_IN_VBATP)
+				return 0444;
+			break;
+		}
+		break;
+	default:
+		break;
+	}
+
+	return 0;
+}
+
+static int mc33978_hwmon_read_chip(struct mc33978_hwmon_priv *priv, u32 attr,
+				   long *val)
+{
+	if (attr == hwmon_chip_alarms) {
+		*val = priv->last_faults;
+		return 0;
+	}
+
+	return -EOPNOTSUPP;
+}
+
+static int mc33978_hwmon_read_in_vbatp(struct mc33978_hwmon_priv *priv,
+				       u32 attr, long *val)
+{
+	switch (attr) {
+	case hwmon_in_crit:
+		*val = MC33978_VBATP_OV_MV;
+		return 0;
+	case hwmon_in_max:
+		*val = MC33978_VBATP_FUNC_MV;
+		return 0;
+	case hwmon_in_min:
+		*val = MC33978_VBATP_DEGRADED_MV;
+		return 0;
+	case hwmon_in_lcrit:
+		*val = MC33978_VBATP_UVLO_MV;
+		return 0;
+	case hwmon_in_crit_alarm:
+		*val = !!(priv->last_faults & MC33978_FAULT_OV);
+		return 0;
+	case hwmon_in_lcrit_alarm:
+		*val = !!(priv->last_faults & MC33978_FAULT_UV);
+		return 0;
+	default:
+		break;
+	}
+
+	return -EOPNOTSUPP;
+}
+
+static int mc33978_hwmon_read_in_vddq(u32 attr, long *val)
+{
+	switch (attr) {
+	case hwmon_in_max:
+		*val = MC33978_VDDQ_MAX_MV;
+		return 0;
+	case hwmon_in_min:
+		*val = MC33978_VDDQ_MIN_MV;
+		return 0;
+	case hwmon_in_lcrit:
+		*val = MC33978_VDDQ_UV_MV;
+		return 0;
+	default:
+		break;
+	}
+
+	return -EOPNOTSUPP;
+}
+
+static int mc33978_hwmon_read_in(struct mc33978_hwmon_priv *priv, u32 attr,
+				 int channel, long *val)
+{
+	switch (channel) {
+	case MC33978_IN_VBATP:
+		return mc33978_hwmon_read_in_vbatp(priv, attr, val);
+	case MC33978_IN_VDDQ:
+		return mc33978_hwmon_read_in_vddq(attr, val);
+	default:
+		break;
+	}
+
+	return -EOPNOTSUPP;
+}
+
+static int mc33978_hwmon_read_temp(struct mc33978_hwmon_priv *priv, u32 attr,
+				   long *val)
+{
+	switch (attr) {
+	case hwmon_temp_max:
+		*val = MC33978_TEMP_WARN_MC;
+		return 0;
+	case hwmon_temp_crit:
+		*val = MC33978_TEMP_CRIT_MC;
+		return 0;
+	case hwmon_temp_crit_hyst:
+		*val = MC33978_TEMP_CRIT_MC - MC33978_TEMP_HYST_MC;
+		return 0;
+	case hwmon_temp_max_alarm:
+		*val = !!(priv->last_faults & MC33978_FAULT_TEMP_WARN);
+		return 0;
+	case hwmon_temp_crit_alarm:
+		*val = !!(priv->last_faults & MC33978_FAULT_OT);
+		return 0;
+	case hwmon_temp_rated_min:
+		*val = MC33978_TEMP_MIN_MC;
+		return 0;
+	case hwmon_temp_rated_max:
+		*val = priv->hw_info->rated_max_temp;
+		return 0;
+	default:
+		break;
+	}
+
+	return -EOPNOTSUPP;
+}
+
+static int mc33978_hwmon_read(struct device *dev,
+			      enum hwmon_sensor_types type,
+			      u32 attr, int channel, long *val)
+{
+	struct mc33978_hwmon_priv *priv = dev_get_drvdata(dev);
+
+	switch (type) {
+	case hwmon_chip:
+		return mc33978_hwmon_read_chip(priv, attr, val);
+	case hwmon_in:
+		return mc33978_hwmon_read_in(priv, attr, channel, val);
+	case hwmon_temp:
+		return mc33978_hwmon_read_temp(priv, attr, val);
+	default:
+		break;
+	}
+
+	return -EOPNOTSUPP;
+}
+
+static int mc33978_hwmon_read_string(struct device *dev,
+				     enum hwmon_sensor_types type,
+				     u32 attr, int channel, const char **str)
+{
+	/* Only in_label is supported for string reads */
+	if (type != hwmon_in || attr != hwmon_in_label)
+		return -EOPNOTSUPP;
+
+	switch (channel) {
+	case MC33978_IN_VBATP:
+		*str = "VBATP";
+		return 0;
+	case MC33978_IN_VDDQ:
+		*str = "VDDQ";
+		return 0;
+	default:
+		return -EINVAL;
+	}
+}
+
+static const struct hwmon_channel_info * const mc33978_hwmon_info[] = {
+	HWMON_CHANNEL_INFO(chip,
+			   HWMON_C_ALARMS),
+	HWMON_CHANNEL_INFO(temp,
+			   HWMON_T_MAX | HWMON_T_CRIT | HWMON_T_CRIT_HYST |
+			   HWMON_T_MAX_ALARM | HWMON_T_CRIT_ALARM |
+			   HWMON_T_RATED_MIN | HWMON_T_RATED_MAX),
+	HWMON_CHANNEL_INFO(in,
+			   /* Index 0: MC33978_IN_VBATP */
+			   HWMON_I_LABEL | HWMON_I_CRIT | HWMON_I_MAX |
+			   HWMON_I_MIN | HWMON_I_LCRIT |
+			   HWMON_I_CRIT_ALARM | HWMON_I_LCRIT_ALARM,
+
+			   /* Index 1: MC33978_IN_VDDQ */
+			   HWMON_I_LABEL | HWMON_I_MAX | HWMON_I_MIN |
+			   HWMON_I_LCRIT),
+	NULL
+};
+
+static const struct hwmon_ops mc33978_hwmon_ops = {
+	.is_visible = mc33978_hwmon_is_visible,
+	.read_string = mc33978_hwmon_read_string,
+	.read = mc33978_hwmon_read,
+};
+
+static const struct hwmon_chip_info mc33978_hwmon_chip_info = {
+	.ops = &mc33978_hwmon_ops,
+	.info = mc33978_hwmon_info,
+};
+
+static void mc33978_hwmon_action_cancel_work(void *data)
+{
+	struct mc33978_hwmon_priv *priv = data;
+
+	cancel_delayed_work_sync(&priv->poll_work);
+}
+
+static int mc33978_hwmon_probe(struct platform_device *pdev)
+{
+	const struct platform_device_id *id;
+	struct device *dev = &pdev->dev;
+	struct mc33978_hwmon_priv *priv;
+	struct device *hwmon_dev;
+	int ret;
+
+	priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
+	if (!priv)
+		return -ENOMEM;
+
+	priv->dev = dev;
+
+	id = platform_get_device_id(pdev);
+	if (!id || !id->driver_data)
+		return dev_err_probe(dev, -EINVAL, "missing device match data\n");
+
+	priv->hw_info = (const struct mc33978_hwmon_hw_info *)id->driver_data;
+
+	priv->map = dev_get_regmap(dev->parent, NULL);
+	if (!priv->map)
+		return dev_err_probe(dev, -ENODEV, "failed to get regmap\n");
+
+	platform_set_drvdata(pdev, priv);
+
+	INIT_DELAYED_WORK(&priv->poll_work, mc33978_hwmon_poll_work);
+
+	priv->fault_irq = platform_get_irq(pdev, 0);
+	if (priv->fault_irq < 0)
+		return priv->fault_irq;
+
+	hwmon_dev = devm_hwmon_device_register_with_info(dev, "mc33978", priv,
+							 &mc33978_hwmon_chip_info,
+							 NULL);
+	if (IS_ERR(hwmon_dev))
+		return dev_err_probe(dev, PTR_ERR(hwmon_dev),
+				     "failed to register hwmon device\n");
+
+	priv->hwmon_dev = hwmon_dev;
+
+	ret = devm_add_action_or_reset(dev, mc33978_hwmon_action_cancel_work,
+				       priv);
+	if (ret)
+		return ret;
+
+	/*
+	 * The FAULT child IRQ is generated by the MFD core from transitions of
+	 * the aggregated FAULT_STAT bus state. Request a rising-edge nested
+	 * IRQ so the core dispatches the hwmon fault handler when faults become
+	 * active.
+	 *
+	 * Fault clearing and secondary faults while FAULT_STAT remains asserted
+	 * are handled by the hwmon polling path.
+	 */
+	ret = devm_request_threaded_irq(dev, priv->fault_irq, NULL,
+					mc33978_hwmon_fault_irq,
+					IRQF_ONESHOT | IRQF_TRIGGER_RISING,
+					dev_name(dev), priv);
+	if (ret)
+		return dev_err_probe(dev, ret, "failed to request fault IRQ\n");
+
+	return mc33978_hwmon_update_faults(priv);
+}
+
+static const struct platform_device_id mc33978_hwmon_id[] = {
+	{ "mc33978-hwmon", (kernel_ulong_t)&hwmon_hwinfo_mc33978 },
+	{ "mc34978-hwmon", (kernel_ulong_t)&hwmon_hwinfo_mc34978 },
+	{ }
+};
+MODULE_DEVICE_TABLE(platform, mc33978_hwmon_id);
+
+static struct platform_driver mc33978_hwmon_driver = {
+	.driver = {
+		.name = "mc33978-hwmon",
+	},
+	.probe = mc33978_hwmon_probe,
+	.id_table = mc33978_hwmon_id,
+};
+module_platform_driver(mc33978_hwmon_driver);
+
+MODULE_AUTHOR("Oleksij Rempel <kernel@pengutronix.de>");
+MODULE_DESCRIPTION("NXP MC33978/MC34978 Hardware Monitor Driver");
+MODULE_LICENSE("GPL");
-- 
2.47.3


^ permalink raw reply related

* [PATCH 0/3] Move adis16203 inclinometer driver out of staging
From: Sheng Kun Chang @ 2026-04-01 16:24 UTC (permalink / raw)
  To: jic23
  Cc: lars, Michael.Hennerich, dlechner, nuno.sa, andy, gregkh, robh,
	krzk+dt, conor+dt, linux-iio, linux-staging, devicetree,
	linux-kernel, Sheng Kun Chang

This series moves the ADIS16203 Programmable 360 Degrees Inclinometer
driver out of staging and into drivers/iio/accel/.

The driver already uses standard IIO channel interfaces and devm
managed APIs. The only missing piece was devicetree binding
documentation, which is added in patch 1.

Patch 1: Add devicetree binding documentation
Patch 2: Fix MODULE_LICENSE to match SPDX identifier
Patch 3: Move the driver from staging to drivers/iio/accel/

Sheng Kun Chang (3):
  dt-bindings: iio: accel: add binding for adi,adis16203
  staging: iio: adis16203: align MODULE_LICENSE with SPDX identifier
  iio: accel: move adis16203 out of staging

 .../bindings/iio/accel/adi,adis16203.yaml     | 52 +++++++++++++++++++
 drivers/iio/accel/Kconfig                     | 12 +++++
 drivers/iio/accel/Makefile                    |  1 +
 drivers/{staging => }/iio/accel/adis16203.c   |  2 +-
 drivers/staging/iio/Kconfig                   |  1 -
 drivers/staging/iio/Makefile                  |  1 -
 drivers/staging/iio/accel/Kconfig             | 19 -------
 drivers/staging/iio/accel/Makefile            |  6 ---
 8 files changed, 66 insertions(+), 28 deletions(-)
 create mode 100644 Documentation/devicetree/bindings/iio/accel/adi,adis16203.yaml
 rename drivers/{staging => }/iio/accel/adis16203.c (99%)
 delete mode 100644 drivers/staging/iio/accel/Kconfig
 delete mode 100644 drivers/staging/iio/accel/Makefile

-- 
2.43.0


^ permalink raw reply

* [PATCH v10 2/6] mfd: add NXP MC33978/MC34978 core driver
From: Oleksij Rempel @ 2026-04-01 16:25 UTC (permalink / raw)
  To: Guenter Roeck, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
	Lee Jones, Peter Rosin, Linus Walleij
  Cc: Oleksij Rempel, kernel, linux-kernel, devicetree, linux-hwmon,
	linux-gpio, David Jander
In-Reply-To: <20260401162510.1706073-1-o.rempel@pengutronix.de>

Add core Multi-Function Device (MFD) driver 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 driver handles:
- SPI communications via a custom regmap bus to support the device's
  pipelined two-frame MISO response requirement.
- Power sequencing for the VDDQ (logic) and VBATP (battery) regulators.
- 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.

Child devices (pinctrl, hwmon, mux) are instantiated by the core driver
from match data.

Signed-off-by: Oleksij Rempel <o.rempel@pengutronix.de>
---
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/mfd/Kconfig         |   15 +
 drivers/mfd/Makefile        |    2 +
 drivers/mfd/mc33978.c       | 1079 +++++++++++++++++++++++++++++++++++
 include/linux/mfd/mc33978.h |   95 +++
 4 files changed, 1191 insertions(+)
 create mode 100644 drivers/mfd/mc33978.c
 create mode 100644 include/linux/mfd/mc33978.h

diff --git a/drivers/mfd/Kconfig b/drivers/mfd/Kconfig
index 7192c9d1d268..6dc9554822c9 100644
--- a/drivers/mfd/Kconfig
+++ b/drivers/mfd/Kconfig
@@ -2566,6 +2566,21 @@ config MFD_UPBOARD_FPGA
 	  To compile this driver as a module, choose M here: the module will be
 	  called upboard-fpga.
 
+config MFD_MC33978
+	tristate "NXP MC33978/MC34978 industrial input controller core"
+	depends on OF
+	depends on SPI
+	select IRQ_DOMAIN_HIERARCHY
+	select MFD_CORE
+	select REGMAP
+	help
+	  Support for the NXP MC33978/MC34978 industrial input controllers
+	  using the SPI interface.
+
+	  This driver provides common support for accessing the device.
+	  Additional drivers must be enabled in order to use the functionality
+	  of the device.
+
 config MFD_MAX7360
 	tristate "Maxim MAX7360 I2C IO Expander"
 	depends on I2C
diff --git a/drivers/mfd/Makefile b/drivers/mfd/Makefile
index e75e8045c28a..dcd99315f683 100644
--- a/drivers/mfd/Makefile
+++ b/drivers/mfd/Makefile
@@ -122,6 +122,8 @@ obj-$(CONFIG_MFD_MC13XXX)	+= mc13xxx-core.o
 obj-$(CONFIG_MFD_MC13XXX_SPI)	+= mc13xxx-spi.o
 obj-$(CONFIG_MFD_MC13XXX_I2C)	+= mc13xxx-i2c.o
 
+obj-$(CONFIG_MFD_MC33978)	+= mc33978.o
+
 obj-$(CONFIG_MFD_PF1550)	+= pf1550.o
 
 obj-$(CONFIG_MFD_NCT6694)	+= nct6694.o
diff --git a/drivers/mfd/mc33978.c b/drivers/mfd/mc33978.c
new file mode 100644
index 000000000000..ce93d35e4446
--- /dev/null
+++ b/drivers/mfd/mc33978.c
@@ -0,0 +1,1079 @@
+// 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 Multiple Switch Detection Interface - MFD Core Driver
+ *
+ * Driver Architecture:
+ * This is the core MFD driver handling the physical SPI interface, power
+ * management, and central interrupt routing. It instantiates the following
+ * child devices:
+ * - pinctrl: For GPIO read/write and wetting current configuration.
+ * - hwmon:   For hardware fault monitoring (tLIM, over/under-voltage).
+ * - mux:     For the 24-to-1 analog multiplexer (AMUX).
+ *
+ * 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:
+ * - Clear-on-Read: The physical INT_B line is directly tied to the INT_flg
+ * bit. The hardware deasserts INT_B immediately upon *any* SPI transfer
+ * that returns INT_flg. Harvesting this bit from all SPI traffic is the
+ * ONLY way to know this device triggered an interrupt (crucial for shared
+ * IRQ lines).
+ * - Stateless Pin Edge Detection: The hardware lacks per-pin interrupt status
+ * registers. To determine which pin triggered an event, the driver must
+ * read the current pin states and XOR them against a previously cached state.
+ * - Missed Short Pulses: Because pin interrupts are state-derived rather than
+ * hardware-latched, very short physical pulses (shorter than the SPI read
+ * latency) will be missed entirely if the pin reverts to its original state
+ * before the READ_IN register is sampled by the IRQ thread.
+ * - Edge-Only Pin Interrupts: The hardware only asserts INT_B on a state
+ * change. It cannot continuously assert an interrupt while a pin is held at a
+ * specific logic level. Consequently, the driver strictly emulates edge
+ * interrupts (RISING/FALLING) and explicitly rejects LEVEL interrupt
+ * configurations to prevent consumer misalignment.
+ */
+
+#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/devm-helpers.h>
+#include <linux/interrupt.h>
+#include <linux/irq.h>
+#include <linux/irqdomain.h>
+#include <linux/mfd/core.h>
+#include <linux/mod_devicetable.h>
+#include <linux/module.h>
+#include <linux/of_platform.h>
+#include <linux/property.h>
+#include <linux/regmap.h>
+#include <linux/regulator/consumer.h>
+#include <linux/spi/spi.h>
+#include <linux/string.h>
+
+#include <linux/mfd/mc33978.h>
+
+#define MC33978_DRV_NAME		"mc33978"
+
+/* 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 CHECK, 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)
+
+/* High Payload Byte Masks (FRAME_DATA_HI / PAYLOAD_HI). */
+#define MC33978_HI_BYTE_STAT_FAULT     BIT(7) /* Maps to frame bit [23] */
+#define MC33978_HI_BYTE_STAT_INT       BIT(6) /* Maps to frame bit [22] */
+
+#define MC33978_HI_BYTE_STATUS_MASK    (MC33978_HI_BYTE_STAT_FAULT | \
+					MC33978_HI_BYTE_STAT_INT)
+
+/* Maps to frame bits [21:16] */
+#define MC33978_HI_BYTE_DATA_MASK	GENMASK(5, 0)
+
+#define MC33978_CACHE_SG_PIN_MASK	GENMASK(13, 0)
+#define MC33978_CACHE_SP_PIN_MASK	GENMASK(21, 14)
+
+#define MC33978_SG_PIN_MASK		GENMASK(13, 0)
+#define MC33978_SP_PIN_MASK		GENMASK(7, 0)
+
+struct mc33978_data {
+	const struct mfd_cell *cells;
+	int num_cells;
+};
+
+struct mc33978_mfd_priv {
+	/* Immutable after initialization (no lock needed) */
+	struct spi_device *spi;
+	struct regmap *map;
+	struct regulator *vddq;
+	struct regulator *vbatp;
+	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;
+
+	/* Protected by event_lock */
+	struct mutex event_lock;
+	u32 cached_pin_state;		/* Previous pin state for edge detection */
+
+	/* Protected by irq_lock */
+	struct mutex irq_lock;
+	u32 cached_pin_mask;		/* IRQ mask for 22 pins */
+	u32 irq_rise;			/* Rising edge IRQ enable mask */
+	u32 irq_fall;			/* Falling edge IRQ enable mask */
+
+	/* Protected by teardown_lock */
+	spinlock_t teardown_lock;
+	bool tearing_down;		/* Prevents work scheduling during teardown */
+
+	/* Atomic operations (no lock needed) */
+	atomic_t harvested_flags;	/* Status bits from SPI responses */
+
+	/*
+	 * Cross-context lockless access (READ_ONCE/WRITE_ONCE).
+	 * Accessed from regmap callbacks (unpredictable context) and event work.
+	 * Cannot use lock in regmap callback. Benign race acceptable.
+	 */
+	bool bus_fault_active;		/* Latest physical fault state on bus */
+	bool cached_fault_active;	/* Cached fault state from previous event */
+
+	/*
+	 * Work scheduling protected by teardown_lock.
+	 * Work execution serialized by workqueue subsystem.
+	 */
+	struct work_struct event_work;
+
+	/*
+	 * DMA buffers protected by SPI subsystem + regmap serialization.
+	 * Modified before spi_sync(), read after it returns.
+	 * Must be at end for ____cacheline_aligned.
+	 */
+	u8 tx_frame[MC33978_FRAME_COUNT][MC33978_FRAME_LEN] ____cacheline_aligned;
+	u8 rx_frame[MC33978_FRAME_COUNT][MC33978_FRAME_LEN] ____cacheline_aligned;
+};
+
+static void mc33978_irq_mask(struct irq_data *data)
+{
+	struct mc33978_mfd_priv *mc = irq_data_get_irq_chip_data(data);
+	irq_hw_number_t hwirq = irqd_to_hwirq(data);
+
+	if (hwirq < MC33978_NUM_PINS)
+		mc->cached_pin_mask &= ~BIT(hwirq);
+}
+
+static void mc33978_irq_unmask(struct irq_data *data)
+{
+	struct mc33978_mfd_priv *mc = irq_data_get_irq_chip_data(data);
+	irq_hw_number_t hwirq = irqd_to_hwirq(data);
+
+	if (hwirq < MC33978_NUM_PINS)
+		mc->cached_pin_mask |= BIT(hwirq);
+}
+
+static void mc33978_irq_bus_lock(struct irq_data *data)
+{
+	struct mc33978_mfd_priv *mc = irq_data_get_irq_chip_data(data);
+
+	mutex_lock(&mc->irq_lock);
+}
+
+/**
+ * mc33978_irq_bus_sync_unlock() - Sync cached IRQ mask to hardware and unlock
+ * @data: IRQ data
+ *
+ * Writes the cached interrupt mask to the hardware IE_SG and IE_SP registers,
+ * then releases the IRQ lock. This is where the actual hardware update occurs
+ * after mask/unmask operations.
+ */
+static void mc33978_irq_bus_sync_unlock(struct irq_data *data)
+{
+	struct mc33978_mfd_priv *mc = irq_data_get_irq_chip_data(data);
+	u32 sg_mask, sp_mask, cached_mask;
+	int ret;
+
+	cached_mask = mc->cached_pin_mask;
+
+	/*
+	 * 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_CACHE_SG_PIN_MASK, cached_mask);
+	sp_mask = FIELD_GET(MC33978_CACHE_SP_PIN_MASK, cached_mask);
+
+	ret = regmap_update_bits(mc->map, MC33978_REG_IE_SG,
+				 MC33978_SG_PIN_MASK, sg_mask);
+	if (ret)
+		goto unlock;
+
+	ret = regmap_update_bits(mc->map, MC33978_REG_IE_SP,
+				 MC33978_SP_PIN_MASK, sp_mask);
+unlock:
+	if (ret)
+		dev_err(&mc->spi->dev, "failed to sync IRQ mask to hardware: %d\n",
+			ret);
+
+	mutex_unlock(&mc->irq_lock);
+}
+
+static int mc33978_irq_set_type(struct irq_data *data, unsigned int type)
+{
+	struct mc33978_mfd_priv *mc = irq_data_get_irq_chip_data(data);
+	irq_hw_number_t hwirq = irqd_to_hwirq(data);
+	u32 mask = BIT(hwirq);
+
+	if (type & (IRQ_TYPE_LEVEL_HIGH | IRQ_TYPE_LEVEL_LOW))
+		return -EINVAL;
+
+	/*
+	 * No locking needed here - irq_bus_lock/irq_bus_sync_unlock
+	 * already provide serialization via mc->irq_lock mutex.
+	 */
+
+	if (type & IRQ_TYPE_EDGE_RISING)
+		mc->irq_rise |= mask;
+	else
+		mc->irq_rise &= ~mask;
+
+	if (type & IRQ_TYPE_EDGE_FALLING)
+		mc->irq_fall |= mask;
+	else
+		mc->irq_fall &= ~mask;
+
+	return 0;
+}
+
+static int mc33978_irq_set_wake(struct irq_data *data, unsigned int on)
+{
+	struct mc33978_mfd_priv *mc = irq_data_get_irq_chip_data(data);
+
+	return irq_set_irq_wake(mc->spi->irq, on);
+}
+
+static struct irq_chip mc33978_irq_chip = {
+	.name			= MC33978_DRV_NAME,
+	.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,
+	.irq_set_wake		= mc33978_irq_set_wake,
+};
+
+static void mc33978_irq_setup(struct irq_domain *domain, unsigned int virq,
+			      irq_hw_number_t hwirq)
+{
+	struct mc33978_mfd_priv *mc = domain->host_data;
+
+	irq_domain_set_info(domain, virq, hwirq, &mc33978_irq_chip, mc,
+			    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;
+
+	if (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));
+}
+
+/*
+ * IRQ domain operations for dual-mode interrupt allocation.
+ *
+ * This domain serves two types of consumers:
+ *
+ * 1. Direct MFD child (hwmon):
+ *    - Uses platform_get_irq() with DEFINE_RES_IRQ(MC33978_HWIRQ_FAULT)
+ *    - Calls irq_create_mapping(domain, hwirq)
+ *    - Invokes .map callback -> mc33978_irq_map()
+ *
+ * 2. Hierarchical child domain (pinctrl's GPIO IRQ chip):
+ *    - Pinctrl finds this domain via irq_find_matching_fwnode(DOMAIN_BUS_NEXUS)
+ *    - Creates GPIO IRQ domain with parent_domain = this domain
+ *    - External devices reference pinctrl as interrupt-parent in devicetree
+ *    - When GPIO-to-IRQ translation occurs, calls irq_domain_alloc_irqs()
+ *    - Chains up to parent (this domain), invokes .alloc callback
+ *    - See drivers/pinctrl/pinctrl-mc33978.c for hierarchical setup
+ *
+ * The .xlate callback translates devicetree interrupt specifiers (2-cell
+ * format:
+ * hwirq number 0-22, IRQ type flags) into kernel hwirq and type values.
+ *
+ * Both .map and .alloc perform similar initialization (set chip, handler,
+ * flags) but are invoked by different IRQ subsystem code paths.
+ * IRQ_DOMAIN_FLAG_HIERARCHY enables the .alloc path for 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_mfd_priv *mc = data;
+	struct irq_domain *domain = mc->domain;
+	int hwirq;
+
+	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 void mc33978_handle_pin_changes(struct mc33978_mfd_priv *mc,
+				       unsigned int pin_state)
+{
+	unsigned long fired_pins = 0;
+	u32 changed_pins;
+	u32 rise, fall, pin_mask;
+	int i;
+
+	changed_pins = pin_state ^ mc->cached_pin_state;
+	if (!changed_pins)
+		return;
+
+	mc->cached_pin_state = pin_state;
+
+	scoped_guard(mutex, &mc->irq_lock) {
+		pin_mask = mc->cached_pin_mask;
+		rise = mc->irq_rise;
+		fall = mc->irq_fall;
+	}
+
+	changed_pins &= pin_mask;
+
+	if (!changed_pins)
+		return;
+
+	fired_pins |= (changed_pins & pin_state) & rise;
+	fired_pins |= (changed_pins & ~pin_state) & fall;
+
+	for_each_set_bit(i, &fired_pins, MC33978_NUM_PINS) {
+		int virq = irq_find_mapping(mc->domain, i);
+
+		if (virq)
+			handle_nested_irq(virq);
+	}
+}
+
+static void mc33978_handle_fault_condition(struct mc33978_mfd_priv *mc,
+					   u8 hw_flags)
+{
+	bool fault_active, cached_fault, transient, changed;
+	u32 rise, fall;
+	int virq;
+
+	/* Read the absolute latest physical state seen on the bus */
+	fault_active = READ_ONCE(mc->bus_fault_active);
+
+	/* Read the cached fault state from the previous event loop */
+	cached_fault = READ_ONCE(mc->cached_fault_active);
+
+	/* Check if the fault state has changed since the last event loop */
+	changed = fault_active ^ cached_fault;
+	if (changed)
+		WRITE_ONCE(mc->cached_fault_active, fault_active);
+
+	/*
+	 * A transient fault is a pulse that was caught by the clear-on-read
+	 * status flags, but is no longer physically active on the bus.
+	 */
+	transient = !changed && !fault_active &&
+		    (hw_flags & MC33978_HI_BYTE_STAT_FAULT);
+
+	if (!changed && !transient)
+		return;
+
+	scoped_guard(mutex, &mc->irq_lock) {
+		rise = mc->irq_rise;
+		fall = mc->irq_fall;
+	}
+
+	virq = irq_find_mapping(mc->domain, MC33978_HWIRQ_FAULT);
+	if (!virq)
+		return;
+
+	if (transient) {
+		/* Transient pulse: trigger both edges if enabled */
+		if (rise & BIT(MC33978_HWIRQ_FAULT))
+			handle_nested_irq(virq);
+		if (fall & BIT(MC33978_HWIRQ_FAULT))
+			handle_nested_irq(virq);
+	} else if ((fault_active && (rise & BIT(MC33978_HWIRQ_FAULT))) ||
+		    (!fault_active && (fall & BIT(MC33978_HWIRQ_FAULT)))) {
+		/* Normal edge */
+		handle_nested_irq(virq);
+	}
+}
+
+static void mc33978_process_single_event(struct mc33978_mfd_priv *mc)
+{
+	unsigned int pin_state;
+	u8 hw_flags;
+	int ret;
+
+	/*
+	 * Grab harvested_flags BEFORE reading the hardware. If the read itself
+	 * or a concurrent SPI transfer harvests new flags, they will remain set
+	 * in harvested_flags and correctly trigger another pass of the event
+	 * loop.
+	 *
+	 * Note on Performance: This architecture intentionally forces a second
+	 * (redundant) SPI read of READ_IN during almost every interrupt event.
+	 * While SPI framework overhead (CS toggling, DMA setup, context
+	 * switches) makes this 4-byte transfer relatively costly, it is
+	 * mathematically necessary to guarantee no edge events are permanently
+	 * lost when a concurrent regmap access races with the IRQ thread, due
+	 * to the hardware's clear-on-read global INT_flg design.
+	 */
+	hw_flags = atomic_xchg(&mc->harvested_flags, 0);
+
+	ret = regmap_read(mc->map, MC33978_REG_READ_IN, &pin_state);
+	if (ret)
+		dev_err_ratelimited(&mc->spi->dev, "failed to read pin state: %d\n",
+				    ret);
+	else
+		mc33978_handle_pin_changes(mc, pin_state);
+
+	mc33978_handle_fault_condition(mc, hw_flags);
+}
+
+static void mc33978_handle_events(struct mc33978_mfd_priv *mc)
+{
+	guard(mutex)(&mc->event_lock);
+
+	do {
+		mc33978_process_single_event(mc);
+	} while (atomic_read(&mc->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_mfd_priv *mc = data;
+
+	/*
+	 * During the devres LIFO teardown window, the workqueue is canceled
+	 * before the regmap is destroyed. A concurrent debugfs regmap read
+	 * can trigger mc33978_harvest_status() and wrongly reschedule the
+	 * workqueue after it was already canceled.
+	 *
+	 * Flag the teardown state under a lock so the harvester atomically
+	 * checks and ignores status bits before scheduling new work.
+	 */
+	scoped_guard(spinlock_irqsave, &mc->teardown_lock) {
+		mc->tearing_down = true;
+	}
+
+	cancel_work_sync(&mc->event_work);
+}
+
+static int mc33978_irq_init(struct mc33978_mfd_priv *mc,
+			    struct fwnode_handle *fwnode)
+{
+	struct device *dev = &mc->spi->dev;
+	int ret;
+
+	mutex_init(&mc->irq_lock);
+
+	/*
+	 * Create IRQ domain with 23 interrupts:
+	 * - hwirq 0-21: Pin change interrupts (22 pins)
+	 * - hwirq 22: Fault interrupt (for hwmon driver)
+	 */
+	mc->domain = irq_domain_create_linear(fwnode, MC33978_NUM_IRQS,
+					      &mc33978_irq_domain_ops, mc);
+	if (!mc->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(mc->domain, DOMAIN_BUS_NEXUS);
+
+	/*
+	 * Enable hierarchical IRQ domain support for pinctrl's GPIO IRQ chip.
+	 * See mc33978_irq_domain_ops for detailed architecture explanation.
+	 */
+	mc->domain->flags |= IRQ_DOMAIN_FLAG_HIERARCHY;
+
+	ret = devm_add_action_or_reset(dev, mc33978_irq_domain_remove, mc);
+	if (ret)
+		return ret;
+
+	return 0;
+}
+
+static void mc33978_event_work(struct work_struct *work)
+{
+	struct mc33978_mfd_priv *mc =
+		container_of(work, struct mc33978_mfd_priv, event_work);
+
+	mc33978_handle_events(mc);
+}
+
+/**
+ * mc33978_harvest_status() - Collect status flags from SPI responses
+ * @mc: Device private data
+ * @status: Status bits (FAULT_STAT and INT_flg) from MISO frame
+ *
+ * Accumulates status flags harvested from SPI responses and schedules
+ * event processing if not already in progress. Called by the SPI
+ * read/write functions when status bits are detected in responses.
+ */
+static void mc33978_harvest_status(struct mc33978_mfd_priv *mc, int status)
+{
+	bool fault_active;
+
+	fault_active = !!(status & MC33978_HI_BYTE_STAT_FAULT);
+
+	/* Track the absolute latest physical state seen on the bus */
+	WRITE_ONCE(mc->bus_fault_active, fault_active);
+
+	/*
+	 * If the bus state changed from what the IRQ thread last evaluated,
+	 * wake it up.
+	 */
+	if (fault_active != READ_ONCE(mc->cached_fault_active))
+		atomic_or(MC33978_HI_BYTE_STAT_FAULT, &mc->harvested_flags);
+
+	if (status & MC33978_HI_BYTE_STAT_INT)
+		atomic_or(MC33978_HI_BYTE_STAT_INT, &mc->harvested_flags);
+
+	/* Ensure harvested_flags is visible before checking teardown state */
+	smp_mb__after_atomic();
+
+	scoped_guard(spinlock_irqsave, &mc->teardown_lock) {
+		if (!mc->tearing_down && atomic_read(&mc->harvested_flags))
+			schedule_work(&mc->event_work);
+	}
+}
+
+/**
+ * mc33978_prepare_messages() - Initialize the persistent SPI messages
+ * @mc: Device private data
+ *
+ * Hardware pipelining constraints:
+ * - Write (1 Frame): The device executes write commands immediately upon
+ * CS de-assertion. No fetch frame is required.
+ * - Read (2 Frames): The MISO response logically lags by one frame.
+ * Frame 1 transmits the read request and toggles CS to latch it.
+ * Frame 2 transmits a dummy CHECK command to fetch the actual payload.
+ */
+static void mc33978_prepare_messages(struct mc33978_mfd_priv *mc)
+{
+	/* --- Prepare Write Message (1 Frame) --- */
+	spi_message_init(&mc->msg_write);
+
+	mc->xfer_write.tx_buf = mc->tx_frame[MC33978_FRAME_REQ];
+	mc->xfer_write.rx_buf = mc->rx_frame[MC33978_FRAME_REQ];
+	mc->xfer_write.len = MC33978_FRAME_LEN;
+
+	spi_message_add_tail(&mc->xfer_write, &mc->msg_write);
+
+	/* --- Prepare Read Message (2 Frames) --- */
+	spi_message_init(&mc->msg_read);
+
+	/* Frame 1: Request */
+	mc->xfer_read[MC33978_FRAME_REQ].tx_buf =
+		mc->tx_frame[MC33978_FRAME_REQ];
+	mc->xfer_read[MC33978_FRAME_REQ].rx_buf =
+		mc->rx_frame[MC33978_FRAME_REQ];
+	mc->xfer_read[MC33978_FRAME_REQ].len = MC33978_FRAME_LEN;
+	mc->xfer_read[MC33978_FRAME_REQ].cs_change = 1; /* Latch command */
+
+	/* Frame 2: Fetch (Dummy CHECK) */
+	mc->xfer_read[MC33978_FRAME_PIPE].tx_buf =
+		mc->tx_frame[MC33978_FRAME_PIPE];
+	mc->xfer_read[MC33978_FRAME_PIPE].rx_buf =
+		mc->rx_frame[MC33978_FRAME_PIPE];
+	mc->xfer_read[MC33978_FRAME_PIPE].len = MC33978_FRAME_LEN;
+
+	/* Preload the dummy CHECK command statically */
+	mc->tx_frame[MC33978_FRAME_PIPE][MC33978_FRAME_CMD] = MC33978_REG_CHECK;
+
+	spi_message_add_tail(&mc->xfer_read[MC33978_FRAME_REQ], &mc->msg_read);
+	spi_message_add_tail(&mc->xfer_read[MC33978_FRAME_PIPE], &mc->msg_read);
+}
+
+/**
+ * mc33978_rx_decode() - Decode MISO response frame and extract status
+ * @rx_frame: Received SPI frame buffer (4 bytes)
+ * @val_buf: Output buffer for regmap (exactly 3 bytes, optional)
+ *
+ * Translates the 4-byte SPI response into a 3-byte regmap payload.
+ * Harvests the volatile INTflg and FAULT_STAT bits from the MSB.
+ *
+ * Note: MC33978_REG_CHECK, MC33978_REG_WET_SP, and MC33978_REG_WET_SG0 do not
+ * contain fault status or interrupt flags.
+ *
+ * Return: Status bits if present, negative error code otherwise.
+ */
+static int mc33978_rx_decode(const u8 *rx_frame, u8 *val_buf)
+{
+	u8 cmd = rx_frame[MC33978_FRAME_CMD] & ~MC33978_CMD_BYTE_WRITE;
+	bool has_status;
+	u8 status = 0;
+
+	switch (cmd) {
+	case MC33978_REG_CHECK:
+	case MC33978_REG_WET_SP:
+	case MC33978_REG_WET_SG0:
+		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_mfd_priv *mc = ctx;
+	int status;
+	int ret;
+
+	if (count != MC33978_FRAME_LEN)
+		return -EINVAL;
+
+	memcpy(mc->tx_frame[MC33978_FRAME_REQ], data, MC33978_FRAME_LEN);
+
+	ret = spi_sync(mc->spi, &mc->msg_write);
+	if (ret)
+		return ret;
+
+	status = mc33978_rx_decode(mc->rx_frame[MC33978_FRAME_REQ], NULL);
+	if (status >= 0)
+		mc33978_harvest_status(mc, 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_mfd_priv *mc = ctx;
+	int status_req, status_pipe;
+	int ret;
+
+	if (reg_size != 1 || val_size != MC33978_PAYLOAD_LEN)
+		return -EINVAL;
+
+	memset(&mc->tx_frame[MC33978_FRAME_REQ][MC33978_FRAME_DATA_HI], 0,
+	       MC33978_PAYLOAD_LEN);
+	mc->tx_frame[MC33978_FRAME_REQ][MC33978_FRAME_CMD] =
+		((const u8 *)reg_buf)[0];
+
+	ret = spi_sync(mc->spi, &mc->msg_read);
+	if (ret)
+		return ret;
+
+	status_req = mc33978_rx_decode(mc->rx_frame[MC33978_FRAME_REQ], NULL);
+	status_pipe = mc33978_rx_decode(mc->rx_frame[MC33978_FRAME_PIPE],
+					val_buf);
+
+	if (status_req >= 0)
+		mc33978_harvest_status(mc, status_req);
+	if (status_pipe >= 0)
+		mc33978_harvest_status(mc, 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_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_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_IRQ and REG_RESET as readable, so that regcache
+ * will NOT write to 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_WDEB_SG),
+	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_DRV_NAME,
+	.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,
+};
+
+static int mc33978_power_on(struct mc33978_mfd_priv *mc)
+{
+	struct device *dev = &mc->spi->dev;
+	int ret;
+
+	ret = regulator_enable(mc->vddq);
+	if (ret)
+		return dev_err_probe(dev, ret, "failed to enable VDDQ supply\n");
+
+	ret = regulator_enable(mc->vbatp);
+	if (ret) {
+		regulator_disable(mc->vddq);
+		return dev_err_probe(dev, ret, "failed to enable VBATP supply\n");
+	}
+
+	return 0;
+}
+
+static void mc33978_power_off(void *data)
+{
+	struct mc33978_mfd_priv *mc = data;
+
+	regulator_disable(mc->vbatp);
+	regulator_disable(mc->vddq);
+}
+
+/**
+ * mc33978_check_device() - Verify SPI communication with device
+ * @mc: Device context
+ *
+ * Reads the CHECK register which should return a fixed signature (0x123456).
+ * This verifies that SPI communication is working correctly.
+ *
+ * Note: MC33978_REG_CHECK does not contain fault status or interrupt flags.
+ * See mc33978_rx_decode() for details.
+ *
+ * Return: 0 on success, -ENODEV if signature doesn't match
+ */
+static int mc33978_check_device(struct mc33978_mfd_priv *mc)
+{
+	struct device *dev = &mc->spi->dev;
+	unsigned int check;
+	int ret;
+
+	ret = regmap_read(mc->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;
+}
+
+static const struct resource mc33978_hwmon_resources[] = {
+	DEFINE_RES_IRQ(MC33978_HWIRQ_FAULT),
+};
+
+static const struct mfd_cell mc33978_cells[] = {
+	{ .name = "mc33978-pinctrl" },
+	{
+		.name = "mc33978-hwmon",
+		.resources = mc33978_hwmon_resources,
+		.num_resources = ARRAY_SIZE(mc33978_hwmon_resources),
+	},
+	{ .name = "mc33978-mux" },
+};
+
+static const struct mfd_cell mc34978_cells[] = {
+	{ .name = "mc34978-pinctrl" },
+	{
+		.name = "mc34978-hwmon",
+		.resources = mc33978_hwmon_resources,
+		.num_resources = ARRAY_SIZE(mc33978_hwmon_resources),
+	},
+	{ .name = "mc34978-mux" },
+};
+
+static const struct mc33978_data mc33978_match_data = {
+	.cells = mc33978_cells,
+	.num_cells = ARRAY_SIZE(mc33978_cells),
+};
+
+static const struct mc33978_data mc34978_match_data = {
+	.cells = mc34978_cells,
+	.num_cells = ARRAY_SIZE(mc34978_cells),
+};
+
+static int mc33978_probe(struct spi_device *spi)
+{
+	const struct mc33978_data *match_data;
+	struct device *dev = &spi->dev;
+	struct fwnode_handle *fwnode;
+	struct mc33978_mfd_priv *mc;
+	int ret;
+
+	fwnode = dev_fwnode(dev);
+	if (!fwnode)
+		return dev_err_probe(dev, -ENODEV, "missing firmware node\n");
+
+	match_data = spi_get_device_match_data(spi);
+	if (!match_data)
+		return dev_err_probe(dev, -ENODEV, "no device match data found\n");
+
+	mc = devm_kzalloc(dev, sizeof(*mc), GFP_KERNEL);
+	if (!mc)
+		return -ENOMEM;
+
+	mc->spi = spi;
+	spi_set_drvdata(spi, mc);
+
+	mc->vddq = devm_regulator_get(dev, "vddq");
+	if (IS_ERR(mc->vddq))
+		return dev_err_probe(dev, PTR_ERR(mc->vddq),
+				     "failed to get VDDQ regulator\n");
+
+	mc->vbatp = devm_regulator_get(dev, "vbatp");
+	if (IS_ERR(mc->vbatp))
+		return dev_err_probe(dev, PTR_ERR(mc->vbatp),
+				     "failed to get VBATP regulator\n");
+
+	ret = mc33978_power_on(mc);
+	if (ret)
+		return ret;
+
+	ret = devm_add_action_or_reset(dev, mc33978_power_off, mc);
+	if (ret)
+		return ret;
+
+	mutex_init(&mc->event_lock);
+	spin_lock_init(&mc->teardown_lock);
+
+	INIT_WORK(&mc->event_work, mc33978_event_work);
+
+	atomic_set(&mc->harvested_flags, 0);
+
+	mc33978_prepare_messages(mc);
+
+	ret = mc33978_irq_init(mc, fwnode);
+	if (ret)
+		return ret;
+
+	mc->map = devm_regmap_init(dev, &mc33978_regmap_bus, mc,
+				   &mc33978_regmap_config);
+	if (IS_ERR(mc->map))
+		return dev_err_probe(dev, PTR_ERR(mc->map), "can't init regmap\n");
+
+	/*
+	 * Ensure event_work is canceled before regmap and irq_domain teardown,
+	 * since the worker dereferences both mc->map and mc->domain.
+	 */
+	ret = devm_add_action_or_reset(dev, mc33978_teardown, mc);
+	if (ret)
+		return ret;
+
+	ret = mc33978_check_device(mc);
+	if (ret)
+		return dev_err_probe(dev, ret, "can't use SPI bus\n");
+
+	/* Disable interrupts to prevent storms during priming */
+	ret = regmap_write(mc->map, MC33978_REG_IE_SP, 0);
+	if (ret)
+		return ret;
+
+	ret = regmap_write(mc->map, MC33978_REG_IE_SG, 0);
+	if (ret)
+		return ret;
+
+	/* Prime the cached pin state under lock to prevent spurious events */
+	scoped_guard(mutex, &mc->event_lock) {
+		ret = regmap_read(mc->map, MC33978_REG_READ_IN,
+				  &mc->cached_pin_state);
+	}
+	if (ret)
+		return dev_err_probe(dev, ret, "failed to read initial pin state\n");
+
+	if (mc->spi->irq <= 0)
+		return dev_err_probe(dev, -EINVAL, "no valid IRQ provided for INT_B pin\n");
+
+	/*
+	 * Deliberately not using IRQF_SHARED.
+	 *
+	 * MC33978 clear-on-read interrupt status can make shared wiring with
+	 * another MC33978/MC34978 functionally possible, but this handler runs
+	 * threaded with IRQF_ONESHOT and may hold the line masked for a long
+	 * time on slow SPI. The added latency/jitter makes shared operation
+	 * impractical.
+	 */
+	ret = devm_request_threaded_irq(dev, mc->spi->irq,
+					NULL,
+					mc33978_irq_thread,
+					IRQF_ONESHOT,
+					dev_name(dev), mc);
+	if (ret)
+		return dev_err_probe(dev, ret, "failed to request IRQ\n");
+
+	ret = devm_mfd_add_devices(dev, PLATFORM_DEVID_NONE,
+				   match_data->cells, match_data->num_cells,
+				   NULL, 0, mc->domain);
+	if (ret)
+		return dev_err_probe(dev, ret, "failed to add MFD child devices\n");
+
+	return 0;
+}
+
+static const struct of_device_id mc33978_of_match[] = {
+	{ .compatible = "nxp,mc33978", .data = &mc33978_match_data },
+	{ .compatible = "nxp,mc34978", .data = &mc34978_match_data },
+	{ }
+};
+MODULE_DEVICE_TABLE(of, mc33978_of_match);
+
+static const struct spi_device_id mc33978_spi_id[] = {
+	{ "mc33978", (kernel_ulong_t)&mc33978_match_data },
+	{ "mc34978", (kernel_ulong_t)&mc34978_match_data },
+	{ }
+};
+MODULE_DEVICE_TABLE(spi, mc33978_spi_id);
+
+static struct spi_driver mc33978_driver = {
+	.driver = {
+		.name = MC33978_DRV_NAME,
+		.of_match_table = mc33978_of_match,
+	},
+	.probe = mc33978_probe,
+	.id_table = mc33978_spi_id,
+};
+module_spi_driver(mc33978_driver);
+
+MODULE_AUTHOR("David Jander <david@protonic.nl>");
+MODULE_DESCRIPTION("NXP MC33978/MC34978 MFD core driver");
+MODULE_LICENSE("GPL");
diff --git a/include/linux/mfd/mc33978.h b/include/linux/mfd/mc33978.h
new file mode 100644
index 000000000000..06ff3c245093
--- /dev/null
+++ b/include/linux/mfd/mc33978.h
@@ -0,0 +1,95 @@
+/* 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) */
+#define MC33978_REG_CHECK	0x00	/* SPI communication check */
+#define MC33978_REG_CONFIG	0x02	/* Device configuration */
+#define MC33978_REG_TRI_SP	0x04	/* Tri-state enable SP */
+#define MC33978_REG_TRI_SG	0x06	/* Tri-state enable SG */
+#define MC33978_REG_WET_SP	0x08	/* Wetting current level SP */
+#define MC33978_REG_WET_SG0	0x0a	/* Wetting current level SG0 (SG7-SG0) */
+#define MC33978_REG_WET_SG1	0x0c	/* Wetting current level SG1 (SG13-SG8) */
+#define MC33978_REG_CWET_SP	0x16	/* Continuous wetting current SP */
+#define MC33978_REG_CWET_SG	0x18	/* Continuous wetting current SG */
+#define MC33978_REG_IE_SP	0x1a	/* Interrupt enable SP */
+#define MC33978_REG_IE_SG	0x1c	/* Interrupt enable SG */
+#define MC33978_REG_LPM_CONFIG	0x1e	/* Low-power mode configuration */
+#define MC33978_REG_WAKE_SP	0x20	/* Wake-up enable SP */
+#define MC33978_REG_WAKE_SG	0x22	/* Wake-up enable SG */
+#define MC33978_REG_COMP_SP	0x24	/* Comparator only mode SP */
+#define MC33978_REG_COMP_SG	0x26	/* Comparator only mode SG */
+#define MC33978_REG_LPM_VT_SP	0x28	/* LPM voltage threshold SP */
+#define MC33978_REG_LPM_VT_SG	0x2a	/* LPM voltage threshold SG */
+#define MC33978_REG_IP_SP	0x2c	/* Polling current SP */
+#define MC33978_REG_IP_SG	0x2e	/* Polling current SG */
+#define MC33978_REG_SPOLL_SP	0x30	/* Slow polling SP */
+#define MC33978_REG_SPOLL_SG	0x32	/* Slow polling SG */
+#define MC33978_REG_WDEB_SP	0x34	/* Wake-up debounce SP */
+#define MC33978_REG_WDEB_SG	0x36	/* Wake-up debounce SG */
+#define MC33978_REG_ENTER_LPM	0x38	/* Enter low-power mode (write-only) */
+#define MC33978_REG_AMUX_CTRL	0x3a	/* AMUX control */
+#define MC33978_REG_READ_IN	0x3e	/* Read switch status (READ_SW in datasheet) */
+#define MC33978_REG_FAULT	0x42	/* Fault status register */
+#define MC33978_REG_IRQ		0x46	/* Interrupt request (write-only) */
+#define MC33978_REG_RESET	0x48	/* Reset (write-only) */
+
+/*
+ * FAULT Register (0x42) bit definitions
+ * Reading this register clears most fault flags except persistent conditions
+ */
+#define MC33978_FAULT_SPI_ERROR	BIT(10)	/* SPI communication error */
+#define MC33978_FAULT_HASH	BIT(9)	/* SPI register hash mismatch */
+#define MC33978_FAULT_UV	BIT(7)	/* VBATP undervoltage */
+#define MC33978_FAULT_OV	BIT(6)	/* VBATP overvoltage */
+#define MC33978_FAULT_TEMP_WARN	BIT(5)	/* Temperature warning threshold */
+#define MC33978_FAULT_OT	BIT(4)	/* Over-temperature */
+#define MC33978_FAULT_INTB_WAKE	BIT(3)	/* Woken by INT_B pin */
+#define MC33978_FAULT_WAKEB_WAKE BIT(2)	/* Woken by WAKE_B pin */
+#define MC33978_FAULT_SPI_WAKE	BIT(1)	/* Woken by SPI message */
+#define MC33978_FAULT_POR	BIT(0)	/* Power-on reset occurred */
+
+/* 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
+
+/*
+ * 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
+ */
+#define MC33978_AMUX_CH_SG0	0	/* Switch-to-Ground inputs 0-13 */
+#define MC33978_AMUX_CH_SG13	13
+#define MC33978_AMUX_CH_SP0	14	/* Programmable switch inputs 0-7 */
+#define MC33978_AMUX_CH_SP7	21
+#define MC33978_AMUX_CH_TEMP	22	/* Internal temperature diode */
+#define MC33978_AMUX_CH_VBATP	23	/* Battery voltage sense */
+#define MC33978_NUM_AMUX_CH	24	/* Total number of AMUX channels */
+
+#endif /* _LINUX_MFD_MC33978_H */
-- 
2.47.3


^ permalink raw reply related

* [PATCH 1/3] dt-bindings: iio: accel: add binding for adi,adis16203
From: Sheng Kun Chang @ 2026-04-01 16:24 UTC (permalink / raw)
  To: jic23
  Cc: lars, Michael.Hennerich, dlechner, nuno.sa, andy, gregkh, robh,
	krzk+dt, conor+dt, linux-iio, linux-staging, devicetree,
	linux-kernel, Sheng Kun Chang
In-Reply-To: <20260401162458.88110-1-nothingchang@mirrorstack.ai>

Add devicetree binding documentation for the Analog Devices
ADIS16203 Programmable 360 Degrees Inclinometer, in preparation
for moving the driver out of staging.

Signed-off-by: Sheng Kun Chang <nothingchang@mirrorstack.ai>
---
 .../bindings/iio/accel/adi,adis16203.yaml     | 52 +++++++++++++++++++
 1 file changed, 52 insertions(+)
 create mode 100644 Documentation/devicetree/bindings/iio/accel/adi,adis16203.yaml

diff --git a/Documentation/devicetree/bindings/iio/accel/adi,adis16203.yaml b/Documentation/devicetree/bindings/iio/accel/adi,adis16203.yaml
new file mode 100644
index 000000000..6c5e2833c
--- /dev/null
+++ b/Documentation/devicetree/bindings/iio/accel/adi,adis16203.yaml
@@ -0,0 +1,52 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/iio/accel/adi,adis16203.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: ADIS16203 Programmable 360 Degrees Inclinometer
+
+maintainers:
+  - Jonathan Cameron <jic23@kernel.org>
+
+description: |
+  Programmable 360 degrees inclinometer with SPI interface.
+    https://www.analog.com/en/products/adis16203.html
+
+properties:
+  compatible:
+    const: adi,adis16203
+
+  reg:
+    maxItems: 1
+
+  interrupts:
+    maxItems: 1
+
+  vdd-supply: true
+
+required:
+  - compatible
+  - reg
+
+allOf:
+  - $ref: /schemas/spi/spi-peripheral-props.yaml#
+
+unevaluatedProperties: false
+
+examples:
+  - |
+    #include <dt-bindings/interrupt-controller/irq.h>
+    spi {
+        #address-cells = <1>;
+        #size-cells = <0>;
+
+        inclinometer@0 {
+            compatible = "adi,adis16203";
+            reg = <0>;
+            spi-max-frequency = <2500000>;
+            interrupt-parent = <&gpio0>;
+            interrupts = <0 IRQ_TYPE_LEVEL_HIGH>;
+        };
+    };
+...
-- 
2.43.0


^ permalink raw reply related

* [PATCH 2/3] staging: iio: adis16203: align MODULE_LICENSE with SPDX identifier
From: Sheng Kun Chang @ 2026-04-01 16:24 UTC (permalink / raw)
  To: jic23
  Cc: lars, Michael.Hennerich, dlechner, nuno.sa, andy, gregkh, robh,
	krzk+dt, conor+dt, linux-iio, linux-staging, devicetree,
	linux-kernel, Sheng Kun Chang
In-Reply-To: <20260401162458.88110-1-nothingchang@mirrorstack.ai>

The SPDX license identifier is GPL-2.0+ (GPL v2 or later) but
MODULE_LICENSE was set to "GPL v2" which indicates GPL v2 only.
Change to "GPL" which means GPL v2 or later, matching the SPDX
header.

Signed-off-by: Sheng Kun Chang <nothingchang@mirrorstack.ai>
---
 drivers/staging/iio/accel/adis16203.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/drivers/staging/iio/accel/adis16203.c b/drivers/staging/iio/accel/adis16203.c
index 830ff38fd..0ea127c9c 100644
--- a/drivers/staging/iio/accel/adis16203.c
+++ b/drivers/staging/iio/accel/adis16203.c
@@ -310,6 +310,6 @@ module_spi_driver(adis16203_driver);
 
 MODULE_AUTHOR("Barry Song <21cnbao@gmail.com>");
 MODULE_DESCRIPTION("Analog Devices ADIS16203 Programmable 360 Degrees Inclinometer");
-MODULE_LICENSE("GPL v2");
+MODULE_LICENSE("GPL");
 MODULE_ALIAS("spi:adis16203");
 MODULE_IMPORT_NS("IIO_ADISLIB");
-- 
2.43.0


^ permalink raw reply related

* [PATCH 3/3] iio: accel: move adis16203 out of staging
From: Sheng Kun Chang @ 2026-04-01 16:24 UTC (permalink / raw)
  To: jic23
  Cc: lars, Michael.Hennerich, dlechner, nuno.sa, andy, gregkh, robh,
	krzk+dt, conor+dt, linux-iio, linux-staging, devicetree,
	linux-kernel, Sheng Kun Chang
In-Reply-To: <20260401162458.88110-1-nothingchang@mirrorstack.ai>

The adis16203 driver uses standard IIO channel interfaces, devm
managed APIs, and has no remaining code quality issues. Move it
from drivers/staging/iio/accel/ to drivers/iio/accel/ and remove
the now-empty staging accel directory.

Signed-off-by: Sheng Kun Chang <nothingchang@mirrorstack.ai>
---
 drivers/iio/accel/Kconfig                   | 12 ++++++++++++
 drivers/iio/accel/Makefile                  |  1 +
 drivers/{staging => }/iio/accel/adis16203.c |  0
 drivers/staging/iio/Kconfig                 |  1 -
 drivers/staging/iio/Makefile                |  1 -
 drivers/staging/iio/accel/Kconfig           | 19 -------------------
 drivers/staging/iio/accel/Makefile          |  6 ------
 7 files changed, 13 insertions(+), 27 deletions(-)
 rename drivers/{staging => }/iio/accel/adis16203.c (100%)
 delete mode 100644 drivers/staging/iio/accel/Kconfig
 delete mode 100644 drivers/staging/iio/accel/Makefile

diff --git a/drivers/iio/accel/Kconfig b/drivers/iio/accel/Kconfig
index 3d3f8d867..2d8abec49 100644
--- a/drivers/iio/accel/Kconfig
+++ b/drivers/iio/accel/Kconfig
@@ -18,6 +18,18 @@ config ADIS16201
 	  To compile this driver as a module, say M here: the module will
 	  be called adis16201.
 
+config ADIS16203
+	tristate "Analog Devices ADIS16203 Programmable 360 Degrees Inclinometer"
+	depends on SPI
+	select IIO_ADIS_LIB
+	select IIO_ADIS_LIB_BUFFER if IIO_BUFFER
+	help
+	  Say Y here to build support for Analog Devices adis16203 Programmable
+	  360 Degrees Inclinometer.
+
+	  To compile this driver as a module, say M here: the module will
+	  be called adis16203.
+
 config ADIS16209
 	tristate "Analog Devices ADIS16209 Dual-Axis Digital Inclinometer and Accelerometer"
 	depends on SPI
diff --git a/drivers/iio/accel/Makefile b/drivers/iio/accel/Makefile
index fa440a859..eaf8ce443 100644
--- a/drivers/iio/accel/Makefile
+++ b/drivers/iio/accel/Makefile
@@ -5,6 +5,7 @@
 
 # When adding new entries keep the list in alphabetical order
 obj-$(CONFIG_ADIS16201) += adis16201.o
+obj-$(CONFIG_ADIS16203) += adis16203.o
 obj-$(CONFIG_ADIS16209) += adis16209.o
 obj-$(CONFIG_ADXL313) += adxl313_core.o
 obj-$(CONFIG_ADXL313_I2C) += adxl313_i2c.o
diff --git a/drivers/staging/iio/accel/adis16203.c b/drivers/iio/accel/adis16203.c
similarity index 100%
rename from drivers/staging/iio/accel/adis16203.c
rename to drivers/iio/accel/adis16203.c
diff --git a/drivers/staging/iio/Kconfig b/drivers/staging/iio/Kconfig
index a60631c1f..bf6addf29 100644
--- a/drivers/staging/iio/Kconfig
+++ b/drivers/staging/iio/Kconfig
@@ -5,7 +5,6 @@
 menu "IIO staging drivers"
 	depends on IIO
 
-source "drivers/staging/iio/accel/Kconfig"
 source "drivers/staging/iio/adc/Kconfig"
 source "drivers/staging/iio/addac/Kconfig"
 source "drivers/staging/iio/frequency/Kconfig"
diff --git a/drivers/staging/iio/Makefile b/drivers/staging/iio/Makefile
index 628583535..739e4cbf9 100644
--- a/drivers/staging/iio/Makefile
+++ b/drivers/staging/iio/Makefile
@@ -3,7 +3,6 @@
 # Makefile for the industrial I/O core.
 #
 
-obj-y += accel/
 obj-y += adc/
 obj-y += addac/
 obj-y += frequency/
diff --git a/drivers/staging/iio/accel/Kconfig b/drivers/staging/iio/accel/Kconfig
deleted file mode 100644
index cee51f64b..000000000
--- a/drivers/staging/iio/accel/Kconfig
+++ /dev/null
@@ -1,19 +0,0 @@
-# SPDX-License-Identifier: GPL-2.0
-#
-# Accelerometer drivers
-#
-menu "Accelerometers"
-
-config ADIS16203
-	tristate "Analog Devices ADIS16203 Programmable 360 Degrees Inclinometer"
-	depends on SPI
-	select IIO_ADIS_LIB
-	select IIO_ADIS_LIB_BUFFER if IIO_BUFFER
-	help
-	  Say Y here to build support for Analog Devices adis16203 Programmable
-	  360 Degrees Inclinometer.
-
-	  To compile this driver as a module, say M here: the module will be
-	  called adis16203.
-
-endmenu
diff --git a/drivers/staging/iio/accel/Makefile b/drivers/staging/iio/accel/Makefile
deleted file mode 100644
index acac7bc9b..000000000
--- a/drivers/staging/iio/accel/Makefile
+++ /dev/null
@@ -1,6 +0,0 @@
-# SPDX-License-Identifier: GPL-2.0
-#
-# Makefile for industrial I/O accelerometer drivers
-#
-
-obj-$(CONFIG_ADIS16203) += adis16203.o
-- 
2.43.0


^ permalink raw reply related

* Re: [PATCH RFC 3/4] clk: qcom: tcsrcc-glymur: Migrate tcsr_pcie_N_clkref_en to clk_ref common helper
From: Taniya Das @ 2026-04-01 16:35 UTC (permalink / raw)
  To: Qiang Yu, Bjorn Andersson, Michael Turquette, Stephen Boyd,
	Rob Herring, Krzysztof Kozlowski, Conor Dooley, Konrad Dybcio,
	johan
  Cc: linux-arm-msm, linux-clk, devicetree, linux-kernel
In-Reply-To: <20260331-qref_vote-v1-3-3fd7fbf87864@oss.qualcomm.com>



On 4/1/2026 12:05 PM, Qiang Yu wrote:
> Replace local clk_branch-based clkref definitions with descriptor-based
> registration via qcom_clk_ref_probe().
> 
> This keeps the glymur driver focused on clock metadata and reuses common
> runtime logic for regulator handling, enable/disable sequencing, and OF
> provider wiring.
> 
> Signed-off-by: Qiang Yu <qiang.yu@oss.qualcomm.com>
> ---
>  drivers/clk/qcom/tcsrcc-glymur.c | 340 +++++++++++----------------------------
>  1 file changed, 93 insertions(+), 247 deletions(-)
> 
> diff --git a/drivers/clk/qcom/tcsrcc-glymur.c b/drivers/clk/qcom/tcsrcc-glymur.c
> index 9c0edebcdbb12816d1be5249e4f04bcaf02048aa..585f87b23af2d92daef1787b2f38911681c0d8ee 100644
> --- a/drivers/clk/qcom/tcsrcc-glymur.c
> +++ b/drivers/clk/qcom/tcsrcc-glymur.c
> @@ -4,265 +4,115 @@
>   */
>  
>  #include <linux/clk-provider.h>
> +#include <linux/clk/qcom.h>
>  #include <linux/mod_devicetable.h>
>  #include <linux/module.h>
> +#include <linux/of.h>
>  #include <linux/platform_device.h>
>  #include <linux/regmap.h>
>  
>  #include <dt-bindings/clock/qcom,glymur-tcsr.h>
>  
> -#include "clk-alpha-pll.h"
> -#include "clk-branch.h"
> -#include "clk-pll.h"
> -#include "clk-rcg.h"
> -#include "clk-regmap.h"
> -#include "clk-regmap-divider.h"
> -#include "clk-regmap-mux.h"
> -#include "common.h"
> -#include "gdsc.h"
> -#include "reset.h"
> -
> -enum {
> -	DT_BI_TCXO_PAD,
> -};
> -
> -static struct clk_branch tcsr_edp_clkref_en = {
> -	.halt_reg = 0x60,
> -	.halt_check = BRANCH_HALT_DELAY,
> -	.clkr = {
> -		.enable_reg = 0x60,
> -		.enable_mask = BIT(0),
> -		.hw.init = &(const struct clk_init_data) {
> -			.name = "tcsr_edp_clkref_en",
> -			.parent_data = &(const struct clk_parent_data){
> -				.index = DT_BI_TCXO_PAD,
> -			},
> -			.num_parents = 1,
> -			.ops = &clk_branch2_ops,
> -		},
> +static const char * const tcsr_pcie_1_regulators[] = {
> +	"vdda-refgen-0p9",
> +	"vdda-refgen-1p2",
> +	"vdda-qrefrx5-0p9",
> +	"vdda-qreftx0-0p9",
> +	"vdda-qreftx0-1p2",
> +};
> +
> +static const char * const tcsr_pcie_2_regulators[] = {
> +	"vdda-refgen-0p9",
> +	"vdda-refgen-1p2",
> +	"vdda-qreftx1-0p9",
> +	"vdda-qrefrpt0-0p9",
> +	"vdda-qrefrpt1-0p9",
> +	"vdda-qrefrpt2-0p9",
> +	"vdda-qrefrx2-0p9",
> +};
> +
> +static const char * const tcsr_pcie_3_regulators[] = {
> +	"vdda-refgen-0p9",
> +	"vdda-refgen-1p2",
> +	"vdda-qreftx1-0p9",
> +	"vdda-qrefrpt0-0p9",
> +	"vdda-qrefrpt1-0p9",
> +	"vdda-qrefrx1-0p9",
> +};
> +
> +static const char * const tcsr_pcie_4_regulators[] = {
> +	"vdda-refgen-0p9",
> +	"vdda-refgen-1p2",
> +	"vdda-qreftx1-0p9",
> +	"vdda-qrefrpt0-0p9",
> +	"vdda-qrefrpt1-0p9",
> +	"vdda-qrefrpt2-0p9",
> +	"vdda-qrefrx2-0p9",
> +};
> +

TCSR clock refs are just not for PCIe alone, they would have supplies
for all the ref clocks. These supplies can also be shared across other
clock refs. I think it is not the correct way to handle the supplies, as
TCSR does not have the complete supplies map.


> +static const struct qcom_clk_ref_desc tcsr_cc_glymur_clk_descs[] = {
> +	[TCSR_EDP_CLKREF_EN] = {
> +		.name = "tcsr_edp_clkref_en",
> +		.offset = 0x60,
>  	},
> -};
>


-- 
Thanks,
Taniya Das


^ permalink raw reply

* [PATCH v2 0/3] SHIFT 6MQ SD-card support, improved responsivness of touchscreen, and codec
From: David Heidelberg via B4 Relay @ 2026-04-01 16:51 UTC (permalink / raw)
  To: Bjorn Andersson, Konrad Dybcio, Rob Herring, Krzysztof Kozlowski,
	Conor Dooley, Dylan Van Assche
  Cc: linux-arm-msm, Petr Hodina, Casey Connolly, Dmitry Baryshkov,
	Alexander Martinz, Konrad Dybcio, devicetree, linux-kernel,
	phone-devel, David Heidelberg

I've tested that SD card and touchscreen works well, the codec does too,
but for complete enablement needs soundcard support which isn't fully
finished.

Signed-off-by: David Heidelberg <david@ixit.cz>
---
Changes in v2:
- Changed bias-pull-up to bias-disable for SD card detection, since the
  mechanism doesn't work, we can just save few uA. (Casey)
- Link to v1: https://lore.kernel.org/r/20260320-axolotl-misc-p1-v1-0-89c9b5ecb26e@ixit.cz

---
Alexander Martinz (1):
      arm64: dts: qcom: sdm845-shift-axolotl: Enable TFA9890 codec

Casey Connolly (2):
      arm64: dts: qcom: sdm845-shift-axolotl: Enable sdcard
      arm64: dts: qcom: sdm845-shift-axolotl: Set higher touchscreen i2c clock

 arch/arm64/boot/dts/qcom/sdm845-shift-axolotl.dts | 59 +++++++++++++++++++++++
 1 file changed, 59 insertions(+)
---
base-commit: bd0f139e5fc11182777b81cefc3893ea508544ec
change-id: 20260320-axolotl-misc-p1-601d480c25df

Best regards,
-- 
David Heidelberg <david@ixit.cz>



^ permalink raw reply

* [PATCH v2 2/3] arm64: dts: qcom: sdm845-shift-axolotl: Set higher touchscreen i2c clock
From: David Heidelberg via B4 Relay @ 2026-04-01 16:51 UTC (permalink / raw)
  To: Bjorn Andersson, Konrad Dybcio, Rob Herring, Krzysztof Kozlowski,
	Conor Dooley, Dylan Van Assche
  Cc: linux-arm-msm, Petr Hodina, Casey Connolly, Dmitry Baryshkov,
	Alexander Martinz, Konrad Dybcio, devicetree, linux-kernel,
	phone-devel, David Heidelberg
In-Reply-To: <20260401-axolotl-misc-p1-v2-0-f3af384bbb50@ixit.cz>

From: Casey Connolly <casey.connolly@linaro.org>

Sets a higher rate than the default for good responsiveness of
touchscreen.

Signed-off-by: Casey Connolly <casey.connolly@linaro.org>
Reviewed-by: Dmitry Baryshkov <dmitry.baryshkov@oss.qualcomm.com>
Reviewed-by: Konrad Dybcio <konrad.dybcio@oss.qualcomm.com>
Signed-off-by: David Heidelberg <david@ixit.cz>
---
 arch/arm64/boot/dts/qcom/sdm845-shift-axolotl.dts | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/arch/arm64/boot/dts/qcom/sdm845-shift-axolotl.dts b/arch/arm64/boot/dts/qcom/sdm845-shift-axolotl.dts
index c394350998c26..181c61e8ed748 100644
--- a/arch/arm64/boot/dts/qcom/sdm845-shift-axolotl.dts
+++ b/arch/arm64/boot/dts/qcom/sdm845-shift-axolotl.dts
@@ -432,6 +432,8 @@ &gpu_zap_shader {
 };
 
 &i2c5 {
+	clock-frequency = <400000>;
+
 	status = "okay";
 
 	touchscreen@38 {

-- 
2.53.0



^ permalink raw reply related

* [PATCH v2 1/3] arm64: dts: qcom: sdm845-shift-axolotl: Enable sdcard
From: David Heidelberg via B4 Relay @ 2026-04-01 16:51 UTC (permalink / raw)
  To: Bjorn Andersson, Konrad Dybcio, Rob Herring, Krzysztof Kozlowski,
	Conor Dooley, Dylan Van Assche
  Cc: linux-arm-msm, Petr Hodina, Casey Connolly, Dmitry Baryshkov,
	Alexander Martinz, Konrad Dybcio, devicetree, linux-kernel,
	phone-devel, David Heidelberg
In-Reply-To: <20260401-axolotl-misc-p1-v2-0-f3af384bbb50@ixit.cz>

From: Casey Connolly <casey.connolly@linaro.org>

The SHIFT6mq features an sdcard slot, add it.

Signed-off-by: Casey Connolly <casey.connolly@linaro.org>
Signed-off-by: David Heidelberg <david@ixit.cz>
---
 arch/arm64/boot/dts/qcom/sdm845-shift-axolotl.dts | 44 +++++++++++++++++++++++
 1 file changed, 44 insertions(+)

diff --git a/arch/arm64/boot/dts/qcom/sdm845-shift-axolotl.dts b/arch/arm64/boot/dts/qcom/sdm845-shift-axolotl.dts
index 740eb22550724..c394350998c26 100644
--- a/arch/arm64/boot/dts/qcom/sdm845-shift-axolotl.dts
+++ b/arch/arm64/boot/dts/qcom/sdm845-shift-axolotl.dts
@@ -600,6 +600,24 @@ &qupv3_id_1 {
 	status = "okay";
 };
 
+&sdhc_2 {
+	status = "okay";
+
+	pinctrl-names = "default";
+	pinctrl-0 = <&sdc2_default_state &sdc2_card_det_n>;
+
+	vmmc-supply = <&vreg_l21a_2p95>;
+	vqmmc-supply = <&vreg_l13a_2p95>;
+
+	bus-width = <4>;
+	/*
+	 * Card detection is broken, but because the battery must be removed
+	 * to insert the card, we use this rather than the broken-cd property
+	 * which would just waste CPU cycles polling.
+	 */
+	non-removable;
+};
+
 &slpi_pas {
 	firmware-name = "qcom/sdm845/SHIFT/axolotl/slpi.mbn";
 
@@ -609,6 +627,32 @@ &slpi_pas {
 &tlmm {
 	gpio-reserved-ranges = <0 4>, <81 4>;
 
+	sdc2_default_state: sdc2-default-state {
+		clk-pins {
+			pins = "sdc2_clk";
+			bias-disable;
+			drive-strength = <16>;
+		};
+
+		cmd-pins {
+			pins = "sdc2_cmd";
+			bias-pull-up;
+			drive-strength = <10>;
+		};
+
+		data-pins {
+			pins = "sdc2_data";
+			bias-pull-up;
+			drive-strength = <10>;
+		};
+	};
+
+	sdc2_card_det_n: sd-card-det-n-state {
+		pins = "gpio126";
+		function = "gpio";
+		bias-disable;
+	};
+
 	sde_dsi_active: sde-dsi-active-state {
 		pins = "gpio6", "gpio11";
 		function = "gpio";

-- 
2.53.0



^ permalink raw reply related

* [PATCH v2 3/3] arm64: dts: qcom: sdm845-shift-axolotl: Enable TFA9890 codec
From: David Heidelberg via B4 Relay @ 2026-04-01 16:51 UTC (permalink / raw)
  To: Bjorn Andersson, Konrad Dybcio, Rob Herring, Krzysztof Kozlowski,
	Conor Dooley, Dylan Van Assche
  Cc: linux-arm-msm, Petr Hodina, Casey Connolly, Dmitry Baryshkov,
	Alexander Martinz, Konrad Dybcio, devicetree, linux-kernel,
	phone-devel, David Heidelberg
In-Reply-To: <20260401-axolotl-misc-p1-v2-0-f3af384bbb50@ixit.cz>

From: Alexander Martinz <amartinz@shiftphones.com>

Audio amplifier TFA9890 is present on the board.

Signed-off-by: Alexander Martinz <amartinz@shiftphones.com>
Reviewed-by: Konrad Dybcio <konrad.dybcio@oss.qualcomm.com>
Reviewed-by: Dmitry Baryshkov <dmitry.baryshkov@oss.qualcomm.com>
Signed-off-by: David Heidelberg <david@ixit.cz>
---
 arch/arm64/boot/dts/qcom/sdm845-shift-axolotl.dts | 13 +++++++++++++
 1 file changed, 13 insertions(+)

diff --git a/arch/arm64/boot/dts/qcom/sdm845-shift-axolotl.dts b/arch/arm64/boot/dts/qcom/sdm845-shift-axolotl.dts
index 181c61e8ed748..393eb865a71d3 100644
--- a/arch/arm64/boot/dts/qcom/sdm845-shift-axolotl.dts
+++ b/arch/arm64/boot/dts/qcom/sdm845-shift-axolotl.dts
@@ -459,6 +459,19 @@ &i2c10 {
 	/* SMB1355@0x0C */
 };
 
+&i2c11 {
+	clock-frequency = <400000>;
+
+	status = "okay";
+
+	audio-codec@34 {
+		compatible = "nxp,tfa9890";
+		reg = <0x34>;
+		vddd-supply = <&vreg_s4a_1p8>;
+		#sound-dai-cells = <0>;
+	};
+};
+
 &ipa {
 	qcom,gsi-loader = "self";
 	memory-region = <&ipa_fw_mem>;

-- 
2.53.0



^ permalink raw reply related

* Re: [PATCH v8 08/13] ASoC: qcom: q6dsp: Add Senary MI2S audio interface support
From: Srinivas Kandagatla @ 2026-04-01 16:54 UTC (permalink / raw)
  To: Mark Brown, Srinivas Kandagatla
  Cc: robh, krzk+dt, conor+dt, mohammad.rafi.shaik, linux-sound,
	lgirdwood, perex, tiwai, johan, dmitry.baryshkov, konrad.dybcio,
	linux-arm-msm, devicetree, linux-kernel, srini, val,
	mailingradian
In-Reply-To: <38eb807e-8467-4d7b-9094-3007d0ba9c89@sirena.org.uk>



On 4/1/26 11:25 AM, Mark Brown wrote:
> On Mon, Mar 30, 2026 at 08:21:00AM +0000, Srinivas Kandagatla wrote:
>> From: Mohammad Rafi Shaik <mohammad.rafi.shaik@oss.qualcomm.com>
>>
>> Introduces support for the Senary MI2S audio interface in the Qualcomm
>> q6dsp. Add new AFE port IDs for Senary MI2S RX and TX and include the
>> necessary mappings in the port configuration  to allow audio routing
>> over the Senary MI2S interface.
> 
> This breaks the build:
Yep,
[PATCH v8 09/13] ASoC: qcom: common: validate cpu dai id during parsing

re-ordering these two patches, should fix the issue.

Sorry about this, Is it fine if I send this reorder in v9?

--srini


> 
> In file included from /build/stage/linux/include/dt-bindings/sound/qcom,q6afe.h:7,
>                  from /build/stage/linux/sound/soc/qcom/qdsp6/q6afe.c:5:
> /build/stage/linux/include/dt-bindings/sound/qcom,q6dsp-lpass-ports.h:153:33: error: array index in initializer exceeds array bounds
>   153 | #define SENARY_MI2S_RX          147
>       |                                 ^~~
> /build/stage/linux/sound/soc/qcom/qdsp6/q6afe.c:723:10: note: in expansion of macro ‘SENARY_MI2S_RX’
>   723 |         [SENARY_MI2S_RX]  =  { AFE_PORT_ID_SENARY_MI2S_RX,
>       |          ^~~~~~~~~~~~~~
> /build/stage/linux/include/dt-bindings/sound/qcom,q6dsp-lpass-ports.h:153:33: note: (near initialization for ‘port_maps’)
>   153 | #define SENARY_MI2S_RX          147
>       |                                 ^~~
> /build/stage/linux/sound/soc/qcom/qdsp6/q6afe.c:723:10: note: in expansion of macro ‘SENARY_MI2S_RX’
>   723 |         [SENARY_MI2S_RX]  =  { AFE_PORT_ID_SENARY_MI2S_RX,
>       |          ^~~~~~~~~~~~~~
> /build/stage/linux/include/dt-bindings/sound/qcom,q6dsp-lpass-ports.h:154:33: error: array index in initializer exceeds array bounds
>   154 | #define SENARY_MI2S_TX          148
>       |                                 ^~~
> /build/stage/linux/sound/soc/qcom/qdsp6/q6afe.c:725:10: note: in expansion of macro ‘SENARY_MI2S_TX’
>   725 |         [SENARY_MI2S_TX] =   { AFE_PORT_ID_SENARY_MI2S_TX,
>       |          ^~~~~~~~~~~~~~
> /build/stage/linux/include/dt-bindings/sound/qcom,q6dsp-lpass-ports.h:154:33: note: (near initialization for ‘port_maps’)
>   154 | #define SENARY_MI2S_TX          148
>       |                                 ^~~
> /build/stage/linux/sound/soc/qcom/qdsp6/q6afe.c:725:10: note: in expansion of macro ‘SENARY_MI2S_TX’
>   725 |         [SENARY_MI2S_TX] =   { AFE_PORT_ID_SENARY_MI2S_TX,
>       |          ^~~~~~~~~~~~~~
> 


^ permalink raw reply

* Re: [PATCH v8 08/13] ASoC: qcom: q6dsp: Add Senary MI2S audio interface support
From: Mark Brown @ 2026-04-01 16:56 UTC (permalink / raw)
  To: Srinivas Kandagatla
  Cc: robh, krzk+dt, conor+dt, mohammad.rafi.shaik, linux-sound,
	lgirdwood, perex, tiwai, johan, dmitry.baryshkov, konrad.dybcio,
	linux-arm-msm, devicetree, linux-kernel, srini, val,
	mailingradian
In-Reply-To: <6be87f21-1da0-412a-8662-ef05ca32f3c4@oss.qualcomm.com>

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

On Wed, Apr 01, 2026 at 04:54:00PM +0000, Srinivas Kandagatla wrote:
> On 4/1/26 11:25 AM, Mark Brown wrote:

> [PATCH v8 09/13] ASoC: qcom: common: validate cpu dai id during parsing

> re-ordering these two patches, should fix the issue.

> Sorry about this, Is it fine if I send this reorder in v9?

Yes, it was otherwise fine.

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 488 bytes --]

^ permalink raw reply

* Re: [PATCH v8 00/13] ASoC: qcom: q6dsp: few fixes and enhancements
From: Mark Brown @ 2026-03-31 19:15 UTC (permalink / raw)
  To: robh, krzk+dt, conor+dt, Srinivas Kandagatla
  Cc: mohammad.rafi.shaik, linux-sound, lgirdwood, perex, tiwai, johan,
	dmitry.baryshkov, konrad.dybcio, linux-arm-msm, devicetree,
	linux-kernel, srini, val, mailingradian
In-Reply-To: <20260330082105.278055-1-srinivas.kandagatla@oss.qualcomm.com>

On Mon, 30 Mar 2026 08:20:52 +0000, Srinivas Kandagatla wrote:
> ASoC: qcom: q6dsp: few fixes and enhancements
> 
> This patchset contains few fixes for the bugs hit during testing with
> Monza EVK platform
> - around array out of bounds access on dai ids which keep extending but
>   the drivers seems to have hardcoded some numbers, fix this and clean
> the mess up
> - fix few issues discovered while trying to shut down dsp.
> - flooding rpmsg with write requests due to not resetting queue pointer,
>   fix this resetting the pointer in trigger stop.
> - possible multiple graph opens which can result in open failures.
> 
> [...]

Applied to

   https://git.kernel.org/pub/scm/linux/kernel/git/broonie/sound.git for-7.1

Thanks!

[01/13] ASoC: qcom: q6apm: move component registration to unmanaged version
        https://git.kernel.org/broonie/sound/c/b918fa0009c5
[02/13] ASoC: qcom: q6apm: remove child devices when apm is removed
        https://git.kernel.org/broonie/sound/c/53c1971fee3e
[03/13] ASoC: qcom: qdsp6: topology: check widget type before accessing data
        https://git.kernel.org/broonie/sound/c/4acb0d8ae805
[04/13] ASoC: qcom: q6apm-lpass-dai: Fix multiple graph opens
        https://git.kernel.org/broonie/sound/c/08798200d790
[05/13] ASoC: qcom: q6apm-dai: reset queue ptr on trigger stop
        https://git.kernel.org/broonie/sound/c/05e5370b04c3
[06/13] ASoC: dt-bindings: qcom: add LPASS LPI MI2S dai ids
        https://git.kernel.org/broonie/sound/c/c6e2c74a1999
[07/13] ASoC: qcom: qdsp6: lpass-ports: add support for LPASS LPI MI2S dais
        https://git.kernel.org/broonie/sound/c/2d99c2869c35
[08/13] ASoC: qcom: q6dsp: Add Senary MI2S audio interface support
        https://git.kernel.org/broonie/sound/c/cd1716423637
[09/13] ASoC: qcom: common: validate cpu dai id during parsing
        https://git.kernel.org/broonie/sound/c/4847eb57fcf1
[10/13] ASoC: qcom: qdapm-lpass-dai: correct the error message
        https://git.kernel.org/broonie/sound/c/c4c6e17a3bd5
[11/13] ASoC: qcom: q6apm-lpass-dai: move graph start to trigger
        https://git.kernel.org/broonie/sound/c/dce7c38257ad
[12/13] ASoC: qcom: qdsp6: remove search for module iid in hot path
        https://git.kernel.org/broonie/sound/c/aeb8c96748af
[13/13] ASoC: qcom: q6apm: Add support for early buffer mapping on DSP
        https://git.kernel.org/broonie/sound/c/7cc1926fcd8d

All being well this means that it will be integrated into the linux-next
tree (usually sometime in the next 24 hours) and sent to Linus during
the next merge window (or sooner if it is a bug fix), however if
problems are discovered then the patch may be dropped or reverted.

You may get further e-mails resulting from automated or manual testing
and review of the tree, please engage with people reporting problems and
send followup patches addressing any issues that are reported if needed.

If any updates are required or you are submitting further changes they
should be sent as incremental updates against current git, existing
patches will not be replaced.

Please add any relevant lists and maintainers to the CCs when replying
to this mail.

Thanks,
Mark


^ permalink raw reply

* Re: Re: Re: Re: [PATCH 1/2] dt-bindings: gpu: mali-valhall-csf: Document i.MX952 support
From: Liviu Dudau @ 2026-04-01 17:20 UTC (permalink / raw)
  To: Guangliu Ding
  Cc: Daniel Baluta (OSS), Daniel Almeida, Alice Ryhl, Boris Brezillon,
	Steven Price, David Airlie, Simona Vetter, Maarten Lankhorst,
	Maxime Ripard, Thomas Zimmermann, Rob Herring,
	Krzysztof Kozlowski, Conor Dooley, Frank Li, Sascha Hauer,
	Pengutronix Kernel Team, Fabio Estevam,
	dri-devel@lists.freedesktop.org, devicetree@vger.kernel.org,
	linux-kernel@vger.kernel.org, imx@lists.linux.dev,
	linux-arm-kernel@lists.infradead.org, Jiyu Yang
In-Reply-To: <AM0PR04MB4707604AF2C445971C7474EFF350A@AM0PR04MB4707.eurprd04.prod.outlook.com>

On Wed, Apr 01, 2026 at 03:59:23PM +0000, Guangliu Ding wrote:
> Hi Liviu
> 
> > On Wed, Apr 01, 2026 at 10:31:01AM +0000, Guangliu Ding wrote:
> > > Hi Liviu
> > >
> > > > On Wed, Apr 01, 2026 at 09:43:12AM +0000, Guangliu Ding wrote:
> > > > > Hi Daniel
> > > > >
> > > > > > On 4/1/26 11:48, Guangliu Ding wrote:
> > > > > > > [You don't often get email from guangliu.ding@nxp.com. Learn
> > > > > > > why this is important at
> > > > > > > https://aka.ms/LearnAboutSenderIdentification
> > > > > > > ]
> > > > > > >
> > > > > > > Hi Liviu
> > > > > > >
> > > > > > > Thanks for your review. Please refer to my comments below:
> > > > > > >
> > > > > > >> On Tue, Mar 31, 2026 at 06:12:38PM +0800, Guangliu Ding wrote:
> > > > > > >>> Add compatible string of Mali G310 GPU on i.MX952 board.
> > > > > > >>>
> > > > > > >>> Signed-off-by: Guangliu Ding <guangliu.ding@nxp.com>
> > > > > > >>> Reviewed-by: Jiyu Yang <jiyu.yang@nxp.com>
> > > > > > >>> ---
> > > > > > >>>
> > > > > > >>> Documentation/devicetree/bindings/gpu/arm,mali-valhall-csf.y
> > > > > > >>> aml
> > > > > > >>> | 1
> > > > > > >>> +
> > > > > > >>>  1 file changed, 1 insertion(+)
> > > > > > >>>
> > > > > > >>> diff --git
> > > > > > >>> a/Documentation/devicetree/bindings/gpu/arm,mali-valhall-csf
> > > > > > >>> .yam
> > > > > > >>> l
> > > > > > >> b/Documentation/devicetree/bindings/gpu/arm,mali-valhall-csf.
> > > > > > >> yaml
> > > > > > >>> index 8eccd4338a2b..6a10843a26e2 100644
> > > > > > >>> ---
> > > > > > >>> a/Documentation/devicetree/bindings/gpu/arm,mali-valhall-csf
> > > > > > >>> .yam
> > > > > > >>> l
> > > > > > >>> +++ b/Documentation/devicetree/bindings/gpu/arm,mali-valhall
> > > > > > >>> +++ -csf
> > > > > > >>> +++ .yam
> > > > > > >>> +++ l
> > > > > > >>> @@ -20,6 +20,7 @@ properties:
> > > > > > >>>            - enum:
> > > > > > >>>                - mediatek,mt8196-mali
> > > > > > >>>                - nxp,imx95-mali            # G310
> > > > > > >>> +              - nxp,imx952-mali           # G310
> > > > > > >> Can you explain why this is needed? Can it not be covered by
> > > > > > >> the existing compatible?
> > > > > > > There are functional differences in GPU module (GPUMIX)
> > > > > > > between
> > > > > > > i.MX95 and i.MX952. So they cannot be fully covered by a
> > > > > > > single existing
> > > > compatible.
> > > > > > > On i.MX952, The GPU clock is controlled by hardware GPU auto
> > > > > > > clock-gating mechanism, while the GPU clock is managed
> > > > > > > explicitly by the
> > > > > > driver on i.MX95.
> > > > > > > Because of these behavioral differences, separate compatible
> > > > > > > strings "nxp,imx95-mali" and "nxp,imx952-mali" are needed to
> > > > > > > allow the driver to handle the two variants independently and
> > > > > > > to keep room for future
> > > > > > divergence.
> > > > > >
> > > > > >
> > > > > > This information should be added in the commit message
> > > > > > explaining why
> > > > > >
> > > > > > the change is needed.
> > > > > >
> > > > > >
> > > > > > But then where is the driver code taking care of these diferences?
> > > > > >
> > > > >
> > > > > Yes. Currently the driver does not require "nxp,imx952-mali" string.
> > > > > However, when GPU ipa_counters are enabled to calculate the GPU
> > > > > busy_time/idle_time for GPU DVFS feature, they will conflict with
> > > > > the hardware GPU auto clock‑gating mechanism, causing GPU clock to
> > > > > remain
> > > > always on.
> > > > > In such cases, ipa_counters need to be disabled so that the GPU
> > > > > auto clock‑gating mechanism can operate normally, using
> > "nxp,imx952-mali"
> > > > string.
> > > >
> > > > OK, I understand that you're following guidance from some other
> > > > senior people on how to upstream patches so you've tried to create
> > > > the smallest patchset to ensure that it gets reviewed and accepted,
> > > > but in this case we need to see the other patches as well to decide
> > > > if your approach is the right one and we do need a separate compatible
> > string.
> > > >
> > > > If enabling GPU ipa_counters causes the clocks to get stuck active,
> > > > that feels like a hardware bug, so figuring out how to handle that
> > > > is more important than adding a compatible string.
> > > >
> > > > Either add the patch(es) that use the compatible to this series in
> > > > v2, or put a comment in the commit message on where we can see the
> > driver changes.
> > > >
> > >
> > > According to discussions with the GPU vendor, this is a hardware
> > > limitation of Mali-G310 rather than a hardware bug, and it has been
> > > addressed in newer Mali GPU families.
> > 
> > I represent the said GPU vendor and I think I know what you're talking about,
> > but you're taking the wrong approach. All G310s have a problem where in
> > order to enable access to the ipa_counters the automatic clock gating gets
> > disabled. So the solution that needs to be implemented when we add support
> > for IPA_COUNTERs will apply to all GPUs, not just MX952.
> 
> Yes. We have bring-up G310 (V2) GPU on both i.MX95 and i.MX952. And auto clock 
> gating mechanism is firstly introduced in i.MX952 (not supported on i.MX95).
> According to your update, solution needs to be implemented to all GPUs which support 
> auto clock gating mechanism after IPA_COUNTERs are supported in the driver, right?

A solution is needed, yes.

> What's your suggestions for 952 gpu dtb node?

There is no IPA_COUNTER use in Panthor at the moment. Unless your DVFS controller uses
that, I would suggest that we don't introduce a compatible for 952 until the time we
add support for reading the counters.

It helps if you think in terms of what is already in upstream, rather than mixing with
the tests that uses kbase code. Does your hardware need extra code in upstream in order
to function? If so, where is that code? If not, then let's not introduce the compatible
until we are absolutely sure we need it because we have code specific to that SoC. For
everything else we will implement an architecture fix if needed.

Best regards,
Liviu

-- 
====================
| I would like to |
| fix the world,  |
| but they're not |
| giving me the   |
 \ source code!  /
  ---------------
    ¯\_(ツ)_/¯

^ permalink raw reply

* Re: [PATCH v10 1/6] dt-bindings: pinctrl: add NXP MC33978/MC34978 MSDI
From: Rob Herring (Arm) @ 2026-04-01 17:39 UTC (permalink / raw)
  To: Oleksij Rempel
  Cc: Peter Rosin, Krzysztof Kozlowski, Guenter Roeck, Linus Walleij,
	linux-gpio, David Jander, linux-hwmon, kernel, Lee Jones,
	devicetree, linux-kernel, Conor Dooley
In-Reply-To: <20260401162510.1706073-2-o.rempel@pengutronix.de>


On Wed, 01 Apr 2026 18:25:05 +0200, Oleksij Rempel wrote:
> Add device tree binding documentation for the NXP MC33978 and MC34978
> Multiple Switch Detection Interface (MSDI) devices.
> 
> The MC33978 and MC34978 differ primarily in their operating temperature
> ranges. While not software-detectable, providing specific compatible
> strings allows the hwmon subsystem to correctly interpret thermal
> thresholds and hardware faults.
> 
> These ICs monitor up to 22 mechanical switch contacts in automotive and
> industrial environments. They provide configurable wetting currents to
> break through contact oxidation and feature extensive hardware
> protection against thermal overload and voltage transients (load
> dumps/brown-outs).
> 
> The device interfaces via SPI. While it provides multiple functions, its
> primary hardware purpose is pin/switch control. To accurately represent
> the hardware as a single physical integrated circuit without unnecessary
> DT overhead, all functions are flattened into a single pinctrl node:
> - pinctrl: Exposing the 22 switch inputs (SG/SP pins) as a GPIO controller
>   and managing their pin configurations.
> - hwmon: Exposing critical hardware faults (OT, OV, UV) and static
>   voltage/temperature thresholds.
> - mux: Controlling the 24-to-1 analog multiplexer to route pin voltages,
>   internal temperature, or battery voltage to an external SoC ADC.
> 
> Signed-off-by: Oleksij Rempel <o.rempel@pengutronix.de>
> Reviewed-by: Rob Herring (Arm) <robh@kernel.org>
> Reviewed-by: Linus Walleij <linusw@kernel.org>
> ---
> changes v10:
> - no changes
> changes v9:
> - no changes
> changes v8:
> - Update IRQ_TYPE_* macros include path reference in documentation from
>   interrupt-controller.h to dt-bindings/interrupt-controller/irq.h.
> - Add bias-disable, drive-open-drain, drive-open-source, and drive-strength
>   to the list of supported pin configuration properties.
> changes v7:
> - no changes
> changes v6:
> - add Reviewed-by: Rob Herring (Arm) <robh@kernel.org>
> - add Reviewed-by: Linus Walleij <linusw@kernel.org>
> changes v5:
> - Commit Message: Added justification for distinct compatible strings
>   based on temperature ranges.
> - Restricted pins property to an explicit enum of valid hardware pins
> changes v4:
> - Drop the standalone mfd/nxp,mc33978.yaml schema entirely.
> - Move the unified device binding to bindings/pinctrl/nxp,mc33978.yaml,
> - Remove the dedicated child node compatible strings (nxp,mc33978-pinctrl).
> - Flatten the pinctrl/gpio properties directly into the main SPI device
>   node.
> changes v3:
> - Drop regular expression pattern from pinctrl child node and define
>   it as a standard property
> - Reorder required properties list in MFD binding
> - Remove stray blank line from the MFD binding devicetree example
> - Replace unevaluatedProperties with additionalProperties in the pinctrl
>   binding
> changes v2:
> - Squashed MFD, pinctrl, hwmon, and mux bindings into a single patch
> - Removed the empty hwmon child node
> - Folded the mux-controller node into the parent MFD node
> - Added vbatp-supply and vddq-supply to the required properties block
> - Changed the example node name from mc33978@0 to gpio@0
> - Removed unnecessary literal block scalars (|) from descriptions
> - Documented SG, SP, and SB pin acronyms in the pinctrl description
> - Added consumer polarity guidance (GPIO_ACTIVE_LOW/HIGH) for SG/SB
>   inputs, with a note on output circuit dependency
> - Updated commit message
> ---
>  .../bindings/pinctrl/nxp,mc33978.yaml         | 158 ++++++++++++++++++
>  1 file changed, 158 insertions(+)
>  create mode 100644 Documentation/devicetree/bindings/pinctrl/nxp,mc33978.yaml
> 

My bot found errors running 'make dt_binding_check' on your patch:

yamllint warnings/errors:

dtschema/dtc warnings/errors:
/builds/robherring/dt-review-ci/linux/Documentation/devicetree/bindings/pinctrl/nxp,mc33978.example.dtb: gpio@0 (nxp,mc33978): $nodename:0: 'gpio@0' does not match '^mux-controller(@.*|-([0-9]|[1-9][0-9]+))?$'
	from schema $id: http://devicetree.org/schemas/mux/mux-controller.yaml

doc reference errors (make refcheckdocs):

See https://patchwork.kernel.org/project/devicetree/patch/20260401162510.1706073-2-o.rempel@pengutronix.de

The base for the series is generally the latest rc1. A different dependency
should be noted in *this* patch.

If you already ran 'make dt_binding_check' and didn't see the above
error(s), then make sure 'yamllint' is installed and dt-schema is up to
date:

pip3 install dtschema --upgrade

Please check and re-submit after running the above command yourself. Note
that DT_SCHEMA_FILES can be set to your schema file to speed up checking
your schema. However, it must be unset to test all examples with your schema.


^ permalink raw reply

* RE: [EXT] Re: [PATCH 1/2] dt-bindings: gpu: mali-valhall-csf: Document i.MX952 support
From: Guangliu Ding @ 2026-04-01 17:52 UTC (permalink / raw)
  To: Krzysztof Kozlowski, Liviu Dudau
  Cc: Daniel Baluta (OSS), Daniel Almeida, Alice Ryhl, Boris Brezillon,
	Steven Price, David Airlie, Simona Vetter, Maarten Lankhorst,
	Maxime Ripard, Thomas Zimmermann, Rob Herring,
	Krzysztof Kozlowski, Conor Dooley, Frank Li, Sascha Hauer,
	Pengutronix Kernel Team, Fabio Estevam,
	dri-devel@lists.freedesktop.org, devicetree@vger.kernel.org,
	linux-kernel@vger.kernel.org, imx@lists.linux.dev,
	linux-arm-kernel@lists.infradead.org, Jiyu Yang
In-Reply-To: <562530e9-a607-4dae-9b43-786a3946f795@kernel.org>

Hi Krzysztof

Thank you for the helpful guidance.
After going through the writing binding file and the corresponding
driver/dts code, I have a few updates to share:

> On 01/04/2026 13:27, Guangliu Ding wrote:
> >>>>
> >>>> We discuss only the lack of compatibility in terms of DT, how DT
> >>>> sees compatible devices.
> >>>>
> >>>> And lack of driver code is clear indication that devices are
> >>>> compatible in terms how DT understands it. Feel encouraged to bring
> >>>> actual arguments in commit msgs in the future.
> >>>>
> >>>> Best regards,
> >>>> Krzysztof
> >>>
> >>> So the best approach is only reserve "arm,mali-valhall-csf" for now,
> >>> since currently there is no need for an additional compatible entry
> >>> from a DT
> >> compatibility perspective.
> >>> We can introduce "nxp,imx952-mali" in future commits if hardware or
> >>> driver differences actually require it, and include more detailed
> >>> justification
> >> in the commit message. Right?
> >>
> >> So does that mean you decided not to read writing bindings document?
> >
> > Actually, I followed the compatible string of gpu node in imx952.dtsi
> > during code work since they share the same GPU IP.
> >          gpu: gpu@4d900000 {
> >              compatible = "nxp,imx95-mali", "arm,mali-valhall-csf"; >
> >
> > Is this line in writing bindings document that you want to mention about?
> > Could you please share more suggestions about the patch optimization?
> >               DO add new compatibles in case there are new features or
> bugs.
> 
> Please read entire file. I feel like you avoid doing this and just ask me to give
> you shortcut.
> 
> There is more than one point from that trivial writing bindings file which
> applies here.
>
> Best regards,
> Krzysztof

DO use fallback compatibles when devices are the same as or a superset of prior implementations.
Under this rule, the following compatible string is correct since the GPU on
i.MX952 is fully compatible with the GPU on i.MX95 now.
compatible = "nxp,imx95-mali", "arm,mali-valhall-csf";

DO add new compatibles in case there are new features or bugs.
Under this rule, the new compatible string is required to support new feature
and the old one must be kept if the GPU auto clock gating mechanism on i.MX952
needs additional fixes after ipa_counters are enabled in the driver.
compatible = "nxp,imx952-mali", "nxp,imx95-mali", "arm,mali-valhall-csf";

^ permalink raw reply

* Re: [PATCH] dt-bindings: mfd: twl: Reference converted YAML schemas for subnodes
From: Jihed Chaibi @ 2026-04-01 17:57 UTC (permalink / raw)
  To: Krzysztof Kozlowski
  Cc: lee, andreas, robh, krzk+dt, conor+dt, devicetree, linux-kernel
In-Reply-To: <20260327-orthodox-brainy-poodle-738eac@quoll>

On Fri, Mar 27, 2026 at 8:37 AM Krzysztof Kozlowski <krzk@kernel.org> wrote:
>
> No, they were validated by their child device schemas. Everything was
> correct and expected.
>
> Best regards,
> Krzysztof
>

Thanks for the feedback Krzysztof, I have sent a v2 of this patch,
please let me know if anything else needs to be done.

Best regards,
Jihed

^ permalink raw reply

* RE: Re: Re: Re: Re: [PATCH 1/2] dt-bindings: gpu: mali-valhall-csf: Document i.MX952 support
From: Guangliu Ding @ 2026-04-01 18:03 UTC (permalink / raw)
  To: Liviu Dudau
  Cc: Daniel Baluta (OSS), Daniel Almeida, Alice Ryhl, Boris Brezillon,
	Steven Price, David Airlie, Simona Vetter, Maarten Lankhorst,
	Maxime Ripard, Thomas Zimmermann, Rob Herring,
	Krzysztof Kozlowski, Conor Dooley, Frank Li, Sascha Hauer,
	Pengutronix Kernel Team, Fabio Estevam,
	dri-devel@lists.freedesktop.org, devicetree@vger.kernel.org,
	linux-kernel@vger.kernel.org, imx@lists.linux.dev,
	linux-arm-kernel@lists.infradead.org, Jiyu Yang
In-Reply-To: <ac1T282yvpdAp_TP@e142607>

Hi Liviu

Thanks a lot for your sharing.

> On Wed, Apr 01, 2026 at 03:59:23PM +0000, Guangliu Ding wrote:
> > Hi Liviu
> >
> > > On Wed, Apr 01, 2026 at 10:31:01AM +0000, Guangliu Ding wrote:
> > > > Hi Liviu
> > > >
> > > > > On Wed, Apr 01, 2026 at 09:43:12AM +0000, Guangliu Ding wrote:
> > > > > > Hi Daniel
> > > > > >
> > > > > > > On 4/1/26 11:48, Guangliu Ding wrote:
> > > > > > > > [You don't often get email from guangliu.ding@nxp.com.
> > > > > > > > Learn why this is important at
> > > > > > > > https://aka.ms/LearnAboutSenderIdentification
> > > > > > > > ]
> > > > > > > >
> > > > > > > > Hi Liviu
> > > > > > > >
> > > > > > > > Thanks for your review. Please refer to my comments below:
> > > > > > > >
> > > > > > > >> On Tue, Mar 31, 2026 at 06:12:38PM +0800, Guangliu Ding
> wrote:
> > > > > > > >>> Add compatible string of Mali G310 GPU on i.MX952 board.
> > > > > > > >>>
> > > > > > > >>> Signed-off-by: Guangliu Ding <guangliu.ding@nxp.com>
> > > > > > > >>> Reviewed-by: Jiyu Yang <jiyu.yang@nxp.com>
> > > > > > > >>> ---
> > > > > > > >>>
> > > > > > > >>> Documentation/devicetree/bindings/gpu/arm,mali-valhall-c
> > > > > > > >>> sf.y
> > > > > > > >>> aml
> > > > > > > >>> | 1
> > > > > > > >>> +
> > > > > > > >>>  1 file changed, 1 insertion(+)
> > > > > > > >>>
> > > > > > > >>> diff --git
> > > > > > > >>> a/Documentation/devicetree/bindings/gpu/arm,mali-valhall
> > > > > > > >>> -csf
> > > > > > > >>> .yam
> > > > > > > >>> l
> > > > > > > >> b/Documentation/devicetree/bindings/gpu/arm,mali-valhall-csf.
> > > > > > > >> yaml
> > > > > > > >>> index 8eccd4338a2b..6a10843a26e2 100644
> > > > > > > >>> ---
> > > > > > > >>> a/Documentation/devicetree/bindings/gpu/arm,mali-valhall
> > > > > > > >>> -csf
> > > > > > > >>> .yam
> > > > > > > >>> l
> > > > > > > >>> +++ b/Documentation/devicetree/bindings/gpu/arm,mali-val
> > > > > > > >>> +++ hall
> > > > > > > >>> +++ -csf
> > > > > > > >>> +++ .yam
> > > > > > > >>> +++ l
> > > > > > > >>> @@ -20,6 +20,7 @@ properties:
> > > > > > > >>>            - enum:
> > > > > > > >>>                - mediatek,mt8196-mali
> > > > > > > >>>                - nxp,imx95-mali            # G310
> > > > > > > >>> +              - nxp,imx952-mali           # G310
> > > > > > > >> Can you explain why this is needed? Can it not be covered
> > > > > > > >> by the existing compatible?
> > > > > > > > There are functional differences in GPU module (GPUMIX)
> > > > > > > > between
> > > > > > > > i.MX95 and i.MX952. So they cannot be fully covered by a
> > > > > > > > single existing
> > > > > compatible.
> > > > > > > > On i.MX952, The GPU clock is controlled by hardware GPU
> > > > > > > > auto clock-gating mechanism, while the GPU clock is
> > > > > > > > managed explicitly by the
> > > > > > > driver on i.MX95.
> > > > > > > > Because of these behavioral differences, separate
> > > > > > > > compatible strings "nxp,imx95-mali" and "nxp,imx952-mali"
> > > > > > > > are needed to allow the driver to handle the two variants
> > > > > > > > independently and to keep room for future
> > > > > > > divergence.
> > > > > > >
> > > > > > >
> > > > > > > This information should be added in the commit message
> > > > > > > explaining why
> > > > > > >
> > > > > > > the change is needed.
> > > > > > >
> > > > > > >
> > > > > > > But then where is the driver code taking care of these diferences?
> > > > > > >
> > > > > >
> > > > > > Yes. Currently the driver does not require "nxp,imx952-mali" string.
> > > > > > However, when GPU ipa_counters are enabled to calculate the
> > > > > > GPU busy_time/idle_time for GPU DVFS feature, they will
> > > > > > conflict with the hardware GPU auto clock‑gating mechanism,
> > > > > > causing GPU clock to remain
> > > > > always on.
> > > > > > In such cases, ipa_counters need to be disabled so that the
> > > > > > GPU auto clock‑gating mechanism can operate normally, using
> > > "nxp,imx952-mali"
> > > > > string.
> > > > >
> > > > > OK, I understand that you're following guidance from some other
> > > > > senior people on how to upstream patches so you've tried to
> > > > > create the smallest patchset to ensure that it gets reviewed and
> > > > > accepted, but in this case we need to see the other patches as
> > > > > well to decide if your approach is the right one and we do need
> > > > > a separate compatible
> > > string.
> > > > >
> > > > > If enabling GPU ipa_counters causes the clocks to get stuck
> > > > > active, that feels like a hardware bug, so figuring out how to
> > > > > handle that is more important than adding a compatible string.
> > > > >
> > > > > Either add the patch(es) that use the compatible to this series
> > > > > in v2, or put a comment in the commit message on where we can
> > > > > see the
> > > driver changes.
> > > > >
> > > >
> > > > According to discussions with the GPU vendor, this is a hardware
> > > > limitation of Mali-G310 rather than a hardware bug, and it has
> > > > been addressed in newer Mali GPU families.
> > >
> > > I represent the said GPU vendor and I think I know what you're
> > > talking about, but you're taking the wrong approach. All G310s have
> > > a problem where in order to enable access to the ipa_counters the
> > > automatic clock gating gets disabled. So the solution that needs to
> > > be implemented when we add support for IPA_COUNTERs will apply to all
> GPUs, not just MX952.
> >
> > Yes. We have bring-up G310 (V2) GPU on both i.MX95 and i.MX952. And
> > auto clock gating mechanism is firstly introduced in i.MX952 (not supported
> on i.MX95).
> > According to your update, solution needs to be implemented to all GPUs
> > which support auto clock gating mechanism after IPA_COUNTERs are
> supported in the driver, right?
> 
> A solution is needed, yes.
> 
> > What's your suggestions for 952 gpu dtb node?
> 
> There is no IPA_COUNTER use in Panthor at the moment. Unless your DVFS
> controller uses that, I would suggest that we don't introduce a compatible for
> 952 until the time we add support for reading the counters.
> 
> It helps if you think in terms of what is already in upstream, rather than mixing
> with the tests that uses kbase code. Does your hardware need extra code in
> upstream in order to function? If so, where is that code? If not, then let's not
> introduce the compatible until we are absolutely sure we need it because we
> have code specific to that SoC. For everything else we will implement an
> architecture fix if needed.
> 

Got it. The following compatible string is the correct choice since the GPU on
i.MX952 is fully compatible with the GPU on i.MX95 now.
compatible = "nxp,imx95-mali", "arm,mali-valhall-csf";

I will not mix tests with kbase code in the following upstream patches for panthor driver.

> Best regards,
> Liviu
> 
> --
> ====================
> | I would like to |
> | fix the world,  |
> | but they're not |
> | giving me the   |
>  \ source code!  /
>   ---------------
>     ¯\_(ツ)_/¯

^ permalink raw reply

* Re: [PATCH v2] dt-bindings: mfd: twl: Reference converted schemas for subnodes
From: Krzysztof Kozlowski @ 2026-04-01 18:03 UTC (permalink / raw)
  To: Jihed Chaibi, andreas
  Cc: lee, robh, krzk+dt, conor+dt, devicetree, linux-kernel
In-Reply-To: <20260327082350.20573-1-jihed.chaibi.dev@gmail.com>

On 27/03/2026 09:23, Jihed Chaibi wrote:
> Now that all TWL subnode bindings (audio, keypad, twl4030-usb, gpio,
> usb-comparator) have been converted and merged into mainline, update the
> parent ti,twl.yaml to properly reference them via $ref.
> 
> Replace the inline compatible definitions with $ref to the appropriate
> schemas and unevaluatedProperties: false, matching the pattern already
> used by the madc, gpadc, and charger subnodes.

I told you: no.

Why do you replace correct code?

Best regards,
Krzysztof

^ permalink raw reply

* Re: [PATCH] dt-bindings: mfd: twl: Reference converted YAML schemas for subnodes
From: Krzysztof Kozlowski @ 2026-04-01 18:04 UTC (permalink / raw)
  To: Jihed Chaibi
  Cc: lee, andreas, robh, krzk+dt, conor+dt, devicetree, linux-kernel
In-Reply-To: <CANBuOYrPuLKnxqOLOZUEvBSG-4ceeOWMhQKPZhEctzcjBUnbqA@mail.gmail.com>

On 01/04/2026 19:57, Jihed Chaibi wrote:
> On Fri, Mar 27, 2026 at 8:37 AM Krzysztof Kozlowski <krzk@kernel.org> wrote:
>>
>> No, they were validated by their child device schemas. Everything was
>> correct and expected.
>>
>> Best regards,
>> Krzysztof
>>
> 
> Thanks for the feedback Krzysztof, I have sent a v2 of this patch,
> please let me know if anything else needs to be done.

Really? On 27th of Match, 40 minutes after I told you NO, you send the same?


Best regards,
Krzysztof

^ permalink raw reply

* [PATCH v6 3/8] i2c: rtl9300: introduce F_BUSY to the reg_fields struct
From: Rustam Adilov @ 2026-04-01 18:06 UTC (permalink / raw)
  To: Chris Packham, Andi Shyti, Rob Herring, Krzysztof Kozlowski,
	Conor Dooley, linux-i2c, devicetree, linux-kernel
  Cc: Rustam Adilov
In-Reply-To: <20260401180648.337834-1-adilov@disroot.org>

In RTL9607C i2c controller the busy check operation is done on the
separate bit of the command register as opposed to self clearing
command trigger bit on the rtl9300 and rtl9310 i2c controllers.

Introduce a new F_BUSY field to the reg_fields struct for that
and change the regmap read poll function to use F_BUSY
instead of I2C_TRIG.

Reviewed-by: Chris Packham <chris.packham@alliedtelesis.co.nz>
Signed-off-by: Rustam Adilov <adilov@disroot.org>
---
 drivers/i2c/busses/i2c-rtl9300.c | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/drivers/i2c/busses/i2c-rtl9300.c b/drivers/i2c/busses/i2c-rtl9300.c
index 2cada6038b44..e40b4692a3fa 100644
--- a/drivers/i2c/busses/i2c-rtl9300.c
+++ b/drivers/i2c/busses/i2c-rtl9300.c
@@ -53,6 +53,7 @@ enum rtl9300_i2c_reg_fields {
 	F_SCL_SEL,
 	F_SDA_OUT_SEL,
 	F_SDA_SEL,
+	F_BUSY,
 
 	/* keep last */
 	F_NUM_FIELDS
@@ -262,7 +263,7 @@ static int rtl9300_i2c_do_xfer(struct rtl9300_i2c *i2c, struct rtl9300_i2c_xfer
 	if (ret)
 		return ret;
 
-	ret = regmap_field_read_poll_timeout(i2c->fields[F_I2C_TRIG], val, !val, 100, 100000);
+	ret = regmap_field_read_poll_timeout(i2c->fields[F_BUSY], val, !val, 100, 100000);
 	if (ret)
 		return ret;
 
@@ -505,6 +506,7 @@ static const struct rtl9300_i2c_drv_data rtl9300_i2c_drv_data = {
 		[F_MEM_ADDR_WIDTH]	= MST_REG_FIELD(RTL9300_I2C_MST_CTRL2, 2, 3),
 		[F_SCL_FREQ]		= MST_REG_FIELD(RTL9300_I2C_MST_CTRL2, 0, 1),
 		[F_SDA_SEL]		= GLB_REG_FIELD(RTL9300_I2C_MST_GLB_CTRL, 0, 7),
+		[F_BUSY]		= MST_REG_FIELD(RTL9300_I2C_MST_CTRL1, 0, 0),
 	},
 	.select_scl = rtl9300_i2c_select_scl,
 	.rd_reg = RTL9300_I2C_MST_DATA_WORD0,
@@ -527,6 +529,7 @@ static const struct rtl9300_i2c_drv_data rtl9310_i2c_drv_data = {
 		[F_I2C_FAIL]		= MST_REG_FIELD(RTL9310_I2C_MST_CTRL, 1, 1),
 		[F_I2C_TRIG]		= MST_REG_FIELD(RTL9310_I2C_MST_CTRL, 0, 0),
 		[F_MEM_ADDR]		= MST_REG_FIELD(RTL9310_I2C_MST_MEMADDR_CTRL, 0, 23),
+		[F_BUSY]		= MST_REG_FIELD(RTL9310_I2C_MST_CTRL, 0, 0),
 	},
 	.select_scl = rtl9310_i2c_select_scl,
 	.rd_reg = RTL9310_I2C_MST_DATA_CTRL,
-- 
2.53.0


^ permalink raw reply related


This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox