From: Andrei Stancovici <andrei.stancovici@analog.com>
To: "Nuno Sá" <nuno.sa@analog.com>,
"Michael Hennerich" <Michael.Hennerich@analog.com>,
"Jonathan Cameron" <jic23@kernel.org>,
"David Lechner" <dlechner@baylibre.com>,
"Andy Shevchenko" <andy@kernel.org>,
"Rob Herring" <robh@kernel.org>,
"Krzysztof Kozlowski" <krzk+dt@kernel.org>,
"Conor Dooley" <conor+dt@kernel.org>,
"Liam Beguin" <liambeguin@gmail.com>,
linux@analog.com, linux-iio@vger.kernel.org,
devicetree@vger.kernel.org, linux-kernel@vger.kernel.org
Cc: Andrei Stancovici <andrei.stancovici@analog.com>
Subject: [PATCH 3/3] iio: adc: ltc2497: add 2x conversion speed mode
Date: Wed, 5 Aug 2026 12:41:29 +0300 [thread overview]
Message-ID: <20260805094135.11762-4-andrei.stancovici@analog.com> (raw)
In-Reply-To: <20260805094135.11762-1-andrei.stancovici@analog.com>
The LTC2499 supports a 2x output rate (SPD bit in the second
configuration byte). In 2x mode the offset auto-calibration is
disabled, roughly doubling the conversion rate (~13.6Hz vs ~6.8Hz in
simultaneous 50/60Hz rejection) while leaving linearity and full-scale
errors unchanged (datasheet). During a temperature measurement the part
always converts at 1x regardless of SPD.
Expose the rate through the standard sampling_frequency /
sampling_frequency_available ABI on the voltage channels only: SPD is
ignored for temperature conversions, so the temperature channel
deliberately carries no SAMP_FREQ attribute. A new has_speed_mode
capability flag gates the feature (LTC2499); the two-byte command path
is now taken for has_temp || has_speed_mode, since both features need the
second config byte.
The conversion-time wait becomes mode dependent: 150ms at 1x, 76ms at 2x
(datasheet t_CONV max, simultaneous rejection, rounded up). The wait is
keyed on the conversion currently in flight, whose duration is fixed by
the mode that was active when it started - not by the newly selected
mode. This matters on a 1x->2x switch: a 1x conversion may still be
running when the first 2x read arrives, and reprogramming the device
before it finishes would be NACKed with -EIO. Timing is centralized in
ltc2497core_conv_time_ms() so a future FA/FB rejection-mode selection
can extend it into a [rejection][speed] lookup without touching callers.
LTC2496/LTC2497 (no speed mode) keep the single-byte path and the
unchanged 150ms wait.
Validated on a live LTC2499: 20 reads take ~3.1s at 1x and ~1.6s at 2x
(~0.5x, no -EIO), voltage and temperature readings stay sane in both
modes, and the temperature/voltage interleave (sticky-PTAT) regression
still passes at 1x and 2x.
Signed-off-by: Andrei Stancovici <andrei.stancovici@analog.com>
---
drivers/iio/adc/ltc2497-core.c | 169 +++++++++++++++++++++++++++++++--
drivers/iio/adc/ltc2497.c | 24 +++--
drivers/iio/adc/ltc2497.h | 24 ++++-
3 files changed, 200 insertions(+), 17 deletions(-)
diff --git a/drivers/iio/adc/ltc2497-core.c b/drivers/iio/adc/ltc2497-core.c
index 4ca7271ca007..68b53a983e05 100644
--- a/drivers/iio/adc/ltc2497-core.c
+++ b/drivers/iio/adc/ltc2497-core.c
@@ -7,12 +7,14 @@
*/
#include <linux/delay.h>
+#include <linux/device.h>
#include <linux/iio/iio.h>
#include <linux/iio/driver.h>
#include <linux/math64.h>
#include <linux/module.h>
#include <linux/mutex.h>
#include <linux/regulator/consumer.h>
+#include <linux/slab.h>
#include "ltc2497.h"
@@ -20,24 +22,57 @@
#define LTC2497_DIFF 0
#define LTC2497_SIGN BIT(3)
-static int ltc2497core_wait_conv(struct ltc2497core_driverdata *ddata)
+/*
+ * Output-rate modes, indexed by ltc2497core_driverdata.sped_2x
+ * (0 = 1x, the power-on default; 1 = 2x, LTC2499 only). The advertised
+ * sampling_frequency and the conversion-time budget are two views of the same
+ * mode, so they are kept in lock-step here and can never drift apart. Only the
+ * two simultaneous 50/60Hz rejection rates are reachable today; adding FA/FB
+ * rejection selection later turns this into a [rejection][speed] lookup without
+ * changing any caller.
+ */
+static const int ltc2497core_samp_freq_avail[] = {
+ 6, 800000, /* 1x: ~6.8 Hz (1 / t_CONV_1 typ 146.9ms) */
+ 13, 600000, /* 2x: ~13.6 Hz (1 / t_CONV_2 typ 73.6ms) */
+};
+
+static const unsigned int ltc2497core_conv_time_ms_tbl[] = {
+ LTC2497_CONV_TIME_1X_MS, /* 1x */
+ LTC2499_CONV_TIME_2X_MS, /* 2x */
+};
+
+static unsigned int ltc2497core_conv_time_ms(struct ltc2497core_driverdata *ddata,
+ u8 address)
+{
+ /*
+ * SPD is ignored by the part during a temperature measurement: it
+ * always converts at 1x, so budget the 1x time regardless of the
+ * selected voltage-channel mode.
+ */
+ if (address == LTC2497_TEMP_ADDR)
+ return ltc2497core_conv_time_ms_tbl[0];
+
+ return ltc2497core_conv_time_ms_tbl[ddata->sped_2x];
+}
+
+static int ltc2497core_wait_conv(struct ltc2497core_driverdata *ddata,
+ unsigned int conv_time_ms)
{
s64 time_elapsed;
time_elapsed = ktime_ms_delta(ktime_get(), ddata->time_prev);
- if (time_elapsed < LTC2497_CONVERSION_TIME_MS) {
+ if (time_elapsed < conv_time_ms) {
/* delay if conversion time not passed
* since last read or write
*/
- if (msleep_interruptible(
- LTC2497_CONVERSION_TIME_MS - time_elapsed))
+ if (msleep_interruptible(conv_time_ms - time_elapsed))
return -ERESTARTSYS;
return 0;
}
- if (time_elapsed - LTC2497_CONVERSION_TIME_MS <= 0) {
+ if (time_elapsed - conv_time_ms <= 0) {
/* We're in automatic mode -
* so the last reading is still not outdated
*/
@@ -49,9 +84,18 @@ static int ltc2497core_wait_conv(struct ltc2497core_driverdata *ddata)
static int ltc2497core_read(struct ltc2497core_driverdata *ddata, u8 address, int *val)
{
+ unsigned int conv_time_ms = ltc2497core_conv_time_ms(ddata, address);
int ret;
- ret = ltc2497core_wait_conv(ddata);
+ /*
+ * Wait for the conversion currently in flight, whose duration was fixed
+ * by the mode active when it was started (ddata->conv_time_prev). This
+ * can be longer than the freshly selected mode's time - e.g. a 1x
+ * conversion is still running when the first 2x read arrives after a
+ * sampling_frequency change - and reprogramming the device before it
+ * finishes would be NACKed (-EIO).
+ */
+ ret = ltc2497core_wait_conv(ddata, ddata->conv_time_prev);
if (ret < 0)
return ret;
@@ -61,7 +105,9 @@ static int ltc2497core_read(struct ltc2497core_driverdata *ddata, u8 address, in
return ret;
ddata->addr_prev = address;
- if (msleep_interruptible(LTC2497_CONVERSION_TIME_MS))
+ /* The reprogram above starts a conversion in the new mode. */
+ ddata->conv_time_prev = conv_time_ms;
+ if (msleep_interruptible(conv_time_ms))
return -ERESTARTSYS;
}
@@ -70,6 +116,8 @@ static int ltc2497core_read(struct ltc2497core_driverdata *ddata, u8 address, in
return ret;
ddata->time_prev = ktime_get();
+ /* The read above auto-starts the next conversion in the current mode. */
+ ddata->conv_time_prev = conv_time_ms;
return ret;
}
@@ -134,6 +182,81 @@ static int ltc2497core_read_raw(struct iio_dev *indio_dev,
return -EINVAL;
}
+ case IIO_CHAN_INFO_SAMP_FREQ:
+ /*
+ * Only advertised on the voltage channels of parts with a speed
+ * mode; the sampling frequency is a property of the selected 1x/2x
+ * mode, not of an individual conversion.
+ */
+ mutex_lock(&ddata->lock);
+ *val = ltc2497core_samp_freq_avail[ddata->sped_2x * 2];
+ *val2 = ltc2497core_samp_freq_avail[ddata->sped_2x * 2 + 1];
+ mutex_unlock(&ddata->lock);
+
+ return IIO_VAL_INT_PLUS_MICRO;
+
+ default:
+ return -EINVAL;
+ }
+}
+
+static int ltc2497core_read_avail(struct iio_dev *indio_dev,
+ struct iio_chan_spec const *chan,
+ const int **vals, int *type, int *length,
+ long mask)
+{
+ switch (mask) {
+ case IIO_CHAN_INFO_SAMP_FREQ:
+ *vals = ltc2497core_samp_freq_avail;
+ *type = IIO_VAL_INT_PLUS_MICRO;
+ *length = ARRAY_SIZE(ltc2497core_samp_freq_avail);
+ return IIO_AVAIL_LIST;
+
+ default:
+ return -EINVAL;
+ }
+}
+
+static int ltc2497core_write_raw(struct iio_dev *indio_dev,
+ struct iio_chan_spec const *chan,
+ int val, int val2, long mask)
+{
+ struct ltc2497core_driverdata *ddata = iio_priv(indio_dev);
+ bool sped_2x;
+ int i;
+
+ switch (mask) {
+ case IIO_CHAN_INFO_SAMP_FREQ:
+ /* Match the (val, val2) pair against the advertised rates. */
+ for (i = 0; i < ARRAY_SIZE(ltc2497core_samp_freq_avail); i += 2) {
+ if (val == ltc2497core_samp_freq_avail[i] &&
+ val2 == ltc2497core_samp_freq_avail[i + 1])
+ break;
+ }
+ if (i >= ARRAY_SIZE(ltc2497core_samp_freq_avail))
+ return -EINVAL;
+
+ sped_2x = i / 2;
+
+ mutex_lock(&ddata->lock);
+ ddata->sped_2x = sped_2x;
+ /*
+ * The new speed only takes effect once the second command byte
+ * is reprogrammed, so force the next read to reprogram rather
+ * than reuse the value already latched for this address.
+ * LTC2497_CONFIG_DEFAULT is not a valid channel/temperature
+ * address, so it is a safe re-arm sentinel (as used at probe).
+ *
+ * A conversion started under the old speed may still be in
+ * flight; its own duration (conv_time_prev), not the new mode's,
+ * still gates the next reprogram, so the timing state is left
+ * untouched here.
+ */
+ ddata->addr_prev = LTC2497_CONFIG_DEFAULT;
+ mutex_unlock(&ddata->lock);
+
+ return 0;
+
default:
return -EINVAL;
}
@@ -206,6 +329,8 @@ static const struct iio_chan_spec ltc2497core_channel[] = {
static const struct iio_info ltc2497core_info = {
.read_raw = ltc2497core_read_raw,
+ .read_avail = ltc2497core_read_avail,
+ .write_raw = ltc2497core_write_raw,
};
int ltc2497core_probe(struct device *dev, struct iio_dev *indio_dev)
@@ -231,6 +356,34 @@ int ltc2497core_probe(struct device *dev, struct iio_dev *indio_dev)
if (!ddata->chip_info->has_temp)
indio_dev->num_channels--;
+ /*
+ * Parts with a speed mode expose in_voltage_sampling_frequency /
+ * _available on the voltage channels only. SPD is ignored during a
+ * temperature measurement, so the temperature channel deliberately
+ * carries no SAMP_FREQ attribute. Patch a private copy of the shared
+ * channel array so parts without a speed mode stay untouched.
+ */
+ if (ddata->chip_info->has_speed_mode) {
+ struct iio_chan_spec *channels;
+ unsigned int i;
+
+ channels = devm_kmemdup(dev, ltc2497core_channel,
+ sizeof(ltc2497core_channel), GFP_KERNEL);
+ if (!channels)
+ return -ENOMEM;
+
+ for (i = 0; i < indio_dev->num_channels; i++) {
+ if (channels[i].type != IIO_VOLTAGE)
+ continue;
+ channels[i].info_mask_shared_by_type |=
+ BIT(IIO_CHAN_INFO_SAMP_FREQ);
+ channels[i].info_mask_shared_by_type_available |=
+ BIT(IIO_CHAN_INFO_SAMP_FREQ);
+ }
+
+ indio_dev->channels = channels;
+ }
+
ret = ddata->result_and_measure(ddata, LTC2497_CONFIG_DEFAULT, NULL);
if (ret < 0)
return ret;
@@ -255,6 +408,8 @@ int ltc2497core_probe(struct device *dev, struct iio_dev *indio_dev)
ddata->addr_prev = LTC2497_CONFIG_DEFAULT;
ddata->time_prev = ktime_get();
+ /* Power-on default mode is 1x; a conversion is already in flight. */
+ ddata->conv_time_prev = LTC2497_CONV_TIME_1X_MS;
mutex_init(&ddata->lock);
diff --git a/drivers/iio/adc/ltc2497.c b/drivers/iio/adc/ltc2497.c
index 2ff53b292f74..3fc31d418cf4 100644
--- a/drivers/iio/adc/ltc2497.c
+++ b/drivers/iio/adc/ltc2497.c
@@ -85,16 +85,19 @@ static int ltc2497_result_and_measure(struct ltc2497core_driverdata *ddata,
}
/*
- * Parts with the internal PTAT sensor (LTC2499) latch their converter
- * configuration via a second command byte and only re-evaluate it when
- * that byte has EN2 set; a single byte, or a second byte with EN2 = 0,
- * means "keep previous". A one-byte channel select therefore cannot pull
- * the device back out of temperature mode, so a voltage read after a
- * temperature read would keep returning the PTAT result. Always drive the
- * second byte with EN2 set on these parts: IM | temperature-rejection for
- * a temperature read, EN2 alone (IM = 0) to (re)select an external input.
+ * Parts with a second config byte (LTC2499: internal PTAT sensor and/or
+ * the 2x speed mode) latch their converter configuration from that byte
+ * and only re-evaluate it when EN2 is set; a single byte, or a second
+ * byte with EN2 = 0, means "keep previous". A one-byte channel select
+ * therefore cannot pull the device back out of temperature mode, so a
+ * voltage read after a temperature read would keep returning the PTAT
+ * result. Always drive the second byte with EN2 set on these parts:
+ * - temperature read: IM = 1 (SPD is ignored by the part in
+ * temperature mode and is left 0 here);
+ * - voltage read: IM = 0 (external input), plus SPD when 2x is
+ * selected.
*/
- if (ddata->chip_info->has_temp) {
+ if (ddata->chip_info->has_temp || ddata->chip_info->has_speed_mode) {
u8 cmd[2];
if (address == LTC2497_TEMP_ADDR) {
@@ -103,6 +106,8 @@ static int ltc2497_result_and_measure(struct ltc2497core_driverdata *ddata,
} else {
cmd[0] = LTC2497_ENABLE | address;
cmd[1] = LTC2499_EN2;
+ if (ddata->sped_2x)
+ cmd[1] |= LTC2499_SPD;
}
ret = i2c_master_send(st->client, cmd, sizeof(cmd));
@@ -168,6 +173,7 @@ static const struct ltc2497_chip_info ltc2497_info[] = {
.resolution = 24,
.name = "ltc2499",
.has_temp = true,
+ .has_speed_mode = true,
},
};
diff --git a/drivers/iio/adc/ltc2497.h b/drivers/iio/adc/ltc2497.h
index 1da2cfec3b6a..5707ee3ff8a5 100644
--- a/drivers/iio/adc/ltc2497.h
+++ b/drivers/iio/adc/ltc2497.h
@@ -2,7 +2,19 @@
#define LTC2497_ENABLE 0xA0
#define LTC2497_CONFIG_DEFAULT LTC2497_ENABLE
-#define LTC2497_CONVERSION_TIME_MS 150ULL
+
+/*
+ * Worst-case conversion times (datasheet t_CONV max). The driver only ever
+ * programs simultaneous 50/60Hz rejection (FA/FB selection is not implemented),
+ * so only those two rates are listed. The 1x value also covers the
+ * LTC2496/LTC2497, which have no speed mode.
+ *
+ * The 2x mode (LTC2499_SPD, LTC2499 only) disables the offset auto-calibration
+ * to roughly double the output rate; adding the 2x wait time is what makes the
+ * SPD control actually faster.
+ */
+#define LTC2497_CONV_TIME_1X_MS 150ULL /* t_CONV_1 simult. max 149.9 */
+#define LTC2499_CONV_TIME_2X_MS 76ULL /* t_CONV_2 simult. max 75.1 */
/*
* Sentinel passed as `address` to result_and_measure() to request a
@@ -14,11 +26,13 @@
/* Second config-byte bits (LTC2499 / LTC2493 only) */
#define LTC2499_EN2 BIT(7) /* enable second config byte */
#define LTC2499_IM BIT(6) /* 1 = measure internal temp sensor */
+#define LTC2499_SPD BIT(3) /* 1 = 2x output rate (offset cal off) */
struct ltc2497_chip_info {
u32 resolution;
const char *name;
bool has_temp;
+ bool has_speed_mode; /* SPD bit in the 2nd config byte (LTC2499/LTC2493) */
};
struct ltc2497core_driverdata {
@@ -28,6 +42,14 @@ struct ltc2497core_driverdata {
struct mutex lock;
const struct ltc2497_chip_info *chip_info;
u8 addr_prev;
+ bool sped_2x; /* SPD: false = 1x (default), true = 2x */
+ /*
+ * Conversion time (ms) of the conversion currently in flight. It is
+ * fixed by the mode active when that conversion was started, which
+ * differs from the newly selected mode for the first read after a
+ * sampling_frequency change.
+ */
+ unsigned int conv_time_prev;
int (*result_and_measure)(struct ltc2497core_driverdata *ddata,
u8 address, int *val);
};
--
2.43.0
next prev parent reply other threads:[~2026-08-05 9:44 UTC|newest]
Thread overview: 8+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-08-05 9:41 [PATCH 0/3] iio: adc: add LTC2499 features support Andrei Stancovici
2026-08-05 9:41 ` [PATCH 1/3] dt-bindings: iio: adc: lltc,ltc2497: add LTC2499 to title Andrei Stancovici
2026-08-05 9:41 ` [PATCH 2/3] iio: adc: ltc2497: add LTC2499 internal temperature channel Andrei Stancovici
2026-08-05 9:57 ` sashiko-bot
2026-08-05 10:00 ` Andy Shevchenko
2026-08-05 9:41 ` Andrei Stancovici [this message]
2026-08-05 9:56 ` [PATCH 3/3] iio: adc: ltc2497: add 2x conversion speed mode sashiko-bot
2026-08-05 10:08 ` Andy Shevchenko
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260805094135.11762-4-andrei.stancovici@analog.com \
--to=andrei.stancovici@analog.com \
--cc=Michael.Hennerich@analog.com \
--cc=andy@kernel.org \
--cc=conor+dt@kernel.org \
--cc=devicetree@vger.kernel.org \
--cc=dlechner@baylibre.com \
--cc=jic23@kernel.org \
--cc=krzk+dt@kernel.org \
--cc=liambeguin@gmail.com \
--cc=linux-iio@vger.kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=linux@analog.com \
--cc=nuno.sa@analog.com \
--cc=robh@kernel.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox