Built-in project · Arduino Uno R3

Robot arm inverse kinematics on an Arduino Uno

Two knobs set where the claw should be in millimeters, and the Uno solves the shoulder and elbow angles with the law of cosines, the way real arms are steered. The whole build, an Arduino Uno and 3 more parts, runs here in your browser on the firmware below; open it in the editor to change the wiring or the code and run it again.

Teaching? Assign it to your class in one click.

Advanced About 30 minutes to build and run. Runs in your browser. Free, and no account needed.

The parts retail for about $56. On Mokxi they're free.

The circuit runs here once the page has loaded.

What you'll learn

  • Inverse kinematics: from a target position to joint angles
  • The law of cosines on a two-link arm
  • Reach limits and servo limits
  • Mapping knob readings to millimeters with map
  • Returning success or failure from a function
  • Difficulty: Advanced
  • Time: About 30 minutes
  • Board: Arduino Uno R3
  • Parts: 6

How it works

Two potentiometers choose a target point for the claw, measured from the shoulder. The knob on A0 sets x, how far forward, from 20 to 150 mm. The knob on A1 sets y, how high, from -60 to 120 mm.

The sketch then runs inverse kinematics: given where the claw should be, find the shoulder and elbow angles. The upper arm (72 mm) and forearm (84 mm) make a triangle with the line to the target, and the law of cosines gives the angles.

If the target is too far away, too close, or needs a servo past its limits of 40 to 160 degrees, the arm holds still and the Serial Monitor says so.

Parts list

6 parts, plus the jumper wires. They retail for about $56; on Mokxi they're free.

Wiring table

Every connection, pin by pin, read from the circuit itself. Each row joins the pin on the left to the pin on the right, by a jumper wire or a breadboard strip.

Part and pinConnects to
Arduino Uno R3 pin GNDGround
Robot arm, 4 servos pin GRIPArduino Uno R3 pin 9
Robot arm, 4 servos pin ELBOWArduino Uno R3 pin 6
Robot arm, 4 servos pin SHOULDERArduino Uno R3 pin 5
Robot arm, 4 servos pin BASEArduino Uno R3 pin 3
Potentiometer (1) pin 3Arduino Uno R3 pin 5V
Potentiometer (2) pin 3Arduino Uno R3 pin 5V
Potentiometer (1) pin 1Arduino Uno R3 pin GND
Potentiometer (2) pin 1Arduino Uno R3 pin GND
Potentiometer (1) pin 2Arduino Uno R3 pin A0
Potentiometer (2) pin 2Arduino Uno R3 pin A1
Robot arm, 4 servos pin VCC5 V
Robot arm, 4 servos pin GNDGround

The code

The firmware exactly as the editor opens it. Change a line there and press Run: it compiles in the browser.

sketch.ino · Arduino Uno R3
// Robot arm inverse kinematics: say where the claw should be, in
// millimeters, and the sketch works out the shoulder and elbow angles.
//
//   arm      BASE pin 3, SHOULDER pin 5, ELBOW pin 6, GRIP pin 9
//            VCC and GND from a separate 5 V supply, GND joined to the Uno's
//   knobs    target x (how far forward) on A0, target y (how high) on A1
//
// The arm is two links seen from the side: the upper arm, L1 long, from the
// shoulder to the elbow, and the forearm, L2 long, from the elbow to the
// claw. Measured from the shoulder, x is forward and y is up.
//
// FORWARD KINEMATICS is the easy way round: given the two link angles,
// where is the claw? With both angles measured up from level,
//
//     elbow = (L1 cos(upper), L1 sin(upper))
//     claw  = elbow + (L2 cos(fore), L2 sin(fore))
//
// INVERSE KINEMATICS is the way round a robot needs: given the claw's
// (x, y), which angles? The shoulder, the elbow and the claw make a
// triangle with sides L1, L2 and d, the straight line from shoulder to
// claw. The law of cosines gives the angle at the shoulder between d and
// the upper arm:
//
//     d = sqrt(x*x + y*y)
//     a = acos((L1*L1 + d*d - L2*L2) / (2 * L1 * d))
//     upper = atan2(y, x) + a          (the elbow above the line, not below)
//
// That puts the elbow at (L1 cos(upper), L1 sin(upper)), and the forearm
// simply points from there to the target. If d is longer than L1 + L2 the
// claw cannot reach, and acos would be handed a number past 1.
//
// THIS ARM'S SERVOS. On the desktop arm the shoulder servo leans the upper
// arm: 90 is upright and 180 leans fully forward, so the servo angle is
// 180 minus the upper arm's angle above level. The elbow servo sets the
// forearm through a side linkage: 90 is level and 180 points straight up,
// so the servo angle is 90 plus the forearm's angle above level.

#include <Servo.h>
#include <math.h>

Servo base, shoulder, elbow, claw;

// The two link lengths in millimeters, shoulder to elbow and elbow to claw.
// Measure your own arm and put its numbers here.
const float L1 = 72.0;
const float L2 = 84.0;

// The servos stop short of their ends so the arm cannot fold into itself.
const int LOW_END = 40;
const int HIGH_END = 160;

const float DEGREES = 180.0 / M_PI;

// Solve for a target. Returns false, and leaves the angles alone, when the
// target is out of reach or would need a servo past its limits.
bool solve(float x, float y, int &shoulderAngle, int &elbowAngle) {
  float d = sqrt(x * x + y * y);
  if (d > L1 + L2 || d < fabs(L1 - L2) || d < 1) return false;

  // The law of cosines: the angle at the shoulder between the line to the
  // target and the upper arm.
  float a = acos((L1 * L1 + d * d - L2 * L2) / (2 * L1 * d));
  float upper = atan2(y, x) + a;

  // Where that puts the elbow, and the forearm's angle from there.
  float elbowX = L1 * cos(upper);
  float elbowY = L1 * sin(upper);
  float fore = atan2(y - elbowY, x - elbowX);

  // Link angles to this arm's servo angles.
  int s = (int)lround(180 - upper * DEGREES);
  int e = (int)lround(90 + fore * DEGREES);
  if (s < LOW_END || s > HIGH_END || e < LOW_END || e > HIGH_END) return false;
  shoulderAngle = s;
  elbowAngle = e;
  return true;
}

int lastX = -1000, lastY = -1000;

void setup() {
  base.attach(3);
  shoulder.attach(5);
  elbow.attach(6);
  claw.attach(9);
  base.write(90);
  claw.write(60);
  Serial.begin(115200);
  Serial.println("inverse kinematics arm: turn the knobs to move the target");
}

void loop() {
  // The knobs give the target: x from 20 to 150 mm, y from -60 to 120 mm.
  int x = map(analogRead(A0), 0, 1023, 20, 150);
  int y = map(analogRead(A1), 0, 1023, -60, 120);

  int s, e;
  bool reached = solve(x, y, s, e);
  if (reached) {
    shoulder.write(s);
    elbow.write(e);
  }

  // Report each new target once.
  if (x != lastX || y != lastY) {
    lastX = x;
    lastY = y;
    Serial.print("target x ");
    Serial.print(x);
    Serial.print(" mm  y ");
    Serial.print(y);
    Serial.print(" mm  ->  ");
    if (reached) {
      Serial.print("shoulder ");
      Serial.print(s);
      Serial.print("  elbow ");
      Serial.println(e);
    } else {
      Serial.println("out of reach or past a joint limit, holding");
    }
  }
  delay(20);
}

Code walkthrough

The key functions of the sketch, and what each one does.

solve sketch.ino

Finds the distance d from shoulder to target. If d is longer than the two links together, or shorter than their difference, it returns false. Otherwise the law of cosines gives the shoulder angle, the elbow position follows, and the forearm points at the target. Then it converts link angles to this arm's servo angles and rejects anything outside 40 to 160.

// Solve for a target. Returns false, and leaves the angles alone, when the
// target is out of reach or would need a servo past its limits.
bool solve(float x, float y, int &shoulderAngle, int &elbowAngle) {
  float d = sqrt(x * x + y * y);
  if (d > L1 + L2 || d < fabs(L1 - L2) || d < 1) return false;

  // The law of cosines: the angle at the shoulder between the line to the
  // target and the upper arm.
  float a = acos((L1 * L1 + d * d - L2 * L2) / (2 * L1 * d));
  float upper = atan2(y, x) + a;

  // Where that puts the elbow, and the forearm's angle from there.
  float elbowX = L1 * cos(upper);
  float elbowY = L1 * sin(upper);
  float fore = atan2(y - elbowY, x - elbowX);

  // Link angles to this arm's servo angles.
  int s = (int)lround(180 - upper * DEGREES);
  int e = (int)lround(90 + fore * DEGREES);
  if (s < LOW_END || s > HIGH_END || e < LOW_END || e > HIGH_END) return false;
  shoulderAngle = s;
  elbowAngle = e;
  return true;
}

setup sketch.ino

Attaches the four servos: base on pin 3, shoulder on 5, elbow on 6 and claw on 9. It parks the base at 90 and the claw at 60, since only the shoulder and elbow are solved. It also starts the Serial Monitor at 115200.

void setup() {
  base.attach(3);
  shoulder.attach(5);
  elbow.attach(6);
  claw.attach(9);
  base.write(90);
  claw.write(60);
  Serial.begin(115200);
  Serial.println("inverse kinematics arm: turn the knobs to move the target");
}

loop sketch.ino

Reads both knobs, turns them into x and y in millimeters, and calls solve. If it succeeds, the shoulder and elbow servos get the new angles. If not, they stay where they were. A line prints only when the target changes.

void loop() {
  // The knobs give the target: x from 20 to 150 mm, y from -60 to 120 mm.
  int x = map(analogRead(A0), 0, 1023, 20, 150);
  int y = map(analogRead(A1), 0, 1023, -60, 120);

  int s, e;
  bool reached = solve(x, y, s, e);
  if (reached) {
    shoulder.write(s);
    elbow.write(e);
  }

  // Report each new target once.
  if (x != lastX || y != lastY) {
    lastX = x;
    lastY = y;
    Serial.print("target x ");
    Serial.print(x);
    Serial.print(" mm  y ");
    Serial.print(y);
    Serial.print(" mm  ->  ");
    if (reached) {
      Serial.print("shoulder ");
      Serial.print(s);
      Serial.print("  elbow ");
      Serial.println(e);
    } else {
      Serial.println("out of reach or past a joint limit, holding");
  // ... 4 more lines in the editor

Troubleshooting

The usual mistakes with this circuit, and the fix for each.

  • The arm never moves and the monitor says out of reach This project
    The target is outside what 72 mm and 84 mm links can reach. Turn the knobs toward the middle of their range.
  • The arm jerks or the Uno resets This project
    Power the servos from the separate 5 V supply, and join its ground to the Uno's ground.
  • The claw misses the target on a different arm This project
    Measure your own arm and change L1 and L2 to match.
  • The arm jerks, resets the board or moves weakly when several joints move at once. Robot arm
    Four SG90 servos can pull an amp or more together. Power them from a separate 5 V supply with a 470 uF or larger capacitor. Join its ground to the board's ground.
  • A joint hits its end stop and buzzes, or the claw crushes. Robot arm
    Limit each servo to a safe angle range in code and move gradually in small steps. Find the open and closed angle of the claw by testing, and never command beyond them.
  • The reading jumps around or only covers part of the range. Potentiometer
    Wire the two outer legs to 5 V and ground and the middle leg (the wiper) to an analog pin. Using the wrong two legs gives a dead or partial range.
  • Turning the knob does nothing, or the value is stuck at 0 or 1023. Potentiometer
    Check that the wiper goes to an analog input (A0 to A5 on an Uno), not a digital pin. Make sure both ends are connected, since a floating end gives noise.
  • Upload fails or nothing runs after wiring pins 0 and 1 to a module. Arduino Uno R3
    Pins 0 and 1 are the USB serial port. Move the module to other pins, or unplug it while uploading. Also check the right board is selected.

Change it and keep it

Open it in the editor, change the circuit or the code, and keep your version in a free account.