Several temperature sensor types are described below: NTC thermistor, PT100/500/1000 sensor, LMx35 precision analogue sensor, DS18xxx 1-wire digital sensor, LM75x digital temperature sensor, MCP9808 maximum accuracy digital sensor, D6T1A01 MEMS thermal imaging sensor, ... Each has C++ example code for Arduinos, easily adapted for ESP32 etc.
For overheating protection
When working with MOSFETs and other components that can overheat, it's a good idea to use temperature sensors in your prototypes to monitor the components. This helps reduce the size of your component cemetery. If overheating is still a risk, these sensors can be used in the final product too. My high-voltage high-current projects are prone to overheating if they are left running for too long. Low-cost RTD sensors (Resistance Temperature Detector or Resistive Temperature Device) are ideal for this, with ranges up to 600ºC.
MOSFETs and chips can operate at temperatures up to 175ºC or higher, so use a sensor that can detect well above the max. temperature - which will still return meaningful temperatures if it gets even hotter. Some sensor types start returning invalid temperatures if they get too hot (a bad idea), but NTC and Pt100/Pt1000 sensors are OK. Unless they melt.
A tiny NTC or Pt100 or Pt1000 sensor can be glued or taped directly to the case of the component, and sampled via any analogue input. For RTDs you can use a simple voltage divider to get reasonable accuracy. There is no need for a complicated circuit using an op amp unless you want high accuracy over a smaller range.
Fix the sensor to the component's case using high temperature polyamide tape (the copper-coloured stuff) with conductive paste, or heat-resistant-heat-conductive epoxy (this is not easy to find, and sometimes cracks off if regularly heated and cooled).
For temperature compensation
Sometimes temperature compensation is needed for sensitive circuits, like vibrating wire sensors (DigiSens.ch). Precision calibrated integrated-circuit sensors are ideal for this, such as the analogue LM135/235/335. These have a narrower range than RTDs, something like -30..+125ºC. However, if they go over their rated temperature they can start returning lower temperatures, which is not good for detecting overheating!
Digital temperature sensors with one-wire, I2C or SPI interfaces are also available, like the DS18B20 (range -55..+125ºC). These return the temperature as an accurate calibrated digital value in deg C or F. These also don't work for high overheating temperatures.
NTC Thermistors
(almost actual size)
NTC means Negative Temperature Coefficient - the resistance decreases as the temperature increases.
These sensors have a reasonable range, -40..+300ºC is common and some go all the way up to 600ºC. Miniature glass-encapsulated versions are often used in 3D printers. They do not have a linear response, the resistance changes far more at lower temperatures than it does at high temperatures. You can use the Steinhart-Hart equation to convert the resistance to temperature with reasonable accuracy.
If the temperature goes over the maximum, most of them will continue to return a meaningful resistance until the case actually melts. The 100K sensors I used work up to 500ºC, even though their rated maximum is 300ºC. Test this by heating it in a flame or small blowtorch (see Disclaimer). A component freezer spray can be used to test low temperatures down to -60ºC (but my MOSFETs rarely get that cold).
Thermistors are graded by their resistance at 25ºC (room temperature). For example, 10K ohms or 100K ohms at 25ºC. Choose the resistance according to the range you want to measure (see below). You can find the resistance-to-temperature tables on line. Each has a different resistance-to-temperature curve, roughly indicated by the sensor's "Beta value". The Beta value is always provided by the manufacturer, use the Beta value to find the right table. (But note that the online tables are often calculated from the Beta value using the equation - they are not actual measurements.)
The Steinhart-Hart equation needs three constants named A, B and C to configure the sensor. These are sometimes given by the manufacturer, but it's best to calculate them from the resistance-to-temperature tables of your sensor according to the range you want to measure. Three temperature/resistance values are needed to calculate the constants. For a small range, use the lower, middle and upper temperatures you want to measure. For component temperatures, a larger range is better (say) 25, 125 and 250ºC.
There are many thermistor constant calculators available on line. Here's the one that's linked to from the Wackypedia entry. I like this one because it also shows the curve. In the picture below, the curve is not too steep because a small range is being used (5..45ºC). For component temperature measurement you will need a larger range, so you will see a much flatter curve with a lower resolution at high temperatures, but that's OK for our application.
Once you have the three constants, A, B and C, use these in the simplified equation to convert the resistance Rt to temperature K in Kelvins. Don't forget to use the exponents (e-3 etc), and sometimes the mantissa is -ve (which is easy to miss).
K = 1 / (A + B * ln(Rt) + C * (ln(Rt))^3)
Use the 'float' data type and return the result as an 'int', because the accuracy and resolution is not good enough for more than 3 significant digits. At low temperatures around room temperature it is quite accurate, but the accuracy (the resolution) decreases as the temperature increases.
The microcontroller's ADC is usually 10-bit (0-1023), which is spread over the entire temperature range, minus the offset from the voltage divider resistor, etc. This means you have quite a low resolution, especially at high temperatures - a change of '1' in the reading can mean a temperature change of several degrees.
Extreme Accuracy is not necessary for our application. We only need to know if it's warm, hot, very hot, or is about to explode. Short-circuit and open-circuit of the temperature sensor must also be detected.
Use a simple voltage divider to measure the sensor's resistance. Choose R1 according to sensor type and range, e.g. 4.7K or 10K ohms. It should be a 0.1% resistor with a low temperature coefficient like 25ppm/ºC. You must define this resistance value in the software, because it's used in the voltage calculation.
It's easy to calculate the NTC sensor's resistance with this circuit. Vref should be the maximum voltage allowed by the analogue input (3.3 or 5V). 1024 is the 10-bit ADC range.
int count = analogRead(pin);
int vout = ((long)count * vref) / 1024; // voltage across sensor in millivolts
long r = ((long)vout * r1) / (vref - vout); // resistance in ohms
Use a 32-bit long (or unsigned long) for the resistance because it can be > 16 bits. The code also needs averaging/smoothing and tests for short and open-circuit conditions.
Here's the full source code in C. This runs on all Arduinos, ESP32s, etc.
Note that I have used tab characters instead of spaces, with tabs set to 4 spaces. This is because I use 'Microsoft Visual Studio Community 2022' for development, with the Visual Micro Arduino IDE plugin to provide microcontroller support. This has more features than the Arduino IDE, it's more complicated to use but is way more intuitive than the awful 'VS Code'.
#pragma once
// NTC Thermistor using Steinhart-Hart algorithm
// info@muman.ch, 2025.03.02
/*
Requires three parameters [A, B, C] calculated here according to the thermistor data
https://www.thinksrs.com/downloads/programs/therm%20calc/ntccalibrator/ntccalculator.html
https://rusefi.com/Steinhart-Hart.html
K = 1 / (A + B * ln(Rt) + C * (ln(Rt))^3)
Voltage Divider Circuit
=======================
4K7 Sensor
+-----+ +-----+
Vref ----| R1 |----+----| NTC |---- GND
+-----+ | +-----+
|
+-------> Vout
The voltage divider and the 10-bit ADC limits the range, so it's only accurate to
within a degree or so at room temperatures, getting less accurate as the temperature
increases due to the flattening NTC curve.
To prevent self-heating, less than 1mA should flow through the thermistor.
R1 should be a 0.1% resistor with a low temperature coefficient like 25ppm/C.
Disconnected: Returns -32767c (count >= 1022)
Short Circuit: Returns +32767c (count <= 2)
Typical 10K NTC thermistor NRBG104F3435B2F with table:
https://www.eaton.com/content/dam/eaton/products/electronic-components/resources/data-sheet/eaton-nrbg-glass-sealed-radial-lead-ntc-thermistor-datasheet-elx1105-en.pdf
*/
#include "limits.h"
#include "RollingAverage.h"
namespace MattLabs
{
class NTCThermistor
{
public:
NTCThermistor(int analogPin, long R1, int vrefmv, float A, float B, float C, int readingsToAverage = 0);
long getResistance(bool raw = false);
int getTemperature(bool raw = false);
void clearAverager() { averager.clear(); }
private:
int pin; // analog input pin
long r1; // voltage divider R1 resistor value in ohms, e.g. 4700
int vref; // millivolts, e.g. 3.3V = 3300, 5V = 5000
float a, b, c; // Steinhart-Hart constants from internet calculator
RollingAverage<int, long> averager;
};
NTCThermistor::NTCThermistor(int analogPin, long R1, int vrefmv, float A, float B, float C, int readingsToAverage)
{
pin = analogPin;
pinMode(pin, INPUT);
r1 = R1;
vref = vrefmv;
a = A;
b = B;
c = C;
if (readingsToAverage > 1)
averager.begin(readingsToAverage);
}
long NTCThermistor::getResistance(bool raw)
{
#ifdef analogReadMillivolts
int vout = analogReadMilliVolts(pin);
#else
int count = analogRead(pin);
int vout = ((long)count * vref) / 1023; // or 4095 for 12-bit analog
#endif
// short circuit or over temperature
if (count <= 2)
return LONG_MAX;
// disconnected or under temperature
if (count >= 1022) // or 4093 for 12-bit analog
return LONG_MIN;
// raw or smoothed
if (!raw && averager.getBufferLength() > 0) {
averager.addSample(vout);
vout = averager.getAverage();
}
int vref_minus_vout = vref - vout;
if (vref_minus_vout <= 0)
return LONG_MIN;
return ((long)vout * r1) / vref_minus_vout;
}
int NTCThermistor::getTemperature(bool raw)
{
long rSensor = getResistance(raw);
// disconnected or under temperature
if (rSensor <= 0)
return INT_MIN; // 0x8000 -32768c
// short circuit or over temperature
if (rSensor == LONG_MAX)
return INT_MAX; // 0x7FFF +32767c
// Steinhart-Hart algorithm
float lnR = logf(rSensor);
float K = 1.0f / (a + b * lnR + c * powf(lnR, 3));
// return centigrade/Celcius
return K - 273.15f;
}
}
The optimized "rolling averager" is used to smooth the analogue readings.
#pragma once
// Optimized Rolling Average Filter
// matt@muman.ch, 2025.03.04
/*
Averages values of any numerical data type. The data type of the sum value SUMTYPE can
also be defined.
For float and double, SUMTYPE can be omitted, so SUMTYPE is float or double by default.
For integer types, the SUMTYPE must be able to hold 'MAXVALUE * numberOfSamples' without
overflowing or underflowing. e.g. for a 16-bit 'int' you should use a 32-bit 'long',
else the sum may overflow/underflow. This is not checked by the compiler.
On a 32-bit machine, 'int' and 'long' are both 32 bits, so you may need to use 'long long'
(64 bits) or int64_t if the sum is very large.
If you are averaging 10-bit analog input values (0..1023) on an 8 or 16-bit processor
(16-bit integers), then you can use 'unsigned int' for both because it will not overflow
if you don't sum more than 64 values. This is faster than using 'long'.
But if you use 'float' for averaging integer values you will get a much smoother result.
This removes the 'step effect' you can get when reading narrow-range analogue inputs.
As usual, all methods must be called from the same thread (i.e. not from an interrupt),
else use some kind of mutex lock.
*/
namespace MattLabs
{
template <typename T, typename SUMTYPE = T>
class RollingAverage
{
T* samples = NULL;
int bufferLength = 0;
int index = 0;
bool bufferFull = false;
// SUMTYPE must be able to hold the sum of all values (max/min) without overflow
SUMTYPE sum;
public:
RollingAverage() { }
RollingAverage(int numberOfSamples) { begin(numberOfSamples); }
~RollingAverage();
void begin(int numberOfSamples);
void clear();
void addSample(T sample);
T getAverage();
int getNumberOfSamples() { return bufferFull ? bufferLength : index; }
int getBufferLength() { return bufferLength; }
};
// A destructor is not usually needed for embedded code - use static objects
template <typename T, typename SUMTYPE>
RollingAverage<T, SUMTYPE>::~RollingAverage()
{
if (samples) {
free(samples);
samples = NULL;
}
}
template <typename T, typename SUMTYPE>
void RollingAverage<T, SUMTYPE>::begin(int numberOfSamples)
{
if (numberOfSamples <= 0)
numberOfSamples = 1;
bufferLength = numberOfSamples;
if (samples)
free(samples);
samples = (T*)malloc(bufferLength * sizeof(T));
if (!samples) {
//TODO fatal error handling, out of memory
}
bufferLength = bufferLength;
clear();
}
template <typename T, typename SUMTYPE>
void RollingAverage<T, SUMTYPE>::clear()
{
index = 0;
sum = 0;
bufferFull = false;
}
template <typename T, typename SUMTYPE>
void RollingAverage<T, SUMTYPE>::addSample(T sample)
{
if (bufferFull)
sum -= samples[index];
sum += sample;
samples[index] = sample;
if (++index == bufferLength) {
index = 0;
bufferFull = true;
}
}
template <typename T, typename SUMTYPE>
T RollingAverage<T, SUMTYPE>::getAverage()
{
if (bufferFull)
return sum / bufferLength;
if (index == 0)
return 0;
return sum / index;
}
}
These are probably the best type of analogue sensor, with a maximum ranges of -70..+600ºC. They are about the same price as the NTC thermistors. The cases are often flat (Yageo models) which make it easy to tape them to a MOSFET or chip. The resistance increases as the temperature increases.
There are three standard types, Pt100, Pt500 and Pt1000, defined by their resistance at 0ºC (100, 500 and 1000 ohms). It's basically just an etched platinum thin film resistor.
To calculate the temperature from the resistance, the easiest way is to use the IEC 60751 algorithm.
If the temperature is below 0ºC it should use a different algorithm, but since we're only interested in measuring heat, we can use only the > 0ºC algorithm, which is easier.
These sensors also use the voltage divider circuit shown above, the same as for the NTC Thermistor. It's important to limit the current through the sensor to prevent self-heating, and a minimum current is also needed.
Pt100: 0.3 to 1 mA - use 4.7K ohms at 3.3V, 5.6K at 5V Pt1000: 0.1 to 0.3 mA - use 12K ohms at 3.3V, 15K at 5V
But do the calculations yourself... see Ohm's Law. Or was it Cole's Law?
This code may not be accurate for values below 0ºC, but that's not a problem when testing for overheating.
#pragma once
// PT100 and PT1000 - Platinum Resistance Temperature Sensors
// info@muman.ch, 2025.03.02
/*
Uses a simple voltage divider to provide a reasonable accuracy (+-2C) over
a large range -70..+600C for PT100/PT1000 sensors.
Accuracy is affected by the small range of the voltage for a large temperature
range when sampled by a 10-bit ADC. The step value may be several degrees at
high temperatures.
From Vout it calculates the resistance of the PT100x sensor, and uses it in
the DIN EN60751 standard algorithm.
Voltage Divider Circuit
=======================
Sensor
+------+ +--------+
Vref ----| R1 |----+----| PT100x |---- GND
+------+ | +--------+
|
|
+-------> Vout
PT100 : R1 = 4.7K
PT1000 : R1 = 10K
R1 should be a 0.1% resistor with a low temperature coefficient like 25ppm/C.
For PT1000, the max current is 0.3mA. For PT100, the max current is 1mA.
*/
#include <limits.h>
#include "RollingAverage.h"
namespace MattLabs
{
class PT100x
{
public:
PT100x(int analogPin, long RZero, long R1, int vrefmv, int readingsToAverage = 0);
long getResistance(bool raw = false);
int getTemperature(bool raw = false);
void clearAverager() { averager.clear(); }
private:
int pin; // analog input pin
long r1; // accurate voltage divider R1 resistor value in ohms, e.g. 4700 or 10000
int vref; // millivolts, e.g. 3.3V = 3300, 5V = 5000
long rZero; // resistance at 0C, usually 100 or 1000 ohms
// PT100/500/1000 temperature calculation factors according to DIN EN60751 standard
const float A = 3.9083e-3;
const float B = -5.775e-7;
// pre-calculated factors (optimization)
const float Asquared = A * A;
const float twoB = 2.0f * B;
const float fourB = 4.0f * B;
RollingAverage<int, long> averager;
};
PT100x::PT100x(int analogPin, long RZero, long R1, int vrefmv, int readingsToAverage)
{
pin = analogPin;
pinMode(pin, INPUT);
rZero = RZero;
r1 = R1;
vref = vrefmv;
if (readingsToAverage > 1)
averager.begin(readingsToAverage);
}
long PT100x::getResistance(bool raw)
{
#ifdef analogReadMillivolts
int vout = analogReadMilliVolts(pin);
#else
int count = analogRead(pin);
int vout = ((long)count * vref) / 1023; // or 4095 for 12-bit analog
#endif
// short circuit or over temperature
if (count <= 2)
return LONG_MAX;
// disconnected or under temperature
if (count >= 1022)
return LONG_MIN;
// raw or smoothed
if (!raw && averager.getBufferLength() > 0) {
averager.addSample(vout);
vout = averager.getAverage();
}
int vref_minus_vout = vref - vout;
if (vref_minus_vout <= 0)
return LONG_MIN;
return ((long)vout * r1) / vref_minus_vout;
}
int PT100x::getTemperature(bool raw)
{
long rSensor = getResistance(raw);
// disconnected or under temperature
if (rSensor <= 0)
return INT_MIN; // 0x8000 -32768c
// short circuit or over temperature
if (rSensor == LONG_MAX)
return INT_MAX; // 0x7FFF +32767c
// DIN EN60751 temp in degrees C
return ((-A + sqrtf(Asquared - fourB * (1.0f - rSensor / rZero))) / twoB);
}
}
Use these if you are measuring temperatures -55..150ºC and you want greater accuracy. They need a supply of 5..40V, with a series resistor to limit the current to between 450uA and 5mA over the temperature range. They must be powered by a minimum of 5V, so if you're using a 3.3V microcontroller you will also need a voltage divider to keep the voltage below 3.3V.
The three models LM135/235/335 have different temperature ranges:
LM135 = -55 .. +150°C
LM235 = -40 .. +125°C
LM335 = -40 .. +100°C
Inside is a Zener diode with a breakdown voltage that varies according to the temperature, having a linear increase of 10mV per °K (Kelvin). The voltage variation with temperature is almost perfectly linear, so to calibrate the temperature very accurately you only need to add a single offset value. You could use a trimpot on the ADJ terminal for an adjustable offset voltage, but doing it in software is much cheaper.
Sufficient voltage to drive the sensor and its calibration circuit is needed. 3.3V is not high enough. Most of the examples in the data sheet show 15V, but anything from 5..40V works if you choose the right series resistance to limit the current to 450uA..5mA. Keep the current low to reduce internal heating, say max. 3mA, but don't let it go below 0.5mA because the sensor stops working.
When using a 3.3V controller you must use a second divider to reduce the voltage so it doesn't go over 3V. Use a pair of high-value resistors like 512K-512K. This divides the voltage by two, which makes it easy to calculate.
The resistors should have a low temperature coefficient to minimize drift, something like 25ppm/ºC. Avoid mounting the resistors near anything that gets warm, like a MOSFET or voltage regulator.
Because the sensor's output is linear, at 10mV/°K, you can use a single calibration point 'calibrationOffset'. There is no need to measure the voltage exactly, or mess about with the '2.982V at 25C' value. You can just set the calibration offset to adjust the voltage to the temperature. The calibration offset can be found by setting it to zero, and comparing the raw room temperature read by the sensor with the actual room temperature. The difference is the 'calibrationOffset'.
At 25°C the voltage is about 2.98V. With 10mV/°K (that's actually the same as 10mV/°C), then at -55°C it would be about 2.43V and at +150°C it would be 4.48V.
We know the current should be between 0.00045 and 0.005 Amps. The lowest current will be at 150°C, when the sensor drops 4.48V. If we say we want 500uA at 150°C, then we can calculate the resistance...
R1 = V / I
R1 = (Vcc - 4.48) / 0.0005
R1 = (5 - 4.48) / 0.0005
R1 = 1040 ohms -> use 1K ohms
At -55°C the current would be...
I = V / R
I = (5 - 2.43) / 1000
I = 2.57mA -> well within the max. value of 5mA
For higher Vcc voltages, increase the minimum current to 1 or 2mA, because the current range reduces as the voltage increases.
For a 3.3V controller, use the R2/R3 voltage divider to limit the analogue input voltage to less than 3V.
For a 5V controller, you don't need the R2/R3 divider.
#pragma once
// LMx35 Precision Temperature Sensor
// info@muman.ch, 2025.02.18
/*
https://www.st.com/resource/en/datasheet/lm135.pdf
https://www.ti.com/lit/ds/symlink/lm35.pdf
The LM135/235/335 are Zener sensors with a breakdown voltage that's proportional to
the temperature, at 10mV per degree Kelvin. They are calibrated for 2.982V at 25degC.
The temperature range depends on the type:
LM135 = -55 .. +150 C
LM235 = -40 .. +125 C
LM335 = -40 .. +100 C
A voltage divider (R1 in the diagram below) is needed so the voltage across the sensor
can be measured by an analog input.
Sufficient current to drive the sensor and its calibration circuit is needed.
3.3V is not high enough, these sensors don't work well with 3.3V, so min. 5V should
be used. The examples in the data sheets shows 15V, but anything from 5..40V works
if you choose the right series resistance to limit the current.
Because min. 5V is needed, when using a 3.3V controller you must use a second divider
so the voltage won't go over 3V. Use a pair of high value resistors, like 512K-512K.
This divides the voltage by 2, which makes it easy to calculate.
All resistors should have a low temperature coefficient to minimize drift, something
like 25ppm/deg C. And don't mount the resistors near anything that gets warm, like a
MOSFET, etc.
Because the sensor's output is linear, at 10mV/K, you can use a single calibration
point 'calibrationOffset'. There is no need to measure the voltage exactly, or mess
about with the '2.982V at 25C' values. You can just set the calibration offset to
adjust the voltage to the temperature.
The 'calibrationOffset' can be found by setting it to zero, and comparing the read
temperature with the actual temperature. The difference is the 'calibrationOffset'.
>= 5V
|
+---+
| R | 1K for 5V
| 1 |
+---+
|
+--------------+---------> Vout for 5V controller
| |
---A--- +---+
/ \ | R | 512K
/ \ | 2 |
+-----+ +---+
| LMx35 |
| +---------> Vout for 3.3V controller
| |
| +---+
| | R | 512K
| | 3 |
| +---+
| |
GND GND
R1 must be chosen according to the supply voltage, see the data sheets.
For 5V you can use 1K.
If the controller runs on 5V, then the voltage can be taken from R1 and R2/R3
are not needed.
*/
#include "RollingAverage.h"
namespace MattLabs
{
class LMx35TempSensor
{
public:
LMx35TempSensor(int analogPin, bool divider, float calibrationOffset, int readingsToAverage = 0);
float getTemperature(bool raw = false);
void clearAverager() { averager.clear(); }
private:
int pin; // analog input pin
bool div; // true if a divide-by-two divider is used for a 3.3V controller
float calibOffset; // calibration offset in deg C
RollingAverage<int> averager;
};
LMx35TempSensor::LMx35TempSensor(int analogPin, bool divider, float calibrationOffset, int readingsToAverage)
{
pin = analogPin;
pinMode(pin, INPUT);
div = divider;
calibOffset = calibrationOffset;
if (readingsToAverage > 1)
averager.init(readingsToAverage);
}
float LMx35TempSensor::getTemperature(bool raw)
{
#ifdef analogReadMillivolts
int mv = analogReadMilliVolts(pin);
#else
int count = analogRead(pin);
int mv = ((long)count * vref) / 1023; // or 4095 for 12-bit analog
#endif
// smoothing
if (!raw && averager.getBufferLength() > 0) {
averager.addSample(mv);
mv = averager.getAverage();
}
// we use a divide-by-two voltage divider (e.g. 100K/100K) if the sensor is powered by 5V
if (div)
mv *= 2;
// 2.982v = 25degC, change is 10mV per K
// C = K - 273.15 (-273.15 = absolute zero)
float t = (mv / 10.0f) - 273.15f;
// calibration
t += calibOffset;
return t;
}
}
DS18xxx, 1-Wire Digital Thermometers
(TO-92 or SOIC-8 package)
They are also available in stainless steel waterproof packages, often with a connector board that has the I2C pull-ups.
These are cheap sensors for reasonably accurate temperature measurement over the range -55°C .. +125°C. They are no good for over-temperature testing, but are perfect for temperature compensation and indoor or outdoor temperature measurement. These intelligent digital sensors can be powered by 3.3V or 5V, so you can use them with most microcontrollers.
They communicate via a '1-Wire' bus. 1-Wire is a half-duplex serial bus designed by Dallas Semiconductor. You can connect as many sensors as you like (in theory ;-), because each sensor has a unique 64-bit (!) factory-programmed 'ROM code', which is the sensor's serial number. The first byte of the 8-byte ROM Code is the device type.
#pragma once
// DS18xxx Temperature Sensor
// info@muman.ch, 2025.03.18
/*
This works for the three sensor types, DS18S20, DS19B20 and DS1822.
https://www.analog.com/media/en/technical-documentation/data-sheets/ds18b20.pdf
https://www.analog.com/media/en/technical-documentation/data-sheets/ds18s20.pdf
https://www.analog.com/media/en/technical-documentation/data-sheets/ds1822.pdf
These intelligent digital sensors can be powered by 3.3V or 5V, so you can
use them with most microcontrollers.
Many sensors can be connected to a 'OneWire' bus. Each sensor has a unique
64-bit (!) factory-programmed 'ROM code', a serial number. (Unless it's a
Chinese clone, then all the serial numbers are probably the same ;-)
The first byte of the 8-byte ROM Code is the device type. These three device
types are supported:
0x10 = DS18S20 // fixed at 9-bit, 0.5 deg C accuracy
0x28 = DS18B20 // configurable 9/10/11/12 bit, 0.5 deg C accuracy
0x22 = DS1822 // configurable 9/10/11/12 bit, economy version, 2degC accuracy
OneWire needs a single open-drain pin for communications, with a 4.7K
pull-up to the controller's Vcc voltage (3.3 or 5V).
The Arduino 'OneWire' library is used for communications:
https://github.com/PaulStoffregen/OneWire
Full details can be found here:
https://muman.ch/muman/index.htm?muman-temperature-measurement.htm
NOTE: "Parasitic power mode" (allowing power and comms to use the same wire)
is not supported.
TODO Implement the 'Alarm Search' feature.
*/
#include <Arduino.h>
#include <OneWire.h>
class DS18xxxTempSensor
{
//TODO Define your error reporting here, or none
#define LOGERROR(s) Serial.println(s)
//#define LOGERROR(s)
public:
// typedefs allow array size checks, much safer than just a pointer
typedef byte SNUM[8]; // Serial number, 8-byte array
typedef byte SPAD[9]; // Scratch pad, 9-byte array
DS18xxxTempSensor() { } // default constructor, call begin(oneWirePin) in setup()
DS18xxxTempSensor(int oneWirePin) { begin(oneWirePin); }
void begin(int oneWirePin);
bool findNextDevice(int familyCode, SNUM serialNumber, bool findFirst = false);
bool readTemperature(const SNUM serialNumber, float* temperature);
bool writeScratchPad(const SNUM serialNumber, const byte th,
const byte tl, const byte config = 0x7F);
bool copyToEEPROM(const SNUM serialNumber);
bool recallFromEEPROM(const SNUM serialNumber);
bool readScatchPad(const SNUM serialNumber, SPAD scratchPad);
protected:
int pin;
OneWire oneWire;
bool waitUntilDone();
};
void DS18xxxTempSensor::begin(int oneWirePin)
{
pin = oneWirePin;
oneWire.begin(pin);
}
bool DS18xxxTempSensor::findNextDevice(int familyCode, SNUM serialNumber, bool findFirst)
{
if (findFirst) {
oneWire.reset_search();
oneWire.target_search(familyCode);
}
memset(serialNumber, 0, 8);
if (oneWire.search(serialNumber)) {
// validate the 8-bit crc
if (serialNumber[7] == oneWire.crc8(serialNumber, 7))
return true;
LOGERROR("oneWire crc error");
}
return false;
}
bool DS18xxxTempSensor::readTemperature(const SNUM serialNumber, float* temperature)
{
//TODO If there are many sensors, we could issue a 'Skip ROM' command to
//start conversion on all sensors at the same time, wait about 1 second
//until it's done (you can't use waitUntilDone), then read each sensor's
//scratchpad
// start conversion and wait until complete
oneWire.reset();
oneWire.select(serialNumber);
oneWire.write(0x44);
if (!waitUntilDone())
return false;
// read scratchpad, 9 bytes
byte scratchPad[9];
if (!readScatchPad(serialNumber, scratchPad))
return false;
// get raw temperature
int t = (short)(scratchPad[1] << 8) + scratchPad[0];
// zero the "undefined" bits according to the 9/10/11/12 bit resolution
// DS18S20 has a fixed 9-bit resolution
// (I dont have one to test, so do this just to be safe)
if (serialNumber[0] == 0x10)
scratchPad[4] = 0x00;
switch (scratchPad[4] >> 5) {
case 0: // 9-bit
t &= ~7;
break;
case 1: // 10-bit
t &= ~3;
break;
case 2: // 11-bit
t &= ~1;
break;
case 3: // 12-bit, all bits valid
break;
default:
LOGERROR("Invalid config register");
return false;
}
*temperature = (float)t / 16.0f;
return true;
}
// Three bytes can be programmed into the sensor.
// These set the resolution, the high and low alarm points (TH and TL)
// or two user-defined bytes.
// The user-defined bytes could be used to identify the sensor location -
// which sensor is it? These can be programmed into EEPROM with 'copyToEEPROM'
// and are always restored at power-up.
//
// th = TH register or user byte 1
// tl = TL register or user byte 2
// config = Configuration register:
// bit 76543210
// 0xx11111 Conversion time
// 00 9 bits 93.75ms
// 01 10 bits 187.5ms
// 10 11 bits 375ms
// 11 12 bits 750ms (default)
bool DS18xxxTempSensor::writeScratchPad(const SNUM serialNumber, byte th,
byte tl, byte config)
{
oneWire.reset();
oneWire.select(serialNumber);
oneWire.write(0x4E); // write scratchpad command
oneWire.write(th); // TH register / user byte 1
oneWire.write(tl); // TL register / user byte 2
oneWire.write(config); // configuration register, see above
return true;
}
// Don't do this too often, you'll trash the EEPROM memory!
// (limited number of write cycles)
bool DS18xxxTempSensor::copyToEEPROM(const SNUM serialNumber)
{
oneWire.reset();
oneWire.select(serialNumber);
oneWire.write(0x48);
delay(10);
return true;
}
bool DS18xxxTempSensor::recallFromEEPROM(const SNUM serialNumber)
{
oneWire.reset();
oneWire.select(serialNumber);
oneWire.write(0xB8);
return waitUntilDone();
}
bool DS18xxxTempSensor::readScatchPad(const SNUM serialNumber, SPAD scratchPad)
{
memset(scratchPad, 0, 9);
oneWire.reset();
oneWire.select(serialNumber);
oneWire.write(0xBE); // read scratchpad command
bool allZeros = true;
for (int i = 0; i < 9; ++i) {
// if there's no response, read() returns a byte of 0
scratchPad[i] = oneWire.read();
if (scratchPad[i] != 0)
allZeros = false;
}
// check for a valid response
// the response is all zeros if the sensor is not present, but the crc
// is still valid (0) if all data is zeros, so we can't use the crc to
// detect a missing sensor - we must detect an "all zeros" response
if (allZeros) {
LOGERROR("No response");
return false;
}
// validate the crc
if (scratchPad[8] != oneWire.crc8(scratchPad, 8)) {
LOGERROR("Invalid crc");
return false;
}
return true;
}
// Wait until the response is not 0, with a 1-second timeout
// OneWire returns 0 if the sensor is missing
bool DS18xxxTempSensor::waitUntilDone()
{
unsigned long timeout = millis() + 1000;
while (oneWire.read() == 0) {
if (millis() > timeout) {
LOGERROR("Response timed out");
return false;
}
}
return true;
}
NOTE: "Parasitic power mode" (allowing power and communications to use the same single wire) is not supported.
LM75x, Digital Temperature Sensor and Thermal Watchdog
This is a very cheap 8-pin chip with I2C communications. It's only available in a small SMD SOIC or tiny TSSOP package, so it's usually best to buy it as a module, with the pull-up and bypass capacitors installed. It works with both 3.3V and 5V controllers and has a -55..+125°C temperature range. Up to 8 sensors can be connected on the same I2C bus.
The board has a decoupling capacitor and pull-ups on the I2C bus. If connecting several boards, you may need to remove some of the I2C pull-ups.
The LM75 and LM75A have 9-bit resolution. The LM75B has 11-bit resolution (specified as 0.125°C per step).
These also incorporate a useful Overtemperature Shutdown feature called 'OS Mode', using an open-drain 'OS' output. This allows it to work like a thermostat with hysteresis. The behaviour of the OS output is fully configurable.
There are many (rather better) pin-compatible chips available, such as the Maxim MAX7500, MAX6625, MAX6626, the DS75LV and DS7505, or the MicroChip MCP9808 (see below). But take care, these probably have different I2C commands and use different registers - I'll add details if I get to use them.
I2C Addresses
0x48..0x2F according to the A2 A1 A0 jumpers, the default is usually 0x48.
Bit
6543210
1001xxx
Here's the source code, using Arduino's standard 'Wire' library
#pragma once
// LM75 Digital Temperature Sensor
// info@muman.ch, 2025.03.08
/*
The LM75/LM75A/LM75B is an intelligent digital temperature sensor
with an I2C interface. Up to 8 can be connected on the same bus.
The LM75 works from 3.3 or 5V.
It's in an 8-pin SOIC or tiny TSSOP package.
Temperature range: -55 .. +125 C
The LM75 and LM75A have 9-bit resolution, the LM75B has 11-bit resolution.
It also incorporates and Overtemperature Shutdown feature called 'OS Mode',
using the open-drain 'OS' output. This allows it to work like a thermostat
with hysteresis. The behaviour of the OS output is fully configurable.
Data sheets
https://www.nxp.com/docs/en/data-sheet/LM75B.pdf
https://www.analog.com/media/en/technical-documentation/data-sheets/LM75.pdf
https://www.ti.com/lit/ds/symlink/lm75a.pdf
https://www.ti.com/lit/ds/symlink/lm75b.pdf
REGISTERS
---------
0x00 Temperature (Temp)
0x01 Configuration (Conf)
0x02 Hysteresis (Thyst)
0x03 Overtemperature shutdown threshold (Tos)
CONFIGURATION REGISTER
----------------------
Bit 76543210
000qqpms
qq OS fault queue programming, 00=1 01=2 10=4 11=6
p OS polarity, 0=active low, 1=active high
m OS mode, 0=comparator, 1=interrupt
s Operating mode, 0=normal, 1=shutdown
(OS = open-drain Over-temperature Shutdown output, pin 3)
I2C ADDRESSES
-------------
0x48..0x4F according to A2 A1 A0 pins, default is usually 0x48
bit 6543210
1001xxx
The boards usually have jumpers for setting the address.
*/
#include <Arduino.h>
#include <Wire.h>
class LM75TempSensor
{
//TODO Configure your error reporting here, or none
#define LOGERROR(s) Serial.println(s)
//#define LOGERROR(s)
public:
LM75TempSensor() { } // default constructor, call begin(i2cAddress) in setup()
LM75TempSensor(int i2cAddress) { begin(i2cAddress); }
void begin(int i2cAddress);
bool readTemperature(float* temperature);
bool configureOS(int queue, bool polarity, bool osMode, int tosDegC, int thystDegC);
bool readTempRegister(int* tempRegister);
bool readConfigRegister(int* configRegister);
bool readThystRegister(int* thystRegister);
bool readTosRegister(int* tosRegister);
protected:
int i2cAdds;
bool readBytes(int reg, byte* buf, int count);
bool writeBytes(int reg, byte* buf, int count);
};
void LM75TempSensor::begin(int i2cAddress)
{
i2cAdds = i2cAddress;
Wire.begin();
//Wire.setWireTimeout(100000, true); // microseconds
Wire.SetTimeout(100); // milliseconds
}
// Returns the temperature in degC, -55..+125 degC in steps of 0.125 degC
bool LM75TempSensor::readTemperature(float* temperature)
{
int t;
if (!readTempRegister(&t))
return false;
*temperature = t * 0.125f;
return true;
}
// Writes the Configuration register, Overtemperature Shutdown register and Hysteresis register
// See NXP data sheet section 7.4, https://www.nxp.com/docs/en/data-sheet/LM75B.pdf
// tos and thyst are in deg C (not 0.5degC)
bool LM75TempSensor::configureOS(int queue, bool polarity, bool osMode, int tosDegC, int thystDegC)
{
if ((unsigned)queue > 3 || thystDegC < -256 || thystDegC > 255 ||
tosDegC < -256 || tosDegC > 255) {
LOGERROR("Bad parameter");
return false;
}
// 76543210
// 000qqpms
byte config = (queue << 3) | (polarity ? 0x04 : 0) | (osMode ? 0x02 : 0);
if (!writeBytes(0x01, &config, 1)) // Conf = 0x01
return false;
byte buf[2];
tosDegC <<= 1; // resolution is 0.5deg, tos is in deg
buf[0] = (byte)(tosDegC >> 1); // MS byte, bits 8..1
buf[1] = (byte)(tosDegC << 7); // LS byte, bit 0 in bit 7 position
if (!writeBytes(0x03, buf, 2)) // Tos = 0x03
return false;
thystDegC <<= 1; // resolution is 0.5deg, thyst is in deg
thystDegC -= 1;
buf[0] = (byte)(thystDegC >> 1);
buf[1] = (byte)(thystDegC << 7);
if (!writeBytes(0x02, buf, 2)) // Thyst = 0x02
return false;
// read back and verify
int config1, thyst1, tos1;
if (!readConfigRegister(&config1) || !readTosRegister(&tos1) || !readThystRegister(&thyst1)) {
LOGERROR("Verify read failed");
return false;
}
if (config != config1 || tosDegC != tos1 || thystDegC != thyst1) {
LOGERROR("Verify failed");
return false;
}
return true;
}
// Returns the temperature as a signed integer in steps of 0.125 degC
bool LM75TempSensor::readTempRegister(int* tempRegister)
{
byte buf[2];
if (!readBytes(0x00, buf, 2))
return false;
*tempRegister = ((signed char)buf[0] << 3) | buf[1] >> 5;
return true;
}
// See NXP data sheet section 7.4.2
bool LM75TempSensor::readConfigRegister(int* configRegister)
{
byte buf[2];
if (!readBytes(0x01, buf, 2))
return false;
*configRegister = buf[0];
return true;
}
// Returns the Hysteresis register
// range -256..+255 == -128 degC..+127.5 degC in steps of 0.5 degC
bool LM75TempSensor::readThystRegister(int* thystRegister)
{
byte buf[2];
if (!readBytes(0x02, buf, 2))
return false;
*thystRegister = ((signed char)buf[0] << 1) | (buf[1] >> 7);
return true;
}
// Returns the Overtemperature shutdown register
// range -256..+255 == -128 degC..+127.5 degC in steps of 0.5 degC
bool LM75TempSensor::readTosRegister(int* tosRegister)
{
byte buf[2];
if (!readBytes(0x03, buf, 2))
return false;
*tosRegister = ((signed char)buf[0] << 1) | (buf[1] >> 7);
return true;
}
// Internal methods
bool LM75TempSensor::readBytes(int reg, byte* buf, int count)
{
Wire.beginTransmission(i2cAdds);
Wire.write((byte)reg);
if (Wire.endTransmission() != 0) {
LOGERROR("endtx failed");
return false;
}
Wire.requestFrom(i2cAdds, count);
if (Wire.readBytes(buf, count) != count) {
LOGERROR("readBytes failed");
return false;
}
return true;
}
bool LM75TempSensor::writeBytes(int reg, byte* buf, int count)
{
Wire.beginTransmission(i2cAdds);
Wire.write((byte)reg);
Wire.write(buf, count);
bool ok = Wire.endTransmission() == 0;
if (!ok)
LOGERROR("writeBytes failed");
return ok;
}
MCP9808 - ±0.5°C Maximum Accuracy Digital Sensor
This chip is from Microchip. It has the usual I2C interface, user-selectable resolution, a fully programmable open-drain high/low/critical alarm output with readable alarm status, and works with 3.3 and 5V devices.
It is similar to the LM75 described above, but it's even smaller, a bit more sophisticated, and the register usage is sightly different.
The tiny chip costs about CHF1.30 (or CHF0.94 if you buy 2'500 of them :-), the boards are around CHF5.-
The board has a decoupling capacitor, pull-ups on the I2C bus and pull-downs on the A2/A1/A0 pins. If connecting several boards, you may need to remove some of the I2C pull-ups.
#pragma once
// MCP9808 Digital Temperature Sensor
// info@muman.ch, 2025.03.10
/*
The MCP9808 is an intelligent digital temperature sensor with an I2C interface.
2.7..5.5V operation.
Temperature range: -40..+125C (-20..+100C for 0.5C accuracy)
User-programmable resolution: 0.5 0.25 0.125 0.0625 degC steps
It also has user-programmable temperature limits and an open-drain temperature alert output.
REGISTERS
Refer to the data sheet
I2C ADDRESSES
0x18..0x1F according to A2 A1 A0 pins, the default is usually 0x18.
bit 6543210
0011xxx
These boards have pins instead of jumpers for setting the address.
*/
#include <Arduino.h>
#include <Wire.h>
class MCP9808TempSensor
{
//TODO Define your error reporting here, or none
#define LOGERROR(s) Serial.println(s)
//#define LOGERROR(s)
public:
MCP9808TempSensor() { } // default constructor, call begin(i2cAddress) in setup()
MCP9808TempSensor(int i2cAddress) { begin(i2cAddress); }
void begin(int i2cAddress);
bool readIds(int* manufacturerId, int* deviceId, int* revision);
// Config register bits, all default to 0 after reset
enum : unsigned int {
THYST0 = 0 << 9, // Hysteresis, 00=0C
THYST1 = 1 << 9, // 01=1.5C
THYST3 = 2 << 9, // 02=3.0C
THYST6 = 3 << 9, // 03=6.0C
SHDN = 0x0100, // Shutdown mode, 0=continuous conversion, 1=shut down (low power mode)
CRIT_LOCK = 0x0080, // 0=unlocked, 1=locked - tcrit cannot be written
WIN_LOCK = 0x0040, // Temp window locked, 0=unlocked, 1=locked - tupper and tlower cannot be written
INT_CLEAR = 0x0020, // Interrupt Clear, 1=clear interrupt
ALERT_STAT = 0x0010, // Alert Output, 0=no alert output, 1=alert output active
ALERT_CNT = 0x0008, // Alert Output control, 0=disabled, 1=enabled
ALERT_SEL = 0x0004, // 0=Alert Output for tupper, tlower and tcrit, 1=alert output for tcrit only
ALERT_POL = 0x0002, // Alert Output polarity, 0=active low (needs pull-up), 1=active high
ALERT_MOD = 0x0001 // Alert Output mode, 0=comparator, 1=interrupt
};
// Alert bits, optionally returned by readTemperature()
enum : unsigned int {
TCRIT = 0x04,
TUPPER = 0x02,
TLOWER = 0x01
};
bool writeConfig(unsigned int config);
bool readConfig(unsigned int* config);
bool writeLimits(int tupper, int tlower, int tcrit);
bool readLimits(int* tupper, int* tlower, int* tcrit);
bool writeResolution(int resolution);
bool readResolution(int* resolution);
bool readTemperature(float* temperature, unsigned int* alertBits = NULL);
protected:
int i2cAdds;
byte buf[2]; // this is used by all methods
int bufToInt();
void intToBuf(int i);
bool readBytes(int reg, byte* buf, int count);
bool writeBytes(int reg, byte* buf, int count);
};
void MCP9808TempSensor::begin(int i2cAddress)
{
i2cAdds = i2cAddress;
Wire.begin();
Wire.setWireTimeout(100000, true); // microseconds
//Wire.setTimeout(100); // milliseconds
}
bool MCP9808TempSensor::readIds(int* manufacturerId, int* deviceId, int* revision)
{
if (manufacturerId) {
if (!readBytes(0x06, buf, 2))
return false;
*manufacturerId = (buf[0] << 8) + buf[1];
}
if (!readBytes(0x07, buf, 2))
return false;
if (deviceId)
*deviceId = buf[0];
if (revision)
*revision = buf[1];
return true;
}
bool MCP9808TempSensor::writeConfig(unsigned int config)
{
buf[0] = (byte)(config >> 8); // msb first
buf[1] = (byte)config;
return writeBytes(0x01, buf, 2);
}
bool MCP9808TempSensor::readConfig(unsigned int* config)
{
if (!readBytes(0x01, buf, 2))
return false;
*config = ((unsigned int)buf[0] << 8) + buf[1];
return true;
}
// Values are in degC (not 0.25C steps)
bool MCP9808TempSensor::writeLimits(int tupper, int tlower, int tcrit)
{
// values are in steps of 0.25C, multiply-by-4 to get degC in 0.25degC steps
intToBuf(tupper * 4);
if (!writeBytes(0x02, buf, 2))
return false;
intToBuf(tlower * 4);
if (!writeBytes(0x03, buf, 2))
return false;
intToBuf(tcrit * 4);
if (!writeBytes(0x04, buf, 2))
return false;
return true;
}
// Returned values are in degC (not 0.25C steps)
bool MCP9808TempSensor::readLimits(int* tupper, int* tlower, int* tcrit)
{
// values are in steps of 0.25C, divide-by-4 to get degC
if (!readBytes(0x02, buf, 2))
return false;
*tupper = bufToInt() / 4;
if (!readBytes(0x03, buf, 2))
return false;
*tlower = bufToInt() / 4;
if (!readBytes(0x04, buf, 2))
return false;
*tcrit = bufToInt() / 4;
return true;
}
// Resolution and conversion time
// 00 = 0.5C 30ms
// 01 = 0.25C 65ms
// 10 = 0.125C 130ms
// 11 = 0.0625C 250ms Default
bool MCP9808TempSensor::writeResolution(int resolution)
{
buf[0] = (byte)resolution & 0x03;
return writeBytes(0x08, buf, 1);
}
bool MCP9808TempSensor::readResolution(int* resolution)
{
if (!readBytes(0x08, buf, 1))
return false;
*resolution = buf[0] & 0x03;
return true;
}
// Returns the temperature in deg C, resolution is as programmed above
bool MCP9808TempSensor::readTemperature(float* temperature, unsigned int* alertBits)
{
if (!readBytes(0x05, buf, 2))
return false;
if (alertBits)
*alertBits = buf[0] >> 5;
bool negative = (buf[0] & 0x10) ? 1 : 0;
int msb = buf[0] & 0x0f; // clear flag and sign bits
int lsb = buf[1];
*temperature = (float)(msb << 4) + ((float)lsb / 16.0f);
return true;
}
// Internal methods
int MCP9808TempSensor::bufToInt()
{
// buf [0] [1]
// bit FEDCBA98 76543210
// xxxA9876 543210xx
int t = (buf[0] << 11) + ((unsigned int)buf[1] << 3);
return t >> 5;
}
void MCP9808TempSensor::intToBuf(int i)
{
// buf [0] [1]
// bit FEDCBA98 76543210
// xxxA9876 543210xx
buf[0] = (byte)(i >> 6) & 0x1f;
buf[1] = (byte)(i << 2);
}
bool MCP9808TempSensor::readBytes(int reg, byte* buf, int count)
{
Wire.beginTransmission(i2cAdds);
Wire.write((byte)reg);
if (Wire.endTransmission() != 0) {
LOGERROR("endtx failed");
return false;
}
Wire.requestFrom(i2cAdds, count);
if (Wire.readBytes(buf, count) != count) {
LOGERROR("readBytes failed");
return false;
}
return true;
}
bool MCP9808TempSensor::writeBytes(int reg, byte* buf, int count)
{
Wire.beginTransmission(i2cAdds);
Wire.write((byte)reg);
Wire.write(buf, count);
bool ok = Wire.endTransmission() == 0;
if (!ok)
LOGERROR("writeBytes failed");
return ok;
}
D6T1A01 Omron MEMS Thermal Imaging Sensor
With these sensors, you don't need to have the sensor in direct contact with the item being measured. A MEMS (micro-electromechanical system) Thermal Sensor (an infrared sensor) measures the surface temperature of objects without touching them, a thermopile element absorbs radiant energy from the object and converts it to a digital value. In effect, these sensors can "see" the temperature on a small thermal matrix. The cheapest (the D6T1A01) has just one "pixel". The more expensive models have a matrix of up to 1024 pixels (32x32), so you can determine the approximate shape and motion of warm objects. (The MLX90640 is all the rage, but it costs CHF80!)
The values returned for each pixel are multiplied by 10, e.g. 252 = 25.2°C.
The temperature range for the [prohibitively] expensive models is -40..+200°C, with a 0.1°C resolution.
The cheap D6T1A01 model has a smaller range (5..50°C), but I tested it with the freezer and a cigarette lighter and it returned temperatures in the range -14..+125°C, with temperature readings updated every 100ms. I assume that the temperature it "sees" reduces with distance and the size of the temperature source, so 125°C could easily be greater than 200°C - but this must be tested.
These are (currently) 5V devices, so you must use use pull-ups to 3.3V (NOT TO 5V) on the open-drain SCL and SDA lines when using them with a 3.3V controller.
The D6T's I2C slave addresses CANNOT be changed, they are all fixed at 0x0A. For more than one sensor you can use the Software I2C library so any pins can be used for SDA/SCL. Or use an I2C multiplexer or bus switch. Or make a simple mux for the SDA signal with MOSFETs or a cheap CD4066 or 74HC4066 quad bilateral switch (the SCL clock signal can be shared). I'll add details of these circuits later.
#pragma once
// D6T1A01 Omron MEMS Thermal Sensor
// info@muman.ch, 2025.03.11
/*
TODO Update this to support all D6T models and read the temperature matrix.
There are several models of this sensor. Some return a matrix of temperatures,
the D6T1A01 has a single point and returns a single temperature reading (it
sees one pixel).
This sensor is able to detect the temperature of the objects it sees in
its matrix. It is quite accurate and has a vey fast response.
These are (currently) 5V devices, so you must use use pull-ups to 3.3V (NOT 5V)
on the SCL and SDA lines if using them with a 3.3V controller.
The D6T1A01 model updates the temperature readings every 100ms.
The D6T's I2C slave address CANNOT be changed, they are all fixed at 0x0A.
To use more than one sensor you can use a Software I2C library so any pins
can be used for SCL/SDA. Or use an I2C multiplexer or bus switch, or a simple
mux for the SDA signal using mosfets or a cheap CD4066 or 74HC4066 quad
bilateral switch.
D6T Users Manual and programming guide
https://omronfs.omron.com/en_US/ecb/products/pdf/en_D6T_users_manual.pdf
Omron D6T MEMS Thermal Sensors Catalog
https://omronfs.omron.com/en_US/ecb/products/pdf/en_D6T_catalog.pdf
*/
#include <Arduino.h>
#include <Wire.h>
class D6TA01ThermalSensor
{
//TODO Define your error reporting here, or none
#define LOGERROR(s) Serial.println(s)
//#define LOGERROR(s)
public:
D6TA01ThermalSensor() { } // default constructor, call begin(i2cAddress) in setup()
D6TA01ThermalSensor(int i2cAddress) { begin(i2cAddress); }
void begin(int i2cAddress);
bool readValue(int* referenceTemp, int* measuredTemp);
protected:
int i2cAdds;
bool readBytes(int reg, byte* buf, int count);
byte crc8(byte data);
bool checkPEC(byte* buf, int len);
};
void D6TA01ThermalSensor::begin(int i2cAddress)
{
i2cAdds = i2cAddress;
Wire.begin();
// the default is no timeout
Wire.setWireTimeout(100000, true); // microseconds
//Wire.setTimeout(100); // milliseconds
}
// Returned values are degC * 10
// referenceTemp is the temperature of the D6T module itself
bool D6TA01ThermalSensor::readValue(int* referenceTemp, int* measuredTemp)
{
byte buf[5];
if (!readBytes(0x4C, buf, 5))
return false;
*referenceTemp = ((int)buf[1] << 8) + buf[0];
*measuredTemp = ((int)buf[3] << 8) + buf[2];
bool crcOk = checkPEC(buf, 4);
if (!crcOk)
LOGERROR("invalid crc");
return crcOk;
}
// Internal methods
bool D6TA01ThermalSensor::readBytes(int reg, byte* buf, int count)
{
Wire.beginTransmission(i2cAdds);
Wire.write((byte)reg);
if (Wire.endTransmission() != 0) {
LOGERROR("endtx failed");
return false;
}
Wire.requestFrom(i2cAdds, count);
if (Wire.readBytes(buf, count) != count) {
LOGERROR("readBytes failed");
return false;
}
return true;
}
// PEC = Packet Error Check
bool D6TA01ThermalSensor::checkPEC(byte* buf, int len)
{
// crc includes the message
byte crc = crc8(0x14);
crc = crc8(0x4C ^ crc);
crc = crc8(0x15 ^ crc);
for (int i = 0; i < len; i++) {
crc = crc8(buf[i] ^ crc);
}
// last byte is crc
return crc == buf[len];
}
//TODO use look-up table for speed?
byte D6TA01ThermalSensor::crc8(byte data)
{
for (int i = 0; i < 8; i++) {
byte temp = data;
data <<= 1;
if (temp & 0x80)
data ^= 0x07;
}
return data;
}
References
(If a link is broken, Google the title or the App Note number.)