diff --git a/drivers/include/max31855.h b/drivers/include/max31855.h new file mode 100644 index 0000000000..0313dadfc6 --- /dev/null +++ b/drivers/include/max31855.h @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2024 HAW Hamburg + * + * 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. + */ + +/** + * @defgroup drivers_max31855 MAX31855 Thermocouple-to-Digital Converter driver + * @ingroup drivers_sensors + * @brief Driver for the SPI Thermocouple-to-Digital Converter MAX31855. + * + * The MAX31855 performs cold-junction compensation and digitizes + * the signal from a K-, J-, N-, T-, S-, R-, or E-type + * thermocouple. The data is output in a signed 14-bit, + * SPI-compatible, read-only format. This converter resolves + * temperatures to 0.25°C, allows readings as high as +1800°C and + * as low as -270°C, and exhibits thermocouple accuracy of ±2°C + * for temperatures ranging from -200°C to +700°C for K-type + * thermocouples. + * + * @note See the datasheet for more information: + * https://www.analog.com/media/en/technical-documentation/data-sheets/MAX31855.pdf + * + * @note This driver doesn't require a MOSI line, as the MAX31855 is a read-only. + * + * @{ + * + * @file + * + * @author Leandro Lanzieri + */ + +#ifndef MAX31855_H +#define MAX31855_H + +/* Add header includes here */ + +#ifdef __cplusplus +extern "C" { +#endif + +#include "periph/spi.h" + +/* Declare the API of the driver */ + +/** + * @brief Device initialization parameters + */ +typedef struct { + spi_t spi; /**< SPI device */ + spi_cs_t cs_pin; /**< Chip select pin */ +} max31855_params_t; + +/** + * @brief Device descriptor for the driver + */ +typedef struct { + const max31855_params_t *params; /**< device configuration */ +} max31855_t; + +/** + * @brief Fault status of the MAX31855 + */ +typedef enum { + MAX31855_FAULT_VCC_SHORT = 0, /**< VCC short-circuit */ + MAX31855_FAULT_GND_SHORT = 1, /**< GND short-circuit */ + MAX31855_FAULT_OPEN_CIRCUIT = 2, /**< Open circuit */ + MAX31855_FAULT_NO_FAULT = 3 /**< No fault */ +} max31855_fault_t; + +/** + * @brief Data structure for the MAX31855 + */ +typedef struct { + int32_t thermocouple_temperature; /**< Thermocouple temperature in centi degrees C */ + int32_t internal_temperature; /**< Internal temperature in centi degrees C */ + max31855_fault_t fault; /**< Fault status */ +} max31855_data_t; + +/** + * @brief Initialize the given device + * + * @param[inout] 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 max31855_init(max31855_t *dev, const max31855_params_t *params); + +/** + * @brief Parse the raw data from the MAX31855 to the data structure + * + * @param[in] raw_data Raw data from the MAX31855 + * @param[out] data Pointer to the data structure. + * + * @pre @p data must not be NULL + */ +void max31855_raw_to_data(uint32_t raw_data, max31855_data_t *data); + +/** + * @brief Read data from the MAX31855. 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] data Pointer to the data structure. + * + * @pre @p dev and @p data must not be NULL + * + * @retval 0 on success + * @retval -EIO if there is an error detected by the MAX31855. For a detailed + * error description, check the fault field of the data structure. On this + * case, temperature fields are not valid. + */ +int max31855_read(max31855_t *dev, max31855_data_t *data); + +/** + * @brief Read raw data from the MAX31855 + * + * @param[in] dev Device descriptor of the driver + * @param[out] data Pointer where to store the raw data. + * + * @pre @p dev and @p data must not be NULL + */ +void max31855_read_raw(max31855_t *dev, uint32_t *data); + +#ifdef __cplusplus +} +#endif + +#endif /* MAX31855_H */ +/** @} */ diff --git a/drivers/max31855/Makefile b/drivers/max31855/Makefile new file mode 100644 index 0000000000..48422e909a --- /dev/null +++ b/drivers/max31855/Makefile @@ -0,0 +1 @@ +include $(RIOTBASE)/Makefile.base diff --git a/drivers/max31855/Makefile.dep b/drivers/max31855/Makefile.dep new file mode 100644 index 0000000000..6f0dd3d6c3 --- /dev/null +++ b/drivers/max31855/Makefile.dep @@ -0,0 +1 @@ +FEATURES_REQUIRED += periph_spi diff --git a/drivers/max31855/Makefile.include b/drivers/max31855/Makefile.include new file mode 100644 index 0000000000..31fcfa5cde --- /dev/null +++ b/drivers/max31855/Makefile.include @@ -0,0 +1,2 @@ +USEMODULE_INCLUDES_max31855 := $(LAST_MAKEFILEDIR)/include +USEMODULE_INCLUDES += $(USEMODULE_INCLUDES_max31855) diff --git a/drivers/max31855/include/max31855_constants.h b/drivers/max31855/include/max31855_constants.h new file mode 100644 index 0000000000..627298012d --- /dev/null +++ b/drivers/max31855/include/max31855_constants.h @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2024 HAW Hamburg + * + * 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_max31855 + * @{ + * + * @file + * @brief Internal addresses, registers and constants + * + * @author Leandro Lanzieri + */ + +#ifndef MAX31855_CONSTANTS_H +#define MAX31855_CONSTANTS_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Size of the data read from the MAX31855 in bytes. + */ +#define MAX31855_DATA_SIZE (4) + +/** + * @brief Shift value for the thermocouple sign bit. + */ +#define MAX31855_THERMOCOUPLE_SIGN_SHIFT (31) + +/** + @brief Mask value for the thermocouple sign bit. + */ +#define MAX31855_THERMOCOUPLE_SIGN_MASK (1UL << MAX31855_THERMOCOUPLE_SIGN_SHIFT) + +/** + * @brief Shift value for the thermocouple integer bits. + */ +#define MAX31855_THERMOCOUPLE_INTEGER_SHIFT (20) + +/** + * @brief Mask value for the thermocouple integer bits. + */ +#define MAX31855_THERMOCOUPLE_INTEGER_MASK (0x7FFUL << MAX31855_THERMOCOUPLE_INTEGER_SHIFT) + +/** + * @brief Shift value for the thermocouple fractional half degree bit. + */ +#define MAX31855_THERMOCOUPLE_FRACTIONAL_HALF_SHIFT (19) + +/** + * @brief Mask value for the thermocouple fractional half degree bit. + */ +#define MAX31855_THERMOCOUPLE_FRACTIONAL_HALF_MASK (1UL << MAX31855_THERMOCOUPLE_FRACTIONAL_HALF_SHIFT) + +/** + * @brief Shift value for the thermocouple fractional quarter degree bit. + */ +#define MAX31855_THERMOCOUPLE_FRACTIONAL_QUARTER_SHIFT (18) + +/** + * @brief Mask value for the thermocouple fractional quarter degree bit. + */ +#define MAX31855_THERMOCOUPLE_FRACTIONAL_QUARTER_MASK (1UL << MAX31855_THERMOCOUPLE_FRACTIONAL_QUARTER_SHIFT) + +/** + * @brief Shift value for the internal sign bit. + */ +#define MAX31855_INTERNAL_SIGN_SHIFT (15) + +/** + @brief Mask value for the internal sign bit. + */ +#define MAX31855_INTERNAL_SIGN_MASK (1UL << MAX31855_INTERNAL_SIGN_SHIFT) + +/** + * @brief Shift value for the internal integer bits. + */ +#define MAX31855_INTERNAL_INTEGER_SHIFT (8) + +/** + * @brief Mask value for the internal integer bits. + */ +#define MAX31855_INTERNAL_INTEGER_MASK (0x7FUL << MAX31855_INTERNAL_INTEGER_SHIFT) + +/** + * @brief Shift value for the internal fractional half degree bit. + */ +#define MAX31855_INTERNAL_FRACTIONAL_HALF_SHIFT (7) + +/** + * @brief Mask value for the internal fractional half degree bit. + */ +#define MAX31855_INTERNAL_FRACTIONAL_HALF_MASK (1UL << MAX31855_INTERNAL_FRACTIONAL_HALF_SHIFT) + +/** + * @brief Shift value for the internal fractional quarter degree bit. + */ +#define MAX31855_INTERNAL_FRACTIONAL_QUARTER_SHIFT (6) + +/** + * @brief Mask value for the internal fractional quarter degree bit. + */ +#define MAX31855_INTERNAL_FRACTIONAL_QUARTER_MASK (1UL << MAX31855_INTERNAL_FRACTIONAL_QUARTER_SHIFT) + +/** + * @brief Shift value for the internal fractional eighth degree bit. + */ +#define MAX31855_INTERNAL_FRACTIONAL_EIGHTH_SHIFT (5) + +/** + * @brief Shift value for the internal fractional eighth degree bit. + */ +#define MAX31855_INTERNAL_FRACTIONAL_EIGHTH_MASK (1UL << MAX31855_INTERNAL_FRACTIONAL_EIGHTH_SHIFT) + +/** + * @brief Shift value for the internal fractional sixteenth degree bit. + */ +#define MAX31855_INTERNAL_FRACTIONAL_SIXTEENTH_SHIFT (4) + +/** + * @brief Shift value for the internal fractional sixteenth degree bit. + */ +#define MAX31855_INTERNAL_FRACTIONAL_SIXTEENTH_MASK (1UL << MAX31855_INTERNAL_FRACTIONAL_SIXTEENTH_SHIFT) + +/** + * @brief Shift value for the fault bit indicating a VCC short. + */ +#define MAX31855_FAULT_VCC_SHORT_SHIFT (2) + +/** + * @brief Mask value for the fault bit indicating a VCC short. + */ +#define MAX31855_FAULT_VCC_SHORT_MASK (1UL << MAX31855_FAULT_VCC_SHORT_SHIFT) + +/** + * @brief Shift value for the fault bit indicating a GND short. + */ +#define MAX31855_FAULT_GND_SHORT_SHIFT (1) + +/** + * @brief Mask value for the fault bit indicating a GND short. + */ +#define MAX31855_FAULT_GND_SHORT_MASK (1UL << MAX31855_FAULT_GND_SHORT_SHIFT) + +/** + * @brief Shift value for the fault bit indicating an open circuit. + */ +#define MAX31855_FAULT_OPEN_CIRCUIT_SHIFT (0) + +/** + * @brief Mask value for the fault bit indicating an open circuit. + */ +#define MAX31855_FAULT_OPEN_CIRCUIT_MASK (1UL << MAX31855_FAULT_OPEN_CIRCUIT_SHIFT) + +/** + * @brief Mask value for the fault bits. + */ +#define MAX31855_FAULT_MASK (MAX31855_FAULT_VCC_SHORT_MASK | \ + MAX31855_FAULT_GND_SHORT_MASK | \ + MAX31855_FAULT_OPEN_CIRCUIT_MASK) + +#ifdef __cplusplus +} +#endif + +#endif /* MAX31855_CONSTANTS_H */ +/** @} */ diff --git a/drivers/max31855/include/max31855_params.h b/drivers/max31855/include/max31855_params.h new file mode 100644 index 0000000000..1a92153eca --- /dev/null +++ b/drivers/max31855/include/max31855_params.h @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2024 HAW Hamburg + * + * 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_max31855 + * + * @{ + * @file + * @brief Default configuration for the MAX31855 driver + * + * @author Leandro Lanzieri + */ + +#ifndef MAX31855_PARAMS_H +#define MAX31855_PARAMS_H + +#include "board.h" +#include "saul_reg.h" + +#include "max31855.h" +#include "max31855_constants.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @name Default configuration for the MAX31855 driver + * @{ + */ +/** + * @brief Default SPI bus for the MAX31855 driver + */ +#ifndef MAX31855_PARAM_PARAM_SPI +#define MAX31855_PARAM_PARAM_SPI (SPI_DEV(0)) +#endif +/** + * @brief Default CS pin for the MAX31855 driver + */ +#ifndef MAX31855_PARAM_PARAM_CS_PIN +#define MAX31855_PARAM_PARAM_CS_PIN (GPIO_PIN(0, 4)) +#endif +/** + * @brief Default parameters for the MAX31855 driver + */ +#ifndef MAX31855_PARAMS +#define MAX31855_PARAMS { \ + .spi = MAX31855_PARAM_PARAM_SPI, \ + .cs_pin = MAX31855_PARAM_PARAM_CS_PIN, \ +} +#endif +/** + * @brief Default SAUL info for the MAX31855 driver + */ +#ifndef MAX31855_SAULINFO +#define MAX31855_SAULINFO { .name = "max31855_thermo" },{ .name = "max31855_internal" } +#endif +/**@}*/ + +/** + * @brief Configuration structs for the MAX31855 driver + */ +static const max31855_params_t max31855_params[] = +{ + MAX31855_PARAMS +}; + +/** + * @brief Allocate and configure entries to the SAUL registry + */ +static const saul_reg_info_t max31855_saul_info[] = +{ + MAX31855_SAULINFO +}; + +#ifdef __cplusplus +} +#endif + +#endif /* MAX31855_PARAMS_H */ +/** @} */ diff --git a/drivers/max31855/max31855.c b/drivers/max31855/max31855.c new file mode 100644 index 0000000000..18c6f3861f --- /dev/null +++ b/drivers/max31855/max31855.c @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2024 HAW Hamburg + * + * 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_max31855 + * @{ + * + * @file + * @brief Device driver implementation for the drivers_sensors + * + * @author Leandro Lanzieri + * + * @} + */ + +#include +#include +#include + +#include "byteorder.h" +#include "log.h" + +#include "max31855.h" +#include "max31855_constants.h" +#include "max31855_params.h" + + +void _raw_data_to_thermocouple_temperature(uint32_t raw_data, int32_t *temperature) +{ + assert(temperature); + + int32_t is_negative = raw_data & MAX31855_THERMOCOUPLE_SIGN_MASK; + + if (is_negative) { + raw_data = ~raw_data + 1; + } + + *temperature = (raw_data & MAX31855_THERMOCOUPLE_INTEGER_MASK) >> + MAX31855_THERMOCOUPLE_INTEGER_SHIFT; + + /* convert to centi degC */ + *temperature = *temperature * 100; + + /* add fractional parts */ + *temperature += (raw_data & MAX31855_THERMOCOUPLE_FRACTIONAL_HALF_MASK) ? 50 : 0; + *temperature += (raw_data & MAX31855_THERMOCOUPLE_FRACTIONAL_QUARTER_MASK) ? 25 : 0; + + if (is_negative) { + *temperature *= -1; + } +} + +void _raw_data_to_internal_temperature(uint32_t raw_data, int32_t *temperature) +{ + assert(temperature); + + int32_t is_negative = raw_data & MAX31855_INTERNAL_SIGN_MASK; + + if (is_negative) { + raw_data = ~raw_data + 1; + } + + *temperature = (raw_data & MAX31855_INTERNAL_INTEGER_MASK) >> MAX31855_INTERNAL_INTEGER_SHIFT; + + /* convert to centi degC */ + *temperature = *temperature * 100; + + /* add fractional parts */ + *temperature += (raw_data & MAX31855_INTERNAL_FRACTIONAL_HALF_MASK) ? 50 : 0; + *temperature += (raw_data & MAX31855_INTERNAL_FRACTIONAL_QUARTER_MASK) ? 25 : 0; + *temperature += (raw_data & MAX31855_INTERNAL_FRACTIONAL_EIGHTH_MASK) ? 12 : 0; + *temperature += (raw_data & MAX31855_INTERNAL_FRACTIONAL_SIXTEENTH_MASK) ? 6 : 0; + + if (is_negative) { + *temperature *= -1; + } +} + +void _raw_data_to_fault(uint32_t raw_data, max31855_fault_t *fault) +{ + assert(fault); + + switch (raw_data & MAX31855_FAULT_MASK) { + case MAX31855_FAULT_VCC_SHORT_MASK: + *fault = MAX31855_FAULT_VCC_SHORT; + break; + case MAX31855_FAULT_GND_SHORT_MASK: + *fault = MAX31855_FAULT_GND_SHORT; + break; + case MAX31855_FAULT_OPEN_CIRCUIT_MASK: + *fault = MAX31855_FAULT_OPEN_CIRCUIT; + break; + default: + *fault = MAX31855_FAULT_NO_FAULT; + break; + } +} + +int max31855_init(max31855_t *dev, const max31855_params_t *params) +{ + assert(dev); + assert(params); + dev->params = params; + + int ret = spi_init_cs(dev->params->spi, dev->params->cs_pin); + + if (ret < 0) { + LOG_ERROR("Failed to initialize MAX31855\n"); + return ret; + } + + return 0; +} + +void max31855_read_raw(max31855_t *dev, uint32_t *data) +{ + assert(dev); + assert(data); + + uint8_t buffer[MAX31855_DATA_SIZE] = { 0 }; + spi_acquire(dev->params->spi, dev->params->cs_pin, SPI_MODE_0, SPI_CLK_5MHZ); + spi_transfer_bytes(dev->params->spi, dev->params->cs_pin, false, NULL, buffer, sizeof(buffer)); + spi_release(dev->params->spi); + + *data = byteorder_bebuftohl(buffer); +} + +void max31855_raw_to_data(uint32_t raw_data, max31855_data_t *data) +{ + assert(data); + + _raw_data_to_thermocouple_temperature(raw_data, &data->thermocouple_temperature); + _raw_data_to_internal_temperature(raw_data, &data->internal_temperature); + _raw_data_to_fault(raw_data, &data->fault); +} + +int max31855_read(max31855_t *dev, max31855_data_t *data) +{ + assert(dev && data); + + uint32_t raw_data; + max31855_read_raw(dev, &raw_data); + max31855_raw_to_data(raw_data, data); + + if (data->fault != MAX31855_FAULT_NO_FAULT) { + LOG_ERROR("MAX31855 fault: %d\n", data->fault); + return -EIO; + } + + return 0; +} diff --git a/drivers/max31855/max31855_saul.c b/drivers/max31855/max31855_saul.c new file mode 100644 index 0000000000..af18fe51cf --- /dev/null +++ b/drivers/max31855/max31855_saul.c @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2024 HAW Hamburg + * + * 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_max31855 + * @{ + * + * @file + * @brief SAUL adaption for MAX31855 devices + * + * @author Leandro Lanzieri + * + * @} + */ + +#include + +#include "saul.h" +#include "phydat.h" +#include "max31855.h" + +static int read_temp(const void *dev, phydat_t *res, bool thermocouple) +{ + max31855_data_t data; + max31855_read((max31855_t *)dev, &data); + + if (data.fault != MAX31855_FAULT_NO_FAULT) { + return -ECANCELED; + } + + res->unit = UNIT_TEMP_C; + res->scale = -2; + + if (thermocouple) { + phydat_fit(res, &data.thermocouple_temperature, 1); + } + else { + phydat_fit(res, &data.internal_temperature, 1); + } + + return 1; +} + +static int read_thermo_temp(const void *dev, phydat_t *res) +{ + return read_temp(dev, res, true); +} + +static int read_internal_temp(const void *dev, phydat_t *res) +{ + return read_temp(dev, res, false); +} + +const saul_driver_t max31855_thermo_temp_saul_driver = { + .read = read_thermo_temp, + .write = saul_write_notsup, + .type = SAUL_SENSE_TEMP +}; + +const saul_driver_t max31855_internal_temp_saul_driver = { + .read = read_internal_temp, + .write = saul_write_notsup, + .type = SAUL_SENSE_TEMP +}; diff --git a/drivers/saul/init_devs/auto_init_max31855.c b/drivers/saul/init_devs/auto_init_max31855.c new file mode 100644 index 0000000000..d5933de696 --- /dev/null +++ b/drivers/saul/init_devs/auto_init_max31855.c @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2024 HAW Hamburg + * + * 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 sys_auto_init_saul + * @{ + * + * @file + * @brief Auto initialization for MAX31855 thermocouple-to-digital converters + * + * @author Leandro Lanzieri + * + * @} + */ + +#include "assert.h" +#include "log.h" +#include "saul_reg.h" +#include "max31855_params.h" +#include "max31855.h" + +/** + * @brief Define the number of configured devices + */ +#define MAX31855_NUM ARRAY_SIZE(max31855_params) + +/** + * @brief Allocate memory for the device descriptors + */ +static max31855_t max31855_devs[MAX31855_NUM]; + +/** + * @brief Memory for the SAUL registry entries + */ +static saul_reg_t saul_entries[MAX31855_NUM * 2]; + +/** + * @brief Define the number of saul info + */ +#define MAX31855_INFO_NUM ARRAY_SIZE(max31855_saul_info) + +/** + * @name Import SAUL endpoints + * @{ + */ +extern const saul_driver_t max31855_thermo_temp_saul_driver; +extern const saul_driver_t max31855_internal_temp_saul_driver; +/** @} */ + +void auto_init_max31855(void) +{ + /* each device has thermocouple and internal temperature, needs 2 infos */ + assert(MAX31855_INFO_NUM == 2 * MAX31855_NUM); + + for (unsigned int i = 0; i < MAX31855_NUM; i++) { + LOG_DEBUG("[auto_init_saul] initializing MAX31855 #%u\n", i); + + if (max31855_init(&max31855_devs[i], &max31855_params[i]) != 0) { + LOG_ERROR("[auto_init_saul] error initializing MAX31855 #%u\n", i); + continue; + } + + saul_entries[(i * 2)].dev = &(max31855_devs[i]); + saul_entries[(i * 2)].name = max31855_saul_info[(i * 2)].name; + saul_entries[(i * 2)].driver = &max31855_thermo_temp_saul_driver; + + saul_entries[(i * 2) + 1].dev = &(max31855_devs[i]); + saul_entries[(i * 2) + 1].name = max31855_saul_info[(i * 2) + 1].name; + saul_entries[(i * 2) + 1].driver = &max31855_internal_temp_saul_driver; + + saul_reg_add(&(saul_entries[(i * 2)])); + saul_reg_add(&(saul_entries[(i * 2) + 1])); + } +} diff --git a/drivers/saul/init_devs/init.c b/drivers/saul/init_devs/init.c index 1e9146fce5..bfb4d10ead 100644 --- a/drivers/saul/init_devs/init.c +++ b/drivers/saul/init_devs/init.c @@ -207,6 +207,10 @@ void saul_init_devs(void) extern void auto_init_mag3110(void); auto_init_mag3110(); } + if (IS_USED(MODULE_MAX31855)) { + extern void auto_init_max31855(void); + auto_init_max31855(); + } if (IS_USED(MODULE_MCP47XX)) { extern void auto_init_mcp47xx(void); auto_init_mcp47xx();