On 8/28/25 5:17 PM, Duje Mihanović wrote:
> 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>

We usually try to avoid including kernel.h because it includes too much.
There are some recent-ish messages on the iio mailing list discussing
include-what-you-use with some tips on how to pick the headers that are
actually being used for inclusion.

> +#include <linux/mod_devicetable.h>
> +#include <linux/platform_device.h>
> +#include <linux/pm_runtime.h>
> +#include <linux/regmap.h>
> +
> +#include <linux/mfd/88pm886.h>

Odd to have this one not grouped with the rest.

> +
> +static const int regs[] = {

Would be nice to have the pm886_gpadc_ prefix on all global names.

> +     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, \

Do you have a link for the datasheet?

> +}
> +
> +static const struct iio_chan_spec pm886_adc_channels[] = {

Would be nice to be consistent with the prefix, either pm886_gpadc_
or pm886_adc_ everywhere.

> +     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"),

Is it safe (or sensible) to have both voltage and resistance channels
for the same input at the same time? It seems like if a voltage
channel was connected to an active circuit, we would not want to be
supplying current to it to take a resistance reading (this doesn't
sound safe). Likewise, if a voltage input has a passive load on it,
wouldn't the voltage channel always return 0 because no current was
supplied to induce a voltate (doesn't seem sensible to have a channel
that does notthing useful).

It might make sense to have some firmware (e.g. devicetree) to describe
if the input is active or passive on the voltage inputs and set up the
channels accordingly.

I'm also wondering if the other channels like vbat and vbus are always
wired up to these things internally or if this channel usage is only for
a specific system.

> +};
> +
> +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);

The double-pointer is a bit unusual. Maybe consider creating a struct
for private data even if it only has one field for now.

Or write it this like:

        struct regmap *map = *iio_priv(iio);

So that we don't have to write *map everywhere else.

> +     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;

Does this actually work for GPADC3_RES_CHAN?

GPADC3_RES_CHAN == GPADC0_RES_CHAN + 3 but GPADC3_CHAN != GPADC0_CHAN + 3

> +
> +     ret = regmap_bulk_read(*map, regs[chan], buf, 2);
> +
> +     if (ret)
> +             return ret;
> +
> +     val = ((buf[0] & 0xff) << 4) | (buf[1] & 0xf);
> +     val &= 0xfff;

This line seems reduandant as mask was already applied in previous line.

> +
> +     return val;
> +}
> +
> +static int gpadc_enable_bias(struct regmap *map, enum pm886_gpadc_channel 
> chan)
> +{
> +     int adcnum = chan - GPADC0_RES_CHAN, bits;

Jonathan prefers to have initializers on separate line. so bits should be
moved to a new line.

> +
> +     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);

fsleep()

> +
> +             *amp = 1 + i * 5;
> +             *volt = gpadc_get_raw(iio, chan->channel) * chan->address;

I know the address can be used for anything the driver wants it to be. :-)
But this reads a bit weird. It would be a bit easier to understand if we
had a separate lookup table to get this info. Or at least store it in a
local variable first so we can get a meaningful name for th value.

> +
> +             /* Measured voltage should never exceed 1.25V */
> +             if (WARN_ON(*volt > 1250000))

Units of volt is not clear. Would be better named as raw_uv or similar.
Same applies to `amp` parameter.

> +                     return -EIO;
> +
> +             if (*volt < 300000) {

Writing this as `raw_uv < 300 * (MICRO / MILLI)` could make it easier to
understand that we are checking if the raw value (in microvolts) is less
than 300 millivolts. Same applies to 1250000 above.

> +                     dev_dbg(&iio->dev, "bad bias for chan %d: %duA @ 
> %duV\n", chan->channel,
> +                             *amp, *volt);

Could be a bit more clear to put continue; here and drop the else.

> +             } 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)

s/resistor/resistance/ and add unit suffix, e.g. _ohm

> +{
> +     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, &amp);
> +     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,

Wrap to 80 characters.

> +                  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:

If there is IIO_CHAN_INFO_RAW, then we also should have IIO_CHAN_INFO_SCALE.

> +             *val = raw;
> +             dev_dbg(&iio->dev, "chan: %d, raw: %d\n", chan->channel, *val);
> +             ret = IIO_VAL_INT;
> +             break;
> +     case IIO_CHAN_INFO_PROCESSED: {

Unusual to have both raw and processed. What is the motivation?

> +             *val = raw * chan->address;
> +             ret = IIO_VAL_INT;

Why not use IIO_VAL_INT_PLUS_MICRO and not lose information?

> +
> +             /*
> +              * 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;
> +     }

Brace should be before default:.

> +     }
> +
> +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};

IIRC, IIO subsystem prefers spaces around the braces.

                        { 0xff, 0xfd, 0x1 };

Also, could use some macros to explain what these values are.


> +     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;

Move parent to separate lne.

> +     struct pm886_chip *chip = dev_get_drvdata(parent);
> +     struct i2c_client *client = chip->client, *page;

Move page to separate line.

> +     struct regmap **map;
> +     struct iio_dev *iio;
> +     int ret;
> +
> +     iio = devm_iio_device_alloc(dev, sizeof(*map));
> +     if (!iio)
> +             return -ENOMEM;

Add blank line.

> +     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

Could just write this as:

#define PM886_REG_GPADC_CONFIG(n)               (n)

> +
> +/* 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

which covers these too.

Most of these aren't used anyway.

Also suspicious that there are 5 registers listed here
but only 4 channels for resistance.

> +
> +/* 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
>  
> 


Reply via email to