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.

Acked-by: Karel Balej <balejk@matfyz.cz> # for the PMIC
Signed-off-by: Duje Mihanović <duje@dujemihanovic.xyz>
Signed-off-by: Jonathan Cameron <Jonathan.Cameron@huawei.com>
pull/1354/merge
Duje Mihanović 2025-09-11 14:43:45 +02:00 committed by Jonathan Cameron
parent 0f06e28747
commit 3422b4bc60
5 changed files with 470 additions and 0 deletions

View File

@ -14714,6 +14714,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@dujemihanovic.xyz>
S: Maintained
F: drivers/iio/adc/88pm886-gpadc.c
MARVELL ARMADA 3700 PHY DRIVERS
M: Miquel Raynal <miquel.raynal@bootlin.com>
S: Maintained

View File

@ -0,0 +1,393 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Copyright 2025, Duje Mihanović <duje@dujemihanovic.xyz>
*/
#include <linux/bits.h>
#include <linux/bug.h>
#include <linux/delay.h>
#include <linux/device.h>
#include <linux/err.h>
#include <linux/i2c.h>
#include <linux/math.h>
#include <linux/mod_devicetable.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/pm_runtime.h>
#include <linux/regmap.h>
#include <linux/types.h>
#include <linux/units.h>
#include <asm/byteorder.h>
#include <linux/iio/iio.h>
#include <linux/iio/types.h>
#include <linux/mfd/88pm886.h>
struct pm886_gpadc {
struct regmap *map;
};
enum pm886_gpadc_channel {
VSC_CHAN,
VCHG_PWR_CHAN,
VCF_OUT_CHAN,
VBAT_CHAN,
VBAT_SLP_CHAN,
VBUS_CHAN,
GPADC0_CHAN,
GPADC1_CHAN,
GPADC2_CHAN,
GPADC3_CHAN,
GND_DET1_CHAN,
GND_DET2_CHAN,
MIC_DET_CHAN,
TINT_CHAN,
};
static const int pm886_gpadc_regs[] = {
[VSC_CHAN] = PM886_REG_GPADC_VSC,
[VCHG_PWR_CHAN] = PM886_REG_GPADC_VCHG_PWR,
[VCF_OUT_CHAN] = PM886_REG_GPADC_VCF_OUT,
[VBAT_CHAN] = PM886_REG_GPADC_VBAT,
[VBAT_SLP_CHAN] = PM886_REG_GPADC_VBAT_SLP,
[VBUS_CHAN] = PM886_REG_GPADC_VBUS,
[GPADC0_CHAN] = PM886_REG_GPADC_GPADC0,
[GPADC1_CHAN] = PM886_REG_GPADC_GPADC1,
[GPADC2_CHAN] = PM886_REG_GPADC_GPADC2,
[GPADC3_CHAN] = PM886_REG_GPADC_GPADC3,
[GND_DET1_CHAN] = PM886_REG_GPADC_GND_DET1,
[GND_DET2_CHAN] = PM886_REG_GPADC_GND_DET2,
[MIC_DET_CHAN] = PM886_REG_GPADC_MIC_DET,
[TINT_CHAN] = PM886_REG_GPADC_TINT,
};
#define ADC_CHANNEL_VOLTAGE(index, lsb, name) \
{ \
.type = IIO_VOLTAGE, \
.indexed = 1, \
.channel = index, \
.address = lsb, \
.info_mask_separate = BIT(IIO_CHAN_INFO_RAW) | \
BIT(IIO_CHAN_INFO_SCALE), \
.datasheet_name = name, \
}
#define ADC_CHANNEL_RESISTANCE(index, lsb, name) \
{ \
.type = IIO_RESISTANCE, \
.indexed = 1, \
.channel = index, \
.address = lsb, \
.info_mask_separate = BIT(IIO_CHAN_INFO_PROCESSED), \
.datasheet_name = name, \
}
#define ADC_CHANNEL_TEMPERATURE(index, lsb, name) \
{ \
.type = IIO_TEMP, \
.indexed = 1, \
.channel = index, \
.address = lsb, \
.info_mask_separate = BIT(IIO_CHAN_INFO_RAW) | \
BIT(IIO_CHAN_INFO_SCALE) | \
BIT(IIO_CHAN_INFO_OFFSET), \
.datasheet_name = name, \
}
static const struct iio_chan_spec pm886_gpadc_channels[] = {
ADC_CHANNEL_VOLTAGE(VSC_CHAN, 1367, "vsc"),
ADC_CHANNEL_VOLTAGE(VCHG_PWR_CHAN, 1709, "vchg_pwr"),
ADC_CHANNEL_VOLTAGE(VCF_OUT_CHAN, 1367, "vcf_out"),
ADC_CHANNEL_VOLTAGE(VBAT_CHAN, 1367, "vbat"),
ADC_CHANNEL_VOLTAGE(VBAT_SLP_CHAN, 1367, "vbat_slp"),
ADC_CHANNEL_VOLTAGE(VBUS_CHAN, 1709, "vbus"),
ADC_CHANNEL_RESISTANCE(GPADC0_CHAN, 342, "gpadc0"),
ADC_CHANNEL_RESISTANCE(GPADC1_CHAN, 342, "gpadc1"),
ADC_CHANNEL_RESISTANCE(GPADC2_CHAN, 342, "gpadc2"),
ADC_CHANNEL_RESISTANCE(GPADC3_CHAN, 342, "gpadc3"),
ADC_CHANNEL_VOLTAGE(GND_DET1_CHAN, 342, "gnddet1"),
ADC_CHANNEL_VOLTAGE(GND_DET2_CHAN, 342, "gnddet2"),
ADC_CHANNEL_VOLTAGE(MIC_DET_CHAN, 1367, "mic_det"),
ADC_CHANNEL_TEMPERATURE(TINT_CHAN, 104, "tint"),
};
static const struct regmap_config pm886_gpadc_regmap_config = {
.reg_bits = 8,
.val_bits = 8,
.max_register = PM886_GPADC_MAX_REGISTER,
};
static int gpadc_get_raw(struct iio_dev *iio, enum pm886_gpadc_channel chan)
{
struct pm886_gpadc *gpadc = iio_priv(iio);
__be16 buf;
int ret;
ret = regmap_bulk_read(gpadc->map, pm886_gpadc_regs[chan], &buf, sizeof(buf));
if (ret)
return ret;
return be16_to_cpu(buf) >> 4;
}
static int
gpadc_set_bias(struct pm886_gpadc *gpadc, enum pm886_gpadc_channel chan, bool on)
{
unsigned int gpadc_num = chan - GPADC0_CHAN;
unsigned int bits = BIT(gpadc_num + 4) | BIT(gpadc_num);
return regmap_assign_bits(gpadc->map, PM886_REG_GPADC_CONFIG(0x14), bits, on);
}
static int
gpadc_find_bias_current(struct iio_dev *iio, struct iio_chan_spec const *chan,
unsigned int *raw_uV, unsigned int *raw_uA)
{
struct pm886_gpadc *gpadc = iio_priv(iio);
unsigned int gpadc_num = chan->channel - GPADC0_CHAN;
unsigned int reg = PM886_REG_GPADC_CONFIG(0xb + gpadc_num);
unsigned long lsb = chan->address;
int ret;
for (unsigned int i = 0; i < PM886_GPADC_BIAS_LEVELS; i++) {
ret = regmap_update_bits(gpadc->map, reg, GENMASK(3, 0), i);
if (ret)
return ret;
/* Wait for the new bias level to apply. */
fsleep(5 * USEC_PER_MSEC);
*raw_uA = PM886_GPADC_INDEX_TO_BIAS_uA(i);
*raw_uV = gpadc_get_raw(iio, chan->channel) * lsb;
/*
* Vendor kernel errors out above 1.25 V, but testing shows
* that the resistance of the battery detection channel (GPADC2
* on coreprimevelte) reaches about 1.4 MΩ when the battery is
* removed, which can't be measured with such a low upper
* limit. Therefore, to be able to detect the battery without
* ugly externs as used in the vendor fuel gauge driver,
* increase this limit a bit.
*/
if (WARN_ON(*raw_uV > 1500 * (MICRO / MILLI)))
return -EIO;
/*
* Vendor kernel errors out under 300 mV, but for the same
* reason as above (except the channel hovers around 3.5 kΩ
* with battery present) reduce this limit.
*/
if (*raw_uV < 200 * (MICRO / MILLI)) {
dev_dbg(&iio->dev, "bad bias for chan %d: %d uA @ %d uV\n",
chan->channel, *raw_uA, *raw_uV);
continue;
}
dev_dbg(&iio->dev, "good bias for chan %d: %d uA @ %d uV\n",
chan->channel, *raw_uA, *raw_uV);
return 0;
}
dev_err(&iio->dev, "failed to find good bias for chan %d\n", chan->channel);
return -EINVAL;
}
static int
gpadc_get_resistance_ohm(struct iio_dev *iio, struct iio_chan_spec const *chan)
{
struct pm886_gpadc *gpadc = iio_priv(iio);
unsigned int raw_uV, raw_uA;
int ret;
ret = gpadc_set_bias(gpadc, chan->channel, true);
if (ret)
goto out;
ret = gpadc_find_bias_current(iio, chan, &raw_uV, &raw_uA);
if (ret)
goto out;
ret = DIV_ROUND_CLOSEST(raw_uV, raw_uA);
out:
gpadc_set_bias(gpadc, chan->channel, false);
return ret;
}
static int
__pm886_gpadc_read_raw(struct iio_dev *iio, struct iio_chan_spec const *chan,
int *val, int *val2, long mask)
{
unsigned long lsb = chan->address;
switch (mask) {
case IIO_CHAN_INFO_RAW:
*val = gpadc_get_raw(iio, chan->channel);
if (*val < 0)
return *val;
return IIO_VAL_INT;
case IIO_CHAN_INFO_SCALE:
*val = lsb;
if (chan->type == IIO_VOLTAGE) {
*val2 = MILLI;
return IIO_VAL_FRACTIONAL;
} else {
return IIO_VAL_INT;
}
case IIO_CHAN_INFO_OFFSET:
/* Raw value is 104 millikelvin/LSB, convert it to 104 millicelsius/LSB */
*val = ABSOLUTE_ZERO_MILLICELSIUS;
*val2 = lsb;
return IIO_VAL_FRACTIONAL;
case IIO_CHAN_INFO_PROCESSED:
*val = gpadc_get_resistance_ohm(iio, chan);
if (*val < 0)
return *val;
return IIO_VAL_INT;
default:
return -EINVAL;
}
}
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 ret;
ret = pm_runtime_resume_and_get(dev);
if (ret)
return ret;
ret = __pm886_gpadc_read_raw(iio, chan, val, val2, mask);
pm_runtime_put_autosuspend(dev);
return ret;
}
static int pm886_gpadc_hw_enable(struct regmap *map)
{
const u8 config[] = {
PM886_GPADC_CONFIG1_EN_ALL,
PM886_GPADC_CONFIG2_EN_ALL,
PM886_GPADC_GND_DET2_EN,
};
int ret;
/* Enable the ADC block. */
ret = regmap_set_bits(map, PM886_REG_GPADC_CONFIG(0x6), BIT(0));
if (ret)
return ret;
/* Enable all channels. */
return regmap_bulk_write(map, PM886_REG_GPADC_CONFIG(0x1), config, ARRAY_SIZE(config));
}
static int pm886_gpadc_hw_disable(struct regmap *map)
{
return regmap_clear_bits(map, PM886_REG_GPADC_CONFIG(0x6), BIT(0));
}
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;
struct pm886_chip *chip = dev_get_drvdata(dev->parent);
struct i2c_client *client = chip->client;
struct pm886_gpadc *gpadc;
struct i2c_client *page;
struct iio_dev *iio;
int ret;
iio = devm_iio_device_alloc(dev, sizeof(*gpadc));
if (!iio)
return -ENOMEM;
gpadc = 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");
gpadc->map = devm_regmap_init_i2c(page, &pm886_gpadc_regmap_config);
if (IS_ERR(gpadc->map))
return dev_err_probe(dev, PTR_ERR(gpadc->map),
"Failed to initialize GPADC regmap\n");
iio->name = "88pm886-gpadc";
iio->modes = INDIO_DIRECT_MODE;
iio->info = &pm886_gpadc_iio_info;
iio->channels = pm886_gpadc_channels;
iio->num_channels = ARRAY_SIZE(pm886_gpadc_channels);
device_set_node(&iio->dev, dev_fwnode(dev->parent));
ret = devm_pm_runtime_enable(dev);
if (ret)
return dev_err_probe(dev, ret, "Failed to enable runtime PM\n");
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 pm886_gpadc *gpadc = iio_priv(iio);
return pm886_gpadc_hw_enable(gpadc->map);
}
static int pm886_gpadc_runtime_suspend(struct device *dev)
{
struct iio_dev *iio = dev_get_drvdata(dev);
struct pm886_gpadc *gpadc = iio_priv(iio);
return pm886_gpadc_hw_disable(gpadc->map);
}
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@dujemihanovic.xyz>");
MODULE_DESCRIPTION("Marvell 88PM886 GPADC driver");
MODULE_LICENSE("GPL");

View File

@ -9,6 +9,19 @@ menu "Analog to digital converters"
config IIO_ADC_HELPER
tristate
config 88PM886_GPADC
tristate "Marvell 88PM886 GPADC driver"
depends on MFD_88PM886_PMIC
default MFD_88PM886_PMIC
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 Vbus.
To compile this driver as a module, choose M here: the module will be
called 88pm886-gpadc.
config AB8500_GPADC
bool "ST-Ericsson AB8500 GPADC driver"
depends on AB8500_CORE && REGULATOR_AB8500

View File

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

View File

@ -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
@ -70,6 +71,63 @@
#define PM886_LDO_VSEL_MASK 0x0f
#define PM886_BUCK_VSEL_MASK 0x7f
/* GPADC enable/disable registers */
#define PM886_REG_GPADC_CONFIG(n) (n)
#define PM886_GPADC_VSC_EN BIT(0)
#define PM886_GPADC_VBAT_EN BIT(1)
#define PM886_GPADC_GNDDET1_EN BIT(3)
#define PM886_GPADC_VBUS_EN BIT(4)
#define PM886_GPADC_VCHG_PWR_EN BIT(5)
#define PM886_GPADC_VCF_OUT_EN BIT(6)
#define PM886_GPADC_CONFIG1_EN_ALL \
(PM886_GPADC_VSC_EN | \
PM886_GPADC_VBAT_EN | \
PM886_GPADC_GNDDET1_EN | \
PM886_GPADC_VBUS_EN | \
PM886_GPADC_VCHG_PWR_EN | \
PM886_GPADC_VCF_OUT_EN)
#define PM886_GPADC_TINT_EN BIT(0)
#define PM886_GPADC_PMODE_EN BIT(1)
#define PM886_GPADC_GPADC0_EN BIT(2)
#define PM886_GPADC_GPADC1_EN BIT(3)
#define PM886_GPADC_GPADC2_EN BIT(4)
#define PM886_GPADC_GPADC3_EN BIT(5)
#define PM886_GPADC_MIC_DET_EN BIT(6)
#define PM886_GPADC_CONFIG2_EN_ALL \
(PM886_GPADC_TINT_EN | \
PM886_GPADC_GPADC0_EN | \
PM886_GPADC_GPADC1_EN | \
PM886_GPADC_GPADC2_EN | \
PM886_GPADC_GPADC3_EN | \
PM886_GPADC_MIC_DET_EN)
/* No CONFIG3_EN_ALL because this is the only bit there. */
#define PM886_GPADC_GND_DET2_EN BIT(0)
/* 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_GND_DET1 0xa4
#define PM886_REG_GPADC_GND_DET2 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
/* VBAT_SLP is the last register and is 2 bytes wide like other channels. */
#define PM886_GPADC_MAX_REGISTER (PM886_REG_GPADC_VBAT_SLP + 1)
#define PM886_GPADC_BIAS_LEVELS 16
#define PM886_GPADC_INDEX_TO_BIAS_uA(i) (1 + (i) * 5)
struct pm886_chip {
struct i2c_client *client;
unsigned int chip_id;