From nobody Wed Apr 1 11:15:18 2026 Received: from metis.whiteo.stw.pengutronix.de (metis.whiteo.stw.pengutronix.de [185.203.201.7]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 9C99C363C6C for ; Tue, 31 Mar 2026 17:16:38 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=185.203.201.7 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1774977401; cv=none; b=qqqycz9UV9ChsGqXDFMF81oHBuvlRgzQZ/xITGwQsYZMe1BoamMmPBldTWNTPczigBW/AlfxRVqhC4NzwvbrtMBPrvecFWOHo9d6n3OKZVjPzjzFko3wPUTgOkBD7n7FXoV4L7XTnuuEpFMsf0oSBXYBYubZG4e2iJhmvyLLtkw= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1774977401; c=relaxed/simple; bh=Lfa7R/rTVD7ZdnmGvEoZUArw5sNEqa9VXP83IU5olIk=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=So0aqqBoIE5eiKoTBypikmeZA32uDxPnFbxBg8tXa596iiqPXvNwtgDpcS9WMm9se8XPphAg3pLvtCwPuG2Lz9zG7n6Es2hiLRphyxaUF8sMMN61CKxsUbJakGS5aXcS2iUE6hf+OJuReZF8DoTTOlsZcYa5XgSvc2oyChEejnk= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=pengutronix.de; spf=pass smtp.mailfrom=pengutronix.de; arc=none smtp.client-ip=185.203.201.7 Authentication-Results: smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=pengutronix.de Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=pengutronix.de Received: from drehscheibe.grey.stw.pengutronix.de ([2a0a:edc0:0:c01:1d::a2]) by metis.whiteo.stw.pengutronix.de with esmtps (TLS1.3:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.92) (envelope-from ) id 1w7chK-0004mQ-E4; Tue, 31 Mar 2026 19:16:14 +0200 Received: from dude04.red.stw.pengutronix.de ([2a0a:edc0:0:1101:1d::ac] helo=dude04) by drehscheibe.grey.stw.pengutronix.de with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1w7chJ-0034jQ-21; Tue, 31 Mar 2026 19:16:13 +0200 Received: from ore by dude04 with local (Exim 4.98.2) (envelope-from ) id 1w7chJ-00000000QZF-28MN; Tue, 31 Mar 2026 19:16:13 +0200 From: Oleksij Rempel To: Guenter Roeck , Rob Herring , Krzysztof Kozlowski , Conor Dooley , Lee Jones , Peter Rosin , Linus Walleij Cc: David Jander , Oleksij Rempel , kernel@pengutronix.de, linux-kernel@vger.kernel.org, devicetree@vger.kernel.org, linux-hwmon@vger.kernel.org, linux-gpio@vger.kernel.org Subject: [PATCH v9 4/6] pinctrl: add NXP MC33978/MC34978 pinctrl driver Date: Tue, 31 Mar 2026 19:16:10 +0200 Message-ID: <20260331171612.102018-5-o.rempel@pengutronix.de> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260331171612.102018-1-o.rempel@pengutronix.de> References: <20260331171612.102018-1-o.rempel@pengutronix.de> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable X-SA-Exim-Connect-IP: 2a0a:edc0:0:c01:1d::a2 X-SA-Exim-Mail-From: ore@pengutronix.de X-SA-Exim-Scanned: No (on metis.whiteo.stw.pengutronix.de); SAEximRunCond expanded to false X-PTX-Original-Recipient: linux-kernel@vger.kernel.org Content-Type: text/plain; charset="utf-8" From: David Jander Add pin control and GPIO driver for the NXP MC33978/MC34978 Multiple Switch Detection Interface (MSDI) devices. This driver exposes the 22 mechanical switch detection inputs (14 Switch-to-Ground, 8 Programmable) as standard GPIOs. Key features implemented: - GPIO read/write: Translates physical switch states (open/closed) to logical GPIO levels based on the configured switch topology (Switch-to-Ground vs. Switch-to-Battery). - Emulated Output: Allows setting pins "high" or "low" by manipulating the tri-state registers and hardware pull topologies. - Interrupt routing: Proxies GPIO interrupt requests to the irq_domain managed by the parent MFD core driver via a hierarchical irq_chip. Signed-off-by: David Jander Co-developed-by: Oleksij Rempel Signed-off-by: Oleksij Rempel Reviewed-by: Linus Walleij --- changes v9: - Resolve probe fwnode directly from parent (`dev_fwnode(dev->parent)`) and fail early with `-ENODEV` if the parent firmware node is missing. - Set child device node from this validated parent fwnode. - Replace mutex_init() with devm_mutex_init() - Add gpiochip_disable_irq/enable_irq calls in mask/unmask callbacks for proper gpiolib IRQ state tracking with IRQCHIP_IMMUTABLE - Set DOMAIN_BUS_WIRED token for GPIO IRQ domain to distinguish from parent MFD domain sharing same fwnode - Add explanatory comment about fwnode sharing and bus token isolation to prevent domain shadowing concerns - select GPIOLIB_IRQCHIP and IRQ_DOMAIN_HIERARCHY changes v8: - Fix comment documentation to state the driver implements a hierarchical irq_chip instead of proxying .to_irq(). - Add missing include. - Add .irq_set_wake =3D irq_chip_set_wake_parent to the gpio_irq_chip to properly proxy wake-up configuration to the parent domain. - Replace irq_find_host() with irq_find_matching_fwnode() during probe to support parent domain lookup on non-OF platforms. changes v7: - Refactor I/O state reading and tri-state updates for SG/SB topologies - Fix open-drain and open-source pinconf emulation - Make direction_input a no-op to prevent overriding pinctrl bias - Add defensive wrappers for IRQ proxying to prevent NULL pointer panics - Add missing mutex guards to pinconf and get operations - Convert generic internal variables to u32 and add lockdep assertions changes v6: - no changes changes v5: - no changes changes v4: - add Reviewed-by: Linus Walleij ... - Replace the of_device_id match table with a platform_device_id table - Add device_set_node(dev, dev_fwnode(dev->parent)) during probe - Remove the check for a missing dev->of_node changes v3: - replace manual mutex_lock()/mutex_unlock() paths with guard(mutex) - Unify error checking style by replacing if (ret < 0) with if (ret) - Migrate from a custom .to_irq callback to a hierarchical gpio_irq_chip - Implement .irq_bus_lock and .irq_bus_sync_unlock proxies to properly cascade SPI bus lock operations to the parent MFD domain - Set girq->handler to handle_simple_irq changes v2: - Translate all remaining German comments to English. - Remove unnecessary #ifdef CONFIG_OF wrappers around dt_node_to_map. - Add detailed comments to mc33978_get() and mc33978_get_multiple() explain= ing the hardware comparator logic (1 =3D closed, 0 =3D open) and justifying t= he 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 hwm= on). - Drop dummy pin group callbacks (get_groups_count, etc.). This relies on a preparatory patch in this series making these callbacks optional in the c= ore. - 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_WSHI= FT to static inline functions. - Remove artifacts from previous interrupt handling implementations. - Address minor formatting and whitespace nits. --- drivers/pinctrl/Kconfig | 16 + drivers/pinctrl/Makefile | 1 + drivers/pinctrl/pinctrl-mc33978.c | 865 ++++++++++++++++++++++++++++++ 3 files changed, 882 insertions(+) create mode 100644 drivers/pinctrl/pinctrl-mc33978.c diff --git a/drivers/pinctrl/Kconfig b/drivers/pinctrl/Kconfig index afecd9407f53..64f9c5b1aacb 100644 --- a/drivers/pinctrl/Kconfig +++ b/drivers/pinctrl/Kconfig @@ -388,6 +388,22 @@ config PINCTRL_MAX77620 function in alternate mode. This driver also configure push-pull, open drain, FPS slots etc. =20 +config PINCTRL_MC33978 + tristate "MC33978/MC34978 industrial input controller support" + depends on MFD_MC33978 + select GPIOLIB + select GPIOLIB_IRQCHIP + select IRQ_DOMAIN_HIERARCHY + select GENERIC_PINCONF + help + Say Y here to enable support for NXP MC33978/MC34978 Multiple + Switch Detection Interface (MSDI) devices. This driver provides + pinctrl and GPIO interfaces for the 22 mechanical switch inputs + (14 Switch-to-Ground, 8 Programmable). + + It allows reading switch states, configuring hardware pull + topologies, and handling interrupts for state changes. + config PINCTRL_MCP23S08_I2C tristate select REGMAP_I2C diff --git a/drivers/pinctrl/Makefile b/drivers/pinctrl/Makefile index f7d5d5f76d0c..afb58fb5a197 100644 --- a/drivers/pinctrl/Makefile +++ b/drivers/pinctrl/Makefile @@ -40,6 +40,7 @@ obj-$(CONFIG_PINCTRL_XWAY) +=3D pinctrl-xway.o obj-$(CONFIG_PINCTRL_LPC18XX) +=3D pinctrl-lpc18xx.o obj-$(CONFIG_PINCTRL_MAX7360) +=3D pinctrl-max7360.o obj-$(CONFIG_PINCTRL_MAX77620) +=3D pinctrl-max77620.o +obj-$(CONFIG_PINCTRL_MC33978) +=3D pinctrl-mc33978.o obj-$(CONFIG_PINCTRL_MCP23S08_I2C) +=3D pinctrl-mcp23s08_i2c.o obj-$(CONFIG_PINCTRL_MCP23S08_SPI) +=3D pinctrl-mcp23s08_spi.o obj-$(CONFIG_PINCTRL_MCP23S08) +=3D pinctrl-mcp23s08.o diff --git a/drivers/pinctrl/pinctrl-mc33978.c b/drivers/pinctrl/pinctrl-mc= 33978.c new file mode 100644 index 000000000000..415e43199aa3 --- /dev/null +++ b/drivers/pinctrl/pinctrl-mc33978.c @@ -0,0 +1,865 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Copyright (C) 2024 David Jander , Protonic Holland + * Copyright (C) 2026 Oleksij Rempel , Pengutronix + * + * MC33978/MC34978 Multiple Switch Detection Interface - Pinctrl/GPIO Driv= er + * + * 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-Batt= ery) + * This is dictated by the READ_IN register where bits [21:14] =3D SP[7:0] + * and bits [13:0] =3D SG[13:0]. + * + * Register Organization: + * Configuration registers are generally paired. The _SP register at offse= t 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 contact= s. + * - Because these are active current sources, specifying an ohmic value f= or + * pull-up/down biases is physically invalid. The driver ignores ohm argum= ents. + * - 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. Consequ= ently, + * 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 sourc= es + * 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 groun= d; + * 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 =3D closed, 0 =3D open). For SG pins, a closed switch (~0V) reports = as '1'. + * To align with gpiolib expectations where ~0V equals a physical logical = '0', + * this driver explicitly inverts the hardware status for all SG-configured + * pins before reporting them. + * + * Interrupts: + * The physical INT_B line and threaded IRQ domain are managed centrally by + * the parent MFD core. This driver implements a hierarchical irq_chip + * to proxy masking/unmasking and configuration to the parent domain. + * + * Written by David Jander + * + * Datasheet: + * https://www.nxp.com/docs/en/data-sheet/MC33978.pdf + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#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; + + 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[] =3D { + 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 >=3D MC33978_NUM_SG; +} + +/* Choose register offset for _SG/_SP registers. reg is always the _SP add= r. */ +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 =3D 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 ma= sk, + u32 val) +{ + int ret; + + ret =3D 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 =3D { + .dt_node_to_map =3D pinconf_generic_dt_node_to_map_pin, + .dt_free_map =3D 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 =3D 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 =3D MC33978_TRISTATE; + return 0; + } + + /* Pins 0..13 only support pull-up */ + if (!mc33978_is_sp(pin)) { + *val =3D MC33978_PU; + return 0; + } + + /* Check pin pull direction for pins 14..21 */ + ret =3D mc33978_read(mpc, MC33978_REG_CONFIG, &data); + if (ret) + return ret; + + if (data & MC33978_PINMASK(pin)) + *val =3D MC33978_PD; + else + *val =3D MC33978_PU; + + return 0; +} + +static int mc33978_set_pull(struct mc33978_pinctrl *mpc, unsigned int pin,= int val) +{ + u32 mask =3D MC33978_PINMASK(pin); + int ret; + + lockdep_assert_held(&mpc->lock); + + /* SG pins physically lack pull-down current sources */ + if (val =3D=3D MC33978_PD && !mc33978_is_sp(pin)) + return -EINVAL; + + /* Configure direction (Exclusively for SP pins) */ + if (mc33978_is_sp(pin) && val !=3D MC33978_TRISTATE) { + ret =3D mc33978_update_bits(mpc, MC33978_REG_CONFIG, mask, + (val =3D=3D 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 =3D=3D MC33978_TRISTATE) ? mask : 0); +} + +static const unsigned int mc33978_wet_mA[] =3D { 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 =3D 0; i < ARRAY_SIZE(mc33978_wet_mA); i++) { + if (val =3D=3D 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 =3D mc33978_read(mpc, mc33978_wreg(MC33978_REG_WET_SP, pin), &data); + if (ret) + return ret; + + data &=3D MC33978_WMASK(pin); + data >>=3D mc33978_wshift(pin); + + if (data >=3D ARRAY_SIZE(mc33978_wet_mA)) + return -EINVAL; + + *val =3D mc33978_wet_mA[data]; + + return 0; +} + +static int mc33978_pinconf_get(struct pinctrl_dev *pctldev, unsigned int p= in, + unsigned long *config) +{ + struct mc33978_pinctrl *mpc =3D pinctrl_dev_get_drvdata(pctldev); + enum pin_config_param param =3D pinconf_to_config_param(*config); + u32 arg; + u32 data; + int ret; + + guard(mutex)(&mpc->lock); + + switch (param) { + case PIN_CONFIG_BIAS_PULL_UP: + ret =3D mc33978_get_pull(mpc, pin, &data); + if (ret) + return ret; + if (data !=3D MC33978_PU) + return -EINVAL; + arg =3D 1; + break; + case PIN_CONFIG_BIAS_PULL_DOWN: + ret =3D mc33978_get_pull(mpc, pin, &data); + if (ret) + return ret; + if (data !=3D MC33978_PD) + return -EINVAL; + arg =3D 1; + break; + case PIN_CONFIG_DRIVE_OPEN_DRAIN: + if (!mc33978_is_sp(pin)) + return -EINVAL; + + ret =3D mc33978_read(mpc, MC33978_REG_CONFIG, &data); + if (ret) + return ret; + + if (!(data & MC33978_PINMASK(pin))) + return -EINVAL; + arg =3D 1; + break; + case PIN_CONFIG_DRIVE_OPEN_SOURCE: + if (mc33978_is_sp(pin)) { + ret =3D mc33978_read(mpc, MC33978_REG_CONFIG, &data); + if (ret) + return ret; + + if (data & MC33978_PINMASK(pin)) + return -EINVAL; + } + arg =3D 1; + break; + case PIN_CONFIG_BIAS_DISABLE: + case PIN_CONFIG_BIAS_HIGH_IMPEDANCE: + ret =3D mc33978_get_pull(mpc, pin, &data); + if (ret) + return ret; + if (data !=3D MC33978_TRISTATE) + return -EINVAL; + arg =3D 1; + break; + case PIN_CONFIG_DRIVE_STRENGTH: + ret =3D mc33978_get_ds(mpc, pin, &data); + if (ret) + return ret; + arg =3D 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 =3D 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 resistan= ce + * scales dynamically with the fluctuating board voltage (VBATP), computing + * a static ohm value is physically invalid. + * The driver intentionally ignores resistance arguments during configurat= ion + * and continuously reports 0 ohms to the pinctrl framework. + */ +static int mc33978_pinconf_set(struct pinctrl_dev *pctldev, unsigned int p= in, + unsigned long *configs, unsigned int num_configs) +{ + struct mc33978_pinctrl *mpc =3D pinctrl_dev_get_drvdata(pctldev); + enum pin_config_param param; + int ret =3D 0; + u32 arg; + int i; + + guard(mutex)(&mpc->lock); + + for (i =3D 0; i < num_configs; i++) { + param =3D pinconf_to_config_param(configs[i]); + arg =3D 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 =3D mc33978_update_bits(mpc, MC33978_REG_CONFIG, + MC33978_PINMASK(pin), 0); + break; + case PIN_CONFIG_BIAS_PULL_UP: + ret =3D 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 =3D 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 =3D 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 =3D mc33978_set_pull(mpc, pin, MC33978_TRISTATE); + break; + case PIN_CONFIG_DRIVE_STRENGTH_UA: + arg /=3D 1000; + fallthrough; + case PIN_CONFIG_DRIVE_STRENGTH: + ret =3D 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 =3D { + .pin_config_get =3D mc33978_pinconf_get, + .pin_config_set =3D mc33978_pinconf_set, + .is_generic =3D true, +}; + +static int mc33978_direction_input(struct gpio_chip *chip, unsigned int of= fset) +{ + /* 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 =3D closed, 0 =3D 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 =3D 0; + int ret; + + ret =3D 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 =3D mc33978_read(mpc, MC33978_REG_CONFIG, &config_reg); + if (ret) + return ret; + } + + /* + * Create an inversion mask for all pins currently operating in + * Switch-to-Ground (SG) topology. SG pins always have pull-ups. + * For SP pins, CONFIG bit value 0 =3D Switch-to-Ground (PU), + * CONFIG bit value 1 =3D Switch-to-Battery (PD). + */ + inv_mask =3D MC33978_SG_MASK | + (~(config_reg << MC33978_NUM_SG) & MC33978_SP_MASK); + + *state =3D (status ^ inv_mask) & mask; + + return 0; +} + +static int mc33978_get(struct gpio_chip *chip, unsigned int offset) +{ + struct mc33978_pinctrl *mpc =3D gpiochip_get_data(chip); + unsigned long state; + int ret; + + guard(mutex)(&mpc->lock); + + ret =3D 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 =3D gpiochip_get_data(chip); + unsigned long state; + int ret; + + guard(mutex)(&mpc->lock); + + ret =3D mc33978_read_in_state(mpc, *mask, &state); + if (ret) + return ret; + + *bits =3D (*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=3D1 turns it ON, value=3D0 is High-Z. + * For Open-Drain (Pull-Down): value=3D0 turns it ON, value=3D1 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 =3D (mask & MC33978_SG_MASK) >> MC33978_SG_SHIFT; + u32 sgbits =3D (bits & MC33978_SG_MASK) >> MC33978_SG_SHIFT; + u32 spmask =3D (mask & MC33978_SP_MASK) >> MC33978_SP_SHIFT; + u32 spbits =3D (bits & MC33978_SP_MASK) >> MC33978_SP_SHIFT; + u32 config_reg =3D 0; + int ret =3D 0; + + if (spmask) { + /* Read topology: 1 =3D PD (Open-Drain), 0 =3D PU (Open-Source) */ + ret =3D 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=3D1 to be High-Z. + */ + spbits ^=3D (config_reg & spmask); + + ret =3D 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 =3D mc33978_update_bits(mpc, MC33978_REG_TRI_SG, sgmask, + ~sgbits); + + return ret; +} + +static int mc33978_set(struct gpio_chip *chip, unsigned int offset, int va= lue) +{ + struct mc33978_pinctrl *mpc =3D gpiochip_get_data(chip); + u32 mask =3D BIT(offset); + u32 bits =3D 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 =3D 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 o= ffset, + 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 =3D child_type; + *parent =3D 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() initial= izes + * 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 dereferen= ce + * data->parent_data->chip, causing an immediate NULL pointer panic during + * this gap. These wrappers check for a valid parent chip to safely drop + * premature locking or masking events while the legacy subsystem hierarchy + * is still assembling itself. + */ +static void mc33978_gpio_irq_mask(struct irq_data *data) +{ + struct gpio_chip *gc =3D irq_data_get_irq_chip_data(data); + struct irq_data *parent =3D data->parent_data; + + if (parent && parent->chip && parent->chip->irq_mask) + parent->chip->irq_mask(parent); + gpiochip_disable_irq(gc, data->hwirq); +} + +static void mc33978_gpio_irq_unmask(struct irq_data *data) +{ + struct gpio_chip *gc =3D irq_data_get_irq_chip_data(data); + struct irq_data *parent =3D data->parent_data; + + gpiochip_enable_irq(gc, data->hwirq); + if (parent && parent->chip && parent->chip->irq_unmask) + parent->chip->irq_unmask(parent); +} + +static int mc33978_gpio_irq_set_type(struct irq_data *data, unsigned int t= ype) +{ + struct irq_data *parent =3D 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 =3D 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 =3D 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 =3D { + .name =3D "mc33978-gpio", + .irq_mask =3D mc33978_gpio_irq_mask, + .irq_unmask =3D mc33978_gpio_irq_unmask, + .irq_set_type =3D mc33978_gpio_irq_set_type, + .irq_bus_lock =3D mc33978_gpio_irq_bus_lock, + .irq_bus_sync_unlock =3D mc33978_gpio_irq_bus_sync_unlock, + .irq_set_wake =3D irq_chip_set_wake_parent, + .flags =3D 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 =3D dev_name(dev); + mpc->chip.direction_input =3D mc33978_direction_input; + mpc->chip.get =3D mc33978_get; + mpc->chip.get_multiple =3D mc33978_get_multiple; + mpc->chip.direction_output =3D mc33978_direction_output; + mpc->chip.set =3D mc33978_set; + mpc->chip.set_multiple =3D mc33978_set_multiple; + mpc->chip.set_config =3D gpiochip_generic_config; + + mpc->chip.base =3D -1; + mpc->chip.ngpio =3D MC33978_NGPIO; + mpc->chip.can_sleep =3D true; + mpc->chip.parent =3D dev; + mpc->chip.owner =3D THIS_MODULE; + + girq =3D &mpc->chip.irq; + gpio_irq_chip_set_chip(girq, &mc33978_gpio_irqchip); + /* + * Share parent's DT fwnode. This does NOT cause IRQ domain shadowing + * because the parent MFD domain uses DOMAIN_BUS_NEXUS while this GPIO + * domain will use DOMAIN_BUS_WIRED (set after gpiochip registration). + * Domain lookups match on both fwnode AND bus_token, ensuring proper + * domain isolation. See crystalcove GPIO driver for similar pattern. + */ + girq->fwnode =3D dev_fwnode(dev); + girq->parent_domain =3D mpc->domain; + girq->child_to_parent_hwirq =3D mc33978_gpio_child_to_parent_hwirq; + girq->handler =3D handle_simple_irq; + girq->default_type =3D IRQ_TYPE_NONE; +} + +static void mc33978_init_pinctrl_desc(struct mc33978_pinctrl *mpc, + struct device *dev) +{ + mpc->pinctrl_desc.name =3D dev_name(dev); + + mpc->pinctrl_desc.pctlops =3D &mc33978_pinctrl_ops; + mpc->pinctrl_desc.confops =3D &mc33978_pinconf_ops; + mpc->pinctrl_desc.pins =3D mc33978_pins; + mpc->pinctrl_desc.npins =3D MC33978_NGPIO; + mpc->pinctrl_desc.owner =3D THIS_MODULE; +} + +static int mc33978_pinctrl_probe(struct platform_device *pdev) +{ + struct device *dev =3D &pdev->dev; + struct fwnode_handle *fwnode; + struct mc33978_pinctrl *mpc; + int ret; + + fwnode =3D dev_fwnode(dev->parent); + if (!fwnode) + return dev_err_probe(dev, -ENODEV, + "Missing parent firmware node\n"); + + device_set_node(dev, fwnode); + + mpc =3D devm_kzalloc(dev, sizeof(*mpc), GFP_KERNEL); + if (!mpc) + return -ENOMEM; + + mpc->dev =3D dev; + + mpc->regmap =3D dev_get_regmap(dev->parent, NULL); + if (!mpc->regmap) + return dev_err_probe(dev, -ENODEV, "Failed to get parent regmap\n"); + + /* Find parent MFD IRQ domain (uses DOMAIN_BUS_NEXUS token) */ + mpc->domain =3D irq_find_matching_fwnode(fwnode, DOMAIN_BUS_NEXUS); + if (!mpc->domain) + return dev_err_probe(dev, -ENODEV, "Failed to find parent IRQ domain\n"); + + ret =3D devm_mutex_init(dev, &mpc->lock); + if (ret) + return ret; + + mc33978_init_gpio_chip(mpc, dev); + mc33978_init_pinctrl_desc(mpc, dev); + + mpc->pctldev =3D 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 =3D devm_gpiochip_add_data(dev, &mpc->chip, mpc); + if (ret) + return dev_err_probe(dev, ret, "can't add GPIO chip\n"); + + /* + * Distinguish GPIO IRQ domain from parent MFD domain sharing the same + * fwnode. Matches the pattern used by other GPIO drivers (e.g., + * crystalcove). DOMAIN_BUS_WIRED indicates this domain represents + * actual GPIO pin interrupts (wired lines). + */ + irq_domain_update_bus_token(mpc->chip.irq.domain, DOMAIN_BUS_WIRED); + + ret =3D 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[] =3D { + { "mc33978-pinctrl", }, + { "mc34978-pinctrl", }, + { } +}; +MODULE_DEVICE_TABLE(platform, mc33978_pinctrl_id); + +static struct platform_driver mc33978_pinctrl_driver =3D { + .driver =3D { + .name =3D "mc33978-pinctrl", + }, + .probe =3D mc33978_pinctrl_probe, + .id_table =3D mc33978_pinctrl_id, +}; +module_platform_driver(mc33978_pinctrl_driver); + +MODULE_AUTHOR("David Jander "); +MODULE_DESCRIPTION("NXP MC33978/MC34978 pinctrl driver"); +MODULE_LICENSE("GPL"); --=20 2.47.3