public inbox for devicetree@vger.kernel.org
 help / color / mirror / Atom feed
* [PATCH v1 0/8] mfd: Add support for NXP MC33978/MC34978 MSDI
@ 2026-02-25 17:15 Oleksij Rempel
  2026-02-25 17:15 ` [PATCH v1 1/8] dt-bindings: mfd: add " Oleksij Rempel
                   ` (7 more replies)
  0 siblings, 8 replies; 20+ messages in thread
From: Oleksij Rempel @ 2026-02-25 17:15 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

This series adds support for the NXP MC33978/MC34978 Multiple Switch Detection
Interface (MSDI) via the MFD framework.

Architecture overview:
* mfd: Core driver handling 2-frame pipelined SPI, regulator sequencing, and
  linear irq_domain. Harvests status bits from SPI MISO MSB.
* pinctrl: Exposes 22 physical switch inputs as standard GPIOs. Proxies IRQs to
  the MFD domain.
* hwmon: Exposes thermal limits, VBATP/VDDQ voltage boundaries, and dynamic
  fault alarms.
* mux: Controls the 24-to-1 AMUX routing analog signals (switch voltages,
  temperature, VBATP) to an external ADC.

Initial pinctrl implementation by David Jander, reworked into this MFD
architecture.

Best regards,
Oleksij

David Jander (1):
  pinctrl: add NXP MC33978/MC34978 pinctrl driver

Oleksij Rempel (7):
  dt-bindings: mfd: add NXP MC33978/MC34978 MSDI
  mfd: add NXP MC33978/MC34978 core driver
  dt-bindings: pinctrl: add NXP MC33978/MC34978 pinctrl
  dt-bindings: hwmon: add NXP MC33978/MC34978 hwmon
  hwmon: add NXP MC33978/MC34978 driver
  dt-bindings: mux: add NXP MC33978/MC34978 AMUX
  mux: add NXP MC33978/MC34978 AMUX driver

 .../bindings/hwmon/nxp,mc33978-hwmon.yaml     |  34 +
 .../devicetree/bindings/mfd/nxp,mc33978.yaml  | 119 +++
 .../bindings/mux/nxp,mc33978-mux.yaml         |  55 ++
 .../bindings/pinctrl/nxp,mc33978-pinctrl.yaml |  66 ++
 drivers/hwmon/Kconfig                         |  10 +
 drivers/hwmon/Makefile                        |   1 +
 drivers/hwmon/mc33978-hwmon.c                 | 439 ++++++++++
 drivers/mfd/Kconfig                           |  13 +
 drivers/mfd/Makefile                          |   2 +
 drivers/mfd/mc33978.c                         | 749 ++++++++++++++++++
 drivers/mux/Kconfig                           |  14 +
 drivers/mux/Makefile                          |   2 +
 drivers/mux/mc33978-mux.c                     | 119 +++
 drivers/pinctrl/Kconfig                       |  14 +
 drivers/pinctrl/Makefile                      |   1 +
 drivers/pinctrl/pinctrl-mc33978.c             | 709 +++++++++++++++++
 include/linux/mfd/mc33978.h                   |  97 +++
 17 files changed, 2444 insertions(+)
 create mode 100644 Documentation/devicetree/bindings/hwmon/nxp,mc33978-hwmon.yaml
 create mode 100644 Documentation/devicetree/bindings/mfd/nxp,mc33978.yaml
 create mode 100644 Documentation/devicetree/bindings/mux/nxp,mc33978-mux.yaml
 create mode 100644 Documentation/devicetree/bindings/pinctrl/nxp,mc33978-pinctrl.yaml
 create mode 100644 drivers/hwmon/mc33978-hwmon.c
 create mode 100644 drivers/mfd/mc33978.c
 create mode 100644 drivers/mux/mc33978-mux.c
 create mode 100644 drivers/pinctrl/pinctrl-mc33978.c
 create mode 100644 include/linux/mfd/mc33978.h

--
2.47.3


^ permalink raw reply	[flat|nested] 20+ messages in thread

* [PATCH v1 1/8] dt-bindings: mfd: add NXP MC33978/MC34978 MSDI
  2026-02-25 17:15 [PATCH v1 0/8] mfd: Add support for NXP MC33978/MC34978 MSDI Oleksij Rempel
@ 2026-02-25 17:15 ` Oleksij Rempel
  2026-02-26  8:16   ` Krzysztof Kozlowski
  2026-02-25 17:15 ` [PATCH v1 2/8] mfd: add NXP MC33978/MC34978 core driver Oleksij Rempel
                   ` (6 subsequent siblings)
  7 siblings, 1 reply; 20+ messages in thread
From: Oleksij Rempel @ 2026-02-25 17:15 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

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

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 and is modeled as an MFD due to its discrete
internal functional blocks:
- pinctrl: Manages the 22 switch inputs (SG/SP pins).
- hwmon: Exposes critical hardware faults (OT, OV, UV) and static
  voltage/temperature thresholds.
- mux: Controls 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>
---
 .../devicetree/bindings/mfd/nxp,mc33978.yaml  | 86 +++++++++++++++++++
 1 file changed, 86 insertions(+)
 create mode 100644 Documentation/devicetree/bindings/mfd/nxp,mc33978.yaml

diff --git a/Documentation/devicetree/bindings/mfd/nxp,mc33978.yaml b/Documentation/devicetree/bindings/mfd/nxp,mc33978.yaml
new file mode 100644
index 000000000000..85720f389509
--- /dev/null
+++ b/Documentation/devicetree/bindings/mfd/nxp,mc33978.yaml
@@ -0,0 +1,86 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/mfd/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.
+
+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 interrupt-controller.h).
+
+  vddq-supply:
+    description: Digital supply voltage
+
+  vbatp-supply:
+    description: Battery/power supply
+
+required:
+  - compatible
+  - reg
+  - interrupts
+  - interrupt-controller
+  - '#interrupt-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: mc33978@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>;
+        };
+    };
-- 
2.47.3


^ permalink raw reply related	[flat|nested] 20+ messages in thread

* [PATCH v1 2/8] mfd: add NXP MC33978/MC34978 core driver
  2026-02-25 17:15 [PATCH v1 0/8] mfd: Add support for NXP MC33978/MC34978 MSDI Oleksij Rempel
  2026-02-25 17:15 ` [PATCH v1 1/8] dt-bindings: mfd: add " Oleksij Rempel
@ 2026-02-25 17:15 ` Oleksij Rempel
  2026-02-25 17:15 ` [PATCH v1 3/8] dt-bindings: pinctrl: add NXP MC33978/MC34978 pinctrl Oleksij Rempel
                   ` (5 subsequent siblings)
  7 siblings, 0 replies; 20+ messages in thread
From: Oleksij Rempel @ 2026-02-25 17:15 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

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 populated automatically via
the device tree.

Signed-off-by: Oleksij Rempel <o.rempel@pengutronix.de>
---
 drivers/mfd/Kconfig         |  13 +
 drivers/mfd/Makefile        |   2 +
 drivers/mfd/mc33978.c       | 749 ++++++++++++++++++++++++++++++++++++
 include/linux/mfd/mc33978.h |  97 +++++
 4 files changed, 861 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..689922772e97 100644
--- a/drivers/mfd/Kconfig
+++ b/drivers/mfd/Kconfig
@@ -2566,6 +2566,19 @@ 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 SPI
+	select MFD_CORE
+	select REGMAP_SPI
+	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..2f6fe90d5c98
--- /dev/null
+++ b/drivers/mfd/mc33978.c
@@ -0,0 +1,749 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * MC33978/MC34978 Multiple Switch Detection Interface - MFD Core Driver
+ *
+ * Copyright (C) 2024 David Jander <david@protonic.nl>, Protonic Holland
+ * Copyright (C) 2026 Oleksij Rempel <kernel@pengutronix.de>, Pengutronix
+ *
+ * Hardware Overview:
+ * - 22 switch detection inputs: 14 SG (Switch-to-Ground), 8 SP (programmable)
+ * - 24-to-1 analog multiplexer (AMUX) for reading inputs as analog voltage
+ * - Temperature and battery voltage monitoring via AMUX
+ * - Interrupt output (INT_B) on switch state changes
+ */
+
+#include <linux/array_size.h>
+#include <linux/atomic.h>
+#include <linux/bitfield.h>
+#include <linux/bits.h>
+#include <linux/device.h>
+#include <linux/interrupt.h>
+#include <linux/irq.h>
+#include <linux/irqdomain.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_mfd_priv {
+	struct spi_device *spi;
+	struct regmap *map;
+
+	struct regulator *vddq;
+	struct regulator *vbatp;
+
+	struct mutex event_lock;
+	struct work_struct event_work;
+	atomic_t is_handling;
+	atomic_t harvested_flags;
+	u32 cached_pin_state;
+	u32 cached_pin_mask;
+	u32 irq_rise;
+	u32 irq_fall;
+
+	struct mutex irq_lock;
+	struct irq_domain *domain;
+
+	/* Pre-built SPI messages */
+	struct spi_message msg_read;
+	struct spi_message msg_write;
+	struct spi_transfer xfer_read[MC33978_FRAME_COUNT];
+	struct spi_transfer xfer_write;
+
+	/* DMA buffers at the end */
+	u8 tx_frame[MC33978_FRAME_COUNT][MC33978_FRAME_LEN] ____cacheline_aligned;
+	u8 rx_frame[MC33978_FRAME_COUNT][MC33978_FRAME_LEN];
+};
+
+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);
+
+	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);
+
+	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;
+	int ret;
+
+	/*
+	 * 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, mc->cached_pin_mask);
+	sp_mask = FIELD_GET(MC33978_CACHE_SP_PIN_MASK, mc->cached_pin_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 (hwirq == MC33978_HWIRQ_FAULT)
+		return 0;
+
+	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 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,
+};
+
+static int mc33978_irq_map(struct irq_domain *d, unsigned int virq,
+			   irq_hw_number_t hw)
+{
+	struct mc33978_mfd_priv *mc = d->host_data;
+
+	irq_set_chip_data(virq, mc);
+	irq_set_chip_and_handler(virq, &mc33978_irq_chip, handle_simple_irq);
+
+	irq_set_nested_thread(virq, 1);
+	irq_clear_status_flags(virq, IRQ_NOREQUEST | IRQ_NOPROBE);
+
+	return 0;
+}
+
+static const struct irq_domain_ops mc33978_irq_domain_ops = {
+	.map	= mc33978_irq_map,
+	.xlate	= irq_domain_xlate_twocell,
+};
+
+static void mc33978_irq_domain_remove(void *data)
+{
+	struct irq_domain *domain = data;
+
+	irq_domain_remove(domain);
+}
+
+static bool mc33978_handle_pin_changes(struct mc33978_mfd_priv *mc,
+				       unsigned int pin_state)
+{
+	u32 fired_pins = 0;
+	u32 changed_pins;
+	int i;
+
+	changed_pins = pin_state ^ mc->cached_pin_state;
+	if (!changed_pins)
+		return false;
+
+	mc->cached_pin_state = pin_state;
+	changed_pins &= mc->cached_pin_mask;
+
+	if (!changed_pins)
+		return false;
+
+	fired_pins |= (changed_pins & pin_state) & mc->irq_rise;
+	fired_pins |= (changed_pins & ~pin_state) & mc->irq_fall;
+
+	for_each_set_bit(i, (unsigned long *)&fired_pins, MC33978_NUM_PINS) {
+		int virq = irq_find_mapping(mc->domain, i);
+
+		handle_nested_irq(virq);
+	}
+
+	return true;
+}
+
+static bool mc33978_handle_fault_condition(struct mc33978_mfd_priv *mc)
+{
+	int virq;
+
+	virq = irq_find_mapping(mc->domain, MC33978_HWIRQ_FAULT);
+	if (virq > 0)
+		handle_nested_irq(virq);
+
+	return true;
+}
+
+static bool mc33978_process_single_event(struct mc33978_mfd_priv *mc)
+{
+	unsigned int pin_state;
+	bool handled = false;
+	u8 hw_flags;
+	int ret;
+
+	ret = regmap_read(mc->map, MC33978_REG_READ_IN, &pin_state);
+	if (ret)
+		return false;
+
+	/*
+	 * harvested_flags will be set by regmap_read() above if the FAULT_STAT
+	 * or INT_flg bits were detected in the response
+	 */
+	hw_flags = atomic_xchg(&mc->harvested_flags, 0);
+
+	if (mc33978_handle_pin_changes(mc, pin_state))
+		handled = true;
+
+	if (hw_flags & MC33978_HI_BYTE_STAT_FAULT &&
+	    mc33978_handle_fault_condition(mc))
+		handled = true;
+
+	if (hw_flags & MC33978_HI_BYTE_STAT_INT)
+		handled = true;
+
+	return handled;
+}
+
+static bool mc33978_handle_events(struct mc33978_mfd_priv *mc)
+{
+	bool handled = false;
+
+	mutex_lock(&mc->event_lock);
+
+	do {
+		atomic_set(&mc->is_handling, 1);
+
+		if (mc33978_process_single_event(mc))
+			handled = true;
+
+		atomic_set(&mc->is_handling, 0);
+
+	} while (atomic_read(&mc->harvested_flags) != 0);
+
+	mutex_unlock(&mc->event_lock);
+
+	return handled;
+}
+
+static irqreturn_t mc33978_irq_thread(int irq, void *data)
+{
+	return mc33978_handle_events(data) ? IRQ_HANDLED : IRQ_NONE;
+}
+
+static int mc33978_irq_init(struct mc33978_mfd_priv *mc)
+{
+	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_add_linear(dev->of_node, MC33978_NUM_PINS + 1,
+					   &mc33978_irq_domain_ops, mc);
+	if (!mc->domain)
+		return dev_err_probe(dev, -ENOMEM, "Failed to create IRQ domain\n");
+
+	ret = devm_add_action_or_reset(dev, mc33978_irq_domain_remove,
+				       mc->domain);
+	if (ret)
+		return ret;
+
+	if (mc->spi->irq <= 0)
+		return dev_err_probe(dev, -EINVAL, "No valid IRQ provided for INT_B pin\n");
+
+	ret = devm_request_threaded_irq(dev, mc->spi->irq, NULL,
+					mc33978_irq_thread,
+					IRQF_ONESHOT | IRQF_SHARED,
+					dev_name(dev), mc);
+	if (ret)
+		return dev_err_probe(dev, ret, "Failed to request IRQ\n");
+
+
+	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, u8 status)
+{
+	if (!status)
+		return;
+
+	atomic_or(status, &mc->harvested_flags);
+
+	if (!atomic_read(&mc->is_handling))
+		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.
+ *
+ * Return: Status bits if present, 0 otherwise.
+ */
+static u8 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)
+		return status;
+
+	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 status;
+}
+
+static int mc33978_spi_write(void *ctx, const void *data, size_t count)
+{
+	struct mc33978_mfd_priv *mc = ctx;
+	u8 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);
+	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;
+	u8 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);
+
+	mc33978_harvest_status(mc, status_req | 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.
+ *
+ * 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 int mc33978_probe(struct spi_device *spi)
+{
+	struct device *dev = &spi->dev;
+	struct mc33978_mfd_priv *mc;
+	int ret;
+
+	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);
+	INIT_WORK(&mc->event_work, mc33978_event_work);
+
+	atomic_set(&mc->harvested_flags, 0);
+	atomic_set(&mc->is_handling, 0);
+
+	mc33978_prepare_messages(mc);
+
+	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");
+
+	ret = mc33978_check_device(mc);
+	if (ret)
+		return dev_err_probe(dev, ret, "Can't use SPI bus\n");
+
+	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;
+
+	ret = mc33978_irq_init(mc);
+	if (ret)
+		return ret;
+
+	ret = devm_of_platform_populate(dev);
+	if (ret)
+		return dev_err_probe(dev, ret, "Failed to populate child devices\n");
+
+	return 0;
+}
+
+static const struct of_device_id mc33978_of_match[] = {
+	{ .compatible = "nxp,mc33978" },
+	{ .compatible = "nxp,mc34978" },
+	{ }
+};
+MODULE_DEVICE_TABLE(of, mc33978_of_match);
+
+static const struct spi_device_id mc33978_spi_id[] = {
+	{ "mc33978" },
+	{ "mc34978" },
+	{ }
+};
+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..70af5e1e945b
--- /dev/null
+++ b/include/linux/mfd/mc33978.h
@@ -0,0 +1,97 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * MC34978/MC33978 Multiple Switch Detection Interface - Shared Definitions
+ *
+ * Hardware Configuration:
+ * =======================
+ * - 22 switch inputs: 14 SG (Switch-to-Ground), 8 SP (programmable SG or SB)
+ * - Input voltage tolerance: -1.0V to 36V (no level translation needed)
+ * - Wetting current selection: 2, 6, 8, 10, 12, 14, 16, 20 mA (3-bit field)
+ * - 24-to-1 AMUX (future IIO driver will use AMUX_CTRL register)
+ *
+ * Register Addressing:
+ * ====================
+ * Addresses are command bytes with R/W bit = 0 (read mode).
+ * For write operations, set bit 0 (command_byte | 0x01).
+ * This is why reg_stride = 2 in regmap_config.
+ *
+ * Copyright 2024 Protonic Holland
+ */
+#ifndef _LINUX_MFD_MC34978_H
+#define _LINUX_MFD_MC34978_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)
+
+#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
+
+/*
+ * 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_MC34978_H */
-- 
2.47.3


^ permalink raw reply related	[flat|nested] 20+ messages in thread

* [PATCH v1 3/8] dt-bindings: pinctrl: add NXP MC33978/MC34978 pinctrl
  2026-02-25 17:15 [PATCH v1 0/8] mfd: Add support for NXP MC33978/MC34978 MSDI Oleksij Rempel
  2026-02-25 17:15 ` [PATCH v1 1/8] dt-bindings: mfd: add " Oleksij Rempel
  2026-02-25 17:15 ` [PATCH v1 2/8] mfd: add NXP MC33978/MC34978 core driver Oleksij Rempel
@ 2026-02-25 17:15 ` Oleksij Rempel
  2026-02-26 23:00   ` Linus Walleij
  2026-02-25 17:15 ` [PATCH v1 4/8] pinctrl: add NXP MC33978/MC34978 pinctrl driver Oleksij Rempel
                   ` (4 subsequent siblings)
  7 siblings, 1 reply; 20+ messages in thread
From: Oleksij Rempel @ 2026-02-25 17:15 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

Add device tree binding documentation for the pin control and GPIO block
of the NXP MC33978/MC34978 Multiple Switch Detection Interface (MSDI).

This block manages 22 switch detection inputs (14 Switch-to-Ground,
8 Programmable) and acts as a GPIO controller.

Additionally, it supports pin configuration for hardware-specific features
required for long-term contact maintenance in harsh environments, such as
adjusting the continuous/pulsed wetting current to penetrate oxide layers
on mechanical switches.

Signed-off-by: Oleksij Rempel <o.rempel@pengutronix.de>
---
 .../devicetree/bindings/mfd/nxp,mc33978.yaml  | 12 ++++
 .../bindings/pinctrl/nxp,mc33978-pinctrl.yaml | 66 +++++++++++++++++++
 2 files changed, 78 insertions(+)
 create mode 100644 Documentation/devicetree/bindings/pinctrl/nxp,mc33978-pinctrl.yaml

diff --git a/Documentation/devicetree/bindings/mfd/nxp,mc33978.yaml b/Documentation/devicetree/bindings/mfd/nxp,mc33978.yaml
index 85720f389509..5e8ab2cff685 100644
--- a/Documentation/devicetree/bindings/mfd/nxp,mc33978.yaml
+++ b/Documentation/devicetree/bindings/mfd/nxp,mc33978.yaml
@@ -52,6 +52,12 @@ properties:
   vbatp-supply:
     description: Battery/power supply
 
+  pinctrl:
+    type: object
+    $ref: /schemas/pinctrl/nxp,mc33978-pinctrl.yaml#
+    description: |
+      Pinctrl and GPIO controller child node for the 22 switch inputs.
+
 required:
   - compatible
   - reg
@@ -82,5 +88,11 @@ examples:
 
             vddq-supply = <&reg_3v3>;
             vbatp-supply = <&reg_12v>;
+
+            pinctrl {
+                compatible = "nxp,mc33978-pinctrl";
+                gpio-controller;
+                #gpio-cells = <2>;
+            };
         };
     };
diff --git a/Documentation/devicetree/bindings/pinctrl/nxp,mc33978-pinctrl.yaml b/Documentation/devicetree/bindings/pinctrl/nxp,mc33978-pinctrl.yaml
new file mode 100644
index 000000000000..4eeb70da6752
--- /dev/null
+++ b/Documentation/devicetree/bindings/pinctrl/nxp,mc33978-pinctrl.yaml
@@ -0,0 +1,66 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/pinctrl/nxp,mc33978-pinctrl.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: NXP MC33978/MC34978 Pinctrl/GPIO Driver
+
+maintainers:
+  - David Jander <david@protonic.nl>
+  - Oleksij Rempel <o.rempel@pengutronix.de>
+
+description: |
+  Pin control and GPIO driver for the MC33978/MC34978 MSDI device.
+
+  Pin numbering:
+  - Pins 0-13: SG0-SG13 (Switch-to-Ground inputs)
+  - Pins 14-21: SP0-SP7 (Programmable inputs, can be SG or SB)
+
+properties:
+  compatible:
+    enum:
+      - nxp,mc33978-pinctrl
+      - nxp,mc34978-pinctrl
+
+  gpio-controller: true
+
+  '#gpio-cells':
+    const: 2
+
+  ngpios:
+    const: 22
+
+patternProperties:
+  "-grp$":
+    type: object
+    $ref: pincfg-node.yaml#
+    additionalProperties: false
+    description: |
+      Pin configuration subnodes.
+    properties:
+      pins: true
+      bias-pull-up: true
+      bias-pull-down: true
+      bias-high-impedance: true
+
+required:
+  - compatible
+  - gpio-controller
+  - '#gpio-cells'
+
+unevaluatedProperties: false
+
+examples:
+  - |
+    pinctrl {
+        compatible = "nxp,mc33978-pinctrl";
+        gpio-controller;
+        #gpio-cells = <2>;
+        ngpios = <22>;
+
+        door-grp {
+            pins = "sg0";
+            bias-high-impedance;
+        };
+    };
-- 
2.47.3


^ permalink raw reply related	[flat|nested] 20+ messages in thread

* [PATCH v1 4/8] pinctrl: add NXP MC33978/MC34978 pinctrl driver
  2026-02-25 17:15 [PATCH v1 0/8] mfd: Add support for NXP MC33978/MC34978 MSDI Oleksij Rempel
                   ` (2 preceding siblings ...)
  2026-02-25 17:15 ` [PATCH v1 3/8] dt-bindings: pinctrl: add NXP MC33978/MC34978 pinctrl Oleksij Rempel
@ 2026-02-25 17:15 ` Oleksij Rempel
  2026-02-26 23:40   ` Linus Walleij
  2026-02-25 17:15 ` [PATCH v1 5/8] dt-bindings: hwmon: add NXP MC33978/MC34978 hwmon Oleksij Rempel
                   ` (3 subsequent siblings)
  7 siblings, 1 reply; 20+ messages in thread
From: Oleksij Rempel @ 2026-02-25 17:15 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

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.

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>
---
 drivers/pinctrl/Kconfig           |  14 +
 drivers/pinctrl/Makefile          |   1 +
 drivers/pinctrl/pinctrl-mc33978.c | 709 ++++++++++++++++++++++++++++++
 3 files changed, 724 insertions(+)
 create mode 100644 drivers/pinctrl/pinctrl-mc33978.c

diff --git a/drivers/pinctrl/Kconfig b/drivers/pinctrl/Kconfig
index afecd9407f53..c315656c0fe5 100644
--- a/drivers/pinctrl/Kconfig
+++ b/drivers/pinctrl/Kconfig
@@ -388,6 +388,20 @@ 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 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..e62bc0b7c6e3
--- /dev/null
+++ b/drivers/pinctrl/pinctrl-mc33978.c
@@ -0,0 +1,709 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * MC33978/MC33978 Multiple Switch Detection Interface - Pinctrl/GPIO Driver
+ *
+ * Driver Purpose:
+ * ===============
+ * Provides GPIO and pinctrl interfaces for the 22 switch detection inputs.
+ * Handles digital input reading, interrupt processing, and wetting current
+ * configuration. (Analog AMUX functionality will be handled by separate IIO driver)
+ *
+ * GPIO Number to Hardware Input Mapping:
+ * =======================================
+ * GPIO 0-13:  SG0-SG13 (Switch-to-Ground inputs)
+ * GPIO 14-21: SP0-SP7 (Programmable: Switch-to-Ground or Switch-to-Battery)
+ *
+ * This mapping is dictated by the READ_IN register bit layout where
+ * bits [21:14] = SP[7:0] and bits [13:0] = SG[13:0].
+ *
+ * Register Organization:
+ * ======================
+ * Most configuration registers are paired:
+ * - _SP register at offset N controls SP0-SP7
+ * - _SG register at offset N+2 controls SG0-SG13
+ * Helper macros MC33978_SPSG() and MC33978_PINSHIFT() handle this mapping.
+ *
+ * Wetting Current Configuration:
+ * ===============================
+ * - 8 selectable values: 2, 6, 8, 10, 12, 14, 16, 20 mA (3-bit encoding)
+ * - Stored in WET_SP (0x08), WET_SG0 (0x0a), WET_SG1 (0x0c)
+ * - Each input uses 3 bits, 8 inputs per register
+ * - Exposed via pinconf PIN_CONFIG_DRIVE_STRENGTH parameter (in mA)
+ *
+ * Interrupt Handling:
+ * ===================
+ * - Device asserts INT_B when any enabled input changes state
+ * - Driver must read READ_IN to get current state and clear INT_B
+ * - Previous state stored in mpc->state to compute edges
+ * - Per-input interrupt enable via IE_SP/IE_SG registers
+ * - IRQ handler and child IRQ operations run in threaded context (can sleep)
+ *
+ * Regcache Management:
+ * ====================
+ * - READ_IN is volatile (always re-read from hardware)
+ * - Configuration registers are cached by regmap
+ * - irq_bus_lock/unlock pattern batches multiple register writes into single
+ *   SPI transaction for efficiency when configuring multiple IRQs
+ *
+ * Copyright 2024 Protonic Holland
+ * Written by David Jander <david@protonic.nl>
+ * Based loosely on pinctrl-mcp23s08
+ *
+ * Datasheet:
+ * https://www.nxp.com/docs/en/data-sheet/MC33978.pdf
+ */
+
+#include <linux/device.h>
+#include <linux/gpio/driver.h>
+#include <linux/interrupt.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/seq_file.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_IS_SP(pin)	((pin) >= MC33978_NUM_SG)
+#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
+
+/* Choose register offset for _SG/_SP registers. reg is always the _SP addr. */
+#define MC33978_SPSG(reg, pin)	(MC33978_IS_SP(pin) ? (reg) : (reg) + 2)
+
+/* Get the bit index into the corresponding register */
+#define MC33978_PINSHIFT(pin)	(MC33978_IS_SP(pin) ? (pin) - MC33978_NUM_SG : (pin))
+#define MC33978_PINMASK(pin)	BIT(MC33978_PINSHIFT(pin))
+
+/*
+ * The same thing for the wetting current registers, but those are 3 in total
+ * and each pin uses a 3-bit field, 8 pins per register, except for the last
+ * one.
+ */
+#define MC33978_WREG(reg, pin)	((reg) + (MC33978_IS_SP(pin) ? \
+			0 : 2 + 2 * ((pin) / 8)))
+#define MC33978_WSHIFT(pin)	(MC33978_IS_SP(pin) ? \
+		(3 * ((pin) - MC33978_NUM_SG)) : (3 * ((pin) % 8)))
+#define MC33978_WMASK(pin)	(7 << MC33978_WSHIFT(pin))
+
+#define MC33978_TRISTATE	0
+#define MC33978_PU		1
+#define MC33978_PD		2
+
+
+
+struct mc33978_pinctrl {
+	struct device *dev;
+	struct regmap *regmap;
+	int irq;
+
+	struct irq_domain *domain;
+
+	struct gpio_chip chip;
+	struct pinctrl_dev *pctldev;
+	struct pinctrl_desc pinctrl_desc;
+
+	/* Interrupt state management */
+	struct mutex lock;		/* Protects state, irq_rise/fall */
+	unsigned int state;		/* Last read input state */
+	unsigned int irq_rise;		/* Rising edge config mask */
+	unsigned int irq_fall;		/* Falling edge config mask */
+};
+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 int mc33978_read(struct mc33978_pinctrl *mpc, u8 reg, u32 *val)
+{
+	int 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 int mc33978_pinctrl_get_groups_count(struct pinctrl_dev *pctldev)
+{
+	return 0;
+}
+
+static const char *mc33978_pinctrl_get_group_name(struct pinctrl_dev *pctldev,
+							unsigned int group)
+{
+	return NULL;
+}
+
+static int mc33978_pinctrl_get_group_pins(struct pinctrl_dev *pctldev,
+						unsigned int group,
+						const unsigned int **pins,
+						unsigned int *num_pins)
+{
+	return -EOPNOTSUPP;
+}
+
+static const struct pinctrl_ops mc33978_pinctrl_ops = {
+	.get_groups_count = mc33978_pinctrl_get_groups_count,
+	.get_group_name = mc33978_pinctrl_get_group_name,
+	.get_group_pins = mc33978_pinctrl_get_group_pins,
+#ifdef CONFIG_OF
+	.dt_node_to_map = pinconf_generic_dt_node_to_map_pin,
+	.dt_free_map = pinconf_generic_dt_free_map,
+#endif
+};
+
+static int mc33978_get_pull(struct mc33978_pinctrl *mpc, unsigned int pin, int *val)
+{
+	int ret;
+	unsigned int data;
+
+	ret = mc33978_read(mpc, MC33978_SPSG(MC33978_REG_TRI_SP, pin), &data);
+	if (ret < 0)
+		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 < 0)
+		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, int val)
+{
+	int ret;
+	unsigned int mask = MC33978_PINMASK(pin);
+
+	/* 1. Hardware-Schutz: SG-Pins haben physikalisch keine Pull-Downs */
+	if ((val == MC33978_PD) && !MC33978_IS_SP(pin))
+		return -EINVAL;
+
+	/* 2. Richtung konfigurieren (Ausschließlich für SP-Pins) */
+	if (MC33978_IS_SP(pin) && val != MC33978_TRISTATE) {
+		/* CONFIG (0x02): 0 = Switch-to-Ground (PU), 1 = Switch-to-Battery (PD) */
+		ret = mc33978_update_bits(mpc, MC33978_REG_CONFIG, mask,
+					  (val == MC33978_PD) ? mask : 0);
+		if (ret)
+			return ret;
+	}
+
+	/* 3. Pull-Widerstand aktivieren oder in Tri-State versetzen
+	 * TRI-Register: 0 = Pull aktiv, 1 = Tri-State (Hochohmig)
+	 */
+	ret = mc33978_update_bits(mpc, MC33978_SPSG(MC33978_REG_TRI_SP, pin),
+				  mask,
+				  (val == MC33978_TRISTATE) ? mask : 0);
+	return ret;
+}
+
+static int mc33978_get_ds(struct mc33978_pinctrl *mpc, unsigned int pin,
+		unsigned int *val)
+{
+	int ret;
+	unsigned int data;
+
+	ret = mc33978_read(mpc, MC33978_WREG(MC33978_REG_WET_SP, pin), &data);
+	if (ret)
+		return ret;
+
+	data &= MC33978_WMASK(pin);
+	data >>= MC33978_WSHIFT(pin);
+
+	/* DS levels: 2, 6, 8, 10, 12, 14, 16, 20mA */
+	if (!data)
+		*val = 2;
+	else if (data == 7)
+		*val = 20;
+	else
+		*val = (data + 2) * 2;
+
+	return 0;
+}
+
+static int mc33978_set_ds(struct mc33978_pinctrl *mpc, unsigned int pin,
+		unsigned int val)
+{
+	int ret;
+
+	/* DS levels: 2, 6, 8, 10, 12, 14, 16, 20mA */
+	if ((val < 2) || (val > 20) || (val == 4) || (val == 18) || (val & 1))
+		return -EOPNOTSUPP;
+
+	val >>= 1;
+	val--;
+	if (val)
+		val--;
+	if (val > 7)
+		val = 7;
+	ret = mc33978_update_bits(mpc, MC33978_WREG(MC33978_REG_WET_SP, pin),
+			MC33978_WMASK(pin),
+			val << MC33978_WSHIFT(pin));
+
+	return ret;
+}
+
+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);
+	unsigned int data, status;
+	int ret;
+
+	switch (param) {
+	case PIN_CONFIG_BIAS_PULL_UP:
+		ret = mc33978_get_pull(mpc, pin, &data);
+		if (ret)
+			return ret;
+		status = (data == MC33978_PU);
+		break;
+	case PIN_CONFIG_BIAS_PULL_DOWN:
+		ret = mc33978_get_pull(mpc, pin, &data);
+		if (ret)
+			return ret;
+		status = (data == MC33978_PD);
+		break;
+	case PIN_CONFIG_BIAS_DISABLE:
+	case PIN_CONFIG_BIAS_HIGH_IMPEDANCE:
+		ret = mc33978_get_pull(mpc, pin, &data);
+		if (ret)
+			return ret;
+		status = (data == MC33978_TRISTATE);
+		break;
+	case PIN_CONFIG_DRIVE_STRENGTH:
+		ret = mc33978_get_ds(mpc, pin, &data);
+		if (ret)
+			return ret;
+		*config = pinconf_to_config_packed(param, data);
+		status = 1;
+		break;
+	default:
+		return -EOPNOTSUPP;
+	}
+
+	return status ? 0 : -EINVAL;
+}
+
+/*
+ * 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;
+	u32 arg;
+	int ret = 0;
+	int i;
+
+	for (i = 0; i < num_configs; i++) {
+		param = pinconf_to_config_param(configs[i]);
+		arg = pinconf_to_config_argument(configs[i]);
+
+		switch (param) {
+		case PIN_CONFIG_DRIVE_OPEN_SOURCE:
+		case PIN_CONFIG_BIAS_PULL_UP:
+			ret = mc33978_set_pull(mpc, pin, MC33978_PU);
+			break;
+		case PIN_CONFIG_DRIVE_OPEN_DRAIN:
+		case PIN_CONFIG_BIAS_PULL_DOWN:
+			if (!MC33978_IS_SP(pin)) {
+				dev_err(mpc->dev, "Pin %u is SG and does not support pull-down\n",
+					pin);
+				return -EINVAL;
+			}
+			ret = mc33978_set_pull(mpc, pin, MC33978_PD);
+			break;
+		case PIN_CONFIG_DRIVE_STRENGTH_UA:
+			arg /= 1000;
+			fallthrough;
+		case PIN_CONFIG_DRIVE_STRENGTH:
+			ret = mc33978_set_ds(mpc, pin, arg);
+			break;
+		default:
+			return -EOPNOTSUPP;
+		}
+
+		if (ret) {
+			dev_err(mpc->dev, "Failed to set config param %04x for pin %u: %d\n",
+					param, pin, ret);
+			return ret;
+		}
+	}
+
+	return ret;
+}
+
+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);
+
+	/*
+	 * This chip only has inputs. We emulate outputs by setting a
+	 * wetting current and/or using the tri-state register to turn it on
+	 * and off. If a pin was an output and is now tri-stated, we should
+	 * disable the tri-state now to make the input work correctly.
+	 */
+	mutex_lock(&mpc->lock);
+	mc33978_update_bits(mpc, MC33978_SPSG(MC33978_REG_TRI_SP, offset),
+			MC33978_PINMASK(offset), 0);
+	mutex_unlock(&mpc->lock);
+
+	return 0;
+}
+
+static int mc33978_get(struct gpio_chip *chip, unsigned int offset)
+{
+	struct mc33978_pinctrl *mpc = gpiochip_get_data(chip);
+	int status, ret;
+	bool is_switch_closed;
+	bool is_switch_to_ground = true; /* Default for all SG pins */
+
+	mutex_lock(&mpc->lock);
+
+	/* Read hardware switch status (open or closed) */
+	ret = mc33978_read(mpc, MC33978_REG_READ_IN, &status);
+	if (ret < 0) {
+		mutex_unlock(&mpc->lock);
+		return 0;
+	}
+	is_switch_closed = !!(status & BIT(offset));
+
+	/* Determine current topology for SP pins */
+	if (MC33978_IS_SP(offset)) {
+		int config_reg;
+
+		ret = mc33978_read(mpc, MC33978_REG_CONFIG, &config_reg);
+		if (ret == 0) {
+			/* CONFIG: 0 = Switch-to-Ground (PU), 1 = Switch-to-Battery (PD) */
+			if (config_reg & MC33978_PINMASK(offset))
+				is_switch_to_ground = false;
+		}
+	}
+
+	mutex_unlock(&mpc->lock);
+
+	/* Translate hardware switch semantics to logical GPIO levels */
+	if (is_switch_to_ground) {
+		/* SG: Switch open -> High (1), Switch to GND -> Low (0) */
+		status = !is_switch_closed;
+	} else {
+		/* SB: Switch open -> Low (0), Switch to Vbat -> High (1) */
+		status = is_switch_closed;
+	}
+
+	return status;
+}
+
+static int mc33978_get_multiple(struct gpio_chip *chip,
+				unsigned long *mask, unsigned long *bits)
+{
+	struct mc33978_pinctrl *mpc = gpiochip_get_data(chip);
+	unsigned int status;
+	unsigned int config_reg = 0;
+	unsigned int inv_mask;
+	int ret;
+
+	mutex_lock(&mpc->lock);
+
+	ret = mc33978_read(mpc, MC33978_REG_READ_IN, &status);
+	if (ret)
+		goto out_unlock;
+
+	/* 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)
+			goto out_unlock;
+	}
+
+	/*
+	 * SG pins (0-13) are always Switch-to-Ground.
+	 * SP pins (14-21) are Switch-to-Ground if their CONFIG bit is 0.
+	 * Switch-to-Ground logic: HW bit 0 (open) -> Logical 1 (High)
+	 * HW bit 1 (closed) -> Logical 0 (Low)
+	 * We create a mask for all Switch-to-Ground pins and XOR the status.
+	 */
+	inv_mask = MC33978_SG_MASK | (~(config_reg << MC33978_NUM_SG) & MC33978_SP_MASK);
+
+	*bits = (status ^ inv_mask) & *mask;
+
+out_unlock:
+	mutex_unlock(&mpc->lock);
+
+	return ret;
+}
+
+static int mc33978_set(struct gpio_chip *chip, unsigned int offset, int value)
+{
+	struct mc33978_pinctrl *mpc = gpiochip_get_data(chip);
+	int pull;
+	int ret;
+
+	/*
+	 * We only have inputs with wetting current sources, that we mis-use
+	 * as open-drain/-source outputs.
+	 */
+	if (MC33978_IS_SP(offset)) {
+		pull = value ? MC33978_PU : MC33978_PD;
+		value = 1;
+	} else {
+		pull = MC33978_PU;
+	}
+
+	mutex_lock(&mpc->lock);
+
+	/*
+	 * Break-before-make sequencing to prevent hardware glitches (spikes).
+	 * Since SPI transfers take time, writing the pull and tri-state
+	 * registers in the wrong order causes a brief moment where current
+	 * flows to the pin before it is masked, causing a visible LED flash.
+	 */
+	if (value) {
+		/*
+		 * Turn ON: Configure the underlying current source (pull) first,
+		 * then route it to the pin by disabling tri-state.
+		 */
+		ret = mc33978_set_pull(mpc, offset, pull);
+		if (ret)
+			goto out_unlock;
+
+		ret = mc33978_update_bits(mpc, MC33978_SPSG(MC33978_REG_TRI_SP, offset),
+					  MC33978_PINMASK(offset), 0);
+	} else {
+		/*
+		 * Turn OFF: Isolate the pin first by enabling tri-state,
+		 * then safely disable the underlying current source.
+		 */
+		ret = mc33978_update_bits(mpc, MC33978_SPSG(MC33978_REG_TRI_SP, offset),
+					  MC33978_PINMASK(offset), MC33978_PINMASK(offset));
+	}
+
+out_unlock:
+	mutex_unlock(&mpc->lock);
+
+	return ret;
+}
+
+static int mc33978_set_multiple(struct gpio_chip *chip,
+				unsigned long *mask, unsigned long *bits)
+{
+	struct mc33978_pinctrl *mpc = gpiochip_get_data(chip);
+	unsigned int sgmask = (*mask & MC33978_SG_MASK) >> MC33978_SG_SHIFT;
+	unsigned int sgbits = (*bits & MC33978_SG_MASK) >> MC33978_SG_SHIFT;
+	unsigned int spmask = (*mask & MC33978_SP_MASK) >> MC33978_SP_SHIFT;
+	unsigned int spbits = (*bits & MC33978_SP_MASK) >> MC33978_SP_SHIFT;
+
+	mutex_lock(&mpc->lock);
+	if (spmask)
+		mc33978_update_bits(mpc, MC33978_REG_TRI_SP, spmask, ~spbits);
+	if (sgmask)
+		mc33978_update_bits(mpc, MC33978_REG_TRI_SG, sgmask, ~sgbits);
+	mutex_unlock(&mpc->lock);
+
+	return 0;
+}
+
+static int mc33978_direction_output(struct gpio_chip *chip, unsigned int offset,
+		int value)
+{
+	return mc33978_set(chip, offset, value);
+}
+
+static int mc33978_gpio_to_irq(struct gpio_chip *chip, unsigned int offset)
+{
+	struct mc33978_pinctrl *mpc = gpiochip_get_data(chip);
+	int virq;
+
+	if (!mpc->domain)
+		return -ENXIO;
+
+	/* * Erzeugt das Mapping zur Laufzeit (oder gibt ein bestehendes zurück).
+	 * Ohne diesen Aufruf bleibt die lineare IRQ-Domain leer.
+	 */
+	virq = irq_create_mapping(mpc->domain, offset);
+	if (!virq) {
+		dev_err(mpc->dev, "Failed to map hwirq %u to virq\n", offset);
+		return -ENXIO;
+	}
+
+	return virq;
+}
+
+static void mc33978_init_gpio_chip(struct mc33978_pinctrl *mpc,
+				   struct device *dev)
+{
+	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.to_irq = mc33978_gpio_to_irq;
+
+	mpc->chip.base = -1;
+	mpc->chip.ngpio = MC33978_NGPIO;
+	mpc->chip.can_sleep = true;
+	mpc->chip.parent = dev;
+	mpc->chip.owner = THIS_MODULE;
+}
+
+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_pinctrl_probe(struct platform_device *pdev)
+{
+	struct device *dev = &pdev->dev;
+	struct mc33978_pinctrl *mpc;
+	struct device_node *np = dev->of_node;
+	int ret;
+
+	if (!np)
+		return dev_err_probe(dev, -EINVAL, "Missing device tree node\n");
+
+	mpc = devm_kzalloc(dev, sizeof(*mpc), GFP_KERNEL);
+	if (!mpc)
+		return -ENOMEM;
+
+	mpc->dev = dev;
+
+	/* Get regmap from parent MFD device */
+	mpc->regmap = dev_get_regmap(dev->parent, NULL);
+	if (!mpc->regmap)
+		return dev_err_probe(dev, -ENODEV, "Failed to get parent regmap\n");
+
+	/*
+	 * Get IRQ domain from parent's interrupt-controller.
+	 * The parent (MFD) node has interrupt-controller properties,
+	 * so we can get the domain from there.
+	 */
+	mpc->domain = irq_find_host(dev->parent->of_node);
+	if (!mpc->domain)
+		return dev_err_probe(dev, -ENODEV, "Failed to find parent IRQ domain\n");
+
+	mutex_init(&mpc->lock);
+
+	/* 3. GPIO Chip Setup */
+	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 < 0)
+		return dev_err_probe(dev, ret, "can't add GPIO chip\n");
+
+	platform_set_drvdata(pdev, mpc);
+
+	return 0;
+}
+
+static const struct of_device_id mc33978_pinctrl_of_match[] = {
+	{ .compatible = "nxp,mc33978-pinctrl" },
+	{ .compatible = "nxp,mc34978-pinctrl" },
+	{ }
+};
+MODULE_DEVICE_TABLE(of, mc33978_pinctrl_of_match);
+
+static struct platform_driver mc33978_pinctrl_driver = {
+	.driver = {
+		.name = "mc33978-pinctrl",
+		.of_match_table = mc33978_pinctrl_of_match,
+	},
+	.probe = mc33978_pinctrl_probe,
+};
+module_platform_driver(mc33978_pinctrl_driver);
+
+MODULE_AUTHOR("David Jander <david@protonic.nl>");
+MODULE_DESCRIPTION("NXP MC33978/MC33978 pinctrl driver");
+MODULE_LICENSE("GPL");
-- 
2.47.3


^ permalink raw reply related	[flat|nested] 20+ messages in thread

* [PATCH v1 5/8] dt-bindings: hwmon: add NXP MC33978/MC34978 hwmon
  2026-02-25 17:15 [PATCH v1 0/8] mfd: Add support for NXP MC33978/MC34978 MSDI Oleksij Rempel
                   ` (3 preceding siblings ...)
  2026-02-25 17:15 ` [PATCH v1 4/8] pinctrl: add NXP MC33978/MC34978 pinctrl driver Oleksij Rempel
@ 2026-02-25 17:15 ` Oleksij Rempel
  2026-02-26  8:17   ` Krzysztof Kozlowski
  2026-02-25 17:15 ` [PATCH v1 6/8] hwmon: add NXP MC33978/MC34978 driver Oleksij Rempel
                   ` (2 subsequent siblings)
  7 siblings, 1 reply; 20+ messages in thread
From: Oleksij Rempel @ 2026-02-25 17:15 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

Add device tree binding documentation for the hardware monitoring block
of the NXP MC33978/MC34978 Multiple Switch Detection Interface (MSDI).

Signed-off-by: Oleksij Rempel <o.rempel@pengutronix.de>
---
 .../bindings/hwmon/nxp,mc33978-hwmon.yaml     | 34 +++++++++++++++++++
 .../devicetree/bindings/mfd/nxp,mc33978.yaml  | 10 ++++++
 2 files changed, 44 insertions(+)
 create mode 100644 Documentation/devicetree/bindings/hwmon/nxp,mc33978-hwmon.yaml

diff --git a/Documentation/devicetree/bindings/hwmon/nxp,mc33978-hwmon.yaml b/Documentation/devicetree/bindings/hwmon/nxp,mc33978-hwmon.yaml
new file mode 100644
index 000000000000..b7e2aaa51a33
--- /dev/null
+++ b/Documentation/devicetree/bindings/hwmon/nxp,mc33978-hwmon.yaml
@@ -0,0 +1,34 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/hwmon/nxp,mc33978-hwmon.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: NXP MC33978/MC34978 Hardware Monitor
+
+maintainers:
+  - David Jander <david@protonic.nl>
+  - Oleksij Rempel <o.rempel@pengutronix.de>
+
+description: |
+  Hardware monitoring driver for the MC33978/MC34978 MSDI device.
+  Provides fault detection and monitoring for:
+  - Battery voltage (VBATP) faults: undervoltage, overvoltage
+  - Temperature faults: over-temperature, warning threshold
+
+properties:
+  compatible:
+    enum:
+      - nxp,mc33978-hwmon
+      - nxp,mc34978-hwmon
+
+required:
+  - compatible
+
+additionalProperties: false
+
+examples:
+  - |
+    hwmon {
+        compatible = "nxp,mc33978-hwmon";
+    };
diff --git a/Documentation/devicetree/bindings/mfd/nxp,mc33978.yaml b/Documentation/devicetree/bindings/mfd/nxp,mc33978.yaml
index 5e8ab2cff685..58fcfe24d415 100644
--- a/Documentation/devicetree/bindings/mfd/nxp,mc33978.yaml
+++ b/Documentation/devicetree/bindings/mfd/nxp,mc33978.yaml
@@ -58,6 +58,12 @@ properties:
     description: |
       Pinctrl and GPIO controller child node for the 22 switch inputs.
 
+  hwmon:
+    type: object
+    $ref: /schemas/hwmon/nxp,mc33978-hwmon.yaml#
+    description: |
+      Hardware monitoring child node for fault detection.
+
 required:
   - compatible
   - reg
@@ -94,5 +100,9 @@ examples:
                 gpio-controller;
                 #gpio-cells = <2>;
             };
+
+            hwmon {
+                compatible = "nxp,mc33978-hwmon";
+            };
         };
     };
-- 
2.47.3


^ permalink raw reply related	[flat|nested] 20+ messages in thread

* [PATCH v1 6/8] hwmon: add NXP MC33978/MC34978 driver
  2026-02-25 17:15 [PATCH v1 0/8] mfd: Add support for NXP MC33978/MC34978 MSDI Oleksij Rempel
                   ` (4 preceding siblings ...)
  2026-02-25 17:15 ` [PATCH v1 5/8] dt-bindings: hwmon: add NXP MC33978/MC34978 hwmon Oleksij Rempel
@ 2026-02-25 17:15 ` Oleksij Rempel
  2026-02-25 17:15 ` [PATCH v1 7/8] dt-bindings: mux: add NXP MC33978/MC34978 AMUX Oleksij Rempel
  2026-02-25 17:15 ` [PATCH v1 8/8] mux: add NXP MC33978/MC34978 AMUX driver Oleksij Rempel
  7 siblings, 0 replies; 20+ messages in thread
From: Oleksij Rempel @ 2026-02-25 17:15 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

Add hardware monitoring support for the NXP MC33978/MC34978 MSDI.

The driver exposes static operating thresholds (thermal, over-voltage,
under-voltage) and reports dynamic hardware fault alarms.

Signed-off-by: Oleksij Rempel <o.rempel@pengutronix.de>
---
 drivers/hwmon/Kconfig         |  10 +
 drivers/hwmon/Makefile        |   1 +
 drivers/hwmon/mc33978-hwmon.c | 439 ++++++++++++++++++++++++++++++++++
 3 files changed, 450 insertions(+)
 create mode 100644 drivers/hwmon/mc33978-hwmon.c

diff --git a/drivers/hwmon/Kconfig b/drivers/hwmon/Kconfig
index 41c381764c2b..c5d99510cc00 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 eade8e3b1bde..e40bc29b9850 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..3dd4c6ecb42b
--- /dev/null
+++ b/drivers/hwmon/mc33978-hwmon.c
@@ -0,0 +1,439 @@
+// SPDX-License-Identifier: GPL-2.0-only
+// Copyright (c) 2026 Pengutronix, Oleksij Rempel <kernel@pengutronix.de>
+/*
+ * MC33978/MC34978 Hardware Monitor Driver
+ *
+ * CRITICAL HARDWARE BEHAVIOR - THERMAL (tLIM):
+ * When the thermal limit (>155°C) is reached, the IC autonomously
+ * reduces the continuous wetting current (CWET) to 2.0 mA to prevent
+ * thermal destruction. This throttling persists until the silicon cools
+ * down below 140°C (15°C hysteresis).
+ *
+ * WARNING FOR PINCTRL/GPIO CONSUMERS:
+ * During an active tLIM fault, the switch state detection becomes
+ * inherently unreliable. A throttled wetting current of 2.0 mA may
+ * be insufficient to break through the oxide layer of mechanical
+ * contacts in the field, leading to false-open GPIO readings.
+ *
+ * VOLTAGE DEGRADATION WARNING (VBATP):
+ * While the hard Undervoltage Lockout (UVLO) asserts strictly at 4.5V,
+ * the silicon operates with degraded parametrics whenever the supply
+ * drops below 6.0V. System designers must be aware that analog routing
+ * (AMUX) and switch detection logic may behave non-deterministically
+ * before the actual UV alarm triggers.
+ */
+
+#include <linux/device.h>
+#include <linux/hwmon.h>
+#include <linux/interrupt.h>
+#include <linux/irqdomain.h>
+#include <linux/mod_devicetable.h>
+#include <linux/module.h>
+#include <linux/platform_device.h>
+#include <linux/regmap.h>
+
+#include <linux/mfd/mc33978.h>
+
+/* 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 */
+
+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;
+	int fault_irq;
+	u32 last_faults;
+};
+
+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)
+{
+	/*
+	 * Log only newly asserted critical faults to prevent kernel log spam
+	 * during persistent hardware fault conditions.
+	 * dev_*_ratelimited provides an additional safety net against noisy IRQs.
+	 */
+	if (!new_faults)
+		return;
+
+	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 irqreturn_t mc33978_hwmon_fault_irq(int irq, void *data)
+{
+	struct mc33978_hwmon_priv *priv = data;
+	u32 faults, new_faults, changed_faults;
+	int ret;
+
+	ret = mc33978_hwmon_read_fault(priv, &faults);
+	if (ret) {
+		dev_err_ratelimited(priv->dev, "Failed to read fault register: %pe\n",
+				    ERR_PTR(ret));
+		return IRQ_NONE;
+	}
+
+	changed_faults = faults ^ priv->last_faults;
+	if (!changed_faults)
+		return IRQ_HANDLED;
+
+	new_faults = faults & ~priv->last_faults;
+	if (new_faults)
+		mc33978_hwmon_report_faults(priv, new_faults);
+
+	priv->last_faults = 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);
+
+	/* Push a chip-level alarm on any hardware status change */
+	hwmon_notify_event(priv->hwmon_dev, hwmon_chip,
+			   hwmon_chip_alarms, 0);
+
+	return IRQ_HANDLED;
+}
+
+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:
+			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(struct device *dev,
+			      enum hwmon_sensor_types type,
+			      u32 attr, int channel, long *val)
+{
+	struct mc33978_hwmon_priv *priv = dev_get_drvdata(dev);
+	u32 faults;
+	int ret;
+
+	switch (type) {
+	case hwmon_in:
+		if (channel == MC33978_IN_VBATP) {
+			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;
+			default:
+				break;
+			}
+		} else if (channel == MC33978_IN_VDDQ) {
+			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;
+			}
+		}
+		break;
+
+	case hwmon_temp:
+		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;
+		default:
+			break;
+		}
+		break;
+
+	default:
+		break;
+	}
+
+	/* 2. Dynamic alarms (read hardware flags) */
+	ret = mc33978_hwmon_read_fault(priv, &faults);
+	if (ret)
+		return ret;
+
+	switch (type) {
+	case hwmon_chip:
+		if (attr == hwmon_chip_alarms) {
+			*val = faults;
+			return 0;
+		}
+		break;
+
+	case hwmon_in:
+		if (channel == MC33978_IN_VBATP) {
+			switch (attr) {
+			case hwmon_in_crit_alarm:
+				*val = !!(faults & MC33978_FAULT_OV);
+				return 0;
+			case hwmon_in_lcrit_alarm:
+				*val = !!(faults & MC33978_FAULT_UV);
+				return 0;
+			default:
+				*val = 0;
+				return 0;
+			}
+		}
+		/* VDDQ has no dedicated hardware fault flags */
+		*val = 0;
+		return 0;
+
+	case hwmon_temp:
+		switch (attr) {
+		case hwmon_temp_max_alarm:
+			*val = !!(faults & MC33978_FAULT_TEMP_WARN);
+			return 0;
+		case hwmon_temp_crit_alarm:
+			*val = !!(faults & MC33978_FAULT_OT);
+			return 0;
+		default:
+			break;
+		}
+		break;
+
+	default:
+		return -EOPNOTSUPP;
+	}
+
+	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 *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_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 int mc33978_hwmon_probe(struct platform_device *pdev)
+{
+	struct device *dev = &pdev->dev;
+	struct device_node *np = dev->of_node;
+	struct mc33978_hwmon_priv *priv;
+	struct device *hwmon_dev;
+	struct irq_domain *domain;
+	int ret;
+
+	if (!np)
+		return dev_err_probe(dev, -EINVAL, "Missing device tree node\n");
+
+	priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
+	if (!priv)
+		return -ENOMEM;
+
+	priv->dev = dev;
+
+	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);
+
+	domain = irq_find_host(dev->parent->of_node);
+	if (!domain)
+		return dev_err_probe(dev, -ENODEV, "failed to find parent IRQ domain\n");
+
+	priv->fault_irq = irq_create_mapping(domain, MC33978_HWIRQ_FAULT);
+	if (priv->fault_irq <= 0)
+		return dev_err_probe(dev, -ENOENT, "failed to map fault IRQ\n");
+
+	ret = mc33978_hwmon_read_fault(priv, &priv->last_faults);
+	if (ret)
+		return dev_err_probe(dev, ret, "failed to read initial faults\n");
+
+	if (priv->last_faults & MC33978_FAULT_CRITICAL)
+		mc33978_hwmon_report_faults(priv, priv->last_faults);
+
+	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_request_threaded_irq(dev, priv->fault_irq, NULL,
+					mc33978_hwmon_fault_irq, IRQF_ONESHOT,
+					dev_name(dev), priv);
+	if (ret)
+		return dev_err_probe(dev, ret, "failed to request fault IRQ\n");
+
+	return 0;
+}
+
+static const struct of_device_id mc33978_hwmon_of_match[] = {
+	{ .compatible = "nxp,mc33978-hwmon" },
+	{ .compatible = "nxp,mc34978-hwmon" },
+	{ }
+};
+MODULE_DEVICE_TABLE(of, mc33978_hwmon_of_match);
+
+static struct platform_driver mc33978_hwmon_driver = {
+	.driver = {
+		.name = "mc33978-hwmon",
+		.of_match_table = mc33978_hwmon_of_match,
+	},
+	.probe = mc33978_hwmon_probe,
+};
+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	[flat|nested] 20+ messages in thread

* [PATCH v1 7/8] dt-bindings: mux: add NXP MC33978/MC34978 AMUX
  2026-02-25 17:15 [PATCH v1 0/8] mfd: Add support for NXP MC33978/MC34978 MSDI Oleksij Rempel
                   ` (5 preceding siblings ...)
  2026-02-25 17:15 ` [PATCH v1 6/8] hwmon: add NXP MC33978/MC34978 driver Oleksij Rempel
@ 2026-02-25 17:15 ` Oleksij Rempel
  2026-02-26  8:14   ` Krzysztof Kozlowski
  2026-02-25 17:15 ` [PATCH v1 8/8] mux: add NXP MC33978/MC34978 AMUX driver Oleksij Rempel
  7 siblings, 1 reply; 20+ messages in thread
From: Oleksij Rempel @ 2026-02-25 17:15 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

Add device tree binding documentation for the Analog Multiplexer (AMUX)
block of the NXP MC33978/MC34978 Multiple Switch Detection Interface
(MSDI).

The AMUX acts as a 24-to-1 router, allowing an external SoC ADC to
sample the analog voltage of any of the 22 mechanical switch inputs, the
internal silicon temperature diode, or the main battery supply (VBATP).

Signed-off-by: Oleksij Rempel <o.rempel@pengutronix.de>
---
 .../devicetree/bindings/mfd/nxp,mc33978.yaml  | 11 ++++
 .../bindings/mux/nxp,mc33978-mux.yaml         | 55 +++++++++++++++++++
 2 files changed, 66 insertions(+)
 create mode 100644 Documentation/devicetree/bindings/mux/nxp,mc33978-mux.yaml

diff --git a/Documentation/devicetree/bindings/mfd/nxp,mc33978.yaml b/Documentation/devicetree/bindings/mfd/nxp,mc33978.yaml
index 58fcfe24d415..f86f72cfe230 100644
--- a/Documentation/devicetree/bindings/mfd/nxp,mc33978.yaml
+++ b/Documentation/devicetree/bindings/mfd/nxp,mc33978.yaml
@@ -64,6 +64,12 @@ properties:
     description: |
       Hardware monitoring child node for fault detection.
 
+  mux-controller:
+    type: object
+    $ref: /schemas/mux/nxp,mc33978-mux.yaml#
+    description: |
+      Analog multiplexer (AMUX) controller child node.
+
 required:
   - compatible
   - reg
@@ -104,5 +110,10 @@ examples:
             hwmon {
                 compatible = "nxp,mc33978-hwmon";
             };
+
+            mux-controller {
+                compatible = "nxp,mc33978-mux";
+                #mux-control-cells = <0>;
+            };
         };
     };
diff --git a/Documentation/devicetree/bindings/mux/nxp,mc33978-mux.yaml b/Documentation/devicetree/bindings/mux/nxp,mc33978-mux.yaml
new file mode 100644
index 000000000000..ccb8efb91d8e
--- /dev/null
+++ b/Documentation/devicetree/bindings/mux/nxp,mc33978-mux.yaml
@@ -0,0 +1,55 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/mux/nxp,mc33978-mux.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: NXP MC33978/MC34978 Analog Multiplexer
+
+maintainers:
+  - David Jander <david@protonic.nl>
+  - Oleksij Rempel <o.rempel@pengutronix.de>
+
+description: |
+  The MC33978 and MC34978 include a 24-to-1 analog multiplexer (AMUX) that
+  routes one of the following signals to an external AMUX pin for measurement
+  by an external ADC:
+
+  - Channels 0-13: SG0-SG13 switch input voltages
+  - Channels 14-21: SP0-SP7 switch input voltages
+  - Channel 22: Internal temperature diode
+  - Channel 23: Battery voltage (VBATP)
+
+  The AMUX requires a settling time of up to 200 us for full-scale voltage
+  steps. Consumers (e.g., io-channel-mux) must configure this delay.
+
+properties:
+  compatible:
+    enum:
+      - nxp,mc33978-mux
+      - nxp,mc34978-mux
+
+  '#mux-control-cells':
+    const: 0
+
+required:
+  - compatible
+  - '#mux-control-cells'
+
+additionalProperties: false
+
+examples:
+  - |
+    amux: mux-controller {
+        compatible = "nxp,mc33978-mux";
+        #mux-control-cells = <0>;
+    };
+
+    iio-mux {
+        compatible = "io-channel-mux";
+        io-channels = <&adc 0>;
+        io-channel-names = "parent";
+        mux-controls = <&amux>;
+        settle-time-us = <200>;
+        channels = "sg0", "sg1", "sg2"; /* ... */
+    };
-- 
2.47.3


^ permalink raw reply related	[flat|nested] 20+ messages in thread

* [PATCH v1 8/8] mux: add NXP MC33978/MC34978 AMUX driver
  2026-02-25 17:15 [PATCH v1 0/8] mfd: Add support for NXP MC33978/MC34978 MSDI Oleksij Rempel
                   ` (6 preceding siblings ...)
  2026-02-25 17:15 ` [PATCH v1 7/8] dt-bindings: mux: add NXP MC33978/MC34978 AMUX Oleksij Rempel
@ 2026-02-25 17:15 ` Oleksij Rempel
  7 siblings, 0 replies; 20+ messages in thread
From: Oleksij Rempel @ 2026-02-25 17:15 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

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>
---
 drivers/mux/Kconfig       |  14 +++++
 drivers/mux/Makefile      |   2 +
 drivers/mux/mc33978-mux.c | 119 ++++++++++++++++++++++++++++++++++++++
 3 files changed, 135 insertions(+)
 create mode 100644 drivers/mux/mc33978-mux.c

diff --git a/drivers/mux/Kconfig b/drivers/mux/Kconfig
index c68132e38138..7532da7e087e 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 mc33978-mux.
+
 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..ac46604c8f5e
--- /dev/null
+++ b/drivers/mux/mc33978-mux.c
@@ -0,0 +1,119 @@
+// 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/mod_devicetable.h>
+#include <linux/module.h>
+#include <linux/mux/driver.h>
+#include <linux/platform_device.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 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;
+	}
+
+	return 0;
+}
+
+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 device_node *np = dev->of_node;
+	struct mc33978_mux_priv *priv;
+	struct mux_chip *mux_chip;
+	struct mux_control *mux;
+	int ret;
+
+	if (!np)
+		return dev_err_probe(dev, -EINVAL, "Missing device tree node\n");
+
+	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");
+
+	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 = 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 of_device_id mc33978_mux_of_match[] = {
+	{ .compatible = "nxp,mc33978-mux" },
+	{ .compatible = "nxp,mc34978-mux" },
+	{ }
+};
+MODULE_DEVICE_TABLE(of, mc33978_mux_of_match);
+
+static struct platform_driver mc33978_mux_driver = {
+	.driver = {
+		.name = "mc33978-mux",
+		.of_match_table = mc33978_mux_of_match,
+	},
+	.probe = mc33978_mux_probe,
+};
+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	[flat|nested] 20+ messages in thread

* Re: [PATCH v1 7/8] dt-bindings: mux: add NXP MC33978/MC34978 AMUX
  2026-02-25 17:15 ` [PATCH v1 7/8] dt-bindings: mux: add NXP MC33978/MC34978 AMUX Oleksij Rempel
@ 2026-02-26  8:14   ` Krzysztof Kozlowski
  0 siblings, 0 replies; 20+ messages in thread
From: Krzysztof Kozlowski @ 2026-02-26  8:14 UTC (permalink / raw)
  To: Oleksij Rempel
  Cc: Guenter Roeck, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
	Lee Jones, Peter Rosin, Linus Walleij, kernel, linux-kernel,
	devicetree, linux-hwmon, linux-gpio, David Jander

On Wed, Feb 25, 2026 at 06:15:40PM +0100, Oleksij Rempel wrote:
> +description: |
> +  The MC33978 and MC34978 include a 24-to-1 analog multiplexer (AMUX) that
> +  routes one of the following signals to an external AMUX pin for measurement
> +  by an external ADC:
> +
> +  - Channels 0-13: SG0-SG13 switch input voltages
> +  - Channels 14-21: SP0-SP7 switch input voltages
> +  - Channel 22: Internal temperature diode
> +  - Channel 23: Battery voltage (VBATP)
> +
> +  The AMUX requires a settling time of up to 200 us for full-scale voltage
> +  steps. Consumers (e.g., io-channel-mux) must configure this delay.
> +
> +properties:
> +  compatible:
> +    enum:
> +      - nxp,mc33978-mux
> +      - nxp,mc34978-mux
> +
> +  '#mux-control-cells':
> +    const: 0

No resources here, so this should not be separate device node but folded
into the parent.

Best regards,
Krzysztof


^ permalink raw reply	[flat|nested] 20+ messages in thread

* Re: [PATCH v1 1/8] dt-bindings: mfd: add NXP MC33978/MC34978 MSDI
  2026-02-25 17:15 ` [PATCH v1 1/8] dt-bindings: mfd: add " Oleksij Rempel
@ 2026-02-26  8:16   ` Krzysztof Kozlowski
  0 siblings, 0 replies; 20+ messages in thread
From: Krzysztof Kozlowski @ 2026-02-26  8:16 UTC (permalink / raw)
  To: Oleksij Rempel
  Cc: Guenter Roeck, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
	Lee Jones, Peter Rosin, Linus Walleij, kernel, linux-kernel,
	devicetree, linux-hwmon, linux-gpio, David Jander

On Wed, Feb 25, 2026 at 06:15:34PM +0100, Oleksij Rempel wrote:
> +# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
> +%YAML 1.2
> +---
> +$id: http://devicetree.org/schemas/mfd/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: |

Do not need '|' unless you need to preserve formatting.

> +  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.
> +
> +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 interrupt-controller.h).
> +
> +  vddq-supply:
> +    description: Digital supply voltage
> +
> +  vbatp-supply:
> +    description: Battery/power supply

This is incomplete. I see several other patches changing the file -
please post complete binding in one patch, not half-baked solutions.

> +
> +required:
> +  - compatible
> +  - reg
> +  - interrupts
> +  - interrupt-controller
> +  - '#interrupt-cells'

Supplies should be required, unless datasheet tells otherwise.

> +
> +unevaluatedProperties: false
> +
> +examples:
> +  - |
> +    #include <dt-bindings/interrupt-controller/irq.h>
> +    #include <dt-bindings/gpio/gpio.h>
> +
> +    spi {
> +        #address-cells = <1>;
> +        #size-cells = <0>;
> +
> +        msdi: mc33978@0 {

Node names should be generic. See also an explanation and list of
examples (not exhaustive) in DT specification:
https://devicetree-specification.readthedocs.io/en/latest/chapter2-devicetree-basics.html#generic-names-recommendation
If you cannot find a name matching your device, please check in kernel
sources for similar cases or you can grow the spec (via pull request to
DT spec repo).

Best regards,
Krzysztof


^ permalink raw reply	[flat|nested] 20+ messages in thread

* Re: [PATCH v1 5/8] dt-bindings: hwmon: add NXP MC33978/MC34978 hwmon
  2026-02-25 17:15 ` [PATCH v1 5/8] dt-bindings: hwmon: add NXP MC33978/MC34978 hwmon Oleksij Rempel
@ 2026-02-26  8:17   ` Krzysztof Kozlowski
  0 siblings, 0 replies; 20+ messages in thread
From: Krzysztof Kozlowski @ 2026-02-26  8:17 UTC (permalink / raw)
  To: Oleksij Rempel
  Cc: Guenter Roeck, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
	Lee Jones, Peter Rosin, Linus Walleij, kernel, linux-kernel,
	devicetree, linux-hwmon, linux-gpio, David Jander

On Wed, Feb 25, 2026 at 06:15:38PM +0100, Oleksij Rempel wrote:
> Add device tree binding documentation for the hardware monitoring block
> of the NXP MC33978/MC34978 Multiple Switch Detection Interface (MSDI).
> 
> Signed-off-by: Oleksij Rempel <o.rempel@pengutronix.de>
> ---
>  .../bindings/hwmon/nxp,mc33978-hwmon.yaml     | 34 +++++++++++++++++++
>  .../devicetree/bindings/mfd/nxp,mc33978.yaml  | 10 ++++++
>  2 files changed, 44 insertions(+)
>  create mode 100644 Documentation/devicetree/bindings/hwmon/nxp,mc33978-hwmon.yaml
> 
> diff --git a/Documentation/devicetree/bindings/hwmon/nxp,mc33978-hwmon.yaml b/Documentation/devicetree/bindings/hwmon/nxp,mc33978-hwmon.yaml
> new file mode 100644
> index 000000000000..b7e2aaa51a33
> --- /dev/null
> +++ b/Documentation/devicetree/bindings/hwmon/nxp,mc33978-hwmon.yaml
> @@ -0,0 +1,34 @@
> +# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
> +%YAML 1.2
> +---
> +$id: http://devicetree.org/schemas/hwmon/nxp,mc33978-hwmon.yaml#
> +$schema: http://devicetree.org/meta-schemas/core.yaml#
> +
> +title: NXP MC33978/MC34978 Hardware Monitor
> +
> +maintainers:
> +  - David Jander <david@protonic.nl>
> +  - Oleksij Rempel <o.rempel@pengutronix.de>
> +
> +description: |
> +  Hardware monitoring driver for the MC33978/MC34978 MSDI device.
> +  Provides fault detection and monitoring for:
> +  - Battery voltage (VBATP) faults: undervoltage, overvoltage
> +  - Temperature faults: over-temperature, warning threshold
> +
> +properties:
> +  compatible:
> +    enum:
> +      - nxp,mc33978-hwmon
> +      - nxp,mc34978-hwmon
> +
> +required:
> +  - compatible

This is completely empty thus pointless. Do not create nodes which has 0
properties (compatible does not count since it is used to tell what the
device is).

Best regards,
Krzysztof


^ permalink raw reply	[flat|nested] 20+ messages in thread

* Re: [PATCH v1 3/8] dt-bindings: pinctrl: add NXP MC33978/MC34978 pinctrl
  2026-02-25 17:15 ` [PATCH v1 3/8] dt-bindings: pinctrl: add NXP MC33978/MC34978 pinctrl Oleksij Rempel
@ 2026-02-26 23:00   ` Linus Walleij
  2026-02-26 23:02     ` Linus Walleij
  2026-02-27 10:04     ` Oleksij Rempel
  0 siblings, 2 replies; 20+ messages in thread
From: Linus Walleij @ 2026-02-26 23:00 UTC (permalink / raw)
  To: Oleksij Rempel
  Cc: Guenter Roeck, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
	Lee Jones, Peter Rosin, kernel, linux-kernel, devicetree,
	linux-hwmon, linux-gpio, David Jander

Hi Oleksij,

thanks for your patch for this very interesting hardware!

On Wed, Feb 25, 2026 at 6:16 PM Oleksij Rempel <o.rempel@pengutronix.de> wrote:

> Add device tree binding documentation for the pin control and GPIO block
> of the NXP MC33978/MC34978 Multiple Switch Detection Interface (MSDI).
>
> This block manages 22 switch detection inputs (14 Switch-to-Ground,
> 8 Programmable) and acts as a GPIO controller.
>
> Additionally, it supports pin configuration for hardware-specific features
> required for long-term contact maintenance in harsh environments, such as
> adjusting the continuous/pulsed wetting current to penetrate oxide layers
> on mechanical switches.
>
> Signed-off-by: Oleksij Rempel <o.rempel@pengutronix.de>

Are the hardware-specific pin configurations for oxide layer
penetration (!) and stuff excluded from these bindings? (It looks
like.)

> +++ b/Documentation/devicetree/bindings/pinctrl/nxp,mc33978-pinctrl.yaml
> @@ -0,0 +1,66 @@
> +# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
> +%YAML 1.2
> +---
> +$id: http://devicetree.org/schemas/pinctrl/nxp,mc33978-pinctrl.yaml#
> +$schema: http://devicetree.org/meta-schemas/core.yaml#
> +
> +title: NXP MC33978/MC34978 Pinctrl/GPIO Driver
> +
> +maintainers:
> +  - David Jander <david@protonic.nl>
> +  - Oleksij Rempel <o.rempel@pengutronix.de>
> +
> +description: |
> +  Pin control and GPIO driver for the MC33978/MC34978 MSDI device.
> +
> +  Pin numbering:
> +  - Pins 0-13: SG0-SG13 (Switch-to-Ground inputs)

I don't know what a switch-to-ground input is, but I talked to an AI
about it.

It appears to be directly incorrect to use such a line without
flagging it as GPIO_ACTIVE_LOW in the consumer so this should
be mentioned here, GPIO lines 0-13 *must* be flagged
GPIO_ACTIVE_LOW. (I don't know a good way to enforce it
in schema, sadly, maybe Conor has ideas.)

> +  - Pins 14-21: SP0-SP7 (Programmable inputs, can be SG or SB)

What is SB now? Please explain :)

Other than that it looks good to me!

Yours,
Linus Walleij

^ permalink raw reply	[flat|nested] 20+ messages in thread

* Re: [PATCH v1 3/8] dt-bindings: pinctrl: add NXP MC33978/MC34978 pinctrl
  2026-02-26 23:00   ` Linus Walleij
@ 2026-02-26 23:02     ` Linus Walleij
  2026-02-27 10:41       ` Oleksij Rempel
  2026-02-27 10:04     ` Oleksij Rempel
  1 sibling, 1 reply; 20+ messages in thread
From: Linus Walleij @ 2026-02-26 23:02 UTC (permalink / raw)
  To: Oleksij Rempel
  Cc: Guenter Roeck, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
	Lee Jones, Peter Rosin, kernel, linux-kernel, devicetree,
	linux-hwmon, linux-gpio, David Jander

On Fri, Feb 27, 2026 at 12:00 AM Linus Walleij <linusw@kernel.org> wrote:
> On Wed, Feb 25, 2026 at 6:16 PM Oleksij Rempel <o.rempel@pengutronix.de> wrote:

> > +  - Pins 14-21: SP0-SP7 (Programmable inputs, can be SG or SB)
>
> What is SB now? Please explain :)

Oh I see in the driver that this is Switch-to-battery. So document that here
in the bindings too.

Also it seems that something configured as switch-to-batter must be
flagged GPIO_ACTIVE_HIGH.

Yours,
Linus Walleij

^ permalink raw reply	[flat|nested] 20+ messages in thread

* Re: [PATCH v1 4/8] pinctrl: add NXP MC33978/MC34978 pinctrl driver
  2026-02-25 17:15 ` [PATCH v1 4/8] pinctrl: add NXP MC33978/MC34978 pinctrl driver Oleksij Rempel
@ 2026-02-26 23:40   ` Linus Walleij
  2026-02-27 10:58     ` Oleksij Rempel
  0 siblings, 1 reply; 20+ messages in thread
From: Linus Walleij @ 2026-02-26 23:40 UTC (permalink / raw)
  To: Oleksij Rempel, Bartosz Golaszewski
  Cc: Guenter Roeck, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
	Lee Jones, Peter Rosin, David Jander, kernel, linux-kernel,
	devicetree, linux-hwmon, linux-gpio

Hi Oleksij,

thanks for your patch!

On Wed, Feb 25, 2026 at 6:16 PM Oleksij Rempel <o.rempel@pengutronix.de> wrote:

> - 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.
>
> 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>
(...)
> +/*
> + * The same thing for the wetting current registers, but those are 3 in total
> + * and each pin uses a 3-bit field, 8 pins per register, except for the last
> + * one.
> + */
> +#define MC33978_WREG(reg, pin) ((reg) + (MC33978_IS_SP(pin) ? \
> +                       0 : 2 + 2 * ((pin) / 8)))
> +#define MC33978_WSHIFT(pin)    (MC33978_IS_SP(pin) ? \
> +               (3 * ((pin) - MC33978_NUM_SG)) : (3 * ((pin) % 8)))
> +#define MC33978_WMASK(pin)     (7 << MC33978_WSHIFT(pin))
> +
> +#define MC33978_TRISTATE       0
> +#define MC33978_PU             1
> +#define MC33978_PD             2
> +
> +
> +

Nit: a bit of surplus space here?

> +       /* Interrupt state management */
> +       struct mutex lock;              /* Protects state, irq_rise/fall */
> +       unsigned int state;             /* Last read input state */
> +       unsigned int irq_rise;          /* Rising edge config mask */
> +       unsigned int irq_fall;          /* Falling edge config mask */

Isn't regmap able to cache this stuff for you already so you
don't need local copies in the state?

> +static int mc33978_pinctrl_get_groups_count(struct pinctrl_dev *pctldev)
> +{
> +       return 0;
> +}
> +
> +static const char *mc33978_pinctrl_get_group_name(struct pinctrl_dev *pctldev,
> +                                                       unsigned int group)
> +{
> +       return NULL;
> +}
> +
> +static int mc33978_pinctrl_get_group_pins(struct pinctrl_dev *pctldev,
> +                                               unsigned int group,
> +                                               const unsigned int **pins,
> +                                               unsigned int *num_pins)
> +{
> +       return -EOPNOTSUPP;
> +}
> +
> +static const struct pinctrl_ops mc33978_pinctrl_ops = {
> +       .get_groups_count = mc33978_pinctrl_get_groups_count,
> +       .get_group_name = mc33978_pinctrl_get_group_name,
> +       .get_group_pins = mc33978_pinctrl_get_group_pins,

Hmmmmmmm it looks like we should just patch the pin control
core to make these callback optional don't you think? It would
certainly cut down on boilerplate in your case.

Do you think you can do a patch like that?

> +#ifdef CONFIG_OF
> +       .dt_node_to_map = pinconf_generic_dt_node_to_map_pin,
> +       .dt_free_map = pinconf_generic_dt_free_map,
> +#endif

Does this driver have any value without OF?

Can't you just depend on OF in Kconfig and delete the ifdefs?

> +       /* 1. Hardware-Schutz: SG-Pins haben physikalisch keine Pull-Downs */
> +       /* 2. Richtung konfigurieren (Ausschließlich für SP-Pins) */
> +       /* 3. Pull-Widerstand aktivieren oder in Tri-State versetzen
> +        * TRI-Register: 0 = Pull aktiv, 1 = Tri-State (Hochohmig)
> +        */

Ich bewundere die deutsche Sprache, muss Sie aber dennoch bitten,
dies auf Angelsächsisch zu schreiben. Es tut mir leid.

> +/*
> + * 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.
> + */

Fair enough, exotic hardware yields exotic code.

> +static int mc33978_direction_input(struct gpio_chip *chip, unsigned int offset)
> +{
> +       struct mc33978_pinctrl *mpc = gpiochip_get_data(chip);
> +
> +       /*
> +        * This chip only has inputs. We emulate outputs by setting a
> +        * wetting current and/or using the tri-state register to turn it on
> +        * and off. If a pin was an output and is now tri-stated, we should
> +        * disable the tri-state now to make the input work correctly.
> +        */
> +       mutex_lock(&mpc->lock);
> +       mc33978_update_bits(mpc, MC33978_SPSG(MC33978_REG_TRI_SP, offset),
> +                       MC33978_PINMASK(offset), 0);
> +       mutex_unlock(&mpc->lock);
> +
> +       return 0;
> +}
> +
> +static int mc33978_get(struct gpio_chip *chip, unsigned int offset)
> +{
> +       struct mc33978_pinctrl *mpc = gpiochip_get_data(chip);
> +       int status, ret;
> +       bool is_switch_closed;
> +       bool is_switch_to_ground = true; /* Default for all SG pins */
> +
> +       mutex_lock(&mpc->lock);
> +
> +       /* Read hardware switch status (open or closed) */
> +       ret = mc33978_read(mpc, MC33978_REG_READ_IN, &status);
> +       if (ret < 0) {
> +               mutex_unlock(&mpc->lock);
> +               return 0;
> +       }
> +       is_switch_closed = !!(status & BIT(offset));
> +
> +       /* Determine current topology for SP pins */
> +       if (MC33978_IS_SP(offset)) {
> +               int config_reg;
> +
> +               ret = mc33978_read(mpc, MC33978_REG_CONFIG, &config_reg);
> +               if (ret == 0) {
> +                       /* CONFIG: 0 = Switch-to-Ground (PU), 1 = Switch-to-Battery (PD) */
> +                       if (config_reg & MC33978_PINMASK(offset))
> +                               is_switch_to_ground = false;
> +               }
> +       }
> +
> +       mutex_unlock(&mpc->lock);
> +
> +       /* Translate hardware switch semantics to logical GPIO levels */
> +       if (is_switch_to_ground) {
> +               /* SG: Switch open -> High (1), Switch to GND -> Low (0) */
> +               status = !is_switch_closed;
> +       } else {
> +               /* SB: Switch open -> Low (0), Switch to Vbat -> High (1) */
> +               status = is_switch_closed;
> +       }

I don't think this is right.

The driver needs to report the *physical* level on the line. Then the
lines need to be flagged with GPIO_ACTIVE_LOW or GPIO_ACTIVE_HIGH
on the consumers in the device tree.

The GPIO_ACTIVE_LOW will provide inversion in gpiolib.

We need to do it this way otherwise the double-inversion cases
when people *actually* start to use GPIO_ACTIVE_LOW in the
device tree for a SG input will start to look like madness.

> +static int mc33978_get_multiple(struct gpio_chip *chip,
> +                               unsigned long *mask, unsigned long *bits)
> +{
> +       struct mc33978_pinctrl *mpc = gpiochip_get_data(chip);
> +       unsigned int status;
> +       unsigned int config_reg = 0;
> +       unsigned int inv_mask;
> +       int ret;
> +
> +       mutex_lock(&mpc->lock);
> +
> +       ret = mc33978_read(mpc, MC33978_REG_READ_IN, &status);
> +       if (ret)
> +               goto out_unlock;
> +
> +       /* 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)
> +                       goto out_unlock;
> +       }
> +
> +       /*
> +        * SG pins (0-13) are always Switch-to-Ground.
> +        * SP pins (14-21) are Switch-to-Ground if their CONFIG bit is 0.
> +        * Switch-to-Ground logic: HW bit 0 (open) -> Logical 1 (High)
> +        * HW bit 1 (closed) -> Logical 0 (Low)
> +        * We create a mask for all Switch-to-Ground pins and XOR the status.
> +        */
> +       inv_mask = MC33978_SG_MASK | (~(config_reg << MC33978_NUM_SG) & MC33978_SP_MASK);
> +
> +       *bits = (status ^ inv_mask) & *mask;

Again, I disagree with this.

All consumers should use GPIO_ACTIVE_LOW.

> +static int mc33978_set(struct gpio_chip *chip, unsigned int offset, int value)
> +{
> +       struct mc33978_pinctrl *mpc = gpiochip_get_data(chip);
> +       int pull;
> +       int ret;
> +
> +       /*
> +        * We only have inputs with wetting current sources, that we mis-use
> +        * as open-drain/-source outputs.
> +        */
> +       if (MC33978_IS_SP(offset)) {
> +               pull = value ? MC33978_PU : MC33978_PD;
> +               value = 1;
> +       } else {
> +               pull = MC33978_PU;
> +       }
> +
> +       mutex_lock(&mpc->lock);
> +
> +       /*
> +        * Break-before-make sequencing to prevent hardware glitches (spikes).
> +        * Since SPI transfers take time, writing the pull and tri-state
> +        * registers in the wrong order causes a brief moment where current
> +        * flows to the pin before it is masked, causing a visible LED flash.
> +        */
> +       if (value) {
> +               /*
> +                * Turn ON: Configure the underlying current source (pull) first,
> +                * then route it to the pin by disabling tri-state.
> +                */
> +               ret = mc33978_set_pull(mpc, offset, pull);
> +               if (ret)
> +                       goto out_unlock;
> +
> +               ret = mc33978_update_bits(mpc, MC33978_SPSG(MC33978_REG_TRI_SP, offset),
> +                                         MC33978_PINMASK(offset), 0);
> +       } else {
> +               /*
> +                * Turn OFF: Isolate the pin first by enabling tri-state,
> +                * then safely disable the underlying current source.
> +                */
> +               ret = mc33978_update_bits(mpc, MC33978_SPSG(MC33978_REG_TRI_SP, offset),
> +                                         MC33978_PINMASK(offset), MC33978_PINMASK(offset));
> +       }

So here I'm a bit confused about what to do and the best way to
handle this.

gpiolib contains "open drain/source emulation" where we essentially
set the line as input to achieve high impedance and let pull up/down
resistors do their job.

In device tree this is possible, for example:

        i2c {
                compatible = "i2c-gpio";
                sda-gpios = <&gpio0 5 (GPIO_ACTIVE_HIGH|GPIO_OPEN_DRAIN)>;
                scl-gpios = <&gpio0 6 (GPIO_ACTIVE_HIGH|GPIO_OPEN_DRAIN)>;
                #address-cells = <1>;
                #size-cells = <0>;
...

How do you expect your code to react to these GPIO_OPEN_DRAIN
settings?

What gpiolib does is set the flag GPIOD_FLAG_OPEN_DRAIN
on the line internally, then in gpiod_direction_output_nonotify():

        if (test_bit(GPIOD_FLAG_OPEN_DRAIN, &flags)) {
                /* First see if we can enable open drain in hardware */
                ret = gpio_set_config(desc, PIN_CONFIG_DRIVE_OPEN_DRAIN);
                if (!ret)
                        goto set_output_value;
                /* Emulate open drain by not actively driving the line high */
                if (value)
                        goto set_output_flag;
        } else if (test_bit(GPIOD_FLAG_OPEN_SOURCE, &flags)) {
                ret = gpio_set_config(desc, PIN_CONFIG_DRIVE_OPEN_SOURCE);
                if (!ret)
                        goto set_output_value;
                /* Emulate open source by not actively driving the line low */
                if (!value)
                        goto set_output_flag;
        } else {
                gpio_set_config(desc, PIN_CONFIG_DRIVE_PUSH_PULL);
        }

set_output_value:
        ret = gpio_set_bias(desc);
        if (ret)
                return ret;
        return gpiod_direction_output_raw_commit(desc, value);

set_output_flag:
        ret = gpiod_direction_input_nonotify(desc);
        if (ret)
                return ret;
        /*
         * When emulating open-source or open-drain functionalities by not
         * actively driving the line (setting mode to input) we still need to
         * set the IS_OUT flag or otherwise we won't be able to set the line
         * value anymore.
         */
        set_bit(GPIOD_FLAG_IS_OUT, &desc->flags);
        return 0;

With this as background I think it is better if you:

- You have already implemented open drain (etc) in
  the .set_config callback of the gpio_chip.

- Whenever a line that is effectively an open drain or open
  source line, then *flag that* on the consumer in the device
  tree using the DT flags GPIO_LINE_OPEN_SOURCE
  or GPIO_LINE_OPEN_DRAIN

I guess that flag goes for anything using this chip?
GPIO_LINE_OPEN_SOURCE seems like a compulatory
flag, just like GPIO_ACTIVE_LOW is compulsory for
all SG inputs?

I understand that this makes the device trees more complicated
but I think it *does* describe the hardware way better, and the
presence of GPIO_LINE_OPEN_DRAIN is something your driver
*must* support anyway, otherwise gpiolib will start to emulate it
and there will be disaster when the driver is trying to second-guess
what the core actually wants to handle.

With these changes, I also wonder if it will be possible to use
GPIO_REGMAP to simplify the remaining set/get callbacks?

> +static int mc33978_gpio_to_irq(struct gpio_chip *chip, unsigned int offset)
> +{
> +       struct mc33978_pinctrl *mpc = gpiochip_get_data(chip);
> +       int virq;
> +
> +       if (!mpc->domain)
> +               return -ENXIO;
> +
> +       /* * Erzeugt das Mapping zur Laufzeit (oder gibt ein bestehendes zurück).
> +        * Ohne diesen Aufruf bleibt die lineare IRQ-Domain leer.
> +        */

Language...

> +       virq = irq_create_mapping(mpc->domain, offset);
> +       if (!virq) {
> +               dev_err(mpc->dev, "Failed to map hwirq %u to virq\n", offset);
> +               return -ENXIO;
> +       }
> +
> +       return virq;
> +}

Brrrr please try at all costs to create custom gpio_to_irq() callbacks!

Try to your hardest to use the gpiolib irqchip that will provide a proper
.to_irq() callback and set up the interrupts in the core. There are
many examples of how to do this, also in MFD devices using
regmap.

But maybe the MFD has some central IRQ handling? So I'm not
100% sure here.

Yours,
Linus Walleij

^ permalink raw reply	[flat|nested] 20+ messages in thread

* Re: [PATCH v1 3/8] dt-bindings: pinctrl: add NXP MC33978/MC34978 pinctrl
  2026-02-26 23:00   ` Linus Walleij
  2026-02-26 23:02     ` Linus Walleij
@ 2026-02-27 10:04     ` Oleksij Rempel
  1 sibling, 0 replies; 20+ messages in thread
From: Oleksij Rempel @ 2026-02-27 10:04 UTC (permalink / raw)
  To: Linus Walleij
  Cc: Guenter Roeck, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
	Lee Jones, Peter Rosin, kernel, linux-kernel, devicetree,
	linux-hwmon, linux-gpio, David Jander

On Fri, Feb 27, 2026 at 12:00:43AM +0100, Linus Walleij wrote:
> Hi Oleksij,
> 
> thanks for your patch for this very interesting hardware!
> 
> On Wed, Feb 25, 2026 at 6:16 PM Oleksij Rempel <o.rempel@pengutronix.de> wrote:
> 
> > Add device tree binding documentation for the pin control and GPIO block
> > of the NXP MC33978/MC34978 Multiple Switch Detection Interface (MSDI).
> >
> > This block manages 22 switch detection inputs (14 Switch-to-Ground,
> > 8 Programmable) and acts as a GPIO controller.
> >
> > Additionally, it supports pin configuration for hardware-specific features
> > required for long-term contact maintenance in harsh environments, such as
> > adjusting the continuous/pulsed wetting current to penetrate oxide layers
> > on mechanical switches.
> >
> > Signed-off-by: Oleksij Rempel <o.rempel@pengutronix.de>
> 
> Are the hardware-specific pin configurations for oxide layer
> penetration (!) and stuff excluded from these bindings? (It looks
> like.)

Yes, they are excluded for now. The target board is general-purpose, so
the attached switches or LEDs will vary. I'd prefer to gather more
concrete use cases before designing a generic interface for the
wetting currents.

Best Regards,
Oleksij
-- 
Pengutronix e.K.                           |                             |
Steuerwalder Str. 21                       | http://www.pengutronix.de/  |
31137 Hildesheim, Germany                  | Phone: +49-5121-206917-0    |
Amtsgericht Hildesheim, HRA 2686           | Fax:   +49-5121-206917-5555 |

^ permalink raw reply	[flat|nested] 20+ messages in thread

* Re: [PATCH v1 3/8] dt-bindings: pinctrl: add NXP MC33978/MC34978 pinctrl
  2026-02-26 23:02     ` Linus Walleij
@ 2026-02-27 10:41       ` Oleksij Rempel
  2026-02-27 14:06         ` Linus Walleij
  0 siblings, 1 reply; 20+ messages in thread
From: Oleksij Rempel @ 2026-02-27 10:41 UTC (permalink / raw)
  To: Linus Walleij
  Cc: Guenter Roeck, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
	Lee Jones, Peter Rosin, kernel, linux-kernel, devicetree,
	linux-hwmon, linux-gpio, David Jander

On Fri, Feb 27, 2026 at 12:02:53AM +0100, Linus Walleij wrote:
> On Fri, Feb 27, 2026 at 12:00 AM Linus Walleij <linusw@kernel.org> wrote:
> > On Wed, Feb 25, 2026 at 6:16 PM Oleksij Rempel <o.rempel@pengutronix.de> wrote:
> 
> > > +  - Pins 14-21: SP0-SP7 (Programmable inputs, can be SG or SB)
> >
> > What is SB now? Please explain :)

> Oh I see in the driver that this is Switch-to-battery. So document that here
> in the bindings too.
> 
> Also it seems that something configured as switch-to-batter must be
> flagged GPIO_ACTIVE_HIGH.

Actually, the active polarity depends entirely on the external circuit,
especially since these pins can also be used as controllable current
outputs.

For example, we attach LEDs directly to the pins. If an LED is
attached to an SG pin (or an SP pin operating in SG mode), the pin sinks
current to ground to turn the LED on, making it GPIO_ACTIVE_HIGH from
the LED consumer's perspective. 
-- 
Pengutronix e.K.                           |                             |
Steuerwalder Str. 21                       | http://www.pengutronix.de/  |
31137 Hildesheim, Germany                  | Phone: +49-5121-206917-0    |
Amtsgericht Hildesheim, HRA 2686           | Fax:   +49-5121-206917-5555 |

^ permalink raw reply	[flat|nested] 20+ messages in thread

* Re: [PATCH v1 4/8] pinctrl: add NXP MC33978/MC34978 pinctrl driver
  2026-02-26 23:40   ` Linus Walleij
@ 2026-02-27 10:58     ` Oleksij Rempel
  2026-02-27 14:08       ` Linus Walleij
  0 siblings, 1 reply; 20+ messages in thread
From: Oleksij Rempel @ 2026-02-27 10:58 UTC (permalink / raw)
  To: Linus Walleij
  Cc: Bartosz Golaszewski, Guenter Roeck, Rob Herring,
	Krzysztof Kozlowski, Conor Dooley, Lee Jones, Peter Rosin,
	David Jander, kernel, linux-kernel, devicetree, linux-hwmon,
	linux-gpio

Hi Linus,

On Fri, Feb 27, 2026 at 12:40:40AM +0100, Linus Walleij wrote:
..
> > +static int mc33978_get(struct gpio_chip *chip, unsigned int offset)
> > +{
> > +       struct mc33978_pinctrl *mpc = gpiochip_get_data(chip);
> > +       int status, ret;
> > +       bool is_switch_closed;
> > +       bool is_switch_to_ground = true; /* Default for all SG pins */
> > +
> > +       mutex_lock(&mpc->lock);
> > +
> > +       /* Read hardware switch status (open or closed) */
> > +       ret = mc33978_read(mpc, MC33978_REG_READ_IN, &status);
> > +       if (ret < 0) {
> > +               mutex_unlock(&mpc->lock);
> > +               return 0;
> > +       }
> > +       is_switch_closed = !!(status & BIT(offset));
> > +
> > +       /* Determine current topology for SP pins */
> > +       if (MC33978_IS_SP(offset)) {
> > +               int config_reg;
> > +
> > +               ret = mc33978_read(mpc, MC33978_REG_CONFIG, &config_reg);
> > +               if (ret == 0) {
> > +                       /* CONFIG: 0 = Switch-to-Ground (PU), 1 = Switch-to-Battery (PD) */
> > +                       if (config_reg & MC33978_PINMASK(offset))
> > +                               is_switch_to_ground = false;
> > +               }
> > +       }
> > +
> > +       mutex_unlock(&mpc->lock);
> > +
> > +       /* Translate hardware switch semantics to logical GPIO levels */
> > +       if (is_switch_to_ground) {
> > +               /* SG: Switch open -> High (1), Switch to GND -> Low (0) */
> > +               status = !is_switch_closed;
> > +       } else {
> > +               /* SB: Switch open -> Low (0), Switch to Vbat -> High (1) */
> > +               status = is_switch_closed;
> > +       }
> 
> I don't think this is right.
> 
> The driver needs to report the *physical* level on the line. Then the
> lines need to be flagged with GPIO_ACTIVE_LOW or GPIO_ACTIVE_HIGH
> on the consumers in the device tree.

Returning the physical level is actually exactly what this code is
trying to do. I need to rewrite the comment :) 

The issue is that the MC33978 hardware does not report the physical
voltage level on the pin. As per section 9.10.27 (Read switch status) of
the datasheet: "A Logic [1] means the switch is closed while a Logic [0]
is an open switch."

Because it only reports this abstract "contact status", I have to
translate it back to the actual physical voltage level (1 = High, 0 =
Low) based on the pin's current configuration:

In Switch-to-Ground (SG) mode: the status bit stays 0 when the physical
voltage on the line is High (open), and reports 1 when the physical
voltage is Low (shorted to ground).
In Switch-to-Battery (SB) mode: the exact opposite happens.

Best Regards,
Oleksij
-- 
Pengutronix e.K.                           |                             |
Steuerwalder Str. 21                       | http://www.pengutronix.de/  |
31137 Hildesheim, Germany                  | Phone: +49-5121-206917-0    |
Amtsgericht Hildesheim, HRA 2686           | Fax:   +49-5121-206917-5555 |

^ permalink raw reply	[flat|nested] 20+ messages in thread

* Re: [PATCH v1 3/8] dt-bindings: pinctrl: add NXP MC33978/MC34978 pinctrl
  2026-02-27 10:41       ` Oleksij Rempel
@ 2026-02-27 14:06         ` Linus Walleij
  0 siblings, 0 replies; 20+ messages in thread
From: Linus Walleij @ 2026-02-27 14:06 UTC (permalink / raw)
  To: Oleksij Rempel
  Cc: Guenter Roeck, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
	Lee Jones, Peter Rosin, kernel, linux-kernel, devicetree,
	linux-hwmon, linux-gpio, David Jander

On Fri, Feb 27, 2026 at 11:41 AM Oleksij Rempel <o.rempel@pengutronix.de> wrote:
> On Fri, Feb 27, 2026 at 12:02:53AM +0100, Linus Walleij wrote:
> > On Fri, Feb 27, 2026 at 12:00 AM Linus Walleij <linusw@kernel.org> wrote:
> > > On Wed, Feb 25, 2026 at 6:16 PM Oleksij Rempel <o.rempel@pengutronix.de> wrote:
> >
> > > > +  - Pins 14-21: SP0-SP7 (Programmable inputs, can be SG or SB)
> > >
> > > What is SB now? Please explain :)
>
> > Oh I see in the driver that this is Switch-to-battery. So document that here
> > in the bindings too.
> >
> > Also it seems that something configured as switch-to-batter must be
> > flagged GPIO_ACTIVE_HIGH.
>
> Actually, the active polarity depends entirely on the external circuit,
> especially since these pins can also be used as controllable current
> outputs.
>
> For example, we attach LEDs directly to the pins. If an LED is
> attached to an SG pin (or an SP pin operating in SG mode), the pin sinks
> current to ground to turn the LED on, making it GPIO_ACTIVE_HIGH from
> the LED consumer's perspective.

Aha OK I see.

My main input to the bindings and the driver design is a second pass
at polarity handling and how consumers in the device tree should
be flagged with GPIO_ACTIVE_LOW and/or GPIO_OPEN_DRAIN
and similar settings and how the Operating System (any operating
system) is supposed to react to that.

My main concern is the semantics with these flags.

Yours,
Linus Walleij

^ permalink raw reply	[flat|nested] 20+ messages in thread

* Re: [PATCH v1 4/8] pinctrl: add NXP MC33978/MC34978 pinctrl driver
  2026-02-27 10:58     ` Oleksij Rempel
@ 2026-02-27 14:08       ` Linus Walleij
  0 siblings, 0 replies; 20+ messages in thread
From: Linus Walleij @ 2026-02-27 14:08 UTC (permalink / raw)
  To: Oleksij Rempel
  Cc: Bartosz Golaszewski, Guenter Roeck, Rob Herring,
	Krzysztof Kozlowski, Conor Dooley, Lee Jones, Peter Rosin,
	David Jander, kernel, linux-kernel, devicetree, linux-hwmon,
	linux-gpio

On Fri, Feb 27, 2026 at 11:58 AM Oleksij Rempel <o.rempel@pengutronix.de> wrote:

> > The driver needs to report the *physical* level on the line. Then the
> > lines need to be flagged with GPIO_ACTIVE_LOW or GPIO_ACTIVE_HIGH
> > on the consumers in the device tree.
>
> Returning the physical level is actually exactly what this code is
> trying to do. I need to rewrite the comment :)

Aha OK I'm not very smart at times...

> The issue is that the MC33978 hardware does not report the physical
> voltage level on the pin. As per section 9.10.27 (Read switch status) of
> the datasheet: "A Logic [1] means the switch is closed while a Logic [0]
> is an open switch."
>
> Because it only reports this abstract "contact status", I have to
> translate it back to the actual physical voltage level (1 = High, 0 =
> Low) based on the pin's current configuration:
>
> In Switch-to-Ground (SG) mode: the status bit stays 0 when the physical
> voltage on the line is High (open), and reports 1 when the physical
> voltage is Low (shorted to ground).
> In Switch-to-Battery (SB) mode: the exact opposite happens.

Fair enough, as long as we have a (possibly verbose...) explanation
about what is going on in the binding document I think the
proper driver behaviour will come out as obvious in the end.

Yours,
Linus Walleij

^ permalink raw reply	[flat|nested] 20+ messages in thread

end of thread, other threads:[~2026-02-27 14:08 UTC | newest]

Thread overview: 20+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-02-25 17:15 [PATCH v1 0/8] mfd: Add support for NXP MC33978/MC34978 MSDI Oleksij Rempel
2026-02-25 17:15 ` [PATCH v1 1/8] dt-bindings: mfd: add " Oleksij Rempel
2026-02-26  8:16   ` Krzysztof Kozlowski
2026-02-25 17:15 ` [PATCH v1 2/8] mfd: add NXP MC33978/MC34978 core driver Oleksij Rempel
2026-02-25 17:15 ` [PATCH v1 3/8] dt-bindings: pinctrl: add NXP MC33978/MC34978 pinctrl Oleksij Rempel
2026-02-26 23:00   ` Linus Walleij
2026-02-26 23:02     ` Linus Walleij
2026-02-27 10:41       ` Oleksij Rempel
2026-02-27 14:06         ` Linus Walleij
2026-02-27 10:04     ` Oleksij Rempel
2026-02-25 17:15 ` [PATCH v1 4/8] pinctrl: add NXP MC33978/MC34978 pinctrl driver Oleksij Rempel
2026-02-26 23:40   ` Linus Walleij
2026-02-27 10:58     ` Oleksij Rempel
2026-02-27 14:08       ` Linus Walleij
2026-02-25 17:15 ` [PATCH v1 5/8] dt-bindings: hwmon: add NXP MC33978/MC34978 hwmon Oleksij Rempel
2026-02-26  8:17   ` Krzysztof Kozlowski
2026-02-25 17:15 ` [PATCH v1 6/8] hwmon: add NXP MC33978/MC34978 driver Oleksij Rempel
2026-02-25 17:15 ` [PATCH v1 7/8] dt-bindings: mux: add NXP MC33978/MC34978 AMUX Oleksij Rempel
2026-02-26  8:14   ` Krzysztof Kozlowski
2026-02-25 17:15 ` [PATCH v1 8/8] mux: add NXP MC33978/MC34978 AMUX driver Oleksij Rempel

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