| // SPDX-License-Identifier: GPL-2.0+ | |
| /* | |
| * Copyright 2012 Samsung Electronics Co., Ltd | |
| * http://www.samsung.com | |
| * Copyright 2025 Linaro Ltd. | |
| * | |
| * Samsung SxM core driver | |
| */ | |
| #include <linux/device.h> | |
| #include <linux/err.h> | |
| #include <linux/export.h> | |
| #include <linux/interrupt.h> | |
| #include <linux/mfd/core.h> | |
| #include <linux/mfd/samsung/core.h> | |
| #include <linux/mfd/samsung/irq.h> | |
| #include <linux/mfd/samsung/s2mps11.h> | |
| #include <linux/mfd/samsung/s2mps13.h> | |
| #include <linux/mfd/samsung/s2mu005.h> | |
| #include <linux/module.h> | |
| #include <linux/of.h> | |
| #include <linux/pm.h> | |
| #include <linux/pm_runtime.h> | |
| #include <linux/regmap.h> | |
| #include "sec-core.h" | |
| static const struct resource s5m8767_rtc_resources[] = { | |
| DEFINE_RES_IRQ_NAMED(S5M8767_IRQ_RTCA1, "alarm"), | |
| }; | |
| static const struct mfd_cell s5m8767_devs[] = { | |
| MFD_CELL_NAME("s5m8767-pmic"), | |
| MFD_CELL_RES("s5m-rtc", s5m8767_rtc_resources), | |
| MFD_CELL_OF("s5m8767-clk", NULL, NULL, 0, 0, "samsung,s5m8767-clk"), | |
| }; | |
| static const struct mfd_cell s2dos05_devs[] = { | |
| MFD_CELL_NAME("s2dos05-regulator"), | |
| }; | |
| static const struct resource s2mpg10_rtc_resources[] = { | |
| DEFINE_RES_IRQ_NAMED(S2MPG10_IRQ_RTCA0, "alarm"), | |
| }; | |
| static const struct mfd_cell s2mpg10_devs[] = { | |
| MFD_CELL_NAME("s2mpg10-meter"), | |
| MFD_CELL_NAME("s2mpg10-regulator"), | |
| MFD_CELL_RES("s2mpg10-rtc", s2mpg10_rtc_resources), | |
| MFD_CELL_OF("s2mpg10-clk", NULL, NULL, 0, 0, "samsung,s2mpg10-clk"), | |
| MFD_CELL_OF("s2mpg10-gpio", NULL, NULL, 0, 0, "samsung,s2mpg10-gpio"), | |
| }; | |
| static const struct mfd_cell s2mpg11_devs[] = { | |
| MFD_CELL_NAME("s2mpg11-meter"), | |
| MFD_CELL_NAME("s2mpg11-regulator"), | |
| MFD_CELL_OF("s2mpg11-gpio", NULL, NULL, 0, 0, "samsung,s2mpg11-gpio"), | |
| }; | |
| static const struct resource s2mps11_rtc_resources[] = { | |
| DEFINE_RES_IRQ_NAMED(S2MPS11_IRQ_RTCA0, "alarm"), | |
| }; | |
| static const struct mfd_cell s2mps11_devs[] = { | |
| MFD_CELL_NAME("s2mps11-regulator"), | |
| MFD_CELL_RES("s2mps14-rtc", s2mps11_rtc_resources), | |
| MFD_CELL_OF("s2mps11-clk", NULL, NULL, 0, 0, "samsung,s2mps11-clk"), | |
| }; | |
| static const struct resource s2mps14_rtc_resources[] = { | |
| DEFINE_RES_IRQ_NAMED(S2MPS14_IRQ_RTCA0, "alarm"), | |
| }; | |
| static const struct mfd_cell s2mps13_devs[] = { | |
| MFD_CELL_NAME("s2mps13-regulator"), | |
| MFD_CELL_RES("s2mps13-rtc", s2mps14_rtc_resources), | |
| MFD_CELL_OF("s2mps13-clk", NULL, NULL, 0, 0, "samsung,s2mps13-clk"), | |
| }; | |
| static const struct mfd_cell s2mps14_devs[] = { | |
| MFD_CELL_NAME("s2mps14-regulator"), | |
| MFD_CELL_RES("s2mps14-rtc", s2mps14_rtc_resources), | |
| MFD_CELL_OF("s2mps14-clk", NULL, NULL, 0, 0, "samsung,s2mps14-clk"), | |
| }; | |
| static const struct mfd_cell s2mps15_devs[] = { | |
| MFD_CELL_NAME("s2mps15-regulator"), | |
| MFD_CELL_RES("s2mps15-rtc", s2mps14_rtc_resources), | |
| MFD_CELL_OF("s2mps13-clk", NULL, NULL, 0, 0, "samsung,s2mps13-clk"), | |
| }; | |
| static const struct mfd_cell s2mpa01_devs[] = { | |
| MFD_CELL_NAME("s2mpa01-pmic"), | |
| MFD_CELL_RES("s2mps14-rtc", s2mps14_rtc_resources), | |
| }; | |
| static const struct mfd_cell s2mpu02_devs[] = { | |
| MFD_CELL_NAME("s2mpu02-regulator"), | |
| }; | |
| static const struct resource s2mpu05_rtc_resources[] = { | |
| DEFINE_RES_IRQ_NAMED(S2MPU05_IRQ_RTCA0, "alarm"), | |
| }; | |
| static const struct mfd_cell s2mpu05_devs[] = { | |
| MFD_CELL_NAME("s2mpu05-regulator"), | |
| MFD_CELL_RES("s2mps15-rtc", s2mpu05_rtc_resources), | |
| }; | |
| static const struct resource s2mu005_muic_resources[] = { | |
| DEFINE_RES_IRQ_NAMED(S2MU005_IRQ_MUIC_ATTACH, "attach"), | |
| DEFINE_RES_IRQ_NAMED(S2MU005_IRQ_MUIC_DETACH, "detach"), | |
| }; | |
| static const struct mfd_cell s2mu005_devs[] = { | |
| MFD_CELL_NAME("s2mu005-charger"), | |
| MFD_CELL_OF("s2mu005-flash", NULL, NULL, 0, 0, "samsung,s2mu005-flash"), | |
| MFD_CELL_OF("s2mu005-muic", s2mu005_muic_resources, NULL, 0, 0, "samsung,s2mu005-muic"), | |
| MFD_CELL_OF("s2mu005-rgb", NULL, NULL, 0, 0, "samsung,s2mu005-rgb"), | |
| }; | |
| static void sec_pmic_dump_rev(struct sec_pmic_dev *sec_pmic) | |
| { | |
| unsigned int reg, mask, val; | |
| switch (sec_pmic->device_type) { | |
| case S2MPG10: | |
| case S2MPG11: | |
| /* For s2mpg1x, the revision is in a different regmap */ | |
| return; | |
| case S2MU005: | |
| reg = S2MU005_REG_ID; | |
| mask = S2MU005_ID_MASK; | |
| break; | |
| default: | |
| /* For other device types, REG_ID is always the first register. */ | |
| reg = S2MPS11_REG_ID; | |
| mask = ~0; | |
| } | |
| if (!regmap_read(sec_pmic->regmap_pmic, reg, &val)) | |
| dev_dbg(sec_pmic->dev, "Revision: 0x%x\n", field_get(mask, val)); | |
| } | |
| static void sec_pmic_configure(struct sec_pmic_dev *sec_pmic) | |
| { | |
| int err; | |
| if (sec_pmic->device_type != S2MPS13X) | |
| return; | |
| if (sec_pmic->pdata->disable_wrstbi) { | |
| /* | |
| * If WRSTBI pin is pulled down this feature must be disabled | |
| * because each Suspend to RAM will trigger buck voltage reset | |
| * to default values. | |
| */ | |
| err = regmap_update_bits(sec_pmic->regmap_pmic, | |
| S2MPS13_REG_WRSTBI, | |
| S2MPS13_REG_WRSTBI_MASK, 0x0); | |
| if (err) | |
| dev_warn(sec_pmic->dev, | |
| "Cannot initialize WRSTBI config: %d\n", | |
| err); | |
| } | |
| } | |
| /* | |
| * Only the common platform data elements for s5m8767 are parsed here from the | |
| * device tree. Other sub-modules of s5m8767 such as pmic, rtc , charger and | |
| * others have to parse their own platform data elements from device tree. | |
| * | |
| * The s5m8767 platform data structure is instantiated here and the drivers for | |
| * the sub-modules need not instantiate another instance while parsing their | |
| * platform data. | |
| */ | |
| static struct sec_platform_data * | |
| sec_pmic_parse_dt_pdata(struct device *dev) | |
| { | |
| struct sec_platform_data *pd; | |
| pd = devm_kzalloc(dev, sizeof(*pd), GFP_KERNEL); | |
| if (!pd) | |
| return ERR_PTR(-ENOMEM); | |
| pd->manual_poweroff = of_property_read_bool(dev->of_node, | |
| "samsung,s2mps11-acokb-ground"); | |
| pd->disable_wrstbi = of_property_read_bool(dev->of_node, | |
| "samsung,s2mps11-wrstbi-ground"); | |
| return pd; | |
| } | |
| int sec_pmic_probe(struct device *dev, int device_type, unsigned int irq, | |
| struct regmap *regmap, struct i2c_client *client) | |
| { | |
| struct regmap_irq_chip_data *irq_data; | |
| struct sec_platform_data *pdata; | |
| const struct mfd_cell *sec_devs; | |
| struct sec_pmic_dev *sec_pmic; | |
| int ret, num_sec_devs; | |
| sec_pmic = devm_kzalloc(dev, sizeof(*sec_pmic), GFP_KERNEL); | |
| if (!sec_pmic) | |
| return -ENOMEM; | |
| dev_set_drvdata(dev, sec_pmic); | |
| sec_pmic->dev = dev; | |
| sec_pmic->device_type = device_type; | |
| sec_pmic->i2c = client; | |
| sec_pmic->irq = irq; | |
| sec_pmic->regmap_pmic = regmap; | |
| pdata = sec_pmic_parse_dt_pdata(sec_pmic->dev); | |
| if (IS_ERR(pdata)) { | |
| ret = PTR_ERR(pdata); | |
| return ret; | |
| } | |
| sec_pmic->pdata = pdata; | |
| irq_data = sec_irq_init(sec_pmic); | |
| if (IS_ERR(irq_data)) | |
| return PTR_ERR(irq_data); | |
| dev->coherent_dma_mask = 0; | |
| dev->dma_mask = &dev->coherent_dma_mask; | |
| pm_runtime_set_active(sec_pmic->dev); | |
| switch (sec_pmic->device_type) { | |
| case S5M8767X: | |
| sec_devs = s5m8767_devs; | |
| num_sec_devs = ARRAY_SIZE(s5m8767_devs); | |
| break; | |
| case S2DOS05: | |
| sec_devs = s2dos05_devs; | |
| num_sec_devs = ARRAY_SIZE(s2dos05_devs); | |
| break; | |
| case S2MPA01: | |
| sec_devs = s2mpa01_devs; | |
| num_sec_devs = ARRAY_SIZE(s2mpa01_devs); | |
| break; | |
| case S2MPG10: | |
| sec_devs = s2mpg10_devs; | |
| num_sec_devs = ARRAY_SIZE(s2mpg10_devs); | |
| break; | |
| case S2MPG11: | |
| sec_devs = s2mpg11_devs; | |
| num_sec_devs = ARRAY_SIZE(s2mpg11_devs); | |
| break; | |
| case S2MPS11X: | |
| sec_devs = s2mps11_devs; | |
| num_sec_devs = ARRAY_SIZE(s2mps11_devs); | |
| break; | |
| case S2MPS13X: | |
| sec_devs = s2mps13_devs; | |
| num_sec_devs = ARRAY_SIZE(s2mps13_devs); | |
| break; | |
| case S2MPS14X: | |
| sec_devs = s2mps14_devs; | |
| num_sec_devs = ARRAY_SIZE(s2mps14_devs); | |
| break; | |
| case S2MPS15X: | |
| sec_devs = s2mps15_devs; | |
| num_sec_devs = ARRAY_SIZE(s2mps15_devs); | |
| break; | |
| case S2MPU02: | |
| sec_devs = s2mpu02_devs; | |
| num_sec_devs = ARRAY_SIZE(s2mpu02_devs); | |
| break; | |
| case S2MPU05: | |
| sec_devs = s2mpu05_devs; | |
| num_sec_devs = ARRAY_SIZE(s2mpu05_devs); | |
| break; | |
| case S2MU005: | |
| sec_devs = s2mu005_devs; | |
| num_sec_devs = ARRAY_SIZE(s2mu005_devs); | |
| break; | |
| default: | |
| return dev_err_probe(sec_pmic->dev, -EINVAL, | |
| "Unsupported device type %d\n", | |
| sec_pmic->device_type); | |
| } | |
| ret = devm_mfd_add_devices(sec_pmic->dev, -1, sec_devs, num_sec_devs, | |
| NULL, 0, regmap_irq_get_domain(irq_data)); | |
| if (ret) | |
| return ret; | |
| sec_pmic_configure(sec_pmic); | |
| sec_pmic_dump_rev(sec_pmic); | |
| return ret; | |
| } | |
| EXPORT_SYMBOL_GPL(sec_pmic_probe); | |
| void sec_pmic_shutdown(struct device *dev) | |
| { | |
| struct sec_pmic_dev *sec_pmic = dev_get_drvdata(dev); | |
| unsigned int reg, mask; | |
| if (!sec_pmic->pdata->manual_poweroff) | |
| return; | |
| switch (sec_pmic->device_type) { | |
| case S2MPS11X: | |
| reg = S2MPS11_REG_CTRL1; | |
| mask = S2MPS11_CTRL1_PWRHOLD_MASK; | |
| break; | |
| default: | |
| /* | |
| * Currently only one board with S2MPS11 needs this, so just | |
| * ignore the rest. | |
| */ | |
| dev_warn(sec_pmic->dev, | |
| "Unsupported device %d for manual power off\n", | |
| sec_pmic->device_type); | |
| return; | |
| } | |
| regmap_update_bits(sec_pmic->regmap_pmic, reg, mask, 0); | |
| } | |
| EXPORT_SYMBOL_GPL(sec_pmic_shutdown); | |
| static int sec_pmic_suspend(struct device *dev) | |
| { | |
| struct sec_pmic_dev *sec_pmic = dev_get_drvdata(dev); | |
| if (device_may_wakeup(dev)) | |
| enable_irq_wake(sec_pmic->irq); | |
| /* | |
| * PMIC IRQ must be disabled during suspend for RTC alarm | |
| * to work properly. | |
| * When device is woken up from suspend, an | |
| * interrupt occurs before resuming I2C bus controller. | |
| * The interrupt is handled by regmap_irq_thread which tries | |
| * to read RTC registers. This read fails (I2C is still | |
| * suspended) and RTC Alarm interrupt is disabled. | |
| */ | |
| disable_irq(sec_pmic->irq); | |
| return 0; | |
| } | |
| static int sec_pmic_resume(struct device *dev) | |
| { | |
| struct sec_pmic_dev *sec_pmic = dev_get_drvdata(dev); | |
| if (device_may_wakeup(dev)) | |
| disable_irq_wake(sec_pmic->irq); | |
| enable_irq(sec_pmic->irq); | |
| return 0; | |
| } | |
| DEFINE_SIMPLE_DEV_PM_OPS(sec_pmic_pm_ops, sec_pmic_suspend, sec_pmic_resume); | |
| EXPORT_SYMBOL_GPL(sec_pmic_pm_ops); | |
| MODULE_AUTHOR("Chanwoo Choi <cw00.choi@samsung.com>"); | |
| MODULE_AUTHOR("Krzysztof Kozlowski <krzk@kernel.org>"); | |
| MODULE_AUTHOR("Sangbeom Kim <sbkim73@samsung.com>"); | |
| MODULE_AUTHOR("André Draszik <andre.draszik@linaro.org>"); | |
| MODULE_DESCRIPTION("Core driver for the Samsung S5M"); | |
| MODULE_LICENSE("GPL"); |