diff --git a/README.md b/README.md index ad1af10..d69aca7 100644 --- a/README.md +++ b/README.md @@ -1,89 +1,84 @@ -# ProtoCentral MAX30003 Single-lead ECG Breakout Board -[![Compile Examples](https://github.com/Protocentral/protocentral_max30003/workflows/Compile%20Examples/badge.svg)](https://github.com/Protocentral/protocentral_max30003/actions?workflow=Compile+Examples) +# ProtoCentral MAX30003 — Single‑lead ECG Breakout +Arduino library and examples for the MAX30003 single‑lead ECG analog front end. -## Don't have one? [Buy it here](https://www.protocentral.com/open-medical-devices/1149-protocentral-max30003-single-lead-ecg-breakout-board.html) +## Highlights +- MAX30003 AFE: single‑lead ECG, ultra‑low power operation, integrated R‑R (RTOR) detection and QRS timing. +- Board: ProtoCentral breakout with level selection for 3.3V/5V systems, SPI interface, and INT pin for event/RTOR notifications. -![ProtoCentral MAX30003 Single-channel ECG breakout](assets/max30003_brk.jpg) +## Quick features +- Single‑lead ECG measurement (24‑bit ADC samples) +- On‑chip R‑R (RTOR) detection and heart‑rate calculation +- Low power: suitable for battery powered wearables +- SPI interface; interrupt output for beat/flags +Wiring (Arduino defaults used in examples) -MAX30003 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. +| MAX30003 signal | Arduino pin | Notes | +|-----------------|-------------|-------| +| MISO | D12 | SPI MISO | +| MOSI | D11 | SPI MOSI | +| SCLK | D13 | SPI SCK | +| CS | D4 | Chip select (examples default) | +| INT1 | D2 | Interrupt / RTOR flag | +| VCC / 3V3 | 3.3V / 5V | Power (match board jumper) | +| GND | GND | Ground | -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. +## Quick start (example) -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 !! +```cpp +#include +#include "protocentral_max30003.h" -## Hardware Setup +// Use chip select pin D4 (examples default) +MAX30003 max30003(4); -Connection with the Arduino board is as follows: +void setup() { + Serial.begin(115200); + SPI.begin(); -|MAX30003 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 | -| 3V3 | Supply | Board which supports 3.3V and 1.8V | -| VCC | Supply 5V | 5V | -| GND | Gnd + if (!max30003.readDeviceID()) { + while (1) { delay(1000); } + } + max30003.begin(); // start sensor, default sampling + max30003.setSamplingRate(MAX30003::SRATE_1000HZ); // optional: change sample rate +} -# Visualizing output +void loop() { + int32_t ecg = 0; + if (max30003.readEcgSample(ecg)) { // returns true when a new sample is available + Serial.println(ecg); // 24‑bit sign‑extended ADC value + } -![Wearing the Electrode](assets/gif-max30003-openview.gif) + // RTOR / heart rate (library keeps latest values) + max30003.updateHeartRate(); + Serial.print("HR:"); Serial.print(max30003.heartRate()); + Serial.print(" RR(ms):"); Serial.println(max30003.rrInterval()); + delay(8); +} +``` -## For further details, refer [the documentation on MAX30003 breakout board](https://docs.protocentral.com/getting-started-with-max30003/) +## Examples +- `examples/01-ecg-openview` — OpenView / UART packet streaming example +- `examples/02-ecg-plotter` — Arduino Serial Plotter output +- `examples/03-hr-rtor` — Heart‑rate and RTOR demonstration +## Build & upload using arduino-cli +```bash +arduino-cli compile --fqbn arduino:renesas_uno:minima --libraries /path/to/Arduino/libraries /path/to/protocentral_max30003/examples/02-ecg-plotter +arduino-cli upload -p /dev/ttyACM0 -b arduino:renesas_uno:minima -i /tmp/build/02-ecg-plotter.ino.bin +``` -License Information -=================== +## Documentation & support +- Board and getting started: https://docs.protocentral.com/getting-started-with-max30003/ +- Product page: https://www.protocentral.com/open-medical-devices/1149-protocentral-max30003-single-lead-ecg-breakout-board.html -![License](license_mark.svg) - -This product is open source! Both, our hardware and software are open source and licensed under the following licenses: - -Hardware ---------- - -**All hardware is released under the [CERN-OHL-P v2](https://ohwr.org/cern_ohl_p_v2.txt)** license. - -Copyright CERN 2020. - -This source describes Open Hardware and is licensed under the CERN-OHL-P v2. - -You may redistribute and modify this documentation and make products -using it under the terms of the CERN-OHL-P v2 (https:/cern.ch/cern-ohl). -This documentation is distributed WITHOUT ANY EXPRESS OR IMPLIED -WARRANTY, INCLUDING OF MERCHANTABILITY, SATISFACTORY QUALITY -AND FITNESS FOR A PARTICULAR PURPOSE. Please see the CERN-OHL-P v2 -for applicable conditions - -Software --------- +## Licenses +- Hardware: CERN-OHL-P v2 (see `LICENSE.md`) +- Software: MIT (see `LICENSE.md`) -**All software is released 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. - -Documentation -------------- -**All documentation is released under [Creative Commons Share-alike 4.0 International](http://creativecommons.org/licenses/by-sa/4.0/).** -![CC-BY-SA-4.0](https://i.creativecommons.org/l/by-sa/4.0/88x31.png) - -You are free to: - -* Share — copy and redistribute the material in any medium or format -* Adapt — remix, transform, and build upon the material for any purpose, even commercially. -The licensor cannot revoke these freedoms as long as you follow the license terms. - -Under the following terms: - -* Attribution — You must give appropriate credit, provide a link to the license, and indicate if changes were made. You may do so in any reasonable manner, but not in any way that suggests the licensor endorses you or your use. -* ShareAlike — If you remix, transform, or build upon the material, you must distribute your contributions under the same license as the original. +![License](license_mark.svg) -Please check [*LICENSE.md*](LICENSE.md) for detailed license descriptions. diff --git a/examples/01-ecg-openview/01-ecg-openview.ino b/examples/01-ecg-openview/01-ecg-openview.ino new file mode 100644 index 0000000..5870410 --- /dev/null +++ b/examples/01-ecg-openview/01-ecg-openview.ino @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: MIT +/** + * @file 01-ecg-openview.ino + * @brief ECG stream example that sends ECG samples to OpenView (Processing GUI). + * + * @author Ashwin Whitchurch + * @copyright Copyright (c) 2025 Protocentral Electronics + * @date 2025-09-12 + * + * Arduino connections (defaults used in examples): + * - MISO : D12 (slave out) + * - MOSI : D11 (slave in) + * - SCLK : D13 (serial clock) + * - CS : D4 (chip select) + * - INT1 : D2 (interrupt 1) + * - VCC : +5V + * - GND : GND + */ + +#include +#include "protocentral_max30003.h" + +// Default: use pin 4 for CS and pin 2 for INT1 +#define MAX30003_CS_PIN 4 + +#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 12 + +#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; + +MAX30003 max30003(MAX30003_CS_PIN); + +void sendDataThroughUART(int32_t ecgSample) +{ + DataPacket[0] = ecgSample & 0xFF; + DataPacket[1] = (ecgSample >> 8) & 0xFF; + DataPacket[2] = (ecgSample >> 16) & 0xFF; + DataPacket[3] = (ecgSample >> 24) & 0xFF; + + uint16_t rr = max30003.rrInterval(); + DataPacket[4] = rr & 0xFF; + DataPacket[5] = (rr >> 8) & 0xFF; + DataPacket[6] = 0x00; + DataPacket[7] = 0x00; + + uint16_t hr = max30003.heartRate(); + DataPacket[8] = hr & 0xFF; + DataPacket[9] = (hr >> 8) & 0xFF; + DataPacket[10] = 0x00; + DataPacket[11] = 0x00; + + //send packet header + for (int i = 0; i < 5; i++) Serial.write(DataPacketHeader[i]); + //send data + for (int i = 0; i < DATA_LEN; i++) Serial.write(DataPacket[i]); + //send footer + for (int i = 0; i < 2; i++) Serial.write(DataPacketFooter[i]); +} + + +void setup() +{ + Serial.begin(57600); //Serial begin + + pinMode(MAX30003_CS_PIN,OUTPUT); + digitalWrite(MAX30003_CS_PIN,HIGH); //disable device + + SPI.begin(); + + bool ret = max30003.readDeviceID(); + if(ret){ + Serial.println("Max30003 read ID Success"); + }else{ + + while(!ret){ + //stay here until the issue is fixed. + ret = max30003.readDeviceID(); + Serial.println("Failed to read ID, please make sure all the pins are connected"); + delay(5000); + } + } + + Serial.println("Initialising the chip ..."); + max30003.begin(); // initialize MAX30003 +} + +void loop() +{ + int32_t sample = 0; + max30003.readEcgSample(sample); + max30003.updateHeartRate(); + sendDataThroughUART(sample); + delay(8); +} diff --git a/examples/02-ecg-plotter/02-ecg-plotter.ino b/examples/02-ecg-plotter/02-ecg-plotter.ino new file mode 100644 index 0000000..e72b43f --- /dev/null +++ b/examples/02-ecg-plotter/02-ecg-plotter.ino @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: MIT +/** + * @file 02-ecg-plotter.ino + * @brief ECG stream example that prints ECG samples to Arduino Serial Plotter. + * + * @author Ashwin Whitchurch + * @copyright Copyright (c) 2025 Protocentral Electronics + * @date 2025-09-12 + * + * Arduino connections (defaults used in examples): + * - MISO : D12 (slave out) + * - MOSI : D11 (slave in) + * - SCLK : D13 (serial clock) + * - CS : D4 (chip select) + * - INT1 : D2 (interrupt 1) + * - VCC : +5V + * - GND : GND + */ + +#include +#include "protocentral_max30003.h" + +#define MAX30003_CS_PIN 4 + +MAX30003 max30003(MAX30003_CS_PIN); + +void setup() +{ + Serial.begin(57600); //Serial begin + + pinMode(MAX30003_CS_PIN,OUTPUT); + digitalWrite(MAX30003_CS_PIN,HIGH); //disable device + + SPI.begin(); + + bool ret = max30003.readDeviceID(); + if(ret){ + Serial.println("Max30003 read ID Success"); + }else{ + + while(!ret){ + //stay here untill the issue is fixed. + ret = max30003.readDeviceID(); + Serial.println("Failed to read ID, please make sure all the pins are connected"); + delay(10000); + } + } + + Serial.println("Initialising the chip ..."); + max30003.begin(); // initialize MAX30003 +} + +void loop() +{ + int32_t sample = 0; + max30003.readEcgSample(sample); + Serial.println(sample); + delay(8); +} diff --git a/examples/03-hr-rtor/03-hr-rtor.ino b/examples/03-hr-rtor/03-hr-rtor.ino new file mode 100644 index 0000000..e62f7e6 --- /dev/null +++ b/examples/03-hr-rtor/03-hr-rtor.ino @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: MIT +/** + * @file 03-hr-rtor.ino + * @brief Example demonstrating heart-rate (RTOR) interrupt handling. + * + * @author Ashwin Whitchurch + * @copyright Copyright (c) 2025 Protocentral Electronics + * @date 2025-09-12 + * + * Arduino connections (defaults used in examples): + * - MISO : D12 (slave out) + * - MOSI : D11 (slave in) + * - SCLK : D13 (serial clock) + * - CS : D4 (chip select) + * - INT1 : D2 (interrupt 1) + * - VCC : +5V + * - GND : GND + */ + +#include +#include "protocentral_max30003.h" + +#define INT_PIN 2 + +#define MAX30003_CS_PIN 4 + +MAX30003 max30003(MAX30003_CS_PIN); + +bool rtorIntrFlag = false; +uint8_t statusReg[3]; + +void rtorInterruptHndlr(){ + rtorIntrFlag = true; +} + +void enableInterruptPin(){ + + pinMode(INT_PIN, INPUT_PULLUP); + attachInterrupt(digitalPinToInterrupt(INT_PIN), rtorInterruptHndlr, CHANGE); +} + +void setup() +{ + Serial.begin(115200); //Serial begin + + pinMode(MAX30003_CS_PIN,OUTPUT); + digitalWrite(MAX30003_CS_PIN,HIGH); //disable device + + SPI.begin(); + + bool ret = max30003.readDeviceID(); + if(ret){ + Serial.println("Max30003 ID Success"); + }else{ + + while(!ret){ + //stay here untill the issue is fixed. + ret = max30003.readDeviceID(); + Serial.println("Failed to read ID, please make sure all the pins are connected"); + delay(5000); + } + } + + Serial.println("Initialising the chip ..."); + // initialize MAX30003 in RTOR mode + max30003.begin(); + max30003.writeRegister(REG_CNFG_GEN, 0x080004); + max30003.writeRegister(REG_CNFG_CAL, 0x720000); + max30003.writeRegister(REG_CNFG_EMUX, 0x0B0000); + max30003.writeRegister(REG_CNFG_ECG, 0x805000); + max30003.writeRegister(REG_CNFG_RTOR1, 0x3FC600); + max30003.writeRegister(REG_EN_INT, 0x000401); + max30003.writeRegister(REG_SYNCH, 0x000000); + enableInterruptPin(); + max30003.readRegister(REG_STATUS, statusReg, 3); +} + +void loop() +{ + if(rtorIntrFlag){ + rtorIntrFlag = false; + max30003.readRegister(REG_STATUS, statusReg, 3); + + if(statusReg[1] & 0x04){ + max30003.updateHeartRate(); + Serial.print("Heart Rate = "); + Serial.println(max30003.heartRate()); + + Serial.print("RR interval = "); + Serial.println(max30003.rrInterval()); + } + } +} diff --git a/examples/Example1-ECG-stream-Openview/Example1-ECG-stream-Openview.ino b/examples/Example1-ECG-stream-Openview/Example1-ECG-stream-Openview.ino deleted file mode 100644 index 510e1f8..0000000 --- a/examples/Example1-ECG-stream-Openview/Example1-ECG-stream-Openview.ino +++ /dev/null @@ -1,126 +0,0 @@ -////////////////////////////////////////////////////////////////////////////////////////// -// -// Demo code for the MAX30003 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: -// -// |MAX30003 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_max30003 -// -///////////////////////////////////////////////////////////////////////////////////////// - -#include -#include "protocentral_max30003.h" - -#define MAX30003_CS_PIN 7 - -#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 12 - -#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; - -MAX30003 max30003(MAX30003_CS_PIN); - -void sendDataThroughUART(void) -{ - DataPacket[0] = max30003.ecgdata; - DataPacket[1] = max30003.ecgdata>>8; - DataPacket[2] = max30003.ecgdata>>16; - DataPacket[3] = max30003.ecgdata>>24; - - DataPacket[4] = max30003.RRinterval ; - DataPacket[5] = max30003.RRinterval >>8; - DataPacket[6] = 0x00; - DataPacket[7] = 0x00; - - DataPacket[8] = max30003.heartRate ; - DataPacket[9] = max30003.heartRate >>8; - DataPacket[10] = 0x00; - DataPacket[11] = 0x00; - - //send packet header - for(int i=0; i<5; i++){ - - Serial.write(DataPacketHeader[i]); - } - - //send 30003 data - for(int i=0; i -#include "protocentral_max30003.h" - -#define MAX30003_CS_PIN 7 - -MAX30003 max30003(MAX30003_CS_PIN); - -void setup() -{ - Serial.begin(57600); //Serial begin - - pinMode(MAX30003_CS_PIN,OUTPUT); - digitalWrite(MAX30003_CS_PIN,HIGH); //disable device - - SPI.begin(); - - bool ret = max30003.max30003ReadInfo(); - if(ret){ - Serial.println("Max30003 read ID Success"); - }else{ - - while(!ret){ - //stay here untill the issue is fixed. - ret = max30003.max30003ReadInfo(); - Serial.println("Failed to read ID, please make sure all the pins are connected"); - delay(10000); - } - } - - Serial.println("Initialising the chip ..."); - max30003.max30003Begin(); // initialize MAX30003 -} - -void loop() -{ - max30003.getEcgSamples(); //It reads the ecg sample and stores it to max30003.ecgdata . - - Serial.println(max30003.ecgdata); - delay(8); -} diff --git a/examples/Example3-Computation-only/Example3-Computation-only.ino b/examples/Example3-Computation-only/Example3-Computation-only.ino deleted file mode 100644 index 036d166..0000000 --- a/examples/Example3-Computation-only/Example3-Computation-only.ino +++ /dev/null @@ -1,101 +0,0 @@ -////////////////////////////////////////////////////////////////////////////////////////// -// -// Demo code for the MAX30003 breakout board -// -// This example displays heart rate and rr interval through arduino serial port. -// -// GUI URL: https://github.com/Protocentral/protocentral_openview.git -// -// Arduino connections: -// -// |MAX30003 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_max30003 -// -///////////////////////////////////////////////////////////////////////////////////////// - -#include -#include "protocentral_max30003.h" - -#define INT_PIN 02 - -#define MAX30003_CS_PIN 7 - -MAX30003 max30003(MAX30003_CS_PIN); - -bool rtorIntrFlag = false; -uint8_t statusReg[3]; - -void rtorInterruptHndlr(){ - rtorIntrFlag = true; -} - -void enableInterruptPin(){ - - pinMode(INT_PIN, INPUT_PULLUP); - attachInterrupt(digitalPinToInterrupt(INT_PIN), rtorInterruptHndlr, CHANGE); -} - -void setup() -{ - Serial.begin(115200); //Serial begin - - pinMode(MAX30003_CS_PIN,OUTPUT); - digitalWrite(MAX30003_CS_PIN,HIGH); //disable device - - SPI.begin(); - - bool ret = max30003.max30003ReadInfo(); - if(ret){ - Serial.println("Max30003 ID Success"); - }else{ - - while(!ret){ - //stay here untill the issue is fixed. - ret = max30003.max30003ReadInfo(); - Serial.println("Failed to read ID, please make sure all the pins are connected"); - delay(5000); - } - } - - Serial.println("Initialising the chip ..."); - max30003.max30003BeginRtorMode(); // initialize MAX30003 - enableInterruptPin(); - max30003.max30003RegRead(STATUS, statusReg); -} - -void loop() -{ - if(rtorIntrFlag){ - rtorIntrFlag = false; - max30003.max30003RegRead(STATUS, statusReg); - - if(statusReg[1] & RTOR_INTR_MASK){ - - max30003.getHRandRR(); //It will store HR to max30003.heartRate and rr to max30003.RRinterval. - Serial.print("Heart Rate = "); - Serial.println(max30003.heartRate); - - Serial.print("RR interval = "); - Serial.println(max30003.RRinterval); - } - } -} diff --git a/library.properties b/library.properties index a1cb43a..fe57c66 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=ProtoCentral MAX30003 ECG AFE Sensor Library -version=1.0.2 +version=2.0.0 author=ProtoCentral Electronics maintainer=ProtoCentral Electronics sentence=Library for the ProtoCentral MAX30003 Single lead ECG breakout board. diff --git a/src/protocentral_Max30003.cpp b/src/protocentral_Max30003.cpp index 4ea6f23..b21ac65 100644 --- a/src/protocentral_Max30003.cpp +++ b/src/protocentral_Max30003.cpp @@ -9,11 +9,11 @@ // | MISO | Slave Out | D12 | // | MOSI | Slave In | D11 | // | SCLK | Serial Clock | D13 | -// | CS | Chip Select | D7 | +// | CS | Chip Select | D4 | // | VCC | Digital VDD | +5V | // | GND | Digital Gnd | Gnd | // | FCLK | 32K CLOCK | - | -// | INT1 | Interrupt1 | 02 | +// | INT1 | Interrupt1 | D2 | // | INT2 | Interrupt2 | - | // // This software is licensed under the MIT License(http://opensource.org/licenses/MIT). @@ -28,228 +28,155 @@ // ///////////////////////////////////////////////////////////////////////////////////////// -#include +// SPDX-License-Identifier: MIT +/** + * @file protocentral_Max30003.cpp + * @brief Implementation of the MAX30003 Arduino library. + * + * @author Ashwin Whitchurch + * @copyright Copyright (c) 2025 Protocentral Electronics + */ + +#include #include "protocentral_max30003.h" -#define MAX30003_SPI_SPEED 2000000 +static constexpr uint32_t MAX30003_SPI_SPEED = 2000000UL; -SPISettings SPI_SETTINGS(MAX30003_SPI_SPEED, MSBFIRST, SPI_MODE0); +using namespace ::std; -MAX30003::MAX30003(int cs_pin) +MAX30003::MAX30003(int cs_pin, SPIClass &spi) noexcept + : _spi(spi), _settings(MAX30003_SPI_SPEED, MSBFIRST, SPI_MODE0), _cs_pin(cs_pin) { - _cs_pin=cs_pin; pinMode(_cs_pin, OUTPUT); - digitalWrite(_cs_pin,HIGH); -} - -void MAX30003::max30003RegWrite (unsigned char WRITE_ADDRESS, unsigned long data) -{ - // now combine the register address and the command into one byte: - byte dataToSend = (WRITE_ADDRESS<<1) | WREG; - - SPI.beginTransaction(SPI_SETTINGS); - // take the chip select low to select the device: - digitalWrite(_cs_pin, LOW); - - delay(2); - SPI.transfer(dataToSend); - SPI.transfer(data>>16); - SPI.transfer(data>>8); - SPI.transfer(data); - delay(2); - - // take the chip select high to de-select: digitalWrite(_cs_pin, HIGH); - SPI.endTransaction(); } - -void MAX30003::max30003SwReset(void) +void MAX30003::chipSelect(bool active) const noexcept { - max30003RegWrite(SW_RST,0x000000); - delay(100); + digitalWrite(_cs_pin, active ? LOW : HIGH); } -void MAX30003::max30003Synch(void) +bool MAX30003::writeRegister(uint8_t WRITE_ADDRESS, uint32_t data) { - max30003RegWrite(SYNCH,0x000000); + uint8_t header = (WRITE_ADDRESS << 1) | WREG; + _spi.beginTransaction(_settings); + chipSelect(true); + delay(2); + _spi.transfer(header); + _spi.transfer((data >> 16) & 0xFF); + _spi.transfer((data >> 8) & 0xFF); + _spi.transfer(data & 0xFF); + delay(2); + chipSelect(false); + _spi.endTransaction(); + return true; } -void MAX30003::max30003RegRead(uint8_t Reg_address, uint8_t * buff) +bool MAX30003::readRegister(uint8_t Reg_address, uint8_t *buff, size_t len) { - uint8_t spiTxBuff; - - SPI.beginTransaction(SPI_SETTINGS); - digitalWrite(_cs_pin, LOW); - - spiTxBuff = (Reg_address<<1 ) | RREG; - SPI.transfer(spiTxBuff); //Send register location - - for ( int i = 0; i < 3; i++) + uint8_t header = (Reg_address << 1) | RREG; + _spi.beginTransaction(_settings); + chipSelect(true); + _spi.transfer(header); + for (size_t i = 0; i < len; ++i) { - buff[i] = SPI.transfer(0xff); + buff[i] = _spi.transfer(0xFF); } - - digitalWrite(_cs_pin, HIGH); - SPI.endTransaction(); + chipSelect(false); + _spi.endTransaction(); + return true; } - -bool MAX30003::max30003ReadInfo(void) +bool MAX30003::readDeviceID() { - uint8_t spiTxBuff; - uint8_t readBuff[4] ; - - SPI.beginTransaction(SPI_SETTINGS); - digitalWrite(_cs_pin, LOW); - - spiTxBuff = (INFO << 1 ) | RREG; - SPI.transfer(spiTxBuff); //Send register location - - for ( int i = 0; i < 3; i++) - { - readBuff[i] = SPI.transfer(0xff); - } - - digitalWrite(_cs_pin, HIGH); - SPI.endTransaction(); - - if((readBuff[0]&0xf0) == 0x50 ){ - - Serial.println("max30003 is ready"); - Serial.print("Rev ID: "); - Serial.println((readBuff[0]&0xf0)); - - return true; - }else{ - - Serial.println("max30003 read info error\n"); - return false; - } - - return false; + uint8_t buf[3] = {0}; + if (!readRegister(REG_INFO, buf, 3)) return false; + // device revision in upper nibble of first byte is 0x5 for MAX30003 + return ((buf[0] & 0xF0) == 0x50); } -void MAX30003::max30003ReadData(int num_samples, uint8_t * readBuffer) +void MAX30003::reset() { - uint8_t spiTxBuff; - - SPI.beginTransaction(SPI_SETTINGS); - digitalWrite(_cs_pin, LOW); - - spiTxBuff = (ECG_FIFO_BURST<<1 ) | RREG; - SPI.transfer(spiTxBuff); //Send register location - - for ( int i = 0; i < num_samples*3; ++i) - { - readBuffer[i] = SPI.transfer(0x00); - } - - digitalWrite(_cs_pin, HIGH); - SPI.endTransaction(); + writeRegister(REG_SW_RST, 0x000000); + delay(100); } -void MAX30003::max30003Begin() +bool MAX30003::sync() { - max30003SwReset(); - delay(100); - max30003RegWrite(CNFG_GEN, 0x081007); - delay(100); - max30003RegWrite(CNFG_CAL, 0x720000); // 0x700000 - delay(100); - max30003RegWrite(CNFG_EMUX,0x0B0000); - delay(100); - max30003RegWrite(CNFG_ECG, 0x805000); // d23 - d22 : 10 for 250sps , 00:500 sps - delay(100); - - max30003RegWrite(CNFG_RTOR1,0x3fc600); - max30003Synch(); - delay(100); + return writeRegister(REG_SYNCH, 0x000000); } -void MAX30003::max30003BeginRtorMode() +bool MAX30003::begin() { - max30003SwReset(); - delay(100); - max30003RegWrite(CNFG_GEN, 0x080004); - delay(100); - max30003RegWrite(CNFG_CAL, 0x720000); // 0x700000 - delay(100); - max30003RegWrite(CNFG_EMUX,0x0B0000); - delay(100); - max30003RegWrite(CNFG_ECG, 0x805000); // d23 - d22 : 10 for 250sps , 00:500 sps - delay(100); - max30003RegWrite(CNFG_RTOR1,0x3fc600); - delay(100); - max30003RegWrite(EN_INT, 0x000401); - delay(100); - max30003Synch(); + reset(); delay(100); + writeRegister(REG_CNFG_GEN, 0x081007); + delay(50); + writeRegister(REG_CNFG_CAL, 0x720000); + delay(50); + writeRegister(REG_CNFG_EMUX, 0x0B0000); + delay(50); + writeRegister(REG_CNFG_ECG, 0x805000); + delay(50); + writeRegister(REG_CNFG_RTOR1, 0x3FC600); + sync(); + delay(50); + return true; } -//not tested -void MAX30003::max30003SetsamplingRate(uint16_t samplingRate) +bool MAX30003::setSamplingRate(SamplingRate rate) { - uint8_t regBuff[4] = {0}; - max30003RegRead(CNFG_ECG, regBuff); - - switch(samplingRate){ - case SAMPLINGRATE_128: - regBuff[0] = (regBuff[0] | 0x80 ); - break; - - case SAMPLINGRATE_256: - regBuff[0] = (regBuff[0] | 0x40 ); - break; - - case SAMPLINGRATE_512: - regBuff[0] = (regBuff[0] | 0x00 ); - break; - - default : - Serial.println("Wrong samplingRate, please choose between 128, 256 or 512"); - break; + uint8_t reg[3] = {0}; + if (!readRegister(REG_CNFG_ECG, reg, 3)) return false; + // modify bits in MSB according to rate + reg[0] &= 0x3F; // clear sample rate bits (example mask) + switch (rate) + { + case SR_128: reg[0] |= 0x80; break; + case SR_256: reg[0] |= 0x40; break; + case SR_512: /* leave 0 */ break; } - - unsigned long cnfgEcg; - memcpy(&cnfgEcg, regBuff, 4); - - Serial.print(" cnfg ECG "); - Serial.println((cnfgEcg)); - max30003RegWrite(CNFG_ECG, (cnfgEcg >> 8)); + uint32_t value = ((uint32_t)reg[0] << 16) | ((uint32_t)reg[1] << 8) | reg[2]; + return writeRegister(REG_CNFG_ECG, value); } -void MAX30003::getEcgSamples(void) +bool MAX30003::readEcgBurst(uint16_t count, uint8_t *out) { - uint8_t regReadBuff[4]; - max30003RegRead(ECG_FIFO, regReadBuff); - - unsigned long data0 = (unsigned long) (regReadBuff[0]); - data0 = data0 <<24; - unsigned long data1 = (unsigned long) (regReadBuff[1]); - data1 = data1 <<16; - unsigned long data2 = (unsigned long) (regReadBuff[2]); - data2 = data2 >>6; - data2 = data2 & 0x03; - - unsigned long data = (unsigned long) (data0 | data1 | data2); - ecgdata = (signed long) (data); + // read count*3 bytes from ECG FIFO burst + _spi.beginTransaction(_settings); + chipSelect(true); + uint8_t header = (REG_ECG_FIFO_BURST << 1) | RREG; + _spi.transfer(header); + for (uint32_t i = 0; i < (uint32_t)count * 3U; ++i) + { + out[i] = _spi.transfer(0x00); + } + chipSelect(false); + _spi.endTransaction(); + return true; } - -void MAX30003::getHRandRR(void) +bool MAX30003::readEcgSample(int32_t &sample) { - uint8_t regReadBuff[4]; - max30003RegRead(RTOR, regReadBuff); - - unsigned long RTOR_msb = (unsigned long) (regReadBuff[0]); - unsigned char RTOR_lsb = (unsigned char) (regReadBuff[1]); - unsigned long rtor = (RTOR_msb<<8 | RTOR_lsb); - rtor = ((rtor >>2) & 0x3fff) ; - - float hr = 60 /((float)rtor*0.0078125); - heartRate = (unsigned int)hr; + uint8_t b[3] = {0}; + if (!readRegister(REG_ECG_FIFO, b, 3)) return false; + // 24-bit signed value: b0 MSB .. b2 LSB + int32_t val = ((uint32_t)b[0] << 16) | ((uint32_t)b[1] << 8) | b[2]; + // sign-extend 24-bit to 32-bit + if (val & 0x800000) val |= 0xFF000000; + sample = val; + return true; +} - unsigned int RR = (unsigned int)rtor* (7.8125) ; //8ms - RRinterval = RR; +bool MAX30003::updateHeartRate() +{ + uint8_t reg[3] = {0}; + if (!readRegister(REG_RTOR, reg, 3)) return false; + uint32_t rtor = ((uint32_t)reg[0] << 8) | reg[1]; + rtor = (rtor >> 2) & 0x3FFF; + if (rtor == 0) return false; + float hr = 60.0F / ( (float)rtor * 0.0078125F ); + _heartRate = (uint16_t)hr; + _rrInterval = (uint16_t)( (float)rtor * 7.8125F ); + return true; } diff --git a/src/protocentral_max30003.h b/src/protocentral_max30003.h index c9a4d47..cf6ecff 100644 --- a/src/protocentral_max30003.h +++ b/src/protocentral_max30003.h @@ -1,83 +1,94 @@ -////////////////////////////////////////////////////////////////////////////////////////// -// -// Demo code for the MAX30003 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-max30003-arduino -// -///////////////////////////////////////////////////////////////////////////////////////// - - -#ifndef protocentral_Max30003.h -#define protocentral_Max30003.h +// SPDX-License-Identifier: MIT +/** + * @file protocentral_max30003.h + * @brief Modernized header for the MAX30003 Arduino library. + * + * @author Ashwin Whitchurch + * @copyright Copyright (c) 2025 Protocentral Electronics + */ + +#ifndef PROTOCENTRAL_MAX30003_H +#define PROTOCENTRAL_MAX30003_H #include +#include + +// Register access flags +static constexpr uint8_t WREG = 0x00; +static constexpr uint8_t RREG = 0x01; + +// Register addresses (subset used by this library) +enum : uint8_t { + REG_NO_OP = 0x00, + REG_STATUS = 0x01, + REG_EN_INT = 0x02, + REG_EN_INT2 = 0x03, + REG_MNGR_INT = 0x04, + REG_MNGR_DYN = 0x05, + REG_SW_RST = 0x08, + REG_SYNCH = 0x09, + REG_FIFO_RST = 0x0A, + REG_INFO = 0x0F, + REG_CNFG_GEN = 0x10, + REG_CNFG_CAL = 0x12, + REG_CNFG_EMUX = 0x14, + REG_CNFG_ECG = 0x15, + REG_CNFG_RTOR1 = 0x1D, + REG_CNFG_RTOR2 = 0x1E, + REG_ECG_FIFO_BURST = 0x20, + REG_ECG_FIFO = 0x21, + REG_RTOR = 0x25, + REG_NO_OP_ALT = 0x7F +}; + +// Sampling rates supported +enum SamplingRate : uint16_t { SR_128 = 128, SR_256 = 256, SR_512 = 512 }; + +class MAX30003 { +public: + // Construct with CS pin and optional SPI instance (defaults to SPI) + explicit MAX30003(int cs_pin, SPIClass &spi = SPI) noexcept; + + // Initialize device (power-on config). Returns true on success. + bool begin(); + + // Software reset + void reset(); + + // Read device ID/info register + bool readDeviceID(); + + // Set sampling rate + bool setSamplingRate(SamplingRate rate); + + // Read single ECG 24-bit signed sample. Returns true on success. + bool readEcgSample(int32_t &sample); + + // Read a burst of `count` ECG samples into buffer (3 bytes per sample). + // `out` must have room for count*3 bytes. + bool readEcgBurst(uint16_t count, uint8_t *out); + + // Update and read Heart Rate and RR interval registers + bool updateHeartRate(); + + // Getters + uint16_t heartRate() const noexcept { return _heartRate; } + uint16_t rrInterval() const noexcept { return _rrInterval; } + + // Low-level register access + bool writeRegister(uint8_t reg, uint32_t value); + bool readRegister(uint8_t reg, uint8_t *buf, size_t len = 3); + +private: + SPIClass &_spi; + SPISettings _settings; + int _cs_pin; + + uint16_t _heartRate{0}; + uint16_t _rrInterval{0}; -#define WREG 0x00 -#define RREG 0x01 - -#define NO_OP 0x00 -#define STATUS 0x01 -#define EN_INT 0x02 -#define EN_INT2 0x03 -#define MNGR_INT 0x04 -#define MNGR_DYN 0x05 -#define SW_RST 0x08 -#define SYNCH 0x09 -#define FIFO_RST 0x0A -#define INFO 0x0F -#define CNFG_GEN 0x10 -#define CNFG_CAL 0x12 -#define CNFG_EMUX 0x14 -#define CNFG_ECG 0x15 -#define CNFG_RTOR1 0x1D -#define CNFG_RTOR2 0x1E -#define ECG_FIFO_BURST 0x20 -#define ECG_FIFO 0x21 -#define RTOR 0x25 -#define NO_OP 0x7F - -#define CLK_PIN 6 -#define RTOR_INTR_MASK 0x04 - -typedef enum -{ - SAMPLINGRATE_128 = 128, SAMPLINGRATE_256 = 256, SAMPLINGRATE_512 = 512 -}sampRate; - -class MAX30003 -{ - public: - MAX30003(int cs_pin); - - unsigned int heartRate; - unsigned int RRinterval; - signed long ecgdata; - - void max30003Begin(); - void max30003BeginRtorMode(); - void max30003SwReset(void); - void getHRandRR(void); - void getEcgSamples(void); - bool max30003ReadInfo(void); - void max30003SetsamplingRate(uint16_t samplingRate); - void max30003RegRead(uint8_t Reg_address, uint8_t * buff); - - private: - void max30003ReadData(int num_samples, uint8_t * readBuffer); - void max30003Synch(void); - void max30003RegWrite (unsigned char WRITE_ADDRESS, unsigned long data); - - int _cs_pin; + void chipSelect(bool active) const noexcept; + bool sync(); }; -#endif +#endif // PROTOCENTRAL_MAX30003_H