From 98fc2fb4fcd91ab1e6410941b9b1f42e0d927df1 Mon Sep 17 00:00:00 2001 From: Logan Saint-Germain Date: Thu, 25 Sep 2025 10:28:56 +0200 Subject: [PATCH 1/4] drivers: sensor: max30101: Enable support for multiple instance The max30101 sensor driver doesn't support multiple instance. Update Kconfig and maxim,max30101.yaml for instance based configuration. Propagate changes over existing files. Signed-off-by: Logan Saint-Germain --- boards/nxp/hexiwear/hexiwear_mk64f12.dts | 8 +- drivers/sensor/maxim/max30101/Kconfig | 203 +----------------- drivers/sensor/maxim/max30101/max30101.c | 115 +++++----- drivers/sensor/maxim/max30101/max30101.h | 54 +++-- dts/bindings/sensor/maxim,max30101.yaml | 128 +++++++++++ .../boards/hexiwear_mk64f12.overlay | 1 + tests/drivers/build_all/sensor/i2c.dtsi | 1 + 7 files changed, 227 insertions(+), 283 deletions(-) diff --git a/boards/nxp/hexiwear/hexiwear_mk64f12.dts b/boards/nxp/hexiwear/hexiwear_mk64f12.dts index 12f0d8c0aab5c..6ac4661aebd04 100644 --- a/boards/nxp/hexiwear/hexiwear_mk64f12.dts +++ b/boards/nxp/hexiwear/hexiwear_mk64f12.dts @@ -1,4 +1,9 @@ -/* SPDX-License-Identifier: Apache-2.0 */ +/* + * Copyright (c) 2022, NXP + * Copyright (c) 2025, CATIE + * + * SPDX-License-Identifier: Apache-2.0 + */ /dts-v1/; @@ -124,6 +129,7 @@ status = "disabled"; compatible = "maxim,max30101"; reg = <0x57>; + acq-mode = "multi-led"; }; }; diff --git a/drivers/sensor/maxim/max30101/Kconfig b/drivers/sensor/maxim/max30101/Kconfig index dbc968995a59e..164ec7140d12b 100644 --- a/drivers/sensor/maxim/max30101/Kconfig +++ b/drivers/sensor/maxim/max30101/Kconfig @@ -1,205 +1,12 @@ # MAX30101 heart rate sensor - +# # Copyright (c) 2017, NXP +# Copyright (c) 2025, CATIE +# # SPDX-License-Identifier: Apache-2.0 -menuconfig MAX30101 +config MAX30101 bool "MAX30101 Pulse Oximeter and Heart Rate Sensor" default y depends on DT_HAS_MAXIM_MAX30101_ENABLED - select I2C - -if MAX30101 - -config MAX30101_SMP_AVE - int "Sample averaging" - range 0 7 - default 0 - help - To reduce the amount of data throughput, adjacent samples (in each - individual channel) can be averaged and decimated on the chip by - setting this register. Set to 0 for no averaging. - 0 = 1 sample (no averaging) - 1 = 2 samples - 2 = 4 samples - 3 = 8 samples - 4 = 16 samples - 5 = 32 samples - 6 = 32 samples - 7 = 32 samples - -config MAX30101_FIFO_ROLLOVER_EN - bool "FIFO rolls on full" - help - Controls the behavior of the FIFO when the FIFO becomes completely - filled with data. If set, the FIFO address rolls over to zero and the - FIFO continues to fill with new data. If not set, then the FIFO is - not updated until FIFO_DATA is read or the WRITE/READ pointer - positions are changed. - -config MAX30101_FIFO_A_FULL - int "FIFO almost full value" - range 0 15 - default 0 - help - Set the trigger for the FIFO_A_FULL interrupt - -choice MAX30101_MODE - prompt "Mode control" - default MAX30101_MULTI_LED_MODE - -config MAX30101_HEART_RATE_MODE - bool "Heart rate mode" - help - Set to operate in heart rate only mode. The red LED channel is - active. - -config MAX30101_SPO2_MODE - bool "SpO2 mode" - help - Set to operate in SpO2 mode. The red and IR LED channels are active. - -config MAX30101_MULTI_LED_MODE - bool "Multi-LED mode" - help - Set to operate in multi-LED mode. The green, red, and/or IR LED - channels are active. - -endchoice - -config MAX30101_ADC_RGE - int "ADC range control" - range 0 3 - default 2 - help - Set the ADC's full-scale range. - 0 = 7.81 pA/LSB - 1 = 15.63 pA/LSB - 2 = 31.25 pA/LSB - 3 = 62.5 pA/LSB - -config MAX30101_SR - int "ADC sample rate control" - range 0 7 - default 0 - help - Set the effective sampling rate with one sample consisting of one - pulse/conversion per active LED channel. In SpO2 mode, these means - one IR pulse/conversion and one red pulse/conversion per sample - period. - 0 = 50 Hz - 1 = 100 Hz - 2 = 200 Hz - 3 = 400 Hz - 4 = 800 Hz - 5 = 1000 Hz - 6 = 1600 Hz - 7 = 3200 Hz - -config MAX30101_LED1_PA - hex "LED1 (red) pulse amplitude" - range 0 0xff - default 0xff - help - Set the pulse amplitude to control the LED1 (red) current. The actual - measured LED current for each part can vary significantly due to the - trimming methodology. - 0x00 = 0.0 mA - 0x01 = 0.2 mA - 0x02 = 0.4 mA - 0x0f = 3.1 mA - 0xff = 50.0 mA - -config MAX30101_LED2_PA - hex "LED2 (IR) pulse amplitude" - range 0 0xff - default 0x33 - help - Set the pulse amplitude to control the LED2 (IR) current. The actual - measured LED current for each part can vary significantly due to the - trimming methodology. - 0x00 = 0.0 mA - 0x01 = 0.2 mA - 0x02 = 0.4 mA - 0x0f = 3.1 mA - 0xff = 50.0 mA - -config MAX30101_LED3_PA - hex "LED3 (green) pulse amplitude" - range 0 0xff - default 0xff - help - Set the pulse amplitude to control the LED3 (green) current. The - actual measured LED current for each part can vary significantly due - to the trimming methodology. - 0x00 = 0.0 mA - 0x01 = 0.2 mA - 0x02 = 0.4 mA - 0x0f = 3.1 mA - 0xff = 50.0 mA - -if MAX30101_MULTI_LED_MODE - -config MAX30101_SLOT1 - int "Slot 1" - range 0 7 - default 3 - help - Set which LED and pulse amplitude are active in time slot 1. - 0: None (disabled) - 1: LED1 (red), LED1_PA - 2: LED2 (IR), LED2_PA - 3: LED3 (green), LED3_PA - 4: None (disabled) - 5: LED1 (red), PILOT_PA - 6: LED2 (IR), PILOT_PA - 7: LED3 (green), PILOT_PA - -config MAX30101_SLOT2 - int "Slot 2" - range 0 7 - default 0 - help - Set which LED and pulse amplitude are active in time slot 2. - 0: None (disabled) - 1: LED1 (red), LED1_PA - 2: LED2 (IR), LED2_PA - 3: LED3 (green), LED3_PA - 4: None (disabled) - 5: LED1 (red), PILOT_PA - 6: LED2 (IR), PILOT_PA - 7: LED3 (green), PILOT_PA - -config MAX30101_SLOT3 - int "Slot 3" - range 0 7 - default 0 - help - Set which LED and pulse amplitude are active in time slot 3. - 0: None (disabled) - 1: LED1 (red), LED1_PA - 2: LED2 (IR), LED2_PA - 3: LED3 (green), LED3_PA - 4: None (disabled) - 5: LED1 (red), PILOT_PA - 6: LED2 (IR), PILOT_PA - 7: LED3 (green), PILOT_PA - -config MAX30101_SLOT4 - int "Slot 4" - range 0 7 - default 0 - help - Set which LED and pulse amplitude are active in time slot 4. - 0: None (disabled) - 1: LED1 (red), LED1_PA - 2: LED2 (IR), LED2_PA - 3: LED3 (green), LED3_PA - 4: None (disabled) - 5: LED1 (red), PILOT_PA - 6: LED2 (IR), PILOT_PA - 7: LED3 (green), PILOT_PA - -endif # MAX30101_MULTI_LED_MODE - -endif # MAX30101 + select I2C if $(dt_compat_on_bus,$(DT_COMPAT_MAXIM_MAX30101),i2c) diff --git a/drivers/sensor/maxim/max30101/max30101.c b/drivers/sensor/maxim/max30101/max30101.c index d7e66035a35ef..b32adfdacb6d8 100644 --- a/drivers/sensor/maxim/max30101/max30101.c +++ b/drivers/sensor/maxim/max30101/max30101.c @@ -100,7 +100,6 @@ static int max30101_init(const struct device *dev) uint8_t part_id; uint8_t mode_cfg; uint32_t led_chan; - int fifo_chan; if (!device_is_ready(config->i2c.bus)) { LOG_ERR("Bus device is not ready"); @@ -142,7 +141,7 @@ static int max30101_init(const struct device *dev) /* Write the mode configuration register */ if (i2c_reg_write_byte_dt(&config->i2c, MAX30101_REG_MODE_CFG, - config->mode)) { + max30101_mode_convert[config->mode])) { return -EIO; } @@ -165,23 +164,24 @@ static int max30101_init(const struct device *dev) config->led_pa[2])) { return -EIO; } + if (i2c_reg_write_byte_dt(&config->i2c, MAX30101_REG_LED4_PA, config->led_pa[2])) { + return -EIO; + } -#ifdef CONFIG_MAX30101_MULTI_LED_MODE - uint8_t multi_led[2]; + if (!config->mode) { + uint8_t multi_led[2]; - /* Write the multi-LED mode control registers */ - multi_led[0] = (config->slot[1] << 4) | (config->slot[0]); - multi_led[1] = (config->slot[3] << 4) | (config->slot[2]); + /* Write the multi-LED mode control registers */ + multi_led[0] = (config->slot[1] << 4) | (config->slot[0]); + multi_led[1] = (config->slot[3] << 4) | (config->slot[2]); - if (i2c_reg_write_byte_dt(&config->i2c, MAX30101_REG_MULTI_LED, - multi_led[0])) { - return -EIO; - } - if (i2c_reg_write_byte_dt(&config->i2c, MAX30101_REG_MULTI_LED + 1, - multi_led[1])) { - return -EIO; + if (i2c_reg_write_byte_dt(&config->i2c, MAX30101_REG_MULTI_LED, multi_led[0])) { + return -EIO; + } + if (i2c_reg_write_byte_dt(&config->i2c, MAX30101_REG_MULTI_LED + 1, multi_led[1])) { + return -EIO; + } } -#endif /* Initialize the channel map and active channel count */ data->num_channels = 0U; @@ -192,9 +192,8 @@ static int max30101_init(const struct device *dev) /* Count the number of active channels and build a map that translates * the LED channel number (red/ir/green) to the fifo channel number. */ - for (fifo_chan = 0; fifo_chan < MAX30101_MAX_NUM_CHANNELS; - fifo_chan++) { - led_chan = (config->slot[fifo_chan] & MAX30101_SLOT_LED_MASK)-1; + for (int fifo_chan = 0; fifo_chan < MAX30101_MAX_NUM_CHANNELS; fifo_chan++) { + led_chan = (config->slot[fifo_chan] & MAX30101_SLOT_LED_MASK) - 1; if (led_chan < MAX30101_MAX_NUM_CHANNELS) { data->map[led_chan] = fifo_chan; data->num_channels++; @@ -204,47 +203,39 @@ static int max30101_init(const struct device *dev) return 0; } -static struct max30101_config max30101_config = { - .i2c = I2C_DT_SPEC_INST_GET(0), - .fifo = (CONFIG_MAX30101_SMP_AVE << MAX30101_FIFO_CFG_SMP_AVE_SHIFT) | -#ifdef CONFIG_MAX30101_FIFO_ROLLOVER_EN - MAX30101_FIFO_CFG_ROLLOVER_EN_MASK | -#endif - (CONFIG_MAX30101_FIFO_A_FULL << - MAX30101_FIFO_CFG_FIFO_FULL_SHIFT), - -#if defined(CONFIG_MAX30101_HEART_RATE_MODE) - .mode = MAX30101_MODE_HEART_RATE, - .slot[0] = MAX30101_SLOT_RED_LED1_PA, - .slot[1] = MAX30101_SLOT_DISABLED, - .slot[2] = MAX30101_SLOT_DISABLED, - .slot[3] = MAX30101_SLOT_DISABLED, -#elif defined(CONFIG_MAX30101_SPO2_MODE) - .mode = MAX30101_MODE_SPO2, - .slot[0] = MAX30101_SLOT_RED_LED1_PA, - .slot[1] = MAX30101_SLOT_IR_LED2_PA, - .slot[2] = MAX30101_SLOT_DISABLED, - .slot[3] = MAX30101_SLOT_DISABLED, -#else - .mode = MAX30101_MODE_MULTI_LED, - .slot[0] = CONFIG_MAX30101_SLOT1, - .slot[1] = CONFIG_MAX30101_SLOT2, - .slot[2] = CONFIG_MAX30101_SLOT3, - .slot[3] = CONFIG_MAX30101_SLOT4, -#endif - - .spo2 = (CONFIG_MAX30101_ADC_RGE << MAX30101_SPO2_ADC_RGE_SHIFT) | - (CONFIG_MAX30101_SR << MAX30101_SPO2_SR_SHIFT) | - (MAX30101_PW_18BITS << MAX30101_SPO2_PW_SHIFT), - - .led_pa[0] = CONFIG_MAX30101_LED1_PA, - .led_pa[1] = CONFIG_MAX30101_LED2_PA, - .led_pa[2] = CONFIG_MAX30101_LED3_PA, -}; - -static struct max30101_data max30101_data; - -SENSOR_DEVICE_DT_INST_DEFINE(0, max30101_init, NULL, - &max30101_data, &max30101_config, - POST_KERNEL, CONFIG_SENSOR_INIT_PRIORITY, - &max30101_driver_api); +#define MAX30101_CHECK(n) \ + BUILD_ASSERT(DT_INST_PROP_LEN(n, led_pa) == 3, \ + "MAX30101 led-pa property must have exactly 3 elements"); \ + BUILD_ASSERT(DT_INST_PROP_LEN(n, led_slot) == 4, \ + "MAX30101 led-slot property must have exactly 4 elements") + +#define MAX30101_SLOT_CFG(n) \ + COND_CODE_1(DT_INST_ENUM_HAS_VALUE(n, acq_mode, heart_rate), \ + (MAX30101_HR_SLOTS), \ + (COND_CODE_1(DT_INST_ENUM_HAS_VALUE(n, acq_mode, spo2), \ + (MAX30101_SPO2_SLOTS), \ + (MAX30101_MULTI_LED(n)) \ + )) \ + ) + +#define MAX30101_INIT(n) \ + MAX30101_CHECK(n); \ + static const struct max30101_config max30101_config_##n = { \ + .i2c = I2C_DT_SPEC_INST_GET(n), \ + .fifo = (DT_INST_ENUM_IDX(n, smp_ave) << MAX30101_FIFO_CFG_SMP_AVE_SHIFT) | \ + (DT_INST_PROP(n, fifo_rollover_en) \ + << MAX30101_FIFO_CFG_ROLLOVER_EN_SHIFT) | \ + (DT_INST_PROP(n, fifo_a_full) << MAX30101_FIFO_CFG_FIFO_FULL_SHIFT), \ + .mode = DT_INST_ENUM_IDX(n, acq_mode), \ + .spo2 = (DT_INST_ENUM_IDX(n, adc_rge) << MAX30101_SPO2_ADC_RGE_SHIFT) | \ + (DT_INST_ENUM_IDX(n, smp_sr) << MAX30101_SPO2_SR_SHIFT) | \ + (DT_INST_ENUM_IDX(n, led_pw) << MAX30101_SPO2_PW_SHIFT), \ + .led_pa = DT_INST_PROP(n, led_pa), \ + .slot = MAX30101_SLOT_CFG(n), \ + }; \ + static struct max30101_data max30101_data_##n; \ + SENSOR_DEVICE_DT_INST_DEFINE(n, max30101_init, NULL, &max30101_data_##n, \ + &max30101_config_##n, POST_KERNEL, \ + CONFIG_SENSOR_INIT_PRIORITY, &max30101_driver_api); + +DT_INST_FOREACH_STATUS_OKAY(MAX30101_INIT) diff --git a/drivers/sensor/maxim/max30101/max30101.h b/drivers/sensor/maxim/max30101/max30101.h index fd8f56e6edae8..00aacdc8e7e64 100644 --- a/drivers/sensor/maxim/max30101/max30101.h +++ b/drivers/sensor/maxim/max30101/max30101.h @@ -22,6 +22,7 @@ #define MAX30101_REG_LED1_PA 0x0c #define MAX30101_REG_LED2_PA 0x0d #define MAX30101_REG_LED3_PA 0x0e +#define MAX30101_REG_LED4_PA 0x0f #define MAX30101_REG_PILOT_PA 0x10 #define MAX30101_REG_MULTI_LED 0x11 #define MAX30101_REG_TINT 0x1f @@ -34,6 +35,7 @@ #define MAX30101_INT_PPG_MASK (1 << 6) #define MAX30101_FIFO_CFG_SMP_AVE_SHIFT 5 +#define MAX30101_FIFO_CFG_ROLLOVER_EN_SHIFT 4 #define MAX30101_FIFO_CFG_FIFO_FULL_SHIFT 0 #define MAX30101_FIFO_CFG_ROLLOVER_EN_MASK (1 << 4) @@ -57,32 +59,40 @@ #define MAX30101_FIFO_DATA_MASK ((1 << MAX30101_FIFO_DATA_BITS) - 1) enum max30101_mode { - MAX30101_MODE_HEART_RATE = 2, - MAX30101_MODE_SPO2 = 3, - MAX30101_MODE_MULTI_LED = 7, + MAX30101_MODE_HEART_RATE = 2, + MAX30101_MODE_SPO2 = 3, + MAX30101_MODE_MULTI_LED = 7, }; +static const uint8_t max30101_mode_convert[3] = {7, 2, 3}; + enum max30101_slot { - MAX30101_SLOT_DISABLED = 0, - MAX30101_SLOT_RED_LED1_PA, - MAX30101_SLOT_IR_LED2_PA, - MAX30101_SLOT_GREEN_LED3_PA, - MAX30101_SLOT_RED_PILOT_PA, - MAX30101_SLOT_IR_PILOT_PA, - MAX30101_SLOT_GREEN_PILOT_PA, + MAX30101_SLOT_DISABLED = 0, + MAX30101_SLOT_RED_LED = 1, + MAX30101_SLOT_IR_LED = 2, + MAX30101_SLOT_GREEN_LED = 3, }; -enum max30101_led_channel { - MAX30101_LED_CHANNEL_RED = 0, - MAX30101_LED_CHANNEL_IR, - MAX30101_LED_CHANNEL_GREEN, -}; +#define MAX30101_HR_SLOTS \ + {MAX30101_SLOT_RED_LED, MAX30101_SLOT_DISABLED, MAX30101_SLOT_DISABLED, \ + MAX30101_SLOT_DISABLED} -enum max30101_pw { - MAX30101_PW_15BITS = 0, - MAX30101_PW_16BITS, - MAX30101_PW_17BITS, - MAX30101_PW_18BITS, +#define MAX30101_SPO2_SLOTS \ + {MAX30101_SLOT_RED_LED, MAX30101_SLOT_IR_LED, MAX30101_SLOT_DISABLED, \ + MAX30101_SLOT_DISABLED} + +#define MAX30101_MULTI_LED(n) \ + { \ + DT_INST_PROP_BY_IDX(n, led_slot, 0), \ + DT_INST_PROP_BY_IDX(n, led_slot, 1), \ + DT_INST_PROP_BY_IDX(n, led_slot, 2), \ + DT_INST_PROP_BY_IDX(n, led_slot, 3), \ + } + +enum max30101_led_channel { + MAX30101_LED_CHANNEL_RED = 0, + MAX30101_LED_CHANNEL_IR = 1, + MAX30101_LED_CHANNEL_GREEN = 2, }; struct max30101_config { @@ -90,8 +100,8 @@ struct max30101_config { uint8_t fifo; uint8_t spo2; uint8_t led_pa[MAX30101_MAX_NUM_CHANNELS]; - enum max30101_mode mode; - enum max30101_slot slot[4]; + uint8_t mode; + uint8_t slot[4]; }; struct max30101_data { diff --git a/dts/bindings/sensor/maxim,max30101.yaml b/dts/bindings/sensor/maxim,max30101.yaml index afd357c02e400..2cf02e2ccb1e8 100644 --- a/dts/bindings/sensor/maxim,max30101.yaml +++ b/dts/bindings/sensor/maxim,max30101.yaml @@ -1,4 +1,8 @@ +# Device Tree binding for Maxim MAX30101 +# # Copyright (c) 2018, NXP +# Copyright (c) 2025, CATIE +# # SPDX-License-Identifier: Apache-2.0 description: MAX30101 heart rate sensor @@ -6,3 +10,127 @@ description: MAX30101 heart rate sensor compatible: "maxim,max30101" include: [sensor-device.yaml, i2c-device.yaml] + +properties: + fifo-rollover-en: + type: boolean + description: | + Controls the behavior of the FIFO when the FIFO becomes completely + filled with data. If set, the FIFO address rolls over to zero and the + FIFO continues to fill with new data. If not set, then the FIFO is + not updated until FIFO_DATA is read or the WRITE/READ pointer + positions are changed. + fifo-a-full: + type: int + default: 0 + enum: + - 0 # Each 32 samples @ 0 empty space + - 1 # Each 31 samples @ 1 empty space + - 2 # Each 30 samples @ 2 empty space + - 3 # Each 29 samples @ 3 empty space + - 4 # Each 28 samples @ 4 empty space + - 5 # Each 27 samples @ 5 empty space + - 6 # Each 26 samples @ 6 empty space + - 7 # Each 25 samples @ 7 empty space + - 8 # Each 24 samples @ 8 empty space + - 9 # Each 23 samples @ 9 empty space + - 10 # Each 22 samples @ 10 empty space + - 11 # Each 21 samples @ 11 empty space + - 12 # Each 20 samples @ 12 empty space + - 13 # Each 19 samples @ 13 empty space + - 14 # Each 18 samples @ 14 empty space + - 15 # Each 17 samples @ 15 empty space + description: | + Configure the trigger for the FIFO_A_FULL interrupt (e.g. if set to 2, + then the flag is set when the 30th word is written to the FIFO). + Default set to 0, same as after Power Reset. Range: 0 - 15. + acq-mode: + type: string + required: true + enum: + - "multi-led" # Multi-LED mode, leds according configuration of slots + - "heart-rate" # Heart rate (HR) mode, only red led + - "spo2" # SpO2 mode, red and ir led + description: | + Set the operation mode of the MAX30101. + smp-ave: + type: int + default: 1 + enum: + - 1 # 1 sample (no averaging) + - 2 # 2 samples + - 4 # 4 samples + - 8 # 8 samples + - 16 # 16 samples + - 32 # 32 samples + description: | + To reduce the amount of data throughput, adjacent samples (in each + individual channel) can be averaged and decimated on the chip. + Default set to 1 for no averaging, same as after Power Reset. + adc-rge: + type: int + default: 8192 + enum: + - 2048 # 7.81 pA/LSB + - 4096 # 15.63 pA/LSB + - 8192 # 31.25 pA/LSB + - 16384 # 62.5 pA/LSB + description: | + Set the ADC's full-scale range at 18 bits resolution. + Default set to 8192, compromise between precision and consumption. + smp-sr: + type: int + default: 50 + enum: + - 50 # 50 Hz + - 100 # 100 Hz + - 200 # 200 Hz + - 400 # 400 Hz + - 800 # 800 Hz + - 1000 # 1000 Hz + - 1600 # 1600 Hz + - 3200 # 3200 Hz + description: | + Set the effective sampling rate with one sample consisting of one + pulse/conversion per active LED channel. In SpO2 mode, these means + one IR pulse/conversion and one red pulse/conversion per sample + period. Only one RED pulse/conversion in HR mode. + Default set to 50 Hz, same as after Power Reset. + led-pw: + type: int + default: 69 + enum: + - 69 # 69 us | 15 bits resolution + - 118 # 118 us | 16 bits resolution + - 215 # 215 us | 17 bits resolution + - 411 # 411 us | 18 bits resolution + description: | + Set the pulse width for each LED to control the integration time + of the ADC in us. The ADC resolution is directly related to the + integration time. + Default set to 69 us, same as after Power Reset. + led-pa: + type: uint8-array + default: [0xff, 0xff, 0xff] + description: | + Set the pulse amplitude to control the LED current. The actual + measured LED current for each part can vary significantly due to the + trimming methodology. + [0]: Red LED + [1]: IR LED + [2]: Green LED + Default set to [0xff, 0xff, 0xff], activate any chosen LED channel. + Value range: 0x00 - 0xFF | 0.0 mA - 50.0 mA + led-slot: + type: array + default: [0, 0, 0, 0] + description: | + Set which LED are active in each time slot for Multi-LED mode only. + 0: None (Disabled) + 1: Red LED + 2: InfraRed LED + 3: Green LED + Default set to [0, 0, 0, 0], no LED activated in multi-LED mode. + User needs to choose wich LED is active for each slot. + NOTE: If a LED is present on multiple slots, `sensor_channel_get` + will result in the averaging of the values. diff --git a/samples/sensor/heart_rate/boards/hexiwear_mk64f12.overlay b/samples/sensor/heart_rate/boards/hexiwear_mk64f12.overlay index 79b6f41a93c3f..87ec686e8122e 100644 --- a/samples/sensor/heart_rate/boards/hexiwear_mk64f12.overlay +++ b/samples/sensor/heart_rate/boards/hexiwear_mk64f12.overlay @@ -13,6 +13,7 @@ &i2c0 { max30101: max30101@57 { status = "okay"; + led-slot = <3 0 0 0>; }; }; diff --git a/tests/drivers/build_all/sensor/i2c.dtsi b/tests/drivers/build_all/sensor/i2c.dtsi index eb505c663f7ff..58bca0c5f54bb 100644 --- a/tests/drivers/build_all/sensor/i2c.dtsi +++ b/tests/drivers/build_all/sensor/i2c.dtsi @@ -166,6 +166,7 @@ test_i2c_isl29035: isl29035@14 { test_i2c_max30101: max30101@15 { compatible = "maxim,max30101"; reg = <0x15>; + acq-mode = "multi-led"; }; test_i2c_max44009: max44009@16 { From 4c540aa31139cdfddc2687d20da86065c74e0bb3 Mon Sep 17 00:00:00 2001 From: Logan Saint-Germain Date: Thu, 25 Sep 2025 13:56:22 +0200 Subject: [PATCH 2/4] drivers: sensor: max30101: Enhanced driver to support triggers The max30101 sensor driver doesn't support triggers. Add `.trigger_set` API and corresponding Kconfig and device tree parameters. Add `SENSOR_CHAN_AMBIENT_LIGHT` and `SENSOR_TRIG_OVERFLOW`. Signed-off-by: Logan Saint-Germain --- drivers/sensor/maxim/max30101/CMakeLists.txt | 2 + drivers/sensor/maxim/max30101/Kconfig | 50 +++- drivers/sensor/maxim/max30101/max30101.c | 16 +- drivers/sensor/maxim/max30101/max30101.h | 40 +++- .../sensor/maxim/max30101/max30101_trigger.c | 215 ++++++++++++++++++ drivers/sensor/sensor_shell.c | 3 + dts/bindings/sensor/maxim,max30101.yaml | 9 +- include/zephyr/drivers/sensor.h | 5 + samples/sensor/heart_rate/README.rst | 6 +- .../boards/nrf52840dk_nrf52840.overlay | 11 +- samples/sensor/heart_rate/sample.yaml | 17 +- samples/sensor/heart_rate/src/main.c | 38 +++- .../sensor_shell/pytest/test_sensor_shell.py | 6 +- 13 files changed, 396 insertions(+), 22 deletions(-) create mode 100644 drivers/sensor/maxim/max30101/max30101_trigger.c diff --git a/drivers/sensor/maxim/max30101/CMakeLists.txt b/drivers/sensor/maxim/max30101/CMakeLists.txt index 063cffa8123a2..9ba664e03b981 100644 --- a/drivers/sensor/maxim/max30101/CMakeLists.txt +++ b/drivers/sensor/maxim/max30101/CMakeLists.txt @@ -1,9 +1,11 @@ # Makefile - MAX30101 heart rate sensor # # Copyright (c) 2017, NXP +# Copyright (c) 2025, CATIE # # SPDX-License-Identifier: Apache-2.0 # zephyr_library() zephyr_library_sources(max30101.c) +zephyr_library_sources_ifdef(CONFIG_MAX30101_TRIGGER max30101_trigger.c) diff --git a/drivers/sensor/maxim/max30101/Kconfig b/drivers/sensor/maxim/max30101/Kconfig index 164ec7140d12b..ebbdcb4e9ce58 100644 --- a/drivers/sensor/maxim/max30101/Kconfig +++ b/drivers/sensor/maxim/max30101/Kconfig @@ -5,8 +5,56 @@ # # SPDX-License-Identifier: Apache-2.0 -config MAX30101 +menuconfig MAX30101 bool "MAX30101 Pulse Oximeter and Heart Rate Sensor" default y depends on DT_HAS_MAXIM_MAX30101_ENABLED select I2C if $(dt_compat_on_bus,$(DT_COMPAT_MAXIM_MAX30101),i2c) + +if MAX30101 + +choice MAX30101_TRIGGER_MODE + prompt "Trigger mode" + default MAX30101_TRIGGER_NONE + help + Specify the type of triggering to be used by the driver. + +config MAX30101_TRIGGER_NONE + bool "No trigger" + +config MAX30101_TRIGGER_GLOBAL_THREAD + bool "Use global thread" + depends on GPIO + depends on $(dt_compat_any_has_prop,$(DT_COMPAT_MAXIM_MAX30101),irq-gpios) + select MAX30101_TRIGGER + +config MAX30101_TRIGGER_OWN_THREAD + bool "Use own thread" + depends on GPIO + depends on $(dt_compat_any_has_prop,$(DT_COMPAT_MAXIM_MAX30101),irq-gpios) + select MAX30101_TRIGGER + +endchoice + +config MAX30101_TRIGGER + bool + +if MAX30101_TRIGGER + +config MAX30101_THREAD_PRIORITY + int "Thread priority" + depends on MAX30101_TRIGGER_OWN_THREAD + default 10 + help + Priority of thread used by the driver to handle interrupts. + +config MAX30101_THREAD_SIZE + int "Thread stack size" + depends on MAX30101_TRIGGER_OWN_THREAD + default 2048 + help + Stack size of thread used by the driver to handle interrupts. + +endif # MAX30101_TRIGGER + +endif # MAX30101 diff --git a/drivers/sensor/maxim/max30101/max30101.c b/drivers/sensor/maxim/max30101/max30101.c index b32adfdacb6d8..505eb4d951ce2 100644 --- a/drivers/sensor/maxim/max30101/max30101.c +++ b/drivers/sensor/maxim/max30101/max30101.c @@ -91,6 +91,9 @@ static int max30101_channel_get(const struct device *dev, static DEVICE_API(sensor, max30101_driver_api) = { .sample_fetch = max30101_sample_fetch, .channel_get = max30101_channel_get, +#if CONFIG_MAX30101_TRIGGER + .trigger_set = max30101_trigger_set, +#endif }; static int max30101_init(const struct device *dev) @@ -183,6 +186,13 @@ static int max30101_init(const struct device *dev) } } +#if CONFIG_MAX30101_TRIGGER + if (max30101_init_interrupts(dev)) { + LOG_ERR("Failed to initialize interrupts"); + return -EIO; + } +#endif + /* Initialize the channel map and active channel count */ data->num_channels = 0U; for (led_chan = 0U; led_chan < MAX30101_MAX_NUM_CHANNELS; led_chan++) { @@ -225,14 +235,16 @@ static int max30101_init(const struct device *dev) .fifo = (DT_INST_ENUM_IDX(n, smp_ave) << MAX30101_FIFO_CFG_SMP_AVE_SHIFT) | \ (DT_INST_PROP(n, fifo_rollover_en) \ << MAX30101_FIFO_CFG_ROLLOVER_EN_SHIFT) | \ - (DT_INST_PROP(n, fifo_a_full) << MAX30101_FIFO_CFG_FIFO_FULL_SHIFT), \ + (DT_INST_PROP(n, fifo_watermark) << MAX30101_FIFO_CFG_FIFO_FULL_SHIFT), \ .mode = DT_INST_ENUM_IDX(n, acq_mode), \ .spo2 = (DT_INST_ENUM_IDX(n, adc_rge) << MAX30101_SPO2_ADC_RGE_SHIFT) | \ (DT_INST_ENUM_IDX(n, smp_sr) << MAX30101_SPO2_SR_SHIFT) | \ (DT_INST_ENUM_IDX(n, led_pw) << MAX30101_SPO2_PW_SHIFT), \ .led_pa = DT_INST_PROP(n, led_pa), \ .slot = MAX30101_SLOT_CFG(n), \ - }; \ + IF_ENABLED(CONFIG_MAX30101_TRIGGER, \ + (.irq_gpio = GPIO_DT_SPEC_INST_GET_OR(n, irq_gpios, {0}),) \ + ) }; \ static struct max30101_data max30101_data_##n; \ SENSOR_DEVICE_DT_INST_DEFINE(n, max30101_init, NULL, &max30101_data_##n, \ &max30101_config_##n, POST_KERNEL, \ diff --git a/drivers/sensor/maxim/max30101/max30101.h b/drivers/sensor/maxim/max30101/max30101.h index 00aacdc8e7e64..286193f5c9862 100644 --- a/drivers/sensor/maxim/max30101/max30101.h +++ b/drivers/sensor/maxim/max30101/max30101.h @@ -32,8 +32,6 @@ #define MAX30101_REG_REV_ID 0xfe #define MAX30101_REG_PART_ID 0xff -#define MAX30101_INT_PPG_MASK (1 << 6) - #define MAX30101_FIFO_CFG_SMP_AVE_SHIFT 5 #define MAX30101_FIFO_CFG_ROLLOVER_EN_SHIFT 4 #define MAX30101_FIFO_CFG_FIFO_FULL_SHIFT 0 @@ -58,6 +56,27 @@ #define MAX30101_FIFO_DATA_BITS 18 #define MAX30101_FIFO_DATA_MASK ((1 << MAX30101_FIFO_DATA_BITS) - 1) +#if CONFIG_MAX30101_TRIGGER +#define MAX30101_SUPPORTED_INTERRUPTS 4 /* FIFO_FULL | PPG | ALC | TEMP */ + +enum max30101_callback_idx { + MAX30101_FULL_CB_INDEX = 0, + MAX30101_PPG_CB_INDEX = 1, + MAX30101_ALC_CB_INDEX = 2, + MAX30101_TEMP_CB_INDEX = 3, +}; + +#define MAX30101_INT_FULL_MASK BIT(7) /* FIFO full */ +#define MAX30101_INT_PPG_MASK BIT(6) /* PPG data ready */ +#define MAX30101_INT_ALC_OVF_MASK BIT(5) /* Ambient Light Cancellation overflow */ +#define MAX30101_INT_TEMP_MASK BIT(1) /* DIE Temperature data ready */ +#define MAX30101_STAT_POR_MASK BIT(0) /* Power on Reset status */ + +/* SPO2 channels RED/IR/GREEN */ +#define MAX30101_SENSOR_PPG_CHANNEL_MIN SENSOR_CHAN_IR +#define MAX30101_SENSOR_PPG_CHANNEL_MAX SENSOR_CHAN_GREEN +#endif + enum max30101_mode { MAX30101_MODE_HEART_RATE = 2, MAX30101_MODE_SPO2 = 3, @@ -102,10 +121,27 @@ struct max30101_config { uint8_t led_pa[MAX30101_MAX_NUM_CHANNELS]; uint8_t mode; uint8_t slot[4]; +#if CONFIG_MAX30101_TRIGGER + const struct gpio_dt_spec irq_gpio; +#endif }; struct max30101_data { uint32_t raw[MAX30101_MAX_NUM_CHANNELS]; uint8_t map[MAX30101_MAX_NUM_CHANNELS]; uint8_t num_channels; +#if CONFIG_MAX30101_TRIGGER + const struct device *dev; + struct gpio_callback gpio_cb; + sensor_trigger_handler_t trigger_handler[MAX30101_SUPPORTED_INTERRUPTS]; + const struct sensor_trigger *trigger[MAX30101_SUPPORTED_INTERRUPTS]; + struct k_work cb_work; +#endif }; + +#ifdef CONFIG_MAX30101_TRIGGER +int max30101_trigger_set(const struct device *dev, const struct sensor_trigger *trig, + sensor_trigger_handler_t handler); + +int max30101_init_interrupts(const struct device *dev); +#endif diff --git a/drivers/sensor/maxim/max30101/max30101_trigger.c b/drivers/sensor/maxim/max30101/max30101_trigger.c new file mode 100644 index 0000000000000..2a9e2e5b927d3 --- /dev/null +++ b/drivers/sensor/maxim/max30101/max30101_trigger.c @@ -0,0 +1,215 @@ +/* + * Copyright (c) 2025, CATIE + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include "max30101.h" + +LOG_MODULE_DECLARE(MAX30101, CONFIG_SENSOR_LOG_LEVEL); + +#if CONFIG_MAX30101_TRIGGER_OWN_THREAD +K_THREAD_STACK_DEFINE(max30101_workqueue_stack, CONFIG_MAX30101_THREAD_SIZE); +static struct k_work_q max30101_workqueue; + +static int max30101_workqueue_init(void) +{ + k_work_queue_init(&max30101_workqueue); + k_work_queue_start(&max30101_workqueue, max30101_workqueue_stack, + K_THREAD_STACK_SIZEOF(max30101_workqueue_stack), + CONFIG_MAX30101_THREAD_PRIORITY, NULL); + + return 0; +} + +/* The work-queue is shared across all instances, hence it is initialized separately */ +SYS_INIT(max30101_workqueue_init, POST_KERNEL, CONFIG_I2C_INIT_PRIORITY); +#endif /* CONFIG_MAX30101_TRIGGER_OWN_THREAD */ + +static void max30101_gpio_callback_handler(const struct device *p_port, struct gpio_callback *p_cb, + uint32_t pins) +{ + ARG_UNUSED(p_port); + ARG_UNUSED(pins); + + struct max30101_data *data = CONTAINER_OF(p_cb, struct max30101_data, gpio_cb); + + /* Using work queue to exit isr context */ +#if CONFIG_MAX30101_TRIGGER_OWN_THREAD + k_work_submit_to_queue(&max30101_workqueue, &data->cb_work); +#else + k_work_submit(&data->cb_work); +#endif /* CONFIG_MAX30101_TRIGGER_OWN_THREAD */ +} + +static void max30101_work_cb(struct k_work *p_work) +{ + struct max30101_data *data = CONTAINER_OF(p_work, struct max30101_data, cb_work); + const struct max30101_config *config = data->dev->config; + uint8_t reg; + + /* Read INTERRUPT status */ + if (i2c_reg_read_byte_dt(&config->i2c, MAX30101_REG_INT_STS1, ®)) { + LOG_ERR("Trigger worker I2C read STS1 FLAGS error"); + return; + } + + if ((reg & MAX30101_INT_FULL_MASK) && + (data->trigger_handler[MAX30101_FULL_CB_INDEX] != NULL)) { + data->trigger_handler[MAX30101_FULL_CB_INDEX]( + data->dev, data->trigger[MAX30101_FULL_CB_INDEX]); + } + if ((reg & MAX30101_INT_PPG_MASK) && + (data->trigger_handler[MAX30101_PPG_CB_INDEX] != NULL)) { + data->trigger_handler[MAX30101_PPG_CB_INDEX](data->dev, + data->trigger[MAX30101_PPG_CB_INDEX]); + } + if ((reg & MAX30101_INT_ALC_OVF_MASK) && + (data->trigger_handler[MAX30101_ALC_CB_INDEX] != NULL)) { + data->trigger_handler[MAX30101_ALC_CB_INDEX](data->dev, + data->trigger[MAX30101_ALC_CB_INDEX]); + } + +#if CONFIG_MAX30101_DIE_TEMPERATURE + /* Read INTERRUPT status */ + if (i2c_reg_read_byte_dt(&config->i2c, MAX30101_REG_INT_STS2, ®)) { + LOG_ERR("Trigger worker I2C read STS2 FLAGS error"); + return; + } + + if ((reg & MAX30101_INT_TEMP_MASK) && + (data->trigger_handler[MAX30101_TEMP_CB_INDEX] != NULL)) { + data->trigger_handler[MAX30101_TEMP_CB_INDEX]( + data->dev, data->trigger[MAX30101_TEMP_CB_INDEX]); + } +#endif /* CONFIG_MAX30101_DIE_TEMPERATURE */ +} + +int max30101_trigger_set(const struct device *dev, const struct sensor_trigger *trig, + sensor_trigger_handler_t handler) +{ + const struct max30101_config *config = dev->config; + struct max30101_data *data = dev->data; + uint8_t mask, index, enable = 0x00; + + switch (trig->type) { + case SENSOR_TRIG_FIFO_WATERMARK: + mask = MAX30101_INT_FULL_MASK; + index = MAX30101_FULL_CB_INDEX; + break; + + case SENSOR_TRIG_OVERFLOW: + if (trig->chan == SENSOR_CHAN_AMBIENT_LIGHT) { + mask = MAX30101_INT_ALC_OVF_MASK; + index = MAX30101_ALC_CB_INDEX; + } else { + LOG_ERR("Only SENSOR_CHAN_AMBIENT_LIGHT is supported for overflow trigger"); + return -EINVAL; + } + break; + + case SENSOR_TRIG_DATA_READY: + switch (trig->chan) { + case SENSOR_CHAN_DIE_TEMP: + mask = MAX30101_INT_TEMP_MASK; + index = MAX30101_TEMP_CB_INDEX; + break; + + case SENSOR_CHAN_LIGHT: + case SENSOR_CHAN_IR: + case SENSOR_CHAN_RED: + case SENSOR_CHAN_GREEN: + mask = MAX30101_INT_PPG_MASK; + index = MAX30101_PPG_CB_INDEX; + break; + + default: + LOG_ERR("Only SENSOR_CHAN_DIE_TEMP and SENSOR_CHAN_LIGHT/IR/RED/GREEN are " + "supported for data ready trigger"); + return -EINVAL; + } + break; + + default: + LOG_ERR("Unsupported trigger type"); + return -EINVAL; + } + + if (handler != NULL) { + enable = 0xFF; + } + + /* Write the Interrupt enable register */ + LOG_DBG("Writing Interrupt enable register: [0x%02X][0x%02X]", mask, enable); + if (i2c_reg_update_byte_dt(&config->i2c, MAX30101_REG_INT_EN1, mask, enable)) { + LOG_ERR("Could not set interrupt enable register"); + return -EIO; + } + +#if CONFIG_MAX30101_DIE_TEMPERATURE + if (i2c_reg_update_byte_dt(&config->i2c, MAX30101_REG_INT_EN2, mask, enable)) { + LOG_ERR("Could not set interrupt enable register"); + return -EIO; + } + + /* Start die temperature acquisition */ + if (i2c_reg_write_byte_dt(&config->i2c, MAX30101_REG_TEMP_CFG, 1)) { + LOG_ERR("Could not start die temperature acquisition"); + return -EIO; + } +#endif /* CONFIG_MAX30101_DIE_TEMPERATURE */ + + /* CLEAR ALL INTERRUPT STATUS */ + uint8_t int_status; + + if (i2c_reg_read_byte_dt(&config->i2c, MAX30101_REG_INT_STS1, &int_status)) { + LOG_ERR("Could not get interrupt STATUS register"); + return -EIO; + } + + if (!!enable) { + data->trigger_handler[index] = handler; + data->trigger[index] = trig; + } + LOG_DBG("TRIGGER %sset [%d][%d]", !!enable ? "" : "un", trig->type, trig->chan); + return 0; +} + +int max30101_init_interrupts(const struct device *dev) +{ + const struct max30101_config *config = dev->config; + struct max30101_data *data = dev->data; + + if (!gpio_is_ready_dt(&config->irq_gpio)) { + LOG_ERR("GPIO is not ready"); + return -ENODEV; + } + + if (gpio_pin_configure_dt(&config->irq_gpio, GPIO_INPUT)) { + LOG_ERR("Failed to configure GPIO"); + return -EIO; + } + + if (gpio_pin_interrupt_configure_dt(&config->irq_gpio, GPIO_INT_EDGE_TO_ACTIVE)) { + LOG_ERR("Failed to configure interrupt"); + return -EIO; + } + + gpio_init_callback(&data->gpio_cb, max30101_gpio_callback_handler, + BIT(config->irq_gpio.pin)); + + if (gpio_add_callback_dt(&config->irq_gpio, &data->gpio_cb)) { + LOG_ERR("Failed to add GPIO callback"); + return -EIO; + } + LOG_DBG("GPIO callback configured"); + + data->dev = dev; + memset(&(data->trigger_handler[0]), 0, sizeof(data->trigger_handler)); + memset(&(data->trigger[0]), 0, sizeof(data->trigger)); + k_work_init(&data->cb_work, max30101_work_cb); + + return 0; +} diff --git a/drivers/sensor/sensor_shell.c b/drivers/sensor/sensor_shell.c index 767b0da66b245..369c2c9e004ec 100644 --- a/drivers/sensor/sensor_shell.c +++ b/drivers/sensor/sensor_shell.c @@ -67,6 +67,7 @@ static const char *const sensor_channel_name[SENSOR_CHAN_COMMON_COUNT] = { [SENSOR_CHAN_PRESS] = "press", [SENSOR_CHAN_PROX] = "prox", [SENSOR_CHAN_HUMIDITY] = "humidity", + [SENSOR_CHAN_AMBIENT_LIGHT] = "ambient_light", [SENSOR_CHAN_LIGHT] = "light", [SENSOR_CHAN_IR] = "ir", [SENSOR_CHAN_RED] = "red", @@ -217,6 +218,8 @@ static const struct { TRIGGER_DATA_ENTRY(SENSOR_TRIG_STATIONARY, stationary, NULL), TRIGGER_DATA_ENTRY(SENSOR_TRIG_FIFO_WATERMARK, fifo_wm, NULL), TRIGGER_DATA_ENTRY(SENSOR_TRIG_FIFO_FULL, fifo_full, NULL), + TRIGGER_DATA_ENTRY(SENSOR_TRIG_TILT, tilt, NULL), + TRIGGER_DATA_ENTRY(SENSOR_TRIG_OVERFLOW, overflow, NULL), }; /** diff --git a/dts/bindings/sensor/maxim,max30101.yaml b/dts/bindings/sensor/maxim,max30101.yaml index 2cf02e2ccb1e8..d5611b5605e7b 100644 --- a/dts/bindings/sensor/maxim,max30101.yaml +++ b/dts/bindings/sensor/maxim,max30101.yaml @@ -12,6 +12,11 @@ compatible: "maxim,max30101" include: [sensor-device.yaml, i2c-device.yaml] properties: + irq-gpios: + type: phandle-array + description: | + Active low interrupt signal. It is an open drain signal, so it + require either hardware or software pull-up. fifo-rollover-en: type: boolean description: | @@ -20,7 +25,7 @@ properties: FIFO continues to fill with new data. If not set, then the FIFO is not updated until FIFO_DATA is read or the WRITE/READ pointer positions are changed. - fifo-a-full: + fifo-watermark: type: int default: 0 enum: @@ -41,7 +46,7 @@ properties: - 14 # Each 18 samples @ 14 empty space - 15 # Each 17 samples @ 15 empty space description: | - Configure the trigger for the FIFO_A_FULL interrupt (e.g. if set to 2, + Configure the trigger for the FIFO_WATERMARK interrupt (e.g. if set to 2, then the flag is set when the 30th word is written to the FIFO). Default set to 0, same as after Power Reset. Range: 0 - 15. acq-mode: diff --git a/include/zephyr/drivers/sensor.h b/include/zephyr/drivers/sensor.h index 5e0a7e7a33926..3857c46573c85 100644 --- a/include/zephyr/drivers/sensor.h +++ b/include/zephyr/drivers/sensor.h @@ -100,6 +100,8 @@ enum sensor_channel { SENSOR_CHAN_PROX, /** Humidity, in percent. */ SENSOR_CHAN_HUMIDITY, + /** Ambient illuminance in visible spectrum, in lux. */ + SENSOR_CHAN_AMBIENT_LIGHT, /** Illuminance in visible spectrum, in lux. */ SENSOR_CHAN_LIGHT, /** Illuminance in infra-red spectrum, in lux. */ @@ -282,6 +284,9 @@ enum sensor_trigger_type { /** Trigger fires when a tilt is detected. */ SENSOR_TRIG_TILT, + /** Trigger fires when data overflows. */ + SENSOR_TRIG_OVERFLOW, + /** * Number of all common sensor triggers. */ diff --git a/samples/sensor/heart_rate/README.rst b/samples/sensor/heart_rate/README.rst index ba1dde58e48f7..6b12f8185c391 100644 --- a/samples/sensor/heart_rate/README.rst +++ b/samples/sensor/heart_rate/README.rst @@ -2,13 +2,13 @@ :name: Heart Rate Sensor :relevant-api: sensor_interface - Get heart rate data from a sensor (polling mode). + Get heart rate data from a sensor (polling/interrupt mode). Overview ******** -A sensor application that demonstrates how to poll data from a heart rate -sensor. +A sensor application that demonstrates how to get data from a heart rate +sensor either by polling or interrupt. Requirements ************ diff --git a/samples/sensor/heart_rate/boards/nrf52840dk_nrf52840.overlay b/samples/sensor/heart_rate/boards/nrf52840dk_nrf52840.overlay index 8fc90f458b951..f79ce36cffca1 100644 --- a/samples/sensor/heart_rate/boards/nrf52840dk_nrf52840.overlay +++ b/samples/sensor/heart_rate/boards/nrf52840dk_nrf52840.overlay @@ -6,7 +6,7 @@ / { aliases { - heart-rate-sensor = &bh1790; + heart-rate-sensor = &max30101; }; }; @@ -21,4 +21,13 @@ compatible = "rohm,bh1790"; reg = <0x5b>; }; + + /* Example configuration of a MAX30101 device on an I2C bus. */ + max30101: max30101@57 { + compatible = "maxim,max30101"; + reg = <0x57>; + acq-mode = "multi-led"; + led-slot = <3 0 0 0>; + irq-gpios = <&gpio0 12 GPIO_ACTIVE_LOW>; + }; }; diff --git a/samples/sensor/heart_rate/sample.yaml b/samples/sensor/heart_rate/sample.yaml index 3d6c8899ced48..2b4fae812e94a 100644 --- a/samples/sensor/heart_rate/sample.yaml +++ b/samples/sensor/heart_rate/sample.yaml @@ -1,10 +1,23 @@ sample: name: Heart Rate Sensor Sample +common: + filter: dt_alias_exists("heart-rate-sensor") + harness: sensor tests: sample.sensor.heart_rate: - harness: sensor tags: sensors platform_allow: hexiwear/mk64f12 - depends_on: i2c + depends_on: + - i2c integration_platforms: - hexiwear/mk64f12 + sample.sensor.heart_rate_interrupt: + tags: sensors + platform_allow: nrf52840dk/nrf52840 + depends_on: + - i2c + integration_platforms: + - nrf52840dk/nrf52840 + extra_configs: + - CONFIG_GPIO=y + - CONFIG_MAX30101_TRIGGER_GLOBAL_THREAD=y diff --git a/samples/sensor/heart_rate/src/main.c b/samples/sensor/heart_rate/src/main.c index fe1e92fd60e06..55dcadb533b63 100644 --- a/samples/sensor/heart_rate/src/main.c +++ b/samples/sensor/heart_rate/src/main.c @@ -1,5 +1,6 @@ /* * Copyright (c) 2017, NXP + * Copyright (c) 2025, CATIE * * SPDX-License-Identifier: Apache-2.0 */ @@ -8,9 +9,30 @@ #include #include +#define MAX30101_SENSOR_CHANNEL SENSOR_CHAN_GREEN + +static void print_sample_fetch(const struct device *dev) +{ + static struct sensor_value green; + + sensor_sample_fetch(dev); + sensor_channel_get(dev, MAX30101_SENSOR_CHANNEL, &green); + + /* Print LED data*/ + printf("GREEN = %d\n", green.val1); +} + +#if CONFIG_MAX30101_TRIGGER +static struct sensor_trigger trig_drdy; + +void sensor_data_ready(const struct device *dev, const struct sensor_trigger *trigger) +{ + print_sample_fetch(dev); +} +#endif /* CONFIG_MAX30101_TRIGGER */ + int main(void) { - struct sensor_value green; const struct device *const dev = DEVICE_DT_GET(DT_ALIAS(heart_rate_sensor)); if (dev == NULL) { @@ -22,12 +44,16 @@ int main(void) return 0; } - while (1) { - sensor_sample_fetch(dev); - sensor_channel_get(dev, SENSOR_CHAN_GREEN, &green); +#if CONFIG_MAX30101_TRIGGER + trig_drdy.type = SENSOR_TRIG_DATA_READY; + trig_drdy.chan = MAX30101_SENSOR_CHANNEL; + sensor_trigger_set(dev, &trig_drdy, sensor_data_ready); +#endif /* CONFIG_MAX30101_TRIGGER */ - /* Print green LED data*/ - printf("GREEN=%d\n", green.val1); + while (1) { +#if !CONFIG_MAX30101_TRIGGER + print_sample_fetch(dev); +#endif /* !CONFIG_MAX30101_TRIGGER */ k_sleep(K_MSEC(20)); } diff --git a/samples/sensor/sensor_shell/pytest/test_sensor_shell.py b/samples/sensor/sensor_shell/pytest/test_sensor_shell.py index 366c3a9876236..8256524020edd 100644 --- a/samples/sensor/sensor_shell/pytest/test_sensor_shell.py +++ b/samples/sensor/sensor_shell/pytest/test_sensor_shell.py @@ -24,7 +24,7 @@ def test_sensor_shell_get(shell: Shell): # Channel should be the last one before 'all' (because 'all' doesn't print anything) so that the # for-loop in `parse_named_int()` will go through everything - for channel in range(59): + for channel in range(65): logger.info(f'channel {channel}') shell.wait_for_prompt() lines = shell.exec_command(f'sensor get sensor@0 {channel}') @@ -41,7 +41,7 @@ def test_sensor_shell_attr_get(shell: Shell): assert any(['sensor@0(channel=co2, attr=sampling_frequency)' in line for line in lines]), 'expected response not found' shell.wait_for_prompt() - lines = shell.exec_command('sensor attr_get sensor@1 55 3') + lines = shell.exec_command('sensor attr_get sensor@1 gauge_state_of_health 3') assert any(['sensor@1(channel=gauge_state_of_health, attr=slope_th)' in line for line in lines]), 'expected response not found' logger.info('response is valid') @@ -56,7 +56,7 @@ def test_sensor_shell_attr_set(shell: Shell): assert any([expected_line in line for line in lines]), 'expected response not found' shell.wait_for_prompt() - lines = shell.exec_command('sensor attr_set sensor@1 55 3 1') + lines = shell.exec_command('sensor attr_set sensor@1 gauge_state_of_health 3 1') expected_line = 'sensor@1 channel=gauge_state_of_health, attr=slope_th set to value=1' assert any([expected_line in line for line in lines]), 'expected response not found' From 4a2f408b653df30a317b5342e373883be2367ed5 Mon Sep 17 00:00:00 2001 From: Logan Saint-Germain Date: Tue, 30 Sep 2025 16:29:00 +0200 Subject: [PATCH 3/4] drivers: sensor: max30101: Enhanced sample_fetch to match datasheet The max30101 allows to configure time slots in samples acquisition. It is now supported by adding matrix mapping for the slot/fifo indexing. When a channel is present multiple times, the resulting sample from the `sensor_channel_get` is averaging each entry. Added Die temperature sample acquisition with `CONFIG_MAX30101_DIS_TEMPERATURE` Kconfig. Signed-off-by: Logan Saint-Germain --- drivers/sensor/maxim/max30101/Kconfig | 3 + drivers/sensor/maxim/max30101/max30101.c | 148 ++++++++++++------ drivers/sensor/maxim/max30101/max30101.h | 12 +- .../sensor/maxim/max30101/max30101_trigger.c | 5 + 4 files changed, 116 insertions(+), 52 deletions(-) diff --git a/drivers/sensor/maxim/max30101/Kconfig b/drivers/sensor/maxim/max30101/Kconfig index ebbdcb4e9ce58..59a808c1d45f7 100644 --- a/drivers/sensor/maxim/max30101/Kconfig +++ b/drivers/sensor/maxim/max30101/Kconfig @@ -13,6 +13,9 @@ menuconfig MAX30101 if MAX30101 +config MAX30101_DIE_TEMPERATURE + bool "Die temperature acquisition" + choice MAX30101_TRIGGER_MODE prompt "Trigger mode" default MAX30101_TRIGGER_NONE diff --git a/drivers/sensor/maxim/max30101/max30101.c b/drivers/sensor/maxim/max30101/max30101.c index 505eb4d951ce2..1cc64caefae87 100644 --- a/drivers/sensor/maxim/max30101/max30101.c +++ b/drivers/sensor/maxim/max30101/max30101.c @@ -24,7 +24,7 @@ static int max30101_sample_fetch(const struct device *dev, int i; /* Read all the active channels for one sample */ - num_bytes = data->num_channels * MAX30101_BYTES_PER_CHANNEL; + num_bytes = data->total_channels * MAX30101_BYTES_PER_CHANNEL; if (i2c_burst_read_dt(&config->i2c, MAX30101_REG_FIFO_DATA, buffer, num_bytes)) { LOG_ERR("Could not fetch sample"); @@ -36,12 +36,27 @@ static int max30101_sample_fetch(const struct device *dev, /* Each channel is 18-bits */ fifo_data = (buffer[i] << 16) | (buffer[i + 1] << 8) | (buffer[i + 2]); - fifo_data &= MAX30101_FIFO_DATA_MASK; + fifo_data = (fifo_data & MAX30101_FIFO_DATA_MASK) >> config->data_shift; /* Save the raw data */ data->raw[fifo_chan++] = fifo_data; } +#if CONFIG_MAX30101_DIE_TEMPERATURE + /* Read the die temperature */ + if (i2c_burst_read_dt(&config->i2c, MAX30101_REG_TINT, buffer, 2)) { + LOG_ERR("Could not fetch die temperature"); + return -EIO; + } + + /* Save the raw data */ + data->die_temp[0] = buffer[0]; + data->die_temp[1] = buffer[1]; + if (i2c_reg_write_byte_dt(&config->i2c, MAX30101_REG_TEMP_CFG, 1)) { + return -EIO; + } +#endif /* CONFIG_MAX30101_DIE_TEMPERATURE */ + return 0; } @@ -66,6 +81,13 @@ static int max30101_channel_get(const struct device *dev, led_chan = MAX30101_LED_CHANNEL_GREEN; break; +#if CONFIG_MAX30101_DIE_TEMPERATURE + case SENSOR_CHAN_DIE_TEMP: + val->val1 = data->die_temp[0]; + val->val2 = (1000000 * data->die_temp[1]) >> MAX30101_TEMP_FRAC_SHIFT; + return 0; +#endif /* CONFIG_MAX30101_DIE_TEMPERATURE */ + default: LOG_ERR("Unsupported sensor channel"); return -ENOTSUP; @@ -75,14 +97,19 @@ static int max30101_channel_get(const struct device *dev, * channel. If the fifo channel isn't valid, then the led channel * isn't active. */ - fifo_chan = data->map[led_chan]; - if (fifo_chan >= MAX30101_MAX_NUM_CHANNELS) { + fifo_chan = data->num_channels[led_chan]; + if (!fifo_chan) { LOG_ERR("Inactive sensor channel"); return -ENOTSUP; } + val->val1 = 0; + for (fifo_chan = 0; fifo_chan < data->num_channels[led_chan]; fifo_chan++) { + val->val1 += data->raw[data->map[led_chan][fifo_chan]]; + } + /* TODO: Scale the raw data to standard units */ - val->val1 = data->raw[fifo_chan]; + val->val1 /= data->num_channels[led_chan]; val->val2 = 0; return 0; @@ -96,45 +123,9 @@ static DEVICE_API(sensor, max30101_driver_api) = { #endif }; -static int max30101_init(const struct device *dev) +static int max30101_configure(const struct device *dev) { const struct max30101_config *config = dev->config; - struct max30101_data *data = dev->data; - uint8_t part_id; - uint8_t mode_cfg; - uint32_t led_chan; - - if (!device_is_ready(config->i2c.bus)) { - LOG_ERR("Bus device is not ready"); - return -ENODEV; - } - - /* Check the part id to make sure this is MAX30101 */ - if (i2c_reg_read_byte_dt(&config->i2c, MAX30101_REG_PART_ID, - &part_id)) { - LOG_ERR("Could not get Part ID"); - return -EIO; - } - if (part_id != MAX30101_PART_ID) { - LOG_ERR("Got Part ID 0x%02x, expected 0x%02x", - part_id, MAX30101_PART_ID); - return -EIO; - } - - /* Reset the sensor */ - if (i2c_reg_write_byte_dt(&config->i2c, MAX30101_REG_MODE_CFG, - MAX30101_MODE_CFG_RESET_MASK)) { - return -EIO; - } - - /* Wait for reset to be cleared */ - do { - if (i2c_reg_read_byte_dt(&config->i2c, MAX30101_REG_MODE_CFG, - &mode_cfg)) { - LOG_ERR("Could read mode cfg after reset"); - return -EIO; - } - } while (mode_cfg & MAX30101_MODE_CFG_RESET_MASK); /* Write the FIFO configuration register */ if (i2c_reg_write_byte_dt(&config->i2c, MAX30101_REG_FIFO_CFG, @@ -186,6 +177,12 @@ static int max30101_init(const struct device *dev) } } +#if CONFIG_MAX30101_DIE_TEMPERATURE + if (i2c_reg_write_byte_dt(&config->i2c, MAX30101_REG_TEMP_CFG, 1)) { + return -EIO; + } +#endif /* CONFIG_MAX30101_DIE_TEMPERATURE */ + #if CONFIG_MAX30101_TRIGGER if (max30101_init_interrupts(dev)) { LOG_ERR("Failed to initialize interrupts"); @@ -193,10 +190,48 @@ static int max30101_init(const struct device *dev) } #endif - /* Initialize the channel map and active channel count */ - data->num_channels = 0U; - for (led_chan = 0U; led_chan < MAX30101_MAX_NUM_CHANNELS; led_chan++) { - data->map[led_chan] = MAX30101_MAX_NUM_CHANNELS; + return 0; +} + +static int max30101_init(const struct device *dev) +{ + const struct max30101_config *config = dev->config; + struct max30101_data *data = dev->data; + uint8_t part_id; + uint8_t mode_cfg; + uint32_t led_chan; + + if (!device_is_ready(config->i2c.bus)) { + LOG_ERR("Bus device is not ready"); + return -ENODEV; + } + + /* Check the part id to make sure this is MAX30101 */ + if (i2c_reg_read_byte_dt(&config->i2c, MAX30101_REG_PART_ID, &part_id)) { + LOG_ERR("Could not get Part ID"); + return -EIO; + } + if (part_id != MAX30101_PART_ID) { + LOG_ERR("Got Part ID 0x%02x, expected 0x%02x", part_id, MAX30101_PART_ID); + return -EIO; + } + + /* Reset the sensor */ + if (i2c_reg_write_byte_dt(&config->i2c, MAX30101_REG_MODE_CFG, + MAX30101_MODE_CFG_RESET_MASK)) { + return -EIO; + } + + /* Wait for reset to be cleared */ + do { + if (i2c_reg_read_byte_dt(&config->i2c, MAX30101_REG_MODE_CFG, &mode_cfg)) { + LOG_ERR("Could read mode cfg after reset"); + return -EIO; + } + } while (mode_cfg & MAX30101_MODE_CFG_RESET_MASK); + + if (max30101_configure(dev)) { + return -EIO; } /* Count the number of active channels and build a map that translates @@ -204,10 +239,18 @@ static int max30101_init(const struct device *dev) */ for (int fifo_chan = 0; fifo_chan < MAX30101_MAX_NUM_CHANNELS; fifo_chan++) { led_chan = (config->slot[fifo_chan] & MAX30101_SLOT_LED_MASK) - 1; - if (led_chan < MAX30101_MAX_NUM_CHANNELS) { - data->map[led_chan] = fifo_chan; - data->num_channels++; + if (led_chan >= MAX30101_MAX_NUM_CHANNELS) { + continue; + } + + for (int i = 0; i < MAX30101_MAX_NUM_CHANNELS; i++) { + if (data->map[led_chan][i] == MAX30101_MAX_NUM_CHANNELS) { + data->map[led_chan][i] = fifo_chan; + data->num_channels[led_chan]++; + break; + } } + data->total_channels++; } return 0; @@ -242,10 +285,15 @@ static int max30101_init(const struct device *dev) (DT_INST_ENUM_IDX(n, led_pw) << MAX30101_SPO2_PW_SHIFT), \ .led_pa = DT_INST_PROP(n, led_pa), \ .slot = MAX30101_SLOT_CFG(n), \ + .data_shift = MAX30101_FIFO_DATA_MAX_SHIFT - DT_INST_ENUM_IDX(n, led_pw), \ IF_ENABLED(CONFIG_MAX30101_TRIGGER, \ (.irq_gpio = GPIO_DT_SPEC_INST_GET_OR(n, irq_gpios, {0}),) \ ) }; \ - static struct max30101_data max30101_data_##n; \ + static struct max30101_data max30101_data_##n = { \ + .map = {{3, 3, 3}, {3, 3, 3}, {3, 3, 3}}, \ + .num_channels = {0, 0, 0}, \ + .total_channels = 0, \ + }; \ SENSOR_DEVICE_DT_INST_DEFINE(n, max30101_init, NULL, &max30101_data_##n, \ &max30101_config_##n, POST_KERNEL, \ CONFIG_SENSOR_INIT_PRIORITY, &max30101_driver_api); diff --git a/drivers/sensor/maxim/max30101/max30101.h b/drivers/sensor/maxim/max30101/max30101.h index 286193f5c9862..391a36721ac07 100644 --- a/drivers/sensor/maxim/max30101/max30101.h +++ b/drivers/sensor/maxim/max30101/max30101.h @@ -55,6 +55,9 @@ #define MAX30101_FIFO_DATA_BITS 18 #define MAX30101_FIFO_DATA_MASK ((1 << MAX30101_FIFO_DATA_BITS) - 1) +#define MAX30101_FIFO_DATA_MAX_SHIFT 3 + +#define MAX30101_TEMP_FRAC_SHIFT 4 #if CONFIG_MAX30101_TRIGGER #define MAX30101_SUPPORTED_INTERRUPTS 4 /* FIFO_FULL | PPG | ALC | TEMP */ @@ -121,6 +124,7 @@ struct max30101_config { uint8_t led_pa[MAX30101_MAX_NUM_CHANNELS]; uint8_t mode; uint8_t slot[4]; + uint8_t data_shift; #if CONFIG_MAX30101_TRIGGER const struct gpio_dt_spec irq_gpio; #endif @@ -128,8 +132,12 @@ struct max30101_config { struct max30101_data { uint32_t raw[MAX30101_MAX_NUM_CHANNELS]; - uint8_t map[MAX30101_MAX_NUM_CHANNELS]; - uint8_t num_channels; + uint8_t map[MAX30101_MAX_NUM_CHANNELS][MAX30101_MAX_NUM_CHANNELS]; + uint8_t num_channels[MAX30101_MAX_NUM_CHANNELS]; + uint8_t total_channels; +#if CONFIG_MAX30101_DIE_TEMPERATURE + uint8_t die_temp[2]; +#endif /* CONFIG_MAX30101_DIE_TEMPERATURE */ #if CONFIG_MAX30101_TRIGGER const struct device *dev; struct gpio_callback gpio_cb; diff --git a/drivers/sensor/maxim/max30101/max30101_trigger.c b/drivers/sensor/maxim/max30101/max30101_trigger.c index 2a9e2e5b927d3..93a10eb787ed1 100644 --- a/drivers/sensor/maxim/max30101/max30101_trigger.c +++ b/drivers/sensor/maxim/max30101/max30101_trigger.c @@ -113,8 +113,13 @@ int max30101_trigger_set(const struct device *dev, const struct sensor_trigger * case SENSOR_TRIG_DATA_READY: switch (trig->chan) { case SENSOR_CHAN_DIE_TEMP: +#if CONFIG_MAX30101_DIE_TEMPERATURE mask = MAX30101_INT_TEMP_MASK; index = MAX30101_TEMP_CB_INDEX; +#else + LOG_ERR("SENSOR_CHAN_DIE_TEMP needs CONFIG_MAX30101_DIE_TEMPERATURE"); + return -EINVAL; +#endif /* CONFIG_MAX30101_DIE_TEMPERATURE */ break; case SENSOR_CHAN_LIGHT: From 52a93b36857bb940815581bfc176e929c1803c61 Mon Sep 17 00:00:00 2001 From: Logan Saint-Germain Date: Fri, 3 Oct 2025 19:17:50 +0200 Subject: [PATCH 4/4] samples: sensor: sensor_shell: Added channels count for sensor_shell tests Use of the `sensor get` help. No channel provided allows the sensor_shell to iterate through every channels. Getting the last channel gives the last channel index and therefor the channel count. Provide futur proofing for new channels. Signed-off-by: Logan Saint-Germain --- .../sensor/sensor_shell/pytest/test_sensor_shell.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/samples/sensor/sensor_shell/pytest/test_sensor_shell.py b/samples/sensor/sensor_shell/pytest/test_sensor_shell.py index 8256524020edd..9fad704d28a15 100644 --- a/samples/sensor/sensor_shell/pytest/test_sensor_shell.py +++ b/samples/sensor/sensor_shell/pytest/test_sensor_shell.py @@ -20,11 +20,14 @@ def test_sensor_shell_info(shell: Shell): def test_sensor_shell_get(shell: Shell): - logger.info('send "sensor get" command') + logger.info('get "sensor get" command count') + + lines = shell.exec_command('sensor get sensor@0') + channel_count = int(lines[-2].split("=")[1].split("(")[0]) + 1 + logger.info(f'channel count: [{channel_count}]') - # Channel should be the last one before 'all' (because 'all' doesn't print anything) so that the - # for-loop in `parse_named_int()` will go through everything - for channel in range(65): + logger.info('send "sensor get" command') + for channel in range(channel_count): logger.info(f'channel {channel}') shell.wait_for_prompt() lines = shell.exec_command(f'sensor get sensor@0 {channel}')