Myoelectric Prosthetic Hand
An underactuated single-servo prosthetic hand that grasps everyday 20–400g objects from surface EMG alone.
SolidWorks · KiCAD · C++ · Arduino · Analog Circuits
Mechanical
The prosthetic combines 3D-printed PLA with machined sheet metal. From our target stakeholders' needs, grasping and moving kitchen objects was the main objective — hence a simple pincer over a full hand. It's actuated by a single continuous-rotation motor: a worm drives two worm wheels (one per claw) to open and close the hand. Silicone pads on the claw tips increase friction and deform slightly around irregular or small objects.

Electrical
A five-stage analog circuit processes and digitizes the input surface-EMG signal. The instrumentation amplifier (in-amp) and op-amps are powered single-supply — 5 V on the positive terminal, ground on the negative — with the in-amp referenced to 1.5 V; the comparator's negative input is set to 1.67 V via a voltage divider off the 5 V rail. The signal first enters the in-amp at a gain of 10, then passes through second-order high-pass and low-pass filters with cutoffs of 26 Hz and 500 Hz respectively, filtering out low-frequency motion artifacts and high-frequency electrical noise. It then passes through a non-inverting amplifier (gain 20) to amplify the clean signal before the peak detector, which rectifies and holds it. Finally, the signal passes through a comparator, which outputs a digital 0 V or 5 V signal for Arduino processing.
Input
Surface EMG
electrodes
Stage 1
In-amp
gain ×10
Stage 2
Band-pass
26–500 Hz
Stage 3
Non-inv amp
gain ×20
Stage 4
Peak detector
rectify + hold
Stage 5
Comparator
→ 0 / 5 V
Output
Arduino
grip state machine



Firmware
A non-blocking Arduino loop reads the comparator's digital output from a single pin — the analog front end already thresholds the signal, so no software filtering is required — and debounces it over a 30 ms window to reject chatter as the EMG signal crosses the threshold. A sustained contraction closes the pincer and relaxing reopens it, driving a continuous-rotation servo through writeMicroseconds (1500 µs to stop, 1300 / 1700 µs for each direction). Because a continuous-rotation servo gives no position feedback, the gripper is tracked open-loop: a 0–100 estimate is integrated from elapsed time over a hand-calibrated travel duration, and the motor is stopped at each limit; otherwise it would stall against the mechanical hard stops once fully open or closed, drawing stall current and overloading the servo and worm gearing.
motor_actuation.ino
#include <Servo.h>
Servo gripperServo;
const int emgPin = 2; //end of circuit
const int servoPin = 9; //control motor
const int CLOSE_COMMAND = 1300; // reversed: was 1700
const int OPEN_COMMAND = 1700; // reversed: was 1300
const int STOP_COMMAND = 1500;
// Calibrate by hand-timing full travel each direction, trim ~10% off
const unsigned long CLOSE_TRAVEL_MS = 1800; //need to determine
const unsigned long OPEN_TRAVEL_MS = 1800; //need to determine
const unsigned long DEBOUNCE_MS = 30; //delay
//true = HIGH, false = LOW
bool lastEmgReading = false;
bool stableEmgState = false;
unsigned long lastChangeTime = 0;
// Assume the gripper is fully open when powered on
float gripperPosition = 100.0;
unsigned long lastPositionUpdate = 0;
void setup() {
pinMode(emgPin, INPUT);
gripperServo.attach(servoPin);
gripperServo.writeMicroseconds(STOP_COMMAND);
lastPositionUpdate = millis();
Serial.begin(9600);
}
void loop() {
unsigned long currentTime = millis();
unsigned long elapsedTime =
currentTime - lastPositionUpdate;
lastPositionUpdate = currentTime;
// Read and debounce comparator output
bool reading = digitalRead(emgPin);
if (reading != lastEmgReading) {
lastChangeTime = currentTime;
}
if (currentTime - lastChangeTime >= DEBOUNCE_MS) {
stableEmgState = reading;
}
lastEmgReading = reading;
if (stableEmgState) {
// HIGH: close the grippers
if (gripperPosition > 0.0) {
gripperServo.writeMicroseconds(CLOSE_COMMAND);
gripperPosition -=
100.0 * elapsedTime / CLOSE_TRAVEL_MS;
if (gripperPosition <= 0.0) {
gripperPosition = 0.0;
gripperServo.writeMicroseconds(STOP_COMMAND);
}
}
else {
// Already fully closed
gripperServo.writeMicroseconds(STOP_COMMAND);
}
}
else {
// LOW: open the grippers
if (gripperPosition < 100.0) {
gripperServo.writeMicroseconds(OPEN_COMMAND);
gripperPosition +=
100.0 * elapsedTime / OPEN_TRAVEL_MS;
if (gripperPosition >= 100.0) {
gripperPosition = 100.0;
gripperServo.writeMicroseconds(STOP_COMMAND);
}
}
else {
// Already fully open
gripperServo.writeMicroseconds(STOP_COMMAND);
}
}
Serial.print("raw=");
Serial.print(reading);
Serial.print(" stable=");
Serial.print(stableEmgState);
Serial.print(" pos=");
Serial.println(gripperPosition);
}