[PATCH v4 2/3] iio: adc: ltc2497: add LTC2499 internal temperature channel

From: Andrei Stancovici

Date: Wed Sep 09 2026 - 04:35:25 EST


The LTC2499 includes an internal temperature sensor. Expose it as an
IIO_TEMP channel with raw, scale and offset.

Scale and offset are derived from the reference voltage, so reading them
returns an error if firmware does not describe a vref-supply. As this is
a new feature on a part that already works without it, failing probe is
not an option.

Selecting the sensor needs a second command byte, and the part keeps the
configuration it last latched unless that byte is sent again. Voltage
reads on this part therefore also send the second byte.

Datasheet: https://www.analog.com/media/en/technical-documentation/data-sheets/2499fe.pdf
Reviewed-by: Andy Shevchenko <andriy.shevchenko@xxxxxxxxx>
Signed-off-by: Andrei Stancovici <andrei.stancovici@xxxxxxxxxx>
---
Changes in v4:
- Description cut down to what a reader needs: the sensor, the ABI it
exposes and the vref dependency. The PTAT derivation and the
num_channels detail are gone; the maths stays as comments next to the
code that does it.
- struct ltc2497_chip_info: booleans moved last, as requested, with one
deviation I should flag - I hoisted `name` to the front rather than
only shifting the flags down. pahole, LP64, all three instances:

as posted (flags after resolution) 16 B, 2 B hole, 48 B
flags moved last, as requested 24 B, 4 B hole + 6 B tail, 72 B
name, resolution, flags last (v4) 16 B, no hole, 48 B

The pre-series struct already had a 4-byte hole after `resolution` and
the flags fell into it, which is why the posted version did not grow.
Moving them to the end leaves that hole and adds tail padding. Hoisting
the pointer puts the flags last as asked and removes the hole. On ILP32
all three orderings are 12 B. If you would rather have the literal
reorder, say so and I will take the 24 B.
- Why the second command byte is sent on every conversion rather than
only when the latched configuration already differs: simplicity. It
costs one extra byte per conversion, and a conversion takes 150 ms, so
tracking the latched state to avoid it does not pay for itself. The
code comment now records that the unconditional send is deliberate; the
rationale itself is here rather than in the commit message.
- Added the Datasheet: trailer.
- Andy's Reviewed-by is kept. The only code change in this patch since v3
is the member reorder he asked for, plus one sentence of comment.

drivers/iio/adc/ltc2497-core.c | 58 +++++++++++++++++++++++++++++++---
drivers/iio/adc/ltc2497.c | 36 +++++++++++++++++++++
drivers/iio/adc/ltc2497.h | 14 +++++++-
3 files changed, 103 insertions(+), 5 deletions(-)

diff --git a/drivers/iio/adc/ltc2497-core.c b/drivers/iio/adc/ltc2497-core.c
index 2dc5c7044269..6df9c72bd8cf 100644
--- a/drivers/iio/adc/ltc2497-core.c
+++ b/drivers/iio/adc/ltc2497-core.c
@@ -9,9 +9,11 @@
#include <linux/delay.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/units.h>

#include "ltc2497.h"

@@ -95,10 +97,45 @@ static int ltc2497core_read_raw(struct iio_dev *indio_dev,
if (ret < 0)
return ret;

- *val = ret / 1000;
- *val2 = ddata->chip_info->resolution + 1;
+ switch (chan->type) {
+ case IIO_TEMP:
+ /*
+ * raw is normalised to 2^(resolution + 1), i.e.
+ * raw = 2 * DATAOUT24, so the PTAT scale (datasheet
+ * Vref / 1570 per Kelvin) doubles its denominator and,
+ * in m°C, becomes Vref_uV / 3140000.
+ */
+ *val = ret;
+ *val2 = 3140000;
+ return IIO_VAL_FRACTIONAL;
+ case IIO_VOLTAGE:
+ *val = ret / (MICRO / MILLI);
+ *val2 = ddata->chip_info->resolution + 1;
+ return IIO_VAL_FRACTIONAL_LOG2;
+ default:
+ return -EINVAL;
+ }

- return IIO_VAL_FRACTIONAL_LOG2;
+ case IIO_CHAN_INFO_OFFSET:
+ switch (chan->type) {
+ case IIO_TEMP:
+ ret = regulator_get_voltage(ddata->ref);
+ if (ret < 0)
+ return ret;
+ if (ret == 0)
+ return -EINVAL;
+ /*
+ * 0 °C == 273.15 K must map to raw + offset such that
+ * (raw + offset) * scale == 0 m°C, i.e.
+ * offset = -273150 / scale
+ * = -273150 * 3140000 / Vref_uV
+ * Computed in 64-bit to avoid overflow.
+ */
+ *val = div_s64(ABSOLUTE_ZERO_MILLICELSIUS * 3140000LL, ret);
+ return IIO_VAL_INT;
+ default:
+ return -EINVAL;
+ }

default:
return -EINVAL;
@@ -126,6 +163,14 @@ static int ltc2497core_read_raw(struct iio_dev *indio_dev,
.differential = 1, \
}

+#define LTC2497_TEMP_CHANNEL { \
+ .type = IIO_TEMP, \
+ .address = LTC2497_TEMP_ADDR, \
+ .info_mask_separate = BIT(IIO_CHAN_INFO_RAW) | \
+ BIT(IIO_CHAN_INFO_SCALE) | \
+ BIT(IIO_CHAN_INFO_OFFSET), \
+}
+
static const struct iio_chan_spec ltc2497core_channel[] = {
LTC2497_CHAN(0, LTC2497_SGL, "CH0"),
LTC2497_CHAN(1, LTC2497_SGL, "CH1"),
@@ -159,6 +204,7 @@ static const struct iio_chan_spec ltc2497core_channel[] = {
LTC2497_CHAN_DIFF(5, LTC2497_DIFF | LTC2497_SIGN),
LTC2497_CHAN_DIFF(6, LTC2497_DIFF | LTC2497_SIGN),
LTC2497_CHAN_DIFF(7, LTC2497_DIFF | LTC2497_SIGN),
+ LTC2497_TEMP_CHANNEL,
};

static const struct iio_info ltc2497core_info = {
@@ -183,7 +229,11 @@ int ltc2497core_probe(struct device *dev, struct iio_dev *indio_dev)
indio_dev->info = &ltc2497core_info;
indio_dev->modes = INDIO_DIRECT_MODE;
indio_dev->channels = ltc2497core_channel;
- indio_dev->num_channels = ARRAY_SIZE(ltc2497core_channel);
+ /* Only the ltc2499 has a temperature channel; it is the last entry. */
+ if (ddata->chip_info->has_temp)
+ indio_dev->num_channels = ARRAY_SIZE(ltc2497core_channel);
+ else
+ indio_dev->num_channels = ARRAY_SIZE(ltc2497core_channel) - 1;

ret = ddata->result_and_measure(ddata, LTC2497_CONFIG_DEFAULT, NULL);
if (ret < 0)
diff --git a/drivers/iio/adc/ltc2497.c b/drivers/iio/adc/ltc2497.c
index c1668b5a351e..ea55b417213a 100644
--- a/drivers/iio/adc/ltc2497.c
+++ b/drivers/iio/adc/ltc2497.c
@@ -84,6 +84,41 @@ static int ltc2497_result_and_measure(struct ltc2497core_driverdata *ddata,
return 0;
}

+ /*
+ * 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 = 1 for a temperature read,
+ * EN2 alone (IM = 0) to (re)select an external input. FA = FB = 0 keeps
+ * the power-on simultaneous 50/60Hz rejection, whose worst-case
+ * conversion time the driver's wait already covers.
+ *
+ * The byte could be skipped while the latched configuration is already
+ * the one wanted; it is sent on every conversion for simplicity.
+ */
+ if (ddata->chip_info->has_temp) {
+ u8 cmd[2];
+
+ if (address == LTC2497_TEMP_ADDR) {
+ cmd[0] = LTC2497_ENABLE | LTC2497_CONFIG_DEFAULT;
+ cmd[1] = LTC2499_EN2 | LTC2499_IM;
+ } else {
+ cmd[0] = LTC2497_ENABLE | address;
+ cmd[1] = LTC2499_EN2;
+ }
+
+ ret = i2c_master_send(st->client, cmd, sizeof(cmd));
+ if (ret < 0) {
+ dev_err(&st->client->dev, "i2c transfer failed: %pe\n",
+ ERR_PTR(ret));
+ return ret;
+ }
+ return 0;
+ }
+
ret = i2c_smbus_write_byte(st->client,
LTC2497_ENABLE | address);
if (ret)
@@ -137,6 +172,7 @@ static const struct ltc2497_chip_info ltc2497_info[] = {
[TYPE_LTC2499] = {
.resolution = 24,
.name = "ltc2499",
+ .has_temp = true,
},
};

diff --git a/drivers/iio/adc/ltc2497.h b/drivers/iio/adc/ltc2497.h
index 64e81c95a3dd..2b797fd19651 100644
--- a/drivers/iio/adc/ltc2497.h
+++ b/drivers/iio/adc/ltc2497.h
@@ -4,9 +4,21 @@
#define LTC2497_CONFIG_DEFAULT LTC2497_ENABLE
#define LTC2497_CONVERSION_TIME_MS 150ULL

+/*
+ * Sentinel passed as `address` to result_and_measure() to request a
+ * temperature conversion instead of a voltage channel. Valid channel
+ * addresses fit in 5 bits (0x00–0x1F), so 0xFF is unambiguous.
+ */
+#define LTC2497_TEMP_ADDR 0xFF
+
+/* 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 */
+
struct ltc2497_chip_info {
- u32 resolution;
const char *name;
+ u32 resolution;
+ bool has_temp;
};

struct ltc2497core_driverdata {
--
2.43.0