[<prev] [next>] [<thread-prev] [day] [month] [year] [list]
Message-ID: <4f93d53a-3dfa-4b9f-8c09-73703888d263@baylibre.com>
Date: Thu, 28 Aug 2025 18:40:56 -0500
From: David Lechner <dlechner@...libre.com>
To: Duje Mihanović <duje@...emihanovic.xyz>,
Jonathan Cameron <jic23@...nel.org>, Nuno Sá
<nuno.sa@...log.com>, Andy Shevchenko <andy@...nel.org>,
Karel Balej <balejk@...fyz.cz>, Lee Jones <lee@...nel.org>
Cc: David Wronek <david@...nlining.org>, phone-devel@...r.kernel.org,
~postmarketos/upstreaming@...ts.sr.ht, linux-kernel@...r.kernel.org,
linux-iio@...r.kernel.org
Subject: Re: [PATCH 1/2] iio: adc: Add driver for Marvell 88PM886 PMIC ADC
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ć <duje@...emihanovic.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ć <duje@...emihanovic.xyz>
> +S: Maintained
> +F: drivers/iio/adc/88pm886-gpadc.c
> +
> MARVELL ARMADA 3700 PHY DRIVERS
> M: Miquel Raynal <miquel.raynal@...tlin.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ć <duje@...emihanovic.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, &);
> + 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ć <duje@...emihanovic.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
>
>
Powered by blists - more mailing lists