Infrared Remote Control Protocols: Part 2

In the previous post, techniques on how to capture an IR remote signal were presented and the most reliable one was using the Arduino sketch. The captured signal was also analyzed, although we had much of our work already done for us.

In this concluding post, a remote control whose protocol is unknown will be captured and analyzed as a case study. Lastly, we will cover the re-transmission of the IR signal. The remote control in question is for my ceiling fan, KDK model M56SR. The remote also works for two other fan models M56QR and M11SU.

KDK remote control

The first step is to perform a capture of the remote signal to determine what are the pulse durations involved. For this, I pressed the On/Off button about 400 times, and obtained the following histograms. The value in bold is the chosen duration value that lies somewhere close to the middle of all the occurrences, indicated by the blue arrow. The middle value should be chosen since there will probably be some error when you try to reproduce the signal as well.

histogram for KDK IR signals

If we take a look at the raw values, it looks similar to the Apple Remote that we previously analyzed, except that the duration values are different. This protocol also seems to have 4 unique duration values, 2 of which are used in the header, and the other 2 are used in the payload. The signal even ends with a single “stop bit”.

3516  1664  500   372 
468   1264  496   372 
496   404   472   372 
496   396   444   400 
492   380   496   400 
...   
436   432   436   1296    
436   440   436

Assuming that the protocols are similar, we begin decoding the signal by taking the pair (440, 440) as “0” and (440, 1300) as “1”. Each signal consists of 56 bits, as opposed to only 32 bits used by the Apple Remote. The signal for the “On/Off” button is decoded into the following 7 bytes (in hexadecimal):

02 20 d0 84 30 31 55

At this point, it looks quite good so we will proceed to decode the rest of the remote buttons in the same way:

  • 02 20 d0 84 30 31 55 - on/off
  • 02 20 d0 84 30 21 45 - fan speed 1
  • 02 20 d0 84 30 22 46 - fan speed 2
  • 02 20 d0 84 30 23 47 - fan speed 3
  • 02 20 d0 84 30 40 24 - timer cancel
  • 02 20 d0 84 30 41 25 - timer 1 hr
  • 02 20 d0 84 30 43 27 - timer 3 hr
  • 02 20 d0 84 30 46 27 - timer 6 hr
  • 02 20 d0 84 30 4e 2a - sleep mode

This is typical of remote signals - they share most of the bytes and only a few bytes vary across the different signals (in this case it’s just the last 2 bytes). One way to tell that the decoded bytes are correct is the pattern formed by the second last byte. Let’s call this the “command byte”. For fan speeds 1, 2 and 3, the corresponding command bytes are 21, 22 and 23, and for the timer values 1 hr, 3 hr and 6 hr, the command bytes are 41, 43 and 46, respectively. At this point, it makes you wonder what would happen if you send 42 or 44 instead.

Note that we were just lucky to have chosen to (correctly) interpret the pair (440, 440) as “0”. If we had did it the other way round, the bits would have all been inverted. This is where you have to start looking for patterns, or inverting the bits to make sure you’ve got it right, and this will take some time.

As for the last byte, notice that it varies with the command byte, but yet it doesn’t change between the timer 3 hr and 6h command bytes. This should hint that the last byte is some type of checksum. Since remote control protocols are transmit-only, there needs to be a way for the receiver to make sure the signal is valid. To test this theory, I have printed the protocol bytes in binary, stacked on top of each other:

0x02 (00000010)
0x20 (00100000)
0xd0 (11010000)
0x84 (10000100)
0x30 (00110000)
0x31 (00110001)
0x55 (01010101) <-- checksum

Notice that if you do an exclusive-‍OR (XOR) on the highlighted bytes, you will get the same value as the last byte. Columns with a single 1 carry that 1 down to the result, whereas columns with two 1’s will result in a 0, and columns with three 1’s gives a 1. If you try to extend the XOR operation to include the first or second bytes, the result will no longer be correct.

So at this point, the protocol has been pretty much reverse-engineered:

  • Each command consists of 7 bytes, and the first 5 bytes are constant
  • The 6th byte is the “command byte”
  • The last byte is the checksum, computed by XOR-‍ing the 3rd to 6th bytes

This is illustrated in the following figure.

KDK IR code format diagram

Armed with information about the protocol, we can begin writing the code to re-transmit this signal.

IR Transmission

As you may recall, each pulse that the IR is “on” is actually a 38 kHz carrier signal. To emit this signal, I shall steal some code from Ken Sherrrif’s IR Arduino library to initialize Timer2 for PWM output. The IR LED needs to be connected to pin 3 on the Arduino. If you do not use an output transistor, you need to get quite close for the target device to pick up the IR signal.

The initialization for Timer2 is as follows. Wave Generation Mode for Timer2 (WGM2) is set to mode 5 (phase-correct with OCR2A as TOP), and the prescaler is set to none. OCR2A is then set to the count for half the period of 38 kHz (~13.158 µs) at 16 MHz, and OCR2B is set to one third that value, which would yield an approximate 33% duty cycle. The PWM output is enabled by setting the Compare Match output B (COM2B) bits to 2, which enables output during down-counting and clears during up-counting. The generated waveform and associated Timer2 values is shown in the figure below.

PWM waveform timings diagram

The functions to enable and disable the 38 kHz IR signal are called mark() and space(), respectively. These functions are called by sendbyte(), which take a byte and starts transmitting from the least significant bit by calling mark() and space() with the durations measured earlier.

The demo sketch shown below delays 1 millisecond on reset and transmits the IR signal for On/Off. Note that the header and the “stop bit” must be transmitted as well. The sketch then goes into an endless loop, waiting for the next reset.

#define SYSCLOCK 16000000

// must use pin 3 for IR output

void setup() {
  pinMode(3, OUTPUT);
  digitalWrite(3, LOW); // When not sending PWM, we want it low

  // WGM2 = 101: phase-correct PWM with OCRA as top
  // CS2 = 000: no prescaling
  TCCR2A = _BV(WGM20);
  TCCR2B = _BV(WGM22) | _BV(CS20);

  // frequency and duty cycle
  int khz = 38;
  OCR2A = SYSCLOCK / 2 / khz / 1000;
  OCR2B = OCR2A / 3; // 33% duty cycle
}

inline void mark(int time) {
  TCCR2A |= _BV(COM2B1); // Enable pin 3 PWM output
  delayMicroseconds(time);
}

inline void space(int time) {
  TCCR2A &= ~(_BV(COM2B1)); // Disable pin 3 PWM output
  delayMicroseconds(time);
}

void sendbyte(unsigned char b) {
  int i;
  for (i = 0; i < 8; i++) {
	if (b & 1) {
	  // one
	  mark (440);
	  space(1300);
	} else {
	  // zero
	  mark (440);
	  space(440);
	}
	b >>= 1;
  }
}

void loop() {
  delayMicroseconds(1000);

  // header
  mark (3500);
  space(1700);

  sendbyte(0x02);
  sendbyte(0x20);
  sendbyte(0xd0);
  sendbyte(0x84);
  sendbyte(0x30);
  sendbyte(0x31);
  sendbyte(0x55);

  // stop
  mark (440);
  space(0);

  // sleep forever
  while (1);
}

Conclusion

This post concludes the series by reverse-engineering the IR signal used by the KDK M56SR remote control, as well as show an Arduino sketch that transmits these IR signals to emulate the remote.

As previously mentioned, if you are just re-transmitting the code, reverse-engineering is not necessary but it does satisfy my curiousity and it’s fun! However if you are building a receiver for this code, then reverse-engineering would definitely help. For this instance, we just need to match the first 5 bytes to the fixed pattern, record the command byte, and compute the XOR checksum to make sure the signal is valid. The receiver code will be much cleaner if you use a single switch-case block to handle the different button presses.

Hopefully you have found this walkthrough useful, whether you’re trying to reverse-engineer your own, or just happen have the same ceiling fan as me and want to know what IR protocol it uses.