diff --git a/drivers/include/mlx90393.h b/drivers/include/mlx90393.h
new file mode 100644
index 000000000000..42d0a236be14
--- /dev/null
+++ b/drivers/include/mlx90393.h
@@ -0,0 +1,463 @@
+/*
+ * Copyright (C) 2023 Michael Ristau
+ *
+ * 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_mlx90393 MLX90393 3-axis magnetometer
+ * @ingroup drivers_sensors
+ * @ingroup drivers_saul
+ * @brief Device driver for the MLX90393 3-axis magnetometer
+ *
+ * # Overview
+ *
+ * ## About the sensor
+ *
+ * The MLX90393 is a magnetic field sensor offering a 16-bit output proportional
+ * to the magnetic flux density sensed along the X, Y, and Z axes.
+ * In addition, the sensor offers a 16-bit temperature output signal. These values
+ * are available via I2C and SPI. The sensor can be run in single shot and continuous
+ * modes, see below.
+ * To minimize host operations, interrupts can be used either when new sensor data is
+ * ready to be read or when sensor values exceed configured thresholds.
+ *
+ * ## Supported Features
+ *
+ * Functionality the driver supports:
+ * - Magnetic field and temperature sensing in single-shot or continuous mode
+ * - Configurable thresholds for triggering interrupt
+ * - Configurable output data rate
+ * - Configurable analog chain gain
+ * - Configurable 16-bit output value from the 19-bit ADC
+ * - Configurable oversampling ratio for meagnetic and temperature sensor
+ * - Digital filter applicable to ADC
+ * - Support for I2C and SPI
+ * - Measurement retrieval via interrupt or polling
+ * - Setting the sensor into idle (sleep) mode
+ * - Fixed configuration of the sensor by a default parameter set of
+ * type #mlx90393_params_t as defined in the file `mlx90393_params.h
+ * - SAUL sensor interface
+ *
+ * The following pseudomodules are used to choose the communication bus or add
+ * additional functionalities:
+ *
+ * Pseudomodule | Functionality
+ * :-------------------|:-------------------------------------------------------
+ * `mlx90393_i2c` | Use I2C bus
+ * `mlx90393_spi` | Use SPI bus
+ * `mlx90393_int` | Data ready interrupt handling
+ * `mlx90393_woc` | Wake-up on change mode
+ *
+ *
+ *
+ * @note
+ * - If the handling of data ready interrupts is enabled by module `mlx90393_int`,
+ * the GPIO pin for the interrupt signal must be defined by the configuration parameter
+ * mlx90393_params_t::int_pin. The default configuration of this GPIO pin
+ * is defined by #MLX90393_PARAM_INT_PIN that can be overridden by the
+ * board definition. The interrupt signal is HIGH active. With the use of the module
+ * `mlx90393_int` polling is no longer supported.
+ * - For the Wake-up on change measurement mode the use of interrupt is mandatory.
+ * If the module `mlx90393_woc` is used, the module `mlx90393_int` is added automatically.
+ *
+ * # Using the driver
+ *
+ * ## Operation modes
+ *
+ * The MLX90393 can be used in three modes:
+ *
+ * - **Single measurement mode**
+ * The master will ask for data via the corresponding interface (I2C or SPI),
+ * waking up the mlx90393 to make a single conversion, immediately followed by an
+ * automatic return to sleep mode (IDLE) until the next polling of the master.
+ *
+ * - **Burst mode**
+ * When the sensor is operating in burst mode, it will make continuous conversions at
+ * configurable time intervals. Whenever the MLX90393 has made the selected conversions,
+ * the DRDY signal will be set (active High) on the INT and/or INT/TRG pin to indicate that
+ * the data is ready for read.
+ *
+ * - **Wake-up on change mode**
+ * The Wake-Up on Change (WOC) functionality can be set to only receive interrupts when a
+ * certain threshold is crossed. In WOC mode, the sensor is operating in burst
+ * mode and compares each new burst value with a reference value to assert an
+ * interrupt if the difference between both exceeds a user-defined threshold.
+ * The reference value is defined as one of the following:
+ * - The first measurement of WOC mode is stored once as reference value. This measurement at
+ * “t=0” is then the basis for comparison (Absolute mode).
+ * - The reference for acquisition(t) is always acquisition(t-1) (Relative mode).
+ * For this mode the use of interrupt is mandatory.
+ *
+ * ## Initialization
+ *
+ * The **easiest way to use the driver** is simply to initialize the sensor
+ * with function #mlx90393_init using the default configuration parameter set
+ * #mlx90393_params as defined in file mlx90393_params.h.
+ *
+ * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~{.c}
+ * static mlx90393_t dev;
+ *
+ * if (mlx90393_init(&dev, &mlx90393_params[0]) != 0) {
+ * ... // error handling
+ * }
+ * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ *
+ * After initialization, the sensor is configured according to the standard
+ * configuration parameters and is fully operational.
+ *
+ * ## Fetching data
+ *
+ * - In single measurement mode the #mlx90393_read function starts the measurement.
+ * - When interrupt is configured the #mlx90393_read function locks a mutex
+ * and sleeps until a measurement is ready and the sensor triggers an interrupt.
+ * In the corresponding ISR the mutex is unlocked and the function proceeds reading
+ * and converting the data.
+ * - If interrupt is not used, the function either sleeps for the conversion time
+ * in single measurement mode or performs continuous queries for data in burst mode,
+ * with sleep intervals of 10 ms (polling).
+ * - For the wake-up on change mode the use of interrupt is mandatory.
+ *
+ * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~{.c}
+ * while (1)
+ * {
+ * mlx90393_data_t mag_data;
+ *
+ * if (mlx90393_read(&dev, &mag_data) != 0) {
+ * ... // error handling
+ * }
+ * ...
+ * }
+ * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ *
+ * ## Output data format
+ *
+ * Function #mlx90393_read returns magnetic field data in micro tesla and temperature
+ * in deci celsius in parameter \p data. The range of the magnetic field value depends on:
+ * - the analog chain gain defined by mlx90393_params_t::gain
+ * - the resolution defined by mlx90393_params_t::resolution
+ *
+ * ## Using Interrupts
+ *
+ * The MLX90393 sensor supports the use of data-ready interrupts to signal when data is available.
+ * To use interrupts, add the module `mlx90393_int`, connect the INT pin of the MLX90393 to the
+ * designated interrupt pin on the MCU, as configured in the parameters.
+ * In single measurement and burst mode, when calling the function #mlx90393_read,
+ * it will block the calling thread until an interrupt is triggered. Once an interrupt is
+ * triggered, the driver handles the interrupt with an internal ISR and then proceeds with
+ * reading and converting data and finally returns from the #mlx90393_read function.
+ * In wake-up on change mode the #mlx90393_enable_woc function can be used to activate the
+ * interrupt which is triggered, when the user defined threshold is crossed.
+ * Additionally a callback function for handling the interrupt and its argument are
+ * passed to the function.
+ * @warning
+ * The passed callback function is called in interrupt context and should not be used to access
+ * the sensor or execute time-consuming code.
+ *
+ * ## Power Saving
+ *
+ * The MLX90393 sensor can be shutdown into idle mode when no continuous measurements are
+ * required using the function #mlx90393_stop_cont. The power consumption is then reduced to
+ * a maximum of 5 uA. To restart the MLX90393 in previous continuous measurement mode,
+ * the #mlx90393_start_cont function can be used.
+ * In single measurement mode, once the measurement is completed, the sensor transitions into
+ * idle mode, awaiting a new command from the master to initiate another acquisition.
+ *
+ * @{
+ *
+ * @file
+ *
+ * @author Michael Ristau
+ */
+
+#ifndef MLX90393_H
+#define MLX90393_H
+
+#include "periph/gpio.h"
+#if IS_USED(MODULE_MLX90393_SPI)
+#include "periph/spi.h"
+#elif IS_USED(MODULE_MLX90393_I2C)
+#include "periph/i2c.h"
+#endif
+
+/* Add header includes here */
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @brief Data container of the MLX90393 sensor
+ */
+typedef struct {
+ int32_t x_axis; /**< Magnometer data from x-axis */
+ int32_t y_axis; /**< Magnometer data from y_axis */
+ int32_t z_axis; /**< Magnometer data from z_axis */
+ int16_t temp; /**< Temperature */
+} mlx90393_data_t;
+
+/**
+ * @brief Measurement mode of the MLX90393 sensor
+ */
+typedef enum {
+ MLX90393_MODE_BURST, /**< Burst mode */
+ MLX90393_MODE_SINGLE_MEASUREMENT, /**< Single measurement mode */
+#if IS_USED(MODULE_MLX90393_WOC) || DOXYGEN
+ MLX90393_MODE_WOC_ABSOLUTE, /**< Wake-up on change mode,
+ compared to first measurement */
+ MLX90393_MODE_WOC_RELATIVE, /**< Wake-up on change mode,
+ compared to previous measurement */
+#endif
+} mlx90393_mode_t;
+
+/**
+ * @brief Output data rate (sample rate) of the MLX90393 sensor
+ *
+ * @note When MLX90393_ODR_MAX is used, the output data rata depends only
+ * on the configuration of oversampling ratio and digital filter.
+ */
+typedef enum {
+ MLX90393_ODR_MAX = 0x00, /**< Maximum output data rate */
+ MLX90393_ODR_50HZ = 0x01, /**< Output data rate 50 Hz */
+ MLX90393_ODR_25HZ = 0x02, /**< Output data rate 25 Hz */
+ MLX90393_ODR_12_5HZ = 0x04, /**< Output data rate 12.5 Hz */
+ MLX90393_ODR_10HZ = 0x05, /**< Output data rate 10 Hz */
+ MLX90393_ODR_6_25HZ = 0x08, /**< Output data rate 6.25 Hz */
+ MLX90393_ODR_5HZ = 0x0A, /**< Output data rate 5 Hz */
+ MLX90393_ODR_2_5HZ = 0x14, /**< Output data rate 2.5 Hz */
+ MLX90393_ODR_2HZ = 0x19, /**< Output data rate 2 Hz */
+ MLX90393_ODR_1_25HZ = 0x28, /**< Output data rate 1.25 Hz */
+ MLX90393_ODR_1HZ = 0x32, /**< Output data rate 1 Hz */
+} mlx90393_odr_t;
+
+/**
+ * @brief Analog chain gain of the MLX90393 sensor
+ */
+typedef enum {
+ MLX90393_GAIN_5X, /**< gain factor 5 */
+ MLX90393_GAIN_4X, /**< gain factor 4 */
+ MLX90393_GAIN_3X, /**< gain factor 3 */
+ MLX90393_GAIN_2_5X, /**< gain factor 2.5 */
+ MLX90393_GAIN_2X, /**< gain factor 2 */
+ MLX90393_GAIN_1_67X, /**< gain factor 1.67 */
+ MLX90393_GAIN_1_33X, /**< gain factor 1.33 */
+ MLX90393_GAIN_1X, /**< gain factor 1 */
+} mlx90393_gain_t;
+
+/**
+ * @brief Desired 16-bit output value from the 19-bit ADC of the MLX90393 sensor
+ */
+typedef enum {
+ MLX90393_RES_16, /**< resolution 16 bit */
+ MLX90393_RES_17, /**< resolution 17 bit */
+ MLX90393_RES_18, /**< resolution 18 bit */
+ MLX90393_RES_19, /**< resolution 19 bit */
+} mlx90393_resolution_t;
+
+/**
+ * @brief Oversampling ratio of the MLX90393 sensor
+ */
+typedef enum {
+ MLX90393_OSR_0, /**< Oversampling ratio 0 */
+ MLX90393_OSR_1, /**< Oversampling ratio 1 */
+ MLX90393_OSR_2, /**< Oversampling ratio 2 */
+ MLX90393_OSR_3, /**< Oversampling ratio 3 */
+} mlx90393_oversampling_ratio_t;
+
+/**
+ * @brief Digital filter applicable to ADC of the MLX90393 sensor
+ */
+typedef enum {
+ MLX90393_DIG_FILT_0, /**< Digital filter 0 */
+ MLX90393_DIG_FILT_1, /**< Digital filter 1 */
+ MLX90393_DIG_FILT_2, /**< Digital filter 2 */
+ MLX90393_DIG_FILT_3, /**< Digital filter 3 */
+ MLX90393_DIG_FILT_4, /**< Digital filter 4 */
+ MLX90393_DIG_FILT_5, /**< Digital filter 5 */
+ MLX90393_DIG_FILT_6, /**< Digital filter 6 */
+ MLX90393_DIG_FILT_7, /**< Digital filter 7 */
+} mlx90393_digital_filter_t;
+
+/**
+ * @brief Oversampling rates of magnetic and temperature sensors
+ */
+typedef struct {
+ mlx90393_oversampling_ratio_t mag; /**< Oversampling ratio magnetic sensor */
+ mlx90393_oversampling_ratio_t temp; /**< Oversampling ratio temperature sensor */
+} mlx90393_oversampling_t;
+
+#if IS_USED(MODULE_MLX90393_WOC) || DOXYGEN
+/**
+ * @brief Thresholds for wake-up on change mode
+ */
+typedef struct {
+ int xy; /**< Threshold for x and y axes */
+ int z; /**< Threshold for z axis */
+ int temp; /**< Threshold for temperature */
+} mlx90393_threshold_t;
+
+/**
+ * @brief Signature of callback function triggered from wake-up on change interrupt
+ *
+ * @param[in] arg argument for the callback
+ */
+typedef void (*mlx90393_cb_t)(void *arg);
+
+#endif
+
+/**
+ * @brief Device configuration parameters
+ */
+typedef struct {
+#if IS_USED(MODULE_MLX90393_SPI) || DOXYGEN
+ spi_t spi; /**< SPI bus */
+ gpio_t cs_pin; /**< Connected chip select pin */
+ spi_clk_t clk; /**< clock speed for the SPI bus */
+#elif IS_USED(MODULE_MLX90393_I2C) || DOXYGEN
+ i2c_t i2c; /**< I2C device */
+ uint8_t addr; /**< Magnometer I2C address */
+#endif
+ mlx90393_mode_t mode; /**< Measurement mode */
+#if IS_USED(MODULE_MLX90393_INT) || DOXYGEN
+ gpio_t int_pin; /**< Interrupt pin */
+#endif
+ mlx90393_gain_t gain; /**< Analog chain gain */
+ mlx90393_resolution_t resolution; /**< Desired 16-bit output value from the 19-bit ADC */
+ mlx90393_odr_t odr; /**< Output data rate */
+ mlx90393_oversampling_t oversampling; /**< Oversampling ratio */
+ mlx90393_digital_filter_t dig_filt; /**< Digital filter applicable to ADC */
+#if IS_USED(MODULE_MLX90393_WOC) || DOXYGEN
+ mlx90393_threshold_t threshold; /**< Threshold for wake-up on change mode */
+#endif
+} mlx90393_params_t;
+
+/**
+ * @brief Device descriptor for the MLX90393 magnetometer
+ */
+typedef struct {
+ const mlx90393_params_t *params; /**< Device configuration parameters */
+ uint16_t ref_temp; /**< Reference temperature for converting raw temp data
+ in centi celsius */
+#if !IS_USED(MODULE_MLX90393_INT) || DOXYGEN
+ uint8_t conversion_time; /**< Conversion time for single measurement mode */
+#endif
+} mlx90393_t;
+
+/**
+ * @brief Initialize the MLX90393 sensor device
+ *
+ * This function resets the sensor and initializes it based on the given configuration
+ * parameters. Burst and wake-up on change modes are activated immediately. In single
+ * measurement mode, the measurement is not initiated by this function, mlx90393_read
+ * must be called for that purpose.
+ * If wake-up on change mode is used, configuring a valid interrupt pin is mandatory.
+ *
+ * @note Some configurations of oversampling and digital filter are not permitted:
+ * - #MLX90393_OSR_0 and #MLX90393_DIG_FILT_0
+ * - #MLX90393_OSR_0 and #MLX90393_DIG_FILT_1
+ * - #MLX90393_OSR_1 and #MLX90393_DIG_FILT_0
+ * In such cases, the function will return with -EINVAL.
+ *
+ * @note The conversion time increases with higher oversampling rates and digital filters.
+ * If the values are too high, it may result in the configured output data rate
+ * not being achieved.
+ *
+ * @param[inout] dev Device descriptor of MLX90393 device to be initialized
+ * @param[in] params Configuration parameters
+ *
+ * @retval 0 on success
+ * @retval -ENXIO device not available
+ * @retval -EIO SPI or I2C communication error
+ * @retval -EINVAL invalid parameter
+ * @retval -EFAULT device error
+ */
+int mlx90393_init(mlx90393_t *dev, const mlx90393_params_t *params);
+
+/**
+ * @brief Read magnetic and temperature data from the given device and convert it
+ * to physical values
+ *
+ * This function performs a single measurement or retrieves data from continuous
+ * measurements based on the configured mode. It supports both interrupt-based and
+ * polling-based measurement retrieval.
+ *
+ * If interrupt is not used, the function either sleeps for the conversion time
+ * in single measurement mode or performs continuous queries for data in burst mode,
+ * with sleep intervals of 10 ms. When interrupt is used, a mutex is locked, and
+ * the function sleeps until data is ready, triggering an interrupt.
+ *
+ * @note For wake-up on change mode the use of interrupt is mandatory.
+ *
+ * @pre
+ * - Device must be properly initialized using mlx90393_init
+ *
+ * @param[in] dev Device descriptor of MLX90393 device to read from
+ * @param[out] data MLX90393 data in micro Tesla and deci Celsius
+ *
+ * @retval 0 on success
+ * @retval -EIO SPI or I2C communication error
+ * @retval -EFAULT device error
+ */
+int mlx90393_read(mlx90393_t *dev, mlx90393_data_t *data);
+
+/**
+ * @brief Stop measurements in continuous mode
+ *
+ * Burst and Wake-up on change measurement modes are stopped and the device
+ * is forced into idle mode. Once stopped, the modes can be restarted with mlx90393_start_cont.
+ *
+ * @param[in] dev Device descriptor of MLX90393 device
+ *
+ * @retval 0 on success
+ * @retval -EIO SPI or I2C communication error
+ * @retval -EFAULT device error
+ */
+int mlx90393_stop_cont(mlx90393_t *dev);
+
+/**
+ * @brief Start measurements in continuous mode
+ *
+ * Turns the device out of idle mode and starts a previously stopped burst or wake-up on change
+ * mode with same parameters as defined in configuration parameters. This function does not
+ * initialize the device and the continuous measurement mode.
+ *
+ * @pre
+ * - Device must be initialized with mlx90393_init
+ * - Continuous measurement was stopped with mlx90393_stop_cont
+ *
+ * @param[in] dev Device descriptor of MLX90393 device
+ *
+ * @retval 0 on success
+ * @retval -EIO SPI or I2C communication error
+ * @retval -EFAULT device error
+ */
+int mlx90393_start_cont(mlx90393_t *dev);
+
+#if IS_USED(MODULE_MLX90393_WOC) || DOXYGEN
+/**
+ * @brief Activate wake-up on change interrupt
+ *
+ * Activates the interrupt which is triggered if the user defined woc threshold is crossed.
+ * The passed callback function is called when the interrupt occurs. After the interrupt
+ * the measurement data can be read with mlx90393_read.
+ *
+ * @warning
+ * The passed callback function is called in interrupt context and should not be used to access
+ * the sensor or execute time-consuming code.
+ *
+ * @param[in] dev Device descriptor of MLX90393 device
+ * @param cb Callback to be executed when interrupt occurs
+ * @param arg Callback argument
+ */
+void mlx90393_enable_woc(mlx90393_t *dev, mlx90393_cb_t cb, void *arg);
+
+#endif
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* MLX90393_H */
+/** @} */
diff --git a/drivers/mlx90393/Kconfig b/drivers/mlx90393/Kconfig
new file mode 100644
index 000000000000..30fe746ba76a
--- /dev/null
+++ b/drivers/mlx90393/Kconfig
@@ -0,0 +1,15 @@
+# Copyright (c) 2023 mi6527ri
+#
+# 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.
+
+config MODULE_MLX90393
+ bool "drivers_mlx90393"
+ depends on TEST_KCONFIG
+ depends on HAS_PERIPH_GPIO
+ depends on HAS_PERIPH_GPIO_IRQ
+ depends on HAS_PERIPH_I2C
+ select MODULE_PERIPH_GPIO
+ select MODULE_PERIPH_GPIO_IRQ
+ select MODULE_PERIPH_I2C
diff --git a/drivers/mlx90393/Makefile b/drivers/mlx90393/Makefile
new file mode 100644
index 000000000000..48422e909a47
--- /dev/null
+++ b/drivers/mlx90393/Makefile
@@ -0,0 +1 @@
+include $(RIOTBASE)/Makefile.base
diff --git a/drivers/mlx90393/Makefile.dep b/drivers/mlx90393/Makefile.dep
new file mode 100644
index 000000000000..c6c84b13f235
--- /dev/null
+++ b/drivers/mlx90393/Makefile.dep
@@ -0,0 +1,21 @@
+ifneq (,$(filter mlx90393,$(USEMODULE)))
+ USEMODULE += ztimer
+ USEMODULE += ztimer_msec
+endif
+
+ifneq (,$(filter mlx90393_i2c,$(USEMODULE)))
+ FEATURES_REQUIRED += periph_i2c
+endif
+
+ifneq (,$(filter mlx90393_spi,$(USEMODULE)))
+ FEATURES_REQUIRED += periph_spi
+endif
+
+ifneq (,$(filter mlx90393_woc,$(USEMODULE)))
+ USEMODULE += mlx90393_int
+endif
+
+ifneq (,$(filter mlx90393_int,$(USEMODULE)))
+ FEATURES_REQUIRED += periph_gpio
+ FEATURES_REQUIRED += periph_gpio_irq
+endif
diff --git a/drivers/mlx90393/Makefile.include b/drivers/mlx90393/Makefile.include
new file mode 100644
index 000000000000..71f4491b6cae
--- /dev/null
+++ b/drivers/mlx90393/Makefile.include
@@ -0,0 +1,8 @@
+# include variants of the MLX90393 drivers as pseudo modules
+PSEUDOMODULES += mlx90393_i2c
+PSEUDOMODULES += mlx90393_spi
+PSEUDOMODULES += mlx90393_int
+PSEUDOMODULES += mlx90393_woc
+
+USEMODULE_INCLUDES_mlx90393 := $(LAST_MAKEFILEDIR)/include
+USEMODULE_INCLUDES += $(USEMODULE_INCLUDES_mlx90393)
diff --git a/drivers/mlx90393/include/mlx90393_constants.h b/drivers/mlx90393/include/mlx90393_constants.h
new file mode 100644
index 000000000000..2e9084f2b99d
--- /dev/null
+++ b/drivers/mlx90393/include/mlx90393_constants.h
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2023 mi6527ri
+ *
+ * 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_mlx90393
+ * @{
+ *
+ * @file
+ * @brief MLX90393 internal addresses, registers and constants
+ *
+ * @author Michael Ristau
+ */
+
+#ifndef MLX90393_CONSTANTS_H
+#define MLX90393_CONSTANTS_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @brief Register address map
+ */
+#define MLX90393_REG_CONF0 0x00 /**< Configuration register 0 */
+#define MLX90393_REG_CONF1 0x01 /**< Configuration register 1 */
+#define MLX90393_REG_CONF2 0x02 /**< Configuration register 2 */
+#define MLX90393_REG_SENS_TC 0x03 /**< Sensitivity drift compensation factor */
+#define MLX90393_REG_OFFSET_X 0x04 /**< Offset for x axis */
+#define MLX90393_REG_OFFSET_Y 0x05 /**< Offset for y axis */
+#define MLX90393_REG_OFFSET_Z 0x06 /**< Offset for z axis */
+#define MLX90393_REG_WOXY_THRESHOLD 0x07 /**< Wake up on change threshold for x and y axis */
+#define MLX90393_REG_WOZ_THRESHOLD 0x08 /**< Wake up on change threshold for z axis */
+#define MLX90393_REG_WOT_THRESHOLD 0x09 /**< Wake up on change threshold for temp */
+#define MLX90393_REG_CONN_TEST 0x0A /**< Free available register used for
+ connectivity test */
+#define MLX90393_REG_REF_TEMP 0x24 /**< Reference temperature */
+
+/**
+ * @brief Configuration parameter bit masks
+ */
+#define MLX90393_MASK_BDR 0x003F /**< burst data rate */
+#define MLX90393_MASK_WOC_DIFF 0x1000 /**< wake up on change mode (relative or absolute) */
+#define MLX90393_MASK_TCMP_EN 0x0400 /**< enable temperature compensation */
+#define MLX90393_MASK_GAIN_SEL 0x0070 /**< analog chain gain */
+#define MLX90393_MASK_RES_XYZ 0x07E0 /**< xyz resolution */
+#define MLX90393_MASK_COMM_MODE 0x6000 /**< set allowed bus communication (I2C or SPI) */
+#define MLX90393_MASK_OSR 0x0003 /**< oversampling ratio magnetic sensor */
+#define MLX90393_MASK_OSR2 0x1800 /**< oversampling ratio temperature */
+#define MLX90393_MASK_DIG_FILT 0x001C /**< digital filter magnetic sensor */
+
+/**
+ * @brief Number of left shifts in configuration registers
+ */
+#define MLX90393_SHIFT_OSR2 11 /**< oversampling ratio temperature */
+#define MLX90393_SHIFT_DIG_FILT 2 /**< digital filter magnetic sensor */
+#define MLX90393_SHIFT_GAIN 4 /**< analog chain gain */
+#define MLX90393_SHIFT_RES_X 5 /**< x resolution */
+#define MLX90393_SHIFT_RES_Y 7 /**< y resolution */
+#define MLX90393_SHIFT_RES_Z 9 /**< z resolution */
+#define MLX90393_SHIFT_WOC_MODE 12 /**< wake-up on change mode */
+
+/**
+ * @brief Command Set
+ */
+#define MLX90393_COMMAND_SB 0x1F /**< start burst mode */
+#define MLX90393_COMMAND_SW 0x2F /**< start wake up on change mode */
+#define MLX90393_COMMAND_SM 0x3F /**< start single measurement mode */
+#define MLX90393_COMMAND_RM 0x4F /**< read measurement */
+#define MLX90393_COMMAND_RR 0x50 /**< read register */
+#define MLX90393_COMMAND_WR 0x60 /**< write register */
+#define MLX90393_COMMAND_EX 0x80 /**< exit mode */
+#define MLX90393_COMMAND_HR 0xD0 /**< memory recall */
+#define MLX90393_COMMAND_HS 0xE0 /**< memory store */
+#define MLX90393_COMMAND_RT 0xF0 /**< reset */
+
+/**
+ * @brief Status byte bit map
+ */
+#define MLX90393_STATUS_RESET 0x04 /**< reset bit */
+#define MLX90393_STATUS_ERROR 0x10 /**< error bit */
+
+/**
+ * @brief Timeout durations in ms
+ */
+#define MLX90393_COMMAND_EX_TIMEOUT 1 /**< Timeout after exit command */
+#define MLX90393_COMMAND_RT_TIMEOUT 2 /**< Timeout after reset command */
+
+/**
+ * @brief Temperature conversion constants
+ */
+#define MLX90393_TEMP_OFFSET 3500 /**< Temperature offset in centi celsius */
+#define MLX90393_TEMP_RESOLUTION 452 /**< Temperature sensor resolution
+ (45.2 * 10 for avoiding float values) */
+
+/**
+ * @brief Magnetic flux conversion constants
+ */
+#define MLX90393_XY_SENS 150 /**< xy sensitivity in nT/LSB */
+#define MLX90393_Z_SENS 242 /**< z sensitivity in nT/LSB */
+
+/**
+ * @brief Timing constants
+ */
+#define MLX90393_T_STBY 264 /**< Time from IDLE to STBY in us */
+#define MLX90393_T_ACTIVE 432 /**< Time from STBY to ACTIVE in us */
+#define MLX90393_T_CONV_END 120 /**< Time to end analog active mode in us */
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* MLX90393_CONSTANTS_H */
+/** @} */
diff --git a/drivers/mlx90393/include/mlx90393_params.h b/drivers/mlx90393/include/mlx90393_params.h
new file mode 100644
index 000000000000..ee5cc65a06b2
--- /dev/null
+++ b/drivers/mlx90393/include/mlx90393_params.h
@@ -0,0 +1,205 @@
+/*
+ * Copyright (C) 2023 mi6527ri
+ *
+ * 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_mlx90393
+ *
+ * @{
+ * @file
+ * @brief Default configuration for MLX90393 device driver
+ *
+ * @author Michael Ristau
+ */
+
+#ifndef MLX90393_PARAMS_H
+#define MLX90393_PARAMS_H
+
+#include "board.h"
+#include "mlx90393.h"
+#include "mlx90393_constants.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @name Set default configuration parameters for the MLX90393
+ * @{
+ */
+#ifndef MLX90393_PARAM_MODE
+/**
+ * @brief Default measurement mode
+ */
+#define MLX90393_PARAM_MODE (MLX90393_MODE_BURST)
+#endif
+
+#if IS_USED(MODULE_MLX90393_INT) || DOXYGEN
+#ifndef MLX90393_PARAM_INT_PIN
+/**
+ * @brief Default interrupt pin
+ */
+#define MLX90393_PARAM_INT_PIN .int_pin = (GPIO_PIN(PORT_C, 8)),
+#endif
+#else
+#define MLX90393_PARAM_INT_PIN
+#endif
+
+#ifndef MLX90393_PARAM_GAIN
+/**
+ * @brief Default gain
+ */
+#define MLX90393_PARAM_GAIN (MLX90393_GAIN_1X)
+#endif
+
+#ifndef MLX90393_PARAM_RES
+/**
+ * @brief Default resolution
+ */
+#define MLX90393_PARAM_RES (MLX90393_RES_19)
+#endif
+
+#ifndef MLX90393_PARAM_ODR
+/**
+ * @brief Default sampling rate
+ */
+#define MLX90393_PARAM_ODR (MLX90393_ODR_10HZ)
+#endif
+
+#ifndef MLX90393_PARAM_OSR
+/**
+ * @brief Default oversampling ratio
+ */
+#define MLX90393_PARAM_OSR \
+{ \
+ .mag = MLX90393_OSR_1, \
+ .temp = MLX90393_OSR_1 \
+}
+#endif
+
+#ifndef MLX90393_PARAM_DIG_FILT
+/**
+ * @brief Default digital filter
+ */
+#define MLX90393_PARAM_DIG_FILT (MLX90393_DIG_FILT_1)
+#endif
+
+#if IS_USED(MODULE_MLX90393_WOC) || DOXYGEN
+#ifndef MLX90393_PARAM_THRESHOLD
+/**
+ * @brief Default thresholds for Wake-up On Change mode
+ */
+#define MLX90393_PARAM_THRESHOLD \
+.threshold = \
+{ \
+ .xy = 0xFFFF, \
+ .z = 1000, \
+ .temp = 0xFFFF \
+},
+#endif
+#else
+#define MLX90393_PARAM_THRESHOLD
+#endif
+
+/* Default configuration for SPI mode */
+#if IS_USED(MODULE_MLX90393_SPI) || DOXYGEN
+
+#ifndef MLX90393_PARAM_SPI
+/**
+ * @brief Default SPI device
+ */
+#define MLX90393_PARAM_SPI (SPI_DEV(0))
+#endif
+
+#ifndef MLX90393_PARAM_SPI_CS_PIN
+/**
+ * @brief Default SPI chip select pin
+ */
+#define MLX90393_PARAM_SPI_CS_PIN (GPIO_PIN(PORT_B, 6))
+#endif
+
+#ifndef MLX90393_PARAM_SPI_CLK
+/**
+ * @brief Default SPI clock speed
+ */
+#define MLX90393_PARAM_SPI_CLK (SPI_CLK_10MHZ)
+#endif
+
+#ifndef MLX90393_PARAMS_SPI
+/**
+ * @brief Default SPI params
+ */
+#define MLX90393_PARAMS_SPI { \
+ .spi = MLX90393_PARAM_SPI, \
+ .cs_pin = MLX90393_PARAM_SPI_CS_PIN, \
+ .clk = MLX90393_PARAM_SPI_CLK, \
+ .mode = MLX90393_PARAM_MODE, \
+ .gain = MLX90393_PARAM_GAIN, \
+ .resolution = MLX90393_PARAM_RES, \
+ .odr = MLX90393_PARAM_ODR, \
+ .oversampling = MLX90393_PARAM_OSR, \
+ .dig_filt = MLX90393_PARAM_DIG_FILT, \
+ MLX90393_PARAM_INT_PIN \
+ MLX90393_PARAM_THRESHOLD \
+ }
+#endif
+
+/* Default configuration for I2C mode */
+#elif IS_USED(MODULE_MLX90393_I2C) || DOXYGEN
+
+#ifndef MLX90393_PARAM_I2C
+/**
+ * @brief Default I2C device
+ */
+#define MLX90393_PARAM_I2C (I2C_DEV(0))
+#endif
+
+#ifndef MLX90393_PARAM_I2C_ADDR
+/**
+ * @brief Default I2C device address
+ */
+#define MLX90393_PARAM_I2C_ADDR (0x0C)
+#endif
+
+#ifndef MLX90393_PARAMS_I2C
+/**
+ * @brief Default I2C params
+ */
+#define MLX90393_PARAMS_I2C { \
+ .i2c = MLX90393_PARAM_I2C, \
+ .addr = MLX90393_PARAM_I2C_ADDR, \
+ .mode = MLX90393_PARAM_MODE, \
+ .gain = MLX90393_PARAM_GAIN, \
+ .resolution = MLX90393_PARAM_RES, \
+ .odr = MLX90393_PARAM_ODR, \
+ .oversampling = MLX90393_PARAM_OSR, \
+ .dig_filt = MLX90393_PARAM_DIG_FILT, \
+ MLX90393_PARAM_INT_PIN \
+ MLX90393_PARAM_THRESHOLD \
+ }
+#endif
+#endif
+/**@}*/
+
+/**
+ * @brief Configure params for MLX90393
+ */
+static const mlx90393_params_t mlx90393_params[] =
+{
+#if IS_USED(MODULE_MLX90393_SPI)
+ MLX90393_PARAMS_SPI
+#elif IS_USED(MODULE_MLX90393_I2C)
+ MLX90393_PARAMS_I2C
+#endif
+};
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* MLX90393_PARAMS_H */
+/** @} */
diff --git a/drivers/mlx90393/mlx90393.c b/drivers/mlx90393/mlx90393.c
new file mode 100644
index 000000000000..76ea9fb14ed0
--- /dev/null
+++ b/drivers/mlx90393/mlx90393.c
@@ -0,0 +1,678 @@
+/*
+ * Copyright (C) 2023 Michael Ristau
+ *
+ * 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_mlx90393
+ * @{
+ *
+ * @file
+ * @brief Device driver implementation for the MLX90393
+ *
+ * @author Michael Ristau
+ *
+ * @}
+ */
+
+#include
+#include "mlx90393.h"
+#include "mlx90393_constants.h"
+#include "mlx90393_params.h"
+#include "ztimer.h"
+
+#if !IS_USED(MODULE_MLX90393_INT)
+#include "imath.h"
+#endif
+
+#define ENABLE_DEBUG 0
+#include "debug.h"
+
+#if IS_USED(MODULE_MLX90393_SPI)
+#define DEV_SPI (dev->params->spi)
+#define DEV_CS_PIN (dev->params->cs_pin)
+#define DEV_CLK (dev->params->clk)
+#define SPI_MODE (SPI_MODE_3)
+#elif IS_USED(MODULE_MLX90393_I2C)
+#define DEV_I2C (dev->params->i2c)
+#define DEV_ADDR (dev->params->addr)
+#endif
+
+#if IS_USED(MODULE_MLX90393_INT)
+#define DEV_INT_PIN (dev->params->int_pin)
+#endif
+
+#define DEV_MODE (dev->params->mode)
+#define DEV_ODR (dev->params->odr)
+#define DEV_GAIN (dev->params->gain)
+#define DEV_RESOLUTION (dev->params->resolution)
+#define DEV_OSR_MAG (dev->params->oversampling.mag)
+#define DEV_OSR_TEMP (dev->params->oversampling.temp)
+#define DEV_DIG_FILT (dev->params->dig_filt)
+#define DEV_REF_TEMP (dev->ref_temp)
+#define CONN_TEST_DATA (0xAF03)
+
+#define MLX90393_BM_READ_TIMEOUT (10)
+
+/** Forward declaration of functions for internal use */
+static int _init_bus(const mlx90393_t *dev);
+static void _acquire(mlx90393_t *dev);
+static void _release(mlx90393_t *dev);
+static int _write_byte(mlx90393_t *dev, uint8_t data);
+static int _read_byte(mlx90393_t *dev, uint8_t *buffer);
+static int _write_bytes(mlx90393_t *dev, void *data, size_t len);
+static int _read_bytes(mlx90393_t *dev, void *buffer, size_t len);
+static int _check_status_byte(mlx90393_t *dev);
+static int _write_register(mlx90393_t *dev, uint8_t addr, uint16_t value);
+static int _read_register(mlx90393_t *dev, uint8_t addr, uint16_t *value);
+static int _write_register_bits(mlx90393_t *dev, uint8_t addr, uint16_t mask, uint16_t value);
+static int _calculate_temp(uint16_t raw_temp, uint16_t ref_temp);
+static int _get_gain_factor(mlx90393_gain_t gain);
+static int _reset(mlx90393_t *dev);
+static int _exit(mlx90393_t *dev);
+static int _is_avaiable(mlx90393_t *dev);
+static int _read_measurement(mlx90393_t *dev, uint8_t *buffer);
+
+#if !IS_USED(MODULE_MLX90393_INT)
+static void _calculate_conv_time(mlx90393_t *dev);
+#endif
+
+int mlx90393_init(mlx90393_t *dev, const mlx90393_params_t *params)
+{
+ assert(dev);
+ assert(params);
+
+#if IS_USED(MODULE_MLX90393_INT)
+ assert(gpio_is_valid(DEV_INT_PIN));
+#endif
+
+ dev->params = params;
+
+ int error = 0;
+
+ if ((error = _init_bus(dev)) != 0) {
+ return error;
+ }
+
+ _acquire(dev);
+
+ /* exit all continuous measurement modes */
+ if ((error = _exit(dev)) != 0) {
+ _release(dev);
+ return error;
+ }
+ ztimer_sleep(ZTIMER_MSEC, MLX90393_COMMAND_EX_TIMEOUT);
+ /* reset mlx90393 */
+ if ((error = _reset(dev)) != 0) {
+ _release(dev);
+ return error;
+ }
+ ztimer_sleep(ZTIMER_MSEC, MLX90393_COMMAND_RT_TIMEOUT);
+ /* check availability of the sensor */
+ if ((error = _is_avaiable(dev)) != 0) {
+ _release(dev);
+ DEBUG("[mlx90393] error: device not available\n");
+ return error;
+ }
+ /* store ref temp in dev */
+ if ((error = _read_register(dev, MLX90393_REG_REF_TEMP, &DEV_REF_TEMP)) != 0) {
+ _release(dev);
+ return error;
+ }
+ /* check oversampling and digital filter configuration */
+ if ((DEV_OSR_MAG == 0 && DEV_DIG_FILT == 0) ||
+ (DEV_OSR_MAG == 0 && DEV_DIG_FILT == 1) ||
+ (DEV_OSR_MAG == 1 && DEV_DIG_FILT == 0)) {
+ _release(dev);
+ DEBUG("[mlx90393] error: the configuration of oversampling and digital filter \
+ is not permitted\n");
+ return -EINVAL;
+ }
+ /* magnetic sensor oversampling */
+ if ((error = _write_register_bits(dev, MLX90393_REG_CONF2, MLX90393_MASK_OSR,
+ DEV_OSR_MAG)) != 0) {
+ _release(dev);
+ return error;
+ }
+ /* magnetic sensor digital filter */
+ uint16_t dig_filt_value_mask = DEV_DIG_FILT << MLX90393_SHIFT_DIG_FILT;
+ if ((error = _write_register_bits(dev, MLX90393_REG_CONF2, MLX90393_MASK_DIG_FILT,
+ dig_filt_value_mask)) != 0) {
+ _release(dev);
+ return error;
+ }
+ /* temperature oversampling */
+ uint16_t temp_osr_value_mask = DEV_OSR_TEMP << MLX90393_SHIFT_OSR2;
+ if ((error = _write_register_bits(dev, MLX90393_REG_CONF2, MLX90393_MASK_OSR2,
+ temp_osr_value_mask)) != 0) {
+ _release(dev);
+ return error;
+ }
+ /* gain */
+ uint16_t gain_value_mask = DEV_GAIN << MLX90393_SHIFT_GAIN;
+ if ((error = _write_register_bits(dev, MLX90393_REG_CONF0, MLX90393_MASK_GAIN_SEL,
+ gain_value_mask)) != 0) {
+ _release(dev);
+ return error;
+ }
+ /* resolution */
+ uint16_t xyz_resolution_value_mask = (DEV_RESOLUTION << MLX90393_SHIFT_RES_Z) |
+ (DEV_RESOLUTION << MLX90393_SHIFT_RES_Y) |
+ (DEV_RESOLUTION << MLX90393_SHIFT_RES_X);
+ if ((error = _write_register_bits(dev, MLX90393_REG_CONF2, MLX90393_MASK_RES_XYZ,
+ xyz_resolution_value_mask)) != 0) {
+ _release(dev);
+ return error;
+ }
+ /* burst more */
+ if (DEV_MODE == MLX90393_MODE_BURST) {
+ /* set burst data rate */
+ if ((error = _write_register_bits(dev, MLX90393_REG_CONF1, MLX90393_MASK_BDR,
+ DEV_ODR)) != 0) {
+ _release(dev);
+ return error;
+ }
+ /* start burst mode */
+ if ((error = _write_byte(dev, MLX90393_COMMAND_SB)) != 0) {
+ _release(dev);
+ return error;
+ }
+ if ((error = _check_status_byte(dev)) != 0) {
+ _release(dev);
+ return error;
+ }
+ }
+#if IS_USED(MODULE_MLX90393_WOC)
+ /* wake up on change mode */
+ else if (DEV_MODE == MLX90393_MODE_WOC_ABSOLUTE || DEV_MODE == MLX90393_MODE_WOC_RELATIVE) {
+ /* set absolute or relative wake up on change mode */
+ int woc_mode = 0;
+ if (DEV_MODE == MLX90393_MODE_WOC_RELATIVE) {
+ woc_mode = 1;
+ }
+ woc_mode <<= MLX90393_SHIFT_WOC_MODE;
+ if ((error = _write_register_bits(dev, MLX90393_REG_CONF1, MLX90393_MASK_WOC_DIFF,
+ woc_mode)) != 0) {
+ _release(dev);
+ return error;
+ }
+ /* set burst data rate */
+ if ((error = _write_register_bits(dev, MLX90393_REG_CONF1, MLX90393_MASK_BDR,
+ DEV_ODR)) != 0) {
+ _release(dev);
+ return error;
+ }
+ /* set thresholds */
+ int gain = _get_gain_factor(DEV_GAIN);
+ uint16_t raw_xy_threshold = (1000 * dev->params->threshold.xy / (MLX90393_XY_SENS
+ * (1 << DEV_RESOLUTION) * gain)) * 100;
+ uint16_t raw_z_threshold = (1000 * dev->params->threshold.z / (MLX90393_Z_SENS
+ * (1 << DEV_RESOLUTION) * gain)) * 100;
+ uint16_t raw_temp_threshold = dev->params->threshold.temp * MLX90393_TEMP_RESOLUTION
+ / 1000;
+
+ if ((error = _write_register(dev, MLX90393_REG_WOXY_THRESHOLD, raw_xy_threshold))) {
+ _release(dev);
+ return error;
+ }
+ if ((error = _write_register(dev, MLX90393_REG_WOZ_THRESHOLD, raw_z_threshold))) {
+ _release(dev);
+ return error;
+ }
+ if ((error = _write_register(dev, MLX90393_REG_WOT_THRESHOLD, raw_temp_threshold))) {
+ _release(dev);
+ return error;
+ }
+ /* start wake up on change mode */
+ if ((error = _write_byte(dev, MLX90393_COMMAND_SW)) != 0) {
+ _release(dev);
+ return error;
+ }
+ if ((error = _check_status_byte(dev)) != 0) {
+ _release(dev);
+ return error;
+ }
+ }
+#endif
+
+#if !IS_USED(MODULE_MLX90393_INT)
+ if (DEV_MODE == MLX90393_MODE_SINGLE_MEASUREMENT) {
+ _calculate_conv_time(dev);
+ }
+#endif
+
+ _release(dev);
+ return 0;
+}
+
+#if IS_USED(MODULE_MLX90393_INT)
+
+static void _isr(void *lock)
+{
+ mutex_unlock(lock);
+}
+
+#endif
+
+int mlx90393_read(mlx90393_t *dev, mlx90393_data_t *data)
+{
+ assert(dev);
+ assert(data);
+
+#if IS_USED(MODULE_MLX90393_INT)
+ assert(gpio_is_valid(DEV_INT_PIN));
+#endif
+
+ int error = 0;
+
+ /* start single measurement if used */
+ if (DEV_MODE == MLX90393_MODE_SINGLE_MEASUREMENT) {
+ _acquire(dev);
+ if ((error = _write_byte(dev, MLX90393_COMMAND_SM)) != 0) {
+ _release(dev);
+ return error;
+ }
+ if ((error = _check_status_byte(dev)) != 0) {
+ _release(dev);
+ return error;
+ }
+ _release(dev);
+ }
+
+ uint8_t buffer[9];
+
+#if IS_USED(MODULE_MLX90393_INT)
+#if IS_USED(MODULE_MLX90393_WOC)
+ if (DEV_MODE == MLX90393_MODE_WOC_RELATIVE || DEV_MODE == MLX90393_MODE_WOC_ABSOLUTE) {
+ if ((error = _read_measurement(dev, buffer)) != 0) {
+ return error;
+ }
+ gpio_irq_disable(DEV_INT_PIN);
+ }
+#endif
+ if (DEV_MODE == MLX90393_MODE_SINGLE_MEASUREMENT || DEV_MODE == MLX90393_MODE_BURST) {
+ /* wait for interrupt */
+ mutex_t lock = MUTEX_INIT_LOCKED;
+ gpio_init_int(DEV_INT_PIN, GPIO_IN_PU, GPIO_RISING, _isr, &lock);
+ mutex_lock(&lock);
+ gpio_irq_disable(DEV_INT_PIN);
+ if ((error = _read_measurement(dev, buffer)) != 0) {
+ return error;
+ }
+ }
+#else
+ /* sleep for conversion time in single measurement mode */
+ if (DEV_MODE == MLX90393_MODE_SINGLE_MEASUREMENT) {
+ ztimer_sleep(ZTIMER_MSEC, dev->conversion_time);
+ if ((error = _read_measurement(dev, buffer)) != 0) {
+ return error;
+ }
+ }
+ /* polling in burst mode */
+ else if (DEV_MODE ==MLX90393_MODE_BURST) {
+ while (_read_measurement(dev, buffer) != 0) {
+ ztimer_sleep(ZTIMER_MSEC, MLX90393_BM_READ_TIMEOUT);
+ }
+ }
+#endif
+
+ /* convert read data according to Table 17 and 21 from Datasheet */
+ int16_t raw_x, raw_y, raw_z;
+ uint16_t raw_temp;
+ raw_temp = (uint16_t)((buffer[1] << 8) | buffer[2]);
+ raw_x = (int16_t)((buffer[3] << 8) | buffer[4]);
+ raw_y = (int16_t)((buffer[5] << 8) | buffer[6]);
+ raw_z = (int16_t)((buffer[7] << 8) | buffer[8]);
+
+ if (DEV_RESOLUTION == MLX90393_RES_18) {
+ raw_x -= 0x8000;
+ raw_y -= 0x8000;
+ raw_z -= 0x8000;
+ }
+ else if (DEV_RESOLUTION == MLX90393_RES_19) {
+ raw_x -= 0x4000;
+ raw_y -= 0x4000;
+ raw_z -= 0x4000;
+ }
+
+ data->temp = _calculate_temp(raw_temp, DEV_REF_TEMP);
+ int gain = _get_gain_factor(DEV_GAIN);
+ data->x_axis = ((raw_x * gain) / 100) * MLX90393_XY_SENS * (1 << DEV_RESOLUTION) / 1000;
+ data->y_axis = ((raw_y * gain) / 100) * MLX90393_XY_SENS * (1 << DEV_RESOLUTION) / 1000;
+ data->z_axis = ((raw_z * gain) / 100) * MLX90393_Z_SENS * (1 << DEV_RESOLUTION) / 1000;
+
+ return 0;
+}
+
+#if IS_USED(MODULE_MLX90393_WOC)
+
+void mlx90393_enable_woc(mlx90393_t *dev, mlx90393_cb_t cb, void *arg)
+{
+ assert(dev);
+ assert(gpio_is_valid(DEV_INT_PIN));
+ assert(cb);
+
+ gpio_init_int(DEV_INT_PIN, GPIO_IN_PU, GPIO_RISING, cb, arg);
+}
+
+#endif
+
+int mlx90393_stop_cont(mlx90393_t *dev)
+{
+ assert(dev);
+
+ int error = 0;
+
+ _acquire(dev);
+
+ if ((error = _exit(dev)) != 0) {
+ _release(dev);
+ return error;
+ }
+
+ _release(dev);
+ return 0;
+}
+
+int mlx90393_start_cont(mlx90393_t *dev)
+{
+ assert(dev);
+
+ if (DEV_MODE == MLX90393_MODE_SINGLE_MEASUREMENT) {
+ return -EFAULT;
+ }
+
+ int error = 0;
+
+ _acquire(dev);
+
+ if (DEV_MODE == MLX90393_MODE_BURST) {
+ if ((error = _write_byte(dev, MLX90393_COMMAND_SB)) != 0) {
+ _release(dev);
+ return error;
+ }
+ }
+
+#if IS_USED(MODULE_MLX90393_WOC)
+ else if (DEV_MODE == MLX90393_MODE_WOC_RELATIVE || DEV_MODE == MLX90393_MODE_WOC_ABSOLUTE) {
+ if ((error = _write_byte(dev, MLX90393_COMMAND_SW)) != 0) {
+ _release(dev);
+ return error;
+ }
+ }
+#endif
+
+ if ((error = _check_status_byte(dev)) != 0) {
+ _release(dev);
+ return error;
+ }
+
+ _release(dev);
+ return 0;
+}
+
+#if IS_USED(MODULE_MLX90393_SPI)
+
+static int _init_bus(const mlx90393_t *dev)
+{
+ if (spi_init_cs(DEV_SPI, DEV_CS_PIN) != SPI_OK) {
+ DEBUG("[mlx90393] error: unable to configure the chip select pin\n");
+ return -EIO;
+ }
+ return 0;
+}
+
+static void _acquire(mlx90393_t *dev)
+{
+ spi_acquire(DEV_SPI, DEV_CS_PIN, SPI_MODE, DEV_CLK);
+}
+
+static void _release(mlx90393_t *dev)
+{
+ spi_release(DEV_SPI);
+}
+
+static int _write_byte(mlx90393_t *dev, uint8_t data)
+{
+ spi_transfer_byte(DEV_SPI, DEV_CS_PIN, false, data);
+ return 0;
+}
+
+static int _read_byte(mlx90393_t *dev, uint8_t *buffer)
+{
+ *buffer = spi_transfer_byte(DEV_SPI, DEV_CS_PIN, false, 0x0);
+ return 0;
+}
+
+static int _write_bytes(mlx90393_t *dev, void *data, size_t len)
+{
+ spi_transfer_bytes(DEV_SPI, DEV_CS_PIN, false, data, NULL, len);
+ return 0;
+}
+
+static int _read_bytes(mlx90393_t *dev, void *buffer, size_t len)
+{
+ spi_transfer_bytes(DEV_SPI, DEV_CS_PIN, false, NULL, buffer, len);
+ return 0;
+}
+
+#elif IS_USED(MODULE_MLX90393_I2C)
+
+static int _init_bus(const mlx90393_t *dev)
+{
+ (void)dev;
+ return 0;
+}
+
+static void _acquire(mlx90393_t *dev)
+{
+ i2c_acquire(DEV_I2C);
+}
+
+static void _release(mlx90393_t *dev)
+{
+ i2c_release(DEV_I2C);
+}
+
+static int _write_byte(mlx90393_t *dev, uint8_t data)
+{
+ return i2c_write_byte(DEV_I2C, DEV_ADDR, data, 0) ? -EIO : 0;
+}
+
+static int _read_byte(mlx90393_t *dev, uint8_t *buffer)
+{
+ return i2c_read_byte(DEV_I2C, DEV_ADDR, buffer, 0) ? -EIO : 0;
+}
+
+static int _write_bytes(mlx90393_t *dev, void *data, size_t len)
+{
+ return i2c_write_bytes(DEV_I2C, DEV_ADDR, data, len, 0) ? -EIO : 0;
+}
+
+static int _read_bytes(mlx90393_t *dev, void *buffer, size_t len)
+{
+ return i2c_read_bytes(DEV_I2C, DEV_ADDR, buffer, len, 0) ? -EIO : 0;
+}
+
+#endif
+
+static int _check_status_byte(mlx90393_t *dev)
+{
+ uint8_t status;
+ int error = 0;
+
+ if ((error = _read_byte(dev, &status)) != 0) {
+ return error;
+ }
+ return (status & MLX90393_STATUS_ERROR) ? -EFAULT : 0;
+}
+
+static int _write_register(mlx90393_t *dev, uint8_t addr, uint16_t value)
+{
+ uint8_t buffer[4];
+ buffer[0] = MLX90393_COMMAND_WR;
+ buffer[1] = (uint8_t) (value >> 8);
+ buffer[2] = (uint8_t) (value & 0xFF);
+ buffer[3] = addr << 2;
+
+ int error = 0;
+
+ if ((error = _write_bytes(dev, buffer, 4)) != 0) {
+ return error;
+ }
+ if ((error = _check_status_byte(dev)) != 0) {
+ return error;
+ }
+ return 0;
+}
+
+static int _read_register(mlx90393_t *dev, uint8_t addr, uint16_t *value)
+{
+ uint8_t buffer_send[2];
+ buffer_send[0] = MLX90393_COMMAND_RR;
+ buffer_send[1] = addr << 2;
+ int error = 0;
+
+ if ((error = _write_bytes(dev, buffer_send, 2)) != 0) {
+ return error;
+ }
+ uint8_t buffer_receive[3];
+ if ((error = _read_bytes(dev, buffer_receive, 3)) != 0) {
+ return error;
+ }
+ if (buffer_receive[0] & MLX90393_STATUS_ERROR) {
+ return -EFAULT;
+ }
+ *value = (uint16_t)((buffer_receive[1] << 8) | buffer_receive[2]);
+
+ return 0;
+}
+
+static int _write_register_bits(mlx90393_t *dev, uint8_t addr, uint16_t mask, uint16_t value)
+{
+ uint16_t reg_value;
+ int error = 0;
+
+ if ((error = _read_register(dev, addr, ®_value)) != 0) {
+ return error;
+ }
+ reg_value &= ~mask;
+ reg_value |= (mask & value);
+ if ((error = _write_register(dev, addr, reg_value)) != 0) {
+ return error;
+ }
+ return 0;
+}
+
+static int _calculate_temp(uint16_t raw_temp, uint16_t ref_temp)
+{
+ /* calculate temp in deci celsius (Application note MLX90393 temperature compensation - v4) */
+ return (MLX90393_TEMP_OFFSET + (((raw_temp - ref_temp) * 1000) / MLX90393_TEMP_RESOLUTION));
+}
+
+static int _get_gain_factor(mlx90393_gain_t gain)
+{
+ switch (gain)
+ {
+ case MLX90393_GAIN_5X:
+ return 500;
+ case MLX90393_GAIN_4X:
+ return 400;
+ case MLX90393_GAIN_3X:
+ return 300;
+ case MLX90393_GAIN_2_5X:
+ return 250;
+ case MLX90393_GAIN_2X:
+ return 200;
+ case MLX90393_GAIN_1_67X:
+ return 167;
+ case MLX90393_GAIN_1_33X:
+ return 133;
+ case MLX90393_GAIN_1X:
+ return 100;
+ default:
+ return -1;
+ }
+}
+
+static int _reset(mlx90393_t *dev)
+{
+ int error = 0;
+
+ if ((error = _write_byte(dev, MLX90393_COMMAND_RT)) != 0) {
+ return error;
+ }
+ if ((error = _check_status_byte(dev)) != 0) {
+ return error;
+ }
+ return 0;
+}
+
+static int _exit(mlx90393_t *dev)
+{
+ int error = 0;
+
+ if ((error = _write_byte(dev, MLX90393_COMMAND_EX)) != 0) {
+ return error;
+ }
+ if ((error = _check_status_byte(dev)) != 0) {
+ return error;
+ }
+ return 0;
+}
+
+static int _is_avaiable(mlx90393_t *dev)
+{
+ int error = 0;
+
+ if ((error = _write_register(dev, MLX90393_REG_CONN_TEST, CONN_TEST_DATA)) != 0) {
+ return error;
+ }
+ uint16_t buffer = 0x00;
+ if ((error = _read_register(dev, MLX90393_REG_CONN_TEST, &buffer)) != 0) {
+ return error;
+ }
+ return buffer == CONN_TEST_DATA ? 0 : -ENXIO;
+}
+
+#if !IS_USED(MODULE_MLX90393_INT)
+
+static void _calculate_conv_time(mlx90393_t *dev)
+{
+ /* calculate single measurement conversion time in ms
+ (Datasheet table 8: Timing specifications) */
+ int conv_mag = 67 + 64 * powi(2, DEV_OSR_MAG) * (2 + powi(2, DEV_DIG_FILT));
+ int conv_temp = 67 + 192 * powi(2, DEV_OSR_TEMP);
+ dev->conversion_time = (MLX90393_T_STBY + MLX90393_T_ACTIVE + 3 * conv_mag + conv_temp
+ + MLX90393_T_CONV_END) / 1000 + 1;
+}
+
+#endif
+
+static int _read_measurement(mlx90393_t *dev, uint8_t *buffer)
+{
+ int error = 0;
+
+ _acquire(dev);
+
+ /* read measurement */
+ if ((error = _write_byte(dev, MLX90393_COMMAND_RM)) != 0) {
+ _release(dev);
+ return error;
+ }
+ /* check status byte */
+ if ((error = _read_bytes(dev, buffer, 9)) != 0) {
+ _release(dev);
+ return error;
+ }
+ if (buffer[0] & MLX90393_STATUS_ERROR) {
+ DEBUG("Data could not be read out\n\r");
+ _release(dev);
+ return -EFAULT;
+ }
+ _release(dev);
+ return 0;
+}
diff --git a/drivers/mlx90393/mlx90393_saul.c b/drivers/mlx90393/mlx90393_saul.c
new file mode 100644
index 000000000000..164eed397b25
--- /dev/null
+++ b/drivers/mlx90393/mlx90393_saul.c
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2023 Michael Ristau
+ *
+ * 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_mlx90393
+ * @{
+ *
+ * @file
+ * @brief MLX90393 adaption to the RIOT actuator/sensor interface
+ *
+ * @author Michael Ristau
+ *
+ * @}
+ */
+
+#include "saul.h"
+#include "mlx90393.h"
+
+static int read(const void *dev, phydat_t *res)
+{
+ mlx90393_data_t data;
+
+ if (mlx90393_read((mlx90393_t*) dev, &data) == 0) {
+ res->val[0] = (int16_t)(data.x_axis / 10);
+ res->val[1] = (int16_t)(data.y_axis / 10);
+ res->val[2] = (int16_t)(data.z_axis / 10);
+ res->unit = UNIT_T;
+ res->scale = -5;
+ return 1;
+ }
+ return -ECANCELED;
+}
+
+const saul_driver_t mlx90393_saul_driver = {
+ .read = read,
+ .write = saul_write_notsup,
+ .type = SAUL_SENSE_MAG,
+};
diff --git a/tests/drivers/mlx90393/Makefile b/tests/drivers/mlx90393/Makefile
new file mode 100644
index 000000000000..b3c0a12c49cc
--- /dev/null
+++ b/tests/drivers/mlx90393/Makefile
@@ -0,0 +1,9 @@
+include ../Makefile.drivers_common
+
+DRIVER ?= mlx90393_i2c
+
+USEMODULE += mlx90393
+USEMODULE += $(DRIVER)
+USEMODULE += ztimer_sec
+
+include $(RIOTBASE)/Makefile.include
diff --git a/tests/drivers/mlx90393/Makefile.ci b/tests/drivers/mlx90393/Makefile.ci
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/tests/drivers/mlx90393/README.md b/tests/drivers/mlx90393/README.md
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/tests/drivers/mlx90393/main.c b/tests/drivers/mlx90393/main.c
new file mode 100644
index 000000000000..a91a3824ea77
--- /dev/null
+++ b/tests/drivers/mlx90393/main.c
@@ -0,0 +1,194 @@
+/*
+ * Copyright (C) 2023 Michael Ristau
+ *
+ * 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_mlx90393
+ * @{
+ *
+ * @file
+ * @brief Test application for the MLX90393 magnetometer driver.
+ *
+ * @author Michael Ristau
+ *
+ * ## About
+ *
+ * The test application demonstrates the use of different functions of
+ * the MLX90393 sensor driver depending on the used modules and configuration params.
+ *
+ * Pseudomodule | Functionality
+ * :-------------------|:-------------------------------------------------------
+ * `mlx90393_i2c` | Use I2C bus
+ * `mlx90393_spi` | Use SPI bus
+ * `mlx90393_int` | Data ready interrupt handling
+ * `mlx90393_woc` | Wake-up on change mode
+ *
+ * By default the test application uses the I2C bus, polling and the default params set
+ * defined in file mlx90393_params.h. The default params use the Burst mode.
+ * To use data ready interrupts instead of polling for new data, the `mlx90393_int` module
+ * has to be used.
+ *
+ * ## Usage
+ * To compile and execute the test application, use command in the test directory:
+ * make BOARD=... flash
+ *
+ * To test the different driver functions you can overwrite the parameters in the
+ * default configuration set or add modules.
+ *
+ * Some examples:
+ *
+ * Wake-up on change mode absolute:
+ * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ * CFLAGS="-DMLX90393_PARAM_MODE=MLX90393_MODE_WOC_ABSOLUTE" \
+ * USEMODULE='mlx90393_woc' \
+ * make BOARD=... flash
+ * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ *
+ * Single measurement mode:
+ * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ * CFLAGS="-DMLX90393_PARAM_MODE=MLX90393_MODE_SINGLE_MEASUREMENT" \
+ * make BOARD=... flash
+ * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ *
+ * Burst mode with interrupt:
+ * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ * CFLAGS="-DMLX90393_PARAM_MODE=MLX90393_MODE_BURST" \
+ * USEMODULE='mlx90393_int' \
+ * make BOARD=... flash
+ * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ *
+ * To test the sensor with the SPI Interface you can use the mlx90393_spi module:
+ * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ * DRIVER='mlx90393_spi' \
+ * make BOARD=... flash
+ * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ * @}
+ */
+
+#include
+#include
+#include
+#include "mlx90393.h"
+#include "mlx90393_params.h"
+#include "ztimer.h"
+
+#define BUS_ERROR -EIO
+#define DEVICE_NOT_AVAILABLE -ENXIO
+#define INVALID_PARAM -EINVAL
+#define DEVICE_ERROR -EFAULT
+
+static void print_error(int error)
+{
+ switch (error)
+ {
+ case 0:
+ puts("No error");
+ break;
+ case BUS_ERROR:
+ puts("Communication bus error");
+ break;
+ case DEVICE_NOT_AVAILABLE:
+ puts("Connectivity error, device not available");
+ break;
+ case INVALID_PARAM:
+ puts("Invalid configuration parameter");
+ break;
+ case DEVICE_ERROR:
+ puts("Device error");
+ break;
+ default:
+ break;
+ }
+}
+
+#if IS_USED(MODULE_MLX90393_WOC)
+void woc_cb(void *arg)
+{
+ *(int*)arg = 1;
+}
+#endif
+
+int main(void)
+{
+ puts("MLX90393 magnetometer driver test application\n\r");
+#if IS_USED(MODULE_MLX90393_SPI)
+ printf("Initializing MLX90393 magnetometer at SPI_%i", mlx90393_params[0].spi);
+#elif IS_USED(MODULE_MLX90393_I2C)
+ printf("Initializing MLX90393 magnetometer at I2C_%i", mlx90393_params[0].i2c);
+#endif
+
+ mlx90393_t dev;
+ int error = 0;
+
+#if IS_USED(MODULE_MLX90393_WOC)
+ int woc_triggered = 0;
+#endif
+
+ if ((error = mlx90393_init(&dev, &mlx90393_params[0])) != 0) {
+ puts("[FAILED]");
+ print_error(error);
+ return -1;
+ }
+ puts("[SUCCESS]");
+
+ unsigned count = 0;
+ mlx90393_data_t data;
+
+ puts("Starting read data from the device");
+
+#if IS_USED(MODULE_MLX90393_WOC)
+ mlx90393_enable_woc(&dev, woc_cb, &woc_triggered);
+#endif
+ while (1) {
+
+#if IS_USED(MODULE_MLX90393_WOC)
+ if (woc_triggered) {
+ if ((error = mlx90393_read(&dev, &data)) != 0) {
+ puts("Failed to read data from the device");
+ print_error(error);
+ return -1;
+ }
+ printf("Field strength: X: %ld uT Y: %ld uT Z: %ld uT\n\r",
+ data.x_axis, data.y_axis, data.z_axis);
+ printf("Temperature: %d d°C\n\r", data.temp);
+ count++;
+ woc_triggered = 0;
+ mlx90393_enable_woc(&dev, woc_cb, &woc_triggered);
+ }
+#endif
+ if (dev.params->mode == MLX90393_MODE_BURST ||
+ dev.params->mode == MLX90393_MODE_SINGLE_MEASUREMENT) {
+ if ((error = mlx90393_read(&dev, &data)) != 0) {
+ puts("Failed to read data from the device");
+ print_error(error);
+ return -1;
+ }
+ printf("Field strength: X: %ld uT Y: %ld uT Z: %ld uT\n\r",
+ data.x_axis, data.y_axis, data.z_axis);
+ printf("Temperature: %d d°C\n\r", data.temp);
+
+ if (dev.params->mode == MLX90393_MODE_SINGLE_MEASUREMENT) {
+ ztimer_sleep(ZTIMER_SEC, 1);
+ }
+ count++;
+ }
+
+ /*
+ * the continuous measurement is stopped, the sensor is set to idle mode
+ * and started again after 5 seconds every 50 cycles
+ */
+ if (dev.params->mode != MLX90393_MODE_SINGLE_MEASUREMENT && count == 50) {
+ mlx90393_stop_cont(&dev);
+ puts("Measurement stopped and sensor set to idle mode.");
+ ztimer_sleep(ZTIMER_SEC, 5);
+ mlx90393_start_cont(&dev);
+ puts("Measurement started again.");
+ count = 0;
+ }
+ }
+ return 0;
+}