To decode infrared (IR) remote signals with a microcontroller, you need a 38kHz demodulating receiver (like the KY-022 module or bare TSOP38238 chip), a digital input pin, and the IRremote v4.x library. While hundreds of tutorials exist, most rely on outdated v2/v3 library syntax that throws compilation errors on modern IDEs, or they ignore the hardware filtering required to stop ambient light from drowning out your signal.

This guide targets the Arduino Uno R3 (ATmega328P) and the ESP32 DevKit V1. We will cover the exact hardware specs, the Vishay-recommended RC filter circuit for bare chips, fully compilable v4 code, and a decision tree for debugging the most common IR failures.

Hardware Specs & Pin Mapping

Not all IR receivers are created equal. The bare TSOP38238 chip requires external passive components to filter power supply noise, while hobbyist modules like the KY-022 integrate these onto a PCB. Choosing the wrong variant for your environment leads to phantom triggers.

IR Receiver Module Comparison

Component / Module Carrier Freq Vcc Range Icc (Typical) Max Range Built-in RC Filter Best Use Case
KY-022 Module 38 kHz 2.7V - 5.5V ~1.0 mA 10 - 15m Yes (Onboard) Prototyping, Arduino Uno 5V logic
VS1838B Module 38 kHz 3.3V - 5.0V ~1.2 mA 8 - 12m Yes (Onboard) Budget builds, ESP32 3.3V logic
Bare TSOP38238 38 kHz 2.5V - 5.5V 0.7 mA Up to 35m* No (Requires external) Custom PCBs, high-reliability products
TSOP4838 38 kHz 2.5V - 5.5V 0.7 mA Up to 35m* No (Requires external) High-noise environments (AGC3 filter)

*Range dependent on IR LED transmitter power and ambient lighting. Data sourced from the Vishay TSOP382 Datasheet.

Microcontroller Pin Mapping

The IRremote library can use any digital pin with an interrupt, but standardizing your pinout saves debugging time. Note the logic level differences between the Uno and ESP32.

Function Arduino Uno R3 (5V Logic) ESP32 DevKit V1 (3.3V Logic) Notes
IR Receiver Data (OUT) Pin 11 GPIO 15 Any interrupt-capable digital pin works.
IR Transmitter Data (IN) Pin 3 GPIO 4 Hardcoded to Pin 3 on Uno for PWM timer in IRremote.
Vcc 5V 3.3V Do not feed 5V to an ESP32 GPIO pin.
GND GND GND Ensure common ground with transmitter circuit.

Wiring the Arduino IR Circuit

If you are using a pre-assembled module like the KY-022, wiring is trivial: Vcc to 5V, GND to GND, and OUT to Pin 11. However, if you are using a bare TSOP38238 chip on a breadboard or custom PCB, you must include the decoupling network. Without it, switching noise from your microcontroller or nearby power supplies will trigger phantom IR decodes.

⚠️ Callout: Bare Chip RC Filter Requirement
According to Vishay's application notes, a bare TSOP38238 requires a 100Ω resistor in series with the Vcc line, and a 4.7µF electrolytic capacitor placed in parallel across the Vcc and GND pins (after the resistor). This suppresses power line ripple and prevents the internal AGC (Automatic Gain Control) from falsely triggering on continuous noise.

Step-by-Step Wiring (KY-022 Module to Uno R3)

  1. Power Down: Disconnect the USB cable from your Arduino Uno R3.
  2. Ground Connection: Connect a 22 AWG black jumper wire from the GND pin on the KY-022 to any GND pin on the Uno.
  3. Power Connection: Connect a 22 AWG red jumper wire from the VCC (or +) pin on the KY-022 to the 5V pin on the Uno.
  4. Signal Connection: Connect a 22 AWG yellow jumper wire from the OUT (or S) pin on the KY-022 to Digital Pin 11 on the Uno.
  5. Verify: Use a multimeter in continuity mode to ensure there are no shorts between Vcc and GND before applying power.

Compilable Code with Error Handling

The code below targets the Arduino Uno R3 and uses the modern IRremote v4.x API. Older tutorials use IRrecv irrecv(PIN), which will fail to compile on v4.x. This script includes robust error handling for unknown protocols and repeat codes.

Prerequisite: Install the "IRremote" library by shirriff/z3t0 via the Arduino Library Manager. Ensure you select version 4.0.0 or higher.

#include <IRremote.hpp>

// --- PIN DEFINITIONS ---
const int IR_RECV_PIN = 11;

// --- GLOBAL VARIABLES ---
unsigned long lastDecodedValue = 0;

void setup() {
  Serial.begin(115200);
  while (!Serial); // Wait for serial port (needed for Leonardo/Micro, safe for Uno)
  
  // Initialize the IR receiver (v4.x syntax)
  // ENABLE_LED_FEEDBACK blinks the Uno's onboard LED when an IR signal is received
  IrReceiver.begin(IR_RECV_PIN, ENABLE_LED_FEEDBACK);
  
  Serial.println("IR Receiver initialized on Pin 11.");
  Serial.println("Point your remote at the sensor and press a button.");
}

void loop() {
  // Check if new IR data is available
  if (IrReceiver.decode()) {
    
    // 1. Handle Repeat Codes (e.g., holding down a volume button)
    if (IrReceiver.decodedIRData.flags & IRDATA_FLAGS_IS_REPEAT) {
      Serial.print("REPEAT: ");
      Serial.println(lastDecodedValue, HEX);
    } 
    // 2. Handle Unknown Protocols or Noise
    else if (IrReceiver.decodedIRData.protocol == UNKNOWN) {
      Serial.print("UNKNOWN PROTOCOL / NOISE detected. Raw data length: ");
      Serial.println(IrReceiver.decodedIRData.rawDataPtr->rawlen);
      // You can print the raw buffer here for custom decoding
    } 
    // 3. Handle Valid Decoded Signals
    else {
      lastDecodedValue = IrReceiver.decodedIRData.decodedRawData;
      
      Serial.print("Protocol: ");
      // printIRResultMinimal prints the protocol name automatically
      IrReceiver.printIRResultMinimal(&Serial);
      Serial.print(" | Hex Value: 0x");
      Serial.println(lastDecodedValue, HEX);
      
      // Example: Trigger an action for a specific NEC button (e.g., Power button)
      if (IrReceiver.decodedIRData.protocol == NEC && lastDecodedValue == 0x10EFD827) {
        Serial.println(">>> ACTION: Power button pressed!");
        // Add relay toggle or LED logic here
      }
    }
    
    // CRITICAL: Resume receiving after processing
    IrReceiver.resume(); 
  }
}

Debugging Common Arduino IR Failures

When an IR circuit fails, the issue is almost always a mismatch between library versions, ambient light saturation, or power noise. If your serial monitor is blank or throwing errors, check these three things first.

1. The First Three Things to Check

  1. Library Version Syntax: The IRremote library underwent a massive rewrite at v3.0 and v4.0. If you are copying code from a 2018 tutorial, it will not compile on the current library.
  2. Ambient Light Saturation: Direct sunlight or certain CFL/LED bulbs emit broadband IR or 100/120Hz flicker that blinds the 38kHz sensor. Cup your hand over the receiver; if it starts working, you have an ambient light problem.
  3. Carrier Frequency Mismatch: Most consumer remotes use 38kHz. If you are trying to decode an older Bang & Olufsen remote (455kHz) or a specific Sony device (40kHz), a standard TSOP38238 will physically filter out the signal before the Arduino ever sees it.

Ranked Error Strings and Fixes

Error: error: 'IRrecv' does not name a type
Cause: You are using v2.x syntax (IRrecv irrecv(RECV_PIN);) with the v4.x library installed.
Fix: Delete the object instantiation. Replace irrecv.enableIRIn(); with IrReceiver.begin(RECV_PIN, ENABLE_LED_FEEDBACK); and use IrReceiver.decode() in the loop.
Error: Serial monitor spams UNKNOWN or random hex values continuously
Cause: The receiver is saturated by 100Hz/120Hz ripple from cheap LED drivers or fluorescent ballasts, or the AGC is amplifying thermal noise due to missing decoupling capacitors.
Fix: Move the receiver away from lighting fixtures. If using a bare chip, verify the 100Ω resistor and 4.7µF capacitor are present. Switch to a TSOP4838, which uses a more aggressive AGC3 filter designed specifically to suppress continuous noise.
Error: Button hold registers as 0xFFFFFFFF or REPEAT but you need the actual hex code
Cause: The remote is sending a dedicated repeat frame (common in NEC protocol) rather than re-sending the original payload.
Fix: This is correct hardware behavior. Store the last valid decoded hex value in a global variable (as shown in the code above) and reference that variable when a repeat flag is detected.

Extending and Simplifying Your IR Build

Once you have reliable decoding, you can scale the project up for home automation or scale it down to eliminate custom C++ coding entirely.

How to Extend: Add an IR Blaster

To control a TV or AC unit, you need to transmit. Wire a 940nm IR LED (like the TSAL6200) to Pin 3 on the Uno R3. Do not connect the LED directly to the GPIO pin; the ATmega328P can only source ~20mA per pin, which limits your range to a few inches. Instead, use an NPN transistor (like a 2N2222) to switch the LED from the 5V rail, allowing you to push 100mA pulses for multi-meter range. Use IrSender.sendNEC(lastDecodedValue, 32); in your code to re-transmit captured signals.

How to Simplify: Skip C++ with ESPHome

If your end goal is integrating an IR remote into Home Assistant, writing custom Arduino C++ to bridge MQTT is tedious. Instead, use an ESP32 DevKit V1 and flash it with ESPHome. ESPHome handles the 38kHz demodulation, protocol decoding, and WiFi MQTT bridging natively via YAML configuration. You map the decoded hex strings directly to Home Assistant entities without writing a single line of C++.

For deeper protocol analysis and raw timing dumps, the Arduino-IRremote GitHub repository remains the definitive reference for supported protocols and edge-case timing tolerances.