Title: Unstable and incorrect heart-rate readings from ProtoCentral MAX30001 with nRF52840 DK and Zephyr

Hello,

I am trying to integrate a ProtoCentral MAX30001 ECG/BioZ breakout board with an nRF52840 DK using the nRF Connect SDK and the ProtoCentral Zephyr driver:

https://github.com/Protocentral/protocentral_max30001_zephyr_driver

My environment is:

  • Board: nRF52840 DK
  • nRF Connect SDK: v3.3.1
  • Zephyr: v4.3.99
  • Target: nrf52840dk/nrf52840
  • MAX30001 communication: SPI Mode 0, 1 MHz
  • Breakout supply: 3 V from the nRF52840 DK

My wiring is:

  • MAX30001 SCK → P1.15
  • MAX30001 MOSI → P1.13
  • MAX30001 MISO → P1.14
  • MAX30001 CS → P1.12
  • MAX30001 INT1 → P1.10
  • MAX30001 VCC → 3.3 V
  • MAX30001 GND → GND
  • FCLK is not connected because the breakout has an onboard clock source
  • INT2 is not connected

SPI communication appears to work correctly. I can read the device ID consistently:

MAX30001 ID: 52 00 00
MAX30001 detected, ID=0x52

The original Zephyr driver only accepted 0x54, so I changed the ID check to accept both 0x52 and 0x54:

if ((chip_id[0] != 0x52) &&
    (chip_id[0] != 0x54)) {
	LOG_ERR("MAX30001 not found, ID=0x%02X", chip_id[0]);
	return -ENODEV;
}

The sensor initializes, ECG and R-to-R detection start, and I can read heart-rate values. However, the results are very unstable. My actual heart rate is approximately 76 BPM, but the output jumps between values such as:

HR = 58 BPM
HR = 119 BPM
HR = 60 BPM
HR = 187 BPM
HR = 33 BPM
HR = 170 BPM
HR waiting...

Sometimes the same value is printed repeatedly. Looking at the driver, SENSOR_CHAN_HR appears to return the cached lastHR value, even if a new RRINT event has not occurred:

case SENSOR_CHAN_HR:
	val->val1 = data->lastHR;
	break;

The HR calculation in the driver is:

data->lastRRI = (uint16_t)(max30001_rtor >> 10) * 8;
data->lastHR = (uint16_t)(60 * 1000 / data->lastRRI);

Therefore, I am not sure how the application should distinguish a new heartbeat from a previously cached HR value.

I initially used the overlay configuration from the driver README:

max30001: max30001@0 {
	compatible = "maxim,max30001";
	status = "okay";
	reg = <0>;
	spi-max-frequency = <1000000>;

	intb-gpios = <&gpio1 10 GPIO_ACTIVE_LOW>;

	ecg-enabled;
	rtor-enabled;
	ecg-gain = <3>;
	ecg-invert;
};

According to the binding file, ecg-gain = <3> means 160 V/V. I suspect this may be amplifying mains noise and motion artifacts. I am now planning to test this configuration:

max30001: max30001@0 {
	compatible = "maxim,max30001";
	status = "okay";
	reg = <0>;
	spi-max-frequency = <1000000>;

	intb-gpios = <&gpio1 10 GPIO_ACTIVE_LOW>;

	ecg-enabled;
	rtor-enabled;
	ecg-gain = <0>;
};

This removes ecg-invert, disables BioZ, and selects 20 V/V ECG gain.

I am using three electrodes with the following placement:

  • LA: below the left collarbone
  • RA: below the right collarbone
  • RL: lower-right abdomen

The raw ECG waveform is also quite noisy and appears to contain significant mains-frequency interference. The R-to-R detector may therefore be interpreting noise or motion artifacts as R peaks.

I would appreciate help with the following questions:

  1. Is 0x52 a valid expected device ID for this ProtoCentral MAX30001 breakout revision?
  2. What ECG gain and polarity settings are recommended for an initial three-electrode heart-rate test?
  3. Should internal R-bias be enabled or disabled when using the breakout’s LA, RA, and RL connections?
  4. Are CNFG_RTOR1 = 0x3FC600 and the driver’s default RTOR settings suitable for this setup?
  5. How should a Zephyr application determine whether a new R-to-R event occurred instead of repeatedly reading the cached lastHR value?
  6. Should INT1 generate a GPIO interrupt for every new R-to-R event, or is polling the STATUS register every 8 ms acceptable?
  7. Has this driver been tested with nRF Connect SDK v3.3.1 / Zephyr v4.3.99? The repository mentions testing with Zephyr 3.5 and 3.6.
  8. Are there any known issues in this driver that could cause incorrect RTOR or ECG FIFO decoding?

The SPI bus and chip detection appear to be working, so I believe the remaining problem is related to ECG signal quality, register configuration, or the driver’s handling of new R-to-R events.

Thank you for any guidance.  

Related