Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Planner trapezoidal nominal_rate fix #26881

Open
wants to merge 26 commits into
base: bugfix-2.1.x
Choose a base branch
from

Conversation

HoverClub
Copy link
Contributor

@HoverClub HoverClub commented Mar 17, 2024

Fix nominal_rate range in planner

In Planner::calculate_trapezoid_for_block(), block->nominal_rate could be lower than MINIMAL_STEP_RATE on entry. this produces invalid acceleration results as it attempts to match the exit rate from the previous block. The initial_rate and final_rate are NOLESS'ed to MINIMAL_STEP_RATE at entry. However, block->nominal_rate isn't so you get some weird trapezoid outputs. The stepper fixes the worst case issues but the bad data causes the laser trap calculation to produce wrong results.

This happens when the feed rate is set lower than MINIMAL_STEP_RATE . MINIMAL_STEP_RATE is set to 120 (a #define in the middle of planner.cpp) - I'm not sure how this number was derived? I've changed this value to be calculated which should give better results - unless there is some reason, other than isr counter overflow, for the slowest step rate to be limited?

Requirements

#define LASER_POWER_TRAP, #define LASER_FEATURE

Run this Gcode:

G21
G90
G91
g1y10s0 F10 ; reduce this until if fails
g1y10s10
g1y10s20
g1y10s30

Planner output:
(ir = initial_rate, nr = nominal_rate, fr = final_rate)
=> ir:120 nr:45 fr:120
==> step acc:0->4294967293 decel: 270->266
==> ir:120 nr:45 fr:120
==> step acc:0->4294967293 decel: 270->266

==> ir:120 nr:45 fr:120
==> step acc:0->4294967293 decel: 271->267
==> ir:120 nr:45 fr:120
==> step acc:0->4294967293 decel: 271->267

==> ir:120 nr:45 fr:120
==> step acc:0->4294967293 decel: 270->266
==> ir:120 nr:45 fr:120
==> step acc:0->4294967293 decel: 270->266

==> ir:120 nr:45 fr:120
==> step acc:0->4294967293 decel: 271->267

If the planner `entry_rate` or `final_rate` are larger thanthe  `block->nominal_rate` then the trapezoid entry ramp continuously accelerates. Only happens if feed rate is less than MAXIMAL_STEP_RATE.
removed 
#define MINIMAL_STEP_RATE 120
Moved MINIMAL_STEP_RATE to this file.
Added calc for MINIMAL_STEP_RATE
fix minimal_step_rate calc
@sjasonsmith
Copy link
Contributor

How did you discover this? I've seen some weird acceleration related to that 120 Hz rate in the past when I was capturing analyzer traces of movements (for real or in the simulator), but I don't remember whether those issues were fixed with some of other other step timing issues that @descipher and/or @tombrazier resolved a while back.

@HoverClub
Copy link
Contributor Author

HoverClub commented Apr 7, 2024

I found it initially when using a puny laser that needed to move slowly to cut - it's easy to visually see over/under burn (related to step rate) with a laser.

The code decelerates on entry from MINIMAL_STEP_RATE to the slow target rate then accelerates on exit from the slower rate to MINIMAL_STEP_RATE for each move - resulting in a sawtooth trap shape.

I'm still unsure what the function of MINIMAL_STEP_RATE is - there must have be a reason why it's there (other than ISR counter overflow)?
#20150
#107

@tombrazier
Copy link
Contributor

Great work tracking this down. The comment in the code says MINIMAL_STEP_RATE is limited to prevent timer overflow.

  // Limit minimal step rate (Otherwise the timer will overflow.)
  NOLESS(initial_rate, uint32_t(MINIMAL_STEP_RATE));
  NOLESS(final_rate, uint32_t(MINIMAL_STEP_RATE));

The comment goes back to Eric's original commit of the source code in 2011. Back then the minimum was 32Hz. Three days later he changed it to 120Hz in a commit with comment "Fixed lookup table bug." and it's been the same ever since. Along with this code there was also a line in a different place that limited block->nominal_rate to >= 32 (and later 120). This line disappeared in commit 65934ee, comment "A lot of changes in the planner code". This commit also introduced MINIMUM_PLANNER_SPEED which was used for planning as the minimum speed the planner would use for the start or end of a trapezoidal profile. Much later, I improved on MINIMUM_PLANNER_SPEED in #26198. I think commit 65934ee probably missed a trick because as far as I can tell the introduction of MINIMUM_PLANNER_SPEED did not make up for the removal of the lower bound on block->nominal_rate.

The stepper timer has always been 2MHz for 16MHz ATmega2560 which, with a 16 bit timer, means timer/counter 1 will overflow at frequencies below 30.5Hz. So that's where 32Hz comes from. However, calc_timer() (now calc_timer_interval()) has always protected against timer overflows so even in the original code the 32Hz limit seems to have been unnecessary. And I do not understand why there was a the change to 120Hz. The lookup table (for calculating step timing) has always been able to handle any step rate.

120Hz is pretty slow for 3D printing equating to 1.5mm/s for X and Y on a 80 steps/mm printer. So I'm guessing this is why this has not been spotted and explored before.

Anyway, it is not clear to me that there is any need for the MINIMAL_STEP_RATE lower bounds. I would be interested to know what would happen if they were just removed entirely.

@tombrazier
Copy link
Contributor

On a related note, for the use case described with a laser that required really slow movement it might make sense to increase the microstepping level so that step rates don't get so low in the first place. Not a solution to the bug but likely a good idea if you're approaching the minimum rate the stepper interrupt can run at.

Comment on lines 800 to 810
// Limit minimal step rate (Otherwise the timer will overflow.)
NOLESS(initial_rate, uint32_t(MINIMAL_STEP_RATE));
NOLESS(final_rate, uint32_t(MINIMAL_STEP_RATE));

Copy link
Contributor

Choose a reason for hiding this comment

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

@tombrazier in the latest diff MINMAL_STEP_RATE and these limits are removed entirely.

Are you confident that this is a correct thing to do, and that Otherwise the timer will overflow is just an obsolete relic?

Copy link
Contributor

Choose a reason for hiding this comment

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

I imagine we should have some testing on this in its final form before merging it.

Copy link

@GelidusResearch GelidusResearch Apr 7, 2024

Choose a reason for hiding this comment

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

I believe there is likely a need to limit the minimum with AVR HAL hardware timers e.g. ATM2560 etc.
see https://github.com/MarlinFirmware/Marlin/blob/bugfix-2.1.x/Marlin/src/HAL/AVR/timers.h
Don't think you can remove them entirely.
regards @descipher

#define TEMP_TIMER_FREQUENCY    (((F_CPU) + 0x2000) / 0x4000)

#define STEPPER_TIMER_RATE      HAL_TIMER_RATE
#define STEPPER_TIMER_PRESCALE  8
#define STEPPER_TIMER_TICKS_PER_US ((STEPPER_TIMER_RATE) / 1000000)

#define PULSE_TIMER_RATE         STEPPER_TIMER_RATE
#define PULSE_TIMER_PRESCALE     STEPPER_TIMER_PRESCALE
#define PULSE_TIMER_TICKS_PER_US STEPPER_TIMER_TICKS_PER_US

#define ENABLE_STEPPER_DRIVER_INTERRUPT()  SBI(TIMSK1, OCIE1A)
#define DISABLE_STEPPER_DRIVER_INTERRUPT() CBI(TIMSK1, OCIE1A)
#define STEPPER_ISR_ENABLED()             TEST(TIMSK1, OCIE1A)

#define ENABLE_TEMPERATURE_INTERRUPT()     SBI(TIMSK0, OCIE0A)
#define DISABLE_TEMPERATURE_INTERRUPT()    CBI(TIMSK0, OCIE0A)
#define TEMPERATURE_ISR_ENABLED()         TEST(TIMSK0, OCIE0A)

FORCE_INLINE void HAL_timer_start(const uint8_t timer_num, const uint32_t) {
  switch (timer_num) {
    case MF_TIMER_STEP:
      // waveform generation = 0100 = CTC
      SET_WGM(1, CTC_OCRnA);

      // output mode = 00 (disconnected)
      SET_COMA(1, NORMAL);

      // Set the timer pre-scaler
      // Generally we use a divider of 8, resulting in a 2MHz timer
      // frequency on a 16MHz MCU. If you are going to change this, be
      // sure to regenerate speed_lookuptable.h with
      // create_speed_lookuptable.py
      SET_CS(1, PRESCALER_8);  //  CS 2 = 1/8 prescaler

      // Init Stepper ISR to 122 Hz for quick starting
      // (F_CPU) / (STEPPER_TIMER_PRESCALE) / frequency
      OCR1A = 0x4000;
      TCNT1 = 0;
      break;

    case MF_TIMER_TEMP:
      // Use timer0 for temperature measurement
      // Interleave temperature interrupt with millies interrupt
      OCR0A = 128;
      break;
  }
}

Copy link
Contributor

Choose a reason for hiding this comment

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

@tombrazier in the latest diff MINMAL_STEP_RATE and these limits are removed entirely.

Are you confident that this is a correct thing to do, and that Otherwise the timer will overflow is just an obsolete relic?

Not confident enough to merge it without testing. I am fairly sure it is redundant but it needs to be tried.

@sjasonsmith
Copy link
Contributor

sjasonsmith commented Apr 7, 2024

I found it initially when using a puny laser that needed to move slowly to cut - it's easy to visually see over/under burn (related to step rate) with a laser.

This feels very similar to something we fixed in the past, but perhaps it's a slightly different scenario. We might have fixed long pulses when starting from a standstill, versus yours transitioning form block to block.

Here is the link to the old bug with a bunch of discussion for historical reference. It has a bunch of analyzer captures and charts in it which may appear somewhat similar to what you were seeing.

@tombrazier
Copy link
Contributor

This feels very similar to something we fixed in the past, but perhaps it's a slightly different scenario. We might have fixed long pulses when starting from a standstill, versus yours transitioning form block to block.

It was the long pulses from standstill that we fixed.

@tombrazier
Copy link
Contributor

@GelidusResearch Could you elaborate? I think that it doesn't matter how low the step rates gets because in the stepper module it will not step any slower than 32 steps per second. That's built into Stepper::calc_timer_interval().

@GelidusResearch
Copy link

@GelidusResearch Could you elaborate? I think that it doesn't matter how low the step rates gets because in the stepper module it will not step any slower than 32 steps per second. That's built into Stepper::calc_timer_interval().

Certainly, with the AVR and a period of 32Hz that sets OCR1A at ~62500, if there is any delay or latency in the ISR processing this could result in an overflow. We have no preemption capability to assert priority so if the processor is heavily loaded with other IRQ's this period value could be unstable. Of course we would need significant built up testing to see that potential issue.

We have a window of ~500us before it overflows. I think that's what Eric may have encountered and thus he went to 120Hz for safety. So with the AVR 16bit timers @ 16Mhz and lower we may have issues near the 32Hz minimum for some users.

@sjasonsmith sjasonsmith added Needs: Testing Testing is needed for this change Needs: Discussion Discussion is needed labels Apr 8, 2024
@tombrazier
Copy link
Contributor

@GelidusResearch Having given it more thought and reminded myself what the code does, I believe overflow is not a concern. The stepper interrupt timer gets reset to zero on compare match. It makes no difference how close it was to the maximum count of the timer when this happens. This must be the case for all MCUs because Stepper::isr() assumes that the counter will have started again at zero when the interrupt was triggered.

@HoverClub HoverClub closed this Apr 8, 2024
@HoverClub HoverClub reopened this Apr 8, 2024
@HoverClub
Copy link
Contributor Author

HoverClub commented Apr 8, 2024

Removing the MINIMAL_STEP_RATE NOLESS in planner will invalidate the laser trapezoidal and dynamic mode power calculations as the physical step rate will be forced higher by stepper.cpp at low feed rates. It also means that the block accel/decel calcs/counts will be incorrect. This may cause other issues so it may be better to keep the NOLESS checks using the calculated MINIMAL_STEP_RATE to avoid any other consequences?

I have tested removal on a slow Laser and a 3D printer but that is no guarantee that it woks in all situations.

@tombrazier
Copy link
Contributor

Removing the MINIMAL_STEP_RATE NOLESS in planner will invalidate the laser trapezoidal and dynamic mode power calculations as the physical step rate will be forced higher by stepper.cpp at low feed rates. It also means that the block accel/decel calcs/counts will be incorrect. This may cause other issues so it may be better to keep the NOLESS checks using the calculated MINIMAL_STEP_RATE to avoid any other consequences?

Excellent point. All said, I now think the original implementation of this PR was about right.

Marlin/src/module/planner.h Outdated Show resolved Hide resolved
@mh-dm
Copy link
Contributor

mh-dm commented May 11, 2024

From the other discussion:

Is there any reason to delay merging #26881?

No, there isn't. It could be improved further but it's better as it is than not merging it and I can do the extra improvements at some point.

There is. I'll rephrase a comment I've made previously.

Right now, the MINIMAL_STEP_RATE of 120 acts like a kludge to prevent the worst cases of acceleration spikes. Short explainer: The first step of a segment is always at intial_rate. If it's too slow, it will result in too much accumulated acceleration_time (see stepper.cpp) and that means a very high calculated second step rate, and the difference is the acceleration spike.

To see acceleration spikes (and maybe even lost steps) with this PR in this current state, just test with something that results in a low junction speed / a low initial_rate. For example set a low jerk of 0.1mm/s, high acceleration of 2000+mm/s^2 and a speed of 100+mm/s.

@thinkyhead
Copy link
Member

I went ahead and added a commit applying suggestions from the discussion.

MINIMAL_STEP_RATE is an integer but STEPPER_TIMER_RATE / HAL_TIMER_TYPE_MAX generally won't be.

It will always be an integer. If STEPPER_TIMER_RATE > HAL_TIMER_TYPE_MAX it may be > 1, otherwise it will just be 1. Let's look at some samples: and see if we always get what we want:

HC32…

  • STEPPER_TIMER_RATE = (HAL_TIMER_RATE / STEPPER_TIMER_PRESCALE) => (50000000 / 16) = 3125000
  • HAL_TIMER_TYPE_MAX = 0xFFFF
  • MINIMAL_STEP_RATE = (3125000 / 0xFFFF) = 47

Teensy 3.1/3.2

  • STEPPER_TIMER_RATE = HAL_TIMER_RATE => 7500000
  • HAL_TIMER_TYPE_MAX = 0xFFFFFFFF
  • MINIMAL_STEP_RATE = (7500000 / 0xFFFFFFFF) = 0

SAMD21

  • STEPPER_TIMER_RATE = HAL_TIMER_RATE => F_CPU => 48000000
  • HAL_TIMER_TYPE_MAX = 0xFFFFFFFF
  • MINIMAL_STEP_RATE = (48000000 / 0xFFFFFFFF) = 0

I presume that the MINIMAL_STEP_RATE really represents the smallest number of steps-per-second that we can handle given our timer size, and when it comes down to our architecture, the smallest number we permit is 1 step per second. Of course, that might just mean the smallest number of interrupts per second, whereas we could allow much lower feedrates, presuming we don't require a step event to occur on every stepper interrupt. Anyway, I've never personally tried it, so I don't know yet what happens when you request a feedrate that would result in under one step per second.

Is that all that the MINIMAL_STEP_RATE does?

@tombrazier
Copy link
Contributor

Right now, the MINIMAL_STEP_RATE of 120 acts like a kludge to prevent the worst cases of acceleration spikes. Short explainer: The first step of a segment is always at intial_rate. If it's too slow, it will result in too much accumulated acceleration_time (see stepper.cpp) and that means a very high calculated second step rate, and the difference is the acceleration spike.

So we need #27035 before redefining MINIMAL_STEP_RATE as the speed the stepper driver will actually use.

@tombrazier
Copy link
Contributor

@mh-dm Can you pull the min_entry_speed_sqr change out of #27035 and submit it as a standalone PR? I think that would work. Then it can be merged first, followed by this PR. And, as a bonus, it will make the changes in #27035 a bit simpler to read.

@tombrazier
Copy link
Contributor

It will always be an integer. If STEPPER_TIMER_RATE > HAL_TIMER_TYPE_MAX it may be > 1, otherwise it will just be 1. Let's look at some samples: and see if we always get what we want:...

@thinkyhead I mean if you type the division into a calculator, it won't be generally be an integer. If we take just the first case, for HC32, the step rate that the stepper will generate is 3125000 / 0xFFFF, which is 47.68444342717632. Truncating to an integer takes that to 47, which is not achievable. However I'm splitting hairs. A 1.4% difference in the speed used to calculate laser power is probably not a great problem.

@mh-dm
Copy link
Contributor

mh-dm commented May 13, 2024

@mh-dm Can you pull the min_entry_speed_sqr change out of #27035 and submit it as a standalone PR? I think that would work. Then it can be merged first, followed by this PR. And, as a bonus, it will make the changes in #27035 a bit simpler to read.

@tombrazier Based on your request I started PR #27089 but I kinda hesitate to recommend this option. Adding min_entry_speed_sqr necessarily touches the planner forward/backward passes that will then be further changed by #27035. I don't yet have a good feel for the expected level of testing/stability each PR should get before being accepted into bugfix. If it's high then I'd rather spend it once on #27035 as a whole (esp. since I already did test it out). If it's not that high then #27089 is good to go.

@thinkyhead
Copy link
Member

Of course, we shouldn't leave the branch in a known broken state, so although it's good to have 27089 as a separate PR that can be merged first (for clarity with the changes to follow) we should just decide when this PR and 27035 are "good enough" and I'll merge them all at the same time. Then we should do some testing soon after to make sure everything is stable and nothing is wonky, then continue repairs from there. If it ends up needing to be reverted for any reason, it will get reverted as a unit.

@mh-dm
Copy link
Contributor

mh-dm commented May 14, 2024

Of course, we shouldn't leave the branch in a known broken state

Well, definitely.

it will get reverted as a unit.

If #27089 and #27035 are going to be treated as a unit they might as well stay a unit.

And it would be nice to decouple this PR from them. So he're my proposal that to get this PR submitted now. Just add this:

// Keep existing kludge against acceleration spikes until #27035
NOLESS(initial_rate, MIN(120, block->nominal_rate));
NOLESS(final_rate, MIN(120, block->nominal_rate));

to Planner::calculate_trapezoid_for_block() right before the existing NOLESSs.

@thinkyhead
Copy link
Member

Please double-check the results of my merge. It was unclear whether to keep most of the stuff from #27013 or make it look more like the prior state of this PR. Maybe a hybrid is needed.

Comment on lines +797 to +798
const uint32_t initial_rate = _MAX((long int)MINIMAL_STEP_RATE, LROUND(block->nominal_rate * entry_factor)), // (steps per second)
final_rate = _MAX((long int)MINIMAL_STEP_RATE, LROUND(block->nominal_rate * exit_factor));
Copy link
Contributor

Choose a reason for hiding this comment

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

Just a style/readability comment: with the cast and the _MAX there's too much on each line.

Comment on lines 797 to 808
uint32_t initial_rate = LROUND(block->nominal_rate * entry_factor),
final_rate = LROUND(block->nominal_rate * exit_factor); // (steps per second)
const uint32_t initial_rate = _MAX(LROUND(block->nominal_rate * entry_factor), MINIMAL_STEP_RATE),
final_rate = _MAX(LROUND(block->nominal_rate * exit_factor), MINIMAL_STEP_RATE); // (steps per second)

// Limit minimal step rate (Otherwise the timer will overflow.)
NOLESS(initial_rate, uint32_t(MINIMAL_STEP_RATE)); // Enforce the minimum speed
NOLESS(final_rate, uint32_t(MINIMAL_STEP_RATE));
// Now ensure the nominal rate is above minimum
NOLESS(block->nominal_rate, MINIMAL_STEP_RATE);

// Assume these are handled elsewhere. Retained for debugging.
//NOMORE(initial_rate, block->nominal_rate); // NOTE: The nominal rate may be less than MINIMAL_STEP_RATE!
//NOMORE(final_rate, block->nominal_rate);

Copy link
Contributor

Choose a reason for hiding this comment

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

Following up on this PR: I think this version of the code is the most readable and easiest for the follow up PR #27035.

  uint32_t initial_rate = LROUND(block->nominal_rate * entry_factor),
           final_rate = LROUND(block->nominal_rate * exit_factor); // (steps per second)

  // Limit minimal step rate (Otherwise the timer will overflow.)
  NOLESS(initial_rate, MINIMAL_STEP_RATE);
  NOLESS(final_rate, MINIMAL_STEP_RATE);
  NOLESS(block->nominal_rate, MINIMAL_STEP_RATE);

Copy link
Contributor

Choose a reason for hiding this comment

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

I like keeping the limiting code as a separate block for readability as well.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
C: Motion Needs: Discussion Discussion is needed Needs: Testing Testing is needed for this change PR: Bug Fix
Projects
Status: In Progress
Development

Successfully merging this pull request may close these issues.

None yet

7 participants