drivers: sensor: Add driver for MAX32664C
- Add DTS for MAX32664C
- Add driver for MAX32664C
- Add example for MAX32664C Heart rate measurement with Bluetooth
- Add private attributes and channels for health measurement
Closes: #93473
Signed-off-by: Daniel Kampert <DanielKampert@kampis-elektroecke.de>
diff --git a/drivers/sensor/adi/CMakeLists.txt b/drivers/sensor/adi/CMakeLists.txt
index f0bb591..e524f38 100644
--- a/drivers/sensor/adi/CMakeLists.txt
+++ b/drivers/sensor/adi/CMakeLists.txt
@@ -10,4 +10,5 @@
add_subdirectory_ifdef(CONFIG_ADXL362 adxl362)
add_subdirectory_ifdef(CONFIG_ADXL367 adxl367)
add_subdirectory_ifdef(CONFIG_ADXL372 adxl372)
+add_subdirectory_ifdef(CONFIG_SENSOR_MAX32664C max32664c)
# zephyr-keep-sorted-stop
diff --git a/drivers/sensor/adi/Kconfig b/drivers/sensor/adi/Kconfig
index dea030c..d0812de 100644
--- a/drivers/sensor/adi/Kconfig
+++ b/drivers/sensor/adi/Kconfig
@@ -10,4 +10,5 @@
source "drivers/sensor/adi/adxl362/Kconfig"
source "drivers/sensor/adi/adxl367/Kconfig"
source "drivers/sensor/adi/adxl372/Kconfig"
+source "drivers/sensor/adi/max32664c/Kconfig"
# zephyr-keep-sorted-stop
diff --git a/drivers/sensor/adi/max32664c/CMakeLists.txt b/drivers/sensor/adi/max32664c/CMakeLists.txt
new file mode 100644
index 0000000..b36663c
--- /dev/null
+++ b/drivers/sensor/adi/max32664c/CMakeLists.txt
@@ -0,0 +1,13 @@
+# SPDX-License-Identifier: Apache-2.0
+
+zephyr_library()
+
+zephyr_sources_ifdef(CONFIG_MAX32664C_USE_FIRMWARE_LOADER max32664c_bl.c)
+
+zephyr_sources_ifdef(CONFIG_MAX32664C_USE_INTERRUPT max32664c_interrupt.c)
+
+zephyr_sources_ifdef(CONFIG_SENSOR_MAX32664C max32664c.c
+ max32664c_worker.c
+ max32664c_init.c
+ max32664c_acc.c
+ )
diff --git a/drivers/sensor/adi/max32664c/Kconfig b/drivers/sensor/adi/max32664c/Kconfig
new file mode 100644
index 0000000..5b939fa
--- /dev/null
+++ b/drivers/sensor/adi/max32664c/Kconfig
@@ -0,0 +1,45 @@
+# Copyright(c) 2025, Daniel Kampert
+# SPDX-License-Identifier: Apache-2.0
+
+config SENSOR_MAX32664C
+ bool "MAX32664C Driver"
+ default y
+ depends on DT_HAS_MAXIM_MAX32664C_ENABLED
+ select I2C
+ help
+ Enable the driver for the MAX32664C biometric sensor hub.
+
+if SENSOR_MAX32664C
+config MAX32664C_USE_FIRMWARE_LOADER
+ bool "Use this option if you want to flash the sensor hub over the I2C firmware loader"
+
+config MAX32664C_USE_EXTERNAL_ACC
+ bool "Use this option if you want to use an external accelerometer"
+
+config MAX32664C_USE_EXTENDED_REPORTS
+ bool "Use this option if you want to use extended reports instead of the default reports"
+
+config MAX32664C_USE_STATIC_MEMORY
+ bool "Disable this option if the driver should use dynamic memory"
+ default y
+
+config MAX32664C_QUEUE_SIZE
+ int "Length of the message queue"
+ default 32
+
+config MAX32664C_SAMPLE_BUFFER_SIZE
+ depends on MAX32664C_USE_STATIC_MEMORY
+ int "Length of the sample buffer for the I2C reading thread"
+ default 64
+ help
+ This is the number of samples that will be read from the sensor hub in one go.
+ The maximum value is 64, but you can set it lower if you want to reduce memory usage.
+
+config MAX32664C_THREAD_STACK_SIZE
+ int "MAX32664C sample thread stack size"
+ default 4096
+
+config MAX32664C_USE_INTERRUPT
+ bool "Use this option if you want to use the MFIO interrupt support"
+ depends on GPIO
+endif
diff --git a/drivers/sensor/adi/max32664c/max32664c.c b/drivers/sensor/adi/max32664c/max32664c.c
new file mode 100644
index 0000000..99d89cc
--- /dev/null
+++ b/drivers/sensor/adi/max32664c/max32664c.c
@@ -0,0 +1,1101 @@
+/*
+ * Copyright (c) 2025, Daniel Kampert
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#include <zephyr/sys/byteorder.h>
+
+#include "max32664c.h"
+
+#define DT_DRV_COMPAT maxim_max32664c
+
+#if (DT_NUM_INST_STATUS_OKAY(DT_DRV_COMPAT) == 0)
+#warning "max32664c driver enabled without any devices"
+#endif
+
+LOG_MODULE_REGISTER(maxim_max32664c, CONFIG_SENSOR_LOG_LEVEL);
+
+int max32664c_i2c_transmit(const struct device *dev, uint8_t *tx_buf, uint8_t tx_len,
+ uint8_t *rx_buf, uint32_t rx_len, uint16_t delay_ms)
+{
+ const struct max32664c_config *config = dev->config;
+
+ /* Wake up the sensor hub before the transmission starts (min. 300 us) */
+ gpio_pin_set_dt(&config->mfio_gpio, false);
+ k_usleep(500);
+
+ if (i2c_write_dt(&config->i2c, tx_buf, tx_len)) {
+ LOG_ERR("I2C transmission error!");
+ return -EBUSY;
+ }
+
+ k_msleep(delay_ms);
+
+ if (i2c_read_dt(&config->i2c, rx_buf, rx_len)) {
+ LOG_ERR("I2C read error!");
+ return -EBUSY;
+ }
+
+ k_msleep(MAX32664C_DEFAULT_CMD_DELAY);
+
+ /* The sensor hub can enter sleep mode again now */
+ gpio_pin_set_dt(&config->mfio_gpio, true);
+ k_usleep(300);
+
+ /* Check the status byte for a valid transaction */
+ if (rx_buf[0] != 0) {
+ return -EINVAL;
+ }
+
+ return 0;
+}
+
+/** @brief Check the accelerometer and AFE WHOAMI registers.
+ * This function is called during device initialization.
+ * @param dev Pointer to device
+ * @return 0 when successful
+ */
+static int max32664c_check_sensors(const struct device *dev)
+{
+ uint8_t afe_id;
+ uint8_t tx[3];
+ uint8_t rx[2];
+ struct max32664c_data *data = dev->data;
+ const struct max32664c_config *config = dev->config;
+
+ LOG_DBG("Checking sensors...");
+
+ /* Read MAX86141 WHOAMI */
+ tx[0] = 0x41;
+ tx[1] = 0x00;
+ tx[2] = 0xFF;
+ if (max32664c_i2c_transmit(dev, tx, 3, rx, 2, MAX32664C_DEFAULT_CMD_DELAY)) {
+ return -EINVAL;
+ }
+
+ if (config->use_max86141) {
+ LOG_DBG("\tUsing MAX86141 as AFE");
+ afe_id = 0x25;
+ } else if (config->use_max86161) {
+ LOG_DBG("\tUsing MAX86161 as AFE");
+ afe_id = 0x36;
+ } else {
+ LOG_ERR("\tNo AFE defined!");
+ return -ENODEV;
+ }
+
+ data->afe_id = rx[1];
+ if (data->afe_id != afe_id) {
+ LOG_ERR("\tAFE WHOAMI failed: 0x%X", data->afe_id);
+ return -ENODEV;
+ }
+
+ LOG_DBG("\tAFE WHOAMI OK: 0x%X", data->afe_id);
+
+ /* Read Accelerometer WHOAMI */
+ tx[0] = 0x41;
+ tx[1] = 0x04;
+ tx[2] = 0x0F;
+ if (max32664c_i2c_transmit(dev, tx, 3, rx, 2, MAX32664C_DEFAULT_CMD_DELAY)) {
+ return -EINVAL;
+ }
+
+ data->accel_id = rx[1];
+ /* The sensor hub firmware supports only two accelerometers and one is set to */
+ /* EoL. The remaining one is the ST LIS2DS12. */
+ if (data->accel_id != 0x43) {
+ LOG_ERR("\tAccelerometer WHOAMI failed: 0x%X", data->accel_id);
+ return -ENODEV;
+ }
+
+ LOG_DBG("\tAccelerometer WHOAMI OK: 0x%X", data->accel_id);
+
+ return 0;
+}
+
+/** @brief Stop the current algorithm.
+ * @param dev Pointer to device
+ * @return 0 when successful
+ */
+static int max32664c_stop_algo(const struct device *dev)
+{
+ uint8_t rx;
+ uint8_t tx[3];
+ struct max32664c_data *data = dev->data;
+
+ if (data->op_mode == MAX32664C_OP_MODE_IDLE) {
+ LOG_DBG("No algorithm running, nothing to stop.");
+ return 0;
+ }
+
+ LOG_DBG("Stop the current algorithm...");
+
+ /* Stop the algorithm */
+ tx[0] = 0x52;
+ tx[1] = 0x07;
+ tx[2] = 0x00;
+ if (max32664c_i2c_transmit(dev, tx, 3, &rx, 1, 120)) {
+ return -EINVAL;
+ }
+
+ switch (data->op_mode) {
+ case MAX32664C_OP_MODE_RAW: {
+#ifndef CONFIG_MAX32664C_USE_STATIC_MEMORY
+ k_msgq_cleanup(&data->raw_report_queue);
+#endif /* CONFIG_MAX32664C_USE_STATIC_MEMORY */
+ break;
+ }
+#ifdef CONFIG_MAX32664C_USE_EXTENDED_REPORTS
+ case MAX32664C_OP_MODE_ALGO_AEC_EXT:
+ case MAX32664C_OP_MODE_ALGO_AGC_EXT: {
+#ifndef CONFIG_MAX32664C_USE_STATIC_MEMORY
+ k_msgq_cleanup(&data->ext_report_queue);
+#endif /* CONFIG_MAX32664C_USE_STATIC_MEMORY */
+ break;
+ }
+#else
+ case MAX32664C_OP_MODE_ALGO_AEC:
+ case MAX32664C_OP_MODE_ALGO_AGC: {
+#ifndef CONFIG_MAX32664C_USE_STATIC_MEMORY
+ k_msgq_cleanup(&data->report_queue);
+#endif /* CONFIG_MAX32664C_USE_STATIC_MEMORY */
+ break;
+ }
+#endif /* CONFIG_MAX32664C_USE_EXTENDED_REPORTS */
+ case MAX32664C_OP_MODE_SCD: {
+#ifndef CONFIG_MAX32664C_USE_STATIC_MEMORY
+ k_msgq_cleanup(&data->scd_report_queue);
+#endif /* CONFIG_MAX32664C_USE_STATIC_MEMORY */
+ break;
+ }
+ default: {
+ LOG_ERR("Unknown algorithm mode: %d", data->op_mode);
+ return -EINVAL;
+ }
+ };
+
+ data->op_mode = MAX32664C_OP_MODE_IDLE;
+
+ k_thread_suspend(data->thread_id);
+
+ return 0;
+}
+
+/** @brief Put the device into raw measurement mode.
+ * @param dev Pointer to device
+ * @return 0 when successful
+ */
+static int max32664c_set_mode_raw(const struct device *dev)
+{
+ uint8_t rx;
+ uint8_t tx[4];
+ struct max32664c_data *data = dev->data;
+
+ /* Stop the current algorithm mode */
+ if (max32664c_stop_algo(dev)) {
+ LOG_ERR("Failed to stop the algorithm!");
+ return -EINVAL;
+ }
+
+ LOG_INF("Entering RAW mode...");
+
+ /* Set the output format to sensor data only */
+ tx[0] = 0x10;
+ tx[1] = 0x00;
+ tx[2] = MAX32664C_OUT_SENSOR_ONLY;
+ if (max32664c_i2c_transmit(dev, tx, 3, &rx, 1, MAX32664C_DEFAULT_CMD_DELAY)) {
+ return -EINVAL;
+ }
+
+ /* Enable the AFE */
+ tx[0] = 0x44;
+ tx[1] = 0x00;
+ tx[2] = 0x01;
+ tx[3] = 0x00;
+ if (max32664c_i2c_transmit(dev, tx, 4, &rx, 1, 250)) {
+ return -EINVAL;
+ }
+
+ /* Enable the accelerometer */
+ if (max32664c_acc_enable(dev, true)) {
+ return -EINVAL;
+ }
+
+ /* Set AFE sample rate to 100 Hz */
+ tx[0] = 0x40;
+ tx[1] = 0x00;
+ tx[2] = 0x12;
+ tx[3] = 0x18;
+ if (max32664c_i2c_transmit(dev, tx, 4, &rx, 1, MAX32664C_DEFAULT_CMD_DELAY)) {
+ return -EINVAL;
+ }
+
+ /* Set the LED current */
+ for (uint8_t i = 0; i < sizeof(data->led_current); i++) {
+ tx[0] = 0x40;
+ tx[1] = 0x00;
+ tx[2] = 0x23 + i;
+ tx[3] = data->led_current[i];
+ LOG_INF("Set LED%d current: %u", i + 1, data->led_current[i]);
+ if (max32664c_i2c_transmit(dev, tx, 4, &rx, 1, MAX32664C_DEFAULT_CMD_DELAY)) {
+ LOG_ERR("Can not set LED%d current", i + 1);
+ return -EINVAL;
+ }
+ }
+
+#ifndef CONFIG_MAX32664C_USE_STATIC_MEMORY
+ if (k_msgq_alloc_init(&data->raw_report_queue, sizeof(struct max32664c_raw_report_t),
+ CONFIG_MAX32664C_QUEUE_SIZE)) {
+ LOG_ERR("Failed to allocate RAW report queue!");
+ return -ENOMEM;
+ }
+#endif /* CONFIG_MAX32664C_USE_STATIC_MEMORY */
+
+ data->op_mode = MAX32664C_OP_MODE_RAW;
+
+ k_thread_resume(data->thread_id);
+
+ return 0;
+}
+
+/** @brief Put the sensor hub into algorithm mode.
+ * @param dev Pointer to device
+ * @param device_mode Target device mode
+ * @param algo_mode Target algorithm mode
+ * @param extended Set to #true when the extended mode should be used
+ * @return 0 when successful
+ */
+static int max32664c_set_mode_algo(const struct device *dev, enum max32664c_device_mode device_mode,
+ enum max32664c_algo_mode algo_mode, bool extended)
+{
+ uint8_t rx;
+ uint8_t tx[5];
+ struct max32664c_data *data = dev->data;
+
+ /* Stop the current algorithm mode */
+ if (max32664c_stop_algo(dev)) {
+ LOG_ERR("Failed to stop the algorithm!");
+ return -EINVAL;
+ }
+
+ LOG_DBG("Entering algorithm mode...");
+
+#ifndef CONFIG_MAX32664C_USE_EXTENDED_REPORTS
+ if (extended) {
+ LOG_ERR("No support for extended reports enabled!");
+ return -EINVAL;
+ }
+#endif /* CONFIG_MAX32664C_USE_EXTENDED_REPORTS */
+
+ /* Set the output mode to sensor and algorithm data */
+ tx[0] = 0x10;
+ tx[1] = 0x00;
+ tx[2] = MAX32664C_OUT_ALGO_AND_SENSOR;
+ if (max32664c_i2c_transmit(dev, tx, 3, &rx, 1, MAX32664C_DEFAULT_CMD_DELAY)) {
+ return -EINVAL;
+ }
+
+ /* Set the algorithm mode */
+ tx[0] = 0x50;
+ tx[1] = 0x07;
+ tx[2] = 0x0A;
+ tx[3] = algo_mode;
+ if (max32664c_i2c_transmit(dev, tx, 4, &rx, 1, MAX32664C_DEFAULT_CMD_DELAY)) {
+ return -EINVAL;
+ }
+
+ if (device_mode == MAX32664C_OP_MODE_ALGO_AEC) {
+ LOG_DBG("Entering AEC mode...");
+
+ /* Enable AEC */
+ tx[0] = 0x50;
+ tx[1] = 0x07;
+ tx[2] = 0x0B;
+ tx[3] = 0x01;
+ if (max32664c_i2c_transmit(dev, tx, 4, &rx, 1, MAX32664C_DEFAULT_CMD_DELAY)) {
+ return -EINVAL;
+ }
+
+ /* Enable Auto PD */
+ tx[0] = 0x50;
+ tx[1] = 0x07;
+ tx[2] = 0x12;
+ tx[3] = 0x01;
+ if (max32664c_i2c_transmit(dev, tx, 4, &rx, 1, MAX32664C_DEFAULT_CMD_DELAY)) {
+ return -EINVAL;
+ }
+
+ /* Enable SCD */
+ LOG_DBG("Enabling SCD...");
+ tx[0] = 0x50;
+ tx[1] = 0x07;
+ tx[2] = 0x0C;
+ tx[3] = 0x01;
+ if (max32664c_i2c_transmit(dev, tx, 4, &rx, 1, MAX32664C_DEFAULT_CMD_DELAY)) {
+ return -EINVAL;
+ }
+
+ data->op_mode = MAX32664C_OP_MODE_ALGO_AEC;
+
+ if (extended) {
+ data->op_mode = MAX32664C_OP_MODE_ALGO_AEC_EXT;
+ }
+ } else if (device_mode == MAX32664C_OP_MODE_ALGO_AGC) {
+ LOG_DBG("Entering AGC mode...");
+
+ /* TODO: Test if this works */
+ /* Set the LED current */
+ for (uint8_t i = 0; i < sizeof(data->led_current); i++) {
+ tx[0] = 0x40;
+ tx[1] = 0x00;
+ tx[2] = 0x23 + i;
+ tx[3] = data->led_current[i];
+ LOG_INF("Set LED%d current: %u", i + 1, data->led_current[i]);
+ if (max32664c_i2c_transmit(dev, tx, 4, &rx, 1,
+ MAX32664C_DEFAULT_CMD_DELAY)) {
+ LOG_ERR("Can not set LED%d current", i + 1);
+ return -EINVAL;
+ }
+ }
+
+ /* Enable AEC */
+ tx[0] = 0x50;
+ tx[1] = 0x07;
+ tx[2] = 0x0B;
+ tx[3] = 0x01;
+ if (max32664c_i2c_transmit(dev, tx, 4, &rx, 1, MAX32664C_DEFAULT_CMD_DELAY)) {
+ return -EINVAL;
+ }
+
+ /* Disable PD auto current calculation */
+ tx[0] = 0x50;
+ tx[1] = 0x07;
+ tx[2] = 0x12;
+ tx[3] = 0x00;
+ if (max32664c_i2c_transmit(dev, tx, 4, &rx, 1, MAX32664C_DEFAULT_CMD_DELAY)) {
+ return -EINVAL;
+ }
+
+ /* Disable SCD */
+ tx[0] = 0x50;
+ tx[1] = 0x07;
+ tx[2] = 0x0C;
+ tx[3] = 0x00;
+ if (max32664c_i2c_transmit(dev, tx, 4, &rx, 1, MAX32664C_DEFAULT_CMD_DELAY)) {
+ return -EINVAL;
+ }
+
+ /* Set AGC target PD current to 10 uA */
+ /* TODO: Add setting of PD current via API or DT? */
+ tx[0] = 0x50;
+ tx[1] = 0x07;
+ tx[2] = 0x11;
+ tx[3] = 0x00;
+ tx[4] = 0x64;
+ if (max32664c_i2c_transmit(dev, tx, 5, &rx, 1, MAX32664C_DEFAULT_CMD_DELAY)) {
+ return -EINVAL;
+ }
+
+ data->op_mode = MAX32664C_OP_MODE_ALGO_AGC;
+
+ if (extended) {
+ data->op_mode = MAX32664C_OP_MODE_ALGO_AGC_EXT;
+ }
+ } else {
+ LOG_ERR("Invalid mode!");
+ return -EINVAL;
+ }
+
+ /* Enable HR and SpO2 algorithm */
+ tx[2] = 0x01;
+ if (extended) {
+ tx[2] = 0x02;
+ }
+
+ tx[0] = 0x52;
+ tx[1] = 0x07;
+
+ /* Use the maximum time to cover all modes (see Table 6 and 12 in the User Guide) */
+ if (max32664c_i2c_transmit(dev, tx, 3, &rx, 1, 500)) {
+ return -EINVAL;
+ }
+
+#ifndef CONFIG_MAX32664C_USE_STATIC_MEMORY
+ if (k_msgq_alloc_init(&data->raw_report_queue, sizeof(struct max32664c_raw_report_t),
+ CONFIG_MAX32664C_QUEUE_SIZE)) {
+ LOG_ERR("Failed to allocate RAW report queue!");
+ return -ENOMEM;
+ }
+
+ if (!extended && k_msgq_alloc_init(&data->report_queue, sizeof(struct max32664c_report_t),
+ CONFIG_MAX32664C_QUEUE_SIZE)) {
+ LOG_ERR("Failed to allocate report queue!");
+ return -ENOMEM;
+ }
+
+#ifdef CONFIG_MAX32664C_USE_EXTENDED_REPORTS
+ if (extended &&
+ k_msgq_alloc_init(&data->ext_report_queue, sizeof(struct max32664c_ext_report_t),
+ CONFIG_MAX32664C_QUEUE_SIZE)) {
+ LOG_ERR("Failed to allocate extended report queue!");
+ return -ENOMEM;
+ }
+#endif /* CONFIG_MAX32664C_USE_EXTENDED_REPORTS */
+#endif /* CONFIG_MAX32664C_USE_STATIC_MEMORY */
+
+ k_thread_resume(data->thread_id);
+
+ return 0;
+}
+
+/** @brief Enable the skin contact detection only mode.
+ * @param dev Pointer to device
+ * @return 0 when successful
+ */
+static int max32664c_set_mode_scd(const struct device *dev)
+{
+ uint8_t rx;
+ uint8_t tx[4];
+ struct max32664c_data *data = dev->data;
+
+ /* Stop the current algorithm mode */
+ if (max32664c_stop_algo(dev)) {
+ LOG_ERR("Failed to stop the algorithm!");
+ return -EINVAL;
+ }
+
+ LOG_DBG("MAX32664C entering SCD mode...");
+
+ /* Use LED2 for SCD */
+ tx[0] = 0xE5;
+ tx[1] = 0x02;
+ if (max32664c_i2c_transmit(dev, tx, 2, &rx, 1, MAX32664C_DEFAULT_CMD_DELAY)) {
+ return -EINVAL;
+ }
+
+ /* Set the output mode to algorithm data */
+ tx[0] = 0x10;
+ tx[1] = 0x00;
+ tx[2] = MAX32664C_OUT_ALGORITHM_ONLY;
+ if (max32664c_i2c_transmit(dev, tx, 3, &rx, 1, MAX32664C_DEFAULT_CMD_DELAY)) {
+ return -EINVAL;
+ }
+
+ /* Enable SCD only algorithm */
+ tx[0] = 0x52;
+ tx[1] = 0x07;
+ tx[2] = 0x03;
+ if (max32664c_i2c_transmit(dev, tx, 3, &rx, 1, 500)) {
+ return -EINVAL;
+ }
+
+#ifndef CONFIG_MAX32664C_USE_STATIC_MEMORY
+ if (k_msgq_alloc_init(&data->scd_report_queue, sizeof(struct max32664c_scd_report_t),
+ CONFIG_MAX32664C_QUEUE_SIZE)) {
+ LOG_ERR("Failed to allocate SCD report queue!");
+ return -ENOMEM;
+ }
+#endif /* CONFIG_MAX32664C_USE_STATIC_MEMORY */
+
+ data->op_mode = MAX32664C_OP_MODE_SCD;
+
+ k_thread_resume(data->thread_id);
+
+ return 0;
+}
+
+static int max32664c_set_mode_wake_on_motion(const struct device *dev)
+{
+ uint8_t rx;
+ uint8_t tx[6];
+ struct max32664c_data *data = dev->data;
+
+ LOG_DBG("MAX32664C entering wake on motion mode...");
+
+ /* Stop the current algorithm */
+ tx[0] = 0x52;
+ tx[1] = 0x07;
+ tx[2] = 0x00;
+ if (max32664c_i2c_transmit(dev, tx, 3, &rx, 1, MAX32664C_DEFAULT_CMD_DELAY)) {
+ return -EINVAL;
+ }
+
+ /* Set the motion detection threshold (see Table 12 in the SpO2 and Heart Rate Using Guide)
+ */
+ tx[0] = 0x46;
+ tx[1] = 0x04;
+ tx[2] = 0x00;
+ tx[3] = 0x01;
+ tx[4] = MAX32664C_MOTION_TIME(data->motion_time);
+ tx[5] = MAX32664C_MOTION_THRESHOLD(data->motion_threshold);
+ if (max32664c_i2c_transmit(dev, tx, 6, &rx, 1, MAX32664C_DEFAULT_CMD_DELAY)) {
+ return -EINVAL;
+ }
+
+ /* Set the output mode to sensor data */
+ tx[0] = 0x10;
+ tx[1] = 0x00;
+ tx[2] = MAX32664C_OUT_SENSOR_ONLY;
+ if (max32664c_i2c_transmit(dev, tx, 3, &rx, 1, MAX32664C_DEFAULT_CMD_DELAY)) {
+ return -EINVAL;
+ }
+
+ /* Enable the accelerometer */
+ if (max32664c_acc_enable(dev, true)) {
+ return -EINVAL;
+ }
+
+ data->op_mode = MAX32664C_OP_MODE_WAKE_ON_MOTION;
+
+ return 0;
+}
+
+static int max32664c_exit_mode_wake_on_motion(const struct device *dev)
+{
+ uint8_t rx;
+ uint8_t tx[6];
+ struct max32664c_data *data = dev->data;
+
+ LOG_DBG("MAX32664C exiting wake on motion mode...");
+
+ /* Exit wake on motion mode */
+ tx[0] = 0x46;
+ tx[1] = 0x04;
+ tx[2] = 0x00;
+ tx[3] = 0x00;
+ tx[4] = 0xFF;
+ tx[5] = 0xFF;
+ if (max32664c_i2c_transmit(dev, tx, 6, &rx, 1, MAX32664C_DEFAULT_CMD_DELAY)) {
+ return -EINVAL;
+ }
+
+ /* Disable the accelerometer */
+ if (max32664c_acc_enable(dev, false)) {
+ return -EINVAL;
+ }
+
+ data->op_mode = MAX32664C_OP_MODE_IDLE;
+
+ return 0;
+}
+
+static int max32664c_disable_sensors(const struct device *dev)
+{
+ uint8_t rx;
+ uint8_t tx[4];
+ struct max32664c_data *data = dev->data;
+
+ if (max32664c_stop_algo(dev)) {
+ LOG_ERR("Failed to stop the algorithm!");
+ return -EINVAL;
+ }
+
+ /* Leave wake on motion first because we disable the accelerometer */
+ if (max32664c_exit_mode_wake_on_motion(dev)) {
+ LOG_ERR("Failed to exit wake on motion mode!");
+ return -EINVAL;
+ }
+
+ LOG_DBG("Disable the sensors...");
+
+ /* Disable the AFE */
+ tx[0] = 0x44;
+ tx[1] = 0x00;
+ tx[2] = 0x00;
+ tx[3] = 0x00;
+ if (max32664c_i2c_transmit(dev, tx, 4, &rx, 1, 250)) {
+ return -EINVAL;
+ }
+
+ /* Disable the accelerometer */
+ if (max32664c_acc_enable(dev, false)) {
+ return -EINVAL;
+ }
+
+ data->op_mode = MAX32664C_OP_MODE_IDLE;
+
+ return 0;
+}
+
+static int max32664c_sample_fetch(const struct device *dev, enum sensor_channel chan)
+{
+ struct max32664c_data *data = dev->data;
+
+ switch (data->op_mode) {
+ case MAX32664C_OP_MODE_STOP_ALGO:
+ case MAX32664C_OP_MODE_IDLE:
+ LOG_DBG("Device is idle, no data to fetch!");
+ return -EAGAIN;
+ case MAX32664C_OP_MODE_SCD:
+ k_msgq_get(&data->scd_report_queue, &data->scd, K_NO_WAIT);
+ return 0;
+#ifdef CONFIG_MAX32664C_USE_EXTENDED_REPORTS
+ case MAX32664C_OP_MODE_ALGO_AEC_EXT:
+ case MAX32664C_OP_MODE_ALGO_AGC_EXT:
+ k_msgq_get(&data->ext_report_queue, &data->ext, K_NO_WAIT);
+ return 0;
+#else
+ case MAX32664C_OP_MODE_ALGO_AEC:
+ case MAX32664C_OP_MODE_ALGO_AGC:
+ k_msgq_get(&data->report_queue, &data->report, K_NO_WAIT);
+ return 0;
+#endif /* CONFIG_MAX32664C_USE_EXTENDED_REPORTS */
+ /* Raw data are reported with normal and extended algorithms so we need to fetch them too */
+ case MAX32664C_OP_MODE_RAW:
+ k_msgq_get(&data->raw_report_queue, &data->raw, K_NO_WAIT);
+ return 0;
+ default:
+ return -ENOTSUP;
+ }
+}
+
+static int max32664c_channel_get(const struct device *dev, enum sensor_channel chan,
+ struct sensor_value *val)
+{
+ struct max32664c_data *data = dev->data;
+
+ switch ((int)chan) {
+ case SENSOR_CHAN_ACCEL_X: {
+ val->val1 = data->raw.acc.x;
+ break;
+ }
+ case SENSOR_CHAN_ACCEL_Y: {
+ val->val1 = data->raw.acc.y;
+ break;
+ }
+ case SENSOR_CHAN_ACCEL_Z: {
+ val->val1 = data->raw.acc.z;
+ break;
+ }
+ case SENSOR_CHAN_GREEN: {
+ val->val1 = data->raw.PPG1;
+ break;
+ }
+ case SENSOR_CHAN_IR: {
+ val->val1 = data->raw.PPG2;
+ break;
+ }
+ case SENSOR_CHAN_RED: {
+ val->val1 = data->raw.PPG3;
+ break;
+ }
+ case SENSOR_CHAN_MAX32664C_HEARTRATE: {
+#ifdef CONFIG_MAX32664C_USE_EXTENDED_REPORTS
+ val->val1 = data->ext.hr;
+ val->val2 = data->ext.hr_confidence;
+#else
+ val->val1 = data->report.hr;
+ val->val2 = data->report.hr_confidence;
+#endif
+ break;
+ }
+ case SENSOR_CHAN_MAX32664C_RESPIRATION_RATE: {
+#ifdef CONFIG_MAX32664C_USE_EXTENDED_REPORTS
+ val->val1 = data->ext.rr;
+ val->val2 = data->ext.rr_confidence;
+#else
+ val->val1 = data->report.rr;
+ val->val2 = data->report.rr_confidence;
+#endif
+ break;
+ }
+ case SENSOR_CHAN_MAX32664C_BLOOD_OXYGEN_SATURATION: {
+#ifdef CONFIG_MAX32664C_USE_EXTENDED_REPORTS
+ val->val1 = data->ext.spo2_meas.value;
+ val->val2 = data->ext.spo2_meas.confidence;
+#else
+ val->val1 = data->report.spo2_meas.value;
+ val->val2 = data->report.spo2_meas.confidence;
+#endif
+ break;
+ }
+ case SENSOR_CHAN_MAX32664C_SKIN_CONTACT: {
+ val->val1 = data->report.scd_state;
+ break;
+ }
+ default: {
+ LOG_ERR("Channel %u not supported!", chan);
+ return -ENOTSUP;
+ }
+ }
+
+ return 0;
+}
+
+static int max32664c_attr_set(const struct device *dev, enum sensor_channel chan,
+ enum sensor_attribute attr, const struct sensor_value *val)
+{
+ int err;
+ uint8_t tx[5];
+ uint8_t rx;
+ struct max32664c_data *data = dev->data;
+
+ err = 0;
+
+ switch ((int)attr) {
+ case SENSOR_ATTR_SAMPLING_FREQUENCY: {
+ break;
+ }
+ case SENSOR_ATTR_MAX32664C_HEIGHT: {
+ tx[0] = 0x50;
+ tx[1] = 0x07;
+ tx[2] = 0x06;
+ tx[3] = (val->val1 & 0xFF00) >> 8;
+ tx[4] = val->val1 & 0x00FF;
+ if (max32664c_i2c_transmit(dev, tx, 5, &rx, 1, MAX32664C_DEFAULT_CMD_DELAY)) {
+ LOG_ERR("Can not set height!");
+ return -EINVAL;
+ }
+
+ break;
+ }
+ case SENSOR_ATTR_MAX32664C_WEIGHT: {
+ tx[0] = 0x50;
+ tx[1] = 0x07;
+ tx[2] = 0x07;
+ tx[3] = (val->val1 & 0xFF00) >> 8;
+ tx[4] = val->val1 & 0x00FF;
+ if (max32664c_i2c_transmit(dev, tx, 5, &rx, 1, MAX32664C_DEFAULT_CMD_DELAY)) {
+ LOG_ERR("Can not set weight!");
+ return -EINVAL;
+ }
+
+ break;
+ }
+ case SENSOR_ATTR_MAX32664C_AGE: {
+ tx[0] = 0x50;
+ tx[1] = 0x07;
+ tx[2] = 0x08;
+ tx[3] = val->val1 & 0x00FF;
+ if (max32664c_i2c_transmit(dev, tx, 4, &rx, 1, MAX32664C_DEFAULT_CMD_DELAY)) {
+ LOG_ERR("Can not set age!");
+ return -EINVAL;
+ }
+
+ break;
+ }
+ case SENSOR_ATTR_MAX32664C_GENDER: {
+ tx[0] = 0x50;
+ tx[1] = 0x07;
+ tx[2] = 0x08;
+ tx[3] = val->val1 & 0x00FF;
+ if (max32664c_i2c_transmit(dev, tx, 4, &rx, 1, MAX32664C_DEFAULT_CMD_DELAY)) {
+ LOG_ERR("Can not set gender!");
+ return -EINVAL;
+ }
+
+ break;
+ }
+ case SENSOR_ATTR_SLOPE_DUR: {
+ data->motion_time = val->val1;
+ break;
+ }
+ case SENSOR_ATTR_SLOPE_TH: {
+ data->motion_threshold = val->val1;
+ break;
+ }
+ case SENSOR_ATTR_CONFIGURATION: {
+ switch ((int)chan) {
+ case SENSOR_CHAN_GREEN: {
+ data->led_current[0] = val->val1 & 0xFF;
+ break;
+ }
+ case SENSOR_CHAN_IR: {
+ data->led_current[1] = val->val1 & 0xFF;
+ break;
+ }
+ case SENSOR_CHAN_RED: {
+ data->led_current[2] = val->val1 & 0xFF;
+ break;
+ }
+ default: {
+ LOG_ERR("Channel %u not supported for setting attribute!", (int)chan);
+ return -ENOTSUP;
+ }
+ }
+ break;
+ }
+ case SENSOR_ATTR_MAX32664C_OP_MODE: {
+ switch (val->val1) {
+ case MAX32664C_OP_MODE_ALGO_AEC: {
+#ifndef CONFIG_MAX32664C_USE_EXTENDED_REPORTS
+ err = max32664c_set_mode_algo(dev, MAX32664C_OP_MODE_ALGO_AEC, val->val2,
+ false);
+#else
+ return -EINVAL;
+#endif /* CONFIG_MAX32664C_USE_EXTENDED_REPORTS */
+ break;
+ }
+ case MAX32664C_OP_MODE_ALGO_AEC_EXT: {
+#ifdef CONFIG_MAX32664C_USE_EXTENDED_REPORTS
+ err = max32664c_set_mode_algo(dev, MAX32664C_OP_MODE_ALGO_AEC, val->val2,
+ true);
+#else
+ return -EINVAL;
+#endif /* CONFIG_MAX32664C_USE_EXTENDED_REPORTS */
+ break;
+ }
+ case MAX32664C_OP_MODE_ALGO_AGC: {
+#ifndef CONFIG_MAX32664C_USE_EXTENDED_REPORTS
+ err = max32664c_set_mode_algo(dev, MAX32664C_OP_MODE_ALGO_AGC, val->val2,
+ false);
+#else
+ return -EINVAL;
+#endif /* CONFIG_MAX32664C_USE_EXTENDED_REPORTS */
+ break;
+ }
+ case MAX32664C_OP_MODE_ALGO_AGC_EXT: {
+#ifdef CONFIG_MAX32664C_USE_EXTENDED_REPORTS
+ err = max32664c_set_mode_algo(dev, MAX32664C_OP_MODE_ALGO_AGC, val->val2,
+ true);
+#else
+ return -EINVAL;
+#endif /* CONFIG_MAX32664C_USE_EXTENDED_REPORTS */
+ break;
+ }
+ case MAX32664C_OP_MODE_RAW: {
+ err = max32664c_set_mode_raw(dev);
+ break;
+ }
+ case MAX32664C_OP_MODE_SCD: {
+ err = max32664c_set_mode_scd(dev);
+ break;
+ }
+ case MAX32664C_OP_MODE_WAKE_ON_MOTION: {
+ err = max32664c_set_mode_wake_on_motion(dev);
+ break;
+ }
+ case MAX32664C_OP_MODE_EXIT_WAKE_ON_MOTION: {
+ err = max32664c_exit_mode_wake_on_motion(dev);
+ break;
+ }
+ case MAX32664C_OP_MODE_STOP_ALGO: {
+ err = max32664c_stop_algo(dev);
+ break;
+ }
+ case MAX32664C_OP_MODE_IDLE: {
+ err = max32664c_disable_sensors(dev);
+ break;
+ }
+ default: {
+ LOG_ERR("Unsupported sensor operation mode");
+ return -ENOTSUP;
+ }
+ }
+
+ break;
+ }
+ default: {
+ LOG_ERR("Unsupported sensor attribute!");
+ return -ENOTSUP;
+ }
+ }
+
+ return err;
+}
+
+static int max32664c_attr_get(const struct device *dev, enum sensor_channel chan,
+ enum sensor_attribute attr, struct sensor_value *val)
+{
+ struct max32664c_data *data = dev->data;
+
+ switch ((int)attr) {
+ case SENSOR_ATTR_MAX32664C_OP_MODE: {
+ val->val1 = data->op_mode;
+ val->val2 = 0;
+ break;
+ }
+ case SENSOR_ATTR_CONFIGURATION: {
+ switch ((int)chan) {
+ case SENSOR_CHAN_GREEN: {
+ val->val1 = data->led_current[0];
+ break;
+ }
+ case SENSOR_CHAN_IR: {
+ val->val1 = data->led_current[1];
+ break;
+ }
+ case SENSOR_CHAN_RED: {
+ val->val1 = data->led_current[2];
+ break;
+ }
+ default: {
+ LOG_ERR("Channel %u not supported for getting attribute!", (int)chan);
+ return -ENOTSUP;
+ }
+ }
+ break;
+ }
+ default: {
+ LOG_ERR("Unsupported sensor attribute!");
+ return -ENOTSUP;
+ }
+ }
+
+ return 0;
+}
+
+static DEVICE_API(sensor, max32664c_driver_api) = {
+ .attr_set = max32664c_attr_set,
+ .attr_get = max32664c_attr_get,
+ .sample_fetch = max32664c_sample_fetch,
+ .channel_get = max32664c_channel_get,
+};
+
+static int max32664c_init(const struct device *dev)
+{
+ uint8_t tx[2];
+ uint8_t rx[4];
+ const struct max32664c_config *config = dev->config;
+ struct max32664c_data *data = dev->data;
+
+ if (!i2c_is_ready_dt(&config->i2c)) {
+ LOG_ERR("I2C not ready");
+ return -ENODEV;
+ }
+
+ gpio_pin_configure_dt(&config->reset_gpio, GPIO_OUTPUT);
+ gpio_pin_configure_dt(&config->mfio_gpio, GPIO_OUTPUT);
+
+ /* Put the hub into application mode */
+ LOG_DBG("Set app mode");
+ gpio_pin_set_dt(&config->reset_gpio, false);
+ k_msleep(20);
+
+ gpio_pin_set_dt(&config->mfio_gpio, true);
+ k_msleep(20);
+
+ /* Wait for 50 ms (switch into app mode) + 1500 ms (initialization) */
+ /* (see page 17 of the User Guide) */
+ gpio_pin_set_dt(&config->reset_gpio, true);
+ k_msleep(1600);
+
+ /* Read the device mode */
+ tx[0] = 0x02;
+ tx[1] = 0x00;
+ if (max32664c_i2c_transmit(dev, tx, 2, rx, 2, MAX32664C_DEFAULT_CMD_DELAY)) {
+ return -EINVAL;
+ }
+
+ data->op_mode = rx[1];
+ LOG_DBG("Mode: %x ", data->op_mode);
+ if (data->op_mode != 0) {
+ return -EINVAL;
+ }
+
+ /* Read the firmware version */
+ tx[0] = 0xFF;
+ tx[1] = 0x03;
+ if (max32664c_i2c_transmit(dev, tx, 2, rx, 4, MAX32664C_DEFAULT_CMD_DELAY)) {
+ return -EINVAL;
+ }
+
+ memcpy(data->hub_ver, &rx[1], 3);
+
+ LOG_DBG("Version: %d.%d.%d", data->hub_ver[0], data->hub_ver[1], data->hub_ver[2]);
+
+ if (max32664c_check_sensors(dev)) {
+ return -EINVAL;
+ }
+
+ if (max32664c_init_hub(dev)) {
+ return -EINVAL;
+ }
+
+#ifdef CONFIG_MAX32664C_USE_STATIC_MEMORY
+ k_msgq_init(&data->raw_report_queue, data->raw_report_queue_buffer,
+ sizeof(struct max32664c_raw_report_t),
+ sizeof(data->raw_report_queue_buffer) / sizeof(struct max32664c_raw_report_t));
+
+ k_msgq_init(&data->scd_report_queue, data->scd_report_queue_buffer,
+ sizeof(struct max32664c_scd_report_t),
+ sizeof(data->scd_report_queue_buffer) / sizeof(struct max32664c_scd_report_t));
+
+#ifdef CONFIG_MAX32664C_USE_EXTENDED_REPORTS
+ k_msgq_init(&data->ext_report_queue, data->ext_report_queue_buffer,
+ sizeof(struct max32664c_ext_report_t),
+ sizeof(data->ext_report_queue_buffer) / sizeof(struct max32664c_ext_report_t));
+#else
+ k_msgq_init(&data->report_queue, data->report_queue_buffer,
+ sizeof(struct max32664c_report_t),
+ sizeof(data->report_queue_buffer) / sizeof(struct max32664c_report_t));
+#endif /* CONFIG_MAX32664C_USE_EXTENDED_REPORTS */
+#endif /* CONFIG_MAX32664C_USE_STATIC_MEMORY */
+
+ return 0;
+}
+
+#ifdef CONFIG_PM_DEVICE
+static int max32664c_pm_action(const struct device *dev, enum pm_device_action action)
+{
+ switch (action) {
+ case PM_DEVICE_ACTION_RESUME: {
+ break;
+ }
+ case PM_DEVICE_ACTION_SUSPEND: {
+ const struct max32664c_config *config = dev->config;
+
+ /* Pulling MFIO high will cause the hub to enter sleep mode */
+ gpio_pin_set_dt(&config->mfio_gpio, true);
+ k_msleep(20);
+ break;
+ }
+ case PM_DEVICE_ACTION_TURN_OFF: {
+ uint8_t rx;
+ uint8_t tx[3];
+
+ /* Send a shut down command */
+ /* NOTE: Toggling RSTN is needed to wake the device */
+ tx[0] = 0x01;
+ tx[1] = 0x00;
+ tx[2] = 0x01;
+ if (max32664c_i2c_transmit(dev, tx, 3, &rx, 1, MAX32664C_DEFAULT_CMD_DELAY)) {
+ return -EINVAL;
+ }
+ break;
+ }
+ case PM_DEVICE_ACTION_TURN_ON: {
+ /* Toggling RSTN is needed to turn the device on */
+ max32664c_init(dev);
+ break;
+ }
+ default: {
+ return -ENOTSUP;
+ }
+ }
+
+ return 0;
+}
+#endif /* CONFIG_PM_DEVICE */
+
+#define MAX32664C_INIT(inst) \
+ static struct max32664c_data max32664c_data_##inst; \
+ \
+ static const struct max32664c_config max32664c_config_##inst = { \
+ .i2c = I2C_DT_SPEC_INST_GET(inst), \
+ .reset_gpio = GPIO_DT_SPEC_INST_GET(inst, reset_gpios), \
+ .mfio_gpio = GPIO_DT_SPEC_INST_GET(inst, mfio_gpios), \
+ .spo2_calib = DT_INST_PROP(inst, spo2_calib), \
+ .hr_config = DT_INST_PROP(inst, hr_config), \
+ .spo2_config = DT_INST_PROP(inst, spo2_config), \
+ .use_max86141 = DT_INST_PROP(inst, use_max86141), \
+ .use_max86161 = DT_INST_PROP(inst, use_max86161), \
+ .motion_time = DT_INST_PROP(inst, motion_time), \
+ .motion_threshold = DT_INST_PROP(inst, motion_threshold), \
+ .min_integration_time_idx = DT_INST_ENUM_IDX(inst, min_integration_time), \
+ .min_sampling_rate_idx = DT_INST_ENUM_IDX(inst, min_sampling_rate), \
+ .max_integration_time_idx = DT_INST_ENUM_IDX(inst, max_integration_time), \
+ .max_sampling_rate_idx = DT_INST_ENUM_IDX(inst, max_sampling_rate), \
+ .report_period = DT_INST_PROP(inst, report_period), \
+ .led_current = DT_INST_PROP(inst, led_current), \
+ }; \
+ \
+ PM_DEVICE_DT_INST_DEFINE(inst, max32664c_pm_action); \
+ \
+ SENSOR_DEVICE_DT_INST_DEFINE(inst, max32664c_init, PM_DEVICE_DT_INST_GET(inst), \
+ &max32664c_data_##inst, &max32664c_config_##inst, \
+ POST_KERNEL, CONFIG_SENSOR_INIT_PRIORITY, \
+ &max32664c_driver_api)
+
+DT_INST_FOREACH_STATUS_OKAY(MAX32664C_INIT)
diff --git a/drivers/sensor/adi/max32664c/max32664c.h b/drivers/sensor/adi/max32664c/max32664c.h
new file mode 100644
index 0000000..facca74
--- /dev/null
+++ b/drivers/sensor/adi/max32664c/max32664c.h
@@ -0,0 +1,294 @@
+/*
+ * Copyright (c) 2025, Daniel Kampert
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#include <zephyr/pm/device.h>
+#include <zephyr/drivers/sensor.h>
+#include <zephyr/drivers/i2c.h>
+#include <zephyr/drivers/gpio.h>
+#include <zephyr/logging/log.h>
+
+#include <zephyr/drivers/sensor/max32664c.h>
+
+#define MAX32664C_BIT_STATUS_NO_ERR 1
+#define MAX32664C_BIT_STATUS_DATA_RDY 3
+#define MAX32664C_BIT_STATUS_OUT_OVFL 4
+#define MAX32664C_BIT_STATUS_IN_OVFL 5
+#define MAX32664C_BIT_STATUS_BUSY 6
+
+#define MAX32664C_DEFAULT_CMD_DELAY 10
+
+/** @brief Output formats of the sensor hub.
+ */
+enum max32664c_output_format {
+ MAX32664C_OUT_PAUSE,
+ MAX32664C_OUT_SENSOR_ONLY,
+ MAX32664C_OUT_ALGORITHM_ONLY,
+ MAX32664C_OUT_ALGO_AND_SENSOR,
+};
+
+/** @brief Skin contact detection states.
+ * @note The SCD states are only available when the SCD only mode is enabled.
+ */
+enum max32664c_scd_states {
+ MAX32664C_SCD_STATE_UNKNOWN,
+ MAX32664C_SCD_STATE_OFF_SKIN,
+ MAX32664C_SCD_STATE_ON_OBJECT,
+ MAX32664C_SCD_STATE_ON_SKIN,
+};
+
+/** @brief LED current structure.
+ */
+struct max32664c_led_current_t {
+ uint8_t adj_flag;
+ uint16_t adj_val;
+} __packed;
+
+/** @brief SpO2 measurement result structure.
+ */
+struct max32664c_spo2_meas_t {
+ uint8_t confidence;
+ uint16_t value;
+ uint8_t complete;
+ uint8_t low_signal_quality;
+ uint8_t motion;
+ uint8_t low_pi;
+ uint8_t unreliable_r;
+ uint8_t state;
+} __packed;
+
+/** @brief Extended SpO2 measurement result structure.
+ */
+struct max32664c_ext_spo2_meas_t {
+ uint8_t confidence;
+ uint16_t value;
+ uint8_t valid_percent;
+ uint8_t low_signal_flag;
+ uint8_t motion_flag;
+ uint8_t low_pi_flag;
+ uint8_t unreliable_r_flag;
+ uint8_t state;
+} __packed;
+
+/** @brief Raw data structure, reported by the sensor hub.
+ */
+struct max32664c_raw_report_t {
+ uint32_t PPG1: 24;
+ uint32_t PPG2: 24;
+ uint32_t PPG3: 24;
+ uint32_t PPG4: 24;
+ uint32_t PPG5: 24;
+ uint32_t PPG6: 24;
+ struct max32664c_acc_data_t acc;
+} __packed;
+
+/** @brief SCD only data structure, reported by the sensor hub.
+ */
+struct max32664c_scd_report_t {
+ uint8_t scd_classifier;
+} __packed;
+
+/** @brief Algorithm data structure, reported by the sensor hub.
+ */
+struct max32664c_report_t {
+ uint8_t op_mode;
+ uint16_t hr;
+ uint8_t hr_confidence;
+ uint16_t rr;
+ uint8_t rr_confidence;
+ uint8_t activity_class;
+ uint16_t r;
+ struct max32664c_spo2_meas_t spo2_meas;
+ uint8_t scd_state;
+} __packed;
+
+/** @brief Extended algorithm data structure, reported by the sensor hub.
+ */
+struct max32664c_ext_report_t {
+ uint8_t op_mode;
+ uint16_t hr;
+ uint8_t hr_confidence;
+ uint16_t rr;
+ uint8_t rr_confidence;
+ uint8_t activity_class;
+
+ uint32_t total_walk_steps;
+ uint32_t total_run_steps;
+ uint32_t total_energy_kcal;
+ uint32_t total_amr_kcal;
+
+ struct max32664c_led_current_t led_current_adj1;
+ struct max32664c_led_current_t led_current_adj2;
+ struct max32664c_led_current_t led_current_adj3;
+
+ uint8_t integration_time_adj_flag;
+ uint8_t requested_integration_time;
+
+ uint8_t sampling_rate_adj_flag;
+ uint8_t requested_sampling_rate;
+ uint8_t requested_sampling_average;
+
+ uint8_t hrm_afe_ctrl_state;
+ uint8_t is_high_motion_for_hrm;
+
+ uint8_t scd_state;
+
+ uint16_t r_value;
+ struct max32664c_ext_spo2_meas_t spo2_meas;
+
+ uint8_t ibi_offset;
+ uint8_t unreliable_orientation_flag;
+
+ uint8_t reserved[2];
+} __packed;
+
+/** @brief Device configuration structure.
+ */
+struct max32664c_config {
+ struct i2c_dt_spec i2c;
+ struct gpio_dt_spec reset_gpio;
+
+#ifdef CONFIG_MAX32664C_USE_INTERRUPT
+ const struct device *dev;
+ struct gpio_callback gpio_cb;
+ struct k_work interrupt_work;
+#endif /* CONFIG_MAX32664C_USE_INTERRUPT */
+
+ struct gpio_dt_spec mfio_gpio;
+
+ int32_t spo2_calib[3];
+ uint16_t motion_time;
+ uint16_t motion_threshold;
+
+ uint8_t hr_config[2];
+ uint8_t spo2_config[2];
+ uint8_t led_current[3]; /**< Initial LED current in mA */
+ uint8_t min_integration_time_idx;
+ uint8_t min_sampling_rate_idx;
+ uint8_t max_integration_time_idx;
+ uint8_t max_sampling_rate_idx;
+ uint8_t report_period; /*< Samples report period */
+
+ bool use_max86141;
+ bool use_max86161;
+};
+
+/** @brief Device runtime data structure.
+ */
+struct max32664c_data {
+ struct max32664c_raw_report_t raw;
+ struct max32664c_scd_report_t scd;
+ struct max32664c_report_t report;
+ struct max32664c_ext_report_t ext;
+
+ enum max32664c_device_mode op_mode; /**< Current device mode */
+
+ uint8_t motion_time; /**< Motion time in milliseconds */
+ uint8_t motion_threshold; /**< Motion threshold in milli-g */
+ uint8_t led_current[3]; /**< LED current in mA */
+ uint8_t min_integration_time_idx;
+ uint8_t min_sampling_rate_idx;
+ uint8_t max_integration_time_idx;
+ uint8_t max_sampling_rate_idx;
+ uint8_t report_period; /*< Samples report period */
+ uint8_t afe_id;
+ uint8_t accel_id;
+ uint8_t hub_ver[3];
+
+ /* Internal */
+ struct k_thread thread;
+ k_tid_t thread_id;
+ bool is_thread_running;
+
+#ifdef CONFIG_MAX32664C_USE_STATIC_MEMORY
+ /** @brief This buffer is used to read all available messages from the sensor hub plus the
+ * status byte. The buffer size is defined by the CONFIG_MAX32664C_SAMPLE_BUFFER_SIZE
+ * Kconfig and the largest possible message. The buffer must contain enough space to store
+ * all available messages at every time because it is not possible to read a single message
+ * from the sensor hub.
+ */
+#ifdef CONFIG_MAX32664C_USE_EXTENDED_REPORTS
+ uint8_t max32664_i2c_buffer[(CONFIG_MAX32664C_SAMPLE_BUFFER_SIZE *
+ (sizeof(struct max32664c_raw_report_t) +
+ sizeof(struct max32664c_ext_report_t))) +
+ 1];
+#else
+ uint8_t max32664_i2c_buffer[(CONFIG_MAX32664C_SAMPLE_BUFFER_SIZE *
+ (sizeof(struct max32664c_raw_report_t) +
+ sizeof(struct max32664c_report_t))) +
+ 1];
+#endif /* CONFIG_MAX32664C_USE_EXTENDED_REPORTS */
+#else
+ uint8_t *max32664_i2c_buffer;
+#endif /* CONFIG_MAX32664C_USE_STATIC_MEMORY */
+
+ K_KERNEL_STACK_MEMBER(thread_stack, CONFIG_MAX32664C_THREAD_STACK_SIZE);
+
+ struct k_msgq raw_report_queue;
+ struct k_msgq scd_report_queue;
+
+#ifdef CONFIG_MAX32664C_USE_EXTENDED_REPORTS
+ struct k_msgq ext_report_queue;
+#else
+ struct k_msgq report_queue;
+#endif /* CONFIG_MAX32664C_USE_EXTENDED_REPORTS */
+
+#ifdef CONFIG_MAX32664C_USE_STATIC_MEMORY
+ uint8_t raw_report_queue_buffer[CONFIG_MAX32664C_QUEUE_SIZE *
+ sizeof(struct max32664c_raw_report_t)];
+ uint8_t scd_report_queue_buffer[CONFIG_MAX32664C_QUEUE_SIZE *
+ sizeof(struct max32664c_scd_report_t)];
+
+#ifdef CONFIG_MAX32664C_USE_EXTENDED_REPORTS
+ uint8_t ext_report_queue_buffer[CONFIG_MAX32664C_QUEUE_SIZE *
+ (sizeof(struct max32664c_raw_report_t) +
+ sizeof(struct max32664c_ext_report_t))];
+#else
+ uint8_t report_queue_buffer[CONFIG_MAX32664C_QUEUE_SIZE *
+ (sizeof(struct max32664c_raw_report_t) +
+ sizeof(struct max32664c_report_t))];
+#endif /* CONFIG_MAX32664C_USE_EXTENDED_REPORTS */
+#endif /* CONFIG_MAX32664C_USE_STATIC_MEMORY*/
+};
+
+/** @brief Enable / Disable the accelerometer.
+ * NOTE: This code is untested and may not work as expected.
+ * @param dev Pointer to device
+ * @param enable Enable / Disable
+ * @return 0 when successful
+ */
+int max32664c_acc_enable(const struct device *dev, bool enable);
+
+/** @brief Background worker for reading the sensor hub.
+ * @param dev Pointer to device
+ */
+void max32664c_worker(const struct device *dev);
+
+/** @brief Read / write data from / to the sensor hub.
+ * @param dev Pointer to device
+ * @param tx_buf Pointer to transmit buffer
+ * @param tx_len Length of transmit buffer
+ * @param rx_buf Pointer to receive buffer
+ * NOTE: The buffer must be large enough to store the response and the status byte!
+ * @param rx_len Length of the receive buffer
+ * @param delay Command delay (milliseconds)
+ * @return 0 when successful
+ */
+int max32664c_i2c_transmit(const struct device *dev, uint8_t *tx_buf, uint8_t tx_len,
+ uint8_t *rx_buf, uint32_t rx_len, uint16_t delay);
+
+/** @brief Run a basic initialization on the sensor hub.
+ * @param dev Pointer to device
+ * @return 0 when successful
+ */
+int max32664c_init_hub(const struct device *dev);
+
+#if CONFIG_MAX32664C_USE_INTERRUPT
+/** @brief Initialize the interrupt support for the sensor hub.
+ * @param dev Pointer to device
+ * @return 0 when successful
+ */
+int max32664c_init_interrupt(const struct device *dev);
+#endif /* CONFIG_MAX32664C_USE_INTERRUPT */
diff --git a/drivers/sensor/adi/max32664c/max32664c_acc.c b/drivers/sensor/adi/max32664c/max32664c_acc.c
new file mode 100644
index 0000000..1b8dfc1
--- /dev/null
+++ b/drivers/sensor/adi/max32664c/max32664c_acc.c
@@ -0,0 +1,58 @@
+/*
+ * External accelerometer driver for the MAX32664C biometric sensor hub.
+ *
+ * Copyright (c) 2025, Daniel Kampert
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#include "max32664c.h"
+
+LOG_MODULE_REGISTER(maxim_max32664c_acc, CONFIG_SENSOR_LOG_LEVEL);
+
+int max32664c_acc_enable(const struct device *dev, bool enable)
+{
+ uint8_t tx[4];
+ uint8_t rx;
+
+ tx[0] = 0x44;
+ tx[1] = 0x04;
+ tx[2] = enable;
+
+#if CONFIG_MAX32664C_USE_EXTERNAL_ACC
+ tx[3] = 1;
+#else
+ tx[3] = 0;
+#endif /* CONFIG_MAX32664C_USE_EXTERNAL_ACC */
+
+ if (max32664c_i2c_transmit(dev, tx, 4, &rx, 1, 20)) {
+ return -EINVAL;
+ }
+
+ return 0;
+}
+
+#ifdef CONFIG_MAX32664C_USE_EXTERNAL_ACC
+int max32664c_acc_fill_fifo(const struct device *dev, struct max32664c_acc_data_t *data,
+ uint8_t length)
+{
+ uint8_t tx[2 + 16 * sizeof(struct max32664c_acc_data_t)];
+ uint8_t rx;
+
+ if (length > 16) {
+ LOG_ERR("Length exceeds maximum of 16 samples!");
+ return -EINVAL;
+ }
+
+ tx[0] = 0x14;
+ tx[1] = 0x00;
+ memcpy(&tx[2], data, length * sizeof(struct max32664c_acc_data_t));
+
+ if (max32664c_i2c_transmit(dev, tx, 2 + (length * sizeof(struct max32664c_acc_data_t)), &rx,
+ 1, 20)) {
+ return -EINVAL;
+ }
+
+ return 0;
+}
+#endif /* CONFIG_MAX32664C_USE_EXTERNAL_ACC */
diff --git a/drivers/sensor/adi/max32664c/max32664c_bl.c b/drivers/sensor/adi/max32664c/max32664c_bl.c
new file mode 100644
index 0000000..147c9ae
--- /dev/null
+++ b/drivers/sensor/adi/max32664c/max32664c_bl.c
@@ -0,0 +1,383 @@
+/*
+ * I2C firmware loader for the MAX32664C biometric sensor hub.
+ *
+ * Copyright (c) 2025, Daniel Kampert
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#include <zephyr/kernel.h>
+#include <zephyr/drivers/i2c.h>
+#include <zephyr/drivers/gpio.h>
+#include <zephyr/sys/byteorder.h>
+#include <zephyr/logging/log.h>
+
+#include "max32664c.h"
+
+#define MAX32664C_FW_PAGE_SIZE 8192
+#define MAX32664C_FW_UPDATE_CRC_SIZE 16
+#define MAX32664C_FW_UPDATE_WRITE_SIZE (MAX32664C_FW_PAGE_SIZE + MAX32664C_FW_UPDATE_CRC_SIZE)
+#define MAX32664C_DEFAULT_CMD_DELAY_MS 10
+#define MAX32664C_PAGE_WRITE_DELAY_MS 680
+
+static uint8_t max32664c_fw_init_vector[11];
+static uint8_t max32664c_fw_auth_vector[16];
+
+LOG_MODULE_REGISTER(max32664_loader, CONFIG_SENSOR_LOG_LEVEL);
+
+/** @brief Read / write bootloader data from / to the sensor hub.
+ * @param dev Pointer to device
+ * @param tx_buf Pointer to transmit buffer
+ * @param tx_len Length of transmit buffer
+ * @param rx_buf Pointer to receive buffer
+ * NOTE: The buffer must be large enough to store the response and the status byte!
+ * @param rx_len Length of the receive buffer
+ * @return 0 when successful
+ */
+static int max32664c_bl_i2c_transmit(const struct device *dev, uint8_t *tx_buf, uint8_t tx_len,
+ uint8_t *rx_buf, uint8_t rx_len)
+{
+ int err;
+ const struct max32664c_config *config = dev->config;
+
+ err = i2c_write_dt(&config->i2c, tx_buf, tx_len);
+ if (err) {
+ LOG_ERR("I2C transmission error %d!", err);
+ return err;
+ }
+ k_msleep(MAX32664C_DEFAULT_CMD_DELAY_MS);
+ err = i2c_read_dt(&config->i2c, rx_buf, rx_len);
+ if (err) {
+ LOG_ERR("I2C transmission error %d!", err);
+ return err;
+ }
+ k_msleep(MAX32664C_DEFAULT_CMD_DELAY_MS);
+
+ /* Check the status byte for a valid transaction */
+ LOG_DBG("Status: %u", rx_buf[0]);
+ if (rx_buf[0] != 0) {
+ return -EINVAL;
+ }
+
+ return 0;
+}
+
+/** @brief Read application data from the sensor hub.
+ * @param dev Pointer to device
+ * @param family Family byte
+ * @param index Index byte
+ * @param rx_buf Pointer to receive buffer
+ * NOTE: The buffer must be large enough to store the response and the status byte!
+ * @param rx_len Length of receive buffer
+ * @return 0 when successful
+ */
+static int max32664c_app_i2c_read(const struct device *dev, uint8_t family, uint8_t index,
+ uint8_t *rx_buf, uint8_t rx_len)
+{
+ uint8_t tx_buf[] = {family, index};
+ const struct max32664c_config *config = dev->config;
+
+ /* Wake the sensor hub before starting an I2C read (see page 17 of the user Guide) */
+ gpio_pin_set_dt(&config->mfio_gpio, false);
+ k_usleep(300);
+
+ i2c_write_dt(&config->i2c, tx_buf, sizeof(tx_buf));
+ k_msleep(MAX32664C_DEFAULT_CMD_DELAY_MS);
+ i2c_read_dt(&config->i2c, rx_buf, rx_len);
+ k_msleep(MAX32664C_DEFAULT_CMD_DELAY_MS);
+
+ gpio_pin_set_dt(&config->mfio_gpio, true);
+
+ /* Check the status byte for a valid transaction */
+ if (rx_buf[0] != 0) {
+ return -EINVAL;
+ }
+
+ return 0;
+}
+
+/** @brief Write a page of data into the sensor hub.
+ * @param dev Pointer to device
+ * @param data Pointer to firmware data
+ * @param offset Start address in the firmware data
+ * @return 0 when successful
+ */
+static int max32664c_bl_write_page(const struct device *dev, const uint8_t *data, uint32_t offset)
+{
+ int err;
+ uint8_t rx_buf;
+ uint8_t *tx_buf;
+ const struct max32664c_config *config = dev->config;
+
+ /* Alloc memory for one page plus two command bytes */
+ tx_buf = (uint8_t *)k_malloc(MAX32664C_FW_UPDATE_WRITE_SIZE + 2);
+ if (tx_buf == NULL) {
+ return -ENOMEM;
+ }
+
+ /* Copy the data for one page into the buffer but leave space for the two command bytes */
+ memcpy(&tx_buf[2], &data[offset], MAX32664C_FW_UPDATE_WRITE_SIZE);
+
+ /* Set the two command bytes */
+ tx_buf[0] = 0x80;
+ tx_buf[1] = 0x04;
+
+ if (i2c_write_dt(&config->i2c, tx_buf, MAX32664C_FW_UPDATE_WRITE_SIZE + 2)) {
+ err = -EINVAL;
+ goto max32664c_bl_write_page_exit;
+ };
+ k_msleep(MAX32664C_PAGE_WRITE_DELAY_MS);
+ err = i2c_read_dt(&config->i2c, &rx_buf, 1);
+ if (err) {
+ LOG_ERR("I2C read error %d!", err);
+ err = -EINVAL;
+ goto max32664c_bl_write_page_exit;
+ };
+ k_msleep(MAX32664C_DEFAULT_CMD_DELAY_MS);
+
+ err = rx_buf;
+
+ LOG_DBG("Write page status: %u", err);
+
+max32664c_bl_write_page_exit:
+ k_free(tx_buf);
+ return err;
+}
+
+/** @brief Erase the application from the sensor hub.
+ * @param dev Pointer to device
+ * @return 0 when successful
+ */
+static int max32664c_bl_erase_app(const struct device *dev)
+{
+ uint8_t tx_buf[2] = {0x80, 0x03};
+ uint8_t rx_buf;
+ const struct max32664c_config *config = dev->config;
+
+ if (i2c_write_dt(&config->i2c, tx_buf, sizeof(tx_buf))) {
+ return -EINVAL;
+ };
+
+ k_msleep(1500);
+
+ if (i2c_read_dt(&config->i2c, &rx_buf, sizeof(rx_buf))) {
+ return -EINVAL;
+ };
+
+ k_msleep(MAX32664C_DEFAULT_CMD_DELAY_MS);
+
+ /* Check the status byte for a valid transaction */
+ if (rx_buf != 0) {
+ return -EINVAL;
+ }
+
+ return 0;
+}
+
+/** @brief Load the firmware into the hub.
+ * NOTE: See User Guide, Table 9 for the required steps.
+ * @param dev Pointer to device
+ * @param firmware Pointer to firmware data
+ * @param size Firmware size
+ * @return 0 when successful
+ */
+static int max32664c_bl_load_fw(const struct device *dev, const uint8_t *firmware, uint32_t size)
+{
+ uint8_t rx_buf;
+ uint8_t tx_buf[18] = {0};
+ uint32_t page_offset;
+
+ /* Get the number of pages from the firmware file (see User Guide page 53) */
+ uint8_t num_pages = firmware[0x44];
+
+ LOG_INF("Loading firmware...");
+ LOG_INF("\tSize: %u", size);
+ LOG_INF("\tPages: %u", num_pages);
+
+ /* Set the number of pages */
+ tx_buf[0] = 0x80;
+ tx_buf[1] = 0x02;
+ tx_buf[2] = 0x00;
+ tx_buf[3] = num_pages;
+ if (max32664c_bl_i2c_transmit(dev, tx_buf, 4, &rx_buf, 1)) {
+ return -EINVAL;
+ }
+
+ if (rx_buf != 0) {
+ LOG_ERR("Failed to set number of pages: %d", rx_buf);
+ return -EINVAL;
+ }
+
+ /* Get the initialization and authentication vectors from the firmware */
+ /* (see User Guide page 53) */
+ memcpy(max32664c_fw_init_vector, &firmware[0x28], sizeof(max32664c_fw_init_vector));
+ memcpy(max32664c_fw_auth_vector, &firmware[0x34], sizeof(max32664c_fw_auth_vector));
+
+ /* Write the initialization vector */
+ LOG_INF("\tWriting init vector...");
+ tx_buf[0] = 0x80;
+ tx_buf[1] = 0x00;
+ memcpy(&tx_buf[2], max32664c_fw_init_vector, sizeof(max32664c_fw_init_vector));
+ if (max32664c_bl_i2c_transmit(dev, tx_buf, 13, &rx_buf, 1)) {
+ return -EINVAL;
+ }
+ if (rx_buf != 0) {
+ LOG_ERR("Failed to set init vector: %d", rx_buf);
+ return -EINVAL;
+ }
+
+ /* Write the authentication vector */
+ LOG_INF("\tWriting auth vector...");
+ tx_buf[0] = 0x80;
+ tx_buf[1] = 0x01;
+ memcpy(&tx_buf[2], max32664c_fw_auth_vector, sizeof(max32664c_fw_auth_vector));
+ if (max32664c_bl_i2c_transmit(dev, tx_buf, 18, &rx_buf, 1)) {
+ return -EINVAL;
+ }
+ if (rx_buf != 0) {
+ LOG_ERR("Failed to set auth vector: %d", rx_buf);
+ return -EINVAL;
+ }
+
+ /* Remove the old app from the hub */
+ LOG_INF("\tRemove old app...");
+ if (max32664c_bl_erase_app(dev)) {
+ return -EINVAL;
+ }
+
+ /* Write the new firmware */
+ LOG_INF("\tWriting new firmware...");
+ page_offset = 0x4C;
+ for (uint8_t i = 0; i < num_pages; i++) {
+ uint8_t status;
+
+ LOG_INF("\t\tPage: %d of %d", (i + 1), num_pages);
+ LOG_INF("\t\tOffset: 0x%x", page_offset);
+ status = max32664c_bl_write_page(dev, firmware, page_offset);
+ LOG_INF("\t\tStatus: %u", status);
+ if (status != 0) {
+ return -EINVAL;
+ }
+
+ page_offset += MAX32664C_FW_UPDATE_WRITE_SIZE;
+ }
+
+ LOG_INF("\tSuccessful!");
+
+ return max32664c_bl_leave(dev);
+}
+
+int max32664c_bl_enter(const struct device *dev, const uint8_t *firmware, uint32_t size)
+{
+ uint8_t rx_buf[4] = {0};
+ uint8_t tx_buf[3];
+ const struct max32664c_config *config = dev->config;
+
+ gpio_pin_configure_dt(&config->reset_gpio, GPIO_OUTPUT);
+ gpio_pin_configure_dt(&config->mfio_gpio, GPIO_OUTPUT);
+
+ /* Put the processor into bootloader mode */
+ LOG_INF("Entering bootloader mode");
+ gpio_pin_set_dt(&config->reset_gpio, false);
+ k_msleep(20);
+
+ gpio_pin_set_dt(&config->mfio_gpio, false);
+ k_msleep(20);
+
+ gpio_pin_set_dt(&config->reset_gpio, true);
+ k_msleep(200);
+
+ /* Set bootloader mode */
+ tx_buf[0] = 0x01;
+ tx_buf[1] = 0x00;
+ tx_buf[2] = 0x08;
+ if (max32664c_bl_i2c_transmit(dev, tx_buf, 3, rx_buf, 1)) {
+ return -EINVAL;
+ }
+
+ /* Read the device mode */
+ tx_buf[0] = 0x02;
+ tx_buf[1] = 0x00;
+ if (max32664c_bl_i2c_transmit(dev, tx_buf, 2, rx_buf, 2)) {
+ return -EINVAL;
+ }
+
+ LOG_DBG("Mode: %x ", rx_buf[1]);
+ if (rx_buf[1] != 8) {
+ LOG_ERR("Device not in bootloader mode!");
+ return -EINVAL;
+ }
+
+ /* Read the bootloader information */
+ tx_buf[0] = 0x81;
+ tx_buf[1] = 0x00;
+ if (max32664c_bl_i2c_transmit(dev, tx_buf, 2, rx_buf, 4)) {
+ return -EINVAL;
+ }
+
+ LOG_INF("Version: %d.%d.%d", rx_buf[1], rx_buf[2], rx_buf[3]);
+
+ /* Read the bootloader page size */
+ tx_buf[0] = 0x81;
+ tx_buf[1] = 0x01;
+ if (max32664c_bl_i2c_transmit(dev, tx_buf, 2, rx_buf, 3)) {
+ return -EINVAL;
+ }
+
+ LOG_INF("Page size: %u", (uint16_t)(rx_buf[1] << 8) | rx_buf[2]);
+
+ return max32664c_bl_load_fw(dev, firmware, size);
+}
+
+int max32664c_bl_leave(const struct device *dev)
+{
+ uint8_t hub_ver[3];
+ uint8_t rx_buf[4] = {0};
+ const struct max32664c_config *config = dev->config;
+
+ gpio_pin_configure_dt(&config->reset_gpio, GPIO_OUTPUT);
+ gpio_pin_configure_dt(&config->mfio_gpio, GPIO_OUTPUT);
+
+ LOG_INF("Entering app mode");
+ gpio_pin_set_dt(&config->reset_gpio, true);
+ gpio_pin_set_dt(&config->mfio_gpio, false);
+ k_msleep(2000);
+
+ gpio_pin_set_dt(&config->reset_gpio, false);
+ k_msleep(5);
+
+ gpio_pin_set_dt(&config->mfio_gpio, true);
+ k_msleep(15);
+
+ gpio_pin_set_dt(&config->reset_gpio, true);
+ k_msleep(1700);
+
+ /* Read the device mode */
+ if (max32664c_app_i2c_read(dev, 0x02, 0x00, rx_buf, 2)) {
+ return -EINVAL;
+ }
+
+ LOG_DBG("Mode: %x ", rx_buf[1]);
+ if (rx_buf[1] != 0) {
+ LOG_ERR("Device not in application mode!");
+ return -EINVAL;
+ }
+
+ /* Read the MCU type */
+ if (max32664c_app_i2c_read(dev, 0xFF, 0x00, rx_buf, 2)) {
+ return -EINVAL;
+ }
+
+ LOG_INF("MCU type: %u", rx_buf[1]);
+
+ /* Read the firmware version */
+ if (max32664c_app_i2c_read(dev, 0xFF, 0x03, rx_buf, 4)) {
+ return -EINVAL;
+ }
+
+ memcpy(hub_ver, &rx_buf[1], 3);
+
+ LOG_INF("Version: %d.%d.%d", hub_ver[0], hub_ver[1], hub_ver[2]);
+
+ return 0;
+}
diff --git a/drivers/sensor/adi/max32664c/max32664c_init.c b/drivers/sensor/adi/max32664c/max32664c_init.c
new file mode 100644
index 0000000..e65d2ec
--- /dev/null
+++ b/drivers/sensor/adi/max32664c/max32664c_init.c
@@ -0,0 +1,246 @@
+/*
+ * Initialization code for the MAX32664C biometric sensor hub.
+ *
+ * Copyright (c) 2025, Daniel Kampert
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#include "max32664c.h"
+
+LOG_MODULE_REGISTER(maxim_max32664c_init, CONFIG_SENSOR_LOG_LEVEL);
+
+/** @brief Set the SpO2 calibration coefficients.
+ * NOTE: See page 10 of the SpO2 and Heart Rate User Guide for additional information.
+ * @param dev Pointer to device
+ * @return 0 when successful
+ */
+static int max32664c_set_spo2_coeffs(const struct device *dev)
+{
+ const struct max32664c_config *config = dev->config;
+
+ uint8_t tx[15];
+ uint8_t rx;
+
+ /* Write the calibration coefficients */
+ tx[0] = 0x50;
+ tx[1] = 0x07;
+ tx[2] = 0x00;
+
+ /* Copy the A value (index 0) into the transmission buffer */
+ memcpy(&tx[3], &config->spo2_calib[0], sizeof(int32_t));
+
+ /* Copy the B value (index 1) into the transmission buffer */
+ memcpy(&tx[7], &config->spo2_calib[1], sizeof(int32_t));
+
+ /* Copy the C value (index 2) into the transmission buffer */
+ memcpy(&tx[11], &config->spo2_calib[2], sizeof(int32_t));
+
+ return max32664c_i2c_transmit(dev, tx, sizeof(tx), &rx, sizeof(rx),
+ MAX32664C_DEFAULT_CMD_DELAY);
+}
+
+/** @brief Write the default configuration to the sensor hub.
+ * @param dev Pointer to device
+ * @return 0 when successful
+ */
+static int max32664c_write_config(const struct device *dev)
+{
+ uint8_t rx;
+ uint8_t tx[5];
+ const struct max32664c_config *config = dev->config;
+ struct max32664c_data *data = dev->data;
+
+ /* Write the default settings */
+ tx[0] = 0x50;
+ tx[1] = 0x07;
+ tx[2] = 0x13;
+ tx[3] = config->min_integration_time_idx;
+ if (max32664c_i2c_transmit(dev, tx, 4, &rx, 1, MAX32664C_DEFAULT_CMD_DELAY)) {
+ LOG_ERR("Can not write minimum integration time!");
+ return -EINVAL;
+ }
+
+ tx[0] = 0x50;
+ tx[1] = 0x07;
+ tx[2] = 0x14;
+ tx[3] = config->min_sampling_rate_idx;
+ if (max32664c_i2c_transmit(dev, tx, 4, &rx, 1, MAX32664C_DEFAULT_CMD_DELAY)) {
+ LOG_ERR("Can not write minimum sampling rate!");
+ return -EINVAL;
+ }
+
+ tx[0] = 0x50;
+ tx[1] = 0x07;
+ tx[2] = 0x15;
+ tx[3] = config->max_integration_time_idx;
+ if (max32664c_i2c_transmit(dev, tx, 4, &rx, 1, MAX32664C_DEFAULT_CMD_DELAY)) {
+ LOG_ERR("Can not write maximum integration time!");
+ return -EINVAL;
+ }
+
+ tx[0] = 0x50;
+ tx[1] = 0x07;
+ tx[2] = 0x16;
+ tx[3] = config->max_sampling_rate_idx;
+ if (max32664c_i2c_transmit(dev, tx, 4, &rx, 1, MAX32664C_DEFAULT_CMD_DELAY)) {
+ LOG_ERR("Can not write maximum sampling rate!");
+ return -EINVAL;
+ }
+
+ tx[0] = 0x10;
+ tx[1] = 0x02;
+ tx[2] = config->report_period;
+ if (max32664c_i2c_transmit(dev, tx, 3, &rx, 1, MAX32664C_DEFAULT_CMD_DELAY)) {
+ LOG_ERR("Can not set report period!");
+ return -EINVAL;
+ }
+
+ /* Configure WHRM */
+ tx[0] = 0x50;
+ tx[1] = 0x07;
+ tx[2] = 0x17;
+ tx[3] = config->hr_config[0];
+ tx[4] = config->hr_config[1];
+ LOG_DBG("Configuring WHRM: 0x%02X%02X", tx[3], tx[4]);
+ if (max32664c_i2c_transmit(dev, tx, 5, &rx, 1, MAX32664C_DEFAULT_CMD_DELAY)) {
+ LOG_ERR("Can not configure WHRM!");
+ return -EINVAL;
+ }
+
+ /* Configure SpO2 */
+ tx[0] = 0x50;
+ tx[1] = 0x07;
+ tx[2] = 0x18;
+ tx[3] = config->spo2_config[0];
+ tx[4] = config->spo2_config[1];
+ LOG_DBG("Configuring SpO2: 0x%02X%02X", tx[3], tx[4]);
+ if (max32664c_i2c_transmit(dev, tx, 5, &rx, 1, MAX32664C_DEFAULT_CMD_DELAY)) {
+ LOG_ERR("Can not configure SpO2!");
+ return -EINVAL;
+ }
+
+ /* Set the interrupt threshold */
+ tx[0] = 0x10;
+ tx[1] = 0x01;
+ tx[2] = 0x01;
+ if (max32664c_i2c_transmit(dev, tx, 3, &rx, 1, MAX32664C_DEFAULT_CMD_DELAY)) {
+ LOG_ERR("Can not set interrupt threshold!");
+ return -EINVAL;
+ }
+
+ if (max32664c_set_spo2_coeffs(dev)) {
+ LOG_ERR("Can not set SpO2 calibration coefficients!");
+ return -EINVAL;
+ }
+
+ data->motion_time = config->motion_time;
+ data->motion_threshold = config->motion_threshold;
+ memcpy(data->led_current, config->led_current, sizeof(data->led_current));
+
+ return 0;
+}
+
+/** @brief Read the configuration from the sensor hub.
+ * @param dev Pointer to device
+ * @return 0 when successful
+ */
+static int max32664c_read_config(const struct device *dev)
+{
+ uint8_t tx[3];
+ uint8_t rx[2];
+ struct max32664c_data *data = dev->data;
+
+ tx[0] = 0x11;
+ tx[1] = 0x02;
+ if (max32664c_i2c_transmit(dev, tx, 2, rx, 2, MAX32664C_DEFAULT_CMD_DELAY)) {
+ LOG_ERR("Can not read report period!");
+ return -EINVAL;
+ }
+ data->report_period = rx[1];
+
+ tx[0] = 0x51;
+ tx[1] = 0x07;
+ tx[2] = 0x13;
+ if (max32664c_i2c_transmit(dev, tx, 3, rx, 2, MAX32664C_DEFAULT_CMD_DELAY)) {
+ LOG_ERR("Can not read minimum integration time!");
+ return -EINVAL;
+ }
+ data->min_integration_time_idx = rx[1];
+
+ tx[0] = 0x51;
+ tx[1] = 0x07;
+ tx[2] = 0x14;
+ if (max32664c_i2c_transmit(dev, tx, 3, rx, 2, MAX32664C_DEFAULT_CMD_DELAY)) {
+ LOG_ERR("Can not read minimum sampling rate!");
+ return -EINVAL;
+ }
+ data->min_sampling_rate_idx = rx[1];
+
+ tx[0] = 0x51;
+ tx[1] = 0x07;
+ tx[2] = 0x15;
+ if (max32664c_i2c_transmit(dev, tx, 3, rx, 2, MAX32664C_DEFAULT_CMD_DELAY)) {
+ LOG_ERR("Can not read maximum integration time!");
+ return -EINVAL;
+ }
+ data->max_integration_time_idx = rx[1];
+
+ tx[0] = 0x51;
+ tx[1] = 0x07;
+ tx[2] = 0x16;
+ if (max32664c_i2c_transmit(dev, tx, 3, rx, 2, MAX32664C_DEFAULT_CMD_DELAY)) {
+ LOG_ERR("Can not read maximum sampling rate!");
+ return -EINVAL;
+ }
+ data->max_sampling_rate_idx = rx[1];
+
+ return 0;
+}
+
+int max32664c_init_hub(const struct device *dev)
+{
+ struct max32664c_data *data = dev->data;
+
+ LOG_DBG("Initialize sensor hub");
+
+ if (max32664c_write_config(dev)) {
+ LOG_ERR("Can not write default configuration!");
+ return -EINVAL;
+ }
+
+ if (max32664c_read_config(dev)) {
+ LOG_ERR("Can not read configuration!");
+ return -EINVAL;
+ }
+
+ data->is_thread_running = true;
+ data->thread_id = k_thread_create(&data->thread, data->thread_stack,
+ K_THREAD_STACK_SIZEOF(data->thread_stack),
+ (k_thread_entry_t)max32664c_worker, (void *)dev, NULL,
+ NULL, K_LOWEST_APPLICATION_THREAD_PRIO, 0, K_NO_WAIT);
+ k_thread_suspend(data->thread_id);
+ k_thread_name_set(data->thread_id, "max32664c_worker");
+
+ LOG_DBG("Initial configuration:");
+
+#ifndef CONFIG_MAX32664C_USE_STATIC_MEMORY
+ LOG_DBG("\tUsing dynamic memory for queues and buffers");
+#else
+ LOG_DBG("\tUsing static memory for queues and buffers");
+#endif /* CONFIG_MAX32664C_USE_STATIC_MEMORY */
+
+#ifdef CONFIG_MAX32664C_USE_EXTENDED_REPORTS
+ LOG_DBG("\tUsing extended reports");
+#else
+ LOG_DBG("\tUsing normal reports");
+#endif /* CONFIG_MAX32664C_USE_EXTENDED_REPORTS*/
+
+ LOG_DBG("\tReport period: %u", data->report_period);
+ LOG_DBG("\tMinimum integration time: %u", data->min_integration_time_idx);
+ LOG_DBG("\tMinimum sampling rate: %u", data->min_sampling_rate_idx);
+ LOG_DBG("\tMaximum integration time: %u", data->max_integration_time_idx);
+ LOG_DBG("\tMaximum sampling rate: %u", data->max_sampling_rate_idx);
+
+ return 0;
+}
diff --git a/drivers/sensor/adi/max32664c/max32664c_interrupt.c b/drivers/sensor/adi/max32664c/max32664c_interrupt.c
new file mode 100644
index 0000000..492cdf3
--- /dev/null
+++ b/drivers/sensor/adi/max32664c/max32664c_interrupt.c
@@ -0,0 +1,80 @@
+/*
+ * Trigger code for the MAX32664C biometric sensor hub.
+ *
+ * Copyright (c) 2025, Daniel Kampert
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#include "max32664c.h"
+
+LOG_MODULE_REGISTER(maxim_max32664c_interrupt, CONFIG_SENSOR_LOG_LEVEL);
+
+#ifdef CONFIG_MAX32664C_USE_INTERRUPT
+static void max32664c_interrupt_worker(struct k_work *p_work)
+{
+ struct max32664c_data *data = CONTAINER_OF(p_work, struct max32664c_data, interrupt_work);
+
+ /* TODO */
+}
+
+static void max32664c_gpio_callback_handler(const struct device *p_port, struct gpio_callback *p_cb,
+ gpio_port_pins_t pins)
+{
+ ARG_UNUSED(pins);
+ ARG_UNUSED(p_port);
+
+ struct max32664c_data *data = CONTAINER_OF(p_cb, struct max32664c_data, gpio_cb);
+
+ k_work_submit(&data->interrupt_work);
+}
+
+int max32664c_init_interrupt(const struct device *dev)
+{
+ LOG_DBG("\tUsing MFIO interrupt mode");
+
+ int err;
+ uint8_t tx[2];
+ uint8_t rx;
+ struct max32664c_data *data = dev->data;
+ const struct max32664c_config *config = dev->config;
+
+ LOG_DBG("Configure interrupt pin");
+ if (!gpio_is_ready_dt(&config->int_gpio)) {
+ LOG_ERR("GPIO not ready!");
+ return -ENODEV;
+ }
+
+ err = gpio_pin_configure_dt(&config->int_gpio, GPIO_INPUT);
+ if (err < 0) {
+ LOG_ERR("Failed to configure GPIO! Error: %u", err);
+ return err;
+ }
+
+ err = gpio_pin_interrupt_configure_dt(&config->int_gpio, GPIO_INT_EDGE_FALLING);
+ if (err < 0) {
+ LOG_ERR("Failed to configure interrupt! Error: %u", err);
+ return err;
+ }
+
+ gpio_init_callback(&data->gpio_cb, max32664c_gpio_callback_handler,
+ BIT(config->int_gpio.pin));
+
+ err = gpio_add_callback_dt(&config->int_gpio, &data->gpio_cb);
+ if (err < 0) {
+ LOG_ERR("Failed to add GPIO callback! Error: %u", err);
+ return err;
+ }
+
+ data->interrupt_work.handler = max32664c_interrupt_worker;
+
+ tx[0] = 0xB8;
+ tx[1] = 0x01;
+ if (max32664c_i2c_transmit(dev, tx, 2, &rx, 1, MAX32664C_DEFAULT_CMD_DELAY)) {
+ LOG_ERR("Can not enable interrupt mode!");
+ return -EINVAL;
+ }
+
+ return 0;
+}
+#endif /* CONFIG_MAX32664C_USE_INTERRUPT */
diff --git a/drivers/sensor/adi/max32664c/max32664c_worker.c b/drivers/sensor/adi/max32664c/max32664c_worker.c
new file mode 100644
index 0000000..4943c62
--- /dev/null
+++ b/drivers/sensor/adi/max32664c/max32664c_worker.c
@@ -0,0 +1,369 @@
+/*
+ * Background worker for the MAX32664C biometric sensor hub.
+ *
+ * Copyright (c) 2025, Daniel Kampert
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#include <zephyr/sys/byteorder.h>
+
+#include "max32664c.h"
+
+LOG_MODULE_REGISTER(maxim_max32664c_worker, CONFIG_SENSOR_LOG_LEVEL);
+
+/** @brief Read the status from the sensor hub.
+ * NOTE: Table 7 Sensor Hub Status Byte
+ * @param dev Pointer to device
+ * @param status Pointer to status byte
+ * @param i2c_error Pointer to I2C error byte
+ * @return 0 when successful, otherwise an error code
+ */
+static int max32664c_get_hub_status(const struct device *dev, uint8_t *status, uint8_t *i2c_error)
+{
+ uint8_t tx[2] = {0x00, 0x00};
+ uint8_t rx[2];
+
+ if (max32664c_i2c_transmit(dev, tx, sizeof(tx), rx, sizeof(rx),
+ MAX32664C_DEFAULT_CMD_DELAY)) {
+ return -EINVAL;
+ }
+
+ *i2c_error = rx[0];
+ *status = rx[1];
+
+ return 0;
+}
+
+/** @brief Read the FIFO sample count.
+ * @param dev Pointer to device
+ * @param fifo Pointer to FIFO count
+ */
+static int max32664c_get_fifo_count(const struct device *dev, uint8_t *fifo)
+{
+ uint8_t tx[2] = {0x12, 0x00};
+ uint8_t rx[2];
+
+ if (max32664c_i2c_transmit(dev, tx, sizeof(tx), rx, sizeof(rx),
+ MAX32664C_DEFAULT_CMD_DELAY)) {
+ return -EINVAL;
+ }
+
+ *fifo = rx[1];
+
+ return rx[0];
+}
+
+/** @brief Push a item into the message queue.
+ * @param msgq Pointer to message queue
+ * @param data Pointer to data to push
+ */
+static void max32664c_push_to_queue(struct k_msgq *msgq, const void *data)
+{
+ while (k_msgq_put(msgq, data, K_NO_WAIT) != 0) {
+ k_msgq_purge(msgq);
+ }
+}
+
+/** @brief Process the buffer to get the raw data from the sensor hub.
+ * @param dev Pointer to device
+ */
+static void max32664c_parse_and_push_raw(const struct device *dev)
+{
+ struct max32664c_data *data = dev->data;
+ struct max32664c_raw_report_t report;
+
+ report.PPG1 = ((uint32_t)(data->max32664_i2c_buffer[1]) << 16) |
+ ((uint32_t)(data->max32664_i2c_buffer[2]) << 8) |
+ data->max32664_i2c_buffer[3];
+ report.PPG2 = ((uint32_t)(data->max32664_i2c_buffer[4]) << 16) |
+ ((uint32_t)(data->max32664_i2c_buffer[5]) << 8) |
+ data->max32664_i2c_buffer[6];
+ report.PPG3 = ((uint32_t)(data->max32664_i2c_buffer[7]) << 16) |
+ ((uint32_t)(data->max32664_i2c_buffer[8]) << 8) |
+ data->max32664_i2c_buffer[9];
+
+ /* PPG4 to 6 are used for PD2 */
+ report.PPG4 = 0;
+ report.PPG5 = 0;
+ report.PPG6 = 0;
+
+ report.acc.x =
+ ((int16_t)(data->max32664_i2c_buffer[19]) << 8) | data->max32664_i2c_buffer[20];
+ report.acc.y =
+ ((int16_t)(data->max32664_i2c_buffer[21]) << 8) | data->max32664_i2c_buffer[22];
+ report.acc.z =
+ ((int16_t)(data->max32664_i2c_buffer[23]) << 8) | data->max32664_i2c_buffer[24];
+
+ max32664c_push_to_queue(&data->raw_report_queue, &report);
+}
+
+#ifdef CONFIG_MAX32664C_USE_EXTENDED_REPORTS
+/** @brief Process the buffer to get the extended report data from the sensor hub.
+ * @param dev Pointer to device
+ */
+static void max32664c_parse_and_push_ext_report(const struct device *dev)
+{
+ struct max32664c_data *data = dev->data;
+ struct max32664c_ext_report_t report;
+
+ report.op_mode = data->max32664_i2c_buffer[25];
+ report.hr =
+ (((uint16_t)(data->max32664_i2c_buffer[26]) << 8) | data->max32664_i2c_buffer[27]) /
+ 10;
+ report.hr_confidence = data->max32664_i2c_buffer[28];
+ report.rr =
+ (((uint16_t)(data->max32664_i2c_buffer[29]) << 8) | data->max32664_i2c_buffer[30]) /
+ 10;
+ report.rr_confidence = data->max32664_i2c_buffer[31];
+ report.activity_class = data->max32664_i2c_buffer[32];
+ report.total_walk_steps = data->max32664_i2c_buffer[33] |
+ ((uint32_t)(data->max32664_i2c_buffer[34]) << 8) |
+ ((uint32_t)(data->max32664_i2c_buffer[35]) << 16) |
+ ((uint32_t)(data->max32664_i2c_buffer[36]) << 24);
+ report.total_run_steps = data->max32664_i2c_buffer[37] |
+ ((uint32_t)(data->max32664_i2c_buffer[38]) << 8) |
+ ((uint32_t)(data->max32664_i2c_buffer[39]) << 16) |
+ ((uint32_t)(data->max32664_i2c_buffer[40]) << 24);
+ report.total_energy_kcal = data->max32664_i2c_buffer[41] |
+ ((uint32_t)(data->max32664_i2c_buffer[42]) << 8) |
+ ((uint32_t)(data->max32664_i2c_buffer[43]) << 16) |
+ ((uint32_t)(data->max32664_i2c_buffer[44]) << 24);
+ report.total_amr_kcal = data->max32664_i2c_buffer[45] |
+ ((uint32_t)(data->max32664_i2c_buffer[46]) << 8) |
+ ((uint32_t)(data->max32664_i2c_buffer[47]) << 16) |
+ ((uint32_t)(data->max32664_i2c_buffer[48]) << 24);
+ report.led_current_adj1.adj_flag = data->max32664_i2c_buffer[49];
+ report.led_current_adj1.adj_val =
+ (((uint16_t)(data->max32664_i2c_buffer[50]) << 8) | data->max32664_i2c_buffer[51]) /
+ 10;
+ report.led_current_adj2.adj_flag = data->max32664_i2c_buffer[52];
+ report.led_current_adj2.adj_val =
+ (((uint16_t)(data->max32664_i2c_buffer[53]) << 8) | data->max32664_i2c_buffer[54]) /
+ 10;
+ report.led_current_adj3.adj_flag = data->max32664_i2c_buffer[55];
+ report.led_current_adj3.adj_val =
+ (((uint16_t)(data->max32664_i2c_buffer[56]) << 8) | data->max32664_i2c_buffer[57]) /
+ 10;
+ report.integration_time_adj_flag = data->max32664_i2c_buffer[58];
+ report.requested_integration_time = data->max32664_i2c_buffer[59];
+ report.sampling_rate_adj_flag = data->max32664_i2c_buffer[60];
+ report.requested_sampling_rate = data->max32664_i2c_buffer[61];
+ report.requested_sampling_average = data->max32664_i2c_buffer[62];
+ report.hrm_afe_ctrl_state = data->max32664_i2c_buffer[63];
+ report.is_high_motion_for_hrm = data->max32664_i2c_buffer[64];
+ report.scd_state = data->max32664_i2c_buffer[65];
+ report.r_value =
+ (((uint16_t)(data->max32664_i2c_buffer[66]) << 8) | data->max32664_i2c_buffer[67]) /
+ 1000;
+ report.spo2_meas.confidence = data->max32664_i2c_buffer[68];
+ report.spo2_meas.value =
+ (((uint16_t)(data->max32664_i2c_buffer[69]) << 8) | data->max32664_i2c_buffer[70]) /
+ 10;
+ report.spo2_meas.valid_percent = data->max32664_i2c_buffer[71];
+ report.spo2_meas.low_signal_flag = data->max32664_i2c_buffer[72];
+ report.spo2_meas.motion_flag = data->max32664_i2c_buffer[73];
+ report.spo2_meas.low_pi_flag = data->max32664_i2c_buffer[74];
+ report.spo2_meas.unreliable_r_flag = data->max32664_i2c_buffer[75];
+ report.spo2_meas.state = data->max32664_i2c_buffer[76];
+ report.ibi_offset = data->max32664_i2c_buffer[77];
+ report.unreliable_orientation_flag = data->max32664_i2c_buffer[78];
+ report.reserved[0] = data->max32664_i2c_buffer[79];
+ report.reserved[1] = data->max32664_i2c_buffer[80];
+
+ max32664c_push_to_queue(&data->ext_report_queue, &report);
+}
+#else
+/** @brief Process the buffer to get the report data from the sensor hub.
+ * @param dev Pointer to device
+ */
+static void max32664c_parse_and_push_report(const struct device *dev)
+{
+ struct max32664c_data *data = dev->data;
+ struct max32664c_report_t report;
+
+ report.op_mode = data->max32664_i2c_buffer[25];
+ report.hr =
+ (((uint16_t)(data->max32664_i2c_buffer[26]) << 8) | data->max32664_i2c_buffer[27]) /
+ 10;
+ report.hr_confidence = data->max32664_i2c_buffer[28];
+ report.rr =
+ (((uint16_t)(data->max32664_i2c_buffer[29]) << 8) | data->max32664_i2c_buffer[30]) /
+ 10;
+ report.rr_confidence = data->max32664_i2c_buffer[31];
+ report.activity_class = data->max32664_i2c_buffer[32];
+ report.r =
+ (((uint16_t)(data->max32664_i2c_buffer[33]) << 8) | data->max32664_i2c_buffer[34]) /
+ 1000;
+ report.spo2_meas.confidence = data->max32664_i2c_buffer[35];
+ report.spo2_meas.value =
+ (((uint16_t)(data->max32664_i2c_buffer[36]) << 8) | data->max32664_i2c_buffer[37]) /
+ 10;
+ report.spo2_meas.complete = data->max32664_i2c_buffer[38];
+ report.spo2_meas.low_signal_quality = data->max32664_i2c_buffer[39];
+ report.spo2_meas.motion = data->max32664_i2c_buffer[40];
+ report.spo2_meas.low_pi = data->max32664_i2c_buffer[41];
+ report.spo2_meas.unreliable_r = data->max32664_i2c_buffer[42];
+ report.spo2_meas.state = data->max32664_i2c_buffer[43];
+ report.scd_state = data->max32664_i2c_buffer[44];
+
+ max32664c_push_to_queue(&data->report_queue, &report);
+}
+#endif /* CONFIG_MAX32664C_USE_EXTENDED_REPORTS */
+
+/** @brief Worker thread to read the sensor hub.
+ * This thread does the following:
+ * - It polls the sensor hub periodically for new results
+ * - If new messages are available it reads the number of samples
+ * - Then it reads all the samples to clear the FIFO.
+ * It's necessary to clear the complete FIFO because the sensor hub
+ * doesn´t support the reading of a single message and not clearing
+ * the FIFO can cause a FIFO overrun.
+ * - Extract the message data from the FIRST item from the FIFO and
+ * copy them into the right message structure
+ * - Put the message into a message queue
+ * @param dev Pointer to device
+ */
+void max32664c_worker(const struct device *dev)
+{
+ int err;
+ uint8_t fifo = 0;
+ uint8_t status = 0;
+ uint8_t i2c_error = 0;
+ struct max32664c_data *data = dev->data;
+
+ LOG_DBG("Starting worker thread for device: %s", dev->name);
+
+ while (data->is_thread_running) {
+ err = max32664c_get_hub_status(dev, &status, &i2c_error);
+ if (err) {
+ LOG_ERR("Failed to get hub status! Error: %d", err);
+ continue;
+ }
+
+ if (!(status & (1 << MAX32664C_BIT_STATUS_DATA_RDY))) {
+ LOG_WRN("No data ready! Status: 0x%X", status);
+ k_msleep(100);
+ continue;
+ }
+
+ err = max32664c_get_fifo_count(dev, &fifo);
+ if (err) {
+ LOG_ERR("Failed to get FIFO count! Error: %d", err);
+ continue;
+ }
+
+ if (fifo == 0) {
+ LOG_DBG("No data available in the FIFO.");
+ continue;
+ }
+#ifdef CONFIG_MAX32664C_USE_STATIC_MEMORY
+ else if (fifo > CONFIG_MAX32664C_SAMPLE_BUFFER_SIZE) {
+ LOG_ERR("FIFO count %u exceeds maximum buffer size %u!",
+ fifo, CONFIG_MAX32664C_SAMPLE_BUFFER_SIZE);
+
+ /* TODO: Find a good way to clear the FIFO */
+ continue;
+ }
+#endif /* CONFIG_MAX32664C_USE_STATIC_MEMORY */
+
+#ifndef CONFIG_MAX32664C_USE_STATIC_MEMORY
+#ifdef CONFIG_MAX32664C_USE_EXTENDED_REPORTS
+ size_t buffer_size = fifo * (sizeof(struct max32664c_raw_report_t) +
+ sizeof(struct max32664c_ext_report_t)) +
+ 1;
+#else
+ size_t buffer_size = fifo * (sizeof(struct max32664c_raw_report_t) +
+ sizeof(struct max32664c_report_t)) +
+ 1;
+#endif /* CONFIG_MAX32664C_USE_EXTENDED_REPORTS */
+
+ LOG_DBG("Allocating memory %u samples", fifo);
+ LOG_DBG("Allocating memory for the I2C buffer with size: %u", buffer_size);
+ data->max32664_i2c_buffer = (uint8_t *)k_malloc(buffer_size);
+
+ if (data->max32664_i2c_buffer == NULL) {
+ LOG_ERR("Can not allocate memory for the I2C buffer!");
+ continue;
+ }
+#endif /* CONFIG_MAX32664C_USE_STATIC_MEMORY */
+
+ uint8_t tx[2] = {0x12, 0x01};
+
+ switch (data->op_mode) {
+ case MAX32664C_OP_MODE_RAW: {
+ /* Get all samples to clear the FIFO */
+ max32664c_i2c_transmit(
+ dev, tx, 2, data->max32664_i2c_buffer,
+ (fifo * (sizeof(struct max32664c_raw_report_t))) +
+ 1,
+ MAX32664C_DEFAULT_CMD_DELAY);
+
+ if (data->max32664_i2c_buffer[0] != 0) {
+ break;
+ }
+
+ max32664c_parse_and_push_raw(dev);
+
+ break;
+ }
+#ifdef CONFIG_MAX32664C_USE_EXTENDED_REPORTS
+ case MAX32664C_OP_MODE_ALGO_AEC_EXT:
+ case MAX32664C_OP_MODE_ALGO_AGC_EXT: {
+
+ /* Get all samples to clear the FIFO */
+ max32664c_i2c_transmit(
+ dev, tx, 2, data->max32664_i2c_buffer,
+ (fifo * (sizeof(struct max32664c_raw_report_t) +
+ sizeof(struct max32664c_ext_report_t))) +
+ 1,
+ MAX32664C_DEFAULT_CMD_DELAY);
+
+ if (data->max32664_i2c_buffer[0] != 0) {
+ break;
+ }
+
+ max32664c_parse_and_push_raw(dev);
+ max32664c_parse_and_push_ext_report(dev);
+
+ break;
+ }
+#else
+ case MAX32664C_OP_MODE_ALGO_AEC:
+ case MAX32664C_OP_MODE_ALGO_AGC: {
+
+ /* Get all samples to clear the FIFO */
+ max32664c_i2c_transmit(
+ dev, tx, 2, data->max32664_i2c_buffer,
+ (fifo * (sizeof(struct max32664c_raw_report_t) +
+ sizeof(struct max32664c_report_t))) +
+ 1,
+ MAX32664C_DEFAULT_CMD_DELAY);
+
+ if (data->max32664_i2c_buffer[0] != 0) {
+ break;
+ }
+
+ max32664c_parse_and_push_raw(dev);
+ max32664c_parse_and_push_report(dev);
+
+ break;
+ }
+#endif /* CONFIG_MAX32664C_USE_EXTENDED_REPORTS */
+ default: {
+ break;
+ }
+ }
+
+ if (data->max32664_i2c_buffer[0] != 0) {
+ LOG_ERR("Can not read report! Status: 0x%X",
+ data->max32664_i2c_buffer[0]);
+ }
+
+#ifndef CONFIG_MAX32664C_USE_STATIC_MEMORY
+ k_free(data->max32664_i2c_buffer);
+#endif /* CONFIG_MAX32664C_USE_STATIC_MEMORY */
+
+ k_msleep(100);
+ }
+}
diff --git a/dts/bindings/sensor/maxim,max32664c.yml b/dts/bindings/sensor/maxim,max32664c.yml
new file mode 100644
index 0000000..3f8c36c
--- /dev/null
+++ b/dts/bindings/sensor/maxim,max32664c.yml
@@ -0,0 +1,159 @@
+title: |
+ MAX32664 biometric sensor hub
+
+description: |
+ The MAX32664 is a ultra-low power biometric sensor hub.
+
+ NOTES:
+ This driver is primarily written to work with a MAX86141. Other sensors can be
+ used but they are untested! The driver supports up to two photodetectors (PDs)
+ and three LEDs fix. It requires a specific LED
+ configuration for the MAX86141.
+ LED1 -> Green
+ LED2 -> IR
+ LED3 -> Red
+ The LEDs can be changed manually but this may require changes in the driver.
+
+ This driver is tested with Sensor Hub firmware 30.13.31 and an external
+ Accelerometer (e.g. LIS2DH12).
+
+ See more info at:
+ https://www.analog.com/media/en/technical-documentation/data-sheets/MAX32664.pdf
+
+compatible: "maxim,max32664c"
+
+include: [sensor-device.yaml, i2c-device.yaml]
+
+properties:
+ reset-gpios:
+ type: phandle-array
+ required: true
+ description:
+ External System Reset (Active-Low) Input.
+
+ mfio-gpios:
+ type: phandle-array
+ required: true
+ description:
+ MFIO asserts low as an output when the sensor hub needs to
+ communication with the host; MFIO acts as an input and when held
+ low during a reset, the sensor hub enters bootloader mode.
+
+ use-max86141:
+ type: boolean
+ description:
+ Use the MAX86141 as the AFE for the MAX32664C. This is the
+ default and recommended configuration. The driver is optimized for
+ this sensor.
+
+ use-max86161:
+ type: boolean
+ description:
+ Use the MAX86161 as the AFE for the MAX32664C.
+
+ motion-time:
+ type: int
+ default: 200
+ description:
+ Sensor Hub configuration - Motion activation time in milliseconds.
+ The default corresponds to Table 12 in the HR and SpO2 User guide.
+
+ motion-threshold:
+ type: int
+ default: 500
+ description:
+ Sensor Hub configuration - Motion activation time in milli-g.
+ The default corresponds to Table 12 in the HR and SpO2 User guide.
+
+ report-period:
+ type: int
+ default: 1
+ description:
+ Sensor Hub configuration - Set the samples report period (e.g., a value
+ of 25 means a samples report is generated once every 25 samples).
+ The default corresponds to Table 16 in the HR and SpO2 User guide.
+
+ spo2-calib:
+ type: array
+ default: [0xFFE69196, 0x000CB735, 0x00989680]
+ description:
+ Algorithm configuration - SpO2 calibration coefficients.
+ The default corresponds to Table 12 in the HR and SpO2 User guide.
+
+ min-integration-time:
+ type: int
+ default: 14
+ enum:
+ - 14
+ - 29
+ - 58
+ - 117
+ description:
+ Algorithm configuration - Minimum integration time in microseconds.
+ The default corresponds to Table 11 in the HR and SpO2 User guide.
+
+ min-sampling-rate:
+ type: int
+ default: 50
+ enum:
+ - 25
+ - 50
+ - 100
+ - 200
+ - 400
+ description:
+ Algorithm configuration - Minimum sampling rate (samples per second)
+ and averaging (samples).
+ The default corresponds to Table 11 in the HR and SpO2 User guide.
+
+ max-integration-time:
+ type: int
+ default: 117
+ enum:
+ - 14
+ - 29
+ - 58
+ - 117
+ description:
+ Algorithm configuration - Maximum integration time in microseconds.
+ The default corresponds to Table 11 in the HR and SpO2 User guide.
+
+ max-sampling-rate:
+ type: int
+ default: 100
+ enum:
+ - 25
+ - 50
+ - 100
+ - 200
+ - 400
+ description:
+ Algorithm configuration - Maximum sampling rate (samples per second)
+ and averaging (samples).
+ The default corresponds to Table 11 in the HR and SpO2 User guide.
+
+ led-current:
+ type: uint8-array
+ default: [0x7F, 0x7F, 0x7F]
+ description:
+ Initial LED current configuration in bits. Please check the datasheet
+ of the attached AFE to determine the appropriate values.
+ The current can also be changed later by the firmware.
+ Index 0 corresponds to LED1, index 1 to LED2, and index 2 to LED3.
+ The default corresponds to Table 5 in the HR and SpO2 User guide.
+
+ hr-config:
+ type: uint8-array
+ default: [0x00, 0x01]
+ description:
+ Algorithm configuration - LED and PD configuration for the heartrate measurement.
+ The first entry configures channel 1, the second channel 2.
+ The default corresponds to Table 15 in the HR and SpO2 User guide.
+
+ spo2-config:
+ type: uint8-array
+ default: [0x10, 0x20]
+ description:
+ Algorithm configuration - LED and PD configuration for the SpO2 measurement.
+ The first entry configures the IR channel, the second the red channel.
+ The default corresponds to Table 15 in the HR and SpO2 User guide.
diff --git a/include/zephyr/drivers/sensor/max32664c.h b/include/zephyr/drivers/sensor/max32664c.h
new file mode 100644
index 0000000..128f15e
--- /dev/null
+++ b/include/zephyr/drivers/sensor/max32664c.h
@@ -0,0 +1,186 @@
+/*
+ * Copyright (c) 2025 Daniel Kampert
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#ifndef ZEPHYR_INCLUDE_DRIVERS_SENSOR_MAX32664C_H_
+#define ZEPHYR_INCLUDE_DRIVERS_SENSOR_MAX32664C_H_
+
+#include <zephyr/device.h>
+
+/** @brief Converts a motion time in milli-seconds to the corresponding value for the MAX32664C
+ * sensor. This macro should be used when configuring the motion based wake up settings for the
+ * sensor.
+ */
+#define MAX32664C_MOTION_TIME(ms) ((uint8_t)((ms * 25UL) / 1000))
+
+/** @brief Converts a motion threshold in milli-g (Acceleration) to the corresponding value for the
+ * MAX32664C sensor. This macro should be used when configuring the motion based wake up settings
+ * for the sensor.
+ */
+#define MAX32664C_MOTION_THRESHOLD(mg) ((uint8_t)((mg * 16UL) / 1000))
+
+/* MAX32664C specific channels */
+enum sensor_channel_max32664c {
+ /** Heart rate value (bpm) */
+ SENSOR_CHAN_MAX32664C_HEARTRATE = SENSOR_CHAN_PRIV_START,
+ /** SpO2 value (%) */
+ SENSOR_CHAN_MAX32664C_BLOOD_OXYGEN_SATURATION,
+ /** Respiration rate (breaths per minute) */
+ SENSOR_CHAN_MAX32664C_RESPIRATION_RATE,
+ /** Skin contact (1 -> Skin contact, 0, no contact) */
+ SENSOR_CHAN_MAX32664C_SKIN_CONTACT,
+ /** Activity class (index). The reported index is vendor specific. */
+ SENSOR_CHAN_MAX32664C_ACTIVITY,
+ /** Step counter */
+ SENSOR_CHAN_MAX32664C_STEP_COUNTER,
+};
+
+/* MAX32664C specific attributes */
+enum sensor_attribute_max32664c {
+ /** Gender of the subject being monitored */
+ SENSOR_ATTR_MAX32664C_GENDER = SENSOR_ATTR_PRIV_START,
+ /** Age of the subject being monitored */
+ SENSOR_ATTR_MAX32664C_AGE,
+ /** Weight of the subject being monitored */
+ SENSOR_ATTR_MAX32664C_WEIGHT,
+ /** Height of the subject being monitored */
+ SENSOR_ATTR_MAX32664C_HEIGHT,
+ /** Get / Set the operation mode of a sensor. This can be used to
+ * switch between different measurement modes when a sensor supports them.
+ */
+ SENSOR_ATTR_MAX32664C_OP_MODE,
+};
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/** @brief Device operating modes for the MAX32664C sensor.
+ *
+ * This enum defines the various operating modes that the MAX32664C sensor
+ * can be configured to. These modes control the sensor's behavior and
+ * functionality, such as calibration, idle state, raw data output, and
+ * algorithm-based operations.
+ */
+enum max32664c_device_mode {
+ MAX32664C_OP_MODE_IDLE, /**< Idle mode, no algorithm, */
+ /**< sensors or wake on motion running */
+ MAX32664C_OP_MODE_RAW, /**< Raw output mode */
+ /* For hardware testing purposes, the user may choose to start the sensor hub to collect
+ * raw PPG samples. In this case, the host configures the sensor hub to work in Raw Data
+ * mode (no algorithm) by enabling the accelerometer and the AFE.
+ */
+ MAX32664C_OP_MODE_ALGO_AEC, /**< Algorithm AEC mode */
+ /* Automatic Exposure Control (AEC) is Maxim’s gain control algorithm that is superior to
+ * AGC. The AEC algorithm optimally maintains the best SNR range and power optimization. The
+ * targeted SNR range is maintained regardless of skin color or ambient temperature within
+ * the limits of the LED currents configurations; The AEC dynamically manages the
+ * appropriate register settings for sampling rate, LED current, pulse width and integration
+ * time.
+ */
+ MAX32664C_OP_MODE_ALGO_AEC_EXT, /**< Algorithm with extended reports */
+ MAX32664C_OP_MODE_ALGO_AGC, /**< Algorithm AGC mode */
+ /* In this mode, the wearable algorithm suite (SpO2 and WHRM) is enabled and the R value,
+ * SpO2, SpO2 confidence level, heart rate, heart rate confidence level, RR value, and
+ * activity class are reported. Furthermore, automatic gain control (AGC) is enabled.
+ * Because AGC is a subset of AEC functionality, to enable AGC, AEC still needs to be
+ * enabled. However, automatic calculation of target PD should be turned off, and the
+ * desired level of AGC target PD current is set by the user. The user may change the
+ * algorithm to the desired configuration mode. If signal quality is poor, the user may need
+ * to adjust the AGC settings to maintain optimal performance. If signal quality is low, a
+ * LowSNR flag will be set. Excessive motion is also reported with a flag.
+ */
+ MAX32664C_OP_MODE_ALGO_AGC_EXT, /**< Algorithm AGC with extended reports */
+ MAX32664C_OP_MODE_SCD, /**< SCD only mode */
+ MAX32664C_OP_MODE_WAKE_ON_MOTION, /**< Wake on motion mode */
+ MAX32664C_OP_MODE_EXIT_WAKE_ON_MOTION, /**< Exit wake on motion mode */
+ MAX32664C_OP_MODE_STOP_ALGO, /**< Stop the current algorithm */
+};
+
+/** @brief Algorithm modes for the MAX32664C sensor.
+ *
+ * This enum defines the various algorithm modes supported by the MAX32664C sensor.
+ * These modes determine the type of data processing performed by the sensor,
+ * such as continuous heart rate monitoring, SpO2 calculation, or activity tracking.
+ */
+enum max32664c_algo_mode {
+ MAX32664C_ALGO_MODE_CONT_HR_CONT_SPO2,
+ MAX32664C_ALGO_MODE_CONT_HR_SHOT_SPO2,
+ MAX32664C_ALGO_MODE_CONT_HRM,
+ /* NOTE: These algorithm modes are untested */
+ /*MAX32664C_ALGO_MODE_SAMPLED_HRM,*/
+ /*MAX32664C_ALGO_MODE_SAMPLED_HRM_SHOT_SPO2,*/
+ /*MAX32664C_ALGO_MODE_ACTIVITY_TRACK,*/
+ /*MAX32664C_ALGO_MODE_SAMPLED_HRM_FAST_SPO2 = 7,*/
+};
+
+/** @brief Gender settings for the MAX32664C sensor.
+ *
+ * This enum defines the supported gender settings for the MAX32664C sensor.
+ */
+enum max32664c_algo_gender {
+ MAX32664_ALGO_GENDER_MALE,
+ MAX32664_ALGO_GENDER_FEMALE,
+};
+
+/** @brief Activity classes for the MAX32664C sensor.
+ *
+ * This enum defines the supported activity classes for the MAX32664C sensor.
+ */
+enum max32664c_algo_activity {
+ MAX32664C_ALGO_ACTIVITY_REST,
+ MAX32664C_ALGO_ACTIVITY_OTHER,
+ MAX32664C_ALGO_ACTIVITY_WALK,
+ MAX32664C_ALGO_ACTIVITY_RUN,
+ MAX32664C_ALGO_ACTIVITY_BIKE,
+};
+
+/** @brief Data structure for external accelerometer data.
+ *
+ * This structure is used to represent the accelerometer data that can be
+ * collected from an external accelerometer and then fed into the MAX32664C
+ * sensor hub. It contains the x, y, and z acceleration values.
+ * This structure is only used when the external accelerometer is enabled.
+ */
+struct max32664c_acc_data_t {
+ int16_t x;
+ int16_t y;
+ int16_t z;
+} __packed;
+
+#ifdef CONFIG_MAX32664C_USE_FIRMWARE_LOADER
+/** @brief Enter the bootloader mode and run a firmware update.
+ * @param dev Pointer to device
+ * @param firmware Pointer to firmware data
+ * @param size Size of the firmware
+ * @return 0 when successful
+ */
+int max32664c_bl_enter(const struct device *dev, const uint8_t *firmware, uint32_t size);
+
+/** @brief Leave the bootloader and enter the application mode.
+ * @param dev Pointer to device
+ * @return 0 when successful
+ */
+int max32664c_bl_leave(const struct device *dev);
+#endif /* CONFIG_MAX32664C_USE_FIRMWARE_LOADER */
+
+#ifdef CONFIG_MAX32664C_USE_EXTERNAL_ACC
+/** @brief Fill the FIFO buffer with accelerometer data
+ * NOTE: This function supports up to 16 samples and it must be called
+ * periodically to provide accelerometer data to the MAX32664C!
+ * @param dev Pointer to device
+ * @param data Pointer to the accelerometer data structure
+ * @param length Number of samples to fill
+ * @return 0 when successful
+ */
+int max32664c_acc_fill_fifo(const struct device *dev, struct max32664c_acc_data_t *data,
+ uint8_t length);
+#endif /* CONFIG_MAX32664C_USE_EXTERNAL_ACC*/
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* ZEPHYR_INCLUDE_DRIVERS_SENSOR_MAX32664C_H_ */
diff --git a/samples/sensor/max32664c/CMakeLists.txt b/samples/sensor/max32664c/CMakeLists.txt
new file mode 100644
index 0000000..4992762
--- /dev/null
+++ b/samples/sensor/max32664c/CMakeLists.txt
@@ -0,0 +1,7 @@
+# SPDX-License-Identifier: Apache-2.0
+
+cmake_minimum_required(VERSION 3.20.0)
+find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
+project(max32664c)
+
+target_sources(app PRIVATE src/main.c)
diff --git a/samples/sensor/max32664c/README.rst b/samples/sensor/max32664c/README.rst
new file mode 100644
index 0000000..0c25e00
--- /dev/null
+++ b/samples/sensor/max32664c/README.rst
@@ -0,0 +1,49 @@
+.. zephyr:code-sample:: max32664c
+ :name: MAX32664C + MAX86141 Sensor Hub
+ :relevant-api: sensor_interface
+
+Get health data from a MAX32664C and a MAX86141 sensor (polling mode).
+
+NOTE: This example requires sensor hub firmware 30.13.31!
+
+Overview
+********
+
+This sample measures the heart rate and the blood oxygen saturation on a wrist.
+It uses the MAX32664C sensor to control the MAX86141 sensor.
+
+Requirements
+************
+
+This sample uses the MAX32664 sensor controlled using the I2C30 interface at
+the nRF54L15-DK board.
+
+References
+**********
+
+- MAX32664C: https://www.analog.com/en/products/max32664.html
+
+Building and Running
+********************
+
+This project outputs sensor data to the console. It requires a MAX32664C
+sensor to be connected to the desired board. An additional MAX86141 sensor
+must be connected to the MAX32664C to provide the sensor data for the algorithms.
+
+.. zephyr-app-commands::
+ :zephyr-app: samples/sensor/max32664c/
+ :goals: build flash
+
+Sample Output
+=============
+
+.. code-block:: console
+
+ [00:00:00.000,000] <inf> sensor: MAX32664C: Initializing...
+ [00:00:01.600,000] <inf> sensor: MAX32664C: Initialization complete.
+ [00:00:01.600,000] <inf> sensor: MAX32664C: HR: 75 bpm
+ [00:00:01.600,100] <inf> sensor: MAX32664C: HR Confidence: 98
+ [00:00:02.600,000] <inf> sensor: MAX32664C: HR: 76 bpm
+ [00:00:02.600,100] <inf> sensor: MAX32664C: HR Confidence: 97
+ [00:00:03.600,000] <inf> sensor: MAX32664C: HR: 74 bpm
+ [00:00:03.600,100] <inf> sensor: MAX32664C: HR Confidence: 98
diff --git a/samples/sensor/max32664c/boards/nrf54l15dk_nrf54l15_cpuapp.overlay b/samples/sensor/max32664c/boards/nrf54l15dk_nrf54l15_cpuapp.overlay
new file mode 100644
index 0000000..92d64ae
--- /dev/null
+++ b/samples/sensor/max32664c/boards/nrf54l15dk_nrf54l15_cpuapp.overlay
@@ -0,0 +1,79 @@
+/*
+ * Copyright (c) 2025 Daniel Kampert
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+&pinctrl {
+ i2c30_default: i2c30_default {
+ group1 {
+ psels = <NRF_PSEL(TWIM_SDA, 0, 4)>,
+ <NRF_PSEL(TWIM_SCL, 0, 3)>;
+ bias-pull-up;
+ };
+ };
+
+ i2c30_sleep: i2c30_sleep {
+ group1 {
+ psels = <NRF_PSEL(TWIM_SDA, 0, 4)>,
+ <NRF_PSEL(TWIM_SCL, 0, 3)>;
+ low-power-enable;
+ };
+ };
+};
+
+/ {
+ aliases {
+ sensor = &biometric_hub;
+ };
+};
+
+&i2c30 {
+ compatible = "nordic,nrf-twim";
+ status = "okay";
+ clock-frequency = <I2C_BITRATE_FAST>;
+
+ // Flash buffer size is needed for the I2C bootloader to flash the firmware
+ zephyr,flash-buf-max-size = <8250>;
+
+ pinctrl-0 = <&i2c30_default>;
+ pinctrl-1 = <&i2c30_sleep>;
+ pinctrl-names = "default", "sleep";
+
+ biometric_hub: max32664c@55 {
+ compatible = "maxim,max32664c";
+ reg = <0x55>;
+ status = "okay";
+ reset-gpios = <&gpio2 0 GPIO_ACTIVE_HIGH>;
+ mfio-gpios = <&gpio2 1 GPIO_ACTIVE_HIGH>;
+ use-max86141;
+ };
+};
+
+&dppic10 {
+ status = "okay";
+};
+
+&ppib11 {
+ status = "okay";
+};
+
+&ppib21 {
+ status = "okay";
+};
+
+&dppic20 {
+ status = "okay";
+};
+
+&ppib22 {
+ status = "okay";
+};
+
+&ppib30 {
+ status = "okay";
+};
+
+&dppic30 {
+ status = "okay";
+};
diff --git a/samples/sensor/max32664c/prj.conf b/samples/sensor/max32664c/prj.conf
new file mode 100644
index 0000000..653e8d1
--- /dev/null
+++ b/samples/sensor/max32664c/prj.conf
@@ -0,0 +1,7 @@
+CONFIG_I2C=y
+CONFIG_SENSOR=y
+
+CONFIG_LOG=y
+CONFIG_LOG_PRINTK=y
+
+CONFIG_HEAP_MEM_POOL_SIZE=16384
diff --git a/samples/sensor/max32664c/sample.yaml b/samples/sensor/max32664c/sample.yaml
new file mode 100644
index 0000000..e515826
--- /dev/null
+++ b/samples/sensor/max32664c/sample.yaml
@@ -0,0 +1,15 @@
+sample:
+ name: MAX32664C heart rate monitor sample
+tests:
+ sample.sensor.max32664c:
+ harness: sensor
+ platform_allow:
+ - nrf54l15dk/nrf54l15/cpuapp
+ integration_platforms:
+ - nrf54l15dk/nrf54l15/cpuapp
+ tags:
+ - sensors
+ - heart rate
+ filter: dt_compat_enabled("maxim,max32664c")
+ depends_on:
+ - i2c
diff --git a/samples/sensor/max32664c/src/main.c b/samples/sensor/max32664c/src/main.c
new file mode 100644
index 0000000..96a32cb
--- /dev/null
+++ b/samples/sensor/max32664c/src/main.c
@@ -0,0 +1,77 @@
+/*
+ * Copyright (c) 2025 Daniel Kampert
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#include <zephyr/kernel.h>
+#include <zephyr/devicetree.h>
+#include <zephyr/logging/log.h>
+#include <zephyr/drivers/sensor.h>
+
+#include <zephyr/drivers/sensor/max32664c.h>
+
+static const struct device *const sensor_hub = DEVICE_DT_GET(DT_ALIAS(sensor));
+
+LOG_MODULE_REGISTER(main, LOG_LEVEL_INF);
+
+static void update(void)
+{
+ struct sensor_value value;
+
+ sensor_sample_fetch(sensor_hub);
+ sensor_attr_get(sensor_hub, SENSOR_CHAN_MAX32664C_HEARTRATE, SENSOR_ATTR_MAX32664C_OP_MODE,
+ &value);
+ if (value.val1 == MAX32664C_OP_MODE_RAW) {
+ struct sensor_value x;
+ struct sensor_value y;
+ struct sensor_value z;
+
+ if (sensor_channel_get(sensor_hub, SENSOR_CHAN_ACCEL_X, &x) ||
+ sensor_channel_get(sensor_hub, SENSOR_CHAN_ACCEL_Y, &y) ||
+ sensor_channel_get(sensor_hub, SENSOR_CHAN_ACCEL_Z, &z)) {
+ LOG_ERR("Failed to get accelerometer data");
+ return;
+ }
+
+ LOG_INF("\tx: %i", x.val1);
+ LOG_INF("\ty: %i", y.val1);
+ LOG_INF("\tz: %i", z.val1);
+ } else if (value.val1 == MAX32664C_OP_MODE_ALGO_AEC) {
+ struct sensor_value hr;
+
+ if (sensor_channel_get(sensor_hub, SENSOR_CHAN_MAX32664C_HEARTRATE, &hr)) {
+ LOG_ERR("Failed to get heart rate data");
+ return;
+ }
+
+ LOG_INF("HR: %u bpm", hr.val1);
+ LOG_INF("HR Confidence: %u", hr.val2);
+ } else {
+ LOG_WRN("Operation mode not implemented: %u", value.val1);
+ }
+}
+
+int main(void)
+{
+ struct sensor_value value;
+
+ if (!device_is_ready(sensor_hub)) {
+ LOG_ERR("Sensor hub not ready!");
+ return -1;
+ }
+
+ LOG_INF("Sensor hub ready");
+
+ value.val1 = MAX32664C_OP_MODE_ALGO_AEC;
+ value.val2 = MAX32664C_ALGO_MODE_CONT_HRM;
+ sensor_attr_set(sensor_hub, SENSOR_CHAN_MAX32664C_HEARTRATE, SENSOR_ATTR_MAX32664C_OP_MODE,
+ &value);
+
+ while (1) {
+ update();
+ k_msleep(1000);
+ }
+
+ return 0;
+}