Marvell's 88PM886 PMIC has a so-called General Purpose ADC used for monitoring various system voltages and temperatures. Add the relevant register definitions to the MFD header and a driver for the ADC.
Signed-off-by: Duje Mihanović <d...@dujemihanovic.xyz> --- MAINTAINERS | 5 + drivers/iio/adc/88pm886-gpadc.c | 352 ++++++++++++++++++++++++++++++++++++++++ drivers/iio/adc/Kconfig | 10 ++ drivers/iio/adc/Makefile | 1 + include/linux/mfd/88pm886.h | 30 ++++ 5 files changed, 398 insertions(+) diff --git a/MAINTAINERS b/MAINTAINERS index fed6cd812d796a08cebc0c1fd540c8901d1bf448..b362d81e9c1532cc7920f9cec65b1fd1f81471c6 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -14710,6 +14710,11 @@ F: drivers/regulator/88pm886-regulator.c F: drivers/rtc/rtc-88pm886.c F: include/linux/mfd/88pm886.h +MARVELL 88PM886 PMIC GPADC DRIVER +M: Duje Mihanović <d...@dujemihanovic.xyz> +S: Maintained +F: drivers/iio/adc/88pm886-gpadc.c + MARVELL ARMADA 3700 PHY DRIVERS M: Miquel Raynal <miquel.ray...@bootlin.com> S: Maintained diff --git a/drivers/iio/adc/88pm886-gpadc.c b/drivers/iio/adc/88pm886-gpadc.c new file mode 100644 index 0000000000000000000000000000000000000000..129cff48641f1505175e64cf7dbdd0133f265ce8 --- /dev/null +++ b/drivers/iio/adc/88pm886-gpadc.c @@ -0,0 +1,352 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Copyright 2025, Duje Mihanović <d...@dujemihanovic.xyz> + */ + +#include <linux/device.h> +#include <linux/i2c.h> +#include <linux/iio/driver.h> +#include <linux/iio/iio.h> +#include <linux/iio/types.h> +#include <linux/kernel.h> +#include <linux/mod_devicetable.h> +#include <linux/platform_device.h> +#include <linux/pm_runtime.h> +#include <linux/regmap.h> + +#include <linux/mfd/88pm886.h> + +static const int regs[] = { + PM886_REG_GPADC_VSC, + PM886_REG_GPADC_VCHG_PWR, + PM886_REG_GPADC_VCF_OUT, + PM886_REG_GPADC_TINT, + + PM886_REG_GPADC_GPADC0, + PM886_REG_GPADC_GPADC1, + PM886_REG_GPADC_GPADC2, + + PM886_REG_GPADC_VBAT, + PM886_REG_GPADC_GNDDET1, + PM886_REG_GPADC_GNDDET2, + PM886_REG_GPADC_VBUS, + PM886_REG_GPADC_GPADC3, + + PM886_REG_GPADC_MIC_DET, + PM886_REG_GPADC_VBAT_SLP, +}; + +enum pm886_gpadc_channel { + VSC_CHAN, + VCHG_PWR_CHAN, + VCF_OUT_CHAN, + TINT_CHAN, + + GPADC0_CHAN, + GPADC1_CHAN, + GPADC2_CHAN, + + VBAT_CHAN, + GNDDET1_CHAN, + GNDDET2_CHAN, + VBUS_CHAN, + GPADC3_CHAN, + + MIC_DET_CHAN, + VBAT_SLP_CHAN, + + GPADC0_RES_CHAN, + GPADC1_RES_CHAN, + GPADC2_RES_CHAN, + GPADC3_RES_CHAN, +}; + +#define ADC_CHANNEL(index, lsb, _type, name) { \ + .type = _type, \ + .indexed = 1, \ + .channel = index, \ + .address = lsb, \ + .info_mask_separate = BIT(IIO_CHAN_INFO_RAW) | \ + BIT(IIO_CHAN_INFO_PROCESSED), \ + .datasheet_name = name, \ +} + +static const struct iio_chan_spec pm886_adc_channels[] = { + ADC_CHANNEL(VSC_CHAN, 1367, IIO_VOLTAGE, "vsc"), + ADC_CHANNEL(VCHG_PWR_CHAN, 1709, IIO_VOLTAGE, "vchg_pwr"), + ADC_CHANNEL(VCF_OUT_CHAN, 1367, IIO_VOLTAGE, "vcf_out"), + ADC_CHANNEL(TINT_CHAN, 104, IIO_TEMP, "tint"), + + ADC_CHANNEL(GPADC0_CHAN, 342, IIO_VOLTAGE, "gpadc0"), + ADC_CHANNEL(GPADC1_CHAN, 342, IIO_VOLTAGE, "gpadc1"), + ADC_CHANNEL(GPADC2_CHAN, 342, IIO_VOLTAGE, "gpadc2"), + + ADC_CHANNEL(VBAT_CHAN, 1367, IIO_VOLTAGE, "vbat"), + ADC_CHANNEL(GNDDET1_CHAN, 342, IIO_VOLTAGE, "gnddet1"), + ADC_CHANNEL(GNDDET2_CHAN, 342, IIO_VOLTAGE, "gnddet2"), + ADC_CHANNEL(VBUS_CHAN, 1709, IIO_VOLTAGE, "vbus"), + ADC_CHANNEL(GPADC3_CHAN, 342, IIO_VOLTAGE, "gpadc3"), + ADC_CHANNEL(MIC_DET_CHAN, 1367, IIO_VOLTAGE, "mic_det"), + ADC_CHANNEL(VBAT_SLP_CHAN, 1367, IIO_VOLTAGE, "vbat_slp"), + + ADC_CHANNEL(GPADC0_RES_CHAN, 342, IIO_RESISTANCE, "gpadc0_res"), + ADC_CHANNEL(GPADC1_RES_CHAN, 342, IIO_RESISTANCE, "gpadc1_res"), + ADC_CHANNEL(GPADC2_RES_CHAN, 342, IIO_RESISTANCE, "gpadc2_res"), + ADC_CHANNEL(GPADC3_RES_CHAN, 342, IIO_RESISTANCE, "gpadc3_res"), +}; + +static const struct regmap_config pm886_gpadc_regmap_config = { + .reg_bits = 8, + .val_bits = 8, + .max_register = PM886_REG_GPADC_VBAT_SLP + 1, +}; + +static int gpadc_get_raw(struct iio_dev *iio, enum pm886_gpadc_channel chan) +{ + struct regmap **map = iio_priv(iio); + int val, ret; + u8 buf[2]; + + if (chan >= GPADC0_RES_CHAN) + /* Resistor voltage drops are read from the corresponding voltage channel */ + chan -= GPADC0_RES_CHAN - GPADC0_CHAN; + + ret = regmap_bulk_read(*map, regs[chan], buf, 2); + + if (ret) + return ret; + + val = ((buf[0] & 0xff) << 4) | (buf[1] & 0xf); + val &= 0xfff; + + return val; +} + +static int gpadc_enable_bias(struct regmap *map, enum pm886_gpadc_channel chan) +{ + int adcnum = chan - GPADC0_RES_CHAN, bits; + + if (adcnum < 0 || adcnum > 3) + return -EINVAL; + + bits = BIT(adcnum + 4) | BIT(adcnum); + + return regmap_set_bits(map, PM886_REG_GPADC_CONFIG20, bits); +} + +static int +gpadc_find_bias_current(struct iio_dev *iio, struct iio_chan_spec const *chan, int *volt, + int *amp) +{ + struct regmap **map = iio_priv(iio); + int adcnum = chan->channel - GPADC0_RES_CHAN; + int reg = PM886_REG_GPADC_CONFIG11 + adcnum; + int ret; + + for (int i = 0; i < 16; i++) { + ret = regmap_update_bits(*map, reg, 0xf, i); + if (ret) + return ret; + + usleep_range(5000, 10000); + + *amp = 1 + i * 5; + *volt = gpadc_get_raw(iio, chan->channel) * chan->address; + + /* Measured voltage should never exceed 1.25V */ + if (WARN_ON(*volt > 1250000)) + return -EIO; + + if (*volt < 300000) { + dev_dbg(&iio->dev, "bad bias for chan %d: %duA @ %duV\n", chan->channel, + *amp, *volt); + } else { + dev_dbg(&iio->dev, "good bias for chan %d: %duA @ %duV\n", chan->channel, + *amp, *volt); + return 0; + } + } + + dev_err(&iio->dev, "failed to find good bias for chan %d\n", chan->channel); + return -EINVAL; +} + +static int +gpadc_get_resistor(struct iio_dev *iio, struct iio_chan_spec const *chan) +{ + struct regmap **map = iio_priv(iio); + int ret, volt, amp; + + ret = gpadc_enable_bias(*map, chan->channel); + if (ret) + return ret; + + ret = gpadc_find_bias_current(iio, chan, &volt, &); + if (ret) + return ret; + + return DIV_ROUND_CLOSEST(volt, amp); +} + +static int +pm886_gpadc_read_raw(struct iio_dev *iio, struct iio_chan_spec const *chan, int *val, int *val2, + long mask) +{ + struct device *dev = iio->dev.parent; + int raw, ret; + + ret = pm_runtime_resume_and_get(dev); + if (ret) + return ret; + + if (chan->type == IIO_RESISTANCE) { + raw = gpadc_get_resistor(iio, chan); + if (raw < 0) { + ret = raw; + goto out; + } + + *val = raw; + dev_dbg(&iio->dev, "chan: %d, %d Ohm\n", chan->channel, *val); + ret = IIO_VAL_INT; + goto out; + } + + raw = gpadc_get_raw(iio, chan->channel); + if (raw < 0) { + ret = raw; + goto out; + } + + switch (mask) { + case IIO_CHAN_INFO_RAW: + *val = raw; + dev_dbg(&iio->dev, "chan: %d, raw: %d\n", chan->channel, *val); + ret = IIO_VAL_INT; + break; + case IIO_CHAN_INFO_PROCESSED: { + *val = raw * chan->address; + ret = IIO_VAL_INT; + + /* + * Voltage measurements are scaled into uV. Scale them back + * into the mV dimension. + */ + if (chan->type == IIO_VOLTAGE) + *val = DIV_ROUND_CLOSEST(*val, 1000); + + dev_dbg(&iio->dev, "chan: %d, raw: %d, processed: %d\n", chan->channel, raw, *val); + break; + default: + ret = -EINVAL; + } + } + +out: + pm_runtime_mark_last_busy(dev); + pm_runtime_put_autosuspend(dev); + return ret; +} + +static int pm886_gpadc_setup(struct regmap *map, bool enable) +{ + const u8 config[] = {0xff, 0xfd, 0x1}; + int ret; + + /* Enable/disable the ADC block */ + ret = regmap_assign_bits(map, PM886_REG_GPADC_CONFIG6, BIT(0), enable); + if (ret || !enable) + return ret; + + /* If enabling, enable each individual ADC */ + return regmap_bulk_write(map, PM886_REG_GPADC_CONFIG1, config, ARRAY_SIZE(config)); +} + +static const struct iio_info pm886_gpadc_iio_info = { + .read_raw = pm886_gpadc_read_raw, +}; + +static int pm886_gpadc_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev, *parent = dev->parent; + struct pm886_chip *chip = dev_get_drvdata(parent); + struct i2c_client *client = chip->client, *page; + struct regmap **map; + struct iio_dev *iio; + int ret; + + iio = devm_iio_device_alloc(dev, sizeof(*map)); + if (!iio) + return -ENOMEM; + map = iio_priv(iio); + + dev_set_drvdata(dev, iio); + + page = devm_i2c_new_dummy_device(dev, client->adapter, + client->addr + PM886_PAGE_OFFSET_GPADC); + if (IS_ERR(page)) + return dev_err_probe(dev, PTR_ERR(page), "Failed to initialize GPADC page\n"); + + *map = devm_regmap_init_i2c(page, &pm886_gpadc_regmap_config); + if (IS_ERR(*map)) + return dev_err_probe(dev, PTR_ERR(*map), + "Failed to initialize GPADC regmap\n"); + + iio->name = "88pm886-gpadc"; + iio->dev.parent = dev; + iio->dev.of_node = parent->of_node; + iio->modes = INDIO_DIRECT_MODE; + iio->info = &pm886_gpadc_iio_info; + iio->channels = pm886_adc_channels; + iio->num_channels = ARRAY_SIZE(pm886_adc_channels); + + devm_pm_runtime_enable(dev); + pm_runtime_set_autosuspend_delay(dev, 50); + pm_runtime_use_autosuspend(dev); + + ret = devm_iio_device_register(dev, iio); + if (ret) + return dev_err_probe(dev, ret, "Failed to register ADC\n"); + + return 0; +} + +static int pm886_gpadc_runtime_resume(struct device *dev) +{ + struct iio_dev *iio = dev_get_drvdata(dev); + struct regmap **map = iio_priv(iio); + + return pm886_gpadc_setup(*map, true); +} + +static int pm886_gpadc_runtime_suspend(struct device *dev) +{ + struct iio_dev *iio = dev_get_drvdata(dev); + struct regmap **map = iio_priv(iio); + + return pm886_gpadc_setup(*map, false); +} + +static DEFINE_RUNTIME_DEV_PM_OPS(pm886_gpadc_pm_ops, + pm886_gpadc_runtime_suspend, + pm886_gpadc_runtime_resume, NULL); + +static const struct platform_device_id pm886_gpadc_id[] = { + { "88pm886-gpadc" }, + { } +}; +MODULE_DEVICE_TABLE(platform, pm886_gpadc_id); + +static struct platform_driver pm886_gpadc_driver = { + .driver = { + .name = "88pm886-gpadc", + .pm = pm_ptr(&pm886_gpadc_pm_ops), + }, + .probe = pm886_gpadc_probe, + .id_table = pm886_gpadc_id, +}; +module_platform_driver(pm886_gpadc_driver); + +MODULE_AUTHOR("Duje Mihanović <d...@dujemihanovic.xyz>"); +MODULE_DESCRIPTION("Marvell 88PM886 GPADC driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/iio/adc/Kconfig b/drivers/iio/adc/Kconfig index 24f2572c487ea3db2abec3283ebd93357c08baab..708a4f9b7b70b5044d070a8526a014c4bd362a10 100644 --- a/drivers/iio/adc/Kconfig +++ b/drivers/iio/adc/Kconfig @@ -9,6 +9,16 @@ menu "Analog to digital converters" config IIO_ADC_HELPER tristate +config 88PM886_GPADC + tristate "Marvell 88PM886 GPADC driver" + depends on MFD_88PM886_PMIC + default y + help + Say Y here to enable support for the GPADC (General Purpose ADC) + found on the Marvell 88PM886 PMIC. The GPADC measures various + internal voltages and temperatures, including (but not limited to) + system, battery and USB. + config AB8500_GPADC bool "ST-Ericsson AB8500 GPADC driver" depends on AB8500_CORE && REGULATOR_AB8500 diff --git a/drivers/iio/adc/Makefile b/drivers/iio/adc/Makefile index 1c6ca5fd4b6db8c4c40a351b231ba0892e8cd70e..64854907bf3bef7da39f95247e4e502d01232af3 100644 --- a/drivers/iio/adc/Makefile +++ b/drivers/iio/adc/Makefile @@ -6,6 +6,7 @@ obj-$(CONFIG_IIO_ADC_HELPER) += industrialio-adc.o # When adding new entries keep the list in alphabetical order +obj-$(CONFIG_88PM886_GPADC) += 88pm886-gpadc.o obj-$(CONFIG_AB8500_GPADC) += ab8500-gpadc.o obj-$(CONFIG_AD_SIGMA_DELTA) += ad_sigma_delta.o obj-$(CONFIG_AD4000) += ad4000.o diff --git a/include/linux/mfd/88pm886.h b/include/linux/mfd/88pm886.h index 85eca44f39ab58ba4cb9ec4216118ee9604d021f..44675f762ce6865dd6053b1aed00cc5987a7d5a2 100644 --- a/include/linux/mfd/88pm886.h +++ b/include/linux/mfd/88pm886.h @@ -10,6 +10,7 @@ #define PM886_IRQ_ONKEY 0 #define PM886_PAGE_OFFSET_REGULATORS 1 +#define PM886_PAGE_OFFSET_GPADC 2 #define PM886_REG_ID 0x00 @@ -67,6 +68,35 @@ #define PM886_REG_BUCK4_VOUT 0xcf #define PM886_REG_BUCK5_VOUT 0xdd +/* GPADC enable/disable registers */ +#define PM886_REG_GPADC_CONFIG1 0x1 +#define PM886_REG_GPADC_CONFIG2 0x2 +#define PM886_REG_GPADC_CONFIG3 0x3 +#define PM886_REG_GPADC_CONFIG6 0x6 + +/* GPADC bias current configuration registers */ +#define PM886_REG_GPADC_CONFIG11 0xb +#define PM886_REG_GPADC_CONFIG12 0xc +#define PM886_REG_GPADC_CONFIG13 0xd +#define PM886_REG_GPADC_CONFIG14 0xe +#define PM886_REG_GPADC_CONFIG20 0x14 + +/* GPADC channel registers */ +#define PM886_REG_GPADC_VSC 0x40 +#define PM886_REG_GPADC_VCHG_PWR 0x4c +#define PM886_REG_GPADC_VCF_OUT 0x4e +#define PM886_REG_GPADC_TINT 0x50 +#define PM886_REG_GPADC_GPADC0 0x54 +#define PM886_REG_GPADC_GPADC1 0x56 +#define PM886_REG_GPADC_GPADC2 0x58 +#define PM886_REG_GPADC_VBAT 0xa0 +#define PM886_REG_GPADC_GNDDET1 0xa4 +#define PM886_REG_GPADC_GNDDET2 0xa6 +#define PM886_REG_GPADC_VBUS 0xa8 +#define PM886_REG_GPADC_GPADC3 0xaa +#define PM886_REG_GPADC_MIC_DET 0xac +#define PM886_REG_GPADC_VBAT_SLP 0xb0 + #define PM886_LDO_VSEL_MASK 0x0f #define PM886_BUCK_VSEL_MASK 0x7f -- 2.51.0