diff --git a/config/generic-mini-rambo.cfg b/config/generic-mini-rambo.cfg index 61e2ac847d01..1a616cf80418 100644 --- a/config/generic-mini-rambo.cfg +++ b/config/generic-mini-rambo.cfg @@ -84,7 +84,7 @@ pwm: True scale: 2.0 cycle_time: .000030 hardware_pwm: True -static_value: 1.3 +value: 1.3 [output_pin stepper_z_current] pin: PL4 @@ -92,7 +92,7 @@ pwm: True scale: 2.0 cycle_time: .000030 hardware_pwm: True -static_value: 1.3 +value: 1.3 [output_pin stepper_e_current] pin: PL5 @@ -100,7 +100,7 @@ pwm: True scale: 2.0 cycle_time: .000030 hardware_pwm: True -static_value: 1.25 +value: 1.25 [static_digital_output stepper_config] pins: diff --git a/config/generic-ultimaker-ultimainboard-v2.cfg b/config/generic-ultimaker-ultimainboard-v2.cfg index 9a4d4e6da09a..b1ce3fa55882 100644 --- a/config/generic-ultimaker-ultimainboard-v2.cfg +++ b/config/generic-ultimaker-ultimainboard-v2.cfg @@ -97,7 +97,7 @@ max_z_accel: 30 [output_pin case_light] pin: PH5 -static_value: 1.0 +value: 1.0 # Motor current settings. [output_pin stepper_xy_current] @@ -107,7 +107,7 @@ scale: 2.000 # Max power setting. cycle_time: .000030 hardware_pwm: True -static_value: 1.200 +value: 1.200 # Power adjustment setting. [output_pin stepper_z_current] @@ -116,7 +116,7 @@ pwm: True scale: 2.000 cycle_time: .000030 hardware_pwm: True -static_value: 1.200 +value: 1.200 [output_pin stepper_e_current] pin: PL3 @@ -124,4 +124,4 @@ pwm: True scale: 2.000 cycle_time: .000030 hardware_pwm: True -static_value: 1.250 +value: 1.250 diff --git a/config/printer-adimlab-2018.cfg b/config/printer-adimlab-2018.cfg index 2f02173dde1b..d810e9d7e6fd 100644 --- a/config/printer-adimlab-2018.cfg +++ b/config/printer-adimlab-2018.cfg @@ -89,7 +89,7 @@ pwm: True scale: 2.0 cycle_time: .000030 hardware_pwm: True -static_value: 1.3 +value: 1.3 [output_pin stepper_z_current] pin: PL4 @@ -97,7 +97,7 @@ pwm: True scale: 2.0 cycle_time: .000030 hardware_pwm: True -static_value: 1.3 +value: 1.3 [output_pin stepper_e_current] pin: PL3 @@ -105,7 +105,7 @@ pwm: True scale: 2.0 cycle_time: .000030 hardware_pwm: True -static_value: 1.25 +value: 1.25 [display] lcd_type: st7920 diff --git a/config/printer-anycubic-kobra-plus-2022.cfg b/config/printer-anycubic-kobra-plus-2022.cfg index 96099797042c..3f71caab81b5 100644 --- a/config/printer-anycubic-kobra-plus-2022.cfg +++ b/config/printer-anycubic-kobra-plus-2022.cfg @@ -14,6 +14,7 @@ # To build the firmware, use the following configuration: # - Micro-controller: Huada Semiconductor HC32F460 # - Communication interface: Serial (PA3 & PA2) - Anycube +# - Clock Speed: 200 MHz # # Installation: # 1. Rename the klipper bin to `firmware.bin` and copy it to an SD Card. @@ -144,10 +145,9 @@ max_temp: 120 pause_on_runout: True switch_pin: !PC13 -[heater_fan controller_fan] +[controller_fan controller_fan] pin: PA14 heater: heater_bed -heater_temp: 45.0 [heater_fan hotend_fan] pin: PA13 diff --git a/config/printer-creality-cr30-2021.cfg b/config/printer-creality-cr30-2021.cfg index de946920032b..1edc75313660 100644 --- a/config/printer-creality-cr30-2021.cfg +++ b/config/printer-creality-cr30-2021.cfg @@ -98,7 +98,6 @@ max_temp: 100 [output_pin led] pin: PC14 -static_value: 0 # Neopixel LED support # [neopixel led_neopixel] diff --git a/config/printer-geeetech-301-2019.cfg b/config/printer-geeetech-301-2019.cfg index 5b3a9a4ab25e..6889fae5ae08 100644 --- a/config/printer-geeetech-301-2019.cfg +++ b/config/printer-geeetech-301-2019.cfg @@ -71,25 +71,21 @@ pid_Kp: 39 pid_Ki: 2 pid_Kd: 210 -[extruder1] +[extruder_stepper e1] +extruder: step_pin: PA0 dir_pin: !PB6 enable_pin: !PA1 microsteps: 16 rotation_distance: 32 -nozzle_diameter: 0.4 -filament_diameter: 1.75 -shared_heater: extruder -[extruder2] +[extruder_stepper e2] +extruder: step_pin: PB2 dir_pin: !PB11 enable_pin: !PC4 microsteps: 16 rotation_distance: 32 -nozzle_diameter: 0.4 -filament_diameter: 1.75 -shared_heater: extruder [heater_bed] heater_pin: PB1 diff --git a/config/printer-lulzbot-mini1-2016.cfg b/config/printer-lulzbot-mini1-2016.cfg index 9be60cbddaaf..52b8061eeb4a 100644 --- a/config/printer-lulzbot-mini1-2016.cfg +++ b/config/printer-lulzbot-mini1-2016.cfg @@ -125,7 +125,7 @@ pwm: True scale: 2.0 cycle_time: .000030 hardware_pwm: True -static_value: 1.300 +value: 1.300 [output_pin stepper_z_current] pin: PL4 @@ -133,7 +133,7 @@ pwm: True scale: 2.0 cycle_time: .000030 hardware_pwm: True -static_value: 1.630 +value: 1.630 [output_pin stepper_e_current] pin: PL5 @@ -141,7 +141,7 @@ pwm: True scale: 2.0 cycle_time: .000030 hardware_pwm: True -static_value: 1.250 +value: 1.250 [static_digital_output stepper_config] # Microstepping pins diff --git a/config/printer-modix-big60-2020.cfg b/config/printer-modix-big60-2020.cfg index 0224851ea4b9..2fc311cf19dc 100644 --- a/config/printer-modix-big60-2020.cfg +++ b/config/printer-modix-big60-2020.cfg @@ -199,7 +199,6 @@ algorithm: bicubic bicubic_tension: 0.15 fade_start: 0.5 fade_end: 2.5 -relative_reference_index: 60 [bed_screws] screw1: 0,0 diff --git a/config/printer-wanhao-duplicator-6-2016.cfg b/config/printer-wanhao-duplicator-6-2016.cfg index b1d35faecb82..de8a3de87d2a 100644 --- a/config/printer-wanhao-duplicator-6-2016.cfg +++ b/config/printer-wanhao-duplicator-6-2016.cfg @@ -86,7 +86,7 @@ pwm: True scale: 2.782 cycle_time: .000030 hardware_pwm: True -static_value: 1.2 +value: 1.2 [output_pin stepper_z_current] pin: PL4 @@ -94,7 +94,7 @@ pwm: True scale: 2.782 cycle_time: .000030 hardware_pwm: True -static_value: 1.2 +value: 1.2 [output_pin stepper_e_current] pin: PL3 @@ -102,7 +102,7 @@ pwm: True scale: 2.782 cycle_time: .000030 hardware_pwm: True -static_value: 1.0 +value: 1.0 [display] lcd_type: ssd1306 diff --git a/config/sample-macros.cfg b/config/sample-macros.cfg index 5132e1c99c37..f5649d61a396 100644 --- a/config/sample-macros.cfg +++ b/config/sample-macros.cfg @@ -61,12 +61,10 @@ gcode: # P is the tone duration, S the tone frequency. # The frequency won't be pitch perfect. -[output_pin BEEPER_pin] +[pwm_cycle_time BEEPER_pin] pin: ar37 # Beeper pin. This parameter must be provided. # ar37 is the default RAMPS/MKS pin. -pwm: True -# A piezo beeper needs a PWM signal, a DC buzzer doesn't. value: 0 # Silent at power on, set to 1 if active low. shutdown_value: 0 diff --git a/docs/Bed_Mesh.md b/docs/Bed_Mesh.md index d2a417dd40ec..9ee8df507e77 100644 --- a/docs/Bed_Mesh.md +++ b/docs/Bed_Mesh.md @@ -370,14 +370,68 @@ are identified in green. ![bedmesh_interpolated](img/bedmesh_faulty_regions.svg) +### Adaptive Meshes + +Adaptive bed meshing is a way to speed up the bed mesh generation by only probing +the area of the bed used by the objects being printed. When used, the method will +automatically adjust the mesh parameters based on the area occupied by the defined +print objects. + +The adapted mesh area will be computed from the area defined by the boundaries of all +the defined print objects so it covers every object, including any margins defined in +the configuration. After the area is computed, the number of probe points will be +scaled down based on the ratio of the default mesh area and the adapted mesh area. To +illustrate this consider the following example: + +For a 150mmx150mm bed with `mesh_min` set to `25,25` and `mesh_max` set to `125,125`, +the default mesh area is a 100mmx100mm square. An adapted mesh area of `50,50` +means a ratio of `0.5x0.5` between the adapted area and default mesh area. + +If the `bed_mesh` configuration specified `probe_count` as `7x7`, the adapted bed +mesh will use 4x4 probe points (7 * 0.5 rounded up). + +![adaptive_bedmesh](img/adaptive_bed_mesh.svg) + +``` +[bed_mesh] +speed: 120 +horizontal_move_z: 5 +mesh_min: 35, 6 +mesh_max: 240, 198 +probe_count: 5, 3 +adaptive_margin: 5 +``` + +- `adaptive_margin` \ + _Default Value: 0_ \ + Margin (in mm) to add around the area of the bed used by the defined objects. The diagram + below shows the adapted bed mesh area with an `adaptive_margin` of 5mm. The adapted mesh + area (area in green) is computed as the used bed area (area in blue) plus the defined margin. + + ![adaptive_bedmesh_margin](img/adaptive_bed_mesh_margin.svg) + +By nature, adaptive bed meshes use the objects defined by the Gcode file being printed. +Therefore, it is expected that each Gcode file will generate a mesh that probes a different +area of the print bed. Therefore, adapted bed meshes should not be re-used. The expectation +is that a new mesh will be generated for each print if adaptive meshing is used. + +It is also important to consider that adaptive bed meshing is best used on machines that can +normally probe the entire bed and achieve a maximum variance less than or equal to 1 layer +height. Machines with mechanical issues that a full bed mesh normally compensates for may +have undesirable results when attempting print moves **outside** of the probed area. If a +full bed mesh has a variance greater than 1 layer height, caution must be taken when using +adaptive bed meshes and attempting print moves outside of the meshed area. + ## Bed Mesh Gcodes ### Calibration `BED_MESH_CALIBRATE PROFILE= METHOD=[manual | automatic] [=] - [=]`\ + [=] [ADAPTIVE=[0|1] [ADAPTIVE_MARGIN=]`\ _Default Profile: default_\ -_Default Method: automatic if a probe is detected, otherwise manual_ +_Default Method: automatic if a probe is detected, otherwise manual_ \ +_Default Adaptive: 0_ \ +_Default Adaptive Margin: 0_ Initiates the probing procedure for Bed Mesh Calibration. @@ -399,6 +453,8 @@ following parameters are available: - `ROUND_PROBE_COUNT` - All beds: - `ALGORITHM` + - `ADAPTIVE` + - `ADAPTIVE_MARGIN` See the configuration documentation above for details on how each parameter applies to the mesh. @@ -486,11 +542,19 @@ This gcode may be used to clear the internal mesh state. ### Apply X/Y offsets -`BED_MESH_OFFSET [X=] [Y=]` +`BED_MESH_OFFSET [X=] [Y=] [ZFADE=]` This is useful for printers with multiple independent extruders, as an offset is necessary to produce correct Z adjustment after a tool change. Offsets should be specified relative to the primary extruder. That is, a positive X offset should be specified if the secondary extruder is mounted to the -right of the primary extruder, and a positive Y offset should be specified -if the secondary extruder is mounted "behind" the primary extruder. +right of the primary extruder, a positive Y offset should be specified +if the secondary extruder is mounted "behind" the primary extruder, and +a positive ZFADE offset should be specified if the secondary extruder's +nozzle is above the primary extruder's. + +Note that a ZFADE offset does *NOT* directly apply additional adjustment. It +is intended to compensate for a `gcode offset` when [mesh fade](#mesh-fade) +is enabled. For example, if a secondary extruder is higher than the primary +and needs a negative gcode offset, ie: `SET_GCODE_OFFSET Z=-.2`, it can be +accounted for in `bed_mesh` with `BED_MESH_OFFSET ZFADE=.2`. diff --git a/docs/Config_Changes.md b/docs/Config_Changes.md index 2ceb868db0b9..980c4f33ac7f 100644 --- a/docs/Config_Changes.md +++ b/docs/Config_Changes.md @@ -8,6 +8,27 @@ All dates in this document are approximate. ## Changes +20240215: Several deprecated features have been removed. Using "NTC +100K beta 3950" as a thermistor name has been removed (deprecated on +20211110). The `SYNC_STEPPER_TO_EXTRUDER` and +`SET_EXTRUDER_STEP_DISTANCE` commands have been removed, and the +extruder `shared_heater` config option has been removed (deprecated on +20220210). The bed_mesh `relative_reference_index` option has been +removed (deprecated on 20230619). + +20240123: The output_pin SET_PIN CYCLE_TIME parameter has been +removed. Use the new +[pwm_cycle_time](Config_Reference.md#pwm_cycle_time) module if it is +necessary to dynamically change a pwm pin's cycle time. + +20240123: The output_pin `maximum_mcu_duration` parameter is +deprecated. Use a [pwm_tool config section](Config_Reference.md#pwm_tool) +instead. The option will be removed in the near future. + +20240123: The output_pin `static_value` parameter is deprecated. +Replace with `value` and `shutdown_value` parameters. The option will +be removed in the near future. + 20231216: The `[hall_filament_width_sensor]` is changed to trigger filament runout when the thickness of the filament exceeds `max_diameter`. The maximum diameter defaults to `default_nominal_filament_diameter + max_difference`. See diff --git a/docs/Config_Reference.md b/docs/Config_Reference.md index 4b53ef264e44..14b89200fb00 100644 --- a/docs/Config_Reference.md +++ b/docs/Config_Reference.md @@ -979,18 +979,14 @@ Visual Examples: # where Z = 0. When this option is specified the mesh will be offset # so that zero Z adjustment occurs at this location. The default is # no zero reference. -#relative_reference_index: -# **DEPRECATED, use the "zero_reference_position" option** -# The legacy option superceded by the "zero reference position". -# Rather than a coordinate this option takes an integer "index" that -# refers to the location of one of the generated points. It is recommended -# to use the "zero_reference_position" instead of this option for new -# configurations. The default is no relative reference index. #faulty_region_1_min: #faulty_region_1_max: # Optional points that define a faulty region. See docs/Bed_Mesh.md # for details on faulty regions. Up to 99 faulty regions may be added. # By default no faulty regions are set. +#adaptive_margin: +# An optional margin (in mm) to be added around the bed area used by +# the defined print objects when generating an adaptive mesh. ``` ### [bed_tilt] @@ -3096,24 +3092,12 @@ pin: # If this is true, the value fields should be between 0 and 1; if it # is false the value fields should be either 0 or 1. The default is # False. -#static_value: -# If this is set, then the pin is assigned to this value at startup -# and the pin can not be changed during runtime. A static pin uses -# slightly less ram in the micro-controller. The default is to use -# runtime configuration of pins. #value: # The value to initially set the pin to during MCU configuration. # The default is 0 (for low voltage). #shutdown_value: # The value to set the pin to on an MCU shutdown event. The default # is 0 (for low voltage). -#maximum_mcu_duration: -# The maximum duration a non-shutdown value may be driven by the MCU -# without an acknowledge from the host. -# If host can not keep up with an update, the MCU will shutdown -# and set all pins to their respective shutdown values. -# Default: 0 (disabled) -# Usual values are around 5 seconds. #cycle_time: 0.100 # The amount of time (in seconds) per PWM cycle. It is recommended # this be 10 milliseconds or greater when using software based PWM. @@ -3133,6 +3117,9 @@ pin: # then the 'value' parameter can be specified using the desired # amperage for the stepper. The default is to not scale the 'value' # parameter. +#maximum_mcu_duration: +#static_value: +# These options are deprecated and should no longer be specified. ``` ### [pwm_tool] @@ -3147,15 +3134,39 @@ extended [g-code commands](G-Codes.md#output_pin). [pwm_tool my_tool] pin: # The pin to configure as an output. This parameter must be provided. +#maximum_mcu_duration: +# The maximum duration a non-shutdown value may be driven by the MCU +# without an acknowledge from the host. +# If host can not keep up with an update, the MCU will shutdown +# and set all pins to their respective shutdown values. +# Default: 0 (disabled) +# Usual values are around 5 seconds. #value: #shutdown_value: -#maximum_mcu_duration: #cycle_time: 0.100 #hardware_pwm: False #scale: # See the "output_pin" section for the definition of these parameters. ``` +### [pwm_cycle_time] + +Run-time configurable output pins with dynamic pwm cycle timing (one +may define any number of sections with an "pwm_cycle_time" prefix). +Pins configured here will be setup as output pins and one may modify +them at run-time using "SET_PIN PIN=my_pin VALUE=.1 CYCLE_TIME=0.100" +type extended [g-code commands](G-Codes.md#pwm_cycle_time). + +``` +[pwm_cycle_time my_pin] +pin: +#value: +#shutdown_value: +#cycle_time: 0.100 +#scale: +# See the "output_pin" section for information on these parameters. +``` + ### [static_digital_output] Statically configured digital output pins (one may define any number diff --git a/docs/G-Codes.md b/docs/G-Codes.md index 8c70609f18c9..199dc283b7ce 100644 --- a/docs/G-Codes.md +++ b/docs/G-Codes.md @@ -146,15 +146,21 @@ The following commands are available when the (also see the [bed mesh guide](Bed_Mesh.md)). #### BED_MESH_CALIBRATE -`BED_MESH_CALIBRATE [METHOD=manual] [HORIZONTAL_MOVE_Z=] -[=] [=]`: This command probes -the bed using generated points specified by the parameters in the config. After -probing, a mesh is generated and z-movement is adjusted according to the mesh. +`BED_MESH_CALIBRATE [PROFILE=] [METHOD=manual] [HORIZONTAL_MOVE_Z=] +[=] [=] [ADAPTIVE=1] +[ADAPTIVE_MARGIN=]`: This command probes the bed using generated points +specified by the parameters in the config. After probing, a mesh is generated +and z-movement is adjusted according to the mesh. +The mesh will be saved into a profile specified by the `PROFILE` parameter, +or `default` if unspecified. See the PROBE command for details on the optional probe parameters. If METHOD=manual is specified then the manual probing tool is activated - see the MANUAL_PROBE command above for details on the additional commands available while this tool is active. The optional `HORIZONTAL_MOVE_Z` value overrides the -`horizontal_move_z` option specified in the config file. +`horizontal_move_z` option specified in the config file. If ADAPTIVE=1 is +specified then the objects defined by the Gcode file being printed will be used +to define the probed area. The optional `ADAPTIVE_MARGIN` value overrides the +`adaptive_margin` option specified in the config file. #### BED_MESH_OUTPUT `BED_MESH_OUTPUT PGP=[<0:1>]`: This command outputs the current probed @@ -184,10 +190,12 @@ SAVE_CONFIG gcode must be run to make the changes to persistent memory permanent. #### BED_MESH_OFFSET -`BED_MESH_OFFSET [X=] [Y=]`: Applies X and/or Y offsets -to the mesh lookup. This is useful for printers with independent -extruders, as an offset is necessary to produce correct Z adjustment -after a tool change. +`BED_MESH_OFFSET [X=] [Y=] [ZFADE= [CYCLE_TIME=]`: Set -the pin to the given output `VALUE`. VALUE should be 0 or 1 for -"digital" output pins. For PWM pins, set to a value between 0.0 and -1.0, or between 0.0 and `scale` if a scale is configured in the -output_pin config section. - -Some pins (currently only "soft PWM" pins) support setting an explicit -cycle time using the CYCLE_TIME parameter (specified in seconds). Note -that the CYCLE_TIME parameter is not stored between SET_PIN commands -(any SET_PIN command without an explicit CYCLE_TIME parameter will use -the `cycle_time` specified in the output_pin config section). +`SET_PIN PIN=config_name VALUE=`: Set the pin to the given +output `VALUE`. VALUE should be 0 or 1 for "digital" output pins. For +PWM pins, set to a value between 0.0 and 1.0, or between 0.0 and +`scale` if a scale is configured in the output_pin config section. ### [palette2] @@ -978,6 +973,21 @@ babystepping), and subtract if from the probe's z_offset. This acts to take a frequently used babystepping value, and "make it permanent". Requires a `SAVE_CONFIG` to take effect. +### [pwm_cycle_time] + +The following command is available when a +[pwm_cycle_time config section](Config_Reference.md#pwm_cycle_time) +is enabled. + +#### SET_PIN +`SET_PIN PIN=config_name VALUE= [CYCLE_TIME=]`: +This command works similarly to [output_pin](#output_pin) SET_PIN +commands. The command here supports setting an explicit cycle time +using the CYCLE_TIME parameter (specified in seconds). Note that the +CYCLE_TIME parameter is not stored between SET_PIN commands (any +SET_PIN command without an explicit CYCLE_TIME parameter will use the +`cycle_time` specified in the pwm_cycle_time config section). + ### [query_adc] The query_adc module is automatically loaded. diff --git a/docs/Measuring_Resonances.md b/docs/Measuring_Resonances.md index 28cda9d0c656..d1d998941dac 100644 --- a/docs/Measuring_Resonances.md +++ b/docs/Measuring_Resonances.md @@ -207,7 +207,7 @@ software dependencies not installed by default. First, run on your Raspberry Pi the following commands: ``` sudo apt update -sudo apt install python3-numpy python3-matplotlib libatlas-base-dev +sudo apt install python3-numpy python3-matplotlib libatlas-base-dev libopenblas-base ``` Next, in order to install NumPy in the Klipper environment, run the command: @@ -662,6 +662,19 @@ The same notice applies to the input shaper `max_accel` value after the auto-calibration, and the suggested acceleration limits will not be applied automatically. +Keep in mind that the maximum acceleration without too much smoothing depends +on the `square_corner_velocity`. The general recommendation is not to change +it from its default value 5.0, and this is the value used by default by the +`calibrate_shaper.py` script. If you did change it though, you should inform +the script about it by passing `--square_corner_velocity=...` parameter, e.g. +``` +~/klipper/scripts/calibrate_shaper.py /tmp/resonances_x_*.csv -o /tmp/shaper_calibrate_x.png --square_corner_velocity=10.0 +``` +so that it can calculate the maximum acceleration recommendations correctly. +Note that the `SHAPER_CALIBRATE` command already takes the configured +`square_corner_velocity` parameter into account, and there is no need +to specify it explicitly. + 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. diff --git a/docs/Probe_Calibrate.md b/docs/Probe_Calibrate.md index ed3964e68923..5f1d44365d91 100644 --- a/docs/Probe_Calibrate.md +++ b/docs/Probe_Calibrate.md @@ -64,7 +64,7 @@ automatic probe point, then `ABORT` the manual probe tool and perform the XY probe offset calibration described above. Once the manual probe tool starts, follow the steps described at -["the paper test"](Bed_Level.md#the-paper-test)) to determine the +["the paper test"](Bed_Level.md#the-paper-test) to determine the actual distance between the nozzle and bed at the given location. Once those steps are complete one can `ACCEPT` the position and save the results to the config file with: diff --git a/docs/Skew_Correction.md b/docs/Skew_Correction.md index 0475de89ae66..55bc22b0a5cc 100644 --- a/docs/Skew_Correction.md +++ b/docs/Skew_Correction.md @@ -21,7 +21,7 @@ or by issuing a `SET_SKEW CLEAR=1` gcode. ## Take your measurements -The `[skew_correcton]` module requires 3 measurements for each plane you want +The `[skew_correction]` module requires 3 measurements for each plane you want to correct; the length from Corner A to Corner C, the length from Corner B to Corner D, and the length from Corner A to Corner D. When measuring length AD do not include the flats on the corners that some test objects provide. diff --git a/docs/Status_Reference.md b/docs/Status_Reference.md index b64108ae25c0..055d1dc0eac0 100644 --- a/docs/Status_Reference.md +++ b/docs/Status_Reference.md @@ -374,6 +374,13 @@ is defined): template expansion, the PROBE (or similar) command must be run prior to the macro containing this reference. +## pwm_cycle_time + +The following information is available in +[pwm_cycle_time some_name](Config_Reference.md#pwm_cycle_time) +objects: +- `value`: The "value" of the pin, as set by a `SET_PIN` command. + ## quad_gantry_level The following information is available in the `quad_gantry_level` object diff --git a/docs/img/adaptive_bed_mesh.svg b/docs/img/adaptive_bed_mesh.svg new file mode 100644 index 000000000000..954ca0b322c7 --- /dev/null +++ b/docs/img/adaptive_bed_mesh.svg @@ -0,0 +1,4 @@ + + + +
Origin
(0,0)
Origin...

Legend


Legend
Object Polygon
Object Polygon
Used Bed Area
Used Bed Area
Text is not SVG - cannot display
\ No newline at end of file diff --git a/docs/img/adaptive_bed_mesh_margin.svg b/docs/img/adaptive_bed_mesh_margin.svg new file mode 100644 index 000000000000..6c6216d041aa --- /dev/null +++ b/docs/img/adaptive_bed_mesh_margin.svg @@ -0,0 +1,4 @@ + + + +
Origin
(0,0)
Origin...

Legend


Legend
Object Polygon
Object Polygon
Used Bed Area
Used Bed Area
Adapted Bed Mesh Area
Adapted Bed Mesh Area
(90,75)
(90,75)
(130,140)
(130,140)
(125,135)
(125,1...
(95,120)
(95,12...
(60,90)
(60,90)
(90,130)
(90,13...
(95,80)
(95,80)
(125,110)
(125,1...
Text is not SVG - cannot display
\ No newline at end of file diff --git a/klippy/extras/bed_mesh.py b/klippy/extras/bed_mesh.py index 6c714304fb8a..87f2324a4376 100644 --- a/klippy/extras/bed_mesh.py +++ b/klippy/extras/bed_mesh.py @@ -1,6 +1,5 @@ # Mesh Bed Leveling # -# Copyright (C) 2018 Kevin O'Connor # Copyright (C) 2018-2019 Eric Callahan # # This file may be distributed under the terms of the GNU GPLv3 license. @@ -103,6 +102,7 @@ def __init__(self, config): self.log_fade_complete = False self.base_fade_target = config.getfloat('fade_target', None) self.fade_target = 0. + self.tool_offset = 0. self.gcode = self.printer.lookup_object('gcode') self.splitter = MoveSplitter(config, self.gcode) # setup persistent storage @@ -158,6 +158,7 @@ def set_mesh(self, mesh): "mesh max: %.4f" % (self.fade_dist, min_z, max_z)) else: self.fade_target = 0. + self.tool_offset = 0. self.z_mesh = mesh self.splitter.initialize(mesh, self.fade_target) # cache the current position before a transform takes place @@ -165,6 +166,7 @@ def set_mesh(self, mesh): gcode_move.reset_last_position() self.update_status() def get_z_factor(self, z_pos): + z_pos += self.tool_offset if z_pos >= self.fade_end: return 0. elif z_pos >= self.fade_start: @@ -183,14 +185,15 @@ def get_position(self): max_adj = self.z_mesh.calc_z(x, y) factor = 1. z_adj = max_adj - self.fade_target - if min(z, (z - max_adj)) >= self.fade_end: + fade_z_pos = z + self.tool_offset + if min(fade_z_pos, (fade_z_pos - max_adj)) >= self.fade_end: # Fade out is complete, no factor factor = 0. - elif max(z, (z - max_adj)) >= self.fade_start: + elif max(fade_z_pos, (fade_z_pos - max_adj)) >= self.fade_start: # Likely in the process of fading out adjustment. # Because we don't yet know the gcode z position, use # algebra to calculate the factor from the toolhead pos - factor = ((self.fade_end + self.fade_target - z) / + factor = ((self.fade_end + self.fade_target - fade_z_pos) / (self.fade_dist - z_adj)) factor = constrain(factor, 0., 1.) final_z_adj = factor * z_adj + self.fade_target @@ -234,7 +237,7 @@ def update_status(self): mesh_max = (params['max_x'], params['max_y']) probed_matrix = self.z_mesh.get_probed_matrix() mesh_matrix = self.z_mesh.get_mesh_matrix() - self.status['profile_name'] = self.pmgr.get_current_profile() + self.status['profile_name'] = self.z_mesh.get_profile_name() self.status['mesh_min'] = mesh_min self.status['mesh_max'] = mesh_max self.status['probed_matrix'] = probed_matrix @@ -272,6 +275,9 @@ def cmd_BED_MESH_OFFSET(self, gcmd): for i, axis in enumerate(['X', 'Y']): offsets[i] = gcmd.get_float(axis, None) self.z_mesh.set_mesh_offsets(offsets) + tool_offset = gcmd.get_float("ZFADE", None) + if tool_offset is not None: + self.tool_offset = tool_offset gcode_move = self.printer.lookup_object('gcode_move') gcode_move.reset_last_position() else: @@ -291,22 +297,10 @@ def __init__(self, config, bedmesh): self.orig_config = {'radius': None, 'origin': None} self.radius = self.origin = None self.mesh_min = self.mesh_max = (0., 0.) + self.adaptive_margin = config.getfloat('adaptive_margin', 0.0) self.zero_ref_pos = config.getfloatlist( "zero_reference_position", None, count=2 ) - self.relative_reference_index = config.getint( - 'relative_reference_index', None, minval=0) - config.deprecate('relative_reference_index') - if ( - self.zero_ref_pos is not None and - self.relative_reference_index is not None - ): - self.relative_reference_index = None - logging.info( - "bed_mesh: both 'zero_reference_postion' and " - "'relative_reference_index' options are specified, " - "the 'zero_reference_position' value will be used." - ) self.zero_reference_mode = ZrefMode.DISABLED self.faulty_regions = [] self.substituted_indices = collections.OrderedDict() @@ -314,7 +308,7 @@ def __init__(self, config, bedmesh): self.mesh_config = collections.OrderedDict() self._init_mesh_config(config) self._generate_points(config.error) - self._profile_name = None + self._profile_name = "default" self.probe_helper = probe.ProbePointsHelper( config, self.probe_finalize, self._get_adjusted_points()) self.probe_helper.minimum_points(3) @@ -323,7 +317,7 @@ def __init__(self, config, bedmesh): self.gcode.register_command( 'BED_MESH_CALIBRATE', self.cmd_BED_MESH_CALIBRATE, desc=self.cmd_BED_MESH_CALIBRATE_help) - def _generate_points(self, error): + def _generate_points(self, error, probe_method="automatic"): x_cnt = self.mesh_config['x_count'] y_cnt = self.mesh_config['y_count'] min_x, min_y = self.mesh_min @@ -366,13 +360,7 @@ def _generate_points(self, error): (self.origin[0] + pos_x, self.origin[1] + pos_y)) pos_y += y_dist self.points = points - rri = self.relative_reference_index - if self.zero_ref_pos is None and rri is not None: - # Zero ref position needs to be initialized - if rri >= len(self.points): - raise error("bed_mesh: relative reference index out of range") - self.zero_ref_pos = points[rri] - if self.zero_ref_pos is None: + if self.zero_ref_pos is None or probe_method == "manual": # Zero Reference Disabled self.zero_reference_mode = ZrefMode.DISABLED elif within(self.zero_ref_pos, self.mesh_min, self.mesh_max): @@ -389,8 +377,6 @@ def _generate_points(self, error): for min_c, max_c in self.faulty_regions: if within(self.zero_ref_pos, min_c, max_c): opt = "zero_reference_position" - if self.relative_reference_index is not None: - opt = "relative_reference_index" raise error( "bed_mesh: Cannot probe zero reference position at " "(%.2f, %.2f) as it is located within a faulty region." @@ -398,6 +384,8 @@ def _generate_points(self, error): % (self.zero_ref_pos[0], self.zero_ref_pos[1], opt,) ) # Check to see if any points fall within faulty regions + if probe_method == "manual": + return last_y = self.points[0][1] is_reversed = False for i, coord in enumerate(self.points): @@ -447,17 +435,10 @@ def print_generated_points(self, print_func): print_func( " %-4d| %-16s| %s" % (i, adj_pt, mesh_pt)) if self.zero_ref_pos is not None: - rri = self.relative_reference_index - if rri is not None: - print_func( - "bed_mesh: relative_reference_index %d is (%.2f, %.2f)" - % (rri, self.zero_ref_pos[0], self.zero_ref_pos[1]) - ) - else: - print_func( - "bed_mesh: zero_reference_position is (%.2f, %.2f)" - % (self.zero_ref_pos[0], self.zero_ref_pos[1]) - ) + print_func( + "bed_mesh: zero_reference_position is (%.2f, %.2f)" + % (self.zero_ref_pos[0], self.zero_ref_pos[1]) + ) if self.substituted_indices: print_func("bed_mesh: faulty region points") for i, v in self.substituted_indices.items(): @@ -573,6 +554,113 @@ def _verify_algorithm(self, error): "interpolation. Configured Probe Count: %d, %d" % (self.mesh_config['x_count'], self.mesh_config['y_count'])) params['algo'] = 'lagrange' + def set_adaptive_mesh(self, gcmd): + if not gcmd.get_int('ADAPTIVE', 0): + return False + exclude_objects = self.printer.lookup_object("exclude_object", None) + if exclude_objects is None: + gcmd.respond_info("Exclude objects not enabled. Using full mesh...") + return False + objects = exclude_objects.get_status().get("objects", []) + if not objects: + return False + margin = gcmd.get_float('ADAPTIVE_MARGIN', self.adaptive_margin) + + # List all exclude_object points by axis and iterate over + # all polygon points, and pick the min and max or each axis + list_of_xs = [] + list_of_ys = [] + gcmd.respond_info("Found %s objects" % (len(objects))) + for obj in objects: + for point in obj["polygon"]: + list_of_xs.append(point[0]) + list_of_ys.append(point[1]) + + # Define bounds of adaptive mesh area + mesh_min = [min(list_of_xs), min(list_of_ys)] + mesh_max = [max(list_of_xs), max(list_of_ys)] + adjusted_mesh_min = [x - margin for x in mesh_min] + adjusted_mesh_max = [x + margin for x in mesh_max] + + # Force margin to respect original mesh bounds + adjusted_mesh_min[0] = max(adjusted_mesh_min[0], + self.orig_config["mesh_min"][0]) + adjusted_mesh_min[1] = max(adjusted_mesh_min[1], + self.orig_config["mesh_min"][1]) + adjusted_mesh_max[0] = min(adjusted_mesh_max[0], + self.orig_config["mesh_max"][0]) + adjusted_mesh_max[1] = min(adjusted_mesh_max[1], + self.orig_config["mesh_max"][1]) + + adjusted_mesh_size = (adjusted_mesh_max[0] - adjusted_mesh_min[0], + adjusted_mesh_max[1] - adjusted_mesh_min[1]) + + # Compute a ratio between the adapted and original sizes + ratio = (adjusted_mesh_size[0] / + (self.orig_config["mesh_max"][0] - + self.orig_config["mesh_min"][0]), + adjusted_mesh_size[1] / + (self.orig_config["mesh_max"][1] - + self.orig_config["mesh_min"][1])) + + gcmd.respond_info("Original mesh bounds: (%s,%s)" % + (self.orig_config["mesh_min"], + self.orig_config["mesh_max"])) + gcmd.respond_info("Original probe count: (%s,%s)" % + (self.mesh_config["x_count"], + self.mesh_config["y_count"])) + gcmd.respond_info("Adapted mesh bounds: (%s,%s)" % + (adjusted_mesh_min, adjusted_mesh_max)) + gcmd.respond_info("Ratio: (%s, %s)" % ratio) + + new_x_probe_count = int( + math.ceil(self.mesh_config["x_count"] * ratio[0])) + new_y_probe_count = int( + math.ceil(self.mesh_config["y_count"] * ratio[1])) + + # There is one case, where we may have to adjust the probe counts: + # axis0 < 4 and axis1 > 6 (see _verify_algorithm). + min_num_of_probes = 3 + if max(new_x_probe_count, new_y_probe_count) > 6 and \ + min(new_x_probe_count, new_y_probe_count) < 4: + min_num_of_probes = 4 + + new_x_probe_count = max(min_num_of_probes, new_x_probe_count) + new_y_probe_count = max(min_num_of_probes, new_y_probe_count) + + gcmd.respond_info("Adapted probe count: (%s,%s)" % + (new_x_probe_count, new_y_probe_count)) + + # If the adapted mesh size is too small, adjust it to something + # useful. + adjusted_mesh_size = (max(adjusted_mesh_size[0], new_x_probe_count), + max(adjusted_mesh_size[1], new_y_probe_count)) + + if self.radius is not None: + adapted_radius = math.sqrt((adjusted_mesh_size[0] ** 2) + + (adjusted_mesh_size[1] ** 2)) / 2 + adapted_origin = (adjusted_mesh_min[0] + + (adjusted_mesh_size[0] / 2), + adjusted_mesh_min[1] + + (adjusted_mesh_size[1] / 2)) + to_adapted_origin = math.sqrt(adapted_origin[0]**2 + + adapted_origin[1]**2) + # If the adapted mesh size is smaller than the default/full + # mesh, adjust the parameters. Otherwise, just do the full mesh. + if adapted_radius + to_adapted_origin < self.radius: + self.radius = adapted_radius + self.origin = adapted_origin + self.mesh_min = (-self.radius, -self.radius) + self.mesh_max = (self.radius, self.radius) + self.mesh_config["x_count"] = self.mesh_config["y_count"] = \ + max(new_x_probe_count, new_y_probe_count) + else: + self.mesh_min = adjusted_mesh_min + self.mesh_max = adjusted_mesh_max + self.mesh_config["x_count"] = new_x_probe_count + self.mesh_config["y_count"] = new_y_probe_count + self._profile_name = None + return True def update_config(self, gcmd): # reset default configuration self.radius = self.orig_config['radius'] @@ -616,20 +704,21 @@ def update_config(self, gcmd): self.mesh_config['algo'] = gcmd.get('ALGORITHM').strip().lower() need_cfg_update = True + need_cfg_update |= self.set_adaptive_mesh(gcmd) + probe_method = gcmd.get("METHOD", "automatic") + if need_cfg_update: self._verify_algorithm(gcmd.error) - self._generate_points(gcmd.error) + self._generate_points(gcmd.error, probe_method) gcmd.respond_info("Generating new points...") self.print_generated_points(gcmd.respond_info) pts = self._get_adjusted_points() self.probe_helper.update_probe_points(pts, 3) - msg = "relative_reference_index: %s\n" % \ - (self.relative_reference_index) - msg += "\n".join(["%s: %s" % (k, v) for k, v - in self.mesh_config.items()]) + msg = "\n".join(["%s: %s" % (k, v) + for k, v in self.mesh_config.items()]) logging.info("Updated Mesh Configuration:\n" + msg) else: - self._generate_points(gcmd.error) + self._generate_points(gcmd.error, probe_method) pts = self._get_adjusted_points() self.probe_helper.update_probe_points(pts, 3) def _get_adjusted_points(self): @@ -660,7 +749,7 @@ def probe_finalize(self, offsets, positions): x_offset, y_offset, z_offset = offsets positions = [[round(p[0], 2), round(p[1], 2), p[2]] for p in positions] - if self.zero_reference_mode == ZrefMode.PROBE : + if self.zero_reference_mode == ZrefMode.PROBE: ref_pos = positions.pop() logging.info( "bed_mesh: z-offset replaced with probed z value at " @@ -769,7 +858,7 @@ def probe_finalize(self, offsets, positions): "Probed table length: %d Probed Table:\n%s") % (len(probed_matrix), str(probed_matrix))) - z_mesh = ZMesh(params) + z_mesh = ZMesh(params, self._profile_name) try: z_mesh.build_mesh(probed_matrix) except BedMeshError as e: @@ -781,7 +870,8 @@ def probe_finalize(self, offsets, positions): z_mesh.set_zero_reference(*self.zero_ref_pos) self.bedmesh.set_mesh(z_mesh) self.gcode.respond_info("Mesh Bed Leveling Complete") - self.bedmesh.save_profile(self._profile_name) + if self._profile_name is not None: + self.bedmesh.save_profile(self._profile_name) def _dump_points(self, probed_pts, corrected_pts, offsets): # logs generated points with offset applied, points received # from the finalize callback, and the list of corrected points @@ -867,7 +957,8 @@ def split(self): class ZMesh: - def __init__(self, params): + def __init__(self, params, name): + self.profile_name = name or "adaptive-%X" % (id(self),) self.probed_matrix = self.mesh_matrix = None self.mesh_params = params self.mesh_offsets = [0., 0.] @@ -916,6 +1007,8 @@ def get_probed_matrix(self): return [[]] def get_mesh_params(self): return self.mesh_params + def get_profile_name(self): + return self.profile_name def print_probed_matrix(self, print_func): if self.probed_matrix is not None: msg = "Mesh Leveling Probed Z positions:\n" @@ -1176,7 +1269,6 @@ def __init__(self, config, bedmesh): self.gcode = self.printer.lookup_object('gcode') self.bedmesh = bedmesh self.profiles = {} - self.current_profile = "" self.incompatible_profiles = [] # Fetch stored profiles from Config stored_profs = config.get_prefix_sections(self.name) @@ -1210,8 +1302,6 @@ def __init__(self, config, bedmesh): desc=self.cmd_BED_MESH_PROFILE_help) def get_profiles(self): return self.profiles - def get_current_profile(self): - return self.current_profile def _check_incompatible_profiles(self): if self.incompatible_profiles: configfile = self.printer.lookup_object('configfile') @@ -1252,7 +1342,6 @@ def save_profile(self, prof_name): profile['points'] = probed_matrix profile['mesh_params'] = collections.OrderedDict(mesh_params) self.profiles = profiles - self.current_profile = prof_name self.bedmesh.update_status() self.gcode.respond_info( "Bed Mesh state has been saved to profile [%s]\n" @@ -1266,12 +1355,11 @@ def load_profile(self, prof_name): "bed_mesh: Unknown profile [%s]" % prof_name) probed_matrix = profile['points'] mesh_params = profile['mesh_params'] - z_mesh = ZMesh(mesh_params) + z_mesh = ZMesh(mesh_params, prof_name) try: z_mesh.build_mesh(probed_matrix) except BedMeshError as e: raise self.gcode.error(str(e)) - self.current_profile = prof_name self.bedmesh.set_mesh(z_mesh) def remove_profile(self, prof_name): if prof_name in self.profiles: diff --git a/klippy/extras/heaters.py b/klippy/extras/heaters.py index 0e9411c9f6eb..1c29aae81a16 100644 --- a/klippy/extras/heaters.py +++ b/klippy/extras/heaters.py @@ -281,8 +281,6 @@ def setup_sensor(self, config): if sensor_type not in self.sensor_factories: raise self.printer.config_error( "Unknown temperature sensor '%s'" % (sensor_type,)) - if sensor_type == 'NTC 100K beta 3950': - config.deprecate('sensor_type', 'NTC 100K beta 3950') return self.sensor_factories[sensor_type](config) def register_sensor(self, config, psensor, gcode_id=None): self.available_sensors.append(config.get_name()) diff --git a/klippy/extras/multi_pin.py b/klippy/extras/multi_pin.py index f5177bd97534..c834ee077f04 100644 --- a/klippy/extras/multi_pin.py +++ b/klippy/extras/multi_pin.py @@ -46,9 +46,9 @@ def setup_cycle_time(self, cycle_time, hardware_pwm=False): def set_digital(self, print_time, value): for mcu_pin in self.mcu_pins: mcu_pin.set_digital(print_time, value) - def set_pwm(self, print_time, value, cycle_time=None): + def set_pwm(self, print_time, value): for mcu_pin in self.mcu_pins: - mcu_pin.set_pwm(print_time, value, cycle_time) + mcu_pin.set_pwm(print_time, value) def load_config_prefix(config): return PrinterMultiPin(config) diff --git a/klippy/extras/output_pin.py b/klippy/extras/output_pin.py index 8b41aca7a454..ef094674c1c1 100644 --- a/klippy/extras/output_pin.py +++ b/klippy/extras/output_pin.py @@ -1,6 +1,6 @@ -# Code to configure miscellaneous chips +# PWM and digital output pin handling # -# Copyright (C) 2017-2021 Kevin O'Connor +# Copyright (C) 2017-2024 Kevin O'Connor # # This file may be distributed under the terms of the GNU GPLv3 license. @@ -12,6 +12,7 @@ class PrinterOutputPin: def __init__(self, config): self.printer = config.get_printer() ppins = self.printer.lookup_object('pins') + # Determine pin type self.is_pwm = config.getboolean('pwm', False) if self.is_pwm: self.mcu_pin = ppins.setup_pin('pwm', config.get('pin')) @@ -20,68 +21,65 @@ def __init__(self, config): hardware_pwm = config.getboolean('hardware_pwm', False) self.mcu_pin.setup_cycle_time(cycle_time, hardware_pwm) self.scale = config.getfloat('scale', 1., above=0.) - self.last_cycle_time = self.default_cycle_time = cycle_time else: self.mcu_pin = ppins.setup_pin('digital_out', config.get('pin')) self.scale = 1. - self.last_cycle_time = self.default_cycle_time = 0. self.last_print_time = 0. - static_value = config.getfloat('static_value', None, - minval=0., maxval=self.scale) + # Support mcu checking for maximum duration self.reactor = self.printer.get_reactor() self.resend_timer = None self.resend_interval = 0. + max_mcu_duration = config.getfloat('maximum_mcu_duration', 0., + minval=0.500, + maxval=MAX_SCHEDULE_TIME) + self.mcu_pin.setup_max_duration(max_mcu_duration) + if max_mcu_duration: + config.deprecate('maximum_mcu_duration') + self.resend_interval = max_mcu_duration - RESEND_HOST_TIME + # Determine start and shutdown values + static_value = config.getfloat('static_value', None, + minval=0., maxval=self.scale) if static_value is not None: - self.mcu_pin.setup_max_duration(0.) - self.last_value = static_value / self.scale - self.mcu_pin.setup_start_value( - self.last_value, self.last_value, True) + config.deprecate('static_value') + self.last_value = self.shutdown_value = static_value / self.scale else: - max_mcu_duration = config.getfloat('maximum_mcu_duration', 0., - minval=0.500, - maxval=MAX_SCHEDULE_TIME) - self.mcu_pin.setup_max_duration(max_mcu_duration) - if max_mcu_duration: - self.resend_interval = max_mcu_duration - RESEND_HOST_TIME - self.last_value = config.getfloat( 'value', 0., minval=0., maxval=self.scale) / self.scale self.shutdown_value = config.getfloat( 'shutdown_value', 0., minval=0., maxval=self.scale) / self.scale - self.mcu_pin.setup_start_value(self.last_value, self.shutdown_value) - pin_name = config.get_name().split()[1] - gcode = self.printer.lookup_object('gcode') - gcode.register_mux_command("SET_PIN", "PIN", pin_name, - self.cmd_SET_PIN, - desc=self.cmd_SET_PIN_help) + self.mcu_pin.setup_start_value(self.last_value, self.shutdown_value) + # Register commands + pin_name = config.get_name().split()[1] + gcode = self.printer.lookup_object('gcode') + gcode.register_mux_command("SET_PIN", "PIN", pin_name, + self.cmd_SET_PIN, + desc=self.cmd_SET_PIN_help) def get_status(self, eventtime): return {'value': self.last_value} - def _set_pin(self, print_time, value, cycle_time, is_resend=False): - if value == self.last_value and cycle_time == self.last_cycle_time: - if not is_resend: - return + def _set_pin(self, print_time, value, is_resend=False): + if value == self.last_value and not is_resend: + return print_time = max(print_time, self.last_print_time + PIN_MIN_TIME) if self.is_pwm: - self.mcu_pin.set_pwm(print_time, value, cycle_time) + self.mcu_pin.set_pwm(print_time, value) else: self.mcu_pin.set_digital(print_time, value) self.last_value = value - self.last_cycle_time = cycle_time self.last_print_time = print_time if self.resend_interval and self.resend_timer is None: self.resend_timer = self.reactor.register_timer( self._resend_current_val, self.reactor.NOW) cmd_SET_PIN_help = "Set the value of an output pin" def cmd_SET_PIN(self, gcmd): + # Read requested value value = gcmd.get_float('VALUE', minval=0., maxval=self.scale) value /= self.scale - cycle_time = gcmd.get_float('CYCLE_TIME', self.default_cycle_time, - above=0., maxval=MAX_SCHEDULE_TIME) if not self.is_pwm and value not in [0., 1.]: raise gcmd.error("Invalid pin value") + # Obtain print_time and apply requested settings toolhead = self.printer.lookup_object('toolhead') toolhead.register_lookahead_callback( - lambda print_time: self._set_pin(print_time, value, cycle_time)) + lambda print_time: self._set_pin(print_time, value)) def _resend_current_val(self, eventtime): if self.last_value == self.shutdown_value: @@ -95,8 +93,7 @@ def _resend_current_val(self, eventtime): if time_diff > 0.: # Reschedule for resend time return systime + time_diff - self._set_pin(print_time + PIN_MIN_TIME, - self.last_value, self.last_cycle_time, True) + self._set_pin(print_time + PIN_MIN_TIME, self.last_value, True) return systime + self.resend_interval def load_config_prefix(config): diff --git a/klippy/extras/pwm_cycle_time.py b/klippy/extras/pwm_cycle_time.py new file mode 100644 index 000000000000..cebbec7512b8 --- /dev/null +++ b/klippy/extras/pwm_cycle_time.py @@ -0,0 +1,123 @@ +# Handle pwm output pins with variable frequency +# +# Copyright (C) 2017-2023 Kevin O'Connor +# +# This file may be distributed under the terms of the GNU GPLv3 license. + +PIN_MIN_TIME = 0.100 +MAX_SCHEDULE_TIME = 5.0 + +class MCU_pwm_cycle: + def __init__(self, pin_params, cycle_time, start_value, shutdown_value): + self._mcu = pin_params['chip'] + self._cycle_time = cycle_time + self._oid = None + self._mcu.register_config_callback(self._build_config) + self._pin = pin_params['pin'] + self._invert = pin_params['invert'] + if self._invert: + start_value = 1. - start_value + shutdown_value = 1. - shutdown_value + self._start_value = max(0., min(1., start_value)) + self._shutdown_value = max(0., min(1., shutdown_value)) + self._last_clock = self._cycle_ticks = 0 + self._set_cmd = self._set_cycle_ticks = None + def _build_config(self): + cmd_queue = self._mcu.alloc_command_queue() + curtime = self._mcu.get_printer().get_reactor().monotonic() + printtime = self._mcu.estimated_print_time(curtime) + self._last_clock = self._mcu.print_time_to_clock(printtime + 0.200) + cycle_ticks = self._mcu.seconds_to_clock(self._cycle_time) + if self._shutdown_value not in [0., 1.]: + raise self._mcu.get_printer().config_error( + "shutdown value must be 0.0 or 1.0 on soft pwm") + if cycle_ticks >= 1<<31: + raise self._mcu.get_printer().config_error( + "PWM pin cycle time too large") + self._mcu.request_move_queue_slot() + self._oid = self._mcu.create_oid() + self._mcu.add_config_cmd( + "config_digital_out oid=%d pin=%s value=%d" + " default_value=%d max_duration=%d" + % (self._oid, self._pin, self._start_value >= 1.0, + self._shutdown_value >= 0.5, 0)) + self._mcu.add_config_cmd( + "set_digital_out_pwm_cycle oid=%d cycle_ticks=%d" + % (self._oid, cycle_ticks)) + self._cycle_ticks = cycle_ticks + svalue = int(self._start_value * cycle_ticks + 0.5) + self._mcu.add_config_cmd( + "queue_digital_out oid=%d clock=%d on_ticks=%d" + % (self._oid, self._last_clock, svalue), is_init=True) + self._set_cmd = self._mcu.lookup_command( + "queue_digital_out oid=%c clock=%u on_ticks=%u", cq=cmd_queue) + self._set_cycle_ticks = self._mcu.lookup_command( + "set_digital_out_pwm_cycle oid=%c cycle_ticks=%u", cq=cmd_queue) + def set_pwm_cycle(self, print_time, value, cycle_time): + clock = self._mcu.print_time_to_clock(print_time) + minclock = self._last_clock + # Send updated cycle_time if necessary + cycle_ticks = self._mcu.seconds_to_clock(cycle_time) + if cycle_ticks != self._cycle_ticks: + if cycle_ticks >= 1<<31: + raise self._mcu.get_printer().command_error( + "PWM cycle time too large") + self._set_cycle_ticks.send([self._oid, cycle_ticks], + minclock=minclock, reqclock=clock) + self._cycle_ticks = cycle_ticks + # Send pwm update + if self._invert: + value = 1. - value + v = int(max(0., min(1., value)) * float(self._cycle_ticks) + 0.5) + self._set_cmd.send([self._oid, clock, v], + minclock=self._last_clock, reqclock=clock) + self._last_clock = clock + +class PrinterOutputPWMCycle: + def __init__(self, config): + self.printer = config.get_printer() + self.last_print_time = 0. + cycle_time = config.getfloat('cycle_time', 0.100, above=0., + maxval=MAX_SCHEDULE_TIME) + self.last_cycle_time = self.default_cycle_time = cycle_time + # Determine start and shutdown values + self.scale = config.getfloat('scale', 1., above=0.) + self.last_value = config.getfloat( + 'value', 0., minval=0., maxval=self.scale) / self.scale + self.shutdown_value = config.getfloat( + 'shutdown_value', 0., minval=0., maxval=self.scale) / self.scale + # Create pwm pin object + ppins = self.printer.lookup_object('pins') + pin_params = ppins.lookup_pin(config.get('pin'), can_invert=True) + self.mcu_pin = MCU_pwm_cycle(pin_params, cycle_time, + self.last_value, self.shutdown_value) + # Register commands + pin_name = config.get_name().split()[1] + gcode = self.printer.lookup_object('gcode') + gcode.register_mux_command("SET_PIN", "PIN", pin_name, + self.cmd_SET_PIN, + desc=self.cmd_SET_PIN_help) + def get_status(self, eventtime): + return {'value': self.last_value} + def _set_pin(self, print_time, value, cycle_time): + if value == self.last_value and cycle_time == self.last_cycle_time: + return + print_time = max(print_time, self.last_print_time + PIN_MIN_TIME) + self.mcu_pin.set_pwm_cycle(print_time, value, cycle_time) + self.last_value = value + self.last_cycle_time = cycle_time + self.last_print_time = print_time + cmd_SET_PIN_help = "Set the value of an output pin" + def cmd_SET_PIN(self, gcmd): + # Read requested value + value = gcmd.get_float('VALUE', minval=0., maxval=self.scale) + value /= self.scale + cycle_time = gcmd.get_float('CYCLE_TIME', self.default_cycle_time, + above=0., maxval=MAX_SCHEDULE_TIME) + # Obtain print_time and apply requested settings + toolhead = self.printer.lookup_object('toolhead') + toolhead.register_lookahead_callback( + lambda print_time: self._set_pin(print_time, value, cycle_time)) + +def load_config_prefix(config): + return PrinterOutputPWMCycle(config) diff --git a/klippy/extras/replicape.py b/klippy/extras/replicape.py index 4c3762974654..ab501cafc427 100644 --- a/klippy/extras/replicape.py +++ b/klippy/extras/replicape.py @@ -30,7 +30,6 @@ def __init__(self, replicape, channel, pin_type, pin_params): self._invert = pin_params['invert'] self._start_value = self._shutdown_value = float(self._invert) self._is_enable = not not self._start_value - self._is_static = False self._last_clock = 0 self._pwm_max = 0. self._set_cmd = None @@ -44,28 +43,18 @@ def setup_cycle_time(self, cycle_time, hardware_pwm=False): if cycle_time != self._cycle_time: logging.info("Ignoring pca9685 cycle time of %.6f (using %.6f)", cycle_time, self._cycle_time) - def setup_start_value(self, start_value, shutdown_value, is_static=False): - if is_static and start_value != shutdown_value: - raise pins.error("Static pin can not have shutdown value") + def setup_start_value(self, start_value, shutdown_value): if self._invert: start_value = 1. - start_value shutdown_value = 1. - shutdown_value self._start_value = max(0., min(1., start_value)) self._shutdown_value = max(0., min(1., shutdown_value)) - self._is_static = is_static self._replicape.note_pwm_start_value( self._channel, self._start_value, self._shutdown_value) self._is_enable = not not self._start_value def _build_config(self): self._pwm_max = self._mcu.get_constant_float("PCA9685_MAX") cycle_ticks = self._mcu.seconds_to_clock(self._cycle_time) - if self._is_static: - self._mcu.add_config_cmd( - "set_pca9685_out bus=%d addr=%d channel=%d" - " cycle_ticks=%d value=%d" % ( - self._bus, self._address, self._channel, - cycle_ticks, self._start_value * self._pwm_max)) - return self._mcu.request_move_queue_slot() self._oid = self._mcu.create_oid() self._mcu.add_config_cmd( @@ -78,7 +67,7 @@ def _build_config(self): cmd_queue = self._mcu.alloc_command_queue() self._set_cmd = self._mcu.lookup_command( "queue_pca9685_out oid=%c clock=%u value=%hu", cq=cmd_queue) - def set_pwm(self, print_time, value, cycle_time=None): + def set_pwm(self, print_time, value): clock = self._mcu.print_time_to_clock(print_time) if self._invert: value = 1. - value diff --git a/klippy/extras/resonance_tester.py b/klippy/extras/resonance_tester.py index d5f43b77f531..d249412cadcc 100644 --- a/klippy/extras/resonance_tester.py +++ b/klippy/extras/resonance_tester.py @@ -1,6 +1,6 @@ # A utility class to test resonances of the printer # -# Copyright (C) 2020 Dmitry Butyugin +# Copyright (C) 2020-2024 Dmitry Butyugin # # This file may be distributed under the terms of the GNU GPLv3 license. import logging, math, os, time @@ -52,7 +52,7 @@ def __init__(self, config): self.min_freq = config.getfloat('min_freq', 5., minval=1.) # Defaults are such that max_freq * accel_per_hz == 10000 (max_accel) self.max_freq = config.getfloat('max_freq', 10000. / 75., - minval=self.min_freq, maxval=200.) + minval=self.min_freq, maxval=300.) self.accel_per_hz = config.getfloat('accel_per_hz', 75., above=0.) self.hz_per_sec = config.getfloat('hz_per_sec', 1., minval=0.1, maxval=2.) @@ -64,7 +64,7 @@ def get_start_test_points(self): def prepare_test(self, gcmd): self.freq_start = gcmd.get_float("FREQ_START", self.min_freq, minval=1.) self.freq_end = gcmd.get_float("FREQ_END", self.max_freq, - minval=self.freq_start, maxval=200.) + minval=self.freq_start, maxval=300.) self.hz_per_sec = gcmd.get_float("HZ_PER_SEC", self.hz_per_sec, above=0., maxval=2.) def run_test(self, axis, gcmd): @@ -114,6 +114,8 @@ def run_test(self, axis, gcmd): if input_shaper is not None: input_shaper.enable_shaping() gcmd.respond_info("Re-enabled [input_shaper]") + def get_max_freq(self): + return self.freq_end class ResonanceTester: def __init__(self, config): @@ -217,6 +219,8 @@ def _parse_chips(self, accel_chips): chip = self.printer.lookup_object(chip_lookup_name) parsed_chips.append(chip) return parsed_chips + def _get_max_calibration_freq(self): + return 1.5 * self.test.get_max_freq() cmd_TEST_RESONANCES_help = ("Runs the resonance test for a specifed axis") def cmd_TEST_RESONANCES(self, gcmd): # Parse parameters @@ -261,9 +265,9 @@ def cmd_TEST_RESONANCES(self, gcmd): raw_name_suffix=name_suffix if raw_output else None, accel_chips=accel_chips, test_point=test_point)[axis] if csv_output: - csv_name = self.save_calibration_data('resonances', name_suffix, - helper, axis, data, - point=test_point) + csv_name = self.save_calibration_data( + 'resonances', name_suffix, helper, axis, data, + point=test_point, max_freq=self._get_max_calibration_freq()) gcmd.respond_info( "Resonances data written to %s file" % (csv_name,)) cmd_SHAPER_CALIBRATE_help = ( @@ -302,8 +306,14 @@ def cmd_SHAPER_CALIBRATE(self, gcmd): "Calculating the best input shaper parameters for %s axis" % (axis_name,)) calibration_data[axis].normalize_to_frequencies() + systime = self.printer.get_reactor().monotonic() + toolhead = self.printer.lookup_object('toolhead') + toolhead_info = toolhead.get_status(systime) + scv = toolhead_info['square_corner_velocity'] + max_freq = self._get_max_calibration_freq() best_shaper, all_shapers = helper.find_best_shaper( - calibration_data[axis], max_smoothing, gcmd.respond_info) + calibration_data[axis], max_smoothing=max_smoothing, + scv=scv, max_freq=max_freq, logger=gcmd.respond_info) gcmd.respond_info( "Recommended shaper_type_%s = %s, shaper_freq_%s = %.1f Hz" % (axis_name, best_shaper.name, @@ -315,7 +325,7 @@ def cmd_SHAPER_CALIBRATE(self, gcmd): best_shaper.name, best_shaper.freq) csv_name = self.save_calibration_data( 'calibration_data', name_suffix, helper, axis, - calibration_data[axis], all_shapers) + calibration_data[axis], all_shapers, max_freq=max_freq) gcmd.respond_info( "Shaper calibration data written to %s file" % (csv_name,)) gcmd.respond_info( @@ -361,10 +371,10 @@ def get_filename(self, base, name_suffix, axis=None, def save_calibration_data(self, base_name, name_suffix, shaper_calibrate, axis, calibration_data, - all_shapers=None, point=None): + all_shapers=None, point=None, max_freq=None): output = self.get_filename(base_name, name_suffix, axis, point) shaper_calibrate.save_calibration_data(output, calibration_data, - all_shapers) + all_shapers, max_freq) return output def load_config(config): diff --git a/klippy/extras/shaper_calibrate.py b/klippy/extras/shaper_calibrate.py index af77845c5cd5..6891fefb3bf9 100644 --- a/klippy/extras/shaper_calibrate.py +++ b/klippy/extras/shaper_calibrate.py @@ -1,6 +1,6 @@ # Automatic calibration of input shapers # -# Copyright (C) 2020 Dmitry Butyugin +# Copyright (C) 2020-2024 Dmitry Butyugin # # This file may be distributed under the terms of the GNU GPLv3 license. import collections, importlib, logging, math, multiprocessing, traceback @@ -227,34 +227,49 @@ def _get_shaper_smoothing(self, shaper, accel=5000, scv=5.): offset_180 *= inv_D return max(offset_90, offset_180) - def fit_shaper(self, shaper_cfg, calibration_data, max_smoothing): + def fit_shaper(self, shaper_cfg, calibration_data, shaper_freqs, + damping_ratio, scv, max_smoothing, test_damping_ratios, + max_freq): np = self.numpy - test_freqs = np.arange(shaper_cfg.min_freq, MAX_SHAPER_FREQ, .2) + damping_ratio = damping_ratio or shaper_defs.DEFAULT_DAMPING_RATIO + test_damping_ratios = test_damping_ratios or TEST_DAMPING_RATIOS + + if not shaper_freqs: + shaper_freqs = (None, None, None) + if isinstance(shaper_freqs, tuple): + freq_end = shaper_freqs[1] or MAX_SHAPER_FREQ + freq_start = min(shaper_freqs[0] or shaper_cfg.min_freq, + freq_end - 1e-7) + freq_step = shaper_freqs[2] or .2 + test_freqs = np.arange(freq_start, freq_end, freq_step) + else: + test_freqs = np.array(shaper_freqs) + + max_freq = max(max_freq or MAX_FREQ, test_freqs.max()) freq_bins = calibration_data.freq_bins - psd = calibration_data.psd_sum[freq_bins <= MAX_FREQ] - freq_bins = freq_bins[freq_bins <= MAX_FREQ] + psd = calibration_data.psd_sum[freq_bins <= max_freq] + freq_bins = freq_bins[freq_bins <= max_freq] best_res = None results = [] for test_freq in test_freqs[::-1]: shaper_vibrations = 0. shaper_vals = np.zeros(shape=freq_bins.shape) - shaper = shaper_cfg.init_func( - test_freq, shaper_defs.DEFAULT_DAMPING_RATIO) - shaper_smoothing = self._get_shaper_smoothing(shaper) + shaper = shaper_cfg.init_func(test_freq, damping_ratio) + shaper_smoothing = self._get_shaper_smoothing(shaper, scv=scv) if max_smoothing and shaper_smoothing > max_smoothing and best_res: return best_res # Exact damping ratio of the printer is unknown, pessimizing # remaining vibrations over possible damping values - for dr in TEST_DAMPING_RATIOS: + for dr in test_damping_ratios: vibrations, vals = self._estimate_remaining_vibrations( shaper, dr, freq_bins, psd) shaper_vals = np.maximum(shaper_vals, vals) if vibrations > shaper_vibrations: shaper_vibrations = vibrations - max_accel = self.find_shaper_max_accel(shaper) + max_accel = self.find_shaper_max_accel(shaper, scv) # The score trying to minimize vibrations, but also accounting # the growth of smoothing. The formula itself does not have any # special meaning, it simply shows good results on real user data @@ -278,6 +293,8 @@ def fit_shaper(self, shaper_cfg, calibration_data, max_smoothing): def _bisect(self, func): left = right = 1. + if not func(1e-9): + return 0. while not func(left): right = left left *= .5 @@ -292,22 +309,27 @@ def _bisect(self, func): right = middle return left - def find_shaper_max_accel(self, shaper): + def find_shaper_max_accel(self, shaper, scv): # Just some empirically chosen value which produces good projections # for max_accel without much smoothing TARGET_SMOOTHING = 0.12 max_accel = self._bisect(lambda test_accel: self._get_shaper_smoothing( - shaper, test_accel) <= TARGET_SMOOTHING) + shaper, test_accel, scv) <= TARGET_SMOOTHING) return max_accel - def find_best_shaper(self, calibration_data, max_smoothing, logger=None): + def find_best_shaper(self, calibration_data, shapers=None, + damping_ratio=None, scv=None, shaper_freqs=None, + max_smoothing=None, test_damping_ratios=None, + max_freq=None, logger=None): best_shaper = None all_shapers = [] + shapers = shapers or AUTOTUNE_SHAPERS for shaper_cfg in shaper_defs.INPUT_SHAPERS: - if shaper_cfg.name not in AUTOTUNE_SHAPERS: + if shaper_cfg.name not in shapers: continue shaper = self.background_process_exec(self.fit_shaper, ( - shaper_cfg, calibration_data, max_smoothing)) + shaper_cfg, calibration_data, shaper_freqs, damping_ratio, + scv, max_smoothing, test_damping_ratios, max_freq)) if logger is not None: logger("Fitted shaper '%s' frequency = %.1f Hz " "(vibrations = %.1f%%, smoothing ~= %.3f)" % ( @@ -346,8 +368,10 @@ def apply_params(self, input_shaper, axis, shaper_name, shaper_freq): "SHAPER_TYPE_" + axis: shaper_name, "SHAPER_FREQ_" + axis: shaper_freq})) - def save_calibration_data(self, output, calibration_data, shapers=None): + def save_calibration_data(self, output, calibration_data, shapers=None, + max_freq=None): try: + max_freq = max_freq or MAX_FREQ with open(output, "w") as csvfile: csvfile.write("freq,psd_x,psd_y,psd_z,psd_xyz") if shapers: @@ -356,7 +380,7 @@ def save_calibration_data(self, output, calibration_data, shapers=None): csvfile.write("\n") num_freqs = calibration_data.freq_bins.shape[0] for i in range(num_freqs): - if calibration_data.freq_bins[i] >= MAX_FREQ: + if calibration_data.freq_bins[i] >= max_freq: break csvfile.write("%.1f,%.3e,%.3e,%.3e,%.3e" % ( calibration_data.freq_bins[i], diff --git a/klippy/extras/static_digital_output.py b/klippy/extras/static_digital_output.py index ce20937204c5..2fa0bb3f52c6 100644 --- a/klippy/extras/static_digital_output.py +++ b/klippy/extras/static_digital_output.py @@ -10,8 +10,10 @@ def __init__(self, config): ppins = printer.lookup_object('pins') pin_list = config.getlist('pins') for pin_desc in pin_list: - mcu_pin = ppins.setup_pin('digital_out', pin_desc) - mcu_pin.setup_start_value(1, 1, True) + pin_params = ppins.lookup_pin(pin_desc, can_invert=True) + mcu = pin_params['chip'] + mcu.add_config_cmd("set_digital_out pin=%s value=%d" + % (pin_params['pin'], not pin_params['invert'])) def load_config_prefix(config): return PrinterStaticDigitalOut(config) diff --git a/klippy/extras/sx1509.py b/klippy/extras/sx1509.py index 8b19dda801db..3bfecb7831c3 100644 --- a/klippy/extras/sx1509.py +++ b/klippy/extras/sx1509.py @@ -104,7 +104,6 @@ def __init__(self, sx1509, pin_params): self._invert = pin_params['invert'] self._mcu.register_config_callback(self._build_config) self._start_value = self._shutdown_value = self._invert - self._is_static = False self._max_duration = 2. self._set_cmd = self._clear_cmd = None # Set direction to output @@ -116,12 +115,9 @@ def get_mcu(self): return self._mcu def setup_max_duration(self, max_duration): self._max_duration = max_duration - def setup_start_value(self, start_value, shutdown_value, is_static=False): - if is_static and start_value != shutdown_value: - raise pins.error("Static pin can not have shutdown value") + def setup_start_value(self, start_value, shutdown_value): self._start_value = (not not start_value) ^ self._invert self._shutdown_value = self._invert - self._is_static = is_static # We need to set the start value here so the register is # updated before the SX1509 class writes it. if self._start_value: @@ -148,7 +144,6 @@ def __init__(self, sx1509, pin_params): self._invert = pin_params['invert'] self._mcu.register_config_callback(self._build_config) self._start_value = self._shutdown_value = float(self._invert) - self._is_static = False self._max_duration = 2. self._hardware_pwm = False self._pwm_max = 0. @@ -182,16 +177,13 @@ def setup_max_duration(self, max_duration): def setup_cycle_time(self, cycle_time, hardware_pwm=False): self._cycle_time = cycle_time self._hardware_pwm = hardware_pwm - def setup_start_value(self, start_value, shutdown_value, is_static=False): - if is_static and start_value != shutdown_value: - raise pins.error("Static pin can not have shutdown value") + def setup_start_value(self, start_value, shutdown_value): if self._invert: start_value = 1. - start_value shutdown_value = 1. - shutdown_value self._start_value = max(0., min(1., start_value)) self._shutdown_value = max(0., min(1., shutdown_value)) - self._is_static = is_static - def set_pwm(self, print_time, value, cycle_time=None): + def set_pwm(self, print_time, value): self._sx1509.set_register(self._i_on_reg, ~int(255 * value) if not self._invert else int(255 * value) & 0xFF) diff --git a/klippy/extras/temperature_sensors.cfg b/klippy/extras/temperature_sensors.cfg index 96aa996401ae..107fcd24b663 100644 --- a/klippy/extras/temperature_sensors.cfg +++ b/klippy/extras/temperature_sensors.cfg @@ -102,12 +102,6 @@ temperature1: 25 resistance1: 100000 beta: 3974 -# Definition inherent from name. This sensor is deprecated! -[thermistor NTC 100K beta 3950] -temperature1: 25 -resistance1: 100000 -beta: 3950 - # Definition from description of Marlin "thermistor 75" [thermistor NTC 100K MGB18-104F39050L32] temperature1: 25 diff --git a/klippy/extras/virtual_sdcard.py b/klippy/extras/virtual_sdcard.py index 31f283ff256b..1bb914ab22c4 100644 --- a/klippy/extras/virtual_sdcard.py +++ b/klippy/extras/virtual_sdcard.py @@ -258,7 +258,7 @@ def work_handler(self, eventtime): # Dispatch command self.cmd_from_sd = True line = lines.pop() - next_file_position = self.file_position + len(line) + 1 + next_file_position = self.file_position + len(line.encode()) + 1 self.next_file_position = next_file_position try: self.gcode.run_script(line) diff --git a/klippy/kinematics/extruder.py b/klippy/kinematics/extruder.py index 4fe041c5b5e3..6924003783e8 100644 --- a/klippy/kinematics/extruder.py +++ b/klippy/kinematics/extruder.py @@ -38,12 +38,6 @@ def __init__(self, config): gcode.register_mux_command("SYNC_EXTRUDER_MOTION", "EXTRUDER", self.name, self.cmd_SYNC_EXTRUDER_MOTION, desc=self.cmd_SYNC_EXTRUDER_MOTION_help) - gcode.register_mux_command("SET_EXTRUDER_STEP_DISTANCE", "EXTRUDER", - self.name, self.cmd_SET_E_STEP_DISTANCE, - desc=self.cmd_SET_E_STEP_DISTANCE_help) - gcode.register_mux_command("SYNC_STEPPER_TO_EXTRUDER", "STEPPER", - self.name, self.cmd_SYNC_STEPPER_TO_EXTRUDER, - desc=self.cmd_SYNC_STEPPER_TO_EXTRUDER_help) def _handle_connect(self): toolhead = self.printer.lookup_object('toolhead') toolhead.register_step_generator(self.stepper.generate_steps) @@ -133,24 +127,6 @@ def cmd_SYNC_EXTRUDER_MOTION(self, gcmd): self.sync_to_extruder(ename) gcmd.respond_info("Extruder '%s' now syncing with '%s'" % (self.name, ename)) - cmd_SET_E_STEP_DISTANCE_help = "Set extruder step distance" - def cmd_SET_E_STEP_DISTANCE(self, gcmd): - step_dist = gcmd.get_float('DISTANCE', None, above=0.) - if step_dist is not None: - toolhead = self.printer.lookup_object('toolhead') - toolhead.flush_step_generation() - rd, steps_per_rotation = self.stepper.get_rotation_distance() - self.stepper.set_rotation_distance(step_dist * steps_per_rotation) - else: - step_dist = self.stepper.get_step_dist() - gcmd.respond_info("Extruder '%s' step distance set to %0.6f" - % (self.name, step_dist)) - cmd_SYNC_STEPPER_TO_EXTRUDER_help = "Set extruder stepper" - def cmd_SYNC_STEPPER_TO_EXTRUDER(self, gcmd): - ename = gcmd.get('EXTRUDER') - self.sync_to_extruder(ename) - gcmd.respond_info("Extruder '%s' now syncing with '%s'" - % (self.name, ename)) # Tracking for hotend heater, extrusion motion queue, and extruder stepper class PrinterExtruder: @@ -159,14 +135,9 @@ def __init__(self, config, extruder_num): self.name = config.get_name() self.last_position = 0. # Setup hotend heater - shared_heater = config.get('shared_heater', None) pheaters = self.printer.load_object(config, 'heaters') gcode_id = 'T%d' % (extruder_num,) - if shared_heater is None: - self.heater = pheaters.setup_heater(config, gcode_id) - else: - config.deprecate('shared_heater') - self.heater = pheaters.lookup_heater(shared_heater) + self.heater = pheaters.setup_heater(config, gcode_id) # Setup kinematic checks self.nozzle_diameter = config.getfloat('nozzle_diameter', above=0.) filament_diameter = config.getfloat( diff --git a/klippy/mcu.py b/klippy/mcu.py index cfc389e76912..f9b547c94479 100644 --- a/klippy/mcu.py +++ b/klippy/mcu.py @@ -335,7 +335,6 @@ def __init__(self, mcu, pin_params): self._pin = pin_params['pin'] self._invert = pin_params['invert'] self._start_value = self._shutdown_value = self._invert - self._is_static = False self._max_duration = 2. self._last_clock = 0 self._set_cmd = None @@ -343,17 +342,10 @@ def get_mcu(self): return self._mcu def setup_max_duration(self, max_duration): self._max_duration = max_duration - def setup_start_value(self, start_value, shutdown_value, is_static=False): - if is_static and start_value != shutdown_value: - raise pins.error("Static pin can not have shutdown value") + def setup_start_value(self, start_value, shutdown_value): self._start_value = (not not start_value) ^ self._invert self._shutdown_value = (not not shutdown_value) ^ self._invert - self._is_static = is_static def _build_config(self): - if self._is_static: - self._mcu.add_config_cmd("set_digital_out pin=%s value=%d" - % (self._pin, self._start_value)) - return if self._max_duration and self._start_value != self._shutdown_value: raise pins.error("Pin with max duration must have start" " value equal to shutdown value") @@ -389,10 +381,9 @@ def __init__(self, mcu, pin_params): self._pin = pin_params['pin'] self._invert = pin_params['invert'] self._start_value = self._shutdown_value = float(self._invert) - self._is_static = False - self._last_clock = self._last_cycle_ticks = 0 + self._last_clock = 0 self._pwm_max = 0. - self._set_cmd = self._set_cycle_ticks = None + self._set_cmd = None def get_mcu(self): return self._mcu def setup_max_duration(self, max_duration): @@ -400,15 +391,12 @@ def setup_max_duration(self, max_duration): def setup_cycle_time(self, cycle_time, hardware_pwm=False): self._cycle_time = cycle_time self._hardware_pwm = hardware_pwm - def setup_start_value(self, start_value, shutdown_value, is_static=False): - if is_static and start_value != shutdown_value: - raise pins.error("Static pin can not have shutdown value") + def setup_start_value(self, start_value, shutdown_value): if self._invert: start_value = 1. - start_value shutdown_value = 1. - shutdown_value self._start_value = max(0., min(1., start_value)) self._shutdown_value = max(0., min(1., shutdown_value)) - self._is_static = is_static def _build_config(self): if self._max_duration and self._start_value != self._shutdown_value: raise pins.error("Pin with max duration must have start" @@ -423,12 +411,6 @@ def _build_config(self): raise pins.error("PWM pin max duration too large") if self._hardware_pwm: self._pwm_max = self._mcu.get_constant_float("PWM_MAX") - if self._is_static: - self._mcu.add_config_cmd( - "set_pwm_out pin=%s cycle_ticks=%d value=%d" - % (self._pin, cycle_ticks, - self._start_value * self._pwm_max)) - return self._mcu.request_move_queue_slot() self._oid = self._mcu.create_oid() self._mcu.add_config_cmd( @@ -447,10 +429,6 @@ def _build_config(self): # Software PWM if self._shutdown_value not in [0., 1.]: raise pins.error("shutdown value must be 0.0 or 1.0 on soft pwm") - if self._is_static: - self._mcu.add_config_cmd("set_digital_out pin=%s value=%d" - % (self._pin, self._start_value >= 0.5)) - return if cycle_ticks >= 1<<31: raise pins.error("PWM pin cycle time too large") self._mcu.request_move_queue_slot() @@ -463,40 +441,21 @@ def _build_config(self): self._mcu.add_config_cmd( "set_digital_out_pwm_cycle oid=%d cycle_ticks=%d" % (self._oid, cycle_ticks)) - self._last_cycle_ticks = cycle_ticks + self._pwm_max = float(cycle_ticks) svalue = int(self._start_value * cycle_ticks + 0.5) self._mcu.add_config_cmd( "queue_digital_out oid=%d clock=%d on_ticks=%d" % (self._oid, self._last_clock, svalue), is_init=True) self._set_cmd = self._mcu.lookup_command( "queue_digital_out oid=%c clock=%u on_ticks=%u", cq=cmd_queue) - self._set_cycle_ticks = self._mcu.lookup_command( - "set_digital_out_pwm_cycle oid=%c cycle_ticks=%u", cq=cmd_queue) - def set_pwm(self, print_time, value, cycle_time=None): - clock = self._mcu.print_time_to_clock(print_time) - minclock = self._last_clock - self._last_clock = clock + def set_pwm(self, print_time, value): if self._invert: value = 1. - value - if self._hardware_pwm: - v = int(max(0., min(1., value)) * self._pwm_max + 0.5) - self._set_cmd.send([self._oid, clock, v], - minclock=minclock, reqclock=clock) - return - # Soft pwm update - if cycle_time is None: - cycle_time = self._cycle_time - cycle_ticks = self._mcu.seconds_to_clock(cycle_time) - if cycle_ticks != self._last_cycle_ticks: - if cycle_ticks >= 1<<31: - raise self._mcu.get_printer().command_error( - "PWM cycle time too large") - self._set_cycle_ticks.send([self._oid, cycle_ticks], - minclock=minclock, reqclock=clock) - self._last_cycle_ticks = cycle_ticks - on_ticks = int(max(0., min(1., value)) * float(cycle_ticks) + 0.5) - self._set_cmd.send([self._oid, clock, on_ticks], - minclock=minclock, reqclock=clock) + v = int(max(0., min(1., value)) * self._pwm_max + 0.5) + clock = self._mcu.print_time_to_clock(print_time) + self._set_cmd.send([self._oid, clock, v], + minclock=self._last_clock, reqclock=clock) + self._last_clock = clock class MCU_adc: def __init__(self, mcu, pin_params): diff --git a/scripts/calibrate_shaper.py b/scripts/calibrate_shaper.py index 8a0fcdf00b6b..b56ce5daa836 100755 --- a/scripts/calibrate_shaper.py +++ b/scripts/calibrate_shaper.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # Shaper auto-calibration script # -# Copyright (C) 2020 Dmitry Butyugin +# Copyright (C) 2020-2024 Dmitry Butyugin # Copyright (C) 2020 Kevin O'Connor # # This file may be distributed under the terms of the GNU GPLv3 license. @@ -40,7 +40,9 @@ def parse_log(logname): ###################################################################### # Find the best shaper parameters -def calibrate_shaper(datas, csv_output, max_smoothing): +def calibrate_shaper(datas, csv_output, *, shapers, damping_ratio, scv, + shaper_freqs, max_smoothing, test_damping_ratios, + max_freq): helper = shaper_calibrate.ShaperCalibrate(printer=None) if isinstance(datas[0], shaper_calibrate.CalibrationData): calibration_data = datas[0] @@ -52,8 +54,17 @@ def calibrate_shaper(datas, csv_output, max_smoothing): for data in datas[1:]: calibration_data.add_data(helper.process_accelerometer_data(data)) calibration_data.normalize_to_frequencies() + + shaper, all_shapers = helper.find_best_shaper( - calibration_data, max_smoothing, print) + calibration_data, shapers=shapers, damping_ratio=damping_ratio, + scv=scv, shaper_freqs=shaper_freqs, max_smoothing=max_smoothing, + test_damping_ratios=test_damping_ratios, max_freq=max_freq, + logger=print) + if not shaper: + print("No recommended shaper, possibly invalid value for --shapers=%s" % + (','.join(shapers))) + return None, None, None print("Recommended shaper is %s @ %.1f Hz" % (shaper.name, shaper.freq)) if csv_output is not None: helper.save_calibration_data( @@ -140,28 +151,94 @@ def main(): opts.add_option("-c", "--csv", type="string", dest="csv", default=None, help="filename of output csv file") opts.add_option("-f", "--max_freq", type="float", default=200., - help="maximum frequency to graph") - opts.add_option("-s", "--max_smoothing", type="float", default=None, - help="maximum shaper smoothing to allow") + help="maximum frequency to plot") + opts.add_option("-s", "--max_smoothing", type="float", dest="max_smoothing", + default=None, help="maximum shaper smoothing to allow") + opts.add_option("--scv", "--square_corner_velocity", type="float", + dest="scv", default=5., help="square corner velocity") + opts.add_option("--shaper_freq", type="string", dest="shaper_freq", + default=None, help="shaper frequency(-ies) to test, " + + "either a comma-separated list of floats, or a range in " + + "the format [start]:end[:step]") + opts.add_option("--shapers", type="string", dest="shapers", default=None, + help="a comma-separated list of shapers to test") + opts.add_option("--damping_ratio", type="float", dest="damping_ratio", + 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 " + + "input shaper for") options, args = opts.parse_args() if len(args) < 1: opts.error("Incorrect number of arguments") if options.max_smoothing is not None and options.max_smoothing < 0.05: opts.error("Too small max_smoothing specified (must be at least 0.05)") + max_freq = options.max_freq + if options.shaper_freq is None: + shaper_freqs = [] + elif options.shaper_freq.find(':') >= 0: + freq_start = None + freq_end = None + freq_step = None + try: + freqs_parsed = options.shaper_freq.partition(':') + if freqs_parsed[0]: + freq_start = float(freqs_parsed[0]) + freqs_parsed = freqs_parsed[-1].partition(':') + freq_end = float(freqs_parsed[0]) + if freq_start and freq_start > freq_end: + opts.error("Invalid --shaper_freq param: start range larger " + + "than its end") + if freqs_parsed[-1].find(':') >= 0: + opts.error("Invalid --shaper_freq param format") + if freqs_parsed[-1]: + freq_step = float(freqs_parsed[-1]) + except ValueError: + opts.error("--shaper_freq param does not specify correct range " + + "in the format [start]:end[:step]") + shaper_freqs = (freq_start, freq_end, freq_step) + max_freq = max(max_freq, freq_end * 4./3.) + else: + try: + shaper_freqs = [float(s) for s in options.shaper_freq.split(',')] + except ValueError: + opts.error("invalid floating point value in --shaper_freq param") + max_freq = max(max_freq, max(shaper_freqs) * 4./3.) + 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 + if options.shapers is None: + shapers = None + else: + shapers = options.shapers.lower().split(',') + # Parse data datas = [parse_log(fn) for fn in args] # Calibrate shaper and generate outputs selected_shaper, shapers, calibration_data = calibrate_shaper( - datas, options.csv, options.max_smoothing) + datas, options.csv, shapers=shapers, + damping_ratio=options.damping_ratio, + scv=options.scv, shaper_freqs=shaper_freqs, + max_smoothing=options.max_smoothing, + test_damping_ratios=test_damping_ratios, + max_freq=max_freq) + if selected_shaper is None: + return if not options.csv or options.output: # Draw graph setup_matplotlib(options.output is not None) fig = plot_freq_response(args, calibration_data, shapers, - selected_shaper, options.max_freq) + selected_shaper, max_freq) # Show graph if options.output is None: diff --git a/src/avr/Kconfig b/src/avr/Kconfig index f1ad54395442..d4d78d279392 100644 --- a/src/avr/Kconfig +++ b/src/avr/Kconfig @@ -11,7 +11,7 @@ config AVR_SELECT select HAVE_GPIO_I2C select HAVE_GPIO_HARD_PWM select HAVE_STRICT_TIMING - select HAVE_LIMITED_CODE_SIZE if MACH_atmega168 || MACH_atmega328 || MACH_atmega328p + select HAVE_LIMITED_CODE_SIZE if MACH_atmega168 || MACH_atmega328 || MACH_atmega328p || MACH_atmega32u4 config BOARD_DIRECTORY string diff --git a/src/generic/armcm_boot.c b/src/generic/armcm_boot.c index f83ca60de0ff..9d2ce0bbfe5e 100644 --- a/src/generic/armcm_boot.c +++ b/src/generic/armcm_boot.c @@ -22,7 +22,31 @@ extern uint32_t _stack_end; * Basic interrupt handlers ****************************************************************/ -static void __noreturn +// Inlined version of memset (to avoid function calls during intial boot code) +static void __always_inline +boot_memset(void *s, int c, size_t n) +{ + volatile uint32_t *p = s; + while (n) { + *p++ = c; + n -= sizeof(*p); + } +} + +// Inlined version of memcpy (to avoid function calls during intial boot code) +static void __always_inline +boot_memcpy(void *dest, const void *src, size_t n) +{ + const uint32_t *s = src; + volatile uint32_t *d = dest; + while (n) { + *d++ = *s++; + n -= sizeof(*d); + } +} + +// Main initialization code (called from ResetHandler below) +static void __noreturn __section(".text.armcm_boot.stage_two") reset_handler_stage_two(void) { int i; @@ -60,10 +84,10 @@ reset_handler_stage_two(void) // Copy global variables from flash to ram uint32_t count = (&_data_end - &_data_start) * 4; - __builtin_memcpy(&_data_start, &_data_flash, count); + boot_memcpy(&_data_start, &_data_flash, count); // Clear the bss segment - __builtin_memset(&_bss_start, 0, (&_bss_end - &_bss_start) * 4); + boot_memset(&_bss_start, 0, (&_bss_end - &_bss_start) * 4); barrier(); @@ -80,7 +104,7 @@ reset_handler_stage_two(void) // Initial code entry point - invoked by the processor after a reset // Reset interrupts and stack to take control from bootloaders -void +void __section(".text.armcm_boot.stage_one") ResetHandler(void) { __disable_irq(); diff --git a/src/rp2040/Makefile b/src/rp2040/Makefile index 71ed90a0cc0a..64199014009a 100644 --- a/src/rp2040/Makefile +++ b/src/rp2040/Makefile @@ -55,7 +55,7 @@ $(OUT)klipper.bin: $(OUT)klipper.elf $(Q)$(OBJCOPY) -O binary $< $@ rptarget-$(CONFIG_RP2040_HAVE_BOOTLOADER) := $(OUT)klipper.bin -rplink-$(CONFIG_RP2040_HAVE_BOOTLOADER) := $(OUT)src/generic/armcm_link.ld +rplink-$(CONFIG_RP2040_HAVE_BOOTLOADER) := $(OUT)src/rp2040/rp2040_link.ld # Set klipper.elf linker rules target-y += $(rptarget-y) diff --git a/src/rp2040/main.c b/src/rp2040/main.c index 0b144d0bba0c..e7b64e5f0da2 100644 --- a/src/rp2040/main.c +++ b/src/rp2040/main.c @@ -16,6 +16,26 @@ #include "sched.h" // sched_main +/**************************************************************** + * Ram IRQ vector table + ****************************************************************/ + +// Copy vector table to ram and activate it +static void +enable_ram_vectortable(void) +{ + // Symbols created by rp2040_link.lds.S linker script + extern uint32_t _ram_vectortable_start, _ram_vectortable_end; + extern uint32_t _text_vectortable_start; + + uint32_t count = (&_ram_vectortable_end - &_ram_vectortable_start) * 4; + __builtin_memcpy(&_ram_vectortable_start, &_text_vectortable_start, count); + barrier(); + + SCB->VTOR = (uint32_t)&_ram_vectortable_start; +} + + /**************************************************************** * Bootloader ****************************************************************/ @@ -145,6 +165,7 @@ clock_setup(void) void armcm_main(void) { + enable_ram_vectortable(); clock_setup(); sched_main(); } diff --git a/src/rp2040/rp2040_link.lds.S b/src/rp2040/rp2040_link.lds.S index 43d6115e4983..9b0264a2b941 100644 --- a/src/rp2040/rp2040_link.lds.S +++ b/src/rp2040/rp2040_link.lds.S @@ -1,6 +1,6 @@ // rp2040 linker script (based on armcm_link.lds.S and customized for stage2) // -// Copyright (C) 2019-2021 Kevin O'Connor +// Copyright (C) 2019-2024 Kevin O'Connor // // This file may be distributed under the terms of the GNU GPLv3 license. @@ -9,9 +9,15 @@ OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm") OUTPUT_ARCH(arm) +#if CONFIG_RP2040_HAVE_STAGE2 + #define ROM_ORIGIN 0x10000000 +#else + #define ROM_ORIGIN CONFIG_FLASH_APPLICATION_ADDRESS +#endif + MEMORY { - rom (rx) : ORIGIN = 0x10000000 , LENGTH = CONFIG_FLASH_SIZE + rom (rx) : ORIGIN = ROM_ORIGIN , LENGTH = CONFIG_FLASH_SIZE ram (rwx) : ORIGIN = CONFIG_RAM_START , LENGTH = CONFIG_RAM_SIZE } @@ -19,22 +25,31 @@ SECTIONS { .text : { . = ALIGN(4); +#if CONFIG_RP2040_HAVE_STAGE2 KEEP(*(.boot2)) +#endif _text_vectortable_start = .; KEEP(*(.vector_table)) _text_vectortable_end = .; - *(.text .text.*) - *(.rodata .rodata*) + *(.text.armcm_boot*) } > rom . = ALIGN(4); _data_flash = .; + .ram_vectortable (NOLOAD) : { + _ram_vectortable_start = .; + . = . + ( _text_vectortable_end - _text_vectortable_start ) ; + _ram_vectortable_end = .; + } > ram + .data : AT (_data_flash) { . = ALIGN(4); _data_start = .; + *(.text .text.*) *(.ramfunc .ramfunc.*); + *(.rodata .rodata*) *(.data .data.*); . = ALIGN(4); _data_end = .; diff --git a/src/rp2040/spi.c b/src/rp2040/spi.c index e6aafa0053a4..758d57308b79 100644 --- a/src/rp2040/spi.c +++ b/src/rp2040/spi.c @@ -89,8 +89,12 @@ void spi_prepare(struct spi_config config) { spi_hw_t *spi = config.spi; + if (spi->cr0 == config.cr0 && spi->cpsr == config.cpsr) + return; + spi->cr1 = 0; spi->cr0 = config.cr0; spi->cpsr = config.cpsr; + spi->cr1 = SPI_SSPCR1_SSE_BITS; } void diff --git a/test/klippy/pwm.cfg b/test/klippy/pwm.cfg index fbda912694ac..af5b5b10ec45 100644 --- a/test/klippy/pwm.cfg +++ b/test/klippy/pwm.cfg @@ -5,6 +5,12 @@ value: 0 shutdown_value: 0 cycle_time: 0.01 +[pwm_cycle_time cycle_pwm_pin] +pin: PH7 +value: 0 +shutdown_value: 0 +cycle_time: 0.01 + [output_pin hard_pwm_pin] pin: PH6 pwm: True diff --git a/test/klippy/pwm.test b/test/klippy/pwm.test index 5e74a3e0569e..fdbf42f2acf8 100644 --- a/test/klippy/pwm.test +++ b/test/klippy/pwm.test @@ -16,18 +16,24 @@ SET_PIN PIN=soft_pwm_pin VALUE=0 SET_PIN PIN=soft_pwm_pin VALUE=0.5 SET_PIN PIN=soft_pwm_pin VALUE=1 +# Soft PWM with dynamic cycle time +# Test basic on off +SET_PIN PIN=cycle_pwm_pin VALUE=0 +SET_PIN PIN=cycle_pwm_pin VALUE=0.5 +SET_PIN PIN=cycle_pwm_pin VALUE=1 + # Test cycle time -SET_PIN PIN=soft_pwm_pin VALUE=0 CYCLE_TIME=0.1 -SET_PIN PIN=soft_pwm_pin VALUE=1 CYCLE_TIME=0.5 -SET_PIN PIN=soft_pwm_pin VALUE=0.5 CYCLE_TIME=0.001 -SET_PIN PIN=soft_pwm_pin VALUE=0.75 CYCLE_TIME=0.01 -SET_PIN PIN=soft_pwm_pin VALUE=0.5 CYCLE_TIME=1 +SET_PIN PIN=cycle_pwm_pin VALUE=0 CYCLE_TIME=0.1 +SET_PIN PIN=cycle_pwm_pin VALUE=1 CYCLE_TIME=0.5 +SET_PIN PIN=cycle_pwm_pin VALUE=0.5 CYCLE_TIME=0.001 +SET_PIN PIN=cycle_pwm_pin VALUE=0.75 CYCLE_TIME=0.01 +SET_PIN PIN=cycle_pwm_pin VALUE=0.5 CYCLE_TIME=1 # Test duplicate values -SET_PIN PIN=soft_pwm_pin VALUE=0.5 CYCLE_TIME=0.5 -SET_PIN PIN=soft_pwm_pin VALUE=0.5 CYCLE_TIME=0.5 -SET_PIN PIN=soft_pwm_pin VALUE=0.75 CYCLE_TIME=0.5 -SET_PIN PIN=soft_pwm_pin VALUE=0.75 CYCLE_TIME=0.75 +SET_PIN PIN=cycle_pwm_pin VALUE=0.5 CYCLE_TIME=0.5 +SET_PIN PIN=cycle_pwm_pin VALUE=0.5 CYCLE_TIME=0.5 +SET_PIN PIN=cycle_pwm_pin VALUE=0.75 CYCLE_TIME=0.5 +SET_PIN PIN=cycle_pwm_pin VALUE=0.75 CYCLE_TIME=0.75 # PWM tool # Basic test