diff --git a/docs/Config_Changes.md b/docs/Config_Changes.md index ecac5a7aa..a73a0c37f 100644 --- a/docs/Config_Changes.md +++ b/docs/Config_Changes.md @@ -8,6 +8,14 @@ All dates in this document are approximate. ## Changes +20241203: The resonance test has been changed to include slow sweeping +moves. This change requires that testing point(s) have some clearance +in X/Y plane (+/- 30 mm from the test point should suffice when using +the default settings). The new test should generally produce more +accurate and reliable test results. However, if required, the previous +test behavior can be restored by adding options `sweeping_period: 0` and +`accel_per_hz: 75` to the `[resonance_tester]` config section. + 20241201: In some cases Klipper may have ignored leading characters or spaces in a traditional G-Code command. For example, "99M123" may have been interpreted as "M123" and "M 321" may have been interpreted as diff --git a/docs/Config_Reference.md b/docs/Config_Reference.md index 9e3f71685..0b44aaa4a 100644 --- a/docs/Config_Reference.md +++ b/docs/Config_Reference.md @@ -1790,11 +1790,14 @@ section of the measuring resonances guide for more information on # auto-calibration (with 'SHAPER_CALIBRATE' command). By default no # maximum smoothing is specified. Refer to Measuring_Resonances guide # for more details on using this feature. +#move_speed: 50 +# The speed (in mm/s) to move the toolhead to and between test points +# during the calibration. The default is 50. #min_freq: 5 # Minimum frequency to test for resonances. The default is 5 Hz. #max_freq: 133.33 # Maximum frequency to test for resonances. The default is 133.33 Hz. -#accel_per_hz: 75 +#accel_per_hz: 60 # This parameter is used to determine which acceleration to use to # test a specific frequency: accel = accel_per_hz * freq. Higher the # value, the higher is the energy of the oscillations. Can be set to @@ -1808,6 +1811,13 @@ section of the measuring resonances guide for more information on # hz_per_sec. Small values make the test slow, and the large values # will decrease the precision of the test. The default value is 1.0 # (Hz/sec == sec^-2). +#sweeping_accel: 400 +# An acceleration of slow sweeping moves. The default is 400 mm/sec^2. +#sweeping_period: 1.2 +# A period of slow sweeping moves. Setting this parameter to 0 +# disables slow sweeping moves. Avoid setting it to a too small +# non-zero value in order to not poison the measurements. +# The default is 1.2 sec which is a good all-round choice. ``` ## Config file helpers @@ -5045,8 +5055,9 @@ serial: ### [angle] Magnetic hall angle sensor support for reading stepper motor angle -shaft measurements using a1333, as5047d, or tle5012b SPI chips. The -measurements are available via the [API Server](API_Server.md) and +shaft measurements using a1333, as5047d, mt6816, mt6826s, +or tle5012b SPI chips. +The measurements are available via the [API Server](API_Server.md) and [motion analysis tool](Debugging.md#motion-analysis-and-data-logging). See the [G-Code reference](G-Codes.md#angle) for available commands. @@ -5054,7 +5065,7 @@ See the [G-Code reference](G-Codes.md#angle) for available commands. [angle my_angle_sensor] sensor_type: # The type of the magnetic hall sensor chip. Available choices are -# "a1333", "as5047d", and "tle5012b". This parameter must be +# "a1333", "as5047d", "mt6816", "mt6826s", and "tle5012b". This parameter must be # specified. #sample_period: 0.000400 # The query period (in seconds) to use during measurements. The diff --git a/docs/G-Codes.md b/docs/G-Codes.md index 7c90417ca..d44017dea 100644 --- a/docs/G-Codes.md +++ b/docs/G-Codes.md @@ -127,6 +127,14 @@ use this tool the Python "numpy" package must be installed (see the [measuring resonance document](Measuring_Resonances.md#software-installation) for more information). +#### ANGLE_CHIP_CALIBRATE +`ANGLE_CHIP_CALIBRATE CHIP=`: Perform internal sensor calibration, +if implemented (MT6826S/MT6835). + +- **MT68XX**: The motor should be disconnected +from any printer carriage before performing calibration. +After calibration, the sensor should be reset by disconnecting the power. + #### ANGLE_DEBUG_READ `ANGLE_DEBUG_READ CHIP= REG=`: Queries sensor register "register" (e.g. 44 or 0x2C). Can be useful for debugging @@ -1059,6 +1067,21 @@ CYCLE_TIME parameter is not stored between SET_PIN commands (any SET_PIN command without an explicit CYCLE_TIME parameter will use the `cycle_time` specified in the pwm_cycle_time config section). +### [quad_gantry_level] + +The following commands are available when the +[quad_gantry_level config section](Config_Reference.md#quad_gantry_level) +is enabled. + +#### QUAD_GANTRY_LEVEL +`QUAD_GANTRY_LEVEL [RETRIES=] [RETRY_TOLERANCE=] +[HORIZONTAL_MOVE_Z=] [=]`: This command +will probe the points specified in the config and then make +independent adjustments to each Z stepper to compensate for tilt. See +the PROBE command for details on the optional probe parameters. The +optional `RETRIES`, `RETRY_TOLERANCE`, and `HORIZONTAL_MOVE_Z` values +override those options specified in the config file. + ### [query_adc] The query_adc module is automatically loaded. @@ -1448,11 +1471,13 @@ The following commands are available when the [z_tilt config section](Config_Reference.md#z_tilt) is enabled. #### Z_TILT_ADJUST -`Z_TILT_ADJUST [HORIZONTAL_MOVE_Z=] [=]`: This -command will probe the points specified in the config and then make independent -adjustments to each Z stepper to compensate for tilt. See the PROBE command for -details on the optional probe parameters. The optional `HORIZONTAL_MOVE_Z` -value overrides the `horizontal_move_z` option specified in the config file. +`Z_TILT_ADJUST [RETRIES=] [RETRY_TOLERANCE=] +[HORIZONTAL_MOVE_Z=] [=]`: This command +will probe the points specified in the config and then make +independent adjustments to each Z stepper to compensate for tilt. See +the PROBE command for details on the optional probe parameters. The +optional `RETRIES`, `RETRY_TOLERANCE`, and `HORIZONTAL_MOVE_Z` values +override those options specified in the config file. ### [temperature_probe] diff --git a/docs/Measuring_Resonances.md b/docs/Measuring_Resonances.md index 6c58409bf..d6aaf5671 100644 --- a/docs/Measuring_Resonances.md +++ b/docs/Measuring_Resonances.md @@ -694,6 +694,24 @@ If you are doing a shaper re-calibration and the reported smoothing for the suggested shaper configuration is almost the same as what you got during the previous calibration, this step can be skipped. +### Unreliable measurements of resonance frequencies + +Sometimes the resonance measurements can produce bogus results, leading to +the incorrect suggestions for the input shapers. This can be caused by a +variety of reasons, including running fans on the toolhead, incorrect +position or non-rigid mounting of the accelerometer, or mechanical problems +such as loose belts or binding or bumpy axis. Keep in mind that all fans +should be disabled for resonance testing, especially the noisy ones, and +that the accelerometer should be rigidly mounted on the corresponding +moving part (e.g. on the bed itself for the bed slinger, or on the extruder +of the printer itself and not the carriage, and some people get better +results by mounting the accelerometer on the nozzle itself). As for +mechanical problems, the user should inspect if there is any fault that +can be fixed with a moving axis (e.g. linear guide rails cleaned up and +lubricated and V-slot wheels tension adjusted correctly). If none of that +helps, a user may try the other shapers from the produced list besides the +one recommended by default. + ### Testing custom axes `TEST_RESONANCES` command supports custom axes. While this is not really diff --git a/klippy/extras/angle.py b/klippy/extras/angle.py index c51d8bf06..3b1f323e9 100644 --- a/klippy/extras/angle.py +++ b/klippy/extras/angle.py @@ -411,6 +411,196 @@ class HelperTLE5012B: parser=lambda x: int(x, 0)) self._write_reg(reg, val) +class HelperMT6816: + SPI_MODE = 3 + SPI_SPEED = 10000000 + def __init__(self, config, spi, oid): + self.printer = config.get_printer() + self.spi = spi + self.oid = oid + self.mcu = spi.get_mcu() + self.mcu.register_config_callback(self._build_config) + self.spi_angle_transfer_cmd = None + self.is_tcode_absolute = False + self.last_temperature = None + name = config.get_name().split()[-1] + gcode = self.printer.lookup_object("gcode") + gcode.register_mux_command("ANGLE_DEBUG_READ", "CHIP", name, + self.cmd_ANGLE_DEBUG_READ, + desc=self.cmd_ANGLE_DEBUG_READ_help) + def _build_config(self): + cmdqueue = self.spi.get_command_queue() + self.spi_angle_transfer_cmd = self.mcu.lookup_query_command( + "spi_angle_transfer oid=%c data=%*s", + "spi_angle_transfer_response oid=%c clock=%u response=%*s", + oid=self.oid, cq=cmdqueue) + def _send_spi(self, msg): + return self.spi.spi_transfer(msg) + def get_static_delay(self): + return .000001 + def _read_reg(self, reg): + msg = [reg, 0, 0] + params = self._send_spi(msg) + resp = bytearray(params['response']) + val = (resp[1] << 8) | resp[2] + return val + def start(self): + pass + cmd_ANGLE_DEBUG_READ_help = "Query low-level angle sensor register" + def cmd_ANGLE_DEBUG_READ(self, gcmd): + reg = 0x83 + val = self._read_reg(reg) + gcmd.respond_info("ANGLE REG[0x%02x] = 0x%04x" % (reg, val)) + angle = val >> 2 + parity = bin(val >> 1).count("1") % 2 + gcmd.respond_info("Angle %i ~ %.2f" % (angle, angle * 360 / (1 << 14))) + gcmd.respond_info("No Mag: %i" % (val >> 1 & 0x1)) + gcmd.respond_info("Parity: %i == %i" % (parity, val & 0x1)) + +class HelperMT6826S: + SPI_MODE = 3 + SPI_SPEED = 10000000 + def __init__(self, config, spi, oid): + self.printer = config.get_printer() + self.stepper_name = config.get('stepper', None) + self.spi = spi + self.oid = oid + self.mcu = spi.get_mcu() + self.mcu.register_config_callback(self._build_config) + self.spi_angle_transfer_cmd = None + self.is_tcode_absolute = False + self.last_temperature = None + name = config.get_name().split()[-1] + gcode = self.printer.lookup_object("gcode") + gcode.register_mux_command("ANGLE_DEBUG_READ", "CHIP", name, + self.cmd_ANGLE_DEBUG_READ, + desc=self.cmd_ANGLE_DEBUG_READ_help) + gcode.register_mux_command("ANGLE_CHIP_CALIBRATE", "CHIP", name, + self.cmd_ANGLE_CHIP_CALIBRATE, + desc=self.cmd_ANGLE_CHIP_CALIBRATE_help) + self.status_map = { + 0: "No Calibration", + 1: "Running Calibration", + 2: "Calibration Failed", + 3: "Calibration Successful" + } + def _build_config(self): + cmdqueue = self.spi.get_command_queue() + self.spi_angle_transfer_cmd = self.mcu.lookup_query_command( + "spi_angle_transfer oid=%c data=%*s", + "spi_angle_transfer_response oid=%c clock=%u response=%*s", + oid=self.oid, cq=cmdqueue) + def _send_spi(self, msg): + params = self.spi.spi_transfer(msg) + return params + def get_static_delay(self): + return .00001 + def _read_reg(self, reg): + reg = 0x3000 | reg + msg = [reg >> 8, reg & 0xff, 0] + params = self._send_spi(msg) + resp = bytearray(params['response']) + return resp[2] + def _write_reg(self, reg, data): + reg = 0x6000 | reg + msg = [reg >> 8, reg & 0xff, data] + self._send_spi(msg) + def crc8(self, data): + polynomial = 0x07 + crc = 0x00 + for byte in data: + crc ^= byte + for _ in range(8): + if crc & 0x80: + crc = (crc << 1) ^ polynomial + else: + crc <<= 1 + crc &= 0xFF + return crc + def _read_angle(self, reg): + reg = 0x3000 | reg + msg = [reg >> 8, reg & 0xff, 0, 0, 0, 0] + params = self._send_spi(msg) + resp = bytearray(params['response']) + angle = (resp[2] << 7) | (resp[3] >> 1) + status = resp[4] + crc_computed = self.crc8([resp[2], resp[3], resp[4]]) + crc = resp[5] + return angle, status, crc, crc_computed + def start(self): + val = self._read_reg(0x00d) + # Set histeresis to 0.003 degree + self._write_reg(0x00d, (val & 0xf8) | 0x5) + def get_microsteps(self): + configfile = self.printer.lookup_object('configfile') + sconfig = configfile.get_status(None)['settings'] + stconfig = sconfig.get(self.stepper_name, {}) + microsteps = stconfig['microsteps'] + full_steps = stconfig['full_steps_per_rotation'] + return microsteps, full_steps + cmd_ANGLE_CHIP_CALIBRATE_help = "Run MT6826s calibration sequence" + def cmd_ANGLE_CHIP_CALIBRATE(self, gcmd): + fmove = self.printer.lookup_object('force_move') + mcu_stepper = fmove.lookup_stepper(self.stepper_name) + if self.stepper_name is None: + gcmd.respond_info("stepper not defined") + return + + gcmd.respond_info("MT6826S Run calibration sequence") + gcmd.respond_info("Motor will do 18+ rotations -" + + " ensure pulley is disconnected") + req_freq = self._read_reg(0x00e) >> 4 & 0x7 + # Minimal calibration speed + rpm = (3200 >> req_freq) + 1 + rps = rpm / 60 + move = fmove.manual_move + # Move stepper several turns (to allow internal sensor calibration) + microsteps, full_steps = self.get_microsteps() + step_dist = mcu_stepper.get_step_dist() + full_step_dist = step_dist * microsteps + rotation_dist = full_steps * full_step_dist + move(mcu_stepper, 2 * rotation_dist, rps * rotation_dist) + self._write_reg(0x155, 0x5e) + move(mcu_stepper, 20 * rotation_dist, rps * rotation_dist) + val = self._read_reg(0x113) + code = val >> 6 + gcmd.respond_info("Status: %s" % (self.status_map[code])) + while code == 1: + move(mcu_stepper, 5 * rotation_dist, rps * rotation_dist) + val = self._read_reg(0x113) + code = val >> 6 + gcmd.respond_info("Status: %s" % (self.status_map[code])) + if code == 2: + gcmd.respond_info("Calibration failed") + if code == 3: + gcmd.respond_info("Calibration success, please poweroff sensor") + cmd_ANGLE_DEBUG_READ_help = "Query low-level angle sensor register" + def cmd_ANGLE_DEBUG_READ(self, gcmd): + reg = gcmd.get("REG", minval=0, maxval=0x155, + parser=lambda x: int(x, 0)) + if reg == 0x003: + angle, status, crc1, crc2 = self._read_angle(reg) + gcmd.respond_info("ANGLE REG[0x003] = 0x%02x" % + (angle >> 7)) + gcmd.respond_info("ANGLE REG[0x004] = 0x%02x" % + ((angle << 1) & 0xff)) + gcmd.respond_info("Angle %i ~ %.2f" % (angle, + angle * 360 / (1 << 15))) + gcmd.respond_info("Weak Mag: %i" % (status >> 1 & 0x1)) + gcmd.respond_info("Under Voltage: %i" % (status >> 2 & 0x1)) + gcmd.respond_info("CRC: 0x%02x == 0x%02x" % (crc1, crc2)) + elif reg == 0x00e: + val = self._read_reg(reg) + gcmd.respond_info("GPIO_DS = %i" % (val >> 7)) + gcmd.respond_info("AUTOCAL_FREQ = %i" % (val >> 4 & 0x7)) + elif reg == 0x113: + val = self._read_reg(reg) + gcmd.respond_info("Status: %s" % (self.cal_status[val >> 6])) + else: + val = self._read_reg(reg) + gcmd.respond_info("REG[0x%04x] = 0x%02x" % (reg, val)) + + BYTES_PER_SAMPLE = 3 SAMPLES_PER_BLOCK = bulk_sensor.MAX_BULK_MSG_SIZE // BYTES_PER_SAMPLE @@ -427,8 +617,11 @@ class Angle: self.start_clock = self.time_shift = self.sample_ticks = 0 self.last_sequence = self.last_angle = 0 # Sensor type - sensors = { "a1333": HelperA1333, "as5047d": HelperAS5047D, - "tle5012b": HelperTLE5012B } + sensors = { "a1333": HelperA1333, + "as5047d": HelperAS5047D, + "tle5012b": HelperTLE5012B, + "mt6816": HelperMT6816, + "mt6826s": HelperMT6826S } sensor_type = config.getchoice('sensor_type', {s: s for s in sensors}) sensor_class = sensors[sensor_type] self.spi = bus.MCU_SPI_from_config(config, sensor_class.SPI_MODE, diff --git a/klippy/extras/resonance_tester.py b/klippy/extras/resonance_tester.py index e9d4e9d92..76e56f536 100644 --- a/klippy/extras/resonance_tester.py +++ b/klippy/extras/resonance_tester.py @@ -45,42 +45,96 @@ def _parse_axis(gcmd, raw_axis): "Unable to parse axis direction '%s'" % (raw_axis,)) return TestAxis(vib_dir=(dir_x, dir_y)) -class VibrationPulseTest: +class VibrationPulseTestGenerator: def __init__(self, config): - self.printer = config.get_printer() - self.gcode = self.printer.lookup_object('gcode') self.min_freq = config.getfloat('min_freq', 5., minval=1.) - # Defaults are such that max_freq * accel_per_hz == 10000 (max_accel) - self.max_freq = config.getfloat('max_freq', 10000. / 75., + self.max_freq = config.getfloat('max_freq', 135., minval=self.min_freq, maxval=300.) - self.accel_per_hz = config.getfloat('accel_per_hz', 75., above=0.) + self.accel_per_hz = config.getfloat('accel_per_hz', 60., above=0.) self.hz_per_sec = config.getfloat('hz_per_sec', 1., minval=0.1, maxval=2.) - - self.probe_points = config.getlists('probe_points', seps=(',', '\n'), - parser=float, count=3) - def get_start_test_points(self): - return self.probe_points def prepare_test(self, gcmd): self.freq_start = gcmd.get_float("FREQ_START", self.min_freq, minval=1.) self.freq_end = gcmd.get_float("FREQ_END", self.max_freq, minval=self.freq_start, maxval=300.) - self.accel_per_hz = gcmd.get_float("ACCEL_PER_HZ", - self.accel_per_hz, above=0.) - self.hz_per_sec = gcmd.get_float("HZ_PER_SEC", self.hz_per_sec, - above=0., maxval=2.) - def run_test(self, axis, gcmd): + self.test_accel_per_hz = gcmd.get_float("ACCEL_PER_HZ", + self.accel_per_hz, above=0.) + self.test_hz_per_sec = gcmd.get_float("HZ_PER_SEC", self.hz_per_sec, + above=0., maxval=2.) + def gen_test(self): + freq = self.freq_start + res = [] + sign = 1. + time = 0. + while freq <= self.freq_end + 0.000001: + t_seg = .25 / freq + accel = self.test_accel_per_hz * freq + time += t_seg + res.append((time, sign * accel, freq)) + time += t_seg + res.append((time, -sign * accel, freq)) + freq += 2. * t_seg * self.test_hz_per_sec + sign = -sign + return res + def get_max_freq(self): + return self.freq_end + +class SweepingVibrationsTestGenerator: + def __init__(self, config): + self.vibration_generator = VibrationPulseTestGenerator(config) + self.sweeping_accel = config.getfloat('sweeping_accel', 400., above=0.) + self.sweeping_period = config.getfloat('sweeping_period', 1.2, + minval=0.) + def prepare_test(self, gcmd): + self.vibration_generator.prepare_test(gcmd) + self.test_sweeping_accel = gcmd.get_float( + "SWEEPING_ACCEL", self.sweeping_accel, above=0.) + self.test_sweeping_period = gcmd.get_float( + "SWEEPING_PERIOD", self.sweeping_period, minval=0.) + def gen_test(self): + test_seq = self.vibration_generator.gen_test() + accel_fraction = math.sqrt(2.0) * 0.125 + if self.test_sweeping_period: + t_rem = self.test_sweeping_period * accel_fraction + sweeping_accel = self.test_sweeping_accel + else: + t_rem = float('inf') + sweeping_accel = 0. + res = [] + last_t = 0. + sig = 1. + accel_fraction += 0.25 + for next_t, accel, freq in test_seq: + t_seg = next_t - last_t + while t_rem <= t_seg: + last_t += t_rem + res.append((last_t, accel + sweeping_accel * sig, freq)) + t_seg -= t_rem + t_rem = self.test_sweeping_period * accel_fraction + accel_fraction = 0.5 + sig = -sig + t_rem -= t_seg + res.append((next_t, accel + sweeping_accel * sig, freq)) + last_t = next_t + return res + def get_max_freq(self): + return self.vibration_generator.get_max_freq() + +class ResonanceTestExecutor: + def __init__(self, config): + self.printer = config.get_printer() + self.gcode = self.printer.lookup_object('gcode') + def run_test(self, test_seq, axis, gcmd): + reactor = self.printer.get_reactor() toolhead = self.printer.lookup_object('toolhead') X, Y, Z, E = toolhead.get_position() - sign = 1. - freq = self.freq_start # Override maximum acceleration and acceleration to # deceleration based on the maximum test frequency - systime = self.printer.get_reactor().monotonic() + systime = reactor.monotonic() toolhead_info = toolhead.get_status(systime) old_max_accel = toolhead_info['max_accel'] old_minimum_cruise_ratio = toolhead_info['minimum_cruise_ratio'] - max_accel = self.freq_end * self.accel_per_hz + max_accel = max([abs(a) for _, a, _ in test_seq]) self.gcode.run_script_from_command( "SET_VELOCITY_LIMIT ACCEL=%.3f MINIMUM_CRUISE_RATIO=0" % (max_accel,)) @@ -90,24 +144,46 @@ class VibrationPulseTest: gcmd.respond_info("Disabled [input_shaper] for resonance testing") else: input_shaper = None - gcmd.respond_info("Testing frequency %.0f Hz" % (freq,)) - while freq <= self.freq_end + 0.000001: - t_seg = .25 / freq - accel = self.accel_per_hz * freq - max_v = accel * t_seg + last_v = last_t = last_accel = last_freq = 0. + for next_t, accel, freq in test_seq: + t_seg = next_t - last_t toolhead.cmd_M204(self.gcode.create_gcode_command( - "M204", "M204", {"S": accel})) - L = .5 * accel * t_seg**2 - dX, dY = axis.get_point(L) - nX = X + sign * dX - nY = Y + sign * dY - toolhead.move([nX, nY, Z, E], max_v) - toolhead.move([X, Y, Z, E], max_v) - sign = -sign - old_freq = freq - freq += 2. * t_seg * self.hz_per_sec - if math.floor(freq) > math.floor(old_freq): + "M204", "M204", {"S": abs(accel)})) + v = last_v + accel * t_seg + abs_v = abs(v) + if abs_v < 0.000001: + v = abs_v = 0. + abs_last_v = abs(last_v) + v2 = v * v + last_v2 = last_v * last_v + half_inv_accel = .5 / accel + d = (v2 - last_v2) * half_inv_accel + dX, dY = axis.get_point(d) + nX = X + dX + nY = Y + dY + toolhead.limit_next_junction_speed(abs_last_v) + if v * last_v < 0: + # The move first goes to a complete stop, then changes direction + d_decel = -last_v2 * half_inv_accel + decel_X, decel_Y = axis.get_point(d_decel) + toolhead.move([X + decel_X, Y + decel_Y, Z, E], abs_last_v) + toolhead.move([nX, nY, Z, E], abs_v) + else: + toolhead.move([nX, nY, Z, E], max(abs_v, abs_last_v)) + if math.floor(freq) > math.floor(last_freq): gcmd.respond_info("Testing frequency %.0f Hz" % (freq,)) + reactor.pause(reactor.monotonic() + 0.01) + X, Y = nX, nY + last_t = next_t + last_v = v + last_accel = accel + last_freq = freq + if last_v: + d_decel = -.5 * last_v2 / old_max_accel + decel_X, decel_Y = axis.get_point(d_decel) + toolhead.cmd_M204(self.gcode.create_gcode_command( + "M204", "M204", {"S": old_max_accel})) + toolhead.move([X + decel_X, Y + decel_Y, Z, E], abs(last_v)) # Restore the original acceleration values self.gcode.run_script_from_command( "SET_VELOCITY_LIMIT ACCEL=%.3f MINIMUM_CRUISE_RATIO=%.3f" @@ -116,14 +192,13 @@ class VibrationPulseTest: if input_shaper is not None: input_shaper.enable_shaping() gcmd.respond_info("Re-enabled [input_shaper]") - def get_max_freq(self): - return self.freq_end class ResonanceTester: def __init__(self, config): self.printer = config.get_printer() self.move_speed = config.getfloat('move_speed', 50., above=0.) - self.test = VibrationPulseTest(config) + self.generator = SweepingVibrationsTestGenerator(config) + self.executor = ResonanceTestExecutor(config) if not config.get('accel_chip_x', None): self.accel_chip_names = [('xy', config.get('accel_chip').strip())] else: @@ -133,6 +208,8 @@ class ResonanceTester: if self.accel_chip_names[0][1] == self.accel_chip_names[1][1]: self.accel_chip_names = [('xy', self.accel_chip_names[0][1])] self.max_smoothing = config.getfloat('max_smoothing', None, minval=0.05) + self.probe_points = config.getlists('probe_points', seps=(',', '\n'), + parser=float, count=3) self.gcode = self.printer.lookup_object('gcode') self.gcode.register_command("MEASURE_AXES_NOISE", @@ -156,12 +233,9 @@ class ResonanceTester: toolhead = self.printer.lookup_object('toolhead') calibration_data = {axis: None for axis in axes} - self.test.prepare_test(gcmd) + self.generator.prepare_test(gcmd) - if test_point is not None: - test_points = [test_point] - else: - test_points = self.test.get_start_test_points() + test_points = [test_point] if test_point else self.probe_points for point in test_points: toolhead.manual_move(point, self.move_speed) @@ -186,7 +260,8 @@ class ResonanceTester: raw_values.append((axis, aclient, chip.name)) # Generate moves - self.test.run_test(axis, gcmd) + test_seq = self.generator.gen_test() + self.executor.run_test(test_seq, axis, gcmd) for chip_axis, aclient, chip_name in raw_values: aclient.finish_measurements() if raw_name_suffix is not None: @@ -218,7 +293,7 @@ class ResonanceTester: parsed_chips.append(chip) return parsed_chips def _get_max_calibration_freq(self): - return 1.5 * self.test.get_max_freq() + return 1.5 * self.generator.get_max_freq() cmd_TEST_RESONANCES_help = ("Runs the resonance test for a specifed axis") def cmd_TEST_RESONANCES(self, gcmd): # Parse parameters diff --git a/klippy/extras/shaper_calibrate.py b/klippy/extras/shaper_calibrate.py index 6891fefb3..f497171f6 100644 --- a/klippy/extras/shaper_calibrate.py +++ b/klippy/extras/shaper_calibrate.py @@ -48,7 +48,9 @@ class CalibrationData: # Avoid division by zero errors psd /= self.freq_bins + .1 # Remove low-frequency noise - psd[self.freq_bins < MIN_FREQ] = 0. + low_freqs = self.freq_bins < 2. * MIN_FREQ + psd[low_freqs] *= self.numpy.exp( + -(2. * MIN_FREQ / (self.freq_bins[low_freqs] + .1))**2 + 1.) def get_psd(self, axis='all'): return self._psd_map[axis] diff --git a/klippy/extras/z_tilt.py b/klippy/extras/z_tilt.py index 49bafc4a3..9f5ea0b9c 100644 --- a/klippy/extras/z_tilt.py +++ b/klippy/extras/z_tilt.py @@ -108,7 +108,7 @@ class RetryHelper: return self.increasing > 1 def check_retry(self, z_positions): if self.max_retries == 0: - return + return "done" error = round(max(z_positions) - min(z_positions),6) self.gcode.respond_info( "Retries: %d/%d %s: %0.6f tolerance: %0.6f" % ( diff --git a/klippy/toolhead.py b/klippy/toolhead.py index ed26092f6..256915daa 100644 --- a/klippy/toolhead.py +++ b/klippy/toolhead.py @@ -47,6 +47,7 @@ class Move: self.delta_v2 = 2.0 * move_d * self.accel self.max_smoothed_v2 = 0. self.smooth_delta_v2 = 2.0 * move_d * toolhead.max_accel_to_decel + self.next_junction_v2 = 999999999.9 def limit_speed(self, speed, accel): speed2 = speed**2 if speed2 < self.max_cruise_v2: @@ -55,6 +56,8 @@ class Move: self.accel = min(self.accel, accel) self.delta_v2 = 2.0 * self.move_d * self.accel self.smooth_delta_v2 = min(self.smooth_delta_v2, self.delta_v2) + def limit_next_junction_speed(self, speed): + self.next_junction_v2 = min(self.next_junction_v2, speed**2) def move_error(self, msg="Move out of range"): ep = self.end_pos m = "%s: %.3f %.3f %.3f [%.3f]" % (msg, ep[0], ep[1], ep[2], ep[3]) @@ -64,9 +67,9 @@ class Move: return # Allow extruder to calculate its maximum junction extruder_v2 = self.toolhead.extruder.calc_junction(prev_move, self) - max_start_v2 = min( - extruder_v2, self.max_cruise_v2, prev_move.max_cruise_v2, - prev_move.max_start_v2 + prev_move.delta_v2) + max_start_v2 = min(extruder_v2, self.max_cruise_v2, + prev_move.max_cruise_v2, prev_move.next_junction_v2, + prev_move.max_start_v2 + prev_move.delta_v2) # Find max velocity using "approximated centripetal velocity" axes_r = self.axes_r prev_axes_r = prev_move.axes_r @@ -462,6 +465,10 @@ class ToolHead: self.commanded_pos[:] = newpos self.kin.set_position(newpos, homing_axes) self.printer.send_event("toolhead:set_position") + def limit_next_junction_speed(self, speed): + last_move = self.lookahead.get_last() + if last_move is not None: + last_move.limit_next_junction_speed(speed) def move(self, newpos, speed): move = Move(self, self.commanded_pos, newpos, speed) if not move.move_d: diff --git a/lib/rp2040_flash/main.c b/lib/rp2040_flash/main.c index 780071308..017fe9455 100644 --- a/lib/rp2040_flash/main.c +++ b/lib/rp2040_flash/main.c @@ -146,7 +146,7 @@ int picoboot_flash(libusb_device_handle *handle, struct flash_data *image, model } fprintf(stderr, "Rebooting device\n"); - if (model == 2040) { + if (model == rp2040) { if (picoboot_reboot(handle, 0, 0, 500)) { return report_error(handle, "reboot"); } diff --git a/src/Kconfig b/src/Kconfig index ad326e729..168572729 100644 --- a/src/Kconfig +++ b/src/Kconfig @@ -96,15 +96,19 @@ config WANT_DISPLAYS bool depends on HAVE_GPIO default y -config WANT_SENSORS +config WANT_THERMOCOUPLE bool - depends on HAVE_GPIO_I2C || HAVE_GPIO_SPI + depends on HAVE_GPIO_SPI + default y +config WANT_ADXL345 + bool + depends on HAVE_GPIO_SPI default y config WANT_LIS2DW bool depends on HAVE_GPIO_SPI || HAVE_GPIO_I2C default y -config WANT_LDC1612 +config WANT_MPU9250 bool depends on HAVE_GPIO_I2C default y @@ -116,6 +120,14 @@ config WANT_ADS1220 bool depends on HAVE_GPIO_SPI default y +config WANT_LDC1612 + bool + depends on HAVE_GPIO_I2C + default y +config WANT_SENSOR_ANGLE + bool + depends on HAVE_GPIO_SPI + default y config WANT_SOFTWARE_I2C bool depends on HAVE_GPIO && HAVE_GPIO_I2C @@ -126,8 +138,8 @@ config WANT_SOFTWARE_SPI default y config NEED_SENSOR_BULK bool - depends on WANT_SENSORS || WANT_LIS2DW || WANT_LDC1612 || WANT_HX71X \ - || WANT_ADS1220 + depends on WANT_ADXL345 || WANT_LIS2DW || WANT_MPU9250 \ + || WANT_HX71X || WANT_ADS1220 || WANT_LDC1612 || WANT_SENSOR_ANGLE default y menu "Optional features (to reduce code size)" depends on HAVE_LIMITED_CODE_SIZE @@ -137,14 +149,17 @@ config WANT_GPIO_BITBANGING config WANT_DISPLAYS bool "Support LCD devices" depends on HAVE_GPIO -config WANT_SENSORS - bool "Support external sensor devices" - depends on HAVE_GPIO_I2C || HAVE_GPIO_SPI +config WANT_THERMOCOUPLE + bool "Support thermocouple MAX sensors" + depends on HAVE_GPIO_SPI +config WANT_ADXL345 + bool "Support adxl accelerometers" + depends on HAVE_GPIO_SPI config WANT_LIS2DW bool "Support lis2dw and lis3dh 3-axis accelerometers" depends on HAVE_GPIO_SPI || HAVE_GPIO_I2C -config WANT_LDC1612 - bool "Support ldc1612 eddy current sensor" +config WANT_MPU9250 + bool "Support MPU accelerometers" depends on HAVE_GPIO_I2C config WANT_HX71X bool "Support HX711 and HX717 ADC chips" @@ -152,6 +167,12 @@ config WANT_HX71X config WANT_ADS1220 bool "Support ADS 1220 ADC chip" depends on HAVE_GPIO_SPI +config WANT_LDC1612 + bool "Support ldc1612 eddy current sensor" + depends on HAVE_GPIO_I2C +config WANT_SENSOR_ANGLE + bool "Support angle sensors" + depends on HAVE_GPIO_SPI config WANT_SOFTWARE_I2C bool "Support software based I2C \"bit-banging\"" depends on HAVE_GPIO && HAVE_GPIO_I2C diff --git a/src/Makefile b/src/Makefile index 86c7407e6..a0ca02b9e 100644 --- a/src/Makefile +++ b/src/Makefile @@ -14,12 +14,12 @@ src-$(CONFIG_WANT_GPIO_BITBANGING) += buttons.c tmcuart.c neopixel.c \ src-$(CONFIG_WANT_DISPLAYS) += lcd_st7920.c lcd_hd44780.c src-$(CONFIG_WANT_SOFTWARE_SPI) += spi_software.c src-$(CONFIG_WANT_SOFTWARE_I2C) += i2c_software.c -sensors-src-$(CONFIG_HAVE_GPIO_SPI) := thermocouple.c sensor_adxl345.c \ - sensor_angle.c -sensors-src-$(CONFIG_HAVE_GPIO_I2C) += sensor_mpu9250.c -src-$(CONFIG_WANT_SENSORS) += $(sensors-src-y) +src-$(CONFIG_WANT_THERMOCOUPLE) += thermocouple.c +src-$(CONFIG_WANT_ADXL345) += sensor_adxl345.c src-$(CONFIG_WANT_LIS2DW) += sensor_lis2dw.c -src-$(CONFIG_WANT_LDC1612) += sensor_ldc1612.c +src-$(CONFIG_WANT_MPU9250) += sensor_mpu9250.c src-$(CONFIG_WANT_HX71X) += sensor_hx71x.c src-$(CONFIG_WANT_ADS1220) += sensor_ads1220.c +src-$(CONFIG_WANT_LDC1612) += sensor_ldc1612.c +src-$(CONFIG_WANT_SENSOR_ANGLE) += sensor_angle.c src-$(CONFIG_NEED_SENSOR_BULK) += sensor_bulk.c diff --git a/src/sensor_angle.c b/src/sensor_angle.c index 54caecc21..d01835794 100644 --- a/src/sensor_angle.c +++ b/src/sensor_angle.c @@ -13,11 +13,20 @@ #include "sensor_bulk.h" // sensor_bulk_report #include "spicmds.h" // spidev_transfer -enum { SA_CHIP_A1333, SA_CHIP_AS5047D, SA_CHIP_TLE5012B, SA_CHIP_MAX }; +enum { + SA_CHIP_A1333, + SA_CHIP_AS5047D, + SA_CHIP_TLE5012B, + SA_CHIP_MT6816, + SA_CHIP_MT6826S, + SA_CHIP_MAX +}; DECL_ENUMERATION("spi_angle_type", "a1333", SA_CHIP_A1333); DECL_ENUMERATION("spi_angle_type", "as5047d", SA_CHIP_AS5047D); DECL_ENUMERATION("spi_angle_type", "tle5012b", SA_CHIP_TLE5012B); +DECL_ENUMERATION("spi_angle_type", "mt6816", SA_CHIP_MT6816); +DECL_ENUMERATION("spi_angle_type", "mt6826s", SA_CHIP_MT6826S); enum { TCODE_ERROR = 0xff }; enum { @@ -131,6 +140,15 @@ a1333_query(struct spi_angle *sa, uint32_t stime) angle_add_data(sa, stime, mtime1, (msg[0] << 9) | (msg[1] << 1)); } +static int bit_parity(uint8_t *msg) +{ + uint_fast8_t parity = msg[0] ^ msg[1]; + parity ^= parity >> 4; + parity ^= parity >> 2; + parity ^= parity >> 1; + return parity; +} + // as5047d sensor query static void as5047d_query(struct spi_angle *sa, uint32_t stime) @@ -147,10 +165,7 @@ as5047d_query(struct spi_angle *sa, uint32_t stime) msg[0] = 0xC0; msg[1] = 0x00; spidev_transfer(sa->spi, 1, sizeof(msg), msg); - uint_fast8_t parity = msg[0] ^ msg[1]; - parity ^= parity >> 4; - parity ^= parity >> 2; - parity ^= parity >> 1; + uint_fast8_t parity = bit_parity(msg); if (parity & 1) angle_add_error(sa, SE_CRC); else if (msg[0] & 0x40) @@ -159,6 +174,60 @@ as5047d_query(struct spi_angle *sa, uint32_t stime) angle_add_data(sa, stime, mtime2, (msg[0] << 10) | (msg[1] << 2)); } +static void mt6816_query(struct spi_angle *sa, uint32_t stime) +{ + uint8_t msg[3] = {0x83, 0x00, 0x00}; + uint32_t mtime1 = timer_read_time(); + spidev_transfer(sa->spi, 1, sizeof(msg), msg); + uint32_t mtime2 = timer_read_time(); + // Data is latched on first sclk edge of response + if (mtime2 - mtime1 > MAX_SPI_READ_TIME) { + angle_add_error(sa, SE_SPI_TIME); + return; + } + uint_fast8_t parity = bit_parity(&msg[1]); + if (parity & 1) + angle_add_error(sa, SE_CRC); + else if (msg[2] & 0x02) + angle_add_error(sa, SE_NO_ANGLE); + else + angle_add_data(sa, stime, mtime2, (msg[1] << 8) | (msg[2] & 0xfc)); +} + +static uint8_t +crc8_mt(uint8_t crc, uint8_t data) +{ + crc ^= data; + int i; + for (i = 0; i < 8; i++) + crc = crc & 0x80 ? (crc << 1) ^ 0x07 : crc << 1; + return crc; +} + +static void mt6826s_query(struct spi_angle *sa, uint32_t stime) +{ + uint8_t msg[6] = {0x30, 0x03, 0x00, 0x00, 0x00, 0x00}; + uint32_t mtime1 = timer_read_time(); + spidev_transfer(sa->spi, 1, sizeof(msg), msg); + uint32_t mtime2 = timer_read_time(); + // Data is latched on first sclk edge of response + if (mtime2 - mtime1 > MAX_SPI_READ_TIME) { + angle_add_error(sa, SE_SPI_TIME); + return; + } + uint8_t crc = 0; + for (int i = 2; i < 5; i++) + crc = crc8_mt(crc, msg[i]); + + if (crc != msg[5]) + angle_add_error(sa, SE_CRC); + else if (msg[4] & 0x02) + angle_add_error(sa, SE_NO_ANGLE); + else + angle_add_data(sa, stime, mtime2, (msg[2] << 8) | msg[3]); +} + + #define TLE_READ 0x80 #define TLE_READ_LATCH (TLE_READ | 0x04) #define TLE_REG_AVAL 0x02 @@ -301,6 +370,10 @@ spi_angle_task(void) as5047d_query(sa, stime); else if (chip == SA_CHIP_TLE5012B) tle5012b_query(sa, stime); + else if (chip == SA_CHIP_MT6816) + mt6816_query(sa, stime); + else if (chip == SA_CHIP_MT6826S) + mt6826s_query(sa, stime); angle_check_report(sa, oid); } } diff --git a/test/configs/ar100.config b/test/configs/ar100.config index a1335176f..7d32e57cf 100644 --- a/test/configs/ar100.config +++ b/test/configs/ar100.config @@ -6,3 +6,4 @@ CONFIG_WANT_SOFTWARE_SPI=n CONFIG_WANT_LIS2DW=n CONFIG_WANT_HX71X=n CONFIG_WANT_ADS1220=n +CONFIG_WANT_SENSOR_ANGLE=n