Browse Source

️ Fix and improve Inline Laser Power (#22690)

FB4S_WIFI
Mike La Spina 2 years ago
committed by Scott Lahteine
parent
commit
d965303a7a
  1. 104
      Marlin/Configuration_adv.h
  2. 88
      Marlin/src/feature/spindle_laser.cpp
  3. 333
      Marlin/src/feature/spindle_laser.h
  4. 6
      Marlin/src/feature/spindle_laser_types.h
  5. 9
      Marlin/src/gcode/calibrate/G28.cpp
  6. 134
      Marlin/src/gcode/control/M3-M5.cpp
  7. 37
      Marlin/src/gcode/gcode.cpp
  8. 45
      Marlin/src/inc/SanityCheck.h
  9. 2
      Marlin/src/lcd/dogm/status_screen_DOGM.cpp
  10. 5
      Marlin/src/lcd/menu/menu_item.h
  11. 18
      Marlin/src/lcd/menu/menu_spindle_laser.cpp
  12. 286
      Marlin/src/module/planner.cpp
  13. 92
      Marlin/src/module/planner.h
  14. 236
      Marlin/src/module/stepper.cpp
  15. 19
      Marlin/src/module/stepper.h
  16. 5
      Marlin/src/module/temperature.cpp
  17. 4
      buildroot/tests/mega2560
  18. 137
      docs/Cutter.md

104
Marlin/Configuration_adv.h

@ -3668,8 +3668,11 @@
#endif #endif
// Define the minimum and maximum test pulse time values for a laser test fire function // Define the minimum and maximum test pulse time values for a laser test fire function
#define LASER_TEST_PULSE_MIN 1 // Used with Laser Control Menu #define LASER_TEST_PULSE_MIN 1 // (ms) Used with Laser Control Menu
#define LASER_TEST_PULSE_MAX 999 // Caution: Menu may not show more than 3 characters #define LASER_TEST_PULSE_MAX 999 // (ms) Caution: Menu may not show more than 3 characters
#define SPINDLE_LASER_POWERUP_DELAY 50 // (ms) Delay to allow the spindle/laser to come up to speed/power
#define SPINDLE_LASER_POWERDOWN_DELAY 50 // (ms) Delay to allow the spindle to stop
/** /**
* Laser Safety Timeout * Laser Safety Timeout
@ -3682,79 +3685,38 @@
#define LASER_SAFETY_TIMEOUT_MS 1000 // (ms) #define LASER_SAFETY_TIMEOUT_MS 1000 // (ms)
/** /**
* Enable inline laser power to be handled in the planner / stepper routines. * Any M3 or G1/2/3/5 command with the 'I' parameter enables continuous inline power mode.
* Inline power is specified by the I (inline) flag in an M3 command (e.g., M3 S20 I) *
* or by the 'S' parameter in G0/G1/G2/G3 moves (see LASER_MOVE_POWER). * e.g., 'M3 I' enables continuous inline power which is processed by the planner.
* Power is stored in move blocks and applied when blocks are processed by the Stepper ISR.
*
* 'M4 I' sets dynamic mode which uses the current feedrate to calculate a laser power OCR value.
* *
* This allows the laser to keep in perfect sync with the planner and removes * Any move in dynamic mode will use the current feedrate to calculate the laser power.
* the powerup/down delay since lasers require negligible time. * Feed rates are set by the F parameter of a move command e.g. G1 X0 Y10 F6000
* Laser power would be calculated by bit shifting off 8 LSB's. In binary this is div 256.
* The calculation gives us ocr values from 0 to 255, values over F65535 will be set as 255 .
* More refined power control such as compesation for accell/decell will be addressed in future releases.
*
* M5 I clears inline mode and set power to 0, M5 sets the power output to 0 but leaves inline mode on.
*/ */
//#define LASER_POWER_INLINE
#if ENABLED(LASER_POWER_INLINE)
/**
* Scale the laser's power in proportion to the movement rate.
*
* - Sets the entry power proportional to the entry speed over the nominal speed.
* - Ramps the power up every N steps to approximate the speed trapezoid.
* - Due to the limited power resolution this is only approximate.
*/
#define LASER_POWER_INLINE_TRAPEZOID
/**
* Continuously calculate the current power (nominal_power * current_rate / nominal_rate).
* Required for accurate power with non-trapezoidal acceleration (e.g., S_CURVE_ACCELERATION).
* This is a costly calculation so this option is discouraged on 8-bit AVR boards.
*
* LASER_POWER_INLINE_TRAPEZOID_CONT_PER defines how many step cycles there are between power updates. If your
* board isn't able to generate steps fast enough (and you are using LASER_POWER_INLINE_TRAPEZOID_CONT), increase this.
* Note that when this is zero it means it occurs every cycle; 1 means a delay wait one cycle then run, etc.
*/
//#define LASER_POWER_INLINE_TRAPEZOID_CONT
/**
* Stepper iterations between power updates. Increase this value if the board
* can't keep up with the processing demands of LASER_POWER_INLINE_TRAPEZOID_CONT.
* Disable (or set to 0) to recalculate power on every stepper iteration.
*/
//#define LASER_POWER_INLINE_TRAPEZOID_CONT_PER 10
/**
* Include laser power in G0/G1/G2/G3/G5 commands with the 'S' parameter
*/
//#define LASER_MOVE_POWER
#if ENABLED(LASER_MOVE_POWER)
// Turn off the laser on G0 moves with no power parameter.
// If a power parameter is provided, use that instead.
//#define LASER_MOVE_G0_OFF
// Turn off the laser on G28 homing. /**
//#define LASER_MOVE_G28_OFF * Enable M3 commands for laser mode inline power planner syncing.
#endif * This feature enables any M3 S-value to be injected into the block buffers while in
* CUTTER_MODE_CONTINUOUS. The option allows M3 laser power to be commited without waiting
/** * for a planner syncronization
* Inline flag inverted */
* //#define LASER_POWER_SYNC
* WARNING: M5 will NOT turn off the laser unless another move
* is done (so G-code files must end with 'M5 I').
*/
//#define LASER_POWER_INLINE_INVERT
/**
* Continuously apply inline power. ('M3 S3' == 'G1 S3' == 'M3 S3 I')
*
* The laser might do some weird things, so only enable this
* feature if you understand the implications.
*/
//#define LASER_POWER_INLINE_CONTINUOUS
#else
#define SPINDLE_LASER_POWERUP_DELAY 50 // (ms) Delay to allow the spindle/laser to come up to speed/power
#define SPINDLE_LASER_POWERDOWN_DELAY 50 // (ms) Delay to allow the spindle to stop
#endif /**
* Scale the laser's power in proportion to the movement rate.
*
* - Sets the entry power proportional to the entry speed over the nominal speed.
* - Ramps the power up every N steps to approximate the speed trapezoid.
* - Due to the limited power resolution this is only approximate.
*/
//#define LASER_POWER_TRAP
// //
// Laser I2C Ammeter (High precision INA226 low/high side module) // Laser I2C Ammeter (High precision INA226 low/high side module)

88
Marlin/src/feature/spindle_laser.cpp

@ -39,18 +39,26 @@
#endif #endif
SpindleLaser cutter; SpindleLaser cutter;
uint8_t SpindleLaser::power, bool SpindleLaser::enable_state; // Virtual enable state, controls enable pin if present and or apply power if > 0
uint8_t SpindleLaser::power, // Actual power output 0-255 ocr or "0 = off" > 0 = "on"
SpindleLaser::last_power_applied; // = 0 // Basic power state tracking SpindleLaser::last_power_applied; // = 0 // Basic power state tracking
#if ENABLED(LASER_FEATURE) #if ENABLED(LASER_FEATURE)
cutter_test_pulse_t SpindleLaser::testPulse = 50; // Test fire Pulse time ms value. cutter_test_pulse_t SpindleLaser::testPulse = 50; // (ms) Test fire pulse default duration
uint8_t SpindleLaser::last_block_power; // = 0 // Track power changes for dynamic inline power
feedRate_t SpindleLaser::feedrate_mm_m = 1500,
SpindleLaser::last_feedrate_mm_m; // = 0 // (mm/min) Track feedrate changes for dynamic power
#endif #endif
bool SpindleLaser::isReady; // Ready to apply power setting from the UI to OCR
cutter_power_t SpindleLaser::menuPower, // Power set via LCD menu in PWM, PERCENT, or RPM
SpindleLaser::unitPower; // LCD status power in PWM, PERCENT, or RPM
#if ENABLED(MARLIN_DEV_MODE) bool SpindleLaser::isReadyForUI = false; // Ready to apply power setting from the UI to OCR
cutter_frequency_t SpindleLaser::frequency; // PWM frequency setting; range: 2K - 50K CutterMode SpindleLaser::cutter_mode = CUTTER_MODE_STANDARD; // Default is standard mode
#endif
constexpr cutter_cpower_t SpindleLaser::power_floor;
cutter_power_t SpindleLaser::menuPower = 0, // Power value via LCD menu in PWM, PERCENT, or RPM based on configured format set by CUTTER_POWER_UNIT.
SpindleLaser::unitPower = 0; // Unit power is in PWM, PERCENT, or RPM based on CUTTER_POWER_UNIT.
cutter_frequency_t SpindleLaser::frequency; // PWM frequency setting; range: 2K - 50K
#define SPINDLE_LASER_PWM_OFF TERN(SPINDLE_LASER_PWM_INVERT, 255, 0) #define SPINDLE_LASER_PWM_OFF TERN(SPINDLE_LASER_PWM_INVERT, 255, 0)
/** /**
@ -65,14 +73,14 @@ void SpindleLaser::init() {
#if ENABLED(SPINDLE_CHANGE_DIR) #if ENABLED(SPINDLE_CHANGE_DIR)
OUT_WRITE(SPINDLE_DIR_PIN, SPINDLE_INVERT_DIR); // Init rotation to clockwise (M3) OUT_WRITE(SPINDLE_DIR_PIN, SPINDLE_INVERT_DIR); // Init rotation to clockwise (M3)
#endif #endif
#if ENABLED(HAL_CAN_SET_PWM_FREQ) && SPINDLE_LASER_FREQUENCY
frequency = SPINDLE_LASER_FREQUENCY;
hal.set_pwm_frequency(pin_t(SPINDLE_LASER_PWM_PIN), SPINDLE_LASER_FREQUENCY);
#endif
#if ENABLED(SPINDLE_LASER_USE_PWM) #if ENABLED(SPINDLE_LASER_USE_PWM)
SET_PWM(SPINDLE_LASER_PWM_PIN); SET_PWM(SPINDLE_LASER_PWM_PIN);
hal.set_pwm_duty(pin_t(SPINDLE_LASER_PWM_PIN), SPINDLE_LASER_PWM_OFF); // Set to lowest speed hal.set_pwm_duty(pin_t(SPINDLE_LASER_PWM_PIN), SPINDLE_LASER_PWM_OFF); // Set to lowest speed
#endif #endif
#if ENABLED(HAL_CAN_SET_PWM_FREQ) && SPINDLE_LASER_FREQUENCY
hal.set_pwm_frequency(pin_t(SPINDLE_LASER_PWM_PIN), SPINDLE_LASER_FREQUENCY);
TERN_(MARLIN_DEV_MODE, frequency = SPINDLE_LASER_FREQUENCY);
#endif
#if ENABLED(AIR_EVACUATION) #if ENABLED(AIR_EVACUATION)
OUT_WRITE(AIR_EVACUATION_PIN, !AIR_EVACUATION_ACTIVE); // Init Vacuum/Blower OFF OUT_WRITE(AIR_EVACUATION_PIN, !AIR_EVACUATION_ACTIVE); // Init Vacuum/Blower OFF
#endif #endif
@ -90,7 +98,7 @@ void SpindleLaser::init() {
*/ */
void SpindleLaser::_set_ocr(const uint8_t ocr) { void SpindleLaser::_set_ocr(const uint8_t ocr) {
#if ENABLED(HAL_CAN_SET_PWM_FREQ) && SPINDLE_LASER_FREQUENCY #if ENABLED(HAL_CAN_SET_PWM_FREQ) && SPINDLE_LASER_FREQUENCY
hal.set_pwm_frequency(pin_t(SPINDLE_LASER_PWM_PIN), TERN(MARLIN_DEV_MODE, frequency, SPINDLE_LASER_FREQUENCY)); hal.set_pwm_frequency(pin_t(SPINDLE_LASER_PWM_PIN), frequency);
#endif #endif
hal.set_pwm_duty(pin_t(SPINDLE_LASER_PWM_PIN), ocr ^ SPINDLE_LASER_PWM_OFF); hal.set_pwm_duty(pin_t(SPINDLE_LASER_PWM_PIN), ocr ^ SPINDLE_LASER_PWM_OFF);
} }
@ -107,35 +115,41 @@ void SpindleLaser::init() {
#endif // SPINDLE_LASER_USE_PWM #endif // SPINDLE_LASER_USE_PWM
/** /**
* Apply power for laser/spindle * Apply power for Laser or Spindle
* *
* Apply cutter power value for PWM, Servo, and on/off pin. * Apply cutter power value for PWM, Servo, and on/off pin.
* *
* @param opwr Power value. Range 0 to MAX. When 0 disable spindle/laser. * @param opwr Power value. Range 0 to MAX.
*/ */
void SpindleLaser::apply_power(const uint8_t opwr) { void SpindleLaser::apply_power(const uint8_t opwr) {
if (opwr == last_power_applied) return; if (enabled() || opwr == 0) { // 0 check allows us to disable where no ENA pin exists
last_power_applied = opwr; // Test and set the last power used to improve performance
power = opwr; if (opwr == last_power_applied) return;
#if ENABLED(SPINDLE_LASER_USE_PWM) last_power_applied = opwr;
if (cutter.unitPower == 0 && CUTTER_UNIT_IS(RPM)) { // Handle PWM driven or just simple on/off
ocr_off(); #if ENABLED(SPINDLE_LASER_USE_PWM)
isReady = false; if (CUTTER_UNIT_IS(RPM) && unitPower == 0)
} ocr_off();
else if (ENABLED(CUTTER_POWER_RELATIVE) || enabled()) { else if (ENABLED(CUTTER_POWER_RELATIVE) || enabled() || opwr == 0) {
set_ocr(power); set_ocr(opwr);
isReady = true; isReadyForUI = true;
} }
else { else
ocr_off(); ocr_off();
isReady = false; #elif ENABLED(SPINDLE_SERVO)
} MOVE_SERVO(SPINDLE_SERVO_NR, power);
#elif ENABLED(SPINDLE_SERVO) #else
servo[SPINDLE_SERVO_NR].move(power); WRITE(SPINDLE_LASER_ENA_PIN, enabled() ? SPINDLE_LASER_ACTIVE_STATE : !SPINDLE_LASER_ACTIVE_STATE);
#else isReadyForUI = true;
WRITE(SPINDLE_LASER_ENA_PIN, enabled() ? SPINDLE_LASER_ACTIVE_STATE : !SPINDLE_LASER_ACTIVE_STATE); #endif
isReady = true; }
#endif else {
#if PIN_EXISTS(SPINDLE_LASER_ENA)
WRITE(SPINDLE_LASER_ENA_PIN, !SPINDLE_LASER_ACTIVE_STATE);
#endif
isReadyForUI = false; // Only used for UI display updates.
TERN_(SPINDLE_LASER_USE_PWM, ocr_off());
}
} }
#if ENABLED(SPINDLE_CHANGE_DIR) #if ENABLED(SPINDLE_CHANGE_DIR)

333
Marlin/src/feature/spindle_laser.h

@ -34,85 +34,98 @@
#include "../libs/buzzer.h" #include "../libs/buzzer.h"
#endif #endif
#if ENABLED(LASER_POWER_INLINE) // Inline laser power
#include "../module/planner.h" #include "../module/planner.h"
#endif
#define PCT_TO_PWM(X) ((X) * 255 / 100) #define PCT_TO_PWM(X) ((X) * 255 / 100)
#define PCT_TO_SERVO(X) ((X) * 180 / 100) #define PCT_TO_SERVO(X) ((X) * 180 / 100)
// Laser/Cutter operation mode
enum CutterMode : int8_t {
CUTTER_MODE_ERROR = -1,
CUTTER_MODE_STANDARD, // M3 power is applied directly and waits for planner moves to sync.
CUTTER_MODE_CONTINUOUS, // M3 or G1/2/3 move power is controlled within planner blocks, set with 'M3 I', cleared with 'M5 I'.
CUTTER_MODE_DYNAMIC // M4 laser power is proportional to the feed rate, set with 'M4 I', cleared with 'M5 I'.
};
class SpindleLaser { class SpindleLaser {
public: public:
static const inline uint8_t pct_to_ocr(const_float_t pct) { return uint8_t(PCT_TO_PWM(pct)); } static CutterMode cutter_mode;
// cpower = configured values (e.g., SPEED_POWER_MAX) static constexpr uint8_t pct_to_ocr(const_float_t pct) { return uint8_t(PCT_TO_PWM(pct)); }
// cpower = configured values (e.g., SPEED_POWER_MAX)
// Convert configured power range to a percentage // Convert configured power range to a percentage
static const inline uint8_t cpwr_to_pct(const cutter_cpower_t cpwr) { static constexpr cutter_cpower_t power_floor = TERN(CUTTER_POWER_RELATIVE, SPEED_POWER_MIN, 0);
constexpr cutter_cpower_t power_floor = TERN(CUTTER_POWER_RELATIVE, SPEED_POWER_MIN, 0), static constexpr uint8_t cpwr_to_pct(const cutter_cpower_t cpwr) {
power_range = SPEED_POWER_MAX - power_floor; return cpwr ? round(100.0f * (cpwr - power_floor) / (SPEED_POWER_MAX - power_floor)) : 0;
return cpwr ? round(100.0f * (cpwr - power_floor) / power_range) : 0;
} }
// Convert a cpower (e.g., SPEED_POWER_STARTUP) to unit power (upwr, upower), // Convert config defines from RPM to %, angle or PWM when in Spindle mode
// which can be PWM, Percent, Servo angle, or RPM (rel/abs). // and convert from PERCENT to PWM when in Laser mode
static const inline cutter_power_t cpwr_to_upwr(const cutter_cpower_t cpwr) { // STARTUP power to Unit power static constexpr cutter_power_t cpwr_to_upwr(const cutter_cpower_t cpwr) { // STARTUP power to Unit power
const cutter_power_t upwr = ( return (
#if ENABLED(SPINDLE_FEATURE) #if ENABLED(SPINDLE_FEATURE)
// Spindle configured values are in RPM // Spindle configured define values are in RPM
#if CUTTER_UNIT_IS(RPM) #if CUTTER_UNIT_IS(RPM)
cpwr // to RPM cpwr // to same
#elif CUTTER_UNIT_IS(PERCENT) // to PCT #elif CUTTER_UNIT_IS(PERCENT)
cpwr_to_pct(cpwr) cpwr_to_pct(cpwr) // to Percent
#elif CUTTER_UNIT_IS(SERVO) // to SERVO angle #elif CUTTER_UNIT_IS(SERVO)
PCT_TO_SERVO(cpwr_to_pct(cpwr)) PCT_TO_SERVO(cpwr_to_pct(cpwr)) // to SERVO angle
#else // to PWM #else
PCT_TO_PWM(cpwr_to_pct(cpwr)) PCT_TO_PWM(cpwr_to_pct(cpwr)) // to PWM
#endif #endif
#else #else
// Laser configured values are in PCT // Laser configured define values are in Percent
#if CUTTER_UNIT_IS(PWM255) #if CUTTER_UNIT_IS(PWM255)
PCT_TO_PWM(cpwr) PCT_TO_PWM(cpwr) // to PWM
#else #else
cpwr // to RPM/PCT cpwr // to same
#endif #endif
#endif #endif
); );
return upwr;
} }
static const cutter_power_t mpower_min() { return cpwr_to_upwr(SPEED_POWER_MIN); } static constexpr cutter_power_t mpower_min() { return cpwr_to_upwr(SPEED_POWER_MIN); }
static const cutter_power_t mpower_max() { return cpwr_to_upwr(SPEED_POWER_MAX); } static constexpr cutter_power_t mpower_max() { return cpwr_to_upwr(SPEED_POWER_MAX); }
#if ENABLED(LASER_FEATURE) #if ENABLED(LASER_FEATURE)
static cutter_test_pulse_t testPulse; // Test fire Pulse ms value static cutter_test_pulse_t testPulse; // (ms) Test fire pulse duration
static uint8_t last_block_power; // Track power changes for dynamic power
static feedRate_t feedrate_mm_m, last_feedrate_mm_m; // (mm/min) Track feedrate changes for dynamic power
static bool laser_feedrate_changed() {
const bool changed = last_feedrate_mm_m != feedrate_mm_m;
if (changed) last_feedrate_mm_m = feedrate_mm_m;
return changed;
}
#endif #endif
static bool isReady; // Ready to apply power setting from the UI to OCR static bool isReadyForUI; // Ready to apply power setting from the UI to OCR
static bool enable_state;
static uint8_t power, static uint8_t power,
last_power_applied; // Basic power state tracking last_power_applied; // Basic power state tracking
#if ENABLED(MARLIN_DEV_MODE) static cutter_frequency_t frequency; // Set PWM frequency; range: 2K-50K
static cutter_frequency_t frequency; // Set PWM frequency; range: 2K-50K
#endif
static cutter_power_t menuPower, // Power as set via LCD menu in PWM, Percentage or RPM static cutter_power_t menuPower, // Power as set via LCD menu in PWM, Percentage or RPM
unitPower; // Power as displayed status in PWM, Percentage or RPM unitPower; // Power as displayed status in PWM, Percentage or RPM
static void init(); static void init();
#if ENABLED(MARLIN_DEV_MODE) #if ENABLED(HAL_CAN_SET_PWM_FREQ) && SPINDLE_LASER_FREQUENCY
static void refresh_frequency() { hal.set_pwm_frequency(pin_t(SPINDLE_LASER_PWM_PIN), frequency); } static void refresh_frequency() { hal.set_pwm_frequency(pin_t(SPINDLE_LASER_PWM_PIN), frequency); }
#endif #endif
// Modifying this function should update everywhere // Modifying this function should update everywhere
static bool enabled(const cutter_power_t opwr) { return opwr > 0; } static bool enabled(const cutter_power_t opwr) { return opwr > 0; }
static bool enabled() { return enabled(power); } static bool enabled() { return enable_state; }
static void apply_power(const uint8_t inpow); static void apply_power(const uint8_t inpow);
FORCE_INLINE static void refresh() { apply_power(power); } FORCE_INLINE static void refresh() { apply_power(power); }
FORCE_INLINE static void set_power(const uint8_t upwr) { power = upwr; refresh(); }
#if ENABLED(SPINDLE_LASER_USE_PWM) #if ENABLED(SPINDLE_LASER_USE_PWM)
@ -123,7 +136,6 @@ public:
public: public:
static void set_ocr(const uint8_t ocr); static void set_ocr(const uint8_t ocr);
static void ocr_set_power(const uint8_t ocr) { power = ocr; set_ocr(ocr); }
static void ocr_off(); static void ocr_off();
/** /**
@ -141,78 +153,76 @@ public:
); );
} }
/**
* Correct power to configured range
*/
static cutter_power_t power_to_range(const cutter_power_t pwr) {
return power_to_range(pwr, _CUTTER_POWER(CUTTER_POWER_UNIT));
}
static cutter_power_t power_to_range(const cutter_power_t pwr, const uint8_t pwrUnit) {
static constexpr float
min_pct = TERN(CUTTER_POWER_RELATIVE, 0, TERN(SPINDLE_FEATURE, round(100.0f * (SPEED_POWER_MIN) / (SPEED_POWER_MAX)), SPEED_POWER_MIN)),
max_pct = TERN(SPINDLE_FEATURE, 100, SPEED_POWER_MAX);
if (pwr <= 0) return 0;
cutter_power_t upwr;
switch (pwrUnit) {
case _CUTTER_POWER_PWM255:
upwr = cutter_power_t(
(pwr < pct_to_ocr(min_pct)) ? pct_to_ocr(min_pct) // Use minimum if set below
: (pwr > pct_to_ocr(max_pct)) ? pct_to_ocr(max_pct) // Use maximum if set above
: pwr
);
break;
case _CUTTER_POWER_PERCENT:
upwr = cutter_power_t(
(pwr < min_pct) ? min_pct // Use minimum if set below
: (pwr > max_pct) ? max_pct // Use maximum if set above
: pwr // PCT
);
break;
case _CUTTER_POWER_RPM:
upwr = cutter_power_t(
(pwr < SPEED_POWER_MIN) ? SPEED_POWER_MIN // Use minimum if set below
: (pwr > SPEED_POWER_MAX) ? SPEED_POWER_MAX // Use maximum if set above
: pwr // Calculate OCR value
);
break;
default: break;
}
return upwr;
}
#endif // SPINDLE_LASER_USE_PWM #endif // SPINDLE_LASER_USE_PWM
/** /**
* Enable/Disable spindle/laser * Correct power to configured range
* @param enable true = enable; false = disable
*/ */
static void set_enabled(const bool enable) { static cutter_power_t power_to_range(const cutter_power_t pwr, const uint8_t pwrUnit=_CUTTER_POWER(CUTTER_POWER_UNIT)) {
uint8_t value = 0; static constexpr float
if (enable) { min_pct = TERN(CUTTER_POWER_RELATIVE, 0, TERN(SPINDLE_FEATURE, round(100.0f * (SPEED_POWER_MIN) / (SPEED_POWER_MAX)), SPEED_POWER_MIN)),
#if ENABLED(SPINDLE_LASER_USE_PWM) max_pct = TERN(SPINDLE_FEATURE, 100, SPEED_POWER_MAX);
if (power) if (pwr <= 0) return 0;
value = power; cutter_power_t upwr;
else if (unitPower) switch (pwrUnit) {
value = upower_to_ocr(cpwr_to_upwr(SPEED_POWER_STARTUP)); case _CUTTER_POWER_PWM255: { // PWM
#else const uint8_t pmin = pct_to_ocr(min_pct), pmax = pct_to_ocr(max_pct);
value = 255; upwr = cutter_power_t(constrain(pwr, pmin, pmax));
#endif } break;
case _CUTTER_POWER_PERCENT: // Percent
upwr = cutter_power_t(constrain(pwr, min_pct, max_pct));
break;
case _CUTTER_POWER_RPM: // Calculate OCR value
upwr = cutter_power_t(constrain(pwr, SPEED_POWER_MIN, SPEED_POWER_MAX));
break;
default: break;
} }
set_power(value); return upwr;
} }
static void disable() { isReady = false; set_enabled(false); }
/** /**
* Wait for spindle to spin up or spin down * Enable Laser or Spindle output.
* It's important to prevent changing the power output value during inline cutter operation.
* Inline power is adjusted in the planner to support LASER_TRAP_POWER and CUTTER_MODE_DYNAMIC mode.
*
* This method accepts one of the following control states:
*
* - For CUTTER_MODE_STANDARD the cutter power is either full on/off or ocr-based and it will apply
* SPEED_POWER_STARTUP if no value is assigned.
* *
* @param on true = state to on; false = state to off. * - For CUTTER_MODE_CONTINUOUS inline and power remains where last set and the cutter output enable flag is set.
*
* - CUTTER_MODE_DYNAMIC is also inline-based and it just sets the enable output flag.
*
* - For CUTTER_MODE_ERROR set the output enable_state flag directly and set power to 0 for any mode.
* This mode allows a global power shutdown action to occur.
*/ */
static void power_delay(const bool on) { static void set_enabled(const bool enable) {
#if DISABLED(LASER_POWER_INLINE) switch (cutter_mode) {
safe_delay(on ? SPINDLE_LASER_POWERUP_DELAY : SPINDLE_LASER_POWERDOWN_DELAY); case CUTTER_MODE_STANDARD:
apply_power(enable ? TERN(SPINDLE_LASER_USE_PWM, (power ?: (unitPower ? upower_to_ocr(cpwr_to_upwr(SPEED_POWER_STARTUP)) : 0)), 255) : 0);
break;
case CUTTER_MODE_CONTINUOUS:
TERN_(LASER_FEATURE, set_inline_enabled(enable));
break;
case CUTTER_MODE_DYNAMIC:
TERN_(LASER_FEATURE, set_inline_enabled(enable));
break;
case CUTTER_MODE_ERROR: // Error mode, no enable and kill power.
enable_state = false;
apply_power(0);
}
#if SPINDLE_LASER_ENA_PIN
WRITE(SPINDLE_LASER_ENA_PIN, enable ? SPINDLE_LASER_ACTIVE_STATE : !SPINDLE_LASER_ACTIVE_STATE);
#endif #endif
enable_state = enable;
}
static void disable() { isReadyForUI = false; set_enabled(false); }
// Wait for spindle/laser to startup or shutdown
static void power_delay(const bool on) {
safe_delay(on ? SPINDLE_LASER_POWERUP_DELAY : SPINDLE_LASER_POWERDOWN_DELAY);
} }
#if ENABLED(SPINDLE_CHANGE_DIR) #if ENABLED(SPINDLE_CHANGE_DIR)
@ -224,47 +234,60 @@ public:
#endif #endif
#if ENABLED(AIR_EVACUATION) #if ENABLED(AIR_EVACUATION)
static void air_evac_enable(); // Turn On Cutter Vacuum or Laser Blower motor static void air_evac_enable(); // Turn On Cutter Vacuum or Laser Blower motor
static void air_evac_disable(); // Turn Off Cutter Vacuum or Laser Blower motor static void air_evac_disable(); // Turn Off Cutter Vacuum or Laser Blower motor
static void air_evac_toggle(); // Toggle Cutter Vacuum or Laser Blower motor static void air_evac_toggle(); // Toggle Cutter Vacuum or Laser Blower motor
static bool air_evac_state() { // Get current state static bool air_evac_state() { // Get current state
return (READ(AIR_EVACUATION_PIN) == AIR_EVACUATION_ACTIVE); return (READ(AIR_EVACUATION_PIN) == AIR_EVACUATION_ACTIVE);
} }
#endif #endif
#if ENABLED(AIR_ASSIST) #if ENABLED(AIR_ASSIST)
static void air_assist_enable(); // Turn on air assist static void air_assist_enable(); // Turn on air assist
static void air_assist_disable(); // Turn off air assist static void air_assist_disable(); // Turn off air assist
static void air_assist_toggle(); // Toggle air assist static void air_assist_toggle(); // Toggle air assist
static bool air_assist_state() { // Get current state static bool air_assist_state() { // Get current state
return (READ(AIR_ASSIST_PIN) == AIR_ASSIST_ACTIVE); return (READ(AIR_ASSIST_PIN) == AIR_ASSIST_ACTIVE);
} }
#endif #endif
#if HAS_MARLINUI_MENU #if HAS_MARLINUI_MENU
static void enable_with_dir(const bool reverse) {
isReady = true; #if ENABLED(SPINDLE_FEATURE)
const uint8_t ocr = TERN(SPINDLE_LASER_USE_PWM, upower_to_ocr(menuPower), 255); static void enable_with_dir(const bool reverse) {
if (menuPower) isReadyForUI = true;
power = ocr; const uint8_t ocr = TERN(SPINDLE_LASER_USE_PWM, upower_to_ocr(menuPower), 255);
else if (menuPower)
menuPower = cpwr_to_upwr(SPEED_POWER_STARTUP); power = ocr;
unitPower = menuPower; else
set_reverse(reverse); menuPower = cpwr_to_upwr(SPEED_POWER_STARTUP);
set_enabled(true); unitPower = menuPower;
} set_reverse(reverse);
FORCE_INLINE static void enable_forward() { enable_with_dir(false); } set_enabled(true);
FORCE_INLINE static void enable_reverse() { enable_with_dir(true); } }
FORCE_INLINE static void enable_same_dir() { enable_with_dir(is_reverse()); } FORCE_INLINE static void enable_forward() { enable_with_dir(false); }
FORCE_INLINE static void enable_reverse() { enable_with_dir(true); }
FORCE_INLINE static void enable_same_dir() { enable_with_dir(is_reverse()); }
#endif // SPINDLE_FEATURE
#if ENABLED(SPINDLE_LASER_USE_PWM) #if ENABLED(SPINDLE_LASER_USE_PWM)
static void update_from_mpower() { static void update_from_mpower() {
if (isReady) power = upower_to_ocr(menuPower); if (isReadyForUI) power = upower_to_ocr(menuPower);
unitPower = menuPower; unitPower = menuPower;
} }
#endif #endif
#if ENABLED(LASER_FEATURE) #if ENABLED(LASER_FEATURE)
// Toggle the laser on/off with menuPower. Apply SPEED_POWER_STARTUP if it was 0 on entry.
static void laser_menu_toggle(const bool state) {
set_enabled(state);
if (state) {
if (!menuPower) menuPower = cpwr_to_upwr(SPEED_POWER_STARTUP);
power = upower_to_ocr(menuPower);
apply_power(power);
}
}
/** /**
* Test fire the laser using the testPulse ms duration * Test fire the laser using the testPulse ms duration
* Also fires with any PWM power that was previous set * Also fires with any PWM power that was previous set
@ -272,74 +295,36 @@ public:
*/ */
static void test_fire_pulse() { static void test_fire_pulse() {
TERN_(HAS_BEEPER, buzzer.tone(30, 3000)); TERN_(HAS_BEEPER, buzzer.tone(30, 3000));
enable_forward(); // Turn Laser on (Spindle speak but same funct) cutter_mode = CUTTER_MODE_STANDARD;// Menu needs standard mode.
laser_menu_toggle(true); // Laser On
delay(testPulse); // Delay for time set by user in pulse ms menu screen. delay(testPulse); // Delay for time set by user in pulse ms menu screen.
disable(); // Turn laser off laser_menu_toggle(false); // Laser Off
} }
#endif #endif // LASER_FEATURE
#endif // HAS_MARLINUI_MENU #endif // HAS_MARLINUI_MENU
#if ENABLED(LASER_POWER_INLINE) #if ENABLED(LASER_FEATURE)
/**
* Inline power adds extra fields to the planner block
* to handle laser power and scale to movement speed.
*/
// Force disengage planner power control // Dynamic mode rate calculation
static void inline_disable() { static uint8_t calc_dynamic_power() {
isReady = false; if (feedrate_mm_m > 65535) return 255; // Too fast, go always on
unitPower = 0; uint16_t rate = uint16_t(feedrate_mm_m); // 16 bits from the G-code parser float input
planner.laser_inline.status.isPlanned = false; rate >>= 8; // Take the G-code input e.g. F40000 and shift off the lower bits to get an OCR value from 1-255
planner.laser_inline.status.isEnabled = false; return uint8_t(rate);
planner.laser_inline.power = 0;
} }
// Inline modes of all other functions; all enable planner inline power control // Inline modes of all other functions; all enable planner inline power control
static void set_inline_enabled(const bool enable) { static void set_inline_enabled(const bool enable) { planner.laser_inline.status.isEnabled = enable;}
if (enable)
inline_power(255);
else {
isReady = false;
unitPower = menuPower = 0;
planner.laser_inline.status.isPlanned = false;
TERN(SPINDLE_LASER_USE_PWM, inline_ocr_power, inline_power)(0);
}
}
// Set the power for subsequent movement blocks // Set the power for subsequent movement blocks
static void inline_power(const cutter_power_t upwr) { static void inline_power(const cutter_power_t cpwr) {
unitPower = menuPower = upwr; TERN(SPINDLE_LASER_USE_PWM, power = planner.laser_inline.power = cpwr, planner.laser_inline.power = cpwr > 0 ? 255 : 0);
#if ENABLED(SPINDLE_LASER_USE_PWM)
#if ENABLED(SPEED_POWER_RELATIVE) && !CUTTER_UNIT_IS(RPM) // relative mode does not turn laser off at 0, except for RPM
planner.laser_inline.status.isEnabled = true;
planner.laser_inline.power = upower_to_ocr(upwr);
isReady = true;
#else
inline_ocr_power(upower_to_ocr(upwr));
#endif
#else
planner.laser_inline.status.isEnabled = enabled(upwr);
planner.laser_inline.power = upwr;
isReady = enabled(upwr);
#endif
} }
static void inline_direction(const bool) { /* never */ } #endif // LASER_FEATURE
#if ENABLED(SPINDLE_LASER_USE_PWM)
static void inline_ocr_power(const uint8_t ocrpwr) {
isReady = ocrpwr > 0;
planner.laser_inline.status.isEnabled = ocrpwr > 0;
planner.laser_inline.power = ocrpwr;
}
#endif
#endif // LASER_POWER_INLINE
static void kill() { static void kill() { disable(); }
TERN_(LASER_POWER_INLINE, inline_disable());
disable();
}
}; };
extern SpindleLaser cutter; extern SpindleLaser cutter;

6
Marlin/src/feature/spindle_laser_types.h

@ -74,12 +74,10 @@ typedef IF<(SPEED_POWER_MAX > 255), uint16_t, uint8_t>::type cutter_cpower_t;
#endif #endif
#endif #endif
typedef uint16_t cutter_frequency_t;
#if ENABLED(LASER_FEATURE) #if ENABLED(LASER_FEATURE)
typedef uint16_t cutter_test_pulse_t; typedef uint16_t cutter_test_pulse_t;
#define CUTTER_MENU_PULSE_TYPE uint16_3 #define CUTTER_MENU_PULSE_TYPE uint16_3
#endif
#if ENABLED(MARLIN_DEV_MODE)
typedef uint16_t cutter_frequency_t;
#define CUTTER_MENU_FREQUENCY_TYPE uint16_5 #define CUTTER_MENU_FREQUENCY_TYPE uint16_5
#endif #endif

9
Marlin/src/gcode/calibrate/G28.cpp

@ -59,7 +59,7 @@
#include "../../libs/L64XX/L64XX_Marlin.h" #include "../../libs/L64XX/L64XX_Marlin.h"
#endif #endif
#if ENABLED(LASER_MOVE_G28_OFF) #if ENABLED(LASER_FEATURE)
#include "../../feature/spindle_laser.h" #include "../../feature/spindle_laser.h"
#endif #endif
@ -205,7 +205,12 @@ void GcodeSuite::G28() {
DEBUG_SECTION(log_G28, "G28", DEBUGGING(LEVELING)); DEBUG_SECTION(log_G28, "G28", DEBUGGING(LEVELING));
if (DEBUGGING(LEVELING)) log_machine_info(); if (DEBUGGING(LEVELING)) log_machine_info();
TERN_(LASER_MOVE_G28_OFF, cutter.set_inline_enabled(false)); // turn off laser /*
* Set the laser power to false to stop the planner from processing the current power setting.
*/
#if ENABLED(LASER_FEATURE)
planner.laser_inline.status.isPowered = false;
#endif
#if ENABLED(DUAL_X_CARRIAGE) #if ENABLED(DUAL_X_CARRIAGE)
bool IDEX_saved_duplication_state = extruder_duplication_enabled; bool IDEX_saved_duplication_state = extruder_duplication_enabled;

134
Marlin/src/gcode/control/M3-M5.cpp

@ -31,17 +31,27 @@
/** /**
* Laser: * Laser:
* M3 - Laser ON/Power (Ramped power) * M3 - Laser ON/Power (Ramped power)
* M4 - Laser ON/Power (Continuous power) * M4 - Laser ON/Power (Ramped power)
* M5 - Set power output to 0 (leaving inline mode unchanged).
*
* M3I - Enable continuous inline power to be processed by the planner, with power
* calculated and set in the planner blocks, processed inline during stepping.
* Within inline mode M3 S-Values will set the power for the next moves e.g. G1 X10 Y10 powers on with the last S-Value.
* M3I must be set before using planner-synced M3 inline S-Values (LASER_POWER_SYNC).
*
* M4I - Set dynamic mode which calculates laser power OCR based on the current feedrate.
*
* M5I - Clear inline mode and set power to 0.
* *
* Spindle: * Spindle:
* M3 - Spindle ON (Clockwise) * M3 - Spindle ON (Clockwise)
* M4 - Spindle ON (Counter-clockwise) * M4 - Spindle ON (Counter-clockwise)
* M5 - Spindle OFF
* *
* Parameters: * Parameters:
* S<power> - Set power. S0 will turn the spindle/laser off, except in relative mode. * S<power> - Set power. S0 will turn the spindle/laser off.
* O<ocr> - Set power and OCR (oscillator count register)
* *
* If no PWM pin is defined then M3/M4 just turns it on. * If no PWM pin is defined then M3/M4 just turns it on or off.
* *
* At least 12.8kHz (50Hz * 256) is needed for Spindle PWM. * At least 12.8kHz (50Hz * 256) is needed for Spindle PWM.
* Hardware PWM is required on AVR. ISRs are too slow. * Hardware PWM is required on AVR. ISRs are too slow.
@ -70,77 +80,77 @@ void GcodeSuite::M3_M4(const bool is_M4) {
reset_stepper_timeout(); // Reset timeout to allow subsequent G-code to power the laser (imm.) reset_stepper_timeout(); // Reset timeout to allow subsequent G-code to power the laser (imm.)
#endif #endif
#if EITHER(SPINDLE_LASER_USE_PWM, SPINDLE_SERVO) if (cutter.cutter_mode == CUTTER_MODE_STANDARD)
auto get_s_power = [] { planner.synchronize(); // Wait for previous movement commands (G0/G1/G2/G3) to complete before changing power
if (parser.seenval('S')) {
const float spwr = parser.value_float();
#if ENABLED(SPINDLE_SERVO)
cutter.unitPower = spwr;
#else
cutter.unitPower = TERN(SPINDLE_LASER_USE_PWM,
cutter.power_to_range(cutter_power_t(round(spwr))),
spwr > 0 ? 255 : 0);
#endif
}
else
cutter.unitPower = cutter.cpwr_to_upwr(SPEED_POWER_STARTUP);
return cutter.unitPower;
};
#endif
#if ENABLED(LASER_POWER_INLINE) #if ENABLED(LASER_FEATURE)
if (parser.seen('I') == DISABLED(LASER_POWER_INLINE_INVERT)) { if (parser.seen_test('I')) {
// Laser power in inline mode cutter.cutter_mode = is_M4 ? CUTTER_MODE_DYNAMIC : CUTTER_MODE_CONTINUOUS;
cutter.inline_direction(is_M4); // Should always be unused cutter.inline_power(0);
#if ENABLED(SPINDLE_LASER_USE_PWM) cutter.set_enabled(true);
if (parser.seenval('O')) {
cutter.unitPower = cutter.power_to_range(parser.value_byte(), 0);
cutter.inline_ocr_power(cutter.unitPower); // The OCR is a value from 0 to 255 (uint8_t)
}
else
cutter.inline_power(cutter.upower_to_ocr(get_s_power()));
#else
cutter.set_inline_enabled(true);
#endif
return;
} }
// Non-inline, standard case
cutter.inline_disable(); // Prevent future blocks re-setting the power
#endif #endif
planner.synchronize(); // Wait for previous movement commands (G0/G0/G2/G3) to complete before changing power auto get_s_power = [] {
cutter.set_reverse(is_M4); float u;
if (parser.seenval('S')) {
#if ENABLED(SPINDLE_LASER_USE_PWM) const float v = parser.value_float();
if (parser.seenval('O')) { u = TERN(LASER_POWER_TRAP, v, cutter.power_to_range(v));
cutter.unitPower = cutter.power_to_range(parser.value_byte(), 0);
cutter.ocr_set_power(cutter.unitPower); // The OCR is a value from 0 to 255 (uint8_t)
} }
else else if (cutter.cutter_mode == CUTTER_MODE_STANDARD)
cutter.set_power(cutter.upower_to_ocr(get_s_power())); u = cutter.cpwr_to_upwr(SPEED_POWER_STARTUP);
#elif ENABLED(SPINDLE_SERVO)
cutter.set_power(get_s_power()); cutter.menuPower = cutter.unitPower = u;
#else
// PWM not implied, power converted to OCR from unit definition and on/off if not PWM.
cutter.power = TERN(SPINDLE_LASER_USE_PWM, cutter.upower_to_ocr(u), u > 0 ? 255 : 0);
return u;
};
if (cutter.cutter_mode == CUTTER_MODE_CONTINUOUS || cutter.cutter_mode == CUTTER_MODE_DYNAMIC) { // Laser power in inline mode
#if ENABLED(LASER_FEATURE)
planner.laser_inline.status.isPowered = true; // M3 or M4 is powered either way
get_s_power(); // Update cutter.power if seen
#if ENABLED(LASER_POWER_SYNC)
// With power sync we only set power so it does not effect queued inline power sets
planner.buffer_sync_block(BLOCK_BIT_LASER_PWR); // Send the flag, queueing inline power
#else
planner.synchronize();
cutter.inline_power(cutter.power);
#endif
#endif
}
else {
cutter.set_enabled(true); cutter.set_enabled(true);
#endif get_s_power();
cutter.menuPower = cutter.unitPower; cutter.apply_power(
#if ENABLED(SPINDLE_SERVO)
cutter.unitPower
#elif ENABLED(SPINDLE_LASER_USE_PWM)
cutter.upower_to_ocr(cutter.unitPower)
#else
cutter.unitPower > 0 ? 255 : 0
#endif
);
TERN_(SPINDLE_CHANGE_DIR, cutter.set_reverse(is_M4));
}
} }
/** /**
* M5 - Cutter OFF (when moves are complete) * M5 - Cutter OFF (when moves are complete)
*/ */
void GcodeSuite::M5() { void GcodeSuite::M5() {
#if ENABLED(LASER_POWER_INLINE)
if (parser.seen('I') == DISABLED(LASER_POWER_INLINE_INVERT)) {
cutter.set_inline_enabled(false); // Laser power in inline mode
return;
}
// Non-inline, standard case
cutter.inline_disable(); // Prevent future blocks re-setting the power
#endif
planner.synchronize(); planner.synchronize();
cutter.set_enabled(false); cutter.power = 0;
cutter.menuPower = cutter.unitPower; cutter.apply_power(0); // M5 just kills power, leaving inline mode unchanged
if (cutter.cutter_mode != CUTTER_MODE_STANDARD) {
if (parser.seen_test('I')) {
TERN_(LASER_FEATURE, cutter.inline_power(cutter.power));
cutter.set_enabled(false); // Needs to happen while we are in inline mode to clear inline power.
cutter.cutter_mode = CUTTER_MODE_STANDARD; // Switch from inline to standard mode.
}
}
cutter.set_enabled(false); // Disable enable output setting
} }
#endif // HAS_CUTTER #endif // HAS_CUTTER

37
Marlin/src/gcode/gcode.cpp

@ -53,7 +53,7 @@ GcodeSuite gcode;
#include "../feature/cancel_object.h" #include "../feature/cancel_object.h"
#endif #endif
#if ENABLED(LASER_MOVE_POWER) #if ENABLED(LASER_FEATURE)
#include "../feature/spindle_laser.h" #include "../feature/spindle_laser.h"
#endif #endif
@ -210,8 +210,11 @@ void GcodeSuite::get_destination_from_command() {
recovery.save(); recovery.save();
#endif #endif
if (parser.floatval('F') > 0) if (parser.floatval('F') > 0) {
feedrate_mm_s = parser.value_feedrate(); feedrate_mm_s = parser.value_feedrate();
// Update the cutter feed rate for use by M4 I set inline moves.
TERN_(LASER_FEATURE, cutter.feedrate_mm_m = MMS_TO_MMM(feedrate_mm_s));
}
#if BOTH(PRINTCOUNTER, HAS_EXTRUDERS) #if BOTH(PRINTCOUNTER, HAS_EXTRUDERS)
if (!DEBUGGING(DRYRUN) && !skip_move) if (!DEBUGGING(DRYRUN) && !skip_move)
@ -223,15 +226,29 @@ void GcodeSuite::get_destination_from_command() {
M165(); M165();
#endif #endif
#if ENABLED(LASER_MOVE_POWER) #if ENABLED(LASER_FEATURE)
// Set the laser power in the planner to configure this move if (cutter.cutter_mode == CUTTER_MODE_CONTINUOUS || cutter.cutter_mode == CUTTER_MODE_DYNAMIC) {
if (parser.seen('S')) { // Set the cutter power in the planner to configure this move
const float spwr = parser.value_float(); cutter.last_feedrate_mm_m = 0;
cutter.inline_power(TERN(SPINDLE_LASER_USE_PWM, cutter.power_to_range(cutter_power_t(round(spwr))), spwr > 0 ? 255 : 0)); if (WITHIN(parser.codenum, 1, TERN(ARC_SUPPORT, 3, 1)) || TERN0(BEZIER_CURVE_SUPPORT, parser.codenum == 5)) {
planner.laser_inline.status.isPowered = true;
if (parser.seen('I')) cutter.set_enabled(true); // This is set for backward LightBurn compatibility.
if (parser.seen('S')) {
const float v = parser.value_float(),
u = TERN(LASER_POWER_TRAP, v, cutter.power_to_range(v));
cutter.menuPower = cutter.unitPower = u;
cutter.inline_power(TERN(SPINDLE_LASER_USE_PWM, cutter.upower_to_ocr(u), u > 0 ? 255 : 0));
}
}
else if (parser.codenum == 0) {
// For dynamic mode we need to flag isPowered off, dynamic power is calculated in the stepper based on feedrate.
if (cutter.cutter_mode == CUTTER_MODE_DYNAMIC) planner.laser_inline.status.isPowered = false;
cutter.inline_power(0); // This is planner-based so only set power and do not disable inline control flags.
}
} }
else if (ENABLED(LASER_MOVE_G0_OFF) && parser.codenum == 0) // G0 else if (parser.codenum == 0)
cutter.set_inline_enabled(false); cutter.apply_power(0);
#endif #endif // LASER_FEATURE
} }
/** /**

45
Marlin/src/inc/SanityCheck.h

@ -447,6 +447,16 @@
#error "SPINDLE_LASER_ACTIVE_HIGH is now SPINDLE_LASER_ACTIVE_STATE." #error "SPINDLE_LASER_ACTIVE_HIGH is now SPINDLE_LASER_ACTIVE_STATE."
#elif defined(SPINDLE_LASER_ENABLE_INVERT) #elif defined(SPINDLE_LASER_ENABLE_INVERT)
#error "SPINDLE_LASER_ENABLE_INVERT is now SPINDLE_LASER_ACTIVE_STATE." #error "SPINDLE_LASER_ENABLE_INVERT is now SPINDLE_LASER_ACTIVE_STATE."
#elif defined(LASER_POWER_INLINE)
#error "LASER_POWER_INLINE is not required, inline mode is enabled with 'M3 I' and disabled with 'M5 I'."
#elif defined(LASER_POWER_INLINE_TRAPEZOID)
#error "LASER_POWER_INLINE_TRAPEZOID is now LASER_POWER_TRAP."
#elif defined(LASER_POWER_INLINE_TRAPEZOID_CONT)
#error "LASER_POWER_INLINE_TRAPEZOID_CONT is replaced with LASER_POWER_TRAP."
#elif defined(LASER_POWER_INLINE_TRAPEZOID_PER)
#error "LASER_POWER_INLINE_TRAPEZOID_CONT_PER replaced with LASER_POWER_TRAP."
#elif defined(LASER_POWER_INLINE_CONTINUOUS)
#error "LASER_POWER_INLINE_CONTINUOUS is not required, inline mode is enabled with 'M3 I' and disabled with 'M5 I'."
#elif defined(CUTTER_POWER_DISPLAY) #elif defined(CUTTER_POWER_DISPLAY)
#error "CUTTER_POWER_DISPLAY is now CUTTER_POWER_UNIT." #error "CUTTER_POWER_DISPLAY is now CUTTER_POWER_UNIT."
#elif defined(CHAMBER_HEATER_PIN) #elif defined(CHAMBER_HEATER_PIN)
@ -595,6 +605,8 @@
#error "ARC_SUPPORT no longer uses ARC_SEGMENTS_PER_R." #error "ARC_SUPPORT no longer uses ARC_SEGMENTS_PER_R."
#elif ENABLED(ARC_SUPPORT) && (!defined(MIN_ARC_SEGMENT_MM) || !defined(MAX_ARC_SEGMENT_MM)) #elif ENABLED(ARC_SUPPORT) && (!defined(MIN_ARC_SEGMENT_MM) || !defined(MAX_ARC_SEGMENT_MM))
#error "ARC_SUPPORT now requires MIN_ARC_SEGMENT_MM and MAX_ARC_SEGMENT_MM." #error "ARC_SUPPORT now requires MIN_ARC_SEGMENT_MM and MAX_ARC_SEGMENT_MM."
#elif defined(LASER_POWER_INLINE)
#error "LASER_POWER_INLINE is obsolete."
#elif defined(SPINDLE_LASER_PWM) #elif defined(SPINDLE_LASER_PWM)
#error "SPINDLE_LASER_PWM (true) is now set with SPINDLE_LASER_USE_PWM (enabled)." #error "SPINDLE_LASER_PWM (true) is now set with SPINDLE_LASER_USE_PWM (enabled)."
#elif ANY(IS_RAMPS_EEB, IS_RAMPS_EEF, IS_RAMPS_EFB, IS_RAMPS_EFF, IS_RAMPS_SF) #elif ANY(IS_RAMPS_EEB, IS_RAMPS_EEF, IS_RAMPS_EFB, IS_RAMPS_EFF, IS_RAMPS_SF)
@ -3841,37 +3853,26 @@ static_assert(_PLUS_TEST(4), "HOMING_FEEDRATE_MM_M values must be positive.");
#error "CUTTER_POWER_UNIT must be PWM255, PERCENT, RPM, or SERVO." #error "CUTTER_POWER_UNIT must be PWM255, PERCENT, RPM, or SERVO."
#endif #endif
#if ENABLED(LASER_POWER_INLINE) #if ENABLED(LASER_FEATURE)
#if ENABLED(SPINDLE_CHANGE_DIR) #if ENABLED(SPINDLE_CHANGE_DIR)
#error "SPINDLE_CHANGE_DIR and LASER_POWER_INLINE are incompatible." #error "SPINDLE_CHANGE_DIR and LASER_FEATURE are incompatible."
#elif ENABLED(LASER_MOVE_G0_OFF) && DISABLED(LASER_MOVE_POWER) #elif ENABLED(LASER_MOVE_G0_OFF)
#error "LASER_MOVE_G0_OFF requires LASER_MOVE_POWER." #error "LASER_MOVE_G0_OFF is no longer required, G0 and G28 cannot apply power."
#elif ENABLED(LASER_MOVE_G28_OFF)
#error "LASER_MOVE_G0_OFF is no longer required, G0 and G28 cannot apply power."
#elif ENABLED(LASER_MOVE_POWER)
#error "LASER_MOVE_POWER is no longer applicable."
#endif #endif
#if ENABLED(LASER_POWER_INLINE_TRAPEZOID) #if ENABLED(LASER_POWER_TRAP)
#if DISABLED(SPINDLE_LASER_USE_PWM) #if DISABLED(SPINDLE_LASER_USE_PWM)
#error "LASER_POWER_INLINE_TRAPEZOID requires SPINDLE_LASER_USE_PWM to function." #error "LASER_POWER_TRAP requires SPINDLE_LASER_USE_PWM to function."
#elif ENABLED(S_CURVE_ACCELERATION)
//#ifndef LASER_POWER_INLINE_S_CURVE_ACCELERATION_WARN
// #define LASER_POWER_INLINE_S_CURVE_ACCELERATION_WARN
// #warning "Combining LASER_POWER_INLINE_TRAPEZOID with S_CURVE_ACCELERATION may result in unintended behavior."
//#endif
#endif #endif
#endif #endif
#if ENABLED(LASER_POWER_INLINE_INVERT)
//#ifndef LASER_POWER_INLINE_INVERT_WARN
// #define LASER_POWER_INLINE_INVERT_WARN
// #warning "Enabling LASER_POWER_INLINE_INVERT means that `M5` won't kill the laser immediately; use `M5 I` instead."
//#endif
#endif
#else #else
#if SPINDLE_LASER_POWERUP_DELAY < 1 #if SPINDLE_LASER_POWERUP_DELAY < 1
#error "SPINDLE_LASER_POWERUP_DELAY must be greater than 0." #error "SPINDLE_LASER_POWERUP_DELAY must be greater than 0."
#elif SPINDLE_LASER_POWERDOWN_DELAY < 1 #elif SPINDLE_LASER_POWERDOWN_DELAY < 1
#error "SPINDLE_LASER_POWERDOWN_DELAY must be greater than 0." #error "SPINDLE_LASER_POWERDOWN_DELAY must be greater than 0."
#elif ENABLED(LASER_MOVE_POWER)
#error "LASER_MOVE_POWER requires LASER_POWER_INLINE."
#elif ANY(LASER_POWER_INLINE_TRAPEZOID, LASER_POWER_INLINE_INVERT, LASER_MOVE_G0_OFF, LASER_MOVE_POWER)
#error "Enabled an inline laser feature without inline laser power being enabled."
#endif #endif
#endif #endif
@ -3889,7 +3890,7 @@ static_assert(_PLUS_TEST(4), "HOMING_FEEDRATE_MM_M values must be positive.");
#error "SPINDLE_LASER_PWM_PIN not assigned to a PWM pin." #error "SPINDLE_LASER_PWM_PIN not assigned to a PWM pin."
#elif !defined(SPINDLE_LASER_PWM_INVERT) #elif !defined(SPINDLE_LASER_PWM_INVERT)
#error "SPINDLE_LASER_PWM_INVERT is required for (SPINDLE|LASER)_FEATURE." #error "SPINDLE_LASER_PWM_INVERT is required for (SPINDLE|LASER)_FEATURE."
#elif !(defined(SPEED_POWER_INTERCEPT) && defined(SPEED_POWER_MIN) && defined(SPEED_POWER_MAX) && defined(SPEED_POWER_STARTUP)) #elif !(defined(SPEED_POWER_MIN) && defined(SPEED_POWER_MAX) && defined(SPEED_POWER_STARTUP))
#error "SPINDLE_LASER_USE_PWM equation constant(s) missing." #error "SPINDLE_LASER_USE_PWM equation constant(s) missing."
#elif _PIN_CONFLICT(X_MIN) #elif _PIN_CONFLICT(X_MIN)
#error "SPINDLE_LASER_USE_PWM pin conflicts with X_MIN_PIN." #error "SPINDLE_LASER_USE_PWM pin conflicts with X_MIN_PIN."

2
Marlin/src/lcd/dogm/status_screen_DOGM.cpp

@ -670,7 +670,7 @@ void MarlinUI::draw_status_screen() {
// Laser / Spindle // Laser / Spindle
#if DO_DRAW_CUTTER #if DO_DRAW_CUTTER
if (cutter.isReady && PAGE_CONTAINS(STATUS_CUTTER_TEXT_Y - INFO_FONT_ASCENT, STATUS_CUTTER_TEXT_Y - 1)) { if (cutter.isReadyForUI && PAGE_CONTAINS(STATUS_CUTTER_TEXT_Y - INFO_FONT_ASCENT, STATUS_CUTTER_TEXT_Y - 1)) {
#if CUTTER_UNIT_IS(PERCENT) #if CUTTER_UNIT_IS(PERCENT)
lcd_put_u8str(STATUS_CUTTER_TEXT_X, STATUS_CUTTER_TEXT_Y, cutter_power2str(cutter.unitPower)); lcd_put_u8str(STATUS_CUTTER_TEXT_X, STATUS_CUTTER_TEXT_Y, cutter_power2str(cutter.unitPower));
#elif CUTTER_UNIT_IS(RPM) #elif CUTTER_UNIT_IS(RPM)

5
Marlin/src/lcd/menu/menu_item.h

@ -27,6 +27,10 @@
#include "../../inc/MarlinConfigPre.h" #include "../../inc/MarlinConfigPre.h"
#if ENABLED(LASER_SYNCHRONOUS_M106_M107)
#include "../../module/planner.h"
#endif
void lcd_move_z(); void lcd_move_z();
//////////////////////////////////////////// ////////////////////////////////////////////
@ -538,6 +542,7 @@ class MenuItem_bool : public MenuEditItemBase {
inline void on_fan_update() { inline void on_fan_update() {
thermalManager.set_fan_speed(MenuItemBase::itemIndex, editable.uint8); thermalManager.set_fan_speed(MenuItemBase::itemIndex, editable.uint8);
TERN_(LASER_SYNCHRONOUS_M106_M107, planner.buffer_sync_block(BLOCK_FLAG_SYNC_FANS));
} }
#if ENABLED(EXTRA_FAN_SPEED) #if ENABLED(EXTRA_FAN_SPEED)

18
Marlin/src/lcd/menu/menu_spindle_laser.cpp

@ -33,7 +33,7 @@
#include "../../feature/spindle_laser.h" #include "../../feature/spindle_laser.h"
void menu_spindle_laser() { void menu_spindle_laser() {
bool is_enabled = cutter.enabled() && cutter.isReady; bool is_enabled = cutter.enabled();
#if ENABLED(SPINDLE_CHANGE_DIR) #if ENABLED(SPINDLE_CHANGE_DIR)
bool is_rev = cutter.is_reverse(); bool is_rev = cutter.is_reverse();
#endif #endif
@ -49,7 +49,13 @@
#endif #endif
editable.state = is_enabled; editable.state = is_enabled;
EDIT_ITEM(bool, MSG_CUTTER(TOGGLE), &is_enabled, []{ if (editable.state) cutter.disable(); else cutter.enable_same_dir(); }); EDIT_ITEM(bool, MSG_CUTTER(TOGGLE), &is_enabled, []{
#if ENABLED(SPINDLE_FEATURE)
if (editable.state) cutter.disable(); else cutter.enable_same_dir();
#else
cutter.laser_menu_toggle(!editable.state);
#endif
});
#if ENABLED(AIR_EVACUATION) #if ENABLED(AIR_EVACUATION)
bool evac_state = cutter.air_evac_state(); bool evac_state = cutter.air_evac_state();
@ -72,12 +78,10 @@
// Setup and fire a test pulse using the current PWM power level for for a duration of test_pulse_min to test_pulse_max ms. // Setup and fire a test pulse using the current PWM power level for for a duration of test_pulse_min to test_pulse_max ms.
EDIT_ITEM_FAST(CUTTER_MENU_PULSE_TYPE, MSG_LASER_PULSE_MS, &cutter.testPulse, LASER_TEST_PULSE_MIN, LASER_TEST_PULSE_MAX); EDIT_ITEM_FAST(CUTTER_MENU_PULSE_TYPE, MSG_LASER_PULSE_MS, &cutter.testPulse, LASER_TEST_PULSE_MIN, LASER_TEST_PULSE_MAX);
ACTION_ITEM(MSG_LASER_FIRE_PULSE, cutter.test_fire_pulse); ACTION_ITEM(MSG_LASER_FIRE_PULSE, cutter.test_fire_pulse);
#if ENABLED(HAL_CAN_SET_PWM_FREQ) && SPINDLE_LASER_FREQUENCY
EDIT_ITEM_FAST(CUTTER_MENU_FREQUENCY_TYPE, MSG_CUTTER_FREQUENCY, &cutter.frequency, 2000, 80000, cutter.refresh_frequency);
#endif
#endif #endif
#if BOTH(MARLIN_DEV_MODE, HAL_CAN_SET_PWM_FREQ) && SPINDLE_LASER_FREQUENCY
EDIT_ITEM_FAST(CUTTER_MENU_FREQUENCY_TYPE, MSG_CUTTER_FREQUENCY, &cutter.frequency, 2000, 80000, cutter.refresh_frequency);
#endif
END_MENU(); END_MENU();
} }

286
Marlin/src/module/planner.cpp

@ -128,8 +128,13 @@ uint8_t Planner::delay_before_delivering; // This counter delays delivery
planner_settings_t Planner::settings; // Initialized by settings.load() planner_settings_t Planner::settings; // Initialized by settings.load()
#if ENABLED(LASER_POWER_INLINE) /**
* Set up inline block variables
* Set laser_power_floor based on SPEED_POWER_MIN to pevent a zero power output state with LASER_POWER_TRAP
*/
#if ENABLED(LASER_FEATURE)
laser_state_t Planner::laser_inline; // Current state for blocks laser_state_t Planner::laser_inline; // Current state for blocks
const uint8_t laser_power_floor = cutter.pct_to_ocr(SPEED_POWER_MIN);
#endif #endif
uint32_t Planner::max_acceleration_steps_per_s2[DISTINCT_AXES]; // (steps/s^2) Derived from mm_per_s2 uint32_t Planner::max_acceleration_steps_per_s2[DISTINCT_AXES]; // (steps/s^2) Derived from mm_per_s2
@ -799,6 +804,7 @@ void Planner::calculate_trapezoid_for_block(block_t * const block, const_float_t
if (plateau_steps < 0) { if (plateau_steps < 0) {
const float accelerate_steps_float = CEIL(intersection_distance(initial_rate, final_rate, accel, block->step_event_count)); const float accelerate_steps_float = CEIL(intersection_distance(initial_rate, final_rate, accel, block->step_event_count));
accelerate_steps = _MIN(uint32_t(_MAX(accelerate_steps_float, 0)), block->step_event_count); accelerate_steps = _MIN(uint32_t(_MAX(accelerate_steps_float, 0)), block->step_event_count);
decelerate_steps = block->step_event_count - accelerate_steps;
plateau_steps = 0; plateau_steps = 0;
#if ENABLED(S_CURVE_ACCELERATION) #if ENABLED(S_CURVE_ACCELERATION)
@ -822,7 +828,7 @@ void Planner::calculate_trapezoid_for_block(block_t * const block, const_float_t
// Store new block parameters // Store new block parameters
block->accelerate_until = accelerate_steps; block->accelerate_until = accelerate_steps;
block->decelerate_after = accelerate_steps + plateau_steps; block->decelerate_after = block->step_event_count - decelerate_steps;
block->initial_rate = initial_rate; block->initial_rate = initial_rate;
#if ENABLED(S_CURVE_ACCELERATION) #if ENABLED(S_CURVE_ACCELERATION)
block->acceleration_time = acceleration_time; block->acceleration_time = acceleration_time;
@ -833,46 +839,52 @@ void Planner::calculate_trapezoid_for_block(block_t * const block, const_float_t
#endif #endif
block->final_rate = final_rate; block->final_rate = final_rate;
/** #if ENABLED(LASER_POWER_TRAP)
* Laser trapezoid calculations /**
* * Laser Trapezoid Calculations
* Approximate the trapezoid with the laser, incrementing the power every `entry_per` while accelerating *
* and decrementing it every `exit_power_per` while decelerating, thus ensuring power is related to feedrate. * Approximate the trapezoid with the laser, incrementing the power every `trap_ramp_entry_incr` steps while accelerating,
* * and decrementing the power every `trap_ramp_exit_decr` while decelerating, to keep power proportional to feedrate.
* LASER_POWER_INLINE_TRAPEZOID_CONT doesn't need this as it continuously approximates * Laser power trap will reduce the initial power to no less than the laser_power_floor value. Based on the number
* * of calculated accel/decel steps the power is distributed over the trapezoid entry- and exit-ramp steps.
* Note this may behave unreliably when running with S_CURVE_ACCELERATION *
*/ * trap_ramp_active_pwr - The active power is initially set at a reduced level factor of initial power / accel steps and
#if ENABLED(LASER_POWER_INLINE_TRAPEZOID) * will be additively incremented using a trap_ramp_entry_incr value for each accel step processed later in the stepper code.
if (block->laser.power > 0) { // No need to care if power == 0 * The trap_ramp_exit_decr value is calculated as power / decel steps and is also adjusted to no less than the power floor.
const uint8_t entry_power = block->laser.power * entry_factor; // Power on block entry *
#if DISABLED(LASER_POWER_INLINE_TRAPEZOID_CONT) * If the power == 0 the inline mode variables need to be set to zero to prevent stepper processing. The method allows
// Speedup power * for simpler non-powered moves like G0 or G28.
const uint8_t entry_power_diff = block->laser.power - entry_power; *
if (entry_power_diff) { * Laser Trap Power works for all Jerk and Curve modes; however Arc-based moves will have issues since the segments are
block->laser.entry_per = accelerate_steps / entry_power_diff; * usually too small.
block->laser.power_entry = entry_power; */
} if (cutter.cutter_mode == CUTTER_MODE_CONTINUOUS) {
else { if (planner.laser_inline.status.isPowered && planner.laser_inline.status.isEnabled) {
block->laser.entry_per = 0; if (block->laser.power > 0) {
block->laser.power_entry = block->laser.power; NOLESS(block->laser.power, laser_power_floor);
} block->laser.trap_ramp_active_pwr = (block->laser.power - laser_power_floor) * (initial_rate / float(block->nominal_rate)) + laser_power_floor;
// Slowdown power block->laser.trap_ramp_entry_incr = (block->laser.power - block->laser.trap_ramp_active_pwr) / accelerate_steps;
const uint8_t exit_power = block->laser.power * exit_factor, // Power on block entry float laser_pwr = block->laser.power * (final_rate / float(block->nominal_rate));
exit_power_diff = block->laser.power - exit_power; NOLESS(laser_pwr, laser_power_floor);
if (exit_power_diff) { block->laser.trap_ramp_exit_decr = (block->laser.power - laser_pwr) / decelerate_steps;
block->laser.exit_per = (block->step_event_count - block->decelerate_after) / exit_power_diff; #if ENABLED(DEBUG_LASER_TRAP)
block->laser.power_exit = exit_power; SERIAL_ECHO_MSG("lp:",block->laser.power);
SERIAL_ECHO_MSG("as:",accelerate_steps);
SERIAL_ECHO_MSG("ds:",decelerate_steps);
SERIAL_ECHO_MSG("p.trap:",block->laser.trap_ramp_active_pwr);
SERIAL_ECHO_MSG("p.incr:",block->laser.trap_ramp_entry_incr);
SERIAL_ECHO_MSG("p.decr:",block->laser.trap_ramp_exit_decr);
#endif
} }
else { else {
block->laser.exit_per = 0; block->laser.trap_ramp_active_pwr = 0;
block->laser.power_exit = block->laser.power; block->laser.trap_ramp_entry_incr = 0;
block->laser.trap_ramp_exit_decr = 0;
} }
#else
block->laser.power_entry = entry_power; }
#endif
} }
#endif #endif // LASER_POWER_TRAP
} }
/* PLANNER SPEED DEFINITION /* PLANNER SPEED DEFINITION
@ -1130,10 +1142,9 @@ void Planner::recalculate_trapezoids() {
// The tail may be changed by the ISR so get a local copy. // The tail may be changed by the ISR so get a local copy.
uint8_t block_index = block_buffer_tail, uint8_t block_index = block_buffer_tail,
head_block_index = block_buffer_head; head_block_index = block_buffer_head;
// Since there could be a sync block in the head of the queue, and the
// Since there could be non-move blocks in the head of the queue, and the
// next loop must not recalculate the head block (as it needs to be // next loop must not recalculate the head block (as it needs to be
// specially handled), scan backwards to the first move block. // specially handled), scan backwards to the first non-SYNC block.
while (head_block_index != block_index) { while (head_block_index != block_index) {
// Go back (head always point to the first free block) // Go back (head always point to the first free block)
@ -1203,7 +1214,7 @@ void Planner::recalculate_trapezoids() {
// Last/newest block in buffer. Exit speed is set with MINIMUM_PLANNER_SPEED. Always recalculated. // Last/newest block in buffer. Exit speed is set with MINIMUM_PLANNER_SPEED. Always recalculated.
if (next) { if (next) {
// Mark the last block as RECALCULATE, to prevent the Stepper ISR running it. // Mark the next(last) block as RECALCULATE, to prevent the Stepper ISR running it.
// As the last block is always recalculated here, there is a chance the block isn't // As the last block is always recalculated here, there is a chance the block isn't
// marked as RECALCULATE yet. That's the reason for the following line. // marked as RECALCULATE yet. That's the reason for the following line.
block->flag.recalculate = true; block->flag.recalculate = true;
@ -1295,7 +1306,7 @@ void Planner::recalculate() {
#endif // HAS_FAN #endif // HAS_FAN
/** /**
* Maintain fans, paste extruder pressure, * Maintain fans, paste extruder pressure, spindle/laser power
*/ */
void Planner::check_axes_activity() { void Planner::check_axes_activity() {
@ -1359,7 +1370,7 @@ void Planner::check_axes_activity() {
} }
else { else {
TERN_(HAS_CUTTER, cutter.refresh()); TERN_(HAS_CUTTER, if (cutter.cutter_mode == CUTTER_MODE_STANDARD) cutter.refresh());
#if HAS_TAIL_FAN_SPEED #if HAS_TAIL_FAN_SPEED
FANS_LOOP(i) { FANS_LOOP(i) {
@ -1459,7 +1470,7 @@ void Planner::check_axes_activity() {
for (uint8_t b = block_buffer_tail; b != block_buffer_head; b = next_block_index(b)) { for (uint8_t b = block_buffer_tail; b != block_buffer_head; b = next_block_index(b)) {
const block_t * const block = &block_buffer[b]; const block_t * const block = &block_buffer[b];
if (NUM_AXIS_GANG(block->steps.x, || block->steps.y, || block->steps.z, || block->steps.i, || block->steps.j, || block->steps.k, || block->steps.u, || block->steps.v, || block->steps.w)) { if (NUM_AXIS_GANG(block->steps.x, || block->steps.y, || block->steps.z, || block->steps.i, || block->steps.j, || block->steps.k, || block->steps.u, || block->steps.v, || block->steps.w)) {
const float se = (float)block->steps.e / block->step_event_count * SQRT(block->nominal_speed_sqr); // mm/sec const float se = (float)block->steps.e / block->step_event_count * SQRT(block->nominal_speed_sqr); // mm/sec;
NOLESS(high, se); NOLESS(high, se);
} }
} }
@ -1781,7 +1792,7 @@ void Planner::synchronize() { while (busy()) idle(); }
bool Planner::_buffer_steps(const xyze_long_t &target bool Planner::_buffer_steps(const xyze_long_t &target
OPTARG(HAS_POSITION_FLOAT, const xyze_pos_t &target_float) OPTARG(HAS_POSITION_FLOAT, const xyze_pos_t &target_float)
OPTARG(HAS_DIST_MM_ARG, const xyze_float_t &cart_dist_mm) OPTARG(HAS_DIST_MM_ARG, const xyze_float_t &cart_dist_mm)
, feedRate_t fr_mm_s, const uint8_t extruder, const_float_t millimeters/*=0.0*/ , feedRate_t fr_mm_s, const uint8_t extruder, const_float_t millimeters
) { ) {
// Wait for the next available block // Wait for the next available block
@ -1863,8 +1874,36 @@ bool Planner::_populate_block(
); );
/* <-- add a slash to enable /* <-- add a slash to enable
#define _ALINE(A) " " STR_##A ":", target[_AXIS(A)], " (", int32_t(target[_AXIS(A)] - position[_AXIS(A)]), " steps)" SERIAL_ECHOLNPGM(
SERIAL_ECHOLNPGM(" _populate_block FR:", fr_mm_s, LOGICAL_AXIS_MAP(_ALINE)); " _populate_block FR:", fr_mm_s,
" A:", target.a, " (", da, " steps)"
#if HAS_Y_AXIS
" B:", target.b, " (", db, " steps)"
#endif
#if HAS_Z_AXIS
" C:", target.c, " (", dc, " steps)"
#endif
#if HAS_I_AXIS
" " STR_I ":", target.i, " (", di, " steps)"
#endif
#if HAS_J_AXIS
" " STR_J ":", target.j, " (", dj, " steps)"
#endif
#if HAS_K_AXIS
" " STR_K ":", target.k, " (", dk, " steps)"
#endif
#if HAS_U_AXIS
" " STR_U ":", target.u, " (", du, " steps)"
#endif
#if HAS_V_AXIS
" " STR_V ":", target.v, " (", dv, " steps)"
#endif
#if HAS_W_AXIS
" " STR_W ":", target.w, " (", dw, " steps)"
#if HAS_EXTRUDERS
" E:", target.e, " (", de, " steps)"
#endif
);
//*/ //*/
#if EITHER(PREVENT_COLD_EXTRUSION, PREVENT_LENGTHY_EXTRUDE) #if EITHER(PREVENT_COLD_EXTRUSION, PREVENT_LENGTHY_EXTRUDE)
@ -1962,11 +2001,34 @@ bool Planner::_populate_block(
// Set direction bits // Set direction bits
block->direction_bits = dm; block->direction_bits = dm;
// Update block laser power /**
#if ENABLED(LASER_POWER_INLINE) * Update block laser power
laser_inline.status.isPlanned = true; * For standard mode get the cutter.power value for processing, since it's
block->laser.status = laser_inline.status; * only set by apply_power().
block->laser.power = laser_inline.power; */
#if HAS_CUTTER
switch (cutter.cutter_mode) {
default: break;
case CUTTER_MODE_STANDARD: block->cutter_power = cutter.power; break;
#if ENABLED(LASER_FEATURE)
/**
* For inline mode get the laser_inline variables, including power and status.
* Dynamic mode only needs to update if the feedrate has changed, since it's
* calculated from the current feedrate and power level.
*/
case CUTTER_MODE_CONTINUOUS:
block->laser.power = laser_inline.power;
block->laser.status = laser_inline.status;
break;
case CUTTER_MODE_DYNAMIC:
if (cutter.laser_feedrate_changed()) // Only process changes in rate
block->laser.power = laser_inline.power = cutter.calc_dynamic_power();
break;
#endif
}
#endif #endif
// Number of steps for each axis // Number of steps for each axis
@ -2028,9 +2090,9 @@ bool Planner::_populate_block(
#endif #endif
#elif ENABLED(MARKFORGED_XY) #elif ENABLED(MARKFORGED_XY)
steps_dist_mm.a = (da - db) * mm_per_step[A_AXIS]; steps_dist_mm.a = (da - db) * mm_per_step[A_AXIS];
steps_dist_mm.b = db * mm_per_step[B_AXIS]; steps_dist_mm.b = db * mm_per_step[B_AXIS];
#elif ENABLED(MARKFORGED_YX) #elif ENABLED(MARKFORGED_YX)
steps_dist_mm.a = da * mm_per_step[A_AXIS]; steps_dist_mm.a = da * mm_per_step[A_AXIS];
steps_dist_mm.b = (db - da) * mm_per_step[B_AXIS]; steps_dist_mm.b = (db - da) * mm_per_step[B_AXIS];
#else #else
XYZ_CODE( XYZ_CODE(
@ -2076,21 +2138,12 @@ bool Planner::_populate_block(
block->millimeters = millimeters; block->millimeters = millimeters;
else { else {
/** /**
* Distance for interpretation of feedrate in accordance with LinuxCNC (the successor of * Distance for interpretation of feedrate in accordance with LinuxCNC (the successor of NIST
* NIST RS274NGC interpreter - version 3) and its default CANON_XYZ feed reference mode. * RS274NGC interpreter - version 3) and its default CANON_XYZ feed reference mode.
* * Assume that X, Y, Z are the primary linear axes and U, V, W are secondary linear axes and A, B, C are
* Assume: * rotational axes. Then dX, dY, dZ are the displacements of the primary linear axes and dU, dV, dW are the displacements of linear axes and
* - X, Y, Z are the primary linear axes; * dA, dB, dC are the displacements of rotational axes.
* - U, V, W are secondary linear axes; * The time it takes to execute move command with feedrate F is t = D/F, where D is the total distance, calculated as follows:
* - A, B, C are rotational axes.
*
* Then:
* - dX, dY, dZ are the displacements of the primary linear axes;
* - dU, dV, dW are the displacements of linear axes;
* - dA, dB, dC are the displacements of rotational axes.
*
* The time it takes to execute move command with feedrate F is t = D/F,
* where D is the total distance, calculated as follows:
* D^2 = dX^2 + dY^2 + dZ^2 * D^2 = dX^2 + dY^2 + dZ^2
* if D^2 == 0 (none of XYZ move but any secondary linear axes move, whether other axes are moved or not): * if D^2 == 0 (none of XYZ move but any secondary linear axes move, whether other axes are moved or not):
* D^2 = dU^2 + dV^2 + dW^2 * D^2 = dU^2 + dV^2 + dW^2
@ -2099,9 +2152,8 @@ bool Planner::_populate_block(
*/ */
float distance_sqr = ( float distance_sqr = (
#if ENABLED(ARTICULATED_ROBOT_ARM) #if ENABLED(ARTICULATED_ROBOT_ARM)
// For articulated robots, interpreting feedrate like LinuxCNC would require inverse kinematics. As a workaround, // For articulated robots, interpreting feedrate like LinuxCNC would require inverse kinematics. As a workaround, pretend that motors sit on n mutually orthogonal
// assume that motors sit on a mutually-orthogonal axes and we can think of distance as magnitude of an n-vector // axes and assume that we could think of distance as magnitude of an n-vector in an n-dimensional Euclidian space.
// in an n-dimensional Euclidian space.
NUM_AXIS_GANG( NUM_AXIS_GANG(
sq(steps_dist_mm.x), + sq(steps_dist_mm.y), + sq(steps_dist_mm.z), sq(steps_dist_mm.x), + sq(steps_dist_mm.y), + sq(steps_dist_mm.z),
+ sq(steps_dist_mm.i), + sq(steps_dist_mm.j), + sq(steps_dist_mm.k), + sq(steps_dist_mm.i), + sq(steps_dist_mm.j), + sq(steps_dist_mm.k),
@ -2121,7 +2173,7 @@ bool Planner::_populate_block(
#elif CORE_IS_YZ #elif CORE_IS_YZ
XYZ_GANG(sq(steps_dist_mm.x), + sq(steps_dist_mm.head.y), + sq(steps_dist_mm.head.z)) XYZ_GANG(sq(steps_dist_mm.x), + sq(steps_dist_mm.head.y), + sq(steps_dist_mm.head.z))
#else #else
XYZ_GANG(sq(steps_dist_mm.x), + sq(steps_dist_mm.y), + sq(steps_dist_mm.z)) XYZ_GANG(sq(steps_dist_mm.x), + sq(steps_dist_mm.y), + sq(steps_dist_mm.z))
#endif #endif
); );
@ -2154,9 +2206,9 @@ bool Planner::_populate_block(
/** /**
* At this point at least one of the axes has more steps than * At this point at least one of the axes has more steps than
* MIN_STEPS_PER_SEGMENT, ensuring the segment won't get dropped * MIN_STEPS_PER_SEGMENT, ensuring the segment won't get dropped as
* as zero-length. It's important to not apply corrections to blocks * zero-length. It's important to not apply corrections
* that would get dropped! * to blocks that would get dropped!
* *
* A correction function is permitted to add steps to an axis, it * A correction function is permitted to add steps to an axis, it
* should *never* remove steps! * should *never* remove steps!
@ -2177,7 +2229,6 @@ bool Planner::_populate_block(
TERN_(MIXING_EXTRUDER, mixer.populate_block(block->b_color)); TERN_(MIXING_EXTRUDER, mixer.populate_block(block->b_color));
TERN_(HAS_CUTTER, block->cutter_power = cutter.power);
#if HAS_FAN #if HAS_FAN
FANS_LOOP(i) block->fan_speed[i] = thermalManager.fan_speed[i]; FANS_LOOP(i) block->fan_speed[i] = thermalManager.fan_speed[i];
@ -2192,9 +2243,15 @@ bool Planner::_populate_block(
#if ENABLED(AUTO_POWER_CONTROL) #if ENABLED(AUTO_POWER_CONTROL)
if (NUM_AXIS_GANG( if (NUM_AXIS_GANG(
block->steps.x, || block->steps.y, || block->steps.z, block->steps.x,
|| block->steps.i, || block->steps.j, || block->steps.k, || block->steps.y,
|| block->steps.u, || block->steps.v, || block->steps.w || block->steps.z,
|| block->steps.i,
|| block->steps.j,
|| block->steps.k,
|| block->steps.u,
|| block->steps.v,
|| block->steps.w
)) powerManager.power_on(); )) powerManager.power_on();
#endif #endif
@ -2428,7 +2485,7 @@ bool Planner::_populate_block(
if (speed_factor < 1.0f) { if (speed_factor < 1.0f) {
current_speed *= speed_factor; current_speed *= speed_factor;
block->nominal_rate *= speed_factor; block->nominal_rate *= speed_factor;
block->nominal_speed_sqr *= sq(speed_factor); block->nominal_speed_sqr = block->nominal_speed_sqr * sq(speed_factor);
} }
// Compute and limit the acceleration rate for the trapezoid generator. // Compute and limit the acceleration rate for the trapezoid generator.
@ -2630,15 +2687,14 @@ bool Planner::_populate_block(
vmax_junction_sqr = sq(float(MINIMUM_PLANNER_SPEED)); vmax_junction_sqr = sq(float(MINIMUM_PLANNER_SPEED));
} }
else { else {
NOLESS(junction_cos_theta, -0.999999f); // Check for numerical round-off to avoid divide by zero.
// Convert delta vector to unit vector // Convert delta vector to unit vector
xyze_float_t junction_unit_vec = unit_vec - prev_unit_vec; xyze_float_t junction_unit_vec = unit_vec - prev_unit_vec;
normalize_junction_vector(junction_unit_vec); normalize_junction_vector(junction_unit_vec);
const float junction_acceleration = limit_value_by_axis_maximum(block->acceleration, junction_unit_vec); const float junction_acceleration = limit_value_by_axis_maximum(block->acceleration, junction_unit_vec),
sin_theta_d2 = SQRT(0.5f * (1.0f - junction_cos_theta)); // Trig half angle identity. Always positive.
NOLESS(junction_cos_theta, -0.999999f); // Check for numerical round-off to avoid divide by zero.
const float sin_theta_d2 = SQRT(0.5f * (1.0f - junction_cos_theta)); // Trig half angle identity. Always positive.
vmax_junction_sqr = junction_acceleration * junction_deviation_mm * sin_theta_d2 / (1.0f - sin_theta_d2); vmax_junction_sqr = junction_acceleration * junction_deviation_mm * sin_theta_d2 / (1.0f - sin_theta_d2);
@ -2889,21 +2945,19 @@ bool Planner::_populate_block(
/** /**
* Planner::buffer_sync_block * Planner::buffer_sync_block
* Add a block to the buffer that just updates the position, * Add a block to the buffer that just updates the position
* or in case of LASER_SYNCHRONOUS_M106_M107 the fan PWM * @param sync_flag BLOCK_FLAG_SYNC_FANS & BLOCK_FLAG_LASER_PWR
* Supports LASER_SYNCHRONOUS_M106_M107 and LASER_POWER_SYNC power sync block buffer queueing.
*/ */
void Planner::buffer_sync_block(TERN_(LASER_SYNCHRONOUS_M106_M107, const BlockFlagBit sync_flag/*=BLOCK_BIT_SYNC_POSITION*/)) {
#if DISABLED(LASER_SYNCHRONOUS_M106_M107) void Planner::buffer_sync_block(const BlockFlagBit sync_flag/*=BLOCK_BIT_SYNC_POSITION*/) {
constexpr BlockFlagBit sync_flag = BLOCK_BIT_SYNC_POSITION;
#endif
// Wait for the next available block // Wait for the next available block
uint8_t next_buffer_head; uint8_t next_buffer_head;
block_t * const block = get_next_free_block(next_buffer_head); block_t * const block = get_next_free_block(next_buffer_head);
// Clear block // Clear block
block->reset(); memset(block, 0, sizeof(block_t));
block->flag.apply(sync_flag); block->flag.apply(sync_flag);
block->position = position; block->position = position;
@ -2915,6 +2969,12 @@ void Planner::buffer_sync_block(TERN_(LASER_SYNCHRONOUS_M106_M107, const BlockFl
FANS_LOOP(i) block->fan_speed[i] = thermalManager.fan_speed[i]; FANS_LOOP(i) block->fan_speed[i] = thermalManager.fan_speed[i];
#endif #endif
/**
* M3-based power setting can be processed inline with a laser power sync block.
* During active moves cutter.power is processed immediately, otherwise on the next move.
*/
TERN_(LASER_POWER_SYNC, block->laser.power = cutter.power);
// If this is the first added movement, reload the delay, otherwise, cancel it. // If this is the first added movement, reload the delay, otherwise, cancel it.
if (block_buffer_head == block_buffer_tail) { if (block_buffer_head == block_buffer_tail) {
// If it was the first queued block, restart the 1st block delivery delay, to // If it was the first queued block, restart the 1st block delivery delay, to
@ -3052,8 +3112,8 @@ bool Planner::buffer_segment(const abce_pos_t &abce
if (!_buffer_steps(target if (!_buffer_steps(target
OPTARG(HAS_POSITION_FLOAT, target_float) OPTARG(HAS_POSITION_FLOAT, target_float)
OPTARG(HAS_DIST_MM_ARG, cart_dist_mm) OPTARG(HAS_DIST_MM_ARG, cart_dist_mm)
, fr_mm_s, extruder, millimeters , fr_mm_s, extruder, millimeters)
)) return false; ) return false;
stepper.wake_up(); stepper.wake_up();
return true; return true;
@ -3099,7 +3159,7 @@ bool Planner::buffer_line(const xyze_pos_t &cart, const_feedRate_t fr_mm_s, cons
inverse_kinematics(machine); inverse_kinematics(machine);
#if ENABLED(SCARA_FEEDRATE_SCALING) #if ENABLED(SCARA_FEEDRATE_SCALING)
// For SCARA scale the feed rate from mm/s to degrees/s // For SCARA scale the feedrate from mm/s to degrees/s
// i.e., Complete the angular vector in the given time. // i.e., Complete the angular vector in the given time.
const float duration_recip = inv_duration ?: fr_mm_s / mm; const float duration_recip = inv_duration ?: fr_mm_s / mm;
const xyz_pos_t diff = delta - position_float; const xyz_pos_t diff = delta - position_float;
@ -3120,14 +3180,6 @@ bool Planner::buffer_line(const xyze_pos_t &cart, const_feedRate_t fr_mm_s, cons
#if ENABLED(DIRECT_STEPPING) #if ENABLED(DIRECT_STEPPING)
/**
* @brief Add a direct stepping page block to the buffer
* and wake up the Stepper ISR to process it.
*
* @param page_idx Page index provided by G6 I<index>
* @param extruder The extruder to use in the move
* @param num_steps Number of steps to process in the ISR
*/
void Planner::buffer_page(const page_idx_t page_idx, const uint8_t extruder, const uint16_t num_steps) { void Planner::buffer_page(const page_idx_t page_idx, const uint8_t extruder, const uint16_t num_steps) {
if (!last_page_step_rate) { if (!last_page_step_rate) {
kill(GET_TEXT_F(MSG_BAD_PAGE_SPEED)); kill(GET_TEXT_F(MSG_BAD_PAGE_SPEED));
@ -3212,7 +3264,7 @@ void Planner::set_machine_position_mm(const abce_pos_t &abce) {
if (has_blocks_queued()) { if (has_blocks_queued()) {
//previous_nominal_speed_sqr = 0.0; // Reset planner junction speeds. Assume start from rest. //previous_nominal_speed_sqr = 0.0; // Reset planner junction speeds. Assume start from rest.
//previous_speed.reset(); //previous_speed.reset();
buffer_sync_block(); buffer_sync_block(BLOCK_BIT_SYNC_POSITION);
} }
else { else {
#if ENABLED(BACKLASH_COMPENSATION) #if ENABLED(BACKLASH_COMPENSATION)
@ -3225,12 +3277,6 @@ void Planner::set_machine_position_mm(const abce_pos_t &abce) {
} }
} }
/**
* @brief Set the Planner position in mm
* @details Set the Planner position from a native machine position in mm
*
* @param xyze A native (Cartesian) machine position
*/
void Planner::set_position_mm(const xyze_pos_t &xyze) { void Planner::set_position_mm(const xyze_pos_t &xyze) {
xyze_pos_t machine = xyze; xyze_pos_t machine = xyze;
TERN_(HAS_POSITION_MODIFIERS, apply_modifiers(machine, true)); TERN_(HAS_POSITION_MODIFIERS, apply_modifiers(machine, true));
@ -3259,20 +3305,14 @@ void Planner::set_position_mm(const xyze_pos_t &xyze) {
TERN_(IS_KINEMATIC, TERN_(HAS_EXTRUDERS, position_cart.e = e)); TERN_(IS_KINEMATIC, TERN_(HAS_EXTRUDERS, position_cart.e = e));
if (has_blocks_queued()) if (has_blocks_queued())
buffer_sync_block(); buffer_sync_block(BLOCK_BIT_SYNC_POSITION);
else else
stepper.set_axis_position(E_AXIS, position.e); stepper.set_axis_position(E_AXIS, position.e);
} }
#endif #endif
/** // Recalculate the steps/s^2 acceleration rates, based on the mm/s^2
* @brief Recalculate the steps/s^2 acceleration rates, based on the mm/s^2
* @details Update planner movement factors after a change to certain settings:
* - max_acceleration_steps_per_s2 from settings max_acceleration_mm_per_s2 * axis_steps_per_mm (M201, M92)
* - acceleration_long_cutoff based on the largest max_acceleration_steps_per_s2 (M201)
* - max_e_jerk for all extruders based on junction_deviation_mm (M205 J)
*/
void Planner::refresh_acceleration_rates() { void Planner::refresh_acceleration_rates() {
uint32_t highest_rate = 1; uint32_t highest_rate = 1;
LOOP_DISTINCT_AXES(i) { LOOP_DISTINCT_AXES(i) {
@ -3285,8 +3325,8 @@ void Planner::refresh_acceleration_rates() {
} }
/** /**
* @brief Recalculate 'position' and 'mm_per_step'. * Recalculate 'position' and 'mm_per_step'.
* @details Required whenever settings.axis_steps_per_mm changes! * Must be called whenever settings.axis_steps_per_mm changes!
*/ */
void Planner::refresh_positioning() { void Planner::refresh_positioning() {
LOOP_DISTINCT_AXES(i) mm_per_step[i] = 1.0f / settings.axis_steps_per_mm[i]; LOOP_DISTINCT_AXES(i) mm_per_step[i] = 1.0f / settings.axis_steps_per_mm[i];

92
Marlin/src/module/planner.h

@ -89,30 +89,6 @@
#define HAS_DIST_MM_ARG 1 #define HAS_DIST_MM_ARG 1
#endif #endif
#if ENABLED(LASER_POWER_INLINE)
typedef struct {
bool isPlanned:1;
bool isEnabled:1;
bool dir:1;
bool Reserved:6;
} power_status_t;
typedef struct {
power_status_t status; // See planner settings for meaning
uint8_t power; // Ditto; When in trapezoid mode this is nominal power
#if ENABLED(LASER_POWER_INLINE_TRAPEZOID)
uint8_t power_entry; // Entry power for the laser
#if DISABLED(LASER_POWER_INLINE_TRAPEZOID_CONT)
uint8_t power_exit; // Exit power for the laser
uint32_t entry_per, // Steps per power increment (to avoid floats in stepper calcs)
exit_per; // Steps per power decrement
#endif
#endif
} block_laser_t;
#endif
/** /**
* Planner block flags as boolean bit fields * Planner block flags as boolean bit fields
*/ */
@ -132,14 +108,14 @@ enum BlockFlagBit {
BLOCK_BIT_SYNC_POSITION BLOCK_BIT_SYNC_POSITION
// Direct stepping page // Direct stepping page
#if ENABLED(DIRECT_STEPPING) OPTARG(DIRECT_STEPPING, BLOCK_BIT_PAGE)
, BLOCK_BIT_PAGE
#endif
// Sync the fan speeds from the block // Sync the fan speeds from the block
#if ENABLED(LASER_SYNCHRONOUS_M106_M107) OPTARG(LASER_SYNCHRONOUS_M106_M107, BLOCK_BIT_SYNC_FANS)
, BLOCK_BIT_SYNC_FANS
#endif // Sync laser power from a queued block
OPTARG(LASER_POWER_SYNC, BLOCK_BIT_LASER_PWR)
}; };
/** /**
@ -165,6 +141,10 @@ typedef struct {
#if ENABLED(LASER_SYNCHRONOUS_M106_M107) #if ENABLED(LASER_SYNCHRONOUS_M106_M107)
bool sync_fans:1; bool sync_fans:1;
#endif #endif
#if ENABLED(LASER_POWER_SYNC)
bool sync_laser_pwr:1;
#endif
}; };
}; };
@ -176,9 +156,34 @@ typedef struct {
} block_flags_t; } block_flags_t;
#if ENABLED(LASER_FEATURE)
typedef struct {
bool isEnabled:1; // Set to engage the inline laser power output.
bool dir:1;
bool isPowered:1; // Set on any parsed G1, G2, G3, or G5 powered move, cleared on G0 and G28.
bool isSyncPower:1; // Set on a M3 sync based set laser power, used to determine active trap power
bool Reserved:4;
} power_status_t;
typedef struct {
power_status_t status; // See planner settings for meaning
uint8_t power; // Ditto; When in trapezoid mode this is nominal power
#if ENABLED(LASER_POWER_TRAP)
float trap_ramp_active_pwr; // Laser power level during active trapezoid smoothing
float trap_ramp_entry_incr; // Acceleration per step laser power increment (trap entry)
float trap_ramp_exit_decr; // Deceleration per step laser power decrement (trap exit)
#endif
} block_laser_t;
#endif
/** /**
* A single entry in the planner buffer, used to set up and * struct block_t
* track a coordinated linear motion for one or more axes. *
* A single entry in the planner buffer.
* Tracks linear movement over multiple axes.
* *
* The "nominal" values are as-specified by G-code, and * The "nominal" values are as-specified by G-code, and
* may never actually be reached due to acceleration limits. * may never actually be reached due to acceleration limits.
@ -188,7 +193,8 @@ typedef struct block_t {
volatile block_flags_t flag; // Block flags volatile block_flags_t flag; // Block flags
volatile bool is_fan_sync() { return TERN0(LASER_SYNCHRONOUS_M106_M107, flag.sync_fans); } volatile bool is_fan_sync() { return TERN0(LASER_SYNCHRONOUS_M106_M107, flag.sync_fans); }
volatile bool is_sync() { return flag.sync_position || is_fan_sync(); } volatile bool is_pwr_sync() { return TERN0(LASER_POWER_SYNC, flag.sync_laser_pwr); }
volatile bool is_sync() { return flag.sync_position || is_fan_sync() || is_pwr_sync(); }
volatile bool is_page() { return TERN0(DIRECT_STEPPING, flag.page); } volatile bool is_page() { return TERN0(DIRECT_STEPPING, flag.page); }
volatile bool is_move() { return !(is_sync() || is_page()); } volatile bool is_move() { return !(is_sync() || is_page()); }
@ -270,12 +276,10 @@ typedef struct block_t {
xyze_pos_t start_position; xyze_pos_t start_position;
#endif #endif
#if ENABLED(LASER_POWER_INLINE) #if ENABLED(LASER_FEATURE)
block_laser_t laser; block_laser_t laser;
#endif #endif
void reset() { memset((char*)this, 0, sizeof(*this)); }
} block_t; } block_t;
#if ANY(LIN_ADVANCE, SCARA_FEEDRATE_SCALING, GRADIENT_MIX, LCD_SHOW_E_TOTAL, POWER_LOSS_RECOVERY) #if ANY(LIN_ADVANCE, SCARA_FEEDRATE_SCALING, GRADIENT_MIX, LCD_SHOW_E_TOTAL, POWER_LOSS_RECOVERY)
@ -284,7 +288,7 @@ typedef struct block_t {
#define BLOCK_MOD(n) ((n)&(BLOCK_BUFFER_SIZE-1)) #define BLOCK_MOD(n) ((n)&(BLOCK_BUFFER_SIZE-1))
#if ENABLED(LASER_POWER_INLINE) #if ENABLED(LASER_FEATURE)
typedef struct { typedef struct {
/** /**
* Laser status flags * Laser status flags
@ -293,11 +297,10 @@ typedef struct block_t {
/** /**
* Laser power: 0 or 255 in case of PWM-less laser, * Laser power: 0 or 255 in case of PWM-less laser,
* or the OCR (oscillator count register) value; * or the OCR (oscillator count register) value;
*
* Using OCR instead of raw power, because it avoids * Using OCR instead of raw power, because it avoids
* floating point operations during the move loop. * floating point operations during the move loop.
*/ */
uint8_t power; volatile uint8_t power;
} laser_state_t; } laser_state_t;
#endif #endif
@ -399,7 +402,7 @@ class Planner {
static planner_settings_t settings; static planner_settings_t settings;
#if ENABLED(LASER_POWER_INLINE) #if ENABLED(LASER_FEATURE)
static laser_state_t laser_inline; static laser_state_t laser_inline;
#endif #endif
@ -784,12 +787,11 @@ class Planner {
/** /**
* Planner::buffer_sync_block * Planner::buffer_sync_block
* Add a block to the buffer that just updates the position or in * Add a block to the buffer that just updates the position
* case of LASER_SYNCHRONOUS_M106_M107 the fan pwm * @param sync_flag sets a condition bit to process additional items
* such as sync fan pwm or sync M3/M4 laser power into a queued block
*/ */
static void buffer_sync_block( static void buffer_sync_block(const BlockFlagBit flag=BLOCK_BIT_SYNC_POSITION);
TERN_(LASER_SYNCHRONOUS_M106_M107, const BlockFlagBit flag=BLOCK_BIT_SYNC_POSITION)
);
#if IS_KINEMATIC #if IS_KINEMATIC
private: private:

236
Marlin/src/module/stepper.cpp

@ -253,20 +253,6 @@ xyz_long_t Stepper::endstops_trigsteps;
xyze_long_t Stepper::count_position{0}; xyze_long_t Stepper::count_position{0};
xyze_int8_t Stepper::count_direction{0}; xyze_int8_t Stepper::count_direction{0};
#if ENABLED(LASER_POWER_INLINE_TRAPEZOID)
Stepper::stepper_laser_t Stepper::laser_trap = {
.enabled = false,
.cur_power = 0,
.cruise_set = false,
#if DISABLED(LASER_POWER_INLINE_TRAPEZOID_CONT)
.last_step_count = 0,
.acc_step_count = 0
#else
.till_update = 0
#endif
};
#endif
#define MINDIR(A) (count_direction[_AXIS(A)] < 0) #define MINDIR(A) (count_direction[_AXIS(A)] < 0)
#define MAXDIR(A) (count_direction[_AXIS(A)] > 0) #define MAXDIR(A) (count_direction[_AXIS(A)] > 0)
@ -1964,7 +1950,6 @@ uint32_t Stepper::block_phase_isr() {
// If there is a current block // If there is a current block
if (current_block) { if (current_block) {
// If current block is finished, reset pointer and finalize state // If current block is finished, reset pointer and finalize state
if (step_events_completed >= step_event_count) { if (step_events_completed >= step_event_count) {
#if ENABLED(DIRECT_STEPPING) #if ENABLED(DIRECT_STEPPING)
@ -2017,32 +2002,28 @@ uint32_t Stepper::block_phase_isr() {
else if (LA_steps) nextAdvanceISR = 0; else if (LA_steps) nextAdvanceISR = 0;
#endif #endif
// Update laser - Accelerating /*
#if ENABLED(LASER_POWER_INLINE_TRAPEZOID) * Adjust Laser Power - Accelerating
if (laser_trap.enabled) { * isPowered - True when a move is powered.
#if DISABLED(LASER_POWER_INLINE_TRAPEZOID_CONT) * isEnabled - laser power is active.
if (current_block->laser.entry_per) { * Laser power variables are calulated and stored in this block by the planner code.
laser_trap.acc_step_count -= step_events_completed - laser_trap.last_step_count; *
laser_trap.last_step_count = step_events_completed; * trap_ramp_active_pwr - the active power in this block across accel or decel trap steps.
* trap_ramp_entry_incr - holds the precalculated value to increase the current power per accel step.
// Should be faster than a divide, since this should trip just once *
if (laser_trap.acc_step_count < 0) { * Apply the starting active power and then increase power per step by the trap_ramp_entry_incr value if positive.
while (laser_trap.acc_step_count < 0) { */
laser_trap.acc_step_count += current_block->laser.entry_per;
if (laser_trap.cur_power < current_block->laser.power) laser_trap.cur_power++; #if ENABLED(LASER_POWER_TRAP)
} if (cutter.cutter_mode == CUTTER_MODE_CONTINUOUS) {
cutter.ocr_set_power(laser_trap.cur_power); if (planner.laser_inline.status.isPowered && planner.laser_inline.status.isEnabled) {
} if (current_block->laser.trap_ramp_entry_incr > 0) {
} cutter.apply_power(current_block->laser.trap_ramp_active_pwr);
#else current_block->laser.trap_ramp_active_pwr += current_block->laser.trap_ramp_entry_incr;
if (laser_trap.till_update)
laser_trap.till_update--;
else {
laser_trap.till_update = LASER_POWER_INLINE_TRAPEZOID_CONT_PER;
laser_trap.cur_power = (current_block->laser.power * acc_step_rate) / current_block->nominal_rate;
cutter.ocr_set_power(laser_trap.cur_power); // Cycle efficiency is irrelevant it the last line was many cycles
} }
#endif }
// Not a powered move.
else cutter.apply_power(0);
} }
#endif #endif
} }
@ -2066,7 +2047,6 @@ uint32_t Stepper::block_phase_isr() {
: current_block->final_rate; : current_block->final_rate;
} }
#else #else
// Using the old trapezoidal control // Using the old trapezoidal control
step_rate = STEP_MULTIPLY(deceleration_time, current_block->acceleration_rate); step_rate = STEP_MULTIPLY(deceleration_time, current_block->acceleration_rate);
if (step_rate < acc_step_rate) { // Still decelerating? if (step_rate < acc_step_rate) { // Still decelerating?
@ -2094,37 +2074,25 @@ uint32_t Stepper::block_phase_isr() {
else if (LA_steps) nextAdvanceISR = 0; else if (LA_steps) nextAdvanceISR = 0;
#endif // LIN_ADVANCE #endif // LIN_ADVANCE
// Update laser - Decelerating /*
#if ENABLED(LASER_POWER_INLINE_TRAPEZOID) * Adjust Laser Power - Decelerating
if (laser_trap.enabled) { * trap_ramp_entry_decr - holds the precalculated value to decrease the current power per decel step.
#if DISABLED(LASER_POWER_INLINE_TRAPEZOID_CONT) */
if (current_block->laser.exit_per) { #if ENABLED(LASER_POWER_TRAP)
laser_trap.acc_step_count -= step_events_completed - laser_trap.last_step_count; if (cutter.cutter_mode == CUTTER_MODE_CONTINUOUS) {
laser_trap.last_step_count = step_events_completed; if (planner.laser_inline.status.isPowered && planner.laser_inline.status.isEnabled) {
if (current_block->laser.trap_ramp_exit_decr > 0) {
// Should be faster than a divide, since this should trip just once current_block->laser.trap_ramp_active_pwr -= current_block->laser.trap_ramp_exit_decr;
if (laser_trap.acc_step_count < 0) { cutter.apply_power(current_block->laser.trap_ramp_active_pwr);
while (laser_trap.acc_step_count < 0) {
laser_trap.acc_step_count += current_block->laser.exit_per;
if (laser_trap.cur_power > current_block->laser.power_exit) laser_trap.cur_power--;
}
cutter.ocr_set_power(laser_trap.cur_power);
}
}
#else
if (laser_trap.till_update)
laser_trap.till_update--;
else {
laser_trap.till_update = LASER_POWER_INLINE_TRAPEZOID_CONT_PER;
laser_trap.cur_power = (current_block->laser.power * step_rate) / current_block->nominal_rate;
cutter.ocr_set_power(laser_trap.cur_power); // Cycle efficiency isn't relevant when the last line was many cycles
} }
#endif // Not a powered move.
else cutter.apply_power(0);
}
} }
#endif #endif
} }
// Must be in cruise phase otherwise else { // Must be in cruise phase otherwise
else {
#if ENABLED(LIN_ADVANCE) #if ENABLED(LIN_ADVANCE)
// If there are any esteps, fire the next advance_isr "now" // If there are any esteps, fire the next advance_isr "now"
@ -2139,24 +2107,50 @@ uint32_t Stepper::block_phase_isr() {
// The timer interval is just the nominal value for the nominal speed // The timer interval is just the nominal value for the nominal speed
interval = ticks_nominal; interval = ticks_nominal;
}
// Update laser - Cruising /* Adjust Laser Power - Cruise
#if ENABLED(LASER_POWER_INLINE_TRAPEZOID) * power - direct or floor adjusted active laser power.
if (laser_trap.enabled) { */
if (!laser_trap.cruise_set) {
laser_trap.cur_power = current_block->laser.power; #if ENABLED(LASER_POWER_TRAP)
cutter.ocr_set_power(laser_trap.cur_power); if (cutter.cutter_mode == CUTTER_MODE_CONTINUOUS) {
laser_trap.cruise_set = true; if (step_events_completed + 1 == accelerate_until) {
if (planner.laser_inline.status.isPowered && planner.laser_inline.status.isEnabled) {
if (current_block->laser.trap_ramp_entry_incr > 0) {
current_block->laser.trap_ramp_active_pwr = current_block->laser.power;
cutter.apply_power(current_block->laser.power);
}
} }
#if ENABLED(LASER_POWER_INLINE_TRAPEZOID_CONT) // Not a powered move.
laser_trap.till_update = LASER_POWER_INLINE_TRAPEZOID_CONT_PER; else cutter.apply_power(0);
#else
laser_trap.last_step_count = step_events_completed;
#endif
} }
#endif }
} #endif
} }
#if ENABLED(LASER_FEATURE)
/*
* CUTTER_MODE_DYNAMIC is experimental and developing.
* Super-fast method to dynamically adjust the laser power OCR value based on the input feedrate in mm-per-minute.
* TODO: Set up Min/Max OCR offsets to allow tuning and scaling of various lasers.
* TODO: Integrate accel/decel +-rate into the dynamic laser power calc.
*/
if (cutter.cutter_mode == CUTTER_MODE_DYNAMIC
&& planner.laser_inline.status.isPowered // isPowered flag set on any parsed G1, G2, G3, or G5 move; cleared on any others.
&& cutter.last_block_power != current_block->laser.power // Prevent constant update without change
) {
cutter.apply_power(current_block->laser.power);
cutter.last_block_power = current_block->laser.power;
}
#endif
}
else { // !current_block
#if ENABLED(LASER_FEATURE)
if (cutter.cutter_mode == CUTTER_MODE_DYNAMIC) {
cutter.apply_power(0); // No movement in dynamic mode so turn Laser off
}
#endif
} }
// If there is no current block at this point, attempt to pop one from the buffer // If there is no current block at this point, attempt to pop one from the buffer
@ -2169,11 +2163,18 @@ uint32_t Stepper::block_phase_isr() {
// Sync block? Sync the stepper counts or fan speeds and return // Sync block? Sync the stepper counts or fan speeds and return
while (current_block->is_sync()) { while (current_block->is_sync()) {
if (current_block->is_fan_sync()) { #if ENABLED(LASER_POWER_SYNC)
TERN_(LASER_SYNCHRONOUS_M106_M107, planner.sync_fan_speeds(current_block->fan_speed)); if (cutter.cutter_mode == CUTTER_MODE_CONTINUOUS) {
} if (current_block->is_pwr_sync()) {
else planner.laser_inline.status.isSyncPower = true;
_set_position(current_block->position); cutter.apply_power(current_block->laser.power);
}
}
#endif
TERN_(LASER_SYNCHRONOUS_M106_M107, if (current_block->is_fan_sync()) planner.sync_fan_speeds(current_block->fan_speed));
if (!(current_block->is_fan_sync() || current_block->is_pwr_sync())) _set_position(current_block->position);
discard_current_block(); discard_current_block();
@ -2183,8 +2184,10 @@ uint32_t Stepper::block_phase_isr() {
} }
// For non-inline cutter, grossly apply power // For non-inline cutter, grossly apply power
#if ENABLED(LASER_FEATURE) && DISABLED(LASER_POWER_INLINE) #if HAS_CUTTER
cutter.apply_power(current_block->cutter_power); if (cutter.cutter_mode == CUTTER_MODE_STANDARD) {
cutter.apply_power(current_block->cutter_power);
}
#endif #endif
#if ENABLED(POWER_LOSS_RECOVERY) #if ENABLED(POWER_LOSS_RECOVERY)
@ -2357,36 +2360,22 @@ uint32_t Stepper::block_phase_isr() {
set_directions(current_block->direction_bits); set_directions(current_block->direction_bits);
} }
#if ENABLED(LASER_POWER_INLINE) #if ENABLED(LASER_FEATURE)
const power_status_t stat = current_block->laser.status; if (cutter.cutter_mode == CUTTER_MODE_CONTINUOUS) { // Planner controls the laser
#if ENABLED(LASER_POWER_INLINE_TRAPEZOID) if (planner.laser_inline.status.isSyncPower)
laser_trap.enabled = stat.isPlanned && stat.isEnabled; // If the previous block was a M3 sync power then skip the trap power init otherwise it will 0 the sync power.
laser_trap.cur_power = current_block->laser.power_entry; // RESET STATE planner.laser_inline.status.isSyncPower = false; // Clear the flag to process subsequent trap calc's.
laser_trap.cruise_set = false; else if (current_block->laser.status.isEnabled) {
#if DISABLED(LASER_POWER_INLINE_TRAPEZOID_CONT) #if ENABLED(LASER_POWER_TRAP)
laser_trap.last_step_count = 0; TERN_(DEBUG_LASER_TRAP, SERIAL_ECHO_MSG("InitTrapPwr:",current_block->laser.trap_ramp_active_pwr));
laser_trap.acc_step_count = current_block->laser.entry_per / 2; cutter.apply_power(current_block->laser.status.isPowered ? current_block->laser.trap_ramp_active_pwr : 0);
#else
laser_trap.till_update = 0;
#endif
// Always have PWM in this case
if (stat.isPlanned) { // Planner controls the laser
cutter.ocr_set_power(
stat.isEnabled ? laser_trap.cur_power : 0 // ON with power or OFF
);
}
#else
if (stat.isPlanned) { // Planner controls the laser
#if ENABLED(SPINDLE_LASER_USE_PWM)
cutter.ocr_set_power(
stat.isEnabled ? current_block->laser.power : 0 // ON with power or OFF
);
#else #else
cutter.set_enabled(stat.isEnabled); TERN_(DEBUG_CUTTER_POWER, SERIAL_ECHO_MSG("InlinePwr:",current_block->laser.power));
cutter.apply_power(current_block->laser.status.isPowered ? current_block->laser.power : 0);
#endif #endif
} }
#endif }
#endif // LASER_POWER_INLINE #endif // LASER_FEATURE
// If the endstop is already pressed, endstop interrupts won't invoke // If the endstop is already pressed, endstop interrupts won't invoke
// endstop_triggered and the move will grind. So check here for a // endstop_triggered and the move will grind. So check here for a
@ -2416,21 +2405,6 @@ uint32_t Stepper::block_phase_isr() {
// Calculate the initial timer interval // Calculate the initial timer interval
interval = calc_timer_interval(current_block->initial_rate, &steps_per_isr); interval = calc_timer_interval(current_block->initial_rate, &steps_per_isr);
} }
#if ENABLED(LASER_POWER_INLINE_CONTINUOUS)
else { // No new block found; so apply inline laser parameters
// This should mean ending file with 'M5 I' will stop the laser; thus the inline flag isn't needed
const power_status_t stat = planner.laser_inline.status;
if (stat.isPlanned) { // Planner controls the laser
#if ENABLED(SPINDLE_LASER_USE_PWM)
cutter.ocr_set_power(
stat.isEnabled ? planner.laser_inline.power : 0 // ON with power or OFF
);
#else
cutter.set_enabled(stat.isEnabled);
#endif
}
}
#endif
} }
// Return the interval to wait // Return the interval to wait

19
Marlin/src/module/stepper.h

@ -444,25 +444,6 @@ class Stepper {
// Current stepper motor directions (+1 or -1) // Current stepper motor directions (+1 or -1)
static xyze_int8_t count_direction; static xyze_int8_t count_direction;
#if ENABLED(LASER_POWER_INLINE_TRAPEZOID)
typedef struct {
bool enabled; // Trapezoid needed flag (i.e., laser on, planner in control)
uint8_t cur_power; // Current laser power
bool cruise_set; // Power set up for cruising?
#if ENABLED(LASER_POWER_INLINE_TRAPEZOID_CONT)
uint16_t till_update; // Countdown to the next update
#else
uint32_t last_step_count, // Step count from the last update
acc_step_count; // Bresenham counter for laser accel/decel
#endif
} stepper_laser_t;
static stepper_laser_t laser_trap;
#endif
public: public:
// Initialize stepper hardware // Initialize stepper hardware
static void init(); static void init();

5
Marlin/src/module/temperature.cpp

@ -1904,9 +1904,10 @@ void Temperature::task() {
#if ENABLED(LASER_COOLANT_FLOW_METER) #if ENABLED(LASER_COOLANT_FLOW_METER)
cooler.flowmeter_task(ms); cooler.flowmeter_task(ms);
#if ENABLED(FLOWMETER_SAFETY) #if ENABLED(FLOWMETER_SAFETY)
if (cutter.enabled() && cooler.check_flow_too_low()) { if (cooler.check_flow_too_low()) {
TERN_(HAS_DISPLAY, if (cutter.enabled()) ui.flow_fault());
cutter.disable(); cutter.disable();
TERN_(HAS_DISPLAY, ui.flow_fault()); cutter.cutter_mode = CUTTER_MODE_ERROR; // Immediately kill stepper inline power output
} }
#endif #endif
#endif #endif

4
buildroot/tests/mega2560

@ -189,7 +189,7 @@ opt_set MOTHERBOARD BOARD_RAMPS_14_EFB EXTRUDERS 0 LCD_LANGUAGE en TEMP_SENSOR_C
AXIS_RELATIVE_MODES '{ false, false, false }' AXIS_RELATIVE_MODES '{ false, false, false }'
opt_enable REPRAP_DISCOUNT_FULL_GRAPHIC_SMART_CONTROLLER SDSUPPORT EEPROM_SETTINGS EEPROM_BOOT_SILENT EEPROM_AUTO_INIT MEATPACK_ON_SERIAL_PORT_1 \ opt_enable REPRAP_DISCOUNT_FULL_GRAPHIC_SMART_CONTROLLER SDSUPPORT EEPROM_SETTINGS EEPROM_BOOT_SILENT EEPROM_AUTO_INIT MEATPACK_ON_SERIAL_PORT_1 \
LASER_FEATURE LASER_SAFETY_TIMEOUT_MS LASER_COOLANT_FLOW_METER AIR_EVACUATION AIR_EVACUATION_PIN AIR_ASSIST AIR_ASSIST_PIN LASER_FEATURE LASER_SAFETY_TIMEOUT_MS LASER_COOLANT_FLOW_METER AIR_EVACUATION AIR_EVACUATION_PIN AIR_ASSIST AIR_ASSIST_PIN
exec_test $1 $2 "MEGA2560 RAMPS | Laser Feature | Air Evacuation | Air Assist | Cooler | Flowmeter | 12864 LCD | meatpack | SERIAL_PORT_2 " "$3" exec_test $1 $2 "MEGA2560 RAMPS | Laser Feature | Air Evacuation | Air Assist | Cooler | Flowmeter | 12864 LCD | meatpack | Laser Safety Timeout | M3 Power Sync | Trap Power Smoothing | SERIAL_PORT_2 " "$3"
# #
# Test Laser features with 44780 LCD # Test Laser features with 44780 LCD
@ -203,7 +203,7 @@ opt_set MOTHERBOARD BOARD_RAMPS_14_EFB EXTRUDERS 0 LCD_LANGUAGE en TEMP_SENSOR_C
AXIS_RELATIVE_MODES '{ false, false, false }' AXIS_RELATIVE_MODES '{ false, false, false }'
opt_enable REPRAP_DISCOUNT_SMART_CONTROLLER SDSUPPORT EEPROM_SETTINGS EEPROM_BOOT_SILENT EEPROM_AUTO_INIT PRINTCOUNTER I2C_AMMETER \ opt_enable REPRAP_DISCOUNT_SMART_CONTROLLER SDSUPPORT EEPROM_SETTINGS EEPROM_BOOT_SILENT EEPROM_AUTO_INIT PRINTCOUNTER I2C_AMMETER \
LASER_FEATURE LASER_SAFETY_TIMEOUT_MS LASER_COOLANT_FLOW_METER AIR_EVACUATION AIR_EVACUATION_PIN AIR_ASSIST AIR_ASSIST_PIN LASER_FEATURE LASER_SAFETY_TIMEOUT_MS LASER_COOLANT_FLOW_METER AIR_EVACUATION AIR_EVACUATION_PIN AIR_ASSIST AIR_ASSIST_PIN
exec_test $1 $2 "MEGA2560 RAMPS | Laser Feature | Air Evacuation | Air Assist | Cooler | Flowmeter | 44780 LCD " "$3" exec_test $1 $2 "MEGA2560 RAMPS | Laser Feature | Air Evacuation | Air Assist | Cooler | Laser Safety Timeout | Flowmeter | 44780 LCD " "$3"
# #
# Test redundant temperature sensors + MAX TC # Test redundant temperature sensors + MAX TC

137
docs/Cutter.md

@ -0,0 +1,137 @@
### Introduction
With Marlin version 2.0.9.x or higher, Laser improvements were introduced that enhance inline functionality. Previously the inline feature option was not operational without enabling and recompiling the source. Also with inline enabled the base features are not functional. With v2.0.9.x new functionality is added which allows the standard and inline modes to be G-Code selectable and also compatible with each other. Additionally an experimental dynamic mode is also available. Spindle operational features are available with defines and recompiling.
### Architecture
Laser selectable feature capability is defined through 4 global mode flags within gcode ,laser/spindle, planner and stepper routines. The default mode maintains the standard laser function. G-Codes are received, processed and parsed to determine what mode to set through M3, M4 and M5 commands. When the inline mode parameter set is detected, laser power processing will be driven through the planner and stepper routines. Handling of the initial power values and settings are performed by G-Code parsing and the laser/spindle routines.
Inline power feeds from the block->inline_power variable into the planner's laser.power when in continuous power mode. Further power adjustment will be applied if the laser power trap feature is active otherwise laser.power is used as set in the stepper for the entire block. When laser power trap is active the power levels are step incremented during acceleration and step decremented during deceleration.
Two additional power sets are fed in the planner by features laser power sync and laser fan power sync. Both of these power sets are done with planner sync block bit flags. With laser power sync, when the bit flag is matched the global block laser.power value is updated from laser/spindle standard M3 S-Value power sets. For laser fan sync, power values are updated into the planner block->fan_speed[i] variable from fan G-Code S-Value sets.
With dynamic inline power mode, F-Value feedrate sets are processed with cutter.calc_dynamic_power() and fed into the planner laser.power value.
Irrespective of what laser power value source is used, the final laser output pin is always updated using the laser/spindle code. Specifically the apply_power(value) call is used to set the laser or spindle output. This call permits safe power control in the event that a sensor fault occurs.
Note: Spindle operation is not selectable with G-Codes at this time.
The following flow charts depict the flow control logic for spindle and laser operations in the code base.
#### Spindle Mode Logic:
┌──────────┐ ┌───────────┐ ┌───────────┐
│M3 S-Value│ │Dir !same ?│ │Stepper │
│Spindle │ │stop & wait│ │processes │
┌──┤Clockwise ├──┤ & start ├──┤moves │
┌─────┐ │ │ │ │spindle │ │ │
│GCode│ │ └──────────┘ └───────────┘ └───────────┘
│Send ├──┤ ┌──────────┐ ┌───────────┐ ┌───────────┐
└─────┘ │ │M4 S-Value│ │Dir !same ?│ │Stepper │
├──┤Spindle ├──┤stop & wait├──┤processes │
│ │Counter │ │& start │ │moves │
│ │Clockwise │ │spindle │ │ │
│ └──────────┘ └───────────┘ └───────────┘
│ ┌──────────┐ ┌────────┐
│ │M5 │ │Wait for│
│ │Spindle ├──┤move &
└──┤Stop │ │disable │
└──────────┘ └────────┘
┌──────────┐ ┌──────────┐
Sensors─────┤Fault ├──┤Disable │
└──────────┘ │power │
└──────────┘
#### Laser Mode Logic:
┌──────────┐ ┌─────────────┐ ┌───────────┐
│M3,M4,M5 I│ │Set power │ │Stepper │
┌──┤Standard ├──┤Immediately &├──┤processes │
│ │Default │ │wait for move│ │moves │
│ │ │ │completion │ │ │
│ └──────────┘ └─────────────┘ └───────────┘
│ ┌──────────┐ ┌───────────┐ ┌───────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌───────────┐
┌─────┐ │ │M3 I │ │G0,G1,G2,G4│ │Planner │ │Planner │ │Planner fan │ │Planner │ │Stepper │
│GCode│ │ │Continuous│ │M3 receive │ │sets block │ │sync power ?│ │sync power ?│ │trap power ?│ │uses block │
│Send ├──┼──┤Inline ├──┤power from ├──┤power using├──┤process M3 ├──┤process fan ├──┤adjusts for ├──┤values to │
└─────┘ │ │ │ │S-Value │ │Gx S-Value │ │power inline│ │power inline│ │accel/decel │ │apply power│
│ └──────────┘ └───────────┘ └───────────┘ └────────────┘ └────────────┘ └────────────┘ └───────────┘
│ ┌──────────┐ ┌───────────┐ ┌────────────────┐ ┌───────────┐
│ │M4 I │ │Gx F-Value │ │Planner │ │Stepper │
│ │Dynamic │ │set power │ │Calc & set block│ │uses block │
└──┤Inline ├──┤or use ├──┤block power ├──┤values to │
│ │ │default │ │using F-Value │ │apply power│
└──────────┘ └───────────┘ └────────────────┘ └───────────┘
┌──────────┐ ┌──────────┐
Sensors─────┤Fault ├──┤Disable │
└──────────┘ │Power │
└──────────┘
<!-- https://asciiflow.com/#/ -->
### Continuous Inline Trap Power Calculations
When LASER_FEATURE and LASER_POWER_TRAP are defined, planner calculations are performed and applied to the incoming laser power S-Value. The power will be factored and distributed across trapezoid acceleration and deceleration movements.
When the laser.power > 0
We set a minimum power if defined in SPEED_POWER_MIN it's fed into the planner block as laser_power_floor.
A reduced entry laser power factor is based on the entry step rate to cruise step rate ratio for acceleration.
block entry laser power = laser power * ( entry step rate / cruise step rate )
The initial power will be set to no less than the laser_power_floor or the inital power calculation.
The reduced final power factor is based on the final step rate to cruise step rate ratio for deceleration.
block exit laser power = laser power * ( exit step rate / cruise step rate )
Once the entry and exit power values are determined, the values are divided into step increments to be applied in the stepper.
trap step power incr_decr = ( cruize power - entry_exit ) / accel_decel_steps
The trap steps are incremented or decremented during each accel or decel step until the block is complete.
Step power is either cumulatively added or subtracted during trapeziod ramp progressions.
#### Planner Code:
```
if (block->laser.power > 0) {
NOLESS(block->laser.power, laser_power_floor);
block->laser.trap_ramp_active_pwr = (block->laser.power - laser_power_floor) * (initial_rate / float(block->nominal_rate)) + laser_power_floor;
block->laser.trap_ramp_entry_incr = (block->laser.power - block->laser.trap_ramp_active_pwr) / accelerate_steps;
float laser_pwr = block->laser.power * (final_rate / float(block->nominal_rate));
NOLESS(laser_pwr, laser_power_floor);
block->laser.trap_ramp_exit_decr = (block->laser.power - laser_pwr) / decelerate_steps;
```
#### Stepper Code:
```
if (current_block->laser.trap_ramp_entry_incr > 0) {
cutter.apply_power(current_block->laser.trap_ramp_active_pwr);
current_block->laser.trap_ramp_active_pwr += current_block->laser.trap_ramp_entry_incr;
```
```
if (current_block->laser.trap_ramp_exit_decr > 0) {
current_block->laser.trap_ramp_active_pwr -= current_block->laser.trap_ramp_exit_decr;
cutter.apply_power(current_block->laser.trap_ramp_active_pwr);
```
### Dynamic Inline Calculations
Dynamic mode will calculate laser power based on the F-Value feedrate. The method uses bit shifting to set a power level from 0 to 255. It's simple and fast and we can use a scaler to shift the laser power output to center on a given power level.
#### Spindle/Laser Code:
```
// Dynamic mode rate calculation
static inline uint8_t calc_dynamic_power() {
if (feedrate_mm_m > 65535) return 255; // Too fast, go always on
uint16_t rate = uint16_t(feedrate_mm_m); // 16 bits from the G-code parser float input
rate >>= 8; // Take the G-code input e.g. F40000 and shift off the lower bits to get an OCR value from 1-255
return uint8_t(rate);
}
```
Loading…
Cancel
Save