Skip to the content
PCBKitNew board
2 layers71.1 × 28 mmPCA9685, the printed circuit board in 3D

3D-printed robotic arm controlled live in the browser via 3D sliders, driven by an Arduino Uno R3 and a PCA9685 PWM servo board. Like a claw machine, minus the part where you never actually win.

Size
71.1 × 28 mm
Copper layers
2
Thickness
1.6 mm
Parts
51
Nets
46
Pads
169
Tracks, vias
458, 25
Pours
1
Routed
Yes
KiCad
KiCad 9.0 (file 20241229)
Created
9 Aug 2026
Updated
2 Oct 2026

Overview

PCA9685 is an open-source printed circuit board by Will946, from the Will946/SirGrab repository on GitHub, published under the MIT licence. It is a 2-layer board of 71.1 × 28 mm with 51 parts on 46 nets, routed with 458 tracks and 25 vias.

It is built around the PCA9685PW and IRLML6401. Its parts include 26 resistors, 2 capacitors, 1 IC, 1 diode and 1 transistor. It belongs with the robots and motor control boards of this library.

Main components

2

README

README.md

Robotic Arm Project

A 3D-printed robotic arm, driven by an Arduino Uno R3 and a PCA9685 PWM servo board, controlled through a live 3D interface in the browser.

PCA9685 PWM driver board

The PCA9685 is a 16-channel PWM driver board built around Adafruit's Arduino-based PWM servo driver design, controlled here by an Arduino Uno R3. The Arduino doesn't drive the servos directly. It talks to the PCA9685 over I2C, and the board generates all 16 PWM signals on its own. This is what lets a single Arduino control far more servos than its own pins could handle directly, without fighting timer conflicts.

Arm model

The arm itself is 3D printed in PETG, using roughly 900g of filament for the full model.

Channel map and calibration

ChJointMotionCalibration center
0BaseRotates parallel to the ground90, no fixed reference direction
3 / 8Bottom hingeUp/down, mirrored pair75 / 105
123rd hingeUp/down90
72nd hingeBends left/right, perpendicular plane to the other hinges105
4Topmost hingeUp/down90
15Grip rotationTwists around the claw's own pointing axis60
11GripOpen/close90, no fixed reference direction

Ch3 and ch8 are mechanically mirrored, since they physically face each other. Their raw angles always sum to 180. Both the Arduino sketch and the control script derive one from the other so they can never drift apart from rounding.

How it works

arm_final_control.ino listens over serial for channel:angle commands and applies each one immediately, with no internal ramping. Early on, the sketch did ramp motion on its own, stepping one degree at a time with a delay between steps. That caused a real bug: with the control side also trying to smooth motion, the Arduino would be mid-delay and stop reading incoming serial data, the buffer backed up, and the arm would freeze or desync. Moving all smoothing to the control side and keeping the Arduino a simple instant-write listener fixed it. The control side owns pacing, the Arduino just executes, and that split is the architecture everything downstream is built on.

The control side is a two-part local setup. A small Python bridge script opens the actual serial connection to the Arduino and a local WebSocket server. A browser page renders the arm in 3D using three.js and exposes a slider per joint. Each joint rotates on its real physical axis. Up/down hinges bend in one plane, the 2nd hinge bends in the perpendicular plane, and the grip rotation twists around the claw's own pointing direction, like a screwdriver bit, rather than around the arm's main axis.

Arm 3D Control interface

Dragging a slider doesn't jump the arm straight to the new position. Every joint has a target, wherever the slider currently is, and a current value, what's actually been sent so far. A smoothing loop nudges the current value a small step closer to the target roughly 50 times a second. That's what keeps motion gradual, both from the browser interaction and from any script that talks to the bridge.

Each slider's range is centered on that joint's own calibration value instead of just running 0 to 180 for everything, and is capped to a safety window. That means it's not possible to drag a joint into a position closer to its mechanical limit than intended.

There's a second control page, arm_gamepad_control.html, that connects to the same bridge and drives the arm with a PS5 DualSense controller instead of sliders. The left stick handles base rotation and the bottom hinge, the right stick handles the 2nd and 3rd hinge, the triggers handle the topmost hinge, the bumpers handle grip rotation, and the face buttons open and close the grip. This is velocity control, not position control. Holding a stick or trigger moves the joint at a constant rate for as long as it's held, rather than mapping stick position directly to arm position. Every joint still respects the same calibrated safety range as the sliders, so holding an input at the edge of its range just holds the joint there instead of continuing to push against the limit.

Limitations of the current approach

  • Control is entirely manual. Posing the arm means dragging sliders one at a time. There's no way to move several joints together with one gesture, and definitely no way to "reach for" a position the way a real arm would.
  • No feedback loop. The system has no idea where the arm actually is beyond what it last commanded. If a servo stalls, gets physically blocked, or the arm is bumped, nothing detects or corrects for it.
  • No inverse kinematics. Every joint is set independently by angle. There's no way to say "move the gripper to this point in space" and have the joints figure out how to get there together.
  • Slider control doesn't scale well to fast or complex motion. It's fine for careful positioning, but clumsy for anything that needs to happen quickly across multiple joints at once.

Limitations of gamepad control

  • Stick drift. Analog sticks often don't rest at exactly zero on every axis, and that resting bias can be enough to register as unintended input on its own. The gamepad page has a calibration button that measures each axis's true resting position and uses that as zero instead of assuming it's perfectly centered.
  • Cross-talk during a push, which calibration can't fix. Calibration only measures the stick at rest. Pushing a stick in a straight line along one axis can still leak a small signal into the other axis on some hardware, this showed up as pushing straight down also triggering a small rotation. The fix in place compares both axes on a stick each frame and suppresses the smaller one whenever one axis is clearly dominant, so single-axis pushes read as single-axis input. Genuine diagonal pushes, where both axes are deliberately similar in magnitude, still pass through normally.
  • Calibration doesn't persist. Stick calibration resets every time the page is reloaded and has to be redone each session.
  • Button and axis index mapping can vary between controllers and browsers. The standard layout assumed here, for example buttons 6 and 7 for the triggers, is common but not guaranteed. A different controller or browser may need the index numbers adjusted.
  • The Gamepad API only recognizes a controller per browser tab, and only after a real button press while that tab is focused. Pairing a controller over Bluetooth, or detecting it on another page, doesn't carry over automatically. Each new tab needs its own button press to register the controller.

Currently working on

Exploring computer-vision-based control so the arm can mirror real arm movement directly, rather than being posed joint by joint through sliders. Two directions being evaluated: Google's MediaPipe pose and hand tracking, and a marker-based tracking approach using printed fiducial tags on the arm itself. Either would replace manual slider control with live tracking of an actual human arm's position.

Both approaches run into the same core issue: a single camera struggles to reliably measure rotation around certain axes. Bending a joint up and down, toward or away from the camera, is easy to see. Twisting a joint, like forearm rotation or wrist twist, is much harder, since a monocular camera has no real depth information and the visual cue for "this segment just rotated along its own length" is subtle. Certain angles are genuinely difficult to achieve reliably this way, particularly when a tracked segment is edge-on to the camera or briefly occluded by another part of the arm or hand. Marker-based tracking helps with the flexion and extension angles, but rotation still needs either a better camera angle, multiple markers per segment to catch whichever one is currently facing the camera, or a fallback like an IMU for the axes vision alone can't resolve well.

Development environment

This was built and run on macOS. Every Python script here expects to run inside a virtual environment rather than the system Python, both to keep dependencies isolated and because macOS's built-in Python has caused real problems in this project, including a broken Tk installation that crashed the GUI outright until a separate Python install was used instead. Setup looks like this:

bash
python3 -m venv arm_env
source arm_env/bin/activate
pip install pyserial opencv-contrib-python numpy websockets

The virtual environment needs to be activated in any new terminal window before running a script. Only one program can hold the Arduino's serial port open at a time, so the Arduino IDE's Serial Monitor has to be closed before running anything here.

Parts

51
QtyValueFootprintReferences
172200805R10, R11, R12, R13, R14, R15, R16, R17, R18, R19, R20, R21, R22, R23, R24, R25 +1
910K0805R1, R2, R3, R4, R5, R6, R7, R8, R9
6SOLDERJUMPER_REFLOW_NOPASTESJ_A0, SJ_A1, SJ_A2, SJ_A3, SJ_A4, SJ_A5
43X04JP1, JP2, JP5, JP6
4MOUNTINGHOLE2.5MOUNTINGHOLE_2.5_PLATEDU$47, U$48, U$49, U$50
21X06-CLEANBIGJP3, JP4
2FIDUCIAL""FIDUCIAL_1MMFID1, FID2
1E3,5-8C2
110uF0805C1
1IRLML6401SOT23-WIDEQ1
1LOGOLOGOG***
1PCA9685TSSOP28U1
1TERMBLOCK_1X2-3.5MM1X2-3.5MMJ1
1VCCCHIPLED_0805LED1

Questions

How big is the PCA9685 PCB?

It measures 71.1 × 28 mm and has 2 copper layers, 1.6 mm thick.

What parts does the PCA9685 use?

51 parts, built around the PCA9685PW and IRLML6401. The full list of its parts is on this page, with links to each part's symbol and footprint where PCBKit's library has them.

Where can I download the PCA9685 KiCad files?

From the Will946/SirGrab repository on GitHub, at the commit this page was made from: the board is PCA9685/PCA9685.kicad_pcb and its schematic PCA9685/PCA9685.kicad_sch. PCBKit opens the board in the browser, with nothing to install.

Can I use the PCA9685 design in my own project?

Its repository publishes it under the MIT licence: you may reuse it on that licence's terms (keep the author's notice, and for a copyleft licence share your changes alike).

Similar boards

  • 4 layers65 × 30 mmPca9685-rpi hat, the printed circuit board in 3D

    Pca9685-rpi hat

    C-ake/pca9685-rpi-hat

    KiCAD schematics and pcb for a raspberry pi enabling control of 16 PWM channels using a PCA9685.

    52 parts1GPL-3.0
  • 2 layers86.2 × 69.2 mmProsthetic hand v2.0, the printed circuit board in 3D

    Prosthetic hand v2.0

    Rifath-1729/11-DOF-Low_Cost-Bionic-Hand

    Hi! This is the build log for a low-cost 11-DOF bionic hand.

    71 parts
  • 2 layers129.5 × 50.8 mmLilka, the printed circuit board in 3D

    Lilka

    lilka-dev/lilka

    Схема та документація ігрової консолі Lilka

    39 parts234GPL-2.0
  • 2 layers40 × 100 mmBack pcb, the printed circuit board in 3D

    Back pcb

    ZeroPhone/ZeroPhone-PCBs

    PCB files for ZeroPhone - for gerbers and schematics, go to "Releases" tab!

    118 parts148GPL-3.0
  • 4 layers41.4 × 108 mmM17 3310, the printed circuit board in 3D

    M17 3310

    M17-Project/M17_3310

    Mainboard replacement for Nokia 3310

    124 parts68
  • 4 layers141.9 × 131.1 mmRS64c, the printed circuit board in 3D

    RS64c

    lhm0/rotating_sphere

    Rotating LED Sphere - a POV display with RP2040

    145 parts46
  • 4 layers140 × 104 mmRemram, the printed circuit board in 3D

    Remram

    hasenbanck/remram

    32bit STM32F7 based 3D printing controller using TMC2130 stepper driver

    264 parts40