How to read codes from (many) IR remote controls with as little code as possible
Background
Inexpensive infrared (IR) remote controls have been ubiquitous in our lives for many many years. This is because the technology is inexpensive and works so well that it fades into the background. The IR signal gets 'sprayed' (transmitted/reflected/received) so effectively in most indoor environments that you rarely have to care in which direction the LED is pointing and it somehow 'just works'. The clever inventors of the technology discovered that by modulating the digital data on a 36 to 40KHz carrier wave, it would have a high signal to noise ratio (be detectable with a relatively weak transmitter in very noisy conditions). The wavelength of IR chosen (940nm) was useful because it is just beyond visible light, but not too far beyond the properties of light that it still reflects well off of ordinary surfaces. The reason for this article, and the software project I created, is to harness these ubiquitous controls to restore functionality to my MacBook that Apple took away several generations ago - to use an IR remote control to drive Mac media playback (e.g. watch a movie with the Mac connected to your TV) from the comfort of your sofa. After seeing the complicated (and sometimes expensive) projects and products to accomplish this, I decided to learn some of the gritty details and create it for myself. As an eternal optimizer, I also wanted to see the minimum amount of code needed to accomplish the task.
The Hardware
Vishay Semiconductors seems to be the "go-to" source of IR receiver hardware these days. They sell several low cost IR demodulators (e.g. TSOP1836) that are very efficient at detecting, amplifying and demodulating the digital signal from IR remote controls. The carrier frequency is stripped off and the output becomes an active-low (open collector) signal which is active when the carrier frequency is present. These modern demodulators are extremely power efficient too; a few microamps is all that is needed to continuously receive the data. I'm not going to cover the transmission side of IR remote control codes for this article. The job of modulating output signals on a 38KHz carrier is usually left for the MCU/CPU to do. IR transmit hardware normally consists of one or more IR LEDs and little else.
I was able to buy 10 pairs of VS1838B receivers plus 5mm IR LEDs for about $5. These are what I'm using for my project(s). Since this receiver can coexist on an I2C bus (it has open collector output), I made it easy to use with some of my IoT devices which have a QWIIC socket.
Existing Software
A quick search of existing blogs/info about this topic generally provides very little technical info and just links to the IRremote library for Arduino or the LIRC package for Linux. Both of these are mature, complete solutions with a lot of history. That's all well and good, but using other people's libraries is usually not interesting to me; I need to re-invent every wheel. I'm working with both Nordic and Espressif microcontrollers, so I started by looking at the IRremote library. The sheer bulk of the code and quantity of #ifdef's discouraged me from exploring that code too deeply. I decided instead to just look at the details of the protocol and write my own code from a blank slate.
The Protocol Standard(s)
There have been many IR control schemes over the years, but the two most prevalent were created by Philips and NEC. From a brief sampling of the remote controls in my house, NEC protocol is the current winner of this battle. Since I only see NEC codes coming out of my remotes, my project goal is to write software to receive these codes.
This is the image I used to figure out how to decode these signals:
The image came from this article: https://www.histo.cat/e/IR-NEC-Remote-Pic-Source-GitHub
(The solid red bars are actually a 50% duty cycle 36-40KHz square wave)
The article goes on to show how the 0's and 1's are encoded with PDM (pulse distance encoding):
- Logical '0' – a 562.5µs mark followed by a 562.5µs space, with a total transmit time of 1.125ms
- Logical '1' – a 562.5µs mark followed by a 1687.5µs space, with a total transmit time of 2.25ms
The protocol looks simple enough. After the start phase, there are 32 data bits which encode an 8-bit address, followed by 8 bits of the same value inverted, then an 8-bit command followed by its inverted value. This is a clever way to encode 16 bits in a fixed amount of time (it's guaranteed that there are an equal number of 1s and 0s) and have the data be self-verifying. I saw different values of 'address' on my various remote controls, so for my projects I combined the address and command values into a unique 16-bit value which I use to identify the buttons.
My initial decoding concept: once I see the initial falling edge from the demodulator (active signal at the start phase of each code), measure that 'mark' period and the space period after it. If it checks out, then capture the next series of transitions as 32 bits and voilá.
All of the timing is based on a multiple of the 562.5us minimum period, so as long as I can count the time in terms of multiples of this period, I should be able to decode the signal.
My First Attempts
The Arduino API includes a function called micros() which returns the number of microseconds elapsed since the power was turned on. The problem with this function is that it's implemented very differently on different microcontrollers and the resolution of the counter can vary quite a bit. I decided to use the Seeed Xiao nRF52840 1.47" Touch LCD board as the basis of the project since it has support for native USB HID keyboard output and two pushbuttons and a capacitive touch layer for user input (photo below):
This board has a 'Xiao nRF52840 Plus' board underneath - this is a 64MHz Cortex-M4 MCU with plenty of speed and memory to handle the job. I soldered the IR demodulator onto the header pins directly since this board doesn't expose a QWIIC socket (see photo). My first try at capturing codes on the nRF52 was getting unreliable results - every button press from my chosen remote control resulted in a random or invalid value. I looked through the Arduino source code for the nRF52 and saw that the micros() function defaulted to using some interrupt-driven counter that had a low resolution. The nRF52840 has a hardware counter register that increments at the rate of the CPU clock. Its use is optional and requires that you enable it explicitly. This is the code I found in the cores/nRF5/delay.h file:
static inline uint32_t micros( void ) __attribute__((always_inline));
static inline uint32_t micros( void )
{
// Use DWT cycle count if it is enabled, otherwise use rtos tick
return dwt_enabled() ? (DWT->CYCCNT / 64) : tick2us(xTaskGetTickCount());
}
- It's not necessary to check for timeouts while waiting for a 'mark' to finish; the IR signal can't continue forever.
The Project
HID_KEY_VOLUME_DOWN
HID_KEY_MUTE
HID_KEY_SPACE (used to toggle play/pause on most media players)
HID_KEY_C (used to toggle closed captions)
HID_KEY_ARROW_RIGHT + KEYBOARD_MODIFIER_LEFTSHIFT
HID_KEY_ARROW_LEFT + KEYBOARD_MODIFIER_LEFTSHIFT



Comments
Post a Comment