Digital Lowpass Filter and Peak Detection Algorithm for Pulse Detection

Hello everyone,

I am working on a PPG sensor for measuring heart rate, and I am having issues with the digital aspect of the system. I have my coefficients for the difference equation (from Matlab) and an algorithm to implement the difference equation in near-real time, but the Serial Plotter shows that my y_current values are decreasing for all time. If anyone can help me in spotting where I may be going wrong, that would be greatly appreciated!

/* Filter variables */
float x_current ;                // current input
float y_current ;                // variable used to hold current output
const int num_B = 3 ;            // number of B coefficients
const int num_A = 3 ;            // number of A coefficients
float x[num_B] = {0.0} ;         // current and previous inputs
float y[num_A] = {0.0} ;         // current and previous outputs
float A[num_A] = {
  +1.0000, 
  -1.9989 ,
  +0.9989 } ;         // denominator polynomial coefficients
float B[num_B] = {
  +0.9994 ,
  -1.9989 ,
  +0.9994} ;         // numerator polynomial coefficients
unsigned long t0 , t1 ;          // variables to keep track of time
unsigned int samplingIntervalMillis = 50 ;

float y_peak ;                    // peak value of output
int peaking = false ;             // flag for peak detector, true if signal is "high"
unsigned long t_peak ;            // time at which peak occurred
int thresh_high = 500 ;           // high-level threshold for peak detector
int thresh_low  = 450 ;           // low-level threshold for peak detector

/* SETUP Block */
void setup()
{
   pinMode(7,OUTPUT);
  digitalWrite(7,HIGH);

  pinMode(8,OUTPUT);
  digitalWrite(8,LOW);
  
  Serial.begin(250000);           //  setup serial baud rate
}

/* LOOP Block */
void loop()
{
  t0 = millis() ;

  x_current = analogRead(A0);
  x[0] = x_current ;

  // implement difference equation
  y_current = 0.0 ;
  for ( int i=0 ; i<num_B ; ++i ) {
    y_current += ( B[i] * x[i] ) ;
  }
  for ( int k=1 ; k<num_A ; ++k ) {
    y_current -= ( A[k] * y[k] ) ;
  }
  y_current /= A[0] ;
  y[0] = y_current ;

  // peak detection
  if ( !peaking ) {
    y_peak = 0.0 ;
    peaking = ( y_current >= thresh_high ) ;
  }
  if ( peaking ) {
    if ( y_current > y_peak ) {
      y_peak = y_current ; // maximum peak value in the spike
      t_peak = t0 ;        // time at which the peak value occurred
    }
    peaking = ( y_current > thresh_low ) ;
  }

  // output to serial
  Serial.print("x:");
  Serial.print(x_current);
  Serial.print(",y:");
  Serial.print(y_current);
  Serial.print(",peak:");
  Serial.println(y_peak);

  // shift our tracking windows for x[n] and y[n] by one sample
  // gets rid of oldest sample no longer used
  for ( int i=(num_B-1) ; i>0 ; --i ) {
    x[i] = x[i-1];
  }
  for ( int k=(num_A-1) ; k>0 ; --k ) {
    y[k] = y[k-1];
  }

  t1 = millis();
  delay(samplingIntervalMillis - (t1 - t0));
}

Use the Serial Plotter to draw the raw and processed curves or peaks.

1 Like

What are the design goals and predicted characteristics of the filter?

Your filter doesn't looks like lowpass.

For LPF all nom coefficients must be positive.
What cut-off frequency you want?

Example, 20Hz sample rate, Fc=0.2Hz , Q=1:
norm
0.000956603
0.001913206
0.000956603

1
-1.935294387
0.939120799

And, you don't need to normalise with
y_current /= A[0]
I think ...

Sorry, I have mistake in above simulation. Now is ok, compared with Excel.
Probably your error is the b1 coefficient, it must be positive:

Cheers

No, will not work again, the filter is not stable. It is oscillating.
This looks stable:

FILTER:
fc = 0.2
Q = 1
G,dB = 60
BQ LPF

.param Fs = 20
.param b0 = 0.9566
.param b1 = 1.9132
.param b2 = 0.9566
.param a0 = 1
.param a1 = -1.9352
.param a2 = 0.9391

I wonder about the purpose of the filter. For diagnostic purposes you risk to suppress flicker or produce misleading artifacts, for bpm the determination of peaks is sufficient.

I guess he is getting the peak value as all above the average, in this case the average is LPF biquad.
But the gain seems too high, it is not clear what is the level of the input signal. A photodiode without amplifier and self biasing will give hardly any signal ...

I was playing long time ago with this :slight_smile:

It works ... I think:


#include <avr/interrupt.h>



// ***************************************    Process    *************************************

float fdt = 50;                 // Filter sampling interval.

double filt_out;
double filt_pre;

unsigned int  input;
unsigned int  setPoint = 0;    // COMMAND
double output ;

double b0, b1, b2, a1, a2; // a0 is 1
double z1, z2;


// ********************************  System ticks *********************************************
// parallel time measurement variables
volatile unsigned int tick_0 = 1;
volatile unsigned int tick_1 = 1;
volatile byte flag = 0;

// *******************************************************************************************
// *******************************************************************************************
// ********************************** System Timer 1mS **************************************
ISR(TCB2_INT_vect) {
  TCB2.INTFLAGS = TCB_CAPT_bm;
  tick_0++;                       // SP square wave time.
  tick_1++;                       // PID  sample rate
  flag = 1;                          // Strobe
}
// ********************************************************************************************

void setup_timer_B() {
  // compilation from diff sources
  //TCB2.CCMP = 0x9C3;  // 10ms with TCB_CLKSEL_CLKTCA_gc
  //TCB2.CCMP = 0x9C3F; // 5ms with TCB_CLKSEL_CLKDIV2_gc
  TCB2.CCMP = 0x1F3F; // 1ms with TCB_CLKSEL_CLKDIV2_gc
  TCB2.CTRLB = 0; // interrupt mode
  TCB2.CTRLA = TCB_CLKSEL_CLKDIV2_gc | TCB_ENABLE_bm;
  //TCB2.CTRLA = TCB_CLKSEL_CLKTCA_gc | TCB_ENABLE_bm;  // use TCA 250kHz clock
  TCB2.INTCTRL =  TCB_CAPT_bm;
}
// **********************************************************************************************

// **************************************  Setup  ******************************************
void setup() {

  b0 = 0.9566;
  b1 = 1.9132;
  b2 = 0.9566;
  //a0 = 1;
  a1 = -1.9352;
  a2 = 0.9391;

  setup_timer_B();            // Start the interrupt
  Serial.begin(115200);     // Be careful when and where you will use this
  while (!Serial) {
    ;  // wait for serial port to connect. Needed for native USB port only
  }
}
// *******************************  Start Main Loop  **************************************
// ****************************************************************************************
void loop() {

  while (flag == 0) {
    ;// synchronize the loop  with the timer
  }
  flag = 0; // run once

  refPoint();
  input = biquad(setPoint);

}
// ******************************* end of main loop ****************************************
//************************************************************************************

void refPoint() {
  if ( tick_0 > 1000) {
    tick_0 = 1;
    if (setPoint == 512) {
      setPoint = 0;
    } else if (setPoint == 0) {
      setPoint = 512;
    }
    Serial.println(setPoint);     // marker
  }
}

//**************************************************************************************
double biquad(int inp) {
  if (   tick_1 > fdt  ) {
    tick_1 = 1;
    double out = inp * b0 + z1;
    z1 = inp * b1 + z2 - a1 * out;
    z2 = inp * b2 - a2 * out;
    Serial.println(out);
    return out;
  }
}

//**************************************************************************************

Simulation:

That does not answer my question about the filter purpose.

It is.
The filter is basically an integrator. The impulses will be above the averaged signal. The averaged signal will vary depending on the ambient light. This is a good attempt to avoid the HW I posted. The code can repeat the HW strategy. However, a photodiode must have at least a current-to-voltage converter to work.

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.