Cloning an infrared remote controller

Context :

One of my earliest paychecks went into a cheap 22" TV. It's not fancy at all, but it was a nice upgrade from the old Sony Trinitron TV that was starting to show its age. The new TV has a brand name of "Vision", which is less glamorous than "Sony", but it has been working well so far, so I can't really complain. Just the other day, I found out that it is possible to get it to record something while it's on standby. It's a feature I never knew I needed until I discovered it.

A couple of months after I got it, I ran into a problem with the remote controller. Luckily it turned out to be caused by a dead battery. Even though it was trivial to fix, this minor incident planted the seed of fear in me. Since the TV doesn't have physical buttons, I concluded that it would become useless should the remote stop working for some reason or another. Because of this, I decided to make a backup of the remote in case something happens to it. To do so, I had to find out more about the infrared communication protocol that the TV understands.

Edit : I found out that the TV does have physical buttons, but they're behind the screen, which makes it horrible in terms of UX.

Typing "Vision TV infrared protocol" into google yielded no useful results. I figured that I might have to do some reverse engineering to find out about more the protocol. Maybe if I inspect the signal that the remote produces, I could deduce what commands to send to the TV. But that would be tedious and it would require an IR receiver coupled with some sort of logic analyzer. I did some more googling and discovered IRMP, an Arduino library that does exactly what I need. In order to use it, I would need an infrared receiver so that I could feed infrared signals to the Arduino. I thought about salvaging it from an old VCR, but eventually decided to just order it online. I ended up ordering the TL1838 IR sensor along with a few infrared LEDs. Shopping Maroc delivered the products ASAP, which I appreciated.

The sensor is easy enough to use. I plugged the VCC and GND pins accordingly, then attached its OUT pin to one of the Arduino's digital inputs. After installing the library in the Arduino IDE, I uploaded the AllProtocols sketch to it. It can be found in File > Examples > IRMP > AllProtocols. I fired up the serial monitor, reset the Arduino, and was immediately greeted with the following message :

23:44:11.409 -> START /tmp/arduino_modified_sketch_735654/AllProtocols.ino
23:44:11.409 -> Version 1.3.1 from Nov 14 2020
23:44:11.409 -> Ready to receive IR signals at pin 11

I pointed the remote towards the IR sensor and pressed the standby button. The device detected a NEC signal.

23:44:17.317 -> P=NEC  A=0xFE01 C=0x10

Great! I could have stopped right then and there. The IRMP library already recognizes the protocol in question, so the problem of "backing up the remote" was technically solved. However, I decided that it would be interesting to try and reproduce the protocol from scratch, especially since I had a lot of fun writing a similar tool for the Sony TV I mentioned earlier. This time though, I wanted to do it without relying on the Arduino software.

Theory :

The SB-Projects website contains documentation for many IR protocols. It came in handy it when I was working on the Sony remote clone, and I was pleasantly surprised to find out that it also explains the workings of the NEC protocol.

When a button is pressed, the remote sends a signal that includes an address and a command. I found that in my case, the address is set to 0xfe01. The commands change depending on what button was pressed. For example, 0 to 9 conveniently map to 0x0 to 0x9, POWER maps to 0x10, volume up maps to 0xe, etc. These values are sent LSB first. In order to send 0x19, which is 0b11001 in binary, we send the rightmost bit(1), shift to the right, send the rightmost bit again (0), shift to the right again, and repeat this process 8 times. The bits of 0x19 are therefore transmitted in this order : 1, 0, 0, 1, 1, 0, 0, 0

Bits are encoded like this :

  • ones are encoded as a 560us pulse followed by three 560us spaces
  • zeros are encoded as a 560us pulse followed by a 560us space

This is what the sequence "1, 0, 0, 1, 1, 0, 0, 0" looks like on GTKWave

If you zoom in on the pulses, you'll notice that they're not in a continuously high state. Instead, the LED rapidly switches between the ON and OFF states. It does this at a frequency of roughly 38 khz. Most infrared protocols operate at this frequency. The percentage at which the high state is activated (duty cycle) should be about 33% according to sbprojects, although I did get the TV to respond with a different duty cycle.

One peculiarity of the NEC protocol is that addresses and commands are sent twice. Once an address is sent, it is immediately followed by its complement. The remote basically inverts the bits of the address then sends it again. Once done, it sends the command, inverts its bits and sends it again. This serves as an error checking mechanism.

I found that in my case, the TV uses a modified version of the NEC protocol that has 16-bit long addresses and 8-bit commands. It took me a while to figure out that the first 16 bits are set to 0xfe01, and that the last 16 bits contain an 8-bit command followed by its complement.

All sessions are preceded by a start pulse of 9ms and a 4.5ms space. All sessions finish with a 560us pulse.

Implementation :

One of the main roadblocks I ran into consisted of the inability to tell what the code I wrote actually did. In the beginning, I only had a binary feedback. I'd write some code, upload it to the Arduino, point the infrared LED to the TV and hope for the best. It quickly became apparent that this was not optimal.

When I wrote a similar tool for the Sony TV, I had a Windows installation so I used Proteus. It was called Proteus ISIS then, but then the terrorist organization of the same name went and gave it a bad rep.

I had access to Proteus because I was a student, which is no longer the case. I also switched to Linux since, and although I was able to get the demo version of Proteus to run on Wine, it was not enough. I did some googling and found out about simavr. Thanks to this project, I was able to get an output from a specific pin of the microcontroller in the form of a VCD file. I used GTKWave to visualize it. That's how I took the screenshots I posted in this article.

To get the code to work with simavr, I had the following code to main.c :

#include <avr/avr_mcu_section.h>

AVR_MCU(F_CPU, "atmega328");

const struct avr_mmcu_vcd_trace_t _mytrace[]  _MMCU_ = {
    { AVR_MCU_VCD_SYMBOL("PB5"), .mask = (1 << PB5), .what = (void*)&PORTB, },
};

I also had to put sleep_cpu at the end of main() to keep simavr from running indefinitely.

And finally, I made sure to incorporate -Wl,--undefined=_mmcu,--section-start=.mmcu=0x910000 in the avr-gcc invocation. After compiling the code, I run simavr -m atmega328p -f 16000000 -t main.elf to get a gtkwave-compatile VCD file in the output.

The Arduino I'm using has an Atmega328p chip. I plugged the LED into the D13 pin, which corresponds to the atmega's PB5 pin. To switch the state of PB5, we have to switch the state of the fifth bit of the PORTB register. To set it to 1, all we have to do is OR PORTB with 00100000, or 1 left-shifted 5 times :

PORTB |= (1 << PB5)

The other bits will keep their values because ORing them with 0 has no effect. To disable the PB5 bit however, we will have to AND it with 0 and AND the other bits with 1, so that their value remains intact. In other words, PORTB has to be ANDed with 11011111. This happens to be the complement of 00100000 (1 << PB5) :

PORTB &= ~(1 << PB5)

If you made it this far , congratulations, you can now consider yourself an embedded software engineer

Remember when I said that bytes should be sent LSB first ? It can be done with a for loop :

void send_byte(int pin, uint8_t value)
{
    for(int i = 0; i < 8; i++)
    {
        if(value & 1)
        {
            mark(pin);
        }
        else
        {
            space(pin);
        }
        value >>= 1;
    }
}

What about mark and space ? According to the theory section, a binary 1 is a 560 microsecond pulse followed by a 1680 space. A zero is a 560 microsecond pulse followed by a 560 microsecond space :

void mark(int pin)
{
    modulate(pin, 560);
    _delay_us(1680);
}

void space(int pin)
{
    modulate(pin, 560);
    _delay_us(560);
}

Now to the trickiest part : modulate. Its responsibility is to switch the PB5 pin on and off at a frequency of 38 khz for a given duration. Each on and off switch has to take place in 1/38000 seconds, which roughly corresponds to 26 microseconds. As such, it should perform this operation duration / 26 times.

void modulate(int pin, int microseconds)
{
    for(int i = 0; i < microseconds / 26; i++)
    {
        PORTB |= (1 << pin);
        _delay_us(13);
        PORTB &= ~(1 << pin);
        _delay_us(13);
    }
}

I kept changing the duty cycle until I got satisfying results, so I left it at 50%. These timings are not accurate because the code doesn't take into consideration the time it takes to switch the state of the PB5 pin. Despite this, it still gives acceptable results. To test this, I hardcoded a set of commands that would allow me to play a file from USB if I point the infrared at the TV and reset the Arduino. The video below demonstrates this. I apologize in advance for the abysmal quality :

The full code is available on Github, but here's the relevant portion :

int main(void)
{
    send_message(PB5, ADDRESS, POWER);
    _delay_ms(8000);
    send_message(PB5, ADDRESS, SOURCE);
    _delay_ms(2000);
    send_message(PB5, ADDRESS, UP);
    _delay_ms(2000);
    send_message(PB5, ADDRESS, OK);
    _delay_ms(1000);
    send_message(PB5, ADDRESS, OK);
    _delay_ms(1000);
    send_message(PB5, ADDRESS, DOWN);
    _delay_ms(1000);
    send_message(PB5, ADDRESS, OK);
    _delay_ms(2000);
    send_message(PB5, ADDRESS, UP);
    _delay_ms(2000);
    send_message(PB5, ADDRESS, OK);

    sleep_cpu();

    return 0;
}

Next steps

Even though the project is incomplete, I had to stop here and push out an article because it has been brewing in my drafts for a very long time. I'll come back to it some time in the future to add a second part.

While the code above works, it isn't very optimal. The PWM generation is done in software even though the MCU has dedicated hardware for that. I guess you could say that the code isn't hardware accelerated, so that's one obvious area of improvement.

I still have no way of controlling the TV in real time. One possible way to do this would be to have a computer communicate the buttons to the MCU through the USB port, thus controlling the TV from the computer in question. I'm still figuring out how to interface with USART on the atmega328p through the Arduino's USB port, so I'll make sure to add a second part to this article when it's done.

Edit of January 2024: I did it on an Android phone

Credits

Commentaires

Posts les plus consultés de ce blog

Writing a fast(er) youtube downloader

My experience with Win by Inwi

Porting a Golang and Rust CLI tool to D