Skip to content

Commit 937ada7

Browse files
author
Luca Toniolo
committed
fix velocity spikes at segment junctions under feed override
Two root causes addressed: 1. Backward-pass missing kink constraint (motion_planning_9d.cc): The backward pass computed prev_tc's final_vel without applying tc->kink_vel (the junction kink at the prev_tc→tc boundary). Only prev_tc->kink_vel (its own entry kink) was applied, leaving the predecessor free to exit faster than the downstream junction allows. Add tc->kink_vel as Constraint 4 in tpComputeOptimalVelocity_9D so the predecessor's exit is always capped to the physical junction limit. 2. Stale-feed profile v0 mismatch at handoff (tp.c, tc_types.h): When feed override changes after a profile is written but before RT reaches that segment, profile.v[0] reflects the old feed while the actual junction velocity reflects the new feed. tpUpdateCycle samples the profile at t=0, snapping currentvel to the stale v0. Fix: stamp each profile with written_at_feed (the committed feed at write time). At split-cycle handoff, when the feed drift exceeds 5%, clamp nexttc->currentvel to the physical junction_vel and correct progress/position_base proportionally. Userspace re-converges a fresh profile within 1-2 cycles.
1 parent 681d94e commit 937ada7

File tree

3 files changed

+52
-42
lines changed

3 files changed

+52
-42
lines changed

src/emc/motion_planning/motion_planning_9d.cc

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1116,10 +1116,12 @@ double tpComputeOptimalVelocity_9D(TC_STRUCT const * const tc,
11161116
// All inputs are in physical mm/s (feed-scaled). jerkLimitedMaxEntryVelocity
11171117
// is nonlinear (braking distance ∝ v²), so computing in scaled space avoids
11181118
// the systematic gap that appears when the forward pass re-checks at feed≠1.
1119-
// Use trapezoidal for v_f≈0 (tail/STOP segments) for fast velocity recovery
1120-
// in the backward chain.
1119+
// Always use jerk-limited formula, including v_f=0 (STOP/EXACT segments).
1120+
// Trapezoidal (sqrt(2*a*d)) ignores jerk and overestimates the safe approach
1121+
// velocity for short STOP segments — the predecessor gets too-high final_vel,
1122+
// which the forward pass then delivers to the STOP segment causing overshoot.
11211123
double vs_back;
1122-
if (jrk_this > 0.0 && v_f_this > 1e-6) {
1124+
if (jrk_this > 0.0) {
11231125
vs_back = jerkLimitedMaxEntryVelocity(v_f_this, tc->target, acc_this, jrk_this);
11241126
} else {
11251127
vs_back = sqrt(v_f_this * v_f_this + 2.0 * acc_this * tc->target);
@@ -1128,13 +1130,19 @@ double tpComputeOptimalVelocity_9D(TC_STRUCT const * const tc,
11281130
// Constraint 2: Current segment velocity limit (scaled to physical mm/s)
11291131
double vf_limit_this = tcGetPlanMaxTargetVel(tc, feed_tc);
11301132

1131-
// Constraint 3: Previous segment velocity limit (with kink)
1132-
// kink_vel is an absolute physical limit — applyKinkVelLimit caps correctly
1133+
// Constraint 3: Previous segment velocity limit (with kink at its own entry)
1134+
// prev_tc->kink_vel is the kink at the junction before prev_tc.
11331135
double v_max_prev = tcGetPlanMaxTargetVel(prev_tc, feed_prev);
11341136
double vf_limit_prev = applyKinkVelLimit(prev_tc, v_max_prev);
11351137

1138+
// Constraint 4: Junction kink at the prev_tc→tc boundary.
1139+
// tc->kink_vel is the physical kink limit for *entering* tc, which equals
1140+
// the *exit* velocity of prev_tc. The backward pass must enforce this here
1141+
// so prev_tc's final_vel never exceeds the junction constraint ahead of it.
1142+
double vf_kink_tc = (tc->kink_vel > 0.0) ? tc->kink_vel : 1e9;
1143+
11361144
// Return minimum of all constraints
1137-
double v_optimal = std::min({vs_back, vf_limit_this, vf_limit_prev});
1145+
double v_optimal = std::min({vs_back, vf_limit_this, vf_limit_prev, vf_kink_tc});
11381146

11391147
return v_optimal;
11401148
}
@@ -1554,6 +1562,7 @@ static bool computeAndStoreProfile(TC_STRUCT *tc, const RuckigProfileParams &p)
15541562
tc->shared_9d.profile.computed_vLimit = p.vLimit;
15551563
tc->shared_9d.profile.computed_desired_fvel = p.desired_fvel;
15561564
tc->shared_9d.profile.locked = (result == ruckig::Result::Working) ? 1 : 0;
1565+
tc->shared_9d.profile.written_at_feed = g_feed_mgr.committed_feed;
15571566
copyRuckigProfile(traj, &tc->shared_9d.profile);
15581567
return true;
15591568
} else {
@@ -2289,7 +2298,6 @@ static int replanForward(TP_STRUCT *tp, double v0_override, double budget_sec)
22892298
scaled_v_entry = fmin(scaled_v_entry, prev_tc->kink_vel);
22902299
}
22912300
}
2292-
22932301
// Target exit velocity from backward pass
22942302
double v_exit = (tc->term_cond == TC_TERM_COND_TANGENT)
22952303
? readFinalVelCapped(tc) : 0.0;
@@ -2441,7 +2449,12 @@ static int replanForward(TP_STRUCT *tp, double v0_override, double budget_sec)
24412449
bool bidir_restore_fired = false;
24422450
if (pre_bidir_v_entry - scaled_v_entry > 0.1 && i > 0) {
24432451
TC_STRUCT *prev_tc_fix = tcqItem_user(queue, i - 1);
2444-
if (prev_tc_fix && (prev_tc_fix->active || prev_was_corridor)) {
2452+
// Don't restore for STOP/EXACT: the machine must stop regardless.
2453+
// Restoring a high v_entry and then setting v_exit=min_exit>0 produces
2454+
// a profile that doesn't stop at a STOP segment — causing overshoot.
2455+
// Accept the v0-gap spike instead; it's less bad than position overshoot.
2456+
if (prev_tc_fix && (prev_tc_fix->active || prev_was_corridor) &&
2457+
tc->term_cond == TC_TERM_COND_TANGENT) {
24452458
scaled_v_entry = pre_bidir_v_entry;
24462459
// Override exit with physics-based minimum from braking.
24472460
double min_exit = jerkLimitedMinExitVelocity(

src/emc/tp/tc_types.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,7 @@ typedef struct {
210210
volatile int generation; // Incremented each time profile is rewritten (stopwatch reset detection)
211211
double duration; // Total profile duration (seconds)
212212
double computed_feed_scale; // Feed scale when profile was computed
213+
double written_at_feed; // committed_feed when profile was written (planner-side)
213214
double computed_vel_limit; // Velocity limit when profile was computed (for vLimit change detection)
214215
double computed_vLimit; // tp->vLimit when profile was computed (absolute cap, not scaled by feed)
215216
double computed_desired_fvel; // Desired final velocity when profile was computed (for convergence detection)

src/emc/tp/tp.c

Lines changed: 30 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -4917,7 +4917,6 @@ STATIC int tpHandleSplitCycle(TP_STRUCT * const tp, TC_STRUCT * const tc,
49174917
double maxacc_next = tcGetTangentialMaxAccel(nexttc);
49184918
nexttc->currentacc = saturate(tc->currentacc, maxacc_next);
49194919

4920-
49214920
} else {
49224921
// Trapezoidal: can use term_vel (assumes instant velocity change)
49234922
nexttc->currentvel = tc->term_vel;
@@ -5127,44 +5126,41 @@ STATIC int tpHandleSplitCycle(TP_STRUCT * const tp, TC_STRUCT * const tc,
51275126
}
51285127
}
51295128

5130-
// Correct profile v0 mismatch at split junction.
5129+
// Stale-feed v0 clamp at split junction.
51315130
// When feed override changes between profile computation and junction
5132-
// arrival, the profile's v[0] doesn't match the actual junction velocity.
5133-
// tpUpdateCycle samples the profile and overwrites currentvel with the
5134-
// profile velocity, creating a discontinuity. Correct by shifting
5135-
// velocity and position by the mismatch delta. Both progress and
5136-
// position_base are shifted equally so subsequent cycle displacements
5137-
// (which depend on delta-progress) are unaffected.
5138-
// Correct positive v0 mismatch (junction_vel > profile_v0), capped at
5139-
// one jerk step (maxjerk * dt²) so the velocity correction on cycle 2
5140-
// (when the profile takes over) stays within the segment's jerk limit.
5141-
// Negative corrections (junction_vel < profile_v0) are NOT applied:
5142-
// they shift position_base negative, which causes the segment to land
5143-
// short of target and trigger a 10-cycle stuck detection stall — worse
5144-
// than the velocity step itself for small mismatches, and ineffective
5145-
// for large ones (e.g. feed hold recovery with 40 mm/s gap).
5146-
// v0_correction DISABLED for testing — OLD code had none.
5147-
// When enabled, this shifts currentvel/progress/position_base to partially
5148-
// absorb junction velocity mismatch, but the position_base discontinuity
5149-
// itself may cause jerk spikes visible in halscope.
5150-
#if 0
5131+
// arrival, profile.v[0] may not match the actual junction velocity.
5132+
// tpUpdateCycle samples the profile at t=0 and overwrites currentvel
5133+
// with profile.v[0], creating a discontinuity.
5134+
//
5135+
// Gate: only apply when the profile's written_at_feed differs from the
5136+
// current canonical_feed_scale by more than 5%. This avoids perturbing
5137+
// normal (feed-consistent) profile handoffs.
5138+
//
5139+
// Correction: clamp nexttc->currentvel to junction_vel and shift
5140+
// progress/position_base by the proportional first-cycle displacement
5141+
// correction so the path position is consistent with the clamped velocity.
5142+
// Userspace will write a fresh profile at the new feed within 1-2 cycles.
51515143
if (GET_TRAJ_PLANNER_TYPE() == 2 &&
51525144
__atomic_load_n(&nexttc->shared_9d.profile.valid, __ATOMIC_ACQUIRE)) {
5153-
double profile_v0 = nexttc->shared_9d.profile.v[0];
5154-
double vel_mismatch = junction_vel - profile_v0;
5155-
if (vel_mismatch > 0.01) {
5156-
double dt = tp->cycleTime;
5157-
double max_pos_corr = nexttc->maxjerk * dt * dt;
5158-
if (vel_mismatch > max_pos_corr)
5159-
vel_mismatch = max_pos_corr;
5160-
nexttc->currentvel += vel_mismatch;
5161-
// Correct split-cycle displacement to reflect actual velocity
5162-
double pos_correction = vel_mismatch * nexttc_remain_time;
5163-
nexttc->progress += pos_correction;
5164-
nexttc->position_base += pos_correction;
5145+
double _wf = nexttc->shared_9d.profile.written_at_feed;
5146+
double _cf = nexttc->shared_9d.canonical_feed_scale;
5147+
if (_cf > 0.001 && _wf > 0.001 && fabs(_wf - _cf) / _cf > 0.05) {
5148+
double profile_v0 = nexttc->shared_9d.profile.v[0];
5149+
double vel_mismatch = junction_vel - profile_v0;
5150+
if (fabs(vel_mismatch) > 0.5) {
5151+
// Correct currentvel to physical junction velocity
5152+
nexttc->currentvel = junction_vel;
5153+
// Correct first-cycle displacement: was profile_v0*remain_time,
5154+
// should be junction_vel*remain_time.
5155+
double pos_correction = vel_mismatch * nexttc_remain_time;
5156+
nexttc->progress += pos_correction;
5157+
nexttc->position_base += pos_correction;
5158+
// Clamp progress to [0, target] to be safe
5159+
if (nexttc->progress < 0.0) nexttc->progress = 0.0;
5160+
if (nexttc->progress > nexttc->target) nexttc->progress = nexttc->target;
5161+
}
51655162
}
51665163
}
5167-
#endif
51685164

51695165
// Post-correct elapsed_time for Ruckig split cycle.
51705166
// tpUpdateCycle sampled at remain_time then advanced by remain_time internally

0 commit comments

Comments
 (0)