* [PATCH 0/1] iio: health: add MAX86150 ECG and PPG biosensor driver
@ 2026-06-23 14:01 Md Shofiqul Islam
2026-06-23 14:01 ` [PATCH 1/1] " Md Shofiqul Islam
2026-06-23 15:55 ` [PATCH v2 0/3] " Md Shofiqul Islam
0 siblings, 2 replies; 9+ messages in thread
From: Md Shofiqul Islam @ 2026-06-23 14:01 UTC (permalink / raw)
To: linux-iio
Cc: jic23, dlechner, nuno.sa, robh, krzk+dt, devicetree,
Md Shofiqul Islam
The MAX86150 (Maxim/Analog Devices) combines two PPG optical channels
(Red/IR LED) and one ECG biopotential channel in a single I2C device.
Data is produced at up to 3200 Hz and buffered in a 32-entry hardware
FIFO with a configurable almost-full interrupt.
This patch adds an IIO driver that exposes three channels:
in_intensity_red_raw -- PPG Red LED, 19-bit unsigned
in_intensity_ir_raw -- PPG IR LED, 19-bit unsigned
in_voltage0_raw -- ECG biopotential, 18-bit signed
Key implementation choices:
- regmap_noinc_read() for FIFO access: reads 9 bytes (3 slots x 3
bytes) from the streaming FIFO_DATA register in a single I2C
burst without address auto-increment.
- IIO hardware trigger backed by the device interrupt (active-low,
FIFO almost-full). When an interrupt GPIO is provided in the device
tree, a trigger is registered and wired to the IIO triggered buffer
path for continuous low-overhead capture.
- All resource management uses devm_* APIs.
Known limitations to be addressed in v2:
- IIO_CHAN_INFO_SCALE not yet implemented.
- Runtime PM (SYS_CTRL SHDN bit) not yet implemented.
- Not yet tested on physical hardware.
Md Shofiqul Islam (1):
iio: health: add MAX86150 ECG and PPG biosensor driver
.../bindings/iio/health/maxim,max86150.yaml | 67 +++
drivers/iio/health/Kconfig | 23 +
drivers/iio/health/Makefile | 1 +
drivers/iio/health/max86150.c | 512 ++++++++++++++++++
4 files changed, 603 insertions(+)
create mode 100755 Documentation/devicetree/bindings/iio/health/maxim,max86150.yaml
create mode 100755 drivers/iio/health/max86150.c
--
2.51.1
^ permalink raw reply [flat|nested] 9+ messages in thread
* [PATCH 1/1] iio: health: add MAX86150 ECG and PPG biosensor driver
2026-06-23 14:01 [PATCH 0/1] iio: health: add MAX86150 ECG and PPG biosensor driver Md Shofiqul Islam
@ 2026-06-23 14:01 ` Md Shofiqul Islam
2026-06-23 14:14 ` sashiko-bot
` (2 more replies)
2026-06-23 15:55 ` [PATCH v2 0/3] " Md Shofiqul Islam
1 sibling, 3 replies; 9+ messages in thread
From: Md Shofiqul Islam @ 2026-06-23 14:01 UTC (permalink / raw)
To: linux-iio
Cc: jic23, dlechner, nuno.sa, robh, krzk+dt, devicetree,
Md Shofiqul Islam
The MAX86150 (Maxim/Analog Devices) integrates two PPG optical channels
(Red/IR LED) and one ECG biopotential channel in a single I2C device.
Data is captured via a 32-entry hardware FIFO with a configurable
almost-full interrupt.
The driver exposes three IIO channels:
- in_intensity_red_raw (PPG Red, 19-bit unsigned)
- in_intensity_ir_raw (PPG IR, 19-bit unsigned)
- in_voltage0_raw (ECG, 18-bit signed)
A hardware trigger is registered when an interrupt GPIO is provided in
the device tree, enabling the IIO triggered buffer path for continuous
low-overhead capture.
FIFO reads use regmap_noinc_read() to burst-read 9 bytes (3 slots x
3 bytes) from the streaming FIFO_DATA register in a single I2C
transaction.
Signed-off-by: Md Shofiqul Islam <shofiqtest@gmail.com>
---
.../bindings/iio/health/maxim,max86150.yaml | 67 +++
drivers/iio/health/Kconfig | 23 +
drivers/iio/health/Makefile | 1 +
drivers/iio/health/max86150.c | 512 ++++++++++++++++++
4 files changed, 603 insertions(+)
create mode 100755 Documentation/devicetree/bindings/iio/health/maxim,max86150.yaml
create mode 100755 drivers/iio/health/max86150.c
diff --git a/Documentation/devicetree/bindings/iio/health/maxim,max86150.yaml b/Documentation/devicetree/bindings/iio/health/maxim,max86150.yaml
new file mode 100755
index 000000000000..1bf10fd1a3d2
--- /dev/null
+++ b/Documentation/devicetree/bindings/iio/health/maxim,max86150.yaml
@@ -0,0 +1,67 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/iio/health/maxim,max86150.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Maxim MAX86150 ECG and PPG Biosensor
+
+maintainers:
+ - Md Shofiqul Islam <shofiqtest@gmail.com>
+
+description: |
+ The MAX86150 is an integrated biosensor SoC that combines:
+ - Two PPG (photoplethysmography) channels: Red LED and IR LED,
+ for heart rate and blood-oxygen saturation (SpO2) measurement.
+ - One ECG (electrocardiogram) channel for biopotential recording.
+
+ The device communicates over I2C at up to 400 kHz and raises an
+ active-low interrupt when the 32-entry hardware FIFO reaches its
+ configurable almost-full threshold.
+
+ Datasheet:
+ https://www.analog.com/media/en/technical-documentation/data-sheets/MAX86150.pdf
+
+properties:
+ compatible:
+ const: maxim,max86150
+
+ reg:
+ maxItems: 1
+ description: I2C device address, always 0x5E.
+
+ interrupts:
+ maxItems: 1
+ description: |
+ Active-low interrupt line. Asserted when the FIFO almost-full
+ threshold is reached or when a new PPG sample is ready.
+
+ vdd-supply:
+ description: Digital core supply, 1.71 V to 1.89 V.
+
+ leds-supply:
+ description: LED anode supply, 3.0 V to 5.5 V.
+
+required:
+ - compatible
+ - reg
+
+additionalProperties: false
+
+examples:
+ - |
+ #include <dt-bindings/interrupt-controller/irq.h>
+
+ i2c {
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ biosensor@5e {
+ compatible = "maxim,max86150";
+ reg = <0x5e>;
+ interrupt-parent = <&gpio1>;
+ interrupts = <5 IRQ_TYPE_EDGE_FALLING>;
+ vdd-supply = <&vdd_1v8>;
+ leds-supply = <&vdd_3v3>;
+ };
+ };
diff --git a/drivers/iio/health/Kconfig b/drivers/iio/health/Kconfig
index a89f3abf11f4..6496cf55290c 100644
--- a/drivers/iio/health/Kconfig
+++ b/drivers/iio/health/Kconfig
@@ -13,6 +13,7 @@ config AFE4403
depends on SPI_MASTER
select REGMAP_SPI
select IIO_BUFFER
+ select IIO_TRIGGER
select IIO_TRIGGERED_BUFFER
help
Say yes to choose the Texas Instruments AFE4403
@@ -26,6 +27,7 @@ config AFE4404
depends on I2C
select REGMAP_I2C
select IIO_BUFFER
+ select IIO_TRIGGER
select IIO_TRIGGERED_BUFFER
help
Say yes to choose the Texas Instruments AFE4404
@@ -39,6 +41,7 @@ config MAX30100
depends on I2C
select REGMAP_I2C
select IIO_BUFFER
+ select IIO_TRIGGER
select IIO_KFIFO_BUF
help
Say Y here to build I2C interface support for the Maxim
@@ -52,6 +55,7 @@ config MAX30102
depends on I2C
select REGMAP_I2C
select IIO_BUFFER
+ select IIO_TRIGGER
select IIO_KFIFO_BUF
help
Say Y here to build I2C interface support for the Maxim
@@ -62,4 +66,23 @@ config MAX30102
endmenu
+
+config MAX86150
+ tristate "MAX86150 ECG and PPG biosensor"
+ depends on I2C
+ select IIO_BUFFER
+ select IIO_TRIGGER
+ select IIO_TRIGGERED_BUFFER
+ select REGMAP_I2C
+ help
+ Say Y here to enable support for the Maxim MAX86150 combined
+ ECG and photoplethysmography (PPG) biosensor.
+
+ The driver exposes three IIO channels: two PPG optical channels
+ (Red and IR LED) for heart rate and SpO2 monitoring, and one
+ ECG channel for biopotential recording.
+
+ This driver can also be built as a module. If so, the module
+ will be called max86150.
+
endmenu
diff --git a/drivers/iio/health/Makefile b/drivers/iio/health/Makefile
index 910817112258..04fc73c58444 100644
--- a/drivers/iio/health/Makefile
+++ b/drivers/iio/health/Makefile
@@ -9,3 +9,4 @@ obj-$(CONFIG_AFE4403) += afe4403.o
obj-$(CONFIG_AFE4404) += afe4404.o
obj-$(CONFIG_MAX30100) += max30100.o
obj-$(CONFIG_MAX30102) += max30102.o
+obj-$(CONFIG_MAX86150) += max86150.o
diff --git a/drivers/iio/health/max86150.c b/drivers/iio/health/max86150.c
new file mode 100755
index 000000000000..3cde4af6650b
--- /dev/null
+++ b/drivers/iio/health/max86150.c
@@ -0,0 +1,512 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * MAX86150 combined ECG and PPG biosensor driver
+ *
+ * Copyright (C) 2026 Md Shofiqul Islam <shofiqtest@gmail.com>
+ *
+ * The MAX86150 integrates two PPG optical channels (Red/IR LED) and one
+ * ECG biopotential channel in a single I2C device. Data is captured
+ * through a 32-entry hardware FIFO with a configurable almost-full
+ * interrupt, making it well-suited for continuous monitoring with a
+ * low-power host.
+ *
+ * Datasheet:
+ * https://www.analog.com/media/en/technical-documentation/data-sheets/MAX86150.pdf
+ */
+
+#include <linux/bitfield.h>
+#include <linux/delay.h>
+#include <linux/i2c.h>
+#include <linux/iio/buffer.h>
+#include <linux/iio/iio.h>
+#include <linux/iio/trigger.h>
+#include <linux/iio/trigger_consumer.h>
+#include <linux/iio/triggered_buffer.h>
+#include <linux/module.h>
+#include <linux/regmap.h>
+
+/* Register addresses */
+#define MAX86150_REG_INT_STATUS1 0x00
+#define MAX86150_REG_INT_STATUS2 0x01
+#define MAX86150_REG_INT_ENABLE1 0x02
+#define MAX86150_REG_INT_ENABLE2 0x03
+#define MAX86150_REG_FIFO_WR_PTR 0x04
+#define MAX86150_REG_OVF_COUNTER 0x05
+#define MAX86150_REG_FIFO_RD_PTR 0x06
+#define MAX86150_REG_FIFO_DATA 0x07
+#define MAX86150_REG_FIFO_CONFIG 0x08
+#define MAX86150_REG_FIFO_DCTRL1 0x09 /* FD1[3:0] FD2[7:4] */
+#define MAX86150_REG_FIFO_DCTRL2 0x0A /* FD3[3:0] FD4[7:4] */
+#define MAX86150_REG_SYS_CTRL 0x0D
+#define MAX86150_REG_PPG_CONFIG1 0x10
+#define MAX86150_REG_PPG_CONFIG2 0x11
+#define MAX86150_REG_LED1_PA 0x14 /* Red LED pulse amplitude */
+#define MAX86150_REG_LED2_PA 0x15 /* IR LED pulse amplitude */
+#define MAX86150_REG_ECG_CONFIG1 0x3C
+#define MAX86150_REG_ECG_CONFIG3 0x3E
+#define MAX86150_REG_PART_ID 0xFF
+
+/* Field masks */
+#define MAX86150_PART_ID_VAL 0x1E
+
+/* INT_STATUS1 / INT_ENABLE1 */
+#define MAX86150_INT_A_FULL BIT(7) /* FIFO almost full */
+#define MAX86150_INT_PPG_RDY BIT(6) /* new PPG sample ready */
+
+/* SYS_CTRL */
+#define MAX86150_SYS_SHDN BIT(1)
+#define MAX86150_SYS_RESET BIT(0)
+
+/* FIFO_CONFIG */
+#define MAX86150_FIFO_SMP_AVE GENMASK(7, 5)
+#define MAX86150_FIFO_ROLLOVER_EN BIT(4)
+#define MAX86150_FIFO_A_FULL GENMASK(3, 0)
+
+/* FIFO slot data-type codes */
+#define MAX86150_FD_NONE 0x0
+#define MAX86150_FD_LED1 0x1 /* Red PPG */
+#define MAX86150_FD_LED2 0x2 /* IR PPG */
+#define MAX86150_FD_ECG 0x9
+#define MAX86150_FIFO_FD1 GENMASK(3, 0)
+#define MAX86150_FIFO_FD2 GENMASK(7, 4)
+#define MAX86150_FIFO_FD3 GENMASK(3, 0)
+#define MAX86150_FIFO_FD4 GENMASK(7, 4)
+
+/* PPG_CONFIG1 */
+#define MAX86150_PPG_ADC_RGE GENMASK(7, 6)
+#define MAX86150_PPG_SR GENMASK(5, 1)
+
+/* Geometry */
+#define MAX86150_FIFO_DEPTH 32
+#define MAX86150_BYTES_PER_SLOT 3 /* 24-bit word per slot */
+#define MAX86150_NUM_SLOTS 3 /* Red, IR, ECG */
+#define MAX86150_SAMPLE_BYTES (MAX86150_NUM_SLOTS * MAX86150_BYTES_PER_SLOT)
+
+/* Default hardware configuration */
+#define MAX86150_LED_PA_DEFAULT 0x3F /* ~50 mA */
+#define MAX86150_PPG_SR_100HZ 4 /* PPG_SR field value for 100 Hz */
+#define MAX86150_PPG_ADC_RGE_16384 2 /* 16384 nA full scale */
+/* Fire A_FULL when 17 slots remain (32 - 15 = 17 samples in FIFO) */
+#define MAX86150_FIFO_A_FULL_VAL 15
+
+/* Scan element indices */
+enum max86150_scan_idx {
+ MAX86150_IDX_PPG_RED = 0,
+ MAX86150_IDX_PPG_IR = 1,
+ MAX86150_IDX_ECG = 2,
+ MAX86150_IDX_TS,
+};
+
+/**
+ * struct max86150_data - driver private state
+ * @regmap: register map for this device
+ * @dev: parent device (for dev_err logging)
+ * @trig: IIO hardware trigger backed by the device interrupt line
+ * @buf: DMA-safe capture buffer: 3 x s32 samples + s64 timestamp.
+ * __aligned(8) satisfies iio_push_to_buffers_with_timestamp().
+ */
+struct max86150_data {
+ struct regmap *regmap;
+ struct device *dev;
+ struct iio_trigger *trig;
+ s32 buf[4] __aligned(8);
+};
+
+/* IIO channel specification */
+
+static const struct iio_chan_spec max86150_channels[] = {
+ {
+ /* PPG Red LED - optical intensity, 19-bit unsigned */
+ .type = IIO_INTENSITY,
+ .modified = 1,
+ .channel2 = IIO_MOD_LIGHT_RED,
+ .info_mask_separate = BIT(IIO_CHAN_INFO_RAW) |
+ BIT(IIO_CHAN_INFO_SCALE),
+ .scan_index = MAX86150_IDX_PPG_RED,
+ .scan_type = {
+ .sign = 'u',
+ .realbits = 19,
+ .storagebits = 32,
+ .endianness = IIO_LE,
+ },
+ },
+ {
+ /* PPG IR LED - optical intensity, 19-bit unsigned */
+ .type = IIO_INTENSITY,
+ .modified = 1,
+ .channel2 = IIO_MOD_LIGHT_IR,
+ .info_mask_separate = BIT(IIO_CHAN_INFO_RAW) |
+ BIT(IIO_CHAN_INFO_SCALE),
+ .scan_index = MAX86150_IDX_PPG_IR,
+ .scan_type = {
+ .sign = 'u',
+ .realbits = 19,
+ .storagebits = 32,
+ .endianness = IIO_LE,
+ },
+ },
+ {
+ /* ECG biopotential - voltage, 18-bit signed two's complement */
+ .type = IIO_VOLTAGE,
+ .channel = 0,
+ .info_mask_separate = BIT(IIO_CHAN_INFO_RAW) |
+ BIT(IIO_CHAN_INFO_SCALE),
+ .scan_index = MAX86150_IDX_ECG,
+ .scan_type = {
+ .sign = 's',
+ .realbits = 18,
+ .storagebits = 32,
+ .endianness = IIO_LE,
+ },
+ },
+ IIO_CHAN_SOFT_TIMESTAMP(MAX86150_IDX_TS),
+};
+
+/* Regmap configuration */
+
+static const struct regmap_config max86150_regmap_config = {
+ .reg_bits = 8,
+ .val_bits = 8,
+ .max_register = MAX86150_REG_PART_ID,
+};
+
+/* FIFO helper */
+
+/**
+ * max86150_read_one_sample - burst-read one complete 3-slot FIFO entry
+ * @data: driver state
+ * @ppg_red: out - 19-bit PPG Red ADC value (unsigned)
+ * @ppg_ir: out - 19-bit PPG IR ADC value (unsigned)
+ * @ecg: out - 18-bit ECG ADC value (sign-extended to s32)
+ *
+ * Each FIFO entry is 9 bytes (3 slots x 3 bytes). FIFO_DATA is a
+ * streaming register - the address does not auto-increment on each
+ * byte, so regmap_noinc_read() is used instead of regmap_bulk_read().
+ *
+ * Byte layout in the 24-bit FIFO word (MSB first):
+ * PPG 19-bit unsigned: bits [18:0], top 5 bits are always zero
+ * ECG 18-bit signed: bits [17:0], top 6 bits are sign extension
+ */
+static int max86150_read_one_sample(struct max86150_data *data,
+ u32 *ppg_red, u32 *ppg_ir, s32 *ecg)
+{
+ u8 buf[MAX86150_SAMPLE_BYTES]; /* 9 bytes */
+ int ret;
+
+ ret = regmap_noinc_read(data->regmap, MAX86150_REG_FIFO_DATA,
+ buf, sizeof(buf));
+ if (ret)
+ return ret;
+
+ /* Bytes [0..2]: PPG Red - 19-bit value in bits [18:0] */
+ *ppg_red = (u32)(buf[0] & 0x07) << 16 | (u32)buf[1] << 8 | buf[2];
+
+ /* Bytes [3..5]: PPG IR - same format */
+ *ppg_ir = (u32)(buf[3] & 0x07) << 16 | (u32)buf[4] << 8 | buf[5];
+
+ /* Bytes [6..8]: ECG - 18-bit signed, sign-extend to s32 */
+ *ecg = sign_extend32((u32)(buf[6] & 0x03) << 16 |
+ (u32)buf[7] << 8 | buf[8], 17);
+
+ return 0;
+}
+
+/* IIO read_raw */
+
+static int max86150_read_raw(struct iio_dev *indio_dev,
+ struct iio_chan_spec const *chan,
+ int *val, int *val2, long mask)
+{
+ struct max86150_data *data = iio_priv(indio_dev);
+ u32 ppg_red, ppg_ir;
+ s32 ecg;
+ int ret;
+
+ switch (mask) {
+ case IIO_CHAN_INFO_RAW:
+ /*
+ * Single-shot path: clear the FIFO so we know we are reading
+ * a sample that arrived after this call, not stale data.
+ */
+ ret = regmap_write(data->regmap, MAX86150_REG_FIFO_WR_PTR, 0);
+ if (ret)
+ return ret;
+ ret = regmap_write(data->regmap, MAX86150_REG_OVF_COUNTER, 0);
+ if (ret)
+ return ret;
+ ret = regmap_write(data->regmap, MAX86150_REG_FIFO_RD_PTR, 0);
+ if (ret)
+ return ret;
+
+ /* Wait for one complete sample period at 100 Hz (<= 10 ms) */
+ usleep_range(11000, 13000);
+
+ ret = max86150_read_one_sample(data, &ppg_red, &ppg_ir, &ecg);
+ if (ret)
+ return ret;
+
+ switch (chan->scan_index) {
+ case MAX86150_IDX_PPG_RED:
+ *val = ppg_red;
+ break;
+ case MAX86150_IDX_PPG_IR:
+ *val = ppg_ir;
+ break;
+ case MAX86150_IDX_ECG:
+ *val = ecg;
+ break;
+ default:
+ return -EINVAL;
+ }
+ return IIO_VAL_INT;
+
+ case IIO_CHAN_INFO_SCALE:
+ /*
+ * TODO: return the hardware scale factor based on the
+ * configured PPG ADC range (PPG_ADC_RGE) and ECG PGA gain
+ * (ECG_PGA_GAIN). For PPG, scale is nA/LSB; for ECG, uV/LSB.
+ */
+ return -EINVAL;
+
+ default:
+ return -EINVAL;
+ }
+}
+
+static const struct iio_info max86150_iio_info = {
+ .read_raw = max86150_read_raw,
+};
+
+/* Triggered buffer */
+
+/**
+ * max86150_trigger_handler - threaded IRQ handler for FIFO almost-full
+ *
+ * Called by the IIO buffer infrastructure when the hardware trigger fires.
+ * Reads INT_STATUS1 to de-assert the interrupt, then drains all available
+ * FIFO samples into the IIO push buffer.
+ */
+static irqreturn_t max86150_trigger_handler(int irq, void *p)
+{
+ struct iio_poll_func *pf = p;
+ struct iio_dev *idev = pf->indio_dev;
+ struct max86150_data *data = iio_priv(idev);
+ unsigned int status, wr_ptr, rd_ptr, available;
+ u32 ppg_red, ppg_ir;
+ s32 ecg;
+ int ret;
+
+ /*
+ * Reading INT_STATUS1 clears the interrupt. Do this before touching
+ * the FIFO so the pin is de-asserted while we drain samples.
+ */
+ ret = regmap_read(data->regmap, MAX86150_REG_INT_STATUS1, &status);
+ if (ret)
+ goto done;
+
+ ret = regmap_read(data->regmap, MAX86150_REG_FIFO_WR_PTR, &wr_ptr);
+ if (ret)
+ goto done;
+ ret = regmap_read(data->regmap, MAX86150_REG_FIFO_RD_PTR, &rd_ptr);
+ if (ret)
+ goto done;
+
+ /*
+ * Number of unread samples. The FIFO is circular with depth 32, so
+ * wrap with (depth - 1) as mask.
+ */
+ available = (wr_ptr - rd_ptr) & (MAX86150_FIFO_DEPTH - 1);
+
+ while (available--) {
+ ret = max86150_read_one_sample(data, &ppg_red, &ppg_ir, &ecg);
+ if (ret)
+ break;
+
+ data->buf[MAX86150_IDX_PPG_RED] = ppg_red;
+ data->buf[MAX86150_IDX_PPG_IR] = ppg_ir;
+ data->buf[MAX86150_IDX_ECG] = ecg;
+
+ iio_push_to_buffers_with_timestamp(idev, data->buf,
+ iio_get_time_ns(idev));
+ }
+
+done:
+ iio_trigger_notify_done(idev->trig);
+ return IRQ_HANDLED;
+}
+
+/* Chip initialisation */
+
+static int max86150_chip_init(struct max86150_data *data)
+{
+ int ret;
+
+ /* Software reset; the bit self-clears within 1 ms */
+ ret = regmap_write(data->regmap, MAX86150_REG_SYS_CTRL,
+ MAX86150_SYS_RESET);
+ if (ret)
+ return ret;
+ usleep_range(1000, 2000);
+
+ /*
+ * FIFO: no sample averaging, rollover enabled, assert A_FULL when
+ * 17 samples are in the FIFO (32 - 15 = 17 available to read).
+ */
+ ret = regmap_write(data->regmap, MAX86150_REG_FIFO_CONFIG,
+ MAX86150_FIFO_ROLLOVER_EN |
+ FIELD_PREP(MAX86150_FIFO_A_FULL,
+ MAX86150_FIFO_A_FULL_VAL));
+ if (ret)
+ return ret;
+
+ /* Slot 1 = PPG Red (LED1), Slot 2 = PPG IR (LED2) */
+ ret = regmap_write(data->regmap, MAX86150_REG_FIFO_DCTRL1,
+ FIELD_PREP(MAX86150_FIFO_FD1, MAX86150_FD_LED1) |
+ FIELD_PREP(MAX86150_FIFO_FD2, MAX86150_FD_LED2));
+ if (ret)
+ return ret;
+
+ /* Slot 3 = ECG, Slot 4 = disabled */
+ ret = regmap_write(data->regmap, MAX86150_REG_FIFO_DCTRL2,
+ FIELD_PREP(MAX86150_FIFO_FD3, MAX86150_FD_ECG) |
+ FIELD_PREP(MAX86150_FIFO_FD4, MAX86150_FD_NONE));
+ if (ret)
+ return ret;
+
+ /* PPG: 100 Hz sample rate, 16384 nA ADC full-scale range */
+ ret = regmap_write(data->regmap, MAX86150_REG_PPG_CONFIG1,
+ FIELD_PREP(MAX86150_PPG_ADC_RGE,
+ MAX86150_PPG_ADC_RGE_16384) |
+ FIELD_PREP(MAX86150_PPG_SR,
+ MAX86150_PPG_SR_100HZ));
+ if (ret)
+ return ret;
+
+ /* LED pulse amplitudes (~50 mA) */
+ ret = regmap_write(data->regmap, MAX86150_REG_LED1_PA,
+ MAX86150_LED_PA_DEFAULT);
+ if (ret)
+ return ret;
+
+ ret = regmap_write(data->regmap, MAX86150_REG_LED2_PA,
+ MAX86150_LED_PA_DEFAULT);
+ if (ret)
+ return ret;
+
+ /* Enable FIFO almost-full interrupt only */
+ return regmap_write(data->regmap, MAX86150_REG_INT_ENABLE1,
+ MAX86150_INT_A_FULL);
+}
+
+/* Probe */
+
+static int max86150_probe(struct i2c_client *client)
+{
+ struct iio_dev *indio_dev;
+ struct max86150_data *data;
+ unsigned int part_id;
+ int ret;
+
+ indio_dev = devm_iio_device_alloc(&client->dev, sizeof(*data));
+ if (!indio_dev)
+ return -ENOMEM;
+
+ data = iio_priv(indio_dev);
+ data->dev = &client->dev;
+
+ data->regmap = devm_regmap_init_i2c(client, &max86150_regmap_config);
+ if (IS_ERR(data->regmap))
+ return dev_err_probe(&client->dev, PTR_ERR(data->regmap),
+ "Failed to initialise regmap\n");
+
+ ret = regmap_read(data->regmap, MAX86150_REG_PART_ID, &part_id);
+ if (ret)
+ return dev_err_probe(&client->dev, ret,
+ "Cannot read part ID\n");
+
+ if (part_id != MAX86150_PART_ID_VAL)
+ return dev_err_probe(&client->dev, -ENODEV,
+ "Unexpected part ID 0x%02x (expected 0x%02x)\n",
+ part_id, MAX86150_PART_ID_VAL);
+
+ ret = max86150_chip_init(data);
+ if (ret)
+ return dev_err_probe(&client->dev, ret,
+ "Chip initialisation failed\n");
+
+ indio_dev->name = "max86150";
+ indio_dev->channels = max86150_channels;
+ indio_dev->num_channels = ARRAY_SIZE(max86150_channels);
+ indio_dev->info = &max86150_iio_info;
+ indio_dev->modes = INDIO_DIRECT_MODE;
+
+ /*
+ * If the device tree provides an interrupt, set up a hardware
+ * trigger so userspace can use the FIFO almost-full signal to
+ * drive capture without polling.
+ */
+ if (client->irq > 0) {
+ data->trig = devm_iio_trigger_alloc(&client->dev,
+ "%s-dev%d",
+ indio_dev->name,
+ iio_device_id(indio_dev));
+ if (!data->trig)
+ return -ENOMEM;
+
+ ret = devm_request_irq(&client->dev, client->irq,
+ iio_trigger_generic_data_rdy_poll,
+ IRQF_TRIGGER_FALLING | IRQF_ONESHOT,
+ "max86150", data->trig);
+ if (ret)
+ return dev_err_probe(&client->dev, ret,
+ "Cannot request IRQ %d\n",
+ client->irq);
+
+ iio_trigger_set_drvdata(data->trig, indio_dev);
+
+ ret = devm_iio_trigger_register(&client->dev, data->trig);
+ if (ret)
+ return dev_err_probe(&client->dev, ret,
+ "Cannot register trigger\n");
+
+ indio_dev->trig = iio_trigger_get(data->trig);
+ }
+
+ ret = devm_iio_triggered_buffer_setup(&client->dev, indio_dev,
+ iio_pollfunc_store_time,
+ max86150_trigger_handler,
+ NULL);
+ if (ret)
+ return dev_err_probe(&client->dev, ret,
+ "Cannot setup triggered buffer\n");
+
+ return devm_iio_device_register(&client->dev, indio_dev);
+}
+
+/* I2C driver table */
+
+static const struct i2c_device_id max86150_id[] = {
+ { "max86150" },
+ { }
+};
+MODULE_DEVICE_TABLE(i2c, max86150_id);
+
+static const struct of_device_id max86150_of_match[] = {
+ { .compatible = "maxim,max86150" },
+ { }
+};
+MODULE_DEVICE_TABLE(of, max86150_of_match);
+
+static struct i2c_driver max86150_driver = {
+ .driver = {
+ .name = "max86150",
+ .of_match_table = max86150_of_match,
+ },
+ .probe = max86150_probe,
+ .id_table = max86150_id,
+};
+module_i2c_driver(max86150_driver);
+
+MODULE_AUTHOR("Md Shofiqul Islam <shofiqtest@gmail.com>");
+MODULE_DESCRIPTION("MAX86150 ECG and PPG biosensor driver");
+MODULE_LICENSE("GPL");
--
2.51.1
^ permalink raw reply related [flat|nested] 9+ messages in thread
* Re: [PATCH 1/1] iio: health: add MAX86150 ECG and PPG biosensor driver
2026-06-23 14:01 ` [PATCH 1/1] " Md Shofiqul Islam
@ 2026-06-23 14:14 ` sashiko-bot
2026-06-23 14:38 ` Joshua Crofts
2026-06-23 15:38 ` Krzysztof Kozlowski
2 siblings, 0 replies; 9+ messages in thread
From: sashiko-bot @ 2026-06-23 14:14 UTC (permalink / raw)
To: Md Shofiqul Islam; +Cc: robh, conor+dt, devicetree
Thank you for your contribution! Sashiko AI review found 10 potential issue(s) to consider:
- [High] Buffer overflow during triggered buffer capture.
- [High] Missing IIO direct mode locking allows concurrent sysfs reads to silently corrupt the hardware FIFO state.
- [High] Calculation of available FIFO samples evaluates to 0 when the FIFO is fully saturated, leading to a permanent capture stall.
- [High] Buffer data packing ignores `active_scan_mask`, leading to data corruption when a subset of channels is enabled.
- [High] Requesting an IRQ with `IRQF_ONESHOT` without a threaded handler causes unconditional probe failure.
- [High] Hardware interrupt unconditionally enabled at boot can permanently stall the edge-triggered line.
- [Medium] UAPI Endianness Mismatch causing data corruption on Big Endian architectures.
- [Medium] Device is left running at full power and emitting interrupts when the driver is unloaded.
- [Medium] Documented DT regulators `vdd-supply` and `leds-supply` are ignored, causing initialization failure on unpowered platforms.
- [Medium] Unbalanced `iio_trigger_get()` causes a reference count memory leak.
--
commit 86527e62ebc7afa90aeb829c2bdf9c041cf99dd9
Author: Md Shofiqul Islam <shofiqtest@gmail.com>
iio: health: add MAX86150 ECG and PPG biosensor driver
The MAX86150 integrates PPG and ECG channels in a single I2C device. The
driver exposes three IIO channels and supports hardware-triggered continuous
low-overhead capture.
> diff --git a/drivers/iio/health/max86150.c b/drivers/iio/health/max86150.c
> new file mode 100755
> index 0000000000000..3cde4af6650b7
> --- /dev/null
> +++ b/drivers/iio/health/max86150.c
[ ... ]
> +struct max86150_data {
> + struct regmap *regmap;
> + struct device *dev;
> + struct iio_trigger *trig;
> + s32 buf[4] __aligned(8);
> +};
[Severity: High]
Does this buffer have enough space for the 64-bit timestamp?
With three 32-bit channels (12 bytes) and a 64-bit timestamp needing 8-byte
alignment, the timestamp would be placed at byte offset 16. However, this
array is exactly 16 bytes.
Calling iio_push_to_buffers_with_timestamp() in max86150_trigger_handler()
could write the timestamp past the end of the array.
[ ... ]
> +static const struct iio_chan_spec max86150_channels[] = {
> + {
> + /* PPG Red LED - optical intensity, 19-bit unsigned */
> + .type = IIO_INTENSITY,
> + .modified = 1,
> + .channel2 = IIO_MOD_LIGHT_RED,
> + .info_mask_separate = BIT(IIO_CHAN_INFO_RAW) |
> + BIT(IIO_CHAN_INFO_SCALE),
> + .scan_index = MAX86150_IDX_PPG_RED,
> + .scan_type = {
> + .sign = 'u',
> + .realbits = 19,
> + .storagebits = 32,
> + .endianness = IIO_LE,
> + },
[Severity: Medium]
Since max86150_trigger_handler() places native integers directly into the
buffer without cpu_to_le32() conversions, will this advertise incorrect
endianness to userspace on big endian architectures?
[ ... ]
> +static int max86150_read_raw(struct iio_dev *indio_dev,
> + struct iio_chan_spec const *chan,
> + int *val, int *val2, long mask)
> +{
> + struct max86150_data *data = iio_priv(indio_dev);
> + u32 ppg_red, ppg_ir;
> + s32 ecg;
> + int ret;
> +
> + switch (mask) {
> + case IIO_CHAN_INFO_RAW:
> + /*
> + * Single-shot path: clear the FIFO so we know we are reading
> + * a sample that arrived after this call, not stale data.
> + */
> + ret = regmap_write(data->regmap, MAX86150_REG_FIFO_WR_PTR, 0);
> + if (ret)
> + return ret;
> + ret = regmap_write(data->regmap, MAX86150_REG_OVF_COUNTER, 0);
> + if (ret)
> + return ret;
> + ret = regmap_write(data->regmap, MAX86150_REG_FIFO_RD_PTR, 0);
[Severity: High]
Does this path need iio_device_claim_direct_mode() to prevent concurrent
sysfs reads from silently corrupting the hardware FIFO pointers while a
continuous triggered buffer capture is active?
[ ... ]
> +static irqreturn_t max86150_trigger_handler(int irq, void *p)
> +{
> + struct iio_poll_func *pf = p;
> + struct iio_dev *idev = pf->indio_dev;
> + struct max86150_data *data = iio_priv(idev);
> + unsigned int status, wr_ptr, rd_ptr, available;
> + u32 ppg_red, ppg_ir;
> + s32 ecg;
> + int ret;
> +
> + /*
> + * Reading INT_STATUS1 clears the interrupt. Do this before touching
> + * the FIFO so the pin is de-asserted while we drain samples.
> + */
> + ret = regmap_read(data->regmap, MAX86150_REG_INT_STATUS1, &status);
> + if (ret)
> + goto done;
> +
> + ret = regmap_read(data->regmap, MAX86150_REG_FIFO_WR_PTR, &wr_ptr);
> + if (ret)
> + goto done;
> + ret = regmap_read(data->regmap, MAX86150_REG_FIFO_RD_PTR, &rd_ptr);
> + if (ret)
> + goto done;
> +
> + /*
> + * Number of unread samples. The FIFO is circular with depth 32, so
> + * wrap with (depth - 1) as mask.
> + */
> + available = (wr_ptr - rd_ptr) & (MAX86150_FIFO_DEPTH - 1);
[Severity: High]
When the 32-entry FIFO becomes perfectly full and wr_ptr wraps around to
equal rd_ptr, will this evaluate to 0?
If so, the handler might skip the read loop entirely and leave the FIFO full,
causing the interrupt condition to persist and resulting in an endless
interrupt loop or a permanent capture stall.
> +
> + while (available--) {
> + ret = max86150_read_one_sample(data, &ppg_red, &ppg_ir, &ecg);
> + if (ret)
> + break;
> +
> + data->buf[MAX86150_IDX_PPG_RED] = ppg_red;
> + data->buf[MAX86150_IDX_PPG_IR] = ppg_ir;
> + data->buf[MAX86150_IDX_ECG] = ecg;
[Severity: High]
If userspace enables only a subset of the channels (e.g., only ECG), will
this static indexing ignore the active_scan_mask?
IIO expects enabled channels to be tightly packed at the start of the buffer.
If only ECG is enabled, the IIO core calculates the timestamp offset based on
a single channel. The static writes here might cause the timestamp to later
overwrite the channel data, corrupting both the measurement and the
timestamp.
[ ... ]
> +static int max86150_chip_init(struct max86150_data *data)
> +{
> + int ret;
> +
> + /* Software reset; the bit self-clears within 1 ms */
> + ret = regmap_write(data->regmap, MAX86150_REG_SYS_CTRL,
> + MAX86150_SYS_RESET);
[ ... ]
> + /* Enable FIFO almost-full interrupt only */
> + return regmap_write(data->regmap, MAX86150_REG_INT_ENABLE1,
> + MAX86150_INT_A_FULL);
> +}
[Severity: Medium]
Are there teardown steps needed when the driver is unloaded?
This initialization starts continuous sampling, but no .remove callback or
devm action seems to be registered to write to MAX86150_SYS_SHDN or disable
the interrupts, leaving the device fully active on unload.
[Severity: High]
Could enabling the hardware interrupt here before the IIO buffer is active
cause the edge-triggered interrupt pin to get permanently stuck?
If the sensor asserts the active-low pin early, the hardirq handler will
fire but won't read INT_STATUS1 to clear the line (since the threaded handler
is only active when the buffer is enabled). The interrupt controller might
ignore the stuck low line forever, breaking capture once the buffer is
actually started.
[ ... ]
> +static int max86150_probe(struct i2c_client *client)
> +{
> + struct iio_dev *indio_dev;
> + struct max86150_data *data;
> + unsigned int part_id;
> + int ret;
> +
> + indio_dev = devm_iio_device_alloc(&client->dev, sizeof(*data));
> + if (!indio_dev)
> + return -ENOMEM;
> +
> + data = iio_priv(indio_dev);
> + data->dev = &client->dev;
> +
> + data->regmap = devm_regmap_init_i2c(client, &max86150_regmap_config);
> + if (IS_ERR(data->regmap))
> + return dev_err_probe(&client->dev, PTR_ERR(data->regmap),
> + "Failed to initialise regmap\n");
[Severity: Medium]
Will this fail to probe on unpowered platforms since the documented
devicetree regulators vdd-supply and leds-supply are not requested or
enabled before I2C communication starts?
[ ... ]
> + if (client->irq > 0) {
> + data->trig = devm_iio_trigger_alloc(&client->dev,
> + "%s-dev%d",
> + indio_dev->name,
> + iio_device_id(indio_dev));
> + if (!data->trig)
> + return -ENOMEM;
> +
> + ret = devm_request_irq(&client->dev, client->irq,
> + iio_trigger_generic_data_rdy_poll,
> + IRQF_TRIGGER_FALLING | IRQF_ONESHOT,
> + "max86150", data->trig);
[Severity: High]
Will the irq core reject this request during probe?
The interrupt core requires a threaded handler when IRQF_ONESHOT is used,
but iio_trigger_generic_data_rdy_poll is passed as the hardirq handler with
no threaded handler provided.
> + if (ret)
> + return dev_err_probe(&client->dev, ret,
> + "Cannot request IRQ %d\n",
> + client->irq);
> +
> + iio_trigger_set_drvdata(data->trig, indio_dev);
> +
> + ret = devm_iio_trigger_register(&client->dev, data->trig);
> + if (ret)
> + return dev_err_probe(&client->dev, ret,
> + "Cannot register trigger\n");
> +
> + indio_dev->trig = iio_trigger_get(data->trig);
> + }
[Severity: Medium]
Is there a corresponding iio_trigger_put() needed here? Without it, the
trigger reference count could be leaked when the device is unbound or if a
subsequent probe step fails.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260623140113.12574-2-shofiqtest@gmail.com?part=1
^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH 1/1] iio: health: add MAX86150 ECG and PPG biosensor driver
2026-06-23 14:01 ` [PATCH 1/1] " Md Shofiqul Islam
2026-06-23 14:14 ` sashiko-bot
@ 2026-06-23 14:38 ` Joshua Crofts
2026-06-23 15:38 ` Krzysztof Kozlowski
2 siblings, 0 replies; 9+ messages in thread
From: Joshua Crofts @ 2026-06-23 14:38 UTC (permalink / raw)
To: Md Shofiqul Islam
Cc: linux-iio, jic23, dlechner, nuno.sa, robh, krzk+dt, devicetree
On Tue, 23 Jun 2026 17:01:13 +0300
Md Shofiqul Islam <shofiqtest@gmail.com> wrote:
> The MAX86150 (Maxim/Analog Devices) integrates two PPG optical channels
> (Red/IR LED) and one ECG biopotential channel in a single I2C device.
> Data is captured via a 32-entry hardware FIFO with a configurable
> almost-full interrupt.
>
> The driver exposes three IIO channels:
> - in_intensity_red_raw (PPG Red, 19-bit unsigned)
> - in_intensity_ir_raw (PPG IR, 19-bit unsigned)
> - in_voltage0_raw (ECG, 18-bit signed)
>
> A hardware trigger is registered when an interrupt GPIO is provided in
> the device tree, enabling the IIO triggered buffer path for continuous
> low-overhead capture.
>
> FIFO reads use regmap_noinc_read() to burst-read 9 bytes (3 slots x
> 3 bytes) from the streaming FIFO_DATA register in a single I2C
> transaction.
>
> Signed-off-by: Md Shofiqul Islam <shofiqtest@gmail.com>
> ---
> .../bindings/iio/health/maxim,max86150.yaml | 67 +++
> drivers/iio/health/Kconfig | 23 +
> drivers/iio/health/Makefile | 1 +
> drivers/iio/health/max86150.c | 512 ++++++++++++++++++
> 4 files changed, 603 insertions(+)
> create mode 100755 Documentation/devicetree/bindings/iio/health/maxim,max86150.yaml
> create mode 100755 drivers/iio/health/max86150.c
>
> diff --git a/Documentation/devicetree/bindings/iio/health/maxim,max86150.yaml b/Documentation/devicetree/bindings/iio/health/maxim,max86150.yaml
> new file mode 100755
> index 000000000000..1bf10fd1a3d2
> --- /dev/null
> +++ b/Documentation/devicetree/bindings/iio/health/maxim,max86150.yaml
> @@ -0,0 +1,67 @@
> +# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
> +%YAML 1.2
> +---
> +$id: http://devicetree.org/schemas/iio/health/maxim,max86150.yaml#
> +$schema: http://devicetree.org/meta-schemas/core.yaml#
> +
Split this into 2 patches - one for the new dt-binding and one for the
actual driver, KConfig and Makefile.
Additionally, since you're adding a brand new driver, a MAINTAINERS entry
is required.
Kind regards
CJD
^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH 1/1] iio: health: add MAX86150 ECG and PPG biosensor driver
2026-06-23 14:01 ` [PATCH 1/1] " Md Shofiqul Islam
2026-06-23 14:14 ` sashiko-bot
2026-06-23 14:38 ` Joshua Crofts
@ 2026-06-23 15:38 ` Krzysztof Kozlowski
2 siblings, 0 replies; 9+ messages in thread
From: Krzysztof Kozlowski @ 2026-06-23 15:38 UTC (permalink / raw)
To: Md Shofiqul Islam, linux-iio
Cc: jic23, dlechner, nuno.sa, robh, krzk+dt, devicetree
On 23/06/2026 16:01, Md Shofiqul Islam wrote:
> The MAX86150 (Maxim/Analog Devices) integrates two PPG optical channels
> (Red/IR LED) and one ECG biopotential channel in a single I2C device.
> Data is captured via a 32-entry hardware FIFO with a configurable
> almost-full interrupt.
>
> The driver exposes three IIO channels:
> - in_intensity_red_raw (PPG Red, 19-bit unsigned)
> - in_intensity_ir_raw (PPG IR, 19-bit unsigned)
> - in_voltage0_raw (ECG, 18-bit signed)
>
> A hardware trigger is registered when an interrupt GPIO is provided in
> the device tree, enabling the IIO triggered buffer path for continuous
> low-overhead capture.
>
> FIFO reads use regmap_noinc_read() to burst-read 9 bytes (3 slots x
> 3 bytes) from the streaming FIFO_DATA register in a single I2C
> transaction.
>
> Signed-off-by: Md Shofiqul Islam <shofiqtest@gmail.com>
Please run scripts/checkpatch.pl on the patches and fix reported
warnings. After that, run also 'scripts/checkpatch.pl --strict' on the
patches and (probably) fix more warnings. Some warnings can be ignored,
especially from --strict run, but the code here looks like it needs a
fix. Feel free to get in touch if the warning is not clear.
Best regards,
Krzysztof
^ permalink raw reply [flat|nested] 9+ messages in thread
* [PATCH v2 0/3] iio: health: add MAX86150 ECG and PPG biosensor driver
2026-06-23 14:01 [PATCH 0/1] iio: health: add MAX86150 ECG and PPG biosensor driver Md Shofiqul Islam
2026-06-23 14:01 ` [PATCH 1/1] " Md Shofiqul Islam
@ 2026-06-23 15:55 ` Md Shofiqul Islam
2026-06-23 15:55 ` [PATCH v2 1/3] dt-bindings: iio: health: add maxim,max86150 Md Shofiqul Islam
` (2 more replies)
1 sibling, 3 replies; 9+ messages in thread
From: Md Shofiqul Islam @ 2026-06-23 15:55 UTC (permalink / raw)
To: linux-iio
Cc: jic23, dlechner, nuno.sa, robh, krzk+dt, conor+dt, devicetree,
joshua.crofts1, Md Shofiqul Islam
The MAX86150 (Maxim/Analog Devices) combines two PPG optical channels
(Red/IR LED) and one ECG biopotential channel in a single I2C device.
Data is produced at up to 3200 Hz and buffered in a 32-entry hardware
FIFO with a configurable almost-full interrupt.
This series adds:
1. DT binding YAML schema for maxim,max86150
2. IIO driver with triggered buffer support
3. MAINTAINERS entry
Changes in v2 (addressing Sashiko AI review, Joshua Crofts, Krzysztof Kozlowski):
- Fix buffer overflow: expand scan buffer to s32 buf[6] to hold
3 x s32 channels + 4-byte pad + s64 timestamp (24 bytes total)
- Add iio_device_claim_direct() in read_raw to prevent concurrent
sysfs reads from corrupting FIFO pointers during buffer capture
- Fix FIFO-full detection: read OVF_COUNTER to distinguish a fully
saturated FIFO (wr_ptr == rd_ptr after wrap) from an empty one
- Fix active_scan_mask handling: pack only active channels at
consecutive positions using test_bit(idx, active_scan_mask)
- Remove IRQF_ONESHOT from IRQ request (not needed for edge-triggered;
iio_trigger_generic_data_rdy_poll is a hard-IRQ handler)
- Add set_trigger_state callback to enable/disable A_FULL interrupt
only when the IIO buffer is active, preventing the interrupt line
from asserting before the handler is registered
- Fix endianness: change scan_type.endianness from IIO_LE to IIO_CPU
to match native integers written directly into the buffer
- Add max86150_powerdown() devm action to disable interrupts and
assert SYS_SHDN on driver unload
- Add devm_regulator_get_enable() for vdd-supply and leds-supply
before any I2C access
- Add devm action for iio_trigger_put() to balance the reference
count increment from iio_trigger_get()
- Split DT binding into a separate first patch (requested by Joshua Crofts)
- Add MAINTAINERS entry (requested by Joshua Crofts)
Known limitations (to be addressed in v3):
- IIO_CHAN_INFO_SCALE not yet implemented
- Runtime PM (SYS_CTRL SHDN bit) not yet implemented
- Not yet tested on physical hardware
Md Shofiqul Islam (3):
dt-bindings: iio: health: add maxim,max86150
iio: health: add MAX86150 ECG and PPG biosensor driver
MAINTAINERS: add entry for MAX86150 IIO health driver
.../bindings/iio/health/maxim,max86150.yaml | 67 ++
MAINTAINERS | 7 +
drivers/iio/health/Kconfig | 18 +
drivers/iio/health/Makefile | 1 +
drivers/iio/health/max86150.c | 612 ++++++++++++++++++
5 files changed, 705 insertions(+)
create mode 100644 Documentation/devicetree/bindings/iio/health/maxim,max86150.yaml
create mode 100644 drivers/iio/health/max86150.c
--
2.51.1
^ permalink raw reply [flat|nested] 9+ messages in thread
* [PATCH v2 1/3] dt-bindings: iio: health: add maxim,max86150
2026-06-23 15:55 ` [PATCH v2 0/3] " Md Shofiqul Islam
@ 2026-06-23 15:55 ` Md Shofiqul Islam
2026-06-23 15:55 ` [PATCH v2 2/3] iio: health: add MAX86150 ECG and PPG biosensor driver Md Shofiqul Islam
2026-06-23 15:55 ` [PATCH v2 3/3] MAINTAINERS: add entry for MAX86150 IIO health driver Md Shofiqul Islam
2 siblings, 0 replies; 9+ messages in thread
From: Md Shofiqul Islam @ 2026-06-23 15:55 UTC (permalink / raw)
To: linux-iio
Cc: jic23, dlechner, nuno.sa, robh, krzk+dt, conor+dt, devicetree,
joshua.crofts1, Md Shofiqul Islam
Add YAML schema binding for the Maxim MAX86150 combined ECG and PPG
biosensor. The device communicates over I2C at up to 400 kHz and
exposes an active-low interrupt when the 32-entry hardware FIFO
reaches its configurable almost-full threshold.
Signed-off-by: Md Shofiqul Islam <shofiqtest@gmail.com>
---
.../bindings/iio/health/maxim,max86150.yaml | 67 +++++++++++++++++++
1 file changed, 67 insertions(+)
create mode 100755 Documentation/devicetree/bindings/iio/health/maxim,max86150.yaml
diff --git a/Documentation/devicetree/bindings/iio/health/maxim,max86150.yaml b/Documentation/devicetree/bindings/iio/health/maxim,max86150.yaml
new file mode 100755
index 000000000000..1bf10fd1a3d2
--- /dev/null
+++ b/Documentation/devicetree/bindings/iio/health/maxim,max86150.yaml
@@ -0,0 +1,67 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/iio/health/maxim,max86150.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Maxim MAX86150 ECG and PPG Biosensor
+
+maintainers:
+ - Md Shofiqul Islam <shofiqtest@gmail.com>
+
+description: |
+ The MAX86150 is an integrated biosensor SoC that combines:
+ - Two PPG (photoplethysmography) channels: Red LED and IR LED,
+ for heart rate and blood-oxygen saturation (SpO2) measurement.
+ - One ECG (electrocardiogram) channel for biopotential recording.
+
+ The device communicates over I2C at up to 400 kHz and raises an
+ active-low interrupt when the 32-entry hardware FIFO reaches its
+ configurable almost-full threshold.
+
+ Datasheet:
+ https://www.analog.com/media/en/technical-documentation/data-sheets/MAX86150.pdf
+
+properties:
+ compatible:
+ const: maxim,max86150
+
+ reg:
+ maxItems: 1
+ description: I2C device address, always 0x5E.
+
+ interrupts:
+ maxItems: 1
+ description: |
+ Active-low interrupt line. Asserted when the FIFO almost-full
+ threshold is reached or when a new PPG sample is ready.
+
+ vdd-supply:
+ description: Digital core supply, 1.71 V to 1.89 V.
+
+ leds-supply:
+ description: LED anode supply, 3.0 V to 5.5 V.
+
+required:
+ - compatible
+ - reg
+
+additionalProperties: false
+
+examples:
+ - |
+ #include <dt-bindings/interrupt-controller/irq.h>
+
+ i2c {
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ biosensor@5e {
+ compatible = "maxim,max86150";
+ reg = <0x5e>;
+ interrupt-parent = <&gpio1>;
+ interrupts = <5 IRQ_TYPE_EDGE_FALLING>;
+ vdd-supply = <&vdd_1v8>;
+ leds-supply = <&vdd_3v3>;
+ };
+ };
--
2.51.1
^ permalink raw reply related [flat|nested] 9+ messages in thread
* [PATCH v2 2/3] iio: health: add MAX86150 ECG and PPG biosensor driver
2026-06-23 15:55 ` [PATCH v2 0/3] " Md Shofiqul Islam
2026-06-23 15:55 ` [PATCH v2 1/3] dt-bindings: iio: health: add maxim,max86150 Md Shofiqul Islam
@ 2026-06-23 15:55 ` Md Shofiqul Islam
2026-06-23 15:55 ` [PATCH v2 3/3] MAINTAINERS: add entry for MAX86150 IIO health driver Md Shofiqul Islam
2 siblings, 0 replies; 9+ messages in thread
From: Md Shofiqul Islam @ 2026-06-23 15:55 UTC (permalink / raw)
To: linux-iio
Cc: jic23, dlechner, nuno.sa, robh, krzk+dt, conor+dt, devicetree,
joshua.crofts1, Md Shofiqul Islam
The MAX86150 (Maxim/Analog Devices) integrates two PPG optical channels
(Red/IR LED) and one ECG biopotential channel in a single I2C device.
Data is captured via a 32-entry hardware FIFO with a configurable
almost-full interrupt.
The driver exposes three IIO channels:
- in_intensity_red_raw (PPG Red, 19-bit unsigned)
- in_intensity_ir_raw (PPG IR, 19-bit unsigned)
- in_voltage0_raw (ECG, 18-bit signed)
A hardware trigger is registered when an interrupt GPIO is provided in
the device tree, enabling the IIO triggered buffer path for continuous
low-overhead capture. The trigger enable/disable is driven by the
set_trigger_state callback to prevent the interrupt line from asserting
before the handler is ready.
FIFO reads use regmap_noinc_read() to burst-read 9 bytes (3 slots x
3 bytes) from the streaming FIFO_DATA register in a single I2C
transaction.
Signed-off-by: Md Shofiqul Islam <shofiqtest@gmail.com>
---
drivers/iio/health/Kconfig | 23 ++
drivers/iio/health/Makefile | 1 +
drivers/iio/health/max86150.c | 612 ++++++++++++++++++++++++++++++++++
3 files changed, 636 insertions(+)
create mode 100755 drivers/iio/health/max86150.c
diff --git a/drivers/iio/health/Kconfig b/drivers/iio/health/Kconfig
index a89f3abf11f4..6496cf55290c 100644
--- a/drivers/iio/health/Kconfig
+++ b/drivers/iio/health/Kconfig
@@ -13,6 +13,7 @@ config AFE4403
depends on SPI_MASTER
select REGMAP_SPI
select IIO_BUFFER
+ select IIO_TRIGGER
select IIO_TRIGGERED_BUFFER
help
Say yes to choose the Texas Instruments AFE4403
@@ -26,6 +27,7 @@ config AFE4404
depends on I2C
select REGMAP_I2C
select IIO_BUFFER
+ select IIO_TRIGGER
select IIO_TRIGGERED_BUFFER
help
Say yes to choose the Texas Instruments AFE4404
@@ -39,6 +41,7 @@ config MAX30100
depends on I2C
select REGMAP_I2C
select IIO_BUFFER
+ select IIO_TRIGGER
select IIO_KFIFO_BUF
help
Say Y here to build I2C interface support for the Maxim
@@ -52,6 +55,7 @@ config MAX30102
depends on I2C
select REGMAP_I2C
select IIO_BUFFER
+ select IIO_TRIGGER
select IIO_KFIFO_BUF
help
Say Y here to build I2C interface support for the Maxim
@@ -62,4 +66,23 @@ config MAX30102
endmenu
+
+config MAX86150
+ tristate "MAX86150 ECG and PPG biosensor"
+ depends on I2C
+ select IIO_BUFFER
+ select IIO_TRIGGER
+ select IIO_TRIGGERED_BUFFER
+ select REGMAP_I2C
+ help
+ Say Y here to enable support for the Maxim MAX86150 combined
+ ECG and photoplethysmography (PPG) biosensor.
+
+ The driver exposes three IIO channels: two PPG optical channels
+ (Red and IR LED) for heart rate and SpO2 monitoring, and one
+ ECG channel for biopotential recording.
+
+ This driver can also be built as a module. If so, the module
+ will be called max86150.
+
endmenu
diff --git a/drivers/iio/health/Makefile b/drivers/iio/health/Makefile
index 910817112258..04fc73c58444 100644
--- a/drivers/iio/health/Makefile
+++ b/drivers/iio/health/Makefile
@@ -9,3 +9,4 @@ obj-$(CONFIG_AFE4403) += afe4403.o
obj-$(CONFIG_AFE4404) += afe4404.o
obj-$(CONFIG_MAX30100) += max30100.o
obj-$(CONFIG_MAX30102) += max30102.o
+obj-$(CONFIG_MAX86150) += max86150.o
diff --git a/drivers/iio/health/max86150.c b/drivers/iio/health/max86150.c
new file mode 100755
index 000000000000..70c51ccb4672
--- /dev/null
+++ b/drivers/iio/health/max86150.c
@@ -0,0 +1,612 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * MAX86150 combined ECG and PPG biosensor driver
+ *
+ * Copyright (C) 2026 Md Shofiqul Islam <shofiqtest@gmail.com>
+ *
+ * The MAX86150 integrates two PPG optical channels (Red/IR LED) and one
+ * ECG biopotential channel in a single I2C device. Data is captured
+ * through a 32-entry hardware FIFO with a configurable almost-full
+ * interrupt, making it well-suited for continuous monitoring with a
+ * low-power host.
+ *
+ * Datasheet:
+ * https://www.analog.com/media/en/technical-documentation/data-sheets/MAX86150.pdf
+ */
+
+#include <linux/bitfield.h>
+#include <linux/delay.h>
+#include <linux/i2c.h>
+#include <linux/iio/buffer.h>
+#include <linux/iio/iio.h>
+#include <linux/iio/trigger.h>
+#include <linux/iio/trigger_consumer.h>
+#include <linux/iio/triggered_buffer.h>
+#include <linux/module.h>
+#include <linux/regmap.h>
+#include <linux/regulator/consumer.h>
+
+/* Register addresses */
+#define MAX86150_REG_INT_STATUS1 0x00
+#define MAX86150_REG_INT_STATUS2 0x01
+#define MAX86150_REG_INT_ENABLE1 0x02
+#define MAX86150_REG_INT_ENABLE2 0x03
+#define MAX86150_REG_FIFO_WR_PTR 0x04
+#define MAX86150_REG_OVF_COUNTER 0x05
+#define MAX86150_REG_FIFO_RD_PTR 0x06
+#define MAX86150_REG_FIFO_DATA 0x07
+#define MAX86150_REG_FIFO_CONFIG 0x08
+#define MAX86150_REG_FIFO_DCTRL1 0x09 /* FD1[3:0] FD2[7:4] */
+#define MAX86150_REG_FIFO_DCTRL2 0x0A /* FD3[3:0] FD4[7:4] */
+#define MAX86150_REG_SYS_CTRL 0x0D
+#define MAX86150_REG_PPG_CONFIG1 0x10
+#define MAX86150_REG_PPG_CONFIG2 0x11
+#define MAX86150_REG_LED1_PA 0x14 /* Red LED pulse amplitude */
+#define MAX86150_REG_LED2_PA 0x15 /* IR LED pulse amplitude */
+#define MAX86150_REG_ECG_CONFIG1 0x3C
+#define MAX86150_REG_ECG_CONFIG3 0x3E
+#define MAX86150_REG_PART_ID 0xFF
+
+/* Field masks */
+#define MAX86150_PART_ID_VAL 0x1E
+
+/* INT_STATUS1 / INT_ENABLE1 */
+#define MAX86150_INT_A_FULL BIT(7) /* FIFO almost full */
+#define MAX86150_INT_PPG_RDY BIT(6) /* new PPG sample ready */
+
+/* SYS_CTRL */
+#define MAX86150_SYS_SHDN BIT(1)
+#define MAX86150_SYS_RESET BIT(0)
+
+/* FIFO_CONFIG */
+#define MAX86150_FIFO_SMP_AVE GENMASK(7, 5)
+#define MAX86150_FIFO_ROLLOVER_EN BIT(4)
+#define MAX86150_FIFO_A_FULL GENMASK(3, 0)
+
+/* FIFO slot data-type codes */
+#define MAX86150_FD_NONE 0x0
+#define MAX86150_FD_LED1 0x1 /* Red PPG */
+#define MAX86150_FD_LED2 0x2 /* IR PPG */
+#define MAX86150_FD_ECG 0x9
+#define MAX86150_FIFO_FD1 GENMASK(3, 0)
+#define MAX86150_FIFO_FD2 GENMASK(7, 4)
+#define MAX86150_FIFO_FD3 GENMASK(3, 0)
+#define MAX86150_FIFO_FD4 GENMASK(7, 4)
+
+/* PPG_CONFIG1 */
+#define MAX86150_PPG_ADC_RGE GENMASK(7, 6)
+#define MAX86150_PPG_SR GENMASK(5, 1)
+
+/* Geometry */
+#define MAX86150_FIFO_DEPTH 32
+#define MAX86150_BYTES_PER_SLOT 3 /* 24-bit word per slot */
+#define MAX86150_NUM_SLOTS 3 /* Red, IR, ECG */
+#define MAX86150_SAMPLE_BYTES (MAX86150_NUM_SLOTS * MAX86150_BYTES_PER_SLOT)
+
+/* Default hardware configuration */
+#define MAX86150_LED_PA_DEFAULT 0x3F /* ~50 mA */
+#define MAX86150_PPG_SR_100HZ 4 /* PPG_SR field value for 100 Hz */
+#define MAX86150_PPG_ADC_RGE_16384 2 /* 16384 nA full scale */
+/* Fire A_FULL when 17 slots remain (32 - 15 = 17 samples in FIFO) */
+#define MAX86150_FIFO_A_FULL_VAL 15
+
+/* Scan element indices */
+enum max86150_scan_idx {
+ MAX86150_IDX_PPG_RED = 0,
+ MAX86150_IDX_PPG_IR = 1,
+ MAX86150_IDX_ECG = 2,
+ MAX86150_IDX_TS,
+};
+
+/**
+ * struct max86150_data - driver private state
+ * @regmap: register map for this device
+ * @dev: parent device (for dev_err logging)
+ * @trig: IIO hardware trigger backed by the device interrupt line
+ * @buf: DMA-safe capture buffer sized for worst-case (all 3 channels
+ * active): 3 x s32 (12 bytes) + 4-byte pad + s64 timestamp = 24
+ * bytes. __aligned(8) satisfies iio_push_to_buffers_with_timestamp().
+ */
+struct max86150_data {
+ struct regmap *regmap;
+ struct device *dev;
+ struct iio_trigger *trig;
+ s32 buf[6] __aligned(8);
+};
+
+/* IIO channel specification */
+
+static const struct iio_chan_spec max86150_channels[] = {
+ {
+ /* PPG Red LED - optical intensity, 19-bit unsigned */
+ .type = IIO_INTENSITY,
+ .modified = 1,
+ .channel2 = IIO_MOD_LIGHT_RED,
+ .info_mask_separate = BIT(IIO_CHAN_INFO_RAW) |
+ BIT(IIO_CHAN_INFO_SCALE),
+ .scan_index = MAX86150_IDX_PPG_RED,
+ .scan_type = {
+ .sign = 'u',
+ .realbits = 19,
+ .storagebits = 32,
+ .endianness = IIO_CPU,
+ },
+ },
+ {
+ /* PPG IR LED - optical intensity, 19-bit unsigned */
+ .type = IIO_INTENSITY,
+ .modified = 1,
+ .channel2 = IIO_MOD_LIGHT_IR,
+ .info_mask_separate = BIT(IIO_CHAN_INFO_RAW) |
+ BIT(IIO_CHAN_INFO_SCALE),
+ .scan_index = MAX86150_IDX_PPG_IR,
+ .scan_type = {
+ .sign = 'u',
+ .realbits = 19,
+ .storagebits = 32,
+ .endianness = IIO_CPU,
+ },
+ },
+ {
+ /* ECG biopotential - voltage, 18-bit signed two's complement */
+ .type = IIO_VOLTAGE,
+ .channel = 0,
+ .info_mask_separate = BIT(IIO_CHAN_INFO_RAW) |
+ BIT(IIO_CHAN_INFO_SCALE),
+ .scan_index = MAX86150_IDX_ECG,
+ .scan_type = {
+ .sign = 's',
+ .realbits = 18,
+ .storagebits = 32,
+ .endianness = IIO_CPU,
+ },
+ },
+ IIO_CHAN_SOFT_TIMESTAMP(MAX86150_IDX_TS),
+};
+
+/* Regmap configuration */
+
+static const struct regmap_config max86150_regmap_config = {
+ .reg_bits = 8,
+ .val_bits = 8,
+ .max_register = MAX86150_REG_PART_ID,
+};
+
+/* FIFO helper */
+
+/**
+ * max86150_read_one_sample - burst-read one complete 3-slot FIFO entry
+ * @data: driver state
+ * @ppg_red: out - 19-bit PPG Red ADC value (unsigned)
+ * @ppg_ir: out - 19-bit PPG IR ADC value (unsigned)
+ * @ecg: out - 18-bit ECG ADC value (sign-extended to s32)
+ *
+ * Each FIFO entry is 9 bytes (3 slots x 3 bytes). FIFO_DATA is a
+ * streaming register - the address does not auto-increment on each
+ * byte, so regmap_noinc_read() is used instead of regmap_bulk_read().
+ *
+ * Byte layout in the 24-bit FIFO word (MSB first):
+ * PPG 19-bit unsigned: bits [18:0], top 5 bits are always zero
+ * ECG 18-bit signed: bits [17:0], top 6 bits are sign extension
+ */
+static int max86150_read_one_sample(struct max86150_data *data,
+ u32 *ppg_red, u32 *ppg_ir, s32 *ecg)
+{
+ u8 buf[MAX86150_SAMPLE_BYTES]; /* 9 bytes */
+ int ret;
+
+ ret = regmap_noinc_read(data->regmap, MAX86150_REG_FIFO_DATA,
+ buf, sizeof(buf));
+ if (ret)
+ return ret;
+
+ /* Bytes [0..2]: PPG Red - 19-bit value in bits [18:0] */
+ *ppg_red = (u32)(buf[0] & 0x07) << 16 | (u32)buf[1] << 8 | buf[2];
+
+ /* Bytes [3..5]: PPG IR - same format */
+ *ppg_ir = (u32)(buf[3] & 0x07) << 16 | (u32)buf[4] << 8 | buf[5];
+
+ /* Bytes [6..8]: ECG - 18-bit signed, sign-extend to s32 */
+ *ecg = sign_extend32((u32)(buf[6] & 0x03) << 16 |
+ (u32)buf[7] << 8 | buf[8], 17);
+
+ return 0;
+}
+
+/* IIO read_raw */
+
+static int max86150_read_raw(struct iio_dev *indio_dev,
+ struct iio_chan_spec const *chan,
+ int *val, int *val2, long mask)
+{
+ struct max86150_data *data = iio_priv(indio_dev);
+ u32 ppg_red, ppg_ir;
+ s32 ecg;
+ int ret;
+
+ switch (mask) {
+ case IIO_CHAN_INFO_RAW:
+ /*
+ * Claim direct mode to prevent concurrent sysfs reads from
+ * corrupting the FIFO pointers while a triggered buffer
+ * capture is active.
+ */
+ if (!iio_device_claim_direct(indio_dev))
+ return -EBUSY;
+
+ /*
+ * Single-shot path: reset FIFO pointers so the next sample
+ * read is one that arrived after this call.
+ */
+ ret = regmap_write(data->regmap, MAX86150_REG_FIFO_WR_PTR, 0);
+ if (!ret)
+ ret = regmap_write(data->regmap,
+ MAX86150_REG_OVF_COUNTER, 0);
+ if (!ret)
+ ret = regmap_write(data->regmap,
+ MAX86150_REG_FIFO_RD_PTR, 0);
+ if (ret) {
+ iio_device_release_direct(indio_dev);
+ return ret;
+ }
+
+ /* Wait for one complete sample period at 100 Hz (<= 10 ms) */
+ usleep_range(11000, 13000);
+
+ ret = max86150_read_one_sample(data, &ppg_red, &ppg_ir, &ecg);
+ iio_device_release_direct(indio_dev);
+ if (ret)
+ return ret;
+
+ switch (chan->scan_index) {
+ case MAX86150_IDX_PPG_RED:
+ *val = ppg_red;
+ break;
+ case MAX86150_IDX_PPG_IR:
+ *val = ppg_ir;
+ break;
+ case MAX86150_IDX_ECG:
+ *val = ecg;
+ break;
+ default:
+ return -EINVAL;
+ }
+ return IIO_VAL_INT;
+
+ case IIO_CHAN_INFO_SCALE:
+ /*
+ * TODO: return the hardware scale factor based on the
+ * configured PPG ADC range (PPG_ADC_RGE) and ECG PGA gain
+ * (ECG_PGA_GAIN). For PPG, scale is nA/LSB; for ECG, uV/LSB.
+ */
+ return -EINVAL;
+
+ default:
+ return -EINVAL;
+ }
+}
+
+static const struct iio_info max86150_iio_info = {
+ .read_raw = max86150_read_raw,
+};
+
+/* Trigger */
+
+/*
+ * Enable or disable the FIFO almost-full interrupt when the IIO triggered
+ * buffer is started or stopped. Keeping the interrupt masked until capture
+ * is active prevents a pre-existing FIFO fill from asserting the line before
+ * the handler is ready to clear it.
+ */
+static int max86150_set_trigger_state(struct iio_trigger *trig, bool state)
+{
+ struct iio_dev *indio_dev = iio_trigger_get_drvdata(trig);
+ struct max86150_data *data = iio_priv(indio_dev);
+ unsigned int mask = state ? MAX86150_INT_A_FULL : 0;
+
+ return regmap_write(data->regmap, MAX86150_REG_INT_ENABLE1, mask);
+}
+
+static const struct iio_trigger_ops max86150_trigger_ops = {
+ .set_trigger_state = max86150_set_trigger_state,
+};
+
+/* Triggered buffer */
+
+/**
+ * max86150_trigger_handler - threaded IRQ handler for FIFO almost-full
+ *
+ * Called by the IIO buffer infrastructure when the hardware trigger fires.
+ * Reads INT_STATUS1 to de-assert the interrupt, then drains all available
+ * FIFO samples into the IIO push buffer, packing only the channels that
+ * are currently enabled in active_scan_mask.
+ */
+static irqreturn_t max86150_trigger_handler(int irq, void *p)
+{
+ struct iio_poll_func *pf = p;
+ struct iio_dev *idev = pf->indio_dev;
+ struct max86150_data *data = iio_priv(idev);
+ unsigned int status, wr_ptr, rd_ptr, ovf, available;
+ u32 ppg_red, ppg_ir;
+ s32 ecg;
+ int ret, j;
+
+ /*
+ * Reading INT_STATUS1 clears the interrupt. Do this before touching
+ * the FIFO so the pin is de-asserted while we drain samples.
+ */
+ ret = regmap_read(data->regmap, MAX86150_REG_INT_STATUS1, &status);
+ if (ret)
+ goto done;
+
+ ret = regmap_read(data->regmap, MAX86150_REG_FIFO_WR_PTR, &wr_ptr);
+ if (ret)
+ goto done;
+ ret = regmap_read(data->regmap, MAX86150_REG_FIFO_RD_PTR, &rd_ptr);
+ if (ret)
+ goto done;
+
+ /*
+ * Read OVF_COUNTER to distinguish a full FIFO from an empty one:
+ * both produce wr_ptr == rd_ptr after the pointers wrap around.
+ * If OVF_COUNTER is non-zero the FIFO has saturated; treat all 32
+ * slots as available so we drain the entire FIFO and clear the
+ * interrupt condition.
+ */
+ ret = regmap_read(data->regmap, MAX86150_REG_OVF_COUNTER, &ovf);
+ if (ret)
+ goto done;
+
+ if (ovf > 0)
+ available = MAX86150_FIFO_DEPTH;
+ else
+ available = (wr_ptr - rd_ptr) & (MAX86150_FIFO_DEPTH - 1);
+
+ while (available--) {
+ ret = max86150_read_one_sample(data, &ppg_red, &ppg_ir, &ecg);
+ if (ret)
+ break;
+
+ /*
+ * Pack only active channels at consecutive positions [0..j-1].
+ * iio_push_to_buffers_with_timestamp() uses scan_bytes (which
+ * accounts for the active channel count) to place the timestamp,
+ * so static indexing would misplace it when fewer than 3
+ * channels are enabled.
+ */
+ j = 0;
+ if (test_bit(MAX86150_IDX_PPG_RED, idev->active_scan_mask))
+ data->buf[j++] = ppg_red;
+ if (test_bit(MAX86150_IDX_PPG_IR, idev->active_scan_mask))
+ data->buf[j++] = ppg_ir;
+ if (test_bit(MAX86150_IDX_ECG, idev->active_scan_mask))
+ data->buf[j++] = ecg;
+
+ iio_push_to_buffers_with_timestamp(idev, data->buf,
+ iio_get_time_ns(idev));
+ }
+
+done:
+ iio_trigger_notify_done(idev->trig);
+ return IRQ_HANDLED;
+}
+
+/* Chip initialisation / teardown */
+
+static void max86150_powerdown(void *arg)
+{
+ struct max86150_data *data = arg;
+
+ regmap_write(data->regmap, MAX86150_REG_INT_ENABLE1, 0);
+ regmap_write(data->regmap, MAX86150_REG_SYS_CTRL, MAX86150_SYS_SHDN);
+}
+
+static int max86150_chip_init(struct max86150_data *data)
+{
+ int ret;
+
+ /* Software reset; the bit self-clears within 1 ms */
+ ret = regmap_write(data->regmap, MAX86150_REG_SYS_CTRL,
+ MAX86150_SYS_RESET);
+ if (ret)
+ return ret;
+ usleep_range(1000, 2000);
+
+ /*
+ * FIFO: no sample averaging, rollover enabled, assert A_FULL when
+ * 17 samples are in the FIFO (32 - 15 = 17 available to read).
+ */
+ ret = regmap_write(data->regmap, MAX86150_REG_FIFO_CONFIG,
+ MAX86150_FIFO_ROLLOVER_EN |
+ FIELD_PREP(MAX86150_FIFO_A_FULL,
+ MAX86150_FIFO_A_FULL_VAL));
+ if (ret)
+ return ret;
+
+ /* Slot 1 = PPG Red (LED1), Slot 2 = PPG IR (LED2) */
+ ret = regmap_write(data->regmap, MAX86150_REG_FIFO_DCTRL1,
+ FIELD_PREP(MAX86150_FIFO_FD1, MAX86150_FD_LED1) |
+ FIELD_PREP(MAX86150_FIFO_FD2, MAX86150_FD_LED2));
+ if (ret)
+ return ret;
+
+ /* Slot 3 = ECG, Slot 4 = disabled */
+ ret = regmap_write(data->regmap, MAX86150_REG_FIFO_DCTRL2,
+ FIELD_PREP(MAX86150_FIFO_FD3, MAX86150_FD_ECG) |
+ FIELD_PREP(MAX86150_FIFO_FD4, MAX86150_FD_NONE));
+ if (ret)
+ return ret;
+
+ /* PPG: 100 Hz sample rate, 16384 nA ADC full-scale range */
+ ret = regmap_write(data->regmap, MAX86150_REG_PPG_CONFIG1,
+ FIELD_PREP(MAX86150_PPG_ADC_RGE,
+ MAX86150_PPG_ADC_RGE_16384) |
+ FIELD_PREP(MAX86150_PPG_SR,
+ MAX86150_PPG_SR_100HZ));
+ if (ret)
+ return ret;
+
+ /* LED pulse amplitudes (~50 mA) */
+ ret = regmap_write(data->regmap, MAX86150_REG_LED1_PA,
+ MAX86150_LED_PA_DEFAULT);
+ if (ret)
+ return ret;
+
+ return regmap_write(data->regmap, MAX86150_REG_LED2_PA,
+ MAX86150_LED_PA_DEFAULT);
+ /*
+ * Do NOT enable the A_FULL interrupt here. It is enabled and
+ * disabled via max86150_set_trigger_state() when the IIO triggered
+ * buffer is started and stopped, which prevents the edge-triggered
+ * interrupt line from asserting before the handler is registered.
+ */
+}
+
+static void max86150_trigger_put(void *trig)
+{
+ iio_trigger_put(trig);
+}
+
+/* Probe */
+
+static int max86150_probe(struct i2c_client *client)
+{
+ struct iio_dev *indio_dev;
+ struct max86150_data *data;
+ unsigned int part_id;
+ int ret;
+
+ indio_dev = devm_iio_device_alloc(&client->dev, sizeof(*data));
+ if (!indio_dev)
+ return -ENOMEM;
+
+ data = iio_priv(indio_dev);
+ data->dev = &client->dev;
+
+ /*
+ * Enable power supplies before any I2C access. Both are optional
+ * in the device tree (devm_regulator_get_enable returns 0 when the
+ * supply is not described), but must be present and stable on real
+ * hardware before the first register read.
+ */
+ ret = devm_regulator_get_enable(&client->dev, "vdd");
+ if (ret)
+ return dev_err_probe(&client->dev, ret,
+ "Failed to get/enable vdd supply\n");
+
+ ret = devm_regulator_get_enable(&client->dev, "leds");
+ if (ret)
+ return dev_err_probe(&client->dev, ret,
+ "Failed to get/enable leds supply\n");
+
+ data->regmap = devm_regmap_init_i2c(client, &max86150_regmap_config);
+ if (IS_ERR(data->regmap))
+ return dev_err_probe(&client->dev, PTR_ERR(data->regmap),
+ "Failed to initialise regmap\n");
+
+ ret = regmap_read(data->regmap, MAX86150_REG_PART_ID, &part_id);
+ if (ret)
+ return dev_err_probe(&client->dev, ret,
+ "Cannot read part ID\n");
+
+ if (part_id != MAX86150_PART_ID_VAL)
+ return dev_err_probe(&client->dev, -ENODEV,
+ "Unexpected part ID 0x%02x (expected 0x%02x)\n",
+ part_id, MAX86150_PART_ID_VAL);
+
+ ret = max86150_chip_init(data);
+ if (ret)
+ return dev_err_probe(&client->dev, ret,
+ "Chip initialisation failed\n");
+
+ ret = devm_add_action_or_reset(&client->dev, max86150_powerdown, data);
+ if (ret)
+ return ret;
+
+ indio_dev->name = "max86150";
+ indio_dev->channels = max86150_channels;
+ indio_dev->num_channels = ARRAY_SIZE(max86150_channels);
+ indio_dev->info = &max86150_iio_info;
+ indio_dev->modes = INDIO_DIRECT_MODE;
+
+ /*
+ * If the device tree provides an interrupt, set up a hardware
+ * trigger so userspace can use the FIFO almost-full signal to
+ * drive capture without polling.
+ */
+ if (client->irq > 0) {
+ data->trig = devm_iio_trigger_alloc(&client->dev,
+ "%s-dev%d",
+ indio_dev->name,
+ iio_device_id(indio_dev));
+ if (!data->trig)
+ return -ENOMEM;
+
+ data->trig->ops = &max86150_trigger_ops;
+ iio_trigger_set_drvdata(data->trig, indio_dev);
+
+ /*
+ * iio_trigger_generic_data_rdy_poll is a hard-IRQ handler
+ * that calls iio_trigger_poll() and returns IRQ_HANDLED.
+ * No IRQF_ONESHOT is needed for this edge-triggered line.
+ */
+ ret = devm_request_irq(&client->dev, client->irq,
+ iio_trigger_generic_data_rdy_poll,
+ IRQF_TRIGGER_FALLING,
+ "max86150", data->trig);
+ if (ret)
+ return dev_err_probe(&client->dev, ret,
+ "Cannot request IRQ %d\n",
+ client->irq);
+
+ ret = devm_iio_trigger_register(&client->dev, data->trig);
+ if (ret)
+ return dev_err_probe(&client->dev, ret,
+ "Cannot register trigger\n");
+
+ indio_dev->trig = iio_trigger_get(data->trig);
+ ret = devm_add_action_or_reset(&client->dev,
+ max86150_trigger_put,
+ data->trig);
+ if (ret)
+ return ret;
+ }
+
+ ret = devm_iio_triggered_buffer_setup(&client->dev, indio_dev,
+ iio_pollfunc_store_time,
+ max86150_trigger_handler,
+ NULL);
+ if (ret)
+ return dev_err_probe(&client->dev, ret,
+ "Cannot setup triggered buffer\n");
+
+ return devm_iio_device_register(&client->dev, indio_dev);
+}
+
+/* I2C driver table */
+
+static const struct i2c_device_id max86150_id[] = {
+ { "max86150" },
+ { }
+};
+MODULE_DEVICE_TABLE(i2c, max86150_id);
+
+static const struct of_device_id max86150_of_match[] = {
+ { .compatible = "maxim,max86150" },
+ { }
+};
+MODULE_DEVICE_TABLE(of, max86150_of_match);
+
+static struct i2c_driver max86150_driver = {
+ .driver = {
+ .name = "max86150",
+ .of_match_table = max86150_of_match,
+ },
+ .probe = max86150_probe,
+ .id_table = max86150_id,
+};
+module_i2c_driver(max86150_driver);
+
+MODULE_AUTHOR("Md Shofiqul Islam <shofiqtest@gmail.com>");
+MODULE_DESCRIPTION("MAX86150 ECG and PPG biosensor driver");
+MODULE_LICENSE("GPL");
--
2.51.1
^ permalink raw reply related [flat|nested] 9+ messages in thread
* [PATCH v2 3/3] MAINTAINERS: add entry for MAX86150 IIO health driver
2026-06-23 15:55 ` [PATCH v2 0/3] " Md Shofiqul Islam
2026-06-23 15:55 ` [PATCH v2 1/3] dt-bindings: iio: health: add maxim,max86150 Md Shofiqul Islam
2026-06-23 15:55 ` [PATCH v2 2/3] iio: health: add MAX86150 ECG and PPG biosensor driver Md Shofiqul Islam
@ 2026-06-23 15:55 ` Md Shofiqul Islam
2 siblings, 0 replies; 9+ messages in thread
From: Md Shofiqul Islam @ 2026-06-23 15:55 UTC (permalink / raw)
To: linux-iio
Cc: jic23, dlechner, nuno.sa, robh, krzk+dt, conor+dt, devicetree,
joshua.crofts1, Md Shofiqul Islam
Add maintainer entry for the new MAX86150 ECG and PPG biosensor driver.
Signed-off-by: Md Shofiqul Islam <shofiqtest@gmail.com>
---
MAINTAINERS | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/MAINTAINERS b/MAINTAINERS
index 3115538ce829..a441ec44bb27 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -15863,6 +15863,13 @@ S: Supported
F: Documentation/devicetree/bindings/power/supply/maxim,max77976.yaml
F: drivers/power/supply/max77976_charger.c
+MAXIM MAX86150 ECG AND PPG BIOSENSOR DRIVER
+M: Md Shofiqul Islam <shofiqtest@gmail.com>
+L: linux-iio@vger.kernel.org
+S: Maintained
+F: Documentation/devicetree/bindings/iio/health/maxim,max86150.yaml
+F: drivers/iio/health/max86150.c
+
MAXIM MUIC CHARGER DRIVERS FOR EXYNOS BASED BOARDS
M: Krzysztof Kozlowski <krzk@kernel.org>
L: linux-pm@vger.kernel.org
--
2.51.1
^ permalink raw reply related [flat|nested] 9+ messages in thread
end of thread, other threads:[~2026-06-23 15:56 UTC | newest]
Thread overview: 9+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-06-23 14:01 [PATCH 0/1] iio: health: add MAX86150 ECG and PPG biosensor driver Md Shofiqul Islam
2026-06-23 14:01 ` [PATCH 1/1] " Md Shofiqul Islam
2026-06-23 14:14 ` sashiko-bot
2026-06-23 14:38 ` Joshua Crofts
2026-06-23 15:38 ` Krzysztof Kozlowski
2026-06-23 15:55 ` [PATCH v2 0/3] " Md Shofiqul Islam
2026-06-23 15:55 ` [PATCH v2 1/3] dt-bindings: iio: health: add maxim,max86150 Md Shofiqul Islam
2026-06-23 15:55 ` [PATCH v2 2/3] iio: health: add MAX86150 ECG and PPG biosensor driver Md Shofiqul Islam
2026-06-23 15:55 ` [PATCH v2 3/3] MAINTAINERS: add entry for MAX86150 IIO health driver Md Shofiqul Islam
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox