Saturday, June 12, 2010

Radioshack Infrared receiver <--> Arduino

Gumbo Labs

I recently came across the need to read some IR signals from my universal remote. Having hacked around with IR in the past, I had an idea of what I was looking for so I stopped by RadioShack and found one of these:
Radioshack 38khz IR receiver module

A Brief Overview of IR

The idea behind transmission of data over infrared is dirt simple. Reading about it just reminds me of how overly-complicated some communication protocols have become. The idea is that you have a transmitter, which is just an LED that emits light in the infrared spectrum and you have a receiver, which is just a phototransistor that changes the current flowing through it based on how much infrared light is hitting it. The transmitter pulses out light and the receiver is able to ’sense’ those pulses in the exact condition they were in when they left the other circuit. All information is sent in a binary format. In most protocols, you have two different types of pulses, a long and a short. The long usually corresponds to 1 and the short to 0. Here is a diagram of Sony’s IR protocol:

Here you can see that the 1 corresponds to a 1.2 millisecond pulse and the 0 a 0.6 millisecond pulse. So, there is one more detail that slightly complicates the issue. An astute reader may note:
“IR light is everywhere. How does the receiver know the difference between a pulse and noise or ambient light?”
The light coming from the transmitter is modulated at a very high frequency, usually between 38kHz and 40 kHz. It is then demodulated or integrated on the other end and turned into a solid waveform. Think of it like a high pass filter, which it may actually be in implementation, I didn’t look it up. Any naturally occurring IR light is likely to be changing at a low frequency, so this gives us a way to distinguish the signal from the noise. On top of that, many commercial products cover their receiver with a piece of plastic or lens that is coated in a material that only lets in infrared light. This helps further eliminate noise that bright visible light may cause.

The Radioshack module

This thing was extremely easy to hook up. All you need to do is supply a resistor (I used 200 Ohms, with a 3.3V supply) to the power line, GND it, then connect the data line to one of your digital pins (I used pin #2). The module itself contains the demodulation circuit so what you see is a solid signal.
Arduino connected to IR module
At this point, I decided to hook up my oscilloscope to see what was coming out of this thing. I programmed a free channel on my universal remote for a Sony DVD and started hitting some buttons. This is generally what I saw:
Signal captured
This signal was upside-down from what I had expected. The data pin appears to be idling HIGH and the signal drives it LOW. I verified this on my multimeter just to be sure. After this, I looked it up online and it appears to be the expected behavior. Not really a problem for our software, a pulse is a pulse, it is just a need to know detail for later. Another note, that first long low pulse is what is often called a start bit. It is an extra long, 2.4 millisecond pulse to notify the receiver that some bytes are coming. To read more about the protocol, take a look at this link: http://www.sbprojects.com/knowledge/ir/sirc.htm.
This is usually the point where I go online to see what others have done with the device and see if there is any code I can steal. I looked around for a bit and found this link and this code:
// 0.1 by pmalmsten
// 0.2 by farkinga
 
#define IR_BIT_LENGTH 12
#define BIT_1 1000          //Binary 1 threshold (Microseconds)
#define BIT_0 400           //Binary 0 threshold (Microseconds)
#define BIT_START 2000      //Start bit threshold (Microseconds)
#define DEBUG 0             //Serial connection must be started to debug
#define IR_PIN 7            //Sensor pin 1 wired through a 220 ohm resistor
#define LED_PIN 9           //"Ready to Recieve" flag, not needed but nice
#define POWER_PIN 11     // the red LED that indicates if the power button is pressed.
 
int runtime_debug = 0;
int output_key = 0;
int power_button = 0;
 
void setup() {
  pinMode(LED_PIN, OUTPUT);  //This shows when we're ready to recieve
  pinMode(POWER_PIN, OUTPUT);  //This is the "power on" indicator
  pinMode(IR_PIN, INPUT);
  digitalWrite(LED_PIN, LOW);     //not ready yet
  Serial.begin(9600);
}
 
void loop() {
  digitalWrite(LED_PIN, HIGH);    //Ok, i'm ready to recieve
 
  int key = get_ir_key();      //Fetch the key
 
  digitalWrite(LED_PIN, LOW);
 
  do_response(key);
 
  delay(200);
}
 
void do_response(int key)
{
  switch (key)
  {
    case 1437:
      Serial.println("toggle debug pulse");
      runtime_debug = 1 - runtime_debug;
      break;
    case 1498:
      Serial.println("Toggle key output");
      output_key = 1 - output_key;
      break;
    case 1429:
      Serial.println("Power");
      power_button = 1 - power_button;
      if (power_button)
      {
        digitalWrite(POWER_PIN, HIGH);
      }
      else
      {
        digitalWrite(POWER_PIN, LOW);
      }
      break;
    case 1424:
      Serial.println("Channel Up");
      break;
    case 1425:
      Serial.println("Channel Down");
      break;
    default:
      if (output_key)
      {
        Serial.print("Key ");
        Serial.print(key);
        Serial.println(" not programmed");
      }
      break;
  }
}
 
void read_pulse(int data[], int num_bits)
{
  for (int i = 0; i < num_bits; i++)
  {
    data[i] = pulseIn(IR_PIN, LOW);
  }
}
 
void pulse_to_bits(int pulse[], int bits[], int num_bits)
{
  if (DEBUG || runtime_debug) { Serial.println("-----"); }
 
  for(int i = 0; i < num_bits ; i++)
  {
    if (DEBUG || runtime_debug) { Serial.println(pulse[i]); }
 
    if(pulse[i] > BIT_1) //is it a 1?
    {
      bits[i] = 1;
    }
    else if(pulse[i] > BIT_0) //is it a 0?
    {
      bits[i] = 0;
    } 
 
    else //data is invalid...
    {
      Serial.println("Error");
    }
  }
}
 
int bits_to_int(int bits[], int num_bits)
{
  int result = 0;
  int seed = 1;
 
  //Convert bits to integer
  for(int i = 0 ; i < num_bits ; i++)
  {
    if(bits[i] == 1)
    {
 result += seed;
    }
 
    seed *= 2;
  }
 
  return result;
}
 
int get_ir_key()
{
  int pulse[IR_BIT_LENGTH];
  int bits[IR_BIT_LENGTH];  
 
  do {} //Wait for a start bit
  while(pulseIn(IR_PIN, LOW) < BIT_START);
 
  read_pulse(pulse, IR_BIT_LENGTH);
 
  pulse_to_bits(pulse, bits, IR_BIT_LENGTH);
 
  return bits_to_int(bits, IR_BIT_LENGTH);
}
This seemed to me like a lot of code and logic for such a simple protocol. I could go on about how wasteful I think the code is but honestly, it works and that is what is important. So, I don’t want to make it sound like it is a ‘bad’ implementation. His is actually meant to be general and to debug different protocols. I just want to make it simpler for the processor and the human reader to understand and optimized for this one particular protocol. Here is my implementation, which itself may be more flaky:
#define BITS_PER_MESSAGE 12    
#define B_1 1000              //1.2 milliseconds        
#define B_0 400                //0.6 milliseconds       
#define START_BIT 2000   //making it a little shorter than the actual 2400   
#define IR_PIN 2
 
//find the threshold time b/w the two pulse sizes
#define BIT_THRESH B_0+((B_1-B_0)/2)
 
void setup() {
  pinMode(IR_PIN, INPUT);
  delay(1000);
  Serial.begin(9600);
  Serial.println("Ready...");
}
 
void loop() {
  //int where we are storing code, must start at 0
  unsigned int code = 0x00;
 
  Serial.println("Waiting for first pulse");
 
  //block until start pulse
  while (pulseIn(IR_PIN, LOW) < START_BIT) {}
 
  //read in next 12 bits shifting and masking them into the unsigned int 'code'
  //remeber boolean is basically 0 [false] or 1 [true]
  for (byte i = 0; i < BITS_PER_MESSAGE; i++) {
      code |= (pulseIn(IR_PIN, LOW) > BIT_THRESH) << i;  
  }
 
  Serial.println(code);
  delay(500);
}
My general idea was this, calculate a threshold, the midpoint between the 2 bit pulse sizes, and compare every incoming pulse to that. So in this situation, BIT_THRESH is 700. First thing we do is create an unsigned int to store the code. Remember, an integer is 16 bits so it has more than enough space to store the 12 bits of the message. I just made it unsigned just in case but it is not likely that we will reach all the way to the sign bit on the MSB side. The next thing we do in the algorithm is wait for the start bit:
while (pulseIn(IR_PIN, LOW) < START_BIT) {}
It is set to 2000 instead of 2400 so we have a little bit of leeway. You can increase it if you start getting noise but it is not likely to happen. This is just an empty while loop or a blocking conditional. It just holds the program until it’s condition is met. In this case, we find a pulse longer than START_BIT. PulseIn is a really nice function if you aren’t familiar. The first parameter is the pin you are waiting on, and the next parameter describes how the pulse will come. From the reference:
if value is HIGH, pulseIn() waits for the pin to go HIGH, starts timing, then waits for the pin to go LOW and stops timing
Remember that our signal is inverted, so we are waiting for the pin to go LOW. Our pulse is defined as a LOW then HIGH trough. That is why we chose LOW here. Once we get our start bit, we perform the meat of the logic:
for (byte i = 0; i < BITS_PER_MESSAGE; i++) {
      code |= (pulseIn(IR_PIN, LOW) > BIT_THRESH) << i;  
}
This one is full of microcontroller jargon but shouldn’t be too hard to figure out. We are going through 12 steps, one for each bit, and calling pulseIn to get the pulse length then comparing that length to BIT_THRESH. That is going to give us a true or false which is in actuality a 1 or 0. After we get a result, we shift the 1 or 0 over by i bits and perform a bitwise OR to set the corresponding bit in the code variable. See here for an explanation of bit logic. So when the result is 0, it effectively skips the number. When it is 1, it sets bit i of the code to 1. This is relying on the fact that the code starts out as all 0 at first. Now that I think about it, it may be more efficient to do something like this:
for (byte i = 0; i < BITS_PER_MESSAGE; i++) {
      if (pulseIn(IR_PIN, LOW) > BIT_THRESH) code |= (1 << i);  
}
That way you only do the bitwise OR when you encounter a 1. Honestly consistency is much more important than efficiency in this situation so do whatever works for you. Also, the performance would be completely unnoticeable.
Technically, depending on the protocol, that integer may contain different control messages, devices, etc. But if you are just trying to map a button on your remote to an action in arduino, this should be good enough. If you are concerned about other people using your code with different remotes for different devices, I suggest further reading.
One last thing to note, I just realized that the B_1 and B_0 values don’t match with what the Sony specification says. I just took those values from that guy’s original code and didn’t think about it. It still works so I am not changing it without testing. Just a head’s up.
Hope this helps someone. I will address any questions in the comments section.

No comments:

Post a Comment