element14 Community
element14 Community
    Register Log In
  • Site
  • Search
  • Log In Register
  • Community Hub
    Community Hub
    • What's New on element14
    • Feedback and Support
    • Benefits of Membership
    • Personal Blogs
    • Members Area
    • Achievement Levels
  • Learn
    Learn
    • Ask an Expert
    • eBooks
    • element14 presents
    • Learning Center
    • Tech Spotlight
    • STEM Academy
    • Webinars, Training and Events
    • Learning Groups
  • Technologies
    Technologies
    • 3D Printing
    • FPGA
    • Industrial Automation
    • Internet of Things
    • Power & Energy
    • Sensors
    • Technology Groups
  • Challenges & Projects
    Challenges & Projects
    • Design Challenges
    • element14 presents Projects
    • Project14
    • Arduino Projects
    • Raspberry Pi Projects
    • Project Groups
  • Products
    Products
    • Arduino
    • Avnet & Tria Boards Community
    • Dev Tools
    • Manufacturers
    • Multicomp Pro
    • Product Groups
    • Raspberry Pi
    • RoadTests & Reviews
  • About Us
    About the element14 Community
  • Store
    Store
    • Visit Your Store
    • Choose another store...
      • Europe
      •  Austria (German)
      •  Belgium (Dutch, French)
      •  Bulgaria (Bulgarian)
      •  Czech Republic (Czech)
      •  Denmark (Danish)
      •  Estonia (Estonian)
      •  Finland (Finnish)
      •  France (French)
      •  Germany (German)
      •  Hungary (Hungarian)
      •  Ireland
      •  Israel
      •  Italy (Italian)
      •  Latvia (Latvian)
      •  
      •  Lithuania (Lithuanian)
      •  Netherlands (Dutch)
      •  Norway (Norwegian)
      •  Poland (Polish)
      •  Portugal (Portuguese)
      •  Romania (Romanian)
      •  Russia (Russian)
      •  Slovakia (Slovak)
      •  Slovenia (Slovenian)
      •  Spain (Spanish)
      •  Sweden (Swedish)
      •  Switzerland(German, French)
      •  Turkey (Turkish)
      •  United Kingdom
      • Asia Pacific
      •  Australia
      •  China
      •  Hong Kong
      •  India
      •  Japan
      •  Korea (Korean)
      •  Malaysia
      •  New Zealand
      •  Philippines
      •  Singapore
      •  Taiwan
      •  Thailand (Thai)
      •  Vietnam
      • Americas
      •  Brazil (Portuguese)
      •  Canada
      •  Mexico (Spanish)
      •  United States
      Can't find the country/region you're looking for? Visit our export site or find a local distributor.
  • Translate
  • Profile
  • Settings
Smart Home & Healthcare Challenge
  • Challenges & Projects
  • Design Challenges
  • Smart Home & Healthcare Challenge
  • More
  • Cancel
Smart Home & Healthcare Challenge
Forum VitaRF - Part 2 - Interface MAX30102 on NRF52
  • News
  • Projects
  • Forum
  • Details
  • Leaderboard
  • Files
  • Members
  • More
  • Cancel
  • New
Join Smart Home & Healthcare Challenge to participate - click to join for free!
Actions
  • Share
  • More
  • Cancel
Forum Thread Details
  • Replies 5 replies
  • Subscribers 18 subscribers
  • Views 93 views
  • Users 0 members are here
  • design challenge
  • maxrefdes117
  • e14-smart home & healthcare
  • nordic
  • nrf
  • max30102
Related

VitaRF - Part 2 - Interface MAX30102 on NRF52

arvindsa
arvindsa 1 day ago

Recap: I am building a wireless vitals sensor node (heart rate, SpO₂, temperature) that reports over BLE into Home Assistant. This way we smartly keep watch over our loved ones. It is built up in three phases — DK bring-up, a custom miniaturized nRF52832 PCB, then a Matter over Thread proof of concept.

Past Posts

  1. VitaRF - Part 1 - The Plan

Generic MAX30102 breakout

The sponsored kit is still on it's way, In-fact It is gonna be delivered tomorrow, but I had some spare time and a cheap generic MAX30102 breakout so started firmware bring-up early. Same underlying MAX30102 die as the MAXREFDES117, so It should work the same.

Pin Function Notes
VIN Power in 1.8–5V
GND Ground  
SCL I²C clock to nRF52832 DK
SDA I²C data to nRF52832 DK
INT Interrupt output active-low, open-drain; needs a pull-up
IRD IR LED driver override Overrise Pin
RD Red LED driver override Override Pin

image

There was one minor difference - the breakout exposes two extra pins, IRD and RD, that the official MAXREFDES117 doesn't have at all. These are manual override test points wired straight to the chip's internal LED driver outputs, for bypassing the chip's own current control and driving the LEDs externally. Left both floating as per https://circuitdigest.com/microcontroller-projects/how-max30102-pulse-oximeter-and-heart-rate-sensor-works-and-how-to-interface-with-arduino

How PPG actually works

The sensor has two LEDs one Red ~660nm wavelength and another Infra-Red with ~880nm wavelength. They shine into the fingertip, and a photodiode on the same side measures how much light comes back. Oxygenated and Deoxygenated blood reflects the light differently. So by measuring the intensity as well as the rhythemic change in Intensity as the heart beats, you can measure the heartreate. Once we get the base line, we move on to the Blood Oxygen. Comparing the AC/DC ratio of the Red channel against the AC/DC ratio of the IR channel gives a value that gives us the blood oxygen saturation.

Interfacing the Sensor

When the sensor is I2C, the easiest way to break ice is with an I2C Scanner, Thankfully, the sensor was not shy and responded ACK to my scan.

Then I wrote a register readback test: set LED1_PA (Red) and LED2_PA (IR) alternately, reading each register back after every write to confirm the write. Red flashing was visible by eye; IR was not visible, So I took it for granted.

Next was draining the sensor's FIFO continuously and logging raw Red/IR sample pairs. The raw PPG data streamed cleanly, and a plain peak-counting script on the captured trace estimated ~78 BPM

 

image

Porting SparkFun's Library

I used SparkFun's spo2_algorithm which is an derivative of Maxim's original MAXREFDES117 reference algorithm/

It has one entry point.

void maxim_heart_rate_and_oxygen_saturation(
    uint32_t *pun_ir_buffer, int32_t n_ir_buffer_length,
    uint32_t *pun_red_buffer,
    int32_t *pn_spo2, int8_t *pch_spo2_valid,
    int32_t *pn_heart_rate, int8_t *pch_hr_valid
);

You hand it a buffer of IR samples and a buffer of Red samples (BUFFER_SIZE, which is FreqS * 4 — 4 seconds' worth at whatever sample rate FreqS is set to, 25Hz by default), and it hands back a heart rate and an SpO₂ estimate, each with its own valid flag.

Heart rate comes from straightforward valley detection on the IR trace:

// calculates DC mean and subtract DC from ir
un_ir_mean = 0;
for (k = 0; k < n_ir_buffer_length; k++) un_ir_mean += pun_ir_buffer[k];
un_ir_mean = un_ir_mean / n_ir_buffer_length;

// remove DC and invert signal so that we can use peak detector as valley detector
for (k = 0; k < n_ir_buffer_length; k++)
  an_x[k] = -1 * (pun_ir_buffer[k] - un_ir_mean);

// 4 pt Moving Average
for (k = 0; k < BUFFER_SIZE - MA4_SIZE; k++) {
  an_x[k] = (an_x[k] + an_x[k+1] + an_x[k+2] + an_x[k+3]) / (int)4;
}

Subtract the DC baseline, invert the signal, smooth it with a 4-point moving average, then run a peak finder on the inverted trace — since a pulse is a dip followed by a rise in raw IR, flipping it turns valleys into peaks so the same peak finder can be reused. Peak spacing directly gives heart rate: (FreqS * 60) / average_samples_between_peaks.

SpO2 is the part that actually needs both LED colors: it finds the AC and DC component of both the Red and IR channel between consecutive valleys, computes their ratio, and looks the result up in a precomputed table (uch_spo2_table, 184 entries) rather than evaluating a curve-fit formula directly — the comment in the original source says this is because register overflow made the direct formula inaccurate on the small microcontrollers (Arduino Uno / ARM M0/M3) it originally targeted.

The actual port is as below

// spo2_algorithm.h — before (Arduino):
#include <Arduino.h>
...
const uint8_t uch_spo2_table[184] = { ... };
static int32_t an_x[BUFFER_SIZE];
static int32_t an_y[BUFFER_SIZE];

// after (bare-metal, this repo):
#include <stdint.h>
#ifndef min
#define min(x,y) ((x) < (y) ? (x) : (y))
#endif
...
static const uint8_t uch_spo2_table[184] = { ... };   // static added
static int32_t an_x[BUFFER_SIZE] __attribute__((unused));  // silences -Werror
static int32_t an_y[BUFFER_SIZE] __attribute__((unused));  // in the non-owning TU

Wiring it into the firmware meant configuring the sensor to actually produce 25Hz-equivalent samples from the raw ADC that runs at 100sps, then feeding it with SparkFun's own reference example's sliding-window pattern rather than a fresh buffer each time:

// initial 4-second (100-sample) fill, once
for (int32_t i = 0; i < BUFFER_SIZE; i++)
    max30102_collect_one_sample(&red_buffer[i], &ir_buffer[i]);

maxim_heart_rate_and_oxygen_saturation(ir_buffer, BUFFER_SIZE, red_buffer,
    &spo2, &spo2_valid, &heart_rate, &hr_valid);

while (true) {
    // drop the oldest 25 samples (1s), shift the remaining 75 down
    for (int32_t i = 25; i < BUFFER_SIZE; i++) {
        red_buffer[i - 25] = red_buffer[i];
        ir_buffer[i - 25]  = ir_buffer[i];
    }
    // collect 25 new samples (1s) to refill the window
    for (int32_t i = BUFFER_SIZE - 25; i < BUFFER_SIZE; i++)
        max30102_collect_one_sample(&red_buffer[i], &ir_buffer[i]);

    maxim_heart_rate_and_oxygen_saturation(ir_buffer, BUFFER_SIZE, red_buffer,
        &spo2, &spo2_valid, &heart_rate, &hr_valid);
}

image

image

With finger on sensor: Sp02 locked at 98–100%, HR settling around 70–90 BPM which means I made productive use of the spare time.

What's next

MAX30208 (temperature) bring-up is the next item. I should also check if the MAXREFDES117 also works with the same code and compare it with an actual PPG device.

  • Sign in to reply
  • Cancel

Top Replies

  • arvindsa
    arvindsa 1 day ago in reply to embeddedguy +2
    I like the idea of using the mm wave but instead of using to figure out if a guy is alive, i can use it to find out if the heart rate sensor is saying false no heart rate scenario in cases where it fell…
Parents
  • embeddedguy
    embeddedguy 1 day ago

    Will you add, mmWave sensor? to monitor if person is alive?Stuck out tongue

    • Cancel
    • Vote Up 0 Vote Down
    • Sign in to reply
    • Cancel
Reply
  • embeddedguy
    embeddedguy 1 day ago

    Will you add, mmWave sensor? to monitor if person is alive?Stuck out tongue

    • Cancel
    • Vote Up 0 Vote Down
    • Sign in to reply
    • Cancel
Children
  • arvindsa
    arvindsa 1 day ago in reply to embeddedguy

    I like the idea of using the mm wave but instead of using to figure out if a guy is alive, i can use it to find out if the heart rate sensor is saying false no heart rate scenario in cases where it fell off or not attached properly..

    • Cancel
    • Vote Up +2 Vote Down
    • Sign in to reply
    • Cancel
  • embeddedguy
    embeddedguy 11 hours ago in reply to arvindsa

    Yes, because I know that you have couple of them.Smile

    • Cancel
    • Vote Up 0 Vote Down
    • Sign in to reply
    • Cancel
  • arvindsa
    arvindsa 8 hours ago in reply to embeddedguy

    Oh yes I know. Smiley

    • Cancel
    • Vote Up 0 Vote Down
    • Sign in to reply
    • Cancel
element14 Community

element14 is the first online community specifically for engineers. Connect with your peers and get expert answers to your questions.

  • Members
  • Learn
  • Technologies
  • Challenges & Projects
  • Products
  • Store
  • About Us
  • Feedback & Support
  • FAQs
  • Terms of Use
  • Privacy Policy
  • Legal and Copyright Notices
  • Sitemap
  • Cookies

An Avnet Company © 2026 Premier Farnell Limited. All Rights Reserved.

Premier Farnell Ltd, registered in England and Wales (no 00876412), registered office: Farnell House, Forge Lane, Leeds LS12 2NE.

Follow element14

  • X
  • Facebook
  • linkedin
  • YouTube