Devicetree
 help / color / mirror / Atom feed
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


  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