Lesson 9 of 10

Robotic Arm Basics

Joints, Degrees of Freedom and Reach

A robotic arm is a chain of joints, each driven by one servo, ending in something that grips. The number of independently movable joints is the arm's degrees of freedom, and it decides what the arm can and cannot do. A three-joint arm can reach a lot of places, but it cannot always arrive at them from the angle you want. A human arm has considerably more, which is why you can reach behind a shelf and pick something up sideways.

The three-joint arm in this project is the standard starting design. The base servo rotates the whole arm left and right. The shoulder servo raises and lowers the arm. The gripper servo opens and closes the claw. Add a fourth for an elbow and the arm becomes much more capable, at the cost of more weight and more power.

The single most important thing to understand before you build is that torque is not about weight, it is about weight multiplied by distance. A servo holding a load close to itself has an easy job; the same servo holding the same load at the end of a long arm may not manage at all. This is why a long, elegant arm made from a lightweight material can outperform a shorter, heavier one, and why the object you plan to pick up should be light — a bottle cap or a small foam cube, not a full water bottle.

The shoulder servo carries the whole arm plus whatever is being held, so it works hardest and is the first to strain, buzz or fail. If your arm sags or trembles when extended, that is a mechanical and power problem, not a code problem, and lengthening the arm further will make it worse.

  • Base — rotates the arm horizontally; carries the least load
  • Shoulder — lifts the arm; the hardest-working joint by a wide margin
  • Elbow — optional fourth joint, adds reach and flexibility
  • Gripper — opens and closes; needs precision rather than strength
  • Torque — turning force, and what limits everything; it rises with the length of the arm
Notes
  • Keep the arm short and light for your first build. Card, ice-cream sticks, foam board or thin plywood are all fine, and all of them are easier to modify than a bought kit when a joint turns out to be in the wrong place.

Powering Servos: the Mistake Everyone Makes Once

A single small servo, unloaded, will often run from the Arduino's 5 V pin. This is precisely why so many people get burned by it: the arrangement works on the bench, so it looks correct, and then it fails the moment the arm is real.

A servo is a motor. It draws a serious current when it starts moving, and it keeps drawing current when it is holding a position against gravity — which, for an arm, is nearly all the time. Three servos moving together can pull the Arduino's supply down far enough to reset the board, and USB from a laptop has its own limit that is easily reached too.

The symptom is unmistakable once you know it. The servos jitter and twitch, the Serial Monitor shows your startup message printing over and over, and the arm never completes a movement. That is the board browning out and restarting in a loop, and no change to your code will fix it.

The correct arrangement is a separate 5 V supply for the servos — a dedicated adapter or a battery pack with a suitable regulator — with the Arduino powered separately. Then run the connection that makes it all work: a wire from the servo supply's ground to the Arduino's GND. The servo's signal wire carries a pulse whose voltage means nothing unless both devices agree where zero is. Miss that wire and the servos behave completely erratically even though everything appears connected.

One more Uno-specific detail from the motors lesson, worth repeating because it bites people here: the Servo library takes over the timer that produces PWM on pins 9 and 10, so analogWrite() stops working on those two pins as soon as the library is used. If you are combining an arm with driven wheels, put the motor driver's enable pins elsewhere.

  • Give the servos their own 5 V supply; do not run three of them from the Arduino's 5 V pin
  • Connect the servo supply's ground to the Arduino's GND — always, without exception
  • Servo wires: brown or black is ground, red is power, orange, yellow or white is signal
  • Jitter plus a repeating startup message means the board is browning out, not a code bug
  • Using the Servo library disables analogWrite() on Uno pins 9 and 10
Notes
  • A servo that buzzes loudly without moving is stalled — jammed against a mechanical limit or commanded past the angle it can physically reach. Cut the power promptly. A stalled servo draws its highest current, gets hot, and can strip its plastic gears within seconds.

Building the Arm and Finding Safe Limits

Before attaching a single bracket, centre every servo. Power the board, command each servo to ninety degrees, and only then screw its horn — the plastic arm that bolts to the output shaft — into place. Skip this and you will discover that your shoulder can move fifty degrees one way and only ten the other, because the horn was fitted while the servo was already near the end of its travel. Refitting means dismantling the arm.

Next, find each joint's real limits. A servo will happily accept a command to move somewhere your mechanical structure cannot go, and it will then push against the obstruction until something gives — the gears, the bracket, or the servo. So test each joint alone, moving in small steps, and note the highest and lowest angles it can reach without straining. Put those numbers in your sketch as named constants and never command outside them.

The constrain() function makes this automatic. Wrap every angle you are about to send in constrain(angle, minimum, maximum) and it becomes impossible for a bug elsewhere in the sketch to drive a joint into its own frame. That one habit will save more servos than any other.

Fix the base firmly to something heavy. An arm reaching out has real leverage, and an unsecured base will lift, tip, or drag itself across the table when the arm extends — which looks like a control problem and is not one. Tape or clamp the base to a plank or a heavy book.

Example
#include <Servo.h>

Servo baseServo;
Servo shoulderServo;
Servo gripperServo;

// Limits found by testing each joint by hand. Yours will differ.
const int BASE_MIN = 20,  BASE_MAX = 160;
const int SHOULDER_MIN = 40, SHOULDER_MAX = 140;
const int GRIP_OPEN = 100, GRIP_CLOSED = 55;

void setup() {
  baseServo.attach(6);
  shoulderServo.attach(7);
  gripperServo.attach(8);

  Serial.begin(9600);
  homePosition();
}

// Every command goes through here, so no bug can exceed a joint's limits.
void moveJoint(Servo &joint, int angle, int lowLimit, int highLimit) {
  angle = constrain(angle, lowLimit, highLimit);
  joint.write(angle);
  delay(20);            // give the servo time to actually get there
}

void homePosition() {
  moveJoint(baseServo,     90, BASE_MIN, BASE_MAX);
  moveJoint(shoulderServo, 90, SHOULDER_MIN, SHOULDER_MAX);
  moveJoint(gripperServo,  GRIP_OPEN, GRIP_CLOSED, GRIP_OPEN);
  delay(500);
}

void loop() {
  // nothing yet - the next section fills this in
}
Notes
  • Test one joint at a time with the others disconnected. Debugging three moving joints together is far harder than debugging them one after another, and a mistake with only one servo powered breaks much less.
  • Write down the working limits for each joint on paper and tape it near the arm. You will rebuild this sketch several times and you do not want to rediscover those numbers each time.

Smooth Motion and a Pick-and-Place Sequence

Commanding a servo to jump straight from thirty degrees to a hundred and fifty makes it move as fast as it possibly can. On an arm that means the whole structure whips, the base rocks, and whatever the gripper is holding is flung out. It also loads the joints far harder than a controlled movement does.

The fix is to move in small steps with a short pause between them, which is what the moveSlowly() function below does. It walks the servo one degree at a time from where it is to where you want it, so the arm glides. Changing the step size and the pause length gives you a speed control that costs nothing.

This also removes a subtle bug. write() only sends a target; it returns immediately and does not wait for the servo to arrive. Commanding a new position on the very next line means the servo abandons the first move before it finished. Stepping with pauses guarantees the joint has actually got there before the sequence continues.

A pick-and-place sequence is then just a list of moves in a sensible order, and the order matters more than you would think. Open the gripper before reaching for the object, or the arm will knock it away. Lift before rotating, or the object will be dragged across the table. And return to a known home position at the end, so the next run starts from somewhere predictable rather than from wherever the last run happened to stop.

Example
// Move a joint gradually instead of snapping to the target.
void moveSlowly(Servo &joint, int from, int to, int stepDelay) {
  if (from < to) {
    for (int a = from; a <= to; a++) { joint.write(a); delay(stepDelay); }
  } else {
    for (int a = from; a >= to; a--) { joint.write(a); delay(stepDelay); }
  }
}

void pickAndPlace() {
  // 1. open the gripper BEFORE approaching, or we knock the object away
  moveSlowly(gripperServo, GRIP_CLOSED, GRIP_OPEN, 15);

  // 2. swing the base to the pick-up side
  moveSlowly(baseServo, 90, 45, 15);

  // 3. lower the shoulder onto the object
  moveSlowly(shoulderServo, 90, 130, 15);

  // 4. close the gripper and pause so it can grip firmly
  moveSlowly(gripperServo, GRIP_OPEN, GRIP_CLOSED, 15);
  delay(300);

  // 5. LIFT before rotating, so we do not drag the object
  moveSlowly(shoulderServo, 130, 70, 15);

  // 6. swing across to the drop-off side
  moveSlowly(baseServo, 45, 135, 15);

  // 7. lower, release, and lift clear
  moveSlowly(shoulderServo, 70, 120, 15);
  moveSlowly(gripperServo, GRIP_CLOSED, GRIP_OPEN, 15);
  moveSlowly(shoulderServo, 120, 90, 15);

  // 8. always finish somewhere known
  moveSlowly(baseServo, 135, 90, 15);
}

void loop() {
  pickAndPlace();
  delay(3000);
}
Notes
  • The gripper needs the lightest touch of any joint. Closing too far crushes the object or stalls the servo against it; a foam cube or a bottle cap is forgiving while you find the right angle. Adding a small piece of rubber band or foam to each jaw improves grip enormously and lets you close less tightly.

Driving the Arm by Hand with a Joystick

A joystick module is two potentiometers at right angles plus a push button. Each potentiometer is read with analogRead(), giving 0 to 1023, with roughly the middle value when the stick is at rest. Feed those numbers to the servos and you can pilot the arm by hand, which is far more satisfying than watching a fixed sequence and much more useful for finding good positions to hard-code later.

Do not map the joystick position straight onto an absolute angle. It sounds like the obvious approach, and it produces an arm that jumps violently the instant you power up, because the servos snap to wherever the stick happens to be sitting. It is also exhausting to use — releasing the stick returns the arm to the middle, so you can never let go.

The better approach is rate control: the stick's displacement from centre sets how fast the joint moves, not where it is. Push right and the base rotates right for as long as you hold it. Release and it stays exactly where you left it. This is how real machinery is controlled, and it feels natural within seconds.

Two practical details make it usable. First, add a dead zone around the centre — a small band where the stick is treated as neutral. Real potentiometers do not read exactly the middle value at rest and they drift slightly, so without a dead zone the arm creeps steadily in one direction while you are not touching it. Second, keep every movement inside the joint limits you measured earlier, so that holding the stick against a limit simply stops rather than straining the servo.

Example
const int joyX = A0;      // left / right  -> base
const int joyY = A1;      // up / down      -> shoulder
const int joyButton = 2;  // press          -> toggle the gripper

int baseAngle     = 90;   // remembered positions, not stick positions
int shoulderAngle = 90;
bool gripperClosed = false;

const int CENTRE   = 512;
const int DEADZONE = 80;  // ignore small movements around the centre

void setup() {
  baseServo.attach(6);
  shoulderServo.attach(7);
  gripperServo.attach(8);
  pinMode(joyButton, INPUT_PULLUP);
  homePosition();
}

void loop() {
  int x = analogRead(joyX);
  int y = analogRead(joyY);

  // RATE control: the stick sets speed, not position
  if (x > CENTRE + DEADZONE) baseAngle++;
  if (x < CENTRE - DEADZONE) baseAngle--;
  if (y > CENTRE + DEADZONE) shoulderAngle++;
  if (y < CENTRE - DEADZONE) shoulderAngle--;

  baseAngle     = constrain(baseAngle, BASE_MIN, BASE_MAX);
  shoulderAngle = constrain(shoulderAngle, SHOULDER_MIN, SHOULDER_MAX);

  baseServo.write(baseAngle);
  shoulderServo.write(shoulderAngle);

  if (digitalRead(joyButton) == LOW) {          // pull-up: LOW is pressed
    gripperClosed = !gripperClosed;
    gripperServo.write(gripperClosed ? GRIP_CLOSED : GRIP_OPEN);
    delay(250);                                 // crude debounce for a demo
  }

  delay(20);        // this sets how fast the arm sweeps
}
Notes
  • The final delay(20) is the arm's speed control. A larger value makes the arm move more slowly and gives you finer control; a smaller one makes it quicker and twitchier. Adjust it to taste before you change anything else.
  • The button here uses a rough delay() debounce, which is acceptable for a demonstration but blocks the arm for a quarter of a second. Replacing it with the millis() debounce from the sensors lesson is a good exercise once the arm works.
Ask AI