IR Remote Control

IR Remote Control 1

Setup. 1

Modulation. 2

Decoding. 3

De-bouncing. 8

Emulation. 8

Conclusion. 9

Appendix A: Decoding. 10

Appendix B: Encoding: IR Codes. 13

Appendix C: Encoding: Send. 15

 

The goal is to simulate an Infra Red (IR) Remote Control so that we can use an Arduino like device to control a Settop box.  Verizon has a really nice FIOS Mobile app that allows simulating remote control buttons from a tablet or phone.  It works well, however, I needed to write my own interface.  There were two alternatives:

  1. Analyze the IR data between the remote and the Settop box
  2. Analyze the IP byte stream between the FIOS app and the Settop box

This note is about the first alternative; I'll try the second later.  The Arduino has IR libraries written in C++, but I couldn't find any decent documentation for them to be useful. 

Setup

I used an oscilloscope and a TCRT5000L IR sensor to view the waveform emitted by the remote.  The LED portion of the TCRT5000L will be used later.  Turn off any bright light in the area.  Aim the Remote at the sensor and press any button.

You should see a pattern like this on the scope for Remote button 3, @10 ms/div.  We get very nice TTL signals with the simple setup.  The signal is high when there is IR and low without the IR.

Modulation

The first thing you notice in the picture above is that the signal peaks seem very noisy (circled).  Let's zoom in.  We see a nice waveform that does not seem like noise.  Click on the screen shots below to expand.

 

A bit of research on Google explains that this is the commonly used 38kHz modulation.  Many IRs use this technique to reduce interference from other IR sources (like fireplaces, warm bodies, etc.)  Checking the wavelength of the signal above shows that it is pretty close to 38kHz.

Decoding

Pressing each button on the Remote shows that they all have the general format as shown below.  There are 2 pulse trains, separated by 100 ms.  Here is the trace for button 3: a Button Start Marker, followed by data, followed by a Stop Marker. 

 

I made up the terms Start and Stop Marker.  The Marker pulse is wider than the data pulses.  Here are some measurements:

It seems that the time between pulses indicates whether the data bit is a 0 or 1.  Again Google comes to the rescue.  The tutorial from Vishay discusses Phase encoding, Pulse distance encoding and Pulse width encoding.  Clearly, Pulse width is not being used here because most of the pulses are narrow.  Let's assume that Pulse length encoding is being used, since it is a bit easier to work with.  (Turns out this is the correct assumption!)  A picture on the Web describes this nicely, though the timings don't quite match ours.

 

Let's get some more waveforms to study.

Button 1 = 10000000 00001111

===========================

Button 2 = 01000000 00000111

===========================

Button 3 = 11000000 00001011

===========================

Button 4 = 10010000000000011

===========================

 

Based on the waveforms seen on the scope, I couldn't imagine how to have a bit stream ending with a 0 while using the Pulse distance encoding.  Every button as seen would have to end with a 1 bit since there is a 30 ms Low between the last data bit and the Stop Marker.  So I am guessing that the encoding is a bit different from the standard Pulse distance encoding described.  I see it as being Low for a time, followed by a 0.5 ms High, i.e., the High pulse appears at the end of the data bit, not the beginning.

With the above assumption, it appears that the Stop and Start markers are as follows:

Using the decoding program in Appendix A, one can collect the codes transmitted for each button.  Connect an Arduino digital input pin along with the Oscilloscope.  No additional circuitry is necessary, however, you may need to 'de-bounce' the signal in software.  The codes are perfectly repeatable.  They are in the table below.  The transmitted code doesn't make much sense until you realize that it is transmitted Least Significant Bit (LSB) first.  So let's reverse it and display it in Hex.  Ta da, the patterns jump out.  The low order byte contains a command code, while the high order byte contains a check-sum of some sort.

Button

Transmitted code

Binary (Reversed)

Hex

Command (Dec)

0

0000000000000000

00000000 00000000

0x0000

0

1

1000000000001111

11110000 00000001

0xF001

1

2

0100000000000111

11100000 00000010

0xE002

2

3

1100000000001011

11010000 00000011

0xD003

3

4

0010000000000011

11000000 00000100

0xC004

4

5

1010000000001101

10110000 00000101

0xB005

5

6

0110000000000101

10100000 00000110

0xA006

6

7

1110000000001001

10010000 00000111

0x9007

7

8

0001000000000001

10000000 00001000

0x8008

8

9

1001000000001110

01110000 00001001

0x7009

9

*

0010001000000001

10000000 01000100

0x8044

68

#

0000001000000011

11000000 01000000

0xC040

64

ok

1000100000000111

11100000 00010001

0xE011

17

ch+

1101000000001010

01010000 00001011

0x500B

11

ch-

0011000000000010

01000000 00001100

0x400C

12

Exit

0100100000001011

11010000 00010010

0xD012

18

Opts

0100001000000101

10100000 01000010

0xA042

66

Menu

1001100000000110

01100000 00011001

0x6019

25

Guide

0000110000001011

11010000 00110000

0xD030

48

Info

1100110000000101

10100000 00110011

0xA033

51

STB

0101000000000110

01100000 00001010

0x600A

10

Up

0010110000001001

10010000 00110100

0x9034

52

Down

1010110000000001

10000000 00110101

0x8035

53

Left

0110110000001110

01110000 00110110

0x7036

54

Right

1110110000000110

01100000 00110111

0x6037

55

FIOS

0111110000001111

11110000 00111110

0xF03E

62

DVR

1011110000000000

00000000 00111101

0x003D

61

Star

0101100000001010

01010000 00011010

0x501A

26

Plus

1100001000001001

10010000 01000011

0x9043

67

Heart

1010100000000101

10100000 00010101

0xA015

21

Play

1101100000000010

01000000 00011011

0x401B

27

Pause

1111100000000000

00000000 00011111

0x001F

31

Stop

0011100000001100

00110000 00011100

0x301C

28

Rec

1000110000000011

11000000 00110001

0xC031

49

Prev

0011110000001000

00010000 00111100

0x103C

60

Next

1111110000000111

11100000 00111111

0xE03F

63

Rwd

0111100000001000

00010000 00011110

0x101E

30

Fwd

1011100000000100

00100000 00011101

0x201D

29

A

1110100000000001

10000000 00010111

0x8017

23

B

1110100000000001

01110000 00100111

0x7027

39

C

0001010000000110

01100000 00101000

0x6028

40

D

1001010000001010

01010000 00101001

0x5029

41

PIP

0100010000000011

11000000 00100010

0xC022

34

 

The Volume, Mute & A/V buttons depend on the type of TV selected.

When something appears so simple, we must be on the right track.  All we need to do is to figure out the formula for the checksum.  A Check byte is appended to the command byte.  The two bytes are transmitted LSB first.  The Check byte serves to verify that the data is received correctly.  The Settop box will ignore a received command if received checksum does not match the expected value that it computes. 

Just by looking at the many example values in the Hex column, we see that the hex digits always add up to 16 (or a multiple of it.)  So, the Check digit seems to be the negative of sum of 2 hex digits of the command code.  The result is moved to the high order nibble of the Check byte.

checkByte = (-((command & 0xF0) + (command << 4))) & 0xF0

De-bouncing

There is a problem associated with the minimalist approach used for sensing the signal.  The voltage at the photo transistor is an analog value that we are treating as a digital signal.  While the signal looks fine on the scope, it will tend to confuse the program.  Here is a nice explanation of the issue.  It has to do with the voltage being in an "indeterminate" zone between High and Low while it is changing state.  We could use an A to D converter to measure the voltage, but we just wanna to use the Digital pin.  An electronics enthusiast would go nuts addressing the problem with op amps, but we can solve the problem quite easily in software.  In the function waitForState(), we wait for 4 consecutive readings with the same value before we accept it.  We are waiting for the signal to "settle" when it is changing.  You may need to increase the number if you have a noisier situation.

Emulation

The hard part is done.  Generating the IR codes is relatively easy.  Hook up an IR LED to the Digital Out pin of an Arduino.  If you are using a 3.3V device, you may need to reduce the resistor to 47 Ω.  However, the Settop receiver seems to be quite sensitive and 100 Ω should be fine.

The associated code for Arduinos can be downloaded from here.  Since Arduinos are relatively slow, generating the 38 kHz modulation will be a challenge.  I generated pulses and viewed the waveforms on the scope.  The delay values in the function sendPulse() had to be adjusted by trial and error till the output came close to 38 kHz.  On testing, it worked on the first attempt !!

Then I added WiFi support just because I could. The code was tested on an ESP-12, which is like an Arduino that supports WiFi.  The delay values will need to be changed if some other device is used.  If you don't have an oscilloscope to examine the output, try reducing or increasing the delay times in steps of 10% till it works.  Note that adding print statements will affect timings.

Conclusion

My analysis didn't quite fit with the standards, but it fit the measured data.

The purpose of this educational note is just to show how something can be analyzed. Use it to annoy your parents by changing the channel to your favorite show every 5 mins even when you are not anywhere nearby.  Any harmful or commercial use of this information is deprecated. 


Appendix A: Decoding

// IRRemoteScan

#include <arduino.h>

 

const int ird = 5;            // IR input

unsigned long tWait, tLow, tHigh;

String pulses;

 

void setup() {

  Serial.begin(9600);

  pinMode(ird, INPUT);

  Serial.println("\nStarting IRRemoteScan");

}

 

void loop() {

  if (!getPulseTrain(ird, 200000))    // 200 ms

    return;

 

  Serial.print("\nCode:");

  Serial.print(pulses);

  Serial.print("  Reversed:");

  Serial.print(reverse(pulses));

  Serial.print("  Hex: 0x");

  Serial.println(value(pulses), HEX);

}

 

/////////////////////////////////

 

// detect a sequence of bits after a marker

boolean getPulseTrain(int ird, long timeout) {

  if (!getPulse(ird, timeout))   // on success, tLow & tHigh are set

    return false;                   // timeout

 

  // tHigh should be about 10 ms for Marker

  if (tHigh < 8000  ||  tHigh > 12000)

    return false;

 

  // get the next data pulse

  if (!getPulse(ird, timeout))

    return false;                     // unexpected timeout

 

  if (tLow < 4000) {         

    pulses = "STOP";

    //Serial.println(" STOP");

    return true;

  }

 

  pulses = "";

  for (;;) {

    // get the next data pulse

    if (!getPulse(ird, 6000)) // timeout = 6 ms

      return true;      // done

    if (tLow < 4000)

      pulses += "0";

    else

      pulses += "1";

  }

}

 

 

// returns tHigh and tLow (in uSec)

boolean getPulse(int ird, long timeout) {

  // wait for signal to go HIGH

  if (!waitForState(ird, HIGH, timeout))

    return false;

 

  tLow = tWait;

  //Serial.print(" tLow=");

  //Serial.print(tLow);

  //if (tLow < 1500  ||  tLow > 6000)

  //  Serial.print(" !!!");

  //Serial.print("; ");

 

  // wait for signal to go LOW

  if (!waitForState(ird, LOW, timeout))

    return false;

 

  tHigh = tWait;

  //Serial.print(" tHigh=");

  //Serial.print(tHigh);

  //if (tHigh < 300  ||  tHigh > 600)

  //  Serial.print(" !!!");

  //Serial.print(";  ");

  return true;

}

 

// returns duration (in ms) till pin is at requested state (HIGH or LOW)

// returns +ve on success, -ve on timeout

boolean waitForState(int pin, int state, long timeout) {

  unsigned long start = micros();

  int repeat = 0;

  for (;;) {

    tWait = micros() - start;

    if (digitalRead(pin) == state) {

      if (++repeat > 4) // need to see 4 consecutive readings to avoid noise

        return true;

    }

    else

      repeat = 0;

    if (tWait > timeout)

      return false;

    yield();

  }

}

 

// reverses input string

String reverse(String str) {

  String rev = "";

  while (str.length() > 0) {

    rev = str.substring(0, 1) + rev;

    str = str.substring(1);         // pop

  }

  return rev;

}

 

// converts LSB-First bit string to int. 

int value(String str) {

  int val = 0;

  int mask = 1;

  while (str.length() > 0) {

    if (str.substring(0, 1).equals("1"))

      val |= mask;

 

    // prepare for next bit

    mask <<= 1;

    str = str.substring(1);         // pop

  }

  return val;

}


Appendix B: Encoding: IR Codes

// IR Codes for VZ FIOS remote

 

struct IRCode {

  String buttonName;

  int    buttonCode;

};

 

struct IRCode codes[] = {

  { "0",  0 },

  { "1",  1 },

  { "2",  2 },

  { "3",  3 },

  { "4",  4 },

  { "5",  5 },

  { "6",  6 },

  { "7",  7 },

  { "8",  8 },

  { "9",  9 },

  { "*",  68 },

  { "#",  64 },

  { "ok",   17 },

  { "ch+",  11 },

  { "ch-",  12 },

  { "Exit",   18 },

  { "Opts",   66 },

  { "Menu",   25 },

  { "Guide",  48 },

  { "Info",   51 },

  { "STB",  10 },

  { "Up",   52 },

  { "Down",   53 },

  { "Left",   54 },

  { "Right",  55 },

  { "FIOS",   62 },

  { "DVR",  61 },

  { "Star",   26 },

  { "Plus",   67 },

  { "Heart",  21 },

  { "Play",   27 },

  { "Pause",  31 },

  { "Stop",   28 },

  { "Rec",  49 },

  { "Prev",   60 },

  { "Next",   63 },

  { "Rwd",  30 },

  { "Fwd",  29 },

  { "A",  23 },

  { "B",  39 },

  { "C",  40 },

  { "D",  41 },

  { "PIP",  34 }

};

 

const int numButtons = (sizeof(codes)/sizeof(codes[0]));

 

// find button and return 16 bit value to be transmitted

int buttonValue(String button) {

  for (int i = 0; i < numButtons; ++i)

    if (codes[i].buttonName.equalsIgnoreCase(button))

      return value(codes[i].buttonCode);

  return -1;

}

 

// compute 16 bit value: checkDigit followed by command code

int value(int val) {

  return (checkDigit(val) << 8) | val;

}

 

// compute checksum digit, 4 bit value in high order nibble

int checkDigit(int val) {

  return (-((val & 0xF0) + (val << 4))) & 0xF0;

}


Appendix C: Encoding: Send

// IRRemoteSend

#include <arduino.h>

#include <ESP8266WiFi.h>

 

const int ird = 14;           // Pin 14, LED output

WiFiServer server(80);

WiFiClient client;

const String http = "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n";

 

void setup() {

  Serial.begin(115200);

  Serial.println("\nStarting IRRemoteSend");

  pinMode(ird, OUTPUT);

 

  WiFi.mode(WIFI_AP);

  WiFi.softAP("remote", "", 1);  // wide open AP, no password

  Serial.print("AP 'remote' up at ");

  Serial.println(WiFi.softAPIP());

 

  server.begin();  // start TCP server

}

 

void loop() {

  if (! client.connected())

    client = server.available();

  if (client  &&  client.available())

    handleWiFiClient();   // process request from browser

}

 

// process request from browser client

void handleWiFiClient() {

  String req = client.readStringUntil('\r');  // get first line

  client.flush();                             // this is needed

 

  Serial.print("\nFrom ");

  Serial.print(client.remoteIP());            // debug

  Serial.print(": ");

  Serial.println(req);                        // debug

 

  if (req.indexOf("favicon") < 0) {

    String seq = req.substring(req.indexOf("/")+1);

    seq = seq.substring(0, seq.indexOf(" "));

    String resp = http+"<html>"+seq+"</html>\r\n";

    client.print(resp);                       // send web page

 

    sendButtonSeq(seq);       // IR stuff

  }

 

  client.stop();   // cannot open another connection without closing this one

}

 

/////////////////////////////////

 

// input is a sequence of buttons separated by ','.

void sendButtonSeq(String seq) {

  String button;

  while (seq.length() > 0) {

    int dlm = seq.indexOf(",");

    if (dlm < 0) {            // no more?

      button = seq;

      seq = ""; 

    }

    else {                    // pop off first cmd

      button = seq.substring(0, dlm);

      dlm++;                  // skip past ','

      if (dlm < seq.length())

        seq = seq.substring(dlm);

      else

        seq = "";    }

    sendButton(button);

    Serial.print(",");

    Serial.print(button);

    delay(500);               // wait a bit between buttons, adds to suspense!

  }

}

 

// send one button command

void sendButton(String button) {

  int val = buttonValue(button);          // get button code

  if (val < 0)                                        // invalid command ignored

    return;

 

  sendMarker(1);      // send start

  for (int i = 0; i < 16; ++i)                  // send 16 bits

    sendBit((val >> i) & 1);

  delay(30);

  sendMarker(0);      // send stop

}

 

// STOP=sendMarker(0)  and  START=sendMarker(1)

void sendMarker(int data) {

  sendPulse(9000);         // 9 ms

  sendBit(data);

}

 

// send a data pulse

void sendBit(int data) {

  delay((data == 0)? 2 : 5);  // Low signal

  sendPulse(500);             // 500 us wide pulse

}

 

// send 38kHz modulated pulse for specified us

// the 2 delay values are tuned for ESP-12 by watching the output on a scope

// time for signal on is less than signal off, perhaps because of the resistor value chosen

void sendPulse(int us) {

  unsigned long endTime = us + micros();

  for (;;) {

    digitalWrite(ird, HIGH);

    delayMicroseconds(6);           // ramps up faster, reason unknown

    digitalWrite(ird, LOW);

    delayMicroseconds(18);          // ramps down slowly

 

    if (micros() > endTime)

      break;        // end of pulse

  }

}