Community project

Magnetic Chess Piece Mover

Photo of Magnetic Chess Piece Mover
Generated with AI

. Tito.

Published November 11, 2026

This project builds an automated chess piece mover using stepper motors and a lifting electromagnet to position and retrieve pieces on a chessboard. The system uses three NEMA 17 motors controlled by an ESP32 to move a magnetic head across the X and Y axes and raise or lower it on the Z axis, with home switches ensuring precise calibration.

The guide provides a complete wiring diagram, full parts list, and tested firmware that handles motor control, homing sequences, and magnet activation. Assembly covers building the moving frame, installing the three limit switches, connecting each motor to its A4988 driver, wiring the ESP32 control lines, safely integrating the 5V electromagnet circuit, and testing movement before operation.

Wiring diagram

Wiring diagram for Magnetic Chess Piece Mover
Loading diagram…

Gather all the parts

QtyComponent
1

Nema 17

NEMA 17-size bipolar stepper motor for motion axes. Typical hobby modules are 42mm-frame, 4-wire/two-phase motors with 1.8 degree step angle (200 steps/rev); use with a current-limited stepper driver such as TMC2209/A4988/DRV8825 rather than direct GPIO.

1

Nema 17

NEMA 17-size bipolar stepper motor for motion axes. Typical hobby modules are 42mm-frame, 4-wire/two-phase motors with 1.8 degree step angle (200 steps/rev); use with a current-limited stepper driver such as TMC2209/A4988/DRV8825 rather than direct GPIO.

1

Nema 17

NEMA 17-size bipolar stepper motor for motion axes. Typical hobby modules are 42mm-frame, 4-wire/two-phase motors with 1.8 degree step angle (200 steps/rev); use with a current-limited stepper driver such as TMC2209/A4988/DRV8825 rather than direct GPIO.

1

Pololu A4988 Stepper Motor Driver Carrier, Black Edition

Pololu carrier board for the Allegro A4988 microstepping driver that runs one bipolar stepper motor at up to about 1.2 A per coil without cooling (2 A with extra cooling). A microcontroller drives it with STEP and DIR pins at 3-5.5 V logic, MS1-MS3 select full to 1/16 microstepping, and the motor supply is 8-35 V on VMOT with an onboard potentiometer setting the current limit.

1

Pololu A4988 Stepper Motor Driver Carrier, Black Edition

Pololu carrier board for the Allegro A4988 microstepping driver that runs one bipolar stepper motor at up to about 1.2 A per coil without cooling (2 A with extra cooling). A microcontroller drives it with STEP and DIR pins at 3-5.5 V logic, MS1-MS3 select full to 1/16 microstepping, and the motor supply is 8-35 V on VMOT with an onboard potentiometer setting the current limit.

1

Pololu A4988 Stepper Motor Driver Carrier, Black Edition

Pololu carrier board for the Allegro A4988 microstepping driver that runs one bipolar stepper motor at up to about 1.2 A per coil without cooling (2 A with extra cooling). A microcontroller drives it with STEP and DIR pins at 3-5.5 V logic, MS1-MS3 select full to 1/16 microstepping, and the motor supply is 8-35 V on VMOT with an onboard potentiometer setting the current limit.

1

Micro Switch - Premium Zippy 3-Terminal

Higher-quality 3-terminal bump-actuator microswitch for arcade controls and light mechatronics, rated 30 VDC / 250 mA max. Common, normally-open, and normally-closed contacts.

1

Micro Switch - Premium Zippy 3-Terminal

Higher-quality 3-terminal bump-actuator microswitch for arcade controls and light mechatronics, rated 30 VDC / 250 mA max. Common, normally-open, and normally-closed contacts.

1

Micro Switch - Premium Zippy 3-Terminal

Higher-quality 3-terminal bump-actuator microswitch for arcade controls and light mechatronics, rated 30 VDC / 250 mA max. Common, normally-open, and normally-closed contacts.

1

5 V P25/20 Holding Electromagnet

A small 5 V coil that becomes magnetic to pick up a steel disk fixed under each chess piece.

1

AO3400A

30V 5.7A N-channel enhancement-mode logic-level MOSFET in SOT-23-3L. Vgs(th) typically ~0.7V (max 1.45V at Id=250 uA), fully enhanced at Vgs=2.5V-4.5V. RDS(on) ~26 mOhm at Vgs=4.5V, ~38 mOhm at Vgs=2.5V. Driven directly by 3.3V or 5V microcontroller GPIO/PWM as a low-side switch for loads up to its Id and VDS limits. No firmware library required.

1

1N4007 Diode

General-purpose silicon rectifier diode in DO-204AL (DO-41) axial through-hole package. Rated 1A average forward current and 1000V peak reverse voltage (VRRM). Forward voltage drop ~0.7V at typical load (up to 1.1V at full 1A). Used here in series with LM7805 Vout for reverse polarity protection: anode to LM7805 output, cathode to 5V rail, resulting in ~4.3V on the output rail. Not suitable for high-frequency switching (reverse recovery ~2µs–30µs); intended for DC or 50/60Hz rectification only.

1

Resistor

220 Ω

Through-hole resistor (current-limiting in series with an LED)

1

Resistor

100 kΩ

Through-hole resistor (current-limiting in series with an LED)

1

12V Barrel-Jack Adapter

12 V / 2 A wall adapter with a 5.5 mm / 2.1 mm barrel jack. Used to power motor drivers, LED strips, or boards that need a higher rail.

1

24v Buck Converter

set output to 5.0 V

LM2596-based adjustable step-down buck converter module. Commonly used to regulate a higher battery rail, such as a 2S 18650 pack, down to 5V for Arduino logic. It is a regulator, not a charger or battery protection board.

1

Electrolytic Capacitor

100 µF, 25 V

Radial aluminium electrolytic capacitor bought by value and voltage rating, such as 10 µF, 100 µF or 1000 µF 10 V. It is polarised: the + lead goes to the higher voltage and the - lead, marked by the stripe on the sleeve, to the lower voltage or ground. It is mostly used for bulk filtering across a power rail, with a voltage rating comfortably above the rail.

1

Electrolytic Capacitor

100 µF, 25 V

Radial aluminium electrolytic capacitor bought by value and voltage rating, such as 10 µF, 100 µF or 1000 µF 10 V. It is polarised: the + lead goes to the higher voltage and the - lead, marked by the stripe on the sleeve, to the lower voltage or ground. It is mostly used for bulk filtering across a power rail, with a voltage rating comfortably above the rail.

1

Electrolytic Capacitor

100 µF, 25 V

Radial aluminium electrolytic capacitor bought by value and voltage rating, such as 10 µF, 100 µF or 1000 µF 10 V. It is polarised: the + lead goes to the higher voltage and the - lead, marked by the stripe on the sleeve, to the lower voltage or ground. It is mostly used for bulk filtering across a power rail, with a voltage rating comfortably above the rail.

Assemble it in 7 steps

1. Build the moving frame

Make a rigid 8 by 8 chessboard with 50 mm squares and leave an open space below it. Fit one belt-driven rail left-to-right for X, a second rail front-to-back for Y, and a short vertical lead-screw slide for Z. Mount x_motor, y_motor, and z_motor to their matching axes, and bolt electromagnet to the Z carriage with its flat face pointing down.

  • Put the X and Y home corners together at the physical a1 corner; the firmware treats that corner as a1.
  • Glue a small flat steel washer into the bottom of every chess piece so the electromagnet has a flat metal surface to grip.
  • Keep fingers, loose sleeves, and cables out of the belts and lead screw; the mechanism can pinch them.

2. Fit the three home switches

Screw x_home_switch where the X carriage presses its lever at the a-file edge, y_home_switch where the Y carriage presses at rank 1, and z_home_switch at the safe highest position. Use the COM and NO terminals only: COM goes to GND (ground), and NO goes to GPIO32, GPIO33, or GPIO27 (signal).

  • Press each lever by hand: it should make a small click before the carriage hits anything hard.
  • Position each switch so it is pressed before the carriage reaches the end stop.
  • Do not use the NC terminals in this design; connecting the wrong switch terminal makes the board think it is already at home.

3. Connect each motor to its driver

Place x_driver, y_driver, and z_driver where their heatsinks can get air. Connect each motor's four wires to its own driver: 1A to A+ (motor coil), 1B to A- (motor coil), 2A to B+ (motor coil), and 2B to B- (motor coil). Place x_vmot_cap, y_vmot_cap, and z_vmot_cap directly beside the matching driver, with capacitor + to VMOT (motor power) and capacitor - to GND (ground).

  • Find the two wire pairs belonging to each motor coil with a multimeter before connecting them.
  • Set each A4988 current-limit adjustment very low first, then raise it only enough that the axis moves without skipping.
  • Never plug in or unplug a motor while its A4988 driver has power; that can permanently damage the driver. Do not reverse the capacitor leads; a reversed electrolytic capacitor can burst.

4. Wire the ESP32 control lines

Wire x_driver STEP to GPIO13 (motion pulses), DIR to GPIO14 (direction), and EN to GPIO4 (turns X on). Wire y_driver STEP to GPIO16 (motion pulses), DIR to GPIO17 (direction), and EN to GPIO21 (turns Y on). Wire z_driver STEP to GPIO18 (motion pulses), DIR to GPIO19 (direction), and EN to GPIO22 (turns Z on). On every driver, VDD goes to 3V3 (logic power), both GND pins go to GND (ground), and join RST to SLP then connect SLP to 3V3 (keeps the driver awake).

  • Keep the thin ESP32 wires away from the motor wires to reduce electrical noise.
  • Leave MS1, MS2, and MS3 unconnected for the safe full-step starting setup; adjust the firmware calibration if you later change the microstep jumpers.
  • Do not connect 12 V to an ESP32 pin or to an A4988 VDD pin; both use low-voltage logic and can be damaged.

5. Wire the lifting magnet safely

Connect logic_buck VOUT+ to electromagnet Positive lead (5 V power). Connect the magnet Negative lead to magnet_mosfet Drain, and magnet_mosfet Source to GND (ground). Connect GPIO26 through gate_resistor to the MOSFET Gate, then connect gate_pulldown from that same Gate point to GND. Put flyback_diode directly across the magnet: its striped Cathode goes to the magnet positive wire and its unstriped Anode goes to the magnet negative wire.

  • The stripe on the diode identifies the Cathode end.
  • Keep the diode physically close to the magnet wires.
  • A missing or reversed flyback diode can send a voltage spike into the electronics when the magnet turns off.

6. Connect the power supply

With power unplugged, connect motor_supply +12V to all three A4988 VMOT pins and to logic_buck VIN+. Connect motor_supply GND to every driver GND and to logic_buck VIN-. Adjust the buck converter with a multimeter until VOUT reads exactly 5.0 V, then connect VOUT+ to ESP32 VIN (power) and VOUT- to ESP32 GND (ground).

  • Use thicker wire for the 12 V motor wires and keep the three motor-driver grounds joined to the ESP32 ground.
  • Plug in the 12 V adapter only after you have checked the buck output with a meter.
  • Do not power the ESP32 VIN pin with 12 V; set the buck converter to 5.0 V first or the board can be damaged.

7. Test movement with no chess pieces

Start with the magnet carriage near its top switch and the X/Y carriage near the a1 corner. Power the system, then test homing before fitting pieces. If an axis moves away from its switch during homing, unplug power and reverse that motor's direction by swapping one complete coil pair on its driver.

  • Run short moves first and measure the travel of one square; change the calibration values only after the mechanics are repeatable.
  • Keep captured pieces out of the travel area; this first build moves normal chess moves and expects captured pieces to be removed by hand.
  • Keep hands clear during the first homing run because the carriages move automatically.

Review all connections

1. Connections between "motor_supply" and "ESP32"

Functionmotor_supplyESP32
power+12V → Pololu A4988 Stepper Motor Driver Carrier, Black Edition VMOTEXT
power+12V → Pololu A4988 Stepper Motor Driver Carrier, Black Edition VMOTEXT
power+12V → Pololu A4988 Stepper Motor Driver Carrier, Black Edition VMOTEXT
power+12V → 24v Buck Converter VIN+EXT
groundGND → shared groundGND

2. Connections between "logic_buck" and "ESP32"

Functionlogic_buckESP32
groundVIN- → input groundGND
powerVOUT+ → regulated 5 V to ESP32 VINVIN
powerVOUT+ → 5 V P25/20 Holding Electromagnet Positive leadEXT
groundVOUT- → 5 V groundGND

3. Connections between "x_vmot_cap" and "ESP32"

Functionx_vmot_capESP32
power+ → Pololu A4988 Stepper Motor Driver Carrier, Black Edition VMOTEXT
ground- → groundGND

4. Connections between "y_vmot_cap" and "ESP32"

Functiony_vmot_capESP32
power+ → Pololu A4988 Stepper Motor Driver Carrier, Black Edition VMOTEXT
ground- → groundGND

5. Connections between "z_vmot_cap" and "ESP32"

Functionz_vmot_capESP32
power+ → Pololu A4988 Stepper Motor Driver Carrier, Black Edition VMOTEXT
ground- → groundGND

6. Connections between "x_driver" and "ESP32"

Functionx_driverESP32
groundGND → motor groundGND
powerVDD → 3.3 V driver logic power3V3
groundGND_2 → logic groundGND
digitalRST → Pololu A4988 Stepper Motor Driver Carrier, Black Edition SLPEXT
powerSLP → normal operation3V3
digitalSTEP → X step pulsesGPIO 13
digitalDIR → X directionGPIO 14
power1A → Nema 17 A+EXT
power1B → Nema 17 A-EXT
power2A → Nema 17 B+EXT
power2B → Nema 17 B-EXT
digitalEN → X motor enableGPIO 4

7. Connections between "y_driver" and "ESP32"

Functiony_driverESP32
groundGND → motor groundGND
powerVDD → 3.3 V driver logic power3V3
groundGND_2 → logic groundGND
digitalRST → Pololu A4988 Stepper Motor Driver Carrier, Black Edition SLPEXT
powerSLP → normal operation3V3
digitalSTEP → Y step pulsesGPIO 16
digitalDIR → Y directionGPIO 17
power1A → Nema 17 A+EXT
power1B → Nema 17 A-EXT
power2A → Nema 17 B+EXT
power2B → Nema 17 B-EXT
digitalEN → Y motor enableGPIO 21

8. Connections between "z_driver" and "ESP32"

Functionz_driverESP32
groundGND → motor groundGND
powerVDD → 3.3 V driver logic power3V3
groundGND_2 → logic groundGND
digitalRST → Pololu A4988 Stepper Motor Driver Carrier, Black Edition SLPEXT
powerSLP → normal operation3V3
digitalSTEP → Z step pulsesGPIO 18
digitalDIR → Z directionGPIO 19
power1A → Nema 17 A+EXT
power1B → Nema 17 A-EXT
power2A → Nema 17 B+EXT
power2B → Nema 17 B-EXT
digitalEN → Z motor enableGPIO 22

9. Connections between "x_home_switch" and "ESP32"

Functionx_home_switchESP32
groundCOM → switch groundGND
digitalNO → X home signalGPIO 32

10. Connections between "y_home_switch" and "ESP32"

Functiony_home_switchESP32
groundCOM → switch groundGND
digitalNO → Y home signalGPIO 33

11. Connections between "z_home_switch" and "ESP32"

Functionz_home_switchESP32
groundCOM → switch groundGND
digitalNO → Z home signalGPIO 27

12. Connections between "gate_resistor" and "ESP32"

Functiongate_resistorESP32
digitalP1 → magnet control signalGPIO 26
digitalP2 → AO3400A Gate (G)EXT

13. Connections between "gate_pulldown" and "ESP32"

Functiongate_pulldownESP32
digitalP1 → AO3400A Gate (G)EXT
groundP2 → groundGND

14. Connections between "magnet_mosfet" and "ESP32"

Functionmagnet_mosfetESP32
groundSource (S) → magnet switch groundGND
powerDrain (D) → 5 V P25/20 Holding Electromagnet Negative leadEXT

15. Connections between "flyback_diode" and "ESP32"

Functionflyback_diodeESP32
powerCathode → 5 V P25/20 Holding Electromagnet Positive leadEXT
powerAnode → AO3400A Drain (D)EXT

Deploy the firmware

#include <Arduino.h>

// Pin assignments match the wiring diagram.
constexpr int X_STEP_PIN = 13;
constexpr int X_DIR_PIN = 14;
constexpr int Y_STEP_PIN = 16;
constexpr int Y_DIR_PIN = 17;
constexpr int Z_STEP_PIN = 18;
constexpr int Z_DIR_PIN = 19;
constexpr int X_ENABLE_PIN = 4;
constexpr int Y_ENABLE_PIN = 21;
constexpr int Z_ENABLE_PIN = 22;
constexpr int X_HOME_PIN = 32;
constexpr int Y_HOME_PIN = 33;
constexpr int Z_HOME_PIN = 27;
constexpr int MAGNET_PIN = 26;

// Calibration defaults for GT2 belts, 20-tooth pulleys and A4988 drivers at 1/16 microstep.
constexpr long XY_STEPS_PER_SQUARE = 4000;
constexpr long Z_PICKUP_STEPS = 800;
constexpr unsigned int STEP_HALF_PERIOD_US = 500;
constexpr long HOME_MAX_STEPS = 60000;
constexpr long HOME_BACKOFF_STEPS = 200;

struct Axis {
  int stepPin;
  int dirPin;
  int enablePin;
  int homePin;
  long position;
};

Axis xAxis{X_STEP_PIN, X_DIR_PIN, X_ENABLE_PIN, X_HOME_PIN, 0};
Axis yAxis{Y_STEP_PIN, Y_DIR_PIN, Y_ENABLE_PIN, Y_HOME_PIN, 0};
Axis zAxis{Z_STEP_PIN, Z_DIR_PIN, Z_ENABLE_PIN, Z_HOME_PIN, 0};
bool isHomed = false;

void setAxisEnabled(Axis &axis, bool enabled) {
  digitalWrite(axis.enablePin, enabled ? LOW : HIGH); // A4988 EN is active low.
}

void pulse(Axis &axis) {
  digitalWrite(axis.stepPin, HIGH);
  delayMicroseconds(STEP_HALF_PERIOD_US);
  digitalWrite(axis.stepPin, LOW);
  delayMicroseconds(STEP_HALF_PERIOD_US);
}

void moveRelative(Axis &axis, long steps) {
  if (steps == 0) return;
  setAxisEnabled(axis, true);
  digitalWrite(axis.dirPin, steps > 0 ? HIGH : LOW);
  long count = labs(steps);
  for (long i = 0; i < count; ++i) pulse(axis);
  axis.position += steps;
}

bool homeAxis(Axis &axis, const char *name) {
  setAxisEnabled(axis, true);
  // LOW is the assumed direction toward the limit switch. Reverse this only if the mechanism moves away.
  digitalWrite(axis.dirPin, LOW);
  long travelled = 0;
  while (digitalRead(axis.homePin) == HIGH && travelled < HOME_MAX_STEPS) {
    pulse(axis);
    ++travelled;
  }
  if (travelled >= HOME_MAX_STEPS) {
    Serial.printf("ERROR: %s home switch was not reached.\n", name);
    return false;
  }
  // Move away, then approach slowly again so the zero position is repeatable.
  digitalWrite(axis.dirPin, HIGH);
  for (long i = 0; i < HOME_BACKOFF_STEPS; ++i) pulse(axis);
  digitalWrite(axis.dirPin, LOW);
  travelled = 0;
  while (digitalRead(axis.homePin) == HIGH && travelled < HOME_BACKOFF_STEPS * 3) {
    pulse(axis);
    ++travelled;
  }
  if (digitalRead(axis.homePin) == HIGH) {
    Serial.printf("ERROR: %s switch did not close on the second approach.\n", name);
    return false;
  }
  axis.position = 0;
  return true;
}

bool homeAll() {
  digitalWrite(MAGNET_PIN, LOW);
  Serial.println("Homing Z, X, then Y...");
  if (!homeAxis(zAxis, "Z")) return false;
  if (!homeAxis(xAxis, "X")) return false;
  if (!homeAxis(yAxis, "Y")) return false;
  setAxisEnabled(xAxis, false);
  setAxisEnabled(yAxis, false);
  setAxisEnabled(zAxis, false);
  isHomed = true;
  Serial.println("HOME complete: a1 is the lower-left square by the X and Y switches.");
  return true;
}

bool decodeSquare(const String &square, int &file, int &rank) {
  if (square.length() != 2) return false;
  char f = tolower(square.charAt(0));
  char r = square.charAt(1);
  if (f < 'a' || f > 'h' || r < '1' || r > '8') return false;
  file = f - 'a';
  rank = r - '1';
  return true;
}

void moveToSquare(int file, int rank) {
  const long targetX = (long)file * XY_STEPS_PER_SQUARE;
  const long targetY = (long)rank * XY_STEPS_PER_SQUARE;
  moveRelative(xAxis, targetX - xAxis.position);
  moveRelative(yAxis, targetY - yAxis.position);
}

void pickupAndPlace(int fromFile, int fromRank, int toFile, int toRank) {
  moveToSquare(fromFile, fromRank);
  moveRelative(zAxis, Z_PICKUP_STEPS);
  digitalWrite(MAGNET_PIN, HIGH);
  delay(250);
  moveRelative(zAxis, -Z_PICKUP_STEPS);
  moveToSquare(toFile, toRank);
  moveRelative(zAxis, Z_PICKUP_STEPS);
  digitalWrite(MAGNET_PIN, LOW);
  delay(100);
  moveRelative(zAxis, -Z_PICKUP_STEPS);
  setAxisEnabled(xAxis, false);
  setAxisEnabled(yAxis, false);
  setAxisEnabled(zAxis, false);
}

void handleCommand(String command) {
  command.trim();
  command.toUpperCase();
  if (command == "HOME") {
    homeAll();
    return;
  }
  if (!command.startsWith("MOVE ") || command.length() != 9) {
    Serial.println("Use HOME or MOVE e2e4. Captured pieces must be removed by hand in this first build.");
    return;
  }
  String from = command.substring(5, 7);
  String to = command.substring(7, 9);
  int fromFile, fromRank, toFile, toRank;
  if (!decodeSquare(from, fromFile, fromRank) || !decodeSquare(to, toFile, toRank)) {
    Serial.println("ERROR: squares use a1 through h8, for example MOVE e2e4.");
    return;
  }
  if (!isHomed) {
    Serial.println("ERROR: send HOME before moving a piece.");
    return;
  }
  Serial.printf("Moving %s to %s.\n", from.c_str(), to.c_str());
  pickupAndPlace(fromFile, fromRank, toFile, toRank);
  Serial.println("Move complete.");
}

void setup() {
  Serial.begin(115200);
  for (Axis *axis : {&xAxis, &yAxis, &zAxis}) {
    pinMode(axis->stepPin, OUTPUT);
    pinMode(axis->dirPin, OUTPUT);
    pinMode(axis->enablePin, OUTPUT);
    pinMode(axis->homePin, INPUT_PULLUP);
    setAxisEnabled(*axis, false);
  }
  pinMode(MAGNET_PIN, OUTPUT);
  digitalWrite(MAGNET_PIN, LOW);
  Serial.println("Chess gantry ready. Send HOME, then MOVE e2e4.");
}

void loop() {
  if (Serial.available()) {
    String command = Serial.readStringUntil('\n');
    handleCommand(command);
  }
  delay(5);
}

Remix this project

Make it yours in one click

Open a full copy of this project in your own Schematik workspace, diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.

Open in Schematik