From e7d1be3bd10d868578f53687d14888aa475b9eee Mon Sep 17 00:00:00 2001 From: ElliotMebane Date: Sun, 28 Feb 2016 20:39:51 -0800 Subject: [PATCH] Updated with several time-domain calculations for evaluating Heart Rate Variance, including: SDNN, SDSD, RMSSD, NN50, PNN50. --- .../AllSerialHandling.ino | 29 +++- PulseSensorAmped_Arduino_1dot4/Interrupt.ino | 131 ++++++++++++++---- .../PulseSensorAmped_Arduino_1dot4.ino | 11 ++ 3 files changed, 138 insertions(+), 33 deletions(-) diff --git a/PulseSensorAmped_Arduino_1dot4/AllSerialHandling.ino b/PulseSensorAmped_Arduino_1dot4/AllSerialHandling.ino index bc49585..02e9de6 100644 --- a/PulseSensorAmped_Arduino_1dot4/AllSerialHandling.ino +++ b/PulseSensorAmped_Arduino_1dot4/AllSerialHandling.ino @@ -20,10 +20,27 @@ void serialOutputWhenBeatHappens(){ Serial.print("*** Heart-Beat Happened *** "); //ASCII Art Madness Serial.print("BPM: "); Serial.print(BPM); - Serial.print(" "); + Serial.print(", IBI: "); + Serial.print( IBI ); + Serial.print( ", NNDelta: " ); + Serial.print( RRDelta ); + Serial.print( ", SDNN: " ); + Serial.print( SDNN ); + Serial.print(" || HRV: "); + Serial.print( int( LN20RMSSD ) ); + Serial.print(" (RMSSD: "); + Serial.print( RMSSD ); + Serial.print( "), SDSD: " ); + Serial.print( SDSD ); + Serial.print( ", NN50: " ); + Serial.print( NN50 ); + Serial.print( ", pNN50: " ); + Serial.print( PNN50 ); + Serial.println( " " ); } else{ - sendDataToSerial('B',BPM); // send heart rate with a 'B' prefix - sendDataToSerial('Q',IBI); // send time between beats with a 'Q' prefix + sendDataToSerial('B',BPM); // send heart rate with a 'B' prefix + sendDataToSerial('Q',IBI); // send time between beats with a 'Q' prefix + sendDataToSerial('H', int( LN20RMSSD ) ); // send HRV with a 'H' prefix } } @@ -39,8 +56,8 @@ void sendDataToSerial(char symbol, int data ){ // Code to Make the Serial Monitor Visualizer Work void arduinoSerialMonitorVisual(char symbol, int data ){ - const int sensorMin = 0; // sensor minimum, discovered through experiment -const int sensorMax = 1024; // sensor maximum, discovered through experiment + const int sensorMin = 0; // sensor minimum, discovered through experiment + const int sensorMax = 1024; // sensor maximum, discovered through experiment int sensorReading = data; // map the sensor range to a range of 12 options: @@ -50,7 +67,7 @@ const int sensorMax = 1024; // sensor maximum, discovered through experiment // range value: switch (range) { case 0: - Serial.println(""); /////ASCII Art Madness + Serial.println(""); /////ASCII Art Madness break; case 1: Serial.println("---"); diff --git a/PulseSensorAmped_Arduino_1dot4/Interrupt.ino b/PulseSensorAmped_Arduino_1dot4/Interrupt.ino index 011e3cd..71808c2 100644 --- a/PulseSensorAmped_Arduino_1dot4/Interrupt.ino +++ b/PulseSensorAmped_Arduino_1dot4/Interrupt.ino @@ -1,17 +1,14 @@ - - - -volatile int rate[10]; // array to hold last ten IBI values -volatile unsigned long sampleCounter = 0; // used to determine pulse timing -volatile unsigned long lastBeatTime = 0; // used to find IBI -volatile int P =512; // used to find peak in pulse wave, seeded +volatile int rate[61]; // array to hold last 61 IBI values +volatile unsigned long sampleCounter = 0; // used to determine pulse timing +volatile unsigned long lastBeatTime = 0; // used to find IBI +volatile int P = 512; // used to find peak in pulse wave, seeded volatile int T = 512; // used to find trough in pulse wave, seeded volatile int thresh = 525; // used to find instant moment of heart beat, seeded volatile int amp = 100; // used to hold amplitude of pulse waveform, seeded volatile boolean firstBeat = true; // used to seed rate array so we startup with reasonable BPM volatile boolean secondBeat = false; // used to seed rate array so we startup with reasonable BPM - - +volatile int successiveRRDeltas[60]; // used to track differences between successive RR values + void interruptSetup(){ // Initializes Timer2 to throw an interrupt every 2mS. TCCR2A = 0x02; // DISABLE PWM ON DIGITAL PINS 3 AND 11, AND GO INTO CTC MODE @@ -24,22 +21,23 @@ void interruptSetup(){ // THIS IS THE TIMER 2 INTERRUPT SERVICE ROUTINE. // Timer 2 makes sure that we take a reading every 2 miliseconds -ISR(TIMER2_COMPA_vect){ // triggered when Timer2 counts to 124 + +ISR(TIMER2_COMPA_vect){ // triggered when Timer2 counts to 124 cli(); // disable interrupts while we do this Signal = analogRead(pulsePin); // read the Pulse Sensor sampleCounter += 2; // keep track of the time in mS with this variable int N = sampleCounter - lastBeatTime; // monitor the time since the last beat to avoid noise - // find the peak and trough of the pulse wave + // find the peak and trough of the pulse wave if(Signal < thresh && N > (IBI/5)*3){ // avoid dichrotic noise by waiting 3/5 of last IBI - if (Signal < T){ // T is the trough - T = Signal; // keep track of lowest point in pulse wave + if (Signal < T){ // T is the trough + T = Signal; // keep track of lowest point in pulse wave } } if(Signal > thresh && Signal > P){ // thresh condition helps avoid noise - P = Signal; // P is the peak - } // keep track of highest point in pulse wave + P = Signal; // P is the peak + } // keep track of highest point in pulse wave // NOW IT'S TIME TO LOOK FOR THE HEART BEAT // signal surges up in value every time there is a pulse @@ -50,10 +48,14 @@ ISR(TIMER2_COMPA_vect){ // triggered when Timer2 counts IBI = sampleCounter - lastBeatTime; // measure time between beats in mS lastBeatTime = sampleCounter; // keep track of time for next pulse + int i; if(secondBeat){ // if this is the second beat, if secondBeat == TRUE secondBeat = false; // clear secondBeat flag - for(int i=0; i<=9; i++){ // seed the running total to get a realisitic BPM at startup - rate[i] = IBI; + for(i=0; i<=60; i++){ // seed the running total to get a realisitic BPM at startup + rate[i] = IBI; + } + for( i=0; i<59; i++){ // seed the successiveRRDeltas array with 0s + successiveRRDeltas[i] = 0; } } @@ -64,21 +66,96 @@ ISR(TIMER2_COMPA_vect){ // triggered when Timer2 counts return; // IBI value is unreliable so discard it } - - // keep a running total of the last 10 IBI values - word runningTotal = 0; // clear the runningTotal variable - - for(int i=0; i<=8; i++){ // shift data in the rate array - rate[i] = rate[i+1]; // and drop the oldest IBI value - runningTotal += rate[i]; // add up the 9 oldest IBI values + for( i=0; i<=59; i++){ // update rate array. shift data + rate[i] = rate[i+1]; // and drop the oldest IBI value + } + rate[60] = IBI; // add the latest IBI in the rate array + + // calculate a running total of the last 10 IBI values + word runningTotal = 0; // clear the runningTotal variable + for( i=51; i<=60; i++ ) + { + runningTotal += rate[i]; } - - rate[9] = IBI; // add the latest IBI to the rate array - runningTotal += rate[9]; // add the latest IBI to runningTotal runningTotal /= 10; // average the last 10 IBI values BPM = 60000/runningTotal; // how many beats can fit into a minute? that's BPM! QS = true; // set Quantified Self flag // QS FLAG IS NOT CLEARED INSIDE THIS ISR + + // HRV Analysis + // https://en.wikipedia.org/wiki/Heart_rate_variability#HRV_analysis + + for( i=0; i<=58; i++){ // update successive RR difference array. shift data + successiveRRDeltas[i] = successiveRRDeltas[ i + 1 ]; + } + successiveRRDeltas[59] = rate[ 60 ] - rate[ 59 ]; // set the newest RR difference as the last entry in the array + + RRDelta = successiveRRDeltas[59]; // store the newest RR difference + + // SDNN: the standard deviation of NN (RR) intervals. + // alternate calculation method for stdDev without a second loop may be possible: + // https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Online_algorithm + double tVal = 0; // temp utility variable to be re-used in loops + double aveRR = 0; // calculate the average RR value. + + for( i=1; i<=60; i++){ // add up the last 60 RR values and divide by 60 + aveRR += rate[ i ]; // (Don't include the first entry or you'll add 61 values) + } + aveRR /= 60; + + for( i=1; i<=60; i++){ // calculate the standard devation of RR intervals + tVal = aveRR - rate[ i ]; // using the average that was just calculated + SDNN += tVal * tVal; + } + SDNN = sqrt( SDNN / 60 ); + + // SDSD: standard deviation of successive differences + // The standard deviation of the successive differences between adjacent NNs (RRs). + SDSD = 0; + int aveRRDelta; + for(int i=0; i<=59; i++) // find the average of successive differences + { + aveRRDelta = successiveRRDeltas[ i ]; + } + aveRRDelta /= 60; + + for(int i=0; i<=59; i++) // calculate the standard deviation of successive differences + { // using the average that was just calculated + tVal = aveRRDelta - successiveRRDeltas[ i ]; + SDSD += tVal * tVal; + } + SDSD = sqrt( SDSD / 60 ); + + // RMSSD: root mean square of successive differences + // The square root of the mean of the squares of the successive differences between adjacent NNs (RRs). + RMSSD = 0; + + for(int i=0; i<=59; i++) // square each successive RR difference + { // take the sum of all the values + tVal = successiveRRDeltas[ i ]; // divide by the total sample size to get the mean + RMSSD += tVal * tVal; // and take the square root + } + RMSSD = sqrt( RMSSD / 60 ); + + LN20RMSSD = 20 * log( RMSSD ); // take the natural log of RMSSD + // then scale the result so the range is linear and values + // fall approximately on the scale of 0-100. + + // NN50: the number of pairs of successive NNs (RRs) that differ by more than 50 ms. + NN50 = 0; // add all the successive differences that are greater than 50ms + for(int i=0; i<=59; i++) + { + if( abs( successiveRRDeltas[ i ] ) > 50 ) + { + NN50 ++; + } + } + + // pNN50: the proportion of NN50 divided by total number of NNs (RRs). + PNN50 = NN50 / 60; // divide the total number of successive differences greater than 50s + // that were found and divide by the total sample size to get the + // percent of successive differences greater than 50s. + } } diff --git a/PulseSensorAmped_Arduino_1dot4/PulseSensorAmped_Arduino_1dot4.ino b/PulseSensorAmped_Arduino_1dot4/PulseSensorAmped_Arduino_1dot4.ino index 297a432..559b1f2 100644 --- a/PulseSensorAmped_Arduino_1dot4/PulseSensorAmped_Arduino_1dot4.ino +++ b/PulseSensorAmped_Arduino_1dot4/PulseSensorAmped_Arduino_1dot4.ino @@ -10,6 +10,9 @@ This code: Read Me: https://github.com/WorldFamousElectronics/PulseSensor_Amped_Arduino/blob/master/README.md + +Updated with HRV display by Elliot Mebane, Roguish, Inc. Feb 2016 +HRV requires 1 minute of data before it's accurate. ---------------------- ---------------------- ---------------------- */ @@ -26,6 +29,14 @@ volatile int IBI = 600; // int that holds the time interval between volatile boolean Pulse = false; // "True" when User's live heartbeat is detected. "False" when not a "live beat". volatile boolean QS = false; // becomes true when Arduoino finds a beat. +volatile int RRDelta = 0; +volatile double RMSSD = 0; +volatile double LN20RMSSD = 0; +volatile double SDNN = 0; +volatile double SDSD = 0; +volatile double NN50 = 0; +volatile double PNN50 = 0; + // Regards Serial OutPut -- Set This Up to your needs static boolean serialVisual = false; // Set to 'false' by Default. Re-set to 'true' to see Arduino Serial Monitor ASCII Visual Pulse