Debugging SimpleFOC: From Motor Clicks to Smooth Haptic Feedback
Theory says this should just work. Reality had other ideas. Here’s what actually happened when I tried to get SimpleFOCMini working for my haptic glove yesterday.
The setup that should have worked
STM32G431 Blue Pill
SimpleFOCMini gate driver
2208 BLDC gimbal motor (7 pole pairs)
AS5600 magnetic encoder
Goal: impedance control for haptic feedback. Motor should track finger position, resist fast movement, simulate object contact with force feedback.
I flashed the basic closed-loop torque control example. Motor calibrated (did the left-right wiggle during initFOC), then… nothing. No resistance, no feedback. Just spun freely like there was no control at all.
Problem 1: Motor goes dead after calibration
First code had motor.move(0). Zero torque target. Which the motor dutifully executed. Zero torque means no holding force. The motor calibrated fine but then literally did nothing because I was commanding it to do nothing.
Fix: actually command some torque based on velocity.
void loop() {
motor.loopFOC();
float velocity = motor.shaft_velocity;
float target = 0.8 * velocity;
motor.move(target);
}
Now it resisted when I rotated fast. This is impedance control, damping coefficient times velocity equals torque.
Problem 2: Jumping and vibration
With impedance working I hit a new issue. Slow movement felt smooth. But at certain angles the motor would vibrate violently. Jump to a position, shake, settle. Move further, more shaking.
This is control instability. Damping gain at 0.8 was too high. Small velocity fluctuations got amplified into large torque commands. That caused position jumps, which caused velocity spikes, which fed back into more torque. Classic positive feedback loop.
Fix: reduce gain and add velocity filtering.
motor.LPF_velocity.Tf = 0.1;
void loop() {
motor.loopFOC();
float velocity = motor.shaft_velocity;
float damping = 0.3; // down from 0.8
float target = damping * velocity;
target = constrain(target, -3.0, 3.0);
motor.move(target);
}
LPF_velocity.Tf is a time constant for exponential smoothing. I started at 0.05, bumped to 0.1, then 0.2 until vibration stopped. Settled on 0.1 as the balance point between smooth and responsive.
Problem 3: Cogging and clicks
Vibration gone but now there were clicks. Small periodic clicks as the motor rotated. Not software instability. Physical detents.
This is cogging torque. The 2208 has 14 magnetic poles (7 pole pairs) which creates 7 natural detent positions per revolution. You can feel these even with motor unpowered.
Fix: anti-cogging compensation.
float angle = motor.shaft_angle;
float cogging_compensation = 0.15 * sin(7.0 * angle);
float raw_target = damping * velocity;
raw_target -= cogging_compensation;
motor.move(raw_target);
Sinusoidal torque opposes the motor’s detent positions. When rotor approaches a magnetic pole, compensation torque pushes against it. Result: smoother rotation through detents.
Tuning was trial and error. Too low (0.08) and clicks remained. Too high (0.3) and motor felt jerky in the opposite direction. 0.15 was the sweet spot.
Problem 4: Contact wall clicks
With smooth impedance working I added a virtual contact wall for object simulation. First try used proportional spring:
float wall = 1.5;
if (displacement > wall) {
float push = displacement - wall;
target += push * 2.0;
}
Soft boundary for foam simulation. For rigid objects I tried jumping straight to maximum torque:
if (displacement > wall) {
target = 4.0;
}
Terrible clicking. Sudden step from 0 to 4.0 made the motor cog badly as it tried to snap phases.
Fix: fast ramp instead of instant step.
if (displacement > wall) {
float penetration = displacement - wall;
float contact_force = penetration * 30.0;
contact_force = constrain(contact_force, -4.0, 4.0);
target = contact_force;
}
Multiplying by 30 means torque ramps from 0 to max within 0.13 radians (about 7 degrees). To your finger this feels like hitting a wall. To the motor it’s smooth enough to avoid cogging.
Problem 5: Output smoothing
Even with the ramp there were small clicks during transitions. Free zone to contact, or contact release back to free. The issue: control loop running fast enough that even the smooth ramp looked like discrete steps to the motor.
Fix: exponential smoothing on output.
float smooth_target = 0; // global
void loop() {
// calculate raw_target...
float alpha = 0.12;
smooth_target = alpha * raw_target + (1.0 - alpha) * smooth_target;
motor.move(smooth_target);
}
Single-pole IIR filter on the torque command. Instead of motor seeing instantaneous jumps, it sees smooth exponential transition. Time constant is 1/alpha. At 0.12 the output reaches 63% of target in about 8 loop cycles.
Lower alpha equals smoother but slower. Higher alpha equals faster but more clicks. 0.12 balanced smoothness with haptic realism.
What the final control loop looks like
After all fixes:
void loop() {
motor.loopFOC();
sensor.update();
float velocity = motor.shaft_velocity;
float angle = motor.shaft_angle;
float displacement = angle - start_angle;
// 1. Anti-cogging
float cogging = 0.15 * sin(7.0 * angle);
// 2. Base impedance damping
float raw_target = 0.15 * velocity;
raw_target -= cogging;
// 3. Contact wall (rigid object)
if (abs(displacement) > 1.5) {
float penetration = abs(displacement) - 1.5;
float direction = (displacement > 0) ? 1.0 : -1.0;
float contact_force = direction * penetration * 30.0;
contact_force = constrain(contact_force, -4.0, 4.0);
raw_target = contact_force;
}
// 4. Output smoothing
raw_target = constrain(raw_target, -4.0, 4.0);
smooth_target = 0.12 * raw_target + 0.88 * smooth_target;
motor.move(smooth_target);
}
Four stages. Each solves a specific problem I hit during debugging.
Video Demonstrations
Speed Control Using SimpleFOC Mini
This shows basic velocity control mode where the motor maintains a target speed. The SimpleFOC library’s PID controller adjusts torque to match the setpoint.
Demonstrating FOC velocity control with the motor maintaining target speed
Haptic Contact Simulation: Virtual Object Resistance
Here you can see the contact wall working. Motor rotates freely within the threshold then hits a firm virtual wall and resists further movement.
Simulating rigid object contact: free rotation until threshold then strong resistance
What I learned from this
Documentation lies by omission. SimpleFOC examples show basic torque control in 10 lines. They don’t show vibration at certain gains, cogging on cheap motors, or clicking during rapid setpoint changes. Those only appear when you build something real.
Control theory and reality diverge. PID tuning formulas assume ideal actuators. Real motors have cogging, encoder noise, driver delays, EMI. Textbook gave starting values. Trial and error gave working values.
Smoothing fixes most problems. Vibration, cogging artifacts, transition clicks all improved with filtering. Velocity filter, anti-cogging compensation, output smoothing. When in doubt smooth it out.
Iteration matters more than initial design. I didn’t design this control loop. I debugged it into existence. Start simple, add complexity only when specific problem appears, tune until it works.
Reality of embedded development
Total time from motor spins open loop to smooth haptic impedance control: about 6 hours. SimpleFOC library did the hard part (FOC mathematics). But getting from library example to production-quality haptic feedback required addressing cogging, instability, and control artifacts that no tutorial mentions.
This is normal. Embedded development is iterative debugging with brief moments of “oh that’s what was wrong.” The motor that clicks is telling you something. The vibration at certain angles is data. The jump during contact is a clue.
Listen to the hardware. It’s louder than the documentation.
Next steps
This is one finger. Full glove needs five running simultaneously, coordinated through ESP32, synchronized with Apple Vision Pro under 40ms latency. Each finger will have different cogging patterns, encoder offsets, optimal damping values.
But now I have the debugging methodology. When finger 2 vibrates at different gain than finger 1, I know where to look. When finger 4 has worse cogging, I know how to compensate. The proof-of-concept validated both hardware stack and debugging process.
One motor down. Four to go.
~Ajit George