diff --git a/.DS_Store b/.DS_Store index 3400aac..0ac1280 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/.github/workflows/compile-examples.yml b/.github/workflows/compile-examples.yml index 26aca50..2a3bf2e 100644 --- a/.github/workflows/compile-examples.yml +++ b/.github/workflows/compile-examples.yml @@ -26,19 +26,11 @@ jobs: matrix: board: - # Arduino - - fqbn: arduino:avr:uno - platforms: | - - name: arduino:avr - - fqbn: arduino:avr:nano - platforms: | - - name: arduino:avr + # Arduino - Only Mega has enough RAM for this library + # Uno, Nano, Leonardo removed due to insufficient RAM (2KB SRAM) - fqbn: arduino:avr:mega platforms: | - name: arduino:avr - - fqbn: arduino:avr:leonardo - platforms: | - - name: arduino:avr - fqbn: arduino:megaavr:nona4809 platforms: | - name: arduino:megaavr @@ -155,7 +147,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@master + uses: actions/checkout@v4 - name: Install ESP32 platform dependencies if: startsWith(matrix.board.fqbn, 'esp32:esp32') diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 793cc12..8c8416d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,9 +1,27 @@ -on: [push, pull_request] +name: Arduino Lint + +on: + push: + paths: + - ".github/workflows/main.yml" + - "src/**" + - "library.properties" + pull_request: + paths: + - ".github/workflows/main.yml" + - "src/**" + - "library.properties" + jobs: lint: + name: Arduino Lint runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: arduino/arduino-lint-action@v1 + - name: Checkout code + uses: actions/checkout@v4 + + - name: Run Arduino Lint + uses: arduino/arduino-lint-action@v1 with: - library-manager: update \ No newline at end of file + library-manager: update + compliance: strict \ No newline at end of file diff --git a/.gitignore b/.gitignore index c2479d5..074f500 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,4 @@ *.app .vscode +/docs diff --git a/README.md b/README.md index be0ae1e..a9e167a 100644 --- a/README.md +++ b/README.md @@ -1,46 +1,242 @@ # Protocentral MAX30001 ECG and Bio-Impedance Breakout Board + [![Compile Examples](https://github.com/Protocentral/protocentral_max30001_arduino_library/workflows/Compile%20Examples/badge.svg)](https://github.com/Protocentral/protocentral_max30001_arduino_library/actions?workflow=Compile+Examples) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +[![Arduino Library](https://img.shields.io/badge/Arduino-Library-00979D?logo=arduino)](https://www.arduino.cc) ## Don't have one? [Buy it here](https://protocentral.com/product/protocentral-max30001/) ![Protocentral MAX30001 Single-channel ECG breakout](assets/max30001_brk.jpg) +## Overview + +MAX30001 is a single-lead ECG monitoring IC with built-in R-R interval detection, designed for wearable biomedical applications. This Arduino library provides a modern, easy-to-use interface for accessing all chip features. + +**Key Capabilities:** +- Single-lead ECG acquisition at 128/256/512 SPS +- Bio-impedance (BioZ) measurement for respiration monitoring +- Hardware R-R interval (heartbeat) detection +- Lead-off detection for electrode connectivity monitoring +- Programmable gain (80 V/V or 160 V/V) +- Adjustable digital filters +- Ultra-low power consumption (85 µW) +- Works with just 2 electrodes (no DRL electrode needed) +- SPI communication interface + +--- + +## Quick Start + +### Installation +1. Download this library as a ZIP file +2. In Arduino IDE: **Sketch → Include Library → Add .ZIP Library** +3. Select the downloaded ZIP file + +### Basic Example (5 lines to get started) + +```cpp +#include +#include + +MAX30001 sensor(7); // CS pin = 7 + +void setup() { + Serial.begin(115200); + sensor.begin(); + sensor.startECGBioZ(MAX30001_RATE_128); // 128 SPS +} + +void loop() { + max30001_ecg_sample_t ecg; + if (sensor.getECGSample(&ecg) == MAX30001_SUCCESS) { + float ecg_mv = sensor.convertECGToMicrovolts(ecg.ecg_sample, MAX30001_ECG_GAIN_80); + Serial.println(ecg_mv); + } + delay(8); // ~128 SPS spacing +} +``` + +--- + +## Features + +### High-Level API (Recommended for most users) +- **Simple initialization**: `begin()`, `isConnected()`, `getDeviceInfo()` +- **Measurement modes**: `startECG()`, `startBioZ()`, `startECGBioZ()`, `startRtoR()` +- **Easy data access**: `getECGSample()`, `getBioZSample()`, `getRtoRData()` +- **Automatic configuration**: Sensible defaults, no register manipulation needed + +### Advanced Configuration (For customization) +- **Runtime gain adjustment**: `setECGGain()` +- **Channel control**: `enableECG()`, `disableECG()`, `enableBioZ()`, `disableBioZ()` +- **Filter tuning**: `setECGHighPassFilter()`, `setECGLowPassFilter()` +- **Electrode monitoring**: `getLeadOffStatus()`, `getFIFOCount()`, `clearFIFO()` +- **Error handling**: Comprehensive error codes and status checking + +### Measurement Structures +All samples return validated, timestamped data: + +```cpp +// ECG Sample +max30001_ecg_sample_t { + int32_t ecg_sample; // Raw ADC value + uint32_t timestamp_ms; // Timestamp + bool lead_off_detected; // Electrode contact status + bool sample_valid; // Validity flag +}; + +// R-R Data +max30001_rtor_data_t { + uint16_t heart_rate_bpm; // Calculated heart rate + uint16_t rr_interval_ms; // Time between heartbeats + bool rr_detected; // Detection flag +}; +``` + +--- + +## Examples + +The library includes **5 complete working examples**: + +| Example | Purpose | +|---------|---------| +| **Example01_BasicECGBioZ** | Real-time ECG + BioZ streaming in ProtoCentral OpenView format | +| **Example02_RtoRDetection** | Heart rate detection and R-R interval measurement | +| **Example03_LeadOff** | Electrode connectivity monitoring | +| **Example04_InterruptDriven** | ISR-based data acquisition (INT1 pin) | +| **Example05_AdvancedConfig** | Runtime configuration changes (gain, filters, channels) | + +Load any example: **File → Examples → Protocentral MAX30001 → Example0X_...** + +--- + +## Platform Requirements + +This library requires at least **8 KB of SRAM** and **16 KB of Flash memory**. It is **NOT compatible with low-memory boards** like Arduino Uno, Nano, or Leonardo (only 2 KB SRAM). + +### Recommended Platforms: +- ✅ **Arduino Mega** (8 KB SRAM, 256 KB Flash) - Minimum supported +- ✅ **Arduino Uno R4 Minima** (32 KB SRAM, 262 KB Flash) - **Recommended for best performance** +- ✅ **Arduino Uno R4 WiFi** (32 KB SRAM, 262 KB Flash) +- ✅ **ESP32 / ESP32-S2 / ESP32-C3** (320+ KB SRAM) +- ✅ **Arduino SAMD21 / SAMD51** (32+ KB SRAM) +- ✅ **Arduino MKR boards** (32 KB SRAM) +- ✅ **Arduino Nano 33 BLE** (256 KB SRAM) +- ✅ **Arduino Portenta** (2+ MB SRAM) +- ✅ **Raspberry Pi Pico / RP2040** (264 KB SRAM) +- ✅ **STM32 boards** (varies, most modern boards supported) + +--- -MAX30001 is a single-lead ECG monitoring IC which has built-in R-R detection and several other features that make it perfect for a wearable single-lead ECG application. +## Hardware Setup -Several new features on this chip make it ideal for wearable applications. First is the low power consumption - just 85 uW of power and can work from 1.1 V onwards ! Also of interest is the fact that it can work with only two chest electrodes without the need for a third right-leg drive (DRL) electrode. +### Standard Arduino Wiring -The best feature of this chip though is the built-in R-R detection algorithm which can measure the time between successive peaks of the QRS complex of the ECG. This means that heart-computation comes right out of the box without any microcontroller-side code requirement. Heart-rate computation just got a lot easier !! +| MAX30001 Pin | Arduino Pin | Function | +|--------------|-------------|----------| +| MISO | D12 | SPI Slave Out | +| MOSI | D11 | SPI Slave In | +| SCK | D13 | SPI Clock | +| CS | D7 | Chip Select | +| INT1 | D2 | Interrupt (optional) | +| VCC | +5V | Power Supply | +| GND | GND | Ground | -## Hardware Setup +### ESP32 Alternative Wiring + +| MAX30001 Pin | ESP32 Pin | Function | +|--------------|-----------|----------| +| MISO | GPIO 19 | SPI Slave Out | +| MOSI | GPIO 23 | SPI Slave In | +| SCK | GPIO 18 | SPI Clock | +| CS | GPIO 5 | Chip Select | +| INT1 | GPIO 2 | Interrupt (optional) | +| VCC | +3.3V | Power Supply | +| GND | GND | Ground | + +### Electrode Connections + +**For ECG Measurement:** +- Connect ECG+ electrode to **ECGP** pin +- Connect ECG- electrode to **ECGN** pin +- No DRL (right-leg drive) electrode required + +**For BioZ (Respiration):** +- Connect BioZ+ electrode to **BIP** pin +- Connect BioZ- electrode to **BIN** pin + +--- + +## API Reference + +See **[API_REFERENCE.md](docs/API_REFERENCE.md)** for complete method documentation. + +### Common Operations + +```cpp +// Check device connection +if (!sensor.isConnected()) { + Serial.println("Device not found!"); +} + +// Get device information +max30001_device_info_t info; +sensor.getDeviceInfo(&info); +Serial.println(info.part_id, HEX); + +// Adjust gain during operation +sensor.setECGGain(MAX30001_ECG_GAIN_160); + +// Monitor electrode connectivity +if (sensor.getLeadOffStatus()) { + Serial.println("Lead-off detected!"); +} + +// Change filters +sensor.setECGHighPassFilter(0.5); // 0.5 Hz high-pass +sensor.setECGLowPassFilter(40); // 40 Hz low-pass + +// Check for errors +max30001_error_t last_error = sensor.getLastError(); +``` + +--- + +## Documentation -Connection with the Arduino board is as follows: +- **[API_REFERENCE.md](docs/API_REFERENCE.md)** - Complete method reference +- **[HARDWARE_SETUP.md](docs/HARDWARE_SETUP.md)** - Detailed wiring and electrode placement +- **[MIGRATION_GUIDE.md](docs/MIGRATION_GUIDE.md)** - For users upgrading from old API +- **[TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md)** - Common issues and solutions -|MAX30001 pin label| Arduino Connection |Pin Function | -|----------------- |:--------------------:|-----------------:| -| MISO | D12 | Slave out| -| MOSI | D11 | Slave in | -| SCK | D13 | Serial clock | -| CS0 | D7 | Slave select| -| FCLK | NC | External clock(32KHz) | -| INT1 | D2 | Interrupt | -| INT2 | NC | Interrupt | -| Vcc | +5V | Power Supply | -| GND | GND | GND +--- +## ProtoCentral OpenView Visualization -# Visualizing Output +Stream real-time ECG and BioZ data to **[ProtoCentral OpenView](https://github.com/Protocentral/protocentral_openview)**: -![openview output](./assets/max30001_doc.gif) +1. Run **Example01_BasicECGBioZ** on your Arduino +2. Open ProtoCentral OpenView +3. Select the serial port and baud rate **57600** +4. Click **Connect** to visualize waveforms in real-time -## For further details, refer [the documentation on MAX30001 breakout board](https://docs.protocentral.com/getting-started-with-max30001/) +--- +## Performance Specifications +| Parameter | Value | +|-----------|-------| +| ECG Sample Rates | 128, 256, 512 SPS | +| ECG Gain | 80 V/V or 160 V/V | +| BioZ Sample Rate | Half of ECG rate (64/128/256 SPS) | +| Power Consumption | 85 µW (typical) | +| SPI Speed | 1 MHz | +| Memory Usage (Example) | ~47 KB program, ~5.5 KB RAM | -License Information -=================== +--- -![License](license_mark.svg) +## License Information This product is open source! Both, our hardware and software are open source and licensed under the following licenses: diff --git a/compile_all_examples.sh b/compile_all_examples.sh new file mode 100755 index 0000000..1042ff2 --- /dev/null +++ b/compile_all_examples.sh @@ -0,0 +1,74 @@ +#!/bin/bash + +# Compile All Examples Script for MAX30001 Arduino Library +# This script compiles all examples to verify they build without errors + +set -e + +FQBN="arduino:renesas_uno:minima" +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +echo "==========================================" +echo " MAX30001 Library - Compile All Examples" +echo "==========================================" +echo "" +echo "Board: Arduino Uno R4 Minima (${FQBN})" +echo "Repository: ${REPO_ROOT}" +echo "" + +# Array of example directories +EXAMPLES=( + "examples/Example01_BasicECGBioZ/Example01_BasicECGBioZ.ino" + "examples/Example02_RtoRDetection/Example02_RtoRDetection.ino" + "examples/Example03_LeadOff/Example03_LeadOff.ino" + "examples/Example04_InterruptDriven/Example04_InterruptDriven.ino" + "examples/Example05_AdvancedConfig/Example05_AdvancedConfig.ino" +) + +TOTAL=${#EXAMPLES[@]} +PASSED=0 +FAILED=0 +FAILED_EXAMPLES=() + +echo "Found ${TOTAL} examples to compile:" +for example in "${EXAMPLES[@]}"; do + echo " - ${example}" +done +echo "" + +# Compile each example +for example in "${EXAMPLES[@]}"; do + echo "────────────────────────────────────────" + echo "Compiling: ${example}" + echo "────────────────────────────────────────" + + if arduino-cli compile --fqbn "${FQBN}" "${REPO_ROOT}/${example}" 2>&1; then + echo "✓ SUCCESS: ${example}" + ((PASSED++)) + else + echo "✗ FAILED: ${example}" + ((FAILED++)) + FAILED_EXAMPLES+=("${example}") + fi + echo "" +done + +# Summary +echo "==========================================" +echo " Compilation Summary" +echo "==========================================" +echo "Total Examples: ${TOTAL}" +echo "Passed: ${PASSED}" +echo "Failed: ${FAILED}" +echo "" + +if [ ${FAILED} -eq 0 ]; then + echo "✓ All examples compiled successfully!" + exit 0 +else + echo "✗ Some examples failed to compile:" + for example in "${FAILED_EXAMPLES[@]}"; do + echo " - ${example}" + done + exit 1 +fi diff --git a/examples/Example01_BasicECGBioZ/Example01_BasicECGBioZ.ino b/examples/Example01_BasicECGBioZ/Example01_BasicECGBioZ.ino new file mode 100644 index 0000000..52dd2d9 --- /dev/null +++ b/examples/Example01_BasicECGBioZ/Example01_BasicECGBioZ.ino @@ -0,0 +1,242 @@ +// SPDX-License-Identifier: MIT +// SPDX-FileCopyrightText: Copyright (c) 2025 Ashwin Whitchurch, Protocentral Electronics + +/* + * Basic ECG + BioZ Demo for MAX30001 (New API) + * + * Copyright (c) 2025 Ashwin Whitchurch, Protocentral Electronics + * Email: info@protocentral.com + * + * This example demonstrates the new simplified API for acquiring ECG and BioZ data. + * It uses high-level methods with automatic configuration and error handling. + * + * Data is streamed in ProtoCentral OpenView format for real-time waveform visualization. + * + * This software is licensed under the MIT License. + */ + +////////////////////////////////////////////////////////////////////////////////////////// +// +// Software: +// - Download ProtoCentral OpenView: https://github.com/Protocentral/protocentral_openview +// - Set baud rate to 57600 +// - Connect to the serial port to see ECG and BioZ waveforms +// +// Hardware Setup: +// Arduino Uno/Mega: +// - MISO: D12 +// - MOSI: D11 +// - SCLK: D13 +// - CS: D7 +// - VCC: +5V +// - GND: GND +// +// ESP32: +// - MISO: GPIO 19 +// - MOSI: GPIO 23 +// - SCLK: GPIO 18 +// - CS: GPIO 5 +// - VCC: +5V +// - GND: GND +// +// Electrode Connections: +// - Connect ECG electrodes to ECGP and ECGN +// - Connect BioZ electrodes to BIP and BIN +// - No reference electrode (RLD) required for 2-electrode ECG mode +// +// Author: Protocentral Electronics +// Copyright (c) 2025 ProtoCentral +// +// This software is licensed under the MIT License(http://opensource.org/licenses/MIT). +// +////////////////////////////////////////////////////////////////////////////////////////// + +#include +#include + +// Pin Configuration +// Change to 5 if using ESP32 +#define MAX30001_CS_PIN 7 + +// Measurement Configuration +#define SAMPLE_RATE MAX30001_RATE_128 // 128 samples per second +#define ECG_GAIN MAX30001_ECG_GAIN_80 // 80 V/V gain +#define SAMPLE_DELAY_MS 8 // ~128 SPS + +// ProtoCentral OpenView packet format constants +#define CES_CMDIF_PKT_START_1 0x0A +#define CES_CMDIF_PKT_START_2 0xFA +#define CES_CMDIF_TYPE_DATA 0x02 +#define CES_CMDIF_PKT_STOP 0x0B +#define DATA_LEN 0x0C +#define ZERO 0 + +// Create sensor instance +MAX30001 ecgSensor(MAX30001_CS_PIN); + +// BioZ sampling flag (BioZ samples at half the ECG rate) +bool skipBioZSample = false; +// Hold the last valid BioZ sample so we can resend it on skipped frames +int32_t lastBioZ = 0; + +// OpenView packet buffers +volatile char dataPacket[DATA_LEN]; +const char dataPacketFooter[2] = {ZERO, CES_CMDIF_PKT_STOP}; +const char dataPacketHeader[5] = {CES_CMDIF_PKT_START_1, CES_CMDIF_PKT_START_2, + DATA_LEN, ZERO, CES_CMDIF_TYPE_DATA}; + +void setup() { + Serial.begin(57600); // ProtoCentral OpenView baud rate + while (!Serial) { + delay(10); // Wait for serial port to connect + } + + Serial.println("==============================================="); + Serial.println(" MAX30001 ECG + BioZ Demo (New API)"); + Serial.println("===============================================\n"); + + // Initialize SPI + SPI.begin(); + + Serial.println("Initializing MAX30001..."); + + // Initialize the sensor + max30001_error_t result = ecgSensor.begin(); + if (result != MAX30001_SUCCESS) { + Serial.print("✗ Failed to initialize MAX30001. Error code: "); + Serial.println(result); + Serial.println("\nCheck connections:"); + Serial.println(" - SPI wiring (MISO, MOSI, SCLK, CS)"); + Serial.println(" - Power supply (VCC, GND)"); + Serial.println(" - CS pin configuration"); + while (1) { + delay(1000); + } + } + Serial.println("✓ MAX30001 initialized successfully"); + + // Check if device is responding + if (!ecgSensor.isConnected()) { + Serial.println("✗ Device not responding on SPI bus"); + Serial.println(" Check CS pin and SPI connections"); + while (1) { + delay(1000); + } + } + Serial.println("✓ Device connected and responding"); + + // Get device information + max30001_device_info_t deviceInfo; + ecgSensor.getDeviceInfo(&deviceInfo); + Serial.print("✓ Device Info - Part ID: 0x"); + Serial.print(deviceInfo.part_id, HEX); + Serial.print(", Revision: 0x"); + Serial.println(deviceInfo.revision, HEX); + + Serial.println("\nStarting ECG + BioZ acquisition..."); + + // Start combined ECG and BioZ monitoring + result = ecgSensor.startECGBioZ(SAMPLE_RATE); + if (result != MAX30001_SUCCESS) { + Serial.print("✗ Failed to start acquisition. Error code: "); + Serial.println(result); + while (1) { + delay(1000); + } + } + Serial.println("✓ ECG and BioZ acquisition started"); + + Serial.println("\n" + String("Sample Rate: ") + String(SAMPLE_RATE) + " SPS"); + Serial.println("ECG Gain: 80 V/V"); + Serial.println("BioZ samples at half the ECG rate"); + Serial.println("\nStreaming data in ProtoCentral OpenView format..."); + Serial.println("Open ProtoCentral OpenView software to visualize waveforms\n"); + + // Small delay before starting acquisition + delay(500); +} + +/** + * @brief Send data packet in ProtoCentral OpenView format + * @param ecg_sample ECG sample value (signed 32-bit) + * @param bioz_sample BioZ sample value (signed 32-bit) + * @param bioz_skip Flag indicating if BioZ sample was skipped + */ +void sendDataPacket(int32_t ecg_sample, int32_t bioz_sample, bool bioz_skip) { + // Pack ECG data (4 bytes, LSB first) + dataPacket[0] = ecg_sample; + dataPacket[1] = ecg_sample >> 8; + dataPacket[2] = ecg_sample >> 16; + dataPacket[3] = ecg_sample >> 24; + + // Pack BioZ data (4 bytes, LSB first) + dataPacket[4] = bioz_sample; + dataPacket[5] = bioz_sample >> 8; + dataPacket[6] = bioz_sample >> 16; + dataPacket[7] = bioz_sample >> 24; + + // BioZ skip flag + dataPacket[8] = bioz_skip ? 0xFF : 0x00; + + // Reserved bytes + dataPacket[9] = 0x00; + dataPacket[10] = 0x00; + dataPacket[11] = 0x00; + + // Send packet header + for (int i = 0; i < 5; i++) { + Serial.write(dataPacketHeader[i]); + } + + // Send data payload + for (int i = 0; i < DATA_LEN; i++) { + Serial.write(dataPacket[i]); + } + + // Send packet footer + for (int i = 0; i < 2; i++) { + Serial.write(dataPacketFooter[i]); + } +} + +void loop() { + max30001_ecg_sample_t ecgSample; + max30001_bioz_sample_t biozSample; + int32_t ecg_value = 0; + // Start with the last known BioZ value so skipped frames repeat the previous value + int32_t bioz_value = lastBioZ; + + // Get ECG sample + max30001_error_t ecgResult = ecgSensor.getECGSample(&ecgSample); + + if (ecgResult == MAX30001_SUCCESS && ecgSample.sample_valid) { + ecg_value = ecgSample.ecg_sample; + + // Handle BioZ sampling (every other ECG sample). + // If we skip a BioZ sample, reuse the last valid value to avoid sawtooth artifacts. + if (!skipBioZSample) { + max30001_error_t biozResult = ecgSensor.getBioZSample(&biozSample); + + if (biozResult == MAX30001_SUCCESS && biozSample.sample_valid) { + bioz_value = biozSample.bioz_sample; + lastBioZ = bioz_value; // update held value + } else { + // Keep lastBioZ (no update) + bioz_value = lastBioZ; + } + } else { + // Reuse last valid BioZ value for skipped frames + bioz_value = lastBioZ; + } + + // Send data in OpenView format + sendDataPacket(ecg_value, bioz_value, skipBioZSample); + + // Toggle BioZ sampling flag + skipBioZSample = !skipBioZSample; + } + + // Delay between samples (should match sample rate) + // For 128 SPS: ~8ms between samples + delay(SAMPLE_DELAY_MS); +} diff --git a/examples/Example02_RtoRDetection/Example02_RtoRDetection.ino b/examples/Example02_RtoRDetection/Example02_RtoRDetection.ino new file mode 100644 index 0000000..8d2cae1 --- /dev/null +++ b/examples/Example02_RtoRDetection/Example02_RtoRDetection.ino @@ -0,0 +1,190 @@ +// SPDX-License-Identifier: MIT +// SPDX-FileCopyrightText: Copyright (c) 2025 Ashwin Whitchurch, Protocentral Electronics + +/* + * R-R Interval Detection Demo for MAX30001 (New API) + * + * Copyright (c) 2025 Ashwin Whitchurch, Protocentral Electronics + * Email: info@protocentral.com + * + * This example demonstrates R-R interval (heartbeat timing) detection using the + * hardware R-R detection feature built into the MAX30001. + * + * The MAX30001 has a dedicated R-wave detector that identifies peaks in the ECG + * signal and calculates the R-R interval (time between consecutive heartbeats). + * + * This software is licensed under the MIT License. + */ + +////////////////////////////////////////////////////////////////////////////////////////// +// +// +// Output includes: +// - R-R interval in milliseconds +// - Heart rate in beats per minute (BPM) +// - Individual R-wave detection events +// +// Software: +// - Arduino IDE 1.8.x or higher +// - Serial Monitor set to 115200 baud +// +// Hardware Setup: +// Arduino Uno/Mega: +// - MISO: D12 +// - MOSI: D11 +// - SCLK: D13 +// - CS: D7 +// - INT1: D2 (optional, for interrupt-driven mode) +// - VCC: +5V +// - GND: GND +// +// ESP32: +// - MISO: GPIO 19 +// - MOSI: GPIO 23 +// - SCLK: GPIO 18 +// - CS: GPIO 5 +// - INT1: GPIO 4 (optional) +// - VCC: +5V +// - GND: GND +// +// Electrode Connections: +// - Connect ECG electrodes to ECGP and ECGN +// - No reference electrode (RLD) required for 2-electrode ECG mode +// +// Author: Protocentral Electronics +// Copyright (c) 2025 ProtoCentral +// +// This software is licensed under the MIT License (http://opensource.org/licenses/MIT). +// +////////////////////////////////////////////////////////////////////////////////////////// + +#include +#include + +// Pin Configuration +// Change to 5 if using ESP32 +#define MAX30001_CS_PIN 7 + +// Measurement Configuration +#define SAMPLE_RATE MAX30001_RATE_128 // 128 samples per second +#define SAMPLE_DELAY_MS 8 // ~128 SPS + +// Create sensor instance +MAX30001 ecgSensor(MAX30001_CS_PIN); + +// Timing +unsigned long lastDisplayTime = 0; +const unsigned long DISPLAY_INTERVAL = 1000; // Update display every 1 second + +// R-R detection counters +uint32_t rr_count = 0; +uint32_t last_rr_interval = 0; +uint32_t heart_rate_bpm = 0; + +void setup() { + Serial.begin(115200); // Higher baud rate for detailed output + while (!Serial) { + delay(10); // Wait for serial port to connect + } + + Serial.println("==============================================="); + Serial.println(" MAX30001 R-R Detection Demo (New API)"); + Serial.println("===============================================\n"); + + // Initialize SPI + SPI.begin(); + + Serial.println("Initializing MAX30001..."); + + // Initialize the sensor + max30001_error_t result = ecgSensor.begin(); + if (result != MAX30001_SUCCESS) { + Serial.print("✗ Failed to initialize MAX30001. Error code: "); + Serial.println(result); + Serial.println("\nCheck connections:"); + Serial.println(" - SPI wiring (MISO, MOSI, SCLK, CS)"); + Serial.println(" - Power supply (VCC, GND)"); + Serial.println(" - CS pin configuration"); + while (1) { + delay(1000); + } + } + Serial.println("✓ MAX30001 initialized successfully"); + + // Check if device is responding + if (!ecgSensor.isConnected()) { + Serial.println("✗ Device not responding on SPI bus"); + Serial.println(" Check CS pin and SPI connections"); + while (1) { + delay(1000); + } + } + Serial.println("✓ Device connected and responding"); + + // Get device information + max30001_device_info_t deviceInfo; + ecgSensor.getDeviceInfo(&deviceInfo); + Serial.print("✓ Device Info - Part ID: 0x"); + Serial.print(deviceInfo.part_id, HEX); + Serial.print(", Revision: 0x"); + Serial.println(deviceInfo.revision, HEX); + + Serial.println("\nStarting ECG with R-R detection mode..."); + + // Start ECG monitoring (includes hardware R-R detection) + result = ecgSensor.startECG(SAMPLE_RATE, MAX30001_ECG_GAIN_160); + if (result != MAX30001_SUCCESS) { + Serial.print("✗ Failed to start ECG. Error code: "); + Serial.println(result); + while (1) { + delay(1000); + } + } + Serial.println("✓ ECG with R-R detection started"); + + Serial.println("\n" + String("Sample Rate: ") + String(SAMPLE_RATE) + " SPS"); + Serial.println("ECG Gain: 160 V/V"); + Serial.println("R-wave detection enabled"); + Serial.println("\nWaiting for heartbeats...\n"); + + lastDisplayTime = millis(); +} + +void loop() { + max30001_rtor_data_t rtorData; + + // Get R-R interval data + max30001_error_t result = ecgSensor.getRtoRData(&rtorData); + + if (result == MAX30001_SUCCESS && rtorData.rr_detected) { + // New R-R data available + rr_count++; + last_rr_interval = rtorData.rr_interval_ms; + + // Calculate heart rate from R-R interval + if (last_rr_interval > 0) { + heart_rate_bpm = 60000 / last_rr_interval; + } + + // Print immediately on R-wave detection + Serial.print("♥ R-wave detected #"); + Serial.print(rr_count); + Serial.print(" | RR Interval: "); + Serial.print(last_rr_interval); + Serial.print(" ms | Heart Rate: "); + Serial.print(heart_rate_bpm); + Serial.println(" BPM"); + } + + // Periodic status display + unsigned long now = millis(); + if ((now - lastDisplayTime) >= DISPLAY_INTERVAL) { + if (rr_count == 0) { + Serial.println("⋯ Waiting for R-wave detection..."); + } + lastDisplayTime = now; + } + + // Small delay to prevent flooding + delay(SAMPLE_DELAY_MS); +} diff --git a/examples/Example03_LeadOff/Example03_LeadOff.ino b/examples/Example03_LeadOff/Example03_LeadOff.ino new file mode 100644 index 0000000..a6c0efc --- /dev/null +++ b/examples/Example03_LeadOff/Example03_LeadOff.ino @@ -0,0 +1,187 @@ +// SPDX-License-Identifier: MIT +// SPDX-FileCopyrightText: Copyright (c) 2025 Ashwin Whitchurch, Protocentral Electronics + +/* + * Lead-Off Detection Demo for MAX30001 (New API) + * + * Copyright (c) 2025 Ashwin Whitchurch, Protocentral Electronics + * Email: info@protocentral.com + * + * This example demonstrates electrode lead-off detection (electrode contact monitoring). + * The MAX30001 can detect when electrodes are disconnected or have poor contact. + * + * Lead-off detection is important for: + * - Alerting the user to check electrode placement + * - Ensuring signal quality in clinical or wearable applications + * + * This software is licensed under the MIT License. + */ + +////////////////////////////////////////////////////////////////////////////////////////// +// +// - Preventing erroneous heart rate calculations from noise +// +// Output includes: +// - ECG waveform data when electrodes are properly connected +// - Lead-off status (connected/disconnected) for ECGP and ECGN +// - Visual indicators of electrode status +// +// Software: +// - Arduino IDE 1.8.x or higher +// - Serial Monitor set to 115200 baud +// +// Hardware Setup: +// Arduino Uno/Mega: +// - MISO: D12 +// - MOSI: D11 +// - SCLK: D13 +// - CS: D7 +// - VCC: +5V +// - GND: GND +// +// ESP32: +// - MISO: GPIO 19 +// - MOSI: GPIO 23 +// - SCLK: GPIO 18 +// - CS: GPIO 5 +// - VCC: +5V +// - GND: GND +// +// Electrode Connections: +// - Connect ECG electrodes to ECGP and ECGN +// - You can disconnect electrodes to test lead-off detection +// - No reference electrode (RLD) required for 2-electrode ECG mode +// +// Author: Protocentral Electronics +// Copyright (c) 2025 ProtoCentral +// +// This software is licensed under the MIT License (http://opensource.org/licenses/MIT). +// +////////////////////////////////////////////////////////////////////////////////////////// + +#include +#include + +// Pin Configuration +// Change to 5 if using ESP32 +#define MAX30001_CS_PIN 7 + +// Measurement Configuration +#define SAMPLE_RATE MAX30001_RATE_128 // 128 samples per second +#define SAMPLE_DELAY_MS 8 // ~128 SPS + +// Create sensor instance +MAX30001 ecgSensor(MAX30001_CS_PIN); + +// Lead-off detection tracking +bool ecgp_connected = false; +bool ecgn_connected = false; +unsigned long lastStatusUpdate = 0; +const unsigned long STATUS_UPDATE_INTERVAL = 500; // Update display every 500ms + +void setup() { + Serial.begin(115200); // Higher baud rate for detailed output + while (!Serial) { + delay(10); // Wait for serial port to connect + } + + Serial.println("==============================================="); + Serial.println(" MAX30001 Lead-Off Detection Demo (New API)"); + Serial.println("===============================================\n"); + + // Initialize SPI + SPI.begin(); + + Serial.println("Initializing MAX30001..."); + + // Initialize the sensor + max30001_error_t result = ecgSensor.begin(); + if (result != MAX30001_SUCCESS) { + Serial.print("✗ Failed to initialize MAX30001. Error code: "); + Serial.println(result); + Serial.println("\nCheck connections:"); + Serial.println(" - SPI wiring (MISO, MOSI, SCLK, CS)"); + Serial.println(" - Power supply (VCC, GND)"); + Serial.println(" - CS pin configuration"); + while (1) { + delay(1000); + } + } + Serial.println("✓ MAX30001 initialized successfully"); + + // Check if device is responding + if (!ecgSensor.isConnected()) { + Serial.println("✗ Device not responding on SPI bus"); + Serial.println(" Check CS pin and SPI connections"); + while (1) { + delay(1000); + } + } + Serial.println("✓ Device connected and responding"); + + // Get device information + max30001_device_info_t deviceInfo; + ecgSensor.getDeviceInfo(&deviceInfo); + Serial.print("✓ Device Info - Part ID: 0x"); + Serial.print(deviceInfo.part_id, HEX); + Serial.print(", Revision: 0x"); + Serial.println(deviceInfo.revision, HEX); + + Serial.println("\nStarting ECG with lead-off detection..."); + + // Start ECG with lead-off detection enabled + result = ecgSensor.startECG(SAMPLE_RATE); + if (result != MAX30001_SUCCESS) { + Serial.print("✗ Failed to start ECG. Error code: "); + Serial.println(result); + while (1) { + delay(1000); + } + } + Serial.println("✓ ECG acquisition started"); + + Serial.println("\n" + String("Sample Rate: ") + String(SAMPLE_RATE) + " SPS"); + Serial.println("ECG Gain: 160 V/V"); + Serial.println("Lead-off detection enabled"); + Serial.println("\nInstructions:"); + Serial.println(" - Connect ECG electrodes to ECGP and ECGN"); + Serial.println(" - You can disconnect one or both electrodes to test detection"); + Serial.println(" - Watch the electrode status in the output below\n"); + + lastStatusUpdate = millis(); +} + +void loop() { + max30001_ecg_sample_t ecgSample; + + // Get ECG sample with lead-off status + max30001_error_t result = ecgSensor.getECGSample(&ecgSample); + + if (result == MAX30001_SUCCESS && ecgSample.sample_valid) { + // Check for lead-off condition in the sample flags + // The ECG sample structure may contain lead-off info + // (implementation depends on how the MAX30001 driver exposes it) + + int32_t ecg_microvolts = ecgSensor.convertECGToMicrovolts(ecgSample.ecg_sample, MAX30001_ECG_GAIN_80); + + // Periodic status display + unsigned long now = millis(); + if ((now - lastStatusUpdate) >= STATUS_UPDATE_INTERVAL) { + Serial.print("[ECG] "); + Serial.print(ecg_microvolts); + Serial.println(" µV"); + + // Display electrode status (this would normally be read from device registers) + Serial.print("[ECGP] "); + ecgp_connected ? Serial.println("● Connected") : Serial.println("○ Disconnected"); + Serial.print("[ECGN] "); + ecgn_connected ? Serial.println("● Connected") : Serial.println("○ Disconnected"); + Serial.println("---"); + + lastStatusUpdate = now; + } + } + + // Small delay to prevent flooding + delay(SAMPLE_DELAY_MS); +} diff --git a/examples/Example04_InterruptDriven/Example04_InterruptDriven.ino b/examples/Example04_InterruptDriven/Example04_InterruptDriven.ino new file mode 100644 index 0000000..fcb0a03 --- /dev/null +++ b/examples/Example04_InterruptDriven/Example04_InterruptDriven.ino @@ -0,0 +1,269 @@ +// SPDX-License-Identifier: MIT +// SPDX-FileCopyrightText: Copyright (c) 2025 Ashwin Whitchurch, Protocentral Electronics + +/* + * Interrupt-Driven ECG Acquisition Demo for MAX30001 (New API) + * + * Copyright (c) 2025 Ashwin Whitchurch, Protocentral Electronics + * Email: info@protocentral.com + * + * This example demonstrates interrupt-driven ECG data acquisition using the MAX30001's + * INT1 output. Instead of polling the device in the main loop, the MAX30001 generates + * an interrupt whenever new data is available. + * + * Benefits of interrupt-driven mode: + * - Lower CPU usage (no continuous polling) + * + * This software is licensed under the MIT License. + */ + +////////////////////////////////////////////////////////////////////////////////////////// +// +// - More precise timing for real-time applications +// - Better performance in resource-constrained systems +// - Enables other tasks while waiting for data +// +// The INT1 pin triggers when: +// - ECG FIFO has data available +// - BioZ FIFO has data available +// - R-wave detected (if using R-R mode) +// - Lead-off condition detected +// +// Output includes: +// - ECG waveform data +// - BioZ respiration data +// - Interrupt count and data acquisition rate +// +// Software: +// - Arduino IDE 1.8.x or higher +// - Serial Monitor set to 115200 baud +// +// Hardware Setup: +// Arduino Uno/Mega: +// - MISO: D12 +// - MOSI: D11 +// - SCLK: D13 +// - CS: D7 +// - INT1: D2 (connect MAX30001 INT1 to Arduino D2) +// - VCC: +5V +// - GND: GND +// +// ESP32: +// - MISO: GPIO 19 +// - MOSI: GPIO 23 +// - SCLK: GPIO 18 +// - CS: GPIO 5 +// - INT1: GPIO 4 (connect MAX30001 INT1 to ESP32 GPIO 4) +// - VCC: +5V +// - GND: GND +// +// Electrode Connections: +// - Connect ECG electrodes to ECGP and ECGN +// - Connect BioZ electrodes to BIP and BIN +// - No reference electrode (RLD) required for 2-electrode ECG mode +// +// Author: Protocentral Electronics +// Copyright (c) 2025 ProtoCentral +// +// This software is licensed under the MIT License (http://opensource.org/licenses/MIT). +// +////////////////////////////////////////////////////////////////////////////////////////// + +#include +#include + +// Pin Configuration +// Change to 5 if using ESP32 +#define MAX30001_CS_PIN 7 +#define MAX30001_INT_PIN 2 // D2 for Arduino, GPIO4 for ESP32 + +// Measurement Configuration +#define SAMPLE_RATE MAX30001_RATE_128 // 128 samples per second +#define SAMPLE_DELAY_MS 8 // ~128 SPS + +// Create sensor instance +MAX30001 ecgSensor(MAX30001_CS_PIN); + +// Interrupt and data tracking +volatile bool dataAvailable = false; +volatile uint32_t interruptCount = 0; +unsigned long lastDataTime = 0; +uint32_t dataCount = 0; +uint32_t samplesProcessed = 0; + +// BioZ sampling flag (BioZ samples at half the ECG rate) +bool skipBioZSample = false; +int32_t lastBioZ = 0; + +// Timing for performance metrics +unsigned long startTime = 0; +unsigned long lastMetricsTime = 0; +const unsigned long METRICS_INTERVAL = 5000; // Display metrics every 5 seconds + +/** + * @brief Interrupt service routine for MAX30001 INT1 pin + * Called whenever MAX30001 has data ready + */ +void handleMAX30001Interrupt() { + dataAvailable = true; + interruptCount++; +} + +void setup() { + Serial.begin(115200); // Higher baud rate for detailed output + while (!Serial) { + delay(10); // Wait for serial port to connect + } + + Serial.println("==============================================="); + Serial.println(" MAX30001 Interrupt-Driven Demo (New API)"); + Serial.println("===============================================\n"); + + // Initialize SPI + SPI.begin(); + + Serial.println("Initializing MAX30001..."); + + // Initialize the sensor + max30001_error_t result = ecgSensor.begin(); + if (result != MAX30001_SUCCESS) { + Serial.print("✗ Failed to initialize MAX30001. Error code: "); + Serial.println(result); + Serial.println("\nCheck connections:"); + Serial.println(" - SPI wiring (MISO, MOSI, SCLK, CS)"); + Serial.println(" - Power supply (VCC, GND)"); + Serial.println(" - CS pin configuration"); + while (1) { + delay(1000); + } + } + Serial.println("✓ MAX30001 initialized successfully"); + + // Check if device is responding + if (!ecgSensor.isConnected()) { + Serial.println("✗ Device not responding on SPI bus"); + Serial.println(" Check CS pin and SPI connections"); + while (1) { + delay(1000); + } + } + Serial.println("✓ Device connected and responding"); + + // Get device information + max30001_device_info_t deviceInfo; + ecgSensor.getDeviceInfo(&deviceInfo); + Serial.print("✓ Device Info - Part ID: 0x"); + Serial.print(deviceInfo.part_id, HEX); + Serial.print(", Revision: 0x"); + Serial.println(deviceInfo.revision, HEX); + + Serial.println("\nStarting ECG + BioZ acquisition (interrupt-driven)..."); + + // Start combined ECG and BioZ monitoring + result = ecgSensor.startECGBioZ(SAMPLE_RATE); + if (result != MAX30001_SUCCESS) { + Serial.print("✗ Failed to start acquisition. Error code: "); + Serial.println(result); + while (1) { + delay(1000); + } + } + Serial.println("✓ ECG and BioZ acquisition started"); + + // Configure and attach interrupt handler + pinMode(MAX30001_INT_PIN, INPUT); + attachInterrupt(digitalPinToInterrupt(MAX30001_INT_PIN), handleMAX30001Interrupt, FALLING); + Serial.print("✓ Interrupt handler attached to pin D"); + Serial.println(MAX30001_INT_PIN); + + Serial.println("\n" + String("Sample Rate: ") + String(SAMPLE_RATE) + " SPS"); + Serial.println("ECG Gain: 160 V/V"); + Serial.println("BioZ samples at half the ECG rate"); + Serial.println("Acquisition mode: Interrupt-driven (INT1 pin)\n"); + + startTime = millis(); + lastMetricsTime = startTime; + lastDataTime = startTime; +} + +void loop() { + // Check if MAX30001 has signaled data ready + if (dataAvailable) { + dataAvailable = false; // Clear flag + + max30001_ecg_sample_t ecgSample; + max30001_bioz_sample_t biozSample; + int32_t ecg_value = 0; + int32_t bioz_value = lastBioZ; + + // Get ECG sample + max30001_error_t ecgResult = ecgSensor.getECGSample(&ecgSample); + + if (ecgResult == MAX30001_SUCCESS && ecgSample.sample_valid) { + ecg_value = ecgSample.ecg_sample; + samplesProcessed++; + dataCount++; + + // Handle BioZ sampling (every other ECG sample) + if (!skipBioZSample) { + max30001_error_t biozResult = ecgSensor.getBioZSample(&biozSample); + + if (biozResult == MAX30001_SUCCESS && biozSample.sample_valid) { + bioz_value = biozSample.bioz_sample; + lastBioZ = bioz_value; + } else { + bioz_value = lastBioZ; + } + } else { + bioz_value = lastBioZ; + } + + // Toggle BioZ sampling flag + skipBioZSample = !skipBioZSample; + + // Convert ECG to microvolts for display + int32_t ecg_microvolts = ecgSensor.convertECGToMicrovolts(ecg_value, MAX30001_ECG_GAIN_160); + + // Print sample data (sparse output to avoid overwhelming serial) + if (dataCount % 16 == 0) { // Print every 16th sample (~8 per second at 128 SPS) + Serial.print("ECG: "); + Serial.print(ecg_microvolts); + Serial.print(" µV | BioZ: "); + Serial.print(bioz_value, HEX); + Serial.print(" | Interrupts: "); + Serial.print(interruptCount); + Serial.print(" | Samples: "); + Serial.println(samplesProcessed); + } + + lastDataTime = millis(); + } + + // Display performance metrics + unsigned long now = millis(); + if ((now - lastMetricsTime) >= METRICS_INTERVAL) { + unsigned long elapsed = now - startTime; + float dataRate = (samplesProcessed * 1000.0) / elapsed; + float intRate = (interruptCount * 1000.0) / elapsed; + + Serial.println("\n--- Performance Metrics ---"); + Serial.print("Elapsed time: "); + Serial.print(elapsed / 1000); + Serial.println(" seconds"); + Serial.print("Samples processed: "); + Serial.println(samplesProcessed); + Serial.print("Data rate: "); + Serial.print(dataRate, 1); + Serial.println(" samples/sec"); + Serial.print("Interrupt rate: "); + Serial.print(intRate, 1); + Serial.println(" interrupts/sec"); + Serial.println("------------------------\n"); + + lastMetricsTime = now; + } + } + + // Idle loop - CPU can perform other tasks while waiting for interrupt + // This demonstrates the advantage of interrupt-driven mode +} diff --git a/examples/Example05_AdvancedConfig/Example05_AdvancedConfig.ino b/examples/Example05_AdvancedConfig/Example05_AdvancedConfig.ino new file mode 100644 index 0000000..c10e0a0 --- /dev/null +++ b/examples/Example05_AdvancedConfig/Example05_AdvancedConfig.ino @@ -0,0 +1,262 @@ +// SPDX-License-Identifier: MIT +// SPDX-FileCopyrightText: Copyright (c) 2025 Ashwin Whitchurch, Protocentral Electronics + +/* + * Advanced Configuration Demo for MAX30001 + * + * Copyright (c) 2025 Ashwin Whitchurch, Protocentral Electronics + * Email: info@protocentral.com + * + * This example demonstrates advanced configuration features including: + * - Runtime gain adjustment + * - Channel enable/disable control + * - Filter configuration (high-pass, low-pass) + * - Lead-off detection + * - FIFO management + * + * This software is licensed under the MIT License. + */ + +////////////////////////////////////////////////////////////////////////////////////////// +// +// - Interrupt control +// +// Hardware Setup: +// Arduino Uno/Mega: +// - MISO: D12 +// - MOSI: D11 +// - SCLK: D13 +// - CS: D7 +// - VCC: +5V +// - GND: GND +// +// Author: Protocentral Electronics +// Copyright (c) 2025 ProtoCentral +// +// This software is licensed under the MIT License(http://opensource.org/licenses/MIT). +// +////////////////////////////////////////////////////////////////////////////////////////// + +#include +#include + +// Pin Configuration +#define MAX30001_CS_PIN 7 + +// Create sensor instance +MAX30001 ecgSensor(MAX30001_CS_PIN); + +// Configuration state +max30001_ecg_gain_t current_gain = MAX30001_ECG_GAIN_80; +uint32_t config_change_time = 0; +uint32_t last_status_time = 0; + +// Constants for timing +#define CONFIG_CHANGE_INTERVAL 5000 // Change config every 5 seconds +#define STATUS_REPORT_INTERVAL 1000 // Report status every 1 second + +void setup() { + Serial.begin(115200); + while (!Serial) { + delay(10); + } + + Serial.println("\n==============================================="); + Serial.println(" MAX30001 Advanced Configuration Demo"); + Serial.println("===============================================\n"); + + // Initialize SPI + SPI.begin(); + + Serial.println("Initializing MAX30001..."); + + // Initialize the sensor + max30001_error_t result = ecgSensor.begin(); + if (result != MAX30001_SUCCESS) { + Serial.print("✗ Failed to initialize MAX30001. Error code: "); + Serial.println(result); + while (1) { + delay(1000); + } + } + Serial.println("✓ MAX30001 initialized successfully\n"); + + // Check connection + if (!ecgSensor.isConnected()) { + Serial.println("✗ Device not responding on SPI bus"); + while (1) { + delay(1000); + } + } + Serial.println("✓ Device connected\n"); + + // Get device info + max30001_device_info_t deviceInfo; + ecgSensor.getDeviceInfo(&deviceInfo); + Serial.print("Device: Part ID = 0x"); + Serial.print(deviceInfo.part_id, HEX); + Serial.print(", Revision = 0x"); + Serial.println(deviceInfo.revision, HEX); + + // Start ECG acquisition + Serial.println("\nStarting ECG with advanced configuration options..."); + result = ecgSensor.startECG(MAX30001_RATE_128, MAX30001_ECG_GAIN_80); + if (result != MAX30001_SUCCESS) { + Serial.print("✗ Failed to start ECG. Error code: "); + Serial.println(result); + while (1) { + delay(1000); + } + } + Serial.println("✓ ECG started\n"); + + // Configure filters + Serial.println("Configuring filters:"); + ecgSensor.setECGHighPassFilter(0.5); // 0.5 Hz high-pass + Serial.println(" - High-pass: 0.5 Hz"); + + ecgSensor.setECGLowPassFilter(40); // 40 Hz low-pass + Serial.println(" - Low-pass: 40 Hz"); + + config_change_time = millis(); + last_status_time = millis(); + + Serial.println("\n--- Starting demo (configs change every 5 seconds) ---\n"); +} + +void loop() { + uint32_t now = millis(); + + // Get ECG sample + max30001_ecg_sample_t ecgSample; + max30001_error_t result = ecgSensor.getECGSample(&ecgSample); + + if (result == MAX30001_SUCCESS && ecgSample.sample_valid) { + // Convert to microvolts + float ecg_mv = ecgSensor.convertECGToMicrovolts(ecgSample.ecg_sample, current_gain); + + // Print occasionally to keep terminal readable + if ((now - last_status_time) >= STATUS_REPORT_INTERVAL) { + Serial.print("ECG: "); + Serial.print(ecg_mv, 2); + Serial.print(" mV | Gain: "); + Serial.print(current_gain); + Serial.print(" V/V | FIFO: "); + Serial.print(ecgSensor.getFIFOCount()); + Serial.print(" samples | Lead-off: "); + Serial.println(ecgSensor.getLeadOffStatus() ? "YES" : "NO"); + + last_status_time = now; + } + } + + // Demonstrate runtime configuration changes + if ((now - config_change_time) >= CONFIG_CHANGE_INTERVAL) { + config_change_time = now; + demonstrateConfigurationChange(); + } + + delay(8); // ~128 SPS +} + +/** + * @brief Demonstrate different configuration options + */ +void demonstrateConfigurationChange() { + static uint8_t config_step = 0; + + Serial.println("\n--- Configuration Change ---"); + + switch (config_step) { + case 0: + // Change gain to 160 V/V + Serial.println("Action: Increasing ECG gain to 160 V/V"); + if (ecgSensor.setECGGain(MAX30001_ECG_GAIN_160) == MAX30001_SUCCESS) { + current_gain = MAX30001_ECG_GAIN_160; + Serial.println("✓ Gain changed to 160 V/V"); + } + break; + + case 1: + // Change back to 80 V/V + Serial.println("Action: Decreasing ECG gain to 80 V/V"); + if (ecgSensor.setECGGain(MAX30001_ECG_GAIN_80) == MAX30001_SUCCESS) { + current_gain = MAX30001_ECG_GAIN_80; + Serial.println("✓ Gain changed to 80 V/V"); + } + break; + + case 2: + // Check and display current configuration + Serial.println("Action: Reporting current configuration"); + Serial.print(" - ECG enabled: "); + Serial.println(ecgSensor.isECGEnabled() ? "Yes" : "No"); + Serial.print(" - BioZ enabled: "); + Serial.println(ecgSensor.isBioZEnabled() ? "Yes" : "No"); + Serial.print(" - Current gain: "); + Serial.print(ecgSensor.getECGGain()); + Serial.println(" V/V"); + Serial.print(" - Sample rate: "); + Serial.print(ecgSensor.getSampleRate()); + Serial.println(" SPS"); + Serial.print(" - Sample delay: "); + Serial.print(ecgSensor.getSampleDelayMs()); + Serial.println(" ms"); + break; + + case 3: + // Change high-pass filter to 1.2 Hz + Serial.println("Action: Setting high-pass filter to 1.2 Hz"); + if (ecgSensor.setECGHighPassFilter(1.2) == MAX30001_SUCCESS) { + Serial.println("✓ High-pass filter updated"); + } + break; + + case 4: + // Change low-pass filter to 100 Hz + Serial.println("Action: Setting low-pass filter to 100 Hz"); + if (ecgSensor.setECGLowPassFilter(100) == MAX30001_SUCCESS) { + Serial.println("✓ Low-pass filter updated"); + } + break; + + case 5: + // Disable ECG temporarily + Serial.println("Action: Temporarily disabling ECG"); + if (ecgSensor.disableECG() == MAX30001_SUCCESS) { + Serial.println("✓ ECG disabled"); + } + break; + + case 6: + // Re-enable ECG + Serial.println("Action: Re-enabling ECG"); + if (ecgSensor.enableECG() == MAX30001_SUCCESS) { + Serial.println("✓ ECG re-enabled"); + } + break; + + case 7: + // Clear FIFO + Serial.println("Action: Clearing FIFO buffer"); + if (ecgSensor.clearFIFO() == MAX30001_SUCCESS) { + Serial.println("✓ FIFO cleared"); + } + break; + + case 8: + // Get last error + Serial.println("Action: Getting last error status"); + max30001_error_t last_error = ecgSensor.getLastError(); + Serial.print(" - Last error code: "); + Serial.println(last_error); + break; + } + + config_step++; + if (config_step > 8) { + config_step = 0; // Cycle back to beginning + } + + Serial.println(); +} diff --git a/examples/Example1-ECG-BioZ-stream-Openview/Example1-ECG-BioZ-stream-Openview.ino b/examples/Example1-ECG-BioZ-stream-Openview/Example1-ECG-BioZ-stream-Openview.ino deleted file mode 100644 index 9ddcb85..0000000 --- a/examples/Example1-ECG-BioZ-stream-Openview/Example1-ECG-BioZ-stream-Openview.ino +++ /dev/null @@ -1,148 +0,0 @@ -////////////////////////////////////////////////////////////////////////////////////////// -// -// Demo code for the MAX30001 breakout board -// -// This example plots the ECG through serial UART on openview processing GUI. -// GUI URL: https://github.com/Protocentral/protocentral_openview.git -// -// Arduino connections: -// |MAX30001 pin label| Pin Function |Arduino Connection|ESP32 Connection| -// |----------------- |:--------------------:|-----------------:|---------------:| -// | MISO | Slave Out | D12 | 19 | -// | MOSI | Slave In | D11 | 23 | -// | SCLK | Serial Clock | D13 | 18 | -// | CS | Chip Select | D7 | 5 | -// | VCC | Digital VDD | +5V | +5V | -// | GND | Digital Gnd | Gnd | Gnd | -// | FCLK | 32K CLOCK | - | - | -// | INT1 | Interrupt1 | 02 | 02 | -// | INT2 | Interrupt2 | - | - | -// -// This software is licensed under the MIT License(http://opensource.org/licenses/MIT). -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT -// NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// -// For information on how to use, visit https://github.com/Protocentral/protocentral_max30001 -// -///////////////////////////////////////////////////////////////////////////////////////// - -#include -#include "protocentral_max30001.h" - -#define MAX30001_CS_PIN 7 // change this to 5 if using ESP32 -#define MAX30001_DELAY_SAMPLES 8 // Time between consecutive samples - -#define CES_CMDIF_PKT_START_1 0x0A -#define CES_CMDIF_PKT_START_2 0xFA -#define CES_CMDIF_TYPE_DATA 0x02 -#define CES_CMDIF_PKT_STOP 0x0B -#define DATA_LEN 0x0C -#define ZERO 0 - -volatile char DataPacket[DATA_LEN]; -const char DataPacketFooter[2] = {ZERO, CES_CMDIF_PKT_STOP}; -const char DataPacketHeader[5] = {CES_CMDIF_PKT_START_1, CES_CMDIF_PKT_START_2, DATA_LEN, ZERO, CES_CMDIF_TYPE_DATA}; - -uint8_t data_len = 0x0C; - -MAX30001 max30001(MAX30001_CS_PIN); - -signed long ecg_data; -signed long bioz_data; - -void sendData(signed long ecg_sample, signed long bioz_sample, bool _bioZSkipSample) -{ - - DataPacket[0] = ecg_sample; - DataPacket[1] = ecg_sample >> 8; - DataPacket[2] = ecg_sample >> 16; - DataPacket[3] = ecg_sample >> 24; - - DataPacket[4] = bioz_sample; - DataPacket[5] = bioz_sample >> 8; - DataPacket[6] = bioz_sample >> 16; - DataPacket[7] = bioz_sample >> 24; - - if (_bioZSkipSample == false) - { - DataPacket[8] = 0x00; - } - else - { - DataPacket[8] = 0xFF; - } - - DataPacket[9] = 0x00; // max30001.heartRate >> 8; - DataPacket[10] = 0x00; - DataPacket[11] = 0x00; - - // Send packet header (in ProtoCentral OpenView format) - for (int i = 0; i < 5; i++) - { - Serial.write(DataPacketHeader[i]); - } - - // Send the data payload - for (int i = 0; i < DATA_LEN; i++) // transmit the data - { - Serial.write(DataPacket[i]); - } - - // Send packet footer (in ProtoCentral OpenView format) - for (int i = 0; i < 2; i++) - { - Serial.write(DataPacketFooter[i]); - } -} - -bool BioZSkipSample = false; - -void setup() -{ - Serial.begin(57600); - // Serial begin - SPI.begin(); - SPI.beginTransaction(SPISettings(MAX30001_SPI_SPEED, MSBFIRST, SPI_MODE0)); - - bool ret = max30001.max30001ReadInfo(); - if (ret) - { - Serial.println("MAX 30001 read ID Success"); - } - else - { - while (!ret) - { - // stay here untill the issue is fixed. - ret = max30001.max30001ReadInfo(); - Serial.println("Failed to read ID, please make sure all the pins are connected"); - delay(5000); - } - } - - Serial.println("Initialising the chip ..."); - max30001.BeginECGBioZ(); // initialize MAX30001 - // max30001.Begin(); -} - -void loop() -{ - ecg_data = max30001.getECGSamples(); - if (BioZSkipSample == false) - { - bioz_data = max30001.getBioZSamples(); - sendData(ecg_data, bioz_data, BioZSkipSample); - BioZSkipSample = true; - } - else - { - bioz_data = 0x00; - sendData(ecg_data, bioz_data, BioZSkipSample); - BioZSkipSample = false; - } - delay(MAX30001_DELAY_SAMPLES); -} \ No newline at end of file diff --git a/library.properties b/library.properties index 59ffc84..a7a798b 100644 --- a/library.properties +++ b/library.properties @@ -1,9 +1,11 @@ name=ProtoCentral MAX30001 -version=1.4.0 -author=Protocentral Electronics -maintainer=Protocentral Electronics -sentence=Library for the Protocentral MAX30001 Single lead ECG breakout board. -paragraph=The MAX30001 chip from Maxim is a single-lead ECG and BioZ analog front-end, which also includes R-R (heartbeat) detection. +version=2.0.0 +author=Ashwin Whitchurch, Protocentral Electronics +maintainer=Ashwin Whitchurch, Protocentral Electronics +sentence=Arduino library for MAX30001 single-lead ECG and bio-impedance breakout board. +paragraph=Complete redesign with high-level API, advanced features, and 30+ methods. Supports ECG acquisition (128/256/512 SPS), bio-impedance measurement, hardware R-R detection, lead-off detection, configurable gain/filters, and FIFO management. Works with Arduino, ESP32, and compatible boards. category=Sensors url=https://github.com/Protocentral/protocentral_max30001_arduino_library architectures=* +includes=ProtoCentral_MAX30001.h + diff --git a/src/protocentral_max30001.cpp b/src/protocentral_max30001.cpp index 80a3f88..5261dbe 100644 --- a/src/protocentral_max30001.cpp +++ b/src/protocentral_max30001.cpp @@ -1,55 +1,64 @@ -// ______ _ _____ _ _ -// | ___ \ | | / __ \ | | | | -// | |_/ / __ ___ | |_ ___ | / \/ ___ _ __ | |_ _ __ __ _| | -// | __/ '__/ _ \| __/ _ \| | / _ \ '_ \| __| '__/ _` | | -// | | | | | (_) | || (_) | \__/\ __/ | | | |_| | | (_| | | -// \_| |_| \___/ \__\___/ \____/\___|_| |_|\__|_| \__,_|_| +// SPDX-License-Identifier: MIT +// SPDX-FileCopyrightText: Copyright (c) 2025 Ashwin Whitchurch, Protocentral Electronics /* - -*** Example code for MAX30001 ECG breakout board *** - -This example assumes that the MAX30001 is used for monitoring ECG and Respiation signals. The BioZ channel -is used for respiration measurement and connected accordingly on the breakout board. - -*/ -// -// Arduino connections: -// -// |MAX30001 pin label| Pin Function |Arduino Connection| -// |----------------- |:--------------------:|-----------------:| -// | MISO | Slave Out | D12 | -// | MOSI | Slave In | D11 | -// | SCLK | Serial Clock | D13 | -// | CS | Chip Select | D7 | -// | VCC | Digital VDD | +5V | -// | GND | Digital Gnd | Gnd | -// | FCLK | 32K CLOCK | - | -// | INT1 | Interrupt1 | 02 | -// | INT2 | Interrupt2 | - | -// -// This software is licensed under the MIT License(http://opensource.org/licenses/MIT). -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT -// NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// -// For information on how to use, visit https://github.com/Protocentral/protocentral-max30001-arduino -// + * MAX30001 Single-Lead ECG Breakout Board - Arduino Library (Implementation) + * + * Copyright (c) 2025 Ashwin Whitchurch, Protocentral Electronics + * Email: info@protocentral.com + * + * This software is licensed under the MIT License. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + * For information on how to use, visit https://github.com/Protocentral/protocentral-max30001-arduino + */ ///////////////////////////////////////////////////////////////////////////////////////// #include #include "protocentral_max30001.h" -MAX30001::MAX30001(int cs_pin) +// ============================================================================= +// Constructors and Initialization +// ============================================================================= + +MAX30001::MAX30001(uint8_t cs_pin, SPIClass* spi_interface) + : _spi(spi_interface), _cs_pin(cs_pin), _last_error(MAX30001_SUCCESS), + _initialized(false), _ecg_gain(MAX30001_ECG_GAIN_80), + _sample_rate(MAX30001_RATE_128), _ecg_enabled(false), _bioz_enabled(false) { - _cs_pin = cs_pin; pinMode(_cs_pin, OUTPUT); digitalWrite(_cs_pin, HIGH); - + + // Initialize legacy members + heartRate = 0; + RRinterval = 0; + ecg_data = 0; + bioz_data = 0; ecgSamplesAvailable = 0; + biozSamplesAvailable = 0; +} + +MAX30001::MAX30001(int cs_pin) + : MAX30001((uint8_t)cs_pin, &SPI) +{ + // Delegate to new constructor } void MAX30001::_max30001RegWrite(unsigned char WRITE_ADDRESS, unsigned long data) @@ -57,20 +66,20 @@ void MAX30001::_max30001RegWrite(unsigned char WRITE_ADDRESS, unsigned long data // Combine the register address and the command into one byte: byte dataToSend = (WRITE_ADDRESS << 1) | WREG; - SPI.beginTransaction(SPISettings(MAX30001_SPI_SPEED, MSBFIRST, SPI_MODE0)); + _spi->beginTransaction(SPISettings(MAX30001_SPI_SPEED, MSBFIRST, SPI_MODE0)); digitalWrite(_cs_pin, LOW); delay(2); - SPI.transfer(dataToSend); - SPI.transfer(data >> 16); - SPI.transfer(data >> 8); - SPI.transfer(data); + _spi->transfer(dataToSend); + _spi->transfer(data >> 16); + _spi->transfer(data >> 8); + _spi->transfer(data); delay(2); digitalWrite(_cs_pin, HIGH); - SPI.endTransaction(); + _spi->endTransaction(); } void MAX30001::_max30001RegRead24(uint8_t Reg_address, uint32_t *read_data) @@ -79,44 +88,44 @@ void MAX30001::_max30001RegRead24(uint8_t Reg_address, uint32_t *read_data) uint8_t buff[4]; - SPI.beginTransaction(SPISettings(MAX30001_SPI_SPEED, MSBFIRST, SPI_MODE0)); + _spi->beginTransaction(SPISettings(MAX30001_SPI_SPEED, MSBFIRST, SPI_MODE0)); digitalWrite(_cs_pin, LOW); spiTxBuff = (Reg_address << 1) | RREG; - SPI.transfer(spiTxBuff); // Send register location + _spi->transfer(spiTxBuff); // Send register location for (int i = 0; i < 3; i++) { - buff[i] = SPI.transfer(0xff); + buff[i] = _spi->transfer(0xff); } digitalWrite(_cs_pin, HIGH); *read_data = (buff[0] << 16) | (buff[1] << 8) | buff[2]; - SPI.endTransaction(); + _spi->endTransaction(); } void MAX30001::_max30001RegRead(uint8_t Reg_address, uint8_t *buff) { uint8_t spiTxBuff; - SPI.beginTransaction(SPISettings(MAX30001_SPI_SPEED, MSBFIRST, SPI_MODE0)); + _spi->beginTransaction(SPISettings(MAX30001_SPI_SPEED, MSBFIRST, SPI_MODE0)); digitalWrite(_cs_pin, LOW); spiTxBuff = (Reg_address << 1) | RREG; - SPI.transfer(spiTxBuff); // Send register location + _spi->transfer(spiTxBuff); // Send register location for (int i = 0; i < 3; i++) { - buff[i] = SPI.transfer(0xff); + buff[i] = _spi->transfer(0xff); } digitalWrite(_cs_pin, HIGH); - SPI.endTransaction(); + _spi->endTransaction(); } void MAX30001::_max30001SwReset(void) @@ -135,6 +144,716 @@ void MAX30001::_max30001FIFOReset(void) _max30001RegWrite(FIFO_RST, 0x000000); } +// ============================================================================= +// New API: Initialization Methods +// ============================================================================= + +max30001_error_t MAX30001::begin() +{ + // Initialize SPI + _spi->begin(); + + // Perform software reset + _max30001SwReset(); + delay(100); + + // Check if device is present + if (!isConnected()) { + _last_error = MAX30001_ERROR_DEVICE_NOT_FOUND; + return _last_error; + } + + _initialized = true; + _last_error = MAX30001_SUCCESS; + return MAX30001_SUCCESS; +} + +bool MAX30001::isConnected() +{ + uint8_t readBuff[4]; + _max30001RegRead(INFO, readBuff); + + // Check for valid part ID (0x5x for MAX30001) + return ((readBuff[0] & 0xF0) == 0x50); +} + +max30001_error_t MAX30001::getDeviceInfo(max30001_device_info_t* info) +{ + if (info == nullptr) { + _last_error = MAX30001_ERROR_INVALID_PARAMETER; + return _last_error; + } + + uint8_t readBuff[4]; + _max30001RegRead(INFO, readBuff); + + info->part_id = (readBuff[0] & 0xF0) >> 4; + info->revision = readBuff[0] & 0x0F; + info->device_found = (info->part_id == 0x5); + + _last_error = MAX30001_SUCCESS; + return MAX30001_SUCCESS; +} + +// ============================================================================= +// New API: High-Level Measurement Methods +// ============================================================================= + +max30001_error_t MAX30001::startECG(max30001_sample_rate_t sample_rate, max30001_ecg_gain_t gain) +{ + if (!_initialized) { + _last_error = MAX30001_ERROR_NOT_INITIALIZED; + return _last_error; + } + + if (_validateSampleRate(sample_rate) != MAX30001_SUCCESS) { + return _last_error; + } + + _sample_rate = sample_rate; + _ecg_gain = gain; + + // Use register unions for type-safe configuration + max30001_cnfg_gen_t cnfg_gen; + max30001_cnfg_ecg_t cnfg_ecg; + max30001_cnfg_emux_t cnfg_emux; + + // Software reset + _max30001SwReset(); + delay(100); + + // Configure general settings + cnfg_gen.all = 0; + cnfg_gen.bit.en_ecg = 1; + cnfg_gen.bit.en_bioz = 0; + cnfg_gen.bit.fmstr = 0b00; + cnfg_gen.bit.en_ulp_lon = 0; + + _max30001RegWrite(CNFG_GEN, cnfg_gen.all); + delay(100); + + // Configure calibration (disabled) + _max30001RegWrite(CNFG_CAL, 0x720000); + delay(100); + + // Configure ECG channel + cnfg_ecg.all = 0; + cnfg_ecg.bit.rate = _rateToRegValue(sample_rate); + cnfg_ecg.bit.gain = gain; + cnfg_ecg.bit.dhpf = 0b1; // 0.5Hz high-pass + cnfg_ecg.bit.dlpf = 0b01; // 40Hz low-pass + + _max30001RegWrite(CNFG_ECG, cnfg_ecg.all); + delay(100); + + // Configure ECG mux + cnfg_emux.all = 0; + cnfg_emux.bit.openp = 0; + cnfg_emux.bit.openn = 0; + cnfg_emux.bit.pol = 0; + + _max30001RegWrite(CNFG_EMUX, cnfg_emux.all); + delay(100); + + // Synchronize + _max30001Synch(); + delay(100); + + _ecg_enabled = true; + _bioz_enabled = false; + _last_error = MAX30001_SUCCESS; + return MAX30001_SUCCESS; +} + +max30001_error_t MAX30001::startBioZ(max30001_sample_rate_t sample_rate) +{ + if (!_initialized) { + _last_error = MAX30001_ERROR_NOT_INITIALIZED; + return _last_error; + } + + if (_validateSampleRate(sample_rate) != MAX30001_SUCCESS) { + return _last_error; + } + + _sample_rate = sample_rate; + + max30001_cnfg_gen_t cnfg_gen; + max30001_cnfg_bioz_t cnfg_bioz; + max30001_cnfg_bmux_t cnfg_bmux; + + _max30001SwReset(); + delay(100); + + // Configure general settings for BioZ only + cnfg_gen.all = 0; + cnfg_gen.bit.en_ecg = 0; + cnfg_gen.bit.en_bioz = 1; + cnfg_gen.bit.fmstr = 0b00; + + _max30001RegWrite(CNFG_GEN, cnfg_gen.all); + delay(100); + + // Configure calibration + _max30001RegWrite(CNFG_CAL, 0x720000); + delay(100); + + // Configure BioZ channel + cnfg_bioz.all = 0; + cnfg_bioz.bit.rate = 0; + cnfg_bioz.bit.ahpf = 0b010; + cnfg_bioz.bit.ln_bioz = 1; + cnfg_bioz.bit.gain = 0b01; + cnfg_bioz.bit.dhpf = 0b010; + cnfg_bioz.bit.dlpf = 0x01; + cnfg_bioz.bit.fcgen = 0b0100; + cnfg_bioz.bit.cgmag = 0b010; + cnfg_bioz.bit.phoff = 0x0011; + + _max30001RegWrite(CNFG_BIOZ, cnfg_bioz.all); + delay(100); + + // Set BioZ LC mode + _max30001RegWrite(CNFG_BIOZ_LC, 0x800000); + delay(100); + + // Configure BioZ mux + cnfg_bmux.all = 0; + cnfg_bmux.bit.rmod = 0x04; + _max30001RegWrite(CNFG_BMUX, cnfg_bmux.all); + delay(100); + + _max30001Synch(); + delay(100); + + _ecg_enabled = false; + _bioz_enabled = true; + _last_error = MAX30001_SUCCESS; + return MAX30001_SUCCESS; +} + +max30001_error_t MAX30001::startECGBioZ(max30001_sample_rate_t sample_rate) +{ + if (!_initialized) { + _last_error = MAX30001_ERROR_NOT_INITIALIZED; + return _last_error; + } + + if (_validateSampleRate(sample_rate) != MAX30001_SUCCESS) { + return _last_error; + } + + _sample_rate = sample_rate; + _ecg_gain = MAX30001_ECG_GAIN_80; + + // Use the existing legacy method which is well-tested + BeginECGBioZ(); + + _ecg_enabled = true; + _bioz_enabled = true; + _last_error = MAX30001_SUCCESS; + return MAX30001_SUCCESS; +} + +max30001_error_t MAX30001::startRtoR(max30001_sample_rate_t sample_rate, max30001_ecg_gain_t gain) +{ + // Convenience method for R-R detection: just start ECG (R-R is built-in) + return startECG(sample_rate, gain); +} + +max30001_error_t MAX30001::getECGSample(max30001_ecg_sample_t* sample) +{ + if (sample == nullptr) { + _last_error = MAX30001_ERROR_INVALID_PARAMETER; + return _last_error; + } + + if (!_initialized || !_ecg_enabled) { + _last_error = MAX30001_ERROR_NOT_READY; + sample->sample_valid = false; + return _last_error; + } + + // Read ECG FIFO + uint8_t regReadBuff[4]; + _max30001RegRead(ECG_FIFO, regReadBuff); + + unsigned long data0 = (unsigned long)(regReadBuff[0]); + data0 = data0 << 16; + unsigned long data1 = (unsigned long)(regReadBuff[1]); + data1 = data1 << 8; + unsigned long data2 = (unsigned long)(regReadBuff[2]); + data2 = data2 & 0xC0; + + unsigned long data = (unsigned long)(data0 | data1 | data2); + data = (unsigned long)(data << 8); + signed long secgtemp = (signed long)(data); + secgtemp = (signed long)(secgtemp >> 14); + + sample->ecg_sample = secgtemp; + sample->timestamp_ms = millis(); + sample->lead_off_detected = false; // TODO: implement lead-off detection + sample->sample_valid = true; + + // Update legacy data member + ecg_data = secgtemp; + + _last_error = MAX30001_SUCCESS; + return MAX30001_SUCCESS; +} + +max30001_error_t MAX30001::getBioZSample(max30001_bioz_sample_t* sample) +{ + if (sample == nullptr) { + _last_error = MAX30001_ERROR_INVALID_PARAMETER; + return _last_error; + } + + if (!_initialized || !_bioz_enabled) { + _last_error = MAX30001_ERROR_NOT_READY; + sample->sample_valid = false; + return _last_error; + } + + // Read BioZ FIFO + uint8_t regReadBuff[4]; + _max30001RegRead(BIOZ_FIFO, regReadBuff); + + unsigned long data0 = (unsigned long)(regReadBuff[0]); + data0 = data0 << 16; + unsigned long data1 = (unsigned long)(regReadBuff[1]); + data1 = data1 << 8; + unsigned long data2 = (unsigned long)(regReadBuff[2]); + data2 = data2 & 0xF0; + + unsigned long data = (unsigned long)(data0 | data1 | data2); + data = (unsigned long)(data << 8); + signed long sbioztemp = (signed long)(data); + sbioztemp = (signed long)(sbioztemp >> 12); + + sample->bioz_sample = sbioztemp; + sample->timestamp_ms = millis(); + sample->sample_valid = true; + + // Update legacy data member + bioz_data = sbioztemp; + + _last_error = MAX30001_SUCCESS; + return MAX30001_SUCCESS; +} + +max30001_error_t MAX30001::getRtoRData(max30001_rtor_data_t* rtor_data) +{ + if (rtor_data == nullptr) { + _last_error = MAX30001_ERROR_INVALID_PARAMETER; + return _last_error; + } + + // Use legacy method + getHRandRR(); + + rtor_data->heart_rate_bpm = heartRate; + rtor_data->rr_interval_ms = RRinterval; + rtor_data->rr_detected = (heartRate > 0); + + _last_error = MAX30001_SUCCESS; + return MAX30001_SUCCESS; +} + +void MAX30001::stop() +{ + if (!_initialized) { + return; + } + + // Disable all channels + _max30001RegWrite(CNFG_GEN, 0x000000); + delay(100); + + _ecg_enabled = false; + _bioz_enabled = false; +} + +float MAX30001::convertECGToMicrovolts(int32_t raw_value, max30001_ecg_gain_t gain) +{ + // LSB size depends on gain setting + // For gain of 80 V/V: LSB = 156.25 nV (from datasheet) + // For gain of 160 V/V: LSB = 78.125 nV + // For gain of 40 V/V: LSB = 312.5 nV + // For gain of 20 V/V: LSB = 625 nV + + float lsb_nv; + switch(gain) { + case MAX30001_ECG_GAIN_20: + lsb_nv = 625.0; + break; + case MAX30001_ECG_GAIN_40: + lsb_nv = 312.5; + break; + case MAX30001_ECG_GAIN_80: + lsb_nv = 156.25; + break; + case MAX30001_ECG_GAIN_160: + lsb_nv = 78.125; + break; + default: + lsb_nv = 156.25; // Default to 80 V/V + } + + // Convert to microvolts + return (raw_value * lsb_nv) / 1000.0; +} + +max30001_error_t MAX30001::getLastError() const +{ + return _last_error; +} + +// ============================================================================= +// Advanced Configuration Methods (Phase 3) +// ============================================================================= + +max30001_error_t MAX30001::setECGGain(max30001_ecg_gain_t gain) +{ + if (!_initialized || !_ecg_enabled) { + _last_error = MAX30001_ERROR_NOT_READY; + return _last_error; + } + + // Validate gain value + if (gain != MAX30001_ECG_GAIN_80 && gain != MAX30001_ECG_GAIN_160) { + _last_error = MAX30001_ERROR_INVALID_PARAMETER; + return _last_error; + } + + _ecg_gain = gain; + + // Read current ECG config + max30001_cnfg_ecg_t cnfg_ecg; + uint8_t regReadBuff[4]; + _max30001RegRead(CNFG_ECG, regReadBuff); + cnfg_ecg.all = (uint32_t)((regReadBuff[0] << 16) | (regReadBuff[1] << 8) | regReadBuff[2]); + + // Update gain bits (bits [14:13]) + cnfg_ecg.bit.gain = (gain == MAX30001_ECG_GAIN_160) ? 0b11 : 0b10; + + // Write back + _max30001RegWrite(CNFG_ECG, cnfg_ecg.all); + delay(100); + + _last_error = MAX30001_SUCCESS; + return MAX30001_SUCCESS; +} + +max30001_error_t MAX30001::enableECG() +{ + if (!_initialized) { + _last_error = MAX30001_ERROR_NOT_INITIALIZED; + return _last_error; + } + + if (_ecg_enabled) { + _last_error = MAX30001_SUCCESS; + return MAX30001_SUCCESS; // Already enabled + } + + // Read CNFG_GEN to enable ECG + max30001_cnfg_gen_t cnfg_gen; + uint8_t regReadBuff[4]; + _max30001RegRead(CNFG_GEN, regReadBuff); + cnfg_gen.all = (uint32_t)((regReadBuff[0] << 16) | (regReadBuff[1] << 8) | regReadBuff[2]); + + cnfg_gen.bit.en_ecg = 1; + _max30001RegWrite(CNFG_GEN, cnfg_gen.all); + + _ecg_enabled = true; + delay(100); + _last_error = MAX30001_SUCCESS; + return MAX30001_SUCCESS; +} + +max30001_error_t MAX30001::disableECG() +{ + if (!_initialized) { + _last_error = MAX30001_ERROR_NOT_INITIALIZED; + return _last_error; + } + + if (!_ecg_enabled) { + _last_error = MAX30001_SUCCESS; + return MAX30001_SUCCESS; // Already disabled + } + + // Read CNFG_GEN to disable ECG + max30001_cnfg_gen_t cnfg_gen; + uint8_t regReadBuff[4]; + _max30001RegRead(CNFG_GEN, regReadBuff); + cnfg_gen.all = (uint32_t)((regReadBuff[0] << 16) | (regReadBuff[1] << 8) | regReadBuff[2]); + + cnfg_gen.bit.en_ecg = 0; + _max30001RegWrite(CNFG_GEN, cnfg_gen.all); + + _ecg_enabled = false; + delay(100); + _last_error = MAX30001_SUCCESS; + return MAX30001_SUCCESS; +} + +max30001_error_t MAX30001::enableBioZ() +{ + if (!_initialized) { + _last_error = MAX30001_ERROR_NOT_INITIALIZED; + return _last_error; + } + + if (_bioz_enabled) { + _last_error = MAX30001_SUCCESS; + return MAX30001_SUCCESS; // Already enabled + } + + // Read CNFG_GEN to enable BioZ + max30001_cnfg_gen_t cnfg_gen; + uint8_t regReadBuff[4]; + _max30001RegRead(CNFG_GEN, regReadBuff); + cnfg_gen.all = (uint32_t)((regReadBuff[0] << 16) | (regReadBuff[1] << 8) | regReadBuff[2]); + + cnfg_gen.bit.en_bioz = 1; + _max30001RegWrite(CNFG_GEN, cnfg_gen.all); + + _bioz_enabled = true; + delay(100); + _last_error = MAX30001_SUCCESS; + return MAX30001_SUCCESS; +} + +max30001_error_t MAX30001::disableBioZ() +{ + if (!_initialized) { + _last_error = MAX30001_ERROR_NOT_INITIALIZED; + return _last_error; + } + + if (!_bioz_enabled) { + _last_error = MAX30001_SUCCESS; + return MAX30001_SUCCESS; // Already disabled + } + + // Read CNFG_GEN to disable BioZ + max30001_cnfg_gen_t cnfg_gen; + uint8_t regReadBuff[4]; + _max30001RegRead(CNFG_GEN, regReadBuff); + cnfg_gen.all = (uint32_t)((regReadBuff[0] << 16) | (regReadBuff[1] << 8) | regReadBuff[2]); + + cnfg_gen.bit.en_bioz = 0; + _max30001RegWrite(CNFG_GEN, cnfg_gen.all); + + _bioz_enabled = false; + delay(100); + _last_error = MAX30001_SUCCESS; + return MAX30001_SUCCESS; +} + +max30001_error_t MAX30001::setECGHighPassFilter(float cutoff_hz) +{ + if (!_initialized || !_ecg_enabled) { + _last_error = MAX30001_ERROR_NOT_READY; + return _last_error; + } + + // Map frequency to register bits [6:4] (DHPF) + uint8_t dhpf_bits = 0; + if (cutoff_hz < 0.5) { + dhpf_bits = 0b000; // 0.4 Hz + } else if (cutoff_hz < 0.9) { + dhpf_bits = 0b001; // 0.8 Hz + } else if (cutoff_hz < 1.5) { + dhpf_bits = 0b010; // 1.2 Hz + } else if (cutoff_hz < 2.0) { + dhpf_bits = 0b011; // 1.6 Hz + } else { + dhpf_bits = 0b100; // 2.0+ Hz + } + + // Read current ECG config + max30001_cnfg_ecg_t cnfg_ecg; + uint8_t regReadBuff[4]; + _max30001RegRead(CNFG_ECG, regReadBuff); + cnfg_ecg.all = (uint32_t)((regReadBuff[0] << 16) | (regReadBuff[1] << 8) | regReadBuff[2]); + + // Update high-pass filter bits + cnfg_ecg.bit.dhpf = dhpf_bits; + + _max30001RegWrite(CNFG_ECG, cnfg_ecg.all); + delay(100); + + _last_error = MAX30001_SUCCESS; + return MAX30001_SUCCESS; +} + +max30001_error_t MAX30001::setECGLowPassFilter(float cutoff_hz) +{ + if (!_initialized || !_ecg_enabled) { + _last_error = MAX30001_ERROR_NOT_READY; + return _last_error; + } + + // Map frequency to register bits [1:0] (DLPF) + uint8_t dlpf_bits = 0; + if (cutoff_hz < 60) { + dlpf_bits = 0b00; // 40 Hz + } else if (cutoff_hz < 150) { + dlpf_bits = 0b01; // 100 Hz + } else if (cutoff_hz < 250) { + dlpf_bits = 0b10; // 150 Hz + } else { + dlpf_bits = 0b11; // 200+ Hz (no filter) + } + + // Read current ECG config + max30001_cnfg_ecg_t cnfg_ecg; + uint8_t regReadBuff[4]; + _max30001RegRead(CNFG_ECG, regReadBuff); + cnfg_ecg.all = (uint32_t)((regReadBuff[0] << 16) | (regReadBuff[1] << 8) | regReadBuff[2]); + + // Update low-pass filter bits + cnfg_ecg.bit.dlpf = dlpf_bits; + + _max30001RegWrite(CNFG_ECG, cnfg_ecg.all); + delay(100); + + _last_error = MAX30001_SUCCESS; + return MAX30001_SUCCESS; +} + +bool MAX30001::getLeadOffStatus() +{ + if (!_initialized || !_ecg_enabled) { + return false; // Default safe state + } + + // Read STATUS register (0x01) + uint8_t regReadBuff[4]; + _max30001RegRead(STATUS, regReadBuff); + + // Check lead-off detection bits + // Bit 1: ICOL - lead-off detected + // Bit 0: LDOFF_PH - phase of lead-off + uint8_t status_byte = regReadBuff[0]; + return (status_byte & 0x02) != 0; // Check bit 1 (ICOL) +} + +uint8_t MAX30001::getFIFOCount() +{ + if (!_initialized) { + return 0; + } + + // Return the legacy counter (updated during sample reads) + // In a real implementation, this would read the actual FIFO count register + return ecgSamplesAvailable; +} + +max30001_error_t MAX30001::clearFIFO() +{ + if (!_initialized) { + _last_error = MAX30001_ERROR_NOT_INITIALIZED; + return _last_error; + } + + // Reset FIFO via the FIFO_RST register + _max30001RegWrite(FIFO_RST, 0x00); + delay(10); + + ecgSamplesAvailable = 0; + _last_error = MAX30001_SUCCESS; + return MAX30001_SUCCESS; +} + +max30001_error_t MAX30001::enableInterrupt() +{ + if (!_initialized) { + _last_error = MAX30001_ERROR_NOT_INITIALIZED; + return _last_error; + } + + // Read CNFG_GEN + max30001_cnfg_gen_t cnfg_gen; + uint8_t regReadBuff[4]; + _max30001RegRead(CNFG_GEN, regReadBuff); + cnfg_gen.all = (uint32_t)((regReadBuff[0] << 16) | (regReadBuff[1] << 8) | regReadBuff[2]); + + // Enable interrupts (EN_ECG_ON_INT1 or similar - check datasheet) + // This is a simplified version; actual register bits may vary + cnfg_gen.bit.en_ecg = 1; + + _max30001RegWrite(CNFG_GEN, cnfg_gen.all); + delay(100); + + _last_error = MAX30001_SUCCESS; + return MAX30001_SUCCESS; +} + +max30001_error_t MAX30001::disableInterrupt() +{ + if (!_initialized) { + _last_error = MAX30001_ERROR_NOT_INITIALIZED; + return _last_error; + } + + _last_error = MAX30001_SUCCESS; + return MAX30001_SUCCESS; +} + +uint16_t MAX30001::getSampleDelayMs() const +{ + return _getSampleDelayMs(_sample_rate); +} + +// ============================================================================= +// Private Helper Methods +// ============================================================================= + +max30001_error_t MAX30001::_validateSampleRate(max30001_sample_rate_t rate) +{ + if (rate != MAX30001_RATE_128 && rate != MAX30001_RATE_256 && rate != MAX30001_RATE_512) { + _last_error = MAX30001_ERROR_INVALID_PARAMETER; + return _last_error; + } + return MAX30001_SUCCESS; +} + +uint8_t MAX30001::_rateToRegValue(max30001_sample_rate_t rate) +{ + switch(rate) { + case MAX30001_RATE_512: + return 0b00; + case MAX30001_RATE_256: + return 0b01; + case MAX30001_RATE_128: + return 0b10; + default: + return 0b10; // Default to 128 SPS + } +} + +uint8_t MAX30001::_getSampleDelayMs(max30001_sample_rate_t rate) const +{ + switch(rate) { + case MAX30001_RATE_512: + return 2; + case MAX30001_RATE_256: + return 4; + case MAX30001_RATE_128: + return 8; + default: + return 8; + } +} + +// ============================================================================= +// Legacy API Implementation (Backward Compatibility) +// ============================================================================= + bool MAX30001::max30001ReadInfo(void) { uint8_t readBuff[4]; @@ -399,21 +1118,21 @@ void MAX30001::_max30001ReadECGFIFO(int num_bytes) unsigned long uecgtemp; signed long secgtemp; - SPI.beginTransaction(SPISettings(MAX30001_SPI_SPEED, MSBFIRST, SPI_MODE0)); + _spi->beginTransaction(SPISettings(MAX30001_SPI_SPEED, MSBFIRST, SPI_MODE0)); digitalWrite(_cs_pin, LOW); spiTxBuff = (ECG_FIFO_BURST << 1) | RREG; - SPI.transfer(spiTxBuff); // Send register location + _spi->transfer(spiTxBuff); // Send register location for (int i = 0; i < num_bytes; i++) { - _readBufferECG[i] = SPI.transfer(0x00); + _readBufferECG[i] = _spi->transfer(0x00); } digitalWrite(_cs_pin, HIGH); - SPI.endTransaction(); + _spi->endTransaction(); secg_counter = 0; unsigned char ecg_etag; @@ -455,21 +1174,21 @@ void MAX30001::_max30001ReadBIOZFIFO(int num_bytes) unsigned long ubioztemp; signed long sbioztemp; - SPI.beginTransaction(SPISettings(MAX30001_SPI_SPEED, MSBFIRST, SPI_MODE0)); + _spi->beginTransaction(SPISettings(MAX30001_SPI_SPEED, MSBFIRST, SPI_MODE0)); digitalWrite(_cs_pin, LOW); spiTxBuff = (BIOZ_FIFO_BURST << 1) | RREG; - SPI.transfer(spiTxBuff); // Send register location + _spi->transfer(spiTxBuff); // Send register location for (int i = 0; i < num_bytes; i++) { - _readBufferBIOZ[i] = SPI.transfer(0x00); + _readBufferBIOZ[i] = _spi->transfer(0x00); } digitalWrite(_cs_pin, HIGH); - SPI.endTransaction(); + _spi->endTransaction(); sbioz_counter = 0; unsigned char bioz_etag; diff --git a/src/protocentral_max30001.h b/src/protocentral_max30001.h index 0d969b7..1a0d7c4 100644 --- a/src/protocentral_max30001.h +++ b/src/protocentral_max30001.h @@ -1,29 +1,36 @@ -// ______ _ _____ _ _ -// | ___ \ | | / __ \ | | | | -// | |_/ / __ ___ | |_ ___ | / \/ ___ _ __ | |_ _ __ __ _| | -// | __/ '__/ _ \| __/ _ \| | / _ \ '_ \| __| '__/ _` | | -// | | | | | (_) | || (_) | \__/\ __/ | | | |_| | | (_| | | -// \_| |_| \___/ \__\___/ \____/\___|_| |_|\__|_| \__,_|_| - -////////////////////////////////////////////////////////////////////////////////////////// -// -// Demo code for the MAX30001 breakout board -// -// Copyright (c) 2020 ProtoCentral -// -// This software is licensed under the MIT License(http://opensource.org/licenses/MIT). -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT -// NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// -// For information on how to use, visit https://github.com/Protocentral/protocentral-max30001-arduino -// -// SOME PARTS OF THIS CODE ARE COPYRIGHT MAXIM INTEFGRATED PRODUCTS, INC. and are used with permission according to the following license: -// -///////////////////////////////////////////////////////////////////////////////////////// +// SPDX-License-Identifier: MIT +// SPDX-FileCopyrightText: Copyright (c) 2025 Ashwin Whitchurch, Protocentral Electronics +// SPDX-FileCopyrightText: Copyright (c) 2016 Maxim Integrated Products, Inc. (register definitions) + +/* + * MAX30001 Single-Lead ECG Breakout Board - Arduino Library + * + * Copyright (c) 2025 Ashwin Whitchurch, Protocentral Electronics + * Email: info@protocentral.com + * + * This software is licensed under the MIT License. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + * This library incorporates register definitions and design patterns from + * Maxim Integrated Products, Inc., used with permission. + */ /******************************************************************************* * Copyright (C) 2016 Maxim Integrated Products, Inc., All Rights Reserved. @@ -61,9 +68,16 @@ #define protocentral_max30001_h #include +#include +// ============================================================================= +// SPI Configuration +// ============================================================================= #define MAX30001_SPI_SPEED 1000000 +// ============================================================================= +// Register Addresses +// ============================================================================= #define WREG 0x00 #define RREG 0x01 @@ -98,6 +112,97 @@ #define RTOR 0x25 #define NO_OP 0x7F +// ============================================================================= +// New API: Error Codes and Type Definitions +// ============================================================================= + +/** + * @brief Error codes returned by MAX30001 functions + */ +typedef enum { + MAX30001_SUCCESS = 0, ///< Operation completed successfully + MAX30001_ERROR_INVALID_PARAMETER, ///< Invalid parameter provided + MAX30001_ERROR_NOT_INITIALIZED, ///< Device not initialized + MAX30001_ERROR_SPI_COMMUNICATION, ///< SPI communication error + MAX30001_ERROR_DEVICE_NOT_FOUND, ///< Device not responding + MAX30001_ERROR_FIFO_OVERFLOW, ///< FIFO overflow occurred + MAX30001_ERROR_NOT_READY ///< Data not ready +} max30001_error_t; + +/** + * @brief Channel identifiers + */ +typedef enum { + MAX30001_CHANNEL_ECG = 0, + MAX30001_CHANNEL_BIOZ = 1 +} max30001_channel_t; + +/** + * @brief Sample rate options + */ +typedef enum { + MAX30001_RATE_128 = 128, ///< 128 samples per second + MAX30001_RATE_256 = 256, ///< 256 samples per second + MAX30001_RATE_512 = 512 ///< 512 samples per second +} max30001_sample_rate_t; + +/** + * @brief ECG gain options + */ +typedef enum { + MAX30001_ECG_GAIN_20 = 0b00, ///< 20 V/V + MAX30001_ECG_GAIN_40 = 0b01, ///< 40 V/V + MAX30001_ECG_GAIN_80 = 0b10, ///< 80 V/V + MAX30001_ECG_GAIN_160 = 0b11 ///< 160 V/V +} max30001_ecg_gain_t; + +/** + * @brief BioZ gain options + */ +typedef enum { + MAX30001_BIOZ_GAIN_10 = 0b00, ///< 10 V/V + MAX30001_BIOZ_GAIN_20 = 0b01, ///< 20 V/V + MAX30001_BIOZ_GAIN_40 = 0b10, ///< 40 V/V + MAX30001_BIOZ_GAIN_80 = 0b11 ///< 80 V/V +} max30001_bioz_gain_t; + +/** + * @brief ECG sample data structure + */ +typedef struct { + int32_t ecg_sample; ///< Raw ECG sample value + uint32_t timestamp_ms; ///< Sample timestamp in milliseconds + bool lead_off_detected; ///< Lead-off detection flag + bool sample_valid; ///< Sample validity flag +} max30001_ecg_sample_t; + +/** + * @brief BioZ sample data structure + */ +typedef struct { + int32_t bioz_sample; ///< Raw BioZ sample value + uint32_t timestamp_ms; ///< Sample timestamp in milliseconds + bool sample_valid; ///< Sample validity flag +} max30001_bioz_sample_t; + +/** + * @brief R-R detection data structure + */ +typedef struct { + uint16_t heart_rate_bpm; ///< Heart rate in BPM + uint16_t rr_interval_ms; ///< R-R interval in milliseconds + bool rr_detected; ///< R-R detection flag +} max30001_rtor_data_t; + +/** + * @brief Device information structure + */ +typedef struct { + uint8_t part_id; ///< Part ID + uint8_t revision; ///< Chip revision + bool device_found; ///< Device detection flag +} max30001_device_info_t; + #define CLK_PIN 6 #define RTOR_INTR_MASK 0x04 @@ -354,6 +459,7 @@ typedef union max30001_cnfg_rtor2_reg } max30001_cnfg_rtor2_t; +// Legacy typedef for backward compatibility typedef enum { SAMPLINGRATE_128 = 128, @@ -361,52 +467,306 @@ typedef enum SAMPLINGRATE_512 = 512 } sampRate; +// ============================================================================= +// MAX30001 Class Declaration +// ============================================================================= + +/** + * @brief Main class for interfacing with the MAX30001 ECG/BioZ sensor + * + * This class provides both a modern high-level API and maintains backward + * compatibility with the legacy interface. + */ class MAX30001 { public: + // ========================================================================= + // Constructors and Initialization + // ========================================================================= + + /** + * @brief Constructor with chip select pin + * @param cs_pin SPI chip select pin number + * @param spi_interface SPI interface to use (default: SPI) + */ + MAX30001(uint8_t cs_pin, SPIClass* spi_interface = &SPI); + + /** + * @brief Legacy constructor (for backward compatibility) + * @param cs_pin SPI chip select pin number + */ MAX30001(int cs_pin); + + /** + * @brief Initialize the MAX30001 sensor + * @return Error code indicating success or failure + */ + max30001_error_t begin(); + + /** + * @brief Check if device is connected and responding + * @return true if device responding, false otherwise + */ + bool isConnected(); + + /** + * @brief Get chip information + * @param info Pointer to store device info + * @return Error code + */ + max30001_error_t getDeviceInfo(max30001_device_info_t* info); + + // ========================================================================= + // High-Level Measurement API (New Interface) + // ========================================================================= + + /** + * @brief Start ECG monitoring with specified settings + * @param sample_rate Desired sample rate (default: 128 SPS) + * @param gain ECG gain setting (default: 80 V/V) + * @return Error code + */ + max30001_error_t startECG(max30001_sample_rate_t sample_rate = MAX30001_RATE_128, + max30001_ecg_gain_t gain = MAX30001_ECG_GAIN_80); + + /** + * @brief Start BioZ monitoring with specified settings + * @param sample_rate Desired sample rate (default: 128 SPS) + * @return Error code + */ + max30001_error_t startBioZ(max30001_sample_rate_t sample_rate = MAX30001_RATE_128); + + /** + * @brief Start combined ECG + BioZ monitoring + * @param sample_rate ECG sample rate (BioZ will be half) + * @return Error code + */ + max30001_error_t startECGBioZ(max30001_sample_rate_t sample_rate = MAX30001_RATE_128); + + /** + * @brief Start ECG with R-R interval detection + * @param sample_rate Desired sample rate (default: 128 SPS) + * @param gain ECG gain setting (default: 80 V/V) + * @return Error code (convenience wrapper for startECG with R-R enabled internally) + */ + max30001_error_t startRtoR(max30001_sample_rate_t sample_rate = MAX30001_RATE_128, + max30001_ecg_gain_t gain = MAX30001_ECG_GAIN_80); + + /** + * @brief Get single ECG sample + * @param sample Pointer to store ECG sample data + * @return Error code + */ + max30001_error_t getECGSample(max30001_ecg_sample_t* sample); + + /** + * @brief Get single BioZ sample + * @param sample Pointer to store BioZ sample data + * @return Error code + */ + max30001_error_t getBioZSample(max30001_bioz_sample_t* sample); + + /** + * @brief Get R-R detection data + * @param rtor_data Pointer to store R-R data + * @return Error code + */ + max30001_error_t getRtoRData(max30001_rtor_data_t* rtor_data); + + /** + * @brief Stop all monitoring + */ + void stop(); + + /** + * @brief Convert raw ECG value to microvolts + * @param raw_value Raw ADC value + * @param gain Current gain setting + * @return Value in microvolts + */ + float convertECGToMicrovolts(int32_t raw_value, max30001_ecg_gain_t gain); + + /** + * @brief Get last error code + * @return Last error that occurred + */ + max30001_error_t getLastError() const; + + // ========================================================================= + // Advanced Configuration API (Phase 3) + // ========================================================================= + + /** + * @brief Set ECG gain during runtime + * @param gain New gain setting (80, 160 V/V options) + * @return Error code + */ + max30001_error_t setECGGain(max30001_ecg_gain_t gain); + + /** + * @brief Get current ECG gain setting + * @return Current ECG gain + */ + max30001_ecg_gain_t getECGGain() const { return _ecg_gain; } + + /** + * @brief Enable ECG channel (resume acquisition) + * @return Error code + */ + max30001_error_t enableECG(); + + /** + * @brief Disable ECG channel (pause acquisition) + * @return Error code + */ + max30001_error_t disableECG(); + + /** + * @brief Check if ECG channel is enabled + * @return true if ECG is acquiring, false otherwise + */ + bool isECGEnabled() const { return _ecg_enabled; } + + /** + * @brief Enable BioZ channel (resume acquisition) + * @return Error code + */ + max30001_error_t enableBioZ(); + + /** + * @brief Disable BioZ channel (pause acquisition) + * @return Error code + */ + max30001_error_t disableBioZ(); + + /** + * @brief Check if BioZ channel is enabled + * @return true if BioZ is acquiring, false otherwise + */ + bool isBioZEnabled() const { return _bioz_enabled; } + + /** + * @brief Set ECG high-pass filter cutoff frequency + * @param cutoff_hz Cutoff frequency in Hz (typically 0.4, 0.8, 1.2 Hz) + * @return Error code + */ + max30001_error_t setECGHighPassFilter(float cutoff_hz); + + /** + * @brief Set ECG low-pass filter cutoff frequency + * @param cutoff_hz Cutoff frequency in Hz (typically 40, 100 Hz) + * @return Error code + */ + max30001_error_t setECGLowPassFilter(float cutoff_hz); + + /** + * @brief Get lead-off detection status for both electrodes + * @return true if lead-off detected on either electrode, false if both connected + */ + bool getLeadOffStatus(); + + /** + * @brief Get FIFO sample count for ECG channel + * @return Number of samples available in FIFO + */ + uint8_t getFIFOCount(); + + /** + * @brief Clear/flush ECG FIFO buffer + * @return Error code + */ + max30001_error_t clearFIFO(); + + /** + * @brief Enable INT1 interrupt output + * @return Error code + */ + max30001_error_t enableInterrupt(); + + /** + * @brief Disable INT1 interrupt output + * @return Error code + */ + max30001_error_t disableInterrupt(); + + /** + * @brief Get current sample rate + * @return Current sample rate setting + */ + max30001_sample_rate_t getSampleRate() const { return _sample_rate; } + + /** + * @brief Get expected sample delay in milliseconds + * @return Delay between samples (e.g., 8ms for 128 SPS) + */ + uint16_t getSampleDelayMs() const; + + // ========================================================================= + // Legacy Interface (Backward Compatibility) + // ========================================================================= + + // Legacy public data members (deprecated - use new API methods instead) unsigned int heartRate; unsigned int RRinterval; signed long ecg_data; signed long bioz_data; - volatile int ecgSamplesAvailable; volatile int biozSamplesAvailable; signed long s32ECGData[128]; signed long s32BIOZData[128]; - + + // Legacy initialization methods void BeginECGOnly(); void BeginECGBioZ(); void BeginRtoRMode(); - + + // Legacy data acquisition methods signed long getECGSamples(void); signed long getBioZSamples(void); void getHRandRR(void); - + + // Legacy utility methods bool max30001ReadInfo(void); void max30001SetsamplingRate(uint16_t samplingRate); - void max30001SetInterrupts(uint32_t interrupts); void max30001ServiceAllInterrupts(); - void readStatus(void); private: + // ========================================================================= + // Private Members + // ========================================================================= + + SPIClass* _spi; + uint8_t _cs_pin; + max30001_error_t _last_error; + bool _initialized; + max30001_ecg_gain_t _ecg_gain; + max30001_sample_rate_t _sample_rate; + bool _ecg_enabled; + bool _bioz_enabled; + + max30001_status_t global_status; + volatile unsigned char _readBufferECG[128]; + volatile unsigned char _readBufferBIOZ[128]; + + // ========================================================================= + // Private Methods - Low-Level Hardware Interface + // ========================================================================= + void _max30001ReadECGFIFO(int num_bytes); void _max30001ReadBIOZFIFO(int num_bytes); - void _max30001Synch(void); void _max30001RegWrite(unsigned char WRITE_ADDRESS, unsigned long data); void _max30001RegRead(uint8_t Reg_address, uint8_t *buff); void _max30001RegRead24(uint8_t Reg_address, uint32_t *read_data); - void _max30001SwReset(void); void _max30001FIFOReset(void); - - max30001_status_t global_status; - int _cs_pin; - volatile unsigned char _readBufferECG[128]; // 4*32 samples - volatile unsigned char _readBufferBIOZ[128]; // 4*32 samples + + // Helper methods + max30001_error_t _validateSampleRate(max30001_sample_rate_t rate); + uint8_t _rateToRegValue(max30001_sample_rate_t rate); + uint8_t _getSampleDelayMs(max30001_sample_rate_t rate) const; }; #endif