DEV Community

Eric Park
Eric Park

Posted on

Step 5: Moving in Millimeters — Position Tracking with Real Units

Step 5: Moving in Millimeters — Position Tracking with Real Units

Hello everyone. This is Eric Park, 3D Printer Engineer from South Korea.

We are now at Step 5. Here is a quick recap of where we have been:

  • Step 1: Sent 200 pulses, made the motor complete one full revolution
  • Step 2: Controlled speed by adjusting the delay between pulses
  • Step 3: Rotated the motor by a specific angle using a formula
  • Step 4: Added direction control and started tracking angle as a global variable
  • Step 5 (today): Forget angles. We move in millimeters.

This step felt like a real turning point for me. For the first time, the motor started speaking the same language as a real machine.


Why Millimeters?

In Steps 3 and 4, I was controlling the motor by angle — 90 degrees, 180 degrees, and so on. That works fine for a rotating shaft.

But a 3D printer or CNC machine does not think in degrees. It thinks in millimeters. When Klipper receives G1 X50, it moves the X axis to 50mm. Not 1000 degrees, not 4000 steps — just 50mm.

So the question is: how do we convert mm to motor steps?


The Key Formula

A stepper motor has a mechanical chain between it and the actual moving part. In my test setup, I am assuming a GT2 timing belt with a 20-tooth pulley. Here is how the math works:

Pulley teeth × belt pitch = distance per revolution
20 teeth × 2mm = 40mm per revolution
Enter fullscreen mode Exit fullscreen mode

Wait — actually in my setup I am using a simpler approximation:

1 revolution = 10mm movement
200 steps = 10mm
1mm = 200 / 10 = 20 steps
Enter fullscreen mode Exit fullscreen mode

So the conversion is:

steps = distance_mm × steps_per_mm
steps = distance_mm × 20
Enter fullscreen mode Exit fullscreen mode

Example:

  • Move 5mm → 5 × 20 = 100 steps
  • Move 3mm → 3 × 20 = 60 steps
  • Move 0.1mm → 0.1 × 20 = 2 steps

This is defined once at the top of the code, and everything else uses it automatically.


What Changed From Step 4

In Step 4, the function looked like this:

rotateAngle(90, clockwise, 2);   // rotate 90 degrees, clockwise, delay=2
Enter fullscreen mode Exit fullscreen mode

In Step 5, it looks like this:

moveX(5.0, true, 2);   // move 5.0mm in positive direction, delay=2
Enter fullscreen mode Exit fullscreen mode

The interface is now talking about physical distance, not motor rotation. That feels much more natural when you are thinking about building a machine.


The Code

/*
 * step5_position_tracking.c
 * Step 5: Coordinate-based position tracking (mm units)
 *
 * Compile: gcc -o step5 step5_position_tracking.c -lwiringPi -lm
 * Run    : sudo ./step5
 */

#include <wiringPi.h>
#include <stdio.h>
#include <stdbool.h>
#include <math.h>

/* ── Pin definitions (BCM GPIO numbers) ── */
#define STEP_PIN    4
#define DIR_PIN     3
#define ENABLE_PIN  2

/* ── Motor and mechanical settings ── */
#define STEPS_PER_REV  200      /* 360 / 1.8 = 200 steps per revolution */
#define MM_PER_REV     10.0f   /* 1 revolution moves 10mm (adjust for your machine) */
#define STEPS_PER_MM   (STEPS_PER_REV / MM_PER_REV)   /* 20 steps/mm */

/* ── Current position tracking ── */
float currentX = 0.0f;   /* unit: mm */

/* Low-level pulse generation */
void rotateMotor(int steps, int delayMs) {
    for (int i = 0; i < steps; i++) {
        digitalWrite(STEP_PIN, HIGH);
        delay(delayMs);
        digitalWrite(STEP_PIN, LOW);
        delay(delayMs);
    }
}

/*
 * moveX: move by a relative distance in mm
 *
 * distance_mm : how far to move (always positive)
 * positive    : true = forward (+X), false = backward (-X)
 * speedDelay  : delay between pulses (ms)
 */
void moveX(float distance_mm, bool positive, int speedDelay) {

    /* Set direction */
    if (positive) {
        digitalWrite(DIR_PIN, HIGH);
        printf("X+ direction: +%.2fmm\n", distance_mm);
    } else {
        digitalWrite(DIR_PIN, LOW);
        printf("X- direction: -%.2fmm\n", distance_mm);
    }

    /* Convert mm to steps */
    int steps = (int)(distance_mm * STEPS_PER_MM);
    printf("  -> %d steps\n", steps);

    /* Execute */
    rotateMotor(steps, speedDelay);

    /* Update position */
    if (positive) {
        currentX += distance_mm;
    } else {
        currentX -= distance_mm;
    }

    printf("  -> current position: X = %.2fmm\n\n", currentX);
}

int main(void) {

    if (wiringPiSetupGpio() == -1) {
        printf("wiringPi init failed\n");
        return 1;
    }

    pinMode(STEP_PIN, OUTPUT);
    pinMode(DIR_PIN, OUTPUT);
    pinMode(ENABLE_PIN, OUTPUT);

    digitalWrite(ENABLE_PIN, LOW);
    printf("Motor enabled\n\n");

    printf("=== Position tracking test (mm units) ===\n");
    printf("Settings: 1 rev = %.1fmm, 1mm = %.0f steps\n\n",
           MM_PER_REV, STEPS_PER_MM);
    printf("Start: X = %.2fmm\n\n", currentX);

    moveX(5.0f,  true,  2);   /* +5mm */
    moveX(3.0f,  true,  2);   /* +3mm */
    moveX(2.0f,  false, 2);   /* -2mm */
    moveX(1.5f,  true,  2);   /* +1.5mm */

    printf("=== Result ===\n");
    printf("Final position: X = %.2fmm\n", currentX);
    printf("Expected      : 5 + 3 - 2 + 1.5 = 7.5mm\n");

    digitalWrite(ENABLE_PIN, HIGH);
    return 0;
}
Enter fullscreen mode Exit fullscreen mode

Walking Through the Code

STEPS_PER_MM macro:

#define STEPS_PER_MM   (STEPS_PER_REV / MM_PER_REV)
Enter fullscreen mode Exit fullscreen mode

This calculates itself from the two values above it. If I change MM_PER_REV to match my actual machine, everything else updates automatically. That is the benefit of defining constants at the top — I only need to change one number.

currentX global variable:

float currentX = 0.0f;
Enter fullscreen mode Exit fullscreen mode

This keeps track of where the motor is right now. Every time moveX() runs, it adds or subtracts from this value. This is the same idea as Step 4's currentAngle, but now in mm.

The conversion inside moveX():

int steps = (int)(distance_mm * STEPS_PER_MM);
Enter fullscreen mode Exit fullscreen mode

This is the only place the formula appears. The rest of the code just calls moveX() with millimeters. Clean separation between "what I want" and "how to achieve it."


What I Discovered During Testing

When I tested moveX(0.1f, true, 2), the motor moved only 2 steps. That is the smallest possible movement. Anything smaller than 1 / STEPS_PER_MM = 0.05mm rounds down to zero and the motor does not move at all.

This is not a bug — it is a real physical limitation. One step equals one minimum increment. With 20 steps/mm, the resolution is 0.05mm per step.

If I need higher resolution, I would switch the driver to microstepping (for example, 1/16 stepping gives 320 steps/mm, or 0.003mm resolution). But for now, 0.05mm is more than enough for learning.


Adjusting for Your Machine

The MM_PER_REV value depends entirely on your mechanical setup. Here are common examples:

Setup MM_PER_REV
GT2 belt + 20-tooth pulley 40.0
GT2 belt + 16-tooth pulley 32.0
Lead screw, 2mm pitch 2.0
Lead screw, 8mm pitch 8.0

The easiest way to find your actual value: move the motor exactly 200 steps (one revolution) and measure how far the carriage moved with a ruler. That measurement is your MM_PER_REV.


Practice Ideas

  1. Change MM_PER_REV to match your actual machine. Measure 1 revolution and set the correct value.
  2. Try moveX(0.1f, true, 2) — what is the smallest movement you can actually see?
  3. Add a moveY() function for a second axis with different steps_per_mm.
  4. Try calling moveX() with distance_mm = 0. What happens?

What's Next

In Step 5, direction is still manual — I have to type true or false to choose which way to go.

In Step 6, I will change the interface to moveToX(target_mm). You give it a destination, and the code figures out direction automatically. That brings us one step closer to how G-code actually works.

See you next time.


Eric Park, from Daegu, South Korea

Tags: raspberrypi c steppermotor 3dprinting beginners embeddedsystems

Top comments (0)