How to filter out noise/spikes from PIR sensor

Hi,

I had to invent a way to filter out disturbances in my three-PIR setup. My PIR sensors are Robomaa.com Oy / Robomaa.fi, they produce LOW signal when movement is detected. The noise/spikes are caused by the PIR sensors themselves, but also by the environment they are in. I may have re-invented the wheel but here it comes anyway, as a sample code for anyone to re-use in their projects.

The code might not be perfect (I am not a professional SW developer) so please feel free to suggest improvements.

/*

 This is a code snippet for PIR (Infared) sensor status reading, without disturbance from their noise and spikes.
 
 The primary objective was to develope code for reading the sensor status and to filter out the non-wanted spikes and noise. 
 The secondary objective was to have code which does not stop the main loop while doing the filtering.
 The third objective was to have code which allows multiple PIR sensors to be used.
 
 The basic approach is to use an interrupt routine to capture the PIR sensor changes (falling or rising edge of the sensor pin signal)
 and the main loop for doing the noise filtering. 
 
 The code example use the naming convention "PIR1". You can copy-paste the code and change the second part names to "PIR2" in case you
 have two sensors in your setup. Someone with better coding skills could make this to a proper library so that it would be easy to
 create several instances of the code without the copy-paste-rename excercise :)
  
 Copyright 2011 Juha Olkkonen
 Date: May 14, 2011
 Version: 1.0
 
 The example circuit: PIR1 sensor in digital pin 2 (=interrupt 0), pull-up 10kohm to +5V (goes LOW when detecting movement)
 
*/

// definitions

const int pinPIR1 = 2;                                             // The PIR1 sensor pin
const int calibrationTime = 30;                                    // the time we give the sensor to calibrate (10-60 secs according to the datasheet)  
volatile int candidatePIR1;                                        // the un-filtered new status candidate of the PIR sensor
int statusPIR1 = HIGH;                                             // the noise/spike-filtered status of the PIR sensor
volatile boolean changePIR1 = false;                               // the indication that a potential status change has happened
volatile long unsigned int changeTimePIR1 = 0;                     // the time stamp of the potential status change (edge timer start)
const long unsigned int delayNoise = 100;                          // milliseconds. Status changes shorter than this are deemed noise/spike. Test to find the best value.

// functions

void isrPIR1() {                                                   // PIR1 sensor edge detection interrupt service routine (isr)
  int readPIR1;                                                    // temporary storage for the actual sensor status
  readPIR1 = digitalRead(pinPIR1);
  if (statusPIR1 != readPIR1) {                                    // this should always be true, but just in case some earlier edges went undetected                         
    changePIR1 = true;                                             // status change detected
    candidatePIR1 = readPIR1;                                      // we have a new status candidate (noise/spike might still cause this change)
    changeTimePIR1 = millis();                                     // mark the time of the change
  }
  else {                                                           // no status change after all 
    changePIR1 = false;
  }
}

boolean isMovementPIR1() {                                         // function to filter out noise/spikes from the PIR1 status, returns TRUE when movement is on
  if (changePIR1 == true) {                                        // if status change has been detected
    if ((millis() - changeTimePIR1) > delayNoise) {                // and if the change is stabile enough
      statusPIR1 = candidatePIR1 ;                                 // then, candidate promoted to current status
      changePIR1 = false;                                          // no more change going on
    }
  }
  if (statusPIR1 == LOW) {                                         // movement going on
    return true;
  }
  else {
    return false;
  }
}
  
void setup() {
// Calibrate the PIR sensor
  Serial.begin(9600);
  pinMode(pinPIR1, INPUT);
  digitalWrite(pinPIR1, HIGH);
  Serial.print("Calibrating");
  for(int i = 0; i < calibrationTime; i++){                        // give the sensor some time to calibrate
    Serial.print(".");
    delay(1000);
  }
  Serial.println("done.");
    
// initialize the interrupt service routines for the PIR sensor
  attachInterrupt(0, isrPIR1, CHANGE);
}

void loop() {
  if (isMovementPIR1() == true) {
    // this code is executed in every loop round as long as the movement is being detected
    Serial.print("x");
    delay(1000);
  }
}

You might find the attached image helpful to understand the signal analysis problem I have tried to solve here.

PIR_filter.pde (4.31 KB)

you shouldn't have that much noise in the first place - filtering it via software just takes time :frowning:
just two clarifying questions:
-the signal analysis if from you oscilloscope or hand drawn?
-what resistor value per arduino pin did you use for pullup to 5V?
-what supply voltage did you use for the sensor?

It is the feature of these sensors to have occasional short LOW/HIGH spikes, despite of their internal filtering process.
The picture is hand-drawn. But I have verified this behavior by serial output of the sensor value stream.
Pull-up resistors are 10k.
These ones consume 5V.

My setup is under my aquarium. The biggest cause of interference is the ballast and ingniter of the strong metal-halide lamp when it goes on or off. In many cases, it causes all my three PIR's to report movement instantly, at the same time. The filtering SW removes this problem perfectly, so I am happy. Yes, some amount of time is consumed by the filtering, but in my case it is not critical and the interrupt routines make sure that I do not miss any of the events. I have plenty of other stuff running in my system, including a web server, no problems so far with the performance. It is amazing what you can do with a 16MHz processor :slight_smile:

-Juissi

@juissi,

Can you tell something about the period of the filtered signal?

--- update ---

Did some tinkering with some simpler model, could you test if it works as I do not have a spiking PIR :wink:

The essence of the code below is that state only changes when the lastIRQ was longer than MAXSPIKE milliseconds ago.

#define PIRPIN 2
#define CALLIBRATIONTIME 60
#define MAXSPIKE 100                      // max spike duration in millis()

volatile uint8_t state = HIGH;              // to be initialized correctly in setup()
volatile unsigned long lastIRQ = 0;        // time of last IRQ

void isr()
{
  int x = digitalRead(PIRPIN);
  if (millis() - lastIRQ > MAXSPIKE )
  {
	state = HIGH - x;  // previous state was the right one: HIGH = 1 LOW = 0
  }
  lastIRQ = millis();
}

boolean isMovement() 
{
  int x = digitalRead(PIRPIN);
  if (millis() - lastIRQ > MAXSPIKE )
  {
	state = x;  // new state is right one
  }
  return (state == LOW);
}


void setup() 
{
  Serial.begin(9600);

  pinMode(PIRPIN, INPUT);
  digitalWrite(PIRPIN, HIGH);
  Serial.print("Calibrating");

  for(int i = 0; i < CALLIBRATIONTIME ; i++)
  {
    Serial.print(".");
    delay(1000);
  }
  Serial.println("done.");
    
  // initialize the interrupt service routines for the PIR sensor
  attachInterrupt(0, isr, CHANGE);
}

void loop() 
{
  if (isMovement())
  {
    Serial.print(millis()/1000);
    Serial.print(" ");
    delay(1000);
  }
}

-- fixed a typo in the code --

Thanks, I will run some tests. It is actually easy to test: just set the maxspike to, say, 500 milliseconds. Replace the PIR by a wire that you take from the pirpin to ground. The internal pull-up resistor does the job. By quickly touching GND the status should not change. If you keep the wire in GND for 0.5s the status changes to true. Removing the wire quickly for a while should not change the status, but by removing the wire for >0.5s changes the status to false. This is how I tested my code.

The period of the filtered signal varies. But it is all amount detecting human movement, so the period is usually quite long, >200 ms anyway.

-juha

I presume the same code could debounce a button or switch...

@markT
yep, polling with a debounce lib is also a good solution if the delay (e.g. 20 millis) is acceptable. See - Arduino Playground - Debounce -

The advantage of the IRQ based solution is that there is no such delay.

I had finally some time with my Arduino. I tested robtillaart's code by setting the MAXSPIKE to 1000ms simulating the PIR with a jumpwire from pin 2 to GND. The code detects movements (pin goes to LOW) reliably and filters out the spikes of short LOW's i.e. false alarms. But the code does not filter out the opposite spikes i.e. movement being on (pin LOW) and spikes taking it shortly to HIGH. My code filters reliably both ways, but I assume it all depends on how you want it to work in your project. Below my slightly improved code.

// definitions

const int pinPIR1 = 2;                                             // The PIR1 sensor pin
const int calibrationTime = 30;                                    // the time we give the sensor to calibrate (10-60 secs according to the datasheet)  
volatile int candidatePIR1;                                        // the un-filtered new status candidate of the PIR sensor
int statusPIR1 = HIGH;                                             // the noise/spike-filtered status of the PIR sensor
volatile boolean changePIR1 = false;                               // the indication that a potential status change has happened
volatile long unsigned int changeTimePIR1 = 0;                     // the time stamp of the potential status change (edge timer start)
const long unsigned int delayNoise = 1000;                          // milliseconds. Status changes shorter than this are deemed noise/spike. Test to find the best value. 100ms works for me.

// functions

void isrPIR1() {                                                   // PIR1 sensor edge detection interrupt service routine (isr)
  int readPIR1 = digitalRead(pinPIR1);                             // temporary storage for the actual sensor status
  if (statusPIR1 != readPIR1) {                                    // this should always be true, but just in case some earlier edges went undetected                         
    changePIR1 = true;                                             // status change detected
    candidatePIR1 = readPIR1;                                      // we have a new status candidate (noise/spike might still cause this change)
    changeTimePIR1 = millis();                                     // mark the time of the change
  }
  else {                                                           // no status change after all 
    changePIR1 = false;
  }
}

boolean isMovementPIR1() {                                         // function to filter out noise/spikes from the PIR1 status, returns TRUE when movement is on
  if (changePIR1 == true) {                                        // if status change has been detected
    if ((millis() - changeTimePIR1) > delayNoise) {                // and if the change is stabile enough
      statusPIR1 = candidatePIR1 ;                                 // then, candidate promoted to current status
      changePIR1 = false;                                          // no more change going on
    }
  }
  return (statusPIR1 == LOW);                                    
}
  
void setup() {
// Calibrate the PIR sensor
  Serial.begin(115200);
  pinMode(pinPIR1, INPUT);
  digitalWrite(pinPIR1, HIGH);
  Serial.print("Calibrating");
  for(int i = 0; i < calibrationTime; i++){                        // give the sensor some time to calibrate
    Serial.print(".");
    delay(1000);
  }
  Serial.println("done.");
    
// initialize the interrupt service routines for the PIR sensor
  attachInterrupt(0, isrPIR1, CHANGE);
}

void loop() {
  if (isMovementPIR1()) {
    // this code is executed in every loop round as long as the movement is being detected
    Serial.print("X");
  }
  else {
    Serial.print("_");
  }
    delay(500);
}