Decoding remote control signals. A weird and wonderful world I know nothing about.

Introduction

So I bought this little device, and ran the example code (in Python). It worked, to an extent, and I got things like:

Received: 5D05C03F
Received: 5D0540BF
Received: 5D05807F

when I pressed Vol+, Vol- and Mute respectively.

This looks like “extended NEC protocol”, in that the fourth byte is the 2s-complement of the third byte, and the first and second bytes are unrelated. In other words, the “address” is 0x5d05 and the “commands” are 0x03, 0x40 and 0x80.

But I want to make sure that I understand what’s going on, because when I try to emit the same signal, I fail. I want to build a little remote control to turn my soundbar on and off (currently using a Fire Stick remote to do that, which is a bit silly).

So, NEC protocol. Apparently (I say that, but I have to believe it), the start of the frame is signalled by “on” for 9ms, then “off” for 4.5ms. Then bits are sent in the following way:

  • “1” is a 0.5625ms pulse followed by a 1.6875ms pause
  • “0” is a 0.5625ms pulse followed by a 0.5625ms pause

So transmitting “1” takes 0.5625ms + 1.6875ms = 2.25ms, and “0” takes 0.5625ms + 0.5625ms = 1.125ms.

A final pulse burst of 0.5625ms signals the end of the transmission.

See https://techdocs.altium.com/display/FPGA/NEC%2bInfrared%2bTransmission%2bProtocol for example (the same stuff exists on multiple websites, with the same graphics). https://circuitdigest.com/microcontroller-projects/build-your-own-ir-remote-decoder-using-tsop-and-pic-microcontroller is also a good source of information, as it shows what the signal actually looks like.

Since I have an IR decoder, it should “demodulate” the signal, in other words convert the 38 kHz pulse signal into 1s and 0s. They still need to be decoded, though, but it’s more doable.

Model

The model I’d like to use is a state machine. The idea is to poll the signal and move to another state based on the signal.

There are several states:

  1. Wait for “1”. That means that on “0” we stay in this state. On “1” we move to state 2.
  2. Wait for “0” after roughly 9ms. That means that on “1” we stay in this state, unless we’ve been here for too long (with some threshold). On “0” we got to state 3 if we’ve been here for roughly 9ms (with some threshold) and to state 1 if not.
  3. Wait for “1” after roughly 4.5ms. The same thing as in state 2 happens, except that now we want an “error state” because if we get “1” after 0.2ms, then that’s an error. We want to wait until it’s “0” again, then go to state 1.
  4. Wait for “0” after roughly 562.5us. Same game again, and we go to state 5.
  5. Wait for “1”, this time it can come after 562.5us or 1687us. Depending on which it is, we remember which bit we received. Then we go to state 4.
  6. Error state (see state 3).

We expect to move between states 4 and 5 for 32 bits + one pulse, then go back to state 1. If that’s the case, we received a valid code. Anything else is an error.

I’d love to draw a diagram for this. Maybe:

diagram

I don’t know of an easy way to encode timings into a state machine, so maybe just some simple code.

First Attempt

Sadly, it’s not as easy as I hoped. Trying this:

include <stdio.h>
#include "pico/stdlib.h"

#define IR_INPUT_PIN 5

int main()
{
    stdio_init_all();

    gpio_init(IR_INPUT_PIN);
    gpio_set_dir(IR_INPUT_PIN, GPIO_IN);
    gpio_set_input_enabled(IR_INPUT_PIN, true);

    while (gpio_get(IR_INPUT_PIN) == 0) {
        sleep_us(10);
    }

    puts("Saw 1");

    return 0;
}

doesn’t work. I never see “Saw 1”. It never leaves the loop. I can press buttons on the remote control all day long, and I can see the little LED turning red (so the receiver can see the signal), but the code doesn’t see it.

Time to dig a bit deeper. I know that the Python module works.

Digging Deeper

I forget that sometimes a signal is low (that is, 1 means “no signal” and 0 means “signal”). Also, I need to avoid “floating” signals. So this seems to work, at least at a basic level:

    gpio_init(IR_INPUT_PIN);
    gpio_pull_up(IR_INPUT_PIN);

    while (gpio_get(IR_INPUT_PIN)) {
        sleep_us(10);
    }

    puts("saw 0");

It waits until I press something on the remote control, then exists with “Saw 0”.

So now the questions is whether everything is reversed. Let’s see how long the first pulse is (should be 9ms).

And the answer is:

Pulse duration 8984us

Looking positive!

Implementing the State Machine

Well, that was a good idea at the time, but the timing were so off that I ended up just having a busy loop reading the GPIO and storing the durations of pulses. It’s possible to be more accurate using PIO but I won’t go there (yet) - it’s an overkill for what I’m trying to do.

The result is encouraging. I saw the initial pulse of 9ms, then 4.5ms pause, then the bits started coming in. Bit 0 is indeed 562.5us pulse followed by 562.5us pause. Bit 1 is indeed 562.5us pulse followed by 1687.5us pause. I even saw the last pulse after the 32 bits. So the description I found was pretty accurate!

I was able to confirm that Vol+ is indeed 5D05C03F.

Conclusion

Every blog these days has a conclusion, so this one will too.

And the conclusion is that:

  1. The hardware works
  2. My remote control uses extended NEC encoding
  3. Turning the soundbar on and off is 5D0532CD