Learn

Decoding NEC Infrared Signals with an IR Receiver + Remote on a Raspberry Pi 4B

Wire a TSOP38238 IR receiver to a Raspberry Pi 4B and decode a real, timed NEC-protocol pulse train in Python when a paired virtual remote's button is pressed — including telling a fresh press apart from a held one via real repeat frames.

Note: the IR Receiver + Remote (TSOP38238) is a VIP-tier component in the simulator — you’ll need a VIP plan to use it there.

Try this directly in the free Raspberry Pi 4B simulator — no hardware or signup required. New to the GPIO header? Start with the interactive pinout guide.

What you’ll need

  • Raspberry Pi 4B
  • IR Receiver (TSOP38238)
  • IR Remote Control

Step by step

  1. Drag Raspberry Pi 4B onto the Canvas.
  2. Drag the IR Receiver (TSOP38238) onto the Canvas — a real 3-pin module, connects via drawn wires (it doesn't mount into a breadboard).
  3. Hover over its 3 pins to confirm the labels, left to right: GND, VCC, OUT — this specific breakout's own printed order, which is different from the bare TSOP38238 chip's own datasheet order (OUT/GND/VCC). Always check a real part's own silkscreen rather than assuming every breakout of the same chip is wired identically.
  4. Wire GND → a Pi GND pin and VCC → a Pi 3.3V pin. Don't wire VCC to 5V — a real TSOP38238's OUT pin swings close to whatever VCC is, and OUT is wired directly to a Pi input pin next, so a 5V VCC would present an unsafe voltage there (the simulator's own overvoltage advisory will warn you in the Console if you do this).
  5. Wire OUT → any Pi GPIO pin you'll configure as an input (e.g. physical pin 38).
  6. Drag the IR Remote Control onto the Canvas anywhere — it's wireless, so it never connects to anything and never mounts on the breadboard. Any receiver placed anywhere in the circuit will pick up its button presses, matching how a real IR remote broadcasts to any receiver in line of sight.
  7. Go to the Code tab and write a script that busy-polls OUT and measures each transition's real duration — the standard way a real NEC decoder works, since NEC encodes each bit as a space (HIGH, no burst) of one of two durations:
import RPi.GPIO as GPIO
import time
import asyncio
 
GPIO.setmode(GPIO.BOARD)
OUT = 38 # adjust to match your wiring
 
GPIO.setup(OUT, GPIO.IN)
 
 
def read_nec_frame():
# OUT is active-LOW: LOW while a burst is present, HIGH when idle.
# Wait for the next frame's leader mark to start.
while GPIO.input(OUT) == 1:
pass
# Wait out the leader mark, then time the leader space that follows —
# this is what tells a full frame (~40ms space) apart from a repeat
# frame (~20ms space), both of which start with the same-length mark.
while GPIO.input(OUT) == 0:
pass
leader_space_start = time.time()
while GPIO.input(OUT) == 1:
pass
leader_space_ms = (time.time() - leader_space_start) * 1000
 
if leader_space_ms < 30:
return "repeat", None
 
# A full frame: 32 bits follow, each a short mark then a space whose
# duration (short = 0, long = 1) is the actual encoded bit.
bits = []
for _ in range(32):
while GPIO.input(OUT) == 0:
pass
space_start = time.time()
while GPIO.input(OUT) == 1:
pass
space_ms = (time.time() - space_start) * 1000
bits.append(1 if space_ms > 10 else 0)
 
def bits_to_byte(byte_bits):
value = 0
for i, b in enumerate(byte_bits):
value |= b << i
return value
 
# NEC layout: address, ~address, command, ~command — each LSB-first.
command = bits_to_byte(bits[16:24])
return "frame", command
 
 
last_command = None
while True:
kind, command = read_nec_frame()
if kind == "frame":
last_command = command
print(f"New press: command=0x{command:02x}")
elif last_command is not None:
print(f"Still held: command=0x{last_command:02x}")
await asyncio.sleep(0)
  1. Click Start.
  2. Click any button on the remote — a short press should print one "New press: command=0x.." line with a specific hex value for that button.
  3. Click and hold a different button — the first "New press" line prints once, then "Still held" lines keep printing roughly every 150ms for as long as you keep holding it, stopping the instant you release.

What “working correctly” looks like

  • Every button reports its own distinct command byte — the same byte every time you press that specific button, never the same byte as a different button.
  • A quick tap always prints exactly one "New press" line, never a "Still held" line.
  • A held button prints one "New press" line, then a steady stream of "Still held" lines until released — a real decode script can tell "a new button was pressed" apart from "the same button is still held," the real distinction a genuine NEC remote makes.
  • Any receiver you've placed reacts to any remote's button press, even if you've placed several receivers — real IR broadcasts to whatever's in range, it isn't paired to one specific receiver.

If something’s wrong

  • Nothing ever prints, even after clicking a remote button → confirm OUT is wired to the exact pin your script configures with GPIO.setup(pin, GPIO.IN), and confirm the receiver's VCC/GND are both wired (an unpowered receiver never produces a pulse train at all).
  • The script seems to hang instead of printing → double-check GND and VCC aren't swapped — a receiver with its polarity backwards behaves the same as one with no power wired at all.
  • Every press reports the same command byte no matter which button you click → make sure you're actually clicking different buttons on the remote (its layout matches a real universal-remote button grid — hover to confirm which one you're clicking) rather than repeatedly clicking the same spot.
  • Wiring VCC to a Pi 5V pin logs the same overvoltage advisory a bare, undivided sensor wire would — the same reminder to use 3.3V here as with any other component whose signal pin swings with its own supply voltage.