diff options
author | Ricardo Ribalda Delgado <ricardo.ribalda@gmail.com> | 2016-01-16 14:24:07 +0100 |
---|---|---|
committer | Jonathan Cameron <jic23@kernel.org> | 2016-01-24 14:46:21 +0000 |
commit | 131497acd88a4456d99247cee457baefd2817835 (patch) | |
tree | 5272e44155727b8d3bf1ecc7380cc44bfac1da1c /drivers/iio/dac/ad5761.c | |
parent | 7ca6574a7afb669b0b4b30dd63adeb310e97e250 (diff) |
iio: add ad5761 DAC driver
ad5761 is a 1-channel DAC with configurable output range.
The driver uses the regulator interface for its voltage ref.
It shares its register layout with ad5761r, ad5721 and ad5721r.
Differences:
ad5761* are 16 bit, ad5721* are 12 bits.
ad57*1r have an internal reference.
Signed-off-by: Ricardo Ribalda Delgado <ricardo.ribalda@gmail.com>
Signed-off-by: Jonathan Cameron <jic23@kernel.org>
Diffstat (limited to 'drivers/iio/dac/ad5761.c')
-rw-r--r-- | drivers/iio/dac/ad5761.c | 430 |
1 files changed, 430 insertions, 0 deletions
diff --git a/drivers/iio/dac/ad5761.c b/drivers/iio/dac/ad5761.c new file mode 100644 index 000000000000..d6510d6928b3 --- /dev/null +++ b/drivers/iio/dac/ad5761.c @@ -0,0 +1,430 @@ +/* + * AD5721, AD5721R, AD5761, AD5761R, Voltage Output Digital to Analog Converter + * + * Copyright 2016 Qtechnology A/S + * 2016 Ricardo Ribalda <ricardo.ribalda@gmail.com> + * + * Licensed under the GPL-2. + */ +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/spi/spi.h> +#include <linux/bitops.h> +#include <linux/iio/iio.h> +#include <linux/iio/sysfs.h> +#include <linux/regulator/consumer.h> +#include <linux/platform_data/ad5761.h> + +#define AD5761_ADDR(addr) ((addr & 0xf) << 16) +#define AD5761_ADDR_NOOP 0x0 +#define AD5761_ADDR_DAC_WRITE 0x3 +#define AD5761_ADDR_CTRL_WRITE_REG 0x4 +#define AD5761_ADDR_SW_DATA_RESET 0x7 +#define AD5761_ADDR_DAC_READ 0xb +#define AD5761_ADDR_CTRL_READ_REG 0xc +#define AD5761_ADDR_SW_FULL_RESET 0xf + +#define AD5761_CTRL_USE_INTVREF BIT(5) +#define AD5761_CTRL_ETS BIT(6) + +/** + * struct ad5761_chip_info - chip specific information + * @int_vref: Value of the internal reference voltage in mV - 0 if external + * reference voltage is used + * @channel: channel specification +*/ + +struct ad5761_chip_info { + unsigned long int_vref; + const struct iio_chan_spec channel; +}; + +struct ad5761_range_params { + int m; + int c; +}; + +enum ad5761_supported_device_ids { + ID_AD5721, + ID_AD5721R, + ID_AD5761, + ID_AD5761R, +}; + +/** + * struct ad5761_state - driver instance specific data + * @spi: spi_device + * @vref_reg: reference voltage regulator + * @use_intref: true when the internal voltage reference is used + * @vref: actual voltage reference in mVolts + * @range: output range mode used + * @data: cache aligned spi buffer + */ +struct ad5761_state { + struct spi_device *spi; + struct regulator *vref_reg; + + bool use_intref; + int vref; + enum ad5761_voltage_range range; + + /* + * DMA (thus cache coherency maintenance) requires the + * transfer buffers to live in their own cache lines. + */ + union { + __be32 d32; + u8 d8[4]; + } data[3] ____cacheline_aligned; +}; + +static const struct ad5761_range_params ad5761_range_params[] = { + [AD5761_VOLTAGE_RANGE_M10V_10V] = { + .m = 80, + .c = 40, + }, + [AD5761_VOLTAGE_RANGE_0V_10V] = { + .m = 40, + .c = 0, + }, + [AD5761_VOLTAGE_RANGE_M5V_5V] = { + .m = 40, + .c = 20, + }, + [AD5761_VOLTAGE_RANGE_0V_5V] = { + .m = 20, + .c = 0, + }, + [AD5761_VOLTAGE_RANGE_M2V5_7V5] = { + .m = 40, + .c = 10, + }, + [AD5761_VOLTAGE_RANGE_M3V_3V] = { + .m = 24, + .c = 12, + }, + [AD5761_VOLTAGE_RANGE_0V_16V] = { + .m = 64, + .c = 0, + }, + [AD5761_VOLTAGE_RANGE_0V_20V] = { + .m = 80, + .c = 0, + }, +}; + +static int _ad5761_spi_write(struct ad5761_state *st, u8 addr, u16 val) +{ + st->data[0].d32 = cpu_to_be32(AD5761_ADDR(addr) | val); + + return spi_write(st->spi, &st->data[0].d8[1], 3); +} + +static int ad5761_spi_write(struct iio_dev *indio_dev, u8 addr, u16 val) +{ + struct ad5761_state *st = iio_priv(indio_dev); + int ret; + + mutex_lock(&indio_dev->mlock); + ret = _ad5761_spi_write(st, addr, val); + mutex_unlock(&indio_dev->mlock); + + return ret; +} + +static int _ad5761_spi_read(struct ad5761_state *st, u8 addr, u16 *val) +{ + int ret; + struct spi_transfer xfers[] = { + { + .tx_buf = &st->data[0].d8[1], + .bits_per_word = 8, + .len = 3, + .cs_change = true, + }, { + .tx_buf = &st->data[1].d8[1], + .rx_buf = &st->data[2].d8[1], + .bits_per_word = 8, + .len = 3, + }, + }; + + st->data[0].d32 = cpu_to_be32(AD5761_ADDR(addr)); + st->data[1].d32 = cpu_to_be32(AD5761_ADDR(AD5761_ADDR_NOOP)); + + ret = spi_sync_transfer(st->spi, xfers, ARRAY_SIZE(xfers)); + + *val = be32_to_cpu(st->data[2].d32); + + return ret; +} + +static int ad5761_spi_read(struct iio_dev *indio_dev, u8 addr, u16 *val) +{ + struct ad5761_state *st = iio_priv(indio_dev); + int ret; + + mutex_lock(&indio_dev->mlock); + ret = _ad5761_spi_read(st, addr, val); + mutex_unlock(&indio_dev->mlock); + + return ret; +} + +static int ad5761_spi_set_range(struct ad5761_state *st, + enum ad5761_voltage_range range) +{ + u16 aux; + int ret; + + aux = (range & 0x7) | AD5761_CTRL_ETS; + + if (st->use_intref) + aux |= AD5761_CTRL_USE_INTVREF; + + ret = _ad5761_spi_write(st, AD5761_ADDR_SW_FULL_RESET, 0); + if (ret) + return ret; + + ret = _ad5761_spi_write(st, AD5761_ADDR_CTRL_WRITE_REG, aux); + if (ret) + return ret; + + st->range = range; + + return 0; +} + +static int ad5761_read_raw(struct iio_dev *indio_dev, + struct iio_chan_spec const *chan, + int *val, + int *val2, + long mask) +{ + struct ad5761_state *st; + int ret; + u16 aux; + + switch (mask) { + case IIO_CHAN_INFO_RAW: + ret = ad5761_spi_read(indio_dev, AD5761_ADDR_DAC_READ, &aux); + if (ret) + return ret; + *val = aux >> chan->scan_type.shift; + return IIO_VAL_INT; + case IIO_CHAN_INFO_SCALE: + st = iio_priv(indio_dev); + *val = st->vref * ad5761_range_params[st->range].m; + *val /= 10; + *val2 = chan->scan_type.realbits; + return IIO_VAL_FRACTIONAL_LOG2; + case IIO_CHAN_INFO_OFFSET: + st = iio_priv(indio_dev); + *val = -(1 << chan->scan_type.realbits); + *val *= ad5761_range_params[st->range].c; + *val /= ad5761_range_params[st->range].m; + return IIO_VAL_INT; + default: + return -EINVAL; + } +} + +static int ad5761_write_raw(struct iio_dev *indio_dev, + struct iio_chan_spec const *chan, + int val, + int val2, + long mask) +{ + u16 aux; + + if (mask != IIO_CHAN_INFO_RAW) + return -EINVAL; + + if (val2 || (val << chan->scan_type.shift) > 0xffff || val < 0) + return -EINVAL; + + aux = val << chan->scan_type.shift; + + return ad5761_spi_write(indio_dev, AD5761_ADDR_DAC_WRITE, aux); +} + +static const struct iio_info ad5761_info = { + .read_raw = &ad5761_read_raw, + .write_raw = &ad5761_write_raw, + .driver_module = THIS_MODULE, +}; + +#define AD5761_CHAN(_bits) { \ + .type = IIO_VOLTAGE, \ + .output = 1, \ + .info_mask_separate = BIT(IIO_CHAN_INFO_RAW), \ + .info_mask_shared_by_type = BIT(IIO_CHAN_INFO_SCALE) | \ + BIT(IIO_CHAN_INFO_OFFSET), \ + .scan_type = { \ + .sign = 'u', \ + .realbits = (_bits), \ + .storagebits = 16, \ + .shift = 16 - (_bits), \ + }, \ +} + +static const struct ad5761_chip_info ad5761_chip_infos[] = { + [ID_AD5721] = { + .int_vref = 0, + .channel = AD5761_CHAN(12), + }, + [ID_AD5721R] = { + .int_vref = 2500, + .channel = AD5761_CHAN(12), + }, + [ID_AD5761] = { + .int_vref = 0, + .channel = AD5761_CHAN(16), + }, + [ID_AD5761R] = { + .int_vref = 2500, + .channel = AD5761_CHAN(16), + }, +}; + +static int ad5761_get_vref(struct ad5761_state *st, + const struct ad5761_chip_info *chip_info) +{ + int ret; + + st->vref_reg = devm_regulator_get_optional(&st->spi->dev, "vref"); + if (PTR_ERR(st->vref_reg) == -ENODEV) { + /* Use Internal regulator */ + if (!chip_info->int_vref) { + dev_err(&st->spi->dev, + "Voltage reference not found\n"); + return -EIO; + } + + st->use_intref = true; + st->vref = chip_info->int_vref; + return 0; + } + + if (IS_ERR(st->vref_reg)) { + dev_err(&st->spi->dev, + "Error getting voltage reference regulator\n"); + return PTR_ERR(st->vref_reg); + } + + ret = regulator_enable(st->vref_reg); + if (ret) { + dev_err(&st->spi->dev, + "Failed to enable voltage reference\n"); + return ret; + } + + ret = regulator_get_voltage(st->vref_reg); + if (ret < 0) { + dev_err(&st->spi->dev, + "Failed to get voltage reference value\n"); + goto disable_regulator_vref; + } + + if (ret < 2000000 || ret > 3000000) { + dev_warn(&st->spi->dev, + "Invalid external voltage ref. value %d uV\n", ret); + ret = -EIO; + goto disable_regulator_vref; + } + + st->vref = ret / 1000; + st->use_intref = false; + + return 0; + +disable_regulator_vref: + regulator_disable(st->vref_reg); + st->vref_reg = NULL; + return ret; +} + +static int ad5761_probe(struct spi_device *spi) +{ + struct iio_dev *iio_dev; + struct ad5761_state *st; + int ret; + const struct ad5761_chip_info *chip_info = + &ad5761_chip_infos[spi_get_device_id(spi)->driver_data]; + enum ad5761_voltage_range voltage_range = AD5761_VOLTAGE_RANGE_0V_5V; + struct ad5761_platform_data *pdata = dev_get_platdata(&spi->dev); + + iio_dev = devm_iio_device_alloc(&spi->dev, sizeof(*st)); + if (!iio_dev) + return -ENOMEM; + + st = iio_priv(iio_dev); + + st->spi = spi; + spi_set_drvdata(spi, iio_dev); + + ret = ad5761_get_vref(st, chip_info); + if (ret) + return ret; + + if (pdata) + voltage_range = pdata->voltage_range; + + ret = ad5761_spi_set_range(st, voltage_range); + if (ret) + goto disable_regulator_err; + + iio_dev->dev.parent = &spi->dev; + iio_dev->info = &ad5761_info; + iio_dev->modes = INDIO_DIRECT_MODE; + iio_dev->channels = &chip_info->channel; + iio_dev->num_channels = 1; + iio_dev->name = spi_get_device_id(st->spi)->name; + ret = iio_device_register(iio_dev); + if (ret) + goto disable_regulator_err; + + return 0; + +disable_regulator_err: + if (!IS_ERR_OR_NULL(st->vref_reg)) + regulator_disable(st->vref_reg); + + return ret; +} + +static int ad5761_remove(struct spi_device *spi) +{ + struct iio_dev *iio_dev = spi_get_drvdata(spi); + struct ad5761_state *st = iio_priv(iio_dev); + + iio_device_unregister(iio_dev); + + if (!IS_ERR_OR_NULL(st->vref_reg)) + regulator_disable(st->vref_reg); + + return 0; +} + +static const struct spi_device_id ad5761_id[] = { + {"ad5721", ID_AD5721}, + {"ad5721r", ID_AD5721R}, + {"ad5761", ID_AD5761}, + {"ad5761r", ID_AD5761R}, + {} +}; +MODULE_DEVICE_TABLE(spi, ad5761_id); + +static struct spi_driver ad5761_driver = { + .driver = { + .name = "ad5761", + }, + .probe = ad5761_probe, + .remove = ad5761_remove, + .id_table = ad5761_id, +}; +module_spi_driver(ad5761_driver); + +MODULE_AUTHOR("Ricardo Ribalda <ricardo.ribalda@gmail.com>"); +MODULE_DESCRIPTION("Analog Devices AD5721, AD5721R, AD5761, AD5761R driver"); +MODULE_LICENSE("GPL v2"); |