IR Remote Control Reverse Engineering

Published 16th Mar 2014

What if you'd like to control your HiFi with your smartphone, but it lacks the needed interface? One solution could be to emulate your ir-remote with an Arduino.

That's what I did with my Onkyo TX-SR309.

First hook an oscilloscope to the IR diode of your remote control and try to measure the carrier frequency. Most likely this will be a 38KHz signal.

I use a DSO-203 even though it has bugs, it is very suitible for that kind of work. If the trigger is configured correctly and VOLTS/DIV and TIME/DIV are set properly, you should see an image similar to one of the screenshots below.


Reverse Engineering Procedure

I found measuring directly on the diode simpler then building a ir-receiver, even though one has to take apart the remote control.


Now we have to press each button on the remote and write down the bits as they are sent.


The first measurements already show some repeating patterns, e.g. start pulse and remote ID are always the same.


This is the 38KHz carrier frequency. 3 pulses take 79μs so we calculate 1÷(79÷3×10^-6=37974.68Hz≈38KHz.


The start of one transmission is marked by a 9ms pulse. This is like the start- bit in RS232.


Here, actual bits are transmitted, i.e. the byte 01001011 (last 1 not shown completely). This happens to be the ID of the remote sent on every button press.


Onkyo RC-799M Protocol

Timings
-------

T=580μs

0 ... 1T high 1T low
1 ... 1T high 3T low

Protocol
--------

Start ... 16T high 8T low

power:        01001011 00110110 11010011 00101100 x
display:      01001011 00110110 10101010 01010101 x

bd/dvd:       01001011 00110110 00110001 11001110 x
vcr/vdr:      01001011 10110110 11110000 00001111 x
cbl/sat:      01001011 10110110 01110000 10001111 x
game:         01001011 10110110 10110000 01001111 x
aux:          01001011 10110110 11111001 00000110 x
am:           01001011 10110110 11100010 00011101 x
fm:           01001011 10110110 10010000 01101111 x
tv/cd:        01001011 10110110 10010000 01101111 x
tone:         01001011 00110110 10010010 01101101 x
usb:          01001011 00110101 01111001 10000110 x
mute:         01001011 10110110 10100000 01011111 x

plus:         01001011 00110110 00000010 11111101 x
minus:        01001011 00110110 10000010 01111101 x
channel up:   01001011 10110110 00000000 11111111 x
channel down: 01001011 10110110 10000000 01111111 x
volum up:     01001011 10110110 01000000 10111111 x
volum down:   01001011 10110110 11000000 00111111 x

sp a/b:       01001011 00110110 00010001 11101110 x
setup:        01001011 00110110 11001010 00110101 x
home:         01001011 10011000 00000011 11111100 x
return:       01001011 00110110 00101010 11010101 x
up:           01001011 10110110 01000001 10111110 x
down:         01001011 10110110 11000001 00111110 x
left:         01001011 10110110 00100001 11011110 x
right:        01001011 10110110 10100001 01011110 x
enter:        01001011 10110110 11101001 00010110 x

|<<:          01001011 00110100 01111000 10000111 x
>>|:          01001011 00110100 10111000 01000111 x
<<:           01001011 00110100 10000000 01111111 x
>:            01001011 00110100 11011000 00100111 x
>>:           01001011 00110100 00000000 11111111 x
||:           01001011 00110100 11111000 00000111 x
[]:           01001011 00110100 00111000 11000111 x

movie/tv:     01001011 00110110 00010001 11101110 x
music:        01001011 00110110 10001001 01110110 x
game:         01001011 00110110 10001011 01110100 x
stereo:       01001011 00110110 00110010 11001101 x

1:            01001011 01000000 10101011 01010100 x
2:            01001011 01000000 01101011 10010100 x
3:            01001011 01000000 11101011 00010100 x
4:            01001011 01000000 00011011 11100100 x
5:            01001011 01000000 10011011 01100100 x
6:            01001011 01000000 01011011 10100100 x
7:            01001011 01000000 11011011 00100100 x
8:            01001011 01000000 00111011 11000100 x
9:            01001011 01000000 10111011 01000100 x
0:            01001011 01000000 01111011 10000100 x
+10:          01001011 01000000 00001011 11110100 x
clr:          01001011 01000000 10001011 01110100 x

Stop ... 1T high

Key Repeat
----------

When a key is held down, there is a gap of 38.5ms before the next key event is
sent.


There are two things that imediately catch our attention here. First the first byte transmitted is always 01001011, obviously the remote controller ID and second the last byte is always the same as the third one but inverted. Now we have all we need to write a sketch to emulate the ir-remote.

The following sketch does exactly that. It listens for commands on the serial interface and sends the correct pulses on pin 11. The bits from the protocol specification have to be reversed (MSB first to LSB first) and converted to hex (see example code below). Keep in mind that measured times differe from actual times in the code due to function call overheads.


Arduino Onkyo RC-799M Emu

// ---------------------------------------------------------------------
// IR class (Onkyo RC-799M)
// ---------------------------------------------------------------------

#define RED_LED_PIN     13
#define IR_LED_PIN      11

#define TIME_BASE      580  // calc: 579us, measure: 583us

#define PULSE_HIGH       3  // 9μs
#define PULSE_LOW        9  // 17μs

#define DEVICE_ID     0xD2  // reverse of 01001011 in hex



class IR {

private:

  // Send a 38kHz-modulated pulse
  static void
  Pulse(void) {
    for (byte i=0; i<22; i++) {
      digitalWrite(IR_LED_PIN, HIGH);
      delayMicroseconds(PULSE_HIGH);
      digitalWrite(IR_LED_PIN, LOW);
      delayMicroseconds(PULSE_LOW);
    }
  }

  // Send a bit
  static void
  SendBit(byte b) {
    Pulse(); 

    if (b) {
      delayMicroseconds(TIME_BASE * 3); // one
    } else {
      delayMicroseconds(TIME_BASE * 1); // zero
    }
  }

  // Send a byte
  static void
  SendByte(byte b) {
    for (byte i=0; i<8; i++) SendBit((b>>i) & 0x01);
  }

public:

  static void
  SendCommand(byte device, byte byte1, byte byte2, byte byte3) {
    cli(); // disable interrupts

    // start transmission
    for (byte i=0; i<16; i++) Pulse();
    delayMicroseconds(TIME_BASE * 8);

    // send data
    SendByte(device);
    SendByte(byte1);
    SendByte(byte2);
    SendByte(byte3);

    // stop bit
    Pulse();

    sei(); // enable interrupts
  }
};


// ---------------------------------------------------------------------
// Initialization
// ---------------------------------------------------------------------

String input = "";         // a string to hold incoming data
boolean process = false;   // whether the string is complete

void
setup() {
  // Onboard Led
  pinMode(RED_LED_PIN, OUTPUT);
  digitalWrite(RED_LED_PIN, LOW);

  // IR Led
  pinMode(IR_LED_PIN, OUTPUT);
  digitalWrite(IR_LED_PIN, LOW);

  // Serial Console
  Serial.begin(19200);

  // reserve 20 bytes for the inputString:
  input.reserve(20);
}

// ---------------------------------------------------------------------
// Main Program
// ---------------------------------------------------------------------

void
loop() {
  if (process) {
    byte byte0 = DEVICE_ID, byte1 = 0x00, byte2 = 0x00;

    if (input == "status") {
      Serial.println("OK");
    } else if (input == "mute") {
      byte1 = 0x6D; byte2 = 0x05;
    } else if (input == "vol+") {
      byte1 = 0x6D; byte2 = 0x02;
    } else if (input == "vol-") {
      byte1 = 0x6D; byte2 = 0x03;
    } else if (input == "ps2") {
      byte1 = 0x6D; byte2 = 0x0D;
    } else if (input == "xbmc") {
      byte1 = 0x6C; byte2 = 0x8C;
    } else if (input == "power") {
      byte1 = 0x6C; byte2 = 0xCB;
    } else if (input == "speaker") {
      byte1 = 0x6C; byte2 = 0x88;
    } else if (input == "movie") {
      byte1 = 0x6C; byte2 = 0x53;
    } else if (input == "music") {
      byte1 = 0x6C; byte2 = 0x91;
    } else if (input == "game") {
      byte1 = 0x6C; byte2 = 0xD1;
    } else if (input == "stereo") {
      byte1 = 0x6C; byte2 = 0x4C;
    } else {
       Serial.println("?");
    }

    if (byte1 != 0x00) {
      digitalWrite(RED_LED_PIN, HIGH);
      IR::SendCommand(byte0, byte1, byte2, ~byte2);
      digitalWrite(RED_LED_PIN, LOW);
      Serial.println("!");

      delay(38); // minimum delay before next key can be sent
    }

    input = "";
    process = false;
  }
}

void
serialEvent() {
  while (Serial.available()) {
    char c = (char)Serial.read();

    if (c == '\n') {
      process = true;
    } else {
      input += c;
    }
  }
}


Now we are ready to test the sketch on actual hardware. An Arduino Mega was the closest to me at that time of day, so that's what I used. I ripped out the ir- diode from an old remote control, soldered it to two resistors (2x330Ohm in parallel -> 165Ohm) and was ready to go.


With only a resistance 165Ohm we push the pin to it's limits, but I wanted the diode to be as bright as possible.


After everything is connected, we can use the same test method to check the output signal of the Arduino.


Compared to the original signal we have a perfect match of pulse width and pulse pause.


Not really surprising - the signal shape (rectangle) from the emulation is even better then the original.


comments powered by Disqus