To build a reliable ESP32 IR emitter, you cannot simply wire an infrared LED directly to a GPIO pin. The ESP32-WROOM-32 GPIO pins max out at 40mA (with a safe continuous limit of 20mA), while a high-power 940nm IR LED requires 100mA to 200mA pulsed current for effective room-wide range. The direct answer is to use an NPN transistor (like a 2N2222) as a low-side switch, driven by the ESP32's hardware LEDC (LED Control) peripheral to generate the 38kHz carrier frequency.

This guide covers the exact hardware BOM, the transistor driver circuit, complete compilable code using the IRremoteESP8266 library, and a systematic debugging framework for when the signal fails to reach your target device.

Project Overview & Difficulty Rating

Difficulty: Intermediate (Requires basic transistor biasing and breadboard wiring)

Time to Build: 45 minutes

Target Board: ESP32-DevKitC V4 (ESP32-WROOM-32 module)

Core Library: IRremoteESP8266 (v2.8.6 or newer)

Hardware BOM & Pin Mapping

Do not use the internal 3.3V regulator to power the IR LED; it will cause brownouts and ESP32 reboots when the LED pulses. We pull 5V directly from the VBUS (USB) pin to drive the LED collector circuit, while the 3.3V GPIO handles the logic-level base switching.

Component Exact Variant / Spec Estimated Cost Purpose
Microcontroller ESP32-DevKitC V4 (WROOM-32) $6.00 Main logic and PWM generation
IR LED Vishay TSAL6200 (940nm, 5mm) $0.50 Infrared signal emission
NPN Transistor 2N2222 or 2N3904 (TO-92) $0.10 Low-side current switching
Base Resistor 1kΩ (1/4W, 5%) $0.02 Limit GPIO base current to ~2.6mA
Collector Resistor 33Ω (1/2W, 5%) $0.05 Limit LED pulsed current to ~115mA

Pin Mapping Table

ESP32 Pin Wiring Destination Notes
GPIO 17 1kΩ Resistor → 2N2222 Base Safe boot pin. Avoids strapping conflicts.
VBUS (5V) 33Ω Resistor → IR LED Anode Provides high peak current for maximum range.
GND 2N2222 Emitter Common ground reference.

Wiring the IR Emitter Stage

The ESP32 uses its LEDC hardware peripheral to generate the 38kHz PWM carrier wave. The transistor acts as a switch, turning the high-current 5V supply on and off at 38kHz, which modulates the IR LED. For a deeper understanding of using BJT transistors as switches, refer to this All About Circuits guide on transistor switching.

  1. Prepare the Base Drive: Connect a jumper wire from ESP32 GPIO 17 to one end of the 1kΩ resistor. Connect the other end of the resistor to the Base (middle pin) of the 2N2222 transistor.
  2. Wire the Emitter to Ground: Connect the Emitter (right pin, flat side facing you) of the 2N2222 directly to the ESP32 GND pin.
  3. Build the Collector Load: Connect the 33Ω resistor to the ESP32 VBUS (5V) pin. Connect the other end of the 33Ω resistor to the Anode (long leg) of the 940nm IR LED.
  4. Complete the Circuit: Connect the Cathode (short leg) of the IR LED to the Collector (left pin) of the 2N2222 transistor.
  5. Verify Polarity: Double-check the transistor pinout. A standard TO-92 2N2222 with the flat side facing you is Emitter (Left), Base (Middle), Collector (Right) for some brands, but always verify your specific datasheet. (Note: Standard 2N3904 is Emitter-Base-Collector left-to-right. Adjust physical wiring to match your specific component's datasheet).
Pro-Tip: The Smartphone Camera Test
Human eyes cannot see 940nm infrared light. Before uploading code, open your smartphone camera and point it at the IR LED. When the ESP32 fires the signal, you should see a distinct purple/white flickering on your phone screen. If you see a solid, unblinking light, your carrier frequency is missing and the LED is just acting as a standard DC diode.

Complete ESP32 IR Emitter Code

This code targets the ESP32 Dev Module board variant in the Arduino IDE. It uses the IRremoteESP8266 library. Install it via the Library Manager (Tools > Manage Libraries > search 'IRremoteESP8266'). For detailed API documentation, check the official IRremoteESP8266 Wiki.

#include <Arduino.h>
#include <IRremoteESP8266.h>
#include <IRsend.h>

// PIN DEFINITION: Must match physical wiring
const uint16_t kIrLedPin = 17;  // ESP32 GPIO 17

// Initialize the IR sender object on the specified pin
IRsend irsend(kIrLedPin);

// Example NEC Power Code for a generic Samsung TV
const uint32_t kTvPowerCode = 0xE0E040BF; 

void setup() {
  Serial.begin(115200);
  
  // Wait for serial monitor to connect (useful for debugging boot logs)
  while (!Serial) {
    delay(50);
  }
  
  // Initialize the IR sender (configures ESP32 LEDC peripheral for 38kHz)
  irsend.begin();
  
  Serial.println("ESP32 IR Emitter initialized.");
  Serial.print("Carrier frequency set to: 38kHz on GPIO ");
  Serial.println(kIrLedPin);
}

void loop() {
  // Send the NEC code
  Serial.println("Sending TV Power Command (NEC)...");
  
  // irsend.sendNEC() handles the carrier modulation and timing automatically
  bool success = irsend.sendNEC(kTvPowerCode, 32);
  
  if (success) {
    Serial.println("Command transmitted successfully.");
  } else {
    Serial.println("ERROR: Transmission failed. Check LEDC peripheral allocation.");
  }
  
  // Wait 5 seconds before sending again to avoid flooding the receiver
  delay(5000);
}

Debugging: First 3 Things to Check When It Fails

When your circuit compiles but the target device (TV, AC unit, fan) does not respond, follow this ranked decision tree. These are the most common failure modes encountered on the bench.

1. Symptom: Phone camera shows solid light, no flickering

Cause: The 38kHz carrier frequency is not being generated. The LED is receiving raw DC or a duty cycle without the high-frequency PWM.

Fix: The ESP32 uses the LEDC peripheral for IR modulation. If another library (like FastLED or a servo library) has claimed the LEDC timers, the IR library will silently fail to generate the carrier. Remove conflicting libraries, or explicitly assign the IRsend object to an unused LEDC channel in the library initialization. Also, ensure you aren't using a GPIO pin reserved for internal flash SPI (GPIO 6-11).

2. Symptom: Flickering is visible on camera, but TV ignores it

Cause: Weak signal intensity or incorrect protocol/frequency.

Fix: First, verify the carrier frequency. While 38kHz is standard for NEC/Sony protocols, some older AC units use 56kHz or 40kHz. Second, check your collector resistor. If you used a 220Ω resistor instead of 33Ω, your pulsed current is only ~17mA, which limits range to about 1 meter. Drop to a 33Ω or 22Ω resistor to push 100mA+ pulses. Finally, ensure you are sending the correct bit-length (e.g., 32 bits for NEC).

3. Symptom: ESP32 reboots or throws Brownout Detector error when firing

Cause: VBUS voltage sag due to high current draw from a poor USB cable or weak PC USB port.

Fix: The brownout detector was triggered error in the serial monitor means the 5V rail dipped below the ESP32's threshold when the IR LED pulsed. Use a high-quality, short USB-A to Micro-USB cable rated for 2A charging, or power the ESP32 via a dedicated 5V 2A wall adapter. Do not rely on a standard laptop USB 2.0 port (limited to 500mA).

Compilation Error?
If you see fatal error: IRremoteESP8266.h: No such file or directory, you have not installed the library correctly. Go to Sketch > Include Library > Manage Libraries, search for IRremoteESP8266 by David Conran, and install it. Do not use the legacy 'IRremote' library by shirriff, as it lacks native ESP32 LEDC hardware support and will cause timer conflicts.

Extending and Simplifying the Build

How to Extend: To build a full IR blaster/learning remote, add a 38kHz IR receiver module (like the VS1838B) to GPIO 15. Use the IRrecvDumpV2 example sketch from the library to capture the raw hex codes from your existing physical remotes, then hardcode those arrays into your emitter sketch. You can also integrate MQTT over WiFi to trigger the IR blaster from Home Assistant.

How to Simplify: If you want to skip the transistor biasing and breadboard wiring, purchase an off-the-shelf IR Shield for ESP32 or a pre-wired IR Blaster Cable (often sold for Raspberry Pi or broadlink replacements). These modules include the MOSFET/transistor driver and current-limiting resistors on a single PCB, requiring only VCC, GND, and a single Signal wire to a GPIO pin.

Frequently Asked Questions

Can I connect an ESP32 IR emitter directly to a GPIO pin without a transistor?

Technically yes, but practically no. An ESP32 GPIO pin can safely source about 20mA. A standard 5mm IR LED requires 100mA for reliable room-wide transmission. Driving it directly at 20mA will limit your range to roughly 12 inches. Furthermore, pulsing high currents directly from the GPIO can degrade the ESP32 silicon over time. Always use a transistor or MOSFET driver stage.

What is the best carrier frequency for an ESP32 IR emitter?

38kHz is the industry standard for 90% of consumer electronics (TVs, soundbars, basic fans) using NEC, RC5, or Sony SIRC protocols. However, if you are targeting Daikin or Mitsubishi HVAC systems, you may need to configure the IRsend object to 36.7kHz or 40kHz. Always check the target device's datasheet or capture the signal from the OEM remote using an IR receiver to verify the exact carrier.

Why does my ESP32 IR emitter range drop after a few minutes of use?

This is almost always caused by thermal throttling of the IR LED or the current-limiting resistor. If your collector resistor is rated for 1/4W but is dissipating 0.5W during heavy pulsing, it will heat up, increase in resistance, and choke the current to the LED. Upgrade the collector resistor to a 1/2W or 1W variant, and ensure your IR LED has adequate airflow.

How do I find the right IR codes for my AC unit?

AC units rarely use simple toggle codes like TVs; they transmit massive 200+ bit state arrays containing temperature, fan speed, and swing settings. Use the IRrecvDumpV2 sketch to capture the raw uint8_t state[] array from your physical remote. You must send this exact array back using irsend.sendRaw() or the specific manufacturer function (e.g., irsend.sendKelvinator()) included in the library. For a comprehensive list of supported AC protocols, consult the Espressif LEDC documentation and the library's supported protocols markdown file.