Fix planner block optimization

- Fixed the planner incorrectly avoiding optimization of the block following the active one.
- Added extra conditions to terminate planner early and avoid redundant computations.
This commit is contained in:
etagle 2018-05-18 04:04:01 -03:00 committed by Scott Lahteine
parent e0ca627033
commit a4af975873
2 changed files with 181 additions and 77 deletions

View file

@ -107,7 +107,8 @@ block_t Planner::block_buffer[BLOCK_BUFFER_SIZE];
volatile uint8_t Planner::block_buffer_head, // Index of the next block to be pushed
Planner::block_buffer_tail; // Index of the busy block, if any
uint16_t Planner::cleaning_buffer_counter; // A counter to disable queuing of blocks
uint8_t Planner::delay_before_delivering; // This counter delays delivery of blocks when queue becomes empty to allow the opportunity of merging blocks
uint8_t Planner::delay_before_delivering, // This counter delays delivery of blocks when queue becomes empty to allow the opportunity of merging blocks
Planner::block_buffer_planned; // Index of the optimally planned block
float Planner::max_feedrate_mm_s[XYZE_N], // Max speeds in mm per second
Planner::axis_steps_per_mm[XYZE_N],
@ -227,6 +228,7 @@ void Planner::init() {
bed_level_matrix.set_to_identity();
#endif
clear_block_buffer();
block_buffer_planned = 0;
delay_before_delivering = 0;
}
@ -825,6 +827,68 @@ void Planner::calculate_trapezoid_for_block(block_t* const block, const float &e
if (was_enabled) ENABLE_STEPPER_DRIVER_INTERRUPT();
}
/* PLANNER SPEED DEFINITION
+--------+ <- current->nominal_speed
/ \
current->entry_speed -> + \
| + <- next->entry_speed (aka exit speed)
+-------------+
time -->
Recalculates the motion plan according to the following basic guidelines:
1. Go over every feasible block sequentially in reverse order and calculate the junction speeds
(i.e. current->entry_speed) such that:
a. No junction speed exceeds the pre-computed maximum junction speed limit or nominal speeds of
neighboring blocks.
b. A block entry speed cannot exceed one reverse-computed from its exit speed (next->entry_speed)
with a maximum allowable deceleration over the block travel distance.
c. The last (or newest appended) block is planned from a complete stop (an exit speed of zero).
2. Go over every block in chronological (forward) order and dial down junction speed values if
a. The exit speed exceeds the one forward-computed from its entry speed with the maximum allowable
acceleration over the block travel distance.
When these stages are complete, the planner will have maximized the velocity profiles throughout the all
of the planner blocks, where every block is operating at its maximum allowable acceleration limits. In
other words, for all of the blocks in the planner, the plan is optimal and no further speed improvements
are possible. If a new block is added to the buffer, the plan is recomputed according to the said
guidelines for a new optimal plan.
To increase computational efficiency of these guidelines, a set of planner block pointers have been
created to indicate stop-compute points for when the planner guidelines cannot logically make any further
changes or improvements to the plan when in normal operation and new blocks are streamed and added to the
planner buffer. For example, if a subset of sequential blocks in the planner have been planned and are
bracketed by junction velocities at their maximums (or by the first planner block as well), no new block
added to the planner buffer will alter the velocity profiles within them. So we no longer have to compute
them. Or, if a set of sequential blocks from the first block in the planner (or a optimal stop-compute
point) are all accelerating, they are all optimal and can not be altered by a new block added to the
planner buffer, as this will only further increase the plan speed to chronological blocks until a maximum
junction velocity is reached. However, if the operational conditions of the plan changes from infrequently
used feed holds or feedrate overrides, the stop-compute pointers will be reset and the entire plan is
recomputed as stated in the general guidelines.
Planner buffer index mapping:
- block_buffer_tail: Points to the beginning of the planner buffer. First to be executed or being executed.
- block_buffer_head: Points to the buffer block after the last block in the buffer. Used to indicate whether
the buffer is full or empty. As described for standard ring buffers, this block is always empty.
- block_buffer_planned: Points to the first buffer block after the last optimally planned block for normal
streaming operating conditions. Use for planning optimizations by avoiding recomputing parts of the
planner buffer that don't change with the addition of a new block, as describe above. In addition,
this block can never be less than block_buffer_tail and will always be pushed forward and maintain
this requirement when encountered by the plan_discard_current_block() routine during a cycle.
NOTE: Since the planner only computes on what's in the planner buffer, some motions with lots of short
line segments, like G2/3 arcs or complex curves, may seem to move slow. This is because there simply isn't
enough combined distance traveled in the entire buffer to accelerate up to the nominal speed and then
decelerate to a complete stop at the end of the buffer, as stated by the guidelines. If this happens and
becomes an annoyance, there are a few simple solutions: (1) Maximize the machine acceleration. The planner
will be able to compute higher velocity profiles within the same combined distance. (2) Maximize line
motion(s) distance per block to a desired tolerance. The more combined distance the planner has to use,
the faster it can go. (3) Maximize the planner buffer size. This also will increase the combined distance
for the planner to compute over. It also increases the number of computations the planner has to perform
to compute an optimal plan, so select carefully.
*/
// The kernel called by recalculate() when scanning the plan from last to first entry.
void Planner::reverse_pass_kernel(block_t* const current, const block_t * const next) {
if (current) {
@ -851,6 +915,8 @@ void Planner::reverse_pass_kernel(block_t* const current, const block_t * const
: MIN(max_entry_speed_sqr, max_allowable_speed_sqr(-current->acceleration, next ? next->entry_speed_sqr : sq(MINIMUM_PLANNER_SPEED), current->millimeters));
if (current->entry_speed_sqr != new_entry_speed_sqr) {
current->entry_speed_sqr = new_entry_speed_sqr;
// Need to recalculate the block speed
SBI(current->flag, BLOCK_BIT_RECALCULATE);
}
}
@ -862,44 +928,72 @@ void Planner::reverse_pass_kernel(block_t* const current, const block_t * const
* Once in reverse and once forward. This implements the reverse pass.
*/
void Planner::reverse_pass() {
if (movesplanned() > 2) {
const uint8_t endnr = next_block_index(block_buffer_tail); // tail is running. tail+1 shouldn't be altered because it's connected to the running block.
uint8_t blocknr = prev_block_index(block_buffer_head);
// Initialize block index to the last block in the planner buffer.
uint8_t block_index = prev_block_index(block_buffer_head);
// Read the index of the last buffer planned block.
// The ISR may change it so get a stable local copy.
uint8_t planned_block_index = block_buffer_planned;
// If there was a race condition and block_buffer_planned was incremented
// or was pointing at the head (queue empty) break loop now and avoid
// planning already consumed blocks
if (planned_block_index == block_buffer_head) return;
// Reverse Pass: Coarsely maximize all possible deceleration curves back-planning from the last
// block in buffer. Cease planning when the last optimal planned or tail pointer is reached.
// NOTE: Forward pass will later refine and correct the reverse pass to create an optimal plan.
block_t *current;
const block_t *next = NULL;
while (block_index != planned_block_index) {
// Perform the reverse pass
block_t *current, *next = NULL;
while (blocknr != endnr) {
// Perform the reverse pass - Only consider non sync blocks
current = &block_buffer[blocknr];
if (!TEST(current->flag, BLOCK_BIT_SYNC_POSITION)) {
reverse_pass_kernel(current, next);
next = current;
}
// Advance to the next
blocknr = prev_block_index(blocknr);
current = &block_buffer[block_index];
// Only consider non sync blocks
if (!TEST(current->flag, BLOCK_BIT_SYNC_POSITION)) {
reverse_pass_kernel(current, next);
next = current;
}
// Advance to the next
block_index = prev_block_index(block_index);
}
}
// The kernel called by recalculate() when scanning the plan from first to last entry.
void Planner::forward_pass_kernel(const block_t * const previous, block_t* const current) {
void Planner::forward_pass_kernel(const block_t * const previous, block_t* const current, uint8_t block_index) {
if (previous) {
// If the previous block is an acceleration block, too short to complete the full speed
// change, adjust the entry speed accordingly. Entry speeds have already been reset,
// maximized, and reverse-planned. If nominal length is set, max junction speed is
// guaranteed to be reached. No need to recheck.
if (!TEST(previous->flag, BLOCK_BIT_NOMINAL_LENGTH)) {
if (previous->entry_speed_sqr < current->entry_speed_sqr) {
// Compute the maximum allowable speed
const float new_entry_speed_sqr = max_allowable_speed_sqr(-previous->acceleration, previous->entry_speed_sqr, previous->millimeters);
// If true, current block is full-acceleration
if (current->entry_speed_sqr > new_entry_speed_sqr) {
// Always <= max_entry_speed_sqr. Backward pass sets this.
current->entry_speed_sqr = new_entry_speed_sqr;
SBI(current->flag, BLOCK_BIT_RECALCULATE);
}
if (!TEST(previous->flag, BLOCK_BIT_NOMINAL_LENGTH) &&
previous->entry_speed_sqr < current->entry_speed_sqr) {
// Compute the maximum allowable speed
const float new_entry_speed_sqr = max_allowable_speed_sqr(-previous->acceleration, previous->entry_speed_sqr, previous->millimeters);
// If true, current block is full-acceleration and we can move the planned pointer forward.
if (new_entry_speed_sqr < current->entry_speed_sqr) {
// Always <= max_entry_speed_sqr. Backward pass sets this.
current->entry_speed_sqr = new_entry_speed_sqr; // Always <= max_entry_speed_sqr. Backward pass sets this.
// Set optimal plan pointer.
block_buffer_planned = block_index;
// And mark we need to recompute the trapezoidal shape
SBI(current->flag, BLOCK_BIT_RECALCULATE);
}
}
// Any block set at its maximum entry speed also creates an optimal plan up to this
// point in the buffer. When the plan is bracketed by either the beginning of the
// buffer and a maximum entry speed or two maximum entry speeds, every block in between
// cannot logically be further improved. Hence, we don't have to recompute them anymore.
if (current->entry_speed_sqr == current->max_entry_speed_sqr)
block_buffer_planned = block_index;
}
}
@ -908,20 +1002,30 @@ void Planner::forward_pass_kernel(const block_t * const previous, block_t* const
* Once in reverse and once forward. This implements the forward pass.
*/
void Planner::forward_pass() {
const uint8_t endnr = block_buffer_head;
uint8_t blocknr = block_buffer_tail;
// Perform the forward pass
block_t *current, *previous = NULL;
while (blocknr != endnr) {
// Perform the forward pass - Only consider non-sync blocks
current = &block_buffer[blocknr];
// Forward Pass: Forward plan the acceleration curve from the planned pointer onward.
// Also scans for optimal plan breakpoints and appropriately updates the planned pointer.
// Begin at buffer planned pointer. Note that block_buffer_planned can be modified
// by the stepper ISR, so read it ONCE. It it guaranteed that block_buffer_planned
// will never lead head, so the loop is safe to execute. Also note that the forward
// pass will never modify the values at the tail.
uint8_t block_index = block_buffer_planned;
block_t *current;
const block_t * previous = NULL;
while (block_index != block_buffer_head) {
// Perform the forward pass
current = &block_buffer[block_index];
// Skip SYNC blocks
if (!TEST(current->flag, BLOCK_BIT_SYNC_POSITION)) {
forward_pass_kernel(previous, current);
forward_pass_kernel(previous, current, block_index);
previous = current;
}
// Advance to the previous
blocknr = next_block_index(blocknr);
block_index = next_block_index(block_index);
}
}
@ -931,6 +1035,7 @@ void Planner::forward_pass() {
* recalculate() after updating the blocks.
*/
void Planner::recalculate_trapezoids() {
// The tail may be changed by the ISR so get a local copy.
uint8_t block_index = block_buffer_tail;
// As there could be a sync block in the head of the queue, and the next loop must not
@ -1004,33 +1109,14 @@ void Planner::recalculate_trapezoids() {
}
}
/**
* Recalculate the motion plan according to the following algorithm:
*
* 1. Go over every block in reverse order...
*
* Calculate a junction speed reduction (block_t.entry_factor) so:
*
* a. The junction jerk is within the set limit, and
*
* b. No speed reduction within one block requires faster
* deceleration than the one, true constant acceleration.
*
* 2. Go over every block in chronological order...
*
* Dial down junction speed reduction values if:
* a. The speed increase within one block would require faster
* acceleration than the one, true constant acceleration.
*
* After that, all blocks will have an entry_factor allowing all speed changes to
* be performed using only the one, true constant acceleration, and where no junction
* jerk is jerkier than the set limit, Jerky. Finally it will:
*
* 3. Recalculate "trapezoids" for all blocks.
*/
void Planner::recalculate() {
reverse_pass();
forward_pass();
// Initialize block index to the last block in the planner buffer.
const uint8_t block_index = prev_block_index(block_buffer_head);
// If there is just one block, no planning can be done. Avoid it!
if (block_index != block_buffer_planned) {
reverse_pass();
forward_pass();
}
recalculate_trapezoids();
}
@ -1348,10 +1434,18 @@ void Planner::check_axes_activity() {
#endif // PLANNER_LEVELING
void Planner::quick_stop() {
// Remove all the queued blocks. Note that this function is NOT
// called from the Stepper ISR, so we must consider tail as readonly!
// that is why we set head to tail!
block_buffer_head = block_buffer_tail;
// that is why we set head to tail - But there is a race condition that
// must be handled: The tail could change between the read and the assignment
// so this must be enclosed in a critical section
const bool was_enabled = STEPPER_ISR_ENABLED();
if (was_enabled) DISABLE_STEPPER_DRIVER_INTERRUPT();
// Drop all queue entries
block_buffer_planned = block_buffer_head = block_buffer_tail;
// Restart the block delay for the first movement - As the queue was
// forced to empty, there's no risk the ISR will touch this.
@ -1365,6 +1459,9 @@ void Planner::quick_stop() {
// Make sure to drop any attempt of queuing moves for at least 1 second
cleaning_buffer_counter = 1000;
// Reenable Stepper ISR
if (was_enabled) ENABLE_STEPPER_DRIVER_INTERRUPT();
// And stop the stepper ISR
stepper.quick_stop();
}

View file

@ -177,7 +177,9 @@ class Planner {
static volatile uint8_t block_buffer_head, // Index of the next block to be pushed
block_buffer_tail; // Index of the busy block, if any
static uint16_t cleaning_buffer_counter; // A counter to disable queuing of blocks
static uint8_t delay_before_delivering; // This counter delays delivery of blocks when queue becomes empty to allow the opportunity of merging blocks
static uint8_t delay_before_delivering, // This counter delays delivery of blocks when queue becomes empty to allow the opportunity of merging blocks
block_buffer_planned; // Index of the optimally planned block
#if ENABLED(DISTINCT_E_FACTORS)
static uint8_t last_extruder; // Respond to extruder change
@ -655,9 +657,7 @@ class Planner {
block_t * const block = &block_buffer[block_buffer_tail];
// No trapezoid calculated? Don't execute yet.
if ( TEST(block->flag, BLOCK_BIT_RECALCULATE)
|| (movesplanned() > 1 && TEST(block_buffer[next_block_index(block_buffer_tail)].flag, BLOCK_BIT_RECALCULATE))
) return NULL;
if (TEST(block->flag, BLOCK_BIT_RECALCULATE)) return NULL;
#if ENABLED(ULTRA_LCD)
block_buffer_runtime_us -= block->segment_time_us; // We can't be sure how long an active block will take, so don't count it.
@ -667,13 +667,13 @@ class Planner {
SBI(block->flag, BLOCK_BIT_BUSY);
return block;
}
else {
// The queue became empty
#if ENABLED(ULTRA_LCD)
clear_block_buffer_runtime(); // paranoia. Buffer is empty now - so reset accumulated time to zero.
#endif
return NULL;
}
// The queue became empty
#if ENABLED(ULTRA_LCD)
clear_block_buffer_runtime(); // paranoia. Buffer is empty now - so reset accumulated time to zero.
#endif
return NULL;
}
/**
@ -682,7 +682,14 @@ class Planner {
* NB: There MUST be a current block to call this function!!
*/
FORCE_INLINE static void discard_current_block() {
block_buffer_tail = BLOCK_MOD(block_buffer_tail + 1);
if (has_blocks_queued()) { // Discard non-empty buffer.
uint8_t block_index = next_block_index( block_buffer_tail );
// Push block_buffer_planned pointer, if encountered.
if (!has_blocks_queued()) block_buffer_planned = block_index;
block_buffer_tail = block_index;
}
}
#if ENABLED(ULTRA_LCD)
@ -741,8 +748,8 @@ class Planner {
/**
* Get the index of the next / previous block in the ring buffer
*/
static constexpr int8_t next_block_index(const int8_t block_index) { return BLOCK_MOD(block_index + 1); }
static constexpr int8_t prev_block_index(const int8_t block_index) { return BLOCK_MOD(block_index - 1); }
static constexpr uint8_t next_block_index(const uint8_t block_index) { return BLOCK_MOD(block_index + 1); }
static constexpr uint8_t prev_block_index(const uint8_t block_index) { return BLOCK_MOD(block_index - 1); }
/**
* Calculate the distance (not time) it takes to accelerate
@ -787,7 +794,7 @@ class Planner {
static void calculate_trapezoid_for_block(block_t* const block, const float &entry_factor, const float &exit_factor);
static void reverse_pass_kernel(block_t* const current, const block_t * const next);
static void forward_pass_kernel(const block_t * const previous, block_t* const current);
static void forward_pass_kernel(const block_t * const previous, block_t* const current, uint8_t block_index);
static void reverse_pass();
static void forward_pass();