Pico W infrared remote decoder on GP17
MicroPython on a Pico W: an IR receiver on GP17 and a small NEC decoder timed from pin interrupts print the name of each remote button pressed. The whole build, a Raspberry Pi Pico W and 2 more parts, runs here in your browser on the firmware below; open it in the editor to change the wiring or the code and run it again.
Teaching? Assign it to your class in one click.
Intermediate About 20 minutes to build and run. Runs in your browser. Free, and no account needed.
The parts retail for about $7.50. On Mokxi they're free.
What you'll learn
- How an NEC infrared remote encodes buttons
- Timing pin edges with an interrupt in MicroPython
- Error checking with inverse bytes
- Looking up a code in a dictionary
- Difficulty: Intermediate
- Time: About 20 minutes
- Board: Raspberry Pi Pico W
- Parts: 4
How it works
A VS1838B infrared receiver on GP17 pulls its pin low while it sees infrared light. The remote sends each button as a train of bursts and gaps in the NEC format. A small decoder in nec.py times every edge with a pin interrupt.
A frame starts with a 9 ms burst and a 4.5 ms gap, then 32 bits. A short gap is a 0 and a long gap is a 1. The decoder checks that the second byte is the inverse of the first, and likewise for the command, and then hands the command to main.py, which prints the button name.
Parts list
4 parts, plus the jumper wires. They retail for about $7.50; on Mokxi they're free.
- 1 × Raspberry Pi Pico W
- 1 × Infrared receiver, VS1838B
- 1 × Infrared remote, 21 buttons
- 1 × Power, 3.3 V
Kits that can build it
These 2 starter kits have every part in the circuit. Each kit's page lists what else it can build.
Wiring table
Every connection, pin by pin, read from the circuit itself. Each row joins the pin on the left to the pin on the right, by a jumper wire or a breadboard strip.
| Part and pin | Connects to |
|---|---|
| Infrared receiver, VS1838B pin OUT | Raspberry Pi Pico W pin GP17 |
| Infrared receiver, VS1838B pin GND | Ground |
| Infrared receiver, VS1838B pin VCC | 3.3 V |
| Infrared remote, 21 buttons pin IR | Infrared receiver, VS1838B pin IR |
The code
The MicroPython code exactly as the editor opens it. Change a line there and press Run: the board runs it on the official MicroPython firmware, with nothing to compile.
# IR remote control: the receiver's output is on GP17. nec.py, in its
# own tab, times its edges and decodes the remote's NEC frames.
from machine import Pin
from nec import NEC
import time
NAMES = {0x45: "CH-", 0x46: "CH", 0x47: "CH+", 0x44: "PREV", 0x40: "NEXT",
0x43: "PLAY/PAUSE", 0x07: "VOL-", 0x15: "VOL+", 0x09: "EQ",
0x16: "0", 0x19: "100+", 0x0D: "200+", 0x0C: "1", 0x18: "2",
0x5E: "3", 0x08: "4", 0x1C: "5", 0x5A: "6", 0x42: "7",
0x52: "8", 0x4A: "9"}
pressed = []
def got(address, command):
pressed.append(command)
NEC(Pin(17, Pin.IN), got)
print("point the remote and press a button")
while True:
while pressed:
command = pressed.pop(0)
print("key", NAMES.get(command, hex(command)))
time.sleep_ms(20)
# nec.py: decode an NEC infrared remote from the receiver's output.
# The receiver pulls its pin low while light arrives. A frame starts with
# a 9 ms burst and a 4.5 ms gap, then 32 bits: each bit is a 562 us burst
# followed by a short gap (0) or a long one (1), least significant first.
# The bytes are the address, its inverse, the command and its inverse.
from machine import Pin
import time
class NEC:
def __init__(self, pin, callback):
self.pin = pin
self.callback = callback
self.edges = []
self.last = time.ticks_us()
pin.irq(handler=self._edge, trigger=Pin.IRQ_FALLING | Pin.IRQ_RISING)
def _edge(self, pin):
now = time.ticks_us()
gap = time.ticks_diff(now, self.last)
self.last = now
if gap > 12000: # a long quiet spell: a new frame begins
self.edges = []
self.edges.append(gap)
if len(self.edges) == 67: # the start, the leader's burst and gap, 32 bits
self._decode(self.edges)
self.edges = []
def _decode(self, edges):
bits = 0
for i in range(32):
space = edges[4 + 2 * i] # the gap after each burst
if space > 1100:
bits |= 1 << i
address = bits & 0xFF
command = (bits >> 16) & 0xFF
if (bits >> 8) & 0xFF == address ^ 0xFF and bits >> 24 == command ^ 0xFF:
self.callback(address, command)
Code walkthrough
The key functions of the sketch, and what each one does.
got main.py
A callback that the decoder runs when it has read a valid button press. It does very little: it adds the command byte to the pressed list. The main loop then prints the name, which keeps slow work out of the interrupt.
def got(address, command):
pressed.append(command)Troubleshooting
The usual mistakes with this circuit, and the fix for each.
- Nothing prints when you press buttons This project
Check the receiver's OUT goes to GP17 and that it has power and ground. - A key prints as a hex number This project
That code is not inNAMES. Add it to the dictionary with a label. - Buttons are missed now and then This project
The decoder drops frames whose check bytes do not match. Press again and point the remote at the receiver. - No signal is seen, or the module gets hot, because the three legs are in the wrong order. Infrared receiver
On the VS1838B, with the dome facing you, the legs are usually OUT, GND, VCC from left to right. Check your module's marking before powering it. Supply is 2.7 to 5.5 V. - A plain LED blinking at the receiver is never detected. Infrared receiver
The receiver only responds to light modulated at about 38 kHz. Send with a library such as IRremote, which makes the carrier. OUT idles high and goes low during a burst, so decode that inverted signal. - Button codes come out wrong or do not match another sketch. Infrared remote
Remotes differ. Print the raw codes for your buttons first, then compare against those values. A repeat while you hold a button is a special repeat code, not a new press. - Nothing is received at all. Infrared remote
Check the receiver pin order of OUT, GND and VCC, which varies by module. Use the pin your IR library expects. Keep the receiver away from bright light and aim the remote at it. - Pins are powered from 5 V, or a 5 V signal goes into a GPIO. The RP2040 pins are 3.3 V only and are not 5 V tolerant. Raspberry Pi Pico W
Power the board from VBUS or VSYS at 5 V, but keep GPIO signals at 3.3 V. Use a level shifter or a divider for 5 V parts.
Change it and keep it
Open it in the editor, change the circuit or the code, and keep your version in a free account.