Implement stepper shaper

Hi, I’m trying to implement an input shaper for stepper linear motion to compensate fullstep/microsteps vibration. I already looked at how the current input shaper works—proxy kinematic and alter movement/position in time before iter solver. But I’m not sure it is possible to do what I want here. For example, there are several motors moving, and I want to do pulses for each one based on their phase. And because I have CoreXY, it simplifies nothing for me.

I currently want tight coupling with a specific stepper and stepper phase, so I added hooks to step compress code, specifically to queue_flush(), because I want to modify step timings a little before compressing. Here they are mapped one to one with steps.

For now, I am working on phase tracking for this, the basic idea looks like this:

static int
queue_flush(struct stepcompress *sc, uint64_t move_clock)
{
    if (sc->queue_pos >= sc->queue_next)
        return 0;
    stepper_shaper(sc->oid, sc->queue_pos, sc->queue_next, sc->last_step_clock, move_clock, sc->sdir);
    while (sc->last_step_clock < move_clock) {
        struct step_move move = compress_bisect_add(sc);
        int ret = check_line(sc, move);
        if (ret)
            return ret;
        add_move(sc, sc->last_step_clock + move.interval, &move);
        if (sc->queue_pos + move.count >= sc->queue_next) {
            uint32_t diff = sc->queue_pos + move.count - sc->queue_next;
            while (diff--)
                stepper_shaper_far(sc->oid, sc->sdir);
            sc->queue_pos = sc->queue_next = sc->queue;
            break;
        }
        sc->queue_pos += move.count;
    }
    calc_last_step_print_time(sc);
    return 0;
}
....
uint32_t diff(uint32_t a, uint32_t b) {
    return (a >= b) ? (a - b) : (UINT32_MAX - b + a + 1);
}

void stepper_shaper(uint32_t oid, uint32_t *queue_pos, uint32_t *queue_next, uint64_t last_step_clock, uint64_t move_clock, int sdir)
{
    uint32_t *qlast = queue_next;
    uint32_t *pos = queue_pos;
    struct tracker *stepper = &steppers[oid];
    int msteps = (sdir) ? -stepper->msteps : stepper->msteps;
    while (pos < qlast && last_step_clock < move_clock) {
        stepper->mscnt = (stepper->mscnt + msteps) % 1024;
        last_step_clock += diff(*(pos+1), *pos);
        pos++;
    }
}

void stepper_shaper_far(uint32_t oid, int sdir) {
    struct tracker *stepper = &steppers[oid];
    int msteps = (sdir) ? -stepper->msteps : stepper->msteps;
    stepper->mscnt = (stepper->mscnt + msteps) % 1024;
}

But there is something off (it works for short moves and doesn’t for long), it does not work for long movements and I think I misunderstood some crucial parts of the code here, like time conversion, to track flushed steps reliably.

I would appreciate it if anyone had any ideas on what I’m doing wrong or maybe how to do it properly.

Thanks.

There is corner case, this is a reason why my implementation get out of sync.
compress_bisect_add() can compress past move_clock, and it is will still well below sc->queue_next.
In that specific case I miss N steps flushed from the queue.
So, more or less proper handling for me will be:

static int
queue_flush(struct stepcompress *sc, uint64_t move_clock)
{
    if (sc->queue_pos >= sc->queue_next)
        return 0;
    uint32_t *shaper_pos = stepper_shaper(sc->oid, sc->queue_pos, sc->queue_next, sc->last_step_clock, move_clock, sc->sdir);
    while (sc->last_step_clock < move_clock) {
        struct step_move move = compress_bisect_add(sc);
        int ret = check_line(sc, move);
        if (ret)
            return ret;
        add_move(sc, sc->last_step_clock + move.interval, &move);
        // compress bisect can compress more than move_clock, but less then or equal to queue_next
        while (shaper_pos < sc->queue_pos + move.count) {
            stepper_shaper_far(sc->oid, sc->sdir);
            shaper_pos++;
        }
        if (sc->queue_pos + move.count >= sc->queue_next) {
            uint32_t diff = sc->queue_pos + move.count - sc->queue_next;
            // not sure if it ever happens
            while (diff--)
                stepper_shaper_far(sc->oid, sc->sdir);
            sc->queue_pos = sc->queue_next = sc->queue;
            break;
        }
        sc->queue_pos += move.count;
    }
    calc_last_step_print_time(sc);
    return 0;
}
1 Like

Interesting. For what it is worth, I’m surprised you’d want to alter the step compression code. I suspect making changes at the kinematics or iterative solver layer would be easier.

The input shaper code is a little complex, because it is altering the stepper timing based on the requested x and y positions. However, what you’re trying to do (I think) is a little easier - you want to alter the stepper motor step timing based on the stepper motor position.

At a high-level the kinematic code is given a movement plan (trapq) and time which it then needs to return the stepper position that the stepper should be at given that time. It should be possible for a wrapper kinematic class to take that time, call the actual stepper kinematic class to determine the nominal position, and then return some other position based on the desired alterations.

Just thinking out loud.
-Kevin

1 Like

I’m thinking that stealth compensation (TMC StealthChop Rotor Positional Lag and Compensation) would be easier than the motor phase compensation, for me, and now at least.

Basic idea: the motor’s rotor lag is predictable, lag <90 degrees/fullstep.
This stepper in the kinematic, at any point in time, would be in the past position.

Basically, calc_position_cb should return to the itersolver the position at the T-motor_phase_lag (200us..2ms), in the past.

But do to that, I need to know the instant speed at the requested time, for the stepper.
To calculate the speed of the rotor.
(specifically, speed of the phase change, so precise time between steps is enough, but it would be available only at the output of the itersolver).

So, I’m trying to implement a kinematics wrapper: GitHub - nefelim4ag/klipper at stepper-shaper-20250503 - it is boilerplate, which only wraps.
There are a lot of caveats. For example, if I wrap IS kinematics → step compress error, but if IS wraps me, well, then I do not have access to speed/time relations.

Right now, I have a slightly different issue.
I simply want to determine the stepper speed at a given moment of time without IS:

inline double
move_get_speed(struct move *m, double move_time)
{
    return m->start_v + 2 * m->half_accel * move_time;
}

static double
shaper_calc_position(struct stepper_kinematics *sk, struct move *m, double move_time)
{
    struct stepper_shaper *ss = container_of(sk, struct stepper_shaper, sk);
    double cur = ss->orig_sk->calc_position_cb(ss->orig_sk, m, move_time);
    double next = ss->orig_sk->calc_position_cb(ss->orig_sk, m, move_time + 0.00001);
    double diff = next - cur;
    double speed = diff / 0.000001;
    double speed2 = move_get_speed(m, move_time);
    fprintf(stderr, "diff: %f, speed: %f, speed2: %f\n", diff, speed, speed2);

    if (ss->motor_constant == .0)
        return ss->orig_sk->calc_position_cb(ss->orig_sk, m, move_time);

}

The problem is, I’m not sure that it is a stepper speed here on a CoreXY.

// G0 X10 F120 ; expect 2 mm/s
diff: 0.000020, speed: 20.000000, speed2: 2.000000
diff: 0.000020, speed: 20.000000, speed2: 2.000000
diff: 0.000020, speed: 20.000000, speed2: 2.000000
diff: 0.000020, speed: 20.000000, speed2: 2.000000
INFO:root:Exiting (print time 105.015s)
diff: 0.000020, speed: 20.000000, speed2: 2.000000
diff: 0.000020, speed: 20.000000, speed2: 2.000000
diff: 0.000020, speed: 20.000000, speed2: 2.000000
diff: 0.000020, speed: 20.000000, speed2: 2.000000
diff: 0.000020, speed: 20.000000, speed2: 2.000000

// G0 X10 Y10 F120 ; expect 2 / 0.707 ~ 2.82 mm/s
diff: -0.000019, speed: -19.380281, speed2: 2.000000
diff: -0.000019, speed: -19.380280, speed2: 2.000000
diff: -0.000019, speed: -19.380280, speed2: 2.000000
diff: -0.000019, speed: -19.380280, speed2: 2.000000
INFO:root:Exiting (print time 295.606s)
diff: -0.000019, speed: -19.380280, speed2: 2.000000
diff: -0.000019, speed: -19.380281, speed2: 2.000000
diff: -0.000019, speed: -19.380281, speed2: 2.000000
diff: -0.000019, speed: -19.380280, speed2: 2.000000

Is there a good way to determine the instant speed of the stepper motor?

Thanks.

The only reliable way I know of to calculate the instantaneous velocity of a stepper is to look ahead/behind in your calc_position() implementation as you are doing.

You have move_time + 0.00001 and diff / 0.000001 - that typo is the cause of your 10x error.

I’m not sure why you’ve got a different from expected value for your second move. Make sure you are properly setting the sk.gen_steps_pre_active and sk.gen_steps_post_active times, and make sure you are properly propagating those times to the toolhead class.

Hope that helps a little,
-Kevin

2 Likes

You are right, thank you, that works for speed calculations.
I fooled myself with G-code and batch mode.

I’m trying to debug stepcompress errors without IS (I should fix my code first, and only then touch the code above).

ERROR:root:b'stepcompress o=0 i=0 c=3 a=0: Invalid sequence'
tstep: 0 // fprintf(stderr, "tstep: %i\n", *(sc->queue_pos - 2));
tstep: 634416823 // fprintf(stderr, "tstep: %i\n", *(sc->queue_pos - 1));
tstep: 634416823 // fprintf(stderr, "tstep: %i\n", *(sc->queue_pos));
tstep: 634416823 // fprintf(stderr, "tstep: %i\n", *(sc->queue_pos + 1));
tstep: 634416823 // fprintf(stderr, "tstep: %i\n", *(sc->queue_pos + 2));
tstep: 634427330 // fprintf(stderr, "tstep: %i\n", *(sc->queue_pos + 3));
tstep: 634451346 // fprintf(stderr, "tstep: %i\n", *(sc->queue_pos + 4));

I suspect this is a place where it happens:

---
move_time: 0.021217310134161796, speed: -0.63249672166421078,	pos: 356.35000000000002	pos_corrected: 356.34877929687502, dlag: -0.001220703125
itersolve_gen_steps_range():103 stepcompress_commit
move_time: 0.0001, speed: 6.506127192551503,	pos: 356.35065061245837	pos_corrected: 356.36334592495837, dlag: 0.0126953125
itersolve_gen_steps_range():89 dir change: rel_dist: -0.014908425761
move_time: 1.1707692878774969e-05, speed: 6.506127192551503,	pos: 356.35007617170845	pos_corrected: 356.36277148420845, dlag: 0.0126953125
...
move_time: 2.8583234567321703e-09, speed: 6.5061840359703638,	pos: 356.35000001859657	pos_corrected: 356.36269533109657, dlag: 0.0126953125
move_time: 1.4291617283660851e-09, speed: 6.506127192551503,	pos: 356.3500000092983	pos_corrected: 356.3626953217983, dlag: 0.0126953125
move_time: 7.1458086418304257e-10, speed: 6.506127192551503,	pos: 356.35000000464913	pos_corrected: 356.36269531714913, dlag: 0.0126953125
itersolve_gen_steps_range():111 -> stepcompress_append() sdir: 1 print_time: 98.58768238, guess.time: 7.1458086418304257e-10
stepcompress_append():525 step_clock: 4929384119
---
move_time: 1.7864521604576063e-09, speed: 6.5060703491326421,	pos: 356.35000001162291	pos_corrected: 356.36269532412291, dlag: 0.0126953125
itersolve_gen_steps_range():103 stepcompress_commit
move_time: 1.2505165123203244e-09, speed: 6.506127192551503,	pos: 356.35000000813602	pos_corrected: 356.36269532063602, dlag: 0.0126953125
itersolve_gen_steps_range():111 -> stepcompress_append() sdir: 1 print_time: 98.58768238, guess.time: 1.2505165123203244e-09
stepcompress_append():525 step_clock: 4929384119
---
move_time: 2.2505165123203245e-09, speed: 6.506127192551503,	pos: 356.35000001464215	pos_corrected: 356.36269532714215, dlag: 0.0126953125
itersolve_gen_steps_range():111 -> stepcompress_append() sdir: 1 print_time: 98.58768238, guess.time: 2.2505165123203245e-09
stepcompress_append():525 step_clock: 4929384119
---
move_time: 3.7505165123203247e-09, speed: 6.5061840359703638,	pos: 356.35000002440131	pos_corrected: 356.36269533690131, dlag: 0.0126953125
itersolve_gen_steps_range():103 stepcompress_commit
move_time: 3.0005165123203246e-09, speed: 6.5060703491326421,	pos: 356.35000001952176	pos_corrected: 356.36269533202176, dlag: 0.0126953125
itersolve_gen_steps_range():111 -> stepcompress_append() sdir: 1 print_time: 98.58768238, guess.time: 3.0005165123203246e-09
stepcompress_append():525 step_clock: 4929384119
---

So, 4 identical step_clocks got committed.
For me, it seems like a point where the stepper slows down for a kinematic angle traversal.
Initially, I assumed my correction was off, so speed jumps → lag jumps → position jumps.
I tried to limit the correction precision to avoid forward/backward jumps, but it seems like there is a different reason for this issue.
(In the log dlag is constant)

// enforce stable distance correction
dist_lag = roundf(dist_lag * 8192) / 8192;

Maybe I’m missing something?
Such small move_time increments probably would produce the same step clock in the output.
But why are they so small? I cannot get it.
Batch tests done over the Linux MCU dictionary, so there is 50MHz timer precision.
So, a 1 ns difference would probably produce the same clock.

I basically do underneath distance correction, instead of previous time correction
double res = ss->orig_sk->calc_position_cb(ss->orig_sk, m, move_time);
....
// speed -> radians lag -> degrees lag -> distance lag
double dist_lag = ss->fstep_dist * sin(degrees * M_PI / 180);
double res_cor = res + dist_lag;
fprintf(stderr, "move_time: %.17g, speed: %.17g, pos: %.17g pos_corrected: %.17g, dlag: %.17g\n", move_time, speed, res, res_cor, dist_lag);
return res_cor;

Thanks in advance.


Hmmm.. seems like there should be a dir change, but it does not happen.
pos decreases and then increases back.

I’m not sure. You could try graphing the results with motan using something like:

  1. Temporarily disable stepcompress checks by setting CHECK_LINES to zero in klippy/chelper/stepcompress.c .
  2. Run the host code with webhooks enabled, but output forwarded to a file (eg, python klippy.py ... -o myfile.data -d out/klipper.dict). See the Debugging.md document if you are not familiar with this.
  3. Attach the motan client to the webhooks api.
  4. Run some sample g-code by sending it into the pseudo-tty (eg, cat myprint.gcode >> /tmp/printer

Then graph the resulting motan data and look for infinite velocity spikes.

-Kevin

1 Like

FWIW, if you’re ultimately trying to alter the position by using an instantaneous velocity then I think you’ll need to be careful to avoid instantaneous position changes. Basically, if square_corner_velocity is above zero (which it almost always is) then the steppers can be requested to have abrupt instantaneous velocity changes. Thus, if you’re multiplying the position by that, it can result in instantaneous position changes (which the steppers can’t obtain).

This was the same issue we had when we updated pressure advance so that we could support an instantaneous_corner_velocity config setting (ie, “smooth pressure advance”). There were long discussions on this in the PRs Smoothed pressure advance and extruder cornering support by KevinOConnor · Pull Request #1997 · Klipper3d/klipper · GitHub and Convert pressure advance to "weighted average smoothing" by KevinOConnor · Pull Request #2275 · Klipper3d/klipper · GitHub ).

So, if you’re looking to do something with instantaneous velocity, then you may want to look at the pressure advance code (klippy/chelper/kin_extruder.c) as that code may be closer to what you want to do than the input shaper code. That is, you may want to look at integrating over the position to obtain a smoothed velocity instead of directly trying to calculate a velocity.

Hope that helps,
-Kevin

2 Likes

@nefelim4ag As per offline chat, I’ve put together a proof-of-concept stepper shaper code that uses your math to compensate stealthchop lag, but instead of fiddling with stepcompress or MCU code, it just performs compensation in the C kinematics code, so please take a look and give it a try. Please note that I only did some very basic checks (like, just running an added regression test and validating some internally calculated parameters), so exercise caution. It is also possible that I’ve messed up some calculations somewhere, ranging from using the wrong sign somewhere to perhaps computing some offset values incorrectly. But otherwise you should be able to add [stepper_shaper stepper_?] to your config and run a command like SET_STEPPER_SHAPER STEPPER=<stepper> STEALTHCHOP_COMP=<L/R>, which would suffice for testing for now, I think.

Edit: the velocity calculation code was added only to some kinematics, but it does cover cartesian, corexy, hybrid_corexy, generic_cartesian, and also IDEX and input shaper, so I hope that suffices for testing for now.

1 Like

JFYI:
You were right about SQV and speed jumps.
I was not very successful in writing a similar weighted average smoothing.
I put a 5-point stencil to achieve similar results.

So, snapshot:

[stepper_shaper stepper_y]
shaper: stealthchop
inductance_mh: 1.5
resistance_ohm: 1.2

I did get some info from another machine with steppers and encoders and a different motor, but alas, it also had low inductance (Trinamic).
At least I could say, it does not overfit to my motors :D.


Thank you!

I did brief tests, something seems a bit off:


stealth_compensated_dmb.zip (4.1 MB)

# I reconfigured my machine as Cartesian to do such tests
./scripts/motan/motan_graph.py -g '[["deviation(angle(mt6826s),stepq(stepper_x))"],["deviation(angle(mt6826s),trapq(toolhead,x))"],["derivative(stepq(stepper_x))"],["derivative(stepq(stepper_y))"],["step_phase(tmc5160 stepper_x)"],["step_phase(tmc5160 stepper_y)"]]' ./stealth_compensated_dmb -s 1 -d 30

Also, I peeked at the code a little.
This seems similar to the old version, where I try to account for the time and return the kinematics state at T before the current.
From my small sample of 2 machines, it seems like lag is under the hood also has a sinusoidal nature:

double res = ss->orig_sk->calc_position_cb(ss->orig_sk, m, move_time);
// could be positive or negative
double speed = guess_speed_smooth(ss, m, move_time, res);
double phase_speed = speed / ss->phase_dist;
double omega = M_2PI * phase_speed;
double rads = atan(omega * ss->motor_constant);
double dist_lag = ss->fstep_dist * sin(rads);
double res_cor = res + dist_lag;
return res_cor;

So, looks like the real curve is a little bit steeper than the time-based compensation.

My code most certainly uses that ‘old’ version which was mentioned in the other thread. However, it should be possible to fairly easily adapt to a new calculation. I can get to that later, or you can try updating it yourself if you feel like it.

Also, can you explain what is the difference in the motan topmost and the next graph? The titles and the line descriptions are exactly the same, yet the data is different.

Separately, I now realized that I missed the part of the discussion about instantaneous stepper velocity jumps at corners, and corresponding position jumps after compensation. So I did not implement the averaging or smoothing there, and as such, my code is prone to stepcompress errors ATM. Though it seems that for now the issues can be mitigated by using not too fine microstepping (e.g. 128 microsteps, scv=5 is sufficient for the H=1.5mH and R=1.2Ohm to not trigger stepcompress errors). I’ll see what can be done here, but at first, ideally the approach and the compensation model needs to be confirmed.

I will try to do local sketches to at least be familiar with it, ty.

Because the IS/Stepper shaper does alter the stepq, I have no way to measure the difference before/after in a simple way.
["deviation(angle(mt6826s),stepq(stepper_x))"]
So, trick there is to trick printer into cartesian, and do basically the same (shake head at some velocity range).
But, compare encoder data not against the stepq, but against the trapq
["deviation(angle(mt6826s),trapq(toolhead,x))"]

So, there are 2 identically named graphs, but the upper one is raw stepper lag (we could not cancel it). The second one is the actual kinematic lag, which we could cancel.
And the left one is because my helper just wants to have it, and I left it to show that, under normal circumstances, trapq/stepq data are almost the same.

(if there is a way to do the same with CoreXY and motan_graphs it would be nice, but for now, this is the solution that I’ve come up with :D)

Does it clarify something?

Partially.

I think you could do along the lines

["deviation(angle(mt6826s),kin(stepper_x))"]

which would infer the expected stepper position from the corresponding trapq data, and compare the measured angle data against it, if I understand correctly.
Edit: similarly, I’d expect

["deviation(stepq(stepper_x),kin(stepper_x))"]

to show the stepper position compensation effects.

I’m a bit unsure I understood it, specifically ‘raw stepper lag’ part. Do I understand correctly that the top of the two charts correspond to ["deviation(angle(mt6826s),stepq(stepper_x))"], which includes compensation from the stepper shaper, and the bottom one - effectively ["deviation(angle(mt6826s),kin(stepper_x))"] - the deviation against the expected stepper position?

1 Like

:scream: I will check that :smiley:

Yes, you are correct.


btw, you are doing compensation 2 times, this is why there was overcompensation.

patch
@@ -56,8 +59,7 @@ stepcorr_calc_position(struct stepper_kinematics *sk, struct move *m
     move_time += calc_time_lag(sc, stepper_velocity);
     m = find_move_at_time(m, &move_time);
     double stepper_position = sk->calc_position_cb(sk, m, move_time);
-    return stepper_position + calc_position_lag(
-            sc, sk->calc_velocity_cb(sk, m, move_time));
+    return stepper_position; // + calc_position_lag(sc, sk->calc_velocity_cb(sk, m, move_time))
 }
 
 inline void
With patch, it looks like it was with just time compensation

Thanks again!



Yes, kin() works! Awesome!
["deviation(angle(mt6826s),kin(stepper_x))"]

./scripts/motan/motan_graph.py -g '[["deviation(angle(mt6826s),stepq(stepper_x))"],["deviation(angle(mt6826s),kin(stepper_x))"],["derivative(stepq(stepper_x))"],["derivative(stepq(stepper_y))"],["step_phase(tmc5160 stepper_x)"],["step_phase(tmc5160 stepper_y)"]]' ./stealth_compensated_dmb_v2 -s 11 -d 30

sin() patch
diff --git a/klippy/chelper/stepcorr.c b/klippy/chelper/stepcorr.c
index c6285f051..635db587c 100644
--- a/klippy/chelper/stepcorr.c
+++ b/klippy/chelper/stepcorr.c
@@ -27,7 +27,9 @@ calc_position_lag(struct stepper_corrections *sc, double velocity)
 {
     double omega = sc->rad_per_mm * velocity;
     double phi = atan(omega * sc->motor_lag_const);
-    return phi / sc->rad_per_mm;
+    double dist_lag = sc->fstep_dist * sin(phi);
+    double res_cor = dist_lag;
+    return dist_lag;
 }
 
 static inline struct move*
@@ -53,8 +55,6 @@ stepcorr_calc_position(struct stepper_kinematics *sk, struct move *m
         return sk->calc_position_cb(sk, m, move_time);
 
     double stepper_velocity = sk->calc_velocity_cb(sk, m, move_time);
-    move_time += calc_time_lag(sc, stepper_velocity);
-    m = find_move_at_time(m, &move_time);
     double stepper_position = sk->calc_position_cb(sk, m, move_time);
     return stepper_position + calc_position_lag(
             sc, sk->calc_velocity_cb(sk, m, move_time));
Left is time, right is positional compensation


So, aside from initial x2 compensation, overall math looks correct for me.
stealth_compensated_dmb_v3.zip (5.5 MB)
(without compensation)
stealth.zip (5.4 MB)

OK, that sounds good, but do you have any intuition behind the updated position compensation formula? FWIW, implementing a stepper shaper that only shifts the position, but does not alter the time is much easier and less involved. I need to see, but perhaps even the smoothing of velocity can be implemented directly then.

Honestly, I’m not completely sure.
I noticed that time-based calculation does not perfectly fit the data.
I assumed maybe I missed something, idk, sort of motor electromagnetic inertia?
I just guessed that the position has a sort of sinusoidal relationship, because of how microstepping works:


It feels confusing because I assumed that the time-based formula should account for it.
Because it basically calculates the lag based on the sinusoidal voltage curve.

Regardless, I added sin(rads), created 2 graphs rads/time, and just do random sampling by hand:
Like if I see the N lag at motor velocity V in dump, I could convert it to RPS and draw imaginary lines to the lag at those RPS.
With sin(rads), it just fits with much less error.

That is my intuition, not like I can comprehensively explain why it is like that, and why the time formulas are undercompensating.
This is my explanation in hindsight.
So, probably a time-based formula provides “linear” positional lag estimation.
With account for the sinusoidal current form, we just replace the linear relationship with a sinusoidal one.

@nefelim4ag

BTW, I’ve implemented the calculation of the smoothed velocity, such as to avoid velocity and corresponding position jumps at corners caused by non-zero square corner velocity, which causes the stepper motors to change their motion abruptly at corners. I’ve also eliminated the double position correction; the code pushed uses time lag compensation, but I’ve left the code to do also position compensation, including that sin(rads) hack that perhaps overcompensates the position shift.

1 Like

I did check for my sin(rads) hack.
Well, it is overcompensated.
I removed the pulley from the motor.
There are graphs

Spread vs Stealth (time compensated)

So, time-based compensation is a pretty close approximation, slightly undercompensating.

Stealth dist vs Stealth time

So, my hack in reality probably just compensates for mechanical motor load.


Also, I invested some time in stealth/spread switching.
Well, I do not have a good solution.
I did try to invent a smooth transition function there.

static inline double
calc_spread_stealth_switch(double velocity, double min, double max)
{
    velocity = fabs(velocity);
    double t = 2.0 * (velocity - min) / (max - min) - 1.0;
    return 1.0 - (0.5 * (t / sqrtf(1.0 + t * t)) + 0.5);
}

...
    double scale = calc_spread_stealth_switch(stepper_velocity, 100.0, 106.25);
    move_time += calc_time_lag(sc, stepper_velocity) * scale;
...

Predicting the switching is tricky; we can, but I feel it error error-prone.
The basic issue is that it is by default at V/16*17 to switch the spread. and at V to switch to stealth.
It could be reduced on SPI TMC drivers to 1/32, but I’m not sure if it is better from the smoothing point.
So, at 100 mm/s the second threshold is ~100/16*17 = 106.25 mm/s

In the simplest case, where I just do the smooth transition to 0 compensation.

It is actually not so smooth.

If I widen the min/max a little, like 95..112

It is comparable, but it is hacky.
Also, under normal circumstances, there is no acceleration jerk while decelerating the toolhead.
My solution introduced it.
While accelerating the toolhead, it is comparable to the original jerk.

I’m not sure how to properly approach it for now.

All my solutions feel hacky.
Like in the simplified case, there should be 2 transitions.
While approaching the min speed threshold, I can reduce the compensation in half.
It will introduce some error, but will smooth out the transition for min and max.

Or it is possible to do some heuristics to predict if the switch will happen,
If it is in the following 1 ms, then we enable the transition code.
But I cannot imagine the short, simple solution here.
It will introduce the internal shaper state between calls.
Feels wrong.

I think it is not feasible to try to cancel or handle a switch jerk from here.

I did come up with something like dynamic compensation
static inline double
smooth(double v, double min, double max)
{
    double t = 2.0 * (v - min) / (max - min) - 1.0;
    return 1.0 - (0.5 * (t / sqrtf(1.0 + t * t)) + 0.5);
}

static inline double
scale_switch(struct stepper_kinematics* sk, double v)
{
    struct stepper_corrections* sc = &sk->step_corr;
    double min = sc->switch_min;
    double max = sc->switch_max;
    double offset = sc->jerk_offset;
    double diff = max - min;
    v = fabs(v);
    min += sc->jerk_offset;
    max += sc->jerk_offset;
    if (v > max + diff*2 && sc->jerk_offset >= -diff)
        sc->jerk_offset = -diff * 1.5;
    else if (v < min - diff*2 && sc->jerk_offset <= diff)
        sc->jerk_offset = diff * 1.5;
    return 0.5 + 0.5 * smooth(v, min-diff*0.5, max+diff*0.5);
}

....
    double tlag = calc_time_lag(sc, velocity);
    double scale = scale_switch(sk, velocity);
    move_time += tlag * scale;
    m = find_move_at_time(m, &move_time);
    return sk->calc_position_cb(sk, m, move_time);

But the thing is, the more I try to catch the moment of switching, the more it shifts.
So, I may suggest ignoring it for now.

For TMC2240, it would be enough to just clamp the Stealth compensation at the switching speed.

double velocity = sk->calc_smoothed_velocity_cb(sk, m, move_time, sc->hst);
if (velocity > sc->switch_max)
    velocity = sc->switch_max;
double tlag = calc_time_lag(sc, velocity);

Because switching is jerkless and jerkless is enabled by default, and internally freezes the lag degree.
On others, I fear it is not stable enough to be useful.
Like, I think, a stepper velocity limit (instead of kinematics) to avoid switching, can be more useful than trying to work around jerk, and avoiding touching the Stealth EM resonance range.


So, I guess there are no more suggestions for code on my side.
Nor do I have an idea of what else I can test, except for just doing some printing.

Thanks.