IR Remote Control
This tutorial covers using an infrared (IR) remote control to command the robot, integrating with the state machine architecture.
What You'll Learn
In this tutorial, you'll understand:
- How IR communication works (carrier frequency, modulation)
- The structure of IR remote protocols (NEC, RC5, etc.)
- How to decode IR signals in MicroPython
- How to integrate IR events into a state machine
After completing this tutorial, you'll be able to:
- ✅ Receive and decode IR remote signals
- ✅ Map remote buttons to robot actions
- ✅ Create an event-driven remote control system
- ✅ Combine IR input with sensor-based automation
How IR Remotes Work
The Signal
IR remotes transmit data using modulated infrared light:
The 38kHz carrier frequency helps distinguish the signal from ambient IR light (sunlight, lamps).
NEC Protocol
The most common protocol for hobbyist remotes:
┌─────────┬─────────┬──────────┬──────────┬─────────┐
│ Leader │ Address │ Address │ Command │ Command │
│ (9ms+ │ (8 bits)│ (inverse)│ (8 bits) │(inverse)│
│ 4.5ms) │ │ │ │ │
└─────────┴─────────┴──────────┴──────────┴─────────┘
- Leader: 9ms pulse + 4.5ms space (marks start of transmission)
- Address: Identifies the device (e.g., TV, robot)
- Command: The button pressed
- Inverse bytes: Error checking
NEC protocol timing showing leader pulse, bit encoding (0 vs 1), and frame structure
Why Inverse Bytes?
If Address + Address_Inverse ≠ 0xFF, there was a transmission error.
Hardware Setup
The Pico Robot includes an IR receiver module:
| Component | Pin |
|---|---|
| IR Receiver Signal | GP6 |
| IR Receiver VCC | 3.3V |
| IR Receiver GND | GND |
Basic IR Receiving
Step 1: Simple Signal Detection
First, verify the IR receiver is working:
from machine import Pin
import time
ir_pin = Pin(6, Pin.IN)
print("Waiting for IR signal...")
print("Press any button on the remote")
while True:
if ir_pin.value() == 0: # Signal received (active low)
print("IR signal detected!")
time.sleep(0.5) # Debounce
Step 2: Measure Pulse Timing
To decode the protocol, measure pulse durations:
from machine import Pin
import time
ir_pin = Pin(6, Pin.IN)
def measure_pulses(count=50):
"""Measure pulse timings for analysis."""
pulses = []
# Wait for signal start
while ir_pin.value() == 1:
pass
for _ in range(count):
# Measure low time
start = time.ticks_us()
while ir_pin.value() == 0:
pass
low_time = time.ticks_diff(time.ticks_us(), start)
# Measure high time
start = time.ticks_us()
while ir_pin.value() == 1:
if time.ticks_diff(time.ticks_us(), start) > 10000:
break # Timeout
high_time = time.ticks_diff(time.ticks_us(), start)
pulses.append((low_time, high_time))
return pulses
print("Press a button...")
pulses = measure_pulses()
for i, (low, high) in enumerate(pulses):
print(f"{i}: low={low}µs, high={high}µs")
NEC Protocol Decoder
from machine import Pin
import time
ir_pin = Pin(6, Pin.IN)
def decode_nec():
"""Decode NEC protocol IR signal."""
# Wait for leader pulse (9ms low)
while ir_pin.value() == 1:
pass
start = time.ticks_us()
while ir_pin.value() == 0:
pass
leader_low = time.ticks_diff(time.ticks_us(), start)
# Check leader timing (should be ~9000µs)
if leader_low < 8000 or leader_low > 10000:
return None # Not NEC
# Wait through leader space (4.5ms high)
start = time.ticks_us()
while ir_pin.value() == 1:
pass
leader_high = time.ticks_diff(time.ticks_us(), start)
if leader_high < 4000 or leader_high > 5000:
return None
# Decode 32 bits
data = 0
for i in range(32):
# Skip the 562µs low pulse
while ir_pin.value() == 0:
pass
# Measure high time to determine bit value
start = time.ticks_us()
while ir_pin.value() == 1:
pass
high_time = time.ticks_diff(time.ticks_us(), start)
# 562µs = 0, 1687µs = 1
if high_time > 1000:
data |= (1 << i)
# Extract fields
address = data & 0xFF
address_inv = (data >> 8) & 0xFF
command = (data >> 16) & 0xFF
command_inv = (data >> 24) & 0xFF
# Verify
if (address ^ address_inv) != 0xFF:
return None
if (command ^ command_inv) != 0xFF:
return None
return {'address': address, 'command': command}
# Main loop
while True:
result = decode_nec()
if result:
print(f"Address: 0x{result['address']:02X}, Command: 0x{result['command']:02X}")
Mapping Buttons to Commands
Create a button map for your remote:
# Common NEC remote button codes (adjust for your remote)
BUTTON_MAP = {
0x45: 'CH-',
0x46: 'CH',
0x47: 'CH+',
0x44: 'PREV',
0x40: 'NEXT',
0x43: 'PLAY',
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',
}
def get_button_name(command):
return BUTTON_MAP.get(command, f'Unknown (0x{command:02X})')
Finding Your Button Codes
Run the decoder and press each button on your remote. Record the command values to build your button map.
Integrating with State Machine
Add IR events to your state machine:
from machine import Pin
from pico_car import pico_car
# Add IR events
class Event:
# Sensor events
RIGHT_OVER = "ev_RightOver"
LEFT_OVER = "ev_LeftOver"
# IR remote events
IR_FORWARD = "ev_IrForward"
IR_BACK = "ev_IrBack"
IR_LEFT = "ev_IrLeft"
IR_RIGHT = "ev_IrRight"
IR_STOP = "ev_IrStop"
# Map IR commands to events
IR_COMMAND_MAP = {
0x18: Event.IR_FORWARD, # Button '2' = forward
0x52: Event.IR_BACK, # Button '8' = back
0x08: Event.IR_LEFT, # Button '4' = left
0x5A: Event.IR_RIGHT, # Button '6' = right
0x1C: Event.IR_STOP, # Button '5' = stop
}
# In the state machine process():
def process(self, event):
# ... existing state handling ...
# Add IR handling in any state
if event == Event.IR_STOP:
self.state = State.STOP
self.set_robot_stop()
elif event == Event.IR_FORWARD:
self.state = State.FORWARD
self.set_robot_forward(100)
# ... etc ...
Complete Remote Control Example
from machine import Pin
from pico_car import pico_car
import time
Motor = pico_car()
ir_pin = Pin(6, Pin.IN)
# Button mapping (adjust for your remote)
CMD_FORWARD = 0x18 # '2'
CMD_BACK = 0x52 # '8'
CMD_LEFT = 0x08 # '4'
CMD_RIGHT = 0x5A # '6'
CMD_STOP = 0x1C # '5'
def decode_nec():
# ... (decoder code from above) ...
pass
def handle_command(cmd):
"""Execute robot action based on IR command."""
if cmd == CMD_FORWARD:
print("Forward")
Motor.Car_Run(100, 100)
elif cmd == CMD_BACK:
print("Back")
Motor.Car_Back(100, 100)
elif cmd == CMD_LEFT:
print("Left")
Motor.Car_Left(80, 80)
elif cmd == CMD_RIGHT:
print("Right")
Motor.Car_Right(80, 80)
elif cmd == CMD_STOP:
print("Stop")
Motor.Car_Stop()
else:
print(f"Unknown: 0x{cmd:02X}")
print("IR Remote Control Ready")
print("Use number pad: 2=fwd, 8=back, 4=left, 6=right, 5=stop")
while True:
result = decode_nec()
if result:
handle_command(result['command'])
Self-Assessment
Quick Check – Did You Get It?
🔹 Why do IR remotes use a 38kHz carrier frequency? 🔹 What is the purpose of the inverse bytes in the NEC protocol? 🔹 How do you distinguish between a '0' bit and a '1' bit in NEC encoding? 🔹 Why might IR reception fail in bright sunlight? 🔹 How would you handle held buttons (repeat codes)?
Research Tasks
-
Protocol comparison: Research the RC5 protocol. How does it differ from NEC?
-
Repeat codes: The NEC protocol sends a different pattern when a button is held. Research how to detect and handle repeat codes.
-
IR transmission: Research how to use an IR LED to transmit commands, turning your robot into a remote control for other devices.
Next Steps
- [[Reference/06-software/state-machine|State Machines]] - Integrate IR with structured control
- [[Reference/07-data-analysis/overview|Data Analysis]] - Log and analyze robot behavior
- Return to Course Overview