Build it, stage by stage
Nine stages, each one a working thing on its own with a pass/fail test you can actually run. Pick a difficulty path, tick tasks off as you go, and use the animated figures where the mechanism is hard to picture. Nothing here is a summary — the tests are quantitative and the numbers come from the simulator.
Pick how far you're going [ easiest first ]
These are not three different projects; they are the same project stopped at three points. Every path starts at stage 1. Choosing one re-labels the stages below and dims what you don't need.
Do not start at the corner cube. Both of the best-documented projects in this field — ETH's Cubli and the HTL Hollabrunn diploma build — built a single-axis prototype first and only then scaled to three. Most of the abandoned hobby repos did not. The stage-1 rig costs about $58 and teaches you 90% of what the full cube needs.
Bench pendulum
Objective. A rig that can fall over in exactly one plane, with a motor and flywheel at the top. No cube. No CAD. You are building a test bench, not a product, and it should look like it.
Why a flywheel can hold anything up
Get this in your bones before cutting anything. There is no outside force available — the cube cannot push on the air. All it can do is move momentum around inside itself. Spin the wheel one way and the body must rotate the other way, because the total has to stay put. The figure below has gravity switched off so the bookkeeping is visible: the blue bar is the sum, and it never moves.
Parts
- Gimbal BLDC motor, driver, magnetic encoder, IMU, microcontroller, bench supply or LiPo — see the stacks page for four costed options.
- One 608 bearing (skateboard bearing, 8 mm bore) and an 8 mm shaft or bolt.
- An arm: 150–200 mm of aluminium extrusion, plywood, or a printed beam. Stiff beats light.
- A base with real mass — 2 kg minimum, or a G-clamp to the bench.
Steps
- Hold the arm at 10° and release. It should fall smoothly and accelerate. On a 200 mm arm expect roughly a quarter to half a second to reach the stop.
- Flick the flywheel by hand. It should coast for five seconds or more. If it stops dead in two, the bearing is preloaded or the wheel is rubbing, and that friction eats a torque budget you have not measured yet.
- Waggle the arm sideways. Any perceptible side play means a loose bearing fit — fix it now, because it becomes phantom dynamics later.
Printed disc + bolts. Print a disc with a ring of holes and put M5 bolts and nuts through them. Inertia becomes trivially adjustable — add or remove bolts. This is what the blueprint generator dimensions for you.
Printed hub + steel ring. A ring of steel bar or a stack of large washers bonded into a printed carrier. More inertia for the same diameter, harder to adjust once glued.
Machined aluminium with a steel rim. Best inertia-to-mass, and it can be balanced properly on a mandrel. Only worth it once you know the design works.
Tilt estimate, motor unplugged
Objective. An angle you trust, at your real loop rate, with the motor physically disconnected. Most "my cube oscillates" problems are stage-2 problems that were never tested.
You have two sensors and both lie, in different directions. The accelerometer tells you which way gravity is — true on average, buried in noise, and corrupted by any real acceleration. The gyroscope tells you how fast you are rotating — smooth and clean, with a small bias that integrates into a steadily growing error. Blend them: trust the gyro over short times, let the accelerometer slowly pull the estimate back.
// once per control tick, dt seconds apart
float accAngle = atan2f(ax, ay) * 57.2958f; // degrees, check YOUR axes
tilt = (1.0f - ALPHA) * (tilt + gyroZ * dt) + ALPHA * accAngle;
// ALPHA around 0.02 at 500 Hz. Smaller = trust the gyro more. Steps
- Print a 10° wedge, or use a phone level. Rest the arm on it and hold for 60 seconds. The estimate must read 10.0° ±0.3° the whole minute. If it creeps, the bias subtraction is wrong or ALPHA is too small.
- Tap the rig sharply. The estimate should wobble and settle inside about 100 ms, not ring for a second.
- Check loop jitter, not just rate: log actual microseconds between ticks for 10 s. Spread should be a few percent. A loop that averages 500 Hz but occasionally stalls 8 ms is worse than a steady 200 Hz one.
Torque control, arm clamped
Objective. Prove that when you ask for 0.05 N·m you get 0.05 N·m. The controller's output is a torque; if your driver only does voltage or speed, every stage after this is built on sand.
Check your driver honestly here. The most-recommended cheap board in this space, the SimpleFOCMini (DRV8313), is voltage-mode only — SimpleFOC's own driver comparison lists it as having no current sensing. It will spin a gimbal motor beautifully and cannot accept a true torque command. For real torque control you need in-line or low-side current sensing: the SimpleFOC Shield v2, a DRV8302 module, or the B‑G431B‑ESC1.
The measurement that settles it
Do not trust the library's word for it. Clamp the arm so it cannot move, command a step of torque, and record the wheel's speed from the encoder. The wheel's angular acceleration times its inertia is the delivered torque:
tau_delivered = J_wheel × (dω/dt) Command 25%, 50% and 100% of your intended maximum and plot delivered against commanded. You want a straight line through the origin. A flat spot near zero is motor deadzone — identify it now and compensate, or the controller spends its life fighting an invisible nonlinearity. A curve that flattens at the top means you hit a current limit earlier than you thought.
Why the top end sags
Available torque falls as the wheel speeds up, because back-EMF eats the voltage headroom. This is not a detail; it is the reason cubes fall over. Watch the operating point walk out along the envelope during a real catch:
Delivered torque within ±15% of commanded across the range, in both directions, with deadzone under about 5% of maximum. If torque does not reverse cleanly through zero, stop — a balancing wheel lives at zero crossings, and a driver that stumbles there will never hold the cube up.
Measure, then simulate
Objective. Four numbers measured off your actual rig, put into the simulator, giving you three gains and — more importantly — a verdict on whether your motor is big enough, before you tune anything.
Measuring the four numbers
| Number | How to get it |
|---|---|
| Total mass M | Kitchen scales. Everything that moves — arm, motor, wheel, any electronics mounted on the arm. |
| CoM distance l | Balance the whole arm horizontally on a knife edge (a ruler works). The balance point is the centre of mass; measure from the pivot. |
| Pendulum inertia Jp | Let it swing hanging down (stable, not inverted) through a small angle and time 20 swings for period T. Then Jp = M·g·l·T²/4π². |
| Flywheel inertia Jw | Bifilar pendulum: hang the wheel level on two threads a distance d apart and L long, twist a few degrees, time 20 torsional oscillations. Then Jw = m·g·d²·T²/(16π²L). |
The bifilar trick is worth ten minutes. Flywheel inertia is the number the whole design hinges on, and a printed wheel with bolts in it is nothing like the uniform disc a CAD mass-properties dialog assumes.
Max recoverable lean above 5° with real noise and latency included. Below 3°, stop and fix hardware — bigger motor, heavier rim, or smaller cube. No amount of tuning rescues a torque budget that is not there. For scale, ETH reported roughly 7° on the original Cubli's 1‑DOF prototype, torque-limited because they ran gearless.
Close the loop
Objective. It balances. Three gains, applied in a specific order, with safety cutoffs written before the control law.
The failure that kills most first builds
Leave out the wheel-speed term and your cube balances beautifully for four seconds and then drops. The wheel winds up one way, reaches no-load speed, has no torque left, and that is that. With the term in, the controller deliberately leans a fraction to bleed the wheel back toward zero — trading a little tilt for momentum headroom. Both cubes below are identical and get identical kicks; the left has that one gain deleted.
Order of tuning
- Safety first. Tilt past 20°, wheel speed past 90% of no-load, or a missed control tick — cut the motor. Write this before the control law, because you need it in the first thirty seconds.
- Kθ alone, raised until the rig fights back and then oscillates. Note where oscillation starts.
- Kθ̇ raised until that oscillation damps. Too much and it goes sluggish and noise-sensitive.
- Kω last, and never zero. Start at the simulator's value; raise it until the wheel reliably returns toward zero within a few seconds of a disturbance.
Firmware skeleton
Structure matters more than the exact API — fixed-rate task, filter, state feedback, saturate, safety. The motor calls are whatever your driver library uses.
// ---- gains straight from the simulator ----
const float K_TH = 49.37f; // tilt (N.m per rad)
const float K_TD = 5.35f; // tilt rate (N.m per rad/s)
const float K_W = 0.0268f; // wheel speed (N.m per rad/s)
const float ALPHA = 0.02f; // complementary filter
const float DT = 1.0f / 500.0f;
const float TILT_LIMIT = 20.0f * DEG2RAD;
const float W_LIMIT = 0.90f * W_NOLOAD;
const float TAU_MAX = 0.18f;
float tilt = 0, tiltRate = 0, biasInt = 0;
void controlTick() { // from a hardware timer, NOT loop()
imu.read();
tiltRate = imu.gyroZ - gyroBias;
float accAngle = atan2f(imu.ax, imu.ay);
tilt = (1.0f - ALPHA) * (tilt + tiltRate * DT) + ALPHA * accAngle;
float w = motor.shaftVelocity(); // rad/s, relative to the body
// ETH's trick: absorb constant sensor bias into a filter state instead
// of letting it park the wheel at a non-zero speed forever.
biasInt += 0.02f * tilt * DT;
float tiltCorrected = tilt - biasInt;
if (fabsf(tilt) > TILT_LIMIT || fabsf(w) > W_LIMIT) { motor.disable(); return; }
float tau = K_TH * tiltCorrected + K_TD * tiltRate + K_W * w;
tau = constrain(tau, -TAU_MAX, TAU_MAX);
motor.move(tau); // TORQUE mode, not voltage
logRing[logIdx++ & LOG_MASK] = { tilt, w, tau }; // dump later, never print here
} The biasInt term is worth understanding rather than copying. A constant offset in your tilt estimate — a millimetre of IMU misalignment — looks to the controller like a permanent lean, so it holds permanent torque, so the wheel accelerates forever. ETH's published trace shows an uncorrected 0.058 rad offset parking the wheel at 37 rad/s indefinitely. The low-pass integrator soaks that into a filter state instead.
- Balances unaided for 60 seconds.
- Wheel speed stays bounded over that minute — wandering around zero, not climbing.
- Recovers from a finger flick, and the wheel returns toward zero within a few seconds afterwards.
Bench-pendulum path finishes here. Everything below is optional.
Mechanical refinement
Objective. Remove the mechanical problems that would otherwise be multiplied by three. The big one is flywheel balance, and it has a quantitative test most people skip.
Balancing the flywheel, with a number
An unbalanced wheel shakes the frame and your IMU reads that shake as tilt. You do not need a balancing machine — you already have an accelerometer bolted to the thing:
- Clamp the rig. Spin the wheel to a steady 1500 rpm under control.
- Log accelerometer magnitude for 5 seconds; record peak-to-peak.
- Stop, mark the wheel at 12 o'clock, add a small mass (tape, a nut) at one position, repeat.
- Work around the wheel until you find the position that reduces vibration, then refine the amount.
Target: vibration at 1500 rpm small compared to the tilt signal you are trying to measure. In practice, under about 0.05 g peak-to-peak and the control loop stops caring.
Vibration at 1500 rpm under 0.05 g peak-to-peak, no visible rim wobble, and the stage-5 balance test still passes on the same gains.
Cube on an edge
Objective. The same controller, now inside a cube standing on one edge. The plant has changed, so you measure it again — a real cube with motors bolted to its faces is nothing like the uniform solid a simulator assumes.
Dimensioned drawings for frame, flywheel, motor mount and pivot are generated from your cube size on the blueprints page — including how many bolts to put in the wheel to reach the inertia the physics actually asks for.
Geometry worth knowing before you cut
Balanced on an edge, the centre of mass sits a/√2 above the pivot — for a 120 mm cube, 84.9 mm. That is your lever arm, and required torque scales with it and with mass, which is why doubling cube size costs far more than double the motor.
Stands on one edge 60 seconds unaided, recovers from a light push, wheel speed bounded. Edge-balancer path finishes here.
Three axes
Objective. Corner balancing. Three wheels, spin axes along the three cube edges meeting at the balancing corner, each wheel plane ideally passing through the centre of mass.
The hard part is electronic, not mathematical. There is no cheap microcontroller that cleanly runs three current-sensed FOC loops at once — each wants its own set of high-resolution PWM channels plus phase-current ADC sampling synchronised to the PWM edge, and sub-$30 MCUs generally expose one or two complete sets. The architecture that works is three driver boards each closing its own current loop, reporting to one coordinator that runs IMU fusion and the balance law and sends torque setpoints over UART or CAN. No single library glues that together; that firmware is yours to write.
Centre of mass becomes a first-class problem
For corner balancing the CoM must sit on the body diagonal, and you will not hit that by luck. Both ETH and the HTL Hollabrunn team independently flag that at an unstable equilibrium, small CoM offsets create outsized tipping moments. Find it empirically: suspend the cube from one corner on a thread and mark the vertical line on the body; repeat from a second corner. The lines intersect at the CoM. Add trim mass until that point lands on the diagonal.
Stands on a corner 60 seconds, started by hand near equilibrium. Worth knowing: in the ECC 2013 paper the full 3D Cubli also had to be started near equilibrium by hand — the autonomous jump-up was demonstrated on the 1‑DOF prototype.
Jump-up
Objective. Get from lying flat to balancing on a corner without being placed there. Spin a wheel up, brake it hard, and the momentum transfer flips the cube.
ETH used an RC servo slamming a metal barrier into a bolt head on the wheel — a mechanical brake, not an electrical one. Shorting the motor phases is the cheap electrical version; it transfers momentum more slowly, so you need a bigger wheel or a faster spin for the same impulse. Their first design used a solenoid and was dropped for being 39 g heavier.
Flat to edge, unassisted, then caught and held. Corner-to-corner jump-up is a stretch goal that ETH themselves only published on the 1‑DOF rig.
Failure modes, attributed
| What went wrong | Documented by | The lesson |
|---|---|---|
| Sensor update rate too slow for the loop; abandoned before ever balancing | croomjm/Cubli | Verify sensor throughput at your target rate before writing the controller |
| Magnetic encoder "super sensitive and unreliable" beside the spinning wheel | Dami Kim, Hackaday.io | Magnet gap tolerance is tight — validate in situ, not on the bench |
| Motor overheated and died mid public demo | Dami Kim, Hackaday.io | Balancing is continuous-torque duty; size for thermal, not just peak |
| PWM routed to an unsupported controller pin, found after PCB fab | Willem Pennings | Breadboard the driver interface before committing a board |
| Thread taps snapped off inside stainless motor mounts | HTL Hollabrunn | Plan tap depth and hole geometry for the actual fastener material |
| Linearisation breaks down past ~25°, causing steady-state error | mecatronica-UTC RWIP | Your LQR is only valid near upright — which is fine, your torque budget is too |
| Motor deadzone below a PWM threshold | mecatronica-UTC RWIP | Identify and compensate it in stage 3 |
| Constant sensor bias parks the wheel at non-zero speed forever | ETH Cubli, both papers | Add the bias integrator — Kω alone does not fix this |
One pattern visible only in aggregate: most public "Cubli" repositories are short-lived student projects with a handful of commits and no documented successful balance. The ones that worked published their full parameter set or their actual gains. Treat an undocumented build as unverified, however good the video looks.