From 9a61d8df709f02b031998ffd67c98ef3315b4eae Mon Sep 17 00:00:00 2001 From: Luca Toniolo <10792599+grandixximo@users.noreply.github.com> Date: Sat, 22 Aug 2026 00:52:11 +1000 Subject: [PATCH 1/7] tp: fix velocity jitter in spindle position sync The error correction added sqrt(x_err * a_max) to the tracking velocity. That is the velocity needed to close the error from a standstill, so it is only right when the tracking velocity is zero, which during spindle tracking it never is. It over-corrected, and its gain diverged as the error went to zero, so the loop limit-cycled at the servo rate: the axis chattered and the acceleration slammed between its limits every cycle even with a noise-free encoder. Use v_p = sqrt(v_0^2 + x_err * a_max), which accounts for the non-zero tracking velocity. In simulation the jitter goes away entirely with an ideal encoder; at 256 count/rev the acceleration standard deviation drops from 38.3 to 7.4 and the RMS tracking error from 1.25e-4 to 7.7e-5 in. Formula by Robert W. Ellenberg, from the unmerged PR #581. Fixes #164 --- src/emc/tp/tp.c | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/src/emc/tp/tp.c b/src/emc/tp/tp.c index b193b76fb83..f657a729cac 100644 --- a/src/emc/tp/tp.c +++ b/src/emc/tp/tp.c @@ -3616,14 +3616,31 @@ STATIC void tpSyncPositionMode(TP_STRUCT * const tp, TC_STRUCT * const tc, // we have synced the beginning of the move as best we can - // track position (minimize pos_error). tc_debug_print("tracking in pos_sync\n"); - double errorvel; spindle_vel = (tp->spindle.revs - oldrevs) / tp->cycleTime; target_vel = spindle_vel * tc->uu_per_rev; - errorvel = pmSqrt(fabs(pos_error) * tcGetTangentialMaxAccel(tc)); - if(pos_error<0) { - errorvel *= -1.0; - } - tc->target_vel = target_vel + errorvel; + + /* Correct the position error without losing the spindle: rise above + * the tracking velocity v_0 and come back to it, so the area of the + * blip is the error. + * + * velocity + * | v_p + * | /\ + * | /..\ v_0 + * |--------....----------- + * | .... + * | .... + * |_________________________ + * |----| t time + * + * That gives v_p = sqrt(v_0^2 + x_err*a_max). The + * old form added sqrt(x_err*a_max) to v_0, which is the same with v_0 + * taken as zero, so it over-corrected and its gain diverged as the + * error went to zero, limit-cycling at the servo rate. + * From robEllenberg, PR #581. */ + double a_max = tcGetTangentialMaxAccel(tc); + double v_sq = pmSq(target_vel) + pos_error * a_max; + tc->target_vel = pmSqrt(fmax(v_sq, 0.0)); } //Finally, clip requested velocity at zero From ac6cb5466d6bc394ea2f47d117aecba6ebb2737f Mon Sep 17 00:00:00 2001 From: Luca Toniolo <10792599+grandixximo@users.noreply.github.com> Date: Sat, 22 Aug 2026 00:53:38 +1000 Subject: [PATCH 2/7] interp: reject a thread pitch the axes cannot follow G33, G33.1 and G76 take the axis feed from pitch times spindle speed, and nothing rejected a feed the machine cannot deliver: the planner clamped the velocity, the axis fell behind, and the thread was cut wrong with no message. Check the pitch against the per-axis maximum velocity at interpret time, so the offending line is named before it cuts. The feed is projected onto each axis by its share of the move length, as the planner distributes it. The bound is the per-axis limit rather than the traj maximum because tpGetMaxTargetVel exempts position-synced moves from the max velocity slider, so the traj value would reject valid programs with the slider down. G33.1 uses K times I, since I multiplies the retract speed. Needs a new GET_EXTERNAL_AXIS_MAX_VELOCITY canon call. The standalone interpreter has no machine and reports zero, which skips the check. Refs #4391, #167 --- docs/src/gcode/g-code.adoc | 16 +++--- src/emc/nml_intf/canon.hh | 4 ++ src/emc/rs274ngc/canonmodule.cc | 1 + src/emc/rs274ngc/gcodemodule.cc | 1 + src/emc/rs274ngc/interp_check.cc | 77 +++++++++++++++++++++++++++++ src/emc/rs274ngc/interp_convert.cc | 42 +++++++++++++++- src/emc/rs274ngc/rs274ngc_interp.hh | 2 + src/emc/sai/saicanon.cc | 6 +++ src/emc/task/emccanon.cc | 15 ++++++ 9 files changed, 153 insertions(+), 11 deletions(-) diff --git a/docs/src/gcode/g-code.adoc b/docs/src/gcode/g-code.adoc index 6253e57afb2..e110e31d62e 100644 --- a/docs/src/gcode/g-code.adoc +++ b/docs/src/gcode/g-code.adoc @@ -1113,11 +1113,8 @@ It is an error if: * An F word is given (the feed follows from K and the spindle speed). * The selected spindle is not commanded to turn (M3 or M4 active) when this command is executed. - -[NOTE] -The pitch and the spindle speed together set the axis feed. If they ask for -more than the machine can deliver, the program is not rejected: there is no -interpreter check and no error message for that case. +* The pitch and the commanded spindle speed together ask an axis to move + faster than its maximum velocity. [[gcode:g33.1]] == G33.1 Rigid Tapping(((G33.1 Rigid Tapping))) @@ -1186,11 +1183,8 @@ It is an error if: * An F word is given (the feed follows from K and the spindle speed). * The selected spindle is not commanded to turn (M3 or M4 active) when this command is executed. - -[NOTE] -The pitch and the spindle speed together set the axis feed. If they ask for -more than the machine can deliver, the program is not rejected: there is no -interpreter check and no error message for that case. +* The pitch and the commanded spindle speed together ask an axis to move + faster than its maximum velocity. [[gcode:g38]] == G38._n_ Straight Probe(((G38.n Probe))) @@ -2046,6 +2040,8 @@ It is an error if: * All the required words are not specified. * 'P-', 'J-', 'K-' or 'H-' is negative. * 'E-' is greater than half the drive line length. +* The pitch and the commanded spindle speed together ask an axis to move + faster than its maximum velocity. .HAL Connections The pins 'spindle.N.at-speed' and the 'encoder._n_.phase-Z' for the diff --git a/src/emc/nml_intf/canon.hh b/src/emc/nml_intf/canon.hh index 916b3e92971..05e21ef106f 100644 --- a/src/emc/nml_intf/canon.hh +++ b/src/emc/nml_intf/canon.hh @@ -874,6 +874,10 @@ below. extern double GET_EXTERNAL_ANGLE_UNIT_FACTOR(); */ +// Returns the maximum velocity of one axis, indexed 0-8 as XYZABCUVW, in +// program units per minute, or zero if that limit is not available +extern double GET_EXTERNAL_AXIS_MAX_VELOCITY(int axis); + // Returns the system feed rate extern double GET_EXTERNAL_FEED_RATE(); diff --git a/src/emc/rs274ngc/canonmodule.cc b/src/emc/rs274ngc/canonmodule.cc index 3399fb2fc91..8e32184575e 100644 --- a/src/emc/rs274ngc/canonmodule.cc +++ b/src/emc/rs274ngc/canonmodule.cc @@ -178,6 +178,7 @@ BOOST_PYTHON_MODULE(emccanon) { def("GET_EXTERNAL_TOOL_LENGTH_ZOFFSET",&GET_EXTERNAL_TOOL_LENGTH_ZOFFSET); def("GET_EXTERNAL_TOOL_SLOT",&GET_EXTERNAL_TOOL_SLOT); def("GET_EXTERNAL_TOOL_TABLE",&GET_EXTERNAL_TOOL_TABLE); + def("GET_EXTERNAL_AXIS_MAX_VELOCITY",&GET_EXTERNAL_AXIS_MAX_VELOCITY); def("GET_EXTERNAL_TRAVERSE_RATE",&GET_EXTERNAL_TRAVERSE_RATE); def("GET_OPTIONAL_PROGRAM_STOP",&GET_OPTIONAL_PROGRAM_STOP); def("INIT_CANON",&INIT_CANON); diff --git a/src/emc/rs274ngc/gcodemodule.cc b/src/emc/rs274ngc/gcodemodule.cc index 3b15edea612..bdfd976240e 100644 --- a/src/emc/rs274ngc/gcodemodule.cc +++ b/src/emc/rs274ngc/gcodemodule.cc @@ -1064,6 +1064,7 @@ CANON_DIRECTION GET_EXTERNAL_SPINDLE(int) { return CANON_STOPPED; } int GET_EXTERNAL_TOOL_SLOT() { return 0; } int GET_EXTERNAL_SELECTED_TOOL_SLOT() { return 0; } double GET_EXTERNAL_FEED_RATE() { return 1; } +double GET_EXTERNAL_AXIS_MAX_VELOCITY(int /*axis*/) { return 0; } double GET_EXTERNAL_TRAVERSE_RATE() { return 0; } int GET_EXTERNAL_FLOOD() { return 0; } int GET_EXTERNAL_MIST() { return 0; } diff --git a/src/emc/rs274ngc/interp_check.cc b/src/emc/rs274ngc/interp_check.cc index 196f2772763..6eb889f5527 100644 --- a/src/emc/rs274ngc/interp_check.cc +++ b/src/emc/rs274ngc/interp_check.cc @@ -393,3 +393,80 @@ int Interp::check_other_codes(block_pointer block) //!< pointer to a block return INTERP_OK; } + +/****************************************************************************/ + +/*! check_spindle_sync_feed + +Returned Value: int + Returns an error if any axis of the move would have to run faster than its + maximum velocity to hold the commanded pitch at the commanded spindle speed. + Otherwise returns INTERP_OK. + +Side effects: none + +Called by: + Interp::convert_straight (G33, G33.1) + Interp::convert_threading_cycle (G76) + +Nothing downstream rejects a feed the machine cannot deliver: the planner +clamps the velocity, the axis falls behind, and the thread is cut wrong. + +The bound is the per-axis maximum, not the traj maximum, because the max +velocity slider is deliberately not applied to position-synchronized moves (see +tpGetMaxTargetVel). The feed is projected onto each axis by its share of the +move length, as the planner distributes it. Rotary axes are ignored: a pitch +is a linear distance per revolution. + +Using the commanded S word means the error names the offending line and does +not depend on the spindle already running. Skipped in constant surface speed +mode, and for any axis whose limit is unavailable (the standalone interpreter +reports zero). + +*/ + +int Interp::check_spindle_sync_feed(setup_pointer settings, //!< pointer to machine settings + double pitch, //!< program units per revolution + const char *code, //!< G code name, for the message + const double delta[9]) //!< move, program units, XYZABCUVW +{ + static const char axis_name[] = "XYZABCUVW"; + int spindle = settings->active_spindle; + + if (settings->spindle_mode[spindle] != SPINDLE_MODE::CONSTANT_RPM) + return INTERP_OK; + + double speed = settings->speed[spindle]; + if (speed <= 0.0 || pitch == 0.0) + return INTERP_OK; + + double length = 0.0; + for (int ax = 0; ax < 9; ax++) { + if (ax >= 3 && ax <= 5) + continue; /* rotary */ + length += delta[ax] * delta[ax]; + } + length = sqrt(length); + if (length <= 0.0) + return INTERP_OK; + + /* program units per minute along the path */ + double required_rate = fabs(pitch) * speed; + + for (int ax = 0; ax < 9; ax++) { + if (ax >= 3 && ax <= 5) + continue; + if (delta[ax] == 0.0) + continue; + double max_rate = GET_EXTERNAL_AXIS_MAX_VELOCITY(ax); + if (max_rate <= 0.0) + continue; + double axis_rate = required_rate * fabs(delta[ax]) / length; + CHKS((axis_rate > max_rate), + _("%s pitch %g at spindle speed %g needs %g per minute on the %c axis, " + "which exceeds its maximum velocity of %g"), + code, fabs(pitch), speed, axis_rate, axis_name[ax], max_rate); + } + + return INTERP_OK; +} diff --git a/src/emc/rs274ngc/interp_convert.cc b/src/emc/rs274ngc/interp_convert.cc index 638e87eb0c3..9784807b19f 100644 --- a/src/emc/rs274ngc/interp_convert.cc +++ b/src/emc/rs274ngc/interp_convert.cc @@ -5114,6 +5114,26 @@ int Interp::convert_spindle_mode(int dollar_number, block_pointer block, setup_p } return INTERP_OK; } + +/* Displacement of a move, ordered XYZABCUVW. */ + +static void sync_move_delta(setup_pointer settings, + double end_x, double end_y, double end_z, + double AA_end, double BB_end, double CC_end, + double u_end, double v_end, double w_end, + double delta[9]) +{ + delta[0] = end_x - settings->current_x; + delta[1] = end_y - settings->current_y; + delta[2] = end_z - settings->current_z; + delta[3] = AA_end - settings->AA_current; + delta[4] = BB_end - settings->BB_current; + delta[5] = CC_end - settings->CC_current; + delta[6] = u_end - settings->u_current; + delta[7] = v_end - settings->v_current; + delta[8] = w_end - settings->w_current; +} + /****************************************************************************/ /*! convert_stop @@ -5526,6 +5546,10 @@ int Interp::convert_straight(int move, //!< either G_0 or G_1 CHKS(((settings->spindle_turning[settings->active_spindle] != CANON_CLOCKWISE) && (settings->spindle_turning[settings->active_spindle] != CANON_COUNTERCLOCKWISE)), _("Spindle not turning in G33")); + double delta[9]; + sync_move_delta(settings, end_x, end_y, end_z, AA_end, BB_end, CC_end, + u_end, v_end, w_end, delta); + CHP(check_spindle_sync_feed(settings, block->k_number, "G33", delta)); START_SPEED_FEED_SYNCH(settings->active_spindle, block->k_number, 0); STRAIGHT_FEED(block->line_number, end_x, end_y, end_z, AA_end, BB_end, CC_end, u_end, v_end, w_end); STOP_SPEED_FEED_SYNCH(); @@ -5541,7 +5565,6 @@ int Interp::convert_straight(int move, //!< either G_0 or G_1 CHKS(((settings->spindle_turning[settings->active_spindle] != CANON_CLOCKWISE) && (settings->spindle_turning[settings->active_spindle] != CANON_COUNTERCLOCKWISE)), _("Spindle not turning in G33.1")); - START_SPEED_FEED_SYNCH(settings->active_spindle, block->k_number, 0); double scale = 1; if(block->i_flag){ scale = block->i_number; @@ -5549,6 +5572,13 @@ int Interp::convert_straight(int move, //!< either G_0 or G_1 scale = 1; } } + double delta[9]; + sync_move_delta(settings, end_x, end_y, end_z, AA_end, BB_end, CC_end, + u_end, v_end, w_end, delta); + // I multiplies the spindle speed for the retract + CHP(check_spindle_sync_feed(settings, block->k_number * scale, "G33.1", + delta)); + START_SPEED_FEED_SYNCH(settings->active_spindle, block->k_number, 0); RIGID_TAP(block->line_number, end_x, end_y, end_z, scale); STOP_SPEED_FEED_SYNCH(); // after the RIGID_TAP cycle we'll be in the same spot @@ -5771,6 +5801,16 @@ int Interp::convert_threading_cycle(block_pointer block, double target_z = end_z + fabs(k_number) * tan(compound_angle); + // A taper also moves X by the thread height over the taper distance, at + // the correspondingly larger pitch. + double plain_pass[9] = {0.0, 0.0, target_z - start_z, 0, 0, 0, 0, 0, 0}; + CHP(check_spindle_sync_feed(settings, pitch, "G76", plain_pass)); + if (taper_dist != 0.0 && (entry_taper || exit_taper)) { + double taper_pass[9] = {full_threadheight, 0.0, taper_dist, + 0, 0, 0, 0, 0, 0}; + CHP(check_spindle_sync_feed(settings, taper_pitch, "G76", taper_pass)); + } + depth = start_depth; zoff = (depth - full_dia_depth) * tan(compound_angle); while (depth < end_depth) { diff --git a/src/emc/rs274ngc/rs274ngc_interp.hh b/src/emc/rs274ngc/rs274ngc_interp.hh index af7d27e58bc..21ee6c02156 100644 --- a/src/emc/rs274ngc/rs274ngc_interp.hh +++ b/src/emc/rs274ngc/rs274ngc_interp.hh @@ -223,6 +223,8 @@ public: int check_items(block_pointer block, setup_pointer settings); int check_m_codes(block_pointer block); int check_other_codes(block_pointer block); + int check_spindle_sync_feed(setup_pointer settings, double pitch, + const char *code, const double delta[9]); int close_and_downcase(char *line); void nurbs_reset_global_variables(void); int convert_nurbs(int move, block_pointer block, setup_pointer settings); diff --git a/src/emc/sai/saicanon.cc b/src/emc/sai/saicanon.cc index 169e73a8a39..a4e0bb76b16 100644 --- a/src/emc/sai/saicanon.cc +++ b/src/emc/sai/saicanon.cc @@ -968,6 +968,12 @@ extern CANON_TOOL_TABLE GET_EXTERNAL_TOOL_TABLE(int idx) #endif //} } +/* The standalone interpreter has no machine, so no axis limits */ +double GET_EXTERNAL_AXIS_MAX_VELOCITY(int /*axis*/) +{ + return 0.0; +} + /* Returns the system traverse rate */ double GET_EXTERNAL_TRAVERSE_RATE() { diff --git a/src/emc/task/emccanon.cc b/src/emc/task/emccanon.cc index a5f45837c99..36a95e81696 100644 --- a/src/emc/task/emccanon.cc +++ b/src/emc/task/emccanon.cc @@ -3813,6 +3813,21 @@ double GET_EXTERNAL_FEED_RATE() return feed; } +// maximum velocity of one axis, in program units per minute +double GET_EXTERNAL_AXIS_MAX_VELOCITY(int axis) +{ + if (axis < 0 || axis > 8 || !axis_valid(axis)) { + return 0.0; + } + + double vel = emcAxisGetMaxVelocity(axis); + + if (axis >= 3 && axis <= 5) { + return TO_PROG_ANG(FROM_EXT_ANG(vel)) * 60.0; + } + return TO_PROG_LEN(FROM_EXT_LEN(vel)) * 60.0; +} + // traverse rate wanted is in program units per minute double GET_EXTERNAL_TRAVERSE_RATE() { From da20d147c2df42c36b6acfb88add294a27127b73 Mon Sep 17 00:00:00 2001 From: Luca Toniolo <10792599+grandixximo@users.noreply.github.com> Date: Sat, 22 Aug 2026 00:53:57 +1000 Subject: [PATCH 3/7] motion: fault when the spindle outruns a synchronized move The interpreter check only sees the commanded S word. The spindle can still outrun the axis at run time through the spindle override, constant surface speed, or a drive that overshoots, and the planner has no answer but to clamp and cut a wrong thread in silence. Raise an error and abort instead. Demand is measured as revolutions turned across a 0.25 s window times the pitch, the one quantity here with no transient in it: the tracking error carries the v^2/2a lag every G33 picks up while the axis ramps up from rest, and a single-cycle spindle velocity is buried in encoder quantization noise. The axis must also be pinned at its ceiling, which excludes the ramp at the start and the stop on the endpoint at the end. The planner is a separate module and can neither report to the operator nor abort re-entrantly from its own cycle, so it records the fault in emcmotStatus and the controller raises it. Refs #4391 --- src/emc/motion/control.c | 11 +++++++ src/emc/motion/motion.c | 2 ++ src/emc/motion/motion.h | 4 +++ src/emc/tp/tp.c | 65 +++++++++++++++++++++++++++++++++++++++- src/emc/tp/tp_types.h | 13 ++++++++ 5 files changed, 94 insertions(+), 1 deletion(-) diff --git a/src/emc/motion/control.c b/src/emc/motion/control.c index 2ddf587b484..5bc132f9b37 100644 --- a/src/emc/motion/control.c +++ b/src/emc/motion/control.c @@ -1349,6 +1349,17 @@ static void get_pos_cmds(long period) /* run coordinated trajectory planning cycle */ tpRunCycle(&emcmotInternal->coord_tp, period); + + if (emcmotStatus->syncOverrunSpindle) { + tpAbort(&emcmotInternal->coord_tp); + reportError(_("spindle-synchronized move exceeds axis limits: " + "spindle %d is outrunning the axis by %f per " + "second, reduce the spindle speed or the pitch"), + emcmotStatus->syncOverrunSpindle - 1, + emcmotStatus->syncOverrunError); + emcmotStatus->syncOverrunSpindle = 0; + SET_MOTION_ERROR_FLAG(1); + } /* get new commanded traj pos */ tpGetPos(&emcmotInternal->coord_tp, &emcmotStatus->carte_pos_cmd); diff --git a/src/emc/motion/motion.c b/src/emc/motion/motion.c index d2cb7615958..f3eef62aae4 100644 --- a/src/emc/motion/motion.c +++ b/src/emc/motion/motion.c @@ -890,6 +890,8 @@ static int init_comm_buffers(void) ZERO_EMC_POSE(emcmotStatus->carte_pos_cmd); ZERO_EMC_POSE(emcmotStatus->carte_pos_fb); emcmotStatus->vel = 0.0; + emcmotStatus->syncOverrunSpindle = 0; + emcmotStatus->syncOverrunError = 0.0; emcmotConfig->limitVel = 0.0; emcmotStatus->acc = 0.0; emcmotStatus->feed_scale = 1.0; diff --git a/src/emc/motion/motion.h b/src/emc/motion/motion.h index 1312b5e45dd..f31d4dedc0e 100644 --- a/src/emc/motion/motion.h +++ b/src/emc/motion/motion.h @@ -600,6 +600,10 @@ Suggestion: Split this in to an Error and a Status flag register.. emcmot_joint_status_t joint_status[EMCMOT_MAX_JOINTS]; /* all joint status data */ emcmot_axis_status_t axis_status[EMCMOT_MAX_AXIS]; /* all axis status data */ int spindleSync; /* spindle used for synchronised moves. -1 = none */ + int syncOverrunSpindle; /* spindle that outran the axis in a synced move, + plus one; 0 = none. Set by the planner, raised + by the motion controller. */ + double syncOverrunError; /* by how much per second */ spindle_status_t spindle_status[EMCMOT_MAX_SPINDLES]; /* all spindle data */ diff --git a/src/emc/tp/tp.c b/src/emc/tp/tp.c index f657a729cac..9cb2ee7276a 100644 --- a/src/emc/tp/tp.c +++ b/src/emc/tp/tp.c @@ -498,6 +498,9 @@ int tpInit(TP_STRUCT * const tp) tp->spindle.offset = 0.0; tp->spindle.revs = 0.0; + tp->spindle.overrun_cycles = 0; + tp->spindle.overrun_revs = 0.0; + tp->spindle.overrun_reported = 0; tp->spindle.waiting_for_index = MOTION_INVALID_ID; tp->spindle.waiting_for_atspeed = MOTION_INVALID_ID; @@ -3567,6 +3570,23 @@ STATIC void tpSyncVelocityMode(TP_STRUCT * const tp, TC_STRUCT * const tc, TC_ST } +/** + * Record a spindle-synchronized overrun for the motion controller to raise. + * The planner is a separate module and can neither report to the operator nor + * abort re-entrantly from inside its own cycle. + */ +STATIC void tpSyncOverrun(TP_STRUCT * const tp, double amount) +{ + if (tp->spindle.overrun_reported) { + return; /* one per move; it keeps slipping while it stops */ + } + emcmotStatus->syncOverrunSpindle = tp->spindle.spindle_num + 1; + emcmotStatus->syncOverrunError = amount; + tp->spindle.overrun_reported = 1; + tp->spindle.overrun_cycles = 0; +} + + /** * Run position mode synchronization. * Updates requested velocity for a trajectory segment to track the spindle's position. @@ -3611,6 +3631,15 @@ STATIC void tpSyncPositionMode(TP_STRUCT * const tp, TC_STRUCT * const tc, tc_debug_print("accelerating in pos_sync\n"); // beginning of move and we are behind: accel as fast as we can tc->target_vel = tc->maxvel; + + /* If the pitch at this speed needs more than the segment can + * deliver, the handoff above never happens and the whole move runs + * here clamped. spindle_vel is revs averaged over the move, so it + * is smooth enough to compare once settled. */ + if (tc->sync_accel * dt > TP_SYNC_OVERRUN_WINDOW && + target_vel > tc->maxvel * TP_SYNC_OVERRUN_MARGIN) { + tpSyncOverrun(tp, target_vel - tc->maxvel); + } } } else { // we have synced the beginning of the move as best we can - @@ -3618,7 +3647,6 @@ STATIC void tpSyncPositionMode(TP_STRUCT * const tp, TC_STRUCT * const tc, tc_debug_print("tracking in pos_sync\n"); spindle_vel = (tp->spindle.revs - oldrevs) / tp->cycleTime; target_vel = spindle_vel * tc->uu_per_rev; - /* Correct the position error without losing the spindle: rise above * the tracking velocity v_0 and come back to it, so the area of the * blip is the error. @@ -3638,9 +3666,41 @@ STATIC void tpSyncPositionMode(TP_STRUCT * const tp, TC_STRUCT * const tc, * taken as zero, so it over-corrected and its gain diverged as the * error went to zero, limit-cycling at the servo rate. * From robEllenberg, PR #581. */ + double dt = fmax(tp->cycleTime, TP_TIME_EPSILON); double a_max = tcGetTangentialMaxAccel(tc); double v_sq = pmSq(target_vel) + pos_error * a_max; tc->target_vel = pmSqrt(fmax(v_sq, 0.0)); + + /* Rigid tap reversals move the target by design, so watch only the + * tapping pass. */ + bool tap_reversing = (tc->motion_type == TC_RIGIDTAP) && + (tc->coords.rigidtap.state != TAPPING); + + /* Only an axis pinned at its ceiling can be outrun. Below it the + * error grows for reasons a correct G33 has anyway: the spindle turns + * while the axis ramps up, and again while it stops on the endpoint. */ + bool saturated = tc->currentvel >= tc->maxvel * TP_SYNC_OVERRUN_CEILING; + int window_cycles = (int)(TP_SYNC_OVERRUN_WINDOW / dt); + if (window_cycles < 1) { + window_cycles = 1; + } + + if (!tap_reversing && saturated) { + if (tp->spindle.overrun_cycles == 0) { + tp->spindle.overrun_revs = tp->spindle.revs; + } + if (++tp->spindle.overrun_cycles >= window_cycles) { + double window = window_cycles * dt; + double demand = fabs(tp->spindle.revs - tp->spindle.overrun_revs) + * fabs(tc->uu_per_rev) / window; + if (demand > tc->maxvel * TP_SYNC_OVERRUN_MARGIN) { + tpSyncOverrun(tp, demand - tc->maxvel); + } + tp->spindle.overrun_cycles = 0; + } + } else { + tp->spindle.overrun_cycles = 0; + } } //Finally, clip requested velocity at zero @@ -4223,6 +4283,9 @@ int tpSetSpindleSync(TP_STRUCT * const tp, int spindle, double sync, int mode) { } tp->uu_per_rev = sync; tp->spindle.spindle_num = spindle; + /* each synced move may report again */ + tp->spindle.overrun_reported = 0; + tp->spindle.overrun_cycles = 0; } else tp->synchronized = 0; diff --git a/src/emc/tp/tp_types.h b/src/emc/tp/tp_types.h index 6687ef3a2d1..0e9ab844322 100644 --- a/src/emc/tp/tp_types.h +++ b/src/emc/tp/tp_types.h @@ -46,6 +46,16 @@ #define TP_MIN_ARC_LENGTH 1e-6 #define TP_BIG_NUM 1e10 +/* The spindle has outrun a position-synchronized move when the feed it asks + * for, averaged over this window, exceeds the segment maximum velocity by this + * margin while the axis is already pinned at that ceiling. Demand is measured + * from the spindle alone, as revolutions turned times the pitch: the tracking + * error carries the v^2/2a lag every G33 picks up while the axis ramps up, and + * a single-cycle spindle velocity is buried in encoder quantization noise. */ +#define TP_SYNC_OVERRUN_WINDOW 0.25 +#define TP_SYNC_OVERRUN_MARGIN 1.02 +#define TP_SYNC_OVERRUN_CEILING 0.99 + /** * TP return codes. * This enum is a catch-all for useful return statuses from TP @@ -83,6 +93,9 @@ typedef struct { double revs; int waiting_for_index; int waiting_for_atspeed; + int overrun_cycles; /* cycles elapsed in the current overrun window */ + double overrun_revs; /* spindle position when that window opened */ + int overrun_reported; /* fault already raised for this synced move */ } tp_spindle_t; /** From 084cd880afa255e7422a4758ce226eeac5a4c204 Mon Sep 17 00:00:00 2001 From: Luca Toniolo <10792599+grandixximo@users.noreply.github.com> Date: Sat, 22 Aug 2026 00:54:17 +1000 Subject: [PATCH 4/7] interp: suspend the spindle override during G33 and G76 A thread is cut in several passes over the same helix, so moving the spindle speed part way through shifts the lead. Suspend the override for the duration of a G33 move and a G76 cycle, restoring whatever M48, M49 or M51 last selected. Commercial controls inhibit it during thread cutting too. G33.1 is deliberately left alone, as it is on at least one commercial control: a tap is self-guiding in its own hole, so there is no lead to spoil, and slowing the spindle while it is in the work is useful. --- docs/src/gcode/g-code.adoc | 16 ++++++++++++++++ docs/src/gcode/m-code.adoc | 4 ++++ src/emc/rs274ngc/interp_convert.cc | 22 ++++++++++++++++++++++ tests/interp/g76/expected | 2 ++ 4 files changed, 44 insertions(+) diff --git a/docs/src/gcode/g-code.adoc b/docs/src/gcode/g-code.adoc index e110e31d62e..aae0194a8a5 100644 --- a/docs/src/gcode/g-code.adoc +++ b/docs/src/gcode/g-code.adoc @@ -1071,6 +1071,12 @@ K follows the drive line described by 'X- Y- Z-'. K is not parallel to the Z axis if X or Y endpoints are used for example when cutting tapered threads. +[NOTE] +The spindle speed override has no effect during a G33 move, and the previous +setting is restored when the move ends. A thread is cut in several passes over +the same helix, so changing the spindle speed part way through would change the +lead and spoil the thread. + [[gcode:g33-tech-info]] .Technical Info At the beginning of each G33 pass, LinuxCNC uses the spindle speed and @@ -1176,6 +1182,12 @@ M2 (end program) * See <> & <> & <> sections for more information. +[NOTE] +Unlike G33, the spindle speed override stays active during a G33.1 move and the +feed follows whatever the spindle actually does. A tap guides itself in its own +hole, so there is no thread lead to spoil, and being able to slow the spindle +while the tap is in the work is useful. + It is an error if: * All axis words are omitted. @@ -2043,6 +2055,10 @@ It is an error if: * The pitch and the commanded spindle speed together ask an axis to move faster than its maximum velocity. +[NOTE] +The spindle speed override has no effect during the G76 cycle, for the same +reason as G33, and the previous setting is restored when the cycle ends. + .HAL Connections The pins 'spindle.N.at-speed' and the 'encoder._n_.phase-Z' for the spindle must be connected in your HAL file before G76 will work. diff --git a/docs/src/gcode/m-code.adoc b/docs/src/gcode/m-code.adoc index 21a3da5d6a9..ffd215f6a48 100644 --- a/docs/src/gcode/m-code.adoc +++ b/docs/src/gcode/m-code.adoc @@ -307,6 +307,10 @@ no influence, and the spindle speed will have the exact program specified value of the S-word (described in the <> section). +The override is also suspended for the duration of a +<> move or a <> cycle, whatever M48, M49 or M51 +last selected, and that selection is restored afterwards. + [[mcode:m52]] == M52 Adaptive Feed Control diff --git a/src/emc/rs274ngc/interp_convert.cc b/src/emc/rs274ngc/interp_convert.cc index 9784807b19f..d48dfff5972 100644 --- a/src/emc/rs274ngc/interp_convert.cc +++ b/src/emc/rs274ngc/interp_convert.cc @@ -5115,6 +5115,23 @@ int Interp::convert_spindle_mode(int dollar_number, block_pointer block, setup_p return INTERP_OK; } +/* Thread cutting re-enters the same helix each pass, so moving the spindle + speed part way through shifts the lead. G33.1 deliberately keeps the + override: a tap is self-guiding and slowing down is useful. */ + +static void suspend_speed_override(setup_pointer settings) +{ + DISABLE_SPEED_OVERRIDE(settings->active_spindle); +} + +static void restore_speed_override(setup_pointer settings) +{ + /* back to what the program asked for, so an M49 or M51 P0 still holds */ + if (settings->speed_override[settings->active_spindle]) { + ENABLE_SPEED_OVERRIDE(settings->active_spindle); + } +} + /* Displacement of a move, ordered XYZABCUVW. */ static void sync_move_delta(setup_pointer settings, @@ -5550,9 +5567,11 @@ int Interp::convert_straight(int move, //!< either G_0 or G_1 sync_move_delta(settings, end_x, end_y, end_z, AA_end, BB_end, CC_end, u_end, v_end, w_end, delta); CHP(check_spindle_sync_feed(settings, block->k_number, "G33", delta)); + suspend_speed_override(settings); START_SPEED_FEED_SYNCH(settings->active_spindle, block->k_number, 0); STRAIGHT_FEED(block->line_number, end_x, end_y, end_z, AA_end, BB_end, CC_end, u_end, v_end, w_end); STOP_SPEED_FEED_SYNCH(); + restore_speed_override(settings); settings->current_x = end_x; settings->current_y = end_y; settings->current_z = end_z; @@ -5811,6 +5830,8 @@ int Interp::convert_threading_cycle(block_pointer block, CHP(check_spindle_sync_feed(settings, taper_pitch, "G76", taper_pass)); } + suspend_speed_override(settings); + depth = start_depth; zoff = (depth - full_dia_depth) * tan(compound_angle); while (depth < end_depth) { @@ -5829,6 +5850,7 @@ int Interp::convert_threading_cycle(block_pointer block, start_z, zoff, taper_dist, entry_taper, exit_taper, taper_pitch, pitch, full_threadheight, target_z); } + restore_speed_override(settings); STRAIGHT_TRAVERSE(block->line_number, end_x, end_y, end_z, AABBCC); settings->current_x = end_x; settings->current_y = end_y; diff --git a/tests/interp/g76/expected b/tests/interp/g76/expected index c2c4921112c..562e3d39861 100644 --- a/tests/interp/g76/expected +++ b/tests/interp/g76/expected @@ -31,6 +31,7 @@ N..... COMMENT("h = number of spring passes") N..... COMMENT("e = distance along drive line used for tapered start/end") N..... COMMENT("l = which ends get the taper: 0 = neither, 1 = begin, 2 = end, 3 = both") + N..... DISABLE_SPEED_OVERRIDE(0) N..... STRAIGHT_TRAVERSE(0.2370, 0.0000, 0.1955, 0.0000, 0.0000, 0.0000) N..... STRAIGHT_TRAVERSE(0.1170, 0.0000, 0.1955, 0.0000, 0.0000, 0.0000) N..... DISABLE_FEED_OVERRIDE() @@ -381,6 +382,7 @@ N..... STOP_SPEED_FEED_SYNCH() N..... STRAIGHT_TRAVERSE(0.2000, 0.0000, -0.5000, 0.0000, 0.0000, 0.0000) N..... ENABLE_FEED_OVERRIDE() + N..... ENABLE_SPEED_OVERRIDE(0) N..... STRAIGHT_TRAVERSE(0.2000, 0.0000, -0.5000, 0.0000, 0.0000, 0.0000) N..... STRAIGHT_TRAVERSE(0.5000, 0.0000, -0.5000, 0.0000, 0.0000, 0.0000) N..... STRAIGHT_TRAVERSE(0.5000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000) From 0fdc993cff9332ce9a4d5d2e9414ce89331df265 Mon Sep 17 00:00:00 2001 From: Luca Toniolo <10792599+grandixximo@users.noreply.github.com> Date: Sun, 23 Aug 2026 09:46:31 +1000 Subject: [PATCH 5/7] interp: check the thread pitch in constant surface speed mode G96 sets a surface speed, so the spindle speed depends on where the tool is and the pitch check had to skip it. Use the fastest speed the move can reach instead: the speed at its smallest cutting radius, capped by the G96 D word and by [SPINDLE_n]MAX_FORWARD_VELOCITY, which is how motion caps it too. The ini limit now bounds G97 as well, so an S above what the spindle can turn no longer rejects a move the machine would run slower anyway. --- docs/src/gcode/g-code.adoc | 21 ++++++--- src/emc/nml_intf/canon.hh | 4 ++ src/emc/nml_intf/emc.hh | 1 + src/emc/rs274ngc/canonmodule.cc | 1 + src/emc/rs274ngc/gcodemodule.cc | 1 + src/emc/rs274ngc/interp_check.cc | 71 ++++++++++++++++++++++++----- src/emc/rs274ngc/interp_convert.cc | 24 +++++++--- src/emc/rs274ngc/interp_internal.hh | 10 ++++ src/emc/rs274ngc/interp_setup.cc | 1 + src/emc/rs274ngc/rs274ngc_interp.hh | 3 +- src/emc/rs274ngc/rs274ngc_pre.cc | 1 + src/emc/sai/saicanon.cc | 5 ++ src/emc/task/emccanon.cc | 16 +++++++ src/emc/task/taskintf.cc | 14 ++++++ 14 files changed, 148 insertions(+), 25 deletions(-) diff --git a/docs/src/gcode/g-code.adoc b/docs/src/gcode/g-code.adoc index aae0194a8a5..825d5f84d28 100644 --- a/docs/src/gcode/g-code.adoc +++ b/docs/src/gcode/g-code.adoc @@ -1119,8 +1119,15 @@ It is an error if: * An F word is given (the feed follows from K and the spindle speed). * The selected spindle is not commanded to turn (M3 or M4 active) when this command is executed. -* The pitch and the commanded spindle speed together ask an axis to move - faster than its maximum velocity. +* The pitch and the spindle speed together ask an axis to move faster than + its maximum velocity. + +[NOTE] +In constant surface speed mode the spindle speed follows the radius, so the +check uses the fastest speed the move can reach: the speed at its smallest +cutting radius, limited by the G96 'D' word and by +`[SPINDLE_n]MAX_FORWARD_VELOCITY`. If the move reaches the centre of rotation +and neither limit is set the speed is unbounded and no check is made. [[gcode:g33.1]] == G33.1 Rigid Tapping(((G33.1 Rigid Tapping))) @@ -1195,8 +1202,9 @@ It is an error if: * An F word is given (the feed follows from K and the spindle speed). * The selected spindle is not commanded to turn (M3 or M4 active) when this command is executed. -* The pitch and the commanded spindle speed together ask an axis to move - faster than its maximum velocity. +* The pitch and the spindle speed together ask an axis to move faster than + its maximum velocity. See <> for how the speed is determined + in constant surface speed mode. [[gcode:g38]] == G38._n_ Straight Probe(((G38.n Probe))) @@ -2052,8 +2060,9 @@ It is an error if: * All the required words are not specified. * 'P-', 'J-', 'K-' or 'H-' is negative. * 'E-' is greater than half the drive line length. -* The pitch and the commanded spindle speed together ask an axis to move - faster than its maximum velocity. +* The pitch and the spindle speed together ask an axis to move faster than + its maximum velocity. See <> for how the speed is determined + in constant surface speed mode. [NOTE] The spindle speed override has no effect during the G76 cycle, for the same diff --git a/src/emc/nml_intf/canon.hh b/src/emc/nml_intf/canon.hh index 05e21ef106f..324dcb835cc 100644 --- a/src/emc/nml_intf/canon.hh +++ b/src/emc/nml_intf/canon.hh @@ -878,6 +878,10 @@ extern double GET_EXTERNAL_ANGLE_UNIT_FACTOR(); // program units per minute, or zero if that limit is not available extern double GET_EXTERNAL_AXIS_MAX_VELOCITY(int axis); +// Returns the maximum forward speed of one spindle, in RPM, or zero if that +// limit is not available +extern double GET_EXTERNAL_SPINDLE_MAX_VELOCITY(int spindle); + // Returns the system feed rate extern double GET_EXTERNAL_FEED_RATE(); diff --git a/src/emc/nml_intf/emc.hh b/src/emc/nml_intf/emc.hh index 2738b34144b..10e4bb67a5e 100644 --- a/src/emc/nml_intf/emc.hh +++ b/src/emc/nml_intf/emc.hh @@ -334,6 +334,7 @@ extern int emcJointSetMaxJerk(int joint, double jerk); extern int emcSpindleSetParams(int spindle, double max_pos, double min_pos, double max_neg, double min_neg, double search_vel, double home_angle, int sequence, double increment); +extern double emcSpindleGetMaxVelocity(int spindle); // implementation functions for EMC_TRAJ types diff --git a/src/emc/rs274ngc/canonmodule.cc b/src/emc/rs274ngc/canonmodule.cc index 8e32184575e..d25924235b8 100644 --- a/src/emc/rs274ngc/canonmodule.cc +++ b/src/emc/rs274ngc/canonmodule.cc @@ -179,6 +179,7 @@ BOOST_PYTHON_MODULE(emccanon) { def("GET_EXTERNAL_TOOL_SLOT",&GET_EXTERNAL_TOOL_SLOT); def("GET_EXTERNAL_TOOL_TABLE",&GET_EXTERNAL_TOOL_TABLE); def("GET_EXTERNAL_AXIS_MAX_VELOCITY",&GET_EXTERNAL_AXIS_MAX_VELOCITY); + def("GET_EXTERNAL_SPINDLE_MAX_VELOCITY",&GET_EXTERNAL_SPINDLE_MAX_VELOCITY); def("GET_EXTERNAL_TRAVERSE_RATE",&GET_EXTERNAL_TRAVERSE_RATE); def("GET_OPTIONAL_PROGRAM_STOP",&GET_OPTIONAL_PROGRAM_STOP); def("INIT_CANON",&INIT_CANON); diff --git a/src/emc/rs274ngc/gcodemodule.cc b/src/emc/rs274ngc/gcodemodule.cc index bdfd976240e..45c99a05c99 100644 --- a/src/emc/rs274ngc/gcodemodule.cc +++ b/src/emc/rs274ngc/gcodemodule.cc @@ -1065,6 +1065,7 @@ int GET_EXTERNAL_TOOL_SLOT() { return 0; } int GET_EXTERNAL_SELECTED_TOOL_SLOT() { return 0; } double GET_EXTERNAL_FEED_RATE() { return 1; } double GET_EXTERNAL_AXIS_MAX_VELOCITY(int /*axis*/) { return 0; } +double GET_EXTERNAL_SPINDLE_MAX_VELOCITY(int /*spindle*/) { return 0; } double GET_EXTERNAL_TRAVERSE_RATE() { return 0; } int GET_EXTERNAL_FLOOD() { return 0; } int GET_EXTERNAL_MIST() { return 0; } diff --git a/src/emc/rs274ngc/interp_check.cc b/src/emc/rs274ngc/interp_check.cc index 6eb889f5527..12a4c978623 100644 --- a/src/emc/rs274ngc/interp_check.cc +++ b/src/emc/rs274ngc/interp_check.cc @@ -419,24 +419,63 @@ move length, as the planner distributes it. Rotary axes are ignored: a pitch is a linear distance per revolution. Using the commanded S word means the error names the offending line and does -not depend on the spindle already running. Skipped in constant surface speed -mode, and for any axis whose limit is unavailable (the standalone interpreter -reports zero). +not depend on the spindle already running. Skipped for any axis whose limit is +unavailable (the standalone interpreter reports zero). + +In constant surface speed mode the S word is a surface speed, so the spindle +speed depends on where the tool is. The worst case over the move is the speed +at its smallest radius, capped the same way motion caps it: by the G96 D word, +and by [SPINDLE_n]MAX_FORWARD_VELOCITY. If the move reaches the centre of +rotation and neither cap is configured the speed is unbounded and the check is +skipped. */ +/* Fastest the spindle will turn during the move, in RPM, or zero if that + cannot be bounded. MIN_FORWARD_VELOCITY is not applied: motion raises a + speed below it, so ignoring it can only make this too low, and too low + passes a move the runtime overrun check still catches. */ +static double sync_worst_case_rpm(setup_pointer settings, int spindle, + double min_radius) +{ + double speed = settings->speed[spindle]; + double rpm; + + if (speed <= 0.0) + return 0.0; + + if (settings->spindle_mode[spindle] == SPINDLE_MODE::CONSTANT_RPM) { + rpm = speed; + } else if (min_radius > 0.0) { + /* surface speed is metres or feet per minute against a radius in program + units, the css_factor motion works from (see SET_SPINDLE_SPEED) */ + double per_unit = (settings->length_units == CANON_UNITS_INCHES) ? 12.0 : 1000.0; + rpm = per_unit / (2.0 * M_PI) * speed / min_radius; + if (settings->css_maximum[spindle] > 0.0) + rpm = fmin(rpm, settings->css_maximum[spindle]); /* G96 D word */ + } else { + /* the move reaches the centre of rotation, so only the caps bound it */ + rpm = settings->css_maximum[spindle]; + } + + double ini_cap = GET_EXTERNAL_SPINDLE_MAX_VELOCITY(spindle); + if (ini_cap > 0.0 && (rpm <= 0.0 || rpm > ini_cap)) + rpm = ini_cap; + + return rpm; +} + int Interp::check_spindle_sync_feed(setup_pointer settings, //!< pointer to machine settings double pitch, //!< program units per revolution const char *code, //!< G code name, for the message - const double delta[9]) //!< move, program units, XYZABCUVW + const double delta[9], //!< move, program units, XYZABCUVW + double min_radius) //!< smallest cutting radius of the move { static const char axis_name[] = "XYZABCUVW"; int spindle = settings->active_spindle; + bool css = settings->spindle_mode[spindle] == SPINDLE_MODE::CONSTANT_SURFACE; - if (settings->spindle_mode[spindle] != SPINDLE_MODE::CONSTANT_RPM) - return INTERP_OK; - - double speed = settings->speed[spindle]; + double speed = sync_worst_case_rpm(settings, spindle, min_radius); if (speed <= 0.0 || pitch == 0.0) return INTERP_OK; @@ -462,10 +501,18 @@ int Interp::check_spindle_sync_feed(setup_pointer settings, //!< pointer to mac if (max_rate <= 0.0) continue; double axis_rate = required_rate * fabs(delta[ax]) / length; - CHKS((axis_rate > max_rate), - _("%s pitch %g at spindle speed %g needs %g per minute on the %c axis, " - "which exceeds its maximum velocity of %g"), - code, fabs(pitch), speed, axis_rate, axis_name[ax], max_rate); + if (css) { + CHKS((axis_rate > max_rate), + _("%s pitch %g reaches spindle speed %g in constant surface speed " + "mode and needs %g per minute on the %c axis, which exceeds its " + "maximum velocity of %g"), + code, fabs(pitch), speed, axis_rate, axis_name[ax], max_rate); + } else { + CHKS((axis_rate > max_rate), + _("%s pitch %g at spindle speed %g needs %g per minute on the %c axis, " + "which exceeds its maximum velocity of %g"), + code, fabs(pitch), speed, axis_rate, axis_name[ax], max_rate); + } } return INTERP_OK; diff --git a/src/emc/rs274ngc/interp_convert.cc b/src/emc/rs274ngc/interp_convert.cc index d48dfff5972..c14a282ae88 100644 --- a/src/emc/rs274ngc/interp_convert.cc +++ b/src/emc/rs274ngc/interp_convert.cc @@ -5102,14 +5102,18 @@ int Interp::convert_spindle_mode(int dollar_number, block_pointer block, setup_p if (dollar_number == -1 || s == dollar_number){ if(block->g_modes[GM_SPINDLE_MODE] == G_97) { settings->spindle_mode[s] = SPINDLE_MODE::CONSTANT_RPM; + settings->css_maximum[s] = 0.0; enqueue_SET_SPINDLE_MODE(s, 0); } else { /* G_96 */ settings->spindle_mode[s] = SPINDLE_MODE::CONSTANT_SURFACE; - if(block->d_flag) + if(block->d_flag) { + settings->css_maximum[s] = fabs(block->d_number_float); enqueue_SET_SPINDLE_MODE(s, fabs(block->d_number_float)); - else + } else { + settings->css_maximum[s] = 0.0; enqueue_SET_SPINDLE_MODE(s, 1e30); } + } } } return INTERP_OK; @@ -5566,7 +5570,8 @@ int Interp::convert_straight(int move, //!< either G_0 or G_1 double delta[9]; sync_move_delta(settings, end_x, end_y, end_z, AA_end, BB_end, CC_end, u_end, v_end, w_end, delta); - CHP(check_spindle_sync_feed(settings, block->k_number, "G33", delta)); + CHP(check_spindle_sync_feed(settings, block->k_number, "G33", delta, + min_abs_over_range(settings->current_x, end_x))); suspend_speed_override(settings); START_SPEED_FEED_SYNCH(settings->active_spindle, block->k_number, 0); STRAIGHT_FEED(block->line_number, end_x, end_y, end_z, AA_end, BB_end, CC_end, u_end, v_end, w_end); @@ -5596,7 +5601,7 @@ int Interp::convert_straight(int move, //!< either G_0 or G_1 u_end, v_end, w_end, delta); // I multiplies the spindle speed for the retract CHP(check_spindle_sync_feed(settings, block->k_number * scale, "G33.1", - delta)); + delta, fabs(settings->current_x))); START_SPEED_FEED_SYNCH(settings->active_spindle, block->k_number, 0); RIGID_TAP(block->line_number, end_x, end_y, end_z, scale); STOP_SPEED_FEED_SYNCH(); @@ -5823,11 +5828,18 @@ int Interp::convert_threading_cycle(block_pointer block, // A taper also moves X by the thread height over the taper distance, at // the correspondingly larger pitch. double plain_pass[9] = {0.0, 0.0, target_z - start_z, 0, 0, 0, 0, 0, 0}; - CHP(check_spindle_sync_feed(settings, pitch, "G76", plain_pass)); + /* the passes run between the first and last cut depth, so the tightest + radius is the last cut outside, the first cut boring */ + double thread_min_x = boring + ? min_abs_over_range(safe_x + start_depth, safe_x + end_depth) + : min_abs_over_range(safe_x - end_depth, safe_x - start_depth); + CHP(check_spindle_sync_feed(settings, pitch, "G76", plain_pass, + thread_min_x)); if (taper_dist != 0.0 && (entry_taper || exit_taper)) { double taper_pass[9] = {full_threadheight, 0.0, taper_dist, 0, 0, 0, 0, 0, 0}; - CHP(check_spindle_sync_feed(settings, taper_pitch, "G76", taper_pass)); + CHP(check_spindle_sync_feed(settings, taper_pitch, "G76", taper_pass, + thread_min_x)); } suspend_speed_override(settings); diff --git a/src/emc/rs274ngc/interp_internal.hh b/src/emc/rs274ngc/interp_internal.hh index 22268854211..8beb7d67312 100644 --- a/src/emc/rs274ngc/interp_internal.hh +++ b/src/emc/rs274ngc/interp_internal.hh @@ -94,6 +94,15 @@ static inline bool equal(double a, double b) return (fabs(a - b) < TOLERANCE_EQUAL); } +/* Smallest distance from zero over the closed interval [a, b], which is zero + if the interval spans it. */ +static inline double min_abs_over_range(double a, double b) +{ + if ((a <= 0.0 && b >= 0.0) || (b <= 0.0 && a >= 0.0)) + return 0.0; + return fmin(fabs(a), fabs(b)); +} + #define TINY 1e-12 /* for arc_data_r */ // max number of m codes on one line @@ -763,6 +772,7 @@ struct setup int active_spindle; // the spindle currently used for CSS, FPR etc. double speed[EMCMOT_MAX_SPINDLES];// array of spindle speeds SPINDLE_MODE spindle_mode[EMCMOT_MAX_SPINDLES];// SPINDLE_MODE::CONSTANT_RPM or SPINDLE_MODE::CONSTANT_SURFACE + double css_maximum[EMCMOT_MAX_SPINDLES];// G96 D word, RPM ceiling in CSS mode, 0 if not given CANON_SPEED_FEED_MODE speed_feed_mode; // independent or synched bool speed_override[EMCMOT_MAX_SPINDLES]; // whether speed override is enabled CANON_DIRECTION spindle_turning[EMCMOT_MAX_SPINDLES]; // direction spindle is turning diff --git a/src/emc/rs274ngc/interp_setup.cc b/src/emc/rs274ngc/interp_setup.cc index 365e4682d6c..ba9b918385b 100644 --- a/src/emc/rs274ngc/interp_setup.cc +++ b/src/emc/rs274ngc/interp_setup.cc @@ -132,6 +132,7 @@ setup::setup() : active_spindle(0), speed {0.0}, spindle_mode{SPINDLE_MODE::CONSTANT_RPM}, + css_maximum{0.0}, speed_feed_mode{CANON_INDEPENDENT}, speed_override{false}, spindle_turning{CANON_STOPPED}, diff --git a/src/emc/rs274ngc/rs274ngc_interp.hh b/src/emc/rs274ngc/rs274ngc_interp.hh index 21ee6c02156..0c8ebfcb20a 100644 --- a/src/emc/rs274ngc/rs274ngc_interp.hh +++ b/src/emc/rs274ngc/rs274ngc_interp.hh @@ -224,7 +224,8 @@ public: int check_m_codes(block_pointer block); int check_other_codes(block_pointer block); int check_spindle_sync_feed(setup_pointer settings, double pitch, - const char *code, const double delta[9]); + const char *code, const double delta[9], + double min_radius); int close_and_downcase(char *line); void nurbs_reset_global_variables(void); int convert_nurbs(int move, block_pointer block, setup_pointer settings); diff --git a/src/emc/rs274ngc/rs274ngc_pre.cc b/src/emc/rs274ngc/rs274ngc_pre.cc index 54212aa1850..57de1da0794 100644 --- a/src/emc/rs274ngc/rs274ngc_pre.cc +++ b/src/emc/rs274ngc/rs274ngc_pre.cc @@ -2074,6 +2074,7 @@ int Interp::synch() _setup.spindle_turning[s] = GET_EXTERNAL_SPINDLE(s); _setup.speed_override[s] = GET_EXTERNAL_SPINDLE_OVERRIDE_ENABLE(s); _setup.spindle_mode[s] = SPINDLE_MODE::CONSTANT_RPM; + _setup.css_maximum[s] = 0.0; } GET_EXTERNAL_PARAMETER_FILE_NAME(file_name, (LINELEN - 1)); save_parameters(((file_name[0] == diff --git a/src/emc/sai/saicanon.cc b/src/emc/sai/saicanon.cc index a4e0bb76b16..24e85e1e5f5 100644 --- a/src/emc/sai/saicanon.cc +++ b/src/emc/sai/saicanon.cc @@ -974,6 +974,11 @@ double GET_EXTERNAL_AXIS_MAX_VELOCITY(int /*axis*/) return 0.0; } +double GET_EXTERNAL_SPINDLE_MAX_VELOCITY(int /*spindle*/) +{ + return 0.0; +} + /* Returns the system traverse rate */ double GET_EXTERNAL_TRAVERSE_RATE() { diff --git a/src/emc/task/emccanon.cc b/src/emc/task/emccanon.cc index 36a95e81696..1c8a7875ac7 100644 --- a/src/emc/task/emccanon.cc +++ b/src/emc/task/emccanon.cc @@ -3828,6 +3828,22 @@ double GET_EXTERNAL_AXIS_MAX_VELOCITY(int axis) return TO_PROG_LEN(FROM_EXT_LEN(vel)) * 60.0; } +double GET_EXTERNAL_SPINDLE_MAX_VELOCITY(int spindle) +{ + if (spindle < 0 || spindle >= emcStatus->motion.traj.spindles) { + return 0.0; + } + + double rpm = emcSpindleGetMaxVelocity(spindle); + + /* the ini default when MAX_FORWARD_VELOCITY is absent is a stand-in for + "no limit", not a speed anyone can reach */ + if (rpm <= 0.0 || rpm >= 1e30) { + return 0.0; + } + return rpm; +} + // traverse rate wanted is in program units per minute double GET_EXTERNAL_TRAVERSE_RATE() { diff --git a/src/emc/task/taskintf.cc b/src/emc/task/taskintf.cc index 482d8bf8afe..b8cdfc0dccf 100644 --- a/src/emc/task/taskintf.cc +++ b/src/emc/task/taskintf.cc @@ -1957,6 +1957,11 @@ int emcSpindleSetParams(int spindle, double max_pos, double min_pos, double max_ return 0; } + SpindleConfig[spindle].max_pos_speed = max_pos; + SpindleConfig[spindle].max_neg_speed = max_neg; + SpindleConfig[spindle].min_pos_speed = min_pos; + SpindleConfig[spindle].min_neg_speed = min_neg; + emcmotCommand.command = EMCMOT_SET_SPINDLE_PARAMS; emcmotCommand.spindle = spindle; emcmotCommand.maxLimit = max_pos; @@ -1978,6 +1983,15 @@ int emcSpindleSetParams(int spindle, double max_pos, double min_pos, double max_ return retval; } +double emcSpindleGetMaxVelocity(int spindle) +{ + if (spindle < 0 || spindle >= EMCMOT_MAX_SPINDLES) { + return 0; + } + + return SpindleConfig[spindle].max_pos_speed; +} + int emcSpindleAbort(int spindle) { return emcSpindleOff(spindle); From e6cc88d34aa1a545f95d7c4053af8ae11e39ce7a Mon Sep 17 00:00:00 2001 From: Luca Toniolo <10792599+grandixximo@users.noreply.github.com> Date: Mon, 24 Aug 2026 00:54:58 +1000 Subject: [PATCH 6/7] interp: restore the spindle override after an aborted thread G33 and G76 suspend the spindle override and queue the restore behind the move. An abort clears the interpreter list, so the restore is lost and nothing else re-enables it: the override stays dead until a later thread completes, and Interp::synch() reads that back as the modal M48 or M49 state. Re-assert it in Interp::on_abort, on the active spindle only. Motion keeps one global enable bit, so looping over every spindle has the inactive ones clear it again. --- src/emc/rs274ngc/rs274ngc_pre.cc | 8 ++++++++ tests/motion-logger/basic/expected.builtin-startup.in | 1 + tests/motion-logger/mountaindew/expected.motion-logger | 1 + .../startup-gcode-abort/expected.motion-logger.in | 1 + 4 files changed, 11 insertions(+) diff --git a/src/emc/rs274ngc/rs274ngc_pre.cc b/src/emc/rs274ngc/rs274ngc_pre.cc index 57de1da0794..d7fa98f8ba3 100644 --- a/src/emc/rs274ngc/rs274ngc_pre.cc +++ b/src/emc/rs274ngc/rs274ngc_pre.cc @@ -2673,6 +2673,14 @@ int Interp::on_abort(int reason, const char *message) reset(); _setup.mdi_interrupt = false; + /* A thread's queued override restore is lost when abort clears the + interpreter list, so re-assert the modal state here. */ + if (_setup.speed_override[_setup.active_spindle]) { + ENABLE_SPEED_OVERRIDE(_setup.active_spindle); + } else { + DISABLE_SPEED_OVERRIDE(_setup.active_spindle); + } + // clear in case set by an interrupted remapped procedure // if set, may cause a "Queue is not empty after tool change" error _setup.toolchange_flag = false; diff --git a/tests/motion-logger/basic/expected.builtin-startup.in b/tests/motion-logger/basic/expected.builtin-startup.in index 1f23b69565e..2296018b31c 100644 --- a/tests/motion-logger/basic/expected.builtin-startup.in +++ b/tests/motion-logger/basic/expected.builtin-startup.in @@ -200,6 +200,7 @@ SPINDLE_OFF # turns on Motion and all the amps. # +SS_ENABLE ENABLE # diff --git a/tests/motion-logger/mountaindew/expected.motion-logger b/tests/motion-logger/mountaindew/expected.motion-logger index efbcc4db7eb..c52ea9598ed 100644 --- a/tests/motion-logger/mountaindew/expected.motion-logger +++ b/tests/motion-logger/mountaindew/expected.motion-logger @@ -98,6 +98,7 @@ JOG_ABORT joint=14 JOG_ABORT joint=15 ABORT SPINDLE_OFF +SS_ENABLE ENABLE COORD JOG_ABORT joint=0 diff --git a/tests/motion-logger/startup-gcode-abort/expected.motion-logger.in b/tests/motion-logger/startup-gcode-abort/expected.motion-logger.in index 5e1d61600b2..8bef6b8b1a0 100644 --- a/tests/motion-logger/startup-gcode-abort/expected.motion-logger.in +++ b/tests/motion-logger/startup-gcode-abort/expected.motion-logger.in @@ -157,4 +157,5 @@ JOG_ABORT joint=14 JOG_ABORT joint=15 ABORT SPINDLE_OFF +SS_ENABLE DISABLE From 37b61f5e61b725ce3368a05fd359260156904f22 Mon Sep 17 00:00:00 2001 From: Luca Toniolo <10792599+grandixximo@users.noreply.github.com> Date: Mon, 24 Aug 2026 00:54:58 +1000 Subject: [PATCH 7/7] tp: hold the spindle override for a G76 cycle instead of dropping it A G76 cycle is a single block, so the override can be held at the value in effect when it starts rather than forced down to the programmed speed. Threading at 80% then needs no edit to S. A G33 thread is one pass per line with the retract in between, so there is no span to hold and it keeps the plain suspend. The hold rides on the per-segment enables mask, which already carries the suspend correctly through read-ahead. Motion latches min(scale, 1.0) on the rising edge, so the value is the one in effect when the first cut starts, and drops the hold on abort. Sim lathe, S300, 0.05 in/rev: entering at 80% cuts every pass at 240 and moving the slider to 40% mid-cycle has no effect; entering at 120% cuts at 300; G33 at 80% still runs at 300; aborting mid-cycle leaves the override live. --- docs/src/gcode/g-code.adoc | 18 +++++++++++++----- docs/src/gcode/m-code.adoc | 7 ++++--- src/emc/motion/command.c | 21 ++++++++++++++++----- src/emc/motion/control.c | 18 +++++++++++++++++- src/emc/motion/motion.h | 1 + src/emc/nml_intf/canon.hh | 2 ++ src/emc/nml_intf/emc_nml.hh | 2 +- src/emc/nml_intf/motion_types.h | 5 +++++ src/emc/rs274ngc/canonmodule.cc | 1 + src/emc/rs274ngc/gcodemodule.cc | 1 + src/emc/rs274ngc/interp_convert.cc | 15 +++++++++++++-- src/emc/sai/saicanon.cc | 3 +++ src/emc/task/emccanon.cc | 9 +++++++-- tests/interp/g76/expected | 2 +- 14 files changed, 85 insertions(+), 20 deletions(-) diff --git a/docs/src/gcode/g-code.adoc b/docs/src/gcode/g-code.adoc index 825d5f84d28..917ba5e42a0 100644 --- a/docs/src/gcode/g-code.adoc +++ b/docs/src/gcode/g-code.adoc @@ -1073,9 +1073,13 @@ threads. [NOTE] The spindle speed override has no effect during a G33 move, and the previous -setting is restored when the move ends. A thread is cut in several passes over -the same helix, so changing the spindle speed part way through would change the -lead and spoil the thread. +setting is restored when the move ends. The commanded position follows the +spindle, but the axis lags it by an amount proportional to speed, so changing +the speed part way through shifts the tool along the helix it is already +cutting. A <> cycle holds the override at the value the cycle +started with rather than dropping it, because the whole cycle is one block; a +G33 thread is one pass per line with the retract in between, so there is no +span to hold. [[gcode:g33-tech-info]] .Technical Info @@ -2065,8 +2069,12 @@ It is an error if: in constant surface speed mode. [NOTE] -The spindle speed override has no effect during the G76 cycle, for the same -reason as G33, and the previous setting is restored when the cycle ends. +The spindle speed override is held for the whole G76 cycle at the value in +effect when the cycle starts, capped at 100% because the pitch is checked +against the programmed speed. Moving it while the cycle runs has no effect, so +every pass cuts at one speed and they stay on the same helix. The previous +setting is restored when the cycle ends. To thread at 80% of the programmed +speed, set the override before the cycle starts. .HAL Connections The pins 'spindle.N.at-speed' and the 'encoder._n_.phase-Z' for the diff --git a/docs/src/gcode/m-code.adoc b/docs/src/gcode/m-code.adoc index ffd215f6a48..f4618881f01 100644 --- a/docs/src/gcode/m-code.adoc +++ b/docs/src/gcode/m-code.adoc @@ -307,9 +307,10 @@ no influence, and the spindle speed will have the exact program specified value of the S-word (described in the <> section). -The override is also suspended for the duration of a -<> move or a <> cycle, whatever M48, M49 or M51 -last selected, and that selection is restored afterwards. +The override is also suspended for the duration of a <> move, +and held at the value the cycle started with for the duration of a +<> cycle, whatever M48, M49 or M51 last selected. That selection +is restored afterwards. [[mcode:m52]] == M52 Adaptive Feed Control diff --git a/src/emc/motion/command.c b/src/emc/motion/command.c index 8905ad05d13..8953574d5ce 100644 --- a/src/emc/motion/command.c +++ b/src/emc/motion/command.c @@ -534,6 +534,8 @@ void emcmotCommandHandler_locked(void *arg, long servo_period) does yet), and if in free mode, it disables the free mode traj planners which stops joint motion */ rtapi_print_msg(RTAPI_MSG_DBG, "ABORT"); + /* the override release is queued behind the cycle, drop it here */ + emcmotStatus->enables_new &= ~SS_LOCKED; /* check for coord or free space motion active */ if (GET_MOTION_TELEOP_FLAG()) { axis_jog_abort_all(0); @@ -1409,14 +1411,23 @@ void emcmotCommandHandler_locked(void *arg, long servo_period) break; case EMCMOT_SS_ENABLE: - /* enable/disable overriding spindle speed */ + /* enable/disable/lock overriding spindle speed */ /* can happen at any time */ - if ( emcmotCommand->mode != 0 ) { + switch ( emcmotCommand->mode ) { + case EMC_SO_OVERRIDE_LOCK: + /* hold whatever is in effect when the first locked move starts */ + rtapi_print_msg(RTAPI_MSG_DBG, "SPINDLE SCALE: LOCK"); + emcmotStatus->enables_new |= SS_LOCKED; + break; + case EMC_SO_OVERRIDE_OFF: + rtapi_print_msg(RTAPI_MSG_DBG, "SPINDLE SCALE: OFF"); + emcmotStatus->enables_new &= ~(SS_ENABLED | SS_LOCKED); + break; + default: rtapi_print_msg(RTAPI_MSG_DBG, "SPINDLE SCALE: ON"); emcmotStatus->enables_new |= SS_ENABLED; - } else { - rtapi_print_msg(RTAPI_MSG_DBG, "SPINDLE SCALE: OFF"); - emcmotStatus->enables_new &= ~SS_ENABLED; + emcmotStatus->enables_new &= ~SS_LOCKED; + break; } break; diff --git a/src/emc/motion/control.c b/src/emc/motion/control.c index 5bc132f9b37..e01e05a0299 100644 --- a/src/emc/motion/control.c +++ b/src/emc/motion/control.c @@ -68,6 +68,10 @@ extern struct emcmot_status_t *emcmotStatus; // etc. static double *pcmd_p[EMCMOT_MAX_AXIS]; +/* spindle override held while SS_LOCKED, see process_inputs */ +static double locked_spindle_scale[EMCMOT_MAX_SPINDLES]; +static unsigned char prev_enables = 0; + /*********************************************************************** * LOCAL FUNCTION PROTOTYPES * ************************************************************************/ @@ -371,6 +375,16 @@ static void process_inputs(void) /* use the enables that are in effect right now */ enables = emcmotStatus->enables_new; } + /* Latch as the first locked move starts, so every pass of a G76 cycle + cuts at one speed. Capped at 1.0: the pitch check used the programmed + speed. */ + if ( (enables & SS_LOCKED) && !(prev_enables & SS_LOCKED) ) { + for (spindle_num = 0; spindle_num < emcmotConfig->numSpindles; spindle_num++) { + double s = emcmotStatus->spindle_status[spindle_num].scale; + locked_spindle_scale[spindle_num] = s < 1.0 ? s : 1.0; + } + } + prev_enables = enables; /* feed scaling first: feed_scale, adaptive_feed, and feed_hold */ scale = 1.0; if ( (emcmotStatus->motion_state != EMCMOT_MOTION_FREE) @@ -427,7 +441,9 @@ static void process_inputs(void) for (spindle_num=0; spindle_num < emcmotConfig->numSpindles; spindle_num++){ scale = 1.0; if ( enables & SS_ENABLED ) { - scale *= emcmotStatus->spindle_status[spindle_num].scale; + scale *= (enables & SS_LOCKED) + ? locked_spindle_scale[spindle_num] + : emcmotStatus->spindle_status[spindle_num].scale; } /*non maskable (except during spindle synch move) spindle inhibit pin */ if ( enables & hal_get_bool(emcmot_hal_data->spindle[spindle_num].spindle_inhibit) ) { diff --git a/src/emc/motion/motion.h b/src/emc/motion/motion.h index f31d4dedc0e..d15bf14ec21 100644 --- a/src/emc/motion/motion.h +++ b/src/emc/motion/motion.h @@ -425,6 +425,7 @@ Suggestion: Split this in to an Error and a Status flag register.. #define FS_ENABLED 0x02 #define AF_ENABLED 0x04 #define FH_ENABLED 0x08 +#define SS_LOCKED 0x10 /* This structure contains all of the data associated with a single joint. Note that this structure does not need diff --git a/src/emc/nml_intf/canon.hh b/src/emc/nml_intf/canon.hh index 324dcb835cc..8e37b73e5a6 100644 --- a/src/emc/nml_intf/canon.hh +++ b/src/emc/nml_intf/canon.hh @@ -734,6 +734,8 @@ extern void ENABLE_FEED_OVERRIDE(); /* used to deactivate user control of spindle speed override */ extern void DISABLE_SPEED_OVERRIDE(int spindle); extern void ENABLE_SPEED_OVERRIDE(int spindle); +/* hold the override at whatever is in effect when the next move starts */ +extern void LOCK_SPEED_OVERRIDE(int spindle); /* used to deactivate user control of feed hold */ extern void DISABLE_FEED_HOLD(); diff --git a/src/emc/nml_intf/emc_nml.hh b/src/emc/nml_intf/emc_nml.hh index 5cede52b09f..e7931f803ba 100644 --- a/src/emc/nml_intf/emc_nml.hh +++ b/src/emc/nml_intf/emc_nml.hh @@ -657,7 +657,7 @@ class EMC_TRAJ_SET_SO_ENABLE:public EMC_TRAJ_CMD_MSG { void update(CMS * cms); int spindle; - unsigned char mode; //mode=0, override off (will work with 100% SO), mode != 0, override on, user can change SO + unsigned char mode; //EMC_SO_OVERRIDE_OFF, _ON or _LOCK }; class EMC_TRAJ_SET_FH_ENABLE:public EMC_TRAJ_CMD_MSG { diff --git a/src/emc/nml_intf/motion_types.h b/src/emc/nml_intf/motion_types.h index dc7de0b3bac..60cadaa05ed 100644 --- a/src/emc/nml_intf/motion_types.h +++ b/src/emc/nml_intf/motion_types.h @@ -22,4 +22,9 @@ #define EMC_MOTION_TYPE_PROBING 5 #define EMC_MOTION_TYPE_INDEXROTARY 6 +/* modes of EMC_TRAJ_SET_SO_ENABLE, passed on as EMCMOT_SS_ENABLE */ +#define EMC_SO_OVERRIDE_OFF 0 +#define EMC_SO_OVERRIDE_ON 1 +#define EMC_SO_OVERRIDE_LOCK 2 + #endif diff --git a/src/emc/rs274ngc/canonmodule.cc b/src/emc/rs274ngc/canonmodule.cc index d25924235b8..002be0c8de9 100644 --- a/src/emc/rs274ngc/canonmodule.cc +++ b/src/emc/rs274ngc/canonmodule.cc @@ -118,6 +118,7 @@ BOOST_PYTHON_MODULE(emccanon) { def("ENABLE_FEED_HOLD",&ENABLE_FEED_HOLD); def("ENABLE_FEED_OVERRIDE",&ENABLE_FEED_OVERRIDE); def("ENABLE_SPEED_OVERRIDE",&ENABLE_SPEED_OVERRIDE); + def("LOCK_SPEED_OVERRIDE",&LOCK_SPEED_OVERRIDE); def("FINISH",&FINISH); def("ON_RESET", &ON_RESET); def("FLOOD_OFF",&FLOOD_OFF); diff --git a/src/emc/rs274ngc/gcodemodule.cc b/src/emc/rs274ngc/gcodemodule.cc index 45c99a05c99..79be4c2d915 100644 --- a/src/emc/rs274ngc/gcodemodule.cc +++ b/src/emc/rs274ngc/gcodemodule.cc @@ -925,6 +925,7 @@ void ENABLE_FEED_HOLD() {} void DISABLE_SPEED_OVERRIDE(int /*spindle*/) {} void ENABLE_FEED_OVERRIDE() {} void ENABLE_SPEED_OVERRIDE(int /*spindle*/) {} +void LOCK_SPEED_OVERRIDE(int /*spindle*/) {} void MIST_OFF() {} void FLOOD_OFF() {} void MIST_ON() {} diff --git a/src/emc/rs274ngc/interp_convert.cc b/src/emc/rs274ngc/interp_convert.cc index c14a282ae88..acbe0ea2f70 100644 --- a/src/emc/rs274ngc/interp_convert.cc +++ b/src/emc/rs274ngc/interp_convert.cc @@ -5130,12 +5130,23 @@ static void suspend_speed_override(setup_pointer settings) static void restore_speed_override(setup_pointer settings) { - /* back to what the program asked for, so an M49 or M51 P0 still holds */ + /* back to what the program asked for, so an M49 or M51 P0 still holds. + Sent either way, since it also clears a lock */ if (settings->speed_override[settings->active_spindle]) { ENABLE_SPEED_OVERRIDE(settings->active_spindle); + } else { + DISABLE_SPEED_OVERRIDE(settings->active_spindle); } } +/* A G76 cycle is one block, so the override can be held for it. A G33 thread + is one pass per line with the retract in between, so it keeps the suspend. */ + +static void lock_speed_override(setup_pointer settings) +{ + LOCK_SPEED_OVERRIDE(settings->active_spindle); +} + /* Displacement of a move, ordered XYZABCUVW. */ static void sync_move_delta(setup_pointer settings, @@ -5842,7 +5853,7 @@ int Interp::convert_threading_cycle(block_pointer block, thread_min_x)); } - suspend_speed_override(settings); + lock_speed_override(settings); depth = start_depth; zoff = (depth - full_dia_depth) * tan(compound_angle); diff --git a/src/emc/sai/saicanon.cc b/src/emc/sai/saicanon.cc index 24e85e1e5f5..942b60a7768 100644 --- a/src/emc/sai/saicanon.cc +++ b/src/emc/sai/saicanon.cc @@ -623,6 +623,9 @@ void ENABLE_FEED_OVERRIDE() void ENABLE_SPEED_OVERRIDE(int spindle) {PRINT("ENABLE_SPEED_OVERRIDE(%i)\n", spindle); so_enable = true; } +void LOCK_SPEED_OVERRIDE(int spindle) +{PRINT("LOCK_SPEED_OVERRIDE(%i)\n", spindle); } + void FLOOD_OFF() { PRINT("FLOOD_OFF()\n"); diff --git a/src/emc/task/emccanon.cc b/src/emc/task/emccanon.cc index 1c8a7875ac7..27249f610c1 100644 --- a/src/emc/task/emccanon.cc +++ b/src/emc/task/emccanon.cc @@ -3390,12 +3390,17 @@ void SPEED_OVERRIDE_(int spindle, int mode) //refers to spindle speed void DISABLE_SPEED_OVERRIDE(int spindle) { - SPEED_OVERRIDE_(spindle, 0); + SPEED_OVERRIDE_(spindle, EMC_SO_OVERRIDE_OFF); } void ENABLE_SPEED_OVERRIDE(int spindle) { - SPEED_OVERRIDE_(spindle, 1); + SPEED_OVERRIDE_(spindle, EMC_SO_OVERRIDE_ON); +} + +void LOCK_SPEED_OVERRIDE(int spindle) +{ + SPEED_OVERRIDE_(spindle, EMC_SO_OVERRIDE_LOCK); } void FEED_HOLD_(int mode) diff --git a/tests/interp/g76/expected b/tests/interp/g76/expected index 562e3d39861..488e01b9ffe 100644 --- a/tests/interp/g76/expected +++ b/tests/interp/g76/expected @@ -31,7 +31,7 @@ N..... COMMENT("h = number of spring passes") N..... COMMENT("e = distance along drive line used for tapered start/end") N..... COMMENT("l = which ends get the taper: 0 = neither, 1 = begin, 2 = end, 3 = both") - N..... DISABLE_SPEED_OVERRIDE(0) + N..... LOCK_SPEED_OVERRIDE(0) N..... STRAIGHT_TRAVERSE(0.2370, 0.0000, 0.1955, 0.0000, 0.0000, 0.0000) N..... STRAIGHT_TRAVERSE(0.1170, 0.0000, 0.1955, 0.0000, 0.0000, 0.0000) N..... DISABLE_FEED_OVERRIDE()