Self Catching Projectile Machine

by NewsonsElectronics in Circuits > Arduino

23 Views, 0 Favorites, 0 Comments

Self Catching Projectile Machine

World's First Arduino Self Catching Machine (Video 1: The Build)

In this Instructions, I show you how I built an automatic ball-catching machine. This would make a great classroom demonstration if you teach SPH3U or SPH4U Physics — bringing projectile motion and real-world problem solving to life.


Link to the full build video https://youtu.be/eg7z2-UQLCc

Link to the theory video https://youtu.be/ggqOKfbcum0

Supplies

Materials.jpg

10K pots x 2 → Amazon , Taobao

TCR5000- IR sensor → Amazon, Taobao

MGN12H rail - 1 Meter → Amazon , Taobao

Nidec 24H055M020 Motor →Amazon, Taobao

2GT timing Belt -3Meters → Amazon, Taobao

Contact switch→ Amazon , Taobao

Arduino UNO Amazon, Taobao

12mm pulleys → Amazon, Taobao

60tooth gear → Amazon, Taobao

Meter Sticks x 2 → Amazon, Taobao

PVC pipe 21mm dia. 15cm long → Amazon, Taobao

20mm dia. Ball Bearing → Amazon, Taobao

Spring 10mm x 30mm x 1mm → Amazon , Taobao

M2 Bolts→ Amazon, Taobao

12V 2A charger → Amazon , Taobao

The Cannon

vlcsnap-2026-06-20-23h18m05s737.jpg
vlcsnap-2026-06-20-23h16m44s695.jpg
vlcsnap-2026-06-20-23h16m52s037.jpg
vlcsnap-2026-06-20-23h17m14s340.jpg
vlcsnap-2026-06-20-23h17m27s967.jpg

The cannon barrel is made from a PVC pipe with an inside diameter of 21 mm, allowing a 20 mm ball bearing to fit smoothly while minimizing friction during launch.

Start by cutting the PVC pipe to a length of 15 cm and sanding the cut ends smooth. To create the loading slots, I used a laser cutter. The technique I used was to first laser-cut the slot pattern into a piece of wood, creating a positioning jig. The PVC pipe was then placed into the cut-out location, ensuring it was held in the exact position needed for the laser to cut the slots accurately into the pipe.

Refer to the Cannon Slots.dxf file for the slot design and dimensions.

Downloads

3D Printing

vlcsnap-2026-06-20-23h17m50s413.jpg
vlcsnap-2026-06-20-23h17m54s767.jpg
vlcsnap-2026-06-20-23h17m39s870.jpg
vlcsnap-2026-06-20-23h17m44s893.jpg
Ir sensor.jpg

everal parts will need to be 3D printed. I try to use wood wherever possible, as 3D printing can take a long time. The STL files included are for the cup, cannon plunger, and end cap.

The end cap holds a TCRT5000 IR sensor, which is used to detect when a shot has been launched. The IR LED inside the sensor is powered from 5 V through a 100 Ω resistor connected to the Din pin. The sensor output is connected to A2 on the Arduino.

Configure A2 as a digital input with the internal pull-up resistor enabled:


pinMode(A2, INPUT_PULLUP);

Under normal operation, A2 will read HIGH. When the plunger passes the sensor and reflects the infrared light, the sensor output changes state, allowing the Arduino to detect that the shot has been fired.

The Electronics

vlcsnap-2026-06-20-23h18m20s199.jpg
Motor Wiring.jpg
Sensor Wiring.jpg
vlcsnap-2026-06-20-23h18m36s790.jpg

Next, we need to connect the motor. To do this, we will solder some pins onto the motor connector so it can interface with the Arduino Uno.

For this setup, we are using Arduino pins 10, 9, and 8 for motor control, but you can change these pins to any available digital pins if you modify the code accordingly.

The connections are:

  1. Motor PFM (speed control) → Arduino Pin 9
  2. Motor Brake control → Arduino Pin 10
  3. Motor Direction control → Arduino Pin 8
  4. IR sensor output → Arduino A2
  5. Homing switch → Arduino A1
const int brakePin = 10;
const int pfmPin = 9;
const int dirPin = 8;
const int IRpin = A2;
const int homingPin = A1;

Motor Pins and Connections

Pin 1 – PFM (Speed Control)

Connect to an Arduino digital pin (for example, Pin 9). The motor speed is controlled using tone(pin, frequency).

The operating range is approximately 250 Hz to 26 kHz, where an increase of about 1 kHz ≈ 150 RPM.

Pin 2 – Enable

Connect directly to 12V to enable the motor.

Connecting to GND places the motor into standby mode.

Pin 3 – Brake

  1. GND = Brake ON
  2. VCC = Brake OFF
  3. Connect this pin to an Arduino output if you want to control braking through software.

Pin 4 – Direction

  1. VCC = Counter-clockwise (CCW)
  2. GND = Clockwise (CW)
  3. Connect to an Arduino digital pin to control motor direction.

Pins 5, 6, 7 – Not Used

Leave these pins disconnected.

Pins 8, 9, 10 – Ground

Connect to Arduino GND and the motor power supply ground.

Pins 11, 12 – Motor Power Supply

Connect to a 12V power supply (recommended range: 10–13V).

Make sure the Arduino ground and motor power supply ground are connected together so the control signals have a common reference.

The Rail

vlcsnap-2026-06-20-23h19m00s378.jpg
vlcsnap-2026-06-20-23h19m10s898.jpg
vlcsnap-2026-06-20-23h19m29s205.jpg
vlcsnap-2026-06-20-23h19m42s736.jpg
vlcsnap-2026-06-20-23h19m59s575.jpg

For the main assembly, I laser-cut the parts from 3 mm wood panels. Before assembly, the ruler pieces require a few pilot holes to be drilled near the ends, allowing the parts to be securely fastened together using M2 bolts.

The GT2 timing belt is installed by routing it over the motor gear and underneath the slider rail. Make sure the belt is properly aligned and has enough tension so the slider can move smoothly without slipping.

Downloads

The Code

Adjusting the Motor Speed Calibration

The code will need to be adjusted based on your individual setup. Since each motor, spring, and mechanical assembly can behave slightly differently, the PFM frequency values need to be calibrated for your specific system.

I manually tested different motor frequencies and recorded the speed required to consistently hit each target distance. You can repeat this process by testing your launcher and finding the correct PFM frequency values for every 10 cm interval.

My calibrated values were:

// PFM calibration table
float distanceTable[] = {10, 20, 30, 40, 50, 60, 70, 80, 90, 100};

float pfmTable[] = {2000, 3500, 5000, 6000, 6200, 7500, 7900, 8000, 8700, 9000};

The distanceTable represents the target distance (cm), and the pfmTable contains the corresponding motor frequency required to achieve that distance.

You can create your own calibration table by testing your launcher at different frequencies and recording the results.


Adjusting the Initial Launch Velocity

You may also need to adjust the initial launch velocity value in the code. This value depends on the mechanical properties of your launcher, including the spring strength, spring compression distance, and the mass of the ball bearing.

Since each build will be slightly different, you will need to calibrate this value for your own cannon. Adjust the velocity until the calculated projectile path matches the real-world movement of your launcher.

For my setup, I used:

float v = 2.1; // Initial launch velocity

If your spring is stronger, or your ball bearing has a different mass, this value will need to be changed to match your system.


Full code for the machine


/// Projectile Machine by Newson's Electronics
/// June 21, 2026


//// ===============================
// Projectile data
// ===============================
bool shot=true;
int IRsaved = 0;
float val=60;
float g=9.8;
float angle=48;
float v=2.1;// slow speed
//float v=3.1;// fast speed
float x=50;

// Cannon geometry
float r=77.0;
float x_init_offset=18.0;
float h_init_offset=36.0;
float rad;
float x_offset;
float h_offset;

// Pot calibration
const int RAW_0_DEG=623;
const int RAW_80_DEG=941;
const float SLOPE=3.975;

// Pins
const int homingPin=A1;
const int fgPin=2;
const int brakePin=10;
const int pfmPin=9;
const int dirPin=8;
const int IRpin=A2;

// Motor constants
const double PI_VAL=3.14159265;
const double gearDiameter=3.9;
const double circumference=PI_VAL*gearDiameter;
float offsetDistance=3.7;
float targetDistance=50;
float distance=0;

// PFM table
float distanceTable[]={10,20,30,40,50,60,70,80,90,100};
float pfmTable[]={2000,3500,5000,6000,6200,7500,7900,8000,8700,9000};
const int tableSize=10;



double getPFM(double distance){
if(distance<=distanceTable[0])return pfmTable[0];
if(distance>=distanceTable[tableSize-1])return pfmTable[tableSize-1];

for(int i=0;i<tableSize-1;i++){
if(distance>=distanceTable[i]&&distance<=distanceTable[i+1]){
double x0=distanceTable[i];
double x1=distanceTable[i+1];
double y0=pfmTable[i];
double y1=pfmTable[i+1];
double factor=(distance-x0)/(x1-x0);
return y0+factor*(y1-y0);
}
}
return pfmTable[tableSize-1];
}

// Motion planner
double minPFM=300;
double pfmFrequency;
float accelPercent=30;
float decelPercent=25;
float accelerationDistance;
float decelerationDistance;
double rpm;
double speed;
const double rpmPerKHz=150;
unsigned long lastTime;

// State machine
enum State{
WAIT_FOR_SHOT,
MOVING,
WAIT_FOR_RELOAD
};

State state=WAIT_FOR_SHOT;
int IRsensor;

// Setup
void setup(){
Serial.begin(115200);
pinMode(IRpin,INPUT_PULLUP);
pinMode(homingPin,INPUT_PULLUP);
pinMode(fgPin,INPUT);
pinMode(pfmPin,OUTPUT);
pinMode(dirPin,OUTPUT);
pinMode(brakePin,OUTPUT);
homing();
pfmFrequency=getPFM(targetDistance);
lastTime=micros();
}

// Homing
void homing(){
digitalWrite(dirPin,0);
digitalWrite(brakePin,1);
tone(pfmPin,600);

while(digitalRead(homingPin)==HIGH){}

noTone(pfmPin);
distance=offsetDistance;
digitalWrite(dirPin,1);
delay(100);
}

// Projectile calculation
void calculations(){
if (IRsensor <= 56)
{
v = 3.1;
}
else
{
v = 2.35;
}

angle=(analogRead(A0)-RAW_0_DEG)/SLOPE;


rad=angle*PI/180.0;

x_offset=(x_init_offset+r*(1-cos(rad+0.2618)))/1000.0;
h_offset=(h_init_offset+r*sin(rad+0.2618))/1000.0;

float vx=v*cos(rad);
float vy=v*sin(rad);

float t=(vy+sqrt(vy*vy+2*g*h_offset))/g;

x=((vx*t)-x_offset)*100.0;

targetDistance=constrain(x,0,95);
if (angle<43&&v==2.3)
{
targetDistance=62;
}
pfmFrequency=getPFM(targetDistance);

//Serial.print("Angle=");
//Serial.print(angle);
// Serial.print(" Range=");
//Serial.print(" IR=");
// Serial.println(IRsensor);
}

// Motion planner
double motionPlannerPFM(){
float moveDist=targetDistance-offsetDistance;

accelerationDistance=moveDist*(accelPercent/100.0);
decelerationDistance=moveDist*(decelPercent/100.0);

double remaining=targetDistance-distance;

if(remaining<=0)return minPFM;

if(distance-offsetDistance<accelerationDistance){
double factor=(distance-offsetDistance)/accelerationDistance;
return minPFM+factor*(pfmFrequency-minPFM);
}

if(remaining<decelerationDistance){
double factor=remaining/decelerationDistance;
return minPFM+factor*(pfmFrequency-minPFM);
}

return pfmFrequency;
}

// ===============================
// Display function
// ===============================
int getIRAverage()
{
long total = 0;

for(int i=0;i<50;i++)
{
total += analogRead(IRpin);
delay(5);
}

return total / 50;
}


void display(){

//Serial.print("STATE=");

//if(state==WAIT_FOR_SHOT)
//Serial.print("WAIT");
//else if(state==MOVING)
//Serial.print("MOVE");
//else
// Serial.print("RELOAD");


Serial.print(" Angle=");
Serial.print(angle,1);

Serial.print(" Range=");
Serial.print(targetDistance,1);

// Serial.print(" Xoff=");
// Serial.print(x_offset*1000,1);

// Serial.print(" Hoff=");
// Serial.print(h_offset*1000,1);

Serial.print(" IR=");
Serial.print(IRsensor);

Serial.print(" Dist=");
Serial.print(distance,1);

// Serial.print(" RPM=");
// Serial.print(rpm,0);

//Serial.print(" Speed=");
//Serial.print(speed,1);

//Serial.print(" PFM=");
// Serial.println(pfmFrequency,0);
Serial.print(" v=");
Serial.print(v);
Serial.print(" value=");
Serial.print(val);
Serial.println("");
}


// ===============================
// Main loop
// ===============================

void loop(){

IRsensor=analogRead(IRpin);


// always update projectile math
calculations();


switch(state){


case WAIT_FOR_SHOT:


display();

if(IRsensor>100){

//Serial.println("SHOT");

distance=offsetDistance;

lastTime=micros();

digitalWrite(brakePin,1);

state=MOVING;
}

break;



case MOVING:
{

double currentPFM=motionPlannerPFM();


tone(pfmPin,currentPFM);


rpm=(currentPFM/1000.0)*rpmPerKHz;

speed=(rpm*circumference)/60.0;


unsigned long now=micros();

double dt=(now-lastTime)/1000000.0;

lastTime=now;


distance+=speed*dt;


display();



if(distance>=targetDistance){

noTone(pfmPin);

digitalWrite(brakePin,0);


Serial.println("AT TARGET");
IRsensor=analogRead(IRpin);
if (IRsensor<600) {IRsaved=IRsensor;
val = IRsaved * (55.0 / 390.0);
}


state=WAIT_FOR_RELOAD;
}

}
break;



case WAIT_FOR_RELOAD:


display();



if(IRsensor<100){

// Serial.println("RELOAD");


homing();


state=WAIT_FOR_SHOT;
shot= true;
}

break;

}
}


How It Works

World's First Arduino Self Catching Machine (Video 2: How it works)

This video goes into more details about the code and the physics on how it works.