diff mbox series

[v4,2/2] iio: add MCP4728 I2C DAC driver

Message ID a0933003ed3c855f9d80d6ce0a40add2b6f0ba36.1691066050.git.andrea.collamati@gmail.com (mailing list archive)
State Accepted
Headers show
Series add MCP4728 I2C DAC driver​ | expand

Commit Message

Andrea Collamati Aug. 3, 2023, 12:56 p.m. UTC
MCP4728 is a 12-bit quad channel DAC with I2C interface.

support for:
 * per-channel gain
 * per-channel power state
 * per-channel power down mode control
 * per-channel vref selection internal/vdd
 * store current state to on-chip EEPROM

Signed-off-by: Andrea Collamati <andrea.collamati@gmail.com>
---
 drivers/iio/dac/Kconfig   |  11 +
 drivers/iio/dac/Makefile  |   1 +
 drivers/iio/dac/mcp4728.c | 626 ++++++++++++++++++++++++++++++++++++++
 3 files changed, 638 insertions(+)
 create mode 100644 drivers/iio/dac/mcp4728.c

Comments

Jonathan Cameron Aug. 5, 2023, 6:28 p.m. UTC | #1
On Thu,  3 Aug 2023 14:56:35 +0200
Andrea Collamati <andrea.collamati@gmail.com> wrote:

> MCP4728 is a 12-bit quad channel DAC with I2C interface.
> 
> support for:
>  * per-channel gain
>  * per-channel power state
>  * per-channel power down mode control
>  * per-channel vref selection internal/vdd
>  * store current state to on-chip EEPROM
> 
> Signed-off-by: Andrea Collamati <andrea.collamati@gmail.com>
Hi Andrea,

On this final read through I noticed a few minor things. Rather than
get you to do a v5 I've made the changes whilst applying. Please
take a sanity check at
https://git.kernel.org/pub/scm/linux/kernel/git/jic23/iio.git/log/?h=testing
to make sure I didn't break anything and let me know if I did!

Applied with the below mentioned changes to the togreg branch of iio.git
which is initially pushed out as testing to let 0-day poke at it and see
if it can find any problems.

Thanks,

Jonathan

> diff --git a/drivers/iio/dac/mcp4728.c b/drivers/iio/dac/mcp4728.c
> new file mode 100644
> index 000000000000..ba3eab349b0a
> --- /dev/null
> +++ b/drivers/iio/dac/mcp4728.c
> @@ -0,0 +1,626 @@
...



> +
> +static int mcp4728_set_scale(int channel, struct mcp4728_data *data, int val,
> +			     int val2)
> +{
> +	int scale = mcp4728_find_matching_scale(data, val, val2);
> +
> +	if (scale < 0)
> +		return scale;
> +
> +	switch (scale) {
> +	case MCP4728_SCALE_VDD:
> +		data->chdata[channel].ref_mode = MCP4728_VREF_EXTERNAL_VDD;
> +		break;
> +	case MCP4728_SCALE_VINT_NO_GAIN:
> +		data->chdata[channel].ref_mode = MCP4728_VREF_INTERNAL_2048mV;
> +		data->chdata[channel].g_mode   = MCP4728_GAIN_X1;
> +		break;
> +	case MCP4728_SCALE_VINT_GAIN_X2:
> +		data->chdata[channel].ref_mode = MCP4728_VREF_INTERNAL_2048mV;
> +		data->chdata[channel].g_mode   = MCP4728_GAIN_X2;
> +		break;
> +	default:
> +		return -EINVAL;
> +	}
> +	return 0;

return instead of breaking above as nothing to be done after the switch.
Side effect of that is this return 0 not needed.

> +}

...

> +static int mcp4728_write_raw(struct iio_dev *indio_dev,
> +			     struct iio_chan_spec const *chan, int val,
> +			     int val2, long mask)
> +{
> +	struct mcp4728_data *data = iio_priv(indio_dev);
> +	int ret;
> +
> +	switch (mask) {
> +	case IIO_CHAN_INFO_RAW:
> +		if (val < 0 || val > GENMASK(MCP4728_RESOLUTION - 1, 0))
> +			return -EINVAL;
> +		data->chdata[chan->channel].dac_value = val;
> +		ret = mcp4728_program_channel_cfg(chan->channel, indio_dev);
> +		break;

Direct returns preferred if no cleanup to do.

> +	case IIO_CHAN_INFO_SCALE:
> +		ret = mcp4728_set_scale(chan->channel, data, val, val2);
> +		if (ret)
> +			break;
> +
> +		ret = mcp4728_program_channel_cfg(chan->channel, indio_dev);
> +		break;
> +	default:
> +		ret = -EINVAL;
> +		break;
> +	}
> +
> +	return ret;
> +}



> +static int mcp4728_init_channels_data(struct mcp4728_data *data)
> +{
> +	u8 inbuf[MCP4728_READ_RESPONSE_LEN];
> +	int ret;
> +	unsigned int i;
> +
> +	ret = i2c_master_recv(data->client, inbuf, MCP4728_READ_RESPONSE_LEN);
> +	if (ret < 0) {
> +		dev_err(&data->client->dev,
> +			"failed to read mcp4728 conf. Err=%d\n", ret);
> +		return ret;

As this is only called from probe, we should still use dev_err_probe() in here
as mentioned below.


> +	} else if (ret != MCP4728_READ_RESPONSE_LEN) {
> +		dev_err(&data->client->dev,
> +			"failed to read mcp4728 conf. Wrong Response Len ret=%d\n",
> +			ret);
> +		return -EIO;
> +	}
> +
> +	for (i = 0; i < MCP4728_N_CHANNELS; i++) {
> +		struct mcp4728_channel_data *ch = &data->chdata[i];
> +		u8 r2				= inbuf[i * 6 + 1];
> +		u8 r3				= inbuf[i * 6 + 2];
> +
> +		ch->dac_value = FIELD_GET(MCP4728_DAC_H_MASK, r2) << 8 |
> +				FIELD_GET(MCP4728_DAC_L_MASK, r3);
> +		ch->ref_mode = FIELD_GET(MCP4728_VREF_MASK, r2);
> +		ch->pd_mode  = FIELD_GET(MCP4728_PDMODE_MASK, r2);
> +		ch->g_mode   = FIELD_GET(MCP4728_GAIN_MASK, r2);
> +	}
> +
> +	return 0;
> +}

...

> +static int mcp4728_probe(struct i2c_client *client)
> +{
> +	const struct i2c_device_id *id = i2c_client_get_device_id(client);
> +	struct mcp4728_data *data;
> +	struct iio_dev *indio_dev;
> +	int err;
> +
> +	indio_dev = devm_iio_device_alloc(&client->dev, sizeof(*data));
> +	if (!indio_dev)
> +		return -ENOMEM;
> +
> +	data = iio_priv(indio_dev);
> +	i2c_set_clientdata(client, indio_dev);
> +	data->client = client;
> +
> +	data->vdd_reg = devm_regulator_get(&client->dev, "vdd");
> +	if (IS_ERR(data->vdd_reg))
> +		return PTR_ERR(data->vdd_reg);
> +
> +	err = regulator_enable(data->vdd_reg);
> +	if (err)
> +		return err;
> +
> +	err = devm_add_action_or_reset(&client->dev, mcp4728_reg_disable,
> +				       data->vdd_reg);
> +	if (err)
> +		return err;
> +
> +	/* MCP4728 has internal EEPROM that save each channel boot configuration.

Trivial but comment syntax in IIO (there are some differences in a few other subsystems)
is
	/*
	 * MCP...

> +	 * It means that device configuration is unknown to the driver at kernel boot.
> +	 * mcp4728_init_channels_data reads back DAC settings and stores them in data
> +	 * structure.
> +	 */
> +	err = mcp4728_init_channels_data(data);
> +	if (err) {

I'd missed this previously but dev_err_probe() has several advantages in that
it handles deferring neatly and also ends up with less code.

> +		dev_err(&client->dev,
> +			"failed to read mcp4728 current configuration\n");
> +		return err;
> +	}
> +
> +	err = mcp4728_init_scales_avail(data);
> +	if (err) {
> +		dev_err(&client->dev, "failed to init scales\n");
> +		return err;
> +	}
> +
> +	indio_dev->name		= id->name;
> +	indio_dev->info		= &mcp4728_info;
> +	indio_dev->channels	= mcp4728_channels;
> +	indio_dev->num_channels = MCP4728_N_CHANNELS;
> +	indio_dev->modes	= INDIO_DIRECT_MODE;
> +
> +	return devm_iio_device_register(&client->dev, indio_dev);
> +}
Andrea Collamati Aug. 6, 2023, 1:40 p.m. UTC | #2
Hi Jonathan,

I tested the driver and I didn't find any issue.

These are the tests I performed:

Test Configuration:
SW:
git://git.kernel.org/pub/scm/linux/kernel/git/jic23/iio.git
commit: ee812337ee74cc403b4fbd85840dbe1b21d9a60f

HW:
- Aster board from Toradex hosting imx7d processor
- Adafruit MCP4728 https://www.adafruit.com/product/4470

TEST:

Probe Test                                OK
#> insmod mcp4728.ko

Read Scales                                OK
~# cat /sys/devices/platform/soc/30800000.bus/30a50000.i2c/i2c-3/3-0064/iio:device1/out_voltage_scale_available
0.805664 0.500000 1.000000

Write DAC CH0                            OK
#>iio_attr -c mcp4728 voltage0 scale    1.000000
#>iio_attr -c mcp4728 voltage0 raw         750
Voltage Monitor Reading:                749 mV

Write DAC CH1                            OK
#>iio_attr -c mcp4728 voltage1 scale    0.5
#>iio_attr -c mcp4728 voltage1 raw        750
Voltage Monitor Reading:                378 mV

Write DAC CH2                            OK
#>iio_attr -c mcp4728 voltage2 scale    0.805664
#>iio_attr -c mcp4728 voltage2 raw        750
Voltage Monitor Reading:                603 mV ~ 750*0.805664 ~ 604 mV

Write DAC CH3                            OK
#>iio_attr -c mcp4728 voltage3 scale    1.000000
#>iio_attr -c mcp4728 voltage3 raw        350
Voltage Monitor Reading:                354 mV

Store EEPROM                            OK
#>echo y > /sys/devices/platform/soc/30800000.bus/30a50000.i2c/i2c-3/3-0064/iio:device1/store_eeprom
#>power cycle board
Voltage Monitor Reading CH0                749 mV
Voltage Monitor Reading CH1                378 mV
Voltage Monitor Reading CH2                605 mV
Voltage Monitor Reading CH3                355 mV

Power Down CH0
#>iio_attr -c mcp4728 voltage0 powerdown_mode 1kohm_to_gnd
#>iio_attr -c mcp4728 voltage0 powerdown 1
Voltage Monitor Reading CH0                 0 mV

Power Down CH1
#>iio_attr -c mcp4728 voltage1 powerdown_mode 1kohm_to_gnd
#>iio_attr -c mcp4728 voltage1 powerdown 1
Voltage Monitor Reading CH1                 0 mV

Power Down CH2
#>iio_attr -c mcp4728 voltage2 powerdown_mode 1kohm_to_gnd
#>iio_attr -c mcp4728 voltage2 powerdown 1
Voltage Monitor Reading CH2                 0 mV

Power Down CH3
#>iio_attr -c mcp4728 voltage3 powerdown_mode 1kohm_to_gnd
#>iio_attr -c mcp4728 voltage3 powerdown 1
Voltage Monitor Reading CH3                 0 mV


Regards

        Andrea

On 8/5/23 20:28, Jonathan Cameron wrote:
> On Thu,  3 Aug 2023 14:56:35 +0200
> Andrea Collamati <andrea.collamati@gmail.com> wrote:
>
>> MCP4728 is a 12-bit quad channel DAC with I2C interface.
>>
>> support for:
>>  * per-channel gain
>>  * per-channel power state
>>  * per-channel power down mode control
>>  * per-channel vref selection internal/vdd
>>  * store current state to on-chip EEPROM
>>
>> Signed-off-by: Andrea Collamati <andrea.collamati@gmail.com>
> Hi Andrea,
>
> On this final read through I noticed a few minor things. Rather than
> get you to do a v5 I've made the changes whilst applying. Please
> take a sanity check at
> https://git.kernel.org/pub/scm/linux/kernel/git/jic23/iio.git/log/?h=testing
> to make sure I didn't break anything and let me know if I did!
>
> Applied with the below mentioned changes to the togreg branch of iio.git
> which is initially pushed out as testing to let 0-day poke at it and see
> if it can find any problems.
>
> Thanks,
>
> Jonathan
>
>> diff --git a/drivers/iio/dac/mcp4728.c b/drivers/iio/dac/mcp4728.c
>> new file mode 100644
>> index 000000000000..ba3eab349b0a
>> --- /dev/null
>> +++ b/drivers/iio/dac/mcp4728.c
>> @@ -0,0 +1,626 @@
> ...
>
>
>
>> +
>> +static int mcp4728_set_scale(int channel, struct mcp4728_data *data, int val,
>> +			     int val2)
>> +{
>> +	int scale = mcp4728_find_matching_scale(data, val, val2);
>> +
>> +	if (scale < 0)
>> +		return scale;
>> +
>> +	switch (scale) {
>> +	case MCP4728_SCALE_VDD:
>> +		data->chdata[channel].ref_mode = MCP4728_VREF_EXTERNAL_VDD;
>> +		break;
>> +	case MCP4728_SCALE_VINT_NO_GAIN:
>> +		data->chdata[channel].ref_mode = MCP4728_VREF_INTERNAL_2048mV;
>> +		data->chdata[channel].g_mode   = MCP4728_GAIN_X1;
>> +		break;
>> +	case MCP4728_SCALE_VINT_GAIN_X2:
>> +		data->chdata[channel].ref_mode = MCP4728_VREF_INTERNAL_2048mV;
>> +		data->chdata[channel].g_mode   = MCP4728_GAIN_X2;
>> +		break;
>> +	default:
>> +		return -EINVAL;
>> +	}
>> +	return 0;
> return instead of breaking above as nothing to be done after the switch.
> Side effect of that is this return 0 not needed.
>
>> +}
> ...
>
>> +static int mcp4728_write_raw(struct iio_dev *indio_dev,
>> +			     struct iio_chan_spec const *chan, int val,
>> +			     int val2, long mask)
>> +{
>> +	struct mcp4728_data *data = iio_priv(indio_dev);
>> +	int ret;
>> +
>> +	switch (mask) {
>> +	case IIO_CHAN_INFO_RAW:
>> +		if (val < 0 || val > GENMASK(MCP4728_RESOLUTION - 1, 0))
>> +			return -EINVAL;
>> +		data->chdata[chan->channel].dac_value = val;
>> +		ret = mcp4728_program_channel_cfg(chan->channel, indio_dev);
>> +		break;
> Direct returns preferred if no cleanup to do.
>
>> +	case IIO_CHAN_INFO_SCALE:
>> +		ret = mcp4728_set_scale(chan->channel, data, val, val2);
>> +		if (ret)
>> +			break;
>> +
>> +		ret = mcp4728_program_channel_cfg(chan->channel, indio_dev);
>> +		break;
>> +	default:
>> +		ret = -EINVAL;
>> +		break;
>> +	}
>> +
>> +	return ret;
>> +}
>
>
>> +static int mcp4728_init_channels_data(struct mcp4728_data *data)
>> +{
>> +	u8 inbuf[MCP4728_READ_RESPONSE_LEN];
>> +	int ret;
>> +	unsigned int i;
>> +
>> +	ret = i2c_master_recv(data->client, inbuf, MCP4728_READ_RESPONSE_LEN);
>> +	if (ret < 0) {
>> +		dev_err(&data->client->dev,
>> +			"failed to read mcp4728 conf. Err=%d\n", ret);
>> +		return ret;
> As this is only called from probe, we should still use dev_err_probe() in here
> as mentioned below.
>
>
>> +	} else if (ret != MCP4728_READ_RESPONSE_LEN) {
>> +		dev_err(&data->client->dev,
>> +			"failed to read mcp4728 conf. Wrong Response Len ret=%d\n",
>> +			ret);
>> +		return -EIO;
>> +	}
>> +
>> +	for (i = 0; i < MCP4728_N_CHANNELS; i++) {
>> +		struct mcp4728_channel_data *ch = &data->chdata[i];
>> +		u8 r2				= inbuf[i * 6 + 1];
>> +		u8 r3				= inbuf[i * 6 + 2];
>> +
>> +		ch->dac_value = FIELD_GET(MCP4728_DAC_H_MASK, r2) << 8 |
>> +				FIELD_GET(MCP4728_DAC_L_MASK, r3);
>> +		ch->ref_mode = FIELD_GET(MCP4728_VREF_MASK, r2);
>> +		ch->pd_mode  = FIELD_GET(MCP4728_PDMODE_MASK, r2);
>> +		ch->g_mode   = FIELD_GET(MCP4728_GAIN_MASK, r2);
>> +	}
>> +
>> +	return 0;
>> +}
> ...
>
>> +static int mcp4728_probe(struct i2c_client *client)
>> +{
>> +	const struct i2c_device_id *id = i2c_client_get_device_id(client);
>> +	struct mcp4728_data *data;
>> +	struct iio_dev *indio_dev;
>> +	int err;
>> +
>> +	indio_dev = devm_iio_device_alloc(&client->dev, sizeof(*data));
>> +	if (!indio_dev)
>> +		return -ENOMEM;
>> +
>> +	data = iio_priv(indio_dev);
>> +	i2c_set_clientdata(client, indio_dev);
>> +	data->client = client;
>> +
>> +	data->vdd_reg = devm_regulator_get(&client->dev, "vdd");
>> +	if (IS_ERR(data->vdd_reg))
>> +		return PTR_ERR(data->vdd_reg);
>> +
>> +	err = regulator_enable(data->vdd_reg);
>> +	if (err)
>> +		return err;
>> +
>> +	err = devm_add_action_or_reset(&client->dev, mcp4728_reg_disable,
>> +				       data->vdd_reg);
>> +	if (err)
>> +		return err;
>> +
>> +	/* MCP4728 has internal EEPROM that save each channel boot configuration.
> Trivial but comment syntax in IIO (there are some differences in a few other subsystems)
> is
> 	/*
> 	 * MCP...
>
>> +	 * It means that device configuration is unknown to the driver at kernel boot.
>> +	 * mcp4728_init_channels_data reads back DAC settings and stores them in data
>> +	 * structure.
>> +	 */
>> +	err = mcp4728_init_channels_data(data);
>> +	if (err) {
> I'd missed this previously but dev_err_probe() has several advantages in that
> it handles deferring neatly and also ends up with less code.
>
>> +		dev_err(&client->dev,
>> +			"failed to read mcp4728 current configuration\n");
>> +		return err;
>> +	}
>> +
>> +	err = mcp4728_init_scales_avail(data);
>> +	if (err) {
>> +		dev_err(&client->dev, "failed to init scales\n");
>> +		return err;
>> +	}
>> +
>> +	indio_dev->name		= id->name;
>> +	indio_dev->info		= &mcp4728_info;
>> +	indio_dev->channels	= mcp4728_channels;
>> +	indio_dev->num_channels = MCP4728_N_CHANNELS;
>> +	indio_dev->modes	= INDIO_DIRECT_MODE;
>> +
>> +	return devm_iio_device_register(&client->dev, indio_dev);
>> +}
diff mbox series

Patch

diff --git a/drivers/iio/dac/Kconfig b/drivers/iio/dac/Kconfig
index 3acd9c3f388e..93b8be183de6 100644
--- a/drivers/iio/dac/Kconfig
+++ b/drivers/iio/dac/Kconfig
@@ -389,6 +389,17 @@  config MCP4725
 	  To compile this driver as a module, choose M here: the module
 	  will be called mcp4725.
 
+config MCP4728
+	tristate "MCP4728 DAC driver"
+	depends on I2C
+	help
+	  Say Y here if you want to build a driver for the Microchip
+	  MCP4728 quad channel, 12-bit digital-to-analog converter (DAC)
+	  with I2C interface.
+
+	  To compile this driver as a module, choose M here: the module
+	  will be called mcp4728.
+
 config MCP4922
 	tristate "MCP4902, MCP4912, MCP4922 DAC driver"
 	depends on SPI
diff --git a/drivers/iio/dac/Makefile b/drivers/iio/dac/Makefile
index addd97a78838..5b2bac900d5a 100644
--- a/drivers/iio/dac/Makefile
+++ b/drivers/iio/dac/Makefile
@@ -41,6 +41,7 @@  obj-$(CONFIG_MAX517) += max517.o
 obj-$(CONFIG_MAX5522) += max5522.o
 obj-$(CONFIG_MAX5821) += max5821.o
 obj-$(CONFIG_MCP4725) += mcp4725.o
+obj-$(CONFIG_MCP4728) += mcp4728.o
 obj-$(CONFIG_MCP4922) += mcp4922.o
 obj-$(CONFIG_STM32_DAC_CORE) += stm32-dac-core.o
 obj-$(CONFIG_STM32_DAC) += stm32-dac.o
diff --git a/drivers/iio/dac/mcp4728.c b/drivers/iio/dac/mcp4728.c
new file mode 100644
index 000000000000..ba3eab349b0a
--- /dev/null
+++ b/drivers/iio/dac/mcp4728.c
@@ -0,0 +1,626 @@ 
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Support for Microchip MCP4728
+ *
+ * Copyright (C) 2023 Andrea Collamati <andrea.collamati@gmail.com>
+ *
+ * Based on mcp4725 by Peter Meerwald <pmeerw@pmeerw.net>
+ *
+ * Driver for the Microchip I2C 12-bit digital-to-analog quad channels
+ * converter (DAC).
+ *
+ * (7-bit I2C slave address 0x60, the three LSBs can be configured in
+ * hardware)
+ */
+
+#include <linux/bitfield.h>
+#include <linux/delay.h>
+#include <linux/err.h>
+#include <linux/i2c.h>
+#include <linux/iio/iio.h>
+#include <linux/iio/sysfs.h>
+#include <linux/module.h>
+#include <linux/mod_devicetable.h>
+#include <linux/property.h>
+#include <linux/regulator/consumer.h>
+
+#define MCP4728_RESOLUTION	  12
+#define MCP4728_N_CHANNELS	  4
+
+#define MCP4728_CMD_MASK	  GENMASK(7, 3)
+#define MCP4728_CHSEL_MASK	  GENMASK(2, 1)
+#define MCP4728_UDAC_MASK	  BIT(0)
+
+#define MCP4728_VREF_MASK	  BIT(7)
+#define MCP4728_PDMODE_MASK	  GENMASK(6, 5)
+#define MCP4728_GAIN_MASK	  BIT(4)
+
+#define MCP4728_DAC_H_MASK	  GENMASK(3, 0)
+#define MCP4728_DAC_L_MASK	  GENMASK(7, 0)
+
+#define MCP4728_RDY_MASK	  BIT(7)
+
+#define MCP4728_MW_CMD		  0x08 /* Multiwrite Command */
+#define MCP4728_SW_CMD		  0x0A /* Sequential Write Command with EEPROM */
+
+#define MCP4728_READ_RESPONSE_LEN (MCP4728_N_CHANNELS * 3 * 2)
+#define MCP4728_WRITE_EEPROM_LEN  (1 + MCP4728_N_CHANNELS * 2)
+
+enum vref_mode {
+	MCP4728_VREF_EXTERNAL_VDD    = 0,
+	MCP4728_VREF_INTERNAL_2048mV = 1,
+};
+
+enum gain_mode {
+	MCP4728_GAIN_X1 = 0,
+	MCP4728_GAIN_X2 = 1,
+};
+
+enum iio_powerdown_mode {
+	MCP4728_IIO_1K,
+	MCP4728_IIO_100K,
+	MCP4728_IIO_500K,
+};
+
+struct mcp4728_channel_data {
+	enum vref_mode ref_mode;
+	enum iio_powerdown_mode pd_mode;
+	enum gain_mode g_mode;
+	u16 dac_value;
+};
+
+/* MCP4728 Full Scale Ranges
+ * the device available ranges are
+ * - VREF = VDD				FSR = from 0.0V to VDD
+ * - VREF = Internal	Gain = 1	FSR = from 0.0V to VREF
+ * - VREF = Internal	Gain = 2	FSR = from 0.0V to 2*VREF
+ */
+enum mcp4728_scale {
+	MCP4728_SCALE_VDD,
+	MCP4728_SCALE_VINT_NO_GAIN,
+	MCP4728_SCALE_VINT_GAIN_X2,
+	MCP4728_N_SCALES
+};
+
+struct mcp4728_data {
+	struct i2c_client *client;
+	struct regulator *vdd_reg;
+	bool powerdown;
+	int scales_avail[MCP4728_N_SCALES * 2];
+	struct mcp4728_channel_data chdata[MCP4728_N_CHANNELS];
+};
+
+#define MCP4728_CHAN(chan) {						\
+	.type = IIO_VOLTAGE,						\
+	.output = 1,							\
+	.indexed = 1,							\
+	.channel = chan,						\
+	.info_mask_separate = BIT(IIO_CHAN_INFO_RAW)	|		\
+			      BIT(IIO_CHAN_INFO_SCALE),			\
+	.info_mask_shared_by_type_available = BIT(IIO_CHAN_INFO_SCALE),	\
+	.ext_info = mcp4728_ext_info,					\
+}
+
+static int mcp4728_suspend(struct device *dev);
+static int mcp4728_resume(struct device *dev);
+
+static ssize_t mcp4728_store_eeprom(struct device *dev,
+				    struct device_attribute *attr,
+				    const char *buf, size_t len)
+{
+	struct iio_dev *indio_dev = dev_to_iio_dev(dev);
+	struct mcp4728_data *data = iio_priv(indio_dev);
+	u8 outbuf[MCP4728_WRITE_EEPROM_LEN];
+	int tries = 20;
+	u8 inbuf[3];
+	bool state;
+	int ret;
+	unsigned int i;
+
+	ret = kstrtobool(buf, &state);
+	if (ret < 0)
+		return ret;
+
+	if (!state)
+		return 0;
+
+	outbuf[0] = FIELD_PREP(MCP4728_CMD_MASK, MCP4728_SW_CMD);
+
+	for (i = 0; i < MCP4728_N_CHANNELS; i++) {
+		struct mcp4728_channel_data *ch = &data->chdata[i];
+		int offset			= 1 + i * 2;
+
+		outbuf[offset] = FIELD_PREP(MCP4728_VREF_MASK, ch->ref_mode);
+
+		if (data->powerdown) {
+			u8 mcp4728_pd_mode = ch->pd_mode + 1;
+
+			outbuf[offset] |= FIELD_PREP(MCP4728_PDMODE_MASK,
+						     mcp4728_pd_mode);
+		}
+
+		outbuf[offset] |= FIELD_PREP(MCP4728_GAIN_MASK, ch->g_mode);
+		outbuf[offset] |=
+			FIELD_PREP(MCP4728_DAC_H_MASK, ch->dac_value >> 8);
+		outbuf[offset + 1] =
+			FIELD_PREP(MCP4728_DAC_L_MASK, ch->dac_value);
+	}
+
+	ret = i2c_master_send(data->client, outbuf, MCP4728_WRITE_EEPROM_LEN);
+	if (ret < 0)
+		return ret;
+	else if (ret != MCP4728_WRITE_EEPROM_LEN)
+		return -EIO;
+
+	/* wait RDY signal for write complete, takes up to 50ms */
+	while (tries--) {
+		msleep(20);
+		ret = i2c_master_recv(data->client, inbuf, 3);
+		if (ret < 0)
+			return ret;
+		else if (ret != 3)
+			return -EIO;
+
+		if (FIELD_GET(MCP4728_RDY_MASK, inbuf[0]))
+			break;
+	}
+
+	if (tries < 0) {
+		dev_err(&data->client->dev, "%s failed, incomplete\n",
+			__func__);
+		return -EIO;
+	}
+	return len;
+}
+
+static IIO_DEVICE_ATTR(store_eeprom, 0200, NULL, mcp4728_store_eeprom, 0);
+
+static struct attribute *mcp4728_attributes[] = {
+	&iio_dev_attr_store_eeprom.dev_attr.attr,
+	NULL,
+};
+
+static const struct attribute_group mcp4728_attribute_group = {
+	.attrs = mcp4728_attributes,
+};
+
+static int mcp4728_program_channel_cfg(int channel, struct iio_dev *indio_dev)
+{
+	struct mcp4728_data *data	= iio_priv(indio_dev);
+	struct mcp4728_channel_data *ch = &data->chdata[channel];
+	u8 outbuf[3];
+	int ret;
+
+	outbuf[0] = FIELD_PREP(MCP4728_CMD_MASK, MCP4728_MW_CMD);
+	outbuf[0] |= FIELD_PREP(MCP4728_CHSEL_MASK, channel);
+	outbuf[0] |= FIELD_PREP(MCP4728_UDAC_MASK, 0);
+
+	outbuf[1] = FIELD_PREP(MCP4728_VREF_MASK, ch->ref_mode);
+
+	if (data->powerdown)
+		outbuf[1] |= FIELD_PREP(MCP4728_PDMODE_MASK, ch->pd_mode + 1);
+
+	outbuf[1] |= FIELD_PREP(MCP4728_GAIN_MASK, ch->g_mode);
+	outbuf[1] |= FIELD_PREP(MCP4728_DAC_H_MASK, ch->dac_value >> 8);
+	outbuf[2] = FIELD_PREP(MCP4728_DAC_L_MASK, ch->dac_value);
+
+	ret = i2c_master_send(data->client, outbuf, 3);
+	if (ret < 0)
+		return ret;
+	else if (ret != 3)
+		return -EIO;
+
+	return 0;
+}
+
+static const char *const mcp4728_powerdown_modes[] = { "1kohm_to_gnd",
+						       "100kohm_to_gnd",
+						       "500kohm_to_gnd" };
+
+static int mcp4728_get_powerdown_mode(struct iio_dev *indio_dev,
+				      const struct iio_chan_spec *chan)
+{
+	struct mcp4728_data *data = iio_priv(indio_dev);
+
+	return data->chdata[chan->channel].pd_mode;
+}
+
+static int mcp4728_set_powerdown_mode(struct iio_dev *indio_dev,
+				      const struct iio_chan_spec *chan,
+				      unsigned int mode)
+{
+	struct mcp4728_data *data = iio_priv(indio_dev);
+
+	data->chdata[chan->channel].pd_mode = mode;
+
+	return 0;
+}
+
+static ssize_t mcp4728_read_powerdown(struct iio_dev *indio_dev,
+				      uintptr_t private,
+				      const struct iio_chan_spec *chan,
+				      char *buf)
+{
+	struct mcp4728_data *data = iio_priv(indio_dev);
+
+	return sysfs_emit(buf, "%d\n", data->powerdown);
+}
+
+static ssize_t mcp4728_write_powerdown(struct iio_dev *indio_dev,
+				       uintptr_t private,
+				       const struct iio_chan_spec *chan,
+				       const char *buf, size_t len)
+{
+	struct mcp4728_data *data = iio_priv(indio_dev);
+	bool state;
+	int ret;
+
+	ret = kstrtobool(buf, &state);
+	if (ret)
+		return ret;
+
+	if (state)
+		ret = mcp4728_suspend(&data->client->dev);
+	else
+		ret = mcp4728_resume(&data->client->dev);
+
+	if (ret < 0)
+		return ret;
+
+	return len;
+}
+
+static const struct iio_enum mcp4728_powerdown_mode_enum = {
+	.items	   = mcp4728_powerdown_modes,
+	.num_items = ARRAY_SIZE(mcp4728_powerdown_modes),
+	.get	   = mcp4728_get_powerdown_mode,
+	.set	   = mcp4728_set_powerdown_mode,
+};
+
+static const struct iio_chan_spec_ext_info mcp4728_ext_info[] = {
+	{
+		.name	= "powerdown",
+		.read	= mcp4728_read_powerdown,
+		.write	= mcp4728_write_powerdown,
+		.shared = IIO_SEPARATE,
+	},
+	IIO_ENUM("powerdown_mode", IIO_SEPARATE, &mcp4728_powerdown_mode_enum),
+	IIO_ENUM_AVAILABLE("powerdown_mode", IIO_SHARED_BY_TYPE,
+			   &mcp4728_powerdown_mode_enum),
+	{},
+};
+
+static const struct iio_chan_spec mcp4728_channels[MCP4728_N_CHANNELS] = {
+	MCP4728_CHAN(0),
+	MCP4728_CHAN(1),
+	MCP4728_CHAN(2),
+	MCP4728_CHAN(3),
+};
+
+static void mcp4728_get_scale_avail(enum mcp4728_scale scale,
+				    struct mcp4728_data *data, int *val,
+				    int *val2)
+{
+	*val  = data->scales_avail[scale * 2];
+	*val2 = data->scales_avail[scale * 2 + 1];
+}
+
+static void mcp4728_get_scale(int channel, struct mcp4728_data *data, int *val,
+			      int *val2)
+{
+	int ref_mode = data->chdata[channel].ref_mode;
+	int g_mode   = data->chdata[channel].g_mode;
+
+	if (ref_mode == MCP4728_VREF_EXTERNAL_VDD) {
+		mcp4728_get_scale_avail(MCP4728_SCALE_VDD, data, val, val2);
+	} else {
+		if (g_mode == MCP4728_GAIN_X1) {
+			mcp4728_get_scale_avail(MCP4728_SCALE_VINT_NO_GAIN,
+						data, val, val2);
+		} else {
+			mcp4728_get_scale_avail(MCP4728_SCALE_VINT_GAIN_X2,
+						data, val, val2);
+		}
+	}
+}
+
+static int mcp4728_find_matching_scale(struct mcp4728_data *data, int val,
+				       int val2)
+{
+	for (int i = 0; i < MCP4728_N_SCALES; i++) {
+		if (data->scales_avail[i * 2] == val &&
+		    data->scales_avail[i * 2 + 1] == val2)
+			return i;
+	}
+	return -EINVAL;
+}
+
+static int mcp4728_set_scale(int channel, struct mcp4728_data *data, int val,
+			     int val2)
+{
+	int scale = mcp4728_find_matching_scale(data, val, val2);
+
+	if (scale < 0)
+		return scale;
+
+	switch (scale) {
+	case MCP4728_SCALE_VDD:
+		data->chdata[channel].ref_mode = MCP4728_VREF_EXTERNAL_VDD;
+		break;
+	case MCP4728_SCALE_VINT_NO_GAIN:
+		data->chdata[channel].ref_mode = MCP4728_VREF_INTERNAL_2048mV;
+		data->chdata[channel].g_mode   = MCP4728_GAIN_X1;
+		break;
+	case MCP4728_SCALE_VINT_GAIN_X2:
+		data->chdata[channel].ref_mode = MCP4728_VREF_INTERNAL_2048mV;
+		data->chdata[channel].g_mode   = MCP4728_GAIN_X2;
+		break;
+	default:
+		return -EINVAL;
+	}
+	return 0;
+}
+
+static int mcp4728_read_raw(struct iio_dev *indio_dev,
+			    struct iio_chan_spec const *chan, int *val,
+			    int *val2, long mask)
+{
+	struct mcp4728_data *data = iio_priv(indio_dev);
+
+	switch (mask) {
+	case IIO_CHAN_INFO_RAW:
+		*val = data->chdata[chan->channel].dac_value;
+		return IIO_VAL_INT;
+	case IIO_CHAN_INFO_SCALE:
+		mcp4728_get_scale(chan->channel, data, val, val2);
+		return IIO_VAL_INT_PLUS_MICRO;
+	}
+	return -EINVAL;
+}
+
+static int mcp4728_write_raw(struct iio_dev *indio_dev,
+			     struct iio_chan_spec const *chan, int val,
+			     int val2, long mask)
+{
+	struct mcp4728_data *data = iio_priv(indio_dev);
+	int ret;
+
+	switch (mask) {
+	case IIO_CHAN_INFO_RAW:
+		if (val < 0 || val > GENMASK(MCP4728_RESOLUTION - 1, 0))
+			return -EINVAL;
+		data->chdata[chan->channel].dac_value = val;
+		ret = mcp4728_program_channel_cfg(chan->channel, indio_dev);
+		break;
+	case IIO_CHAN_INFO_SCALE:
+		ret = mcp4728_set_scale(chan->channel, data, val, val2);
+		if (ret)
+			break;
+
+		ret = mcp4728_program_channel_cfg(chan->channel, indio_dev);
+		break;
+	default:
+		ret = -EINVAL;
+		break;
+	}
+
+	return ret;
+}
+
+static void mcp4728_init_scale_avail(enum mcp4728_scale scale, int vref_mv,
+				     struct mcp4728_data *data)
+{
+	s64 tmp;
+	int value_micro;
+	int value_int;
+
+	tmp	  = (s64)vref_mv * 1000000LL >> MCP4728_RESOLUTION;
+	value_int = div_s64_rem(tmp, 1000000LL, &value_micro);
+
+	data->scales_avail[scale * 2]	  = value_int;
+	data->scales_avail[scale * 2 + 1] = value_micro;
+}
+
+static int mcp4728_init_scales_avail(struct mcp4728_data *data)
+{
+	int ret;
+
+	ret = regulator_get_voltage(data->vdd_reg);
+	if (ret < 0)
+		return ret;
+
+	mcp4728_init_scale_avail(MCP4728_SCALE_VDD, ret / 1000, data);
+	mcp4728_init_scale_avail(MCP4728_SCALE_VINT_NO_GAIN, 2048, data);
+	mcp4728_init_scale_avail(MCP4728_SCALE_VINT_GAIN_X2, 4096, data);
+
+	return 0;
+}
+
+static int mcp4728_read_avail(struct iio_dev *indio_dev,
+			      struct iio_chan_spec const *chan,
+			      const int **vals, int *type, int *length,
+			      long info)
+{
+	struct mcp4728_data *data = iio_priv(indio_dev);
+
+	switch (info) {
+	case IIO_CHAN_INFO_SCALE:
+		*type = IIO_VAL_INT_PLUS_MICRO;
+
+		switch (chan->type) {
+		case IIO_VOLTAGE:
+			*vals	= data->scales_avail;
+			*length = MCP4728_N_SCALES * 2;
+			return IIO_AVAIL_LIST;
+		default:
+			return -EINVAL;
+		}
+	default:
+		return -EINVAL;
+	}
+}
+
+static const struct iio_info mcp4728_info = {
+	.read_raw   = mcp4728_read_raw,
+	.write_raw  = mcp4728_write_raw,
+	.read_avail = &mcp4728_read_avail,
+	.attrs	    = &mcp4728_attribute_group,
+};
+
+static int mcp4728_suspend(struct device *dev)
+{
+	struct iio_dev *indio_dev = dev_get_drvdata(dev);
+	struct mcp4728_data *data = iio_priv(indio_dev);
+	unsigned int i;
+
+	data->powerdown = true;
+
+	for (i = 0; i < MCP4728_N_CHANNELS; i++) {
+		int err = mcp4728_program_channel_cfg(i, indio_dev);
+
+		if (err)
+			return err;
+	}
+	return 0;
+}
+
+static int mcp4728_resume(struct device *dev)
+{
+	struct iio_dev *indio_dev = dev_get_drvdata(dev);
+	struct mcp4728_data *data = iio_priv(indio_dev);
+	int err			  = 0;
+	unsigned int i;
+
+	data->powerdown = false;
+
+	for (i = 0; i < MCP4728_N_CHANNELS; i++) {
+		int ret = mcp4728_program_channel_cfg(i, indio_dev);
+
+		if (ret)
+			err = ret;
+	}
+	return err;
+}
+
+static DEFINE_SIMPLE_DEV_PM_OPS(mcp4728_pm_ops, mcp4728_suspend,
+				mcp4728_resume);
+
+static int mcp4728_init_channels_data(struct mcp4728_data *data)
+{
+	u8 inbuf[MCP4728_READ_RESPONSE_LEN];
+	int ret;
+	unsigned int i;
+
+	ret = i2c_master_recv(data->client, inbuf, MCP4728_READ_RESPONSE_LEN);
+	if (ret < 0) {
+		dev_err(&data->client->dev,
+			"failed to read mcp4728 conf. Err=%d\n", ret);
+		return ret;
+	} else if (ret != MCP4728_READ_RESPONSE_LEN) {
+		dev_err(&data->client->dev,
+			"failed to read mcp4728 conf. Wrong Response Len ret=%d\n",
+			ret);
+		return -EIO;
+	}
+
+	for (i = 0; i < MCP4728_N_CHANNELS; i++) {
+		struct mcp4728_channel_data *ch = &data->chdata[i];
+		u8 r2				= inbuf[i * 6 + 1];
+		u8 r3				= inbuf[i * 6 + 2];
+
+		ch->dac_value = FIELD_GET(MCP4728_DAC_H_MASK, r2) << 8 |
+				FIELD_GET(MCP4728_DAC_L_MASK, r3);
+		ch->ref_mode = FIELD_GET(MCP4728_VREF_MASK, r2);
+		ch->pd_mode  = FIELD_GET(MCP4728_PDMODE_MASK, r2);
+		ch->g_mode   = FIELD_GET(MCP4728_GAIN_MASK, r2);
+	}
+
+	return 0;
+}
+
+static void mcp4728_reg_disable(void *reg)
+{
+	regulator_disable(reg);
+}
+
+static int mcp4728_probe(struct i2c_client *client)
+{
+	const struct i2c_device_id *id = i2c_client_get_device_id(client);
+	struct mcp4728_data *data;
+	struct iio_dev *indio_dev;
+	int err;
+
+	indio_dev = devm_iio_device_alloc(&client->dev, sizeof(*data));
+	if (!indio_dev)
+		return -ENOMEM;
+
+	data = iio_priv(indio_dev);
+	i2c_set_clientdata(client, indio_dev);
+	data->client = client;
+
+	data->vdd_reg = devm_regulator_get(&client->dev, "vdd");
+	if (IS_ERR(data->vdd_reg))
+		return PTR_ERR(data->vdd_reg);
+
+	err = regulator_enable(data->vdd_reg);
+	if (err)
+		return err;
+
+	err = devm_add_action_or_reset(&client->dev, mcp4728_reg_disable,
+				       data->vdd_reg);
+	if (err)
+		return err;
+
+	/* MCP4728 has internal EEPROM that save each channel boot configuration.
+	 * It means that device configuration is unknown to the driver at kernel boot.
+	 * mcp4728_init_channels_data reads back DAC settings and stores them in data
+	 * structure.
+	 */
+	err = mcp4728_init_channels_data(data);
+	if (err) {
+		dev_err(&client->dev,
+			"failed to read mcp4728 current configuration\n");
+		return err;
+	}
+
+	err = mcp4728_init_scales_avail(data);
+	if (err) {
+		dev_err(&client->dev, "failed to init scales\n");
+		return err;
+	}
+
+	indio_dev->name		= id->name;
+	indio_dev->info		= &mcp4728_info;
+	indio_dev->channels	= mcp4728_channels;
+	indio_dev->num_channels = MCP4728_N_CHANNELS;
+	indio_dev->modes	= INDIO_DIRECT_MODE;
+
+	return devm_iio_device_register(&client->dev, indio_dev);
+}
+
+static const struct i2c_device_id mcp4728_id[] = {
+	{ "mcp4728", 0 },
+	{}
+};
+MODULE_DEVICE_TABLE(i2c, mcp4728_id);
+
+static const struct of_device_id mcp4728_of_match[] = {
+	{ .compatible = "microchip,mcp4728" },
+	{}
+};
+MODULE_DEVICE_TABLE(of, mcp4728_of_match);
+
+static struct i2c_driver mcp4728_driver = {
+	.driver = {
+		.name = "mcp4728",
+		.of_match_table = mcp4728_of_match,
+		.pm = pm_sleep_ptr(&mcp4728_pm_ops),
+	},
+	.probe = mcp4728_probe,
+	.id_table = mcp4728_id,
+};
+module_i2c_driver(mcp4728_driver);
+
+MODULE_AUTHOR("Andrea Collamati <andrea.collamati@gmail.com>");
+MODULE_DESCRIPTION("MCP4728 12-bit DAC");
+MODULE_LICENSE("GPL");