Files
g2/g2core/plan_zoid.cpp
2017-02-19 06:34:28 -05:00

594 lines
24 KiB
C++

/*
* plan_zoid.cpp - acceleration managed line planning and motion execution - trapezoid planner
* This file is part of the g2core project
*
* Copyright (c) 2010 - 2017 Alden S. Hart, Jr.
* Copyright (c) 2012 - 2017 Rob Giseburt
*
* This file ("the software") is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License, version 2 as published by the
* Free Software Foundation. You should have received a copy of the GNU General Public
* License, version 2 along with the software. If not, see <http://www.gnu.org/licenses/>.
*
* As a special exception, you may use this file as part of a software library without
* restriction. Specifically, if other files instantiate templates or use macros or
* inline functions from this file, or you compile this file and link it with other
* files to produce an executable, this file does not by itself cause the resulting
* executable to be covered by the GNU General Public License. This exception does not
* however invalidate any other reasons why the executable file might be covered by the
* GNU General Public License.
*
* THE SOFTWARE IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, BUT WITHOUT ANY
* WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
* SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
* OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include "g2core.h"
#include "config.h"
#include "planner.h"
#include "report.h"
#include "util.h"
// DIAGNOSTICS
//#ifndef IN_DEBUGGER
#define LOG_RETURN(msg) // LOG_RETURN with no action (production)
//#else
//#include "xio.h"
//static char logbuf[128];
//static void _logger(const char *msg, const mpBuf_t *bf) // LOG_RETURN with full state dump
//{
// sprintf(logbuf, "[%2d] %s (%d) mt:%5.2f, L:%1.3f [%1.3f, %1.3f, %1.3f] V:[%1.2f, %1.2f, %1.2f]\n",
// bf->buffer_number, msg, bf->hint, (bf->block_time * 60000),
// bf->length, bf->head_length, bf->body_length, bf->tail_length,
// bf->pv->exit_velocity, bf->cruise_velocity, bf->exit_velocity);
// xio_writeline(logbuf);
//}
//#define LOG_RETURN(msg) { _logger(msg, bf); }
//#endif
#ifndef IN_DEBUGGER
#define TRAP_ZERO(t,m)
#else
#define TRAP_ZERO(t, m) \
if (fp_ZERO(t)) { \
rpt_exception(STAT_MINIMUM_LENGTH_MOVE, m); \
_debug_trap(m); \
}
#endif
// END DIAGNOSTICS
/* local functions */
// static float _get_target_length_min(const float v_0, const float v_1, const mpBuf_t *bf, const float min);
static float _get_meet_velocity(const float v_0,
const float v_2,
const float L,
mpBuf_t* bf,
mpBlockRuntimeBuf_t* block);
/****************************************************************************************
* mp_calculate_ramps() - calculate trapezoid-like ramp parameters for a block
*
* This long-ish function sets section lengths and velocities based on the move length
* and velocities requested. It modifies the incoming bf buffer and returns accurate
* head, body and tail lengths, and accurate or reasonably approximate velocities.
* We care about length accuracy, less so for velocity (as long as jerk is not exceeded).
*
* We need velocities to be set even for zero-length sections (nb: sections, not moves)
* so plan_exec can compute entry and exits for adjacent sections.
*
* Note we use three data structures: mr, bf, and block.
*
* bf holds the data from aline and back-planning. For the most part it is immutable.
*
* block is the data for forward-planning. There are only two block structures, and they
* are for the current block and the next block.
*
* bf values treated as constants:
* All except block_time and hint
*
* mr holds the current velocity, which is th entry_vlocity to the first block. The
* exit_vlocity of the first vlock is the entry of the next. To save having to figure
* out what block this is, we pass mp_calculate_ramps the entry_velocity is it to use.
*
* All values of block are expected to be setup by mp_calculate_ramps.
*
* Quick cheat-sheet on which is in buffer (bf) and which is in block:
* buffer (bf):
* block_type
* hint
* {cruise,exit}_vmax
* block_time
* length
* (start values of {cruise,exit}_velocity)
*
* block:
* {cruise,exit}_velocity (final)
* {head,body,tail}_length
* {head,body,tail}_time
*
*/
void _zoid_exit(mpBuf_t* bf, zoidExitPoint exit_point)
{
#ifdef __PLANNER_DIAGNOSTICS
// bf->zoid_exit = exit_point;
if (mp_runtime_is_idle()) { // normally the runtime keeps this value fresh
// bf->time_in_plan_ms += bf->block_time_ms;
bf->plannable_time_ms += bf->block_time_ms;
}
#endif
}
// Hint will be one of these from back-planning: COMMAND_BLOCK, PERFECT_DECELERATION, PERFECT_CRUISE,
// MIXED_DECELERATION, ASYMMETRIC_BUMP
// We are incorporating both the forward planning and ramp-planning into one function, since we use the same data.
void mp_calculate_ramps(mpBlockRuntimeBuf_t* block, mpBuf_t* bf, const float entry_velocity)
{
// *** Skip non-move commands ***
if (bf->block_type == BLOCK_TYPE_COMMAND) {
bf->hint = COMMAND_BLOCK;
return;
}
TRAP_ZERO(bf->length, "zoid() got L=0"); //+++++ Move this outside of zoid
TRAP_ZERO(bf->cruise_velocity, "zoid() got Vc=0"); // move this outside
// Timing from *here*
// initialize parameters to know values
block->head_time = 0;
block->body_time = 0;
block->tail_time = 0;
block->head_length = 0;
block->body_length = 0;
block->tail_length = 0;
block->cruise_velocity = min(bf->cruise_velocity, bf->cruise_vmax);
block->exit_velocity = min(bf->exit_velocity, bf->exit_vmax);
// We *might* do this exact computation later, so cache the value.
float test_velocity = 0;
bool test_velocity_valid = false; // record if we have a validly cached value
// *** Perfect-Fit Cases (1) *** Cases where curve fitting has already been done
// PERFECT_CRUISE (1c) Velocities all match (or close enough), treat as body-only
// NOTE: PERFECT_CRUISE is set in back-planning without knowledge of pv->exit, since it can't see it yet.
// Here we verify it moving forward, checking to make sure it still is true.
// If so, we plan the "ramp" as flat, body-only.
if (bf->hint == PERFECT_CRUISE) {
if ((!mp->entry_changed) && fp_EQ(entry_velocity, bf->cruise_vmax)) {
// We need to ensure that neither the entry or the exit velocities are
// <= the cruise velocity even though there is tolerance in fp_EQ comparison.
block->exit_velocity = entry_velocity;
block->cruise_velocity = entry_velocity;
block->body_length = bf->length;
block->body_time = block->body_length / block->cruise_velocity;
bf->block_time = block->body_time;
LOG_RETURN("1c");
return (_zoid_exit(bf, ZOID_EXIT_1c));
} else {
// we need to degrade the hint to MIXED_ACCELERATION
bf->hint = MIXED_ACCELERATION;
}
}
// Quick test to ensure we haven't violated the hint
if (entry_velocity > block->exit_velocity) {
// We're in a deceleration.
if (mp->entry_changed) {
// If entry_changed, then entry_velocity is lower than the hints expect.
// A deceleration will never become an acceleration (post-hinting).
// If it is marked as MIXED_DECELERATION, it means the entry was CRUISE_VMAX.
// If it is marked as PERFECT_DECELERATION, it means the entry was as <= CRUISE_VMAX,
// but as high as possible.
// So, this move is and was a DECELERATION, meaning it *could* achieve the previous (higher)
// entry safely, it will likely get a head section, so we will now degrade the hint to an
// ASYMMETRIC_BUMP.
bf->hint = ASYMMETRIC_BUMP;
}
// MIXED_DECELERATION (2d) 2 segment BT deceleration move
// Only possible if the entry has not changed since hinting.
else if (bf->hint == MIXED_DECELERATION) {
block->tail_length = mp_get_target_length(block->exit_velocity, block->cruise_velocity, bf);
block->body_length = bf->length - block->tail_length;
block->head_length = 0;
block->body_time = block->body_length / block->cruise_velocity;
block->tail_time = block->tail_length * 2 / (block->exit_velocity + block->cruise_velocity);
bf->block_time = block->body_time + block->tail_time;
LOG_RETURN("2d");
return (_zoid_exit(bf, ZOID_EXIT_2d));
}
// PERFECT_DECELERATION (1d) single tail segment (deltaV == delta_vmax)
// Only possible if the entry has not changed since hinting.
else if (bf->hint == PERFECT_DECELERATION) {
block->tail_length = bf->length;
block->cruise_velocity = entry_velocity;
block->tail_time = block->tail_length * 2 / (block->exit_velocity + block->cruise_velocity);
bf->block_time = block->tail_time;
LOG_RETURN("1d");
return (_zoid_exit(bf, ZOID_EXIT_1d));
}
// Reset entry_changed. We won't likely be changing the next block's entry velocity.
mp->entry_changed = false;
// Since we are not generally decelerating, this is effectively all of forward planning that we need.
} else {
// Note that the hints from back-planning are ignored in this section, since back-planing can only predict decel
// and cruise.
float accel_velocity;
if (test_velocity_valid) {
accel_velocity = test_velocity;
test_velocity_valid = false;
} else {
accel_velocity = mp_get_target_velocity(entry_velocity, bf->length, bf);
}
if (accel_velocity < block->exit_velocity) { // still accelerating
mp->entry_changed = true; // we are changing the *next* block's entry velocity
block->exit_velocity = accel_velocity;
block->cruise_velocity = accel_velocity;
bf->hint = PERFECT_ACCELERATION;
// PERFECT_ACCELERATION (1a) single head segment (deltaV == delta_vmax)
block->head_length = bf->length;
block->cruise_velocity = block->exit_velocity;
block->head_time = (block->head_length * 2.0) / (entry_velocity + block->cruise_velocity);
bf->block_time = block->head_time;
LOG_RETURN("1a");
return (_zoid_exit(bf, ZOID_EXIT_1a));
} else { // it's hit the cusp
mp->entry_changed = false; // we are NOT changing the next block's entry velocity
block->cruise_velocity = bf->cruise_vmax;
if (block->cruise_velocity > block->exit_velocity) {
// We will likely have a head section, so hint the move as an ASYMMETRIC_BUMP
bf->hint = ASYMMETRIC_BUMP;
} else {
// We know that exit_velocity is higher than cruise_vmax, so adjust it
block->exit_velocity = bf->cruise_vmax;
bf->hint = MIXED_ACCELERATION;
// MIXED_ACCELERATION (2a) 2 segment HB acceleration move
block->head_length = mp_get_target_length(entry_velocity, block->cruise_velocity, bf);
block->body_length = bf->length - block->head_length;
block->tail_length = 0; // we just set it, now we unset it
block->head_time = (block->head_length * 2.0) / (entry_velocity + block->cruise_velocity);
block->body_time = block->body_length / block->cruise_velocity;
bf->block_time = block->head_time + block->body_time;
LOG_RETURN("2a");
return (_zoid_exit(bf, ZOID_EXIT_2a));
}
}
}
// We've eliminated the following at this point:
// PERFECT_ACCELERATION
// MIXED_ACCELERATION
// PERFECT_DECELERATION
// MIXED_DECELERATION
// All that remains is ASYMMETRIC_BUMP and SYMMETRIC_BUMP.
// We don't really care if it's symmetric, since the first test that _get_meet_velocity
// does is for a symmetric move. It's cheaper to just let it do that then to try and prevent it.
// *** Requested-Fit cases (2) ***
// Prepare the head and tail lengths for evaluating cases (nb: zeros head / tail < min length)
block->head_length = mp_get_target_length(entry_velocity, block->cruise_velocity, bf);
block->tail_length = mp_get_target_length(block->exit_velocity, block->cruise_velocity, bf);
if ((bf->length - 0.0001) > (block->head_length + block->tail_length)) {
// 3 segment HBT move (2c) - either with a body or just a symmetric bump
block->body_length = bf->length - (block->head_length + block->tail_length); // body guaranteed to be positive
block->head_time = (block->head_length * 2.0) / (entry_velocity + block->cruise_velocity);
block->body_time = block->body_length / block->cruise_velocity;
block->tail_time = (block->tail_length * 2.0) / (block->exit_velocity + block->cruise_velocity);
bf->block_time = block->head_time + block->body_time + block->tail_time;
bf->hint = ASYMMETRIC_BUMP;
LOG_RETURN("2c");
return (_zoid_exit(bf, ZOID_EXIT_2c));
}
// *** Rate-Limited-Fit cases (3) ***
// This means that bf->length < (bf->head_length + bf->tail_length)
// Rate-limited asymmetric cases (3)
// compute meet velocity to see if the cruise velocity rises above the entry and/or exit velocities
block->cruise_velocity = _get_meet_velocity(entry_velocity, block->exit_velocity, bf->length, bf, block);
// TRAP_ZERO(block->cruise_velocity, "zoid() Vc=0 asymmetric HT case");
if (fp_ZERO(block->cruise_velocity)) {
__asm__("BKPT");
}
// We now store the head/tail lengths we computed in _get_meet_velocity.
// treat as a full up and down (head and tail)
bf->hint = ASYMMETRIC_BUMP;
// Compute move times
// save a few divides where we can
if (fp_NOT_ZERO(block->head_length)) {
block->head_time = (block->head_length * 2.0) / (entry_velocity + block->cruise_velocity);
}
if (fp_NOT_ZERO(block->body_length)) {
block->body_time = block->body_length / block->cruise_velocity;
}
if (fp_NOT_ZERO(block->tail_length)) {
block->tail_time = (block->tail_length * 2.0) / (block->exit_velocity + block->cruise_velocity);
}
bf->block_time = block->head_time + block->body_time + block->tail_time;
LOG_RETURN("3c");
return (_zoid_exit(bf, ZOID_EXIT_3c)); // 550us worst case
}
/**** Planner helpers ****
*
* mp_get_target_length() - find accel/decel length from delta V and jerk
* mp_get_target_velocity() - find velocity achievable from Vi, length and jerk
* _get_target_length_min() - find target length with correction for minimum length moves
* _get_meet_velocity() - find velocity at which two lines intersect
*
* The get_target functions know 3 things and return the 4th:
* Jm = maximum jerk of the move
* T = time of the entire move
* Vi = initial velocity
* Vf = final velocity
*/
/*
* mp_get_target_length() - find accel/decel length from delta V and jerk
*/
// Just calling this tl_constant. It's full name is:
// static const float tl_constant = 1.201405707067378; // sqrt(5)/( sqrt(2)pow(3,4) )
float mp_get_target_length(const float v_0, const float v_1, const mpBuf_t* bf)
{
const float q_recip_2_sqrt_j = bf->q_recip_2_sqrt_j;
return q_recip_2_sqrt_j * sqrt(fabs(v_1 - v_0)) * (v_1 + v_0);
}
/*
* mp_get_target_velocity() - find the velocity we would achieve if we *accelerated* from v_0
*
* Get "the velocity" that we would end up at if we *accelerated* from v_0
* over the provided L (length) and J (jerk, provided in the bf structure).
*/
// 14 *, 1 /, 1 sqrt, 1 cbrt
// time: 68 us
float mp_get_target_velocity(const float v_0, const float L, const mpBuf_t* bf)
{
if (fp_ZERO(L)) { // handle exception case
return (0);
}
const float j = bf->jerk;
const float a80 = 7.698003589195; // 80 * a
const float a_2 = 0.00925925925926; // a^2
const float v_0_2 = v_0 * v_0; // v_0^2
const float v_0_3 = v_0_2 * v_0; // v_0^3
const float L_2 = L * L; // L^2
const float b_part1 = 9 * j * L_2; // 9 j L^2
const float b_part2 = a80 * v_0_3; // 80 a v_0^3
// b^3 = a^2 (3 L sqrt(j (2 b_part2 + b_part1)) + b_part2 + b_part1)
const float b_cubed = a_2 * (3 * L * sqrt(j * (2 * b_part2 + b_part1)) + b_part2 + b_part1);
const float b = cbrtf(b_cubed);
const float const1a = 0.8292422988276; // 4 * 10^(1/3) * a
const float const2a = 4.823680612597; // 1/(10^(1/3) * a)
const float const3 = 0.333333333333333; // 1/3
// v_1 = 1/3 ((const1a v_0^2)/b + b const2a - v_0)
const float v_1 = const3 * ((const1a * v_0_2) / b + b * const2a - v_0);
return fabs(v_1);
}
/*
* mp_get_decel_velocity() - mp_get_target_velocity but ONLY for deceleration
*
* Get "the velocity" that we would end up at if we *decelerated* from v_0,
* over the provided L (length) and J (jerk, provided in the bf structure).
*
* We have to use a root finding solution, since there is actually three possible
* solutions. We can eliminate one quickly, since it's the acceleration case.
*
* The other two cases are occasionally the same value, but this is rare.
* Otherwise there is a "high" value and a "low" value. The low value can be
* negative and then eliminated.
*
* This function may generate minor errors in target velocity, and should only
* be used to compute feedholds or other cases where exact velocity is not mandatory.
*/
float mp_get_decel_velocity(const float v_0, const float L, const mpBuf_t* bf)
{
const float q_recip_2_sqrt_j = bf->q_recip_2_sqrt_j;
float v_1 = 0; // start the guess at zero
int i = 0; // limit the iterations
while (i++ < 20) { // If it fails after 20 iterations something's wrong
// l_t is the difference in length between the L provided and the current guessed deceleration length
const float sqrt_delta_v_0 = sqrt(v_0 - v_1);
const float l_t = q_recip_2_sqrt_j * (sqrt_delta_v_0 * (v_1 + v_0)) - L;
// The return condition allows a minor error in length (in mm).
// Note: This comparison does NOT affect actual lengths or steps, which would be bad.
// The actual lengths traveled must be controlled by the caller.
if (fabs(l_t) < 0.001) {
break;
}
// For the first pass we tested velocity 0. If velocity 0 yields a l_t > 0,
// then we need to start searching at v_1 instead.
// (We can't start AT v_1, so we start at v_1 - 0.1)
if (i==1 && (l_t > 0)) {
v_1 = v_0 - 0.1;
continue;
}
const float v_1x3 = 3 * v_1;
const float recip_l_t = (2 * sqrt_delta_v_0) / ((v_0 - v_1x3) * q_recip_2_sqrt_j);
v_1 = v_1 - (l_t * recip_l_t);
}
return v_1;
}
/*
* _get_meet_velocity() - find intersection velocity
*
* This function, when given two velocities (v_0 and v_2) along with a length (L)
* and jerk (J), will locate the velocity v_1 that will allow acceleration from v_0
* at jerk J to v_1 and then deceleration at jerk J to v_2, all over total length L.
*
*/
static float _get_meet_velocity(const float v_0,
const float v_2,
const float L,
mpBuf_t* bf,
mpBlockRuntimeBuf_t* block)
{
const float q_recip_2_sqrt_j = bf->q_recip_2_sqrt_j;
// v_1 can never be smaller than v_0 or v_2, so we keep track of this value
const float min_v_1 = max(v_0, v_2);
// v_1 is our estimated return value.
// We estimate with the speed obtained by L/2 traveled from the highest speed of v_0 or v_2.
float v_1 = mp_get_target_velocity(min_v_1, L / 2.0, bf);
// var v_1 = min_v_1 + 100;
if (fp_EQ(v_0, v_2)) {
// Case (1)
// We can catch a symmetric case early and return now
// We'll have a head roughly equal to the tail, and no body
block->head_length = L / 2.0;
block->body_length = 0;
block->tail_length = L - block->head_length;
SET_PLANNER_ITERATIONS(-1); // DIAGNOSTIC
return v_1;
}
// Per iteration: 2 sqrt, 2 abs, 6 -, 4 +, 12 *, 3 /
int i = 0; // limit the iterations // 466us - 644us
while (i++ < 30) { // If it fails after 30, something's wrong
if (v_1 < min_v_1) {
// Case (2)
// We have caught a rather nasty problem. There is no meet velocity.
// This is due to an inversion in the velocities of very short moves.
// We need to compute the head OR tail length, and the body will be the rest.
// Yes, that means we're computing a cruise in here.
v_1 = min_v_1;
if (v_0 < v_2) {
// acceleration - it'll be a head/body
block->head_length = mp_get_target_length(v_0, v_2, bf);
if (block->head_length > L) {
block->head_length = L;
block->body_length = 0;
v_1 = mp_get_target_velocity(v_0, L, bf);
} else {
block->body_length = L - block->head_length;
}
block->tail_length = 0;
} else {
// deceleration - it'll be tail/body
block->tail_length = mp_get_target_length(v_2, v_0, bf);
if (block->tail_length > L) {
block->tail_length = L;
block->body_length = 0;
v_1 = mp_get_target_velocity(v_2, L, bf);
} else {
block->body_length = L - block->tail_length;
}
block->head_length = 0;
}
break;
}
// Precompute some common chunks -- note that some attempts may have v_1 < v_0 or v_1 < v_2
const float sqrt_delta_v_0 = sqrt(fabs(v_1 - v_0));
const float sqrt_delta_v_2 = sqrt(fabs(v_1 - v_2)); // 849us
// l_c is our total-length calculation with the current v_1 estimate, minus the expected length.
// This makes l_c == 0 when v_1 is the correct value.
// GAMBLE: At the cost of one more multiply per iteration, we will keep the two length calculations seperate.
// This allows us to store the resulting head/tail lengths.
const float l_h = q_recip_2_sqrt_j * (sqrt_delta_v_0 * (v_1 + v_0));
const float l_t = q_recip_2_sqrt_j * (sqrt_delta_v_2 * (v_1 + v_2));
const float l_c = (l_h + l_t) - L;
block->head_length = l_h;
block->tail_length = l_t;
block->body_length = 0;
// We need this level of precision, or our length computations fail to match the block length.
// What we really want to ensure is that the two lengths down add up to be too much.
// We can be a little under (and have a small body).
// TODO: make these tunable
if ((l_c < 0.00001) && (l_c > -1.0)) { // allow 0.00001 overlap, OR up to a 1mm gap
if (l_c < 0.0) {
// Case (3a)
block->body_length = -l_c;
} else {
// Case (3b)
// fix the overlap
block->tail_length = L - block->head_length;
}
break;
}
const float v_1x3 = 3 * v_1;
const float recip_l_d = (2 * sqrt_delta_v_0 * sqrt_delta_v_2) /
((sqrt_delta_v_0 * (v_1x3 - v_2) - (v_0 - v_1x3) * sqrt_delta_v_2) * q_recip_2_sqrt_j);
v_1 = v_1 - (l_c * recip_l_d);
}
SET_MEET_ITERATIONS(i); // DIAGNOSTIC
return v_1;
}