Week 09 – Input Devices

This week is about input devices that can be connected to microcontrollers to measure real-world phenomena such as motion, temperature, light, sound, pressure, humidity, distance, acceleration, orientation, and electric or magnetic fields.

The goal is to add a sensor to my microcontroller that I built last week and then sense data from the surrounding environment mostly sense data that are related to my final project input and read it.


Assignment Overview

Group Task:
  • Probe an input device's analog levels and digital signals
Individual Task:
  • Measure something: add a sensor to a microcontroller board that you have designed and read it

Weekly Activity Log
Day Activity Status
Thursday Week Planning πŸ“…, Research πŸ“– on input devices for my project In progress
Friday Holiday Holiday
Monday Sensor Documentation Completed
Designed Breakout Board for MAX30100 module Completed
Tuesday Regional Review πŸ‘¨β€πŸ«πŸ’¬ Completed
Breakout Board Milling πŸ›  Completed
Sensor Testing πŸ›  Completed
Documentation Completed
Sensor Calibration and Accuracy πŸ›  In progress
Wednesday Go the Extra Mile - On Final Projectβš™οΈ Pending
NuEval - Final Documentation 🏁 Pending
Make instagram Reel πŸŽ¬πŸš€ Pending

Group Assignment

In this week’s group assignment, we used input device rotary encoder module to prob and understand its digital signals and we probed joystick module sensor to understand analog levels, and we also understand usage of an oscilloscope and its different functions such as Trigger.

Group Assignment


Individual Assignment

This week, I am designed, fabricated and assembled a custom PCB breakout board for MAX30100/30102 (integrated pulse oximeter and heart-rate monitor module).

The MAX30100/30102 is an integrated pulse oximetry and heart-rate monitor module. It includes internal LEDs, photodetectors, optical elements, and low-noise electronics with ambient light rejection. The MAX30100/30102 provides a complete system solution to ease the design-in process for mobile and wearable devices.

In my final project (computational textile) I just don't want to blink the LEDs, I want it to blink based on concept of affective computing - to detect, interpret, process, and respond to human emotions. Thus, MAX30100/30102 becomes one of the sensors for me to derive human emotions.

MAX30100/30102 - Heart-Rate Sensor for Wearable Health

Working Principle

It works on the principle of photo-plethysmo-graphy (PPG). It emits red (660nm) and infrared (940nm) light into the skin and uses a photodetector to measure changes in light absorption caused by blood pulses, which determines heart rate.

MAX30100/30102

IC MAX30100 Datasheet Vs.IC MAX30102 Datasheet

System Block Diagram

MAX30100 is low-power, I2C-based module can measure both heart rate.

Including presence detection, temperature reading, and heart rate, beat by beat!

The module has MAX30100 IC that has one light source as a RED LED and IR LED these LEDs throw light into our skin and a photoreceptor detects and measures the light that has bounces back. Special optics and signal processing β€” that make sure the measurements are accurate. The RED and IR LED works on 3.3V and the main chip works on 1.8V thus the module has two different voltage regulator. The MAX30100 module consumes really less current , in normal operation it consumes 600uA, in standby mode (not actively measuring) it consumes 0.7uA. The in-build temp sensors which measure IC internal temp to have precise results. It has I2C interface with fixed I2C addresses, 0xAE for writing data and 0xAF for reading data. The MAX30100 collects data faster than a microcontroller can process it thus there is FIFO buffer (First In, First Out) to hold the data and prevent from data loss. It works like temporary storage while the main controller is busy. The buffer can hold up to 16 sets of readings. This gives the microcontroller plenty of time to finish other tasks before coming back for the data.

Power-up / Detecting the sensor

I checked IΒ²C scanner first to confirm the sensor is detected before writing any sensor code, since the sensor module was not having the JST connector, I used jumper wires to connect the sensor to my board.

Arduino Code

          #include   // Include I2C communication library

          void setup() {

            Serial.begin(115200);  
            // Start serial communication at 115200 baud rate

            pinMode(22, INPUT_PULLUP);  
            // Enable internal pull-up resistor on SDA pin (GPIO22)

            pinMode(23, INPUT_PULLUP);  
            // Enable internal pull-up resistor on SCL pin (GPIO23)

            Wire.begin(22, 23);  
            // Initialize I2C communication, SDA = GPIO22, SCL = GPIO23

            Serial.println("Scanning I2C bus...");  

            // Loop through all valid I2C addresses (1 to 126)
            for (byte addr = 1; addr < 127; addr++) {

              Wire.beginTransmission(addr);  
              // Start communication with device at this address

              if (Wire.endTransmission() == 0) {  
                // endTransmission() returns 0 if device responds (ACK)

                Serial.print("Device found at 0x");  

                Serial.println(addr, HEX);  
                // Print address in hexadecimal format (common for I2C)
              }
            }

            Serial.println("Done.");  // Indicate scan is complete
          }

          void loop() {
          }
        

This result shows that the sensor module is connected to I2C address 0X57.

Version Check MAX30100 or MAX30102

Next, I did is to check the version or part number of the sensor, whether it is MAX30100 or MAX30102?

AI prompt: "Can you please give code to read the Part ID register over IΒ²C, to confirm which IC it is MAX30100 or MAX30102?"

Arduino Code

          #include   
          // Include I2C library for communication

          #define MAX3010x_ADDRESS  0x57  
          // I2C address of MAX3010x sensors (MAX30100, MAX30102, etc.)

          #define PART_ID_REG       0xFF  
          // Register address that stores the chip's Part ID

          // Function to read 1 byte from a specific register of a device
          uint8_t readRegister(uint8_t address, uint8_t reg) {

            Wire.beginTransmission(address);  
            // Start communication with device at given I2C address

            Wire.write(reg);  
            // Tell the device which register we want to read

            Wire.endTransmission(false);        
            // Send the register address but KEEP connection active
            // 'false' = repeated start (important for proper I2C read sequence)

            Wire.requestFrom(address, (uint8_t)1);  
            // Request 1 byte of data from the device

            if (Wire.available()) {  
              // Check if data is available to read

              return Wire.read();  
              // Read and return the byte from the register
            }

            return 0x00;  
            // If no data received, return 0 (indicates failure/no response)
          }

          void setup() {

            Serial.begin(115200);  
            // Start serial communication for debugging output

            delay(1000);  
            // Small delay to allow Serial Monitor to initialize

            pinMode(22, INPUT_PULLUP);          
            // Enable internal pull-up resistor on SDA (GPIO22)

            pinMode(23, INPUT_PULLUP);          
            // Enable internal pull-up resistor on SCL (GPIO23)

            Wire.begin(22, 23);  
            // Initialize I2C with SDA = 22, SCL = 23

            Serial.println("Reading MAX3010x Part ID register (0xFF)...");  
            Serial.println("------------------------------------------");

            // Read the Part ID register (0xFF) from sensor at address 0x57
            uint8_t partID = readRegister(MAX3010x_ADDRESS, PART_ID_REG);

            Serial.print("Raw Part ID value: 0x");
            Serial.println(partID, HEX);  
            // Print the value in hexadecimal format

            Serial.println();

            // Check which chip is connected based on Part ID
            if (partID == 0x11) {
              Serial.println("Chip identified: MAX30100");
              Serial.println("Note: MAX30100 is discontinued.");
              Serial.println("Use library: oxullo/MAX30100lib");
            }

            else if (partID == 0x15) {
              Serial.println("Chip identified: MAX30102");
              Serial.println("Note: MAX30102 is the current active chip.");
              Serial.println("Use library: SparkFun MAX3010x");
            }

            else if (partID == 0x00) {
              Serial.println("No response from sensor.");
              Serial.println("Check wiring β€” SDA, SCL, VCC, GND.");
            }

            else {
              Serial.print("Unknown Part ID: 0x");
              Serial.println(partID, HEX);
              Serial.println("Could be MAX30101 or a clone IC.");
            }

            Serial.println("------------------------------------------");
          }

          void loop() {
            
          }
        

From serial monitor, we can see that this module is having Part ID : 0X11, which means it is MAX30100 IC module.

Measuring the Pulse

First, I installed arduino library MAX30100lib by oxullo and then flashed below code to get the readings.

Arduino Code

          #include   
          // I2C communication library

          #include "MAX30100_PulseOximeter.h"  
          // Library that handles MAX30100 sensor (heart rate + SpO2)

          // ── Tuning parameters ────────────────────────────────────────────
          #define REPORTING_PERIOD_MS   2000    
          // Print results every 2 seconds

          #define FINGER_THRESHOLD      3000    
          // If IR value is below this β†’ no finger detected

          #define LED_CURRENT           MAX30100_LED_CURR_11MA   
          // LED brightness (affects signal strength)

          #define SAMPLING_RATE         MAX30100_SAMPRATE_100HZ  
          // Sensor sampling rate (100 samples/sec)
          // ─────────────────────────────────────────────────────────────────

          // Create PulseOximeter object
          PulseOximeter pox;

          // Store last time data was printed
          uint32_t tsLastReport = 0;

          // Count number of detected heartbeats
          uint32_t beatCount    = 0;

          // Track whether finger is placed on sensor
          bool     fingerOn     = false;

          // Callback function: called automatically when a heartbeat is detected
          void onBeatDetected() {
            beatCount++;  
            // Increase beat counter

            Serial.println("  [BEAT " + String(beatCount) + "]");  
            // Print beat number
          }

          void setup() {

            Serial.begin(115200);  
            // Start serial communication

            delay(2000);                         
            // Wait 2 seconds β†’ allow sensor to stabilize after power ON

            pinMode(22, INPUT_PULLUP);  
            // Enable pull-up resistor on SDA

            pinMode(23, INPUT_PULLUP);  
            // Enable pull-up resistor on SCL

            Wire.begin(22, 23);  
            // Initialize I2C (SDA=22, SCL=23)

            Wire.setClock(100000);               
            // Set I2C speed to 100kHz (more stable than 400kHz)

            Serial.println("Initializing MAX30100...");

            if (!pox.begin()) {
              // Try to initialize sensor

              Serial.println("ERROR: MAX30100 not found. Check wiring.");

              while (1);  
              // Stop program if sensor not detected
            }

            // ── Sensor configuration ───────────────────────────────────────
            pox.setIRLedCurrent(LED_CURRENT);  
            // Set IR LED brightness

            pox.setSamplingRate(SAMPLING_RATE);  
            // Set how fast sensor collects data

            pox.setLEDsPulseWidth(MAX30100_SPC_PW_1600US_16BITS);  
            // Set pulse width β†’ longer pulse = better signal

            pox.setHighresModeEnabled(true);  
            // Enable 16-bit resolution (higher accuracy)
            // ──────────────────────────────────────────────────────────────

            pox.setOnBeatDetectedCallback(onBeatDetected);  
            // Register callback β†’ triggers when heartbeat detected

            Serial.println("MAX30100 ready. Place finger GENTLY on sensor.");
            Serial.println("────────────────────────────────────────────────");
          }

          void loop() {

            pox.update();                        
            // VERY IMPORTANT:
            // Continuously processes sensor data
            // Must run as fast as possible (no delay here)

            // ── Finger detection ──────────────────────────────────────────
            bool currentlyOn = (pox.getIR() > FINGER_THRESHOLD);
            // Check IR value β†’ if high, finger is present

            if (currentlyOn != fingerOn) {
              // If finger status changed (placed or removed)

              fingerOn = currentlyOn;

              if (fingerOn) {
                Serial.println("Finger detected β€” warming up (~5 sec)...");
                beatCount = 0;  
                // Reset beat count when finger placed
              } else {
                Serial.println("Finger removed.");
              }
            }
            // ─────────────────────────────────────────────────────────────

            // ── Periodic report ──────────────────────────────────────────
            if (millis() - tsLastReport > REPORTING_PERIOD_MS) {
              // Check if 2 seconds have passed

              tsLastReport = millis();  
              // Update last report time

              if (!fingerOn) {
                Serial.println("No finger detected. Place fingertip on sensor.");
                return;  
                // Skip rest if no finger
              }

              float bpm  = pox.getHeartRate();  
              // Get calculated heart rate

              uint8_t sp = pox.getSpO2();  
              // Get oxygen saturation %

              // Sanity filter β€” discard clearly wrong readings
              if (bpm < 40 || bpm > 200) {
                Serial.println("Reading unstable β€” keep finger still...");
                return;  
                // Ignore unrealistic values
              }

              Serial.print("Heart rate : ");
              Serial.print(bpm, 1);  
              // Print BPM with 1 decimal

              Serial.print(" bpm  |  SpO2 : ");
              Serial.print(sp);  
              // Print oxygen %

              Serial.print(" %  |  IR raw : ");
              Serial.println(pox.getIR());  
              // Print raw IR signal value
            }
            // ─────────────────────────────────────────────────────────────
          }
        

I can see that I was getting random reading on the serial monitor, there was not consistency in the reading and most of them were out of the range of normal human heart beat which is 60 bmp to 90 bpm.

What Went Wrong

As, I see in the serial monitor above the model is giving erratic beats per minute (bpm) readings. The readings are not stable, and the module is turning off randomly.

  • Random and not accurate BPM readings
  • Unstable readings
  • Module turns off randomly

The human hart rate (bpm) range in resting positing is between 60 - 100 bpm, whereas I am getting 35 bpm, 45 bpm, ans sometimes even 0 bpm and 150 bpm.

What I Learned

First learning, pull-up resisters of SDA and SCL shall be pull-up to 3.3V not 1.8V because my controller work on 3.3V so to have stable communication the pull-up shall be to 3.3V. Here, the MAX30100 IC module was having pull-up to 1.8V.

Desoldered the 4.7K pull-up resisters from SDA, SCL and INT

Secund learning, do not use jumper wires to connect sensor module to controller. It creates unstable contacts, and when touching the sensor module it, looses the power or ground and turns-off.

Do not use Jumper Wires

Third, MAX30100 is obsolete and the best and most accurate upgrade in that lineage is MAX30102 IC.

MAX30100 and MAX30102
MAX30100 vs MAX30102 Comparison
Feature πŸ§ͺ MAX30100 βš™οΈ MAX30102 ⭐
Basic Function Basic SpOβ‚‚ + heart rate Improved SpOβ‚‚ + heart rate sensing
Signal Quality Lower signal quality More stable and accurate PPG signal
Noise & Motion More noise and motion sensitivity Better ambient light rejection
Power Consumption Higher power usage Lower power consumption
Accuracy Less accurate BPM readings More accurate BPM readings
Status Now considered outdated Modern and widely used ⭐

Correction Steps

I removed the resisters from the MAX30100 module, so that I can pull-up in the XIAO-ESP32C6, to narrow down to the rootcause of erratic BPM readings.

Desoldered the 4.7K pull-up resisters from SDA, SCL and INT

Next, my instructor advised to design and make breakout board with 3.3 pull-up resisters connected to SDA, SCL and also INT pins, and also use JST 4P connector to connect the I2C between sensor module and my development board. so by doing this I can correct the pull-up resistors connect of sensor module and also eliminate the jumper wires for de-stable connectivity issue.

For the new design, I made two breakout board one for MAX30100 Heart rate sensor and another for IMU6050 module. Here I added 4.7K pull-up resisters so on to breakout board. Because the MAX30100 module has wrongly connect pull-up resisters to 1.8V instead of 3.3V. thus I removed the 4.7k smd resisters from the MAX30100 module.

Below are the design snaps of making breakout board for sensor.

Schematics
PCB Design
3D Preview

Then I milled the PCBs for detailed information on PCB milling / production please refer my week 8 documentation.

PCB Milling
PCB Soldering
Final Breakout Module

my instructor Saheen suggested to apply the Kapton Tape to insulate model’s electronics from disturbance, when placed on to the skin.

Kapton tape (polyimide tape)

Now, I tested again to check if the reading are more accurate and stable or not.

AI prompt: I tried above code, however it is giving erratic reading and sometimes get stuck at one reading.!

The key fixes in this version compared to the previous one:

Wire.setClock(100000) β€” forces IΒ²C to 100kHz (standard mode). Because the module's noisy pull-ups struggle at 400kHz fast mode, causing corrupted samples that produce wild readings.

pox.update() with no delay() in loop β€” the oxullo library needs update() called as fast as possible. Any delay() in the loop starves the algorithm of samples and causes it to get stuck.

Finger detection gate β€” readings below FINGER_THRESHOLD are suppressed so you don't get garbage values when no finger is present. Sanity filter on BPM β€” anything below 40 or above 200 is silently discarded while the algorithm stabilizes.

Finger placement tip β€” this is often the real culprit. Rest your fingertip very lightly on the sensor window β€” the pad of your index finger, not the tip. Do not press hard. Movement and pressure changes are the number one cause of erratic readings with this sensor.

Try Changing #define LED_CURRENT MAX30100_LED_CURR_11MA a higher value like - try MAX30100_LED_CURR_27_1MA or MAX30100_LED_CURR_33_8MA if readings are weak. The options in increasing brightness are:

  • MAX30100_LED_CURR_7_6MA
  • MAX30100_LED_CURR_11MA
  • MAX30100_LED_CURR_11MA
  • MAX30100_LED_CURR_14_2MA
  • MAX30100_LED_CURR_17_4MA
  • MAX30100_LED_CURR_20_8MA
  • MAX30100_LED_CURR_24MA
  • MAX30100_LED_CURR_27_1MA
  • MAX30100_LED_CURR_30_6MA
  • MAX30100_LED_CURR_33_8MA
  • MAX30100_LED_CURR_37MA
  • MAX30100_LED_CURR_40_2MA
  • MAX30100_LED_CURR_43_6MA
  • MAX30100_LED_CURR_46_8MA
  • MAX30100_LED_CURR_50MA
Arduino Code

          #include                         // Arduino I2C library β€” handles SDA/SCL communication
          #include "MAX30100_PulseOximeter.h"      // oxullo MAX30100 library β€” provides PulseOximeter class

          // ── Timing constants ──────────────────────────────────────────────────────────
          #define SAMPLE_PERIOD_MS    20           // call pox.update() and sample BPM every 20ms
          #define REPORT_PERIOD_MS    1000         // print to Serial Plotter once every 1 second
          #define WARMUP_MS           10000        // wait 10 seconds after finger detected before trusting readings

          // ── Smoothing constant ────────────────────────────────────────────────────────
          #define SMOOTH_FACTOR       0.15         // controls how fast display reacts to BPM changes
                                                  // 0.0 = never changes, 1.0 = no smoothing (jumpy)
                                                  // 0.15 = gentle smoothing β€” stable but still responsive

          // ── Beat-based BPM calculation ────────────────────────────────────────────────
          #define MIN_BEAT_INTERVAL_MS  300        // ignore beats faster than 300ms apart (> 200 BPM) β€” noise rejection
          #define MAX_BEAT_INTERVAL_MS  1500       // ignore beats slower than 1500ms apart (< 40 BPM) β€” noise rejection
          #define BEAT_HISTORY_SIZE     8          // number of recent beat intervals to average β€” more = smoother

          // ── Global objects ────────────────────────────────────────────────────────────
          PulseOximeter pox;                       // main interface object to MAX30100 sensor

          // ── Timing variables ──────────────────────────────────────────────────────────
          uint32_t tsLastSample    = 0;            // timestamp of last 20ms sample
          uint32_t tsLastReport    = 0;            // timestamp of last 1 second Serial print
          uint32_t tsFingerOn      = 0;            // timestamp of when finger was first placed on sensor
          uint32_t tsLastBeat      = 0;            // timestamp of last valid heartbeat detected

          // ── BPM calculation variables ─────────────────────────────────────────────────
          float    smoothedBPM     = 0;            // exponentially smoothed BPM β€” what gets printed to plotter
          float    beatIntervals[BEAT_HISTORY_SIZE]; // circular buffer storing last 8 beat-to-beat intervals in ms
          uint8_t  beatIndex       = 0;            // current write position in circular buffer (0 to 7)
          uint8_t  beatsFilled     = 0;            // how many slots in buffer have valid data (0 to 8)
          bool     fingerPresent   = false;        // true when finger is detected on sensor
          bool     warmedUp        = false;        // true after 10 second warmup period has passed

          // ── Beat callback β€” called automatically by library on every detected heartbeat
          void onBeatDetected() {
            uint32_t now      = millis();          // get current time in milliseconds
            uint32_t interval = now - tsLastBeat;  // calculate time since last beat in milliseconds

            if (tsLastBeat == 0) {                 // first beat ever β€” no previous timestamp to compare
              tsLastBeat = now;                    // just store timestamp, skip interval calculation
              return;                              // exit early β€” need at least 2 beats to get an interval
            }

            if (interval < MIN_BEAT_INTERVAL_MS || interval > MAX_BEAT_INTERVAL_MS) {
                                                  // interval is outside valid human heart rate range
                                                  // likely noise, motion artifact, or false detection
              tsLastBeat = now;                    // update timestamp but discard this interval
              return;                              // exit early β€” don't add bad data to buffer
            }

            beatIntervals[beatIndex] = interval;   // store valid interval in circular buffer at current position
            beatIndex  = (beatIndex + 1) % BEAT_HISTORY_SIZE;  
                                                  // advance write position β€” wraps back to 0 after reaching size 8
            if (beatsFilled < BEAT_HISTORY_SIZE) { // if buffer not yet completely filled
              beatsFilled++;                       // increment filled count until we reach BEAT_HISTORY_SIZE
            }

            tsLastBeat = now;                      // update last beat timestamp for next interval calculation
          }

          // ── Calculate BPM from stored beat intervals ──────────────────────────────────
          float calculateBPM() {
            if (beatsFilled == 0) return 0;        // no beats collected yet β€” return 0 (no reading)

            float sum = 0;                         // will accumulate sum of all valid intervals
            for (uint8_t i = 0; i < beatsFilled; i++) {  
                                                  // loop through all filled slots in buffer
              sum += beatIntervals[i];             // add each interval to sum
            }

            float avgInterval = sum / beatsFilled; // average interval in milliseconds between beats
            return 60000.0 / avgInterval;          // convert ms interval to BPM
                                                  // 60000ms = 1 minute, divide by avg interval = beats per minute
          }

          void setup() {
            Serial.begin(115200);                  // start Serial at 115200 baud β€” must match Serial Plotter baud rate
            delay(3000);                           // wait 3 seconds β€” gives MAX30100 stable power before init attempt

            Wire.begin(22, 23);                    // initialise I2C bus β€” GPIO22 = SDA, GPIO23 = SCL on XIAO ESP32-C6
            Wire.setClock(100000);                 // set I2C to 100kHz standard mode β€” reliable for this module

            while (!pox.begin()) {                 // keep retrying until MAX30100 responds β€” prevents silent failure
              Serial.println("MAX30100 not found β€” retrying...");  // inform user of retry
              delay(1000);                         // wait 1 second between retries
            }

            pox.setIRLedCurrent(MAX30100_LED_CURR_27_1MA);  
                                                  // set IR LED current to 27.1mA
                                                  // higher = stronger signal through finger tissue
                                                  // 27.1mA is optimal balance for most fingers

            pox.setOnBeatDetectedCallback(onBeatDetected);   
                                                  // register onBeatDetected() as beat callback
                                                  // library calls this function every time it detects a heartbeat peak

            Serial.println("MAX30100 ready β€” place finger gently on sensor");
          }

          void loop() {
            pox.update();                          // MUST be called every loop iteration β€” no delay() anywhere in loop
                                                  // reads new samples from MAX30100 hardware FIFO buffer
                                                  // runs peak detection algorithm on each sample
                                                  // triggers onBeatDetected() callback when beat found
                                                  // any delay() here starves algorithm and causes missed beats

            // ── Finger presence detection ──────────────────────────────────────────────
            float rawBPM = pox.getHeartRate();     // get library's raw BPM estimate β€” used only for finger detection here
            bool  currentFinger = (rawBPM > 0);   // if library returns any BPM > 0, finger is present on sensor

            if (currentFinger && !fingerPresent) { // finger just placed β€” was absent, now present
              fingerPresent = true;                // update finger state to present
              tsFingerOn    = millis();            // record time finger was placed β€” used for warmup countdown
              warmedUp      = false;               // reset warmup flag β€” need fresh 10 second warmup
              beatsFilled   = 0;                   // clear beat history β€” old data from previous session is invalid
              beatIndex     = 0;                   // reset circular buffer write position to start
              smoothedBPM   = 0;                   // reset smoothed value β€” start fresh for new session
              tsLastBeat    = 0;                   // reset last beat timestamp β€” first beat will set this
              Serial.println("Finger detected β€” warming up 10 seconds...");
            }

            if (!currentFinger && fingerPresent) { // finger just removed β€” was present, now absent
              fingerPresent = false;               // update finger state to absent
              warmedUp      = false;               // reset warmup β€” next placement needs fresh warmup
              Serial.println("Finger removed.");
            }

            // ── Check warmup completion ────────────────────────────────────────────────
            if (fingerPresent && !warmedUp) {      // finger is on but warmup not yet complete
              if (millis() - tsFingerOn > WARMUP_MS) {  
                                                  // check if 10 seconds have passed since finger was placed
                warmedUp = true;                   // warmup complete β€” readings are now trustworthy
                Serial.println("Warmup complete β€” reading BPM...");
              }
            }

            // ── Sample and smooth BPM every 20ms ──────────────────────────────────────
            if (millis() - tsLastSample > SAMPLE_PERIOD_MS) {  
                                                  // check if 20ms has passed since last sample
              tsLastSample = millis();             // reset 20ms sample timer

              if (fingerPresent && warmedUp) {     // only update smoothed value if finger on AND warmed up
                float bpm = calculateBPM();        // calculate BPM from our own beat interval buffer

                if (bpm > 40 && bpm < 200) {       // only accept physiologically valid BPM values
                  if (smoothedBPM == 0) {          // first valid reading after warmup
                    smoothedBPM = bpm;             // initialise smoothed value directly β€” no history to blend with
                  } else {
                    smoothedBPM = smoothedBPM + SMOOTH_FACTOR * (bpm - smoothedBPM);
                                                  // exponential moving average formula:
                                                  // new smoothed = old smoothed + 0.15 * (new reading - old smoothed)
                                                  // blends new reading gently into running average
                                                  // reduces effect of single noisy samples on displayed value
                  }
                }
              }
            }

            // ── Print to Serial Plotter every 1 second ────────────────────────────────
            if (millis() - tsLastReport > REPORT_PERIOD_MS) {  
                                                  // check if 1 second has passed since last print
              tsLastReport = millis();             // reset 1 second report timer

              if (!fingerPresent) {                // no finger on sensor
                Serial.println("BPM:0");          // print 0 β€” tells plotter signal is absent
                                                  // NOTE: this will show as a dip on plotter β€” expected behaviour
              } else if (!warmedUp) {              // finger present but still in warmup period
                uint32_t remaining = (WARMUP_MS - (millis() - tsFingerOn)) / 1000;
                                                  // calculate remaining warmup seconds
                Serial.print("BPM:0 β€” warming up, ");   // print 0 during warmup
                Serial.print(remaining);           // print remaining seconds
                Serial.println("s remaining...");  // complete the message
              } else {                             // finger present, warmed up, valid reading available
                Serial.print("BPM:");             // label for Serial Plotter line
                Serial.println(smoothedBPM, 1);   // print smoothed BPM with 1 decimal place
                                                  // println adds newline β€” tells plotter this data point is complete
              }
            }
          }
        
Hero shot
Measurement

What Went Wrong

I achieved stability in the reading however I am still not getting accurate reading, my module shows 85-92 BPM which is quite high. I have ordered the MAX30102 and I will try reading heart rate aging soon .......

Additionally, I will be adding sound sensor, and IMU sensor as they are related to my final project.

Hero Shot


OLD Designed board

Note: below is for my future reference, I designed SAMD21 developments board but did not provided for milling.

Actually, I started designing a new board with SAMD21E but later my instructor told that as I am not embedding or designing any input on the board so I shall not proceed with it and instead design a breakout board to plug the MAX30100/30102 module.

However, referring SAMD21 datasheet while designing the CKT was good experience and learning about also learned good points from its such as it has flexible peripheral system called SERCOM (Serial Communication) β€” unlike AVR chips where IΒ²C is fixed to specific pins, the SAMD21 lets you assign IΒ²C (and SPI, UART) to multiple pin combinations.

SDAM21 Datasheet and SAMD21E PIN Details

AI prompt: "Can you please explain me what is REF, ADC, AC, PTC, DAC in column B ?, can you please explain me what is TC/TCC in column E?, can you please explain me what is TCC in column F?, can you please explain me what is COM in column G?, can you please explain me what is AC/GCLK in column H?"

Schematics
PCB Design
3D of PCB

Key Learnings This Week

  • Always keep power and ground line fatter then signal lines. To do this go to Board Setup >> Net Class >> Add.
  • Board Setup >> Net Classes >> ADD new row for Power and GND
  • Remove island after PCB Milling because this islands can become bridges and can create short or unwanted connections.
  • Remove small islands
  • I need to try out thinner traves that acts as fuse, I can blow this traces with current and check.
  • Explore PID Control -It constantly watches what’s happening, compares it to what should happen, and nudges the system until reality matches intention.

Downloads & Useful Links

Reflection

To deal with accuracy and stability of sensor's reading, first fix hardware variations such as avoid jumpers wires (loose connections), use connectors and breakout board, use Kapton tape and minimize external disturbances and then do the software optimization such as using averaging or using pi-filters.

Most Important again keep things simple, I tried to add human emotions into my projects and added new sensors like pulse sensor and GSR. nby adding new sensors, I missed the chance to try IMU and Sound sensors which are related to basic functions to my project. I did not follow my week plan properly and unnecessary wasted 2 to 3 days in developing SAMD21E development board. Communicate frequently with Saheen.

Reflection Chair