Project
In ConstructionIn Progress

Motor Control & Characterization

How this project demonstrates each skill

Controls & Motor Control
Skill

D6374 BLDC on ODrive and VESC. Set up bare motor, 5:1 gearbox, and gearbox+flywheel configs. Tuned position, velocity, and integrator gains across them and watched how reflected inertia changed what the gains needed to be.

Physical Testing & Instrumentation
Skill

Have used multiple boards (ODrive, VESC) in different configurations. Fried hardware along the way, but I understand the systems well enough to diagnose what went wrong and fix it. Still active work.


Motor Testing and Characterization Platform

A bench platform for characterizing a D6374 150kv BLDC motor across static parameters, no-load dynamics, and flywheel-loaded spin-up, using two different controllers (ODrive 3.6 and MKS VESC 75200) as cross-validation. The strongest outputs are not a single dyno number but the test structure, the explicit separation of motor behavior from controller artifacts, and the iteration history behind each result.

Status: six closed findings across static, dynamic, and friction characterization, cross-validated on two controllers. Loaded steady-state testing blocked on a known flywheel identifiability issue (isolation plan written).

Why I built this

I want to design humanoid robot actuators, and characterizing a BLDC from scratch is the honest prerequisite. I wanted Kt, friction, inertia, and thermal behavior as numbers I derived myself, not datasheet values I took on faith. So I built the bench, ran the same motor through both an ODrive and a VESC, fried one of them along the way, and ended up with results I understand from the instrument up.

Closed results

FindingResultSignal
Kt cross-validation0.0551 / 0.0558 Nm/A (1.3% agreement)Two independent controllers confirm the same physical constant
Kv143.6 ± 0.7 RPM/V over 5 points4.3% below datasheet; tight bench spread
Sensorless observer overhead0.85 AQuantified by running both sensored and sensorless on the same motor
Rotor inertia170–175 g·mm²Matched spin-up / coast-down technique, friction-independent extraction
Friction after casing rebuild45% Tc/J drop, 100× repeatability improvementIsolated a rotor rub, proved it with before / after data
Multi-current inertia driftTelemetry bias, not mechanicalRuled out by holding friction constant; drift persisted identically

Platform

  • Motor: D6374 150kv BLDC, 7 pole pairs, star-wound
  • Controllers: ODrive 3.6 (sensored, 19 A ceiling after M0 FET failure) and MKS VESC 75200 V2 (200 A hardware max)
  • Encoder: CUI AMT-102, 8192 CPR, ABI mode
  • Load path: 5:1 custom 3D-printed planetary gearbox into Saris H2 freehub, ~9 kg flywheel + eddy-current brake
  • Bench: Riden RD6030 55 V DC supply, YMC01 micro-ohm meter, clamp DMM, USB + BLE telemetry

Methodology

Every result is tagged to a configuration ID that fixes the controller, sensor mode, and mechanical setup:

  • ODR-ENC: ODrive 3.6 + AMT-102 encoder (sensored, 0 RPM start capable)
  • VESC-SL: VESC 75200 sensorless (observer works badly at low speeds / high starting inertia)
  • VESC-ENC: VESC 75200 + AMT-102 encoder (in process of debugging why this isn't working)

Mechanical configs orthogonal: BARE, GEAR (+ 5:1 gearbox), H2 (+ gearbox + flywheel). A result reported as VESC-SL / GEAR is not interchangeable with ODR-ENC / BARE, and the differences are documented upfront in the Test Setup Log rather than buried in footnotes.

Tests follow a dependency graph: static, no-load dynamic, spin-up sanity, flywheel inertia, coast-down characterization, loaded dyno. Each test either stands alone or explicitly consumes a parameter from an earlier test.

Tooling

Every test is scripted. Custom Python drives the ODrive (odrive_sweep.py, odrive_rotor_inertia.py, odrive_kv_verify.py), and odrive_safe_startup.py validates bus voltage and motor parameters before energising the power stage so a future cal cycle cannot repeat the M0 failure. The VESC side uses a hand-built CLI for remote duty and current commands, plus on-MCU LispBM scripts that log Iq, velocity, and FET temperature at the control-loop rate. That was the only path that got COMM_LOG packets through reliably after VESC Tool 6.06 was identified as a silent packet-dropper; VESC Tool 7.00 is the working version.

Configuration state is tracked separately from data. The Test Setup Log freezes the exact controller, sensor, and mechanical configuration for every run, and the Motor Test Matrix tracks the dependency graph and status across runs. A result without a matching entry in both is not considered closed.

Test coverage at a glance

#TestStatusKey result or gate
1Static parametersDoneR, L, pole pairs, winding confirmed across both controllers
2.1Back-EMF / KtDone0.0551 / 0.0558 Nm/A (ODR / VESC), 1.3% cross-tool agreement
2.2Kv verificationDone143.6 ± 0.7 RPM/V over 5 points
2.3Cogging breakawayDone (VESC-ENC re-run pending)1.9 A sensored vs 2.75 A sensorless = 0.85 A observer overhead
2.4Friction modelDoneTc/J 167 after new casing, 45% drop from V1
2.5Rotor inertiaDoneJ = 170 to 175 g·mm² at 3 A
2.6Step response (no load)Done on ODriveBare and gearbox closed; VESC-ENC version pending
3H2 spin-up sanityDone10 runs, 7.5 t/s max before FOC desync and coupling slip
4Flywheel inertiaPlan written, blockedJ through gearbox + freehub is not identifiable; isolation plan written
5Coast-down characterizationNot startedBlocked on test 4
6Inertia dyno spin-upNot startedBlocked on tests 4 and 5
7Efficiency and Kv under loadNot startedPost-processing on test 6 data
8Gearbox efficiencyNot startedNeeds test 6 baseline
9.1Torque-speed curvePartialODrive hit a 5 t/s PID ceiling; VESC expected to extend
9.2 to 9.5Load / thermal / bandwidthNot startedNeeds sensored VESC + load instrumentation
10Environmental durabilityNot startedProduction-validation stage

Dependency graph

flowchart TD
    T1["Test 1<br/>Static<br/>DONE"] --> T2["Test 2<br/>Dynamic<br/>DONE"]
    T2 --> T3["Test 3<br/>H2 Spin-Up Sanity<br/>DONE"]
    T3 --> T4["Test 4<br/>Flywheel J<br/>PINCH POINT"]
    T4 --> T5["Test 5<br/>Coast-Down"]
    T5 --> T6["Test 6<br/>Dyno Spin-Up"]
    T6 --> T7["Test 7<br/>Efficiency / Kv"]
    T6 --> T8["Test 8<br/>Gearbox Eff."]
    T1 -.-> T9["Test 9<br/>Load Testing<br/>PARTIAL"]
    T2 -.-> T9
    T9 --> T10["Test 10<br/>Environmental<br/>DEFERRED"]

    classDef done fill:#c8e6c9,stroke:#2e7d32,color:#1b5e20
    classDef pinch fill:#ffcdd2,stroke:#c62828,color:#b71c1c
    classDef partial fill:#fff9c4,stroke:#f9a825,color:#795548
    classDef future fill:#f5f5f5,stroke:#9e9e9e,color:#616161
    class T1,T2,T3 done
    class T4 pinch
    class T9 partial
    class T5,T6,T7,T8,T10 future

Test 4 is the current pinch point. Everything from test 5 onward depends on a cleanly identified flywheel inertia, which the current gearbox + freehub path does not provide.


Test 1: Static Parameters (done)

Four-wire Kelvin measurement of motor phase resistance with a micro-ohm meter
Four-wire Kelvin measurement of phase resistance with a (cheap) YMC01 micro-ohm meter; 34 mOhm reading across two phase wires.
ParameterODR-ENCVESC-ENC
R50.2 mOhm (phase-to-phase)24.3 mOhm (per-phase)
L23.1 µH27.6 µH
Pole pairs77
WindingStarStar

The 2x apparent R gap is reporting convention, not a motor variation: line-to-line equals 2x per-phase for a Y-wound motor, so 2 × 24.3 = 48.6 mOhm sits within a few percent of 50.2 mOhm. The residual gap comes from the different detection methods used by each controller. Pole count confirmed by hand rotation. I'm guessing the difference from the cheap milliohm meter comes from the additional resistance from the bullet connections to the ESC internals.

D6374 motor on the bench test stand coupled to the Saris H2 flywheel through a 5:1 gearbox
D6374 installed in the bench test stand, coupled via 5:1 3D-printed planetary gearbox and a one-way freehub to the Saris H2 flywheel. This is the first-iteration coupling; every dynamic and loaded test below runs on some variant of this stand.

Test 2: No-Load Dynamic (done)

Kt / Ke (torque constant)

ConfigKt (Nm/A)
ODR-ENC0.0551
VESC-ENC0.0558

Cross-tool agreement within 1.3%. This is a strong point of validation. These two independent controllers with different detection methods agree on the physically meaningful constant.

Kv verification

143.6 ± 0.7 RPM/V over 5 points, 4.3% below the datasheet rating of 150. Tight spread for bench conditions.

Cogging breakaway

ConfigBreakaway current
ODR-ENC1.9 A
VESC-SL2.75 A (range 2.2 to 3.2 A)

The 0.85 A gap is not additional cogging. It is the sensorless observer spending current to locate the rotor from back-EMF. Running both configurations made that bias visible and quantifiable instead of mixed into a single number.

VESC duty-ramp trace used for cogging breakaway and friction mapping
Full VESC duty-ramp trace used for cogging breakaway identification and friction mapping above the sensorless dead zone.

Planned plot: constant-torque vs back-EMF-limited torque-speed curve

A future addition to this section: the go-kart / EV style torque-speed plot that is flat in the constant-torque region at low speed and drops off linearly as back-EMF approaches bus voltage. Same plot ODrive publishes on their product pages.

Needed inputs: Test 6 (Inertia Dyno Spin-Up) data at multiple current levels, so the kink between constant-torque and back-EMF-limited regions shows up as a function of Iq. Blocked until the Test 4 pinch point clears.


Test 2.6: Step Response, No Load (done on ODrive)

Commanded velocity-loop step inputs on ODR-ENC, with and without the 5:1 gearbox, and logged encoder response. Two purposes: verify the controller tune, and expose how gearbox inertia and friction shift the effective closed-loop bandwidth.

Bare and gearbox configurations closed cleanly on ODrive. The VESC version is blocked until a VESC-ENC tune pass, because the sensorless observer cannot produce velocity feedback tight enough for a meaningful step trace. The ODrive position-control path also stopped here after the M0 driver damage described in Failures below.


Test 2.4 / 2.5: Friction and Rotor Inertia (done, with a mechanical iteration)

Coast-down from regulated speed, fit to a Coulomb + viscous friction model, followed by multi-current inertia extraction.

The iteration. Initial casing had a rotor rub. Rebuilt the casing on 2026-04-10. Results after the rebuild:

  • dropped from ~304 to 167 (~45% reduction in normalized Coulomb friction)
  • settled at 0.602
  • Run-to-run standard deviation dropped from 12.3 to 0.1 rad/s2, a ~100x repeatability improvement

Best rotor inertia estimate: = 170 to 175 g·mm² (at 3 A drive current).

How we actually pull J out of the data

The graph above is a fit to one ODE, not a direct inertia reading. Walk through the math:

Step 1. Write Newton's second law for the coasting rotor. With no drive current, the only torques are Coulomb friction (speed-independent, opposing motion) and viscous drag (proportional to speed):

Step 2. Solve the ODE. This is linear first-order. The closed-form solution is:

Fitting this curve to a measured coast-down trace gives two numbers: (Coulomb-to-inertia ratio) and (viscous time constant). That is what the plot below shows. But notice: neither , , nor appears by itself. We only get ratios. This is why a single coast-down cannot identify .

Step 3. Break the symmetry with a matched spin-up. Now drive the motor with known current . At the same instantaneous speed , write the spin-up equation:

And the coast-down equation at the same :

Step 4. Add them. The friction terms cancel:

No friction model needed. No assumption about how friction scales. The two matched accelerations do the work.

Result. Plugging in measured accelerations at and gives g·mm². Full procedure and validation checks in the Inertia Isolation Test Plan.

Four independent methods for extracting rotor J from the same dataset
Cross-check: four independent methods for extracting rotor J from the same Apr 7 dataset. Geometric estimate (184), point-by-point at 3 A (178), point-by-point at 10 A (211), combined-trajectory fit (198), and steady-state + tau (269). Spread is the honest uncertainty envelope. The 3 A point-by-point and combined-trajectory methods converge near 180-198, which is the working number used elsewhere; the 10 A and steady-state outliers are explained by the Iq telemetry bias discussed below.

The drift

The multi-current inertia drift that had shown up in earlier sweeps did not change after the friction fix. That ruled out a mechanical cause and pointed at Iq telemetry bias from the controller instead, which is the direction future work continues. Full discussion in Cross-Cutting Findings #3.

Coast-down fit to the closed-form omega(t) equation
Coast-down fit to the closed-form ω(t) above. Ratios T_c/J and b/J come from this plot; absolute J comes from pairing it with a matched spin-up at known I_q.
D6374 motor in the original 3D-printed casing
D6374 in the original 3D-printed casing.

Test 3: H2 Flywheel Spin-Up (done, with constraints)

Saris Hammer H2 bike trainer repurposed as an inertia dyno: ~9 kg flywheel + eddy-current brake, BLE telemetry at ~1.1 Hz. Motor drives the flywheel through the gearbox and a one-way freehub, which means spin-up is motor data and coast-down is drag-only data. There is no steady-state loaded operating point through this path.

Max achieved: 7.5 t/s motor shaft (~450 RPM), before the ODrive FOC desync soft ceiling at ~50 t/s mechanical (350 electrical Hz against the 8 kHz current loop).

Field workaround. Winding temperature was read by clipping a DMM across the motor thermistor leads and manually converting voltage to temperature through a lookup. Not a long-term instrumentation plan, but enough to protect the motor for the first campaign while a proper logging path is built.

First H2 dyno run with clamp DMM on thermistor leads
First H2 dyno run. Clamp DMM on the thermistor leads for manual temperature lookup; laptop running the spin-up script.
All 10 H2 spin-up runs overlaid
All 10 spin-up runs overlaid. The knee on the smoother ramps is coupling slip, which became a redesign driver (see below).
Detail view of the best single H2 spin-up run
Detail view of the best single H2 run (smooth ramp 0.3 t/s/s, 70 A limit). Top panels: velocity follows commanded ramp until the coupling / FOC ceiling, Iq climbs steadily through friction. Bottom panels (viridis-colored by time): Friction Profile Iq vs Speed and Motor Torque vs Speed. The bottom-left is effectively a heat-map of how much current the drivetrain demands at each operating speed; the spatial density along the curve shows where the motor spent most of its test time.

Test 4: Flywheel Inertia (plan written, blocked)

The goal was to measure the H2 flywheel's moment of inertia so that later dyno torque extraction (test 6 onward) could use a known J. With the current drive path that turned out to be an identifiability problem, not a measurement problem:

  • The one-way freehub means the motor only drives the flywheel in the positive direction; the motor cannot decelerate the flywheel directly for a clean powered-deceleration coast-down
  • The 5:1 gearbox adds its own friction and inertia, and that contribution is not separable from the flywheel itself through the current coupling

The decision was to stop and write an isolation plan rather than collect data that would fold motor, gearbox, freehub, and flywheel losses into one number. Two unblockers under consideration: direct drive with the flywheel replacing the freehub, or BLE-only coast-down of the flywheel alone after an external spin-up.

Why this has stalled

I accidentally fried the motor-driver IC on the ODrive board, which forced me onto the VESC. The VESC sensored mode has not worked despite many hours of debugging, so closed-loop encoder tests are currently blocked on either ESC. Plan after exam season is to get one of the two paths working: either replace the chip on the ODrive (or swap to a non-deprecated board like the ODrive Pro), or solve the VESC encoder mode directly. Full debugging log in Cross-Cutting Findings.

Written up in full as the Inertia Isolation Test Plan. Blocks tests 5, 6, 7, and 8 until resolved.

Freehub interface that makes flywheel J unidentifiable
The freehub interface that makes flywheel J unidentifiable through the current drive path. One-way engagement by design; a measurement obstacle for inertia extraction.

ODrive vs VESC

Two controllers, same motor. The point is not to pick a winner; it is to cross-validate and to give each test the controller best suited to it.

RoleODrive 3.6VESC 75200
Current headroom19 A self-imposed safety ceiling (DRV8301 hardware goes higher; 19 A set after M0 FET failure)200 A hardware
Min reliable speed (sensorless)n/a~400 RPM
Position controlyespartial (VESC-ENC not working yet)
Telemetry depthodrivetool Pythoncustom CLI + VESC Tool GUI + LispBM on-MCU
Best used forfast sensored bring-up, Kv/Kt baseline, rotor inertiahigh-current sweeps, encoder + sensorless contrast, thermal work
ODrive 3.6 with RD6030 bench power supply
ODrive 3.6 + RD6030 at 55 V (ODR-ENC). The fans are here so that when I plan to do high current testing, I can unlock much of the operating region of this motor and increase its continuous torque output.
MKS VESC 75200 V2 with phase wiring
MKS VESC 75200 V2 with phase wiring (VESC-SL / VESC-ENC).
Hand-crimped JST-PH harness from AMT-102 encoder to VESC sensor header
Sensor-port interface on the VESC: hand-crimped JST-PH harness connecting the AMT-102 encoder to the VESC sensor header. This is the connector path whose commutation is still being debugged.

Engineering lessons

Four that came out of the cross-controller structure and generalize beyond this motor:

  1. Config IDs paid for themselves. Tagging each result to ODR-ENC / VESC-SL / VESC-ENC and BARE / GEAR / H2 let the 0.85 A sensorless-observer overhead fall out as a clean delta instead of disappearing into run-to-run scatter. Without the tagging, the same data reads as noise.
  2. A single-number Kt is a lie. Measured Kt dropped ~4% from 2.3 kRPM to 6.3 kRPM. Speed-proportional iron loss, not an observer artifact. Any Kt reported without an operating speed is already half wrong.
  3. Multi-current inertia drift was the telemetry, not the mechanics. Rotor J fits grew with Iq, and the drift stayed identical after a major friction rework. That ruled out a mechanical cause and pinned it on VESC Iq window-averaging. Low current is where the method stays honest, which is the opposite of the usual signal-to-noise intuition.
  4. The gearbox is not a passive modifier. Adding the 5:1 reduction changed the control problem, not just the gain schedule. A ramp rate that worked bare stalled the integrator through the reduction, and a separate 3D-printed coupling adapter added ~51% to the measured inertia on its own. Load-path geometry is part of the test, not a boundary condition.

Failures and Redesigns

Below are the three physical failures from the campaign. Controller-level tuning surprises from the same work are collected separately in the ODrive Tuning Discoveries (ODrive era) and Cross-Cutting Findings (VESC era) notes.

ODrive M0 FETs blown (2026-03-29)

Current limit raised during troubleshooting, power stage did not survive. The M0 gate driver IC shows visible chip damage consistent with either a phase short or pushing bus voltage too close to the device limit. This is the reason M1 is the only live motor port on the ODrive, why the 19 A ceiling is now a hard rule, and why loaded testing and position-control work migrated to the VESC. odrive_safe_startup.py was written so future cal cycles validate bus voltage and parameters before energising.

Next steps to bring the ODrive back:

  1. Order the replacement driver IC (DRV8301-family)
  2. Rework the M0 power stage on the bench soldering setup
  3. Bring M0 back online under the 19 A ceiling before trusting it
Damaged M0 driver IC on the ODrive 3.6 with visible package damage
Damaged M0 driver IC on the ODrive 3.6. Chipped / cracked package visible on the power stage.
Stepped H2 runs at 35 / 50 / 70 A showing FET thermal rise during stall
Evidence for the thermal-during-stall pattern: three stepped velocity tests at 35 / 50 / 70 A current limits. Top panel (velocity) shows the motor holds until the commanded step exceeds what the drivetrain can sustain, then stalls. Middle (Iq) shows current pegs at the set limit during stall. Bottom (FET temperature) shows the 70 A run climbing past 85 °C and hitting the derating line. FETs only cook when the motor is stalled at maximum current. Normal operation through all three runs kept FETs in the 35 to 55 °C band.

H2 coupling slip (2026-03-28)

Freehub interface loosened on higher-torque runs. Visible as the early knee on the smoother ramps in the H2 overlay. Captured as a fix item, not glossed over.

Gearbox V1 drag dominated the friction map

Once the new casing ruled out the rotor rub, the remaining friction story pointed at the reduction, not the motor. V1 was pulled and opened for teardown.

5:1 planetary gearbox V1 opened for teardown
5:1 planetary gearbox V1 opened up. First case of gear stripping, no woodruff key at this point.
Motor shaft after the woodruff-key fix
Motor shaft after the woodruff-key fix (plastic key, pressed into a keyway on the motor shaft and the pinion). The key was the change that stopped the interface from rounding out under load. Candidate photo: confirm this is the right shot, otherwise swap.

Status and Next

  • Tests 1 (static) and 2 (no-load dynamic) closed
  • Test 2.3 cogging still needs a VESC-ENC re-run to close the observer-overhead loop with direct sensor data
  • Test 3 H2 spin-up sanity closed, 10 runs logged
  • Test 4 flywheel inertia blocked: gearbox + one-way freehub makes flywheel J unidentifiable through the current path. The Inertia Isolation Test Plan is written; unblocks via direct-drive or BLE-only coast-down
  • Tests 5 through 8 follow once test 4 clears
  • Tests 9 (load) and 10 (environmental) are production-validation territory