Merge branch 'master' into patch-1

This commit is contained in:
Antonio Navarro Perez 2025-10-14 10:34:10 +02:00 committed by GitHub
commit 47430c0aa5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
71 changed files with 2489 additions and 1556 deletions

View file

@ -133,44 +133,34 @@ max_z_accel: 100
#[tmc2130 stepper_x]
#cs_pin: PB8
#spi_software_miso_pin: PC11
#spi_software_mosi_pin: PC12
#spi_software_sclk_pin: PC10
#spi_bus: spi3_PC11_PC12_PC10
##diag1_pin: PF3
#run_current: 0.800
#stealthchop_threshold: 999999
#[tmc2130 stepper_y]
#cs_pin: PC9
#spi_software_miso_pin: PC11
#spi_software_mosi_pin: PC12
#spi_software_sclk_pin: PC10
#spi_bus: spi3_PC11_PC12_PC10
##diag1_pin: PF4
#run_current: 0.800
#stealthchop_threshold: 999999
#[tmc2130 stepper_z]
#cs_pin: PD0
#spi_software_miso_pin: PC11
#spi_software_mosi_pin: PC12
#spi_software_sclk_pin: PC10
#spi_bus: spi3_PC11_PC12_PC10
##diag1_pin: PF5
#run_current: 0.650
#stealthchop_threshold: 999999
#[tmc2130 extruder]
#cs_pin: PD1
#spi_software_miso_pin: PC11
#spi_software_mosi_pin: PC12
#spi_software_sclk_pin: PC10
#spi_bus: spi3_PC11_PC12_PC10
#run_current: 0.800
#stealthchop_threshold: 999999
#[tmc2130 extruder1]
#cs_pin: PB5
#spi_software_miso_pin: PC11
#spi_software_mosi_pin: PC12
#spi_software_sclk_pin: PC10
#spi_bus: spi3_PC11_PC12_PC10
#run_current: 0.800
#stealthchop_threshold: 999999
@ -195,6 +185,4 @@ aliases:
#[adxl345]
#cs_pin: PC15
#spi_software_miso_pin: PC11
#spi_software_mosi_pin: PC12
#spi_software_sclk_pin: PC10
#spi_bus: spi3_PC11_PC12_PC10

View file

@ -11,9 +11,7 @@ serial: /dev/serial/by-id/usb-Klipper_Klipper_firmware_12345-if00
[adxl345]
cs_pin: EBBCan:PB12
spi_software_sclk_pin: EBBCan:PB10
spi_software_mosi_pin: EBBCan:PB11
spi_software_miso_pin: EBBCan:PB2
spi_bus: spi2_PB2_PB11_PB10
axes_map: x,y,z
[extruder]

View file

@ -11,9 +11,7 @@ serial: /dev/serial/by-id/usb-Klipper_Klipper_firmware_12345-if00
[adxl345]
cs_pin: EBBCan:PB12
spi_software_sclk_pin: EBBCan:PB10
spi_software_mosi_pin: EBBCan:PB11
spi_software_miso_pin: EBBCan:PB2
spi_bus: spi2_PB2_PB11_PB10
axes_map: x,y,z
[extruder]

View file

@ -15,9 +15,7 @@ sensor_pin: EBBCan:PA2
[adxl345]
cs_pin: EBBCan:PB12
spi_software_sclk_pin: EBBCan:PB10
spi_software_mosi_pin: EBBCan:PB11
spi_software_miso_pin: EBBCan:PB2
spi_bus: spi2_PB2_PB11_PB10
axes_map: x,y,z
[extruder]

View file

@ -102,20 +102,35 @@ some functionality in C code.
Initial execution starts in **klippy/klippy.py**. This reads the
command-line arguments, opens the printer config file, instantiates
the main printer objects, and starts the serial connection. The main
execution of G-code commands is in the process_commands() method in
execution of G-code commands is in the _process_commands() method in
**klippy/gcode.py**. This code translates the G-code commands into
printer object calls, which frequently translate the actions to
commands to be executed on the micro-controller (as declared via the
DECL_COMMAND macro in the micro-controller code).
There are four threads in the Klippy host code. The main thread
handles incoming gcode commands. A second thread (which resides
entirely in the **klippy/chelper/serialqueue.c** C code) handles
low-level IO with the serial port. The third thread is used to process
response messages from the micro-controller in the Python code (see
**klippy/serialhdl.py**). The fourth thread writes debug messages to
the log (see **klippy/queuelogger.py**) so that the other threads
never block on log writes.
There are several threads in the Klipper host code:
* There is a Python "main thread" that handles incoming G-Code
commands and is the starting point for most actions. This thread
runs the [reactor](https://en.wikipedia.org/wiki/Reactor_pattern)
(**klippy/reactor.py**) and most high-level actions originate from
IO and timer event callbacks from that reactor.
* A thread for writing messages to the log so that the other threads
do not block on log writes. This thread resides in the
**klippy/queuelogger.py** code and its multi-threaded nature is not
exposed to the main Python thread.
* A thread per micro-controller that performs the low-level reading
and writing of messages to that micro-controller. It resides in the
**klippy/chelper/serialqueue.c** C code and its multi-threaded
nature is not exposed to the Python code.
* A thread per micro-controller for processing messages received from
that micro-controller in the Python code. This thread is created in
**klippy/serialhdl.py**. Care must be taken in Python callbacks
invoked from this thread as this thread may directly interact with
the main Python thread.
* A thread per stepper motor that calculates the timing of stepper
motor step pulses and compresses those times. This thread resides in
the **klippy/chelper/steppersync.c** C code and its multi-threaded
nature is not exposed to the Python code.
## Code flow of a move command
@ -136,9 +151,10 @@ provides further information on the mechanics of moves.
* The ToolHead class (in toolhead.py) handles "look-ahead" and tracks
the timing of printing actions. The main codepath for a move is:
`ToolHead.move() -> LookAheadQueue.add_move() ->
LookAheadQueue.flush() -> Move.set_junction() ->
ToolHead._process_moves()`.
`ToolHead.move() -> LookAheadQueue.add_move()`, then
`ToolHead.move() -> ToolHead._process_lookahead() ->
LookAheadQueue.flush() -> Move.set_junction()`, and then
`ToolHead._process_lookahead() -> trapq_append()`.
* ToolHead.move() creates a Move() object with the parameters of the
move (in cartesian space and in units of seconds and millimeters).
* The kinematics class is given the opportunity to audit each move
@ -157,39 +173,47 @@ provides further information on the mechanics of moves.
phase, followed by a constant deceleration phase. Every move
contains these three phases in this order, but some phases may be of
zero duration.
* When ToolHead._process_moves() is called, everything about the
* When ToolHead._process_lookahead() resumes, everything about the
move is known - its start location, its end location, its
acceleration, its start/cruising/end velocity, and distance traveled
during acceleration/cruising/deceleration. All the information is
stored in the Move() class and is in cartesian space in units of
millimeters and seconds.
* The moves are then placed on a "trapezoid motion queue" via
trapq_append() (in klippy/chelper/trapq.c). The trapq stores all the
information in the Move() class in a C struct accessible to the host
C code.
* Note that the extruder is handled in its own kinematic class:
`ToolHead._process_lookahead() -> PrinterExtruder.process_move()`.
Since the Move() class specifies the exact movement time and since
step pulses are sent to the micro-controller with specific timing,
stepper movements produced by the extruder class will be in sync
with head movement even though the code is kept separate.
* For efficiency reasons, stepper motion is generated in the C code in
a thread per stepper motor. The threads are notified when steps
should be generated by the motion_queuing module
(klippy/extras/motion_queuing.py):
`PrinterMotionQueuing._flush_handler() ->
PrinterMotionQueuing._advance_move_time() ->
steppersyncmgr_gen_steps() -> se_start_gen_steps()`.
* Klipper uses an
[iterative solver](https://en.wikipedia.org/wiki/Root-finding_algorithm)
to generate the step times for each stepper. For efficiency reasons,
the stepper pulse times are generated in C code. The moves are first
placed on a "trapezoid motion queue": `ToolHead._process_moves() ->
trapq_append()` (in klippy/chelper/trapq.c). The step times are then
generated: `ToolHead._process_moves() ->
ToolHead._advance_move_time() -> ToolHead._advance_flush_time() ->
MCU_Stepper.generate_steps() -> itersolve_generate_steps() ->
itersolve_gen_steps_range()` (in klippy/chelper/itersolve.c). The
goal of the iterative solver is to find step times given a function
that calculates a stepper position from a time. This is done by
repeatedly "guessing" various times until the stepper position
formula returns the desired position of the next step on the
stepper. The feedback produced from each guess is used to improve
future guesses so that the process rapidly converges to the desired
time. The kinematic stepper position formulas are located in the
klippy/chelper/ directory (eg, kin_cart.c, kin_corexy.c,
kin_delta.c, kin_extruder.c).
* Note that the extruder is handled in its own kinematic class:
`ToolHead._process_moves() -> PrinterExtruder.move()`. Since
the Move() class specifies the exact movement time and since step
pulses are sent to the micro-controller with specific timing,
stepper movements produced by the extruder class will be in sync
with head movement even though the code is kept separate.
to generate the step times for each stepper. The step times are
generated from the background thread (klippy/chelper/steppersync.c):
`se_background_thread() -> se_generate_steps() ->
itersolve_generate_steps() -> itersolve_gen_steps_range()` (in
klippy/chelper/itersolve.c). The goal of the iterative solver is to
find step times given a function that calculates a stepper position
from a time. This is done by repeatedly "guessing" various times
until the stepper position formula returns the desired position of
the next step on the stepper. The feedback produced from each guess
is used to improve future guesses so that the process rapidly
converges to the desired time. The kinematic stepper position
formulas are located in the klippy/chelper/ directory (eg,
kin_cart.c, kin_corexy.c, kin_delta.c, kin_extruder.c).
* After the iterative solver calculates the step times they are added
to an array: `itersolve_gen_steps_range() -> stepcompress_append()`
@ -206,7 +230,7 @@ provides further information on the mechanics of moves.
commands that correspond to the list of stepper step times built in
the previous stage. These "queue_step" commands are then queued,
prioritized, and sent to the micro-controller (via
stepcompress.c:steppersync and serialqueue.c:serialqueue).
steppersync.c:steppersync and serialqueue.c:serialqueue).
* Processing of the queue_step commands on the micro-controller starts
in src/command.c which parses the command and calls

View file

@ -8,6 +8,17 @@ All dates in this document are approximate.
## Changes
20251010: During normal printing the command processing will now
attempt to stay one second ahead of printer movement (reduced from two
seconds previously).
20251003: Support for the undocumented `max_stepper_error` option in
the `[printer]` config section has been removed.
20250916: The definitions of EI, 2HUMP_EI, and 3HUMP_EI input shapers
were updated. For best performance it is recommended to recalibrate
input shapers, especially if some of these shapers are currently used.
20250811: Support for the `max_accel_to_decel` parameter in the
`[printer]` config section has been removed and support for the
`ACCEL_TO_DECEL` parameter in the `SET_VELOCITY_LIMIT` command has

View file

@ -1780,17 +1780,22 @@ the [command reference](G-Codes.md#input_shaper).
# input shapers, this parameter can be set from different
# considerations. The default value is 0, which disables input
# shaping for Y axis.
#shaper_freq_z: 0
# A frequency (in Hz) of the input shaper for Z axis. The default
# value is 0, which disables input shaping for Z axis.
#shaper_type: mzv
# A type of the input shaper to use for both X and Y axes. Supported
# A type of the input shaper to use for all axes. Supported
# shapers are zv, mzv, zvd, ei, 2hump_ei, and 3hump_ei. The default
# is mzv input shaper.
#shaper_type_x:
#shaper_type_y:
# If shaper_type is not set, these two parameters can be used to
# configure different input shapers for X and Y axes. The same
#shaper_type_z:
# If shaper_type is not set, these parameters can be used to
# configure different input shapers for X, Y, and Z axes. The same
# values are supported as for shaper_type parameter.
#damping_ratio_x: 0.1
#damping_ratio_y: 0.1
#damping_ratio_z: 0.1
# Damping ratios of vibrations of X and Y axes used by input shapers
# to improve vibration suppression. Default value is 0.1 which is a
# good all-round value for most printers. In most circumstances this
@ -1955,11 +1960,10 @@ section of the measuring resonances guide for more information on
# are reachable by the toolhead.
#accel_chip:
# A name of the accelerometer chip to use for measurements. If
# adxl345 chip was defined without an explicit name, this parameter
# can simply reference it as "accel_chip: adxl345", otherwise an
# explicit name must be supplied as well, e.g. "accel_chip: adxl345
# my_chip_name". Either this, or the next two parameters must be
# set.
# an accelerometer was defined without an explicit name, this parameter
# can simply reference it by type, e.g. "accel_chip: adxl345", otherwise
# a full name must be supplied, e.g. "accel_chip: adxl345 my_chip_name".
# Either this, or the next two parameters must be set.
#accel_chip_x:
#accel_chip_y:
# Names of the accelerometer chips to use for measurements for each
@ -1968,6 +1972,10 @@ section of the measuring resonances guide for more information on
# and on the toolhead (for X axis). These parameters have the same
# format as 'accel_chip' parameter. Only 'accel_chip' or these two
# parameters must be provided.
#accel_chip_z:
# A name of the accelerometer chip to use for measurements of Z axis.
# This parameter has the same format as 'accel_chip'. The default is
# not to configure an accelerometer for Z axis.
#max_smoothing:
# Maximum input shaper smoothing to allow for each axis during shaper
# auto-calibration (with 'SHAPER_CALIBRATE' command). By default no
@ -1978,16 +1986,20 @@ section of the measuring resonances guide for more information on
# 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.
#max_freq: 135
# Maximum frequency to test for resonances. The default is 135 Hz.
#max_freq_z: 100
# Maximum frequency to test Z axis for resonances. The default is 100 Hz.
#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
# a lower than the default value if the resonances get too strong on
# the printer. However, lower values make measurements of
# high-frequency resonances less precise. The default value is 75
# (mm/sec).
# the printer. However, lower values make measurements of high-frequency
# resonances less precise. The default value is 60 (mm/sec).
#accel_per_hz_z: 15
# This parameter has the same meaning as accel_per_hz, but applies to
# Z axis specifically. The default is 15 (mm/sec).
#hz_per_sec: 1
# Determines the speed of the test. When testing all frequencies in
# range [min_freq, max_freq], each second the frequency increases by
@ -1996,6 +2008,8 @@ section of the measuring resonances guide for more information on
# (Hz/sec == sec^-2).
#sweeping_accel: 400
# An acceleration of slow sweeping moves. The default is 400 mm/sec^2.
#sweeping_accel_z: 50
# Same as sweeping_accel above, but for Z axis. The default is 50 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
@ -4944,8 +4958,8 @@ detection_length: 7.0
# a state change on the switch_pin
# Default is 7 mm.
extruder:
# The name of the extruder section this sensor is associated with.
# This parameter must be provided.
# The name of the extruder or extruder_stepper section this sensor
# is associated with. This parameter must be provided.
switch_pin:
#pause_on_runout:
#runout_gcode:

View file

@ -98,17 +98,16 @@ bootloaders.
## Can I run Klipper on something other than a Raspberry Pi 3?
The recommended hardware is a Raspberry Pi 2, Raspberry Pi 3, or
Raspberry Pi 4.
The recommended hardware is a Raspberry Pi Zero2w, Raspberry Pi 3,
Raspberry Pi 4 or Raspberry Pi 5. Klipper will also run on other SBC
devices as well as x86 hardware, as described below.
Klipper will run on a Raspberry Pi 1 and on the Raspberry Pi Zero, but
these boards don't have enough processing power to run OctoPrint
Klipper will run on a Raspberry Pi 1, 2 and on the Raspberry Pi Zero1,
but these boards don't have enough processing power to run Klipper
well. It is common for print stalls to occur on these slower machines
when printing directly from OctoPrint. (The printer may move faster
than OctoPrint can send movement commands.) If you wish to run on one
one of these slower boards anyway, consider using the "virtual_sdcard"
feature when printing (see
[config reference](Config_Reference.md#virtual_sdcard) for details).
when printing (The printer may move faster than Klipper can send
movement commands.) It is not reccomended to run Klipper on these older
machines.
For running on the Beaglebone, see the
[Beaglebone specific installation instructions](Beaglebone.md).

View file

@ -493,7 +493,7 @@ enabled.
`SET_FAN_SPEED FAN=config_name SPEED=<speed>` This command sets the
speed of a fan. "speed" must be between 0.0 and 1.0.
`SET_FAN_SPEED PIN=config_name TEMPLATE=<template_name>
`SET_FAN_SPEED FAN=config_name TEMPLATE=<template_name>
[<param_x>=<literal>]`: If `TEMPLATE` is specified then it assigns a
[display_template](Config_Reference.md#display_template) to the given
fan. For example, if one defined a `[display_template
@ -828,15 +828,17 @@ been enabled (also see the
#### SET_INPUT_SHAPER
`SET_INPUT_SHAPER [SHAPER_FREQ_X=<shaper_freq_x>]
[SHAPER_FREQ_Y=<shaper_freq_y>] [DAMPING_RATIO_X=<damping_ratio_x>]
[DAMPING_RATIO_Y=<damping_ratio_y>] [SHAPER_TYPE=<shaper>]
[SHAPER_TYPE_X=<shaper_type_x>] [SHAPER_TYPE_Y=<shaper_type_y>]`:
[SHAPER_FREQ_Y=<shaper_freq_y>] [SHAPER_FREQ_Y=<shaper_freq_z>]
[DAMPING_RATIO_X=<damping_ratio_x>] [DAMPING_RATIO_Y=<damping_ratio_y>]
[DAMPING_RATIO_Z=<damping_ratio_z>] [SHAPER_TYPE=<shaper>]
[SHAPER_TYPE_X=<shaper_type_x>] [SHAPER_TYPE_Y=<shaper_type_y>]
[SHAPER_TYPE_Z=<shaper_type_z>]`:
Modify input shaper parameters. Note that SHAPER_TYPE parameter resets
input shaper for both X and Y axes even if different shaper types have
input shaper for all axes even if different shaper types have
been configured in [input_shaper] section. SHAPER_TYPE cannot be used
together with either of SHAPER_TYPE_X and SHAPER_TYPE_Y parameters.
See [config reference](Config_Reference.md#input_shaper) for more
details on each of these parameters.
together with any of SHAPER_TYPE_X, SHAPER_TYPE_Y, and SHAPER_TYPE_Z
parameters. See [config reference](Config_Reference.md#input_shaper)
for more details on each of these parameters.
### [led]
@ -1284,13 +1286,14 @@ all enabled accelerometer chips.
[POINT=x,y,z] [INPUT_SHAPING=<0:1>]`: Runs the resonance
test in all configured probe points for the requested "axis" and
measures the acceleration using the accelerometer chips configured for
the respective axis. "axis" can either be X or Y, or specify an
arbitrary direction as `AXIS=dx,dy`, where dx and dy are floating
the respective axis. "axis" can either be X, Y or Z, or specify an
arbitrary direction as `AXIS=dx,dy[,dz]`, where dx, dy, dz are floating
point numbers defining a direction vector (e.g. `AXIS=X`, `AXIS=Y`, or
`AXIS=1,-1` to define a diagonal direction). Note that `AXIS=dx,dy`
and `AXIS=-dx,-dy` is equivalent. `chip_name` can be one or
more configured accel chips, delimited with comma, for example
`CHIPS="adxl345, adxl345 rpi"`. If POINT is specified it will override the point(s)
`AXIS=1,-1` to define a diagonal direction in XY plane, or `AXIS=0,1,1`
to define a direction in YZ plane). Note that `AXIS=dx,dy` and `AXIS=-dx,-dy`
is equivalent. `chip_name` can be one or more configured accel chips,
delimited with comma, for example `CHIPS="adxl345, adxl345 rpi"`.
If POINT is specified it will override the point(s)
configured in `[resonance_tester]`. If `INPUT_SHAPING=0` or not set(default),
disables input shaping for the resonance testing, because
it is not valid to run the resonance testing with the input shaper

View file

@ -697,6 +697,95 @@ 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.
### Measuring the resonances of Z axis
Measuring the resonances of Z axis is similar in many aspects to measuring
resonances of X and Y axes, with some subtle differences. Similarly to other
axes measurements, you will need to have an accelerometer mounted on the
moving parts of Z axis - either the bed itself (if the bed moves over Z axis),
or the toolhead (if the toolhead/gantry moves over Z). You will need to
add the appropriate chip configuration to `printer.cfg` and also add it to
`[resonance_tester]` section, e.g.
```
[resonance_tester]
accel_chip_z: <accelerometer full name>
```
Also make sure that `probe_points` configured in `[resonance_tester]` allow
sufficient clearance for Z axis movements (20 mm above bed surface should
provide enough clearance with the default test parameters).
The next consideration is that Z axis can typically reach lower maximum
speeds and accelerations that X and Y axes. Default parameters of the test
take that into consideration and are much less agressive, but it may still
be necessary to increase `max_z_accel` and `max_z_velocity`. If you have
them configured in `[printer]` section, make sure to set them to at least
```
[printer]
max_z_velocity: 20
max_z_accel: 1550
```
but only for the duration of the test, afterwards you can revert them back
to their original values if necessary. And if you use custom test parameters
for Z axis, `TEST_RESONANCES` and `SHAPER_CALIBRATE` will provide the minimum
required limits if necessary for your specific case.
After all changes to `printer.cfg` have been made, restart Klipper and run
either
```
TEST_RESONANCES AXIS=Z
```
or
```
SHAPER_CALIBRATE AXIS=Z
```
and proceed from there accordingly how you would for other axes.
For example, after `TEST_RESONANCES` command you can run
`calibrate_shaper.py` script and get shaper recommendations and
the chart of resonance response:
![Resonances](img/calibrate-z.png)
After the calibration, the shaper parameters can be stored in the
`printer.cfg`, e.g. from the example above:
```
[input_shaper]
...
shaper_type_z: mzv
shaper_freq_z: 42.6
```
Also, given the movements of Z axis are slow, you can easily consider
more aggressive input shapers, e.g.
```
[input_shaper]
...
shaper_type_z: 2hump_ei
shaper_freq_z: 63.0
```
If the test produces bogus results, you may try to increase
`accel_per_hz_z` parameter in `[resonance_tester]` from its
default value 15 to a larger value in the range of 20-30, e.g.
```
[resonance_tester]
accel_per_hz_z: 25
```
and repeat the test. Increasing this value will likely require
increasing `max_z_accel` and `max_z_velocity` parameters as well.
You can run `TEST_RESONANCES AXIS=Z` command to get the required
minimum values.
However, if you are unable to measure the resonances of Z axis,
you can consider just using
```
[input_shaper]
...
shaper_type_z: 3hump_ei
shaper_freq_z: 65
```
as an acceptable all-round choice, given that the smoothing of
Z axis movements is not of particular concerns.
### Unreliable measurements of resonance frequencies
Sometimes the resonance measurements can produce bogus results, leading to

View file

@ -439,9 +439,12 @@ gcode:
SET_INPUT_SHAPER SHAPER_TYPE_X=<primary_carriage_shaper> SHAPER_FREQ_X=<primary_carriage_freq> SHAPER_TYPE_Y=<y_shaper> SHAPER_FREQ_Y=<y_freq>
```
Note that `SHAPER_TYPE_Y` and `SHAPER_FREQ_Y` should be the same in both
commands. It is also possible to put a similar snippet into the start g-code
in the slicer, however then the shaper will not be enabled until any print
is started.
commands. If you need to configure an input shaper for Z axis, include
its parameters in both `SET_INPUT_SHAPER` commands.
Besides `delayed_gcode`, it is also possible to put a similar snippet into
the start g-code in the slicer, however then the shaper will not be enabled
until any print is started.
Note that the input shaper only needs to be configured once. Subsequent changes
of the carriages or their modes via `SET_DUAL_CARRIAGE` command will preserve
@ -453,15 +456,40 @@ No, `input_shaper` feature has pretty much no impact on the print times by
itself. However, the value of `max_accel` certainly does (tuning of this
parameter described in [this section](#selecting-max_accel)).
### Should I enable and tune input shaper for Z axis?
Most of the users are not likely to see improvements in the quality of
the prints directly, much unlike X and Y shapers. However, users of
delta printers, printers with flying gantry, or printers with heavy
moving beds may be able to increase the `max_z_accel` and `max_z_velocity`
kinematics limits and thus get faster Z movements. This can be especially
useful e.g. for toolchangers, but also when Z-hops are enabled in slicer.
And in general, after enabling Z input shaper many users will hear that
Z axis operates more smoothly, which may increase the comfort of printer
operation, and may somewhat extend lifespan of Z axis parts.
## Technical details
### Input shapers
Input shapers used in Klipper are rather standard, and one can find more
in-depth overview in the articles describing the corresponding shapers.
This section contains a brief overview of some technical aspects of the
supported input shapers. The table below shows some (usually approximate)
parameters of each shaper.
supported input shapers. Input shapers used in Klipper are rather standard,
with the exception of MZV, and one can find more in-depth overview in
the articles describing the corresponding shapers.
MZV stands for a Modified-ZV input shaper. The classic definition of ZV shaper
assumes the duration Ts equal to 1/2 of the damped period of oscillations Td and
has two pulses. However, ZV input shaper has a generalized form for an arbitrary
duration in the range (0, Td] with three pulses (Specified-Duration ZV, see also
SNA-ZV), with a negative middle pulse if Ts < Td and a positive one if Ts > Td.
The MZV shaper was designed as an intermediate shaper between ZV and ZVD,
offering better vibrations suppression than ZV when the determined (measured)
shaper parameters deviate from the ones actually required by the printer,
and smaller smoothing than ZVD. Effectively, it is a SD-ZV shaper with the
specific duration Ts = 3/4 Td, exactly between ZV (Ts = 1/2 Td) and
ZVD (Ts = Td), and it happens to work well for many real-life 3D printers.
The table below shows some (usually approximate) parameters of each shaper.
| Input <br> shaper | Shaper <br> duration | Vibration reduction 20x <br> (5% vibration tolerance) | Vibration reduction 10x <br> (10% vibration tolerance) |
|:--:|:--:|:--:|:--:|
@ -469,8 +497,8 @@ parameters of each shaper.
| MZV | 0.75 / shaper_freq | ± 4% shaper_freq | -10%...+15% shaper_freq |
| ZVD | 1 / shaper_freq | ± 15% shaper_freq | ± 22% shaper_freq |
| EI | 1 / shaper_freq | ± 20% shaper_freq | ± 25% shaper_freq |
| 2HUMP_EI | 1.5 / shaper_freq | ± 35% shaper_freq | ± 40 shaper_freq |
| 3HUMP_EI | 2 / shaper_freq | -45...+50% shaper_freq | -50%...+55% shaper_freq |
| 2HUMP_EI | 1.5 / shaper_freq | -40...+45% shaper_freq | -45..+50% shaper_freq |
| 3HUMP_EI | 2 / shaper_freq | -50...+60% shaper_freq | -55%...+65% shaper_freq |
A note on vibration reduction: the values in the table above are approximate.
If the damping ratio of the printer is known for each axis, the shaper can be
@ -502,11 +530,11 @@ so the values for 10% vibration tolerance are provided only for the reference.
resonances at 35 Hz and 60 Hz on the same axis: a) EI shaper needs to have
shaper_freq = 35 / (1 - 0.2) = 43.75 Hz, and it will reduce resonances
until 43.75 * (1 + 0.2) = 52.5 Hz, so it is not sufficient; b) 2HUMP_EI
shaper needs to have shaper_freq = 35 / (1 - 0.35) = 53.85 Hz and will
reduce vibrations until 53.85 * (1 + 0.35) = 72.7 Hz - so this is an
shaper needs to have shaper_freq = 35 / (1 - 0.4) = 58.3 Hz and will
reduce vibrations until 58.3 * (1 + 0.45) = 84.5 Hz - so this is an
acceptable configuration. Always try to use as high shaper_freq as possible
for a given shaper (perhaps with some safety margin, so in this example
shaper_freq ≈ 50-52 Hz would work best), and try to use a shaper with as
shaper_freq ≈ 55 Hz would work best), and try to use a shaper with as
small shaper duration as possible.
* If one needs to reduce vibrations at several very different frequencies
(say, 30 Hz and 100 Hz), they may see that the table above does not provide

BIN
docs/img/calibrate-z.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

View file

@ -36,38 +36,41 @@ defs_stepcompress = """
int step_count, interval, add;
};
struct stepcompress *stepcompress_alloc(uint32_t oid);
void stepcompress_fill(struct stepcompress *sc, uint32_t max_error
, int32_t queue_step_msgtag, int32_t set_next_step_dir_msgtag);
void stepcompress_fill(struct stepcompress *sc, uint32_t oid
, uint32_t max_error, int32_t queue_step_msgtag
, int32_t set_next_step_dir_msgtag);
void stepcompress_set_invert_sdir(struct stepcompress *sc
, uint32_t invert_sdir);
void stepcompress_free(struct stepcompress *sc);
int stepcompress_reset(struct stepcompress *sc, uint64_t last_step_clock);
int stepcompress_set_last_position(struct stepcompress *sc
, uint64_t clock, int64_t last_position);
int64_t stepcompress_find_past_position(struct stepcompress *sc
, uint64_t clock);
int stepcompress_queue_msg(struct stepcompress *sc
, uint32_t *data, int len);
int stepcompress_queue_mq_msg(struct stepcompress *sc, uint64_t req_clock
, uint32_t *data, int len);
int stepcompress_extract_old(struct stepcompress *sc
, struct pull_history_steps *p, int max
, uint64_t start_clock, uint64_t end_clock);
void stepcompress_set_stepper_kinematics(struct stepcompress *sc
, struct stepper_kinematics *sk);
"""
defs_steppersync = """
struct steppersync *steppersync_alloc(struct serialqueue *sq
, struct stepcompress **sc_list, int sc_num, int move_num);
void steppersync_free(struct steppersync *ss);
struct stepcompress *syncemitter_get_stepcompress(struct syncemitter *se);
void syncemitter_set_stepper_kinematics(struct syncemitter *se
, struct stepper_kinematics *sk);
struct stepper_kinematics *syncemitter_get_stepper_kinematics(
struct syncemitter *se);
void syncemitter_queue_msg(struct syncemitter *se, uint64_t req_clock
, uint32_t *data, int len);
struct syncemitter *steppersync_alloc_syncemitter(struct steppersync *ss
, char name[16], int alloc_stepcompress);
void steppersync_setup_movequeue(struct steppersync *ss
, struct serialqueue *sq, int move_num);
void steppersync_set_time(struct steppersync *ss
, double time_offset, double mcu_freq);
int32_t steppersync_generate_steps(struct steppersync *ss
, double gen_steps_time, uint64_t flush_clock);
void steppersync_history_expire(struct steppersync *ss, uint64_t end_clock);
int steppersync_flush(struct steppersync *ss, uint64_t move_clock);
struct steppersyncmgr *steppersyncmgr_alloc(void);
void steppersyncmgr_free(struct steppersyncmgr *ssm);
struct steppersync *steppersyncmgr_alloc_steppersync(
struct steppersyncmgr *ssm);
int32_t steppersyncmgr_gen_steps(struct steppersyncmgr *ssm
, double flush_time, double gen_steps_time, double clear_history_time);
"""
defs_itersolve = """
@ -76,11 +79,14 @@ defs_itersolve = """
int32_t itersolve_is_active_axis(struct stepper_kinematics *sk, char axis);
void itersolve_set_trapq(struct stepper_kinematics *sk, struct trapq *tq
, double step_dist);
struct trapq *itersolve_get_trapq(struct stepper_kinematics *sk);
double itersolve_calc_position_from_coord(struct stepper_kinematics *sk
, double x, double y, double z);
void itersolve_set_position(struct stepper_kinematics *sk
, double x, double y, double z);
double itersolve_get_commanded_pos(struct stepper_kinematics *sk);
double itersolve_get_gen_steps_pre_active(struct stepper_kinematics *sk);
double itersolve_get_gen_steps_post_active(struct stepper_kinematics *sk);
"""
defs_trapq = """
@ -157,8 +163,6 @@ defs_kin_extruder = """
"""
defs_kin_shaper = """
double input_shaper_get_step_generation_window(
struct stepper_kinematics *sk);
int input_shaper_set_shaper_params(struct stepper_kinematics *sk, char axis
, int n, double a[], double t[]);
int input_shaper_set_sk(struct stepper_kinematics *sk

View file

@ -151,7 +151,6 @@ itersolve_generate_steps(struct stepper_kinematics *sk, struct stepcompress *sc
sk->last_flush_time = flush_time;
if (!sk->tq)
return 0;
trapq_check_sentinels(sk->tq);
struct move *m = list_first_entry(&sk->tq->moves, struct move, node);
while (last_flush_time >= m->print_time + m->move_t)
m = list_next_entry(m, node);
@ -248,6 +247,12 @@ itersolve_set_trapq(struct stepper_kinematics *sk, struct trapq *tq
sk->step_dist = step_dist;
}
struct trapq * __visible
itersolve_get_trapq(struct stepper_kinematics *sk)
{
return sk->tq;
}
double __visible
itersolve_calc_position_from_coord(struct stepper_kinematics *sk
, double x, double y, double z)
@ -273,3 +278,15 @@ itersolve_get_commanded_pos(struct stepper_kinematics *sk)
{
return sk->commanded_pos;
}
double __visible
itersolve_get_gen_steps_pre_active(struct stepper_kinematics *sk)
{
return sk->gen_steps_pre_active;
}
double __visible
itersolve_get_gen_steps_post_active(struct stepper_kinematics *sk)
{
return sk->gen_steps_post_active;
}

View file

@ -31,10 +31,13 @@ double itersolve_check_active(struct stepper_kinematics *sk, double flush_time);
int32_t itersolve_is_active_axis(struct stepper_kinematics *sk, char axis);
void itersolve_set_trapq(struct stepper_kinematics *sk, struct trapq *tq
, double step_dist);
struct trapq *itersolve_get_trapq(struct stepper_kinematics *sk);
double itersolve_calc_position_from_coord(struct stepper_kinematics *sk
, double x, double y, double z);
void itersolve_set_position(struct stepper_kinematics *sk
, double x, double y, double z);
double itersolve_get_commanded_pos(struct stepper_kinematics *sk);
double itersolve_get_gen_steps_pre_active(struct stepper_kinematics *sk);
double itersolve_get_gen_steps_post_active(struct stepper_kinematics *sk);
#endif // itersolve.h

View file

@ -1,7 +1,7 @@
// Kinematic input shapers to minimize motion vibrations in XY plane
//
// Copyright (C) 2019-2020 Kevin O'Connor <kevin@koconnor.net>
// Copyright (C) 2020 Dmitry Butyugin <dmbutyugin@google.com>
// Copyright (C) 2020-2025 Dmitry Butyugin <dmbutyugin@google.com>
//
// This file may be distributed under the terms of the GNU GPLv3 license.
@ -18,6 +18,8 @@
* Shaper initialization
****************************************************************/
static const int KIN_FLAGS[3] = { AF_X, AF_Y, AF_Z };
struct shaper_pulses {
int num_pulses;
struct {
@ -113,7 +115,7 @@ struct input_shaper {
struct stepper_kinematics sk;
struct stepper_kinematics *orig_sk;
struct move m;
struct shaper_pulses sx, sy;
struct shaper_pulses sp[3];
};
// Optimized calc_position when only x axis is needed
@ -122,9 +124,10 @@ shaper_x_calc_position(struct stepper_kinematics *sk, struct move *m
, double move_time)
{
struct input_shaper *is = container_of(sk, struct input_shaper, sk);
if (!is->sx.num_pulses)
struct shaper_pulses *sx = &is->sp[0];
if (!sx->num_pulses)
return is->orig_sk->calc_position_cb(is->orig_sk, m, move_time);
is->m.start_pos.x = calc_position(m, 'x', move_time, &is->sx);
is->m.start_pos.x = calc_position(m, 'x', move_time, sx);
return is->orig_sk->calc_position_cb(is->orig_sk, &is->m, DUMMY_T);
}
@ -134,25 +137,41 @@ shaper_y_calc_position(struct stepper_kinematics *sk, struct move *m
, double move_time)
{
struct input_shaper *is = container_of(sk, struct input_shaper, sk);
if (!is->sy.num_pulses)
struct shaper_pulses *sy = &is->sp[1];
if (!sy->num_pulses)
return is->orig_sk->calc_position_cb(is->orig_sk, m, move_time);
is->m.start_pos.y = calc_position(m, 'y', move_time, &is->sy);
is->m.start_pos.y = calc_position(m, 'y', move_time, sy);
return is->orig_sk->calc_position_cb(is->orig_sk, &is->m, DUMMY_T);
}
// General calc_position for both x and y axes
// Optimized calc_position when only z axis is needed
static double
shaper_xy_calc_position(struct stepper_kinematics *sk, struct move *m
, double move_time)
shaper_z_calc_position(struct stepper_kinematics *sk, struct move *m
, double move_time)
{
struct input_shaper *is = container_of(sk, struct input_shaper, sk);
if (!is->sx.num_pulses && !is->sy.num_pulses)
struct shaper_pulses *sz = &is->sp[2];
if (!sz->num_pulses)
return is->orig_sk->calc_position_cb(is->orig_sk, m, move_time);
is->m.start_pos.z = calc_position(m, 'z', move_time, sz);
return is->orig_sk->calc_position_cb(is->orig_sk, &is->m, DUMMY_T);
}
// General calc_position for all x, y, and z axes
static double
shaper_xyz_calc_position(struct stepper_kinematics *sk, struct move *m
, double move_time)
{
struct input_shaper *is = container_of(sk, struct input_shaper, sk);
if (!is->sp[0].num_pulses && !is->sp[1].num_pulses && !is->sp[2].num_pulses)
return is->orig_sk->calc_position_cb(is->orig_sk, m, move_time);
is->m.start_pos = move_get_coord(m, move_time);
if (is->sx.num_pulses)
is->m.start_pos.x = calc_position(m, 'x', move_time, &is->sx);
if (is->sy.num_pulses)
is->m.start_pos.y = calc_position(m, 'y', move_time, &is->sy);
if (is->sp[0].num_pulses)
is->m.start_pos.x = calc_position(m, 'x', move_time, &is->sp[0]);
if (is->sp[1].num_pulses)
is->m.start_pos.y = calc_position(m, 'y', move_time, &is->sp[1]);
if (is->sp[2].num_pulses)
is->m.start_pos.z = calc_position(m, 'z', move_time, &is->sp[2]);
return is->orig_sk->calc_position_cb(is->orig_sk, &is->m, DUMMY_T);
}
@ -170,15 +189,24 @@ static void
shaper_note_generation_time(struct input_shaper *is)
{
double pre_active = 0., post_active = 0.;
if ((is->sk.active_flags & AF_X) && is->sx.num_pulses) {
pre_active = is->sx.pulses[is->sx.num_pulses-1].t;
post_active = -is->sx.pulses[0].t;
struct shaper_pulses *sx = &is->sp[0];
if ((is->sk.active_flags & AF_X) && sx->num_pulses) {
pre_active = sx->pulses[sx->num_pulses-1].t;
post_active = -sx->pulses[0].t;
}
if ((is->sk.active_flags & AF_Y) && is->sy.num_pulses) {
pre_active = is->sy.pulses[is->sy.num_pulses-1].t > pre_active
? is->sy.pulses[is->sy.num_pulses-1].t : pre_active;
post_active = -is->sy.pulses[0].t > post_active
? -is->sy.pulses[0].t : post_active;
struct shaper_pulses *sy = &is->sp[1];
if ((is->sk.active_flags & AF_Y) && sy->num_pulses) {
pre_active = sy->pulses[sy->num_pulses-1].t > pre_active
? sy->pulses[sy->num_pulses-1].t : pre_active;
post_active = -sy->pulses[0].t > post_active
? -sy->pulses[0].t : post_active;
}
struct shaper_pulses *sz = &is->sp[2];
if ((is->sk.active_flags & AF_Z) && sz->num_pulses) {
pre_active = sz->pulses[sz->num_pulses-1].t > pre_active
? sz->pulses[sz->num_pulses-1].t : pre_active;
post_active = -sz->pulses[0].t > post_active
? -sz->pulses[0].t : post_active;
}
is->sk.gen_steps_pre_active = pre_active;
is->sk.gen_steps_post_active = post_active;
@ -188,12 +216,15 @@ void __visible
input_shaper_update_sk(struct stepper_kinematics *sk)
{
struct input_shaper *is = container_of(sk, struct input_shaper, sk);
if ((is->orig_sk->active_flags & (AF_X | AF_Y)) == (AF_X | AF_Y))
is->sk.calc_position_cb = shaper_xy_calc_position;
else if (is->orig_sk->active_flags & AF_X)
int kin_flags = is->orig_sk->active_flags & (AF_X | AF_Y | AF_Z);
if (kin_flags == AF_X)
is->sk.calc_position_cb = shaper_x_calc_position;
else if (is->orig_sk->active_flags & AF_Y)
else if (kin_flags == AF_Y)
is->sk.calc_position_cb = shaper_y_calc_position;
else if (kin_flags == AF_Z)
is->sk.calc_position_cb = shaper_z_calc_position;
else
is->sk.calc_position_cb = shaper_xyz_calc_position;
is->sk.active_flags = is->orig_sk->active_flags;
shaper_note_generation_time(is);
}
@ -207,8 +238,10 @@ input_shaper_set_sk(struct stepper_kinematics *sk
is->sk.calc_position_cb = shaper_x_calc_position;
else if (orig_sk->active_flags == AF_Y)
is->sk.calc_position_cb = shaper_y_calc_position;
else if (orig_sk->active_flags & (AF_X | AF_Y))
is->sk.calc_position_cb = shaper_xy_calc_position;
else if (orig_sk->active_flags == AF_Z)
is->sk.calc_position_cb = shaper_z_calc_position;
else if (orig_sk->active_flags & (AF_X | AF_Y | AF_Z))
is->sk.calc_position_cb = shaper_xyz_calc_position;
else
return -1;
is->sk.active_flags = orig_sk->active_flags;
@ -226,27 +259,20 @@ int __visible
input_shaper_set_shaper_params(struct stepper_kinematics *sk, char axis
, int n, double a[], double t[])
{
if (axis != 'x' && axis != 'y')
int axis_ind = axis-'x';
if (axis_ind < 0 || axis_ind >= ARRAY_SIZE(KIN_FLAGS))
return -1;
struct input_shaper *is = container_of(sk, struct input_shaper, sk);
struct shaper_pulses *sp = axis == 'x' ? &is->sx : &is->sy;
struct shaper_pulses *sp = &is->sp[axis_ind];
int status = 0;
// Ignore input shaper update if the axis is not active
if (is->orig_sk->active_flags & (axis == 'x' ? AF_X : AF_Y)) {
if (is->orig_sk->active_flags & KIN_FLAGS[axis_ind]) {
status = init_shaper(n, a, t, sp);
shaper_note_generation_time(is);
}
return status;
}
double __visible
input_shaper_get_step_generation_window(struct stepper_kinematics *sk)
{
struct input_shaper *is = container_of(sk, struct input_shaper, sk);
return is->sk.gen_steps_pre_active > is->sk.gen_steps_post_active
? is->sk.gen_steps_pre_active : is->sk.gen_steps_post_active;
}
struct stepper_kinematics * __visible
input_shaper_alloc(void)
{

View file

@ -116,6 +116,11 @@ list_join_tail(struct list_head *add, struct list_head *h)
; &pos->member != &(head)->root \
; pos = list_next_entry(pos, member))
#define list_for_each_entry_reverse(pos, head, member) \
for (pos = list_last_entry((head), typeof(*pos), member) \
; &pos->member != &(head)->root \
; pos = list_prev_entry(pos, member))
#define list_for_each_entry_safe(pos, n, head, member) \
for (pos = list_first_entry((head), typeof(*pos), member) \
, n = list_next_entry(pos, member) \

View file

@ -207,3 +207,14 @@ clock_from_time(struct clock_estimate *ce, double time)
{
return (int64_t)((time - ce->conv_time)*ce->est_freq + .5) + ce->conv_clock;
}
// Fill the fields of a 'struct clock_estimate'
void
clock_fill(struct clock_estimate *ce, double est_freq, double conv_time
, uint64_t conv_clock, uint64_t last_clock)
{
ce->est_freq = est_freq;
ce->conv_time = conv_time;
ce->conv_clock = conv_clock;
ce->last_clock = last_clock;
}

View file

@ -50,5 +50,7 @@ void message_queue_free(struct list_head *root);
uint64_t clock_from_clock32(struct clock_estimate *ce, uint32_t clock32);
double clock_to_time(struct clock_estimate *ce, uint64_t clock);
uint64_t clock_from_time(struct clock_estimate *ce, double time);
void clock_fill(struct clock_estimate *ce, double est_freq, double conv_time
, uint64_t conv_clock, uint64_t last_clock);
#endif // msgblock.h

View file

@ -10,7 +10,6 @@
#include <stdio.h> // fprintf
#include <string.h> // strerror
#include <time.h> // struct timespec
#include <linux/prctl.h> // PR_SET_NAME
#include <sys/prctl.h> // prctl
#include "compiler.h" // __visible
#include "pyhelper.h" // get_monotonic

View file

@ -909,10 +909,7 @@ serialqueue_set_clock_est(struct serialqueue *sq, double est_freq
, uint64_t last_clock)
{
pthread_mutex_lock(&sq->lock);
sq->ce.est_freq = est_freq;
sq->ce.conv_time = conv_time;
sq->ce.conv_clock = conv_clock;
sq->ce.last_clock = last_clock;
clock_fill(&sq->ce, est_freq, conv_time, conv_clock, last_clock);
pthread_mutex_unlock(&sq->lock);
}

View file

@ -21,7 +21,6 @@
#include <stdlib.h> // malloc
#include <string.h> // memset
#include "compiler.h" // DIV_ROUND_UP
#include "itersolve.h" // itersolve_generate_steps
#include "pyhelper.h" // errorf
#include "serialqueue.h" // struct queue_message
#include "stepcompress.h" // stepcompress_alloc
@ -37,7 +36,7 @@ struct stepcompress {
double mcu_time_offset, mcu_freq, last_step_print_time;
// Message generation
uint64_t last_step_clock;
struct list_head msg_queue;
struct list_head *msg_queue;
uint32_t oid;
int32_t queue_step_msgtag, set_next_step_dir_msgtag;
int sdir, invert_sdir;
@ -47,8 +46,6 @@ struct stepcompress {
// History tracking
int64_t last_position;
struct list_head history_list;
// Itersolve reference
struct stepper_kinematics *sk;
};
struct step_move {
@ -245,23 +242,23 @@ check_line(struct stepcompress *sc, struct step_move move)
****************************************************************/
// Allocate a new 'stepcompress' object
struct stepcompress * __visible
stepcompress_alloc(uint32_t oid)
struct stepcompress *
stepcompress_alloc(struct list_head *msg_queue)
{
struct stepcompress *sc = malloc(sizeof(*sc));
memset(sc, 0, sizeof(*sc));
list_init(&sc->msg_queue);
list_init(&sc->history_list);
sc->oid = oid;
sc->sdir = -1;
sc->msg_queue = msg_queue;
return sc;
}
// Fill message id information
void __visible
stepcompress_fill(struct stepcompress *sc, uint32_t max_error
stepcompress_fill(struct stepcompress *sc, uint32_t oid, uint32_t max_error
, int32_t queue_step_msgtag, int32_t set_next_step_dir_msgtag)
{
sc->oid = oid;
sc->max_error = max_error;
sc->queue_step_msgtag = queue_step_msgtag;
sc->set_next_step_dir_msgtag = set_next_step_dir_msgtag;
@ -294,13 +291,12 @@ stepcompress_history_expire(struct stepcompress *sc, uint64_t end_clock)
}
// Free memory associated with a 'stepcompress' object
void __visible
void
stepcompress_free(struct stepcompress *sc)
{
if (!sc)
return;
free(sc->queue);
message_queue_free(&sc->msg_queue);
stepcompress_history_expire(sc, UINT64_MAX);
free(sc);
}
@ -317,12 +313,6 @@ stepcompress_get_step_dir(struct stepcompress *sc)
return sc->next_step_dir;
}
struct list_head *
stepcompress_get_msg_queue(struct stepcompress *sc)
{
return &sc->msg_queue;
}
// Determine the "print time" of the last_step_clock
static void
calc_last_step_print_time(struct stepcompress *sc)
@ -360,7 +350,7 @@ add_move(struct stepcompress *sc, uint64_t first_clock, struct step_move *move)
qm->min_clock = qm->req_clock = sc->last_step_clock;
if (move->count == 1 && first_clock >= sc->last_step_clock + CLOCK_DIFF_MAX)
qm->req_clock = first_clock;
list_add_tail(&qm->node, &sc->msg_queue);
list_add_tail(&qm->node, sc->msg_queue);
sc->last_step_clock = last_clock;
// Create and store move in history tracking
@ -424,7 +414,7 @@ set_next_step_dir(struct stepcompress *sc, int sdir)
};
struct queue_message *qm = message_alloc_and_encode(msg, 3);
qm->req_clock = sc->last_step_clock;
list_add_tail(&qm->node, &sc->msg_queue);
list_add_tail(&qm->node, sc->msg_queue);
return 0;
}
@ -541,7 +531,7 @@ stepcompress_commit(struct stepcompress *sc)
}
// Flush pending steps
static int
int
stepcompress_flush(struct stepcompress *sc, uint64_t move_clock)
{
if (sc->next_step_clock && move_clock >= sc->next_step_clock) {
@ -613,35 +603,6 @@ stepcompress_find_past_position(struct stepcompress *sc, uint64_t clock)
return last_position;
}
// Queue an mcu command to go out in order with stepper commands
int __visible
stepcompress_queue_msg(struct stepcompress *sc, uint32_t *data, int len)
{
int ret = stepcompress_flush(sc, UINT64_MAX);
if (ret)
return ret;
struct queue_message *qm = message_alloc_and_encode(data, len);
qm->req_clock = sc->last_step_clock;
list_add_tail(&qm->node, &sc->msg_queue);
return 0;
}
// Queue an mcu command that will consume space in the mcu move queue
int __visible
stepcompress_queue_mq_msg(struct stepcompress *sc, uint64_t req_clock
, uint32_t *data, int len)
{
int ret = stepcompress_flush(sc, UINT64_MAX);
if (ret)
return ret;
struct queue_message *qm = message_alloc_and_encode(data, len);
qm->min_clock = qm->req_clock = req_clock;
list_add_tail(&qm->node, &sc->msg_queue);
return 0;
}
// Return history of queue_step commands
int __visible
stepcompress_extract_old(struct stepcompress *sc, struct pull_history_steps *p
@ -665,26 +626,3 @@ stepcompress_extract_old(struct stepcompress *sc, struct pull_history_steps *p
}
return res;
}
// Store a reference to stepper_kinematics
void __visible
stepcompress_set_stepper_kinematics(struct stepcompress *sc
, struct stepper_kinematics *sk)
{
sc->sk = sk;
}
// Generate steps (via itersolve) and flush
int32_t
stepcompress_generate_steps(struct stepcompress *sc, double gen_steps_time
, uint64_t flush_clock)
{
if (!sc->sk)
return 0;
// Generate steps
int32_t ret = itersolve_generate_steps(sc->sk, sc, gen_steps_time);
if (ret)
return ret;
// Flush steps
return stepcompress_flush(sc, flush_clock);
}

View file

@ -11,8 +11,9 @@ struct pull_history_steps {
int step_count, interval, add;
};
struct stepcompress *stepcompress_alloc(uint32_t oid);
void stepcompress_fill(struct stepcompress *sc, uint32_t max_error
struct list_head;
struct stepcompress *stepcompress_alloc(struct list_head *msg_queue);
void stepcompress_fill(struct stepcompress *sc, uint32_t oid, uint32_t max_error
, int32_t queue_step_msgtag
, int32_t set_next_step_dir_msgtag);
void stepcompress_set_invert_sdir(struct stepcompress *sc
@ -21,28 +22,19 @@ void stepcompress_history_expire(struct stepcompress *sc, uint64_t end_clock);
void stepcompress_free(struct stepcompress *sc);
uint32_t stepcompress_get_oid(struct stepcompress *sc);
int stepcompress_get_step_dir(struct stepcompress *sc);
struct list_head *stepcompress_get_msg_queue(struct stepcompress *sc);
void stepcompress_set_time(struct stepcompress *sc
, double time_offset, double mcu_freq);
int stepcompress_append(struct stepcompress *sc, int sdir
, double print_time, double step_time);
int stepcompress_commit(struct stepcompress *sc);
int stepcompress_flush(struct stepcompress *sc, uint64_t move_clock);
int stepcompress_reset(struct stepcompress *sc, uint64_t last_step_clock);
int stepcompress_set_last_position(struct stepcompress *sc, uint64_t clock
, int64_t last_position);
int64_t stepcompress_find_past_position(struct stepcompress *sc
, uint64_t clock);
int stepcompress_queue_msg(struct stepcompress *sc, uint32_t *data, int len);
int stepcompress_queue_mq_msg(struct stepcompress *sc, uint64_t req_clock
, uint32_t *data, int len);
int stepcompress_extract_old(struct stepcompress *sc
, struct pull_history_steps *p, int max
, uint64_t start_clock, uint64_t end_clock);
struct stepper_kinematics;
void stepcompress_set_stepper_kinematics(struct stepcompress *sc
, struct stepper_kinematics *sk);
int32_t stepcompress_generate_steps(struct stepcompress *sc
, double gen_steps_time
, uint64_t flush_clock);
#endif // stepcompress.h

View file

@ -11,57 +11,250 @@
// mcu step queue is ordered between steppers so that no stepper
// starves the other steppers of space in the mcu step queue.
#include <pthread.h> // pthread_mutex_lock
#include <stddef.h> // offsetof
#include <stdlib.h> // malloc
#include <string.h> // memset
#include "compiler.h" // __visible
#include "pyhelper.h" // set_thread_name
#include "itersolve.h" // itersolve_generate_steps
#include "serialqueue.h" // struct queue_message
#include "stepcompress.h" // stepcompress_flush
#include "steppersync.h" // steppersync_alloc
#include "trapq.h" // trapq_check_sentinels
/****************************************************************
* SyncEmitter - message generation for each stepper
****************************************************************/
struct syncemitter {
// List node for storage in steppersync list
struct list_node ss_node;
// Transmit message queue
struct list_head msg_queue;
// Thread for step generation
struct stepcompress *sc;
struct stepper_kinematics *sk;
char name[16];
pthread_t tid;
pthread_mutex_t lock; // protects variables below
pthread_cond_t cond;
int have_work;
double bg_gen_steps_time;
uint64_t bg_flush_clock, bg_clear_history_clock;
int32_t bg_result;
};
// Return this emitters 'struct stepcompress' (or NULL if not allocated)
struct stepcompress * __visible
syncemitter_get_stepcompress(struct syncemitter *se)
{
return se->sc;
}
// Store a reference to stepper_kinematics
void __visible
syncemitter_set_stepper_kinematics(struct syncemitter *se
, struct stepper_kinematics *sk)
{
se->sk = sk;
}
// Report current stepper_kinematics
struct stepper_kinematics * __visible
syncemitter_get_stepper_kinematics(struct syncemitter *se)
{
return se->sk;
}
// Queue an mcu command that will consume space in the mcu move queue
void __visible
syncemitter_queue_msg(struct syncemitter *se, uint64_t req_clock
, uint32_t *data, int len)
{
struct queue_message *qm = message_alloc_and_encode(data, len);
qm->min_clock = qm->req_clock = req_clock;
list_add_tail(&qm->node, &se->msg_queue);
}
// Generate steps (via itersolve) and flush
static int32_t
se_generate_steps(struct syncemitter *se)
{
if (!se->sc || !se->sk)
return 0;
double gen_steps_time = se->bg_gen_steps_time;
uint64_t flush_clock = se->bg_flush_clock;
uint64_t clear_history_clock = se->bg_clear_history_clock;
// Generate steps
int32_t ret = itersolve_generate_steps(se->sk, se->sc, gen_steps_time);
if (ret)
return ret;
// Flush steps
ret = stepcompress_flush(se->sc, flush_clock);
if (ret)
return ret;
// Clear history
stepcompress_history_expire(se->sc, clear_history_clock);
return 0;
}
// Main background thread for generating steps
static void *
se_background_thread(void *data)
{
struct syncemitter *se = data;
set_thread_name(se->name);
pthread_mutex_lock(&se->lock);
for (;;) {
if (!se->have_work) {
pthread_cond_wait(&se->cond, &se->lock);
continue;
}
if (se->have_work < 0)
// Exit request
break;
// Request to generate steps
se->bg_result = se_generate_steps(se);
if (se->bg_result)
errorf("Error in syncemitter '%s' step generation", se->name);
se->have_work = 0;
pthread_cond_signal(&se->cond);
}
pthread_mutex_unlock(&se->lock);
return NULL;
}
// Signal background thread to start step generation
static void
se_start_gen_steps(struct syncemitter *se, double gen_steps_time
, uint64_t flush_clock, uint64_t clear_history_clock)
{
if (!se->sc || !se->sk)
return;
pthread_mutex_lock(&se->lock);
while (se->have_work)
pthread_cond_wait(&se->cond, &se->lock);
se->bg_gen_steps_time = gen_steps_time;
se->bg_flush_clock = flush_clock;
se->bg_clear_history_clock = clear_history_clock;
se->have_work = 1;
pthread_mutex_unlock(&se->lock);
pthread_cond_signal(&se->cond);
}
// Wait for background thread to complete last step generation request
static int32_t
se_finalize_gen_steps(struct syncemitter *se)
{
if (!se->sc || !se->sk)
return 0;
pthread_mutex_lock(&se->lock);
while (se->have_work)
pthread_cond_wait(&se->cond, &se->lock);
int32_t res = se->bg_result;
pthread_mutex_unlock(&se->lock);
return res;
}
// Allocate syncemitter and start thread
static struct syncemitter *
syncemitter_alloc(char name[16], int alloc_stepcompress)
{
struct syncemitter *se = malloc(sizeof(*se));
memset(se, 0, sizeof(*se));
list_init(&se->msg_queue);
strncpy(se->name, name, sizeof(se->name));
se->name[sizeof(se->name)-1] = '\0';
if (!alloc_stepcompress)
return se;
se->sc = stepcompress_alloc(&se->msg_queue);
int ret = pthread_mutex_init(&se->lock, NULL);
if (ret)
goto fail;
ret = pthread_cond_init(&se->cond, NULL);
if (ret)
goto fail;
ret = pthread_create(&se->tid, NULL, se_background_thread, se);
if (ret)
goto fail;
return se;
fail:
report_errno("se alloc", ret);
return NULL;
}
// Free syncemitter and exit background thread
static void
syncemitter_free(struct syncemitter *se)
{
if (!se)
return;
if (se->sc) {
pthread_mutex_lock(&se->lock);
while (se->have_work)
pthread_cond_wait(&se->cond, &se->lock);
se->have_work = -1;
pthread_cond_signal(&se->cond);
pthread_mutex_unlock(&se->lock);
int ret = pthread_join(se->tid, NULL);
if (ret)
report_errno("se pthread_join", ret);
stepcompress_free(se->sc);
}
message_queue_free(&se->msg_queue);
free(se);
}
/****************************************************************
* StepperSync - sort move queue for a micro-controller
****************************************************************/
struct steppersync {
// List node for storage in steppersyncmgr list
struct list_node ssm_node;
// Serial port
struct serialqueue *sq;
struct command_queue *cq;
// Storage for associated stepcompress objects
struct stepcompress **sc_list;
int sc_num;
// The syncemitters that generate messages on this mcu
struct list_head se_list;
// Convert from time to clock
struct clock_estimate ce;
// Storage for list of pending move clocks
uint64_t *move_clocks;
int num_move_clocks;
};
// Allocate a new 'steppersync' object
struct steppersync * __visible
steppersync_alloc(struct serialqueue *sq, struct stepcompress **sc_list
, int sc_num, int move_num)
// Allocate a new syncemitter instance
struct syncemitter * __visible
steppersync_alloc_syncemitter(struct steppersync *ss, char name[16]
, int alloc_stepcompress)
{
struct steppersync *ss = malloc(sizeof(*ss));
memset(ss, 0, sizeof(*ss));
struct syncemitter *se = syncemitter_alloc(name, alloc_stepcompress);
if (se)
list_add_tail(&se->ss_node, &ss->se_list);
return se;
}
// Fill information on mcu move queue
void __visible
steppersync_setup_movequeue(struct steppersync *ss, struct serialqueue *sq
, int move_num)
{
serialqueue_free_commandqueue(ss->cq);
free(ss->move_clocks);
ss->sq = sq;
ss->cq = serialqueue_alloc_commandqueue();
ss->sc_list = malloc(sizeof(*sc_list)*sc_num);
memcpy(ss->sc_list, sc_list, sizeof(*sc_list)*sc_num);
ss->sc_num = sc_num;
ss->move_clocks = malloc(sizeof(*ss->move_clocks)*move_num);
memset(ss->move_clocks, 0, sizeof(*ss->move_clocks)*move_num);
ss->num_move_clocks = move_num;
return ss;
}
// Free memory associated with a 'steppersync' object
void __visible
steppersync_free(struct steppersync *ss)
{
if (!ss)
return;
free(ss->sc_list);
free(ss->move_clocks);
serialqueue_free_commandqueue(ss->cq);
free(ss);
}
// Set the conversion rate of 'print_time' to mcu clock
@ -69,37 +262,11 @@ void __visible
steppersync_set_time(struct steppersync *ss, double time_offset
, double mcu_freq)
{
int i;
for (i=0; i<ss->sc_num; i++) {
struct stepcompress *sc = ss->sc_list[i];
stepcompress_set_time(sc, time_offset, mcu_freq);
}
}
// Generate steps and flush stepcompress objects
int32_t __visible
steppersync_generate_steps(struct steppersync *ss, double gen_steps_time
, uint64_t flush_clock)
{
int i;
for (i=0; i<ss->sc_num; i++) {
struct stepcompress *sc = ss->sc_list[i];
int32_t ret = stepcompress_generate_steps(sc, gen_steps_time
, flush_clock);
if (ret)
return ret;
}
return 0;
}
// Expire the stepcompress history before the given clock time
void __visible
steppersync_history_expire(struct steppersync *ss, uint64_t end_clock)
{
int i;
for (i = 0; i < ss->sc_num; i++) {
struct stepcompress *sc = ss->sc_list[i];
stepcompress_history_expire(sc, end_clock);
clock_fill(&ss->ce, mcu_freq, time_offset, 0, 0);
struct syncemitter *se;
list_for_each_entry(se, &ss->se_list, ss_node) {
if (se->sc)
stepcompress_set_time(se->sc, time_offset, mcu_freq);
}
}
@ -129,7 +296,7 @@ heap_replace(struct steppersync *ss, uint64_t req_clock)
}
// Find and transmit any scheduled steps prior to the given 'move_clock'
int __visible
static void
steppersync_flush(struct steppersync *ss, uint64_t move_clock)
{
// Order commands by the reqclock of each pending command
@ -139,13 +306,11 @@ steppersync_flush(struct steppersync *ss, uint64_t move_clock)
// Find message with lowest reqclock
uint64_t req_clock = MAX_CLOCK;
struct queue_message *qm = NULL;
int i;
for (i=0; i<ss->sc_num; i++) {
struct stepcompress *sc = ss->sc_list[i];
struct list_head *sc_mq = stepcompress_get_msg_queue(sc);
if (!list_empty(sc_mq)) {
struct syncemitter *se;
list_for_each_entry(se, &ss->se_list, ss_node) {
if (!list_empty(&se->msg_queue)) {
struct queue_message *m = list_first_entry(
sc_mq, struct queue_message, node);
&se->msg_queue, struct queue_message, node);
if (m->req_clock < req_clock) {
qm = m;
req_clock = m->req_clock;
@ -172,6 +337,100 @@ steppersync_flush(struct steppersync *ss, uint64_t move_clock)
// Transmit commands
if (!list_empty(&msgs))
serialqueue_send_batch(ss->sq, ss->cq, &msgs);
return 0;
}
/****************************************************************
* StepperSyncMgr - manage a list of steppersync
****************************************************************/
struct steppersyncmgr {
struct list_head ss_list;
};
// Allocate a new 'steppersyncmgr' object
struct steppersyncmgr * __visible
steppersyncmgr_alloc(void)
{
struct steppersyncmgr *ssm = malloc(sizeof(*ssm));
memset(ssm, 0, sizeof(*ssm));
list_init(&ssm->ss_list);
return ssm;
}
// Free memory associated with a 'steppersync' object
void __visible
steppersyncmgr_free(struct steppersyncmgr *ssm)
{
if (!ssm)
return;
while (!list_empty(&ssm->ss_list)) {
struct steppersync *ss = list_first_entry(
&ssm->ss_list, struct steppersync, ssm_node);
list_del(&ss->ssm_node);
free(ss->move_clocks);
serialqueue_free_commandqueue(ss->cq);
while (!list_empty(&ss->se_list)) {
struct syncemitter *se = list_first_entry(
&ss->se_list, struct syncemitter, ss_node);
list_del(&se->ss_node);
syncemitter_free(se);
}
free(ss);
}
free(ssm);
}
// Allocate a new 'steppersync' object
struct steppersync * __visible
steppersyncmgr_alloc_steppersync(struct steppersyncmgr *ssm)
{
struct steppersync *ss = malloc(sizeof(*ss));
memset(ss, 0, sizeof(*ss));
list_init(&ss->se_list);
list_add_tail(&ss->ssm_node, &ssm->ss_list);
return ss;
}
// Generate and flush steps
int32_t __visible
steppersyncmgr_gen_steps(struct steppersyncmgr *ssm, double flush_time
, double gen_steps_time, double clear_history_time)
{
struct steppersync *ss;
// Prepare trapqs for step generation
list_for_each_entry(ss, &ssm->ss_list, ssm_node) {
struct syncemitter *se;
list_for_each_entry(se, &ss->se_list, ss_node) {
if (!se->sc || !se->sk)
continue;
struct trapq *tq = itersolve_get_trapq(se->sk);
if (tq)
trapq_check_sentinels(tq);
}
}
// Start step generation threads
list_for_each_entry(ss, &ssm->ss_list, ssm_node) {
uint64_t flush_clock = clock_from_time(&ss->ce, flush_time);
uint64_t clear_clock = clock_from_time(&ss->ce, clear_history_time);
struct syncemitter *se;
list_for_each_entry(se, &ss->se_list, ss_node) {
se_start_gen_steps(se, gen_steps_time, flush_clock, clear_clock);
}
}
// Wait for step generation threads to complete
int32_t res = 0;
list_for_each_entry(ss, &ssm->ss_list, ssm_node) {
struct syncemitter *se;
list_for_each_entry(se, &ss->se_list, ss_node) {
int32_t ret = se_finalize_gen_steps(se);
if (ret)
res = ret;
}
if (res)
continue;
uint64_t flush_clock = clock_from_time(&ss->ce, flush_time);
steppersync_flush(ss, flush_clock);
}
return res;
}

View file

@ -3,16 +3,30 @@
#include <stdint.h> // uint64_t
struct serialqueue;
struct steppersync *steppersync_alloc(
struct serialqueue *sq, struct stepcompress **sc_list, int sc_num
, int move_num);
void steppersync_free(struct steppersync *ss);
struct syncemitter;
struct stepcompress *syncemitter_get_stepcompress(struct syncemitter *se);
void syncemitter_set_stepper_kinematics(struct syncemitter *se
, struct stepper_kinematics *sk);
struct stepper_kinematics *syncemitter_get_stepper_kinematics(
struct syncemitter *se);
void syncemitter_queue_msg(struct syncemitter *se, uint64_t req_clock
, uint32_t *data, int len);
struct steppersync;
struct syncemitter *steppersync_alloc_syncemitter(
struct steppersync *ss, char name[16], int alloc_stepcompress);
void steppersync_setup_movequeue(struct steppersync *ss, struct serialqueue *sq
, int move_num);
void steppersync_set_time(struct steppersync *ss, double time_offset
, double mcu_freq);
int32_t steppersync_generate_steps(struct steppersync *ss, double gen_steps_time
, uint64_t flush_clock);
void steppersync_history_expire(struct steppersync *ss, uint64_t end_clock);
int steppersync_flush(struct steppersync *ss, uint64_t move_clock);
struct steppersyncmgr *steppersyncmgr_alloc(void);
void steppersyncmgr_free(struct steppersyncmgr *ssm);
struct serialqueue;
struct steppersync *steppersyncmgr_alloc_steppersync(
struct steppersyncmgr *ssm);
int32_t steppersyncmgr_gen_steps(struct steppersyncmgr *ssm, double flush_time
, double gen_steps_time
, double clear_history_time);
#endif // steppersync.h

View file

@ -49,6 +49,7 @@ trapq_alloc(void)
list_init(&tq->moves);
list_init(&tq->history);
struct move *head_sentinel = move_alloc(), *tail_sentinel = move_alloc();
head_sentinel->print_time = -1.0;
tail_sentinel->print_time = tail_sentinel->move_t = NEVER_TIME;
list_add_head(&head_sentinel->node, &tq->moves);
list_add_tail(&tail_sentinel->node, &tq->moves);
@ -103,7 +104,7 @@ trapq_add_move(struct trapq *tq, struct move *m)
// Add a null move to fill time gap
struct move *null_move = move_alloc();
null_move->start_pos = m->start_pos;
if (!prev->print_time && m->print_time > MAX_NULL_MOVE)
if (prev->print_time <= 0. && m->print_time > MAX_NULL_MOVE)
// Limit the first null move to improve numerical stability
null_move->print_time = m->print_time - MAX_NULL_MOVE;
else
@ -227,6 +228,22 @@ trapq_set_position(struct trapq *tq, double print_time
list_add_head(&m->node, &tq->history);
}
// Copy the info in a 'struct move' to a 'struct pull_move'
static void
copy_pull_move(struct pull_move *p, struct move *m)
{
p->print_time = m->print_time;
p->move_t = m->move_t;
p->start_v = m->start_v;
p->accel = 2. * m->half_accel;
p->start_x = m->start_pos.x;
p->start_y = m->start_pos.y;
p->start_z = m->start_pos.z;
p->x_r = m->axes_r.x;
p->y_r = m->axes_r.y;
p->z_r = m->axes_r.z;
}
// Return history of movement queue
int __visible
trapq_extract_old(struct trapq *tq, struct pull_move *p, int max
@ -234,21 +251,21 @@ trapq_extract_old(struct trapq *tq, struct pull_move *p, int max
{
int res = 0;
struct move *m;
list_for_each_entry_reverse(m, &tq->moves, node) {
if (start_time >= m->print_time + m->move_t || res >= max)
break;
if (end_time <= m->print_time || (!m->start_v && !m->half_accel))
continue;
copy_pull_move(p, m);
p++;
res++;
}
list_for_each_entry(m, &tq->history, node) {
if (start_time >= m->print_time + m->move_t || res >= max)
break;
if (end_time <= m->print_time)
continue;
p->print_time = m->print_time;
p->move_t = m->move_t;
p->start_v = m->start_v;
p->accel = 2. * m->half_accel;
p->start_x = m->start_pos.x;
p->start_y = m->start_pos.y;
p->start_z = m->start_pos.z;
p->x_r = m->axes_r.x;
p->y_r = m->axes_r.y;
p->z_r = m->axes_r.z;
copy_pull_move(p, m);
p++;
res++;
}

View file

@ -212,6 +212,12 @@ class MCU_I2C:
"i2c_read oid=%c reg=%*s read_len=%u",
"i2c_read_response oid=%c response=%*s", oid=self.oid,
cq=self.cmd_queue)
def i2c_write_noack(self, data, minclock=0, reqclock=0):
if self.i2c_write_cmd is None:
self._to_write.append(data)
return
self.i2c_write_cmd.send([self.oid, data],
minclock=minclock, reqclock=reqclock)
def i2c_write(self, data, minclock=0, reqclock=0):
if self.i2c_write_cmd is None:
self._to_write.append(data)

View file

@ -236,6 +236,8 @@ class PrinterLCD:
except:
logging.exception("Error during display screen update")
self.lcd_chip.flush()
if self.redraw_request_pending:
return self.redraw_time
return eventtime + REDRAW_TIME
def request_redraw(self):
if self.redraw_request_pending:

View file

@ -138,7 +138,7 @@ class I2C:
hdr = 0x00
cmds = bytearray(cmds)
cmds.insert(0, hdr)
self.i2c.i2c_write(cmds, reqclock=BACKGROUND_PRIORITY_CLOCK)
self.i2c.i2c_write_noack(cmds, reqclock=BACKGROUND_PRIORITY_CLOCK)
# Helper code for toggling a reset pin on startup
class ResetHelper:

View file

@ -15,6 +15,8 @@ class PrinterExtruderStepper:
self.handle_connect)
def handle_connect(self):
self.extruder_stepper.sync_to_extruder(self.extruder_name)
def find_past_position(self, print_time):
return self.extruder_stepper.find_past_position(print_time)
def get_status(self, eventtime):
return self.extruder_stepper.get_status(eventtime)

View file

@ -1,6 +1,6 @@
# Utility for manually moving a stepper for diagnostic purposes
#
# Copyright (C) 2018-2019 Kevin O'Connor <kevin@koconnor.net>
# Copyright (C) 2018-2025 Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import math, logging
@ -10,7 +10,6 @@ BUZZ_DISTANCE = 1.
BUZZ_VELOCITY = BUZZ_DISTANCE / .250
BUZZ_RADIANS_DISTANCE = math.radians(1.)
BUZZ_RADIANS_VELOCITY = BUZZ_RADIANS_DISTANCE / .250
STALL_TIME = 0.100
# Calculate a move's accel_t, cruise_t, and cruise_v
def calc_move_time(dist, speed, accel):
@ -56,24 +55,16 @@ class ForceMove:
raise self.printer.config_error("Unknown stepper %s" % (name,))
return self.steppers[name]
def _force_enable(self, stepper):
toolhead = self.printer.lookup_object('toolhead')
print_time = toolhead.get_last_move_time()
stepper_name = stepper.get_name()
stepper_enable = self.printer.lookup_object('stepper_enable')
enable = stepper_enable.lookup_enable(stepper.get_name())
was_enable = enable.is_motor_enabled()
if not was_enable:
enable.motor_enable(print_time)
toolhead.dwell(STALL_TIME)
return was_enable
def _restore_enable(self, stepper, was_enable):
if not was_enable:
toolhead = self.printer.lookup_object('toolhead')
toolhead.dwell(STALL_TIME)
print_time = toolhead.get_last_move_time()
stepper_enable = self.printer.lookup_object('stepper_enable')
enable = stepper_enable.lookup_enable(stepper.get_name())
enable.motor_disable(print_time)
toolhead.dwell(STALL_TIME)
did_enable = stepper_enable.set_motors_enable([stepper_name], True)
return did_enable
def _restore_enable(self, stepper, did_enable):
if not did_enable:
return
stepper_name = stepper.get_name()
stepper_enable = self.printer.lookup_object('stepper_enable')
stepper_enable.set_motors_enable([stepper_name], False)
def manual_move(self, stepper, dist, speed, accel=0.):
toolhead = self.printer.lookup_object('toolhead')
toolhead.flush_step_generation()
@ -85,7 +76,7 @@ class ForceMove:
self.trapq_append(self.trapq, print_time, accel_t, cruise_t, accel_t,
0., 0., 0., axis_r, 0., 0., 0., cruise_v, accel)
print_time = print_time + accel_t + cruise_t + accel_t
toolhead.note_mcu_movequeue_activity(print_time)
self.motion_queuing.note_mcu_movequeue_activity(print_time)
toolhead.dwell(accel_t + cruise_t + accel_t)
toolhead.flush_step_generation()
stepper.set_trapq(prev_trapq)
@ -100,7 +91,7 @@ class ForceMove:
def cmd_STEPPER_BUZZ(self, gcmd):
stepper = self._lookup_stepper(gcmd)
logging.info("Stepper buzz %s", stepper.get_name())
was_enable = self._force_enable(stepper)
did_enable = self._force_enable(stepper)
toolhead = self.printer.lookup_object('toolhead')
dist, speed = BUZZ_DISTANCE, BUZZ_VELOCITY
if stepper.units_in_radians():
@ -110,7 +101,7 @@ class ForceMove:
toolhead.dwell(.050)
self.manual_move(stepper, -dist, speed)
toolhead.dwell(.450)
self._restore_enable(stepper, was_enable)
self._restore_enable(stepper, did_enable)
cmd_FORCE_MOVE_help = "Manually move a stepper; invalidates kinematics"
def cmd_FORCE_MOVE(self, gcmd):
stepper = self._lookup_stepper(gcmd)

View file

@ -92,7 +92,7 @@ class GCodeMove:
def _get_gcode_position(self):
p = [lp - bp for lp, bp in zip(self.last_position, self.base_position)]
p[3] /= self.extrude_factor
return p
return p[:4]
def _get_gcode_speed(self):
return self.speed / self.speed_factor
def _get_gcode_speed_override(self):
@ -107,7 +107,7 @@ class GCodeMove:
'absolute_extrude': self.absolute_extrude,
'homing_origin': self.Coord(*self.homing_position[:4]),
'position': self.Coord(*self.last_position[:4]),
'gcode_position': self.Coord(*move_position[:4]),
'gcode_position': self.Coord(*move_position),
}
def reset_last_position(self):
if self.is_printer_ready:

View file

@ -1,7 +1,7 @@
# Kinematic input shaper to minimize motion vibrations in XY plane
#
# Copyright (C) 2019-2020 Kevin O'Connor <kevin@koconnor.net>
# Copyright (C) 2020 Dmitry Butyugin <dmbutyugin@google.com>
# Copyright (C) 2020-2025 Dmitry Butyugin <dmbutyugin@google.com>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import collections
@ -11,34 +11,39 @@ from . import shaper_defs
class InputShaperParams:
def __init__(self, axis, config):
self.axis = axis
self.shapers = {s.name : s.init_func for s in shaper_defs.INPUT_SHAPERS}
self.shapers = {s.name : s for s in shaper_defs.INPUT_SHAPERS}
shaper_type = config.get('shaper_type', 'mzv')
self.shaper_type = config.get('shaper_type_' + axis, shaper_type)
if self.shaper_type not in self.shapers:
raise config.error(
'Unsupported shaper type: %s' % (self.shaper_type,))
self.damping_ratio = config.getfloat('damping_ratio_' + axis,
shaper_defs.DEFAULT_DAMPING_RATIO,
minval=0., maxval=1.)
self.damping_ratio = config.getfloat(
'damping_ratio_' + axis,
shaper_defs.DEFAULT_DAMPING_RATIO, minval=0.,
maxval=self.shapers[self.shaper_type].max_damping_ratio)
self.shaper_freq = config.getfloat('shaper_freq_' + axis, 0., minval=0.)
def update(self, gcmd):
axis = self.axis.upper()
self.damping_ratio = gcmd.get_float('DAMPING_RATIO_' + axis,
self.damping_ratio,
minval=0., maxval=1.)
self.shaper_freq = gcmd.get_float('SHAPER_FREQ_' + axis,
self.shaper_freq, minval=0.)
shaper_type = gcmd.get('SHAPER_TYPE', None)
if shaper_type is None:
shaper_type = gcmd.get('SHAPER_TYPE_' + axis, self.shaper_type)
if shaper_type.lower() not in self.shapers:
raise gcmd.error('Unsupported shaper type: %s' % (shaper_type,))
damping_ratio = gcmd.get_float('DAMPING_RATIO_' + axis,
self.damping_ratio, minval=0.)
if damping_ratio > self.shapers[shaper_type.lower()].max_damping_ratio:
raise gcmd.error(
'Too high value of damping_ratio=%.3f for shaper %s'
' on axis %c' % (damping_ratio, shaper_type, axis))
self.shaper_freq = gcmd.get_float('SHAPER_FREQ_' + axis,
self.shaper_freq, minval=0.)
self.damping_ratio = damping_ratio
self.shaper_type = shaper_type.lower()
def get_shaper(self):
if not self.shaper_freq:
A, T = shaper_defs.get_none_shaper()
else:
A, T = self.shapers[self.shaper_type](
A, T = self.shapers[self.shaper_type].init_func(
self.shaper_freq, self.damping_ratio)
return len(A), A, T
def get_status(self):
@ -95,7 +100,8 @@ class InputShaper:
self._update_kinematics)
self.toolhead = None
self.shapers = [AxisInputShaper('x', config),
AxisInputShaper('y', config)]
AxisInputShaper('y', config),
AxisInputShaper('z', config)]
self.input_shaper_stepper_kinematics = []
self.orig_stepper_kinematics = []
# Register gcode commands
@ -138,6 +144,7 @@ class InputShaper:
if self.toolhead is None:
# Klipper initialization is not yet completed
return
self.toolhead.flush_step_generation()
ffi_main, ffi_lib = chelper.get_ffi()
kin = self.toolhead.get_kinematics()
for s in kin.get_steppers():
@ -146,12 +153,9 @@ class InputShaper:
is_sk = self._get_input_shaper_stepper_kinematics(s)
if is_sk is None:
continue
old_delay = ffi_lib.input_shaper_get_step_generation_window(is_sk)
ffi_lib.input_shaper_update_sk(is_sk)
new_delay = ffi_lib.input_shaper_get_step_generation_window(is_sk)
if old_delay != new_delay:
self.toolhead.note_step_generation_scan_time(new_delay,
old_delay)
motion_queuing = self.printer.lookup_object("motion_queuing")
motion_queuing.check_step_generation_scan_windows()
def _update_input_shaping(self, error=None):
self.toolhead.flush_step_generation()
ffi_main, ffi_lib = chelper.get_ffi()
@ -163,16 +167,13 @@ class InputShaper:
is_sk = self._get_input_shaper_stepper_kinematics(s)
if is_sk is None:
continue
old_delay = ffi_lib.input_shaper_get_step_generation_window(is_sk)
for shaper in self.shapers:
if shaper in failed_shapers:
continue
if not shaper.set_shaper_kinematics(is_sk):
failed_shapers.append(shaper)
new_delay = ffi_lib.input_shaper_get_step_generation_window(is_sk)
if old_delay != new_delay:
self.toolhead.note_step_generation_scan_time(new_delay,
old_delay)
motion_queuing = self.printer.lookup_object("motion_queuing")
motion_queuing.check_step_generation_scan_windows()
if failed_shapers:
error = error or self.printer.command_error
raise error("Failed to configure shaper(s) %s with given parameters"
@ -191,8 +192,9 @@ class InputShaper:
for shaper in self.shapers:
shaper.update(gcmd)
self._update_input_shaping()
for shaper in self.shapers:
shaper.report(gcmd)
for ind, shaper in enumerate(self.shapers):
if ind < 2 or shaper.is_enabled():
shaper.report(gcmd)
def load_config(config):
return InputShaper(config)

View file

@ -4,7 +4,7 @@
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import logging
import stepper, chelper
import stepper
from . import force_move
class ManualStepper:
@ -49,17 +49,9 @@ class ManualStepper:
else:
self.next_cmd_time = print_time
def do_enable(self, enable):
self.sync_print_time()
stepper_names = [s.get_name() for s in self.steppers]
stepper_enable = self.printer.lookup_object('stepper_enable')
if enable:
for s in self.steppers:
se = stepper_enable.lookup_enable(s.get_name())
se.motor_enable(self.next_cmd_time)
else:
for s in self.steppers:
se = stepper_enable.lookup_enable(s.get_name())
se.motor_disable(self.next_cmd_time)
self.sync_print_time()
stepper_enable.set_motors_enable(stepper_names, enable)
def do_set_position(self, setpos):
toolhead = self.printer.lookup_object('toolhead')
toolhead.flush_step_generation()
@ -80,8 +72,7 @@ class ManualStepper:
self.sync_print_time()
self.next_cmd_time = self._submit_move(self.next_cmd_time, movepos,
speed, accel)
toolhead = self.printer.lookup_object('toolhead')
toolhead.note_mcu_movequeue_activity(self.next_cmd_time)
self.motion_queuing.note_mcu_movequeue_activity(self.next_cmd_time)
if sync:
self.sync_print_time()
def do_homing_move(self, movepos, speed, accel, triggered, check_trigger):
@ -205,13 +196,13 @@ class ManualStepper:
def drip_move(self, newpos, speed, drip_completion):
# Submit move to trapq
self.sync_print_time()
maxtime = self._submit_move(self.next_cmd_time, newpos[0],
speed, self.homing_accel)
start_time = self.next_cmd_time
end_time = self._submit_move(start_time, newpos[0],
speed, self.homing_accel)
# Drip updates to motors
toolhead = self.printer.lookup_object('toolhead')
toolhead.drip_update_time(maxtime, drip_completion)
self.motion_queuing.drip_update_time(start_time, end_time,
drip_completion)
# Clear trapq of any remaining parts of movement
reactor = self.printer.get_reactor()
self.motion_queuing.wipe_trapq(self.trapq)
self.rail.set_position([self.commanded_pos, 0., 0.])
self.sync_print_time()

View file

@ -6,95 +6,269 @@
import logging
import chelper
BGFLUSH_LOW_TIME = 0.200
BGFLUSH_HIGH_TIME = 0.400
BGFLUSH_SG_LOW_TIME = 0.450
BGFLUSH_SG_HIGH_TIME = 0.700
BGFLUSH_EXTRA_TIME = 0.250
MOVE_HISTORY_EXPIRE = 30.
MIN_KIN_TIME = 0.100
STEPCOMPRESS_FLUSH_TIME = 0.050
SDS_CHECK_TIME = 0.001 # step+dir+step filter in stepcompress.c
DRIP_SEGMENT_TIME = 0.050
DRIP_TIME = 0.100
class PrinterMotionQueuing:
def __init__(self, config):
self.printer = config.get_printer()
self.printer = printer = config.get_printer()
self.reactor = printer.get_reactor()
# C trapq tracking
self.trapqs = []
self.stepcompress = []
self.steppersyncs = []
self.flush_callbacks = []
ffi_main, ffi_lib = chelper.get_ffi()
self.trapq_finalize_moves = ffi_lib.trapq_finalize_moves
self.steppersync_generate_steps = ffi_lib.steppersync_generate_steps
self.steppersync_flush = ffi_lib.steppersync_flush
self.steppersync_history_expire = ffi_lib.steppersync_history_expire
# C steppersync tracking
self.steppersyncmgr = ffi_main.gc(ffi_lib.steppersyncmgr_alloc(),
ffi_lib.steppersyncmgr_free)
self.syncemitters = []
self.steppersyncs = []
self.steppersyncmgr_gen_steps = ffi_lib.steppersyncmgr_gen_steps
# History expiration
self.clear_history_time = 0.
is_debug = self.printer.get_start_args().get('debugoutput') is not None
self.is_debugoutput = is_debug
# Flush notification callbacks
self.flush_callbacks = []
# Kinematic step generation scan window time tracking
self.kin_flush_delay = SDS_CHECK_TIME
# MCU tracking
self.all_mcus = [m for n, m in printer.lookup_objects(module='mcu')]
self.mcu = self.all_mcus[0]
self.can_pause = True
if self.mcu.is_fileoutput():
self.can_pause = False
# Flush tracking
flush_handler = self._flush_handler
if not self.can_pause:
flush_handler = self._flush_handler_debug
self.flush_timer = self.reactor.register_timer(flush_handler)
self.do_kick_flush_timer = True
self.last_flush_time = self.last_step_gen_time = 0.
self.need_flush_time = self.need_step_gen_time = 0.
# Register handlers
printer.register_event_handler("klippy:shutdown", self._handle_shutdown)
# C trapq tracking
def allocate_trapq(self):
ffi_main, ffi_lib = chelper.get_ffi()
trapq = ffi_main.gc(ffi_lib.trapq_alloc(), ffi_lib.trapq_free)
self.trapqs.append(trapq)
return trapq
def allocate_stepcompress(self, mcu, oid):
def wipe_trapq(self, trapq):
# Expire any remaining movement in the trapq (force to history list)
self.trapq_finalize_moves(trapq, self.reactor.NEVER, 0.)
def lookup_trapq_append(self):
ffi_main, ffi_lib = chelper.get_ffi()
sc = ffi_main.gc(ffi_lib.stepcompress_alloc(oid),
ffi_lib.stepcompress_free)
self.stepcompress.append((mcu, sc))
return sc
def allocate_steppersync(self, mcu, serialqueue, move_count):
stepqueues = []
for sc_mcu, sc in self.stepcompress:
if sc_mcu is mcu:
stepqueues.append(sc)
return ffi_lib.trapq_append
# C steppersync tracking
def _lookup_steppersync(self, mcu):
for ss_mcu, ss in self.steppersyncs:
if ss_mcu is mcu:
return ss
ffi_main, ffi_lib = chelper.get_ffi()
ss = ffi_main.gc(
ffi_lib.steppersync_alloc(serialqueue, stepqueues, len(stepqueues),
move_count),
ffi_lib.steppersync_free)
ss = ffi_lib.steppersyncmgr_alloc_steppersync(self.steppersyncmgr)
self.steppersyncs.append((mcu, ss))
return ss
def register_flush_callback(self, callback):
self.flush_callbacks.append(callback)
def allocate_syncemitter(self, mcu, name, alloc_stepcompress=True):
name = name.encode("utf-8")[:15]
ss = self._lookup_steppersync(mcu)
ffi_main, ffi_lib = chelper.get_ffi()
se = ffi_lib.steppersync_alloc_syncemitter(ss, name, alloc_stepcompress)
self.syncemitters.append(se)
return se
def setup_mcu_movequeue(self, mcu, serialqueue, move_count):
# Setup steppersync object for the mcu's main movequeue
ffi_main, ffi_lib = chelper.get_ffi()
ss = self._lookup_steppersync(mcu)
ffi_lib.steppersync_setup_movequeue(ss, serialqueue, move_count)
mcu_freq = float(mcu.seconds_to_clock(1.))
ffi_lib.steppersync_set_time(ss, 0., mcu_freq)
def stats(self, eventtime):
# Globally calibrate mcu clocks (and step generation clocks)
sync_time = self.last_step_gen_time
ffi_main, ffi_lib = chelper.get_ffi()
for mcu, ss in self.steppersyncs:
offset, freq = mcu.calibrate_clock(sync_time, eventtime)
ffi_lib.steppersync_set_time(ss, offset, freq)
# Calculate history expiration
est_print_time = self.mcu.estimated_print_time(eventtime)
self.clear_history_time = max(0., est_print_time - MOVE_HISTORY_EXPIRE)
return False, ""
# Flush notification callbacks
def register_flush_callback(self, callback, can_add_trapq=False):
if can_add_trapq:
self.flush_callbacks = [callback] + self.flush_callbacks
else:
self.flush_callbacks = self.flush_callbacks + [callback]
def unregister_flush_callback(self, callback):
if callback in self.flush_callbacks:
fcbs = list(self.flush_callbacks)
fcbs.remove(callback)
self.flush_callbacks = fcbs
def flush_motion_queues(self, must_flush_time, max_step_gen_time,
trapq_free_time):
# Kinematic step generation scan window time tracking
def get_kin_flush_delay(self):
return self.kin_flush_delay
def check_step_generation_scan_windows(self):
ffi_main, ffi_lib = chelper.get_ffi()
kin_flush_delay = SDS_CHECK_TIME
for se in self.syncemitters:
sk = ffi_lib.syncemitter_get_stepper_kinematics(se)
if sk == ffi_main.NULL:
continue
trapq = ffi_lib.itersolve_get_trapq(sk)
if trapq == ffi_main.NULL:
continue
pre_active = ffi_lib.itersolve_get_gen_steps_pre_active(sk)
post_active = ffi_lib.itersolve_get_gen_steps_post_active(sk)
kin_flush_delay = max(kin_flush_delay, pre_active, post_active)
self.kin_flush_delay = kin_flush_delay
# Flush tracking
def _handle_shutdown(self):
self.can_pause = False
def _advance_flush_time(self, want_flush_time, want_step_gen_time=0.):
flush_time = max(want_flush_time, self.last_flush_time,
want_step_gen_time - STEPCOMPRESS_FLUSH_TIME)
step_gen_time = max(want_step_gen_time, self.last_step_gen_time,
flush_time)
# Invoke flush callbacks (if any)
for cb in self.flush_callbacks:
cb(must_flush_time, max_step_gen_time)
# Generate stepper movement and transmit
for mcu, ss in self.steppersyncs:
clock = max(0, mcu.print_time_to_clock(must_flush_time))
# Generate steps
ret = self.steppersync_generate_steps(ss, max_step_gen_time, clock)
if ret:
raise mcu.error("Internal error in MCU '%s' stepcompress"
% (mcu.get_name(),))
# Flush steps from steppersync
ret = self.steppersync_flush(ss, clock)
if ret:
raise mcu.error("Internal error in MCU '%s' stepcompress"
% (mcu.get_name(),))
cb(flush_time, step_gen_time)
# Determine maximum history to keep
trapq_free_time = step_gen_time - self.kin_flush_delay
clear_history_time = self.clear_history_time
if self.is_debugoutput:
clear_history_time = trapq_free_time - MOVE_HISTORY_EXPIRE
# Move processed trapq moves to history list, and expire old history
if not self.can_pause:
clear_history_time = max(0., trapq_free_time - MOVE_HISTORY_EXPIRE)
# Generate stepper movement and transmit
ret = self.steppersyncmgr_gen_steps(self.steppersyncmgr, flush_time,
step_gen_time, clear_history_time)
if ret:
raise self.mcu.error("Internal error in stepcompress")
self.last_flush_time = flush_time
self.last_step_gen_time = step_gen_time
# Move processed trapq entries to history list, and expire old history
for trapq in self.trapqs:
self.trapq_finalize_moves(trapq, trapq_free_time,
clear_history_time)
# Clean up old history entries in stepcompress objects
for mcu, ss in self.steppersyncs:
clock = max(0, mcu.print_time_to_clock(clear_history_time))
self.steppersync_history_expire(ss, clock)
def wipe_trapq(self, trapq):
# Expire any remaining movement in the trapq (force to history list)
NEVER = 9999999999999999.
self.trapq_finalize_moves(trapq, NEVER, 0.)
def lookup_trapq_append(self):
ffi_main, ffi_lib = chelper.get_ffi()
return ffi_lib.trapq_append
def stats(self, eventtime):
mcu = self.printer.lookup_object('mcu')
est_print_time = mcu.estimated_print_time(eventtime)
self.clear_history_time = est_print_time - MOVE_HISTORY_EXPIRE
return False, ""
def _await_flush_time(self, want_flush_time):
while 1:
if self.last_flush_time >= want_flush_time or not self.can_pause:
return
systime = self.reactor.monotonic()
est_print_time = self.mcu.estimated_print_time(systime)
wait = want_flush_time - BGFLUSH_HIGH_TIME - est_print_time
if wait <= 0.:
return
self.reactor.pause(systime + min(1., wait))
def flush_all_steps(self):
flush_time = self.need_step_gen_time
self._await_flush_time(flush_time)
self._advance_flush_time(flush_time)
def calc_step_gen_restart(self, est_print_time):
kin_time = max(est_print_time + MIN_KIN_TIME, self.last_step_gen_time)
return kin_time + self.kin_flush_delay
def _flush_handler(self, eventtime):
try:
est_print_time = self.mcu.estimated_print_time(eventtime)
aggr_sg_time = self.need_step_gen_time - 2.*self.kin_flush_delay
if self.last_step_gen_time < aggr_sg_time:
# Actively stepping - want more aggressive flushing
want_sg_time = est_print_time + BGFLUSH_SG_HIGH_TIME
batch_time = BGFLUSH_SG_HIGH_TIME - BGFLUSH_SG_LOW_TIME
next_batch_time = self.last_step_gen_time + batch_time
if next_batch_time > est_print_time:
# Improve run-to-run reproducibility by batching from last
if next_batch_time > want_sg_time + 0.005:
# Delay flushing until next wakeup
next_batch_time = self.last_step_gen_time
want_sg_time = next_batch_time
want_sg_time = min(want_sg_time, aggr_sg_time)
# Flush motion queues (if needed)
if want_sg_time > self.last_step_gen_time:
self._advance_flush_time(0., want_sg_time)
else:
# Not stepping (or only step remnants) - use relaxed flushing
want_flush_time = est_print_time + BGFLUSH_HIGH_TIME
max_flush_time = self.need_flush_time + BGFLUSH_EXTRA_TIME
want_flush_time = min(want_flush_time, max_flush_time)
# Flush motion queues (if needed)
if want_flush_time > self.last_flush_time:
self._advance_flush_time(want_flush_time)
# Reschedule timer
aggr_sg_time = self.need_step_gen_time - 2.*self.kin_flush_delay
if self.last_step_gen_time < aggr_sg_time:
waketime = self.last_step_gen_time - BGFLUSH_SG_LOW_TIME
else:
self.do_kick_flush_timer = True
max_flush_time = self.need_flush_time + BGFLUSH_EXTRA_TIME
if self.last_flush_time >= max_flush_time:
return self.reactor.NEVER
waketime = self.last_flush_time - BGFLUSH_LOW_TIME
return eventtime + waketime - est_print_time
except:
logging.exception("Exception in flush_handler")
self.printer.invoke_shutdown("Exception in flush_handler")
return self.reactor.NEVER
def _flush_handler_debug(self, eventtime):
# Use custom flushing code when in batch output mode
try:
faux_time = self.need_flush_time - 1.5
batch_time = BGFLUSH_SG_HIGH_TIME - BGFLUSH_SG_LOW_TIME
flush_count = 0
while self.last_step_gen_time < faux_time:
target = self.last_step_gen_time + batch_time
if flush_count > 100. and faux_time > target:
target += int((faux_time-target) / batch_time) * batch_time
self._advance_flush_time(0., target)
flush_count += 1
if flush_count:
return self.reactor.NOW
self._advance_flush_time(self.need_flush_time + BGFLUSH_EXTRA_TIME)
self.do_kick_flush_timer = True
return self.reactor.NEVER
except:
logging.exception("Exception in flush_handler_debug")
self.printer.invoke_shutdown("Exception in flush_handler_debug")
return self.reactor.NEVER
def note_mcu_movequeue_activity(self, mq_time, is_step_gen=True):
if is_step_gen:
mq_time += self.kin_flush_delay
self.need_step_gen_time = max(self.need_step_gen_time, mq_time)
self.need_flush_time = max(self.need_flush_time, mq_time)
if self.do_kick_flush_timer:
self.do_kick_flush_timer = False
self.reactor.update_timer(self.flush_timer, self.reactor.NOW)
def drip_update_time(self, start_time, end_time, drip_completion):
self._await_flush_time(start_time)
# Disable background flushing from timer
self.reactor.update_timer(self.flush_timer, self.reactor.NEVER)
self.do_kick_flush_timer = False
self._advance_flush_time(start_time - SDS_CHECK_TIME, start_time)
# Flush in segments until drip_completion signal
flush_time = start_time
while flush_time < end_time:
if drip_completion.test():
break
curtime = self.reactor.monotonic()
est_print_time = self.mcu.estimated_print_time(curtime)
wait_time = flush_time - est_print_time - DRIP_TIME
if wait_time > 0. and self.can_pause:
# Pause before sending more steps
drip_completion.wait(curtime + wait_time)
continue
flush_time = min(flush_time + DRIP_SEGMENT_TIME, end_time)
self.note_mcu_movequeue_activity(flush_time)
self._advance_flush_time(flush_time - SDS_CHECK_TIME, flush_time)
# Restore background flushing
self.reactor.update_timer(self.flush_timer, self.reactor.NOW)
self._advance_flush_time(flush_time + self.kin_flush_delay)
def load_config(config):
return PrinterMotionQueuing(config)

View file

@ -20,8 +20,8 @@ class GCodeRequestQueue:
self.rqueue = []
self.next_min_flush_time = 0.
self.toolhead = None
motion_queuing = printer.load_object(config, 'motion_queuing')
motion_queuing.register_flush_callback(self._flush_notification)
self.motion_queuing = printer.load_object(config, 'motion_queuing')
self.motion_queuing.register_flush_callback(self._flush_notification)
printer.register_event_handler("klippy:connect", self._handle_connect)
def _handle_connect(self):
self.toolhead = self.printer.lookup_object('toolhead')
@ -51,11 +51,12 @@ class GCodeRequestQueue:
del rqueue[:pos+1]
self.next_min_flush_time = next_time + max(min_wait, min_sched_time)
# Ensure following queue items are flushed
self.toolhead.note_mcu_movequeue_activity(self.next_min_flush_time,
is_step_gen=False)
self.motion_queuing.note_mcu_movequeue_activity(
self.next_min_flush_time, is_step_gen=False)
def _queue_request(self, print_time, value):
self.rqueue.append((print_time, value))
self.toolhead.note_mcu_movequeue_activity(print_time, is_step_gen=False)
self.motion_queuing.note_mcu_movequeue_activity(
print_time, is_step_gen=False)
def queue_gcode_request(self, value):
self.toolhead.register_lookahead_callback(
(lambda pt: self._queue_request(pt, value)))

View file

@ -27,8 +27,8 @@ class PCA9533:
minclock = 0
if print_time is not None:
minclock = self.i2c.get_mcu().print_time_to_clock(print_time)
self.i2c.i2c_write([PCA9533_PLS0, ls0], minclock=minclock,
reqclock=BACKGROUND_PRIORITY_CLOCK)
self.i2c.i2c_write_noack([PCA9533_PLS0, ls0], minclock=minclock,
reqclock=BACKGROUND_PRIORITY_CLOCK)
def get_status(self, eventtime):
return self.led_helper.get_status(eventtime)

View file

@ -37,8 +37,8 @@ class PCA9632:
if self.prev_regs.get(reg) == val:
return
self.prev_regs[reg] = val
self.i2c.i2c_write([reg, val], minclock=minclock,
reqclock=BACKGROUND_PRIORITY_CLOCK)
self.i2c.i2c_write_noack([reg, val], minclock=minclock,
reqclock=BACKGROUND_PRIORITY_CLOCK)
def handle_connect(self):
#Configure MODE1
self.reg_write(PCA9632_MODE1, 0x00)

View file

@ -14,12 +14,14 @@ class MCU_queued_pwm:
self._hardware_pwm = False
self._cycle_time = 0.100
self._max_duration = 2.
self._oid = oid = mcu.create_oid()
self._oid = mcu.create_oid()
printer = mcu.get_printer()
motion_queuing = printer.load_object(config, 'motion_queuing')
self._stepqueue = motion_queuing.allocate_stepcompress(mcu, oid)
sname = config.get_name().split()[-1]
self._motion_queuing = printer.load_object(config, 'motion_queuing')
self._syncemitter = self._motion_queuing.allocate_syncemitter(
mcu, sname, alloc_stepcompress=False)
ffi_main, ffi_lib = chelper.get_ffi()
self._stepcompress_queue_mq_msg = ffi_lib.stepcompress_queue_mq_msg
self._syncemitter_queue_msg = ffi_lib.syncemitter_queue_msg
mcu.register_config_callback(self._build_config)
self._pin = pin_params['pin']
self._invert = pin_params['invert']
@ -62,8 +64,8 @@ class MCU_queued_pwm:
if self._duration_ticks >= 1<<31:
raise config_error("PWM pin max duration too large")
if self._duration_ticks:
motion_queuing = printer.lookup_object('motion_queuing')
motion_queuing.register_flush_callback(self._flush_notification)
self._motion_queuing.register_flush_callback(
self._flush_notification)
if self._hardware_pwm:
self._pwm_max = self._mcu.get_constant_float("PWM_MAX")
self._default_value = self._shutdown_value * self._pwm_max
@ -106,18 +108,15 @@ class MCU_queued_pwm:
self._last_clock = clock = max(self._last_clock, clock)
self._last_value = val
data = (self._set_cmd_tag, self._oid, clock & 0xffffffff, val)
ret = self._stepcompress_queue_mq_msg(self._stepqueue, clock,
data, len(data))
if ret:
raise error("Internal error in stepcompress")
self._syncemitter_queue_msg(self._syncemitter, clock, data, len(data))
# Notify toolhead so that it will flush this update
wakeclock = clock
if self._last_value != self._default_value:
# Continue flushing to resend time
wakeclock += self._duration_ticks
wake_print_time = self._mcu.clock_to_print_time(wakeclock)
self._toolhead.note_mcu_movequeue_activity(wake_print_time,
is_step_gen=False)
self._motion_queuing.note_mcu_movequeue_activity(wake_print_time,
is_step_gen=False)
def set_pwm(self, print_time, value):
clock = self._mcu.print_time_to_clock(print_time)
if self._invert:

View file

@ -1,19 +1,23 @@
# A utility class to test resonances of the printer
#
# Copyright (C) 2020-2024 Dmitry Butyugin <dmbutyugin@google.com>
# Copyright (C) 2020-2025 Dmitry Butyugin <dmbutyugin@google.com>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import logging, math, os, time
import itertools, logging, math, os, time
from . import shaper_calibrate
class TestAxis:
def __init__(self, axis=None, vib_dir=None):
if axis is None:
self._name = "axis=%.3f,%.3f" % (vib_dir[0], vib_dir[1])
self._name = "axis=%.3f,%.3f,%.3f" % (
vib_dir[0], vib_dir[1],
(vib_dir[2] if len(vib_dir) == 3 else 0.))
else:
self._name = axis
if vib_dir is None:
self._vib_dir = (1., 0.) if axis == 'x' else (0., 1.)
self._vib_dir = [(1., 0., 0.),
(0., 1., 0.),
(0., 0., 1.)][ord(axis)-ord('x')]
else:
s = math.sqrt(sum([d*d for d in vib_dir]))
self._vib_dir = [d / s for d in vib_dir]
@ -22,43 +26,54 @@ class TestAxis:
return True
if self._vib_dir[1] and 'y' in chip_axis:
return True
if self._vib_dir[2] and 'z' in chip_axis:
return True
return False
def get_dir(self):
return self._vib_dir
def get_name(self):
return self._name
def get_point(self, l):
return (self._vib_dir[0] * l, self._vib_dir[1] * l)
return tuple(d * l for d in self._vib_dir)
def _parse_axis(gcmd, raw_axis):
if raw_axis is None:
return None
raw_axis = raw_axis.lower()
if raw_axis in ['x', 'y']:
if raw_axis in ['x', 'y', 'z']:
return TestAxis(axis=raw_axis)
dirs = raw_axis.split(',')
if len(dirs) != 2:
if len(dirs) not in (2, 3):
raise gcmd.error("Invalid format of axis '%s'" % (raw_axis,))
try:
dir_x = float(dirs[0].strip())
dir_y = float(dirs[1].strip())
dir_z = float(dirs[2].strip()) if len(dirs) == 3 else 0.
except:
raise gcmd.error(
"Unable to parse axis direction '%s'" % (raw_axis,))
return TestAxis(vib_dir=(dir_x, dir_y))
return TestAxis(vib_dir=(dir_x, dir_y, dir_z))
class VibrationPulseTestGenerator:
def __init__(self, config):
self.min_freq = config.getfloat('min_freq', 5., minval=1.)
self.max_freq = config.getfloat('max_freq', 135.,
minval=self.min_freq, maxval=300.)
self.max_freq_z = config.getfloat('max_freq_z', 100.,
minval=self.min_freq, maxval=300.)
self.accel_per_hz = config.getfloat('accel_per_hz', 60., above=0.)
self.accel_per_hz_z = config.getfloat('accel_per_hz_z', 15., above=0.)
self.hz_per_sec = config.getfloat('hz_per_sec', 1.,
minval=0.1, maxval=2.)
def prepare_test(self, gcmd):
def prepare_test(self, gcmd, is_z):
self.freq_start = gcmd.get_float("FREQ_START", self.min_freq, minval=1.)
self.freq_end = gcmd.get_float("FREQ_END", self.max_freq,
self.freq_end = gcmd.get_float("FREQ_END", (self.max_freq_z if is_z
else self.max_freq),
minval=self.freq_start, maxval=300.)
self.test_accel_per_hz = gcmd.get_float("ACCEL_PER_HZ",
self.accel_per_hz, above=0.)
(self.accel_per_hz_z if is_z
else 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):
@ -83,12 +98,15 @@ class SweepingVibrationsTestGenerator:
def __init__(self, config):
self.vibration_generator = VibrationPulseTestGenerator(config)
self.sweeping_accel = config.getfloat('sweeping_accel', 400., above=0.)
self.sweeping_accel_z = config.getfloat('sweeping_accel_z', 50.,
above=0.)
self.sweeping_period = config.getfloat('sweeping_period', 1.2,
minval=0.)
def prepare_test(self, gcmd):
self.vibration_generator.prepare_test(gcmd)
def prepare_test(self, gcmd, is_z):
self.vibration_generator.prepare_test(gcmd, is_z)
self.test_sweeping_accel = gcmd.get_float(
"SWEEPING_ACCEL", self.sweeping_accel, above=0.)
"SWEEPING_ACCEL", (self.sweeping_accel_z if is_z
else self.sweeping_accel), above=0.)
self.test_sweeping_period = gcmd.get_float(
"SWEEPING_PERIOD", self.sweeping_period, minval=0.)
def gen_test(self):
@ -120,73 +138,121 @@ class SweepingVibrationsTestGenerator:
def get_max_freq(self):
return self.vibration_generator.get_max_freq()
# Helper to lookup Z kinematics limits
def lookup_z_limits(configfile):
sconfig = configfile.get_status(None)['settings']
printer_config = sconfig.get('printer')
max_z_velocity = printer_config.get('max_z_velocity')
if max_z_velocity is None:
max_z_velocity = printer_config.get('max_velocity')
max_z_accel = printer_config.get('max_z_accel')
if max_z_accel is None:
max_z_accel = printer_config.get('max_accel')
return max_z_velocity, max_z_accel
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()
configfile = self.printer.lookup_object('configfile')
toolhead = self.printer.lookup_object('toolhead')
tpos = toolhead.get_position()
X, Y = tpos[:2]
X, Y, Z = tpos[:3]
# Override maximum acceleration and acceleration to
# deceleration based on the maximum test frequency
systime = reactor.monotonic()
toolhead_info = toolhead.get_status(systime)
old_max_velocity = toolhead_info['max_velocity']
old_max_accel = toolhead_info['max_accel']
old_minimum_cruise_ratio = toolhead_info['minimum_cruise_ratio']
max_accel = max([abs(a) for _, a, _ in test_seq])
max_velocity = 0.
last_v = last_t = 0.
for next_t, accel, freq in test_seq:
v = last_v + accel * (next_t - last_t)
max_velocity = max(max_velocity, abs(v))
last_t, last_v = next_t, v
if axis.get_dir()[2]:
max_z_velocity, max_z_accel = lookup_z_limits(configfile)
error_msg = ""
if max_velocity > max_z_velocity:
error_msg = (
"Insufficient maximum Z velocity for these"
" test parameters, increase at least to %.f mm/s"
" for the resonance test." % (max_velocity+0.5))
if max_accel > max_z_accel:
if error_msg:
error_msg += "\n"
error_msg += (
"Insufficient maximum Z acceleration for these"
" test parameters, increase at least to %.f mm/s^2"
" for the resonance test." % (max_accel+0.5))
if error_msg:
raise gcmd.error(error_msg)
self.gcode.run_script_from_command(
"SET_VELOCITY_LIMIT ACCEL=%.3f MINIMUM_CRUISE_RATIO=0"
% (max_accel,))
"SET_VELOCITY_LIMIT VELOCITY=%.f ACCEL=%.f MINIMUM_CRUISE_RATIO=0"
% (max_velocity+0.5, max_accel+0.5,))
input_shaper = self.printer.lookup_object('input_shaper', None)
if input_shaper is not None and not gcmd.get_int('INPUT_SHAPING', 0):
input_shaper.disable_shaping()
gcmd.respond_info("Disabled [input_shaper] for resonance testing")
else:
input_shaper = None
last_v = last_t = last_accel = last_freq = 0.
last_v = last_t = last_freq = 0.
for next_t, accel, freq in test_seq:
t_seg = next_t - last_t
toolhead.set_max_velocities(None, abs(accel), None, None)
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)
if abs(accel) < 0.000001:
v, abs_v = last_v, abs_last_v
if abs_v < 0.000001:
toolhead.dwell(t_seg)
last_t, last_freq = next_t, freq
continue
half_inv_accel = 0.
d = v * t_seg
else:
toolhead.set_max_velocities(None, abs(accel), None, None)
v = last_v + accel * t_seg
abs_v = abs(v)
if abs_v < 0.000001:
v = abs_v = 0.
half_inv_accel = .5 / accel
d = (v * v - last_v2) * half_inv_accel
dX, dY, dZ = axis.get_point(d)
nX = X + dX
nY = Y + dY
nZ = Z + dZ
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] + tpos[2:], abs_last_v)
toolhead.move([nX, nY] + tpos[2:], abs_v)
decel_X, decel_Y, decel_Z = axis.get_point(d_decel)
toolhead.move([X + decel_X, Y + decel_Y, Z + decel_Z]
+ tpos[3:], abs_last_v)
toolhead.move([nX, nY, nZ] + tpos[3:], abs_v)
else:
toolhead.move([nX, nY] + tpos[2:], max(abs_v, abs_last_v))
toolhead.move([nX, nY, nZ] + tpos[3:], 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
X, Y, Z = nX, nY, nZ
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)
decel_X, decel_Y, decel_Z = axis.get_point(d_decel)
toolhead.set_max_velocities(None, old_max_accel, None, None)
toolhead.move([X + decel_X, Y + decel_Y] + tpos[2:], abs(last_v))
toolhead.move([X + decel_X, Y + decel_Y, Z + decel_Z] + tpos[3:],
abs(last_v))
# Restore the original acceleration values
self.gcode.run_script_from_command(
"SET_VELOCITY_LIMIT ACCEL=%.3f MINIMUM_CRUISE_RATIO=%.3f"
% (old_max_accel, old_minimum_cruise_ratio))
("SET_VELOCITY_LIMIT VELOCITY=%.3f ACCEL=%.3f"
+ " MINIMUM_CRUISE_RATIO=%.3f") % (old_max_velocity, old_max_accel,
old_minimum_cruise_ratio))
# Restore input shaper if it was disabled for resonance testing
if input_shaper is not None:
input_shaper.enable_shaping()
@ -199,13 +265,21 @@ class ResonanceTester:
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())]
accel_chip_names = [
('xy', config.get('accel_chip').strip()),
('z', config.get('accel_chip_z', '').strip())]
else:
self.accel_chip_names = [
('x', config.get('accel_chip_x').strip()),
('y', config.get('accel_chip_y').strip())]
if self.accel_chip_names[0][1] == self.accel_chip_names[1][1]:
self.accel_chip_names = [('xy', self.accel_chip_names[0][1])]
accel_chip_names = [
('x', config.get('accel_chip_x').strip()),
('y', config.get('accel_chip_y').strip()),
('z', config.get('accel_chip_z', '').strip())]
get_chip_name = lambda t: t[1]
# Group chips by their axes
self.accel_chip_names = [
(''.join(sorted(axis for axis, _ in vals)), chip_name)
for chip_name, vals in itertools.groupby(
sorted(accel_chip_names, key=get_chip_name),
key=get_chip_name)]
self.max_smoothing = config.getfloat('max_smoothing', None, minval=0.05)
self.probe_points = config.getlists('probe_points', seps=(',', '\n'),
parser=float, count=3)
@ -223,16 +297,25 @@ class ResonanceTester:
self.printer.register_event_handler("klippy:connect", self.connect)
def connect(self):
self.accel_chips = [
(chip_axis, self.printer.lookup_object(chip_name))
for chip_axis, chip_name in self.accel_chip_names]
self.accel_chips = []
for chip_axis, chip_name in self.accel_chip_names:
if not chip_name:
continue
chip = self.printer.lookup_object(chip_name)
if not hasattr(chip, 'start_internal_client'):
raise self.printer.config_error(
"'%s' is not an accelerometer" % chip_name)
self.accel_chips.append((chip_axis, chip))
def _run_test(self, gcmd, axes, helper, raw_name_suffix=None,
accel_chips=None, test_point=None):
toolhead = self.printer.lookup_object('toolhead')
calibration_data = {axis: None for axis in axes}
self.generator.prepare_test(gcmd)
has_z = [axis.get_dir()[2] for axis in axes]
if all(has_z) != any(has_z):
raise gcmd.error("Cannot test Z axis together with other axes")
self.generator.prepare_test(gcmd, is_z=all(has_z))
test_points = [test_point] if test_point else self.probe_points
@ -257,6 +340,10 @@ class ResonanceTester:
for chip in accel_chips:
aclient = chip.start_internal_client()
raw_values.append((axis, aclient, chip.name))
if not raw_values:
raise gcmd.error(
"No accelerometers specified that can measure"
" resonances over axis '%s'" % axis.get_name())
# Generate moves
test_seq = self.generator.gen_test()
@ -267,7 +354,8 @@ class ResonanceTester:
raw_name = self.get_filename(
'raw_data', raw_name_suffix, axis,
point if len(test_points) > 1 else None,
chip_name if accel_chips is not None else None,)
chip_name if (accel_chips is not None
or len(raw_values) > 1) else None)
aclient.write_to_file(raw_name)
gcmd.respond_info(
"Writing raw accelerometer data to "
@ -288,7 +376,13 @@ class ResonanceTester:
def _parse_chips(self, accel_chips):
parsed_chips = []
for chip_name in accel_chips.split(','):
chip = self.printer.lookup_object(chip_name.strip())
chip = self.printer.lookup_object(chip_name.strip(), None)
if chip is None:
raise self.printer.command_error("Name '%s' is not valid for"
" CHIPS parameter" % chip_name)
if not hasattr(chip, 'start_internal_client'):
raise self.printer.command_error(
"'%s' is not an accelerometer" % chip_name)
parsed_chips.append(chip)
return parsed_chips
def _get_max_calibration_freq(self):
@ -349,7 +443,7 @@ class ResonanceTester:
axis = gcmd.get("AXIS", None)
if not axis:
calibrate_axes = [TestAxis('x'), TestAxis('y')]
elif axis.lower() not in 'xy':
elif axis.lower() not in 'xyz':
raise gcmd.error("Unsupported axis '%s'" % (axis,))
else:
calibrate_axes = [TestAxis(axis.lower())]

View file

@ -9,7 +9,8 @@ SHAPER_VIBRATION_REDUCTION=20.
DEFAULT_DAMPING_RATIO = 0.1
InputShaperCfg = collections.namedtuple(
'InputShaperCfg', ('name', 'init_func', 'min_freq'))
'InputShaperCfg',
('name', 'init_func', 'min_freq', 'max_damping_ratio'))
def get_none_shaper():
return ([], [])
@ -46,57 +47,75 @@ def get_mzv_shaper(shaper_freq, damping_ratio):
def get_ei_shaper(shaper_freq, damping_ratio):
v_tol = 1. / SHAPER_VIBRATION_REDUCTION # vibration tolerance
df = math.sqrt(1. - damping_ratio**2)
K = math.exp(-damping_ratio * math.pi / df)
t_d = 1. / (shaper_freq * df)
dr = damping_ratio
a1 = .25 * (1. + v_tol)
a2 = .5 * (1. - v_tol) * K
a3 = a1 * K * K
a1 = (0.24968 + 0.24961 * v_tol) + (( 0.80008 + 1.23328 * v_tol) +
( 0.49599 + 3.17316 * v_tol) * dr) * dr
a3 = (0.25149 + 0.21474 * v_tol) + ((-0.83249 + 1.41498 * v_tol) +
( 0.85181 - 4.90094 * v_tol) * dr) * dr
a2 = 1. - a1 - a3
t2 = 0.4999 + ((( 0.46159 + 8.57843 * v_tol) * v_tol) +
(((4.26169 - 108.644 * v_tol) * v_tol) +
((1.75601 + 336.989 * v_tol) * v_tol) * dr) * dr) * dr
A = [a1, a2, a3]
T = [0., .5*t_d, t_d]
T = [0., t2 * t_d, t_d]
return (A, T)
def _get_shaper_from_expansion_coeffs(shaper_freq, damping_ratio, t, a):
tau = 1. / shaper_freq
T = []
A = []
n = len(a)
k = len(a[0])
for i in range(n):
u = t[i][k-1]
v = a[i][k-1]
for j in range(k-1):
u = u * damping_ratio + t[i][k-j-2]
v = v * damping_ratio + a[i][k-j-2]
T.append(u * tau)
A.append(v)
return (A, T)
def get_2hump_ei_shaper(shaper_freq, damping_ratio):
v_tol = 1. / SHAPER_VIBRATION_REDUCTION # vibration tolerance
df = math.sqrt(1. - damping_ratio**2)
K = math.exp(-damping_ratio * math.pi / df)
t_d = 1. / (shaper_freq * df)
V2 = v_tol**2
X = pow(V2 * (math.sqrt(1. - V2) + 1.), 1./3.)
a1 = (3.*X*X + 2.*X + 3.*V2) / (16.*X)
a2 = (.5 - a1) * K
a3 = a2 * K
a4 = a1 * K * K * K
A = [a1, a2, a3, a4]
T = [0., .5*t_d, t_d, 1.5*t_d]
return (A, T)
t = [[0., 0., 0., 0.],
[0.49890, 0.16270, -0.54262, 6.16180],
[0.99748, 0.18382, -1.58270, 8.17120],
[1.49920, -0.09297, -0.28338, 1.85710]]
a = [[0.16054, 0.76699, 2.26560, -1.22750],
[0.33911, 0.45081, -2.58080, 1.73650],
[0.34089, -0.61533, -0.68765, 0.42261],
[0.15997, -0.60246, 1.00280, -0.93145]]
return _get_shaper_from_expansion_coeffs(shaper_freq, damping_ratio, t, a)
def get_3hump_ei_shaper(shaper_freq, damping_ratio):
v_tol = 1. / SHAPER_VIBRATION_REDUCTION # vibration tolerance
df = math.sqrt(1. - damping_ratio**2)
K = math.exp(-damping_ratio * math.pi / df)
t_d = 1. / (shaper_freq * df)
K2 = K*K
a1 = 0.0625 * (1. + 3. * v_tol + 2. * math.sqrt(2. * (v_tol + 1.) * v_tol))
a2 = 0.25 * (1. - v_tol) * K
a3 = (0.5 * (1. + v_tol) - 2. * a1) * K2
a4 = a2 * K2
a5 = a1 * K2 * K2
A = [a1, a2, a3, a4, a5]
T = [0., .5*t_d, t_d, 1.5*t_d, 2.*t_d]
return (A, T)
t = [[0., 0., 0., 0.],
[0.49974, 0.23834, 0.44559, 12.4720],
[0.99849, 0.29808, -2.36460, 23.3990],
[1.49870, 0.10306, -2.01390, 17.0320],
[1.99960, -0.28231, 0.61536, 5.40450]]
a = [[0.11275, 0.76632, 3.29160 -1.44380],
[0.23698, 0.61164, -2.57850, 4.85220],
[0.30008, -0.19062, -2.14560, 0.13744],
[0.23775, -0.73297, 0.46885, -2.08650],
[0.11244, -0.45439, 0.96382, -1.46000]]
return _get_shaper_from_expansion_coeffs(shaper_freq, damping_ratio, t, a)
# min_freq for each shaper is chosen to have projected max_accel ~= 1500
INPUT_SHAPERS = [
InputShaperCfg('zv', get_zv_shaper, min_freq=21.),
InputShaperCfg('mzv', get_mzv_shaper, min_freq=23.),
InputShaperCfg('zvd', get_zvd_shaper, min_freq=29.),
InputShaperCfg('ei', get_ei_shaper, min_freq=29.),
InputShaperCfg('2hump_ei', get_2hump_ei_shaper, min_freq=39.),
InputShaperCfg('3hump_ei', get_3hump_ei_shaper, min_freq=48.),
InputShaperCfg(name='zv', init_func=get_zv_shaper,
min_freq=21., max_damping_ratio=0.99),
InputShaperCfg(name='mzv', init_func=get_mzv_shaper,
min_freq=23., max_damping_ratio=0.99),
InputShaperCfg(name='zvd', init_func=get_zvd_shaper,
min_freq=29., max_damping_ratio=0.99),
InputShaperCfg(name='ei', init_func=get_ei_shaper,
min_freq=29., max_damping_ratio=0.4),
InputShaperCfg(name='2hump_ei', init_func=get_2hump_ei_shaper,
min_freq=39., max_damping_ratio=0.3),
InputShaperCfg(name='3hump_ei', init_func=get_3hump_ei_shaper,
min_freq=48., max_damping_ratio=0.2),
]

View file

@ -1,6 +1,6 @@
# Support for enable pins on stepper motor drivers
#
# Copyright (C) 2019-2021 Kevin O'Connor <kevin@koconnor.net>
# Copyright (C) 2019-2025 Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import logging
@ -88,30 +88,38 @@ class PrinterStepperEnable:
name = mcu_stepper.get_name()
enable = setup_enable_pin(self.printer, config.get('enable_pin', None))
self.enable_lines[name] = EnableTracking(mcu_stepper, enable)
def set_motors_enable(self, stepper_names, enable):
toolhead = self.printer.lookup_object('toolhead')
# Flush steps to ensure all auto enable callbacks invoked
toolhead.flush_step_generation()
print_time = None
did_change = False
for stepper_name in stepper_names:
el = self.enable_lines[stepper_name]
if el.is_motor_enabled() == enable:
continue
if print_time is None:
# Dwell for sufficient delay from any previous auto enable
if not enable:
toolhead.dwell(DISABLE_STALL_TIME)
print_time = toolhead.get_last_move_time()
if enable:
el.motor_enable(print_time)
else:
el.motor_disable(print_time)
did_change = True
# Dwell to ensure sufficient delay prior to a future auto enable
if did_change and not enable:
toolhead.dwell(DISABLE_STALL_TIME)
return did_change
def motor_off(self):
self.set_motors_enable(self.get_steppers(), False)
toolhead = self.printer.lookup_object('toolhead')
toolhead.dwell(DISABLE_STALL_TIME)
print_time = toolhead.get_last_move_time()
for el in self.enable_lines.values():
el.motor_disable(print_time)
toolhead.get_kinematics().clear_homing_state("xyz")
self.printer.send_event("stepper_enable:motor_off", print_time)
toolhead.dwell(DISABLE_STALL_TIME)
def motor_debug_enable(self, stepper, enable):
toolhead = self.printer.lookup_object('toolhead')
toolhead.dwell(DISABLE_STALL_TIME)
print_time = toolhead.get_last_move_time()
el = self.enable_lines[stepper]
if enable:
el.motor_enable(print_time)
logging.info("%s has been manually enabled", stepper)
else:
el.motor_disable(print_time)
logging.info("%s has been manually disabled", stepper)
toolhead.dwell(DISABLE_STALL_TIME)
self.printer.send_event("stepper_enable:motor_off")
def get_status(self, eventtime):
steppers = { name: et.is_motor_enabled()
for (name, et) in self.enable_lines.items() }
for (name, et) in self.enable_lines.items() }
return {'steppers': steppers}
def _handle_request_restart(self, print_time):
self.motor_off()
@ -126,7 +134,11 @@ class PrinterStepperEnable:
% (stepper_name,))
return
stepper_enable = gcmd.get_int('ENABLE', 1)
self.motor_debug_enable(stepper_name, stepper_enable)
self.set_motors_enable([stepper_name], stepper_enable)
if stepper_enable:
logging.info("%s has been manually enabled", stepper_name)
else:
logging.info("%s has been manually disabled", stepper_name)
def lookup_enable(self, name):
if name not in self.enable_lines:
raise self.printer.config_error("Unknown stepper '%s'" % (name,))

View file

@ -92,7 +92,8 @@ class SX1509(object):
# Byte
data += [self.reg_i_on_dict[reg] & 0xFF]
clock = self._mcu.print_time_to_clock(print_time)
self._i2c.i2c_write(data, minclock=self._last_clock, reqclock=clock)
self._i2c.i2c_write_noack(data, minclock=self._last_clock,
reqclock=clock)
self._last_clock = clock
class SX1509_digital_out(object):

View file

@ -132,7 +132,7 @@ class TemperatureProbe:
self.start_pos = []
# Register GCode Commands
pname = self.name.split(maxsplit=1)[-1]
pname = self.name.split(None, 1)[-1]
self.gcode.register_mux_command(
"TEMPERATURE_PROBE_CALIBRATE", "PROBE", pname,
self.cmd_TEMPERATURE_PROBE_CALIBRATE,
@ -357,8 +357,8 @@ class TemperatureProbe:
self._check_homed()
probe = self._get_probe()
probe_name = probe.get_status(None)["name"]
short_name = probe_name.split(maxsplit=1)[-1]
if short_name != self.name.split(maxsplit=1)[-1]:
short_name = probe_name.split(None, 1)[-1]
if short_name != self.name.split(None, 1)[-1]:
raise self.gcode.error(
"[%s] not linked to registered probe [%s]."
% (self.name, probe_name)
@ -588,7 +588,7 @@ class EddyDriftCompensation:
temps[idx] = cur_temp
probe_samples[idx].append(sample)
return True
sect_name = "probe_eddy_current " + self.name.split(maxsplit=1)[-1]
sect_name = "probe_eddy_current " + self.name.split(None, 1)[-1]
self.printer.lookup_object(sect_name).add_client(_on_bulk_data_recd)
for i in range(DRIFT_SAMPLE_COUNT):
if i == 0:

View file

@ -259,7 +259,7 @@ class VirtualSD:
continue
# Pause if any other request is pending in the gcode class
if gcode_mutex.test():
self.reactor.pause(self.reactor.monotonic() + 0.100)
self.reactor.pause(self.reactor.monotonic() + 0.050)
continue
# Dispatch command
self.cmd_from_sd = True

View file

@ -79,7 +79,7 @@ class ZAdjustStatus:
self.applied = False
def get_status(self, eventtime):
return {'applied': self.applied}
def _motor_off(self, print_time):
def _motor_off(self):
self.reset()
class RetryHelper:

View file

@ -430,18 +430,17 @@ class GCodeIO:
self.gcode.request_restart('exit')
pending_commands.append("")
# Handle case where multiple commands pending
if self.is_processing_data or len(pending_commands) > 1:
if len(pending_commands) < 20:
# Check for M112 out-of-order
for line in lines:
if self.m112_r.match(line) is not None:
self.gcode.cmd_M112(None)
if self.is_processing_data:
if len(pending_commands) >= 20:
# Stop reading input
self.reactor.unregister_fd(self.fd_handle)
self.fd_handle = None
return
if len(pending_commands) < 20:
# Check for M112 out-of-order
for line in lines:
if self.m112_r.match(line) is not None:
self.gcode.cmd_M112(None)
if self.is_processing_data:
if len(pending_commands) >= 20:
# Stop reading input
self.reactor.unregister_fd(self.fd_handle)
self.fd_handle = None
return
# Process commands
self.is_processing_data = True
while pending_commands:

View file

@ -50,9 +50,11 @@ class ExtruderStepper:
def sync_to_extruder(self, extruder_name):
toolhead = self.printer.lookup_object('toolhead')
toolhead.flush_step_generation()
motion_queuing = self.printer.lookup_object('motion_queuing')
if not extruder_name:
self.stepper.set_trapq(None)
self.motion_queue = None
motion_queuing.check_step_generation_scan_windows()
return
extruder = self.printer.lookup_object(extruder_name, None)
if extruder is None or not isinstance(extruder, PrinterExtruder):
@ -61,6 +63,7 @@ class ExtruderStepper:
self.stepper.set_position([extruder.last_position, 0., 0.])
self.stepper.set_trapq(extruder.get_trapq())
self.motion_queue = extruder_name
motion_queuing.check_step_generation_scan_windows()
def _set_pressure_advance(self, pressure_advance, smooth_time):
old_smooth_time = self.pressure_advance_smooth_time
if not self.pressure_advance:
@ -69,14 +72,18 @@ class ExtruderStepper:
if not pressure_advance:
new_smooth_time = 0.
toolhead = self.printer.lookup_object("toolhead")
if new_smooth_time != old_smooth_time:
toolhead.note_step_generation_scan_time(
new_smooth_time * .5, old_delay=old_smooth_time * .5)
ffi_main, ffi_lib = chelper.get_ffi()
espa = ffi_lib.extruder_set_pressure_advance
toolhead.register_lookahead_callback(
lambda print_time: espa(self.sk_extruder, print_time,
pressure_advance, new_smooth_time))
if new_smooth_time != old_smooth_time:
# Need full kinematic flush to change the smooth time
toolhead.flush_step_generation()
espa(self.sk_extruder, 0., pressure_advance, new_smooth_time)
motion_queuing = self.printer.lookup_object('motion_queuing')
motion_queuing.check_step_generation_scan_windows()
else:
toolhead.register_lookahead_callback(
lambda print_time: espa(self.sk_extruder, print_time,
pressure_advance, new_smooth_time))
self.pressure_advance = pressure_advance
self.pressure_advance_smooth_time = smooth_time
cmd_SET_PRESSURE_ADVANCE_help = "Set pressure advance parameters"

View file

@ -327,12 +327,16 @@ def main():
extra_git_desc += "\nTracked URL: %s" % (git_info["url"])
start_args['software_version'] = git_vers
start_args['cpu_info'] = util.get_cpu_info()
start_args['device'] = util.get_device_info()
start_args['linux_version'] = util.get_linux_version()
if bglogger is not None:
versions = "\n".join([
"Args: %s" % (sys.argv,),
"Git version: %s%s" % (repr(start_args['software_version']),
extra_git_desc),
"CPU: %s" % (start_args['cpu_info'],),
"Device: %s" % (start_args['device']),
"Linux: %s" % (start_args['linux_version']),
"Python: %s" % (repr(sys.version),)])
logging.info(versions)
elif not options.debugoutput:

View file

@ -549,7 +549,7 @@ class MCU_adc:
######################################################################
# Main MCU class
# Main MCU class (and its helper classes)
######################################################################
# Minimum time host needs to get scheduled events queued into mcu
@ -557,103 +557,31 @@ MIN_SCHEDULE_TIME = 0.100
# Maximum time all MCUs can internally schedule into the future
MAX_NOMINAL_DURATION = 3.0
class MCU:
error = error
def __init__(self, config, clocksync):
# Support for restarting a micro-controller
class MCURestartHelper:
def __init__(self, config, conn_helper):
self._printer = printer = config.get_printer()
self._clocksync = clocksync
self._conn_helper = conn_helper
self._mcu = mcu = conn_helper.get_mcu()
self._serial = conn_helper.get_serial()
self._clocksync = conn_helper.get_clocksync()
self._reactor = printer.get_reactor()
self._name = config.get_name()
if self._name.startswith('mcu '):
self._name = self._name[4:]
# Serial port
name = self._name
self._serial = serialhdl.SerialReader(self._reactor, mcu_name = name)
self._baud = 0
self._canbus_iface = None
canbus_uuid = config.get('canbus_uuid', None)
if canbus_uuid is not None:
self._serialport = canbus_uuid
self._canbus_iface = config.get('canbus_interface', 'can0')
cbid = self._printer.load_object(config, 'canbus_ids')
cbid.add_uuid(config, canbus_uuid, self._canbus_iface)
self._printer.load_object(config, 'canbus_stats %s' % (self._name,))
else:
self._serialport = config.get('serial')
if not (self._serialport.startswith("/dev/rpmsg_")
or self._serialport.startswith("/tmp/klipper_host_")):
self._baud = config.getint('baud', 250000, minval=2400)
# Restarts
self._name = mcu.get_name()
# Restart tracking
restart_methods = [None, 'arduino', 'cheetah', 'command', 'rpi_usb']
self._restart_method = 'command'
if self._baud:
serialport, baud = conn_helper.get_serialport()
if baud:
self._restart_method = config.getchoice('restart_method',
restart_methods, None)
self._reset_cmd = self._config_reset_cmd = None
self._is_mcu_bridge = False
self._emergency_stop_cmd = None
self._is_shutdown = self._is_timeout = False
self._shutdown_clock = 0
self._shutdown_msg = ""
# Config building
printer.lookup_object('pins').register_chip(self._name, self)
self._oid_count = 0
self._config_callbacks = []
self._config_cmds = []
self._restart_cmds = []
self._init_cmds = []
self._mcu_freq = 0.
# Move command queuing
ffi_main, self._ffi_lib = chelper.get_ffi()
self._max_stepper_error = config.getfloat('max_stepper_error', 0.000025,
minval=0.)
self._reserved_move_slots = 0
self._steppersync = None
# Stats
self._get_status_info = {}
self._stats_sumsq_base = 0.
self._mcu_tick_avg = 0.
self._mcu_tick_stddev = 0.
self._mcu_tick_awake = 0.
# Register handlers
printer.load_object(config, "error_mcu")
printer.register_event_handler("klippy:firmware_restart",
self._firmware_restart)
printer.register_event_handler("klippy:disconnect", self._disconnect)
printer.register_event_handler("klippy:mcu_identify",
self._mcu_identify)
printer.register_event_handler("klippy:connect", self._connect)
printer.register_event_handler("klippy:shutdown", self._shutdown)
printer.register_event_handler("klippy:disconnect", self._disconnect)
printer.register_event_handler("klippy:ready", self._ready)
# Serial callbacks
def _handle_mcu_stats(self, params):
count = params['count']
tick_sum = params['sum']
c = 1.0 / (count * self._mcu_freq)
self._mcu_tick_avg = tick_sum * c
tick_sumsq = params['sumsq'] * self._stats_sumsq_base
diff = count*tick_sumsq - tick_sum**2
self._mcu_tick_stddev = c * math.sqrt(max(0., diff))
self._mcu_tick_awake = tick_sum / self._mcu_freq
def _handle_shutdown(self, params):
if self._is_shutdown:
return
self._is_shutdown = True
clock = params.get("clock")
if clock is not None:
self._shutdown_clock = self.clock32_to_clock64(clock)
self._shutdown_msg = msg = params['static_string_id']
event_type = params['#name']
self._printer.invoke_async_shutdown(
"MCU shutdown", {"reason": msg, "mcu": self._name,
"event_type": event_type})
logging.info("MCU '%s' %s: %s\n%s\n%s", self._name, event_type,
self._shutdown_msg, self._clocksync.dump_debug(),
self._serial.dump_debug())
def _handle_starting(self, params):
if not self._is_shutdown:
self._printer.invoke_async_shutdown("MCU '%s' spontaneous restart"
% (self._name,))
# Connection phase
def _check_restart(self, reason):
start_reason = self._printer.get_start_args().get("start_reason")
@ -664,160 +592,25 @@ class MCU:
self._printer.request_exit('firmware_restart')
self._reactor.pause(self._reactor.monotonic() + 2.000)
raise error("Attempt MCU '%s' restart failed" % (self._name,))
def _connect_file(self, pace=False):
# In a debugging mode. Open debug output file and read data dictionary
start_args = self._printer.get_start_args()
if self._name == 'mcu':
out_fname = start_args.get('debugoutput')
dict_fname = start_args.get('dictionary')
else:
out_fname = start_args.get('debugoutput') + "-" + self._name
dict_fname = start_args.get('dictionary_' + self._name)
outfile = open(out_fname, 'wb')
dfile = open(dict_fname, 'rb')
dict_data = dfile.read()
dfile.close()
self._serial.connect_file(outfile, dict_data)
self._clocksync.connect_file(self._serial, pace)
# Handle pacing
if not pace:
def dummy_estimated_print_time(eventtime):
return 0.
self.estimated_print_time = dummy_estimated_print_time
def _send_config(self, prev_crc):
# Build config commands
for cb in self._config_callbacks:
cb()
self._config_cmds.insert(0, "allocate_oids count=%d"
% (self._oid_count,))
# Resolve pin names
ppins = self._printer.lookup_object('pins')
pin_resolver = ppins.get_pin_resolver(self._name)
for cmdlist in (self._config_cmds, self._restart_cmds, self._init_cmds):
for i, cmd in enumerate(cmdlist):
cmdlist[i] = pin_resolver.update_command(cmd)
# Calculate config CRC
encoded_config = '\n'.join(self._config_cmds).encode()
config_crc = zlib.crc32(encoded_config) & 0xffffffff
self.add_config_cmd("finalize_config crc=%d" % (config_crc,))
if prev_crc is not None and config_crc != prev_crc:
self._check_restart("CRC mismatch")
raise error("MCU '%s' CRC does not match config" % (self._name,))
# Transmit config messages (if needed)
self.register_response(self._handle_starting, 'starting')
try:
if prev_crc is None:
logging.info("Sending MCU '%s' printer configuration...",
self._name)
for c in self._config_cmds:
self._serial.send(c)
else:
for c in self._restart_cmds:
self._serial.send(c)
# Transmit init messages
for c in self._init_cmds:
self._serial.send(c)
except msgproto.enumeration_error as e:
enum_name, enum_value = e.get_enum_params()
if enum_name == 'pin':
# Raise pin name errors as a config error (not a protocol error)
raise self._printer.config_error(
"Pin '%s' is not a valid pin name on mcu '%s'"
% (enum_value, self._name))
raise
def _send_get_config(self):
get_config_cmd = self.lookup_query_command(
"get_config",
"config is_config=%c crc=%u is_shutdown=%c move_count=%hu")
if self.is_fileoutput():
return { 'is_config': 0, 'move_count': 500, 'crc': 0 }
config_params = get_config_cmd.send()
if self._is_shutdown:
raise error("MCU '%s' error during config: %s" % (
self._name, self._shutdown_msg))
if config_params['is_shutdown']:
raise error("Can not update MCU '%s' config as it is shutdown" % (
self._name,))
return config_params
def _log_info(self):
msgparser = self._serial.get_msgparser()
message_count = len(msgparser.get_messages())
version, build_versions = msgparser.get_version_info()
log_info = [
"Loaded MCU '%s' %d commands (%s / %s)"
% (self._name, message_count, version, build_versions),
"MCU '%s' config: %s" % (self._name, " ".join(
["%s=%s" % (k, v) for k, v in self.get_constants().items()]))]
return "\n".join(log_info)
def _connect(self):
config_params = self._send_get_config()
if not config_params['is_config']:
if self._restart_method == 'rpi_usb':
# Only configure mcu after usb power reset
self._check_restart("full reset before config")
# Not configured - send config and issue get_config again
self._send_config(None)
config_params = self._send_get_config()
if not config_params['is_config'] and not self.is_fileoutput():
raise error("Unable to configure MCU '%s'" % (self._name,))
else:
start_reason = self._printer.get_start_args().get("start_reason")
if start_reason == 'firmware_restart':
raise error("Failed automated reset of MCU '%s'"
% (self._name,))
# Already configured - send init commands
self._send_config(config_params['crc'])
# Setup steppersync with the move_count returned by get_config
move_count = config_params['move_count']
if move_count < self._reserved_move_slots:
raise error("Too few moves available on MCU '%s'" % (self._name,))
ss_move_count = move_count - self._reserved_move_slots
motion_queuing = self._printer.lookup_object('motion_queuing')
self._steppersync = motion_queuing.allocate_steppersync(
self, self._serial.get_serialqueue(), ss_move_count)
self._ffi_lib.steppersync_set_time(self._steppersync,
0., self._mcu_freq)
# Log config information
move_msg = "Configured MCU '%s' (%d moves)" % (self._name, move_count)
logging.info(move_msg)
log_info = self._log_info() + "\n" + move_msg
self._printer.set_rollover_info(self._name, log_info, log=False)
def check_restart_on_crc_mismatch(self):
self._check_restart("CRC mismatch")
def check_restart_on_send_config(self):
if self._restart_method == 'rpi_usb':
# Only configure mcu after usb power reset
self._check_restart("full reset before config")
def check_restart_on_attach(self):
resmeth = self._restart_method
serialport, baud = self._conn_helper.get_serialport()
if resmeth == 'rpi_usb' and not os.path.exists(serialport):
# Try toggling usb power
self._check_restart("enable power")
def lookup_attach_uart_rts(self):
# Cheetah boards require RTS to be deasserted
# else a reset will trigger the built-in bootloader.
return (self._restart_method != "cheetah")
def _mcu_identify(self):
if self.is_fileoutput():
self._connect_file()
else:
resmeth = self._restart_method
if resmeth == 'rpi_usb' and not os.path.exists(self._serialport):
# Try toggling usb power
self._check_restart("enable power")
try:
if self._canbus_iface is not None:
cbid = self._printer.lookup_object('canbus_ids')
nodeid = cbid.get_nodeid(self._serialport)
self._serial.connect_canbus(self._serialport, nodeid,
self._canbus_iface)
elif self._baud:
# Cheetah boards require RTS to be deasserted
# else a reset will trigger the built-in bootloader.
rts = (resmeth != "cheetah")
self._serial.connect_uart(self._serialport, self._baud, rts)
else:
self._serial.connect_pipe(self._serialport)
self._clocksync.connect(self._serial)
except serialhdl.error as e:
raise error(str(e))
logging.info(self._log_info())
ppins = self._printer.lookup_object('pins')
pin_resolver = ppins.get_pin_resolver(self._name)
for cname, value in self.get_constants().items():
if cname.startswith("RESERVE_PINS_"):
for pin in value.split(','):
pin_resolver.reserve_pin(pin, cname[13:])
self._mcu_freq = self.get_constant_float('CLOCK_FREQ')
self._stats_sumsq_base = self.get_constant_float('STATS_SUMSQ_BASE')
self._emergency_stop_cmd = self.lookup_command("emergency_stop")
self._reset_cmd = self.try_lookup_command("reset")
self._config_reset_cmd = self.try_lookup_command("config_reset")
self._reset_cmd = self._mcu.try_lookup_command("reset")
self._config_reset_cmd = self._mcu.try_lookup_command("config_reset")
ext_only = self._reset_cmd is None and self._config_reset_cmd is None
msgparser = self._serial.get_msgparser()
mbaud = msgparser.get_constant('SERIAL_BAUD', None)
@ -827,112 +620,18 @@ class MCU:
self._is_mcu_bridge = True
self._printer.register_event_handler("klippy:firmware_restart",
self._firmware_restart_bridge)
version, build_versions = msgparser.get_version_info()
self._get_status_info['mcu_version'] = version
self._get_status_info['mcu_build_versions'] = build_versions
self._get_status_info['mcu_constants'] = msgparser.get_constants()
self.register_response(self._handle_shutdown, 'shutdown')
self.register_response(self._handle_shutdown, 'is_shutdown')
self.register_response(self._handle_mcu_stats, 'stats')
def _ready(self):
if self.is_fileoutput():
return
# Check that reported mcu frequency is in range
mcu_freq = self._mcu_freq
systime = self._reactor.monotonic()
get_clock = self._clocksync.get_clock
calc_freq = get_clock(systime + 1) - get_clock(systime)
freq_diff = abs(mcu_freq - calc_freq)
mcu_freq_mhz = int(mcu_freq / 1000000. + 0.5)
calc_freq_mhz = int(calc_freq / 1000000. + 0.5)
if freq_diff > mcu_freq*0.01 and mcu_freq_mhz != calc_freq_mhz:
pconfig = self._printer.lookup_object('configfile')
msg = ("MCU '%s' configured for %dMhz but running at %dMhz!"
% (self._name, mcu_freq_mhz, calc_freq_mhz))
pconfig.runtime_warning(msg)
# Config creation helpers
def setup_pin(self, pin_type, pin_params):
pcs = {'endstop': MCU_endstop,
'digital_out': MCU_digital_out, 'pwm': MCU_pwm, 'adc': MCU_adc}
if pin_type not in pcs:
raise pins.error("pin type %s not supported on mcu" % (pin_type,))
return pcs[pin_type](self, pin_params)
def create_oid(self):
self._oid_count += 1
return self._oid_count - 1
def register_config_callback(self, cb):
self._config_callbacks.append(cb)
def add_config_cmd(self, cmd, is_init=False, on_restart=False):
if is_init:
self._init_cmds.append(cmd)
elif on_restart:
self._restart_cmds.append(cmd)
else:
self._config_cmds.append(cmd)
def get_query_slot(self, oid):
slot = self.seconds_to_clock(oid * .01)
t = int(self.estimated_print_time(self._reactor.monotonic()) + 1.5)
return self.print_time_to_clock(t) + slot
def seconds_to_clock(self, time):
return int(time * self._mcu_freq)
def get_max_stepper_error(self):
return self._max_stepper_error
def min_schedule_time(self):
return MIN_SCHEDULE_TIME
def max_nominal_duration(self):
return MAX_NOMINAL_DURATION
# Wrapper functions
def get_printer(self):
return self._printer
def get_name(self):
return self._name
def register_response(self, cb, msg, oid=None):
self._serial.register_response(cb, msg, oid)
def alloc_command_queue(self):
return self._serial.alloc_command_queue()
def lookup_command(self, msgformat, cq=None):
return CommandWrapper(self._serial, msgformat, cq,
debugoutput=self.is_fileoutput())
def lookup_query_command(self, msgformat, respformat, oid=None,
cq=None, is_async=False):
return CommandQueryWrapper(self._serial, msgformat, respformat, oid,
cq, is_async, self._printer.command_error)
def try_lookup_command(self, msgformat):
try:
return self.lookup_command(msgformat)
except self._serial.get_msgparser().error as e:
return None
def get_enumerations(self):
return self._serial.get_msgparser().get_enumerations()
def get_constants(self):
return self._serial.get_msgparser().get_constants()
def get_constant_float(self, name):
return self._serial.get_msgparser().get_constant_float(name)
def print_time_to_clock(self, print_time):
return self._clocksync.print_time_to_clock(print_time)
def clock_to_print_time(self, clock):
return self._clocksync.clock_to_print_time(clock)
def estimated_print_time(self, eventtime):
return self._clocksync.estimated_print_time(eventtime)
def clock32_to_clock64(self, clock32):
return self._clocksync.clock32_to_clock64(clock32)
# Restarts
def _disconnect(self):
self._serial.disconnect()
self._steppersync = None
def _shutdown(self, force=False):
if (self._emergency_stop_cmd is None
or (self._is_shutdown and not force)):
return
self._emergency_stop_cmd.send()
def _restart_arduino(self):
logging.info("Attempting MCU '%s' reset", self._name)
self._disconnect()
serialhdl.arduino_reset(self._serialport, self._reactor)
serialport, baud = self._conn_helper.get_serialport()
serialhdl.arduino_reset(serialport, self._reactor)
def _restart_cheetah(self):
logging.info("Attempting MCU '%s' Cheetah-style reset", self._name)
self._disconnect()
serialhdl.cheetah_reset(self._serialport, self._reactor)
serialport, baud = self._conn_helper.get_serialport()
serialhdl.cheetah_reset(serialport, self._reactor)
def _restart_via_command(self):
if ((self._reset_cmd is None and self._config_reset_cmd is None)
or not self._clocksync.is_active()):
@ -942,8 +641,7 @@ class MCU:
if self._reset_cmd is None:
# Attempt reset via config_reset command
logging.info("Attempting MCU '%s' config_reset command", self._name)
self._is_shutdown = True
self._shutdown(force=True)
self._conn_helper.force_local_shutdown()
self._reactor.pause(self._reactor.monotonic() + 0.015)
self._config_reset_cmd.send()
else:
@ -971,15 +669,133 @@ class MCU:
self._restart_arduino()
def _firmware_restart_bridge(self):
self._firmware_restart(True)
# Move queue tracking
def request_move_queue_slot(self):
self._reserved_move_slots += 1
def check_active(self, print_time, eventtime):
if self._steppersync is None:
# Low-level mcu connection management helper
class MCUConnectHelper:
def __init__(self, config, mcu, clocksync):
self._mcu = mcu
self._clocksync = clocksync
self._printer = printer = config.get_printer()
self._reactor = printer.get_reactor()
self._name = name = mcu.get_name()
# Serial port
self._serial = serialhdl.SerialReader(self._reactor, mcu_name=name)
self._baud = 0
self._canbus_iface = None
canbus_uuid = config.get('canbus_uuid', None)
if canbus_uuid is not None:
self._serialport = canbus_uuid
self._canbus_iface = config.get('canbus_interface', 'can0')
cbid = self._printer.load_object(config, 'canbus_ids')
cbid.add_uuid(config, canbus_uuid, self._canbus_iface)
self._printer.load_object(config, 'canbus_stats %s' % (name,))
else:
self._serialport = config.get('serial')
if not (self._serialport.startswith("/dev/rpmsg_")
or self._serialport.startswith("/tmp/klipper_host_")):
self._baud = config.getint('baud', 250000, minval=2400)
# Shutdown tracking
self._emergency_stop_cmd = None
self._is_shutdown = self._is_timeout = False
self._shutdown_clock = 0
self._shutdown_msg = ""
# Register handlers
printer.register_event_handler("klippy:mcu_identify",
self._mcu_identify)
self._restart_helper = MCURestartHelper(config, self)
printer.register_event_handler("klippy:shutdown", self._shutdown)
def get_mcu(self):
return self._mcu
def get_serial(self):
return self._serial
def get_clocksync(self):
return self._clocksync
def get_serialport(self):
return self._serialport, self._baud
def get_restart_helper(self):
return self._restart_helper
def _handle_shutdown(self, params):
if self._is_shutdown:
return
offset, freq = self._clocksync.calibrate_clock(print_time, eventtime)
self._ffi_lib.steppersync_set_time(self._steppersync, offset, freq)
if (self._clocksync.is_active() or self.is_fileoutput()
self._is_shutdown = True
clock = params.get("clock")
if clock is not None:
self._shutdown_clock = self._mcu.clock32_to_clock64(clock)
self._shutdown_msg = msg = params['static_string_id']
event_type = params['#name']
self._printer.invoke_async_shutdown(
"MCU shutdown", {"reason": msg, "mcu": self._name,
"event_type": event_type})
logging.info("MCU '%s' %s: %s\n%s\n%s", self._name, event_type,
self._shutdown_msg, self._clocksync.dump_debug(),
self._serial.dump_debug())
def _handle_starting(self, params):
if not self._is_shutdown:
self._printer.invoke_async_shutdown("MCU '%s' spontaneous restart"
% (self._name,))
def log_info(self):
msgparser = self._serial.get_msgparser()
message_count = len(msgparser.get_messages())
version, build_versions = msgparser.get_version_info()
log_info = [
"Loaded MCU '%s' %d commands (%s / %s)"
% (self._name, message_count, version, build_versions),
"MCU '%s' config: %s" % (self._name, " ".join(
["%s=%s" % (k, v)
for k, v in msgparser.get_constants().items()]))]
return "\n".join(log_info)
def _attach_file(self):
# In a debugging mode. Open debug output file and read data dictionary
start_args = self._printer.get_start_args()
if self._name == 'mcu':
out_fname = start_args.get('debugoutput')
dict_fname = start_args.get('dictionary')
else:
out_fname = start_args.get('debugoutput') + "-" + self._name
dict_fname = start_args.get('dictionary_' + self._name)
outfile = open(out_fname, 'wb')
dfile = open(dict_fname, 'rb')
dict_data = dfile.read()
dfile.close()
self._serial.connect_file(outfile, dict_data)
self._clocksync.connect_file(self._serial)
def _attach(self):
self._restart_helper.check_restart_on_attach()
try:
if self._canbus_iface is not None:
cbid = self._printer.lookup_object('canbus_ids')
nodeid = cbid.get_nodeid(self._serialport)
self._serial.connect_canbus(self._serialport, nodeid,
self._canbus_iface)
elif self._baud:
rts = self._restart_helper.lookup_attach_uart_rts()
self._serial.connect_uart(self._serialport, self._baud, rts)
else:
self._serial.connect_pipe(self._serialport)
self._clocksync.connect(self._serial)
except serialhdl.error as e:
raise error(str(e))
def _mcu_identify(self):
if self._mcu.is_fileoutput():
self._attach_file()
else:
self._attach()
logging.info(self.log_info())
# Setup shutdown handling
self._emergency_stop_cmd = self._mcu.lookup_command("emergency_stop")
self._mcu.register_response(self._handle_shutdown, 'shutdown')
self._mcu.register_response(self._handle_shutdown, 'is_shutdown')
self._mcu.register_response(self._handle_starting, 'starting')
def _shutdown(self, force=False):
if (self._emergency_stop_cmd is None
or (self._is_shutdown and not force)):
return
self._emergency_stop_cmd.send()
def force_local_shutdown(self):
self._is_shutdown = True
self._shutdown(force=True)
def check_timeout(self, eventtime):
if (self._clocksync.is_active() or self._mcu.is_fileoutput()
or self._is_timeout):
return
self._is_timeout = True
@ -987,13 +803,68 @@ class MCU:
self._name, eventtime)
self._printer.invoke_shutdown("Lost communication with MCU '%s'" % (
self._name,))
# Misc external commands
def is_fileoutput(self):
return self._printer.get_start_args().get('debugoutput') is not None
def is_shutdown(self):
return self._is_shutdown
def get_shutdown_clock(self):
return self._shutdown_clock
def get_shutdown_msg(self):
return self._shutdown_msg
# Handle statistics reporting
class MCUStatsHelper:
def __init__(self, config, conn_helper):
self._printer = printer = config.get_printer()
self._mcu = mcu = conn_helper.get_mcu()
self._serial = conn_helper.get_serial()
self._clocksync = conn_helper.get_clocksync()
self._reactor = printer.get_reactor()
self._name = mcu.get_name()
# Statistics tracking
self._mcu_freq = 0.
self._get_status_info = {}
self._stats_sumsq_base = 0.
self._mcu_tick_avg = 0.
self._mcu_tick_stddev = 0.
self._mcu_tick_awake = 0.
# Register handlers
printer.register_event_handler("klippy:ready", self._ready)
printer.register_event_handler("klippy:mcu_identify",
self._mcu_identify)
def _handle_mcu_stats(self, params):
count = params['count']
tick_sum = params['sum']
c = 1.0 / (count * self._mcu_freq)
self._mcu_tick_avg = tick_sum * c
tick_sumsq = params['sumsq'] * self._stats_sumsq_base
diff = count*tick_sumsq - tick_sum**2
self._mcu_tick_stddev = c * math.sqrt(max(0., diff))
self._mcu_tick_awake = tick_sum / self._mcu_freq
def _mcu_identify(self):
self._mcu_freq = self._mcu.get_constant_float('CLOCK_FREQ')
self._stats_sumsq_base = self._mcu.get_constant_float(
'STATS_SUMSQ_BASE')
msgparser = self._serial.get_msgparser()
version, build_versions = msgparser.get_version_info()
self._get_status_info['mcu_version'] = version
self._get_status_info['mcu_build_versions'] = build_versions
self._get_status_info['mcu_constants'] = msgparser.get_constants()
self._mcu.register_response(self._handle_mcu_stats, 'stats')
def _ready(self):
if self._mcu.is_fileoutput():
return
# Check that reported mcu frequency is in range
mcu_freq = self._mcu_freq
systime = self._reactor.monotonic()
get_clock = self._clocksync.get_clock
calc_freq = get_clock(systime + 1) - get_clock(systime)
freq_diff = abs(mcu_freq - calc_freq)
mcu_freq_mhz = int(mcu_freq / 1000000. + 0.5)
calc_freq_mhz = int(calc_freq / 1000000. + 0.5)
if freq_diff > mcu_freq*0.01 and mcu_freq_mhz != calc_freq_mhz:
pconfig = self._printer.lookup_object('configfile')
msg = ("MCU '%s' configured for %dMhz but running at %dMhz!"
% (self._name, mcu_freq_mhz, calc_freq_mhz))
pconfig.runtime_warning(msg)
def get_status(self, eventtime=None):
return dict(self._get_status_info)
def stats(self, eventtime):
@ -1006,6 +877,244 @@ class MCU:
self._get_status_info['last_stats'] = last_stats
return False, '%s: %s' % (self._name, stats)
# Handle process of configuring an mcu
class MCUConfigHelper:
def __init__(self, config, conn_helper):
self._printer = printer = config.get_printer()
self._conn_helper = conn_helper
self._mcu = mcu = conn_helper.get_mcu()
self._serial = conn_helper.get_serial()
self._clocksync = conn_helper.get_clocksync()
self._reactor = printer.get_reactor()
self._name = mcu.get_name()
# Configuration tracking
self._oid_count = 0
self._config_callbacks = []
self._config_cmds = []
self._restart_cmds = []
self._init_cmds = []
self._mcu_freq = 0.
self._reserved_move_slots = 0
# Register handlers
printer.lookup_object('pins').register_chip(self._name, mcu)
printer.register_event_handler("klippy:mcu_identify",
self._mcu_identify)
printer.register_event_handler("klippy:connect", self._connect)
def _send_config(self, prev_crc):
# Build config commands
for cb in self._config_callbacks:
cb()
self._config_cmds.insert(0, "allocate_oids count=%d"
% (self._oid_count,))
# Resolve pin names
ppins = self._printer.lookup_object('pins')
pin_resolver = ppins.get_pin_resolver(self._name)
for cmdlist in (self._config_cmds, self._restart_cmds, self._init_cmds):
for i, cmd in enumerate(cmdlist):
cmdlist[i] = pin_resolver.update_command(cmd)
# Calculate config CRC
encoded_config = '\n'.join(self._config_cmds).encode()
config_crc = zlib.crc32(encoded_config) & 0xffffffff
self.add_config_cmd("finalize_config crc=%d" % (config_crc,))
if prev_crc is not None and config_crc != prev_crc:
restart_helper = self._conn_helper.get_restart_helper()
restart_helper.check_restart_on_crc_mismatch()
raise error("MCU '%s' CRC does not match config" % (self._name,))
# Transmit config messages (if needed)
try:
if prev_crc is None:
logging.info("Sending MCU '%s' printer configuration...",
self._name)
for c in self._config_cmds:
self._serial.send(c)
else:
for c in self._restart_cmds:
self._serial.send(c)
# Transmit init messages
for c in self._init_cmds:
self._serial.send(c)
except msgproto.enumeration_error as e:
enum_name, enum_value = e.get_enum_params()
if enum_name == 'pin':
# Raise pin name errors as a config error (not a protocol error)
raise self._printer.config_error(
"Pin '%s' is not a valid pin name on mcu '%s'"
% (enum_value, self._name))
raise
def _send_get_config(self):
get_config_cmd = self._mcu.lookup_query_command(
"get_config",
"config is_config=%c crc=%u is_shutdown=%c move_count=%hu")
if self._mcu.is_fileoutput():
return { 'is_config': 0, 'move_count': 500, 'crc': 0 }
config_params = get_config_cmd.send()
if self._conn_helper.is_shutdown():
raise error("MCU '%s' error during config: %s" % (
self._name, self._conn_helper.get_shutdown_msg()))
if config_params['is_shutdown']:
raise error("Can not update MCU '%s' config as it is shutdown" % (
self._name,))
return config_params
def _connect(self):
config_params = self._send_get_config()
if not config_params['is_config']:
restart_helper = self._conn_helper.get_restart_helper()
restart_helper.check_restart_on_send_config()
# Not configured - send config and issue get_config again
self._send_config(None)
config_params = self._send_get_config()
if not config_params['is_config'] and not self._mcu.is_fileoutput():
raise error("Unable to configure MCU '%s'" % (self._name,))
else:
start_reason = self._printer.get_start_args().get("start_reason")
if start_reason == 'firmware_restart':
raise error("Failed automated reset of MCU '%s'"
% (self._name,))
# Already configured - send init commands
self._send_config(config_params['crc'])
# Setup steppersync with the move_count returned by get_config
move_count = config_params['move_count']
if move_count < self._reserved_move_slots:
raise error("Too few moves available on MCU '%s'" % (self._name,))
ss_move_count = move_count - self._reserved_move_slots
motion_queuing = self._printer.lookup_object('motion_queuing')
motion_queuing.setup_mcu_movequeue(
self._mcu, self._serial.get_serialqueue(), ss_move_count)
# Log config information
move_msg = "Configured MCU '%s' (%d moves)" % (self._name, move_count)
logging.info(move_msg)
log_info = self._conn_helper.log_info() + "\n" + move_msg
self._printer.set_rollover_info(self._name, log_info, log=False)
def _mcu_identify(self):
self._mcu_freq = self._mcu.get_constant_float('CLOCK_FREQ')
ppins = self._printer.lookup_object('pins')
pin_resolver = ppins.get_pin_resolver(self._name)
for cname, value in self._mcu.get_constants().items():
if cname.startswith("RESERVE_PINS_"):
for pin in value.split(','):
pin_resolver.reserve_pin(pin, cname[13:])
# Config creation helpers
def setup_pin(self, pin_type, pin_params):
pcs = {'endstop': MCU_endstop,
'digital_out': MCU_digital_out, 'pwm': MCU_pwm, 'adc': MCU_adc}
if pin_type not in pcs:
raise pins.error("pin type %s not supported on mcu" % (pin_type,))
return pcs[pin_type](self._mcu, pin_params)
def create_oid(self):
self._oid_count += 1
return self._oid_count - 1
def register_config_callback(self, cb):
self._config_callbacks.append(cb)
def add_config_cmd(self, cmd, is_init=False, on_restart=False):
if is_init:
self._init_cmds.append(cmd)
elif on_restart:
self._restart_cmds.append(cmd)
else:
self._config_cmds.append(cmd)
def get_query_slot(self, oid):
slot = self.seconds_to_clock(oid * .01)
t = int(self._mcu.estimated_print_time(self._reactor.monotonic()) + 1.5)
return self._mcu.print_time_to_clock(t) + slot
def seconds_to_clock(self, time):
return int(time * self._mcu_freq)
def request_move_queue_slot(self):
self._reserved_move_slots += 1
# Main MCU class
class MCU:
error = error
def __init__(self, config, clocksync):
self._printer = printer = config.get_printer()
self._clocksync = clocksync
self._name = config.get_name()
if self._name.startswith('mcu '):
self._name = self._name[4:]
# Low-level connection and helpers
self._conn_helper = MCUConnectHelper(config, self, clocksync)
self._serial = self._conn_helper.get_serial()
self._config_helper = MCUConfigHelper(self, self._conn_helper)
self._stats_helper = MCUStatsHelper(self, self._conn_helper)
printer.load_object(config, "error_mcu")
# Alter time reporting when debugging
if self.is_fileoutput():
def dummy_estimated_print_time(eventtime):
return 0.
self.estimated_print_time = dummy_estimated_print_time
def get_name(self):
return self._name
def get_printer(self):
return self._printer
def is_fileoutput(self):
return self._printer.get_start_args().get('debugoutput') is not None
# MCU Configuration wrappers
def setup_pin(self, pin_type, pin_params):
return self._config_helper.setup_pin(pin_type, pin_params)
def create_oid(self):
return self._config_helper.create_oid()
def register_config_callback(self, cb):
self._config_helper.register_config_callback(cb)
def add_config_cmd(self, cmd, is_init=False, on_restart=False):
self._config_helper.add_config_cmd(cmd, is_init, on_restart)
def request_move_queue_slot(self):
self._config_helper.request_move_queue_slot()
def get_query_slot(self, oid):
return self._config_helper.get_query_slot(oid)
def seconds_to_clock(self, time):
return self._config_helper.seconds_to_clock(time)
# Command Handler helpers
def min_schedule_time(self):
return MIN_SCHEDULE_TIME
def max_nominal_duration(self):
return MAX_NOMINAL_DURATION
def lookup_command(self, msgformat, cq=None):
return CommandWrapper(self._serial, msgformat, cq,
debugoutput=self.is_fileoutput())
def lookup_query_command(self, msgformat, respformat, oid=None,
cq=None, is_async=False):
return CommandQueryWrapper(self._serial, msgformat, respformat, oid,
cq, is_async, self._printer.command_error)
def try_lookup_command(self, msgformat):
try:
return self.lookup_command(msgformat)
except self._serial.get_msgparser().error as e:
return None
# SerialHdl wrappers
def register_response(self, cb, msg, oid=None):
self._serial.register_response(cb, msg, oid)
def alloc_command_queue(self):
return self._serial.alloc_command_queue()
# MsgParser wrappers
def get_enumerations(self):
return self._serial.get_msgparser().get_enumerations()
def get_constants(self):
return self._serial.get_msgparser().get_constants()
def get_constant_float(self, name):
return self._serial.get_msgparser().get_constant_float(name)
# ClockSync wrappers
def print_time_to_clock(self, print_time):
return self._clocksync.print_time_to_clock(print_time)
def clock_to_print_time(self, clock):
return self._clocksync.clock_to_print_time(clock)
def estimated_print_time(self, eventtime):
return self._clocksync.estimated_print_time(eventtime)
def clock32_to_clock64(self, clock32):
return self._clocksync.clock32_to_clock64(clock32)
def calibrate_clock(self, print_time, eventtime):
offset, freq = self._clocksync.calibrate_clock(print_time, eventtime)
self._conn_helper.check_timeout(eventtime)
return offset, freq
# Low-level connection wrappers
def is_shutdown(self):
return self._conn_helper.is_shutdown()
def get_shutdown_clock(self):
return self._conn_helper.get_shutdown_clock()
# Statistics wrappers
def get_status(self, eventtime=None):
return self._stats_helper.get_status(eventtime)
def stats(self, eventtime):
return self._stats_helper.stats(eventtime)
def add_printer_objects(config):
printer = config.get_printer()
reactor = printer.get_reactor()

View file

@ -1,6 +1,6 @@
# File descriptor and timer event helper
#
# Copyright (C) 2016-2020 Kevin O'Connor <kevin@koconnor.net>
# Copyright (C) 2016-2025 Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import os, gc, select, math, time, logging, queue
@ -14,6 +14,7 @@ class ReactorTimer:
def __init__(self, callback, waketime):
self.callback = callback
self.waketime = waketime
self.timer_is_running = False
class ReactorCompletion:
class sentinel: pass
@ -54,8 +55,6 @@ class ReactorFileHandler:
self.fd = fd
self.read_callback = read_callback
self.write_callback = write_callback
def fileno(self):
return self.fd
class ReactorGreenlet(greenlet.greenlet):
def __init__(self, run):
@ -108,8 +107,13 @@ class SelectReactor:
self._pipe_fds = None
self._async_queue = queue.Queue()
# File descriptors
self._dummy_fd_hdl = ReactorFileHandler(-1, (lambda e: None),
(lambda e: None))
self._fds = {}
self._read_fds = []
self._write_fds = []
self._READ = 1
self._WRITE = 2
# Greenlets
self._g_dispatch = None
self._greenlets = []
@ -118,6 +122,8 @@ class SelectReactor:
return tuple(self._last_gc_times)
# Timers
def update_timer(self, timer_handler, waketime):
if timer_handler.timer_is_running:
return
timer_handler.waketime = waketime
self._next_timer = min(self._next_timer, waketime)
def register_timer(self, callback, waketime=NEVER):
@ -155,7 +161,9 @@ class SelectReactor:
waketime = t.waketime
if eventtime >= waketime:
t.waketime = self.NEVER
t.timer_is_running = True
t.waketime = waketime = t.callback(eventtime)
t.timer_is_running = False
if g_dispatch is not self._g_dispatch:
self._next_timer = min(self._next_timer, waketime)
self._end_greenlet(g_dispatch)
@ -240,48 +248,54 @@ class SelectReactor:
# File descriptors
def register_fd(self, fd, read_callback, write_callback=None):
file_handler = ReactorFileHandler(fd, read_callback, write_callback)
self._fds[fd] = file_handler
self.set_fd_wake(file_handler, True, False)
return file_handler
def unregister_fd(self, file_handler):
if file_handler in self._read_fds:
self._read_fds.pop(self._read_fds.index(file_handler))
if file_handler in self._write_fds:
self._write_fds.pop(self._write_fds.index(file_handler))
self.set_fd_wake(file_handler, False, False)
del self._fds[file_handler.fd]
def set_fd_wake(self, file_handler, is_readable=True, is_writeable=False):
if file_handler in self._read_fds:
fd = file_handler.fd
if fd in self._read_fds:
if not is_readable:
self._read_fds.pop(self._read_fds.index(file_handler))
self._read_fds.remove(fd)
elif is_readable:
self._read_fds.append(file_handler)
if file_handler in self._write_fds:
self._read_fds.append(fd)
if fd in self._write_fds:
if not is_writeable:
self._write_fds.pop(self._write_fds.index(file_handler))
self._write_fds.remove(fd)
elif is_writeable:
self._write_fds.append(file_handler)
self._write_fds.append(fd)
def _check_fds(self, eventtime, hdls):
g_dispatch = self._g_dispatch
for fd, event in hdls:
hdl = self._fds.get(fd, self._dummy_fd_hdl)
if event & self._READ:
hdl.read_callback(eventtime)
if g_dispatch is not self._g_dispatch:
self._end_greenlet(g_dispatch)
return self.monotonic()
if event & self._WRITE:
hdl.write_callback(eventtime)
if g_dispatch is not self._g_dispatch:
self._end_greenlet(g_dispatch)
return self.monotonic()
return eventtime
# Main loop
def _dispatch_loop(self):
self._g_dispatch = g_dispatch = greenlet.getcurrent()
self._g_dispatch = greenlet.getcurrent()
busy = True
eventtime = self.monotonic()
while self._process:
timeout = self._check_timers(eventtime, busy)
busy = False
res = select.select(self._read_fds, self.write_fds, [], timeout)
res = select.select(self._read_fds, self._write_fds, [], timeout)
eventtime = self.monotonic()
for fd in res[0]:
if res[0] or res[1]:
busy = True
fd.read_callback(eventtime)
if g_dispatch is not self._g_dispatch:
self._end_greenlet(g_dispatch)
eventtime = self.monotonic()
break
for fd in res[1]:
busy = True
fd.write_callback(eventtime)
if g_dispatch is not self._g_dispatch:
self._end_greenlet(g_dispatch)
eventtime = self.monotonic()
break
hdls = ([(fd, self._READ) for fd in res[0]]
+ [(fd, self._WRITE) for fd in res[1]])
eventtime = self._check_fds(eventtime, hdls)
self._g_dispatch = None
def run(self):
if self._pipe_fds is None:
@ -310,30 +324,27 @@ class PollReactor(SelectReactor):
def __init__(self, gc_checking=False):
SelectReactor.__init__(self, gc_checking)
self._poll = select.poll()
self._fds = {}
self._READ = select.POLLIN | select.POLLHUP
self._WRITE = select.POLLOUT
# File descriptors
def register_fd(self, fd, read_callback, write_callback=None):
file_handler = ReactorFileHandler(fd, read_callback, write_callback)
fds = self._fds.copy()
fds[fd] = file_handler
self._fds = fds
self._poll.register(file_handler, select.POLLIN | select.POLLHUP)
self._fds[fd] = file_handler
self._poll.register(file_handler.fd, select.POLLIN | select.POLLHUP)
return file_handler
def unregister_fd(self, file_handler):
self._poll.unregister(file_handler)
fds = self._fds.copy()
del fds[file_handler.fd]
self._fds = fds
self._poll.unregister(file_handler.fd)
del self._fds[file_handler.fd]
def set_fd_wake(self, file_handler, is_readable=True, is_writeable=False):
flags = select.POLLHUP
if is_readable:
flags |= select.POLLIN
if is_writeable:
flags |= select.POLLOUT
self._poll.modify(file_handler, flags)
self._poll.modify(file_handler.fd, flags)
# Main loop
def _dispatch_loop(self):
self._g_dispatch = g_dispatch = greenlet.getcurrent()
self._g_dispatch = greenlet.getcurrent()
busy = True
eventtime = self.monotonic()
while self._process:
@ -341,50 +352,36 @@ class PollReactor(SelectReactor):
busy = False
res = self._poll.poll(int(math.ceil(timeout * 1000.)))
eventtime = self.monotonic()
for fd, event in res:
if res:
busy = True
if event & (select.POLLIN | select.POLLHUP):
self._fds[fd].read_callback(eventtime)
if g_dispatch is not self._g_dispatch:
self._end_greenlet(g_dispatch)
eventtime = self.monotonic()
break
if event & select.POLLOUT:
self._fds[fd].write_callback(eventtime)
if g_dispatch is not self._g_dispatch:
self._end_greenlet(g_dispatch)
eventtime = self.monotonic()
break
eventtime = self._check_fds(eventtime, res)
self._g_dispatch = None
class EPollReactor(SelectReactor):
def __init__(self, gc_checking=False):
SelectReactor.__init__(self, gc_checking)
self._epoll = select.epoll()
self._fds = {}
self._READ = select.EPOLLIN | select.EPOLLHUP
self._WRITE = select.EPOLLOUT
# File descriptors
def register_fd(self, fd, read_callback, write_callback=None):
file_handler = ReactorFileHandler(fd, read_callback, write_callback)
fds = self._fds.copy()
fds[fd] = read_callback
self._fds = fds
self._fds[fd] = file_handler
self._epoll.register(fd, select.EPOLLIN | select.EPOLLHUP)
return file_handler
def unregister_fd(self, file_handler):
self._epoll.unregister(file_handler.fd)
fds = self._fds.copy()
del fds[file_handler.fd]
self._fds = fds
del self._fds[file_handler.fd]
def set_fd_wake(self, file_handler, is_readable=True, is_writeable=False):
flags = select.POLLHUP
flags = select.EPOLLHUP
if is_readable:
flags |= select.EPOLLIN
if is_writeable:
flags |= select.EPOLLOUT
self._epoll.modify(file_handler, flags)
self._epoll.modify(file_handler.fd, flags)
# Main loop
def _dispatch_loop(self):
self._g_dispatch = g_dispatch = greenlet.getcurrent()
self._g_dispatch = greenlet.getcurrent()
busy = True
eventtime = self.monotonic()
while self._process:
@ -392,20 +389,9 @@ class EPollReactor(SelectReactor):
busy = False
res = self._epoll.poll(timeout)
eventtime = self.monotonic()
for fd, event in res:
if res:
busy = True
if event & (select.EPOLLIN | select.EPOLLHUP):
self._fds[fd].read_callback(eventtime)
if g_dispatch is not self._g_dispatch:
self._end_greenlet(g_dispatch)
eventtime = self.monotonic()
break
if event & select.EPOLLOUT:
self._fds[fd].write_callback(eventtime)
if g_dispatch is not self._g_dispatch:
self._end_greenlet(g_dispatch)
eventtime = self.monotonic()
break
eventtime = self._check_fds(eventtime, res)
self._g_dispatch = None
# Use the poll based reactor if it is available

View file

@ -16,6 +16,7 @@ class error(Exception):
MIN_BOTH_EDGE_DURATION = 0.000000500
MIN_OPTIMIZED_BOTH_EDGE_DURATION = 0.000000150
MAX_STEPCOMPRESS_ERROR = 0.000025
# Interface to low-level mcu and chelper code
class MCU_stepper:
@ -29,7 +30,7 @@ class MCU_stepper:
self._units_in_radians = units_in_radians
self._step_dist = rotation_dist / steps_per_rotation
self._mcu = mcu = step_pin_params['chip']
self._oid = oid = mcu.create_oid()
self._oid = mcu.create_oid()
mcu.register_config_callback(self._build_config)
self._step_pin = step_pin_params['pin']
self._invert_step = step_pin_params['invert']
@ -44,8 +45,11 @@ class MCU_stepper:
self._reset_cmd_tag = self._get_position_cmd = None
self._active_callbacks = []
motion_queuing = printer.load_object(config, 'motion_queuing')
self._stepqueue = motion_queuing.allocate_stepcompress(mcu, oid)
sname = self._name.split()[-1]
self._syncemitter = motion_queuing.allocate_syncemitter(mcu, sname)
ffi_main, ffi_lib = chelper.get_ffi()
self._stepqueue = ffi_lib.syncemitter_get_stepcompress(
self._syncemitter)
ffi_lib.stepcompress_set_invert_sdir(self._stepqueue, self._invert_dir)
self._stepper_kinematics = None
self._itersolve_check_active = ffi_lib.itersolve_check_active
@ -119,10 +123,9 @@ class MCU_stepper:
self._get_position_cmd = self._mcu.lookup_query_command(
"stepper_get_position oid=%c",
"stepper_position oid=%c pos=%i", oid=self._oid)
max_error = self._mcu.get_max_stepper_error()
max_error_ticks = self._mcu.seconds_to_clock(max_error)
max_error_ticks = self._mcu.seconds_to_clock(MAX_STEPCOMPRESS_ERROR)
ffi_main, ffi_lib = chelper.get_ffi()
ffi_lib.stepcompress_fill(self._stepqueue, max_error_ticks,
ffi_lib.stepcompress_fill(self._stepqueue, self._oid, max_error_ticks,
step_cmd_tag, dir_cmd_tag)
def get_oid(self):
return self._oid
@ -192,7 +195,7 @@ class MCU_stepper:
mcu_pos = self.get_mcu_position()
self._stepper_kinematics = sk
ffi_main, ffi_lib = chelper.get_ffi()
ffi_lib.stepcompress_set_stepper_kinematics(self._stepqueue, sk);
ffi_lib.syncemitter_set_stepper_kinematics(self._syncemitter, sk);
self.set_trapq(self._trapq)
self._set_mcu_position(mcu_pos)
return old_sk
@ -202,9 +205,7 @@ class MCU_stepper:
if ret:
raise error("Internal error in stepcompress")
data = (self._reset_cmd_tag, self._oid, 0)
ret = ffi_lib.stepcompress_queue_msg(self._stepqueue, data, len(data))
if ret:
raise error("Internal error in stepcompress")
ffi_lib.syncemitter_queue_msg(self._syncemitter, 0, data, len(data))
self._query_mcu_position()
def _query_mcu_position(self):
if self._mcu.is_fileoutput():

View file

@ -45,9 +45,10 @@ class Move:
self.max_start_v2 = 0.
self.max_cruise_v2 = velocity**2
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
# Setup for minimum_cruise_ratio checks
self.max_mcr_start_v2 = 0.
self.mcr_delta_v2 = 2.0 * move_d * toolhead.mcr_pseudo_accel
def limit_speed(self, speed, accel):
speed2 = speed**2
if speed2 < self.max_cruise_v2:
@ -55,7 +56,7 @@ class Move:
self.min_move_t = self.move_d / speed
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)
self.mcr_delta_v2 = min(self.mcr_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"):
@ -94,8 +95,8 @@ class Move:
move_centripetal_v2, pmove_centripetal_v2)
# Apply limits
self.max_start_v2 = max_start_v2
self.max_smoothed_v2 = min(
max_start_v2, prev_move.max_smoothed_v2 + prev_move.smooth_delta_v2)
self.max_mcr_start_v2 = min(
max_start_v2, prev_move.max_mcr_start_v2 + prev_move.mcr_delta_v2)
def set_junction(self, start_v2, cruise_v2, end_v2):
# Determine accel, cruise, and decel portions of the move distance
half_inv_accel = .5 / self.accel
@ -112,7 +113,7 @@ class Move:
self.cruise_t = cruise_d / cruise_v
self.decel_t = decel_d / ((end_v + cruise_v) * 0.5)
LOOKAHEAD_FLUSH_TIME = 0.250
LOOKAHEAD_FLUSH_TIME = 0.150
# Class to track a list of pending move requests and to facilitate
# "look-ahead" across moves to reduce acceleration between moves.
@ -125,6 +126,8 @@ class LookAheadQueue:
self.junction_flush = LOOKAHEAD_FLUSH_TIME
def set_flush_time(self, flush_time):
self.junction_flush = flush_time
def is_empty(self):
return not self.queue
def get_last(self):
if self.queue:
return self.queue[-1]
@ -137,46 +140,45 @@ class LookAheadQueue:
# Traverse queue from last to first move and determine maximum
# junction speed assuming the robot comes to a complete stop
# after the last move.
delayed = []
next_end_v2 = next_smoothed_v2 = peak_cruise_v2 = 0.
junction_info = [None] * flush_count
next_start_v2 = next_mcr_start_v2 = peak_cruise_v2 = 0.
pending_cv2_assign = 0
for i in range(flush_count-1, -1, -1):
move = queue[i]
reachable_start_v2 = next_end_v2 + move.delta_v2
reachable_start_v2 = next_start_v2 + move.delta_v2
start_v2 = min(move.max_start_v2, reachable_start_v2)
reachable_smoothed_v2 = next_smoothed_v2 + move.smooth_delta_v2
smoothed_v2 = min(move.max_smoothed_v2, reachable_smoothed_v2)
if smoothed_v2 < reachable_smoothed_v2:
cruise_v2 = None
pending_cv2_assign += 1
reach_mcr_start_v2 = next_mcr_start_v2 + move.mcr_delta_v2
mcr_start_v2 = min(move.max_mcr_start_v2, reach_mcr_start_v2)
if mcr_start_v2 < reach_mcr_start_v2:
# It's possible for this move to accelerate
if (smoothed_v2 + move.smooth_delta_v2 > next_smoothed_v2
or delayed):
# This move can decelerate or this is a full accel
# move after a full decel move
if (mcr_start_v2 + move.mcr_delta_v2 > next_mcr_start_v2
or pending_cv2_assign > 1):
# This move can both accel and decel, or this is a
# full accel move followed by a full decel move
if update_flush_count and peak_cruise_v2:
flush_count = i
flush_count = i + pending_cv2_assign
update_flush_count = False
peak_cruise_v2 = min(move.max_cruise_v2, (
smoothed_v2 + reachable_smoothed_v2) * .5)
if delayed:
# Propagate peak_cruise_v2 to any delayed moves
if not update_flush_count and i < flush_count:
mc_v2 = peak_cruise_v2
for m, ms_v2, me_v2 in reversed(delayed):
mc_v2 = min(mc_v2, ms_v2)
m.set_junction(min(ms_v2, mc_v2), mc_v2
, min(me_v2, mc_v2))
del delayed[:]
if not update_flush_count and i < flush_count:
cruise_v2 = min((start_v2 + reachable_start_v2) * .5
, move.max_cruise_v2, peak_cruise_v2)
move.set_junction(min(start_v2, cruise_v2), cruise_v2
, min(next_end_v2, cruise_v2))
else:
# Delay calculating this move until peak_cruise_v2 is known
delayed.append((move, start_v2, next_end_v2))
next_end_v2 = start_v2
next_smoothed_v2 = smoothed_v2
peak_cruise_v2 = (mcr_start_v2 + reach_mcr_start_v2) * .5
cruise_v2 = min((start_v2 + reachable_start_v2) * .5
, move.max_cruise_v2, peak_cruise_v2)
pending_cv2_assign = 0
junction_info[i] = (move, start_v2, cruise_v2, next_start_v2)
next_start_v2 = start_v2
next_mcr_start_v2 = mcr_start_v2
if update_flush_count or not flush_count:
return []
# Traverse queue in forward direction to propagate cruise_v2
prev_cruise_v2 = 0.
for i in range(flush_count):
move, start_v2, cruise_v2, next_start_v2 = junction_info[i]
if cruise_v2 is None:
# This move can't accelerate - propagate cruise_v2 from previous
cruise_v2 = min(prev_cruise_v2, start_v2)
move.set_junction(min(start_v2, cruise_v2), cruise_v2
, min(next_start_v2, cruise_v2))
prev_cruise_v2 = cruise_v2
# Remove processed moves from the queue
res = queue[:flush_count]
del queue[:flush_count]
@ -190,28 +192,16 @@ class LookAheadQueue:
# Check if enough moves have been queued to reach the target flush time.
return self.junction_flush <= 0.
BUFFER_TIME_LOW = 1.0
BUFFER_TIME_HIGH = 2.0
BUFFER_TIME_HIGH = 1.0
BUFFER_TIME_START = 0.250
BGFLUSH_LOW_TIME = 0.200
BGFLUSH_BATCH_TIME = 0.200
BGFLUSH_EXTRA_TIME = 0.250
MIN_KIN_TIME = 0.100
MOVE_BATCH_TIME = 0.500
STEPCOMPRESS_FLUSH_TIME = 0.050
SDS_CHECK_TIME = 0.001 # step+dir+step filter in stepcompress.c
DRIP_SEGMENT_TIME = 0.050
DRIP_TIME = 0.100
PRIMING_CMD_TIME = 0.100
# Main code to track events (and their timing) on the printer toolhead
class ToolHead:
def __init__(self, config):
self.printer = config.get_printer()
self.reactor = self.printer.get_reactor()
self.all_mcus = [
m for n, m in self.printer.lookup_objects(module='mcu')]
self.mcu = self.all_mcus[0]
self.mcu = self.printer.lookup_object('mcu')
self.lookahead = LookAheadQueue()
self.lookahead.set_flush_time(BUFFER_TIME_HIGH)
self.commanded_pos = [0., 0., 0., 0.]
@ -222,7 +212,7 @@ class ToolHead:
0.5, below=1., minval=0.)
self.square_corner_velocity = config.getfloat(
'square_corner_velocity', 5., minval=0.)
self.junction_deviation = self.max_accel_to_decel = 0.
self.junction_deviation = self.mcr_pseudo_accel = 0.
self._calc_junction_deviation()
# Input stall detection
self.check_stall_time = 0.
@ -236,16 +226,10 @@ class ToolHead:
self.print_time = 0.
self.special_queuing_state = "NeedPrime"
self.priming_timer = None
# Flush tracking
self.flush_timer = self.reactor.register_timer(self._flush_handler)
self.do_kick_flush_timer = True
self.last_flush_time = self.min_restart_time = 0.
self.need_flush_time = self.step_gen_time = 0.
# Kinematic step generation scan window time tracking
self.kin_flush_delay = SDS_CHECK_TIME
self.kin_flush_times = []
# Setup for generating moves
self.motion_queuing = self.printer.load_object(config, 'motion_queuing')
self.motion_queuing.register_flush_callback(self._handle_step_flush,
can_add_trapq=True)
self.trapq = self.motion_queuing.allocate_trapq()
self.trapq_append = self.motion_queuing.lookup_trapq_append()
# Create kinematics class
@ -268,33 +252,13 @@ class ToolHead:
# Register handlers
self.printer.register_event_handler("klippy:shutdown",
self._handle_shutdown)
# Print time and flush tracking
def _advance_flush_time(self, flush_time):
flush_time = max(flush_time, self.last_flush_time)
# Generate steps via itersolve
sg_flush_want = min(flush_time + STEPCOMPRESS_FLUSH_TIME,
self.print_time - self.kin_flush_delay)
sg_flush_time = max(sg_flush_want, flush_time)
trapq_free_time = sg_flush_time - self.kin_flush_delay
self.motion_queuing.flush_motion_queues(flush_time, sg_flush_time,
trapq_free_time)
self.min_restart_time = max(self.min_restart_time, sg_flush_time)
self.last_flush_time = flush_time
# Print time tracking
def _advance_move_time(self, next_print_time):
pt_delay = self.kin_flush_delay + STEPCOMPRESS_FLUSH_TIME
flush_time = max(self.last_flush_time, self.print_time - pt_delay)
self.print_time = max(self.print_time, next_print_time)
want_flush_time = max(flush_time, self.print_time - pt_delay)
while 1:
flush_time = min(flush_time + MOVE_BATCH_TIME, want_flush_time)
self._advance_flush_time(flush_time)
if flush_time >= want_flush_time:
break
def _calc_print_time(self):
curtime = self.reactor.monotonic()
est_print_time = self.mcu.estimated_print_time(curtime)
kin_time = max(est_print_time + MIN_KIN_TIME, self.min_restart_time)
kin_time += self.kin_flush_delay
kin_time = self.motion_queuing.calc_step_gen_restart(est_print_time)
min_print_time = max(est_print_time + BUFFER_TIME_START, kin_time)
if min_print_time > self.print_time:
self.print_time = min_print_time
@ -328,19 +292,28 @@ class ToolHead:
for cb in move.timing_callbacks:
cb(next_move_time)
# Generate steps for moves
self.note_mcu_movequeue_activity(next_move_time + self.kin_flush_delay)
self._advance_move_time(next_move_time)
def _flush_lookahead(self):
# Transit from "NeedPrime"/"Priming"/"Drip"/main state to "NeedPrime"
self.motion_queuing.note_mcu_movequeue_activity(next_move_time)
def _flush_lookahead(self, is_runout=False):
# Transit from "NeedPrime"/"Priming"/main state to "NeedPrime"
prev_print_time = self.print_time
self._process_lookahead()
self.special_queuing_state = "NeedPrime"
self.need_check_pause = -1.
self.lookahead.set_flush_time(BUFFER_TIME_HIGH)
self.check_stall_time = 0.
if is_runout and prev_print_time != self.print_time:
self.check_stall_time = self.print_time
def _handle_step_flush(self, flush_time, step_gen_time):
if self.special_queuing_state:
return
# In "main" state - flush lookahead if buffer runs low
kin_flush_delay = self.motion_queuing.get_kin_flush_delay()
if step_gen_time >= self.print_time - kin_flush_delay - 0.001:
self._flush_lookahead(is_runout=True)
def flush_step_generation(self):
self._flush_lookahead()
self._advance_flush_time(self.step_gen_time)
self.min_restart_time = max(self.min_restart_time, self.print_time)
self.motion_queuing.flush_all_steps()
def get_last_move_time(self):
if self.special_queuing_state:
self._flush_lookahead()
@ -348,78 +321,59 @@ class ToolHead:
else:
self._process_lookahead()
return self.print_time
def _check_pause(self):
eventtime = self.reactor.monotonic()
est_print_time = self.mcu.estimated_print_time(eventtime)
buffer_time = self.print_time - est_print_time
if self.special_queuing_state:
if self.check_stall_time:
# Was in "NeedPrime" state and got there from idle input
if est_print_time < self.check_stall_time:
self.print_stall += 1
self.check_stall_time = 0.
# Transition from "NeedPrime"/"Priming" state to "Priming" state
self.special_queuing_state = "Priming"
self.need_check_pause = -1.
if self.priming_timer is None:
self.priming_timer = self.reactor.register_timer(
self._priming_handler)
wtime = eventtime + max(0.100, buffer_time - BUFFER_TIME_LOW)
self.reactor.update_timer(self.priming_timer, wtime)
# Check if there are lots of queued moves and pause if so
while 1:
pause_time = buffer_time - BUFFER_TIME_HIGH
if pause_time <= 0.:
break
if not self.can_pause:
self.need_check_pause = self.reactor.NEVER
return
eventtime = self.reactor.pause(eventtime + min(1., pause_time))
est_print_time = self.mcu.estimated_print_time(eventtime)
buffer_time = self.print_time - est_print_time
if not self.special_queuing_state:
# In main state - defer pause checking until needed
self.need_check_pause = est_print_time + BUFFER_TIME_HIGH + 0.100
def _priming_handler(self, eventtime):
self.reactor.unregister_timer(self.priming_timer)
self.priming_timer = None
try:
if self.special_queuing_state == "Priming":
self._flush_lookahead()
self.check_stall_time = self.print_time
self._flush_lookahead(is_runout=True)
except:
logging.exception("Exception in priming_handler")
self.printer.invoke_shutdown("Exception in priming_handler")
return self.reactor.NEVER
def _flush_handler(self, eventtime):
try:
def _check_priming_state(self, eventtime):
if self.lookahead.is_empty():
# In "NeedPrime" state and can remain there
return
est_print_time = self.mcu.estimated_print_time(eventtime)
if self.check_stall_time:
# Was in "NeedPrime" state and got there from idle input
if est_print_time < self.check_stall_time:
self.print_stall += 1
self.check_stall_time = 0.
# Transition from "NeedPrime"/"Priming" state to "Priming" state
self.special_queuing_state = "Priming"
self.need_check_pause = -1.
if self.priming_timer is None:
self.priming_timer = self.reactor.register_timer(
self._priming_handler)
will_pause_time = self.print_time - est_print_time - BUFFER_TIME_HIGH
wtime = eventtime + max(0., will_pause_time) + PRIMING_CMD_TIME
self.reactor.update_timer(self.priming_timer, wtime)
def _check_pause(self):
eventtime = self.reactor.monotonic()
if self.special_queuing_state:
# In "NeedPrime"/"Priming" state - update priming expiration timer
self._check_priming_state(eventtime)
# Check if there are lots of queued moves and pause if so
did_pause = False
while 1:
est_print_time = self.mcu.estimated_print_time(eventtime)
if not self.special_queuing_state:
# In "main" state - flush lookahead if buffer runs low
print_time = self.print_time
buffer_time = print_time - est_print_time
if buffer_time > BUFFER_TIME_LOW:
# Running normally - reschedule check
return eventtime + buffer_time - BUFFER_TIME_LOW
# Under ran low buffer mark - flush lookahead queue
self._flush_lookahead()
if print_time != self.print_time:
self.check_stall_time = self.print_time
# In "NeedPrime"/"Priming" state - flush queues if needed
while 1:
end_flush = self.need_flush_time + BGFLUSH_EXTRA_TIME
if self.last_flush_time >= end_flush:
self.do_kick_flush_timer = True
return self.reactor.NEVER
buffer_time = self.last_flush_time - est_print_time
if buffer_time > BGFLUSH_LOW_TIME:
return eventtime + buffer_time - BGFLUSH_LOW_TIME
ftime = est_print_time + BGFLUSH_LOW_TIME + BGFLUSH_BATCH_TIME
self._advance_flush_time(min(end_flush, ftime))
except:
logging.exception("Exception in flush_handler")
self.printer.invoke_shutdown("Exception in flush_handler")
return self.reactor.NEVER
pause_time = self.print_time - est_print_time - BUFFER_TIME_HIGH
if pause_time <= 0.:
break
if not self.can_pause:
self.need_check_pause = self.reactor.NEVER
return
pause_time = max(.005, min(1., pause_time))
eventtime = self.reactor.pause(eventtime + pause_time)
did_pause = True
if not self.special_queuing_state:
# In main state - defer pause checking
self.need_check_pause = self.print_time
if not did_pause:
# May be falling behind - yield to avoid starving other tasks
self.reactor.pause(self.reactor.NOW)
# Movement commands
def get_position(self):
return list(self.commanded_pos)
@ -458,6 +412,7 @@ class ToolHead:
self.move(curpos, speed)
self.printer.send_event("toolhead:manual_move")
def dwell(self, delay):
self._flush_lookahead()
next_print_time = self.get_last_move_time() + max(0., delay)
self._advance_move_time(next_print_time)
self._check_pause()
@ -491,32 +446,6 @@ class ToolHead:
def get_extra_axes(self):
return [None, None, None] + self.extra_axes
# Homing "drip move" handling
def drip_update_time(self, next_print_time, drip_completion):
# Transition from "NeedPrime"/"Priming"/main state to "Drip" state
self.special_queuing_state = "Drip"
self.need_check_pause = self.reactor.NEVER
self.reactor.update_timer(self.flush_timer, self.reactor.NEVER)
self.do_kick_flush_timer = False
self.lookahead.set_flush_time(BUFFER_TIME_HIGH)
self.check_stall_time = 0.
# Update print_time in segments until drip_completion signal
flush_delay = DRIP_TIME + STEPCOMPRESS_FLUSH_TIME + self.kin_flush_delay
while self.print_time < next_print_time:
if drip_completion.test():
break
curtime = self.reactor.monotonic()
est_print_time = self.mcu.estimated_print_time(curtime)
wait_time = self.print_time - est_print_time - flush_delay
if wait_time > 0. and self.can_pause:
# Pause before sending more steps
drip_completion.wait(curtime + wait_time)
continue
npt = min(self.print_time + DRIP_SEGMENT_TIME, next_print_time)
self.note_mcu_movequeue_activity(npt + self.kin_flush_delay)
self._advance_move_time(npt)
# Exit "Drip" state
self.reactor.update_timer(self.flush_timer, self.reactor.NOW)
self.flush_step_generation()
def _drip_load_trapq(self, submit_move):
# Queue move into trapezoid motion queue (trapq)
if submit_move.move_d:
@ -524,18 +453,17 @@ class ToolHead:
self.lookahead.add_move(submit_move)
moves = self.lookahead.flush()
self._calc_print_time()
next_move_time = self.print_time
start_time = end_time = self.print_time
for move in moves:
self.trapq_append(
self.trapq, next_move_time,
self.trapq, end_time,
move.accel_t, move.cruise_t, move.decel_t,
move.start_pos[0], move.start_pos[1], move.start_pos[2],
move.axes_r[0], move.axes_r[1], move.axes_r[2],
move.start_v, move.cruise_v, move.accel)
next_move_time = (next_move_time + move.accel_t
+ move.cruise_t + move.decel_t)
end_time = end_time + move.accel_t + move.cruise_t + move.decel_t
self.lookahead.reset()
return next_move_time
return start_time, end_time
def drip_move(self, newpos, speed, drip_completion):
# Create and verify move is valid
newpos = newpos[:3] + self.commanded_pos[3:]
@ -543,29 +471,25 @@ class ToolHead:
if move.move_d:
self.kin.check_move(move)
# Make sure stepper movement doesn't start before nominal start time
self.dwell(self.kin_flush_delay)
kin_flush_delay = self.motion_queuing.get_kin_flush_delay()
self.dwell(kin_flush_delay)
# Transmit move in "drip" mode
self._process_lookahead()
next_move_time = self._drip_load_trapq(move)
self.drip_update_time(next_move_time, drip_completion)
start_time, end_time = self._drip_load_trapq(move)
self.motion_queuing.drip_update_time(start_time, end_time,
drip_completion)
# Move finished; cleanup any remnants on trapq
self.motion_queuing.wipe_trapq(self.trapq)
# Misc commands
def stats(self, eventtime):
max_queue_time = max(self.print_time, self.last_flush_time)
for m in self.all_mcus:
m.check_active(max_queue_time, eventtime)
est_print_time = self.mcu.estimated_print_time(eventtime)
buffer_time = self.print_time - est_print_time
is_active = buffer_time > -60. or not self.special_queuing_state
if self.special_queuing_state == "Drip":
buffer_time = 0.
return is_active, "print_time=%.3f buffer_time=%.3f print_stall=%d" % (
self.print_time, max(buffer_time, 0.), self.print_stall)
def check_busy(self, eventtime):
est_print_time = self.mcu.estimated_print_time(eventtime)
lookahead_empty = not self.lookahead.queue
return self.print_time, est_print_time, lookahead_empty
return self.print_time, est_print_time, self.lookahead.is_empty()
def get_status(self, eventtime):
print_time = self.print_time
estimated_print_time = self.mcu.estimated_print_time(eventtime)
@ -588,33 +512,18 @@ class ToolHead:
return self.kin
def get_trapq(self):
return self.trapq
def note_step_generation_scan_time(self, delay, old_delay=0.):
self.flush_step_generation()
if old_delay:
self.kin_flush_times.pop(self.kin_flush_times.index(old_delay))
if delay:
self.kin_flush_times.append(delay)
new_delay = max(self.kin_flush_times + [SDS_CHECK_TIME])
self.kin_flush_delay = new_delay
def register_lookahead_callback(self, callback):
last_move = self.lookahead.get_last()
if last_move is None:
callback(self.get_last_move_time())
return
last_move.timing_callbacks.append(callback)
def note_mcu_movequeue_activity(self, mq_time, is_step_gen=True):
self.need_flush_time = max(self.need_flush_time, mq_time)
if is_step_gen:
self.step_gen_time = max(self.step_gen_time, mq_time)
if self.do_kick_flush_timer:
self.do_kick_flush_timer = False
self.reactor.update_timer(self.flush_timer, self.reactor.NOW)
def get_max_velocity(self):
return self.max_velocity, self.max_accel
def _calc_junction_deviation(self):
scv2 = self.square_corner_velocity**2
self.junction_deviation = scv2 * (math.sqrt(2.) - 1.) / self.max_accel
self.max_accel_to_decel = self.max_accel * (1. - self.min_cruise_ratio)
self.mcr_pseudo_accel = self.max_accel * (1. - self.min_cruise_ratio)
def set_max_velocities(self, max_velocity, max_accel,
square_corner_velocity, min_cruise_ratio):
if max_velocity is not None:
@ -662,7 +571,7 @@ class ToolHeadCommandHelper:
msg = ("max_velocity: %.6f\n"
"max_accel: %.6f\n"
"minimum_cruise_ratio: %.6f\n"
"square_corner_velocity: %.6f" % (mv, ma, scv, mcr))
"square_corner_velocity: %.6f" % (mv, ma, mcr, scv))
self.printer.set_rollover_info("toolhead", "toolhead: %s" % (msg,))
if (max_velocity is None and max_accel is None
and square_corner_velocity is None and min_cruise_ratio is None):

View file

@ -51,6 +51,15 @@ def create_pty(ptyname):
# Helper code for extracting mcu build info
######################################################################
def _try_read_file(filename, maxsize=32*1024):
try:
with open(filename, 'r') as f:
return f.read(maxsize)
except (IOError, OSError) as e:
logging.debug("Exception on read %s: %s", filename,
traceback.format_exc())
return None
def dump_file_stats(build_dir, filename):
fname = os.path.join(build_dir, filename)
try:
@ -66,20 +75,14 @@ def dump_mcu_build():
build_dir = os.path.join(os.path.dirname(__file__), '..')
# Try to log last mcu config
dump_file_stats(build_dir, '.config')
try:
f = open(os.path.join(build_dir, '.config'), 'r')
data = f.read(32*1024)
f.close()
data = _try_read_file(os.path.join(build_dir, '.config'))
if data is not None:
logging.info("========= Last MCU build config =========\n%s"
"=======================", data)
except:
pass
# Try to log last mcu build version
dump_file_stats(build_dir, 'out/klipper.dict')
try:
f = open(os.path.join(build_dir, 'out/klipper.dict'), 'r')
data = f.read(32*1024)
f.close()
data = _try_read_file(os.path.join(build_dir, 'out/klipper.dict'))
data = json.loads(data)
logging.info("Last MCU build version: %s", data.get('version', ''))
logging.info("Last MCU build tools: %s", data.get('build_versions', ''))
@ -111,13 +114,8 @@ setup_python2_wrappers()
######################################################################
def get_cpu_info():
try:
f = open('/proc/cpuinfo', 'r')
data = f.read()
f.close()
except (IOError, OSError) as e:
logging.debug("Exception on read /proc/cpuinfo: %s",
traceback.format_exc())
data = _try_read_file('/proc/cpuinfo', maxsize=1024*1024)
if data is None:
return "?"
lines = [l.split(':', 1) for l in data.split('\n')]
lines = [(l[0].strip(), l[1].strip()) for l in lines if len(l) == 2]
@ -125,13 +123,25 @@ def get_cpu_info():
model_name = dict(lines).get("model name", "?")
return "%d core %s" % (core_count, model_name)
def get_device_info():
data = _try_read_file('/proc/device-tree/model')
if data is None:
data = _try_read_file("/sys/class/dmi/id/product_name")
if data is None:
return "?"
return data.rstrip(' \0').strip()
def get_linux_version():
data = _try_read_file('/proc/version')
if data is None:
return "?"
return data.strip()
def get_version_from_file(klippy_src):
try:
with open(os.path.join(klippy_src, '.version')) as h:
return h.read().rstrip()
except IOError:
pass
return "?"
data = _try_read_file(os.path.join(klippy_src, '.version'))
if data is None:
return "?"
return data.rstrip()
def _get_repo_info(gitdir):
repo_info = {"branch": "?", "remote": "?", "url": "?"}

View file

@ -77,6 +77,9 @@ def calibrate_shaper(datas, csv_output, *, shapers, damping_ratio, scv,
def plot_freq_response(lognames, calibration_data, shapers,
selected_shaper, max_freq):
max_freq_bin = calibration_data.freq_bins.max()
if max_freq > max_freq_bin:
max_freq = max_freq_bin
freqs = calibration_data.freq_bins
psd = calibration_data.psd_sum[freqs <= max_freq]
px = calibration_data.psd_x[freqs <= max_freq]
@ -166,7 +169,7 @@ def main():
default=None, help="shaper damping_ratio parameter")
opts.add_option("--test_damping_ratios", type="string",
dest="test_damping_ratios", default=None,
help="a comma-separated liat of damping ratios to test " +
help="a comma-separated list of damping ratios to test " +
"input shaper for")
options, args = opts.parse_args()
if len(args) < 1:

View file

@ -5,20 +5,15 @@
# Copyright (C) 2020 Dmitry Butyugin <dmbutyugin@google.com>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import optparse, math
import importlib, math, optparse, os, sys
import matplotlib
sys.path.append(os.path.join(os.path.dirname(os.path.realpath(__file__)),
'..', 'klippy'))
shaper_defs = importlib.import_module('.shaper_defs', 'extras')
# A set of damping ratios to calculate shaper response for
DAMPING_RATIOS=[0.05, 0.1, 0.2]
# Parameters of the input shaper
SHAPER_FREQ=50.0
SHAPER_DAMPING_RATIO=0.1
# Simulate input shaping of step function for these true resonance frequency
# and damping ratio
STEP_SIMULATION_RESONANCE_FREQ=60.
STEP_SIMULATION_DAMPING_RATIO=0.15
DEFAULT_DAMPING_RATIOS=[0.075, 0.1, 0.15]
# If set, defines which range of frequencies to plot shaper frequency response
PLOT_FREQ_RANGE = [] # If empty, will be automatically determined
@ -30,86 +25,8 @@ PLOT_FREQ_STEP = .01
# Input shapers
######################################################################
def get_zv_shaper():
df = math.sqrt(1. - SHAPER_DAMPING_RATIO**2)
K = math.exp(-SHAPER_DAMPING_RATIO * math.pi / df)
t_d = 1. / (SHAPER_FREQ * df)
A = [1., K]
T = [0., .5*t_d]
return (A, T, "ZV")
def get_zvd_shaper():
df = math.sqrt(1. - SHAPER_DAMPING_RATIO**2)
K = math.exp(-SHAPER_DAMPING_RATIO * math.pi / df)
t_d = 1. / (SHAPER_FREQ * df)
A = [1., 2.*K, K**2]
T = [0., .5*t_d, t_d]
return (A, T, "ZVD")
def get_mzv_shaper():
df = math.sqrt(1. - SHAPER_DAMPING_RATIO**2)
K = math.exp(-.75 * SHAPER_DAMPING_RATIO * math.pi / df)
t_d = 1. / (SHAPER_FREQ * df)
a1 = 1. - 1. / math.sqrt(2.)
a2 = (math.sqrt(2.) - 1.) * K
a3 = a1 * K * K
A = [a1, a2, a3]
T = [0., .375*t_d, .75*t_d]
return (A, T, "MZV")
def get_ei_shaper():
v_tol = 0.05 # vibration tolerance
df = math.sqrt(1. - SHAPER_DAMPING_RATIO**2)
K = math.exp(-SHAPER_DAMPING_RATIO * math.pi / df)
t_d = 1. / (SHAPER_FREQ * df)
a1 = .25 * (1. + v_tol)
a2 = .5 * (1. - v_tol) * K
a3 = a1 * K * K
A = [a1, a2, a3]
T = [0., .5*t_d, t_d]
return (A, T, "EI")
def get_2hump_ei_shaper():
v_tol = 0.05 # vibration tolerance
df = math.sqrt(1. - SHAPER_DAMPING_RATIO**2)
K = math.exp(-SHAPER_DAMPING_RATIO * math.pi / df)
t_d = 1. / (SHAPER_FREQ * df)
V2 = v_tol**2
X = pow(V2 * (math.sqrt(1. - V2) + 1.), 1./3.)
a1 = (3.*X*X + 2.*X + 3.*V2) / (16.*X)
a2 = (.5 - a1) * K
a3 = a2 * K
a4 = a1 * K * K * K
A = [a1, a2, a3, a4]
T = [0., .5*t_d, t_d, 1.5*t_d]
return (A, T, "2-hump EI")
def get_3hump_ei_shaper():
v_tol = 0.05 # vibration tolerance
df = math.sqrt(1. - SHAPER_DAMPING_RATIO**2)
K = math.exp(-SHAPER_DAMPING_RATIO * math.pi / df)
t_d = 1. / (SHAPER_FREQ * df)
K2 = K*K
a1 = 0.0625 * (1. + 3. * v_tol + 2. * math.sqrt(2. * (v_tol + 1.) * v_tol))
a2 = 0.25 * (1. - v_tol) * K
a3 = (0.5 * (1. + v_tol) - 2. * a1) * K2
a4 = a2 * K2
a5 = a1 * K2 * K2
A = [a1, a2, a3, a4, a5]
T = [0., .5*t_d, t_d, 1.5*t_d, 2.*t_d]
return (A, T, "3-hump EI")
def estimate_shaper(shaper, freq, damping_ratio):
A, T, _ = shaper
A, T = shaper
n = len(T)
inv_D = 1. / sum(A)
omega = 2. * math.pi * freq
@ -123,14 +40,18 @@ def estimate_shaper(shaper, freq, damping_ratio):
return math.sqrt(S*S + C*C) * inv_D
def shift_pulses(shaper):
A, T, name = shaper
A, T = shaper
n = len(T)
ts = sum([A[i] * T[i] for i in range(n)]) / sum(A)
for i in range(n):
T[i] -= ts
# Shaper selection
get_shaper = get_ei_shaper
def get_shaper(shaper_name, shaper_freq, damping_ratio):
for s in shaper_defs.INPUT_SHAPERS:
if shaper_name.lower() == s.name:
return s.init_func(shaper_freq, damping_ratio)
return shaper_defs.get_none_shaper()
######################################################################
@ -148,44 +69,46 @@ def bisect(func, left, right):
right = mid
return .5 * (left + right)
def find_shaper_plot_range(shaper, vib_tol):
def find_shaper_plot_range(shaper, shaper_freq, test_damping_ratios, vib_tol):
def eval_shaper(freq):
return estimate_shaper(shaper, freq, DAMPING_RATIOS[0]) - vib_tol
return estimate_shaper(shaper, freq, test_damping_ratios[0]) - vib_tol
if not PLOT_FREQ_RANGE:
left = bisect(eval_shaper, 0., SHAPER_FREQ)
right = bisect(eval_shaper, SHAPER_FREQ, 2.4 * SHAPER_FREQ)
left = bisect(eval_shaper, 0., shaper_freq)
right = bisect(eval_shaper, shaper_freq, 2.4 * shaper_freq)
else:
left, right = PLOT_FREQ_RANGE
return (left, right)
def gen_shaper_response(shaper):
def gen_shaper_response(shaper, shaper_freq, test_damping_ratios):
# Calculate shaper vibration response on a range of frequencies
response = []
freqs = []
freq, freq_end = find_shaper_plot_range(shaper, vib_tol=0.25)
freq, freq_end = find_shaper_plot_range(shaper, shaper_freq,
test_damping_ratios, vib_tol=0.25)
while freq <= freq_end:
vals = []
for damping_ratio in DAMPING_RATIOS:
for damping_ratio in test_damping_ratios:
vals.append(estimate_shaper(shaper, freq, damping_ratio))
response.append(vals)
freqs.append(freq)
freq += PLOT_FREQ_STEP
legend = ['damping ratio = %.3f' % d_r for d_r in DAMPING_RATIOS]
legend = ['damping ratio = %.3f' % d_r for d_r in test_damping_ratios]
return freqs, response, legend
def gen_shaped_step_function(shaper):
def gen_shaped_step_function(shaper, shaper_freq,
system_freq, system_damping_ratio):
# Calculate shaping of a step function
A, T, _ = shaper
A, T = shaper
inv_D = 1. / sum(A)
n = len(T)
omega = 2. * math.pi * STEP_SIMULATION_RESONANCE_FREQ
damping = STEP_SIMULATION_DAMPING_RATIO * omega
omega_d = omega * math.sqrt(1. - STEP_SIMULATION_DAMPING_RATIO**2)
phase = math.acos(STEP_SIMULATION_DAMPING_RATIO)
omega = 2. * math.pi * system_freq
damping = system_damping_ratio * omega
omega_d = omega * math.sqrt(1. - system_damping_ratio**2)
phase = math.acos(system_damping_ratio)
t_start = T[0] - .5 / SHAPER_FREQ
t_end = T[-1] + 1.5 / STEP_SIMULATION_RESONANCE_FREQ
t_start = T[0] - .5 / shaper_freq
t_end = T[-1] + 1.5 / system_freq
result = []
time = []
t = t_start
@ -214,20 +137,24 @@ def gen_shaped_step_function(shaper):
result.append(val)
time.append(t)
t += .01 / SHAPER_FREQ
t += .01 / shaper_freq
legend = ['step', 'shaper commanded', 'system response']
return time, result, legend
def plot_shaper(shaper):
def plot_shaper(shaper_name, shaper_freq, damping_ratio, test_damping_ratios,
system_freq, system_damping_ratio):
shaper = get_shaper(shaper_name, shaper_freq, damping_ratio)
shift_pulses(shaper)
freqs, response, response_legend = gen_shaper_response(shaper)
time, step_vals, step_legend = gen_shaped_step_function(shaper)
freqs, response, response_legend = gen_shaper_response(
shaper, shaper_freq, test_damping_ratios)
time, step_vals, step_legend = gen_shaped_step_function(
shaper, shaper_freq, system_freq, system_damping_ratio)
fig, (ax1, ax2) = matplotlib.pyplot.subplots(nrows=2, figsize=(10,9))
ax1.set_title("Vibration response simulation for shaper '%s',\n"
"shaper_freq=%.1f Hz, damping_ratio=%.3f"
% (shaper[-1], SHAPER_FREQ, SHAPER_DAMPING_RATIO))
% (shaper_name, shaper_freq, damping_ratio))
ax1.plot(freqs, response)
ax1.set_ylim(bottom=0.)
fontP = matplotlib.font_manager.FontProperties()
@ -241,8 +168,7 @@ def plot_shaper(shaper):
ax1.grid(which='minor', color='lightgrey')
ax2.set_title("Unit step input, resonance frequency=%.1f Hz, "
"damping ratio=%.3f" % (STEP_SIMULATION_RESONANCE_FREQ,
STEP_SIMULATION_DAMPING_RATIO))
"damping ratio=%.3f" % (system_freq, system_damping_ratio))
ax2.plot(time, step_vals)
ax2.legend(step_legend, loc='best', prop=fontP)
ax2.set_xlabel('Time, sec')
@ -264,13 +190,48 @@ def main():
opts = optparse.OptionParser(usage)
opts.add_option("-o", "--output", type="string", dest="output",
default=None, help="filename of output graph")
opts.add_option("--shaper", type="string", dest="shaper", default="mzv",
help="a shaper to plot")
opts.add_option("--shaper_freq", type="float", dest="shaper_freq",
default=50.0, help="shaper frequency")
opts.add_option("--damping_ratio", type="float", dest="damping_ratio",
default=shaper_defs.DEFAULT_DAMPING_RATIO,
help="shaper damping_ratio parameter")
opts.add_option("--test_damping_ratios", type="string",
dest="test_damping_ratios",
default=",".join(["%.3f" % dr
for dr in DEFAULT_DAMPING_RATIOS]),
help="a comma-separated list of damping ratios to test " +
"input shaper for")
opts.add_option("--system_freq", type="float", dest="system_freq",
default=60.0,
help="natural frequency of a system for step simulation")
opts.add_option("--system_damping_ratio", type="float",
dest="system_damping_ratio", default=0.15,
help="damping_ratio of a system for step simulation")
options, args = opts.parse_args()
if len(args) != 0:
opts.error("Incorrect number of arguments")
if options.shaper.lower() not in [
s.name for s in shaper_defs.INPUT_SHAPERS]:
opts.error("Invalid --shaper=%s specified" % options.shaper)
if options.test_damping_ratios:
try:
test_damping_ratios = [float(s) for s in
options.test_damping_ratios.split(',')]
except ValueError:
opts.error("invalid floating point value in " +
"--test_damping_ratios param")
else:
test_damping_ratios = None
# Draw graph
setup_matplotlib(options.output is not None)
fig = plot_shaper(get_shaper())
fig = plot_shaper(options.shaper, options.shaper_freq,
options.damping_ratio, test_damping_ratios,
options.system_freq, options.system_damping_ratio)
# Show graph
if options.output is None:

View file

@ -1,14 +1,14 @@
#!/usr/bin/env python
# Script to parse a logging file, extract the stats, and graph them
#
# Copyright (C) 2016-2021 Kevin O'Connor <kevin@koconnor.net>
# Copyright (C) 2016-2025 Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import optparse, datetime
import matplotlib
MAXBANDWIDTH=25000.
MAXBUFFER=2.
MAXBUFFER=1.
STATS_INTERVAL=5.
TASK_MAX=0.0025

View file

@ -123,7 +123,8 @@ BOARD_DEFS = {
'spi_bus': "swspi",
'spi_pins': "PC8,PD2,PC12",
'cs_pin': "PC11",
'skip_verify': True
'skip_verify': True,
'requires_unique_fw_name': True
},
'monster8': {
'mcu': "stm32f407xx",

View file

@ -1380,7 +1380,32 @@ class MCUConnection:
input_sha = hashlib.sha1()
sd_sha = hashlib.sha1()
klipper_bin_path = self.board_config['klipper_bin_path']
add_ts = self.board_config.get('requires_unique_fw_name', False)
fw_path = self.board_config.get('firmware_path', "firmware.bin")
if add_ts:
fw_dir = os.path.dirname(fw_path)
fw_name, fw_ext = os.path.splitext(os.path.basename(fw_path))
ts = time.strftime("%Y%m%d%H%M%S")
fw_name_ts = f"{ts}{fw_name}{fw_ext}"
if fw_dir:
fw_path = os.path.join(fw_dir, fw_name_ts)
else:
fw_path = fw_name_ts
list_dir = fw_dir if fw_dir else ""
try:
output_line("\nSD Card FW Directory Contents:")
for f in self.fatfs.list_sd_directory(list_dir):
fname = f['name'].decode('utf-8')
if fname.endswith(fw_ext):
self.fatfs.remove_item(
os.path.join(list_dir, fname)
)
output_line(
"Old firmware file %s found and deleted"
% (fname,)
)
except Exception:
logging.exception("Error cleaning old firmware files")
try:
with open(klipper_bin_path, 'rb') as local_f:
with self.fatfs.open_file(fw_path, "wb") as sd_f:

View file

@ -141,7 +141,7 @@ lis2dw_query(struct lis2dw *ax, uint8_t oid)
if (ax->model == LIS3DH)
fifo_empty = fifo[1] & 0x20;
else
fifo_empty = fifo[1] & 0x3F;
fifo_empty = ((fifo[1] & 0x3F) == 0);
fifo_ovrn = fifo[1] & 0x40;
@ -167,7 +167,7 @@ lis2dw_query(struct lis2dw *ax, uint8_t oid)
if (ax->model == LIS3DH)
fifo_empty = fifo[0] & 0x20;
else
fifo_empty = fifo[0] & 0x3F;
fifo_empty = ((fifo[0] & 0x3F) == 0);
fifo_ovrn = fifo[0] & 0x40;

View file

@ -33,7 +33,7 @@ DECL_ENUMERATION_RANGE("pin", "PH0", GPIO('H', 0), 16);
DECL_ENUMERATION_RANGE("pin", "PI0", GPIO('I', 0), 16);
#endif
GPIO_TypeDef * const digital_regs[] = {
static GPIO_TypeDef * const digital_regs[] = {
['A' - 'A'] = GPIOA, GPIOB, GPIOC,
#ifdef GPIOD
['D' - 'A'] = GPIOD,
@ -66,20 +66,20 @@ regs_to_pin(GPIO_TypeDef *regs, uint32_t bit)
return 0;
}
// Verify that a gpio is a valid pin
static int
gpio_valid(uint32_t pin)
// Verify that a gpio is a valid pin and return its hardware register
GPIO_TypeDef *
gpio_pin_to_regs(uint32_t pin)
{
uint32_t port = GPIO2PORT(pin);
return port < ARRAY_SIZE(digital_regs) && digital_regs[port];
if (port >= ARRAY_SIZE(digital_regs) || !digital_regs[port])
shutdown("Not a valid pin");
return digital_regs[port];
}
struct gpio_out
gpio_out_setup(uint32_t pin, uint32_t val)
{
if (!gpio_valid(pin))
shutdown("Not an output pin");
GPIO_TypeDef *regs = digital_regs[GPIO2PORT(pin)];
GPIO_TypeDef *regs = gpio_pin_to_regs(pin);
gpio_clock_enable(regs);
struct gpio_out g = { .regs=regs, .bit=GPIO2BIT(pin) };
gpio_out_reset(g, val);
@ -129,9 +129,7 @@ gpio_out_write(struct gpio_out g, uint32_t val)
struct gpio_in
gpio_in_setup(uint32_t pin, int32_t pull_up)
{
if (!gpio_valid(pin))
shutdown("Not a valid input pin");
GPIO_TypeDef *regs = digital_regs[GPIO2PORT(pin)];
GPIO_TypeDef *regs = gpio_pin_to_regs(pin);
struct gpio_in g = { .regs=regs, .bit=GPIO2BIT(pin) };
gpio_in_reset(g, pull_up);
return g;

View file

@ -10,7 +10,7 @@
void
gpio_peripheral(uint32_t gpio, uint32_t mode, int pullup)
{
GPIO_TypeDef *regs = digital_regs[GPIO2PORT(gpio)];
GPIO_TypeDef *regs = gpio_pin_to_regs(gpio);
// Enable GPIO clock
gpio_clock_enable(regs);

View file

@ -25,7 +25,7 @@
#endif
// gpio.c
extern GPIO_TypeDef * const digital_regs[];
GPIO_TypeDef *gpio_pin_to_regs(uint32_t pin);
#define GPIO(PORT, NUM) (((PORT)-'A') * 16 + (NUM))
#define GPIO2PORT(PIN) ((PIN) / 16)
#define GPIO2BIT(PIN) (1<<((PIN) % 16))

View file

@ -15,79 +15,281 @@ struct spi_info {
uint8_t miso_pin, mosi_pin, sck_pin, miso_af, mosi_af, sck_af;
};
DECL_ENUMERATION("spi_bus", "spi2", 0);
DECL_CONSTANT_STR("BUS_PINS_spi2", "PB14,PB15,PB13");
DECL_ENUMERATION("spi_bus", "spi1", 1);
DECL_CONSTANT_STR("BUS_PINS_spi1", "PA6,PA7,PA5");
DECL_ENUMERATION("spi_bus", "spi1a", 2);
DECL_CONSTANT_STR("BUS_PINS_spi1a", "PB4,PB5,PB3");
#if CONFIG_MACH_STM32G4
DECL_ENUMERATION("spi_bus", "spi2_PA10_PA11_PF1", 3);
DECL_CONSTANT_STR("BUS_PINS_spi2_PA10_PA11_PF1", "PA10,PA11,PF1");
#elif !CONFIG_MACH_STM32F1
DECL_ENUMERATION("spi_bus", "spi2a", 3);
DECL_CONSTANT_STR("BUS_PINS_spi2a", "PC2,PC3,PB10");
#endif
#ifdef SPI3
DECL_ENUMERATION("spi_bus", "spi3", 4);
DECL_CONSTANT_STR("BUS_PINS_spi3", "PB4,PB5,PB3");
#if CONFIG_MACH_STM32F4 || CONFIG_MACH_STM32G4
#if CONFIG_MACH_STM32F0
DECL_ENUMERATION("spi_bus", "spi2_PB14_PB15_PB13", 0);
DECL_CONSTANT_STR("BUS_PINS_spi2_PB14_PB15_PB13", "PB14,PB15,PB13");
DECL_ENUMERATION("spi_bus", "spi1_PA6_PA7_PA5", 1);
DECL_CONSTANT_STR("BUS_PINS_spi1_PA6_PA7_PA5", "PA6,PA7,PA5");
DECL_ENUMERATION("spi_bus", "spi1_PB4_PB5_PB3", 2);
DECL_CONSTANT_STR("BUS_PINS_spi1_PB4_PB5_PB3", "PB4,PB5,PB3");
DECL_ENUMERATION("spi_bus", "spi2_PC2_PC3_PB10", 3);
DECL_CONSTANT_STR("BUS_PINS_spi2_PC2_PC3_PB10", "PC2,PC3,PB10");
// Deprecated "spi1" style mappings
DECL_ENUMERATION("spi_bus", "spi2", 0);
DECL_CONSTANT_STR("BUS_PINS_spi2", "PB14,PB15,PB13");
DECL_ENUMERATION("spi_bus", "spi1", 1);
DECL_CONSTANT_STR("BUS_PINS_spi1", "PA6,PA7,PA5");
DECL_ENUMERATION("spi_bus", "spi1a", 2);
DECL_CONSTANT_STR("BUS_PINS_spi1a", "PB4,PB5,PB3");
DECL_ENUMERATION("spi_bus", "spi2a", 3);
DECL_CONSTANT_STR("BUS_PINS_spi2a", "PC2,PC3,PB10");
#elif CONFIG_MACH_STM32F1
DECL_ENUMERATION("spi_bus", "spi2_PB14_PB15_PB13", 0);
DECL_CONSTANT_STR("BUS_PINS_spi2_PB14_PB15_PB13", "PB14,PB15,PB13");
DECL_ENUMERATION("spi_bus", "spi1_PA6_PA7_PA5", 1);
DECL_CONSTANT_STR("BUS_PINS_spi1_PA6_PA7_PA5", "PA6,PA7,PA5");
DECL_ENUMERATION("spi_bus", "spi1_PB4_PB5_PB3", 2);
DECL_CONSTANT_STR("BUS_PINS_spi1_PB4_PB5_PB3", "PB4,PB5,PB3");
DECL_ENUMERATION("spi_bus", "spi3_PB4_PB5_PB3", 3);
DECL_CONSTANT_STR("BUS_PINS_spi3_PB4_PB5_PB3", "PB4,PB5,PB3");
// Deprecated "spi1" style mappings
DECL_ENUMERATION("spi_bus", "spi2", 0);
DECL_CONSTANT_STR("BUS_PINS_spi2", "PB14,PB15,PB13");
DECL_ENUMERATION("spi_bus", "spi1", 1);
DECL_CONSTANT_STR("BUS_PINS_spi1", "PA6,PA7,PA5");
DECL_ENUMERATION("spi_bus", "spi1a", 2);
DECL_CONSTANT_STR("BUS_PINS_spi1a", "PB4,PB5,PB3");
DECL_ENUMERATION("spi_bus", "spi3", 3);
DECL_CONSTANT_STR("BUS_PINS_spi3", "PB4,PB5,PB3");
#elif CONFIG_MACH_STM32F2
DECL_ENUMERATION("spi_bus", "spi2_PB14_PB15_PB13", 0);
DECL_CONSTANT_STR("BUS_PINS_spi2_PB14_PB15_PB13", "PB14,PB15,PB13");
DECL_ENUMERATION("spi_bus", "spi1_PA6_PA7_PA5", 1);
DECL_CONSTANT_STR("BUS_PINS_spi1_PA6_PA7_PA5", "PA6,PA7,PA5");
DECL_ENUMERATION("spi_bus", "spi1_PB4_PB5_PB3", 2);
DECL_CONSTANT_STR("BUS_PINS_spi1_PB4_PB5_PB3", "PB4,PB5,PB3");
DECL_ENUMERATION("spi_bus", "spi2_PC2_PC3_PB10", 3);
DECL_CONSTANT_STR("BUS_PINS_spi2_PC2_PC3_PB10", "PC2,PC3,PB10");
DECL_ENUMERATION("spi_bus", "spi3_PB4_PB5_PB3", 4);
DECL_CONSTANT_STR("BUS_PINS_spi3_PB4_PB5_PB3", "PB4,PB5,PB3");
// Deprecated "spi1" style mappings
DECL_ENUMERATION("spi_bus", "spi2", 0);
DECL_CONSTANT_STR("BUS_PINS_spi2", "PB14,PB15,PB13");
DECL_ENUMERATION("spi_bus", "spi1", 1);
DECL_CONSTANT_STR("BUS_PINS_spi1", "PA6,PA7,PA5");
DECL_ENUMERATION("spi_bus", "spi1a", 2);
DECL_CONSTANT_STR("BUS_PINS_spi1a", "PB4,PB5,PB3");
DECL_ENUMERATION("spi_bus", "spi2a", 3);
DECL_CONSTANT_STR("BUS_PINS_spi2a", "PC2,PC3,PB10");
DECL_ENUMERATION("spi_bus", "spi3", 4);
DECL_CONSTANT_STR("BUS_PINS_spi3", "PB4,PB5,PB3");
#elif CONFIG_MACH_STM32F4
DECL_ENUMERATION("spi_bus", "spi2_PB14_PB15_PB13", 0);
DECL_CONSTANT_STR("BUS_PINS_spi2_PB14_PB15_PB13", "PB14,PB15,PB13");
DECL_ENUMERATION("spi_bus", "spi1_PA6_PA7_PA5", 1);
DECL_CONSTANT_STR("BUS_PINS_spi1_PA6_PA7_PA5", "PA6,PA7,PA5");
DECL_ENUMERATION("spi_bus", "spi1_PB4_PB5_PB3", 2);
DECL_CONSTANT_STR("BUS_PINS_spi1_PB4_PB5_PB3", "PB4,PB5,PB3");
DECL_ENUMERATION("spi_bus", "spi2_PC2_PC3_PB10", 3);
DECL_CONSTANT_STR("BUS_PINS_spi2_PC2_PC3_PB10", "PC2,PC3,PB10");
DECL_ENUMERATION("spi_bus", "spi3_PB4_PB5_PB3", 4);
DECL_CONSTANT_STR("BUS_PINS_spi3_PB4_PB5_PB3", "PB4,PB5,PB3");
DECL_ENUMERATION("spi_bus", "spi3_PC11_PC12_PC10", 5);
DECL_CONSTANT_STR("BUS_PINS_spi3_PC11_PC12_PC10", "PC11,PC12,PC10");
DECL_ENUMERATION("spi_bus", "spi2_PI2_PI3_PI1", 6);
DECL_CONSTANT_STR("BUS_PINS_spi2_PI2_PI3_PI1", "PI2,PI3,PI1");
#ifdef SPI4
DECL_ENUMERATION("spi_bus", "spi4_PE13_PE14_PE12", 7);
DECL_CONSTANT_STR("BUS_PINS_spi4_PE13_PE14_PE12", "PE13,PE14,PE12");
#define SPI6_INDEX (1 + 7)
#else
#define SPI6_INDEX (0 + 7)
#endif
#ifdef SPI6
DECL_ENUMERATION("spi_bus", "spi6_PG12_PG14_PG13", SPI6_INDEX);
DECL_CONSTANT_STR("BUS_PINS_spi6_PG12_PG14_PG13", "PG12,PG14,PG13");
#endif
// Deprecated "spi1" style mappings
DECL_ENUMERATION("spi_bus", "spi2", 0);
DECL_CONSTANT_STR("BUS_PINS_spi2", "PB14,PB15,PB13");
DECL_ENUMERATION("spi_bus", "spi1", 1);
DECL_CONSTANT_STR("BUS_PINS_spi1", "PA6,PA7,PA5");
DECL_ENUMERATION("spi_bus", "spi1a", 2);
DECL_CONSTANT_STR("BUS_PINS_spi1a", "PB4,PB5,PB3");
DECL_ENUMERATION("spi_bus", "spi2a", 3);
DECL_CONSTANT_STR("BUS_PINS_spi2a", "PC2,PC3,PB10");
DECL_ENUMERATION("spi_bus", "spi3", 4);
DECL_CONSTANT_STR("BUS_PINS_spi3", "PB4,PB5,PB3");
DECL_ENUMERATION("spi_bus", "spi3a", 5);
DECL_CONSTANT_STR("BUS_PINS_spi3a", "PC11,PC12,PC10");
DECL_ENUMERATION("spi_bus", "spi2b", 6);
DECL_CONSTANT_STR("BUS_PINS_spi2b", "PI2,PI3,PI1");
#ifdef SPI4
DECL_ENUMERATION("spi_bus", "spi4", 7);
DECL_CONSTANT_STR("BUS_PINS_spi4", "PE13,PE14,PE12");
#endif
#elif CONFIG_MACH_STM32F7
DECL_ENUMERATION("spi_bus", "spi2_PB14_PB15_PB13", 0);
DECL_CONSTANT_STR("BUS_PINS_spi2_PB14_PB15_PB13", "PB14,PB15,PB13");
DECL_ENUMERATION("spi_bus", "spi1_PA6_PA7_PA5", 1);
DECL_CONSTANT_STR("BUS_PINS_spi1_PA6_PA7_PA5", "PA6,PA7,PA5");
DECL_ENUMERATION("spi_bus", "spi1_PB4_PB5_PB3", 2);
DECL_CONSTANT_STR("BUS_PINS_spi1_PB4_PB5_PB3", "PB4,PB5,PB3");
DECL_ENUMERATION("spi_bus", "spi2_PC2_PC3_PB10", 3);
DECL_CONSTANT_STR("BUS_PINS_spi2_PC2_PC3_PB10", "PC2,PC3,PB10");
DECL_ENUMERATION("spi_bus", "spi3_PB4_PB5_PB3", 4);
DECL_CONSTANT_STR("BUS_PINS_spi3_PB4_PB5_PB3", "PB4,PB5,PB3");
// Deprecated "spi1" style mappings
DECL_ENUMERATION("spi_bus", "spi2", 0);
DECL_CONSTANT_STR("BUS_PINS_spi2", "PB14,PB15,PB13");
DECL_ENUMERATION("spi_bus", "spi1", 1);
DECL_CONSTANT_STR("BUS_PINS_spi1", "PA6,PA7,PA5");
DECL_ENUMERATION("spi_bus", "spi1a", 2);
DECL_CONSTANT_STR("BUS_PINS_spi1a", "PB4,PB5,PB3");
DECL_ENUMERATION("spi_bus", "spi2a", 3);
DECL_CONSTANT_STR("BUS_PINS_spi2a", "PC2,PC3,PB10");
DECL_ENUMERATION("spi_bus", "spi3", 4);
DECL_CONSTANT_STR("BUS_PINS_spi3", "PB4,PB5,PB3");
#elif CONFIG_MACH_STM32G0
DECL_ENUMERATION("spi_bus", "spi2_PB14_PB15_PB13", 0);
DECL_CONSTANT_STR("BUS_PINS_spi2_PB14_PB15_PB13", "PB14,PB15,PB13");
DECL_ENUMERATION("spi_bus", "spi1_PA6_PA7_PA5", 1);
DECL_CONSTANT_STR("BUS_PINS_spi1_PA6_PA7_PA5", "PA6,PA7,PA5");
DECL_ENUMERATION("spi_bus", "spi1_PB4_PB5_PB3", 2);
DECL_CONSTANT_STR("BUS_PINS_spi1_PB4_PB5_PB3", "PB4,PB5,PB3");
DECL_ENUMERATION("spi_bus", "spi2_PC2_PC3_PB10", 3);
DECL_CONSTANT_STR("BUS_PINS_spi2_PC2_PC3_PB10", "PC2,PC3,PB10");
DECL_ENUMERATION("spi_bus", "spi2_PB2_PB11_PB10", 4);
DECL_CONSTANT_STR("BUS_PINS_spi2_PB2_PB11_PB10", "PB2,PB11,PB10");
DECL_ENUMERATION("spi_bus", "spi2_PB6_PB7_PB8", 5);
DECL_CONSTANT_STR("BUS_PINS_spi2_PB6_PB7_PB8", "PB6,PB7,PB8");
#ifdef SPI3
DECL_ENUMERATION("spi_bus", "spi3_PB4_PB5_PB3", 6);
DECL_CONSTANT_STR("BUS_PINS_spi3_PB4_PB5_PB3", "PB4,PB5,PB3");
DECL_ENUMERATION("spi_bus", "spi3_PC11_PC12_PC10", 7);
DECL_CONSTANT_STR("BUS_PINS_spi3_PC11_PC12_PC10", "PC11,PC12,PC10");
#endif
// Deprecated "spi1" style mappings
DECL_ENUMERATION("spi_bus", "spi2", 0);
DECL_CONSTANT_STR("BUS_PINS_spi2", "PB14,PB15,PB13");
DECL_ENUMERATION("spi_bus", "spi1", 1);
DECL_CONSTANT_STR("BUS_PINS_spi1", "PA6,PA7,PA5");
DECL_ENUMERATION("spi_bus", "spi1a", 2);
DECL_CONSTANT_STR("BUS_PINS_spi1a", "PB4,PB5,PB3");
DECL_ENUMERATION("spi_bus", "spi2a", 3);
DECL_CONSTANT_STR("BUS_PINS_spi2a", "PC2,PC3,PB10");
#ifdef SPI3
DECL_ENUMERATION("spi_bus", "spi3", 6);
DECL_CONSTANT_STR("BUS_PINS_spi3", "PB4,PB5,PB3");
#endif
#elif CONFIG_MACH_STM32G4
DECL_ENUMERATION("spi_bus", "spi2_PB14_PB15_PB13", 0);
DECL_CONSTANT_STR("BUS_PINS_spi2_PB14_PB15_PB13", "PB14,PB15,PB13");
DECL_ENUMERATION("spi_bus", "spi1_PA6_PA7_PA5", 1);
DECL_CONSTANT_STR("BUS_PINS_spi1_PA6_PA7_PA5", "PA6,PA7,PA5");
DECL_ENUMERATION("spi_bus", "spi1_PB4_PB5_PB3", 2);
DECL_CONSTANT_STR("BUS_PINS_spi1_PB4_PB5_PB3", "PB4,PB5,PB3");
DECL_ENUMERATION("spi_bus", "spi2_PA10_PA11_PF1", 3);
DECL_CONSTANT_STR("BUS_PINS_spi2_PA10_PA11_PF1", "PA10,PA11,PF1");
DECL_ENUMERATION("spi_bus", "spi3_PB4_PB5_PB3", 4);
DECL_CONSTANT_STR("BUS_PINS_spi3_PB4_PB5_PB3", "PB4,PB5,PB3");
DECL_ENUMERATION("spi_bus", "spi3_PC11_PC12_PC10", 5);
DECL_CONSTANT_STR("BUS_PINS_spi3_PC11_PC12_PC10", "PC11,PC12,PC10");
#ifdef SPI4
DECL_ENUMERATION("spi_bus", "spi4_PE13_PE14_PE12", 6);
DECL_CONSTANT_STR("BUS_PINS_spi4_PE13_PE14_PE12", "PE13,PE14,PE12");
#endif
// Deprecated "spi1" style mappings
DECL_ENUMERATION("spi_bus", "spi2", 0);
DECL_CONSTANT_STR("BUS_PINS_spi2", "PB14,PB15,PB13");
DECL_ENUMERATION("spi_bus", "spi1", 1);
DECL_CONSTANT_STR("BUS_PINS_spi1", "PA6,PA7,PA5");
DECL_ENUMERATION("spi_bus", "spi1a", 2);
DECL_CONSTANT_STR("BUS_PINS_spi1a", "PB4,PB5,PB3");
DECL_ENUMERATION("spi_bus", "spi3", 4);
DECL_CONSTANT_STR("BUS_PINS_spi3", "PB4,PB5,PB3");
DECL_ENUMERATION("spi_bus", "spi3a", 5);
DECL_CONSTANT_STR("BUS_PINS_spi3a", "PC11,PC12,PC10");
#ifdef SPI4
DECL_ENUMERATION("spi_bus", "spi4", 6);
DECL_CONSTANT_STR("BUS_PINS_spi4", "PE13,PE14,PE12");
#elif defined(GPIOI)
DECL_ENUMERATION("spi_bus", "spi2b", 6);
DECL_CONSTANT_STR("BUS_PINS_spi2b", "PI2,PI3,PI1");
DECL_ENUMERATION("spi_bus", "spi4", 6);
DECL_CONSTANT_STR("BUS_PINS_spi4", "PE13,PE14,PE12");
#endif
#ifdef SPI6
DECL_ENUMERATION("spi_bus", "spi6_PG12_PG14_PG13", 7);
DECL_CONSTANT_STR("BUS_PINS_spi6_PG12_PG14_PG13", "PG12,PG14,PG13");
#endif
#endif
#if CONFIG_MACH_STM32G0B1
DECL_ENUMERATION("spi_bus", "spi2_PB2_PB11_PB10", 5);
DECL_CONSTANT_STR("BUS_PINS_spi2_PB2_PB11_PB10", "PB2,PB11,PB10");
#endif
#elif CONFIG_MACH_STM32L4
DECL_ENUMERATION("spi_bus", "spi2_PB14_PB15_PB13", 0);
DECL_CONSTANT_STR("BUS_PINS_spi2_PB14_PB15_PB13", "PB14,PB15,PB13");
DECL_ENUMERATION("spi_bus", "spi1_PA6_PA7_PA5", 1);
DECL_CONSTANT_STR("BUS_PINS_spi1_PA6_PA7_PA5", "PA6,PA7,PA5");
DECL_ENUMERATION("spi_bus", "spi1_PB4_PB5_PB3", 2);
DECL_CONSTANT_STR("BUS_PINS_spi1_PB4_PB5_PB3", "PB4,PB5,PB3");
DECL_ENUMERATION("spi_bus", "spi2_PC2_PC3_PB10", 3);
DECL_CONSTANT_STR("BUS_PINS_spi2_PC2_PC3_PB10", "PC2,PC3,PB10");
// Deprecated "spi1" style mappings
DECL_ENUMERATION("spi_bus", "spi2", 0);
DECL_CONSTANT_STR("BUS_PINS_spi2", "PB14,PB15,PB13");
DECL_ENUMERATION("spi_bus", "spi1", 1);
DECL_CONSTANT_STR("BUS_PINS_spi1", "PA6,PA7,PA5");
DECL_ENUMERATION("spi_bus", "spi1a", 2);
DECL_CONSTANT_STR("BUS_PINS_spi1a", "PB4,PB5,PB3");
DECL_ENUMERATION("spi_bus", "spi2a", 3);
DECL_CONSTANT_STR("BUS_PINS_spi2a", "PC2,PC3,PB10");
#endif
#define GPIO_FUNCTION_ALL(fn) GPIO_FUNCTION(fn), \
GPIO_FUNCTION(fn), GPIO_FUNCTION(fn)
#if CONFIG_MACH_STM32F0 || CONFIG_MACH_STM32G0
#define SPI_FUNCTION_ALL GPIO_FUNCTION_ALL(0)
#else
#define SPI_FUNCTION_ALL GPIO_FUNCTION_ALL(5)
#endif
#define SPI_FUNCTION(miso, mosi, sck) GPIO_FUNCTION(miso), \
GPIO_FUNCTION(mosi), GPIO_FUNCTION(sck)
static const struct spi_info spi_bus[] = {
{ SPI2, GPIO('B', 14), GPIO('B', 15), GPIO('B', 13), SPI_FUNCTION_ALL },
{ SPI1, GPIO('A', 6), GPIO('A', 7), GPIO('A', 5), SPI_FUNCTION_ALL },
{ SPI1, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), SPI_FUNCTION_ALL },
#if CONFIG_MACH_STM32G4
{ SPI2, GPIO('A', 10), GPIO('A', 11), GPIO('F', 1), SPI_FUNCTION_ALL },
#else
{ SPI2, GPIO('C', 2), GPIO('C', 3), GPIO('B', 10), SPI_FUNCTION_ALL },
#endif
#ifdef SPI3
{ SPI3, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), GPIO_FUNCTION_ALL(6) },
#if CONFIG_MACH_STM32F4 || CONFIG_MACH_STM32G4
{ SPI3, GPIO('C', 11), GPIO('C', 12), GPIO('C', 10), GPIO_FUNCTION_ALL(6) },
#ifdef SPI4
{ SPI4, GPIO('E', 13), GPIO('E', 14), GPIO('E', 12), GPIO_FUNCTION_ALL(5) },
#elif defined(GPIOI)
{ SPI2, GPIO('I', 2), GPIO('I', 3), GPIO('I', 1), GPIO_FUNCTION_ALL(5) },
#endif
#ifdef SPI6
{ SPI6, GPIO('G', 12), GPIO('G', 14), GPIO('G', 13), SPI_FUNCTION_ALL}
#endif
#if CONFIG_MACH_STM32F0
{ SPI2, GPIO('B', 14), GPIO('B', 15), GPIO('B', 13), SPI_FUNCTION(0, 0, 0) },
{ SPI1, GPIO('A', 6), GPIO('A', 7), GPIO('A', 5), SPI_FUNCTION(0, 0, 0) },
{ SPI1, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), SPI_FUNCTION(0, 0, 0) },
{ SPI2, GPIO('C', 2), GPIO('C', 3), GPIO('B', 10), SPI_FUNCTION(1, 1, 5) },
#elif CONFIG_MACH_STM32F1
{ SPI2, GPIO('B', 14), GPIO('B', 15), GPIO('B', 13), SPI_FUNCTION(0, 0, 0) },
{ SPI1, GPIO('A', 6), GPIO('A', 7), GPIO('A', 5), SPI_FUNCTION(0, 0, 0) },
{ SPI1, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), SPI_FUNCTION(5, 5, 5) },
{ SPI3, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), SPI_FUNCTION(0, 0, 0) },
#elif CONFIG_MACH_STM32F2
{ SPI2, GPIO('B', 14), GPIO('B', 15), GPIO('B', 13), SPI_FUNCTION(5, 5, 5) },
{ SPI1, GPIO('A', 6), GPIO('A', 7), GPIO('A', 5), SPI_FUNCTION(5, 5, 5) },
{ SPI1, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), SPI_FUNCTION(5, 5, 5) },
{ SPI2, GPIO('C', 2), GPIO('C', 3), GPIO('B', 10), SPI_FUNCTION(5, 5, 5) },
{ SPI3, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), SPI_FUNCTION(6, 6, 6) },
#elif CONFIG_MACH_STM32F4
{ SPI2, GPIO('B', 14), GPIO('B', 15), GPIO('B', 13), SPI_FUNCTION(5, 5, 5) },
{ SPI1, GPIO('A', 6), GPIO('A', 7), GPIO('A', 5), SPI_FUNCTION(5, 5, 5) },
{ SPI1, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), SPI_FUNCTION(5, 5, 5) },
{ SPI2, GPIO('C', 2), GPIO('C', 3), GPIO('B', 10), SPI_FUNCTION(5, 5, 5) },
{ SPI3, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), SPI_FUNCTION(6, 6, 6) },
{ SPI3, GPIO('C', 11), GPIO('C', 12), GPIO('C', 10), SPI_FUNCTION(6, 6, 6) },
{ SPI2, GPIO('I', 2), GPIO('I', 3), GPIO('I', 1), SPI_FUNCTION(5, 5, 5) },
#ifdef SPI4
{ SPI4, GPIO('E', 13), GPIO('E', 14), GPIO('E', 12), SPI_FUNCTION(5, 5, 5) },
#endif
#if CONFIG_MACH_STM32G0B1
{ SPI2, GPIO('B', 2), GPIO('B', 11), GPIO('B', 10),
GPIO_FUNCTION(1), GPIO_FUNCTION(0), GPIO_FUNCTION(5) },
#ifdef SPI6
{ SPI6, GPIO('G', 12), GPIO('G', 14), GPIO('G', 13), SPI_FUNCTION(5, 5, 5)},
#endif
#elif CONFIG_MACH_STM32F7
{ SPI2, GPIO('B', 14), GPIO('B', 15), GPIO('B', 13), SPI_FUNCTION(5, 5, 5) },
{ SPI1, GPIO('A', 6), GPIO('A', 7), GPIO('A', 5), SPI_FUNCTION(5, 5, 5) },
{ SPI1, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), SPI_FUNCTION(5, 5, 5) },
{ SPI2, GPIO('C', 2), GPIO('C', 3), GPIO('B', 10), SPI_FUNCTION(5, 5, 5) },
{ SPI3, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), SPI_FUNCTION(6, 6, 6) },
#elif CONFIG_MACH_STM32G0
{ SPI2, GPIO('B', 14), GPIO('B', 15), GPIO('B', 13), SPI_FUNCTION(0, 0, 0) },
{ SPI1, GPIO('A', 6), GPIO('A', 7), GPIO('A', 5), SPI_FUNCTION(0, 0, 0) },
{ SPI1, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), SPI_FUNCTION(0, 0, 0) },
{ SPI2, GPIO('C', 2), GPIO('C', 3), GPIO('B', 10), SPI_FUNCTION(1, 1, 5) },
{ SPI2, GPIO('B', 2), GPIO('B', 11), GPIO('B', 10), SPI_FUNCTION(1, 0, 5) },
{ SPI2, GPIO('B', 6), GPIO('B', 7), GPIO('B', 8), SPI_FUNCTION(4, 1, 1) },
#ifdef SPI3
{ SPI3, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), SPI_FUNCTION(9, 9, 9) },
{ SPI3, GPIO('C', 11), GPIO('C', 12), GPIO('C', 10), SPI_FUNCTION(4, 4, 4) },
#endif
#elif CONFIG_MACH_STM32G4
{ SPI2, GPIO('B', 14), GPIO('B', 15), GPIO('B', 13), SPI_FUNCTION(5, 5, 5) },
{ SPI1, GPIO('A', 6), GPIO('A', 7), GPIO('A', 5), SPI_FUNCTION(5, 5, 5) },
{ SPI1, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), SPI_FUNCTION(5, 5, 5) },
{ SPI2, GPIO('A', 10), GPIO('A', 11), GPIO('F', 1), SPI_FUNCTION(5, 5, 5) },
{ SPI3, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), SPI_FUNCTION(6, 6, 6) },
{ SPI3, GPIO('C', 11), GPIO('C', 12), GPIO('C', 10), SPI_FUNCTION(6, 6, 6) },
#ifdef SPI4
{ SPI4, GPIO('E', 13), GPIO('E', 14), GPIO('E', 12), SPI_FUNCTION(5, 5, 5) },
#endif
#elif CONFIG_MACH_STM32L4
{ SPI2, GPIO('B', 14), GPIO('B', 15), GPIO('B', 13), SPI_FUNCTION(5, 5, 5) },
{ SPI1, GPIO('A', 6), GPIO('A', 7), GPIO('A', 5), SPI_FUNCTION(5, 5, 5) },
{ SPI1, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), SPI_FUNCTION(5, 5, 5) },
{ SPI2, GPIO('C', 2), GPIO('C', 3), GPIO('B', 10), SPI_FUNCTION(5, 5, 5) },
#endif
};

View file

@ -115,7 +115,7 @@ stm32f1_alternative_remap(uint32_t mapr_mask, uint32_t mapr_value)
void
gpio_peripheral(uint32_t gpio, uint32_t mode, int pullup)
{
GPIO_TypeDef *regs = digital_regs[GPIO2PORT(gpio)];
GPIO_TypeDef *regs = gpio_pin_to_regs(gpio);
// Enable GPIO clock
gpio_clock_enable(regs);

View file

@ -33,7 +33,7 @@ DECL_ENUMERATION_RANGE("pin", "PH0", GPIO('H', 0), 16);
DECL_ENUMERATION_RANGE("pin", "PI0", GPIO('I', 0), 16);
#endif
GPIO_TypeDef * const digital_regs[] = {
static GPIO_TypeDef * const digital_regs[] = {
['A' - 'A'] = GPIOA, GPIOB, GPIOC,
#ifdef GPIOD
['D' - 'A'] = GPIOD,
@ -66,12 +66,14 @@ regs_to_pin(GPIO_TypeDef *regs, uint32_t bit)
return 0;
}
// Verify that a gpio is a valid pin
static int
gpio_valid(uint32_t pin)
// Verify that a gpio is a valid pin and return its hardware register
GPIO_TypeDef *
gpio_pin_to_regs(uint32_t pin)
{
uint32_t port = GPIO2PORT(pin);
return port < ARRAY_SIZE(digital_regs) && digital_regs[port];
if (port >= ARRAY_SIZE(digital_regs) || !digital_regs[port])
shutdown("Not a valid pin");
return digital_regs[port];
}
// The stm32h7 has very slow read access to the gpio registers. Cache
@ -83,10 +85,7 @@ static struct odr_cache {
struct gpio_out
gpio_out_setup(uint32_t pin, uint32_t val)
{
if (!gpio_valid(pin))
shutdown("Not an output pin");
uint32_t port = GPIO2PORT(pin);
GPIO_TypeDef *regs = digital_regs[port];
GPIO_TypeDef *regs = gpio_pin_to_regs(pin);
gpio_clock_enable(regs);
struct gpio_out g = { .regs=regs, .oc=&ODR_CACHE[pin] };
@ -141,9 +140,7 @@ gpio_out_write(struct gpio_out g, uint32_t val)
struct gpio_in
gpio_in_setup(uint32_t pin, int32_t pull_up)
{
if (!gpio_valid(pin))
shutdown("Not a valid input pin");
GPIO_TypeDef *regs = digital_regs[GPIO2PORT(pin)];
GPIO_TypeDef *regs = gpio_pin_to_regs(pin);
struct gpio_in g = { .regs=regs, .bit=GPIO2BIT(pin) };
gpio_in_reset(g, pull_up);
return g;

View file

@ -16,68 +16,38 @@ struct spi_info {
uint8_t miso_pin, mosi_pin, sck_pin, function;
};
DECL_ENUMERATION("spi_bus", "spi2", __COUNTER__);
DECL_ENUMERATION("spi_bus", "spi2", 0);
DECL_CONSTANT_STR("BUS_PINS_spi2", "PB14,PB15,PB13");
DECL_ENUMERATION("spi_bus", "spi1", __COUNTER__);
DECL_ENUMERATION("spi_bus", "spi1", 1);
DECL_CONSTANT_STR("BUS_PINS_spi1", "PA6,PA7,PA5");
DECL_ENUMERATION("spi_bus", "spi1a", __COUNTER__);
DECL_ENUMERATION("spi_bus", "spi1a", 2);
DECL_CONSTANT_STR("BUS_PINS_spi1a", "PB4,PB5,PB3");
#if !CONFIG_MACH_STM32F1
DECL_ENUMERATION("spi_bus", "spi2a", __COUNTER__);
DECL_ENUMERATION("spi_bus", "spi2a", 3);
DECL_CONSTANT_STR("BUS_PINS_spi2a", "PC2,PC3,PB10");
#endif
#ifdef SPI3
DECL_ENUMERATION("spi_bus", "spi3a", __COUNTER__);
DECL_ENUMERATION("spi_bus", "spi3a", 4);
DECL_CONSTANT_STR("BUS_PINS_spi3a", "PC11,PC12,PC10");
#endif
#ifdef SPI4
DECL_ENUMERATION("spi_bus", "spi4", __COUNTER__);
DECL_ENUMERATION("spi_bus", "spi4", 5);
DECL_CONSTANT_STR("BUS_PINS_spi4", "PE13,PE14,PE12");
#endif
#ifdef GPIOI
DECL_ENUMERATION("spi_bus", "spi2b", __COUNTER__);
DECL_CONSTANT_STR("BUS_PINS_spi2b", "PI2,PI3,PI1");
#endif
#ifdef SPI5
DECL_ENUMERATION("spi_bus", "spi5", __COUNTER__);
DECL_ENUMERATION("spi_bus", "spi5", 6);
DECL_CONSTANT_STR("BUS_PINS_spi5", "PF8,PF9,PF7");
DECL_ENUMERATION("spi_bus", "spi5a", __COUNTER__);
DECL_ENUMERATION("spi_bus", "spi5a", 7);
DECL_CONSTANT_STR("BUS_PINS_spi5a", "PH7,PF11,PH6");
#endif
#ifdef SPI6
DECL_ENUMERATION("spi_bus", "spi6", __COUNTER__);
DECL_ENUMERATION("spi_bus", "spi6", 8);
DECL_CONSTANT_STR("BUS_PINS_spi6", "PG12,PG14,PG13");
#endif
DECL_ENUMERATION("spi_bus", "spi2b", 9);
DECL_CONSTANT_STR("BUS_PINS_spi2b", "PI2,PI3,PI1");
static const struct spi_info spi_bus[] = {
{ SPI2, GPIO('B', 14), GPIO('B', 15), GPIO('B', 13), GPIO_FUNCTION(5) },
{ SPI1, GPIO('A', 6), GPIO('A', 7), GPIO('A', 5), GPIO_FUNCTION(5) },
{ SPI1, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), GPIO_FUNCTION(5) },
#if !CONFIG_MACH_STM32F1
{ SPI2, GPIO('C', 2), GPIO('C', 3), GPIO('B', 10), GPIO_FUNCTION(5) },
#endif
#ifdef SPI3
{ SPI3, GPIO('C', 11), GPIO('C', 12), GPIO('C', 10), GPIO_FUNCTION(6) },
#endif
#ifdef SPI4
{ SPI4, GPIO('E', 13), GPIO('E', 14), GPIO('E', 12), GPIO_FUNCTION(5) },
#endif
{ SPI2, GPIO('I', 2), GPIO('I', 3), GPIO('I', 1), GPIO_FUNCTION(5) },
#ifdef SPI5
{ SPI5, GPIO('F', 8), GPIO('F', 9), GPIO('F', 7), GPIO_FUNCTION(5) },
{ SPI5, GPIO('H', 7), GPIO('F', 11), GPIO('H', 6), GPIO_FUNCTION(5) },
#endif
#ifdef SPI6
{ SPI6, GPIO('G', 12), GPIO('G', 14), GPIO('G', 13), GPIO_FUNCTION(5)},
#endif
{ SPI2, GPIO('I', 2), GPIO('I', 3), GPIO('I', 1), GPIO_FUNCTION(5) },
};
struct spi_config

View file

@ -66,3 +66,19 @@ max_velocity: 300
max_accel: 3000
max_z_velocity: 5
max_z_accel: 100
[filament_switch_sensor runout_switch]
switch_pin = PD4
[filament_motion_sensor runout_encoder]
switch_pin = PD5
detection_length = 4
extruder = extruder
[filament_switch_sensor runout_switch1]
switch_pin = PL4
[filament_motion_sensor runout_encoder1]
switch_pin = PL6
detection_length = 4
extruder = extruder_stepper my_extra_stepper

View file

@ -70,8 +70,10 @@ max_z_accel: 100
[input_shaper]
shaper_type_x: mzv
shaper_freq_x: 33.2
shaper_type_x: ei
shaper_freq_x: 39.3
shaper_type_y: ei
shaper_freq_y: 39.3
damping_ratio_y: 0.4
shaper_freq_z: 42
[adxl345]
cs_pin: PK7
@ -83,3 +85,4 @@ axes_map: -x,-y,z
probe_points: 20,20,20
accel_chip_x: adxl345
accel_chip_y: mpu9250 my_mpu
accel_chip_z: adxl345

View file

@ -4,4 +4,4 @@ DICTIONARY atmega2560.dict
# Simple command test
SET_INPUT_SHAPER SHAPER_FREQ_X=22.2 DAMPING_RATIO_X=.1 SHAPER_TYPE_X=zv
SET_INPUT_SHAPER SHAPER_FREQ_Y=33.3 DAMPING_RATIO_X=.11 SHAPER_TYPE_X=2hump_ei
SET_INPUT_SHAPER SHAPER_FREQ_Y=33.3 DAMPING_RATIO_Y=.11 SHAPER_TYPE_Y=2hump_ei

View file

@ -33,6 +33,10 @@ G28
G1 X20 Y20 Z10
G1 A10 X22
# Verify position query commands work with extra axis
GET_POSITION
M114
# Test unregistering
MANUAL_STEPPER STEPPER=basic_stepper GCODE_AXIS=
G1 X15