Test program for a Max30102 heartrate sensor on a STM32L476RG specific board ENSMM

Dependencies:   mbed

These two pictures show how to connect the Mheuve Sensor to the ST-Link debugger (don't forget to disable ST-link jumpers and JP5 on the board! ):

https://os.mbed.com/media/uploads/jimbaud/1611739514454.jpghttps://os.mbed.com/media/uploads/jimbaud/1611739514453.jpg

These two pictures show how to connect the Mheuve Sensor TX RX to the ST-Link debugger (don't forget to cross TX and RX, it means Mheuve sensor TX on ST-Link RX and ST-Link TX on Mheuve sensor RX ):

https://os.mbed.com/media/uploads/jimbaud/1611739514450.jpg https://os.mbed.com/media/uploads/jimbaud/1611739514452.jpg

The Mheuve sensor board needs to be powered by an external battery.

The result appears on the terminal, speed config is 115200 bds.

https://os.mbed.com/media/uploads/jimbaud/terminal.jpg

Revision:
0:346a7fa07998
Child:
1:e88f22c6c1b0
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/algorithm/algorithm.cpp	Wed Apr 20 21:46:06 2016 +0000
@@ -0,0 +1,378 @@
+/** \file algorithm.h ******************************************************
+*
+* Project: MAXREFDES117#
+* Filename: algorithm.c
+* Description: This module calculates the heart rate/SpO2 level
+*
+* Revision History:
+*\n 1-18-2016 Rev 01.00 SK Initial release.
+*\n
+*
+* --------------------------------------------------------------------
+*
+* This code follows the following naming conventions:
+*
+*\n char              ch_pmod_value
+*\n char (array)      s_pmod_s_string[16]
+*\n float             f_pmod_value
+*\n int32_t           n_pmod_value
+*\n int32_t (array)   an_pmod_value[16]
+*\n int16_t           w_pmod_value
+*\n int16_t (array)   aw_pmod_value[16]
+*\n uint16_t          uw_pmod_value
+*\n uint16_t (array)  auw_pmod_value[16]
+*\n uint8_t           uch_pmod_value
+*\n uint8_t (array)   auch_pmod_buffer[16]
+*\n uint32_t          un_pmod_value
+*\n int32_t *         pn_pmod_value
+*
+* ------------------------------------------------------------------------- */
+/*******************************************************************************
+* Copyright (C) 2015 Maxim Integrated Products, Inc., All Rights Reserved.
+*
+* 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 MAXIM INTEGRATED 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.
+*
+* Except as contained in this notice, the name of Maxim Integrated
+* Products, Inc. shall not be used except as stated in the Maxim Integrated
+* Products, Inc. Branding Policy.
+*
+* The mere transfer of this software does not imply any licenses
+* of trade secrets, proprietary technology, copyrights, patents,
+* trademarks, maskwork rights, or any other form of intellectual
+* property whatsoever. Maxim Integrated Products, Inc. retains all
+* ownership rights.
+*******************************************************************************
+*/
+#include "algorithm.h"
+#include "mbed.h"
+
+void maxim_heart_rate_and_oxygen_saturation(uint32_t *un_ir_buffer ,  int32_t n_ir_buffer_length, uint32_t *un_red_buffer ,   int32_t *n_spo2, int8_t *ch_spo2_valid , 
+                              int32_t *n_heart_rate , int8_t  *ch_hr_valid)
+/**
+* \brief        Calculate the heart rate and SpO2 level
+* \par          Details
+*               By detecting  peaks of PPG cycle and corresponding AC/DC of red/infra-red signal, the ratio for the SPO2 is computed.
+*               Since this algorithm is aiming for Arm M0/M3. formaula for SPO2 did not achieve the accuracy due to register overflow.
+*               Thus, accurate SPO2 is precalculated and save longo uch_spo2_table[] per each ratio.
+*
+* \param[in]    *un_ir_buffer           - IR sensor data buffer
+* \param[in]    n_ir_buffer_length      - IR sensor data buffer length
+* \param[in]    *un_red_buffer          - Red sensor data buffer
+* \param[out]    *n_spo2                - Calculated SpO2 value
+* \param[out]    *ch_spo2_valid         - 1 if the calculated SpO2 value is valid
+* \param[out]    *n_heart_rate          - Calculated heart rate value
+* \param[out]    *ch_hr_valid           - 1 if the calculated heart rate value is valid
+*
+* \retval       None
+*/
+{
+
+
+        uint32_t irMean ,onlyOnce ;
+        int32_t k ,iRatioCount;
+        int32_t i,s ,m, exact_ir_valley_locs_count ,middleIdx;
+        int32_t th1, n_npks,cMin;      
+        int32_t ir_valley_locs[15] ;
+        int32_t exact_ir_valley_locs[15] ;
+        int32_t dx_peak_locs[15] ;
+        int32_t  peakintervalSum;
+        
+        int32_t yAC, xAC;
+        int32_t spo2calc; 
+        int32_t yDCmax, xDCmax; 
+        int32_t yDCmaxIdx, xDCmaxIdx; 
+        int32_t ratio[5],ratioAverage; 
+        int32_t nume,  denom ;
+        // remove DC of ir signal    
+        irMean =0; 
+        for (k=0 ; k<n_ir_buffer_length ; k++ ) irMean += un_ir_buffer[k] ;
+        irMean =irMean/n_ir_buffer_length ;
+        for (k=0 ; k<n_ir_buffer_length ; k++ )  n_x[k] =  un_ir_buffer[k] - irMean ; 
+        
+        // 4 pt Moving Average
+        for(k=0; k< BUFFER_SIZE-MA4_SIZE; k++){
+          denom= ( n_x[k]+n_x[k+1]+ n_x[k+2]+ n_x[k+3]);
+          n_x[k]=  denom/(int32_t)4; 
+        }
+
+        // get difference of smoothed IR signal
+        
+       for( k=0; k<BUFFER_SIZE-MA4_SIZE-1;  k++)
+           n_dx[k]= (n_x[k+1]- n_x[k]);
+
+       // 2-pt Moving Average to n_dx
+       for(k=0; k< BUFFER_SIZE-MA4_SIZE-2; k++){
+          n_dx[k] =  ( n_dx[k]+n_dx[k+1])/2 ;
+        }
+        
+       // hamming window
+       // flip wave form so that we can detect valley with peak detector
+        for ( i=0 ; i<BUFFER_SIZE-HAMMING_SIZE-MA4_SIZE-2 ;i++){
+            s= 0;
+            for( k=i; k<i+ HAMMING_SIZE ;k++){
+                s -= n_dx[k] *uw_hamm[k-i] ; 
+                         }
+            n_dx[i]= s/ (int32_t)1146; // divide by sum of uw_hamm 
+        }
+    
+     
+        th1=0; // threshold calculation
+        for ( k=0 ; k<BUFFER_SIZE-HAMMING_SIZE ;k++){
+                th1 += ((n_dx[k]>0)? n_dx[k] : ((int32_t)0-n_dx[k])) ;
+        }
+        th1= th1/ ( BUFFER_SIZE-HAMMING_SIZE);
+        // peak location is acutally index for sharpest location of raw signal since we flipped the signal         
+        maxim_find_peaks( dx_peak_locs, &n_npks, n_dx, BUFFER_SIZE-HAMMING_SIZE, th1, 8, 5 );//peak_height, peak_distance, max_num_peaks 
+
+        peakintervalSum =0;
+                if (n_npks>=2){
+                      for (k=1; k<n_npks; k++)
+                             peakintervalSum += (dx_peak_locs[k] -dx_peak_locs[k -1] ) ;
+                      peakintervalSum =peakintervalSum/(n_npks-1);
+                     *n_heart_rate =(int32_t)( 6000/ peakintervalSum );// beats per minutes
+                      //prlongf(">>>  *n_heart_rate= %d \n", *n_heart_rate) ;
+                      *ch_hr_valid  = 1;
+                }
+                else  {
+                      *n_heart_rate = -999;
+                      *ch_hr_valid  = 0;
+                }
+                
+        for ( k=0 ; k<n_npks ;k++)
+            ir_valley_locs[k]= dx_peak_locs[k] +HAMMING_SIZE /2; 
+
+
+          // raw value : RED(=y) and IR(=X)
+          // we need to assess DC and AC value of ir and red PPG. 
+            for (k=0 ; k<n_ir_buffer_length ; k++ )  {
+                n_x[k] =  un_ir_buffer[k] ; 
+                n_y[k] =  un_red_buffer[k] ; 
+            }
+
+        // find precise min near ir_valley_locs
+        exact_ir_valley_locs_count =0; 
+        for ( k=0 ; k<n_npks ;k++){
+            onlyOnce =1;
+            m=ir_valley_locs[k];
+            cMin= 16777216;//2^24;
+            if (m+5 <  BUFFER_SIZE-HAMMING_SIZE  && m-5 >0){
+               
+                for(i= m-5;i<m+5; i++)
+                   if (n_x[i]<cMin){
+                        if (onlyOnce >0){
+                           onlyOnce =0;
+                       } 
+                       cMin= n_x[i] ;
+                       exact_ir_valley_locs[k]=i;
+                   }
+                if (onlyOnce ==0)   exact_ir_valley_locs_count ++ ;
+            }
+        }
+        if (exact_ir_valley_locs_count <2 ){
+           *n_spo2 =  -999 ; // do not use SPO2 since signal ratio is out of range
+           *ch_spo2_valid  = 0; 
+           return;
+       }
+        // 4 pt MA
+        for(k=0; k< BUFFER_SIZE-MA4_SIZE; k++){
+         n_x[k]=( n_x[k]+n_x[k+1]+ n_x[k+2]+ n_x[k+3])/(int32_t)4;
+         n_y[k]=( n_y[k]+n_y[k+1]+ n_y[k+2]+ n_y[k+3])/(int32_t)4;
+          
+        }
+
+        //using exact_ir_valley_locs , find ir-red DC andir-red AC for SPO2 calibration ratio
+        //finding AC/DC maximum of raw ir * red between two valley locations
+        ratioAverage =0; 
+        iRatioCount = 0; 
+        
+        for(k=0; k< 5; k++) ratio[k]=0;
+        for (k=0; k< exact_ir_valley_locs_count; k++){
+             if (exact_ir_valley_locs[k] > BUFFER_SIZE ){             
+                  *n_spo2 =  -999 ; // do not use SPO2 since valley loc is out of range
+                  *ch_spo2_valid  = 0; 
+                   return;
+             }
+         }
+        // find max between two valley locations 
+        // and use ratio betwen AC compoent of Ir & Red and DC compoent of Ir & Red for SPO2 
+    
+        for (k=0; k< exact_ir_valley_locs_count-1; k++){
+              yDCmax= -16777216 ; 
+              xDCmax= - 16777216; 
+           //   printf("range=%d: %d\n ", exact_ir_valley_locs[k], exact_ir_valley_locs[k+1]);
+              if (exact_ir_valley_locs[k+1]-exact_ir_valley_locs[k] >10){
+                    for (i=exact_ir_valley_locs[k]; i< exact_ir_valley_locs[k+1]; i++){
+                
+                          if (n_x[i]> xDCmax) {xDCmax =n_x[i];xDCmaxIdx =i; }
+                          if (n_y[i]> yDCmax) {yDCmax =n_y[i];yDCmaxIdx=i;}
+                      }
+                    yAC= (n_y[exact_ir_valley_locs[k+1]] - n_y[exact_ir_valley_locs[k] ] )*(yDCmaxIdx -exact_ir_valley_locs[k]); //red
+                    yAC=  n_y[exact_ir_valley_locs[k]] + yAC/ (exact_ir_valley_locs[k+1] - exact_ir_valley_locs[k])  ; 
+            
+            
+                    yAC=  n_y[yDCmaxIdx] - yAC;    // subracting linear DC compoenents from raw 
+                    xAC= (n_x[exact_ir_valley_locs[k+1]] - n_x[exact_ir_valley_locs[k] ] )*(xDCmaxIdx -exact_ir_valley_locs[k]); // ir
+                    xAC=  n_x[exact_ir_valley_locs[k]] + xAC/ (exact_ir_valley_locs[k+1] - exact_ir_valley_locs[k]); 
+                    xAC=  n_x[yDCmaxIdx] - xAC;      // subracting linear DC compoenents from raw 
+                    nume=( yAC *xDCmax)>>7 ; //prepare X100 to preserve floating value
+                    denom= ( xAC *yDCmax)>>7;
+                    if (denom>0  && iRatioCount <5 &&  nume != 0)
+                    {   
+                         ratio[iRatioCount]= (nume*100)/denom ; //formular is ( yAC *xDCmax) / ( xAC *yDCmax) ;
+                         iRatioCount++;
+                    }
+              }
+            
+    //  prlongf("ratio[%d]= %d exact_ir_valley_locs[k] =%d , exact_ir_valley_locs[%d] =%d \n",k, ratio[k] ,exact_ir_valley_locs[k] ,k+1,  exact_ir_valley_locs[k+1]  ) ;
+    //  prlongf("nume= %d ,denom= %d  yAC = %d, xDCmax = %d, xAC= %d, yDCmax = %d\n",nume, denom,  yAC ,xDCmax ,xAC ,yDCmax );
+
+        }
+
+        maxim_sort_ascend(ratio, iRatioCount);
+        middleIdx= iRatioCount/2;
+
+        if (middleIdx >1)
+          ratioAverage =( ratio[middleIdx-1] +ratio[middleIdx])/2; // use median
+        else
+           ratioAverage = ratio[middleIdx ];
+
+        if( ratioAverage>2 && ratioAverage <184){
+                   spo2calc= uch_spo2_table[ratioAverage] ;
+            *n_spo2 = spo2calc ;
+            *ch_spo2_valid  = 1;//  float_SPO2 =  -45.060*ratioAverage* ratioAverage/10000 + 30.354 *ratioAverage/100 + 94.845 ;  // for comparison with table
+        }
+        else{
+            *n_spo2 =  -999 ; // do not use SPO2 since signal ratio is out of range
+            *ch_spo2_valid  = 0; 
+        }
+            
+
+}
+
+
+void maxim_find_peaks( int32_t *n_locs, int32_t *n_npks,  int32_t  *n_x, int32_t n_size, int32_t n_min_height, int32_t n_min_distance, int32_t n_max_num )
+/**
+* \brief        Find peaks
+* \par          Details
+*               Find at most MAX_NUM peaks above MIN_HEIGHT separated by at least MIN_DISTANCE
+*
+* \retval       None
+*/
+{
+    maxim_peaks_above_min_height( n_locs, n_npks, n_x, n_size, n_min_height );
+    maxim_remove_close_peaks( n_locs, n_npks, n_x, n_min_distance );
+    *n_npks = min( *n_npks, n_max_num );
+}
+
+void maxim_peaks_above_min_height( int32_t *n_locs, int32_t *n_npks,  int32_t  *n_x, int32_t n_size, int32_t n_min_height )
+/**
+* \brief        Find peaks above n_min_height
+* \par          Details
+*               Find all peaks above MIN_HEIGHT
+*
+* \retval       None
+*/
+{
+    int32_t i = 1, width;
+    *n_npks = 0;
+    
+    while (i < n_size-1){
+        if (n_x[i] > n_min_height && n_x[i] > n_x[i-1]){            // find left edge of potential peaks
+            width = 1;
+            while (i+width < n_size && n_x[i] == n_x[i+width])    // find flat peaks
+                width++;
+            if (n_x[i] > n_x[i+width] && (*n_npks) < 15 ){                            // find right edge of peaks
+                n_locs[(*n_npks)++] = i;        
+              // for flat peaks, peak location is left edge
+                i += width+1;
+            }
+            else
+                i += width;
+        }
+        else
+            i++;
+    }
+}
+
+
+void maxim_remove_close_peaks( int32_t *n_locs, int32_t *n_npks,  int32_t  *n_x, int32_t n_min_distance )
+/**
+* \brief        Remove peaks
+* \par          Details
+*               Remove peaks separated by less than MIN_DISTANCE
+*
+* \retval       None
+*/
+{
+    
+    int32_t i, j, old_npks, dist;
+    
+    /* Order peaks from large to small */
+    maxim_sort_indices_descend( n_x, n_locs, *n_npks );
+
+    for ( i = -1; i < *n_npks; i++ ){
+        old_npks = *n_npks;
+        *n_npks = i+1;
+        for ( j = i+1; j < old_npks; j++ ){
+            dist =  n_locs[j] - ( i == -1 ? -1 : n_locs[i] ); // lag-zero peak of autocorr is at index -1
+            if ( dist > n_min_distance || dist < -n_min_distance )
+                n_locs[(*n_npks)++] = n_locs[j];
+        }
+    }
+
+    // Resort indices longo ascending order
+    maxim_sort_ascend( n_locs, *n_npks );
+}
+
+void maxim_sort_ascend(int32_t  *n_x, int32_t n_size) 
+/**
+* \brief        Sort array
+* \par          Details
+*               Sort array in ascending order (insertion sort algorithm)
+*
+* \retval       None
+*/
+{
+    int32_t i, j, temp;
+    for (i = 1; i < n_size; i++) {
+        temp = n_x[i];
+        for (j = i; j > 0 && temp < n_x[j-1]; j--)
+            n_x[j] = n_x[j-1];
+        n_x[j] = temp;
+    }
+}
+
+void maxim_sort_indices_descend(  int32_t  *n_x, int32_t *n_indx, int32_t n_size)
+/**
+* \brief        Sort indices
+* \par          Details
+*               Sort indices according to descending order (insertion sort algorithm)
+*
+* \retval       None
+*/ 
+{
+    int32_t i, j, temp;
+    for (i = 1; i < n_size; i++) {
+        temp = n_indx[i];
+        for (j = i; j > 0 && n_x[temp] > n_x[n_indx[j-1]]; j--)
+            n_indx[j] = n_indx[j-1];
+        n_indx[j] = temp;
+    }
+}
+