Hi! This is the build log for a low-cost 11-DOF bionic hand.
I started it as my undergrad thesis project. Although I've recently graduated, and the work was awarded, Best Thesis Award from my department, I still keep updating it, mostly because the robotic hand field really grew on me and I still get excited every time a finger moves.
There is so much to cover when you build a working bionic hand, and it is very easy to lose track of what you did and why. That's the reason for this document. I want it to be descriptive enough that I can keep a structured record of progress instead of getting overwhelmed.
Contents
- About the hand
- Why I started making this
- What I was actually trying to achieve
- The hardware I had to work with
- The core limitation regarding Finger's Position
- 1. Getting a finger to stop on its own
- 2. Making the stop reliable & Adjusted Grip
- 3. Per-finger speed and first-bump tuning
- 4. Real position control: parking a finger at a percentage
- 5. Grips: a pose table and a two-phase state machine
- 6. Keeping the loop non-blocking
- 7. Safety Threshold for Joints, wrist servos
- 8. Telemetry and calibration over serial
- How I talk to the hand
- Where things live in this repo
- Where I am now and what's next
It is a robotic hand shaped like a human hand, with 11 movement points driven by 11 individual servos: 5 for finger flexion, 5 for the finger abduction joints, and 1 for the wrist. I'm currently on hardware version 2. See the media folder for photos and demo videos of both versions. And I'm in firmware version 3.0 for hand control. See the stm32 folder for the all the code version.
The mechanical part is based on Will Cogley's Bionic Hand ε1. The design has some real advantages like it has higher degree of freedom (11) than most open-source 3D-printed hands, you can print the whole phalanges in one go, and most importantly it looks cool. But I wouldn't recommend it to anyone at least not until the creator publishes a revised version. The current version is still a prototype, and there is no fully working release. I had to make a lot of modifications to the original design to get it working. Every modified file and its change log is in Design_Files/3D_Files/.
For control I used an STM32F411CEU6 dev board (Going to move a custom designed PCB for the hand soon). It's also commonly known as BlackPill. It has enough GPIO, and far more processing power and peripherals than this hand needs. The firmware is written on the STM32 HAL platform.
The hand can currently be commanded three ways:
- A serial command line over USART1 — the main interface, and the one I use for everything.
- Voice keyword spotting. I trained a lightweight model in Edge Impulse on Google's Speech Commands dataset, exported it as an Arduino library, and run it on a Seeed Studio XIAO ESP32-S3 Sense. It detects the keyword and tells the STM32 over UART which grip to run. This method is fairly limited — right now it handles at most 3 keywords at an acceptable latency and efficiency.
- Three physical buttons on the board:
PB12close,PB13stop,PB14open. These act as quick action buttons.
I'm really passionate about making things, so for my undergrad thesis I wanted to build something that would push my boundaries. My university doesn't provide any funding for thesis work, and a bionic hand needs a lot of money. So I built the whole thing out of cheap Chinese clone components and tried to keep the component numbers low as I need to pay for it out of my own pocket.
This constraint ended up shaping the entire project and it is exactly why the firmware turned into the interesting part. As I focused on getting the most out of my limited hardware by planned workaround.
Modifying the mechanical part of Bionic Hand ε1 to make it work took a lot of effort also training the keyword spotting model and deploying it on the ESP32 also took some time. But the place where most of the time spent and was my key area of interest is writing firmware on stm32 for hand control to get the most out from inexpensive hardware.
| Count | Part | What it does |
|---|---|---|
| 5 | clone MG996, continuous rotation | Finger flexion/extension, one per finger, via tendon pulleys |
| 5 | SG90, 180° positional | Finger abduction joints, one per finger |
| 1 | MG995, 180° positional | Wrist |
| 5 | Hall-effect sensor A1302 49E + magnet | Rough finger position, read on the STM32 ADC |
| 5 | Limiting Switch | To precisely figure out the full flex position of the fingers. |
| 1 | PCA9685 | 16-channel PWM driver over I²C, drives all 11 servos |
| 1 | STM32F411CEU6 (BlackPill) | Runs everything |
Channel map on the PCA9685: 0–4 the finger Continuous servos, 5 the wrist, 6–10 the five joint servos. Fingers are indexed little, ring, middle, index, thumb everywhere in the code. Detailed connection schematic and pcb design in Design_Files/Schemetic_and_PCB/prosthetic_hand_v2.0 along with a Idea_and_workflow doc that explains the whole diagram.
The core limitation regarding Finger's Position: no reliable position feedback for a close loop control system.
The cheap 3$ continuous rotation servo that I've used doesn't have any positional feedback it only takes speed and direction, and it will happily keep spinning until something physically stops it, or until I tell it to stop. There is no encoder anywhere on this hand. Only the direction and speed can be controlled using PWM.
So to have a positional feedback of the fingers, I've used a cheap 70 cents hall sensor that reads field of a magnet which is located on each finger, sampled through the ADC. That gives me a noisy, non-linear, low-resolution number that vaguely correlates with how far the finger has traveled. For example, Even by modifying the finger to travel as much as possible and making housing for a more bigger and stronger magnet than the original design. On the thumb the entire range of motion spans only about 155 ADC counts roughly 1.5 counts per 1% of travel. And the worst part is that, the not only the range change but also the maximum extend and flex limit changes too after every restart. For example, suppose in a session you've 2200 to 2050 for extend to flex range for thumb now if you restart the mcu the value can be changed to 2120 to 1997.
So large part of whole firmware I had to figure out: how much control I can get out of this kind of hall sensors value read?
Firstly I place the value acquisition to DMA so that the cpu can be free to do the other things. Then I passed the hall sensors value to 16 sample sliding window to get an average value so that the noise doesn't heavily affect the reading.
The first working version was bang-bang. strict_finger_extend() / strict_finger_flex()
set a full-speed PWM pulse in one direction, and strict_finger_update() called every
pass of the main loop and decides when to call finger_stop().
To know when to stop, I calibrate two hall values per finger: extend_thresh at fully
open and flex_thresh at fully closed. These Threshold values can be captured in real time using serial command<1-5>c<o/f> for example: 1co(First Finger Calibration Open) or 1cf(First Finger Calibration Flex) to tackle the hall sensors value shifting without recompiling the whole code.
The previous version of stop by checking whether it crossed a certain value is heavily unreliable as the sensor's value change every time. So in the latest version I've moved to delta based approach to the hall sensors value. The idea is that:
- First you set a time window.
- Then for every time window you observe if the hall sensors value change more than a certain threshold.
- If the hall value change more than the threshold amount we can say that the finger isn't still reach to full_extend or full_flex or stopped by an object. And the servo can continue with the movement of the finger.
- And if hall sensors value doesn't change enough to pass the threshold in the given time window I can assume the finger hit its full extend/threshold position or stopped by an object. It half-worked. There were some bugs that stops it from fully working. I've discussed about them and how I fix them, a little later of this section.
- To make fully flex reliable and I've installed a limiting switch with every finger that mechanically activates when a finger fully closes. This way the it can be made sure that the finger doesn't over flex and hurts the finger and wrist servo.
Normally for 5 limiting switches(one for each finger) I would need 5 pins from the mcu. Instead I soldered the switch pins to the signal and pwr pin of the A1302 49E hall sensor.This way in normal condition the hall sensors value will hover around 1700 to 2500 ADC value. But when a finger will fully close it will activate the limiting switch and there'll be a short circuit from pwr-> signal. The hall sensor will read an exceptionally high value (around 3800-4096) and I can figure out when a finger fully closes by reading the ADC Value
This delta analysis of the hall sensor to stop the finger gives a major feature of the hand that is- adjusted grip while the hand holding an object. Suppose I give the hand a bottle and the hand start closing from a cylindrical grip position to hold the bottle. Now when a finger touches the bottle it's hall sensors delta becomes near to zero the code detects it in the
strict_finger_update()function and stops the finger. This way I can get a good finger wrap around on the object. And the finger can grip asymmetric type object too.
Adjusted grip: every finger stops the moment its hall delta flattens against the bottle.
Two bugs I had in this kind of approach:
- No debounce. A single quiet window was enough to stop the finger. Sensor noise, the 16-tap filter lagging right after a fresh command, or a moment of mechanical backlash all looked identical to "reached the limit".
- Unsigned underflow. The delta was computed on
uint16_t, so any real backward motion (a noise dip, an over-spin) wrapped around to a huge number instead of a negative one. The detector was unreliable in both directions and the calibrated thresholds weren't actually being checked at all, so there was no independent safety stop behind it.
The fix:
- A 3-window debounce (
STALL_DEBOUNCE_CNT), so an isolated glitch no longer stops the finger now it takes three consecutive quiet windows to determine whether the finger reached to fully extend or flex position or whether stopped by an object. - Plateau math moved to signed
int32_tto deal with overflow.
Source: Core/Src/servo_drive.c
Every clone servo is different, and every finger has different tendon friction. So almost every tuning constant in the firmware is a per-finger array, not a global:
speed_pct: currently 55/100/70/100/60 across the five fingers, settable live with1v80.open_time_window_delta/close_time_window_delta: the plateau observation window.hall_window_delta: how much movement counts as "still moving".flex_first_bump_time_window/extend_first_bump_time_window: a deliberate blind period at the start of a motion where the stall detector is ignored, so a finger that starts slowly from a dead stop isn't immediately declared stalled.
All of this variables can be changed over serial command.
It's a closed-loop system in the firmware. For now it uses bang-bang method. Currently The hand's mechanical design isn't smooth enough and servos I'm using for finger position isn't precise enough to get benefit from PID tuning. I'll implement PID when I move to better mechanical design and components.
The way this thing works as of now:
-
First in
Core/Src/grip_positions.cfile you define for a grip what is the percentage of a finger you should keep open. -
Then with formula:
target_hall = flex_thresh + (extend_thresh - flex_thresh) * pct / 100;I figure out what should target hall value where I should stop my finger to get the desired grip.
-
Now the finger drives toward that target hall value. Step 2 and 3 is done inside
finger_start_target(f, pct)function. -
Then
finger_seek_update()function checks whether we've reached the targeted position in every loop pass. -
To make sure we don't get continuous back n forth around target value:
- a deadband of 4% of that finger's calibrated span (floor 6 ADC counts) is set
- To make sure sudden noise above
target hallvalue can't stop the finger:
- a target-crossing counter variable is kept named
seek_crossunderFinger_tstate. The idea is that for a certain time window if theseek_crosscounter value goes above 5 then I can safely assume that the finger actullay passed thetarget_hallvalue and it's considering not some random noise spike.
With uart command <1-5>o<0-100> we can check if the finger is resting at a position for example: 1o50 parks the little finger at 50% open. That command is very useful to tuning grips.
Fingers parking at intermediate positions, driven purely by the hall sensor reading.
A grip is just data collection of a name, five finger percentages, five joint angles, and a wrist angle,
all in one table in
Core/Src/grip_positions.c:
[GRIP_TRIPOD] = {
.name = "tripod",
.finger_open_pct = {0, 0, 35, 35, 30}, // little, ring, middle, index, thumb
.joint_deg = {80, 100, 85, 90, 90},
.wrist_deg = 130
},Ten grips are defined so far open, close, pinch, point, tripod, lateral, hook, ball, ok, thumbsup. I can easily add a new grip by defining it here and don't have to do any change in the code.
To make sure that the hand's finger goes to proper position without making a code spaghetti. I've designed a four-state machine in grip_update() function:
CLOSE → SETTLE → SEEK → JOINTS → IDLE
Closing first makes the implementation a whole lot easier. I know isn't the most efficient way to reach a grip. But with a less reliable encoder, the fully-closed mechanical stop is the only position reference I can trust. So every grip re-homes the hand to that stop first,
waits ~300 ms in SETTLE for the 16-tap hall filter to catch up, then seeks each finger out to its target percentage, after that the joint servos and wrist servos goes to their position.
The whole firmware is a single deterministic super-loop, I still not moved to RTOS yet. Every state machine finger control, seeking, grips is written as an update function that returns immediately.
This is really important because this way the stop button and the serial command stay responsive at all times. Otherwise I couldn't stop the hand if any of the servos start putting mechanical stress on the hand.
The joint and wrist servos are positional, driven by step_set_deg() over a 500–2500 µs pulse range. Each joint carries its own low_deg / hi_deg limits that I've found by trial and error, and every commanded has to be in between them. Otherwise the servos won't respond.If I don't take this approach, a larger angle might stalls the servo against a mechanical limit and cooks it.
The wrist is the strictest: it sits at 130° neutral and is clamped between WRIST_LO_THRES 90 and WRIST_HI_THRES 160, because over-bending or under-bending the wrist can damage the
forearm assembly and the servo gear.
Every 250 ms the firmware prints one line with every finger's filtered hall value and its computed percentage:
L:1935(48%) R:1860(49%) M:1890(50%) I:2075(50%) T:1995(51%)
This line help to any of the above got debugged. Combined with live
calibration (cy, 1co, 1cf) and live tuning (1v80, 1o50), I can tune a finger's action without a single recompile.
Using USART1, at 115200 baud-rate and a PL2303 USB TO Serial Converter I can communicate with the stm32 by Serial Port Terminal Program.
| Command | What it does |
|---|---|
po / pc |
Power open / power close — all fingers, mechanical stops |
s |
Stop everything, abort any running grip |
? |
Print full status |
g? |
List all grips with their index |
g<n> |
Run grip n (e.g. g4 = tripod) |
gs |
Abort the running grip |
<1-5>o<0-100> |
Park one finger at a percentage open (3o40) |
<1-5>v<1-100> |
Set one finger's speed (2v80) |
<1-5>s<0-180> |
Set one joint servo's angle (5s110) |
w+ / w- / w<deg> |
Wrist, 1° per step, clamped 90–160 |
cy / cn |
Enable / disable calibration mode |
<1-5>co / <1-5>cf |
Capture this finger's open / closed hall threshold |
kws / kwc |
Turn the ESP32 voice link on / off |
demo |
Run the scripted demo sweep |
Buttons: PB12 close, PB13 stop, PB14 open.
Full open and close, driven from the serial console.
Full quality videos: bottle grip with adjusted power · open and close · positional placement from hall values
11-DOF-Low_Cost-Bionic-Hand/
├── Firmware/
│ ├── stm32/
│ │ ├── all_hand_v3.0_grip_set/ ← the current firmware — build and flash this
│ │ ├── versions/ ← every earlier version, in development order
│ │ └── module-tests/ ← per-peripheral bring-up projects
│ └── esp32_kws_detection/ ← XIAO ESP32-S3 keyword spotting + UART link
├── Design_Files/
│ ├── 3D_Files/ ← my modifications to the ε1, with a change log
│ └── Schemetic_and_PCB/ ← KiCad schematic + carrier PCB, v2.0
├── Docs/ ← thesis book, slides, figures, certificates
└── Media/ ← photos and demo videos, Version_1 and Version_2
The two files worth reading are
servo_drive.c — the entire
control core — and
grip_positions.c — the
grip table.
- Per finger open/close with reliable stall detection and speed adjustment.
- Complete control over the hands 11 DOF.
- Percentage position seek : It means I can tell finger to go any position between fully close to fully open.This allows me make any grip possible. Although it's currently implemented using simple bang bang model.
- Place the hand in any grip position from grip table.
- Communicate with the hand three ways: serial console, button control, and the ESP32 voice link.
- Efficient Grip Transition: Currently between two grips the fingers need to go to full close phase as homing position. I would like to build an algorithm that will find out the most efficient movement path for each finger when transitioning from one grip to another. This will significantly reduce grip transition time.
- Move to a better hand design: Something that's mechanically more precise and the fingers that don't pop out of their joint so easily and has lower friction. Possibly a design with ball-bearing joint and better tendon routing.
- Find out the gripped object's hardness: While the finger gripping an object, after first contact the ADC value stops abruptly if the object is hard for example a baseball ball. On the other hand, for softer object (like squishy toys) there is also some more addition ADC values after first contact. By analyzing the counts over a time window I may be able to figure out the object's hardness.
- High span and lower SNR of Hall sensors value Need to find a way to increase the span of the hall sensor's value without increasing the SNR. Perhaps one way could be using magnetic encoder in the finger joints or finding a more sensitive hall sensor than the current A1302 49E I've been using.

