diff --git a/drivers/power/supply/Kconfig b/drivers/power/supply/Kconfig index c56d7ace7d55..f0ede1cecb6a 100644 --- a/drivers/power/supply/Kconfig +++ b/drivers/power/supply/Kconfig @@ -856,6 +856,16 @@ config CHARGER_RK817 help Say Y to include support for Rockchip RK817 Battery Charger. +config CHARGER_S2M + tristate "Samsung S2M series PMIC battery charger support" + depends on EXTCON_S2M + depends on MFD_SEC_CORE + help + This option enables support for charger devices found in + certain Samsung S2M series PMICs, such as the S2MU005. These + devices provide USB power supply information and also required + for USB OTG role switching. + config CHARGER_SMB347 tristate "Summit Microelectronics SMB3XX Battery Charger" depends on I2C diff --git a/drivers/power/supply/Makefile b/drivers/power/supply/Makefile index b55612d626c8..31fe4a145929 100644 --- a/drivers/power/supply/Makefile +++ b/drivers/power/supply/Makefile @@ -107,6 +107,7 @@ obj-$(CONFIG_CHARGER_BQ25890) += bq25890_charger.o obj-$(CONFIG_CHARGER_BQ25980) += bq25980_charger.o obj-$(CONFIG_CHARGER_BQ256XX) += bq256xx_charger.o obj-$(CONFIG_CHARGER_RK817) += rk817_charger.o +obj-$(CONFIG_CHARGER_S2M) += s2m-charger.o obj-$(CONFIG_CHARGER_SMB347) += smb347-charger.o obj-$(CONFIG_CHARGER_TPS65090) += tps65090-charger.o obj-$(CONFIG_CHARGER_TPS65217) += tps65217_charger.o diff --git a/drivers/power/supply/s2m-charger.c b/drivers/power/supply/s2m-charger.c new file mode 100644 index 000000000000..4d1f2c2c7144 --- /dev/null +++ b/drivers/power/supply/s2m-charger.c @@ -0,0 +1,313 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Battery Charger Driver for Samsung S2M series PMICs. + * + * Copyright (c) 2015 Samsung Electronics Co., Ltd + * Copyright (c) 2026 Kaustabh Chakraborty + * Copyright (c) 2026 Łukasz Lebiedziński + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct s2m_chgr { + struct device *dev; + struct regmap *regmap; + struct power_supply *psy; + struct extcon_dev *extcon; + struct work_struct extcon_work; + struct notifier_block extcon_nb; +}; + +static int s2mu005_chgr_get_online(struct s2m_chgr *priv, int *value) +{ + u32 val; + int ret; + + ret = regmap_read(priv->regmap, S2MU005_REG_CHGR_STATUS0, &val); + if (ret) { + dev_err(priv->dev, "failed to read register (%d)\n", ret); + return ret; + } + + *value = !!(val & S2MU005_CHGR_CHG); + + return 0; +} + +static void s2mu005_chgr_get_usb_type(struct s2m_chgr *priv, int *value) +{ + if (extcon_get_state(priv->extcon, EXTCON_CHG_USB_CDP) > 0) + *value = POWER_SUPPLY_USB_TYPE_CDP; + else if (extcon_get_state(priv->extcon, EXTCON_CHG_USB_SDP) > 0) + *value = POWER_SUPPLY_USB_TYPE_SDP; + else if (extcon_get_state(priv->extcon, EXTCON_CHG_USB_DCP) > 0) + *value = POWER_SUPPLY_USB_TYPE_DCP; + else + *value = POWER_SUPPLY_USB_TYPE_UNKNOWN; +} + +static int s2mu005_chgr_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct s2m_chgr *priv = power_supply_get_drvdata(psy); + int ret; + + switch (psp) { + case POWER_SUPPLY_PROP_ONLINE: + ret = s2mu005_chgr_get_online(priv, &val->intval); + if (ret) + return ret; + break; + case POWER_SUPPLY_PROP_USB_TYPE: + s2mu005_chgr_get_usb_type(priv, &val->intval); + break; + default: + return -EINVAL; + } + + return 0; +} + +static int s2mu005_chgr_mode_set_host(struct s2m_chgr *priv) +{ + int ret; + + /* set mode to OTG */ + ret = regmap_update_bits(priv->regmap, S2MU005_REG_CHGR_CTRL0, + S2MU005_CHGR_OP_MODE, + FIELD_PREP(S2MU005_CHGR_OP_MODE, + S2MU005_CHGR_OP_MODE_OTG)); + if (ret) { + dev_err(priv->dev, "failed to set OTG mode (%d)\n", ret); + return ret; + } + + /* set boost frequency to 2MHz */ + ret = regmap_update_bits(priv->regmap, S2MU005_REG_CHGR_CTRL11, + S2MU005_CHGR_OSC_BOOST, + FIELD_PREP(S2MU005_CHGR_OSC_BOOST, + S2MU005_CHGR_OSC_BOOST_2MHZ)); + if (ret) { + dev_err(priv->dev, "failed to set boost frequency (%d)\n", ret); + return ret; + } + + /* set OTG current limit to 1.5 A */ + ret = regmap_update_bits(priv->regmap, S2MU005_REG_CHGR_CTRL4, + S2MU005_CHGR_OTG_OCP, + FIELD_PREP(S2MU005_CHGR_OTG_OCP, + S2MU005_CHGR_OTG_OCP_1P5A)); + if (ret) { + dev_err(priv->dev, "failed to set OTG current limit (%d)\n", ret); + return ret; + } + + /* VBUS switches are OFF when OTG over-current happens */ + ret = regmap_set_bits(priv->regmap, S2MU005_REG_CHGR_CTRL4, + S2MU005_CHGR_OTG_OCP_OFF); + if (ret) { + dev_err(priv->dev, "failed to set OTG OCP switch (%d)\n", ret); + return ret; + } + + /* set OTG voltage to 5.1 V */ + ret = regmap_update_bits(priv->regmap, S2MU005_REG_CHGR_CTRL5, + S2MU005_CHGR_VMID_BOOST, + FIELD_PREP(S2MU005_CHGR_VMID_BOOST, + S2MU005_CHGR_VMID_BOOST_5P1V)); + if (ret) { + dev_err(priv->dev, "failed to set OTG voltage (%d)\n", ret); + return ret; + } + + /* turn on OTG */ + ret = regmap_update_bits(priv->regmap, S2MU005_REG_CHGR_CTRL15, + S2MU005_CHGR_OTG_EN, + FIELD_PREP(S2MU005_CHGR_OTG_EN, + S2MU005_CHGR_OTG_EN_ON)); + if (ret) { + dev_err(priv->dev, "failed to turn on OTG (%d)\n", ret); + return ret; + } + + return 0; +} + +static int s2mu005_chgr_mode_set_charger(struct s2m_chgr *priv) +{ + int ret; + + /* first reset to mode 0 */ + ret = regmap_clear_bits(priv->regmap, S2MU005_REG_CHGR_CTRL0, + S2MU005_CHGR_OP_MODE); + if (ret) { + dev_err(priv->dev, "failed to reset opmode (%d)\n", ret); + return ret; + } + + /* wait for the charger to settle before switching to charging mode */ + msleep(50); + /* then set to charging mode */ + ret = regmap_update_bits(priv->regmap, S2MU005_REG_CHGR_CTRL0, + S2MU005_CHGR_OP_MODE, + FIELD_PREP(S2MU005_CHGR_OP_MODE, + S2MU005_CHGR_OP_MODE_CHG)); + if (ret) { + dev_err(priv->dev, "failed to set opmode to charging (%d)\n", ret); + return ret; + } + + return 0; +} + +static int s2mu005_chgr_mode_unset(struct s2m_chgr *priv) +{ + int ret; + + /* turn off OTG */ + ret = regmap_clear_bits(priv->regmap, S2MU005_REG_CHGR_CTRL15, + S2MU005_CHGR_OTG_EN); + if (ret) { + dev_err(priv->dev, "failed to turn off OTG (%d)\n", ret); + return ret; + } + + /* reset operation mode */ + ret = regmap_clear_bits(priv->regmap, S2MU005_REG_CHGR_CTRL0, + S2MU005_CHGR_OP_MODE); + if (ret) { + dev_err(priv->dev, "failed to reset opmode (%d)\n", ret); + return ret; + } + + return 0; +} + +static void s2mu005_chgr_extcon_work(struct work_struct *work) +{ + struct s2m_chgr *priv = container_of(work, struct s2m_chgr, extcon_work); + + if (extcon_get_state(priv->extcon, EXTCON_USB_HOST) > 0) + s2mu005_chgr_mode_set_host(priv); + else if (extcon_get_state(priv->extcon, EXTCON_USB) > 0) + s2mu005_chgr_mode_set_charger(priv); + else + s2mu005_chgr_mode_unset(priv); + + power_supply_changed(priv->psy); +} + +static const enum power_supply_property s2mu005_chgr_properties[] = { + POWER_SUPPLY_PROP_ONLINE, + POWER_SUPPLY_PROP_USB_TYPE, +}; + +static const struct power_supply_desc s2mu005_chgr_psy_desc = { + .name = "s2mu005-charger", + .type = POWER_SUPPLY_TYPE_USB, + .properties = s2mu005_chgr_properties, + .num_properties = ARRAY_SIZE(s2mu005_chgr_properties), + .get_property = s2mu005_chgr_get_property, + .usb_types = BIT(POWER_SUPPLY_USB_TYPE_CDP) | + BIT(POWER_SUPPLY_USB_TYPE_SDP) | + BIT(POWER_SUPPLY_USB_TYPE_DCP) | + BIT(POWER_SUPPLY_USB_TYPE_UNKNOWN), +}; + +static int s2m_chgr_extcon_notifier(struct notifier_block *nb, + unsigned long event, void *param) +{ + struct s2m_chgr *priv = container_of(nb, struct s2m_chgr, extcon_nb); + + schedule_work(&priv->extcon_work); + + return NOTIFY_OK; +} + +static int s2m_chgr_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct sec_pmic_dev *pmic_drvdata = dev_get_drvdata(dev->parent); + struct s2m_chgr *priv; + struct device_node *extcon_node __free(device_node) = NULL; + struct power_supply_config psy_cfg = {}; + const struct power_supply_desc *psy_desc; + work_func_t extcon_work_func; + int ret; + + 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; + + switch (platform_get_device_id(pdev)->driver_data) { + case S2MU005: + psy_desc = &s2mu005_chgr_psy_desc; + extcon_work_func = s2mu005_chgr_extcon_work; + break; + default: + return dev_err_probe(dev, -ENODEV, + "device type %d is not supported by driver\n", + pmic_drvdata->device_type); + } + + /* MUIC is mandatory. If unavailable, request probe deferral */ + extcon_node = of_get_child_by_name(dev->parent->of_node, "muic"); + if (!extcon_node) + return dev_err_probe(dev, -ENODEV, "MUIC node required but not found\n"); + + priv->extcon = extcon_find_edev_by_node(extcon_node); + if (IS_ERR(priv->extcon)) + return -EPROBE_DEFER; + + psy_cfg.drv_data = priv; + psy_cfg.fwnode = dev_fwnode(dev->parent); + priv->psy = devm_power_supply_register(dev, psy_desc, &psy_cfg); + if (IS_ERR(priv->psy)) + return dev_err_probe(dev, PTR_ERR(priv->psy), + "failed to register power supply subsystem\n"); + + ret = devm_work_autocancel(dev, &priv->extcon_work, extcon_work_func); + if (ret) + return dev_err_probe(dev, ret, "failed to initialize extcon work\n"); + + priv->extcon_nb.notifier_call = s2m_chgr_extcon_notifier; + ret = devm_extcon_register_notifier_all(dev, priv->extcon, &priv->extcon_nb); + if (ret) + return dev_err_probe(dev, ret, "failed to register extcon notifier\n"); + + return 0; +} + +static const struct platform_device_id s2m_chgr_id_table[] = { + { "s2mu005-charger", S2MU005 }, + { /* sentinel */ }, +}; +MODULE_DEVICE_TABLE(platform, s2m_chgr_id_table); + +static struct platform_driver s2m_chgr_driver = { + .driver = { + .name = "s2m-charger", + }, + .probe = s2m_chgr_probe, + .id_table = s2m_chgr_id_table, +}; +module_platform_driver(s2m_chgr_driver); + +MODULE_DESCRIPTION("Battery Charger Driver For Samsung S2M Series PMICs"); +MODULE_AUTHOR("Kaustabh Chakraborty "); +MODULE_AUTHOR("Łukasz Lebiedziński "); +MODULE_LICENSE("GPL");