A drawing robot (pen plotter) is a robot that holds a pen or marker and moves it across a surface in precise, programmed paths — the simplest version uses the robot arm from the previous article with a pen holder attached to the end effector and a pen-lift servo that raises the pen between strokes, while more capable designs use a Cartesian XY stage (like a 3D printer without the extruder) for rectangular drawing areas, or a polar coordinate system for circular artwork, enabling the robot to reproduce vector graphics, geometric patterns, and even handwriting automatically.
Introduction
A drawing robot sits at an unusual intersection in robotics: it is precise enough to produce reproducible geometric art, physical enough to engage the mechanical world, and creative enough to produce results that are genuinely beautiful. Among all the robots in this series, the drawing robot tends to generate the most audience reaction — people are fascinated watching a machine produce something that looks like human craft.
More technically, the drawing robot introduces three new concepts that appear throughout robotics. First, coordinate systems — the mathematical framework that maps robot joint angles to positions on the drawing surface, and back. Second, path planning — deciding not just where to go, but the sequence and continuity of moves to produce a desired output without lifting the pen unnecessarily. Third, G-code — the standardized language used by plotters, CNC machines, and 3D printers to describe motion sequences, which your drawing robot can interpret to produce arbitrary graphics from computer-generated design files.
Two architectures dominate hobby drawing robots and are both worth understanding: the polar arm plotter (built on the robot arm from Article 78) and the Cartesian XY stage (the simpler, more precise alternative for rectangular media). This article builds the arm-based polar plotter first, then shows the Cartesian alternative, and finishes with the G-code interpretation that lets both draw computer-generated graphics.
Architecture 1: The Arm-Based Polar Plotter
The robot arm from Article 78 becomes a drawing robot with two modifications: replace the gripper with a pen holder, and add a pen-lift mechanism that raises the pen off the paper between strokes.
Pen Holder Design
The pen holder mounts on the arm’s wrist or end effector plate. It must:
- Hold the pen perpendicular to the paper surface (or at a consistent angle)
- Allow the pen to slide vertically (for pen lift)
- Apply consistent downward pressure when the pen is lowered
Pen holder cross-section:
[Pen lift servo arm]
│
┌───────┴───────┐
│ Pen holder │ ← small tube or bracket matching pen diameter
│ │
│ [PEN] │ ← pen can slide up/down ~10mm
└───────────────┘
│
Paper surface
Pen lifted: servo rotates arm, pushing pen holder upward → pen clears paper
Pen down: servo rotates back, allowing pen weight to contact paper
(gravity provides gentle, consistent contact pressure)
Simple pen lift with a micro servo:
#include <Servo.h>
Servo penLiftServo;
const int PEN_LIFT_PIN = 6;
const int PEN_UP = 60; // Servo angle when pen is lifted (adjust for your mechanism)
const int PEN_DOWN = 30; // Servo angle when pen contacts paper
void penUp() {
penLiftServo.write(PEN_UP);
delay(150); // Allow time for servo to fully lift pen before moving arm
}
void penDown() {
penLiftServo.write(PEN_DOWN);
delay(150); // Allow time for pen to settle before drawing
}
The delay after pen lift/lower is important. If the arm starts moving before the pen is fully lifted, the pen drags across the paper during repositioning — producing unwanted lines. Typical servo travel time for 30° is ~100ms at 4.8V; 150ms gives comfortable margin.
Coordinate Systems: Mapping Angles to Paper Position
The arm-based plotter works in polar coordinates — position is described by a radial distance from the base (r) and an angle around the base (θ). But humans and most design software think in Cartesian coordinates (x, y). The robot must convert between them.
Polar Coordinate System
Arm plotter coordinate system (top view):
y
│
│
────────────┼──────────── x
│ BASE
│
A point on the paper at Cartesian (x, y) from the arm base:
Polar distance: r = sqrt(x² + y²)
Polar angle: θ = atan2(y, x) [in radians, use atan2f() in C]
For the arm to position its end effector at this point:
Base servo J1: rotate to θ (converted from radians to servo degrees)
Shoulder + elbow: configure to achieve reach r at the correct height z
Cartesian-to-Joint-Angle Conversion (Inverse Kinematics for Drawing)
For drawing on a flat horizontal surface, the arm holds the pen at a fixed height z. The problem reduces to positioning the arm’s 2D planar reach at the correct radial distance r from the base while maintaining that height. This is the inverse kinematics problem:
Given target point (x, y) on paper:
Step 1: Convert to polar
r = sqrt(x² + y²)
theta_base = atan2f(y, x) → base servo angle
Step 2: 2-link inverse kinematics for planar reach r at height z_draw
Using the law of cosines:
d = sqrt(r² + z_draw²) [3D distance from shoulder joint to target]
cos(elbow_angle) = (d² - L1² - L2²) / (2 × L1 × L2)
elbow_angle = acos(result) [in radians; convert to degrees for servo]
alpha = atan2f(z_draw, r) [angle of line from shoulder to target]
beta = acos((d² + L1² - L2²) / (2 × d × L1))
shoulder_angle = alpha + beta [or alpha - beta for elbow-down configuration]
Step 3: Convert radians to degrees, apply servo offset corrections
servo_base = degrees(theta_base) + BASE_OFFSET
servo_shoulder = degrees(shoulder_angle) + SHOULDER_OFFSET
servo_elbow = degrees(elbow_angle) + ELBOW_OFFSET
In code:
#include <math.h>
// Link lengths in cm
const float L1 = 15.0; // Shoulder to elbow
const float L2 = 12.0; // Elbow to pen tip
// Drawing surface height below shoulder joint (adjust for your setup)
const float Z_DRAW = -10.0; // Pen is 10cm below shoulder when drawing
// Servo offsets (calibrate these to your physical arm assembly)
const int BASE_OFFSET = 90; // servo.write(90) = arm facing forward
const int SHOULDER_OFFSET = 0; // adjust until shoulder horizontal = 90°
const int ELBOW_OFFSET = 0;
struct JointAngles {
float base;
float shoulder;
float elbow;
bool reachable;
};
JointAngles inverseKinematics(float x_cm, float y_cm) {
JointAngles result;
result.reachable = true;
// Step 1: Base angle from Cartesian
result.base = atan2f(y_cm, x_cm) * 180.0 / PI + BASE_OFFSET;
// Step 2: Planar distance from base
float r = sqrtf(x_cm * x_cm + y_cm * y_cm);
float d = sqrtf(r * r + Z_DRAW * Z_DRAW); // 3D reach from shoulder
// Check reachability
if (d > L1 + L2 || d < fabsf(L1 - L2)) {
result.reachable = false;
return result;
}
// Elbow angle (law of cosines)
float cosElbow = (d * d - L1 * L1 - L2 * L2) / (2.0 * L1 * L2);
cosElbow = constrain(cosElbow, -1.0, 1.0); // Clamp for floating-point errors
float elbowRad = acosf(cosElbow);
// Shoulder angle
float alpha = atan2f(-Z_DRAW, r); // Negative: pen is below shoulder
float beta = acosf((d * d + L1 * L1 - L2 * L2) / (2.0 * d * L1));
float shoulderRad = alpha + beta;
result.shoulder = shoulderRad * 180.0 / PI + SHOULDER_OFFSET;
result.elbow = elbowRad * 180.0 / PI + ELBOW_OFFSET;
return result;
}
Architecture 2: The Cartesian XY Plotter
While the arm-based polar plotter is built from existing hardware (the robot arm), a Cartesian XY plotter offers superior precision and a larger, rectangular drawing area more suited to reproducing text and detailed graphics.
How a Cartesian Plotter Works
Cartesian plotter layout:
┌──────────────────────────────────────────┐
│ Y motor → moves pen carriage along Y axis│
│ │
│ Carriage ──────────────────────── Guide │
│ │ │
│ X motor → moves pen along X axis │
│ │ │
│ [PEN] │
│ │
│ DRAWING SURFACE (paper) │
└──────────────────────────────────────────┘
X axis: pen moves left/right (one stepper motor + belt or lead screw)
Y axis: carriage moves forward/backward (second stepper motor)
Z axis: pen up/down (servo motor)
This is identical to a desktop 3D printer without the extruder —
the mechanics are the same, only the tool at the end differs.
Cartesian Plotter with Stepper Motors
For a Cartesian plotter, stepper motors replace servos for X and Y axes — steppers provide precise, repeatable, open-loop position control ideal for plotting:
// Cartesian plotter using AccelStepper library
// Requires: AccelStepper library (install via Arduino IDE Library Manager)
#include <AccelStepper.h>
#include <Servo.h>
// Stepper setup (4-wire stepper with A4988 driver)
// A4988 STEP and DIR pins:
AccelStepper stepperX(AccelStepper::DRIVER, 2, 3); // STEP=2, DIR=3
AccelStepper stepperY(AccelStepper::DRIVER, 4, 5); // STEP=4, DIR=5
Servo penServo;
const int PEN_UP = 70;
const int PEN_DOWN = 40;
// Steps per mm (calibrate for your specific belt/pulley)
// Typical GT2 belt, 20-tooth pulley, A4988 at 1/16 microstepping:
// Steps per mm = (200 × 16) / (20 × 2) = 80 steps/mm
const float STEPS_PER_MM = 80.0;
void setup() {
Serial.begin(9600);
penServo.attach(6);
penServo.write(PEN_UP);
// Configure stepper speeds
stepperX.setMaxSpeed(3000); // steps/second
stepperX.setAcceleration(2000); // steps/second²
stepperY.setMaxSpeed(3000);
stepperY.setAcceleration(2000);
// Home position
stepperX.setCurrentPosition(0);
stepperY.setCurrentPosition(0);
Serial.println(F("Cartesian plotter ready."));
}
void moveTo_mm(float x_mm, float y_mm) {
long x_steps = (long)(x_mm * STEPS_PER_MM);
long y_steps = (long)(y_mm * STEPS_PER_MM);
stepperX.moveTo(x_steps);
stepperY.moveTo(y_steps);
// Run both steppers simultaneously until both reach target
while (stepperX.distanceToGo() != 0 || stepperY.distanceToGo() != 0) {
stepperX.run();
stepperY.run();
}
}
void penUp() { penServo.write(PEN_UP); delay(150); }
void penDown() { penServo.write(PEN_DOWN); delay(150); }
// Draw a line from current position to (x_mm, y_mm) with pen down
void lineTo(float x_mm, float y_mm) {
moveTo_mm(x_mm, y_mm);
}
// Move without drawing from current position to (x_mm, y_mm)
void moveTo(float x_mm, float y_mm) {
penUp();
moveTo_mm(x_mm, y_mm);
penDown();
}
Drawing Geometric Shapes
With either architecture, the same geometric drawing functions apply. These build from simple primitives to complex patterns:
Drawing Primitives
// All coordinates in mm from home position (0,0)
void drawLine(float x0, float y0, float x1, float y1) {
moveTo(x0, y0); // Lift pen, move to start
lineTo(x1, y1); // Draw to end
}
void drawRectangle(float x, float y, float w, float h) {
moveTo(x, y); // Move to top-left corner
lineTo(x + w, y); // Top edge
lineTo(x + w, y + h); // Right edge
lineTo(x, y + h); // Bottom edge
lineTo(x, y); // Left edge (close)
penUp();
}
void drawCircle(float cx, float cy, float r, int segments = 36) {
// Approximate circle with many short line segments
float angleStep = 2.0 * PI / segments;
// Move to start point (pen up)
moveTo(cx + r, cy); // Start at rightmost point
// Draw segments
for (int i = 1; i <= segments; i++) {
float angle = i * angleStep;
float x = cx + r * cosf(angle);
float y = cy + r * sinf(angle);
lineTo(x, y);
}
penUp();
}
void drawPolygon(float cx, float cy, float r, int sides) {
float angleStep = 2.0 * PI / sides;
float startAngle = -PI / 2.0; // Start at top
// Move to first vertex
moveTo(cx + r * cosf(startAngle), cy + r * sinf(startAngle));
for (int i = 1; i <= sides; i++) {
float angle = startAngle + i * angleStep;
lineTo(cx + r * cosf(angle), cy + r * sinf(angle));
}
penUp();
}
Drawing Patterns: Spirograph-Style Curves
The most visually striking drawings from simple robots come from mathematical curves — spirographs, Lissajous figures, and roses:
// Epitrochoid (spirograph-style curve)
// Produces complex looping patterns from two radii and an offset parameter
// Classic spirograph equation: parametric form
void drawEpitrochoid(float cx, float cy,
float R, float r, float d,
int steps = 360) {
// Parameters:
// R = radius of fixed circle
// r = radius of rolling circle
// d = distance from center of rolling circle to pen
// Classic spirograph values to try:
// R=70, r=30, d=50 → 3-petal flower
// R=70, r=10, d=60 → star with 7 points
// R=60, r=25, d=40 → complex looping rose
float tMax = 2.0 * PI * (r / gcd_approx(R, r)); // Full period
float tStep = tMax / steps;
float x0 = cx + (R + r) * cosf(0) - d * cosf(0);
float y0 = cy + (R + r) * sinf(0) - d * sinf(0);
moveTo(x0, y0);
for (int i = 1; i <= steps; i++) {
float t = i * tStep;
float x = cx + (R + r) * cosf(t) - d * cosf((R + r) / r * t);
float y = cy + (R + r) * sinf(t) - d * sinf((R + r) / r * t);
lineTo(x, y);
}
penUp();
}
// Helper: approximate GCD for computing epitrochoid period
float gcd_approx(float a, float b) {
while (b > 0.001) {
float temp = b;
b = fmod(a, b);
a = temp;
}
return a;
}
// Lissajous curve: two sinusoids at different frequencies
void drawLissajous(float cx, float cy, float Ax, float Ay,
float fx, float fy, float phase,
int steps = 300) {
// Try: Ax=50, Ay=50, fx=3, fy=2, phase=PI/4 → classic 3:2 Lissajous
float x0 = cx + Ax * sinf(0);
float y0 = cy + Ay * sinf(phase);
moveTo(x0, y0);
for (int i = 1; i <= steps; i++) {
float t = (float)i / steps * 2.0 * PI;
float x = cx + Ax * sinf(fx * t);
float y = cy + Ay * sinf(fy * t + phase);
lineTo(x, y);
}
penUp();
}
// Polar rose: r = cos(n×θ)
void drawRose(float cx, float cy, float radius, int n, int steps = 360) {
// n petals if n is odd, 2n petals if n is even
// Try: n=3 (3-petal rose), n=5 (5-petal), n=4 (8-petal)
bool firstPoint = true;
for (int i = 0; i <= steps; i++) {
float theta = (float)i / steps * 2.0 * PI;
float r = radius * cosf(n * theta);
float x = cx + r * cosf(theta);
float y = cy + r * sinf(theta);
if (firstPoint) {
moveTo(x, y);
firstPoint = false;
} else {
lineTo(x, y);
}
}
penUp();
}
A Complete Drawing Program
void setup() {
// ... servo/stepper initialization ...
penUp();
delay(1000);
// Center of paper (assuming 150×150mm drawing area)
float cx = 75, cy = 75;
// Draw a series of nested polygons from 3 sides to 9 sides
for (int sides = 3; sides <= 9; sides++) {
float radius = (sides - 2) * 10.0; // Increasing radius
drawPolygon(cx, cy, radius, sides);
delay(200);
}
// Draw a spirograph in the center
drawEpitrochoid(cx, cy, 40, 15, 30, 180);
// Draw a circle framing the composition
drawCircle(cx, cy, 70, 72); // 72 segments → very smooth
// Return home
moveTo(0, 0);
Serial.println(F("Drawing complete!"));
}
Reading G-Code: Toward Computer-Generated Graphics
G-code is the standard language used by CNC machines, laser cutters, 3D printers, and professional plotters to describe motion. A subset of G-code lets your drawing robot reproduce graphics exported from any vector drawing program (Inkscape, Illustrator, online tools).
Relevant G-Code Commands
G-code subset for plotters:
G0 X{x} Y{y} → Rapid move (pen up) to (x, y) in mm
G1 X{x} Y{y} F{f} → Linear move (pen down) to (x, y) at feed rate f
G28 → Home all axes (return to origin)
M3 S255 → Pen down (some plotters use spindle commands for pen)
M5 → Pen up
Example G-code for drawing a 50×50mm square:
G28
M5 ; pen up
G0 X0 Y0 ; move to origin
M3 S255 ; pen down
G1 X50 Y0 ; right 50mm
G1 X50 Y50 ; up 50mm
G1 X0 Y50 ; left 50mm
G1 X0 Y0 ; down 50mm
M5 ; pen up
A Simple G-Code Interpreter
// Minimal G-code interpreter over Serial
// Upload sketch, connect via Serial Monitor at 115200 baud
// Send G-code commands line by line
float currentX = 0, currentY = 0;
bool penIsDown = false;
void parseGCode(String line) {
line.trim();
if (line.startsWith(F(";"))) return; // Comment
if (line.length() == 0) return; // Empty line
// Parse G0 / G1 commands
if (line.startsWith(F("G0")) || line.startsWith(F("G1"))) {
float x = currentX, y = currentY;
// Extract X value
int xIdx = line.indexOf('X');
if (xIdx >= 0) x = line.substring(xIdx + 1).toFloat();
// Extract Y value
int yIdx = line.indexOf('Y');
if (yIdx >= 0) y = line.substring(yIdx + 1).toFloat();
if (line.startsWith(F("G0"))) {
// Rapid move: pen up first
if (penIsDown) { penUp(); penIsDown = false; }
moveTo_mm(x, y);
} else {
// Cutting move: pen must be down
if (!penIsDown) { penDown(); penIsDown = true; }
moveTo_mm(x, y);
}
currentX = x;
currentY = y;
Serial.println(F("ok"));
} else if (line.startsWith(F("G28"))) {
if (penIsDown) { penUp(); penIsDown = false; }
moveTo_mm(0, 0);
currentX = 0; currentY = 0;
Serial.println(F("ok"));
} else if (line.startsWith(F("M3"))) {
penDown(); penIsDown = true;
Serial.println(F("ok"));
} else if (line.startsWith(F("M5"))) {
penUp(); penIsDown = false;
Serial.println(F("ok"));
} else {
Serial.print(F("unknown: "));
Serial.println(line);
}
}
void loop() {
if (Serial.available()) {
String line = Serial.readStringUntil('\n');
parseGCode(line);
}
}
With this interpreter, you can:
- Design artwork in Inkscape (free vector drawing software)
- Export as G-code using the Inkscape “Gcodetools” or “vpype” extension
- Send the G-code file to the plotter via a serial terminal (or write a Python script to pipe it)
- Watch the robot reproduce your design
This workflow transforms the drawing robot from a device that draws hardcoded shapes into a general-purpose plotter for any vector artwork.
Calibration: Getting Accurate Drawings
Even perfectly written code produces distorted drawings without calibration. These are the main calibration steps:
Step Calibration (Cartesian Plotter)
Command the robot to move exactly 100mm in X. Measure the actual distance traveled. Adjust STEPS_PER_MM:
// Calibration: command 100mm, measure actual distance
// Actual 100mm, commanded 100mm → steps_per_mm is correct
// Actual 96mm, commanded 100mm → robot is moving too little
// New STEPS_PER_MM = old × (100 / 96) = old × 1.0417
float calibrateStepsPerMm(float commandedMm, float measuredMm) {
return STEPS_PER_MM * (commandedMm / measuredMm);
}
Pen Pressure Calibration
The pen’s downward pressure affects line width and consistency. Too light and lines are scratchy or missing. Too heavy and the pen drags, causing the arm to resist motion and distort paths.
Adjust PEN_DOWN servo angle until lines are consistent and slightly darker than the background without requiring the servo to hold a tense position. The ideal is gravity-assisted contact: the pen’s own weight provides the pressure, with the servo only guiding position, not forcing the pen down.
IK Accuracy Calibration (Arm Plotter)
Command the arm to a known point (e.g., 10cm directly in front of the base), then measure where the pen actually lands. Adjust servo offsets (BASE_OFFSET, SHOULDER_OFFSET, ELBOW_OFFSET) until the pen lands at the commanded position:
// Calibration grid: draw a grid of points, measure their actual positions
void drawCalibrationGrid() {
for (float x = -10; x <= 10; x += 5) {
for (float y = 10; y <= 25; y += 5) {
JointAngles angles = inverseKinematics(x, y);
if (angles.reachable) {
baseServo.write(angles.base);
shoulderServo.write(angles.shoulder);
elbowServo.write(angles.elbow);
delay(500);
penDown();
delay(200); // Mark dot
penUp();
}
}
}
}
// Measure the printed dots, note systematic offset or distortion,
// adjust link length constants and servo offsets accordingly
Path Optimization: Drawing Smarter
A naive plotter draws each shape in the order it was programmed, lifting the pen and repositioning between every stroke regardless of whether the next stroke starts near the current pen position. This wastes time and creates unnecessary rapid-move marks (if pen lift timing is imperfect).
Path optimization reorders drawing operations to minimize pen-up travel:
// Simple nearest-neighbor path optimization
// Input: array of line segments (start, end points)
// Output: reordered segments minimizing total pen-up distance
struct Segment {
float x0, y0; // Start point
float x1, y1; // End point
};
float distance(float x0, float y0, float x1, float y1) {
return sqrtf((x1-x0)*(x1-x0) + (y1-y0)*(y1-y0));
}
void optimizePath(Segment segments[], int n) {
float currentX = 0, currentY = 0;
for (int i = 0; i < n; i++) {
// Find nearest undrawn segment start (or end — segments can be reversed)
int nearest = i;
float minDist = 1e9;
bool reversed = false;
for (int j = i; j < n; j++) {
float distToStart = distance(currentX, currentY, segments[j].x0, segments[j].y0);
float distToEnd = distance(currentX, currentY, segments[j].x1, segments[j].y1);
if (distToStart < minDist) { minDist = distToStart; nearest = j; reversed = false; }
if (distToEnd < minDist) { minDist = distToEnd; nearest = j; reversed = true; }
}
// Swap nearest segment into position i
Segment temp = segments[i];
segments[i] = segments[nearest];
segments[nearest] = temp;
// Reverse if approaching from the end
if (reversed) {
float tx = segments[i].x0; segments[i].x0 = segments[i].x1; segments[i].x1 = tx;
float ty = segments[i].y0; segments[i].y0 = segments[i].y1; segments[i].y1 = ty;
}
currentX = segments[i].x1;
currentY = segments[i].y1;
}
}
For complex drawings with many strokes, nearest-neighbor path optimization can reduce drawing time by 30–60% by eliminating long repositioning moves.
Troubleshooting Reference
| Problem | Likely Cause | Fix |
|---|---|---|
| Lines not smooth — jagged steps visible | Steps-per-mm too low; microstepping not enabled | Enable 1/16 microstepping on A4988; recalibrate steps/mm |
| Drawing is distorted (correct shape, wrong size) | Steps-per-mm wrong | Calibrate by commanding 100mm and measuring result |
| Pen leaves marks during repositioning | Pen-up delay too short | Increase delay after penUp() before movement starts |
| Arm plotter draws curves instead of straight lines | IK errors compound along path; link lengths wrong | Recalibrate link lengths; increase path segment count |
| Robot draws first stroke correctly, drifts on subsequent strokes | Stepper losing steps (too fast or insufficient current) | Reduce max speed; increase A4988 current trim |
| Pen pressure inconsistent | Pen-down servo angle wrong; pen weight insufficient | Adjust PEN_DOWN angle; use heavier pen or add small weight |
| Circles appear as polygons | Too few segments in drawCircle() | Increase segment count to 72+ |
| G-code interpreter misses coordinates | toFloat() failing on malformed G-code | Add error checking; verify G-code sender line endings (LF not CR+LF) |
The drawing robot represents the intersection of precision mechanics, coordinate geometry, and creative output. Building one develops skills that transfer immediately to CNC machining, laser cutting, 3D printing, and any other system that moves a tool through programmed paths.
The two architectures — arm-based polar plotter and Cartesian XY stage — illustrate a fundamental design trade-off in robotics: the arm is mechanically simpler (built from existing hardware) but geometrically complex (requires inverse kinematics, polar coordinate conversion, inherent nonlinearities from joint angles). The Cartesian stage is mechanically more complex but geometrically trivial (steps directly map to millimeters with no conversion needed), which is why Cartesian layouts dominate CNC machines and 3D printers where precision is paramount.
The G-code interpreter, even in its simplified form, opens the robot to any computer-generated vector graphics — transforming it from a device that executes hardcoded geometric programs into a general-purpose output device for design software. Combined with path optimization, it produces results at quality and speed that would impress anyone who hasn’t watched it happen.
Designing for Reproducibility: The Engineering of Repeatability
A drawing robot’s output is only as impressive as its repeatability — the ability to draw the same shape in the same place every time it runs. Understanding the sources of irreproducibility helps design against them.
Mechanical Sources of Error
Backlash: In gear trains, belt drives, and lead screws, there is always a small amount of play between components — the follower (belt, nut, gear tooth) can move a small distance before the driver engages. When a motor reverses direction, the follower must travel through this backlash distance before actual output movement resumes. On a plotter, this manifests as corners that aren’t sharp — the pen continues briefly in the old direction before the new direction takes effect.
Backlash effect on a square corner:
Intended: Actual (with backlash):
┌── ┌──
│ │ ← slight overrun before
└── └────── reversal takes effect
Measurement: command a 10mm move, reverse 5mm, forward 5mm.
Net should be 10mm from start.
If less: backlash measured = 10mm - actual distance.
Compensation:
When reversing direction: move an extra BACKLASH_COMP steps before
counting position, to take up the gear play.
// Backlash compensation for Cartesian plotter
const float BACKLASH_X_MM = 0.3; // Measure for your machine
const float BACKLASH_Y_MM = 0.2;
int lastDirectionX = 1; // +1 or -1
int lastDirectionY = 1;
void moveTo_mm_compensated(float x_mm, float y_mm) {
// Determine movement direction
int dirX = (x_mm > currentX) ? 1 : -1;
int dirY = (y_mm > currentY) ? 1 : -1;
// Apply backlash compensation on direction reversal
if (dirX != lastDirectionX && x_mm != currentX) {
long comp = (long)(BACKLASH_X_MM * STEPS_PER_MM * dirX);
stepperX.move(comp); // Take up backlash
while (stepperX.distanceToGo() != 0) stepperX.run();
}
if (dirY != lastDirectionY && y_mm != currentY) {
long comp = (long)(BACKLASH_Y_MM * STEPS_PER_MM * dirY);
stepperY.move(comp);
while (stepperY.distanceToGo() != 0) stepperY.run();
}
lastDirectionX = dirX;
lastDirectionY = dirY;
// Now move to target
moveTo_mm(x_mm, y_mm);
}
Belt stretch: GT2 belts stretch slightly under tension, particularly on long spans. A longer belt spans more distance than the belt’s nominal pitch predicts, making distant positions appear shifted. This produces drawings that are accurate near the home position but drift progressively at the far end of the travel. Fix: tighten belts consistently and use the calibration grid to measure and correct for this drift.
Thermal expansion: Metal components expand with temperature. For a small desktop plotter operating indoors, this effect is negligible (aluminum expands ~23 µm per meter per °C — less than 0.5mm across a 150mm span over a 10°C temperature change). For precision CNC machining, it matters significantly.
Electrical Sources of Error
Stepper motor step loss: If a stepper motor is driven faster than its torque can sustain, or if the load torque exceeds the motor’s detent torque, it loses steps — skips tooth positions. The controller doesn’t know this has happened (open-loop control). All subsequent positions are offset by the lost steps. On a plotter, this produces all-subsequent-strokes being shifted by the same amount — drawings that start correctly and then drift.
Diagnosing step loss:
1. Draw a vertical line from y=0 to y=100mm
2. Return home (G28 / moveTo(0,0))
3. Draw a second vertical line next to the first
4. The two lines should be perfectly parallel
5. If the second line is offset: steps were lost during the first pass
Fix:
Reduce maximum speed (setMaxSpeed())
Reduce acceleration (setAcceleration())
Increase motor current (A4988 vref trimmer — carefully, too high causes overheating)
Add cooling to motor driver if drawing many complex pieces back-to-back
Making the Drawing Area Work For You
A common mistake is treating the robot’s full physical travel range as the drawing area. The edges of the travel range are where mechanical imperfections are greatest (near-singularity for arm plotters, belt tension issues at travel limits for Cartesian). The best drawings come from using the middle 60–70% of the available range, where the robot is most accurate.
Setting a Logical Origin
Rather than always drawing from the absolute home position (0,0), define a logical origin that places the drawing in the sweet spot of the working area:
// Logical coordinate offset: add to all coordinates before sending to machine
const float ORIGIN_X = 20.0; // mm offset from home
const float ORIGIN_Y = 20.0;
void drawAt(float logical_x, float logical_y) {
moveTo_mm(ORIGIN_X + logical_x, ORIGIN_Y + logical_y);
}
void lineAt(float logical_x, float logical_y) {
lineTo_mm(ORIGIN_X + logical_x, ORIGIN_Y + logical_y);
}
// Now all drawing code uses logical 0,0 as the bottom-left of the drawing area,
// and the machine adds the offset to keep it in the accurate zone.
Media Setup
The drawing surface affects output quality as much as the robot mechanics:
Paper: 80gsm copy paper works fine for markers and ballpoint pens. For fine-liner pens (0.1–0.3mm tip), heavier weight paper (100–120gsm) provides better surface texture and prevents ink bleed. Tape the paper flat to the plotter bed — even slight paper curl causes inconsistent pen contact.
Pen choice: Fine-liner pens (Micron, Staedtler pigment liner) produce the cleanest lines and don’t bleed or dry at the tip during pauses. Ballpoint pens require more contact pressure. Felt-tip pens blur at slow speeds (ink pools). For the clearest results, start with a 0.3mm fine-liner.
Preventing smearing: If the pen path crosses previous strokes before the ink is dry, the crossings smear. Path optimization (drawing strokes in geographic order, allowing ink to dry before returning to that region) reduces this, or use quick-drying pigment inks.
Taking the Drawing Robot Further
Once the basic plotter is working, these extensions significantly expand its capability:
Multiple pen colors: A servo-driven pen carousel holds several pens at fixed positions. The robot drives to the carousel, lifts the current pen, picks up a new color, and returns to the drawing area. Multi-color plotter art is striking and surprisingly achievable with a pen changer.
Image to vector conversion: Tools like Inkscape’s “Trace Bitmap” or the command-line tool potrace convert raster images (photographs, scanned drawings) into vector paths that can be exported as G-code for the plotter. Feed a photo into Inkscape → trace → export G-code → pipe to plotter: your robot draws a stylized version of the photo.
Generative art algorithms: Instead of fixed geometric programs, generate drawing coordinates algorithmically — Lindenmayer systems (L-systems) that produce fractal plant shapes, cellular automata patterns, reaction-diffusion patterns, or Perlin noise fields that generate organic, non-repeating textures. The plotter becomes an interface between mathematical processes and physical ink.
Dual-surface plotting: Mount the robot arm over a rotating turntable carrying the paper. The combination of arm rotation (base servo) and turntable rotation creates compound motion that produces interference patterns impossible to draw with fixed-base Cartesian motion. This is the principle behind spirograph machines — mechanical rather than digital, but the mathematical relationship is identical to the epitrochoid equations presented earlier.
Each of these extensions builds on the same foundation: precise positioning, reliable pen contact, and a coordinate system that maps robot motion to drawing surface coordinates. The drawing robot is not a finished endpoint — it’s a platform whose capabilities grow as your understanding of its geometry deepens.




