diff --git a/MAINTAINERS b/MAINTAINERS
index f1d696f29f11..cd1773d39dd8 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -5011,6 +5011,7 @@ W:	https://github.com/CirrusLogic/linux-drivers/wiki
 T:	git https://github.com/CirrusLogic/linux-drivers.git
 F:	Documentation/devicetree/bindings/mfd/cirrus,cs48l32.yaml
 F:	Documentation/devicetree/bindings/mfd/cirrus,madera.yaml
+F:	Documentation/devicetree/bindings/pinctrl/cirrus,cs48l32.yaml
 F:	Documentation/devicetree/bindings/pinctrl/cirrus,madera.yaml
 F:	Documentation/devicetree/bindings/sound/cirrus,madera.yaml
 F:	drivers/gpio/gpio-madera*
diff --git a/drivers/pinctrl/cirrus/Kconfig b/drivers/pinctrl/cirrus/Kconfig
index 530426a74f75..c51192bde87a 100644
--- a/drivers/pinctrl/cirrus/Kconfig
+++ b/drivers/pinctrl/cirrus/Kconfig
@@ -30,3 +30,8 @@ config PINCTRL_CS47L90
 
 config PINCTRL_CS47L92
 	bool
+
+config PINCTRL_CS48L32
+	tristate
+	select PINMUX
+	select GENERIC_PINCONF
diff --git a/drivers/pinctrl/cirrus/Makefile b/drivers/pinctrl/cirrus/Makefile
index a484518c840e..18290f6be00c 100644
--- a/drivers/pinctrl/cirrus/Makefile
+++ b/drivers/pinctrl/cirrus/Makefile
@@ -19,4 +19,6 @@ ifeq ($(CONFIG_PINCTRL_CS47L92),y)
 pinctrl-madera-objs		+= pinctrl-cs47l92.o
 endif
 
+obj-$(CONFIG_PINCTRL_CS48L32)	+= pinctrl-cs48l32.o
+
 obj-$(CONFIG_PINCTRL_MADERA)	+= pinctrl-madera.o
diff --git a/drivers/pinctrl/cirrus/pinctrl-cs48l32.c b/drivers/pinctrl/cirrus/pinctrl-cs48l32.c
new file mode 100644
index 000000000000..cb5031d6d0ce
--- /dev/null
+++ b/drivers/pinctrl/cirrus/pinctrl-cs48l32.c
@@ -0,0 +1,932 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Pinctrl for Cirrus Logic CS48L32
+ *
+ * Copyright (C) 2017-2018, 2020, 2022 Cirrus Logic, Inc. and
+ *               Cirrus Logic International Semiconductor Ltd.
+ */
+#include <linux/err.h>
+#include <linux/mfd/cs48l32/core.h>
+#include <linux/mfd/cs48l32/registers.h>
+#include <linux/module.h>
+#include <linux/pinctrl/pinctrl.h>
+#include <linux/pinctrl/pinmux.h>
+#include <linux/pinctrl/pinconf.h>
+#include <linux/pinctrl/pinconf-generic.h>
+#include <linux/platform_device.h>
+#include <linux/regmap.h>
+#include <linux/slab.h>
+
+#include "../pinctrl-utils.h"
+#include "pinctrl-cs48l32.h"
+
+/*
+ * Pins are named after their GPIO number
+ * NOTE: IDs are zero-indexed for coding convenience
+ */
+static const struct pinctrl_pin_desc cs48l32_pins[] = {
+	PINCTRL_PIN(0, "gpio1"),
+	PINCTRL_PIN(1, "gpio2"),
+	PINCTRL_PIN(2, "gpio3"),
+	PINCTRL_PIN(3, "gpio4"),
+	PINCTRL_PIN(4, "gpio5"),
+	PINCTRL_PIN(5, "gpio6"),
+	PINCTRL_PIN(6, "gpio7"),
+	PINCTRL_PIN(7, "gpio8"),
+	PINCTRL_PIN(8, "gpio9"),
+	PINCTRL_PIN(9, "gpio10"),
+	PINCTRL_PIN(10, "gpio11"),
+	PINCTRL_PIN(11, "gpio12"),
+	PINCTRL_PIN(12, "gpio13"),
+	PINCTRL_PIN(13, "gpio14"),
+	PINCTRL_PIN(14, "gpio15"),
+	PINCTRL_PIN(15, "gpio16"),
+};
+
+/*
+ * All single-pin functions can be mapped to any GPIO, however pinmux applies
+ * functions to pin groups and only those groups declared as supporting that
+ * function. To make this work we must put each pin in its own dummy group so
+ * that the functions can be described as applying to all pins.
+ * Since these do not correspond to anything in the actual hardware - they are
+ * merely an adaptation to pinctrl's view of the world - we use the same name
+ * as the pin to avoid confusion when comparing with datasheet instructions
+ */
+static const char * const cs48l32_pin_single_group_names[] = {
+	"gpio1",  "gpio2",  "gpio3",  "gpio4",  "gpio5",  "gpio6",  "gpio7",
+	"gpio8",  "gpio9",  "gpio10", "gpio11", "gpio12", "gpio13", "gpio14",
+	"gpio15", "gpio16",
+};
+
+/* set of pin numbers for single-pin groups */
+static const unsigned int cs48l32_pin_single_group_pins[] = {
+	0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
+};
+
+static const char * const cs48l32_asp1_group_names[] = { "asp1" };
+static const char * const cs48l32_asp2_group_names[] = { "asp2" };
+static const char * const cs48l32_in1pdm_group_names[] = { "in1-pdm" };
+static const char * const cs48l32_in2pdm_group_names[] = { "in2-pdm" };
+static const char * const cs48l32_spi2_group_names[] = { "spi2" };
+
+/*
+ * alt-functions always apply to only one group, other functions always
+ * apply to all pins
+ */
+static const struct {
+	const char *name;
+	const char * const *group_names;
+	u32 func;
+} cs48l32_mux_funcs[] = {
+	{
+		.name = "asp1",
+		.group_names = cs48l32_asp1_group_names,
+		.func = 0x000
+	},
+	{
+		.name = "asp2",
+		.group_names = cs48l32_asp2_group_names,
+		.func = 0x000
+	},
+	{
+		.name = "in1-pdm",
+		.group_names = cs48l32_in1pdm_group_names,
+		.func = 0x000
+	},
+	{
+		.name = "in2-pdm",
+		.group_names = cs48l32_in2pdm_group_names,
+		.func = 0x000,
+	},
+	{
+		.name = "spi2",
+		.group_names = cs48l32_spi2_group_names,
+		.func = 0x000
+	},
+	{
+		.name = "io",
+		.group_names = cs48l32_pin_single_group_names,
+		.func = 0x001
+	},
+	{
+		.name = "dsp-gpio",
+		.group_names = cs48l32_pin_single_group_names,
+		.func = 0x002
+	},
+	{
+		.name = "irq1",
+		.group_names = cs48l32_pin_single_group_names,
+		.func = 0x003
+	},
+	{
+		.name = "fll1-clk",
+		.group_names = cs48l32_pin_single_group_names,
+		.func = 0x010
+	},
+	{
+		.name = "fll1-lock",
+		.group_names = cs48l32_pin_single_group_names,
+		.func = 0x018
+	},
+	{
+		.name = "opclk",
+		.group_names = cs48l32_pin_single_group_names,
+		.func = 0x048
+	},
+	{
+		.name = "opclk-dsp",
+		.group_names = cs48l32_pin_single_group_names,
+		.func = 0x04a
+	},
+	{
+		.name = "uart",
+		.group_names = cs48l32_pin_single_group_names,
+		.func = 0x04c
+	},
+	{
+		.name = "input-path-signal-detect",
+		.group_names = cs48l32_pin_single_group_names,
+		.func = 0x08c
+	},
+	{
+		.name = "ultrasonic-in1-activity-detect",
+		.group_names = cs48l32_pin_single_group_names,
+		.func = 0x090
+	},
+	{
+		.name = "ultrasonic-in2-activity-detect",
+		.group_names = cs48l32_pin_single_group_names,
+		.func = 0x092
+	},
+	{
+		.name = "dma-ch0-programmable-transfer-complete",
+		.group_names = cs48l32_pin_single_group_names,
+		.func = 0x190
+	},
+	{
+		.name = "dma-ch1-programmable-transfer-complete",
+		.group_names = cs48l32_pin_single_group_names,
+		.func = 0x191
+	},
+	{
+		.name = "dma-ch2-programmable-transfer-complete",
+		.group_names = cs48l32_pin_single_group_names,
+		.func = 0x192
+	},
+	{
+		.name = "dma-ch3-programmable-transfer-complete",
+		.group_names = cs48l32_pin_single_group_names,
+		.func = 0x193
+	},
+	{
+		.name = "dma-ch4-programmable-transfer-complete",
+		.group_names = cs48l32_pin_single_group_names,
+		.func = 0x194
+	},
+	{
+		.name = "dma-ch5-programmable-transfer-complete",
+		.group_names = cs48l32_pin_single_group_names,
+		.func = 0x195
+	},
+	{
+		.name = "dma-ch6-programmable-transfer-complete",
+		.group_names = cs48l32_pin_single_group_names,
+		.func = 0x196
+	},
+	{
+		.name = "dma-ch7-programmable-transfer-complete",
+		.group_names = cs48l32_pin_single_group_names,
+		.func = 0x197
+	},
+	{
+		.name = "sample-rate-change-trigger-a",
+		.group_names = cs48l32_pin_single_group_names,
+		.func = 0x214
+	},
+	{
+		.name = "sample-rate-change-trigger-b",
+		.group_names = cs48l32_pin_single_group_names,
+		.func = 0x215
+	},
+	{
+		.name = "sample-rate-change-trigger-c",
+		.group_names = cs48l32_pin_single_group_names,
+		.func = 0x216
+	},
+	{
+		.name = "sample-rate-change-trigger-d",
+		.group_names = cs48l32_pin_single_group_names,
+		.func = 0x217
+	},
+	{
+		.name = "timer1-irq-ch1",
+		.group_names = cs48l32_pin_single_group_names,
+		.func = 0x230
+	},
+	{
+		.name = "timer1-irq-ch2",
+		.group_names = cs48l32_pin_single_group_names,
+		.func = 0x231
+	},
+	{
+		.name = "timer1-irq-ch3",
+		.group_names = cs48l32_pin_single_group_names,
+		.func = 0x232
+	},
+	{
+		.name = "timer1-irq-ch4",
+		.group_names = cs48l32_pin_single_group_names,
+		.func = 0x233
+	},
+	{
+		.name = "timer2-irq-ch1",
+		.group_names = cs48l32_pin_single_group_names,
+		.func = 0x234
+	},
+	{
+		.name = "timer2-irq-ch2",
+		.group_names = cs48l32_pin_single_group_names,
+		.func = 0x235
+	},
+	{
+		.name = "timer2-irq-ch3",
+		.group_names = cs48l32_pin_single_group_names,
+		.func = 0x236
+	},
+	{
+		.name = "timer2-irq-ch4",
+		.group_names = cs48l32_pin_single_group_names,
+		.func = 0x237
+	},
+	{
+		.name = "timer3-irq-ch1",
+		.group_names = cs48l32_pin_single_group_names,
+		.func = 0x238
+	},
+	{
+		.name = "timer3-irq-ch2",
+		.group_names = cs48l32_pin_single_group_names,
+		.func = 0x239
+	},
+	{
+		.name = "timer3-irq-ch3",
+		.group_names = cs48l32_pin_single_group_names,
+		.func = 0x23a
+	},
+	{
+		.name = "timer3-irq-ch4",
+		.group_names = cs48l32_pin_single_group_names,
+		.func = 0x23b
+	},
+	{
+		.name = "timer4-irq-ch1",
+		.group_names = cs48l32_pin_single_group_names,
+		.func = 0x23c
+	},
+	{
+		.name = "timer4-irq-ch2",
+		.group_names = cs48l32_pin_single_group_names,
+		.func = 0x23d
+	},
+	{
+		.name = "timer4-irq-ch3",
+		.group_names = cs48l32_pin_single_group_names,
+		.func = 0x23e
+	},
+	{
+		.name = "timer4-irq-ch4",
+		.group_names = cs48l32_pin_single_group_names,
+		.func = 0x23f
+	},
+	{
+		.name = "timer5-irq-ch1",
+		.group_names = cs48l32_pin_single_group_names,
+		.func = 0x240
+	},
+	{
+		.name = "timer5-irq-ch2",
+		.group_names = cs48l32_pin_single_group_names,
+		.func = 0x241
+	},
+	{
+		.name = "timer5-irq-ch3",
+		.group_names = cs48l32_pin_single_group_names,
+		.func = 0x242
+	},
+	{
+		.name = "timer5-irq-ch4",
+		.group_names = cs48l32_pin_single_group_names,
+		.func = 0x243
+	},
+	{
+		.name = "timer-1",
+		.group_names = cs48l32_pin_single_group_names,
+		.func = 0x250
+	},
+	{
+		.name = "timer-2",
+		.group_names = cs48l32_pin_single_group_names,
+		.func = 0x251
+	},
+	{
+		.name = "timer-3",
+		.group_names = cs48l32_pin_single_group_names,
+		.func = 0x252
+	},
+	{
+		.name = "timer-4",
+		.group_names = cs48l32_pin_single_group_names,
+		.func = 0x253
+	},
+	{
+		.name = "timer-5",
+		.group_names = cs48l32_pin_single_group_names,
+		.func = 0x254
+	},
+};
+
+/* Note - all 1 less than in datasheet because these are zero-indexed */
+static const unsigned int cs48l32_asp1_pins[] = { 2, 3, 4, 5 };
+static const unsigned int cs48l32_asp2_pins[] = { 6, 7, 8, 9 };
+static const unsigned int cs48l32_spi2_pins[] = { 10, 11, 12, 13, 14, 15 };
+
+static const struct cs48l32_pin_groups cs48l32_pin_groups[] = {
+	{ "asp1", cs48l32_asp1_pins, ARRAY_SIZE(cs48l32_asp1_pins) },
+	{ "asp2", cs48l32_asp2_pins, ARRAY_SIZE(cs48l32_asp2_pins) },
+	{ "spi2", cs48l32_spi2_pins, ARRAY_SIZE(cs48l32_spi2_pins) },
+};
+
+static const struct cs48l32_pin_chip cs48l32_pin_chip = {
+	.n_pins = CS48L32_NUM_GPIOS,
+	.pin_groups = cs48l32_pin_groups,
+	.n_pin_groups = ARRAY_SIZE(cs48l32_pin_groups),
+};
+
+static unsigned int cs48l32_pin_make_drv_str(struct cs48l32_pin_private *priv,
+					     unsigned int milliamps)
+{
+	switch (milliamps) {
+	case 4:
+		return 0;
+	case 8:
+		return 1 << CS48L32_GP_DRV_STR_SHIFT;
+	default:
+		break;
+	}
+
+	dev_warn(priv->dev, "%u mA is not a valid drive strength\n", milliamps);
+
+	return 0;
+}
+
+static unsigned int cs48l32_pin_unmake_drv_str(struct cs48l32_pin_private *priv,
+					       unsigned int regval)
+{
+	regval = (regval & CS48L32_GP_DRV_STR_MASK) >> CS48L32_GP_DRV_STR_SHIFT;
+
+	switch (regval) {
+	case 0:
+		return 4;
+	case 1:
+		return 8;
+	default:
+		break;
+	}
+
+	return 0;
+}
+
+static int cs48l32_get_groups_count(struct pinctrl_dev *pctldev)
+{
+	struct cs48l32_pin_private *priv = pinctrl_dev_get_drvdata(pctldev);
+
+	/* Number of alt function groups plus number of single-pin groups */
+	return priv->chip->n_pin_groups + priv->chip->n_pins;
+}
+
+static const char *cs48l32_get_group_name(struct pinctrl_dev *pctldev,
+					  unsigned int selector)
+{
+	struct cs48l32_pin_private *priv = pinctrl_dev_get_drvdata(pctldev);
+
+	if (selector < priv->chip->n_pin_groups)
+		return priv->chip->pin_groups[selector].name;
+
+	selector -= priv->chip->n_pin_groups;
+
+	return cs48l32_pin_single_group_names[selector];
+}
+
+static int cs48l32_get_group_pins(struct pinctrl_dev *pctldev,
+				  unsigned int selector,
+				  const unsigned int **pins,
+				  unsigned int *num_pins)
+{
+	struct cs48l32_pin_private *priv = pinctrl_dev_get_drvdata(pctldev);
+
+	if (selector < priv->chip->n_pin_groups) {
+		*pins = priv->chip->pin_groups[selector].pins;
+		*num_pins = priv->chip->pin_groups[selector].n_pins;
+	} else {
+		/* return the dummy group for a single pin */
+		selector -= priv->chip->n_pin_groups;
+		*pins = &cs48l32_pin_single_group_pins[selector];
+		*num_pins = 1;
+	}
+
+	return 0;
+}
+
+static void cs48l32_pin_dbg_show_fn(struct cs48l32_pin_private *priv,
+				    struct seq_file *s,
+				    unsigned int pin, unsigned int fn)
+{
+	const struct cs48l32_pin_chip *chip = priv->chip;
+	int i, g_pin;
+
+	if (fn != 0) {
+		for (i = 0; i < ARRAY_SIZE(cs48l32_mux_funcs); ++i) {
+			if (cs48l32_mux_funcs[i].func == fn) {
+				seq_printf(s, " FN=%s", cs48l32_mux_funcs[i].name);
+				return;
+			}
+		}
+		return;	/* ignore unknown function values */
+	}
+
+	/* alt function */
+	for (i = 0; i < chip->n_pin_groups; ++i) {
+		for (g_pin = 0; g_pin < chip->pin_groups[i].n_pins; ++g_pin) {
+			if (chip->pin_groups[i].pins[g_pin] == pin) {
+				seq_printf(s, " FN=%s", chip->pin_groups[i].name);
+				return;
+			}
+		}
+	}
+}
+
+static void cs48l32_pin_dbg_show(struct pinctrl_dev *pctldev,
+				 struct seq_file *s, unsigned int pin)
+{
+	struct cs48l32_pin_private *priv = pinctrl_dev_get_drvdata(pctldev);
+	unsigned int reg = CS48L32_GPIO1_CTRL1 + (4 * pin);
+	unsigned int conf, fn;
+	int ret;
+
+	ret = regmap_read(priv->mfd->regmap, reg, &conf);
+	if (ret)
+		return;
+
+	seq_printf(s, "%08x", conf);
+
+	fn = (conf & CS48L32_GP_FN_MASK) >> CS48L32_GP_FN_SHIFT;
+	cs48l32_pin_dbg_show_fn(priv, s, pin, fn);
+
+	/* State of direction bit is only relevant if function==1 */
+	if (fn == 1) {
+		if (conf & CS48L32_GP_DIR_MASK)
+			seq_puts(s, " IN");
+		else
+			seq_puts(s, " OUT");
+	}
+
+	if (conf & CS48L32_GP_PU_MASK)
+		seq_puts(s, " PU");
+
+	if (conf & CS48L32_GP_PD_MASK)
+		seq_puts(s, " PD");
+
+	if (conf & CS48L32_GP_DB_MASK)
+		seq_puts(s, " DB");
+
+	if (conf & CS48L32_GP_OP_CFG_MASK)
+		seq_puts(s, " OD");
+	else
+		seq_puts(s, " CMOS");
+
+	seq_printf(s, " DRV=%umA", cs48l32_pin_unmake_drv_str(priv, conf));
+}
+
+
+static const struct pinctrl_ops cs48l32_pin_group_ops = {
+	.get_groups_count = &cs48l32_get_groups_count,
+	.get_group_name = &cs48l32_get_group_name,
+	.get_group_pins = &cs48l32_get_group_pins,
+	.pin_dbg_show = &cs48l32_pin_dbg_show,
+	.dt_node_to_map = &pinconf_generic_dt_node_to_map_all,
+	.dt_free_map = &pinctrl_utils_free_map,
+};
+
+static int cs48l32_mux_get_funcs_count(struct pinctrl_dev *pctldev)
+{
+	return ARRAY_SIZE(cs48l32_mux_funcs);
+}
+
+static const char *cs48l32_mux_get_func_name(struct pinctrl_dev *pctldev,
+					     unsigned int selector)
+{
+	return cs48l32_mux_funcs[selector].name;
+}
+
+static int cs48l32_mux_get_groups(struct pinctrl_dev *pctldev,
+				  unsigned int selector,
+				  const char * const **groups,
+				  unsigned int * const num_groups)
+{
+	struct cs48l32_pin_private *priv = pinctrl_dev_get_drvdata(pctldev);
+
+	*groups = cs48l32_mux_funcs[selector].group_names;
+
+	if (cs48l32_mux_funcs[selector].func == 0) {
+		/* alt func always maps to a single group */
+		*num_groups = 1;
+	} else {
+		/* other funcs map to all available gpio pins */
+		*num_groups = priv->chip->n_pins;
+	}
+
+	return 0;
+}
+
+static int cs48l32_mux_set_mux(struct pinctrl_dev *pctldev, unsigned int selector,
+			       unsigned int group)
+{
+	struct cs48l32_pin_private *priv = pinctrl_dev_get_drvdata(pctldev);
+	struct cs48l32_mfd *mfd = priv->mfd;
+	const struct cs48l32_pin_groups *pin_group = priv->chip->pin_groups;
+	unsigned int n_chip_groups = priv->chip->n_pin_groups;
+	const char *func_name = cs48l32_mux_funcs[selector].name;
+	unsigned int reg;
+	int i, ret;
+
+	dev_dbg(priv->dev, "%s selecting %u (%s) for group %u (%s)\n",
+		__func__, selector, func_name, group,
+		cs48l32_get_group_name(pctldev, group));
+
+	if (cs48l32_mux_funcs[selector].func == 0) {
+		/* alt func pin assignments are codec-specific */
+		for (i = 0; i < n_chip_groups; ++i) {
+			if (strcmp(func_name, pin_group->name) == 0)
+				break;
+
+			++pin_group;
+		}
+
+		if (i == n_chip_groups)
+			return -EINVAL;
+
+		for (i = 0; i < pin_group->n_pins; ++i) {
+			reg = CS48L32_GPIO1_CTRL1 + (4 * pin_group->pins[i]);
+
+			dev_dbg(priv->dev, "%s setting 0x%x func bits to 0\n", __func__, reg);
+
+			ret = regmap_update_bits(mfd->regmap, reg, CS48L32_GP_FN_MASK, 0);
+			if (ret)
+				break;
+
+		}
+	} else {
+		/*
+		 * for other funcs the group will be the gpio number and will
+		 * be offset by the number of chip-specific functions at the
+		 * start of the group list
+		 */
+		group -= n_chip_groups;
+		reg = CS48L32_GPIO1_CTRL1 + (4 * group);
+
+		dev_dbg(priv->dev, "%s setting 0x%x func bits to 0x%x\n",
+			__func__, reg, cs48l32_mux_funcs[selector].func);
+
+		ret = regmap_update_bits(mfd->regmap,
+					 reg,
+					 CS48L32_GP_FN_MASK,
+					 cs48l32_mux_funcs[selector].func);
+	}
+
+	if (ret)
+		dev_err(priv->dev, "Failed to write to 0x%x (%d)\n", reg, ret);
+
+	return ret;
+}
+
+static int cs48l32_gpio_set_direction(struct pinctrl_dev *pctldev,
+				      struct pinctrl_gpio_range *range,
+				      unsigned int pin,
+				      bool input)
+{
+	struct cs48l32_pin_private *priv = pinctrl_dev_get_drvdata(pctldev);
+	struct cs48l32_mfd *mfd = priv->mfd;
+	unsigned int reg = CS48L32_GPIO1_CTRL1 + (4 * pin);
+	unsigned int val;
+	int ret;
+
+	if (input)
+		val = CS48L32_GP_DIR_MASK;
+	else
+		val = 0;
+
+	ret = regmap_update_bits(mfd->regmap, reg, CS48L32_GP_DIR_MASK, val);
+	if (ret)
+		dev_err(priv->dev, "Failed to write to 0x%x (%d)\n", reg, ret);
+
+	return ret;
+}
+
+static int cs48l32_gpio_request_enable(struct pinctrl_dev *pctldev,
+				       struct pinctrl_gpio_range *range,
+				       unsigned int pin)
+{
+	struct cs48l32_pin_private *priv = pinctrl_dev_get_drvdata(pctldev);
+	struct cs48l32_mfd *mfd = priv->mfd;
+	unsigned int reg = CS48L32_GPIO1_CTRL1 + (4 * pin);
+	int ret;
+
+	/* put the pin into GPIO mode */
+	ret = regmap_update_bits(mfd->regmap, reg, CS48L32_GP_FN_MASK, 1);
+	if (ret)
+		dev_err(priv->dev, "Failed to write to 0x%x (%d)\n", reg, ret);
+
+	return ret;
+}
+
+static void cs48l32_gpio_disable_free(struct pinctrl_dev *pctldev,
+				      struct pinctrl_gpio_range *range,
+				      unsigned int pin)
+{
+	struct cs48l32_pin_private *priv = pinctrl_dev_get_drvdata(pctldev);
+	struct cs48l32_mfd *mfd = priv->mfd;
+	unsigned int reg = CS48L32_GPIO1_CTRL1 + (4 * pin);
+	int ret;
+
+	/* disable GPIO by setting to GPIO IN */
+	cs48l32_gpio_set_direction(pctldev, range, pin, true);
+
+	ret = regmap_update_bits(mfd->regmap, reg, CS48L32_GP_FN_MASK, 1);
+	if (ret)
+		dev_err(priv->dev, "Failed to write to 0x%x (%d)\n", reg, ret);
+}
+static const struct pinmux_ops cs48l32_pin_mux_ops = {
+	.get_functions_count = &cs48l32_mux_get_funcs_count,
+	.get_function_name = &cs48l32_mux_get_func_name,
+	.get_function_groups = &cs48l32_mux_get_groups,
+	.set_mux = &cs48l32_mux_set_mux,
+	.gpio_request_enable = &cs48l32_gpio_request_enable,
+	.gpio_disable_free = &cs48l32_gpio_disable_free,
+	.gpio_set_direction = &cs48l32_gpio_set_direction,
+	.strict = true, /* GPIO and other functions are exclusive */
+};
+
+static int cs48l32_pin_conf_get(struct pinctrl_dev *pctldev, unsigned int pin,
+				unsigned long *config)
+{
+	struct cs48l32_pin_private *priv = pinctrl_dev_get_drvdata(pctldev);
+	unsigned int param = pinconf_to_config_param(*config);
+	unsigned int result = 0;
+	unsigned int conf;
+	int ret;
+
+	ret = regmap_read(priv->mfd->regmap, CS48L32_GPIO1_CTRL1 + (4 * pin), &conf);
+	if (ret) {
+		dev_err(priv->dev, "Failed to read GP%d conf (%d)\n", pin + 1, ret);
+		return ret;
+	}
+
+	switch (param) {
+	case PIN_CONFIG_BIAS_BUS_HOLD:
+		conf &= CS48L32_GP_PU_MASK | CS48L32_GP_PD_MASK;
+		if (conf == (CS48L32_GP_PU_MASK | CS48L32_GP_PD_MASK))
+			result = 1;
+		break;
+	case PIN_CONFIG_BIAS_DISABLE:
+		conf &= CS48L32_GP_PU_MASK | CS48L32_GP_PD_MASK;
+		if (!conf)
+			result = 1;
+		break;
+	case PIN_CONFIG_BIAS_PULL_DOWN:
+		conf &= CS48L32_GP_PU_MASK | CS48L32_GP_PD_MASK;
+		if (conf == CS48L32_GP_PD_MASK)
+			result = 1;
+		break;
+	case PIN_CONFIG_BIAS_PULL_UP:
+		conf &= CS48L32_GP_PU_MASK | CS48L32_GP_PD_MASK;
+		if (conf == CS48L32_GP_PU_MASK)
+			result = 1;
+		break;
+	case PIN_CONFIG_DRIVE_OPEN_DRAIN:
+		if (conf & CS48L32_GP_OP_CFG_MASK)
+			result = 1;
+		break;
+	case PIN_CONFIG_DRIVE_PUSH_PULL:
+		if (!(conf & CS48L32_GP_OP_CFG_MASK))
+			result = 1;
+		break;
+	case PIN_CONFIG_DRIVE_STRENGTH:
+		result = cs48l32_pin_unmake_drv_str(priv, conf);
+		break;
+	case PIN_CONFIG_INPUT_DEBOUNCE:
+		dev_dbg(priv->dev, "Input debounce time not supported.");
+		break;
+	case PIN_CONFIG_INPUT_ENABLE:
+		if (conf & CS48L32_GP_DIR_MASK)
+			result = 1;
+		break;
+	case PIN_CONFIG_OUTPUT:
+		if ((conf & CS48L32_GP_DIR_MASK) && (conf & CS48L32_GP_LVL_MASK))
+			result = 1;
+		break;
+	default:
+		return -EINVAL;
+	}
+
+	*config = pinconf_to_config_packed(param, result);
+
+	return 0;
+}
+
+static int cs48l32_pin_conf_set(struct pinctrl_dev *pctldev, unsigned int pin,
+				unsigned long *configs, unsigned int num_configs)
+{
+	struct cs48l32_pin_private *priv = pinctrl_dev_get_drvdata(pctldev);
+	unsigned int conf = 0;
+	unsigned int mask = 0;
+	unsigned int reg = CS48L32_GPIO1_CTRL1 + (4 * pin);
+	unsigned int val;
+	int ret;
+
+	while (num_configs) {
+		dev_dbg(priv->dev, "%s config 0x%lx\n", __func__, *configs);
+
+		switch (pinconf_to_config_param(*configs)) {
+		case PIN_CONFIG_BIAS_BUS_HOLD:
+			mask |= CS48L32_GP_PU_MASK | CS48L32_GP_PD_MASK;
+			conf |= CS48L32_GP_PU_MASK | CS48L32_GP_PD_MASK;
+			break;
+		case PIN_CONFIG_BIAS_DISABLE:
+			mask |= CS48L32_GP_PU_MASK | CS48L32_GP_PD_MASK;
+			conf &= ~(CS48L32_GP_PU_MASK | CS48L32_GP_PD_MASK);
+			break;
+		case PIN_CONFIG_BIAS_PULL_DOWN:
+			mask |= CS48L32_GP_PU_MASK | CS48L32_GP_PD_MASK;
+			conf |= CS48L32_GP_PD_MASK;
+			conf &= ~CS48L32_GP_PU_MASK;
+			break;
+		case PIN_CONFIG_BIAS_PULL_UP:
+			mask |= CS48L32_GP_PU_MASK | CS48L32_GP_PD_MASK;
+			conf |= CS48L32_GP_PU_MASK;
+			conf &= ~CS48L32_GP_PD_MASK;
+			break;
+		case PIN_CONFIG_DRIVE_OPEN_DRAIN:
+			mask |= CS48L32_GP_OP_CFG_MASK;
+			conf |= CS48L32_GP_OP_CFG_MASK;
+			break;
+		case PIN_CONFIG_DRIVE_PUSH_PULL:
+			mask |= CS48L32_GP_OP_CFG_MASK;
+			conf &= ~CS48L32_GP_OP_CFG_MASK;
+			break;
+		case PIN_CONFIG_DRIVE_STRENGTH:
+			val = pinconf_to_config_argument(*configs);
+			mask |= CS48L32_GP_DRV_STR_MASK;
+			conf &= ~CS48L32_GP_DRV_STR_MASK;
+			conf |= cs48l32_pin_make_drv_str(priv, val);
+			break;
+		case PIN_CONFIG_INPUT_DEBOUNCE:
+			dev_dbg(priv->dev, "Input debounce time not supported.");
+			break;
+		case PIN_CONFIG_INPUT_ENABLE:
+			val = pinconf_to_config_argument(*configs);
+			mask |= CS48L32_GP_DIR_MASK;
+			if (val)
+				conf |= CS48L32_GP_DIR_MASK;
+			else
+				conf &= ~CS48L32_GP_DIR_MASK;
+			break;
+		case PIN_CONFIG_OUTPUT:
+			val = pinconf_to_config_argument(*configs);
+			mask |= CS48L32_GP_LVL_MASK;
+			if (val)
+				conf |= CS48L32_GP_LVL_MASK;
+			else
+				conf &= ~CS48L32_GP_LVL_MASK;
+
+			mask |= CS48L32_GP_DIR_MASK;
+			conf &= ~CS48L32_GP_DIR_MASK;
+			break;
+		default:
+			return -EINVAL;
+		}
+
+		++configs;
+		--num_configs;
+	}
+
+	dev_dbg(priv->dev, "%s gpio%d 0x%x:0x%x\n", __func__, pin + 1, reg, conf);
+
+	ret = regmap_update_bits(priv->mfd->regmap, reg, mask, conf);
+	if (ret)
+		dev_err(priv->dev, "Failed to write GPIO%d conf (%d) reg 0x%x\n",
+			pin + 1, ret, reg);
+
+	return ret;
+}
+
+static int cs48l32_pin_conf_group_set(struct pinctrl_dev *pctldev,
+				      unsigned int selector,
+				      unsigned long *configs,
+				      unsigned int num_configs)
+{
+	struct cs48l32_pin_private *priv = pinctrl_dev_get_drvdata(pctldev);
+	const struct cs48l32_pin_groups *pin_group;
+	unsigned int n_groups = priv->chip->n_pin_groups;
+	int i, ret;
+
+	dev_dbg(priv->dev, "%s setting group %s\n", __func__,
+		cs48l32_get_group_name(pctldev, selector));
+
+	if (selector >= n_groups) {
+		/* group is a single pin, convert to pin number and set */
+		return cs48l32_pin_conf_set(pctldev,
+					  selector - n_groups,
+					  configs,
+					  num_configs);
+	} else {
+		pin_group = &priv->chip->pin_groups[selector];
+
+		for (i = 0; i < pin_group->n_pins; ++i) {
+			ret = cs48l32_pin_conf_set(pctldev,
+						 pin_group->pins[i],
+						 configs,
+						 num_configs);
+			if (ret)
+				return ret;
+		}
+	}
+
+	return 0;
+}
+
+static const struct pinconf_ops cs48l32_pin_conf_ops = {
+	.is_generic = true,
+	.pin_config_get = &cs48l32_pin_conf_get,
+	.pin_config_set = &cs48l32_pin_conf_set,
+	.pin_config_group_set = &cs48l32_pin_conf_group_set,
+
+};
+
+static struct pinctrl_desc cs48l32_pin_desc = {
+	.name = "cs48l32-pinctrl",
+	.pins = cs48l32_pins,
+	.pctlops = &cs48l32_pin_group_ops,
+	.pmxops = &cs48l32_pin_mux_ops,
+	.confops = &cs48l32_pin_conf_ops,
+	.owner = THIS_MODULE,
+};
+
+static int cs48l32_pin_probe(struct platform_device *pdev)
+{
+	struct cs48l32_mfd *mfd = dev_get_drvdata(pdev->dev.parent);
+	struct cs48l32_pin_private *priv;
+	int ret;
+
+	BUILD_BUG_ON(ARRAY_SIZE(cs48l32_pin_single_group_names) !=
+		     ARRAY_SIZE(cs48l32_pin_single_group_pins));
+
+	priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);
+	if (!priv)
+		return -ENOMEM;
+
+	priv->dev = &pdev->dev;
+	priv->mfd = mfd;
+	/* Composite MFD device so shares the parent OF node. */
+	pdev->dev.of_node = mfd->dev->of_node;
+
+	priv->chip = &cs48l32_pin_chip;
+	cs48l32_pin_desc.npins = priv->chip->n_pins;
+
+	ret = devm_pinctrl_register_and_init(&pdev->dev, &cs48l32_pin_desc, priv, &priv->pctl);
+	if (ret)
+		return dev_err_probe(priv->dev, ret, "Failed pinctrl register\n");
+
+	ret = pinctrl_enable(priv->pctl);
+	if (ret)
+		return dev_err_probe(priv->dev, ret, "Failed to enable pinctrl\n");
+
+	platform_set_drvdata(pdev, priv);
+
+	dev_dbg(priv->dev, "pinctrl registered\n");
+
+	return 0;
+}
+
+static struct platform_driver cs48l32_pin_driver = {
+	.probe = &cs48l32_pin_probe,
+	.driver = {
+		.name = "cs48l32-pinctrl",
+	},
+};
+
+module_platform_driver(cs48l32_pin_driver);
+
+MODULE_DESCRIPTION("CS48L32 pinctrl driver");
+MODULE_AUTHOR("Richard Fitzgerald <rf@opensource.cirrus.com>");
+MODULE_AUTHOR("Piotr Stankiewicz <piotrs@opensource.cirrus.com>");
+MODULE_LICENSE("GPL");
diff --git a/drivers/pinctrl/cirrus/pinctrl-cs48l32.h b/drivers/pinctrl/cirrus/pinctrl-cs48l32.h
new file mode 100644
index 000000000000..2193c7558dd3
--- /dev/null
+++ b/drivers/pinctrl/cirrus/pinctrl-cs48l32.h
@@ -0,0 +1,62 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Pinctrl for Cirrus Logic CS48L32
+ *
+ * Copyright (C) 2020, 2022 Cirrus Logic, Inc. and
+ *               Cirrus Logic International Semiconductor Ltd.
+ */
+
+#ifndef PINCTRL_CS48L32_H
+#define PINCTRL_CS48L32_H
+
+#include <linux/device.h>
+#include <linux/mfd/cs48l32/core.h>
+
+struct pinctrl_dev;
+
+#define CS48L32_GP_DIR_MASK		0x80000000
+#define CS48L32_GP_DIR_SHIFT			31
+#define CS48L32_GP_PU_MASK		0x40000000
+#define CS48L32_GP_PU_SHIFT			30
+#define CS48L32_GP_PD_MASK		0x20000000
+#define CS48L32_GP_PD_SHIFT			29
+#define CS48L32_GP_DRV_STR_MASK		0x03000000
+#define CS48L32_GP_DRV_STR_SHIFT		24
+#define CS48L32_GP_DBTIME_MASK		0x000f0000
+#define CS48L32_GP_DBTIME_SHIFT			16
+#define CS48L32_GP_LVL_MASK		0x00008000
+#define CS48L32_GP_LVL_SHIFT			15
+#define CS48L32_GP_OP_CFG_MASK		0x00004000
+#define CS48L32_GP_OP_CFG_SHIFT			14
+#define CS48L32_GP_DB_MASK		0x00002000
+#define CS48L32_GP_DB_SHIFT			13
+#define CS48L32_GP_POL_MASK		0x00001000
+#define CS48L32_GP_POL_SHIFT			12
+#define CS48L32_GP_FN_MASK		0x000007ff
+#define CS48L32_GP_FN_SHIFT			 0
+
+#define CS48L32_NUM_GPIOS			16
+
+struct cs48l32_pin_groups {
+	const char *name;
+	const unsigned int *pins;
+	unsigned int n_pins;
+};
+
+struct cs48l32_pin_chip {
+	unsigned int n_pins;
+
+	const struct cs48l32_pin_groups *pin_groups;
+	unsigned int n_pin_groups;
+};
+
+struct cs48l32_pin_private {
+	struct cs48l32_mfd *mfd;
+
+	const struct cs48l32_pin_chip *chip; /* chip-specific groups */
+
+	struct device *dev;
+	struct pinctrl_dev *pctl;
+};
+
+#endif
