Browse Source

⚡️ Fix and improve Inline Laser Power (#22690)

Mike La Spina 1 year ago
parent
commit
6a67ad4e4a

+ 33
- 71
Marlin/Configuration_adv.h View File

@@ -3668,8 +3668,11 @@
3668 3668
     #endif
3669 3669
 
3670 3670
     // Define the minimum and maximum test pulse time values for a laser test fire function
3671
-    #define LASER_TEST_PULSE_MIN           1   // Used with Laser Control Menu
3672
-    #define LASER_TEST_PULSE_MAX         999   // Caution: Menu may not show more than 3 characters
3671
+    #define LASER_TEST_PULSE_MIN           1   // (ms) Used with Laser Control Menu
3672
+    #define LASER_TEST_PULSE_MAX         999   // (ms) Caution: Menu may not show more than 3 characters
3673
+
3674
+    #define SPINDLE_LASER_POWERUP_DELAY   50   // (ms) Delay to allow the spindle/laser to come up to speed/power
3675
+    #define SPINDLE_LASER_POWERDOWN_DELAY 50   // (ms) Delay to allow the spindle to stop
3673 3676
 
3674 3677
    /**
3675 3678
     * Laser Safety Timeout
@@ -3682,79 +3685,38 @@
3682 3685
     #define LASER_SAFETY_TIMEOUT_MS     1000   // (ms)
3683 3686
 
3684 3687
     /**
3685
-     * Enable inline laser power to be handled in the planner / stepper routines.
3686
-     * Inline power is specified by the I (inline) flag in an M3 command (e.g., M3 S20 I)
3687
-     * or by the 'S' parameter in G0/G1/G2/G3 moves (see LASER_MOVE_POWER).
3688
+     * Any M3 or G1/2/3/5 command with the 'I' parameter enables continuous inline power mode.
3689
+     *
3690
+     * e.g., 'M3 I' enables continuous inline power which is processed by the planner.
3691
+     * Power is stored in move blocks and applied when blocks are processed by the Stepper ISR.
3692
+     *
3693
+     * 'M4 I' sets dynamic mode which uses the current feedrate to calculate a laser power OCR value.
3688 3694
      *
3689
-     * This allows the laser to keep in perfect sync with the planner and removes
3690
-     * the powerup/down delay since lasers require negligible time.
3695
+     * Any move in dynamic mode will use the current feedrate to calculate the laser power.
3696
+     * Feed rates are set by the F parameter of a move command e.g. G1 X0 Y10 F6000
3697
+     * Laser power would be calculated by bit shifting off 8 LSB's. In binary this is div 256.
3698
+     * The calculation gives us ocr values from 0 to 255, values over F65535 will be set as 255 .
3699
+     * More refined power control such as compesation for accell/decell will be addressed in future releases.
3700
+     *
3701
+     * M5 I clears inline mode and set power to 0, M5 sets the power output to 0 but leaves inline mode on.
3691 3702
      */
3692
-    //#define LASER_POWER_INLINE
3693
-
3694
-    #if ENABLED(LASER_POWER_INLINE)
3695
-      /**
3696
-       * Scale the laser's power in proportion to the movement rate.
3697
-       *
3698
-       * - Sets the entry power proportional to the entry speed over the nominal speed.
3699
-       * - Ramps the power up every N steps to approximate the speed trapezoid.
3700
-       * - Due to the limited power resolution this is only approximate.
3701
-       */
3702
-      #define LASER_POWER_INLINE_TRAPEZOID
3703
-
3704
-      /**
3705
-       * Continuously calculate the current power (nominal_power * current_rate / nominal_rate).
3706
-       * Required for accurate power with non-trapezoidal acceleration (e.g., S_CURVE_ACCELERATION).
3707
-       * This is a costly calculation so this option is discouraged on 8-bit AVR boards.
3708
-       *
3709
-       * LASER_POWER_INLINE_TRAPEZOID_CONT_PER defines how many step cycles there are between power updates. If your
3710
-       * board isn't able to generate steps fast enough (and you are using LASER_POWER_INLINE_TRAPEZOID_CONT), increase this.
3711
-       * Note that when this is zero it means it occurs every cycle; 1 means a delay wait one cycle then run, etc.
3712
-       */
3713
-      //#define LASER_POWER_INLINE_TRAPEZOID_CONT
3714
-
3715
-      /**
3716
-       * Stepper iterations between power updates. Increase this value if the board
3717
-       * can't keep up with the processing demands of LASER_POWER_INLINE_TRAPEZOID_CONT.
3718
-       * Disable (or set to 0) to recalculate power on every stepper iteration.
3719
-       */
3720
-      //#define LASER_POWER_INLINE_TRAPEZOID_CONT_PER 10
3721
-
3722
-      /**
3723
-       * Include laser power in G0/G1/G2/G3/G5 commands with the 'S' parameter
3724
-       */
3725
-      //#define LASER_MOVE_POWER
3726
-
3727
-      #if ENABLED(LASER_MOVE_POWER)
3728
-        // Turn off the laser on G0 moves with no power parameter.
3729
-        // If a power parameter is provided, use that instead.
3730
-        //#define LASER_MOVE_G0_OFF
3731 3703
 
3732
-        // Turn off the laser on G28 homing.
3733
-        //#define LASER_MOVE_G28_OFF
3734
-      #endif
3735
-
3736
-      /**
3737
-       * Inline flag inverted
3738
-       *
3739
-       * WARNING: M5 will NOT turn off the laser unless another move
3740
-       *          is done (so G-code files must end with 'M5 I').
3741
-       */
3742
-      //#define LASER_POWER_INLINE_INVERT
3743
-
3744
-      /**
3745
-       * Continuously apply inline power. ('M3 S3' == 'G1 S3' == 'M3 S3 I')
3746
-       *
3747
-       * The laser might do some weird things, so only enable this
3748
-       * feature if you understand the implications.
3749
-       */
3750
-      //#define LASER_POWER_INLINE_CONTINUOUS
3751
-
3752
-    #else
3753
-
3754
-      #define SPINDLE_LASER_POWERUP_DELAY     50 // (ms) Delay to allow the spindle/laser to come up to speed/power
3755
-      #define SPINDLE_LASER_POWERDOWN_DELAY   50 // (ms) Delay to allow the spindle to stop
3704
+    /**
3705
+     * Enable M3 commands for laser mode inline power planner syncing.
3706
+     * This feature enables any M3 S-value to be injected into the block buffers while in
3707
+     * CUTTER_MODE_CONTINUOUS. The option allows M3 laser power to be commited without waiting
3708
+     * for a planner syncronization
3709
+     */
3710
+    //#define LASER_POWER_SYNC
3756 3711
 
3757
-    #endif
3712
+    /**
3713
+     * Scale the laser's power in proportion to the movement rate.
3714
+     *
3715
+     * - Sets the entry power proportional to the entry speed over the nominal speed.
3716
+     * - Ramps the power up every N steps to approximate the speed trapezoid.
3717
+     * - Due to the limited power resolution this is only approximate.
3718
+     */
3719
+    //#define LASER_POWER_TRAP
3758 3720
 
3759 3721
     //
3760 3722
     // Laser I2C Ammeter (High precision INA226 low/high side module)

+ 51
- 37
Marlin/src/feature/spindle_laser.cpp View File

@@ -39,18 +39,26 @@
39 39
 #endif
40 40
 
41 41
 SpindleLaser cutter;
42
-uint8_t SpindleLaser::power,
42
+bool SpindleLaser::enable_state;                                      // Virtual enable state, controls enable pin if present and or apply power if > 0
43
+uint8_t SpindleLaser::power,                                          // Actual power output 0-255 ocr or "0 = off" > 0 = "on"
43 44
         SpindleLaser::last_power_applied; // = 0                      // Basic power state tracking
45
+
44 46
 #if ENABLED(LASER_FEATURE)
45
-  cutter_test_pulse_t SpindleLaser::testPulse = 50;                   // Test fire Pulse time ms value.
47
+  cutter_test_pulse_t SpindleLaser::testPulse = 50;                   // (ms) Test fire pulse default duration
48
+  uint8_t SpindleLaser::last_block_power; // = 0                      // Track power changes for dynamic inline power
49
+  feedRate_t SpindleLaser::feedrate_mm_m = 1500,
50
+             SpindleLaser::last_feedrate_mm_m; // = 0                 // (mm/min) Track feedrate changes for dynamic power
46 51
 #endif
47
-bool SpindleLaser::isReady;                                           // Ready to apply power setting from the UI to OCR
48
-cutter_power_t SpindleLaser::menuPower,                               // Power set via LCD menu in PWM, PERCENT, or RPM
49
-               SpindleLaser::unitPower;                               // LCD status power in PWM, PERCENT, or RPM
50 52
 
51
-#if ENABLED(MARLIN_DEV_MODE)
52
-  cutter_frequency_t SpindleLaser::frequency;                         // PWM frequency setting; range: 2K - 50K
53
-#endif
53
+bool SpindleLaser::isReadyForUI = false;                              // Ready to apply power setting from the UI to OCR
54
+CutterMode SpindleLaser::cutter_mode = CUTTER_MODE_STANDARD;          // Default is standard mode
55
+
56
+constexpr cutter_cpower_t SpindleLaser::power_floor;
57
+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.
58
+               SpindleLaser::unitPower = 0;                           // Unit power is in PWM, PERCENT, or RPM based on CUTTER_POWER_UNIT.
59
+
60
+cutter_frequency_t SpindleLaser::frequency;                           // PWM frequency setting; range: 2K - 50K
61
+
54 62
 #define SPINDLE_LASER_PWM_OFF TERN(SPINDLE_LASER_PWM_INVERT, 255, 0)
55 63
 
56 64
 /**
@@ -65,14 +73,14 @@ void SpindleLaser::init() {
65 73
   #if ENABLED(SPINDLE_CHANGE_DIR)
66 74
     OUT_WRITE(SPINDLE_DIR_PIN, SPINDLE_INVERT_DIR);                   // Init rotation to clockwise (M3)
67 75
   #endif
76
+  #if ENABLED(HAL_CAN_SET_PWM_FREQ) && SPINDLE_LASER_FREQUENCY
77
+    frequency = SPINDLE_LASER_FREQUENCY;
78
+    hal.set_pwm_frequency(pin_t(SPINDLE_LASER_PWM_PIN), SPINDLE_LASER_FREQUENCY);
79
+  #endif
68 80
   #if ENABLED(SPINDLE_LASER_USE_PWM)
69 81
     SET_PWM(SPINDLE_LASER_PWM_PIN);
70 82
     hal.set_pwm_duty(pin_t(SPINDLE_LASER_PWM_PIN), SPINDLE_LASER_PWM_OFF); // Set to lowest speed
71 83
   #endif
72
-  #if ENABLED(HAL_CAN_SET_PWM_FREQ) && SPINDLE_LASER_FREQUENCY
73
-    hal.set_pwm_frequency(pin_t(SPINDLE_LASER_PWM_PIN), SPINDLE_LASER_FREQUENCY);
74
-    TERN_(MARLIN_DEV_MODE, frequency = SPINDLE_LASER_FREQUENCY);
75
-  #endif
76 84
   #if ENABLED(AIR_EVACUATION)
77 85
     OUT_WRITE(AIR_EVACUATION_PIN, !AIR_EVACUATION_ACTIVE);            // Init Vacuum/Blower OFF
78 86
   #endif
@@ -90,7 +98,7 @@ void SpindleLaser::init() {
90 98
    */
91 99
   void SpindleLaser::_set_ocr(const uint8_t ocr) {
92 100
     #if ENABLED(HAL_CAN_SET_PWM_FREQ) && SPINDLE_LASER_FREQUENCY
93
-      hal.set_pwm_frequency(pin_t(SPINDLE_LASER_PWM_PIN), TERN(MARLIN_DEV_MODE, frequency, SPINDLE_LASER_FREQUENCY));
101
+      hal.set_pwm_frequency(pin_t(SPINDLE_LASER_PWM_PIN), frequency);
94 102
     #endif
95 103
     hal.set_pwm_duty(pin_t(SPINDLE_LASER_PWM_PIN), ocr ^ SPINDLE_LASER_PWM_OFF);
96 104
   }
@@ -107,35 +115,41 @@ void SpindleLaser::init() {
107 115
 #endif // SPINDLE_LASER_USE_PWM
108 116
 
109 117
 /**
110
- * Apply power for laser/spindle
118
+ * Apply power for Laser or Spindle
111 119
  *
112 120
  * Apply cutter power value for PWM, Servo, and on/off pin.
113 121
  *
114
- * @param opwr Power value. Range 0 to MAX. When 0 disable spindle/laser.
122
+ * @param opwr Power value. Range 0 to MAX.
115 123
  */
116 124
 void SpindleLaser::apply_power(const uint8_t opwr) {
117
-  if (opwr == last_power_applied) return;
118
-  last_power_applied = opwr;
119
-  power = opwr;
120
-  #if ENABLED(SPINDLE_LASER_USE_PWM)
121
-    if (cutter.unitPower == 0 && CUTTER_UNIT_IS(RPM)) {
122
-      ocr_off();
123
-      isReady = false;
124
-    }
125
-    else if (ENABLED(CUTTER_POWER_RELATIVE) || enabled()) {
126
-      set_ocr(power);
127
-      isReady = true;
128
-    }
129
-    else {
130
-      ocr_off();
131
-      isReady = false;
132
-    }
133
-  #elif ENABLED(SPINDLE_SERVO)
134
-    servo[SPINDLE_SERVO_NR].move(power);
135
-  #else
136
-    WRITE(SPINDLE_LASER_ENA_PIN, enabled() ? SPINDLE_LASER_ACTIVE_STATE : !SPINDLE_LASER_ACTIVE_STATE);
137
-    isReady = true;
138
-  #endif
125
+  if (enabled() || opwr == 0) {                                   // 0 check allows us to disable where no ENA pin exists
126
+    // Test and set the last power used to improve performance
127
+    if (opwr == last_power_applied) return;
128
+    last_power_applied = opwr;
129
+    // Handle PWM driven or just simple on/off
130
+    #if ENABLED(SPINDLE_LASER_USE_PWM)
131
+      if (CUTTER_UNIT_IS(RPM) && unitPower == 0)
132
+        ocr_off();
133
+      else if (ENABLED(CUTTER_POWER_RELATIVE) || enabled() || opwr == 0) {
134
+        set_ocr(opwr);
135
+        isReadyForUI = true;
136
+      }
137
+      else
138
+        ocr_off();
139
+    #elif ENABLED(SPINDLE_SERVO)
140
+      MOVE_SERVO(SPINDLE_SERVO_NR, power);
141
+    #else
142
+      WRITE(SPINDLE_LASER_ENA_PIN, enabled() ? SPINDLE_LASER_ACTIVE_STATE : !SPINDLE_LASER_ACTIVE_STATE);
143
+      isReadyForUI = true;
144
+    #endif
145
+  }
146
+  else {
147
+    #if PIN_EXISTS(SPINDLE_LASER_ENA)
148
+      WRITE(SPINDLE_LASER_ENA_PIN, !SPINDLE_LASER_ACTIVE_STATE);
149
+    #endif
150
+    isReadyForUI = false; // Only used for UI display updates.
151
+    TERN_(SPINDLE_LASER_USE_PWM, ocr_off());
152
+  }
139 153
 }
140 154
 
141 155
 #if ENABLED(SPINDLE_CHANGE_DIR)

+ 159
- 174
Marlin/src/feature/spindle_laser.h View File

@@ -34,85 +34,98 @@
34 34
   #include "../libs/buzzer.h"
35 35
 #endif
36 36
 
37
-#if ENABLED(LASER_POWER_INLINE)
38
-  #include "../module/planner.h"
39
-#endif
37
+// Inline laser power
38
+#include "../module/planner.h"
40 39
 
41 40
 #define PCT_TO_PWM(X) ((X) * 255 / 100)
42 41
 #define PCT_TO_SERVO(X) ((X) * 180 / 100)
43 42
 
43
+
44
+// Laser/Cutter operation mode
45
+enum CutterMode : int8_t {
46
+  CUTTER_MODE_ERROR = -1,
47
+  CUTTER_MODE_STANDARD,     // M3 power is applied directly and waits for planner moves to sync.
48
+  CUTTER_MODE_CONTINUOUS,   // M3 or G1/2/3 move power is controlled within planner blocks, set with 'M3 I', cleared with 'M5 I'.
49
+  CUTTER_MODE_DYNAMIC       // M4 laser power is proportional to the feed rate, set with 'M4 I', cleared with 'M5 I'.
50
+};
51
+
44 52
 class SpindleLaser {
45 53
 public:
46
-  static const inline uint8_t pct_to_ocr(const_float_t pct) { return uint8_t(PCT_TO_PWM(pct)); }
54
+  static CutterMode cutter_mode;
47 55
 
48
-  // cpower = configured values (e.g., SPEED_POWER_MAX)
56
+  static constexpr uint8_t pct_to_ocr(const_float_t pct) { return uint8_t(PCT_TO_PWM(pct)); }
49 57
 
58
+  // cpower = configured values (e.g., SPEED_POWER_MAX)
50 59
   // Convert configured power range to a percentage
51
-  static const inline uint8_t cpwr_to_pct(const cutter_cpower_t cpwr) {
52
-    constexpr cutter_cpower_t power_floor = TERN(CUTTER_POWER_RELATIVE, SPEED_POWER_MIN, 0),
53
-                              power_range = SPEED_POWER_MAX - power_floor;
54
-    return cpwr ? round(100.0f * (cpwr - power_floor) / power_range) : 0;
60
+  static constexpr cutter_cpower_t power_floor = TERN(CUTTER_POWER_RELATIVE, SPEED_POWER_MIN, 0);
61
+  static constexpr uint8_t cpwr_to_pct(const cutter_cpower_t cpwr) {
62
+    return cpwr ? round(100.0f * (cpwr - power_floor) / (SPEED_POWER_MAX - power_floor)) : 0;
55 63
   }
56 64
 
57
-  // Convert a cpower (e.g., SPEED_POWER_STARTUP) to unit power (upwr, upower),
58
-  // which can be PWM, Percent, Servo angle, or RPM (rel/abs).
59
-  static const inline cutter_power_t cpwr_to_upwr(const cutter_cpower_t cpwr) { // STARTUP power to Unit power
60
-    const cutter_power_t upwr = (
65
+  // Convert config defines from RPM to %, angle or PWM when in Spindle mode
66
+  // and convert from PERCENT to PWM when in Laser mode
67
+  static constexpr cutter_power_t cpwr_to_upwr(const cutter_cpower_t cpwr) { // STARTUP power to Unit power
68
+    return (
61 69
       #if ENABLED(SPINDLE_FEATURE)
62
-        // Spindle configured values are in RPM
70
+        // Spindle configured define values are in RPM
63 71
         #if CUTTER_UNIT_IS(RPM)
64
-          cpwr                            // to RPM
65
-        #elif CUTTER_UNIT_IS(PERCENT)     // to PCT
66
-          cpwr_to_pct(cpwr)
67
-        #elif CUTTER_UNIT_IS(SERVO)       // to SERVO angle
68
-          PCT_TO_SERVO(cpwr_to_pct(cpwr))
69
-        #else                             // to PWM
70
-          PCT_TO_PWM(cpwr_to_pct(cpwr))
72
+          cpwr                            // to same
73
+        #elif CUTTER_UNIT_IS(PERCENT)
74
+          cpwr_to_pct(cpwr)               // to Percent
75
+        #elif CUTTER_UNIT_IS(SERVO)
76
+          PCT_TO_SERVO(cpwr_to_pct(cpwr)) // to SERVO angle
77
+        #else
78
+          PCT_TO_PWM(cpwr_to_pct(cpwr))   // to PWM
71 79
         #endif
72 80
       #else
73
-        // Laser configured values are in PCT
81
+        // Laser configured define values are in Percent
74 82
         #if CUTTER_UNIT_IS(PWM255)
75
-          PCT_TO_PWM(cpwr)
83
+          PCT_TO_PWM(cpwr)                // to PWM
76 84
         #else
77
-          cpwr                            // to RPM/PCT
85
+          cpwr                            // to same
78 86
         #endif
79 87
       #endif
80 88
     );
81
-    return upwr;
82 89
   }
83 90
 
84
-  static const cutter_power_t mpower_min() { return cpwr_to_upwr(SPEED_POWER_MIN); }
85
-  static const cutter_power_t mpower_max() { return cpwr_to_upwr(SPEED_POWER_MAX); }
91
+  static constexpr cutter_power_t mpower_min() { return cpwr_to_upwr(SPEED_POWER_MIN); }
92
+  static constexpr cutter_power_t mpower_max() { return cpwr_to_upwr(SPEED_POWER_MAX); }
86 93
 
87 94
   #if ENABLED(LASER_FEATURE)
88
-    static cutter_test_pulse_t testPulse; // Test fire Pulse ms value
95
+    static cutter_test_pulse_t testPulse;                 // (ms) Test fire pulse duration
96
+    static uint8_t last_block_power;                      // Track power changes for dynamic power
97
+
98
+    static feedRate_t feedrate_mm_m, last_feedrate_mm_m;  // (mm/min) Track feedrate changes for dynamic power
99
+    static bool laser_feedrate_changed() {
100
+      const bool changed = last_feedrate_mm_m != feedrate_mm_m;
101
+      if (changed) last_feedrate_mm_m = feedrate_mm_m;
102
+      return changed;
103
+    }
89 104
   #endif
90 105
 
91
-  static bool isReady;                    // Ready to apply power setting from the UI to OCR
106
+  static bool isReadyForUI;               // Ready to apply power setting from the UI to OCR
107
+  static bool enable_state;
92 108
   static uint8_t power,
93 109
                  last_power_applied;      // Basic power state tracking
94 110
 
95
-  #if ENABLED(MARLIN_DEV_MODE)
96
-    static cutter_frequency_t frequency;  // Set PWM frequency; range: 2K-50K
97
-  #endif
111
+  static cutter_frequency_t frequency;  // Set PWM frequency; range: 2K-50K
98 112
 
99 113
   static cutter_power_t menuPower,        // Power as set via LCD menu in PWM, Percentage or RPM
100 114
                         unitPower;        // Power as displayed status in PWM, Percentage or RPM
101 115
 
102 116
   static void init();
103 117
 
104
-  #if ENABLED(MARLIN_DEV_MODE)
118
+  #if ENABLED(HAL_CAN_SET_PWM_FREQ) && SPINDLE_LASER_FREQUENCY
105 119
     static void refresh_frequency() { hal.set_pwm_frequency(pin_t(SPINDLE_LASER_PWM_PIN), frequency); }
106 120
   #endif
107 121
 
108 122
   // Modifying this function should update everywhere
109 123
   static bool enabled(const cutter_power_t opwr) { return opwr > 0; }
110
-  static bool enabled() { return enabled(power); }
124
+  static bool enabled() { return enable_state; }
111 125
 
112 126
   static void apply_power(const uint8_t inpow);
113 127
 
114 128
   FORCE_INLINE static void refresh() { apply_power(power); }
115
-  FORCE_INLINE static void set_power(const uint8_t upwr) { power = upwr; refresh(); }
116 129
 
117 130
   #if ENABLED(SPINDLE_LASER_USE_PWM)
118 131
 
@@ -123,7 +136,6 @@ public:
123 136
     public:
124 137
 
125 138
     static void set_ocr(const uint8_t ocr);
126
-    static void ocr_set_power(const uint8_t ocr) { power = ocr; set_ocr(ocr); }
127 139
     static void ocr_off();
128 140
 
129 141
     /**
@@ -141,78 +153,76 @@ public:
141 153
       );
142 154
     }
143 155
 
144
-    /**
145
-     * Correct power to configured range
146
-     */
147
-    static cutter_power_t power_to_range(const cutter_power_t pwr) {
148
-      return power_to_range(pwr, _CUTTER_POWER(CUTTER_POWER_UNIT));
149
-    }
150
-
151
-    static cutter_power_t power_to_range(const cutter_power_t pwr, const uint8_t pwrUnit) {
152
-      static constexpr float
153
-        min_pct = TERN(CUTTER_POWER_RELATIVE, 0, TERN(SPINDLE_FEATURE, round(100.0f * (SPEED_POWER_MIN) / (SPEED_POWER_MAX)), SPEED_POWER_MIN)),
154
-        max_pct = TERN(SPINDLE_FEATURE, 100, SPEED_POWER_MAX);
155
-      if (pwr <= 0) return 0;
156
-      cutter_power_t upwr;
157
-      switch (pwrUnit) {
158
-        case _CUTTER_POWER_PWM255:
159
-          upwr = cutter_power_t(
160
-              (pwr < pct_to_ocr(min_pct)) ? pct_to_ocr(min_pct) // Use minimum if set below
161
-            : (pwr > pct_to_ocr(max_pct)) ? pct_to_ocr(max_pct) // Use maximum if set above
162
-            :  pwr
163
-          );
164
-          break;
165
-        case _CUTTER_POWER_PERCENT:
166
-          upwr = cutter_power_t(
167
-              (pwr < min_pct) ? min_pct                         // Use minimum if set below
168
-            : (pwr > max_pct) ? max_pct                         // Use maximum if set above
169
-            :  pwr                                              // PCT
170
-          );
171
-          break;
172
-        case _CUTTER_POWER_RPM:
173
-          upwr = cutter_power_t(
174
-              (pwr < SPEED_POWER_MIN) ? SPEED_POWER_MIN         // Use minimum if set below
175
-            : (pwr > SPEED_POWER_MAX) ? SPEED_POWER_MAX         // Use maximum if set above
176
-            : pwr                                               // Calculate OCR value
177
-          );
178
-          break;
179
-        default: break;
180
-      }
181
-      return upwr;
182
-    }
183
-
184 156
   #endif // SPINDLE_LASER_USE_PWM
185 157
 
186 158
   /**
187
-   * Enable/Disable spindle/laser
188
-   * @param enable true = enable; false = disable
159
+   * Correct power to configured range
189 160
    */
190
-  static void set_enabled(const bool enable) {
191
-    uint8_t value = 0;
192
-    if (enable) {
193
-      #if ENABLED(SPINDLE_LASER_USE_PWM)
194
-        if (power)
195
-          value = power;
196
-        else if (unitPower)
197
-          value = upower_to_ocr(cpwr_to_upwr(SPEED_POWER_STARTUP));
198
-      #else
199
-        value = 255;
200
-      #endif
161
+  static cutter_power_t power_to_range(const cutter_power_t pwr, const uint8_t pwrUnit=_CUTTER_POWER(CUTTER_POWER_UNIT)) {
162
+    static constexpr float
163
+      min_pct = TERN(CUTTER_POWER_RELATIVE, 0, TERN(SPINDLE_FEATURE, round(100.0f * (SPEED_POWER_MIN) / (SPEED_POWER_MAX)), SPEED_POWER_MIN)),
164
+      max_pct = TERN(SPINDLE_FEATURE, 100, SPEED_POWER_MAX);
165
+    if (pwr <= 0) return 0;
166
+    cutter_power_t upwr;
167
+    switch (pwrUnit) {
168
+      case _CUTTER_POWER_PWM255: {  // PWM
169
+        const uint8_t pmin = pct_to_ocr(min_pct), pmax = pct_to_ocr(max_pct);
170
+        upwr = cutter_power_t(constrain(pwr, pmin, pmax));
171
+      } break;
172
+      case _CUTTER_POWER_PERCENT:   // Percent
173
+        upwr = cutter_power_t(constrain(pwr, min_pct, max_pct));
174
+        break;
175
+      case _CUTTER_POWER_RPM:       // Calculate OCR value
176
+        upwr = cutter_power_t(constrain(pwr, SPEED_POWER_MIN, SPEED_POWER_MAX));
177
+        break;
178
+      default: break;
201 179
     }
202
-    set_power(value);
180
+    return upwr;
203 181
   }
204 182
 
205
-  static void disable() { isReady = false; set_enabled(false); }
206
-
207 183
   /**
208
-   * Wait for spindle to spin up or spin down
184
+   * Enable Laser or Spindle output.
185
+   * It's important to prevent changing the power output value during inline cutter operation.
186
+   * Inline power is adjusted in the planner to support LASER_TRAP_POWER and CUTTER_MODE_DYNAMIC mode.
187
+   *
188
+   * This method accepts one of the following control states:
189
+   *
190
+   *  - For CUTTER_MODE_STANDARD the cutter power is either full on/off or ocr-based and it will apply
191
+   *    SPEED_POWER_STARTUP if no value is assigned.
209 192
    *
210
-   * @param on true = state to on; false = state to off.
193
+   *  - For CUTTER_MODE_CONTINUOUS inline and power remains where last set and the cutter output enable flag is set.
194
+   *
195
+   *  - CUTTER_MODE_DYNAMIC is also inline-based and it just sets the enable output flag.
196
+   *
197
+   *  - For CUTTER_MODE_ERROR set the output enable_state flag directly and set power to 0 for any mode.
198
+   *    This mode allows a global power shutdown action to occur.
211 199
    */
212
-  static void power_delay(const bool on) {
213
-    #if DISABLED(LASER_POWER_INLINE)
214
-      safe_delay(on ? SPINDLE_LASER_POWERUP_DELAY : SPINDLE_LASER_POWERDOWN_DELAY);
200
+  static void set_enabled(const bool enable) {
201
+    switch (cutter_mode) {
202
+      case CUTTER_MODE_STANDARD:
203
+        apply_power(enable ? TERN(SPINDLE_LASER_USE_PWM, (power ?: (unitPower ? upower_to_ocr(cpwr_to_upwr(SPEED_POWER_STARTUP)) : 0)), 255) : 0);
204
+        break;
205
+      case CUTTER_MODE_CONTINUOUS:
206
+        TERN_(LASER_FEATURE, set_inline_enabled(enable));
207
+        break;
208
+      case CUTTER_MODE_DYNAMIC:
209
+        TERN_(LASER_FEATURE, set_inline_enabled(enable));
210
+        break;
211
+      case CUTTER_MODE_ERROR: // Error mode, no enable and kill power.
212
+        enable_state = false;
213
+        apply_power(0);
214
+    }
215
+    #if SPINDLE_LASER_ENA_PIN
216
+      WRITE(SPINDLE_LASER_ENA_PIN, enable ? SPINDLE_LASER_ACTIVE_STATE : !SPINDLE_LASER_ACTIVE_STATE);
215 217
     #endif
218
+    enable_state = enable;
219
+  }
220
+
221
+  static void disable() { isReadyForUI = false; set_enabled(false); }
222
+
223
+  // Wait for spindle/laser to startup or shutdown
224
+  static void power_delay(const bool on) {
225
+    safe_delay(on ? SPINDLE_LASER_POWERUP_DELAY : SPINDLE_LASER_POWERDOWN_DELAY);
216 226
   }
217 227
 
218 228
   #if ENABLED(SPINDLE_CHANGE_DIR)
@@ -224,47 +234,60 @@ public:
224 234
   #endif
225 235
 
226 236
   #if ENABLED(AIR_EVACUATION)
227
-    static void air_evac_enable();         // Turn On Cutter Vacuum or Laser Blower motor
228
-    static void air_evac_disable();        // Turn Off Cutter Vacuum or Laser Blower motor
229
-    static void air_evac_toggle();         // Toggle Cutter Vacuum or Laser Blower motor
230
-    static bool air_evac_state() {  // Get current state
237
+    static void air_evac_enable();     // Turn On Cutter Vacuum or Laser Blower motor
238
+    static void air_evac_disable();    // Turn Off Cutter Vacuum or Laser Blower motor
239
+    static void air_evac_toggle();     // Toggle Cutter Vacuum or Laser Blower motor
240
+    static bool air_evac_state() {     // Get current state
231 241
       return (READ(AIR_EVACUATION_PIN) == AIR_EVACUATION_ACTIVE);
232 242
     }
233 243
   #endif
234 244
 
235 245
   #if ENABLED(AIR_ASSIST)
236
-    static void air_assist_enable();         // Turn on air assist
237
-    static void air_assist_disable();        // Turn off air assist
238
-    static void air_assist_toggle();         // Toggle air assist
239
-    static bool air_assist_state() {  // Get current state
246
+    static void air_assist_enable();   // Turn on air assist
247
+    static void air_assist_disable();  // Turn off air assist
248
+    static void air_assist_toggle();   // Toggle air assist
249
+    static bool air_assist_state() {   // Get current state
240 250
       return (READ(AIR_ASSIST_PIN) == AIR_ASSIST_ACTIVE);
241 251
     }
242 252
   #endif
243 253
 
244 254
   #if HAS_MARLINUI_MENU
245
-    static void enable_with_dir(const bool reverse) {
246
-      isReady = true;
247
-      const uint8_t ocr = TERN(SPINDLE_LASER_USE_PWM, upower_to_ocr(menuPower), 255);
248
-      if (menuPower)
249
-        power = ocr;
250
-      else
251
-        menuPower = cpwr_to_upwr(SPEED_POWER_STARTUP);
252
-      unitPower = menuPower;
253
-      set_reverse(reverse);
254
-      set_enabled(true);
255
-    }
256
-    FORCE_INLINE static void enable_forward() { enable_with_dir(false); }
257
-    FORCE_INLINE static void enable_reverse() { enable_with_dir(true); }
258
-    FORCE_INLINE static void enable_same_dir() { enable_with_dir(is_reverse()); }
255
+
256
+    #if ENABLED(SPINDLE_FEATURE)
257
+      static void enable_with_dir(const bool reverse) {
258
+        isReadyForUI = true;
259
+        const uint8_t ocr = TERN(SPINDLE_LASER_USE_PWM, upower_to_ocr(menuPower), 255);
260
+        if (menuPower)
261
+          power = ocr;
262
+        else
263
+          menuPower = cpwr_to_upwr(SPEED_POWER_STARTUP);
264
+        unitPower = menuPower;
265
+        set_reverse(reverse);
266
+        set_enabled(true);
267
+      }
268
+      FORCE_INLINE static void enable_forward() { enable_with_dir(false); }
269
+      FORCE_INLINE static void enable_reverse() { enable_with_dir(true); }
270
+      FORCE_INLINE static void enable_same_dir() { enable_with_dir(is_reverse()); }
271
+    #endif // SPINDLE_FEATURE
259 272
 
260 273
     #if ENABLED(SPINDLE_LASER_USE_PWM)
261 274
       static void update_from_mpower() {
262
-        if (isReady) power = upower_to_ocr(menuPower);
275
+        if (isReadyForUI) power = upower_to_ocr(menuPower);
263 276
         unitPower = menuPower;
264 277
       }
265 278
     #endif
266 279
 
267 280
     #if ENABLED(LASER_FEATURE)
281
+      // Toggle the laser on/off with menuPower. Apply SPEED_POWER_STARTUP if it was 0 on entry.
282
+      static void laser_menu_toggle(const bool state) {
283
+        set_enabled(state);
284
+        if (state) {
285
+          if (!menuPower) menuPower = cpwr_to_upwr(SPEED_POWER_STARTUP);
286
+          power = upower_to_ocr(menuPower);
287
+          apply_power(power);
288
+        }
289
+      }
290
+
268 291
       /**
269 292
        * Test fire the laser using the testPulse ms duration
270 293
        * Also fires with any PWM power that was previous set
@@ -272,74 +295,36 @@ public:
272 295
        */
273 296
       static void test_fire_pulse() {
274 297
         TERN_(HAS_BEEPER, buzzer.tone(30, 3000));
275
-        enable_forward();                  // Turn Laser on (Spindle speak but same funct)
298
+        cutter_mode = CUTTER_MODE_STANDARD;// Menu needs standard mode.
299
+        laser_menu_toggle(true);           // Laser On
276 300
         delay(testPulse);                  // Delay for time set by user in pulse ms menu screen.
277
-        disable();                         // Turn laser off
301
+        laser_menu_toggle(false);          // Laser Off
278 302
       }
279
-    #endif
303
+    #endif // LASER_FEATURE
280 304
 
281 305
   #endif // HAS_MARLINUI_MENU
282 306
 
283
-  #if ENABLED(LASER_POWER_INLINE)
284
-    /**
285
-     * Inline power adds extra fields to the planner block
286
-     * to handle laser power and scale to movement speed.
287
-     */
307
+  #if ENABLED(LASER_FEATURE)
288 308
 
289
-    // Force disengage planner power control
290
-    static void inline_disable() {
291
-      isReady = false;
292
-      unitPower = 0;
293
-      planner.laser_inline.status.isPlanned = false;
294
-      planner.laser_inline.status.isEnabled = false;
295
-      planner.laser_inline.power = 0;
309
+    // Dynamic mode rate calculation
310
+    static uint8_t calc_dynamic_power() {
311
+      if (feedrate_mm_m > 65535) return 255;         // Too fast, go always on
312
+      uint16_t rate = uint16_t(feedrate_mm_m);       // 16 bits from the G-code parser float input
313
+      rate >>= 8;                                    // Take the G-code input e.g. F40000 and shift off the lower bits to get an OCR value from 1-255
314
+      return uint8_t(rate);
296 315
     }
297 316
 
298 317
     // Inline modes of all other functions; all enable planner inline power control
299
-    static void set_inline_enabled(const bool enable) {
300
-      if (enable)
301
-        inline_power(255);
302
-      else {
303
-        isReady = false;
304
-        unitPower = menuPower = 0;
305
-        planner.laser_inline.status.isPlanned = false;
306
-        TERN(SPINDLE_LASER_USE_PWM, inline_ocr_power, inline_power)(0);
307
-      }
308
-    }
318
+    static void set_inline_enabled(const bool enable) { planner.laser_inline.status.isEnabled = enable;}
309 319
 
310 320
     // Set the power for subsequent movement blocks
311
-    static void inline_power(const cutter_power_t upwr) {
312
-      unitPower = menuPower = upwr;
313
-      #if ENABLED(SPINDLE_LASER_USE_PWM)
314
-        #if ENABLED(SPEED_POWER_RELATIVE) && !CUTTER_UNIT_IS(RPM) // relative mode does not turn laser off at 0, except for RPM
315
-          planner.laser_inline.status.isEnabled = true;
316
-          planner.laser_inline.power = upower_to_ocr(upwr);
317
-          isReady = true;
318
-        #else
319
-          inline_ocr_power(upower_to_ocr(upwr));
320
-        #endif
321
-      #else
322
-        planner.laser_inline.status.isEnabled = enabled(upwr);
323
-        planner.laser_inline.power = upwr;
324
-        isReady = enabled(upwr);
325
-      #endif
321
+    static void inline_power(const cutter_power_t cpwr) {
322
+      TERN(SPINDLE_LASER_USE_PWM, power = planner.laser_inline.power = cpwr, planner.laser_inline.power = cpwr > 0 ? 255 : 0);
326 323
     }
327 324
 
328
-    static void inline_direction(const bool) { /* never */ }
329
-
330
-    #if ENABLED(SPINDLE_LASER_USE_PWM)
331
-      static void inline_ocr_power(const uint8_t ocrpwr) {
332
-        isReady = ocrpwr > 0;
333
-        planner.laser_inline.status.isEnabled = ocrpwr > 0;
334
-        planner.laser_inline.power = ocrpwr;
335
-      }
336
-    #endif
337
-  #endif // LASER_POWER_INLINE
325
+  #endif // LASER_FEATURE
338 326
 
339
-  static void kill() {
340
-    TERN_(LASER_POWER_INLINE, inline_disable());
341
-    disable();
342
-  }
327
+  static void kill() { disable(); }
343 328
 };
344 329
 
345 330
 extern SpindleLaser cutter;

+ 2
- 4
Marlin/src/feature/spindle_laser_types.h View File

@@ -74,12 +74,10 @@ typedef IF<(SPEED_POWER_MAX > 255), uint16_t, uint8_t>::type cutter_cpower_t;
74 74
   #endif
75 75
 #endif
76 76
 
77
+typedef uint16_t cutter_frequency_t;
78
+
77 79
 #if ENABLED(LASER_FEATURE)
78 80
   typedef uint16_t cutter_test_pulse_t;
79 81
   #define CUTTER_MENU_PULSE_TYPE uint16_3
80
-#endif
81
-
82
-#if ENABLED(MARLIN_DEV_MODE)
83
-  typedef uint16_t cutter_frequency_t;
84 82
   #define CUTTER_MENU_FREQUENCY_TYPE uint16_5
85 83
 #endif

+ 7
- 2
Marlin/src/gcode/calibrate/G28.cpp View File

@@ -59,7 +59,7 @@
59 59
   #include "../../libs/L64XX/L64XX_Marlin.h"
60 60
 #endif
61 61
 
62
-#if ENABLED(LASER_MOVE_G28_OFF)
62
+#if ENABLED(LASER_FEATURE)
63 63
   #include "../../feature/spindle_laser.h"
64 64
 #endif
65 65
 
@@ -205,7 +205,12 @@ void GcodeSuite::G28() {
205 205
   DEBUG_SECTION(log_G28, "G28", DEBUGGING(LEVELING));
206 206
   if (DEBUGGING(LEVELING)) log_machine_info();
207 207
 
208
-  TERN_(LASER_MOVE_G28_OFF, cutter.set_inline_enabled(false));  // turn off laser
208
+  /*
209
+   * Set the laser power to false to stop the planner from processing the current power setting.
210
+   */
211
+  #if ENABLED(LASER_FEATURE)
212
+    planner.laser_inline.status.isPowered = false;
213
+  #endif
209 214
 
210 215
   #if ENABLED(DUAL_X_CARRIAGE)
211 216
     bool IDEX_saved_duplication_state = extruder_duplication_enabled;

+ 72
- 62
Marlin/src/gcode/control/M3-M5.cpp View File

@@ -31,17 +31,27 @@
31 31
 /**
32 32
  * Laser:
33 33
  *  M3 - Laser ON/Power (Ramped power)
34
- *  M4 - Laser ON/Power (Continuous power)
34
+ *  M4 - Laser ON/Power (Ramped power)
35
+ *  M5 - Set power output to 0 (leaving inline mode unchanged).
36
+ *
37
+ *  M3I - Enable continuous inline power to be processed by the planner, with power
38
+ *        calculated and set in the planner blocks, processed inline during stepping.
39
+ *        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.
40
+ *        M3I must be set before using planner-synced M3 inline S-Values (LASER_POWER_SYNC).
41
+ *
42
+ *  M4I - Set dynamic mode which calculates laser power OCR based on the current feedrate.
43
+ *
44
+ *  M5I - Clear inline mode and set power to 0.
35 45
  *
36 46
  * Spindle:
37 47
  *  M3 - Spindle ON (Clockwise)
38 48
  *  M4 - Spindle ON (Counter-clockwise)
49
+ *  M5 - Spindle OFF
39 50
  *
40 51
  * Parameters:
41
- *  S<power> - Set power. S0 will turn the spindle/laser off, except in relative mode.
42
- *  O<ocr>   - Set power and OCR (oscillator count register)
52
+ *  S<power> - Set power. S0 will turn the spindle/laser off.
43 53
  *
44
- *  If no PWM pin is defined then M3/M4 just turns it on.
54
+ *  If no PWM pin is defined then M3/M4 just turns it on or off.
45 55
  *
46 56
  *  At least 12.8kHz (50Hz * 256) is needed for Spindle PWM.
47 57
  *  Hardware PWM is required on AVR. ISRs are too slow.
@@ -70,77 +80,77 @@ void GcodeSuite::M3_M4(const bool is_M4) {
70 80
     reset_stepper_timeout(); // Reset timeout to allow subsequent G-code to power the laser (imm.)
71 81
   #endif
72 82
 
73
-  #if EITHER(SPINDLE_LASER_USE_PWM, SPINDLE_SERVO)
74
-    auto get_s_power = [] {
75
-      if (parser.seenval('S')) {
76
-        const float spwr = parser.value_float();
77
-        #if ENABLED(SPINDLE_SERVO)
78
-          cutter.unitPower = spwr;
79
-        #else
80
-          cutter.unitPower = TERN(SPINDLE_LASER_USE_PWM,
81
-                                cutter.power_to_range(cutter_power_t(round(spwr))),
82
-                                spwr > 0 ? 255 : 0);
83
-        #endif
84
-      }
85
-      else
86
-        cutter.unitPower = cutter.cpwr_to_upwr(SPEED_POWER_STARTUP);
87
-      return cutter.unitPower;
88
-    };
89
-  #endif
83
+  if (cutter.cutter_mode == CUTTER_MODE_STANDARD)
84
+    planner.synchronize();   // Wait for previous movement commands (G0/G1/G2/G3) to complete before changing power
90 85
 
91
-  #if ENABLED(LASER_POWER_INLINE)
92
-    if (parser.seen('I') == DISABLED(LASER_POWER_INLINE_INVERT)) {
93
-      // Laser power in inline mode
94
-      cutter.inline_direction(is_M4); // Should always be unused
95
-      #if ENABLED(SPINDLE_LASER_USE_PWM)
96
-        if (parser.seenval('O')) {
97
-          cutter.unitPower = cutter.power_to_range(parser.value_byte(), 0);
98
-          cutter.inline_ocr_power(cutter.unitPower); // The OCR is a value from 0 to 255 (uint8_t)
99
-        }
100
-        else
101
-          cutter.inline_power(cutter.upower_to_ocr(get_s_power()));
102
-      #else
103
-        cutter.set_inline_enabled(true);
104
-      #endif
105
-      return;
86
+  #if ENABLED(LASER_FEATURE)
87
+    if (parser.seen_test('I')) {
88
+      cutter.cutter_mode = is_M4 ? CUTTER_MODE_DYNAMIC : CUTTER_MODE_CONTINUOUS;
89
+      cutter.inline_power(0);
90
+      cutter.set_enabled(true);
106 91
     }
107
-    // Non-inline, standard case
108
-    cutter.inline_disable(); // Prevent future blocks re-setting the power
109 92
   #endif
110 93
 
111
-  planner.synchronize();   // Wait for previous movement commands (G0/G0/G2/G3) to complete before changing power
112
-  cutter.set_reverse(is_M4);
113
-
114
-  #if ENABLED(SPINDLE_LASER_USE_PWM)
115
-    if (parser.seenval('O')) {
116
-      cutter.unitPower = cutter.power_to_range(parser.value_byte(), 0);
117
-      cutter.ocr_set_power(cutter.unitPower); // The OCR is a value from 0 to 255 (uint8_t)
94
+  auto get_s_power = [] {
95
+    float u;
96
+    if (parser.seenval('S')) {
97
+      const float v = parser.value_float();
98
+      u = TERN(LASER_POWER_TRAP, v, cutter.power_to_range(v));
118 99
     }
119
-    else
120
-      cutter.set_power(cutter.upower_to_ocr(get_s_power()));
121
-  #elif ENABLED(SPINDLE_SERVO)
122
-    cutter.set_power(get_s_power());
123
-  #else
100
+    else if (cutter.cutter_mode == CUTTER_MODE_STANDARD)
101
+      u = cutter.cpwr_to_upwr(SPEED_POWER_STARTUP);
102
+
103
+    cutter.menuPower = cutter.unitPower = u;
104
+
105
+    // PWM not implied, power converted to OCR from unit definition and on/off if not PWM.
106
+    cutter.power = TERN(SPINDLE_LASER_USE_PWM, cutter.upower_to_ocr(u), u > 0 ? 255 : 0);
107
+    return u;
108
+  };
109
+
110
+  if (cutter.cutter_mode == CUTTER_MODE_CONTINUOUS || cutter.cutter_mode == CUTTER_MODE_DYNAMIC) {  // Laser power in inline mode
111
+    #if ENABLED(LASER_FEATURE)
112
+      planner.laser_inline.status.isPowered = true;                                                 // M3 or M4 is powered either way
113
+      get_s_power();                                                                                // Update cutter.power if seen
114
+      #if ENABLED(LASER_POWER_SYNC)
115
+        // With power sync we only set power so it does not effect queued inline power sets
116
+        planner.buffer_sync_block(BLOCK_BIT_LASER_PWR);                                            // Send the flag, queueing inline power
117
+      #else
118
+        planner.synchronize();
119
+        cutter.inline_power(cutter.power);
120
+      #endif
121
+    #endif
122
+  }
123
+  else {
124 124
     cutter.set_enabled(true);
125
-  #endif
126
-  cutter.menuPower = cutter.unitPower;
125
+    get_s_power();
126
+    cutter.apply_power(
127
+      #if ENABLED(SPINDLE_SERVO)
128
+        cutter.unitPower
129
+      #elif ENABLED(SPINDLE_LASER_USE_PWM)
130
+        cutter.upower_to_ocr(cutter.unitPower)
131
+      #else
132
+        cutter.unitPower > 0 ? 255 : 0
133
+      #endif
134
+    );
135
+    TERN_(SPINDLE_CHANGE_DIR, cutter.set_reverse(is_M4));
136
+  }
127 137
 }
128 138
 
129 139
 /**
130 140
  * M5 - Cutter OFF (when moves are complete)
131 141
  */
132 142
 void GcodeSuite::M5() {
133
-  #if ENABLED(LASER_POWER_INLINE)
134
-    if (parser.seen('I') == DISABLED(LASER_POWER_INLINE_INVERT)) {
135
-      cutter.set_inline_enabled(false); // Laser power in inline mode
136
-      return;
137
-    }
138
-    // Non-inline, standard case
139
-    cutter.inline_disable(); // Prevent future blocks re-setting the power
140
-  #endif
141 143
   planner.synchronize();
142
-  cutter.set_enabled(false);
143
-  cutter.menuPower = cutter.unitPower;
144
+  cutter.power = 0;
145
+  cutter.apply_power(0);                          // M5 just kills power, leaving inline mode unchanged
146
+  if (cutter.cutter_mode != CUTTER_MODE_STANDARD) {
147
+    if (parser.seen_test('I')) {
148
+      TERN_(LASER_FEATURE, cutter.inline_power(cutter.power));
149
+      cutter.set_enabled(false);                  // Needs to happen while we are in inline mode to clear inline power.
150
+      cutter.cutter_mode = CUTTER_MODE_STANDARD;  // Switch from inline to standard mode.
151
+    }
152
+  }
153
+  cutter.set_enabled(false);                      // Disable enable output setting
144 154
 }
145 155
 
146 156
 #endif // HAS_CUTTER

+ 27
- 10
Marlin/src/gcode/gcode.cpp View File

@@ -53,7 +53,7 @@ GcodeSuite gcode;
53 53
   #include "../feature/cancel_object.h"
54 54
 #endif
55 55
 
56
-#if ENABLED(LASER_MOVE_POWER)
56
+#if ENABLED(LASER_FEATURE)
57 57
   #include "../feature/spindle_laser.h"
58 58
 #endif
59 59
 
@@ -210,8 +210,11 @@ void GcodeSuite::get_destination_from_command() {
210 210
       recovery.save();
211 211
   #endif
212 212
 
213
-  if (parser.floatval('F') > 0)
213
+  if (parser.floatval('F') > 0) {
214 214
     feedrate_mm_s = parser.value_feedrate();
215
+    // Update the cutter feed rate for use by M4 I set inline moves.
216
+    TERN_(LASER_FEATURE, cutter.feedrate_mm_m = MMS_TO_MMM(feedrate_mm_s));
217
+  }
215 218
 
216 219
   #if BOTH(PRINTCOUNTER, HAS_EXTRUDERS)
217 220
     if (!DEBUGGING(DRYRUN) && !skip_move)
@@ -223,15 +226,29 @@ void GcodeSuite::get_destination_from_command() {
223 226
     M165();
224 227
   #endif
225 228
 
226
-  #if ENABLED(LASER_MOVE_POWER)
227
-    // Set the laser power in the planner to configure this move
228
-    if (parser.seen('S')) {
229
-      const float spwr = parser.value_float();
230
-      cutter.inline_power(TERN(SPINDLE_LASER_USE_PWM, cutter.power_to_range(cutter_power_t(round(spwr))), spwr > 0 ? 255 : 0));
229
+  #if ENABLED(LASER_FEATURE)
230
+    if (cutter.cutter_mode == CUTTER_MODE_CONTINUOUS || cutter.cutter_mode == CUTTER_MODE_DYNAMIC) {
231
+      // Set the cutter power in the planner to configure this move
232
+      cutter.last_feedrate_mm_m = 0;
233
+      if (WITHIN(parser.codenum, 1, TERN(ARC_SUPPORT, 3, 1)) || TERN0(BEZIER_CURVE_SUPPORT, parser.codenum == 5)) {
234
+        planner.laser_inline.status.isPowered = true;
235
+        if (parser.seen('I')) cutter.set_enabled(true);       // This is set for backward LightBurn compatibility.
236
+        if (parser.seen('S')) {
237
+          const float v = parser.value_float(),
238
+                      u = TERN(LASER_POWER_TRAP, v, cutter.power_to_range(v));
239
+          cutter.menuPower = cutter.unitPower = u;
240
+          cutter.inline_power(TERN(SPINDLE_LASER_USE_PWM, cutter.upower_to_ocr(u), u > 0 ? 255 : 0));
241
+        }
242
+      }
243
+      else if (parser.codenum == 0) {
244
+        // For dynamic mode we need to flag isPowered off, dynamic power is calculated in the stepper based on feedrate.
245
+        if (cutter.cutter_mode == CUTTER_MODE_DYNAMIC) planner.laser_inline.status.isPowered = false;
246
+        cutter.inline_power(0); // This is planner-based so only set power and do not disable inline control flags.
247
+      }
231 248
     }
232
-    else if (ENABLED(LASER_MOVE_G0_OFF) && parser.codenum == 0) // G0
233
-      cutter.set_inline_enabled(false);
234
-  #endif
249
+    else if (parser.codenum == 0)
250
+      cutter.apply_power(0);
251
+  #endif // LASER_FEATURE
235 252
 }
236 253
 
237 254
 /**

+ 23
- 22
Marlin/src/inc/SanityCheck.h View File

@@ -447,6 +447,16 @@
447 447
   #error "SPINDLE_LASER_ACTIVE_HIGH is now SPINDLE_LASER_ACTIVE_STATE."
448 448
 #elif defined(SPINDLE_LASER_ENABLE_INVERT)
449 449
   #error "SPINDLE_LASER_ENABLE_INVERT is now SPINDLE_LASER_ACTIVE_STATE."
450
+#elif defined(LASER_POWER_INLINE)
451
+  #error "LASER_POWER_INLINE is not required, inline mode is enabled with 'M3 I' and disabled with 'M5 I'."
452
+#elif defined(LASER_POWER_INLINE_TRAPEZOID)
453
+  #error "LASER_POWER_INLINE_TRAPEZOID is now LASER_POWER_TRAP."
454
+#elif defined(LASER_POWER_INLINE_TRAPEZOID_CONT)
455
+  #error "LASER_POWER_INLINE_TRAPEZOID_CONT is replaced with LASER_POWER_TRAP."
456
+#elif defined(LASER_POWER_INLINE_TRAPEZOID_PER)
457
+  #error "LASER_POWER_INLINE_TRAPEZOID_CONT_PER  replaced with LASER_POWER_TRAP."
458
+#elif defined(LASER_POWER_INLINE_CONTINUOUS)
459
+  #error "LASER_POWER_INLINE_CONTINUOUS is not required, inline mode is enabled with 'M3 I' and disabled with 'M5 I'."
450 460
 #elif defined(CUTTER_POWER_DISPLAY)
451 461
   #error "CUTTER_POWER_DISPLAY is now CUTTER_POWER_UNIT."
452 462
 #elif defined(CHAMBER_HEATER_PIN)
@@ -595,6 +605,8 @@
595 605
   #error "ARC_SUPPORT no longer uses ARC_SEGMENTS_PER_R."
596 606
 #elif ENABLED(ARC_SUPPORT) && (!defined(MIN_ARC_SEGMENT_MM) || !defined(MAX_ARC_SEGMENT_MM))
597 607
   #error "ARC_SUPPORT now requires MIN_ARC_SEGMENT_MM and MAX_ARC_SEGMENT_MM."
608
+#elif defined(LASER_POWER_INLINE)
609
+  #error "LASER_POWER_INLINE is obsolete."
598 610
 #elif defined(SPINDLE_LASER_PWM)
599 611
   #error "SPINDLE_LASER_PWM (true) is now set with SPINDLE_LASER_USE_PWM (enabled)."
600 612
 #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.");
3841 3853
     #error "CUTTER_POWER_UNIT must be PWM255, PERCENT, RPM, or SERVO."
3842 3854
   #endif
3843 3855
 
3844
-  #if ENABLED(LASER_POWER_INLINE)
3856
+  #if ENABLED(LASER_FEATURE)
3845 3857
     #if ENABLED(SPINDLE_CHANGE_DIR)
3846
-      #error "SPINDLE_CHANGE_DIR and LASER_POWER_INLINE are incompatible."
3847
-    #elif ENABLED(LASER_MOVE_G0_OFF) && DISABLED(LASER_MOVE_POWER)
3848
-      #error "LASER_MOVE_G0_OFF requires LASER_MOVE_POWER."
3858
+      #error "SPINDLE_CHANGE_DIR and LASER_FEATURE are incompatible."
3859
+    #elif ENABLED(LASER_MOVE_G0_OFF)
3860
+      #error "LASER_MOVE_G0_OFF is no longer required, G0 and G28 cannot apply power."
3861
+    #elif ENABLED(LASER_MOVE_G28_OFF)
3862
+      #error "LASER_MOVE_G0_OFF is no longer required, G0 and G28 cannot apply power."
3863
+    #elif ENABLED(LASER_MOVE_POWER)
3864
+      #error "LASER_MOVE_POWER is no longer applicable."
3849 3865
     #endif
3850
-    #if ENABLED(LASER_POWER_INLINE_TRAPEZOID)
3866
+    #if ENABLED(LASER_POWER_TRAP)
3851 3867
       #if DISABLED(SPINDLE_LASER_USE_PWM)
3852
-        #error "LASER_POWER_INLINE_TRAPEZOID requires SPINDLE_LASER_USE_PWM to function."
3853
-      #elif ENABLED(S_CURVE_ACCELERATION)
3854
-        //#ifndef LASER_POWER_INLINE_S_CURVE_ACCELERATION_WARN
3855
-        //  #define LASER_POWER_INLINE_S_CURVE_ACCELERATION_WARN
3856
-        //  #warning "Combining LASER_POWER_INLINE_TRAPEZOID with S_CURVE_ACCELERATION may result in unintended behavior."
3857
-        //#endif
3868
+        #error "LASER_POWER_TRAP requires SPINDLE_LASER_USE_PWM to function."
3858 3869
       #endif
3859 3870
     #endif
3860
-    #if ENABLED(LASER_POWER_INLINE_INVERT)
3861
-      //#ifndef LASER_POWER_INLINE_INVERT_WARN
3862
-      //  #define LASER_POWER_INLINE_INVERT_WARN
3863
-      //  #warning "Enabling LASER_POWER_INLINE_INVERT means that `M5` won't kill the laser immediately; use `M5 I` instead."
3864
-      //#endif
3865
-    #endif
3866 3871
   #else
3867 3872
     #if SPINDLE_LASER_POWERUP_DELAY < 1
3868 3873
       #error "SPINDLE_LASER_POWERUP_DELAY must be greater than 0."
3869 3874
     #elif SPINDLE_LASER_POWERDOWN_DELAY < 1
3870 3875
       #error "SPINDLE_LASER_POWERDOWN_DELAY must be greater than 0."
3871
-    #elif ENABLED(LASER_MOVE_POWER)
3872
-      #error "LASER_MOVE_POWER requires LASER_POWER_INLINE."
3873
-    #elif ANY(LASER_POWER_INLINE_TRAPEZOID, LASER_POWER_INLINE_INVERT, LASER_MOVE_G0_OFF, LASER_MOVE_POWER)
3874
-      #error "Enabled an inline laser feature without inline laser power being enabled."
3875 3876
     #endif
3876 3877
   #endif
3877 3878
 
@@ -3889,7 +3890,7 @@ static_assert(_PLUS_TEST(4), "HOMING_FEEDRATE_MM_M values must be positive.");
3889 3890
       #error "SPINDLE_LASER_PWM_PIN not assigned to a PWM pin."
3890 3891
     #elif !defined(SPINDLE_LASER_PWM_INVERT)
3891 3892
       #error "SPINDLE_LASER_PWM_INVERT is required for (SPINDLE|LASER)_FEATURE."
3892
-    #elif !(defined(SPEED_POWER_INTERCEPT) && defined(SPEED_POWER_MIN) && defined(SPEED_POWER_MAX) && defined(SPEED_POWER_STARTUP))
3893
+    #elif !(defined(SPEED_POWER_MIN) && defined(SPEED_POWER_MAX) && defined(SPEED_POWER_STARTUP))
3893 3894
       #error "SPINDLE_LASER_USE_PWM equation constant(s) missing."
3894 3895
     #elif _PIN_CONFLICT(X_MIN)
3895 3896
       #error "SPINDLE_LASER_USE_PWM pin conflicts with X_MIN_PIN."

+ 1
- 1
Marlin/src/lcd/dogm/status_screen_DOGM.cpp View File

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

+ 5
- 0
Marlin/src/lcd/menu/menu_item.h View File

@@ -27,6 +27,10 @@
27 27
 
28 28
 #include "../../inc/MarlinConfigPre.h"
29 29
 
30
+#if ENABLED(LASER_SYNCHRONOUS_M106_M107)
31
+  #include "../../module/planner.h"
32
+#endif
33
+
30 34
 void lcd_move_z();
31 35
 
32 36
 ////////////////////////////////////////////
@@ -538,6 +542,7 @@ class MenuItem_bool : public MenuEditItemBase {
538 542
 
539 543
   inline void on_fan_update() {
540 544
     thermalManager.set_fan_speed(MenuItemBase::itemIndex, editable.uint8);
545
+    TERN_(LASER_SYNCHRONOUS_M106_M107, planner.buffer_sync_block(BLOCK_FLAG_SYNC_FANS));
541 546
   }
542 547
 
543 548
   #if ENABLED(EXTRA_FAN_SPEED)

+ 11
- 7
Marlin/src/lcd/menu/menu_spindle_laser.cpp View File

@@ -33,7 +33,7 @@
33 33
   #include "../../feature/spindle_laser.h"
34 34
 
35 35
   void menu_spindle_laser() {
36
-    bool is_enabled = cutter.enabled() && cutter.isReady;
36
+    bool is_enabled = cutter.enabled();
37 37
     #if ENABLED(SPINDLE_CHANGE_DIR)
38 38
       bool is_rev = cutter.is_reverse();
39 39
     #endif
@@ -49,7 +49,13 @@
49 49
     #endif
50 50
 
51 51
     editable.state = is_enabled;
52
-    EDIT_ITEM(bool, MSG_CUTTER(TOGGLE), &is_enabled, []{ if (editable.state) cutter.disable(); else cutter.enable_same_dir(); });
52
+    EDIT_ITEM(bool, MSG_CUTTER(TOGGLE), &is_enabled, []{
53
+      #if ENABLED(SPINDLE_FEATURE)
54
+        if (editable.state) cutter.disable(); else cutter.enable_same_dir();
55
+      #else
56
+        cutter.laser_menu_toggle(!editable.state);
57
+      #endif
58
+    });
53 59
 
54 60
     #if ENABLED(AIR_EVACUATION)
55 61
       bool evac_state = cutter.air_evac_state();
@@ -72,12 +78,10 @@
72 78
       // 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.
73 79
       EDIT_ITEM_FAST(CUTTER_MENU_PULSE_TYPE, MSG_LASER_PULSE_MS, &cutter.testPulse, LASER_TEST_PULSE_MIN, LASER_TEST_PULSE_MAX);
74 80
       ACTION_ITEM(MSG_LASER_FIRE_PULSE, cutter.test_fire_pulse);
81
+      #if ENABLED(HAL_CAN_SET_PWM_FREQ) && SPINDLE_LASER_FREQUENCY
82
+        EDIT_ITEM_FAST(CUTTER_MENU_FREQUENCY_TYPE, MSG_CUTTER_FREQUENCY, &cutter.frequency, 2000, 80000, cutter.refresh_frequency);
83
+      #endif
75 84
     #endif
76
-
77
-    #if BOTH(MARLIN_DEV_MODE, HAL_CAN_SET_PWM_FREQ) && SPINDLE_LASER_FREQUENCY
78
-      EDIT_ITEM_FAST(CUTTER_MENU_FREQUENCY_TYPE, MSG_CUTTER_FREQUENCY, &cutter.frequency, 2000, 80000, cutter.refresh_frequency);
79
-    #endif
80
-
81 85
     END_MENU();
82 86
   }
83 87
 

+ 163
- 123
Marlin/src/module/planner.cpp View File

@@ -128,8 +128,13 @@ uint8_t Planner::delay_before_delivering;       // This counter delays delivery
128 128
 
129 129
 planner_settings_t Planner::settings;           // Initialized by settings.load()
130 130
 
131
-#if ENABLED(LASER_POWER_INLINE)
131
+/**
132
+ * Set up inline block variables
133
+ * Set laser_power_floor based on SPEED_POWER_MIN to pevent a zero power output state with LASER_POWER_TRAP
134
+ */
135
+#if ENABLED(LASER_FEATURE)
132 136
   laser_state_t Planner::laser_inline;          // Current state for blocks
137
+  const uint8_t laser_power_floor = cutter.pct_to_ocr(SPEED_POWER_MIN);
133 138
 #endif
134 139
 
135 140
 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
799 804
   if (plateau_steps < 0) {
800 805
     const float accelerate_steps_float = CEIL(intersection_distance(initial_rate, final_rate, accel, block->step_event_count));
801 806
     accelerate_steps = _MIN(uint32_t(_MAX(accelerate_steps_float, 0)), block->step_event_count);
807
+    decelerate_steps = block->step_event_count - accelerate_steps;
802 808
     plateau_steps = 0;
803 809
 
804 810
     #if ENABLED(S_CURVE_ACCELERATION)
@@ -822,7 +828,7 @@ void Planner::calculate_trapezoid_for_block(block_t * const block, const_float_t
822 828
 
823 829
   // Store new block parameters
824 830
   block->accelerate_until = accelerate_steps;
825
-  block->decelerate_after = accelerate_steps + plateau_steps;
831
+  block->decelerate_after = block->step_event_count - decelerate_steps;
826 832
   block->initial_rate = initial_rate;
827 833
   #if ENABLED(S_CURVE_ACCELERATION)
828 834
     block->acceleration_time = acceleration_time;
@@ -833,46 +839,52 @@ void Planner::calculate_trapezoid_for_block(block_t * const block, const_float_t
833 839
   #endif
834 840
   block->final_rate = final_rate;
835 841
 
836
-  /**
837
-   * Laser trapezoid calculations
838
-   *
839
-   * Approximate the trapezoid with the laser, incrementing the power every `entry_per` while accelerating
840
-   * and decrementing it every `exit_power_per` while decelerating, thus ensuring power is related to feedrate.
841
-   *
842
-   * LASER_POWER_INLINE_TRAPEZOID_CONT doesn't need this as it continuously approximates
843
-   *
844
-   * Note this may behave unreliably when running with S_CURVE_ACCELERATION
845
-   */
846
-  #if ENABLED(LASER_POWER_INLINE_TRAPEZOID)
847
-    if (block->laser.power > 0) { // No need to care if power == 0
848
-      const uint8_t entry_power = block->laser.power * entry_factor; // Power on block entry
849
-      #if DISABLED(LASER_POWER_INLINE_TRAPEZOID_CONT)
850
-        // Speedup power
851
-        const uint8_t entry_power_diff = block->laser.power - entry_power;
852
-        if (entry_power_diff) {
853
-          block->laser.entry_per = accelerate_steps / entry_power_diff;
854
-          block->laser.power_entry = entry_power;
855
-        }
856
-        else {
857
-          block->laser.entry_per = 0;
858
-          block->laser.power_entry = block->laser.power;
859
-        }
860
-        // Slowdown power
861
-        const uint8_t exit_power = block->laser.power * exit_factor, // Power on block entry
862
-                      exit_power_diff = block->laser.power - exit_power;
863
-        if (exit_power_diff) {
864
-          block->laser.exit_per = (block->step_event_count - block->decelerate_after) / exit_power_diff;
865
-          block->laser.power_exit = exit_power;
842
+  #if ENABLED(LASER_POWER_TRAP)
843
+    /**
844
+     * Laser Trapezoid Calculations
845
+     *
846
+     * Approximate the trapezoid with the laser, incrementing the power every `trap_ramp_entry_incr` steps while accelerating,
847
+     * and decrementing the power every `trap_ramp_exit_decr` while decelerating, to keep power proportional to feedrate.
848
+     * Laser power trap will reduce the initial power to no less than the laser_power_floor value. Based on the number
849
+     * of calculated accel/decel steps the power is distributed over the trapezoid entry- and exit-ramp steps.
850
+     *
851
+     * trap_ramp_active_pwr - The active power is initially set at a reduced level factor of initial power / accel steps and
852
+     * will be additively incremented using a trap_ramp_entry_incr value for each accel step processed later in the stepper code.
853
+     * The trap_ramp_exit_decr value is calculated as power / decel steps and is also adjusted to no less than the power floor.
854
+     *
855
+     * If the power == 0 the inline mode variables need to be set to zero to prevent stepper processing. The method allows
856
+     * for simpler non-powered moves like G0 or G28.
857
+     *
858
+     * Laser Trap Power works for all Jerk and Curve modes; however Arc-based moves will have issues since the segments are
859
+     * usually too small.
860
+     */
861
+    if (cutter.cutter_mode == CUTTER_MODE_CONTINUOUS) {
862
+      if (planner.laser_inline.status.isPowered && planner.laser_inline.status.isEnabled) {
863
+        if (block->laser.power > 0) {
864
+          NOLESS(block->laser.power, laser_power_floor);
865
+          block->laser.trap_ramp_active_pwr = (block->laser.power - laser_power_floor) * (initial_rate / float(block->nominal_rate)) + laser_power_floor;
866
+          block->laser.trap_ramp_entry_incr = (block->laser.power - block->laser.trap_ramp_active_pwr) / accelerate_steps;
867
+          float laser_pwr = block->laser.power * (final_rate / float(block->nominal_rate));
868
+          NOLESS(laser_pwr, laser_power_floor);
869
+          block->laser.trap_ramp_exit_decr = (block->laser.power - laser_pwr) / decelerate_steps;
870
+          #if ENABLED(DEBUG_LASER_TRAP)
871
+            SERIAL_ECHO_MSG("lp:",block->laser.power);
872
+            SERIAL_ECHO_MSG("as:",accelerate_steps);
873
+            SERIAL_ECHO_MSG("ds:",decelerate_steps);
874
+            SERIAL_ECHO_MSG("p.trap:",block->laser.trap_ramp_active_pwr);
875
+            SERIAL_ECHO_MSG("p.incr:",block->laser.trap_ramp_entry_incr);
876
+            SERIAL_ECHO_MSG("p.decr:",block->laser.trap_ramp_exit_decr);
877
+          #endif
866 878
         }
867 879
         else {
868
-          block->laser.exit_per = 0;
869
-          block->laser.power_exit = block->laser.power;
880
+          block->laser.trap_ramp_active_pwr = 0;
881
+          block->laser.trap_ramp_entry_incr = 0;
882
+          block->laser.trap_ramp_exit_decr = 0;
870 883
         }
871
-      #else
872
-        block->laser.power_entry = entry_power;
873
-      #endif
884
+
885
+      }
874 886
     }
875
-  #endif
887
+  #endif // LASER_POWER_TRAP
876 888
 }
877 889
 
878 890
 /*                            PLANNER SPEED DEFINITION
@@ -1130,10 +1142,9 @@ void Planner::recalculate_trapezoids() {
1130 1142
   // The tail may be changed by the ISR so get a local copy.
1131 1143
   uint8_t block_index = block_buffer_tail,
1132 1144
           head_block_index = block_buffer_head;
1133
-
1134
-  // Since there could be non-move blocks in the head of the queue, and the
1145
+  // Since there could be a sync block in the head of the queue, and the
1135 1146
   // next loop must not recalculate the head block (as it needs to be
1136
-  // specially handled), scan backwards to the first move block.
1147
+  // specially handled), scan backwards to the first non-SYNC block.
1137 1148
   while (head_block_index != block_index) {
1138 1149
 
1139 1150
     // Go back (head always point to the first free block)
@@ -1203,7 +1214,7 @@ void Planner::recalculate_trapezoids() {
1203 1214
   // Last/newest block in buffer. Exit speed is set with MINIMUM_PLANNER_SPEED. Always recalculated.
1204 1215
   if (next) {
1205 1216
 
1206
-    // Mark the last block as RECALCULATE, to prevent the Stepper ISR running it.
1217
+    // Mark the next(last) block as RECALCULATE, to prevent the Stepper ISR running it.
1207 1218
     // As the last block is always recalculated here, there is a chance the block isn't
1208 1219
     // marked as RECALCULATE yet. That's the reason for the following line.
1209 1220
     block->flag.recalculate = true;
@@ -1295,7 +1306,7 @@ void Planner::recalculate() {
1295 1306
 #endif // HAS_FAN
1296 1307
 
1297 1308
 /**
1298
- * Maintain fans, paste extruder pressure,
1309
+ * Maintain fans, paste extruder pressure, spindle/laser power
1299 1310
  */
1300 1311
 void Planner::check_axes_activity() {
1301 1312
 
@@ -1359,7 +1370,7 @@ void Planner::check_axes_activity() {
1359 1370
   }
1360 1371
   else {
1361 1372
 
1362
-    TERN_(HAS_CUTTER, cutter.refresh());
1373
+    TERN_(HAS_CUTTER, if (cutter.cutter_mode == CUTTER_MODE_STANDARD) cutter.refresh());
1363 1374
 
1364 1375
     #if HAS_TAIL_FAN_SPEED
1365 1376
       FANS_LOOP(i) {
@@ -1459,7 +1470,7 @@ void Planner::check_axes_activity() {
1459 1470
     for (uint8_t b = block_buffer_tail; b != block_buffer_head; b = next_block_index(b)) {
1460 1471
       const block_t * const block = &block_buffer[b];
1461 1472
       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)) {
1462
-        const float se = (float)block->steps.e / block->step_event_count * SQRT(block->nominal_speed_sqr); // mm/sec
1473
+        const float se = (float)block->steps.e / block->step_event_count * SQRT(block->nominal_speed_sqr); // mm/sec;
1463 1474
         NOLESS(high, se);
1464 1475
       }
1465 1476
     }
@@ -1781,7 +1792,7 @@ void Planner::synchronize() { while (busy()) idle(); }
1781 1792
 bool Planner::_buffer_steps(const xyze_long_t &target
1782 1793
   OPTARG(HAS_POSITION_FLOAT, const xyze_pos_t &target_float)
1783 1794
   OPTARG(HAS_DIST_MM_ARG, const xyze_float_t &cart_dist_mm)
1784
-  , feedRate_t fr_mm_s, const uint8_t extruder, const_float_t millimeters/*=0.0*/
1795
+  , feedRate_t fr_mm_s, const uint8_t extruder, const_float_t millimeters
1785 1796
 ) {
1786 1797
 
1787 1798
   // Wait for the next available block
@@ -1863,8 +1874,36 @@ bool Planner::_populate_block(
1863 1874
   );
1864 1875
 
1865 1876
   /* <-- add a slash to enable
1866
-    #define _ALINE(A) " " STR_##A  ":", target[_AXIS(A)], " (", int32_t(target[_AXIS(A)] - position[_AXIS(A)]), " steps)"
1867
-    SERIAL_ECHOLNPGM("  _populate_block FR:", fr_mm_s, LOGICAL_AXIS_MAP(_ALINE));
1877
+    SERIAL_ECHOLNPGM(
1878
+      "  _populate_block FR:", fr_mm_s,
1879
+      " A:", target.a, " (", da, " steps)"
1880
+      #if HAS_Y_AXIS
1881
+        " B:", target.b, " (", db, " steps)"
1882
+      #endif
1883
+      #if HAS_Z_AXIS
1884
+        " C:", target.c, " (", dc, " steps)"
1885
+      #endif
1886
+      #if HAS_I_AXIS
1887
+        " " STR_I ":", target.i, " (", di, " steps)"
1888
+      #endif
1889
+      #if HAS_J_AXIS
1890
+        " " STR_J ":", target.j, " (", dj, " steps)"
1891
+      #endif
1892
+      #if HAS_K_AXIS
1893
+        " " STR_K ":", target.k, " (", dk, " steps)"
1894
+      #endif
1895
+      #if HAS_U_AXIS
1896
+        " " STR_U ":", target.u, " (", du, " steps)"
1897
+      #endif
1898
+      #if HAS_V_AXIS
1899
+        " " STR_V ":", target.v, " (", dv, " steps)"
1900
+      #endif
1901
+      #if HAS_W_AXIS
1902
+        " " STR_W ":", target.w, " (", dw, " steps)"
1903
+      #if HAS_EXTRUDERS
1904
+        " E:", target.e, " (", de, " steps)"
1905
+      #endif
1906
+    );
1868 1907
   //*/
1869 1908
 
1870 1909
   #if EITHER(PREVENT_COLD_EXTRUSION, PREVENT_LENGTHY_EXTRUDE)
@@ -1962,11 +2001,34 @@ bool Planner::_populate_block(
1962 2001
   // Set direction bits
1963 2002
   block->direction_bits = dm;
1964 2003
 
1965
-  // Update block laser power
1966
-  #if ENABLED(LASER_POWER_INLINE)
1967
-    laser_inline.status.isPlanned = true;
1968
-    block->laser.status = laser_inline.status;
1969
-    block->laser.power = laser_inline.power;
2004
+  /**
2005
+   * Update block laser power
2006
+   * For standard mode get the cutter.power value for processing, since it's
2007
+   * only set by apply_power().
2008
+   */
2009
+  #if HAS_CUTTER
2010
+    switch (cutter.cutter_mode) {
2011
+      default: break;
2012
+
2013
+      case CUTTER_MODE_STANDARD: block->cutter_power = cutter.power; break;
2014
+
2015
+      #if ENABLED(LASER_FEATURE)
2016
+        /**
2017
+         * For inline mode get the laser_inline variables, including power and status.
2018
+         * Dynamic mode only needs to update if the feedrate has changed, since it's
2019
+         * calculated from the current feedrate and power level.
2020
+         */
2021
+        case CUTTER_MODE_CONTINUOUS:
2022
+          block->laser.power = laser_inline.power;
2023
+          block->laser.status = laser_inline.status;
2024
+          break;
2025
+
2026
+        case CUTTER_MODE_DYNAMIC:
2027
+          if (cutter.laser_feedrate_changed())  // Only process changes in rate
2028
+            block->laser.power = laser_inline.power = cutter.calc_dynamic_power();
2029
+          break;
2030
+      #endif
2031
+    }
1970 2032
   #endif
1971 2033
 
1972 2034
   // Number of steps for each axis
@@ -2028,9 +2090,9 @@ bool Planner::_populate_block(
2028 2090
     #endif
2029 2091
   #elif ENABLED(MARKFORGED_XY)
2030 2092
     steps_dist_mm.a      = (da - db) * mm_per_step[A_AXIS];
2031
-    steps_dist_mm.b      =       db  * mm_per_step[B_AXIS];
2093
+    steps_dist_mm.b      = db * mm_per_step[B_AXIS];
2032 2094
   #elif ENABLED(MARKFORGED_YX)
2033
-    steps_dist_mm.a      =       da  * mm_per_step[A_AXIS];
2095
+    steps_dist_mm.a      = da * mm_per_step[A_AXIS];
2034 2096
     steps_dist_mm.b      = (db - da) * mm_per_step[B_AXIS];
2035 2097
   #else
2036 2098
     XYZ_CODE(
@@ -2076,21 +2138,12 @@ bool Planner::_populate_block(
2076 2138
       block->millimeters = millimeters;
2077 2139
     else {
2078 2140
       /**
2079
-       * Distance for interpretation of feedrate in accordance with LinuxCNC (the successor of
2080
-       * NIST RS274NGC interpreter - version 3) and its default CANON_XYZ feed reference mode.
2081
-       *
2082
-       * Assume:
2083
-       *   - X, Y, Z are the primary linear axes;
2084
-       *   - U, V, W are secondary linear axes;
2085
-       *   - A, B, C are rotational axes.
2086
-       *
2087
-       * Then:
2088
-       *   - dX, dY, dZ are the displacements of the primary linear axes;
2089
-       *   - dU, dV, dW are the displacements of linear axes;
2090
-       *   - dA, dB, dC are the displacements of rotational axes.
2091
-       *
2092
-       * The time it takes to execute move command with feedrate F is t = D/F,
2093
-       * where D is the total distance, calculated as follows:
2141
+       * Distance for interpretation of feedrate in accordance with LinuxCNC (the successor of NIST
2142
+       * RS274NGC interpreter - version 3) and its default CANON_XYZ feed reference mode.
2143
+       * Assume that X, Y, Z are the primary linear axes and U, V, W are secondary linear axes and A, B, C are
2144
+       * 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
2145
+       * dA, dB, dC are the displacements of rotational axes.
2146
+       * The time it takes to execute move command with feedrate F is t = D/F, where D is the total distance, calculated as follows:
2094 2147
        *   D^2 = dX^2 + dY^2 + dZ^2
2095 2148
        *   if D^2 == 0 (none of XYZ move but any secondary linear axes move, whether other axes are moved or not):
2096 2149
        *     D^2 = dU^2 + dV^2 + dW^2
@@ -2099,9 +2152,8 @@ bool Planner::_populate_block(
2099 2152
        */
2100 2153
       float distance_sqr = (
2101 2154
         #if ENABLED(ARTICULATED_ROBOT_ARM)
2102
-          // For articulated robots, interpreting feedrate like LinuxCNC would require inverse kinematics. As a workaround,
2103
-          // assume that motors sit on a mutually-orthogonal axes and we can think of distance as magnitude of an n-vector
2104
-          // in an n-dimensional Euclidian space.
2155
+          // For articulated robots, interpreting feedrate like LinuxCNC would require inverse kinematics. As a workaround, pretend that motors sit on n mutually orthogonal
2156
+          // axes and assume that we could think of distance as magnitude of an n-vector in an n-dimensional Euclidian space.
2105 2157
           NUM_AXIS_GANG(
2106 2158
               sq(steps_dist_mm.x), + sq(steps_dist_mm.y), + sq(steps_dist_mm.z),
2107 2159
             + sq(steps_dist_mm.i), + sq(steps_dist_mm.j), + sq(steps_dist_mm.k),
@@ -2121,7 +2173,7 @@ bool Planner::_populate_block(
2121 2173
         #elif CORE_IS_YZ
2122 2174
           XYZ_GANG(sq(steps_dist_mm.x),      + sq(steps_dist_mm.head.y), + sq(steps_dist_mm.head.z))
2123 2175
         #else
2124
-          XYZ_GANG(sq(steps_dist_mm.x),      + sq(steps_dist_mm.y),      + sq(steps_dist_mm.z))
2176
+          XYZ_GANG(sq(steps_dist_mm.x),       + sq(steps_dist_mm.y),      + sq(steps_dist_mm.z))
2125 2177
         #endif
2126 2178
       );
2127 2179
 
@@ -2154,9 +2206,9 @@ bool Planner::_populate_block(
2154 2206
 
2155 2207
     /**
2156 2208
      * At this point at least one of the axes has more steps than
2157
-     * MIN_STEPS_PER_SEGMENT, ensuring the segment won't get dropped
2158
-     * as zero-length. It's important to not apply corrections to blocks
2159
-     * that would get dropped!
2209
+     * MIN_STEPS_PER_SEGMENT, ensuring the segment won't get dropped as
2210
+     * zero-length. It's important to not apply corrections
2211
+     * to blocks that would get dropped!
2160 2212
      *
2161 2213
      * A correction function is permitted to add steps to an axis, it
2162 2214
      * should *never* remove steps!
@@ -2177,7 +2229,6 @@ bool Planner::_populate_block(
2177 2229
 
2178 2230
   TERN_(MIXING_EXTRUDER, mixer.populate_block(block->b_color));
2179 2231
 
2180
-  TERN_(HAS_CUTTER, block->cutter_power = cutter.power);
2181 2232
 
2182 2233
   #if HAS_FAN
2183 2234
     FANS_LOOP(i) block->fan_speed[i] = thermalManager.fan_speed[i];
@@ -2192,9 +2243,15 @@ bool Planner::_populate_block(
2192 2243
 
2193 2244
   #if ENABLED(AUTO_POWER_CONTROL)
2194 2245
     if (NUM_AXIS_GANG(
2195
-         block->steps.x, || block->steps.y, || block->steps.z,
2196
-      || block->steps.i, || block->steps.j, || block->steps.k,
2197
-      || block->steps.u, || block->steps.v, || block->steps.w
2246
+         block->steps.x,
2247
+      || block->steps.y,
2248
+      || block->steps.z,
2249
+      || block->steps.i,
2250
+      || block->steps.j,
2251
+      || block->steps.k,
2252
+      || block->steps.u,
2253
+      || block->steps.v,
2254
+      || block->steps.w
2198 2255
     )) powerManager.power_on();
2199 2256
   #endif
2200 2257
 
@@ -2428,7 +2485,7 @@ bool Planner::_populate_block(
2428 2485
   if (speed_factor < 1.0f) {
2429 2486
     current_speed *= speed_factor;
2430 2487
     block->nominal_rate *= speed_factor;
2431
-    block->nominal_speed_sqr *= sq(speed_factor);
2488
+    block->nominal_speed_sqr = block->nominal_speed_sqr * sq(speed_factor);
2432 2489
   }
2433 2490
 
2434 2491
   // Compute and limit the acceleration rate for the trapezoid generator.
@@ -2630,15 +2687,14 @@ bool Planner::_populate_block(
2630 2687
         vmax_junction_sqr = sq(float(MINIMUM_PLANNER_SPEED));
2631 2688
       }
2632 2689
       else {
2690
+        NOLESS(junction_cos_theta, -0.999999f); // Check for numerical round-off to avoid divide by zero.
2691
+
2633 2692
         // Convert delta vector to unit vector
2634 2693
         xyze_float_t junction_unit_vec = unit_vec - prev_unit_vec;
2635 2694
         normalize_junction_vector(junction_unit_vec);
2636 2695
 
2637
-        const float junction_acceleration = limit_value_by_axis_maximum(block->acceleration, junction_unit_vec);
2638
-
2639
-        NOLESS(junction_cos_theta, -0.999999f); // Check for numerical round-off to avoid divide by zero.
2640
-
2641
-        const float sin_theta_d2 = SQRT(0.5f * (1.0f - junction_cos_theta)); // Trig half angle identity. Always positive.
2696
+        const float junction_acceleration = limit_value_by_axis_maximum(block->acceleration, junction_unit_vec),
2697
+                    sin_theta_d2 = SQRT(0.5f * (1.0f - junction_cos_theta)); // Trig half angle identity. Always positive.
2642 2698
 
2643 2699
         vmax_junction_sqr = junction_acceleration * junction_deviation_mm * sin_theta_d2 / (1.0f - sin_theta_d2);
2644 2700
 
@@ -2889,21 +2945,19 @@ bool Planner::_populate_block(
2889 2945
 
2890 2946
 /**
2891 2947
  * Planner::buffer_sync_block
2892
- * Add a block to the buffer that just updates the position,
2893
- * or in case of LASER_SYNCHRONOUS_M106_M107 the fan PWM
2948
+ * Add a block to the buffer that just updates the position
2949
+ * @param sync_flag BLOCK_FLAG_SYNC_FANS & BLOCK_FLAG_LASER_PWR
2950
+ * Supports LASER_SYNCHRONOUS_M106_M107 and LASER_POWER_SYNC power sync block buffer queueing.
2894 2951
  */
2895
-void Planner::buffer_sync_block(TERN_(LASER_SYNCHRONOUS_M106_M107, const BlockFlagBit sync_flag/*=BLOCK_BIT_SYNC_POSITION*/)) {
2896
-  #if DISABLED(LASER_SYNCHRONOUS_M106_M107)
2897
-    constexpr BlockFlagBit sync_flag = BLOCK_BIT_SYNC_POSITION;
2898
-  #endif
2952
+
2953
+void Planner::buffer_sync_block(const BlockFlagBit sync_flag/*=BLOCK_BIT_SYNC_POSITION*/) {
2899 2954
 
2900 2955
   // Wait for the next available block
2901 2956
   uint8_t next_buffer_head;
2902 2957
   block_t * const block = get_next_free_block(next_buffer_head);
2903 2958
 
2904 2959
   // Clear block
2905
-  block->reset();
2906
-
2960
+  memset(block, 0, sizeof(block_t));
2907 2961
   block->flag.apply(sync_flag);
2908 2962
 
2909 2963
   block->position = position;
@@ -2915,6 +2969,12 @@ void Planner::buffer_sync_block(TERN_(LASER_SYNCHRONOUS_M106_M107, const BlockFl
2915 2969
     FANS_LOOP(i) block->fan_speed[i] = thermalManager.fan_speed[i];
2916 2970
   #endif
2917 2971
 
2972
+  /**
2973
+   * M3-based power setting can be processed inline with a laser power sync block.
2974
+   * During active moves cutter.power is processed immediately, otherwise on the next move.
2975
+   */
2976
+  TERN_(LASER_POWER_SYNC, block->laser.power = cutter.power);
2977
+
2918 2978
   // If this is the first added movement, reload the delay, otherwise, cancel it.
2919 2979
   if (block_buffer_head == block_buffer_tail) {
2920 2980
     // 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
3052 3112
   if (!_buffer_steps(target
3053 3113
       OPTARG(HAS_POSITION_FLOAT, target_float)
3054 3114
       OPTARG(HAS_DIST_MM_ARG, cart_dist_mm)
3055
-      , fr_mm_s, extruder, millimeters
3056
-  )) return false;
3115
+      , fr_mm_s, extruder, millimeters)
3116
+  ) return false;
3057 3117
 
3058 3118
   stepper.wake_up();
3059 3119
   return true;
@@ -3099,7 +3159,7 @@ bool Planner::buffer_line(const xyze_pos_t &cart, const_feedRate_t fr_mm_s, cons
3099 3159
     inverse_kinematics(machine);
3100 3160
 
3101 3161
     #if ENABLED(SCARA_FEEDRATE_SCALING)
3102
-      // For SCARA scale the feed rate from mm/s to degrees/s
3162
+      // For SCARA scale the feedrate from mm/s to degrees/s
3103 3163
       // i.e., Complete the angular vector in the given time.
3104 3164
       const float duration_recip = inv_duration ?: fr_mm_s / mm;
3105 3165
       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
3120 3180
 
3121 3181
 #if ENABLED(DIRECT_STEPPING)
3122 3182
 
3123
-  /**
3124
-   * @brief Add a direct stepping page block to the buffer
3125
-   *        and wake up the Stepper ISR to process it.
3126
-   *
3127
-   * @param page_idx Page index provided by G6 I<index>
3128
-   * @param extruder The extruder to use in the move
3129
-   * @param num_steps Number of steps to process in the ISR
3130
-   */
3131 3183
   void Planner::buffer_page(const page_idx_t page_idx, const uint8_t extruder, const uint16_t num_steps) {
3132 3184
     if (!last_page_step_rate) {
3133 3185
       kill(GET_TEXT_F(MSG_BAD_PAGE_SPEED));
@@ -3212,7 +3264,7 @@ void Planner::set_machine_position_mm(const abce_pos_t &abce) {
3212 3264
   if (has_blocks_queued()) {
3213 3265
     //previous_nominal_speed_sqr = 0.0; // Reset planner junction speeds. Assume start from rest.
3214 3266
     //previous_speed.reset();
3215
-    buffer_sync_block();
3267
+    buffer_sync_block(BLOCK_BIT_SYNC_POSITION);
3216 3268
   }
3217 3269
   else {
3218 3270
     #if ENABLED(BACKLASH_COMPENSATION)
@@ -3225,12 +3277,6 @@ void Planner::set_machine_position_mm(const abce_pos_t &abce) {
3225 3277
   }
3226 3278
 }
3227 3279
 
3228
-/**
3229
- * @brief Set the Planner position in mm
3230
- * @details Set the Planner position from a native machine position in mm
3231
- *
3232
- * @param xyze A native (Cartesian) machine position
3233
- */
3234 3280
 void Planner::set_position_mm(const xyze_pos_t &xyze) {
3235 3281
   xyze_pos_t machine = xyze;
3236 3282
   TERN_(HAS_POSITION_MODIFIERS, apply_modifiers(machine, true));
@@ -3259,20 +3305,14 @@ void Planner::set_position_mm(const xyze_pos_t &xyze) {
3259 3305
     TERN_(IS_KINEMATIC, TERN_(HAS_EXTRUDERS, position_cart.e = e));
3260 3306
 
3261 3307
     if (has_blocks_queued())
3262
-      buffer_sync_block();
3308
+      buffer_sync_block(BLOCK_BIT_SYNC_POSITION);
3263 3309
     else
3264 3310
       stepper.set_axis_position(E_AXIS, position.e);
3265 3311
   }
3266 3312
 
3267 3313
 #endif
3268 3314
 
3269
-/**
3270
- * @brief Recalculate the steps/s^2 acceleration rates, based on the mm/s^2
3271
- * @details Update planner movement factors after a change to certain settings:
3272
- *          - max_acceleration_steps_per_s2 from settings max_acceleration_mm_per_s2 * axis_steps_per_mm (M201, M92)
3273
- *          - acceleration_long_cutoff based on the largest max_acceleration_steps_per_s2 (M201)
3274
- *          - max_e_jerk for all extruders based on junction_deviation_mm (M205 J)
3275
- */
3315
+// Recalculate the steps/s^2 acceleration rates, based on the mm/s^2
3276 3316
 void Planner::refresh_acceleration_rates() {
3277 3317
   uint32_t highest_rate = 1;
3278 3318
   LOOP_DISTINCT_AXES(i) {
@@ -3285,8 +3325,8 @@ void Planner::refresh_acceleration_rates() {
3285 3325
 }
3286 3326
 
3287 3327
 /**
3288
- * @brief Recalculate 'position' and 'mm_per_step'.
3289
- * @details Required whenever settings.axis_steps_per_mm changes!
3328
+ * Recalculate 'position' and 'mm_per_step'.
3329
+ * Must be called whenever settings.axis_steps_per_mm changes!
3290 3330
  */
3291 3331
 void Planner::refresh_positioning() {
3292 3332
   LOOP_DISTINCT_AXES(i) mm_per_step[i] = 1.0f / settings.axis_steps_per_mm[i];

+ 47
- 45
Marlin/src/module/planner.h View File

@@ -89,30 +89,6 @@
89 89
   #define HAS_DIST_MM_ARG 1
90 90
 #endif
91 91
 
92
-#if ENABLED(LASER_POWER_INLINE)
93
-
94
-  typedef struct {
95
-    bool isPlanned:1;
96
-    bool isEnabled:1;
97
-    bool dir:1;
98
-    bool Reserved:6;
99
-  } power_status_t;
100
-
101
-  typedef struct {
102
-    power_status_t status;    // See planner settings for meaning
103
-    uint8_t power;            // Ditto; When in trapezoid mode this is nominal power
104
-    #if ENABLED(LASER_POWER_INLINE_TRAPEZOID)
105
-      uint8_t   power_entry;  // Entry power for the laser
106
-      #if DISABLED(LASER_POWER_INLINE_TRAPEZOID_CONT)
107
-        uint8_t   power_exit; // Exit power for the laser
108
-        uint32_t  entry_per,  // Steps per power increment (to avoid floats in stepper calcs)
109
-                  exit_per;   // Steps per power decrement
110
-      #endif
111
-    #endif
112
-  } block_laser_t;
113
-
114
-#endif
115
-
116 92
 /**
117 93
  * Planner block flags as boolean bit fields
118 94
  */
@@ -132,14 +108,14 @@ enum BlockFlagBit {
132 108
   BLOCK_BIT_SYNC_POSITION
133 109
 
134 110
   // Direct stepping page
135
-  #if ENABLED(DIRECT_STEPPING)
136
-    , BLOCK_BIT_PAGE
137
-  #endif
111
+  OPTARG(DIRECT_STEPPING, BLOCK_BIT_PAGE)
112
+
138 113
 
139 114
   // Sync the fan speeds from the block
140
-  #if ENABLED(LASER_SYNCHRONOUS_M106_M107)
141
-    , BLOCK_BIT_SYNC_FANS
142
-  #endif
115
+  OPTARG(LASER_SYNCHRONOUS_M106_M107, BLOCK_BIT_SYNC_FANS)
116
+
117
+  // Sync laser power from a queued block
118
+  OPTARG(LASER_POWER_SYNC, BLOCK_BIT_LASER_PWR)
143 119
 };
144 120
 
145 121
 /**
@@ -165,6 +141,10 @@ typedef struct {
165 141
       #if ENABLED(LASER_SYNCHRONOUS_M106_M107)
166 142
         bool sync_fans:1;
167 143
       #endif
144
+
145
+      #if ENABLED(LASER_POWER_SYNC)
146
+        bool sync_laser_pwr:1;
147
+      #endif
168 148
     };
169 149
   };
170 150
 
@@ -176,9 +156,34 @@ typedef struct {
176 156
 
177 157
 } block_flags_t;
178 158
 
159
+#if ENABLED(LASER_FEATURE)
160
+
161
+  typedef struct {
162
+    bool isEnabled:1;                                 // Set to engage the inline laser power output.
163
+    bool dir:1;
164
+    bool isPowered:1;                                 // Set on any parsed G1, G2, G3, or G5 powered move, cleared on G0 and G28.
165
+    bool isSyncPower:1;                               // Set on a M3 sync based set laser power, used to determine active trap power
166
+    bool Reserved:4;
167
+  } power_status_t;
168
+
169
+  typedef struct {
170
+    power_status_t status;                            // See planner settings for meaning
171
+    uint8_t power;                                    // Ditto; When in trapezoid mode this is nominal power
172
+
173
+    #if ENABLED(LASER_POWER_TRAP)
174
+      float trap_ramp_active_pwr;                     // Laser power level during active trapezoid smoothing
175
+      float trap_ramp_entry_incr;                     // Acceleration per step laser power increment (trap entry)
176
+      float trap_ramp_exit_decr;                      // Deceleration per step laser power decrement (trap exit)
177
+    #endif
178
+  } block_laser_t;
179
+
180
+#endif
181
+
179 182
 /**
180
- * A single entry in the planner buffer, used to set up and
181
- * track a coordinated linear motion for one or more axes.
183
+ * struct block_t
184
+ *
185
+ * A single entry in the planner buffer.
186
+ * Tracks linear movement over multiple axes.
182 187
  *
183 188
  * The "nominal" values are as-specified by G-code, and
184 189
  * may never actually be reached due to acceleration limits.
@@ -188,7 +193,8 @@ typedef struct block_t {
188 193
   volatile block_flags_t flag;              // Block flags
189 194
 
190 195
   volatile bool is_fan_sync() { return TERN0(LASER_SYNCHRONOUS_M106_M107, flag.sync_fans); }
191
-  volatile bool is_sync() { return flag.sync_position || is_fan_sync(); }
196
+  volatile bool is_pwr_sync() { return TERN0(LASER_POWER_SYNC, flag.sync_laser_pwr); }
197
+  volatile bool is_sync() { return flag.sync_position || is_fan_sync() || is_pwr_sync(); }
192 198
   volatile bool is_page() { return TERN0(DIRECT_STEPPING, flag.page); }
193 199
   volatile bool is_move() { return !(is_sync() || is_page()); }
194 200
 
@@ -270,12 +276,10 @@ typedef struct block_t {
270 276
     xyze_pos_t start_position;
271 277
   #endif
272 278
 
273
-  #if ENABLED(LASER_POWER_INLINE)
279
+  #if ENABLED(LASER_FEATURE)
274 280
     block_laser_t laser;
275 281
   #endif
276 282
 
277
-  void reset() { memset((char*)this, 0, sizeof(*this)); }
278
-
279 283
 } block_t;
280 284
 
281 285
 #if ANY(LIN_ADVANCE, SCARA_FEEDRATE_SCALING, GRADIENT_MIX, LCD_SHOW_E_TOTAL, POWER_LOSS_RECOVERY)
@@ -284,7 +288,7 @@ typedef struct block_t {
284 288
 
285 289
 #define BLOCK_MOD(n) ((n)&(BLOCK_BUFFER_SIZE-1))
286 290
 
287
-#if ENABLED(LASER_POWER_INLINE)
291
+#if ENABLED(LASER_FEATURE)
288 292
   typedef struct {
289 293
     /**
290 294
      * Laser status flags
@@ -293,11 +297,10 @@ typedef struct block_t {
293 297
     /**
294 298
      * Laser power: 0 or 255 in case of PWM-less laser,
295 299
      * or the OCR (oscillator count register) value;
296
-     *
297 300
      * Using OCR instead of raw power, because it avoids
298 301
      * floating point operations during the move loop.
299 302
      */
300
-    uint8_t power;
303
+    volatile uint8_t power;
301 304
   } laser_state_t;
302 305
 #endif
303 306
 
@@ -399,7 +402,7 @@ class Planner {
399 402
 
400 403
     static planner_settings_t settings;
401 404
 
402
-    #if ENABLED(LASER_POWER_INLINE)
405
+    #if ENABLED(LASER_FEATURE)
403 406
       static laser_state_t laser_inline;
404 407
     #endif
405 408
 
@@ -784,12 +787,11 @@ class Planner {
784 787
 
785 788
     /**
786 789
      * Planner::buffer_sync_block
787
-     * Add a block to the buffer that just updates the position or in
788
-     * case of LASER_SYNCHRONOUS_M106_M107 the fan pwm
790
+     * Add a block to the buffer that just updates the position
791
+     * @param sync_flag sets a condition bit to process additional items
792
+     * such as sync fan pwm or sync M3/M4 laser power into a queued block
789 793
      */
790
-    static void buffer_sync_block(
791
-      TERN_(LASER_SYNCHRONOUS_M106_M107, const BlockFlagBit flag=BLOCK_BIT_SYNC_POSITION)
792
-    );
794
+      static void buffer_sync_block(const BlockFlagBit flag=BLOCK_BIT_SYNC_POSITION);
793 795
 
794 796
   #if IS_KINEMATIC
795 797
     private:

+ 105
- 131
Marlin/src/module/stepper.cpp View File

@@ -253,20 +253,6 @@ xyz_long_t Stepper::endstops_trigsteps;
253 253
 xyze_long_t Stepper::count_position{0};
254 254
 xyze_int8_t Stepper::count_direction{0};
255 255
 
256
-#if ENABLED(LASER_POWER_INLINE_TRAPEZOID)
257
-  Stepper::stepper_laser_t Stepper::laser_trap = {
258
-    .enabled = false,
259
-    .cur_power = 0,
260
-    .cruise_set = false,
261
-    #if DISABLED(LASER_POWER_INLINE_TRAPEZOID_CONT)
262
-      .last_step_count = 0,
263
-      .acc_step_count = 0
264
-    #else
265
-      .till_update = 0
266
-    #endif
267
-  };
268
-#endif
269
-
270 256
 #define MINDIR(A) (count_direction[_AXIS(A)] < 0)
271 257
 #define MAXDIR(A) (count_direction[_AXIS(A)] > 0)
272 258
 
@@ -1964,7 +1950,6 @@ uint32_t Stepper::block_phase_isr() {
1964 1950
 
1965 1951
   // If there is a current block
1966 1952
   if (current_block) {
1967
-
1968 1953
     // If current block is finished, reset pointer and finalize state
1969 1954
     if (step_events_completed >= step_event_count) {
1970 1955
       #if ENABLED(DIRECT_STEPPING)
@@ -2017,32 +2002,28 @@ uint32_t Stepper::block_phase_isr() {
2017 2002
           else if (LA_steps) nextAdvanceISR = 0;
2018 2003
         #endif
2019 2004
 
2020
-        // Update laser - Accelerating
2021
-        #if ENABLED(LASER_POWER_INLINE_TRAPEZOID)
2022
-          if (laser_trap.enabled) {
2023
-            #if DISABLED(LASER_POWER_INLINE_TRAPEZOID_CONT)
2024
-              if (current_block->laser.entry_per) {
2025
-                laser_trap.acc_step_count -= step_events_completed - laser_trap.last_step_count;
2026
-                laser_trap.last_step_count = step_events_completed;
2027
-
2028
-                // Should be faster than a divide, since this should trip just once
2029
-                if (laser_trap.acc_step_count < 0) {
2030
-                  while (laser_trap.acc_step_count < 0) {
2031
-                    laser_trap.acc_step_count += current_block->laser.entry_per;
2032
-                    if (laser_trap.cur_power < current_block->laser.power) laser_trap.cur_power++;
2033
-                  }
2034
-                  cutter.ocr_set_power(laser_trap.cur_power);
2035
-                }
2036
-              }
2037
-            #else
2038
-              if (laser_trap.till_update)
2039
-                laser_trap.till_update--;
2040
-              else {
2041
-                laser_trap.till_update = LASER_POWER_INLINE_TRAPEZOID_CONT_PER;
2042
-                laser_trap.cur_power = (current_block->laser.power * acc_step_rate) / current_block->nominal_rate;
2043
-                cutter.ocr_set_power(laser_trap.cur_power); // Cycle efficiency is irrelevant it the last line was many cycles
2005
+        /*
2006
+         * Adjust Laser Power - Accelerating
2007
+         * isPowered - True when a move is powered.
2008
+         * isEnabled - laser power is active.
2009
+         * Laser power variables are calulated and stored in this block by the planner code.
2010
+         *
2011
+         * trap_ramp_active_pwr - the active power in this block across accel or decel trap steps.
2012
+         * trap_ramp_entry_incr - holds the precalculated value to increase the current power per accel step.
2013
+         *
2014
+         * Apply the starting active power and then increase power per step by the trap_ramp_entry_incr value if positive.
2015
+         */
2016
+
2017
+        #if ENABLED(LASER_POWER_TRAP)
2018
+          if (cutter.cutter_mode == CUTTER_MODE_CONTINUOUS) {
2019
+            if (planner.laser_inline.status.isPowered && planner.laser_inline.status.isEnabled) {
2020
+              if (current_block->laser.trap_ramp_entry_incr > 0) {
2021
+                cutter.apply_power(current_block->laser.trap_ramp_active_pwr);
2022
+                current_block->laser.trap_ramp_active_pwr += current_block->laser.trap_ramp_entry_incr;
2044 2023
               }
2045
-            #endif
2024
+            }
2025
+            // Not a powered move.
2026
+            else cutter.apply_power(0);
2046 2027
           }
2047 2028
         #endif
2048 2029
       }
@@ -2066,7 +2047,6 @@ uint32_t Stepper::block_phase_isr() {
2066 2047
               : current_block->final_rate;
2067 2048
           }
2068 2049
         #else
2069
-
2070 2050
           // Using the old trapezoidal control
2071 2051
           step_rate = STEP_MULTIPLY(deceleration_time, current_block->acceleration_rate);
2072 2052
           if (step_rate < acc_step_rate) { // Still decelerating?
@@ -2094,37 +2074,25 @@ uint32_t Stepper::block_phase_isr() {
2094 2074
           else if (LA_steps) nextAdvanceISR = 0;
2095 2075
         #endif // LIN_ADVANCE
2096 2076
 
2097
-        // Update laser - Decelerating
2098
-        #if ENABLED(LASER_POWER_INLINE_TRAPEZOID)
2099
-          if (laser_trap.enabled) {
2100
-            #if DISABLED(LASER_POWER_INLINE_TRAPEZOID_CONT)
2101
-              if (current_block->laser.exit_per) {
2102
-                laser_trap.acc_step_count -= step_events_completed - laser_trap.last_step_count;
2103
-                laser_trap.last_step_count = step_events_completed;
2104
-
2105
-                // Should be faster than a divide, since this should trip just once
2106
-                if (laser_trap.acc_step_count < 0) {
2107
-                  while (laser_trap.acc_step_count < 0) {
2108
-                    laser_trap.acc_step_count += current_block->laser.exit_per;
2109
-                    if (laser_trap.cur_power > current_block->laser.power_exit) laser_trap.cur_power--;
2110
-                  }
2111
-                  cutter.ocr_set_power(laser_trap.cur_power);
2112
-                }
2113
-              }
2114
-            #else
2115
-              if (laser_trap.till_update)
2116
-                laser_trap.till_update--;
2117
-              else {
2118
-                laser_trap.till_update = LASER_POWER_INLINE_TRAPEZOID_CONT_PER;
2119
-                laser_trap.cur_power = (current_block->laser.power * step_rate) / current_block->nominal_rate;
2120
-                cutter.ocr_set_power(laser_trap.cur_power); // Cycle efficiency isn't relevant when the last line was many cycles
2077
+        /*
2078
+         * Adjust Laser Power - Decelerating
2079
+         * trap_ramp_entry_decr - holds the precalculated value to decrease the current power per decel step.
2080
+         */
2081
+        #if ENABLED(LASER_POWER_TRAP)
2082
+          if (cutter.cutter_mode == CUTTER_MODE_CONTINUOUS) {
2083
+            if (planner.laser_inline.status.isPowered && planner.laser_inline.status.isEnabled) {
2084
+              if (current_block->laser.trap_ramp_exit_decr > 0) {
2085
+                current_block->laser.trap_ramp_active_pwr -= current_block->laser.trap_ramp_exit_decr;
2086
+                cutter.apply_power(current_block->laser.trap_ramp_active_pwr);
2121 2087
               }
2122
-            #endif
2088
+              // Not a powered move.
2089
+              else cutter.apply_power(0);
2090
+            }
2123 2091
           }
2124 2092
         #endif
2093
+
2125 2094
       }
2126
-      // Must be in cruise phase otherwise
2127
-      else {
2095
+      else {  // Must be in cruise phase otherwise
2128 2096
 
2129 2097
         #if ENABLED(LIN_ADVANCE)
2130 2098
           // If there are any esteps, fire the next advance_isr "now"
@@ -2139,24 +2107,50 @@ uint32_t Stepper::block_phase_isr() {
2139 2107
 
2140 2108
         // The timer interval is just the nominal value for the nominal speed
2141 2109
         interval = ticks_nominal;
2110
+      }
2142 2111
 
2143
-        // Update laser - Cruising
2144
-        #if ENABLED(LASER_POWER_INLINE_TRAPEZOID)
2145
-          if (laser_trap.enabled) {
2146
-            if (!laser_trap.cruise_set) {
2147
-              laser_trap.cur_power = current_block->laser.power;
2148
-              cutter.ocr_set_power(laser_trap.cur_power);
2149
-              laser_trap.cruise_set = true;
2112
+      /* Adjust Laser Power - Cruise
2113
+       * power - direct or floor adjusted active laser power.
2114
+       */
2115
+
2116
+      #if ENABLED(LASER_POWER_TRAP)
2117
+        if (cutter.cutter_mode == CUTTER_MODE_CONTINUOUS) {
2118
+          if (step_events_completed + 1 == accelerate_until) {
2119
+            if (planner.laser_inline.status.isPowered && planner.laser_inline.status.isEnabled) {
2120
+              if (current_block->laser.trap_ramp_entry_incr > 0) {
2121
+                current_block->laser.trap_ramp_active_pwr = current_block->laser.power;
2122
+                cutter.apply_power(current_block->laser.power);
2123
+              }
2150 2124
             }
2151
-            #if ENABLED(LASER_POWER_INLINE_TRAPEZOID_CONT)
2152
-              laser_trap.till_update = LASER_POWER_INLINE_TRAPEZOID_CONT_PER;
2153
-            #else
2154
-              laser_trap.last_step_count = step_events_completed;
2155
-            #endif
2125
+            // Not a powered move.
2126
+            else cutter.apply_power(0);
2156 2127
           }
2157
-        #endif
2158
-      }
2128
+        }
2129
+      #endif
2159 2130
     }
2131
+
2132
+    #if ENABLED(LASER_FEATURE)
2133
+      /*
2134
+       * CUTTER_MODE_DYNAMIC is experimental and developing.
2135
+       * Super-fast method to dynamically adjust the laser power OCR value based on the input feedrate in mm-per-minute.
2136
+       * TODO: Set up Min/Max OCR offsets to allow tuning and scaling of various lasers.
2137
+       * TODO: Integrate accel/decel +-rate into the dynamic laser power calc.
2138
+       */
2139
+      if (cutter.cutter_mode == CUTTER_MODE_DYNAMIC
2140
+        && planner.laser_inline.status.isPowered                  // isPowered flag set on any parsed G1, G2, G3, or G5 move; cleared on any others.
2141
+        && cutter.last_block_power != current_block->laser.power  // Prevent constant update without change
2142
+      ) {
2143
+        cutter.apply_power(current_block->laser.power);
2144
+        cutter.last_block_power = current_block->laser.power;
2145
+      }
2146
+    #endif
2147
+  }
2148
+  else { // !current_block
2149
+    #if ENABLED(LASER_FEATURE)
2150
+      if (cutter.cutter_mode == CUTTER_MODE_DYNAMIC) {
2151
+        cutter.apply_power(0);  // No movement in dynamic mode so turn Laser off
2152
+      }
2153
+    #endif
2160 2154
   }
2161 2155
 
2162 2156
   // 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() {
2169 2163
       // Sync block? Sync the stepper counts or fan speeds and return
2170 2164
       while (current_block->is_sync()) {
2171 2165
 
2172
-        if (current_block->is_fan_sync()) {
2173
-          TERN_(LASER_SYNCHRONOUS_M106_M107, planner.sync_fan_speeds(current_block->fan_speed));
2174
-        }
2175
-        else
2176
-          _set_position(current_block->position);
2166
+        #if ENABLED(LASER_POWER_SYNC)
2167
+          if (cutter.cutter_mode == CUTTER_MODE_CONTINUOUS) {
2168
+            if (current_block->is_pwr_sync()) {
2169
+              planner.laser_inline.status.isSyncPower = true;
2170
+              cutter.apply_power(current_block->laser.power);
2171
+            }
2172
+          }
2173
+        #endif
2174
+
2175
+        TERN_(LASER_SYNCHRONOUS_M106_M107, if (current_block->is_fan_sync()) planner.sync_fan_speeds(current_block->fan_speed));
2176
+
2177
+        if (!(current_block->is_fan_sync() || current_block->is_pwr_sync())) _set_position(current_block->position);
2177 2178
 
2178 2179
         discard_current_block();
2179 2180
 
@@ -2183,8 +2184,10 @@ uint32_t Stepper::block_phase_isr() {
2183 2184
       }
2184 2185
 
2185 2186
       // For non-inline cutter, grossly apply power
2186
-      #if ENABLED(LASER_FEATURE) && DISABLED(LASER_POWER_INLINE)
2187
-        cutter.apply_power(current_block->cutter_power);
2187
+      #if HAS_CUTTER
2188
+        if (cutter.cutter_mode == CUTTER_MODE_STANDARD) {
2189
+          cutter.apply_power(current_block->cutter_power);
2190
+        }
2188 2191
       #endif
2189 2192
 
2190 2193
       #if ENABLED(POWER_LOSS_RECOVERY)
@@ -2357,36 +2360,22 @@ uint32_t Stepper::block_phase_isr() {
2357 2360
         set_directions(current_block->direction_bits);
2358 2361
       }
2359 2362
 
2360
-      #if ENABLED(LASER_POWER_INLINE)
2361
-        const power_status_t stat = current_block->laser.status;
2362
-        #if ENABLED(LASER_POWER_INLINE_TRAPEZOID)
2363
-          laser_trap.enabled = stat.isPlanned && stat.isEnabled;
2364
-          laser_trap.cur_power = current_block->laser.power_entry; // RESET STATE
2365
-          laser_trap.cruise_set = false;
2366
-          #if DISABLED(LASER_POWER_INLINE_TRAPEZOID_CONT)
2367
-            laser_trap.last_step_count = 0;
2368
-            laser_trap.acc_step_count = current_block->laser.entry_per / 2;
2369
-          #else
2370
-            laser_trap.till_update = 0;
2371
-          #endif
2372
-          // Always have PWM in this case
2373
-          if (stat.isPlanned) {                        // Planner controls the laser
2374
-            cutter.ocr_set_power(
2375
-              stat.isEnabled ? laser_trap.cur_power : 0 // ON with power or OFF
2376
-            );
2377
-          }
2378
-        #else
2379
-          if (stat.isPlanned) {                        // Planner controls the laser
2380
-            #if ENABLED(SPINDLE_LASER_USE_PWM)
2381
-              cutter.ocr_set_power(
2382
-                stat.isEnabled ? current_block->laser.power : 0 // ON with power or OFF
2383
-              );
2363
+      #if ENABLED(LASER_FEATURE)
2364
+        if (cutter.cutter_mode == CUTTER_MODE_CONTINUOUS) {           // Planner controls the laser
2365
+          if (planner.laser_inline.status.isSyncPower)
2366
+            // If the previous block was a M3 sync power then skip the trap power init otherwise it will 0 the sync power.
2367
+            planner.laser_inline.status.isSyncPower = false;          // Clear the flag to process subsequent trap calc's.
2368
+          else if (current_block->laser.status.isEnabled) {
2369
+            #if ENABLED(LASER_POWER_TRAP)
2370
+              TERN_(DEBUG_LASER_TRAP, SERIAL_ECHO_MSG("InitTrapPwr:",current_block->laser.trap_ramp_active_pwr));
2371
+              cutter.apply_power(current_block->laser.status.isPowered ? current_block->laser.trap_ramp_active_pwr : 0);
2384 2372
             #else
2385
-              cutter.set_enabled(stat.isEnabled);
2373
+              TERN_(DEBUG_CUTTER_POWER, SERIAL_ECHO_MSG("InlinePwr:",current_block->laser.power));
2374
+              cutter.apply_power(current_block->laser.status.isPowered ? current_block->laser.power : 0);
2386 2375
             #endif
2387 2376
           }
2388
-        #endif
2389
-      #endif // LASER_POWER_INLINE
2377
+        }
2378
+      #endif // LASER_FEATURE
2390 2379
 
2391 2380
       // If the endstop is already pressed, endstop interrupts won't invoke
2392 2381
       // endstop_triggered and the move will grind. So check here for a
@@ -2416,21 +2405,6 @@ uint32_t Stepper::block_phase_isr() {
2416 2405
       // Calculate the initial timer interval
2417 2406
       interval = calc_timer_interval(current_block->initial_rate, &steps_per_isr);
2418 2407
     }
2419
-    #if ENABLED(LASER_POWER_INLINE_CONTINUOUS)
2420
-      else { // No new block found; so apply inline laser parameters
2421
-        // This should mean ending file with 'M5 I' will stop the laser; thus the inline flag isn't needed
2422
-        const power_status_t stat = planner.laser_inline.status;
2423
-        if (stat.isPlanned) {             // Planner controls the laser
2424
-          #if ENABLED(SPINDLE_LASER_USE_PWM)
2425
-            cutter.ocr_set_power(
2426
-              stat.isEnabled ? planner.laser_inline.power : 0 // ON with power or OFF
2427
-            );
2428
-          #else
2429
-            cutter.set_enabled(stat.isEnabled);
2430
-          #endif
2431
-        }
2432
-      }
2433
-    #endif
2434 2408
   }
2435 2409
 
2436 2410
   // Return the interval to wait

+ 0
- 19
Marlin/src/module/stepper.h View File

@@ -444,25 +444,6 @@ class Stepper {
444 444
     // Current stepper motor directions (+1 or -1)
445 445
     static xyze_int8_t count_direction;
446 446
 
447
-    #if ENABLED(LASER_POWER_INLINE_TRAPEZOID)
448
-
449
-      typedef struct {
450
-        bool enabled;       // Trapezoid needed flag (i.e., laser on, planner in control)
451
-        uint8_t cur_power;  // Current laser power
452
-        bool cruise_set;    // Power set up for cruising?
453
-
454
-        #if ENABLED(LASER_POWER_INLINE_TRAPEZOID_CONT)
455
-          uint16_t till_update;     // Countdown to the next update
456
-        #else
457
-          uint32_t last_step_count, // Step count from the last update
458
-                   acc_step_count;  // Bresenham counter for laser accel/decel
459
-        #endif
460
-      } stepper_laser_t;
461
-
462
-      static stepper_laser_t laser_trap;
463
-
464
-    #endif
465
-
466 447
   public:
467 448
     // Initialize stepper hardware
468 449
     static void init();

+ 3
- 2
Marlin/src/module/temperature.cpp View File

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

+ 2
- 2
buildroot/tests/mega2560 View File

@@ -189,7 +189,7 @@ opt_set MOTHERBOARD BOARD_RAMPS_14_EFB EXTRUDERS 0 LCD_LANGUAGE en TEMP_SENSOR_C
189 189
         AXIS_RELATIVE_MODES '{ false, false, false }'
190 190
 opt_enable REPRAP_DISCOUNT_FULL_GRAPHIC_SMART_CONTROLLER SDSUPPORT EEPROM_SETTINGS EEPROM_BOOT_SILENT EEPROM_AUTO_INIT MEATPACK_ON_SERIAL_PORT_1 \
191 191
            LASER_FEATURE LASER_SAFETY_TIMEOUT_MS LASER_COOLANT_FLOW_METER AIR_EVACUATION AIR_EVACUATION_PIN AIR_ASSIST AIR_ASSIST_PIN
192
-exec_test $1 $2 "MEGA2560 RAMPS | Laser Feature | Air Evacuation | Air Assist | Cooler | Flowmeter | 12864 LCD | meatpack | SERIAL_PORT_2 " "$3"
192
+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"
193 193
 
194 194
 #
195 195
 # 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
203 203
         AXIS_RELATIVE_MODES '{ false, false, false }'
204 204
 opt_enable REPRAP_DISCOUNT_SMART_CONTROLLER SDSUPPORT EEPROM_SETTINGS EEPROM_BOOT_SILENT EEPROM_AUTO_INIT PRINTCOUNTER I2C_AMMETER \
205 205
            LASER_FEATURE LASER_SAFETY_TIMEOUT_MS LASER_COOLANT_FLOW_METER AIR_EVACUATION AIR_EVACUATION_PIN AIR_ASSIST AIR_ASSIST_PIN
206
-exec_test $1 $2 "MEGA2560 RAMPS | Laser Feature | Air Evacuation | Air Assist | Cooler | Flowmeter | 44780 LCD " "$3"
206
+exec_test $1 $2 "MEGA2560 RAMPS | Laser Feature | Air Evacuation | Air Assist | Cooler | Laser Safety Timeout | Flowmeter | 44780 LCD " "$3"
207 207
 
208 208
 #
209 209
 # Test redundant temperature sensors + MAX TC

+ 137
- 0
docs/Cutter.md View File

@@ -0,0 +1,137 @@
1
+### Introduction
2
+
3
+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.
4
+
5
+### Architecture
6
+
7
+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.
8
+
9
+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.
10
+
11
+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.
12
+
13
+With dynamic inline power mode, F-Value feedrate sets are processed with cutter.calc_dynamic_power() and fed into the planner laser.power value.
14
+
15
+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.
16
+
17
+Note: Spindle operation is not selectable with G-Codes at this time.
18
+
19
+The following flow charts depict the flow control logic for spindle and laser operations in the code base.
20
+
21
+#### Spindle Mode Logic:
22
+
23
+                ┌──────────┐  ┌───────────┐  ┌───────────┐
24
+                │M3 S-Value│  │Dir !same ?│  │Stepper    │
25
+                │Spindle   │  │stop & wait│  │processes  │
26
+             ┌──┤Clockwise ├──┤ & start   ├──┤moves      │
27
+    ┌─────┐  │  │          │  │spindle    │  │           │
28
+    │GCode│  │  └──────────┘  └───────────┘  └───────────┘
29
+    │Send ├──┤  ┌──────────┐  ┌───────────┐  ┌───────────┐
30
+    └─────┘  │  │M4 S-Value│  │Dir !same ?│  │Stepper    │
31
+             ├──┤Spindle   ├──┤stop & wait├──┤processes  │
32
+             │  │Counter   │  │& start    │  │moves      │
33
+             │  │Clockwise │  │spindle    │  │           │
34
+             │  └──────────┘  └───────────┘  └───────────┘
35
+             │  ┌──────────┐  ┌────────┐
36
+             │  │M5        │  │Wait for│
37
+             │  │Spindle   ├──┤move &  │
38
+             └──┤Stop      │  │disable │
39
+                └──────────┘  └────────┘
40
+                ┌──────────┐  ┌──────────┐
41
+    Sensors─────┤Fault     ├──┤Disable   │
42
+                └──────────┘  │power     │
43
+                              └──────────┘
44
+
45
+#### Laser Mode Logic:
46
+
47
+                ┌──────────┐  ┌─────────────┐  ┌───────────┐
48
+                │M3,M4,M5 I│  │Set power    │  │Stepper    │
49
+             ┌──┤Standard  ├──┤Immediately &├──┤processes  │
50
+             │  │Default   │  │wait for move│  │moves      │
51
+             │  │          │  │completion   │  │           │
52
+             │  └──────────┘  └─────────────┘  └───────────┘
53
+             │  ┌──────────┐  ┌───────────┐  ┌───────────┐  ┌────────────┐  ┌────────────┐  ┌────────────┐  ┌───────────┐
54
+    ┌─────┐  │  │M3 I      │  │G0,G1,G2,G4│  │Planner    │  │Planner     │  │Planner fan │  │Planner     │  │Stepper    │
55
+    │GCode│  │  │Continuous│  │M3 receive │  │sets block │  │sync power ?│  │sync power ?│  │trap power ?│  │uses block │
56
+    │Send ├──┼──┤Inline    ├──┤power from ├──┤power using├──┤process M3  ├──┤process fan ├──┤adjusts for ├──┤values to  │
57
+    └─────┘  │  │          │  │S-Value    │  │Gx S-Value │  │power inline│  │power inline│  │accel/decel │  │apply power│
58
+             │  └──────────┘  └───────────┘  └───────────┘  └────────────┘  └────────────┘  └────────────┘  └───────────┘
59
+             │  ┌──────────┐  ┌───────────┐  ┌────────────────┐  ┌───────────┐
60
+             │  │M4 I      │  │Gx F-Value │  │Planner         │  │Stepper    │
61
+             │  │Dynamic   │  │set power  │  │Calc & set block│  │uses block │
62
+             └──┤Inline    ├──┤or use     ├──┤block power     ├──┤values to  │
63
+                │          │  │default    │  │using F-Value   │  │apply power│
64
+                └──────────┘  └───────────┘  └────────────────┘  └───────────┘
65
+                ┌──────────┐  ┌──────────┐
66
+    Sensors─────┤Fault     ├──┤Disable   │
67
+                └──────────┘  │Power     │
68
+                              └──────────┘
69
+
70
+<!-- https://asciiflow.com/#/ -->
71
+
72
+### Continuous Inline Trap Power Calculations
73
+
74
+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.
75
+
76
+When the laser.power > 0
77
+
78
+We set a minimum power if defined in SPEED_POWER_MIN it's fed into the planner block as laser_power_floor.
79
+
80
+A reduced entry laser power factor is based on the entry step rate to cruise step rate ratio for acceleration.
81
+
82
+    block entry laser power = laser power * ( entry step rate / cruise step rate )
83
+
84
+The initial power will be set to no less than the laser_power_floor or the inital power calculation.
85
+
86
+The reduced final power factor is based on the final step rate to cruise step rate ratio for deceleration.
87
+
88
+    block exit laser power = laser power * ( exit step rate / cruise step rate )
89
+
90
+Once the entry and exit power values are determined, the values are divided into step increments to be applied in the stepper.
91
+
92
+    trap step power incr_decr = ( cruize power - entry_exit ) / accel_decel_steps
93
+
94
+The trap steps are incremented or decremented during each accel or decel step until the block is complete.
95
+Step power is either cumulatively added or subtracted during trapeziod ramp progressions.
96
+
97
+#### Planner Code:
98
+
99
+   ```
100
+   if (block->laser.power > 0) {
101
+      NOLESS(block->laser.power, laser_power_floor);
102
+      block->laser.trap_ramp_active_pwr = (block->laser.power - laser_power_floor) * (initial_rate / float(block->nominal_rate)) + laser_power_floor;
103
+      block->laser.trap_ramp_entry_incr = (block->laser.power - block->laser.trap_ramp_active_pwr) / accelerate_steps;
104
+      float laser_pwr = block->laser.power * (final_rate / float(block->nominal_rate));
105
+      NOLESS(laser_pwr, laser_power_floor);
106
+      block->laser.trap_ramp_exit_decr = (block->laser.power - laser_pwr) / decelerate_steps;
107
+   ```
108
+
109
+#### Stepper Code:
110
+
111
+   ```
112
+   if (current_block->laser.trap_ramp_entry_incr > 0) {
113
+      cutter.apply_power(current_block->laser.trap_ramp_active_pwr);
114
+      current_block->laser.trap_ramp_active_pwr += current_block->laser.trap_ramp_entry_incr;
115
+   ```
116
+
117
+   ```
118
+   if (current_block->laser.trap_ramp_exit_decr > 0) {
119
+      current_block->laser.trap_ramp_active_pwr -= current_block->laser.trap_ramp_exit_decr;
120
+      cutter.apply_power(current_block->laser.trap_ramp_active_pwr);
121
+   ```
122
+
123
+### Dynamic Inline Calculations
124
+
125
+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.
126
+
127
+#### Spindle/Laser Code:
128
+
129
+```
130
+    // Dynamic mode rate calculation
131
+    static inline uint8_t calc_dynamic_power() {
132
+      if (feedrate_mm_m > 65535) return 255;         // Too fast, go always on
133
+      uint16_t rate = uint16_t(feedrate_mm_m);       // 16 bits from the G-code parser float input
134
+      rate >>= 8;                                    // Take the G-code input e.g. F40000 and shift off the lower bits to get an OCR value from 1-255
135
+      return uint8_t(rate);
136
+    }
137
+```

Loading…
Cancel
Save