Project 3.5: "IR Controlled Car 2.0"
🚀 Project 3.5 – IR Car Controlled by Joystick Buttons
🎯 What you’ll learn
- ✅ Goal 1: Use joystick buttons A–F to send IR commands from a controller board
- ✅ Goal 2: Receive IR commands on the car board and run direction/speed actions
- ✅ Goal 3: Build simple modes and a macro using only A–F (no joystick axes)
Key ideas
- Short definition: Buttons on the joystick send IR codes; the car decodes them and moves.
- Real‑world link: Remote controllers use buttons to command robots and RC cars.
🧱 Blocks glossary (used in this project)
- Digital input (pull‑up): Reads button state (pressed = 0).
- IR send: Transmits an infrared code from the controller board.
- IR receive: Listens for infrared codes on the car board.
- Digital output: Sets motor direction pins (ON/OFF).
- PWM output: Controls motor speed by duty cycle.
- Variable: Stores mode, speed, and the last code.
- if / else: Maps a code to an action (direction, speed, macro).
- Loop: Repeats reading/sending/acting continuously.
🧰 What you need
| Part | How many? | Pin connection (D1 R32) |
|---|---|---|
| D1 R32 (Controller with joystick) | 1 | Joystick Shield on top; IR Transmitter → Pin 26 |
| D1 R32 (Car) | 1 | IR Receiver → Pin 26 |
| L298N Motor Driver | 1 | ENA → Pin 5 (PWM), IN1 → Pin 23, IN2 → Pin 19 |
| ENB → Pin 18 (PWM), IN3 → Pin 13, IN4 → Pin 21 | ||
| TT Motors | 2 | Wired to L298N outputs |
- Joystick buttons: A(26), B(25), C(17), D(16), E(27), F(14) on the controller board.
- We’ll map A–F to 6 IR codes (e.g., forward, backward, left, right, speed up, stop).
🔌 Wiring tip: Share GND between boards and L298N. Aim the IR transmitter at the receiver (20–50 cm).
📍 Pin map snapshot: Controller uses joystick buttons (26,25,17,16,27,14) and IR TX on 26 (module). Car uses IR RX on 26, motors on 5,18,23,19,13,21.
✅ Before you start
- Open two serial monitors: one for the controller board, one for the car board.
- Test print shows:
print("Ready!") # Confirm serial is working
🎮 Microprojects (5 mini missions)
🎮 Microproject 3.5.1 – Controller: send IR codes with buttons A–F
Goal: Press A–F to send six distinct IR codes.
Blocks used:
- Digital input (pull‑up): Read A–F
- IR send: Transmit code per button
- Serial print: Log which code was sent
Block sequence:
- Setup A–F pins with pull‑up
- Setup IR transmitter on Pin 26
- On each press, send a unique code
- Print action and delay to debounce
MicroPython code (Controller):
# Microproject 3.5.1 – Controller: Send IR codes with joystick buttons A–F
import machine # Load hardware/pin library
import irremote # Load IR communication library
import time # Load time library for delays
pin26 = machine.Pin(26, machine.Pin.IN, machine.Pin.PULL_UP) # Button A (active LOW)
pin25 = machine.Pin(25, machine.Pin.IN, machine.Pin.PULL_UP) # Button B (active LOW)
pin17 = machine.Pin(17, machine.Pin.IN, machine.Pin.PULL_UP) # Button C (active LOW)
pin16 = machine.Pin(16, machine.Pin.IN, machine.Pin.PULL_UP) # Button D (active LOW)
pin27 = machine.Pin(27, machine.Pin.IN, machine.Pin.PULL_UP) # Button E (active LOW)
pin14 = machine.Pin(14, machine.Pin.IN, machine.Pin.PULL_UP) # Button F (active LOW)
ir_tx = irremote.NEC_TX(26, False, 100) # IR transmitter on Pin 26, power 100%
print("[Controller] IR TX ready on 26 | Buttons A–F active") # Confirm setup
addr = 0x00 # Use simple address 0x00
ctrl = 0x00 # Use simple control 0x00
while True: # Main sending loop
if pin26.value() == 0: # If A pressed
ir_tx.transmit(addr, 0x18, ctrl) # Send code 0x18 (forward)
print("[Send] A → CMD=0x18 (FORWARD)") # Log action
time.sleep_ms(250) # Debounce delay
if pin25.value() == 0: # If B pressed
ir_tx.transmit(addr, 0x52, ctrl) # Send code 0x52 (backward)
print("[Send] B → CMD=0x52 (BACKWARD)") # Log action
time.sleep_ms(250) # Debounce delay
if pin17.value() == 0: # If C pressed
ir_tx.transmit(addr, 0x08, ctrl) # Send code 0x08 (left)
print("[Send] C → CMD=0x08 (LEFT)") # Log action
time.sleep_ms(250) # Debounce delay
if pin16.value() == 0: # If D pressed
ir_tx.transmit(addr, 0x5A, ctrl) # Send code 0x5A (right)
print("[Send] D → CMD=0x5A (RIGHT)") # Log action
time.sleep_ms(250) # Debounce delay
if pin27.value() == 0: # If E pressed
ir_tx.transmit(addr, 0x47, ctrl) # Send code 0x47 (speed up)
print("[Send] E → CMD=0x47 (SPEED +)") # Log action
time.sleep_ms(250) # Debounce delay
if pin14.value() == 0: # If F pressed
ir_tx.transmit(addr, 0x19, ctrl) # Send code 0x19 (stop)
print("[Send] F → CMD=0x19 (STOP)") # Log action
time.sleep_ms(250) # Debounce delay
Reflection: Your joystick buttons now act like a remote—each press beams a command.
Challenge:
- Easy: Swap E/F meanings to match your preference.
- Harder: Add a long‑press (hold > 1 s) to send a different code (e.g., turbo).
🎮 Microproject 3.5.2 – Car: decode A–D as directions
Goal: Map received codes to forward/back/left/right motor actions.
Blocks used:
- IR receive: Listen for codes
- Digital output: Set IN1..IN4
- Serial print: Log direction
Block sequence:
- Setup IR receiver on Pin 26
- Setup direction pins (23,19,13,21)
- Map 0x18/0x52/0x08/0x5A to moves
- Stop on unknown
MicroPython code (Car):
# Microproject 3.5.2 – Car: Decode directions from IR
import irremote # Load IR communication library
import machine # Load hardware pin library
import time # Load time library for delays
ir_rx = irremote.NEC_RX(26, 8) # IR receiver on Pin 26 with buffer 8
print("[Car] IR RX ready on 26") # Confirm IR setup
pin23 = machine.Pin(23, machine.Pin.OUT) # IN1 left motor
pin19 = machine.Pin(19, machine.Pin.OUT) # IN2 left motor
pin13 = machine.Pin(13, machine.Pin.OUT) # IN3 right motor
pin21 = machine.Pin(21, machine.Pin.OUT) # IN4 right motor
print("[Car] Direction pins ready: 23,19,13,21") # Confirm pins
def stop_all(): # Helper: stop motors
pin23.value(0) # Left IN1 OFF
pin19.value(0) # Left IN2 OFF
pin13.value(0) # Right IN3 OFF
pin21.value(0) # Right IN4 OFF
while True: # Continuous control loop
if ir_rx.any(): # If a code arrived
code = ir_rx.code[0] # Read the first buffered code
print("[IR] Code:", hex(code)) # Show code in hex
if code == 0x18: # Forward code
pin23.value(1) # Left forward ON
pin19.value(0) # Left backward OFF
pin13.value(1) # Right forward ON
pin21.value(0) # Right backward OFF
print("[Move] FORWARD") # Log move
elif code == 0x52: # Backward code
pin23.value(0) # Left forward OFF
pin19.value(1) # Left backward ON
pin13.value(0) # Right forward OFF
pin21.value(1) # Right backward ON
print("[Move] BACKWARD") # Log move
elif code == 0x08: # Left code
pin23.value(0) # Left backward ON (spin)
pin19.value(1) # Left backward ON
pin13.value(1) # Right forward ON
pin21.value(0) # Right backward OFF
print("[Turn] LEFT") # Log turn
elif code == 0x5A: # Right code
pin23.value(1) # Left forward ON
pin19.value(0) # Left backward OFF
pin13.value(0) # Right backward ON
pin21.value(1) # Right backward ON
print("[Turn] RIGHT") # Log turn
else: # Unknown code
stop_all() # Stop motors
print("[Move] STOP (unknown)") # Log stop
else: # No code this cycle
print("[Wait] No IR code") # Waiting message
stop_all() # Keep safe stop
time.sleep_ms(250) # Readability delay
Reflection: The car reacts to your button beams—simple, clean control.
Challenge:
- Easy: Add “centered stop” after 2 seconds without commands.
- Harder: Make left/right turns shorter pulses (0.3 s), else stop.
🎮 Microproject 3.5.3 – Car: speed control with E/F
Goal: Use E (speed up) and F (stop) codes to control PWM speed.
Blocks used:
- IR receive: Read E/F codes
- PWM output: Adjust duty on ENA/ENB
- Serial print: Show speed duty
Block sequence:
- Setup PWM on Pin 5 and Pin 18
- On 0x47 (E) increase duty by step
- On 0x19 (F) set duty to 0 and stop
- Print speed
MicroPython code (Car):
# Microproject 3.5.3 – Car: Speed control using E (up) and F (stop)
import irremote # Load IR communication library
import machine # Load hardware/PWM library
import time # Load time library
ir_rx = irremote.NEC_RX(26, 8) # IR receiver on Pin 26
print("[Car] IR RX ready for speed E/F") # Confirm IR setup
pwm5 = machine.PWM(machine.Pin(5)) # PWM enable A (left motor)
pwm18 = machine.PWM(machine.Pin(18)) # PWM enable B (right motor)
pwm5.freq(2000) # PWM frequency 2000 Hz
pwm18.freq(2000) # PWM frequency 2000 Hz
print("[Car] PWM set at 2000 Hz (5,18)") # Confirm PWM setup
speed = 500 # Start duty ~50%
pwm5.duty(speed) # Apply left speed
pwm18.duty(speed) # Apply right speed
print("[Speed] Start =", speed) # Log start speed
while True: # Speed control loop
if ir_rx.any(): # If a code arrived
code = ir_rx.code[0] # Read first code
print("[IR] Code:", hex(code)) # Show code
if code == 0x47: # If E (speed up)
speed = speed + 100 # Increase duty by 100
if speed > 1023: # If above max
speed = 1023 # Clamp to max
print("[Speed] UP =", speed) # Log new speed
elif code == 0x19: # If F (stop)
speed = 0 # Duty to 0
print("[Speed] STOP =", speed) # Log stop
pwm5.duty(speed) # Update left speed
pwm18.duty(speed) # Update right speed
else: # No code now
print("[Wait] No IR code (speed)") # Waiting message
time.sleep_ms(250) # Small delay
Reflection: Two buttons can manage the car’s energy—fast or full stop.
Challenge:
- Easy: Add a minimum speed (don’t go below 300).
- Harder: Use C/D to fine‑tune speed up/down by 50.
🎮 Microproject 3.5.4 – Car: pre‑programmed movement (macro from A)
Goal: When 0x18 (A) arrives, run a short routine: forward→right→stop.
Blocks used:
- IR receive: Detect trigger code
- Digital/PWM: Execute timed sequence
- Serial print: Narrate steps
Block sequence:
- On 0x18, forward for 0.8 s
- Turn right for 0.5 s
- Stop and print “Macro done”
MicroPython code (Car):
# Microproject 3.5.4 – Car: Macro triggered by A (0x18)
import irremote # Load IR communication library
import machine # Load hardware pin/PWM library
import time # Load time library
ir_rx = irremote.NEC_RX(26, 8) # IR receiver on Pin 26
print("[Car] Macro trigger on A(0x18)") # Confirm macro mode
pin23 = machine.Pin(23, machine.Pin.OUT) # IN1 left
pin19 = machine.Pin(19, machine.Pin.OUT) # IN2 left
pin13 = machine.Pin(13, machine.Pin.OUT) # IN3 right
pin21 = machine.Pin(21, machine.Pin.OUT) # IN4 right
def forward(): # Forward helper
pin23.value(1) # Left forward ON
pin19.value(0) # Left backward OFF
pin13.value(1) # Right forward ON
pin21.value(0) # Right backward OFF
def right(): # Right helper
pin23.value(1) # Left forward ON
pin19.value(0) # Left backward OFF
pin13.value(0) # Right backward ON
pin21.value(1) # Right backward ON
def stop_all(): # Stop helper
pin23.value(0) # Left forward OFF
pin19.value(0) # Left backward OFF
pin13.value(0) # Right forward OFF
pin21.value(0) # Right backward OFF
while True: # Macro loop
if ir_rx.any(): # If a code arrived
code = ir_rx.code[0] # Read the code
print("[IR] Code:", hex(code)) # Show hex
if code == 0x18: # If A (forward trigger)
print("[Macro] Start") # Begin macro
forward() # Step 1 forward
time.sleep_ms(800) # Run 0.8 s
right() # Step 2 right
time.sleep_ms(500) # Run 0.5 s
stop_all() # Step 3 stop
print("[Macro] Done") # End macro
else: # If no code
print("[Wait] No IR code (macro)") # Waiting message
time.sleep_ms(250) # Small delay
Reflection: One press runs a move combo—handy for teaching patterns.
Challenge:
- Easy: Trigger another macro with B (0x52).
- Harder: Chain two macros: A then C within 2 seconds.
🎮 Microproject 3.5.5 – Car: driver modes with B/E/F
Goal: Switch modes: Normal (B), Boost (E), Stop mode (F).
Blocks used:
- IR receive: Detect mode codes
- PWM output: Set duty caps per mode
- Serial print: Announce mode
Block sequence:
- On B (0x52) → Normal speed (650)
- On E (0x47) → Boost speed (900)
- On F (0x19) → Stop (0)
MicroPython code (Car):
# Microproject 3.5.5 – Car: Driver modes with B/E/F
import irremote # Load IR communication library
import machine # Load hardware/PWM library
import time # Load time library
ir_rx = irremote.NEC_RX(26, 8) # IR receiver on Pin 26
print("[Car] Modes: B=Normal, E=Boost, F=Stop") # Mode info
pwm5 = machine.PWM(machine.Pin(5)) # PWM enable A
pwm18 = machine.PWM(machine.Pin(18)) # PWM enable B
pwm5.freq(2000) # 2000 Hz PWM
pwm18.freq(2000) # 2000 Hz PWM
mode = "NORMAL" # Start mode
speed = 650 # Start speed
pwm5.duty(speed) # Apply left duty
pwm18.duty(speed) # Apply right duty
print("[Mode] NORMAL | speed =", speed) # Log start state
while True: # Mode control loop
if ir_rx.any(): # If a code arrived
code = ir_rx.code[0] # Read code
print("[IR] Code:", hex(code)) # Show hex code
if code == 0x52: # B → Normal
mode = "NORMAL" # Set Normal
speed = 650 # Duty medium
print("[Mode] NORMAL | speed =", speed) # Log
elif code == 0x47: # E → Boost
mode = "BOOST" # Set Boost
speed = 900 # Duty high
print("[Mode] BOOST | speed =", speed) # Log
elif code == 0x19: # F → Stop
mode = "STOP" # Set Stop
speed = 0 # Duty 0
print("[Mode] STOP | speed =", speed) # Log
pwm5.duty(speed) # Update left duty
pwm18.duty(speed) # Update right duty
else: # No code this cycle
print("[Wait] No IR code (mode)") # Waiting message
time.sleep_ms(300) # Small delay
Reflection: Modes change the car’s personality—calm, fast, or halted.
Challenge:
- Easy: Add C (0x08) for “Slow” mode (400).
- Harder: Display mode on LCD if attached (Project 3.3 blocks).
✨ Main project – Joystick buttons controlling an IR car
🔧 Blocks steps (with glossary)
- Digital input (pull‑up): Read A–F on the controller
- IR send: Transmit code per button
- IR receive: Decode codes on the car
- Digital output: Apply direction pins
- PWM output: Adjust speed and modes
- Serial print: Log sends and actions
Block sequence:
- Controller: Setup A–F (pull‑up) and IR TX on 26 → send codes 0x18,0x52,0x08,0x5A,0x47,0x19.
- Car: Setup IR RX on 26, direction pins (23,19,13,21), PWM (5,18).
- Car: Map codes to actions (forward/back/left/right, speed up, stop).
- Car: Provide modes and macro based on codes.
- Keep clear serial messages at every step.
🐍 MicroPython code (mirroring blocks)
# Main Project 3.5 – Joystick Buttons → IR → Car Control
# --- Controller Board (send A–F as IR) ---
import machine # Load hardware/pin library
import irremote # Load IR communication library
import time # Load time library
pin26 = machine.Pin(26, machine.Pin.IN, machine.Pin.PULL_UP) # A button input
pin25 = machine.Pin(25, machine.Pin.IN, machine.Pin.PULL_UP) # B button input
pin17 = machine.Pin(17, machine.Pin.IN, machine.Pin.PULL_UP) # C button input
pin16 = machine.Pin(16, machine.Pin.IN, machine.Pin.PULL_UP) # D button input
pin27 = machine.Pin(27, machine.Pin.IN, machine.Pin.PULL_UP) # E button input
pin14 = machine.Pin(14, machine.Pin.IN, machine.Pin.PULL_UP) # F button input
ir_tx = irremote.NEC_TX(26, False, 100) # IR TX on pin 26
print("[Controller] Ready A–F → IR") # Controller init log
addr = 0x00 # Address code
ctrl = 0x00 # Control code
# Send once example (press to transmit)
if pin26.value() == 0: # If A pressed
ir_tx.transmit(addr, 0x18, ctrl) # Send forward code
print("[Controller] A → 0x18") # Log send
# --- Car Board (decode IR and drive) ---
ir_rx = irremote.NEC_RX(26, 8) # IR RX on pin 26
print("[Car] IR RX ready") # Car init log
pin23 = machine.Pin(23, machine.Pin.OUT) # IN1 left
pin19 = machine.Pin(19, machine.Pin.OUT) # IN2 left
pin13 = machine.Pin(13, machine.Pin.OUT) # IN3 right
pin21 = machine.Pin(21, machine.Pin.OUT) # IN4 right
print("[Car] Direction pins set") # Pins ready log
pwm5 = machine.PWM(machine.Pin(5)) # ENA left PWM
pwm18 = machine.PWM(machine.Pin(18)) # ENB right PWM
pwm5.freq(2000) # PWM freq set
pwm18.freq(2000) # PWM freq set
print("[Car] PWM 2000 Hz set") # PWM init log
speed = 650 # Start speed
pwm5.duty(speed) # Apply left speed
pwm18.duty(speed) # Apply right speed
print("[Car] Speed =", speed) # Speed log
def stop_all(): # Helper stop
pin23.value(0) # Left IN1 OFF
pin19.value(0) # Left IN2 OFF
pin13.value(0) # Right IN3 OFF
pin21.value(0) # Right IN4 OFF
if ir_rx.any(): # If any IR code
code = ir_rx.code[0] # Read first code
print("[Car] Code:", hex(code)) # Show code
if code == 0x18: # Forward
pin23.value(1) # Left forward ON
pin19.value(0) # Left backward OFF
pin13.value(1) # Right forward ON
pin21.value(0) # Right backward OFF
print("[Car] FORWARD") # Log move
elif code == 0x52: # Backward
pin23.value(0) # Left forward OFF
pin19.value(1) # Left backward ON
pin13.value(0) # Right forward OFF
pin21.value(1) # Right backward ON
print("[Car] BACKWARD") # Log move
elif code == 0x08: # Left
pin23.value(0) # Left backward ON
pin19.value(1) # Left backward ON
pin13.value(1) # Right forward ON
pin21.value(0) # Right backward OFF
print("[Car] LEFT") # Log turn
elif code == 0x5A: # Right
pin23.value(1) # Left forward ON
pin19.value(0) # Left backward OFF
pin13.value(0) # Right backward ON
pin21.value(1) # Right backward ON
print("[Car] RIGHT") # Log turn
elif code == 0x47: # Speed up
speed = speed + 100 # Increase duty
if speed > 1023: # Clamp max
speed = 1023 # Max duty
pwm5.duty(speed) # Update left
pwm18.duty(speed) # Update right
print("[Car] SPEED UP →", speed) # Log speed
elif code == 0x19: # Stop
speed = 0 # Duty zero
pwm5.duty(speed) # Update left
pwm18.duty(speed) # Update right
stop_all() # Stop motors
print("[Car] STOP") # Log stop
else: # Unknown
stop_all() # Safe stop
print("[Car] UNKNOWN") # Log unknown
📖 External explanation
- What it teaches: Turning button presses into wireless commands, and decoding them for motion and speed.
- Why it works: Buttons (pull‑up) give clean digital events; IR carries small hex codes; the car maps codes to actions.
- Key concept: Clear code‑to‑action mapping makes reliable remote control.
✨ Story time
Your joystick becomes a beacon: A–F are signals, the car interprets and moves. It’s like piloting a rover with a six‑button mission pad.
🕵️ Debugging (2)
🐞 Debugging 3.5.A – Lost commands between boards
Problem: Some presses don’t trigger the car.
Clues: Controller logs “Send” but car shows “No IR code”.
Broken code:
ir_tx.transmit(addr, 0x18, ctrl) # Send without debounce or spacing
Fixed code:
ir_tx.transmit(addr, 0x18, ctrl) # Send command
time.sleep_ms(250) # Add spacing to avoid overlaps
Why it works: Short spacing gives the receiver time to catch the signal.
Avoid next time: Debounce button presses and keep delays small but present.
🐞 Debugging 3.5.B – Slow response on the car
Problem: The car reacts late or stutters.
Clues: Very long delays or excessive printing.
Broken code:
time.sleep_ms(1200) # Too long loop delay
print("Spam spam spam") # Too many logs per cycle
Fixed code:
time.sleep_ms(250) # Keep delays short (150–300 ms)
print("[Move] FORWARD") # One concise status line
Why it works: Short delays and focused logs keep the control loop responsive.
Avoid next time: Avoid long sleeps and repeated verbose prints.
✅ Final checklist
- Controller sends IR on A–F with clear logs
- Car decodes directions (A–D) correctly
- Car changes speed with E and stops with F
- Macro runs when A triggers it (if implemented)
- Serial messages are readable and helpful
📚 Extras
- 🧠 Student tip: Point the controller directly at the car; keep a steady arm for reliable sends.
- 🧑🏫 Instructor tip: Test each A–F mapping in place before driving; verify GND is shared.
- 📖 Glossary:
- Pull‑up: Keeps an input HIGH until a button press pulls it LOW.
- IR code: A small hex value sent by light to communicate commands.
- Duty (PWM): Percentage of time power is ON—controls motor speed.
- 💡 Mini tips:
- Use short, distinct presses; avoid holding multiple buttons.
- Keep IR modules aligned and at a stable distance.
- Start at medium speed; increase once direction works.