The finished 6-DOF robot arm with an RGB axis triad on the end effector

6-DOF Robot Arm

A 3D-printed six-axis arm, built at home for $700

2023 to present

My twin brother Steve and I built a six-axis robot arm at home for about $700. It started in 2023, when I watched a YouTube video of someone's DIY six-axis arm and wanted to see if we could build one ourselves for under $1,000. Commercial arms with this kind of reach cost $10k to $50k, and the cheap hobby arms I found couldn't move in a straight, repeatable line, so we decided to build our own.

I designed the mechanical side in Fusion 360 and handled sourcing and fabrication. Steve designed the electronics and firmware. This page is mostly the story of the mechanical problems we hit and how we fixed them, plus a high-level look at how the arm figures out where to go and how to get there.

The finished arm in action

Goals

Design Evolution

Actuators

Every joint uses a stepper motor with a planetary gearbox. I bought everything from AliExpress and Amazon, picking the cheapest parts that looked reliable. Steppers made sense for the budget because they're really cheap, they hold position without any feedback, and the drivers are easy to find.

JointMotorReduction
J1 (base)NEMA 1710:1 planetary + 3:1 pulley = 30:1
J2 (shoulder)NEMA 2350:1 planetary, direct
J3 (elbow)NEMA 2350:1 planetary, direct (belt-driven in V1)
J4integrated planetary5.18:1, direct
J5integrated planetary5.18:1 + 3:1 pulley, about 15.5:1
J6integrated planetary5.18:1, direct

J2 and J3 get the biggest motors because they carry most of the arm. The wrist joints (J4 to J6) only move the tool, so small motors with a light reduction are enough.

CAD render of the arm with arrows pointing to each joint's motor
CAD render showing where each joint's motor sits
All six stepper motors and their driver boards laid out on a cutting mat
All six actuators laid out on the bench

Joint 3 redesign

In the first version, J3 (the elbow) was driven by a timing belt that ran the full length of the upper arm. It slipped. The arm couldn't even hold itself up, let alone a payload.

Section of the upper arm CAD showing the long J3 timing belt
The original J3 layout, with the belt running the length of the upper arm
The J3 belt skipping (turn the sound on, you can hear it)

My first guess was that the motor was too weak. So I redesigned the joint to take a bigger stepper, going from a NEMA 17 to a NEMA 23. That didn't fix it. We tensioned the belt harder and harder, and the joint was still jittery and slow.

The real problem was the belt itself. The L-series timing belts we used are meant for metal machines, and they need a lot of tension to work. We were hanging all of that tension on a 3D-printed shaft. Eventually, the shaft that connected the pulley to the elbow just gave out.

The snapped 3D-printed J3 shaft, showing sparse triangular infill
The snapped J3 shaft. You can see the sparse triangular infill: there was almost no material in the load path

Looking at that fracture surface is when it clicked for me. More tension was never going to work, because the part couldn't take it.

We got rid of the belt on J3 entirely and mounted the motor directly at the joint. That adds weight further out on the arm, but J2's 50:1 reduction had enough margin to absorb it. Mounting the motor at the joint also killed the backlash and jitter that the belt had been adding, so we got a better joint out of the failure.

Wiring

With J3 fixed, the arm worked, but it still had three problems:

  1. The base wobbled under load.
  2. Way too many wires ran through the arm.
  3. Joints would occasionally skip, and since the steppers were open loop, nothing noticed.
An early version moving. Notice the wobble and the jitter

The wiring was the worst of the three. Every motor had 6 wires (4 for signals: step, direction, and two limit switches, plus 2 for power), and every one of them ran back to the controller or the power supply. That meant 24 wires coming out of the controller PCB and 14 coming out of the power supply (2 for each motor, plus 2 for the controller). All of it had to snake through a moving arm, where it gets flexed every time a joint turns. That's an assembly nightmare, the wires fatigue, and long signal lines pick up noise.

Diagram of the old wiring with separate lines from every motor back to the controller
The old wiring: every motor runs its own lines back to the controller

Why run 38 wires when 4 will do? We switched to a CAN bus. Instead of a private road to every house, every house sits on one shared street. Each joint gets its own driver board, and all of them hang off the same 4 wires: 2 for CAN and 2 for power. Now only 2 wires come out of the robot controller, and 2 come out of the power supply. Going from 38 to 4 was the single best decision in the whole project.

Diagram of the CAN bus wiring with every joint on one shared bus
The new CAN wiring: one shared bus with 2 CAN wires and 2 power wires

The V2 rebuild

The CAN switch meant redesigning a lot of the arm anyway, so I used the chance to fix the wobble and clean up the rest of the design. The V2 arm is a bit smaller and stiffer than V1:

Isometric CAD view of the V2 arm
Isometric CAD view of the V2 arm

Homing also got an upgrade. V1 used mechanical limit switches, and they broke. Unplugging the microcontroller once caused erratic motion that snapped the J2 switch clean off. V2 uses inductive proximity sensors instead. They output a voltage when metal gets close, with no contact at all, so there's nothing to break.

Electronics

Steve designed all of this, so I'll keep it high level. There are seven boards in total:

The ssRobot controller v3.1 main controller board, held up over a cutting mat
The main controller. There's one of these
A joint driver board mounted on the back of a NEMA 17 stepper motor
A joint driver board. There are six of these

The bigger change is where the work happens. In V1, one microcontroller planned the path and generated every step pulse for all six motors. Now the main controller only sends high-level targets over CAN, and each joint's own microcontroller generates its own step pulses. That's a distributed control system: the processing load is spread across seven boards instead of piled onto one.

We could have bought off-the-shelf CAN servo drivers. But then we wouldn't own the algorithms, and learning those was the whole point.

Fabrication

All of the structural parts are 3D printed. The broken J3 shaft taught me to look hard at infill on anything that carries load.

Inverse Kinematics

Going from joint angles to the tool position (forward kinematics) is easy. You just chain six transforms together. Going the other way is the hard part: you know where you want the tool and which way it should point, and you need the six joint angles that get it there. For a general six-axis arm there's no clean formula for that.

Our arm has one because of the spherical wrist. The last three joint axes all cross at a single point, called the wrist center. Since rotating about an axis can't move a point that sits on that axis, no amount of wrist rotation can move the wrist center. So the problem splits in two:

To find the wrist center, you start at the tool tip and back up along the direction the tool is pointing:

\[\mathbf{p}_{\text{wrist}} = \mathbf{p}_{\text{tool}} - d_6\,\mathbf{a}\]

Here \(\mathbf{p}_{\text{tool}}\) is where we want the tool tip, \(\mathbf{a}\) is the direction the tool points, and \(d_6\) is the distance from the wrist center to the tool tip (56.5 mm on our arm). Once you have the wrist center, J1 is just the angle of that point seen from above. J2 and J3 come from a single triangle formed by the shoulder, the elbow, and the wrist center, which you solve with the law of cosines. With the first three angles known, the leftover rotation gives the three wrist angles.

To get the wrist angles, we split the tool's rotation into two parts: what the arm does and what the wrist does, so \({}^{0}R_{6} = {}^{0}R_{3}\,{}^{3}R_{6}\). Once \(\theta_1\) to \(\theta_3\) are solved, we know the arm's part (\({}^{0}R_{3}\)), so we undo it to get the rotation the wrist still has to make: \({}^{3}R_{6} = \left({}^{0}R_{3}\right)^{\mathsf T}\,{}^{0}R_{6}\).

Here's the neat part. J4 rolls, J5 bends, and J6 rolls again, so \({}^{3}R_{6}\) always has the same form as a set of ZYZ Euler angles, and its last row and column are simple: the bottom-right entry is \(\cos\theta_5\), the last column is \([\cos\theta_4\sin\theta_5,\; \sin\theta_4\sin\theta_5,\; \cos\theta_5]\), and the bottom row is \([-\sin\theta_5\cos\theta_6,\; \sin\theta_5\sin\theta_6,\; \cos\theta_5]\). Writing \(r_{23}\) for the entry in row 2, column 3, that gives:

\[\begin{aligned} \theta_5 &= \arccos(r_{33}) \\ \theta_4 &= \operatorname{atan2}(r_{23},\, r_{13}) \\ \theta_6 &= \operatorname{atan2}(r_{32},\, -r_{31}) \end{aligned}\]

Dividing the other entries cancels out the \(\sin\theta_5\). In plain terms, \(\theta_5\) is the angle between the forearm and the tool, \(\theta_4\) rolls the forearm so the bend goes the right way, and \(\theta_6\) spins the tool into place. If \(\theta_5\) is zero, the tool lines up with the forearm and J4 and J6 spin about the same axis, so only their sum matters. In that case the firmware sets \(\theta_4\) to zero and solves for \(\theta_6\) on its own.

Hand sketch of the arm's joint frames with link lengths a1, a2, a3, d1, d4 and d6
The arm's joint frames and link lengths
Side view CAD drawing of the arm with link dimensions
Side view of the V2 arm with the link dimensions
Kinematic skeleton of the arm with the shoulder-to-wrist triangle drawn in
The kinematic skeleton with the shoulder-to-wrist triangle drawn in

The DH parameters of our arm are \(a_1 = 42\), \(a_2 = 280\), \(a_3 = 92.5\), \(d_1 = 191\), \(d_4 = 310.5\), and \(d_6 = 56.5\) (all in mm).

Path Planning

IK tells you where the joints need to be. It says nothing about how to get there. If you just jump straight to the target position, you're asking for infinite acceleration, and on our arm that meant skipped steps, ringing, and a lot of stress on printed parts.

So the planner shapes the speed along every move. The classic way is a trapezoidal profile: ramp the speed up at a fixed acceleration, cruise, ramp down. The problem is the corners. A trapezoidal profile switches the acceleration on and off instantly, and that sudden change (called jerk) is what shakes the arm, so the motion isn't really smooth.

We use an S-curve instead. It has seven phases: ease into the acceleration, hold it, ease out of it, cruise, then the same three in reverse to stop. A single number, \(k\), sets what fraction of each ramp is spent easing in, and the same fraction is spent easing out. \(k = 0\) gives you a plain trapezoid, and our default is \(k = 0.25\).

Plots of jerk, acceleration, velocity and position over the seven phases of an S-curve move
The seven phases of the S-curve: jerk, acceleration, velocity, and position

Because you never hold the peak acceleration for the whole ramp, the average acceleration is lower than the peak:

\[a_{\text{avg}} = a\,(1 - k)\]

\(a\) is the peak acceleration and \(k\) is the easing fraction. With our defaults (\(a = 200\ \text{mm/s}^2\), \(k = 0.25\)), the arm averages 150 mm/s² during a ramp. The planner has to use this average, not the peak, when it works out how long a move takes and how much distance it needs to slow down.

There's one case where the S-curve is a bad idea. The planner updates 100 times a second, and if an easing phase would be shorter than 20 ms (two updates), the firmware can't actually draw the smooth part. It would just come out as a single jump in speed anyway. So in that case the planner falls back to a trapezoid on purpose.

Acceleration and jerk plots comparing the S-curve and the trapezoidal profile
Acceleration and jerk for the S-curve vs. the trapezoidal profile on the same move

The planner also looks ahead at the moves waiting in the queue, so it doesn't have to come to a full stop at every corner of a multi-segment path. The default cruise speed is 50 mm/s.

Testing

The video below is the arm running coordinated pick and place with the force-controlled gripper: it picks up a cube and a grape and moves them using straight-line Cartesian moves.

The arm doing pick and place with a cube and a grape
GoalTargetResult
Costunder $1,000$700
Motioncoordinated Cartesian pathsworking, see the demo

Specs

Conclusion

We set out to build a six-axis arm for under $1,000, and the finished arm cost about $700. It follows straight-line Cartesian paths, and with the force-controlled gripper on the end, it can pick up and place a cube and a grape.

Getting there took a full rebuild. The elbow went from a belt that couldn't hold the arm up to a motor mounted right at the joint. The wiring went from 38 wires running through the arm to 4 on a shared CAN bus, with a driver board on every joint. A tapered roller bearing fixed the wobbly base, and contactless proximity sensors replaced the mechanical limit switches.

What's next

Things to fix:

Things to add:

The end of the arm also got its own project: a force-controlled gripper that can pick up a potato chip without crushing it, for under $40.

Thanks for reading!