* [PATCH v8 2/3] hwmon: ltc4283: Add support for the LTC4283 Swap Controller
From: Nuno Sá via B4 Relay @ 2026-03-27 17:26 UTC (permalink / raw)
To: linux-gpio, linux-hwmon, devicetree, linux-doc
Cc: Guenter Roeck, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Jonathan Corbet, Linus Walleij, Bartosz Golaszewski
In-Reply-To: <20260327-ltc4283-support-v8-0-471de255d728@analog.com>
From: Nuno Sá <nuno.sa@analog.com>
Support the LTC4283 Hot Swap Controller. The device features programmable
current limit with foldback and independently adjustable inrush current to
optimize the MOSFET safe operating area (SOA). The SOA timer limits MOSFET
temperature rise for reliable protection against overstresses.
An I2C interface and onboard ADC allow monitoring of board current,
voltage, power, energy, and fault status.
Signed-off-by: Nuno Sá <nuno.sa@analog.com>
---
Documentation/hwmon/index.rst | 1 +
Documentation/hwmon/ltc4283.rst | 266 ++++++
MAINTAINERS | 1 +
drivers/hwmon/Kconfig | 12 +
drivers/hwmon/Makefile | 1 +
drivers/hwmon/ltc4283.c | 1796 +++++++++++++++++++++++++++++++++++++++
6 files changed, 2077 insertions(+)
diff --git a/Documentation/hwmon/index.rst b/Documentation/hwmon/index.rst
index 559c32344cd3..eab95152abee 100644
--- a/Documentation/hwmon/index.rst
+++ b/Documentation/hwmon/index.rst
@@ -144,6 +144,7 @@ Hardware Monitoring Kernel Drivers
ltc4260
ltc4261
ltc4282
+ ltc4283
ltc4286
macsmc-hwmon
max127
diff --git a/Documentation/hwmon/ltc4283.rst b/Documentation/hwmon/ltc4283.rst
new file mode 100644
index 000000000000..ba88445e45f4
--- /dev/null
+++ b/Documentation/hwmon/ltc4283.rst
@@ -0,0 +1,266 @@
+.. SPDX-License-Identifier: GPL-2.0-only
+
+Kernel drivers ltc4283
+==========================================
+
+Supported chips:
+
+ * Analog Devices LTC4283
+
+ Prefix: 'ltc4283'
+
+ Addresses scanned: -
+
+ Datasheet:
+
+ https://www.analog.com/media/en/technical-documentation/data-sheets/ltc4283.pdf
+
+Author: Nuno Sá <nuno.sa@analog.com>
+
+Description
+___________
+
+The LTC4283 negative voltage hot swap controller drives an external N-channel
+MOSFET to allow a board to be safely inserted and removed from a live backplane.
+The device features programmable current limit with foldback and independently
+adjustable inrush current to optimize the MOSFET safe operating area (SOA). The
+SOA timer limits MOSFET temperature rise for reliable protection against
+overstresses. An I2C interface and onboard gear-shift ADC allow monitoring of
+board current, voltage, power, energy, and fault status. Additional features
+respond to input UV/OV, interrupt the host when a fault has occurred, notify
+when output power is good, detect insertion of a board, turn off the MOSFET
+if an external supply monitor fails to indicate power good within a timeout
+period, and auto-reboot after a programmable delay following a host commanded
+turn-off.
+
+Sysfs entries
+_____________
+
+The following attributes are supported. Limits are read-write and all the other
+attributes are read-only. Note that the VADIOx channels might not be available
+if the ADIO pins are used as GPIOs (naturally also affects the respective
+differential channels).
+
+======================= ==========================================
+in0_lcrit_alarm Critical Undervoltage alarm
+in0_crit_alarm Critical Overvoltage alarm
+in0_label Channel label (VIN)
+
+in1_input Output voltage (mV).
+in1_min Undervoltage threshold
+in1_max Overvoltage threshold
+in1_lowest Lowest measured voltage
+in1_highest Highest measured voltage
+in1_reset_history Write 1 to reset history.
+in1_min_alarm Undervoltage alarm
+in1_max_alarm Overvoltage alarm
+in1_label Channel label (VPWR)
+
+in2_input Output voltage (mV).
+in2_min Undervoltage threshold
+in2_max Overvoltage threshold
+in2_lowest Lowest measured voltage
+in2_highest Highest measured voltage
+in2_reset_history Write 1 to reset history.
+in2_min_alarm Undervoltage alarm
+in2_max_alarm Overvoltage alarm
+in2_enable Enable/Disable monitoring.
+in2_label Channel label (VADI1)
+
+in3_input Output voltage (mV).
+in3_min Undervoltage threshold
+in3_max Overvoltage threshold
+in3_lowest Lowest measured voltage
+in3_highest Highest measured voltage
+in3_reset_history Write 1 to reset history.
+in3_min_alarm Undervoltage alarm
+in3_max_alarm Overvoltage alarm
+in3_enable Enable/Disable monitoring.
+in3_label Channel label (VADI2)
+
+in4_input Output voltage (mV).
+in4_min Undervoltage threshold
+in4_max Overvoltage threshold
+in4_lowest Lowest measured voltage
+in4_highest Highest measured voltage
+in4_reset_history Write 1 to reset history.
+in4_min_alarm Undervoltage alarm
+in4_max_alarm Overvoltage alarm
+in4_enable Enable/Disable monitoring.
+in4_label Channel label (VADI3)
+
+in5_input Output voltage (mV).
+in5_min Undervoltage threshold
+in5_max Overvoltage threshold
+in5_lowest Lowest measured voltage
+in5_highest Highest measured voltage
+in5_reset_history Write 1 to reset history.
+in5_min_alarm Undervoltage alarm
+in5_max_alarm Overvoltage alarm
+in5_enable Enable/Disable monitoring.
+in5_label Channel label (VADI4)
+
+in6_input Output voltage (mV).
+in6_min Undervoltage threshold
+in6_max Overvoltage threshold
+in6_lowest Lowest measured voltage
+in6_highest Highest measured voltage
+in6_reset_history Write 1 to reset history.
+in6_min_alarm Undervoltage alarm
+in6_max_alarm Overvoltage alarm
+in6_enable Enable/Disable monitoring.
+in6_label Channel label (VADIO1)
+
+in7_input Output voltage (mV).
+in7_min Undervoltage threshold
+in7_max Overvoltage threshold
+in7_lowest Lowest measured voltage
+in7_highest Highest measured voltage
+in7_reset_history Write 1 to reset history.
+in7_min_alarm Undervoltage alarm
+in7_max_alarm Overvoltage alarm
+in7_enable Enable/Disable monitoring.
+in7_label Channel label (VADIO2)
+
+in8_input Output voltage (mV).
+in8_min Undervoltage threshold
+in8_max Overvoltage threshold
+in8_lowest Lowest measured voltage
+in8_highest Highest measured voltage
+in8_reset_history Write 1 to reset history.
+in8_min_alarm Undervoltage alarm
+in8_max_alarm Overvoltage alarm
+in8_enable Enable/Disable monitoring.
+in8_label Channel label (VADIO3)
+
+in9_input Output voltage (mV).
+in9_min Undervoltage threshold
+in9_max Overvoltage threshold
+in9_lowest Lowest measured voltage
+in9_highest Highest measured voltage
+in9_reset_history Write 1 to reset history.
+in9_min_alarm Undervoltage alarm
+in9_max_alarm Overvoltage alarm
+in9_enable Enable/Disable monitoring.
+in9_label Channel label (VADIO4)
+
+in10_input Output voltage (mV).
+in10_min Undervoltage threshold
+in10_max Overvoltage threshold
+in10_lowest Lowest measured voltage
+in10_highest Highest measured voltage
+in10_reset_history Write 1 to reset history.
+in10_min_alarm Undervoltage alarm
+in10_max_alarm Overvoltage alarm
+in10_enable Enable/Disable monitoring.
+in10_label Channel label (DRNS)
+
+in11_input Output voltage (mV).
+in11_min Undervoltage threshold
+in11_max Overvoltage threshold
+in11_lowest Lowest measured voltage
+in11_highest Highest measured voltage
+in11_reset_history Write 1 to reset history.
+ Also clears fet bad and short fault logs.
+in11_min_alarm Undervoltage alarm
+in11_max_alarm Overvoltage alarm
+in11_enable Enable/Disable monitoring
+in11_fault Failure in the MOSFET. Either bad or shorted FET.
+in11_label Channel label (DRAIN)
+
+in12_input Output voltage (mV).
+in12_min Undervoltage threshold
+in12_max Overvoltage threshold
+in12_lowest Lowest measured voltage
+in12_highest Highest measured voltage
+in12_reset_history Write 1 to reset history.
+in12_min_alarm Undervoltage alarm
+in12_max_alarm Overvoltage alarm
+in12_enable Enable/Disable monitoring.
+in12_label Channel label (ADIN2-ADIN1)
+
+in13_input Output voltage (mV).
+in13_min Undervoltage threshold
+in13_max Overvoltage threshold
+in13_lowest Lowest measured voltage
+in13_highest Highest measured voltage
+in13_reset_history Write 1 to reset history.
+in13_min_alarm Undervoltage alarm
+in13_max_alarm Overvoltage alarm
+in13_enable Enable/Disable monitoring.
+in13_label Channel label (ADIN4-ADIN3)
+
+in14_input Output voltage (mV).
+in14_min Undervoltage threshold
+in14_max Overvoltage threshold
+in14_lowest Lowest measured voltage
+in14_highest Highest measured voltage
+in14_reset_history Write 1 to reset history.
+in14_min_alarm Undervoltage alarm
+in14_max_alarm Overvoltage alarm
+in14_enable Enable/Disable monitoring.
+in14_label Channel label (ADIO2-ADIO1)
+
+in15_input Output voltage (mV).
+in15_min Undervoltage threshold
+in15_max Overvoltage threshold
+in15_lowest Lowest measured voltage
+in15_highest Highest measured voltage
+in15_reset_history Write 1 to reset history.
+in15_min_alarm Undervoltage alarm
+in15_max_alarm Overvoltage alarm
+in15_enable Enable/Disable monitoring.
+in15_label Channel label (ADIO4-ADIO3)
+
+curr1_input Sense current (mA)
+curr1_min Undercurrent threshold
+curr1_max Overcurrent threshold
+curr1_lowest Lowest measured current
+curr1_highest Highest measured current
+curr1_reset_history Write 1 to reset curr1 history.
+ Also clears overcurrent fault logs.
+curr1_min_alarm Undercurrent alarm
+curr1_max_alarm Overcurrent alarm
+curr1_crit_alarm Critical Overcurrent alarm
+curr1_label Channel label (ISENSE)
+
+power1_input Power (in uW)
+power1_min Low power threshold
+power1_max High power threshold
+power1_input_lowest Historical minimum power use
+power1_input_highest Historical maximum power use
+power1_reset_history Write 1 to reset power1 history.
+ Also clears power fault logs.
+power1_min_alarm Low power alarm
+power1_max_alarm High power alarm
+power1_label Channel label (Power)
+
+energy1_input Measured energy over time (in microJoule)
+energy1_enable Enable/Disable Energy accumulation
+======================= ==========================================
+
+DebugFs entries
+_______________
+
+The chip also has a fault log register where failures can be logged. Hence,
+as these are logging events, we give access to them in debugfs. Note that
+even if some failure is detected in these logs, it does necessarily mean
+that the failure is still present. As mentioned in the proper Sysfs entries,
+these logs can be cleared by writing in the proper reset_history attribute.
+
+.. warning:: The debugfs interface is subject to change without notice
+ and is only available when the kernel is compiled with
+ ``CONFIG_DEBUG_FS`` defined.
+
+``/sys/kernel/debug/i2c/i2c-[X]/[X]-addr/``
+contains the following attributes:
+
+======================= ==========================================
+power1_failed_fault_log Set to 1 by a power1 fault occurring.
+power1_good_input_fault_log Set to 1 by a power1 good input fault occurring at PGIO3.
+in11_fet_short_fault_log Set to 1 when a FET-short fault occurs.
+in11_fet_bad_fault_log Set to 1 when a FET-BAD fault occurs.
+in0_lcrit_fault_log Set to 1 by a VIN undervoltage fault occurring.
+in0_crit_fault_log Set to 1 by a VIN overvoltage fault occurring.
+curr1_crit_fault_log Set to 1 by an overcurrent fault occurring.
+======================= ==========================================
diff --git a/MAINTAINERS b/MAINTAINERS
index 13ae2f3db449..38d22cf622b7 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -15146,6 +15146,7 @@ M: Nuno Sá <nuno.sa@analog.com>
L: linux-hwmon@vger.kernel.org
S: Supported
F: Documentation/devicetree/bindings/hwmon/adi,ltc4283.yaml
+F: drivers/hwmon/ltc4283.c
LTC4286 HARDWARE MONITOR DRIVER
M: Delphine CC Chiu <Delphine_CC_Chiu@Wiwynn.com>
diff --git a/drivers/hwmon/Kconfig b/drivers/hwmon/Kconfig
index 7dd8381ba0d0..d8334edf5514 100644
--- a/drivers/hwmon/Kconfig
+++ b/drivers/hwmon/Kconfig
@@ -1157,6 +1157,18 @@ config SENSORS_LTC4282
This driver can also be built as a module. If so, the module will
be called ltc4282.
+config SENSORS_LTC4283
+ tristate "Analog Devices LTC4283"
+ depends on I2C
+ select REGMAP_I2C
+ select AUXILIARY_BUS
+ help
+ If you say yes here you get support for Analog Devices LTC4283
+ Negative Voltage Hot Swap Controller I2C interface.
+
+ This driver can also be built as a module. If so, the module will
+ be called ltc4283.
+
config SENSORS_LTQ_CPUTEMP
bool "Lantiq cpu temperature sensor driver"
depends on SOC_XWAY
diff --git a/drivers/hwmon/Makefile b/drivers/hwmon/Makefile
index 556e86d277b1..cb77938dbe07 100644
--- a/drivers/hwmon/Makefile
+++ b/drivers/hwmon/Makefile
@@ -147,6 +147,7 @@ obj-$(CONFIG_SENSORS_LTC4245) += ltc4245.o
obj-$(CONFIG_SENSORS_LTC4260) += ltc4260.o
obj-$(CONFIG_SENSORS_LTC4261) += ltc4261.o
obj-$(CONFIG_SENSORS_LTC4282) += ltc4282.o
+obj-$(CONFIG_SENSORS_LTC4283) += ltc4283.o
obj-$(CONFIG_SENSORS_LTQ_CPUTEMP) += ltq-cputemp.o
obj-$(CONFIG_SENSORS_MACSMC_HWMON) += macsmc-hwmon.o
obj-$(CONFIG_SENSORS_MAX1111) += max1111.o
diff --git a/drivers/hwmon/ltc4283.c b/drivers/hwmon/ltc4283.c
new file mode 100644
index 000000000000..f5320f34eb45
--- /dev/null
+++ b/drivers/hwmon/ltc4283.c
@@ -0,0 +1,1796 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Analog Devices LTC4283 I2C Negative Voltage Hot Swap Controller (HWMON)
+ *
+ * Copyright 2025 Analog Devices Inc.
+ */
+#include <linux/auxiliary_bus.h>
+#include <linux/bitfield.h>
+#include <linux/bitmap.h>
+#include <linux/bitops.h>
+#include <linux/bits.h>
+
+#include <linux/debugfs.h>
+#include <linux/device.h>
+#include <linux/device/devres.h>
+#include <linux/hwmon.h>
+#include <linux/i2c.h>
+#include <linux/math.h>
+#include <linux/math64.h>
+#include <linux/minmax.h>
+#include <linux/module.h>
+
+#include <linux/mod_devicetable.h>
+#include <linux/overflow.h>
+#include <linux/property.h>
+#include <linux/regmap.h>
+#include <linux/unaligned.h>
+#include <linux/units.h>
+
+#define LTC4283_SYSTEM_STATUS 0x00
+#define LTC4283_FAULT_STATUS 0x03
+#define LTC4283_OV_MASK BIT(0)
+#define LTC4283_UV_MASK BIT(1)
+#define LTC4283_OC_MASK BIT(2)
+#define LTC4283_FET_BAD_MASK BIT(3)
+#define LTC4283_FET_SHORT_MASK BIT(6)
+#define LTC4283_FAULT_LOG 0x04
+#define LTC4283_OV_FAULT_MASK BIT(0)
+#define LTC4283_UV_FAULT_MASK BIT(1)
+#define LTC4283_OC_FAULT_MASK BIT(2)
+#define LTC4283_FET_BAD_FAULT_MASK BIT(3)
+#define LTC4283_PGI_FAULT_MASK BIT(4)
+#define LTC4283_PWR_FAIL_FAULT_MASK BIT(5)
+#define LTC4283_FET_SHORT_FAULT_MASK BIT(6)
+#define LTC4283_ADC_ALM_LOG_1 0x05
+#define LTC4283_POWER_LOW_ALM BIT(0)
+#define LTC4283_POWER_HIGH_ALM BIT(1)
+#define LTC4283_SENSE_LOW_ALM BIT(4)
+#define LTC4283_SENSE_HIGH_ALM BIT(5)
+#define LTC4283_ADC_ALM_LOG_2 0x06
+#define LTC4283_ADC_ALM_LOG_3 0x07
+#define LTC4283_ADC_ALM_LOG_4 0x08
+#define LTC4283_ADC_ALM_LOG_5 0x09
+#define LTC4283_CONTROL_1 0x0a
+#define LTC4283_RW_PAGE_MASK BIT(0)
+#define LTC4283_PIGIO2_ACLB_MASK BIT(2)
+#define LTC4283_PWRGD_RST_CTRL_MASK BIT(3)
+#define LTC4283_FET_BAD_OFF_MASK BIT(4)
+#define LTC4283_THERM_TMR_MASK BIT(5)
+#define LTC4283_DVDT_MASK BIT(6)
+#define LTC4283_CONTROL_2 0x0b
+#define LTC4283_OV_RETRY_MASK BIT(0)
+#define LTC4283_UV_RETRY_MASK BIT(1)
+#define LTC4283_OC_RETRY_MASK GENMASK(3, 2)
+#define LTC4283_FET_BAD_RETRY_MASK GENMASK(5, 4)
+#define LTC4283_EXT_FAULT_RETRY_MASK BIT(7)
+#define LTC4283_RESERVED_OC 0x0c
+#define LTC4283_CONFIG_1 0x0d
+#define LTC4283_FB_MASK GENMASK(3, 2)
+#define LTC4283_ILIM_MASK GENMASK(7, 4)
+#define LTC4283_CONFIG_2 0x0e
+#define LTC4283_COOLING_DL_MASK GENMASK(3, 1)
+#define LTC4283_FTBD_DL_MASK GENMASK(5, 4)
+#define LTC4283_CONFIG_3 0x0f
+#define LTC4283_VPWR_DRNS_MASK BIT(6)
+#define LTC4283_EXTFLT_TURN_OFF_MASK BIT(7)
+#define LTC4283_PGIO_CONFIG 0x10
+#define LTC4283_PGIO1_CFG_MASK GENMASK(1, 0)
+#define LTC4283_PGIO2_CFG_MASK GENMASK(3, 2)
+#define LTC4283_PGIO3_CFG_MASK GENMASK(5, 4)
+#define LTC4283_PGIO4_CFG_MASK GENMASK(7, 6)
+#define LTC4283_PGIO_CONFIG_2 0x11
+#define LTC4283_ADC_MASK GENMASK(2, 0)
+#define LTC4283_ADC_SELECT(c) (0x13 + (c) / 8)
+#define LTC4283_ADC_SELECT_MASK(c) BIT((c) % 8)
+#define LTC4283_SENSE_MIN_TH 0x1b
+#define LTC4283_SENSE_MAX_TH 0x1c
+#define LTC4283_VPWR_MIN_TH 0x1d
+#define LTC4283_VPWR_MAX_TH 0x1e
+#define LTC4283_POWER_MIN_TH 0x1f
+#define LTC4283_POWER_MAX_TH 0x20
+#define LTC4283_ADC_2_MIN_TH(c) (0x21 + (c) * 2)
+#define LTC4283_ADC_2_MAX_TH(c) (0x22 + (c) * 2)
+#define LTC4283_ADC_2_MIN_TH_DIFF(c) (0x39 + (c) * 2)
+#define LTC4283_ADC_2_MAX_TH_DIFF(c) (0x3a + (c) * 2)
+#define LTC4283_SENSE 0x41
+#define LTC4283_SENSE_MIN 0x42
+#define LTC4283_SENSE_MAX 0x43
+#define LTC4283_VPWR 0x44
+#define LTC4283_VPWR_MIN 0x45
+#define LTC4283_VPWR_MAX 0x46
+#define LTC4283_POWER 0x47
+#define LTC4283_POWER_MIN 0x48
+#define LTC4283_POWER_MAX 0x49
+#define LTC4283_RESERVED_68 0x68
+#define LTC4283_RESERVED_6D 0x6D
+/* get channels from ADC 2 */
+#define LTC4283_ADC_2(c) (0x4a + (c) * 3)
+#define LTC4283_ADC_2_MIN(c) (0x4b + (c) * 3)
+#define LTC4283_ADC_2_MAX(c) (0x4c + (c) * 3)
+#define LTC4283_ADC_2_DIFF(c) (0x6e + (c) * 3)
+#define LTC4283_ADC_2_MIN_DIFF(c) (0x6f + (c) * 3)
+#define LTC4283_ADC_2_MAX_DIFF(c) (0x70 + (c) * 3)
+#define LTC4283_ENERGY 0x7a
+#define LTC4283_METER_CONTROL 0x84
+#define LTC4283_INTEGRATE_I_MASK BIT(0)
+#define LTC4283_METER_HALT_MASK BIT(6)
+#define LTC4283_RESERVED_86 0x86
+#define LTC4283_RESERVED_8F 0x8F
+#define LTC4283_FAULT_LOG_CTRL 0x90
+#define LTC4283_FAULT_LOG_EN_MASK BIT(7)
+#define LTC4283_RESERVED_91 0x91
+#define LTC4283_RESERVED_A1 0xA1
+#define LTC4283_RESERVED_A3 0xA3
+#define LTC4283_RESERVED_AC 0xAC
+#define LTC4283_POWER_PLAY_MSB 0xE7
+#define LTC4283_POWER_PLAY_LSB 0xE8
+#define LTC4283_RESERVED_F1 0xF1
+#define LTC4283_RESERVED_FF 0xFF
+
+/* also applies for differential channels */
+#define LTC4283_ADC1_FS_uV 32768
+#define LTC4283_ADC2_FS_mV 2048
+#define LTC4283_TCONV_uS 64103
+#define LTC4283_VILIM_MIN_uV 15000
+#define LTC4283_VILIM_MAX_uV 30000
+#define LTC4283_VILIM_RANGE \
+ (LTC4283_VILIM_MAX_uV - LTC4283_VILIM_MIN_uV + 1)
+
+#define LTC4283_PGIO_FUNC_GPIO 2
+#define LTC4283_PGIO2_FUNC_ACLB 3
+
+/* voltage channels */
+enum {
+ LTC4283_CHAN_VIN,
+ LTC4283_CHAN_VPWR,
+ LTC4283_CHAN_ADI_1,
+ LTC4283_CHAN_ADI_2,
+ LTC4283_CHAN_ADI_3,
+ LTC4283_CHAN_ADI_4,
+ LTC4283_CHAN_ADIO_1,
+ LTC4283_CHAN_ADIO_2,
+ LTC4283_CHAN_ADIO_3,
+ LTC4283_CHAN_ADIO_4,
+ LTC4283_CHAN_DRNS,
+ LTC4283_CHAN_DRAIN,
+ /* differential channels */
+ LTC4283_CHAN_ADIN12,
+ LTC4283_CHAN_ADIN34,
+ LTC4283_CHAN_ADIO12,
+ LTC4283_CHAN_ADIO34,
+ LTC4283_CHAN_MAX
+};
+
+/* Just for ease of use on the regmap */
+#define LTC4283_ADIO34_MAX \
+ LTC4283_ADC_2_MAX_DIFF(LTC4283_CHAN_ADIO34 - LTC4283_CHAN_ADIN12)
+
+struct ltc4283_hwmon {
+ struct regmap *map;
+ struct i2c_client *client;
+ unsigned long gpio_mask;
+ unsigned long ch_enable_mask;
+ /* in microwatt */
+ long power_max;
+ /* in millivolt */
+ u32 vsense_max;
+ /* in tenths of microohm*/
+ u32 rsense;
+ bool energy_en;
+ bool ext_fault;
+};
+
+static int ltc4283_read_voltage_word(const struct ltc4283_hwmon *st,
+ u32 reg, u32 fs, long *val)
+{
+ unsigned int __raw;
+ int ret;
+
+ ret = regmap_read(st->map, reg, &__raw);
+ if (ret)
+ return ret;
+
+ *val = DIV_ROUND_CLOSEST(__raw * fs, BIT(16));
+ return 0;
+}
+
+static int ltc4283_read_voltage_byte(const struct ltc4283_hwmon *st,
+ u32 reg, u32 fs, long *val)
+{
+ int ret;
+ u32 in;
+
+ ret = regmap_read(st->map, reg, &in);
+ if (ret)
+ return ret;
+
+ *val = DIV_ROUND_CLOSEST(in * fs, BIT(8));
+ return 0;
+}
+
+static u32 ltc4283_in_reg(u32 attr, u32 channel)
+{
+ switch (attr) {
+ case hwmon_in_input:
+ if (channel == LTC4283_CHAN_VPWR)
+ return LTC4283_VPWR;
+ if (channel >= LTC4283_CHAN_ADI_1 && channel <= LTC4283_CHAN_DRAIN)
+ return LTC4283_ADC_2(channel - LTC4283_CHAN_ADI_1);
+ return LTC4283_ADC_2_DIFF(channel - LTC4283_CHAN_ADIN12);
+ case hwmon_in_highest:
+ if (channel == LTC4283_CHAN_VPWR)
+ return LTC4283_VPWR_MAX;
+ if (channel >= LTC4283_CHAN_ADI_1 && channel <= LTC4283_CHAN_DRAIN)
+ return LTC4283_ADC_2_MAX(channel - LTC4283_CHAN_ADI_1);
+ return LTC4283_ADC_2_MAX_DIFF(channel - LTC4283_CHAN_ADIN12);
+ case hwmon_in_lowest:
+ if (channel == LTC4283_CHAN_VPWR)
+ return LTC4283_VPWR_MIN;
+ if (channel >= LTC4283_CHAN_ADI_1 && channel <= LTC4283_CHAN_DRAIN)
+ return LTC4283_ADC_2_MIN(channel - LTC4283_CHAN_ADI_1);
+ return LTC4283_ADC_2_MIN_DIFF(channel - LTC4283_CHAN_ADIN12);
+ case hwmon_in_max:
+ if (channel == LTC4283_CHAN_VPWR)
+ return LTC4283_VPWR_MAX_TH;
+ if (channel >= LTC4283_CHAN_ADI_1 && channel <= LTC4283_CHAN_DRAIN)
+ return LTC4283_ADC_2_MAX_TH(channel - LTC4283_CHAN_ADI_1);
+ return LTC4283_ADC_2_MAX_TH_DIFF(channel - LTC4283_CHAN_ADIN12);
+ default:
+ if (channel == LTC4283_CHAN_VPWR)
+ return LTC4283_VPWR_MIN_TH;
+ if (channel >= LTC4283_CHAN_ADI_1 && channel <= LTC4283_CHAN_DRAIN)
+ return LTC4283_ADC_2_MIN_TH(channel - LTC4283_CHAN_ADI_1);
+ return LTC4283_ADC_2_MIN_TH_DIFF(channel - LTC4283_CHAN_ADIN12);
+ }
+}
+
+static int ltc4283_read_in_vals(const struct ltc4283_hwmon *st,
+ u32 attr, u32 channel, long *val)
+{
+ u32 reg = ltc4283_in_reg(attr, channel);
+ int ret;
+
+ if (channel < LTC4283_CHAN_ADIN12) {
+ if (attr != hwmon_in_max && attr != hwmon_in_min)
+ return ltc4283_read_voltage_word(st, reg,
+ LTC4283_ADC2_FS_mV,
+ val);
+
+ return ltc4283_read_voltage_byte(st, reg,
+ LTC4283_ADC2_FS_mV, val);
+ }
+
+ if (attr != hwmon_in_max && attr != hwmon_in_min)
+ ret = ltc4283_read_voltage_word(st, reg,
+ LTC4283_ADC1_FS_uV, val);
+ else
+ ret = ltc4283_read_voltage_byte(st, reg,
+ LTC4283_ADC1_FS_uV, val);
+ if (ret)
+ return ret;
+
+ *val = DIV_ROUND_CLOSEST(*val, MILLI);
+ return 0;
+}
+
+static int ltc4283_read_alarm(struct ltc4283_hwmon *st, u32 reg,
+ u32 mask, long *val)
+{
+ u32 alarm;
+ int ret;
+
+ ret = regmap_read(st->map, reg, &alarm);
+ if (ret)
+ return ret;
+
+ *val = !!(alarm & mask);
+
+ /* If not status/fault logs, clear the alarm after reading it. */
+ if (reg != LTC4283_FAULT_STATUS && reg != LTC4283_FAULT_LOG)
+ return regmap_clear_bits(st->map, reg, mask);
+
+ return 0;
+}
+
+static int ltc4283_read_in_alarm(struct ltc4283_hwmon *st, u32 channel,
+ bool max_alm, long *val)
+{
+ if (channel == LTC4283_VPWR)
+ return ltc4283_read_alarm(st, LTC4283_ADC_ALM_LOG_1,
+ BIT(2 + max_alm), val);
+
+ if (channel >= LTC4283_CHAN_ADI_1 && channel <= LTC4283_CHAN_ADI_4) {
+ u32 bit = (channel - LTC4283_CHAN_ADI_1) * 2;
+ /*
+ * Lower channels go to higher bits. We also want to go +1 down
+ * in the min_alarm case.
+ */
+ return ltc4283_read_alarm(st, LTC4283_ADC_ALM_LOG_2,
+ BIT(7 - bit - !max_alm), val);
+ }
+
+ if (channel >= LTC4283_CHAN_ADIO_1 && channel <= LTC4283_CHAN_ADIO_4) {
+ u32 bit = (channel - LTC4283_CHAN_ADIO_1) * 2;
+
+ return ltc4283_read_alarm(st, LTC4283_ADC_ALM_LOG_3,
+ BIT(7 - bit - !max_alm), val);
+ }
+
+ if (channel >= LTC4283_CHAN_ADIN12 && channel <= LTC4283_CHAN_ADIN34) {
+ u32 bit = (channel - LTC4283_CHAN_ADIN12) * 2;
+
+ return ltc4283_read_alarm(st, LTC4283_ADC_ALM_LOG_5,
+ BIT(7 - bit - !max_alm), val);
+ }
+
+ if (channel == LTC4283_CHAN_DRNS)
+ return ltc4283_read_alarm(st, LTC4283_ADC_ALM_LOG_4,
+ BIT(6 + max_alm), val);
+
+ return ltc4283_read_alarm(st, LTC4283_ADC_ALM_LOG_4, BIT(4 + max_alm),
+ val);
+}
+
+static int ltc4283_read_in(struct ltc4283_hwmon *st, u32 attr, u32 channel,
+ long *val)
+{
+ switch (attr) {
+ case hwmon_in_input:
+ if (!test_bit(channel, &st->ch_enable_mask))
+ return -ENODATA;
+
+ return ltc4283_read_in_vals(st, attr, channel, val);
+ case hwmon_in_highest:
+ case hwmon_in_lowest:
+ case hwmon_in_max:
+ case hwmon_in_min:
+ return ltc4283_read_in_vals(st, attr, channel, val);
+ case hwmon_in_max_alarm:
+ return ltc4283_read_in_alarm(st, channel, true, val);
+ case hwmon_in_min_alarm:
+ return ltc4283_read_in_alarm(st, channel, false, val);
+ case hwmon_in_crit_alarm:
+ return ltc4283_read_alarm(st, LTC4283_FAULT_STATUS,
+ LTC4283_OV_MASK, val);
+ case hwmon_in_lcrit_alarm:
+ return ltc4283_read_alarm(st, LTC4283_FAULT_STATUS,
+ LTC4283_UV_MASK, val);
+ case hwmon_in_fault:
+ /*
+ * We report failure if we detect either a fer_bad or a
+ * fet_short in the status register.
+ */
+ return ltc4283_read_alarm(st, LTC4283_FAULT_STATUS,
+ LTC4283_FET_BAD_MASK | LTC4283_FET_SHORT_MASK, val);
+ case hwmon_in_enable:
+ *val = test_bit(channel, &st->ch_enable_mask);
+ return 0;
+ default:
+ return -EOPNOTSUPP;
+ }
+ return 0;
+}
+
+static int ltc4283_read_current_word(const struct ltc4283_hwmon *st, u32 reg,
+ long *val)
+{
+ u64 temp = (u64)LTC4283_ADC1_FS_uV * DECA * MILLI;
+ unsigned int __raw;
+ int ret;
+
+ ret = regmap_read(st->map, reg, &__raw);
+ if (ret)
+ return ret;
+
+ *val = DIV64_U64_ROUND_CLOSEST(__raw * temp,
+ BIT_ULL(16) * st->rsense);
+
+ return 0;
+}
+
+static int ltc4283_read_current_byte(const struct ltc4283_hwmon *st, u32 reg,
+ long *val)
+{
+ u64 temp = (u64)LTC4283_ADC1_FS_uV * DECA * MILLI;
+ u32 curr;
+ int ret;
+
+ ret = regmap_read(st->map, reg, &curr);
+ if (ret)
+ return ret;
+
+ *val = DIV_ROUND_CLOSEST_ULL(curr * temp, BIT(8) * st->rsense);
+ return 0;
+}
+
+static int ltc4283_read_curr(struct ltc4283_hwmon *st, u32 attr, long *val)
+{
+ switch (attr) {
+ case hwmon_curr_input:
+ return ltc4283_read_current_word(st, LTC4283_SENSE, val);
+ case hwmon_curr_highest:
+ return ltc4283_read_current_word(st, LTC4283_SENSE_MAX, val);
+ case hwmon_curr_lowest:
+ return ltc4283_read_current_word(st, LTC4283_SENSE_MIN, val);
+ case hwmon_curr_max:
+ return ltc4283_read_current_byte(st, LTC4283_SENSE_MAX_TH, val);
+ case hwmon_curr_min:
+ return ltc4283_read_current_byte(st, LTC4283_SENSE_MIN_TH, val);
+ case hwmon_curr_max_alarm:
+ return ltc4283_read_alarm(st, LTC4283_ADC_ALM_LOG_1,
+ LTC4283_SENSE_HIGH_ALM, val);
+ case hwmon_curr_min_alarm:
+ return ltc4283_read_alarm(st, LTC4283_ADC_ALM_LOG_1,
+ LTC4283_SENSE_LOW_ALM, val);
+ case hwmon_curr_crit_alarm:
+ return ltc4283_read_alarm(st, LTC4283_FAULT_STATUS,
+ LTC4283_OC_MASK, val);
+ default:
+ return -EOPNOTSUPP;
+ }
+}
+
+static int ltc4283_read_power_word(const struct ltc4283_hwmon *st,
+ u32 reg, long *val)
+{
+ u64 temp = (u64)LTC4283_ADC1_FS_uV * LTC4283_ADC2_FS_mV * DECA * MILLI;
+ unsigned int __raw;
+ int ret;
+
+ ret = regmap_read(st->map, reg, &__raw);
+ if (ret)
+ return ret;
+
+ /*
+ * Power is given by:
+ * P = CODE(16b) * 32.768mV * 2.048V / (2^16 * Rsense)
+ */
+ *val = DIV64_U64_ROUND_CLOSEST(temp * __raw, BIT_ULL(16) * st->rsense);
+
+ return 0;
+}
+
+static int ltc4283_read_power_byte(const struct ltc4283_hwmon *st,
+ u32 reg, long *val)
+{
+ u64 temp = (u64)LTC4283_ADC1_FS_uV * LTC4283_ADC2_FS_mV * DECA * MILLI;
+ u32 power;
+ int ret;
+
+ ret = regmap_read(st->map, reg, &power);
+ if (ret)
+ return ret;
+
+ *val = DIV_ROUND_CLOSEST_ULL(power * temp, BIT(8) * st->rsense);
+
+ return 0;
+}
+
+static int ltc4283_read_power(struct ltc4283_hwmon *st, u32 attr, long *val)
+{
+ switch (attr) {
+ case hwmon_power_input:
+ return ltc4283_read_power_word(st, LTC4283_POWER, val);
+ case hwmon_power_input_highest:
+ return ltc4283_read_power_word(st, LTC4283_POWER_MAX, val);
+ case hwmon_power_input_lowest:
+ return ltc4283_read_power_word(st, LTC4283_POWER_MIN, val);
+ case hwmon_power_max_alarm:
+ return ltc4283_read_alarm(st, LTC4283_ADC_ALM_LOG_1,
+ LTC4283_POWER_HIGH_ALM, val);
+ case hwmon_power_min_alarm:
+ return ltc4283_read_alarm(st, LTC4283_ADC_ALM_LOG_1,
+ LTC4283_POWER_LOW_ALM, val);
+ case hwmon_power_max:
+ return ltc4283_read_power_byte(st, LTC4283_POWER_MAX_TH, val);
+ case hwmon_power_min:
+ return ltc4283_read_power_byte(st, LTC4283_POWER_MIN_TH, val);
+ default:
+ return -EOPNOTSUPP;
+ }
+}
+
+static int ltc4283_read_energy(struct ltc4283_hwmon *st, u32 attr, s64 *val)
+{
+ u64 temp = LTC4283_ADC1_FS_uV * LTC4283_ADC2_FS_mV, energy, temp_2;
+ u8 raw[8] = {};
+ int ret;
+
+ if (!st->energy_en)
+ return -ENODATA;
+
+ ret = i2c_smbus_read_i2c_block_data(st->client, LTC4283_ENERGY, 6, raw);
+ if (ret < 0)
+ return ret;
+ if (ret != 6)
+ return -EIO;
+
+ energy = get_unaligned_be64(raw) >> 16;
+
+ /*
+ * The formula for energy is given by:
+ * E = CODE(48b) * 32.768mV * 2.048V * Tconv / 2^24 * Rsense
+ *
+ * As Rsense can have tenths of micro-ohm resolution, we need to
+ * multiply by DECA to get microjoule.
+ */
+ if (check_mul_overflow(temp * LTC4283_TCONV_uS, energy, &temp_2)) {
+ /*
+ * We multiply again by 1000 to make sure that we don't get 0
+ * in the following division which could happen for big rsense
+ * values. OTOH, we then divide energy first by 1000 so that
+ * we do not overflow u64 again for very small rsense values.
+ * We add 100 factor for proper conversion to microjoule.
+ */
+ temp_2 = DIV64_U64_ROUND_CLOSEST(temp * LTC4283_TCONV_uS * MILLI,
+ BIT_ULL(24) * st->rsense);
+ energy = DIV_ROUND_CLOSEST_ULL(energy, MILLI * CENTI) * temp_2;
+ } else {
+ /* Put rsense back into nanoohm so we get microjoule. */
+ energy = DIV64_U64_ROUND_CLOSEST(temp_2, BIT_ULL(24) * st->rsense * CENTI);
+ }
+
+ *val = energy;
+ return 0;
+}
+
+static int ltc4283_read(struct device *dev, enum hwmon_sensor_types type,
+ u32 attr, int channel, long *val)
+{
+ struct ltc4283_hwmon *st = dev_get_drvdata(dev);
+
+ switch (type) {
+ case hwmon_in:
+ return ltc4283_read_in(st, attr, channel, val);
+ case hwmon_curr:
+ return ltc4283_read_curr(st, attr, val);
+ case hwmon_power:
+ return ltc4283_read_power(st, attr, val);
+ case hwmon_energy:
+ *val = st->energy_en;
+ return 0;
+ case hwmon_energy64:
+ return ltc4283_read_energy(st, attr, (s64 *)val);
+ default:
+ return -EOPNOTSUPP;
+ }
+}
+
+static int ltc4283_write_power_byte(const struct ltc4283_hwmon *st, u32 reg,
+ long val)
+{
+ u64 temp = (u64)LTC4283_ADC1_FS_uV * LTC4283_ADC2_FS_mV * DECA * MILLI;
+ u32 __raw;
+
+ val = clamp_val(val, 0, st->power_max);
+ __raw = DIV64_U64_ROUND_CLOSEST(val * BIT_ULL(8) * st->rsense, temp);
+
+ return regmap_write(st->map, reg, __raw);
+}
+
+static int ltc4283_write_power_word(const struct ltc4283_hwmon *st,
+ u32 reg, long val)
+{
+ u64 temp = st->rsense * BIT_ULL(16), temp_2;
+ u16 __raw;
+
+ if (check_mul_overflow(val, temp, &temp_2)) {
+ temp = DIV_ROUND_CLOSEST_ULL(temp, DECA * MILLI);
+ __raw = DIV_ROUND_CLOSEST_ULL(temp * val, LTC4283_ADC1_FS_uV * LTC4283_ADC2_FS_mV);
+ } else {
+ temp = (u64)LTC4283_ADC1_FS_uV * LTC4283_ADC2_FS_mV * DECA * MILLI;
+ __raw = DIV64_U64_ROUND_CLOSEST(temp_2, temp);
+ }
+
+ return regmap_write(st->map, reg, __raw);
+}
+
+static int ltc4283_reset_power_hist(struct ltc4283_hwmon *st)
+{
+ int ret;
+
+ ret = ltc4283_write_power_word(st, LTC4283_POWER_MIN, st->power_max);
+ if (ret)
+ return ret;
+
+ ret = ltc4283_write_power_word(st, LTC4283_POWER_MAX, 0);
+ if (ret)
+ return ret;
+
+ /* Clear possible power faults. */
+ return regmap_clear_bits(st->map, LTC4283_FAULT_LOG,
+ LTC4283_PWR_FAIL_FAULT_MASK | LTC4283_PGI_FAULT_MASK);
+}
+
+static int ltc4283_write_power(struct ltc4283_hwmon *st, u32 attr, long val)
+{
+ switch (attr) {
+ case hwmon_power_max:
+ return ltc4283_write_power_byte(st, LTC4283_POWER_MAX_TH, val);
+ case hwmon_power_min:
+ return ltc4283_write_power_byte(st, LTC4283_POWER_MIN_TH, val);
+ case hwmon_power_reset_history:
+ return ltc4283_reset_power_hist(st);
+ default:
+ return -EOPNOTSUPP;
+ }
+}
+
+static int ltc4283_write_in_history(struct ltc4283_hwmon *st, u32 reg,
+ long lowest, u32 fs)
+{
+ u32 __raw;
+ int ret;
+
+ __raw = DIV_ROUND_CLOSEST(BIT(16) * lowest, fs);
+ if (__raw == BIT(16))
+ __raw = U16_MAX;
+
+ ret = regmap_write(st->map, reg, __raw);
+ if (ret)
+ return ret;
+
+ return regmap_write(st->map, reg + 1, 0);
+}
+
+static int ltc4283_write_in_byte(const struct ltc4283_hwmon *st,
+ u32 reg, u32 fs, long val)
+{
+ u32 __raw;
+
+ val = clamp_val(val, 0, fs);
+ __raw = DIV_ROUND_CLOSEST(val * BIT(8), fs);
+ if (__raw == BIT(8))
+ __raw = U8_MAX;
+
+ return regmap_write(st->map, reg, __raw);
+}
+
+static int ltc4283_reset_in_hist(struct ltc4283_hwmon *st, u32 channel)
+{
+ u32 reg, fs;
+ int ret;
+
+ /*
+ * Make sure to clear possible under/over voltage faults. Otherwise the
+ * chip won't latch on again.
+ */
+ if (channel == LTC4283_CHAN_VIN)
+ return regmap_clear_bits(st->map, LTC4283_FAULT_LOG,
+ LTC4283_OV_FAULT_MASK | LTC4283_UV_FAULT_MASK);
+
+ if (channel == LTC4283_CHAN_VPWR)
+ return ltc4283_write_in_history(st, LTC4283_VPWR_MIN,
+ LTC4283_ADC2_FS_mV,
+ LTC4283_ADC2_FS_mV);
+
+ if (channel >= LTC4283_CHAN_ADI_1 && channel <= LTC4283_CHAN_DRAIN) {
+ fs = LTC4283_ADC2_FS_mV;
+ reg = LTC4283_ADC_2_MIN(channel - LTC4283_CHAN_ADI_1);
+ } else {
+ fs = LTC4283_ADC1_FS_uV;
+ reg = LTC4283_ADC_2_MIN_DIFF(channel - LTC4283_CHAN_ADIN12);
+ }
+
+ ret = ltc4283_write_in_history(st, reg, fs, fs);
+ if (ret)
+ return ret;
+ if (channel != LTC4283_CHAN_DRAIN)
+ return 0;
+
+ /* Then, let's also clear possible fet faults. Same as above. */
+ return regmap_clear_bits(st->map, LTC4283_FAULT_LOG,
+ LTC4283_FET_BAD_FAULT_MASK | LTC4283_FET_SHORT_FAULT_MASK);
+}
+
+static int ltc4283_write_in_en(struct ltc4283_hwmon *st, u32 channel, bool en)
+{
+ unsigned int bit, adc_idx = channel - LTC4283_CHAN_ADI_1;
+ unsigned int reg = LTC4283_ADC_SELECT(adc_idx);
+ int ret;
+
+ bit = LTC4283_ADC_SELECT_MASK(adc_idx);
+ if (channel > LTC4283_CHAN_DRAIN)
+ /* Account for two reserved fields after DRAIN. */
+ bit <<= 2;
+
+ if (en)
+ ret = regmap_set_bits(st->map, reg, bit);
+ else
+ ret = regmap_clear_bits(st->map, reg, bit);
+ if (ret)
+ return ret;
+
+ __assign_bit(channel, &st->ch_enable_mask, en);
+ return 0;
+}
+
+static int ltc4283_write_minmax(struct ltc4283_hwmon *st, long val,
+ u32 channel, bool is_max)
+{
+ u32 reg;
+
+ if (channel == LTC4283_CHAN_VPWR) {
+ if (is_max)
+ return ltc4283_write_in_byte(st, LTC4283_VPWR_MAX_TH,
+ LTC4283_ADC2_FS_mV, val);
+
+ return ltc4283_write_in_byte(st, LTC4283_VPWR_MIN_TH,
+ LTC4283_ADC2_FS_mV, val);
+ }
+
+ if (channel >= LTC4283_CHAN_ADI_1 && channel <= LTC4283_CHAN_DRAIN) {
+ if (is_max) {
+ reg = LTC4283_ADC_2_MAX_TH(channel - LTC4283_CHAN_ADI_1);
+ return ltc4283_write_in_byte(st, reg,
+ LTC4283_ADC2_FS_mV, val);
+ }
+
+ reg = LTC4283_ADC_2_MIN_TH(channel - LTC4283_CHAN_ADI_1);
+ return ltc4283_write_in_byte(st, reg, LTC4283_ADC2_FS_mV, val);
+ }
+
+ if (is_max) {
+ reg = LTC4283_ADC_2_MAX_TH_DIFF(channel - LTC4283_CHAN_ADIN12);
+ return ltc4283_write_in_byte(st, reg, LTC4283_ADC1_FS_uV,
+ val * MILLI);
+ }
+
+ reg = LTC4283_ADC_2_MIN_TH_DIFF(channel - LTC4283_CHAN_ADIN12);
+ return ltc4283_write_in_byte(st, reg, LTC4283_ADC1_FS_uV, val * MILLI);
+}
+
+static int ltc4283_write_in(struct ltc4283_hwmon *st, u32 attr, long val,
+ int channel)
+{
+ switch (attr) {
+ case hwmon_in_max:
+ return ltc4283_write_minmax(st, val, channel, true);
+ case hwmon_in_min:
+ return ltc4283_write_minmax(st, val, channel, false);
+ case hwmon_in_reset_history:
+ return ltc4283_reset_in_hist(st, channel);
+ case hwmon_in_enable:
+ return ltc4283_write_in_en(st, channel, !!val);
+ default:
+ return -EOPNOTSUPP;
+ }
+}
+
+static int ltc4283_write_curr_byte(const struct ltc4283_hwmon *st,
+ u32 reg, long val)
+{
+ u32 temp = LTC4283_ADC1_FS_uV * DECA * MILLI;
+ u32 reg_val, isense_max;
+
+ isense_max = DIV_ROUND_CLOSEST(st->vsense_max * MICRO * DECA, st->rsense);
+ val = clamp_val(val, 0, isense_max);
+ reg_val = DIV_ROUND_CLOSEST_ULL(val * BIT_ULL(8) * st->rsense, temp);
+
+ return regmap_write(st->map, reg, reg_val);
+}
+
+static int ltc4283_write_curr_history(struct ltc4283_hwmon *st)
+{
+ int ret;
+
+ ret = ltc4283_write_in_history(st, LTC4283_SENSE_MIN,
+ st->vsense_max * MILLI,
+ LTC4283_ADC1_FS_uV);
+ if (ret)
+ return ret;
+
+ /* Now, let's also clear possible overcurrent logs. */
+ return regmap_clear_bits(st->map, LTC4283_FAULT_LOG,
+ LTC4283_OC_FAULT_MASK);
+}
+
+static int ltc4283_write_curr(struct ltc4283_hwmon *st, u32 attr, long val)
+{
+ switch (attr) {
+ case hwmon_curr_max:
+ return ltc4283_write_curr_byte(st, LTC4283_SENSE_MAX_TH, val);
+ case hwmon_curr_min:
+ return ltc4283_write_curr_byte(st, LTC4283_SENSE_MIN_TH, val);
+ case hwmon_curr_reset_history:
+ return ltc4283_write_curr_history(st);
+ default:
+ return -EOPNOTSUPP;
+ }
+}
+
+static int ltc4283_energy_enable_set(struct ltc4283_hwmon *st, long val)
+{
+ int ret;
+
+ /* Setting the bit halts the meter. */
+ val = !!val;
+ ret = regmap_update_bits(st->map, LTC4283_METER_CONTROL,
+ LTC4283_METER_HALT_MASK,
+ FIELD_PREP(LTC4283_METER_HALT_MASK, !val));
+ if (ret)
+ return ret;
+
+ st->energy_en = val;
+
+ return 0;
+}
+
+static int ltc4283_write(struct device *dev, enum hwmon_sensor_types type,
+ u32 attr, int channel, long val)
+{
+ struct ltc4283_hwmon *st = dev_get_drvdata(dev);
+
+ switch (type) {
+ case hwmon_power:
+ return ltc4283_write_power(st, attr, val);
+ case hwmon_in:
+ return ltc4283_write_in(st, attr, val, channel);
+ case hwmon_curr:
+ return ltc4283_write_curr(st, attr, val);
+ case hwmon_energy:
+ return ltc4283_energy_enable_set(st, val);
+ default:
+ return -EOPNOTSUPP;
+ }
+}
+
+static umode_t ltc4283_in_is_visible(const struct ltc4283_hwmon *st,
+ u32 attr, int channel)
+{
+ /* If ADIO is set as a GPIO, don´t make it visible. */
+ if (channel >= LTC4283_CHAN_ADIO_1 && channel <= LTC4283_CHAN_ADIO_4) {
+ /* ADIOX pins come at index 0 in the gpio mask. */
+ channel -= LTC4283_CHAN_ADIO_1;
+ if (test_bit(channel, &st->gpio_mask))
+ return 0;
+ }
+
+ /* Also take care of differential channels. */
+ if (channel >= LTC4283_CHAN_ADIO12 && channel <= LTC4283_CHAN_ADIO34) {
+ channel -= LTC4283_CHAN_ADIO12;
+ /* If one channel in the pair is used, make it invisible. */
+ if (test_bit(channel * 2, &st->gpio_mask) ||
+ test_bit(channel * 2 + 1, &st->gpio_mask))
+ return 0;
+ }
+
+ switch (attr) {
+ case hwmon_in_input:
+ case hwmon_in_highest:
+ case hwmon_in_lowest:
+ case hwmon_in_max_alarm:
+ case hwmon_in_min_alarm:
+ case hwmon_in_label:
+ case hwmon_in_lcrit_alarm:
+ case hwmon_in_crit_alarm:
+ case hwmon_in_fault:
+ return 0444;
+ case hwmon_in_max:
+ case hwmon_in_min:
+ case hwmon_in_enable:
+ return 0644;
+ case hwmon_in_reset_history:
+ return 0200;
+ default:
+ return 0;
+ }
+}
+
+static umode_t ltc4283_curr_is_visible(u32 attr)
+{
+ switch (attr) {
+ case hwmon_curr_input:
+ case hwmon_curr_highest:
+ case hwmon_curr_lowest:
+ case hwmon_curr_max_alarm:
+ case hwmon_curr_min_alarm:
+ case hwmon_curr_crit_alarm:
+ case hwmon_curr_label:
+ return 0444;
+ case hwmon_curr_max:
+ case hwmon_curr_min:
+ return 0644;
+ case hwmon_curr_reset_history:
+ return 0200;
+ default:
+ return 0;
+ }
+}
+
+static umode_t ltc4283_power_is_visible(u32 attr)
+{
+ switch (attr) {
+ case hwmon_power_input:
+ case hwmon_power_input_highest:
+ case hwmon_power_input_lowest:
+ case hwmon_power_label:
+ case hwmon_power_max_alarm:
+ case hwmon_power_min_alarm:
+ return 0444;
+ case hwmon_power_max:
+ case hwmon_power_min:
+ return 0644;
+ case hwmon_power_reset_history:
+ return 0200;
+ default:
+ return 0;
+ }
+}
+
+static umode_t ltc4283_is_visible(const void *data,
+ enum hwmon_sensor_types type,
+ u32 attr, int channel)
+{
+ switch (type) {
+ case hwmon_in:
+ return ltc4283_in_is_visible(data, attr, channel);
+ case hwmon_curr:
+ return ltc4283_curr_is_visible(attr);
+ case hwmon_power:
+ return ltc4283_power_is_visible(attr);
+ case hwmon_energy:
+ /* hwmon_energy_enable */
+ return 0644;
+ case hwmon_energy64:
+ /* hwmon_energy_input */
+ return 0444;
+ default:
+ return 0;
+ }
+}
+
+static const char * const ltc4283_in_strs[] = {
+ "VIN", "VPWR", "VADI1", "VADI2", "VADI3", "VADI4", "VADIO1", "VADIO2",
+ "VADIO3", "VADIO4", "DRNS", "DRAIN", "ADIN2-ADIN1", "ADIN4-ADIN3",
+ "ADIO2-ADIO1", "ADIO4-ADIO3"
+};
+
+static int ltc4283_read_labels(struct device *dev,
+ enum hwmon_sensor_types type,
+ u32 attr, int channel, const char **str)
+{
+ switch (type) {
+ case hwmon_in:
+ *str = ltc4283_in_strs[channel];
+ return 0;
+ case hwmon_curr:
+ *str = "ISENSE";
+ return 0;
+ case hwmon_power:
+ *str = "Power";
+ return 0;
+ default:
+ return -EOPNOTSUPP;
+ }
+}
+
+/*
+ * Set max limits for ISENSE and Power as that depends on the max voltage on
+ * rsense that is defined in ILIM_ADJUST. This is specially important for power
+ * because for some rsense and vfsout values, if we allow the default raw 255
+ * value, that would overflow long in 32bit archs when reading back the max
+ * power limit.
+ */
+static int ltc4283_set_max_limits(struct ltc4283_hwmon *st, struct device *dev)
+{
+ u32 temp = st->vsense_max * DECA * MICRO;
+ int ret;
+
+ ret = ltc4283_write_in_byte(st, LTC4283_SENSE_MAX_TH, LTC4283_ADC1_FS_uV,
+ st->vsense_max * MILLI);
+ if (ret)
+ return ret;
+
+ /* Power is given by ISENSE * Vout. */
+ st->power_max = DIV_ROUND_CLOSEST(temp, st->rsense) * LTC4283_ADC2_FS_mV;
+ return ltc4283_write_power_byte(st, LTC4283_POWER_MAX_TH, st->power_max);
+}
+
+static int ltc4283_parse_array_prop(const struct ltc4283_hwmon *st,
+ struct device *dev, const char *prop,
+ const u32 *vals, u32 n_vals)
+{
+ u32 prop_val;
+ int ret;
+ u32 i;
+
+ ret = device_property_read_u32(dev, prop, &prop_val);
+ if (ret)
+ return n_vals;
+
+ for (i = 0; i < n_vals; i++) {
+ if (prop_val != vals[i])
+ continue;
+
+ return i;
+ }
+
+ return dev_err_probe(dev, -EINVAL,
+ "Invalid %s property value %u, expected one of: %*ph\n",
+ prop, prop_val, n_vals, vals);
+}
+
+static int ltc4283_get_defaults(struct ltc4283_hwmon *st)
+{
+ u32 reg_val, ilm_adjust, c;
+ int ret;
+
+ ret = regmap_read(st->map, LTC4283_METER_CONTROL, ®_val);
+ if (ret)
+ return ret;
+
+ st->energy_en = !FIELD_GET(LTC4283_METER_HALT_MASK, reg_val);
+
+ ret = regmap_read(st->map, LTC4283_CONFIG_1, ®_val);
+ if (ret)
+ return ret;
+
+ ilm_adjust = FIELD_GET(LTC4283_ILIM_MASK, reg_val);
+ st->vsense_max = LTC4283_VILIM_MIN_uV / MILLI + ilm_adjust;
+
+ ret = regmap_read(st->map, LTC4283_PGIO_CONFIG, ®_val);
+ if (ret)
+ return ret;
+
+ /* Can be latter overwritten in ltc4283_pgio_config() */
+ if (FIELD_GET(LTC4283_PGIO4_CFG_MASK, reg_val) < LTC4283_PGIO_FUNC_GPIO)
+ st->ext_fault = true;
+
+ /* VPWR and VIN are always enabled */
+ __set_bit(LTC4283_CHAN_VIN, &st->ch_enable_mask);
+ __set_bit(LTC4283_CHAN_VPWR, &st->ch_enable_mask);
+ for (c = LTC4283_CHAN_ADI_1; c < LTC4283_CHAN_MAX; c++) {
+ u32 chan = c - LTC4283_CHAN_ADI_1, bit;
+
+ ret = regmap_read(st->map, LTC4283_ADC_SELECT(chan), ®_val);
+ if (ret)
+ return ret;
+
+ bit = LTC4283_ADC_SELECT_MASK(chan);
+ if (c > LTC4283_CHAN_DRAIN)
+ /* account for two reserved fields after DRAIN */
+ bit <<= 2;
+
+ if (!(bit & reg_val))
+ continue;
+
+ __set_bit(c, &st->ch_enable_mask);
+ }
+
+ return 0;
+}
+
+static const char * const ltc4283_pgio1_funcs[] = {
+ "inverted_power_good", "power_good", "gpio"
+};
+
+static const char * const ltc4283_pgio2_funcs[] = {
+ "inverted_power_good", "power_good", "gpio", "active_current_limiting"
+};
+
+static const char * const ltc4283_pgio3_funcs[] = {
+ "inverted_power_good_input", "power_good_input", "gpio"
+};
+
+static const char * const ltc4283_pgio4_funcs[] = {
+ "inverted_external_fault", "external_fault", "gpio"
+};
+
+enum {
+ LTC4283_PIN_ADIO1,
+ LTC4283_PIN_ADIO2,
+ LTC4283_PIN_ADIO3,
+ LTC4283_PIN_ADIO4,
+ LTC4283_PIN_PGIO1,
+ LTC4283_PIN_PGIO2,
+ LTC4283_PIN_PGIO3,
+ LTC4283_PIN_PGIO4,
+};
+
+static int ltc4283_pgio_config(struct ltc4283_hwmon *st, struct device *dev)
+{
+ int ret, func;
+
+ func = device_property_match_property_string(dev, "adi,pgio1-func",
+ ltc4283_pgio1_funcs,
+ ARRAY_SIZE(ltc4283_pgio1_funcs));
+ if (func < 0 && func != -EINVAL)
+ return dev_err_probe(dev, func,
+ "Invalid adi,pgio1-func property\n");
+ if (func >= 0) {
+ if (func == LTC4283_PGIO_FUNC_GPIO) {
+ __set_bit(LTC4283_PIN_PGIO1, &st->gpio_mask);
+ /* If GPIO, default to an input pin. */
+ func++;
+ }
+
+ ret = regmap_update_bits(st->map, LTC4283_PGIO_CONFIG,
+ LTC4283_PGIO1_CFG_MASK,
+ FIELD_PREP(LTC4283_PGIO1_CFG_MASK, func));
+ if (ret)
+ return ret;
+ }
+
+ func = device_property_match_property_string(dev, "adi,pgio2-func",
+ ltc4283_pgio2_funcs,
+ ARRAY_SIZE(ltc4283_pgio2_funcs));
+
+ if (func < 0 && func != -EINVAL)
+ return dev_err_probe(dev, func,
+ "Invalid adi,pgio2-func property\n");
+ if (func >= 0) {
+ if (func != LTC4283_PGIO2_FUNC_ACLB) {
+ if (func == LTC4283_PGIO_FUNC_GPIO) {
+ __set_bit(LTC4283_PIN_PGIO2, &st->gpio_mask);
+ func++;
+ }
+
+ ret = regmap_update_bits(st->map, LTC4283_PGIO_CONFIG,
+ LTC4283_PGIO2_CFG_MASK,
+ FIELD_PREP(LTC4283_PGIO2_CFG_MASK, func));
+ } else {
+ ret = regmap_set_bits(st->map, LTC4283_CONTROL_1,
+ LTC4283_PIGIO2_ACLB_MASK);
+ }
+
+ if (ret)
+ return ret;
+ }
+
+ func = device_property_match_property_string(dev, "adi,pgio3-func",
+ ltc4283_pgio3_funcs,
+ ARRAY_SIZE(ltc4283_pgio3_funcs));
+
+ if (func < 0 && func != -EINVAL)
+ return dev_err_probe(dev, func,
+ "Invalid adi,pgio3-func property\n");
+ if (func >= 0) {
+ if (func == LTC4283_PGIO_FUNC_GPIO) {
+ __set_bit(LTC4283_PIN_PGIO3, &st->gpio_mask);
+ func++;
+ }
+
+ ret = regmap_update_bits(st->map, LTC4283_PGIO_CONFIG,
+ LTC4283_PGIO3_CFG_MASK,
+ FIELD_PREP(LTC4283_PGIO3_CFG_MASK, func));
+ if (ret)
+ return ret;
+ }
+
+ func = device_property_match_property_string(dev, "adi,pgio4-func",
+ ltc4283_pgio4_funcs,
+ ARRAY_SIZE(ltc4283_pgio4_funcs));
+
+ if (func < 0 && func != -EINVAL)
+ return dev_err_probe(dev, func,
+ "Invalid adi,pgio4-func property\n");
+ if (func >= 0) {
+ if (func == LTC4283_PGIO_FUNC_GPIO) {
+ __set_bit(LTC4283_PIN_PGIO4, &st->gpio_mask);
+ func++;
+ st->ext_fault = false;
+ } else {
+ st->ext_fault = true;
+ }
+
+ ret = regmap_update_bits(st->map, LTC4283_PGIO_CONFIG,
+ LTC4283_PGIO4_CFG_MASK,
+ FIELD_PREP(LTC4283_PGIO4_CFG_MASK, func));
+ if (ret)
+ return ret;
+ }
+
+ return 0;
+}
+
+static int ltc4283_adio_config(struct ltc4283_hwmon *st, struct device *dev,
+ const char *prop, u32 pin)
+{
+ u32 adc_idx;
+ int ret;
+
+ if (!device_property_read_bool(dev, prop))
+ return 0;
+
+ adc_idx = LTC4283_CHAN_ADIO_1 - LTC4283_CHAN_ADI_1 + pin;
+ ret = regmap_clear_bits(st->map, LTC4283_ADC_SELECT(adc_idx),
+ LTC4283_ADC_SELECT_MASK(adc_idx));
+ if (ret)
+ return ret;
+
+ __set_bit(pin, &st->gpio_mask);
+ return 0;
+}
+
+static int ltc4283_pin_config(struct ltc4283_hwmon *st, struct device *dev)
+{
+ int ret;
+
+ ret = ltc4283_pgio_config(st, dev);
+ if (ret)
+ return ret;
+
+ ret = ltc4283_adio_config(st, dev, "adi,gpio-on-adio1", LTC4283_PIN_ADIO1);
+ if (ret)
+ return ret;
+
+ ret = ltc4283_adio_config(st, dev, "adi,gpio-on-adio2", LTC4283_PIN_ADIO2);
+ if (ret)
+ return ret;
+
+ ret = ltc4283_adio_config(st, dev, "adi,gpio-on-adio3", LTC4283_PIN_ADIO3);
+ if (ret)
+ return ret;
+
+ return ltc4283_adio_config(st, dev, "adi,gpio-on-adio4", LTC4283_PIN_ADIO4);
+}
+
+static const char * const ltc4283_oc_fet_retry[] = {
+ "latch-off", "1", "7", "unlimited"
+};
+
+static const u32 ltc4283_fb_factor[] = {
+ 100, 50, 20, 10
+};
+
+static const u32 ltc4283_cooling_dl[] = {
+ 512, 1002, 2005, 4100, 8190, 16400, 32800, 65600
+};
+
+static const u32 ltc4283_fet_bad_delay[] = {
+ 256, 512, 1002, 2005
+};
+
+static int ltc4283_setup(struct ltc4283_hwmon *st, struct device *dev)
+{
+ u32 val;
+ int ret;
+
+ /* The part has an eeprom so let's get the needed defaults from it */
+ ret = ltc4283_get_defaults(st);
+ if (ret)
+ return ret;
+
+ /*
+ * Default to 1 micro ohm so we can probe without FW properties. Note
+ * the below division expects rsense in nano ohms.
+ */
+ st->rsense = 1 * MILLI;
+ ret = device_property_read_u32(dev, "adi,rsense-nano-ohms",
+ &st->rsense);
+ if (!ret) {
+ if (st->rsense < CENTI)
+ return dev_err_probe(dev, -EINVAL,
+ "adi,rsense-nano-ohms too small (< %lu)\n",
+ CENTI);
+ }
+
+ /*
+ * The resolution for rsense is tenths of micro (eg: 62.5 uOhm) which
+ * means we need nano in the bindings. However, to make things easier to
+ * handle (with respect to overflows) we divide it by 100 as we don't
+ * really need the last two digits.
+ */
+ st->rsense /= CENTI;
+
+ ret = device_property_read_u32(dev, "adi,current-limit-sense-microvolt",
+ &st->vsense_max);
+ if (!ret) {
+ u32 reg_val;
+
+ if (!in_range(st->vsense_max, LTC4283_VILIM_MIN_uV,
+ LTC4283_VILIM_RANGE)) {
+ return dev_err_probe(dev, -EINVAL,
+ "adi,current-limit-sense-microvolt (%u) out of range [%u %u]\n",
+ st->vsense_max, LTC4283_VILIM_MIN_uV,
+ LTC4283_VILIM_MAX_uV);
+ }
+
+ st->vsense_max /= MILLI;
+ reg_val = FIELD_PREP(LTC4283_ILIM_MASK,
+ st->vsense_max - LTC4283_VILIM_MIN_uV / MILLI);
+ ret = regmap_update_bits(st->map, LTC4283_CONFIG_1,
+ LTC4283_ILIM_MASK, reg_val);
+ if (ret)
+ return ret;
+ }
+
+ ret = ltc4283_parse_array_prop(st, dev, "adi,current-limit-foldback-factor",
+ ltc4283_fb_factor, ARRAY_SIZE(ltc4283_fb_factor));
+ if (ret < 0)
+ return ret;
+ if (ret < ARRAY_SIZE(ltc4283_fb_factor)) {
+ ret = regmap_update_bits(st->map, LTC4283_CONFIG_1, LTC4283_FB_MASK,
+ FIELD_PREP(LTC4283_FB_MASK, ret));
+ if (ret)
+ return ret;
+ }
+
+ ret = ltc4283_parse_array_prop(st, dev, "adi,cooling-delay-ms",
+ ltc4283_cooling_dl, ARRAY_SIZE(ltc4283_cooling_dl));
+ if (ret < 0)
+ return ret;
+ if (ret < ARRAY_SIZE(ltc4283_cooling_dl)) {
+ ret = regmap_update_bits(st->map, LTC4283_CONFIG_2, LTC4283_COOLING_DL_MASK,
+ FIELD_PREP(LTC4283_COOLING_DL_MASK, ret));
+ if (ret)
+ return ret;
+ }
+
+ ret = ltc4283_parse_array_prop(st, dev, "adi,fet-bad-timer-delay-ms",
+ ltc4283_fet_bad_delay, ARRAY_SIZE(ltc4283_fet_bad_delay));
+ if (ret < 0)
+ return ret;
+ if (ret < ARRAY_SIZE(ltc4283_fet_bad_delay)) {
+ ret = regmap_update_bits(st->map, LTC4283_CONFIG_2, LTC4283_FTBD_DL_MASK,
+ FIELD_PREP(LTC4283_FTBD_DL_MASK, ret));
+ if (ret)
+ return ret;
+ }
+
+ ret = ltc4283_set_max_limits(st, dev);
+ if (ret)
+ return ret;
+
+ ret = ltc4283_pin_config(st, dev);
+ if (ret)
+ return ret;
+
+ if (device_property_read_bool(dev, "adi,power-good-reset-on-fet")) {
+ ret = regmap_clear_bits(st->map, LTC4283_CONTROL_1,
+ LTC4283_PWRGD_RST_CTRL_MASK);
+ if (ret)
+ return ret;
+ }
+
+ if (device_property_read_bool(dev, "adi,fet-turn-off-disable")) {
+ ret = regmap_clear_bits(st->map, LTC4283_CONTROL_1,
+ LTC4283_FET_BAD_OFF_MASK);
+ if (ret)
+ return ret;
+ }
+
+ if (device_property_read_bool(dev, "adi,tmr-pull-down-disable")) {
+ ret = regmap_set_bits(st->map, LTC4283_CONTROL_1,
+ LTC4283_THERM_TMR_MASK);
+ if (ret)
+ return ret;
+ }
+
+ if (device_property_read_bool(dev, "adi,dvdt-inrush-control-disable")) {
+ ret = regmap_clear_bits(st->map, LTC4283_CONTROL_1,
+ LTC4283_DVDT_MASK);
+ if (ret)
+ return ret;
+ }
+
+ if (device_property_read_bool(dev, "adi,undervoltage-retry-disable")) {
+ ret = regmap_clear_bits(st->map, LTC4283_CONTROL_2,
+ LTC4283_UV_RETRY_MASK);
+ if (ret)
+ return ret;
+ }
+
+ if (device_property_read_bool(dev, "adi,overvoltage-retry-disable")) {
+ ret = regmap_clear_bits(st->map, LTC4283_CONTROL_2,
+ LTC4283_OV_RETRY_MASK);
+ if (ret)
+ return ret;
+ }
+
+ if (device_property_read_bool(dev, "adi,external-fault-retry-enable")) {
+ if (!st->ext_fault)
+ return dev_err_probe(dev, -EINVAL,
+ "adi,external-fault-retry-enable set but PGIO4 not configured\n");
+ ret = regmap_set_bits(st->map, LTC4283_CONTROL_2,
+ LTC4283_EXT_FAULT_RETRY_MASK);
+ if (ret)
+ return ret;
+ }
+
+ if (device_property_read_bool(dev, "adi,fault-log-enable")) {
+ ret = regmap_set_bits(st->map, LTC4283_FAULT_LOG_CTRL,
+ LTC4283_FAULT_LOG_EN_MASK);
+ if (ret)
+ return ret;
+ }
+
+ ret = device_property_match_property_string(dev, "adi,overcurrent-retries",
+ ltc4283_oc_fet_retry,
+ ARRAY_SIZE(ltc4283_oc_fet_retry));
+ /* We still want to catch when an invalid string is given. */
+ if (ret < 0 && ret != -EINVAL)
+ return dev_err_probe(dev, ret,
+ "adi,overcurrent-retries invalid value\n");
+ if (ret >= 0) {
+ ret = regmap_update_bits(st->map, LTC4283_CONTROL_2,
+ LTC4283_OC_RETRY_MASK,
+ FIELD_PREP(LTC4283_OC_RETRY_MASK, ret));
+ if (ret)
+ return ret;
+ }
+
+ ret = device_property_match_property_string(dev, "adi,fet-bad-retries",
+ ltc4283_oc_fet_retry,
+ ARRAY_SIZE(ltc4283_oc_fet_retry));
+ if (ret < 0 && ret != -EINVAL)
+ return dev_err_probe(dev, ret,
+ "adi,fet-bad-retries invalid value\n");
+ if (ret >= 0) {
+ ret = regmap_update_bits(st->map, LTC4283_CONTROL_2,
+ LTC4283_FET_BAD_RETRY_MASK,
+ FIELD_PREP(LTC4283_FET_BAD_RETRY_MASK, ret));
+ if (ret)
+ return ret;
+ }
+
+ if (device_property_read_bool(dev, "adi,external-fault-fet-off-enable")) {
+ if (!st->ext_fault)
+ return dev_err_probe(dev, -EINVAL,
+ "adi,external-fault-fet-off-enable set but PGIO4 not configured\n");
+ ret = regmap_set_bits(st->map, LTC4283_CONFIG_3,
+ LTC4283_EXTFLT_TURN_OFF_MASK);
+ if (ret)
+ return ret;
+ }
+
+ if (device_property_read_bool(dev, "adi,vpower-drns-enable")) {
+ u32 chan = LTC4283_CHAN_DRNS - LTC4283_CHAN_ADI_1;
+
+ __clear_bit(LTC4283_CHAN_DRNS, &st->ch_enable_mask);
+ /*
+ * Then, let's by default disable DRNS from ADC2 given that it
+ * is already being monitored by the VPWR channel. One can still
+ * enable it later on if needed.
+ */
+ ret = regmap_clear_bits(st->map, LTC4283_ADC_SELECT(chan),
+ LTC4283_ADC_SELECT_MASK(chan));
+ if (ret)
+ return ret;
+
+ val = 1;
+ } else {
+ val = 0;
+ }
+
+ ret = regmap_update_bits(st->map, LTC4283_CONFIG_3,
+ LTC4283_VPWR_DRNS_MASK,
+ FIELD_PREP(LTC4283_VPWR_DRNS_MASK, val));
+ if (ret)
+ return ret;
+
+ /* Make sure the ADC has 12bit resolution since we're assuming that. */
+ ret = regmap_update_bits(st->map, LTC4283_PGIO_CONFIG_2,
+ LTC4283_ADC_MASK,
+ FIELD_PREP(LTC4283_ADC_MASK, 3));
+ if (ret)
+ return ret;
+
+ /* Energy reads (which are 6 byte block reads) rely on page access */
+ ret = regmap_set_bits(st->map, LTC4283_CONTROL_1, LTC4283_RW_PAGE_MASK);
+ if (ret)
+ return ret;
+
+ /*
+ * Make sure we are integrating power as we only support reporting
+ * consumed energy.
+ */
+ return regmap_clear_bits(st->map, LTC4283_METER_CONTROL,
+ LTC4283_INTEGRATE_I_MASK);
+}
+
+static const struct hwmon_channel_info * const ltc4283_info[] = {
+ HWMON_CHANNEL_INFO(in,
+ HWMON_I_LCRIT_ALARM | HWMON_I_CRIT_ALARM |
+ HWMON_I_RESET_HISTORY | HWMON_I_LABEL,
+ HWMON_I_INPUT | HWMON_I_LOWEST | HWMON_I_HIGHEST |
+ HWMON_I_MAX | HWMON_I_MIN | HWMON_I_MIN_ALARM |
+ HWMON_I_MAX_ALARM | HWMON_I_RESET_HISTORY |
+ HWMON_I_LABEL,
+ HWMON_I_INPUT | HWMON_I_LOWEST | HWMON_I_HIGHEST |
+ HWMON_I_MAX | HWMON_I_MIN | HWMON_I_MIN_ALARM |
+ HWMON_I_RESET_HISTORY | HWMON_I_MAX_ALARM |
+ HWMON_I_ENABLE | HWMON_I_LABEL,
+ HWMON_I_INPUT | HWMON_I_LOWEST | HWMON_I_HIGHEST |
+ HWMON_I_MAX | HWMON_I_MIN | HWMON_I_MIN_ALARM |
+ HWMON_I_RESET_HISTORY | HWMON_I_MAX_ALARM |
+ HWMON_I_ENABLE | HWMON_I_LABEL,
+ HWMON_I_INPUT | HWMON_I_LOWEST | HWMON_I_HIGHEST |
+ HWMON_I_MAX | HWMON_I_MIN | HWMON_I_MIN_ALARM |
+ HWMON_I_RESET_HISTORY | HWMON_I_MAX_ALARM |
+ HWMON_I_ENABLE | HWMON_I_LABEL,
+ HWMON_I_INPUT | HWMON_I_LOWEST | HWMON_I_HIGHEST |
+ HWMON_I_MAX | HWMON_I_MIN | HWMON_I_MIN_ALARM |
+ HWMON_I_RESET_HISTORY | HWMON_I_MAX_ALARM |
+ HWMON_I_ENABLE | HWMON_I_LABEL,
+ HWMON_I_INPUT | HWMON_I_LOWEST | HWMON_I_HIGHEST |
+ HWMON_I_MAX | HWMON_I_MIN | HWMON_I_MIN_ALARM |
+ HWMON_I_RESET_HISTORY | HWMON_I_MAX_ALARM |
+ HWMON_I_ENABLE | HWMON_I_LABEL,
+ HWMON_I_INPUT | HWMON_I_LOWEST | HWMON_I_HIGHEST |
+ HWMON_I_MAX | HWMON_I_MIN | HWMON_I_MIN_ALARM |
+ HWMON_I_RESET_HISTORY | HWMON_I_MAX_ALARM |
+ HWMON_I_ENABLE | HWMON_I_LABEL,
+ HWMON_I_INPUT | HWMON_I_LOWEST | HWMON_I_HIGHEST |
+ HWMON_I_MAX | HWMON_I_MIN | HWMON_I_MIN_ALARM |
+ HWMON_I_RESET_HISTORY | HWMON_I_MAX_ALARM |
+ HWMON_I_ENABLE | HWMON_I_LABEL,
+ HWMON_I_INPUT | HWMON_I_LOWEST | HWMON_I_HIGHEST |
+ HWMON_I_MAX | HWMON_I_MIN | HWMON_I_MIN_ALARM |
+ HWMON_I_RESET_HISTORY | HWMON_I_MAX_ALARM |
+ HWMON_I_ENABLE | HWMON_I_LABEL,
+ HWMON_I_INPUT | HWMON_I_LOWEST | HWMON_I_HIGHEST |
+ HWMON_I_MAX | HWMON_I_MIN | HWMON_I_MIN_ALARM |
+ HWMON_I_RESET_HISTORY | HWMON_I_MAX_ALARM |
+ HWMON_I_ENABLE | HWMON_I_LABEL,
+ HWMON_I_INPUT | HWMON_I_LOWEST | HWMON_I_HIGHEST |
+ HWMON_I_MAX | HWMON_I_MIN | HWMON_I_MIN_ALARM |
+ HWMON_I_RESET_HISTORY | HWMON_I_MAX_ALARM |
+ HWMON_I_FAULT | HWMON_I_ENABLE | HWMON_I_LABEL,
+ HWMON_I_INPUT | HWMON_I_LOWEST | HWMON_I_HIGHEST |
+ HWMON_I_MAX | HWMON_I_MIN | HWMON_I_MIN_ALARM |
+ HWMON_I_RESET_HISTORY | HWMON_I_MAX_ALARM |
+ HWMON_I_ENABLE | HWMON_I_LABEL,
+ HWMON_I_INPUT | HWMON_I_LOWEST | HWMON_I_HIGHEST |
+ HWMON_I_MAX | HWMON_I_MIN | HWMON_I_MIN_ALARM |
+ HWMON_I_RESET_HISTORY | HWMON_I_MAX_ALARM |
+ HWMON_I_ENABLE | HWMON_I_LABEL,
+ HWMON_I_INPUT | HWMON_I_LOWEST | HWMON_I_HIGHEST |
+ HWMON_I_MAX | HWMON_I_MIN | HWMON_I_MIN_ALARM |
+ HWMON_I_RESET_HISTORY | HWMON_I_MAX_ALARM |
+ HWMON_I_ENABLE | HWMON_I_LABEL,
+ HWMON_I_INPUT | HWMON_I_LOWEST | HWMON_I_HIGHEST |
+ HWMON_I_MAX | HWMON_I_MIN | HWMON_I_MIN_ALARM |
+ HWMON_I_RESET_HISTORY | HWMON_I_MAX_ALARM |
+ HWMON_I_ENABLE | HWMON_I_LABEL),
+ HWMON_CHANNEL_INFO(curr,
+ HWMON_C_INPUT | HWMON_C_LOWEST | HWMON_C_HIGHEST |
+ HWMON_C_MAX | HWMON_C_MIN | HWMON_C_MIN_ALARM |
+ HWMON_C_MAX_ALARM | HWMON_C_CRIT_ALARM |
+ HWMON_C_RESET_HISTORY | HWMON_C_LABEL),
+ HWMON_CHANNEL_INFO(power,
+ HWMON_P_INPUT | HWMON_P_INPUT_LOWEST |
+ HWMON_P_INPUT_HIGHEST | HWMON_P_MAX | HWMON_P_MIN |
+ HWMON_P_MAX_ALARM | HWMON_P_MIN_ALARM |
+ HWMON_P_RESET_HISTORY | HWMON_P_LABEL),
+ HWMON_CHANNEL_INFO(energy,
+ HWMON_E_ENABLE),
+ HWMON_CHANNEL_INFO(energy64,
+ HWMON_E_INPUT),
+ NULL
+};
+
+static const struct hwmon_ops ltc4283_ops = {
+ .read = ltc4283_read,
+ .write = ltc4283_write,
+ .is_visible = ltc4283_is_visible,
+ .read_string = ltc4283_read_labels,
+};
+
+static const struct hwmon_chip_info ltc4283_chip_info = {
+ .ops = <c4283_ops,
+ .info = ltc4283_info,
+};
+
+static int ltc4283_show_fault_log(void *arg, u64 *val, u32 mask)
+{
+ struct ltc4283_hwmon *st = arg;
+ long alarm;
+ int ret;
+
+ ret = ltc4283_read_alarm(st, LTC4283_FAULT_LOG, mask, &alarm);
+ if (ret)
+ return ret;
+
+ *val = alarm;
+
+ return 0;
+}
+
+static int ltc4283_show_in0_lcrit_fault_log(void *arg, u64 *val)
+{
+ return ltc4283_show_fault_log(arg, val, LTC4283_UV_FAULT_MASK);
+}
+DEFINE_DEBUGFS_ATTRIBUTE(ltc4283_in0_lcrit_fault_log,
+ ltc4283_show_in0_lcrit_fault_log, NULL, "%llu\n");
+
+static int ltc4283_show_in0_crit_fault_log(void *arg, u64 *val)
+{
+ return ltc4283_show_fault_log(arg, val, LTC4283_OV_FAULT_MASK);
+}
+DEFINE_DEBUGFS_ATTRIBUTE(ltc4283_in0_crit_fault_log,
+ ltc4283_show_in0_crit_fault_log, NULL, "%llu\n");
+
+static int ltc4283_show_fet_bad_fault_log(void *arg, u64 *val)
+{
+ return ltc4283_show_fault_log(arg, val, LTC4283_FET_BAD_FAULT_MASK);
+}
+DEFINE_DEBUGFS_ATTRIBUTE(ltc4283_fet_bad_fault_log,
+ ltc4283_show_fet_bad_fault_log, NULL, "%llu\n");
+
+static int ltc4283_show_fet_short_fault_log(void *arg, u64 *val)
+{
+ return ltc4283_show_fault_log(arg, val, LTC4283_FET_SHORT_FAULT_MASK);
+}
+DEFINE_DEBUGFS_ATTRIBUTE(ltc4283_fet_short_fault_log,
+ ltc4283_show_fet_short_fault_log, NULL, "%llu\n");
+
+static int ltc4283_show_curr1_crit_fault_log(void *arg, u64 *val)
+{
+ return ltc4283_show_fault_log(arg, val, LTC4283_OC_FAULT_MASK);
+}
+DEFINE_DEBUGFS_ATTRIBUTE(ltc4283_curr1_crit_fault_log,
+ ltc4283_show_curr1_crit_fault_log, NULL, "%llu\n");
+
+static int ltc4283_show_power1_failed_fault_log(void *arg, u64 *val)
+{
+ return ltc4283_show_fault_log(arg, val, LTC4283_PWR_FAIL_FAULT_MASK);
+}
+DEFINE_DEBUGFS_ATTRIBUTE(ltc4283_power1_failed_fault_log,
+ ltc4283_show_power1_failed_fault_log, NULL, "%llu\n");
+
+static int ltc4283_show_power1_good_input_fault_log(void *arg, u64 *val)
+{
+ return ltc4283_show_fault_log(arg, val, LTC4283_PGI_FAULT_MASK);
+}
+DEFINE_DEBUGFS_ATTRIBUTE(ltc4283_power1_good_input_fault_log,
+ ltc4283_show_power1_good_input_fault_log, NULL, "%llu\n");
+
+static void ltc4283_debugfs_init(struct ltc4283_hwmon *st, struct i2c_client *i2c)
+{
+ debugfs_create_file_unsafe("in0_crit_fault_log", 0400, i2c->debugfs, st,
+ <c4283_in0_crit_fault_log);
+ debugfs_create_file_unsafe("in0_lcrit_fault_log", 0400, i2c->debugfs, st,
+ <c4283_in0_lcrit_fault_log);
+ debugfs_create_file_unsafe("in0_fet_bad_fault_log", 0400, i2c->debugfs, st,
+ <c4283_fet_bad_fault_log);
+ debugfs_create_file_unsafe("in0_fet_short_fault_log", 0400, i2c->debugfs, st,
+ <c4283_fet_short_fault_log);
+ debugfs_create_file_unsafe("curr1_crit_fault_log", 0400, i2c->debugfs, st,
+ <c4283_curr1_crit_fault_log);
+ debugfs_create_file_unsafe("power1_failed_fault_log", 0400, i2c->debugfs, st,
+ <c4283_power1_failed_fault_log);
+ debugfs_create_file_unsafe("power1_good_input_fault_log", 0400, i2c->debugfs,
+ st, <c4283_power1_good_input_fault_log);
+}
+
+static bool ltc4283_is_word_reg(unsigned int reg)
+{
+ return reg >= LTC4283_SENSE && reg <= LTC4283_ADIO34_MAX;
+}
+
+static int ltc4283_reg_read(void *context, unsigned int reg, unsigned int *val)
+{
+ struct i2c_client *client = context;
+ int ret;
+
+ if (ltc4283_is_word_reg(reg))
+ ret = i2c_smbus_read_word_swapped(client, reg);
+ else
+ ret = i2c_smbus_read_byte_data(client, reg);
+
+ if (ret < 0)
+ return ret;
+
+ *val = ret;
+ return 0;
+}
+
+static int ltc4283_reg_write(void *context, unsigned int reg, unsigned int val)
+{
+ struct i2c_client *client = context;
+
+ if (ltc4283_is_word_reg(reg))
+ return i2c_smbus_write_word_swapped(client, reg, val);
+
+ return i2c_smbus_write_byte_data(client, reg, val);
+}
+
+static const struct regmap_bus ltc4283_regmap_bus = {
+ .reg_read = ltc4283_reg_read,
+ .reg_write = ltc4283_reg_write,
+};
+
+static bool ltc4283_writable_reg(struct device *dev, unsigned int reg)
+{
+ switch (reg) {
+ case LTC4283_SYSTEM_STATUS ... LTC4283_FAULT_STATUS:
+ return false;
+ case LTC4283_RESERVED_OC:
+ return false;
+ case LTC4283_RESERVED_86 ... LTC4283_RESERVED_8F:
+ return false;
+ case LTC4283_RESERVED_91 ... LTC4283_RESERVED_A1:
+ return false;
+ case LTC4283_RESERVED_A3:
+ return false;
+ case LTC4283_RESERVED_AC:
+ return false;
+ case LTC4283_POWER_PLAY_MSB ... LTC4283_POWER_PLAY_LSB:
+ return false;
+ case LTC4283_RESERVED_F1 ... LTC4283_RESERVED_FF:
+ return false;
+ default:
+ return true;
+ }
+}
+
+static const struct regmap_config ltc4283_regmap_config = {
+ .reg_bits = 8,
+ .val_bits = 16,
+ .max_register = 0xFF,
+ .writeable_reg = ltc4283_writable_reg,
+};
+
+static int ltc4283_probe(struct i2c_client *client)
+{
+ struct device *dev = &client->dev, *hwmon;
+ struct auxiliary_device *adev;
+ struct ltc4283_hwmon *st;
+ int ret;
+
+ st = devm_kzalloc(dev, sizeof(*st), GFP_KERNEL);
+ if (!st)
+ return -ENOMEM;
+
+ if (!i2c_check_functionality(client->adapter,
+ I2C_FUNC_SMBUS_BYTE_DATA |
+ I2C_FUNC_SMBUS_WORD_DATA |
+ I2C_FUNC_SMBUS_READ_I2C_BLOCK))
+ return -EOPNOTSUPP;
+
+ st->client = client;
+ st->map = devm_regmap_init(dev, <c4283_regmap_bus, client,
+ <c4283_regmap_config);
+ if (IS_ERR(st->map))
+ return dev_err_probe(dev, PTR_ERR(st->map),
+ "Failed to create regmap\n");
+
+ ret = ltc4283_setup(st, dev);
+ if (ret)
+ return ret;
+
+ hwmon = devm_hwmon_device_register_with_info(dev, "ltc4283", st,
+ <c4283_chip_info, NULL);
+
+ if (IS_ERR(hwmon))
+ return PTR_ERR(hwmon);
+
+ ltc4283_debugfs_init(st, client);
+
+ if (!st->gpio_mask)
+ return 0;
+
+ adev = devm_auxiliary_device_create(dev, "gpio", &st->gpio_mask);
+ if (!adev)
+ return dev_err_probe(dev, -ENODEV, "Failed to add GPIO device\n");
+
+ return 0;
+}
+
+static const struct of_device_id ltc4283_of_match[] = {
+ { .compatible = "adi,ltc4283" },
+ { }
+};
+
+static const struct i2c_device_id ltc4283_i2c_id[] = {
+ { "ltc4283" },
+ { }
+};
+MODULE_DEVICE_TABLE(i2c, ltc4283_i2c_id);
+
+static struct i2c_driver ltc4283_driver = {
+ .driver = {
+ .name = "ltc4283",
+ .of_match_table = ltc4283_of_match,
+ },
+ .probe = ltc4283_probe,
+ .id_table = ltc4283_i2c_id,
+};
+module_i2c_driver(ltc4283_driver);
+
+MODULE_AUTHOR("Nuno Sá <nuno.sa@analog.com>");
+MODULE_DESCRIPTION("LTC4283 Hot Swap Controller driver");
+MODULE_LICENSE("GPL");
--
2.53.0
^ permalink raw reply related
* [PATCH v8 3/3] gpio: gpio-ltc4283: Add support for the LTC4283 Swap Controller
From: Nuno Sá via B4 Relay @ 2026-03-27 17:26 UTC (permalink / raw)
To: linux-gpio, linux-hwmon, devicetree, linux-doc
Cc: Guenter Roeck, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Jonathan Corbet, Linus Walleij, Bartosz Golaszewski,
Bartosz Golaszewski
In-Reply-To: <20260327-ltc4283-support-v8-0-471de255d728@analog.com>
From: Nuno Sá <nuno.sa@analog.com>
The LTC4283 device has up to 8 pins that can be configured as GPIOs.
Note that PGIO pins are not set as GPIOs by default so if they are
configured to be used as GPIOs we need to make sure to initialize them
to a sane default. They are set as inputs by default.
Acked-by: Bartosz Golaszewski <bartosz.golaszewski@linaro.org>
Reviewed-by: Linus Walleij <linusw@kernel.org>
Signed-off-by: Nuno Sá <nuno.sa@analog.com>
---
MAINTAINERS | 2 +
drivers/gpio/Kconfig | 15 +++
drivers/gpio/Makefile | 1 +
drivers/gpio/gpio-ltc4283.c | 218 ++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 236 insertions(+)
diff --git a/MAINTAINERS b/MAINTAINERS
index 38d22cf622b7..0f4fc6c189f6 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -15143,9 +15143,11 @@ F: drivers/hwmon/ltc4282.c
LTC4283 HARDWARE MONITOR AND GPIO DRIVER
M: Nuno Sá <nuno.sa@analog.com>
+L: linux-gpio@vger.kernel.org
L: linux-hwmon@vger.kernel.org
S: Supported
F: Documentation/devicetree/bindings/hwmon/adi,ltc4283.yaml
+F: drivers/gpio/gpio-ltc4283.c
F: drivers/hwmon/ltc4283.c
LTC4286 HARDWARE MONITOR DRIVER
diff --git a/drivers/gpio/Kconfig b/drivers/gpio/Kconfig
index b45fb799e36c..ba2621024598 100644
--- a/drivers/gpio/Kconfig
+++ b/drivers/gpio/Kconfig
@@ -1758,6 +1758,21 @@ config GPIO_WM8994
endmenu
+menu "Auxiliary Bus GPIO drivers"
+ depends on AUXILIARY_BUS
+
+config GPIO_LTC4283
+ tristate "Analog Devices LTC4283 GPIO support"
+ depends on SENSORS_LTC4283
+ help
+ If you say yes here you want the GPIO function available in Analog
+ Devices LTC4283 Negative Voltage Hot Swap Controller.
+
+ This driver can also be built as a module. If so, the module will
+ be called gpio-ltc4283.
+
+endmenu
+
menu "PCI GPIO expanders"
depends on PCI
diff --git a/drivers/gpio/Makefile b/drivers/gpio/Makefile
index c05f7d795c43..ff37aca5029c 100644
--- a/drivers/gpio/Makefile
+++ b/drivers/gpio/Makefile
@@ -102,6 +102,7 @@ obj-$(CONFIG_GPIO_LP873X) += gpio-lp873x.o
obj-$(CONFIG_GPIO_LP87565) += gpio-lp87565.o
obj-$(CONFIG_GPIO_LPC18XX) += gpio-lpc18xx.o
obj-$(CONFIG_GPIO_LPC32XX) += gpio-lpc32xx.o
+obj-$(CONFIG_GPIO_LTC4283) += gpio-ltc4283.o
obj-$(CONFIG_GPIO_MACSMC) += gpio-macsmc.o
obj-$(CONFIG_GPIO_MADERA) += gpio-madera.o
obj-$(CONFIG_GPIO_MAX3191X) += gpio-max3191x.o
diff --git a/drivers/gpio/gpio-ltc4283.c b/drivers/gpio/gpio-ltc4283.c
new file mode 100644
index 000000000000..6609443c5d62
--- /dev/null
+++ b/drivers/gpio/gpio-ltc4283.c
@@ -0,0 +1,218 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Analog Devices LTC4283 GPIO driver
+ *
+ * Copyright 2025 Analog Devices Inc.
+ */
+
+#include <linux/auxiliary_bus.h>
+#include <linux/bitfield.h>
+#include <linux/bitmap.h>
+#include <linux/bits.h>
+#include <linux/device.h>
+#include <linux/gpio/driver.h>
+#include <linux/mod_devicetable.h>
+#include <linux/module.h>
+#include <linux/regmap.h>
+
+#define LTC4283_PINS_MAX 8
+#define LTC4283_PGIOX_START_NR 4
+#define LTC4283_INPUT_STATUS 0x02
+#define LTC4283_PGIO_CONFIG 0x10
+#define LTC4283_PGIO_CFG_MASK(pin) \
+ GENMASK(((pin) - LTC4283_PGIOX_START_NR) * 2 + 1, (((pin) - LTC4283_PGIOX_START_NR) * 2))
+#define LTC4283_PGIO_CONFIG_2 0x11
+
+#define LTC4283_ADIO_CONFIG 0x12
+/* starts at bit 4 */
+#define LTC4283_ADIOX_CONFIG_MASK(pin) BIT((pin) + 4)
+#define LTC4283_PGIO_DIR_IN 3
+#define LTC4283_PGIO_DIR_OUT 2
+
+struct ltc4283_gpio {
+ struct gpio_chip gpio_chip;
+ struct regmap *regmap;
+};
+
+static int ltc4283_pgio_get_direction(const struct ltc4283_gpio *st, unsigned int off)
+{
+ unsigned int val;
+ int ret;
+
+ ret = regmap_read(st->regmap, LTC4283_PGIO_CONFIG, &val);
+ if (ret)
+ return ret;
+
+ val = field_get(LTC4283_PGIO_CFG_MASK(off), val);
+ if (val == LTC4283_PGIO_DIR_IN)
+ return GPIO_LINE_DIRECTION_IN;
+
+ return GPIO_LINE_DIRECTION_OUT;
+}
+
+static int ltc4283_gpio_get_direction(struct gpio_chip *gc, unsigned int off)
+{
+ struct ltc4283_gpio *st = gpiochip_get_data(gc);
+ unsigned int val;
+ int ret;
+
+ if (off >= LTC4283_PGIOX_START_NR)
+ return ltc4283_pgio_get_direction(st, off);
+
+ ret = regmap_read(st->regmap, LTC4283_ADIO_CONFIG, &val);
+ if (ret)
+ return ret;
+
+ if (val & LTC4283_ADIOX_CONFIG_MASK(off))
+ return GPIO_LINE_DIRECTION_IN;
+
+ return GPIO_LINE_DIRECTION_OUT;
+}
+
+static int ltc4283_gpio_direction_set(const struct ltc4283_gpio *st,
+ unsigned int off, bool input)
+{
+ if (off >= LTC4283_PGIOX_START_NR) {
+ unsigned int val = LTC4283_PGIO_DIR_OUT;
+
+ if (input)
+ val = LTC4283_PGIO_DIR_IN;
+
+ val = field_prep(LTC4283_PGIO_CFG_MASK(off), val);
+ return regmap_update_bits(st->regmap, LTC4283_PGIO_CONFIG,
+ LTC4283_PGIO_CFG_MASK(off), val);
+ }
+
+ return regmap_update_bits(st->regmap, LTC4283_ADIO_CONFIG,
+ LTC4283_ADIOX_CONFIG_MASK(off),
+ field_prep(LTC4283_ADIOX_CONFIG_MASK(off), input));
+}
+
+static int __ltc4283_gpio_set_value(const struct ltc4283_gpio *st,
+ unsigned int off, int val)
+{
+ u32 reg = off < LTC4283_PGIOX_START_NR ? LTC4283_ADIO_CONFIG : LTC4283_PGIO_CONFIG_2;
+
+ return regmap_update_bits(st->regmap, reg, BIT(off),
+ field_prep(BIT(off), !!val));
+}
+
+static int ltc4283_gpio_direction_input(struct gpio_chip *gc, unsigned int off)
+{
+ struct ltc4283_gpio *st = gpiochip_get_data(gc);
+
+ return ltc4283_gpio_direction_set(st, off, true);
+}
+
+static int ltc4283_gpio_direction_output(struct gpio_chip *gc, unsigned int off, int val)
+{
+ struct ltc4283_gpio *st = gpiochip_get_data(gc);
+ int ret;
+
+ ret = ltc4283_gpio_direction_set(st, off, false);
+ if (ret)
+ return ret;
+
+ return __ltc4283_gpio_set_value(st, off, val);
+}
+
+static int ltc4283_gpio_get_value(struct gpio_chip *gc, unsigned int off)
+{
+ struct ltc4283_gpio *st = gpiochip_get_data(gc);
+ unsigned int val, reg;
+ int ret, dir;
+
+ dir = ltc4283_gpio_get_direction(gc, off);
+ if (dir < 0)
+ return dir;
+
+ if (dir == GPIO_LINE_DIRECTION_IN) {
+ ret = regmap_read(st->regmap, LTC4283_INPUT_STATUS, &val);
+ if (ret)
+ return ret;
+
+ /* ADIO1 is at bit 3. */
+ if (off < LTC4283_PGIOX_START_NR)
+ return !!(val & BIT(3 - off));
+
+ /* PGIO1 is at bit 7. */
+ return !!(val & BIT(7 - (off - LTC4283_PGIOX_START_NR)));
+ }
+
+ if (off < LTC4283_PGIOX_START_NR)
+ reg = LTC4283_ADIO_CONFIG;
+ else
+ reg = LTC4283_PGIO_CONFIG_2;
+
+ ret = regmap_read(st->regmap, reg, &val);
+ if (ret)
+ return ret;
+
+ return !!(val & BIT(off));
+}
+
+static int ltc4283_gpio_set_value(struct gpio_chip *gc, unsigned int off, int val)
+{
+ struct ltc4283_gpio *st = gpiochip_get_data(gc);
+
+ return __ltc4283_gpio_set_value(st, off, val);
+}
+
+static int ltc4283_init_valid_mask(struct gpio_chip *gc, unsigned long *valid_mask,
+ unsigned int ngpios)
+{
+ unsigned long *mask = dev_get_platdata(gc->parent);
+
+ bitmap_copy(valid_mask, mask, ngpios);
+ return 0;
+}
+
+static int ltc4283_gpio_probe(struct auxiliary_device *adev,
+ const struct auxiliary_device_id *id)
+{
+ struct device *dev = &adev->dev;
+ struct ltc4283_gpio *st;
+ struct gpio_chip *gc;
+
+ st = devm_kzalloc(dev, sizeof(*st), GFP_KERNEL);
+ if (!st)
+ return -ENOMEM;
+
+ st->regmap = dev_get_regmap(dev->parent, NULL);
+ if (!st->regmap)
+ return dev_err_probe(dev, -ENODEV,
+ "Failed to get regmap\n");
+
+ gc = &st->gpio_chip;
+ gc->parent = dev;
+ gc->get_direction = ltc4283_gpio_get_direction;
+ gc->direction_input = ltc4283_gpio_direction_input;
+ gc->direction_output = ltc4283_gpio_direction_output;
+ gc->get = ltc4283_gpio_get_value;
+ gc->set = ltc4283_gpio_set_value;
+ gc->init_valid_mask = ltc4283_init_valid_mask;
+ gc->can_sleep = true;
+
+ gc->base = -1;
+ gc->ngpio = LTC4283_PINS_MAX;
+ gc->label = adev->name;
+ gc->owner = THIS_MODULE;
+
+ return devm_gpiochip_add_data(dev, &st->gpio_chip, st);
+}
+
+static const struct auxiliary_device_id ltc4283_aux_id_table[] = {
+ { "ltc4283.gpio" },
+ { }
+};
+MODULE_DEVICE_TABLE(auxiliary, ltc4283_aux_id_table);
+
+static struct auxiliary_driver ltc4283_gpio_driver = {
+ .probe = ltc4283_gpio_probe,
+ .id_table = ltc4283_aux_id_table,
+};
+module_auxiliary_driver(ltc4283_gpio_driver);
+
+MODULE_AUTHOR("Nuno Sá <nuno.sa@analog.com>");
+MODULE_DESCRIPTION("GPIO LTC4283 Driver");
+MODULE_LICENSE("GPL");
--
2.53.0
^ permalink raw reply related
* [PATCH v8 0/3] hwmon: Add support for the LTC4283 Hot Swap Controller
From: Nuno Sá via B4 Relay @ 2026-03-27 17:26 UTC (permalink / raw)
To: linux-gpio, linux-hwmon, devicetree, linux-doc
Cc: Guenter Roeck, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Jonathan Corbet, Linus Walleij, Bartosz Golaszewski,
Bartosz Golaszewski
This is v8 for the LTC4283 how swap controller.
Similar to the LTC4282 device, we're clearing some fault logs in the
reset_history attributes.
---
Changes in v8:
- Patch 1:
* Improved descriptin in adi,fet-turn-off-disable so it's more clear;
* Same for adi,dvdt-inrush-control-disable;
* Fixed typo in adi,external-fault-retry-enable description.
- Patch 2:
* Use return value from clamp_val();
* Add missing 'ret < 0' for device_property_match_property_string()
calls;
* Fixed logic in when adi,vpower-drns-enable is enabled;
* Check default state of the pgio4 pin. So that we can, accordingly
flag st->ext_fault.
- Link to v7: https://lore.kernel.org/linux-hwmon/20260314-ltc4283-support-v7-0-1cda48e93802@analog.com/
---
---
Nuno Sá (3):
dt-bindings: hwmon: Document the LTC4283 Swap Controller
hwmon: ltc4283: Add support for the LTC4283 Swap Controller
gpio: gpio-ltc4283: Add support for the LTC4283 Swap Controller
.../devicetree/bindings/hwmon/adi,ltc4283.yaml | 272 +++
Documentation/hwmon/index.rst | 1 +
Documentation/hwmon/ltc4283.rst | 266 +++
MAINTAINERS | 9 +
drivers/gpio/Kconfig | 15 +
drivers/gpio/Makefile | 1 +
drivers/gpio/gpio-ltc4283.c | 218 +++
drivers/hwmon/Kconfig | 12 +
drivers/hwmon/Makefile | 1 +
drivers/hwmon/ltc4283.c | 1796 ++++++++++++++++++++
10 files changed, 2591 insertions(+)
---
base-commit: cd041796c380961f0e3c04d9627af80131608adc
change-id: 20260303-ltc4283-support-063f78acc5a4
--
Thanks!
- Nuno Sá
^ permalink raw reply
* [PATCH v8 1/3] dt-bindings: hwmon: Document the LTC4283 Swap Controller
From: Nuno Sá via B4 Relay @ 2026-03-27 17:26 UTC (permalink / raw)
To: linux-gpio, linux-hwmon, devicetree, linux-doc
Cc: Guenter Roeck, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Jonathan Corbet, Linus Walleij, Bartosz Golaszewski
In-Reply-To: <20260327-ltc4283-support-v8-0-471de255d728@analog.com>
From: Nuno Sá <nuno.sa@analog.com>
The LTC4283 is a negative voltage hot swap controller that drives an
external N-channel MOSFET to allow a board to be safely inserted and
removed from a live backplane.
Special note for the "adi,vpower-drns-enable" property. It allows to choose
between the attenuated MOSFET drain voltage or the attenuated input
voltage at the RTNS pin (effectively choosing between input or output
power). This is a system level decision not really intended to change at
runtime and hence is being added as a Firmware property.
Reviewed-by: Rob Herring (Arm) <robh@kernel.org>
Signed-off-by: Nuno Sá <nuno.sa@analog.com>
---
.../devicetree/bindings/hwmon/adi,ltc4283.yaml | 272 +++++++++++++++++++++
MAINTAINERS | 6 +
2 files changed, 278 insertions(+)
diff --git a/Documentation/devicetree/bindings/hwmon/adi,ltc4283.yaml b/Documentation/devicetree/bindings/hwmon/adi,ltc4283.yaml
new file mode 100644
index 000000000000..05e2132ad4d8
--- /dev/null
+++ b/Documentation/devicetree/bindings/hwmon/adi,ltc4283.yaml
@@ -0,0 +1,272 @@
+# SPDX-License-Identifier: (GPL-2.0 OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/hwmon/adi,ltc4283.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: LTC4283 Negative Voltage Hot Swap Controller
+
+maintainers:
+ - Nuno Sá <nuno.sa@analog.com>
+
+description: |
+ The LTC4283 negative voltage hot swap controller drives an external N-channel
+ MOSFET to allow a board to be safely inserted and removed from a live
+ backplane.
+
+ https://www.analog.com/media/en/technical-documentation/data-sheets/ltc4283.pdf
+
+properties:
+ compatible:
+ enum:
+ - adi,ltc4283
+
+ reg:
+ maxItems: 1
+
+ adi,rsense-nano-ohms:
+ description: Value of the sense resistor.
+
+ adi,current-limit-sense-microvolt:
+ description:
+ The current limit sense voltage of the chip is adjustable between
+ 15mV and 30mV in 1mV steps. This effectively limits the current
+ on the load.
+ minimum: 15000
+ maximum: 30000
+ default: 15000
+
+ adi,current-limit-foldback-factor:
+ description:
+ Specifies the foldback factor for the current limit. The current limit
+ can be reduced (folded back) to one of four preset levels. The value
+ represents the percentage of the current limit sense voltage to use
+ during foldback. A value of 100 means no foldback.
+ $ref: /schemas/types.yaml#/definitions/uint32
+ enum: [10, 20, 50, 100]
+ default: 100
+
+ adi,cooling-delay-ms:
+ description:
+ Cooling time to apply after an overcurrent fault, FET bad or
+ external fault.
+ enum: [512, 1002, 2005, 4100, 8190, 16400, 32800, 65600]
+ default: 512
+
+ adi,fet-bad-timer-delay-ms:
+ description:
+ FET bad timer delay. After a FET bad status condition is detected,
+ this timer is started. If the condition persists for the
+ specified time, the FET is turned off and a fault is logged.
+ enum: [256, 512, 1002, 2005]
+ default: 256
+
+ adi,power-good-reset-on-fet:
+ description:
+ If set, resets the power good status when the MOSFET is turned off.
+ Otherwise, it resets when a low output voltage is detected.
+ type: boolean
+
+ adi,fet-turn-off-disable:
+ description:
+ If set, the MOSFET is not turned off when a FET fault is detected.
+ type: boolean
+
+ adi,tmr-pull-down-disable:
+ description: Disables 2uA pull-down current on the TMR pin.
+ type: boolean
+
+ adi,dvdt-inrush-control-disable:
+ description:
+ Disables dV/dt inrush control during startup. In dV/dt mode, the inrush
+ current is limited by controlling a constant output voltage ramp rate.
+ When disabled, the inrush control mechanism is active current limiting.
+ type: boolean
+
+ adi,fault-log-enable:
+ description:
+ If set, enables logging fault registers and ADC data into EEPROM upon a
+ fault.
+ type: boolean
+
+ adi,vpower-drns-enable:
+ description:
+ If set, enables the attenuated MOSFET drain voltage to be monitored. This
+ effectively means that the MOSFET power is monitored. If not set, the
+ attenuated input voltage (and hence input power) is monitored.
+ type: boolean
+
+ adi,external-fault-fet-off-enable:
+ description: Turns MOSFET off following an external fault.
+ type: boolean
+
+ adi,undervoltage-retry-disable:
+ description: Do not retry to turn on the MOSFET after an undervoltage fault.
+ type: boolean
+
+ adi,overvoltage-retry-disable:
+ description: Do not retry to turn on the MOSFET after an overvoltage fault.
+ type: boolean
+
+ adi,external-fault-retry-enable:
+ description: Retry to turn on the MOSFET after an external fault.
+ type: boolean
+
+ adi,overcurrent-retries:
+ description: Configures auto-retry following an Overcurrent fault.
+ $ref: /schemas/types.yaml#/definitions/string
+ enum: [latch-off, "1", "7", unlimited]
+ default: latch-off
+
+ adi,fet-bad-retries:
+ description:
+ Configures auto-retry following a FET bad fault and a consequent MOSFET
+ turn off.
+ $ref: /schemas/types.yaml#/definitions/string
+ enum: [latch-off, "1", "7", unlimited]
+ default: latch-off
+
+ adi,pgio1-func:
+ description: Configures the function of the PGIO1 pin.
+ $ref: /schemas/types.yaml#/definitions/string
+ enum: [inverted_power_good, power_good, gpio]
+ default: inverted_power_good
+
+ adi,pgio2-func:
+ description: Configures the function of the PGIO2 pin.
+ $ref: /schemas/types.yaml#/definitions/string
+ enum: [inverted_power_good, power_good, gpio, active_current_limiting]
+ default: inverted_power_good
+
+ adi,pgio3-func:
+ description: Configures the function of the PGIO3 pin.
+ $ref: /schemas/types.yaml#/definitions/string
+ enum: [inverted_power_good_input, power_good_input, gpio]
+ default: inverted_power_good_input
+
+ adi,pgio4-func:
+ description: Configures the function of the PGIO4 pin.
+ $ref: /schemas/types.yaml#/definitions/string
+ enum: [inverted_external_fault, external_fault, gpio]
+ default: inverted_external_fault
+
+ adi,gpio-on-adio1:
+ description: If set, the ADIO1 pin is used as a GPIO.
+ type: boolean
+
+ adi,gpio-on-adio2:
+ description: If set, the ADIO2 pin is used as a GPIO.
+ type: boolean
+
+ adi,gpio-on-adio3:
+ description: If set, the ADIO3 pin is used as a GPIO.
+ type: boolean
+
+ adi,gpio-on-adio4:
+ description: If set, the ADIO4 pin is used as a GPIO.
+ type: boolean
+
+ gpio-controller: true
+
+ '#gpio-cells':
+ const: 2
+
+dependencies:
+ adi,gpio-on-adio1:
+ - gpio-controller
+ - '#gpio-cells'
+ adi,gpio-on-adio2:
+ - gpio-controller
+ - '#gpio-cells'
+ adi,gpio-on-adio3:
+ - gpio-controller
+ - '#gpio-cells'
+ adi,gpio-on-adio4:
+ - gpio-controller
+ - '#gpio-cells'
+ adi,external-fault-retry-enable:
+ - adi,pgio4-func
+ adi,external-fault-fet-off-enable:
+ - adi,pgio4-func
+
+required:
+ - compatible
+ - reg
+ - adi,rsense-nano-ohms
+
+allOf:
+ - if:
+ properties:
+ adi,pgio1-func:
+ const: gpio
+ required:
+ - adi,pgio1-func
+ then:
+ required:
+ - gpio-controller
+ - '#gpio-cells'
+
+ - if:
+ properties:
+ adi,pgio2-func:
+ const: gpio
+ required:
+ - adi,pgio2-func
+ then:
+ required:
+ - gpio-controller
+ - '#gpio-cells'
+
+ - if:
+ properties:
+ adi,pgio3-func:
+ const: gpio
+ required:
+ - adi,pgio3-func
+ then:
+ required:
+ - gpio-controller
+ - '#gpio-cells'
+
+ - if:
+ properties:
+ adi,pgio4-func:
+ const: gpio
+ required:
+ - adi,pgio4-func
+ then:
+ properties:
+ adi,external-fault-retry-enable: false
+ adi,external-fault-fet-off-enable: false
+ required:
+ - gpio-controller
+ - '#gpio-cells'
+
+additionalProperties: false
+
+examples:
+ - |
+ i2c {
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ swap-controller@15 {
+ compatible = "adi,ltc4283";
+ reg = <0x15>;
+
+ adi,rsense-nano-ohms = <500>;
+ adi,current-limit-sense-microvolt = <25000>;
+ adi,current-limit-foldback-factor = <10>;
+ adi,cooling-delay-ms = <8190>;
+ adi,fet-bad-timer-delay-ms = <512>;
+
+ adi,external-fault-fet-off-enable;
+ adi,pgio4-func = "external_fault";
+
+ adi,gpio-on-adio1;
+ adi,pgio1-func = "gpio";
+ gpio-controller;
+ #gpio-cells = <2>;
+ };
+ };
+...
diff --git a/MAINTAINERS b/MAINTAINERS
index 830c6f076b00..13ae2f3db449 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -15141,6 +15141,12 @@ F: Documentation/devicetree/bindings/hwmon/adi,ltc4282.yaml
F: Documentation/hwmon/ltc4282.rst
F: drivers/hwmon/ltc4282.c
+LTC4283 HARDWARE MONITOR AND GPIO DRIVER
+M: Nuno Sá <nuno.sa@analog.com>
+L: linux-hwmon@vger.kernel.org
+S: Supported
+F: Documentation/devicetree/bindings/hwmon/adi,ltc4283.yaml
+
LTC4286 HARDWARE MONITOR DRIVER
M: Delphine CC Chiu <Delphine_CC_Chiu@Wiwynn.com>
L: linux-hwmon@vger.kernel.org
--
2.53.0
^ permalink raw reply related
* Re: [PATCH v1 1/1] arm64: dts: imx91-var-dart-sonata: add RGB select supply for PCA6408
From: Stefano Radaelli @ 2026-03-27 17:16 UTC (permalink / raw)
To: Frank Li
Cc: linux-kernel, devicetree, imx, linux-arm-kernel, pierluigi.p,
Stefano Radaelli, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Sascha Hauer, Pengutronix Kernel Team, Fabio Estevam
In-Reply-To: <aca1jdx0DjmmHqFk@lizhi-Precision-Tower-5810>
Hi Frank,
On Fri, Mar 27, 2026 at 12:51:25PM -0400, Frank Li wrote:
> On Fri, Mar 27, 2026 at 05:32:43PM +0100, Stefano Radaelli wrote:
> > From: Stefano Radaelli <stefano.r@variscite.com>
> >
> > RGB_SEL controls the routing of some carrier board lines on the Sonata
> > board. The two PCA6408 GPIO expanders depend on that path being enabled,
> > so describe the selector as a fixed regulator and use it as their
> > vcc-supply.
>
> Does below resolve your problem?
> https://lore.kernel.org/imx/20260325-pinctrl-mux-v4-0-043c2c82e623@nxp.com/
>
> So needn't hack select as regualtor
>
Thanks for pointing me your patch, interesting improvement!
Actually, in this case RGB_SEL is not meant to model a selectable mux
on the Sonata carrier.
On this board it must stay asserted permanently, otherwise the
downstream path to the two PCA6408 expanders is not accessible.
Modeling it as a mux might be confusing for users of the DART-MX91, as
as it would suggest that the routing is configurable, while on this
board it is actually fixed.
Best Regards,
Stefano
^ permalink raw reply
* Re: [PATCH v3] dt-bindings: sound: Convert pcm3060 to DT Schema
From: Mark Brown @ 2026-03-27 17:15 UTC (permalink / raw)
To: Padmashree S S
Cc: k.marinushkin, lgirdwood, robh, krzk+dt, conor+dt, linux-sound,
devicetree, linux-kernel
In-Reply-To: <20260327121919.603768-1-padmashreess2006@gmail.com>
[-- Attachment #1: Type: text/plain, Size: 426 bytes --]
On Fri, Mar 27, 2026 at 05:49:18PM +0530, Padmashree S S wrote:
> Convert pcm3060 to DT Schema
Please submit patches using subject lines reflecting the style for the
subsystem, this makes it easier for people to identify relevant patches.
Look at what existing commits in the area you're changing are doing and
make sure your subject lines visually resemble what they're doing.
There's no need to resubmit to fix this alone.
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 488 bytes --]
^ permalink raw reply
* Re: [PATCH v4 3/7] pinctrl: extract pinctrl_generic_to_map() from pinctrl_generic_pins_function_dt_node_to_map()
From: Conor Dooley @ 2026-03-27 17:14 UTC (permalink / raw)
To: Frank Li
Cc: Peter Rosin, Linus Walleij, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Rafał Miłecki, Sascha Hauer,
Pengutronix Kernel Team, Fabio Estevam, linux-kernel, linux-gpio,
devicetree, imx, linux-arm-kernel, Haibo Chen
In-Reply-To: <aca2UquiW9lFikhR@lizhi-Precision-Tower-5810>
[-- Attachment #1: Type: text/plain, Size: 4641 bytes --]
On Fri, Mar 27, 2026 at 12:54:42PM -0400, Frank Li wrote:
> On Fri, Mar 27, 2026 at 12:09:32AM +0000, Conor Dooley wrote:
> > On Wed, Mar 25, 2026 at 07:04:12PM -0400, Frank Li wrote:
> > > Refactor pinctrl_generic_pins_function_dt_subnode_to_map() by separating DT
> > > parsing logic from map creation. Introduce a new helper
> > > pinctrl_generic_to_map() to handle mapping to kernel data structures, while
> > > keeping DT property parsing in the subnode function.
> > >
> > > Improve code structure and enables easier reuse for platforms using
> > > different DT properties (e.g. pinmux) without modifying the
> > > dt_node_to_map-style callback API. Avoid unnecessary coupling to
> > > pinctrl_generic_pins_function_dt_node_to_map(), which provides
> > > functionality not needed when the phandle target is unambiguous.
> > >
> > > Maximize code reuse and provide a cleaner extension point for future
> > > pinctrl drivers.
> > >
> > > Signed-off-by: Frank Li <Frank.Li@nxp.com>
> > > ---
> > > change in v4
> > > - new patch
> > > ---
> > > drivers/pinctrl/pinconf.h | 18 ++++++++
> > > drivers/pinctrl/pinctrl-generic.c | 91 ++++++++++++++++++++++++---------------
> > > 2 files changed, 74 insertions(+), 35 deletions(-)
> > >
> > > diff --git a/drivers/pinctrl/pinconf.h b/drivers/pinctrl/pinconf.h
> > > index 2880adef476e68950ffdd540ea42cdee6a16ec27..ffdabddb9660324ed8886a2e8dcacff7e1c6c529 100644
> > > --- a/drivers/pinctrl/pinconf.h
> > > +++ b/drivers/pinctrl/pinconf.h
> > > @@ -166,6 +166,13 @@ int pinctrl_generic_pins_function_dt_node_to_map(struct pinctrl_dev *pctldev,
> > > struct device_node *np,
> > > struct pinctrl_map **maps,
> > > unsigned int *num_maps);
> > > +
> > > +int
> > > +pinctrl_generic_to_map(struct pinctrl_dev *pctldev, struct device_node *parent,
> > > + struct device_node *np, struct pinctrl_map **maps,
> > > + unsigned int *num_maps, unsigned int *num_reserved_maps,
> > > + const char **group_name, unsigned int ngroups,
> > > + const char **functions, unsigned int *pins);
> > > #else
> > > static inline int
> > > pinctrl_generic_pins_function_dt_node_to_map(struct pinctrl_dev *pctldev,
> > > @@ -175,4 +182,15 @@ pinctrl_generic_pins_function_dt_node_to_map(struct pinctrl_dev *pctldev,
> > > {
> > > return -ENOTSUPP;
> > > }
> > > +
> > > +static inline int
> > > +pinctrl_generic_to_map(struct pinctrl_dev *pctldev, struct device_node *parent,
> > > + struct device_node *np, struct pinctrl_map **maps,
> > > + unsigned int *num_maps, unsigned int *num_reserved_maps,
> > > + const char **group_name, unsigned int ngroups,
> > > + const char **functions, unsigned int *pins,
> > > + void *function_data)
> > > +{
> > > + return -ENOTSUPP;
> > > +}
> > > #endif
> > > diff --git a/drivers/pinctrl/pinctrl-generic.c b/drivers/pinctrl/pinctrl-generic.c
> > > index efb39c6a670331775855efdc8566102b5c6202ef..20a216ae63e91b69985ea4cfcd0b57103c6ca950 100644
> > > --- a/drivers/pinctrl/pinctrl-generic.c
> > > +++ b/drivers/pinctrl/pinctrl-generic.c
> > > @@ -17,29 +17,18 @@
> > > #include "pinctrl-utils.h"
> > > #include "pinmux.h"
> > >
> > > -static int pinctrl_generic_pins_function_dt_subnode_to_map(struct pinctrl_dev *pctldev,
> > > - struct device_node *parent,
> > > - struct device_node *np,
> > > - struct pinctrl_map **maps,
> > > - unsigned int *num_maps,
> > > - unsigned int *num_reserved_maps,
> > > - const char **group_names,
> > > - unsigned int ngroups)
> > > +int
> > > +pinctrl_generic_to_map(struct pinctrl_dev *pctldev, struct device_node *parent,
> > > + struct device_node *np, struct pinctrl_map **maps,
> > > + unsigned int *num_maps, unsigned int *num_reserved_maps,
> > > + const char **group_names, unsigned int ngroups,
> > > + const char **functions, unsigned int *pins)
> >
> > npins needs to be an argument to this function also, otherwise
> > pinctrl_generic_add_group() uses it uninitialised...
>
> Is this one the root cause of then broken?
No, this is not the cause of the breakage. I can't believe I still have
to say that. Go read the code and you'll see why that allocation thing
is problematic.
> I am not sure why compiler have not report waring for it.
It did, that's how I found it. Used uninitialised warnings are normally
from clang, so your toolchain might not have seen it. clangd integration
with my editor is how I saw it.
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 228 bytes --]
^ permalink raw reply
* Re: Warning from free_reserved_area() in next-20260325+
From: Mike Rapoport @ 2026-03-27 17:12 UTC (permalink / raw)
To: Bert Karwatzki
Cc: linux-kernel, Liam.Howlett, akpm, andreas, ardb, bp, brauner,
catalin.marinas, chleroy, dave.hansen, davem, david, devicetree,
dvyukov, elver, glider, hannes, hpa, ilias.apalodimas, iommu,
jack, jackmanb, kasan-dev, linux-arm-kernel, linux-efi,
linux-fsdevel, linux-mm, linux-trace-kernel, linuxppc-dev,
lorenzo.stoakes, m.szyprowski, maddy, mhiramat, mhocko, mingo,
mpe, npiggin, robh, robin.murphy, saravanak, sparclinux, surenb,
tglx, vbabka, viro, will, x86, ziy
In-Reply-To: <20260327140109.7561-1-spasswolf@web.de>
Hi Bert,
On Fri, Mar 27, 2026 at 03:01:08PM +0100, Bert Karwatzki wrote:
> Starting with linux next-20260325 I see the following warning early in the
> boot process of a machine running debian stable (trixie) (except for the kernel):
Thanks for the report!
> [ 0.027118] [ T0] ------------[ cut here ]------------
> [ 0.027118] [ T0] Cannot free reserved memory because of deferred initialization of the memory map
> [ 0.027119] [ T0] WARNING: mm/memblock.c:904 at __free_reserved_area+0xa9/0xc0, CPU#0: swapper/0/0
> [ 0.027122] [ T0] Modules linked in:
> [ 0.027123] [ T0] CPU: 0 UID: 0 PID: 0 Comm: swapper/0 Not tainted 7.0.0-rc5-next-20260326-master #385 PREEMPT_RT
> [ 0.027125] [ T0] Hardware name: ASUS System Product Name/ROG STRIX B850-F GAMING WIFI, BIOS 1627 02/05/2026
> [ 0.027125] [ T0] RIP: 0010:__free_reserved_area+0xa9/0xc0
> [ 0.027126] [ T0] Code: 48 89 df 48 89 ee e8 06 fe ff ff 48 89 c3 48 39 e8 72 a0 5b 4c 89 e8 5d 41 5c 41 5d 41 5e c3 cc cc cc cc 48 8d 3d 97 c2 c6 00 <67> 48 0f b9 3a 45 31 ed eb df 66 66 2e 0f 1f 84 00 00 00 00 00 66
> [ 0.027127] [ T0] RSP: 0000:ffffffff9b203e98 EFLAGS: 00010202
> [ 0.027128] [ T0] RAX: 0000000e91c00001 RBX: ffffffff9b100c0f RCX: 0000000080000001
> [ 0.027128] [ T0] RDX: 00000000000000cc RSI: 0000000e2d42d000 RDI: ffffffff9b32ef60
> [ 0.027128] [ T0] RBP: ffff9eeafdd6fbc0 R08: 0000000000000000 R09: 0000000000000001
> [ 0.027129] [ T0] R10: 0000000000001000 R11: 8000000000000163 R12: 000000000000006f
> [ 0.027129] [ T0] R13: 0000000000000000 R14: 0000000000000045 R15: 000000005c8a1000
> [ 0.027129] [ T0] FS: 0000000000000000(0000) GS:ffff9eeb21c05000(0000) knlGS:0000000000000000
> [ 0.027130] [ T0] CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033
> [ 0.027130] [ T0] CR2: ffff9ee8ad801000 CR3: 0000000e2ce1e000 CR4: 0000000000f50ef0
> [ 0.027131] [ T0] PKRU: 55555554
> [ 0.027131] [ T0] Call Trace:
> [ 0.027132] [ T0] <TASK>
> [ 0.027132] [ T0] free_reserved_area+0x89/0xd0
> [ 0.027133] [ T0] alternative_instructions+0xee/0x110
> [ 0.027136] [ T0] arch_cpu_finalize_init+0x10f/0x160
> [ 0.027138] [ T0] start_kernel+0x686/0x710
> [ 0.027140] [ T0] x86_64_start_reservations+0x24/0x30
> [ 0.027141] [ T0] x86_64_start_kernel+0xd4/0xe0
> [ 0.027142] [ T0] common_startup_64+0x13e/0x141
> [ 0.027143] [ T0] </TASK>
> [ 0.027144] [ T0] ---[ end trace 0000000000000000 ]---
Does this patch fix it for you?
diff --git a/arch/x86/kernel/alternative.c b/arch/x86/kernel/alternative.c
index e87da25d1236..62936a3bde19 100644
--- a/arch/x86/kernel/alternative.c
+++ b/arch/x86/kernel/alternative.c
@@ -2448,19 +2448,31 @@ void __init alternative_instructions(void)
__smp_locks, __smp_locks_end,
_text, _etext);
}
+#endif
+ restart_nmi();
+ alternatives_patched = 1;
+
+ alt_reloc_selftest();
+}
+
+#ifdef CONFIG_SMP
+/*
+ * With CONFIG_DEFERRED_STRUCT_PAGE_INIT enabled we can free_init_pages() only
+ * after the deferred initialization of the memory map is complete.
+ */
+static int __init free_smp_locks(void)
+{
if (!uniproc_patched || num_possible_cpus() == 1) {
free_init_pages("SMP alternatives",
(unsigned long)__smp_locks,
(unsigned long)__smp_locks_end);
}
-#endif
- restart_nmi();
- alternatives_patched = 1;
-
- alt_reloc_selftest();
+ return 0;
}
+arch_initcall(free_smp_locks);
+#endif
/**
* text_poke_early - Update instructions on a live kernel at boot time
> Bert Karwatzki
--
Sincerely yours,
Mike.
^ permalink raw reply related
* Re: [PATCH 01/12] dt-bindings: i3c: Add mipi-i3c-static-method to support SETAASA
From: Alexandre Belloni @ 2026-03-27 17:06 UTC (permalink / raw)
To: Akhil R
Cc: Frank.Li, acpica-devel, conor+dt, conor, devicetree, ebiggers,
fredrik.markstrom, jonathanh, krzk+dt, lenb, linux-acpi,
linux-hwmon, linux-i3c, linux-kernel, linux-tegra, linux,
miquel.raynal, p.zabel, rafael, robert.moore, robh, smangipudi,
thierry.reding
In-Reply-To: <20260327114204.46556-1-akhilrajeev@nvidia.com>
On 27/03/2026 17:12:04+0530, Akhil R wrote:
> On Fri, 27 Mar 2026 09:27:21 +0100, Alexandre Belloni wrote:
> > On 27/03/2026 13:48:58+0530, Akhil R wrote:
> >> On Thu, 26 Mar 2026 16:44:31 +0100, Alexandre Belloni wrote:
> >> > On 26/03/2026 10:05:03-0500, Rob Herring wrote:
> >> >> On Wed, Mar 18, 2026 at 05:31:50PM +0000, Conor Dooley wrote:
> >> >> > On Wed, Mar 18, 2026 at 10:57:14PM +0530, Akhil R wrote:
> >> >> > > Add the 'mipi-i3c-static-method' property mentioned in the MIPI I3C
> >> >> > > Discovery and Configuration Specification [1] to specify which discovery
> >> >> > > method an I3C device supports during bus initialization. The property is
> >> >> > > a bitmap, where a bit value of 1 indicates support for that method, and 0
> >> >> > > indicates lack of support.
> >> >> > > Bit 0: SETDASA CCC (Direct)
> >> >> > > Bit 1: SETAASA CCC (Broadcast)
> >> >> > > Bit 2: Other CCC (vendor / standards extension)
> >> >> > > All other bits are reserved.
> >> >> > >
> >> >> > > It is specifically needed when an I3C device requires SETAASA for the
> >> >> > > address assignment. SETDASA will be supported by default if this property
> >> >> > > is absent - which means for now the property just serves as a flag to
> >> >> > > enable SETAASA, but keep the property as a bitmap to align with the
> >> >> > > specifications.
> >> >> > >
> >> >> > > [1] https://www.mipi.org/specifications/disco
> >> >> > >
> >> >> > > Signed-off-by: Akhil R <akhilrajeev@nvidia.com>
> >> >> > > ---
> >> >> > > .../devicetree/bindings/i3c/i3c.yaml | 30 ++++++++++++++++---
> >> >> > > 1 file changed, 26 insertions(+), 4 deletions(-)
> >> >> > >
> >> >> > > diff --git a/Documentation/devicetree/bindings/i3c/i3c.yaml b/Documentation/devicetree/bindings/i3c/i3c.yaml
> >> >> > > index e25fa72fd785..1705d90d4d79 100644
> >> >> > > --- a/Documentation/devicetree/bindings/i3c/i3c.yaml
> >> >> > > +++ b/Documentation/devicetree/bindings/i3c/i3c.yaml
> >> >> > > @@ -31,10 +31,12 @@ properties:
> >> >> > > described in the device tree, which in turn means we have to describe
> >> >> > > I3C devices.
> >> >> > >
> >> >> > > - Another use case for describing an I3C device in the device tree is when
> >> >> > > - this I3C device has a static I2C address and we want to assign it a
> >> >> > > - specific I3C dynamic address before the DAA takes place (so that other
> >> >> > > - devices on the bus can't take this dynamic address).
> >> >> > > + Other use-cases for describing an I3C device in the device tree are:
> >> >> > > + - When the I3C device has a static I2C address and we want to assign
> >> >> > > + it a specific I3C dynamic address before the DAA takes place (so
> >> >> > > + that other devices on the bus can't take this dynamic address).
> >> >> > > + - When the I3C device requires SETAASA for its discovery and uses a
> >> >> > > + pre-defined static address.
> >> >> > >
> >> >> > > "#size-cells":
> >> >> > > const: 0
> >> >> > > @@ -147,6 +149,26 @@ patternProperties:
> >> >> > > through SETDASA. If static address is not present, this address is assigned
> >> >> > > through SETNEWDA after assigning a temporary address via ENTDAA.
> >> >> > >
> >> >> > > + mipi-i3c-static-method:
> >> >> > > + $ref: /schemas/types.yaml#/definitions/uint32
> >> >> > > + minimum: 0x1
> >> >> > > + maximum: 0xff
> >> >> > > + default: 1
> >> >> > > + description: |
> >> >> > > + Bitmap describing which methods of Dynamic Address Assignment from a
> >> >> > > + static address are supported by this I3C Target. A bit value of 1
> >> >> > > + indicates support for that method, and 0 indicates lack of support.
> >> >> >
> >> >> > I really am not keen on properties that are bitmaps, why can't we just
> >> >> > use the strings "setdasa", "setaasa" etc?
> >> >>
> >> >> If this comes from a specification, then I'd tend to just copy it rather
> >> >> than invent our own thing. Obviously if is something structured
> >> >> fundamentally different from how DT is designed, then we wouldn't. But
> >> >> this is just a simple property.
> >> >>
> >> >
> >> > The issue being that the specification is not public so it is difficult
> >> > to take any decision.
> >>
> >> There is a public version available in the same link, but you would still
> >> have to provide them a name and an email ID. The document will be sent to
> >> the mail ID.
> >>
> >
> > The public version only contains one property:
> > mipi-disco-interface-revision
>
> Could you check once if the below link works?
> https://www.mipi.org/mipi-disco-for-i3c-download
It works, thanks. The bitfield is fine then.
>
> Best Regards,
> Akhil
--
Alexandre Belloni, co-owner and COO, Bootlin
Embedded Linux and Kernel engineering
https://bootlin.com
^ permalink raw reply
* Re: [PATCH v4 3/7] pinctrl: extract pinctrl_generic_to_map() from pinctrl_generic_pins_function_dt_node_to_map()
From: Frank Li @ 2026-03-27 16:54 UTC (permalink / raw)
To: Conor Dooley
Cc: Peter Rosin, Linus Walleij, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Rafał Miłecki, Sascha Hauer,
Pengutronix Kernel Team, Fabio Estevam, linux-kernel, linux-gpio,
devicetree, imx, linux-arm-kernel, Haibo Chen
In-Reply-To: <20260327-overdrawn-stretch-2311ec39aa58@spud>
On Fri, Mar 27, 2026 at 12:09:32AM +0000, Conor Dooley wrote:
> On Wed, Mar 25, 2026 at 07:04:12PM -0400, Frank Li wrote:
> > Refactor pinctrl_generic_pins_function_dt_subnode_to_map() by separating DT
> > parsing logic from map creation. Introduce a new helper
> > pinctrl_generic_to_map() to handle mapping to kernel data structures, while
> > keeping DT property parsing in the subnode function.
> >
> > Improve code structure and enables easier reuse for platforms using
> > different DT properties (e.g. pinmux) without modifying the
> > dt_node_to_map-style callback API. Avoid unnecessary coupling to
> > pinctrl_generic_pins_function_dt_node_to_map(), which provides
> > functionality not needed when the phandle target is unambiguous.
> >
> > Maximize code reuse and provide a cleaner extension point for future
> > pinctrl drivers.
> >
> > Signed-off-by: Frank Li <Frank.Li@nxp.com>
> > ---
> > change in v4
> > - new patch
> > ---
> > drivers/pinctrl/pinconf.h | 18 ++++++++
> > drivers/pinctrl/pinctrl-generic.c | 91 ++++++++++++++++++++++++---------------
> > 2 files changed, 74 insertions(+), 35 deletions(-)
> >
> > diff --git a/drivers/pinctrl/pinconf.h b/drivers/pinctrl/pinconf.h
> > index 2880adef476e68950ffdd540ea42cdee6a16ec27..ffdabddb9660324ed8886a2e8dcacff7e1c6c529 100644
> > --- a/drivers/pinctrl/pinconf.h
> > +++ b/drivers/pinctrl/pinconf.h
> > @@ -166,6 +166,13 @@ int pinctrl_generic_pins_function_dt_node_to_map(struct pinctrl_dev *pctldev,
> > struct device_node *np,
> > struct pinctrl_map **maps,
> > unsigned int *num_maps);
> > +
> > +int
> > +pinctrl_generic_to_map(struct pinctrl_dev *pctldev, struct device_node *parent,
> > + struct device_node *np, struct pinctrl_map **maps,
> > + unsigned int *num_maps, unsigned int *num_reserved_maps,
> > + const char **group_name, unsigned int ngroups,
> > + const char **functions, unsigned int *pins);
> > #else
> > static inline int
> > pinctrl_generic_pins_function_dt_node_to_map(struct pinctrl_dev *pctldev,
> > @@ -175,4 +182,15 @@ pinctrl_generic_pins_function_dt_node_to_map(struct pinctrl_dev *pctldev,
> > {
> > return -ENOTSUPP;
> > }
> > +
> > +static inline int
> > +pinctrl_generic_to_map(struct pinctrl_dev *pctldev, struct device_node *parent,
> > + struct device_node *np, struct pinctrl_map **maps,
> > + unsigned int *num_maps, unsigned int *num_reserved_maps,
> > + const char **group_name, unsigned int ngroups,
> > + const char **functions, unsigned int *pins,
> > + void *function_data)
> > +{
> > + return -ENOTSUPP;
> > +}
> > #endif
> > diff --git a/drivers/pinctrl/pinctrl-generic.c b/drivers/pinctrl/pinctrl-generic.c
> > index efb39c6a670331775855efdc8566102b5c6202ef..20a216ae63e91b69985ea4cfcd0b57103c6ca950 100644
> > --- a/drivers/pinctrl/pinctrl-generic.c
> > +++ b/drivers/pinctrl/pinctrl-generic.c
> > @@ -17,29 +17,18 @@
> > #include "pinctrl-utils.h"
> > #include "pinmux.h"
> >
> > -static int pinctrl_generic_pins_function_dt_subnode_to_map(struct pinctrl_dev *pctldev,
> > - struct device_node *parent,
> > - struct device_node *np,
> > - struct pinctrl_map **maps,
> > - unsigned int *num_maps,
> > - unsigned int *num_reserved_maps,
> > - const char **group_names,
> > - unsigned int ngroups)
> > +int
> > +pinctrl_generic_to_map(struct pinctrl_dev *pctldev, struct device_node *parent,
> > + struct device_node *np, struct pinctrl_map **maps,
> > + unsigned int *num_maps, unsigned int *num_reserved_maps,
> > + const char **group_names, unsigned int ngroups,
> > + const char **functions, unsigned int *pins)
>
> npins needs to be an argument to this function also, otherwise
> pinctrl_generic_add_group() uses it uninitialised...
Is this one the root cause of then broken? I am not sure why compiler have
not report waring for it.
Frank
> >
^ permalink raw reply
* Re: [PATCH v1 1/1] arm64: dts: imx91-var-dart-sonata: add RGB select supply for PCA6408
From: Frank Li @ 2026-03-27 16:51 UTC (permalink / raw)
To: Stefano Radaelli
Cc: linux-kernel, devicetree, imx, linux-arm-kernel, pierluigi.p,
Stefano Radaelli, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Sascha Hauer, Pengutronix Kernel Team, Fabio Estevam
In-Reply-To: <20260327163243.17334-1-stefano.r@variscite.com>
On Fri, Mar 27, 2026 at 05:32:43PM +0100, Stefano Radaelli wrote:
> From: Stefano Radaelli <stefano.r@variscite.com>
>
> RGB_SEL controls the routing of some carrier board lines on the Sonata
> board. The two PCA6408 GPIO expanders depend on that path being enabled,
> so describe the selector as a fixed regulator and use it as their
> vcc-supply.
Does below resolve your problem?
https://lore.kernel.org/imx/20260325-pinctrl-mux-v4-0-043c2c82e623@nxp.com/
So needn't hack select as regualtor
Frank
>
> Signed-off-by: Stefano Radaelli <stefano.r@variscite.com>
> ---
> arch/arm64/boot/dts/freescale/imx91-var-dart-sonata.dts | 9 +++++++++
> 1 file changed, 9 insertions(+)
>
> diff --git a/arch/arm64/boot/dts/freescale/imx91-var-dart-sonata.dts b/arch/arm64/boot/dts/freescale/imx91-var-dart-sonata.dts
> index afa39dab240a..3b5816884f24 100644
> --- a/arch/arm64/boot/dts/freescale/imx91-var-dart-sonata.dts
> +++ b/arch/arm64/boot/dts/freescale/imx91-var-dart-sonata.dts
> @@ -90,6 +90,13 @@ reg_vref_1v8: regulator-adc-vref {
> regulator-max-microvolt = <1800000>;
> };
>
> + reg_rgb_sel: regulator-rgb-sel {
> + compatible = "regulator-fixed";
> + regulator-name = "rgb-select";
> + gpio = <&pca9534 7 GPIO_ACTIVE_HIGH>;
> + enable-active-high;
> + };
> +
> reg_usdhc2_vmmc: regulator-vmmc-usdhc2 {
> compatible = "regulator-fixed";
> pinctrl-names = "default";
> @@ -195,6 +202,7 @@ pca6408_1: gpio@20 {
> #gpio-cells = <2>;
> interrupt-parent = <&gpio1>;
> interrupts = <10 IRQ_TYPE_LEVEL_LOW>;
> + vcc-supply = <®_rgb_sel>;
> };
>
> pca6408_2: gpio@21 {
> @@ -204,6 +212,7 @@ pca6408_2: gpio@21 {
> #gpio-cells = <2>;
> interrupt-parent = <&gpio1>;
> interrupts = <10 IRQ_TYPE_LEVEL_LOW>;
> + vcc-supply = <®_rgb_sel>;
> };
>
> pca9534: gpio@22 {
> --
> 2.47.3
>
^ permalink raw reply
* [PATCH v4 5/5] dt-bindings: usb: atmel,at91sam9rl-udc: convert to DT schema
From: Charan Pedumuru @ 2026-03-27 16:47 UTC (permalink / raw)
To: Greg Kroah-Hartman, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Claudiu Beznea, Herve Codina, Nicolas Ferre,
Alexandre Belloni
Cc: linux-usb, devicetree, linux-arm-kernel, linux-kernel,
Charan Pedumuru
In-Reply-To: <20260327-atmel-usb-v4-0-eb8b6e49b29d@gmail.com>
Convert Atmel High-Speed USB Device Controller (USBA) binding to DT schema.
Changes during conversion:
- Make the "clock-names" property flexible enough to accept the items
in any order as the existing in tree DTS nodes doesn't follow an order.
Signed-off-by: Charan Pedumuru <charan.pedumuru@gmail.com>
---
.../bindings/usb/atmel,at91sam9rl-udc.yaml | 74 ++++++++++++++++++++++
.../devicetree/bindings/usb/atmel-usb.txt | 46 --------------
2 files changed, 74 insertions(+), 46 deletions(-)
diff --git a/Documentation/devicetree/bindings/usb/atmel,at91sam9rl-udc.yaml b/Documentation/devicetree/bindings/usb/atmel,at91sam9rl-udc.yaml
new file mode 100644
index 000000000000..cdbbd17f8036
--- /dev/null
+++ b/Documentation/devicetree/bindings/usb/atmel,at91sam9rl-udc.yaml
@@ -0,0 +1,74 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/usb/atmel,at91sam9rl-udc.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Atmel High-Speed USB Device Controller (USBA)
+
+maintainers:
+ - Nicolas Ferre <nicolas.ferre@microchip.com>
+ - Alexandre Belloni <alexandre.belloni@bootlin.com>
+
+description:
+ The Atmel High-Speed USB Device Controller (USBA) provides USB 2.0
+ high-speed gadget functionality on several Atmel and Microchip SoCs.
+ The controller requires a peripheral clock and a host clock for operation
+ and may optionally use a GPIO to detect VBUS presence.
+
+properties:
+ compatible:
+ oneOf:
+ - enum:
+ - atmel,at91sam9rl-udc
+ - atmel,at91sam9g45-udc
+ - atmel,sama5d3-udc
+ - items:
+ - const: microchip,lan9662-udc
+ - const: atmel,sama5d3-udc
+ - const: microchip,sam9x60-udc
+
+ reg:
+ maxItems: 2
+
+ interrupts:
+ maxItems: 1
+
+ clocks:
+ maxItems: 2
+
+ clock-names:
+ minItems: 2
+ maxItems: 2
+ items:
+ enum: [pclk, hclk]
+
+ atmel,vbus-gpio:
+ description: GPIO used to detect the presence of VBUS, indicating that
+ the USB cable is connected.
+ maxItems: 1
+
+required:
+ - compatible
+ - reg
+ - interrupts
+ - clocks
+ - clock-names
+
+unevaluatedProperties: false
+
+examples:
+ - |
+ #include <dt-bindings/interrupt-controller/irq.h>
+ #include <dt-bindings/clock/at91.h>
+ #include <dt-bindings/gpio/gpio.h>
+ gadget@fff78000 {
+ compatible = "atmel,at91sam9g45-udc";
+ reg = <0x00600000 0x80000
+ 0xfff78000 0x400>;
+ interrupts = <27 IRQ_TYPE_LEVEL_HIGH 0>;
+ clocks = <&pmc PMC_TYPE_PERIPHERAL 27>, <&pmc PMC_TYPE_CORE PMC_UTMI>;
+ clock-names = "pclk", "hclk";
+ atmel,vbus-gpio = <&pioC 15 GPIO_ACTIVE_HIGH>;
+ };
+...
diff --git a/Documentation/devicetree/bindings/usb/atmel-usb.txt b/Documentation/devicetree/bindings/usb/atmel-usb.txt
deleted file mode 100644
index ab353576d1de..000000000000
--- a/Documentation/devicetree/bindings/usb/atmel-usb.txt
+++ /dev/null
@@ -1,46 +0,0 @@
-Atmel SOC USB controllers
-
-Atmel High-Speed USB device controller
-
-Required properties:
- - compatible: Should be one of the following
- "atmel,at91sam9rl-udc"
- "atmel,at91sam9g45-udc"
- "atmel,sama5d3-udc"
- "microchip,sam9x60-udc"
- "microchip,lan9662-udc"
- For "microchip,lan9662-udc" the fallback "atmel,sama5d3-udc"
- is required.
- - reg: Address and length of the register set for the device
- - interrupts: Should contain usba interrupt
- - clocks: Should reference the peripheral and host clocks
- - clock-names: Should contain two strings
- "pclk" for the peripheral clock
- "hclk" for the host clock
-
-Deprecated property:
- - ep childnode: To specify the number of endpoints and their properties.
-
-Optional properties:
- - atmel,vbus-gpio: If present, specifies a gpio that allows to detect whether
- vbus is present (USB is connected).
-
-Deprecated child node properties:
- - name: Name of the endpoint.
- - reg: Num of the endpoint.
- - atmel,fifo-size: Size of the fifo.
- - atmel,nb-banks: Number of banks.
- - atmel,can-dma: Boolean to specify if the endpoint support DMA.
- - atmel,can-isoc: Boolean to specify if the endpoint support ISOC.
-
-usb2: gadget@fff78000 {
- #address-cells = <1>;
- #size-cells = <0>;
- compatible = "atmel,at91sam9rl-udc";
- reg = <0x00600000 0x80000
- 0xfff78000 0x400>;
- interrupts = <27 4 0>;
- clocks = <&utmi>, <&udphs_clk>;
- clock-names = "hclk", "pclk";
- atmel,vbus-gpio = <&pioB 19 0>;
-};
--
2.53.0
^ permalink raw reply related
* [PATCH v4 4/5] dt-bindings: usb: atmel,at91rm9200-udc: convert to DT schema
From: Charan Pedumuru @ 2026-03-27 16:47 UTC (permalink / raw)
To: Greg Kroah-Hartman, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Claudiu Beznea, Herve Codina, Nicolas Ferre,
Alexandre Belloni
Cc: linux-usb, devicetree, linux-arm-kernel, linux-kernel,
Charan Pedumuru
In-Reply-To: <20260327-atmel-usb-v4-0-eb8b6e49b29d@gmail.com>
Convert Atmel AT91 USB Device Controller (UDC) binding to DT schema.
Changes during conversion:
- Include "atmel,pullup-gpio" and "atmel,matrix" in the properties since
they are required by existing in-tree DTS definitions.
Reviewed-by: Rob Herring (Arm) <robh@kernel.org>
Signed-off-by: Charan Pedumuru <charan.pedumuru@gmail.com>
---
.../bindings/usb/atmel,at91rm9200-udc.yaml | 76 ++++++++++++++++++++++
.../devicetree/bindings/usb/atmel-usb.txt | 28 --------
2 files changed, 76 insertions(+), 28 deletions(-)
diff --git a/Documentation/devicetree/bindings/usb/atmel,at91rm9200-udc.yaml b/Documentation/devicetree/bindings/usb/atmel,at91rm9200-udc.yaml
new file mode 100644
index 000000000000..a4eabb935e6e
--- /dev/null
+++ b/Documentation/devicetree/bindings/usb/atmel,at91rm9200-udc.yaml
@@ -0,0 +1,76 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/usb/atmel,at91rm9200-udc.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Atmel AT91 USB Device Controller (UDC)
+
+maintainers:
+ - Nicolas Ferre <nicolas.ferre@microchip.com>
+ - Alexandre Belloni <alexandre.belloni@bootlin.com>
+
+description:
+ The Atmel AT91 USB Device Controller provides USB gadget (device-mode)
+ functionality on AT91 SoCs. It requires a peripheral clock and an AHB
+ clock for operation and may optionally control VBUS power through a GPIO.
+
+properties:
+ compatible:
+ enum:
+ - atmel,at91rm9200-udc
+ - atmel,at91sam9260-udc
+ - atmel,at91sam9261-udc
+ - atmel,at91sam9263-udc
+
+ reg:
+ maxItems: 1
+
+ interrupts:
+ maxItems: 1
+
+ clocks:
+ maxItems: 2
+
+ clock-names:
+ items:
+ - const: pclk
+ - const: hclk
+
+ atmel,vbus-gpio:
+ description: GPIO used to enable or control VBUS power for the USB bus.
+ maxItems: 1
+
+ atmel,matrix:
+ $ref: /schemas/types.yaml#/definitions/phandle
+ description: Phandle to the Atmel bus matrix controller.
+
+ atmel,pullup-gpio:
+ description:
+ GPIO controlling the USB D+ pull-up resistor used to signal device
+ connection to the host.
+ maxItems: 1
+
+required:
+ - compatible
+ - reg
+ - interrupts
+ - clocks
+ - clock-names
+
+additionalProperties: false
+
+examples:
+ - |
+ #include <dt-bindings/interrupt-controller/irq.h>
+ #include <dt-bindings/clock/at91.h>
+ #include <dt-bindings/gpio/gpio.h>
+ gadget@fffa4000 {
+ compatible = "atmel,at91rm9200-udc";
+ reg = <0xfffa4000 0x4000>;
+ interrupts = <11 IRQ_TYPE_LEVEL_HIGH 2>;
+ clocks = <&udc_clk>, <&udpck>;
+ clock-names = "pclk", "hclk";
+ atmel,vbus-gpio = <&pioC 5 GPIO_ACTIVE_HIGH>;
+ };
+...
diff --git a/Documentation/devicetree/bindings/usb/atmel-usb.txt b/Documentation/devicetree/bindings/usb/atmel-usb.txt
index bf2149e5f0b3..ab353576d1de 100644
--- a/Documentation/devicetree/bindings/usb/atmel-usb.txt
+++ b/Documentation/devicetree/bindings/usb/atmel-usb.txt
@@ -1,33 +1,5 @@
Atmel SOC USB controllers
-AT91 USB device controller
-
-Required properties:
- - compatible: Should be one of the following
- "atmel,at91rm9200-udc"
- "atmel,at91sam9260-udc"
- "atmel,at91sam9261-udc"
- "atmel,at91sam9263-udc"
- - reg: Address and length of the register set for the device
- - interrupts: Should contain macb interrupt
- - clocks: Should reference the peripheral and the AHB clocks
- - clock-names: Should contain two strings
- "pclk" for the peripheral clock
- "hclk" for the AHB clock
-
-Optional properties:
- - atmel,vbus-gpio: If present, specifies a gpio that needs to be
- activated for the bus to be powered.
-
-usb1: gadget@fffa4000 {
- compatible = "atmel,at91rm9200-udc";
- reg = <0xfffa4000 0x4000>;
- interrupts = <10 4>;
- clocks = <&udc_clk>, <&udpck>;
- clock-names = "pclk", "hclk";
- atmel,vbus-gpio = <&pioC 5 0>;
-};
-
Atmel High-Speed USB device controller
Required properties:
--
2.53.0
^ permalink raw reply related
* [PATCH v4 3/5] dt-bindings: usb: generic-ehci: fix schema structure and add at91sam9g45 constraints
From: Charan Pedumuru @ 2026-03-27 16:47 UTC (permalink / raw)
To: Greg Kroah-Hartman, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Claudiu Beznea, Herve Codina, Nicolas Ferre,
Alexandre Belloni
Cc: linux-usb, devicetree, linux-arm-kernel, linux-kernel,
Charan Pedumuru
In-Reply-To: <20260327-atmel-usb-v4-0-eb8b6e49b29d@gmail.com>
Add clock and phy constraints for atmel,at91sam9g45-ehci and reorganize
the allOf section to fix dtbs_check warnings.
Reviewed-by: Rob Herring (Arm) <robh@kernel.org>
Signed-off-by: Charan Pedumuru <charan.pedumuru@gmail.com>
---
.../devicetree/bindings/usb/atmel-usb.txt | 24 -----------
.../devicetree/bindings/usb/generic-ehci.yaml | 46 ++++++++++++++++------
2 files changed, 33 insertions(+), 37 deletions(-)
diff --git a/Documentation/devicetree/bindings/usb/atmel-usb.txt b/Documentation/devicetree/bindings/usb/atmel-usb.txt
index c09685283109..bf2149e5f0b3 100644
--- a/Documentation/devicetree/bindings/usb/atmel-usb.txt
+++ b/Documentation/devicetree/bindings/usb/atmel-usb.txt
@@ -1,29 +1,5 @@
Atmel SOC USB controllers
-EHCI
-
-Required properties:
- - compatible: Should be "atmel,at91sam9g45-ehci" for USB controllers
- used in host mode.
- - reg: Address and length of the register set for the device
- - interrupts: Should contain ehci interrupt
- - clocks: Should reference the peripheral and the UTMI clocks
- - clock-names: Should contain two strings
- "ehci_clk" for the peripheral clock
- "usb_clk" for the UTMI clock
-
-Optional properties:
- - phy_type : For multi port host USB controllers, should be one of
- "utmi", or "hsic".
-
-usb1: ehci@800000 {
- compatible = "atmel,at91sam9g45-ehci", "usb-ehci";
- reg = <0x00800000 0x100000>;
- interrupts = <22 4>;
- clocks = <&utmi>, <&uhphs_clk>;
- clock-names = "usb_clk", "ehci_clk";
-};
-
AT91 USB device controller
Required properties:
diff --git a/Documentation/devicetree/bindings/usb/generic-ehci.yaml b/Documentation/devicetree/bindings/usb/generic-ehci.yaml
index 601f097c09a6..55a5aa7d7a54 100644
--- a/Documentation/devicetree/bindings/usb/generic-ehci.yaml
+++ b/Documentation/devicetree/bindings/usb/generic-ehci.yaml
@@ -9,19 +9,6 @@ title: USB EHCI Controller
maintainers:
- Greg Kroah-Hartman <gregkh@linuxfoundation.org>
-allOf:
- - $ref: usb-hcd.yaml
- - if:
- properties:
- compatible:
- not:
- contains:
- const: ibm,usb-ehci-440epx
- then:
- properties:
- reg:
- maxItems: 1
-
properties:
compatible:
oneOf:
@@ -167,6 +154,39 @@ required:
- reg
- interrupts
+allOf:
+ - $ref: usb-hcd.yaml
+ - if:
+ properties:
+ compatible:
+ not:
+ contains:
+ const: ibm,usb-ehci-440epx
+ then:
+ properties:
+ reg:
+ maxItems: 1
+ - if:
+ properties:
+ compatible:
+ contains:
+ const: atmel,at91sam9g45-ehci
+ then:
+ properties:
+ clock-names:
+ items:
+ - const: usb_clk
+ - const: ehci_clk
+
+ phy_type:
+ enum:
+ - utmi
+ - hsic
+
+ required:
+ - clocks
+ - clock-names
+
unevaluatedProperties: false
examples:
--
2.53.0
^ permalink raw reply related
* [PATCH v4 2/5] dt-bindings: usb: generic-ohci: add AT91RM9200 OHCI binding support
From: Charan Pedumuru @ 2026-03-27 16:47 UTC (permalink / raw)
To: Greg Kroah-Hartman, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Claudiu Beznea, Herve Codina, Nicolas Ferre,
Alexandre Belloni
Cc: linux-usb, devicetree, linux-arm-kernel, linux-kernel,
Charan Pedumuru
In-Reply-To: <20260327-atmel-usb-v4-0-eb8b6e49b29d@gmail.com>
Convert the Atmel AT91RM9200 OHCI USB host controller binding to DT schema
by defining it in the existing generic OHCI schema.
Signed-off-by: Charan Pedumuru <charan.pedumuru@gmail.com>
---
.../devicetree/bindings/usb/atmel-usb.txt | 27 --------------
.../devicetree/bindings/usb/generic-ohci.yaml | 41 ++++++++++++++++++++++
2 files changed, 41 insertions(+), 27 deletions(-)
diff --git a/Documentation/devicetree/bindings/usb/atmel-usb.txt b/Documentation/devicetree/bindings/usb/atmel-usb.txt
index 12183ef47ee4..c09685283109 100644
--- a/Documentation/devicetree/bindings/usb/atmel-usb.txt
+++ b/Documentation/devicetree/bindings/usb/atmel-usb.txt
@@ -1,32 +1,5 @@
Atmel SOC USB controllers
-OHCI
-
-Required properties:
- - compatible: Should be "atmel,at91rm9200-ohci" for USB controllers
- used in host mode.
- - reg: Address and length of the register set for the device
- - interrupts: Should contain ohci interrupt
- - clocks: Should reference the peripheral, host and system clocks
- - clock-names: Should contain three strings
- "ohci_clk" for the peripheral clock
- "hclk" for the host clock
- "uhpck" for the system clock
- - num-ports: Number of ports.
- - atmel,vbus-gpio: If present, specifies a gpio that needs to be
- activated for the bus to be powered.
- - atmel,oc-gpio: If present, specifies a gpio that needs to be
- activated for the overcurrent detection.
-
-usb0: ohci@500000 {
- compatible = "atmel,at91rm9200-ohci", "usb-ohci";
- reg = <0x00500000 0x100000>;
- clocks = <&uhphs_clk>, <&uhphs_clk>, <&uhpck>;
- clock-names = "ohci_clk", "hclk", "uhpck";
- interrupts = <20 4>;
- num-ports = <2>;
-};
-
EHCI
Required properties:
diff --git a/Documentation/devicetree/bindings/usb/generic-ohci.yaml b/Documentation/devicetree/bindings/usb/generic-ohci.yaml
index 961cbf85eeb5..d42f448fa204 100644
--- a/Documentation/devicetree/bindings/usb/generic-ohci.yaml
+++ b/Documentation/devicetree/bindings/usb/generic-ohci.yaml
@@ -55,6 +55,7 @@ properties:
- ti,ohci-omap3
- items:
- enum:
+ - atmel,at91rm9200-ohci
- cavium,octeon-6335-ohci
- nintendo,hollywood-usb-ohci
- nxp,ohci-nxp
@@ -137,6 +138,24 @@ properties:
The associated ISP1301 device. Necessary for the UDC controller for
connecting to the USB physical layer.
+ atmel,vbus-gpio:
+ description:
+ GPIO used to control or sense the USB VBUS power. Each entry
+ represents a VBUS-related GPIO; count and order may vary by hardware.
+ Entries follow standard GPIO specifier format. A value of 0 indicates
+ an unused or unavailable VBUS signal.
+ minItems: 1
+ maxItems: 3
+
+ atmel,oc-gpio:
+ description:
+ GPIO used to signal USB overcurrent condition. Each entry represents
+ an OC detection GPIO; count and order may vary by hardware. Entries
+ follow standard GPIO specifier format. A value of 0 indicates an
+ unused or unavailable OC signal.
+ minItems: 1
+ maxItems: 3
+
required:
- compatible
- reg
@@ -144,6 +163,28 @@ required:
allOf:
- $ref: usb-hcd.yaml
+ - if:
+ properties:
+ compatible:
+ contains:
+ const: atmel,at91rm9200-ohci
+ then:
+ properties:
+ clock-names:
+ items:
+ - const: ohci_clk
+ - const: hclk
+ - const: uhpck
+
+ required:
+ - clocks
+ - clock-names
+
+ else:
+ properties:
+ atmel,vbus-gpio: false
+ atmel,oc-gpio: false
+
- if:
not:
properties:
--
2.53.0
^ permalink raw reply related
* [PATCH v4 1/5] arm: dts: at91: remove unused #address-cells/#size-cells from sam9x60 udc node
From: Charan Pedumuru @ 2026-03-27 16:47 UTC (permalink / raw)
To: Greg Kroah-Hartman, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Claudiu Beznea, Herve Codina, Nicolas Ferre,
Alexandre Belloni
Cc: linux-usb, devicetree, linux-arm-kernel, linux-kernel,
Charan Pedumuru
In-Reply-To: <20260327-atmel-usb-v4-0-eb8b6e49b29d@gmail.com>
The UDC node does not define any child nodes, so the "#address-cells" and
"#size-cells" properties are unnecessary. Remove these unused properties
to simplify the devicetree node and keep it consistent with DT conventions.
Reviewed-by: Claudiu Beznea <claudiu.beznea@tuxon.dev>
Signed-off-by: Charan Pedumuru <charan.pedumuru@gmail.com>
---
arch/arm/boot/dts/microchip/sam9x60.dtsi | 2 --
1 file changed, 2 deletions(-)
diff --git a/arch/arm/boot/dts/microchip/sam9x60.dtsi b/arch/arm/boot/dts/microchip/sam9x60.dtsi
index b075865e6a76..e708b3df4ccd 100644
--- a/arch/arm/boot/dts/microchip/sam9x60.dtsi
+++ b/arch/arm/boot/dts/microchip/sam9x60.dtsi
@@ -75,8 +75,6 @@ ahb {
ranges;
usb0: gadget@500000 {
- #address-cells = <1>;
- #size-cells = <0>;
compatible = "microchip,sam9x60-udc";
reg = <0x00500000 0x100000
0xf803c000 0x400>;
--
2.53.0
^ permalink raw reply related
* [PATCH v4 0/5] dt-bindings: usb: atmel: convert Atmel USB controller bindings to YAML
From: Charan Pedumuru @ 2026-03-27 16:47 UTC (permalink / raw)
To: Greg Kroah-Hartman, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Claudiu Beznea, Herve Codina, Nicolas Ferre,
Alexandre Belloni
Cc: linux-usb, devicetree, linux-arm-kernel, linux-kernel,
Charan Pedumuru
This patch series converts the legacy text-based Device Tree bindings for
Atmel/Microchip USB controllers to DT schema (YAML) format.
Note:
The patch "dt-bindings: usb: atmel,at91sam9rl-udc: convert to DT schema"
depends on the patch "arm: dts: at91: remove unused #address-cells/#size-cells
from sam9x60 UDC node". If the DT schema patch is applied before the DTS
cleanup patch, `dtbs_check` will fail due to the presence of the removed
properties in the existing DTS.
Signed-off-by: Charan Pedumuru <charan.pedumuru@gmail.com>
---
Changes in v4:
- generic-ohci: Modify the commit message and modify description for the
properties "atmel,vbus-gpio" and "atmel,oc-gpio".
- atmel,at91rm9200-udc: Remove minItems for clocks and rename
unevaluatedProperties to additionalProperties.
- atmel,at91sam9rl-udc: Remove minItems for clocks and modify commit
message.
- all: Remove the corresponding text binding node for each patch from
the text binding file.
- Link to v3: https://lore.kernel.org/r/20260307-atmel-usb-v3-0-3dc48fe772be@gmail.com
Changes in v3:
- sam9x60: Add a new patch removing the unnecessary #address-cells and
#size-cells properties from the sam9x60 UDC node.
- atmel,at91sam9rl-udc: Remove #address-cells and #size-cells from the
atmel,at91sam9rl-udc binding properties.
- generic-ohci: Add an else condition to the generic-ohci schema properties
for improved validation precision.
- Link to v2: https://lore.kernel.org/r/20260224-atmel-usb-v2-0-6d6a615c9c47@gmail.com
Changes in v2:
- Drop the separate YAML patches for OHCI and EHCI.
- Add the compatibles "atmel,at91rm9200-ohci" and "atmel,at91sam9g45-ehci"
to the existing generic OHCI and EHCI binding files.
- Link to v1: https://lore.kernel.org/r/20260201-atmel-usb-v1-0-d1a3e93003f1@gmail.com
---
Charan Pedumuru (5):
arm: dts: at91: remove unused #address-cells/#size-cells from sam9x60 udc node
dt-bindings: usb: generic-ohci: add AT91RM9200 OHCI binding support
dt-bindings: usb: generic-ehci: fix schema structure and add at91sam9g45 constraints
dt-bindings: usb: atmel,at91rm9200-udc: convert to DT schema
dt-bindings: usb: atmel,at91sam9rl-udc: convert to DT schema
.../bindings/usb/atmel,at91rm9200-udc.yaml | 76 +++++++++++++
.../bindings/usb/atmel,at91sam9rl-udc.yaml | 74 ++++++++++++
.../devicetree/bindings/usb/atmel-usb.txt | 125 ---------------------
.../devicetree/bindings/usb/generic-ehci.yaml | 46 +++++---
.../devicetree/bindings/usb/generic-ohci.yaml | 41 +++++++
arch/arm/boot/dts/microchip/sam9x60.dtsi | 2 -
6 files changed, 224 insertions(+), 140 deletions(-)
---
base-commit: 3f24e4edcd1b8981c6b448ea2680726dedd87279
change-id: 20260129-atmel-usb-37f89a141e48
Best regards,
--
Charan Pedumuru <charan.pedumuru@gmail.com>
^ permalink raw reply
* Re: [PATCH v4 0/6] media: iris: enable SM8350 and SC8280XP support
From: Steev Klimaszewski @ 2026-03-27 16:46 UTC (permalink / raw)
To: dmitry.baryshkov
Cc: abhinav.kumar, andersson, bod, bryan.odonoghue, conor+dt, david,
devicetree, dikshita.agarwal, johan+linaro, konrad.dybcio,
konradybcio, krzk+dt, linux-arm-msm, linux-kernel, linux-media,
mchehab, robh, stanimir.varbanov, vikash.garodia
In-Reply-To: <20260312-iris-sc8280xp-v4-0-a047ef1e3c7d@oss.qualcomm.com>
Hi Dmitry, all,
I've tested this on my Thinkpad X13s, after disabling venus module otherwise it
loads venus not iris.
In el2, the device seems to be /dev/video33
steev@finn:~$ v4l2-compliance -d 33
v4l2-compliance 1.32.0, 64 bits, 64-bit time_t
Compliance test for iris_driver device /dev/video33:
Driver Info:
Driver name : iris_driver
Card type : Iris Encoder
Bus info : platform:aa00000.video-codec
Driver version : 6.19.10
Capabilities : 0x84204000
Video Memory-to-Memory Multiplanar
Streaming
Extended Pix Format
Device Capabilities
Device Caps : 0x04204000
Video Memory-to-Memory Multiplanar
Streaming
Extended Pix Format
Detected Stateful Encoder
Required ioctls:
test VIDIOC_QUERYCAP: OK
test invalid ioctls: OK
Allow for multiple opens:
test second /dev/video33 open: OK
test VIDIOC_QUERYCAP: OK
test VIDIOC_G/S_PRIORITY: OK
test for unlimited opens: OK
Debug ioctls:
test VIDIOC_DBG_G/S_REGISTER: OK (Not Supported)
test VIDIOC_LOG_STATUS: OK (Not Supported)
Input ioctls:
test VIDIOC_G/S_TUNER/ENUM_FREQ_BANDS: OK (Not Supported)
test VIDIOC_G/S_FREQUENCY: OK (Not Supported)
test VIDIOC_S_HW_FREQ_SEEK: OK (Not Supported)
test VIDIOC_ENUMAUDIO: OK (Not Supported)
test VIDIOC_G/S/ENUMINPUT: OK (Not Supported)
test VIDIOC_G/S_AUDIO: OK (Not Supported)
Inputs: 0 Audio Inputs: 0 Tuners: 0
Output ioctls:
test VIDIOC_G/S_MODULATOR: OK (Not Supported)
test VIDIOC_G/S_FREQUENCY: OK (Not Supported)
test VIDIOC_ENUMAUDOUT: OK (Not Supported)
test VIDIOC_G/S/ENUMOUTPUT: OK (Not Supported)
test VIDIOC_G/S_AUDOUT: OK (Not Supported)
Outputs: 0 Audio Outputs: 0 Modulators: 0
Input/Output configuration ioctls:
test VIDIOC_ENUM/G/S/QUERY_STD: OK (Not Supported)
test VIDIOC_ENUM/G/S/QUERY_DV_TIMINGS: OK (Not Supported)
test VIDIOC_DV_TIMINGS_CAP: OK (Not Supported)
test VIDIOC_G/S_EDID: OK (Not Supported)
Control ioctls:
test VIDIOC_QUERY_EXT_CTRL/QUERYMENU: OK
test VIDIOC_QUERYCTRL: OK
test VIDIOC_G/S_CTRL: OK
test VIDIOC_G/S/TRY_EXT_CTRLS: OK
test VIDIOC_(UN)SUBSCRIBE_EVENT/DQEVENT: OK
test VIDIOC_G/S_JPEGCOMP: OK (Not Supported)
Standard Controls: 33 Private Controls: 0
Format ioctls:
test VIDIOC_ENUM_FMT/FRAMESIZES/FRAMEINTERVALS: OK
test VIDIOC_G/S_PARM: OK
test VIDIOC_G_FBUF: OK (Not Supported)
test VIDIOC_G_FMT: OK
test VIDIOC_TRY_FMT: OK
test VIDIOC_S_FMT: OK
test VIDIOC_G_SLICED_VBI_CAP: OK (Not Supported)
test Cropping: OK
test Composing: OK (Not Supported)
test Scaling: OK (Not Supported)
Codec ioctls:
test VIDIOC_(TRY_)ENCODER_CMD: OK
test VIDIOC_G_ENC_INDEX: OK (Not Supported)
test VIDIOC_(TRY_)DECODER_CMD: OK (Not Supported)
Buffer ioctls:
test VIDIOC_REQBUFS/CREATE_BUFS/QUERYBUF: OK
test CREATE_BUFS maximum buffers: OK
test VIDIOC_REMOVE_BUFS: OK
test VIDIOC_EXPBUF: OK
test Requests: OK (Not Supported)
test blocking wait: OK
Total for iris_driver device /dev/video33: 48, Succeeded: 48, Failed: 0, Warnings: 0
In el1, the device becomes /dev/video0
steev@finn:~$ v4l2-compliance
v4l2-compliance 1.32.0, 64 bits, 64-bit time_t
Compliance test for iris_driver device /dev/video0:
Driver Info:
Driver name : iris_driver
Card type : Iris Decoder
Bus info : platform:aa00000.video-codec
Driver version : 6.19.10
Capabilities : 0x84204000
Video Memory-to-Memory Multiplanar
Streaming
Extended Pix Format
Device Capabilities
Device Caps : 0x04204000
Video Memory-to-Memory Multiplanar
Streaming
Extended Pix Format
Detected Stateful Decoder
Required ioctls:
test VIDIOC_QUERYCAP: OK
test invalid ioctls: OK
Allow for multiple opens:
test second /dev/video0 open: OK
test VIDIOC_QUERYCAP: OK
test VIDIOC_G/S_PRIORITY: OK
test for unlimited opens: OK
Debug ioctls:
test VIDIOC_DBG_G/S_REGISTER: OK (Not Supported)
test VIDIOC_LOG_STATUS: OK (Not Supported)
Input ioctls:
test VIDIOC_G/S_TUNER/ENUM_FREQ_BANDS: OK (Not Supported)
test VIDIOC_G/S_FREQUENCY: OK (Not Supported)
test VIDIOC_S_HW_FREQ_SEEK: OK (Not Supported)
test VIDIOC_ENUMAUDIO: OK (Not Supported)
test VIDIOC_G/S/ENUMINPUT: OK (Not Supported)
test VIDIOC_G/S_AUDIO: OK (Not Supported)
Inputs: 0 Audio Inputs: 0 Tuners: 0
Output ioctls:
test VIDIOC_G/S_MODULATOR: OK (Not Supported)
test VIDIOC_G/S_FREQUENCY: OK (Not Supported)
test VIDIOC_ENUMAUDOUT: OK (Not Supported)
test VIDIOC_G/S/ENUMOUTPUT: OK (Not Supported)
test VIDIOC_G/S_AUDOUT: OK (Not Supported)
Outputs: 0 Audio Outputs: 0 Modulators: 0
Input/Output configuration ioctls:
test VIDIOC_ENUM/G/S/QUERY_STD: OK (Not Supported)
test VIDIOC_ENUM/G/S/QUERY_DV_TIMINGS: OK (Not Supported)
test VIDIOC_DV_TIMINGS_CAP: OK (Not Supported)
test VIDIOC_G/S_EDID: OK (Not Supported)
Control ioctls:
test VIDIOC_QUERY_EXT_CTRL/QUERYMENU: OK
test VIDIOC_QUERYCTRL: OK
test VIDIOC_G/S_CTRL: OK
test VIDIOC_G/S/TRY_EXT_CTRLS: OK
test VIDIOC_(UN)SUBSCRIBE_EVENT/DQEVENT: OK
test VIDIOC_G/S_JPEGCOMP: OK (Not Supported)
Standard Controls: 2 Private Controls: 0
Format ioctls:
test VIDIOC_ENUM_FMT/FRAMESIZES/FRAMEINTERVALS: OK
test VIDIOC_G/S_PARM: OK (Not Supported)
test VIDIOC_G_FBUF: OK (Not Supported)
test VIDIOC_G_FMT: OK
test VIDIOC_TRY_FMT: OK
test VIDIOC_S_FMT: OK
test VIDIOC_G_SLICED_VBI_CAP: OK (Not Supported)
test Cropping: OK
test Composing: OK
test Scaling: OK (Not Supported)
Codec ioctls:
test VIDIOC_(TRY_)ENCODER_CMD: OK (Not Supported)
test VIDIOC_G_ENC_INDEX: OK (Not Supported)
test VIDIOC_(TRY_)DECODER_CMD: OK
Buffer ioctls:
test VIDIOC_REQBUFS/CREATE_BUFS/QUERYBUF: OK
test CREATE_BUFS maximum buffers: OK
test VIDIOC_REMOVE_BUFS: OK
test VIDIOC_EXPBUF: OK
test Requests: OK (Not Supported)
test blocking wait: OK
Total for iris_driver device /dev/video0: 48, Succeeded: 48, Failed: 0, Warnings: 0
So the compliance tests pass in both el1 and el2, however, if I attempt to play
a video in totem, and then skip forward in it some random amount of time, totem
freezes and I get the following splat:
[ 143.388380] arm-smmu 15000000.iommu: Unhandled context fault: fsr=0x402, iova=0xd516d400, fsynr=0x600002, cbfrsynra=0x2a00, cb=6
[ 143.388390] arm-smmu 15000000.iommu: FSR = 00000402 [Format=2 TF], SID=0x2a00
[ 143.388392] arm-smmu 15000000.iommu: FSYNR0 = 00600002 [S1CBNDX=96 PLVL=2]
[ 143.388423] qcom-iris aa00000.video-codec: sys error (type: 1, session id:ff, data1:1, data2:deadbead)
[ 145.913827] qcom-iris aa00000.video-codec: session error for command: 0, event id:1004, session id:e39bc002
[ 146.945692] qcom-iris aa00000.video-codec: session error for command: 0, event id:1004, session id:e39bc002
[ 147.969651] qcom-iris aa00000.video-codec: session error for command: 0, event id:1004, session id:e39bc002
[ 147.969693] qcom-iris aa00000.video-codec: session error for command: 0, event id:1004, session id:e39bc002
[ 147.970493] qcom-iris aa00000.video-codec: session error for command: 0, event id:1004, session id:e39bc002
[ 148.993683] ------------[ cut here ]------------
[ 148.993695] WARNING: drivers/media/common/videobuf2/videobuf2-core.c:1827 at vb2_start_streaming+0xe0/0x17c [videobuf2_common], CPU#3: totem/7330
[ 148.993727] Modules linked in: michael_mic uhid
[ 148.993732] qcom-iris aa00000.video-codec: session error for command: 0, event id:1004, session id:e39bc002
[ 148.993736] algif_hash
[ 148.993739] algif_skcipher af_alg snd_soc_wsa883x q6prm_clocks q6apm_lpass_dais q6apm_dai snd_q6dsp_common q6prm overlay zram lz4hc_compress zsmalloc lz4_compress binfmt_misc qrtr_mhi ath11k_pci ath11k mac80211 libarc4 sha256 cfg80211 mhi hci_uart btqca btbcm bluetooth ecdh_generic ecc rfkill qcom_spmi_temp_alarm snd_soc_sc8280xp lenovo_thinkpad_t14s snd_soc_qcom_sdw snd_soc_qcom_common sparse_keymap qcom_spmi_adc_tm5 qcom_spmi_adc5 qcom_vadc_common ov5675 snd_soc_hdmi_codec qcom_camss qcom_iris videobuf2_dma_contig videobuf2_dma_sg v4l2_mem2mem snd_soc_wcd938x videobuf2_memops videobuf2_v4l2 v4l2_fwnode videobuf2_common v4l2_async snd_soc_lpass_rx_macro snd_soc_lpass_tx_macro snd_soc_lpass_wsa_macro snd_soc_lpass_va_macro snd_soc_wcd938x_sdw regmap_sdw snd_soc_wcd_mbhc videodev snd_soc_wcd_common soundwire_qcom snd_q6apm snd_soc_wcd_classh snd_soc_lpass_macro_common mc snd_soc_core snd_compress snd_pcm_dmaengine snd_pcm fastrpc slimbus pci_pwrctrl_pwrseq snd_timer qcom_rng snd soundcore soundwire_bus fuse nfnetlink
[ 148.993936] ipv6 btrfs xor xor_neon raid6_pq zstd_compress panel_edp hid_multitouch qcom_pm8008_regulator nvme ucsi_glink pmic_glink_altmode qcom_battmgr aux_hpd_bridge typec_ucsi nvme_core qcom_pm8008 i2c_hid_of_elan i2c_hid_of rpmsg_ctrl apr i2c_hid rpmsg_char qrtr_smd qcom_pd_mapper msm ubwc_config ocmem drm_gpuvm drm_exec gpu_sched drm_display_helper phy_qcom_qmp_combo cec leds_qcom_lpg aux_bridge drm_dp_aux_bus led_class_multicolor qcom_pbs rtc_pm8xxx qcom_pon qcom_stats dispcc_sc8280xp drm_client_lib phy_qcom_edp camcc_sc8280xp i2c_qcom_cci i2c_qcom_geni drm_kms_helper phy_qcom_qmp_usb qcom_refgen_regulator videocc_sm8350 llcc_qcom drm icc_bwmon phy_qcom_snps_femto_v2 gpi gpucc_sc8280xp qrtr qcom_q6v5_pas qcom_pil_info qcom_common qcom_glink_smem lpasscc_sc8280xp pinctrl_sc8280xp_lpass_lpi pinctrl_lpass_lpi qcom_q6v5 pmic_glink qcom_sysmon gpio_sbu_mux phy_qcom_qmp_pcie mdt_loader pdr_interface qcom_wdt icc_osm_l3 qcom_pdr_msg pwrseq_qcom_wcn typec qmi_helpers pwrseq_core socinfo pwm_bl backlight
[ 148.994147] CPU: 3 UID: 1000 PID: 7330 Comm: totem Not tainted 6.19.10 #2 PREEMPT
[ 148.994155] Hardware name: LENOVO 21BX0015US/21BX0015US, BIOS N3HET94W (1.66 ) 09/15/2025
[ 148.994160] pstate: 80401005 (Nzcv daif +PAN -UAO -TCO -DIT +SSBS BTYPE=--)
[ 148.994166] pc : vb2_start_streaming+0xe0/0x17c [videobuf2_common]
[ 148.994178] lr : vb2_start_streaming+0x70/0x17c [videobuf2_common]
[ 148.994187] sp : ffff80009b95bb70
[ 148.994191] x29: ffff80009b95bb70 x28: 0000000000000001 x27: ffff80009b95bce8
[ 148.994201] x26: 0000000000000000 x25: 0000000000000000 x24: ffffd3da9782e138
[ 148.994211] x23: ffff0000902f5cc0 x22: 0000000040045612 x21: ffff0000f7ada910
[ 148.994219] x20: ffff0000f7ada928 x19: 00000000fffffff0 x18: 0000000000000000
[ 148.994228] x17: 0000000000000000 x16: ffffd3daf7056f00 x15: 0000ffff1802f330
[ 148.994237] x14: 0000000000000000 x13: 0000000000000000 x12: 0000000000000000
[ 148.994246] x11: 0000000000000000 x10: 0000000000000000 x9 : 0000000000000035
[ 148.994255] x8 : ffff80009b95bd68 x7 : ffffd3da976bafc0 x6 : 0000000000000012
[ 148.994263] x5 : ffff0000f7ada948 x4 : 0000000000000000 x3 : 0000000000000005
[ 148.994272] x2 : ffff0000c1b82080 x1 : ffff0000c1b82080 x0 : ffff000084517a30
[ 148.994282] Call trace:
[ 148.994285] vb2_start_streaming+0xe0/0x17c [videobuf2_common] (P)
[ 148.994298] vb2_core_streamon+0xd8/0x1bc [videobuf2_common]
[ 148.994308] vb2_streamon+0x18/0x60 [videobuf2_v4l2]
[ 148.994318] v4l2_m2m_ioctl_streamon+0x5c/0xa0 [v4l2_mem2mem]
[ 148.994330] v4l_streamon+0x24/0x30 [videodev]
[ 148.994362] __video_do_ioctl+0x340/0x3ec [videodev]
[ 148.994389] video_usercopy+0x2b0/0x74c [videodev]
[ 148.994415] video_ioctl2+0x18/0x34 [videodev]
[ 148.994441] v4l2_ioctl+0x40/0x60 [videodev]
[ 148.994467] __arm64_sys_ioctl+0xa4/0xf4
[ 148.994480] invoke_syscall.constprop.0+0x40/0xf0
[ 148.994495] el0_svc_common.constprop.0+0x38/0xd8
[ 148.994504] do_el0_svc+0x1c/0x28
[ 148.994513] el0_svc+0x34/0x104
[ 148.994525] el0t_64_sync_handler+0xa0/0xe4
[ 148.994534] el0t_64_sync+0x198/0x19c
[ 148.994543] ---[ end trace 0000000000000000 ]---
This is much better than the previous venus patchset where accessing the
hardware decoding would cause the machine to hard reset at least!
-- steev
^ permalink raw reply
* Re: [PATCH] dt-bindings: timer: renesas,rz-mtu3: Use #pwm-cells = <3>
From: u.kleine-koenig @ 2026-03-27 16:46 UTC (permalink / raw)
To: Cosmin-Gabriel Tanislav
Cc: Biju Das, Daniel Lezcano, Thomas Gleixner, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Geert Uytterhoeven,
magnus.damm, linux-iio@vger.kernel.org, linux-pwm@vger.kernel.org,
linux-renesas-soc@vger.kernel.org, devicetree@vger.kernel.org
In-Reply-To: <TYRPR01MB1561945B6057A2ABFCF25C6788549A@TYRPR01MB15619.jpnprd01.prod.outlook.com>
[-- Attachment #1: Type: text/plain, Size: 1470 bytes --]
Hello,
On Wed, Mar 25, 2026 at 01:24:40PM +0000, Cosmin-Gabriel Tanislav wrote:
> Sorry for replying to an old patch, but the context is relevant to my
> question.
>
> I'm working on adding MTU3 support for a new platform, Renesas RZ/T2H.
>
> From this patch, it is clear that new platforms should use
> #pwm-cells = <3>; as <2> is deprecated.
>
> What I would like to clarify is whether existing platforms are also
> expected to be migrated from #pwm-cells = <2> to #pwm-cells = <3>.
>
> My understanding is that changing the provider to #pwm-cells = <3> would
> break existing consumers that still specify only two cells in their PWM
> specifiers, since those references would then fail the checks inside
> of_phandle_iterator_next().
The conversion doesn't break dtbs, because in a single dtb consumer and
PWM device are consistent. So from my POV updating is fine.
> There are no existing consumers in-tree, but there might be out-of-tree
> ones that depend on #pwm-cells = <2>;.
That might happen for out-of-tree dts files that include the SoC's
.dtsi. IMHO it's ok to break these.
> If we're okay with the ABI breakage I can proceed with the changes for
> existing platforms too.
The reason that made me stop continuing the conversion myself is the
somewhat stuck discussion at
https://patchwork.ozlabs.org/project/linux-pwm/patch/crk42dsypmbyqk7avldghjq32vslmalfmmouwxzgtdci4agfhz@rkbmxj5z22fx/
.
Best regards
Uwe
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 488 bytes --]
^ permalink raw reply
* [PATCH v7 2/6] mfd: add NXP MC33978/MC34978 core driver
From: Oleksij Rempel @ 2026-03-27 16:34 UTC (permalink / raw)
To: Guenter Roeck, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Lee Jones, Peter Rosin, Linus Walleij
Cc: Oleksij Rempel, kernel, linux-kernel, devicetree, linux-hwmon,
linux-gpio, David Jander
In-Reply-To: <20260327163450.3287313-1-o.rempel@pengutronix.de>
Add core Multi-Function Device (MFD) driver for the NXP MC33978 and
MC34978 Multiple Switch Detection Interfaces (MSDI).
The MC33978/MC34978 devices provide 22 switch detection inputs, analog
multiplexing (AMUX), and comprehensive hardware fault detection.
This core driver handles:
- SPI communications via a custom regmap bus to support the device's
pipelined two-frame MISO response requirement.
- Power sequencing for the VDDQ (logic) and VBATP (battery) regulators.
- Interrupt demultiplexing, utilizing an irq_domain to provide 22 virtual
IRQs for switch state changes and 1 virtual IRQ for hardware faults.
- Inline status harvesting from the SPI MSB to detect and trigger events
without requiring dedicated status register polling.
Child devices (pinctrl, hwmon, mux) are instantiated by the core driver
from match data.
Signed-off-by: Oleksij Rempel <o.rempel@pengutronix.de>
---
changes v7:
- Fix event handling race condition with smp_mb()
- Replace INIT_WORK() with devm_work_autocancel()
changes v6:
- Remove the hardcoded bypass in irq_set_type to allow child drivers to
configure the FAULT line for edge-triggering.
- Implement software edge-detection for FAULT interrupt.
- Add MC33978_FAULT_ALARM_MASK to the shared header for child devices
- Use READ_ONCE() and WRITE_ONCE() for lockless shared state variables
(cached_pin_mask, irq_rise, irq_fall, bus_fault_active,
cached_fault_active) accessed across the SPI harvesting context and
the event worker.
- Add an if (hwirq < MC33978_NUM_PINS) guard in irq_mask() and
irq_unmask() to prevent the FAULT hwirq (22) from altering the
physical pin mask registers.
- Lowercase the error strings in dev_err_probe()
- Add inline comments explaining the irq_map fallback behavior
changes v5:
- no changes
changes v4:
- Removed .of_compatible strings from the mfd_cell arrays
changes v3:
- Select IRQ_DOMAIN_HIERARCHY in Kconfig
- Add .alloc and .free callbacks to irq_domain_ops to support hierarchical
IRQ domains
- Set IRQ_DOMAIN_FLAG_HIERARCHY flag on the core MFD irq_domain
- replace manual lock/unlock with guard()
changes v2:
- Rewrite the driver header comment
- Explicitly reject IRQ_TYPE_LEVEL_HIGH and IRQ_TYPE_LEVEL_LOW in
mc33978_irq_set_type() to correctly reflect the hardware's edge-only
interrupt capabilities.
- Pass the hardware fault IRQ to the hwmon child driver via mfd_cell
resources, rather than requiring the child to parse the parent's irq_domain.
- Ensure the Kconfig strictly depends on OF and SPI
---
drivers/mfd/Kconfig | 15 +
drivers/mfd/Makefile | 2 +
drivers/mfd/mc33978.c | 933 ++++++++++++++++++++++++++++++++++++
include/linux/mfd/mc33978.h | 92 ++++
4 files changed, 1042 insertions(+)
create mode 100644 drivers/mfd/mc33978.c
create mode 100644 include/linux/mfd/mc33978.h
diff --git a/drivers/mfd/Kconfig b/drivers/mfd/Kconfig
index 7192c9d1d268..6dc9554822c9 100644
--- a/drivers/mfd/Kconfig
+++ b/drivers/mfd/Kconfig
@@ -2566,6 +2566,21 @@ config MFD_UPBOARD_FPGA
To compile this driver as a module, choose M here: the module will be
called upboard-fpga.
+config MFD_MC33978
+ tristate "NXP MC33978/MC34978 industrial input controller core"
+ depends on OF
+ depends on SPI
+ select IRQ_DOMAIN_HIERARCHY
+ select MFD_CORE
+ select REGMAP
+ help
+ Support for the NXP MC33978/MC34978 industrial input controllers
+ using the SPI interface.
+
+ This driver provides common support for accessing the device.
+ Additional drivers must be enabled in order to use the functionality
+ of the device.
+
config MFD_MAX7360
tristate "Maxim MAX7360 I2C IO Expander"
depends on I2C
diff --git a/drivers/mfd/Makefile b/drivers/mfd/Makefile
index e75e8045c28a..dcd99315f683 100644
--- a/drivers/mfd/Makefile
+++ b/drivers/mfd/Makefile
@@ -122,6 +122,8 @@ obj-$(CONFIG_MFD_MC13XXX) += mc13xxx-core.o
obj-$(CONFIG_MFD_MC13XXX_SPI) += mc13xxx-spi.o
obj-$(CONFIG_MFD_MC13XXX_I2C) += mc13xxx-i2c.o
+obj-$(CONFIG_MFD_MC33978) += mc33978.o
+
obj-$(CONFIG_MFD_PF1550) += pf1550.o
obj-$(CONFIG_MFD_NCT6694) += nct6694.o
diff --git a/drivers/mfd/mc33978.c b/drivers/mfd/mc33978.c
new file mode 100644
index 000000000000..01bfe715e249
--- /dev/null
+++ b/drivers/mfd/mc33978.c
@@ -0,0 +1,933 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (C) 2024 David Jander <david@protonic.nl>, Protonic Holland
+ * Copyright (C) 2026 Oleksij Rempel <kernel@pengutronix.de>, Pengutronix
+ *
+ * MC33978/MC34978 Multiple Switch Detection Interface - MFD Core Driver
+ *
+ * Driver Architecture:
+ * This is the core MFD driver handling the physical SPI interface, power
+ * management, and central interrupt routing. It instantiates the following
+ * child devices:
+ * - pinctrl: For GPIO read/write and wetting current configuration.
+ * - hwmon: For hardware fault monitoring (tLIM, over/under-voltage).
+ * - mux: For the 24-to-1 analog multiplexer (AMUX).
+ *
+ * Custom SPI Regmap & Event Harvesting:
+ * The device uses a non-standard pipelined SPI protocol where the MISO
+ * response logically lags the MOSI command by one frame. Furthermore, the
+ * hardware embeds volatile global status bits (INT_flg, FAULT_STAT) into the
+ * high byte of almost every SPI response (with specific exceptions handled by
+ * the decoder). This core implements a custom regmap_bus to handle the
+ * 2-frame dummy fetches and transparently "harvests" these status bits in
+ * the background to schedule event processing.
+ *
+ * Interrupt Quirks & Limitations:
+ * - Clear-on-Read: The physical INT_B line is directly tied to the INT_flg
+ * bit. The hardware deasserts INT_B immediately upon *any* SPI transfer
+ * that returns INT_flg. Harvesting this bit from all SPI traffic is the
+ * ONLY way to know this device triggered an interrupt (crucial for shared
+ * IRQ lines).
+ * - Stateless Pin Edge Detection: The hardware lacks per-pin interrupt status
+ * registers. To determine which pin triggered an event, the driver must
+ * read the current pin states and XOR them against a previously cached state.
+ * - Missed Short Pulses: Because pin interrupts are state-derived rather than
+ * hardware-latched, very short physical pulses (shorter than the SPI read
+ * latency) will be missed entirely if the pin reverts to its original state
+ * before the READ_IN register is sampled by the IRQ thread.
+ * - Edge-Only Pin Interrupts: The hardware only asserts INT_B on a state
+ * change. It cannot continuously assert an interrupt while a pin is held at a
+ * specific logic level. Consequently, the driver strictly emulates edge
+ * interrupts (RISING/FALLING) and explicitly rejects LEVEL interrupt
+ * configurations to prevent consumer misalignment.
+ */
+
+#include <linux/array_size.h>
+#include <linux/atomic.h>
+#include <linux/bitfield.h>
+#include <linux/bits.h>
+#include <linux/cache.h>
+#include <linux/cleanup.h>
+#include <linux/device.h>
+#include <linux/devm-helpers.h>
+#include <linux/interrupt.h>
+#include <linux/irq.h>
+#include <linux/irqdomain.h>
+#include <linux/mfd/core.h>
+#include <linux/mod_devicetable.h>
+#include <linux/module.h>
+#include <linux/of_platform.h>
+#include <linux/property.h>
+#include <linux/regmap.h>
+#include <linux/regulator/consumer.h>
+#include <linux/spi/spi.h>
+#include <linux/string.h>
+
+#include <linux/mfd/mc33978.h>
+
+#define MC33978_DRV_NAME "mc33978"
+
+/* Device identification signature returned by CHECK register */
+#define MC33978_CHECK_SIGNATURE 0x123456
+
+/*
+ * Pipelined two-frame SPI transfer:
+ * [REQ] - Transmits command/write-data, receives dummy/previous response
+ * [PIPE] - Transmits dummy CHECK, receives actual response to current command
+ */
+enum mc33978_frame_index {
+ MC33978_FRAME_REQ = 0,
+ MC33978_FRAME_PIPE,
+ MC33978_FRAME_COUNT
+};
+
+/* SPI frame byte offsets (transmitted MSB first) */
+enum mc33978_frame_offset {
+ MC33978_FRAME_CMD = 0,
+ MC33978_FRAME_DATA_HI,
+ MC33978_FRAME_DATA_MID,
+ MC33978_FRAME_DATA_LO
+};
+
+#define MC33978_FRAME_LEN 4
+
+/* Regmap internal value buffer offsets */
+enum mc33978_payload_offset {
+ MC33978_PAYLOAD_HI = 0,
+ MC33978_PAYLOAD_MID,
+ MC33978_PAYLOAD_LO
+};
+
+#define MC33978_PAYLOAD_LEN 3
+
+/*
+ * SPI Command Byte (FRAME_CMD).
+ * Maps to frame bit [24] in the datasheet.
+ */
+#define MC33978_CMD_BYTE_WRITE BIT(0)
+
+/* High Payload Byte Masks (FRAME_DATA_HI / PAYLOAD_HI). */
+#define MC33978_HI_BYTE_STAT_FAULT BIT(7) /* Maps to frame bit [23] */
+#define MC33978_HI_BYTE_STAT_INT BIT(6) /* Maps to frame bit [22] */
+
+#define MC33978_HI_BYTE_STATUS_MASK (MC33978_HI_BYTE_STAT_FAULT | \
+ MC33978_HI_BYTE_STAT_INT)
+
+/* Maps to frame bits [21:16] */
+#define MC33978_HI_BYTE_DATA_MASK GENMASK(5, 0)
+
+#define MC33978_CACHE_SG_PIN_MASK GENMASK(13, 0)
+#define MC33978_CACHE_SP_PIN_MASK GENMASK(21, 14)
+
+#define MC33978_SG_PIN_MASK GENMASK(13, 0)
+#define MC33978_SP_PIN_MASK GENMASK(7, 0)
+
+struct mc33978_data {
+ const struct mfd_cell *cells;
+ int num_cells;
+};
+
+struct mc33978_mfd_priv {
+ struct spi_device *spi;
+ struct regmap *map;
+
+ struct regulator *vddq;
+ struct regulator *vbatp;
+
+ /* Protects event processing loop and hardware state machine */
+ 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;
+
+ /* Protects IRQ mask registers and cached IRQ state */
+ 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;
+
+ bool cached_fault_active;
+ bool bus_fault_active;
+
+ /* 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);
+
+ if (hwirq < MC33978_NUM_PINS)
+ WRITE_ONCE(mc->cached_pin_mask,
+ READ_ONCE(mc->cached_pin_mask) & ~BIT(hwirq));
+}
+
+static void mc33978_irq_unmask(struct irq_data *data)
+{
+ struct mc33978_mfd_priv *mc = irq_data_get_irq_chip_data(data);
+ irq_hw_number_t hwirq = irqd_to_hwirq(data);
+
+ if (hwirq < MC33978_NUM_PINS)
+ WRITE_ONCE(mc->cached_pin_mask,
+ READ_ONCE(mc->cached_pin_mask) | BIT(hwirq));
+}
+
+static void mc33978_irq_bus_lock(struct irq_data *data)
+{
+ struct mc33978_mfd_priv *mc = irq_data_get_irq_chip_data(data);
+
+ mutex_lock(&mc->irq_lock);
+}
+
+/**
+ * mc33978_irq_bus_sync_unlock() - Sync cached IRQ mask to hardware and unlock
+ * @data: IRQ data
+ *
+ * Writes the cached interrupt mask to the hardware IE_SG and IE_SP registers,
+ * then releases the IRQ lock. This is where the actual hardware update occurs
+ * after mask/unmask operations.
+ */
+static void mc33978_irq_bus_sync_unlock(struct irq_data *data)
+{
+ struct mc33978_mfd_priv *mc = irq_data_get_irq_chip_data(data);
+ u32 sg_mask, sp_mask, cached_mask;
+ int ret;
+
+ cached_mask = READ_ONCE(mc->cached_pin_mask);
+
+ /*
+ * Split the cached 22-bit pin mask into hardware register format:
+ * - SG pins: bits [13:0] (14 pins, mask 0x3FFF)
+ * - SP pins: bits [21:14] (8 pins, mask 0xFF)
+ */
+ sg_mask = FIELD_GET(MC33978_CACHE_SG_PIN_MASK, cached_mask);
+ sp_mask = FIELD_GET(MC33978_CACHE_SP_PIN_MASK, cached_mask);
+
+ ret = regmap_update_bits(mc->map, MC33978_REG_IE_SG,
+ MC33978_SG_PIN_MASK, sg_mask);
+ if (ret)
+ goto unlock;
+
+ ret = regmap_update_bits(mc->map, MC33978_REG_IE_SP,
+ MC33978_SP_PIN_MASK, sp_mask);
+unlock:
+ if (ret)
+ dev_err(&mc->spi->dev, "failed to sync IRQ mask to hardware: %d\n",
+ ret);
+
+ mutex_unlock(&mc->irq_lock);
+}
+
+static int mc33978_irq_set_type(struct irq_data *data, unsigned int type)
+{
+ struct mc33978_mfd_priv *mc = irq_data_get_irq_chip_data(data);
+ irq_hw_number_t hwirq = irqd_to_hwirq(data);
+ u32 mask = BIT(hwirq);
+
+ if (type & (IRQ_TYPE_LEVEL_HIGH | IRQ_TYPE_LEVEL_LOW))
+ return -EINVAL;
+
+ if (type & IRQ_TYPE_EDGE_RISING)
+ WRITE_ONCE(mc->irq_rise, READ_ONCE(mc->irq_rise) | mask);
+ else
+ WRITE_ONCE(mc->irq_rise, READ_ONCE(mc->irq_rise) & ~mask);
+
+ if (type & IRQ_TYPE_EDGE_FALLING)
+ WRITE_ONCE(mc->irq_fall, READ_ONCE(mc->irq_fall) | mask);
+ else
+ WRITE_ONCE(mc->irq_fall, READ_ONCE(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 int mc33978_irq_domain_alloc(struct irq_domain *domain,
+ unsigned int virq,
+ unsigned int nr_irqs, void *arg)
+{
+ struct mc33978_mfd_priv *mc = domain->host_data;
+ struct irq_fwspec *fwspec = arg;
+ irq_hw_number_t hwirq;
+ int i;
+
+ if (fwspec->param_count < 1)
+ return -EINVAL;
+
+ hwirq = fwspec->param[0];
+
+ for (i = 0; i < nr_irqs; i++) {
+ irq_domain_set_hwirq_and_chip(domain, virq + i, hwirq + i,
+ &mc33978_irq_chip, mc);
+ irq_set_nested_thread(virq + i, 1);
+ irq_clear_status_flags(virq + i, IRQ_NOREQUEST | IRQ_NOPROBE);
+ }
+
+ return 0;
+}
+
+static void mc33978_irq_domain_free(struct irq_domain *domain,
+ unsigned int virq,
+ unsigned int nr_irqs)
+{
+ int i;
+
+ for (i = 0; i < nr_irqs; i++)
+ irq_domain_reset_irq_data(irq_domain_get_irq_data(domain,
+ virq + i));
+}
+
+static const struct irq_domain_ops mc33978_irq_domain_ops = {
+ .map = mc33978_irq_map,
+ .alloc = mc33978_irq_domain_alloc,
+ .free = mc33978_irq_domain_free,
+ .xlate = irq_domain_xlate_twocell,
+};
+
+static void mc33978_irq_domain_remove(void *data)
+{
+ struct 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;
+ u32 rise, fall;
+ int i;
+
+ changed_pins = pin_state ^ mc->cached_pin_state;
+ if (!changed_pins)
+ return false;
+
+ mc->cached_pin_state = pin_state;
+ changed_pins &= READ_ONCE(mc->cached_pin_mask);
+
+ if (!changed_pins)
+ return false;
+
+ rise = READ_ONCE(mc->irq_rise);
+ fall = READ_ONCE(mc->irq_fall);
+
+ fired_pins |= (changed_pins & pin_state) & rise;
+ fired_pins |= (changed_pins & ~pin_state) & 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)
+{
+ bool fault_active = READ_ONCE(mc->bus_fault_active);
+ bool changed = fault_active ^ READ_ONCE(mc->cached_fault_active);
+ u32 rise, fall;
+ int virq;
+
+ WRITE_ONCE(mc->cached_fault_active, fault_active);
+
+ if (!changed)
+ return false;
+
+ rise = READ_ONCE(mc->irq_rise);
+ fall = READ_ONCE(mc->irq_fall);
+
+ if (fault_active && !(rise & BIT(MC33978_HWIRQ_FAULT)))
+ return false;
+
+ if (!fault_active && !(fall & BIT(MC33978_HWIRQ_FAULT)))
+ return false;
+
+ virq = irq_find_mapping(mc->domain, MC33978_HWIRQ_FAULT);
+ if (virq > 0) {
+ handle_nested_irq(virq);
+ return true;
+ }
+
+ return false;
+}
+
+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;
+
+ /*
+ * FAULT_STAT bit from hw_flags is not explicitly consumed here;
+ * the bus_fault_active state drives mc33978_handle_fault_condition()
+ * independently to ensure falling edges are processed.
+ */
+ if (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;
+
+ guard(mutex)(&mc->event_lock);
+
+ do {
+ atomic_set(&mc->is_handling, 1);
+ /*
+ * Ensure the is_handling state is globally visible before we
+ * process the hardware events and re-evaluate harvested_flags.
+ */
+ smp_mb();
+
+ if (mc33978_process_single_event(mc))
+ handled = true;
+
+ atomic_set(&mc->is_handling, 0);
+ /*
+ * Ensure is_handling = 0 is globally visible before we check
+ * harvested_flags in the while loop, preventing a race where
+ * the harvester misses scheduling the workqueue.
+ */
+ smp_mb();
+
+ } while (atomic_read(&mc->harvested_flags) != 0);
+
+ 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");
+
+ mc->domain->flags |= IRQ_DOMAIN_FLAG_HIERARCHY;
+
+ 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)
+{
+ bool fault_active;
+
+ fault_active = !!(status & MC33978_HI_BYTE_STAT_FAULT);
+
+ /* Track the absolute latest physical state seen on the bus */
+ WRITE_ONCE(mc->bus_fault_active, fault_active);
+
+ /* If the bus state changed from what the IRQ thread last evaluated, wake it up */
+ if (fault_active != READ_ONCE(mc->cached_fault_active))
+ atomic_or(MC33978_HI_BYTE_STAT_FAULT, &mc->harvested_flags);
+
+ if (status & MC33978_HI_BYTE_STAT_INT)
+ atomic_or(MC33978_HI_BYTE_STAT_INT, &mc->harvested_flags);
+
+ /*
+ * If the handler is not actively running and there are flags to process,
+ * schedule the background worker. The memory barrier implied by
+ * atomic_read ensures we see the latest is_handling state.
+ */
+ if (!atomic_read(&mc->is_handling) && atomic_read(&mc->harvested_flags))
+ schedule_work(&mc->event_work);
+}
+
+/**
+ * mc33978_prepare_messages() - Initialize the persistent SPI messages
+ * @mc: Device private data
+ *
+ * Hardware pipelining constraints:
+ * - Write (1 Frame): The device executes write commands immediately upon
+ * CS de-assertion. No fetch frame is required.
+ * - Read (2 Frames): The MISO response logically lags by one frame.
+ * Frame 1 transmits the read request and toggles CS to latch it.
+ * Frame 2 transmits a dummy CHECK command to fetch the actual payload.
+ */
+static void mc33978_prepare_messages(struct mc33978_mfd_priv *mc)
+{
+ /* --- Prepare Write Message (1 Frame) --- */
+ spi_message_init(&mc->msg_write);
+
+ mc->xfer_write.tx_buf = mc->tx_frame[MC33978_FRAME_REQ];
+ mc->xfer_write.rx_buf = mc->rx_frame[MC33978_FRAME_REQ];
+ mc->xfer_write.len = MC33978_FRAME_LEN;
+
+ spi_message_add_tail(&mc->xfer_write, &mc->msg_write);
+
+ /* --- Prepare Read Message (2 Frames) --- */
+ spi_message_init(&mc->msg_read);
+
+ /* Frame 1: Request */
+ mc->xfer_read[MC33978_FRAME_REQ].tx_buf =
+ mc->tx_frame[MC33978_FRAME_REQ];
+ mc->xfer_read[MC33978_FRAME_REQ].rx_buf =
+ mc->rx_frame[MC33978_FRAME_REQ];
+ mc->xfer_read[MC33978_FRAME_REQ].len = MC33978_FRAME_LEN;
+ mc->xfer_read[MC33978_FRAME_REQ].cs_change = 1; /* Latch command */
+
+ /* Frame 2: Fetch (Dummy CHECK) */
+ mc->xfer_read[MC33978_FRAME_PIPE].tx_buf =
+ mc->tx_frame[MC33978_FRAME_PIPE];
+ mc->xfer_read[MC33978_FRAME_PIPE].rx_buf =
+ mc->rx_frame[MC33978_FRAME_PIPE];
+ mc->xfer_read[MC33978_FRAME_PIPE].len = MC33978_FRAME_LEN;
+
+ /* Preload the dummy CHECK command statically */
+ mc->tx_frame[MC33978_FRAME_PIPE][MC33978_FRAME_CMD] = MC33978_REG_CHECK;
+
+ spi_message_add_tail(&mc->xfer_read[MC33978_FRAME_REQ], &mc->msg_read);
+ spi_message_add_tail(&mc->xfer_read[MC33978_FRAME_PIPE], &mc->msg_read);
+}
+
+/**
+ * mc33978_rx_decode() - Decode MISO response frame and extract status
+ * @rx_frame: Received SPI frame buffer (4 bytes)
+ * @val_buf: Output buffer for regmap (exactly 3 bytes, optional)
+ *
+ * Translates the 4-byte SPI response into a 3-byte regmap payload.
+ * Harvests the volatile INTflg and FAULT_STAT bits from the MSB.
+ *
+ * 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 const struct resource mc33978_hwmon_resources[] = {
+ DEFINE_RES_IRQ(MC33978_HWIRQ_FAULT),
+};
+
+static const struct mfd_cell mc33978_cells[] = {
+ { .name = "mc33978-pinctrl" },
+ {
+ .name = "mc33978-hwmon",
+ .resources = mc33978_hwmon_resources,
+ .num_resources = ARRAY_SIZE(mc33978_hwmon_resources),
+ },
+ { .name = "mc33978-mux" },
+};
+
+static const struct mfd_cell mc34978_cells[] = {
+ { .name = "mc34978-pinctrl" },
+ {
+ .name = "mc34978-hwmon",
+ .resources = mc33978_hwmon_resources,
+ .num_resources = ARRAY_SIZE(mc33978_hwmon_resources),
+ },
+ { .name = "mc34978-mux" },
+};
+
+static const struct mc33978_data mc33978_match_data = {
+ .cells = mc33978_cells,
+ .num_cells = ARRAY_SIZE(mc33978_cells),
+};
+
+static const struct mc33978_data mc34978_match_data = {
+ .cells = mc34978_cells,
+ .num_cells = ARRAY_SIZE(mc34978_cells),
+};
+
+static int mc33978_probe(struct spi_device *spi)
+{
+ const struct mc33978_data *match_data;
+ struct device *dev = &spi->dev;
+ struct mc33978_mfd_priv *mc;
+ int ret;
+
+ match_data = spi_get_device_match_data(spi);
+ if (!match_data)
+ return dev_err_probe(dev, -ENODEV, "no device match data found\n");
+
+ mc = devm_kzalloc(dev, sizeof(*mc), GFP_KERNEL);
+ if (!mc)
+ return -ENOMEM;
+
+ mc->spi = spi;
+ spi_set_drvdata(spi, mc);
+
+ mc->vddq = devm_regulator_get(dev, "vddq");
+ if (IS_ERR(mc->vddq))
+ return dev_err_probe(dev, PTR_ERR(mc->vddq),
+ "failed to get VDDQ regulator\n");
+
+ mc->vbatp = devm_regulator_get(dev, "vbatp");
+ if (IS_ERR(mc->vbatp))
+ return dev_err_probe(dev, PTR_ERR(mc->vbatp),
+ "failed to get VBATP regulator\n");
+
+ ret = mc33978_power_on(mc);
+ if (ret)
+ return ret;
+
+ ret = devm_add_action_or_reset(dev, mc33978_power_off, mc);
+ if (ret)
+ return ret;
+
+ mutex_init(&mc->event_lock);
+
+ ret = devm_work_autocancel(dev, &mc->event_work, mc33978_event_work);
+ if (ret)
+ return ret;
+
+ 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_mfd_add_devices(dev, PLATFORM_DEVID_NONE,
+ match_data->cells, match_data->num_cells,
+ NULL, 0, mc->domain);
+ if (ret)
+ return dev_err_probe(dev, ret, "failed to add MFD child devices\n");
+
+ return 0;
+}
+
+static const struct of_device_id mc33978_of_match[] = {
+ { .compatible = "nxp,mc33978", .data = &mc33978_match_data },
+ { .compatible = "nxp,mc34978", .data = &mc34978_match_data },
+ { }
+};
+MODULE_DEVICE_TABLE(of, mc33978_of_match);
+
+static const struct spi_device_id mc33978_spi_id[] = {
+ { "mc33978", (kernel_ulong_t)&mc33978_match_data },
+ { "mc34978", (kernel_ulong_t)&mc34978_match_data },
+ { }
+};
+MODULE_DEVICE_TABLE(spi, mc33978_spi_id);
+
+static struct spi_driver mc33978_driver = {
+ .driver = {
+ .name = MC33978_DRV_NAME,
+ .of_match_table = mc33978_of_match,
+ },
+ .probe = mc33978_probe,
+ .id_table = mc33978_spi_id,
+};
+module_spi_driver(mc33978_driver);
+
+MODULE_AUTHOR("David Jander <david@protonic.nl>");
+MODULE_DESCRIPTION("NXP MC33978/MC34978 MFD core driver");
+MODULE_LICENSE("GPL");
diff --git a/include/linux/mfd/mc33978.h b/include/linux/mfd/mc33978.h
new file mode 100644
index 000000000000..e8dec678e5a4
--- /dev/null
+++ b/include/linux/mfd/mc33978.h
@@ -0,0 +1,92 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Copyright (C) 2024 David Jander <david@protonic.nl>, Protonic Holland
+ * Copyright (C) 2026 Oleksij Rempel <kernel@pengutronix.de>, Pengutronix
+ *
+ * MC34978/MC33978 Multiple Switch Detection Interface - Shared Definitions
+ */
+
+#ifndef _LINUX_MFD_MC33978_H
+#define _LINUX_MFD_MC33978_H
+
+#include <linux/bits.h>
+
+/* Register Map - All addresses are base command bytes (R/W bit = 0) */
+#define MC33978_REG_CHECK 0x00 /* SPI communication check */
+#define MC33978_REG_CONFIG 0x02 /* Device configuration */
+#define MC33978_REG_TRI_SP 0x04 /* Tri-state enable SP */
+#define MC33978_REG_TRI_SG 0x06 /* Tri-state enable SG */
+#define MC33978_REG_WET_SP 0x08 /* Wetting current level SP */
+#define MC33978_REG_WET_SG0 0x0a /* Wetting current level SG0 (SG7-SG0) */
+#define MC33978_REG_WET_SG1 0x0c /* Wetting current level SG1 (SG13-SG8) */
+#define MC33978_REG_CWET_SP 0x16 /* Continuous wetting current SP */
+#define MC33978_REG_CWET_SG 0x18 /* Continuous wetting current SG */
+#define MC33978_REG_IE_SP 0x1a /* Interrupt enable SP */
+#define MC33978_REG_IE_SG 0x1c /* Interrupt enable SG */
+#define MC33978_REG_LPM_CONFIG 0x1e /* Low-power mode configuration */
+#define MC33978_REG_WAKE_SP 0x20 /* Wake-up enable SP */
+#define MC33978_REG_WAKE_SG 0x22 /* Wake-up enable SG */
+#define MC33978_REG_COMP_SP 0x24 /* Comparator only mode SP */
+#define MC33978_REG_COMP_SG 0x26 /* Comparator only mode SG */
+#define MC33978_REG_LPM_VT_SP 0x28 /* LPM voltage threshold SP */
+#define MC33978_REG_LPM_VT_SG 0x2a /* LPM voltage threshold SG */
+#define MC33978_REG_IP_SP 0x2c /* Polling current SP */
+#define MC33978_REG_IP_SG 0x2e /* Polling current SG */
+#define MC33978_REG_SPOLL_SP 0x30 /* Slow polling SP */
+#define MC33978_REG_SPOLL_SG 0x32 /* Slow polling SG */
+#define MC33978_REG_WDEB_SP 0x34 /* Wake-up debounce SP */
+#define MC33978_REG_WDEB_SG 0x36 /* Wake-up debounce SG */
+#define MC33978_REG_ENTER_LPM 0x38 /* Enter low-power mode (write-only) */
+#define MC33978_REG_AMUX_CTRL 0x3a /* AMUX control */
+#define MC33978_REG_READ_IN 0x3e /* Read switch status (READ_SW in datasheet) */
+#define MC33978_REG_FAULT 0x42 /* Fault status register */
+#define MC33978_REG_IRQ 0x46 /* Interrupt request (write-only) */
+#define MC33978_REG_RESET 0x48 /* Reset (write-only) */
+
+/*
+ * FAULT Register (0x42) bit definitions
+ * Reading this register clears most fault flags except persistent conditions
+ */
+#define MC33978_FAULT_SPI_ERROR BIT(10) /* SPI communication error */
+#define MC33978_FAULT_HASH BIT(9) /* SPI register hash mismatch */
+#define MC33978_FAULT_UV BIT(7) /* VBATP undervoltage */
+#define MC33978_FAULT_OV BIT(6) /* VBATP overvoltage */
+#define MC33978_FAULT_TEMP_WARN BIT(5) /* Temperature warning threshold */
+#define MC33978_FAULT_OT BIT(4) /* Over-temperature */
+#define MC33978_FAULT_INTB_WAKE BIT(3) /* Woken by INT_B pin */
+#define MC33978_FAULT_WAKEB_WAKE BIT(2) /* Woken by WAKE_B pin */
+#define MC33978_FAULT_SPI_WAKE BIT(1) /* Woken by SPI message */
+#define MC33978_FAULT_POR BIT(0) /* Power-on reset occurred */
+
+/* Critical faults that need immediate attention */
+#define MC33978_FAULT_CRITICAL (MC33978_FAULT_UV | \
+ MC33978_FAULT_OV | \
+ MC33978_FAULT_OT)
+
+/* Bits relevant as hwmon alarms; excludes wake/reset/SPI status bits */
+#define MC33978_FAULT_ALARM_MASK (MC33978_FAULT_UV | \
+ MC33978_FAULT_OV | \
+ MC33978_FAULT_TEMP_WARN | \
+ MC33978_FAULT_OT)
+
+#define MC33978_NUM_PINS 22
+
+/*
+ * Virtual IRQ number for fault handling.
+ * Using hwirq 22 (beyond the 22 pin IRQs 0-21).
+ */
+#define MC33978_HWIRQ_FAULT 22
+
+/*
+ * AMUX channel definitions
+ * The AMUX can route one of 24 signals to the external AMUX pin
+ */
+#define MC33978_AMUX_CH_SG0 0 /* Switch-to-Ground inputs 0-13 */
+#define MC33978_AMUX_CH_SG13 13
+#define MC33978_AMUX_CH_SP0 14 /* Programmable switch inputs 0-7 */
+#define MC33978_AMUX_CH_SP7 21
+#define MC33978_AMUX_CH_TEMP 22 /* Internal temperature diode */
+#define MC33978_AMUX_CH_VBATP 23 /* Battery voltage sense */
+#define MC33978_NUM_AMUX_CH 24 /* Total number of AMUX channels */
+
+#endif /* _LINUX_MFD_MC33978_H */
--
2.47.3
^ permalink raw reply related
* [PATCH v7 4/6] pinctrl: add NXP MC33978/MC34978 pinctrl driver
From: Oleksij Rempel @ 2026-03-27 16:34 UTC (permalink / raw)
To: Guenter Roeck, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Lee Jones, Peter Rosin, Linus Walleij
Cc: David Jander, Oleksij Rempel, kernel, linux-kernel, devicetree,
linux-hwmon, linux-gpio
In-Reply-To: <20260327163450.3287313-1-o.rempel@pengutronix.de>
From: David Jander <david@protonic.nl>
Add pin control and GPIO driver for the NXP MC33978/MC34978 Multiple
Switch Detection Interface (MSDI) devices.
This driver exposes the 22 mechanical switch detection inputs (14
Switch-to-Ground, 8 Programmable) as standard GPIOs.
Key features implemented:
- GPIO read/write: Translates physical switch states (open/closed)
to logical GPIO levels based on the configured switch topology
(Switch-to-Ground vs. Switch-to-Battery).
- Emulated Output: Allows setting pins "high" or "low" by manipulating
the tri-state registers and hardware pull topologies.
- Interrupt routing: Proxies GPIO interrupt requests to the irq_domain
managed by the parent MFD core driver.
Signed-off-by: David Jander <david@protonic.nl>
Co-developed-by: Oleksij Rempel <o.rempel@pengutronix.de>
Signed-off-by: Oleksij Rempel <o.rempel@pengutronix.de>
Reviewed-by: Linus Walleij <linusw@kernel.org>
---
changes v7:
- Refactor I/O state reading and tri-state updates for SG/SB topologies
- Fix open-drain and open-source pinconf emulation
- Make direction_input a no-op to prevent overriding pinctrl bias
- Add defensive wrappers for IRQ proxying to prevent NULL pointer panics
- Add missing mutex guards to pinconf and get operations
- Convert generic internal variables to u32 and add lockdep assertions
changes v6:
- no changes
changes v5:
- no changes
changes v4:
- add Reviewed-by: Linus Walleij ...
- Replace the of_device_id match table with a platform_device_id table
- Add device_set_node(dev, dev_fwnode(dev->parent)) during probe
- Remove the check for a missing dev->of_node
changes v3:
- replace manual mutex_lock()/mutex_unlock() paths with guard(mutex)
- Unify error checking style by replacing if (ret < 0) with if (ret)
- Migrate from a custom .to_irq callback to a hierarchical gpio_irq_chip
- Implement .irq_bus_lock and .irq_bus_sync_unlock proxies to properly
cascade SPI bus lock operations to the parent MFD domain
- Set girq->handler to handle_simple_irq
changes v2:
- Translate all remaining German comments to English.
- Remove unnecessary #ifdef CONFIG_OF wrappers around dt_node_to_map.
- Add detailed comments to mc33978_get() and mc33978_get_multiple() explaining
the hardware comparator logic (1 = closed, 0 = open) and justifying the
bitwise inversion required to report actual physical voltage levels.
- Add comments to the .set() and .set_config() callbacks explaining why
gpiolib's standard open-drain emulation (switching to input mode) fails on
this hardware due to active wetting currents, and why tri-state isolation is
mandatory.
- Add a comment to mc33978_gpio_to_irq() explaining why it must act as a
proxy to the parent MFD's irq_domain (shared physical INT_B line with hwmon).
- Drop dummy pin group callbacks (get_groups_count, etc.). This relies on a
preparatory patch in this series making these callbacks optional in the core.
- Fix debugfs 'pinconf-pins' read errors by correctly returning -ENOTSUPP
instead of -EOPNOTSUPP for unsupported generic configurations.
- Fix empty 'gpio-ranges' and missing debugfs labels by explicitly calling
gpiochip_add_pin_range() during probe.
- Eliminate "magic" bitwise math in the wetting current configuration by
introducing a static lookup array (mc33978_wet_mA).
- Resolve checkpatch.pl strict warnings regarding macro argument reuse by
converting MC33978_SPSG, MC33978_PINSHIFT, MC33978_WREG, and MC33978_WSHIFT
to static inline functions.
- Remove artifacts from previous interrupt handling implementations.
- Address minor formatting and whitespace nits.
---
drivers/pinctrl/Kconfig | 14 +
drivers/pinctrl/Makefile | 1 +
drivers/pinctrl/pinctrl-mc33978.c | 836 ++++++++++++++++++++++++++++++
3 files changed, 851 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..dcbac06db905
--- /dev/null
+++ b/drivers/pinctrl/pinctrl-mc33978.c
@@ -0,0 +1,836 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (C) 2024 David Jander <david@protonic.nl>, Protonic Holland
+ * Copyright (C) 2026 Oleksij Rempel <kernel@pengutronix.de>, Pengutronix
+ *
+ * MC33978/MC34978 Multiple Switch Detection Interface - Pinctrl/GPIO Driver
+ *
+ * Provides GPIO and pinctrl interfaces for the 22 switch detection inputs.
+ * Handles digital input reading and wetting current configuration. Analog AMUX
+ * functionality is handled by a separate mux driver.
+ *
+ * GPIO Mapping:
+ * - GPIO 0-13: SG0-SG13 (Switch-to-Ground inputs)
+ * - GPIO 14-21: SP0-SP7 (Programmable: Switch-to-Ground or Switch-to-Battery)
+ * This is dictated by the READ_IN register where bits [21:14] = SP[7:0]
+ * and bits [13:0] = SG[13:0].
+ *
+ * Register Organization:
+ * Configuration registers are generally paired. The _SP register at offset N
+ * controls SP0-SP7, and the _SG register at offset N+2 controls SG0-SG13.
+ *
+ * Wetting Currents vs. Pull Resistors:
+ * The hardware physically lacks traditional passive pull-up or pull-down
+ * resistors. Instead, it uses active, controllable current regulators
+ * (wetting currents) to detect switch states and clean mechanical contacts.
+ * - Because these are active current sources, specifying an ohmic value for
+ * pull-up/down biases is physically invalid. The driver ignores ohm arguments.
+ * - 8 selectable current values: 2, 6, 8, 10, 12, 14, 16, 20 mA.
+ * - Exposed via the pinconf PIN_CONFIG_DRIVE_STRENGTH parameter (in mA).
+ *
+ * Emulated Outputs:
+ * The hardware lacks traditional push-pull output drivers; it is strictly an
+ * input device. "Outputs" are simulated by toggling the wetting currents and
+ * physically isolating the pins via hardware tri-state registers. Consequently,
+ * consumers MUST flag outputs with GPIO_OPEN_DRAIN or GPIO_OPEN_SOURCE in
+ * the Device Tree.
+ *
+ * Input Detection Mechanics:
+ * This input mechanism relies on the active current regulators rather than
+ * passive hard resistors. For a Switch-to-Ground (SG) pin, the chip sources
+ * a constant current. When the switch is open, the pin voltage floats up to
+ * the battery voltage. When the switch closes, it creates a path to ground;
+ * because the current is strictly regulated, the pin voltage drops sharply
+ * below the internal 4.0V comparator threshold.
+ * * The hardware evaluates this and reports an abstract "contact status"
+ * (1 = closed, 0 = open). For SG pins, a closed switch (~0V) reports as '1'.
+ * To align with gpiolib expectations where ~0V equals a physical logical '0',
+ * this driver explicitly inverts the hardware status for all SG-configured
+ * pins before reporting them.
+ *
+ * Interrupts:
+ * The physical INT_B line and threaded IRQ domain are managed centrally by
+ * the parent MFD core. This driver simply proxies .to_irq() to the parent.
+ *
+ * Written by David Jander <david@protonic.nl>
+ *
+ * Datasheet:
+ * https://www.nxp.com/docs/en/data-sheet/MC33978.pdf
+ */
+
+#include <linux/cleanup.h>
+#include <linux/device.h>
+#include <linux/gpio/driver.h>
+#include <linux/interrupt.h>
+#include <linux/irq.h>
+#include <linux/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_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
+
+#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;
+
+ /*
+ * Protects multi-register hardware sequences in .set() and atomic
+ * READ_IN + CONFIG reads in .get()
+ */
+ struct mutex lock;
+};
+
+static const struct pinctrl_pin_desc mc33978_pins[] = {
+ PINCTRL_PIN(0, "sg0"),
+ PINCTRL_PIN(1, "sg1"),
+ PINCTRL_PIN(2, "sg2"),
+ PINCTRL_PIN(3, "sg3"),
+ PINCTRL_PIN(4, "sg4"),
+ PINCTRL_PIN(5, "sg5"),
+ PINCTRL_PIN(6, "sg6"),
+ PINCTRL_PIN(7, "sg7"),
+ PINCTRL_PIN(8, "sg8"),
+ PINCTRL_PIN(9, "sg9"),
+ PINCTRL_PIN(10, "sg10"),
+ PINCTRL_PIN(11, "sg11"),
+ PINCTRL_PIN(12, "sg12"),
+ PINCTRL_PIN(13, "sg13"),
+ PINCTRL_PIN(14, "sp0"),
+ PINCTRL_PIN(15, "sp1"),
+ PINCTRL_PIN(16, "sp2"),
+ PINCTRL_PIN(17, "sp3"),
+ PINCTRL_PIN(18, "sp4"),
+ PINCTRL_PIN(19, "sp5"),
+ PINCTRL_PIN(20, "sp6"),
+ PINCTRL_PIN(21, "sp7"),
+};
+
+static inline bool mc33978_is_sp(unsigned int pin)
+{
+ return pin >= MC33978_NUM_SG;
+}
+
+/* Choose register offset for _SG/_SP registers. reg is always the _SP addr. */
+static inline u8 mc33978_spsg(u8 reg, unsigned int pin)
+{
+ return mc33978_is_sp(pin) ? reg : reg + 2;
+}
+
+/* Get the bit index into the corresponding register */
+static inline unsigned int mc33978_pinshift(unsigned int pin)
+{
+ return mc33978_is_sp(pin) ? pin - MC33978_NUM_SG : pin;
+}
+
+#define MC33978_PINMASK(pin) BIT(mc33978_pinshift(pin))
+
+/*
+ * Wetting current registers: 3 in total, each pin uses a 3-bit field,
+ * 8 pins per register, except for the last one.
+ */
+static inline u8 mc33978_wreg(u8 reg, unsigned int pin)
+{
+ return reg + (mc33978_is_sp(pin) ? 0 : 2 + 2 * (pin / 8));
+}
+
+static inline unsigned int mc33978_wshift(unsigned int pin)
+{
+ return mc33978_is_sp(pin) ? 3 * (pin - MC33978_NUM_SG) : 3 * (pin % 8);
+}
+
+#define MC33978_WMASK(pin) (7 << mc33978_wshift(pin))
+
+static int mc33978_read(struct mc33978_pinctrl *mpc, u8 reg, u32 *val)
+{
+ int ret;
+
+ ret = regmap_read(mpc->regmap, reg, val);
+ if (ret)
+ dev_err_ratelimited(mpc->dev, "Regmap read error %d at reg: %02x.\n",
+ ret, reg);
+ return ret;
+}
+
+static int mc33978_update_bits(struct mc33978_pinctrl *mpc, u8 reg, u32 mask,
+ u32 val)
+{
+ int ret;
+
+ ret = regmap_update_bits(mpc->regmap, reg, mask, val);
+ if (ret)
+ dev_err_ratelimited(mpc->dev, "Regmap update bits error %d at reg: %02x.\n",
+ ret, reg);
+ return ret;
+}
+
+static const struct pinctrl_ops mc33978_pinctrl_ops = {
+ .dt_node_to_map = pinconf_generic_dt_node_to_map_pin,
+ .dt_free_map = pinconf_generic_dt_free_map,
+};
+
+static int mc33978_get_pull(struct mc33978_pinctrl *mpc, unsigned int pin, u32 *val)
+{
+ u32 data;
+ int ret;
+
+ lockdep_assert_held(&mpc->lock);
+
+ ret = mc33978_read(mpc, mc33978_spsg(MC33978_REG_TRI_SP, pin), &data);
+ if (ret)
+ return ret;
+
+ /* Is the pin tri-stated? */
+ if (data & MC33978_PINMASK(pin)) {
+ *val = MC33978_TRISTATE;
+ return 0;
+ }
+
+ /* Pins 0..13 only support pull-up */
+ if (!mc33978_is_sp(pin)) {
+ *val = MC33978_PU;
+ return 0;
+ }
+
+ /* Check pin pull direction for pins 14..21 */
+ ret = mc33978_read(mpc, MC33978_REG_CONFIG, &data);
+ if (ret)
+ return ret;
+
+ if (data & MC33978_PINMASK(pin))
+ *val = MC33978_PD;
+ else
+ *val = MC33978_PU;
+
+ return 0;
+}
+
+static int mc33978_set_pull(struct mc33978_pinctrl *mpc, unsigned int pin, int val)
+{
+ u32 mask = MC33978_PINMASK(pin);
+ int ret;
+
+ lockdep_assert_held(&mpc->lock);
+
+ /* SG pins physically lack pull-downs current sources */
+ if (val == MC33978_PD && !mc33978_is_sp(pin))
+ return -EINVAL;
+
+ /* Configure direction (Exclusively for SP pins) */
+ if (mc33978_is_sp(pin) && val != MC33978_TRISTATE) {
+ ret = mc33978_update_bits(mpc, MC33978_REG_CONFIG, mask,
+ (val == MC33978_PD) ? mask : 0);
+ if (ret)
+ return ret;
+ }
+
+ /* Enable current source or set to tri-state */
+ return mc33978_update_bits(mpc, mc33978_spsg(MC33978_REG_TRI_SP, pin),
+ mask,
+ (val == MC33978_TRISTATE) ? mask : 0);
+}
+
+static const unsigned int mc33978_wet_mA[] = { 2, 6, 8, 10, 12, 14, 16, 20 };
+
+static int mc33978_set_ds(struct mc33978_pinctrl *mpc, unsigned int pin,
+ u32 val)
+{
+ int i;
+
+ for (i = 0; i < ARRAY_SIZE(mc33978_wet_mA); i++) {
+ if (val == mc33978_wet_mA[i]) {
+ return mc33978_update_bits(mpc,
+ mc33978_wreg(MC33978_REG_WET_SP, pin),
+ MC33978_WMASK(pin),
+ i << mc33978_wshift(pin));
+ }
+ }
+
+ return -EINVAL;
+}
+
+static int mc33978_get_ds(struct mc33978_pinctrl *mpc, unsigned int pin,
+ u32 *val)
+{
+ u32 data;
+ int ret;
+
+ ret = mc33978_read(mpc, mc33978_wreg(MC33978_REG_WET_SP, pin), &data);
+ if (ret)
+ return ret;
+
+ data &= MC33978_WMASK(pin);
+ data >>= mc33978_wshift(pin);
+
+ if (data >= ARRAY_SIZE(mc33978_wet_mA))
+ return -EINVAL;
+
+ *val = mc33978_wet_mA[data];
+
+ return 0;
+}
+
+static int mc33978_pinconf_get(struct pinctrl_dev *pctldev, unsigned int pin,
+ unsigned long *config)
+{
+ struct mc33978_pinctrl *mpc = pinctrl_dev_get_drvdata(pctldev);
+ enum pin_config_param param = pinconf_to_config_param(*config);
+ u32 arg;
+ u32 data;
+ int ret;
+
+ guard(mutex)(&mpc->lock);
+
+ switch (param) {
+ case PIN_CONFIG_BIAS_PULL_UP:
+ ret = mc33978_get_pull(mpc, pin, &data);
+ if (ret)
+ return ret;
+ if (data != MC33978_PU)
+ return -EINVAL;
+ arg = 1;
+ break;
+ case PIN_CONFIG_BIAS_PULL_DOWN:
+ ret = mc33978_get_pull(mpc, pin, &data);
+ if (ret)
+ return ret;
+ if (data != MC33978_PD)
+ return -EINVAL;
+ arg = 1;
+ break;
+ case PIN_CONFIG_DRIVE_OPEN_DRAIN:
+ if (!mc33978_is_sp(pin))
+ return -EINVAL;
+
+ ret = mc33978_read(mpc, MC33978_REG_CONFIG, &data);
+ if (ret)
+ return ret;
+
+ if (!(data & MC33978_PINMASK(pin)))
+ return -EINVAL;
+ arg = 1;
+ break;
+ case PIN_CONFIG_DRIVE_OPEN_SOURCE:
+ if (mc33978_is_sp(pin)) {
+ ret = mc33978_read(mpc, MC33978_REG_CONFIG, &data);
+ if (ret)
+ return ret;
+
+ if (data & MC33978_PINMASK(pin))
+ return -EINVAL;
+ }
+ arg = 1;
+ break;
+ case PIN_CONFIG_BIAS_DISABLE:
+ case PIN_CONFIG_BIAS_HIGH_IMPEDANCE:
+ ret = mc33978_get_pull(mpc, pin, &data);
+ if (ret)
+ return ret;
+ if (data != MC33978_TRISTATE)
+ return -EINVAL;
+ arg = 1;
+ break;
+ case PIN_CONFIG_DRIVE_STRENGTH:
+ ret = mc33978_get_ds(mpc, pin, &data);
+ if (ret)
+ return ret;
+ arg = data;
+ break;
+ default:
+ /*
+ * Ignore checkpatch warning: the pinctrl core specifically
+ * expects -ENOTSUPP to silently skip unsupported generic
+ * parameters. Using -EOPNOTSUPP causes debugfs read failures.
+ */
+ return -ENOTSUPP;
+ }
+
+ *config = pinconf_to_config_packed(param, arg);
+
+ return 0;
+}
+
+/*
+ * Hardware constraint regarding PIN_CONFIG_BIAS_PULL_UP/DOWN:
+ * The MC33978 utilizes active constant current sources (wetting currents)
+ * rather than passive pull-resistors. Since the equivalent ohmic resistance
+ * scales dynamically with the fluctuating board voltage (VBATP), computing
+ * a static ohm value is physically invalid.
+ * The driver intentionally ignores resistance arguments during configuration
+ * and continuously reports 0 ohms to the pinctrl framework.
+ */
+static int mc33978_pinconf_set(struct pinctrl_dev *pctldev, unsigned int pin,
+ unsigned long *configs, unsigned int num_configs)
+{
+ struct mc33978_pinctrl *mpc = pinctrl_dev_get_drvdata(pctldev);
+ enum pin_config_param param;
+ int ret = 0;
+ u32 arg;
+ int i;
+
+ guard(mutex)(&mpc->lock);
+
+ for (i = 0; i < num_configs; i++) {
+ param = pinconf_to_config_param(configs[i]);
+ arg = pinconf_to_config_argument(configs[i]);
+
+ /*
+ * The hardware physically lacks push-pull output drivers.
+ * By explicitly handling OPEN_DRAIN and OPEN_SOURCE here, we
+ * signal to gpiolib that we support these modes "natively".
+ * This crucially prevents gpiolib from falling back to its
+ * software emulation (which sets the pin to input mode to
+ * achieve High-Z). On the MC33978, input mode is NOT High-Z;
+ * it actively drives the line with a wetting current!
+ */
+ switch (param) {
+ case PIN_CONFIG_DRIVE_OPEN_SOURCE:
+ /* Setup topology only; do not turn on current yet */
+ if (mc33978_is_sp(pin))
+ ret = mc33978_update_bits(mpc, MC33978_REG_CONFIG,
+ MC33978_PINMASK(pin), 0);
+ break;
+ case PIN_CONFIG_BIAS_PULL_UP:
+ ret = mc33978_set_pull(mpc, pin, MC33978_PU);
+ break;
+ case PIN_CONFIG_DRIVE_OPEN_DRAIN:
+ if (!mc33978_is_sp(pin)) {
+ dev_err(mpc->dev, "Pin %u is SG and does not support open-drain\n",
+ pin);
+ return -EINVAL;
+ }
+ /* Setup topology only; do not turn on current yet */
+ ret = mc33978_update_bits(mpc, MC33978_REG_CONFIG,
+ MC33978_PINMASK(pin),
+ MC33978_PINMASK(pin));
+ break;
+ 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;
+ /*
+ * The MC33978 uses active wetting currents rather than passive
+ * pull-resistors. Disabling the bias (pull-up/down) is
+ * physically equivalent to putting the pin into a
+ * high-impedance state. Both actions are achieved by isolating
+ * the pin via the hardware tri-state registers.
+ */
+ case PIN_CONFIG_BIAS_DISABLE:
+ case PIN_CONFIG_BIAS_HIGH_IMPEDANCE:
+ ret = mc33978_set_pull(mpc, pin, MC33978_TRISTATE);
+ break;
+ case PIN_CONFIG_DRIVE_STRENGTH_UA:
+ arg /= 1000;
+ fallthrough;
+ case PIN_CONFIG_DRIVE_STRENGTH:
+ ret = mc33978_set_ds(mpc, pin, arg);
+ break;
+ default:
+ /*
+ * Required by the pinctrl core to safely fall back or
+ * skip unsupported configs. Do not use -EOPNOTSUPP.
+ */
+ return -ENOTSUPP;
+ }
+
+ if (ret) {
+ dev_err(mpc->dev, "Failed to set config param %04x for pin %u: %d\n",
+ param, pin, ret);
+ return ret;
+ }
+ }
+
+ return 0;
+}
+
+static const struct pinconf_ops mc33978_pinconf_ops = {
+ .pin_config_get = mc33978_pinconf_get,
+ .pin_config_set = mc33978_pinconf_set,
+ .is_generic = true,
+};
+
+static int mc33978_direction_input(struct gpio_chip *chip, unsigned int offset)
+{
+ /* This chip is strictly an input device (comparators always active) */
+ return 0;
+}
+
+/*
+ * The hardware evaluates pin voltage against a threshold (default 4.0V)
+ * and reports an abstract contact status (1 = closed, 0 = open):
+ *
+ * SG (Switch-to-Ground) topology (pull-up current source):
+ * - Voltage > Threshold: Switch Open (HW reports 0) -> Physical High
+ * - Voltage < Threshold: Switch Closed (HW reports 1) -> Physical Low
+ *
+ * SB (Switch-to-Battery) topology (pull-down current source):
+ * - Voltage > Threshold: Switch Closed (HW reports 1) -> Physical High
+ * - Voltage < Threshold: Switch Open (HW reports 0) -> Physical Low
+ *
+ * We translate this contact status back into physical voltage levels by
+ * inverting the hardware status for all pins operating in SG topology.
+ */
+static int mc33978_read_in_state(struct mc33978_pinctrl *mpc,
+ unsigned long mask, unsigned long *state)
+{
+ u32 status, inv_mask;
+ u32 config_reg = 0;
+ int ret;
+
+ ret = mc33978_read(mpc, MC33978_REG_READ_IN, &status);
+ if (ret)
+ return ret;
+
+ /* Read CONFIG register only if the requested mask involves SP pins */
+ if (mask & MC33978_SP_MASK) {
+ ret = mc33978_read(mpc, MC33978_REG_CONFIG, &config_reg);
+ if (ret)
+ return ret;
+ }
+
+ /*
+ * Create an inversion mask for all pins currently operating in
+ * Switch-to-Ground (SG) topology. SG pins always have pull-ups.
+ * For SP pins, CONFIG bit 0 = Switch-to-Ground (PU),
+ * CONFIG bit 1 = Switch-to-Battery (PD).
+ */
+ inv_mask = MC33978_SG_MASK |
+ (~(config_reg << MC33978_NUM_SG) & MC33978_SP_MASK);
+
+ *state = (status ^ inv_mask) & mask;
+
+ return 0;
+}
+
+static int mc33978_get(struct gpio_chip *chip, unsigned int offset)
+{
+ struct mc33978_pinctrl *mpc = gpiochip_get_data(chip);
+ unsigned long state;
+ int ret;
+
+ guard(mutex)(&mpc->lock);
+
+ ret = mc33978_read_in_state(mpc, BIT(offset), &state);
+ if (ret)
+ return ret;
+
+ return !!(state & BIT(offset));
+}
+
+static int mc33978_get_multiple(struct gpio_chip *chip,
+ unsigned long *mask, unsigned long *bits)
+{
+ struct mc33978_pinctrl *mpc = gpiochip_get_data(chip);
+ unsigned long state;
+ int ret;
+
+ guard(mutex)(&mpc->lock);
+
+ ret = mc33978_read_in_state(mpc, *mask, &state);
+ if (ret)
+ return ret;
+
+ *bits = (*bits & ~*mask) | state;
+
+ return 0;
+}
+
+/*
+ * Emulate output states by routing or isolating active wetting currents.
+ * To turn the line ON, we disable the hardware tri-state (write 0).
+ * To turn the line OFF (High-Z), we enable tri-state (write 1).
+ *
+ * For Open-Source (Pull-Up): value=1 turns it ON, value=0 is High-Z.
+ * For Open-Drain (Pull-Down): value=0 turns it ON, value=1 is High-Z.
+ * We dynamically read the CONFIG register to determine the topology
+ * and invert the bits accordingly for Open-Drain pins.
+ *
+ * Note: The hardware physically lacks push-pull drivers. Toggling outputs
+ * via tri-state isolation may cause transient spikes.
+ */
+static int mc33978_update_tri_state(struct mc33978_pinctrl *mpc, u32 mask,
+ u32 bits)
+{
+ u32 sgmask = (mask & MC33978_SG_MASK) >> MC33978_SG_SHIFT;
+ u32 sgbits = (bits & MC33978_SG_MASK) >> MC33978_SG_SHIFT;
+ u32 spmask = (mask & MC33978_SP_MASK) >> MC33978_SP_SHIFT;
+ u32 spbits = (bits & MC33978_SP_MASK) >> MC33978_SP_SHIFT;
+ u32 config_reg = 0;
+ int ret = 0;
+
+ if (spmask) {
+ /* Read topology: 1 = PD (Open-Drain), 0 = PU (Open-Source) */
+ ret = mc33978_read(mpc, MC33978_REG_CONFIG, &config_reg);
+ if (ret)
+ return ret;
+
+ /*
+ * Invert bits for Open-Drain (PD) pins.
+ * The Open-Drain API contract expects value=1 to be High-Z.
+ */
+ spbits ^= (config_reg & spmask);
+
+ ret = mc33978_update_bits(mpc, MC33978_REG_TRI_SP, spmask,
+ ~spbits);
+ if (ret)
+ return ret;
+ }
+
+ /* SG pins are always Pull-Up (Open-Source), no inversion needed */
+ if (sgmask)
+ ret = mc33978_update_bits(mpc, MC33978_REG_TRI_SG, sgmask,
+ ~sgbits);
+
+ return ret;
+}
+
+static int mc33978_set(struct gpio_chip *chip, unsigned int offset, int value)
+{
+ struct mc33978_pinctrl *mpc = gpiochip_get_data(chip);
+ u32 mask = BIT(offset);
+ u32 bits = value ? mask : 0;
+
+ guard(mutex)(&mpc->lock);
+
+ return mc33978_update_tri_state(mpc, mask, bits);
+}
+
+static int mc33978_set_multiple(struct gpio_chip *chip,
+ unsigned long *mask, unsigned long *bits)
+{
+ struct mc33978_pinctrl *mpc = gpiochip_get_data(chip);
+
+ guard(mutex)(&mpc->lock);
+
+ return mc33978_update_tri_state(mpc, *mask, *bits);
+}
+
+static int mc33978_direction_output(struct gpio_chip *chip, unsigned int offset,
+ int value)
+{
+ return mc33978_set(chip, offset, value);
+}
+
+static int mc33978_gpio_child_to_parent_hwirq(struct gpio_chip *gc,
+ unsigned int child,
+ unsigned int child_type,
+ unsigned int *parent,
+ unsigned int *parent_type)
+{
+ *parent_type = child_type;
+ *parent = child;
+
+ return 0;
+}
+
+/*
+ * Defensive wrappers for hierarchical IRQ proxying.
+ *
+ * gpiolib's hierarchical allocation exposes a lifecycle gap: the child
+ * descriptor is registered before irq_domain_alloc_irqs_parent() fully
+ * instantiates the parent chip.
+ *
+ * During consumer probe (e.g., gpiod_to_irq()), irq_create_fwspec_mapping()
+ * allocates the hierarchy. As part of this, irq_domain_set_info() initializes
+ * the top-level irq_desc and calls __irq_set_handler(). If the irq_desc
+ * requires locking, __irq_get_desc_lock() will invoke the child's
+ * .irq_bus_lock before the parent allocation is complete.
+ *
+ * Upstream generic helpers (e.g., irq_chip_mask_parent) blindly dereference
+ * data->parent_data->chip, causing an immediate NULL pointer panic during
+ * this gap. These wrappers check for a valid parent chip to safely drop
+ * premature locking or masking events while the legacy subsystem hierarchy
+ * is still assembling itself.
+ */
+static void mc33978_gpio_irq_mask(struct irq_data *data)
+{
+ struct irq_data *parent = data->parent_data;
+
+ if (parent && parent->chip && parent->chip->irq_mask)
+ parent->chip->irq_mask(parent);
+}
+
+static void mc33978_gpio_irq_unmask(struct irq_data *data)
+{
+ struct irq_data *parent = data->parent_data;
+
+ if (parent && parent->chip && parent->chip->irq_unmask)
+ parent->chip->irq_unmask(parent);
+}
+
+static int mc33978_gpio_irq_set_type(struct irq_data *data, unsigned int type)
+{
+ struct irq_data *parent = data->parent_data;
+
+ if (parent && parent->chip && parent->chip->irq_set_type)
+ return parent->chip->irq_set_type(parent, type);
+
+ return -EINVAL;
+}
+
+static void mc33978_gpio_irq_bus_lock(struct irq_data *data)
+{
+ struct irq_data *parent = data->parent_data;
+
+ if (parent && parent->chip && parent->chip->irq_bus_lock)
+ parent->chip->irq_bus_lock(parent);
+}
+
+static void mc33978_gpio_irq_bus_sync_unlock(struct irq_data *data)
+{
+ struct irq_data *parent = data->parent_data;
+
+ if (parent && parent->chip && parent->chip->irq_bus_sync_unlock)
+ parent->chip->irq_bus_sync_unlock(parent);
+}
+
+static const struct irq_chip mc33978_gpio_irqchip = {
+ .name = "mc33978-gpio",
+ .irq_mask = mc33978_gpio_irq_mask,
+ .irq_unmask = mc33978_gpio_irq_unmask,
+ .irq_set_type = mc33978_gpio_irq_set_type,
+ .irq_bus_lock = mc33978_gpio_irq_bus_lock,
+ .irq_bus_sync_unlock = mc33978_gpio_irq_bus_sync_unlock,
+ .flags = IRQCHIP_IMMUTABLE,
+ GPIOCHIP_IRQ_RESOURCE_HELPERS,
+};
+
+static void mc33978_init_gpio_chip(struct mc33978_pinctrl *mpc,
+ struct device *dev)
+{
+ struct gpio_irq_chip *girq;
+
+ mpc->chip.label = dev_name(dev);
+ mpc->chip.direction_input = mc33978_direction_input;
+ mpc->chip.get = mc33978_get;
+ mpc->chip.get_multiple = mc33978_get_multiple;
+ mpc->chip.direction_output = mc33978_direction_output;
+ mpc->chip.set = mc33978_set;
+ mpc->chip.set_multiple = mc33978_set_multiple;
+ mpc->chip.set_config = gpiochip_generic_config;
+
+ mpc->chip.base = -1;
+ mpc->chip.ngpio = MC33978_NGPIO;
+ mpc->chip.can_sleep = true;
+ mpc->chip.parent = dev;
+ mpc->chip.owner = THIS_MODULE;
+
+ girq = &mpc->chip.irq;
+ gpio_irq_chip_set_chip(girq, &mc33978_gpio_irqchip);
+ girq->fwnode = dev_fwnode(dev);
+ girq->parent_domain = mpc->domain;
+ girq->child_to_parent_hwirq = mc33978_gpio_child_to_parent_hwirq;
+ girq->handler = handle_simple_irq;
+ girq->default_type = IRQ_TYPE_NONE;
+}
+
+static void mc33978_init_pinctrl_desc(struct mc33978_pinctrl *mpc,
+ struct device *dev)
+{
+ mpc->pinctrl_desc.name = dev_name(dev);
+
+ mpc->pinctrl_desc.pctlops = &mc33978_pinctrl_ops;
+ mpc->pinctrl_desc.confops = &mc33978_pinconf_ops;
+ mpc->pinctrl_desc.pins = mc33978_pins;
+ mpc->pinctrl_desc.npins = MC33978_NGPIO;
+ mpc->pinctrl_desc.owner = THIS_MODULE;
+}
+
+static int mc33978_pinctrl_probe(struct platform_device *pdev)
+{
+ struct device *dev = &pdev->dev;
+ struct mc33978_pinctrl *mpc;
+ int ret;
+
+ device_set_node(dev, dev_fwnode(dev->parent));
+
+ mpc = devm_kzalloc(dev, sizeof(*mpc), GFP_KERNEL);
+ if (!mpc)
+ return -ENOMEM;
+
+ mpc->dev = dev;
+
+ mpc->regmap = dev_get_regmap(dev->parent, NULL);
+ if (!mpc->regmap)
+ return dev_err_probe(dev, -ENODEV, "Failed to get parent regmap\n");
+
+ 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);
+
+ mc33978_init_gpio_chip(mpc, dev);
+ mc33978_init_pinctrl_desc(mpc, dev);
+
+ mpc->pctldev = devm_pinctrl_register(dev, &mpc->pinctrl_desc, mpc);
+ if (IS_ERR(mpc->pctldev))
+ return dev_err_probe(dev, PTR_ERR(mpc->pctldev),
+ "can't register pinctrl\n");
+
+ ret = devm_gpiochip_add_data(dev, &mpc->chip, mpc);
+ if (ret)
+ return dev_err_probe(dev, ret, "can't add GPIO chip\n");
+
+ ret = gpiochip_add_pin_range(&mpc->chip, dev_name(dev), 0, 0,
+ MC33978_NGPIO);
+ if (ret)
+ return dev_err_probe(dev, ret, "failed to add pin range\n");
+
+ platform_set_drvdata(pdev, mpc);
+
+ return 0;
+}
+
+static const struct platform_device_id mc33978_pinctrl_id[] = {
+ { "mc33978-pinctrl", },
+ { "mc34978-pinctrl", },
+ { }
+};
+MODULE_DEVICE_TABLE(platform, mc33978_pinctrl_id);
+
+static struct platform_driver mc33978_pinctrl_driver = {
+ .driver = {
+ .name = "mc33978-pinctrl",
+ },
+ .probe = mc33978_pinctrl_probe,
+ .id_table = mc33978_pinctrl_id,
+};
+module_platform_driver(mc33978_pinctrl_driver);
+
+MODULE_AUTHOR("David Jander <david@protonic.nl>");
+MODULE_DESCRIPTION("NXP MC33978/MC34978 pinctrl driver");
+MODULE_LICENSE("GPL");
--
2.47.3
^ permalink raw reply related
* [PATCH v7 5/6] hwmon: add NXP MC33978/MC34978 driver
From: Oleksij Rempel @ 2026-03-27 16:34 UTC (permalink / raw)
To: Guenter Roeck, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Lee Jones, Peter Rosin, Linus Walleij
Cc: Oleksij Rempel, kernel, linux-kernel, devicetree, linux-hwmon,
linux-gpio, David Jander
In-Reply-To: <20260327163450.3287313-1-o.rempel@pengutronix.de>
Add hardware monitoring support for the NXP MC33978/MC34978 Multiple
Switch Detection Interface (MSDI).
The hardware utilizes a clear-on-read FAULT register, but physical
faults remain asserted as long as the underlying condition exists. This
asserts a global FAULT_STAT bit on the SPI bus. To handle this without
trapping the CPU in an interrupt storm, this driver implements the
following architecture:
- Requests a rising-edge nested IRQ (IRQF_TRIGGER_RISING) from the MFD
core to catch the initial 0 -> 1 transition of the global fault state.
- Caches hwmon-specific alarm bits and calculates state edges (XOR) to
isolate alarm transitions from system integrity faults.
- Implements a 1Hz delayed workqueue that polls the hardware as long as
any alarm is active. This compensates for the edge-triggered IRQ by
discovering secondary faults that occur without a rising edge, and
detecting when the hardware clears.
Signed-off-by: Oleksij Rempel <o.rempel@pengutronix.de>
---
changes v7:
- Fix fault monitoring stall by unconditionally rearming on SPI read
errors.
- Fix use-after-free race during unbind by correcting devm registration
order.
changes v6:
- Protect clear-on-read FAULT register and state updates with hwmon_lock().
- Isolate hwmon alarm bits from system integrity bits to fix edge detection.
- Log system faults (SPI/HASH) as level-triggered and add temperature warning
logs.
- Refactor sysfs read callback into smaller subsystem-specific helpers.
- Fix probe race condition by calling mc33978_hwmon_update_faults() at the end
of probe instead of reading raw faults early.
- Expose static datasheet temperature limits via temp1_rated_min and
temp1_rated_max
- Introduce variant-specific hw_info data to correctly report the max
temperature
- Add a 1Hz delayed workqueue that polls the SPI bus while any alarm is active.
changes v5:
- no changes
changes v4:
- no changes
changes v3:
- no changes
changes v2:
- Switch from OF match table to platform_device_id
---
drivers/hwmon/Kconfig | 10 +
drivers/hwmon/Makefile | 1 +
drivers/hwmon/mc33978-hwmon.c | 548 ++++++++++++++++++++++++++++++++++
3 files changed, 559 insertions(+)
create mode 100644 drivers/hwmon/mc33978-hwmon.c
diff --git a/drivers/hwmon/Kconfig b/drivers/hwmon/Kconfig
index 328867242cb3..0c52e8268a20 100644
--- a/drivers/hwmon/Kconfig
+++ b/drivers/hwmon/Kconfig
@@ -700,6 +700,16 @@ config SENSORS_MC13783_ADC
help
Support for the A/D converter on MC13783 and MC13892 PMIC.
+config SENSORS_MC33978
+ tristate "NXP MC33978/MC34978 fault monitoring"
+ depends on MFD_MC33978
+ help
+ If you say yes here you get fault monitoring support for the
+ NXP MC33978/MC34978 Multiple Switch Detection Interface (MSDI).
+
+ This driver can also be built as a module. If so, the module
+ will be called mc33978-hwmon.
+
config SENSORS_MC33XS2410
tristate "MC33XS2410 HWMON support"
depends on PWM_MC33XS2410
diff --git a/drivers/hwmon/Makefile b/drivers/hwmon/Makefile
index 5833c807c688..4c3db5433a10 100644
--- a/drivers/hwmon/Makefile
+++ b/drivers/hwmon/Makefile
@@ -167,6 +167,7 @@ obj-$(CONFIG_SENSORS_MAX31790) += max31790.o
obj-$(CONFIG_MAX31827) += max31827.o
obj-$(CONFIG_SENSORS_MAX77705) += max77705-hwmon.o
obj-$(CONFIG_SENSORS_MC13783_ADC)+= mc13783-adc.o
+obj-$(CONFIG_SENSORS_MC33978) += mc33978-hwmon.o
obj-$(CONFIG_SENSORS_MC33XS2410) += mc33xs2410_hwmon.o
obj-$(CONFIG_SENSORS_MC34VR500) += mc34vr500.o
obj-$(CONFIG_SENSORS_MCP3021) += mcp3021.o
diff --git a/drivers/hwmon/mc33978-hwmon.c b/drivers/hwmon/mc33978-hwmon.c
new file mode 100644
index 000000000000..0333c0315e06
--- /dev/null
+++ b/drivers/hwmon/mc33978-hwmon.c
@@ -0,0 +1,548 @@
+// SPDX-License-Identifier: GPL-2.0-only
+// Copyright (c) 2026 Pengutronix, Oleksij Rempel <kernel@pengutronix.de>
+/*
+ * MC33978/MC34978 Hardware Monitor Driver
+ *
+ * Fault handling model:
+ *
+ * The FAULT register is clear-on-read for most bits, but persistent fault
+ * conditions remain asserted. The MFD core only harvests the aggregate
+ * FAULT_STAT indication from SPI responses and dispatches the hwmon child
+ * IRQ on that basis. Because a persistent fault can keep FAULT_STAT asserted,
+ * secondary fault assertions and fault clear events may not generate a fresh
+ * interrupt edge visible to the hwmon child.
+ *
+ * To provide stable hwmon alarm state, this driver:
+ * - caches only hwmon-relevant alarm bits
+ * - serializes FAULT register reads with cache updates
+ * - polls while any alarm remains active to detect secondary alarms and
+ * clearing edges
+ *
+ * Raw integrity bits such as SPI_ERROR and HASH are logged, but are not
+ * exported through hwmon alarm attributes.
+ */
+
+#include <linux/device.h>
+#include <linux/err.h>
+#include <linux/hwmon.h>
+#include <linux/interrupt.h>
+#include <linux/mod_devicetable.h>
+#include <linux/module.h>
+#include <linux/platform_device.h>
+#include <linux/regmap.h>
+
+#include <linux/mfd/mc33978.h>
+
+/* Operating Temperature Ranges (Datasheet Rated) */
+#define MC33978_TEMP_MIN_MC (-40000)
+#define MC33978_TEMP_MAX_MC 125000
+#define MC34978_TEMP_MAX_MC 105000
+
+/* Thermal Warning threshold (~120C) */
+#define MC33978_TEMP_WARN_MC 120000
+
+/* Thermal Limit / tLIM (>155C) - Hardware enters CWET throttling */
+#define MC33978_TEMP_CRIT_MC 155000
+
+/* Hysteresis for tLIM recovery (Silicon must cool to <140C) */
+#define MC33978_TEMP_HYST_MC 15000
+
+/* VBATP (in0) IC Level thresholds */
+#define MC33978_VBATP_OV_MV 36000 /* Overvoltage limit */
+#define MC33978_VBATP_FUNC_MV 28000 /* Functional/Normal boundary */
+#define MC33978_VBATP_DEGRADED_MV 6000 /* Degraded parametrics start */
+#define MC33978_VBATP_UVLO_MV 4500 /* UV Rising Threshold max */
+
+/* VDDQ (in1) Logic Supply thresholds */
+#define MC33978_VDDQ_MAX_MV 5250 /* Operating Condition max */
+#define MC33978_VDDQ_MIN_MV 3000 /* Operating Condition min */
+#define MC33978_VDDQ_UV_MV 2800 /* UV Falling Threshold max */
+
+#define MC33978_FAULT_POLL_INTERVAL_MS 1000
+
+enum mc33978_hwmon_in_channels {
+ MC33978_IN_VBATP,
+ MC33978_IN_VDDQ,
+};
+
+struct mc33978_hwmon_priv {
+ struct device *dev;
+ struct device *hwmon_dev;
+ struct regmap *map;
+
+ const struct mc33978_hwmon_hw_info *hw_info;
+
+ int fault_irq;
+
+ /* Cached hwmon alarm bits, serialized by hwmon_lock(). */
+ u32 last_faults;
+
+ /*
+ * Background polling worker. Active only when faults are present
+ * to compensate for the lack of clearing/secondary edge interrupts.
+ */
+ struct delayed_work poll_work;
+};
+
+struct mc33978_hwmon_hw_info {
+ long rated_max_temp;
+};
+
+static const struct mc33978_hwmon_hw_info hwmon_hwinfo_mc33978 = {
+ .rated_max_temp = MC33978_TEMP_MAX_MC,
+};
+
+static const struct mc33978_hwmon_hw_info hwmon_hwinfo_mc34978 = {
+ .rated_max_temp = MC34978_TEMP_MAX_MC,
+};
+
+static int mc33978_hwmon_read_fault(struct mc33978_hwmon_priv *priv,
+ u32 *faults)
+{
+ unsigned int val;
+ int ret;
+
+ ret = regmap_read(priv->map, MC33978_REG_FAULT, &val);
+ if (ret)
+ return ret;
+
+ *faults = val;
+
+ return 0;
+}
+
+static void mc33978_hwmon_report_faults(struct mc33978_hwmon_priv *priv,
+ u32 new_faults)
+{
+ if (!new_faults)
+ return;
+
+ if (new_faults & MC33978_FAULT_TEMP_WARN)
+ dev_warn_ratelimited(priv->dev, "Temperature warning threshold reached\n");
+
+ if (new_faults & MC33978_FAULT_OT)
+ dev_crit_ratelimited(priv->dev, "Over-temperature fault detected!\n");
+
+ if (new_faults & MC33978_FAULT_OV)
+ dev_crit_ratelimited(priv->dev, "Over-voltage fault detected!\n");
+
+ if (new_faults & MC33978_FAULT_UV)
+ dev_err_ratelimited(priv->dev, "Under-voltage fault detected!\n");
+}
+
+static int mc33978_hwmon_update_faults(struct mc33978_hwmon_priv *priv)
+{
+ u32 old_faults, new_faults, changed_faults;
+ u32 alarm_faults = 0;
+ u32 faults = 0;
+ bool rearm;
+ int ret;
+
+ /*
+ * Serialize clear-on-read FAULT register access with cached alarm state
+ * updates and hwmon sysfs readers.
+ */
+ hwmon_lock(priv->hwmon_dev);
+ old_faults = priv->last_faults;
+
+ ret = mc33978_hwmon_read_fault(priv, &faults);
+ if (ret) {
+ hwmon_unlock(priv->hwmon_dev);
+ dev_err_ratelimited(priv->dev,
+ "failed to read fault register: %pe\n",
+ ERR_PTR(ret));
+ /*
+ * Always retry on read failure. If we drop the heartbeat during
+ * the initial fault before caching it, the edge-triggered IRQ
+ * will never fire again and permanently stall fault monitoring.
+ */
+ rearm = true;
+ goto out_poll;
+ }
+
+ /* Isolate hwmon alarm bits from system integrity bits */
+ alarm_faults = faults & MC33978_FAULT_ALARM_MASK;
+ changed_faults = alarm_faults ^ old_faults;
+ new_faults = alarm_faults & ~old_faults;
+ priv->last_faults = alarm_faults;
+
+ hwmon_unlock(priv->hwmon_dev);
+
+ if (faults & MC33978_FAULT_SPI_ERROR)
+ dev_err_ratelimited(priv->dev, "SPI communication error detected\n");
+ if (faults & MC33978_FAULT_HASH)
+ dev_err_ratelimited(priv->dev, "SPI register hash mismatch detected\n");
+
+ if (new_faults)
+ mc33978_hwmon_report_faults(priv, new_faults);
+
+ if (changed_faults & MC33978_FAULT_UV)
+ hwmon_notify_event(priv->hwmon_dev, hwmon_in,
+ hwmon_in_lcrit_alarm, MC33978_IN_VBATP);
+
+ if (changed_faults & MC33978_FAULT_OV)
+ hwmon_notify_event(priv->hwmon_dev, hwmon_in,
+ hwmon_in_crit_alarm, MC33978_IN_VBATP);
+
+ if (changed_faults & MC33978_FAULT_TEMP_WARN)
+ hwmon_notify_event(priv->hwmon_dev, hwmon_temp,
+ hwmon_temp_max_alarm, 0);
+
+ if (changed_faults & MC33978_FAULT_OT)
+ hwmon_notify_event(priv->hwmon_dev, hwmon_temp,
+ hwmon_temp_crit_alarm, 0);
+
+ if (changed_faults)
+ hwmon_notify_event(priv->hwmon_dev, hwmon_chip,
+ hwmon_chip_alarms, 0);
+
+ rearm = !!alarm_faults;
+
+out_poll:
+ /*
+ * If any alarms are currently active, the global FAULT_STAT bit remains
+ * asserted. The hardware will not generate a new rising edge interrupt
+ * if a secondary fault occurs, nor will it interrupt when faults clear.
+ * Schedule a poll to detect both clearing edges and secondary alarms.
+ */
+ if (rearm)
+ mod_delayed_work(system_wq, &priv->poll_work,
+ msecs_to_jiffies(MC33978_FAULT_POLL_INTERVAL_MS));
+
+ return ret;
+}
+
+static irqreturn_t mc33978_hwmon_fault_irq(int irq, void *data)
+{
+ struct mc33978_hwmon_priv *priv = data;
+
+ mc33978_hwmon_update_faults(priv);
+
+ return IRQ_HANDLED;
+}
+
+static void mc33978_hwmon_poll_work(struct work_struct *work)
+{
+ struct mc33978_hwmon_priv *priv =
+ container_of(work, struct mc33978_hwmon_priv, poll_work.work);
+
+ mc33978_hwmon_update_faults(priv);
+}
+
+static umode_t mc33978_hwmon_is_visible(const void *data,
+ enum hwmon_sensor_types type,
+ u32 attr, int channel)
+{
+ switch (type) {
+ case hwmon_chip:
+ if (attr == hwmon_chip_alarms)
+ return 0444;
+ break;
+
+ case hwmon_temp:
+ switch (attr) {
+ case hwmon_temp_max:
+ case hwmon_temp_crit:
+ case hwmon_temp_crit_hyst:
+ case hwmon_temp_max_alarm:
+ case hwmon_temp_crit_alarm:
+ case hwmon_temp_rated_min:
+ case hwmon_temp_rated_max:
+ return 0444;
+ default:
+ break;
+ }
+ break;
+
+ case hwmon_in:
+ switch (attr) {
+ case hwmon_in_label:
+ case hwmon_in_max:
+ case hwmon_in_min:
+ case hwmon_in_lcrit:
+ return 0444;
+ case hwmon_in_crit:
+ if (channel == MC33978_IN_VBATP)
+ return 0444;
+ break;
+ case hwmon_in_crit_alarm:
+ case hwmon_in_lcrit_alarm:
+ if (channel == MC33978_IN_VBATP)
+ return 0444;
+ break;
+ }
+ break;
+ default:
+ break;
+ }
+
+ return 0;
+}
+
+static int mc33978_hwmon_read_chip(struct mc33978_hwmon_priv *priv, u32 attr,
+ long *val)
+{
+ if (attr == hwmon_chip_alarms) {
+ *val = priv->last_faults;
+ return 0;
+ }
+
+ return -EOPNOTSUPP;
+}
+
+static int mc33978_hwmon_read_in_vbatp(struct mc33978_hwmon_priv *priv,
+ u32 attr, long *val)
+{
+ switch (attr) {
+ case hwmon_in_crit:
+ *val = MC33978_VBATP_OV_MV;
+ return 0;
+ case hwmon_in_max:
+ *val = MC33978_VBATP_FUNC_MV;
+ return 0;
+ case hwmon_in_min:
+ *val = MC33978_VBATP_DEGRADED_MV;
+ return 0;
+ case hwmon_in_lcrit:
+ *val = MC33978_VBATP_UVLO_MV;
+ return 0;
+ case hwmon_in_crit_alarm:
+ *val = !!(priv->last_faults & MC33978_FAULT_OV);
+ return 0;
+ case hwmon_in_lcrit_alarm:
+ *val = !!(priv->last_faults & MC33978_FAULT_UV);
+ return 0;
+ default:
+ break;
+ }
+
+ return -EOPNOTSUPP;
+}
+
+static int mc33978_hwmon_read_in_vddq(u32 attr, long *val)
+{
+ switch (attr) {
+ case hwmon_in_max:
+ *val = MC33978_VDDQ_MAX_MV;
+ return 0;
+ case hwmon_in_min:
+ *val = MC33978_VDDQ_MIN_MV;
+ return 0;
+ case hwmon_in_lcrit:
+ *val = MC33978_VDDQ_UV_MV;
+ return 0;
+ default:
+ break;
+ }
+
+ return -EOPNOTSUPP;
+}
+
+static int mc33978_hwmon_read_in(struct mc33978_hwmon_priv *priv, u32 attr,
+ int channel, long *val)
+{
+ switch (channel) {
+ case MC33978_IN_VBATP:
+ return mc33978_hwmon_read_in_vbatp(priv, attr, val);
+ case MC33978_IN_VDDQ:
+ return mc33978_hwmon_read_in_vddq(attr, val);
+ default:
+ break;
+ }
+
+ return -EOPNOTSUPP;
+}
+
+static int mc33978_hwmon_read_temp(struct mc33978_hwmon_priv *priv, u32 attr,
+ long *val)
+{
+ switch (attr) {
+ case hwmon_temp_max:
+ *val = MC33978_TEMP_WARN_MC;
+ return 0;
+ case hwmon_temp_crit:
+ *val = MC33978_TEMP_CRIT_MC;
+ return 0;
+ case hwmon_temp_crit_hyst:
+ *val = MC33978_TEMP_CRIT_MC - MC33978_TEMP_HYST_MC;
+ return 0;
+ case hwmon_temp_max_alarm:
+ *val = !!(priv->last_faults & MC33978_FAULT_TEMP_WARN);
+ return 0;
+ case hwmon_temp_crit_alarm:
+ *val = !!(priv->last_faults & MC33978_FAULT_OT);
+ return 0;
+ case hwmon_temp_rated_min:
+ *val = MC33978_TEMP_MIN_MC;
+ return 0;
+ case hwmon_temp_rated_max:
+ *val = priv->hw_info->rated_max_temp;
+ return 0;
+ default:
+ break;
+ }
+
+ return -EOPNOTSUPP;
+}
+
+static int mc33978_hwmon_read(struct device *dev,
+ enum hwmon_sensor_types type,
+ u32 attr, int channel, long *val)
+{
+ struct mc33978_hwmon_priv *priv = dev_get_drvdata(dev);
+
+ switch (type) {
+ case hwmon_chip:
+ return mc33978_hwmon_read_chip(priv, attr, val);
+ case hwmon_in:
+ return mc33978_hwmon_read_in(priv, attr, channel, val);
+ case hwmon_temp:
+ return mc33978_hwmon_read_temp(priv, attr, val);
+ default:
+ break;
+ }
+
+ return -EOPNOTSUPP;
+}
+
+static int mc33978_hwmon_read_string(struct device *dev,
+ enum hwmon_sensor_types type,
+ u32 attr, int channel, const char **str)
+{
+ /* Only in_label is supported for string reads */
+ if (type != hwmon_in || attr != hwmon_in_label)
+ return -EOPNOTSUPP;
+
+ switch (channel) {
+ case MC33978_IN_VBATP:
+ *str = "VBATP";
+ return 0;
+ case MC33978_IN_VDDQ:
+ *str = "VDDQ";
+ return 0;
+ default:
+ return -EINVAL;
+ }
+}
+
+static const struct hwmon_channel_info * const mc33978_hwmon_info[] = {
+ HWMON_CHANNEL_INFO(chip,
+ HWMON_C_ALARMS),
+ HWMON_CHANNEL_INFO(temp,
+ HWMON_T_MAX | HWMON_T_CRIT | HWMON_T_CRIT_HYST |
+ HWMON_T_MAX_ALARM | HWMON_T_CRIT_ALARM |
+ HWMON_T_RATED_MIN | HWMON_T_RATED_MAX),
+ HWMON_CHANNEL_INFO(in,
+ /* Index 0: MC33978_IN_VBATP */
+ HWMON_I_LABEL | HWMON_I_CRIT | HWMON_I_MAX |
+ HWMON_I_MIN | HWMON_I_LCRIT |
+ HWMON_I_CRIT_ALARM | HWMON_I_LCRIT_ALARM,
+
+ /* Index 1: MC33978_IN_VDDQ */
+ HWMON_I_LABEL | HWMON_I_MAX | HWMON_I_MIN |
+ HWMON_I_LCRIT),
+ NULL
+};
+
+static const struct hwmon_ops mc33978_hwmon_ops = {
+ .is_visible = mc33978_hwmon_is_visible,
+ .read_string = mc33978_hwmon_read_string,
+ .read = mc33978_hwmon_read,
+};
+
+static const struct hwmon_chip_info mc33978_hwmon_chip_info = {
+ .ops = &mc33978_hwmon_ops,
+ .info = mc33978_hwmon_info,
+};
+
+static void mc33978_hwmon_action_cancel_work(void *data)
+{
+ struct mc33978_hwmon_priv *priv = data;
+
+ cancel_delayed_work_sync(&priv->poll_work);
+}
+
+static int mc33978_hwmon_probe(struct platform_device *pdev)
+{
+ const struct platform_device_id *id;
+ struct device *dev = &pdev->dev;
+ struct mc33978_hwmon_priv *priv;
+ struct device *hwmon_dev;
+ int ret;
+
+ priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
+ if (!priv)
+ return -ENOMEM;
+
+ priv->dev = dev;
+
+ id = platform_get_device_id(pdev);
+ if (!id || !id->driver_data)
+ return dev_err_probe(dev, -EINVAL, "missing device match data\n");
+
+ priv->hw_info = (const struct mc33978_hwmon_hw_info *)id->driver_data;
+
+ priv->map = dev_get_regmap(dev->parent, NULL);
+ if (!priv->map)
+ return dev_err_probe(dev, -ENODEV, "failed to get regmap\n");
+
+ platform_set_drvdata(pdev, priv);
+
+ INIT_DELAYED_WORK(&priv->poll_work, mc33978_hwmon_poll_work);
+
+ priv->fault_irq = platform_get_irq(pdev, 0);
+ if (priv->fault_irq < 0)
+ return priv->fault_irq;
+
+ hwmon_dev = devm_hwmon_device_register_with_info(dev, "mc33978", priv,
+ &mc33978_hwmon_chip_info,
+ NULL);
+ if (IS_ERR(hwmon_dev))
+ return dev_err_probe(dev, PTR_ERR(hwmon_dev),
+ "failed to register hwmon device\n");
+
+ priv->hwmon_dev = hwmon_dev;
+
+ ret = devm_add_action_or_reset(dev, mc33978_hwmon_action_cancel_work,
+ priv);
+ if (ret)
+ return ret;
+
+ /*
+ * The FAULT child IRQ is generated by the MFD core from transitions of
+ * the aggregated FAULT_STAT bus state. Request a rising-edge nested
+ * IRQ so the core dispatches the hwmon fault handler when faults become
+ * active.
+ *
+ * Fault clearing and secondary faults while FAULT_STAT remains asserted
+ * are handled by the hwmon polling path.
+ */
+ ret = devm_request_threaded_irq(dev, priv->fault_irq, NULL,
+ mc33978_hwmon_fault_irq,
+ IRQF_ONESHOT | IRQF_TRIGGER_RISING,
+ dev_name(dev), priv);
+ if (ret)
+ return dev_err_probe(dev, ret, "failed to request fault IRQ\n");
+
+ return mc33978_hwmon_update_faults(priv);
+}
+
+static const struct platform_device_id mc33978_hwmon_id[] = {
+ { "mc33978-hwmon", (kernel_ulong_t)&hwmon_hwinfo_mc33978 },
+ { "mc34978-hwmon", (kernel_ulong_t)&hwmon_hwinfo_mc34978 },
+ { }
+};
+MODULE_DEVICE_TABLE(platform, mc33978_hwmon_id);
+
+static struct platform_driver mc33978_hwmon_driver = {
+ .driver = {
+ .name = "mc33978-hwmon",
+ },
+ .probe = mc33978_hwmon_probe,
+ .id_table = mc33978_hwmon_id,
+};
+module_platform_driver(mc33978_hwmon_driver);
+
+MODULE_AUTHOR("Oleksij Rempel <kernel@pengutronix.de>");
+MODULE_DESCRIPTION("NXP MC33978/MC34978 Hardware Monitor Driver");
+MODULE_LICENSE("GPL");
--
2.47.3
^ permalink raw reply related
* [PATCH v7 1/6] dt-bindings: pinctrl: add NXP MC33978/MC34978 MSDI
From: Oleksij Rempel @ 2026-03-27 16:34 UTC (permalink / raw)
To: Guenter Roeck, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Lee Jones, Peter Rosin, Linus Walleij
Cc: Oleksij Rempel, kernel, linux-kernel, devicetree, linux-hwmon,
linux-gpio, David Jander
In-Reply-To: <20260327163450.3287313-1-o.rempel@pengutronix.de>
Add device tree binding documentation for the NXP MC33978 and MC34978
Multiple Switch Detection Interface (MSDI) devices.
The MC33978 and MC34978 differ primarily in their operating temperature
ranges. While not software-detectable, providing specific compatible
strings allows the hwmon subsystem to correctly interpret thermal
thresholds and hardware faults.
These ICs monitor up to 22 mechanical switch contacts in automotive and
industrial environments. They provide configurable wetting currents to
break through contact oxidation and feature extensive hardware
protection against thermal overload and voltage transients (load
dumps/brown-outs).
The device interfaces via SPI. While it provides multiple functions, its
primary hardware purpose is pin/switch control. To accurately represent
the hardware as a single physical integrated circuit without unnecessary
DT overhead, all functions are flattened into a single pinctrl node:
- pinctrl: Exposing the 22 switch inputs (SG/SP pins) as a GPIO controller
and managing their pin configurations.
- hwmon: Exposing critical hardware faults (OT, OV, UV) and static
voltage/temperature thresholds.
- mux: Controlling the 24-to-1 analog multiplexer to route pin voltages,
internal temperature, or battery voltage to an external SoC ADC.
Signed-off-by: Oleksij Rempel <o.rempel@pengutronix.de>
Reviewed-by: Rob Herring (Arm) <robh@kernel.org>
Reviewed-by: Linus Walleij <linusw@kernel.org>
---
changes v7:
- no changes
changes v6:
- add Reviewed-by: Rob Herring (Arm) <robh@kernel.org>
- add Reviewed-by: Linus Walleij <linusw@kernel.org>
changes v5:
- Commit Message: Added justification for distinct compatible strings
based on temperature ranges.
- Restricted pins property to an explicit enum of valid hardware pins
changes v4:
- Drop the standalone mfd/nxp,mc33978.yaml schema entirely.
- Move the unified device binding to bindings/pinctrl/nxp,mc33978.yaml,
- Remove the dedicated child node compatible strings (nxp,mc33978-pinctrl).
- Flatten the pinctrl/gpio properties directly into the main SPI device
node.
changes v3:
- Drop regular expression pattern from pinctrl child node and define
it as a standard property
- Reorder required properties list in MFD binding
- Remove stray blank line from the MFD binding devicetree example
- Replace unevaluatedProperties with additionalProperties in the pinctrl
binding
changes v2:
- Squashed MFD, pinctrl, hwmon, and mux bindings into a single patch
- Removed the empty hwmon child node
- Folded the mux-controller node into the parent MFD node
- Added vbatp-supply and vddq-supply to the required properties block
- Changed the example node name from mc33978@0 to gpio@0
- Removed unnecessary literal block scalars (|) from descriptions
- Documented SG, SP, and SB pin acronyms in the pinctrl description
- Added consumer polarity guidance (GPIO_ACTIVE_LOW/HIGH) for SG/SB
inputs, with a note on output circuit dependency
- Updated commit message
---
.../bindings/pinctrl/nxp,mc33978.yaml | 153 ++++++++++++++++++
1 file changed, 153 insertions(+)
create mode 100644 Documentation/devicetree/bindings/pinctrl/nxp,mc33978.yaml
diff --git a/Documentation/devicetree/bindings/pinctrl/nxp,mc33978.yaml b/Documentation/devicetree/bindings/pinctrl/nxp,mc33978.yaml
new file mode 100644
index 000000000000..e6227e95fee4
--- /dev/null
+++ b/Documentation/devicetree/bindings/pinctrl/nxp,mc33978.yaml
@@ -0,0 +1,153 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/pinctrl/nxp,mc33978.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: NXP MC33978/MC34978 Multiple Switch Detection Interface
+
+maintainers:
+ - David Jander <david@protonic.nl>
+ - Oleksij Rempel <o.rempel@pengutronix.de>
+
+description: |
+ The MC33978 and MC34978 are Multiple Switch Detection Interface (MSDI)
+ devices with 22 switch inputs, integrated fault detection, and analog
+ multiplexer (AMUX) for voltage/temperature monitoring.
+
+ Pin numbering:
+ - Pins 0-13: SG0-SG13 (Switch-to-Ground inputs). These pins monitor
+ contacts closed to ground and typically require GPIO_ACTIVE_LOW
+ flags when used as digital inputs.
+ - Pins 14-21: SP0-SP7 (Programmable inputs). These can be configured
+ as SG (Switch-to-Ground) or SB (Switch-to-Battery) inputs. SB
+ inputs monitor contacts closed to the battery voltage and typically
+ require GPIO_ACTIVE_HIGH flags when used as digital inputs.
+
+ Output Emulation:
+ The hardware lacks standard push-pull output drivers. Outputs are emulated
+ by toggling the programmable wetting current sources (acting as pull-ups
+ or pull-downs) and the hardware tri-state registers. Because of this
+ physical constraint:
+ - Consumers using pins as outputs MUST flag them with GPIO_OPEN_DRAIN or
+ GPIO_OPEN_SOURCE in the device tree.
+ - Push-pull configurations are physically unsupported.
+ - The active polarity depends entirely on the external circuit (e.g., how
+ an LED is wired) and must be flagged accordingly by the consumer.
+
+allOf:
+ - $ref: /schemas/spi/spi-peripheral-props.yaml#
+
+properties:
+ compatible:
+ enum:
+ - nxp,mc33978
+ - nxp,mc34978
+
+ reg:
+ maxItems: 1
+ description: SPI chip select number
+
+ spi-max-frequency:
+ maximum: 8000000
+ description: Maximum SPI clock frequency (up to 8 MHz)
+
+ interrupts:
+ maxItems: 1
+ description:
+ INT_B pin interrupt. Active-low, indicates pin state changes or
+ fault conditions.
+
+ interrupt-controller: true
+
+ '#interrupt-cells':
+ const: 2
+ description:
+ First cell is the IRQ number (0-21 for pins, 22 for faults).
+ Second cell is the trigger type (IRQ_TYPE_* from interrupt-controller.h).
+
+ '#mux-control-cells':
+ const: 0
+ description:
+ Present if the device AMUX selector is used as a mux provider.
+ Consumers (e.g. io-channel-mux) must provide settle-time-us for the
+ external ADC sampling path.
+
+ vddq-supply:
+ description: Digital supply voltage
+
+ vbatp-supply:
+ description: Battery/power supply
+
+ gpio-controller: true
+
+ '#gpio-cells':
+ const: 2
+
+ ngpios:
+ const: 22
+
+patternProperties:
+ '^.*-grp$':
+ type: object
+ $ref: /schemas/pinctrl/pincfg-node.yaml#
+ additionalProperties: false
+ description: Pin configuration subnodes.
+ properties:
+ pins:
+ items:
+ enum: [sg0, sg1, sg2, sg3, sg4, sg5, sg6, sg7, sg8, sg9,
+ sg10, sg11, sg12, sg13, sp0, sp1, sp2, sp3,
+ sp4, sp5, sp6, sp7]
+
+ bias-pull-up: true
+ bias-pull-down: true
+ bias-high-impedance: true
+
+required:
+ - compatible
+ - reg
+ - interrupts
+ - interrupt-controller
+ - '#interrupt-cells'
+ - vddq-supply
+ - vbatp-supply
+ - gpio-controller
+ - '#gpio-cells'
+
+unevaluatedProperties: false
+
+examples:
+ - |
+ #include <dt-bindings/interrupt-controller/irq.h>
+ #include <dt-bindings/gpio/gpio.h>
+
+ spi {
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ msdi: gpio@0 {
+ compatible = "nxp,mc33978";
+ reg = <0>;
+ spi-max-frequency = <4000000>;
+
+ interrupt-parent = <&gpiog>;
+ interrupts = <9 IRQ_TYPE_LEVEL_LOW>;
+ interrupt-controller;
+ #interrupt-cells = <2>;
+
+ vddq-supply = <®_3v3>;
+ vbatp-supply = <®_12v>;
+
+ #mux-control-cells = <0>;
+
+ gpio-controller;
+ #gpio-cells = <2>;
+ ngpios = <22>;
+
+ door-grp {
+ pins = "sg0";
+ bias-high-impedance;
+ };
+ };
+ };
--
2.47.3
^ permalink raw reply related
* [PATCH v7 6/6] mux: add NXP MC33978/MC34978 AMUX driver
From: Oleksij Rempel @ 2026-03-27 16:34 UTC (permalink / raw)
To: Guenter Roeck, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Lee Jones, Peter Rosin, Linus Walleij
Cc: Oleksij Rempel, kernel, linux-kernel, devicetree, linux-hwmon,
linux-gpio, David Jander
In-Reply-To: <20260327163450.3287313-1-o.rempel@pengutronix.de>
Add a mux-control driver for the 24-to-1 analog multiplexer (AMUX)
embedded in the NXP MC33978/MC34978 Multiple Switch Detection
Interface (MSDI) devices.
Signed-off-by: Oleksij Rempel <o.rempel@pengutronix.de>
---
changes v7:
- Simplify the return path and local variable assignment in
mc33978_mux_set().
- Change idle_state to a signed integer to properly handle negative MUX
subsystem constants.
- Default to MUX_IDLE_AS_IS when the "idle-state" device tree property
is missing.
- Explicitly reject MUX_IDLE_DISCONNECT since the hardware does not
support disconnecting the multiplexer.
changes v6:
- parse optional idle-state property
- validate idle-state against available AMUX channels
- lower-case probe error messages
changes v5:
- no changes
changes v4:
- no changes
changes v3:
- no changes
changes v2:
- Add missing <linux/err.h> include.
- Add platform_device_id table
---
drivers/mux/Kconfig | 14 ++++
drivers/mux/Makefile | 2 +
drivers/mux/mc33978-mux.c | 136 ++++++++++++++++++++++++++++++++++++++
3 files changed, 152 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..2cc7abc659a8
--- /dev/null
+++ b/drivers/mux/mc33978-mux.c
@@ -0,0 +1,136 @@
+// SPDX-License-Identifier: GPL-2.0-only
+// Copyright (c) 2026 Pengutronix, Oleksij Rempel <kernel@pengutronix.de>
+/*
+ * MC33978/MC34978 Analog Multiplexer (AMUX) Driver
+ *
+ * This driver provides mux-control for the 24-to-1 analog multiplexer.
+ * The AMUX routes one of the following signals to the external AMUX pin:
+ * - Channels 0-13: SG0-SG13 switch voltages
+ * - Channels 14-21: SP0-SP7 switch voltages
+ * - Channel 22: Internal temperature diode
+ * - Channel 23: Battery voltage (VBATP)
+ *
+ * Consumer drivers (typically IIO ADC drivers) use the mux-control
+ * subsystem to select which signal to measure.
+ *
+ * Architecture:
+ * The MC33978 does not have an internal ADC. Instead, it routes analog
+ * signals to an external AMUX pin that must be connected to an external
+ * ADC (such as the SoC's internal ADC). The IIO subsystem is responsible
+ * for coordinating the mux selection and ADC sampling.
+ */
+
+#include <linux/device.h>
+#include <linux/err.h>
+#include <linux/mod_devicetable.h>
+#include <linux/module.h>
+#include <linux/mux/driver.h>
+#include <linux/platform_device.h>
+#include <linux/property.h>
+#include <linux/regmap.h>
+
+#include <linux/mfd/mc33978.h>
+
+/* AMUX_CTRL register field definitions */
+#define MC33978_AMUX_CTRL_MASK GENMASK(5, 0) /* 6-bit channel select */
+
+struct mc33978_mux_priv {
+ struct device *dev;
+ struct regmap *map;
+};
+
+static int mc33978_mux_set(struct mux_control *mux, int state)
+{
+ struct mux_chip *mux_chip = mux->chip;
+ struct mc33978_mux_priv *priv = mux_chip_priv(mux_chip);
+ int ret;
+
+ if (state < 0 || state >= MC33978_NUM_AMUX_CH)
+ return -EINVAL;
+
+ ret = regmap_update_bits(priv->map, MC33978_REG_AMUX_CTRL,
+ MC33978_AMUX_CTRL_MASK, state);
+ if (ret)
+ dev_err(priv->dev, "failed to set AMUX channel %d: %d\n",
+ state, ret);
+
+ return ret;
+}
+
+static const struct mux_control_ops mc33978_mux_ops = {
+ .set = mc33978_mux_set,
+};
+
+static int mc33978_mux_probe(struct platform_device *pdev)
+{
+ struct device *dev = &pdev->dev;
+ struct mc33978_mux_priv *priv;
+ struct mux_chip *mux_chip;
+ struct mux_control *mux;
+ s32 idle_state;
+ int ret;
+
+ mux_chip = devm_mux_chip_alloc(dev, 1, sizeof(*priv));
+ if (IS_ERR(mux_chip))
+ return dev_err_probe(dev, PTR_ERR(mux_chip), "failed to allocate mux chip\n");
+
+ /* Borrow the parent's DT node so consumers can find this mux chip */
+ device_set_node(&mux_chip->dev, dev_fwnode(dev->parent));
+
+ priv = mux_chip_priv(mux_chip);
+ priv->dev = dev;
+
+ priv->map = dev_get_regmap(dev->parent, NULL);
+ if (!priv->map)
+ return dev_err_probe(dev, -ENODEV, "failed to get parent regmap\n");
+
+ mux_chip->ops = &mc33978_mux_ops;
+
+ mux = &mux_chip->mux[0];
+ mux->states = MC33978_NUM_AMUX_CH;
+
+ ret = device_property_read_u32(&mux_chip->dev, "idle-state",
+ (u32 *)&idle_state);
+ if (ret < 0 && ret != -EINVAL) {
+ return dev_err_probe(dev, ret, "failed to parse idle-state\n");
+ } else if (ret == -EINVAL) {
+ mux->idle_state = MUX_IDLE_AS_IS;
+ } else {
+ if (idle_state == MUX_IDLE_DISCONNECT)
+ return dev_err_probe(dev, -EINVAL,
+ "idle-disconnect not supported by hardware\n");
+ if (idle_state != MUX_IDLE_AS_IS &&
+ (idle_state < 0 || idle_state >= MC33978_NUM_AMUX_CH))
+ return dev_err_probe(dev, -EINVAL, "invalid idle-state %d\n",
+ idle_state);
+ mux->idle_state = idle_state;
+ }
+
+ ret = devm_mux_chip_register(dev, mux_chip);
+ if (ret)
+ return dev_err_probe(dev, ret, "failed to register mux chip\n");
+
+ platform_set_drvdata(pdev, mux_chip);
+
+ return 0;
+}
+
+static const struct platform_device_id mc33978_mux_id[] = {
+ { "mc33978-mux", },
+ { "mc34978-mux", },
+ { }
+};
+MODULE_DEVICE_TABLE(platform, mc33978_mux_id);
+
+static struct platform_driver mc33978_mux_driver = {
+ .driver = {
+ .name = "mc33978-mux",
+ },
+ .probe = mc33978_mux_probe,
+ .id_table = mc33978_mux_id,
+};
+module_platform_driver(mc33978_mux_driver);
+
+MODULE_AUTHOR("Oleksij Rempel <kernel@pengutronix.de>");
+MODULE_DESCRIPTION("NXP MC33978/MC34978 Analog Multiplexer Driver");
+MODULE_LICENSE("GPL");
--
2.47.3
^ permalink raw reply related
* [PATCH v7 0/6] mfd: Add support for NXP MC33978/MC34978 MSDI
From: Oleksij Rempel @ 2026-03-27 16:34 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
changes v7:
- drop gpiolib irq fix and make pinctrl more robust against NULL point
dereference.
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 (5):
dt-bindings: pinctrl: add NXP MC33978/MC34978 MSDI
mfd: add NXP MC33978/MC34978 core driver
pinctrl: core: Make pin group callbacks optional for pin-only drivers
hwmon: add NXP MC33978/MC34978 driver
mux: add NXP MC33978/MC34978 AMUX driver
.../bindings/pinctrl/nxp,mc33978.yaml | 153 +++
drivers/hwmon/Kconfig | 10 +
drivers/hwmon/Makefile | 1 +
drivers/hwmon/mc33978-hwmon.c | 548 ++++++++++
drivers/mfd/Kconfig | 15 +
drivers/mfd/Makefile | 2 +
drivers/mfd/mc33978.c | 933 ++++++++++++++++++
drivers/mux/Kconfig | 14 +
drivers/mux/Makefile | 2 +
drivers/mux/mc33978-mux.c | 136 +++
drivers/pinctrl/Kconfig | 14 +
drivers/pinctrl/Makefile | 1 +
drivers/pinctrl/core.c | 41 +-
drivers/pinctrl/pinconf.c | 9 +-
drivers/pinctrl/pinctrl-mc33978.c | 836 ++++++++++++++++
include/linux/mfd/mc33978.h | 92 ++
16 files changed, 2800 insertions(+), 7 deletions(-)
create mode 100644 Documentation/devicetree/bindings/pinctrl/nxp,mc33978.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
page: next (older) | prev (newer) | latest
- recent:[subjects (threaded)|topics (new)|topics (active)]
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox