←Index
by Professor Petabyte
Using IR control with an RPi Pico typically involves receiving signals from an IR remote using an IR receiver module (like the VS1838B or TSOP38238) and decoding them using MicroPython or C/C++.
The Titanic tragedy is perhaps the best knownPerhaps not immediately obvious, timing is absolutely critical in almost every form of communication, ever since the introduction of Morse code, first used in the 1840s as part of the telegraph system developed by Samuel Morse and his colleagues. It was used to transmit messages over wires by encoding letters and numbers as combinations of dots and dashes. The first message, "What hath God wrought?", was sent in 1844, marking the birth of all electronic communication.
Unlike analogue voice or sound signals, morse code relied entirely on timing; Long beeps (dashes) and short beeps (dots), separated by short quiet pauses to form characters. Probably the most widely known Morse Code message is SOS, notably used during the Titanic sinking in 1912, which was of course a radio transmission, not over wires.
. . . _ _ _ . . . S O S
Three dots = 'S'; Three dashes = 'O'; Three dots = 'S'.
Here is the complete Morse alphabet. There are infact three variants of it:
As you can see, all three have some commonality e.g. A, E, H, I, S, & U although some characters like 'F', 'J', 'K', 'X', 'Y' and 'Z' are quite different, and Continental has some extras line an A with an umlaut, and a special sequence for 'CH'.
What is important though is timing. In transmitted morse, the length of the short marks (aka dots or dits) and long mark (aka dashes or dahs) must be regular and consistent to be differentiated, as must the gaps between letters (comprised of multiple short and long marks), and the gaps between words. Experienced Morse code users would have 'rhythm' when transmitting, and it is notable that experienced Morse code users (receiving) have been known to recognise and differentiate different users (transmitters), simply by their personal rhythm. In general however timings would be something like:-
The concept of dots and dashes to represent letters can also used with light rather than audible tones transmitted as radio waves. Obviously a radio transmission could be detected by a war-time enemy, and worse still the location of the transmission given away by using RDF (Radio Direction Finding) equipment. Using two sets of RDF equipment, triangulation could be used to reveal the location of the transmission.
Morse lamps, also known as signal lamps or Aldis lamps, are visual signaling devices still used by various navies to transmit covert messages over short distances using Morse code. Such lamps can flash light in patterns representing the dots and dashes, enabling communication over distances up to 8 miles (approximately 13 kilometers) in maritime and aviation contexts. The beam of light is normally quite narrow, typically only 2.6 degrees from an Aldis lamp, so the enemy has to be right in line with the beam to 'overhear' it. This is useful particularly when radio silence is necessary or radio communication is unavailable.
Special forces such as the British SAS and US Marine Corp use hand-held torches (aka flashlights) to send messages in a similar way over short distances.
Morse code proves that carefully timed signals can convey text messages, and in fact send enough text and you can transmit a large, high resolution full colour image, but that's something of a leap.
IR devices do not use Morse Code (although that is theoretically possible). Some very much more sophisticated protocols have been developed to allow close-proximity transmission of data using InfraRed light, such as the NEC protocol, which is commonly found (in numerous variations) in lots of domestic devices such as TVs, TV recorders (e.g. VHS, BetaMax, DVD), CD Audio systems, some high-end cameras, etc.
All that is needed is an LED capable of generating InfraRed light, an InfraRed light detector like the VS1838B shown below, and a microprocessor of some kind to control the transmission and receipt of data. IR works far faster and demands considerably more precision than a human could deliver manually opening and closing a switch - precision measured in billionths (1/1,000,000,000) of a second.
A VS1838B Infrared sensor
The ~address and ~command are compliments to the address and command used for error checking. IR signals are vulnerable to external interference, so it makes sense to robustly check the data received. e.g. Without such thorough checking an attempt to change the volume setting on a CD player could be misinterpreted as a singal to change channel on a TV.
Infrared (IR) signals are part of the electromagnetic spectrum, situated between visible light and microwaves. They have wavelengths longer than visible light, typically ranging from 700 nanometers to 1 millimeter. IR radiation is emitted by all objects with a temperature above absolute zero, and it's also used in various technologies. Unlike visible light, IR radiation is not visible to the human eye, but it can be detected as heat. Probably the easiest way to see such signals by pointing the IR control wand at a digital camera such as a smartphone. Many digital cameras are sensitive to IR light.
| IR Receiver Pin | Connect to Pico | Notes |
|---|---|---|
| OUT | GPIO (e.g., GP16) | Data signal to be read |
| GND | GND | Ground |
| VCC | 3.3V Power | NOT 5volts |
from machine import Pin
import utime
ir_pin = Pin(16, Pin.IN)
def wait_for_signal_change(pin, level):
while pin.value() == level:
pass
return utime.ticks_us()
def read_ir_signal():
timings = []
print("Waiting for signal...")
while ir_pin.value() == 1:
pass # Wait for start
start = utime.ticks_us()
for i in range(100):
t0 = wait_for_signal_change(ir_pin, 0)
t1 = wait_for_signal_change(ir_pin, 1)
pulse_duration = utime.ticks_diff(t1, t0)
timings.append(pulse_duration)
if utime.ticks_diff(t1, start) > 100000: # timeout ~100ms
break
print("Timings:", timings)
while True:
read_ir_signal()
utime.sleep(2)
This example simply prints out the pulse durations. You would need to add logic to decode protocols like NEC based on timing patterns. The timing patterns are just time counts of the duration of flashes ff IR light, and the duration of the periods of 'darkness' between the flashes. The timing patterns look something like this....
If you point an IR control wand straight at the camera of most smartphones, you can often see the flashes of IR light when buttons on the control wand are pressed. They usually look like faint flickering purple light.
It's generally safe to look at the IR flashes from a control wand through a digital camera, but there are a few things to keep in mind. While the human eye can't see infrared light, digital cameras can, and they often display it as a bright white or purple light. This light is not harmful in short, infrequent bursts, but prolonged or very close exposure to bright IR sources could potentially cause eye strain or other minor issues.
from machine import Pin
import utime
ir_pin = Pin(16, Pin.IN)
last_msg = ""
def wait_for_edge(expected):
t0 = utime.ticks_us()
while ir_pin.value() == expected:
if utime.ticks_diff(utime.ticks_us(), t0) > 10000:
return -1
return utime.ticks_diff(utime.ticks_us(), t0)
def read_nec():
global last_msg
# Await start of signal
while ir_pin.value():
pass
# Start pulse (LOW 9ms)
low = wait_for_edge(0)
high = wait_for_edge(1)
if (low < 8000 or high < 4000):
# "Not NEC start" will appear if start pulse is not recognised
print("Not NEC start")
return None
bits = []
# Extract binary bits from signal
for i in range(32):
low = wait_for_edge(0)
high = wait_for_edge(1)
if low == -1 or high == -1:
print("Timeout")
return None
if high > 1000:
bits.append(1)
else:
bits.append(0)
print("Bits are:-",bits)
# Convert bits to bytes
def bits_to_byte(bits):
return sum([b << i for i, b in enumerate(bits)])
addr = bits_to_byte(bits[0:8])
addr_inv = bits_to_byte(bits[8:16])
cmd = bits_to_byte(bits[16:24])
cmd_inv = bits_to_byte(bits[24:32])
# Check bytes are valid
if addr ^ addr_inv != 0xFF or cmd ^ cmd_inv != 0xFF:
print("Checksum failed")
return None
return cmd
print("Waiting for IR signals...")
while True:
cmd = read_nec()
if cmd is not None:
print("Button code:", hex(cmd))
last_msg = ""
utime.sleep(0.2)
Running this code you should start to see something more human readable, but perhaps a little more work is needed.
The HEX code we get back is 0x15, which is 21 in decimal (1x16 + 5x1) => 16 + 5 = 21.
Look now at the binary bits for the command:-
Note that the low bits PRECEED the high bits, and the low bytes PRECEED the high bytes.
The code above enables the hex code for each button press to be to be seen. The hex code for the IR control wand pictured here are as follows, but may vary depending on the manufacturer and model.
| Button | Code | Button | Code | Button | Code |
|---|---|---|---|---|---|
| 1 | 0x45 | 7 | 0x07 | UP | 0x18 |
| 2 | 0x46 | 8 | 0x15 | DOWN | 0x52 |
| 3 | 0x47 | 9 | 0x09 | LEFT | 0x08 |
| 4 | 0x44 | 0 | 0x19 | RIGHT | 0x5A |
| 5 | 0x40 | * | 0x16 | OK | 0x1C |
| 6 | 0x43 | # | 0x0d |
Given that the expected hex codes are predictable (and mostly reliable), from this point it is relatively straight forward to build a translation table into the program, and have it react appropriately to each keypress, which would look something like this:-
from machine import Pin
import utime
# Setup
ir_sensor = Pin(16, Pin.IN)
led = Pin(25,Pin.OUT)
last_cmd = None
led_state = False
# Button map (based on typical HX1838 remotes)
button_names = {
0x45: "1",
0x46: "2",
0x47: "3",
0x44: "4",
0x40: "5",
0x43: "6",
0x07: "7",
0x15: "8",
0x09: "9",
0x19: "0",
0x16: "*",
0x0D: "#",
0x1C: "OK",
0x18: "UP",
0x52: "DOWN",
0x08: "LEFT",
0x5A: "RIGHT"
}
def wait_for_edge(expected):
t0 = utime.ticks_us()
while ir_sensor.value() == expected:
if utime.ticks_diff(utime.ticks_us(), t0) > 10000:
return -1
return utime.ticks_diff(utime.ticks_us(), t0)
def bits_to_byte(bits):
return sum([b << i for i, b in enumerate(bits)])
def read_nec():
while ir_sensor.value():
pass # wait for LOW
low = wait_for_edge(0)
high = wait_for_edge(1)
if low < 8000:
return None
# Handle repeat code
if 2000 < high < 2800:
return "REPEAT"
if high < 4000:
return None
# Read 32 bits
bits = []
for i in range(32):
low = wait_for_edge(0)
high = wait_for_edge(1)
if low == -1 or high == -1:
return None
if high > 1000:
bits.append(1)
else:
bits.append(0)
addr = bits_to_byte(bits[0:8])
addr_inv = bits_to_byte(bits[8:16])
cmd = bits_to_byte(bits[16:24])
cmd_inv = bits_to_byte(bits[24:32])
if addr ^ addr_inv != 0xFF or cmd ^ cmd_inv != 0xFF:
return None
return cmd
# Main loop
print("Ready for IR input...")
while True:
cmd = read_nec()
if cmd == "REPEAT":
if last_cmd:
cmd = last_cmd
else:
continue
elif cmd is not None:
last_cmd = cmd
else:
continue
button = button_names.get(cmd, hex(cmd))
print("Pressed:", button)
# Example action: Toggle LED on button "1"
if cmd == 0x45: # "1"
led_state = not led_state
led.value(led_state)
print(led_state)
print("LED is now", "ON" if led_state else "OFF")
utime.sleep(0.1)
N.B. See the 'Button map (based on typical HX1838 remotes)' near the top of this code. Different control wands from different manufacturers may transmit different hex codes than the ones shown in the table above. Using the program that reveals the hex codes per button, the button mappings section can easily be modified for a control wand other than the one used in this demonstration.
Given code that can detect and interpret IR Wand button presses, it is relatively tivial to build that into a large program that, perhaps via Relays, controls devices from a RPi Pico, e.g. Heaters, fans, pumps, motors, servos, etc.
IR signals can very easily be used to to control a huge variety of devices, and the convenience of doing so has made them hugely popular. They are very energy efficient, with most IR control wands capable of providing many months of normal service from a pair of AA batteries, and in some cases use a single button cell such as a CR2032 batteries. It increasingly common to find control wands that can control many different devices because they either use a common protocol like NEC, or can be programmed to emulate a different control wand. While infrared (IR) controls can be used outdoors, their effectiveness is limited by several factors, primarily line-of-sight and range. IR remotes require a clear line of sight to the receiver and have a limited range, typically around 10 meters. Obstacles like walls or even some types of glass can block the signal. However, for most indoor applications, such as controlling TVs, Audio systems, and other indoor devices, infrared is an excellent option.