Experiences ODrive fdm step dir

I have a fdm printer which uses normal hybrid steppers to accelerate when printing up to 5g and when traveling up to 10-15g. Because of the limited supply voltage (back EMF) i am limited to a speed of about 400-500 mm/s. The travel acceleration and max speed is limited by the max torque and because of the back EMF the max torque drops with speed.

Therefore i wanted do push the limits using ODrive, that means more acceleration and more speed, because normal BLDC should not have a speed limit and a better torque/inertia ratio. For all who think that fdm is a “no load” application, i did choose the accelerations settings in a way that about 80 % of the max available torque is used. So there is enough load. Power is for sure low because of low revs.

For fdm applications the steady state setpoint error doesn´t matter at all because there is no steady state. Also high frequency noise on the position doesn´t matter, as long as there is no dominant frequency. But independently from moving fast or slow as soon as the setpoint deviation has any dominant low frequencies it will be visible. Accelerations and speeds are also quite high (for ODrive) as how below. The main problem ist trajectory tracking. In short terms an ODrive solution for fast (and by that non geared) belt driven fdm machines has severe problems with all above. The only part where ODrive can outperform the steppers is the steady state error - which does not give any benefit. I also had never problems with missing steps and there are much cheaper and reliable solutions.

So far with ODrive i am not able to achieve the open loop stepper performance and i am far away from outperforming it. The setpoint deviation with ODrive closed loop ist much bigger than with the open loop stepper. In my opinion it is a bandwidth/noise problem due to the PID-loops/cascades. A too low bandwidth of a PID-loop means, the gains are to small. It is typically for PID-loops, that there is a setpoint deviation if the setpoint is ramping. The faster the PID loop (especially the higher the D-part), the smaller the setpoint deviation.

At the beginning i also had the known cogging issues at low speeds (even with a NEMA23 3 phase hybrid stepper, which by itself cannot have these issues). Anti-cogging didn’t really help, it just added high freq noise, preventing increasing gains.

Luckily with increasing the encoder bandwidth to > 3000 rad/s (as well as the current bandwidth), the cogging problems could be more or less solved. Increasing the bandwidth enables the use of much higher gains, because otherwise the filter would interact with the PID-loop. Above 4000 rad/s the encoder bandwidth is limited by firmware and if the current bandwidth gets close to 4000 it is getting unreliable.

I did compare many and the following motors in more detail:

  • NEMA17 2 phase hybrid stepper on Duet Wifi (reference, open loop)
  • BLDC NTM Propdrive 35-36 on ODrive
  • BLDC SK3 5065 on ODrive
  • NEMA23 3 phase hybrid stepper on ODrive

All BLDCs as well as the NEMA23 performed without significant difference about the same. The higher the velocity the bigger is the position error and the bigger the gap to the open loop 2 phase NEMA17 on the Duet. The acceleration didn’t matter significantly. Even at 10g and 25600 mm/min (426 mm/s) the open loop stepper was better.

E.g. G1 X96 F9600 gives the following result with an 16384 encoder. I have to add that 16384 counts equals to 32 mm of travel. One 1/16th microstep to 0.01 mm or 5.12 counts. So a full step equals to 81.92 counts. The test was done on my test-bench. Load was the inertia of the motors - which is more or less the load in real life in my case.

The 1st pic shows a tuning i could use in real life (but i didn’t really try).

The 2nd pic shows a tuning which is much much too aggressive and only works sometimes, i could never even use it in real life, but i wanted to test, if by increasing gains (bandwidth) we could solve the problem. As shown in principle it works, but we are still not able to outperform the stepper and as mentioned, this tuning doesn’t work in real life.

G1 X96 F9600 turns the motor in total 3 times. at a speed of 5 rps (160 mm/s about my “normal” printing speed) with 2g acceleration in this case. Jerk was set to more or less 0 to sort out problems. With normal jerk values the BLDCs have even more troubles to keep up. The ODrive deviation increases quite linear with speed, a clear bandwidth problem indication. Although the “position noise” is smaller with ODrive, it doesn’t matter at all (you won´t see it) and it will get smaller under real conditions with more damping in open loop, but not so much in closed loop.

I did compare the “BLDC” NEMA23 hybrid stepper 3 because before increasing the encoder bandwidth, i couldn´t have used ODrive at all for fdm because of cogging. Because the NEMA23 hybrid stepper 3 phase cannot have a cogging issue by itself (otherwise hybrid stepper would not be used…), it was clear that it should perform best in this regard. I know that experts say from “a hybrid stepper cannot work at all with ODrive” to “it is experimental in best case”, it is no PMSM… I didn’t care, i just tested it and it performed at least as good as all the others and i have not been able to see any problem/difference. Cogging with the NEMA23 was much better compared to the others (no surprise).

Because it is a bandwidth problem, all kind of “gearing” the encoder won´t work, because we have to increase the ratio encoder_counts/noise. It would be cheaper just to buy a better encoder….

To investigate the influence of the encoder counts it did the following:

In general i use the CUI AMT 8192 encoders and it came out, that at 2048 CPR i could get slightly better results than on 8192 cpr, because i could use much higher gains - but by far not as high as theoretically possible. By reducing the the encoder pulse count by a factor of 4, without noise it should be possible to increase the pos_gain by factor of 4.

If with the same encoder, the encoder count is reduced by a factor of 4, the noise is reduced to 1/sqrt(4) = ½ and the velocity noise to sqrt(2)/sqrt(4)/4 = 0.177 (sqrt(2)*encoder_noise/dt). So in general the velocity noise level is sqrt(2) * encoder noise / dt bigger than the encoder noise itself. Higher counts mean lower dt and as well higher velocity noise.

So by reducing the encoder count i relaxed the velocity noise problem and could increase the vel_gain. This in combination with higher pos_gain leaded in the end a better total performance ! Although i couldn´t increase Kp by 4.

I also used a CUI 16384 cpr encoder instead of the 8192 one and i have not been able to improve results.

Some thoughts:

There is no benefit of running a PID-loop faster than 10-20 time of its bandwidth. But if there is noise and cascades and/or a D-part, a faster execution time will just amplify noise. I don´t know, but because there is only one “encoder_bandwidth” parameter, i assume that the encoder counts are only low pass filtered once and this goes into everything else.

The velocity loop must be about 5-10 times faster than the position loop to work reliably stable. if the position loop runs 10 times slower as the velocity loop, the encoder signal to the position loop should have 10 times less bandwidth, otherwise the noise gets just amplified by the gains and excites the velocity loop, which amplifies the noise again and bothers the current loop and so on. Because the vel_gain is the D-part, this is really bad and limits the use of high gains.

So how is the velocity (position) feedback term into the velocity loop filtered ? Is it just the encoder_bandwith ? What about applying a lower bandwidth filter to the position feedback ?

4000 rad/s equals to 637 Hz. What is the limitation of the current bandwidth - current noise ? Afaik the PID-loops run all at 8 kHz, could that be increased to use a higher current bandwidth ? It is clear the motor cannot response that fast but the problem is the PID-loop and not the motor itself.

Feedforwards especially inertia and speed should help in my case, but for all nonlinear printers like deltas, Hs, Corexy, scaras, etc. it won´t help much. Also for applying FF the input in the FF must be heavily filtered, reducing its benefit. In addition the official current firmware doesn’t support it. But maybe there is a much better solution.

gain scheduling vel_gain velocity loop:
Current equals to torque, so if a current is applied the inertia starts to accelerate. Depending on the ratio inertia / current it takes a while to achieve a certain speed change. The problem is, that this is highly nonlinear. The energy needed to accelerate an inertia from v1 to v2 is proportional to v2^2-v1^2. So if v1 = 0 and v2 = 2 the difference equals to 4. If v1 = 2 and v2 = 4, the difference in speed is also 2 but the needed energy 4^2-2^2 = 12 —> it is already 3 times bigger - which means it lasts 3 times longer to catch the setpoint. From that follows a linear PID loop cannot be tuned optimal for high and low speed regions. The low speed region will limit the gains preventing proper control at high speeds. Or in other words, the bandwidth of the whole PID-loop will decrease with high speeds - that is exactly what i see.

This problem could be overcome by gain scheduling. The proportional gain of the velocity loop should not be fixed, it should act like e.g. vel_gain = base_gain +abs(factor1 *(speed/base_speed)) + factor2 * (speed/base_speed)^2. So one term ~v^2 is for inertia and the other ~v for friction. I didn´t figure out how to set it in combination with the pos_gain, but that should be solvable. Also the input to gain scheduling needs to be filtered a lot.

I did a quick test in velocity control and tuned vel_gain always close to instability. A nice curve like below came out:

Because in position mode the vel_gain acts as the D-part, when ramping position, the D-part will add a constant current proportional to the speed. So it is nothing else as a velocity proportional feedforward. Therefore it is important to get it as high as possible to increase bandwidth.


Thanks for the detailed writeup!

Did you use the feedforward terms? step-dir exhibits significant phase lag and tracking error without the feedforward. Try the Trapezoidal Trajectory planner, or the Input Filter mode for step-dir on the RazorsEdge branch

The encoder bandwidth is only used for velocity estimation - it’s a discrete phase detector with a 2nd order filter. It’s used as an observer more like a PLL to estimate future position, rather than a phase-lag inducing filter.

Time is my most valuable resource and i cannot start to play with the whole ODrive development chain. I am used to program, but i am not a firmware developer. So i can use only the “official” firmware versions, or the one which was originally on my 2nd ODrive (a dev version including anti-cogging).

Maybe the ODrive developers rethink if dev-versions should not also be available as compiled binary.

The encoder bandwidth is only used for velocity estimation - it’s a discrete phase detector with a 2nd order filter. It’s used as an observer more like a PLL to estimate future position, rather than a phase-lag inducing filter.

Well i guess by increasing the encoder_bandwidth, i shifted the bandwidth of the velocity loop to higher values and therefore i also could use high pos_gains, giving in total better results. High pos gains means higher bandwidth of the position loop and if the velocity loop is not fast enough, they fight against each other. If the bandwidth of the velocity loops comes to close to the of the current loop it also gets unstable, that happens about approaching 4000 rad/s.

Which values are used by vel_gain ? The velocity estimator ?
Which values are used in the position loop ? Unfiltered ones ?!?!?

It would be a really really bad bad idea to use cascades on the same frequency with D-part and inappropriate filtered values, that just amplifies noise…

See https://docs.odriverobotics.com/control

It’s a bad idea to filter the position loop, as that creates phase delay, and there’s little to no white noise from an optical encoder (only quantization noise)

Thanks, i know the control doc, but i couldn’t find any filters there, that’s why i have asked.

I am sorry, but i cannot follow:

  1. A bandwidth of xyz Hz doesn´t mean, that everything > xyz Hz is 0. But if the bandwidth of the PID loop is e.g. 50 Hz and the PID frequency is 8Khz, it doesn’t add any significant delay/phase shift at all if one filters the input signal with e.g. 500 Hz.

  2. There is process noise and measurement noise and the encoder itself may behave well, but what about the rest ? E.g. a NEW bearing has a typical static clearance of some um. The CUI encoder discs have a diameter of e.g. roughly 20 mm, that gives about 3-4um per 16384 tic. So only the bearings add already noise in the range of the encoder resolution…

  3. The inertia acts as a filter from current to position, but e.g. clearance is not affected by inertia. The P & D-parts will shortcut any setpoint deviation from input to output with the PID-loop frequency. This in combination with quantization error, cascades and the D-part is as worse as it can get. The input filter only acts on the setpoint (as far as i could find out), but not on the process value.

  4. Adding stupid e.g. PT1 filters doesn´t add any CPU time. But in terms of robustness, they increase the predictable behaviour a lot.

How do you explain that increasing the encoder count (above really low values) reduces or at least not improves the total bandwidth/performance of ODrive ?

Increasing the encoder counts increases the noise on the D-part (vel_gain) most - but also on the position. In addition the velocity loop gets its setpoint from the position loop and the D-part acts on the setpoint deviation and not only on the process value. The P-part (D also) copies/amplifies the input to the output in no time (and not with the bandwidth or any phase shift !). The only filter is the inertia, which does not really act on e.g. bearing clearance in a um scale.

German detected :wink: For everyone else, a PT1 is a simple single pole low pass filter

I’ll have to go back over your math but it should improve the possible bandwidth of the ODrive, with correct tuning.

Ah, I see. I wouldn’t normally classify that as noise although i suppose you could consider it as such. That is backlash in the system which should not be filtered, imo by the commutation encoder. The system dynamics, such as backlash, should be compensated externally by the machine’s coordinated motion controller. While I agree that there are improvements we can make to ODrive’s operation as a servo driver, be careful not to conflate the machine behaviour and the servo behaviour.

That still doesn’t make sense to me. Increasing the count decreases the error in the velocity estimation, you showed that yourself. By decreasing the resolution of the encoder by a factor of 4, you should, in the absence of any noise, be able to increase the gains by a factor of 4, but you were not able to - likely because your velocity estimates were worse. Someone should get a high count (millions of counts) encoder and test this again.

By the way, I’ll just mention that we didn’t really come up with this control scheme. This is the canonical control method for servos from literature. https://www.machinedesign.com/controllers/adaptive-nonlinear-algorithm-optimizes-servo-control

…to make it short, i am playing now with razorsedge.

For beginning i use the following values:

odrv0.axis0.controller.config.input_mode = 3
odrv0.axis0.controller.config.input_filter_bandwidth = 1500
odrv0.axis0.controller.config.inertia = 2e-7

Is there anything else to set ?

I did choose the bandwidth to be very sure it doesn´t slow down anything - but what does it do ? What happens if i set it too high ? Is it smoothing something (quantization?) ?

I did choose the inertia to avoid allpass/invers behavior, i think it is fine.

I see clearly that iq is bumped up much more at the start, which is fine.

But when i try to generate my test charts (benchmarks), something weird is going on, it looks like the .shadow_counts are multiplied or something like that ?!? E.g. the setpoint deviation in counts doesn´t decrease with mode == 3, compared to mode == 1, but the setpoint deviation of the open loop NEMA17 stepper is about 2-3 times higher as before and the sign is reversed ? Putting everything back to mode == 1 and it is fine again. Is this a problem with the step/dir interface in mode 3 ? Can the shadow counts be affected at all ?

Sorta, yes. This plot is what results from a 1000 count to 0 step response. The velocity and current are fed forward (if using .inertia) and the machine should move as if there’s a critically damped 2nd order mass/spring/damper system

Uhh weird. No guarantee that the RazorsEdge branch is 100% working, unfortunately, but if you find any bugs please let me know so I can fix them

Pretty unlikely since the mode doesn’t handle that kind of stuff, but anything is possible…

They shouldn’t be, that’s the reading from the encoder… the counts_per_step is usually set to 2 by default but that shouldn’t have changed.

… the problem was, that i used the pos_setpoint from axis0 also as reference for axis1.
axis0 is odrive & axis1 is the stepper. Axis0 gets the step/dir input and axis1 is only used for the encoder. As soon as i use mode = 3 the setpoint deviation for axis1 is “garbage”, i can influence it with e.g. the bandwidth from axis0. When i use input_pos instead of pos_setpoint it makes sense again… Anyhow, i can test without axis1.

Some findings so far:

  • increasing the inertia leads to overshoot
  • to dampen the overshoot i need to decrease bandwidth - which adds phase shift which seem to compensate the “inertia effect”
  • so in general higher values for bandwidth with lower values for inertia work better (for me) than vice versa

Unfortunately, i am able to improve the deviation from pos_setpoint - estimated_position by about 20 % in best case, but not the deviation from pos_setpoint - shadow_counts (which is the real offset).

Maybe we should use the real values instead of all the estimators/observers ?

So if i don´t do something completely wrong, this is not the key change.

I suddenly understand what you’ve been asking about. The difference between estimated_position and shadow_count during transient is really the issue here, right? Because shadow_count is what the stepper is going to be.

I’m not sure if you’ve already gone through the code, but pos_estimate_ has an observer running (the one previously mentioned), which is dependent on the encoder bandwidth setting. For some reason I completely forgot this was implemented this way, I thought the encoder bandwidth only affected the velocity loop. Sorry if I misled you in previous comments!

If you’d like to try closed loop control on shadow_count instead of pos_estimate, you can put that in on line 322 of axis.cpp on RazorsEdge, or line 297 of axis.cpp on master:

} else if (!controller_.update(encoder_.pos_estimate_, encoder_.vel_estimate_, &current_setpoint))

Replace encoder_.pos_estimate_ with encoder_.shadow_count_ and see what happens?

That´s it !

The shadow_counts are the real position values and what matters, especially when multiple axis have to stay in sync. If the deviation of the shadow counts to the setpoint is minimal, the axis are “in sync” as far as possible.

Good and important news first ! You made my day, with mode == 1 ODrive is now able to outperform an open-loop stepper :slight_smile: I only have troubles at lower speeds now again. I guess in total the observer doesn´t give benefit as soon as enough counts/s arrive. I was able to reduce the low-speed ripple with anticogging. Anticogging also works with pos_estimate, should this be changed too ?
Is vel_estimate now based on the shadow_counts too ?

But i am not able to find a setting where mode == 3 doesn’t decrease the performance.

It’s worth trying, I suppose.

vel_estimate_ is the same.

It would be nice to see some new graphs or data, if you have time.

Graph input_pos, pos_setpoint, shadow_count, pos_estimate controlling on pos estimate and on shadow_count to see the difference.

Separately, graph input_vel, vel_setpoint, vel_estimate

…it was my fault.

I did always compare pos_setpoint - shadow_count, which is correct in mode == 1, but in mode == 3 it has to be input_pos - shadow_count !

So INPUT_MODE_POS_FILTER works ! I could improve the performance furthermore !

I use an input_filter_bandwidth of 2500 and inertia of 12e-9. Lowering the bandwidth adds phase shift and high inertia inverse/allpass behavior.

But that is not the end. I would like to increase the PID-frequency to about 12-16 kHz, to bump up the overall bandwidth furthermore. I don´t care about switching efficiency and use FOC only on one axis. What do i have to change ?


Processors :slight_smile:

There isn’t enough processing time remaining on the chip to double the PID rate, sorry. You can do it if you give up one axis, or if you’re willing to do some heavy optimization.

I would just like to test the outcome, one axis is fine, so where to start?