The IRremote library (currently v4.x) is the definitive Arduino IR remote library for decoding and transmitting infrared signals. For a standard 38kHz NEC protocol remote, wire the VS1838B receiver data pin to Arduino D11, VCC to 5V, and GND to GND, then initialize with IrReceiver.begin(). If you are migrating from older tutorials, note that the v2 IRrecv and decode_results classes are deprecated; v4 uses the unified IrReceiver object and decodedIRData struct.
This guide provides a bench-tested hardware setup, protocol timing specs, fully compilable v4 code targeting the Arduino Uno R3, and a troubleshooting matrix for the exact compiler and hardware errors you will encounter on the workbench.
Hardware BOM, Pin Mapping, and Protocol Specs
Target Board Variant: Arduino Uno R3 (ATmega328P) or Arduino Nano v3 (Old Bootloader)
Parts List
- Microcontroller: Arduino Uno R3 (ATmega328P) or genuine Nano v3.
- IR Receiver: VS1838B 38kHz IR receiver module (includes built-in bandpass filter and AGC). Do not use raw photodiodes for this build.
- IR Transmitter: Generic 24-key or 44-key NEC protocol RGB/LED remote.
- Passives: 220Ω current-limiting resistor (if testing an IR LED transmitter later), 5mm diffused LED for visual feedback.
- Wiring: 22 AWG solid core jumper wires, half-size solderless breadboard.
Pin Mapping Table
| Component | Module Pin | Arduino Uno R3 Pin | Notes / Constraints |
|---|---|---|---|
| VS1838B Receiver | DATA (OUT) | D11 | Avoid D3 and D11 for PWM output simultaneously (Timer2 conflict). |
| VS1838B Receiver | VCC | 5V | Requires 4.5V-5.5V. 3.3V will cause unstable decoding. |
| VS1838B Receiver | GND | GND | Ensure common ground with the microcontroller. |
| Status LED (Optional) | Anode (+) | D13 (via 220Ω) | Library handles onboard LED feedback automatically on D13. |
IR Protocol Timing & Specifications Matrix
Understanding the underlying protocol is critical when the Arduino IR remote library fails to decode a signal. The library relies on mark/space timing thresholds. Here are the exact specifications for the four most common consumer protocols.
| Protocol | Carrier Freq | Lead Mark / Space | Logic 0 Timing | Logic 1 Timing | Bit Length |
|---|---|---|---|---|---|
| NEC | 38 kHz | 9000µs / 4500µs | 560µs / 560µs | 560µs / 1690µs | 32 bits |
| Sony SIRC | 40 kHz | 2400µs / 600µs | 600µs / 600µs | 1200µs / 600µs | 12, 15, or 20 bits |
| RC5 (Philips) | 36 kHz | N/A (Manchester) | 889µs / 889µs | 889µs / 889µs (inverted) | 14 bits |
| Samsung | 38 kHz | 4500µs / 4500µs | 560µs / 560µs | 560µs / 1690µs | 32 bits |
Source: Protocol timings derived from the official Arduino-IRremote GitHub repository and manufacturer datasheets.
Complete Compilable Code (Arduino Uno R3)
The following code targets the Arduino Uno R3 (ATmega328P) and uses the modern v4.x API. It includes pin definitions at the top, handles repeat codes (which occur when you hold a button down), and prints the decoded hex value and protocol type to the Serial Monitor.
<IRremote.hpp>. While <IRremote.h> still redirects for backward compatibility, using the .hpp extension prevents namespace collisions in larger C++ projects.
#include <IRremote.hpp>
// --- PIN DEFINITIONS ---
const int IR_RECEIVE_PIN = 11;
const int LED_FEEDBACK_PIN = 13; // Built-in LED on Uno R3
void setup() {
Serial.begin(115200);
// Initialize the IR receiver.
// ENABLE_LED_FEEDBACK blinks the LED on pin 13 when a signal is received.
IrReceiver.begin(IR_RECEIVE_PIN, ENABLE_LED_FEEDBACK, LED_FEEDBACK_PIN);
Serial.println(F("IRremote v4.x Initialized. Ready to decode NEC/RC5/Sony."));
}
void loop() {
// Check if a complete IR packet has been received
if (IrReceiver.decode()) {
// ERROR HANDLING: Check for repeat codes (button held down)
if (IrReceiver.decodedIRData.flags & IRDATA_FLAGS_IS_REPEAT) {
Serial.println(F("REPEAT: Button is being held down."));
}
// ERROR HANDLING: Check for overflow or parity errors
else if (IrReceiver.decodedIRData.flags & IRDATA_FLAGS_HAS_ERROR) {
Serial.println(F("ERROR: Signal received but failed parity/timing check."));
}
else {
// Successful decode
Serial.print(F("Protocol: "));
Serial.print(IrReceiver.decodedIRData.protocol);
Serial.print(F(" | Hex Value: 0x"));
// Print leading zeros for consistent formatting
if (IrReceiver.decodedIRData.decodedRawData < 0x10000000) Serial.print(F("0"));
if (IrReceiver.decodedIRData.decodedRawData < 0x1000000) Serial.print(F("0"));
if (IrReceiver.decodedIRData.decodedRawData < 0x100000) Serial.print(F("0"));
Serial.println(IrReceiver.decodedIRData.decodedRawData, HEX);
}
// CRITICAL: You MUST call resume() to re-enable the receiver interrupt
IrReceiver.resume();
}
}
Debugging: Exact Error Strings and Hardware Failures
When working with the Arduino IR remote library, failures generally fall into two categories: compiler errors from outdated code, and silent hardware decoding failures. Here is how to resolve both.
Compiler Error: "decode_results was not declared"
If you copy-paste code from a tutorial written before 2021, you will likely hit this exact compiler error:
error: 'decode_results' was not declared in this scopeerror: 'class IRrecv' has no member named 'decode'
The Fix: The v3.0 and v4.0 updates completely overhauled the class structure. Delete your IRrecv irrecv(RECV_PIN); and decode_results results; declarations. Replace them with the global IrReceiver object and use IrReceiver.decodedIRData.decodedRawData to access the hex value, as shown in the code block above.
Hardware Failure: The First 3 Things to Check When It Fails to Decode
If the code compiles but the Serial Monitor is dead, or outputs pure garbage, run through this ranked diagnostic path:
- Ambient Light and Ballast Interference (Most Likely): The VS1838B has an Automatic Gain Control (AGC) circuit. If you are working under modern LED shop lights or CFL bulbs, the power ballasts often emit broadband noise in the 30kHz–40kHz range. This floods the AGC, causing it to lower its sensitivity until it goes blind. Fix: Cup your hand over the sensor to block room light, or test in a darkened room. If it suddenly works, you need to shield the receiver or change your lighting.
- Missing
IrReceiver.resume(): The library uses hardware interrupts to time the microsecond pulses. Oncedecode()finishes, the interrupt buffer is locked to prevent data corruption while you process the result. If you forget to callIrReceiver.resume()at the end of yourifblock, the receiver will never trigger again. Fix: Ensureresume()is the absolute last line inside your decode logic. - ATmega328P Timer2 Conflicts: On the Uno R3, the IRremote library commandeers Timer2 to measure pulse widths. Timer2 also controls hardware PWM on Digital Pins 3 and 11. If you try to use
analogWrite(3, 128)to dim an LED while receiving IR, the PWM will fail, and the IR timing will jitter. Fix: Move PWM outputs to Pins 5, 6, 9, or 10 (controlled by Timer0 and Timer1).
Extending to ESP32 and Simplifying for Single-Button Triggers
How to Extend: Migrating to the ESP32 RMT Peripheral
The Arduino Uno R3 is great for learning, but its 8-bit architecture and limited timers make it a poor choice for complex IR-driven projects (like an IR-controlled LED strip that also requires high-frequency PWM).
To extend this build, migrate to an ESP32-WROOM-32 dev board. The ESP32 features a dedicated hardware peripheral called the Remote Control Peripheral (RMT). The RMT handles IR pulse timing entirely in hardware, freeing up the main CPU cores and eliminating all timer conflicts with PWM or WiFi interrupts. When using the IRremote library on the ESP32 via the Arduino core, the library automatically routes the signal through the RMT driver under the hood. Simply change your receive pin to a safe GPIO (like GPIO 15) and ensure you do not use strapping pins (GPIO 0, 2, 12) for the receiver.
How to Simplify: Raw Hex Matching
If your project only requires triggering a relay when one specific button on a dedicated remote is pressed, you do not need the overhead of full protocol decoding. You can simplify the build by ignoring the protocol type and strictly matching the raw hex payload.
Instead of checking IrReceiver.decodedIRData.protocol == NEC, just map the specific button to a constant:
#define BTN_POWER 0xFFA25D
// Inside loop():
if (IrReceiver.decodedIRData.decodedRawData == BTN_POWER) {
toggleRelay();
}
This strips away protocol-validation logic, reduces memory footprint, and makes the code highly readable for single-purpose appliances. Just remember to map your specific remote's hex codes first by running the full diagnostic sketch and pressing each button to record the Serial Monitor output.






