Skip to content

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:

Button Press → Encode → Modulate (38kHz) → IR LED → Air → IR Receiver → Demodulate → Decode

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 IR Protocol 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

  1. Protocol comparison: Research the RC5 protocol. How does it differ from NEC?

  2. Repeat codes: The NEC protocol sends a different pattern when a button is held. Research how to detect and handle repeat codes.

  3. 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