[PATCH v8] extcon: add support for Samsung S2M series PMIC extcon devices

From: Kaustabh Chakraborty

Date: Mon Jun 15 2026 - 14:25:09 EST


Add a driver for MUIC devices found in certain Samsung S2M series PMICs
These are USB port accessory detectors. These devices report multiple
cable states depending on the ID-GND resistance measured by an internal
ADC.

The driver includes initial support for the S2MU005 PMIC extcon.

Signed-off-by: Kaustabh Chakraborty <kauschluss@xxxxxxxxxxx>
---
S2MU005 is an MFD chip manufactured by Samsung Electronics. This is
found in various devices manufactured by Samsung and others, including
all Exynos 7870 devices. It is known to have the following features:

1. Two LED channels with adjustable brightness for use as a torch, or a
flash strobe.
2. An RGB LED with 8-bit channels. Usually programmed as a notification
indicator.
3. An MUIC, which works with USB micro-B (and USB-C?). For the micro-B
variant though, it measures the ID-GND resistance using an internal
ADC.
4. A charger device, which reports if charger is online, voltage,
resistance, etc.

This patch series implements a lot of these features. Naturally, this
series touches upon a lot of subsystems. The 'parent' is the MFD driver,
so the subsystems have some form of dependency to the MFD driver, so
they are not separable.

Here are the subsystems corresponding to the patch numbers:
dt-bindings - 01, 02, 03
mfd - 03, 04, 05
led - 01, 06, 07, 08
extcon - 02, 09
power - 10
---
Changes in v8:
- Drop applied patches [v7 {1-8,10}/10]
- Add mutex lock for IRQ handlers (Sashiko AI)
- Add error checking for memory alloc (Sashiko AI)
- Link to v7: https://patch.msgid.link/20260516-s2mu005-pmic-v7-0-73f9702fb461@xxxxxxxxxxx

Changes in v7:
- Add missing tags collected from v5
- Add trailing `#` in s2mu005 mfd dt-schema (Sashiko AI)
- Squash [v6 04/11] with [v6 06/11] to prevent bisect regression (Sashiko AI)
- Remove ack_base from regmap IRQ as not needed by hardware (Sashiko AI)
- Update commit message of [v6 04/11] (Sashiko AI)
- Fix reference to leds-trigger-pattern in [v6 09/11] commit (Sashiko AI)
- Remove Kconfig `select REGMAP_IRQ` from [v6 07/11], [v6 08/11] (Sashiko AI)
- Implement lock for s2m_fled_flash_{brightness,timeout}_set() (Sashiko AI)
- Remove superfluous lock from s2m_fled_flash_external_strobe_set() (Sashiko AI)
- Remove incorrect (void *) cast to s2m_fled_v4l2_flash_release() (Sashiko AI)
- Change regmap_{s/update_bits/write} for slope setting in
s2mu005_rgb_apply_params() (Sashiko AI)
- Allow for extrapolation in s2m_rgb_lut_get_closest_duration() (Sashiko AI)
- Explicitly initialize ramp_{up,dn}_en in s2m_rgb_pattern_set() (Sashiko AI)
- Use duplicated s2mu005_rgb_subled_info for allowing multi-driver (Sashiko AI)
- Change s/CDP/DCP in s2mu005_muic_attach() (Sashiko AI)
- Remove EXTCON_USB state from USB DCP (Sashiko AI)
- Move muic_attach call to irq init, preventing undefined extcon (Sashiko AI)
- Properly propagate errors in platform_get_irq_byname_optional() in extcon-s2m
(Sashiko AI)
- Use duplicated s2mu005_muic_irq_data for allowing multi-driver (Sashiko AI)
- Fix if-else-if chain in s2mu005_chgr_get_usb_type() (Sashiko AI)
- Consider errors in extcon_get_state() call (Sashiko AI)
- Handle NULL possibility on extcon child node in charger (Sashiko AI)
- Link to v6: https://patch.msgid.link/20260515-s2mu005-pmic-v6-0-1979106992d4@xxxxxxxxxxx

Changes in v6:
- Fix build, UAF, and functional errors with
CONFIG_V4L2_FLASH_LED_CLASS=m (Lee Jones)
- Remove (ret < 0) wherever redundant (Lee Jones)
- Remove extra conditionals for supporting multiple variants (Lee Jones)
- Fix OOB condition in initailizing flash LED channels (Lee Jones)
- Declare i inside for, like: for (int i = 0; ...) (Lee Jones)
- Rewrite and simplify closest timing function for clarity (Lee Jones)
- Link to v5: https://lore.kernel.org/r/20260424-s2mu005-pmic-v5-0-fcbc9da5a004@xxxxxxxxxxx

Changes in v5:
- Drop port property from charger dt binding (Krzysztof Kozlowski)
- Create separate dt binding for S2MU005 MFD (Krzysztof Kozlowski)
- Move RGB LED and charger schema to parent schema (Rob Herring)
- Fix error of using invalid revision mask
- Link to v4: https://lore.kernel.org/r/20260414-s2mu005-pmic-v4-0-7fe7480577e6@xxxxxxxxxxx

Changes in v4:
- Use OF graph to connect charger with MUIC in device tree
- Move DMA coherent mask to all MFD PMICs (André Draszik)
- Modify pointer names for flash/RGB drivers (Lee Jones)
- Use 100-char line wrap for flash/RGB drivers (Lee Jones)
- Revamp LED device initialization in flash driver (Lee Jones)
- Add proper USB 2.0 support in charger driver (Łukasz Lebiedziński)
- Link to v3: https://lore.kernel.org/r/20260225-s2mu005-pmic-v3-0-b4afee947603@xxxxxxxxxxx

Changes in v3:
- Remove "extcon" text from dt-bindings documentation (Rob Herring)
- Add connector for MUIC node
- Fix dt binding errors reported by robh's bot
- Fix kernel test robot const errors
- Remove FIELD_PREP() values in register header file (André Draszik)
- Add max_register, volatile_reg, cache_type (André Draszik)
- Redo [v2 07/12] to NOT store the PMIC revision (André Draszik)
- Add a commit to fix DMA coherent mask in I2C PMICs
- Implement various flow changes in flash LED driver (André Draszik)
- Use device_for_each_child_node_scoped() (André Draszik)
- Fix CFI panic in devm_add_action_or_reset()
- Link to v2: https://lore.kernel.org/r/20260126-s2mu005-pmic-v2-0-78f1a75f547a@xxxxxxxxxxx

Changes in v2:
- Drop [v1 06/13], instead use regmap_irq_chip::get_irq_regs()
- Remove references to driver in devicetree commits (Conor Dooley)
- Propagate errors of sec_pmic_store_rev() (André Draszik)
- Fix documentation language errors (Randy Dunlap)
- Link to v1: https://lore.kernel.org/r/20251114-s2mu005-pmic-v1-0-9e3184d3a0c9@xxxxxxxxxxx

To: MyungJoo Ham <myungjoo.ham@xxxxxxxxxxx>
To: Chanwoo Choi <cw00.choi@xxxxxxxxxxx>
Cc: linux-kernel@xxxxxxxxxxxxxxx
---
drivers/extcon/Kconfig | 9 ++
drivers/extcon/Makefile | 1 +
drivers/extcon/extcon-s2m.c | 384 ++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 394 insertions(+)

diff --git a/drivers/extcon/Kconfig b/drivers/extcon/Kconfig
index 68d9df7d2dae0..b052da947fc92 100644
--- a/drivers/extcon/Kconfig
+++ b/drivers/extcon/Kconfig
@@ -183,6 +183,15 @@ config EXTCON_RT8973A
and switch that is optimized to protect low voltage system
from abnormal high input voltage (up to 28V).

+config EXTCON_S2M
+ tristate "Samsung S2M series PMIC EXTCON support"
+ depends on MFD_SEC_CORE
+ help
+ This option enables support for MUIC devices found in certain
+ Samsung S2M series PMICs, such as the S2MU005. These devices
+ have internal ADCs measuring the ID-GND resistance, thereby
+ can be used as a USB port accessory detector.
+
config EXTCON_SM5502
tristate "Silicon Mitus SM5502/SM5504/SM5703 EXTCON support"
depends on I2C
diff --git a/drivers/extcon/Makefile b/drivers/extcon/Makefile
index 6482f2bfd6611..e3939786f3474 100644
--- a/drivers/extcon/Makefile
+++ b/drivers/extcon/Makefile
@@ -23,6 +23,7 @@ obj-$(CONFIG_EXTCON_PALMAS) += extcon-palmas.o
obj-$(CONFIG_EXTCON_PTN5150) += extcon-ptn5150.o
obj-$(CONFIG_EXTCON_QCOM_SPMI_MISC) += extcon-qcom-spmi-misc.o
obj-$(CONFIG_EXTCON_RT8973A) += extcon-rt8973a.o
+obj-$(CONFIG_EXTCON_S2M) += extcon-s2m.o
obj-$(CONFIG_EXTCON_SM5502) += extcon-sm5502.o
obj-$(CONFIG_EXTCON_USB_GPIO) += extcon-usb-gpio.o
obj-$(CONFIG_EXTCON_USBC_CROS_EC) += extcon-usbc-cros-ec.o
diff --git a/drivers/extcon/extcon-s2m.c b/drivers/extcon/extcon-s2m.c
new file mode 100644
index 0000000000000..20192e7bc4dd7
--- /dev/null
+++ b/drivers/extcon/extcon-s2m.c
@@ -0,0 +1,384 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Extcon Driver for Samsung S2M series PMICs.
+ *
+ * Copyright (c) 2015 Samsung Electronics Co., Ltd
+ * Copyright (C) 2026 Kaustabh Chakraborty <kauschluss@xxxxxxxxxxx>
+ */
+
+#include <linux/delay.h>
+#include <linux/extcon-provider.h>
+#include <linux/interrupt.h>
+#include <linux/mfd/samsung/core.h>
+#include <linux/mfd/samsung/s2mu005.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/of.h>
+#include <linux/platform_device.h>
+#include <linux/regmap.h>
+
+struct s2m_muic {
+ struct device *dev;
+ struct regmap *regmap;
+ struct extcon_dev *extcon;
+ struct mutex lock;
+ struct s2m_muic_irq_data *irq_data;
+ const unsigned int *extcon_cable;
+ bool attached;
+};
+
+struct s2m_muic_irq_data {
+ const char *name;
+ int (*const handler)(struct s2m_muic *);
+ bool call_on_probe;
+ int irq;
+};
+
+static int s2mu005_muic_detach(struct s2m_muic *priv)
+{
+ int ret;
+ int i;
+
+ mutex_lock(&priv->lock);
+
+ ret = regmap_set_bits(priv->regmap, S2MU005_REG_MUIC_CTRL1,
+ S2MU005_MUIC_MAN_SW);
+ if (ret) {
+ dev_err(priv->dev, "failed to disable manual switching\n");
+ goto err;
+ }
+
+ ret = regmap_set_bits(priv->regmap, S2MU005_REG_MUIC_CTRL3,
+ S2MU005_MUIC_ONESHOT_ADC);
+ if (ret) {
+ dev_err(priv->dev, "failed to enable ADC oneshot mode\n");
+ goto err;
+ }
+
+ ret = regmap_clear_bits(priv->regmap, S2MU005_REG_MUIC_SWCTRL, ~0);
+ if (ret) {
+ dev_err(priv->dev, "failed to clear switch control register\n");
+ goto err;
+ }
+
+ /* Find all set states and clear them */
+ for (i = 0; priv->extcon_cable[i]; i++) {
+ unsigned int state = priv->extcon_cable[i];
+
+ if (extcon_get_state(priv->extcon, state) == true)
+ extcon_set_state_sync(priv->extcon, state, false);
+ }
+
+ priv->attached = false;
+
+err:
+ mutex_unlock(&priv->lock);
+
+ return ret;
+}
+
+static int s2mu005_muic_attach(struct s2m_muic *priv)
+{
+ unsigned int type;
+ int ret;
+
+ mutex_lock(&priv->lock);
+
+ /* If any device is already attached, detach it */
+ if (priv->attached) {
+ s2mu005_muic_detach(priv);
+ msleep(100);
+ }
+
+ ret = regmap_read(priv->regmap, S2MU005_REG_MUIC_DEV1, &type);
+ if (ret) {
+ dev_err(priv->dev, "failed to read DEV1 register\n");
+ goto err;
+ }
+
+ /*
+ * All USB connections which require communication via its D+
+ * and D- wires need it.
+ */
+ if (type & (S2MU005_MUIC_OTG | S2MU005_MUIC_CDP | S2MU005_MUIC_SDP)) {
+ ret = regmap_update_bits(priv->regmap, S2MU005_REG_MUIC_SWCTRL,
+ S2MU005_MUIC_DM_DP,
+ FIELD_PREP(S2MU005_MUIC_DM_DP,
+ S2MU005_MUIC_DM_DP_USB));
+ if (ret) {
+ dev_err(priv->dev, "failed to configure DM/DP pins\n");
+ goto err;
+ }
+ }
+
+ /*
+ * For OTG connections, enable manual switching and ADC oneshot
+ * mode. Since the port will now be supplying power, the
+ * internal ADC (measuring the ID-GND resistance) is made to
+ * poll periodically for any changes, so as to prevent any
+ * damages due to power.
+ */
+ if (type & S2MU005_MUIC_OTG) {
+ ret = regmap_clear_bits(priv->regmap, S2MU005_REG_MUIC_CTRL1,
+ S2MU005_MUIC_MAN_SW);
+ if (ret) {
+ dev_err(priv->dev, "failed to enable manual switching\n");
+ goto err;
+ }
+
+ ret = regmap_clear_bits(priv->regmap, S2MU005_REG_MUIC_CTRL3,
+ S2MU005_MUIC_ONESHOT_ADC);
+ if (ret) {
+ dev_err(priv->dev, "failed to disable ADC oneshot mode\n");
+ goto err;
+ }
+ }
+
+ switch (type) {
+ case S2MU005_MUIC_OTG:
+ dev_dbg(priv->dev, "USB OTG connection detected\n");
+ extcon_set_state_sync(priv->extcon, EXTCON_USB_HOST, true);
+ priv->attached = true;
+ break;
+ case S2MU005_MUIC_CDP:
+ dev_dbg(priv->dev, "USB CDP connection detected\n");
+ extcon_set_state_sync(priv->extcon, EXTCON_USB, true);
+ extcon_set_state_sync(priv->extcon, EXTCON_CHG_USB_CDP, true);
+ priv->attached = true;
+ break;
+ case S2MU005_MUIC_SDP:
+ dev_dbg(priv->dev, "USB SDP connection detected\n");
+ extcon_set_state_sync(priv->extcon, EXTCON_USB, true);
+ extcon_set_state_sync(priv->extcon, EXTCON_CHG_USB_SDP, true);
+ priv->attached = true;
+ break;
+ case S2MU005_MUIC_DCP:
+ dev_dbg(priv->dev, "USB DCP connection detected\n");
+ extcon_set_state_sync(priv->extcon, EXTCON_CHG_USB_DCP, true);
+ priv->attached = true;
+ break;
+ case S2MU005_MUIC_UART:
+ dev_dbg(priv->dev, "UART connection detected\n");
+ extcon_set_state_sync(priv->extcon, EXTCON_JIG, true);
+ priv->attached = true;
+ break;
+ case 0: /* OPEN */
+ break;
+ default:
+ dev_warn(priv->dev,
+ "failed to recognize the device attached, unknown or bad type\n");
+ }
+
+err:
+ mutex_unlock(&priv->lock);
+
+ return ret;
+}
+
+static int s2mu005_muic_init(struct s2m_muic *priv)
+{
+ int ret;
+
+ ret = regmap_update_bits(priv->regmap, S2MU005_REG_MUIC_LDOADC_L,
+ S2MU005_MUIC_VSET,
+ FIELD_PREP(S2MU005_MUIC_VSET,
+ S2MU005_MUIC_VSET_3P0V));
+ if (ret) {
+ dev_err(priv->dev, "failed to set internal ADC voltage regulator\n");
+ return ret;
+ }
+
+ ret = regmap_update_bits(priv->regmap, S2MU005_REG_MUIC_LDOADC_H,
+ S2MU005_MUIC_VSET,
+ FIELD_PREP(S2MU005_MUIC_VSET,
+ S2MU005_MUIC_VSET_3P0V));
+ if (ret) {
+ dev_err(priv->dev, "failed to set internal ADC voltage regulator\n");
+ return ret;
+ }
+
+ ret = regmap_clear_bits(priv->regmap, S2MU005_REG_MUIC_CTRL1,
+ S2MU005_MUIC_IRQ);
+ if (ret) {
+ dev_err(priv->dev, "failed to enable MUIC interrupts\n");
+ return ret;
+ }
+
+ return 0;
+}
+
+static const unsigned int s2mu005_muic_extcon_cable[] = {
+ EXTCON_USB,
+ EXTCON_USB_HOST,
+ EXTCON_CHG_USB_SDP,
+ EXTCON_CHG_USB_DCP,
+ EXTCON_CHG_USB_CDP,
+ EXTCON_JIG,
+ EXTCON_NONE,
+};
+
+static const struct s2m_muic_irq_data s2mu005_muic_irq_data[] = {
+ {
+ .name = "attach",
+ .handler = s2mu005_muic_attach,
+ .call_on_probe = true,
+ }, {
+ .name = "detach",
+ .handler = s2mu005_muic_detach,
+ }, {
+ /* sentinel */
+ }
+};
+
+static irqreturn_t s2m_muic_irq_func(int virq, void *data)
+{
+ struct s2m_muic *priv = data;
+ const struct s2m_muic_irq_data *irq_data = priv->irq_data;
+ int ret;
+ int i;
+
+ for (i = 0; irq_data[i].handler; i++) {
+ if (virq != irq_data[i].irq)
+ continue;
+
+ ret = irq_data[i].handler(priv);
+ if (ret)
+ dev_err(priv->dev, "failed to handle interrupt for %s (%d)\n",
+ irq_data[i].name, ret);
+ break;
+ }
+
+ return IRQ_HANDLED;
+}
+
+static int s2m_muic_probe(struct platform_device *pdev)
+{
+ struct device *dev = &pdev->dev;
+ struct sec_pmic_dev *pmic_drvdata = dev_get_drvdata(dev->parent);
+ struct s2m_muic *priv;
+ int ret;
+ int i;
+
+ priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
+ if (!priv)
+ return -ENOMEM;
+
+ platform_set_drvdata(pdev, priv);
+ priv->dev = dev;
+ priv->regmap = pmic_drvdata->regmap_pmic;
+
+ ret = devm_mutex_init(dev, &priv->lock);
+ if (ret)
+ return dev_err_probe(dev, ret, "failed to initialize mutex lock\n");
+
+ switch (platform_get_device_id(pdev)->driver_data) {
+ case S2MU005:
+ priv->extcon_cable = s2mu005_muic_extcon_cable;
+ priv->irq_data = devm_kmemdup(dev, s2mu005_muic_irq_data,
+ sizeof(s2mu005_muic_irq_data),
+ GFP_KERNEL);
+ if (IS_ERR(priv->irq_data))
+ return -ENOMEM;
+
+ /* Initialize MUIC */
+ ret = s2mu005_muic_init(priv);
+ if (ret)
+ return dev_err_probe(dev, ret, "failed to initialize MUIC\n");
+ break;
+ default:
+ return dev_err_probe(dev, -ENODEV, "device type not supported by driver\n");
+ }
+
+ if (!priv->irq_data)
+ return -ENOMEM;
+
+ priv->extcon = devm_extcon_dev_allocate(dev, priv->extcon_cable);
+ if (IS_ERR(priv->extcon))
+ return dev_err_probe(dev, PTR_ERR(priv->extcon),
+ "failed to allocate memory for extcon\n");
+
+ ret = devm_extcon_dev_register(dev, priv->extcon);
+ if (ret)
+ return dev_err_probe(dev, ret, "failed to register extcon device\n");
+
+ for (i = 0; priv->irq_data[i].handler; i++) {
+ ret = platform_get_irq_byname_optional(pdev, priv->irq_data[i].name);
+ if (ret == -ENXIO)
+ continue;
+ if (ret < 0)
+ return dev_err_probe(dev, ret, "failed to get IRQ %s\n",
+ priv->irq_data[i].name);
+
+ priv->irq_data[i].irq = ret;
+ ret = devm_request_threaded_irq(dev, priv->irq_data[i].irq, NULL,
+ s2m_muic_irq_func, IRQF_ONESHOT,
+ priv->irq_data[i].name, priv);
+ if (ret)
+ return dev_err_probe(dev, ret, "failed to request IRQ\n");
+
+ if (priv->irq_data[i].call_on_probe)
+ priv->irq_data[i].handler(priv);
+ }
+
+ return 0;
+}
+
+static void s2m_muic_remove(struct platform_device *pdev)
+{
+ struct s2m_muic *priv = dev_get_drvdata(&pdev->dev);
+
+ /*
+ * Disabling the MUIC device is important as it disables manual
+ * switching mode, thereby enabling auto switching mode.
+ *
+ * This is to ensure that when the board is powered off, it
+ * goes into LPM charging mode when a USB charger is connected.
+ */
+ switch (platform_get_device_id(pdev)->driver_data) {
+ case S2MU005:
+ s2mu005_muic_detach(priv);
+ break;
+ default:
+ unreachable();
+ }
+}
+
+static const struct platform_device_id s2m_muic_id_table[] = {
+ { "s2mu005-muic", S2MU005 },
+ { /* sentinel */ },
+};
+MODULE_DEVICE_TABLE(platform, s2m_muic_id_table);
+
+/*
+ * Device is instantiated through parent MFD device and device matching
+ * is done through platform_device_id.
+ *
+ * However if device's DT node contains proper clock compatible and
+ * driver is built as a module, then the *module* matching will be done
+ * through DT aliases. This requires of_device_id table. In the same
+ * time this will not change the actual *device* matching so do not add
+ * .of_match_table.
+ */
+static const struct of_device_id s2m_muic_of_match_table[] = {
+ {
+ .compatible = "samsung,s2mu005-muic",
+ .data = (void *)S2MU005,
+ }, {
+ /* sentinel */
+ },
+};
+MODULE_DEVICE_TABLE(of, s2m_muic_of_match_table);
+
+static struct platform_driver s2m_muic_driver = {
+ .driver = {
+ .name = "s2m-muic",
+ },
+ .probe = s2m_muic_probe,
+ .remove = s2m_muic_remove,
+ .id_table = s2m_muic_id_table,
+};
+module_platform_driver(s2m_muic_driver);
+
+MODULE_DESCRIPTION("Extcon Driver For Samsung S2M Series PMICs");
+MODULE_AUTHOR("Kaustabh Chakraborty <kauschluss@xxxxxxxxxxx>");
+MODULE_LICENSE("GPL");

---
base-commit: 8d6dbbbe3ba62de0a63e962ee004afb848c8e3ac
change-id: 20251112-s2mu005-pmic-0c67fa6bac3c

Best regards,
--
Kaustabh Chakraborty <kauschluss@xxxxxxxxxxx>