forked from Minki/linux
efd597b283
The power-down mask of the ad5504 is actually a power-up mask. Meaning if
a bit is set the corresponding channel is powered up and if it is not set
the channel is powered down.
The driver currently has this the wrong way around, resulting in the
channel being powered up when requested to be powered down and vice versa.
Fixes: 3bbbf150ff
("staging:iio:dac:ad5504: Use strtobool for boolean values")
Signed-off-by: Lars-Peter Clausen <lars@metafoo.de>
Acked-by: Alexandru Ardelean <alexandru.ardelean@analog.com>
Link: https://lore.kernel.org/r/20201209104649.5794-1-lars@metafoo.de
Cc: <Stable@vger.kernel.org>
Signed-off-by: Jonathan Cameron <Jonathan.Cameron@huawei.com>
372 lines
8.5 KiB
C
372 lines
8.5 KiB
C
// SPDX-License-Identifier: GPL-2.0-only
|
|
/*
|
|
* AD5504, AD5501 High Voltage Digital to Analog Converter
|
|
*
|
|
* Copyright 2011 Analog Devices Inc.
|
|
*/
|
|
|
|
#include <linux/interrupt.h>
|
|
#include <linux/fs.h>
|
|
#include <linux/device.h>
|
|
#include <linux/kernel.h>
|
|
#include <linux/spi/spi.h>
|
|
#include <linux/slab.h>
|
|
#include <linux/sysfs.h>
|
|
#include <linux/regulator/consumer.h>
|
|
#include <linux/module.h>
|
|
#include <linux/bitops.h>
|
|
|
|
#include <linux/iio/iio.h>
|
|
#include <linux/iio/sysfs.h>
|
|
#include <linux/iio/events.h>
|
|
#include <linux/iio/dac/ad5504.h>
|
|
|
|
#define AD5504_RES_MASK GENMASK(11, 0)
|
|
#define AD5504_CMD_READ BIT(15)
|
|
#define AD5504_CMD_WRITE 0
|
|
#define AD5504_ADDR(addr) ((addr) << 12)
|
|
|
|
/* Registers */
|
|
#define AD5504_ADDR_NOOP 0
|
|
#define AD5504_ADDR_DAC(x) ((x) + 1)
|
|
#define AD5504_ADDR_ALL_DAC 5
|
|
#define AD5504_ADDR_CTRL 7
|
|
|
|
/* Control Register */
|
|
#define AD5504_DAC_PWR(ch) ((ch) << 2)
|
|
#define AD5504_DAC_PWRDWN_MODE(mode) ((mode) << 6)
|
|
#define AD5504_DAC_PWRDN_20K 0
|
|
#define AD5504_DAC_PWRDN_3STATE 1
|
|
|
|
/**
|
|
* struct ad5446_state - driver instance specific data
|
|
* @spi: spi_device
|
|
* @reg: supply regulator
|
|
* @vref_mv: actual reference voltage used
|
|
* @pwr_down_mask: power down mask
|
|
* @pwr_down_mode: current power down mode
|
|
* @data: transfer buffer
|
|
*/
|
|
struct ad5504_state {
|
|
struct spi_device *spi;
|
|
struct regulator *reg;
|
|
unsigned short vref_mv;
|
|
unsigned pwr_down_mask;
|
|
unsigned pwr_down_mode;
|
|
|
|
__be16 data[2] ____cacheline_aligned;
|
|
};
|
|
|
|
/*
|
|
* ad5504_supported_device_ids:
|
|
*/
|
|
enum ad5504_supported_device_ids {
|
|
ID_AD5504,
|
|
ID_AD5501,
|
|
};
|
|
|
|
static int ad5504_spi_write(struct ad5504_state *st, u8 addr, u16 val)
|
|
{
|
|
st->data[0] = cpu_to_be16(AD5504_CMD_WRITE | AD5504_ADDR(addr) |
|
|
(val & AD5504_RES_MASK));
|
|
|
|
return spi_write(st->spi, &st->data[0], 2);
|
|
}
|
|
|
|
static int ad5504_spi_read(struct ad5504_state *st, u8 addr)
|
|
{
|
|
int ret;
|
|
struct spi_transfer t = {
|
|
.tx_buf = &st->data[0],
|
|
.rx_buf = &st->data[1],
|
|
.len = 2,
|
|
};
|
|
|
|
st->data[0] = cpu_to_be16(AD5504_CMD_READ | AD5504_ADDR(addr));
|
|
ret = spi_sync_transfer(st->spi, &t, 1);
|
|
if (ret < 0)
|
|
return ret;
|
|
|
|
return be16_to_cpu(st->data[1]) & AD5504_RES_MASK;
|
|
}
|
|
|
|
static int ad5504_read_raw(struct iio_dev *indio_dev,
|
|
struct iio_chan_spec const *chan,
|
|
int *val,
|
|
int *val2,
|
|
long m)
|
|
{
|
|
struct ad5504_state *st = iio_priv(indio_dev);
|
|
int ret;
|
|
|
|
switch (m) {
|
|
case IIO_CHAN_INFO_RAW:
|
|
ret = ad5504_spi_read(st, chan->address);
|
|
if (ret < 0)
|
|
return ret;
|
|
|
|
*val = ret;
|
|
|
|
return IIO_VAL_INT;
|
|
case IIO_CHAN_INFO_SCALE:
|
|
*val = st->vref_mv;
|
|
*val2 = chan->scan_type.realbits;
|
|
return IIO_VAL_FRACTIONAL_LOG2;
|
|
}
|
|
return -EINVAL;
|
|
}
|
|
|
|
static int ad5504_write_raw(struct iio_dev *indio_dev,
|
|
struct iio_chan_spec const *chan,
|
|
int val,
|
|
int val2,
|
|
long mask)
|
|
{
|
|
struct ad5504_state *st = iio_priv(indio_dev);
|
|
|
|
switch (mask) {
|
|
case IIO_CHAN_INFO_RAW:
|
|
if (val >= (1 << chan->scan_type.realbits) || val < 0)
|
|
return -EINVAL;
|
|
|
|
return ad5504_spi_write(st, chan->address, val);
|
|
default:
|
|
return -EINVAL;
|
|
}
|
|
}
|
|
|
|
static const char * const ad5504_powerdown_modes[] = {
|
|
"20kohm_to_gnd",
|
|
"three_state",
|
|
};
|
|
|
|
static int ad5504_get_powerdown_mode(struct iio_dev *indio_dev,
|
|
const struct iio_chan_spec *chan)
|
|
{
|
|
struct ad5504_state *st = iio_priv(indio_dev);
|
|
|
|
return st->pwr_down_mode;
|
|
}
|
|
|
|
static int ad5504_set_powerdown_mode(struct iio_dev *indio_dev,
|
|
const struct iio_chan_spec *chan, unsigned int mode)
|
|
{
|
|
struct ad5504_state *st = iio_priv(indio_dev);
|
|
|
|
st->pwr_down_mode = mode;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static const struct iio_enum ad5504_powerdown_mode_enum = {
|
|
.items = ad5504_powerdown_modes,
|
|
.num_items = ARRAY_SIZE(ad5504_powerdown_modes),
|
|
.get = ad5504_get_powerdown_mode,
|
|
.set = ad5504_set_powerdown_mode,
|
|
};
|
|
|
|
static ssize_t ad5504_read_dac_powerdown(struct iio_dev *indio_dev,
|
|
uintptr_t private, const struct iio_chan_spec *chan, char *buf)
|
|
{
|
|
struct ad5504_state *st = iio_priv(indio_dev);
|
|
|
|
return sprintf(buf, "%d\n",
|
|
!(st->pwr_down_mask & (1 << chan->channel)));
|
|
}
|
|
|
|
static ssize_t ad5504_write_dac_powerdown(struct iio_dev *indio_dev,
|
|
uintptr_t private, const struct iio_chan_spec *chan, const char *buf,
|
|
size_t len)
|
|
{
|
|
bool pwr_down;
|
|
int ret;
|
|
struct ad5504_state *st = iio_priv(indio_dev);
|
|
|
|
ret = strtobool(buf, &pwr_down);
|
|
if (ret)
|
|
return ret;
|
|
|
|
if (pwr_down)
|
|
st->pwr_down_mask &= ~(1 << chan->channel);
|
|
else
|
|
st->pwr_down_mask |= (1 << chan->channel);
|
|
|
|
ret = ad5504_spi_write(st, AD5504_ADDR_CTRL,
|
|
AD5504_DAC_PWRDWN_MODE(st->pwr_down_mode) |
|
|
AD5504_DAC_PWR(st->pwr_down_mask));
|
|
|
|
/* writes to the CTRL register must be followed by a NOOP */
|
|
ad5504_spi_write(st, AD5504_ADDR_NOOP, 0);
|
|
|
|
return ret ? ret : len;
|
|
}
|
|
|
|
static IIO_CONST_ATTR(temp0_thresh_rising_value, "110000");
|
|
static IIO_CONST_ATTR(temp0_thresh_rising_en, "1");
|
|
|
|
static struct attribute *ad5504_ev_attributes[] = {
|
|
&iio_const_attr_temp0_thresh_rising_value.dev_attr.attr,
|
|
&iio_const_attr_temp0_thresh_rising_en.dev_attr.attr,
|
|
NULL,
|
|
};
|
|
|
|
static const struct attribute_group ad5504_ev_attribute_group = {
|
|
.attrs = ad5504_ev_attributes,
|
|
};
|
|
|
|
static irqreturn_t ad5504_event_handler(int irq, void *private)
|
|
{
|
|
iio_push_event(private,
|
|
IIO_UNMOD_EVENT_CODE(IIO_TEMP,
|
|
0,
|
|
IIO_EV_TYPE_THRESH,
|
|
IIO_EV_DIR_RISING),
|
|
iio_get_time_ns(private));
|
|
|
|
return IRQ_HANDLED;
|
|
}
|
|
|
|
static const struct iio_info ad5504_info = {
|
|
.write_raw = ad5504_write_raw,
|
|
.read_raw = ad5504_read_raw,
|
|
.event_attrs = &ad5504_ev_attribute_group,
|
|
};
|
|
|
|
static const struct iio_chan_spec_ext_info ad5504_ext_info[] = {
|
|
{
|
|
.name = "powerdown",
|
|
.read = ad5504_read_dac_powerdown,
|
|
.write = ad5504_write_dac_powerdown,
|
|
.shared = IIO_SEPARATE,
|
|
},
|
|
IIO_ENUM("powerdown_mode", IIO_SHARED_BY_TYPE,
|
|
&ad5504_powerdown_mode_enum),
|
|
IIO_ENUM_AVAILABLE("powerdown_mode", &ad5504_powerdown_mode_enum),
|
|
{ },
|
|
};
|
|
|
|
#define AD5504_CHANNEL(_chan) { \
|
|
.type = IIO_VOLTAGE, \
|
|
.indexed = 1, \
|
|
.output = 1, \
|
|
.channel = (_chan), \
|
|
.info_mask_separate = BIT(IIO_CHAN_INFO_RAW), \
|
|
.info_mask_shared_by_type = BIT(IIO_CHAN_INFO_SCALE), \
|
|
.address = AD5504_ADDR_DAC(_chan), \
|
|
.scan_type = { \
|
|
.sign = 'u', \
|
|
.realbits = 12, \
|
|
.storagebits = 16, \
|
|
}, \
|
|
.ext_info = ad5504_ext_info, \
|
|
}
|
|
|
|
static const struct iio_chan_spec ad5504_channels[] = {
|
|
AD5504_CHANNEL(0),
|
|
AD5504_CHANNEL(1),
|
|
AD5504_CHANNEL(2),
|
|
AD5504_CHANNEL(3),
|
|
};
|
|
|
|
static int ad5504_probe(struct spi_device *spi)
|
|
{
|
|
struct ad5504_platform_data *pdata = spi->dev.platform_data;
|
|
struct iio_dev *indio_dev;
|
|
struct ad5504_state *st;
|
|
struct regulator *reg;
|
|
int ret, voltage_uv = 0;
|
|
|
|
indio_dev = devm_iio_device_alloc(&spi->dev, sizeof(*st));
|
|
if (!indio_dev)
|
|
return -ENOMEM;
|
|
reg = devm_regulator_get(&spi->dev, "vcc");
|
|
if (!IS_ERR(reg)) {
|
|
ret = regulator_enable(reg);
|
|
if (ret)
|
|
return ret;
|
|
|
|
ret = regulator_get_voltage(reg);
|
|
if (ret < 0)
|
|
goto error_disable_reg;
|
|
|
|
voltage_uv = ret;
|
|
}
|
|
|
|
spi_set_drvdata(spi, indio_dev);
|
|
st = iio_priv(indio_dev);
|
|
if (voltage_uv)
|
|
st->vref_mv = voltage_uv / 1000;
|
|
else if (pdata)
|
|
st->vref_mv = pdata->vref_mv;
|
|
else
|
|
dev_warn(&spi->dev, "reference voltage unspecified\n");
|
|
|
|
st->reg = reg;
|
|
st->spi = spi;
|
|
indio_dev->name = spi_get_device_id(st->spi)->name;
|
|
indio_dev->info = &ad5504_info;
|
|
if (spi_get_device_id(st->spi)->driver_data == ID_AD5501)
|
|
indio_dev->num_channels = 1;
|
|
else
|
|
indio_dev->num_channels = 4;
|
|
indio_dev->channels = ad5504_channels;
|
|
indio_dev->modes = INDIO_DIRECT_MODE;
|
|
|
|
if (spi->irq) {
|
|
ret = devm_request_threaded_irq(&spi->dev, spi->irq,
|
|
NULL,
|
|
&ad5504_event_handler,
|
|
IRQF_TRIGGER_FALLING | IRQF_ONESHOT,
|
|
spi_get_device_id(st->spi)->name,
|
|
indio_dev);
|
|
if (ret)
|
|
goto error_disable_reg;
|
|
}
|
|
|
|
ret = iio_device_register(indio_dev);
|
|
if (ret)
|
|
goto error_disable_reg;
|
|
|
|
return 0;
|
|
|
|
error_disable_reg:
|
|
if (!IS_ERR(reg))
|
|
regulator_disable(reg);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int ad5504_remove(struct spi_device *spi)
|
|
{
|
|
struct iio_dev *indio_dev = spi_get_drvdata(spi);
|
|
struct ad5504_state *st = iio_priv(indio_dev);
|
|
|
|
iio_device_unregister(indio_dev);
|
|
|
|
if (!IS_ERR(st->reg))
|
|
regulator_disable(st->reg);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static const struct spi_device_id ad5504_id[] = {
|
|
{"ad5504", ID_AD5504},
|
|
{"ad5501", ID_AD5501},
|
|
{}
|
|
};
|
|
MODULE_DEVICE_TABLE(spi, ad5504_id);
|
|
|
|
static struct spi_driver ad5504_driver = {
|
|
.driver = {
|
|
.name = "ad5504",
|
|
},
|
|
.probe = ad5504_probe,
|
|
.remove = ad5504_remove,
|
|
.id_table = ad5504_id,
|
|
};
|
|
module_spi_driver(ad5504_driver);
|
|
|
|
MODULE_AUTHOR("Michael Hennerich <michael.hennerich@analog.com>");
|
|
MODULE_DESCRIPTION("Analog Devices AD5501/AD5501 DAC");
|
|
MODULE_LICENSE("GPL v2");
|