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 v2 2/3] iio: adc: ltc2497: add LTC2499 internal temperature channel
Date: Thu, 13 Aug 2026 19:01:36 +0300 [thread overview]
Message-ID: <20260813160139.70000-3-andrei.stancovici@analog.com> (raw)
In-Reply-To: <20260813160139.70000-1-andrei.stancovici@analog.com>
The LTC2499 has an internal PTAT (proportional to absolute temperature)
sensor that is activated by a second I2C configuration byte (EN2 | IM).
Expose it as an IIO_TEMP channel providing raw, scale and offset so the
standard IIO formula
T[m°C] = (raw + offset) * scale
reconstructs the temperature.
The PTAT sensor yields the absolute temperature as
T(K) = DATAOUT24 * Vref / 1570 (Vref in volts)
The raw value exported here is sign-extended and normalised to
2^(resolution + 1) == 2^25, i.e. raw = 2 * DATAOUT24, so on the IIO
milli-degree-Celsius convention
scale[m°C/LSB] = Vref_uV / 3140000
offset = -273150 * 3140000 / Vref_uV
The scale and offset are derived from the reference voltage returned by
regulator_get_voltage(); its error is propagated as before, so a board
that fails to describe vref-supply gets a clear read error instead of a
silently wrong temperature. No board-specific reference value is assumed
in the driver.
The single temperature channel is appended as the last entry of the
shared channel array and excluded via num_channels for parts without an
internal sensor, so the existing LTC2497 channel layout and device name
are unchanged.
The LTC2499 latches its converter configuration from the second command
byte and only re-evaluates it when that byte has EN2 set. EN2 | IM
selects the internal temperature sensor. Because a single-byte command,
or a second byte with EN2 = 0, means "keep previous", a one-byte channel
select cannot pull the device back out of temperature mode: after a
temperature read every subsequent voltage read would keep returning the
PTAT result instead of the selected input. Temperature support is
therefore only correct if the voltage path also emits a second command
byte that re-selects an external input.
Send two-byte commands for all conversions on parts that have the sensor
(has_temp):
temperature: EN2 | IM
voltage: EN2 (IM = 0 -> external input)
The LTC2497 and LTC2496, which lack the second-byte mechanism, keep using
the original single-byte channel select and are unchanged.
Signed-off-by: Andrei Stancovici <andrei.stancovici@analog.com>
---
Changes in v2:
- guard against regulator_get_voltage() returning 0 in the temperature offset path
- use a DMA-safe buffer for LTC2499 two-byte I2C commands
- use ABSOLUTE_ZERO_MILLICELSIUS instead of a hardcoded temperature constant
- use standard SI unit multipliers for the µV -> mV conversion
- make channel count assignment explicit
- clarify LTC2499 second-byte configuration behavior
drivers/iio/adc/ltc2497-core.c | 60 +++++++++++++++++++++++++++++++---
drivers/iio/adc/ltc2497.c | 35 ++++++++++++++++++++
drivers/iio/adc/ltc2497.h | 12 +++++++
3 files changed, 102 insertions(+), 5 deletions(-)
diff --git a/drivers/iio/adc/ltc2497-core.c b/drivers/iio/adc/ltc2497-core.c
index 2dc5c7044269..ed84ae67255d 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;
-
- return IIO_VAL_FRACTIONAL_LOG2;
+ 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;
+ }
+
+ case IIO_CHAN_INFO_OFFSET:
+ switch (chan->type) {
+ case IIO_TEMP:
+ ret = regulator_get_voltage(ddata->ref);
+ if (ret < 0)
+ return ret;
+ if (!ret)
+ return -EINVAL;
+ /*
+ * 0 °C == 273.15 K must map to raw + offset such that
+ * (raw + offset) * scale == 0 m°C, i.e.
+ * offset = ABSOLUTE_ZERO_MILLICELSIUS / scale
+ * = ABSOLUTE_ZERO_MILLICELSIUS * 3140000 / Vref_uV
+ * Computed in 64-bit to avoid overflow.
+ */
+ *val = div_s64((s64)ABSOLUTE_ZERO_MILLICELSIUS * 3140000, 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 = <c2497core_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..57a5406977ed 100644
--- a/drivers/iio/adc/ltc2497.c
+++ b/drivers/iio/adc/ltc2497.c
@@ -84,6 +84,40 @@ 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 two bytes are assembled in the DMA-safe st->data buffer rather than
+ * on the stack, so the pointer handed to i2c_master_send() stays valid on
+ * adapters that DMA the transfer (e.g. with CONFIG_VMAP_STACK).
+ */
+ if (ddata->chip_info->has_temp) {
+ if (address == LTC2497_TEMP_ADDR) {
+ st->data.d8[0] = LTC2497_ENABLE | LTC2497_CONFIG_DEFAULT;
+ st->data.d8[1] = LTC2499_EN2 | LTC2499_IM;
+ } else {
+ st->data.d8[0] = LTC2497_ENABLE | address;
+ st->data.d8[1] = LTC2499_EN2;
+ }
+
+ ret = i2c_master_send(st->client, (char *)st->data.d8, 2);
+ 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 +171,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..1da2cfec3b6a 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;
+ bool has_temp;
};
struct ltc2497core_driverdata {
--
2.43.0
next prev parent reply other threads:[~2026-08-13 16:02 UTC|newest]
Thread overview: 7+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-08-13 16:01 [PATCH 0/3] iio: adc: add LTC2499 features support Andrei Stancovici
2026-08-13 16:01 ` [PATCH v2 1/3] dt-bindings: iio: adc: lltc,ltc2497: add LTC2499 to title Andrei Stancovici
2026-08-13 16:01 ` Andrei Stancovici [this message]
2026-08-13 16:12 ` [PATCH v2 2/3] iio: adc: ltc2497: add LTC2499 internal temperature channel sashiko-bot
2026-08-13 20:57 ` Andy Shevchenko
2026-08-13 16:01 ` [PATCH v2 3/3] iio: adc: ltc2497: add 2x conversion speed mode Andrei Stancovici
2026-08-14 12:01 ` 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=20260813160139.70000-3-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