diff --git a/config/generic-bigtreetech-manta-e3ez.cfg b/config/generic-bigtreetech-manta-e3ez.cfg index 199eae708..39f12fd2a 100644 --- a/config/generic-bigtreetech-manta-e3ez.cfg +++ b/config/generic-bigtreetech-manta-e3ez.cfg @@ -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 diff --git a/config/sample-bigtreetech-ebb-canbus-v1.1.cfg b/config/sample-bigtreetech-ebb-canbus-v1.1.cfg index c84abf173..8da7cccd4 100644 --- a/config/sample-bigtreetech-ebb-canbus-v1.1.cfg +++ b/config/sample-bigtreetech-ebb-canbus-v1.1.cfg @@ -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] diff --git a/config/sample-bigtreetech-ebb-canbus-v1.2.cfg b/config/sample-bigtreetech-ebb-canbus-v1.2.cfg index 053b783c3..7b55a91cf 100644 --- a/config/sample-bigtreetech-ebb-canbus-v1.2.cfg +++ b/config/sample-bigtreetech-ebb-canbus-v1.2.cfg @@ -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] diff --git a/config/sample-bigtreetech-ebb-sb-canbus-v1.0.cfg b/config/sample-bigtreetech-ebb-sb-canbus-v1.0.cfg index 192d385e5..1e60467dd 100644 --- a/config/sample-bigtreetech-ebb-sb-canbus-v1.0.cfg +++ b/config/sample-bigtreetech-ebb-sb-canbus-v1.0.cfg @@ -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] diff --git a/docs/Code_Overview.md b/docs/Code_Overview.md index fd0e90a38..50666da04 100644 --- a/docs/Code_Overview.md +++ b/docs/Code_Overview.md @@ -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 diff --git a/docs/Config_Changes.md b/docs/Config_Changes.md index 838b23273..cb43d9564 100644 --- a/docs/Config_Changes.md +++ b/docs/Config_Changes.md @@ -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 diff --git a/docs/Config_Reference.md b/docs/Config_Reference.md index 83de96096..94d3a9730 100644 --- a/docs/Config_Reference.md +++ b/docs/Config_Reference.md @@ -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: diff --git a/docs/FAQ.md b/docs/FAQ.md index 417fb1d4f..dbea920f0 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -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). diff --git a/docs/G-Codes.md b/docs/G-Codes.md index 7a60aa8a0..eb38e0134 100644 --- a/docs/G-Codes.md +++ b/docs/G-Codes.md @@ -493,7 +493,7 @@ enabled. `SET_FAN_SPEED FAN=config_name 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= +`SET_FAN_SPEED FAN=config_name TEMPLATE= [=]`: 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_Y=] [DAMPING_RATIO_X=] -[DAMPING_RATIO_Y=] [SHAPER_TYPE=] -[SHAPER_TYPE_X=] [SHAPER_TYPE_Y=]`: +[SHAPER_FREQ_Y=] [SHAPER_FREQ_Y=] +[DAMPING_RATIO_X=] [DAMPING_RATIO_Y=] +[DAMPING_RATIO_Z=] [SHAPER_TYPE=] +[SHAPER_TYPE_X=] [SHAPER_TYPE_Y=] +[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 diff --git a/docs/Measuring_Resonances.md b/docs/Measuring_Resonances.md index 49c050e0c..fe140d93d 100644 --- a/docs/Measuring_Resonances.md +++ b/docs/Measuring_Resonances.md @@ -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: +``` +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 diff --git a/docs/Resonance_Compensation.md b/docs/Resonance_Compensation.md index 8f2d2b643..dcfb4ed78 100644 --- a/docs/Resonance_Compensation.md +++ b/docs/Resonance_Compensation.md @@ -439,9 +439,12 @@ gcode: SET_INPUT_SHAPER SHAPER_TYPE_X= SHAPER_FREQ_X= SHAPER_TYPE_Y= SHAPER_FREQ_Y= ``` 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
shaper | Shaper
duration | Vibration reduction 20x
(5% vibration tolerance) | Vibration reduction 10x
(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 diff --git a/docs/img/calibrate-z.png b/docs/img/calibrate-z.png new file mode 100644 index 000000000..b75a65c33 Binary files /dev/null and b/docs/img/calibrate-z.png differ diff --git a/klippy/chelper/__init__.py b/klippy/chelper/__init__.py index 60ba91e7d..c26196e66 100644 --- a/klippy/chelper/__init__.py +++ b/klippy/chelper/__init__.py @@ -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 diff --git a/klippy/chelper/itersolve.c b/klippy/chelper/itersolve.c index eba1deef4..5b1683606 100644 --- a/klippy/chelper/itersolve.c +++ b/klippy/chelper/itersolve.c @@ -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; +} diff --git a/klippy/chelper/itersolve.h b/klippy/chelper/itersolve.h index e2e46ebe3..50a30f7da 100644 --- a/klippy/chelper/itersolve.h +++ b/klippy/chelper/itersolve.h @@ -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 diff --git a/klippy/chelper/kin_shaper.c b/klippy/chelper/kin_shaper.c index 42d572d02..682d7fa49 100644 --- a/klippy/chelper/kin_shaper.c +++ b/klippy/chelper/kin_shaper.c @@ -1,7 +1,7 @@ // Kinematic input shapers to minimize motion vibrations in XY plane // // Copyright (C) 2019-2020 Kevin O'Connor -// Copyright (C) 2020 Dmitry Butyugin +// Copyright (C) 2020-2025 Dmitry Butyugin // // 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) { diff --git a/klippy/chelper/list.h b/klippy/chelper/list.h index 12fe2b038..f38fca6cc 100644 --- a/klippy/chelper/list.h +++ b/klippy/chelper/list.h @@ -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) \ diff --git a/klippy/chelper/msgblock.c b/klippy/chelper/msgblock.c index e6cb298b4..a7385c893 100644 --- a/klippy/chelper/msgblock.c +++ b/klippy/chelper/msgblock.c @@ -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; +} diff --git a/klippy/chelper/msgblock.h b/klippy/chelper/msgblock.h index 43ee95325..9bb066313 100644 --- a/klippy/chelper/msgblock.h +++ b/klippy/chelper/msgblock.h @@ -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 diff --git a/klippy/chelper/pyhelper.c b/klippy/chelper/pyhelper.c index 60c6de9b3..8d4e4ee8c 100644 --- a/klippy/chelper/pyhelper.c +++ b/klippy/chelper/pyhelper.c @@ -10,7 +10,6 @@ #include // fprintf #include // strerror #include // struct timespec -#include // PR_SET_NAME #include // prctl #include "compiler.h" // __visible #include "pyhelper.h" // get_monotonic diff --git a/klippy/chelper/serialqueue.c b/klippy/chelper/serialqueue.c index ed1215df9..914d4c395 100644 --- a/klippy/chelper/serialqueue.c +++ b/klippy/chelper/serialqueue.c @@ -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); } diff --git a/klippy/chelper/stepcompress.c b/klippy/chelper/stepcompress.c index 2889570dc..141c8ac35 100644 --- a/klippy/chelper/stepcompress.c +++ b/klippy/chelper/stepcompress.c @@ -21,7 +21,6 @@ #include // malloc #include // 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); -} diff --git a/klippy/chelper/stepcompress.h b/klippy/chelper/stepcompress.h index e21c4fd96..affc1e23c 100644 --- a/klippy/chelper/stepcompress.h +++ b/klippy/chelper/stepcompress.h @@ -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 diff --git a/klippy/chelper/steppersync.c b/klippy/chelper/steppersync.c index 745578c75..6b571f4b6 100644 --- a/klippy/chelper/steppersync.c +++ b/klippy/chelper/steppersync.c @@ -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_mutex_lock #include // offsetof #include // malloc #include // 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; isc_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; isc_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; isc_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; } diff --git a/klippy/chelper/steppersync.h b/klippy/chelper/steppersync.h index 1320bbaa0..7ad6fcae6 100644 --- a/klippy/chelper/steppersync.h +++ b/klippy/chelper/steppersync.h @@ -3,16 +3,30 @@ #include // 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 diff --git a/klippy/chelper/trapq.c b/klippy/chelper/trapq.c index b9930e997..a2a5f37f3 100644 --- a/klippy/chelper/trapq.c +++ b/klippy/chelper/trapq.c @@ -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++; } diff --git a/klippy/extras/bus.py b/klippy/extras/bus.py index 9fb466390..676ff3b93 100644 --- a/klippy/extras/bus.py +++ b/klippy/extras/bus.py @@ -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) diff --git a/klippy/extras/display/display.py b/klippy/extras/display/display.py index e9ba31d6d..cc33bc154 100644 --- a/klippy/extras/display/display.py +++ b/klippy/extras/display/display.py @@ -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: diff --git a/klippy/extras/display/uc1701.py b/klippy/extras/display/uc1701.py index 8e877cf2e..85b74decd 100644 --- a/klippy/extras/display/uc1701.py +++ b/klippy/extras/display/uc1701.py @@ -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: diff --git a/klippy/extras/extruder_stepper.py b/klippy/extras/extruder_stepper.py index 4ac5289f8..28293a3c0 100644 --- a/klippy/extras/extruder_stepper.py +++ b/klippy/extras/extruder_stepper.py @@ -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) diff --git a/klippy/extras/force_move.py b/klippy/extras/force_move.py index 00f835f5b..277c68e3c 100644 --- a/klippy/extras/force_move.py +++ b/klippy/extras/force_move.py @@ -1,6 +1,6 @@ # Utility for manually moving a stepper for diagnostic purposes # -# Copyright (C) 2018-2019 Kevin O'Connor +# Copyright (C) 2018-2025 Kevin O'Connor # # 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) diff --git a/klippy/extras/gcode_move.py b/klippy/extras/gcode_move.py index 94a0ce422..655b8108f 100644 --- a/klippy/extras/gcode_move.py +++ b/klippy/extras/gcode_move.py @@ -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: diff --git a/klippy/extras/input_shaper.py b/klippy/extras/input_shaper.py index 67a287cdd..39b3f5a85 100644 --- a/klippy/extras/input_shaper.py +++ b/klippy/extras/input_shaper.py @@ -1,7 +1,7 @@ # Kinematic input shaper to minimize motion vibrations in XY plane # # Copyright (C) 2019-2020 Kevin O'Connor -# Copyright (C) 2020 Dmitry Butyugin +# Copyright (C) 2020-2025 Dmitry Butyugin # # 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) diff --git a/klippy/extras/manual_stepper.py b/klippy/extras/manual_stepper.py index 3d3e614a6..8a8911e18 100644 --- a/klippy/extras/manual_stepper.py +++ b/klippy/extras/manual_stepper.py @@ -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() diff --git a/klippy/extras/motion_queuing.py b/klippy/extras/motion_queuing.py index a06b556e1..20473503c 100644 --- a/klippy/extras/motion_queuing.py +++ b/klippy/extras/motion_queuing.py @@ -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) diff --git a/klippy/extras/output_pin.py b/klippy/extras/output_pin.py index 63862d978..a51292990 100644 --- a/klippy/extras/output_pin.py +++ b/klippy/extras/output_pin.py @@ -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))) diff --git a/klippy/extras/pca9533.py b/klippy/extras/pca9533.py index a94e1334e..20f1a6ca9 100644 --- a/klippy/extras/pca9533.py +++ b/klippy/extras/pca9533.py @@ -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) diff --git a/klippy/extras/pca9632.py b/klippy/extras/pca9632.py index b8a813c33..099676e0f 100644 --- a/klippy/extras/pca9632.py +++ b/klippy/extras/pca9632.py @@ -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) diff --git a/klippy/extras/pwm_tool.py b/klippy/extras/pwm_tool.py index 6d401c0b0..a069dd2d8 100644 --- a/klippy/extras/pwm_tool.py +++ b/klippy/extras/pwm_tool.py @@ -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: diff --git a/klippy/extras/resonance_tester.py b/klippy/extras/resonance_tester.py index c6171dc42..b92a163bc 100644 --- a/klippy/extras/resonance_tester.py +++ b/klippy/extras/resonance_tester.py @@ -1,19 +1,23 @@ # A utility class to test resonances of the printer # -# Copyright (C) 2020-2024 Dmitry Butyugin +# Copyright (C) 2020-2025 Dmitry Butyugin # # 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())] diff --git a/klippy/extras/shaper_defs.py b/klippy/extras/shaper_defs.py index 611fed16a..60fb1aa66 100644 --- a/klippy/extras/shaper_defs.py +++ b/klippy/extras/shaper_defs.py @@ -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), ] diff --git a/klippy/extras/stepper_enable.py b/klippy/extras/stepper_enable.py index 2bad75552..926e95922 100644 --- a/klippy/extras/stepper_enable.py +++ b/klippy/extras/stepper_enable.py @@ -1,6 +1,6 @@ # Support for enable pins on stepper motor drivers # -# Copyright (C) 2019-2021 Kevin O'Connor +# Copyright (C) 2019-2025 Kevin O'Connor # # 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,)) diff --git a/klippy/extras/sx1509.py b/klippy/extras/sx1509.py index fd36c7fe1..99df55df3 100644 --- a/klippy/extras/sx1509.py +++ b/klippy/extras/sx1509.py @@ -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): diff --git a/klippy/extras/temperature_probe.py b/klippy/extras/temperature_probe.py index c480ddae8..aebb10764 100644 --- a/klippy/extras/temperature_probe.py +++ b/klippy/extras/temperature_probe.py @@ -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: diff --git a/klippy/extras/virtual_sdcard.py b/klippy/extras/virtual_sdcard.py index 6dc49e2f5..b17004562 100644 --- a/klippy/extras/virtual_sdcard.py +++ b/klippy/extras/virtual_sdcard.py @@ -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 diff --git a/klippy/extras/z_tilt.py b/klippy/extras/z_tilt.py index 9f5ea0b9c..0316ee721 100644 --- a/klippy/extras/z_tilt.py +++ b/klippy/extras/z_tilt.py @@ -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: diff --git a/klippy/gcode.py b/klippy/gcode.py index 975da792b..1c50695d2 100644 --- a/klippy/gcode.py +++ b/klippy/gcode.py @@ -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: diff --git a/klippy/kinematics/extruder.py b/klippy/kinematics/extruder.py index a89e3bdfa..684f4be71 100644 --- a/klippy/kinematics/extruder.py +++ b/klippy/kinematics/extruder.py @@ -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" diff --git a/klippy/klippy.py b/klippy/klippy.py index 316343cbd..1d3ffbf06 100644 --- a/klippy/klippy.py +++ b/klippy/klippy.py @@ -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: diff --git a/klippy/mcu.py b/klippy/mcu.py index 146b5baca..eeec7c0b4 100644 --- a/klippy/mcu.py +++ b/klippy/mcu.py @@ -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() diff --git a/klippy/reactor.py b/klippy/reactor.py index 412d53edf..db6a089e3 100644 --- a/klippy/reactor.py +++ b/klippy/reactor.py @@ -1,6 +1,6 @@ # File descriptor and timer event helper # -# Copyright (C) 2016-2020 Kevin O'Connor +# Copyright (C) 2016-2025 Kevin O'Connor # # 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 diff --git a/klippy/stepper.py b/klippy/stepper.py index d5b3cecde..8d93d6c16 100644 --- a/klippy/stepper.py +++ b/klippy/stepper.py @@ -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(): diff --git a/klippy/toolhead.py b/klippy/toolhead.py index 21aeca3df..d7ce66b5c 100644 --- a/klippy/toolhead.py +++ b/klippy/toolhead.py @@ -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): diff --git a/klippy/util.py b/klippy/util.py index 6a8baee7f..9abb94df6 100644 --- a/klippy/util.py +++ b/klippy/util.py @@ -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": "?"} diff --git a/scripts/calibrate_shaper.py b/scripts/calibrate_shaper.py index b56ce5daa..ebf5c8aa1 100755 --- a/scripts/calibrate_shaper.py +++ b/scripts/calibrate_shaper.py @@ -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: diff --git a/scripts/graph_shaper.py b/scripts/graph_shaper.py index b9a6627c8..e97361693 100755 --- a/scripts/graph_shaper.py +++ b/scripts/graph_shaper.py @@ -5,20 +5,15 @@ # Copyright (C) 2020 Dmitry Butyugin # # 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: diff --git a/scripts/graphstats.py b/scripts/graphstats.py index 5cd2ad34f..c49223705 100755 --- a/scripts/graphstats.py +++ b/scripts/graphstats.py @@ -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 +# Copyright (C) 2016-2025 Kevin O'Connor # # 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 diff --git a/scripts/spi_flash/board_defs.py b/scripts/spi_flash/board_defs.py index 4fdba64cc..0a403d7da 100644 --- a/scripts/spi_flash/board_defs.py +++ b/scripts/spi_flash/board_defs.py @@ -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", diff --git a/scripts/spi_flash/spi_flash.py b/scripts/spi_flash/spi_flash.py index 729dd2bbc..e9394dbe5 100644 --- a/scripts/spi_flash/spi_flash.py +++ b/scripts/spi_flash/spi_flash.py @@ -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: diff --git a/src/sensor_lis2dw.c b/src/sensor_lis2dw.c index bf4beba1d..26452a0ad 100644 --- a/src/sensor_lis2dw.c +++ b/src/sensor_lis2dw.c @@ -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; diff --git a/src/stm32/gpio.c b/src/stm32/gpio.c index d19437cbe..0ef11822e 100644 --- a/src/stm32/gpio.c +++ b/src/stm32/gpio.c @@ -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; diff --git a/src/stm32/gpioperiph.c b/src/stm32/gpioperiph.c index 10d03738e..84fbebf5c 100644 --- a/src/stm32/gpioperiph.c +++ b/src/stm32/gpioperiph.c @@ -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); diff --git a/src/stm32/internal.h b/src/stm32/internal.h index 1d1e0a96b..a30bb6219 100644 --- a/src/stm32/internal.h +++ b/src/stm32/internal.h @@ -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)) diff --git a/src/stm32/spi.c b/src/stm32/spi.c index f64326813..358d21a26 100644 --- a/src/stm32/spi.c +++ b/src/stm32/spi.c @@ -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 }; diff --git a/src/stm32/stm32f1.c b/src/stm32/stm32f1.c index 0e4cb7821..94cfe1390 100644 --- a/src/stm32/stm32f1.c +++ b/src/stm32/stm32f1.c @@ -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); diff --git a/src/stm32/stm32h7_gpio.c b/src/stm32/stm32h7_gpio.c index f653c5708..78a1b4a45 100644 --- a/src/stm32/stm32h7_gpio.c +++ b/src/stm32/stm32h7_gpio.c @@ -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; diff --git a/src/stm32/stm32h7_spi.c b/src/stm32/stm32h7_spi.c index d1e514e7a..82a5ea0eb 100644 --- a/src/stm32/stm32h7_spi.c +++ b/src/stm32/stm32h7_spi.c @@ -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 diff --git a/test/klippy/extruders.cfg b/test/klippy/extruders.cfg index d7123d08e..7384617ef 100644 --- a/test/klippy/extruders.cfg +++ b/test/klippy/extruders.cfg @@ -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 diff --git a/test/klippy/input_shaper.cfg b/test/klippy/input_shaper.cfg index bca94eb77..33ec4e213 100644 --- a/test/klippy/input_shaper.cfg +++ b/test/klippy/input_shaper.cfg @@ -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 diff --git a/test/klippy/input_shaper.test b/test/klippy/input_shaper.test index 8714ec5e2..bc993ce27 100644 --- a/test/klippy/input_shaper.test +++ b/test/klippy/input_shaper.test @@ -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 diff --git a/test/klippy/manual_stepper.test b/test/klippy/manual_stepper.test index 6ab45c2da..127264113 100644 --- a/test/klippy/manual_stepper.test +++ b/test/klippy/manual_stepper.test @@ -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