๐Ÿค–

Build an RC Car with Python

A step-by-step coding guide for high school students โ€” from zero to driving your own robot!

๐Ÿ Python 3 ๐Ÿ“ Raspberry Pi โšก GPIO ๐ŸŽฎ 10 Steps

โš™๏ธ What You'll Need

๐Ÿ“
Raspberry Pi
Any model (3B+ or 4 recommended)
๐Ÿ”Œ
L298N Motor Driver
Controls your motors from GPIO
โš™๏ธ
2ร— DC Motors + Wheels
Left and right drive motors
๐Ÿ”‹
Battery Pack
6โ€“9V for the motors
๐Ÿ
Python 3 + RPi.GPIO
Pre-installed on Raspberry Pi OS
๐ŸŒก๏ธ
HC-SR04 Sensor (Step 9)
Ultrasonic distance sensor

๐Ÿ›’ Your Shopping List

๐Ÿ’ฐ ~$90โ€“120 total
๐Ÿ“Raspberry Pi 4 (2GB)
The brain of your robot. Runs Python and controls all the GPIO pins.
๐Ÿ“ฆ Amazon ยท Adafruit ยท PiShop
๐Ÿ’พMicroSD Card (32GB, Class 10)
Stores Raspberry Pi OS and all your Python files. Needs to be at least 16GB.
๐Ÿ“ฆ Amazon ยท Best Buy ยท Walmart
๐Ÿ”ŒUSB-C Power Supply (5V 3A)
Powers the Pi. Must be 5V/3A โ€” underpowering causes crashes and weird bugs!
๐Ÿ“ฆ Amazon ยท Adafruit ยท SparkFun
๐Ÿš—2WD Robot Car Chassis Kit
Includes the frame, 2 DC motors, wheels, and screws. A huge time-saver for beginners!
๐Ÿ“ฆ Amazon โ€” search "2WD robot chassis kit"
โšกL298N Motor Driver Module
Translates the Pi's tiny GPIO signals into the higher voltage the motors need. Required!
๐Ÿ“ฆ Amazon ยท SparkFun ยท Adafruit
๐Ÿ”‹4ร—AA Battery Holder (with switch)
Powers the L298N motor driver at ~6V. Get one with an on/off switch for safety!
๐Ÿ“ฆ Amazon ยท local electronics store
๐ŸชซAA Batteries (4-pack)
Alkaline batteries work great. Rechargeable NiMH are more eco-friendly long-term.
๐Ÿ“ฆ Any grocery or drug store
๐Ÿ“กHC-SR04 Ultrasonic Sensor
Gives your car "eyes" to detect obstacles. Used in Steps 8 & 9. Very cheap!
๐Ÿ“ฆ Amazon ยท Adafruit ยท SparkFun
๐Ÿ”—Jumper Wires (M-F & M-M, 40pcs each)
Connect the Pi GPIO pins to the motor driver and sensor. Buy both types!
๐Ÿ“ฆ Amazon ยท any beginner electronics kit
๐ŸงฉHalf-size Breadboard
Great for prototyping sensor connections without soldering. Highly recommended!
๐Ÿ“ฆ Amazon ยท Adafruit ยท SparkFun
๐Ÿ”‹USB Power Bank (5000 mAh+)
Powers the Pi wirelessly โ€” so your car can drive around without a cable attached!
๐Ÿ“ฆ Amazon ยท Best Buy
๐Ÿ›ก๏ธRaspberry Pi Case
Protects your Pi. Look for one with GPIO access holes so wires can still plug in.
๐Ÿ“ฆ Amazon ยท Adafruit
๐Ÿช
Best places to shop: Amazon (fast & cheap), Adafruit.com (tutorials included with every product), SparkFun.com (very beginner-friendly), or your local electronics/hobby store. Many schools have maker labs with parts you can borrow โ€” ask your teacher!

๐Ÿ” Python Decoder โ€” What Does the Code Mean?

Every code symbol explained in plain English. Bookmark this before you start!

๐Ÿ“ฆimport — Borrow pre-built tools
import RPi.GPIO as GPIO import time
Python can't do everything on its own — you import a library to add new abilities. Think of it like a toolbox: you grab the tool you need, you don't build it from scratch. The as GPIO part gives the library a shorter nickname so you type less.
From this tutorial: import RPi.GPIO as GPIO
โ†’ "Load the Raspberry Pi GPIO library and call it GPIO"
๐Ÿท๏ธvariable = value — Store information
speed = 75 name = "robot" is_on = True
A variable is a labelled box that holds a value. You create one by writing name = value. The label (left) can be anything you like. The value (right) can be a number, text, or True/False. Python figures out the type automatically — no need to declare it!
From this tutorial: MOTOR_A_EN = 12
โ†’ "Remember that pin 12 controls Motor A's speed"
๐Ÿ”ฅALL_CAPS — A value that never changes
MOTOR_A_IN1 = 17 SAFE_DISTANCE = 20
ALL_CAPS is a naming convention for constants — values you decide at the start and never change. Python doesn't enforce this, but it's a signal to anyone reading your code: "don't touch this value, it's fixed." Keeping constants at the top of your file makes them easy to find and tweak.
From this tutorial: SAFE_DISTANCE = 20
โ†’ "Stop if closer than 20 cm — don't change mid-program"
๐Ÿ’ฌ# comment — A note for humans
# This is a comment speed = 75 # 75% motor speed
Anything after a # is completely ignored by Python — it's purely a human note. Use comments to explain why you wrote something, not just what it does. Good comments are one of the most important habits you can build as a programmer.
From this tutorial: # GPIO 17 โ†’ Forward signal
โ†’ "Remind yourself which physical wire goes here"
๐Ÿงฐdef — Create a reusable block of code
def say_hello(): print("Hello!") say_hello() # runs it
def defines a function — a named, reusable recipe. You write the recipe once, then "call" it by typing its name with parentheses. Functions keep your code tidy and follow the DRY rule: Don't Repeat Yourself. The indented lines underneath are the function's body — indentation is mandatory in Python!
From this tutorial: def move_forward(pwm_a, pwm_b):
โ†’ "A recipe called move_forward that needs two ingredients"
๐Ÿ“จParameters — Ingredients a function needs
def move_forward(pwm_a, pwm_b, speed=75): ...
Parameters are the inputs a function expects, listed inside the parentheses. speed=75 is a default parameter — if you don't provide a speed, Python uses 75 automatically. When you call the function, you pass in actual values: move_forward(a, b, speed=50).
From this tutorial: move_forward(pwm_a, pwm_b, speed=70)
โ†’ "Run the recipe, using these specific ingredients"
๐Ÿ”for loop — Do this for each item
for pin in [17, 18, 12]: GPIO.setup(pin, GPIO.OUT)
A for loop runs the same code once for each item in a list. The variable after for (here: pin) automatically takes on each value in turn: first 17, then 18, then 12. This replaces writing the same line three separate times.
From this tutorial: for pin in ALL_PINS: GPIO.setup(pin, OUT)
โ†’ "Set up every single pin in one go, no copy-paste"
โ™พ๏ธwhile True — Loop forever until told to stop
while True: listen_for_keys() if quit_pressed: break # exits the loop
while True: runs its body over and over, forever. This is very common in hardware/game code where you want the program to keep running until a human decides to stop. break instantly exits the loop when you're ready to quit.
From this tutorial: keyboard control loop
โ†’ "Keep checking keys 10 times/sec until Q is pressed"
๐Ÿค”if / elif / else — Make a decision
if key == 'up': go_forward() elif key == 'down': go_backward() else: stop()
Python checks each condition in order, top to bottom, and runs only the first one that's true. elif means "else if" โ€” another condition to try. else is the catch-all: "none of the above".
From this tutorial: keyboard_control function
โ†’ "Check each arrow key, do the matching action, otherwise stop"
๐Ÿ›‘return — Send a result back
def setup_pwm(): pwm_a = GPIO.PWM(12, 100) pwm_b = GPIO.PWM(13, 100) return pwm_a, pwm_b a, b = setup_pwm()
return sends a value (or multiple values!) back to whoever called the function. It's like asking someone to go to the store โ€” return is them coming back with the groceries. The caller catches the result and stores it: a, b = setup_pwm().
From this tutorial: pwm_a, pwm_b = setup_pwm()
โ†’ "Run setup and give me back the two speed controllers"
๐Ÿ›ก๏ธtry / except / finally — Handle errors safely
try: run_car() except KeyboardInterrupt: print("Stopped!") finally: GPIO.cleanup() # ALWAYS runs
try = "attempt this code normally".
except = "if something goes wrong, do this instead of crashing".
finally = "no matter what happens โ€” crash or no crash โ€” always run this". Perfect for making sure your motors always turn off even if the program breaks.
Ctrl+C triggers KeyboardInterrupt
โ†’ except catches it gracefully; finally cleans up GPIO
๐Ÿ“f"text {variable}" — Put values inside strings
speed = 75 print(f"Speed is {speed}%") # prints: Speed is 75%
An f-string (the f before the quote) lets you embed variable values directly inside text using curly braces {}. Python automatically converts the value to text and slots it in. Way cleaner than the old way: "Speed is " + str(speed) + "%".
From this tutorial: f"Distance: {dist} cm"
โ†’ "Forward at 75% speed", "Distance: 18.3 cm", etc.
๐ŸŒฑ
You don't need to memorise all of this! Every professional programmer looks things up constantly โ€” that's completely normal. The goal is to recognise these patterns when you see them, then use this decoder as a reference. As you build the RC car step by step, each concept will start to feel natural. Programming is learned by doing, not just reading. You've got this!

Your 10-Step RC Car Journey

1
Set Up Your Environment
Install libraries and write your first lines
๐Ÿ“ฆ โ–พ
Before we write any robot code, we need to make sure Python has the right tools installed. We'll use RPi.GPIO to talk to the Raspberry Pi's pins and keyboard to read arrow-key input later on.
  • Open a terminal on your Raspberry Pi
  • Run the install commands shown in the code
  • Then run the Python script to confirm everything works
These libraries come pre-installed on Raspberry Pi OS โ€” but the pip commands make sure you have the latest versions!
๐Ÿ Python Concept โ€” import statements
import loads a library โ€” a package of pre-written code that someone else built so you don't have to! Think of it like downloading an app: it's already finished and you just use it. Writing import RPi.GPIO as GPIO loads the library and gives it a short nickname (GPIO) so we can type GPIO.setmode() instead of RPi.GPIO.setmode() every time. import time gives us tools for pausing the program and measuring elapsed time.
step1_setup.py
# โ”€โ”€ Step 1: Set Up Your Environment โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# First, open a terminal and run these install commands:
#   pip install RPi.GPIO
#   pip install keyboard
# Then run this script to confirm everything is working!

import RPi.GPIO as GPIO
import time
import sys

# Check Python version
print(f"๐Ÿ Python version: {sys.version}")

# Quick GPIO test โ€” just sets up and immediately cleans up
GPIO.setmode(GPIO.BCM)
GPIO.setwarnings(False)
GPIO.cleanup()

print("โœ… RPi.GPIO is working!")
print("๐Ÿš€ Environment ready โ€” let's build that RC car!")
2
Define Your Motor Pins
Map GPIO pin numbers to your motor driver
๐Ÿ“ โ–พ
The L298N motor driver has input pins (IN1โ€“IN4) that tell the motors which direction to spin, and enable pins (ENA, ENB) that control speed. We connect these to specific GPIO pins on the Raspberry Pi.
  • IN1/IN2 โ€” direction for the left motor
  • IN3/IN4 โ€” direction for the right motor
  • ENA/ENB โ€” speed control (we'll use PWM in Step 4)
You can change the pin numbers if your wiring is different โ€” just update the values at the top of your file!
๐Ÿ Python Concept โ€” Variables & Naming Conventions
In Python you create a variable just by writing NAME = value โ€” no need to declare a type like in Java or C! We write pin names in ALL_CAPS because that's the Python convention for constants (values you don't plan to change). Python won't enforce this, but the name acts as a friendly warning: "hey, don't change me!". Lines starting with # are comments โ€” Python ignores them completely. They exist purely as notes for humans reading the code. Good comments are a sign of a thoughtful programmer!
step2_pins.py
# โ”€โ”€ Step 2: Define Motor Pins โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# These numbers match the GPIO pins on your Raspberry Pi
# (using BCM numbering โ€” the numbers printed on the board)

import RPi.GPIO as GPIO

# โ”€โ”€ Left Motor (Motor A) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
MOTOR_A_IN1 = 17   # GPIO 17 โ†’ Forward signal
MOTOR_A_IN2 = 18   # GPIO 18 โ†’ Backward signal
MOTOR_A_EN  = 12   # GPIO 12 โ†’ Speed (PWM)

# โ”€โ”€ Right Motor (Motor B) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
MOTOR_B_IN3 = 22   # GPIO 22 โ†’ Forward signal
MOTOR_B_IN4 = 23   # GPIO 23 โ†’ Backward signal
MOTOR_B_EN  = 13   # GPIO 13 โ†’ Speed (PWM)

print("๐Ÿ“ Motor pins defined:")
print(f"  Left  Motor: IN1={MOTOR_A_IN1}, IN2={MOTOR_A_IN2}, EN={MOTOR_A_EN}")
print(f"  Right Motor: IN3={MOTOR_B_IN3}, IN4={MOTOR_B_IN4}, EN={MOTOR_B_EN}")
3
Initialize GPIO Pins
Tell the Pi which pins are outputs
โšก โ–พ
Every GPIO pin can be either an input (reading a sensor) or an output (sending a signal). We set all motor pins as outputs and wrap everything in a setup() function so we can call it easily.
  • GPIO.BCM โ€” use the BCM pin numbering printed on the board
  • GPIO.OUT โ€” we're sending signals out to the motors
Always call GPIO.cleanup() when your program ends โ€” it resets all pins so nothing stays on accidentally!
๐Ÿ Python Concept โ€” Functions (def) & for loops
A function (def setup_gpio():) is a reusable, named block of code. Instead of copying 6 GPIO.setup() lines into every file, you write it once and call it anywhere with just setup_gpio(). This follows the famous DRY rule: Don't Repeat Yourself โ€” one of the most important ideas in programming. The for loop (for pin in ALL_PINS:) automatically repeats code for every item in a list, so you never have to copy-paste the same line over and over!
step3_init.py
# โ”€โ”€ Step 3: Initialize GPIO Pins โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

import RPi.GPIO as GPIO

# Pin definitions (from Step 2)
MOTOR_A_IN1, MOTOR_A_IN2, MOTOR_A_EN = 17, 18, 12
MOTOR_B_IN3, MOTOR_B_IN4, MOTOR_B_EN = 22, 23, 13

ALL_PINS = [MOTOR_A_IN1, MOTOR_A_IN2, MOTOR_A_EN,
            MOTOR_B_IN3, MOTOR_B_IN4, MOTOR_B_EN]

def setup_gpio():
    GPIO.setmode(GPIO.BCM)        # Use BCM pin numbering
    GPIO.setwarnings(False)       # Silence "pin already in use" warnings

    for pin in ALL_PINS:
        GPIO.setup(pin, GPIO.OUT)  # Set every pin as an OUTPUT

    print("โšก GPIO pins initialized as outputs!")

setup_gpio()
GPIO.cleanup()   # Always clean up when done
print("๐Ÿงน Pins cleaned up safely.")
4
Speed Control with PWM
Use Pulse Width Modulation to vary motor speed
๐ŸŽ›๏ธ โ–พ
PWM (Pulse Width Modulation) turns a pin on and off really fast โ€” so fast the motor "feels" an average voltage between 0V and 3.3V. A duty cycle of 75% means the pin is HIGH 75% of the time โ†’ ~75% speed!
  • Frequency: 100 Hz (100 on/off cycles per second)
  • Duty cycle: 0โ€“100 (0 = stop, 100 = full speed)
Start with a duty cycle around 50โ€“75%. Too high and the car zooms away; too low and it might stall!
๐Ÿ Python Concept โ€” Return Values & range()
return pwm_a, pwm_b hands back two values at once โ€” Python lets you return multiple values separated by commas! The caller catches both in one line: pwm_a, pwm_b = setup_pwm(). This is called tuple unpacking and it's very handy. Also notice range(0, 101, 10) โ€” this generates numbers from 0 to 100 stepping by 10 (so: 0, 10, 20 โ€ฆ 100). The stop value 101 is not included, which is why you write 101 to actually reach 100. A classic beginner gotcha in Python!
step4_pwm.py
# โ”€โ”€ Step 4: PWM Speed Control โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

import RPi.GPIO as GPIO
import time

MOTOR_A_IN1, MOTOR_A_IN2, MOTOR_A_EN = 17, 18, 12
MOTOR_B_IN3, MOTOR_B_IN4, MOTOR_B_EN = 22, 23, 13

def setup_gpio():
    GPIO.setmode(GPIO.BCM)
    GPIO.setwarnings(False)
    for pin in [MOTOR_A_IN1, MOTOR_A_IN2, MOTOR_A_EN,
                MOTOR_B_IN3, MOTOR_B_IN4, MOTOR_B_EN]:
        GPIO.setup(pin, GPIO.OUT)

def setup_pwm():
    # Create PWM objects at 100 Hz frequency
    pwm_a = GPIO.PWM(MOTOR_A_EN, 100)
    pwm_b = GPIO.PWM(MOTOR_B_EN, 100)

    pwm_a.start(0)   # Start with 0% duty cycle (motors off)
    pwm_b.start(0)

    print("๐ŸŽ›๏ธ  PWM ready on both motors!")
    return pwm_a, pwm_b

setup_gpio()
pwm_a, pwm_b = setup_pwm()

# Demo: ramp speed from 0% โ†’ 100% โ†’ 0%
print("๐Ÿš— Ramping speed up...")
for speed in range(0, 101, 10):
    pwm_a.ChangeDutyCycle(speed)
    pwm_b.ChangeDutyCycle(speed)
    print(f"  Speed: {speed}%")
    time.sleep(0.3)

GPIO.cleanup()
5
Move Forward & Backward
Write your first real movement functions
โฌ†๏ธ โ–พ
To spin a motor forward, we set IN1=HIGH and IN2=LOW. To spin it backward, we flip them: IN1=LOW and IN2=HIGH. We do this for both motors at the same time!
  • GPIO.HIGH = 3.3V (signal ON)
  • GPIO.LOW = 0V (signal OFF)
  • The speed parameter (0โ€“100) lets you control how fast
Try calling move_forward() and move_backward() with different speed values โ€” what's the minimum speed before the motors stall?
๐Ÿ Python Concept โ€” Default Parameters & f-strings
Notice speed=75 inside the function definition? That's a default parameter. If you call move_forward(pwm_a, pwm_b) without a speed, Python automatically uses 75. You can still override it: move_forward(pwm_a, pwm_b, speed=50). Default parameters make your functions flexible without requiring every caller to specify everything. Also see f"Forward at {speed}% speed" โ€” the f-string embeds the value of speed directly inside the text using curly braces. Much cleaner than old-style "Forward at " + str(speed) + "% speed"!
step5_move.py
# โ”€โ”€ Step 5: Forward & Backward Movement โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

import RPi.GPIO as GPIO
import time

MOTOR_A_IN1, MOTOR_A_IN2, MOTOR_A_EN = 17, 18, 12
MOTOR_B_IN3, MOTOR_B_IN4, MOTOR_B_EN = 22, 23, 13

def setup():
    GPIO.setmode(GPIO.BCM); GPIO.setwarnings(False)
    for p in [MOTOR_A_IN1,MOTOR_A_IN2,MOTOR_A_EN,MOTOR_B_IN3,MOTOR_B_IN4,MOTOR_B_EN]:
        GPIO.setup(p, GPIO.OUT)
    pwm_a = GPIO.PWM(MOTOR_A_EN, 100); pwm_a.start(0)
    pwm_b = GPIO.PWM(MOTOR_B_EN, 100); pwm_b.start(0)
    return pwm_a, pwm_b

def move_forward(pwm_a, pwm_b, speed=75):
    GPIO.output(MOTOR_A_IN1, GPIO.HIGH)  # Left motor โ†’ forward
    GPIO.output(MOTOR_A_IN2, GPIO.LOW)
    GPIO.output(MOTOR_B_IN3, GPIO.HIGH)  # Right motor โ†’ forward
    GPIO.output(MOTOR_B_IN4, GPIO.LOW)
    pwm_a.ChangeDutyCycle(speed)
    pwm_b.ChangeDutyCycle(speed)
    print(f"โฌ†๏ธ  Forward at {speed}% speed")

def move_backward(pwm_a, pwm_b, speed=75):
    GPIO.output(MOTOR_A_IN1, GPIO.LOW)   # Left motor โ†’ backward
    GPIO.output(MOTOR_A_IN2, GPIO.HIGH)
    GPIO.output(MOTOR_B_IN3, GPIO.LOW)   # Right motor โ†’ backward
    GPIO.output(MOTOR_B_IN4, GPIO.HIGH)
    pwm_a.ChangeDutyCycle(speed)
    pwm_b.ChangeDutyCycle(speed)
    print(f"โฌ‡๏ธ  Backward at {speed}% speed")

# โ”€โ”€ Test it! โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
pwm_a, pwm_b = setup()
move_forward(pwm_a, pwm_b, speed=70)
time.sleep(2)
move_backward(pwm_a, pwm_b, speed=70)
time.sleep(2)
GPIO.cleanup()
6
Turn Left, Turn Right & Stop
Steer your car and bring it to a halt
โ†ฉ๏ธ โ–พ
Turning works by spinning the two motors in opposite directions. To turn left, the right motor goes forward while the left motor goes backward (or stops). The car pivots in place โ€” like a tank!
  • Turn left โ€” left motor backward, right motor forward
  • Turn right โ€” left motor forward, right motor backward
  • Stop โ€” set all direction pins LOW and duty cycle to 0
Turning speed should usually be lower than driving speed โ€” try 50โ€“60% to keep control!
๐Ÿ Python Concept โ€” Code Reuse & the DRY Principle
Look at the stop() function โ€” instead of writing 4 separate GPIO.output(pin, GPIO.LOW) lines, we loop over a list. This is the DRY principle (Don't Repeat Yourself) in action! If you ever need to change how stopping works, you edit one place instead of four. Also notice turn_left and turn_right have near-identical structure. That's a hint to your future self: one day you could refactor these into a single turn(direction) function โ€” a great next challenge as your Python skills grow!
step6_turn.py
# โ”€โ”€ Step 6: Turn Left, Right & Stop โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

import RPi.GPIO as GPIO
import time

MOTOR_A_IN1, MOTOR_A_IN2, MOTOR_A_EN = 17, 18, 12
MOTOR_B_IN3, MOTOR_B_IN4, MOTOR_B_EN = 22, 23, 13

# ... setup() and forward/backward functions from Step 5 ...

def turn_left(pwm_a, pwm_b, speed=55):
    # Left motor goes backward, right motor goes forward
    GPIO.output(MOTOR_A_IN1, GPIO.LOW)
    GPIO.output(MOTOR_A_IN2, GPIO.HIGH)
    GPIO.output(MOTOR_B_IN3, GPIO.HIGH)
    GPIO.output(MOTOR_B_IN4, GPIO.LOW)
    pwm_a.ChangeDutyCycle(speed)
    pwm_b.ChangeDutyCycle(speed)
    print("โ†ฉ๏ธ  Turning left!")

def turn_right(pwm_a, pwm_b, speed=55):
    # Left motor goes forward, right motor goes backward
    GPIO.output(MOTOR_A_IN1, GPIO.HIGH)
    GPIO.output(MOTOR_A_IN2, GPIO.LOW)
    GPIO.output(MOTOR_B_IN3, GPIO.LOW)
    GPIO.output(MOTOR_B_IN4, GPIO.HIGH)
    pwm_a.ChangeDutyCycle(speed)
    pwm_b.ChangeDutyCycle(speed)
    print("โ†ช๏ธ  Turning right!")

def stop(pwm_a, pwm_b):
    # Turn off all direction signals and set speed to 0
    for pin in [MOTOR_A_IN1, MOTOR_A_IN2, MOTOR_B_IN3, MOTOR_B_IN4]:
        GPIO.output(pin, GPIO.LOW)
    pwm_a.ChangeDutyCycle(0)
    pwm_b.ChangeDutyCycle(0)
    print("๐Ÿ›‘ Stopped!")
7
Keyboard Control
Drive your car with the arrow keys!
๐ŸŽฎ โ–พ
Now we add a control loop that reads arrow key presses in real-time using the keyboard library. The loop runs 10 times per second and calls the correct movement function based on which key is held down.
  • Press โ†‘ to go forward
  • Press โ†“ to go backward
  • Press โ† / โ†’ to turn
  • Press Q to quit safely
Run this script as root (sudo python3 step7_keyboard.py) โ€” the keyboard library requires elevated permissions on Linux!
๐Ÿ Python Concept โ€” while True, if/elif/else & break
while True: creates an infinite loop that runs forever โ€” until break exits it. That sounds scary, but it's perfectly normal in hardware code! You want the car to keep listening until you decide to quit. The if / elif / else chain checks conditions in order โ€” Python tries each one from top to bottom and only runs the first that's True. The final else is the "none of the above" case โ€” if no key is pressed, stop the motors. time.sleep(0.1) pauses the loop for 0.1 seconds, giving you ~10 checks per second without overloading the CPU.
step7_keyboard.py
# โ”€โ”€ Step 7: Keyboard Control โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

import RPi.GPIO as GPIO
import keyboard
import time

MOTOR_A_IN1, MOTOR_A_IN2, MOTOR_A_EN = 17, 18, 12
MOTOR_B_IN3, MOTOR_B_IN4, MOTOR_B_EN = 22, 23, 13

# ... paste setup(), move_forward(), move_backward(),
#     turn_left(), turn_right(), stop() from previous steps ...

def keyboard_control(pwm_a, pwm_b):
    print("๐ŸŽฎ RC Car ready! Use arrow keys to drive.")
    print("   Press Q to quit safely.\n")

    while True:
        if keyboard.is_pressed('up'):
            move_forward(pwm_a, pwm_b)
        elif keyboard.is_pressed('down'):
            move_backward(pwm_a, pwm_b)
        elif keyboard.is_pressed('left'):
            turn_left(pwm_a, pwm_b)
        elif keyboard.is_pressed('right'):
            turn_right(pwm_a, pwm_b)
        elif keyboard.is_pressed('q'):
            stop(pwm_a, pwm_b)
            print("๐Ÿ‘‹ Goodbye! Cleaning up...")
            break
        else:
            stop(pwm_a, pwm_b)

        time.sleep(0.1)  # ~10 updates per second

pwm_a, pwm_b = setup()
try:
    keyboard_control(pwm_a, pwm_b)
finally:
    GPIO.cleanup()  # Always runs, even if an error occurs
8
Add an Ultrasonic Distance Sensor
Give your car "eyes" to see obstacles
๐Ÿ“ก โ–พ
The HC-SR04 ultrasonic sensor fires a sound pulse and measures how long it takes to bounce back. From the time, we calculate the distance in centimetres.
  • TRIG pin โ†’ triggers the pulse (output)
  • ECHO pin โ†’ goes HIGH while waiting for the echo (input)
  • Distance formula: distance = (time ร— speed_of_sound) / 2
Mount the sensor on the front of your car pointing forward. It works best for objects 2 cm โ€“ 400 cm away!
๐Ÿ Python Concept โ€” time.time() & Built-in Functions
time.time() returns the current moment as a float (decimal number) representing seconds since January 1, 1970 โ€” this is called a Unix timestamp. By recording the time before and after the echo pulse, we calculate its duration with simple subtraction: pulse_end - pulse_start. round(distance, 1) is a Python built-in function โ€” no import needed! It rounds to 1 decimal place for clean output. Knowing which functions are built-in vs. need an import is a key Python skill to develop over time.
step8_sensor.py
# โ”€โ”€ Step 8: Ultrasonic Distance Sensor โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

import RPi.GPIO as GPIO
import time

# HC-SR04 sensor pins
TRIG = 24    # GPIO 24 โ†’ Trigger (output)
ECHO = 25    # GPIO 25 โ†’ Echo    (input)

def setup_sensor():
    GPIO.setmode(GPIO.BCM)
    GPIO.setup(TRIG, GPIO.OUT)
    GPIO.setup(ECHO, GPIO.IN)
    GPIO.output(TRIG, GPIO.LOW)
    time.sleep(0.5)   # Let sensor settle
    print("๐Ÿ“ก Sensor ready!")

def get_distance():
    # Fire a 10-microsecond trigger pulse
    GPIO.output(TRIG, GPIO.HIGH)
    time.sleep(0.00001)
    GPIO.output(TRIG, GPIO.LOW)

    # Wait for ECHO to go HIGH (pulse starts)
    pulse_start = time.time()
    while GPIO.input(ECHO) == 0:
        pulse_start = time.time()

    # Wait for ECHO to go LOW (pulse ends)
    pulse_end = time.time()
    while GPIO.input(ECHO) == 1:
        pulse_end = time.time()

    # Distance = (time ร— speed of sound) / 2
    duration = pulse_end - pulse_start
    distance = (duration * 34300) / 2   # cm
    return round(distance, 1)

setup_sensor()
for _ in range(10):
    dist = get_distance()
    print(f"๐Ÿ“ Distance: {dist} cm")
    time.sleep(0.5)

GPIO.cleanup()
9
Obstacle Avoidance
Make the car dodge obstacles automatically!
๐Ÿšง โ–พ
We combine the sensor with our motor functions to create basic autonomous obstacle avoidance. If the car detects something closer than 20 cm, it stops, backs up, turns, and then continues forward.
  • Threshold: 20 cm โ€” closer than this = obstacle!
  • When blocked: stop โ†’ back up 0.5 s โ†’ turn right 0.6 s โ†’ go again
  • This loop runs forever until you press Ctrl+C
Experiment with the 20 cm threshold and the backup/turn timing to make your car smarter. Every robot needs tuning!
๐Ÿ Python Concept โ€” try / except / finally
This is Python's error handling system โ€” and it's essential for hardware! Code inside try: runs normally. When you press Ctrl+C, Python raises a KeyboardInterrupt โ€” the except KeyboardInterrupt: block catches it gracefully, showing a friendly message instead of a scary red error. The finally: block always runs โ€” even if an unexpected crash occurs. That's exactly why GPIO.cleanup() lives there: your motors will never get stuck running even if your code crashes halfway through!
step9_avoid.py
# โ”€โ”€ Step 9: Obstacle Avoidance โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

import RPi.GPIO as GPIO
import time

# Motor pins
MOTOR_A_IN1, MOTOR_A_IN2, MOTOR_A_EN = 17, 18, 12
MOTOR_B_IN3, MOTOR_B_IN4, MOTOR_B_EN = 22, 23, 13
# Sensor pins
TRIG, ECHO = 24, 25

SAFE_DISTANCE = 20   # cm โ€” stop if closer than this

# ... paste setup(), setup_sensor(), get_distance(),
#     move_forward(), move_backward(), turn_right(), stop() ...

def avoid_obstacles(pwm_a, pwm_b):
    print("๐Ÿค– Autonomous mode ON! Press Ctrl+C to stop.\n")

    while True:
        dist = get_distance()
        print(f"๐Ÿ“ {dist} cm", end="\r")

        if dist > SAFE_DISTANCE:
            move_forward(pwm_a, pwm_b, speed=65)    # All clear!
        else:
            print(f"\n๐Ÿšง Obstacle at {dist} cm! Avoiding...")
            stop(pwm_a, pwm_b);          time.sleep(0.2)
            move_backward(pwm_a, pwm_b); time.sleep(0.5)
            turn_right(pwm_a, pwm_b);    time.sleep(0.6)
            stop(pwm_a, pwm_b);          time.sleep(0.1)

        time.sleep(0.05)

pwm_a, pwm_b = setup()
setup_sensor()
try:
    avoid_obstacles(pwm_a, pwm_b)
except KeyboardInterrupt:
    print("\n๐Ÿ›‘ Stopped by user.")
finally:
    GPIO.cleanup()
10
The Complete RC Car Program
Everything combined โ€” your full working robot!
๐Ÿ† โ–พ
This is the complete, production-ready RC car script. It combines all previous steps into one clean file with:
  • Full motor setup with PWM speed control
  • All four movement directions + stop
  • Ultrasonic obstacle detection
  • Interactive menu: Manual (keyboard) or Auto (avoid obstacles)
  • Safe GPIO cleanup on exit โ€” no matter what
This is your starting point โ€” now customize it! Add a buzzer, LED headlights, a camera, or even Wi-Fi control. The sky's the limit! ๐Ÿš€
๐Ÿ Python Concept โ€” if __name__ == "__main__"
This is one of Python's most important and most-Googled patterns! When Python runs a file directly, it sets a special variable __name__ to "__main__". If another file imports your code instead, __name__ becomes the filename. The code inside this if block only runs when you run this file directly โ€” not when someone imports your functions into a bigger project. It's what separates a reusable module from a runnable script, and all professional Python projects use this pattern. You've just graduated to writing professional-style code! ๐ŸŽ“
rc_car_complete.py
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
#   ๐Ÿค– COMPLETE RC CAR โ€” rc_car_complete.py
#   Built with Python + Raspberry Pi + L298N + HC-SR04
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•

import RPi.GPIO as GPIO
import keyboard
import time

# โ”€โ”€ Pin Configuration โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
MOTOR_A_IN1, MOTOR_A_IN2, MOTOR_A_EN = 17, 18, 12
MOTOR_B_IN3, MOTOR_B_IN4, MOTOR_B_EN = 22, 23, 13
TRIG, ECHO                           = 24, 25
SAFE_DIST                             = 20    # cm

# โ”€โ”€ Setup โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def setup():
    GPIO.setmode(GPIO.BCM)
    GPIO.setwarnings(False)
    for p in [MOTOR_A_IN1,MOTOR_A_IN2,MOTOR_A_EN,
              MOTOR_B_IN3,MOTOR_B_IN4,MOTOR_B_EN]:
        GPIO.setup(p, GPIO.OUT)
    GPIO.setup(TRIG, GPIO.OUT)
    GPIO.setup(ECHO, GPIO.IN)
    GPIO.output(TRIG, GPIO.LOW)
    time.sleep(0.5)
    pwm_a = GPIO.PWM(MOTOR_A_EN, 100); pwm_a.start(0)
    pwm_b = GPIO.PWM(MOTOR_B_EN, 100); pwm_b.start(0)
    return pwm_a, pwm_b

# โ”€โ”€ Motor Helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def _drive(a1, a2, b1, b2, pwm_a, pwm_b, spd):
    GPIO.output(MOTOR_A_IN1, a1); GPIO.output(MOTOR_A_IN2, a2)
    GPIO.output(MOTOR_B_IN3, b1); GPIO.output(MOTOR_B_IN4, b2)
    pwm_a.ChangeDutyCycle(spd); pwm_b.ChangeDutyCycle(spd)

def forward(p, q, s=70):  _drive(1,0,1,0,p,q,s)
def backward(p, q, s=70): _drive(0,1,0,1,p,q,s)
def left(p, q, s=55):     _drive(0,1,1,0,p,q,s)
def right(p, q, s=55):    _drive(1,0,0,1,p,q,s)
def stop(p, q):            _drive(0,0,0,0,p,q,0)

# โ”€โ”€ Sensor โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def get_distance():
    GPIO.output(TRIG, GPIO.HIGH); time.sleep(0.00001)
    GPIO.output(TRIG, GPIO.LOW)
    t0 = t1 = time.time()
    while GPIO.input(ECHO) == 0: t0 = time.time()
    while GPIO.input(ECHO) == 1: t1 = time.time()
    return round((t1 - t0) * 17150, 1)

# โ”€โ”€ Manual Mode โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def manual_mode(p, q):
    print("๐ŸŽฎ MANUAL MODE โ€” Arrow keys to drive, Q to quit")
    while True:
        if   keyboard.is_pressed('up'):    forward(p, q)
        elif keyboard.is_pressed('down'):  backward(p, q)
        elif keyboard.is_pressed('left'):  left(p, q)
        elif keyboard.is_pressed('right'): right(p, q)
        elif keyboard.is_pressed('q'):     stop(p, q); break
        else: stop(p, q)
        time.sleep(0.1)

# โ”€โ”€ Auto Mode โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def auto_mode(p, q):
    print("๐Ÿค– AUTO MODE โ€” Ctrl+C to quit")
    while True:
        d = get_distance()
        if d > SAFE_DIST:
            forward(p, q, 65)
        else:
            print(f"๐Ÿšง Obstacle at {d}cm!")
            stop(p,q);     time.sleep(0.2)
            backward(p,q); time.sleep(0.5)
            right(p,q);    time.sleep(0.6)
        time.sleep(0.05)

# โ”€โ”€ Main โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
if __name__ == "__main__":
    pwm_a, pwm_b = setup()
    print("\n๐Ÿค– RC CAR READY!")
    print("  [1] Manual (keyboard control)")
    print("  [2] Auto   (obstacle avoidance)")
    choice = input("Choose mode: ").strip()
    try:
        if choice == "1": manual_mode(pwm_a, pwm_b)
        elif choice == "2": auto_mode(pwm_a, pwm_b)
        else: print("Invalid choice.")
    except KeyboardInterrupt:
        print("\n๐Ÿ‘‹ Stopped!")
    finally:
        GPIO.cleanup()
        print("โœ… GPIO cleaned up. See you next time!")