This commit is contained in:
narno2202 2026-02-28 11:13:08 +01:00 committed by GitHub
commit f06f71bbeb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 9069 additions and 277 deletions

3770
Marlin/2Configuration.h Normal file

File diff suppressed because it is too large Load diff

4834
Marlin/2Configuration_adv.h Normal file

File diff suppressed because it is too large Load diff

View file

@ -1204,7 +1204,7 @@
#define FTM_SHAPING_ZETA_E 0.03f // Zeta used by input shapers for E axis
#define FTM_SHAPING_V_TOL_E 0.05f // Vibration tolerance used by EI input shapers for E axis
//#define FTM_RESONANCE_TEST // Sine sweep motion for resonance study
//#define RESONANCE_TEST // Sine sweep motion for resonance study
//#define FTM_SMOOTHING // Smoothing can reduce artifacts and make steppers quieter
// on sharp corners, but too much will round corners.

View file

@ -33,7 +33,7 @@
// Static data members
bool EmergencyParser::killed_by_M112, // = false
EmergencyParser::quickstop_by_M410,
#if ENABLED(FTM_RESONANCE_TEST)
#if ENABLED(RESONANCE_TEST)
EmergencyParser::rt_stop_by_M496, // = false
#endif
#if HAS_MEDIA
@ -151,7 +151,7 @@ void EmergencyParser::update(EmergencyParser::State &state, const uint8_t c) {
case EP_M4:
switch (c) {
case '1' :state = EP_M41; break;
#if ENABLED(FTM_RESONANCE_TEST)
#if ENABLED(RESONANCE_TEST)
case '9': state = EP_M49; break;
#endif
default: state = EP_IGNORE;
@ -160,7 +160,7 @@ void EmergencyParser::update(EmergencyParser::State &state, const uint8_t c) {
case EP_M41: state = (c == '0') ? EP_M410 : EP_IGNORE; break;
#if ENABLED(FTM_RESONANCE_TEST)
#if ENABLED(RESONANCE_TEST)
case EP_M49: state = (c == '6') ? EP_M496 : EP_IGNORE; break;
#endif
@ -209,7 +209,7 @@ void EmergencyParser::update(EmergencyParser::State &state, const uint8_t c) {
case EP_M108: marlin.end_waiting(); break;
case EP_M112: killed_by_M112 = true; break;
case EP_M410: quickstop_by_M410 = true; break;
#if ENABLED(FTM_RESONANCE_TEST)
#if ENABLED(RESONANCE_TEST)
case EP_M496: rt_stop_by_M496 = true; break;
#endif
#if ENABLED(EP_BABYSTEPPING)

View file

@ -43,7 +43,7 @@ public:
#if HAS_MEDIA
EP_M5, EP_M52, EP_M524,
#endif
#if ENABLED(FTM_RESONANCE_TEST)
#if ENABLED(RESONANCE_TEST)
EP_M49, EP_M496,
#endif
#if ENABLED(EP_BABYSTEPPING)
@ -67,7 +67,7 @@ public:
static bool killed_by_M112;
static bool quickstop_by_M410;
#if ENABLED(FTM_RESONANCE_TEST)
#if ENABLED(RESONANCE_TEST)
static bool rt_stop_by_M496;
#endif

View file

@ -0,0 +1,179 @@
/**
* Marlin 3D Printer Firmware
* Copyright (c) 2025 MarlinFirmware [https://github.com/MarlinFirmware/Marlin]
*
* Based on Sprinter and grbl.
* Copyright (c) 2011 Camiel Gubbels / Erik van der Zalm
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
#include "../../inc/MarlinConfigPre.h"
#if ENABLED(RESONANCE_TEST)
#if ENABLED(FT_MOTION)
#include "../../module/ft_motion.h"
#endif
#include "resonance_generator.h"
#include "../../gcode/gcode.h" // for home_all_axes
resonance_test_params_t ResonanceGenerator::rt_params; // Resonance test parameters
bool ResonanceGenerator::active = false; // Resonance test active
bool ResonanceGenerator::done = false; // Resonance test done
int32_t ResonanceGenerator::freq_to_phase_fp;
#if HAS_STANDARD_MOTION
block_t ResonanceGenerator::block;
#endif
ResonanceGenerator rtg;
ResonanceGenerator::ResonanceGenerator() {}
void ResonanceGenerator::start() {
gcode.home_all_axes(); // Always home axes first
motion.blocking_move_xy(X_CENTER, Y_CENTER, Z_CLEARANCE_FOR_HOMING);
rt_params.start_pos = motion.position;
active = true;
done = false;
// Clamp Z-axis acceleration for safety
if (rt_params.axis == Z_AXIS)
NOMORE(rt_params.accel_per_hz, 15.0f);
// Calculate time constant for sine sweep
const float rt_time = rt_params.octave_duration * (logf(RATIO) / logf(2.0f));
#if HAS_STANDARD_MOTION
if (TERN1(FT_MOTION, !ftMotion.cfg.active)) {
block.reset();
block.initial_rate = (rt_params.axis == Z_AXIS) ? 2000 : 8000;
}
#endif
// Precompute fixed-point sine sweep parameters
amplitude_precalc_fp = F2FP((rt_params.amplitude_correction * rt_params.accel_per_hz * 0.25f) / sq(M_PI));
current_freq_fp = F2FP(rt_params.min_freq);
freq_to_phase_fp = F2FP(2.0f * M_PI * rt_time);
max_freq_fp = F2FP(rt_params.max_freq);
phase_fp = 0;
}
void ResonanceGenerator::abort() {
reset();
#if(HAS_STANDARD_MOTION)
if (!TERN0(FT_MOTION, ftMotion.cfg.active))
return;
#endif
#if ENABLED(FT_MOTION)
ftMotion.reset();
#endif
}
void ResonanceGenerator::reset() {
rt_params = resonance_test_params_t();
#if HAS_STANDARD_MOTION
if (!TERN0(FT_MOTION, ftMotion.cfg.active))
block.reset();
#endif
active = false;
done = false;
}
float ResonanceGenerator::calc_next_pos() {
// Phase accumulation and wrapping within [0, 2π)
phase_fp += (int32_t)(((int64_t)current_freq_fp * freq_to_phase_fp) >> FP_BITS);
if (phase_fp >= M_TAU_FP) phase_fp -= M_TAU_FP;
else if (phase_fp < 0) phase_fp += M_TAU_FP;
// -π <= r_fp <= π
int32_t r_fp = (phase_fp > M_PI_FP) ? phase_fp - M_TAU_FP : phase_fp;
// Calculate windowing polynomial: 1.0 - 0.101321184 * r²
int32_t poly_fp = FP_ONE - ((C0101321184_FP * ((r_fp * r_fp) >> FP_BITS)) >> FP_BITS);
// Combine amplitude, phase, and polynomial and return new position
int32_t amplitude_fp = (int32_t)(((int64_t)amplitude_precalc_fp * FP_ONE) / current_freq_fp);
int32_t pos_fp = (int32_t)((((int64_t)amplitude_fp * r_fp) >> FP_BITS) * poly_fp) >> FP_BITS;
return FP2F(pos_fp);
}
#if ENABLED(FT_MOTION)
void ResonanceGenerator::fill_stepper_plan_buffer() {
xyze_pos_t traj_coords = rt_params.start_pos;
// Save starting position, avoid cumulative errors
const float start_pos = rt_params.start_pos[rt_params.axis];
while (!ftMotion.stepping.is_full()) {
// Calculate current frequency with exponential sweep
current_freq_fp += current_freq_fp >> 16;
if (current_freq_fp > max_freq_fp) {
done = true;
return;
}
// Resonate the axis being tested
traj_coords[rt_params.axis] = start_pos + calc_next_pos();
// Store in buffer
ftMotion.stepping_enqueue(traj_coords);
}
}
#endif
#if HAS_STANDARD_MOTION
block_t *ResonanceGenerator::generate_resonance_block() {
// Static variables to retain state between calls and avoid cumulative errors
static float prev_pos = 0.0f, step_accumulator = 0.0f;
const float step_mm = planner.settings.axis_steps_per_mm[rt_params.axis];
const uint8_t axis_bit = 1 << rt_params.axis;
// Calculate current frequency with exponential sweep
current_freq_fp += current_freq_fp >> 16;
if (current_freq_fp > max_freq_fp) {
done = true;
return nullptr;
}
// Calculate position and accumulate steps
float current_pos = calc_next_pos();
step_accumulator += (current_pos - prev_pos) * step_mm;
prev_pos = current_pos;
// Extract steps
int32_t delta_steps = (int32_t)floor(step_accumulator);
step_accumulator -= delta_steps;
uint32_t abs_steps = abs(delta_steps);
// Update block
block.steps[rt_params.axis] = abs_steps;
block.step_event_count = abs_steps;
if (delta_steps < 0)
block.direction_bits |= axis_bit;
else
block.direction_bits &= ~axis_bit;
return &block;
}
#endif
#endif // RESONANCE_TEST

View file

@ -25,11 +25,29 @@
#include <math.h>
#ifndef M_TAU
#define M_TAU (2.0f * M_PI)
#if HAS_STANDARD_MOTION
#include "../../module/planner.h"
#endif
typedef struct FTMResonanceTestParams {
// Fixed-point configuration
#define FP_BITS 16 // 16 bits for fractional part
#define FP_ONE (1UL << FP_BITS) // Fixed-point 1.0
// Convert float to fixed-point
#define F2FP(x) ((int32_t)((x) * FP_ONE + 0.5f))
// Convert fixed-point to float
#define FP2F(x) ((float)(x) / FP_ONE)
// Fixed-point constants
#define M_TAU_FP F2FP(2.0f * M_PI)
#define M_PI_FP F2FP(M_PI)
#define C0101321184_FP F2FP(0.101321184f)
// For rt_time calculation, perfect match between octave duration and frequency sweep
#define RATIO (1.0f + 1.0f / 65536.0f) // 1 + 1/2^16
typedef struct ResonanceTestParams {
AxisEnum axis = NO_AXIS_ENUM; // Axis to test
float min_freq = 5.0f; // Minimum frequency [Hz]
float max_freq = 100.0f; // Maximum frequency [Hz]
@ -37,27 +55,18 @@ typedef struct FTMResonanceTestParams {
float accel_per_hz = 60.0f; // Acceleration per Hz [mm/sec/Hz] or [g/Hz]
int16_t amplitude_correction = 5; // Amplitude correction factor
xyze_pos_t start_pos; // Initial stepper position
} ftm_resonance_test_params_t;
} resonance_test_params_t;
class ResonanceGenerator {
public:
static ftm_resonance_test_params_t rt_params; // Resonance test parameters
static float timeline; // Timeline Value to calculate resonance frequency
static resonance_test_params_t rt_params; // Resonance test parameters
float timeline; // Timeline Value to calculate resonance frequency
ResonanceGenerator();
void reset();
void start(const xyze_pos_t &spos, const float t) {
rt_params.start_pos = spos;
rt_time = t;
active = true;
done = false;
// Precompute frequency multiplier
current_freq = rt_params.min_freq;
const float inv_octave_duration = 1.0f / rt_params.octave_duration;
freq_mul = exp2f(FTM_TS * inv_octave_duration);
}
void start();
// Return frequency based on timeline
float getFrequencyFromTimeline() {
@ -65,7 +74,13 @@ class ResonanceGenerator {
return rt_params.min_freq * exp2f(timeline / rt_params.octave_duration);
}
void fill_stepper_plan_buffer(); // Fill stepper plan buffer with trajectory points
#if HAS_STANDARD_MOTION
block_t *generate_resonance_block(); // Generate planner block for standard motion
#endif
#if ENABLED(FT_MOTION)
void fill_stepper_plan_buffer(); // Fill stepper plan buffer with trajectory points
#endif
void setActive(const bool state) { active = state; }
bool isActive() const { return active; }
@ -76,10 +91,22 @@ class ResonanceGenerator {
void abort(); // Abort resonance test
private:
float fast_sin(float x); // Fast sine approximation
static float rt_time; // Test timer
float freq_mul; // Frequency multiplier for sine sweeping
float current_freq; // Current frequency being generated in sinusoidal motion
static bool active; // Resonance test active
static bool done; // Resonance test done
float calc_next_pos(); // Calculate next position point based on current frequency
// Fixed-point variables
int32_t amplitude_precalc_fp; // Fixed-point amplitude precalculation
int32_t current_freq_fp; // Fixed-point current frequency
// Phase variables (in radians, stored as fixed-point)
int32_t phase_fp; // Fixed-point phase accumulator
static int32_t freq_to_phase_fp; // Fixed-point frequency to phase conversion
int32_t max_freq_fp; // Fixed-point maximum frequency
#if HAS_STANDARD_MOTION
static block_t block;
#endif
static bool active; // Resonance test active
static bool done; // Resonance test done
};
extern ResonanceGenerator rtg;

View file

@ -22,15 +22,14 @@
#include "../../../inc/MarlinConfig.h"
#if ENABLED(FTM_RESONANCE_TEST)
#if ENABLED(RESONANCE_TEST)
#include "../../gcode.h"
#include "../../../lcd/marlinui.h"
#include "../../../module/ft_motion.h"
#include "../../../module/ft_motion/resonance_generator.h"
#include "../../../feature/resonance/resonance_generator.h"
void say_resonance_test() {
const ftm_resonance_test_params_t &p = ftMotion.rtg.rt_params;
const resonance_test_params_t &p = rtg.rt_params;
SERIAL_ECHO_START();
SERIAL_ECHOLN(F("M495 "), F("Resonance Test"));
SERIAL_ECHOLNPGM(" Axis: ", p.axis == NO_AXIS_ENUM ? C('-') : C(AXIS_CHAR(p.axis)));
@ -64,7 +63,7 @@ void say_resonance_test() {
void GcodeSuite::M495() {
if (!parser.seen_any()) return say_resonance_test();
ftm_resonance_test_params_t &p = ftMotion.rtg.rt_params;
resonance_test_params_t &p = rtg.rt_params;
const bool seenX = parser.seen_test('X'), seenY = parser.seen_test('Y'), seenZ = parser.seen_test('Z');
@ -137,8 +136,8 @@ void GcodeSuite::M495() {
if (parser.seenval('G')) {
const float val = parser.value_float();
if (WITHIN(val, 0, 100)) {
ftMotion.rtg.timeline = val;
SERIAL_ECHOLNPGM("Resonance Frequency set to ", ftMotion.rtg.getFrequencyFromTimeline(), " Hz");
rtg.timeline = val;
SERIAL_ECHOLNPGM("Resonance Frequency set to ", rtg.getFrequencyFromTimeline(), " Hz");
}
else {
SERIAL_ECHOLN(F("?Invalid "), F("Timeline value (0..100 s)"));
@ -146,23 +145,18 @@ void GcodeSuite::M495() {
}
if (parser.seen_test('S')) {
if (ftMotion.cfg.active) {
if (p.axis != NO_AXIS_ENUM) {
if (p.max_freq > p.min_freq) {
SERIAL_ECHOLN(F("Starting "), F("Resonance Test"));
ftMotion.start_resonance_test();
// The function returns immediately, the test runs in the background.
}
else {
SERIAL_ECHOLNPGM("?End Frequency must be greater than Start Frequency");
}
if (p.axis != NO_AXIS_ENUM) {
if (p.max_freq > p.min_freq) {
SERIAL_ECHOLN(F("Starting "), F("Resonance Test"));
rtg.start();
// The function returns immediately, the test runs in the background.
}
else {
SERIAL_ECHOLN(F("?Specify X, Y, or Z axis"), F(" first"));
SERIAL_ECHOLNPGM("?End Frequency must be greater than Start Frequency");
}
}
else {
SERIAL_ECHOLN(F("?Activate FT Motion to run the "), F("Resonance Test"));
SERIAL_ECHOLN(F("?Specify X, Y, or Z axis"), F(" first"));
}
}
}
@ -171,8 +165,8 @@ void GcodeSuite::M495() {
* M496: Abort the resonance test (via Emergency Parser)
*/
void GcodeSuite::M496() {
if (ftMotion.rtg.isActive()) {
ftMotion.rtg.abort();
if (rtg.isActive()) {
rtg.abort();
EmergencyParser::rt_stop_by_M496 = false;
ui.refresh();
#if DISABLED(MARLIN_SMALL_BUILD)
@ -185,4 +179,4 @@ void GcodeSuite::M496() {
#endif
}
#endif // FTM_RESONANCE_TEST
#endif // RESONANCE_TEST

View file

@ -924,7 +924,7 @@ void GcodeSuite::process_parsed_command(bool no_ok/*=false*/) {
#if ANY(FTM_SMOOTHING, FTM_POLYS)
case 494: M494(); break; // M494: Fixed-Time Motion extras
#endif
#if ENABLED(FTM_RESONANCE_TEST)
#if ENABLED(RESONANCE_TEST)
case 495: M495(); break; // M495: Resonance test for Input Shaping
case 496: M496(); break; // M496: Abort resonance test
#endif

View file

@ -252,8 +252,8 @@
* M485 - Send RS485 packets (Requires RS485_SERIAL_PORT)
* M486 - Identify and cancel objects. (Requires CANCEL_OBJECTS)
* M493 - Set / Report input FT Motion/Shaping parameters. (Requires FT_MOTION)
* M495 - Set / Start resonance test. (Requires FTM_RESONANCE_TEST)
* M496 - Abort resonance test. (Requires FTM_RESONANCE_TEST)
* M495 - Set / Start resonance test. (Requires RESONANCE_TEST)
* M496 - Abort resonance test. (Requires RESONANCE_TEST)
* M500 - Store parameters in EEPROM. (Requires EEPROM_SETTINGS)
* M501 - Restore parameters from EEPROM. (Requires EEPROM_SETTINGS)
* M502 - Revert to the default "factory settings". ** Does not write them to EEPROM! **
@ -1124,11 +1124,12 @@ private:
static void M493_report(const bool forReplay=true);
static void M494();
static void M494_report(const bool forReplay=true);
#if ENABLED(FTM_RESONANCE_TEST)
#endif
#if ENABLED(RESONANCE_TEST)
static void M495();
static void M495_report(const bool forReplay=true);
static void M496();
#endif
#endif
static void M500();

View file

@ -4511,8 +4511,8 @@ static_assert(_PLUS_TEST(3), "DEFAULT_MAX_ACCELERATION values must be positive."
static_assert(FTM_SMOOTHING_TIME_Z <= FTM_MAX_SMOOTHING_TIME, "FTM_SMOOTHING_TIME_Z must be <= FTM_MAX_SMOOTHING_TIME.");
static_assert(FTM_SMOOTHING_TIME_E <= FTM_MAX_SMOOTHING_TIME, "FTM_SMOOTHING_TIME_E must be <= FTM_MAX_SMOOTHING_TIME.");
#endif
#if ENABLED(FTM_RESONANCE_TEST) && DISABLED(EMERGENCY_PARSER)
#error "EMERGENCY_PARSER is required with FTM_RESONANCE_TEST (to cancel the test)."
#if ENABLED(RESONANCE_TEST) && DISABLED(EMERGENCY_PARSER)
#error "EMERGENCY_PARSER is required with RESONANCE_TEST (to cancel the test)."
#endif
#if !HAS_STANDARD_MOTION
#if ENABLED(SMOOTH_LIN_ADVANCE)

View file

@ -931,13 +931,13 @@ namespace LanguageNarrow_en {
LSTR MSG_FTM_SMOOTH_TIME_N = _UxGT("@ Smoothing Time");
LSTR MSG_FTM_POLY6_OVERSHOOT = _UxGT("@ Poly6 Overshoot");
LSTR MSG_FTM_CONFIGURE_AXIS_N = _UxGT("Configure @ Axis");
LSTR MSG_FTM_RESONANCE_TEST = _UxGT("Resonance Test");
LSTR MSG_FTM_RT_RUNNING = _UxGT("Res. Test Running...");
LSTR MSG_FTM_RT_START_N = _UxGT("Start @ Axis Test");
LSTR MSG_FTM_RT_STOP = _UxGT("Abort Test");
LSTR MSG_FTM_RETRIEVE_FREQ = _UxGT("Calc. Res. Freq.");
LSTR MSG_FTM_RESONANCE_FREQ = _UxGT("Resonance Freq.");
LSTR MSG_FTM_TIMELINE_FREQ = _UxGT("Timeline (s)");
LSTR MSG_RESONANCE_TEST = _UxGT("Resonance Test");
LSTR MSG_RT_RUNNING = _UxGT("Res. Test Running...");
LSTR MSG_RT_START_N = _UxGT("Start @ Axis Test");
LSTR MSG_RT_STOP = _UxGT("Abort Test");
LSTR MSG_RETRIEVE_FREQ = _UxGT("Calc. Res. Freq.");
LSTR MSG_RESONANCE_FREQ = _UxGT("Resonance Freq.");
LSTR MSG_TIMELINE_FREQ = _UxGT("Timeline (s)");
LSTR MSG_TOUCH_CALIBRATION = _UxGT("Touch Calibration");
LSTR MSG_MARLIN = _UxGT("Marlin");
LSTR MSG_PID_P = _UxGT("PID-P");
@ -1099,8 +1099,8 @@ namespace LanguageWide_en {
LSTR MSG_EEPROM_INITIALIZED = _UxGT("Default Settings Restored");
LSTR MSG_MEDIA_MENU_SD = _UxGT("Select from SD Card");
LSTR MSG_MEDIA_MENU_USB = _UxGT("Select from USB Drive");
LSTR MSG_FTM_RT_RUNNING = _UxGT("Resonance Test Running...");
LSTR MSG_FTM_RESONANCE_FREQ = _UxGT("Resonance frequency");
LSTR MSG_RT_RUNNING = _UxGT("Resonance Test Running...");
LSTR MSG_RESONANCE_FREQ = _UxGT("Resonance frequency");
LSTR MSG_HOMING_FEEDRATE_X = _UxGT("X Homing Feedrate");
LSTR MSG_HOMING_FEEDRATE_Y = _UxGT("Y Homing Feedrate");
LSTR MSG_HOMING_FEEDRATE_Z = _UxGT("Z Homing Feedrate");

View file

@ -51,7 +51,6 @@ namespace LanguageNarrow_it {
constexpr uint8_t CHARSIZE = 1;
LSTR LANGUAGE = _UxGT("Italiano");
LSTR WELCOME_MSG = MACHINE_NAME_SUBST _UxGT(" pronta."); // (MACHINE_NAME_SUBST) Ready.
LSTR MSG_MEDIA_INSERTED = MEDIA_TYPE_IT _UxGT(" inserita"); // (MEDIA_TYPE_EN) Inserted
LSTR MSG_MEDIA_REMOVED = MEDIA_TYPE_IT _UxGT(" rimossa"); // (MEDIA_TYPE_EN) Removed
@ -894,13 +893,13 @@ namespace LanguageNarrow_it {
LSTR MSG_FTM_VTOL_N = _UxGT("Livello vib. @"); // @ Vib. Level
LSTR MSG_FTM_SMOOTH_TIME_N = _UxGT("@ Tempo smorzamento"); // @ Smoothing Time
LSTR MSG_FTM_POLY6_OVERSHOOT = _UxGT("@ Overshoot Poly6"); // @ Poly6 Overshoot
LSTR MSG_FTM_RESONANCE_TEST = _UxGT("Test risonanza"); // Resonance Test
LSTR MSG_FTM_RT_RUNNING = _UxGT("Test ris.in corso..."); // Res. Test Running...
LSTR MSG_FTM_RT_START_N = _UxGT("Avvia Test Asse @"); // Start @ Axis Test
LSTR MSG_FTM_RT_STOP = _UxGT("Annulla Test"); // Abort Test
LSTR MSG_FTM_RETRIEVE_FREQ = _UxGT("Calc. Res. Freq."); // Calc. Res. Freq.
LSTR MSG_FTM_RESONANCE_FREQ = _UxGT("Freq.Risonanza"); // Resonance Freq.
LSTR MSG_FTM_TIMELINE_FREQ = _UxGT("Cronologia (s)"); // Timeline (s)
LSTR MSG_RESONANCE_TEST = _UxGT("Test risonanza"); // Resonance Test
LSTR MSG_RT_RUNNING = _UxGT("Test ris.in corso..."); // Res. Test Running...
LSTR MSG_RT_START_N = _UxGT("Avvia Test Asse @"); // Start @ Axis Test
LSTR MSG_RT_STOP = _UxGT("Annulla Test"); // Abort Test
LSTR MSG_RETRIEVE_FREQ = _UxGT("Calc. Res. Freq."); // Calc. Res. Freq.
LSTR MSG_RESONANCE_FREQ = _UxGT("Freq.Risonanza"); // Resonance Freq.
LSTR MSG_TIMELINE_FREQ = _UxGT("Cronologia (s)"); // Timeline (s)
LSTR MSG_TOUCH_CALIBRATION = _UxGT("Calibrazione touch"); // Touch Calibration
LSTR DGUS_MSG_NOT_WHILE_PRINTING = _UxGT("Non ammesso durante la stampa"); // Not allowed during print
LSTR DGUS_MSG_NOT_WHILE_IDLE = _UxGT("Non ammesso mentre è in riposo"); // Not allowed while idle

View file

@ -308,6 +308,46 @@ void menu_move() {
}
#endif
#if ENABLED(RESONANCE_TEST)
#include "../../feature/resonance/resonance_generator.h"
void menu_resonance_freq() {
START_MENU();
BACK_ITEM(MSG_RESONANCE_TEST);
STATIC_ITEM(MSG_RETRIEVE_FREQ);
EDIT_ITEM(float62, MSG_TIMELINE_FREQ, &rtg.timeline, 0.0f, 600.0f);
PSTRING_ITEM(MSG_RESONANCE_FREQ, ftostr53_63(rtg.getFrequencyFromTimeline()), SS_FULL);
END_MENU();
}
void menu_resonance_test() {
START_MENU();
BACK_ITEM(MSG_MOTION);
if (rtg.isActive() && !rtg.isDone()) {
STATIC_ITEM(MSG_RT_RUNNING);
GCODES_ITEM(MSG_RT_STOP, F("M496"));
}
else {
#if HAS_X_AXIS
GCODES_ITEM_N(X_AXIS, MSG_RT_START_N, F("M495 X S"));
#endif
#if HAS_Y_AXIS
GCODES_ITEM_N(Y_AXIS, MSG_RT_START_N, F("M495 Y S"));
#endif
#if HAS_Z_AXIS
GCODES_ITEM_N(Z_AXIS, MSG_RT_START_N, F("M495 Z S"));
#endif
SUBMENU(MSG_RETRIEVE_FREQ, menu_resonance_freq);
}
END_MENU();
}
#endif // RESONANCE_TEST
#if ENABLED(FT_MOTION_MENU)
#include "../../module/ft_motion.h"
@ -405,45 +445,6 @@ void menu_move() {
#endif // FTM_POLYS
#if ENABLED(FTM_RESONANCE_TEST)
void menu_ftm_resonance_freq() {
START_MENU();
BACK_ITEM(MSG_FTM_RESONANCE_TEST);
STATIC_ITEM(MSG_FTM_RETRIEVE_FREQ);
EDIT_ITEM(float62, MSG_FTM_TIMELINE_FREQ, &ftMotion.rtg.timeline, 0.0f, 600.0f);
PSTRING_ITEM(MSG_FTM_RESONANCE_FREQ, ftostr53_63(ftMotion.rtg.getFrequencyFromTimeline()), SS_FULL);
END_MENU();
}
void menu_ftm_resonance_test() {
START_MENU();
BACK_ITEM(MSG_FIXED_TIME_MOTION);
if (ftMotion.rtg.isActive() && !ftMotion.rtg.isDone()) {
STATIC_ITEM(MSG_FTM_RT_RUNNING);
GCODES_ITEM(MSG_FTM_RT_STOP, F("M496"));
}
else {
#if HAS_X_AXIS
GCODES_ITEM_N(X_AXIS, MSG_FTM_RT_START_N, F("M495 X S"));
#endif
#if HAS_Y_AXIS
GCODES_ITEM_N(Y_AXIS, MSG_FTM_RT_START_N, F("M495 Y S"));
#endif
#if HAS_Z_AXIS
GCODES_ITEM_N(Z_AXIS, MSG_FTM_RT_START_N, F("M495 Z S"));
#endif
SUBMENU(MSG_FTM_RETRIEVE_FREQ, menu_ftm_resonance_freq);
}
END_MENU();
}
#endif // FTM_RESONANCE_TEST
#if HAS_DYNAMIC_FREQ
void menu_ftm_dyn_mode() {
@ -558,10 +559,6 @@ void menu_move() {
EDIT_ITEM(bool, MSG_FTM_AXIS_SYNC, &editable.state, []{
queue.inject(TS(F("M493"), IAXIS_CHAR(MenuItemBase::itemIndex), 'T', int(editable.state)));
});
#if ENABLED(FTM_RESONANCE_TEST)
SUBMENU(MSG_FTM_RESONANCE_TEST, menu_ftm_resonance_test);
#endif
}
END_MENU();
@ -650,6 +647,13 @@ void menu_motion() {
SUBMENU(MSG_FIXED_TIME_MOTION, menu_ft_motion);
#endif
//
// M495 Resonance Test
//
#if ENABLED(RESONANCE_TEST)
SUBMENU(MSG_RESONANCE_TEST, menu_resonance_test);
#endif
//
// Pen up/down menu
//

View file

@ -37,10 +37,6 @@
#include "ft_motion/trajectory_poly5.h"
#include "ft_motion/trajectory_poly6.h"
#endif
#if ENABLED(FTM_RESONANCE_TEST)
#include "ft_motion/resonance_generator.h"
#include "../gcode/gcode.h" // for home_all_axes
#endif
#include "stepper.h" // Access stepper block queue function and abort status.
#include "endstops.h"
@ -89,11 +85,6 @@ TrapezoidalTrajectoryGenerator FTMotion::trapezoidalGenerator;
TrajectoryGenerator* FTMotion::currentGenerator = &FTMotion::trapezoidalGenerator;
#endif
// Resonance Test
#if ENABLED(FTM_RESONANCE_TEST)
ResonanceGenerator FTMotion::rtg; // Resonance trajectory generator instance
#endif
#if FTM_HAS_LIN_ADVANCE
bool FTMotion::use_advance_lead;
#endif
@ -167,9 +158,9 @@ void FTMotion::loop() {
* 4. Signal ready for new block.
*/
const bool using_resonance = TERN(FTM_RESONANCE_TEST, rtg.isActive(), false);
const bool using_resonance = TERN(RESONANCE_TEST, rtg.isActive(), false);
#if ENABLED(FTM_RESONANCE_TEST)
#if ENABLED(RESONANCE_TEST)
if (using_resonance) {
// Resonance Test has priority over normal ft_motion operation.
// Process resonance test if active. When it's done, generate the last data points for a clean ending.
@ -684,25 +675,4 @@ void FTMotion::fill_stepper_plan_buffer() {
}
}
#if ENABLED(FTM_RESONANCE_TEST)
// Start Resonance Testing
void FTMotion::start_resonance_test() {
motion.home_if_needed(); // Ensure known axes first
ftm_resonance_test_params_t &p = rtg.rt_params;
// Safe Acceleration per Hz for Z axis
if (p.axis == Z_AXIS && p.accel_per_hz > 15.0f)
p.accel_per_hz = 15.0f;
// Always move to the center of the bed
motion.blocking_move_xy(X_CENTER, Y_CENTER, Z_CLEARANCE_FOR_HOMING);
// Start test at the current position with the configured time-step
rtg.start(motion.position, FTM_TS);
}
#endif // FTM_RESONANCE_TEST
#endif // FT_MOTION

View file

@ -30,8 +30,8 @@
#include "ft_motion/trajectory_poly5.h"
#include "ft_motion/trajectory_poly6.h"
#endif
#if ENABLED(FTM_RESONANCE_TEST)
#include "ft_motion/resonance_generator.h"
#if ENABLED(RESONANCE_TEST)
#include "../feature/resonance/resonance_generator.h"
#endif
#if HAS_FTM_SHAPING
@ -238,10 +238,6 @@ typedef struct FTConfig {
*/
class FTMotion {
#if ENABLED(FTM_RESONANCE_TEST)
friend void ResonanceGenerator::fill_stepper_plan_buffer();
#endif
public:
// Public variables
@ -268,10 +264,6 @@ class FTMotion {
// Public methods
static void init();
static void loop(); // Controller main, to be invoked from non-isr task.
#if ENABLED(FTM_RESONANCE_TEST)
static void start_resonance_test(); // Start a resonance test with given parameters
static ResonanceGenerator rtg; // Resonance trajectory generator instance
#endif
#if ENABLED(FTM_SMOOTHING)
// Refresh alpha and delay samples used by smoothing functions.

View file

@ -1,112 +0,0 @@
/**
* Marlin 3D Printer Firmware
* Copyright (c) 2025 MarlinFirmware [https://github.com/MarlinFirmware/Marlin]
*
* Based on Sprinter and grbl.
* Copyright (c) 2011 Camiel Gubbels / Erik van der Zalm
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
#include "../../inc/MarlinConfigPre.h"
#if ENABLED(FTM_RESONANCE_TEST)
#include "../ft_motion.h"
#include "resonance_generator.h"
#include <math.h>
ftm_resonance_test_params_t ResonanceGenerator::rt_params; // Resonance test parameters
bool ResonanceGenerator::active = false; // Resonance test active
bool ResonanceGenerator::done = false; // Resonance test done
float ResonanceGenerator::rt_time = FTM_TS; // Resonance test timer
float ResonanceGenerator::timeline = 0.0f;
ResonanceGenerator::ResonanceGenerator() {}
void ResonanceGenerator::abort() {
reset();
ftMotion.reset();
}
void ResonanceGenerator::reset() {
rt_params = ftm_resonance_test_params_t();
rt_time = FTM_TS;
active = false;
done = false;
}
// Fast sine approximation
float ResonanceGenerator::fast_sin(float x) {
static constexpr float INV_TAU = (1.0f / M_TAU);
// Reduce the angle to [-π, π]
const float y = x * INV_TAU; // Multiples of 2π
int k = static_cast<int>(y); // Truncates toward zero
// Negative? The truncation is one too high.
if (y < 0.0f) --k; // Correct for negatives
float r = x - k * M_TAU; // -π <= r <= π
if (r > M_PI)
r -= M_TAU;
else if (r < -M_PI)
r += M_TAU;
// Cheap polynomial approximation of sin(r)
return r * (1.27323954f - 0.405284735f * ABS(r));
}
void ResonanceGenerator::fill_stepper_plan_buffer() {
xyze_float_t traj_coords = rt_params.start_pos;
const float amplitude_precalc = (rt_params.amplitude_correction * rt_params.accel_per_hz * 0.25f) / sq(M_PI);
float rt_factor = rt_time * M_TAU;
while (!ftMotion.stepping.is_full()) {
// Calculate current frequency
current_freq *= freq_mul;
if (current_freq > rt_params.max_freq) {
done = true;
return;
}
// Amplitude based on a sinusoidal wave : A = accel / (4 * PI^2 * f^2)
//const float accel_magnitude = rt_params.accel_per_hz * freq;
//const float amplitude = rt_params.amplitude_correction * accel_magnitude / (4.0f * sq(M_PI) * sq(freq));
const float amplitude = amplitude_precalc / current_freq;
// Phase in radians
const float phase = current_freq * rt_factor;
// Position Offset : between -A and +A
const float pos_offset = amplitude * fast_sin(phase);
// Resonate the axis being tested
traj_coords[rt_params.axis] = rt_params.start_pos[rt_params.axis] + pos_offset;
// Increment for the next point (before calling out)
rt_time += FTM_TS;
rt_factor += FTM_TS * M_TAU;
// Store in buffer
ftMotion.stepping_enqueue(traj_coords);
}
}
#endif // FTM_RESONANCE_TEST

View file

@ -74,5 +74,5 @@ protected:
*/
enum class TrajectoryType : uint8_t {
TRAPEZOIDAL, POLY5, POLY6
OPTARG(FTM_RESONANCE_TEST, RESONANCE)
OPTARG(RESONANCE_TEST, RESONANCE)
};

View file

@ -81,6 +81,10 @@ Stepper stepper; // Singleton
#include "ft_motion.h"
#endif
#if (ENABLED(RESONANCE_TEST) && HAS_STANDARD_MOTION)
#include "../feature/resonance/resonance_generator.h"
#endif
#include "../lcd/marlinui.h"
#include "../gcode/queue.h"
#include "../sd/cardreader.h"
@ -618,6 +622,117 @@ bool Stepper::disable_axis(const AxisEnum axis) {
return can_disable;
}
#if (ENABLED(RESONANCE_TEST) && HAS_STANDARD_MOTION)
hal_timer_t Stepper::resonance_block_phase_isr() {
const hal_timer_t time_spent = HAL_timer_get_count(MF_TIMER_STEP);
#if MULTISTEPPING_LIMIT > 1
if (steps_per_isr > 1 && time_spent_out_isr >= time_spent_in_isr + time_spent)
steps_per_isr >>= 1;
#endif
time_spent_in_isr = -time_spent; // Unsigned but guaranteed to be +ve when needed
time_spent_out_isr = 0;
hal_timer_t interval = 0;
// If current block is not finished, continue with it
if (step_events_completed < step_event_count)
return calc_multistep_timer_interval(current_block->initial_rate);
// Current block is finished, reset pointer
current_block = nullptr;
// Generate a new block
if ((current_block = rtg.generate_resonance_block())) {
// Apply direction
DIR_WAIT_BEFORE();
const uint8_t axis = rtg.rt_params.axis;
const bool fwd = current_block->direction_bits[axis];
switch (axis) {
case X_AXIS: X_APPLY_DIR(fwd, false); break;
case Y_AXIS: Y_APPLY_DIR(fwd, false); break;
case Z_AXIS: Z_APPLY_DIR(fwd, false); break;
}
step_event_count = current_block->step_event_count;
step_events_completed = 0;
interval = calc_multistep_timer_interval(current_block->initial_rate);
}
else
rtg.abort();
return interval;
}
void Stepper::resonance_pulse_phase_isr() {
// If there is no current block, do nothing
if (!current_block || step_events_completed >= step_event_count) return;
// Count of pending loops and events for this iteration
const uint32_t pending_events = step_event_count - step_events_completed;
uint8_t events_to_do = _MIN(pending_events, steps_per_isr);
// Just update the value we will get at the end of the loop
step_events_completed += events_to_do;
#define RESONANCE_STEP_SEQUENCE(A) do { \
A##_APPLY_STEP(STEP_STATE_##A, false); \
START_TIMED_PULSE(); \
AWAIT_HIGH_PULSE(); \
A##_APPLY_STEP(!STEP_STATE_##A, false); \
} while(0)
USING_TIMED_PULSE();
const uint8_t axis = rtg.rt_params.axis;
switch (axis) {
case X_AXIS:
#if ISR_MULTI_STEPS
RESONANCE_STEP_SEQUENCE(X);
while (--events_to_do) {
AWAIT_LOW_PULSE();
RESONANCE_STEP_SEQUENCE(X);
}
#else
do {
RESONANCE_STEP_SEQUENCE(X);
} while (--events_to_do);
#endif
break;
case Y_AXIS:
#if ISR_MULTI_STEPS
RESONANCE_STEP_SEQUENCE(Y);
while (--events_to_do) {
AWAIT_LOW_PULSE();
RESONANCE_STEP_SEQUENCE(Y);
}
#else
do {
RESONANCE_STEP_SEQUENCE(Y);
} while (--events_to_do);
#endif
break;
case Z_AXIS:
#if ISR_MULTI_STEPS
RESONANCE_STEP_SEQUENCE(Z);
while (--events_to_do) {
AWAIT_LOW_PULSE();
RESONANCE_STEP_SEQUENCE(Z);
}
#else
do {
RESONANCE_STEP_SEQUENCE(Z);
} while (--events_to_do);
#endif
break;
}
}
#endif
#if HAS_EXTRUDERS
void Stepper::enable_extruder(E_TERN_(const uint8_t eindex)) {
@ -1646,6 +1761,19 @@ void Stepper::isr() {
#if HAS_STANDARD_MOTION
if (!using_ftMotion) {
#if ENABLED(RESONANCE_TEST)
if (rtg.isActive()) {
if (!nextMainISR) resonance_pulse_phase_isr();
hal.isr_on();
if (!nextMainISR) nextMainISR = resonance_block_phase_isr();
interval = hal_timer_t(STEPPER_TIMER_RATE * 0.03); // Max wait of 30ms regardless of stepper timer frequency
NOMORE(interval, nextMainISR); // Time until the next Pulse / Block phase
nextMainISR -= interval;
}
else {
#endif
TERN_(HAS_ZV_SHAPING, shaping_isr()); // Do Shaper stepping, if needed
@ -1709,6 +1837,7 @@ void Stepper::isr() {
TERN_(BABYSTEPPING, if (nextBabystepISR != BABYSTEP_NEVER) nextBabystepISR -= interval);
}
TERN_(RESONANCE_TEST, })
#endif // HAS_STANDARD_MOTION

View file

@ -579,6 +579,11 @@ class Stepper {
static hal_timer_t block_phase_isr();
#endif
#if (ENABLED(RESONANCE_TEST) && HAS_STANDARD_MOTION)
static void resonance_pulse_phase_isr();
static hal_timer_t resonance_block_phase_isr();
#endif
#if HAS_ZV_SHAPING
static void shaping_isr();
#endif

View file

@ -18,7 +18,7 @@ opt_set MOTHERBOARD BOARD_I3DBEEZ9_V1 SERIAL_PORT -1 \
EXTRUDERS 3 TEMP_SENSOR_1 1 TEMP_SENSOR_2 1 \
E0_AUTO_FAN_PIN PC10 E1_AUTO_FAN_PIN PC11 E2_AUTO_FAN_PIN PC12 \
X_DRIVER_TYPE TMC2209 Y_DRIVER_TYPE TMC2130
opt_enable FT_MOTION FTM_SMOOTHING FTM_HOME_AND_PROBE FTM_RESONANCE_TEST FT_MOTION_MENU \
opt_enable FT_MOTION FTM_SMOOTHING FTM_HOME_AND_PROBE RESONANCE_TEST FT_MOTION_MENU \
REPRAP_DISCOUNT_SMART_CONTROLLER EMERGENCY_PARSER EEPROM_SETTINGS \
BLTOUCH AUTO_BED_LEVELING_3POINT Z_SAFE_HOMING PINS_DEBUGGING
opt_disable FTM_SHAPER_ZVDDD FTM_SHAPER_MZV

View file

@ -15,7 +15,7 @@ opt_set MOTHERBOARD BOARD_BTT_SKR_MINI_E3_V1_0 SERIAL_PORT 1 SERIAL_PORT_2 -1 \
X_CURRENT_HOME X_CURRENT/2 Y_CURRENT_HOME Y_CURRENT/2 Z_CURRENT_HOME Y_CURRENT/2
opt_enable CR10_STOCKDISPLAY EMERGENCY_PARSER Z_IDLE_HEIGHT EDITABLE_HOMING_CURRENT \
INPUT_SHAPING_X INPUT_SHAPING_Y \
FT_MOTION FT_MOTION_MENU FTM_RESONANCE_TEST \
FT_MOTION FT_MOTION_MENU RESONANCE_TEST \
BIQU_MICROPROBE_V1 PROBE_ENABLE_DISABLE Z_SAFE_HOMING AUTO_BED_LEVELING_BILINEAR \
ADAPTIVE_STEP_SMOOTHING LIN_ADVANCE SMOOTH_LIN_ADVANCE NONLINEAR_EXTRUSION \
PINS_DEBUGGING

View file

@ -313,7 +313,7 @@ PLATFORM_M997_SUPPORT = build_src_filter=+<src/gcode/control/M9
HAS_TOOLCHANGE = build_src_filter=+<src/gcode/control/T.cpp>
FT_MOTION = build_src_filter=+<src/module/ft_motion.cpp> +<src/module/ft_motion> -<src/module/ft_motion/smoothing.cpp> +<src/gcode/feature/ft_motion> -<src/gcode/feature/ft_motion/M495_M496.cpp> -<src/module/ft_motion/trajectory_poly6.cpp>
FTM_SMOOTHING = build_src_filter=+<src/module/ft_motion/smoothing.cpp>
FTM_RESONANCE_TEST = build_src_filter=+<src/gcode/feature/ft_motion/M495_M496.cpp>
RESONANCE_TEST = build_src_filter=+<src/feature/resonance> +<src/gcode/feature/resonance>
FTM_POLYS = build_src_filter=+<src/module/ft_motion/trajectory_poly6.cpp>
HAS_LIN_ADVANCE_K = build_src_filter=+<src/gcode/feature/advance>
PHOTO_GCODE = build_src_filter=+<src/gcode/feature/camera>