Consistent 2 Hz noise in PPG signal

Unfortunately I am experiencing a consistent 2 Hz noise in my PPG signal using the Pro Micro.

I am filtering KYTO 2511b pulse ox data with a circuit similar to the following:

Unfortunately the fixed gain of this circuit did not accommodate all skin types, so a digitpot was included.

The gain is adjusted via SPI according to the incoming sensor data, preventing square waves, etc.

I recently realized that while the pulse ox is unclipped a 2 Hz signal presents -- when the sensor is clipped onto the ear or finger this noise is also present in the signal.

I have tried the following without any luck:

  • removing all SPI communication
  • changing the sampling rate (Timer interrupt)
  • adjusting the clock and a few other initial settings

Any ideas what could be causing this? Maybe its an intrinsic property of the sensor?

Any feedback is much appreciated :slight_smile:
-B

A 2 Hz signal is likely to result from a sampling artifact, an alias of the actual interfering frequency.

The problem could not be in the circuit you posted, because you are not using it.

It is either in the circuit that you are actually using, or in the code that you did not post.

I have played with sampling @ 500 and 100 Hz, neither removed the 2 Hz noise – Another pulse oximeter model also produced a fair amount of noise, however, the 2Hz spike was not as apparent in the frequency spectrum, it was spread out from 1.75- 4.0 Hz.

The original circuit (without the digipot) does not produce any noise, which may indicate there is some underlying noise in both circuits (w/ and w/out the digipot) that is revealed when amplified.

The updated circuit is attached.

Main

#include <avr/wdt.h>
#include <SPI.h>
int speedMaximum = 10 * 1000000;  // 10MHz

//  interrupt and signal handling 
int inputPin = A1;
volatile word signal;
volatile word sampleCounter = 0;              // counter for pulse timing
volatile boolean interrupt_flagged = false;   // basic flag to check for interrupt
const byte SHUTDOWN = 0x5A;                   // shutdown byte command
const byte START = 0x01;                      // start byte command
byte data_buffer[5];
boolean init_setup_flag = false;

// digipot related
byte address = 0x00;                          // Address of the wiper terminal of the pot, allows you to change the resistance value of the pot
int CS = 18;                                  // The CS, or Chip Select, pin is the SS (slave select) pin for the SPI interface
int digipot_resistance = 0;                  // (0-128) 0: min resistance, 128 max resistance


void setup() 
{
  /* Sets up serial, interurupts, SPI, python-arduino handshake.
  */  
  MCUSR = 0;
  WDTCSR |= _BV(WDCE) | _BV(WDE);
  WDTCSR = 0;
  Serial.begin(115200);         // start serial @ baudrade 
  while (!Serial);              // waits for an active serial connection to be established by the PC
  byte pyResponse = ard_init(); // handshake protocol
  interruptSetup();             // turns on timer interrupts for sampling
  init_setup_flag = true;
  // SPI
  pinMode (SS, OUTPUT);         // set the as an output
  pinMode (CS, OUTPUT);         // set the CS as an output
  SPI.begin();                  // initializes the SPI bus by setting SCK, MOSI, and SS to outputs, pulling SCK and MOSI low, and SS high.
}

void loop() 
{
  /* Sends data to serial if interrupt/timer flag has been triggerd.
   * Adjusts gain according to signal amplitude.
  */
  // start setup
  if (!init_setup_flag) {
    setup();
    digitalPotWrite(0);
  }

  // interrupted by sensor timer
  if (interrupt_flagged) 
  {
    //  send data to serial
    send_buffer_to_serial(signal, sampleCounter, data_buffer); 
    interrupt_flagged = false;             //Wait until next interrupt
  }
  serialEvent();
}
int digitalPotWrite(int value)
{
    /* Write new resistance value to pot.
    INPUT: value of (0-128) 0: min resistance, 128 max resistance
  */
    // Serial.println("In digitalPotWrite"); 
    digitalWrite(CS, LOW);
    SPI.beginTransaction(SPISettings(speedMaximum, MSBFIRST, SPI_MODE0));
    SPI.transfer(address);
    SPI.transfer(value);
    SPI.endTransaction();
    digitalWrite(CS, HIGH);
}

byte ard_init() 
{
  /* ard_init(): Initializes the ino-python handshake by constantly pinging the
     serial port
     RETURNS: Byte found in COM port
  */
  TXLED1; // turn TXLED on
  delay(2000);
  TXLED0; // turn TXLED off
  byte found_byte = 0x00;
  //Wait until byte found
  while (!Serial.available() && (found_byte = Serial.read()) == START) {
    Serial.write(0x01);
    TXLED1; // turn TXLED on
    delay(150);
    TXLED0; // turn TXLED off
    delay(150);
  }
  TXLED0; // make sure TXLED is off
  return found_byte;
}

void serialEvent() 
{
  /* Initializes the ino-python handshake by constantly pinging the
     serial port
  */
  //If Serial event AND byte == shutdown byte...
  byte pyResponse = Serial.read(); // Initiate shutdown sequence
  if (pyResponse == SHUTDOWN) {   // Wait until Serial "wakes" arduino up
    Serial.end();
    TXLED1; // turn LED off
    delay(100);  //this delay is super important... i dont know why
    TXLED0; // turn LED on
    delay(100);
    init_setup_flag = false;
  }
  TXLED0; // make sure TXLED is off
}

Interrupt

const int FRAME_SIZE = 5;  // Header (1 byte) + signal (2 bytes) + count (2 bytes)
const int HEADER = 0xFF;
void interruptSetup(){     
  // Initializes Timer1 to throw an interrupt every 2ms (500Hz).
  TCCR1A = 0x00;  // start timer in CTC mode
  TCCR1B = 0x0C;  // set TOP to OCR1A, set clock prescaler to 256
  OCR1A = 0x7C;   // timer will count to this number (0x7C = 124), then trigger reset
  TIMSK1 = 0x02;  // turn on the OCR1A match interrupt 
  sei();             // MAKE SURE GLOBAL INTERRUPTS ARE ENABLED        
} 

ISR(TIMER1_COMPA_vect)
{
  /* Timer 1 Compare A Interrupt Service Routine
   * The ppg value is acquired, the sample counter increases, and the ISR has
   * run flag (interrupt_flagged becomes true) when the interrupt is thrown.
   */
  cli();                                  // disables interrupts while we do this
  signal = analogRead(inputPin);          // reads sensor value
  sampleCounter += 1;                     // increases arduino sample counter
  interrupt_flagged = true;               // flag interrupt
  sei();                                  // turns on interrupts
}

// Generates an array of header+data bytes
byte* send_buffer_to_serial(word signal, word count, byte output[5])
{
  /* INPUT: signal - ppg value (size of 2 bytes) to be sent to serial
   *        count  - counter (size of 2 bytes) to be sent to serial
   *        output - the array to fill with the bytes sent to serial
   *
   * OUTPUT: (Header (1 byte) + signal (2 bytes) + count (2 bytes)) to Serial
   */
  byte hb = highByte(signal);      // takes in a value (signal) and extracts/returns the highest (left-most) byte of a word
  byte lb = lowByte(signal);       // takes in a value (signal) and extracts/returns the lowest (right-most) byte of a word
  byte hb_count = highByte(count); //refer to documentation for more info
  byte lb_count = lowByte(count);
  output[0] = HEADER;
  output[1] = hb;
  output[2] = lb;
  output[3] = hb_count;
  output[4] = lb_count;            // to send to serial
  Serial.write(output, FRAME_SIZE);
}

Things to try is more decoupling on the supply pins of the digipot.
A ProMicro hasn't got a lot of decoupling on the 5volt rail.
And that circuit has none (for low frequencies).
Try adding 47uF electrolytic to that 100n cap (C1).

Try setting the digipot to a certain fixed value in setup, not influenced by any code in loop().
You could try more than one value.
Leo..

Try setting the digipot to a certain fixed value in setup, not influenced by any code in loop().
You could try more than one value.

Leo, the 2 Hz still presents if the digipot write is removed from the loop -- in fact removing all SPI communication doesn't change the result.

Do you have an effective anti-aliasing filter before sampling? You may simply be aliasing mains hum down to a low frequency if not.

What mains frequency do you have? Are you sampling at a multiple of that frequency?
What is your sample clock derived from - a quartz crystal or a ceramic resonator?

(Sampling at a multiple of mains frequency is a trick to alias mains signals down to DC)

Hmm, just been looking at that circuit - seems to have a brace of bandwidth limited integrators which each have a low cutoff frequency of 2.33Hz, which is suspicious - there doesn't appear to be any supply decoupling so it simply be that you've got a 2Hz oscillator disguised as a low-pass filtered amp.

Each opamp should get say 100--1000uF decoupling cap to see if this theory holds water.

MarkT,

Do you have an effective anti-aliasing filter before sampling? You may simply be aliasing mains hum down to a low frequency if not.

Unfortunately not, the circuit was based off this design.

What mains frequency do you have? Are you sampling at a multiple of that frequency?
What is your sample clock derived from - a quartz crystal or a ceramic resonator?

(Sampling at a multiple of mains frequency is a trick to alias mains signals down to DC)

The 5V Pro Micro runs @ 16 MHz (comparable to the Leonardo), the SPI SCK is set to 10 MHz and I am throwing Timer 1 ISR every 2 ms (sampling the data @ 500 Hz). Decreasing the sampling rate to 100 Hz did not help.
Does sampling at a multiple of mains frequency apply to the SPI SCK and Timer 1 ISR?

Hmm, just been looking at that circuit - seems to have a brace of bandwidth limited integrators which each have a low cutoff frequency of 2.33Hz, which is suspicious - there doesn't appear to be any supply decoupling so it simply be that you've got a 2Hz oscillator disguised as a low-pass filtered amp.

That would be unfortunate :frowning: -- if this is true how would it effect the signal? >2.33 Hz would be passed with a 2 (or 2.33) Hz freq band?

This is curious b/c another pulse oximeter model also produced a fair amount of noise, however, the 2Hz spike was not as apparent in the frequency spectrum, it was spread out from 1.75- 4.0 Hz.

Thanks for your help 8)

I have attached a raw time series plot and freq spectrum plot from each sensor:

pulse ox 1 - noise focused @ 2 Hz

pulse ox 2 - noise dispersed over 1.75 - 4 Hz

idle_ppg_pulse_ox2.png

boogie:
...the SPI SCK is set to 10 MHz...

AFAIK max SPI clock is 8MHz on a 16MHz processor.
Leo..

Leo,

AFAIK max SPI clock is 8MHz on a 16MHz processor.

Interesting point -- I believe the digipot datasheet recommended 10 MHz, but unfortunately reducing the SPI clock down to 1 MHz did not alleviate the issue.