diff --git a/drivers/include/max31865.h b/drivers/include/max31865.h new file mode 100644 index 0000000000..26a8e14b93 --- /dev/null +++ b/drivers/include/max31865.h @@ -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 + * + * datasheet 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Ω + * 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 = a0 + a1 × code + */ +typedef enum { + MAX31865_LUTCOL_CODE = 0, /**< ADC code column index */ + MAX31865_LUTCOL_TEMP = 1, /**< Temperature column index (µ°C) */ + MAX31865_LUTCOL_A0 = 2, /**< a0 coefficient column index */ + MAX31865_LUTCOL_A1 = 3, /**< a1 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 VBIAS 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 VBIAS 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 VBIAS. + * + * 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 × 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 + +/** @} */ diff --git a/drivers/max31865/Makefile b/drivers/max31865/Makefile new file mode 100644 index 0000000000..7131c04432 --- /dev/null +++ b/drivers/max31865/Makefile @@ -0,0 +1 @@ +include $(RIOTMAKE)/driver_with_saul.mk diff --git a/drivers/max31865/Makefile.dep b/drivers/max31865/Makefile.dep new file mode 100644 index 0000000000..c23ecbf83d --- /dev/null +++ b/drivers/max31865/Makefile.dep @@ -0,0 +1,4 @@ +FEATURES_REQUIRED += periph_spi + +USEMODULE += ztimer +USEMODULE += ztimer_usec diff --git a/drivers/max31865/Makefile.include b/drivers/max31865/Makefile.include new file mode 100644 index 0000000000..b4ac9768ec --- /dev/null +++ b/drivers/max31865/Makefile.include @@ -0,0 +1,2 @@ +USEMODULE_INCLUDES_max31865 := $(LAST_MAKEFILEDIR)/include +USEMODULE_INCLUDES += $(USEMODULE_INCLUDES_max31865) diff --git a/drivers/max31865/include/max31865_internal.h b/drivers/max31865/include/max31865_internal.h new file mode 100644 index 0000000000..06637f1b0e --- /dev/null +++ b/drivers/max31865/include/max31865_internal.h @@ -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 VREFIN- is expected to be less than 0.85 × VBIAS. + * - When the FORCE- switch is open, no current should flow through the reference + * resistor nor the RTD and both VREFIN- and VRTDIN- are + * expected to be greater than 0.85 × VBIAS. + * + * 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. + * + * VREFIN- | VRTDIN- | 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: VREFIN- > 0.85 × VBIAS when FORCE- is closed + */ +#define MAX31865_FLT_REF_FC (0b00100000) + +/** + * @brief Fault: VREFIN- < 0.85 × VBIAS when FORCE- is open + */ +#define MAX31865_FLT_REF_FO (0b00010000) + +/** + * @brief Fault: VRTDIN- < 0.85 × × VBIAS 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 + +/** @} */ diff --git a/drivers/max31865/include/max31865_lut.h b/drivers/max31865/include/max31865_lut.h new file mode 100644 index 0000000000..d98e97e2d3 --- /dev/null +++ b/drivers/max31865/include/max31865_lut.h @@ -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Ω + * - RREF = 330Ω + * - 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 + +/** @} */ diff --git a/drivers/max31865/include/max31865_params.h b/drivers/max31865/include/max31865_params.h new file mode 100644 index 0000000000..385b4104db --- /dev/null +++ b/drivers/max31865/include/max31865_params.h @@ -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 + +/** @} */ diff --git a/drivers/max31865/max31865.c b/drivers/max31865/max31865.c new file mode 100644 index 0000000000..78dfc624fe --- /dev/null +++ b/drivers/max31865/max31865.c @@ -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 +#include +#include +#include +#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); +} diff --git a/tests/drivers/max31865/README.md b/tests/drivers/max31865/README.md new file mode 100644 index 0000000000..a6dd7bd204 --- /dev/null +++ b/tests/drivers/max31865/README.md @@ -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Ω reference resistor. +The user can generate one for different resistor values with the +`dist/genlut.py` script.