Project 3.5: "IR Controlled Car"
🚀 Project 3.5 – IR Controlled Car
🎯 What you’ll learn
- ✅ Goal 1: Control a two‑motor car using an IR remote (Forward/Back/Left/Right)
- ✅ Goal 2: Adjust speed with remote buttons and see clear serial feedback
- ✅ Goal 3: Use pre‑programmed moves and basic obstacle avoidance with simple rules
Key ideas
- Short definition: An IR remote sends codes; the car maps codes to directions and speed.
- Real‑world link: Remote‑controlled cars, TV remotes, and robot toys use IR for simple commands.
🧱 Blocks glossary (used in this project)
- IR receive: Listen for IR codes from a handheld remote.
- Serial print: Show status (direction, speed, errors) on your computer.
- Digital output: Set motor direction pins (ON/OFF).
- PWM output: Set motor speed (0–100% duty cycle).
- Variable: Store speed, codes, and state (like “moving forward”).
- if / else: Choose actions based on the received code.
- Loop: Keep reading commands and controlling the car.
🧰 What you need
| Part | How many? | Pin connection (D1 R32) |
|---|---|---|
| D1 R32 | 1 | USB cable |
| L298N Motor Driver | 1 | ENA → Pin 5 (PWM), IN1 → Pin 23, IN2 → Pin 19 |
| ENB → Pin 18 (PWM), IN3 → Pin 13, IN4 → Pin 21 | ||
| IR Receiver Module | 1 | Signal → Pin 26 |
| TT Motors (left/right) | 2 | Wired to L298N outputs (OUT1/OUT2 left, OUT3/OUT4 right) |
- Use your provided IR hex‑command map (examples):
- Up: 0x18, Down: 0x52, Left: 0x8, Right: 0x5A, OK: 0x1C, 1: 0x45, 2: 0x46, 3: 0x47, 0: 0x19, #: 0xD, *: 0x16
- Power L298N from battery or supply per kit; share GND with D1 R32.
🔌 Wiring tip: Tie D1 R32 GND to L298N GND. Keep IR receiver facing forward with clear line of sight.
📍 Pin map snapshot: We use Pins 5 and 18 for PWM speed; 23,19,13,21 for directions; 26 for IR RX.
✅ Before you start
- Plug in USB and open the serial monitor.
- Test print shows:
print("Ready!") # Confirm serial is working
🎮 Microprojects (5 mini missions)
🎮 Microproject 3.5.1 – IR direction control (basic moves)
Goal: Map Up/Down/Left/Right to motor direction pins.
Blocks used:
- IR receive: Read a code
- Digital output: Set IN1..IN4 for direction
- Serial print: Log which direction runs
Block sequence:
- Setup IR receiver (Pin 26)
- Setup direction pins (23,19,13,21) as outputs
- If code is Up → forward; Down → backward; Left/Right → turn
- Print action
MicroPython code:
# Microproject 3.5.1 – IR direction control
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) # Create IR receiver on Pin 26 with buffer size 8
print("[Init] IR RX on 26 (Up/Down/Left/Right)") # Serial: confirm IR setup
pin23 = machine.Pin(23, machine.Pin.OUT) # IN1 left motor direction pin
pin19 = machine.Pin(19, machine.Pin.OUT) # IN2 left motor direction pin
pin13 = machine.Pin(13, machine.Pin.OUT) # IN3 right motor direction pin
pin21 = machine.Pin(21, machine.Pin.OUT) # IN4 right motor direction pin
print("[Init] Direction pins set: 23,19,13,21") # Serial: confirm direction pins
def stop_all(): # Define a helper to stop movement
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 command loop
if ir_rx.any(): # If an IR code arrived
code = ir_rx.code[0] # Read first code from buffer
print("[IR] Code:", hex(code)) # Show code in hex
if code == 0x18: # If Up arrow
pin23.value(1) # Left IN1 ON (forward)
pin19.value(0) # Left IN2 OFF
pin13.value(1) # Right IN3 ON (forward)
pin21.value(0) # Right IN4 OFF
print("[Move] FORWARD") # Serial: forward
elif code == 0x52: # If Down arrow
pin23.value(0) # Left IN1 OFF
pin19.value(1) # Left IN2 ON (backward)
pin13.value(0) # Right IN3 OFF
pin21.value(1) # Right IN4 ON (backward)
print("[Move] BACKWARD") # Serial: backward
elif code == 0x8: # If Left arrow
pin23.value(0) # Left IN1 OFF
pin19.value(1) # Left IN2 ON (backward)
pin13.value(1) # Right IN3 ON (forward)
pin21.value(0) # Right IN4 OFF
print("[Turn] LEFT") # Serial: left turn
elif code == 0x5A: # If Right arrow
pin23.value(1) # Left IN1 ON (forward)
pin19.value(0) # Left IN2 OFF
pin13.value(0) # Right IN3 OFF
pin21.value(1) # Right IN4 ON (backward)
print("[Turn] RIGHT") # Serial: right turn
else: # If another code
stop_all() # Stop motors
print("[Move] STOP (unknown code)") # Serial: stopped
else: # If no code this cycle
stop_all() # Keep motors stopped
print("[Wait] No IR code") # Serial: waiting
time.sleep_ms(250) # Small delay for readability
Reflection: Your car responds to arrow codes—forward, back, and turns.
Challenge:
- Easy: Add “OK” (0x1C) to stop immediately.
- Harder: Make Left/Right spin in place faster by adjusting PWM (next microproject).
🎮 Microproject 3.5.2 – IR speed control (PWM)
Goal: Use number buttons to set speed (low/medium/high).
Blocks used:
- IR receive: Read digits
- PWM output: Adjust duty cycle
- Serial print: Show speed percentage
Block sequence:
- Setup PWM on ENA (Pin 5) and ENB (Pin 18)
- Map codes 1/2/3 to duty levels
- Apply duty to both motors
- Print speed
MicroPython code:
# Microproject 3.5.2 – Speed control with PWM using IR digits
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("[Init] IR RX on 26 (digits 1/2/3)") # Serial: confirm IR setup
pwm5 = machine.PWM(machine.Pin(5)) # PWM on Pin 5 (ENA left motor)
pwm18 = machine.PWM(machine.Pin(18)) # PWM on Pin 18 (ENB right motor)
pwm5.freq(2000) # Set PWM frequency to 2 kHz (quiet tone)
pwm18.freq(2000) # Same PWM frequency for right motor
print("[Init] PWM freq 2000 Hz on 5 and 18") # Serial: confirm PWM setup
speed = 512 # Start at ~50% (ESP32 duty range 0–1023)
pwm5.duty(speed) # Set initial duty for left motor
pwm18.duty(speed) # Set initial duty for right motor
print("[Speed] Start =", speed) # Serial: initial speed
while True: # Continuous speed control loop
if ir_rx.any(): # If an IR code arrived
code = ir_rx.code[0] # Read first code
print("[IR] Code:", hex(code)) # Show code in hex
if code == 0x45: # If '1'
speed = 350 # Low speed duty
print("[Speed] LOW =", speed) # Serial: low speed
elif code == 0x46: # If '2'
speed = 650 # Medium speed duty
print("[Speed] MED =", speed) # Serial: medium speed
elif code == 0x47: # If '3'
speed = 900 # High speed duty
print("[Speed] HIGH =", speed) # Serial: high speed
elif code == 0x19: # If '0'
speed = 0 # Stop speed
print("[Speed] STOP =", speed) # Serial: stop speed
pwm5.duty(speed) # Apply duty to left motor
pwm18.duty(speed) # Apply duty to right motor
else: # If no code this cycle
print("[Wait] No IR code (speed)") # Serial: waiting
time.sleep_ms(250) # Small delay for readability
Reflection: You set the car’s speed with number buttons—simple and powerful.
Challenge:
- Easy: Add “#” (0xD) to set turbo (100%).
- Harder: Use “*” (0x16) to toggle slow‑mode (cap at 60%).
🎮 Microproject 3.5.3 – Pre‑programmed movements (macros)
Goal: Run short moves on a single button (OK).
Blocks used:
- IR receive: Detect OK
- Digital/PWM: Run a timed movement sequence
- Serial print: Narrate the macro
Block sequence:
- On OK (0x1C) → forward for 1 s
- Turn right for 0.5 s
- Backward for 0.5 s
- Stop and print “Macro done”
MicroPython code:
# Microproject 3.5.3 – Pre-programmed macro on OK
import irremote # Load IR communication library
import machine # Load hardware/PWM and pin library
import time # Load time library
ir_rx = irremote.NEC_RX(26, 8) # IR receiver on Pin 26
print("[Init] IR RX (macro on OK)") # Serial: macro mode
pwm5 = machine.PWM(machine.Pin(5)) # PWM left motor enable (ENA)
pwm18 = machine.PWM(machine.Pin(18)) # PWM right motor enable (ENB)
pwm5.freq(2000) # PWM frequency 2 kHz
pwm18.freq(2000) # PWM frequency 2 kHz
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 motors_forward(): # Helper: forward
pin23.value(1) # Left IN1 ON
pin19.value(0) # Left IN2 OFF
pin13.value(1) # Right IN3 ON
pin21.value(0) # Right IN4 OFF
def motors_backward(): # Helper: backward
pin23.value(0) # Left IN1 OFF
pin19.value(1) # Left IN2 ON
pin13.value(0) # Right IN3 OFF
pin21.value(1) # Right IN4 ON
def motors_right(): # Helper: spin right
pin23.value(1) # Left IN1 ON
pin19.value(0) # Left IN2 OFF
pin13.value(0) # Right IN3 OFF
pin21.value(1) # Right IN4 ON
def motors_stop(): # 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
speed = 700 # Macro speed duty
pwm5.duty(speed) # Set speed left
pwm18.duty(speed) # Set speed right
print("[Speed] Macro duty =", speed) # Serial: macro speed
while True: # Macro trigger loop
if ir_rx.any(): # If code arrived
code = ir_rx.code[0] # Read first code
print("[IR] Code:", hex(code)) # Show code
if code == 0x1C: # If OK button
print("[Macro] Start") # Serial: begin macro
motors_forward() # Step 1: forward
time.sleep_ms(1000) # Run 1 second
motors_right() # Step 2: turn right
time.sleep_ms(500) # Run 0.5 second
motors_backward() # Step 3: backward
time.sleep_ms(500) # Run 0.5 second
motors_stop() # Step 4: stop
print("[Macro] Done") # Serial: macro done
else: # If no code
print("[Wait] No IR code (macro)") # Serial: waiting
time.sleep_ms(250) # Small delay
Reflection: One button runs a whole routine—your first move “combo.”
Challenge:
- Easy: Create a Left‑biased macro (left → forward → stop).
- Harder: Chain two macros: OK runs A, then # runs B.
🎮 Microproject 3.5.4 – Basic obstacle avoidance (manual assist)
Goal: If “Up” is held, allow manual stops with “0”.
Blocks used:
- IR receive: Check Up/0
- Digital/PWM: Move forward or stop
- Variable: State flag (forward = True/False)
Block sequence:
- On Up → forward state True
- On 0 → stop state False
- While True, keep moving or stop
- Print state
MicroPython code:
# Microproject 3.5.4 – Assisted forward with manual stop
import irremote # Load IR communication library
import machine # Load hardware/PWM and pin library
import time # Load time library
ir_rx = irremote.NEC_RX(26, 8) # IR receiver on Pin 26
print("[Init] IR RX (Up=go, 0=stop)") # Serial: assisted mode
pwm5 = machine.PWM(machine.Pin(5)) # PWM left motor
pwm18 = machine.PWM(machine.Pin(18)) # PWM right motor
pwm5.freq(2000) # PWM 2 kHz
pwm18.freq(2000) # PWM 2 kHz
pwm5.duty(700) # Default forward speed
pwm18.duty(700) # Default forward speed
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
forward = False # State: moving forward?
print("[State] forward =", forward) # Serial: initial state
def go_forward(): # Helper: forward pins
pin23.value(1) # Left IN1 ON
pin19.value(0) # Left IN2 OFF
pin13.value(1) # Right IN3 ON
pin21.value(0) # Right IN4 OFF
def stop_all(): # Helper: stop pins
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: # Assisted control loop
if ir_rx.any(): # If a code arrived
code = ir_rx.code[0] # Read code
print("[IR] Code:", hex(code)) # Show code
if code == 0x18: # If Up arrow
forward = True # Set forward state
print("[State] forward =", forward) # Serial: now True
elif code == 0x19: # If '0' digit
forward = False # Clear forward state
print("[State] forward =", forward) # Serial: now False
if forward: # If moving forward
go_forward() # Apply forward pins
print("[Move] FORWARD (assist)") # Serial: forward
else: # If not moving
stop_all() # Stop pins
print("[Move] STOP (assist)") # Serial: stop
time.sleep_ms(250) # Small delay
Reflection: You combined “go” and “stop” into a safe manual assist mode.
Challenge:
- Easy: Add “Down” to set reverse assist.
- Harder: Add a speed toggle using “2” (medium) and “3” (high).
🎮 Microproject 3.5.5 – IR operating modes (driver modes)
Goal: Switch between three modes: Normal, Turbo, Quiet.
Blocks used:
- IR receive: Use #, *, 0
- PWM output: Set different duty caps
- Serial print: Announce current mode
Block sequence:
→ Turbo (100%)
- → Quiet (40%)
- 0 → Stop (0%)
MicroPython code:
# Microproject 3.5.5 – Operating modes: Normal/Turbo/Quiet
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("[Init] IR RX (modes: #, *, 0)") # Serial: mode control
pwm5 = machine.PWM(machine.Pin(5)) # PWM left motor
pwm18 = machine.PWM(machine.Pin(18)) # PWM right motor
pwm5.freq(2000) # PWM 2 kHz
pwm18.freq(2000) # PWM 2 kHz
mode = "NORMAL" # Start in NORMAL mode
speed = 700 # Default duty
pwm5.duty(speed) # Apply duty to left motor
pwm18.duty(speed) # Apply duty to right motor
print("[Mode] NORMAL, duty =", speed) # Serial: mode info
while True: # Mode control loop
if ir_rx.any(): # If a code arrived
code = ir_rx.code[0] # Read the code
print("[IR] Code:", hex(code)) # Show code
if code == 0xD: # If '#'
mode = "TURBO" # Set mode Turbo
speed = 1023 # Duty max
print("[Mode] TURBO, duty =", speed) # Serial: turbo
elif code == 0x16: # If '*'
mode = "QUIET" # Set mode Quiet
speed = 410 # Duty ~40%
print("[Mode] QUIET, duty =", speed) # Serial: quiet
elif code == 0x19: # If '0'
mode = "STOP" # Set mode Stop
speed = 0 # Duty stop
print("[Mode] STOP, duty =", speed) # Serial: stop
pwm5.duty(speed) # Apply duty left
pwm18.duty(speed) # Apply duty right
else: # If no code now
print("[Wait] No IR code (mode)") # Serial: waiting
time.sleep_ms(300) # Small delay
Reflection: Modes make the car feel different—quiet indoor vs. full‑speed outdoor.
Challenge:
- Easy: Add “2” to set Normal (700).
- Harder: Display mode on LCD (if available) using your LCD blocks.
✨ Main project – IR remote car with speed and modes
🔧 Blocks steps (with glossary)
- IR receive: Read arrow/digit/mode codes
- Digital output: Set direction pins (IN1..IN4)
- PWM output: Set speed (ENA/ENB)
- Variable: Track speed and mode
- if / else: Map codes to actions
Block sequence:
- Setup IR RX (Pin 26), direction pins (23,19,13,21), PWM (5,18)
- Arrows: Up/Down/Left/Right set direction pins
- Digits: 1/2/3/0 set speed duty
- Modes: # → Turbo, * → Quiet
- Print clear serial logs for each action
🐍 MicroPython code (mirroring blocks)
# Project 3.5 – IR Remote Car: Directions, Speed, and Modes
import irremote # Load IR communication library
import machine # Load hardware/PWM and pin library
import time # Load time library
ir_rx = irremote.NEC_RX(26, 8) # IR receiver on Pin 26
print("[Init] IR RX on 26") # Serial: IR init
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("[Init] Direction pins: 23,19,13,21") # Serial: direction pins set
pwm5 = machine.PWM(machine.Pin(5)) # PWM on Pin 5 (ENA left)
pwm18 = machine.PWM(machine.Pin(18)) # PWM on Pin 18 (ENB right)
pwm5.freq(2000) # PWM frequency 2 kHz (quiet)
pwm18.freq(2000) # PWM frequency 2 kHz
print("[Init] PWM freq set to 2000 Hz") # Serial: PWM init
speed = 650 # Start medium duty
mode = "NORMAL" # Start in NORMAL mode
pwm5.duty(speed) # Apply start duty left
pwm18.duty(speed) # Apply start duty right
print("[State] mode:", mode, "| speed:", speed) # Serial: initial state
def stop_all(): # Helper: stop movement
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: # Main control loop
if ir_rx.any(): # If a code arrived
code = ir_rx.code[0] # Read first buffered code
print("[IR] Code:", hex(code)) # Show code in hex
if code == 0x18: # Up → 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("[Move] FORWARD") # Serial: forward
elif code == 0x52: # Down → 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("[Move] BACKWARD") # Serial: backward
elif code == 0x8: # Left → turn left
pin23.value(0) # Left backward ON for spin
pin19.value(1) # Left backward ON
pin13.value(1) # Right forward ON
pin21.value(0) # Right backward OFF
print("[Turn] LEFT") # Serial: turn left
elif code == 0x5A: # Right → turn 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("[Turn] RIGHT") # Serial: turn right
elif code == 0x1C: # OK → stop
stop_all() # Stop all directions
print("[Move] STOP (OK)") # Serial: stop
elif code == 0x45: # '1' → low speed
speed = 350 # Duty low
print("[Speed] LOW =", speed) # Serial: speed set
elif code == 0x46: # '2' → medium speed
speed = 650 # Duty medium
print("[Speed] MED =", speed) # Serial: speed set
elif code == 0x47: # '3' → high speed
speed = 900 # Duty high
print("[Speed] HIGH =", speed) # Serial: speed set
elif code == 0x19: # '0' → speed stop
speed = 0 # Duty zero
stop_all() # Stop directions too
print("[Speed] STOP =", speed) # Serial: speed stop
elif code == 0xD: # '#' → turbo mode
mode = "TURBO" # Set mode turbo
speed = 1023 # Duty max
print("[Mode] TURBO | speed =", speed)# Serial: turbo
elif code == 0x16: # '*' → quiet mode
mode = "QUIET" # Set mode quiet
speed = 410 # Duty ~40%
print("[Mode] QUIET | speed =", speed)# Serial: quiet
else: # Unknown code
print("[Warn] Unknown code") # Serial: warn
pwm5.duty(speed) # Apply duty left
pwm18.duty(speed) # Apply duty right
else: # If no code this cycle
print("[Wait] No IR code") # Serial: waiting
time.sleep_ms(250) # Loop delay for readability
📖 External explanation
- What it teaches: Mapping IR codes to directions, speeds, and modes—your first full remote car controller.
- Why it works: Direction pins choose forward/back for each motor; PWM sets speed. The IR code decides which action to run.
- Key concept: Small hex codes (like 0x18) trigger big behaviors when mapped cleanly.
✨ Story time
Your robot just learned to drive with a TV‑style remote. Tap arrows to steer, numbers to set speed, and hash/star to change personality—turbo or quiet.
🕵️ Debugging (2)
🐞 Debugging 3.5.A – Lost commands
Problem: Pressing a button sometimes does nothing.
Clues: Serial shows “No IR code” often; actions feel delayed.
Broken code:
if ir_rx.any():
pass # Missing read and action; loop runs too fast without delay
Fixed code:
if ir_rx.any(): # Check code exists
code = ir_rx.code[0] # Read first buffered code
print("[IR] Code:", hex(code)) # Log the code
# Handle action here (direction/speed) # Do the mapped behavior
time.sleep_ms(250) # Add a small delay for stability
Why it works: Reading the buffer and adding a short delay reduces missed events.
Avoid next time: Always read code[0] and keep logs compact.
🐞 Debugging 3.5.B – Slow response
Problem: Car feels sluggish after button presses.
Clues: Very large delays or too many prints clog the loop.
Broken code:
time.sleep_ms(1200) # Delay is too large for responsive driving
print("Lots of text...") # Excessive printing every loop
Fixed code:
time.sleep_ms(250) # Keep delays short (150–300 ms)
print("[Move] FORWARD") # Print single, clear status lines
Why it works: Short delays + concise logs keep the control loop snappy.
Avoid next time: Avoid long sleeps and repeated verbose prints.
✅ Final checklist
- Up/Down/Left/Right control directions correctly
- Numbers set speed levels (low/med/high/stop)
- Modes (# turbo, * quiet) change behavior
- Serial logs are clear and short
- Motors stop safely on OK or 0
📚 Extras
- 🧠 Student tip: Practice gentle button taps—single presses control better than holding.
- 🧑🏫 Instructor tip: Verify GND is shared between D1 R32 and L298N before testing.
- 📖 Glossary:
- PWM (duty): How much power the motor sees (0–1023).
- Direction pins: Choose forward/back for each motor.
- Hex code: Base‑16 number used by the IR remote protocol.
- 💡 Mini tips:
- Keep IR receiver away from bright lights.
- Align the remote to the sensor window.
- Test directions with wheels lifted first.