Build

Electronics

The ESP32-S3 controller, ULN2003 motor drivers, the motor-to-pin map, pin warnings, flashing the firmware with PlatformIO, and the serial protocol.

On this page

The electronics have one job: turn joint angles from your PC into smooth motor motion. The PC does the thinking (control, simulation, later AI); the microcontroller only moves the motors.

Signal path
PC (Python)  --USB-C-->  ESP32-S3  -->  8 × ULN2003  -->  8 × 28BYJ-48  -->  tendons  -->  joints

Parts#

PartQtyNotes
ESP32-S3-N16R8 board116 MB flash, 8 MB PSRAM (kept off, see below). USB-C to the PC.
ULN2003 driver board8One per motor. Switches the four motor coils.
28BYJ-48 stepper, 5 V8Geared ≈ 64:1, ≈ 2038 full steps per output turn.
5 V / 2 A power supply1For the motors only. Its ground must connect to the ESP32 ground.

Motor and pin map#

Motors are numbered M1 to M8. This order is used everywhere: in the firmware, the serial protocol, the Python code and the simulation. IN1–IN4 are the four inputs on each ULN2003 board.

MotorIN1, IN2, IN3, IN4 (GPIO)JointWhat it moves
M14, 5, 6, 7index_dipIndex fingertip joint
M215, 16, 17, 18index_pipIndex middle joint
M38, 3, 14, 9index_mcp_flexIndex knuckle bend
M410, 11, 12, 13index_mcp_abdIndex sideways
M51, 2, 42, 41thumb_ipThumb tip joint
M640, 39, 38, 37thumb_mcpThumb middle joint
M736, 35, 0, 45thumb_cmc_flexThumb base bend
M848, 47, 21, 43thumb_cmc_rotThumb base rotation

Sign convention: positive angles close the hand, negative angles open it, and 0 is the joint held straight. For the thumb base rotation, positive swings the thumb across the palm. For the index sideways joint, positive moves the index toward the thumb.

Pin warnings#

The pins in bold above need care. They work, but only because of choices in the firmware:

  • GPIO 0 (M7) is a strapping pin: if it's held LOW at reset, the chip starts in download mode instead of running the firmware. The ULN2003 input may pull it low, so the board might not boot normally with motor 7 connected. This hasn't been tested on the real hand yet; if you run into it, please open an issue. The servo upgrade (below) frees this pin.
  • GPIO 43 (M8) is the chip's UART0 transmit pin. The boot ROM prints messages on it at reset, so motor 8 may twitch briefly. The firmware never logs on UART0; it talks over USB instead.
  • GPIO 48 (M8) is often wired to the board's RGB LED. The firmware doesn't drive the LED.
  • GPIO 3 and 45 are strapping pins too, but LOW is already their default, so they're fine.
  • GPIO 19 and 20 are the native USB pins. Never use them for motors.

The firmware sets every motor pin LOW (all coils off) as the very first thing it does.

Power#

Each energised 28BYJ-48 draws about 200–250 mA, so eight at once would be close to the 2 A supply's limit. To stay within budget, the firmware switches off a motor's coils after 1 s without motion. The motor's internal gearbox mostly holds the joint in place without power.

Flashing the firmware#

The firmware is a PlatformIO project (Arduino framework, C++17) in firmware/. Install PlatformIO as a VS Code extension or as a command-line tool, then from the firmware/ folder:

Terminal
pio run                 # build
pio run -t upload       # flash (ESP32 on USB-C)
pio device monitor      # type commands, see replies (Ctrl+C to quit)

Serial protocol#

The PC talks to the ESP32 with short text commands, one per line. Joints are numbered 1–8 (= M1–M8), angles are in radians, and positive closes the hand. Every command replies OK unless noted.

CommandWhat it does
P q1 … q8Set targets for all 8 joints
J i qSet the target of joint i
SStatus. Replies S q1 … q8 moving_mask
XStop all joints, smoothly
RRelease all coils (joints go limp)
ZThe current pose becomes zero
M i nRaw move of joint i by n steps, ignoring limits (calibration only; max ±4096)
K i sSet joint i's scale in steps per radian (calibration)
V i v aMax velocity (rad/s) and acceleration (rad/s²) for joint i (0 = all joints)
IFirmware info and scales

Targets are clamped to each joint's limits. Moves speed up and slow down smoothly (a trapezoidal profile), and a new target can arrive mid-move. You rarely need to type these by hand: the Python RealHand class sends them for you.

First power-on#

Do this one motor at a time, with a hand near the power switch.

  1. Flash with only USB connected#

    Keep the motor power off. Open the monitor and send I. It should list 8 joints.

  2. Power the motors and zero#

    Switch on the motor power. Straighten every joint by hand, then send Z.

  3. Check each motor's direction#

    For each motor i, send M i 200 (a small move, about 18°), then M i -200. Check that the right joint moves and that positive closes it. If it opens instead, set invert = true for that joint in firmware/include/config.h.

  4. Calibrate the scale#

    Send M i 1000, measure how far the joint turned, convert it to radians, and send K i <1000 / angle_in_rad>. Write the value down: it will go into config.h.

  5. Move with real angles#

    Only now use J i q or P … with real angles, or drive the hand from the digital twin.

Calibrating a joint#

Why the last steps matter: the motor turns a spool, but how much the joint turns for each motor step depends on the tendon's moment arm at that joint, which hasn't been measured. Until each joint is calibrated, the firmware assumes 1:1 (about 648.7 half-steps per radian). A guided calibration tool is planned.

Coming next: smart servos#

The planned upgrade replaces the steppers with Feetech SCS0009 smart servos on a single serial bus (through an FE-URT-1 adapter). They report their own position, so manual homing goes away, and one shared data line frees up most of the pins, including the tricky ones above. The firmware is already structured for this: joint code talks to a MotorDriver interface, and a servo driver can replace the stepper driver without touching the rest.