[<prev] [next>] [<thread-prev] [thread-next>] [day] [month] [year] [list]
Message-Id: <20250829-88pm886-gpadc-v1-1-f60262266fea@dujemihanovic.xyz>
Date: Fri, 29 Aug 2025 00:17:41 +0200
From: Duje Mihanović <duje@...emihanovic.xyz>
To: Jonathan Cameron <jic23@...nel.org>,
David Lechner <dlechner@...libre.com>,
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,
Duje Mihanović <duje@...emihanovic.xyz>
Subject: [PATCH 1/2] iio: adc: Add driver for Marvell 88PM886 PMIC ADC
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>
+#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ć <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
+
+/* 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
Powered by blists - more mailing lists