Back to Embedded Systems Hardware Engineering Series

Capstone 7: Wearable Health Monitor

April 17, 2026 Wasil Zafar 40 min read

Design a medical-grade wearable that tracks heart rate, SpO2, and single-lead ECG with BLE 5.0 streaming, optimized for 7-day battery life on a 150 mAh cell.

Table of Contents

  1. System Specifications
  2. Hardware Architecture
  3. PPG & SpO2 Signal Chain
  4. ECG Analog Front-End
  5. Power Budget
  6. Sensor Config Planner
  7. Conclusion

System Specifications

ParameterSpecificationComponent
MCU + BLEnRF52840 (Cortex-M4F, 64 MHz)Nordic Semiconductor
PPG / SpO2Dual-LED (red + IR), 18-bit ADCMAX30102
ECGSingle-lead, instrumentation ampAD8232
IMU6-axis (accel + gyro)LSM6DSO
Display0.96″ OLED 128×64SSD1306 (I2C)
Battery150 mAh LiPo, BQ25180 chargerUSB-C charging
Target Life7 days continuous HR, 12h ECGPower management
EnclosureIP67, medical-grade silicone band38 mm watch form

Hardware Architecture

Wearable Health Monitor Block Diagram
flowchart TD
    A["150mAh LiPo"] --> B["BQ25180
Charger IC"] B --> C["TPS62740
1.8V Buck"] C --> D["nRF52840
BLE 5.0 SoC"] D -->|I2C| E["MAX30102
PPG/SpO2"] D -->|ADC| F["AD8232
ECG AFE"] D -->|SPI| G["LSM6DSO
6-axis IMU"] D -->|I2C| H["SSD1306
OLED Display"] D --> I["BLE 5.0
Smartphone App"] D --> J["QSPI Flash
4MB Log Storage"] style D fill:#3B9797,color:#fff style E fill:#BF092F,color:#fff

PPG & SpO2 Signal Chain

/* MAX30102 PPG/SpO2 driver — I2C interface on nRF52840
   Configures dual-LED mode for simultaneous HR + SpO2 */

#include <stdint.h>

#define MAX30102_ADDR         0x57
#define REG_FIFO_WR_PTR       0x04
#define REG_FIFO_DATA         0x07
#define REG_MODE_CONFIG        0x09
#define REG_SPO2_CONFIG        0x0A
#define REG_LED1_PA            0x0C  /* Red LED current */
#define REG_LED2_PA            0x0D  /* IR LED current */

typedef struct {
    uint32_t red_raw;     /* Red LED ADC (18-bit) */
    uint32_t ir_raw;      /* IR LED ADC (18-bit) */
    float    heart_rate;  /* BPM from peak detection */
    float    spo2;        /* SpO2 percentage */
    uint8_t  confidence;  /* Signal quality (0-100) */
} ppg_reading_t;

/* SpO2 calibration curve (empirical):
 * R = (AC_red / DC_red) / (AC_ir / DC_ir)
 * SpO2 = 110.0 - 25.0 * R
 * Valid for R ∈ [0.4, 1.0] → SpO2 ∈ [85%, 100%]
 */

/* Configuration for continuous HR + SpO2 mode:
 * - Sample rate: 100 Hz (50 Hz per LED)
 * - ADC range: 16384 (18-bit)
 * - LED pulse width: 411 µs (best SNR)
 * - Red LED: 6.4 mA, IR LED: 6.4 mA
 * - Average: 4 samples (effective 25 Hz output)
 */

ECG Analog Front-End

# ECG signal processing — R-peak detection and heart rate
# Simulates AD8232 output digitized by nRF52840 ADC (12-bit)

import numpy as np

# ECG parameters
sample_rate_hz = 250       # AD8232 → nRF52840 ADC
duration_sec = 10
n_samples = sample_rate_hz * duration_sec

# Simulate ECG-like signal (simplified PQRST complex)
t = np.arange(n_samples) / sample_rate_hz
heart_rate_bpm = 72
beat_period = 60.0 / heart_rate_bpm

# Generate synthetic ECG with R-peaks
ecg = np.zeros(n_samples)
for beat_time in np.arange(0, duration_sec, beat_period):
    idx = int(beat_time * sample_rate_hz)
    if idx + 20 < n_samples:
        ecg[idx + 5] = -0.15      # Q wave
        ecg[idx + 8] = 1.0        # R peak
        ecg[idx + 11] = -0.3      # S wave
        ecg[idx + 18] = 0.2       # T wave

# Simple R-peak detection (threshold-based)
threshold = 0.5
r_peaks = []
refractory = int(0.2 * sample_rate_hz)  # 200ms refractory period
last_peak = -refractory

for i in range(1, n_samples - 1):
    if ecg[i] > threshold and ecg[i] > ecg[i-1] and ecg[i] > ecg[i+1]:
        if (i - last_peak) > refractory:
            r_peaks.append(i)
            last_peak = i

# Calculate heart rate from R-R intervals
rr_intervals = np.diff(r_peaks) / sample_rate_hz
hr_values = 60.0 / rr_intervals

print("ECG Signal Processing Results")
print("=" * 50)
print(f"Sample rate:       {sample_rate_hz} Hz")
print(f"Duration:          {duration_sec} sec")
print(f"R-peaks detected:  {len(r_peaks)}")
print(f"Mean R-R interval: {np.mean(rr_intervals):.3f} sec")
print(f"Mean heart rate:   {np.mean(hr_values):.1f} BPM")
print(f"HR std deviation:  {np.std(hr_values):.2f} BPM")
print(f"HRV (SDNN):        {np.std(rr_intervals)*1000:.1f} ms")

Power Budget Analysis

# Wearable power budget — 150 mAh battery life estimation
# nRF52840 + MAX30102 + AD8232 + SSD1306 OLED

import numpy as np

components = {
    "nRF52840 (sleep)":       {"current_uA": 1.5,   "duty": 0.90},
    "nRF52840 (active)":      {"current_uA": 3000,  "duty": 0.05},
    "nRF52840 (BLE TX)":      {"current_uA": 5500,  "duty": 0.02},
    "nRF52840 (ADC ECG)":     {"current_uA": 800,   "duty": 0.03},
    "MAX30102 (HR mode)":     {"current_uA": 600,   "duty": 1.00},
    "MAX30102 (SpO2 burst)":  {"current_uA": 1200,  "duty": 0.00},
    "AD8232 (ECG standby)":   {"current_uA": 170,   "duty": 0.95},
    "AD8232 (ECG active)":    {"current_uA": 170,   "duty": 0.05},
    "LSM6DSO (low power)":    {"current_uA": 12,    "duty": 1.00},
    "SSD1306 (off)":          {"current_uA": 1,     "duty": 0.95},
    "SSD1306 (display on)":   {"current_uA": 8000,  "duty": 0.05},
    "QSPI Flash (standby)":   {"current_uA": 5,     "duty": 0.99},
    "QSPI Flash (write)":     {"current_uA": 15000, "duty": 0.01},
    "TPS62740 quiescent":     {"current_uA": 0.36,  "duty": 1.00},
    "BQ25180 quiescent":      {"current_uA": 0.5,   "duty": 1.00},
}

battery_mah = 150
efficiency = 0.88  # Buck converter efficiency

print("Wearable Health Monitor — Power Budget")
print("=" * 60)
print(f"{'Component':<28} {'Avg µA':>8}  {'Duty':>6}")
print("-" * 60)

total_ua = 0
for name, spec in components.items():
    avg = spec["current_uA"] * spec["duty"]
    total_ua += avg
    print(f"{name:<28} {avg:>8.1f}  {spec['duty']:>5.0%}")

system_ua = total_ua / efficiency
battery_hours = (battery_mah * 1000) / system_ua
battery_days = battery_hours / 24

print("-" * 60)
print(f"{'Total (load)':<28} {total_ua:>8.1f} µA")
print(f"{'System (w/ regulator)':<28} {system_ua:>8.1f} µA")
print(f"{'Battery capacity':<28} {battery_mah:>8} mAh")
print(f"{'Estimated battery life':<28} {battery_hours:>8.1f} hours")
print(f"{'                      ':<28} {battery_days:>8.1f} days")
Power Optimization: The nRF52840 spends 90% of time in System ON sleep (1.5 µA). BLE connections use 2 Mbps PHY with 7.5 ms connection interval for fast data transfer, then return to idle. The OLED display is only activated by wrist-raise gesture (LSM6DSO wake-on-motion interrupt), keeping average display duty under 5%.

Sensor Configuration Planner

Wearable Sensor Planner

Configure sensor modes and sampling parameters. Download as Word, Excel, or PDF.

Draft auto-saved

Conclusion

This wearable integrates optical (MAX30102 PPG/SpO2), electrical (AD8232 ECG), and inertial (LSM6DSO IMU) sensing with BLE 5.0 connectivity on a power budget targeting 7+ days on a 150 mAh cell. The design demonstrates the critical balance between measurement fidelity and energy efficiency in medical wearables.

Next Capstone

In Capstone 8: Autonomous Robot Platform, we’ll build a mobile robotics platform with motor control, LIDAR navigation, and real-time path planning on an embedded SoC.