Arduino, detect 38khz with a Bandpass software digital filter.

Edit: This entry has some errors in it.  Namely, the ADC is operating too slowly.  This can be modified, see here.  However I've not had chance to try the fix or update this entry.  Apologies.

[This] is a very good book on digital filters, and it is available online for free.  I doubt I can explain the filter operation better than the author of that book.  However, I have implemented the Recursive Bandpass filter on an arduino, which might be useful to others.  

Why?  First of all, it is useful to remove background noise from an input signal (e.g. lights turning on, or the oscillation in mains power supplies) because it provides a better sense of the environment.

Second of all, using a filter provides a means to look for a specific frequency.  Therefore, we can modulate (add a precise oscillation to) a transmission signal, and this has the effect of making it more detectable, compared to looking for a constant (flat) transmission. 

This might seem strange at first, since a light that is always switched on appears brighter than a light flickered on and off.  However, our brains are tremendously good at filtering for us, and we are gifted with high resolution sensors.  Arduino's are not.

It is an issue of noise again.  With a constant IR beam, any amplification would also amplify the background noise, and the signal would be obscured.  By looking specifically for the modulation frequency, we can cut out the noise and amplify the signal.  Therefore, a tiny far away signal can be extracted from lots of background noise.  

This software filter is different to the 38Khz IR demodulation receiver chips, such as these:
                               

The above devices also use a filter internally, but provide a digital output (1 or 0) to the arduino.  As a sensor, they are best used to transmit data, such as an IR serial link between arduinos.  

This software filter implementation provides an analogue response to a modulated IR signal, meaning it is possible to gauge the distance or strength of a transmission - good for detecting obstacles or beacons. 

This filter implementation is also tuned to 38Khz, which is the common frequency of television remote controls, making it convenient to test this code.  

You will need a simple passive Infra-red photodiode, they look like LED's.  Plug the cathode into A0, and the anode into ground, and the code uses an internal pull up resistor in the arduino.  

I conducted a quick search for Processing, to graph the output of the filter over the Serial port, and that is included at the end of this post.  I only adjusted the processing code a little, so thanks goes to Tom Igoe.  A screen shot of the simple graph and the response of the filter is below:



This implementation directly accesses the timer0 feature.  It is necessary to read the analogue port at precise intervals.  However, this means millis() and delay() will not work.  If either of these are needed, it should not be too challenging to implement a counter on timer0 to create precise delays.  

Do not be intimidated by the timer0 set up procedure.  When code like (1 << CS00) is used, it simply means put a 1 in the position of the CS00 bit.  This bit is a part of a register (a bit like switches), and configures the device.  

Bitwise (logical) commands (like OR, AND) are used to write into a register.  So TCCR01 |= (1 << CS00) is actually the same as TCCR01 = TCCR01 | (1 << CS00).  Logical OR is necessary, because we want to set individual bits, but save the previous state of the register.  If we used just '=', we would overwrite the register with just a single bit activated!  

Which bits to set, and which registers to use, is all in the data sheet for the Atmega devices, which are free online, and there are normally example pseudo-code extracts as well.  

The filter itself works on the following principle.  To detect a specific frequency, we need to collect sensor readings at least twice as fast. The filter is based on some math based on frequency, which creates fixed numbers to process the input data.  Therefore, it is very important that we accurately time the collection of data, or those fixed numbers based on frequency won't work.

So for a 38Khz signal, we need to read a sensor at at least 76Khz.  In the code, I have used 100Khz as it is a nicer number to work with. The timer0 has to be set up to do this specifically, there are no ordinary arduino commands to do it. 

The timer0 is used to automatically run some code, called an Interrupt Service Routine (ISR).  The ISR reads the analogue pin A0.  We want to collect lots of sensor readings over time to get a picture of the outside world, which we place in an array and call a buffer.   The ISR collects 45 readings and then sets a flag. 

Importantly: the ISR needs to be as short as possible, because otherwise the timer0 may try to call itself again before it has even finished, and this will crash the arduino!   The ISR is therefore very fast code that runs automatically, it will happily interrupts any code in the normal loop() routine.

In the normal loop() of the program, nothing happens until the buffer flag is set by the ISR.  This will mean the ISR has finished, and so the filter function is called.  This crunches the numbers and returns a result.  We send the result out on the Serial port.  The process repeats.    All of this is slow code, it is not important when or how long it takes, compared to the ISR and the filter.  That is why it is in the loop(). 

I've prototyped this on a Duemilanove Atmega168 only, so it might throw up something interesting on another board/chip. 

Arduino Code:




  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
#include <avr/interrupt.h>
// Using timer0 disables millis() and delay()
// Using timer2 disables PWM analogue output.
// Not sure about timer 1.
// I think timer0 is best.
//
// Our program will work with a timer interupt service
// routine anyway, so delay shouldn't be necessary.
// You can always add an unsigned int to count and roll over,
// to take a delay from.
// Timer0 -> see around page 106 in atmega368/168 datasheet
// 
// The important parts for the timer are the setup() and ISR() 
// code.
//
// The interupt routine reads the analog A0 pin at 100khz.
// Read the comments through the code!
// Once an input buffer is full, the filter is run to 
// produce a variable analog output of signal strength.
// Then the process loops.
//
// Once you are happy, remove the Serial port commands for
// best performance.
//
// The filter is a recursive band-pass described in this
// incredibly awesome and free book:
// http://www.dspguide.com/ch19/1.htm

// Definitions.
const int BUFFER_SIZE = 45;    // use an odd number

// Global variables.
double buf[BUFFER_SIZE];  // Analog readings at 100khz & stored here
double out[BUFFER_SIZE];  // output of filter stored here.
int buffer_index;         // Interupt increments buffer
boolean buffer_full;      // Flag for when complete.

double a0,a1,a2,b1,b2; // filter kernel poles
double f,bw;           // frequency cutoff and bandwidth
double r,k;            // filter coefficients


void setup() {
   
   // Clear global variables before the timer
   // is activated.
   for( int i = 0; i < BUFFER_SIZE; i++ ) {
      buf[i] = 0;
      out[i] = 0; 
   }
   buffer_index = 0;
   buffer_full = false;
   
   // Set our anolgue input and use 
   // internal pull up resistor.
   pinMode(A0, INPUT);
   digitalWrite(A0, HIGH);
 
   // Configure the trigger and frequency of our timer.
   cli(); // disable interupts.
   
   // Clear the control registers so we can OR in bits safely
   TCCR0A = 0; // initialise control register to 0
   TCCR0B = 0; // initialise control register to 0
   TCNT0 = 0;  // initialise the counter to 0
                               
   // We set no prescaler, meaning the interupt is clocked at 16mhz  
   TCCR0B |= (1 << CS00); 
   
   // Set the max compare value so we achieve 100khz, 
   OCR0A = 159; // (16000000) / (100000) -1   max value is 256
 
   // OR in a bit to WGM01, setting it as CTC  
   // CTC = Clear Timer on Compare match
   TCCR0A |= ( 1 << WGM01); 
                            
   // enable timer compare interrupt
   TIMSK0 |= (1 << OCIE0A);
   
   // Activate!!
   sei();
  
   // Lets sort out the filter variables before we end setup.
   
   // Cut-off frequency.
   // We are looking for a 38khz IR TV remote.
   // F is fraction of the sample frequency 
   // It has to be between 0 and 0.5.  Therefore, the interupt
   // needs to be at least *double* the bandpass frequency.
   // I picked 100khz as a nice number to scale from.
   // So, f = (100khz * 0.38) = 38Khz
   f = 0.38;  
   
   // Bandwidth (allowance) of bandpass filter.
   // Same principle as above (fraction of 100khz).
   // We are using this filter to get rid of ambient environment
   // noise.  20khz seems like a big band, but I wouldn't expect 
   // there to be much in the khz.  You can fine tune downwards.  
   bw = 0.2;  
   
   // Maths. Read the book.  Does the trick.
   r = 1 - ( 3 * bw ); 
   k = 1 - ( 2 * r * cos(2 * PI * f ) ) + ( r * r );
   k = k / (2 - ( 2 * cos( 2 * PI * f ) ) );
   
   a0 = 1 - k;
   a1 = (2 * ( k -r ) ) * ( cos( 2 * PI * f ) );
   a2 = ( r * r ) - k;
   b1 = 2 * r * cos( 2 * PI * f );
   b2 = 0 - ( r * r ); 
   
   
   // Activate serial port.
   Serial.begin( 57600 );
      
   Serial.println("Reset");

   
   
 }

void loop() {
  
  float output;
  
  // We only do something once the buffer is full.
  if( buffer_full == true ) {
    
    // Run the input buffer through the filter
    output = doFilter();
    
    // We are going to transmit as an integer
    // Move up the decimal place
    output *= 1000;
    Serial.println( (int)output );

    // Reset our buffer and interupt routine
    buffer_index = 0;  
    buffer_full = false;
  }
  
}

// This filter looks at the previous elements in the 
// input stream and output stream to compound a pre-set
// amplification.  The amplification is set by a0,a1,a2,
// b1,b2.  Please see the linked book, above.
double doFilter() {
   int i;
   double sum;
   
   // Convolute the input buffer with the filter kernel
   // We work from 2 because we read back by 2 elements.
   // out[0] and out[1] are never set, so we clear them.
   out[0] = out[1] = 0;

   for( i = 2; i < BUFFER_SIZE; i++ ) {
      out[i] = a0 * buf[i];
      out[i] += a1 * buf[i-1];
      out[i] += a2 * buf[i-2];
      out[i] += b1 * out[i-1];
      out[i] += b2 * out[i-2]; 
   }
   
   // Bring all the output values above zero
   // To get a well reinforced average reading.
   for( i = 2; i < BUFFER_SIZE; i++ ) {
    if( out[i] < 0 ) out[i] *= -1;
    sum += out[i]; 
   }
   sum /= BUFFER_SIZE -2;
   
   return sum; 
}

ISR(TIMER0_COMPA_vect) { // called by timer0 automatically evey 100khz
    // Fills our input buffer from analog pin A0 until full and reset
    if( buffer_index >= BUFFER_SIZE ) {
       buffer_index = 0;
       buffer_full = true; 
    } else if ( buffer_full == false ){
       buf[ buffer_index ] = (double)analogRead(A0);
       buffer_index++;  
    }
}


Processing Code:




 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
// Graphing sketch


// This program takes ASCII-encoded strings
// from the serial port at 9600 baud and graphs them. It expects values in the
// range 0 to 1023, followed by a newline, or newline and carriage return

// Created 20 Apr 2005
// Updated 18 Jan 2008
// by Tom Igoe
// This example code is in the public domain.

import processing.serial.*;

Serial myPort;        // The serial port
int xPos = 1;         // horizontal position of the graph
float max_val = 0;
void setup () {
  // set the window size:
  size(800, 500);        

  // List all the available serial ports
  println(Serial.list());
  // I know that the first port in the serial list on my mac
  // is always my  Arduino, so I open Serial.list()[0].
  // Open whatever port is the one you're using.
  myPort = new Serial(this, Serial.list()[0], 57600);
  // don't generate a serialEvent() unless you get a newline character:
  myPort.bufferUntil('\n');
  // set inital background:
  background(0);
}
void draw () {
  // everything happens in the serialEvent()
}

void serialEvent (Serial myPort) {
  // get the ASCII string:
  String inString = myPort.readStringUntil('\n');

  if (inString != null) {
    // trim off any whitespace:
    inString = trim(inString);
    // convert to an int and map to the screen height:
    float inByte = float(inString);
    
    // scale the value to fit on the applet display.
    inByte = map(inByte, 0, 3000, 0, height);
    if( inByte > max_val ) max_val = inByte;
    
    //println( max_val );
    
    
    // draw the line:
    stroke(127, 34, 255);
    line(xPos, height, xPos, height - inByte);
 
    
    // at the edge of the screen, go back to the beginning:
    if (xPos >= width) {
      xPos = 0;
      background(0);
    }
    else {
      // increment the horizontal position:
      xPos++;
    }
  }
}



8 comments:

曾韬 said...

Is it a band-pass filter? I tested it in Matlab and found it is a high-pass filter

Webby said...

this is a very nice illustration of the intensity of IR light being received from the TV remote - but have you worked on taking it further?
By this I'm asking have you managed to use the remotes commands to control the Arduino?

niccokunzmann2014 said...

Thanks to this post I got into dspguide.com and created a sound recognition for 400Hz https://github.com/CoderDojoPotsdam/material/tree/master/tonerkennung

tunjow said...

u could also apply an automatic gain amplifier to the output of the IR sensor and then connect this to the controller. this will help increase the range....

tunjow said...

just thinking,.... u should be able to impliment AGC in software........
; output_to_filter = adc_value * k
then increse k when signal is low , decrease when it is getting high..... :-)
- tunjow@gmail.com

Paul said...

Thanks for the acknowledgement!

Unknown said...

Can I detect another frequency whith this code?

Bipin J said...

Sorry i am new to Aurdino, i tried the above code to capture the IR signals from a Sony remote. But without giving any IR signal itself i can see lot of logs getting displayed in serial window.What i understood is only if the buffer if full, the further processing should happen, then why i am getting these logs?
Please help. What i am expecting is removing the carrier frequency of 40Khz and displaying the mark and space values. Is that possible?

Post a Comment