1
0
mirror of https://github.com/RIOT-OS/RIOT.git synced 2025-12-15 01:23:49 +01:00

drivers/max31865: implement the driver

Implement the driver for the MAX31865 RTD-to-digital converter.
This commit is contained in:
David Picard 2025-04-25 16:16:37 +02:00
parent 803d60c3bc
commit c3a14457f3
9 changed files with 1110 additions and 0 deletions

274
drivers/include/max31865.h Normal file
View File

@ -0,0 +1,274 @@
/*
* Copyright (C) 2025 David Picard
*
* This file is subject to the terms and conditions of the GNU Lesser
* General Public License v2.1. See the file LICENSE in the top level
* directory for more details.
*/
#pragma once
/**
* @defgroup drivers_max31865 MAX31865 RTD-to-Digital converter driver
* @ingroup drivers_sensors
* @brief Driver for the SPI RTD-to-Digital converter MAX31865.
*
* \section sec_max31865_ovrvw Overview
* The MAX31865 is a resistance-to-digital
* converter optimized for platinum resistance temperature
* detectors (RTDs). An external resistor sets the sensitivity
* for the RTD being used and a precision delta-sigma ADC
* converts the ratio of the RTD resistance to the reference
* resistance on the board, to a 15-bit word.
*
* - Compatible with 2-, 3-, and 4-Wire Sensor Connections
* - 15-Bit ADC resolution; resolution 0.03125°C
* - Total accuracy over all operating conditions: 0.5°C (0.05% FS) max
* - 21 ms max conversion time
*
* The RTD register and the threshold registers represent the ratio
* of the RTD resistance to the reference resistance.
*
* @note See the
* <a href="https://www.analog.com/media/en/technical-documentation/data-sheets/MAX31865.pdf">
* datasheet</a> for more information.
*
* \section sec_max31865_lut Lookup table
*
* In order to convert the ADC code to temperature with a decent precision
* and low CPU usage by using fixed point computation, this driver uses a lookup table.
* The values of the lookup table depend on the type of RTD, the reference
* resistance on the board and the temperature range.
* A default lookup table for a Pt100 sensor, a reference resistance of 330&Omega;
* and a temperature range of -200..+650°C is provided.
* With this lookup table, the standard deviation of the computation
* error over the -200..+650°C range is 0.011°C and the maximum error
* is 0.039°C.
*
* If the default lookup doesn't fit the application, the user can generate
* a custom one with the Python script `drivers/max31865/dist/genlut.py`.
* Type `genlut.py -h` for help.
* Move the generated header file to the application project directory.
* In the application code, include `max31865_lut.h`.
* The header files must be included in the following order:
*
* @code
* #include "max31865.h"
* #include "max31865_lut.h"
* #include "max31865_params.h"
* @endcode
*
* The lookup table in the application directory will override the default one.
*
* \section sec_max31865_usage Usage
*
* The default configuration is set in \ref max31865_params.h as a
* \ref max31865_params_t structure.
* Most of them can be changed using the preprocessor definitions either
* in the board definition in `board.h` or in the Makefile of the application
* using the `CFLAGS` variable.
* In particular, let the SPI device and chip select pin fit the board.
*
* @code
* define MAX31865_PARAM_SPI (SPI_DEV(0))
* define MAX31865_PARAM_CS_PIN (GPIO_PIN(0, 5))
* @endcode
*
* The easiest way to read the temperature is to call #max31865_read().
* In critical applications, where a sensor failure may cause damage to the
* system, calling #max31865_read_raw(),
* #max31865_raw_to_data() and #max31865_detect_fault() independently
* provides a better control on fault detection.
*
* @{
*
* @file
*
* @author David Picard
*/
/* Add header includes here */
#ifdef __cplusplus
extern "C" {
#endif
#include "periph/spi.h"
/**
* @brief Lookup table column indexes
*
* The lookup table is a 2D array.
* This enum defines the indexes of the columns of the table.
* The conversion from ADC code to temperature is done with a linear interpolation
* between two lines of the table.
* The coefficients at line n are valid for the temperature range between line n
* and line n+1.
*
* T = a<sub>0</sub> + a<sub>1</sub> &times; code
*/
typedef enum {
MAX31865_LUTCOL_CODE = 0, /**< ADC code column index */
MAX31865_LUTCOL_TEMP = 1, /**< Temperature column index (µ°C) */
MAX31865_LUTCOL_A0 = 2, /**< a<sub>0</sub> coefficient column index */
MAX31865_LUTCOL_A1 = 3, /**< a<sub>1</sub> coefficient column index */
MAX31865_LUTCOL_NUMOF /**< Number of columns in the lookup table */
} max31865_lutcols_t;
/**
* @brief Device initialization parameters
*
* - Use the bits defined in \ref drivers_max31865_constants_regcfg to build the
* configuration byte #max31865_params_t.cfg_byte.
* - The high and low thresholds must be set in units of 0.01 °C, e.g. `12000` for 120 °C.
* - #max31865_params_t.lut_numlines is defined in the header file generated
* by the Python script `drivers/max31865/dist/genlut.py`.
*/
typedef struct {
spi_t spi; /**< SPI device */
spi_cs_t cs_pin; /**< Chip select pin */
uint8_t cfg_byte; /**< Initial value of the configuration register */
int32_t temp_low_threshold; /**< Low threshold temperature (c°C) */
int32_t temp_high_threshold; /**< High threshold temperature (c°C) */
const int32_t (*lut)[][MAX31865_LUTCOL_NUMOF]; /**< Lookup table */
const int lut_numlines; /**< Number of lines in the lookup table */
} max31865_params_t;
/**
* @brief Device descriptor for the driver
*/
typedef struct {
const max31865_params_t *params; /**< device configuration */
} max31865_t;
/**
* @brief Fault status of the MAX31865
*/
typedef enum {
MAX31865_FAULT_NO_FAULT = 0, /**< No fault */
MAX31865_FAULT_RTD_HIGH = 1, /**< The RTD value is too high */
MAX31865_FAULT_RTD_LOW = 2, /**< The RTD value is too small */
MAX31865_FAULT_CIRCUIT = 3, /**< Open or shorted circuit */
MAX31865_FAULT_VOLTAGE = 4 /**< Overvoltage or undervoltage on the FORCE or RTD pin */
} max31865_fault_t;
/**
* @brief Initialize the given device
*
* @param[in,out] dev Device descriptor of the driver
* @param[in] params Initialization parameters
*
* @retval 0 on success
* @retval -ENXIO invalid SPI device
* @retval -EINVAL invalid SPI CS pin/line
*/
int max31865_init(max31865_t *dev, const max31865_params_t *params);
/**
* @brief Clear the fault flag
*
* @param[in] dev Device descriptor of the driver
* @param[out] config If not NULL, set to the value of the config register
*
* Call this function if after #max31865_read() or #max31865_detect_fault() if
* either of them reports a fault.
*/
void max31865_clear_fault(const max31865_t *dev, uint8_t *config);
/**
* @brief Read data from the MAX31865. This is a shortcut to read raw data
* and parse it to the data structure.
*
* @param[in] dev Device descriptor of the driver
* @param[out] rtd_temperature_cdegc Temperature in centi-degrees Celsius (0.01°C)
*
* @pre \a dev and \a data must not be NULL
*
* This function does a minimal error check.
* To get more details on the fault, call
* #max31865_detect_fault().
*
* @retval 0 on success
* @retval -EIO if an error was detected by the MAX31865
*/
int max31865_read(const max31865_t *dev, int32_t *rtd_temperature_cdegc);
/**
* @brief Read raw data from the MAX31865
* @param[in] dev Device descriptor of the driver
* @param[out] raw_data Value of the RTD registers
*
* \a raw_data is a left-justified 15-bit word, representing the ratio
* of the RTD resistance to the reference resistance.
* Bit 0 is a status bit.
*
* @retval 0 on success.
* @retval -EIO if an error was detected by the MAX31865.
* In this case, the temperature is not valid.
* Call #max31865_detect_fault() for more details.
*/
int max31865_read_raw(const max31865_t *dev, uint16_t *raw_data);
/**
* @brief Convert the raw data from the MAX31865 temperature
*
* @param[in] dev Device descriptor of the driver
* @param[in] raw_data Raw data from the MAX31865
* @param[out] rtd_temperature_cdegc Temperature in centi-degrees Celsius (0.01°C)
*
* @pre @p data must not be NULL
*
* @retval 0 on success
* @retval -EINVAL on error
*/
int max31865_raw_to_data(const max31865_t *dev, uint16_t raw_data, int32_t *rtd_temperature_cdegc);
/**
* @brief Run an automatic fault-detection cycle
* @param[in] dev Device descriptor of the driver
* @param[out] flt_code Fault code
*
* Run a full fault-detection cycle, in automatic mode.
* The automatic mode implies that the input filter of the ADC has
* a time constant smaller than 100µs.
* This is the case if the capacitor across the RTD has the
* value which is recommended in the datasheet.
*
* The execution time of this function is at least 100µs, until the
* fault-detection cycle completes.
* It polls the configuration register to check the completion of the cycle.
*
* @pre V<sub>BIAS</sub> must be on for at least 5 time constants.
*
* @retval 0 on success.
* @retval -EIO on error.
*/
int max31865_detect_fault(const max31865_t *dev, max31865_fault_t *flt_code);
/**
* @brief Switch V<sub>BIAS</sub> on or off
* @param[in] dev Device descriptor of the driver
* @param[in] enable Set to \c true to switch on, or to \c false to switch off V<sub>BIAS</sub>.
*
* The bias current of the RTD can be switched off to save power.
* After switching it on, wait for at least 10 time constants -- 10 &times; 100µs,
* if the capacitance across the RTD is as recommended in the datasheet --
* before measuring a valid temperature.
*/
void max31865_switch_vbias(const max31865_t *dev, bool enable);
/**
* @brief Start a one-shot conversion
* @param[in] dev Device descriptor of the driver
*
* The user must wait at least 52ms in 60Hz or 62.5ms in 50Hz filter mode
* for the conversion to complete, before reading the conversion result.
*/
void max31865_oneshot(const max31865_t *dev);
#ifdef __cplusplus
}
#endif
/** @} */

View File

@ -0,0 +1 @@
include $(RIOTMAKE)/driver_with_saul.mk

View File

@ -0,0 +1,4 @@
FEATURES_REQUIRED += periph_spi
USEMODULE += ztimer
USEMODULE += ztimer_usec

View File

@ -0,0 +1,2 @@
USEMODULE_INCLUDES_max31865 := $(LAST_MAKEFILEDIR)/include
USEMODULE_INCLUDES += $(USEMODULE_INCLUDES_max31865)

View File

@ -0,0 +1,220 @@
/*
* Copyright (C) 2025 David Picard
*
* This file is subject to the terms and conditions of the GNU Lesser
* General Public License v2.1. See the file LICENSE in the top level
* directory for more details.
*/
#pragma once
/**
* @defgroup drivers_max31865_constants MAX31865 constants
* @ingroup drivers_max31865
* @{
*
* @file
* @brief Internal addresses, registers and constants
*
* @author David Picard
*/
#ifdef __cplusplus
extern "C" {
#endif
/** Maximum number of bytes that can be read/written from/to the device */
#define MAX31865_DATA_SIZE (8)
/** Reference resistance (ohm) */
#define MAX31865_REF_RESISTANCE_DEFAULT (220)
/** Default value of the high threshold register */
#define MAX31865_HTHRES_DEFAULT (0xFFFF)
/** Default value of the low threshold register */
#define MAX31865_LTHRES_DEFAULT (0x0000)
/**
* @defgroup drivers_max31865_reg_addresses Register addresses
* @ingroup drivers_max31865_constants
* @{
*
* Register | Read address | Write address
* --- | --- | ---
* Configuration | 0x00 | 0x80
* RTD MSB | 0x01 | N/A
* RTD LSB | 0x02 | N/A
* RTD high threshold MSB | 0x03 | 0x83
* RTD high threshold LSB | 0x04 | 0x84
* RTD low threshold MSB | 0x05 | 0x85
* RTD low threshold LSB | 0x06 | 0x86
* Fault | 0x07 | N/A
*/
#define MAX31865_ADDR_CFG_R (0x00) /**< Configuration register, read address */
#define MAX31865_ADDR_CFG_W (0x80) /**< Configuration register, write address */
#define MAX31865_ADDR_RTD_MSB (0x01) /**< RTD MSB, read-only */
#define MAX31865_ADDR_RTD_LSB (0x02) /**< RTD LSB, read-only */
#define MAX31865_ADDR_RTD_HTHRES_MSB_R (0x03) /**< RTD high threshold MSB, read address */
#define MAX31865_ADDR_RTD_HTHRES_MSB_W (0x83) /**< RTD high threshold MSB, write address */
#define MAX31865_ADDR_RTD_HTHRES_LSB_R (0x04) /**< RTD high threshold LSB, read address */
#define MAX31865_ADDR_RTD_HTHRES_LSB_W (0x84) /**< RTD high threshold LSB, write address */
#define MAX31865_ADDR_RTD_LTHRES_MSB_R (0x05) /**< RTD low threshold MSB, read address */
#define MAX31865_ADDR_RTD_LTHRES_MSB_W (0x85) /**< RTD low threshold MSB, write address */
#define MAX31865_ADDR_RTD_LTHRES_LSB_R (0x06) /**< RTD low threshold LSB, read address */
#define MAX31865_ADDR_RTD_LTHRES_LSB_W (0x86) /**< RTD low threshold LSB, write address */
#define MAX31865_ADDR_FAULT (0x07) /**< Fault status register, read-only */
/** @} */
/**
* @defgroup drivers_max31865_constants_regcfg Configuration register bits
* @ingroup drivers_max31865_constants
* @{
*/
/**
* @brief Enable Vbias
*/
#define MAX31865_CFG_VBIAS_ON (0b10000000)
/**
* @brief Enable automatic conversion mode
*
* The conversion rate is the same as the selected filter mode,
* \em i.e. either 50 or 60Hz.
*/
#define MAX31865_CFG_CONV_AUTO (0b01000000)
/**
* @brief Trigger a single conversion
*
* The automatic conversion mode must be off.
* Single conversion time is 52ms in 60Hz or 62.5ms in 50Hz filter mode.
*/
#define MAX31865_CFG_1SHOT (0b00100000)
/**
* @brief 3-wire resistor connection
*
* Clear the bit for 2- or 4-wire connections.
*/
#define MAX31865_CFG_3WIRE (0b00010000)
/**
* @brief Clear fault condition
*/
#define MAX31865_CFG_CLEAR_FAULT (0b00000010)
/**
* @brief Filter out 50Hz if set, or 60Hz otherwise
*
* Clear the bit for 60Hz filter mode.
*/
#define MAX31865_CFG_FILTER_50HZ (0b00000001)
/**
* @brief Fault detection bit mask
*
* To check the status of the fault detection cycle,
* mask the bits of the configuration register and
* compare the value with #MAX31865_CFG_FLTDET_IDLE,
* #MAX31865_CFG_FLTDET_AUTO_START, #MAX31865_CFG_FLTDET_MANU_START
* or #MAX31865_CFG_FLTDET_MANU_START.
*/
#define MAX31865_CFG_FLTDET_MASK (0b00001100)
/**
* @brief No fault detection is running
*/
#define MAX31865_CFG_FLTDET_IDLE (0b00000000)
/**
* @brief Start a fault detection with an automatic delay (about 550µs)
*/
#define MAX31865_CFG_FLTDET_AUTO_START (0b00000100)
/**
* @brief Start a fault detection with a manually controlled delay
*/
#define MAX31865_CFG_FLTDET_MANU_START (0b00001000)
/**
* @brief Terminate fault detection with a manually controlled delay
*/
#define MAX31865_CFG_FLTDET_MANU_STOP (0b00001100)
/** @} */ /* end of group drivers_max31865_constants_regcfg */
/**
* @defgroup drivers_max31865_constants_regflt Fault register bits
* In normal conditions, all bits are cleared.
* Bits 0 and 1 are not used.
*
* During measurement, the reference resistor and the RTD are connected in
* series.
* During a fault detection cycle initiated by the master, the IC can open an
* internal switch on the FORCE- pin.
* - When the FORCE- switch is closed, current should flow through the reference
* resistor and V<sub>REFIN-</sub> is expected to be less than 0.85 &times; V<sub>BIAS</sub>.
* - When the FORCE- switch is open, no current should flow through the reference
* resistor nor the RTD and both V<sub>REFIN-</sub> and V<sub>RTDIN-</sub> are
* expected to be greater than 0.85 &times; V<sub>BIAS</sub>.
*
* The table below sums up the fault conditions when the FORCE- switch is open.
* The two left-most column reflect the values of the bits masked by
* #MAX31865_FLT_REF_FO and #MAX31865_FLT_RTD_FO.
*
* V<sub>REFIN-</sub> | V<sub>RTDIN-</sub> | Fault
* --- | --- | ---
* 0 | 0 | no error
* 0 | 1 | RTD disconnected from the RTD- pin
* 1 | 0 | RTD- is shorted to VCC; very unlikely
* 1 | 1 | RTD disconnected from the RTD+ pin or shorted
*
* In the last case, it can be asserted that the RTD terminals are shorted together if the bit
* masked by #MAX31865_FLT_THRESLOW is set.
*
* @ingroup drivers_max31865_constants
* @{
*/
/**
* @brief Fault: RTD is greater than the high threshold
*/
#define MAX31865_FLT_THRESHIGH (0b10000000)
/**
* @brief Fault: RTD is less than the low threshold
*/
#define MAX31865_FLT_THRESLOW (0b01000000)
/**
* @brief Fault: V<sub>REFIN-</sub> > 0.85 &times; V<sub>BIAS</sub> when FORCE- is closed
*/
#define MAX31865_FLT_REF_FC (0b00100000)
/**
* @brief Fault: V<sub>REFIN-</sub> < 0.85 &times; V<sub>BIAS</sub> when FORCE- is open
*/
#define MAX31865_FLT_REF_FO (0b00010000)
/**
* @brief Fault: V<sub>RTDIN-</sub> < 0.85 &times; &times; V<sub>BIAS</sub> when FORCE- is open
*/
#define MAX31865_FLT_RTD_FO (0b00001000)
/**
* @brief Fault: overvoltage or undervoltage condition
*
* Overvoltage or undervoltage condition on at least one of the
* FORCE or RTD pins.
*/
#define MAX31865_FLT_VOLTAGE (0b00000100)
/** @} */ /* end of group drivers_max31865_constants_regflt */
#ifdef __cplusplus
}
#endif
/** @} */

View File

@ -0,0 +1,140 @@
/*
* Copyright (C) 2025 David Picard
*
* This file is subject to the terms and conditions of the GNU Lesser
* General Public License v2.1. See the file LICENSE in the top level
* directory for more details.
*/
#pragma once
/**
* @ingroup drivers_max31865
* @{
*
* @file
* @brief Default lookup table
*
* @author David Picard
*/
#include "container.h"
#ifdef __cplusplus
extern "C" {
#endif
#ifndef MAX31865_CUSTOM_LUT_PROVIDED
/** Number of lines in #max31865_lut */
#define MAX31865_LUT_NUMLINES (ARRAY_SIZE(max31865_lut))
/**
* @ingroup drivers_max31865
* @brief Default lookup table for temperature conversion
*
* The columns of this table should be indexed by #max31865_lutcols_t.
*
* This lookup table was generated by @p genlut.py, provided with
* the max31865 driver.
*
* Lookup table parameters:
* - RTD @ 0°C = 100&Omega;
* - RREF = 330&Omega;
* - Tmin = -200°C
* - Tmax = 650°C
*/
static const int32_t max31865_lut[][4] =
{ { 0x0E5E, -200000000, -242967290, 11682 },
{ 0x11B6, -190000000, -243466981, 11792 },
{ 0x1506, -180000000, -244071429, 11905 },
{ 0x184E, -170000000, -244425837, 11962 },
{ 0x1B92, -160000000, -245036145, 12048 },
{ 0x1ED0, -150000000, -245496368, 12107 },
{ 0x220A, -140000000, -246268293, 12195 },
{ 0x253E, -130000000, -247125307, 12285 },
{ 0x286C, -120000000, -247438424, 12315 },
{ 0x2B98, -110000000, -248461538, 12407 },
{ 0x2EBE, -100000000, -248830846, 12438 },
{ 0x31E2, -90000000, -249625000, 12500 },
{ 0x3502, -80000000, -250477387, 12563 },
{ 0x381E, -70000000, -251388889, 12626 },
{ 0x3B36, -60000000, -251873418, 12658 },
{ 0x3E4C, -50000000, -252385787, 12690 },
{ 0x4160, -40000000, -253469388, 12755 },
{ 0x4470, -30000000, -254040921, 12788 },
{ 0x477E, -20000000, -254641026, 12821 },
{ 0x4A8A, -10000000, -255269923, 12853 },
{ 0x4D94, 0, -256589147, 12920 },
{ 0x509A, 10000000, -256589147, 12920 },
{ 0x53A0, 20000000, -258025974, 12987 },
{ 0x56A2, 30000000, -258776042, 13021 },
{ 0x59A2, 40000000, -259556136, 13055 },
{ 0x5CA0, 50000000, -260366492, 13089 },
{ 0x5F9C, 60000000, -262052632, 13158 },
{ 0x6294, 70000000, -262052632, 13158 },
{ 0x658C, 80000000, -263862434, 13228 },
{ 0x6880, 90000000, -264801061, 13263 },
{ 0x6B72, 100000000, -265771277, 13298 },
{ 0x6E62, 110000000, -266773333, 13333 },
{ 0x7150, 120000000, -267807487, 13369 },
{ 0x743C, 130000000, -269946237, 13441 },
{ 0x7724, 140000000, -269946237, 13441 },
{ 0x7A0C, 150000000, -272216216, 13514 },
{ 0x7CF0, 160000000, -273387534, 13550 },
{ 0x7FD2, 170000000, -274592391, 13587 },
{ 0x82B2, 180000000, -275831063, 13624 },
{ 0x8590, 190000000, -277103825, 13661 },
{ 0x886C, 200000000, -278410959, 13699 },
{ 0x8B46, 210000000, -281101928, 13774 },
{ 0x8E1C, 220000000, -282486188, 13812 },
{ 0x90F0, 230000000, -283905817, 13850 },
{ 0x93C2, 240000000, -285361111, 13889 },
{ 0x9692, 250000000, -286852368, 13928 },
{ 0x9960, 260000000, -288379888, 13966 },
{ 0x9C2C, 270000000, -291516854, 14045 },
{ 0x9EF4, 280000000, -291516854, 14045 },
{ 0xA1BC, 290000000, -294802260, 14124 },
{ 0xA480, 300000000, -296487252, 14164 },
{ 0xA742, 310000000, -298210227, 14205 },
{ 0xAA02, 320000000, -299971510, 14245 },
{ 0xACC0, 330000000, -301771429, 14286 },
{ 0xAF7C, 340000000, -305459770, 14368 },
{ 0xB234, 350000000, -307348703, 14409 },
{ 0xB4EA, 360000000, -307348703, 14409 },
{ 0xB7A0, 370000000, -311275362, 14493 },
{ 0xBA52, 380000000, -313284884, 14535 },
{ 0xBD02, 390000000, -317397661, 14620 },
{ 0xBFAE, 400000000, -317397661, 14620 },
{ 0xC25A, 410000000, -319530792, 14663 },
{ 0xC504, 420000000, -323893805, 14749 },
{ 0xC7AA, 430000000, -326124260, 14793 },
{ 0xCA4E, 440000000, -328397626, 14837 },
{ 0xCCF0, 450000000, -330714286, 14881 },
{ 0xCF90, 460000000, -333074627, 14925 },
{ 0xD22E, 470000000, -337897898, 15015 },
{ 0xD4C8, 480000000, -337897898, 15015 },
{ 0xD762, 490000000, -342900302, 15106 },
{ 0xD9F8, 500000000, -345454545, 15152 },
{ 0xDC8C, 510000000, -348054711, 15198 },
{ 0xDF1E, 520000000, -350701220, 15244 },
{ 0xE1AE, 530000000, -353394495, 15291 },
{ 0xE43C, 540000000, -356134969, 15337 },
{ 0xE6C8, 550000000, -361728395, 15432 },
{ 0xE950, 560000000, -364582043, 15480 },
{ 0xEBD6, 570000000, -367484472, 15528 },
{ 0xEE5A, 580000000, -370436137, 15576 },
{ 0xF0DC, 590000000, -373437500, 15625 },
{ 0xF35C, 600000000, -376489028, 15674 },
{ 0xF5DA, 610000000, -382712934, 15773 },
{ 0xF854, 620000000, -382712934, 15773 },
{ 0xFACE, 630000000, -389142857, 15873 },
{ 0xFD44, 640000000, -392420382, 15924 },
{ 0xFFB8, 650000000, -395750799, 15974 } };
#endif /* MAX31865_CUSTOM_LUT_PROVIDED */
#ifdef __cplusplus
}
#endif
/** @} */

View File

@ -0,0 +1,103 @@
/*
* Copyright (C) 2025 David Picard
*
* This file is subject to the terms and conditions of the GNU Lesser
* General Public License v2.1. See the file LICENSE in the top level
* directory for more details.
*/
#pragma once
/**
* @ingroup drivers_max31865
*
* @{
* @file
* @brief Default configuration for the MAX31865 driver
*
* @author David Picard
*/
#include "board.h"
#include "max31865.h"
#include "max31865_internal.h"
#include "max31865_lut.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @name Default configuration for the MAX31865 driver
* @{
*/
/**
* @brief Default SPI bus for the MAX31865 driver
*/
#ifndef MAX31865_PARAM_SPI
# define MAX31865_PARAM_SPI (SPI_DEV(0))
#endif
/**
* @brief Default CS pin for the MAX31865 driver
*/
#ifndef MAX31865_PARAM_CS_PIN
# define MAX31865_PARAM_CS_PIN (GPIO_PIN(0, 5))
#endif
/**
* @brief Configuration byte definition
*/
#ifndef MAX31865_PARAM_CFG_BYTE
# define MAX31865_PARAM_CFG_BYTE MAX31865_CFG_VBIAS_ON | \
MAX31865_CFG_CONV_AUTO | \
MAX31865_CFG_3WIRE | \
MAX31865_CFG_FILTER_50HZ
#endif
/**
* @brief Low temperature threshold
*/
#ifndef MAX31865_PARAM_TEMP_THRES_LOW
# define MAX31865_PARAM_TEMP_THRES_LOW -19900
#endif
/**
* @brief High temperature threshold
*/
#ifndef MAX31865_PARAM_TEMP_THRES_HIGH
# define MAX31865_PARAM_TEMP_THRES_HIGH 64900
#endif
/**
* @brief Default parameters for the MAX31865 driver
*/
#ifndef MAX31865_PARAMS
# define MAX31865_PARAMS { \
.spi = MAX31865_PARAM_SPI, \
.cs_pin = MAX31865_PARAM_CS_PIN, \
.cfg_byte = MAX31865_PARAM_CFG_BYTE, \
.temp_low_threshold = MAX31865_PARAM_TEMP_THRES_LOW, \
.temp_high_threshold = MAX31865_PARAM_TEMP_THRES_HIGH, \
.lut = &max31865_lut, \
.lut_numlines = MAX31865_LUT_NUMLINES, \
}
#endif
/**@}*/
/**
* @brief Configuration structs for the MAX31865 driver
*/
static const max31865_params_t max31865_params[] =
{
MAX31865_PARAMS
};
#ifdef __cplusplus
}
#endif
/** @} */

347
drivers/max31865/max31865.c Normal file
View File

@ -0,0 +1,347 @@
/*
* Copyright (C) 2025 David Picard
*
* This file is subject to the terms and conditions of the GNU Lesser
* General Public License v2.1. See the file LICENSE in the top level
* directory for more details.
*/
/**
* @ingroup drivers_max31865
* @{
*
* @file
* @brief Device driver implementation for the drivers_sensors
*
* @author David Picard
*
* @}
*/
#include <assert.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include "ztimer.h"
#include "byteorder.h"
#include "log.h"
#include "max31865.h"
#include "max31865_internal.h"
#include "max31865_params.h"
/*
* Single data byte read transfer:
*
* CS |\_____________________/|
* SCK | 8 periods | 8 periods |
* MOSI | addr byte | idle |
* MISO | idle | data byte |
*/
/* ****************************************************************************
* PRIVATE FUNCTIONS
* ****************************************************************************/
/**
* @brief Convert temperature to raw data
* @param[in] dev Device descriptor of the driver
* @param[in] temp Temperature in Celsius centi-degrees (0.01°C)
* @param[out] raw_data Code used to set the RTD low and RTD high registers
* @return 0 on success.
* @return -EINVAL if temperature is out of LUT range.
*/
static int _temp_to_raw(const max31865_t *dev, int32_t temp, uint16_t *raw_data)
{
int32_t temp_uc = temp * 10000; // c°C --> µ°C
assert(raw_data);
assert(dev->params->lut_numlines);
if ((temp_uc < (*dev->params->lut)[0][MAX31865_LUTCOL_TEMP])
|| (temp_uc > (*dev->params->lut)[dev->params->lut_numlines - 1][MAX31865_LUTCOL_TEMP])) {
return -EINVAL;
}
int i = 0;
for (i = 0 ; i < dev->params->lut_numlines ; i++) {
if (temp_uc < (*dev->params->lut)[i][MAX31865_LUTCOL_TEMP]) {
break;
}
}
i--;
*raw_data = (temp_uc - (*dev->params->lut)[i][MAX31865_LUTCOL_A0]) /
(*dev->params->lut)[i][MAX31865_LUTCOL_A1];
LOG_DEBUG("%s() >> i = %d, T = %"PRIi32" c°C, ADC code = 0x%04u\n",
__FUNCTION__, i, temp, *raw_data);
return 0;
}
/* ****************************************************************************
* PUBLIC FUNCTIONS
* ****************************************************************************/
int max31865_init(max31865_t *dev, const max31865_params_t *params)
{
assert(dev);
assert(params);
uint8_t data_tx[2] = { 0 }; /* data sent by MCU to device on the MOSI line */
uint16_t th_code; /* high/low threshold ADC code */
dev->params = params;
int ret = spi_init_cs(dev->params->spi, dev->params->cs_pin);
if (ret < 0) {
LOG_ERROR("Failed to initialize MAX31865\n");
return ret;
}
/* The MAX31865 supports SPI modes 1 and 3 (datasheet p16, section "Serial interface") */
spi_acquire(dev->params->spi, dev->params->cs_pin, SPI_MODE_1, SPI_CLK_5MHZ);
/* write the configuration register: */
spi_transfer_reg(dev->params->spi, dev->params->cs_pin, MAX31865_ADDR_CFG_W, params->cfg_byte);
/* write the high threshold register; LSB's address is MSB's address + 1 */
if (_temp_to_raw(dev, dev->params->temp_high_threshold, &th_code) == 0) {
data_tx[0] = th_code >> 8; /* MSB */
data_tx[1] = th_code & 0x00FF; /* LSB */
spi_transfer_regs(dev->params->spi, dev->params->cs_pin, MAX31865_ADDR_RTD_HTHRES_MSB_W,
data_tx, NULL, 2);
}
/* write the low threshold register; LSB's address is MSB's address + 1 */
if (_temp_to_raw(dev, dev->params->temp_low_threshold, &th_code) == 0) {
data_tx[0] = th_code >> 8; /* MSB */
data_tx[1] = th_code & 0x00FF; /* LSB */
spi_transfer_regs(dev->params->spi, dev->params->cs_pin, MAX31865_ADDR_RTD_LTHRES_MSB_W,
data_tx, NULL, 2);
}
spi_release(dev->params->spi);
return 0;
}
void max31865_clear_fault(const max31865_t *dev, uint8_t *config)
{
assert(dev);
uint8_t cfg_byte = 0;
uint8_t clr_byte = 0;
spi_acquire(dev->params->spi, dev->params->cs_pin, SPI_MODE_1, SPI_CLK_5MHZ);
cfg_byte = spi_transfer_reg(dev->params->spi, dev->params->cs_pin, MAX31865_ADDR_CFG_R, 0);
if (config) {
*config = cfg_byte;
}
clr_byte = cfg_byte | MAX31865_CFG_CLEAR_FAULT;
spi_transfer_reg(dev->params->spi, dev->params->cs_pin, MAX31865_ADDR_CFG_W, clr_byte);
spi_release(dev->params->spi);
}
int max31865_read_raw(const max31865_t *dev, uint16_t *raw_data)
{
assert(dev);
assert(raw_data);
uint8_t data_tx[3] = { 0 }; /* data sent by MCU to device on the MOSI line */
uint8_t data_rx[3] = { 0 }; /* data received by MCU from device on the MISO line */
data_tx[0] = MAX31865_ADDR_RTD_MSB;
spi_acquire(dev->params->spi, dev->params->cs_pin, SPI_MODE_1, SPI_CLK_5MHZ);
spi_transfer_bytes(dev->params->spi, dev->params->cs_pin, false, data_tx, data_rx,
sizeof(data_tx));
spi_release(dev->params->spi);
*raw_data = byteorder_bebuftohs(data_rx + 1);
/* test error bit b0: */
if (*raw_data & 0x0001) {
return -EIO;
}
return 0;
}
int max31865_raw_to_data(const max31865_t *dev, uint16_t raw_data, int32_t *rtd_temperature_cdegc)
{
assert(dev->params->lut);
assert(rtd_temperature_cdegc);
if (raw_data < (*dev->params->lut)[0][MAX31865_LUTCOL_CODE]) {
LOG_ERROR("%s() >> ERROR: raw_data too small 0x%04X < 0x%04X\n", __FUNCTION__, raw_data,
(uint16_t)(*dev->params->lut)[0][MAX31865_LUTCOL_CODE]);
return -EINVAL;
}
if (raw_data > (*dev->params->lut)[dev->params->lut_numlines - 1][MAX31865_LUTCOL_CODE]) {
LOG_ERROR("%s() >> ERROR: raw_data too big 0x%04X > 0x%04X\n", __FUNCTION__, raw_data,
(uint16_t)(*dev->params->lut)[dev->params->lut_numlines -
1][MAX31865_LUTCOL_CODE]);
return -EINVAL;
}
/* walk the LUT to find the appropriate coefficients for linear interpolation: */
int i = 0;
for (i = 0 ; i < dev->params->lut_numlines ; i++) {
if (raw_data < (*dev->params->lut)[i][MAX31865_LUTCOL_CODE]) {
break;
}
}
i--;
LOG_DEBUG("%s() >> i = %d\n", __FUNCTION__, i);
LOG_DEBUG("%s() >> a0 = %"PRIi32", a1 = %"PRIi32", raw_data = 0x%04X\n", __FUNCTION__,
(*dev->params->lut)[i][MAX31865_LUTCOL_A0],
(*dev->params->lut)[i][MAX31865_LUTCOL_A1],
(uint16_t)(*dev->params->lut)[i][MAX31865_LUTCOL_CODE]);
/* calculate T in µ°C by linear interpolation with the coefficients from the LUT: */
int32_t temp_uc = (*dev->params->lut)[i][MAX31865_LUTCOL_A0]
+ (*dev->params->lut)[i][MAX31865_LUTCOL_A1] * raw_data;
/* convert µ°C to c°C: */
int32_t temp_cc = temp_uc / 10000;
LOG_DEBUG("%s() >> T (°µC) = %"PRIi32"\n", __FUNCTION__, temp_uc);
LOG_DEBUG("%s() >> T (°C) = %"PRIi32".%02d\n", __FUNCTION__,
temp_cc / 100, (int)labs(temp_cc) % 100);
/* convert to centi degC */
*rtd_temperature_cdegc = temp_uc / 10000;
return 0;
}
int max31865_read(const max31865_t *dev, int32_t *rtd_temperature_cdegc)
{
assert(dev);
assert(rtd_temperature_cdegc);
uint16_t raw_data;
if (max31865_read_raw(dev, &raw_data) == -EIO) {
return -EIO;
}
else {
return max31865_raw_to_data(dev, raw_data, rtd_temperature_cdegc);
}
}
int max31865_detect_fault(const max31865_t *dev, max31865_fault_t *flt_code)
{
assert(dev);
assert(flt_code);
uint8_t data_tx[2] = { 0 }; /* data sent by MCU to device on the MOSI line */
uint8_t data_rx[2] = { 0 }; /* data received by MCU from device on the MISO line */
uint8_t cfg; /* value of the config register */
uint8_t fault; /* value of the fault register */
spi_acquire(dev->params->spi, dev->params->cs_pin, SPI_MODE_1, SPI_CLK_5MHZ);
/* read current config: */
data_tx[0] = MAX31865_ADDR_CFG_R;
spi_transfer_bytes(dev->params->spi, dev->params->cs_pin, false, data_tx, data_rx,
sizeof(data_tx));
/* start automatic fault detection: */
cfg = data_rx[1] | MAX31865_CFG_FLTDET_AUTO_START;
data_tx[0] = MAX31865_ADDR_CFG_W;
data_tx[1] = cfg;
spi_transfer_bytes(dev->params->spi, dev->params->cs_pin, false, data_tx, NULL,
sizeof(data_tx));
/* wait for completion and check actual completion; datasheet states 100µs: */
ztimer_sleep(ZTIMER_USEC, 200);
data_tx[0] = MAX31865_ADDR_CFG_R;
data_tx[1] = 0;
spi_transfer_bytes(dev->params->spi, dev->params->cs_pin, false, data_tx, data_rx,
sizeof(data_tx));
cfg = data_rx[1];
if ((cfg & MAX31865_CFG_FLTDET_MASK) != MAX31865_CFG_FLTDET_IDLE) {
spi_release(dev->params->spi);
return -EIO;
}
/* read fault code: */
data_tx[0] = MAX31865_ADDR_FAULT;
data_tx[1] = 0;
spi_transfer_bytes(dev->params->spi, dev->params->cs_pin, false, data_tx, data_rx,
sizeof(data_tx));
fault = data_rx[1];
spi_release(dev->params->spi);
if (fault & (MAX31865_FLT_REF_FC | MAX31865_FLT_REF_FO | MAX31865_FLT_RTD_FO)) {
*flt_code = MAX31865_FAULT_CIRCUIT;
}
else if (fault & MAX31865_FLT_THRESHIGH) {
*flt_code = MAX31865_FAULT_RTD_HIGH;
}
else if (fault & MAX31865_FLT_THRESLOW) {
*flt_code = MAX31865_FAULT_RTD_LOW;
}
else if (fault & MAX31865_FLT_VOLTAGE) {
*flt_code = MAX31865_FAULT_VOLTAGE;
}
else {
*flt_code = MAX31865_FAULT_NO_FAULT;
}
return 0;
}
void max31865_switch_vbias(const max31865_t *dev, bool enable)
{
assert(dev);
uint8_t data_tx[2] = { 0 }; /* data sent by MCU to device on the MOSI line */
uint8_t data_rx[2] = { 0 }; /* data received by MCU from device on the MISO line */
uint8_t cfg; /* value of the config register */
spi_acquire(dev->params->spi, dev->params->cs_pin, SPI_MODE_1, SPI_CLK_5MHZ);
/* read current config: */
data_tx[0] = MAX31865_ADDR_CFG_R;
spi_transfer_bytes(dev->params->spi, dev->params->cs_pin, false, data_tx, data_rx,
sizeof(data_tx));
cfg = data_rx[1];
/* switch Vbias on or off: */
if (enable) {
cfg = cfg | MAX31865_CFG_VBIAS_ON; /* switch on */
}
else {
cfg = cfg & ~MAX31865_CFG_VBIAS_ON; /* switch off */
}
data_tx[0] = MAX31865_ADDR_CFG_W;
data_tx[1] = cfg;
spi_transfer_bytes(dev->params->spi, dev->params->cs_pin, false, data_tx, NULL,
sizeof(data_tx));
spi_release(dev->params->spi);
}
void max31865_oneshot(const max31865_t *dev)
{
assert(dev);
uint8_t data_tx[2] = { 0 }; /* data sent by MCU to device on the MOSI line */
uint8_t data_rx[2] = { 0 }; /* data received by MCU from device on the MISO line */
uint8_t cfg; /* value of the config register */
spi_acquire(dev->params->spi, dev->params->cs_pin, SPI_MODE_1, SPI_CLK_5MHZ);
/* read current config: */
data_tx[0] = MAX31865_ADDR_CFG_R;
spi_transfer_bytes(dev->params->spi, dev->params->cs_pin, false, data_tx, data_rx,
sizeof(data_tx));
cfg = data_rx[1];
/* switch Vbias on or off: */
cfg = cfg | MAX31865_CFG_1SHOT;
data_tx[0] = MAX31865_ADDR_CFG_W;
data_tx[1] = cfg;
spi_transfer_bytes(dev->params->spi, dev->params->cs_pin, false, data_tx, NULL,
sizeof(data_tx));
spi_release(dev->params->spi);
}

View File

@ -0,0 +1,19 @@
# MAX31865 driver test
## About
This is a test application for the
[MAX31865](https://www.analog.com/en/products/max31865.html)
driver, a RTD-to-digital converter, tailored for Pt100 and Pt1000
sensing elements.
## Usage
This test application will initialize a MAX31865 device to output
the temperature every second.
The driver uses fixed-point arithmetic for all conversions.
To do so, it needs a lookup table (LUT).
A default one is provided for a Pt100 and a 330&Omega; reference resistor.
The user can generate one for different resistor values with the
`dist/genlut.py` script.