Build & Tune Your First FOC Actuator
Build & Tune Your First FOC Actuator
A practical, no-fluff walkthrough: take the QDD Actuator Starter Kit from a box of parts to a
smooth, closed-loop, backdrivable robot joint in about an hour. Written for people who've used a
soldering iron but maybe never tuned field-oriented control before.
This guide targets the kit's ODrive-compatible driver (stock ODrive 3.6 firmware /
odrivetool). For the Arduino-based Mini FOC Kit, jump to the SimpleFOC section.
Official references: ODrive docs · SimpleFOC alignment procedure.
What you'll need
- The QDD Actuator Starter Kit (motor + ODrive-compatible driver + AS5047P encoder + magnet/mount)
- A 24 V bench power supply (start at a 3–5 A limit) and a brake resistor (often included on the board)
- A USB cable and a computer (Windows/Mac/Linux) with Python +
pip install odrive - Basic tools, and thread-locker for the magnet/mount
⚠️ Safety: an actuator can spin or kick hard during calibration. Clamp the motor down, keep
fingers and cables clear, start with a low current/voltage limit, and keep one hand on the power
switch.
Step 1 — Mechanical assembly
- Mount the diametric magnet centered on the motor's rear shaft/rotor, axis-aligned. Concentricity
matters — an off-center magnet = noisy angle readings. A drop of thread-locker keeps it put. - Mount the encoder board facing the magnet at the gap in its datasheet (typically ~0.5–2 mm).
Keep it square to the shaft. - Fix the motor to a solid bracket. If you're adding a 6:1–10:1 planetary/cycloidal gearbox
to make a true QDD actuator, mount it now but calibrate on the bare motor first.
Step 2 — Wiring
- Motor phases (A/B/C) → the three driver phase outputs. Order doesn't matter electrically —
calibration sorts out direction. - Encoder → driver SPI: connect MISO/MOSI/SCLK/CS + 3.3 V + GND per the kit's pinout.
- Power: 24 V supply → driver DC input (mind polarity!). Connect the brake resistor — FOC
dumps energy back on deceleration and needs somewhere to put it. - Double-check grounds before powering on.
Step 3 — Configure ODrive (the important part)
Connect USB, run odrivetool, then set the motor up. This motor is a gimbal motor (high
phase resistance, low current) — the single most common first-timer mistake is leaving it in
high-current mode. Use gimbal mode:
# --- Motor (gimbal-type: high resistance, voltage-controlled) ---
odrv0.axis0.motor.config.motor_type = MOTOR_TYPE_GIMBAL
odrv0.axis0.motor.config.pole_pairs = 11 # GM5208-class: count magnets ÷ 2
# In gimbal mode, current_lim / calibration_current are interpreted as VOLTS:
odrv0.axis0.motor.config.calibration_current = 5 # volts
odrv0.axis0.motor.config.current_lim = 8 # volts
odrv0.axis0.motor.config.torque_constant = 8.27 / KV # use the motor's Kv
# --- Encoder (AS5047P, 14-bit absolute over SPI) ---
odrv0.axis0.encoder.config.mode = ENCODER_MODE_SPI_ABS_AMS
odrv0.axis0.encoder.config.cpr = 16384 # 2^14
odrv0.axis0.encoder.config.abs_spi_cs_gpio_pin = 5 # per your board
# --- Bus / brake ---
odrv0.config.dc_bus_overvoltage_trip_level = 28
odrv0.config.brake_resistance = 2.0 # ohms, per your resistor (0 if none)
odrv0.save_configuration()
Now run the one-time calibration — it measures the motor and learns the encoder offset and
direction:
odrv0.axis0.requested_state = AXIS_STATE_FULL_CALIBRATION_SEQUENCE
# motor beeps/clicks, then the shaft sweeps to find the encoder offset
dump_errors(odrv0) # should be clean
If it's clean, make the calibration stick so you don't repeat it every boot:
odrv0.axis0.encoder.config.pre_calibrated = True
odrv0.axis0.motor.config.pre_calibrated = True
odrv0.axis0.config.startup_closed_loop_control = True
odrv0.save_configuration()
Step 4 — Closed-loop control + tuning
odrv0.axis0.requested_state = AXIS_STATE_CLOSED_LOOP_CONTROL
odrv0.axis0.controller.config.control_mode = CONTROL_MODE_POSITION_CONTROL
odrv0.axis0.controller.input_pos = 0
Tune the gains — start low and work up:
vel_gain— raise until the joint feels stiff; back off ~25% when it starts to buzz/whine.pos_gain— raise for snappier position holding; too high → oscillation.vel_integrator_gain— a rule of thumb is0.5 * bandwidth * vel_gain; kills steady-state droop.
Test backdrivability: in torque mode with a low setpoint, the joint should move freely when you
push it — that's the whole point of QDD. Watch motor temperature; if it climbs at idle, your
voltage limit is too high or the encoder offset is off.
odrv0.save_configuration() when you're happy.
Common pitfalls (and the fixes)
| Symptom | Likely cause | Fix |
|---|---|---|
| Vibrates, won't spin smoothly | Wrong pole pairs | Recount magnets ÷ 2; re-run calibration |
| Motor gets hot at standstill | Left in high-current mode, or bad encoder offset | Use MOTOR_TYPE_GIMBAL; recalibrate |
| Encoder error / noisy angle | Magnet off-center or gap wrong | Re-center magnet; set gap per datasheet |
| Overvoltage error on stop | No/!wrong brake resistor | Add resistor; set brake_resistance |
| Spins the wrong way | Phase/encoder direction | Calibration handles it — just re-run it |
| Calibration fails immediately | Wiring/power | Check phase + encoder wiring, PSU current limit |
Appendix — the SimpleFOC (Arduino) path
If you're using the Mini FOC Kit (SimpleFOC-friendly driver + Arduino), the flow is the same
idea, different tools (alignment docs):
#include <SimpleFOC.h>
BLDCMotor motor = BLDCMotor(11); // pole pairs
BLDCDriver3PWM driver = BLDCDriver3PWM(/*pwm pins*/);
MagneticSensorSPI sensor = MagneticSensorSPI(AS5047_SPI, /*CS pin*/);
void setup() {
sensor.init(); motor.linkSensor(&sensor);
driver.voltage_power_supply = 24; driver.init(); motor.linkDriver(&driver);
motor.voltage_limit = 6; // gimbal motor: keep it low
motor.controller = MotionControlType::angle;
motor.init();
motor.initFOC(); // runs the alignment procedure
}
void loop() { motor.loopFOC(); motor.move(target); }
initFOC() does the same three things ODrive's calibration does: pole-pair check, zero
electric angle (records the sensor offset), and current-sense alignment. Once you have the
printed zero_electric_offset and sensor_direction, hard-code them to skip alignment on boot.
Same tuning logic: raise the velocity/angle PID gains until stiff, back off before it buzzes.
Stuck? That's what we're here for — reach out and we'll help you tune it. — [BRAND]