Learning IR codes

Building a reusable IR remote code learner and replayer on STM32

Public Chat
Similar projects worth following
This project aims to deliver an STM32 firmware that can record infrared codes from remote controllers and send the Pronto HEX encoded string over UART, and can emit infrared codes from received strings. To keep costs minimal, I will be using the simplest Nucleo-32 kit, with an infrared LED and receiver (plus the necessary passives). Reusability comes from easy hardware assembly and easy firmware porting. Therefore I will ignore my usual urges to drop ST's HAL libraries, and rely on them instead.

The motivation comes from being unable to use the infrared capability of my Samsung Galaxy S5 phone, as there's no app that knows the infrared codes for my TV, set-top-box or soundbar. There are however apps that take Pronto HEX codes, e.g. Universal TV Remote (org.twinone.irremote), so all I would need is my own IR code learner.

ST has its own app note on how to implement IR RC functionality:

However that document deals with specific protocols, while raw Pronto HEX is a universal format that can describe any modulated IR code, which is why it's so widely supported for IR learning. There is a nice document to understand how raw Pronto HEX codes are interpreted:

Even though I only need an IR receiver, I will be implementing a transmitter as well. The transmitter's implementation seems simpler - it's not learning a foreign code with unknown frequency, just switching PWM on-off - I can use the transmitter to create a Hardware-in-the-Loop test by shorting the MCU's transmitter and receiver pins.

Steps to completion:

  1. Implement IR transmitter that can process raw Pronto HEX codes, verify with logic analyzer
  2. Implement IR receiver that can receive the looped signal near identically (an error in detected carrier frequency is tolerable)
  3. Implement Pronto HEX string conversion, and bidirectional UART transfers
  4. Install real IR sensor to the MCU, verify that it has output with logic analyzer
  5. Emit a simple Pronto code from smartphone, check learning on MCU
  6. Learn a real RC code, replay the code from smartphone / MCU

  • 1 × NUCLEO-L432KC Cheap and compact STM32 dev board
  • 1 × TSMP58000 Opto and Fiber Optic Semiconductors and ICs / Other Optoelectronic ICs

  • Setting up the smartphone app

    benedekkupper11/27/2021 at 20:38 0 comments

    Finally I have stumbled upon Advanced settings on my Samsung Galaxy S5 mini (LineageOS), where one entry is for USB-OTG mode, where the powered option is not default as it defeats the phone's supposed waterproof capability. Once I flipped that switch from Unpowered to Powered, the connected ST board's LEDs lit up. Now I could open Serial USB Terminal (in CDC mode), and receive the serial data from my MCU. (This Nucleo board's programming chip has a CDC interface that connects to a UART of the target MCU.)

    The next step is to press the interesting keys on the TV's and soundbar's remotess and copy the received text into the Universal TV Remote app (see photo). The app is fully functional, and nicely customizable. I do need to scroll the page to get access to all the keys, but I can arrange the keys in a way that I have all keys on the same area that I use in a given situation.

    One thing I was curious about, how the app handles repetitions, so when the user is long pressing the key. The Pronto raw format allows specifying the repeating part of the IR code, so I modified the original captured code to include the repeating part in a single code, and added it to the app that way. The app however only emits the once and repeat parts one time, and then repeats this whole signal again, with a repeat delay that is configurable in settings.

  • When it all comes together

    benedekkupper11/24/2021 at 21:18 0 comments

    I banged out the missing piece of the firmware, namely to send the received Pronto codes over UART, and the opposite direction as well. This part of the code is not meant to be optimal or well-structured, just something to get things working.

    I first tested this still in loopback mode, and it works fine. So it was time to get dirty with some real-life infrared signals.

    There's a myriad of IR receivers available, I went with Vishay's TSMP58000, which they recommend for IR learning applications - perfect for me, I thought. I removed the loopback between the STM32's I/Os, and hooked up the TSMP58000 on the breadboard without any additional components.

    I have configured a known Pronto code in the IR Remote app, and pressed the new button. The IR receiver has picked up the signal, as the logic analyzer confirmed it. The STM32's firmware rejected the signal though. As I have seen on the logic analyzer's capture, the ON durations are significantly less than 50% of the period. The TSMP58000 recommends some passive components to improve the signal quality, but they aren't addressing this particular problem. I have instead increased the tolerance value in the firmware. After that, I have seen the same code on the terminal, as was entered in the app. Success! (To be fair, there are some expected differences, e.g. the once count shows the number of burst pairs instead of the repeat count, the last off duration is different.)

    Now that the firmware is ready for action, I have connected it to my Galaxy S5 mini, so I can learn the IR codes of the remotes in the house, and directly save it into the IR Remote app. That's when I got really disappointed: the power LEDs are off on the STM32 kit, meaning that the phone's USB OTG host mode is broken. After some searching it seems that this is a known issue in LineageOS :(

    I will have to connect it to my laptop and transfer the Pronto codes to my phone after learning the remote codes.

    It would be great to share these device codes, but such projects (e.g. don't take raw Pronto HEX codes, only codes with known protocols. Maybe IrScrutinizer could be used to find out the protocol? At the moment I'm not that motivated to dig deeper into this though.

  • Receive logic working

    benedekkupper11/23/2021 at 22:39 0 comments

    Getting the receive logic to work was a lot tougher than the transmit one. For one, we don't know the carrier frequency of the incoming signal. Furthermore, receiving one IR code requires a lot more processing with PWM input mode, than the transmission, as each falling-rising edge pair needs to be processed individually.

    The TIM's PWM input mode in general is rather well documented, but here's a quick summary: capture channels 1 and 2 work in tandem, one is the direct channel, which has the actual input pin connected, and this channel resets the TIM's counter on the capture event. The other is the indirect channel, used with opposite capture polarity. The direct channel's capture register will reflect the timer periods for the signal's period, the indirect channel's capture register will reflect the the timer periods for the signal's active phase.

    In order to decouple the necessary signal processing from the PWM reception itself, DMA is used to load a sequence of captured PWM on-off timing pairs into memory. The DMA is run in circular mode, and half of the buffer is processed when the half transfer or transfer complete flags are raised. The used TIM peripheral has burst DMA support, which means that one DMA transfer request can result in multiple successive registers being transferred. I use this mode to transfer both CC1 and CC2 capture register values each time a capture event occurs. If the capture event's timing is used to transfer both registers, then the other channel's value will reflect the previous on/off duration. Therefore I had to make use of the CC DMA Selection (CCDS) bit, which in update mode will delay the DMA trigger until the timer is updated.

    - It's nice to find that the TIM design is so flexible, but unfortunately I have to use a mix of HAL and LL drivers to get both the convenience of linking DMAs to peripherals, and the extensive API that lets me control any necessary bit, like this CCDS. -

    So we can capture a lot of on-off pairs, but how do we know that an infrared code has completed? There's really only one way, by seeing no more on pulses until a timeout. Since we are using a timer already, can we use the same one to check for this timeout? Yes we can :) The timer is updated (reset) at each direct channel capture event, so we cannot directly rely on the update event for timeout detection. Fortunately there's an Update Request Source (URS) bit, when it's active, only the timer's counter overflow triggers the update interrupt. So we can just set a long reload value, the URS bit and enable the update interrupt request of the timer, and we automatically get interrupted when the infrared code has been completed, without having to monitor the timeout separately.

    The signal identification itself consists of two parts:

    1. When the first chunk of on-off pairs are received, the carrier frequency is estimated first. This is a balancing act between getting more accuracy by using larger buffers, or saving on RAM.

    2. Once the carrier frequency is known, each on-off pair is counted as one modulated ON bit, and zero or more OFF bits.

    When the timeout is reached, the timeout duration itself should - at least partially - be counted as terminating OFF bits.

    The local loopback test has shown that the final implementation is correct, I only get 1 offset in the pronto code of the measured carrier frequency compared to the test signal (and of course different count for the last OFF symbol), which is a great result in my opinion. I'm looking forward to testing this code with some actual hardware, fingers crossed :)

  • Transmit logic working

    benedekkupper11/14/2021 at 22:32 0 comments

    I got the infrared transmit logic working well now, so let me document how I got here:

    There are two symbols that an IR transmitter needs to send: modulated ON (half of the carrier frequency period ON, half OFF), and OFF. The raw Pronto format contains just that, a list of burst pairs. A burst pair tells how many modulated ON symbols are followed by how many OFF symbols.

    ST's application note tells us that we need to combine two specific TIM peripherals to get a modulated signal, by one timer providing a carrier frequency, and the other the bit signals (the symbol count). Unfortunately the hardware of my choice, NUCLEO-L432KC doesn't have that IR_OUT pin on the 32-pin STM32, so I had to look for another solution. When I closer examined the problem, I realized that ST's solution is over-engineered, and the same task can be achieved with a single timer, using its repetition counter.

    A burst pair can be transmitted like this:

    1. First the modulated ON symbol count is written into the repetition counter, and the compare register is set to half the reload register, so we will get symbol count times 50% PWM output.
    2. As the timer's update event fires, the OFF symbol count is written to the repetition counter, and the compare register is set to 0, so we will get symbol count times no output (off).

    The only problem here is that the repetition counter register is only 8 bits wide, and the Pronto format allows symbol counts on 16 bits. In practice however there is no IR protocol that would repeat the same symbol for more than 256 times. The only case of a symbol count exceeding this boundary is for the OFF symbol of the very last burst pair, in order to space out independent IR codes. At the moment I'm just limiting the last symbol count to 256, if necessary the timer's basic parameters could be reprogrammed to strictly adhere to the spacing.

    Let's talk about efficiency a bit. This approach with the single timer only requires twice as many interrupt servicing as using the two timer scheme. The modified registers are configured to be preloaded, which means that the ISR has at least one carrier frequency period time to be executed, which can be appreciated if this module were integrated into a more complex design. DMA acceleration is a possibility, with a 2-register burst DMA configuration (assuming output channel 1), but that comes with a trade-off for more RAM.

    I have uploaded pages of my setup and the output waveform that is captured with my logic analyzer. As I only need the transmit function to simulate IR codes for my receiver implementation, the output is active low. Conveniently I was able to map a suitable timer's output pin right next to another timer's capturing input, so all I need is a jumper to create a hardware in the loop schenario. The analyzer is hooked up to the jumper so I can cross-check the actual waveform.

View all 4 project logs

Enjoy this project?



Similar Projects

Does this project spark your interest?

Become a member to follow this project and never miss any updates