Skip to content

FT Motion ultra smooth pulse trains (silent, better surface quality)#28115

Merged
thinkyhead merged 45 commits intoMarlinFirmware:bugfix-2.1.xfrom
dbuezas:dbuezas/ftmotion-variable-rate-isr
Nov 2, 2025
Merged

FT Motion ultra smooth pulse trains (silent, better surface quality)#28115
thinkyhead merged 45 commits intoMarlinFirmware:bugfix-2.1.xfrom
dbuezas:dbuezas/ftmotion-variable-rate-isr

Conversation

@dbuezas
Copy link
Copy Markdown
Contributor

@dbuezas dbuezas commented Oct 16, 2025

Description

  • Better surface quality
  • Lower ram usage
  • Silent steppers
  • Removes the need for a multiple magic number constants to in the config

This PR:

  • Removes the relatively low frequency and RAM intensive fixed rate stepping ISR for FTMotion. Instead of pre filling a 20 kHz dir/step buffer, a new stepper plan circular buffer is introduced which requires 20+ less times less elements.
  • Removes the double trajectory buffer and command buffer.
  • A steps delta per axis is calculated from each new trajectory point, and converted into a a dividend and step direction per axis, which go into a single buffer. The deltas are calculated in 64 bit fixed pointer arithmetic in Q32.32 format, where the integer part are whole steps and the fractional part the accumulated error (i.e fractional steps). The precise math ensures zero step drift or jitter.
  • On each interrupt, bresenham is used to decide is a step is due, and a division-less algorithm is used to advance bresenham iterations until the next step.
  • All of this results in a single interrupt per step, with a time resolution as high as the stepper timer frequency (2MHz in 32 bit MCUs), and lower total cpu and ram usage.

The smooth pulse train means higher surface quality, more silent steppers, less VFAs and less resonances.
It also makes step timings of each axis independent of the rest, eliminating a limitation of the classic motion system where the slower axes had slower time resolution (jittery rates) than the fastest one.

Below a comparison of the step trains of the classic motion system vs FTMotion before vs FTMotion after

image

(Note that the bands in classic are because of the oversampling from dynamic smooth stepping)

It is also a lot more ram efficient:

Before

RAM:   [==        ]  15.2% (used 88036 bytes from 577536 bytes)
Flash: [====      ]  44.0% (used 403828 bytes from 917504 bytes)

After

RAM:   [=         ]  13.8% (used 79944 bytes from 577536 bytes)
Flash: [====      ]  44.2% (used 405292 bytes from 917504 bytes)

Extras

This is quite a sizeable refactoring, it also:

  • Modularises FTMotion shaping, stepping and smoothing structs in separate files
  • Fixes runout blocks when axis synch is enabled
  • Makes runout block length a lot tighter than before (as small as it can be given the current axes IS frequencies and smoothing times), enabling extremely fast probing and homing.
probing.mp4

Requirements

Benefits

Configurations

Related Issues

@dbuezas dbuezas force-pushed the dbuezas/ftmotion-variable-rate-isr branch from e4f88cb to 29aad5d Compare October 17, 2025 18:40
@dbuezas
Copy link
Copy Markdown
Contributor Author

dbuezas commented Oct 17, 2025

With the latest optimisations, the
isr can run on every single tick of the stepper timer without requiring a command buffer (stm32h7)
This will allow for a lot smoother pulse trains for all axes

@narno2202
Copy link
Copy Markdown
Contributor

I just gave it a try. Motion on each axis is fine. Sensorless homing is broken with FT_MOTION enabled, and it's the same for Z homing with a BLTouch. Still a long way to go...

@dbuezas dbuezas force-pushed the dbuezas/ftmotion-variable-rate-isr branch from 900d0d1 to 64192a2 Compare October 25, 2025 15:00
@narno2202
Copy link
Copy Markdown
Contributor

Compilation fails in SanityCheck.h : static_assert((FTM_BUFFER_SIZE & FTM_BUFFER_MASK) == 0, "FTM_BUFFER_SIZE must be power of two");. I remove the line and ready to test.

@narno2202
Copy link
Copy Markdown
Contributor

Sensorless homing fails, Z homing with BLTouch fails, manual motions are fine.

@thisiskeithb thisiskeithb changed the title Dbuezas/ftmotion variable rate isr FT Motion Variable Rate ISR Oct 25, 2025
@dbuezas
Copy link
Copy Markdown
Contributor Author

dbuezas commented Oct 26, 2025

Sensorless homing fails, Z homing with BLTouch fails,

I'm not considering homing & probing yet. Homing and probing never worked well for me so still on classic for that. I do think that will be easier once this is ready

@dbuezas
Copy link
Copy Markdown
Contributor Author

dbuezas commented Oct 26, 2025

Compilation fails in SanityCheck.h : static_assert((FTM_BUFFER_SIZE & FTM_BUFFER_MASK) == 0, "FTM_BUFFER_SIZE must be power of two");. I remove the line and ready to test.

you should make ftm_buffer_size a power of two , otherwise the buffer may wrap around in a weird way

@narno2202
Copy link
Copy Markdown
Contributor

I use de default Config_adv.h value which is not a power of 2, needs to be changed to avoid error. Regarding your homing and probing problem, I don't have any issue since a very long time with FT_MOTION.

@dbuezas
Copy link
Copy Markdown
Contributor Author

dbuezas commented Oct 26, 2025

I know you don't have issues, but I do, so imo still experimental

I use de default Config_adv.h value which is not a power of 2, needs to be changed to avoid error

true, I have not updated defaults yet. I have it my printer's settings. thanks for reminding me of that. Still, don't use the default iin this branch

@dbuezas
Copy link
Copy Markdown
Contributor Author

dbuezas commented Oct 26, 2025

After robherc pointed out to me that we should do everything possible to never ever leave any chance open of having step drift, I put more thought on the bresenham dividend calculations, and changed it to use integers with fixed point arithmetic and an exact tracking of steps. Actually, not only steps, but fractions of steps.

Also, fixed the runout block in the presence of axis shaping.

@narno2202
Copy link
Copy Markdown
Contributor

Just a little information, during sensorless homing, the axis moves fine, the motor then grinds until the end of the theoretical do_homing_move and the axis is homed. I've only tested for X and Y.

@thinkyhead
Copy link
Copy Markdown
Member

…during sensorless homing…

I have to modify the stall sensitivity when switching to FT Motion generally, but mainly to avoid false positives. Is there any range of sensitivity that causes sensorless homing to stop at or before the end?

@narno2202
Copy link
Copy Markdown
Contributor

My stallguard sensitivity is fine for homing with ou without FT_MOTION enabled in current bugfix. Here, changing sensitivty has no effect.

@thinkyhead thinkyhead force-pushed the dbuezas/ftmotion-variable-rate-isr branch 3 times, most recently from 33a34c4 to e0c008c Compare October 26, 2025 23:36
// FBS / post processing.
if (batchRdy && !batchRdyForInterp) {

// Call Ulendo FBS here.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ulendo themselves may need this hook for their own internal stuff.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Our recent optimizations are also fixing critical motion shortcomings so we hope to move forward and get this thing shipped with these improvements. But this and the updated M493 means more work for you with FBS @ulendoalex . Can you work with these changes?

Copy link
Copy Markdown
Contributor Author

@dbuezas dbuezas Oct 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should involve only adding your buffer between these two calls:

    xyze_float_t traj_coords = calc_traj_point(currentGenerator->getDistanceAtTime(tau));
    traj_coords = add_to_ulendo_buffer_and_return_a_transformed_coord_point
    stepper_plan_t plan = calc_stepper_plan(traj_coords);

and adding an extra waiting time as big as your buffer to the runout.

You'll also benefit from all the time resolution, perf and ram improvements (including ultra clean & silent pulse trains, increased surface quality, reduced resonances, better linear advance, etc).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dunno, the smoothness of this smoothing is now so good it might make FBS obsolete! ;-)

@thinkyhead thinkyhead force-pushed the dbuezas/ftmotion-variable-rate-isr branch 8 times, most recently from dc6c438 to 0b50d4d Compare October 27, 2025 05:46
@thinkyhead thinkyhead force-pushed the dbuezas/ftmotion-variable-rate-isr branch from 0b50d4d to 4667d72 Compare October 27, 2025 05:55
@dbuezas dbuezas force-pushed the dbuezas/ftmotion-variable-rate-isr branch from c70caa1 to 8c66611 Compare October 30, 2025 22:14
@dbuezas
Copy link
Copy Markdown
Contributor Author

dbuezas commented Oct 30, 2025

Had some very nasty divergences between my private branch (the one i trust because it is where I can do the logic analyser data acquisition and test prints) and this one. I lost way to much time trying to reconcile and not trusting it, so I just grabbed everything back from my trusted branch.
This includes some much needed modularisation, where i moved away the step plan logic to its own file, and for homogeneity did the same with smoothing and shaping.

Tomorrow I'll continue testing and if everything goes well I should be able to update the PR description and mark it as ready for review.

Thanks for all the feedback!

@thinkyhead
Copy link
Copy Markdown
Member

I just grabbed everything back from my trusted branch

I have amazing facility in reconciliation with git, so don't hesitate to ask if you want your changes isolated from adjustments and bug fixes that were made while studying the code. In most cases you can just force-push your "trusted" code, and I can use git diff to pick out the optimizations, patches, and documentation comments I added for my own understanding, and either stash them away for later, or apply them in a separate commit to the bugfix branch to keep them out of the PR.

@narno2202
Copy link
Copy Markdown
Contributor

With the last code, when you compile Marlin with FT_MOTION as the default motion mode, this is the sound of silence... nothing moves. You had to switch off and on FT_MOTION (M493 S0 then M493 S1)and then you can move again. the culprit is

#define _DIVIDEND_RECIP(A) advance_dividend_reciprocal.A = UINT32_MAX / stepper_plan.advance_dividend_q0_32;
LOGICAL_AXIS_MAP_LC(_DIVIDEND_RECIP); in  stepping.cpp. 

Changing to previous code works like a charm

advance_dividend_reciprocal.set(LOGICAL_AXIS_ARRAY_1(UINT32_MAX));
advance_dividend_reciprocal /= stepper_plan.advance_dividend_q0_32;

@dbuezas
Copy link
Copy Markdown
Contributor Author

dbuezas commented Oct 31, 2025

Oh, it's missing .A
Thanks will fix today

@dbuezas
Copy link
Copy Markdown
Contributor Author

dbuezas commented Oct 31, 2025

It's bonkers how smooth it runs

@dbuezas
Copy link
Copy Markdown
Contributor Author

dbuezas commented Oct 31, 2025

Surface quality is also unreal

@dbuezas dbuezas marked this pull request as ready for review October 31, 2025 18:17
@dbuezas
Copy link
Copy Markdown
Contributor Author

dbuezas commented Oct 31, 2025

The description is not up to date, i'll try to do it this weekend. done

@dbuezas dbuezas changed the title FT Motion Variable Rate ISR FT Motion ultra smooth pulse trains (silent, better surface quality) Nov 1, 2025
Comment on lines +351 to +358
#define _SET_MOVE_END(A) do{ \
if (moveDist.A) { \
axis_move_end_ti.A = move_end_ti; \
axis_move_dir.A = moveDist.A > 0; \
} \
}while(0);

LOGICAL_AXIS_MAP(_SET_MOVE_END);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@thinkyhead , you can remove PR #28125 and perhaps we could only keep axis_move_dir = current_block->direction_bits.

Suggested change
#define _SET_MOVE_END(A) do{ \
if (moveDist.A) { \
axis_move_end_ti.A = move_end_ti; \
axis_move_dir.A = moveDist.A > 0; \
} \
}while(0);
LOGICAL_AXIS_MAP(_SET_MOVE_END);
axis_move_dir = current_block->direction_bits;
#define _SET_MOVE_END(A) do{ \
if (moveDist.A) { \
axis_move_end_ti.A = move_end_ti; \
} \
}while(0);
LOGICAL_AXIS_MAP(_SET_MOVE_END);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should revise the axis move ti thing completely (not within this pr). Now there is a single buffer so this whole time math is most likely not even needed. We could just set the last moved buffer index when a block is loaded, and clear it when that index is dequeued.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually we could do that from within the queue and dequeue functions. Axis that move have a non zero dividend

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's just temporary unless this can be refactored. I've found a solution in current bugfix but i always loose a ftmotion_stepper() call.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting, for me (with the other probe patch on top) it works as is. Should I apply your patch as is or does it require extra stuff?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For axis_move_dir only the diff above. In the current bugfix, I create an empty command which marks the start of the block. When losing one ftmotion_stepper() call, everything is fine but not when I try to process the next command if the buffer is not empty.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The main thing is to make sure the "is moving" and "current direction" are related to the immediate state, where "is moving" refers to the block currently being stepped.

Strictly speaking, all an endstop check wants to know is, "is the step count on the axis closer to the endstop end than it was the last time I checked?" and the same goes for probes wanting to check if the new position is lower.

There isn't much besides endstop checking that cares about having these states prepared, so if it is possible to refactor endstops and probes to use these counts instead, that would free up a few cycles from the motion code and localize more of the endstop / probe logic in their respective classes.

@narno2202
Copy link
Copy Markdown
Contributor

Just for fun (or upcoming thing) Not so smooth and noiseless

VID_20251101.mp4

@narno2202
Copy link
Copy Markdown
Contributor

You can remove PROPBATCHES definition in ft_motion.h which is no more used :

// Number of batches needed to propagate the current trajectory to the stepper.
static constexpr uint32_t PROP_BATCHES = 1;

@thinkyhead thinkyhead merged commit 1ead60e into MarlinFirmware:bugfix-2.1.x Nov 2, 2025
67 checks passed
thinkyhead added a commit to MarlinFirmware/Configurations that referenced this pull request Nov 2, 2025
@dbuezas dbuezas deleted the dbuezas/ftmotion-variable-rate-isr branch December 16, 2025 10:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants