Sunday, August 28, 2022

IR remote control library for AVR microcontrollers

A remote controller is a device that... You probably have one near you so I'll skip the introduction. This library can be used for sending or receiving remote controller codes. The supported protocols at the moment depends on what remote controllers I had in the house and all 4 of them used NEC and RC-5.

The IR library is very easy to use and it needs a 16-bit timer for both sending and receiving. I have chose Timer 1 for this purpose. Before diving into the code, let's see how a remote controller works and take a closer look at their protocols.




How a remote controller works

A remote control is using optical communication to send wireless data to a receiver device. For this purpose, the infrared light was chosen. The data is transmitted on top of a carrier frequency that is usually 38kHz. There are many schemes of encoding the data because there are many manufacturers of consumer products. The most commonly used protocols are NEC, RC-5, RC-6, Sony. 

Infrared light

As the name suggests the infrared light is lower in frequency than the visible light and because is not visible to humans is a good choice for IR remote controllers. Another important reason for using infrared leds in remote controllers is the low forward voltage needed for the led, which is around 1.2V. Even with two batteries discharged to 0.7 volts the led can still work provided there's enough current.

Although we can't see it the infrared light, a phone camera can make it visible. Point the remote to the camera and press any button on the remote and on the phone screen you should see the infrared led blink. This is a quick way to check if the remote works.

There are many sources of infrared light that can interfere with a remote controller, such as the sun, light bulbs and everything else that generates heat. That's how a PIR sensor can detect a warm body.

The wavelength used is between 930-950nm and this is preferred because water in atmosphere blocks sunlight in this wavelength, making devices less susceptible to sunlight interference.

Now you might wonder how a receiver distinguish a remote controller from so many sources of infrared radiation. What about if a tree branch swings in front of the sun. Will that change my TV channel? LOL no. To distinguish the remote control from all this sources of noise, the following techniques are used.


The data to be transmitted is modulated on a carrier signal that is usually 38kHz but frequencies between 30 and 60kHz are also used, depending on the manufacturer. Using a different frequency for the carrier signal helps in avoiding conflicts between devices.

IR remote control modulation
Figure 1

In Figure 1 is an example on how modulation works. On first channel is the output from the IR receiver and on second channel is the output from the IR transmitter that is using a carrier frequency of 38kHz and 30% duty cycle. The reasons for the lower duty cycle will be discussed later. The idle state of the IR receiver is high and it goes low only if the carrier signal is detected. So to send a low pulse, the transmitter enables the carrier signal and to send a high pulse, the carrier is disabled and since the receiver won't detect the signal it will drive it's output high.

Note that a low pulse doesn't indicate a bit 0 nor a high pulse bit 1. Most IR protocols are using a low and a high pulse to form a bit, but more on that later.

The transmitter

The transmitter usually is a battery powered handset and so it should consume as little power as possible. Nowadays a microcontroller in sleep mode can consume very little power and it only needs to wake-up only when a button is pressed.

The current through the infrared led can vary from 100mA to 1A, and that is because the power is not applied continuously and the duty cycle of the carrier signal is low (10% to 30%). Average power dissipation within the led should not exceed their maximum value though. You should also see that the maximum peek current for the led is not exceeded.

The lower duty cycle has the benefit of saving battery power and also allows a higher current for the led that produces more light and thus increasing the range of the remote control.

IR led transmitter circuit

The infrared led could be controlled directly from a microcontroller pin but with only 20mA that a pin can safely supply the coverage distance won't be that great. A better way is to use a NPN transistor. Normally you would place the led and a resistor on the collector side and that will work fine if the voltage would be constant. But when using batteries the voltage will gradually drop and so the current through the led since the resistance will remain constant. For this reason a constant current led driver should be used.

IR led transmitter circuit
Figure 2

The 2 diodes in series will clamp the voltage on the base of the transistor to 1.2V and R2 limits the current through these diodes.

R1 sets the led current R1 = Ve / Iled. The voltage across R1 (Ve) is Ve = Vb - Vbe. In the above LT Spice simulation the Vbe was 0.8V and Vb is 1V. So Ve = 1 - 0.8 = 0.2V. Now to set the current to 0.1A (100mA) R1 = 0.2V (Ve) / 0.1A = 2 ohms.

Q: How much current should I drive the led with?

A: Not sure. I guess it depends on the led but in my setup I have a cheap led from China and the maximum current was specified to be 20mA but I think that is for DC and in the case of a remote the led is on for very short pulses. I am using it with 100mA and it still works just fine.

If the voltage is constant (not battery powered), just remove the 2 diodes and move R1 near the led. Then to calculate R1 the formula is like for any other led: (Vcc - Vled) / Iled. The forward voltage (Vled) of an infrared led is typically 1.2V so for a 5V power supply and 100mA led current the resistor R1 = (5 - 1.2) / 0.1 = 38 ohms.

Infrared LED
Infrared LED

The receiver

The main criteria when selecting an infrared receiver is the carrier frequency that usually is 38kHz. It's job is simple - when it detects infrared light pulsed at a specific frequency, it drives it's output low. That sounds simple but the inside circuitry is a bit complex.

IR receiver block diagram - PNA4601M
Figure 3

The above block diagram is from the PNA4601M datasheet. The received IR signal is picked up by the IR detection diode on the left side of the diagram. This signal is amplified and limited by the first 2 stages. The limiter acts as an AGC circuit to get a constant pulse level, regardless of the distance to the handset. The AC signal is sent to the Band Pass Filter which is tuned to a specific carrier frequency. The next stages are a detector, integrator and comparator. The purpose of these three blocks is to detect the presence of the carrier frequency. If this carrier frequency is present the output of the comparator will be pulled low. Because the output pin has a pull-up resistor, the output will remain at VCC when no signal is detected.

IR receiver IC
Infrared receiver module

In the above image is shown a typical infrared receiver IC. Some have a metal case. I recommend checking it's datasheet because some IC's have ground and power pins reversed.

IR receiver wiring - TSOP382
From TSOP38238  datasheet

Many datasheets recommend placing a capacitor across the power pins and a resistor in series with VCC before the capacitor in order to protect against Electrical Overstress (EOS) and for filtering. I am using 100nF for the decoupling capacitor and 100 ohm resistor with great results.

IR receiver response range - PNA4601M
From the PNA4601M datasheet

As you can see in the above datasheet graphs, the peak frequency detection is at 38 KHz and the peak light wavelength is 940 nm. The carrier frequency can vary from 35 KHz to 41 KHz but the sensitivity will drop off so that it wont detect as well from afar. Likewise, you can use 850 to 1100 nm LEDs but they wont work as well as 900 to 1000nm so make sure to get matching LEDs.

NEC protocol

This is a very popular protocol and easier to decode in software.

The NEC protocol uses pulse distance encoding of the bits. Each pulse is a 560µs long taking 21 cycles on a 38kHz carrier.

Logical '0' – a 562.5µs pulse burst followed by a 562.5µs space, with a total transmit time of 1.125ms.

Logical '1' – a 562.5µs pulse burst followed by a 1.6875ms space, with a total transmit time of 2.25ms.

NEC protocol - Modulation


The NEC protocol consists of the following:

  • A 9ms leading pulse burst
  • A 4.5ms space
  • The 8-bit address for the receiving device
  • The 8-bit logical inverse of the address
  • The 8-bit command
  • The 8-bit logical inverse of the command
  • A final 562.5µs pulse burst to signify the end of message transmission

The address and command are inverted and transmitted twice for reliability. The four bytes of data bits are each sent least significant bit first.

NEC protocol

The above figure illustrates the format of an NEC IR transmission frame, for an address of 59h and a command of 16h. 

A command is transmitted only once, even when the key on the remote control remains pressed. Every 110ms a repeat code is transmitted for as long as the key remains down.

NEC repeat sequence

NEC repeat code

The repeat code is simply a 9ms AGC pulse followed by a 2.25ms space and a 560µs burst.

Extended NEC protocol

By sacrificing the address redundancy the address range was extended from 256 possible values to approximately 65000 different values. This way the address range was extended from 8 bits to 16 bits without changing any other property of the protocol. The command redundancy is still preserved. Therefore each address can still handle 256 different commands.

NEC extended protocol
Example message frame using the Extended NEC IR transmission protocol

Whenever the low byte address is the exact inverse of the high byte it is not a valid extended address.

Extended NEC protocol in Pulseview
Extended NEC protocol in Pulseview

Philips RC-5 protocol

The protocol uses bi-phase modulation (or so-called Manchester coding) of a 36kHz IR carrier frequency although it works well with 38kHz carrier too. Each pulse burst is 889us in length.

Logical '0' – 889us pulse burst followed by 889us space, with a total transmission time of 1.778ms.

Logical '1' – 889us space followed by 889us pulse burst, with a total transmission time of 1.778ms.

RC-5 protocol modulation

The pulse/pause ratio of the carrier frequency is 1/3 or 1/4, which reduces power consumption.


Format of a Philips RC5 IR transmission frame, for an address of 05h (00101b) and a command of 35h (110101b).

RC-5 protocol

The message frame transmitted consists of the following 14 bits:

  • two Start bits (S1 and S2), both logical '1'
  • a Toggle bit (T). This bit is inverted each time a key is released and pressed again
  • the 5-bit address for the receiving device
  • the 6-bit command

The address and command bits are each sent most significant bit first.

The Toggle bit (T) is used by the receiver to distinguish between two successive button presses (such as "1", "1" for "11") as opposed to the user simply holding down the button and the repeating commands being interrupted by a person walking by, for example. As long as the key on the remote controller is kept depressed, the message frame will be repeated every 114ms. The Toggle bit will retain the same logic level during all of these repeated message frames. It is up to the receiver software to interpret this auto-repeat feature of the protocol.

Extended RC-5 protocol

Extended RC-5 uses only one start bit. Bit S2 is re-assigned to the most significant bit in the command, providing for a total of 7 command bits. The value of S2 must be inverted. So if the MSB in the Command is 1 the S2 bit must be 0 and if the MSB in the Command is 0 then the S2 will be 1 which means that it will no longer be an extended protocol. That way the first 64 commands remain compatible with the original RC-5 protocol.

RC-5 protocol in Pulseview
RC-5 protocol in Pulseview

The IR Remote library

The library uses 2k of flash memory and 18 bytes of SRAM. It uses one 16-bit timer for both the receiving and transmitting. Developed using ATmega328PB.

The IR receiving is done using an ICP (Input Capture Pin) that triggers an interrupt on every pulse received. An ICP has a timestamp feature that means when a pulse is received, the hardware saves the timer value in a buffer that can be used at a latter time. A regular interrupt pin doesn't have this feature which makes the time measuring not so precise, since if an interrupt is served while receiving a pulse and if that interrupt takes tens of microseconds to execute, the time measurement will be affected.

The pulses are converted to bits inside the ICP interrupt and saved into a 32-bit variable, thus saving SRAM - no buffer array is needed. The decoding is done by a separate function after all bits are received. When the decoding is done, the main application is informed and then the address and command can be obtained.

During IR transmission the same timer is configured to output a PWM carrier signal with 30% duty cycle on an OCR pin but the modulation is done using delays that interrupt the PWM signal.

Code example

UART functions are optional and are commented out since they are used only for debugging purposes.

#define F_CPU	16000000

#include <avr/io.h>
#include "IRremote.h"

int main(void){
    uint16_t address = 0;
    uint16_t command = 0;
    uint8_t toggle_bit = 0;
	    //UART_sendString("--- Toggle bit: ");
	    //UART_sendString("Protocol: ");
	        //UART_sendString("Repeat code \n");
		IR_getCode(&address, &command);
		//UART_sendString("Received code: ");

Defining the pins

The Input Capture Pin and the pin for transmitting the signal are defined like this:

// Input Capture Pin (must be an ICP)
#define ICP_DDR			DDRB
#define ICP_PIN			PB0

// OCRA pin used for transmitting

Initialization function

Sets the Input Capture Pin, the timer and enables the necessary interrupts.

void IR_init(void)


Check if new code is available

Returns true when new code is available. The decoding is also done here.

bool IR_codeAvailable(void)


Get decoded data

When IR_codeAvailable() returns true, this function can be used to obtain the decoded address and command.

void IR_getCode()


uint16_t* address, command

Pointers to variables where to store the received address and command.


uint16_t address = 0;
uint16_t command = 0;
IR_getCode(&address, &command);


Get toggle bit

Returns the toggle bit: 0 or 1. Only for protocols that have this feature.

uint8_t IR_getToggleBit(void)


Is a repeat code

Returns true if a repeat code is received. Only available for NEC protocol.

bool IR_isRepeatCode(void)


Get protocol type

Returns the protocol type defined by the following macros:


uint8_t IR_getProtocol(void)

Disable receiver

Disable the Timer and associated interrupts.

void IR_disable(void)

Send code

Encodes and sends the data with the specified protocol.

void IR_sendCode()


uint16_t address

Address for the receiving device

uint16_t command

The command

uint8_t toggle

The toggle bit, 0 or 1. Only for protocols that have this feature. Set to 0 if not used. For NEC protocol, setting this to 1 will send a repeat code.

uint8_t protocol

One of the defined protocols. See IR_getProtocol().


IR_sendCode(address, command, toggle_bit, IR_PROTOCOL_RC5_EXTENDED);
toggle_bit = ~toggle_bit;

This function is usually used with a button library and when a button is pressed the function is executed. Each time the button is pressed the toggle bit is...well toggled between 0 and 1. If the protocol is NEC and the button is pressed for more than 150ms, the toggle bit could be set to 1 and so a repeat code can be sent every 150ms.

Here is an example of how this is implemented using one of my button reading library.

#define F_CPU	16000000

#include <avr/io.h>
#include "IRremote.h"
#include "buttonDebouncer.h"
#include "millis.h"


enum Buttons{
	BTN_UP 		= 1, // PB2

int main(void){
    buttons_t pressed_buttons = 0;
    buttons_t pushed_buttons = 0;
    uint16_t address = 0;
    uint16_t command = 0;
    uint8_t toggle_bit = 0;
    bool btn_pressed = false;
        // Check if debouncing is complete
	    pressed_buttons = debouncerPressedButtons();
	    pushed_buttons = debouncerPushedButtons();
		if(btn_pressed == false){
		    IR_sendCode(address, command, toggle_bit, IR_PROTOCOL_RC5_EXTENDED);
		    btn_pressed = true;
	    // If a button was long pressed
		// Check which button
	        IR_sendCode(address, command, toggle_bit, IR_PROTOCOL_RC5_EXTENDED);
	    // Check what button or combination of buttons were pressed
		case BTN_UP:
		    btn_pressed = false;
		    toggle_bit = ~toggle_bit;

If you found this useful, don't forget to share and subscribe.


Version 1.0

No comments:

Post a Comment