Most first robots fail for one reason: the builder skips the skills that the project quietly depends on. A line follower needs sensor thresholding. A self-balancing robot needs a tuned control loop. A SLAM rover needs a working transform tree. This guide orders 20 builds so each one introduces one new concept on top of the last. Hardware, wiring, math, and code are included for the projects where they matter most.
Quick Takeaways
- Levels 1-5 teach GPIO, PWM, and sensor reading on an Arduino Uno or ESP32. Cost: under $40 each.
- Levels 6-12 add feedback control: PID, encoders, and IMU filtering.
- Levels 13-20 introduce ROS2, sensor fusion, kinematics, and SLAM on a Raspberry Pi 5.
- Build in order. Each project reuses the code and hardware of the one before it.
| Tier | Projects | Core Skill | Typical Cost | Typical Build Time |
|---|---|---|---|---|
| Tier 1: Starter | 1-5 | GPIO, PWM, basic sensing | $10-$40 | 1-3 hrs |
| Tier 2: Intermediate | 6-12 | Feedback control, encoders, IMU | $30-$80 | 4-10 hrs |
| Tier 3: Advanced Beginner | 13-20 | ROS2, kinematics, SLAM, fusion | $80-$300 | 10-40 hrs |
How Difficulty Is Scored
Each project below is scored on four axes, 1 to 5:
- Electronics: wiring complexity and power management
- Mechanics: fabrication and assembly effort
- Software: code complexity and debugging load
- Math: control theory and geometry required
The total decides the rank. Projects with equal totals are ordered by how many new concepts they introduce.
Tier 1: Starter Projects (Rank 1-5)
1. Blinking LED Controller (Difficulty: 1/20)
Wire an LED with a 220Ω resistor to a digital pin. Toggle it at 1Hz. This confirms your toolchain, USB drivers, and board selection work.
// Blink without delay(): non-blocking timing pattern used in all later projects
const int LED_PIN = 13;
unsigned long previousMillis = 0;
const unsigned long INTERVAL_MS = 500; // 500ms on + 500ms off = 1Hz
bool ledState = false;
void setup() {
pinMode(LED_PIN, OUTPUT);
}
void loop() {
unsigned long now = millis();
if (now - previousMillis >= INTERVAL_MS) {
previousMillis = now;
ledState = !ledState;
digitalWrite(LED_PIN, ledState);
}
}
Learn millis() timing here. Every later project depends on non-blocking loops.
2. Servo Sweeper (Difficulty: 2/20)
Drive an SG90 servo with a 50Hz PWM signal. Pulse widths of 1.0ms to 2.0ms map to roughly 0° to 180°. Power the servo from an external 5V supply, not the board pin, to avoid brownouts.
3. Ultrasonic Distance Meter (Difficulty: 3/20)
Read an HC-SR04 sonar sensor. Distance follows from the echo time:
distance_cm = (echo_time_us * 0.0343) / 2
The sensor has a usable range of about 2cm to 400cm with roughly 3mm resolution. Print readings to the serial monitor at 10Hz.
4. Light-Seeking Robot (Difficulty: 4/20)
Mount two LDRs (light-dependent resistors) at 45° angles. Compare the two analog readings. Steer toward the brighter side with a simple differential rule:
error = left_ldr - right_ldr
steer = Kp * error (Kp ≈ 0.5 as a starting value)
This is your first proportional controller.
5. Obstacle-Avoiding Rover (Difficulty: 5/20)
Combine a 2WD chassis, an L298N or TB6612FNG motor driver, and an HC-SR04 on a servo. Logic: drive forward until distance drops below 20cm, stop, scan left and right, turn toward the larger gap.
Wiring notes:
- Power motors from a separate 6-9V pack.
- Tie all grounds together.
- Use PWM pins (e.g., D5, D6) for speed control.
Tier 2: Intermediate Projects (Rank 6-12)
6. Bluetooth-Controlled Car (Difficulty: 6/20)
Add an HC-05 module or use the ESP32 built-in Bluetooth. Parse single-character commands (F, B, L, R, S). This teaches serial protocols and command parsing.
7. Line Follower Robot (Difficulty: 7/20)
Use a 5-channel IR reflectance array (e.g., QTR-5RC). Compute a weighted line position:
position = (0*s0 + 1000*s1 + 2000*s2 + 3000*s3 + 4000*s4) / (s0+s1+s2+s3+s4)
error = position - 2000
Feed error into a PD controller:
correction = Kp * error + Kd * (error - last_error)
left_speed = base_speed + correction
right_speed = base_speed - correction
Starting gains: Kp = 0.05, Kd = 0.5, base_speed = 150 (of 255).
8. Robotic Arm (3-DOF Servo) (Difficulty: 8/20)
Build a base-shoulder-elbow arm from MG996R servos. The shoulder and elbow form a planar 2-link chain. Inverse kinematics for link lengths L1 and L2, target (x, y):
D = (x² + y² - L1² - L2²) / (2 * L1 * L2)
theta2 = atan2(±sqrt(1 - D²), D) // elbow angle
theta1 = atan2(y, x) - atan2(L2*sin(theta2), L1 + L2*cos(theta2))
Reject targets where |D| > 1 because they lie outside the workspace.
9. Wall-Following Robot (Difficulty: 9/20)
Mount two sonar or VL53L0X time-of-flight sensors on one side. Hold a target distance (e.g., 15cm) using PD control on the lateral error and a second term for heading, computed from the difference between the two sensor readings.
10. Encoder-Based Odometry Rover (Difficulty: 10/20)
Add quadrature encoders (e.g., 20 pulses per revolution on a N20 motor). Wheel radius r and track width b give differential-drive odometry:
d_left = 2π r * (ticks_left / ticks_per_rev)
d_right = 2π r * (ticks_right / ticks_per_rev)
d_center = (d_left + d_right) / 2
dtheta = (d_right - d_left) / b
x += d_center * cos(theta + dtheta/2)
y += d_center * sin(theta + dtheta/2)
theta += dtheta
Use hardware interrupts for the encoder pins. Polling will miss ticks above roughly 1kHz.
11. Closed-Loop Motor Speed Controller (Difficulty: 11/20)
Run a velocity PID loop at 100Hz on one encoder motor:
u(t) = Kp*e(t) + Ki*∫e(t)dt + Kd*de(t)/dt
// Discrete PID with anti-windup clamping
float pid_update(float setpoint, float measured, float dt) {
float error = setpoint - measured;
integral += error * dt;
integral = constrain(integral, -I_MAX, I_MAX); // anti-windup
float derivative = (error - prev_error) / dt;
prev_error = error;
float u = Kp * error + Ki * integral + Kd * derivative;
return constrain(u, -255, 255); // PWM limits
}
Tuning sequence: set Ki = Kd = 0, raise Kp until oscillation starts, halve it, add Ki to remove steady-state error, then add Kd to damp overshoot.
12. Gesture-Controlled Hand Arm (Difficulty: 12/20)
Read an MPU6050 IMU on a glove and map tilt angles to servo positions. Pitch and roll come from accelerometer data:
pitch = atan2(-ax, sqrt(ay² + az²))
roll = atan2(ay, az)
Smooth the output with a low-pass filter: y[n] = α*x[n] + (1-α)*y[n-1], with α ≈ 0.2.
Tier 3: Advanced Beginner Projects (Rank 13-20)
13. Self-Balancing Robot (Difficulty: 13/20)
An inverted pendulum on two wheels. Fuse the MPU6050 gyro and accelerometer with a complementary filter:
angle = 0.98 * (angle + gyro_rate * dt) + 0.02 * accel_angle
Run a PID loop at 200Hz on the tilt angle. Typical starting gains for a 20cm tall chassis: Kp ≈ 15, Ki ≈ 0.5, Kd ≈ 0.8. Loop jitter above 5ms makes balancing fail, so avoid delay() and serial prints inside the loop.
14. ROS2 Differential Drive Robot (Difficulty: 14/20)
Move computation to a Raspberry Pi 5 running ROS2 Jazzy. Keep the Arduino or Pico as a motor and encoder microcontroller. The Pi publishes /cmd_vel and subscribes to wheel feedback.
# Minimal ROS2 (rclpy) node: converts Twist to wheel speeds
import rclpy
from rclpy.node import Node
from geometry_msgs.msg import Twist
class DiffDriveNode(Node):
def __init__(self):
super().__init__('diff_drive')
self.wheel_sep = 0.16 # track width in meters
self.wheel_rad = 0.033 # wheel radius in meters
self.sub = self.create_subscription(Twist, '/cmd_vel', self.cb, 10)
def cb(self, msg: Twist):
v = msg.linear.x # forward velocity (m/s)
w = msg.angular.z # yaw rate (rad/s)
# Differential drive inverse kinematics
v_left = (v - w * self.wheel_sep / 2) / self.wheel_rad # rad/s
v_right = (v + w * self.wheel_sep / 2) / self.wheel_rad # rad/s
self.get_logger().info(f'L={v_left:.2f} R={v_right:.2f} rad/s')
def main():
rclpy.init()
rclpy.spin(DiffDriveNode())
if __name__ == '__main__':
main()
15. Camera Color-Tracking Robot (Difficulty: 15/20)
Use OpenCV on a Pi camera. Convert frames to HSV, threshold a color range, find the largest contour, and steer to keep its centroid at image center:
import cv2, numpy as np
cap = cv2.VideoCapture(0)
lower = np.array([35, 100, 100]) # green lower bound (H,S,V)
upper = np.array([85, 255, 255]) # green upper bound
while True:
ok, frame = cap.read()
if not ok: break
hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
mask = cv2.inRange(hsv, lower, upper)
cnts, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
if cnts:
c = max(cnts, key=cv2.contourArea)
M = cv2.moments(c)
if M['m00'] > 0:
cx = int(M['m10'] / M['m00'])
error = cx - frame.shape[1] // 2 # pixels from center
# feed `error` into a P controller for angular velocity
16. URDF Model and RViz Visualization (Difficulty: 16/20)
Write a URDF describing your robot’s base_link, wheels, and sensor frames. Launch robot_state_publisher and view it in RViz2. This teaches the TF2 transform tree, which every later ROS2 project requires.
17. Teleoperated Rover with Camera Stream (Difficulty: 17/20)
Combine a ROS2 base, teleop_twist_keyboard, and an image-transport camera stream. Add QoS tuning: use BEST_EFFORT reliability for camera topics and RELIABLE for /cmd_vel.
18. LiDAR Obstacle Avoidance (Difficulty: 18/20)
Mount an RPLIDAR A1 (12m range, ~8000 samples/s). Subscribe to /scan (sensor_msgs/LaserScan). Split the scan into front, left, and right sectors, take the minimum range in each, and steer toward the sector with the most clearance.
19. IMU and Odometry Sensor Fusion (Difficulty: 19/20)
Fuse wheel odometry and IMU data with robot_localization and its Extended Kalman Filter (EKF). The EKF state vector for a planar robot is [x, y, θ, vx, vy, ω]. Wheel odometry drifts in heading, so the IMU yaw rate corrects θ.
# ekf.yaml (robot_localization)
ekf_filter_node:
ros__parameters:
frequency: 30.0
two_d_mode: true
odom0: /wheel/odometry
odom0_config: [false, false, false, # x, y, z position
false, false, false, # roll, pitch, yaw
true, true, false, # vx, vy, vz
false, false, true, # roll rate, pitch rate, yaw rate
false, false, false] # ax, ay, az
imu0: /imu/data
imu0_config: [false, false, false,
false, false, true, # fuse yaw
false, false, false,
false, false, true, # fuse yaw rate
false, false, false]
20. SLAM Mapping Robot (Difficulty: 20/20)
Combine the LiDAR, the EKF odometry, and a correct TF tree (map → odom → base_link → laser). Run slam_toolbox in online async mode:
ros2 launch slam_toolbox online_async_launch.py \
slam_params_file:=config/mapper_params_online_async.yaml
Drive the robot slowly (under 0.3m/s) through the room and save the map with ros2 run nav2_map_server map_saver_cli -f my_map. Then load it in Nav2 for goal-based navigation. This project exercises every skill from the previous 19.
Comparison: Controllers and Microcontrollers
| Platform | CPU | Best For | Power Draw | Cost |
|---|---|---|---|---|
| Arduino Uno | 16MHz AVR | Projects 1-12 | ~50mA | ~$25 |
| ESP32 | 240MHz dual-core | Wireless projects, 6-13 | ~80-240mA | ~$8 |
| Raspberry Pi Pico | 133MHz dual-core | Fast PID loops | ~25mA | ~$4 |
| Raspberry Pi 5 | 2.4GHz quad-core | ROS2, OpenCV, SLAM | ~3-8W | ~$60-$80 |
| Motor Type | Control | Precision | Cost | Best Project |
|---|---|---|---|---|
| Hobby DC (TT) | PWM only | Low | $2 | 5, 6 |
| Encoder DC (N20) | PWM + feedback | Medium | $10 | 10, 11, 13 |
| Hobby Servo | PWM position | ~1° | $3-$10 | 2, 8, 12 |
| Stepper (NEMA17) | Step/dir | 1.8°/step | $12 | Plotters, arms |
Real-World Workflow: Tuning a PID Loop for the Balancing Robot
Follow this sequence on project 13.
- Calibrate the IMU. Average 1000 gyro samples at rest. Subtract that bias from every reading.
- Find the balance angle. Hold the robot upright by hand and record the filtered angle. Use this as the setpoint.
- Set
Ki = Kd = 0. RaiseKpuntil the robot oscillates around upright. - Add
Kd. Increase until oscillations damp within 2-3 cycles. - Add small
Ki. This removes the slow lean caused by uneven weight. - Add an outer velocity loop to stop the robot from drifting across the floor.
- Log data. Record angle, error, and motor output at 200Hz. Plot it to spot saturation and loop jitter.
Which Project Should You Start With?
| Your Background | Start At | Why |
|---|---|---|
| Never touched electronics | Project 1 | Learn toolchain and GPIO first |
| Comfortable with Arduino | Project 7 | Jump straight to feedback control |
| Strong programmer, new to hardware | Project 10 | Odometry bridges code and mechanics |
| Know control theory | Project 13 | Apply theory to a real unstable system |
| Python/Linux experience | Project 14 | Begin the ROS2 track directly |
FAQ
What is the easiest beginner robot project?
A blinking LED controller on an Arduino Uno. It needs one resistor, one LED, and 10 lines of code. It verifies your toolchain before you buy motors.
Should beginners learn Arduino or ROS2 first?
Start with Arduino. It teaches PWM, sensor reading, and timing without operating-system overhead. Move to ROS2 after you complete a PID-controlled robot (project 11), since ROS2 assumes you understand feedback loops.
How much does a first robot cost?
An obstacle-avoiding rover (project 5) costs $30-$40: board, 2WD chassis, motor driver, sonar, and battery pack. ROS2 projects with LiDAR run $250-$350.
Why does my self-balancing robot fall over immediately?
The usual causes are an uncalibrated IMU, a control loop slower than 100Hz, or motor deadband. Calibrate gyro bias, fix the loop rate to a hardware timer, and add a deadband compensation offset to the PWM output.




